| 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 |
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.
Things are slightly more complex for a small 1-block file (the irony!):
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. ☺
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.
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:
Web page © Hydrophilic.net, 2026 Project Firestart © Dynamix, 1988 |