Bernd Leitenbergers Blog

Mein 8/16/24 Bit Prozessor

Heute mal eine Neuigkeit: in der Reihe „Wir wissen es besser als die Industrie“ heute ein Konzept für einen Prozessor. Ich habe das mal angefangen für einen 32 Bit Risc Prozessor, aber in der Klasse habe wenig Erfahrung in Bezug auf Befehlssatz. Die habe ich in der 8 und 16 Bit Klasse und inspiriert durch die Unzulänglichkeiten der frühen 8 und 16 Bitter von Intel habe mich an das Design eines eigenen Prozessors gemacht. es sollte ein 8 Bitter sein,  aber es ist ein 16/24 Bitter geworden. Dazu später mehr. Ich will den Artikel auch nutzen ein paar Dinge zu erklären die vielleicht den einen oder anderen interessieren.

Der Grundgedanke war das der Befehlssatz symmetrisch sein Darunter versteht man dass die Register gleichberechtigt sind. Also z.B. jedes Ziel und Quelle einer Rechenoperation sein kann. Das war beim 8080/Z80 nicht der Fall. Alle 8 Bit Rechenoperationen nutzten dort als einen der Operanden den Akkumulator und dort landete auch das Ergebnis. Ein solches „unsymmetrisches“ Modell macht dann zahlreiche Kopieraktionen in den Registern notwendig, wenn man das Ergebnis z. B. noch braucht. Wenn man weiß wie Befehle dekodiert werden, dann ergibt sich beim symmetrischen Modell eine Problematik. Nehmen wir 16 Register bei einem unsymmetrischen Prozessor könnte der Opcode des Befehls ADD A,Reg z.B. so aussehen:

0100.RRRR

Das Befehlswort wird eingelesen, der Prozessor stellt anhand der Maske 0100 in den obersten 4 Bit fest das es ein ADD-Befehl ist und in den unteren 4 Bits steht das Quellregister. Für 16 Register braucht man 4 Bits (ein Nibble). Wenn der Befehlssatz symmetrisch ist sähe der Befehl so aus : Add Reg1,Reg2 und man braucht 8 Bits für die Angabe der beiden Quellregister (wenn man die Dreiadressadressierung nimmt sogar 12 Bits). Damit braucht man zwei Byte für einen Befehl. 16 Register ist auch die Zahl die ich vorhergesehen habe.

Nun haben 8 Bitter mit 64 Kbyte maximal adressierbarem Speicher keinen sehr großen Adressbereich. Wer wie ich seine ersten Erfahrungen mit einem 8-Bit Rechner gemacht hat wie einem C64, Armstrad oder Sincailr Spectrum. Der weiß – man kann damit einiges machen. Das geht aber nur weil viele Befehle nur ein Byte lang sind. Hier wären sie nun zwei Byte lang und das ist schon ein großes Manko. der Speicher eines 8-Bit Systems sollte eher größer als kleiner sein. Da kam ich auf die zweite Änderung: es gibt einen getrennten Daten- und Codebereich, ebenso zwei Adressbereiche. Das macht in meinem Fall zwar ein 64-poliges Gehäuse notwendig, aber es gibt einige Vorteile:

In meinem Fall habe ich jeweils 16 Bit Adressen für Daten und Code. Als ich mich an das Aufstellen der Codes machte (siehe unten) stellte sich raus, das die meisten Befehle nun zwei oder drei Bytes lang sind, die Ein-Byte Befehle sind in der Minderheit denn sie kommen nur bei keinem Parameter vor oder nur einem Parameter. Ich habe mich für eine Maßnahme entscheiden die den Prozessor kräftig auf Trab bringt: Anstatt einzelner Bytes sind Befehle immer drei Bytes lang. Benötigt man nur zwei Bytes, dann enthält das letzte Byte den NOP Befehl, das Bitmuster für 0. Die Befehle ohne Parameter die eigentlich in ein Byte reinpassen habe ich in zwei geschoben so sind zum einen viele Opcodes frei und zum anderen ist das Befehlsformat einheitlicher.

Anders als bei anderen Prozessoren hat NOP  aber nicht die Bedeutung einige Takte nichts zu tun, sondern ist ein Füllbyte das bei der Ausführung ignoriert wird also keine Zeitverzögerung generiert, dekodiert werden immer die gesamten 3 Byte. Der Vorteil wird deutlich wenn man sich ansieht, wie viele Takte bestimmte 8080 Befehle haben:

Aktion Mnemonic 8080 Takte 8080 Takte Mein Prozessor
Lade Register mit Register Mov Reg,Reg 4 4
Lade Register mit 16 Bit Konstante LXI Reg 10 4
Lade Register mit 8 Bit Inhalt der Adresse die in einem zweiten Register steht Mov Reg,M 7 nicht verfügbar
Lade Register mit 16 Bit Inhalt der Adresse die in einem zweiten Register steht LHLD 16 10
Lade Adresse mit dem Inhalt des Registers STAX 10 7

Diese Unterschiede kommen dadurch zustande:

Bei einem Befehl der nur interne Register beim 8080 benutzt, gibt es folgenden Taktablauf:

Bei dem zweiten Befehl LXI muss nun nochmals die Adresse auf den Bus gelegt werden, erneut gewartet werden bis die Daten anliegen und dann kann man das erste niedrigwertige Byte holen, nach einem erneuten Anlegen der Adresse+1 wiederholt sich das Spiel. Das sind pro Lesezyklus immer 3 Takte mehr, also zehn Takte.

Pro Lesezyklus eines Bytes oder Schreiben eines Bytes verlängert sich die Ausführung um 3 Takte. Bei meinem Prozessor ist nun aber das ganze Befehlswort schon im Prozessor, er transferiert nicht 8 Bit sondern 24 Bit über den Code-Datenbus. Das bedeutet beim zweiten Befehl entfallen die Zugriffe auf den Speicher. Beim Befehl LHLD braucht man einen weiteren Zugriff, diesmal auf den Code-Datenbus. Da dieser aber 16 Bit auf einmal transferiert, braucht man nur 3 weitere Takte anstatt 6.

Das ist die erste Maßnahme um Geschwindigkeit aufzunehmen. Die zweite ist, dass man bei dem starren Befehlsformat sehr einfach eine Pipeline implementieren kann. Zusammen mit einem kleinen Buffer von zwei Bytes für Daten und sechs für Code kann man die Geschwindigkeit deutlich erhöhen. Dier mal ein kleines Zeitdiagramm für den Fall eines Speicherzugriffs auf den Datenbereich (7 Takte ohne Beschleunigung)

Takt 1 2 3 4: Zyklus 2 beginnt 5 6 7: Zyklus 3 beginnt 8
Buseinheit Code Adresse auf Bus Warte auf Daten Hole Daten Adresse auf Bus Warte Auf Daten Hole Daten Adresse auf Bus Warte auf Daten
Ausführungseinheit Führe aus Führe aus
Buseinheit Daten Adresse auf Bus Warte auf Daten Hole Daten

Der allererste Zyklus auf einer neuen Adresse (bei einer Verzweigung oder Sprung dauert immer 4 Takte, der folgende unabhängig von der Anzahl der Speicherzugriffe immer 3 Takte. Ausnahme sind einige Befehle, die intern sehr lange brauchen. Das sind Multiplikation und Division diese profitieren von der integrierten dreistufigen Pipeline. Als man diese im 80286 einführte sank die Ausführungszeit der DIV/Mul Operationen stark, denn die werden intern als Mikroprogramm ausgeführt und brauchen keine Speicherzugriffe außer beim Start so von 80-168 je nach Adressierung bei 8086 auf 17-28 beim 286. Dies war der Grund warum der Norton -Sysinfo einen so hohen SI-Wert für IBM-AT kompatible Rechner ausgab, denn dort steckten genau diese Operationen in einer Schleife (ein IBM-AT wurde z.B. 8,8-fach schneller als ein 8088 mit 5 MHz angezeigt anstatt rund 2,5 mal wie es richtig war).

Eine ähnliche Strategie gab es auch beim 8086 der eine 6 Byte Qeue hatte und einen automatischen Fetch d.H. während der Ausführungszeit las der Prozessor schon mal vorrauschauend die nächsten Bytes. Gerade diese Eigenschaft bremste den 8088 stark aus, weil durch den 8-Bit-Bus der Fetch nur noch selten angewandt werden konnte.

Kurzum: Dieser Prozessor ist deutlich schneller als andere 8-Bitter aber auch einfache 16-Bitter wie der 8086. (Würde man synchrones DRAM unterstützen, so wäre die Regelzeit eines Zyklus sogar nur 2 Takte und bei aufeinderfolgendem Code/Daten sogar nur noch 1 Takt.

Adressiert wird der Code und die Daten wortweise. Bei dem Code ist ein Wort 3 Bytes lang wobei drei denkbare Fälle möglich sind:

Drei NOP Bytes (Speicherzustand beim Reset, da der den Code 0 hat).

Nimmt man an das normaler 8 Bit Code im Mittel zwei Byte lang ist (es gibt viele ein-Byte-Befehle aber auch zwei und drei Byte Befehle) so bietet diese Lösung 100% mehr effektiven Speicher obwohl der Codebereich 192 KByte beträgt. Man kann auch Befehle einsparen weil mehr interne Register zur Verfügung stehen und alle mit 16 Bit rechnen.

Als ich den Instruktionssatz aufgestellt hatte, fiel mir was auf – es gibt gar keine 8 Bit Befehle. Alle Register sind, weil sie auch Adressen aufnehmen 16 Bit breit. Die Operationen bisher also auch immer 8 Bit. Wenn ich mir die arithmetischen Operationen ansehe, so sehe ich keine echte Notwendigkeit für 8 Bit. Bei Rechnungen reichen sie meist nicht aus, sie sind wegen dem 16-Bit Datenbus auch nicht schneller. Es gibt noch zwei Operationen bei denen könnte byteweise Operationen notwendig sein. Das eine sind Ein/Ausgabebefehle, das zweite sind Vergleiche, die braucht man z.B. bei einer Stringsuche. Dafür gibt es mehrere Lösungen.

Man könnte einen Befehl Compare Byte / Output / Input Byte einführen. Das zweite ist es bei Ein/Ausgabeoperationen einfach die oberen 8 Bit nicht zu verdrahten und bei Strings hilft einfach eine Konvention: man speichert immer zwei Bytes ab, nutzt also Unicode, das ist allerdings doch sehr platzverschwendend. So wäre ein Compare Byte Befehl nicht schlecht. Den Platz gibt es: im ersten Byte sind noch 164 Opcodes unbelegt, das ist so viel, dass man sogar alle arithmetischen Befehle mit 8 Bit integrieren könnte.

Ansonsten habe ich mich am 8080/Z80 Befehlssatz orientiert. Ich habe nicht alle Befehle übernommen, nur die die ich für sinnvoll hielt. Anstatt dem Befehl PUSH Flags könnte man natürlich auch den EXX Befehl des Z80 nehmen. Ich bin bei dem PUSH geblieben bei so eine Interruptroutine höhere Priorität eine nieder priore unterbrechen kann. Stackoperationen dauernd leider relativ lange. Man könnte auch einen 256 Wort Stack auf dem Chip nutzen. Alternativ, das weiter unten auch ein kleiner Cahce angedacht ist einen externen Speicher der für Cache und Stack gedacht ist.

Weiter ging ich bei den Adressierungsoperationen. Da gibt es ja eine Menge wie ich seit dem Studium der Befehlssätze von MicroVAX, NS32032 und Z8000 weiß. Ich habe mich auf einige sinnvolle Erweiterungen der drei Basisadressierungen des 8080 entschieden:

Allgemein gilt: Die Schreibweise Lad Reg1,Reg2 heißt: Lade das Register1 mit dem Inhalt von Register2. Folgende Adressierungsarten gibt es:

Das wars beim 8080. Der Z80 führte noch die indizierte Adressierung ein. Die Variante hier gibt es auch: Mit einer Konstante die zu einem Basisregister addiert wird. Dies ist ganz nützlich wenn man Datenstrukturen hat bei denen Teile immer an einem bestimmten Offset beginnen. Bei einem CP/M Verzeichniseintrag beginnt z.B. die FAT immer bei Offset 16. Dies ist die Instruktion Ld Reg, (Reg)+C beziehungsweise Ld (reg)+C,Reg, Die Indizierte Adressierung ist auf 12 Bits Offset beschränkt, das sind 4096 Bytes.

Flexibler ist die mit einem Basisregister und einem Offsetregister. Das erste bleibt konstant, das zweite wird erhöht. Der Vorteil ein Offsetregister anstatt dem Basisregister direkt zuer hohen ist dass man bei so bei einem erneuten Durchlauf das Basisregister nicht laden muss und vor allem bei einer Kopieraktion man nur ein Offsetregister erhöhen muss. Das ist ist die Instruktion Ld reg, (reg+reg) bzw. Ld (reg+reg),reg.

Speziell für Pointer braucht man die doppelte indirekte Adressierung: ld reg,((reg)). Dabei steht in Reg eine Adresse. Anstatt den Wert nun aus der Adresse zu holen wird dieser erneut als Adresse  angesehen wo sich der Wert befindet. Zeigervariablen haben als Wert die Adresse der Variablen auf die sie zeigen.

Verbesserungsmöglichkeiten: Sinnvoll beim Einsatz einer Pipeline ist ein kleiner Cache. Zwar kenne ich keinen 8-Bitter der einen hat, doch die TMS 9995 CPU hat einen 128 Byte Workspace auf dem Chip (die Architektur des Vorgängers TMS 9900 war ausgelegt auf nur wenige Register in der CPU aber eines war ein verschiebbarer Zeiger auf 256 Byte im RAM die dann als Register genutzt werden konnten, da RAM nicht so schnell wurde wie man das bei TI annahm bekam der Nachfolger daher sein internes schnelles RAM spendiert). 256 Worte, das sind 768 Byte belegen bei 3 Transistoren (so bei der 8080) pro Bit rund 18432 Transistoren – das ist eine Menge gemessen an den 8200 die eine einfache Z80 CPU hat, doch vergleichen mut den 68000 einer MC68000 oder 134.000 eines 80286 ist es wenig. Der Vorteil ist dass man im Cache dekodierte Anweisungen speichern kann. Ohne Speicherzugriff sinkt dann die Ausführungszeit auf einen Takt pro Befehl.

So nun noch zur Titelzeile. Früher hat man sich sehr um Bezeichnungen herumgestritten, also wann eine CPU ein 8 Bitter oder 16 Bitter ist. Folgende Kriterien gab es:

Alle Angaben konnte man diskutieren, vor allem weil bis zur 32 Bit Generation es so war, das die Breite von Registern für normale Operationen nicht für Adressen ausreichte, sonst wäre der Arbeitsspeicher zu klein gewesen. Einige Beispiele:

Heute könnte man nach demselben System eine aktuelle Intel CPU als 64/256 Bit CPU bezeichnen mit AVX2 wird daraus eine 64/512 Bit CPU … Meine müsste man nach der Breite der Register, die man meistens als Kriterium nimmt als 16 Bit CPU bezeichnen. Dieses Kriterium ist auch meistens das beste, denn mit der Registerbreite stehen auch die Befehle, so haben 16 Bit CPUs eben Befehle um 16 Bit Daten zu verarbeiten und meistens noch weitergehende (Multiplikation, Division) als 8-Bitter. Die Ausnahme ist der MC68000 der weil der 68020 als Nachfolger geplant war breitere Register hat als er intern in einem Rutsch bearbeiten kann, so in etwa vergleichbar mit den 8080.

Für einen 16-Bitter wäre der Adressbereich von 192 KByte Code und 128 KByte Daten recht klein. Das könnte man durch Segmentregister und einige Befehle ändern. Nur würde ich dann einen linearen Adressbereich anstreben, indem man z.B. die 16 Bit breiten Segmentregister als oberste 16 Bit nimmt. Zusammen mit Befehlen die Segmentregister erhöhen und erniedrigen müsste man so auch durch über 64 KWorte große Datenstrukturen gehen können. Die Beschränkung auf 64 KByte pro Datenstruktur war das was ich bei der Programmierung unter DOS immer als größte Einschränkung empfand.

Bedingt durch Pipeline, RISC Datenformat und Prefetch-Buffer müsste mein Prozessor deutlich schneller als normale 9-Bitter aber auch 16-Bitter sein. Selbst ohne Cache hat er Features die ein 80296 noch nicht hat, in dieser Geschwindigkeitsklasse wäre er dann einzuordnen.

Befehlssatz:

Befehl Byte 1 Byte 2 Byte 3
Arithmetrik


Adc Reg,(Reg) 0000.0001 RRRR.RRRR
Adc Reg,Reg 0000.0010 RRRR.RRRR
Add Reg,(Reg) 0000.0011 RRRR.RRRR
Add Reg,Reg 0000.0100 RRRR.RRRR
And Reg,(Reg) 0000.0101 RRRR.RRRR
And Reg,Reg 0000.0110 RRRR.RRRR
Sbc reg,(Reg) 0000.0111 RRRR.RRRR
Sbc reg,Reg 0000.1000 RRRR.RRRR
Sub Reg,(Reg) 0000.1001 RRRR.RRRR
Sub Reg,Reg 0000.1010 RRRR.RRRR
Xor reg,(Reg) 0000.1011 RRRR.RRRR
Xor reg,Reg 0000.1100 RRRR.RRRR
Or Reg,(Reg) 0000.1101 RRRR.RRRR
Or Reg,Reg 0000.1110 RRRR.RRRR
Cmp reg,Reg 0000.1111 RRRR.RRRR
Cmp reg,(Reg) 0001.0000 RRRR.RRRR
Idiv Reg,Reg 0001.0001 RRRR.RRRR
Imul Reg,reg 0001.0010 RRRR.RRRR
Mul Reg,reg 0001.0011 RRRR.RRRR
Div Reg,Reg 0001.0100 RRRR.RRRR
Unäre Arithmetrik
Dec (reg) 0001.0101 RRRR.0000
Dec Reg 0001.0101 RRRR.0001
Inc (reg) 0001.0101 RRRR.0010
Inc Reg 0001.0101 RRRR.0011
Neg Reg 0001.0101 RRRR.0100
Not Reg 0001.0101 RRRR.0101
Rotiere Links mit Carry 0001.0101 RRRR.0110
Rotiere links ohne Carry 0001.0101 RRRR.0111
Rotiere rechts mit Carry 0001.0101 RRRR.1000
Rotiere rechts ohne Carry 0001.0101 RRRR.1001
Schiebe links mit Carry 0001.0101 RRRR.1010
Schiebe links ohne Carry 0001.0101 RRRR.1011
Schiebe rechts mit Carry 0001.0101 RRRR.1100
Schiebe rechts ohne Carry 0001.0101 RRRR.1101
Sprünge
Jp (Reg) 0001.0101 RRRR.1110
Call (reg) 0001.0101 RRRR.1111
Call Cond 0001.0110 AAAA.AAAA AAAA.AAAA
Call Short,Cond 0001.0111 AAAA.AAAA
Call Short 0001.1000 AAAA.AAAA
Call Adr 0001.1001 AAAA.AAAA AAAA.AAAA
Call (Adr) 0001.1010 AAAA.AAAA AAAA.AAAA
Interrupt Adr 0001.1011 AAAA.AAAA
Jp Cond,Adr 0001.1100 AAAA.AAAA AAAA.AAAA
Jp Short Cond,Adr 0001.1101 AAAA.AAAA
Jp Adr 0001.1110 AAAA.AAAA AAAA.AAAA
Jp (Adr) 0001.1111 AAAA.AAAA AAAA.AAAA
Decrement Reg Jumpz,Short 0010.RRRR AAAA.AAAA
Ret 0101.1010 0000.1011
Ret Cond 0101.1010 0000.1100
IRet cond 0101.1010 0000.1101
Iret Cond 0101.1010 0000.1110
Ladebefehle
Ld (Adr),Reg 0011.RRRR AAAA.AAAA AAAA.AAAA
Ld Reg,(Adr) 0001.RRRR AAAA.AAAA AAAA.AAAA
Ld Reg,Konstant 0010.RRRR AAAA.AAAA AAAA.AAAA
Ld (Reg+C),reg 0011.RRRR RRRR.CCCC CCCC.CCCC
Ld Reg,(Reg+C) 0100.RRRR RRRR.CCCC CCCC.CCCC
Ld Reg,Reg 0101.0000 RRRR.RRRR
Ld (reg),Reg 0101.0001 RRRR.RRRR
Ld Reg,(reg) 0101.0010 RRRR.RRRR
Ld Reg,(Reg)+Reg 0101.0011 RRRR.RRRR RRRR.0000
Ld (Reg)+Reg,Reg 0101.0011 RRRR.RRRR RRRR.0001
Ld Reg,((Reg)) 0101.0100 RRRR.RRRR
Ld ((Reg)),Reg 0101.0101 RRRR.RRRR
Stackoperationen
Pop Flags 0101.1010 0000.0000
Pop Instruction Pointer 0101.1010 0000.0001
Pop Reg 0101.1010 0001.RRRR
Pop Stackpoointer 0101.1010 0000.0010
Push Flags 0101.1010 0000.0011
Push Instruction Pointer 0101.1010 0000.0100
Push Reg 0101.1010 0010.RRRR
Push Stackpointer 0101.1010 0000.0101
Ein/Ausgabe
In Reg,Adr 0110.RRRR AAAA.AAAA AAAA.AAAA
Out Reg,Adr 0111.RRRR AAAA.AAAA AAAA.AAAA
In Rg,(Reg) 0101.1011 RRRR.RRRR
Out (reg),Reg 0101.1100 RRRR.RRRR
Sonstiges
Di 0101.1010 0000.0110
EI 0101.1010 0000.0111
Halt 0101.1010 0000.1000
Nop 0000.0000
Set InterruptMask,Reg 0101.1010 0011.RRRR
Read Reg,InterruptMask 0101.1010 0100.RRRR
Complement Carry 0101.1010 0000.1001
Set Carry Flag 0101.1010 0000.1010
Frei für Erweiterungen 0101.1011 1111.1111

Die mobile Version verlassen