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

  Project Firestart Fast-load & Slow-save 

This fast-loader (and slow/"standard" saver) was created by me for two reasons. First, I had never written a fast-loader for the C1581 before (and I haven't written another since). Second, I was annoyed with all the disk-swapping and mediocre performance of the "fast" loader in the game Project Firestart by Electronic Arts / Dynamix. Like the original, my version of the game does not have an auto-run program to boot; the user has to type RUN and press Return. Unlike the original, you do not need a separate disk to save your game.

Let's take a look at some of the initialization code:

.C:65a2   A0 00      LDY #$00    ;index destination
;outer loop for bytes
.C:65a4   A5 BA      LDA $BA     ;disk unit (typically 8)
.C:65a6   20 B1 FF   JSR $FFB1   ;KERNAL Listen
.C:65a9   A9 6F      LDA #$6F    ;channel 15
.C:65ab   20 93 FF   JSR $FF93   ;KERNAL Second
.C:65ae   A2 02      LDX #$02    ;3 characters
;loop for string
.C:65b0   BD 2A 60   LDA $602A,X ;read string "M-W"
.C:65b3   20 A8 FF   JSR $FFA8   ;KERNAL Serial out
.C:65b6   CA         DEX         ;next char, all done?
.C:65b7   10 F7      BPL $65B0   ;no, loop for string
.C:65b9   98         TYA         ;destination low
.C:65ba   20 A8 FF   JSR $FFA8   ;KERNAL Serial out 
.C:65bd   A9 03      LDA #$03    ;destination high
.C:65bf   20 A8 FF   JSR $FFA8   ;KERNAL Serial out
.C:65c2   A2 20      LDX #$20    ;32 bytes to write per chunk
.C:65c4   8A         TXA         ;move count to .A
.C:65c5   20 A8 FF   JSR $FFA8   ;KERNAL Serial out
;inner loop for bytes
.C:65c8   B9 2D 60   LDA $602D,Y ;read fast-load code
.C:65cb   20 A8 FF   JSR $FFA8   ;KERNAL Serial out
.C:65ce   C8         INY         ;next destination address low
.C:65cf   CA         DEX         ;countdown chunk bytes, all done?
.C:65d0   D0 F6      BNE $65C8   ;no, inner loop for bytes
.C:65d2   20 AE FF   JSR $FFAE   ;KERNAL Unlisten (drive will execute command)
.C:65d5   98         TYA         ;test address low, wrap to zero?
.C:65d6   D0 CC      BNE $65A4   ;no, outer loop for bytes

.C:65d8   A5 BA      LDA $BA     ;disk unit (typically 8)
.C:65da   20 B1 FF   JSR $FFB1   ;KERNAL Listen
.C:65dd   A9 6F      LDA #$6F    ;channel 15
.C:65df   20 93 FF   JSR $FF93   ;KERNAL Second
;loop to exec string
.C:65e2   B9 2D 61   LDA $612D,Y ;read string "M-E..."
.C:65e5   20 A8 FF   JSR $FFA8   ;KERNAL Serial out
.C:65e8   C8         INY         ;index string
.C:65e9   C0 1D      CPY #$1D    ;all 29 bytes?
.C:65eb   D0 F5      BNE $65E2   ;no loop to exec string
.C:65ed   20 AE FF   JSR $FFAE   ;KERNAL Unlisten (drive will execute command)

That code transfers 256 bytes of "fast-loader" code to the C1581 (at $300~3FF) using memory-write (M-W) commands. A single command can't transfer that many bytes, so the data is sent in chunks of 32 bytes each. This process takes less than half a second and is only done once.

After the code is transferred, a memory-execute (M-E) command is sent to the drive, but it doesn't execut the code just transferred; instead it executes a small code fragment in the parser buffer:

.8:0205   A0 00      LDY #$00    ;index destination
.8:0207   84 0E      STY $0E     ;index source
;loop to build table
.8:0209   A6 0E      LDX $0E     ;source index (0 to 15)
.8:020b   BD 00 03   LDA $0300,X ;read encoding value
.8:020e   A2 10      LDX #$10    ;16 copies to make
;loop to copy byte
.8:0210   99 00 04   STA $0400,Y ;write to look-up table
.8:0213   C8         INY         ;next destination
.8:0214   CA         DEX         ;countdown, all copies made?
.8:0215   D0 F9      BNE $0210   ;loop to copy byte
.8:0217   E6 0E      INC $0E     ;next source
.8:0219   98         TYA         ;test destination, all done?
.8:021a   D0 ED      BNE $0209   ;no, loop to build table
.8:021c   60         RTS
;encoding value for nibble
>8:0300  1f 17 1d 15  1b 13 19 11  1e 16 1c 14  1a 12 18 10
;result of that code (look-up for high-nibble)
>8:0400  1f 1f 1f 1f  1f 1f 1f 1f  1f 1f 1f 1f  1f 1f 1f 1f
>8:0410  17 17 17 17  17 17 17 17  17 17 17 17  17 17 17 17
>8:0420  1d 1d 1d 1d  1d 1d 1d 1d  1d 1d 1d 1d  1d 1d 1d 1d
>8:0430  15 15 15 15  15 15 15 15  15 15 15 15  15 15 15 15
>8:0440  1b 1b 1b 1b  1b 1b 1b 1b  1b 1b 1b 1b  1b 1b 1b 1b
>8:0450  13 13 13 13  13 13 13 13  13 13 13 13  13 13 13 13
>8:0460  19 19 19 19  19 19 19 19  19 19 19 19  19 19 19 19
>8:0470  11 11 11 11  11 11 11 11  11 11 11 11  11 11 11 11
>8:0480  1e 1e 1e 1e  1e 1e 1e 1e  1e 1e 1e 1e  1e 1e 1e 1e
>8:0490  16 16 16 16  16 16 16 16  16 16 16 16  16 16 16 16
>8:04a0  1c 1c 1c 1c  1c 1c 1c 1c  1c 1c 1c 1c  1c 1c 1c 1c
>8:04b0  14 14 14 14  14 14 14 14  14 14 14 14  14 14 14 14
>8:04c0  1a 1a 1a 1a  1a 1a 1a 1a  1a 1a 1a 1a  1a 1a 1a 1a
>8:04d0  12 12 12 12  12 12 12 12  12 12 12 12  12 12 12 12
>8:04e0  18 18 18 18  18 18 18 18  18 18 18 18  18 18 18 18
>8:04f0  10 10 10 10  10 10 10 10  10 10 10 10  10 10 10 10

That code builds a 256-byte look-up table based on a 16-byte encoding table stored in $300~30F.

Returning to the installer, it next makes 2 minor changes to the C64 loader if the machine is PAL (not shown).

Now when the program wants to load a file it executes code like this:


.C:663e   AD 15 D0   LDA $D015   ;get sprite enables, any on?
.C:6641   85 E2      STA $E2     ;(save sprite enables)
.C:6643   F0 08      BEQ $664D   ;none on, skip border test
.C:6645   8C 15 D0   STY $D015   ;disable all sprites
;wait for border
.C:6648   AD 11 D0   LDA $D011   ;test high bit, in VIC border?
.C:664b   10 FB      BPL $6648   ;no, wait for border
.C:664d   A9 0F      LDA #$0F    ;secondary address 15 (command channel)
.C:664f   85 B9      STA $B9     ;set it
;try again
.C:6651   20 28 02   JSR $0228   ;call custom Listen and Second
.C:6654   B0 FB      BCS $6651   ;error then try again
.C:6656   A0 05      LDY #$05    ;5 chars to transmit
;loop for command string
.C:6658   B9 A6 02   LDA $02A6,Y ;read 'M-E' string (exec $3C9)
.C:665b   20 AC 02   JSR $02AC   ;custom serial byte out
.C:665e   B0 F1      BCS $6651   ;error then try again
.C:6660   88         DEY         ;countdown, all string sent?
.C:6661   D0 F5      BNE $6658   ;no loop for command string
.C:6663   8C 68 01   STY $0168   ;file address high (will point to $00AE)
.C:6666   A9 09      LDA #$09    ;maximum bytes to transmit in burst
.C:6668   20 AC 02   JSR $02AC   ;custom serial byte out
.C:666b   B0 E4      BCS $6651   ;error then try again
;loop for filename
.C:666d   B1 E5      LDA ($E5),Y ;read null-terminated filename
.C:666f   F0 08      BEQ $6679   ;if null exit loop
.C:6671   20 AC 02   JSR $02AC   ;custom serial out
.C:6674   B0 DB      BCS $6651   ;error then try again
.C:6676   C8         INY         ;next char of filename
.C:6677   D0 F4      BNE $666D   ;always, loop for filename
.C:6679   20 E9 02   JSR $02E9   ;call custom Unlisten (drive will execute command)
.C:667c   B0 D3      BCS $6651   ;error then try again
.C:667e   A2 AD      LDX #$AD    ;opcode LDA absolute
.C:6680   8E 35 01   STX $0135   ;prepare loader (self modifying code)
.C:6683   20 9B 01   JSR $019B   ;do fast-load

The code above first saves any sprite enables; if any are enabled they are disabled and the code waits for the VIC to be in the border. Next it calls a RAM version of the KERNAL Listen and Second routines. These are essentially the same as standard ROM so aren't shown. Next it sends a string to the C1581: a memory-execute (M-E) command along with the maximum # bytes to transfer per loop and a filename. Then it calls the RAM version of KERNAL Unlisten to make the drive execute the command. Finally it modifies (prepares) the main loader code before calling it.

Before looking at the drive code and main loader, lets look at the custom Serial out routine. The standard ROM version takes over 8 rasters to execute which would inerfere with the game's IRQ system. (Yes, this loader allows IRQs.) So this custom version sends the data one nibble at a time:

.C:02ac   24 94      BIT $94     ;any character in buffer?
.C:02ae   10 09      BPL $02B9   ;no, buffer character
.C:02b0   48         PHA         ;yes, save new char
.C:02b1   20 20 01   JSR $0120   ;sync with VIC
.C:02b4   20 50 03   JSR $0350   ;send two nibbles (the old, buffered character)
.C:02b7   58         CLI         ;enable interrupts
.C:02b8   68         PLA         ;new char
;buffer char
.C:02b9   85 95      STA $95     ;store in buffer
.C:02bb   38         SEC         ;set high bit of $94
.C:02bc   66 94      ROR $94     ;(i.e., flag character in buffer)
.C:02be   A5 90      LDA $90     ;get KERNAL Status
.C:02c0   C9 01      CMP #$01    ;set carry if not zero
.C:02c2   60         RTS
~
.C:0350   AD 00 DD   LDA $DD00   ;is DATA high?
.C:0353   30 AE      BMI $0303   ;yes, wait for device ready
.C:0355   20 22 02   JSR $0222   ;allow CLK high
.C:0358   10 0A      BPL $0364   ;always, check EOI
;do EOI
.C:035a   2C 00 DD   BIT $DD00   ;is DATA high?
.C:035d   30 FB      BMI $035A   ;yes, wait for EOI acknowledgment
.C:035f   20 20 01   JSR $0120   ;sync with VIC (i.e., delay)
.C:0362   46 A3      LSR $A3     ;clear EOI flag
;wait for drive (check EOI)
.C:0364   AD 00 DD   LDA $DD00   ;is DATA low?
.C:0367   10 FB      BPL $0364   ;yes, wait for drive
.C:0369   24 A3      BIT $A3     ;check is EOI needed?
.C:036b   30 ED      BMI $035A   ;yes, do EOI (once)
.C:036d   20 1F 02   JSR $021F   ;pull CLK low
.C:0370   20 C8 02   JSR $02C8   ;send 4 bits
.C:0373   20 20 01   JSR $0120   ;sync with VIC
.C:0376   20 C8 02   JSR $02C8   ;send 4 bits
;wait for acceptance
.C:0379   AD 00 DD   LDA $DD00   ;get serial lines
.C:037c   C9 80      CMP #$80    ;is DATA high?
.C:037e   B0 F9      BCS $0379   ;yes, wait for acceptance
.C:0380   60         RTS         ;exit carry clear
~
.C:02c8   A9 04      LDA #$04    ;four bits to send
.C:02ca   85 A5      STA $A5     ;set count
;wait DATA high
.C:02cc   AD 00 DD   LDA $DD00   ;is DATA low?
.C:02cf   10 2D      BPL $02FE   ;yes, wait DATA high
.C:02d1   46 95      LSR $95     ;data bit to carry
.C:02d3   B0 02      BCS $02D7   ;send a 0 bit?
.C:02d5   09 20      ORA #$20    ;yes, DATA low (hardware inversion)
.C:02d7   20 22 02   JSR $0222   ;allow CLK high
.C:02da   20 E8 02   JSR $02E8   ;delay 12 cycles
.C:02dd   29 DF      AND #$DF    ;allow DATA high
.C:02df   09 10      ORA #$10    ;pull CLK low
.C:02e1   8D 00 DD   STA $DD00   ;update serial lines
.C:02e4   C6 A5      DEC $A5     ;countdown bits, all done?
.C:02e6   D0 E4      BNE $02CC   ;no, wait DATA high
.C:02e8   60         RTS

That code (like the KERNAL) impliments a one-byte buffer for serial data. It begins by testing if there is anything in the buffer; if so, the buffered character is transmitted. Either way the desired (.A) byte is stored in the buffer (for then next call to Serial Out or Unlisten).

The transmit buffered character part ($350) first ensures DATA is low then allows CLK to go high. Next it waits for DATA to go high and checks the EOI-flag. (This flag is set during Unlisten.) If EOI is needed, the code "backs up" and waits for DATA to go low again (EOI handshake), delays by syncing with VIC, clears the EOI flag, and falls back into the 'main' wait-for-drive code. Either way it then pulls CLK low, sends four bits, waits on the VIC, sends four more bits, and waits for the drive to acknowledge the byte.

Whew, it's a lot of work to implement the slow-serial protocol! If I were to do this project over, I would instead implement a custom serial-write routine (which would also require more code in the C1581). As it stands, the write-byte routine takes 16 rasters or about 1.0 millisecond per typical byte. An EOI-byte takes 24 rasters or about 1.5 milliseconds.

Anyway, let's now look at what the C1581 does with the filename:

.8:03c9   78         SEI         ;disable interrupts
.8:03ca   A9 98      LDA #$98    ;pull CLK low, allow DATA high
.8:03cc   8D 01 40   STA $4001   ;update serial lines
.8:03cf   85 11      STA $11     ;flag send load address
.8:03d1   A9 06      LDA #$06    ;address low 
.8:03d3   8D 91 02   STA $0291   ;filename start
.8:03d6   EE 2F 02   INC $022F   ;fix filename length
.8:03d9   20 B9 82   JSR $82B9   ;ROM find file
.8:03dc   AD 97 02   LDA $0297   ;get track#
.8:03df   F0 0F      BEQ $03F0   ;file not found, exit
.8:03e1   85 4D      STA $4D     ;set track# for DOS
.8:03e3   AD 9C 02   LDA $029C   ;found sector#
.8:03e6   85 4E      STA $4E     ;set for DOS
.8:03e8   A5 F4      LDA $F4     ;found filetype
.8:03ea   29 27      AND #$27    ;mask type
.8:03ec   C9 02      CMP #$02    ;is it PRG?
.8:03ee   F0 A9      BEQ $0399   ;yes, continue load (read block)
~
;Read Block
.8:0399   20 94 9D   JSR $9D94   ;ROM read sector
.8:039c   C9 02      CMP #$02    ;did an error occur?
.8:039e   B0 50      BCS $03F0   ;yes, exit
.8:03a0   20 CE 9D   JSR $9DCE   ;ROM initialize buffer pointer
.8:03a3   A0 04      LDY #$04    ;index+1 of load address high
.8:03a5   A6 11      LDX $11     ;should we send load-address?
.8:03a7   30 0B      BMI $03B4   ;yes, do it (set last index)
;now do data (return here after sending load-address)
.8:03a9   A0 00      LDY #$00    ;index+1 of last byte (i.e., assume full buffer)
.8:03ab   A6 4D      LDX $4D     ;get track#, last block?
.8:03ad   86 11      STX $11     ;(clear load-address flag)
.8:03af   D0 03      BNE $03B4   ;no, set last index
.8:03b1   A4 4E      LDY $4E     ;yes get index of last byte
.8:03b3   C8         INY         ;calc index+1
;set 'last index'
.8:03b4   84 10      STY $10     ;save "last index"
.8:03b6   A9 02      LDA #$02    ;start index
.8:03b8   85 0F      STA $0F     ;flag buffer plenty
.8:03ba   D0 B9      BNE $0375   ;always, prepare next burst
~ 
;Prepare Next Burst
.8:0375   A2 17      LDX #$17    ;value for pull DATA low, allow CLK high
.8:0377   A5 0F      LDA $0F     ;buffer flag, is it empty?
.8:0379   F0 11      BEQ $038C   ;yes, check for new block
.8:037b   8E 01 40   STX $4001   ;update serial lines
.8:037e   10 90      BPL $0310   ;normal, calculate # bytes to transmit
.8:0380   49 FF      EOR #$FF    ;calc bytes remaining
.8:0382   F0 38      BEQ $03BC   ;none, exit
.8:0384   C8         INY         ;zero
.8:0385   84 0F      STY $0F     ;flag buffer empty
.8:0387   E6 64      INC $64     ;skip transmit# in buffer
.8:0389   A8         TAY         ;#bytes to transmit
.8:038a   D0 BE      BNE $034A   ;always, do transmit
;check for new block
.8:038c   A5 11      LDA $11     ;next track/ load-address flag
.8:038e   F0 01      BEQ $0391   ;no more, fake ready
.8:0390   E8         INX         ;value for pull CLK low, allow DATA high
.8:0391   8E 01 40   STX $4001   ;update serial bus -- tell C64 data is ready (or not)
.8:0394   F0 8D      BEQ $0323   ;send EOI if fake
.8:0396   AA         TAX         ;test next track/ load-address flag
.8:0397   30 10      BMI $03A9   ;load address sent, now do data
                                 ;else fall into Read Block (see above)
~
;calc #bytes to transmit (.A = # bytes just transmitted)
.8:0310   AC 05 02   LDY $0205   ;max bytes per burst (default count)
.8:0313   65 64      ADC $64     ;advance source pointer low
.8:0315   85 64      STA $64
.8:0317   49 FF      EOR #$FF    ;negate part 1
.8:0319   38         SEC         ;negate part 2
.8:031a   65 10      ADC $10     ;add end index → #bytes remain
.8:031c   CD 05 02   CMP $0205   ;test with max
.8:031f   F0 23      BEQ $0344   ;same, last burst (normal)
.8:0321   B0 24      BCS $0347   ;more, do normal burst
;(less than max-transmit bytes remain)
.8:0323   AA         TAX         ;remain #bytes
.8:0324   A9 04      LDA #$04    ;CLK-input bit
;wait for EOI acknowledge
.8:0326   2C 01 40   BIT $4001   ;is CLK high?
.8:0329   F0 FB      BEQ $0326   ;yes, wait for EOI acknowledge
.8:032b   09 10      ORA #$10    ;must be set for C1581
.8:032d   8D 01 40   STA $4001   ;allow CLK and DATA high
.8:0330   C6 64      DEC $64     ;back-up data pointer 
.8:0332   A0 00      LDY #$00    ;no index
.8:0334   8A         TXA         ;#bytes to transmit
.8:0335   91 64      STA ($64),Y ;store in buffer
.8:0337   10 01      BPL $033A   ;always, calc buffer flag
.8:0339   98         TYA
;calc buffer flag
.8:033a   49 FF      EOR #$FF    ;calc buffer flag
.8:033c   C8         INY         ;1 byte to transmit
.8:033d   A2 12      LDX #$12    ;pull DATA low, allow CLK high
.8:033f   8E 01 40   STX $4001   ;update serial lines
.8:0342   D0 04      BNE $0348   ;always set buffer flag and transmit

.8:0344   A9 00      LDA #$00    ;last burst (normal)
.8:0346   24                     ;skip next instruction
;do normal burst
.8:0347   98         TYA
;set buffer flag
.8:0348   85 0F      STA $0F     ;set flag (zero if empty buffer)
;do transmit
.8:034a   88         DEY         ;convert count to an index

.8:034b   A9 04      LDA #$04    ;CLK-input bit
;wait for C64
.8:034d   2C 01 40   BIT $4001   ;is CLK high?
.8:0350   F0 FB      BEQ $034D   ;yes, wait for C64
.8:0352   20 FA 03   JSR $03FA   ;waste time
;loop to transmit
.8:0355   B1 64      LDA ($64),Y ;get data byte (or byte-count)
.8:0357   8D 67 03   STA $0367   ;save high nibble in code
.8:035a   29 0F      AND #$0F    ;mask low nibble
.8:035c   AA         TAX         ;index table
.8:035d   BD 00 03   LDA $0300,X ;decode low bits
.8:0360   20 F7 03   JSR $03F7   ;send 2 bits and prepare next pair
.8:0363   8D 01 40   STA $4001   ;send 2 bits
.8:0366   A2 00      LDX #$00    ;value set by code (high nibble)
.8:0368   BD 00 04   LDA $0400,X ;decode high bits
.8:036b   20 F7 03   JSR $03F7   ;send 2 bits and prepare next pair
.8:036e   8D 01 40   STA $4001   ;send 2 bits
.8:0371   88         DEY         ;countdown #bytes, done with burst?
.8:0372   EA         NOP
.8:0373   10 E0      BPL $0355   ;no loop to transmit
                                 ;yes, fall into Prepare Next Burst (see above)
~
;send 2 bits and prepare next pair
.8:03f7   8D 01 40   STA $4001   ;put 2 bits on serial bus
.8:03fa   0A         ASL A       ;shift over next two bits
.8:03fb   29 0A      AND #$0A    ;mask output bits
.8:03fd   09 10      ORA #$10    ;must be set for C1581
.8:03ff   60         RTS

Wow, that's a lot of code; let's break it down. It begins simple enough: pull the CLK low to signal data not ready but code is running, set a flag to send load address, find the filename and check if it's PRG.

Then things get messy because the code doesn't use any subroutines (to send load-address or a byte-count for example). In other words, the code enters one big loop controlled by flags and values in each block-link (next track and sector pointer). One of those flags (really a parameter) is the maximum number of bytes per burst; let's call it MaxBytes.

Conceptually it works by sending MaxBytes at a time (a burst) unless there is an 'exception'. There are 3 kinds of exception: sending load-address, end of block/sector, and end of file (or file not found). When an exception occurs, a special handshake (I call it EOI) is performed with C64 and a byte-count is sent followed by that many bytes. For load-address the byte-count is naturally 2. For end of block/sector the byte-count depends of the file data. For end-of-file or file-not-found, the byte-count is zero.

The main transfer / timing critical part of the code above occurs in 'loop to transmit' at $355~374. I call it a burst of data because no synchronization occurs during that loop; it just sends a long bit stream. It sends either MaxBytes or the number indicated by an exception. MaxBytes is a parameter sent by the C64. The value is 9 when no IRQ is running during load (which is often). A few scenes in the game have music playing (IRQs enabled) while loading. These uses a smaller MaxBytes value of 7 if I remember correctly.

To make things more concrete, let's look at two examples using MaxBytes = 9. First a typical file which consists of 3 or more blocks.

ActionComment
EOI '2'
send load-address
28 times: send 9 bytes
first block
28 times: send 9 bytes
EOI '2'
send 2 bytes
typical block
n times: send 9 bytes
EOI 'remainder'
if not 0, send remainder bytes
if not 0, send EOI '0'
last block

Things are slightly more complex for a small 1-block file (the irony!):

ActionComment
EOI '2'
send load-address
n times: send 9 bytes
EOI 'remainder'
if not 0, send remainder bytes
if not 0, send EOI '0'
first and last block

In summary, a typical block transmits 255 bytes (254 data bytes plus an EOI count). The worst case occurs when the last block contains 254 data bytes, in which case 256 bytes are transitted (252 normals bytes, an EOI count of 2, 2 more data bytes, and an EOI count of zero). This compares favorably with many fast-loaders that use standard sectors; they typically send 256 bytes for every block. Anyway, a transfer of 255 bytes takes less than a full VIC screen (even on the shorter NTSC).

Now let's look at how the C64 fast-loads this data:

.C:019b   A9 60      LDA #$60    ;opcode RTS
.C:019d   8D 6E 01   STA $016E   ;modify code
.C:01a0   A9 AD      LDA #$AD    ;load address low -1 (i.e., store load address at $ae~af)
.C:01a2   8D 67 01   STA $0167   ;modify code 
;wait for drive start
.C:01a5   2C 00 DD   BIT $DD00   ;is drive code running yet (is CLK low)?
.C:01a8   70 FB      BVS $01A5   ;no wait for drive start
.C:01aa   20 7E 01   JSR $017E   ;read load address
.C:01ad   A9 AD      LDA #$AD    ;opcode LDA absolute
.C:01af   8D 6E 01   STA $016E   ;modify code
.C:01b2   B0 D7      BCS $018B   ;error exit
.C:01b4   A5 95      LDA $95     ;get EOI count
.C:01b6   C9 02      CMP #$02    ;was it 2 for load-address?
.C:01b8   F0 4E      BEQ $0208   ;yes, continue
.C:01ba   D0 CC      BNE $0188   ;no, error exit (file not found)
~
.C:0208   A4 E9      LDY $E9     ;.YX = load address of caller (ignore file's load-address)
.C:020a   A6 E8      LDX $E8     ;is low-byte zero?
.C:020c   D0 01      BNE $020F   ;no, skip ahead
.C:020e   88         DEY         ;yes, adjust high-byte
.C:020f   CA         DEX         ;always, adjust low-byte
.C:0210   8E 67 01   STX $0167   ;modify code with load-address -1
.C:0213   8C 68 01   STY $0168
.C:0216   20 7E 01   JSR $017E   ;fast-load data
.C:0219   A5 E2      LDA $E2     ;sprite enables
.C:021b   8D 15 D0   STA $D015   ;restore sprites
.C:021e   60         RTS         ;done!

That first part of the C64 code reads the load-address from the file. Sadly it was a waste of time because Project Firestart over-rides the file's load address, and instead specifies its own load address in $E8~E9. Anyway, it calls $17E to fast-load the file data then restores sprites before exiting. Let's look at the 'main' $17E code:

;prepare MaxBytes
.C:017e   18         CLC         ;clear carry for math later
.C:017f   A0 09      LDY #$09    ;assume MaxBytes in burst
;wait for C1581 has data
.C:0181   2C 00 DD   BIT $DD00   ;is DATA low?
.C:0184   10 9C      BPL $0122   ;yes, begin read data -- else is CLK low?
.C:0186   50 F9      BVC $0181   ;yes, wait for C1581 has data
.C:0188   38         SEC         ;flag error (both CLK and DATA high, should never happen)
.C:0189   24                     ;skip next instruction
.C:018a   18         CLC         ;flag success
.C:018b   58         CLI         ;enable interrupts
.C:018c   AE 67 01   LDX $0167   ;get YX = next address -1
.C:018f   AC 68 01   LDY $0168
.C:0192   E8         INX         ;calc next address (low)
.C:0193   D0 01      BNE $0196   ;continue if no carry
.C:0195   C8         INY         ;calc next address (high)
.C:0196   86 AE      STX $AE     ;save end address +1 like KERNAL Load does
.C:0198   84 AF      STY $AF
.C:019a   60         RTS
~
;reSync with VIC
.C:0121   58         CLI         ;allow interrupts (like music or split-screen)
;begin read data (wait for VIC bad-line; cycle times in [brackets])
.C:0122   AF 12 D0   LAX $D012   ;[4]load .A and .X with raster #
.C:0125   4D 11 D0   EOR $D011   ;[4]toggle Y-scroll bits
.C:0128   29 07      AND #$07    ;[2]mask raster-in-character, on a bad-line? (AND #0 is screen is blanked)
.C:012a   D0 F6      BNE $0122   ;[2]no, wait for VIC bad-line
.C:012c   78         SEI         ;[2]disable interrupts
.C:012d   8A         TXA         ;[2]original raster # (where bad-line happened)
.C:012e   ED 12 D0   SBC $D012   ;[4]calc difference from current raster
.C:0131   C9 FE      CMP #$FE    ;[2]is result $FF or $FE (bad-line or the next)?
.C:0133   90 EC      BCC $0121   ;[2]no (we got hit by interrupt), reSync with VIC

.C:0135   AD 00 DD   LDA $DD00   ;[4]get serial lines
.C:0138   09 10      ORA #$10    ;[2]pull CLK low
.C:013a   8D 00 DD   STA $DD00   ;[4]update serial lines
.C:013d   84 95      STY $95     ;[3]save byte-count
.C:013f   49 10      EOR #$10    ;[2]allow CLK high
.C:0141   8D 00 DD   STA $DD00   ;[4]update serial lines
.C:0144   4A         LSR A       ;[2]shift UserPort bit (bit 2) down to bit 0
.C:0145   4A         LSR A       ;[2]
.C:0146   4D 00 DD   EOR $DD00   ;[4]get serial lines (and calc descramble value) -- is DATA high?
.C:0149   30 B8      BMI $0103   ;[2]yes, fetch EOI byte
.C:014b   29 07      AND #$07    ;[2]mask mangled UserPort and VIC Bank bits
.C:014d   8D 65 01   STA $0165   ;[4]save for descramble (self-modifying code)
.C:0150   24                     ;[1]NTSC skip next instruction (PAL will have NOP here)
;burst loop (42 cyles NTSC, 40 cycles PAL)
.C:0150   EA         NOP         ;[2]waste time (not included in PAL loop)
.C:0152   AD 00 DD   LDA $DD00   ;[4]read 2 bits
.C:0155   4A         LSR A       ;[2]shift down 2 bits
.C:0156   4A         LSR A       ;[2]
.C:0157   0D 00 DD   ORA $DD00   ;[4]merge 2 more bits
.C:015a   4A         LSR A       ;[2]shift down 2 bits
.C:015b   4A         LSR A       ;[2]
.C:015c   4D 00 DD   EOR $DD00   ;[4]merge 2 more bits
.C:015f   4A         LSR A       ;[2]shift down 2 bits
.C:0160   4A         LSR A       ;[2]
.C:0161   4D 00 DD   EOR $DD00   ;[4]merge last 2 bits
.C:0164   49 05      EOR #$05    ;[2]descramble (value set by code)
.C:0166   99 E2 00   STA $00AD,Y ;[5]save data (base address set by code)
.C:0169   88         DEY         ;[2]index next, all done?
.C:016a   D0 E5      BNE $0151   ;[3]no, burst loop
.C:016c   58         CLI         ;enable interrupts after burst
.C:016d   18         CLC         ;prepare for math
.C:016e   AD 67 01   LDA $0167   ;get address-1 low
.C:0171   65 95      ADC $95     ;add #bytes received, need to update address high?
.C:0173   8D 67 01   STA $0167   ;(update address-1 low)
.C:0176   8D 20 D0   STA $D020   ;(debug, update border color)
.C:0179   90 04      BCC $017F   ;no, prepare for MaxBytes
.C:017b   EE 68 01   INC $0168   ;yes, update address high
                                 ;fall into 'prepare MaxBytes' (see above)
~
;fetch EOI byte-count
.C:0103   A9 60      LDA #$60    ;opcode for RTS
.C:0105   CD 66 01   CMP $0166   ;already set for RTS?
.C:0108   F0 5C      BEQ $0166   ;yes, exit error
.C:010a   8D 66 01   STA $0166   ;modify code (so we fetch one byte w/o storing)
.C:010d   20 35 01   JSR $0135   ;get byte count
.C:0110   A2 99      LDX #$99    ;opcode for STA abs,Y
.C:0112   8E 66 01   STX $0166   ;modify code (for multiple bytes)
.C:0115   A8         TAY         ;set index, is it zero?
.C:0116   F0 72      BEQ $018A   ;yes, exit success (end of file)
.C:0118   C0 0A      CPY #$0A    ;is it greater than MaxBytes ?
.C:011a   B0 6C      BCS $0188   ;yes, exit error
.C:011c   C0 05      CPY #$05    ;is count less than 5?
.C:011e   90 15      BCC $0135   ;yes, read bytes now
.C:0120   18         CLC         ;no, prepare for reSync
                                 ;fall into reSync with VIC (see above)

So that C64 code first assumes that MaxBytes will transmitted and then waits for the C1581 to have data. Next it waits for a VIC bad-line to occur while leaving interrupts enabled (for music or split-screen). It disables interrupts when it sees a bad-line and then verifies no IRQ occured since the detection (if an IRQ did occur, it loops to find the next VIC bad-line).

WARNING: The VIC synchronization part of the code uses the undocumented opcode LAX (which loads both the .A and .X registers from a memory location). So unfortunately this code will not run on a SuperCPU. I choose to do it that way partly because I thought it would be cool, but also to save 2 cycles and one byte of code. In retrospect, the timing doesn't strictly require it, and probably should have been written as LDX $D012, TXA.

NOTE: The VIC synchronization normally contains an AND #7 instruction (shown above). However, when the screen is blanked, the instruction becomes AND #0 which means the code never waits for a bad-line.

Anyway, the code next toggles the CLK line so the C1581 can synchronize with the C64. Then it tests if an EOI byte is needed. Assuming no EOI byte, it sets a 'descramble value' in the code. The descramble value is calculated before each burst in case the IRQ changed the VIC Bank or UserPort bit.

The code from 'wait for VIC' up to the read loop (discussed next) takes 60 cycles on NTSC (61 on PAL). This assumes we do not need to wait, which is what I've done in analyzing other fast-loaders.

Next it enters the core read loop which takes an average 41 cycles per byte (42 NTSC, 40 PAL). When you factor-in the 60 cycle synchronization overhead, it averages out to about 48 cycles per byte (over 9 bytes). This time compares favoriably to other fast loaders which typically take 39~131 cycles per byte. Finally it adds the byte-count to the current address for the next address to load and updates the VIC border color (for debugging purposes).

The last code fragment shows how an EOI byte-count is received. It modifies the main code with an RTS after checking RTS isn't already set (it was for debugging, the check should probably be removed). Then it calls its modified self to get the byte-count and then undoes its modification. It moves the byte-count to the Y register and exits if the value is zero (end of file). Otherwise it tests if the count is valid (this was also for debugging and should be removed). For a valid count it then either branches to $135 for a small # bytes or falls into $121 (re-sync) to get five or more bytes.

That raps up the documentation of my fast-loader. I should finally mention that saving your game (code not shown) is done using standard/slow serial commands. Also I added delay code to some of the concluding scenes because the game was playing "too fast" with my loader. ☺

  Try it yourself 

Although I think this is a great fast-loader (especially considering it's my first for the C1581), I am obviously biased. But you don't have to take my word for it; download a D81 image and try it for yourself:

I imagine you can't really appreciate how awesome this is unless you've played the original set of four C1541 disks. Anyway, nothing is perfect (or so they say). Let's discuss the bugs/quirks that are present.

First, although the game will run on either NTSC or PAL, there are a few issues on a PAL machine. The main thing with PAL is the audio plays a bit slower and is off-key (but still sounds okay to me). The second thing with PAL is during one of the introduction screens there is some stuttering during a full-screen horizontal pan. Fortunately, horizontal scrolling works fine during the main game and the closing scenes.

Second, during one of the concluding scenes, things happen too fast! Apparently the authors were relying on their not-so-fast loader for timing purposes.

Third, the disk drive's LED remains on throughout the game. Either I forgot to turn it off, or I ran out of code bytes.

  Playing the game 

If you are using an emulator like VICE, be sure that 'true drive emulation' is enabled and that the disk-drive device-type is set to 1581 (duh). Anyway, use a joystick in port 2. Most everyting is done by joystick and is intuitive. However there are two keyboard commands you should know:

  • C → change your current weapon (you can only carry two, by the way)
  • D → access disk utilities (load/restart/save game)
  Summary 
  • Blank screen: no
  • Interrupts allowed: yes
  • Disk Header: standard
  • Directory structure: standard
  • Allow wildcard in filename: yes
  • File structure: standard
  • Sector structure: standard (256 data bytes)
  • Sector decoding time: 0.0 milliseconds
  • Head stepping speed: fast (6 milliseconds/track)
  • Disk → C64 transfer: fast (about 48 microseconds/byte)
  • C64 → Disk transfer: slow (about 1024 microseconds/byte)
  • C64 memory footprint: about 0.5K ($103~1BF, $208~24F, $2A7~313, $350~3BC, $7FD0~7FFF)
  • Needs KERNAL: no
  • Load $D000~DFFF: I/O
  • Alters User Port: no
  • Requires Unit 8: no
  • Write file/sector: yes
  • Other: emulates KERNAL routines for writing but uses "nibble write"

Web page © Hydrophilic.net, 2026
Project Firestart © Dynamix, 1988