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