Willow Pattern/Schnelllader
<< zurück zu Willow Pattern
Willow Pattern/Schnelllader: Die folgenden Abschnitte stellen den disassemblierten Kassetten-Schnelllader des Spiels Willow Pattern dar. Sie sind gegliedert in einzelne Codeblöcke, die gemäß ihrer Funktion während und nach dem Laden gruppiert sind.
Autostart-Routine[Bearbeiten | Quelltext bearbeiten]
Die folgende, kurze Routine stellt den ersten Teil einer langen Sequenz von Dateien dar, die zusammengenommen die Kassettenversion des Spiels "Willow Pattern" bilden. Die Ladeadresse $02A7 (dezimal 679) ist so gewählt, dass das Maschinenspracheprogramm zwar in einen üblicherweise ungenutzten Bereich des Hauptspeichers geladen wird, die nachfolgenden Datenbytes aber den Sprungvektor für die Eingabe einer BASIC-Zeile überschreiben, auf das Programm selbst umbiegen und dieses dadurch nach dem Laden automatisch starten.
Das Programm stellt aber keinen Schnelllader dar, sondern verändert nur einige Bildschirmeinstellungen und lädt dann mit Hilfe der normalen Kernal-Routinen — und mit der üblichen, langsamen Geschwindigkeit — ein 1 kByte langes, namenloses Programm nach, das zwei etwa gleich lange und gleich leistungsfähige Schnelllader enthält. Anschließend werden drei Datenblöcke mit Hilfe des ersten Schnellladers von Kassette eingelesen, bevor die Kontrolle an die zweite Laderoutine übergeben wird.
ORG $02A7
P_AA: LDA #<AA01 ; Stop-Vektor auf AA01 richten (STOP-Taste abschalten)
STA $0328 ; Low-Byte
LDA #>AA01
STA $0329 ; High-Byte
LDA #$0E ; Farbcode für "hellblau"
STA $D021 ; Bildschirmhintergrund setzen
LDA #$40
JSR $FF90 ; Status setzen
LDA #$01 ; Logische Dateinummer=1
TAX ; Gerätenummer=1 (Datassette)
TAY ; Sekundäradresse=1
JSR $FFBA ; Fileparameter setzen
LDA #$00 ; Länge des Dateinamens=0
JSR $FFBD ; Filenamenparameter setzen
JSR $FFD5 ; LOAD
LDA #$93 ; ASCII-Code für "Bildschirm löschen"
JSR $FFD2 ; BSOUT Ausgabe eines Zeichens
LDX #$00 ; Farbcode für "schwarz"
STX $D021 ; Bildschirmhintergrund setzen
LDA #$01 ; Farbcode für "weiß"
AA00: STA $D900,X ; Farb-RAM für einen Teil des Bildschirms füllen
INX ; Schreibindex erhöhen
BNE AA00 ; Rücksprung falls noch nicht 256 Bytes gefüllt
LDX #$C4 ; Bildschirmzeiger auf $05C4 richten
LDY #$05
JSR P_BA ; Programmteil laden
LDA #$02 ; Dateinummer (und Synchronisationszeichen)
LDX #$00 ; Bildschirmzeiger auf $BE00 richten
LDY #$BE
JSR P_BA ; Programmteil laden
LDA #$03 ; Dateinummer (und Synchronisationszeichen)
LDX #$00 ; Bildschirmzeiger auf $BE00 richten
LDY #$BE
JSR P_BA ; Programmteil laden
JMP P_CA
AA01: RTS
DB $00,$00,$00,$00,$00,$00,$00 ; Füllbytes
DW $E38B ; Vektor für BASIC Warmstart
DW P_AA ; Vektor für Eingabe einer Zeile
Erster Schnelllader[Bearbeiten | Quelltext bearbeiten]
Der erste Schnelllader lädt eine unbeschränkte Anzahl von 256 Byte langen Datenblöcken in beliebige — nicht notwendigerweise direkt aufeinanderfolgende — Pages des C64-Hauptspeichers. Jedem Datenblock muss dabei ein Header vorausgehen, der nacheinander die Blocknummer (1 Byte) sowie die Zieladresse des Blocks, die Endadresse des ganzen Programmteils und sowie dessen Einsprungadresse (jeweils 2 Byte im Little Endian-Format) enthält. Ist die Einsprungadresse auf $0000 gesetzt, so wird der Programmteil nach dem Laden nicht angesprungen. Diese Arbeitsweise weist einige Ähnlichkeiten zum Schnelllader Novaload auf.
Die Unterscheidung von 0- und 1-Bits geschieht in Routine P_BH mit Hilfe des Timers A der CIA 2. Dieser zählt für jedes Bit, beginnend bei einem Startwert von 560, mit dem Systemtakt der CPU abwärts. Ist der Zähler beim Eintreffen des nächsten Signals vom Band bereits abgelaufen, so handelt es sich um ein 1-Bit (Abstand zum vorigen Impuls 720 Takte), ansonsten um ein 0-Bit (Abstand zum vorigen Signal 384 Takte). Die Interruptroutine P_AG fasst jeweils 8 Bit zu einem Byte zusammen. Interessanterweise wird dieses nicht direkt in den Hauptspeicher übertragen, sondern landet zunächst in einem 32 Byte langen Ringpuffer, wo es von Routine P_BL abgeholt werden kann. Da die nachzuladenden Daten zu 41,3% aus 0-Bits und zu 58,7% aus 1-Bits bestehen, dauert das Einlesen eines Byte vom Band durchschnittlich 4650 Systemtakte (fast 5 ms).
PRG $C000
P_BA: STA *$A3 ; Synchronisationszeichen
STX *$A4 ; Bildschirmzeiger low
STY *$A5 ; Bildschirmzeiger high
SEI ; Interrupts abschalten, bevor Interruptvektor geändert wird
LDA #$C1 ; Interruptvektor auf Befehl "RTI" richten
STA $0318 ; Low-Byte
LDA #$FE
STA $0319 ; High-Byte
LDA *$01 ; Prozessorport lesen
AND #$DF ; Datassetten-Motor dauerhaft einschalten
STA *$01 ; und zurückschreiben
LDA #$32 ; Startwert für Timer A von CIA 2 auf $0232 (560)
STA $DD04 ; Low-Byte
LDA #$02
STA $DD05 ; High-Byte
LDA #$19 ; Timer A mit Startwert laden und im "One Shot"-Modus starten
STA $DD0E
LDA #$7F ; Alle Interrupts von CIA 1 abschalten
STA $DC0D
STA $D020 ; Bildschirmrand hellgrau
LDA #$90 ; Einzige Interruptquelle für CIA ist Impuls von Datassette
STA $DC0D
LDY #$00 ; Blockzähler
STY *$B1 ; initialisieren
JSR P_BC ; Meldung "SEARCHING " ausgeben
BA00: JSR P_BI ; Mit Datassette synchronisieren
JSR P_BJ ; 7 Byte langen Blockheader an $A9..$AF einlesen
LDA *$A9 ; Blocknummer aus Blockheader holen
CMP *$B1 ; und mit aktuellem Blockzähler vergleichen
BEQ BA01 ; Sprung falls gleich
JSR P_BE ; sonst Fehlermeldung "BLOCK? " ausgeben
BEQ BA00 ; Unbedingter Sprung, neu synchronisieren und nächster Block
BA01: JSR P_BD ; Meldung "SEARCHING " durch Meldung "LOADING " ersetzen
JSR P_BK ; Nächsten Programmblock einlesen
BEQ BA02 ; Sprung falls Block samt Prüfsumme korrekt gelesen
JSR P_BE ; sonst Fehlermeldung "BLOCK? " ausgeben
BEQ BA00 ; Unbedingter Sprung, neu synchronisieren und nächster Block
BA02: INC *$B1 ; Blockzähler erhöhen
LDA $D020 ; Bildschirm-Rahmenfarbe holen
EOR #$02 ; wechseln
STA $D020 ; und zurückschreiben
DEC *$AD ; Endadresse für nachfolgenden Vergleich erniedrigen
LDA *$AB ; Zieladresse des aktuellen Blocks holen
CMP *$AD ; und mit Endadresse-1 vergleichen
BNE BA00 ; Sprung, falls ungleich (also nicht letzter Block)
JSR P_BM ; Bildschirm und Interrupts wieder auf Standardwerte setzen
LDA *$AE ; Soll soeben geladener Programmteil ausgeführt werden?
ORA *$AF
BEQ BB02 ; Sprung falls nicht ausführen (Einsprungadresse=$0000)
JMP ($00AE) ; sonst ausführen
; Inhalt von <A> als Hexadezimalzahl an ($A4),Y darstellen
P_BB: PHA ; Wert von A retten
LSR A ; High Nibble isolieren
LSR A
LSR A
LSR A
JSR BB00 ; und ausgeben
PLA ; Wert von A zurückholen
INY ; Schreibindex für Bildschirm erhöhen
AND #$0F ; Low Nibble isolieren
BB00: ORA #$30 ; Nibble in Bereich '0'...'9' bringen
CMP #$3A ; Größer als '9'?
BCC BB01 ; Sprung falls nicht
SBC #$39 ; sonst in Bereich 'A'...'F' bringen
BB01: STA ($A4),Y ; und ausgeben
BB02: RTS
; Meldung "SEARCHING " auf Bildschirm ausgeben
P_BC: LDX #T000-T000 ; Zeiger auf Bildschirmmeldung "SEARCHING "
LDY #$28 ; Schreibindex für Bildschirm (zweite Zeile)
JMP BE00 ; Bildschirmmeldung ausgeben
; Meldung "SEARCHING " durch Meldung "LOADING " ersetzen
P_BD: LDY #$28 ; Schreibindex für Bildschirm (zweite Zeile)
LDA #$20 ; ' '
BD00: STA ($A4),Y ; Meldung "SEARCHING " in zweiter Zeile löschen
INY ; Schreibindex erhöhen
CPY #$35 ; Schon ganze Meldung gelöscht?
BNE BD00 ; Rücksprung falls noch nicht
LDX #T001-T000 ; Zeiger auf Bildschirmmeldung "LOADING "
LDY #$02 ; Schreibindex für Bildschirm
JMP BE00 ; Bildschirmmeldung ausgeben
; Fehlermeldung " BLOCK? " und ggf. "REWIND TO " ausgeben
P_BE: LDX #T002-T000 ; Zeiger auf Bildschirmmeldung " BLOCK? "
LDY #$02 ; Schreibindex für Bildschirm
JSR BE00 ; Bildschirmmeldung ausgeben
LDA *$B1 ; Nummer des gesuchten Blocks holen
LDY #$32 ; Schreibindex für Bildschirm
JSR P_BB ; und als Hexadezimalzahl ausgeben
LDA *$A9 ; Blocknummer aus Blockheader holen
CMP *$B1 ; und mit aktuellem Blockzähler vergleichen
BCC P_BC ; Sprung falls kleiner
LDX #T003-T000 ; Zeiger auf Bildschirmmeldung "REWIND TO "
LDY #$28 ; Schreibindex für Bildschirm
; Bildschirmmeldung über Zeiger $A4/$A5 ausgeben
BE00: LDA T000,X ; Meldung byteweise lesen
STA ($A4),Y ; und auf Bildschirm schreiben
INY ; Schreibindex erhöhen
INX ; Leseindex erhöhen
CMP #$20 ; Leerzeichen (Endekennzeichen)?
BNE BE00 ; Rücksprung wenn nicht Leerzeichen
RTS
; Mit Datassette synchronisieren
P_BF: SEI ; Interrupts abschalten (unnötig in Interruptroutine)
JSR P_BH ; Ein Bit von Band einlesen
LDA *$BD ; Gelesenes Byte holen
CMP *$A3 ; und mit Synchronisations-Kennzeichen vergleichen
BNE BG00 ; Rückkehr aus Interrupt falls nicht passend
LDA #$01 ; Speicher für gelesene Bits
STA *$BD ; als Zähler für 8 Bit initialisieren
LDA #<P_BG ; Interruptvektor auf Routine P_BG richten
STA $0314 ; Low-Byte
LDA #>P_BG
STA $0315 ; High-Byte
JMP BG00 ; Alle Register zurückholen, dann Rücksprung aus Interrupt
; Interruptroutine für Band lesen, Byte in Ringpuffer schreiben
P_BG: SEI ; Interrupts abschalten (unnötig in Interruptroutine)
JSR P_BH ; Ein Bit von Band einlesen und nach $BD
BCC BG00 ; Sprung falls noch nicht 8 Bit empfangen
LDY *$72 ; Schreibzeiger für Ringpuffer holen
LDA *$BD ; Von Band gelesenes Byte holen
STA $033C,Y ; und in 32 Byte-Ringpuffer schreiben
LDA #$01 ; Kennzeichen für "Weitere 8 Bit einlesen"
STA *$BD ; in Puffer für empfangene Bits schreiben
INY ; Schreibzeiger erhöhen
TYA ; nach A übertragen
AND #$1F ; auf Wertebereich 0..31 begrenzen
STA *$72 ; und zurückschreiben
BG00: JMP $FEBC ; Alle Register zurückholen, dann Rücksprung aus Interrupt
; Bit von Band lesen, liefert CS wenn 8. Bit (vollständiges Byte empfangen)
P_BH: LDA #$19 ; Timer A von CIA 2 mit Startwert laden
STA $DD0E ; und im "One Shot"-Betrieb starten
LDA $DC0D ; Wartende Interrupts CIA 1 löschen
LDA $DD0D ; Wartende Interrupts CIA 2 löschen
LSR A ; Unterlauf Timer A von CIA 1?
ROL *$BD ; Unterlaufflag als empfangenes Bit merken
RTS
; Mit Datasette synchronisieren
P_BI: SEI ; Interrupts abschalten, bevor Interruptvektor geändert wird
LDA #$00
STA *$72 ; Schreibzeiger auf 32 Byte-Ringpuffer zurücksetzen
STA *$71 ; Lesezeiger auf 32 Byte-Ringpuffer zurücksetzen
STA *$BD
LDA #<P_BF ; Interruptvektor auf P_BF (bitweise Synchronisation) richten
STA $0314 ; Low-Byte
LDA #>P_BF
STA $0315 ; High-Byte
CLI ; Interrupts wieder zulassen
BI00: JSR P_BL ; Byte von Band aus 32 Byte-Ringpuffer nach A holen
CMP *$A3 ; Ist Byte immer noch gleich Synchronisationskennzeichen?
BEQ BI00 ; Weiter warten falls ja
EOR #$FF ; Gelesenes Byte invertieren
CMP *$A3 ; Ist invertiertes Byte gleich Synchronisationskennzeichen?
BNE P_BI ; Neuer Synchronisationsversuch falls nicht
JSR P_BL ; Byte von Band aus 32 Byte-Ringpuffer nach A holen
CMP *$A3 ; Mit ursprünglichem Synchronisationskennzeichen vergleichen
BNE P_BI ; Neuer Synchronisationsversuch falls nicht
RTS
; 7 Byte-Blockheader in Puffer an $A9...$AF einlesen
P_BJ: LDY #$00 ; Schreibindex initialisieren
BJ00: JSR P_BL ; Byte von Band aus 32 Byte-Ringpuffer nach A holen
STA $00A9,Y ; und in Puffer an $A9...$AF schreiben
INY ; Schreibindex erhöhen
CPY #$07 ; Schon 7 Bytes eingelesen?
BNE BJ00 ; Rücksprung falls noch nicht
LDA *$A9 ; Blocknummer aus Blockheader holen
LDY #$0A ; Schreibindex für Ausgabe auf Bildschirm
JMP P_BB ; und als Hexadezimalzahl an ($A4),Y darstellen
; Einen 256 Byte-Block von Band lesen und an ($AA) in Speicher schreiben
P_BK: LDY #$00
STY *$B0 ; Prüfsumme initialisieren
BK00: JSR P_BL ; Byte aus 32 Bit-Ringpuffer nach A holen
STA ($AA),Y ; und an Zieladresse im Speicher schreiben
EOR *$B0 ; und in Prüfsumme einarbeiten
STA *$B0
INY ; Schreibindex erhöhen
BNE BK00 ; Rücksprung falls noch nicht 256 Bytes gelesen
JSR P_BL ; Byte (Prüfsumme) aus 32 Bit-Ringpuffer nach A holen
CMP *$B0 ; und mit errechneter Prüfsumme vergleichen
RTS
; Byte aus 32 Byte-Ringpuffer an $033C nach A holen
P_BL: LDX *$71 ; Lesezeiger holen
CPX *$72 ; und mit Schreibzeiger vergleichen
BEQ P_BL ; Rücksprung wenn gleich (kein Byte im Puffer)
LDA $033C,X ; Byte aus Puffer holen
PHA ; und retten
INX ; Lesezeiger weiterbewegen
TXA ; und nach A übertragen
AND #$1F ; auf Wertebereich 0..31 beschränken
STA *$71 ; und zurückschreiben
PLA ; Byte aus Puffer zurückholen
RTS
; Zurückschalten auf Normalbetrieb
P_BM: LDA $D011 ; Bildschirm wieder einschalten
ORA #$10
STA $D011
LDA *$01 ; Prozessorport lesen
ORA #$20 ; Datassetten-Motor ausschalten
STA *$C0
STA *$01 ; und zurückschreiben
LDA #$7F ; Alle Interrupts von CIA 1 abschalten
STA $DC0D
LDA #$81 ; Unterlauf Timer A als einzige Interruptquelle von CIA 1
STA $DC0D
SEI ; Interrupts abschalten, bevor Interruptvektor geändert wird
LDA #$31 ; Interruptvektor auf normale Interruptroutine richten
STA $0314 ; Low-Byte von $EA31
LDA #$EA
STA $0315 ; High-Byte von $EA31
CLI ; Interrupts wieder zulassen
RTS
T000: DB $13,$05,$01,$12,$03,$08,$09,$0E,$07,$20 ; "SEARCHING "
T001: DB $0C,$0F,$01,$04,$09,$0E,$07,$20 ; "LOADING "
T002: DB $60,$02,$0C,$0F,$03,$0B,$3F,$20 ; " BLOCK? "
T003: DB $12,$05,$17,$09,$0E,$04,$60,$14,$0F,$20 ; "REWIND TO "
Zweiter Schnelllader[Bearbeiten | Quelltext bearbeiten]
Der zweite Schnelllader stellt offensichtliche eine geringfügig modifizierte Kopie des ersten dar — zahlreiche Routinen stimmen überein, und selbst die Bildschirmmeldungen sind, obwohl identisch, ein zweites Mal vorhanden. Auch die Codierung von 0- und 1-Bits ist unverändert, allerdings hat sich die Struktur des Headers geändert, der den eigentlichen Nutzdaten vorausgeht: Diese auf 4 Byte verkürzte Datenstruktur enthält nun das Synchronisationsbyte für den nachfolgenden Block und die laufende Nummer des Blocks (jeweils 1 Byte) sowie dessen Zieladresse im Speicher.
Noch auffallender ist aber eine neu eingeführte Datenstruktur im Anschluss an die Nutzdaten: Diese enthält nicht nur — wie beim ersten Schnelllader — eine Prüfsumme, sondern zudem zwischen 2 und 32 Bytes "Ballast": Daten ohne Bedeutung, die nur die Analyse der Bandimpulse erschweren sollen und von der Laderoutine überlesen und ignoriert werden. Damit dies funktioniert, gibt jeweils das erste überflüssige Byte an, wie viele weitere Bytes überlesen werden sollen.
Neuerdings an jeden Datenblock angehängt wird zudem eine Einsprungadresse, an die nach dem Laden des jeweiligen Datenblocks — und nicht erst nach dem Laden des vollständigen Programmteils — verzweigt wird. In den meisten Fällen zeigt diese Adresse wieder in den Code des Schnellladers (Label CA00).
PRG $C200
P_CA: SEI
LDA #<CG00 ; BRK-Vektor auf CG00 richten (Register zurückholen und RTI)
STA $0316 ; Low-Byte
LDA #>CG00
STA $0317 ; High-Byte
LDA #$C1 ; Interruptvektor auf Befehl "RTI" richten
STA $0318 ; Low-Byte
LDA #$FE
STA $0319 ; High-Byte
LDA *$01 ; Prozessorport lesen
AND #$DE ; Datassetten-Motor dauerhaft einschalten, ROMs ausblenden
STA *$01 ; und zurückschreiben
LDA #$02 ; Startwert für Timer A von CIA 2 auf $0232 (560)
STA $DD05 ; High-Byte
LDA #$32
STA $DD04 ; Low-Byte
LDA #$19 ; Timer A mit Startwert laden und im "One Shot"-Modus starten
STA $DD0E
LDA #$7F ; Alle Interrupts von CIA 1 abschalten
STA $DC0D
STA $D020 ; Bildschirmrand hellgrau
LDA #$90 ; Einzige Interruptquelle für CIA ist Impuls von Datassette
STA $DC0D
LDY #$00 ; Blockzähler
STY *$AB ; initialisieren
LDA #$0F ; Synchronisationszeichen für ersten Block
STA *$A3 ; setzen
JSR P_CC ; Meldung "SEARCHING " ausgeben
CA00: LDY *$AA ; Unnötiger Code, soll nur Verwirrung stiften
LDA ($A8),Y
STA *$AC
INY
LDA ($A8),Y
STA *$AD
JSR P_CI ; Mit Datasette synchronisieren
JSR P_CJ ; 4 Byte-Dateiheader in Puffer an $A4...$A7 einlesen
LDA *$A5 ; Blocknummer aus Header holen
CMP *$AB ; und mit aktuellem Blockzähler vergleichen
BEQ CA01 ; Sprung falls gleich (gesuchter Block gefunden)
JSR P_CE ; sonst Fehlermeldung "BLOCK? " ausgeben
BEQ CA00 ; Unbedingter Sprung, neu synchronisieren und nächster Block
CA01: JSR P_CD ; Meldung "SEARCHING " durch Meldung "LOADING " ersetzen
JSR P_CK ; Nächsten Programmblock einlesen
BEQ CA02 ; Sprung falls Block samt Prüfsumme korrekt gelesen
JSR P_CE ; sonst Fehlermeldung "BLOCK? " ausgeben
BEQ CA00 ; Unbedingter Sprung, neu synchronisieren und nächster Block
CA02: LDA *$A4 ; Synchronisationszeichen für nächsten Block
STA *$A3 ; setzen
INC *$AB ; Blockzähler erhöhen
LDA $D020 ; Bildschirm-Rahmenfarbe holen
EOR #$02 ; wechseln
STA $D020 ; und zurückschreiben
JSR P_CL ; Byte aus 32 Byte-Ringpuffer an $033C nach A holen
STA *$AC ; und als Low-Byte der Einsprungadresse merken
JSR P_CL ; Byte aus 32 Byte-Ringpuffer an $033C nach A holen
STA *$AD ; und als High-Byte der Einsprungadresse merken
JMP ($00AC) ; Nächsten Programmteil anspringen ($C242=BA00 oder $BF00=P_DA)
; Inhalt von <A> als Hexadezimalzahl an $BE00,Y darstellen
P_CB: PHA ; Wert von A retten
LSR A ; High Nibble isolieren
LSR A
LSR A
LSR A
JSR CB00 ; und ausgeben
PLA ; Wert von A zurückholen
INY ; Schreibindex für Bildschirm erhöhen
AND #$0F ; Low Nibble isolieren
CB00: ORA #$30 ; Nibble in Bereich '0'...'9' bringen
CMP #$3A ; Größer als '9'?
BCC CB01 ; Sprung falls nicht
SBC #$39 ; sonst in Bereich 'A'...'F' bringen
CB01: STA $BE00,Y ; und ausgeben
RTS
; Meldung "SEARCHING " auf Bildschirm ausgeben
P_CC: LDX #T100-T100 ; Zeiger auf Bildschirmmeldung "SEARCHING "
LDY #$28 ; Schreibindex für Bildschirm (zweite Zeile)
JMP CE00 ; Bildschirmmeldung ausgeben
; Meldung "SEARCHING " durch Meldung "LOADING " ersetzen
P_CD: LDY #$28 ; Schreibindex für Bildschirm (zweite Zeile)
LDA #$20 ; ' '
CD00: STA $BE00,Y ; Meldung "SEARCHING " in zweiter Zeile löschen
INY ; Schreibindex erhöhen
CPY #$35 ; Schon ganze Meldung gelöscht?
BNE CD00 ; Rücksprung falls noch nicht
LDX #T101-T100 ; Zeiger auf Bildschirmmeldung "LOADING "
LDY #$02 ; Schreibindex für Bildschirm
JMP CE00 ; Bildschirmmeldung ausgeben
; Fehlermeldung " BLOCK? " und ggf. "REWIND TO " ausgeben
P_CE: LDX #T102-T100 ; Zeiger auf Bildschirmmeldung " BLOCK? "
LDY #$02 ; Schreibindex für Bildschirm
JSR CE00 ; Bildschirmmeldung ausgeben
LDA *$AB ; Nummer des gesuchten Blocks holen
LDY #$32 ; Schreibindex für Bildschirm
JSR P_CB ; und als Hexadezimalzahl ausgeben
LDA *$A5 ; Blocknummer aus Blockheader holen
CMP *$AB ; mit Nummer des gesuchten Blocks vergleichen
BCC P_CC ; Weitersuchen falls kleiner
LDX #T003-T000 ; Zeiger auf Bildschirmmeldung "REWIND TO "
LDY #$28 ; Schreibindex für Bildschirm
CE00: LDA T100,X ; Meldung byteweise lesen
STA $BE00,Y ; und auf Bildschirm schreiben
INY ; Schreibindex erhöhen
INX ; Leseindex erhöhen
CMP #$20 ; Leerzeichen (Endekennzeichen)?
BNE CE00 ; Rücksprung wenn nicht Leerzeichen
RTS
; Mit Datassette synchronisieren
P_CF: SEI ; Interrupts abschalten (unnötig in Interruptroutine)
JSR P_CH ; Ein Bit von Band einlesen
LDA *$BD ; Gelesenes Byte holen
CMP *$A3 ; und mit Synchronisations-Kennzeichen vergleichen
BNE CG00 ; Rückkehr aus Interrupt falls nicht passend
LDA #$01 ; Speicher für gelesene Bits
STA *$BD ; als Zähler für 8 Bit initialisieren
LDA #<P_CG ; Interruptvektor auf Routine P_BG richten
STA $0314 ; Low-Byte
LDA #>P_CG
STA $0315 ; High-Byte
JMP CG00 ; Alle Register zurückholen, dann Rücksprung aus Interrupt
; Interruptroutine für Band lesen, Byte in Ringpuffer schreiben
P_CG: SEI ; Interrupts abschalten (unnötig in Interruptroutine)
JSR P_CH ; Ein Bit von Band einlesen und nach $BD
BCC CG00 ; Sprung falls noch nicht 8 Bit empfangen
LDY *$72 ; Schreibzeiger für Ringpuffer holen
LDA *$BD ; Von Band gelesenes Byte holen
STA $033C,Y ; und in 32 Byte-Ringpuffer schreiben
LDA #$01 ; Kennzeichen für "Weitere 8 Bit einlesen"
STA *$BD ; in Puffer für empfangene Bits schreiben
INY ; Schreibzeiger erhöhen
TYA ; nach A übertragen
AND #$1F ; auf Wertebereich 0..31 begrenzen
STA *$72 ; und zurückschreiben
CG00: JMP $FEBC ; Alle Register zurückholen, dann Rücksprung aus Interrupt
; Bit von Band lesen, liefert CS wenn 8. Bit (vollständiges Byte empfangen)
P_CH: LDA #$19 ; Timer A von CIA 2 mit Startwert laden
STA $DD0E ; und im "One Shot"-Betrieb starten
LDA $DC0D ; Wartende Interrupts CIA 1 löschen
LDA $DD0D ; Wartende Interrupts CIA 2 löschen
LSR A ; Unterlauf Timer A von CIA 1?
ROL *$BD ; Unterlaufflag als empfangenes Bit merken
RTS
; Mit Datasette synchronisieren
P_CI: SEI ; Interrupts abschalten, bevor Interruptvektor geändert wird
LDA #$00
STA *$72 ; Schreibzeiger auf 32 Byte-Ringpuffer zurücksetzen
STA *$71 ; Lesezeiger auf 32 Byte-Ringpuffer zurücksetzen
STA *$BD ; Speicherzelle für Bits vom Band initialisieren
LDA #<P_CF ; Interruptvektor auf P_CF (bitweise Synchronisation) richten
STA $0314 ; Low-Byte
LDA #>P_CF
STA $0315 ; High-Byte
CLI ; Interrupts wieder zulassen
CI00: JSR P_CL ; Byte von Band aus 32 Byte-Ringpuffer nach A holen
CMP *$A3 ; Ist Byte immer noch gleich Synchronisationskennzeichen?
BEQ CI00 ; Weiter warten falls ja
EOR #$FF ; Gelesenes Byte invertieren
CMP *$A3 ; Ist invertiertes Byte gleich Synchronisationskennzeichen?
BNE P_CI ; Neuer Synchronisationsversuch falls nicht
JSR P_CL ; Byte von Band aus 32 Byte-Ringpuffer nach A holen
CMP *$A3 ; Mit ursprünglichem Synchronisationskennzeichen vergleichen
BNE P_CI ; Neuer Synchronisationsversuch falls nicht
RTS
; 4 Byte-Dateiheader in Puffer an $A4...$A7 einlesen
P_CJ: LDY #$00 ; Schreibindex initialisieren
STY *$A8 ; Prüfsumme initialisieren
CJ00: JSR P_CL ; Byte von Band aus 32 Byte-Ringpuffer nach A holen
STA $00A4,Y ; und in Puffer an $A4...$A7 schreiben
INY ; Schreibindex erhöhen
CPY #$04 ; Schon 4 Bytes eingelesen?
BNE CJ00 ; Rücksprung falls noch nicht
PLA ; Low-Byte der Rücksprungadresse-1 zurückholen
STA *$AC ; und merken
PLA ; High-Byte der Rücksprungadresse-1 zurückholen
STA *$A9 ; und merken (für Entschlüsseln an DA01/DA02)
PHA ; und wieder auf Stack
LDA *$AC ; Low-Byte der Rücksprungadresse-1
PHA ; auch wieder auf Stack
LDA *$A5 ; Blocknummer aus Header holen
LDY #$0A ; Schreibindex für Ausgabe auf Bildschirm
JMP P_CB ; als Hexadezimalzahl an $BE00,Y darstellen
; Einen 64/256 Byte-Block von Band lesen und an ($A6) in Speicher schreiben
P_CK: LDY #$00
STY *$AA ; Prüfsumme initialisieren
STY *$AE ; Zahl der zu lesenden Bytes auf 256 setzen
LDA *$A5 ; Nummer des aktuellen Blocks holen
BNE CK00 ; Sprung falls nicht Block 0
LDA #$40 ; sonst Zahl der zu lesenden Bytes auf 64 setzen
STA *$AE ;
CK00: JSR P_CL ; Byte aus 32 Byte-Ringpuffer an $033C nach A holen
STA ($A6),Y ; und an Zieladresse im Speicher schreiben
EOR *$AA ; und in Prüfsumme einarbeiten
STA *$AA
INY ; Schreibindex erhöhen
CPY *$AE ; und mit Anzahl zu lesender Bytes vergleichen
BNE CK00
JSR P_CL ; Byte aus 32 Byte-Ringpuffer an $033C nach A holen
TAY ; und als Zahl zu überlesender Füllbytes nach Y
CK01: JSR P_CL ; Byte aus 32 Byte-Ringpuffer an $033C nach A holen
DEY ; Schon alle Füllbytes überlesen?
BNE CK01 ; Sprung falls nicht
JSR P_CL ; Prüfsumme aus 32 Byte-Ringpuffer an $033C nach A holen
CMP *$AA ; und mit errechneter Prüfsumme vergleichen
RTS
; Byte aus 32 Byte-Ringpuffer an $033C nach A holen
P_CL: LDX *$71 ; Lesezeiger holen
CPX *$72 ; und mit Schreibzeiger vergleichen
BEQ P_CL ; Rücksprung wenn gleich (kein Byte im Puffer)
LDA $033C,X ; Byte aus Puffer holen
PHA ; und retten
INX ; Lesezeiger weiterbewegen
TXA ; und nach A übertragen
AND #$1F ; auf Wertebereich 0..31 beschränken
STA *$71 ; und zurückschreiben
PLA ; Byte aus Puffer zurückholen
RTS
T100: DB $13,$05,$01,$12,$03,$08,$09,$0E,$07,$20 ; "SEARCHING "
T101: DB $0C,$0F,$01,$04,$09,$0E,$07,$20 ; "LOADING "
T102: DB $60,$02,$0C,$0F,$03,$0B,$3F,$20 ; " BLOCK? "
T103: DB $12,$05,$17,$09,$0E,$04,$60,$14,$0F,$20 ; "REWIND TO "
Abschließende Decodierung und Start des Spiels[Bearbeiten | Quelltext bearbeiten]
Diese letzte, kurze Routine findet sich im letzten Datenblock auf Band und wird unmittelbar nach dem Laden angesprungen. Sie ist im Adressbereich $BF20–$BFFF zunächst noch verschlüsselt und decodiert sich unmittelbar vor der Ausführung durch XOR-Verknüpfung mit dem Programmcode des Schnellladers — im folgenden Listing ist die Routine in bereits decodierter Form wiedergegeben. Anschließend entschlüsselt sie auch noch große Teile des soeben geladenen Programmcodes, indem sie ihn mehrfach mit Codeausschnitten des zweiten Schnellladers XOR-verknüpft.
An Komplexität nur noch schwerlich zu übertreffen dürfte die Methode sein, wie die Routine schließlich das Spiel startet: Zwischen den Labels DA01 und DA02 biegt sie den BREAK-Vektor auf den Codabschnitt an P_DB um, und überschreibt sich anschließend bei DA04 selbst mit Nullbytes — also dem Opcode für den BRK-Befehl. Sobald hierbei der Anfang der Schleife überschrieben wird, leitet die erste Ausführung einer BRK-Instruktion den Programmfluss nach P_DB um — von wo aus ein indirekter Sprung über den soeben von Band nachgeladenen Adresszeiger an $79/$7A schließlich zum Programmanfang an Adresse $0847 verzweigt.
PRG $BF00
; 8 zusätzliche Bytes nachladen und mit ihrer Hilfe Programmcode entschlüsseln
P_DA: LDY #$00 ; Schreibindex initialisieren
DA00: JSR P_CL ; Byte aus 32 Byte-Ringpuffer an $033C nach A holen
STA $0073,Y ; und an Adresse $0073...$007A schreiben
INY ; Schreibindex erhöhen
CPY #$08 ; Schon 8 Byte von Band gelesen
BNE DA00 ; Rücksprung falls noch nicht 8 Byte
; Inhalt von $0073...$007A: $00,$02,$00,$40,$00,$BD,$47,$08
; Code an $BF20..$BFFF durch EOR-Verknüpfung mit Schnelllader-Code entschlüsseln
LDY #$20
DA01: LDA $BF00,Y
EOR ($A8),Y ; Entspricht "EOR $C200,Y"
STA $BF00,Y
INY
BNE DA01
; Ab hier schon entschlüsselter Code
SEI ; Interrupts abschalten, bevor Interruptvektor geändert wird
LDA #$2F ; Datenrichtungsregister des Prozessorports auf Standardwert
STA *$00
LDA #$7F ; Alle Interrupts von CIA 1 abschalten
STA $DC0D
LDA #<P_DB ; BREAK-Vektor auf P_DB richten
STA $0316 ; Low-Byte
LDA #>P_DB
STA $0317 ; High-Byte
LDA #$C1 ; Interruptvektor auf Befehl "RTI" richten
STA $0318 ; Low-Byte
LDA #$FE
STA $0319 ; High-Byte
; Code im Bereich $4000...$BCFF entschüsseln
DA02: LDA ($75),Y ; Verschlüsseltes Byte lesen
EOR ($A8),Y ; Entspricht "EOR $C200,Y"
INC *$A9
EOR ($A8),Y ; Entspricht "EOR $C300,Y"
DEC *$A9
STY *$7B ; Schreibindex merken
PHA ; A auf Stack retten
TYA ; Y auf Wertebereich 0..63 beschränken
AND #$3F
TAY
PLA ; A zurückholen
EOR ($73),Y ; Entspricht "EOR $C200,Y" mit Y im Bereich 0..63
LDY *$7B ; Schreibindex zurückholen
STA ($75),Y ; Entschlüsseltes Byte zurückschreiben
INY ; Schreibindex erhöhen
BNE DA02 ; Rücksprung falls noch nicht 256 Bytes entschlüsselt
INC *$76 ; High-Byte des Lesezeigers erhöhen
LDA *$76 ; und nach A holen
CMP *$78 ; Ende des Programmcode erreicht?
BNE DA02 ; Sprung falls Ende noch nicht erreicht
LDA $D011 ; Bildschirm wieder einschalten
ORA #$10
STA $D011
LDA *$01 ; Datassettenmotor anhalten
ORA #$20
STA *$C0 ; Flag für Bandmotor merken
STA *$01
LDA #$81 ; Unterlauf Timer A als einzige Interruptquelle von CIA 1
STA $DC0D
LDA *$01 ; Prozessorport lesen
AND #$FD ; alle ROMs ausblenden
STA *$01 ; und zurückschreiben
LDY #$00 ; Für Umkopieren von 8 kByte Programmcode
STY *$73 ; Low-Byte des Schreibzeigers auf 0 setzen
STY *$75 ; Low-Byte des Lesezeigers auf 0 setzen
LDA #$20 ; Schreibzeiger auf $2000 richten
STA *$74
LDA #$E0 ; Lesezeiger auf $E000 richten
STA *$76
DA03: LDA ($75),Y ; Speicherbereich $E000...$FFFF nach $2000...$3FFF umkopieren
STA ($73),Y
INY ; Schreibindes erhöhen
BNE DA03 ; Rücksprung falls noch nicht 256 Bytes
INC *$74 ; High-Byte des Schreibzeigers erhöhen
INC *$76 ; High-Byte des Lesezeigers erhöhen
BNE DA03 ; Rücksprung falls noch nicht ganzer Speicherbereich kopiert
LDA *$01 ; Prozessorport lesen
ORA #$02 ; ROMs wieder in Adressraum einblenden
STA *$01 ; und zurückschreiben
; Aktuell ausgeführten Code mit $00 überschreiben, löst BRK (Sprung nach P_DB) aus
LDY #$00 ; Schreibindex
TYA ; A=$00
DA04: STA $BF00,Y ; Speicher mit $00 überschreiben
INY ; Schreibindex erhöhen
BNE DA04 ; Weiter kopieren (löst BRK aus, sobald Y=DA04+$01)
PRG $BFF3
P_DB: PLA ; Nicht mehr benötigte Rücksprungadressen von Stack löschen
PLA
PLA
PLA
PLA
PLA
JSR $FF8A ; I/O initialisieren
CLI ; Interrupts wieder zulassen
JMP ($0079) ; Start des Spiels durch Sprung nach $0847