Home > CBM > Disk > Fast Load > Access | |||||||||
This fast-loader comes from the game Echelon by Access Software. Interestingly, this fast-loader is not installed automatically when the game boots. A BASIC program will ask the user if (s)he would like to install the fast-loader. Selecting "yes" causes the file "FASTLD" to be loaded (with standard ROM routines) to address $C000~C3A7. Then KERNAL vector for Load is changed to point to $C000. Let's look at the code: .C:c000 A0 00 LDY #$00 ;reset filename index .C:c002 84 90 STY $90 ;clearn KERNAL status .C:c004 B1 BB LDA ($BB),Y ;read filename char .C:c006 C9 24 CMP #$24 ;is it "$" for directory? .C:c008 D0 07 BNE $C011 ;no, skip ahead .C:c00a A5 93 LDA $93 ;we don't handle directory .C:c00c A4 C3 LDY $C3 ;so continue with .C:c00e 4C A5 F4 JMP $F4A5 ;standard KERNAL code .C:c011 A5 C3 LDA $C3 ;user load-address low .C:c013 85 AE STA $AE ;set load-address low (assume we will use it) .C:c015 A5 C4 LDA $C4 ;user load-address high .C:c017 85 AF STA $AF ;set load-address high .C:c019 AD 0E DC LDA $DC0E ;CIA1 Timer A register .C:c01c 85 03 STA $03 ;save for exit routine .C:c01e A9 00 LDA #$00 ;disable (stop) .C:c020 8D 0E DC STA $DC0E ;CIA1 Timer A .C:c023 A9 7F LDA #$7F ;clear all interrupts .C:c025 8D 0D DD STA $DD0D ;on CIA2 .C:c028 AD 00 DD LDA $DD00 ;serial lines, UserPort bit, VIC bank bits .C:c02b 29 07 AND #$07 ;keep UserPort and VIC bits .C:c02d 85 07 STA $07 ;save for exit routine The first part of that code makes sure we're loading a normal file, not a directory listing. Then it set's the user-specified load-address into our working pointer (assuming it will be used). Depending on a KERNAL variable, we may instead get the load-address from the file (as we'll see later). Finally it saves and initializes some hardware registers. Interestingly, to disable IRQs, it clears CIA1 Timer A instead of just issuing SEI command. I believe this is because the KERNAL serial routines will just re-enable interrupts anyway. Next we have code which will write 512 bytes of code to the drive RAM using slow/standard serial routines. .C:c02f A9 10 LDA #$10 ;32 Memory-Writes (512 bytes) .C:c031 85 2A STA $2A ;set counter .C:c033 A9 00 LDA #$00 ;set $C3~C4 to point to $400 .C:c035 85 C3 STA $C3 ;(C1541/71 destination) .C:c037 A9 04 LDA #$04 .C:c039 85 C4 STA $C4 .C:c03b A9 AB LDA #$AB ;set $5~6 to point to $C1AB .C:c03d 85 05 STA $05 ;(C64 source) .C:c03f A9 C1 LDA #$C1 .C:c041 85 06 STA $06 .C:c043 20 AE FF JSR $FFAE ;KERNAL Unlisten .C:c046 20 6B C1 JSR $C16B ;call KERNAL Listen and Second .C:c049 90 08 BCC $C053 ;skip ahead if okay .C:c04b 20 AE FF JSR $FFAE ;KERNAL Unlisten .C:c04e A9 05 LDA #$05 ;error code .C:c050 4C 54 C1 JMP $C154 ;exit routine .C:c053 A9 49 LDA #$49 ;"I" (initialize drive command) .C:c055 20 A8 FF JSR $FFA8 ;KERNAL Serial Out .C:c058 20 AE FF JSR $FFAE ;KERNAL Unlisten (drive will execute command) ;Memory-Write loop .C:c05b 20 81 C1 JSR $C181 ;send "M-W" string to drive (memory write) .C:c05e A5 C3 LDA $C3 ;drive address low .C:c060 20 A8 FF JSR $FFA8 ;KERNAL Serial Out .C:c063 A5 C4 LDA $C4 ;drive address high .C:c065 20 A8 FF JSR $FFA8 ;KERNAL Serial Out .C:c068 A9 20 LDA #$20 ;32 byte count .C:c06a 20 A8 FF JSR $FFA8 ;KERNAL Serial Out .C:c06d A0 00 LDY #$00 ;initial pointer ;write bytes loop .C:c06f B1 05 LDA ($05),Y ;read RAM (from $C1AB~C3AA) .C:c071 20 A8 FF JSR $FFA8 ;KERNAL Serial Out .C:c074 C8 INY ;index C64 RAM .C:c075 C0 20 CPY #$20 ;all 32 bytes? .C:c077 D0 F6 BNE $C06F ;no, write bytes loop .C:c079 20 AE FF JSR $FFAE ;KERNAL Unlisten (drive will execute command) .C:c07c A5 C3 LDA $C3 ;add 32 to pointer $C3~C4 (drive address) .C:c07e 18 CLC .C:c07f 69 20 ADC #$20 .C:c081 85 C3 STA $C3 .C:c083 90 02 BCC $C087 .C:c085 E6 C4 INC $C4 .C:c087 A5 05 LDA $05 ;add 32 to pointer $5~6 (C64 address) .C:c089 18 CLC .C:c08a 69 20 ADC #$20 .C:c08c 85 05 STA $05 .C:c08e 90 02 BCC $C092 .C:c090 E6 06 INC $06 .C:c092 C6 2A DEC $2A ;countdown # M-W commands, all done? .C:c094 D0 C5 BNE $C05B ;no, Memory-Write loop .C:c096 20 96 C1 JSR $C196 ;send "M-E" string to drive (memory execute) .C:c099 A9 7F LDA #$7F ;drive address low .C:c09b 20 A8 FF JSR $FFA8 ;KERNAL Serial Out .C:c09e A9 05 LDA #$05 ;drive address high (i.e., execute $57F) .C:c0a0 20 A8 FF JSR $FFA8 ;KERNAL Serial Out .C:c0a3 A5 B7 LDA $B7 ;filename length .C:c0a5 20 A8 FF JSR $FFA8 ;KERNAL Serial Out .C:c0a8 A0 00 LDY #$00 ;clear index to filename ;filename loop .C:c0aa B1 BB LDA ($BB),Y ;read filename character .C:c0ac 20 A8 FF JSR $FFA8 ;KERNAL Serial Out .C:c0af C8 INY ;index next char .C:c0b0 C4 B7 CPY $B7 ;all chars of filename? .C:c0b2 D0 F6 BNE $C0AA ;no, filename loop .C:c0b4 20 AE FF JSR $FFAE ;KERNAL Unlisten (drive will execute command) .C:c0b7 AD 00 DD LDA $DD00 ;get serial lines .C:c0ba 09 10 ORA #$10 ;pull CLK low, allow DATA high .C:c0bc 8D 00 DD STA $DD00 ;update serial lines .C:c0bf 78 SEI ;disable interrupts (in case VIC IRQ?) ;wait for drive ready .C:c0c0 2C 00 DD BIT $DD00 ;test serial lines, is DATA high? .C:c0c3 30 FB BMI $C0C0 ;yes, wait for drive .C:c0c5 4C 11 C1 JMP $C111 ;do fast-load That code sends 512 bytes of code to the drive RAM $500~6FF. However a single M-W command can't send that much, so the data is transmitted in chunks of 32 bytes. After the code is transferred (which takes about 1.5 seconds), a M-E command instructs the drive to begin execution at $57F. The M-E command will leave the file-length at $205 and filename at $206+ in the drive's RAM (parser buffer). Let's see what the code does. .8:057f A2 02 LDX #$02 ;pull DATA low, allow CLK high .8:0581 8E 00 18 STX $1800 ;update serial lines .8:0584 A9 12 LDA #$12 ;18 .8:0586 85 08 STA $08 ;desired track .8:0588 CA DEX ;1 .8:0589 86 09 STX $09 ;desired sector (start of directory) .8:058b A9 35 LDA #$35 ;set pointer $6F~70 to $435 (for directory search) .8:058d 85 6F STA $6F .8:058f A9 04 LDA #$04 .8:0591 85 70 STA $70 .8:0593 CA DEX ;0 .8:0594 86 3B STX $3B ;index filename for directory search ;scan for comma .8:0596 BD 06 02 LDA $0206,X ;read character from filename .8:0599 C9 2C CMP #$2C ;is it a comma (,)? .8:059b F0 0A BEQ $05A7 ;yes, process comma .8:059d E8 INX ;no, index next character .8:059e EC 05 02 CPX $0205 ;all chars checked? .8:05a1 D0 F3 BNE $0596 ;no, scan for comma ;no comma found .8:05a3 A0 02 LDY #$02 ;filetype index (for PRG) .8:05a5 D0 12 BNE $05B9 ;always, begin filename test ;process comma .8:05a7 BD 07 02 LDA $0207,X ;get character after comma .8:05aa A0 04 LDY #$04 ;5 file types to check ;test type loop .8:05ac D9 B6 FE CMP $FEB6,Y ;test character (D,S,P,U,L) .8:05af F0 05 BEQ $05B6 ;match found .8:05b1 88 DEY ;no match, index prior, all done? .8:05b2 10 F8 BPL $05AC ;no test type loop ;character type invalid .8:05b4 A0 02 LDY #$02 ;default filetype (PRG) .8:05b6 8E 05 02 STX $0205 ;update filename length (shorter: ignore comma and everything after) ;begin filename test .8:05b9 8C 04 02 STY $0204 ;save filetype .8:05bc AD 06 02 LDA $0206 ;first char of filename .8:05bf C9 30 CMP #$30 ;is it a "0"? .8:05c1 D0 0F BNE $05D2 ;no, test for single colon .8:05c3 AD 07 02 LDA $0207 ;second char of filename .8:05c6 C9 3A CMP #$3A ;is it a ":"? .8:05c8 D0 08 BNE $05D2 ;no, test for single colon .8:05ca 20 E9 05 JSR $05E9 ;delete leading character of filename .8:05cd 20 E9 05 JSR $05E9 ;delete leading character of filename .8:05d0 D0 0A BNE $05DC ;always, do search/load ;test for single colon .8:05d2 AD 06 02 LDA $0206 ;first char of filename .8:05d5 C9 3A CMP #$3A ;is it a ":"? .8:05d7 D0 03 BNE $05DC ;no, skip ahead .8:05d9 20 E9 05 JSR $05E9 ;yes, delete leading char of filename ;do search/load .8:05dc A9 E0 LDA #$E0 ;execute buffer command code .8:05de 85 01 STA $01 ;set command for buffer $400 ;wait for command complete .8:05e0 A5 01 LDA $01 ;get command status, is it still pending? .8:05e2 30 FC BMI $05E0 ;yes, wait for command complete .8:05e4 F0 F6 BEQ $05DC ;is loader finished? no, do search/load .8:05e6 4C E7 EB JMP $EBE7 ;yes, exit to ROM That code first signals the C64 that the code is running (but data not ready) by pulling DATA low. Next it sets the desired track and sector (18,1) for the start of the directory in the controller's sector table (for buffer $400). Then it checks the filename for two things: a filetype specifier (like ",S") and/or a leading drive specifier ("0:" or just ":"). If no filetype specifier is found (or it's invalid), the filetype defaults to PRG (value 2). If a drive specifier is found, it is simply deleted (not part of the filename). Finally it stores an "execute buffer" command in the controller's command table. The next time the controller runs, it will move to the correct track (if not already there) and then execute the code at $400. That code will initially search for the given filename. Later the $400 code will read and transfer the file to the C64. Either way that code updates location $01, and this codes tests that result. If the result is zero, the code loops and issues command $E0 again; otherwise the loader ends by jumping to ROM. Let's look at that $400 code: .8:0400 A9 00 LDA #$00 ;set pointe $30~31 to $300 .8:0402 85 30 STA $30 .8:0404 A9 03 LDA #$03 .8:0406 85 31 STA $31 .8:0408 A5 22 LDA $22 ;get current track .8:040a C5 08 CMP $08 ;does it match desired track? .8:040c F0 05 BEQ $0413 ;yes, continue .8:040e A9 00 LDA #$00 ;no, set controller status .8:0410 4C 69 F9 JMP $F969 ;and exit to ROM (we'll get called again later) .8:0413 20 0A F5 JSR $F50A ;find sector header and wait for data-block sync to end (sets .Y = 0) ;wait for high data .8:0416 50 FE BVC $0416 ;wait for byte-ready (high data) .8:0418 B8 CLV ;ready for next .8:0419 AD 01 1C LDA $1C01 ;read drive-head data .8:041c 91 30 STA ($30),Y ;store in $300 buffer .8:041e C8 INY ;index buffer, filled up? .8:041f D0 F5 BNE $0416 ;no, wait for high data .8:0421 A0 BA LDY #$BA ;yes, initialize index for low buffer ;wait for low data .8:0423 50 FE BVC $0423 ;wait for byte-ready (low data) .8:0425 B8 CLV ;ready for more .8:0426 AD 01 1C LDA $1C01 ;read drive-head data .8:0429 99 00 01 STA $0100,Y ;store in $1BA~1FF .8:042c C8 INY ;index next, done? .8:042d D0 F4 BNE $0423 ;no, wait for low data .8:042f 20 E0 F8 JSR $F8E0 ;convert buffer from GCR to binary .8:0432 6C 6F 00 JMP ($006F) ;either $435 (find filename) or $50F (load sector) That code first sets a pointer for the high buffer at $300~3FF. Next it checks if the head is on the correct track; if not, the code will exit to ROM and the code at $5E0 will call us again. In other words, we let the ROM controller do the head-stepping. When the disk-head is on the correct track, we have the ROM search for a sector header. Hopefully it is found and read successfuly, because if not the ROM will set an error code causing our drive code to end prematurely; the poor C64 will be left in an infinit loop! Once the sector is found, the GCR data is read into both a high and low buffer. Then the ROM is called to decode the data (it will be stored in $300~3FF). The code should next calculate and test the data checksum but it does not. This means a bad read will result in corrupt data being sent to the C64. Also the code doesn't check the data-mark byte, which should be 7. A non-standard data-mark could be used as a form of copy-protection, but I don't think this is the case. Finally the code uses an indirect jump to continue. In the first phase (directory search) it will jump to $435. In the second phase (file load) it will jump to $50F Let's look at the $435 code: .8:0435 AD 06 02 LDA $0206 ;first char of filename .8:0438 C9 2A CMP #$2A ;is it "*"? .8:043a D0 0E BNE $044A ;no, do standard name check .8:043c A5 7E LDA $7E ;do we have a prior file's track? .8:043e F0 0A BEQ $044A ;no, do standard name check .8:0440 85 08 STA $08 ;yes set as our file's track .8:0442 AD 6F 02 LDA $026F ;prior file's sector .8:0445 85 09 STA $09 ;set as our file's sector .8:0447 4C B4 04 JMP $04B4 ;set up loading ;standard name check .8:044a A4 3B LDY $3B ;index sector filename (default 0) .8:044c A2 00 LDX #$00 ;index requested filename .8:044e B9 02 03 LDA $0302,Y ;get filetype from sector, is it properly closed? .8:0451 10 1B BPL $046E ;no, next directory entry .8:0453 29 07 AND #$07 ;mask filetype .8:0455 CD 04 02 CMP $0204 ;compare with requested (or default) type .8:0458 D0 14 BNE $046E ;no match, next directory entry ;test filename .8:045a BD 06 02 LDA $0206,X ;read requested filename char .8:045d C9 2A CMP #$2A ;is it an asterisk (*)? .8:045f F0 42 BEQ $04A3 ;yes, file found .8:0461 C9 0D CMP #$0D ;is it an ASCII return? .8:0463 F0 3E BEQ $04A3 ;yes, file found .8:0465 D9 05 03 CMP $0305,Y ;test filename from directory, does it match? .8:0468 F0 27 BEQ $0491 ;yes, next char in filename .8:046a C9 3F CMP #$3F ;no, is requested char "?" ? .8:046c F0 23 BEQ $0491 ;yes, next char in filename ;next directory entry (filename/type mismatch) .8:046e A5 3B LDA $3B ;index in sector .8:0470 18 CLC .8:0471 69 20 ADC #$20 ;add 32 (size of directory entry) .8:0473 85 3B STA $3B ;update index, end of sector? .8:0475 90 D3 BCC $044A ;no, standard filename check .8:0477 AD 00 03 LDA $0300 ;get track of next directory sector .8:047a F0 0E BEQ $048A ;zero → file not found .8:047c 85 08 STA $08 ;set track for next directory block .8:047e AD 01 03 LDA $0301 ;get sector of next directory block .8:0481 C5 09 CMP $09 ;compare with self(!), do they match? .8:0483 F0 05 BEQ $048A ;yes, file not found .8:0485 85 09 STA $09 ;set sector for next directory block .8:0487 4C 00 04 JMP $0400 ;loop to read next sector ;file not found .8:048a A9 FF LDA #$FF ;flag 'file not found' .8:048c 85 21 STA $21 ;set flag .8:048e 4C 0F 05 JMP $050F ;skip ahead ;next character in filename .8:0491 C8 INY ;index char in directory .8:0492 E8 INX ;index char in requested filename .8:0493 EC 05 02 CPX $0205 ;check filename length, end reached? .8:0496 90 C2 BCC $045A ;no, test filename .8:0498 B9 05 03 LDA $0305,Y ;char from directory filename .8:049b C9 A0 CMP #$A0 ;is it a non-breaking-space ? .8:049d F0 04 BEQ $04A3 ;yes, file found .8:049f E0 10 CPX #$10 ;no, have we reached maximum name length (16)? .8:04a1 D0 CB BNE $046E ;no, next directory entry ;file found .8:04a3 A4 3B LDY $3B ;index for directory entry .8:04a5 B9 03 03 LDA $0303,Y ;get starting track .8:04a8 85 08 STA $08 ;save for ourself .8:04aa 85 7E STA $7E ;save for DOS 'last file track' .8:04ac B9 04 03 LDA $0304,Y ;get starting sector .8:04af 85 09 STA $09 ;save for ourself .8:04b1 8D 6F 02 STA $026F ;save for DOS 'last file sector' ;set-up loading .8:04b4 A9 0F LDA #$0F ;set vector $6F~70 to $50F .8:04b6 85 6F STA $6F .8:04b8 A9 05 LDA #$05 .8:04ba 85 70 STA $70 .8:04bc A9 02 LDA #$02 ;flag 'first sector' .8:04be 85 21 STA $21 ;set flag .8:04c0 4C 00 04 JMP $0400 ;read first sector of file This directory search code does several things. First it implements a rarely used (mis)feature of the C1541. After accessing a file (with load or save, for example), the DOS will remember that file's starting track and sector. Then if you try to do something with a file named "*", the DOS will access that last-used file. Many (most?) C64 users think that * will refer to the first file on disk, but this is true only after power-up or a disk swap (i.e., when there is no last-used file). Next the code tests the filename in the directory for a couple of things: properly closed, file-type match, and filename match. The filename matching allows the use of the "*" and "?" wildcards. If a match is found, the track and sector is pulled from the directory and stored for our own code, and for DOS's last-used file. A flag is set (to 2) and code jumps to $50F after reading the requested sector via JMP $400. If a match is not found, a flag is set to $FF and the code jumps directly to $50F (because there is no data to read). Let's see what happens at the $50F code: .8:050f AD 00 03 LDA $0300 ;get next track of file, is it zero? .8:0512 D0 06 BNE $051A ;no, skip ahead .8:0514 A5 21 LDA $21 ;yes, get flag byte .8:0516 09 01 ORA #$01 ;set bit 0 to indicate last sector .8:0518 85 21 STA $21 ;update flag .8:051a A5 21 LDA $21 ;get flag byte .8:051c 20 D3 04 JSR $04D3 ;fast send to C64 .8:051f A5 21 LDA $21 ;(re)get flag byte .8:0521 10 03 BPL $0526 ;continue if okay .8:0523 4C 7A 05 JMP $057A ;exit if file not found .8:0526 29 02 AND #$02 ;test 'first sector' bit .8:0528 F0 0F BEQ $0539 ;not first sector, set starting index .8:052a AD 02 03 LDA $0302 ;get load address, low byte .8:052d 20 D3 04 JSR $04D3 ;fast send to C64 .8:0530 AD 03 03 LDA $0303 ;get laod address, high byte .8:0533 20 D3 04 JSR $04D3 ;fast send to C64 .8:0536 A9 04 LDA #$04 ;starting index (first sector) .8:0538 2C ;skip next instruction ;set starting index .8:0539 A9 04 LDA #$02 ;starting index (not first sector) .8:053b 8D 5F 05 STA $055F ;save start index (self-modifying code) .8:053e A4 21 LDY $21 ;get status flag .8:0540 98 TYA .8:0541 29 01 AND #$01 ;is bit 0 (last sector) set? .8:0543 F0 05 BEQ $054A ;no, calc data length .8:0545 AE 01 03 LDX $0301 ;get ending index .8:0548 CA DEX ;convert to data length .8:0549 2C ;skip next instruction ;calc data length .8:054a A2 FE LDX #$FE ;254 data bytes (not last sector) .8:054c 98 TYA ;get status flag .8:054d 29 02 AND #$02 ;is bit 1 (first sector) set? .8:054f F0 07 BEQ $0558 ;no, set data length .8:0551 CA DEX ;yes, decrease by 2 (load-address already sent) .8:0552 CA DEX .8:0553 98 TYA ;status flag .8:0554 29 FD AND #$FD ;clear bit 1 (first sector) .8:0556 85 21 STA $21 ;save status flag ;set data length .8:0558 86 3C STX $3C ;set data length (1~254 bytes) .8:055a 8A TXA ;move to .A .8:055b 20 D3 04 JSR $04D3 ;fast send to C64 ;transmit data block .8:055e AD 00 03 LDA $0300 ;read from buffer (address modified by code) .8:0561 20 D3 04 JSR $04D3 ;fast send to C64 .8:0564 EE 5F 05 INC $055F ;increment buffer-pointer low .8:0567 C6 3C DEC $3C ;countdown #bytes to send, all done? .8:0569 D0 F3 BNE $055E ;no, transmit data block .8:056b AD 01 03 LDA $0301 ;get next sector from buffer .8:056e 85 09 STA $09 ;save for our code .8:0570 AD 00 03 LDA $0300 ;get next track from buffer .8:0573 85 08 STA $08 ;save for our code, was that last sector? .8:0575 F0 03 BEQ $057A ;yes, set result code .8:0577 4C 00 04 JMP $0400 ;no, loop for next sector ;set result code .8:057a A9 01 LDA #$01 ;status code ok/done .8:057c 4C 69 F9 JMP $F969 ;exit to ROM (will update $01 command-status byte) The first thing the code does is check if the loaded sector is the last one. If so, it sets a bit in a status flag. It transmits whatever the status byte is to the C64. If the status was 'file not found' the code then exits to ROM. Otherwise, it checks the status flag for 'first sector'. When this is true, the first two data bytes from the buffer (the load address) are transmitted. Next the code sets the starting index in the buffer. This is usually 2 but for the first sector it is 4. Then it calculates the number of remaining bytes to transmit. This value is stored in $3C and transmitted to the C64. Finally the code transmits the bulk of the data using the fast protocol. The next track and sector are setup. If this was the last sector, the code exits with code 1 (okay) to ROM. Otherwise the code loops back to $400 to load the next sector.
The C64 has been waiting patiently while the drive searched for the file. Let's see what happens when the drive is finally ready with data: ;loop for sectors .C:c111 20 C8 C0 JSR $C0C8 ;fast-read byte .C:c114 85 06 STA $06 ;save status code .C:c116 A5 06 LDA $06 ;test code, is it okay? .C:c118 10 03 BPL $C11D ;yes, continue .C:c11a 4C 52 C1 JMP $C152 ;no, error exit .C:c11d 29 02 AND #$02 ;test bit 1 (first sector) set? .C:c11f F0 12 BEQ $C133 ;no, get byte count .C:c121 20 C8 C0 JSR $C0C8 ;get load-address low from file .C:c124 A6 B9 LDX $B9 ;test flag -- use file's load address? .C:c126 F0 02 BEQ $C12A ;no, skip ahead (don't set) .C:c128 85 AE STA $AE ;set load-address low .C:c12a 20 C8 C0 JSR $C0C8 ;get load-address high from file .C:c12d A6 B9 LDX $B9 ;test flag -- use file's load address? .C:c12f F0 02 BEQ $C133 ;no, get byte count (don't set) .C:c131 85 AF STA $AF ;set load-address high ;get byte count .C:c133 20 C8 C0 JSR $C0C8 ;fast-load byte count .C:c136 85 05 STA $05 ;save it .C:c138 A0 00 LDY #$00 ;reset index ;loop for data (cycle times in [brackets]) .C:c13a 20 C8 C0 JSR $C0C8 ;[109=6+103]fast-read byte from C1541 .C:c13d 91 AE STA ($AE),Y ;[6]save to C64 RAM .C:c13f E6 AE INC $AE ;[5]increment pointer low, any carry? .C:c141 D0 02 BNE $C145 ;[3]no, skip next instruction .C:c143 E6 AF INC $AF ;[0]increment pointer high .C:c145 C6 05 DEC $05 ;[5]countdown bytes, all done? .C:c147 D0 F1 BNE $C13A ;[3]no, loop for data .C:c149 A5 06 LDA $06 ;get status byte .C:c14b 29 01 AND #$01 ;test last sector? .C:c14d F0 C2 BEQ $C111 ;no, loop for sectors .C:c14f 18 CLC ;flag success (.A = 1) .C:c150 90 07 BCC $C159 ;always, finish up ;error exit .C:c152 A9 04 LDA #$04 ;error code .C:c154 A2 80 LDX #$80 ;error code (device not present!) .C:c156 86 90 STX $90 ;KERNAL Status .C:c158 38 SEC ;flag error ;finish up .C:c159 48 PHA ;save 'error code' .C:c15a A5 07 LDA $07 ;original UserPort and VIC Bank bits .C:c15c 8D 00 DD STA $DD00 ;restore .C:c15f A5 03 LDA $03 ;original Timer A value .C:c161 8D 0E DC STA $DC0E ;restore .C:c164 58 CLI ;enable interrupts .C:c165 68 PLA ;'error code' .C:c166 A6 AE LDX $AE ;get ending address+1 low .C:c168 A4 AF LDY $AF ;and high .C:c16a 60 RTS ;return The first thing the code does is read a status byte and test it. If an error is indicated, the code exits (discussed below). Otherwise it tests for 'first-sector' flag and if set will read the file's load-address. The code may or may not use the file load-address, depending on the setting of KERNAL variable $B9. Next it reads a byte-count and enters a loop to read that many bytes from the disk-drive. Once those bytes are loaded, it tests the status byte to see if that was the last sector. If not, the code loops back to $C111 for another sector. Otherwise it clears the carry flag and branches to finish up. The error routine strangely loads a value of 4 into the accumulator. Strange because the KERNAL does define an error code through the accumulator, but instead through the KERNAL variable 'status' at $90. Also strange is the 'status' code is set to 'device not present' which is obviously a lie! So that's how the main loader works. However I would be remiss if I didn't review the actual fast-read routine: ;wait for data (cycle times in [brackets]) .C:c0c8 AD 00 DD LDA $DD00 ;[4]is serial DATA low? .C:c0cb 10 FB BPL $C0C8 ;[2]yes, wait for data .C:c0cd 29 03 AND #$03 ;[2]allow CLK and DATA to go high (keep VIC Bank, clear UserPort bit) .C:c0cf AA TAX ;[2]save for later .C:c0d0 AD 11 D0 LDA $D011 ;[4]VIC raster bit 8 set? .C:c0d3 30 11 BMI $C0E6 ;[2]yes, do transfer (in border) .C:c0d5 29 10 AND #$10 ;[2]no, is screen blanked? .C:c0d7 F0 0D BEQ $C0E6 ;[2]yes, do transfer ;wait for VIC .C:c0d9 AD 12 D0 LDA $D012 ;[4]VIC raster bits 0~7 .C:c0dc C9 32 CMP #$32 ;[2]test for raster before start of screen .C:c0de 90 06 BCC $C0E6 ;[2]less than, do transfer .C:c0e0 29 07 AND #$07 ;[2]isolate raster-in char bits (shifted!) .C:c0e2 C9 03 CMP #$03 ;[2]test raster value, is it less? .C:c0e4 90 F3 BCC $C0D9 ;[2]yes, wait for VIC .C:c0e6 8E 00 DD STX $DD00 ;[4]allow CLK and DATA high .C:c0e9 A9 00 LDA #$00 ;[2]clear accumulator .C:c0eb A6 00 LDX $00 ;[3]waist time .C:c0ed EA NOP ;[2] .C:c0ee EA NOP ;[2] .C:c0ef EA NOP ;[2] .C:c0f0 EA NOP ;[2] (C:$c0f1) d .C:c0f1 0D 00 DD ORA $DD00 ;[4]merge first 2 bits .C:c0f4 4A LSR A ;[2]shift down two bits .C:c0f5 4A LSR A ;[2] .C:c0f6 EA NOP ;[2] .C:c0f7 0D 00 DD ORA $DD00 ;[4]merge next 2 bits .C:c0fa 4A LSR A ;[2]shift down two bits .C:c0fb 4A LSR A ;[2] .C:c0fc EA NOP ;[2] .C:c0fd 0D 00 DD ORA $DD00 ;[4]merge another 2 bits .C:c100 4A LSR A ;[2]shift down two bits .C:c101 4A LSR A ;[2] .C:c102 85 A5 STA $A5 ;[3]save low 6 bits .C:c104 AD 00 DD LDA $DD00 ;[4]get last 2 bits .C:c107 09 10 ORA #$10 ;[2]pull CLK low, allow DATA high .C:c109 8D 00 DD STA $DD00 ;[4]update serial lines .C:c10c 29 C0 AND #$C0 ;[2]isolate high 2 bits .C:c10e 05 A5 ORA $A5 ;[3]merge low 6 bits .C:c110 60 RTS ;[6] The subroutine takes 103 cycles (not counting JSR $C0C8) and assuming 'typical' VIC behavior: not in the border, screen not blanked, and not 'near' a VIC bad-line. The code is far from optimal as it needlessly waits for the VIC sometimes when it doesn't need to. The correct/better behavior (IMHO) would be to SBC #$32 instead of CMP #$32 at address $C0DC, and then do CMP #1 instead of CMP #3 at address $C0E2. (Such change would need to SEC before the SBC.) When you include the time to store the data in RAM and loop for the next byte (like I do for the other fast loaders), you will see the actual byte-transfer time is about 131 micoseconds. This is more than twice as slow as most of the fast-loaders I've documented. In summary, it's much faster than standard ROM routines, but slow compared to other fast-loaders.
© Hydrophilic.net, 2026 |