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:
- Mindestens doppelt so großer Speicher
- Paralleler Transfer (zwei Datenbusse) erhöht die Geschwindigkeit
- Besserer Prefetch möglich
- Keine Hardware notwendig zum Trennen von Daten und Codezugriffen.
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:
- Lege Adresse auf den Adressbus
- Prüfe ob Daten auf dem Datenbus anliegen
- Lese Daten ein
- Führe Operation aus
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 Byte Wort (enthält Adresse oder drei Register)
- Zwei Byte Wort (die meisten Befehle) + NOP
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:
- Register zu Register: Ld Reg,Reg – Kopieraktionen in Registern. In meinem Prozessor weil alle Register gleichberechtigt sind weniger oft nötig als im 8080, da dieser oft Ergebnisse aus dem Akku in andere Register übernehmen musste.
- Direkt: Eine konstante folgt in Byte 2+3. Das kann eine konstante für eine Rechnung sein oder eine Adresse auf die man im folgenden einfach über das register zugreifen kann. Eine direkte Adressierung ist nur mit einer konstante als Quelle nicht als Ziel möglich.
- Indirekt: Es gibt zwei Möglichkeiten, beide haben ein Klammerpaar als Symbol (): Indrekt aus einem Register oder indirekt aus einer Adresse.
- Ld Reg1,(Reg2): Lade das Register1 mit dem Wert den Du an der Adresse findest die im Register2 steht. Ist dort z.B. die Adresse 4000, dann wird das Wort aus Speicherzelle 4000 geholt.
- Ld (Adr),Reg: Speichere in der Adresse die angegeben ist den Inhalt des Registers (hier Adresse als Ziel).
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:
- Die Breite der internen Register – die Menge an Daten die man auf einmal verarbeiten konnte
- Die Breite des Datenbusses – wie viele Bits man pro Takt einladen konnte
- Wie breit waren Adressen – gibt den adressierbaren Speicher an.
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:
- Die Intel 4004 hatte Operationen nur für 4 Bit. Der Datenbereich wurde aber über 8 Bit Adressiert und dann noch über Bankswitching 5 Stück davon = 1280 Adressen zu je 4 Bit. Das Rom wurde über 12 Bit adressiert und war byteorientiert.
- Der 8008 war ein 8 Bit Prozessor mit nur 8 Bit Operationen auf den Registern. Der Adressbereich war aber 14 Bit = 16 KByte
- Der 8080 hatte einen 64 Kbyte Adressbereich und konnte erstmals Adressen auch in Registern berechnen, das bedeutet einige Rechenoperationen (Addition, Subtraktion, Erhöhen und erniedrigen) gingen mit 16 Bit.
- Der TMS 9900 ist hier der einzige Prozessor mit sauberen Kennzahlen: 16 Bit breite Register, 16 Bit Datenbus, 16 Bit Adressbus – leider byteorientiert hätte man Worte genommen so wären es immer 128 kb gewesen.
- Der Intel 8086 hat Register von 16 Bit Breite, 16 Bit Datenbus, aber 20 Bit Adressbus, Die Adressierung erfolgt durch Addition von Segmentregistern zu Basisregistern. Damit ist der Adressbus segmentiert wie beim 4004. Beim 8088 war auch der Datenbus 8 Bit breit. Beide wurden von Intel als 8/16 Bitter bezeichnet weil sie 8- und 16-Bit Befehle ausführten, das gilt aber auch für den 8080.
- Der MC6800 hat Register von 32 Bit Breilte, 16 Bit Datenbus und 24 Bit Adressbus. Er ist am komplexesten. Er war schon für eine 32 Bit Erweiterung ausgelegt, so warne die Register 32 Bit breit, ebenso konnte der Assembler 32 Bit lange Zahlen als Operanden akzeptieren. Der Adressbus war aber auf 24 Bit beschnitten. Die ALU rechnet mit nur 16 Bit- Motorola bezeichnet die CPU daher als 16/2 Bit CPU.
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 |
Sehr interessant. Als ich den Artikel gelesen habe, fiel mir das Buch „Prozessorbau“ von Christian Siemers ein, dass ich zwar nicht gelesen, aber schon öfter mal in der Unibibliothek gesehen habe.
Beim Aufbau der Befehle und der Speicherstruktur kamen mir dann die AVR-Mikrocontroller in den Sinn, da die auch 16 Bit breite Instruktionen in einem separaten Speicherbereich für diese verwenden (Havard-Architektur), der Wortweise adressiert wird.
Als nächstes stellte sich mir dann die Frage: Hast Du den Proessor auch mal praktisch getestet? – also eine Beschreibung davon in VHDL oder Verilog angefertigt? – Das wäre heutzutage ja der nächste Schritt beim Design eines neuen Prozessors. Wenn der Entwurf der Hardware steht, kann man einen Emulator anschmeissen, der heute auch gleich in der Entwicklungsumgebung der Hardwarebeschreibungssprache enthalten sein dürfte. Dann kann man auch schon mal ein paar Programme entwickeln, natürlich erst mal in dem definierten Assembler, die auf der entworfenen CPU laufen.
Wenn dass auch alles funktioniert, kann man sich entweder über das vollbrachte Werk freuen, oder man macht den nächsten Schritt in die reale Hardware und schiebt die Beschreibung in einen FPGA-Chip, der vorläufig aber noch auf einer FPGA-Evaluations-Platine steckt, und lässt ihn für sich arbeiten… – Wenn auch das alles zufriedenstellend läuft, kann man einen Rechner mit der brandneuen BL-2015 CPU bauen! 😉
Nein von VHGL verstehe ich nichts. Ich halte es mit „Schuster bleib bei deinen Leisten“. Das beschränkt sich bei mir auf die makroskopische eben, nicht das Schaltungsdesign. Ich hinke sowieso was die Umsetzung anderer Projekte angeht hinterher. Immerhin: Die Bauteile für meine Meteologiestation habe ich inzwischen bestellt.
Nun ja, ich kann auch kein VHDL, aber so „ein bischen“ digitale Elektronik. In dem Zusammenhang fiel mir dann noch ein, dass eine ALU schaltungstechnisch eine recht aufwändige Angelegenheit ist, etwa im Vergleich zu einfachen Schieberegistern oder Addierern; – sie muss ja auch beides auch können. Möglicherweise war das ein Grund dafür, das bei vielen Prozessoren der 70er und frühen 80er Jahre nur das Akkumulator-Register direkt mit ALU verbunden war und alle anderen nicht. Ist jedenfalls so eine Vermutung, die ich hiermit mal in den Raum stelle.
Mir kam übrigens später noch in den Sinn, dass man einen Prozessor ja auch erst einmal komplett in Software simulieren (modellieren?) kann. Dazu baut man ein Programm, das so tut, als wäre es der Prozessor. Dieses liesst die einzelnen Bytes oder Worte, die den Programmcode darstellen, analysiert sie und je nach Ergebnis dieser Analyse tut es das, was entsprechende Hardware in dem Fall auch tun soll(te). Wenn man so eine Simulation richtig aufzieht, kann man auch schon eine Menge über die Vor- und Nachteile heraus finden, die einem sonst erst einmal verborgen bleiben. Da stellt sich natürlich die Frage, wie „die Simulation richtig aufziehen“ zu verstehen ist. Aber die lass ich hier mal offen, weil ich die Antwort nicht weis.
Moin,
> Da stellt sich natürlich die Frage, wie „die Simulation richtig aufziehen“ zu verstehen ist. Aber die lass ich hier mal offen, weil ich die Antwort nicht weis.
eine Registermaschine schnell zu emulieren kann man sich im Lua Interpreter, dem Hercules Emulator und einigen anderen Byte-Code-Interpretern von anschauen. Auch der SIMH ist da sehr interessant, weil klassische Maschinen wie den H316, PDP oder Altair emuliert.
Als Einstieg in die Theorie sollte vorher SICP durchgearbeitet werden. In diesem legendären Studienkurs wird am Anfang erklärt was eine Programmiersprache ist, und dann sehr schnell verschiedene Methoden diese zu implementieren, wie rekursives interpretieren (McEval), und auch Übersetzung in eine virtuelle CPU, die dann auch gleich interpretiert wird.
ciao,Michael
Wenn man sich die Mühe macht eine komplett neue CPU zu entwerfen, ist es hilfreich ein paar Vorgaben zu machen bevor man anfängt. z.B.
– eine grobe Anzahl von Transistoren oder Gates.
– was hätte man in 19xx in Technologie Y machen können.
– eine verbesserte Version von CPU Z.
– eine CPU mit der Besonderheit A.
– es soll auf ein FPGA B passen.
– einen Einsatzzweck, für den keine vorhandene CPU passt (warum?).
Es gibt eine große Auswahl verschiedener CPUs oder Modelle, aus denen man sich bedienen kann.
– CPUs mit sehr kompaktem Befehlssatz siehe Bernd Paysans 4stack oder B16 http://bernd-paysan.de/4stack.html
– 16 Bit RISC z.B. Siemens 80C166
Ich komme aus der Software Ecke, und habe mit Embedded Systemen nie viel am Hut gehabt. Daher war für mich was kleineres als eine 32 Bit CPU eher uninteressant. (Ich habe den mc68k immer als verkrüppelte 32 Bit CPU betrachtet.)
Daher würde ich bei einem neuen CPU Design mit einem mc68k/IBM360/MIPS32/arm anfangen, und versuchen die bekannten Probleme zu beheben.
Ein interessantes alternatives Problem wäre eine möglichst kleine, aber noch hinreichend schnelle 32 Bit CPU zu entwerfen. (Da gibt es aber auch schon Vorschläge.)
Echte Harvard Architekturen mit getrenntem RAM für Code und Daten gehören für mich zu den Spezialisten für Signalverarbeitung, wo das Code RAM auf dem CPU Chip integriert ist. Und da wäre dann auch 24 Bit breites RAM für die Daten hilfreich.
Betreffend Bernds Vorschlag für einen verbesserten Z80 (16 Bit CPU):
1) 24 Bit Code Worte sind nicht erforderlich, und von der erforderlichen Speicherbandbreite nicht sonderlich effizient. Man kann darum herumkommen indem man:
– variabel lange Befehle verwendet. Dazu ist es hilfreich die Länge des Befehls in den obersten 2-3 Bits zu kodieren.
– einen kleinen Code Cache, oder zumindest Code Buffer implementiert, und den Code z.B. immer in 2 alignten 16 Worten lädt.
Damit kann ein aktueller Arm mit nur 16 Bit breitem RAM noch effizient laufen.
2) statt 2x 64k Worte oder 2x 64k Byte RAM zu ermöglichen, hat man mehr davon Banking oder Segmente zu implementieren. Das muß nicht so aussehen wie auf dem 8086. Damit hat ein Prozeß 64k Daten zur Verfügung, optional 64k Stack, optional 64k String und aus Software Sicht beliebig viel Code. (Auf der PDP11 gab es unter Modula2/RT11, hat sehr gut funktioniert. (Die Bank Umschaltung hat der Compiler gemacht, war nicht sichtbar für den User.))
3) solange man einigermaßen ausreichend Register hat, sind komplizierte Addressmodi nicht erforderlich. Register Offset sollte aber dabei sein, das gibt kürzeren Code. ggf. alternativ Register | Offset (concat) wenn der zugehörige Addierer zu viel Platz braucht.
Moin,
weils gerade zum Thema passt, und auf HN aktuell ist:
https://en.wikipedia.org/wiki/Little_man_computer
http://peterhigginson.co.uk/LMC/ <- LMC Simulator
ciao,Michael
Sehr schön, dass hier auch noch ein paar andere Betrachtungen dazu gekommen sind. Dazu wahscheinlich heut Abend mehr.
Jetzt mal kurz (mehr oder weniger jedenfalls) zu einem Detail bei Bernd’s Befehlssatz; ich hab da nämlich noch ein Problem: Und zwar kenn ich vom 6502-Befehlssatz so eine Tabelle, wo die Opcodes bitweise aufgeschlüsselt sind: Dazu wird ein Byte in zwei Hälften, den Nibblen, zerlegt. In den Zeilen stehen dann die Hi-Nibble in den Spalten die Lo-Nibble. Anhand dieser Tabelle lässt sich eine Systematik in die Bitmuster der Opcodes bringen oder daraus ablesen. Ein Beispiel dafür findet man in diesem Datenblatt eines 65C02s von WDC auf Seite 22. Die Frage, die sich mir jetzt stellt ist, ob es so eine Zusammenstellung auch für Z80-Opcodes oder andere Befehlssätze gibt, oder ob das ein Spezifikum in den Dokumetationen des 6502-Befehlssatzes ist?
Und falls jemand fragt, Was ich davon habe oder mir davon verspreche? – Eine schnellere Übersicht für einen groben Vergleich der Befehlssätze.
Hans,
Tabellen, die die Befehle einer CPU über die Codierung auflisten gibt es für verschiedene CPUs.
Das muß nicht über Nibbles kodiert sein. IBM 360 und mc68k sind über Nibbles kodiert, RISC CPUs z.B. in der Regel nicht. Es gibt CPUs bei denen in einem 32 Bit Wort 3 Stück 10 Bit Befehle kodiert sind. Die anderen beiden Bits geben das Befehlsformat an, oder kodieren ein optionales Return (bei Forth CPUs).
Für mich ist das erste, was mich an einer CPU interessiert das Registermodell, und wie viele Adressen ein Befehl hat. Dann erst welche Befehle und Adressmodi es im einzelnen gibt.
Moin,
gerade der Z80 war mit seinen zum Vergleich einfacheren Memnonics, für die selben Befehle quasi wie Basic zu programmieren. Und die Z80 Op Code Tabelle ist quadratisch genug, dass nach einiger Übung die Codes im Kopf waren.
http://z80-heaven.wikidot.com/opcode-reference-chart
ciao,Michael
Michael K. schreibt:
Ah ja. SICP, also Structure and Interpretation of Computer Programs. Hab mir das Buch heut‘ Nachmittag in der Bib mal angesehen. Es macht einen brauchbaren Eindruck, ist mir momentan aber zu Umfangreich. Und dann hab ich derzeit auch nicht so grosse Lust, mich in Scheme rein zu knien, obwohl es am Anfang erklärt wird.
„Rechnerorganisation und -entwurf“ von Patterson und Hennessy erscheint mir gerade sinnvoller, vor allem auch in Anbetracht der gesammten Serie über CPUs, die Bernd in letzter Zeit schreibt.
—
Andreas Buschmann schreibt:
Ich dagegen komme eher aus der Hardwareecke und beschäftige mich gerade mit Embedded Systems. Deshalb sind für mich gerade oder besser: immer noch auch 8 Bit CPUs interessant, obwohl man bei den eingebetteten Systemen ja besser von MCUs redet. (Denn neben der reinen CPU sind da ja noch diverse Perepherieschaltungen mit auf dem Chip.)
Interessant. 🙂
Nun ja, die Atmel AVR Controller verfügen auch über Harvard-Architektur, wobei der Befehlsspeicher in 16-Bit Worten organisiert ist. Aber deshalb sind es immer noch Microcontroller, die sich zum Teil zwar auch für DSP-Aufgaben eignen, aber keine speziellen DSP-Chips sind.
Hm… – also entweder hab ich die dann noch nicht entdeckt, oder wir „reden“ hier gerade aneinander vorbei.
Ah ja, nun gut. Mit IBM 360 usw. hatte ich nie zu tun und mc68k gibt es AFAIK ja mittlerweile nicht mehr. Jedenfalls, was die reinen CPUs angeht. Von der Controllerschiene, die inzwischen unter dem Label FreeScale firmiert, hab ich keine Ahnung.
Aber ich glaube, ich sollte mich dann wohl mal selbst ans Werk machen, und mir diese Tabellen für Z80 und die 8 Bit AVRs selbst erstellen…
Was meinst Du mit „wie viele Adressen ein Befehl hat“ genau? Die Bitbreite der Befehlsworte oder was?
Na Bravo! – Da hab ich jetzt wieder so lange an dem Kommentar gewerkelt, dass er sich mit dem letzten von Michael K. überschnitten hat. Anyway danke für den Link. Dann brauch ich mir die Arbeit doch nicht mehr machen. – Bzw. nur noch für die AVRs…
zu FreeScale:
die haben das selbe Registermodell wie der mc68k, und die selben Assemblerbefehle, aber eine andere Codierung der Assemblerbefehle. Also Ähnlich wie die Umstellung 8080 zu 8086. Ein Problem mit dem mc68k war, daß die Codierung der Befehle es sehr schwer gemacht hat die Länge eines Befehls zu erkennen, und daß man den mc68k deswegen nicht mehr schneller machen konnte.
zu „wie viele Adressen ein Befehl hat“:
so wie ich es gelernt habe gibt es:
– 0-Adress Befehle, in denen ein Register impliziert ist oder keines benötigt wird
z.B.: return, rti, halt, swap, pop
werden sehr häufig bei Stackmaschienen verwendet
– 1-Adress Befehle, die einen Parameter haben und alle weiteren ggf. benötigten Register implizit sind
z.B.: call, jump, add to Akku, push
– 2-Adress Befehle, bei denen zwei Operanden angegeben werden und einer davon überschrieben wird
z.B. ein „add a b“ welches die Inhalte von Register A und B addiert, und das Ergebnis in B ablegt.
Viele 16Bit CPUs sind 2-Adress Maschienen. z.B. mc68k, PDP11
– 3-Adress Befehle
z.B. ein „add a b c“ welches die Inhalte von Register A und B addiert, und das Ergebnis in C ablegt.
Die Idee war, daß eine Operation keinen ihrer Operanden zerstören muß, um „move REG, REG“ einzusparen.
Die VAX und die meisten 32 Bit RISC CPUs sind 3-Adress Maschinen
Die IBM 360 hat 2-Adress und 3-Adress Befehle, aber auch die 3-Adress Befehle zerstören einen Operanden. Es gibt da Adressierungen mit REG REG Offset.
– 4-Adress Befehle
z.B.: Bit extract, insert und mask Befehle, sowie die 32 Bit x 32 Bit -> 64 Bit Multiplikation welche 2 Register für das Ergebnis benötigt.
Die IBM 360 hat 16 Integer Register von 32 Bit.
Es gibt dort z.B. 16 Bit Befehle
8 Bit Opcode, 4 Bit Zielregister, 4 Bit Register
sowie 32 Bit Befehle
8 Bit Opcode, 4 Bit Zielregister, 4 Bit Register, 4 Bit Register, 4 Bit Offset
Die Codierung ist sehr schön regelmäßig (Implementierung über Microcode)
Der mc68k hat 8 8 Integer Register von 32 Bit.
Es gibt dort z.B. 16 Bit Befehle
8 Bit Opcode, 3 1 Bit Zielregister, 3 1 Bit Register
Den CPU Designern war 4 Bit pro Register im Befehl zu verschwenderisch, deshalb wurde der Registersatz in zwei Hälften geteilt. (Adressen/Daten)
Der mc68k hat auch 32 Bit und 48 Bit Befehle um z.B. Konstanten zu laden, oder für einen JUMP.
Die klassischen RISC CPUs (Berkley, MIPS, Sparc, HP-PA, Alpha) haben alle 32 Register, ein 32 Bit Befehlswort und 3-Adress Befehle. Dabei werden dann in jedem Befehl für die Adressierung der Register 15 Bit benötigt. Um alle Befehle unterzubringen, und auch Platz für Konstanten im Befehl zu haben, hat jede der CPUs mehrere unterschiedliche Befehlsformate.
Die ARM CPU hat 16 Register statt 32, um Adressbits für die Adressierung der Register zu sparen. (3-Adress mode) Im Thumb mode sind sogar nur 8 Register erreichbar, und es wird ein 2-Adress mode verwendet.
@Andreas Buschmann:
Ah. Danke für die Erklärungen; wieder was dazu gelernt. Nach dieser Adressordnung der Befehle hat der 6502 dann nur 0- und 1-Adressbefehle. So Sachen wie MOV a,b gibt’s da nicht; sondern das muss man dann in 2 Befehle auftrennen: LDA a; STA b;
Hab mir jetzt auch mal ein paar Datenblätter der 68HCxxx-Controller von FreeScale herunter geladen und angesehen. Deren CPU hat ja eine grosse Ähnlichkeit mit dem 6502 und der Befehlssatz scheint in weiten Teilen auch identisch zu sein. (Wenn man dann mal ein bischen auf die Geschichte der Prozessoren guckt, verwundert das auch nicht.)
Dann wäre noch festzustellen, dass die 8-Bit AVRs wohl 2-Adress-Befehle haben, soweit ich das bisher verstehe.
32 Bit Vergleiche sind bei einem 8/16 Bitprozessor nicht so passend ähnlich wie Lua das es als die 8 Bitter in waren noch gar nicht gab.
Für alle die zwar mitreden aber nicht mal die verschiedenen Adressierungsarten kennen ganz neu ein Aufsatz mit den Grundlagen dazu auf der Webseite:
http://www.bernd-leitenberger.de/adressierung.shtml
Was es aber zu 8bit Zeiten schon gab war Sweet16 von Steve Wozniak. Eine 16 Bit CPU die auf dem 6502 emuliert wurde. Der Vergleich von Lua die als moderne Registermaschine kleiner und schneller ist als vergleichbare Stackmaschinen von Perl, Python oder Ruby, ist damit durchaus gegeben. Der Sweet16 Interpreter war erheblich schneller und mit nur 300 Byte auch sehr viel kleiner als die sonst üblichen Stanford und San Diego P-Code Interpreter. Selbst minimale Forth Systeme waren in dieser Zeit meist größer.
Als Beispiel für eine extravagante 16 Bit Architektur möchte ich mal das folgende vorschlagen:
Ziel 16 Bit Architektur deren erste Implementierung relativ klein sein kann, und die Reserven für den späteren Ausbau hat.
Ich nehme mir den 6502 als Vorbild, plus Superakkumulator
Registermodell:
A: Superakkumulator, besteht aus mindestens 4 St. 16 Bit Worten, ggf. in späteren Implementierungen mehr.
C: 24 Bit Register zum Zusammenbauen von Konstanten (optional, wie beim Transputer)
X: 24 Bit Adressregister, ggf. in späteren Implementierungen 32 Bit
Y: 24 Bit Adressregister, ggf. in späteren Implementierungen 32 Bit
PC: 24 Bit Adressregister
SP: 8-12 Bit Stackpointer
Zeropage: Die 16 Bit Worte an den Adressen 0-255
Stack: Die 16 Bit Worte an den Adressen 256-511 (ggf. müssen das 24 Bit Einheiten sein)
Hier wäre noch zu klären ob Byteadressierung besser wäre, und ob man jeden Bereich auf z.B. 1024 Bytes vergrößern sollte.
Dann wären die meisten Befehle 6Bit Code 10 Bit Data,
Der C Befehl 2Bit Code 14 Bit Data.
48 Befehle kann aber knapp werden.
Befehlsformate:
a) ALU Operationen
Ein Operand ist in A, der zweite Operand ist in der Zeropage oder eine Konstante oder eine Konstante /- C. Ergebnis wird nach A geschrieben, dabei wird das oberste Element in A überschrieben.
b) Laden und Speichern von A/X/Y/PC/SP/C aus bzw. in Zeropage.
c) 12 Bit Konstante in die obere Hälfte von C laden. ggf. 14 Bit?
d) Laden und Speichern von A an die Adressen X, Y, C, jeweils auch mit festem Offset. Bedingte und unbedingte Sprünge an diese Adressen und an A. Call.
e) Befehle ohne Parameter wie return, pop, halt, debug
f) Befehle, die mehr als 1 16 Bit Wort von A verwenden wie:
Multiplikation: 1 Wort von A (destruktiv), 1 Wort aus Zeropage oder C Konstante, Ergebnis sind 2 Worte auf A.
MAC: 1 Wort von A (destruktiv), 1 Wort aus Zeropage oder C Konstante, das Produkt wird zu den obersten 3 Worten auf A addiert.
Bit insert und extrakt auf 2 16 Bit Worten
g) Addressoperationen: X/Y /-= Wort aus Zeropage oder C Konstante oder Konstante
h) Stack Befehle ohne Parameter (ähnlich wie e) da ansonsten die Anzahl der möglichen Befehle doch knapp sein kann.
Keine Division, keine Wurzel.
zu I/O habe ich noch keine Idee.
zu einer MMU habe ich noch keine Idee.
Die Befehle sollen alle 16 Bit lang sein, große Konstanten werden in C zusammengebaut.
Die wichtigsten Optimierungen wenn mehr Transistoren zur Verfügung stehen sind:
– Zeropage auf dem Chip
– Stack auf dem Chip
– 256 Worte Code Cache.
– ggf. später noch ein B Register
p.s. gibt es einen vernünftigen Begriff für eine 16 Bit Einheit? Für mich ist das normalerweise ein Halbwort, aber in diesem Zusammenhang passt das überhaupt nicht.
Moin,
wenn ich eine CPU für Bernd designen würde, dann würde da erstmal ganz kleine 8bit oder gar 4bit Alus die Arbeit machen, oben drüber befindet sich ein horizontaler Microcode, da drüber dann der vertikale Microcode. In diesem ist eine BL-P-Maschine implementiert. Ein Control Program Feature übersetzt dann Stanford-P-Code und UCSD-P-Code in BL-P-Code und stellt die Stanford und UCSD Laufzeitumgebungen zur Verfügung, so dass alle echten Pascal Programme laufen.
ciao,Michael
@Andreas Buschmann:
Das klingt nach einem interessanten Design. 🙂
Aber ich hab da neben ein paar Vorschlägen auch noch Fragen:
Zum Superakkumulator: Du schreibst, der soll „aus mindestens 4 St. 16 Bit Worten“ bestehen. Was soll das „St.“ in dem Fall bedeuten? – Stück, Stapel oder was?
Bei dem C-Register frag ich mich gerade, ob man dafür nicht auch die Zeropage benutzen kann, oder den unteren Bereich des Stack? (Für alle, die den 6502 nicht kennen: Da liegt der Stack fest an den Adressen 256 bis 511; er ist also nicht verschiebbar, wie bei anderen Prozessoren.
Die Zeropage ist, wie ihr Name schon sagt, die nullte Seite im Speicher, zu deren Adressierung nur die unteren 8 Bit, (also das LoByte) der Speicheradresse heran gezogen werden. Sie lässt sich dadurch schneller ansprechen und dient bei bestimmten Adressierungsarten auch als „Zeigerbank“, da man dort Zeiger unterbringt (bringen kann), über die sich andere Speicherbereiche adressieren lassen.)
Adressregister: Ich nehme an, Du willst die Register so breit machen, um einige der „interessanten Adressierungsarten“ über die Zeropage (LDA ($nn),Y) einzusparen, um den Speicherzugriff zu beschleunigen, richtig? Dazu hab ich erst geschrieben: „Dazu würde ich die Register erst mal 16 Bit breit machen und später dann gleich von 16 auf 32 Bit erweitern.“ – Aber 16 Bit Adressregister sind bei einem 24 Bit breiten Adressbus Unsinn, wenn nicht wieder so ein Quark wie bei Intel’s x86er dabei heraus kommen soll. Wahrscheinlich wäre es in Anbetracht zukünftiger Erweiterungen des Adressraums eher sinnig, sie gleich 32 Bit breit zu machen.
Dann würde ich darüber nachdenken, ob es sich lohnt, die Register ebenfalls an die ALU anzuschliessen. – Wie breit soll die eigentlich sein? – 16 Bit oder mehr? (Nach Deinem Kommentar vom 22.6. zu schliessen, wahrscheinlich schon 16 Bit. Könnte aber auch mehr sein.)
Was soll ein 12 Bit Stackpointer? – Soll der durch den Stack belegte (belegbare) Adressbereich 2^12 Worte umfassen, wie breit sie auch immer sein mögen? – Oder wie soll man das verstehen?
Ein spezieller Befehl zum Zugriff auf das C-Register, oder was soll das sein?
Die weiteren Vorschläge zum Befehlssatz entsprechen in etwa dem des Originals, wenn man von der Multiplikation und den Bit Insert/-Extract Befehlen absieht. – Aber was soll MAC sein?
Division und Wurzel, evtl. Logarithmus-/Exponentialfunktion sowie trigonometriesche Funktionen in einer FPU? – Hätte ich nix dagegen. 🙂
Zu I/O und MMU: Vielleicht enthält ja mein Aufsatz über die Speicherverwaltung von C64/C128 eine Inspiration dazu. – Hab ja keine Ahnung, ob Du eines der Geräte hattest oder noch hast.
Die Zeropage und den Stack auf dem Chip klingt interessant, aber wie willst Du dann mit dem RAM-Speicher verfahren, der sich ebenfalls an diesen Adressen befindet?
Bei einem 24 Bit breiten Adressbus stellt sich mir auch die Frage, was die MMU da eigentlich leisten soll? – Mir fallen dazu nur Speicherschutzmechanismen ein, weil ich Bankswitching in einem 24 Bit breiten Adressbus, also 16 MB erst mal nicht für nötig halte. – Andrerseits, wenn man an heutige Speicherausstattungen denkt… – Aber das soll ja ein 16-Bit System sein, und kein 32-Bit System.
Etwas später kam ich für das I/O-System auf die Idee, es wie beim Original als Memory Mapped I/O System auszulegen. Dann hätte die MMU die Aufgabe, je nach Zugriffberechtigung entweder die Register der Perepheriebausteine oder aber den an dieser Stelle befindlichen Speicher einzublenden. Betriebssystem und LoLevel Treiber dürften darauf zugreifen, andere Software bekommt in diesem Bereich nur RAM zu sehen. Das BS müsste also so gestaltet werden, dass Anwendersoftware diesen Bereich nicht ohne besonderen Aufwand verwenden kann.
Also in Turbo Pascal war ein WORD 16 Bit breit und ein 32 Bit breites Datum ein DWORD. Das würde bei dem vorgeschlagenen CPU Design mMn auch einen Sinn ergeben.
Und zuletzt kam nach mehmaligem Korrekturlesen noch die Frage auf, ob die CPU Multiprozessorfähig sein soll oder nicht? Also soll sie ähnlich wie der Transputer auch gleich dafür ausgelegt sein, neben anderen CPUs arbeiten zu können, oder eher nicht? (Begriffe wie SIMD bzw. MIMD schwirren mir dazu durch den Kopf.)
@Hans:
Der Superakkumulator ist ein Register, welches deutlich breiter ist als ein normales Register.
Man braucht ein doppelt breites Register für das Ergebnis einer Multiplikation;
und auch für Bit insert/extract und shift/rotate über mehr als die einfache Wortbreite.
Man braucht ein dreifach breites Register für das Ergebnis einer Multiplikation Addition (MAC).
(Ist ein Spezialbefehl für Vektor- und Matrixmultiplikationen. A = m*n)
Um Spezialbefehle für das Lesen und Schreiben des Superakkumulators zu vermeiden würde ich den als einen Stack von mindestens vier Elementen implementieren. (MAC braucht ein drei Worte breites Register auf dem akkumuliert wird, und einen der Multiplikanden auf dem Stack, da die Befehle alle nur einen Parameter haben sollen. Das St. steht dann für Stück.
Das C Register ist zum Zusammenbauen von Konstanten gedacht, so daß für alle Befehle 16 Bit ausreichen.
Für eine 24 Bit Adresse wird dann mit einem c) Befehl die obersten 14 Bit des C Registers geladen. Der Rest bleibt 0. Ich kenne den 6502 und seine Adressierungsarten nicht gut genug um erkennen zu können, ob man ohne C Register auskommt.
X und Y sollten immer mindestens 24 Bit breit seine, und bei einer 32 Bit Erweiterung dann auch 32 Bit breit. X und Y sollen immer den vollen Adressbereich abdecken können. (Eine Ausnahme wäre wenn durch Banking oder einer MMU mehr Speicher im Rechner sein kann als adressierbar ist.)
Mit ist nicht klar, ob X und Y an die normale ALU angeschlossen sein müssen, oder ob die einen eigenen 24 Bit breiten Addierer brauchen. Dazu müßte man eine Simulation machen.
Der 8 bzw. 12 Bit Stackpointer verweist in Page 1 für call und return. Kann man das anders oder besser machen?
Wenn die Zeropage und der Stack auf der CPU implementiert sind wird das RAM an den zugehörigen Adressen nicht benutzt.
Eine optionale MMU hat drei mögliche Aufgaben:
a) Zugriffsschutz
b) mehr RAM verfügbar machen als die CPU adressieren kann
c) virtueller Adressraum
Bei einer 16Bit CPU macht aus meiner Sicht pageing keinen Sinn.
Eine einfache segmentierte MMU reicht völlig aus:
Es gibt z.B. vier MMU Register, welche jeweils einen Adressrange definieren.
jedes besteht aus drei Elementen: virtual base V, length L, physical base P.
Eine virtuelle Adresse ist nur zugreifbar, wenn wie in einem Bereich [Vn .. Vn Ln) liegt.
Eine Adressumsetzung wäre dann physikalische Adresse = virtuelle Adresse – Vn Pn.
Meine Idee war für eine kleine CPU. Die muß nicht mehrprozessorfähig sein. Ein relativ eng angebundenes Ethernet oder serieller Bus würden vielleicht Sinn machen, aber ich dachte an eine erste Implementierung, die kleiner als ein mc68k oder 8086 ist.
Aus meiner Sicht lohnt sich ein eng verbundenes multiprozessorfähiges System erst wenn die CPU schon 32 oder 64 Bit ist, Cache hat und ggf. eine FPU on chip ist.
@Andreas Buschmann:
Ah, danke für die Info. Das ist wirklich eine exotische bzw. extravagante Architektur. Aber gut, das kann man ja mal durchspielen… – wenn man es kann.
Der Superakku ist echt ein Unikum, soweit ich das beurteilen kann. Als Stack organisiert würde er mMn aber wirklich nur für besondere Operationen der Mathematik taugen. Also für Ergebnisse von Multiplikationen von Skalaren oder Vekor- / Matritzenelementen.
Das C-Register würde wahrscheinlich sinnvoll werden, weil spezielle Konstanten sonst im Akku zusammen gebaut werden müssten. Zu den Adressierungsarten des 6502 komme ich weiter unten noch.
Ich vermute, dass es sinnvoll ist, X und Y auch an die ALU anzuschliessen, wenn man die (berüchtigte) Zeigerarithmetik von C/Cpp effektiv in Assembler umsetzen will, so dass für die Adressberechnungen möglichst wenig Overhead entsteht.
Wenn der Stackpointer nur für call und return genutzt wird, ist das okay. Und da wir schon mit unterschiedlichen Wortbreiten operieren, halte ich es für sinnvoll, dass der Stack auch Wortweise organisiert wird, wobei ein Wort 16 Bit breit sein sollte. Dann kommt man auch bei späteren Erweiterungen auf 32 Bit mit 2 Worten für die Rücksprungadresse aus. Bei 12 Bit breite wären es 3 Worte und bei 8 Bit eben 4 Worte.
Ein weiteres Problem ergibt sich auch bei Hochsprachen, die die Parameterübergabe über den Stack organisieren. Das ist beim klassischen 6502 nicht sinnvoll, weil der eben nur 256 Byte gross ist. Entsprechende Hochsprachen (etwa C und Pascal) müssen ihren Parameterstack da per Software realisieren, weil der CPU-Stack schlicht zu klein ist. Man denke etwa an den Quicksortalgorithmus, der ein grosses Array sortieren soll. Rekursiv implementiert bricht der irgendwann wegen Speichermangel auf dem Stack ab, wenn er nur den CPU-Stack nutzt.
Das RAM unter Zeropage und Stackpointer unbenutzt zu lassen, wenn sie in der CPU selbst sitzen, ist okay. Da dürften die 512 Worte bei Speicherkapazitäten im MB-Bereich nicht sonderlich ins Gewicht fallen. Die MMU wäre auch okay und Multiprozessorfähigkeiten braucht man ja auch nicht unbedingt.
Jetzt noch zu den Adressierungsarten des 6502:
Da ist zunächst einmal die unmittelbare Adressierung, d.h. ein auf den Befehl folgendes Byte wird als Konstante betrachtet, die in eines der Register A, X oder Y zu laden ist oder als Operand für Arithmetik oder Logikbefehle dient.
Manche Befehle beziehen sich auch direkt auf den Akku oder ein anderes Register, das wird als „Implied Adressing Mode“ bezeichnet.
Dann gibt es die absolute Adressierung, wobei die nachfolgenden Bytes als Adresse aufgefasst werden. Dabei spielt jetzt auch die Zeropage eine Rolle, denn je nach Opcode wird entweder nur das folgende Byte als Adresse verwendet, dann erfolgt der Zugriff auf die Zeropage. Oder das nachfolgende Wort, womit jede Adresse im Speicher angesprochen werden kann.
Dazu gibt es die Möglichkeit, die Register X oder Y als Index zu verwenden, der zur absoluten Adresse dazu addiert wird. Das nennt sich dann indizierte Adressierung.
Als letztes wären da noch die indirekt indizierten Adressierungsarten, die immer über die Zeropage laufen. Da ist zum einen die indirekte X-Adressierung, mit der man eine Adresstabelle in der Zeropage ansprechen kann. Der Operand des Befehls gibt die Basisadresse der Tabelle in der Zeropage an, der Index X wird zu dieser Basis dazu addiert. Das Ergebnis dieser Operation ist eine Adresse in der Zeorpage, die einen Zeiger auf jene Adresse im Speicher enthält, auf die man zugreifen will.
Zum anderen gibt es die indirekte Y-Adressierung. Der Operand gibt eine Adresse in der Zeropage an, die einen Zeiger auf eine Adresse im Speicher enthält. Zu dieser Adresse wird der Wert des Y-Registers addiert, womit man die Zieladresse erhält, auf die mit der Operation zugegriffen wird.
Und falls das irgendwer jetzt nicht sofort verstanden hat, ist das nicht tragisch. Ich hab auch eine Weile gebraucht, bis ich es verstanden hatte.
Der Vollständigkeit halber sei noch auf die relative Adressierung hingewiesen, die nur bei den Verzweigungs-Befehlen (Branch-Befehlen) verwendet wird. Hier wird der Abstand relativ zum Programmcounter angegeben, der von -128 bis 127 reicht.
@Hans:
Den Superakku hatte ich mir ursprünglich für eine 32 Bit CPU ausgedacht, weil ich mir nicht gefallen hat, wie vorhandenen CPUs eine 32b x 32b -> 64b Multiplikation organisiert ist. Da ist das Umladen des Ergebnisses aus As = A1 | A2 | A3 in normale Register auch kein Problem (2 Adress Befehl).
Bei einer kleinen 16 Bit CPU mit 1 Adress Operationen wollte ich aber nicht so viele von den möglichen Befehlen für das Lesen des Superakkus verschwenden, deshalb die Organisation als Stack (für normale Befehle, die auf 16 Bit Worten arbeiten).
As besteht dann aus den Worten A1, A2, A3, A4, und man kann direkt nur auf A1 zugreifen.
Eine Multiplikation ist dann:
( A2 | A3 ) := Operand x A1, und A1 wird gepopped
Ein MAC (MultiplyAdd) ist dann
( A2 | A3 | A4 ) = Operand * A1, und A1 wird gepopped.
Da man den Stack nun mal hat, kann man auch alle seltenen Befehle als 0 Adress Befehle implementieren, das spart Platz im Opcode Space.
A2 := A2 OP A1, und A1 wird gepopped.
In späteren Implementierungen kann man z.B. 32 Bit Operationen nachrüsten wie:
( A3 | A4 ) OP= ( A1 | A2 ), und A1 und A2 werden gepopped.
Eine Festkomma Multiplikation wäre z.B.
Push Operand A1 := Operand, alle anderen Ax Inhalte 1x weiterschieben
MUL Operand ( A1 | A2 ) := A1 * Operand, alle anderen Ax Inhalte 1x weiterschieben
Shift32 Const ( A1 | A2 ) >>= Const
Pop A1 := A2, alle anderen Ax Inhalte 1x zurückschieben
Ich hatte überlegt, für die Zeropage 10 Bit Adresse in jedem Befehl zu verwenden.
Dann hätte man entweder 1024 Bytes oder 1024 Worte. (Ich weis nicht was besser ist.)
Der Stack sollte 1024 24 Bit Adressen groß sein.
@Andreas Buschmann:
Hast Du zu dem Prozessorentwurf eigentlich auch schon mal eine Befehlsliste erstellt, wie Bernd sie hier für seinen Prozessor entworfen hat?
Da ich nur 6502-Assembler kann, der aber keine Multiplikationen kennt, hab ich damit bisher auch keine Erfahrungen, und kann Deine Idee deshalb leider nicht so sachgerecht bewerten, wie ich es mir selbst wünschen würde. Bin jetzt zwar dabei, daran was zu ändern, aber das wird trotzdem noch eine Weile dauern.
Vielleicht kann Michael K. ja zwischenzeitlich noch den einen oder anderen qualifizierten Kommentar abgeben…
Noch mal zu:
Ich sitze gerade an einem Rechner mit Windows 7 und hab da den mitgelieferten Taschenrechner geöffnet. Der bietet in dieser Version u.a. eine spezielle Ansicht und Oberfläche für Programmierer an. Da hat man links am Rand zwei Auswahlfelder mit (Push)Buttons, wo man zwischen Byte, Word, Dword und Qword wählen kann, wobei die Zuordnug so ist:
Byte: 8 Bit,
Word: 16 Bit
Dword: 32 Bit
Qword: 64 Bit
Im zweiten Feld kann man das Zahlensystem auswählen, in welchem man die Zahlen angezeigt haben möchte. Zur Auswahl stehen die 4 üblichen Verdächtigen: Binär, Oktal, Dezimal und Hexadezimal.
Dazu gibt es unter der Zahlenanzeige noch ein Feld, wo einem immer die Bitmuster der Zahlen angezeigt werden, jeweils zu 4 Bits gruppiert. Das alles aber nur für ganze Zahlen. – Wäre ja auch zu schön, wenn er da auch noch die Bitmuster von Fliesspunktzahlen anzeigen würde. – Aber andrerseits braucht man die auch nicht so oft. Oder falls doch, dann gibt es ja noch andere Programme, die sie einem anzeigen. – Oder man schreibt sich selbst eines.
Soviel noch mal dazu, obwohl das für viele hier sicher auch ein alter Hut ist. Aber egal.
Das Problem ist, das ist die x86 spezifische bzw. Windows spezifische Definition von Byte und Wort. In anderen Umgebungen haben diese Begriffe eine andere Bedeutung.
In der Datenübertragung wird Octet für ein 8Bit Wort verwendet, und wie ich jetzt gefunden habe gibt es den Begriff Hextet für ein 16Bit Wort. (Wird inoffiziell für die Darstellung von IPv6 Adressen verwendet.) Ich erinnere mich vage an einen anderen Begriff aus einem der Bücher über Computerarchitektur. Ich kann mich aber nicht erinnern in welchem das vorkam. (Einer der Bände von „The Art of Computer Programming“ oder einer der Hennessy Patterson.)
Ja, dass die Begriffe je nach Umgebung eine andere Bedeutung haben, ist mir auch schon aufgefallen. Insbesondere, wenn man in der englischen Wikipedia nachschlägt. Hab mir deshalb mal erlaubt, die Frage weiter zu geben, und zwar nach dort, denn dort sind auch einige Informatiker unterwegs.
So, nachdem die Diskussion der Frage im anderen Blog im wesentlichen gelaufen ist, würde ich sagen, dass man hier am Besten von 16-Bit-Worten spricht, u.a. weil das eben die „natürliche Breite“ der Arbeitsregister dieser CPU (von Andreas Buschmann) ist. Alles andere ergibt sich dann daraus.