Softcore CPU NEO430 mit GHDL simulieren

Für meine Masterarbeit beschäftige ich mich gerade mit Softcore-Prozessoren auf FPGAs. Dabei kommt für meine Arbeit der NEO430 zum Einsatz, ein sehr kleiner aber feiner Prozessor der an meinem Institut entwickelt wurde (von Stephan Nolting). Da ich aktuell noch nicht in einem Stadium bin, wo ich den Prozessor wirklich auf einem FPGA implementiere (auch wenn das der nächste Schritt ist) arbeite ich aktuell sehr viel mit Simulation, und da vor allem mit ModelSim. Ein bisschen was dazu hatte ich ja hier im Blog schon mal da zu geschrieben.

Nun hatte ich heute Abend die Idee, man könnte ja mal einen anderen Simulator ausprobieren. Mir hat es dabei GHDL angetan, vor allem, da er OpenSource ist, aber auch weil er einen interessanten Ansatz verfolgt: Der VHDL-Code wird mit einem umgebauten Compiler (GCC oder LLVM) direkt in Maschinencode kompiliert, und dieser Code dann ausgeführt. Dadurch soll der Simulator extrem schnell sein. Auf Windows ist das ganze etwas eingeschränkt, hier wird ein interner Codegenerator verwendet.

Hier nun also eine kleine Anleitung, wie man den NEO430 mit GHDL zum Laufen bekommt.

Als erstes muss natürlich GHDL heruntergeladen werden und das NEO430-Repo ausgecheckt werden. Ich gehe jetzt mal davon aus, dass der geneigte Leser das alleine hinbekommt 🙂

Nun kann im Prinzip schon mit der Simulation begonnen werden. Dazu müssen die VHDL-Dateien zunächst in GHDL importiert werden (Kommandos für Windows, auf Linux analog):


.\ghdl.exe -i ..\neo430\rtl\core\*.vhd ..\neo430\sim\neo430_tb.vhd

Nun kann das Design kompiliert und ausgeführt werden:


\.ghdl.exe -m --std=08 neo430_tb

\.ghdl.exe -r --std=08 neo430_tb --ieee-asserts=disable

Leider klappt es dann doch noch nicht so einfach, weil GHDL sich leider extrem an den VHDL-Standard hält, und Stephan leider nicht 😛 Das erste Problem ist die Deklaration einer Variable vom Typ „file“, die noch nach alter Syntax erfolgt. Mit dieser Änderung ist das Problem schon mal aus der Welt geschafft:


-file uart_rx_file : text is out "uart_rx_dump.txt";
+file uart_rx_file : text open write_mode is "uart_rx_dump.txt";

Nun gibt es noch ein paar Probleme, weil an ein paar Stellen Code â la „0 to 15-1“ verwendet wird, dabei stört sich GHDL daran, das „15-1“ keine Konstante wäre… Mit ein bisschen Kopfrechnen ist das aber zu lösen. Folgende Stellen sind betroffen:


neo430_tb
-type ascii_t is array (0 to 95-1) of character;
+type ascii_t is array (0 to 94) of character;
application/bootload_image
-type bootloader_init_image_t is array (0 to (2**16)-1) of std_ulogic_vector(15 downto 0);
+type bootloader_init_image_t is array (0 to 65535) of std_ulogic_vector(15 downto 0);
neo430_package
-for i in 0 to input'length-1 loop
-output_v(input'length-i-1) := input(i);
+for i in 1 to input'length loop
+output_v(input'length-i-2) := input(i-1);
-for i in input'length-1 downto 0 loop
-if (input(i) = '1') then
+for i in input'length downto 1 loop
+if (input(i-1) = '1') then
-for i in input'length-1 downto 0 loop
-if (input(i) = '0') then
+for i in input'length downto 1 loop
+if (input(i-1) = '0') then
-for i in input'length-2 downto 0 loop
-output_v(i) := input(i) xor input(i+1);
+for i in input'length downto 2 loop
+output_v(i) := input(i-2) xor input(i+1-2);
-for i in input'length-2 downto 0 loop
-output_v(i) := output_v(i+1) xor input(i);
+for i in input'length downto 2 loop
+output_v(i) := output_v(i+1-2) xor input(i-2);

Eine letzte Änderung ist leider noch nötig, und diese ist etwas aufwendiger. VHDL erlaubt offiziell nicht, in einem „case … when …“ Konstrukt (dem Äquivalent zu „switch“ in C etc.) für die einzelnen Zweige Konstanten zu verwenden, wenn diese Konstanten nicht in der gleichen Entity definiert wurden. Leider wird im NEO430 davon kräftig gebraucht gemacht um die die Adressen aller Register zentral in einer Datei zu verwalten. Die meisten Synthesetools geben nur eine Warnung aus, für GHDL ist das leider ein Fehler. Hier bleibt einem nichts anderes über, als alle Adresse direkt einzutragen. Das sieht das z.B. so aus:


-when timer_tctrl_addr_c =>
+when x"FFC4" =>

Durch die Fehlermeldungen sind die Stellen nicht schwer zu finden, alles zu reparieren ist trotzdem ein wenig nervig. Die korrekten Adresse kann man entweder der Datei neo430_package.vhd oder der Dokumentation entnehmen.

Nach diesen Änderungen sollte die Simulation nun starten! Wenn man sie nach einiger Zeit wieder abbricht sollte man in der Datei uart_rx_dump.txt die Ausgabe des Bootloaders finden. Jay!

Nun kann man das ganze noch ein bisschen schöner machen: Wenn man die Daten in der Testbench nicht in „uart_rx_file“ sondern in die „Datei“ „output“ (die nicht vorher deklariert/geöffnet werden muss) landet die Ausgabe direkt auf der Konsole.

Nun ist es natürlich etwas umständlich (und laaaaangsam), die Daten vom Prozessor über eine simulierte UART-Schnittestelle zu übertragen. Ein kleiner Hack in der Datei neo430_usart.vhd verbessert das etwas:


library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
+use std.textio.all;
library work;
use work.neo430_package.all;
...
uart_tx_unit: process(clk_i)
+variable i, j : integer := 0;
+variable line_tmp : line;
begin
elsif (wr_en = '1') and (addr = usart_uart_rtx_addr_c) then
-uart_tx_sreg <= '1' & data_i(7 downto 0) & '0'; -- stopbit & data & startbit
-uart_tx_start <= '1';
+i := to_integer(unsigned(data_i(7 downto 0)));
+j := j+1;
+write(line_tmp,ESC & "[1A" & ESC & "[" & integer'image(j) & "G");
+write(line_tmp, character'val(i));
+writeline(output,line_tmp);
+if (i = 10) then
+    j := 0;
+end if;
end if;
uart_tx_busy <= uart_tx_start and uart_clk;

Kurze Erklärung: Bei einem Schreibzugriff auf das UART-Register wird nun nicht mehr eine Übertragung per UART gestartet, sondern das Zeichen wird direkt auf der Konsole ausgegeben. Leider ist VHDL hier ziemlich beschränkt: Daten können mit „write“ nur in eine „line“-Variable geschrieben werden, die dann per „writeline“ ausgegeben wird — aber eben immer als komplette Zeile, also gefolgt von einem Zeilenumbruch. Eine Möglichkeit, einzelne Zeichen auszugeben ist nicht vorgesehen. Nun kann man natürlich die Daten puffern, und ausgeben, wenn ein Zeilenumbruch von der CPU gesendet wird (Code 10), aber dann sieht man nichts, bis eine Zeile vollständig ist. Meine Lösung ist ein kleiner Hack: Jedes Zeichen wird sofort ausgegebenen, allerdings wird vorher der Cursor eine Zeile nach oben bewegt und dann so viele Zeichen nach rechts, wie bis jetzt Zeichen ausgeben werden. Auf diese Weise wird der letzte Zeilenumbruch quasi „rückgängig“ gemacht. Nicht schön, funktioniert aber 🙂

Damit sollte die Ausgabe jetzt deutlich schneller sein.

Sich den Bootloader anzuschauen ist ja schön, aber eigentlich würden wir ja auch gerne ein Programm sehen 🙂 Wenn wir nun also (wie in der Doku beschrieben) den MSP430-Crosscompiler installiert haben, können wir auch die beigelegten Beispiele ausprobieren. Eventuell muss noch der Pfad zum Compiler in sw\common\compile.bat angepasst werden. Ich habe mir das Game of Life Beispiel vorgenommen. Nach dem Ausführen der make.bat und dem Deaktivieren des Bootloaders in der neo430_tb.vhd (Generic BOOTLD_USE) sollte nun der Start des GoL-Programms auf dem Bildschirm erscheinen.

Leider wartet das Programm vor dem Start auf einen Tastendruck, das muss aus dem Sourcecode entfernt werden. Bei der Gelegenheit würde ich auch gleich noch die Wartezeit zwischen den Generationen entfernen und die Größe des Universums auf etwa 16×16 setzen. Mit diesen Anpassungen kann man nun das Game of Life bewundern, ausgeführt auf einer simulierten CPU!

Nicht schlecht 😀

Als letztes hat mich dann noch mal die Performance interessiert, da sie mich gefühlt nicht so vom Hocker gehauen hat. Eine kleine Messung ergab, dass GHDL für 5ms Simulationszeit 22 Sekunden Echtzeit auf meinem Rechner braucht (Intel i5-6500 @ 3,2 GHz). Die gleiche Simulation wird von Modelsim in exakt der Hälfte der Zeit, in 11 Sekunden ausgeführt. Hier kann GHDL also noch nicht mit Geschwindigkeit glänzen, wobei hier möglicherweise eine Rolle spielt, dass der interne Codegenerator und nicht GCC oder LLVM verwendet werden. Wenn ich Zeit finde, wiederhole ich diese Tests noch mal auf Linux mit dem richtigen Backend. Im Institut haben wir allerdings auch Lizenz von ModelSim (und nicht nur die Starter Edition), die auf ähnlicher Hardware noch mal etwa die doppelte Performance hat (5ms in 5 Sekunden). Das deckt sich auch mit der Aussage des Herstellers, dass die StarterEdition langsamer ist. Wenn man die Preise von ModelSim anschaut, ist das aber alles etwas relativ 🙂 Zudem ist je nach Anwendung nicht nur die Simulationszeit entscheidend, sondern auch die Zeit, die vergeht, bis die Simulation nach einer Änderung des Sourcecodes wieder startet. Hier kann GHDL glänzen: Die Zeiten waren praktisch nicht messbar, sie lagen im Bereich von ein paar 100 ms. ModelSim ist hier deutlich langsamer; selbst wenn man Glück hat und es reicht die entsprechende Datei neu zu kompilieren (und nicht das komplette Design neu kompiliert werden muss) dauert Kompilerlauf und Ladevorgang von Modelsim immer nur gut und gerne 5 Sekunden, eher länger. Damit wir der Zeitvorteil schon wieder sehr relativ. Dafür kann Modelsim dann aber mit einem etwas „flexibleren“ Verständnis von VHDL punkten (es versteht den NEO430-Code ohne Anpassung) und bietet einige nette Funktionen zum Anzeigen von Signalverläufen und zum Debuggen des Designs, die man sich mit GHDL erst zusammenbasteln muss.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert