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

  Epyx Copy-protection & Fast-loader 

This fast-loader was used at least in Super Cycle by Epyx. I believe its name is Vorpal Loader, but I'm not sure.

The first file on the disk is an "auto-run" file. That is, the user types LOAD"*",8,1 and presses return then the program loads and RUNs without further user interaction. It loads into RAM region $128~205 and works by over-writing the CPU stack with 2's. When the system LOAD routine tries to return to its caller, the CPU will instead execute the code at $203 (i.e., $202+1). Let's look at the code:

.C:0203   4C 28 01   JMP $0128
~
.C:0128   EA         NOP
.C:0129   EA         NOP
.C:012a   EA         NOP
.C:012b   EA         NOP
.C:012c   EA         NOP
.C:012d   EA         NOP
.C:012e   EA         NOP
.C:012f   EA         NOP
.C:0130   EA         NOP
.C:0131   EA         NOP
.C:0132   EA         NOP
.C:0133   EA         NOP
.C:0134   EA         NOP
.C:0135   EA         NOP
.C:0136   EA         NOP
.C:0137   EA         NOP
.C:0138   EA         NOP
.C:0139   EA         NOP
.C:013a   EA         NOP
.C:013b   EA         NOP
.C:013c   A9 00      LDA #$00    ;turn off messages (like LOADING)
.C:013e   20 90 FF   JSR $FF90   ;KERNAL SetMsg
.C:0141   A9 0F      LDA #$0F    ;filename length
.C:0143   A2 64      LDX #$64    ;.YX = $164 (address of "VLOADER        ")
.C:0145   A0 01      LDY #$01
.C:0147   20 BD FF   JSR $FFBD   ;KERNAL SetNam
.C:014a   A9 50      LDA #$50    ;file#
.C:014c   A6 BA      LDX $BA     ;device# (usually 8)
.C:014e   A0 01      LDY #$01    ;channel#
.C:0150   20 BA FF   JSR $FFBA   ;KERNAL SetLFS
.C:0153   A9 00      LDA #$00    ;load (not verify)
.C:0155   A2 FF      LDX #$FF    ;.YX = dummy address (ignored)
.C:0157   A0 FF      LDY #$FF
.C:0159   20 D5 FF   JSR $FFD5   ;KERNAL Load
.C:015c   A9 26      LDA #$26    ;KERNAL ROM and I/O (no BASIC ROM)
.C:015e   85 01      STA $01     ;set memory configuration
.C:0160   EA         NOP
.C:0161   4C 98 BD   JMP $BD98   ;start up code

I don't know why it starts with so many NOPs, but after that it turns off KERNAL messages and (slow) loads the file "VLOADER        " (with a bunch of non-breaking spaces) into the file-indicated load-address of $BD70 (to $BFFF). Then it changes memory configuration (no BASIC ROM) and jumps to $BD98; let's have a peek:

.C:bd98   A9 BD      LDA #$BD    ;set KERNAL NMI vector to $BDDC
.C:bd9a   8D 19 03   STA $0319   ;which contains an RTI instruction
.C:bd9d   A9 DC      LDA #$DC
.C:bd9f   8D 18 03   STA $0318
.C:bda2   A9 00      LDA #$00    ;set TimerA high
.C:bda4   8D 05 DD   STA $DD05
.C:bda7   A9 1E      LDA #$1E    ;set TimerA low (about 31 microseconds)
.C:bda9   8D 04 DD   STA $DD04
.C:bdac   A9 81      LDA #$81
.C:bdae   8D 0D DD   STA $DD0D   ;enable TimerA interrupts
.C:bdb1   8D 0E DD   STA $DD0E   ;set TOD to PAL and start TimerA
.C:bdb4   20 F1 BF   JSR $BFF1   ;indirect call $BECE
~
.C:bece   2C FD BE   BIT $BEFD   ;do nothing
.C:bed1   A9 4C      LDA #$4C    ;opcode JMP absolute
.C:bed3   8D CE BE   STA $BECE   ;modify code (next call to $BFF1 will JMP directly to $BEFD)
.C:bed6   AD A6 02   LDA $02A6   ;NTSC/PAL flag
.C:bed9   C9 01      CMP #$01    ;is this PAL
.C:bedb   D0 20      BNE $BEFD   ;no, skip ahead
;(a PAL machine will modify several bytes here -- not shown)
.C:befd   20 FB BD   JSR $BDFB   ;effectively OPEN 2,8,2,"#" (note device 8 is hard-coded)
.C:bf00   20 ED BD   JSR $BDED   ;effectively OPEN 15,8,15,"B-E 2 0 18 6"
.C:bf03   78         SEI
.C:bf04   4C BF BE   JMP $BEBF
~
.C:bebf   AD 00 DD   LDA $DD00   ;get serial I/O bits
.C:bec2   29 0F      AND #$0F    ;allow CLK and DATA to go high
.C:bec4   8D 00 DD   STA $DD00   ;update serial lines
.C:bec7   AE 76 BE   LDX $BE76   ;255
.C:beca   AC 77 BE   LDY $BE77   ;255
.C:becd   60         RTS
~
.C:bdb7   A9 00      LDA #$00
.C:bdb9   85 FE      STA $FE
.C:bdbb   A9 40      LDA #$40
.C:bdbd   85 FF      STA $FF
.C:bdbf   A2 19      LDX #$19    ;2nd command byte: starting track#
.C:bdc1   A0 00      LDY #$00    ;3rd command byte: starting sector#
.C:bdc3   A9 63      LDA #$63    ;1st command byte: # sectors to load
.C:bdc5   20 F4 BF   JSR $BFF4   ;call fast load (JMP $BF07)

The first thing the code does is set the NMI vector to an RTI instruction and sets CIA2 (the NMI source) to run its timer and trigger an NMI. The NMI will return immediately without clearing the trigger. This means further NMIs (from the Restore key, for example) are not possible.

Next it calls an initialize routine $BFF1. The first time it is called it will modify itself so it is faster in the future. But the first time it will check NTSC/PAL system flag. If it is PAL, several code bytes will be updated (not very important for us). More importantly, it will send a Block-Execute (B-E) command to the disk drive. We'll look at that momentarily. But first the routine allows the serial CLK and DATA lines to go high. Finally it goes to load the first file.

Before looking at the C64 side of the fast-loader, let's see what the disk drive is doing:

.8:0500   78         SEI         ;disable controller
.8:0501   D8         CLD         ;clear BCD mode (these guys are serious or silly, you decide)
.8:0502   A9 08      LDA #$08    ;pull CLK low, allow DATA high
.8:0504   8D 00 18   STA $1800   ;update serial lines
.8:0507   A9 60      LDA #$60    ;RTS opcode
.8:0509   8D 00 03   STA $0300   ;store in RAM (this is silly, RTS opcodes occur throught ROM)
.8:050c   20 00 03   JSR $0300   ;immediate return (with return address on stack)
.8:050f   BA         TSX         ;stack pointer → X register
.8:0510   BD 00 01   LDA $0100,X ;get address high
.8:0513   85 15      STA $15     ;set pointer high
.8:0515   A0 00      LDY #$00    ;address low (and index zero)
.8:0517   84 14      STY $14     ;set pointer low
;copy loop
.8:0519   B1 14      LDA ($14),Y ;read from whichever buffer we're in
.8:051b   99 00 07   STA $0700,Y ;store to known location
.8:051e   C8         INY         ;index next, all done?
.8:051f   D0 F8      BNE $0519   ;no, copy loop   
.8:0521   4C 24 07   JMP $0724   ;yes, (re)initialize RAM
~
;(re)initialize RAM
.8:0724   A2 0E      LDX #$0E    ;index last (15 bytes to copy)
;setup read blocks
.8:0726   BD 45 07   LDA $0745,X ;read data table
.8:0729   95 00      STA $00,X   ;store for controller
.8:072b   CA         DEX         ;index prior, all done?
.8:072c   10 F8      BPL $0726   ;no, setup read blocks
.8:072e   58         CLI         ;allow controller to run
;wait loop
.8:072f   A5 00      LDA $00     ;get buffer $300 status
.8:0731   05 01      ORA $01     ;merge buffer $400 status
.8:0733   05 02      ORA $02     ;merge buffer $500 status
.8:0735   05 03      ORA $03     ;merge buffer $600 status
.8:0737   30 F6      BMI $072F   ;any command still running? yes, wait loop
.8:0739   C9 01      CMP #$01    ;no, any error?
.8:073b   D0 E7      BNE $0724   ;yes, (re)initialize RAM

.8:073d   A9 00      LDA #$00    ;allow CLK and DATA to go high
.8:073f   8D 00 18   STA $1800   ;update serial bus lines
.8:0742   4C 03 04   JMP $0403   ;start loader (get 'filename')
~
>8:0745  80 80 80 80  00 00 12 09   ........
>8:074d  12 0c 12 0f  12 12         ......

The first thing done by the code (besides SEI and CLD) is to update the serial bus. Next the code does a trick to copy itself into $700~7FF. You see, this code could be in any buffer ($300, $400, $500, or $600), but we don't know which. So we JSR to an RTS instruction (which immediately returns) with the side-effect of leaving the return address (our location) on the stack. So we examine the stack to determine where the code is currently at. Once we know, we setup a pointer and copy this (wherever this is) buffer to a known location (buffer $700). Then we jump to the copy at $724.

Next we copy commands and track/sector pairs into low RAM for the drive's controller. This instructs the controller to load 4 buffers as follows:

  • track 18, sector 9 → buffer $300
  • track 18, sector 12 → buffer $400
  • track 18, sector 15 → buffer $500
  • track 18, sector 18 → buffer $600

The code then waits for all four commands to execute and checks if there is any error. The code loops to re-load if there was an error. Otherwise it updates the serial bus lines and jumps to $403. This whole process (load four buffers) only takes about 1 second. The code continues at $403:

.8:0403   A2 45      LDX #$45    ;reset CPU stack
.8:0405   9A         TXS
.8:0406   AD 00 1C   LDA $1C00   ;multi-use register
.8:0409   29 F7      AND #$F7    ;turn off drive's LED
.8:040b   8D 00 1C   STA $1C00
.8:040e   58         CLI         ;enable interrupts
.8:040f   20 A5 06   JSR $06A5   ;read a byte from C64
.8:0412   78         SEI         ;disable interrupts
.8:0413   48         PHA         ;save command
.8:0414   20 A5 06   JSR $06A5   ;read a byte from C64
.8:0417   85 0E      STA $0E     ;save track#
.8:0419   20 A5 06   JSR $06A5   ;read a byte from C64
.8:041c   85 8D      STA $8D     ;save sector# (for us)
.8:041e   85 0F      STA $0F     ;save it again (for controller)
.8:0420   AD 00 1C   LDA $1C00   ;multi-use register
.8:0423   09 08      ORA #$08    ;turn on drive's LED
.8:0425   8D 00 1C   STA $1C00
.8:0428   68         PLA         ;get back command
.8:0429   85 8B      STA $8B     ;save as # sectors
.8:042b   C9 FF      CMP #$FF    ;is command 'read sector'?
.8:042d   F0 12      BEQ $0441   ;yes, go do it
.8:042f   C9 FC      CMP #$FC    ;is command 'custom code'? (read code into $700 buffer and execute)
.8:0431   F0 24      BEQ $0457   ;yes, go do it
.8:0433   C9 FE      CMP #$FE    ;is command 'write sector'?
.8:0435   F0 07      BEQ $043E   ;yes, go do it
.8:0437   C9 FD      CMP #$FD    ;is command 'uninstall'?
.8:0439   D0 25      BNE $0460   ;no, do fast-load
.8:043b   4C 4B EB   JMP $EB4B   ;yes, exit to ROM: init RAM and controller wait
.8:043e   4C 30 03   JMP $0330   ;do 'write sector'

That code is the (re)start of a custom command loop. It turns off the drive LED and waits for 3 bytes (a command) to be received from the C64. Once it gets them, it turns on the drive's LED and then branches to a routine based on the command# (the first byte received). For fast-load, the first byte is the number of sectors to load.

Let's look at how the C64 transmits one of those command bytes:

.C:be1c   8D 7D BE   STA $BE7D   ;save byte to transmit
.C:be1f   A2 08      LDX #$08    ;#bits to transmit
;wait for CLK low -- loop for bits
.C:be21   2C 00 DD   BIT $DD00   ;[4]is CLK low?    <--+-- these execute 3 times (20 cycles)
.C:be24   50 FB      BVC $BE21   ;[2 or 3]no, wait  <--+
.C:be26   AD 00 DD   LDA $DD00   ;[4]
.C:be29   29 DF      AND #$DF    ;[2]allow DATA high
.C:be2b   8D 00 DD   STA $DD00   ;[4]update serial bus
.C:be2e   AD 00 DD   LDA $DD00   ;[4]
.C:be31   09 10      ORA #$10    ;[2]pull CLK low
.C:be33   8D 00 DD   STA $DD00   ;[4]update serial bus
.C:be36   C9 80      CMP #$80    ;[2]copy DATA-in to carry flag
.C:be38   6E 7E BE   ROR $BE7E   ;[5]save DATA bit
.C:be3b   29 BF      AND #$BF    ;[2]clear CLK-in, .A = $97 (assuming VIC Bank 0 and UserPort bit 1)
;wait for DATA low
.C:be3d   CD 00 DD   CMP $DD00   ;[4]is DATA high?   <--+-- these execute 3 times (20 cycles)
.C:be40   F0 FB      BEQ $BE3D   ;[2 or 3]yes, wait  <--+
.C:be42   4E 7D BE   LSR $BE7D   ;[5]get low bit of data into carry flag
.C:be45   A9 00      LDA #$00    ;[2]CLK high, assume DATA high (hardware inversion)
.C:be47   90 02      BCC $BE4B   ;[3.5]good assumption
.C:be49   A9 20      LDA #$20    ;[0]CLK high, DATA low (hardware inversion)
.C:be4b   0D 00 DD   ORA $DD00   ;[4]merge with VIC Bank and UserPort bits
.C:be4e   8D 00 DD   STA $DD00   ;[4]update serial lines
.C:be51   29 EF      AND #$EF    ;[2]CLK high (again)
.C:be53   8D 00 DD   STA $DD00   ;[4]update serial lines
;wait for CLK low
.C:be56   2C 00 DD   BIT $DD00   ;[4]is CLK high?    <--+-- these execute 4 times (27 cycles)
.C:be59   70 FB      BVS $BE56   ;[2 or 3]yes, wait  <--+
.C:be5b   49 20      EOR #$20    ;[2]toggle DATA out bit
.C:be5d   8D 00 DD   STA $DD00   ;[4]update serial lines
.C:be60   CA         DEX         ;[2]count bits, all done?
.C:be61   D0 BE      BNE $BE21   ;[3]no, loop for bits
.C:be63   AD 7E BE   LDA $BE7E   ;get DATA received
.C:be66   60         RTS

That code takes an average 131.5 cycles per loop for a total of 1051 cycles per byte. That's slow for a custom transfer routine (similar to standard ROM), but if you notice: the code not only transmits but receives a byte too! Another nice thing is the code can run without the screen being blanked. Anyway, let's look at what the drive does when it receives a fast-load command:

.8:0460   20 86 05   JSR $0586   ;use ROM to read sector into $700 (just for track movement?)
.8:0463   20 31 05   JSR $0531   ;set bit-rate, sectors/track, and sector interleave
;loop for sectors
.8:0466   A5 8D      LDA $8D     ;physical sector #
.8:0468   18         CLC
.8:0469   6D 15 02   ADC $0215   ;add interleave value
.8:046c   C5 15      CMP $15     ;greater/equal to # sectors/track ?
.8:046e   90 02      BCC $0472   ;no, continue
.8:0470   E5 15      SBC $15     ;yes, subtract #sectors in track
.8:0472   85 0F      STA $0F     ;logical sector# to read

.8:0474   20 B5 05   JSR $05B5   ;read sector to $1B0~1FF, $700~7EF (320 GCR bytes, no decoding)

;synchronize drive with C64
.8:0477   A9 00      LDA #$00    ;allow CLK and DATA high
.8:0479   8D 00 18   STA $1800   ;update serial lines
.8:047c   EA         NOP         ;delay 4 cycles
.8:047d   EA         NOP
.8:047e   A9 08      LDA #$08    ;pull CLK low, allow DATA high
.8:0480   8D 00 18   STA $1800   ;update serial lines
.8:0483   24 80      BIT $80     ;delay 3 cycles
.8:0485   A9 00      LDA #$00    ;allow CLK and DATA high
.8:0487   8D 00 18   STA $1800   ;update serial lines
.8:048a   EA         NOP         ;delay 2 cycles
.8:048b   A9 08      LDA #$08    ;pull CLK low, allow DATA high
.8:048d   8D 00 18   STA $1800   ;update serial lines
.8:0490   A0 4F      LDY #$4F    ;80 loops
;loop to send bytes
.8:0492   A9 00      LDA #$00
.8:0494   8D 00 18   STA $1800
.8:0497   BE B0 01   LDX $01B0,Y ;byte 1 low 2 bits
.8:049a   8E 00 18   STX $1800   ;update serial lines
.8:049d   BD 00 03   LDA $0300,X ;byte 1 bits 2~5 (4 bits)
.8:04a0   8D 00 18   STA $1800   ;update serial lines
.8:04a3   0A         ASL A       ;next two bits
.8:04a4   29 0F      AND #$0F    ;isolate serial line bits
.8:04a6   8D 00 18   STA $1800   ;update serial lines
.8:04a9   BE 00 07   LDX $0700,Y ;bit 1 high 2 bits
.8:04ac   8E 00 18   STX $1800   ;update serial lines
.8:04af   BD 00 03   LDA $0300,X ;byte 2 bits 0~3 (4 bits)
.8:04b2   8D 00 18   STA $1800   ;update serial lines
.8:04b5   0A         ASL A       ;next two bits
.8:04b6   29 0F      AND #$0F    ;isolate serial line bits
.8:04b8   8D 00 18   STA $1800   ;update serial lines
.8:04bb   BE 50 07   LDX $0750,Y ;byte 2 bits 5,6
.8:04be   8E 00 18   STX $1800   ;update serial lines
.8:04c1   BD 00 03   LDA $0300,X ;byte 2 bits 6,7 and byte 3 bits 0,1
.8:04c4   8D 00 18   STA $1800   ;update serial lines
.8:04c7   0A         ASL A       ;next two bits
.8:04c8   29 0F      AND #$0F    ;isolate serial line bits
.8:04ca   8D 00 18   STA $1800   ;update serial lines
.8:04cd   BE A0 07   LDX $07A0,Y ;byte 3 bits 2,3
.8:04d0   8E 00 18   STX $1800   ;update serial lines
.8:04d3   BD 00 03   LDA $0300,X ;byte 3 bits 4~7 (4 bits)
.8:04d6   8D 00 18   STA $1800   ;update serial lines
.8:04d9   0A         ASL A       ;next two bits
.8:04da   29 0F      AND #$0F    ;isolate serial lines
.8:04dc   8D 00 18   STA $1800   ;update serial lines
.8:04df   A9 08      LDA #$08    ;pull CLK low, allow DATA high
.8:04e1   8D 00 18   STA $1800   ;update serial lines
.8:04e4   88         DEY         ;countdown byte triples, all done?
.8:04e5   10 AB      BPL $0492   ;no, loop to send bytes

.8:04e7   A5 8D      LDA $8D     ;physical sector#
.8:04e9   18         CLC
.8:04ea   69 02      ADC #$02    ;add constant two
.8:04ec   C5 15      CMP $15     ;greater/equal to #sectors per track?
.8:04ee   90 06      BCC $04F6   ;no skip ahead
.8:04f0   E5 15      SBC $15     ;subtract #sectors per track
.8:04f2   45 8C      EOR $8C     ;apply correction, all sectors in track done?
.8:04f4   F0 0C      BEQ $0502   ;yes, next track
;resume after next track
.8:04f6   85 8D      STA $8D     ;update physical sector#
.8:04f8   C6 8B      DEC $8B     ;countdown # sectors in file, all done?
.8:04fa   F0 03      BEQ $04FF   ;yes, return to main command loop
.8:04fc   4C 66 04   JMP $0466   ;no, loop for sectors 
.8:04ff   4C 03 04   JMP $0403   ;go to main command loop
;next track
.8:0502   A2 02      LDX #$02    ;2 half-track steps
;move head loop
.8:0504   AD 00 1C   LDA $1C00   ;this is a convoluted way
.8:0507   38         SEC         ;to add 1 to the stepper motor
.8:0508   2A         ROL A
.8:0509   29 03      AND #$03    ;isolate scrambled stepper value
.8:050b   4D 00 1C   EOR $1C00   ;unscramble stepper bits and merge non-stepper bits
.8:050e   8D 00 1C   STA $1C00   ;set new head position
.8:0511   A0 0A      LDY #$0A    ;setup delay for about 10,000 cycles
;outer delay
.8:0513   A9 C7      LDA #$C7
.8:0515   38         SEC
;inner delay
.8:0516   E9 01      SBC #$01    ;subtract 1, inner delay done?
.8:0518   B0 FC      BCS $0516   ;no, loop to inner delay
.8:051a   88         DEY         ;subtract 1, outer delay done?
.8:051b   D0 F6      BNE $0513   ;no, loop to outer delay
.8:051d   CA         DEX         ;countdown half-track steps, done?
.8:051e   D0 E4      BNE $0504   ;no, move head loop

.8:0520   E6 0E      INC $0E     ;next (greater) track#
.8:0522   A5 0E      LDA $0E     ;get new track#
.8:0524   C9 12      CMP #$12    ;is it directory track 18?
.8:0526   F0 DA      BEQ $0502   ;yes, next track
.8:0528   E6 22      INC $22     ;next track# for look-up table
.8:052a   20 31 05   JSR $0531   ;set bit-rate, sectors/track, and sector interleave
.8:052d   A9 00      LDA #$00    ;next sector#
.8:052f   F0 C5      BEQ $04F6   ;always, resume after next track

Wow, that's quite a lot! Let's break it down. First (and strangely) it uses ROM to read the requested track and sector, but that data is never used! As far as I can tell, it's just a cheap way (fewest code bytes) to get the ROM to move the disk head to the desired track. Then that code sets the head bit-rate, # sectors per track, and sector interleave according to a table:

TrackBit Rate# sectors
per track
sector
interleave
1%11
fastest
210
2%11214
3%11218
4%112112
5%112116
6%112120
7%11213
8%11217
9%112111
10%112115
11%112119
12%11212
13%11216
14%112110
15%112114
16%112118
17%11211
18%10196
19%101910
20%101914
21%101918
22%10193
23%10197
24%101911
25%011816
26%01182
27%01186
28%011810
29%011814
30%01180
31%00174
32%00178
33%001712
34%001716
35%00
slowest
173

Next the code calculates the next logical sector# (sector# stored in sector header) based on the current physical sector# and the sector interleave for the track. A variety of sector interleaves are used as (presumably) a form of data obfusication.

The code then calls $525 to read in the raw GCR bytes into two buffers (320 bytes total). This scheme uses a custom sector header with a 3-byte gap before the data. Interestingly, the routine doesn't do any GCR decoding before returning to the main loader.

Next the code toggles the CLK line twice with a decreasing number of cycles between each transition. This allows the C64 to "zero in" or "lock on" to the timing of the disk drive. They need to be aligned pretty close because the following code uses minimal synchronization.

The bulk of the code decodes the GCR data and transmits it to the C64... at the same time! It uses a custom 4-byte to 3-byte (32-bit to 24-bit) decoding scheme. This has a 33% overhead which is significantly more than the 25% overhead of standard GCR coding. (However, it's not as wasteful as some schemes which have a 50% overhead.)

The codes sends a whopping 240 bytes with only minimal synchronization (occassional 1-cycle delay)! This is done by 80 loops of 3 bytes each. Since there is no separate routine for decoding, and the byte transmission is done in bulk, this loader is extremely fast. (Unfortunately the screen must be blanked.)

After transmitting the data, the code calculates the next physical sector# using a constant interleave value of 2. If this results in sector zero, the head is moved to the next greater track. Either way, the # sectors requested is decremented and the code loops if sectors remain (if none remain, it jumps back to the command loop at $403).

In case it isn't obvious, the code does not perform any type of data validation! If the data is corrupt due to a bad read, it will be transmitted to the C64 anyway. The poor C64 (and ultimately the user) will have to deal with the consequences... Anyway, let's look at how the NTSC C64 handles the data stream:

.C:bf41   A9 07      LDA #$07    ;allow CLK and DATA high (VIC Bank 0, UserPort bit 1)
.C:bf43   8D 00 DD   STA $DD00   ;update serial lines
;loop for sectors
.C:bf46   2C 00 DD   BIT $DD00   ;is CLK low?
.C:bf49   50 FB      BVC $BF46   ;yes, wait
.C:bf4b   2C 00 DD   BIT $DD00   ;is CLK low?
.C:bf4e   50 03      BVC $BF53   ;yes, skip ahead
.C:bf50   24 80      BIT $80     ;no, delay 5 cycles
.C:bf52   EA         NOP
.C:bf53   2C 00 DD   BIT $DD00   ;is CLK high?
.C:bf56   70 02      BVS $BF5A   ;yes, skip ahead
.C:bf58   24 80      BIT $80     ;no, delay 3 cycles
.C:bf5a   2C 00 DD   BIT $DD00   ;is CLK high?
.C:bf5d   70 00      BVS $BF5F   ;yes, delay 1 cycle
.C:bf5f   EA         NOP         ;delay 12 cycles
.C:bf60   EA         NOP
.C:bf61   EA         NOP
.C:bf62   EA         NOP
.C:bf63   EA         NOP
.C:bf64   EA         NOP
.C:bf65   A0 4F      LDY #$4F    ;80 passes (for 240 bytes)
;loop for bytes (cycle times in [brackets], avg. 115.5 per pass)
.C:bf67   AD 00 DD   LDA $DD00   ;[4]get 2 bits
.C:bf6a   4A         LSR A       ;[2]shift down two bits
.C:bf6b   4A         LSR A       ;[2]
.C:bf6c   4D 00 DD   EOR $DD00   ;[4]merge 2 bits
.C:bf6f   4A         LSR A       ;[2]shift down two bits
.C:bf70   4A         LSR A       ;[2]
.C:bf71   4D 00 DD   EOR $DD00   ;[4]merge 2 bits
.C:bf74   4A         LSR A       ;[2]shift down two bits
.C:bf75   4A         LSR A       ;[2]
.C:bf76   4D 00 DD   EOR $DD00   ;[4]merge 2 bits
.C:bf79   48         PHA         ;[3]save byte
.C:bf7a   EA         NOP         ;[2]
.C:bf7b   AD 00 DD   LDA $DD00   ;[4]get 2 bits
.C:bf7e   4A         LSR A       ;[2]shift down two bits
.C:bf7f   4A         LSR A       ;[2]
.C:bf80   4D 00 DD   EOR $DD00   ;[4]merge 2 bits
.C:bf83   4A         LSR A       ;[2]shift down two bits
.C:bf84   4A         LSR A       ;[2]
.C:bf85   4D 00 DD   EOR $DD00   ;[4]merge 2 bits
.C:bf88   4A         LSR A       ;[2]shift down two bits
.C:bf89   4A         LSR A       ;[2]
.C:bf8a   4D 00 DD   EOR $DD00   ;[4]merge 2 bits
.C:bf8d   48         PHA         ;[3]save byte
.C:bf8e   EA         NOP         ;[2]
.C:bf8f   AD 00 DD   LDA $DD00   ;[4]get 2 bits
.C:bf92   4A         LSR A       ;[2]shift down two bits
.C:bf93   4A         LSR A       ;[2]
.C:bf94   4D 00 DD   EOR $DD00   ;[4]merge 2 bits
.C:bf97   4A         LSR A       ;[2]shift down two bits
.C:bf98   4A         LSR A       ;[2]
.C:bf99   4D 00 DD   EOR $DD00   ;[4]merge 2 bits
.C:bf9c   4A         LSR A       ;[2]shift down two bits
.C:bf9d   4A         LSR A       ;[2]
.C:bf9e   4D 00 DD   EOR $DD00   ;[4]merge 2 bits
.C:bfa1   48         PHA         ;[3]save byte
.C:bfa2   EA         NOP         ;[2]
.C:bfa3   24 80      BIT $80     ;[3]delay
.C:bfa5   2C 00 DD   BIT $DD00   ;[4]is CLK low?
.C:bfa8   50 00      BVC $BFAA   ;[2.5] yes, delay 1 cycle
.C:bfaa   EA         NOP         ;[2]
.C:bfab   88         DEY         ;[2]coundown passes, all done?
.C:bfac   10 B9      BPL $BF67   ;[3]no, loop for bytes

.C:bfae   A5 01      LDA $01     ;get memory config
.C:bfb0   8D 7C BE   STA $BE7C   ;save it
.C:bfb3   29 FC      AND #$FC    ;all RAM
.C:bfb5   85 01      STA $01     ;update memory configuration
.C:bfb7   A0 F0      LDY #$F0    ;#bytes to write (240)
;loop to store bytes
.C:bfb9   68         PLA         ;get data
.C:bfba   49 F9      EOR #$F9    ;unscramble (due to hardware inversion and VIC Bank and UserPort bits)
.C:bfbc   88         DEY         ;index and countdown, all done?
.C:bfbd   91 FE      STA ($FE),Y ;(store data)
.C:bfbf   D0 F8      BNE $BFB9   ;no, loop to store bytes

.C:bfc1   AD 7C BE   LDA $BE7C   ;orig. mem. config.
.C:bfc4   85 01      STA $01     ;restore memory configuration
.C:bfc6   A5 FE      LDA $FE     ;load address low
.C:bfc8   18         CLC
.C:bfc9   69 F0      ADC #$F0    ;add 240
.C:bfcb   85 FE      STA $FE     ;update low, any carry?
.C:bfcd   90 02      BCC $BFD1   ;no, countdown sectors
.C:bfcf   E6 FF      INC $FF     ;update address high
;countdown sectors
.C:bfd1   CE 78 BE   DEC $BE78   ;countdown, all done?
.C:bfd4   F0 03      BEQ $BFD9   ;yes, finish up
.C:bfd6   4C 46 BF   JMP $BF46   ;no, loop for sectors
;finish up
.C:bfd9   AD 7A BE   LDA $BE7A   ;orig $DD00 value
.C:bfdc   8D 00 DD   STA $DD00
.C:bfdf   AD 79 BE   LDA $BE79   ;orig $D015 value
.C:bfe2   8D 15 D0   STA $D015
.C:bfe5   AD 7B BE   LDA $BE7B   ;orig $D011 value
.C:bfe8   29 7F      AND #$7F    ;clear raster bit 8
.C:bfea   4C 70 BD   JMP $BD70   ;restore VIC $D011 and exit

Since we know what the drive is doing, the C64 code above shouldn't be too hard to understand. It begins by setting the serial lines, VIC Bank, and UserPort bit to a known state. Next it repeatedly checks the CLK line on the serial bus, narrowing the gap/delay between attempts to get the computer in-sync with the drive.

Next it fast-loads 240 bytes of data, pushing each byte onto the stack. This means only 16 stack bytes are allowed for the CPU/program to use. This process takes a 'mere' 9239 cycles on average. This is noticibly less than a full VIC screen (even on the shorter NTSC).

Next it pulls the data off the stack and stores it in the appropriate RAM location. The time needed for this is not counted towards the loading speed because it occurs in parallel with the drive loading the next sector. In other words, by the time the drive has a new sector ready, this 'unloading' has already finished.

Note there is no provision for the number of bytes in the last sector. It always loads data in chunks of 240 bytes. This would be a problem for a general-purpose loader. Anyway, after all sectors are loaded, it restores hardware registers and exits.

  Summary 
  • Blank screen: yes (fast load)
  • Interrupts allowed: no (fast load)
  • Disk Header: standard
  • Directory structure: not used
  • Allow wildcard in filename: n/a
  • File structure: custom (no load address nor final #bytes)
  • Sector structure: custom (240 data bytes)
  • Sector decoding time: 0.0 milliseconds
  • Head stepping speed: slow (about 10.0 milliseconds/half-track)
  • Disk → C64 transfer: fast (about 39 microseconds/byte)
  • C64 → Disk transfer: slow (about 1050 microseconds/byte)
  • C64 memory footprint: under 0.75K ($BD70~BFFF)
  • Needs KERNAL: no
  • Load $D000~DFFF: RAM
  • Alters User Port: maybe (uses default value)
  • Requires Unit 8: yes
  • Write file/sector: yes
  • Other: does not check data for errors, loads in multiples of 240 bytes, interrupts allowed and screen enabled during sector read/write

© Hydrophilic.net, 2026