bevor der Gigatron-Emulator Thread bei "Mikrocontroller und Digitale Elektronik" rasch wieder in der Versenkung verschwindet, mache ich jetzt noch mal einen bei "Projekt und Code" auf, denn jetzt gibt es auch ein öffentliches Projekt dazu: http://www.jcwolfram.de/projekte/gtmicro_de/main.php Dabei geht es um eine Emulation des GIGATRON https://www.gigatron.io , ein Computer auf TTL-Basis von Marcel van Kervinck und Walter Belgers. Kernstück des Emulators ist ein auf 225 MHz übertakteter STM32F405, dazu noch etwas "Hühnerfutter". Der ganze Emulator ist in ASM geschrieben, und selbst da wird es knapp, so dass die ROM-Images gepatcht werden müssen. Dafür läuft der Emulator mit exakt 100% Originalgeschwindigkeit (zumindest theoretisch, alle Befehle habe ich nicht nachgemessen). Es werden 64K RAM unterstützt und es lassen sich verschiedene ROM-Images laden. Das ausgewählte ROM-Image wird beim Start in das RAM des Controllers kopiert und das CCM RAM dient als emuliertes RAM. Der Emulator selbst läuft im Flash, weiteres RAM wird nicht benötigt (auch kein Stack). Dafür ist auch keine Zeit, alle relevanten Informationen werden in Registern gehalten. Jörg
Wow, ich gratuliere zum Erfolg des "Single Chip Gigatron, a CPU without a CPU made with a CPU"! :-) Der Neid läßt mich da nur Haare in der Suppe suchen: auf Deiner Webseite dazu sehe ich ein Wort welches ich in keinem Wörterbuch finde: "selbat". Dann wird es wohl so sein dass der auf dem Bildschirmfoto sichtbare farbige "Schnee" dem noch nicht ganz ausgelüfteten Silvesteralkohol zu verdanken ist ;-)
Danke für den Hinweis, ich habe das jetzt korrigiert. Das ist halt der Nachteil, wenn der Texteditor zwar Syntax-Highlighting aber keine Rechtschreibkorrektur hat... Ich habe auch ein Keypad auf Basis eines ATMega8 entwickelt, allerdings nur auf Lochraster. Inzwischen wird auch eine angeschlossene PS/2 Tastatur unterstützt, was auch bei 3,3V Versorgung bei mir mit allen verfügbaren Tastaturen geklappt hat. Datentransfer habe ich aber nicht implementiert. Bei Bedarf kann ich das mit zum Projekt hinzufügen. Jörg
Wahrscheinlich wird es demnächst eine erweiterte Schaltung geben: - UART zum Hochladen eines neuen ROM-Images beim Start - Soft-SPI mit 2 CS-Leitungen Im Moment bin ich am Überlegen, wie man möglichst simpel ein SPI-Interface (via RAM-Zwischensockel in TTL Technik beim Original-Gigatron) realisieren könnte. Damit wäre es auch möglich, einen Massenspeicher z.B. SD-Karte) zu benutzen und könnte den zweiten CS für I/O Erweiterungen nutzen. https://forum.gigatron.io/viewtopic.php?f=4&t=64 Jörg
Den GIGATRON kannte ich noch nicht. Ein sehr schönes Gerät. Es wäre super, wenn man deinen Emulator auf einem Nucleo- oder Discovery-Board laufen lassen könnte. Davon hätte ich noch welche. Wie wär's mit einem STM32F411? Von der Speichergröße müsste eigentlich auch ein Arduino-Due passen.
Der STM43F411 geht leider nicht. Zum Einem hat er zu wenig RAM (128K). Da aber sowohl das emulierte RAM als auch das emulierte ROM im Controller-RAM liegen müssen, sind mindestens 192K notwendig. Zum Zweiten liegt die maximale Frequenz beim F411 bei 100MHz. Hier halte ich es für fraglich, ob dich der Controller auf 225MHz übertakten lässt. Da ich selbst keine Nucleo- bzw. Discovery-Boards besitze, kann ich zu anderen Boards nichts sagen bzw. müsste mir die ganzen Dokus raussuchen. Aber eigentlich sollte alles mit dem F405 / F407 funktionieren, ggf. muss halt die PLL an den verwendeten Quarz angepasst werden. Der "Rest" ist ja einfaches I/O. Jörg
Danke für die Antwort. Du hast Deinen Emulator vollständig in Assembler geschrieben. Ich frage mich, ob's auch in C ginge. Nehmen wir z.B. mal eine ESP32 mit 240MHz. Da hätte man für die 6.25MHz ca. 38 Takte. Vielleicht könnte das für die Emulation reichen. Vielleicht könnte man dann noch einen Timer benutzen um das Ganze zyklengenau hin zu bekommen.
In C wäre man sowieso auf einen Timer angewiesen, da man anders das exakte Timing wohl gar nicht einhalten könnte. In meinem Programm halte ich alle Informationen in Registern, der Emulator selbst benötigt kein RAM. Weil RAM-Zugriffe Zeit kosten. Aus diesem Grunde gibt es auch keine zentrale Schleife. Bei einer Umstellung auf C ohne manuelle Registeroptimierung würde ich einen Geschwindigkeitsverlust von mindestens 30% annehmen. Jörg
Nachtrag: Ich kann ja mal spaßeshalber die gtemu2.c an mein Board anpassen und dann die HSYNC Frequenz messen, falls das von Interesse ist. Spaßeshalber deswegen, weil das Ergebnis absehbar ist... Um im Timer-Interrupt laufen zu können, darf die C-Version bei gleicher Taktfrequenz maximal 15 Clocks je emuliertem Befehl benötigen. Der Cortex-M4 hat eine Interrupt-Latenz von 12 Takten, für das Verlassen der Interrupt-Routine werden nochmal mind. 8 Takte benötigt. Dazu kommt die leere main(), die mind. 1 Takt je Durchlauf braucht. Mein jetziges Programm hat im längsten Zweig genau 1 NOP, braucht also maximal 35 Taktzyklen für die Ausführung von einer Gigatron Instruction. Wenn ich das auf Interrupt umstellen würde, käme ich auf 35+21 Takte, was 350MHz Taktfrequenz entspricht. Wenn ich jetzt noch optimistisch annehme, dass ein C-Programm nur 25% ineffizienter als mein ASM Programm ist, dann liegt die erforderliche Taktfrequenz schon bei über 400MHz. Wahrscheinlich ist es dann schneller, auf Interrupts zu verzichten und den Timer in der main zu pollen. Das mag für die "Der C-Compiler kann das besser optimieren - Fraktion" zwar provokant klingen, ich lasse mich aber gern vom Gegenteil überzeugen. Vielleicht geht das ja mit einem STM32H7xx und 400MHz, wer mag, kann es ja mal ausprobieren. Jörg
>Ich kann ja mal spaßeshalber die gtemu2.c an mein Board anpassen und >dann die HSYNC Frequenz messen, falls das von Interesse ist. >Spaßeshalber deswegen, weil das Ergebnis absehbar ist... Es wäre schon interessant, in welcher "Preisklasse" man da so liegt.
So, ich habe gemessen und bin etwas überrascht. Daher will ich das Ergebnis noch mit anderen Einstellungen verifizieren. Inzwischen können Schätzungen abgegeben werden, um wie viel der "offizielle" C-Emulator (gtemu2.c) langsamer ist, als die ASM Variante. Jörg
Naja, da kommt schon recht nahe ran. Mit den "optimalen" Einstellungen (-O3) komme ich auf ca. 7KHz HSYNC-Frequenz, was durchschnittlich 161 Clocks je Gigatron-Instruction und einen Faktor von ca. 4,5 bedeutet. Allerdings jittert das Signal stark. Da die erreichbare Frequenz vom Befehl mit der längsten Ausführungszeit abhängt, würde ich 10% draufschlagen, das wären dann 177 Clocks. Dazu kommen noch die 21 Clocks für das INT-Handling und wir sind schon bei 198 Clocks. Und... 198 Clocks * 6,25 MHz ergeben stattliche 1,23 GHz notwendige Taktfrequenz! Der Code ist zwar sehr übersichtlich, aber nicht sonderlich effizient. Mit etwas Optimierung habe ich die C-Variante auf ca. 96 Clocks pro Gigatron-Instruction gebracht, das wären dann min. 127 Clocks, was ungefähr 790 MHz Taktfrequenz bedeutet. Mit einem RasPi sollte das zu schaffen sein ;-). Ich denke, damit können wir das Thema "C-Variante" getrost zu den Akten legen. Mit meinen 25% Overhead für C lag ich voll daneben, es sind günstigenfalls ca. 250%. Ich habe mir mal die Belegung vom STM32F407 Discovery angeschaut. Wenn man die Peripherie stilllegen kann und den Quarz tauscht, sollte es gehen. Andernfalls ließe sich das Ganze auch auf 8 oder 16MHz Quarzfrequenz umstricken. Das Projekt ist ja Open Source. Aber ich kann mir auch manchmal nicht den Gedanken verwehren, dass viele Leute sich irgendwelche STM32-Boards gekauft haben, weil es gerade "hipp" ist und jetzt darauf warten, dass jemand eine sinnvolle Anwendung dafür programmiert. Jörg
Guck Dir mal das Disassembly genauer an. Es ist nicht der RAM-Zugriff, der langsam ist, weil das onchip-RAM keine Waitstates braucht. Aber Cortex-M hat einen load/store-Befehlssatz. Wenn Du in Assembler alles in Registern halten kannst, braucht eine Wertemodifikation wie etwa ein & mit einer Konstanten nur einen Befehl. Wenn der Compiler die Register nicht optimal verwaltet, dann braucht er einen Befehl zum Laden, einen zur Modifikation und einen zum Wegspeichern. Das wäre ein Faktor von 3. Manchmal geht es doch, deswegen "nur" 2.5. -O3 ist übrigens nicht unbedingt am schnellsten. Besonders katastrophal kann es bei globalen Variablen werden, weil er die nicht über den Stackpointer nebst Offset lädt. Dann hat man nämlich u.U. noch einen Befehl mehr, weil er die 32-Bit-Adresse der Variablen mit zwei Befehlen lädt, mov und movt. So ein Interpreter hat typisch irgendeine Art von switch-case, und hier ist die Frage, ob das so gemacht ist, daß der Compiler das als jump table umsetzt - schlimmstenfalls macht er das als if/elseif/else-Kette. Eine jump table kann man jedenfalls beim GCC aber auch mit computed goto erzwingen, einer GCC-Erweiterung.
> Wenn Du in Assembler alles in Registern halten kannst, braucht eine > Wertemodifikation wie etwa ein & mit einer Konstanten nur einen Befehl. Deswegen ist auch alles in Registern. Ebenso wie ich die Pipeline durch die Struktur meines ASM Programms emuliere, ohne zusätzliche Takte zu verbrauchen. Dafür muss ich ohne Cache leben und ca. 5-8 Wait-States je Befehl. Die Optimierungsstufe -O3 war halt die schnellste Variante, wobei ich -O1 und -O0 nicht getestet habe. Da ich ja eine funktionierende ASM-Version habe, werde ich mich mit der weiteren Optimierung des C-Codes eher nicht beschäftigen. Die Messungen sind für mich eigentlich nur eine Bestätigung dafür, dass die Entscheidung für ASM richtig war. Falls jemand weitermachen will, meinen minimalistischen Testcode stelle ich hiermit zur Verfügung. Jörg
>Falls jemand weitermachen will, meinen minimalistischen Testcode stelle >ich hiermit zur Verfügung. Danke für das Beispiel. >Und... 198 Clocks * 6,25 MHz ergeben stattliche 1,23 GHz notwendige >Taktfrequenz! >Der Code ist zwar sehr übersichtlich, aber nicht sonderlich effizient. >Mit etwas Optimierung habe ich die C-Variante auf ca. 96 Clocks pro >Gigatron-Instruction gebracht, das wären dann min. 127 Clocks, was >ungefähr 790 MHz Taktfrequenz bedeutet. Mit einem RasPi sollte das zu >schaffen sein ;-). Die Frage ist, ob der Raspi aus Echtzeitsicht geeignet wäre. Es ist schon interessant zu sehen, welche großen Unterschiede es zwischen der Assmbler- und der C-Version gibt. Faktor 4.5 .... ! > Dazu kommen noch die 21 Clocks für das INT-Handling und wir sind schon bei 198 Clocks. Eine Interrupt bräuchte man meiner Meinung nach nicht, weil das System konstant laufen soll, könnte man das auch per Timer-Polling erledigen. Hier gibt es ja das Projekt AVR CP/M, bei dem mit einigen Tricks erreicht wurde, dass der AVR einen Z80 emuliert. Da würde man ja denken, dass man die paar TTL-ICs des Gigatron locker auf einem 160MHz ARM emulieren kann. https://www.mikrocontroller.net/articles/AVR_CP/M Die Frage wäre, ob es noch Optimierungsmöglichkeiten für den CPU-Code gibt:
1 | CpuState cpuCycle(const CpuState S) |
2 | {
|
3 | CpuState T = S; // New state is old state unless something changes |
4 | |
5 | T.IR = rom_data[pc][0]; // Instruction Fetch |
6 | T.D = rom_data[pc][1]; |
7 | |
8 | int ins = S.IR >> 5; // Instruction |
9 | int mod = (S.IR >> 2) & 7; // Addressing mode (or condition) |
10 | int bus = S.IR&3; // Busmode |
11 | int W = (ins == 6); // Write instruction? |
12 | int J = (ins == 7); // Jump instruction? |
13 | |
14 | uint8_t lo=S.D, hi=0, *to=NULL; // Mode Decoder |
15 | int incX=0; |
16 | if (!J) |
17 | switch (mod) { |
18 | #define E(p) (W?0:p) // Disable AC and OUT loading during sim_ram write
|
19 | case 0: to=E(&acc); break; |
20 | case 1: to=E(&acc); lo=rx; break; |
21 | case 2: to=E(&acc); hi=ry; break; |
22 | case 3: to=E(&acc); lo=rx; hi=ry; break; |
23 | case 4: to= ℞ break; |
24 | case 5: to= &ry; break; |
25 | case 6: to=E(&io_out); break; |
26 | case 7: to=E(&io_out); lo=rx; hi=ry; incX=1; break; |
27 | }
|
28 | |
29 | uint16_t addr = (hi << 8) | lo; |
30 | |
31 | int B = 0xff; // Data Bus |
32 | switch (bus) { |
33 | case 0: B=S.D; break; |
34 | case 1: if (!W) B = sim_ram[addr&0x7fff]; break; |
35 | case 2: B=acc; break; |
36 | case 3: B=io_in; break; |
37 | }
|
38 | |
39 | if (W) sim_ram[addr&0x7fff] = B; // Random Access Memory |
40 | |
41 | uint8_t ALU; // Arithmetic and Logic Unit |
42 | |
43 | switch (ins) { |
44 | case 0: ALU = B; break; // LD |
45 | case 1: ALU = acc & B; break; // ANDA |
46 | case 2: ALU = acc | B; break; // ORA |
47 | case 3: ALU = acc ^ B; break; // XORA |
48 | case 4: ALU = acc + B; break; // ADDA |
49 | case 5: ALU = acc - B; break; // SUBA |
50 | case 6: ALU = acc; break; // ST |
51 | case 7: ALU = -acc; break; // Bcc/JMP |
52 | }
|
53 | |
54 | if (to) *to = ALU; // Load value into register |
55 | if (incX) rx = rx + 1; // Increment X |
56 | |
57 | pc = pc + 1; // Next instruction |
58 | if (J) { |
59 | if (mod != 0) { // Conditional branch within page |
60 | int cond = (acc>>7) + 2*(acc==0); |
61 | if (mod & (1 << cond)) // 74153 |
62 | pc = (pc & 0xff00) | B; |
63 | } else |
64 | pc = (ry << 8) | B; // Unconditional far jump |
65 | }
|
66 | return T; |
67 | }
|
:
Bearbeitet durch User
> Die Frage wäre, ob es noch Optimierungsmöglichkeiten für den CPU-Code > gibt: Natürlich gibt es die: Alle 256 Befehle in einer case-Tabelle ausdekodieren. Die ganze Bitschieberei ist auf dem ARM gegenüber z.B. AVR schon wesentlich effizienter geworden, aber richtig effizient für Sprungtabellen ist das meiner Meinung nach nur auf den PowerPC Controllern (z.B. rlwinm). Natürlich könnte man auch den Timer pollen, aber das dauert auch ein paar Takte (Timerwert lesen, vergleichen, ggf. zurückspringen). Den dadurch entstehenden Jitter wird man auch nicht bemerken. Wesentlich interessanter finde ich, das Konzept weiterzudenken. Theoretisch sollte es möglich sein, 640x480 in 4 aus 64/256 Farben darzustellen oder 320x240 in 16 aus 64/256 Farben auf 2 Pages oder 320x240 in 64/256 Farben. Dazu noch einen virtuellen 16-Bit Prozessor mit ein paar Registern, der zudem FP beherrscht und z.B PLOT gleich mit im Befehlssatz hat... Oder einen emulierten Z80 um CP/M darauf laufen zu lassen.Oder man könnte einen PDP11-Emulator nebst Video-Terminal in einem Chip haben. Auf jeden Fall scheint es mir lohnenswert, über größere ASM-Projekte auf diesen Controllern nachzudenken. Jörg
Joerg W. schrieb: > Natürlich gibt es die: Alle 256 Befehle in einer case-Tabelle > ausdekodieren. Und zwar gleich mit Label-Adressen in dieser Tabelle und computed goto. Ach ja, und außerdem sollte man auch dcache/icache aktivieren, da man in C sowieso das Timing nicht über die Befehle machen kann. Dann hat man nämlich meistens einen effektiven 0-Waitstate-Betrieb, was bei 168 MHz einem Faktor 5 entspricht.
>dcache/icache aktivieren,
Kannst Du da mal ein Codeschnipsel zeigen?
Christoph M. schrieb: >>dcache/icache aktivieren, > > Kannst Du da mal ein Codeschnipsel zeigen? Ah das ist einfach: Du setzt ja im Flash_ACR eh schon die Waitstates in den Bits 0-3. Da mußt Du dann bloß noch gleichzeitig die Bits 8-10 auf 1 setzen. Siehe Refman, Kapital 3.9.2.
Korrektur, Kapitel 3.9.1 für F405, 3.9.2 wäre für F42/43, und die Waitstates sind dann in den Bits 0-2. Der Rest stimmt aber. Man kann außerdem dem GCC noch den Parameter -mslow-flash-data mitgeben, dann weiß er, daß Datenzugriffe aus dem Flash langsamer sein können. Mit -mcpu=cortex-m4 -mtune=cortex-m4 kann man vielleicht auch noch einen Tick rausholen.
> Und zwar gleich mit Label-Adressen in dieser Tabelle und computed goto.
So etwas mache ich ja auch bei meiner Variante, nur dass ich mir die
Tabelle spare, indem alle Befehlsvarianten ein 256Byte Alignment haben.
Dann muss ich nur noch nach (Befehlswort & 0xFF00) + Basisiadresse
springen, wobei die Basisadresse+1 schon in einem Register (r14) liegt:
1 | and.w r2,r7,0xFF00 |
2 | add r2,r14 |
3 | bx r2 |
Das steht am Ende jeder Befehlsausführung, es gibt keine zentrale
Schleife. Man könnte es eher mit einer Zustandsmaschine vergleichen, bei
der die Bits 8-16 des PC (r15) den aktuellen Zustand verkörpern.
> was bei 168 MHz einem Faktor 5 entspricht.
Das ist Wunschdenken.
Das Flash ist mit 128 Bits an das Flash-Interface angebunden, ein
weiterer Zugriff innerhalb dieser 16 Bytes erzeugt keine Waitstates. Aus
diesem Grund liegt das emulierte ROM bei meinem Emulator auch im RAM.
Weil dort die Zugriffszeit konstant ist und nicht von der Adresse des
letzten Zugriffs abhängt. In einer früheren Version hatte ich den
Emulatorcode im RAM und das ROM im Flash, damit war ein stabiles Timing
nicht möglich. Mit einem zwischenzeitlichen Zugriff auf eine Adresse
außerhalb des emulierten ROMs (nach jedem Befehl) war es dann zwar
stabil, aber halt zu langsam.
Zum Zweiten werden Prefetch/Dcache/Icache beim Programmstart des
C-Programmes eingeschaltet (via unilib_init). Einen weiteren Test mit
ausgeschaltetem Cache erspare ich mir aber, da er das Ergebnis
sicherlich nicht verbessern würde. In meinem ASM-Code schalte ich
übrigens nur den Prefetch an.
Jörg
Ja in C wird das schön langsam. Der MIPS TTL existiert auch als selbstgestrickter Emulator in C. Da hab ich nicht einfach ein mips qemu genommen, weil das Exception/IRQ System völlig anders ist. Da wars einfacher/schneller das selber zu bauen als qemu SOurcen zu verstehen und dann umzubauen. Zudem konnte ich mir so auch das Speichersystem so bauen, dass sich die Peripherie auch besser mitsimulieren lässt. Aber wie langsam ist das denn jetzt? Der MIPS TTL läuft als Multicycle Maschine mit 4MHz. Auf einem i5-2500k läuft die Emulation nur ~50% schneller als ie echte Hardware! http://www.fritzler-avr.de/spaceage2/gallery/main.php?cmd=imageview&var1=Emulator%2Fmipsemu.png Da ich weis was das für eine Arbeit ist: Hut ab vor Joerg ;)
Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.