Home > CBM > Disk > Fast Load > Microprose 1
6 Subpages: Access, Epyx, GEOS, Microprose 2, Project Firestart, Sega

  Microprose Type 1 Copy-protection & Fast-loader 

This fast-loader was used at least in Gunship and Pirates! For the longest time, I believed this was an exclusive to Microprose Software. However I later saw it used in some Accolade titles. I'm thinking this might be the scheme known as RapidLoc (or Rapid-Lock), but I'm not sure.

Interestingly, almost every file on disk points to the same 'corrupt' file chain! You see, the first sector of these files has a next-sector pointer which is invalid (for example track 37 sector 21). That first sector is the start of an 'auto-run' file (discussed below). The actual start of the file is hidden (and scrambled) in the directory entry. That is, this fast-loader scheme uses a custom directory structure so is not suitable for a general-purpose fast-loader (without some modification). Let's take a look at a hex-dump of such a directory (from Card Sharks):

>8:0600  00 ff 82 12  12 43 4f 50   .....COP
>8:0608  59 52 49 47  48 54 20 31   YRIGHT 1
>8:0610  39 38 37 a0  a0 00 00 00   987.....
>8:0618  b2 0d 00 88  04 00 21 00   ......!.

The 00 ff is not part of the directory entry (ignore it). Next is an 82 which indicates a properly-closed PRG file. Next is the starting track and sector of the file 12 12 = track 18 sector 18. The first sector of the file is located on the directory track, which is unusual since files normally start anywhere except the directory track! It also should be the last sector (18) on the directory track (but custom formats can reorder sectors). All 'fast-load' files in this scheme 'start' at track 18 sector 18. One nice feature is this can be loaded quickly since the drive-head doesn't need to move to another track.

Next is the filename 50 59 52 49 47 48 54 20 31 39 38 37 A0 A0. Following that is 3 zero bytes (reserved for REL files). Following that should be 6 zero bytes (normally unused, except for save-with-replace), but here we see mysterious B2 0D 00 88 04 00. This indirectly indicates where the real file begins (we'll come back to this). The entry ends with bytes 21 00 which indicate a file size of 33 blocks.

  What's an "auto-run" file? 

Normally a program is loaded in BASIC using LOAD"*",8 and after loading is complete READY will appear and the user normally enters the comman RUN. That process loads the file data at the "start of BASIC" (which $801 on the C64). However, by using LOAD"*",8,1 the file will be loaded to the address specified at the beginning of the file. This is normally used to load non-BASIC data (such as graphic images or machine-language code). Normally, LOAD"*",8,1 doesn't do anything special, but if the "right" data gets loaded into the "right" memory used by the KERNAL or CPU, we can trick the operating system into running code without user interaction -- much like a computer virus. In summary, for this kind of file the user only need type LOAD"*",8,1 and the computer will automatically RUN the code even if it's not BASIC code.

This fast-loader scheme loads at $2ED; $2ED~2FF is normally unused by C64 so no problem. But then (starting at $300) comes the BASIC/KERNAL vectors table. This scheme over-writes most of those vectors with "junk" (except IRQ and NMI vectors are valid). Importantly, the KERNAL Stop routine vector is over-written with address $2ED (perhaps not surprising). At this point of LOAD only part of the sector has been transferred to the host computer (presumably a C64). Let's see what this code does:

;note .Y = 0
.C:02ed   84 08      STY $08     ;decrypt low base
.C:02ef   8C 15 D0   STY $D015   ;turn off sprites
.C:02f2   A0 F2      LDY #$F2    ;index 242 (counter -14)
.C:02f4   C6 AF      DEC $AF     ; 3 -> 2
.C:02f6   A5 AF      LDA $AF
.C:02f8   85 C4      STA $C4     ;2 (pointer1 high)
.C:02fa   85 09      STA $09     ;pointer 2 high
.C:02fc   45 AE      EOR $AE
.C:02fe   85 C3      STA $C3     ;$28 (pointer1 low)
.C:0300   49 9E      EOR #$9E
.C:0302   85 AE      STA $AE     ;$B6 (pointer2 low)
;decrypt loop
.C:0304   B1 C3      LDA ($C3),Y ;read ($228+F2+ = $31A+)
.C:0306   51 08      EOR ($08),Y ;decrypt
.C:0308   91 C3      STA ($C3),Y ;save
.C:030a   C8         INY         ;index next and count down
.C:030b   D0 F7      BNE $0304   ;not all 14 bytes, loop

So basically it disables sprites (which can interfere with timing) and decrypts part of the data that has already been loaded ($31A~327). This is known as code obfusication.

The code continues by loading Y with # byte pairs to resume loading from the first sector. The data being loaded consists of two streams! The second stream has been encrypted so we must decrypt it too. Also the data is being loaded 'backwards', that is top-down into memory. If this isn't obfusication, I don't know what is!
.C:030d   A0 59      LDY #$59    ;90 bytes to load per stream; $b4 (180) bytes total
;loader loop 1
.C:030f   88         DEY         ;count down, are we finished?
.C:0310   30 A4      BMI $02B6   ;yes, exit
.C:0312   10 06      BPL $031A   ;not yet
~
.C:031a   20 A5 FF   JSR $FFA5   ;KERNAL Acceptor: read byte from disk
.C:031d   91 AE      STA ($AE),Y ;save stream 1 ($2B6~30e)
.C:031f   20 A5 FF   JSR $FFA5   ;read another byte from disk
.C:0322   51 AE      EOR ($AE),Y ;decrypt
.C:0324   91 08      STA ($08),Y ;save stream 2 ($200~258)
.C:0326   50 E7      BVC $030F   ;always loop

After the two code streams have been loaded, execution jumps to $2B6. Note 241 (of 254) bytes have been read from the first sector -- and that's all we'll get from the "loader stub". Anyway, let's see what's at $2B6:

.C:02b6   A0 00      LDY #$00    ;start of strings
.C:02b8   A5 BA      LDA $BA     ;disk unit (normally 8)
.C:02ba   20 B1 FF   JSR $FFB1   ;KERNAL LISTEN
.C:02bd   A9 FF      LDA #$FF    ;secondary address 15 (command channel)
.C:02bf   20 93 FF   JSR $FF93   ;KERNAL SECOND
;sender loop
.C:02c2   B9 EF 02   LDA $02EF,Y ;read data string
.C:02c5   20 A8 FF   JSR $FFA8   ;KERNAL serial out
.C:02c8   C8         INY         ;index next character in string
.C:02c9   C0 1B      CPY #$1B    ;end 1st string?
.C:02cb   F0 04      BEQ $02D1   ;yes
.C:02cd   C0 20      CPY #$20    ;end 2nd string?
.C:02cf   90 F1      BCC $02C2   ;no, sender loop
;end of string
.C:02d1   20 AE FF   JSR $FFAE   ;KERNAL UNLISTEN (device will process command)
.C:02d4   C0 20      CPY #$20    ;end 2nd string?
.C:02d6   90 E0      BCC $02B8   ;no, sender loop
~
>C:02ef  4d 2d 57 b5  00 15 58 49   M-W...XI
>C:02f7  36 85 06 b1  05 59 17 04   6....Y..
>C:02ff  91 05 88 10  f6 6c 05 00   .....l..
>C:0307  9b 36 ca 4d  2d 45 b0 07   .6.M-E..

So the code sends two instructions to the disk command channel. The first writes a code snippet to drive zero page RAM (more on this later). The second instruction tells the drive to execute code stored in RAM address $7b0. But what's in that RAM? The C1541 and C1571 store BAM (side 1) in page 7 (memory $700~7FF). The C1541/71 always load the BAM whenever a disk swap is detected (or the first disk access after power-up). This information comes from track 18 sector 0 (the so-called disk header). On a single-sided disk, the bytes $7b0~7ff are normally unused (except GEOS). So the disk drive has secretly already loaded code into its memory before the user LOADs his/her first program! Let's take a look at this drive code:

.8:07b0   78         SEI         ;disable interrupts
.8:07b1   AD 90 02   LDA $0290   ;directory sector
.8:07b4   A2 05      LDX #$05    ;index controller sector#
.8:07b6   D5 08      CMP $08,X   ;X is correct buffer?
.8:07b8   F0 04      BEQ $07BE   ;yes
.8:07ba   CA         DEX         ;no, next sector#
.8:07bb   CA         DEX
.8:07bc   10 F8      BPL $07B6   ;'always' -- what if buffer 0 = $300 ???

.8:07be   8A         TXA         ;buffer# *2
.8:07bf   69 99      ADC #$99    ;calc ZP address ($9B, $9D or $9F)
.8:07c1   8D CF 07   STA $07CF   ;set ZP address (self-modifying code)

What the code does is find which buffer ($400, $500, or $600) holds the directory listing. Inerestingly (bug?) it doesn't check buffer $300. Once it's found, it sets a ZeroPage (ZP) pointer into its own code. Next we copy five bytes from the directory entry into ZP location $77~7B. These are the last five mystery bytes from the directory entry we saw above at the beginning of this article. (As far as I can tell, the first three mystery bytes are junk = red herring.):


.8:07c4   A2 04      LDX #$04    ;#bytes-1 to copy (i.e. 5 bytes)
.8:07c6   A0 1A      LDY #$1A    ;index directory entry
.8:07c8   8C 07 1C   STY $1C07   ;timer high-byte (why?)
.8:07cb   8C 00 18   STY $1800   ;pull CLK and DATA serial-bus lines low
;save loop
.8:07ce   B1 9F      LDA ($9F),Y ;read mystery directory byte
.8:07d0   95 77      STA $77,X   ;save
.8:07d2   88         DEY
.8:07d3   CA         DEX
.8:07d4   10 F8      BPL $07CE   ;loop until done

With the mystery directory bytes stashed away in zero page, the code continues by setting up the track/sector for all five buffers! That's 1.25K of code if your counting:

.8:07d6   A5 22      LDA $22     ;current track (18)
.8:07d8   A2 08      LDX #$08    ;last buffer's sector index
.8:07da   A0 03      LDY #$03    ;starting sector
;setup loop
.8:07dc   94 07      STY $07,X   ;set sector# for buffer
.8:07de   95 06      STA $06,X   ;set track# (18) for buffer
.8:07e0   C8         INY         ;sector interleave of 3
.8:07e1   C8         INY
.8:07e2   C8         INY
.8:07e3   CA         DEX         ;prior buffer (track)
.8:07e4   CA         DEX         ;prior buffer (sector)
.8:07e5   10 F5      BPL $07DC   ;not all buffers, setup loop

If you find that code confusing, maybe this will help:

  • Buffer $300 → $06,07 = track 18, sector 15
  • Buffer $400 → $08,09 = track 18, sector 12
  • Buffer $500 → $0a,0b = track 18, sector 9
  • Buffer $600 → $0c,0d = track 18, sector 6
  • Buffer $700 → $0e,0f = track 18, sector 3

Now that the track and sector has been set for the five buffers, it's time to tell the disk-drive controller to read those sectors into its buffers! Well, all buffers except the $700 buffer (where our code is executing):


.8:07e7   A2 03      LDX #$03    ;4 buffers to read
;buffer loop (retry error)
.8:07e9   A0 80      LDY #$80    ;controller code for 'Read Sector'
.8:07eb   94 00      STY $00,X   ;set command for controller
.8:07ed   58         CLI         ;enable interrupts (allow controller to do its job)
;wait loop
.8:07ee   B5 00      LDA $00,X   ;check controller status
.8:07f0   30 FC      BMI $07EE   ;still running, wait loop
.8:07f2   4A         LSR A       ;test result
.8:07f3   D0 F4      BNE $07E9   ;retry if error
.8:07f5   A5 3A      LDA $3A     ;this doesn't belong in the loop! (.A = $35)
.8:07f7   CA         DEX         ;next buffer control
.8:07f8   10 EF      BPL $07E9   ;not all buffers, loop
;
.8:07fa   78         SEI         ;disable interrupts
.8:07fb   84 04      STY $04     ;set 'Read Sector' for buffer $700
.8:07fd   4C 06 02   JMP $0206   ;continue C64 pre-load

If you've been counting, the disk drive has now loaded 6 sectors (not counting directory nor BAM), and we haven't even moved the disk-head to the starting sector! In other words, this 'fast-loader' has a lot of overhead for each file! There are better fast-loaders, but also there are much worse ones! So I consider this scheme mediocre.

Remember the code snippet the C64 wrote to drive-RAM zero page? Well a copy of that code is in the drive's "parser buffer" at $200. More precisely at $206~217. In other words the "Memory-Write" to zero-page was another form of code obfusication. We'll actually execute code at $206 (and not at $00b5). Anyway, let's look at the drive code at $206:

.8:0206   58         CLI         ;enable interrupts; the controller can now load buffer $700
.8:0207   49 36      EOR #$36    ; make .A = $03
.8:0209   85 06      STA $06     ;pointer high-byte (i.e., buffer $300)
;loop (note we begin with .Y = $80)
.8:020b   B1 05      LDA ($05),Y ;read buffer $380-
.8:020d   59 17 04   EOR $0417,Y ;decrypt
.8:0210   91 05      STA ($05),Y ;fix code at $380-
.8:0212   88         DEY         ;previous byte
.8:0213   10 F6      BPL $020B   ;not all, loop
.8:0215   6C 05 00   JMP ($0005) ;jump to $300

So what it does is allow buffer $700 to load (the CLI instruction), and it also decrypts the code at $300~380. Then it jumps to the code at $300:

.8:0300   A2 80      LDX #$80    ;controller code to 'Read Sector'
.8:0302   D0 02      BNE $0306   ;always
;error loop
.8:0304   86 04      STX $04     ;set command for controller
;wait loop
.8:0306   58         CLI         ;allow controller to run (superfluous)
.8:0307   A5 04      LDA $04     ;get status of buffer $700
.8:0309   30 FB      BMI $0306   ;still running, wait loop
.8:030b   4A         LSR A       ;test result
.8:030c   D0 F6      BNE $0304   ;loop if error

That $300 code begins by simply waiting for buffer $700 to be loaded without error. The next part of the code erases the "$206 code" from both the parser buffer ($200 region) and from zero page. It must be a big a secret if they're going through the trouble to erase it twice! Anyway the code isn't complex:

.8:030e   78         SEI
.8:030f   85 24      STA $24     ;zero (why?)
.8:0311   A2 11      LDX #$11    ;17+1 bytes to erase
;erase loop
.8:0313   95 B5      STA $B5,X   ;scrub zero page
.8:0315   9D 08 02   STA $0208,X ;scrub parser buffer (trivia: code bytes $206 and $207 remain!)
.8:0318   CA         DEX         ;prior byte
.8:0319   10 F8      BPL $0313   ;loop until done

Next, the first two of the five mystery bytes in zero page are decrypted and stored into $d6 and $d7:

.8:031b   A2 01      LDX #$01    ;2 bytes to decrypt
.8:031d   A5 C7      LDA $C7     ; $9b
.8:031f   4A         LSR A       ; $4d
.8:0320   85 0B      STA $0B     ;decryption count for $0420
.8:0322   AC 94 02   LDY $0294   ;directory index ($02, $22, $42, $62, $82, $a2, $c2 or $e2)
;decrypt mystery loop
.8:0325   B5 77      LDA $77,X   ;get encrypted data
.8:0327   59 17 07   EOR $0717,Y ;decrypt
.8:032a   95 D6      STA $D6,X   ;set for loader
.8:032c   88         DEY         ;next decrypt index
.8:032d   CA         DEX         ;count down
.8:032e   10 F5      BPL $0325   ;not both track and sector, loop

In case it isn't obvious, the decryption key varies based on which directory entry is accessed. For example, if the directory entry is the first in a sector, the decrypt index would be $02. If the file is the second entry in a sector, the decrypt index would be $22. Let's not worry about that! The import thing is the decrypted data was stored into $d6 and $d7. Anyway, the high-bit of the last decrypted byte is used to toggle bit 4 of byte $3B5 (why?):

.8:0330   29 80      AND #$80    ;isolate bit 7
.8:0332   4A         LSR A       ;shift to bit 6
.8:0333   4A         LSR A       ;shift to bit 5
.8:0334   4A         LSR A       ;shift to bit 4
.8:0335   4D B5 03   EOR $03B5   ;toggle bit
.8:0338   8D B5 03   STA $03B5   ;update code

Do you remember how the drive code at $300 to $380 was decrypted? Well what about the rest of the drive code; is it good to run? No! Most of it is also encrypted, and with a much stronger '32-bit encryption' scheme. The following code decrypts $39c to $7ff:


.8:033b   A2 03      LDX #$03    ; 4 buffers (part $300, all $400, $500 and $600)
.8:033d   86 31      STX $31     ; transfer poiner (used later)
.8:033f   E8         INX         ; make that 5 buffers ($700 too)
.8:0340   A0 9C      LDY #$9C    ; index for buffer $300 (don't over-write ourself!)
;decrypt loop
.8:0342   B1 05      LDA ($05),Y ; read encrypted data
.8:0344   45 C7      EOR $C7     ; decrypt
.8:0346   91 05      STA ($05),Y ; save fixed code 
.8:0348   66 24      ROR $24     ; get a bit
.8:034a   26 C9      ROL $C9     ;32-bit shift left
.8:034c   26 C8      ROL $C8
.8:034e   26 C7      ROL $C7
.8:0350   26 24      ROL $24
.8:0352   C8         INY         ;index next byte in buffer
.8:0353   D0 ED      BNE $0342   ;not all of buffer, loop
.8:0355   E6 06      INC $06     ;next buffer (high byte)
.8:0357   CA         DEX         ;count buffers
.8:0358   10 E8      BPL $0342   ;not all buffers, loop

Next the drive code clears about half of page two ($22A~29E) and also erases the decryption code we just used ($2F3~$367). They don't want us pesky hackers to know what's happening! It also initializes $b5 and $b6 (for buffer transfer of C64 code)

.8:035a   86 C7      STX $C7     ; .X = $ff
.8:035c   86 C8      STX $C8
.8:035e   86 C9      STX $C9
.8:0360   A0 74      LDY #$74    ; $75 (117) bytes to erase
.8:0362   84 B5      STY $B5     ;(actual transfer low-byte count... used later)
.8:0364   A9 01      LDA #$01
.8:0366   85 B6      STA $B6     ;(mangled transfer high-byte count... used later; count = $074)
.8:0368   4A         LSR A       ; .A = 0
;erase loop
.8:0369   99 F3 02   STA $02F3,Y ;scrub $2F3~367
.8:036c   99 2A 02   STA $022A,Y ;scrub $22A~29E (why?)
.8:036f   88         DEY         ;prior byte
.8:0370   10 F7      BPL $0369   ;not all erased, loop

Up to this point, data communication between the host computer (presumably C64) and disk drive have been using standard (slow) serial communication. Now we come to the first part of true fast-loading. Here we transfer more code to the C64 because that first sector wasn't enough! (Later the $625 code is used to transfer actual file data.)


.8:0372   A0 8C      LDY #$8C    ;index in buffer $300
.8:0374   20 25 06   JSR $0625   ;fast transfer to C64 ($38C~3FF)
~
.8:0625   A9 D0      LDA #$D0    ;command code to execute buffer
.8:0627   85 01      STA $01     ;exec buffer $400 (only execute if transfer takes too long)
.8:0629   8D 05 1C   STA $1C05   ;hardware timer set
.8:062c   58         CLI         ;enable interrupts (if transfer goes well, no IRQ will happen)
;xfer loop
.8:062d   B1 30      LDA ($30),Y ;read data
.8:062f   AA         TAX         ;save (low nibble)
.8:0630   4A         LSR A       ;shift high nibble down
.8:0631   4A         LSR A
.8:0632   4A         LSR A
.8:0633   4A         LSR A
.8:0634   48         PHA         ;save shifted high nibble
.8:0635   A9 02      LDA #$02    ;serial bus DATA low, CLK high
.8:0637   8D 00 18   STA $1800   ;tell C64 byte is ready to xfer
.8:063a   8A         TXA         ;low nibble of data
.8:063b   AE 00 18   LDX $1800   ;current serial bus state
;wait for C64 part 1
.8:063e   EC 00 18   CPX $1800   ;test serial bus
.8:0641   F0 FB      BEQ $063E   ;wait for C64 ready
.8:0643   29 0F      AND #$0F    ;mask low nibble
;wait for C64 part 2
.8:0645   EC 00 18   CPX $1800   ;test serial bus
.8:0648   D0 FB      BNE $0645   ;wait for C64 ready
;here we go!
.8:064a   8D 00 18   STA $1800   ;put 2 bits on the serial bus (bits 3 and 1)
.8:064d   0A         ASL A       ;shift bits 2 and 0 up (to bits 3 and 1)
.8:064e   29 0A      AND #$0A    ;clear high nibble
.8:0650   8D 00 18   STA $1800   ;put 2 bits on the serial bus
.8:0653   68         PLA         ;high nibble (shifted down)
.8:0654   8D 00 18   STA $1800   ;put 2 bits on the serial bus
.8:0657   0A         ASL A       ;shift bits 2 and 0 up
.8:0658   29 0A      AND #$0A    ;clear high nibble
.8:065a   8D 00 18   STA $1800   ;put 2 bits on the serial bus
.8:065d   A9 0A      LDA #$0A    ;value for CLK and DATA both high
.8:065f   C6 B5      DEC $B5     ;count bytes in stream (low)
.8:0661   8D 00 18   STA $1800   ;signal 'not ready' to C64
.8:0664   D0 04      BNE $066A   ;not stream end, index next byte
.8:0666   C6 B6      DEC $B6     ;maybe stream end, test high byte
.8:0668   F0 2B      BEQ $0695   ;exit (end of stream)
;index next byte
.8:066a   C8         INY
.8:066b   D0 C0      BNE $062D   ;not all bytes in buffer, xfer loop
;switch buffer (not used for C64 code transfer)
.8:066d   A9 03      LDA #$03    ;last buffer high-byte
.8:066f   C5 31      CMP $31     ;did we just do last buffer?
.8:0671   85 31      STA $31     ;(set high byte)
.8:0673   D0 B8      BNE $062D   ;no, loop to do last buffer now
.8:0675   18         CLC         ;yes, flag buffer transfer complete (stream not finished)
.8:0676   78         SEI
.8:0677   60         RTS
~end of streeam (end of C64 code transfer)
.8:0695   38         SEC         ;flag end-of-stream
.8:0696   78         SEI
.8:0697   60         RTS
  Meanwhile (back at the C64...) 

The last we saw of the C64 was when it sent Memory-Execute command to the disk drive. Let's see what's happened since:

.C:02d8   78         SEI         ;disable interrupts
.C:02d9   18         CLC
.C:02da   AD 00 DD   LDA $DD00   ;get serial bus bits
.C:02dd   48         PHA         ;waste time
.C:02de   68         PLA
.C:02df   30 F9      BMI $02DA   ;wait for C1541/71 code start
.C:02e1   BA         TSX         ;get stack pointer
.C:02e2   E8         INX         ;skip over return address
.C:02e3   E8         INX         ;(from JSR KERNAL_Stop)
.C:02e4   86 C4      STX $C4     ;store fixed stack pointer
.C:02e6   A4 AE      LDY $AE     ;index for loader ($b6)
.C:02e8   86 C3      STX $C3     ;store SP again ?!
.C:02ea   A9 91      LDA #$91    ;opcode for STA (zp),Y
.C:02ec   20 00 02   JSR $0200   ;call fast load

That looks pretty simple, but it isn't going to load file data yet. Oh no, there is more code to load into $2B6~$329. Anyway, let's look at the C64 side of the fast-loader:

.C:0200   8D 41 02   STA $0241   ;set opcode after read byte (RTS or STA (zp),Y)
.C:0203   A2 00      LDX #$00    ;zero buffer poiner low (.Y is the low-byte index)
.C:0205   86 AE      STX $AE
.C:0207   CA         DEX         ;-1... if no bytes xfer then INX = 0
;loader loop (numbers in [brackets] are instruction cycle times)
.C:0208   A9 10      LDA #$10    ;[2]CLK low (DATA high)
;wait for C1541
.C:020a   0D 00 DD   ORA $DD00   ;[4]merge serial line status
.C:020d   30 3C      BMI $024B   ;[2]DATA high, exit!
.C:020f   C9 40      CMP #$40    ;[2]test CLK input
.C:0211   90 F7      BCC $020A   ;[2]currently low, wait for C1541 
;the C1541 let the clock line go high
;now we(C64) set the CLK line back to low
;which signals we are (almost) ready to receive a byte
.C:0213   8D 00 DD   STA $DD00   ;[4]pull CLK low
;the C1541 knows we're ready, but it can't transmit
;while we're holding CLK low...
;Critically we must avoid bad lines, so test the VIC chip...
.C:0216   29 03      AND #$03    ;[2]keep VIC bank (and clear User-Port bit)
.C:0218   AA         TAX         ;[2]save low bits in .X
;wait for VIC (note carry is set)
.C:0219   AD 12 D0   LDA $D012   ;[4]get VIC raster
.C:021c   E9 32      SBC #$32    ;[2]test with top-of-screen
.C:021e   90 04      BCC $0224   ;[2]no bad lines in border, skip ahead
.C:0220   29 07      AND #$07    ;[2]mask raster-in-char
.C:0222   F0 F5      BEQ $0219   ;[2]last raster-in-char, wait for VIC
;here we go!
.C:0224   8E 00 DD   STX $DD00   ;[4]allow both CLK and DATA to go high (we're REALLY ready now)
.C:0227   8E 40 02   STX $0240   ;[4]save low bits for correction (self-modifying code)
.C:022a   EA         NOP         ;[2]waste 4 cycles
.C:022b   EA         NOP         ;[2]
.C:022c   8A         TXA         ;[2]zero upper bits
.C:022d   0D 00 DD   ORA $DD00   ;[4]read two bits (7 and 6)
.C:0230   4A         LSR A       ;[2]shift down two bits
.C:0231   4A         LSR A       ;[2]
.C:0232   0D 00 DD   ORA $DD00   ;[4]read two bits
.C:0235   4A         LSR A       ;[2]shift down two bits
.C:0236   4A         LSR A       ;[2]
.C:0237   0D 00 DD   ORA $DD00   ;[4]read two bits
.C:023a   4A         LSR A       ;[2]shift down two bits
.C:023b   4A         LSR A       ;[2]
.C:023c   4D 00 DD   EOR $DD00   ;[4]read upper two bits and scramble lower two bits
.C:023f   49 FF      EOR #$FF    ;[2]unscramble lower two bits (value set by code above: 0,1,2 or 3)
.C:0241   91 AE      STA ($AE),Y ;[6]store byte in RAM
.C:0243   C8         INY         ;[2]next byte in page
.C:0244   D0 C2      BNE $0208   ;[3]no overflow, loop
.C:0246   E6 AF      INC $AF     ;next page
.C:0248   4C 08 02   JMP $0208   ;loop
;DATA high exit
.C:024b   84 AE      STY $AE     ;save low byte of end-load-address
.C:024d   29 40      AND #$40    ;test serial CLK status
.C:024f   F0 02      BEQ $0253   ;low okay, skip ahead
.C:0251   A9 88      LDA #$88    ;mangled error code
.C:0253   0A         ASL A       ;carry set if error, clear if none
.C:0254   85 90      STA $90     ;set result code (0=okay, $10=error)
.C:0256   60         RTS

That's a nice fast-load routine. It takes 57 cycles (less than 1 raster) to transfer and store a byte (assuming drive is ready, no VIC bad-line, and VIC not in border). Just remember the first time that subroutine is called we're not loading file data, but loading 116 ($74) more bytes of fast-loader code (includes default KERNAL vectors). When we RTS above, we'll be returning to the interior of that freshly loaded code. Let's take a peek:

;post-stream loop
.C:02ef   2C 00 DD   BIT $DD00   ;test serial lines
.C:02f2   70 C2      BVS $02B6   ;CLK high, exit loader
.C:02f4   30 F9      BMI $02EF   ;DATA high, wait
;load new stream
.C:02f6   68         PLA         ;discard old return address 
.C:02f7   68         PLA
.C:02f8   A0 04      LDY #$04    ;get 4 bytes = return/execute address and load address
;stream header loop
.C:02fa   A9 60      LDA #$60    ;RTS opcode (fetch just one byte)
.C:02fc   20 00 02   JSR $0200   ;fast load (byte)
.C:02ff   E8         INX         ;any data?
.C:0300   F0 B4      BEQ $02B6   ;no, exit loader
.C:0302   48         PHA         ;save on stack
.C:0303   88         DEY         ;count, all four bytes?
.C:0304   D0 F4      BNE $02FA   ;no, stream header loop
;get load address off stack
.C:0306   68         PLA         ;high byte
.C:0307   85 AF      STA $AF     ;poiner high
.C:0309   68         PLA         ;low byte
.C:030a   A8         TAY         ;set index (pointer low = 0)
.C:030b   A9 91      LDA #$91    ;opcode for STA (zp),Y
.C:030d   20 00 02   JSR $0200   ;fast load byte stream
.C:0310   4C EF 02   JMP $02EF   ;post-stream loop

Microprose's loader scheme uses custom file data. A single file can consist of multiple streams. Each stream begins with a 7-byte header: (mangled) 2-byte stream length, last stream flag, 2-byte execute (return) address and 2-byte load address. The stream length and last-stream flag bytes are not transmitted to the C64 (as far as the computer knows, each stream has a 4-byte header). Also the drive code doesn't care about the load and execute addresses -- as far as the drive is concerned, a stream header is only 3 bytes. (The 4-byte portion of the header is included in the stream length.) Here's an example:

OffsetValueDescriptionElaboration
0$04Stream-length low 
1$09Stream-length high+1Stream length = $804 bytes
2$01Last stream flagTrue (any non-zero value)
3$e0Execute low -1 
4$02Execute highExecute code at $2e1 after load
5$01Load-address low 
6$08Load-address highLoad starting at $801
7+variesFile data$800 (2048) bytes of data

This is a bit wasteful as the execute/return address is discarded for all but the final stream. The important thing is now (finally!) the C64 is ready to receive real file data. However, the drive code still needs to do its (main) copy-protection code.

.8:0377   A9 08      LDA #$08    ;force CLK low (allow DATA high)
.8:0379   8D 00 18   STA $1800   ;update serial bus lines (tell C64 to wait for stream header)
.8:037c   4C 86 07   JMP $0786   ;do copy protection (?)
~
.8:0786   78         SEI
.8:0787   A6 0B      LDX $0B     ;($4d) 78 bytes to decrypt
.8:0789   4A         LSR A       ;.A = 4
.8:078a   A8         TAY
.8:078b   20 7F 03   JSR $037F
~
.8:037f   3E 20 04   ROL $0420,X ;decryptor buffer
.8:0382   CA         DEX         ;count down
.8:0383   10 FA      BPL $037F   ;not all, loop
.8:0385   CA         DEX
.8:0386   CA         DEX
.8:0387   86 79      STX $79     ;.X = $fd (-3 for stream header)
.8:0389   60         RTS
~
.8:078e   49 0A      EOR #$0A    ;.A = $0E
.8:0790   AA         TAX
.8:0791   B1 38      LDA ($38),Y ;$38 -> $807; missing RAM (get .A = 8)
.8:0793   10 01      BPL $0796   ;always?
.8:0795   E8         INX         ;.A = $0F
.8:0796   86 13      STX $13
.8:0798   8E 00 18   STX $1800   ;set CLK and DATA low (prep to send stream header)
.8:079b   A9 88      LDA #$88
.8:079d   8D A9 02   STA $02A9
.8:07a0   A9 09      LDA #$09
.8:07a2   85 C6      STA $C6
.8:07a4   A9 80      LDA #$80
.8:07a6   85 11      STA $11
.8:07a8   85 0A      STA $0A     ;flag stream header at beginning of file

Remember the mystery directory bytes that we 'decrypted' and stored at $d6, $d7? Well, it's a mangled track/sector pair. Specifically

  • 0 → track 19
  • 16 → track 35
  • 17 → track 17
  • 18 → track 16
  • 33 → track 1
;decode track#
.8:07aa   A5 D6      LDA $D6     ;mangled track#
.8:07ac   29 3F      AND #$3F    ;clear high two bits
.8:07ae   18         CLC
.8:07af   69 13      ADC #$13    ;add 19
.8:07b1   C9 24      CMP #$24    ;test limit (track 36)
.8:07b3   90 08      BCC $07BD   ;less okay, skip ahead
.8:07b5   E9 24      SBC #$24    ;sub 36
.8:07b7   85 C2      STA $C2     ;save delta (possibly zero)
.8:07b9   A9 11      LDA #$11    ;track 17
.8:07bb   E5 C2      SBC $C2     ;subtract delta
.8:07bd   85 C2      STA $C2     ;destination track of file

The drive code then continues with:

.8:07bf   18         CLC
.8:07c0   A5 16      LDA $16     ;sector's disk ID1
.8:07c2   65 17      ADC $17     ;sector's disk ID2
.8:07c4   AA         TAX
.8:07c5   4D 51 01   EOR $0151   ;initially zero
.8:07c8   4D 52 01   EOR $0152   ;initially zero
.8:07cb   8E 51 01   STX $0151   ;update ??
.8:07ce   84 16      STY $16     ;4 (buffer high byte)
.8:07d0   84 D4      STY $D4     ;4
.8:07d2   20 58 05   JSR $0558   ;do main (copy protection, file load)
~
.8:0558   49 6B      EOR #$6B
.8:055a   48         PHA         ;$ea first sector
.8:055b   20 B6 06   JSR $06B6   ;step head in (bigger track#; reads $152,x into $0e)
.8:055e   20 2A 05   JSR $052A
.8:0561   68         PLA
.8:0562   F0 23      BEQ $0587

Let's look at the code at $052A. I don't understand it's purpose, but what it does is calculate a value in $c5 before VIA timer expires. If $c5 underflows before the timer, then the code will attempt to reposition the drive head by half-track steps.

.8:052a   A9 18      LDA #$18    ;24 initial value
.8:052c   85 C5      STA $C5
;calculate $c5 loop
.8:052e   A2 1D      LDX #$1D    ;initial X value
.8:0530   20 33 07   JSR $0733   ;set X counter after sync
.8:0533   B0 17      BCS $054C   ;no Sync, skip ahead
.8:0535   49 83      EOR #$83    ;.A = $85 
.8:0537   38         SEC 
.8:0538   8D 05 1C   STA $1C05   ;set timer 
.8:053b   8A         TXA         ;counter for next-byte
;disk-head byte wait loop
.8:053c   AC 05 1C   LDY $1C05   ;test H/W timer
.8:053f   10 11      BPL $0552   ;timeout!
.8:0541   50 F9      BVC $053C   ;wait for head read byte
.8:0543   E5 BD      SBC $BD     ;counter -delta
.8:0545   B8         CLV         ;ready for next head byte
.8:0546   B0 F4      BCS $053C   ;counter underflow? no, byte wait loop

.8:0548   C6 C5      DEC $C5     ;all attemps done?
.8:054a   D0 E2      BNE $052E   ;no, find calc $c5 loop

.8:054c   20 04 06   JSR $0604   ;reposition head by half-step
.8:054f   90 D9      BCC $052A   ;not out of error-steps, start over
.8:0551   60         RTS
;$c5 has been set
.8:0552   88         DEY         ;$7e
.8:0553   84 17      STY $17     ;address low for $400 buffer
.8:0555   4C 64 07   JMP $0764   ;finish routine
~head on new track, update timer
.8:0764   38         SEC
.8:0765   E5 1D      SBC $1D
.8:0767   85 11      STA $11     ;timer constant for $6f3 delay routine
;head on new track, set magic byte $0e
.8:0769   A6 22      LDX $22     ;track#
.8:076b   E0 13      CPX #$13    ;19 or more?
.8:076d   90 02      BCC $0771   ;no skip ahead
.8:076f   CA         DEX         ;X-=2
.8:0770   CA         DEX
.8:0771   BD 52 01   LDA $0152,X ;magic table
.8:0774   85 0E      STA $0E     ;magic byte
.8:0776   A9 EE      LDA #$EE
.8:0778   8D 0C 1C   STA $1C0C   ;request byte-ready (set overflow)
.8:077b   18         CLC
.8:077c   60         RTS

I'm not sure what the magic table is used for (besides updating magic byte $0e per track). Less importantly there is a custom delay routine (via X register) at $6f3:

.8:06f3   86 10      STX $10
.8:06f5   A6 11      LDX $11     ;'constant' (like $72=114) set by $764 new track routine
.8:06f7   CA         DEX
.8:06f8   D0 FD      BNE $06F7
.8:06fa   C6 10      DEC $10
.8:06fc   D0 F7      BNE $06F5
.8:06fe   18         CLC
.8:06ff   60         RTS

More importantly, let's go back and look at the code at $564:

.8:0564   20 B2 06   JSR $06B2   ;que $700 buffer to execute; step in one track
.8:0567   AD 00 1C   LDA $1C00
.8:056a   49 60      EOR #$60    ;toggle bit-rate
.8:056c   20 E8 06   JSR $06E8   ;update bit-rate, update head on new track (magic $0e byte)
.8:056f   20 0D 05   JSR $050D   ;wait for sync-mark, count up X into $150
.8:0572   50 FE      BVC $0572   ;wait for byte after sync
.8:0574   A0 FD      LDY #$FD    ;253 byte limit limit
.8:0576   8C 05 1C   STY $1C05   ;set hardware timer
;count byte loop
.8:0579   58         CLI
.8:057a   A6 04      LDX $04
.8:057c   10 09      BPL $0587   ;buffer code $700 executed, exit
.8:057e   50 F9      BVC $0579   ;wait for normal byte received (sync bytes don't update this)
.8:0580   78         SEI
.8:0581   B8         CLV         ;ready for next byte
.8:0582   88         DEY         ;byte limit reached?
.8:0583   D0 F4      BNE $0579   ;no, count loop
.8:0585   F0 DD      BEQ $0564   ;yes, try next greater track (step in)

What that code appears to do is search for a track that has a really long sync-mark(s). In other words, no sequence of normal bytes greater than 253. (A normal sector contains way more than 253 bytes). On the disks I've tested, this special track is always on track 36, although that's not required. Interestingly, if no track has the special "lots of sync marks" signature then the disk-head will eventually jam against the track-stop next to the spindle motor. On the C1541 and C1571 disk drives I've owned, this causes the disk-head to get 'permanently' stuck: no disk commands can move the head back to a normal track! In other words, the device must be opened and the disk-head manually repositioned before it can be used again. Anyway, when the special track is found, the hardware timer triggers an IRQ at which point the code in buffer $700 executes:

;do main copy protection (read magic table $152)
.8:0700   78         SEI         ;superflous (we're in an interrupt)
.8:0701   A9 09      LDA #$09    ;result code
.8:0703   85 04      STA $04     ;update controller code (this breaks the $564 search loop)
.8:0705   85 C6      STA $C6
.8:0707   A9 DB      LDA #$DB
.8:0709   85 BB      STA $BB
.8:070b   A9 4E      LDA #$4E    ;bit-rate %10
.8:070d   20 E8 06   JSR $06E8   ;update bit-rate, update head on new track (magic $0e byte)
.8:0710   20 89 04   JSR $0489   ;read-in magic $152 table
.8:0713   B0 6E      BCS $0783   ;error decoding, terminate loader!
.8:0715   46 01      LSR $01     ;clear 'buffer-execute' command for buffer $400
.8:0717   60         RTS

What that code does is either fill $152~174 with 'magic bytes' that correspond to tracks 1~35 (less track 18 and 19), or aborts the load on failure. Whenever the disk head moves, byte $0e is updated based on that table. I still haven't figured out what byte $0e does. Anyway, let's see what happens after the magic table has been read. Spoiler: it's the main load-and-transfer routine.

.8:0587   78         SEI
.8:0588   A9 D0      LDA #$D0    ;'execute buffer' command
.8:058a   85 01      STA $01     ;for buffer $400 -- only if things go horribly wrong
.8:058c   20 5D 07   JSR $075D   ;update $11, update $0e from magic table
.8:058f   A5 D7      LDA $D7     ;starting sector# for file
.8:0591   85 23      STA $23     ;current sector# to read
.8:0593   A6 C2      LDX $C2     ;track# for file
.8:0595   24                     ;skip next opcode
.8:0596   E8         INX         ;next greater track (inward)
.8:0597   24                     ;skip next opcode
.8:0598   CA         DEX         ;next lower track (outward)
.8:0599   86 C2      STX $C2     ;save (new) track# for file
.8:059b   20 B9 06   JSR $06B9   ;move head to new track (multi step possible)
;read file from current track
.8:059e   A9 09      LDA #$09
.8:05a0   85 C6      STA $C6
.8:05a2   85 E7      STA $E7
.8:05a4   20 22 04   JSR $0422   ;find sector (# in $23) and read into $47e~4ff and $300~3ff
.8:05a7   B0 59      BCS $0602   ;couldn't read sector (after multiple tries), exit
.8:05a9   A6 7B      LDX $7B     ;4
.8:05ab   06 0A      ASL $0A
.8:05ad   B0 06      BCS $05B5   ;begin w/stream header
;.A is normally zero
.8:05af   A6 16      LDX $16     ;4
.8:05b1   65 15      ADC $15     ;+$88 typical
.8:05b3   85 7A      STA $7A     ;set initial Y index
;read stream header into $b5~b7 if needed
.8:05b5   86 31      STX $31     ;set pointer high-byte
;
.8:05b7   A4 7A      LDY $7A     ;initial value typically $88 (136)
.8:05b9   A6 79      LDX $79     ;initial value typically $fd (-3) or 0
.8:05bb   10 15      BPL $05D2   ;no/end stream header, skip ahead
.8:05bd   B1 30      LDA ($30),Y ;read buffer
.8:05bf   95 B8      STA $B8,X   ;stream header info ($b5~b7)
.8:05c1   E8         INX
.8:05c2   86 79      STX $79
.8:05c4   C8         INY
.8:05c5   84 7A      STY $7A
.8:05c7   D0 EE      BNE $05B7   ;typically
.8:05c9   A6 16      LDX $16     ;4
.8:05cb   E4 31      CPX $31
.8:05cd   D0 08      BNE $05D7   ;stream header crosses sector boundry
.8:05cf   CA         DEX         ;3
.8:05d0   D0 E3      BNE $05B5   ;always
;
.8:05d2   20 25 06   JSR $0625   ;transfer data to C64
.8:05d5   B0 12      BCS $05E9   ;end of file stream
.8:05d7   A5 23      LDA $23     ;next sector on same track?
.8:05d9   D0 C3      BNE $059E   ;yes, loop
.8:05db   A6 C2      LDX $C2     ;file track#
.8:05dd   E0 13      CPX #$13    ;19 or more?
.8:05df   90 B7      BCC $0598   ;no, decrement track# (step out)
.8:05e1   E0 23      CPX #$23    ;less than 35?
.8:05e3   90 B1      BCC $0596   ;yes, increment track# (step in)
.8:05e5   A2 11      LDX #$11    ;no, go to track #17
.8:05e7   D0 B0      BNE $0599   ;always
;finished file stream (check for another)
.8:05e9   A9 08      LDA #$08    ;pull CLK low (allow DATA high)
.8:05eb   8D 00 18   STA $1800   ;update serial lines
.8:05ee   A2 12      LDX #$12    ;wait counter
.8:05f0   CA         DEX
.8:05f1   D0 FD      BNE $05F0
.8:05f3   A5 B7      LDA $B7     ;last stream?
.8:05f5   D0 0B      BNE $0602   ;yes
.8:05f7   A9 0A      LDA #$0A    ;pull CLK and DATA low
.8:05f9   8D 00 18   STA $1800   ;update serial lines (signal new stream for C64)
.8:05fc   A9 FD      LDA #$FD    ;reset index (-3) for new stream header
.8:05fe   85 79      STA $79
.8:0600   D0 C2      BNE $05C4   ;always
;sector not found or last stream done
.8:0602   38         SEC
.8:0603   60         RTS

If you study the main loop above, you should see a large part (almost 50%?) deals with managing the stream-header (which is a small part of the total data). This is because the stream-header can be split across either a page boundary or a sector boundry. Before moving on to see what happens after all data has been loaded, lets have a detailed look at Read Sector ($422):

;normal error
.8:0415   C6 BE      DEC $BE     ;count retries on track
.8:0417   10 0D      BPL $0426   ;okay, keep going
.8:0419   C6 E7      DEC $E7     ;count retries off-track
.8:041b   30 E3      BMI $0400   ;out of attemps, abort load
.8:041d   20 4C 05   JSR $054C   ;reposition head by half-track
.8:0420   B0 DE      BCS $0400   ;out of attemps, abort load
;entry point for Read Sector
.8:0422   A9 24      LDA #$24    ;36 tries to find sector on track
.8:0424   85 BE      STA $BE
.8:0426   A5 16      LDA $16     ;4
.8:0428   85 31      STA $31     ;high-byte pointer
.8:042a   20 78 06   JSR $0678   ;extended wait for Sync Mark, get .Y = 0
.8:042d   B0 EA      BCS $0419   ;error, no sync found
.8:042f   A4 17      LDY $17     ;$7e initial index for page $400
.8:0431   A9 6B      LDA #$6B    ;desired byte (sector start flag)
.8:0433   50 FE      BVC $0433   ;wait for first byte after sync
.8:0435   B8         CLV         ;ready for next byte
.8:0436   4D 01 1C   EOR $1C01   ;test byte from disk-head
.8:0439   D0 DA      BNE $0415   ;mismatch error
;main sector-read loop
.8:043b   50 FE      BVC $043B   ;wait for data byte
.8:043d   B8         CLV         ;ready for next byte
.8:043e   AD 01 1C   LDA $1C01   ;get data byte
.8:0441   AA         TAX         ;save a copy
.8:0442   29 49      AND #$49    ;magic mask
.8:0444   85 29      STA $29     ;save addend 1
.8:0446   8A         TXA         ;data byte
.8:0447   29 24      AND #$24    ;magic mask
.8:0449   85 2A      STA $2A     ;save addend 2
.8:044b   A9 B6      LDA #$B6    ;magic mask
.8:044d   50 FE      BVC $044D   ;wait for data byte
.8:044f   2D 01 1C   AND $1C01   ;get and mask
.8:0452   65 29      ADC $29     ;addend 1 (clear overflow)
.8:0454   91 30      STA ($30),Y ;save decoded byte 1
.8:0456   45 D5      EOR $D5     ;update checksum
.8:0458   85 D5      STA $D5     ;store it
.8:045a   C8         INY         ;index next (odd) data byte
.8:045b   A9 DB      LDA #$DB    ;magic mask
.8:045d   50 FE      BVC $045D   ;wait for data byte
.8:045f   2D 01 1C   AND $1C01   ;get and mask 
.8:0462   65 2A      ADC $2A     ;addend 2 (clear overflow)
.8:0464   91 30      STA ($30),Y ;save decoded byte 2
.8:0466   45 D5      EOR $D5     ;update checksum
.8:0468   85 D5      STA $D5     ;store it
.8:046a   C8         INY         ;index next (even) data byte
.8:046b   D0 CE      BNE $043B   ;same page of RAM, loop
.8:046d   A2 03      LDX #$03    ;last page of RAM used
.8:046f   E4 31      CPX $31     ;was that the last page?
.8:0471   B8         CLV         ;ready for next byte
.8:0472   86 31      STX $31     ;update page-pointer
.8:0474   AE 01 1C   LDX $1C01   ;get dummy byte
.8:0477   90 C2      BCC $043B   ;not last page, loop
.8:0479   18         CLC         ;flag no error (assume)
.8:047a   A8         TAY         ;test checksum
.8:047b   D0 98      BNE $0415   ;error, try starting again
;this part ($47e~4ff) comes from the sector!
;only executed if the checksum is valid
.8:047d   A6 EE      LDX $EE     ;get dummy byte ($EE is the sector checksum)
.8:047f   A9 05      LDA #$05    ;this is sector 5
.8:0481   45 23      EOR $23     ;test with desired sector in file chain
.8:0483   D0 90      BNE $0415   ;wrong sector, try starting again
.8:0485   E6 23      INC $23     ;calc next sector# in file chain
.8:0487   60         RTS

If you study that code, you should see that this Microprose disk-scheme uses a custom sector format. In particular it uses a 3-byte to 2-byte decoding scheme (24-bit to 16-bit), which has a 50% overhead. Compare this with a standard sector decoding which is 5-byte to 4-byte (40-bit to 32-bit) which has just a 25% overhead. In other words, this encoding scheme has twice as much overhead! On the other hand, decoding is simple and quick (standard decoding is quite a mess and slow). To help make up for the reduced encoding efficiency, sectors are larger than standard. Instead of 260 ($104) decoded bytes, this scheme produces 386 ($182) decoded bytes per sector.

However, not all 386 bytes are file data. One byte is a checksum and typically 9 bytes are 6502-code used to identify the current sector and set the next sector. In other words, 376 bytes are typically file data (could be less depending on how big the loaded code is). Interstingly, the start of the sector includes machine-language code which over-writes the end of the Read_Sector routine! Note this loaded code is not executed unless the sector checksum is valid. Because the sectors are bigger in this disk-scheme, there are fewer sectors per track. As far as I can tell, tracks 1~17 have 12 sectors (0 to 11) while tracks 19~35 have 11 sectors (0 to 10). Hopefully this table will help:

GCR
Offset
Binary
Offset
File
Offset
ValueBuffer
Address
Description
0n/an/a$6Bn/asector identifier
1~20n/avaries$47esector checksum
3~201~9n/avaries$47F~487code to check sector# (typical)
21~26010~1290~119varies$488~4FF120 data bytes (typical)
261n/an/avariesn/adummy byte for buffer change
262~773130~385120~375varies$300~3FF256 data bytes

After all streams are transferred, we return to code at $7d5. Which is basically just clean-up code. It erases most RAM, but in particular the magic table at $152~175 is left untouched. It also moves the disk-head back to track 18 (to read BAM/directory again):

.8:07d5   A9 12      LDA #$12    ;18
.8:07d7   78         SEI
.8:07d8   85 13      STA $13     ;???
.8:07da   20 BA 06   JSR $06BA   ;move disk-head to track .A
.8:07dd   A9 00      LDA #$00
.8:07df   AA         TAX
;zero almost all RAM
.8:07e0   95 00      STA $00,X   ;clear zero page
.8:07e2   9D 00 03   STA $0300,X ;all of page $300
.8:07e5   9D 15 04   STA $0415,X ;most of page $400 (don't erase $400 stub)
.8:07e8   9D 15 05   STA $0515,X ;all of page $500
.8:07eb   9D 15 06   STA $0615,X ;all of page $600
.8:07ee   9D E0 06   STA $06E0,X ;most of page $700 (don't erase ourself!)
.8:07f1   E8         INX
.8:07f2   D0 EC      BNE $07E0
;
.8:07f4   A9 D0      LDA #$D0    ;command code for "execute buffer"
.8:07f6   85 01      STA $01     ;code for buffer $400
.8:07f8   A9 2C      LDA #$2C    ;opcode BIT (don't JMP)
.8:07fa   8D 00 04   STA $0400   ;modify code
.8:07fd   4C 22 EB   JMP $EB22   ;ROM initialize zero-page

After the ROM initializes zero-page RAM, the controller will execute the following code:

.8:0400   2C D5 07   BIT $07D5   ;don't JMP $7D5 (aka, NOP)
.8:0403   A9 C6      LDA #$C6    ;disk motor on, drive light off
.8:0405   8D 00 1C   STA $1C00   ;update hardware
.8:0408   46 01      LSR $01     ;'erase' controller code for this buffer
.8:040a   E6 3E      INC $3E     ; 0 -> 1; one active drive
.8:040c   20 05 D0   JSR $D005   ;read BAM into RAM
.8:040f   A2 45      LDX #$45    ;reset CPU stack
.8:0411   9A         TXS
.8:0412   4C C5 EB   JMP $EBC5   ;enter command-wait loop

And that's the end of the drive code -- yeah! But the C64 needs to finsh too. Let's take a look:

.C:02b6   A0 58      LDY #$58
.C:02b8   2C                     ;skip next opcode
.C:02b9   E6 09      INC $09     ;next page of RAM
.C:02bb   91 08      STA ($08),Y ;erase $201~258 (or EVERYTHING if error)
.C:02bd   88         DEY
.C:02be   D0 FB      BNE $02BB
.C:02c0   A6 9D      LDX $9D     ;show KERNAL messages (like LOADING)?
.C:02c2   F0 02      BEQ $02C6   ;no
.C:02c4   B0 F3      BCS $02B9   ;error, erase more RAM ($300+)
;this code gets called regardless of $9d
.C:02c6   20 53 E4   JSR $E453   ;restore BASIC vectors ($300~30b)
.C:02c9   A2 47      LDX #$47
.C:02cb   8E 18 03   STX $0318   ;change NMI low byte
.C:02ce   A6 AE      LDX $AE     ;load YX with load end address
.C:02d0   A4 AF      LDY $AF
.C:02d2   A5 9D      LDA $9D     ;show KERNAL messages?
.C:02d4   F0 04      BEQ $02DA   ;no, skip ahead
.C:02d6   86 2D      STX $2D     ;save end address into BASIC VarTab
.C:02d8   84 2E      STY $2E
.C:02da   90 03      BCC $02DF   ;no error exit
.C:02dc   A6 C4      LDX $C4     ;error -- restore stack pointer
.C:02de   9A         TXS
.C:02df   58         CLI
.C:02e0   60         RTS

If the loaded file is a BASIC program, the return address is typically set to $2e1:

.C:02e1   A2 02      LDX #$02    ;set BASIC TxtPtr to $2EC
.C:02e3   86 7B      STX $7B
.C:02e5   A2 EC      LDX #$EC
.C:02e7   86 7A      STX $7A
.C:02e9   58         CLI         ;enable interrupts
.C:02ea   6C 08 03   JMP ($0308) ;IGONE -- execute BASIC code (reads from $2ED)
.C:02ed   8A         TXA         ;BASIC token for RUN
.C:02ee   00         BRK         ;BASIC end-of-line
  Summary 
  • Blank screen: no
  • Interrupts allowed: no
  • Disk Header: modified (code at $7B0)
  • Directory structure: modified
  • Allow wildcard in filename: yes
  • File structure: custom (multiple streams and execute address)
  • Sector structure: custom (376 data bytes)
  • Sector decoding time: 0 milliseconds
  • Head stepping speed: medium (about 7.8 milliseconds/half-track)
  • Disk → C64 transfer: fast (nominal 57 microseconds/byte)
  • C64 → Disk transfer (filename): slow/standard (about 1300 microseconds/byte)
  • C64 memory footprint: under 0.25K ($200~258, $2B6~327)
  • Needs KERNAL: yes
  • Load $D000~DFFF: I/O registers
  • Alters User Port: yes
  • Requires Unit 8: no
  • Write file/sector: no
  • Other: every file (re)loads code into C1541 and C64; first file steps head to track 36

© Hydrophilic.net, 2026