Difference between revisions of "Langenscheidt Protection"
| (One intermediate revision by the same user not shown) | |||
| Line 21: | Line 21: | ||
== Mechanism == | == Mechanism == | ||
| − | + | # When the user issues a LOAD"*",8,1 the drive loads the BAM track 18 sector 0 to a buffer at $0700. The sector data of track 18 sector 0 contains already drive code. | |
# The disk boots via a short C64-side autostart stub that immediately hands off to the drive with a single, standard KERNAL <code>M-E $07B0</code> command (Memory-Execute) — confirmed to be the '''only''' KERNAL/IEC command used in an entire session, by a full-session trace that logged <code>CIOUT</code> calls from boot through menu interaction and found exactly five, spelling <code>M-E</code> + <code>$07B0</code> and nothing else, ever again. | # The disk boots via a short C64-side autostart stub that immediately hands off to the drive with a single, standard KERNAL <code>M-E $07B0</code> command (Memory-Execute) — confirmed to be the '''only''' KERNAL/IEC command used in an entire session, by a full-session trace that logged <code>CIOUT</code> calls from boot through menu interaction and found exactly five, spelling <code>M-E</code> + <code>$07B0</code> and nothing else, ever again. | ||
# From that point on, the drive-resident program (itself streamed down from disk in 256-byte pages immediately after the handoff) drives '''everything''' — including standard 1541 DOS ROM job-dispatch calls it invokes internally — over a private, raw bit-banged protocol on CIA2 Port A (<code>$DD00</code>), completely bypassing the KERNAL serial routines for the rest of the session. | # From that point on, the drive-resident program (itself streamed down from disk in 256-byte pages immediately after the handoff) drives '''everything''' — including standard 1541 DOS ROM job-dispatch calls it invokes internally — over a private, raw bit-banged protocol on CIA2 Port A (<code>$DD00</code>), completely bypassing the KERNAL serial routines for the rest of the session. | ||
| Line 283: | Line 283: | ||
== Two-stage bootstrap: commented disassembly == | == Two-stage bootstrap: commented disassembly == | ||
| + | |||
| + | === Stage 0 — Drive loads BAM to buffer at $0700 in drive RAM === | ||
| + | |||
=== Stage 1 — C64 autostart hands off to the drive === | === Stage 1 — C64 autostart hands off to the drive === | ||
| Line 698: | Line 701: | ||
* [[Wörter Rennen mit System]] | * [[Wörter Rennen mit System]] | ||
* [[Jagd auf Vokabeln]] | * [[Jagd auf Vokabeln]] | ||
| + | |||
| + | = Archived first manual analysis = | ||
| + | |||
| + | As found on [[Wörter Rennen mit System]] | ||
| + | |||
| + | There are 4 sectors on track 18 in regular format on the disk: | ||
| + | |||
| + | Sector 0, 1, 8 and 11. | ||
| + | |||
| + | The rest of the tracks were written in speed 0. | ||
| + | Each of these tracks contain a single additional regular sector header that was written with speed 3. | ||
| + | |||
| + | ===== Initial code loaded to C64 ===== | ||
| + | |||
| + | The first file is linked to t18 s8 that loads auto start code to the C64 from $02d1 to $03cb. | ||
| + | Execution entry point is at $0334. | ||
| + | |||
| + | .C:0334 A9 0B LDA #$0B | ||
| + | .C:0336 20 B0 03 JSR $03B0 | ||
| + | .C:0339 A9 2D LDA #$2D // "-" -> Send Memory Execute at $07b0 to floppy | ||
| + | .C:033b 20 A8 FF JSR $FFA8 // Output byte to serial | ||
| + | .C:033e A9 45 LDA #$45 // "E" | ||
| + | .C:0340 20 A8 FF JSR $FFA8 // Output byte to serial | ||
| + | .C:0343 A9 B0 LDA #$B0 // Address $07b0 | ||
| + | .C:0345 20 A8 FF JSR $FFA8 // Output byte to serial | ||
| + | .C:0348 A9 07 LDA #$07 // | ||
| + | .C:034a 20 A8 FF JSR $FFA8 // Output byte to serial | ||
| + | .C:034d 20 FE ED JSR $EDFE // Send UNLISTEN | ||
| + | .C:0350 A2 FF LDX #$FF | ||
| + | .C:0352 AD 00 DD LDA $DD00 | ||
| + | .C:0355 A0 10 LDY #$10 | ||
| + | .C:0357 CA DEX | ||
| + | .C:0358 D0 FD BNE $0357 | ||
| + | .C:035a 88 DEY | ||
| + | .C:035b D0 FA BNE $0357 | ||
| + | .C:035d A9 03 LDA #$03 | ||
| + | .C:035f 8D 00 DD STA $DD00 | ||
| + | .C:0362 AD 00 DD LDA $DD00 | ||
| + | .C:0365 CD 00 DD CMP $DD00 | ||
| + | .C:0368 F0 FB BEQ $0365 | ||
| + | .C:036a 20 80 03 JSR $0380 | ||
| + | .C:036d 85 D1 STA $D1 | ||
| + | .C:036f 20 80 03 JSR $0380 | ||
| + | .C:0372 85 D2 STA $D2 | ||
| + | .C:0374 A0 00 LDY #$00 | ||
| + | .C:0376 20 80 03 JSR $0380 | ||
| + | .C:0379 91 D1 STA ($D1),Y | ||
| + | .C:037b 88 DEY | ||
| + | .C:037c D0 F8 BNE $0376 | ||
| + | .C:037e F0 EA BEQ $036A | ||
| + | .C:0380 78 SEI | ||
| + | .C:0381 A9 27 LDA #$27 | ||
| + | .C:0383 8D 00 DD STA $DD00 | ||
| + | .C:0386 2C 00 DD BIT $DD00 | ||
| + | .C:0389 50 FB BVC $0386 | ||
| + | .C:038b A9 03 LDA #$03 | ||
| + | .C:038d 8D 00 DD STA $DD00 | ||
| + | .C:0390 A2 08 LDX #$08 | ||
| + | .C:0392 CA DEX | ||
| + | .C:0393 D0 FD BNE $0392 | ||
| + | .C:0395 A2 04 LDX #$04 | ||
| + | .C:0397 AD 00 DD LDA $DD00 | ||
| + | .C:039a 0A ASL A | ||
| + | .C:039b 08 PHP | ||
| + | .C:039c 0A ASL A | ||
| + | .C:039d 26 2D ROL $2D | ||
| + | .C:039f 28 PLP | ||
| + | .C:03a0 26 2D ROL $2D | ||
| + | .C:03a2 CA DEX | ||
| + | .C:03a3 D0 F2 BNE $0397 | ||
| + | .C:03a5 A9 17 LDA #$17 | ||
| + | .C:03a7 8D 00 DD STA $DD00 | ||
| + | .C:03aa EA NOP | ||
| + | .C:03ab EA NOP | ||
| + | .C:03ac EA NOP | ||
| + | .C:03ad A5 2D LDA $2D | ||
| + | .C:03af 60 RTS | ||
| + | .C:03b0 8D 11 D0 STA $D011 | ||
| + | .C:03b3 A9 08 LDA #$08 | ||
| + | .C:03b5 20 0C ED JSR $ED0C | ||
| + | .C:03b8 A9 6F LDA #$6F | ||
| + | .C:03ba 20 B9 ED JSR $EDB9 | ||
| + | .C:03bd A9 4D LDA #$4D | ||
| + | .C:03bf 4C A8 FF JMP $FFA8 | ||
| + | .C:03c2 4C FE ED JMP $EDFE | ||
| + | |||
| + | ===== Load Translation Table ===== | ||
| + | |||
| + | The floppy has loaded the previous t18 s0 to the buffer at $0700. | ||
| + | Aside from the disk title the sector also contains code at $0700 and $07b0. | ||
| + | |||
| + | .8:0700 A5 04 LDA $04 // $04 == #$E0 | ||
| + | .8:0702 C9 E2 CMP #$E2 // is it #$E2? | ||
| + | .8:0704 F0 27 BEQ $072D // send command find sector | ||
| + | .8:0706 20 56 F5 JSR $F556 // wait for sync | ||
| + | .8:0709 50 FE BVC $0709 | ||
| + | .8:070b B8 CLV | ||
| + | .8:070c AD 01 1C LDA $1C01 // check if first byte is #$6A | ||
| + | .8:070f C9 6A CMP #$6A | ||
| + | .8:0711 D0 F3 BNE $0706 // No? try next Sync | ||
| + | .8:0713 A2 64 LDX #$64 // We are at the first data (6A) sector we found after changing to track 2 | ||
| + | .8:0715 50 FE BVC $0715 // wait for byte from disk | ||
| + | .8:0717 B8 CLV | ||
| + | .8:0718 AD 01 1C LDA $1C01 // read byte from disk | ||
| + | .8:071b 95 00 STA $00,X // read 156 bytes and put them to $64 to $ff | ||
| + | .8:071d E8 INX | ||
| + | .8:071e D0 F5 BNE $0715 | ||
| + | .8:0720 55 64 EOR $64,X // run EOR checksum over all 156 bytes | ||
| + | .8:0722 E8 INX | ||
| + | .8:0723 E0 9C CPX #$9C // 156 | ||
| + | .8:0725 D0 F9 BNE $0720 | ||
| + | .8:0727 EA NOP | ||
| + | .8:0728 29 7F AND #$7F // use lower 7 bits of checksum for error code, should be #$39 | ||
| + | .8:072a 4C 69 F9 JMP $F969 // error entry disk controller -> user code execution continues in $078d | ||
| + | .8:072d 4C CA F6 JMP $F6CA // send command find sector | ||
| + | .. disk title data | ||
| + | .8:07b0 A9 02 LDA #$02 <- The C64 does start execution in the floppy here | ||
| + | .8:07b2 8D 78 02 STA $0278 // init some values for user file execute - number of file names | ||
| + | .8:07b5 A9 2A LDA #$2A | ||
| + | .8:07b7 8D 00 02 STA $0200 | ||
| + | .8:07ba A9 01 LDA #$01 | ||
| + | .8:07bc 8D 74 02 STA $0274 | ||
| + | .8:07bf A9 00 LDA #$00 | ||
| + | .8:07c1 85 7E STA $7E // last handled program | ||
| + | .8:07c3 4C AB E7 JMP $E7AB // Execute USR file, loads t18 s11 into floppy at $0730 and executes it. | ||
| + | |||
| + | The upper routine loads t18 s11 to the floppy ram starting at $0730 to $07b5. Previously there was unused code space due to disk title space in t18 s0. | ||
| + | Execution continues at $0730. | ||
| + | |||
| + | ===== Check Track 3 Header / Load Translation Table / Create Sparse Table ===== | ||
| + | |||
| + | From t18 s11 | ||
| + | .8:0730 A2 03 LDX #$03 // Seek to track 3 | ||
| + | .8:0732 86 0E STX $0E | ||
| + | .8:0734 A9 00 LDA #$00 // Set sector 0 to look for | ||
| + | .8:0736 85 0F STA $0F | ||
| + | .8:0738 A9 B0 LDA #$B0 // Set new job: find sector header | ||
| + | .8:073a 85 04 STA $04 | ||
| + | .8:073c A5 04 LDA $04 | ||
| + | .8:073e 30 FC BMI $073C // wait for result code | ||
| + | .8:0740 C9 01 CMP #$01 // 1? | ||
| + | .8:0742 F0 3A BEQ $077E // OK, go on, otherwise lockup loop | ||
| + | .8:0744 EE FE 02 INC $02FE // 0 -> 1 | ||
| + | .8:0747 AD FE 02 LDA $02FE // Lockup | ||
| + | .8:074a D0 FB BNE $0747 // Loop | ||
| + | .8:074c A9 B0 LDA #$B0 // not used | ||
| + | .8:074e 85 04 STA $04 // not used | ||
| + | .8:0750 A5 04 LDA $04 // not used | ||
| + | .8:0752 30 FC BMI $0750 // not used | ||
| + | .8:0754 C9 01 CMP #$01 // not used | ||
| + | .8:0756 F0 26 BEQ $077E // not used | ||
| + | .8:0758 CE FE 02 DEC $02FE // not used | ||
| + | .8:075b AD FE 02 LDA $02FE // not used | ||
| + | .8:075e D0 FB BNE $075B // not used | ||
| + | .8:0760 CE FE 02 DEC $02FE // not used | ||
| + | .8:0763 AD FE 02 LDA $02FE // not used | ||
| + | .8:0766 D0 FB BNE $0763 // not used | ||
| + | .8:0768 A9 B0 LDA #$B0 // not used | ||
| + | .8:076a 85 04 STA $04 // not used | ||
| + | .8:076c A5 04 LDA $04 // not used | ||
| + | .8:076e 30 FC BMI $076C // not used | ||
| + | .8:0770 C9 01 CMP #$01 // not used | ||
| + | .8:0772 F0 0A BEQ $077E // not used | ||
| + | .8:0774 EE FE 02 INC $02FE // not used | ||
| + | .8:0777 AD FE 02 LDA $02FE // not used | ||
| + | .8:077a D0 FB BNE $0777 // not used | ||
| + | .8:077c F0 BA BEQ $0738 // not used | ||
| + | .8:077e 20 F5 07 JSR $07F5 // check if we are on track 3 | ||
| + | .8:0781 A9 22 LDA #$22 // set buffer 4, set track 2 | ||
| + | .8:0783 85 0E STA $0E | ||
| + | .8:0785 A9 E0 LDA #$E0 // Job: motor on, seek track and execute code at $0700 | ||
| + | .8:0787 85 04 STA $04 // The code at $0700 loads the forward translation table to $A4 | ||
| + | .8:0789 A5 04 LDA $04 | ||
| + | .8:078b 30 FC BMI $0789 // wait for result | ||
| + | .8:078d C9 39 CMP #$39 // was checksum result #$39 is good | ||
| + | .8:078f D0 9F BNE $0730 // if not, go to track 3 sector header find again | ||
| + | .8:0791 C6 31 DEC $31 // $31: #$07 -> #$06 this is our internal sector id for the next sector | ||
| + | .8:0793 78 SEI | ||
| + | .8:0794 A9 EE LDA #$EE // What does this do? Init PCR? | ||
| + | .8:0796 8D 0C 1C STA $1C0C | ||
| + | .8:0799 EA NOP | ||
| + | .8:079a A2 00 LDX #$00 | ||
| + | .8:079c B4 64 LDY $64,X | ||
| + | .8:079e 8A TXA | ||
| + | .8:079f 99 00 01 STA $0100,Y // [$100 + [$64 + X]] = X | ||
| + | .8:07a2 E8 INX // this means, the loaded data at $64 are used as indices to a new sparse table at $0100. | ||
| + | .8:07a3 E0 40 CPX #$40 // bytes at these indexed positions are filled counting upwards. See table 1 and table 2. | ||
| + | .8:07a5 90 F5 BCC $079C // The counter here that is also written to table 2 is the actual 6 bit byte value | ||
| + | .8:07a7 20 00 06 JSR $0600 // We have now our translation table, load first sector (id 6) to $0200 | ||
| + | .8:07aa B9 00 02 LDA $0200,Y // copy to $0300 and execute | ||
| + | .8:07ad 99 00 03 STA $0300,Y | ||
| + | .8:07b0 C8 INY | ||
| + | .8:07b1 D0 F7 BNE $07AA | ||
| + | .8:07b3 4C 00 03 JMP $0300 | ||
| + | .. remaining bytes | ||
| + | .. | ||
| + | From t18 s0, loads a custom sector header | ||
| + | .8:07c6 20 56 F5 JSR $F556 // wait for sync | ||
| + | .8:07c9 50 FE BVC $07C9 // wait for data byte | ||
| + | .8:07cb B8 CLV | ||
| + | .8:07cc AD 01 1C LDA $1C01 // read data byte | ||
| + | .8:07cf C9 73 CMP #$73 // #$73 is the code for a sector header | ||
| + | .8:07d1 D0 F3 BNE $07C6 // no? then try again | ||
| + | .8:07d3 50 FE BVC $07D3 // wait for data byte | ||
| + | .8:07d5 B8 CLV | ||
| + | .8:07d6 AD 01 1C LDA $1C01 // read a skip byte | ||
| + | .8:07d9 50 FE BVC $07D9 // wait for data byte | ||
| + | .8:07db B8 CLV | ||
| + | .8:07dc AC 01 1C LDY $1C01 // read data byte | ||
| + | .8:07df B9 00 01 LDA $0100,Y // translate to 6 bit byte through lookup table | ||
| + | .8:07e2 C5 31 CMP $31 // $31 contains the wanted sector number (first one read is 6) | ||
| + | .8:07e4 D0 E0 BNE $07C6 // no? then next sector header | ||
| + | .8:07e6 20 03 06 JSR $0603 // Read sector data | ||
| + | .8:07e9 E8 INX // with the correct checksum X is #$FF when $0603 returns | ||
| + | .8:07ea D0 DA BNE $07C6 // wrong checksum? try read again | ||
| + | .8:07ec A5 31 LDA $31 // set next sector id +2 (e.g. 8) | ||
| + | .8:07ee 69 02 ADC #$02 | ||
| + | .8:07f0 29 0F AND #$0F | ||
| + | .8:07f2 85 31 STA $31 | ||
| + | .8:07f4 60 RTS | ||
| + | .8:07f5 A5 22 LDA $22 // current track | ||
| + | .8:07f7 C9 03 CMP #$03 // are we on 3? | ||
| + | .8:07f9 D0 94 BNE $078F // repeat, set track 3, find header | ||
| + | .8:07fb 09 20 ORA #$20 // | ||
| + | .8:07fd 85 22 STA $22 // #$23 -> $22 | ||
| + | .8:07ff 60 RTS | ||
| + | |||
| + | Sector id translation: Mark for header is after snyc #$73, then a skip byte follow, typically #$4d, then the header byte follows: | ||
| + | #$4A -> $31 = 0x00 | ||
| + | #$4B -> $31 = 0x01 | ||
| + | #$4D -> $31 = 0x02 | ||
| + | #$4E -> $31 = 0x03 | ||
| + | #$52 -> $31 = 0x04 | ||
| + | #$53 -> $31 = 0x05 | ||
| + | #$55 -> $31 = 0x06 | ||
| + | #$56 -> $31 = 0x07 | ||
| + | #$57 -> $31 = 0x08 | ||
| + | #$59 -> $31 = 0x09 | ||
| + | #$5A -> $31 = 0x0A | ||
| + | #$5B -> $31 = 0x0B | ||
| + | #$5D -> $31 = 0x0C | ||
| + | #$5E -> $31 = 0x0D | ||
| + | #$65 -> $31 = 0x0E | ||
| + | #$66 -> $31 = 0x0F | ||
| + | As one sees the same lookup is used as installed through table 1 in table 2. | ||
| + | |||
| + | ===== Table 1 (Translation Table) ===== | ||
| + | With the routine at $079a this table is translated to $0100 such that #$00 -> $014a, #$01 -> $014b, #$02 -> 014d a.s.o. | ||
| + | The background is: The floppy can not store native bytes with more than two zeros in a row. The following table translates 6 bit wide numbers | ||
| + | from #$00 to #$3f to native bytes in the floppy sector ($64 to $a3). | ||
| + | |||
| + | With the transfer to $0100 the new table at $0100 can be used as lookup to translate the native floppy bytes to 6 bit wide bytes. | ||
| + | Such that [$0100 + native_floppy_byte] == translated_byte, where translated_byte is a 6 bit wide value from $00 to $3f. | ||
| + | |||
| + | >8:0064 4a 4b 4d 4e 52 53 55 56 JKMNRSUV | ||
| + | >8:006c 57 59 5a 5b 5d 5e 65 66 WYZ[]^ef | ||
| + | >8:0074 67 69 6a 6b 6d 6e 72 73 gijkmnrs | ||
| + | >8:007c 75 76 77 79 7a 7b 7d 7e uvwyz{}~ | ||
| + | >8:0084 95 96 97 9a 9b 9d 9e a5 ........ | ||
| + | >8:008c a6 a7 a9 aa ab ad ae b2 ........ | ||
| + | >8:0094 b3 b5 b6 b7 b9 ba bb bd ........ | ||
| + | >8:009c be ca cb cd ce d2 d3 d5 ........ | ||
| + | |||
| + | >8:00a4 57 4a 4a 4a 76 4b 52 4b WJJJvKRK | ||
| + | >8:00ac 7b 76 4d 56 7a 4a 4a 4a {vMVzJJJ | ||
| + | >8:00b4 4a 75 4e 56 4d 4d 55 4e JuNVMMUN | ||
| + | >8:00bc 4e 56 7b 4d 77 77 4d 4a NV{MwwMJ | ||
| + | >8:00c4 4a 53 55 53 4e 53 75 55 JSUSNSuU | ||
| + | >8:00cc 7d 7b 56 76 4e 7b 52 4d }{VvN{RM | ||
| + | >8:00d4 56 53 4d 4a 4e 4e 4a 75 VSMJNNJu | ||
| + | >8:00dc 4e 76 77 75 75 4e 77 4a NvwuuNwJ | ||
| + | >8:00e4 55 52 4d 4b 4d 53 7a 7a URMKMSzz | ||
| + | >8:00ec 4b 4b 7e 4e 52 4b 55 4e KK~NRKUN | ||
| + | >8:00f4 52 4d 4d 4a 52 79 4a 4d RMMJRyJM | ||
| + | >8:00fc 4a 4d 4e 53 00 c9 00 00 JMNS.... | ||
| + | |||
| + | ===== Resulting "Tempelmann" GCR ===== | ||
| + | {| | ||
| + | | | ||
| + | {| class="wikitable" | ||
| + | |-class="hintergrundfarbe6" | ||
| + | ! Daten !! Code | ||
| + | |- | ||
| + | | 000000 || 01001010 | ||
| + | |- | ||
| + | | 000001 || 01001011 | ||
| + | |- | ||
| + | | 000010 || 01001101 | ||
| + | |- | ||
| + | | 000011 || 01001110 | ||
| + | |- | ||
| + | | 000100 || 01010010 | ||
| + | |- | ||
| + | | 000101 || 01010011 | ||
| + | |- | ||
| + | | 000110 || 01010101 | ||
| + | |- | ||
| + | | 000111 || 01010110 | ||
| + | |} | ||
| + | | | ||
| + | {| class="wikitable" | ||
| + | |-class="hintergrundfarbe6" | ||
| + | ! Daten !! Code | ||
| + | |- | ||
| + | | 001000 || 01010111 | ||
| + | |- | ||
| + | | 001001 || 01011001 | ||
| + | |- | ||
| + | | 001010 || 01011010 | ||
| + | |- | ||
| + | | 001011 || 01011011 | ||
| + | |- | ||
| + | | 001100 || 01011101 | ||
| + | |- | ||
| + | | 001101 || 01011110 | ||
| + | |- | ||
| + | | 001110 || 01100101 | ||
| + | |- | ||
| + | | 001111 || 01100110 | ||
| + | |} | ||
| + | | | ||
| + | {| class="wikitable" | ||
| + | |-class="hintergrundfarbe6" | ||
| + | ! Daten !! Code | ||
| + | |- | ||
| + | | 010000 || 01100111 | ||
| + | |- | ||
| + | | 010001 || 01101001 | ||
| + | |- | ||
| + | | 010010 || 01101010 | ||
| + | |- | ||
| + | | 010011 || 01101011 | ||
| + | |- | ||
| + | | 010100 || 01101101 | ||
| + | |- | ||
| + | | 010101 || 01101110 | ||
| + | |- | ||
| + | | 010110 || 01110010 | ||
| + | |- | ||
| + | | 010111 || 01110011 | ||
| + | |} | ||
| + | | | ||
| + | {| class="wikitable" | ||
| + | |-class="hintergrundfarbe6" | ||
| + | ! Daten !! Code | ||
| + | |- | ||
| + | | 011000 || 01110101 | ||
| + | |- | ||
| + | | 011001 || 01110110 | ||
| + | |- | ||
| + | | 011010 || 01110111 | ||
| + | |- | ||
| + | | 011011 || 01111001 | ||
| + | |- | ||
| + | | 011100 || 01111010 | ||
| + | |- | ||
| + | | 011101 || 01111011 | ||
| + | |- | ||
| + | | 011110 || 01111101 | ||
| + | |- | ||
| + | | 011111 || 01111110 | ||
| + | |} | ||
| + | | | ||
| + | {| class="wikitable" | ||
| + | |-class="hintergrundfarbe6" | ||
| + | ! Daten !! Code | ||
| + | |- | ||
| + | | 100000 || 10010101 | ||
| + | |- | ||
| + | | 100001 || 10010110 | ||
| + | |- | ||
| + | | 100010 || 10010111 | ||
| + | |- | ||
| + | | 100011 || 10011010 | ||
| + | |- | ||
| + | | 100100 || 10011011 | ||
| + | |- | ||
| + | | 100101 || 10011101 | ||
| + | |- | ||
| + | | 100110 || 10011110 | ||
| + | |- | ||
| + | | 100111 || 10100101 | ||
| + | |} | ||
| + | | | ||
| + | {| class="wikitable" | ||
| + | |-class="hintergrundfarbe6" | ||
| + | ! Daten !! Code | ||
| + | |- | ||
| + | | 101000 || 10100110 | ||
| + | |- | ||
| + | | 101001 || 10100111 | ||
| + | |- | ||
| + | | 101010 || 10101001 | ||
| + | |- | ||
| + | | 101011 || 10101010 | ||
| + | |- | ||
| + | | 101100 || 10101011 | ||
| + | |- | ||
| + | | 101101 || 10101101 | ||
| + | |- | ||
| + | | 101110 || 10101110 | ||
| + | |- | ||
| + | | 101111 || 10110010 | ||
| + | |} | ||
| + | | | ||
| + | {| class="wikitable" | ||
| + | |-class="hintergrundfarbe6" | ||
| + | ! Daten !! Code | ||
| + | |- | ||
| + | | 110000 || 10110011 | ||
| + | |- | ||
| + | | 110001 || 10110101 | ||
| + | |- | ||
| + | | 110010 || 10110110 | ||
| + | |- | ||
| + | | 110011 || 10110111 | ||
| + | |- | ||
| + | | 110100 || 10111001 | ||
| + | |- | ||
| + | | 110101 || 10111010 | ||
| + | |- | ||
| + | | 110110 || 10111011 | ||
| + | |- | ||
| + | | 110111 || 10111101 | ||
| + | |} | ||
| + | | | ||
| + | {| class="wikitable" | ||
| + | |-class="hintergrundfarbe6" | ||
| + | ! Daten !! Code | ||
| + | |- | ||
| + | | 111000 || 10111110 | ||
| + | |- | ||
| + | | 111001 || 11001010 | ||
| + | |- | ||
| + | | 111010 || 11001011 | ||
| + | |- | ||
| + | | 111011 || 11001101 | ||
| + | |- | ||
| + | | 111100 || 11001110 | ||
| + | |- | ||
| + | | 111101 || 11010010 | ||
| + | |- | ||
| + | | 111110 || 11010011 | ||
| + | |- | ||
| + | | 111111 || 11010101 | ||
| + | |} | ||
| + | |} | ||
| + | |||
| + | ===== Table 2 (Sparse Table) ===== | ||
| + | sparse table for translating native_floppy_bytes to translated bytes, [$0100 + native_floppy_byte] == translated_byte | ||
| + | >8:0100 00 c9 00 00 00 00 00 00 ........ | ||
| + | >8:0108 00 00 00 00 00 00 00 00 ........ | ||
| + | >8:0110 00 00 00 00 00 00 00 00 ........ | ||
| + | >8:0118 00 00 00 00 00 00 00 00 ........ | ||
| + | >8:0120 00 00 00 00 63 f9 3a f5 ....c.:. | ||
| + | >8:0128 10 63 7e fe 00 00 02 20 .c~.... | ||
| + | >8:0130 7e fe 31 d1 31 d1 ba f4 ~.1.1... | ||
| + | >8:0138 07 e0 77 f9 7e fe 00 03 ..w.~... | ||
| + | >8:0140 e0 a1 89 07 fe eb 00 00 ........ | ||
| + | >8:0148 00 00 00 01 00 02 03 00 ........ | ||
| + | >8:0150 00 00 04 05 00 06 07 08 ........ | ||
| + | >8:0158 00 09 0a 0b 00 0c 0d 00 ........ | ||
| + | >8:0160 00 00 00 00 00 0e 0f 10 ........ | ||
| + | >8:0168 00 11 12 13 00 14 15 00 ........ | ||
| + | >8:0170 00 00 16 17 00 18 19 1a ........ | ||
| + | >8:0178 00 1b 1c 1d 00 1e 1f 00 ........ | ||
| + | >8:0180 00 00 00 00 00 00 00 00 ........ | ||
| + | >8:0188 00 00 00 00 00 00 00 00 ........ | ||
| + | >8:0190 00 00 00 00 00 20 21 22 ..... !" | ||
| + | >8:0198 00 00 23 24 00 25 26 00 ..#$.%&. | ||
| + | >8:01a0 00 00 00 00 00 27 28 29 .....'() | ||
| + | >8:01a8 00 2a 2b 2c 00 2d 2e 00 .*+,.-.. | ||
| + | >8:01b0 00 00 2f 30 00 31 32 33 ../0.123 | ||
| + | >8:01b8 00 34 35 36 2f 37 38 9e .456/78. | ||
| + | >8:01c0 fd e7 4e 52 9d a9 f9 52 ..NR...R | ||
| + | >8:01c8 54 9d 39 3a 52 3b 3c a9 T.9:R;<. | ||
| + | >8:01d0 f9 4b 3d 3e a9 3f 4b 54 .K=>.?KT | ||
| + | >8:01d8 9d a9 f9 4a 54 9d a9 f9 ...JT... | ||
| + | >8:01e0 4a 54 bc 9f 27 c9 4a 7a JT..'.Jz | ||
| + | >8:01e8 ae d7 4f d3 9d a9 7a b5 ..O...z. | ||
| + | >8:01f0 54 9d a9 7a b5 54 96 9b T..z.T.. | ||
| + | >8:01f8 29 ef 5b 6d 95 29 5a d2 ).[m.)Z. | ||
| + | |||
| + | ===== Load Custom Sector ===== | ||
| + | |||
| + | Code at $0603 loads a custom format sector to $0200 | ||
| + | .8:0600 4C C6 07 JMP $07C6 | ||
| + | // this part loads a data sector | ||
| + | .8:0603 20 56 F5 JSR $F556 //wait for sync // - load data from disk and decode | ||
| + | .8:0606 50 FE BVC $0606 | ||
| + | .8:0608 AD 01 1C LDA $1C01 // read data byte | ||
| + | .8:060b B8 CLV | ||
| + | .8:060c C9 6A CMP #$6A // #$6A is the code for a data sector | ||
| + | .8:060e D0 F3 BNE $0603 | ||
| + | .8:0610 98 TYA // Y is returned 0 from wait for sync | ||
| + | .8:0611 AA TAX // now all registers are zeroed | ||
| + | .8:0612 50 FE BVC $0612 | ||
| + | .8:0614 B8 CLV | ||
| + | .8:0615 AC 01 1C LDY $1C01 // Read 256 bytes to $0200 | ||
| + | .8:0618 59 00 01 EOR $0100,Y // translate from table, data sector gets EORed | ||
| + | .8:061b 9D 00 02 STA $0200,X // | ||
| + | .8:061e E8 INX | ||
| + | .8:061f D0 F1 BNE $0612 // loop | ||
| + | .8:0621 A2 55 LDX #$55 // Read 85 bytes more and put from $F9 to $A4 | ||
| + | .8:0623 50 FE BVC $0623 | ||
| + | .8:0625 B8 CLV | ||
| + | .8:0626 AC 01 1C LDY $1C01 | ||
| + | .8:0629 59 00 01 EOR $0100,Y // translate from table | ||
| + | .8:062c 95 A4 STA $A4,X | ||
| + | .8:062e CA DEX | ||
| + | .8:062f 10 F2 BPL $0623 | ||
| + | .8:0631 50 FE BVC $0631 | ||
| + | .8:0633 B8 CLV | ||
| + | .8:0634 AC 01 1C LDY $1C01 // Read byte 86. | ||
| + | .8:0637 59 00 01 EOR $0100,Y // translate from table | ||
| + | .8:063a AA TAX // if checksum is correct, then A should be 0 here, A -> X | ||
| + | |||
| + | ===== Translate 6-bit to 8-bit ===== | ||
| + | |||
| + | The following loop takes #$55 bytes with 6 bit values from $A4 - $FF and add two bits each to bytes in groups of 3 at $0200. | ||
| + | Y counts 1 each from the part at $A4 (downwards) | ||
| + | |||
| + | X counts 3 each for the part at $0200 (upwards) | ||
| + | |||
| + | .8:063b A0 55 LDY #$55 // 85 | ||
| + | .8:063d B9 A4 00 LDA $00A4,Y // get lower bits from $F9 to $A4, e.g. pattern --543210 | ||
| + | .8:0640 4A LSR A // pattern 0 -> Carry | ||
| + | .8:0641 3E 02 02 ROL $0202,X // [$0200 + 3 * X + 2] -bbbbbb0 | ||
| + | .8:0644 4A LSR A // next bit to carry | ||
| + | .8:0645 3E 02 02 ROL $0202,X // [$0200 + 3 * X + 2] bbbbbb01 | ||
| + | .8:0648 4A LSR A // | ||
| + | .8:0649 3E 01 02 ROL $0201,X // | ||
| + | .8:064c 4A LSR A // | ||
| + | .8:064d 3E 01 02 ROL $0201,X // [$0200 + 3 * X + 1] bbbbbb23 | ||
| + | .8:0650 4A LSR A // | ||
| + | .8:0651 3E 00 02 ROL $0200,X | ||
| + | .8:0654 4A LSR A | ||
| + | .8:0655 3E 00 02 ROL $0200,X [$0200 + 3 * X + 0] bbbbbb45 | ||
| + | .8:0658 E8 INX // step 3 bytes | ||
| + | .8:0659 E8 INX | ||
| + | .8:065a E8 INX | ||
| + | .8:065b 88 DEY // do that 0x55 times (0x55 * 3 = 0xFF) == 0x2FF | ||
| + | .8:065c D0 DF BNE $063D | ||
| + | .8:065e A5 A4 LDA $A4 // lowest two bit from start $A4 to 0x2FF | ||
| + | .8:0660 4A LSR A | ||
| + | .8:0661 2E FF 02 ROL $02FF | ||
| + | .8:0664 4A LSR A | ||
| + | .8:0665 2E FF 02 ROL $02FF | ||
| + | .8:0668 18 CLC // If checksum at $063a was correct then X is here 3 * 0x55 = 0xFF | ||
| + | .8:0669 60 RTS | ||
| + | .8:066a 55 00 EOR $00,X | ||
| + | |||
| + | The code at $0300 gets loaded from custom sector id 6 and loads remaining code for the 'second stage loader'. | ||
| + | At the start the next sector to load is id 8. | ||
| + | .8:0300 A2 FF LDX #$FF // reset stack | ||
| + | .8:0302 9A TXS | ||
| + | .8:0303 20 00 06 JSR $0600 // load sector id 8 and move to $0400 | ||
| + | .8:0306 A2 00 LDX #$00 | ||
| + | .8:0308 BD 00 02 LDA $0200,X | ||
| + | .8:030b 9D 00 04 STA $0400,X | ||
| + | .8:030e E8 INX | ||
| + | .8:030f D0 F7 BNE $0308 | ||
| + | .8:0311 20 00 06 JSR $0600 // load sector id a and move to $0500 | ||
| + | .8:0314 A2 00 LDX #$00 | ||
| + | .8:0316 BD 00 02 LDA $0200,X | ||
| + | .8:0319 9D 00 05 STA $0500,X | ||
| + | .8:031c E8 INX | ||
| + | .8:031d D0 F7 BNE $0316 | ||
| + | .8:031f E6 31 INC $31 | ||
| + | .8:0321 E6 31 INC $31 | ||
| + | .8:0323 20 00 06 JSR $0600 // load sector id e and move to $0700 | ||
| + | .8:0326 A2 00 LDX #$00 | ||
| + | .8:0328 BD 00 02 LDA $0200,X | ||
| + | .8:032b 9D 00 07 STA $0700,X | ||
| + | .8:032e E8 INX | ||
| + | .8:032f E0 98 CPX #$98 | ||
| + | .8:0331 90 F5 BCC $0328 | ||
| + | .8:0333 20 00 06 JSR $0600 // load sector id 0 to $0200 | ||
| + | .8:0336 A2 00 LDX #$00 | ||
| + | .8:0338 4C 00 07 JMP $0700 // start second stage | ||
| + | |||
| + | ===== Example Sectors ===== | ||
| + | |||
| + | Sectors decoded with g64conv mode 5. | ||
| + | |||
| + | ===== Sector with Translation Table ===== | ||
| + | |||
| + | Sector loaded for the forward translation table. Routine at $0700: | ||
| + | |||
| + | Header sector 5 (not evaluated): | ||
| + | sync 64 | ||
| + | ; Following raw bytes: 73 4d 53 56 55 55 55 55 55 55 55 55 52 bf | ||
| + | ; Following raw bits: 111 | ||
| + | |||
| + | Data: | ||
| + | sync 23 | ||
| + | ; Following raw bytes: 6a 4a 4b 4d 4e 52 53 55 56 57 59 5a 5b 5d 5e 65 66 67 69 6a 6b 6d 6e 72 73 75 76 77 79 7a 7b 7d 7e 95 96 97 9a 9b 9d 9e a5 a6 a7 a9 aa ab ad ae b2 b3 b5 b6 b7 b9 ba bb bd be ca cb cd ce d2 d3 d5 57 4a 4a 4a 76 4b 52 4b 7b 76 4d 56 7a 4a 4a 4a 4a 75 4e 56 4d 4d 55 4e 4e 56 7b 4d 77 77 4d 4a 4a 53 55 53 4e 53 75 55 7d 7b 56 76 4e 7b 52 4d 56 53 4d 4a 4e 4e 4a 75 4e 76 77 75 75 4e 77 4a 55 52 4d 4b 4d 53 7a 7a 4b 4b 7e 4e 52 4b 55 4e 52 4d 4d 4a 52 79 4a 4d 4a 4d 4e 53 4b 53 76 4a 7b 52 53 4b 7b 76 4a 53 56 4e 4e 53 7d 4a 4a 4a 57 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 6e 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 53 69 ad b2 b7 b3 ce 77 72 b7 9a b2 66 75 76 4e 95 a6 7d 7b 5e d2 57 6e 6e 95 b5 be 79 4b 6d 5a 6a d3 59 67 59 95 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4d 55 55 55 55 55 55 55 55 55 55 55 55 5f | ||
| + | ; Following raw bits: 1111 | ||
| + | |||
| + | ===== Sector with Code ===== | ||
| + | |||
| + | Sector 6, contains the routine loaded to $0300 in the floppy. Routine is called at $07a7 | ||
| + | sync 64 | ||
| + | ; Following raw bytes: 73 4d 55 57 55 55 55 55 55 55 55 56 52 bf | ||
| + | ; Following raw bits: 111 | ||
| + | |||
| + | sync 23 | ||
| + | ; Following raw bytes: 6a a6 73 76 ae 57 4b a7 a6 b2 b2 4a a5 a5 4b cd 65 59 ba 57 4b a7 a6 b2 b2 4a a5 a5 4b cd 65 59 52 ba ba ba 52 57 4b a7 a6 b2 b2 4a a5 a5 4b cd 4d 7d 4d 76 ba 57 4b a7 a6 6b 6b 4b 4b 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 7b 7a 6b 6d 55 5d 59 5e 67 be 72 ae 7b 5e 67 a6 b6 4e 69 7a 5d 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4b 4a aa aa aa aa aa aa aa aa aa aa aa bf | ||
| + | ; Following raw bits: 111 | ||
Latest revision as of 05:26, 29 August 2026
Contents
- 1 Pages that refer to this protection
- 2 Langenscheidt Protection
- 3 Archived first manual analysis
- 3.1 Initial code loaded to C64
- 3.2 Load Translation Table
- 3.3 Check Track 3 Header / Load Translation Table / Create Sparse Table
- 3.4 Table 1 (Translation Table)
- 3.5 Resulting "Tempelmann" GCR
- 3.6 Table 2 (Sparse Table)
- 3.7 Load Custom Sector
- 3.8 Translate 6-bit to 8-bit
- 3.9 Example Sectors
- 3.10 Sector with Translation Table
- 3.11 Sector with Code
Pages that refer to this protection
- Descriptions of old disk copy protections (← links)
- Wörter Rennen mit System (← links)
- Jagd auf Vokabeln (← links)
Langenscheidt Protection
The Langenscheidt Protection is a Commodore 64 disk scheme found on Langenscheidt educational software (confirmed on Wörter Rennen mit System and Jagd auf Vokabeln). It is the disk's own native storage format: a fully custom raw-GCR encoding used for essentially the whole disk, with a small hand-built filesystem (name catalog, page-ownership table) replacing CBM DOS's own BAM/directory conventions entirely. Reading such a disk with standard tools decodes almost nothing.
The findings below extend the analysis with a full sector decode algorithm (validated byte-for-byte against a known worked example), a commented disassembly of every key routine, and a complete description of the on-disk filesystem, including the exact mechanism that resolves a catalog entry to precise byte boundaries on disk.
Mechanism
- When the user issues a LOAD"*",8,1 the drive loads the BAM track 18 sector 0 to a buffer at $0700. The sector data of track 18 sector 0 contains already drive code.
- The disk boots via a short C64-side autostart stub that immediately hands off to the drive with a single, standard KERNAL
M-E $07B0command (Memory-Execute) — confirmed to be the only KERNAL/IEC command used in an entire session, by a full-session trace that loggedCIOUTcalls from boot through menu interaction and found exactly five, spellingM-E+$07B0and nothing else, ever again. - From that point on, the drive-resident program (itself streamed down from disk in 256-byte pages immediately after the handoff) drives everything — including standard 1541 DOS ROM job-dispatch calls it invokes internally — over a private, raw bit-banged protocol on CIA2 Port A (
$DD00), completely bypassing the KERNAL serial routines for the rest of the session. - Individual disk sectors use a custom 6-bit-to-8-bit run-length-limited GCR alphabet (64 valid byte values, no more than 2 consecutive zero bits), not the standard C64 5-bit GCR code. Every data sector carries a whole-payload running-XOR checksum spanning all 343 raw bytes.
- Files are identified by name, through a small hand-built catalog filesystem (fixed 16-byte name records, linearly scanned) — not by a fixed per-menu-option track number. Content offsets are resolved via a second on-disk table (the page-ownership table) that gives each file's exact length in 256-byte pages, letting every file be extracted byte-perfect. See "File system structure" below.
Basic sector structure
Sync-length pattern and marker bytes
Every custom sector — header or data — starts the same way: a standard
sync run, followed immediately by a single marker byte identifying the
block type, decoded straight from a g64conv <image> <out> 5
raw-byte dump with no GCR interpretation needed (the marker and payload
bytes are literal members of the custom 64-value alphabet, not standard
5-bit GCR nibbles):
sync NN
; Following raw bytes: 73 <skip> <id> 55 55 55 55 55 55 55 55 55 55 ff <- HEADER
^^ $73 = header marker ^^ padding/gap bytes to next sync
sync NN
; Following raw bytes: 6a <343 payload bytes...> <- DATA
^^ $6A = data marker
Header block (14 raw bytes total, only the first 3 meaningful):
byte 0 is always $73; byte 1 is an unchecked "skip" byte
(typically $4D in practice, but never validated by the
drive code); byte 2 is the sector-ID byte, translated through the
same 64-entry table used for data (see below) to give a plain 0-15
sector number.
Data block (344 raw bytes total): byte 0 is always $6A;
the following 343 bytes are the sector's translated, checksummed payload
(see "Sector decode algorithm" below) — 256 main bytes, 86 tail bytes,
and 1 running-checksum byte, laid out so that decoding and checksumming
happen in a single pass.
Table 1 — the 64-value GCR alphabet
64 native on-disk bytes (raw floppy alphabet), each a run-length-limited
value (no more than 2 consecutive zero bits) standing in for a 6-bit
value 0-63. Sector-header IDs 0-15 reuse the same table directly —
that is why $4A through $66 below double as
both "6-bit value 0-15" and "header sector ID 0-15":
idx 0 1 2 3 4 5 6 7 idx 8 9 10 11 12 13 14 15
4A 4B 4D 4E 52 53 55 56 57 59 5A 5B 5D 5E 65 66
idx 16 17 18 19 20 21 22 23 idx 24 25 26 27 28 29 30 31
67 69 6A 6B 6D 6E 72 73 75 76 77 79 7A 7B 7D 7E
idx 32 33 34 35 36 37 38 39 idx 40 41 42 43 44 45 46 47
95 96 97 9A 9B 9D 9E A5 A6 A7 A9 AA AB AD AE B2
idx 48 49 50 51 52 53 54 55 idx 56 57 58 59 60 61 62 63
B3 B5 B6 B7 B9 BA BB BD BE CA CB CD CE D2 D3 D5
Resulting GCR table
|
|
|
|
|
|
|
|
The drive-resident code loads this table once at boot and builds a
sparse reverse table at $0100-$01FF
($0100 + native_byte -> 6-bit value), used for every
subsequent decode:
.8:079a A2 00 LDX #$00 .8:079c B4 64 LDY $64,X ; Y = native byte from Table 1 at offset X .8:079e 8A TXA .8:079f 99 00 01 STA $0100,Y ; reverse_table[native_byte] = X (the 6-bit index) .8:07a2 E8 INX .8:07a3 E0 40 CPX #$40 ; 64 entries .8:07a5 90 F5 BCC $079C
The directory track is genuinely mixed-format, not entirely custom
Track 18 is a real exception: it carries a small number of ordinary standard-CBM-format sectors (4 confirmed: sectors 0, 1, 8, 11 — plus one further standard-format header observed with no matching data block behind it) alongside custom sectors. Every other track has zero standard-format sectors and a full complement of custom ones — confirmed directly and used as the detection signal for "which tracks are part of the custom content addressing scheme" (see "File system structure" below): a track with zero custom sync hits is excluded from the sequential numbering, while every genuine content track has exactly 16.
Two-stage bootstrap: commented disassembly
Stage 0 — Drive loads BAM to buffer at $0700 in drive RAM
Stage 1 — C64 autostart hands off to the drive
The very first thing the C64-side autostart code does is send a literal
M-E $07B0 command over the standard KERNAL
CIOUT — the only time the whole session ever uses it:
.C:0334 A9 4D LDA #$4D ; 'M' .C:0336 20 A8 FF JSR $FFA8 ; CIOUT .C:0339 A9 2D LDA #$2D ; '-' .C:033b 20 A8 FF JSR $FFA8 .C:033e A9 45 LDA #$45 ; 'E' .C:0340 20 A8 FF JSR $FFA8 .C:0343 A9 B0 LDA #$B0 ; target address low byte .C:0345 20 A8 FF JSR $FFA8 .C:0348 A9 07 LDA #$07 ; target address high byte -> $07B0 .C:034a 20 A8 FF JSR $FFA8 .C:034d 20 FE ED JSR $EDFE ; finish the IEC command (KERNAL UNTALK)
Stage 2 — receiving the rest of the C64-side program
Immediately after the handoff, the C64 enters a tight receive loop that pulls the entire remainder of the resident program down from the drive as a sequence of 256-byte pages, each preceded by its own 2-byte destination address — this is how the ~7 KB of C64-side code seen disassembled throughout this article actually arrives in RAM:
.C:0350 A2 FF LDX #$FF .C:0352 AD 00 DD LDA $DD00 .C:0355 A0 10 LDY #$10 .C:0357 CA DEX .C:0358 D0 FD BNE $0357 .C:035a 88 DEY .C:035b D0 FA BNE $0357 ; short settle delay .C:035d A9 03 LDA #$03 .C:035f 8D 00 DD STA $DD00 .C:0362 AD 00 DD LDA $DD00 .C:0365 CD 00 DD CMP $DD00 .C:0368 F0 FB BEQ $0365 ; wait for a line transition (drive ready) .C:036a 20 80 03 JSR $0380 ; receive byte -> destination address low .C:036d 85 D1 STA $D1 .C:036f 20 80 03 JSR $0380 ; receive byte -> destination address high .C:0372 85 D2 STA $D2 .C:0374 A0 00 LDY #$00 .C:0376 20 80 03 JSR $0380 ; receive byte -> data .C:0379 91 D1 STA ($D1),Y .C:037b 88 DEY .C:037c D0 F8 BNE $0376 ; 256 bytes per page .C:037e F0 EA BEQ $036A ; next (address, page) pair
$0380 is the core receive-one-byte primitive: a
hand-timed, 2-bits-per-CIA-read bit-banged protocol over
$DD00, entirely independent of the KERNAL:
.C:0380 78 SEI .C:0381 A9 27 LDA #$27 .C:0383 8D 00 DD STA $DD00 .C:0386 2C 00 DD BIT $DD00 .C:0389 50 FB BVC $0386 ; wait for the drive's handshake (CLK IN, bit6) .C:038b A9 03 LDA #$03 .C:038d 8D 00 DD STA $DD00 .C:0390 A2 08 LDX #$08 .C:0392 CA DEX .C:0393 D0 FD BNE $0392 ; short delay .C:0395 A2 04 LDX #$04 .C:0397 AD 00 DD LDA $DD00 ; sample 2 bits per iteration .C:039a 0A ASL A .C:039b 08 PHP .C:039c 0A ASL A .C:039d 26 2D ROL $2D .C:039f 28 PLP .C:03a0 26 2D ROL $2D .C:03a2 CA DEX .C:03a3 D0 F2 BNE $0397 ; 4 iterations x 2 bits = 8 bits total .C:03a5 A9 17 LDA #$17 .C:03a7 8D 00 DD STA $DD00 ; release/ack .C:03aa EA NOP .C:03ab EA NOP .C:03ac EA NOP .C:03ad A5 2D LDA $2D ; assembled byte .C:03af 60 RTS
A live RAM snapshot taken at menu-idle confirms the resulting program
occupies only $0200-$1BFF; everything from
$2000 upward is still the untouched C64 power-on RAM test
pattern, confirming the whole resident program is under 7 KB.
The unified transfer protocol
$1600-$16CB duplicates the exact primitive
seen in $0380, split into four small reusable entry points
sharing an identical handshake shape:
| Address | Role |
|---|---|
$1600 |
send one byte, source = ($2E),Y (memory to drive)
|
$1638 |
receive one byte, destination = ($2E),Y (drive to memory)
|
$166A |
send one byte, source = accumulator |
$169C |
receive one byte, return in accumulator |
A live trace confirms this is genuinely the only transfer layer
used for the rest of the session: even a live-observed menu selection
that triggered a fresh drive-side track seek produced zero further
CIOUT calls — the C64-side command that told the drive
which content to fetch travelled entirely over this custom protocol,
indistinguishable byte-for-byte from ordinary data transfer without
tracing the protocol's own framing (see "The page-ownership table"
below for how that framing was actually decoded).
Sector decode algorithm
$0603-$0669 is the single routine responsible
for turning 343 raw on-disk bytes (already past the $6A
marker) into 256 real content bytes, in one pass that
simultaneously computes and verifies the sector's checksum. This is the
routine that gives every custom sector its error-detection property, and
its exact behavior was validated byte-for-byte against a known worked
example (see the note at the end of this section) before being trusted
for the filesystem/extraction work below.
; ---- read 256 main bytes, translate, and running-XOR checksum in place ---- .8:0603 20 56 F5 JSR $F556 ; wait for sync .8:0606 50 FE BVC $0606 .8:0608 AD 01 1C LDA $1C01 ; read raw byte .8:060b B8 CLV .8:060c C9 6A CMP #$6A ; must be the data marker .8:060e D0 F3 BNE $0603 .8:0610 98 TYA ; Y returned 0 by the sync wait -> A=0 .8:0611 AA TAX ; X=0 too: running checksum accumulator starts at 0 .8:0612 50 FE BVC $0612 ; <-- per-byte loop entry .8:0614 B8 CLV .8:0615 AC 01 1C LDY $1C01 ; read next raw byte .8:0618 59 00 01 EOR $0100,Y ; A ^= reverse_table[raw_byte] (running XOR!) .8:061b 9D 00 02 STA $0200,X ; store the RUNNING accumulator, not the raw lookup .8:061e E8 INX .8:061f D0 F1 BNE $0612 ; 256 iterations ; ---- read 86 more "tail" bytes, same running checksum, stored $A4-$F9 ---- .8:0621 A2 55 LDX #$55 ; X = 85 .8:0623 50 FE BVC $0623 .8:0625 B8 CLV .8:0626 AC 01 1C LDY $1C01 .8:0629 59 00 01 EOR $0100,Y .8:062c 95 A4 STA $A4,X ; X counts 85 downto 0 -> 86 bytes, $A4-$F9 .8:062e CA DEX .8:062f 10 F2 BPL $0623 ; ---- read the final check byte; A must be 0 if every prior byte was correct ---- .8:0631 50 FE BVC $0631 .8:0633 B8 CLV .8:0634 AC 01 1C LDY $1C01 ; read byte 343 (the check byte) .8:0637 59 00 01 EOR $0100,Y .8:063a AA TAX ; checksum-good <=> A==0 <=> X==0 here
The stored "main" bytes are the running XOR prefix of each raw
byte's table-translated value, not the plain per-byte lookup — this is
deliberate differential encoding: if the disk's mastering process stored
delta[i] = plaintext[i] XOR plaintext[i-1] (with an implicit
plaintext[-1] = 0) as the 6-bit value that got GCR-encoded,
then this running-XOR-accumulate loop exactly recovers the
original plaintext byte-for-byte, and the final check byte's own delta
is simply chosen so the whole 343-byte chain's cumulative XOR lands on
zero — a genuine whole-payload checksum, not just a per-header check.
Immediately following, with no RTS in between (this
is a straight fall-through, always executed, not a separate optional
step), the tail's 86 stored 6-bit values get expanded into the upper 2
bits of the 256 main bytes, via a 6-and-2 bit-packing scheme — each
6-bit tail value contributes exactly 2 bits to each of 3 consecutive
main bytes:
.8:063b A0 55 LDY #$55 ; Y = 85 downto 1 (85 iterations) .8:063d B9 A4 00 LDA $00A4,Y ; a 6-bit tail value, bits 5..0 .8:0640 4A LSR A ; bit0 -> carry .8:0641 3E 02 02 ROL $0202,X ; main[X+2] = (main[X+2]<<1)|bit0 .8:0644 4A LSR A ; bit1 -> carry .8:0645 3E 02 02 ROL $0202,X ; main[X+2] = (main[X+2]<<1)|bit1 (now 8 bits) .8:0648 4A LSR A ; bit2 .8:0649 3E 01 02 ROL $0201,X ; main[X+1], same 2-ROL treatment .8:064c 4A LSR A ; bit3 .8:064d 3E 01 02 ROL $0201,X .8:0650 4A LSR A ; bit4 .8:0651 3E 00 02 ROL $0200,X ; main[X+0] .8:0654 4A LSR A ; bit5 .8:0655 3E 00 02 ROL $0200,X .8:0658 E8 INX ; step 3 bytes per tail value .8:0659 E8 INX .8:065a E8 INX .8:065b 88 DEY .8:065c D0 DF BNE $063D ; special-case the 86th (last) tail value into the very last main byte, 2 bits only .8:065e A5 A4 LDA $A4 .8:0660 4A LSR A .8:0661 2E FF 02 ROL $02FF .8:0664 4A LSR A .8:0665 2E FF 02 ROL $02FF .8:0668 18 CLC .8:0669 60 RTS
Each of the 256 $0200-$02FF bytes ends up with
its original 6-bit running-checksum value shifted into bits 2-7, and 2
more bits from a shared tail value filled in at bits 0-1 — recovering
full 8-bit entropy per byte from a 64-value (6-bit) on-disk alphabet.
Validation: this exact algorithm, transliterated instruction-for-
instruction into Python, was run against the raw bytes of the
preliminary analysis's own "Sector with Code" example (a capture of
sector 6, known independently to become the second-stage loader code at
$0300) and produced an exact byte-for-byte match to
the already-known plaintext (A2 FF 9A 20 00 06 A2 00 BD 00 02 9D
00 04 ..., i.e. LDX #$FF; TXS; JSR $0600; LDX #$00; LDA
$0200,X; STA $0400,X; ...) — confirming both the running-XOR
checksum interpretation and the 6-and-2 bit-expansion scheme are
correct, not just plausible.
Custom keyboard handling
The menu is read by a complete, hand-written matrix scanner at
$0CE0-$0DF4 — confirmed by the total absence
of any JSR $FFE4 (KERNAL GETIN) anywhere in
the resident program:
.C:0d03 8D 00 DC STA $DC00 ; select all columns .C:0d0a AE 01 DC LDX $DC01 ; any key at all pressed? .C:0d0d E0 FF CPX #$FF .C:0d0f D0 03 BNE $0D14 .C:0d11 4C B8 0D JMP $0DB8 ; no key -> exit .C:0d15 A9 FE LDA #$FE ; walking-zero column select .C:0d17 8D 00 DC STA $DC00 .C:0d1c 48 PHA .C:0d1d AD 01 DC LDA $DC01 .C:0d20 CD 01 DC CMP $DC01 .C:0d23 D0 F8 BNE $0D1D ; debounce: re-read until stable .C:0d25 4A LSR A ; bit0 -> carry: 0 = key pressed (active low) .C:0d26 B0 19 BCS $0D41 ; not pressed -> skip .C:0d28 48 PHA .C:0d29 B9 4A 1A LDA $1A4A,Y ; decode table: matrix position -> raw code
Decoded scan positions are translated to real ASCII via a table at
$1A4A, including proper A-Z case handling (a self-contained
equivalent of what the KERNAL's own decode table does), then pushed into
a custom circular FIFO at $0C8B (count tracked at
$0C8A) — a hand-built keyboard buffer, independent of the
KERNAL's own.
File system structure
The catalog: a genuine named-file directory
A fixed catalog table holds up to 20 slots of 16-byte records — name, NUL-padded — linearly scanned by name. Confirmed live on both known titles at the identical location, track 2 — the catalog is content-identifiable (a run of 16-byte records starting with an uppercase ASCII letter, zero-padded) rather than needing a fixed track number hardcoded per title:
[track 2, one catalog sector] 57445341 00000000 00000000 00000000 "WDSA............" 57445346 00000000 00000000 00000000 "WDSF............" 57445347 00000000 00000000 00000000 "WDSG............" ...
On Wörter Rennen mit System this resolves to real vocabulary-list
filenames (WDSA, WDSF-WDSW —
letters chosen per topic, not strictly alphabetic; plus
FELD.P64, TITLE.P64,
SYSC64.STARTUP, and other system/resource files). On
Jagd auf Vokabeln the same mechanism resolves to a completely
different file set (SYSC64.GAME, MENU,
SHAPES.SPR, BILD.P64, short 1-2 letter
vocabulary-chapter codes, etc.) — confirming the catalog format itself
is generic across titles, only its contents differ.
Lookup ($1776, called from $17A8 for
insert-or-find, and directly for plain lookup) linear-scans the 20
catalog slots, comparing each 16-byte record against a "wanted name"
buffer at $0C60; on a match, it converts the matching
slot's address back into a small record index via an
LSR/ROR chain (dividing the byte offset by
16). $17A8 additionally scans for a free slot (first
byte $00) to register a new entry, confirmed by two
embedded error strings sitting in the code stream a few bytes after its
call site:
>C:1817 20 b1 0e 0d 0d 43 41 54 41 4c 4f 47 20 46 55 4c ....CATALOG FUL >C:1827 4c 07 00 20 46 0f 4c 03 20 85 41 20 cb 11 a5 40 L.. F.L. .A ...@ >C:1837 20 d8 17 90 16 20 b1 0e 0d 0d 44 49 53 4b 20 46 .... ....DISK F >C:1847 55 4c 4c 07 00 20 46 0f 4c 03 ULL.. F.L.
"CATALOG FULL" (all 20 slots occupied) and
"DISK FULL" — genuine runtime error messages from a save
path, confirming this is a full read/write catalog filesystem, not a
fixed, pre-baked table.
The page-ownership table: resolving a catalog entry to exact file boundaries
A catalog match alone only gives a name, not a length or location.
The mechanism that resolves a matched entry to precise byte boundaries
lives in the same routine's continuation, $171B-
$1732:
.C:171b C8 INY .C:171c B1 07 LDA ($07),Y ; next byte of the ownership table .C:171e 29 1F AND #$1F ; mask to 5 bits .C:1720 C5 3F CMP $3F ; $3F = (matched catalog index) + 1 .C:1722 F0 F7 BEQ $171B ; still the same owner -> keep counting .C:1724 8C 7D 0C STY $0C7D ; Y = run length so far -> becomes the page count! .C:1727 20 58 17 JSR $1758 .C:172a A5 09 LDA $09 .C:172c A6 0A LDX $0A .C:172e 85 2E STA $2E .C:1730 86 2F STX $2F .C:1732 4C DD 10 JMP $10DD ; hand off to the shared page-transfer loop
The table pointed to by $07/$08 is a
page-ownership run-length map: one byte per 256-byte content
page, whose low 5 bits give (owning catalog index) + 1. The
code counts how many consecutive table entries share the wanted
catalog index's key — that count directly is the file's page
count, and the table's own cumulative offset up to that run is the
file's starting page. Located on disk purely by content (a run of
monotonically non-decreasing small values, distinguishable at a glance
from both the name records and real code/data) — on both known titles,
this table lives on the same track as the catalog itself.
Combined with an auto-detected content-start track (the first track past the catalog/loader tracks with a full complement of custom sectors) and the reserved-track exclusion described in "Basic sector structure" above, this gives an exact, generic formula with no per-title constants:
global_page = the ownership table's cumulative run-length position for this file
content_tracks = [ t | t is a used, in-sequence physical track, reserved
tracks like the directory track excluded ]
physical_track = content_tracks[global_page // 16]
physical_sector = global_page % 16
Verified byte-exact on Wörter Rennen mit System: the first catalog
entry (WDSA, 12 pages) decodes to a clean run of
body-parts vocabulary starting exactly with arm Arm back
Rücken...; the very next entry (WDSF, starting
immediately after) decodes to a fresh, unrelated topic starting exactly
with mind Verstand... — no split words, no overlap, at the
computed boundary.
File loading is not literally "1 file = 1 track"
A live trace of a real menu selection (Vokabeltraining -> a submenu
choice) showed the resulting drive-side track seek land in completely
stock, unmodified 1541 ROM code ($F31B: STA $22, the
standard KERNAL DOS job-track store) — meaning the drive-resident
program issues perfectly ordinary job-dispatch calls internally
once it has resolved which track a requested page actually lives on; no
custom drive-side seek routine exists. Individual named files can be
(and, on both known titles, routinely are) shorter than one full track
and packed several to a track — e.g. on Wörter Rennen mit System, 16
WDS* vocabulary entries occupy only 9 physical tracks total
between them, not 16.
Known titles
| Title | Confirmed | Notes |
|---|---|---|
| Wörter Rennen mit System | yes, fully traced | vocabulary trainer; catalog + page-ownership table both located and validated; 29/29 catalog files extract cleanly |
| Jagd auf Vokabeln | yes, catalog + table confirmed | same mechanism, different file set |
Tooling
File:Langenscheidt extract.zip
langenscheidt_extract.py implements the sector decode, catalog scan, and
page-ownership resolution above directly, and provides three functions
confirmed working on both known titles straight from their native
.g64/.p64 images with no per-title hardcoding:
- identify — every custom sector actually carrying non-empty file data, by track/sector.
- verify — checksum-validates every custom sector and reports bad ones, each annotated with which catalog file it belongs to (or
unused). - extract — writes the on-disk catalog listing (names, exact page counts, starting pages) and one exactly byte-bounded file per catalog entry.
Known limitations
The tool operates on g64conv's decoded text dump (a single fixed GCR interpretation per track), not raw flux.
See also
Archived first manual analysis
As found on Wörter Rennen mit System
There are 4 sectors on track 18 in regular format on the disk:
Sector 0, 1, 8 and 11.
The rest of the tracks were written in speed 0. Each of these tracks contain a single additional regular sector header that was written with speed 3.
Initial code loaded to C64
The first file is linked to t18 s8 that loads auto start code to the C64 from $02d1 to $03cb. Execution entry point is at $0334.
.C:0334 A9 0B LDA #$0B .C:0336 20 B0 03 JSR $03B0 .C:0339 A9 2D LDA #$2D // "-" -> Send Memory Execute at $07b0 to floppy .C:033b 20 A8 FF JSR $FFA8 // Output byte to serial .C:033e A9 45 LDA #$45 // "E" .C:0340 20 A8 FF JSR $FFA8 // Output byte to serial .C:0343 A9 B0 LDA #$B0 // Address $07b0 .C:0345 20 A8 FF JSR $FFA8 // Output byte to serial .C:0348 A9 07 LDA #$07 // .C:034a 20 A8 FF JSR $FFA8 // Output byte to serial .C:034d 20 FE ED JSR $EDFE // Send UNLISTEN .C:0350 A2 FF LDX #$FF .C:0352 AD 00 DD LDA $DD00 .C:0355 A0 10 LDY #$10 .C:0357 CA DEX .C:0358 D0 FD BNE $0357 .C:035a 88 DEY .C:035b D0 FA BNE $0357 .C:035d A9 03 LDA #$03 .C:035f 8D 00 DD STA $DD00 .C:0362 AD 00 DD LDA $DD00 .C:0365 CD 00 DD CMP $DD00 .C:0368 F0 FB BEQ $0365 .C:036a 20 80 03 JSR $0380 .C:036d 85 D1 STA $D1 .C:036f 20 80 03 JSR $0380 .C:0372 85 D2 STA $D2 .C:0374 A0 00 LDY #$00 .C:0376 20 80 03 JSR $0380 .C:0379 91 D1 STA ($D1),Y .C:037b 88 DEY .C:037c D0 F8 BNE $0376 .C:037e F0 EA BEQ $036A .C:0380 78 SEI .C:0381 A9 27 LDA #$27 .C:0383 8D 00 DD STA $DD00 .C:0386 2C 00 DD BIT $DD00 .C:0389 50 FB BVC $0386 .C:038b A9 03 LDA #$03 .C:038d 8D 00 DD STA $DD00 .C:0390 A2 08 LDX #$08 .C:0392 CA DEX .C:0393 D0 FD BNE $0392 .C:0395 A2 04 LDX #$04 .C:0397 AD 00 DD LDA $DD00 .C:039a 0A ASL A .C:039b 08 PHP .C:039c 0A ASL A .C:039d 26 2D ROL $2D .C:039f 28 PLP .C:03a0 26 2D ROL $2D .C:03a2 CA DEX .C:03a3 D0 F2 BNE $0397 .C:03a5 A9 17 LDA #$17 .C:03a7 8D 00 DD STA $DD00 .C:03aa EA NOP .C:03ab EA NOP .C:03ac EA NOP .C:03ad A5 2D LDA $2D .C:03af 60 RTS .C:03b0 8D 11 D0 STA $D011 .C:03b3 A9 08 LDA #$08 .C:03b5 20 0C ED JSR $ED0C .C:03b8 A9 6F LDA #$6F .C:03ba 20 B9 ED JSR $EDB9 .C:03bd A9 4D LDA #$4D .C:03bf 4C A8 FF JMP $FFA8 .C:03c2 4C FE ED JMP $EDFE
Load Translation Table
The floppy has loaded the previous t18 s0 to the buffer at $0700. Aside from the disk title the sector also contains code at $0700 and $07b0.
.8:0700 A5 04 LDA $04 // $04 == #$E0 .8:0702 C9 E2 CMP #$E2 // is it #$E2? .8:0704 F0 27 BEQ $072D // send command find sector .8:0706 20 56 F5 JSR $F556 // wait for sync .8:0709 50 FE BVC $0709 .8:070b B8 CLV .8:070c AD 01 1C LDA $1C01 // check if first byte is #$6A .8:070f C9 6A CMP #$6A .8:0711 D0 F3 BNE $0706 // No? try next Sync .8:0713 A2 64 LDX #$64 // We are at the first data (6A) sector we found after changing to track 2 .8:0715 50 FE BVC $0715 // wait for byte from disk .8:0717 B8 CLV .8:0718 AD 01 1C LDA $1C01 // read byte from disk .8:071b 95 00 STA $00,X // read 156 bytes and put them to $64 to $ff .8:071d E8 INX .8:071e D0 F5 BNE $0715 .8:0720 55 64 EOR $64,X // run EOR checksum over all 156 bytes .8:0722 E8 INX .8:0723 E0 9C CPX #$9C // 156 .8:0725 D0 F9 BNE $0720 .8:0727 EA NOP .8:0728 29 7F AND #$7F // use lower 7 bits of checksum for error code, should be #$39 .8:072a 4C 69 F9 JMP $F969 // error entry disk controller -> user code execution continues in $078d .8:072d 4C CA F6 JMP $F6CA // send command find sector
.. disk title data
.8:07b0 A9 02 LDA #$02 <- The C64 does start execution in the floppy here .8:07b2 8D 78 02 STA $0278 // init some values for user file execute - number of file names .8:07b5 A9 2A LDA #$2A .8:07b7 8D 00 02 STA $0200 .8:07ba A9 01 LDA #$01 .8:07bc 8D 74 02 STA $0274 .8:07bf A9 00 LDA #$00 .8:07c1 85 7E STA $7E // last handled program .8:07c3 4C AB E7 JMP $E7AB // Execute USR file, loads t18 s11 into floppy at $0730 and executes it.
The upper routine loads t18 s11 to the floppy ram starting at $0730 to $07b5. Previously there was unused code space due to disk title space in t18 s0. Execution continues at $0730.
Check Track 3 Header / Load Translation Table / Create Sparse Table
From t18 s11
.8:0730 A2 03 LDX #$03 // Seek to track 3 .8:0732 86 0E STX $0E .8:0734 A9 00 LDA #$00 // Set sector 0 to look for .8:0736 85 0F STA $0F .8:0738 A9 B0 LDA #$B0 // Set new job: find sector header .8:073a 85 04 STA $04 .8:073c A5 04 LDA $04 .8:073e 30 FC BMI $073C // wait for result code .8:0740 C9 01 CMP #$01 // 1? .8:0742 F0 3A BEQ $077E // OK, go on, otherwise lockup loop .8:0744 EE FE 02 INC $02FE // 0 -> 1 .8:0747 AD FE 02 LDA $02FE // Lockup .8:074a D0 FB BNE $0747 // Loop .8:074c A9 B0 LDA #$B0 // not used .8:074e 85 04 STA $04 // not used .8:0750 A5 04 LDA $04 // not used .8:0752 30 FC BMI $0750 // not used .8:0754 C9 01 CMP #$01 // not used .8:0756 F0 26 BEQ $077E // not used .8:0758 CE FE 02 DEC $02FE // not used .8:075b AD FE 02 LDA $02FE // not used .8:075e D0 FB BNE $075B // not used .8:0760 CE FE 02 DEC $02FE // not used .8:0763 AD FE 02 LDA $02FE // not used .8:0766 D0 FB BNE $0763 // not used .8:0768 A9 B0 LDA #$B0 // not used .8:076a 85 04 STA $04 // not used .8:076c A5 04 LDA $04 // not used .8:076e 30 FC BMI $076C // not used .8:0770 C9 01 CMP #$01 // not used .8:0772 F0 0A BEQ $077E // not used .8:0774 EE FE 02 INC $02FE // not used .8:0777 AD FE 02 LDA $02FE // not used .8:077a D0 FB BNE $0777 // not used .8:077c F0 BA BEQ $0738 // not used .8:077e 20 F5 07 JSR $07F5 // check if we are on track 3 .8:0781 A9 22 LDA #$22 // set buffer 4, set track 2 .8:0783 85 0E STA $0E .8:0785 A9 E0 LDA #$E0 // Job: motor on, seek track and execute code at $0700 .8:0787 85 04 STA $04 // The code at $0700 loads the forward translation table to $A4 .8:0789 A5 04 LDA $04 .8:078b 30 FC BMI $0789 // wait for result .8:078d C9 39 CMP #$39 // was checksum result #$39 is good .8:078f D0 9F BNE $0730 // if not, go to track 3 sector header find again .8:0791 C6 31 DEC $31 // $31: #$07 -> #$06 this is our internal sector id for the next sector .8:0793 78 SEI .8:0794 A9 EE LDA #$EE // What does this do? Init PCR? .8:0796 8D 0C 1C STA $1C0C .8:0799 EA NOP .8:079a A2 00 LDX #$00 .8:079c B4 64 LDY $64,X .8:079e 8A TXA .8:079f 99 00 01 STA $0100,Y // [$100 + [$64 + X]] = X .8:07a2 E8 INX // this means, the loaded data at $64 are used as indices to a new sparse table at $0100. .8:07a3 E0 40 CPX #$40 // bytes at these indexed positions are filled counting upwards. See table 1 and table 2. .8:07a5 90 F5 BCC $079C // The counter here that is also written to table 2 is the actual 6 bit byte value .8:07a7 20 00 06 JSR $0600 // We have now our translation table, load first sector (id 6) to $0200 .8:07aa B9 00 02 LDA $0200,Y // copy to $0300 and execute .8:07ad 99 00 03 STA $0300,Y .8:07b0 C8 INY .8:07b1 D0 F7 BNE $07AA .8:07b3 4C 00 03 JMP $0300
.. remaining bytes .. From t18 s0, loads a custom sector header
.8:07c6 20 56 F5 JSR $F556 // wait for sync .8:07c9 50 FE BVC $07C9 // wait for data byte .8:07cb B8 CLV .8:07cc AD 01 1C LDA $1C01 // read data byte .8:07cf C9 73 CMP #$73 // #$73 is the code for a sector header .8:07d1 D0 F3 BNE $07C6 // no? then try again .8:07d3 50 FE BVC $07D3 // wait for data byte .8:07d5 B8 CLV .8:07d6 AD 01 1C LDA $1C01 // read a skip byte .8:07d9 50 FE BVC $07D9 // wait for data byte .8:07db B8 CLV .8:07dc AC 01 1C LDY $1C01 // read data byte .8:07df B9 00 01 LDA $0100,Y // translate to 6 bit byte through lookup table .8:07e2 C5 31 CMP $31 // $31 contains the wanted sector number (first one read is 6) .8:07e4 D0 E0 BNE $07C6 // no? then next sector header .8:07e6 20 03 06 JSR $0603 // Read sector data .8:07e9 E8 INX // with the correct checksum X is #$FF when $0603 returns .8:07ea D0 DA BNE $07C6 // wrong checksum? try read again .8:07ec A5 31 LDA $31 // set next sector id +2 (e.g. 8) .8:07ee 69 02 ADC #$02 .8:07f0 29 0F AND #$0F .8:07f2 85 31 STA $31 .8:07f4 60 RTS .8:07f5 A5 22 LDA $22 // current track .8:07f7 C9 03 CMP #$03 // are we on 3? .8:07f9 D0 94 BNE $078F // repeat, set track 3, find header .8:07fb 09 20 ORA #$20 // .8:07fd 85 22 STA $22 // #$23 -> $22 .8:07ff 60 RTS
Sector id translation: Mark for header is after snyc #$73, then a skip byte follow, typically #$4d, then the header byte follows:
#$4A -> $31 = 0x00 #$4B -> $31 = 0x01 #$4D -> $31 = 0x02 #$4E -> $31 = 0x03 #$52 -> $31 = 0x04 #$53 -> $31 = 0x05 #$55 -> $31 = 0x06 #$56 -> $31 = 0x07 #$57 -> $31 = 0x08 #$59 -> $31 = 0x09 #$5A -> $31 = 0x0A #$5B -> $31 = 0x0B #$5D -> $31 = 0x0C #$5E -> $31 = 0x0D #$65 -> $31 = 0x0E #$66 -> $31 = 0x0F
As one sees the same lookup is used as installed through table 1 in table 2.
Table 1 (Translation Table)
With the routine at $079a this table is translated to $0100 such that #$00 -> $014a, #$01 -> $014b, #$02 -> 014d a.s.o. The background is: The floppy can not store native bytes with more than two zeros in a row. The following table translates 6 bit wide numbers from #$00 to #$3f to native bytes in the floppy sector ($64 to $a3).
With the transfer to $0100 the new table at $0100 can be used as lookup to translate the native floppy bytes to 6 bit wide bytes. Such that [$0100 + native_floppy_byte] == translated_byte, where translated_byte is a 6 bit wide value from $00 to $3f.
>8:0064 4a 4b 4d 4e 52 53 55 56 JKMNRSUV
>8:006c 57 59 5a 5b 5d 5e 65 66 WYZ[]^ef
>8:0074 67 69 6a 6b 6d 6e 72 73 gijkmnrs
>8:007c 75 76 77 79 7a 7b 7d 7e uvwyz{}~
>8:0084 95 96 97 9a 9b 9d 9e a5 ........
>8:008c a6 a7 a9 aa ab ad ae b2 ........
>8:0094 b3 b5 b6 b7 b9 ba bb bd ........
>8:009c be ca cb cd ce d2 d3 d5 ........
>8:00a4 57 4a 4a 4a 76 4b 52 4b WJJJvKRK
>8:00ac 7b 76 4d 56 7a 4a 4a 4a {vMVzJJJ
>8:00b4 4a 75 4e 56 4d 4d 55 4e JuNVMMUN
>8:00bc 4e 56 7b 4d 77 77 4d 4a NV{MwwMJ
>8:00c4 4a 53 55 53 4e 53 75 55 JSUSNSuU
>8:00cc 7d 7b 56 76 4e 7b 52 4d }{VvN{RM
>8:00d4 56 53 4d 4a 4e 4e 4a 75 VSMJNNJu
>8:00dc 4e 76 77 75 75 4e 77 4a NvwuuNwJ
>8:00e4 55 52 4d 4b 4d 53 7a 7a URMKMSzz
>8:00ec 4b 4b 7e 4e 52 4b 55 4e KK~NRKUN
>8:00f4 52 4d 4d 4a 52 79 4a 4d RMMJRyJM
>8:00fc 4a 4d 4e 53 00 c9 00 00 JMNS....
Resulting "Tempelmann" GCR
|
|
|
|
|
|
|
|
Table 2 (Sparse Table)
sparse table for translating native_floppy_bytes to translated bytes, [$0100 + native_floppy_byte] == translated_byte
>8:0100 00 c9 00 00 00 00 00 00 ........ >8:0108 00 00 00 00 00 00 00 00 ........ >8:0110 00 00 00 00 00 00 00 00 ........ >8:0118 00 00 00 00 00 00 00 00 ........ >8:0120 00 00 00 00 63 f9 3a f5 ....c.:. >8:0128 10 63 7e fe 00 00 02 20 .c~.... >8:0130 7e fe 31 d1 31 d1 ba f4 ~.1.1... >8:0138 07 e0 77 f9 7e fe 00 03 ..w.~... >8:0140 e0 a1 89 07 fe eb 00 00 ........ >8:0148 00 00 00 01 00 02 03 00 ........ >8:0150 00 00 04 05 00 06 07 08 ........ >8:0158 00 09 0a 0b 00 0c 0d 00 ........ >8:0160 00 00 00 00 00 0e 0f 10 ........ >8:0168 00 11 12 13 00 14 15 00 ........ >8:0170 00 00 16 17 00 18 19 1a ........ >8:0178 00 1b 1c 1d 00 1e 1f 00 ........ >8:0180 00 00 00 00 00 00 00 00 ........ >8:0188 00 00 00 00 00 00 00 00 ........ >8:0190 00 00 00 00 00 20 21 22 ..... !" >8:0198 00 00 23 24 00 25 26 00 ..#$.%&. >8:01a0 00 00 00 00 00 27 28 29 .....'() >8:01a8 00 2a 2b 2c 00 2d 2e 00 .*+,.-.. >8:01b0 00 00 2f 30 00 31 32 33 ../0.123 >8:01b8 00 34 35 36 2f 37 38 9e .456/78. >8:01c0 fd e7 4e 52 9d a9 f9 52 ..NR...R >8:01c8 54 9d 39 3a 52 3b 3c a9 T.9:R;<. >8:01d0 f9 4b 3d 3e a9 3f 4b 54 .K=>.?KT >8:01d8 9d a9 f9 4a 54 9d a9 f9 ...JT... >8:01e0 4a 54 bc 9f 27 c9 4a 7a JT..'.Jz >8:01e8 ae d7 4f d3 9d a9 7a b5 ..O...z. >8:01f0 54 9d a9 7a b5 54 96 9b T..z.T.. >8:01f8 29 ef 5b 6d 95 29 5a d2 ).[m.)Z.
Load Custom Sector
Code at $0603 loads a custom format sector to $0200
.8:0600 4C C6 07 JMP $07C6 // this part loads a data sector .8:0603 20 56 F5 JSR $F556 //wait for sync // - load data from disk and decode .8:0606 50 FE BVC $0606 .8:0608 AD 01 1C LDA $1C01 // read data byte .8:060b B8 CLV .8:060c C9 6A CMP #$6A // #$6A is the code for a data sector .8:060e D0 F3 BNE $0603 .8:0610 98 TYA // Y is returned 0 from wait for sync .8:0611 AA TAX // now all registers are zeroed .8:0612 50 FE BVC $0612 .8:0614 B8 CLV .8:0615 AC 01 1C LDY $1C01 // Read 256 bytes to $0200 .8:0618 59 00 01 EOR $0100,Y // translate from table, data sector gets EORed .8:061b 9D 00 02 STA $0200,X // .8:061e E8 INX .8:061f D0 F1 BNE $0612 // loop .8:0621 A2 55 LDX #$55 // Read 85 bytes more and put from $F9 to $A4 .8:0623 50 FE BVC $0623 .8:0625 B8 CLV .8:0626 AC 01 1C LDY $1C01 .8:0629 59 00 01 EOR $0100,Y // translate from table .8:062c 95 A4 STA $A4,X .8:062e CA DEX .8:062f 10 F2 BPL $0623 .8:0631 50 FE BVC $0631 .8:0633 B8 CLV .8:0634 AC 01 1C LDY $1C01 // Read byte 86. .8:0637 59 00 01 EOR $0100,Y // translate from table .8:063a AA TAX // if checksum is correct, then A should be 0 here, A -> X
Translate 6-bit to 8-bit
The following loop takes #$55 bytes with 6 bit values from $A4 - $FF and add two bits each to bytes in groups of 3 at $0200. Y counts 1 each from the part at $A4 (downwards)
X counts 3 each for the part at $0200 (upwards)
.8:063b A0 55 LDY #$55 // 85 .8:063d B9 A4 00 LDA $00A4,Y // get lower bits from $F9 to $A4, e.g. pattern --543210 .8:0640 4A LSR A // pattern 0 -> Carry .8:0641 3E 02 02 ROL $0202,X // [$0200 + 3 * X + 2] -bbbbbb0 .8:0644 4A LSR A // next bit to carry .8:0645 3E 02 02 ROL $0202,X // [$0200 + 3 * X + 2] bbbbbb01 .8:0648 4A LSR A // .8:0649 3E 01 02 ROL $0201,X // .8:064c 4A LSR A // .8:064d 3E 01 02 ROL $0201,X // [$0200 + 3 * X + 1] bbbbbb23 .8:0650 4A LSR A // .8:0651 3E 00 02 ROL $0200,X .8:0654 4A LSR A .8:0655 3E 00 02 ROL $0200,X [$0200 + 3 * X + 0] bbbbbb45 .8:0658 E8 INX // step 3 bytes .8:0659 E8 INX .8:065a E8 INX .8:065b 88 DEY // do that 0x55 times (0x55 * 3 = 0xFF) == 0x2FF .8:065c D0 DF BNE $063D .8:065e A5 A4 LDA $A4 // lowest two bit from start $A4 to 0x2FF .8:0660 4A LSR A .8:0661 2E FF 02 ROL $02FF .8:0664 4A LSR A .8:0665 2E FF 02 ROL $02FF .8:0668 18 CLC // If checksum at $063a was correct then X is here 3 * 0x55 = 0xFF .8:0669 60 RTS .8:066a 55 00 EOR $00,X
The code at $0300 gets loaded from custom sector id 6 and loads remaining code for the 'second stage loader'. At the start the next sector to load is id 8.
.8:0300 A2 FF LDX #$FF // reset stack .8:0302 9A TXS .8:0303 20 00 06 JSR $0600 // load sector id 8 and move to $0400 .8:0306 A2 00 LDX #$00 .8:0308 BD 00 02 LDA $0200,X .8:030b 9D 00 04 STA $0400,X .8:030e E8 INX .8:030f D0 F7 BNE $0308 .8:0311 20 00 06 JSR $0600 // load sector id a and move to $0500 .8:0314 A2 00 LDX #$00 .8:0316 BD 00 02 LDA $0200,X .8:0319 9D 00 05 STA $0500,X .8:031c E8 INX .8:031d D0 F7 BNE $0316 .8:031f E6 31 INC $31 .8:0321 E6 31 INC $31 .8:0323 20 00 06 JSR $0600 // load sector id e and move to $0700 .8:0326 A2 00 LDX #$00 .8:0328 BD 00 02 LDA $0200,X .8:032b 9D 00 07 STA $0700,X .8:032e E8 INX .8:032f E0 98 CPX #$98 .8:0331 90 F5 BCC $0328 .8:0333 20 00 06 JSR $0600 // load sector id 0 to $0200 .8:0336 A2 00 LDX #$00 .8:0338 4C 00 07 JMP $0700 // start second stage
Example Sectors
Sectors decoded with g64conv mode 5.
Sector with Translation Table
Sector loaded for the forward translation table. Routine at $0700:
Header sector 5 (not evaluated):
sync 64 ; Following raw bytes: 73 4d 53 56 55 55 55 55 55 55 55 55 52 bf ; Following raw bits: 111
Data:
sync 23 ; Following raw bytes: 6a 4a 4b 4d 4e 52 53 55 56 57 59 5a 5b 5d 5e 65 66 67 69 6a 6b 6d 6e 72 73 75 76 77 79 7a 7b 7d 7e 95 96 97 9a 9b 9d 9e a5 a6 a7 a9 aa ab ad ae b2 b3 b5 b6 b7 b9 ba bb bd be ca cb cd ce d2 d3 d5 57 4a 4a 4a 76 4b 52 4b 7b 76 4d 56 7a 4a 4a 4a 4a 75 4e 56 4d 4d 55 4e 4e 56 7b 4d 77 77 4d 4a 4a 53 55 53 4e 53 75 55 7d 7b 56 76 4e 7b 52 4d 56 53 4d 4a 4e 4e 4a 75 4e 76 77 75 75 4e 77 4a 55 52 4d 4b 4d 53 7a 7a 4b 4b 7e 4e 52 4b 55 4e 52 4d 4d 4a 52 79 4a 4d 4a 4d 4e 53 4b 53 76 4a 7b 52 53 4b 7b 76 4a 53 56 4e 4e 53 7d 4a 4a 4a 57 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 6e 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 53 69 ad b2 b7 b3 ce 77 72 b7 9a b2 66 75 76 4e 95 a6 7d 7b 5e d2 57 6e 6e 95 b5 be 79 4b 6d 5a 6a d3 59 67 59 95 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4d 55 55 55 55 55 55 55 55 55 55 55 55 5f ; Following raw bits: 1111
Sector with Code
Sector 6, contains the routine loaded to $0300 in the floppy. Routine is called at $07a7
sync 64 ; Following raw bytes: 73 4d 55 57 55 55 55 55 55 55 55 56 52 bf ; Following raw bits: 111
sync 23 ; Following raw bytes: 6a a6 73 76 ae 57 4b a7 a6 b2 b2 4a a5 a5 4b cd 65 59 ba 57 4b a7 a6 b2 b2 4a a5 a5 4b cd 65 59 52 ba ba ba 52 57 4b a7 a6 b2 b2 4a a5 a5 4b cd 4d 7d 4d 76 ba 57 4b a7 a6 6b 6b 4b 4b 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 7b 7a 6b 6d 55 5d 59 5e 67 be 72 ae 7b 5e 67 a6 b6 4e 69 7a 5d 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4a 4b 4a aa aa aa aa aa aa aa aa aa aa aa bf ; Following raw bits: 111