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
  Microprose Type 2 Copy-protection & Fast-loader 

This fast loader was used at least in Airborne Ranger and Project Stealth Fighter. What's interesting about this one is that not only does it transmit two bits at a time, but it transmits them in bursts of 4 bytes (32 bits)!

The first file on 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. This first file on disk is not a BASIC program, but interestingly the BASIC $302 vector is changed to enable "auto-run". In particular, the file loads at $2a7 in RAM and ends at $303. The vector at $302 (which controls BASIC user input) is set to point to $2a7 (which shouldn't be a surprise). Let's look at the code:

.C:02a7   A9 00      LDA #$00    ;black
.C:02a9   8D 20 D0   STA $D020   ;VIC border color
.C:02ac   A9 0B      LDA #$0B    ;blank screen
.C:02ae   8D 11 D0   STA $D011   ;VIC register
.C:02b1   20 84 FF   JSR $FF84   ;KERNAL initialize I/O chips
.C:02b4   A9 00      LDA #$00    ;disable
.C:02b6   20 90 FF   JSR $FF90   ;KERNAL messages (like LOADING)
.C:02b9   A9 08      LDA #$08    ;file 8
.C:02bb   A2 08      LDX #$08    ;device 8
.C:02bd   A0 00      LDY #$00    ;channel 0
.C:02bf   20 BA FF   JSR $FFBA   ;KERNAL SetLFS
.C:02c2   A2 FB      LDX #$FB    ;.YX = $2FB (filename address -> "C")
.C:02c4   A0 02      LDY #$02
.C:02c6   A9 01      LDA #$01    ;filename length
.C:02c8   20 BD FF   JSR $FFBD   ;KERNAL SetNam
.C:02cb   A2 00      LDX #$00    ;.YX = $4400 (load address)
.C:02cd   A0 44      LDY #$44
.C:02cf   A9 00      LDA #$00    ;load (not verify)
.C:02d1   20 D5 FF   JSR $FFD5   ;KERNAL Load (standard/slow)
.C:02d4   20 03 44   JSR $4403   ;setup fast-loader (see below)
.C:02d7   A9 00      LDA #$00    ;redundant
.C:02d9   20 90 FF   JSR $FF90   ;disable KERNAL messages
.C:02dc   A9 08      LDA #$08    ;file 8
.C:02de   A2 08      LDX #$08    ;device 8
.C:02e0   A0 00      LDY #$00    ;channel 0
.C:02e2   20 BA FF   JSR $FFBA   ;KERNAL SetLFS
.C:02e5   A2 FA      LDX #$FA    ;.YX = $2FA (filename address -> "T")
.C:02e7   A0 02      LDY #$02
.C:02e9   A9 01      LDA #$01    ;filename length
.C:02eb   20 BD FF   JSR $FFBD   ;KERNAL SetNam
.C:02ee   A2 00      LDX #$00    ;.YX = $800 (load address)
.C:02f0   A0 08      LDY #$08
.C:02f2   A9 00      LDA #$00    ;load (not verify)
.C:02f4   20 D5 FF   JSR $FFD5   ;KERNAL Load (fast)
.C:02f7   4C 00 08   JMP $0800   ;start program
>C:02fa  54 43 00 00  00 00 8b e3   TC......
>C:0302  a7 02                      ..

So far everything is pretty straight-forward. The standard (slow) KERNAL Load routine is used to load file "C" into $4400~45fe. This process takes about 3 seconds. Then the fast-load installer at $4403 is called. Finally fast-load is called to load program "T" and execution jumps to $800. Before looking at C64 fast-load code, let's see what the installer does:

.C:4403   A9 53      LDA #$53    ;redirect KERNAL Load
.C:4405   8D 30 03   STA $0330   ;to address $4453
.C:4408   A9 44      LDA #$44
.C:440a   8D 31 03   STA $0331
.C:440d   A9 08      LDA #$08    ;device 8
.C:440f   20 B1 FF   JSR $FFB1   ;KERNAL Listen
.C:4412   A9 6F      LDA #$6F    ;channel 15
.C:4414   20 93 FF   JSR $FF93   ;KERNAL Second
.C:4417   A0 00      LDY #$00    ;index string
;command write loop
.C:4419   B9 03 45   LDA $4503,Y ;read string
.C:441c   20 A8 FF   JSR $FFA8   ;KERNAL serial out
.C:441f   C8         INY         ;index next character
.C:4420   C0 0B      CPY #$0B    ;all done?
.C:4422   D0 F5      BNE $4419   ;no, loop

.C:4424   20 AE FF   JSR $FFAE   ;KERNAL unlisten (drive will execute command)
.C:4427   20 D0 45   JSR $45D0   ;pull DATA low, allow CLK high
.C:442a   2C 00 DD   BIT $DD00   ;test serial lines
.C:442d   70 FB      BVS $442A   ;wait while CLK high (until drive pulls it low)
.C:442f   60         RTS
>C:4503  4d 2d 45 05  02 20 42 d0   M-E.. B.
>C:450b  4c b0 07                   L..

That code is pretty simple. First it redirects the KERNAL Load routine from ROM to RAM address $4453 (called later, see below). Second it sends a command to the disk drive: Memory-Execute. Finally it allows the CLK line to go high and then waits for CLK line to go low. Now let's see what the drive is executing. It starts with code in the command buffer at $205:

.8:0205   20 42 D0   JSR $D042   ;read BAM (redundant)
.8:0208   4C B0 07   JMP $07B0   ;jump to code in BAM buffer
~
.8:07b0   A0 3B      LDY #$3B    ;60 bytes to copy
;copy loop
.8:07b2   B9 C4 07   LDA $07C4,Y ;copy following code to buffer $600
.8:07b5   99 C4 06   STA $06C4,Y 
.8:07b8   B9 0B 02   LDA $020B,Y ;copy mystery data to $150 (why?)
.8:07bb   99 50 01   STA $0150,Y
.8:07be   88         DEY         ;index prior, all done?
.8:07bf   10 F1      BPL $07B2   ;no, copy loop
.8:07c1   4C C4 06   JMP $06C4
~
.8:06c4   78         SEI         ;prevent controller from running
.8:06c5   A0 11      LDY #$11    ;18 bytes to copy
;copy loop
.8:06c7   B9 E2 07   LDA $07E2,Y ;read from BAM buffer
.8:06ca   99 00 00   STA $0000,Y ;write to controller RAM 
.8:06cd   88         DEY         ;index prior, all done?
.8:06ce   10 F7      BPL $06C7   ;no copy loop
.8:06d0   58         CLI         ;allow controller to run
;wait loop
.8:06d1   A0 04      LDY #$04    ;5 buffers to test
.8:06d3   A9 00      LDA #$00
;test loop
.8:06d5   19 00 00   ORA $0000,Y ;merge controller status for buffers $300 to $700
.8:06d8   88         DEY         ;next buffer, all checked?
.8:06d9   10 FA      BPL $06D5   ;no, test loop
.8:06db   C9 01      CMP #$01    ;all buffers report okay?
.8:06dd   D0 F2      BNE $06D1   ;no, wait loop
.8:06df   4C 00 03   JMP $0300
~
>8:07e2  80 80 80 01  80 01 12 0f   ........
>8:07ea  12 10 12 11  01 01 12 12   ........
>8:07f2  41 85                      A.

Besides copying some data, it mainly sets the track and sector numbers for 4 of the 5 buffers (all but buffer $600) and instructs the drive controller to read each corresponding sector:

  • track 18 sector 15 → buffer $300
  • track 18 sector 16 → buffer $400
  • track 18 sector 17 → buffer $500
  • track 18 sector 18 → buffer $700

Note this initialization code is stored in a normally unused portion of the disk-header, after BAM Side 1. That area is used by double-side disks, but this is a single-sided disk. No prevision is made for a read-error. If that happens the code will be stuck in an infinite loop. Loading those four sectors takes about one second. Anyway, let's see what happens after those four sectors are loaded into drive RAM, starting at $300:

.8:0300   78         SEI         ;disable interrupts
.8:0301   A9 08      LDA #$08    ;pull DATA low, allow CLK high
.8:0303   8D 00 18   STA $1800   ;update serial lines (signal not ready)
.8:0306   20 CD 03   JSR $03CD
~
.8:03cd   A0 C4      LDY #$C4    ;start index
;checksum loop1
.8:03cf   59 00 06   EOR $0600,Y ;calc checksum
.8:03d2   C8         INY         ;next index
.8:03d3   C0 F2      CPY #$F2    ;last index?
.8:03d5   D0 F8      BNE $03CF   ;no, checksum loop1
.8:03d7   A0 00      LDY #$00    ;zero index
.8:03d9   4C A7 03   JMP $03A7   ;continue initialization
~
;checksum loop2
.8:03a7   59 00 03   EOR $0300,Y ;build checksum
.8:03aa   59 00 04   EOR $0400,Y
.8:03ad   59 00 05   EOR $0500,Y
.8:03b0   59 00 07   EOR $0700,Y
.8:03b3   C8         INY         ;index next, all done?
.8:03b4   D0 F1      BNE $03A7   ;no, checksum loop2
.8:03b6   8D 45 03   STA $0345   ;store checksum ($6b)
.8:03b9   60         RTS
~
.8:0309   A0 00      LDY #$00    ;256 bytes per table (redundant)
;build loop
.8:030b   98         TYA         ;current index
.8:030c   99 00 06   STA $0600,Y ;build ID table
.8:030f   4A         LSR A       ;shift high nibble 
.8:0310   4A         LSR A       ;down to low nibble
.8:0311   4A         LSR A
.8:0312   4A         LSR A
.8:0313   99 00 02   STA $0200,Y ;build nibble-shift table
.8:0316   C8         INY         ;index next, all done?
.8:0317   D0 F2      BNE $030B   ;no, build loop
.8:0319   4C E5 03   JMP $03E5   ;wait for filename

So the first part of the initialization does two main things: calculate a checksum of the loaded code bytes, and build two tables in RAM (pages 2 and 6). Then it jumps to a wait loop, ready to receive a filename from the C64. Let's see what happens when the C64 tries to load a file:

.C:4453   20 3A 44   JSR $443A   ;prepare to send filename
~
.C:443a   20 D0 45   JSR $45D0   ;signal transmission start
~
.C:45d0   AD 00 DD   LDA $DD00   ;CIA2 port
.C:45d3   29 0F      AND #$0F    ;keep User-Port bit and VIC-Bank
.C:45d5   09 20      ORA #$20    ;pull DATA low, allow CLK high
.C:45d7   8D 00 DD   STA $DD00   ;update serial bus lines
.C:45da   78         SEI         ;disable interrupts
.C:45db   18         CLC
.C:45dc   60         RTS
~
.C:443d   AD 15 D0   LDA $D015   ;read sprite enable bits
.C:4440   8D FE 45   STA $45FE   ;save to re-enable after load
.C:4443   A0 00      LDY #$00    ;all sprites off (also reset index)
.C:4445   8C 15 D0   STY $D015   ;VIC sprite enable
.C:4448   84 90      STY $90     ;no error
;wait loop
.C:444a   C8         INY         ;delay about 5*256 = 1280 microseconds (about 20 rasters)
.C:444b   D0 FD      BNE $444A   ;delay loop (for any sprites to finish rendering)
.C:444d   A9 FF      LDA #$FF    ;key value = begin filename
.C:444f   20 8A 45   JSR $458A   ;fast send byte to C1541
.C:4452   60         RTS

The first part of the loader sets up the serial bus and disables sprites (which interfere with CPU timing) and waits so any enabled sprites can finish rendering. Although it waits long enough for a standard-display sprite, it needs to wait longer for one which is Y-expanded (i.e., potential bug). More importantly it sends a special byte ($FF) to the C1541 to indicate the start of a filename. Next the actual filename is transmitted:

;filename loop
.C:4456   B1 BB      LDA ($BB),Y ;read filename character
.C:4458   20 8A 45   JSR $458A   ;fast send byte to C1541
.C:445b   C8         INY         ;index next char
.C:445c   C6 B7      DEC $B7     ;count down #chars
.C:445e   D0 F6      BNE $4456   ;not all, filename loop

.C:4460   A9 00      LDA #$00    ;flag 'end of filename'
.C:4462   20 8A 45   JSR $458A   ;fast send byte to C1541
.C:4465   AD DD 45   LDA $45DD   ;User-Port bit and VIC bank bits (7 for initial load)
.C:4468   8D 00 DD   STA $DD00   ;allow CLK and DATA to go high
.C:446b   A9 EA      LDA #$EA    ;opcode NOP
.C:446d   AE A6 02   LDX $02A6   ;NTSC/PAL flag
.C:4470   F0 05      BEQ $4477   ;NTSC
.C:4472   A9 24      LDA #$24    ;opcode BIT zp
.C:4474   8D 28 45   STA $4528   ;update loader code
.C:4477   8D 41 45   STA $4541
.C:447a   8D 58 45   STA $4558
.C:447d   8D 6F 45   STA $456F
;wait loop
.C:4480   2C 00 DD   BIT $DD00   ;test serial lines; is CLK high?
.C:4483   70 FB      BVS $4480   ;yes, wait

Besides transmitting the filename to the disk drive, it also modifies its own code. For NTSC machines, the sequence NOP, NOP will appear in some places (about 4 microseconds) while for PAL machines (which are a tad slower) the sequence BIT $EA will appear in those places (about 3 microseconds). This should really be part of the initialization code, and not in the main loader code. Anyway, it creates a delay between the STA $DD00 and BIT $DD00 instructions. This allows the drive time to recognize the 'switch-over' between sending and receiving data.

Before we see what the C64 and C1541 do next, let's take a moment to look back at the how the C64 transmits the filename data to the C1541 (same code is used on both NTSC and PAL machines):

;fast transmit to C1541 (cycle times are in [brackets])
.C:458a   48         PHA         ;[3]save low nibble
.C:458b   4A         LSR A       ;[2]move high nibble down
.C:458c   4A         LSR A       ;[2]
.C:458d   4A         LSR A       ;[2]
.C:458e   4A         LSR A       ;[2]
.C:458f   AA         TAX         ;[2]index high nibble
.C:4590   AD 00 DD   LDA $DD00   ;[4]serial lines
.C:4593   29 07      AND #$07    ;[2]allow both CLK and DATA to go high
.C:4595   8D DD 45   STA $45DD   ;[4]save User port and VIC bank bits
.C:4598   2C 00 DD   BIT $DD00   ;[4] test serial lines, is CLK low?
.C:459b   50 FB      BVC $4598   ;[2] yes, wait (cycle time assumes no wait)
.C:459d   20 EF 45   JSR $45EF   ;[28 typical]wait for VIC if needed 
.C:45a0   AD DD 45   LDA $45DD   ;[4]allow CLK and DATA to go high
.C:45a3   8D 00 DD   STA $DD00   ;[4]update serial bus
.C:45a6   BD DF 45   LDA $45DF,X ;[4]decode high nibble
.C:45a9   0D DD 45   ORA $45DD   ;[4]merge VIC and User-port bits
.C:45ac   8D 00 DD   STA $DD00   ;[4]update serial lines
.C:45af   4A         LSR A       ;[2]shift top 2 bits down
.C:45b0   4A         LSR A       ;[2]
.C:45b1   29 F0      AND #$F0    ;[2]mask CLK and DATA bits (really $30)
.C:45b3   0D DD 45   ORA $45DD   ;[4]merge VIC and User-port bits
.C:45b6   8D 00 DD   STA $DD00   ;[4]update serial lines
.C:45b9   68         PLA         ;[4]low nibble
.C:45ba   29 0F      AND #$0F    ;[2]clear high nibble
.C:45bc   AA         TAX         ;[2]index low nibble
.C:45bd   BD DF 45   LDA $45DF,X ;[4]decode low nibble
.C:45c0   0D DD 45   ORA $45DD   ;[4]merge VIC and User-port bits
.C:45c3   8D 00 DD   STA $DD00   ;[4]update serial lines
.C:45c6   4A         LSR A       ;[2]shift top 2 bits down
.C:45c7   4A         LSR A       ;[2]
.C:45c8   29 F0      AND #$F0    ;[2]mask CLK and DATA bits 
.C:45ca   0D DD 45   ORA $45DD   ;[4]merge VIC and User-port bits
.C:45cd   8D 00 DD   STA $DD00   ;[4]update serial lines
.C:45d0   AD 00 DD   LDA $DD00   ;[4]read I/O bits
.C:45d3   29 0F      AND #$0F    ;[2]mask User-port and VIC bits
.C:45d5   09 20      ORA #$20    ;[2]pull DATA low
.C:45d7   8D 00 DD   STA $DD00   ;[4]update serial lines
.C:45da   78         SEI         ;[2]disable interrupts (redundant)
.C:45db   18         CLC         ;[2]clear carry (why?)
.C:45dc   60         RTS         ;[6]exit

So the byte transmit code typically takes about 147 microseconds per byte. This is much longer than the receive code (as we'll see below), but is much faster than the standard serial bus routines (over 500 microseconds). Next, let's see what the drive does with that filename:

.8:052e   A0 00      LDY #$00    ;reset filename index
;wait for filename start flag
.8:0530   20 63 05   JSR $0563   ;fast receive from C64
.8:0533   C9 FF      CMP #$FF    ;start of filename?
.8:0535   D0 F9      BNE $0530   ;no, wait for filename
;get filename loop
.8:0537   20 63 05   JSR $0563   ;fast receive from C64
.8:053a   99 50 01   STA $0150,Y ;save filename character; is it zero?
.8:053d   F0 03      BEQ $0542   ;yes, skip ahead
.8:053f   C8         INY         ;index next character
.8:0540   D0 F5      BNE $0537   ;always, get filename loop

.8:0542   A9 08      LDA #$08    ;pull CLK low, allow DATA high
.8:0544   8D 00 18   STA $1800   ;update serial lines
.8:0547   AD 50 01   LDA $0150   ;is first character of filename a zero?
.8:054a   F0 05      BEQ $0551   ;yes, skip ahead (uninstall loader)
.8:054c   A2 4E      LDX #$4E    ;??
.8:054e   A0 00      LDY #$00    ;reset index
.8:0550   60         RTS         ;return to $38e
;uninstall fast-loader
.8:0551   A0 00      LDY #$00    ;reset index
.8:0553   98         TYA         ;.A = 0
;clear RAM loop
.8:0554   99 00 02   STA $0200,Y 
.8:0557   99 00 01   STA $0100,Y
.8:055a   99 00 00   STA $0000,Y
.8:055d   C8         INY         ;index next, all done?
.8:055e   D0 F4      BNE $0554   ;no, clear RAM loop
.8:0560   4C 22 EB   JMP $EB22   ;yes, initialize RAM and enter command wait-loop

That code simply stores the filename at $150 in drive RAM and tests the first byte of the filename. If the filename is null, the drive code un-installs itself (resumes normal ROM code). Otherwise the code returns to the main loop:

.8:03e8   B9 50 01   LDA $0150,Y ;read first filename character
.8:03eb   C9 30      CMP #$30    ;is it ASCII zero?
.8:03ed   D0 02      BNE $03F1   ;no, skip ahead
.8:03ef   E8         INX         ;yes, skip over it (.X)
.8:03f0   C8         INY         ;skip (.Y)
.8:03f1   B9 50 01   LDA $0150,Y ;read first or second character of filename
.8:03f4   C9 3A      CMP #$3A    ;is it ASCII colon (:)?
.8:03f6   D0 01      BNE $03F9   ;no, skip ahead
.8:03f8   E8         INX         ;yes, skip over it (.X) [but not .Y = strange!]
.8:03f9   8E CA 04   STX $04CA   ;save 'start-of-filename' index
.8:03fc   4C 1C 03   JMP $031C   ;continue loader
~
.8:031c   8E F1 04   STX $04F1   ;save 'start-of-filename' index again!
.8:031f   4C 27 03   JMP $0327   ;continue loader
~
.8:0327   20 45 07   JSR $0745   ;transmit buffer $600 (ID table)

That part of the code checks if the filename begins with "0:" or simply ":" and skips past those characters (using X register) if so. The next thing it does is transmit a 256-byte table of data. It is a simple sequence of bytes running from 0 to 255 (in order). However, the hardware inversion of serial lines, and the bits for the UserPort line and VIC Bank cause the bytes to get scrambled. The C64 will read the scrambled version and build a decode table. This is a terribly slow way to build a decode table in my opinion, but maybe the code size is minimal?

Let's take a quick peek at how the C64 builds its decode table before we get back to the drive code:

;wait loop
.C:4480   2C 00 DD   BIT $DD00   ;is CLK high?
.C:4483   70 FB      BVS $4480   ;yes, wait loop
.C:4485   20 0E 45   JSR $450E   ;read 256 bytes into $4600 buffer (scrambled data)
.C:4488   A0 00      LDY #$00    ;reset index
;build decode table loop
.C:448a   BE 00 46   LDX $4600,Y ;read scrambled value
.C:448d   98         TYA         ;current index
.C:448e   9D 00 47   STA $4700,X ;save in decode table
.C:4491   C8         INY         ;next index, all done?
.C:4492   D0 F6      BNE $448A   ;no, decode loop

Not much to building the table, although transmitting 256 bytes is time-consuming. It needs to been done each time Load is called because the bits of the VIC Bank or User Port line might change. Anyway, let's see what the C1541 does now:

.8:032a   A9 EE      LDA #$EE    ;request byte-ready signal
.8:032c   8D 0C 1C   STA $1C0C   ;from the disk read/write head
.8:032f   A9 01      LDA #$01    ;desired sector
.8:0331   85 19      STA $19
.8:0333   A2 12      LDX #$12    ;desired track
.8:0335   86 22      STX $22
.8:0337   E8         INX         ;19 
.8:0338   86 58      STX $58     ;??
.8:033a   20 95 05   JSR $0595   ;move disk head and set bit-rate
.8:033d   4C B3 04   JMP $04B3   ;continue loader

That code simply activates the drive-head read electronics and moves the disk head to the directory track. Also track 18 sector 1 (start of directory) is set for loading. The C1541 continues by reading the directory and searching for the requested filename:

;directory sector loop
.8:04b3   20 E5 05   JSR $05E5   ;toggle drive light
.8:04b6   20 C7 03   JSR $03C7   ;read sector (from same track) into $600 buffer
.8:04b9   A9 03      LDA #$03    ;set pointer to $603 (track/sector of first file)
.8:04bb   85 58      STA $58
.8:04bd   A9 06      LDA #$06
.8:04bf   85 59      STA $59
;start filename test
.8:04c1   A0 02      LDY #$02    ;index filename (skip over track/sector)
;test filename loop
.8:04c3   B1 58      LDA ($58),Y ;read directory data
.8:04c5   C9 A0      CMP #$A0    ;end of directory filename?
.8:04c7   F0 27      BEQ $04F0   ;yes, check end of requrested filename
.8:04c9   D9 4E 01   CMP $014E,Y ;no, check current character
.8:04cc   D0 03      BNE $04D1   ;mismatch, skip to next directory entry
.8:04ce   C8         INY         ;ok, next character in filename
.8:04cf   D0 F2      BNE $04C3   ;always, test filename loop
;skip to next directory entry
.8:04d1   A5 58      LDA $58     ;directory pointer low
.8:04d3   18         CLC
.8:04d4   69 20      ADC #$20    ;size of directory entry
.8:04d6   85 58      STA $58     ;update pointer low, end of buffer?
.8:04d8   90 E7      BCC $04C1   ;no, start filename test
.8:04da   AD 01 06   LDA $0601   ;yes, get next sector of directory
.8:04dd   85 19      STA $19     ;set for loader
.8:04df   AD 00 06   LDA $0600   ;next track of directory, is it valid?
.8:04e2   D0 CF      BNE $04B3   ;yes, directory sector loop
;file not found 😞
.8:04e4   8D 01 06   STA $0601   ;zero second byte of buffer (first already zero)
.8:04e7   20 45 07   JSR $0745   ;transmit buffer to C64
.8:04ea   20 EE 05   JSR $05EE   ;turn off drive light
.8:04ed   4C 09 03   JMP $0309   ;build ID table and wait for filename
;check end of requested filename
.8:04f0   B9 4E 01   LDA $014E,Y ;read filename requested
.8:04f3   D0 DC      BNE $04D1   ;not the end, skip to next directory entry
;file found!
.8:04f5   A0 00      LDY #$00    ;index track#
.8:04f7   B1 58      LDA ($58),Y ;get starting track# of file
.8:04f9   85 22      STA $22     ;set for loader
.8:04fb   C8         INY         ;index next
.8:04fc   B1 58      LDA ($58),Y ;get starting sector# of file
.8:04fe   85 19      STA $19     ;set for loader

That code is pretty straight-forward. Interestingly is does not check the file type (DEL/PRG/REL/SEQ/USR). It also does not allow the use of wildcards in the filename request. If the file is not found, the buffer is transmitted to the C64 with the first two bytes set to zero, and then the code jumps back to build the ID table and wait for another filename. If the file is found, the starting track and sector values are extracted from the directory entry and stored in zero page addresses $22 and $19 respectively for the file loader.

Next we have the main C1541 loader loop:

;loop to load and transmit sectors
.8:0500   20 95 05   JSR $0595   ;move head to track and set bit-rate
;retry sector read
.8:0503   20 67 03   JSR $0367   ;read sector and decode it
.8:0506   AD FF 05   LDA $05FF   ;get leading byte
.8:0509   C9 07      CMP #$07    ;correct sector-data mark?
.8:050b   D0 F6      BNE $0503   ;no, retry sector read
.8:050d   AD 00 07   LDA $0700   ;get trailing byte (checksum)
.8:0510   A0 00      LDY #$00    ;reset index
;checksum loop
.8:0512   59 00 06   EOR $0600,Y ;calculate checksum remainder
.8:0515   C8         INY         ;next byte, all 256 data bytes?
.8:0516   D0 FA      BNE $0512   ;no, checksum loop
.8:0518   C9 00      CMP #$00    ;yes, is checksum valid?
.8:051a   D0 E7      BNE $0503   ;no, retry sector read
.8:051c   20 45 07   JSR $0745   ;yes, transmit $600 buffer to C64
.8:051f   AD 01 06   LDA $0601   ;file's next sector
.8:0522   85 19      STA $19     ;set for loader
.8:0524   AD 00 06   LDA $0600   ;file's next track, is it valid?
.8:0527   F0 C1      BEQ $04EA   ;no, exit (turn off drive light, build ID table, wait for filename)
.8:0529   85 22      STA $22     ;yes, set for loader
.8:052b   4C 00 05   JMP $0500   ;loop to load and transmit sectors

There's not much to the main loop. It first positions the drive-head over the correct track, then keeps trying to read the desired sector forever. Once the sector is read, it is transmitted to the C64. Finally if the next track is not zero, the loop repeats. (When next track# = zero we've reached the end of file.)

Before looking at how the C64 fast-loads the sector of data, let's see how the C1541 reads and decodes a sector:

.8:0367   20 D7 07   JSR $07D7   ;find and read sector header
;wait for first byte
.8:036a   50 FE      BVC $036A   ;wait for first byte
.8:036c   B8         CLV         ;ready for next
.8:036d   AD 01 1C   LDA $1C01   ;read byte from disk-head (and discard)
.8:0370   A0 00      LDY #$00    ;reset buffer index
;wait for second byte
.8:0372   50 FE      BVC $0372   ;wait for second byte
.8:0374   B8         CLV         ;ready for next
.8:0375   4C 7B 03   JMP $037B   ;skip ahead for third byte
.8:0378   20 47 03   JSR $0347   ;wait for sync mark (enter here to read normal directory sector)
;wait for third byte
.8:037b   50 FE      BVC $037B   ;wait for third byte
.8:037d   B8         CLV         ;ready for next
.8:037e   AD 01 1C   LDA $1C01   ;get byte from disk-head
.8:0381   8D FF 05   STA $05FF   ;save as leading byte
;wait for $600 buffer byte
.8:0384   50 FE      BVC $0384   ;wait for byte
.8:0386   B8         CLV         ;ready for next
.8:0387   AD 01 1C   LDA $1C01   ;read byte from disk-head
.8:038a   99 00 06   STA $0600,Y ;store in buffer (low)
.8:038d   C8         INY         ;index next, all done?
.8:038e   D0 F4      BNE $0384   ;no, wait for $600 buffer byte
.8:0390   A0 BC      LDY #$BC    ;index for $700 buffer (-68)
;wait for $700 buffer byte
.8:0392   50 FE      BVC $0392   ;wait for byte
.8:0394   B8         CLV         ;ready for next
.8:0395   AD 01 1C   LDA $1C01   ;read byte from disk-head
.8:0398   99 44 06   STA $0644,Y ;store in $700 buffer ($700~743)
.8:039b   C8         INY         ;index next and count, all done?
.8:039c   D0 F4      BNE $0392   ;no, wait for $700 buffer byte
.8:039e   4C FF 03   JMP $03FF   ;do sector decode

Interestingly that code discards/ignores the first two bytes which come after the sector header. Normally gap bytes and another sync-mark would occur before the data block, but not in this custom sector / copy protection. Next it reads 325 GCR bytes into RAM. A leading byte goes into $5FF. 256 bytes go to $600~6FF, and 68 bytes go to $700~743. These GCR bytes need to be decoded into 260 regular bytes. I'll post the code below but won't comment most of it; it's a bit-twiddling mess.

.8:03ff   A9 FF      LDA #$FF    ;set read and write pointers to $5FF
.8:0401   85 52      STA $52     ;read low
.8:0403   85 54      STA $54     ;write low
.8:0405   A9 05      LDA #$05
.8:0407   85 53      STA $53     ;read high
.8:0409   85 55      STA $55     ;write high
.8:040b   A9 41      LDA #$41    ;65 groups of 5 GCR bytes = 325 GCR bytes
.8:040d   85 57      STA $57     ;set counter (translate to $104 regular bytes)
;decode loop
.8:040f   A0 00      LDY #$00    ;reset index
.8:0411   B1 52      LDA ($52),Y
.8:0413   48         PHA
.8:0414   4A         LSR A
.8:0415   4A         LSR A
.8:0416   4A         LSR A
.8:0417   AA         TAX
.8:0418   BD A0 F8   LDA $F8A0,X
.8:041b   85 56      STA $56
.8:041d   C8         INY
.8:041e   B1 52      LDA ($52),Y
.8:0420   0A         ASL A
.8:0421   AA         TAX
.8:0422   68         PLA
.8:0423   2A         ROL A
.8:0424   48         PHA
.8:0425   8A         TXA
.8:0426   0A         ASL A
.8:0427   68         PLA
.8:0428   2A         ROL A
.8:0429   29 1F      AND #$1F
.8:042b   88         DEY
.8:042c   AA         TAX
.8:042d   BD C0 F8   LDA $F8C0,X
.8:0430   05 56      ORA $56
.8:0432   91 54      STA ($54),Y
.8:0434   C8         INY
.8:0435   B1 52      LDA ($52),Y
.8:0437   4A         LSR A
.8:0438   08         PHP
.8:0439   29 1F      AND #$1F
.8:043b   AA         TAX
.8:043c   BD A0 F8   LDA $F8A0,X
.8:043f   85 56      STA $56
.8:0441   C8         INY
.8:0442   B1 52      LDA ($52),Y
.8:0444   28         PLP
.8:0445   6A         ROR A
.8:0446   4A         LSR A
.8:0447   4A         LSR A
.8:0448   4A         LSR A
.8:0449   AA         TAX
.8:044a   BD C0 F8   LDA $F8C0,X
.8:044d   05 56      ORA $56
.8:044f   88         DEY
.8:0450   91 54      STA ($54),Y
.8:0452   C8         INY
.8:0453   B1 52      LDA ($52),Y
.8:0455   AA         TAX
.8:0456   C8         INY
.8:0457   B1 52      LDA ($52),Y
.8:0459   0A         ASL A
.8:045a   8A         TXA
.8:045b   2A         ROL A
.8:045c   29 1F      AND #$1F
.8:045e   AA         TAX
.8:045f   BD A0 F8   LDA $F8A0,X
.8:0462   85 56      STA $56
.8:0464   B1 52      LDA ($52),Y
.8:0466   4A         LSR A
.8:0467   4A         LSR A
.8:0468   29 1F      AND #$1F
.8:046a   AA         TAX
.8:046b   BD C0 F8   LDA $F8C0,X
.8:046e   05 56      ORA $56
.8:0470   88         DEY
.8:0471   91 54      STA ($54),Y
.8:0473   C8         INY
.8:0474   B1 52      LDA ($52),Y
.8:0476   0A         ASL A
.8:0477   29 06      AND #$06
.8:0479   85 56      STA $56
.8:047b   C8         INY
.8:047c   B1 52      LDA ($52),Y
.8:047e   29 E0      AND #$E0
.8:0480   05 56      ORA $56
.8:0482   AA         TAX
.8:0483   BD 00 03   LDA $0300,X
.8:0486   85 56      STA $56
.8:0488   B1 52      LDA ($52),Y
.8:048a   29 1F      AND #$1F
.8:048c   AA         TAX
.8:048d   BD C0 F8   LDA $F8C0,X
.8:0490   05 56      ORA $56
.8:0492   88         DEY
.8:0493   91 54      STA ($54),Y
.8:0495   A5 52      LDA $52     ;read pointer low
.8:0497   18         CLC
.8:0498   69 05      ADC #$05    ;advance by five
.8:049a   85 52      STA $52     ;update read low
.8:049c   90 02      BCC $04A0   ;any carry?
.8:049e   E6 53      INC $53     ;yes, update read high
.8:04a0   A5 54      LDA $54     ;write pointer low
.8:04a2   18         CLC
.8:04a3   69 04      ADC #$04    ;advance by four
.8:04a5   85 54      STA $54     ;update write low
.8:04a7   90 02      BCC $04AB   ;any carry?
.8:04a9   E6 55      INC $55     ;yes, update write high
.8:04ab   C6 57      DEC $57     ;count down
.8:04ad   F0 03      BEQ $04B2   ;all done?
.8:04af   4C 0F 04   JMP $040F   ;no, loop to decode
.8:04b2   60         RTS         ;yes, exit

If I counted correctly, there are 289 cycles (typical) per loop. There are 65 loops so that's 18,785 cycles plus 21 cycles for initialization. That's a bit longer than a full NTSC VIC-II screen (a bit shorter than a PAL VIC-II screen). Although it's very time consuming, it's considerably faster than the C1541 ROM routines.

Now let's look at how the NTSC C64 fast-loads each decoded sector into RAM $4600:

.C:450e   A0 3F      LDY #$3F    ;max index (64 loops)
;wait for CLK high (cycle times in [brackets])
.C:4510   AD 00 DD   LDA $DD00   ;test serial lines
.C:4513   10 D5      BPL $44EA   ;abort load if DATA low
.C:4515   09 30      ORA #$30    ;pull CLK and DATA low (eventually)
.C:4517   AA         TAX         ;save for later
.C:4518   29 40      AND #$40    ;test CLK status
.C:451a   F0 F4      BEQ $4510   ;low, loop
;loop to get 4 bytes [52] (cycle time in [brackets])
.C:451c   20 EF 45   JSR $45EF   ;[28]wait for VIC if needed
.C:451f   8E 00 DD   STX $DD00   ;[4]pull CLK and DATA low
.C:4522   8A         TXA         ;[2]CIA bits
.C:4523   29 03      AND #$03    ;[2]mask VIC bank -- Bug! Should be AND #7
.C:4525   8D 00 DD   STA $DD00   ;[4]update serial bus (and alter User-port line!)
.C:4528   C6 B7      DEC $B7     ;[5]waste 12 cycles
.C:452a   48         PHA         ;[3]
.C:452b   68         PLA         ;[4]
;receive byte 1 [37]
.C:452c   AD 00 DD   LDA $DD00   ;[4] get 2 bits
.C:452f   4A         LSR A       ;[2] shift down 2 bits
.C:4530   4A         LSR A       ;[2]
.C:4531   4D 00 DD   EOR $DD00   ;[4] merge 2 more bits
.C:4534   4A         LSR A       ;[2] shift down 2 bits
.C:4535   4A         LSR A       ;[2]
.C:4536   4D 00 DD   EOR $DD00   ;[4] merge 2 more bits
.C:4539   4A         LSR A       ;[2] shfit down 2 bits
.C:453a   4A         LSR A       ;[2]
.C:453b   4D 00 DD   EOR $DD00   ;[4] merge last 2 bits
.C:453e   99 00 46   STA $4600,Y ;[5] save in buffer
.C:4541   EA         NOP         ;[2] waste 4 cycles
.C:4542   EA         NOP         ;[2]
;receive byte 2 [37]
.C:4543   AD 00 DD   LDA $DD00
.C:4546   4A         LSR A
.C:4547   4A         LSR A
.C:4548   4D 00 DD   EOR $DD00
.C:454b   4A         LSR A
.C:454c   4A         LSR A
.C:454d   4D 00 DD   EOR $DD00
.C:4550   4A         LSR A
.C:4551   4A         LSR A
.C:4552   4D 00 DD   EOR $DD00
.C:4555   99 40 46   STA $4640,Y ;save in buffer
.C:4558   EA         NOP
.C:4559   EA         NOP
;receive byte 3 [37]
.C:455a   AD 00 DD   LDA $DD00
.C:455d   4A         LSR A
.C:455e   4A         LSR A
.C:455f   4D 00 DD   EOR $DD00
.C:4562   4A         LSR A
.C:4563   4A         LSR A
.C:4564   4D 00 DD   EOR $DD00
.C:4567   4A         LSR A
.C:4568   4A         LSR A
.C:4569   4D 00 DD   EOR $DD00
.C:456c   99 80 46   STA $4680,Y ;save in buffer
.C:456f   EA         NOP
.C:4570   EA         NOP
;receive byte 4 [38]
.C:4571   AD 00 DD   LDA $DD00
.C:4574   4A         LSR A
.C:4575   4A         LSR A
.C:4576   4D 00 DD   EOR $DD00
.C:4579   4A         LSR A
.C:457a   4A         LSR A
.C:457b   4D 00 DD   EOR $DD00
.C:457e   4A         LSR A
.C:457f   4A         LSR A
.C:4580   4D 00 DD   EOR $DD00
.C:4583   99 C0 46   STA $46C0,Y ;save in buffer
.C:4586   88         DEY         ;[2]index next, all done?
.C:4587   10 93      BPL $451C   ;[3]loop to get 4 bytes
.C:4589   60         RTS         ;not included in cycle time

So once the drive is ready, the code reads 4 bytes in 52+37+37+37+38 = 201 cycles which averages out to about 50 cycles per byte -- assuming typical VIC timing. However you (or rather the C64) won't get typical VIC timing over the full 256-byte transfer! Naively this would take about 256 x 50 = 12,800 cycles for the full sector, but testing in VICE indicates a typical time of 15,300 cycles. Based on the VICE value, it works out to about 60 cycles per byte! (20% more than a naive calculation.) At any rate, the full sector is transferred in less than a full VIC screen (NTSC or PAL).

I always assumed this code was using the clunky 'scrambled sector' code so that it would avoid altering (corrupting) the User-Port line ($DD00 bit 2), but due to a bug at $4523, this bit gets cleared (corrupted) anyway! Alas, so much effort down the drain!

Even though the C64 has the sector in RAM, it is scrambled and needs to be decoded (unscrambled). This will take more time; let's look at the code:

.C:4494   20 0E 45   JSR $450E   ;read scrambled sector into $4600~46FF
.C:4497   A5 B9      LDA $B9     ;custom load address?
.C:4499   F0 10      BEQ $44AB   ;yes, skip ahead
.C:449b   AE 02 46   LDX $4602   ;no, get scrambled address-low
.C:449e   BD 00 47   LDA $4700,X ;decode
.C:44a1   85 C3      STA $C3     ;set address low
.C:44a3   AE 03 46   LDX $4603   ;get scrambled address-high
.C:44a6   BD 00 47   LDA $4700,X ;decode
.C:44a9   85 C4      STA $C4     ;set address high
.C:44ab   A2 04      LDX #$04    ;starting offset (first sector)
.C:44ad   AC 00 46   LDY $4600   ;get scrambled next-track
.C:44b0   B9 00 47   LDA $4700,Y ;decode, last sector?
.C:44b3   F0 18      BEQ $44CD   ;yes, skip ahead
.C:44b5   4C C5 44   JMP $44C5   ;no, enter main loop
;main file loop
.C:44b8   20 0E 45   JSR $450E   ;read scrambled sector into $4600~46FF
.C:44bb   A2 02      LDX #$02    ;starting offset (not first sector)
.C:44bd   AC 00 46   LDY $4600   ;get scrambled track#
.C:44c0   B9 00 47   LDA $4700,Y ;decode, is it zero?
.C:44c3   F0 08      BEQ $44CD   ;yes, last sector
;full sector byte loop
.C:44c5   20 F1 44   JSR $44F1   ;decode byte and write to RAM
.C:44c8   D0 FB      BNE $44C5   ;not all of sector, full sector byte loop
.C:44ca   4C B8 44   JMP $44B8   ;main file loop
;last sector
.C:44cd   20 D0 45   JSR $45D0   ;pull DATA low, allow CLK high
.C:44d0   AC 01 46   LDY $4601   ;get scrambled byte count
.C:44d3   B9 00 47   LDA $4700,Y ;decode, is it zero?
.C:44d6   F0 12      BEQ $44EA   ;yes, file not found exit
.C:44d8   85 B7      STA $B7     ;bytes in last sector
;last sector byte loop
.C:44da   20 F1 44   JSR $44F1   ;decode byte and write to RAM
.C:44dd   C6 B7      DEC $B7     ;count down, finished?
.C:44df   D0 F9      BNE $44DA   ;no, last sector byte loop
.C:44e1   18         CLC         ;flag no error
;restore sprites and exit
.C:44e2   AD FE 45   LDA $45FE   ;get sprite enable bits
.C:44e5   8D 15 D0   STA $D015   ;update VIC 
.C:44e8   58         CLI         ;enable enterupts
.C:44e9   60         RTS         ;exit loader
;file not found exit
.C:44ea   A9 42      LDA #$42    ;error code
.C:44ec   85 90      STA $90
.C:44ee   38         SEC         ;flag error
.C:44ef   D0 F1      BNE $44E2   ;always, restore sprites and exit
;decode byte and write to RAM
.C:44f1   BC 00 46   LDY $4600,X ;get scrambled data byte
.C:44f4   B9 00 47   LDA $4700,Y ;unscramble
.C:44f7   A0 00      LDY #$00    ;no index
.C:44f9   91 C3      STA ($C3),Y ;write to RAM
.C:44fb   E6 C3      INC $C3     ;index RAM pointer low
.C:44fd   D0 02      BNE $4501   ;carry?
.C:44ff   E6 C4      INC $C4     ;yes, index RAM pointer high
.C:4501   E8         INX         ;index next byte in sector buffer
.C:4502   60         RTS
  Summary 
  • Blank screen: no
  • Interrupts allowed: no
  • Disk Header: modified (code at $7B0)
  • Directory structure: standard
  • Allow wildcard in filename: no
  • File structure: modified (last sector stores #bytes remaining)
  • Sector structure: custom (missing data-block sync; 256 data bytes)
  • Sector decoding time: 18.8 milliseconds
  • Head stepping speed: slow (about 12.5 milliseconds/half-track)
  • Disk → C64 transfer: fast (nominal 50 microseconds/byte)
  • C64 → Disk transfer (filename): fast (about 147 microseconds/byte)
  • C64 memory footprint: 1.0K ($4400~47FF)
  • Needs KERNAL: no
  • Load $D000~DFFF: I/O registers
  • Alters User Port: yes
  • Requires Unit 8: yes
  • Write file/sector: no
  • Other: every file (re)transmits a 256-byte decode table

© Hydrophilic.net, 2026