Ein print-befehl in micro python schritt für schritt bis auf die maschinensprache herunter immer quasi in eine neue niedrigere sprache konvertiert wird.
Wir verfolgen einen einfachen MicroPython-Befehl, z. B.:
print("Hallo Welt")
und analysieren schrittweise, wie dieser Befehl aus der Hochsprache über verschiedene Abstraktionsebenen bis zur Maschinensprache umgesetzt wird.
print("Hallo Welt")
Das ist ein Benutzerbefehl auf hohem Abstraktionsniveau. print() ist eine vordefinierte Funktion in MicroPython, die auf ein Terminal oder eine serielle Konsole ausgibt.
MicroPython ist ein Interpreter, der Python-Code nicht direkt in Maschinensprache, sondern zuerst in einen kompakten Bytecode übersetzt.
Beispielhafter Bytecode für print("Hallo Welt"):
LOAD_NAME 0 (print)
LOAD_CONST 1 ('Hallo Welt')
CALL_FUNCTION 1
Das bedeutet:
Dieser Bytecode wird nicht kompiliert, sondern vom sogenannten MicroPython VM (Virtual Machine Interpreter) interpretiert.
Die VM ist in C geschrieben, z. B. auf dem Raspberry Pi Pico. Der Interpreter läuft Schleifen wie diese:
switch(opcode) {
case LOAD_NAME:
// Suche Symbol in Symboltabelle und pushe es auf den Stack
break;
case LOAD_CONST:
// Lade Konstantenwert und pushe ihn
break;
case CALL_FUNCTION:
// Hole Funktion und Argumente vom Stack, rufe C-Funktion auf
break;
}
Das heißt: Der C-Code in der Firmware des Pi Pico "versteht" die Bytecodes und ruft dann je nach Code die passende C-Funktion auf.
Die print()-Funktion in MicroPython ist mit der internen C-Funktion mp_builtin_print() verbunden. Der Aufruf landet in etwa hier:
mp_obj_t mp_builtin_print(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs) {
// Konvertiere Argumente zu Zeichenketten
// Führe low-level Ausgabe auf stdout (UART etc.) durch
}
Diese Funktion verwendet typischerweise mp_hal_stdout_tx_str(), um tatsächlich Zeichen an die serielle Schnittstelle zu senden.
Die Ausgabe geht z. B. über UART0 beim Pico. Der Code könnte so aussehen:
void uart_tx_char(char c) {
while (!(UART0->FR & UART_FR_TXFF)) {
// Warte bis TX FIFO voll ist
}
UART0->DR = c;
}
Dieser C-Code schreibt ein einzelnes Zeichen in das Datenregister des UART-Peripherieblocks.
Der obige C-Code wird beim Kompilieren in ARM-Maschinencode (z. B. Thumb-Befehlssatz auf dem RP2040) übersetzt. Beispiel (vereinfacht):
LDR r1, =UART0_DR
STRB r0, [r1] ; Schreibe Zeichen r0 an Adresse UART0_DR
Dies ist die tatsächliche Maschinensprache – sie läuft auf dem Mikrocontroller-Prozessor.
1. Hochsprache | print("Hallo Welt") in MicroPython |
2. Bytecode | LOAD_NAME, LOAD_CONST, CALL_FUNCTION |
3. Interpreter (VM) | C-Funktionen in MicroPython-VM |
4. System-API | mp_builtin_print() → mp_hal_stdout_tx_str() |
5. Treiber | uart_tx_char() schreibt ins Register |
6. Maschinencode | STRB r0, [r1] → ARM-Instruktionen |
7. Elektrisches Signal | UART-Ausgabe an TX-Pin |
Hier ist das Schaubild im PNG-Format.
Um den Befehl printf("Hallo Welt\n"); bzw. dessen Funktionalität in Assembler direkt auf dem Raspberry Pi Pico (RP2040) auszuführen, muss man den Textausgabeprozess selbst übernehmen, da es kein printf in Assembler gibt.
Ich zeige wie man in ARM Thumb-Assembler eine Zeichenkette per UART0 ausgeben kann – also funktional dasselbe tut wie printf("Hallo Welt\n").
.syntax unified
.cpu cortex-m0plus
.thumb
.global _start
_start:
ldr r0, =hello @ Adresse der Zeichenkette laden
next_char:
ldrb r1, [r0], #1 @ Lade nächstes Zeichen, erhöhe Adresse
cmp r1, #0 @ Nullterminierung erreicht?
beq done
wait_uart:
ldr r2, =0x40034018 @ UART0 FR (Flag Register)
ldr r3, [r2]
tst r3, #0x20 @ TXFF (Transmit FIFO Full)?
bne wait_uart @ Falls voll: warten
send_uart:
ldr r2, =0x40034000 @ UART0 DR (Datenregister)
str r1, [r2] @ Zeichen senden
b next_char
done:
b done @ Endlosschleife
.data
hello:
.asciz "Hallo Welt\n"
.asciz | Nullterminierte Zeichenkette |
ldr r0, =hello | Lade Basisadresse der Zeichenkette |
ldrb r1, [r0], #1 | Lade ein Byte, erhöhe Adresse um 1 |
cmp r1, #0 | Vergleiche mit Null (Ende?) |
tst r3, #0x20 | Teste ob TX FIFO voll (Bit 5 = 1) |
str r1, [r2] | Schreibe Zeichen an UART |
Befehle in Microcode
prinzipieller Ablauf eines printf("Hallo Welt")-Befehls in Microcode-artiger Darstellung zeigen, wie sie in einem klassischen Prozessor mit Mikrobefehlseinheit (Microsequencer) vorkommen würde.
⚠️ Der RP2040 (Raspberry Pi Pico) hat einen ARM Cortex-M0+, der kein echtes Microcode-ROM verwendet wie z. B. Intel CISC-CPUs. Stattdessen wird alles direkt im Hardwired Control Logic ausgeführt. Dennoch ist es möglich, das Verhalten auf einer abstrakten Mikrocode-Ebene zu modellieren, wie es in klassischen Lehrwerken gezeigt wird.
Der Maschinencode STRB r1, [r2] ist zur UART-Ausgabe geladen. Die Register sind vorbereitet:
T1 | Instruktions-Fetch | MAR ← PC; IR ← MEM[MAR]; PC ← PC+2 |
T2 | Decode + Address Prep | Decode IR; EA ← r2 |
T3 | Daten vorbereiten | MDR ← r1 |
T4 | Schreibzyklus | MEM[EA] ← MDR |
T5 | Warte auf UART-RDY (impl.) | Wait UART0->FR & TXFF == 0 |
➡️ In einem echten ARM Cortex-M0+ ist dies in festen Zustandsautomaten verschaltet, aber funktional entspricht das den Mikrooperationen.
T1: IR ← MEM[PC]; PC ← PC+2
T2: DECODE(IR) → [OP=STRB, R1=r1, R2=r2]
T3: MAR ← R2; MDR ← R1
T4: MEM[MAR] ← MDR
T5: GOTO FETCH
Diese Schritte wären auf einem klassischen Mikroprogrammierten Steuerwerk in ROM oder PLA gespeichert und durch einen Mikroadresszähler sequenziert.
Wenn z. B. der Befehl uart_send(c) implementierst ist, sähe der Ablauf so aus:
WAIT_TX_READY:
BUS ← UART_FR
IF (BUS & 0x20) ≠ 0 THEN GOTO WAIT_TX_READY
SEND:
BUS ← R1
UART_DR ← BUS
GOTO NEXT
💡 Mit handoptimierten C-Loops + DMA kann man teils Assembler schlagen in Realwelt-Szenarien.
✅ In manchen Fällen sogar schneller als C, aber noch nicht getestet.
Zig | ~C-ähnlich bei guter Optimierung | Einfachere Syntax, gute LLVM-Ausbeute |
Nim | 1.2–1.5× langsamer als C | Weniger verbreitet |
TinyGo | 3–5× langsamer als C | Für einfache Projekte, keine Float-Optimierung |
CircuitPython / MicroPython | 30–150× langsamer | Für Prototyping, aber nicht für Speed |
Assembler | 🥇 Schnellste | Nur sinnvoll bei Benchmarks |
C (mit Optimierung) | 🥈 Praktisch optimal | + einfach debugbar, produktiv |
Rust / Zig | 🥉 extrem nah an C | ggf. schneller durch Inliner |
TinyGo | ⚠ langsam | aber kleiner Binary-Overhead |
Python | 🐢 sehr langsam | Interpreter |