| 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 was used at least in Outrun by Sega. The first file on the disk is an "auto-run" file. That is, the user types LOAD"*",8,1 and presses return then the program loads and RUNs without further user interaction. Although the first file on disk is not a BASIC program, it interestingly over-writes the BASIC $302 vector to enable "auto-run". In particular, the file loads at $2DD in RAM and ends at $303; this takes about 1.5 seconds. The vector at $302 (which controls BASIC user input) is set to point to $2DD (which shouldn't be a surprise). Let's look at the code: .C:02dd A9 00 LDA #$00 ;multi-use constant .C:02df 8D 11 D0 STA $D011 ;blank VIC screen .C:02e2 85 9D STA $9D ;turn off KERNAL messages (like LOADING) .C:02e4 8D 20 D0 STA $D020 ;set border color = black .C:02e7 A9 08 LDA #$08 ;file 8 .C:02e9 AA TAX ;device 8 .C:02ea A8 TAY ;channel 8 .C:02eb 20 BA FF JSR $FFBA ;KERNAL SetLFS .C:02ee A9 01 LDA #$01 ;filename length .C:02f0 A2 FF LDX #$FF ;.YX = $2FF (filename -> "0") .C:02f2 A0 02 LDY #$02 .C:02f4 20 BD FF JSR $FFBD ;KERNAL SetNam .C:02f7 A9 00 LDA #$00 ;load (not verify) .C:02f9 20 D5 FF JSR $FFD5 ;KERNAL Load .C:02fc 4C 00 C4 JMP $C400 ;run program (install fast-loader) This simply initializes some VIC registers and loads (standard/slow) the file named "0". That file loads to $C000~C6B9 and takes about 4.5 seconds. Let's look at the initialization routine: .C:c400 A9 00 LDA #$00 ;address $C500 .C:c402 A2 C5 LDX #$C5 .C:c404 85 FB STA $FB ;stored in pointer $FB~FC .C:c406 86 FC STX $FC .C:c408 A9 01 LDA #$01 ;logical file# .C:c40a A2 08 LDX #$08 ;device# .C:c40c A0 0F LDY #$0F ;channel# .C:c40e 20 BA FF JSR $FFBA ;KERNAL SetLFS .C:c411 A2 A4 LDX #$A4 ;.YX = $C4A4 (points to string "I") .C:c413 A0 C4 LDY #$C4 ;note .A is still 1 (filename length) .C:c415 20 BD FF JSR $FFBD ;KERNAL SetNam .C:c418 20 C0 FF JSR $FFC0 ;KERNAL Open (i.e., OPEN 1,8,15,"I") ;send M-W commands loop .C:c41b A0 03 LDY #$03 ;index string "M-W" .C:c41d 20 93 C4 JSR $C493 ;send (partial) string from $C4A4 table .C:c420 A5 FB LDA $FB ;address low .C:c422 20 D2 FF JSR $FFD2 ;send to drive .C:c425 A5 FC LDA $FC ;C64 address high .C:c427 E9 BF SBC #$BF ;subtract $C0 because carry clear .C:c429 20 D2 FF JSR $FFD2 ;send C1541 address high .C:c42c A9 20 LDA #$20 ;32 bytes to send .C:c42e AA TAX ;set count .C:c42f 20 D2 FF JSR $FFD2 ;send to drive ;byte send loop .C:c432 B1 FB LDA ($FB),Y ;read code byte .C:c434 20 D2 FF JSR $FFD2 ;send to drive .C:c437 E6 FB INC $FB ;increment pointer low .C:c439 D0 02 BNE $C43D ;no carry, count down .C:c43b E6 FC INC $FC ;increment pointer high .C:c43d CA DEX ;count bytes, done? .C:c43e D0 F2 BNE $C432 ;no, byte send loop .C:c440 20 CC FF JSR $FFCC ;KERNAL ClrChn -- sends Unlisten causing drive to execute command .C:c443 A5 FB LDA $FB ;test address pointer less than $C6BA? .C:c445 C9 BA CMP #$BA .C:c447 A5 FC LDA $FC .C:c449 E9 C6 SBC #$C6 .C:c44b 90 CE BCC $C41B ;yes, send M-W commands loop That code first sends an 'initialize' command to the disk drive. Then it issues a series of M-W (memory-write) commands to transfer code from $C500~C6BF in the C64 to address $500~6BF in the C1541/71. This is silly is you ask me -- definately slower than it needs to be! Think about it: we slow-loaded the code into the C64 and now we're slow-writing it back to the C1541. It would be more effecient for the C64 to just issue a block-read and/or block-execute command(s). Anyway, the transfer takes about 1.5 seconds. More initialization code follows: .C:c44d A0 06 LDY #$06 ;index string "U3" .C:c44f 20 93 C4 JSR $C493 ;send string from $C4A4 table .C:c452 20 CC FF JSR $FFCC ;KERNAL ClrChn -- sends Unlisten causing drive to execute command .C:c455 78 SEI ;disable interrupts .C:c456 D8 CLD ;clear BCD mode (kind of late don't ya think?) .C:c457 A2 3F LDX #$3F ;multi-use constant .C:c459 9A TXS ;reset CPU stack .C:c45a A9 2F LDA #$2F ;standard CPU data direction .C:c45c 85 00 STA $00 .C:c45e A9 35 LDA #$35 ;memory configuration no ROMs (just RAM and I/O) .C:c460 85 01 STA $01 .C:c462 8E 02 DD STX $DD02 ;standard CIA2 data direction .C:c465 A9 7F LDA #$7F ;clear .C:c467 8D 0D DC STA $DC0D ;CIA1 -- IRQ .C:c46a 8D 0D DD STA $DD0D ;CIA2 -- NMI .C:c46d 2C 0D DC BIT $DC0D ;clear again .C:c470 2C 0D DD BIT $DD0D ;(pretty please) .C:c473 E8 INX ;$40 = opcode RTI (return from interrupt) .C:c474 8E 01 01 STX $0101 ;set for NMI .C:c477 A2 FF LDX #$FF ;clear any pending interrupts .C:c479 8E 19 D0 STX $D019 ;of VIC chip .C:c47c E8 INX ;zero .C:c47d 8E 1A D0 STX $D01A ;disable all interrupts of VIC .C:c480 E8 INX ;one .C:c481 8E FA FF STX $FFFA ;set CPU NMI vector to $101 (where we wrote RTI instruction) .C:c484 8E FB FF STX $FFFB .C:c487 E8 INX ;two ;wait for C1541 .C:c488 2C 00 DD BIT $DD00 ;test serial lines, is CLK high? .C:c48b 70 FB BVS $C488 ;yes, wait for C1541 .C:c48d 8E 00 DD STX $DD00 ;allow CLK and DATA to go high, VIC Bank 1, clear UserPort line .C:c490 4C 00 C3 JMP $C300 ;continue initialization That code sends the (rarely used) U3 command to the disk drive which causes it to execute code at $500. Then it sets up various I/O ports and CPU vectors. Finally it waits for the serial-port CLK line to be pulled low by the C1541 before continuing. Let's take a look at the drive's $500 code: .8:0500 78 SEI ;disable controller .8:0501 A9 EE LDA #$EE ;request byte-ready signal from disk head .8:0503 8D 0C 1C STA $1C0C .8:0506 A9 F3 LDA #$F3 ;turn off drive motor and LED (!) .8:0508 2D 00 1C AND $1C00 .8:050b 8D 00 1C STA $1C00 .8:050e A9 08 LDA #$08 ;pull CLK low, allow DATA high .8:0510 8D 00 18 STA $1800 .8:0513 A9 01 LDA #$01 ;DATA input bit ;wait 1 .8:0515 2C 00 18 BIT $1800 ;is DATA low? .8:0518 D0 FB BNE $0515 ;yes, wait loop1 ;wait 2 .8:051a 2C 00 18 BIT $1800 ;is DATA high? .8:051d F0 FB BEQ $051A ;yes, wait loop2 The disk drive isn't doing much here. It turns off the drive motor and LED and waits for the C64 to toggle serial DATA line. Let's go back to the C64 initialization. .C:c300 78 SEI ;disable interrupts (again -- we'll teach those interrupts!) .C:c301 A9 34 LDA #$34 ;all RAM .C:c303 85 01 STA $01 ;set C64 memory configuration .C:c305 A0 00 LDY #$00 ;reset index ;copy loop .C:c307 B9 00 C0 LDA $C000,Y ;copy $c000~c0ff .C:c30a 99 00 D0 STA $D000,Y ;to $d000~d0ff .C:c30d B9 00 C1 LDA $C100,Y ;copy $c100~c207 .C:c310 99 00 02 STA $0200,Y ;to $200~307 .C:c313 B9 08 C1 LDA $C108,Y .C:c316 99 08 02 STA $0208,Y .C:c319 88 DEY ;index prior, all done? .C:c31a D0 EB BNE $C307 ;no, copy loop .C:c31c E6 01 INC $01 ;enable I/O registers in memory configuation .C:c31e A9 46 LDA #$46 ;filename "FF" .C:c320 A0 46 LDY #$46 .C:c322 2C 00 02 BIT $0200 ;skip loader .C:c325 2C B0 0F BIT $0FB0 ;skip run .C:c328 A9 30 LDA #$30 ;filename "00" .C:c32a A0 30 LDY #$30 .C:c32c 20 00 02 JSR $0200 ;call loader ~ .C:0200 85 FA STA $FA ;filename char 1 .C:0202 84 FB STY $FB ;filename char 2 .C:0204 A9 22 LDA #$22 ;pull DATA low, allow CLK high .C:0206 8D 00 DD STA $DD00 ;update serial bus .C:0209 A9 00 LDA #$00 ;dummy byte .C:020b 85 FF STA $FF ;flag first sector of file .C:020d 20 CF 02 JSR $02CF ;send dummy byte .C:0210 A5 FA LDA $FA ;filename char 1 .C:0212 20 CF 02 JSR $02CF ;send to C1541 .C:0215 98 TYA ;filename char 2 .C:0216 20 CF 02 JSR $02CF ;send to C1541 .C:0219 98 TYA ;filename char 2 .C:021a 45 FA EOR $FA ;calculate checksum .C:021c 20 CF 02 JSR $02CF ;send to C1541 That code copies a data table to RAM $D000 (under I/O registers) and loader code to $200~307. Then it calls the fast-loader to get file "00". The beginning of the loader pulls the DATA line low and transmits four bytes: a dummy byte, two characters of filename, and a checksum. We'll see why it sends a checksum in a moment. Now lets look at the C64-to-C1541 transmit code: ;cycle times are in [brackets] .C:02cf 85 FE STA $FE ;[3]save byte to transmit .C:02d1 A2 00 LDX #$00 ;[2]disable .C:02d3 8E 11 D0 STX $D011 ;[4]screen .C:02d6 8E 15 D0 STX $D015 ;[4]sprites ;send byte loop (subtotal 74*4-1 = 295) .C:02d9 A9 12 LDA #$12 ;[2]CLK low for even bit, assume data bit zero .C:02db 46 FE LSR $FE ;[5]get data bit, is it zero? .C:02dd 90 02 BCC $02E1 ;[3.5]yes, skip ahead .C:02df 09 20 ORA #$20 ;[0]no, DATA low (inverted) .C:02e1 8D 00 DD STA $DD00 ;[4]update serial bus lines .C:02e4 20 05 03 JSR $0305 ;[19]delay .C:02e7 29 0F AND #$0F ;[2]CLK high for odd bit, assume data bit zero .C:02e9 46 FE LSR $FE ;[5]get data bit, is it zero? .C:02eb 90 02 BCC $02EF ;[3.5]yes, skip ahead .C:02ed 09 20 ORA #$20 ;[0]no, DATA low (inverted) .C:02ef 8D 00 DD STA $DD00 ;[4]update serial bus lines .C:02f2 20 05 03 JSR $0305 ;[19]delay .C:02f5 E8 INX ;[2]count bit pairs .C:02f6 E0 04 CPX #$04 ;[2]all 4 (8 bit total)? .C:02f8 D0 DF BNE $02D9 ;[3]no, send byte loop .C:02fa A9 02 LDA #$02 ;[2]allow CLK and DATA high .C:02fc 8D 00 DD STA $DD00 ;[4]update serial lines ;call delay 4 times (subtotal 24*4-1 = 95 cycles) .C:02ff 20 05 03 JSR $0305 ;[19]delay .C:0302 CA DEX ;[2] count, all done? .C:0303 D0 FA BNE $02FF ;[3] no, delay loop .C:0305 48 PHA ;[3] waist time .C:0306 68 PLA ;[4] .C:0307 60 RTS ;[6] That's the slowest transmit routine I've seen so far (excluding system ROMs). It takes a 'base' time of 295 cycles but then adds a delay and there is overhead. The total transmit time is about 422 microseconds. Oh well, it's suppose to be a fast-loader, not a fast transmitter! If you notice, the transmit routine blanks the screen, but doesn't wait for the border to start. This means VIC bad-lines can strike while transmitting data (at least until the border starts). I'm pretty sure this is why the filename includes a checksum. Anyway, let's see what the drive does with the filename: .8:051f 8D 00 18 STA $1800 ;allow CLK and DATA to go high .8:0522 A9 0C LDA #$0C ;turn on drive motor and LED .8:0524 0D 00 1C ORA $1C00 .8:0527 8D 00 1C STA $1C00 .8:052a 20 DC 05 JSR $05DC ;read byte from C64 (and discard!) .8:052d 20 DC 05 JSR $05DC ;read byte from C64 .8:0530 85 B7 STA $B7 ;save filename char 1 .8:0532 20 DC 05 JSR $05DC ;read byte from C64 .8:0535 85 B8 STA $B8 ;save filename char 2 .8:0537 45 B7 EOR $B7 ;calculate checksum .8:0539 85 B9 STA $B9 ;save checksum .8:053b 20 DC 05 JSR $05DC ;read byte from C64 (checksum) .8:053e A0 07 LDY #$07 ;buffer high-byte .8:0540 84 31 STY $31 .8:0542 C8 INY ;8 = pull CLK low, allow DATA high .8:0543 8C 00 18 STY $1800 ;update serial lines .8:0546 C5 B9 CMP $B9 ;test checksum .8:0548 D0 35 BNE $057F ;error .8:054a A9 01 LDA #$01 ;# directory sectors .8:054c 85 BA STA $BA .8:054e 0E B9 06 ASL $06B9 ;get bit, is it set? (yes on first pass) .8:0551 B0 1A BCS $056D ;yes, skip ahead ;search for filename (fresh buffer) .8:0553 A0 05 LDY #$05 ;buffer index for first filename ;search for filename (same buffer) .8:0555 B1 30 LDA ($30),Y ;read $705 (dir filename 1st char) .8:0557 C5 B7 CMP $B7 ;check requested 1st char .8:0559 D0 07 BNE $0562 ;no match, next entry .8:055b B9 01 07 LDA $0701,Y ;read $706 (dir filename 2nd char) .8:055e C5 B8 CMP $B8 ;check requested 2nd char .8:0560 F0 28 BEQ $058A ;match, start loading .8:0562 98 TYA ;buffer index .8:0563 18 CLC .8:0564 69 20 ADC #$20 ;add 32 (size of directory entry) .8:0566 A8 TAY ;set index, end of buffer? .8:0567 90 EC BCC $0555 ;no, search for filename (same buffer) .8:0569 A5 BA LDA $BA ;last directory sector? .8:056b F0 06 BEQ $0573 ;yes, error .8:056d C6 BA DEC $BA ;countdown directory sectors .8:056f A2 01 LDX #$01 ;desired sector .8:0571 D0 05 BNE $0578 ;always, skip ahead ~ .8:0578 A9 12 LDA #$12 ;desired track 18 .8:057a 20 FE 05 JSR $05FE ;read sector .8:057d F0 D4 BEQ $0553 ;always, search for filename (fresh buffer) That code turns on the drive motor and LED, reads the filename (including dummy byte and checksum), and searches the directory for the desired file. Interestingly, it's hard-coded to only read one directory sector, so there can be no more than 8 files. Because the whole directory fits in one sector, it is permanently stored in buffer $700. When another file is loaded (later), the code won't move the disk-head to track 18 to read the directory. This saves some time. Also the code doesn't check the file type, so it can load DELeted files which aren't shown in a standard directory listing. Before looking at the main loader, let's first look at the head-stepping portion of read sector: .8:05fe A0 08 LDY #$08 ;pull CLK low, allow DATA high .8:0600 8C 00 18 STY $1800 ;update serial lines .8:0603 85 18 STA $18 ;set desired track .8:0605 86 19 STX $19 ;set desired sector .8:0607 38 SEC ;calculate distance (difference) .8:0608 E5 22 SBC $22 ;from current track .8:060a F0 2F BEQ $063B ;already on-track, read sector .8:060c A2 01 LDX #$01 ;+1 (assume positive difference) .8:060e B0 06 BCS $0616 ;branch if diff is positive .8:0610 49 FF EOR #$FF ;negate accumulator .8:0612 69 01 ADC #$01 ;so now .A is positive .8:0614 A2 FF LDX #$FF ;-1 (note negative difference) .8:0616 86 B5 STX $B5 ;save delta (+1 or -1) .8:0618 0A ASL A ;double difference .8:0619 AA TAX ;set # half-track steps ;stepper loop .8:061a AD 00 1C LDA $1C00 ;get stepper bits (and other stuff) .8:061d 18 CLC .8:061e 65 B5 ADC $B5 ;add delta (+1 or -1) .8:0620 29 03 AND #$03 ;isolate new stepper bits .8:0622 85 B6 STA $B6 ;save temp .8:0624 AD 00 1C LDA $1C00 ;get stepper bits (and other stuff) .8:0627 29 FC AND #$FC ;clear old stepper bits (keep other stuff) .8:0629 05 B6 ORA $B6 ;merge new stepper bits .8:062b 8D 00 1C STA $1C00 ;update stepper .8:062e A9 95 LDA #$95 ;timer value (about 5.5 milliseconds) .8:0630 8D 05 18 STA $1805 ;start timer ;wait for timer .8:0633 AD 05 18 LDA $1805 ;test timer, still running? .8:0636 30 FB BMI $0633 ;yes, wait for timer .8:0638 CA DEX ;countdown # half-track steps .8:0639 D0 DF BNE $061A ;not done, stepper loop ;head on-track .8:063b A5 18 LDA $18 ;desired track .8:063d 85 22 STA $22 ;is current track .8:063f A2 04 LDX #$04 ;# track zones ;find zone of track .8:0641 CA DEX ;index table .8:0642 DD B1 06 CMP $06B1,X ;test table, current track less ? .8:0645 B0 FA BCS $0641 ;no, loop for zone index .8:0647 AD 00 1C LDA $1C00 ;get bit-rate bits (and stuff) .8:064a 29 9F AND #$9F ;clear bit-rate bits (keep other stuff) .8:064c 1D B5 06 ORA $06B5,X ;merge correct bit-rate bits for this zone .8:064f 8D 00 1C STA $1C00 ;update bit-rate for disk-head ~ >8:06b1 29 1f 19 12 00 20 40 60 The code is pretty standard for stepping the head at a constant rate. Assuming the head is not on the desired track, the code first calculates a delta value (+1 or -1) and number of half-track steps (in .X). Then it just updates the stepper bits with a constant delay from a hardware timer between steps. Next (even if the head was already on-track) the code determines the 'zone' of the track and sets the head bit-rate accordingly:
This is the CBM standard zone table with the exception of extra tracks 36~40. Next the actual reading and decoding of the sector: ;read sector .8:0652 20 7F 06 JSR $067F ;search for sector header and following sync-mark ;loop for buffer high ($700~7FF for directory or $300~3FF for file) .8:0655 50 FE BVC $0655 ;wait for byte-ready .8:0657 B8 CLV ;ready for next .8:0658 AD 01 1C LDA $1C01 ;get GCR byte from disk head .8:065b 91 30 STA ($30),Y ;store in high buffer .8:065d C8 INY ;index next, all done? .8:065e D0 F5 BNE $0655 ;no, loop for buffer high .8:0660 A0 BA LDY #$BA ;index start (-70) ;loop for buffer low ($1BA~1FF) .8:0662 50 FE BVC $0662 ;wait for byte-ready .8:0664 B8 CLV ;ready for next .8:0665 AD 01 1C LDA $1C01 ;get GCR byte from disk head .8:0668 99 00 01 STA $0100,Y ;store in low buffer .8:066b C8 INY ;index next, all done? .8:066c D0 F4 BNE $0662 ;no, loop for buffer low .8:066e 20 E0 F8 JSR $F8E0 ;decode GCR bytes (takes about 22,900 cycles) .8:0671 A5 38 LDA $38 ;get leading byte .8:0673 C5 47 CMP $47 ;test data-block identifier (7) .8:0675 D0 DB BNE $0652 ;mismatch, read sector again .8:0677 20 E9 F5 JSR $F5E9 ;calculate sector checksum (takes about 2,575 cycles) .8:067a C5 3A CMP $3A ;compare with trailing byte .8:067c D0 D4 BNE $0652 ;mismatch, read sector again .8:067e 60 RTS ;exit This code first searches for the sector header and trailing sync-mark (pre-data-block). That routine ($67F) is not shown because it is esentially the same as the ROM code with the difference being it will search forever. Next it reads 256 bytes to a high buffer (either $700 for directory or $300 for file) and 70 bytes into $1BA~1FF buffer. That's a total of 326 bytes which is actually one byte more than needed (like the ROM code). Next it calls ROM routines to decode the GCR bytes and calculate their checksum; this part takes about 25,500 cycles total. I'm using the total for comparison with other loaders which typically calculate the checksum at the same time they do the GCR conversion. Finally it tests the calculated checksum matches the one from disk. In case it isn't obvious, the code will loop forever if the sector is unreadable or not found.
The C64 has done a little work while the drive was finding the file: .C:021f AD 00 DD LDA $DD00 ;read I/O bits .C:0222 29 0C AND #$0C ;isolate ATN and User-Port bits .C:0224 4A LSR A ;shift to lower 2 bits .C:0225 4A LSR A ;(occupied by VIC bank) .C:0226 4D 00 DD EOR $DD00 ;scramble VIC bank bits with ATN and User-Port bit .C:0229 29 0F AND #$0F ;isolate scramble code (low 4 bits) .C:022b 49 FF EOR #$FF ;invert all bits (hardware line inversion) .C:022d 8D 6A 02 STA $026A ;modify our code to un-scramble ;loop for next sector .C:0230 A9 22 LDA #$22 ;pull DATA low, allow CLK high (and VIC bank 1) .C:0232 8D 00 DD STA $DD00 ;update serial lines .C:0235 A0 03 LDY #$03 ;delay 3*5+2-1 = 16 cycles ;delay loop .C:0237 88 DEY ;countdown, done? .C:0238 D0 FD BNE $0237 ;no, delay loop ;wait for C1541 .C:023a 2C 00 DD BIT $DD00 ;is CLK low? .C:023d 50 FB BVC $023A ;yes, wait for C1541 The main thing it does is calculate an 'unscramble' value and stores it in $26A (self-modifying code). This value is needed for two reasons: first the C1541 hardware will invert the bits it sends over the serial bus, but the C64/128 does not have inverters on its inputs (this explains the EOR #$FF instruction). Second, the loader code will EOR its byte-data with the lower bits of $DD00 which will cause the VIC bank and User-Port bits to scramble the value. As we'll see soon, the loader code uses a static decode table, but that isn't used for unscrambling because the unscramble value can change based on the VIC Bank and User-Port line. Trivia: the code goes through the trouble of calculating an un-scramble value based on the User-Port and ATN bits. Although the UserPort bit has a default value of 1, it was changed to zero a long time ago. The ATN bit remains 0 throught loading. Since these bits are both a constant 0, they don't contribute to the (un)scrambling. In short, the code is more complex than neccessary. Next the C64 loader receives and decodes 256 bytes of data and stores it in $D100~D1FF: .C:023f A0 10 LDY #$10 ;delay 16*5+2-1 = 81 cycles .C:0241 88 DEY .C:0242 D0 FD BNE $0241 ;read sector loop (87 cycles per pass) .C:0244 A9 0F LDA #$0F ;[2]allow CLK and DATA to go high .C:0246 2D 00 DD AND $DD00 ;[4] .C:0249 8D 00 DD STA $DD00 ;[4] .C:024c 09 20 ORA #$20 ;[2]value to pull DATA low .C:024e AA TAX ;[2]save for later .C:024f 48 PHA ;[3]waste 7 cycles .C:0250 68 PLA ;[4] .C:0251 AD 00 DD LDA $DD00 ;[4]read 2 bits .C:0254 4A LSR A ;[2]shift down by two .C:0255 4A LSR A ;[2] .C:0256 EA NOP ;[2]waste 2 cycles .C:0257 4D 00 DD EOR $DD00 ;[4]read 2 more bits .C:025a 4A LSR A ;[2]shift down by two .C:025b 4A LSR A ;[2] .C:025c EA NOP ;[2]waste 2 cycles .C:025d 4D 00 DD EOR $DD00 ;[4]read 2 more bits .C:0260 4A LSR A ;[2]shift down by two .C:0261 4A LSR A ;[2] .C:0262 EA NOP ;[2]waste 2 cycles .C:0263 4D 00 DD EOR $DD00 ;[4]read 2 last bits .C:0266 8E 00 DD STX $DD00 ;[4]pull DATA low, allow CLK high .C:0269 49 FD EOR #$FD ;[2]unscramble data (value $FD set earlier) .C:026b AA TAX ;[2]index for decoding .C:026c C6 01 DEC $01 ;[5]memory config = all RAM .C:026e BD 00 D0 LDA $D000,X ;[4]decode value .C:0271 99 00 D1 STA $D100,Y ;[5]save in buffer .C:0274 E6 01 INC $01 ;[5]memory config = RAM + I/O .C:0276 C8 INY ;[2]index next byte, all done? .C:0277 D0 CB BNE $0244 ;[3]no, read sector loop Due to the way the C1541 code is written, we need LDA $D000,X to decode the transmitted value. That code takes 81+256*87-1 = 22,352 cycles which is more than one full VIC screen (even considering the longer PAL). That value averages out to 87.3 cycles per byte. So this is one of the slower loaders, but definately not the slowest. Now that the C64 has a sector of data, let's see how the rest of the loader plays out. .C:0279 C6 01 DEC $01 ;memory config = all RAM .C:027b AE 00 D1 LDX $D100 ;get next track#, is this the final sector? .C:027e F0 06 BEQ $0286 ;yes, skip ahead .C:0280 A2 FF LDX #$FF ;no, all bytes in sector .C:0282 8E 01 D1 STX $D101 ;save for limit .C:0285 E8 INX ;zero .C:0286 C8 INY ;1 = source index for buffer (and flag value) .C:0287 E6 01 INC $01 ;memory config = RAM + I/O .C:0289 A5 FF LDA $FF ;is this the first sector? .C:028b 84 FF STY $FF ;(flag first sector processed) .C:028d D0 1B BNE $02AA ;no, do RAM transfer ;first sector .C:028f C6 01 DEC $01 ;memory config = all RAM .C:0291 A0 03 LDY #$03 ;source index for buffer (skip load address bytes) .C:0293 AD 02 D1 LDA $D102 ;get address low .C:0296 85 FC STA $FC ;initialize pointer .C:0298 AD 03 D1 LDA $D103 ;get address high .C:029b E6 01 INC $01 ;memory config = RAM + I/O .C:029d 85 FD STA $FD ;initialize pointer .C:029f 05 FC ORA $FC ;test load address, is it zero? .C:02a1 D0 07 BNE $02AA ;no, do RAM transfer .C:02a3 A5 FA LDA $FA ;yes, get filename again .C:02a5 A4 FB LDY $FB .C:02a7 4C 00 02 JMP $0200 ;start loader all over ;do RAM transfer .C:02aa C6 01 DEC $01 ;memory config = all RAM ;transfer loop .C:02ac C8 INY ;index source .C:02ad B9 00 D1 LDA $D100,Y ;read decoded value .C:02b0 81 FC STA ($FC,X) ;save to RAM (.X=0 so pointer $FC,FD) .C:02b2 E6 FC INC $FC ;index destination low, any carry? .C:02b4 D0 02 BNE $02B8 ;no, test limit .C:02b6 E6 FD INC $FD ;index destination high ;test limit .C:02b8 CC 01 D1 CPY $D101 ;was that the last byte in sector? .C:02bb D0 EF BNE $02AC ;no, transfer loop .C:02bd AD 00 D1 LDA $D100 ;last sector? .C:02c0 F0 05 BEQ $02C7 ;yes, done loading .C:02c2 E6 01 INC $01 ;memory config = RAM + I/O .C:02c4 4C 30 02 JMP $0230 ;loop for next sector ;done loading .C:02c7 E6 01 INC $01 ;memory config = RAM + I/O .C:02c9 A9 02 LDA #$02 ;allow CLK and DATA to go high (and VIC bank 1) .C:02cb 8D 00 DD STA $DD00 ;update serial lines .C:02ce 60 RTS ;return There's not much to that code, although it's a bit messy due to the memory configuration changing. Basically it's sets a limit of $FF into $D101 unless it's the last sector, in which case $D101 already holds the limit. If it's the first sector, it grabs the load address from $D102~D103 and checks for zero. Then it copies data from the buffer to the correct location in RAM using the rare index-indirect addressing mode. Finally, it loops if not the last sector or else allows the CLK and DATA lines on the serial bus to go high. There is no sign of copy-protection in the fast-load code. It must be somewhere else in the main program.
© Hydrophilic.net, 2026 |