8bit-Computer: bo8h

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

von:   Josef Gnadl (bome)

Übersicht

Habe einen 8bit-Rechner entwickelt und als Prototyp-Gerät realisiert auf folgenden FPGA-Boards:

  • Spartan-3A Starter Kit von Xilinx
  • Spartan-3E Starter Board und
  • Nexys2 Board von Digilent
  • Terasic DE1 Board
  • Terasic DE0 Board
  • Terasic DE0-CV Board
  • Terasic DE0-nano Board
  • Waveshare OpenEP4CE10 Board

Die Realisierung auf dem Spartan-3A Starter Kit funktioniert auch auf dem Spartan-3AN Starter Kit.
Das Spartan-3E Starter Board von Digilent ist baugleich mit dem Spartan-3E Starter Kit von Xilinx.

Die CPU ist eine Eigenentwicklung. Merkmale des Gesamtsystems sind der aus mehreren 64KByte-Seiten bestehende Adressraum, das Steckplatz-Konzept, die an die Hardware angepasste Programmiersprache bola und taktgenau berechenbare Programmlaufzeiten. Der Zeichensatz beinhaltet einen hexadezimalen Ziffernsatz, welcher für die Ziffern A..F spezielle Zeichen verwendet.

Das Prototyp-Gerät besitzt Bildschirm und Tastatur, nutzt RS232 zum Daten-Austausch mit PC und hat eine SD-Karte mit selbstgestricktem Dateisystem. Der VHDL-Code ist so geschrieben, dass leicht weitere Schnittstellen eingebaut werden können.

  • Informationen und Downloads gibt es auf bo8h.de.
  • Kopie des Download-Files von bo8h.de: Datei:Bo8h.zip
  • Wiki-Artikel zur CPU: 8bit-CPU: bo8.
  • Beitrag im Forum Projekte & Code: 8bit-Computing mit FPGA.
  • Im embedded-projects-Journal 14  gibt es den Artikel Ein 8bit-Rechner auf dem Spartan-3A-Starterkit.

Das Prototyp-Gerät hat 4 freie virtuelle Steckplätze mit je 64KByte, wobei je 32KByte für Software vorgesehen sind. Damit haben Software-Entwickler die Möglichkeit, Steckkarten-Software in Eigen-Regie anzubieten. Zur Software-Entwicklung auf PC gibt es einen in C geschriebenen Cross-Assembler.


Die CPU

Der Punkt in den Mnemonics steht für einen auf der Grundlinie liegenden Bindestrich.

Der Akku A und das Erweiterungsregister B sind 8-bit.
Das Doppelregister AB wird abkürzend mit K bezeichnet.
Das Vorzeichenbit A7 wird abkürzend mit U bezeichnet.

Der Programmzähler P und die Adressregister X, Y, Z sind 16-bit.
Das Register Q erhält bei Sprüngen die Rückkehradresse P+1.
R ist Schleifen-Startadresse, S ist Schleifenzähler (16-bit).

Einziges Flag ist der Carry V. Bei bedingten Sprüngen kann ausserdem abgefragt werden, ob U/A/K Null sind.

Alle Speicherzugriffe sind 8bit-Zugriffe.
GTMX lädt nach A den Inhalt der Speicherzelle, auf welche X zeigt.
STMX speichert A in der Speicherzelle, auf welche X zeigt.

IXE inkrementiert X und tauscht A und B.
DXE dekrementiert X und tauscht A und B.

Damit lassen sich 2-byte-Speicherzugriffe aufbauen
nach dem Muster GTMX..IXE..GTMX und STMX..DXE..STMX.
Es gibt hierfür die Assembler-Makros GTMXI und STMXD.

GTMXI   lädt eine Adresse von Position X
ST.Y    überträgt die Adresse nach Y

GTMXI   lädt eine Adresse von Position X
AD.X    addiert X
ST.Y    überträgt die zu X relative Adresse nach Y

J..     Sprung zu der Adresse, welche in K steht

GTA 59  lädt den Wert 0059 nach K
AD. 35  addiert 35 zu A
J..     Sprung nach 3559

Das lässt sich kürzer schreiben mit Assembler-Makro
/GTA 3559
J

GTR 59  lädt den Wert P+0059+3 nach K
AD. 35  addiert 35 zu A
J..     relativer Sprung nach P+3559+3

Das lässt sich kürzer schreiben mit Assembler-Makro
/GTR 3559
J 

GT.Q  lädt Rückkehradresse nach K
J     Rücksprung aus Unterprogramm


Wenn ein Unterprogramm selber J.. ausführen will für andere Zwecke als für den Rücksprung, muss es vorher die Rückkehradresse sichern. Dazu kann es die Rückkehradresse aus Q laden, anfangs steht sie aber auch in K und das Laden kann entfallen.

Ausser dem Sprungbefehl J.. gibt es die bedingten Sprünge mit 8bit-Sprungdistanz:
O.cc nn (Vorwärtssprung falls cc) und B.cc nn (Rückwärtssprung falls cc).

Für schnelle Schleifen gibt es die Repeat-Befehle R.cc.
Repeat-Befehle gibt es kombiniert mit Schleifenzähler-Dekrementieren und Adresse-Inkrementieren/Dekrementieren. Die Schleifenstartadresse R wird gesetzt vor dem Schleifenanfang mittels S.RP oder mittels O.RP nn. Bei letzterem wird gleichzeitig ein Vorwärtssprung ausgeführt. Damit sind Schleifen mit Hineinsprung (while-Schleifen) realisierbar.

Bei J.. wird die Zieladresse auch nach R übertragen. Das garantiert, dass R stets eine Adresse im Nahbereich des Programmzählers enthält. Eine Anwendung hiervon ergibt sich beim Sprung in eine Tabelle, welche aus O.WY nn -Befehlen (Vorwärtssprung always) besteht, wobei die O.WY nn alle zur selben Adresse springen. Die dortige Routine kann durch Auswertung von R die Nummer des O.WY-Befehls ermitteln.

Adressierung mehrerer 64K-Seiten:

Zu jeder auf dem Adressbus ausgegebenen Adresse wird auf den Steuerleitungen angezeigt, von welchem der Adressregister P, X, Y, Z sie kommt. Dadurch kann eine externe Logik jedem der Adressregister eine eigene 64K-Speicherseite zuordnen. Die CPU kann spezielle Steuersignale ausgeben, welche die externe Logik zur Memory-Page-Umschaltung für eines der Adressregister veranlassen sollen.

Der Befehl H.. ist der einzige und universell einsetzbare IO-Befehl. Es wird K auf dem Adressbus ausgegeben und der Datenbus nach A eingelesen.

Wenn auf H.. ein J.. folgt, dann wirkt H.. stattdessen als Präfix und bewirkt, dass J.. mit einer Memory-Page-Umschaltung kombiniert wird. So kann die CPU von einer 64K-Seite in eine andere springen, ohne dass es einen gemeinsamen Speicherbereich geben muss.

Bei den Adressregistern X, Y, Z gibt es spezielle Befehle SW.X, SW.Y, SW.Z für die Memory-Page-Umschaltung.


Die Gesamt-Hardware

Es gibt vier Memory-Pages mit je 64KByte:

  • Page 0: Aktiver Steckplatz
  • Page 1: ROM (32K), Text-RAM (16K), Video-RAM (16K)
  • Page 2: Haupt-RAM
  • Page 3: Zusatz-RAM

Der aktive Steckplatz in Page 0 wird durch ein 3bit-Register SLT ausgewählt. Neben Page 0 wird auch IO-Adressraum zugeteilt. Die Steckkarten sollten ein Software-ROM und IO-Hardware enthalten.

Es gibt die 2bit-Register MP, MX, MY, MZ. Diese Register legen fest, in welche der vier Memory-Pages das betreffende Adressregister P, X, Y, Z zeigt. Ausserdem gibt es die Schattenregister NP, NX, NY, NZ. Das CPU-Signal zur Memory-Page-Umschaltung für ein bestimmtes Adressregister # bewirkt den Austausch von M# und N#, wobei # hier für P, X, Y, Z steht. Beim Sprung mit Memory-Page-Umschaltung  H..  J..  werden also MP und NP getauscht. Die Schattenregister NP, NX, NY, NZ können über IO-Befehle H.. mit einem Wert 0 bis 3 geladen werden.

Das Display ist 512 Pixel breit und 256 Pixel hoch, die Pixel sind zweiwertig. Der Video-Generator liest in Dauerschleife das Video-RAM und erzeugt daraus die VGA-Signale. Einen Hardware-Zeichengenerator gibt es nicht. Die Software liest die Zeichen aus dem Text-RAM und schreibt deren Pixelmuster ins Video-RAM. Die Textzeilen sind 64 Zeichen breit. Durch Invertierung eines Flipflops können Text-RAM und Video-RAM ihre Rollen tauschen. So kann die CPU im Text-RAM eine neue Grafik erstellen, während noch das alte Video-RAM angezeigt wird.


Die Programmiersprache

Der Compiler erzeugt einen Zwischencode, welcher interpretativ abgearbeitet wird. Der Zwischencode besteht aus Aufrufen vorgefertigter Maschinenroutinen. Nur wenige dieser Maschinenroutinen sind interner Teil der Sprache, die meisten liegen in den 8 Steckkarten.

Die Sprache ist strikt imperativ. Es besteht eine direkte Eins-zu-Eins-Entsprechung zwischen dem Quelltext und der Folge der Operationen im Zwischencode. Es gibt im Quelltext keine arithmetischen oder logischen Ausdrücke.

Es gibt in dieser Sprache kein goto, kein break, kein continue, und in jedem Programm oder Unterprogramm genau 1 return, nämlich am Ende des Programms oder Unterprogramms.

Die Strukturelemente sind folgende:

Bedingte Anweisung:  IF.c  ...  ENDF
Verzweigung:         IF.c  ...  ELSE  ...  ENDE
Case-Struktur:       CASE /nn  ...  NEXT /nn  ...  LAST
Do-while-Schleife:   LOOP  ...  RP.c
While-Schleife:      L.JP  ...  HERE  ...  RP.c

c steht hier für die Sprungbedingung, nn steht für eine Zahl fallend bis 00, NEXT /nn ist entsprechend mehrmals anzugeben.
RP.c steht für wiederhole falls c.

Ebenso wie die Verzweigung zählt die Schleife mit Hineinsprung

L.JP ... HERE ... RP.c

als ein Block mit zwei Unterblöcken, die strenge Blockstruktur wird durch den Hineinsprung nicht durchbrochen.

Die Blockstruktur wird bei folgenden Operationen gebraucht:

ACQU  ...  UACQ
FASN  ...  UFAS
LN.1  ...  RELS    //    LN.2  ...  RELS
LX.1  ...  RELS    //    LX.2  ...  RELS

ACQU steht für Aquirieren von Residenten Variablen
UACU macht das Aquirieren rückgängig

Residente Variable sind Variable, zB. vom Typ databox, welche ausserhalb des Programms dauerhaft im RAM liegen. Sie sind über ihren Namen auffindbar und werden über ihre Nummer adressiert. ACQU bindet freie Zeiger, welche im Programm vereinbart sind, an diese externen Variablen. Dadurch sind diese Variablen unter Umgehung des Namens und der Nummer wie gewöhnliche im Programm vereinbarte Variable verfügbar.

FASN vergibt den Status "befestigt" an Variable vom Typ databox
UFAS macht die Vergabe rückgängig.

Variable vom Typ databox, welche beim Start des Programms automatisch installiert werden oder danach durch LN/LX installiert werden, sind von sich aus "befestigt". Das Installieren kann aber auch durch Steckkarten-Kommandos erfolgen, zB. wenn die Größe der databox erst zur Laufzeit des Programms bekannt wird. Dann sind sie zunächst unbefestigt.

LN.1 leiht den von einer databox in RAM1 belegten Speicherplatz an mehrere Variable aus, welche vorher als nicht installiert vereinbart wurden und nach dem Ausleihen den Speicherplatz der databox überdecken.

LX.1 leiht einen Teil des Speicherplatzes einer databox ab einem Startindex an mehrere Variable aus.

Entsprechend LN.2 und LX.2 für RAM2. In RAM1 sind die mehreren Variablen kurze Variable vom Typ data, oder selber wieder vom Typ databox, in RAM2 sind sie stets wieder vom Typ databox.

Beim Ausleihen aus einer databox prüft LN/LX, ob die databox befestigt ist. Andernfalls wäre sie verschiebbar oder könnte uninstalliert werden, so dass Schreibzugriffe auf die durch das Ausleihen erzeugten Variablen Unheil anrichten könnten.

RELS macht das Ausleihen rückgängig.

Bei den Operationen ACQU, FASN, LN/LX garantiert der Compiler, dass das zugehörige UACQ, UFAS, RELS im selben Block liegt. Die jeweilige Operation und das zugehörige Rückgängigmachen bilden selber wieder einen Block.


Ergänzung zu Sprungbedingungen:

Es kann abgefragt werden, ob eine 1-oder 2-Byte-Variable Null ist und ob sie gleich ff bzw. ffff ist.

Die Kommandos =TT.Z und =TT.M können diese Bedingungen speichern in einem speziellen Byte im RAM, dem TF-Merker. Dazu schieben sie eine 1 oder 0 von oben auf den TF-Merker. In gleicher Weise können Steckkarten-Kommandos nCOMMAND beliebige Bedingungen ermitteln und speichern.

Es gibt bedingte Sprünge, welche die obersten 3 Bit des TF-Merker mittels einer Wahrheitstabelle abfragen. Dabei können bis zu 3 Bits wieder vom TF-Merker entfernt werden.

Mittels der Routinen =PP.r, =PZ.r, =PM.r ist es möglich, solche Bedingungen in einer 1- oder 2-Byte-Variablen zu speichern, zu verANDen und zu verORen. Damit lassen sich beliebig komplexe Sprungbedingungen berechnen.


Unterprogramme:

In der Programmiersprache gibt es keine functions mit Rückgabewert, sondern nur einfache Unterprogramme, bei welchen alle Parameter gleichberechtigt sind. Es werden ausschließlich Zeiger übergeben.


Variablen vom Typ FIX:

Ihre Werte sind Teil des Programmcodes und liegen dort am Ende. Der gesamte Speicherbereich dieser Werte kann als "FIXBOX" vom Programm abgetrennt werden und dann mittels eines speziellen Werkzeugs bearbeitet werden.