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

  GEOS Fast-load & Fast-save 

This fast-loader (and fast-saver) was used by Berkley Softwork's GEOS (at least v1.5). The copy protection is in the boot loader. The boot-loader code is very similar to the "main" fast-load routines. I'm only going to document the "main" routines for clarity and brevity.

Before a disk-command is issued, GEOS will call the following routine to prepare the C64 for disk operations.

.C:9048   08         PHP         ;save CPU status on stack
.C:9049   68         PLA         ;get CPU status into .A
.C:904a   8D 33 9D   STA $9D33   ;save for exit
.C:904d   78         SEI         ;disable interrupts
.C:904e   A5 01      LDA $01     ;get memory configuration
.C:9050   8D 35 9D   STA $9D35   ;save for exit
.C:9053   A9 36      LDA #$36    ;I/O and KERNAL ROM
.C:9055   85 01      STA $01     ;set memory configuration
.C:9057   AD 1A D0   LDA $D01A   ;get VIC IRQ enables
.C:905a   8D 34 9D   STA $9D34   ;save for exit
.C:905d   A0 00      LDY #$00   
.C:905f   8C 1A D0   STY $D01A   ;disable all VIC IRQ sources
.C:9062   A9 7F      LDA #$7F    ;multi-use constant
.C:9064   8D 19 D0   STA $D019   ;clear any pending VIC IRQs
.C:9067   8D 0D DC   STA $DC0D   ;disable all CIA1 interrupt sources
.C:906a   8D 0D DD   STA $DD0D   ;disable all CIA2 interrupt sources
.C:906d   A9 90      LDA #$90    ;set KERNAL IRQ vector to $90C4
.C:906f   8D 15 03   STA $0315
.C:9072   A9 C4      LDA #$C4
.C:9074   8D 14 03   STA $0314
.C:9077   A9 90      LDA #$90    ;set KERNAL NMI vector to $90C9
.C:9079   8D 19 03   STA $0319
.C:907c   A9 C9      LDA #$C9
.C:907e   8D 18 03   STA $0318
.C:9081   A9 3F      LDA #$3F    ;standard port direction
.C:9083   8D 02 DD   STA $DD02   ;for serial bus, UserPort bit, and VIC Bank bits
.C:9086   AD 15 D0   LDA $D015   ;sprite enables
.C:9089   8D 36 9D   STA $9D36   ;save for exit
.C:908c   8C 15 D0   STY $D015   ;disable all sprites
.C:908f   8C 05 DD   STY $DD05   ;zero CIA2 TimerA high
.C:9092   C8         INY         ;1
.C:9093   8C 04 DD   STY $DD04   ;set CIA2 TimerA low
.C:9096   A9 81      LDA #$81    ;enable TimerA interrupt (NMI)
.C:9098   8D 0D DD   STA $DD0D
.C:909b   A9 09      LDA #$09    ;start timer, one-shot mode
.C:909d   8D 0E DD   STA $DD0E   ;set TimerA control
.C:90a0   A0 2C      LDY #$2C    ;44 rasters
;loop for next raster
.C:90a2   AD 12 D0   LDA $D012   ;get current raster
.C:90a5   C5 8F      CMP $8F     ;same as before?
.C:90a7   F0 F9      BEQ $90A2   ;yes, loop for next raster
.C:90a9   85 8F      STA $8F     ;temp = current raster
.C:90ab   88         DEY         ;countdown, all rasters checked?
.C:90ac   D0 F4      BNE $90A2   ;no, loop for next raster

.C:90ae   AD 00 DD   LDA $DD00   ;serial lines and more
.C:90b1   29 07      AND #$07    ;mask UserPort bit and VIC Bank bits -- allow CLK and DATA to go high
.C:90b3   85 8E      STA $8E     ;save for quick access
.C:90b5   8D 3C 9D   STA $9D3C   ;save for slow access (really)
.C:90b8   09 30      ORA #$30    ;value to pull CLK and DATA low
.C:90ba   85 8F      STA $8F     ;save in ZP for quick access
.C:90bc   A5 8E      LDA $8E     ;value for CLK and DATA high
.C:90be   09 10      ORA #$10    ;value for CLK low
.C:90c0   8D 3D 9D   STA $9D3D   ;save for slow access
.C:90c3   60         RTS

.C:90c4   68         PLA         ;temporary KERNAL IRQ code
.C:90c5   A8         TAY         ;restore Y
.C:90c6   68         PLA
.C:90c7   AA         TAX         ;restore X
.C:90c8   68         PLA         ;restore A
.C:90c9   40         RTI         ;temporary KERNAL NMI code -- return from interrupt

The code first saves the CPU status (including interrupt disables) and then disables IRQs with SEI. Then it saves the current memory configuration and enables I/O registers and KERNAL ROM in the memory configuration. It clears VIC IRQ sources (mask) and then clears any pending interrupts from the VIC and both CIAs. Interestingly it sets KERNAL IRQ and NMI values to dummy code -- in case an interrupt somehow happens (the standard serial ROM routines generally end by enabling interrupts). Actually, it sets CIA2 to generate an NMI via TimerA but the dummy code doesn't clear it, so no NMIs can happen later. While setting TimerA, it also disables all sprites.

Next the code waits for 44 rasters to pass. This allows any pending sprites to finish rendering (including Y-expanded ones). Finally it calculates a few values to use with the serial port. Note the code at $90CA (not shown) will restore the memory configuration, hardware registers, and CPU status (i.e., it will re-enable interrupts if they were active before).

GOES has a routine (named EnterTurbo) which initializes the drive's RAM with custom code. Let's look at the core part of this routine:

.C:943b   20 5C C2   JSR $C25C   ;call $9048 (see above) to prepare for I/O 
.C:943e   A9 96      LDA #$96    ;set pointer $8D~8E to $9609 (source)
.C:9440   85 8E      STA $8E
.C:9442   A9 09      LDA #$09
.C:9444   85 8D      STA $8D
.C:9446   A9 03      LDA #$03    ;initial pointer $94A5~94A6 to $300 (destination) 
.C:9448   8D A6 94   STA $94A6
.C:944b   A9 00      LDA #$00
.C:944d   8D A5 94   STA $94A5
.C:9450   A9 1A      LDA #$1A    ;27 chunks of 32 bytes (total 864 bytes)
.C:9452   85 8F      STA $8F     ;save counter
;loop to transmit chunks
.C:9454   20 7A 94   JSR $947A   ;send M-W command to drive (using standard/slow transfer)
.C:9457   8A         TXA         ;test result, okay?
.C:9458   D0 1D      BNE $9477   ;no, error exit
.C:945a   18         CLC
.C:945b   A9 20      LDA #$20    ;add 32
.C:945d   65 8D      ADC $8D     ;to source low
.C:945f   85 8D      STA $8D
.C:9461   90 02      BCC $9465
.C:9463   E6 8E      INC $8E     ;carry to sourch high
.C:9465   18         CLC
.C:9466   A9 20      LDA #$20    ;add 32
.C:9468   6D A5 94   ADC $94A5   ;to destination low
.C:946b   8D A5 94   STA $94A5
.C:946e   90 03      BCC $9473
.C:9470   EE A6 94   INC $94A6   ;carry to destination high
.C:9473   C6 8F      DEC $8F     ;countdown chunks, all done?
.C:9475   10 DD      BPL $9454   ;no, loop to transmit chunks
.C:9477   4C 5F C2   JMP $C25F   ;done with I/O, call $90CA (see above)

That code transfers 864 bytes of code ($9609~9968) from the C64 to the C1541 ($300~65F) which takes about 1.5 seconds. Thankfully this is only done once -- when the device is first accessed (early in the boot process). Immediately after the code is written to the drive, a Memory-Execute (M-E) command is sent to the drive to run code at $3DC. Let's take a look:

.8:03dc   08         PHP         ;save interrupt status
.8:03dd   78         SEI         ;disable interrupts
.8:03de   A5 49      LDA $49     ;temporary stack pointer
.8:03e0   48         PHA         ;save on stack
.8:03e1   A0 00      LDY #$00    ;256 loops to delay (256*5 = 1280 cycles)
.8:03e3   88         DEY         ;delay loop
.8:03e4   D0 FD      BNE $03E3
.8:03e6   20 B8 03   JSR $03B8   ;pull DATA low, allow CLK high
.8:03e9   A9 04      LDA #$04    ;CLK input bit
;wait for CLK low
.8:03eb   2C 00 18   BIT $1800   ;is CLK high?
.8:03ee   F0 FB      BEQ $03EB   ;yes, wait for CLK low
;main loop
.8:03f0   20 40 04   JSR $0440   ;drive LED off
.8:03f3   A9 06      LDA #$06    ;set pointer $73~74 to $64A (for code vector)
.8:03f5   85 74      STA $74
.8:03f7   A9 4A      LDA #$4A
.8:03f9   85 73      STA $73
.8:03fb   20 75 03   JSR $0375   ;read a byte-count and then that many bytes to $64A+
.8:03fe   20 44 04   JSR $0444   ;drive LED on
.8:0401   A9 07      LDA #$07    ;set pointer $73~74 to $700
.8:0403   85 74      STA $74
.8:0405   A9 00      LDA #$00
.8:0407   85 73      STA $73
.8:0409   A9 03      LDA #$03    ;push return 'main loop' address $3F0 onto stack 
.8:040b   48         PHA
.8:040c   A9 EF      LDA #$EF
.8:040e   48         PHA
.8:040f   6C 4A 06   JMP ($064A) ;execute command; usually $64C has track#, $64D has sector#

.8:0412   20 8F F9   JSR $F98F   ;drive motor off
.8:0415   A9 00      LDA #$00    ;allow CLK and DATA to go high
.8:0417   8D 00 18   STA $1800   ;update serial bus
.8:041a   85 33      STA $33     ;clear high-byte for controller
.8:041c   A9 EC      LDA #$EC    ;turn off byte-ready request
.8:041e   8D 0C 1C   STA $1C0C
.8:0421   68         PLA         ;discard $3F0 address
.8:0422   68         PLA
.8:0423   68         PLA         ;recall
.8:0424   85 49      STA $49     ;temporary stack pointer
.8:0426   28         PLP         ;restore interrupt status
.8:0427   60         RTS         ;exit

That code basically pulls the serial DATA-line low and waits for the serial CLK-line to go low, turns off the drive LED, and waits for a command. Every command begins with a byte count and an execution address. Most commands also include a track# and sector#.

Once a command is received, the drive turns on its LED and executes the requested code. When the requested code exits with RTS, control will return to the main loop ($3F0). The code at $412 is executed to 'uninstall' the fast code; this happens when you switch drives on the GEOS desktop.

Now let's look at how GEOS sends data (such as a command) to the drive:

.C:919c   20 85 95   JSR $9585   ;wait for drive
~
.C:9585   78         SEI         ;disable interrupts
.C:9586   A5 8E      LDA $8E
.C:9588   8D 00 DD   STA $DD00   ;allow CLK and DATA high
;wait for drive
.C:958b   2C 00 DD   BIT $DD00   ;is DATA low?
.C:958e   10 FB      BPL $958B   ;yes, wait for drive
.C:9590   60         RTS
~
.C:919f   98         TYA         ;# bytes to send
.C:91a0   48         PHA         ;save count
.C:91a1   A0 00      LDY #$00    ;flag end of data
.C:91a3   20 B3 91   JSR $91B3   ;call ourself to send 1 byte (the count)
.C:91a6   68         PLA         ;# bytes to send
.C:91a7   A8         TAY         ;set counter/index
.C:91a8   20 85 95   JSR $9585   ;wait for drive (see above)
;loop to send bytes (cycle counts in [brackets])
.C:91ab   88         DEY         ;[2]decrement count/index
.C:91ac   B1 8B      LDA ($8B),Y ;[5.5]read data
.C:91ae   A6 8E      LDX $8E     ;[3]allow CLK and DATA high
.C:91b0   8E 00 DD   STX $DD00   ;[4]update serial lines
;(called with Y=0 to send one byte)
.C:91b3   AA         TAX         ;[2]save data byte
.C:91b4   29 0F      AND #$0F    ;[2]mask low nibble
.C:91b6   85 8D      STA $8D     ;[3]save for later
.C:91b8   38         SEC         ;[2]prepare to subtract
;loop for VIC
.C:91b9   AD 12 D0   LDA $D012   ;[4]get VIC raster#
.C:91bc   E9 31      SBC #$31    ;[2]calc offset from border, are we in border?
.C:91be   90 04      BCC $91C4   ;[2]yes, skip ahead
.C:91c0   29 06      AND #$06    ;[2]no, are we near bad-line?
.C:91c2   F0 F5      BEQ $91B9   ;[2]yes, loop for VIC
.C:91c4   8A         TXA         ;[2]get data byte
.C:91c5   A6 8F      LDX $8F     ;[3]pull CLK and DATA low
.C:91c7   8E 00 DD   STX $DD00   ;[4]update serial lines
.C:91ca   29 F0      AND #$F0    ;[2]isolate high nibble
.C:91cc   05 8E      ORA $8E     ;[3]merge UserPort bit and VIC Bank bits
.C:91ce   8D 00 DD   STA $DD00   ;[4]update serial lines (and UserPort,VIC)
.C:91d1   6A         ROR A       ;[2]next two bits
.C:91d2   6A         ROR A       ;[2]
.C:91d3   29 F0      AND #$F0    ;[2]isolate CLK and DATA (should be AND #$30)
.C:91d5   0D 3C 9D   ORA $9D3C   ;[4]merge UserPort bit and VIC Bank bits
.C:91d8   8D 00 DD   STA $DD00   ;[4]update serial lines
.C:91db   A6 8D      LDX $8D     ;[3]get low nibble
.C:91dd   BD 3D 91   LDA $913D,X ;[4]look-up encoding value
.C:91e0   05 8E      ORA $8E     ;[3]merge UserPort bit and VIC Bank bits 
.C:91e2   8D 00 DD   STA $DD00   ;[4]update serial lines
.C:91e5   6A         ROR A       ;[2]next two bits
.C:91e6   6A         ROR A       ;[2]
.C:91e7   29 F0      AND #$F0    ;[2]isolate CLK and DATA (should be AND #$30)
.C:91e9   05 8E      ORA $8E     ;[3]merge UserPort bit and VIC Bank bits 
.C:91eb   C0 00      CPY #$00    ;[2]test count/index, all done?
.C:91ed   8D 00 DD   STA $DD00   ;[4](update serial lines)
.C:91f0   D0 B9      BNE $91AB   ;[3]no, loop to send bytes
.C:91f2   EA         NOP
.C:91f3   EA         NOP
.C:91f4   F0 9F      BEQ $9195   ;yes, exit
~
.C:9195   AE 3D 9D   LDX $9D3D   ;CLK low, DATA high
.C:9198   8E 00 DD   STX $DD00   ;update serial bus
.C:919b   60         RTS
;encoding values for low nibble
>C:913d  00 80 20 a0  40 c0 60 e0   .. .@.`.
>C:9145  10 90 30 b0  50 d0 70 f0   ..0.P.p.

The code begins by waiting for the drive then calling (part of) itself to send a single byte (the # bytes to be transmitted next). Next it enters a loop to fast-send the data bytes. A byte transmission takes an average of 99.5 cycles, assuming the VIC is not in the border nor near a bad-line. The routine ends by pulling CLK low and allowing DATA high. I should mention if the byte count is zero, the code will send 256 bytes.

Now let's see what the drive does when GEOS wants to a read a sector:

.8:058e   20 6B 04   JSR $046B   ;drive motor on, move head to desired track# ($64C)
.8:0591   A9 00      LDA #$00    ;read code
.8:0593   A6 00      LDX $00     ;1
.8:0595   CA         DEX         ;0
.8:0596   F0 03      BEQ $059B   ;always
.8:0598   60         RTS
~
.8:059b   85 45      STA $45     ;save command code bits
.8:059d   A9 06      LDA #$06    ;set pointer $32~33 to $64C (track# followed by sector#)
.8:059f   85 33      STA $33
.8:05a1   A9 4C      LDA #$4C
.8:05a3   85 32      STA $32
.8:05a5   A9 07      LDA #$07    ;set pointer $30~31 to $700
.8:05a7   85 31      STA $31
.8:05a9   BA         TSX         ;current stack pointer
.8:05aa   86 49      STX $49     ;temp stack pointer
.8:05ac   A2 01      LDX #$01    ;okay code
.8:05ae   86 00      STX $00     ;save for buffer zero
.8:05b0   CA         DEX         ;zero
.8:05b1   86 3F      STX $3F     ;current buffer# (result code to $00)
.8:05b3   A9 EE      LDA #$EE
.8:05b5   8D 0C 1C   STA $1C0C   ;request byte-ready signal
.8:05b8   A5 45      LDA $45     ;get command code
.8:05ba   C9 10      CMP #$10    ;is it write sector?
.8:05bc   F0 0A      BEQ $05C8   ;yes, do it
.8:05be   C9 30      CMP #$30    ;is it look for sector header?
.8:05c0   F0 03      BEQ $05C5   ;yes, do it
.8:05c2   4C CA F4   JMP $F4CA   ;no, read sector

The code starts by turning on the drive motor and moving the head (if needed) to the desired track# (stored at $64C from the command). For some reason (code obfusication?) the code is using controller address $00 for the job code being executed, which is normally for the buffer at $300. Instead that code sets the memory pointer ($30~31) to the buffer at $700. The important thing is that the code ends by calling the ROM to read the sector. That is, GEOS uses a standard sector layout and standard/slow GCR decoding.

Before looking at how the computer/GEOS receives the data from the drive, let's take a look at the head-stepping routine.

.8:0497   48         PHA         ;save .A on stack
.8:0498   A5 22      LDA $22     ;current track#
.8:049a   A2 FF      LDX #$FF    ;-1 (assume step-out = smaller track#)
.8:049c   38         SEC
.8:049d   ED 4C 06   SBC $064C   ;subtract desired track#, is difference zero?
.8:04a0   F0 13      BEQ $04B5   ;yes, exit -- else is differnce positive?
.8:04a2   B0 06      BCS $04AA   ;yes, move head
.8:04a4   49 FF      EOR #$FF    ;no, negate part 1
.8:04a6   69 01      ADC #$01    ;negate part 2
.8:04a8   A2 01      LDX #$01    ;+1 (step-in = greater track#)
;move head (.A contains the absolute value of the difference between current and desired track)
.8:04aa   20 BC 04   JSR $04BC   ;move head
.8:04ad   AD 4C 06   LDA $064C   ;desired track# is now
.8:04b0   85 22      STA $22     ;current track#
.8:04b2   20 40 05   JSR $0540   ;set #sectors per track and bit-rate
.8:04b5   68         PLA         ;restore .A from stack
.8:04b6   60         RTS
;move head
.8:04bc   86 4A      STX $4A     ;save delta (plus or minus one)
.8:04be   0A         ASL A       ;accumulator times two
.8:04bf   A8         TAY         ;counter = # half-track steps
.8:04c0   AD 00 1C   LDA $1C00   ;stepper bits and other stuff
.8:04c3   29 FE      AND #$FE    ;keep top stepper bit and other stuff
.8:04c5   85 70      STA $70     ;shadow stepper bits and other stuff
.8:04c7   A9 1E      LDA #$1E    ;initial delay value (about 7.5 milliseconds)
.8:04c9   85 71      STA $71     ;save timing value
;loop to move head
.8:04cb   A5 70      LDA $70     ;shadow stepper bits and other stuff
.8:04cd   18         CLC
.8:04ce   65 4A      ADC $4A     ;add delta
.8:04d0   45 70      EOR $70     ;scramble stepper bits and other stuff
.8:04d2   29 03      AND #$03    ;isolate scrambled stepper bits
.8:04d4   45 70      EOR $70     ;unscramble stepper bits, merge other stuff
.8:04d6   85 70      STA $70     ;update shadow
.8:04d8   8D 00 1C   STA $1C00   ;update stepper bits (other stuff unchanged)
.8:04db   A5 71      LDA $71     ;get delay value
.8:04dd   20 FB 04   JSR $04FB   ;delay via hardware timer
.8:04e0   A5 71      LDA $71     ;get delay value
.8:04e2   C0 05      CPY #$05    ;remaining half-steps >= 5 ?
.8:04e4   90 08      BCC $04EE   ;no, apply breaking
.8:04e6   C9 11      CMP #$11    ;is delay value >= 17 ?
.8:04e8   90 0A      BCC $04F4   ;no (max speed = min delay of 16 → 4.0 milliseconds) so update delay
.8:04ea   E9 02      SBC #$02    ;accelerate (faster speed, shorter delay)
.8:04ec   D0 06      BNE $04F4   ;always, update delay
;apply breaking
.8:04ee   C9 1C      CMP #$1C    ;is delay value >= 28
.8:04f0   B0 02      BCS $04F4   ;yes (min speed = max delay of 28 → 7.0 milliseconds) so update delay
.8:04f2   69 04      ADC #$04    ;slow down (slower speed, longer delay)
;update delay
.8:04f4   85 71      STA $71
.8:04f6   88         DEY         ;test # half-track steps, all done?
.8:04f7   D0 D2      BNE $04CB   ;no, loop to move head
.8:04f9   A9 4B      LDA #$4B    ;head-settling time (about 19.0 milliseconds)
;delay via hardware timer
.8:04fb   8D 05 18   STA $1805   ;timer high-byte
;wait for timer
.8:04fe   AD 05 18   LDA $1805   ;is timer down to zero?
.8:0501   D0 FB      BNE $04FE   ;no, wait for timer
.8:0503   60         RTS

GEOS always impressed me with its fast head movement. The code starts by calculating two values: the absolute value between the current track and target track, and a delta value (plus or minus one) that depends on which way the head needs to move (either inward toward the hole or outward towards the rim of the disk). Next it calls the 'move head' routine and then finishes by updating the current track# and setting some variables (number of sectors per track, and disk head's data bit-rate).

The magic happens in 'move head' where it first calculates the number of half-track steps (the track difference times two), initializes a delay value, and sets up a 'shadow register' of $1C00 (which contains the stepper bits and other things). Next it enters a loop which updates both the stepper motor and the delay value. If the head neads to move more than 2.5 tracks, the code will accelarate the stepping speed (decrease the timing delay) until a maximum speed (minimum delay) is reached. If the head is closer (less than 2.5 tracks) then it will apply braking: it will decrease stepping spead (increase the delay) until a minimum speed is reached.

This is neccessary (at least at these 'high' speeds) because the disk-head has inertia: it can't instantly go from a stopped state to maximum speed. If you try to move the head too fast, the stepper motor will 'jam' (not move at all). Also, at high speed, the head can't instantly stop: it needs to slow down or the head will over-shoot the desired track.

Finally, if the disk head had to move, a rather lengthy delay of about 19 milliseconds is imposed. This is known as a head-settling delay. Most fast-loaders don't use this. I believe GEOS does because you want the head stable before it begins writing data, and the head-movement code doesn't know if data is about to be written. (There's an optimization for you to implement.)

Anyway, let's take a look at how GEOS receives data from the drive:

.C:914d   20 85 95   JSR $9585   ;allow CLK and DATA high, wait for DATA high (see far above)
.C:9150   48         PHA         ;waste 14 cycles
.C:9151   68         PLA
.C:9152   48         PHA
.C:9153   68         PLA
.C:9154   84 8D      STY $8D     ;save byte-count
;loop to get bytes (cycle counts in [brackets])
.C:9156   38         SEC         ;[2]
;wait for VIC
.C:9157   AD 12 D0   LDA $D012   ;[4]get VIC raster#
.C:915a   E9 31      SBC #$31    ;[2]is it in the border?
.C:915c   90 04      BCC $9162   ;[2]yes, skip ahead
.C:915e   29 06      AND #$06    ;[2]is it near a bad-line?
.C:9160   F0 F5      BEQ $9157   ;[2]yes, wait for VIC
.C:9162   A5 8F      LDA $8F     ;[3]value for CLK and DATA low
.C:9164   8D 00 DD   STA $DD00   ;[4]update serial lines
.C:9167   A5 8B      LDA $8B     ;[3]waste time
.C:9169   A5 8E      LDA $8E     ;[3]allow CLK and DATA high
.C:916b   8D 00 DD   STA $DD00   ;[4]update serial lines
.C:916e   C6 8D      DEC $8D     ;[5]countdown bytes (set index)
.C:9170   EA         NOP         ;[2]
.C:9171   EA         NOP         ;[2]
.C:9172   EA         NOP         ;[2]
.C:9173   AD 00 DD   LDA $DD00   ;[4]get two bits
.C:9176   4A         LSR A       ;[2]shift down two bits
.C:9177   4A         LSR A       ;[2]
.C:9178   EA         NOP         ;[2]
.C:9179   0D 00 DD   ORA $DD00   ;[4]merge two more bits
.C:917c   4A         LSR A       ;[2]shift down four bits
.C:917d   4A         LSR A       ;[2]
.C:917e   4A         LSR A       ;[2]
.C:917f   4A         LSR A       ;[2]
.C:9180   AC 00 DD   LDY $DD00   ;[4]get two bits
.C:9183   AA         TAX         ;[2]low nibble as table index
.C:9184   98         TYA         ;[2]high bits
.C:9185   4A         LSR A       ;[2]shift down two bits
.C:9186   4A         LSR A       ;[2]
.C:9187   0D 00 DD   ORA $DD00   ;[4]merge two more bits
.C:918a   29 F0      AND #$F0    ;[2]isolate high nibble
.C:918c   1D 2E 91   ORA $912E,X ;[4]merge low nibble
.C:918f   A4 8D      LDY $8D     ;[3]get index, is this the last byte?
.C:9191   91 8B      STA ($8B),Y ;[6](save data)
.C:9193   D0 C1      BNE $9156   ;[3]no, loop to get bytes
.C:9195   AE 3D 9D   LDX $9D3D   ;pull CLK low, allow DATA high
.C:9198   8E 00 DD   STX $DD00   ;update serial lines
.C:919b   60         RTS
;decode low nibble table
>C:912e  0f 07 0d 05  0b 03 09 01   ........
>C:9136  0e 06 0c 04  0a 02 08 00   ........

That code is pretty simple (all things considered). First it allows both the CLK and DATA lines to go high and ensures the DATA line actually is high (drive ready). Then it saves the byte-transfer count (in the .Y register) to zero page and enters a loop to read that many bytes from the drive. This code takes 98 cycles per byte assuming the VIC is not in the border nor near a bad-line. This is much faster than the standard ROM transfer, but actually slow compared to other fast-loaders. But most others don't have to deal with saving data -- in fact GEOS might be the fastest at sending data to the drive.

  Summary 
  • Blank screen: no
  • Interrupts allowed: no
  • Disk Header: modified
  • Directory structure: modified
  • Allow wildcard in filename: n/a
  • File structure: custom (typical file contains VLIR table and Info sector)
  • Sector structure: standard (256 data bytes)
  • Sector decoding time: 25.5 milliseconds (slow/ROM)
  • Head stepping speed: fast (4.0 to 7.5 milliseconds/half-track)
  • Disk → C64 transfer: fast (about 98 microseconds/byte)
  • C64 → Disk transfer: fast (about 100 microseconds/byte)
  • C64 memory footprint: about 3.5K ($9000~9D7F)
  • Needs KERNAL: yes
  • Load $D000~DFFF: I/O
  • Alters User Port: no
  • Requires Unit 8: no
  • Write file/sector: yes
  • Other: none

© Hydrophilic.net, 2026