Forum: Projekte & Code GIGATRON Emulator im STM32


von Joerg W. (joergwolfram)


Angehängte Dateien:

Lesenswert?

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

von Als ich aus dem Internet ab (Gast)


Lesenswert?

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 ;-)

von Joerg W. (joergwolfram)


Angehängte Dateien:

Lesenswert?

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

von Joerg W. (joergwolfram)


Lesenswert?

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

von Christoph M. (mchris)


Lesenswert?

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.

von Joerg W. (joergwolfram)


Lesenswert?

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

von Christoph M. (mchris)


Lesenswert?

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.

von Joerg W. (joergwolfram)


Lesenswert?

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

von Joerg W. (joergwolfram)


Lesenswert?

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

von Christoph M. (mchris)


Lesenswert?

>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.

von Joerg W. (joergwolfram)


Lesenswert?

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

von Christoph M. (mchris)


Lesenswert?

Ich schätze mal: Faktor 5 langsamer ...

von Joerg W. (joergwolfram)


Lesenswert?

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

von Nop (Gast)


Lesenswert?

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.

von Joerg W. (joergwolfram)


Angehängte Dateien:

Lesenswert?

> 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

von Christoph M. (mchris)


Lesenswert?

>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
von Joerg W. (joergwolfram)


Lesenswert?

> 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

von Nop (Gast)


Lesenswert?

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.

von Christoph M. (mchris)


Lesenswert?

>dcache/icache aktivieren,

Kannst Du da mal ein Codeschnipsel zeigen?

von Nop (Gast)


Lesenswert?

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.

von Nop (Gast)


Lesenswert?

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.

von Joerg W. (joergwolfram)


Lesenswert?

> 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

von Mw E. (Firma: fritzler-avr.de) (fritzler)


Lesenswert?

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
Noch kein Account? Hier anmelden.