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
  Access Fast-loader 

This fast-loader comes from the game Echelon by Access Software. Interestingly, this fast-loader is not installed automatically when the game boots. A BASIC program will ask the user if (s)he would like to install the fast-loader. Selecting "yes" causes the file "FASTLD" to be loaded (with standard ROM routines) to address $C000~C3A7. Then KERNAL vector for Load is changed to point to $C000. Let's look at the code:

.C:c000   A0 00      LDY #$00    ;reset filename index
.C:c002   84 90      STY $90     ;clearn KERNAL status
.C:c004   B1 BB      LDA ($BB),Y ;read filename char
.C:c006   C9 24      CMP #$24    ;is it "$" for directory?
.C:c008   D0 07      BNE $C011   ;no, skip ahead
.C:c00a   A5 93      LDA $93     ;we don't handle directory
.C:c00c   A4 C3      LDY $C3     ;so continue with
.C:c00e   4C A5 F4   JMP $F4A5   ;standard KERNAL code

.C:c011   A5 C3      LDA $C3     ;user load-address low
.C:c013   85 AE      STA $AE     ;set load-address low (assume we will use it)
.C:c015   A5 C4      LDA $C4     ;user load-address high
.C:c017   85 AF      STA $AF     ;set load-address high
.C:c019   AD 0E DC   LDA $DC0E   ;CIA1 Timer A register
.C:c01c   85 03      STA $03     ;save for exit routine
.C:c01e   A9 00      LDA #$00    ;disable (stop)
.C:c020   8D 0E DC   STA $DC0E   ;CIA1 Timer A
.C:c023   A9 7F      LDA #$7F    ;clear all interrupts
.C:c025   8D 0D DD   STA $DD0D   ;on CIA2
.C:c028   AD 00 DD   LDA $DD00   ;serial lines, UserPort bit, VIC bank bits
.C:c02b   29 07      AND #$07    ;keep UserPort and VIC bits
.C:c02d   85 07      STA $07     ;save for exit routine

The first part of that code makes sure we're loading a normal file, not a directory listing. Then it set's the user-specified load-address into our working pointer (assuming it will be used). Depending on a KERNAL variable, we may instead get the load-address from the file (as we'll see later). Finally it saves and initializes some hardware registers. Interestingly, to disable IRQs, it clears CIA1 Timer A instead of just issuing SEI command. I believe this is because the KERNAL serial routines will just re-enable interrupts anyway. Next we have code which will write 512 bytes of code to the drive RAM using slow/standard serial routines.

.C:c02f   A9 10      LDA #$10    ;32 Memory-Writes (512 bytes)
.C:c031   85 2A      STA $2A     ;set counter
.C:c033   A9 00      LDA #$00    ;set $C3~C4 to point to $400
.C:c035   85 C3      STA $C3     ;(C1541/71 destination)
.C:c037   A9 04      LDA #$04
.C:c039   85 C4      STA $C4
.C:c03b   A9 AB      LDA #$AB    ;set $5~6 to point to $C1AB
.C:c03d   85 05      STA $05     ;(C64 source)
.C:c03f   A9 C1      LDA #$C1
.C:c041   85 06      STA $06
.C:c043   20 AE FF   JSR $FFAE   ;KERNAL Unlisten
.C:c046   20 6B C1   JSR $C16B   ;call KERNAL Listen and Second
.C:c049   90 08      BCC $C053   ;skip ahead if okay

.C:c04b   20 AE FF   JSR $FFAE   ;KERNAL Unlisten
.C:c04e   A9 05      LDA #$05    ;error code
.C:c050   4C 54 C1   JMP $C154   ;exit routine

.C:c053   A9 49      LDA #$49    ;"I" (initialize drive command)
.C:c055   20 A8 FF   JSR $FFA8   ;KERNAL Serial Out
.C:c058   20 AE FF   JSR $FFAE   ;KERNAL Unlisten (drive will execute command)
;Memory-Write loop
.C:c05b   20 81 C1   JSR $C181   ;send "M-W" string to drive (memory write)
.C:c05e   A5 C3      LDA $C3     ;drive address low
.C:c060   20 A8 FF   JSR $FFA8   ;KERNAL Serial Out
.C:c063   A5 C4      LDA $C4     ;drive address high
.C:c065   20 A8 FF   JSR $FFA8   ;KERNAL Serial Out
.C:c068   A9 20      LDA #$20    ;32 byte count
.C:c06a   20 A8 FF   JSR $FFA8   ;KERNAL Serial Out
.C:c06d   A0 00      LDY #$00    ;initial pointer
;write bytes loop
.C:c06f   B1 05      LDA ($05),Y ;read RAM (from $C1AB~C3AA)
.C:c071   20 A8 FF   JSR $FFA8   ;KERNAL Serial Out
.C:c074   C8         INY         ;index C64 RAM
.C:c075   C0 20      CPY #$20    ;all 32 bytes?
.C:c077   D0 F6      BNE $C06F   ;no, write bytes loop

.C:c079   20 AE FF   JSR $FFAE   ;KERNAL Unlisten (drive will execute command)
.C:c07c   A5 C3      LDA $C3     ;add 32 to pointer $C3~C4 (drive address)
.C:c07e   18         CLC
.C:c07f   69 20      ADC #$20
.C:c081   85 C3      STA $C3
.C:c083   90 02      BCC $C087
.C:c085   E6 C4      INC $C4
.C:c087   A5 05      LDA $05     ;add 32 to pointer $5~6 (C64 address)
.C:c089   18         CLC
.C:c08a   69 20      ADC #$20
.C:c08c   85 05      STA $05
.C:c08e   90 02      BCC $C092
.C:c090   E6 06      INC $06
.C:c092   C6 2A      DEC $2A     ;countdown # M-W commands, all done?
.C:c094   D0 C5      BNE $C05B   ;no, Memory-Write loop

.C:c096   20 96 C1   JSR $C196   ;send "M-E" string to drive (memory execute)
.C:c099   A9 7F      LDA #$7F    ;drive address low
.C:c09b   20 A8 FF   JSR $FFA8   ;KERNAL Serial Out
.C:c09e   A9 05      LDA #$05    ;drive address high (i.e., execute $57F)
.C:c0a0   20 A8 FF   JSR $FFA8   ;KERNAL Serial Out
.C:c0a3   A5 B7      LDA $B7     ;filename length
.C:c0a5   20 A8 FF   JSR $FFA8   ;KERNAL Serial Out
.C:c0a8   A0 00      LDY #$00    ;clear index to filename
;filename loop
.C:c0aa   B1 BB      LDA ($BB),Y ;read filename character
.C:c0ac   20 A8 FF   JSR $FFA8   ;KERNAL Serial Out
.C:c0af   C8         INY         ;index next char
.C:c0b0   C4 B7      CPY $B7     ;all chars of filename?
.C:c0b2   D0 F6      BNE $C0AA   ;no, filename loop

.C:c0b4   20 AE FF   JSR $FFAE   ;KERNAL Unlisten (drive will execute command)
.C:c0b7   AD 00 DD   LDA $DD00   ;get serial lines
.C:c0ba   09 10      ORA #$10    ;pull CLK low, allow DATA high
.C:c0bc   8D 00 DD   STA $DD00   ;update serial lines
.C:c0bf   78         SEI         ;disable interrupts (in case VIC IRQ?)
;wait for drive ready
.C:c0c0   2C 00 DD   BIT $DD00   ;test serial lines, is DATA high?
.C:c0c3   30 FB      BMI $C0C0   ;yes, wait for drive
.C:c0c5   4C 11 C1   JMP $C111   ;do fast-load

That code sends 512 bytes of code to the drive RAM $500~6FF. However a single M-W command can't send that much, so the data is transmitted in chunks of 32 bytes. After the code is transferred (which takes about 1.5 seconds), a M-E command instructs the drive to begin execution at $57F. The M-E command will leave the file-length at $205 and filename at $206+ in the drive's RAM (parser buffer). Let's see what the code does.


.8:057f   A2 02      LDX #$02    ;pull DATA low, allow CLK high
.8:0581   8E 00 18   STX $1800   ;update serial lines
.8:0584   A9 12      LDA #$12    ;18
.8:0586   85 08      STA $08     ;desired track
.8:0588   CA         DEX         ;1
.8:0589   86 09      STX $09     ;desired sector (start of directory)
.8:058b   A9 35      LDA #$35    ;set pointer $6F~70 to $435 (for directory search)
.8:058d   85 6F      STA $6F
.8:058f   A9 04      LDA #$04
.8:0591   85 70      STA $70
.8:0593   CA         DEX         ;0
.8:0594   86 3B      STX $3B     ;index filename for directory search
;scan for comma
.8:0596   BD 06 02   LDA $0206,X ;read character from filename
.8:0599   C9 2C      CMP #$2C    ;is it a comma (,)?
.8:059b   F0 0A      BEQ $05A7   ;yes, process comma
.8:059d   E8         INX         ;no, index next character
.8:059e   EC 05 02   CPX $0205   ;all chars checked?
.8:05a1   D0 F3      BNE $0596   ;no, scan for comma
;no comma found
.8:05a3   A0 02      LDY #$02    ;filetype index (for PRG)
.8:05a5   D0 12      BNE $05B9   ;always, begin filename test
;process comma
.8:05a7   BD 07 02   LDA $0207,X ;get character after comma
.8:05aa   A0 04      LDY #$04    ;5 file types to check
;test type loop
.8:05ac   D9 B6 FE   CMP $FEB6,Y ;test character (D,S,P,U,L)
.8:05af   F0 05      BEQ $05B6   ;match found
.8:05b1   88         DEY         ;no match, index prior, all done?
.8:05b2   10 F8      BPL $05AC   ;no test type loop
;character type invalid
.8:05b4   A0 02      LDY #$02    ;default filetype (PRG)
.8:05b6   8E 05 02   STX $0205   ;update filename length (shorter: ignore comma and everything after)
;begin filename test
.8:05b9   8C 04 02   STY $0204   ;save filetype
.8:05bc   AD 06 02   LDA $0206   ;first char of filename
.8:05bf   C9 30      CMP #$30    ;is it a "0"?
.8:05c1   D0 0F      BNE $05D2   ;no, test for single colon
.8:05c3   AD 07 02   LDA $0207   ;second char of filename
.8:05c6   C9 3A      CMP #$3A    ;is it a ":"?
.8:05c8   D0 08      BNE $05D2   ;no, test for single colon
.8:05ca   20 E9 05   JSR $05E9   ;delete leading character of filename
.8:05cd   20 E9 05   JSR $05E9   ;delete leading character of filename
.8:05d0   D0 0A      BNE $05DC   ;always, do search/load
;test for single colon
.8:05d2   AD 06 02   LDA $0206   ;first char of filename
.8:05d5   C9 3A      CMP #$3A    ;is it a ":"?
.8:05d7   D0 03      BNE $05DC   ;no, skip ahead
.8:05d9   20 E9 05   JSR $05E9   ;yes, delete leading char of filename
;do search/load
.8:05dc   A9 E0      LDA #$E0    ;execute buffer command code
.8:05de   85 01      STA $01     ;set command for buffer $400
;wait for command complete
.8:05e0   A5 01      LDA $01     ;get command status, is it still pending?
.8:05e2   30 FC      BMI $05E0   ;yes, wait for command complete
.8:05e4   F0 F6      BEQ $05DC   ;is loader finished? no, do search/load
.8:05e6   4C E7 EB   JMP $EBE7   ;yes, exit to ROM

That code first signals the C64 that the code is running (but data not ready) by pulling DATA low. Next it sets the desired track and sector (18,1) for the start of the directory in the controller's sector table (for buffer $400). Then it checks the filename for two things: a filetype specifier (like ",S") and/or a leading drive specifier ("0:" or just ":"). If no filetype specifier is found (or it's invalid), the filetype defaults to PRG (value 2). If a drive specifier is found, it is simply deleted (not part of the filename).

Finally it stores an "execute buffer" command in the controller's command table. The next time the controller runs, it will move to the correct track (if not already there) and then execute the code at $400. That code will initially search for the given filename. Later the $400 code will read and transfer the file to the C64. Either way that code updates location $01, and this codes tests that result. If the result is zero, the code loops and issues command $E0 again; otherwise the loader ends by jumping to ROM.

Let's look at that $400 code:

.8:0400   A9 00      LDA #$00    ;set pointe $30~31 to $300
.8:0402   85 30      STA $30
.8:0404   A9 03      LDA #$03
.8:0406   85 31      STA $31
.8:0408   A5 22      LDA $22     ;get current track
.8:040a   C5 08      CMP $08     ;does it match desired track?
.8:040c   F0 05      BEQ $0413   ;yes, continue
.8:040e   A9 00      LDA #$00    ;no, set controller status
.8:0410   4C 69 F9   JMP $F969   ;and exit to ROM (we'll get called again later)

.8:0413   20 0A F5   JSR $F50A   ;find sector header and wait for data-block sync to end (sets .Y = 0)
;wait for high data
.8:0416   50 FE      BVC $0416   ;wait for byte-ready (high data)
.8:0418   B8         CLV         ;ready for next
.8:0419   AD 01 1C   LDA $1C01   ;read drive-head data
.8:041c   91 30      STA ($30),Y ;store in $300 buffer
.8:041e   C8         INY         ;index buffer, filled up?
.8:041f   D0 F5      BNE $0416   ;no, wait for high data
.8:0421   A0 BA      LDY #$BA    ;yes, initialize index for low buffer
;wait for low data
.8:0423   50 FE      BVC $0423   ;wait for byte-ready (low data)
.8:0425   B8         CLV         ;ready for more
.8:0426   AD 01 1C   LDA $1C01   ;read drive-head data
.8:0429   99 00 01   STA $0100,Y ;store in $1BA~1FF
.8:042c   C8         INY         ;index next, done?
.8:042d   D0 F4      BNE $0423   ;no, wait for low data

.8:042f   20 E0 F8   JSR $F8E0   ;convert buffer from GCR to binary
.8:0432   6C 6F 00   JMP ($006F) ;either $435 (find filename) or $50F (load sector)

That code first sets a pointer for the high buffer at $300~3FF. Next it checks if the head is on the correct track; if not, the code will exit to ROM and the code at $5E0 will call us again. In other words, we let the ROM controller do the head-stepping.

When the disk-head is on the correct track, we have the ROM search for a sector header. Hopefully it is found and read successfuly, because if not the ROM will set an error code causing our drive code to end prematurely; the poor C64 will be left in an infinit loop!

Once the sector is found, the GCR data is read into both a high and low buffer. Then the ROM is called to decode the data (it will be stored in $300~3FF). The code should next calculate and test the data checksum but it does not. This means a bad read will result in corrupt data being sent to the C64. Also the code doesn't check the data-mark byte, which should be 7. A non-standard data-mark could be used as a form of copy-protection, but I don't think this is the case.

Finally the code uses an indirect jump to continue. In the first phase (directory search) it will jump to $435. In the second phase (file load) it will jump to $50F Let's look at the $435 code:

.8:0435   AD 06 02   LDA $0206   ;first char of filename
.8:0438   C9 2A      CMP #$2A    ;is it "*"?
.8:043a   D0 0E      BNE $044A   ;no, do standard name check
.8:043c   A5 7E      LDA $7E     ;do we have a prior file's track?
.8:043e   F0 0A      BEQ $044A   ;no, do standard name check
.8:0440   85 08      STA $08     ;yes set as our file's track
.8:0442   AD 6F 02   LDA $026F   ;prior file's sector
.8:0445   85 09      STA $09     ;set as our file's sector
.8:0447   4C B4 04   JMP $04B4   ;set up loading
;standard name check
.8:044a   A4 3B      LDY $3B     ;index sector filename (default 0)
.8:044c   A2 00      LDX #$00    ;index requested filename
.8:044e   B9 02 03   LDA $0302,Y ;get filetype from sector, is it properly closed?
.8:0451   10 1B      BPL $046E   ;no, next directory entry
.8:0453   29 07      AND #$07    ;mask filetype
.8:0455   CD 04 02   CMP $0204   ;compare with requested (or default) type
.8:0458   D0 14      BNE $046E   ;no match, next directory entry
;test filename
.8:045a   BD 06 02   LDA $0206,X ;read requested filename char
.8:045d   C9 2A      CMP #$2A    ;is it an asterisk (*)?
.8:045f   F0 42      BEQ $04A3   ;yes, file found
.8:0461   C9 0D      CMP #$0D    ;is it an ASCII return?
.8:0463   F0 3E      BEQ $04A3   ;yes, file found
.8:0465   D9 05 03   CMP $0305,Y ;test filename from directory, does it match?
.8:0468   F0 27      BEQ $0491   ;yes, next char in filename
.8:046a   C9 3F      CMP #$3F    ;no, is requested char "?" ?
.8:046c   F0 23      BEQ $0491   ;yes, next char in filename
;next directory entry (filename/type mismatch)
.8:046e   A5 3B      LDA $3B     ;index in sector
.8:0470   18         CLC
.8:0471   69 20      ADC #$20    ;add 32 (size of directory entry)
.8:0473   85 3B      STA $3B     ;update index, end of sector?
.8:0475   90 D3      BCC $044A   ;no, standard filename check
.8:0477   AD 00 03   LDA $0300   ;get track of next directory sector
.8:047a   F0 0E      BEQ $048A   ;zero → file not found
.8:047c   85 08      STA $08     ;set track for next directory block
.8:047e   AD 01 03   LDA $0301   ;get sector of next directory block
.8:0481   C5 09      CMP $09     ;compare with self(!), do they match?
.8:0483   F0 05      BEQ $048A   ;yes, file not found
.8:0485   85 09      STA $09     ;set sector for next directory block
.8:0487   4C 00 04   JMP $0400   ;loop to read next sector
;file not found
.8:048a   A9 FF      LDA #$FF    ;flag 'file not found'
.8:048c   85 21      STA $21     ;set flag
.8:048e   4C 0F 05   JMP $050F   ;skip ahead
;next character in filename
.8:0491   C8         INY         ;index char in directory
.8:0492   E8         INX         ;index char in requested filename
.8:0493   EC 05 02   CPX $0205   ;check filename length, end reached?
.8:0496   90 C2      BCC $045A   ;no, test filename
.8:0498   B9 05 03   LDA $0305,Y ;char from directory filename
.8:049b   C9 A0      CMP #$A0    ;is it a non-breaking-space ?
.8:049d   F0 04      BEQ $04A3   ;yes, file found
.8:049f   E0 10      CPX #$10    ;no, have we reached maximum name length (16)?
.8:04a1   D0 CB      BNE $046E   ;no, next directory entry
;file found
.8:04a3   A4 3B      LDY $3B     ;index for directory entry
.8:04a5   B9 03 03   LDA $0303,Y ;get starting track
.8:04a8   85 08      STA $08     ;save for ourself
.8:04aa   85 7E      STA $7E     ;save for DOS 'last file track'
.8:04ac   B9 04 03   LDA $0304,Y ;get starting sector
.8:04af   85 09      STA $09     ;save for ourself
.8:04b1   8D 6F 02   STA $026F   ;save for DOS 'last file sector'
;set-up loading
.8:04b4   A9 0F      LDA #$0F    ;set vector $6F~70 to $50F
.8:04b6   85 6F      STA $6F
.8:04b8   A9 05      LDA #$05
.8:04ba   85 70      STA $70
.8:04bc   A9 02      LDA #$02    ;flag 'first sector'
.8:04be   85 21      STA $21     ;set flag
.8:04c0   4C 00 04   JMP $0400   ;read first sector of file

This directory search code does several things. First it implements a rarely used (mis)feature of the C1541. After accessing a file (with load or save, for example), the DOS will remember that file's starting track and sector. Then if you try to do something with a file named "*", the DOS will access that last-used file. Many (most?) C64 users think that * will refer to the first file on disk, but this is true only after power-up or a disk swap (i.e., when there is no last-used file).

Next the code tests the filename in the directory for a couple of things: properly closed, file-type match, and filename match. The filename matching allows the use of the "*" and "?" wildcards.

If a match is found, the track and sector is pulled from the directory and stored for our own code, and for DOS's last-used file. A flag is set (to 2) and code jumps to $50F after reading the requested sector via JMP $400. If a match is not found, a flag is set to $FF and the code jumps directly to $50F (because there is no data to read). Let's see what happens at the $50F code:

.8:050f   AD 00 03   LDA $0300   ;get next track of file, is it zero?
.8:0512   D0 06      BNE $051A   ;no, skip ahead
.8:0514   A5 21      LDA $21     ;yes, get flag byte
.8:0516   09 01      ORA #$01    ;set bit 0 to indicate last sector
.8:0518   85 21      STA $21     ;update flag

.8:051a   A5 21      LDA $21     ;get flag byte
.8:051c   20 D3 04   JSR $04D3   ;fast send to C64
.8:051f   A5 21      LDA $21     ;(re)get flag byte
.8:0521   10 03      BPL $0526   ;continue if okay
.8:0523   4C 7A 05   JMP $057A   ;exit if file not found

.8:0526   29 02      AND #$02    ;test 'first sector' bit
.8:0528   F0 0F      BEQ $0539   ;not first sector, set starting index
.8:052a   AD 02 03   LDA $0302   ;get load address, low byte
.8:052d   20 D3 04   JSR $04D3   ;fast send to C64
.8:0530   AD 03 03   LDA $0303   ;get laod address, high byte
.8:0533   20 D3 04   JSR $04D3   ;fast send to C64
.8:0536   A9 04      LDA #$04    ;starting index (first sector)
.8:0538   2C                     ;skip next instruction
;set starting index
.8:0539   A9 04      LDA #$02    ;starting index (not first sector)
.8:053b   8D 5F 05   STA $055F   ;save start index (self-modifying code)
.8:053e   A4 21      LDY $21     ;get status flag
.8:0540   98         TYA
.8:0541   29 01      AND #$01    ;is bit 0 (last sector) set?
.8:0543   F0 05      BEQ $054A   ;no, calc data length
.8:0545   AE 01 03   LDX $0301   ;get ending index
.8:0548   CA         DEX         ;convert to data length
.8:0549   2C                     ;skip next instruction
;calc data length
.8:054a   A2 FE      LDX #$FE    ;254 data bytes (not last sector)
.8:054c   98         TYA         ;get status flag
.8:054d   29 02      AND #$02    ;is bit 1 (first sector) set?
.8:054f   F0 07      BEQ $0558   ;no, set data length
.8:0551   CA         DEX         ;yes, decrease by 2 (load-address already sent)
.8:0552   CA         DEX
.8:0553   98         TYA         ;status flag
.8:0554   29 FD      AND #$FD    ;clear bit 1 (first sector)
.8:0556   85 21      STA $21     ;save status flag
;set data length
.8:0558   86 3C      STX $3C     ;set data length (1~254 bytes)
.8:055a   8A         TXA         ;move to .A
.8:055b   20 D3 04   JSR $04D3   ;fast send to C64
;transmit data block
.8:055e   AD 00 03   LDA $0300   ;read from buffer (address modified by code)
.8:0561   20 D3 04   JSR $04D3   ;fast send to C64
.8:0564   EE 5F 05   INC $055F   ;increment buffer-pointer low
.8:0567   C6 3C      DEC $3C     ;countdown #bytes to send, all done?
.8:0569   D0 F3      BNE $055E   ;no, transmit data block

.8:056b   AD 01 03   LDA $0301   ;get next sector from buffer
.8:056e   85 09      STA $09     ;save for our code
.8:0570   AD 00 03   LDA $0300   ;get next track from buffer
.8:0573   85 08      STA $08     ;save for our code, was that last sector?
.8:0575   F0 03      BEQ $057A   ;yes, set result code
.8:0577   4C 00 04   JMP $0400   ;no, loop for next sector
;set result code
.8:057a   A9 01      LDA #$01    ;status code ok/done
.8:057c   4C 69 F9   JMP $F969   ;exit to ROM (will update $01 command-status byte)

The first thing the code does is check if the loaded sector is the last one. If so, it sets a bit in a status flag. It transmits whatever the status byte is to the C64. If the status was 'file not found' the code then exits to ROM.

Otherwise, it checks the status flag for 'first sector'. When this is true, the first two data bytes from the buffer (the load address) are transmitted.

Next the code sets the starting index in the buffer. This is usually 2 but for the first sector it is 4. Then it calculates the number of remaining bytes to transmit. This value is stored in $3C and transmitted to the C64.

Finally the code transmits the bulk of the data using the fast protocol. The next track and sector are setup. If this was the last sector, the code exits with code 1 (okay) to ROM. Otherwise the code loops back to $400 to load the next sector.

  Meanwhile, back at the C64... 

The C64 has been waiting patiently while the drive searched for the file. Let's see what happens when the drive is finally ready with data:

;loop for sectors
.C:c111   20 C8 C0   JSR $C0C8   ;fast-read byte
.C:c114   85 06      STA $06     ;save status code
.C:c116   A5 06      LDA $06     ;test code, is it okay?
.C:c118   10 03      BPL $C11D   ;yes, continue
.C:c11a   4C 52 C1   JMP $C152   ;no, error exit 

.C:c11d   29 02      AND #$02    ;test bit 1 (first sector) set?
.C:c11f   F0 12      BEQ $C133   ;no, get byte count
.C:c121   20 C8 C0   JSR $C0C8   ;get load-address low from file
.C:c124   A6 B9      LDX $B9     ;test flag -- use file's load address?
.C:c126   F0 02      BEQ $C12A   ;no, skip ahead (don't set)
.C:c128   85 AE      STA $AE     ;set load-address low
.C:c12a   20 C8 C0   JSR $C0C8   ;get load-address high from file
.C:c12d   A6 B9      LDX $B9     ;test flag -- use file's load address?
.C:c12f   F0 02      BEQ $C133   ;no, get byte count (don't set)
.C:c131   85 AF      STA $AF     ;set load-address high
;get byte count
.C:c133   20 C8 C0   JSR $C0C8   ;fast-load byte count
.C:c136   85 05      STA $05     ;save it
.C:c138   A0 00      LDY #$00    ;reset index
;loop for data (cycle times in [brackets])
.C:c13a   20 C8 C0   JSR $C0C8   ;[109=6+103]fast-read byte from C1541
.C:c13d   91 AE      STA ($AE),Y ;[6]save to C64 RAM
.C:c13f   E6 AE      INC $AE     ;[5]increment pointer low, any carry?
.C:c141   D0 02      BNE $C145   ;[3]no, skip next instruction
.C:c143   E6 AF      INC $AF     ;[0]increment pointer high
.C:c145   C6 05      DEC $05     ;[5]countdown bytes, all done?
.C:c147   D0 F1      BNE $C13A   ;[3]no, loop for data

.C:c149   A5 06      LDA $06     ;get status byte
.C:c14b   29 01      AND #$01    ;test last sector?
.C:c14d   F0 C2      BEQ $C111   ;no, loop for sectors

.C:c14f   18         CLC         ;flag success (.A = 1)
.C:c150   90 07      BCC $C159   ;always, finish up
;error exit
.C:c152   A9 04      LDA #$04    ;error code
.C:c154   A2 80      LDX #$80    ;error code (device not present!)
.C:c156   86 90      STX $90     ;KERNAL Status
.C:c158   38         SEC         ;flag error
;finish up
.C:c159   48         PHA         ;save 'error code'
.C:c15a   A5 07      LDA $07     ;original UserPort and VIC Bank bits
.C:c15c   8D 00 DD   STA $DD00   ;restore
.C:c15f   A5 03      LDA $03     ;original Timer A value
.C:c161   8D 0E DC   STA $DC0E   ;restore
.C:c164   58         CLI         ;enable interrupts
.C:c165   68         PLA         ;'error code'
.C:c166   A6 AE      LDX $AE     ;get ending address+1 low
.C:c168   A4 AF      LDY $AF     ;and high
.C:c16a   60         RTS         ;return

The first thing the code does is read a status byte and test it. If an error is indicated, the code exits (discussed below). Otherwise it tests for 'first-sector' flag and if set will read the file's load-address. The code may or may not use the file load-address, depending on the setting of KERNAL variable $B9.

Next it reads a byte-count and enters a loop to read that many bytes from the disk-drive. Once those bytes are loaded, it tests the status byte to see if that was the last sector. If not, the code loops back to $C111 for another sector. Otherwise it clears the carry flag and branches to finish up.

The error routine strangely loads a value of 4 into the accumulator. Strange because the KERNAL does define an error code through the accumulator, but instead through the KERNAL variable 'status' at $90. Also strange is the 'status' code is set to 'device not present' which is obviously a lie!

So that's how the main loader works. However I would be remiss if I didn't review the actual fast-read routine:

;wait for data (cycle times in [brackets])
.C:c0c8   AD 00 DD   LDA $DD00   ;[4]is serial DATA low?
.C:c0cb   10 FB      BPL $C0C8   ;[2]yes, wait for data
.C:c0cd   29 03      AND #$03    ;[2]allow CLK and DATA to go high (keep VIC Bank, clear UserPort bit)
.C:c0cf   AA         TAX         ;[2]save for later
.C:c0d0   AD 11 D0   LDA $D011   ;[4]VIC raster bit 8 set?
.C:c0d3   30 11      BMI $C0E6   ;[2]yes, do transfer (in border)
.C:c0d5   29 10      AND #$10    ;[2]no, is screen blanked?
.C:c0d7   F0 0D      BEQ $C0E6   ;[2]yes, do transfer
;wait for VIC
.C:c0d9   AD 12 D0   LDA $D012   ;[4]VIC raster bits 0~7
.C:c0dc   C9 32      CMP #$32    ;[2]test for raster before start of screen
.C:c0de   90 06      BCC $C0E6   ;[2]less than, do transfer
.C:c0e0   29 07      AND #$07    ;[2]isolate raster-in char bits (shifted!)
.C:c0e2   C9 03      CMP #$03    ;[2]test raster value, is it less?
.C:c0e4   90 F3      BCC $C0D9   ;[2]yes, wait for VIC
.C:c0e6   8E 00 DD   STX $DD00   ;[4]allow CLK and DATA high
.C:c0e9   A9 00      LDA #$00    ;[2]clear accumulator
.C:c0eb   A6 00      LDX $00     ;[3]waist time
.C:c0ed   EA         NOP         ;[2]
.C:c0ee   EA         NOP         ;[2]
.C:c0ef   EA         NOP         ;[2]
.C:c0f0   EA         NOP         ;[2]
(C:$c0f1) d
.C:c0f1   0D 00 DD   ORA $DD00   ;[4]merge first 2 bits
.C:c0f4   4A         LSR A       ;[2]shift down two bits
.C:c0f5   4A         LSR A       ;[2]
.C:c0f6   EA         NOP         ;[2]
.C:c0f7   0D 00 DD   ORA $DD00   ;[4]merge next 2 bits
.C:c0fa   4A         LSR A       ;[2]shift down two bits
.C:c0fb   4A         LSR A       ;[2]
.C:c0fc   EA         NOP         ;[2]
.C:c0fd   0D 00 DD   ORA $DD00   ;[4]merge another 2 bits
.C:c100   4A         LSR A       ;[2]shift down two bits
.C:c101   4A         LSR A       ;[2]
.C:c102   85 A5      STA $A5     ;[3]save low 6 bits
.C:c104   AD 00 DD   LDA $DD00   ;[4]get last 2 bits
.C:c107   09 10      ORA #$10    ;[2]pull CLK low, allow DATA high
.C:c109   8D 00 DD   STA $DD00   ;[4]update serial lines
.C:c10c   29 C0      AND #$C0    ;[2]isolate high 2 bits
.C:c10e   05 A5      ORA $A5     ;[3]merge low 6 bits
.C:c110   60         RTS         ;[6]

The subroutine takes 103 cycles (not counting JSR $C0C8) and assuming 'typical' VIC behavior: not in the border, screen not blanked, and not 'near' a VIC bad-line. The code is far from optimal as it needlessly waits for the VIC sometimes when it doesn't need to. The correct/better behavior (IMHO) would be to SBC #$32 instead of CMP #$32 at address $C0DC, and then do CMP #1 instead of CMP #3 at address $C0E2. (Such change would need to SEC before the SBC.)

When you include the time to store the data in RAM and loop for the next byte (like I do for the other fast loaders), you will see the actual byte-transfer time is about 131 micoseconds. This is more than twice as slow as most of the fast-loaders I've documented. In summary, it's much faster than standard ROM routines, but slow compared to other fast-loaders.

  Summary 
  • Blank screen: no
  • Interrupts allowed: no
  • Disk Header: standard
  • Directory structure: standard
  • Allow wildcard in filename: yes
  • File structure: standard
  • Sector structure: standard (256 data bytes)
  • Sector decoding time: 25.5 milliseconds (slow/ROM)
  • Head stepping speed: slow (uses ROM: about 15.0 milliseconds/half-track)
  • Disk → C64 transfer: medium (about 131 microseconds/byte)
  • C64 → Disk transfer (filename): slow/standard (about 1300 microseconds/byte)
  • C64 memory footprint: under 1.0K ($C000~C3A7)
  • Needs KERNAL: yes
  • Load $D000~DFFF: I/O registers
  • Alters User Port: yes
  • Requires Unit 8: no
  • Write file/sector: no
  • Other: for every file loaded, the C64 will (slow) transmit 512 bytes to the drive

© Hydrophilic.net, 2026