Home
CBM
  ASCII-X
  BASIC
  C128
  D64plus
  Disk
    BAM
    Fast Load
      Access
      Epyx
      GEOS
      Microprose 1
      Microprose 2
      Project Firestart
      Sega
    File
    Sector
    Block
    Directory
    Header
    Physics
    Track
  Escape Codes
  Hardware
  PCxface
  PETSCII
  Pet2asc
Futurama
IBM PC-AT
Contact
Games
Glossary
Hall of fame
Hall of shame
Miscellaneous
Privacy policy
Programming
Twisty puzzles
  Sega Copy-protection Fast-loader 

This fast-loader was used at least in Outrun by Sega. 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. Although the first file on disk is not a BASIC program, it interestingly over-writes the BASIC $302 vector to enable "auto-run". In particular, the file loads at $2DD in RAM and ends at $303; this takes about 1.5 seconds. The vector at $302 (which controls BASIC user input) is set to point to $2DD (which shouldn't be a surprise). Let's look at the code:

.C:02dd   A9 00      LDA #$00    ;multi-use constant
.C:02df   8D 11 D0   STA $D011   ;blank VIC screen
.C:02e2   85 9D      STA $9D     ;turn off KERNAL messages (like LOADING)
.C:02e4   8D 20 D0   STA $D020   ;set border color = black
.C:02e7   A9 08      LDA #$08    ;file 8
.C:02e9   AA         TAX         ;device 8
.C:02ea   A8         TAY         ;channel 8
.C:02eb   20 BA FF   JSR $FFBA   ;KERNAL SetLFS
.C:02ee   A9 01      LDA #$01    ;filename length
.C:02f0   A2 FF      LDX #$FF    ;.YX = $2FF (filename -> "0")
.C:02f2   A0 02      LDY #$02
.C:02f4   20 BD FF   JSR $FFBD   ;KERNAL SetNam
.C:02f7   A9 00      LDA #$00    ;load (not verify)
.C:02f9   20 D5 FF   JSR $FFD5   ;KERNAL Load
.C:02fc   4C 00 C4   JMP $C400   ;run program (install fast-loader)

This simply initializes some VIC registers and loads (standard/slow) the file named "0". That file loads to $C000~C6B9 and takes about 4.5 seconds. Let's look at the initialization routine:

.C:c400   A9 00      LDA #$00    ;address $C500
.C:c402   A2 C5      LDX #$C5
.C:c404   85 FB      STA $FB     ;stored in pointer $FB~FC
.C:c406   86 FC      STX $FC
.C:c408   A9 01      LDA #$01    ;logical file#
.C:c40a   A2 08      LDX #$08    ;device#
.C:c40c   A0 0F      LDY #$0F    ;channel#
.C:c40e   20 BA FF   JSR $FFBA   ;KERNAL SetLFS
.C:c411   A2 A4      LDX #$A4    ;.YX = $C4A4 (points to string "I")
.C:c413   A0 C4      LDY #$C4    ;note .A is still 1 (filename length)
.C:c415   20 BD FF   JSR $FFBD   ;KERNAL SetNam
.C:c418   20 C0 FF   JSR $FFC0   ;KERNAL Open (i.e., OPEN 1,8,15,"I")
;send M-W commands loop
.C:c41b   A0 03      LDY #$03    ;index string "M-W"
.C:c41d   20 93 C4   JSR $C493   ;send (partial) string from $C4A4 table
.C:c420   A5 FB      LDA $FB     ;address low
.C:c422   20 D2 FF   JSR $FFD2   ;send to drive
.C:c425   A5 FC      LDA $FC     ;C64 address high
.C:c427   E9 BF      SBC #$BF    ;subtract $C0 because carry clear
.C:c429   20 D2 FF   JSR $FFD2   ;send C1541 address high
.C:c42c   A9 20      LDA #$20    ;32 bytes to send
.C:c42e   AA         TAX         ;set count
.C:c42f   20 D2 FF   JSR $FFD2   ;send to drive
;byte send loop
.C:c432   B1 FB      LDA ($FB),Y ;read code byte
.C:c434   20 D2 FF   JSR $FFD2   ;send to drive
.C:c437   E6 FB      INC $FB     ;increment pointer low
.C:c439   D0 02      BNE $C43D   ;no carry, count down
.C:c43b   E6 FC      INC $FC     ;increment pointer high
.C:c43d   CA         DEX         ;count bytes, done?
.C:c43e   D0 F2      BNE $C432   ;no, byte send loop

.C:c440   20 CC FF   JSR $FFCC   ;KERNAL ClrChn -- sends Unlisten causing drive to execute command
.C:c443   A5 FB      LDA $FB     ;test address pointer less than $C6BA?
.C:c445   C9 BA      CMP #$BA
.C:c447   A5 FC      LDA $FC 
.C:c449   E9 C6      SBC #$C6
.C:c44b   90 CE      BCC $C41B   ;yes, send M-W commands loop

That code first sends an 'initialize' command to the disk drive. Then it issues a series of M-W (memory-write) commands to transfer code from $C500~C6BF in the C64 to address $500~6BF in the C1541/71. This is silly is you ask me -- definately slower than it needs to be! Think about it: we slow-loaded the code into the C64 and now we're slow-writing it back to the C1541. It would be more effecient for the C64 to just issue a block-read and/or block-execute command(s). Anyway, the transfer takes about 1.5 seconds. More initialization code follows:


.C:c44d   A0 06      LDY #$06    ;index string "U3"
.C:c44f   20 93 C4   JSR $C493   ;send string from $C4A4 table
.C:c452   20 CC FF   JSR $FFCC   ;KERNAL ClrChn -- sends Unlisten causing drive to execute command
.C:c455   78         SEI         ;disable interrupts
.C:c456   D8         CLD         ;clear BCD mode (kind of late don't ya think?)
.C:c457   A2 3F      LDX #$3F    ;multi-use constant
.C:c459   9A         TXS         ;reset CPU stack
.C:c45a   A9 2F      LDA #$2F    ;standard CPU data direction
.C:c45c   85 00      STA $00
.C:c45e   A9 35      LDA #$35    ;memory configuration no ROMs (just RAM and I/O)
.C:c460   85 01      STA $01
.C:c462   8E 02 DD   STX $DD02   ;standard CIA2 data direction
.C:c465   A9 7F      LDA #$7F    ;clear
.C:c467   8D 0D DC   STA $DC0D   ;CIA1 -- IRQ
.C:c46a   8D 0D DD   STA $DD0D   ;CIA2 -- NMI
.C:c46d   2C 0D DC   BIT $DC0D   ;clear again
.C:c470   2C 0D DD   BIT $DD0D   ;(pretty please)
.C:c473   E8         INX         ;$40 = opcode RTI (return from interrupt)
.C:c474   8E 01 01   STX $0101   ;set for NMI
.C:c477   A2 FF      LDX #$FF    ;clear any pending interrupts
.C:c479   8E 19 D0   STX $D019   ;of VIC chip
.C:c47c   E8         INX         ;zero
.C:c47d   8E 1A D0   STX $D01A   ;disable all interrupts of VIC
.C:c480   E8         INX         ;one
.C:c481   8E FA FF   STX $FFFA   ;set CPU NMI vector to $101 (where we wrote RTI instruction)
.C:c484   8E FB FF   STX $FFFB
.C:c487   E8         INX         ;two
;wait for C1541
.C:c488   2C 00 DD   BIT $DD00   ;test serial lines, is CLK high?
.C:c48b   70 FB      BVS $C488   ;yes, wait for C1541
.C:c48d   8E 00 DD   STX $DD00   ;allow CLK and DATA to go high, VIC Bank 1, clear UserPort line
.C:c490   4C 00 C3   JMP $C300   ;continue initialization

That code sends the (rarely used) U3 command to the disk drive which causes it to execute code at $500. Then it sets up various I/O ports and CPU vectors. Finally it waits for the serial-port CLK line to be pulled low by the C1541 before continuing. Let's take a look at the drive's $500 code:

.8:0500   78         SEI         ;disable controller
.8:0501   A9 EE      LDA #$EE    ;request byte-ready signal from disk head
.8:0503   8D 0C 1C   STA $1C0C
.8:0506   A9 F3      LDA #$F3    ;turn off drive motor and LED (!)
.8:0508   2D 00 1C   AND $1C00
.8:050b   8D 00 1C   STA $1C00
.8:050e   A9 08      LDA #$08    ;pull CLK low, allow DATA high
.8:0510   8D 00 18   STA $1800
.8:0513   A9 01      LDA #$01    ;DATA input bit
;wait 1 
.8:0515   2C 00 18   BIT $1800   ;is DATA low?
.8:0518   D0 FB      BNE $0515   ;yes, wait loop1
;wait 2
.8:051a   2C 00 18   BIT $1800   ;is DATA high?
.8:051d   F0 FB      BEQ $051A   ;yes, wait loop2

The disk drive isn't doing much here. It turns off the drive motor and LED and waits for the C64 to toggle serial DATA line. Let's go back to the C64 initialization.

.C:c300   78         SEI         ;disable interrupts (again -- we'll teach those interrupts!)
.C:c301   A9 34      LDA #$34    ;all RAM
.C:c303   85 01      STA $01     ;set C64 memory configuration
.C:c305   A0 00      LDY #$00    ;reset index
;copy loop
.C:c307   B9 00 C0   LDA $C000,Y ;copy $c000~c0ff
.C:c30a   99 00 D0   STA $D000,Y ;to $d000~d0ff
.C:c30d   B9 00 C1   LDA $C100,Y ;copy $c100~c207
.C:c310   99 00 02   STA $0200,Y ;to $200~307
.C:c313   B9 08 C1   LDA $C108,Y
.C:c316   99 08 02   STA $0208,Y
.C:c319   88         DEY         ;index prior, all done?
.C:c31a   D0 EB      BNE $C307   ;no, copy loop

.C:c31c   E6 01      INC $01     ;enable I/O registers in memory configuation
.C:c31e   A9 46      LDA #$46    ;filename "FF"
.C:c320   A0 46      LDY #$46
.C:c322   2C 00 02   BIT $0200   ;skip loader
.C:c325   2C B0 0F   BIT $0FB0   ;skip run
.C:c328   A9 30      LDA #$30    ;filename "00"
.C:c32a   A0 30      LDY #$30
.C:c32c   20 00 02   JSR $0200   ;call loader
~
.C:0200   85 FA      STA $FA     ;filename char 1
.C:0202   84 FB      STY $FB     ;filename char 2
.C:0204   A9 22      LDA #$22    ;pull DATA low, allow CLK high
.C:0206   8D 00 DD   STA $DD00   ;update serial bus
.C:0209   A9 00      LDA #$00    ;dummy byte
.C:020b   85 FF      STA $FF     ;flag first sector of file
.C:020d   20 CF 02   JSR $02CF   ;send dummy byte
.C:0210   A5 FA      LDA $FA     ;filename char 1
.C:0212   20 CF 02   JSR $02CF   ;send to C1541
.C:0215   98         TYA         ;filename char 2
.C:0216   20 CF 02   JSR $02CF   ;send to C1541
.C:0219   98         TYA         ;filename char 2
.C:021a   45 FA      EOR $FA     ;calculate checksum
.C:021c   20 CF 02   JSR $02CF   ;send to C1541

That code copies a data table to RAM $D000 (under I/O registers) and loader code to $200~307. Then it calls the fast-loader to get file "00". The beginning of the loader pulls the DATA line low and transmits four bytes: a dummy byte, two characters of filename, and a checksum. We'll see why it sends a checksum in a moment. Now lets look at the C64-to-C1541 transmit code:

;cycle times are in [brackets]
.C:02cf   85 FE      STA $FE     ;[3]save byte to transmit
.C:02d1   A2 00      LDX #$00    ;[2]disable
.C:02d3   8E 11 D0   STX $D011   ;[4]screen
.C:02d6   8E 15 D0   STX $D015   ;[4]sprites
;send byte loop (subtotal 74*4-1 = 295)
.C:02d9   A9 12      LDA #$12    ;[2]CLK low for even bit, assume data bit zero
.C:02db   46 FE      LSR $FE     ;[5]get data bit, is it zero?
.C:02dd   90 02      BCC $02E1   ;[3.5]yes, skip ahead
.C:02df   09 20      ORA #$20    ;[0]no, DATA low (inverted)
.C:02e1   8D 00 DD   STA $DD00   ;[4]update serial bus lines
.C:02e4   20 05 03   JSR $0305   ;[19]delay
.C:02e7   29 0F      AND #$0F    ;[2]CLK high for odd bit, assume data bit zero
.C:02e9   46 FE      LSR $FE     ;[5]get data bit, is it zero?
.C:02eb   90 02      BCC $02EF   ;[3.5]yes, skip ahead
.C:02ed   09 20      ORA #$20    ;[0]no, DATA low (inverted)
.C:02ef   8D 00 DD   STA $DD00   ;[4]update serial bus lines
.C:02f2   20 05 03   JSR $0305   ;[19]delay
.C:02f5   E8         INX         ;[2]count bit pairs
.C:02f6   E0 04      CPX #$04    ;[2]all 4 (8 bit total)?
.C:02f8   D0 DF      BNE $02D9   ;[3]no, send byte loop

.C:02fa   A9 02      LDA #$02    ;[2]allow CLK and DATA high
.C:02fc   8D 00 DD   STA $DD00   ;[4]update serial lines
;call delay 4 times (subtotal 24*4-1 = 95 cycles)
.C:02ff   20 05 03   JSR $0305   ;[19]delay
.C:0302   CA         DEX         ;[2] count, all done?
.C:0303   D0 FA      BNE $02FF   ;[3] no, delay loop

.C:0305   48         PHA         ;[3] waist time
.C:0306   68         PLA         ;[4]
.C:0307   60         RTS         ;[6]

That's the slowest transmit routine I've seen so far (excluding system ROMs). It takes a 'base' time of 295 cycles but then adds a delay and there is overhead. The total transmit time is about 422 microseconds. Oh well, it's suppose to be a fast-loader, not a fast transmitter!

If you notice, the transmit routine blanks the screen, but doesn't wait for the border to start. This means VIC bad-lines can strike while transmitting data (at least until the border starts). I'm pretty sure this is why the filename includes a checksum.

Anyway, let's see what the drive does with the filename:

.8:051f   8D 00 18   STA $1800   ;allow CLK and DATA to go high
.8:0522   A9 0C      LDA #$0C    ;turn on drive motor and LED
.8:0524   0D 00 1C   ORA $1C00
.8:0527   8D 00 1C   STA $1C00
.8:052a   20 DC 05   JSR $05DC   ;read byte from C64 (and discard!)
.8:052d   20 DC 05   JSR $05DC   ;read byte from C64
.8:0530   85 B7      STA $B7     ;save filename char 1
.8:0532   20 DC 05   JSR $05DC   ;read byte from C64
.8:0535   85 B8      STA $B8     ;save filename char 2
.8:0537   45 B7      EOR $B7     ;calculate checksum
.8:0539   85 B9      STA $B9     ;save checksum
.8:053b   20 DC 05   JSR $05DC   ;read byte from C64 (checksum)
.8:053e   A0 07      LDY #$07    ;buffer high-byte 
.8:0540   84 31      STY $31
.8:0542   C8         INY         ;8 = pull CLK low, allow DATA high
.8:0543   8C 00 18   STY $1800   ;update serial lines
.8:0546   C5 B9      CMP $B9     ;test checksum
.8:0548   D0 35      BNE $057F   ;error
.8:054a   A9 01      LDA #$01    ;# directory sectors
.8:054c   85 BA      STA $BA
.8:054e   0E B9 06   ASL $06B9   ;get bit, is it set? (yes on first pass)
.8:0551   B0 1A      BCS $056D   ;yes, skip ahead
;search for filename (fresh buffer)
.8:0553   A0 05      LDY #$05    ;buffer index for first filename
;search for filename (same buffer)
.8:0555   B1 30      LDA ($30),Y ;read $705 (dir filename 1st char)
.8:0557   C5 B7      CMP $B7     ;check requested 1st char
.8:0559   D0 07      BNE $0562   ;no match, next entry
.8:055b   B9 01 07   LDA $0701,Y ;read $706 (dir filename 2nd char)
.8:055e   C5 B8      CMP $B8     ;check requested 2nd char
.8:0560   F0 28      BEQ $058A   ;match, start loading
.8:0562   98         TYA         ;buffer index
.8:0563   18         CLC
.8:0564   69 20      ADC #$20    ;add 32 (size of directory entry)
.8:0566   A8         TAY         ;set index, end of buffer?
.8:0567   90 EC      BCC $0555   ;no, search for filename (same buffer)

.8:0569   A5 BA      LDA $BA     ;last directory sector?
.8:056b   F0 06      BEQ $0573   ;yes, error

.8:056d   C6 BA      DEC $BA     ;countdown directory sectors
.8:056f   A2 01      LDX #$01    ;desired sector
.8:0571   D0 05      BNE $0578   ;always, skip ahead
~
.8:0578   A9 12      LDA #$12    ;desired track 18
.8:057a   20 FE 05   JSR $05FE   ;read sector
.8:057d   F0 D4      BEQ $0553   ;always, search for filename (fresh buffer)

That code turns on the drive motor and LED, reads the filename (including dummy byte and checksum), and searches the directory for the desired file. Interestingly, it's hard-coded to only read one directory sector, so there can be no more than 8 files. Because the whole directory fits in one sector, it is permanently stored in buffer $700. When another file is loaded (later), the code won't move the disk-head to track 18 to read the directory. This saves some time. Also the code doesn't check the file type, so it can load DELeted files which aren't shown in a standard directory listing.

Before looking at the main loader, let's first look at the head-stepping portion of read sector:

.8:05fe   A0 08      LDY #$08    ;pull CLK low, allow DATA high
.8:0600   8C 00 18   STY $1800   ;update serial lines
.8:0603   85 18      STA $18     ;set desired track
.8:0605   86 19      STX $19     ;set desired sector
.8:0607   38         SEC         ;calculate distance (difference)
.8:0608   E5 22      SBC $22     ;from current track
.8:060a   F0 2F      BEQ $063B   ;already on-track, read sector
.8:060c   A2 01      LDX #$01    ;+1 (assume positive difference)
.8:060e   B0 06      BCS $0616   ;branch if diff is positive
.8:0610   49 FF      EOR #$FF    ;negate accumulator
.8:0612   69 01      ADC #$01    ;so now .A is positive
.8:0614   A2 FF      LDX #$FF    ;-1 (note negative difference)
.8:0616   86 B5      STX $B5     ;save delta (+1 or -1)
.8:0618   0A         ASL A       ;double difference
.8:0619   AA         TAX         ;set # half-track steps
;stepper loop
.8:061a   AD 00 1C   LDA $1C00   ;get stepper bits (and other stuff)
.8:061d   18         CLC
.8:061e   65 B5      ADC $B5     ;add delta (+1 or -1)
.8:0620   29 03      AND #$03    ;isolate new stepper bits
.8:0622   85 B6      STA $B6     ;save temp
.8:0624   AD 00 1C   LDA $1C00   ;get stepper bits (and other stuff)
.8:0627   29 FC      AND #$FC    ;clear old stepper bits (keep other stuff)
.8:0629   05 B6      ORA $B6     ;merge new stepper bits
.8:062b   8D 00 1C   STA $1C00   ;update stepper
.8:062e   A9 95      LDA #$95    ;timer value (about 5.5 milliseconds)
.8:0630   8D 05 18   STA $1805   ;start timer
;wait for timer
.8:0633   AD 05 18   LDA $1805   ;test timer, still running?
.8:0636   30 FB      BMI $0633   ;yes, wait for timer
.8:0638   CA         DEX         ;countdown # half-track steps
.8:0639   D0 DF      BNE $061A   ;not done, stepper loop
;head on-track
.8:063b   A5 18      LDA $18     ;desired track
.8:063d   85 22      STA $22     ;is current track
.8:063f   A2 04      LDX #$04    ;# track zones
;find zone of track
.8:0641   CA         DEX         ;index table
.8:0642   DD B1 06   CMP $06B1,X ;test table, current track less ?
.8:0645   B0 FA      BCS $0641   ;no, loop for zone index
.8:0647   AD 00 1C   LDA $1C00   ;get bit-rate bits (and stuff)
.8:064a   29 9F      AND #$9F    ;clear bit-rate bits (keep other stuff)
.8:064c   1D B5 06   ORA $06B5,X ;merge correct bit-rate bits for this zone
.8:064f   8D 00 1C   STA $1C00   ;update bit-rate for disk-head
~
>8:06b1  29 1f 19 12  00 20 40 60

The code is pretty standard for stepping the head at a constant rate. Assuming the head is not on the desired track, the code first calculates a delta value (+1 or -1) and number of half-track steps (in .X). Then it just updates the stepper bits with a constant delay from a hardware timer between steps. Next (even if the head was already on-track) the code determines the 'zone' of the track and sets the head bit-rate accordingly:

  • track 31~40 → bit-rate %00 (slowest)
  • track 25~30 → bit-rate %01
  • track 18~24 → bit-rate %10
  • track 0~17 → bit-rate %11 (fastest)

This is the CBM standard zone table with the exception of extra tracks 36~40. Next the actual reading and decoding of the sector:

;read sector
.8:0652   20 7F 06   JSR $067F   ;search for sector header and following sync-mark
;loop for buffer high ($700~7FF for directory or $300~3FF for file)
.8:0655   50 FE      BVC $0655   ;wait for byte-ready
.8:0657   B8         CLV         ;ready for next
.8:0658   AD 01 1C   LDA $1C01   ;get GCR byte from disk head
.8:065b   91 30      STA ($30),Y ;store in high buffer
.8:065d   C8         INY         ;index next, all done?
.8:065e   D0 F5      BNE $0655   ;no, loop for buffer high
.8:0660   A0 BA      LDY #$BA    ;index start (-70)
;loop for buffer low ($1BA~1FF)
.8:0662   50 FE      BVC $0662   ;wait for byte-ready
.8:0664   B8         CLV         ;ready for next
.8:0665   AD 01 1C   LDA $1C01   ;get GCR byte from disk head
.8:0668   99 00 01   STA $0100,Y ;store in low buffer
.8:066b   C8         INY         ;index next, all done?
.8:066c   D0 F4      BNE $0662   ;no, loop for buffer low

.8:066e   20 E0 F8   JSR $F8E0   ;decode GCR bytes (takes about 22,900 cycles)
.8:0671   A5 38      LDA $38     ;get leading byte
.8:0673   C5 47      CMP $47     ;test data-block identifier (7)
.8:0675   D0 DB      BNE $0652   ;mismatch, read sector again
.8:0677   20 E9 F5   JSR $F5E9   ;calculate sector checksum (takes about 2,575 cycles)
.8:067a   C5 3A      CMP $3A     ;compare with trailing byte
.8:067c   D0 D4      BNE $0652   ;mismatch, read sector again
.8:067e   60         RTS         ;exit

This code first searches for the sector header and trailing sync-mark (pre-data-block). That routine ($67F) is not shown because it is esentially the same as the ROM code with the difference being it will search forever. Next it reads 256 bytes to a high buffer (either $700 for directory or $300 for file) and 70 bytes into $1BA~1FF buffer. That's a total of 326 bytes which is actually one byte more than needed (like the ROM code). Next it calls ROM routines to decode the GCR bytes and calculate their checksum; this part takes about 25,500 cycles total. I'm using the total for comparison with other loaders which typically calculate the checksum at the same time they do the GCR conversion. Finally it tests the calculated checksum matches the one from disk. In case it isn't obvious, the code will loop forever if the sector is unreadable or not found.

  Meanwhile, back at the C64... 

The C64 has done a little work while the drive was finding the file:

.C:021f   AD 00 DD   LDA $DD00   ;read I/O bits
.C:0222   29 0C      AND #$0C    ;isolate ATN and User-Port bits
.C:0224   4A         LSR A       ;shift to lower 2 bits
.C:0225   4A         LSR A       ;(occupied by VIC bank)
.C:0226   4D 00 DD   EOR $DD00   ;scramble VIC bank bits with ATN and User-Port bit
.C:0229   29 0F      AND #$0F    ;isolate scramble code (low 4 bits)
.C:022b   49 FF      EOR #$FF    ;invert all bits (hardware line inversion)
.C:022d   8D 6A 02   STA $026A   ;modify our code to un-scramble
;loop for next sector
.C:0230   A9 22      LDA #$22    ;pull DATA low, allow CLK high (and VIC bank 1)
.C:0232   8D 00 DD   STA $DD00   ;update serial lines
.C:0235   A0 03      LDY #$03    ;delay 3*5+2-1 = 16 cycles
;delay loop
.C:0237   88         DEY         ;countdown, done?
.C:0238   D0 FD      BNE $0237   ;no, delay loop
;wait for C1541
.C:023a   2C 00 DD   BIT $DD00   ;is CLK low?
.C:023d   50 FB      BVC $023A   ;yes, wait for C1541

The main thing it does is calculate an 'unscramble' value and stores it in $26A (self-modifying code). This value is needed for two reasons: first the C1541 hardware will invert the bits it sends over the serial bus, but the C64/128 does not have inverters on its inputs (this explains the EOR #$FF instruction). Second, the loader code will EOR its byte-data with the lower bits of $DD00 which will cause the VIC bank and User-Port bits to scramble the value. As we'll see soon, the loader code uses a static decode table, but that isn't used for unscrambling because the unscramble value can change based on the VIC Bank and User-Port line.

Trivia: the code goes through the trouble of calculating an un-scramble value based on the User-Port and ATN bits. Although the UserPort bit has a default value of 1, it was changed to zero a long time ago. The ATN bit remains 0 throught loading. Since these bits are both a constant 0, they don't contribute to the (un)scrambling. In short, the code is more complex than neccessary.

Next the C64 loader receives and decodes 256 bytes of data and stores it in $D100~D1FF:

.C:023f   A0 10      LDY #$10    ;delay 16*5+2-1 = 81 cycles
.C:0241   88         DEY
.C:0242   D0 FD      BNE $0241
;read sector loop (87 cycles per pass)
.C:0244   A9 0F      LDA #$0F    ;[2]allow CLK and DATA to go high
.C:0246   2D 00 DD   AND $DD00   ;[4]
.C:0249   8D 00 DD   STA $DD00   ;[4]
.C:024c   09 20      ORA #$20    ;[2]value to pull DATA low
.C:024e   AA         TAX         ;[2]save for later
.C:024f   48         PHA         ;[3]waste 7 cycles
.C:0250   68         PLA         ;[4]
.C:0251   AD 00 DD   LDA $DD00   ;[4]read 2 bits
.C:0254   4A         LSR A       ;[2]shift down by two
.C:0255   4A         LSR A       ;[2]
.C:0256   EA         NOP         ;[2]waste 2 cycles
.C:0257   4D 00 DD   EOR $DD00   ;[4]read 2 more bits
.C:025a   4A         LSR A       ;[2]shift down by two
.C:025b   4A         LSR A       ;[2]
.C:025c   EA         NOP         ;[2]waste 2 cycles
.C:025d   4D 00 DD   EOR $DD00   ;[4]read 2 more bits
.C:0260   4A         LSR A       ;[2]shift down by two
.C:0261   4A         LSR A       ;[2]
.C:0262   EA         NOP         ;[2]waste 2 cycles
.C:0263   4D 00 DD   EOR $DD00   ;[4]read 2 last bits
.C:0266   8E 00 DD   STX $DD00   ;[4]pull DATA low, allow CLK high
.C:0269   49 FD      EOR #$FD    ;[2]unscramble data (value $FD set earlier)
.C:026b   AA         TAX         ;[2]index for decoding
.C:026c   C6 01      DEC $01     ;[5]memory config = all RAM
.C:026e   BD 00 D0   LDA $D000,X ;[4]decode value
.C:0271   99 00 D1   STA $D100,Y ;[5]save in buffer
.C:0274   E6 01      INC $01     ;[5]memory config = RAM + I/O
.C:0276   C8         INY         ;[2]index next byte, all done?
.C:0277   D0 CB      BNE $0244   ;[3]no, read sector loop

Due to the way the C1541 code is written, we need LDA $D000,X to decode the transmitted value. That code takes 81+256*87-1 = 22,352 cycles which is more than one full VIC screen (even considering the longer PAL). That value averages out to 87.3 cycles per byte. So this is one of the slower loaders, but definately not the slowest. Now that the C64 has a sector of data, let's see how the rest of the loader plays out.

.C:0279   C6 01      DEC $01     ;memory config = all RAM
.C:027b   AE 00 D1   LDX $D100   ;get next track#, is this the final sector?
.C:027e   F0 06      BEQ $0286   ;yes, skip ahead
.C:0280   A2 FF      LDX #$FF    ;no, all bytes in sector
.C:0282   8E 01 D1   STX $D101   ;save for limit
.C:0285   E8         INX         ;zero

.C:0286   C8         INY         ;1 = source index for buffer (and flag value)
.C:0287   E6 01      INC $01     ;memory config = RAM + I/O
.C:0289   A5 FF      LDA $FF     ;is this the first sector?
.C:028b   84 FF      STY $FF     ;(flag first sector processed)
.C:028d   D0 1B      BNE $02AA   ;no, do RAM transfer
;first sector
.C:028f   C6 01      DEC $01     ;memory config = all RAM
.C:0291   A0 03      LDY #$03    ;source index for buffer (skip load address bytes)
.C:0293   AD 02 D1   LDA $D102   ;get address low
.C:0296   85 FC      STA $FC     ;initialize pointer
.C:0298   AD 03 D1   LDA $D103   ;get address high
.C:029b   E6 01      INC $01     ;memory config = RAM + I/O
.C:029d   85 FD      STA $FD     ;initialize pointer
.C:029f   05 FC      ORA $FC     ;test load address, is it zero?
.C:02a1   D0 07      BNE $02AA   ;no, do RAM transfer
.C:02a3   A5 FA      LDA $FA     ;yes, get filename again
.C:02a5   A4 FB      LDY $FB
.C:02a7   4C 00 02   JMP $0200   ;start loader all over
;do RAM transfer
.C:02aa   C6 01      DEC $01     ;memory config = all RAM
;transfer loop
.C:02ac   C8         INY         ;index source
.C:02ad   B9 00 D1   LDA $D100,Y ;read decoded value
.C:02b0   81 FC      STA ($FC,X) ;save to RAM (.X=0 so pointer $FC,FD)
.C:02b2   E6 FC      INC $FC     ;index destination low, any carry?
.C:02b4   D0 02      BNE $02B8   ;no, test limit
.C:02b6   E6 FD      INC $FD     ;index destination high
;test limit
.C:02b8   CC 01 D1   CPY $D101   ;was that the last byte in sector?
.C:02bb   D0 EF      BNE $02AC   ;no, transfer loop

.C:02bd   AD 00 D1   LDA $D100   ;last sector?
.C:02c0   F0 05      BEQ $02C7   ;yes, done loading
.C:02c2   E6 01      INC $01     ;memory config = RAM + I/O
.C:02c4   4C 30 02   JMP $0230   ;loop for next sector
;done loading
.C:02c7   E6 01      INC $01     ;memory config = RAM + I/O
.C:02c9   A9 02      LDA #$02    ;allow CLK and DATA to go high (and VIC bank 1)
.C:02cb   8D 00 DD   STA $DD00   ;update serial lines
.C:02ce   60         RTS         ;return

There's not much to that code, although it's a bit messy due to the memory configuration changing. Basically it's sets a limit of $FF into $D101 unless it's the last sector, in which case $D101 already holds the limit. If it's the first sector, it grabs the load address from $D102~D103 and checks for zero. Then it copies data from the buffer to the correct location in RAM using the rare index-indirect addressing mode. Finally, it loops if not the last sector or else allows the CLK and DATA lines on the serial bus to go high.

There is no sign of copy-protection in the fast-load code. It must be somewhere else in the main program.

  Summary 
  • Blank screen: yes
  • Interrupts allowed: no
  • Disk Header: standard
  • Directory structure: standard (but can read DEL files)
  • File structure: standard
  • Allow wildcard in filename: no (restricted to 2 chars)
  • Sector structure: standard (256 data bytes)
  • Sector decoding time: 25.5 milliseconds (slow/ROM)
  • Head stepping speed: fast (about 5.5 milliseconds/half-track)
  • Disk → C64 transfer: fast (about 87 microseconds/byte)
  • C64 → Disk transfer (filename): medium (about 422 microseconds/byte)
  • C64 memory footprint: under 1.75K ($C000~C6B9)
  • Needs KERNAL: no
  • Load $D000~DFFF: RAM
  • Alters User Port: yes
  • Requires Unit 8: yes
  • Write file/sector: no
  • Other: none

© Hydrophilic.net, 2026