Archiv für den Monat: Oktober 2025

4 Bit Prozessor, Teil 4 – Erweiterungen

Ich habe einen Simulator in Java geschrieben damit man den Instruktionssatz der 4 Bit CPU einen Praxistest unterziehen kann, ohne dass man Hardware aufbauen und verändern muss. Und erste Gehversuche haben gleich ein paar Schwächen aufgedeckt.

Als ich den Entwurf gestartet habe, hatte ich die Register R1 bis R4 als general purpose Register angesehen und darum den Befehlssatz gebaut. Allerdings hätte eine 2-Adress-Maschine zu viele Bits für die Quell- und Zielregister verbraucht. Deshalb habe ich einen Akkumulator eingeführt, welcher bei Operationen mit zwei Operanden implizit den ersten Operanden enthält und somit zwei Bit spart. Dabei habe ich aber nicht ausreichend darauf geachtet, dass der Akku nun auch ein general purpose Register sein sollte, so fehlte z.B. ein Indexregister indirect load. Wenn der Akku aus der Indexregister Adresse geladen werden sollte, wurde ein temporäres Register benötigt und die beiden Befehle ld R1 [IX1] und ld A R1. Das ist jetzt nach der Erweiterung nicht mehr notwendig, man kann direkt ld A [IX1] ausführen.

Weiterhin waren die Indexregister für streaming Aufgaben gedacht – man kann sie hoch- oder runterzählen. Wenn man sie aber für einen Zugriff innerhalb einer Struktur verwenden will, dann muss man auf eine Basisadresse einen Offset aufaddieren. Das war nur mit Umwegen über den Speicher möglich. Nach der Erweiterung können die drei Digits des Indexregisters einzeln in den Akku übernommen werden und nach der Berechnung wieder eingefügt werden.

Noch eine – hoffentlich nicht fehlgeleitete – Optimierung hat sich aus den ersten Programmierversuchen ergeben. Das Carry/ Borrow Flag wurde von allen ALU Funktionen aktualisiert, auch von einem einfachen Increment oder Decrement. Das ist aber bei Berechnungen, die in einer Schleife laufen, unpraktisch.

Für eine 32 Bit Addition wird in einer Schleife mit acht Runden jeweils das nächste Digit aufaddiert und dabei der Überlauf – das Carry Flag – in der nächsten Runde mit einbezogen. Allerdings finden nach der Addition noch inc und dec Aktionen statt zur Kontrolle der Schleife und zum Weiterzählen der Wertezeiger. Diese hatten in der ursprünglichen Version das Carry Flag verändert, es hätte also irgendwo geeignet zwischengespeichert und wieder abgefragt werden müssen. Dadurch, dass inc/ dec das Carry Flag nicht mehr verändern, spart man diesen Aufwand. Ich hoffe nicht, dass mir diese Entscheidung später auf die Füße fällt, aktuell sehe ich aber keine Probleme.

Im User Interface gibt es auch ein paar Erweiterungen. Zeilen mit einer besonderen Bedeutung werden nun farblich hervorgehoben.

Sprungziele werden Türkis markiert, lokale Sprünge Orange und Subroutine Aufrufe Grün. Fehlerhafte Zeilen werden Rot eingefärbt. Somit kann man im Assembler-Code schneller bestimmte Stellen auffinden.

4 Bit Prozessor, Teil 3 – Hardwaresimulation

Es wird immer noch kein Lötkolben heraus geholt. Ich beginne damit, einzelne Teile zu simulieren. Da ich nur wenig Erfahrung mit dem Entwurf digitaler Schaltungen habe, ist nicht klar, ob die Teile auch so funktionieren wie sie sollen. Fehlersuche und Korrektur sind in der Simulation wesentlich einfacher als bei der echten Hardware. Und wenn sich eine Vorgehensweise als nicht realisierbar herausstellt, hat man weniger Verluste.

Als erste Übung – insbesondere zur Einarbeitung in das Simulationsprogramm – habe ich mit der Addition/ Subtraktion angefangen. Es gibt zwar eine komplette ALU als TTL Baustein, die ist aber nur schwer zu bekommen und deckt nur 90% meines Bedarfs ab. Deshalb habe ich mich entschlossen die ALU komplett selber aufzubauen.

Die 4 Bit Addition ist einfach, es gibt hierzu mit dem 74LS83 einen fertigen Baustein. Und mit geringen Zusatzaufwand kann man daraus auch eine Subtraktion machen. Hierzu muss man nur das zweite Eingabewort negieren (nicht einfach invertieren). Invertieren ist einfach mit XOR Gattern zu erreichen. Zum negieren muss aber dann noch eins abgezogen werden. Das geht aber auch überraschend einfach indem man das carry in ebenfalls invertiert (in diesem Rahmen auch als borrow zu bezeichnen). Zudem muss bei der Subtraktion dann auch noch das carry out invertiert werden.

Das scheint soweit zu funktionieren. Bei der Addition wird die Summe bei aktiven carry in um eins erhöht und bei einer Summer größer als 15 (F Hexadezimal) wird das carry out aktiv. Bei der Subtraktion wird die Differenz bei einem carry in (oder borrow) um eins erniedrigt und sobald der Wert negativ wird, wird carry out aktiv. Die weiteren Funktionen der ALU werde ich in der kommenden Woche simulieren, diese sind jedoch noch einfacher und sollten keine Probleme verursachen.

Mein zweiter Test nimmt das vier Arbeitsregister ins Visier. Sie benötigen einen input port zusammen mit einer zwei Bit Adresse zur Auswahl des Registers welches beschrieben werden soll und einen output port ebenfalls mit einer zwei Bit Adresse zur Auswahl des Ausgabewerts. Hier setze ich auf ein 74LS173 – ein 4 Bit flankengesteuertes Latch. Zur Unterstützung der Simulation habe ich noch einen 4 Bit counter zur Erzeugung des Eingabewerts sowie zwei 2 Bit counter zur Auswahl des Schreib- und des Leseports eingefügt.

Zum Lesen wird die Adresse des Registers über SelOut angelegt und die Leitung OutEna(ble) aktiviert. Im inaktiven Zustand sind alle Ausgänge der vier Register hochohmig. Erst mit der Aktivierung wird über einen 2 nach 4 Demultiplexer das gewählte Register freigeschaltet und gibt seinen Inhalt aus.

Das Schreiben funktioniert ähnlich. Es wird die Schreibadresse über SelIn angelegt und die Leitung InLatch aktiviert. Mit der steigenden Flanke wird dann der aktuelle Wert von DataIn in das gewählte Register übernommen. Alle anderen Register bleiben unverändert. Leider wird bei der Bildschirmaufzeichnung die Mausbewegung nicht mit gespeichert, deshalb kann man nur schlecht verfolgen, was passiert. Es wird zuerst eine 1 in das Register 0, 2 in Register 1, 3 in Register 2 und 4 in Register 3 geschrieben. Danach werden die Inhalte der Register 0 bis 3 nacheinander angezeigt.

Als nächstes müssen die Ausgänge des Registerblocks und des Akkumulators auf die ALU geschaltet werden sowie das Ergebnis der ALU Operation wiederum in den Registerblock eingespeichert werden. Das wird die Aufgabe für die nächsten Tage werden.

4-Bit Prozessor, Teil 2 – Assembler und Simulator

Wenn man sofort anfängt seine Ideen in Hardware zu realisieren, hat man große Probleme, wenn sich Teile der Gedanken als nicht realisierbar oder unpraktisch erweisen. Deshalb habe ich als allererstes einen Simulator mit eingebauten Assembler geschrieben.

Mein Assembler ist offensichtlich stark vom Z80 beeinflusst. Das ist die CPU mit der ich als Jugendlicher zuerst in Berührung gekommen bin. Allerdings gibt es ein paar Vorgehensweisen die erheblich von normaler Assemblerprogrammierung abweichen.

  1. Ein Label für ein Sprungziel steht immer alleine (abgesehen von Kommentaren) und wird durch ein Doppelpunkt abgeschlossen. Funktionen, die per Call aufgerufen werden, haben ein Label welches durch ein fn (für function) eingeleitet werden. Nur solche Labels können von beliebiger Stelle aufgerufen werden. Normale Label werden automatisch „Funktions-lokal“. Das heißt, dass man nicht darauf achten muss, dass alle internen Sprungziele unterschiedliche Namen bekommen. Es ist also erlaubt, dass das Label „loop:“ in mehreren Funktionen verwendet wird und der Assembler verwendet dann jeweils die lokale Version.
  2. Da Label eindeutig durch einen Doppelpunkt gekennzeichnet werden, können OpCodes direkt in der ersten Spalte beginnen. Es ist nicht notwendig mit einem TAB anzufangen – aber es ist erlaubt.
  3. Alle Werte sind Hexadezimal.
  4. Da es keinen Linker gibt, wird auch unterschiedliche Code- und Datenbereiche verzichtet. Es gibt den Pseudobefehl org zur Festlegung des Startpunkts der nachfolgenden Befehle und eine data Anweisung welche nachfolgende Speicherinhalte kennzeichnet.
  5. Push und Pull Befehle gibt es nicht pro Register, sondern es gibt jeweils einen Befehl und dazu eine Registerliste mit den betroffenen Registern.

Der Simulator ist als JavaFX Anwendung realisiert. Er lädt direkt die Assemblerdatei und erzeugt daraus den Maschinencode, löst alle absoluten und relativen Sprungadressen auf und initialisiert den Speicher und die internen Register.

Load: öffnet einen Dateiauswahl Dialog welcher auf die Extension .zw4 voreingestellt ist. Es wird jeweils das zuletzt verwendete Verzeichnis voreingestellt. Falls es eine Funktion mit dem Label main: gibt, wird der PC im Simulator so voreingestellt, dass diese direkt ausgeführt wird.

Reload: lädt die aktuelle Datei neu. Es gibt keinen eingebauten Editor, zum Bearbeiten der Assemblerdatei muss ein externer Editor verwendet werden. Es reicht dann, nach einer Änderung, ein Speichern im Editor (er muss nicht geschlossen werden) und ein Klick auf Reload aus um den Simulator zu aktualisieren.

Quit: beendet den Simulator.

Step: liest den aktuellen PC und führt den nächsten Befehl aus. Der PC kann im User Interface zu beliebigen Zeiten geändert werden und der neue Wert wird beim nächsten Step verwendet. Das ist hilfreich, wenn man im Programm ein paar Schritte zurückspringen möchte, oder um sich aus einer toten Schleife zu befreien. Auch die übrigen Eingabefelder werden beim weiteren Programmablauf verwendet wenn sie manuell verändert wurden. Während des Programmablaufs wird das Textfenster mit dem Assemblercode automatisch aktualisiert, so dass der aktuelle Code sichtbar bleibt.

Run und Stop: führt das Programm aus bis es auf einen Breakpoint trifft oder mit Stop angehalten wurde. Einen Breakpoint kann man setzen indem man in der Programmliste auf die Adresse der jeweiligen Zeile klickt. Das Addressfeld wird dann in Folge rot angezeigt. Durch einen weiteren Klick wird der Breakpoint wieder gelöscht.

Funktions: diese Drop Down Box enthält eine Liste aller definierten Funktionen. Durch Anklicken wird der PC mit der jeweiligen Adresse initialisiert und die Assembleranzeige so gescrollt das diese Stelle sichtbar ist.

Registerliste: hier werden alle internen Registerwerte angezeigt. Die Registerwerte können auch manuell geändert werden und diese Werte werden dann beim nächsten Step verwendet. Umgekehrt werden die geänderten Werte nach dem Step angezeigt. Diese Liste wird auch im Run Modus aktualisiert, eine manuelle Änderung während des Programmlaufs ist deshalb praktisch unmöglich, da die Liste im Millisekunden Rahmen aktualisiert wird.

Speicheranzeige: der Simulator zeigt ein Speicherfenster von 256 Digits an. Die Startadresse kann frei eingestellt werden. Diese Anzeige wird auch während des Programmlaufs aktualisiert. Eine manuelle Änderung ist nicht vorgesehen.

Assembleranzeige: hier wird der Programmcode angezeigt. Die Stelle des aktuellen PCs wird markiert und bei Bedarf das Fenster so gescrollt, dass sie sichtbar ist. Die Anzeige wird auch während eines Programmlaufs mit Run aktualisiert. Falls der Assemblercode Fehler enthält, werden diese ebenfalls hier in der jeweiligen Codezeile angezeigt.

In einem ersten Schritt wird der Simulator dazu verwendet sich selber zu debuggen. Hierzu habe ich verschiedene kleine Assemblerfunktionen geschrieben, die die einzelnen Befehle aufrufen und prüfen, ob das gewünschte Ergebnis erzielt wurde. Im Augenblick habe ich ca. ein Viertel der Befehle getestet und dabei mehrere Abweichungen zwischen dem generierten Maschinencode und der simulierten CPU gefunden. An dieser Stelle sind Korrekturen jedoch viel einfacher als wenn sie erst auffallen nachdem die Hardware zusammengelötet wurde.

Aktuell umfasst der Simulator ca. 65 kB Programmcode. Es sind aber noch ein paar Erweiterungen geplant, wie z.B. die Unterstützung von Makros. Zudem wird der Assemblercode nur rudimentär auf Fehler geprüft. Hier muss auch noch weiterer Aufwand getrieben werden.

4-Bit Prozessor, Teil 1 – Projektbeschreibung

Wie viele aus meiner Generation habe ich oft darüber nachgedacht einen eigenen Prozessor zu implementieren. Das hat keinen praktischen Nutzen, es geht lediglich darum eine Vielzahl von Techniken zu erlernen. Man muss sich einen sinnvollen Befehlssatz überlegen, die notwendigen Entwicklungswerkzeuge implementieren, eine Prozessorarchitektur entwerfen und letztendlich das Teil auch tatsächlich realisieren. Die ersten Schritte sind für mich als Softwareentwickler relativ einfach. Der letzte Teil ist für mich die größte Herausforderung. Ich kann im Augenblick noch nicht absehen, wie weit ich tatsächlich komme und das Projekt wird sicherlich eine Weile dauern.

Ich habe mich für einen 4-Bit Prozessor entschieden damit die Hardware möglichst überschaubar bleibt. Da er am Ende ohnehin keinen sinnvollen Nutzen hat, ist es auch egal wie leistungsfähig er ist. Hauptsache ist, dass er läuft.

Der Befehlssatz ist an eine RISC Architektur angelehnt. Das heißt, es gibt logische und arithmetische Funktionen nur auf den Registern, keine direkte Speichermanipulation. Es ist aber kein echter RISC Befehlssatz, da ich variable lange Befehlssequenzen habe. Um die Codewörter möglichst kurz zu halten, habe ich zwei Beschränkungen gewählt. Zum einen gibt es nur vier Register (zwei Bit für die Registercodierung) und zum anderen wird mit einem Akkumulator gearbeitet, der bei Zwei-Operanden Befehlen implizit verwendet wird. Die meisten (alle?) Ein-Operanden Befehle können aber auch auf den Registern ausgeführt werden.

Im Überblick eine bullet point Liste mit den geplanten Eigenschaften:

  • 4 Bit Operationen (im Wesentlichen)
  • Von Neumann Architektur
  • 4 Register plus Akkumulator
  • 2 Indexregister
  • 1 Stackpointer
  • 12 Bit Adressen für PC, SP, Indexregister
  • 1 8 Bit Zero Page Register, analog zum Direct Page Register im 6809, 4 Bit Offset bei Verwendung
  • Befehlslänge 2 bis 5 Digits, jeweils 4 Bit

Die Anzahl der Adressierungsarten ist aus Gründen der einfachen Hardware beschränkt.

  • Immediate – nur für Ladevorgänge
  • Absolute – Laden, Speichern und Sprünge (Jump und Call)
  • PC Relative – nur für Branch, mit Condition Code, 8 Bit signed offset
  • ZP Relative – Laden und Speichern, 4 Bit lower digit (ZP liefert die oberen 8 Bit)
  • Indexregister – nur Laden und Speichern
  • Register – Register Transfer

In einem ersten Schritt wird ein Assembler für den geplanten Befehlssatz und ein Simulator zur Ausführung entwickelt. Die Ausführung im Simulator hat den Vorteil, dass man viel schneller prüfen kann, ob der Befehlssatz sinnvoll eingesetzt werden kann. Zudem sind Testdurchläufe viel schneller auszuführen und ein debugging des Verhaltens wesentlich einfacher.

Der Befehlssatz sieht vorläufig so aus:


00rr 0000 o1        ld R1..4 zero page 4 bit offset
00ix 0001 o1        ld IX1 IX2 A ZP 4 bit offset
00rr 0010           ld A from R1..4
0000 0011 v1 v2     ld ZP imme 8 bit value
0001 0011 v1        ld A imme 4 bit
0010 0011           ld ZP low digit from accu
0011 0011           ld ZP high digit from accu
00rr 0100 v1 v2 v3  ld R1..4 from abs address
00ix 0101 v1 v2 v3  ld IX1 IX2 A SP from abs address
00rr 0110 v1        ld R1..4 imme 4 bit value
00ix 0111 v1 v2 v3  ld IX1 IX2 SP ZP 12 / 8 bit imme value
00rr 1000           ld R1..4 from [IX1]
00rr 1001           ld R1..4 from [Ix2]
00rr 1010
00rr 1011

01rr 0000 o1        st R1..4 zero page 4 bit offset
01ix 0001 o1        st SP IX1 IX2 zero page 4 bit offset
01rr 0010           st A to R1..4
01?? 0011
01rr 0100 v1 v2 v3  st R1..4 to abs address
01ix 0101 v1 v2 v3  st IX1 IX2 A SP to abs address
01?? 0010
01?? 0011
01rr 1000           st R1..4 to [IX1]
01rr 1001           st R1..4 to [Ix2]
01rr 1010
01rr 1011

1000 d1 d2 d3       jmp 12 bit address
1001 d1 d2 d3       call 12 bit address
1010 cc d1 d2       bra 8 bit signed offset
1011 ????

11rr 0000           add
11rr 0001           sub
11rr 0010           addc
11rr 0011           subb
11rr 0100           and
11rr 0101           or
11rr 0110           xor
11rr 0111           inc R1..4
11rr 1000           dec R1..4
11ix 1001           inc IX1 IX2 A ZP
11ix 1010           dec IX1 IX2 A ZP
1100 1011           not A
1101 1011           neg A
1110 1011           stc set carry
1111 1011           clc clear carry
11rr 1100           cmp
11?? 1101
1100 1110 m1 m2     push m1 mask R1..4, m2 mask IX1 IX2 A ZP
1101 1110 m1 m2     pull
1100 1111           clr A
1101 1111           
1110 1111           ret
1111 1111           nop

Im Augenblick sind ca. 200 von den 256 möglichen Codewörtern belegt. Aber es werden vermutlich noch ein paar Befehle hinzu kommen. Da geplant ist, die Ausführung über einen Microcode Sequenzer zu steuern, ist es aus technischen Gründen nicht besonders wichtig, dass der Code möglichst symmetrisch ist – das ist eher eine Frage der Ästhetik. Die Befehle werden alle durch die beiden ersten Digits bestimmt, die nachfolgenden 0 bis 3 Digits sind dann nur noch Parameter, wie z.B. Adressen oder Offset Werte. Das Microcode ROM wird einfach 256 Einsprungpunkte erhalten. Die beiden letzten Aktionen einer Sequenz bestehen darin, die beiden Codeteile für den nächsten Befehl in das Code Register zu laden.

Nach einem Reset wird das Code Register mit dem jmp Befehl (1000 1101) und der Program Counter mit ffe initialisiert. Der Microcode dazu lädt dadurch den Startpunkt für das Programm aus der Adresse ffd aus (die letzten drei Digits) und springt an diese Adresse.

Vermutlich wird es im Laufe des Projekts hier noch die eine oder andere Änderung geben. Es ist halt ein Lern- und Forschungsprojekt welches nicht vorab vollständig geplant werden kann.