Teil 1 endete mit einer ALU, die rechnet, Teil 2 mit Registern, die sich etwas merken, und Automaten, die Schritt für Schritt eine Folge abarbeiten. Mehr steckt in einem Prozessor nicht. Dieser Teil setzt die Stücke zu einem kleinen Prozessor für einen Ausschnitt von RISC-V zusammen, einem echten und offenen Befehlssatz. Unterwegs machst du aus Bits Befehle, schreibst deine ersten Programme in Assembler und siehst dem Prozessor zu, wie er sie ausführt, Leitung für Leitung.

Vom Automaten zum Prozessor

Auch ein Prozessor ist ein Automat. Sein Zustand ist der Befehlszähler (englisch program counter, PC), der die Adresse des nächsten Befehls hält, dazu die Register und der Speicher. Was ihn von einer Ampel unterscheidet, ist die Herkunft seines nächsten Schritts: Die Befehle liegen als Zahlen im Speicher, genau wie Daten. Patterson und Hennessy nennen diese beiden Prinzipien, Befehle als Zahlen und Programme wie Daten im Speicher, als Grundlage des speicherprogrammierten Rechners [1].

Welche Befehle ein Prozessor versteht, wie sie codiert sind und was sie tun, legt seine Befehlssatzarchitektur (englisch instruction set architecture, ISA) fest. Hennessy und Patterson beschreiben die ISA als Grenze zwischen Software und Hardware [2]: Ein Programm dafür läuft auf jedem Prozessor, der sie umsetzt, egal wie er innen gebaut ist. Dieser Artikel verwendet RISC-V, dessen Spezifikation offen und frei lesbar ist [3]. Sein Basis-Befehlssatz für 32 Bit, RV32I, hat 32 Register mit je 32 Bit, x0 bis x31, und x0 liest sich immer als 0 [3]. Daraus nehmen wir zehn Befehle: add, sub, and, or, addi, lw, sw, beq, bne und blt. Patterson und Hennessy bauen ihren einfachen Prozessor für fast dieselbe Auswahl, in der 64-Bit-Variante mit den Lade- und Speicherbefehlen ld und sd [1].

Ein Befehl ist eine Zahl

Jeder RISC-V-Befehl ist 32 Bit lang und in Felder unterteilt [3]. Der Opcode in den untersten sieben Bits sagt, welche Art von Befehl es ist. Andere Felder nennen Register oder enthalten einen Direktwert (englisch immediate): eine Konstante, die Teil des Befehls selbst ist. Unser Ausschnitt nutzt vier Formate: den R-Typ für Operationen auf zwei Registern, den I-Typ für Operationen mit Direktwert und für Ladebefehle, den S-Typ für Speicherbefehle und den B-Typ für Sprünge. Die Quellregister rs1 und rs2 und das Zielregister rd stehen in jedem Format in denselben Bits, das hält das Decodieren einfach [3].

Klick dich durch die Bits eines Befehls. Immer wenn das Muster einen Befehl ergibt, den der Prozessor versteht, zeigt ihn die Zeile darunter als Text. Diese Zeile ist Assembler.

Probier es aus

Ein Befehl ist 32 Bit

funct7
rs2x2
rs1x1
funct3
rdx3
opcodeR-Typ
add x3, x1, x20x002081B3

R-Typ: add nimmt zwei Register, x1 und x2, und schreibt das Ergebnis nach x3. Der Opcode sagt „Registeroperation“; funct3 und funct7 sagen, welche.

Aufgaben: 0 von 4

  • Mach aus add mit einem einzigen Klick sub. (Schau auf funct7.)
  • Lass ein add, addi oder lw sein Ergebnis nach x10 schreiben.
  • Bau ein addi, das −1 addiert.
  • Bau einen Sprung, der drei Befehle zurückspringt (−12 Bytes).

Formate und Codierungen wie in Kapitel 1 der RISC-V-Spezifikation; siehe die Quellen.

Assembler: Namen für die Bits

Niemand schreibt Programme als Reihen von Bits. Die Assemblersprache gibt jedem Befehl einen kurzen Namen und schreibt seine Felder als Operanden: add x3, x1, x2 bedeutet x3 = x1 + x2, das Ziel zuerst. Ein Assembler übersetzt jede Zeile in ihre 32 Bits: eine Zeile, ein Befehl.

Neben den zehn Befehlen kennt unser Assembler zwei Pseudobefehle aus der Spezifikation: nop, das ist addi x0, x0, 0 und ändert nichts, und mv rd, rs, das ist addi rd, rs, 0 und kopiert ein Register [3]. Eine Konstante lädt man, indem man sie zu x0 addiert: addi x5, x0, 10 schreibt 10 nach x5. Direktwerte haben 12 Bit und reichen von −2048 bis 2047, im Zweierkomplement wie in Teil 1. Eine Marke wie loop: benennt die Adresse des nächsten Befehls; bne x5, x0, loop springt dorthin, wenn x5 nicht 0 ist, und der Assembler rechnet den Abstand in Bytes aus. Der Sprungbefehl addiert ihn zu seiner eigenen Adresse, innerhalb von ±4 KiB [3].

Der Speicher wird in Bytes adressiert, seine Wörter zu vier Bytes liegen also an den Adressen 0, 4, 8 und so weiter. lw x8, 0(x5) lädt das Wort an Adresse x5 + 0, sw x8, 32(x0) speichert x8 an Adresse 32 [3]. Der Datenspeicher hier fasst sechzehn Wörter; die ersten fünf sind 2, 3, 5, 7 und 11.

Mehr Assembler braucht dieser Artikel nicht. Echte Programme rufen außerdem Funktionen auf, legen Daten auf einem Stapel ab und sprechen mit dem Betriebssystem, aber für einen Prozessor, der Schleifen ausführt, reichen zehn Befehle. Geh die Beispiele Schritt für Schritt durch und löse dann die Aufgaben mit eigenem Code.

Probier es aus

Dein erstes Programm

Maschinencode

AdresseMaschinencodeBefehl
000500293addi x5, x0, 5
400700313addi x6, x0, 7
8006283B3add x7, x5, x6

PC

0

Register

x0
0
x1
0
x2
0
x3
0
x4
0
x5
0
x6
0
x7
0
x8
0
x9
0
x10
0
x11
0
x12
0
x13
0
x14
0
x15
0
x16
0
x17
0
x18
0
x19
0
x20
0
x21
0
x22
0
x23
0
x24
0
x25
0
x26
0
x27
0
x28
0
x29
0
x30
0
x31
0

Datenspeicher (Adresse: Wort)

0
2
4
3
8
5
12
7
16
11
20
0
24
0
28
0
32
0
36
0
40
0
44
0
48
0
52
0
56
0
60
0

Mehr sieht man beim Programmieren nicht vom Prozessor: den PC, die 32 Register und den Speicher. Geh ein Programm Schritt für Schritt durch und beobachte den PC: Nach jedem Befehl rückt er um 4 weiter, außer ein Sprung wird ausgeführt.

Aufgaben: 0 von 3

  • Ändere „Summe 1 bis 10“ so, dass am Ende x6 = 1 + 2 + … + 20 = 210 ist.
  • Eine Multiplikation gibt es hier nicht: Berechne x7 = 6 × 7 mit einer Schleife.
  • Kopiere die ersten drei Wörter des Speichers (2, 3, 5) an die Adressen 32, 36 und 40.

Der Registersatz

Im Inneren braucht der Prozessor ein Zuhause für seine 32 Register: den Registersatz (englisch register file). Ein Befehl wie add liest zwei Register und schreibt eines, also hat der Registersatz zwei Leseports und einen Schreibport [1]. Lesen ist kombinatorisch: Für jeden Leseport wählt ein Multiplexer wie in Teil 1 eines der Register aus. Schreiben geht über den Takt: Ein Decoder wählt das Register rd, und nur wenn das Steuersignal RegWrite 1 ist, übernimmt es den neuen Wert an der Taktflanke, wie die Register in Teil 2 [1].

Probier es aus

Der Registersatz: zwei lesen, eins schreiben

DecoderRegWrite 1rd x3Schreibdaten 17x00x15x212x30x47x50x60x730123456701234567rs1 x1rs2 x2Lesedaten 15Lesedaten 212

Zwei Multiplexer wie in Teil 1 wählen die Register zum Lesen. Sie sind kombinatorisch und antworten sofort. Der Decoder gibt das eine Register zum Schreiben frei, und wie jedes Register in Teil 2 ändert es sich nur an der Taktflanke. x0 liest immer 0; Schreiben in x0 hat keine Wirkung. Gezeigt sind x0 bis x7, x8 bis x31 funktionieren genauso.

Der Datenpfad, Stück für Stück

Jetzt lassen sich die Teile zu einem Datenpfad verbinden. Patterson und Hennessy bauen ihn schrittweise auf [1, Abschn. 4.3]. Zuerst das Holen: Der PC adressiert den Befehlsspeicher, und ein Addierer berechnet PC + 4, die Adresse des nächsten Befehls. Der PC mit seinem Addierer ist der Zähler aus Teil 2. Bei R-Typ-Befehlen gehen zwei Register in die ALU, und das Ergebnis geht zurück in den Registersatz. Bei Lade- und Speicherbefehlen addiert die ALU ein Register und den vorzeichenrichtig erweiterten Direktwert zur Adresse. Bei Sprüngen subtrahiert die ALU die beiden Register, und ihr Zero-Flag sagt, ob sie gleich sind; ein zweiter Addierer berechnet das Sprungziel.

Wo zwei Quellen einen Eingang speisen können, wählt ein Multiplexer: Register oder Direktwert für die ALU (ALUSrc), ALU-Ergebnis oder Speicher für den Registersatz (MemtoReg), PC + 4 oder Sprungziel für den PC. Befehle und Daten haben getrennte Speicher, weil ein Eintaktprozessor im selben Takt einen Befehl liest und auf Daten zugreift und jedes Teil nur einmal pro Befehl nutzen kann [1, Abschn. 4.3]. Für bne und blt ergänzen wir einen kleinen Kasten, der die Bedingung prüft: Zero für beq, nicht Zero für bne und für blt den Vergleich mit Vorzeichen [3], der sich aus den Flags von Teil 1 als Negative ≠ Overflow ergibt.

Probier es aus

Der Datenpfad, eine Befehlsart nach der anderen

[6–0][19–15][24–20][11–7][31–0][30, 14–12]4PCAddBefehls-speicherSteuerwerkRegisterrs1rs2rdSchreibdatenLesedaten 1Lesedaten 2Imm GenALU-SteuerungALUAddSprung?DatenspeicherAdresseSchreibdatenLesedaten010101

Jeder Befehl beginnt gleich: Der PC schickt seine Adresse an den Befehlsspeicher, der den Befehl liefert. Gleichzeitig berechnet ein Addierer PC + 4, die Adresse des nächsten Befehls. Der PC ist der Zähler aus Teil 2, nur in Viererschritten.

Gezeichnet nach dem Eintakt-Datenpfad bei Patterson und Hennessy, Abschnitte 4.3 und 4.4, mit unseren Ergänzungen für addi, bne und blt; siehe die Quellen.

Das Steuerwerk stellt die Weichen

Wer stellt all diese Multiplexer und Freigaben? Das Steuerwerk. Es liest den Opcode und setzt sieben Signale. In einem Eintaktprozessor braucht es gar keinen Speicher: Jeder Befehl ist in einem Takt erledigt, also folgen die Signale allein aus dem Opcode, eine Wahrheitstabelle wie die in Teil 1 [1, Abschn. 4.4]. Die ALU wird in zwei Stufen gesteuert. Das Steuerwerk schickt ein zwei Bit breites ALUOp, das heißt addieren, subtrahieren oder „schau auf funct3 und funct7“, und eine kleine ALU-Steuerung macht daraus die Steuerleitungen der ALU aus Teil 1 [1, Abschn. 4.4]. Prozessoren, die einen Befehl auf mehrere Takte verteilen, brauchen für ihr Steuerwerk einen Automaten, wie in Teil 2 angekündigt.

Übernimm die Rolle des Steuerwerks: Stell die Signale für jeden Befehl ein und prüfe dann deine Antwort.

Probier es aus

Sei das Steuerwerk

add x3, x1, x2 addiert zwei Register und schreibt die Summe in ein drittes.

  • RegWrite1: ein Ergebnis nach rd schreiben
  • ALUSrczweiter ALU-Eingang: 0 Register rs2, 1 Direktwert
  • MemRead1: den Datenspeicher lesen
  • MemWrite1: den Datenspeicher schreiben
  • MemtoRegWert für rd: 0 ALU-Ergebnis, 1 Speicher
  • Branch1: darf springen, wenn die Bedingung gilt
  • ALUOp00 addieren, 01 subtrahieren, 10 funct3 und funct7 entscheiden
0 von 5 Befehlen gelöst

Ein Programm läuft

Zusammengesetzt ist das ein vollständiger Prozessor. In jedem Takt stellt sich der Datenpfad auf den Befehl am PC ein; an der Taktflanke übernehmen der PC, höchstens ein Register und höchstens ein Speicherwort ihre neuen Werte. Wähle ein Programm und geh es Schritt für Schritt durch, oder schreib ein eigenes. Die Werte auf den Leitungen sind die des aktuellen Takts.

Probier es aus

Der ganze Prozessor

Maschinencode

AdresseMaschinencodeBefehl
000A00293addi x5, x0, 10
400000313addi x6, x0, 0
800530333loop: add x6, x6, x5
12FFF28293addi x5, x5, -1
16FE029CE3bne x5, x0, loop

Im Bild: der Befehl an Adresse 0, während seines Takts. Die nächste Taktflanke speichert seine Ergebnisse.

[6–0][19–15][24–20][11–7][31–0][30, 14–12]4PCAddBefehls-speicherSteuerwerkRegisterrs1rs2rdSchreibdatenLesedaten 1Lesedaten 2Imm GenALU-SteuerungALUAddSprung?DatenspeicherAdresseSchreibdatenLesedaten010101RegWrite 1ALUSrc 1ALUOp 00MemWrite 0MemRead 0MemtoReg 0Branch 0PCSrc 004addi x5, x0, 100x00A002930101010x5 ← 104

Register

x0
0
x1
0
x2
0
x3
0
x4
0
x5
0
x6
0
x7
0
x8
0
x9
0
x10
0
x11
0
x12
0
x13
0
x14
0
x15
0
x16
0
x17
0
x18
0
x19
0
x20
0
x21
0
x22
0
x23
0
x24
0
x25
0
x26
0
x27
0
x28
0
x29
0
x30
0
x31
0

Datenspeicher (Adresse: Wort)

0
2
4
3
8
5
12
7
16
11
20
0
24
0
28
0
32
0
36
0
40
0
44
0
48
0
52
0
56
0
60
0

Hier ist alles zu sehen, was der Prozessor tut. In jedem Takt stellt sich der Datenpfad auf den Befehl am PC ein, und an der Taktflanke übernehmen der PC, höchstens ein Register und höchstens ein Speicherwort ihre neuen Werte. Ein Befehl pro Takt: ein Eintaktprozessor.

Gezeichnet nach dem Eintakt-Datenpfad bei Patterson und Hennessy, Abschnitte 4.3 und 4.4, mit unseren Ergänzungen für addi, bne und blt; siehe die Quellen.

Warum ein Takt pro Befehl zu langsam ist

Einen Haken gibt es. Jeder Befehl bekommt einen Takt, also muss der Takt langsam genug für den langsamsten sein. Das Beispiel bei Patterson und Hennessy nimmt 200 ps für einen Speicherzugriff und für die ALU an und 100 ps für Lesen oder Schreiben des Registersatzes. Ein Ladebefehl braucht alle fünf Schritte und 800 ps, ein Sprung nur 500 ps, und doch bekommt jeder Befehl die vollen 800 ps [1, Abschn. 4.6]. Verschieb den Regler: Je langsamer der Speicher, desto mehr Zeit geht mit Warten verloren.

Probier es aus

Ein Takt für alles

Zeit, die jeder Befehl braucht, in einem Takt von 800 ps
  • Speicherzugriff
  • Registersatz und ALU
  • Warten auf die Taktflanke
BefehlZeit
lw800 ps
sw700 ps + 100 ps Warten
add, addi …600 ps + 200 ps Warten
beq, bne, blt500 ps + 300 ps Warten

Dieses Programm führt 32 Befehle aus. Bei 800 ps pro Takt dauert das 25,6 ns. Bräuchte jeder Befehl nur so lange, wie er muss, wären es 18,2 ns, 29 % weniger.

Zeiten wie im Beispiel bei Patterson und Hennessy, Abschnitt 4.6: 200 ps für einen Speicherzugriff und für die ALU, 100 ps für Lesen oder Schreiben des Registersatzes. addi braucht dieselben Schritte wie ein R-Typ-Befehl. Siehe die Quellen.

Die bessere Idee kommt vom Fließband. Man zerlegt jeden Befehl in seine fünf Schritte, IF, ID, EX, MEM und WB genannt, und lässt fünf Befehle gleichzeitig arbeiten, jeden in einem anderen Schritt [1, Abschn. 4.6]. Das ist Pipelining, und darum geht es im nächsten Teil.

Wie es weitergeht

Damit hast du jede Schicht eines einfachen Prozessors gesehen, vom Schalter in Teil 1 bis zu einem Programm, das darauf läuft. Echte Prozessoren können viel mehr: Multiplikation, Funktionsaufrufe mit einem Stapel, Unterbrechungen, Caches und vor allem Pipelines. Pattersons und Hennessys Computer Organization and Design geht diesen Weg ausführlich [1], Hennessys und Pattersons Computer Architecture: A Quantitative Approach zeigt, wie Prozessoren schnell werden [2], und den vollständigen Befehlssatz enthält die RISC-V-Spezifikation [3].

Zurück zu Teil 1: vom Schalter bis zur ALU oder Teil 2: Speicher und Automaten.

Quellen

  1. D. A. Patterson und J. L. Hennessy, Computer Organization and Design RISC-V Edition: The Hardware/Software Interface, 2. Aufl. Morgan Kaufmann (Elsevier), 2020. ISBN 978-0-12-820331-6. Verwendet: Kapitel 2 “Instructions: Language of the Computer”; Abschnitte 4.3 “Building a Datapath”, 4.4 “A Simple Implementation Scheme” und 4.6 “An Overview of Pipelining”; Anhang A “The Basics of Logic Design.” Verlagsseite
  2. J. L. Hennessy und D. A. Patterson, Computer Architecture: A Quantitative Approach, 6. Aufl. Morgan Kaufmann (Elsevier), 2017. ISBN 978-0-12-811905-1. Verlagsseite
  3. RISC-V International, The RISC-V Instruction Set Manual, Volume I: Unprivileged Architecture, Version 20260120, 2026. Verwendet: Kapitel 1 “RV32I Base Integer Instruction Set, Version 2.1.” Online bei RISC-V International