Difference between revisions of "Langenscheidt Protection"

From Software Archive
Jump to navigation Jump to search
 
(3 intermediate revisions 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 82: Line 82:
 
     B3  B5  B6  B7  B9  BA  BB  BD        BE  CA  CB  CD  CE  D2  D3  D5
 
     B3  B5  B6  B7  B9  BA  BB  BD        BE  CA  CB  CD  CE  D2  D3  D5
 
</pre>
 
</pre>
 +
 +
===== Resulting GCR table =====
 +
{|
 +
|
 +
{| class="wikitable"
 +
|-class="hintergrundfarbe6"
 +
! Data !! GCR
 +
|-
 +
| 000000 || 01001010
 +
|-
 +
| 000001 || 01001011
 +
|-
 +
| 000010 || 01001101
 +
|-
 +
| 000011 || 01001110
 +
|-
 +
| 000100 || 01010010
 +
|-
 +
| 000101 || 01010011
 +
|-
 +
| 000110 || 01010101
 +
|-
 +
| 000111 || 01010110
 +
|}
 +
|
 +
{| class="wikitable"
 +
|-class="hintergrundfarbe6"
 +
! Data !! GCR
 +
|-
 +
| 001000 || 01010111
 +
|-
 +
| 001001 || 01011001
 +
|-
 +
| 001010 || 01011010
 +
|-
 +
| 001011 || 01011011
 +
|-
 +
| 001100 || 01011101
 +
|-
 +
| 001101 || 01011110
 +
|-
 +
| 001110 || 01100101
 +
|-
 +
| 001111 || 01100110
 +
|}
 +
|
 +
{| class="wikitable"
 +
|-class="hintergrundfarbe6"
 +
! Data !! GCR
 +
|-
 +
| 010000 || 01100111
 +
|-
 +
| 010001 || 01101001
 +
|-
 +
| 010010 || 01101010
 +
|-
 +
| 010011 || 01101011
 +
|-
 +
| 010100 || 01101101
 +
|-
 +
| 010101 || 01101110
 +
|-
 +
| 010110 || 01110010
 +
|-
 +
| 010111 || 01110011
 +
|}
 +
|
 +
{| class="wikitable"
 +
|-class="hintergrundfarbe6"
 +
! Data !! GCR
 +
|-
 +
| 011000 || 01110101
 +
|-
 +
| 011001 || 01110110
 +
|-
 +
| 011010 || 01110111
 +
|-
 +
| 011011 || 01111001
 +
|-
 +
| 011100 || 01111010
 +
|-
 +
| 011101 || 01111011
 +
|-
 +
| 011110 || 01111101
 +
|-
 +
| 011111 || 01111110
 +
|}
 +
|
 +
{| class="wikitable"
 +
|-class="hintergrundfarbe6"
 +
! Data !! GCR
 +
|-
 +
| 100000 || 10010101
 +
|-
 +
| 100001 || 10010110
 +
|-
 +
| 100010 || 10010111
 +
|-
 +
| 100011 || 10011010
 +
|-
 +
| 100100 || 10011011
 +
|-
 +
| 100101 || 10011101
 +
|-
 +
| 100110 || 10011110
 +
|-
 +
| 100111 || 10100101
 +
|}
 +
|
 +
{| class="wikitable"
 +
|-class="hintergrundfarbe6"
 +
! Data !! GCR
 +
|-
 +
| 101000 || 10100110
 +
|-
 +
| 101001 || 10100111
 +
|-
 +
| 101010 || 10101001
 +
|-
 +
| 101011 || 10101010
 +
|-
 +
| 101100 || 10101011
 +
|-
 +
| 101101 || 10101101
 +
|-
 +
| 101110 || 10101110
 +
|-
 +
| 101111 || 10110010
 +
|}
 +
|
 +
{| class="wikitable"
 +
|-class="hintergrundfarbe6"
 +
! Data !! GCR
 +
|-
 +
| 110000 || 10110011
 +
|-
 +
| 110001 || 10110101
 +
|-
 +
| 110010 || 10110110
 +
|-
 +
| 110011 || 10110111
 +
|-
 +
| 110100 || 10111001
 +
|-
 +
| 110101 || 10111010
 +
|-
 +
| 110110 || 10111011
 +
|-
 +
| 110111 || 10111101
 +
|}
 +
|
 +
{| class="wikitable"
 +
|-class="hintergrundfarbe6"
 +
! Data !! GCR
 +
|-
 +
| 111000 || 10111110
 +
|-
 +
| 111001 || 11001010
 +
|-
 +
| 111010 || 11001011
 +
|-
 +
| 111011 || 11001101
 +
|-
 +
| 111100 || 11001110
 +
|-
 +
| 111101 || 11010010
 +
|-
 +
| 111110 || 11010011
 +
|-
 +
| 111111 || 11010101
 +
|}
 +
|}
  
 
The drive-resident code loads this table once at boot and builds a
 
The drive-resident code loads this table once at boot and builds a
Line 111: 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 526: 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

Pages that refer to this protection

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

  1. 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.
  2. The disk boots via a short C64-side autostart stub that immediately hands off to the drive with a single, standard KERNAL M-E $07B0 command (Memory-Execute) — confirmed to be the only KERNAL/IEC command used in an entire session, by a full-session trace that logged CIOUT calls from boot through menu interaction and found exactly five, spelling M-E + $07B0 and nothing else, ever again.
  3. 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.
  4. 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.
  5. 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
Data GCR
000000 01001010
000001 01001011
000010 01001101
000011 01001110
000100 01010010
000101 01010011
000110 01010101
000111 01010110
Data GCR
001000 01010111
001001 01011001
001010 01011010
001011 01011011
001100 01011101
001101 01011110
001110 01100101
001111 01100110
Data GCR
010000 01100111
010001 01101001
010010 01101010
010011 01101011
010100 01101101
010101 01101110
010110 01110010
010111 01110011
Data GCR
011000 01110101
011001 01110110
011010 01110111
011011 01111001
011100 01111010
011101 01111011
011110 01111101
011111 01111110
Data GCR
100000 10010101
100001 10010110
100010 10010111
100011 10011010
100100 10011011
100101 10011101
100110 10011110
100111 10100101
Data GCR
101000 10100110
101001 10100111
101010 10101001
101011 10101010
101100 10101011
101101 10101101
101110 10101110
101111 10110010
Data GCR
110000 10110011
110001 10110101
110010 10110110
110011 10110111
110100 10111001
110101 10111010
110110 10111011
110111 10111101
Data GCR
111000 10111110
111001 11001010
111010 11001011
111011 11001101
111100 11001110
111101 11010010
111110 11010011
111111 11010101

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:

  1. identify — every custom sector actually carrying non-empty file data, by track/sector.
  2. verify — checksum-validates every custom sector and reports bad ones, each annotated with which catalog file it belongs to (or unused).
  3. 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
Daten Code
000000 01001010
000001 01001011
000010 01001101
000011 01001110
000100 01010010
000101 01010011
000110 01010101
000111 01010110
Daten Code
001000 01010111
001001 01011001
001010 01011010
001011 01011011
001100 01011101
001101 01011110
001110 01100101
001111 01100110
Daten Code
010000 01100111
010001 01101001
010010 01101010
010011 01101011
010100 01101101
010101 01101110
010110 01110010
010111 01110011
Daten Code
011000 01110101
011001 01110110
011010 01110111
011011 01111001
011100 01111010
011101 01111011
011110 01111101
011111 01111110
Daten Code
100000 10010101
100001 10010110
100010 10010111
100011 10011010
100100 10011011
100101 10011101
100110 10011110
100111 10100101
Daten Code
101000 10100110
101001 10100111
101010 10101001
101011 10101010
101100 10101011
101101 10101101
101110 10101110
101111 10110010
Daten Code
110000 10110011
110001 10110101
110010 10110110
110011 10110111
110100 10111001
110101 10111010
110110 10111011
110111 10111101
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