Nachdem die CPU Befehle initial implementiert wurden geht es an die Tests.
Dazu habe ich die Testfälle von Mike Naberezny als Basis genommen und daraus Tests für Mocha & Chai geschrieben. Damit ist die Anzahl der Tests auf aktuell 667 Tests gewachsen.
Zusätzlich wurde die CPU mit der Test-Suite von Klaus Dormann geprüft, um sämtliche Fehler im Code und in den Testfällen zu beseitigen.
Am Ende ist es möglich die C64 Firmware zu starten und die Willkommens-Nachricht im Speicher ab Position 0x0400 zu finden.
Implementierung der Testfälle
Neben den initialen Tests habe ich für jeden Befehl eine eigene Datei mit Testfällen angelegt und diese thematisch gruppiert.
Hier der Link zum Git-Repository.
Jeder Test legt eine neue CPU mit eigenem Speicherbereich an:
beforeEach(() => {
mem = new Memory(64 * 1024);
cpu = new CPU(mem);
});
Die eigentlichen Tests werden immer nach dem folgenden Schema implementiert:
it('test_lda_absolute_loads_a_sets_n_flag', function () {
cpu.getRegister().ac = 0x00;
// $0000 LDA $ABCD
mem.writeByte(0x0000, 0xAD);
mem.writeByte(0x0001, 0xCD);
mem.writeByte(0x0002, 0xAB);
mem.writeByte(0xABCD, 0x80);
cpu.step();
assert.equal(cpu.getRegister().pc, 0x0003);
assert.equal(cpu.getRegister().ac, 0x80);
assert.equal(cpu.getRegister().flags & Register.FLAG_N, Register.FLAG_N);
assert.equal(cpu.getRegister().flags & Register.FLAG_Z, 0);
});
- Die Register und / oder Flags werden auf einen initialen Zustand gesetzt.
- Der zu testende Befehl und die Parameter werden in den virtuellen Speicher geschrieben. In den meisten Fällen wird bei Adresse 0 begonnen, da der Program Counter initial dorthin zeigt, wenn man nicht die reset Methode der CPU aufruft.
- Danach wird der gewählte Befehl durch den Aufruf der step Methode ausgeführt.
- Als letztes werden sämtliche betroffenen Register / Speicherstellen und Flags geprüft.
Anhand dieser Tests habe ich die CPU Befehle, die ich initial implementiert habe verifiziert. Wie zu erwarten musste ich einige Befehle korrigieren. Dazu gehören vor allem die Befehle ADC und SBC, bei denen ich mir Hilfe gesucht habe, da ich diese zum aktuellen Zeitpunkt noch nicht 100%ig verstanden habe. Es werden dort neben binärer Addition und Subtraktion auch BCD Rechenoperationen durchgeführt.
In dem Artikel The 6502 overflow flag explained mathematically wird sehr gut beschrieben, wie die Addition und Subtraktion funktioniert. Da aber dies nur den Binärteil abdeckt, gibt es noch einige Stellen, die erklären, wie die BCD (Binary Coded Decimal) Arithmetik funktioniert. z.B. Decimal Mode by Bruce Clark
Da ich nicht selbst auf die Lösung des Problems gekommen bin, habe ich mir hier und hier Hilfe bei den beiden Befehlen geholt.
Alle anderen Instruktionen lassen sich mit Hilfe der Tests leicht prüfen und korrigieren.
Interessant ist noch die Gruppe CPU6502Bugs, da dort fehlerhafte Verhaltensweisen der Original-CPU berücksichtigt werden. Vor allem bei indirekten Sprünge, in denen die Zieladresse in Speicherstellen steht, die über eine Seitengrenze hinweg geht.
D.h. das erste Byte der Zieladresse wird z.b. aus 0x00ff gelesen. Das zweite Byte sollte dann aus 0x0100 gelesen werden, aber in Wirklichkeit wird es aus 0x0000 gelesen.
Da diese Verhaltensweisen sehr gut dokumentiert sind, kann man dies aber in seinem Code relativ leicht umsetzen.
Was nicht umgesetzt wurde sind die undokumentierten Befehle der CPU. Diese werden von mir nur bei Bedarf implementiert.
Ergebnis
Das Ergebnis für diesen Teil befindet sich in meinem Gitlab-Projekt TSC64Emu.
Diesmal gibt es zwei ausführbare Applikationen.
1.) Eine Version, die die erwähnte Testsuite von Klaus Dormann hier lädt. Nachdem man diese startet, kann es einige Zeit dauern, bis alle Tests durchlaufen sind. Dies liegt daran, das alle Kombinationen der ADC / SBC Anweisungen getestet werden.
Bei erfolgreichem Durchlaufen aller Tests landet die CPU in einer Endlosschleife bei Position 0x3469. Dies kann man in dem zugehörigen Assembler Listing nachschlagen.
; S U C C E S S ************************************************
; -------------
success ;if you get here everything went well
3469 : 4c6934 > jmp * ;test passed, no errors
2.) Die zweite Applikation lädt die C64 Firmware und startet sie.
Ich habe dort die erste Revision (kernal.901227-01.bin) verwendet, da diese Version noch nicht das Video System (PAL/NTSC) ermittelt. Bei den beiden neueren Revisionen wird der Rasterzeilenzähler (Speicherstelle $d012) gelesen und der Emulator landet in einer Endlosschleife, da die Funktion des VIC-II Chips noch nicht implementiert ist.
ff5e LDA $d012
ff61 BNE $ff5e
Dies kann man zwar umgehen, wie Johan Steenkamp in seinem Blog beschreibt. Um jedoch nur zu prüfen ob die CPU soweit funktioniert, das etwas lesbares in den Speicher geschrieben wird, habe ich mich dagegen entschieden, dies vorab zu implementieren.
Die eigentliche Ausführung bleibt im Bereich zwischen $E5CD und $E5D4 in einer Schleife stehen. Dies ist die Warteschleife für Tastatureingaben wie man in dem kommentierten ROM Listing sehen kann.
Dafür findet man aber ab der Speicherstelle 0x0400 (Standard Bereich des Bildschirmspeichers - 40 * 25 Zeichen => 1000 Bytes) die Willkommens Nachricht des BASIC Interpreters:
Die Zeichen liegen als Bildschirmcodes vor, die man man hier nachschlagen kann.
042c: 2a 2a 2a 2a 20 03 0f 0d 0d 0f 04 0f 12 05 20 36 34 20 02 01 13 09 03 20 16 32 20 2a 2a 2a 2a
Anhand der Tabelle sieht man, das die Bytes ab Position 0x042c folgendes ergeben:
COMMODORE 64 BASIC V2
0479: 36 34 0b 20 12 01 0d 20 13 19 13 14 05 0d 20 20 33 38 39 31 31 20 02 01 13 09 03 20 02 19 14 05 13 20 06 12 05 05
64K RAM SYSTEM. 38911 BASIC BYTES FREE
Wichtig ist hierbei die Anzahl der freien BASIC Bytes, da diese berechnet werden. Ich hatte initial einen Fehler in der Speicherklasse, in denen ich Schreibzugriffe auf den ROM Bereich zugelassen habe. Dadurch wurde der freie Speicher falsch berechnet.
In der Korrektur lese ich die ROM Bereiche aus eigenen Arrays und schreibe in den darunterliegenden Speicherbereich.
Damit ist der Punkt erreicht, das der Emulator grundsätzlich funktioniert und die Firmware des C64 bootet.
Ausblick
Als nächstes wird mit der Implementierung der zusätzlichen Hardware fortgefahren. Dazu gehört die Anzeige des Bildschirmspeichers in einem virtuellen Bildschirm. Dazu wird der HTML5 Canvas verwendet, in den die einzelnen Zeichen aus dem Character ROM generiert werden.
Danach wird die Tastatur implementiert. Das reicht um mit dem Emulator zu interagieren und kleine BASIC Programme zu schreiben.