HaDesWWW logo
Startseite

Downloads
Geschichte(n)
 
Hardware
FPGA-Board
Prozessor
  Instruktionssatz
Peripherie
  XBus-Referenz
PS/2-Board
Soundboard
USB-MMC-Board
 
PC-Software
HaCom
HoAsm, HLink
Emulator
Connectivity
 
Embedded Software
HAL
Dateimanager
Tetris
PacMan
3D-Engine
Pong
PacMan 3D
 
Kontakt

Der HaDes XP Emulator

Überblick

Mit dem HaDes XP Emulator emu können beliebige HaDes XP Programme auf dem PC ausgeführt werden. Es handelt sich um ein Windows-Programm (das bis auf ein paar Bugs aber auch auf Linux läuft), das den HaDes XP Prozessor und weitgehend auch die angeschlossene Hardware nachbildet. Ausführbare Programme (HIX) können geladen werden, der HaDes XP-Maschinencode wird dann zur Ausführung wahlweise interpretiert oder in x86-Assemblercode übersetzt. Der emu kann auch als Debugger benutzt werden, da Einzelschrittausführung und Breakpoints unterstützt werden und - im Gegensatz zur realen Hardware - das Programm stets angehalten werden und dann der komplette Register- und Speicherinhalt untersucht werden kann. 

Screenshot des HaDes XP Emulators

Bedienung

Nach dem Start des Emulators laden Sie zunächst ein HaDes-Programm über File - Open Program. Öffnen Sie nun die gewünschten Fenster und ordnen Sie diese auf dem Bildschirm an, klicken Sie dann auf Fast Forward button, um die Programmausführung zu starten. Je nach der Art des Programms können Sie mit ihm über die simulierte XConsole oder über die PC-Maus bzw. Tastatur (PS/2 Devices-Fenster) interagieren. Die Bildschirmausgabe wird im XGfx (Textmodus) oder XPix (Grafikmodus) Fenster angezeigt. Die Ausführung kann durch Verwendung des DCT-Emulatorkerns (der alles in x86-Assembler übersetzt) beschleunigt werden, wählen Sie dazu File - New [Emulator Core]. Zum Debuggen eines HaDes-Programms ist dieser aber nicht geeignet. 

Zum Debuggen eines Programms setzen Sie Breakpoints im CPU-Fenster (Doppelklick auf eine Zeile des Programmspeichers). Die Ausführung wird dann an dieser Stelle angehalten; Sie können im Einzelschrittmodus  fortfahren. Das CPU-Fenster enthält die Inhalte der Register (oben), des Programmspeichers (links unten) und des Datenspeichers (rechts unten). (Natürlich ist der Programmspeicher Teil des Datenspeichers. Nur die Darstellung ist eben etwas anders.) Falls das HaDes-Programm mit Debugdatenbank abgelegt wurde, werden Kommentare angezeigt und im Variablen-Fenster können die globalen und lokalen Variablen aller Stackrahmen betrachtet werden. Der Stack ist aber nur dann vollständig, wenn die (relativ aufwändige) Stackverfolgung seit dem letzten Reset aktiviert ist (File - Record Stack exactly). Beachten Sie, dass in all diesen Fenstern die Darstellung der Werte über das Kontextmenü geändert werden kann. 

Stets verfügbar ist der Backtrace (File - Show Backtrace), aus dem man ablesen kann, wie das Programm zur aktuellen Position gelangt ist. (Die Instruktion ganz oben ist immer die zuletzt ausgeführte.) 

Nur mit Debugdatenbank funktioniert hingegen der Profiler (File - Profile), mit dem die Ausführungszeit aller Funktionen des Programms auf einer echten HaDes XP geschätzt werden kann. Für HAL-Programme, die malloc() verwenden, gibt es zusätzlich noch eine Heap-Analysefunktion (File - Memory Diagnose). 

Programmaufbau

HaDesCpp - Maschinencode-Interpreter

Die Klasse HaDesCpp emuliert den HaDes XP Prozessor durch befehlsweise Interpretation des Maschinencodes. Sie ist komplett in C++ geschrieben. Jeder Befehl wird durch einen Aufruf der Methode RunSingle() abgearbeitet, die den Programmspeicher ausliest, den Befehl je nach Opcode abarbeitet (siehe folgenden Code-Auszug) und den Zustand der Maschine aktualisiert. Um Debugging zu unterstützen, wird auch bei jedem Befehl auf Breakpoints getestet und die Debugkomponenten informiert. Für die Performancemessung wird schließlich noch berechnet, wieviel Takte der Befehl auf der echten HaDes XP dauern würde. 

// Beispiel: Implementierung der Multiplikation
void HaDesCpp::RunSingle() { // [SNIP] Interrupt Handling, Listener benachrichtigen, Statistik ... switch (instr.opc) { case HInstr::ALU: { word aop = reg[instr.areg]; // A-Operand word bop = instr.immed ? imop : reg[instr.breg]; // B-Operand switch (instr.opc2) { case HInstr::MUL:
{ dword res = (dword)aop * (dword)bop;
ovMul = ((dword) (word) res != res);
SetReg(instr.wreg, res);
mulHighPart = (word)(res >> 32);
cycleCounter += 16; // extra instruction duration tempdei = true;
return;
}

Kein Wunder, dass die Emulation mit Hilfe der HaDesCpp nicht besonders schnell ist. Auf einem Pentium III mit 1.2 GHz kommt man bei arithmetischen Berechnungen auf etwa 4 MIPS in der Emulation, dies entspricht einem Overhead von etwa 300:1.

HaDesDct - Maschinencode-Übersetzer

Die Klasse HaDesDct (DCT steht für Direct Code Translation) emuliert den HaDes XP Prozessor durch die Übersetzung des HaDes-Maschinencodes in x86-Maschinencode und funktioniert daher logischerweise nur auf x86-Prozessoren. Dabei wird keinerlei Rücksicht auf Debugging-Fähigkeit oder Performancemessungen genommen - diese Features des Emulators werden daher nicht funktionieren, solange DCT aktiviert ist. 

Das Übersetzungskonzept ist relativ einfach. Der komplette Zustand der emulierten HaDes XP wird im Speicher gehalten. Die Register des x86-Prozessors werden also nur innerhalb eines emulierten Befehls genutzt. Dies führt zwar zu einem gewissen Overhead bei der Emulation (durch MOV-Befehle zum Laden und Speichern der Registervariablen), ermöglicht es aber, jeden HaDes-Befehl getrennt zu übersetzen, was die Aufgabe wesentlich vereinfacht. (Da der x86-Prozessor weniger Register als die HaDes XP besitzt, wären sonst aufwändige Registersharing-Algorithmen nötig gewesen.) 

Für jeden HaDes-Befehl ist dann in HaDesDct eine Folge von x86-Befehlen angegeben, die eine äquivalente Operation ausführt. Diese ist direkt als Maschinencode-Bytefolge gespeichert. Zusätzlich wird gespeichert, an welcher Stelle in diesen Code die Befehlsparameter eingesetzt werden müssen. 

Bei der Übersetzung eines Programms werden zunächst alle entsprechenden Maschinencodefolgen bestimmt und deren Länge bestimmt. Damit kann eine Tabelle aufgebaut werden, die einer HaDes-Programmspeicheradresse die PC-Speicheradresse der entsprechenden Befehlsfolge zuordnet. Da die HaDes-Programme stets zur Laufzeit (just in time) übersetzt werden, sind die Adressen aller Zustandsvariablen bekannt. Für jeden Befehl also nun werden die Speicheradressen der Parameter bestimmt (z.B. ein Pointer in das Register-Array oder ein Pointer auf den übersetzten Code für Sprünge). Diese Pointer werden dann in den Maschinencode eingesetzt, der damit stets die schnelle, absolute Adressierung verwendet.

Der übersetzte Maschinencode wird schließlich wie eine C-Funktion ohne Parameter aufgerufen, zum Rücksprung in den C-Code kann also einfach ret 0 verwendet werden. Damit dieser Rücksprung auch nach einer definierten Zeit stattfindet, wird bei jedem emulierten Sprung gezählt, wieviel Befehle ausgeführt wurden, und falls eine vorher eingestellte Zahl überschritten wird, ein Rücksprung durchgeführt.

Wie auch anhand der folgenden Code-Beispiele klar werden dürfte, ist die Emulation mit der HaDesDct deutlich schneller. Auf demselben Pentium III mit 1.2 GHz erreicht der HaDesDct bei arithmetischen Berechnungen mit Schleifen über 200 MIPS, der Overhead ist nur noch etwa 5:1. Damit ist die emulierte HaDesDct mindestens 10x so schnell wie die reale(!). 

; *** Beispiele für generierten Code ***
; addi r2, r1, #653
        mov   eax,[01350048]                ; load 
        add   eax,28Dh                      ; add immediate value
        mov   ds:[0135004C],eax             ; store result
; mul r2, r1, r2
        mov   eax,[01350048]                ; load multiplicand
        imul  dword ptr ds:[0135004C]       ; multiply with multiplier
        mov   ds:[0135004C],eax             ; store result (low part)
        mov   dword ptr ds:[01350088],edx   ; store result (high part)
; div r4, r3, r2
divPrep: ; DIVprep - Division preparation
        mov   eax,[02140050]                ; load dividend low part
        xor   edx,edx                       ; clear dividend high part
        cmp   dword ptr ds:[02140090],0     ; if not div64Prep:
        je    divExpand                     ;   goto divExpand
        mov   dword ptr ds:[02140090],edx   ; clear div64Prep
        mov   edx,dword ptr ds:[0214008C]   ; load high part
        jmp   divCheck
divExpand: ; DIVexpand - Sign expansion cmp eax,0 ; if dividend low part >= 0 jge divCheck ; goto divCheck not edx ; set dividend high part to all ones divCheck: ; DIVcheck - Check for zero mov ecx,dword ptr ds:[214004C] ; load divisor cmp ecx,0 ; if divisor != 0 jne divDo ; goto divDo divError: ; DIVerror - Call fatal error push eax ; push value of dividend push 2 ; push program counter push 3 ; push error type (DIVbyZero) push 2140040h ; push environment parameter call @ILT+3900(_HRteFatalError) ; call fatal-error-function add esp,10h ; restore ESP ret ; return to C++ divDo: ; DIVdo - Perform division idiv eax,ecx ; Perform 64bit/32bit division mov ds:[02140054],eax ; Store quotient mov dword ptr ds:[02140084],edx ; Store remainder

Allerdings gilt dies nur, solange die Peripheriekomponenten (XComponents) nicht beansprucht werden. Hier verwendet die HaDesDct nämlich wie die HaDesCpp den komplett in C++ implementierten XBus mit seinen XComponents. Eine Assembler-Optimierung würde sich hier auch gar nicht lohnen, da gerade bei der Grafikausgabe das Windows GDI die langsamste Komponente ist. Insgesamt kommt man bei grafiklastigem Code damit etwa auf die Performance der realen HaDes.

XBus und XComponents

Der XBus wird durch eine XBus Klasse simuliert, die über "Lese"- und "Schreib"-Methoden verfügt. Sie verfügt über eine Liste von XComponents, aus der jeweils laut Adresse die passende ausgewählt wird. Die Anfrage wird dann an die XComponent delegiert. 

Grafische Oberfläche

Die Benutzeroberfläche von emu wurde mit wxWindows (jetzt wxWidgets) erstellt. 

Für die verschiedenen Emulatoransichten wurde ein MDI-Applikationsmodell gewählt. Daher ist der Hauptframe von wxMDIParentFrame abgeleitet, alle Ansichten dagegen von wxMDIChildFrame. Viele der Ansichten stellen XComponents dar, daher sind sie zusätzlich von den XComponent-Klassen abgeleitet und überladen deren virtuelle Methoden, um bei Änderungen des Komponentenzustandes die Darstellung zu aktualisieren. 

Einen Spezialfall stellt der XPixFrame, der zuständig für den Grafikcontroller XPix ist, dar. Da die Grafikunterstützung von wxWidgets nicht schnell genug ist - einzelne Pixel können hier nur über wxDC::SetPoint() gesetzt werden und zum Zeichnen eines kompletten Bildes muss es zunächst in ein wxBitmap und dann auf den Bildschirm kopiert werden - haben wir zusätzlich eine Variante, die direkt auf der Windows-API basiert, implementiert. XPixFrameMsw verwendet die DIB (Device Independent Bitmap) Funktionen, um das Bild direkt aus dem emulierten Grafikspeicher der XPix heraus zu zeichnen. Dazu wird ein entsprechender BITMAPINFO-Header erzeugt, das Zeichnen wird dann mit der StretchDIBits() Funktion vorgenommen. 

Auch die Soundausgabe (HSoundSystem) wird direkt durch die Windows Multimedia API vorgenommen. Der Inhalt des Soundpuffers der emulierten XSound wird blockweise mit waveOutWrite() an die Soundkarte geschickt. Ein großes (ungelöstes) Problem hierbei ist die hohe Latenz der WinMM API. Insbesondere bei On-Board-Soundkarten kann sie bis zu 500 ms betragen, dies führt zu einem völlig anderen Verhalten gegenüber der realen XSound. Wo bei der realen XSound, wenn man nicht aufpasst, leicht Überläufe des Ausgabepuffers entstehen, ist der Sound bei der emulierten XSound verzögert und Samples unterbrechen sich gegenseitig, da sie immer noch zu laufen scheinen. Hier wäre eine Implementierung, die auf DirectSound oder am besten gleich ASIO aufbaut, sicher zu bevorzugen. Allerdings war schon bisher die Emulation der XSound deutlich aufwändiger als der Bau der Soundkarte (die Entwicklung von XSound in VHDL ist hierbei allerdings nicht berücksichtigt).

 
rrobek.de Hauptseite
 
Valid HTML 4.01!