Projekt | Vierter SoC in VHDL geschrieben |
---|---|
Autor | Richard James Howe |
Copyright | 2013–2019 Richard Howe |
Lizenz | MIT/LGPL |
[email protected] |
Dieses Projekt implementiert einen kleinen Stack-Computer, der auf die Ausführung von Forth auf Basis der J1-CPU zugeschnitten ist. Der Prozessor wurde in VHDL von Verilog neu geschrieben und leicht erweitert.
Die Ziele des Projekts sind wie folgt:
Alle drei sind abgeschlossen.
Der H2-Prozessor ist wie der J1 ein Stack-basierter Prozessor, der einen speziell für FORTH geeigneten Befehlssatz ausführt.
Das aktuelle Ziel ist das Nexys3-Board mit einem Xilinx Spartan-6 XC6LX16-CS324 FPGA. In Zukunft werden neue Boards ins Visier genommen, da dieses Board das Ende seiner Lebensdauer erreicht. Das VHDL ist generisch geschrieben, wobei Hardwarekomponenten abgeleitet und nicht explizit instanziiert werden. Dies sollte den Code einigermaßen portierbar machen, obwohl die Schnittstellen zu den Nexys3-Board-Komponenten spezifisch für die Peripheriegeräte auf diesem Board sind.
Ein Video des Projekts in Aktion auf der Hardware kann hier angesehen werden:
Der SoC kann auch mit einem in C geschriebenen Simulator simuliert werden, wie unten gezeigt:
Die Systemarchitektur ist wie folgt:
Die vom Projekt verwendeten Lizenzen sind gemischt und gelten pro Datei. Für meinen Code verwende ich die MIT-Lizenz – Sie können ihn also gerne nach Ihren Wünschen verwenden. Die anderen verwendeten Lizenzen sind die LGPL- und die Apache 2.0-Lizenz. Sie sind auf einzelne Module beschränkt und können daher entfernt werden, wenn Sie eine gewisse Abneigung gegen LGPL-Code haben.
Das einzige derzeit verfügbare Zielboard ist das Nexys3. Dies sollte sich in Zukunft ändern, da das Board derzeit das Ende seiner Lebensdauer erreicht hat. Die nächsten Boards, die ich unterstützen möchte, sind sein Nachfolger, das Nexys 4, und das myStorm BlackIce (https://mystorm.uk/). Das myStorm-Board verwendet eine vollständig Open-Source-Toolchain für die Synthese, Ortung und Route sowie die Generierung von Bitdateien.
Der Build wurde unter Debian Linux, Version 8, getestet.
Sie benötigen:
Hardware:
Xilinx ISE kann (oder könnte) kostenlos heruntergeladen werden, erfordert jedoch eine Registrierung. ISE muss auf Ihrem Weg sein:
PATH=$PATH:/opt/Xilinx/14.7/ISE_DS/ISE/bin/lin64;
PATH=$PATH:/opt/Xilinx/14.7/ISE_DS/ISE/lib/lin64;
So erstellen Sie die C-basierte Toolchain:
make embed.hex
So erstellen Sie eine Bitdatei, die auf die Zielplatine geflasht werden kann:
make simulation synthesis implementation bitfile
So laden Sie die Bitdatei auf das Zielboard hoch:
make upload
So zeigen Sie die durch „Simulation erstellen“ erzeugte Wellenform an:
make viewer
Der C-basierte CLI-Simulator kann aufgerufen werden mit:
make run
Dadurch wird die H2 Forth-Quelldatei „embed.fth“ zusammengestellt und die zusammengestellte Objektdatei unter dem H2-Simulator mit aktiviertem Debugger ausgeführt. Ein grafischer Simulator kann ausgeführt werden mit:
make gui-run
Was sowohl Freeglut als auch einen C-Compiler erfordert.
Das Original-J1-Projekt ist verfügbar unter:
Dieses Projekt zielt auf den ursprünglichen J1-Kern ab und stellt eine eForth-Implementierung bereit (geschrieben mit Gforth für Meta-Kompilierung/Cross-Kompilierung zum J1-Kern). Es stellt auch einen in C geschriebenen Simulator für das System bereit.
Den eForth-Interpreter, auf dem der Meta-Compiler basiert, finden Sie unter:
Der H2-Prozessor und die zugehörigen Peripheriegeräte sind jetzt recht stabil, die Quelle ist jedoch immer der endgültige Leitfaden für das Verhalten von Befehlen und Peripheriegeräten sowie für die Registerzuordnung.
Es gibt einige Modifikationen an der J1-CPU, darunter:
Die H2-CPU verhält sich sehr ähnlich wie die J1-CPU, und das J1-PDF kann gelesen werden, um diesen Prozessor besser zu verstehen. Der Prozessor ist ein 16-Bit-Prozessor mit Anweisungen, die einen einzigen Taktzyklus benötigen. Die meisten der primitiven Forth-Wörter können auch in einem einzigen Zyklus ausgeführt werden, eine bemerkenswerte Ausnahme ist Store („!“), der in zwei Anweisungen aufgeteilt ist.
Die CPU hat den folgenden Zustand:
Lädt und speichert in den Block-RAM, der das H2-Programm enthält, verwirft das niedrigste Bit, jede andere Speicheroperation verwendet das niedrigere Bit (z. B. Sprünge und Laden und Speichern in Eingabe-/Ausgabe-Peripheriegeräten). Auf diese Weise können Anwendungen beim Zugriff auf den Programm-RAM das niedrigste Bit für Zeichenoperationen verwenden.
Der Befehlssatz wird auf folgende Weise dekodiert:
+---------------------------------------------------------------+
| F | E | D | C | B | A | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
+---------------------------------------------------------------+
| 1 | LITERAL VALUE |
+---------------------------------------------------------------+
| 0 | 0 | 0 | BRANCH TARGET ADDRESS |
+---------------------------------------------------------------+
| 0 | 0 | 1 | CONDITIONAL BRANCH TARGET ADDRESS |
+---------------------------------------------------------------+
| 0 | 1 | 0 | CALL TARGET ADDRESS |
+---------------------------------------------------------------+
| 0 | 1 | 1 | ALU OPERATION |T2N|T2R|N2A|R2P| RSTACK| DSTACK|
+---------------------------------------------------------------+
| F | E | D | C | B | A | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
+---------------------------------------------------------------+
T : Top of data stack
N : Next on data stack
PC : Program Counter
LITERAL VALUES : push a value onto the data stack
CONDITIONAL : BRANCHS pop and test the T
CALLS : PC+1 onto the return stack
T2N : Move T to N
T2R : Move T to top of return stack
N2A : STORE T to memory location addressed by N
R2P : Move top of return stack to PC
RSTACK and DSTACK are signed values (twos compliment) that are
the stack delta (the amount to increment or decrement the stack
by for their respective stacks: return and data)
Alle ALU-Operationen ersetzen T:
Wert | Betrieb | Beschreibung |
---|---|---|
0 | T | Oben im Stapel |
1 | N | Kopieren Sie T nach N |
2 | T + N | Zusatz |
3 | T & N | Bitweises UND |
4 | Zerrissen | Bitweises ODER |
5 | T ^ N | Bitweises XOR |
6 | ~T | Bitweise Inversion |
7 | T = N | Gleichheitstest |
8 | N < T | Signierter Vergleich |
9 | N >> T | Logische Rechtsverschiebung |
10 | T - 1 | Dekrementieren |
11 | R | Oben auf dem Rückgabestapel |
12 | [T] | Von Adresse laden |
13 | N << T | Logische Linksverschiebung |
14 | Tiefe | Stapeltiefe |
15 | N u< T | Vergleich ohne Vorzeichen |
16 | CPU-Status festlegen | Interrupts aktivieren |
17 | CPU-Status abrufen | Sind Interrupts aktiviert? |
18 | Tiefe | Rücklauftiefe stk |
19 | 0= | T == 0? |
20 | CPU-ID | CPU-ID |
21 | Wörtlich | Interne Anweisung |
Register mit dem Präfix „o“ sind Ausgaberegister, Register mit dem Präfix „i“ sind Eingaberegister. Register sind in einen Eingabe- und Ausgaberegisterabschnitt unterteilt und die Adressen der Eingabe- und Ausgaberegister stimmen nicht in allen Fällen überein.
Die folgenden Peripheriegeräte wurden im VHDL-SoC implementiert, um mit Geräten auf dem Nexys3-Board zu kommunizieren:
Der SoC verfügt außerdem über eine begrenzte Anzahl von Interrupts, die aktiviert oder deaktiviert werden können.
Die Ausgaberegisterzuordnung:
Registrieren | Adresse | Beschreibung |
---|---|---|
oUart | 0x4000 | UART-Register |
oVT100 | 0x4002 | VT100-Terminal-Schreiben |
oLeds | 0x4004 | LED-Ausgänge |
oTimerStrg | 0x4006 | Timer-Steuerung |
oMemDout | 0x4008 | Speicherdatenausgabe |
oMemControl | 0x400A | Speichersteuerung / Hi-Adresse |
oMemAddrLow | 0x400C | Speicher-Lo-Adresse |
o7SegLED | 0x400E | 4 x LED 7-Segment-Anzeige |
oIrcMask | 0x4010 | CPU-Interrupt-Maske |
oUartBaudTx | 0x4012 | UART-Tx-Baud-Takteinstellung |
oUartBaudRx | 0x4014 | UART-Rx-Baud-Takteinstellung |
Die Eingaberegister:
Registrieren | Adresse | Beschreibung |
---|---|---|
iUart | 0x4000 | UART-Register |
iVT100 | 0x4002 | Terminalstatus und PS/2-Tastatur |
iSwitches | 0x4004 | Knöpfe und Schalter |
iTimerDin | 0x4006 | Aktueller Timerwert |
iMemDin | 0x4008 | Speicherdateneingabe |
Die folgende Beschreibung der Register sollte der Reihe nach gelesen werden und auch die Funktionsweise der Peripheriegeräte beschreiben.
Auf dem SoC ist ein UART mit fester Baudrate und Format (115200, 8 Bit, 1 Stoppbit) vorhanden. Der UART verfügt sowohl auf dem RX- als auch auf dem TX-Kanal über einen FIFO der Tiefe 8. Die Steuerung des UART ist auf oUart und iUart aufgeteilt.
Um einen Wert in den UART zu schreiben, aktivieren Sie TXWE und legen Sie die Daten in TXDO ab. Der FIFO-Status kann durch einen Blick auf das iUart-Register analysiert werden.
So lesen Sie einen Wert aus dem UART: iUart kann überprüft werden, um festzustellen, ob Daten im FIFO vorhanden sind. Wenn RXRE im oUart-Register aktiviert ist, sind die Daten beim nächsten Taktzyklus im iUart-Register vorhanden.
Die Baudrate des UART kann durch Neuaufbau des VHDL-Projekts geändert werden, Bitlänge, Paritätsbits und Stoppbits können nur durch Änderungen an uart.vhd geändert werden
+-------------------------------------------------------------------------------+
| 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
+-------------------------------------------------------------------------------+
| X | X |TXWE| X | X |RXRE| X | X | TXDO |
+-------------------------------------------------------------------------------+
TXWE: UART TX Write Enable
RXRE: UART RX Read Enable
TXDO: UART TX Data Output
Das VGA-Textgerät emuliert ein Terminal, mit dem der Benutzer durch Schreiben in das oVT100-Register kommunizieren kann. Es unterstützt einen Teil der Funktionalität des VT100-Terminals. Die Schnittstelle verhält sich ähnlich wie das Schreiben auf einen UART mit denselben Busy- und Steuersignalen. Die Eingabe erfolgt über eine auf der Platine vorhandene PS/2-Tastatur, diese verhält sich wie der RX-Mechanismus des UART.
+-------------------------------------------------------------------------------+
| 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
+-------------------------------------------------------------------------------+
| X | X |TXWE| X | X |RXRE| X | X | TXDO |
+-------------------------------------------------------------------------------+
TXWE: VT100 TX Write Enable
RXRE: UART RX Read Enable
TXDO: UART TX Data Output
Auf der Nexys3-Karte gibt es eine Reihe von LEDs, die sich neben den Schaltern befinden. Diese LEDs können durch Schreiben in LEDO ein- (1) oder ausgeschaltet (0) werden. Jede LED hier entspricht dem Schalter, neben dem sie steht.
+-------------------------------------------------------------------------------+
| 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
+-------------------------------------------------------------------------------+
| X | X | X | X | X | X | X | X | LEDO |
+-------------------------------------------------------------------------------+
LEDO: LED Output
Der Timer ist über das oTimerCtrl-Register steuerbar, es handelt sich um einen 13-Bit-Timer mit 100 MHz, er kann optional Interrupts generieren und der aktuelle interne Zählerstand des Timers kann mit dem iTimerDin-Register zurückgelesen werden.
Der Timer zählt, sobald das TE-Bit aktiviert ist. Sobald der Timer den TCMP-Wert erreicht, läuft er um und kann optional einen Interrupt generieren, indem INTE aktiviert wird. Dadurch werden auch die Q- und NQ-Leitungen umgeschaltet, die aus dem Timer kommen und zu Pins auf der Platine geleitet werden (siehe die Einschränkungsdatei top.ucf für die Pins).
Der Timer kann durch Schreiben in RST zurückgesetzt werden.
+-------------------------------------------------------------------------------+
| 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
+-------------------------------------------------------------------------------+
| TE | RST|INTE| TCMP |
+-------------------------------------------------------------------------------+
TE: Timer Enable
RST: Timer Reset
INTE: Interrupt Enable
TCMP: Timer Compare Value
Der H2-Kern verfügt über einen Mechanismus für Interrupts. Interrupts müssen mit einem Befehl aktiviert oder deaktiviert werden. Jeder Interrupt kann mit einem Bit im IMSK maskiert werden, um diesen spezifischen Interrupt zu aktivieren. Eine „1“ in einem Bit von IMSK aktiviert diesen spezifischen Interrupt, der an die CPU übermittelt wird, wenn darin Interrupts aktiviert sind.
+-------------------------------------------------------------------------------+
| 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
+-------------------------------------------------------------------------------+
| X | X | X | X | X | X | X | X | IMSK |
+-------------------------------------------------------------------------------+
IMSK: Interrupt Mask
Dieses Register wird verwendet, um die Baud- und Abtasttaktfrequenz nur für die Übertragung festzulegen.
+-------------------------------------------------------------------------------+
| 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
+-------------------------------------------------------------------------------+
| BTXC |
+-------------------------------------------------------------------------------+
BTXC: Baud Clock Settings
Dieses Register wird verwendet, um die Baud- und Sample-Taktfrequenz nur für den Empfang einzustellen.
+-------------------------------------------------------------------------------+
| 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
+-------------------------------------------------------------------------------+
| BRXC |
+-------------------------------------------------------------------------------+
BRXC: Baud Clock Settings
Daten, die an die ausgewählte Adresse ausgegeben werden sollen, wenn die Schreibfreigabe (WE) in oMemControl erteilt wird.
+-------------------------------------------------------------------------------+
| 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
+-------------------------------------------------------------------------------+
| Data Ouput |
+-------------------------------------------------------------------------------+
Dieses Register enthält die Steuerregister für den Onboard-Speicher auf dem Nexys3-Board. Die Platine enthält drei Speichergeräte, zwei nichtflüchtige Speichergeräte und ein flüchtiges RAM-basiertes Gerät. Die beiden Geräte, auf die über eine einfache SRAM-Schnittstelle zugegriffen werden kann (ein flüchtiges M45W8MW16, ein nichtflüchtiges – ein NP8P128A13T1760E), sind beide zugänglich, das dritte ist ein SPI-basiertes Speichergerät (NP5Q128A13ESFC0E) und ist derzeit nicht zugänglich.
+-------------------------------------------------------------------------------+
| 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
+-------------------------------------------------------------------------------+
| OE | WE | RST|WAIT| RCS| FCS| Address Hi |
+-------------------------------------------------------------------------------+
OE: Output Enable - enable reading from current address into iMemDin
WE: Write Enable - enable writing oMemDout into ram at current address
RST: Reset the Flash memory controller
RCS: RAM Chip Select, Enable Volatile Memory
FCS: Flash Chip Select, Enable Non-Volatile Memory
Address Hi: High Bits of RAM address
OE und WE schließen sich gegenseitig aus. Wenn beide festgelegt sind, hat dies keine Auswirkung.
Der Speichercontroller befindet sich in der aktiven Entwicklung und die Schnittstelle dazu kann sich ändern.
Dies sind die unteren Adressbits des RAM.
+-------------------------------------------------------------------------------+
| 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
+-------------------------------------------------------------------------------+
| Address Lo |
+-------------------------------------------------------------------------------+
Auf der Nexys3-Karte gibt es eine Reihe von 7-Segment-Anzeigen mit einem Dezimalpunkt (eigentlich 8-Segment), die für die numerische Ausgabe verwendet werden können. Die LED-Segmente können nicht direkt angesprochen werden. Stattdessen wird der in L8SD gespeicherte Wert auf einen hexadezimalen Anzeigewert (oder einen BCD-Wert, dies erfordert jedoch eine Neugenerierung des SoC und eine Änderung eines Generic im VHDL) abgebildet.
Der Wert „0“ entspricht einer auf dem LED-Segment angezeigten Null, „15“ einem „F“ usw.
Es gibt 4 Displays hintereinander.
+-------------------------------------------------------------------------------+
| 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
+-------------------------------------------------------------------------------+
| L7SD0 | L7SD1 | L7SD2 | L7SD3 |
+-------------------------------------------------------------------------------+
L7SD0: LED 7 Segment Display (leftmost display)
L7SD1: LED 7 Segment Display
L7SD2: LED 7 Segment Display
L7SD3: LED 7 Segment Display (right most display)
Das iUart-Register arbeitet in Verbindung mit dem oUart-Register. Der Status des FIFO, der sowohl das Senden als auch den Empfang von Bytes puffert, ist im iUart-Register verfügbar, ebenso wie alle empfangenen Bytes.
+-------------------------------------------------------------------------------+
| 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
+-------------------------------------------------------------------------------+
| X | X | X |TFFL|TFEM| X |RFFL|RFEM| RXDI |
+-------------------------------------------------------------------------------+
TFFL: UART TX FIFO Full
TFEM: UART TX FIFO Empty
RFFL: UART RX FIFO Full
RFEM: UART RX FIFO Empty
RXDI: UART RX Data Input
Das iVT100-Register arbeitet in Verbindung mit dem oVT100-Register. Der Status des FIFO, der sowohl das Senden als auch den Empfang von Bytes puffert, ist im iVT100-Register verfügbar, ebenso wie alle empfangenen Bytes. Es funktioniert genauso wie die iUart/oUart-Register.
+-------------------------------------------------------------------------------+
| 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
+-------------------------------------------------------------------------------+
| X | X | X |TFFL|TFEM| X |RFFL|RFEM| 0 | ACHR |
+-------------------------------------------------------------------------------+
TFFL: VGA VT100 TX FIFO Full
TFEM: VGA VT100 TX FIFO Empty
RFFL: PS2 VT100 RX FIFO Full
RFEM: PS2 VT100 RX FIFO Empty
ACHR: New character available on PS2 Keyboard
Dieses Register enthält den aktuellen Wert des Timer-Zählers.
+-------------------------------------------------------------------------------+
| 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
+-------------------------------------------------------------------------------+
| X | X | X | TCNT |
+-------------------------------------------------------------------------------+
TCNT: Timer Counter Value
iSwitches enthält Eingabezeilen aus mehreren Quellen. Die Tasten (BUP, BDWN, BLFT, BRGH und BCNT) entsprechen einem D-Pad auf der Nexys3-Platine. Bei den Schaltern (TSWI) handelt es sich um die in oLeds genannten, jeweils mit einer LED daneben.
Die Schalter und Taster sind bereits in der Hardware entprellt, so dass sie nach dem Einlesen aus diesen Registern nicht weiter verarbeitet werden müssen.
+-------------------------------------------------------------------------------+
| 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
+-------------------------------------------------------------------------------+
| X | X | X | BUP|BDWN|BLFT|BRGH|BCNT| TSWI |
+-------------------------------------------------------------------------------+
BUP: Button Up
BDWN: Button Down
BLFT: Button Left
BRGH: Button Right
BCNT: Button Center
TSWI: Two Position Switches
Speichereingabe, entweder vom SRAM oder Flash, indiziert durch oMemControl und oMemAddrLow. Beim Lesen aus Flash kann es sich tatsächlich um Statusinformationen oder Informationen aus der Abfragetabelle handeln.
+-------------------------------------------------------------------------------+
| 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
+-------------------------------------------------------------------------------+
| Data Input |
+-------------------------------------------------------------------------------+
Die folgenden Interrupt-Serviceroutinen sind definiert:
Name | Nummer | Beschreibung |
---|---|---|
isrNone | 0 | Nicht verwendet |
isrRxFifoNotEmpty | 1 | UART RX FIFO ist nicht leer |
isrRxFifoFull | 2 | UART RX FIFI ist voll |
isrTxFifoNotEmpty | 3 | UART-TX-FIFO ist nicht leer |
isrTxFifoFull | 4 | UART-TX-FIFO ist voll |
isrKbdNeu | 5 | Neuer PS/2-Tastaturcharakter |
isrTimer | 6 | Timer-Zähler |
isrDPadButton | 7 | Der Status einer beliebigen D-Pad-Taste ändert sich |
Wenn ein Interrupt auftritt und Interrupts im Prozessor aktiviert sind, wird ein Aufruf an den Speicherort im Speicher ausgeführt – der Speicherort ist derselbe wie die ISR-Nummer. Ein ISR mit der Nummer „4“ führt beispielsweise einen Aufruf (keinen Sprung) zur Position „4“ im Speicher durch.
Interrupts haben eine Latenz von mindestens 4–5 Zyklen, bevor auf sie reagiert wird. Es gibt eine Verzögerung von zwei bis drei Zyklen im Interrupt-Request-Handler, dann muss der Aufruf an den ISR-Speicherort im Speicher erfolgen, dann der Aufruf an den Wort, das den ISR selbst implementiert.
Wenn zwei Interrupts gleichzeitig auftreten, werden sie von der niedrigsten zur höchsten Interrupt-Nummer abgearbeitet.
Interrupts gehen verloren, wenn ein Interrupt mit derselben Nummer auftritt, der nicht verarbeitet wurde.
Der Disassembler und der C-basierte Simulator für H2 befinden sich in einem einzigen Programm (siehe h2.c). Dieser Simulator ergänzt den VHDL-Prüfstand tb.vhd und ist kein Ersatz dafür. Der Meta-Compiler läuft auf einem eForth-Interpreter und ist in den Dateien embed.c und embed.blk enthalten. Der Meta-Compiler (Forth-Sprache für einen Cross-Compiler) ist ein Forth-Programm, das zum Erstellen des eForth-Images verwendet wird, das auf dem Ziel ausgeführt wird.
Die Toolchain befindet sich derzeit im Wandel. In Zukunft wird es wahrscheinlich zu einer stärkeren Integration zwischen h2.c und embed.c kommen, zusammen mit der Änderung der Embed Virtual Machine in eine, die der H2-CPU ähnlicher ist, mit dem langfristigen Ziel, ein Selbsthosting zu schaffen System.
Um beides zu erstellen, wird ein C-Compiler benötigt, das Build-Ziel „h2“ erstellt die ausführbare Datei, h2, und „embed“ erstellt den Meta-Compiler:
make h2 embed
Und es kann auf der Quelldatei „embed.fth“ mit dem Ziel „make“ ausgeführt werden:
make run
Die Make-Datei wird nicht benötigt:
Linux:
cc -std=c99 h2.c -o h2 # To build the h2 executable
cc -std=c99 embed.c -o embed # To build the embed VM executable
./embed embed.blk embed.hex embed.fth # Create the target eForth image
./h2 -h # For a list of options
./h2 -r embed.hex # Run the assembled file
Windows:
gcc -std=c99 h2.c -o h2.exe # Builds the h2.exe executable
gcc -std=c99 embed.c -o embed.exe # Builds the embed.exe executable
embed.exe embed.blk embed.hex embed.fth # Create the target eForth iamge
h2.exe -h # For a list of options
h2.exe -r embed.hex # Run the assembled file
Eine Liste der verfügbaren Befehlszeilenoptionen:
- stop processing options, following arguments are files
-h print a help message and exit
-v increase logging level
-d disassemble input files (default)
-D full disassembly of input files
-T Enter debug mode when running simulation
-r run hex file
-L # load symbol file
-s # number of steps to run simulation (0 = forever)
-n # specify NVRAM block file (default is nvram.blk)
file* file to process
Dieses Programm steht unter der MIT-Lizenz. Sie können es jederzeit verwenden und nach Belieben ändern. Mit minimalen Modifikationen sollte es in der Lage sein, Programme für den ursprünglichen J1-Kern zusammenzustellen.
Der Meta-Compiler läuft auf der eingebetteten virtuellen Maschine, es handelt sich um eine virtuelle 16-Bit-Maschine, die ursprünglich von der H2-CPU abstammt. Das Projekt umfasst ein Metakompilierungsschema, das es einem eForth-Image ermöglicht, mit Modifikationen ein neues eForth-Image zu generieren. Dieses System wurde für die Verwendung mit dem H2 angepasst, wodurch der in C geschriebene Cross-Compiler ersetzt wurde, der die Erstellung des ersten Images für den H2 ermöglichte.
Der Meta-Compiler ist ein gewöhnliches Forth-Programm, es ist in embed.fth enthalten. Das Meta-Compiler-Forth-Programm wird dann verwendet, um ein eForth-Image zu erstellen, das auf dem H2-Ziel ausgeführt werden kann.
Weitere Informationen zur Metakompilierung in Forth finden Sie unter:
Der Disassembler nimmt eine Textdatei mit dem zusammengestellten Programm, das aus 16-Bit-Hexadezimalzahlen besteht. Anschließend wird versucht, die Anweisungen zu zerlegen. Es kann auch eine Symboldatei eingegeben werden, die vom Assembler generiert werden kann und versucht, die Orte zu finden, auf die Sprünge und Aufrufe verweisen.
Der Disassembler wird von einem von GTKwave aufgerufenen TCL-Skript verwendet. Er wandelt die Befehlsspur des H2 aus einer Reihe von Zahlen in die Anweisungen und Verzweigungsziele um, die sie darstellen. Dies erleichtert das Debuggen der VHDL erheblich.
Die violette Spur zeigt die zerlegte Anleitung.
Der Simulator in C implementiert den H2-Kern und den größten Teil des SoC. Die E/A für den Simulator ist nicht zyklusgenau, kann aber zum Ausführen und Debuggen von Programmen mit Ergebnissen verwendet werden, die dem Verhalten der Hardware sehr ähnlich sind. Dies ist viel schneller als die Neuerstellung der Bitdatei, die zum Flashen des FPGA verwendet wurde.
Der Simulator enthält außerdem einen Debugger, der dem unter DOS verfügbaren Programm DEBUG.COM ähnelt. Mit dem Debugger können Sie Speicherabschnitte zerlegen, den Status der Peripheriegeräte überprüfen und Speicherabschnitte auf dem Bildschirm ausgeben. Es kann auch verwendet werden, um Haltepunkte zu setzen und den Code in Einzelschritten auszuführen, bis ein Haltepunkt erreicht wird.
Um den Debugger auszuführen, muss entweder eine Hex-Datei oder eine Quelldatei angegeben werden:
# -T turns debugging mode on
./h2 -T -r file.hex # Run simulator
Beide Betriebsarten können durch eine Symboldatei erweitert werden, die auflistet, wo sich Variablen, Beschriftungen und Funktionen im zusammengebauten Kern befinden.
Wenn die Option „-T“ angegeben ist, wird vor der Ausführung der Simulation in den Debugmodus gewechselt. Es sollte eine Eingabeaufforderung erscheinen und die Befehlszeile sollte wie folgt aussehen:
$ ./h2 -T -R h2.fth
Debugger running, type 'h' for a list of command
debug>
Haltepunkte können entweder symbolisch oder per Programmspeicherort gesetzt werden, zum Setzen von Haltepunkten wird der Befehl „b“ verwendet:
Zahlen können oktal (der Zahl vorangestellt „0“), hexadezimal (vorgestellt „0x“) oder dezimal eingegeben werden. Beispielsweise setzen die folgenden drei Debug-Befehle alle einen Haltepunkt an derselben Stelle:
debug> b 16
debug> b 0x10
debug> b 020
Mit „k“ können die aktuell gesetzten Haltepunkte aufgelistet werden:
debug> k
0x0010
Dies setzt einen Haltepunkt, wenn die Funktion „Taste?“ heißt:
debug> b key?
Sowohl Funktionen als auch Beschriftungen können angehalten werden. Dazu muss entweder eine Symboldatei in der Befehlszeile angegeben werden oder assembliert und ausgeführt werden, um für die Verwendung in einer Quelldatei und nicht in einer Hex-Datei verwendet zu werden. Symboldateien können für Quell- oder Hex-Dateien verwendet werden.
Für einen einzelnen Schritt kann der Befehl „s“ gegeben werden, allerdings passiert nicht viel, wenn die Ablaufverfolgung ausgeschaltet ist (die Ablaufverfolgung ist standardmäßig ausgeschaltet). Die Ablaufverfolgung kann mit dem Befehl „t“ ein- oder ausgeschaltet werden:
debug> s
debug> s
debug> t
trace on
debug> s
0001: pc(089a) inst(4889) sp(0) rp(0) tos(0000) r(0000) call 889 init
debug> s
0002: pc(0889) inst(807a) sp(0) rp(1) tos(0000) r(089b) 7a
debug> s
0003: pc(088a) inst(e004) sp(1) rp(1) tos(007a) r(089b) 6004
Es empfiehlt sich, die Ablaufverfolgung auszuschalten, wenn Sie den Befehl „c“ oder „continue“ ausführen.
Der '.' Mit dem Befehl kann der interne Status der H2-Kerne angezeigt werden:
debug> .
Return Stack:
0000: 0000 08aa 0883 017b 0000 031b 0000 ffb0 0000 02eb ffb5 0210 0167 0167
0167 0167
0010: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
0000 0000
Variable Stack:
tos: 0000
0001: 0000 0000 0000 0001 0004 0005 0000 ffb0 0000 0000 0000 0000 0000 0000
0000 0000
0011: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
0000 0000
pc: 0538
rp: 0001
dp: 0000
ie: false
Und mit dem Befehl „p“ kann der Zustand der simulierten Peripheriegeräte angezeigt werden:
debug> p
LEDS: 00
VGA Cursor: 0005
VGA Control: 007a
Timer Control: 8032
Timer: 001b
IRC Mask: 0000
UART Input: 6c
LED 7seg: 0005
Switches: 00
LFSR: 40ba
Waiting: false
Für eine vollständige Liste der Befehle verwenden Sie den Befehl „h“.
Andere Möglichkeiten, in den Debug-Modus zu gelangen, umfassen das Einfügen der Assembler-Direktive „.break“ in den Quellcode (dies funktioniert nur, wenn der Befehl „Assemble and Run“ für Quelldateien verwendet wird, nicht für Hex-Dateien) und das Drücken des Escape-Zeichens, wenn der Simulator aktiv ist versucht, Daten über die simulierte UART- oder PS/2-Tastatur zu lesen (das Escape wird weiterhin an den Simulator weitergeleitet, aktiviert aber auch den Debug-Modus).
Ein eigenes Programm kann kompiliert und unter Linux und Windows getestet werden. Dies simuliert die Peripheriegeräte des Nexys3-Boards, mit denen der SoC kommuniziert, bietet jedoch im Gegensatz zum Befehlszeilenprogramm eine grafische Umgebung. Es ist einfacher, mit dem Gerät zu interagieren und zu sehen, was es tut, aber die Debugging-Sitzungen sind weniger kontrolliert. Es erfordert freies Überangebot.
Unten sehen Sie ein Bild einer laufenden Sitzung im GUI-Simulator:
Bauen lässt sich damit machen
make gui
Und läuft:
make gui-run
Oder:
./gui h2.hex (on Linux)
gui.exe h2.hex (on Windows)
Der Linux-Build sollte funktionieren, wenn das Entwicklungspaket für Free Glut auf Ihrem System installiert ist. Der Windows-Build erfordert möglicherweise Änderungen am Build-System und/oder eine manuelle Installation des Compilers, der Bibliotheken und Header.
Die aktuelle Schlüsselkarte ist:
Up Activate Up D-Pad Button, Release turns off
Down Activate Down D-Pad Button, Release turns off
Left Activate Left D-Pad Button, Release turns off
Right Activate Right D-Pad Button, Release turns off
F1 - F8 Toggle Switch On/Off, F1 is left most, F8 Right Most
F11 Toggle UART/PS2 Keyboard Input
F12 Toggle Debugging Information
Escape Quit simulator
Alle anderen Tastaturtasten werden auf die UART- oder PS/2-Tastatureingabe umgeleitet.
Die Schalter und D-Pad-Tasten können angeklickt werden, um sie einzuschalten. Mit Linksklicks werden die Schalter aktiviert und mit Rechtsklicks ausgeschaltet. Die D-Pad-Tasten werden mit einem Klick oben aktiviert und mit dem Loslassen einer Taste an einer beliebigen Stelle auf dem Bildschirm ausgeschaltet.
Die in diesem System verwendeten VHDL-Komponenten sind so konzipiert, dass sie über verschiedene Toolchains und Anbieter hinweg wiederverwendbar und portierbar sind. Hardwarekomponenten wie Block-RAM werden abgeleitet und nicht explizit instanziiert. Auch die Komponenten sind so generisch wie möglich gestaltet, wobei die meisten wählbare Breiten haben. Das wäre zwar übertrieben, doch leider unterstützen viele Anbieter den VHDL-2008-Standard immer noch nicht.
Datei | Lizenz | Autor | Beschreibung |
---|---|---|---|
util.vhd | MIT | Richard J. Howe | Eine Sammlung generischer Komponenten |
h2.vhd | MIT | Richard J. Howe | H2 Forth CPU-Kern |
uart.vhd | MIT | Richard J. Howe | UART TX/RX (Laufzeit anpassbar) |
vga.vhd | LGPL 3.0 | Javier V. García | Textmodus VGA 80x40 Display |
Richard J. Howe | (und VT100 Terminalemulator) | ||
kbd.vhd | ??? | Scott Larson | PS/2-Tastatur |
Die Pseudo-Forth-ähnliche Sprache, die als Assembler verwendet wird, ist oben beschrieben. Die Anwendung, die tatsächlich auf dem Forth-Kern läuft, ist selbst ein Forth-Interpreter. In diesem Abschnitt wird der Forth-Interpreter beschrieben, der auf H2 Core läuft und in embed.fth enthalten ist.
TODO:
In diesem Projekt werden mehrere Sprachen verwendet, die sich alle grundlegend voneinander unterscheiden und ihre eigenen Codierungsstandards und Styleguides erfordern.
Gängige Signalnamen:
clk - The system clock
rst - A reset signal for the module
we - Write Enable
re - Read Enable
di - Data In
din - Data In
do - Data Out
dout - Data Out
control - Generally an input to a register, the documentation
for the module will need to be consulted to find out
what each bit means
signal_we - The write enable for 'signal'
signal_i - This is an input signal
signal_o - This is an output signal
Im Allgemeinen werden die Suffixe „_i“ und „_o“ nicht verwendet, Module werden kurz gehalten und Namen so gewählt, dass ihre Bedeutung offensichtlich ist. Diese Regel kann überarbeitet werden, sobald das Projekt wächst.
Komponenten sollten:
constant N: positive := 4;
signal a: std_logic_vector(N - 1 downto 0) := (others => '1');
Anstatt:
signal a: std_logic_vector(3 downto 0) := x"F";
Die Stilregeln lauten wie folgt:
Als Beispiel für die Formatierungsrichtlinien beschreibt dies ein einfaches Register beliebiger Breite:
-- Lots of comments about what the unit does should go
-- here. Describe the waveforms, states and use ASCII
-- art where possible.
library ieee, work;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all; -- numeric_std not std_logic_arith
entity reg is -- generic and port indented one tab, their parameters two
generic (
N: positive); -- Generic parameters make for a generic component
port (
clk: in std_logic; -- standard signal names
rst: in std_logic; --
we: in std_logic;
di: in std_logic_vector(N - 1 downto 0);
do: out std_logic_vector(N - 1 downto 0)); -- note the position of ");
end entity; -- "end entity", not "end reg"
architecture rtl of reg is
signal r_c, r_n: std_logic_vector(N - 1 downto 0) := (others => '0');
begin
do <= r_c;
process(rst, clk)
begin
if rst = '1' then -- asynchronous reset
r_c <= (others => '0');
elsif rising_edge(clk) then -- rising edge, not "clk'event and clk = '1'"
r_c <= r_n;
end if;
end process;
process(r_c, di, we)
begin
r_n <= r_c;
if we = '1' then
r_n <= di;
end if;
end process;
end; -- "end" or "end architecture"
In diesem Projekt wird ziemlich viel C-Code verwendet, um eine Toolkette für den H2-Kern zu erstellen und das System zu simulieren.
Da der C-Code hier nicht allzu überraschend ist, sollten einige Ausnahmen behandelt werden.
static const char *alu_op_to_string(uint16_t instruction) {
/* notice also that the 'case' clauses are inline with the
* switch selector */
switch (ALU_OP(instruction)) {
case ALU_OP_T: return "T";
case ALU_OP_N: return "N";
case ALU_OP_T_PLUS_N: return "T+N";
case ALU_OP_T_AND_N: return "T&N";
case ALU_OP_T_OR_N: return "T|N";
case ALU_OP_T_XOR_N: return "T^N";
case ALU_OP_T_INVERT: return "~T";
case ALU_OP_T_EQUAL_N: return "N=T";
case ALU_OP_N_LESS_T: return "T>N";
case ALU_OP_N_RSHIFT_T: return "N>>T";
case ALU_OP_T_DECREMENT: return "T-1";
case ALU_OP_R: return "R";
case ALU_OP_T_LOAD: return "[T]";
case ALU_OP_N_LSHIFT_T: return "N<N";
case ALU_OP_ENABLE_INTERRUPTS: return "seti";
case ALU_OP_INTERRUPTS_ENABLED: return "iset?";
case ALU_OP_RDEPTH: return "rdepth";
case ALU_OP_T_EQUAL_0: return "0=";
case ALU_OP_CPU_ID: return "cpu-id";
default: return "unknown";
}
}
if (foo)
bar();
else
baz();
picocom --omap delbs -b 115200 -e b /dev/ttyUSB1