kind: reference

MiniCPU

Der Prozessor der CPU Sandbox in drei Stufen: MiniCPU 8, 8S und 8X. Hier stehen seine Register, Flags und alle Befehle mit Opcode, Länge und Takten, dazu der Stack, die Register X und Y, der Maschinencode und der Assembler. Die Tabellen stammen aus denselben Tabellen, mit denen die Sandbox selbst rechnet.

1. Die drei Stufen

Jede Stufe behält alles von der vorigen und fügt eine Idee hinzu. Welche Stufe ein Programm braucht, steht in seiner ersten Zeile: .cpu 8, .cpu 8s oder .cpu 8x (in C #pragma cpu 8x). Ohne diese Zeile läuft ein Assembler-Programm auf der MiniCPU 8, ein C-Programm auf der 8X. Das Menü »CPU« in der Sandbox ändert die Zeile.

MiniCPU 8 MiniCPU 8S MiniCPU 8X
Idee Register, Flags, Sprünge + ein Stack + viel Speicher und Zeiger
RAM 256 Bytes 256 Bytes 65.536 Bytes (64 KiB)
Adressen 1 Byte 1 Byte 2 Bytes
Register A B C D, PC + SP + X Y (16 Bit) mit XL XH YL YH
Flags ZF CF ZF CF ZF CF
Neue Befehle – PUSH POP CALL RET, [SP+n], ADD/SUB SP, n keine: die bekannten Befehle können X und Y
Geräte auf allen Stufen dieselben, über IN und OUT (Geräte)

2. Vier Regeln für alle Befehle

Auf allen drei Stufen gelten dieselben Regeln. Wer eine Stufe weitergeht, muss nichts umlernen.

  • Das Ziel steht vorn: LDI A, 5 lädt 5 in A, ADD A, B ändert A, STA [0x70], A schreibt nach 0x70.
  • Eckige Klammern heißen »der Speicher an«. LDI A, 0x70 lädt die Zahl 0x70, LDA A, [0x70] das Byte, das an Adresse 0x70 steht, und LDA A, [B] das Byte an der Adresse, die in B steht.
  • Der zweite Wert ist ein Register oder eine Zahl, bei allen Rechen-, Logik- und Vergleichsbefehlen: ADD A, B und ADD A, 16, AND A, 0x0F, CMP A, 10.
  • Höhere Stufen bringen Register, keine neuen Schreibweisen. Auf der 8X funktionieren LDI, LDA, STA, MOV, ADD, SUB, CMP, INC, DEC, PUSH und POP auch mit X und Y.

Ob eine Zahl dezimal (112), hexadezimal (0x70) oder binär (0b0111_0000) dasteht, ist gleich; mehr dazu unter Assembler.

3. MiniCPU 8

Speicher und Register

  • Ein RAM mit 256 Bytes, Adressen 0x00 bis 0xFF. Programm, Daten und Bildschirm teilen sich diesen Speicher; nichts hindert ein Programm daran, seine eigenen Befehle zu überschreiben.
  • Vier Register A, B, C, D mit je 8 Bit, also Werten von 0 bis 255. Mehr passt nicht hinein: 255 + 1 ergibt 0, und das Flag CF merkt sich den Übertrag.
  • Der Programmzähler PC enthält die Adresse des nächsten Befehls. Die CPU beginnt bei 0x00 und läuft, bis sie auf HLT trifft.
  • Zahlen über 255 belegen zwei Bytes. ADC und SBC rechnen den Übertrag des Befehls davor mit, so addiert ADD B, D und danach ADC C, 0 den Wert D zur 16-Bit-Zahl in C (oben) und B (unten).

Beim Start stehen A bis D und die Flags auf 0.

Flags

Zwei Flags halten fest, wie der letzte Rechenbefehl ausgegangen ist. Die Sprungbefehle entscheiden danach.

Flag ist 1, wenn …
ZF (Zero) das Ergebnis 0 ist. Nach CMP: beide Werte gleich waren.
CF (Carry) es einen Übertrag aus Bit 7 gab (ADD, ADC, INC, SHL), etwas geborgt werden musste, weil der erste Wert kleiner war (SUB, SBC, DEC, CMP), oder SHR eine 1 hinausgeschoben hat.

Ein Beispiel: 3 − 5 passt nicht in ein Byte.

        LDI A, 3
        LDI B, 5
        SUB A, B            ; 3 − 5
        HLT

Die Register nach SUB A, B: A ist FE, also 254, und CF ist 1; beide sind hervorgehoben.

Nach SUB A, B steht in A 254, also 3 − 5 + 256, und CF = 1 sagt: Es musste geborgt werden. Die Sandbox hebt hervor, was der letzte Befehl geändert hat.

AND, OR und XOR setzen ZF nach ihrem Ergebnis und löschen CF immer; CLF löscht beide. Laden, Speichern, MOV, Sprünge, der Stack und die Geräte lassen die Flags, wie sie sind. In der Befehlstabelle steht , wo ein Befehl das Flag nach seinem Ergebnis setzt, 0, wo er es löscht, und –, wo er es nicht verändert.

Vergleichen. CMP A, B rechnet A − B, behält aber nur die Flags. Danach gilt (für Zahlen von 0 bis 255):

Bedingung Flags Sprung
A = B ZF = 1 JZ
A ≠ B ZF = 0 JNZ
A < B CF = 1 JC
A ≥ B CF = 0 JNC
A > B CF = 0 und ZF = 0 andersherum vergleichen: CMP B, A, dann JC

In der Sandbox erklärt die Maus über ZF und CF, warum der letzte Befehl das Flag gesetzt oder gelöscht hat.

Befehle

Alle Befehle der MiniCPU 8. Die Beispiele stehen für alle Register: Statt ADD A, B geht auch ADD C, D, statt 5 jede Zahl von 0 bis 255. Bei bedingten Sprüngen stehen zwei Taktzahlen: ohne Sprung / mit Sprung.

Befehl Wirkung ZF CF Bytes Takte Opcode
Steuerung
NOP Tut nichts. – – 1 1 0x00
HLT Hält die CPU an. – – 1 1 0x01
CLF Löscht ZF und CF. 0 0 1 1 0x02
Daten bewegen
LDI A, 42 Lädt die Zahl 42 in A. – – 3 3 0x10
LDA A, [0x70] Lädt das Byte an Adresse 0x70 in A. – – 3 4 0x11
LDA A, [B] Lädt das Byte an der Adresse, die in B steht. – – 2 3 0x12
STA [0x70], A Speichert A an Adresse 0x70. – – 3 4 0x13
STA [B], A Speichert A an der Adresse, die in B steht. – – 2 3 0x14
MOV A, B Kopiert B nach A. – – 2 2 0x15
Rechnen
ADD A, B A = A + B. CF = Übertrag aus dem obersten Bit. 2 2 0x20
ADD A, 5 A = A + 5. 3 3 0x21
SUB A, B A = A − B. CF = geborgt (A war kleiner). 2 2 0x22
SUB A, 5 A = A − 5. 3 3 0x23
ADC A, B A = A + B + CF: addiert den Übertrag eines ADD davor. 2 2 0x24
ADC A, 0 A = A + 0 + CF. 3 3 0x25
SBC A, B A = A − B − CF: zieht das Geborgte eines SUB davor ab. 2 2 0x26
SBC A, 0 A = A − 0 − CF. 3 3 0x27
INC A A = A + 1. 2 2 0x28
DEC A A = A − 1. 2 2 0x29
SHL A Schiebt nach links; das oberste Bit kommt nach CF. 2 2 0x2A
SHR A Schiebt nach rechts; Bit 0 kommt nach CF. 2 2 0x2B
Logik
AND A, B Bitweises UND. 0 2 2 0x30
AND A, 0x0F Bitweises UND mit einer Zahl. 0 3 3 0x31
OR A, B Bitweises ODER. 0 2 2 0x32
OR A, 0x80 Bitweises ODER mit einer Zahl. 0 3 3 0x33
XOR A, B Bitweises exklusives ODER. 0 2 2 0x34
XOR A, 0xFF Bitweises exklusives ODER mit einer Zahl. 0 3 3 0x35
Vergleichen
CMP A, B Rechnet A − B, nur für die Flags. 2 2 0x40
CMP A, 10 Rechnet A − 10, nur für die Flags. 3 3 0x41
Springen
JMP label Springt immer. – – 2 3 0x60
JZ label Springt, wenn ZF = 1 (null, nach CMP: gleich). – – 2 2 / 3 0x61
JNZ label Springt, wenn ZF = 0. – – 2 2 / 3 0x62
JC label Springt, wenn CF = 1 (Übertrag, nach CMP: kleiner). – – 2 2 / 3 0x63
JNC label Springt, wenn CF = 0. – – 2 2 / 3 0x64
Ein- und Ausgabe
IN A, 0x02 Liest ein Byte vom Gerät an Port 2. – – 3 4 0x70
OUT 0x11, A Schickt A an das Gerät an Port 0x11. – – 3 4 0x71

4. MiniCPU 8S

Der Stack

  • SP, der Stackzeiger, beginnt bei 0x100, eins hinter dem Ende des RAMs. PUSH A zieht 1 von SP ab und speichert A dort; POP A liest das Byte und zählt SP wieder hoch. Der Stack wächst also durch das gewöhnliche RAM nach unten.
  • CALL label legt die Adresse des nächsten Befehls auf den Stack und springt; RET holt sie wieder herunter und macht dort weiter. Aufrufe dürfen verschachtelt sein.
  • LDA A, [SP+n] und STA [SP+n], A lesen und schreiben n Bytes über SP, ohne etwas vom Stack zu nehmen. SUB SP, n macht Platz für n Bytes, ADD SP, n gibt sie wieder frei.
  • Stack, Programm und Bildschirm teilen sich weiterhin 256 Bytes: Zu viele PUSHes überschreiben, was unter dem Stack liegt. Die RAM-Ansicht der Sandbox markiert den Stack.

Befehle für den Stack

Diese Befehle kommen auf der 8S zu allen Befehlen der MiniCPU 8 hinzu.

Befehl Wirkung ZF CF Bytes Takte Opcode
Stack
PUSH A SP = SP − 1, dann A nach [SP]. – – 2 3 0x50
POP A A aus [SP], dann SP = SP + 1. – – 2 3 0x51
CALL label Legt die Rücksprungadresse auf den Stack und springt. – – 2 4 0x52
RET Nimmt die Rücksprungadresse vom Stack und springt dorthin. – – 1 3 0x53
LDA A, [SP+2] Lädt das Byte 2 Plätze über SP: ein Argument oder eine lokale Variable. – – 3 4 0x54
STA [SP+2], A Speichert A 2 Plätze über SP. – – 3 4 0x55
ADD SP, 3 Gibt 3 Bytes auf dem Stack frei (lokale Variablen, oder Argumente nach dem Aufruf). – – 2 3 0x56
SUB SP, 3 Macht auf dem Stack Platz für 3 Bytes lokale Variablen. – – 2 3 0x57

Funktionen mit Argumenten

So rufen sich Funktionen gegenseitig auf; der C-Compiler macht es bei jeder Funktion genauso:

  1. Der Aufrufer legt die Argumente mit PUSH auf den Stack und ruft die Funktion mit CALL auf.
  2. Die Funktion macht mit SUB SP, n Platz für ihre lokalen Variablen.
  3. Sie liest und schreibt lokale Variablen und Argumente mit [SP+n]: zuerst kommen die lokalen Variablen, dann die Rücksprungadresse, dann die Argumente.
  4. Sie gibt ihre lokalen Variablen mit ADD SP, n frei und kehrt mit RET zurück, das Ergebnis in A.
  5. Der Aufrufer gibt die Argumente mit ADD SP, n frei.
.cpu 8s
        LDI A, 21
        PUSH A              ; das Argument
        CALL double
        ADD SP, 1           ; Argument wieder freigeben
        OUT 0x11, A         ; zeigt 42
        HLT

double: LDA A, [SP+1]       ; [SP+0] ist die Rücksprungadresse
        ADD A, A
        RET

Die RAM-Ansicht nach CALL double: oben bei 0xFE und 0xFF liegen 07 und 15, als Stack markiert.

Nach CALL double liegt ganz oben im RAM, an 0xFF, das Argument 21 (15) und darunter, an 0xFE, die Rücksprungadresse 07: Dort steht ADD SP, 1. Die RAM-Ansicht markiert den Stack.

Jeder Aufruf hat seine eigenen Argumente und lokalen Variablen, deshalb darf eine Funktion sich selbst aufrufen. Einen eigenen Rahmenzeiger gibt es nicht, alle Abstände zählen ab SP. Jedes PUSH mitten in einer Funktion verschiebt sie deshalb um eins. Am einfachsten reserviert eine Funktion alle lokalen Variablen gleich am Anfang. Auf der 8X ist die Rücksprungadresse zwei Bytes lang, die Argumente beginnen dort ein Byte höher.

5. MiniCPU 8X

64 KiB und Adressen mit zwei Bytes

  • 65.536 Bytes RAM, Adressen 0x0000 bis 0xFFFF: alles, was eine 16-Bit-Adresse erreicht, so wie 256 Bytes alles sind, was 8 Bit erreichen. Platz für Tabellen, große Bildschirme mit zwei Seiten und längere Programme.
  • Jede Adresse hat zwei Bytes, das untere zuerst. LDA A, [0x0812], STA [0x0812], A, Sprünge und CALL werden dadurch ein Byte länger und einen Takt langsamer.
  • PC und SP haben 16 Bit. SP beginnt bei 0x10000, ganz oben. X und Y stehen beim Start auf 0. CALL legt eine Rücksprungadresse mit zwei Bytes ab.
  • LDA A, [B] gibt es weiter, aber B erreicht nur die ersten 256 Bytes. Für alles darüber sind X und Y da.

X und Y

X und Y sind zwei Register mit 16 Bit, gedacht als Zeiger. Sie brauchen keine neuen Befehle:

Befehl Wirkung
LDI X, 0x0800 lädt eine 16-Bit-Zahl
LDA X, [0x0700] und STA [0x0700], X lädt und speichert zwei Bytes, das untere zuerst
LDA X, [SP+2] und STA [SP+2], X dasselbe auf dem Stack
MOV X, Y kopiert
ADD X, 32, ADD X, A oder ADD X, Y addiert eine Zahl, ein Byte (0–255) oder ein 16-Bit-Register; SUB ebenso
CMP X, 0x0900 oder CMP X, Y vergleicht alle 16 Bit
INC X und DEC X zum nächsten oder vorigen Byte
PUSH X und POP X zwei Bytes auf den Stack und zurück
LDA A, [X] und STA [X], A X als Zeiger: das Byte an der Adresse, die in X steht

Bei X und Y beziehen sich ZF und CF auf alle 16 Bit. Schieben, Logik, ADC und SBC gibt es nur für A bis D, und ein 8-Bit-Register kann kein 16-Bit-Register aufnehmen: ADD A, X geht nicht.

XL, XH, YL, YH sind das untere und das obere Byte von X und Y. Sie funktionieren nur mit MOV, in beide Richtungen zwischen ihnen und A bis D: MOV XH, A, MOV B, YL.

Was die 8X kostet

Breiter ist langsamer: Eine Adresse hat zwei Bytes, eine Zahl für X oder Y auch, Laden und Speichern bewegen bei X und Y zwei Bytes, und die 8-Bit-Recheneinheit braucht für 16-Bit-Rechnungen einen Takt mehr. Die Tabelle nennt Bytes und Takte für jeden Befehl, der auf der 8X anders kostet oder X und Y kann; alle anderen kosten so viel wie auf der 8.

Befehl 8 / 8S 8X 8X mit X oder Y
LDI A, 42 3 Bytes, 3 Takte 3 Bytes, 3 Takte 4 Bytes, 5 Takte
LDA A, [0x70] 3 Bytes, 4 Takte 4 Bytes, 5 Takte 4 Bytes, 6 Takte
STA [0x70], A 3 Bytes, 4 Takte 4 Bytes, 5 Takte 4 Bytes, 6 Takte
MOV A, B 2 Bytes, 2 Takte 2 Bytes, 2 Takte 2 Bytes, 3 Takte
ADD A, B 2 Bytes, 2 Takte 2 Bytes, 2 Takte 2 Bytes, 3 Takte
ADD A, 5 3 Bytes, 3 Takte 3 Bytes, 3 Takte 4 Bytes, 5 Takte
SUB A, B 2 Bytes, 2 Takte 2 Bytes, 2 Takte 2 Bytes, 3 Takte
SUB A, 5 3 Bytes, 3 Takte 3 Bytes, 3 Takte 4 Bytes, 5 Takte
INC A 2 Bytes, 2 Takte 2 Bytes, 2 Takte 2 Bytes, 3 Takte
DEC A 2 Bytes, 2 Takte 2 Bytes, 2 Takte 2 Bytes, 3 Takte
CMP A, B 2 Bytes, 2 Takte 2 Bytes, 2 Takte 2 Bytes, 3 Takte
CMP A, 10 3 Bytes, 3 Takte 3 Bytes, 3 Takte 4 Bytes, 5 Takte
PUSH A 2 Bytes, 3 Takte 2 Bytes, 3 Takte 2 Bytes, 4 Takte
POP A 2 Bytes, 3 Takte 2 Bytes, 3 Takte 2 Bytes, 4 Takte
CALL label 2 Bytes, 4 Takte 3 Bytes, 6 Takte –
RET 1 Bytes, 3 Takte 1 Bytes, 4 Takte –
LDA A, [SP+2] 3 Bytes, 4 Takte 3 Bytes, 4 Takte 3 Bytes, 5 Takte
STA [SP+2], A 3 Bytes, 4 Takte 3 Bytes, 4 Takte 3 Bytes, 5 Takte
JMP label 2 Bytes, 3 Takte 3 Bytes, 4 Takte –
JZ label 2 Bytes, 2 / 3 Takte 3 Bytes, 3 / 4 Takte –
JNZ label 2 Bytes, 2 / 3 Takte 3 Bytes, 3 / 4 Takte –
JC label 2 Bytes, 2 / 3 Takte 3 Bytes, 3 / 4 Takte –
JNC label 2 Bytes, 2 / 3 Takte 3 Bytes, 3 / 4 Takte –

6. Takte

Der Regler »Clock« stellt den Takt von 1 Hz bis 64 MHz ein, und die CPU läuft in diesem Takt in Echtzeit. Wie viele Takte ein Befehl braucht, folgt aus einem einfachen Modell einer 8-Bit-CPU, deren Tempo der Speicherbus begrenzt:

  • 1 Takt für jedes Byte des Befehls, das geholt wird,
  • 1 Takt für jedes Datenbyte, das gelesen oder geschrieben wird (RAM, Stack, Rücksprungadresse, Port), 2 bei X und Y,
  • 1 Takt, wenn PC einen neuen Wert bekommt (JMP, ein bedingter Sprung, der springt, CALL, RET),
  • 1 Takt für eine 16-Bit-Rechnung (mit X, Y oder SP), weil die Recheneinheit nur 8 Bit breit ist.

MiniCPU im Taktmodus: Holphase von LDI B, 0 mit Adress- und Datenbus, daneben Quelltext, Register und RAM.

So braucht ADD A, B 2 Takte, LDI A, 5 3, LDA A, [0x70] 4 (auf der 8X 5) und JNZ loop 2, wenn es nicht springt, und 3, wenn es springt. Die Statuszeile der Sandbox zeigt die verbrauchten Takte und wie lange sie beim gewählten Takt dauern; »Cycles« zeigt einen Befehl Takt für Takt.

7. Maschinencode

Neben dem Code zeigt die Sandbox die Adresse und die Bytes jeder Zeile:

loop:   LDI A, 5
        ADD A, B
        MOV B, A
        JMP loop

Das Programm im Editor der Sandbox; links neben den vier Zeilen 00 10 00 05, 03 20 01, 05 15 10 und 07 60 00.

So entstehen sie:

  • Der Opcode ist das erste Byte. Seine erste Hex-Ziffer nennt die Familie: 0x Steuerung, 1x Daten bewegen, 2x Rechnen, 3x Logik, 4x Vergleichen, 5x Stack, 6x Springen, 7x Ein- und Ausgabe. Höhere Stufen fügen Opcodes hinzu, nummerieren aber keine um.
  • Ein Register ist ein Byte: A = 0, B = 1, C = 2, D = 3; auf der 8X auch XL = 4, XH = 5, YL = 6, YH = 7, X = 8, Y = 9.
  • Zwei Register teilen sich ein Byte: erstes × 16 + zweites. ADD A, B ist 20 01, MOV B, A ist 15 10.
  • Eine Zahl folgt dem Registerbyte: ein Byte, bei X und Y zwei (das untere zuerst). LDI A, 5 ist 10 00 05, LDI X, 0x0800 ist 10 08 00 08.
  • Eine Adresse hat ein Byte (8, 8S) oder zwei, das untere zuerst (8X): JMP 0x23 ist auf der 8 60 23, auf der 8X 60 23 00.

Ein Byte, das kein Befehl ist, führt zu einer Fehlermeldung, ebenso ein Befehl, den die Stufe noch nicht kennt.

8. Assembler

Eine Zeile enthält höchstens einen Befehl. Groß- und Kleinschreibung der Befehle ist egal, Kommentare beginnen mit ;.

Schreibweise Bedeutung
loop: Eine Marke ist die Adresse des Bytes, das danach kommt. Sie beginnt mit einem Buchstaben oder _; ein Befehl darf in derselben Zeile folgen.
.cpu 8x wählt die Stufe: 8, 8s oder 8x. Ohne diese Zeile die MiniCPU 8.
.org 0x60 legt die folgenden Bytes ab dieser Adresse ab.
.db 1, 2, "Hi", 0 schreibt Bytes ins RAM: Zahlen, Zeichen oder Texte.
WIDTH .equ 16 eine Konstante mit Namen.
.display 0x80, 16, 8, gray8 schlägt vor, wie der Bildschirm das RAM zeigt (Bildschirm).

Werte schreibst du dezimal 42, hexadezimal 0x2A, binär 0b0010_1010 oder als Zeichen 'A' (65). Marken und Konstanten lassen sich mit + - * / & | << >> und Klammern verrechnen, etwa screen + 16 * 3 oder table >> 8. Negative Bytes laufen über: -1 ist 255.

Die Sandbox übersetzt schon beim Tippen. Fehler erscheinen unter dem Editor, ein Klick springt in die Zeile. Die Meldungen erklären mit diesen Regeln, was gemeint war: Auf LDA A, 0x70 antwortet sie, dass LDA den Speicher liest, und schlägt LDA A, [0x70] oder LDI A, 0x70 vor.

9. Zum Nachschlagen

Die MiniCPU gibt es nur in der CPU Sandbox; diese Seite ist ihre Referenz. Im Repository der Sandbox steht dazu:

  • CPU Sandbox: die Anleitung, auf Deutsch: Sandbox und Klassen-Repository klonen, starten, Aufgaben bearbeiten und veröffentlichen, und was bei häufigen Problemen hilft.
  • The three MiniCPU levels: die drei Stufen, Takte und Maschinencode noch einmal im Zusammenhang, auf Englisch.
  • Beispielprogramme: fertige Programme in Assembler und C, von der Addition bis zum Raycaster, zum Lesen und Abwandeln.