| 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 Airborne Ranger and Project Stealth Fighter. What's interesting about this one is that not only does it transmit two bits at a time, but it transmits them in bursts of 4 bytes (32 bits)! The first file on disk is an "auto-run" file. That is, the user types LOAD"*",8,1 and presses return then the program loads and RUNs without further user interaction. This first file on disk is not a BASIC program, but interestingly the BASIC $302 vector is changed to enable "auto-run". In particular, the file loads at $2a7 in RAM and ends at $303. The vector at $302 (which controls BASIC user input) is set to point to $2a7 (which shouldn't be a surprise). Let's look at the code: .C:02a7 A9 00 LDA #$00 ;black .C:02a9 8D 20 D0 STA $D020 ;VIC border color .C:02ac A9 0B LDA #$0B ;blank screen .C:02ae 8D 11 D0 STA $D011 ;VIC register .C:02b1 20 84 FF JSR $FF84 ;KERNAL initialize I/O chips .C:02b4 A9 00 LDA #$00 ;disable .C:02b6 20 90 FF JSR $FF90 ;KERNAL messages (like LOADING) .C:02b9 A9 08 LDA #$08 ;file 8 .C:02bb A2 08 LDX #$08 ;device 8 .C:02bd A0 00 LDY #$00 ;channel 0 .C:02bf 20 BA FF JSR $FFBA ;KERNAL SetLFS .C:02c2 A2 FB LDX #$FB ;.YX = $2FB (filename address -> "C") .C:02c4 A0 02 LDY #$02 .C:02c6 A9 01 LDA #$01 ;filename length .C:02c8 20 BD FF JSR $FFBD ;KERNAL SetNam .C:02cb A2 00 LDX #$00 ;.YX = $4400 (load address) .C:02cd A0 44 LDY #$44 .C:02cf A9 00 LDA #$00 ;load (not verify) .C:02d1 20 D5 FF JSR $FFD5 ;KERNAL Load (standard/slow) .C:02d4 20 03 44 JSR $4403 ;setup fast-loader (see below) .C:02d7 A9 00 LDA #$00 ;redundant .C:02d9 20 90 FF JSR $FF90 ;disable KERNAL messages .C:02dc A9 08 LDA #$08 ;file 8 .C:02de A2 08 LDX #$08 ;device 8 .C:02e0 A0 00 LDY #$00 ;channel 0 .C:02e2 20 BA FF JSR $FFBA ;KERNAL SetLFS .C:02e5 A2 FA LDX #$FA ;.YX = $2FA (filename address -> "T") .C:02e7 A0 02 LDY #$02 .C:02e9 A9 01 LDA #$01 ;filename length .C:02eb 20 BD FF JSR $FFBD ;KERNAL SetNam .C:02ee A2 00 LDX #$00 ;.YX = $800 (load address) .C:02f0 A0 08 LDY #$08 .C:02f2 A9 00 LDA #$00 ;load (not verify) .C:02f4 20 D5 FF JSR $FFD5 ;KERNAL Load (fast) .C:02f7 4C 00 08 JMP $0800 ;start program >C:02fa 54 43 00 00 00 00 8b e3 TC...... >C:0302 a7 02 .. So far everything is pretty straight-forward. The standard (slow) KERNAL Load routine is used to load file "C" into $4400~45fe. This process takes about 3 seconds. Then the fast-load installer at $4403 is called. Finally fast-load is called to load program "T" and execution jumps to $800. Before looking at C64 fast-load code, let's see what the installer does: .C:4403 A9 53 LDA #$53 ;redirect KERNAL Load .C:4405 8D 30 03 STA $0330 ;to address $4453 .C:4408 A9 44 LDA #$44 .C:440a 8D 31 03 STA $0331 .C:440d A9 08 LDA #$08 ;device 8 .C:440f 20 B1 FF JSR $FFB1 ;KERNAL Listen .C:4412 A9 6F LDA #$6F ;channel 15 .C:4414 20 93 FF JSR $FF93 ;KERNAL Second .C:4417 A0 00 LDY #$00 ;index string ;command write loop .C:4419 B9 03 45 LDA $4503,Y ;read string .C:441c 20 A8 FF JSR $FFA8 ;KERNAL serial out .C:441f C8 INY ;index next character .C:4420 C0 0B CPY #$0B ;all done? .C:4422 D0 F5 BNE $4419 ;no, loop .C:4424 20 AE FF JSR $FFAE ;KERNAL unlisten (drive will execute command) .C:4427 20 D0 45 JSR $45D0 ;pull DATA low, allow CLK high .C:442a 2C 00 DD BIT $DD00 ;test serial lines .C:442d 70 FB BVS $442A ;wait while CLK high (until drive pulls it low) .C:442f 60 RTS >C:4503 4d 2d 45 05 02 20 42 d0 M-E.. B. >C:450b 4c b0 07 L.. That code is pretty simple. First it redirects the KERNAL Load routine from ROM to RAM address $4453 (called later, see below). Second it sends a command to the disk drive: Memory-Execute. Finally it allows the CLK line to go high and then waits for CLK line to go low. Now let's see what the drive is executing. It starts with code in the command buffer at $205: .8:0205 20 42 D0 JSR $D042 ;read BAM (redundant) .8:0208 4C B0 07 JMP $07B0 ;jump to code in BAM buffer ~ .8:07b0 A0 3B LDY #$3B ;60 bytes to copy ;copy loop .8:07b2 B9 C4 07 LDA $07C4,Y ;copy following code to buffer $600 .8:07b5 99 C4 06 STA $06C4,Y .8:07b8 B9 0B 02 LDA $020B,Y ;copy mystery data to $150 (why?) .8:07bb 99 50 01 STA $0150,Y .8:07be 88 DEY ;index prior, all done? .8:07bf 10 F1 BPL $07B2 ;no, copy loop .8:07c1 4C C4 06 JMP $06C4 ~ .8:06c4 78 SEI ;prevent controller from running .8:06c5 A0 11 LDY #$11 ;18 bytes to copy ;copy loop .8:06c7 B9 E2 07 LDA $07E2,Y ;read from BAM buffer .8:06ca 99 00 00 STA $0000,Y ;write to controller RAM .8:06cd 88 DEY ;index prior, all done? .8:06ce 10 F7 BPL $06C7 ;no copy loop .8:06d0 58 CLI ;allow controller to run ;wait loop .8:06d1 A0 04 LDY #$04 ;5 buffers to test .8:06d3 A9 00 LDA #$00 ;test loop .8:06d5 19 00 00 ORA $0000,Y ;merge controller status for buffers $300 to $700 .8:06d8 88 DEY ;next buffer, all checked? .8:06d9 10 FA BPL $06D5 ;no, test loop .8:06db C9 01 CMP #$01 ;all buffers report okay? .8:06dd D0 F2 BNE $06D1 ;no, wait loop .8:06df 4C 00 03 JMP $0300 ~ >8:07e2 80 80 80 01 80 01 12 0f ........ >8:07ea 12 10 12 11 01 01 12 12 ........ >8:07f2 41 85 A. Besides copying some data, it mainly sets the track and sector numbers for 4 of the 5 buffers (all but buffer $600) and instructs the drive controller to read each corresponding sector:
Note this initialization code is stored in a normally unused portion of the disk-header, after BAM Side 1. That area is used by double-side disks, but this is a single-sided disk. No prevision is made for a read-error. If that happens the code will be stuck in an infinite loop. Loading those four sectors takes about one second. Anyway, let's see what happens after those four sectors are loaded into drive RAM, starting at $300: .8:0300 78 SEI ;disable interrupts .8:0301 A9 08 LDA #$08 ;pull DATA low, allow CLK high .8:0303 8D 00 18 STA $1800 ;update serial lines (signal not ready) .8:0306 20 CD 03 JSR $03CD ~ .8:03cd A0 C4 LDY #$C4 ;start index ;checksum loop1 .8:03cf 59 00 06 EOR $0600,Y ;calc checksum .8:03d2 C8 INY ;next index .8:03d3 C0 F2 CPY #$F2 ;last index? .8:03d5 D0 F8 BNE $03CF ;no, checksum loop1 .8:03d7 A0 00 LDY #$00 ;zero index .8:03d9 4C A7 03 JMP $03A7 ;continue initialization ~ ;checksum loop2 .8:03a7 59 00 03 EOR $0300,Y ;build checksum .8:03aa 59 00 04 EOR $0400,Y .8:03ad 59 00 05 EOR $0500,Y .8:03b0 59 00 07 EOR $0700,Y .8:03b3 C8 INY ;index next, all done? .8:03b4 D0 F1 BNE $03A7 ;no, checksum loop2 .8:03b6 8D 45 03 STA $0345 ;store checksum ($6b) .8:03b9 60 RTS ~ .8:0309 A0 00 LDY #$00 ;256 bytes per table (redundant) ;build loop .8:030b 98 TYA ;current index .8:030c 99 00 06 STA $0600,Y ;build ID table .8:030f 4A LSR A ;shift high nibble .8:0310 4A LSR A ;down to low nibble .8:0311 4A LSR A .8:0312 4A LSR A .8:0313 99 00 02 STA $0200,Y ;build nibble-shift table .8:0316 C8 INY ;index next, all done? .8:0317 D0 F2 BNE $030B ;no, build loop .8:0319 4C E5 03 JMP $03E5 ;wait for filename So the first part of the initialization does two main things: calculate a checksum of the loaded code bytes, and build two tables in RAM (pages 2 and 6). Then it jumps to a wait loop, ready to receive a filename from the C64. Let's see what happens when the C64 tries to load a file: .C:4453 20 3A 44 JSR $443A ;prepare to send filename ~ .C:443a 20 D0 45 JSR $45D0 ;signal transmission start ~ .C:45d0 AD 00 DD LDA $DD00 ;CIA2 port .C:45d3 29 0F AND #$0F ;keep User-Port bit and VIC-Bank .C:45d5 09 20 ORA #$20 ;pull DATA low, allow CLK high .C:45d7 8D 00 DD STA $DD00 ;update serial bus lines .C:45da 78 SEI ;disable interrupts .C:45db 18 CLC .C:45dc 60 RTS ~ .C:443d AD 15 D0 LDA $D015 ;read sprite enable bits .C:4440 8D FE 45 STA $45FE ;save to re-enable after load .C:4443 A0 00 LDY #$00 ;all sprites off (also reset index) .C:4445 8C 15 D0 STY $D015 ;VIC sprite enable .C:4448 84 90 STY $90 ;no error ;wait loop .C:444a C8 INY ;delay about 5*256 = 1280 microseconds (about 20 rasters) .C:444b D0 FD BNE $444A ;delay loop (for any sprites to finish rendering) .C:444d A9 FF LDA #$FF ;key value = begin filename .C:444f 20 8A 45 JSR $458A ;fast send byte to C1541 .C:4452 60 RTS The first part of the loader sets up the serial bus and disables sprites (which interfere with CPU timing) and waits so any enabled sprites can finish rendering. Although it waits long enough for a standard-display sprite, it needs to wait longer for one which is Y-expanded (i.e., potential bug). More importantly it sends a special byte ($FF) to the C1541 to indicate the start of a filename. Next the actual filename is transmitted: ;filename loop .C:4456 B1 BB LDA ($BB),Y ;read filename character .C:4458 20 8A 45 JSR $458A ;fast send byte to C1541 .C:445b C8 INY ;index next char .C:445c C6 B7 DEC $B7 ;count down #chars .C:445e D0 F6 BNE $4456 ;not all, filename loop .C:4460 A9 00 LDA #$00 ;flag 'end of filename' .C:4462 20 8A 45 JSR $458A ;fast send byte to C1541 .C:4465 AD DD 45 LDA $45DD ;User-Port bit and VIC bank bits (7 for initial load) .C:4468 8D 00 DD STA $DD00 ;allow CLK and DATA to go high .C:446b A9 EA LDA #$EA ;opcode NOP .C:446d AE A6 02 LDX $02A6 ;NTSC/PAL flag .C:4470 F0 05 BEQ $4477 ;NTSC .C:4472 A9 24 LDA #$24 ;opcode BIT zp .C:4474 8D 28 45 STA $4528 ;update loader code .C:4477 8D 41 45 STA $4541 .C:447a 8D 58 45 STA $4558 .C:447d 8D 6F 45 STA $456F ;wait loop .C:4480 2C 00 DD BIT $DD00 ;test serial lines; is CLK high? .C:4483 70 FB BVS $4480 ;yes, wait Besides transmitting the filename to the disk drive, it also modifies its own code. For NTSC machines, the sequence NOP, NOP will appear in some places (about 4 microseconds) while for PAL machines (which are a tad slower) the sequence BIT $EA will appear in those places (about 3 microseconds). This should really be part of the initialization code, and not in the main loader code. Anyway, it creates a delay between the STA $DD00 and BIT $DD00 instructions. This allows the drive time to recognize the 'switch-over' between sending and receiving data. Before we see what the C64 and C1541 do next, let's take a moment to look back at the how the C64 transmits the filename data to the C1541 (same code is used on both NTSC and PAL machines): ;fast transmit to C1541 (cycle times are in [brackets]) .C:458a 48 PHA ;[3]save low nibble .C:458b 4A LSR A ;[2]move high nibble down .C:458c 4A LSR A ;[2] .C:458d 4A LSR A ;[2] .C:458e 4A LSR A ;[2] .C:458f AA TAX ;[2]index high nibble .C:4590 AD 00 DD LDA $DD00 ;[4]serial lines .C:4593 29 07 AND #$07 ;[2]allow both CLK and DATA to go high .C:4595 8D DD 45 STA $45DD ;[4]save User port and VIC bank bits .C:4598 2C 00 DD BIT $DD00 ;[4] test serial lines, is CLK low? .C:459b 50 FB BVC $4598 ;[2] yes, wait (cycle time assumes no wait) .C:459d 20 EF 45 JSR $45EF ;[28 typical]wait for VIC if needed .C:45a0 AD DD 45 LDA $45DD ;[4]allow CLK and DATA to go high .C:45a3 8D 00 DD STA $DD00 ;[4]update serial bus .C:45a6 BD DF 45 LDA $45DF,X ;[4]decode high nibble .C:45a9 0D DD 45 ORA $45DD ;[4]merge VIC and User-port bits .C:45ac 8D 00 DD STA $DD00 ;[4]update serial lines .C:45af 4A LSR A ;[2]shift top 2 bits down .C:45b0 4A LSR A ;[2] .C:45b1 29 F0 AND #$F0 ;[2]mask CLK and DATA bits (really $30) .C:45b3 0D DD 45 ORA $45DD ;[4]merge VIC and User-port bits .C:45b6 8D 00 DD STA $DD00 ;[4]update serial lines .C:45b9 68 PLA ;[4]low nibble .C:45ba 29 0F AND #$0F ;[2]clear high nibble .C:45bc AA TAX ;[2]index low nibble .C:45bd BD DF 45 LDA $45DF,X ;[4]decode low nibble .C:45c0 0D DD 45 ORA $45DD ;[4]merge VIC and User-port bits .C:45c3 8D 00 DD STA $DD00 ;[4]update serial lines .C:45c6 4A LSR A ;[2]shift top 2 bits down .C:45c7 4A LSR A ;[2] .C:45c8 29 F0 AND #$F0 ;[2]mask CLK and DATA bits .C:45ca 0D DD 45 ORA $45DD ;[4]merge VIC and User-port bits .C:45cd 8D 00 DD STA $DD00 ;[4]update serial lines .C:45d0 AD 00 DD LDA $DD00 ;[4]read I/O bits .C:45d3 29 0F AND #$0F ;[2]mask User-port and VIC bits .C:45d5 09 20 ORA #$20 ;[2]pull DATA low .C:45d7 8D 00 DD STA $DD00 ;[4]update serial lines .C:45da 78 SEI ;[2]disable interrupts (redundant) .C:45db 18 CLC ;[2]clear carry (why?) .C:45dc 60 RTS ;[6]exit So the byte transmit code typically takes about 147 microseconds per byte. This is much longer than the receive code (as we'll see below), but is much faster than the standard serial bus routines (over 500 microseconds). Next, let's see what the drive does with that filename: .8:052e A0 00 LDY #$00 ;reset filename index ;wait for filename start flag .8:0530 20 63 05 JSR $0563 ;fast receive from C64 .8:0533 C9 FF CMP #$FF ;start of filename? .8:0535 D0 F9 BNE $0530 ;no, wait for filename ;get filename loop .8:0537 20 63 05 JSR $0563 ;fast receive from C64 .8:053a 99 50 01 STA $0150,Y ;save filename character; is it zero? .8:053d F0 03 BEQ $0542 ;yes, skip ahead .8:053f C8 INY ;index next character .8:0540 D0 F5 BNE $0537 ;always, get filename loop .8:0542 A9 08 LDA #$08 ;pull CLK low, allow DATA high .8:0544 8D 00 18 STA $1800 ;update serial lines .8:0547 AD 50 01 LDA $0150 ;is first character of filename a zero? .8:054a F0 05 BEQ $0551 ;yes, skip ahead (uninstall loader) .8:054c A2 4E LDX #$4E ;?? .8:054e A0 00 LDY #$00 ;reset index .8:0550 60 RTS ;return to $38e ;uninstall fast-loader .8:0551 A0 00 LDY #$00 ;reset index .8:0553 98 TYA ;.A = 0 ;clear RAM loop .8:0554 99 00 02 STA $0200,Y .8:0557 99 00 01 STA $0100,Y .8:055a 99 00 00 STA $0000,Y .8:055d C8 INY ;index next, all done? .8:055e D0 F4 BNE $0554 ;no, clear RAM loop .8:0560 4C 22 EB JMP $EB22 ;yes, initialize RAM and enter command wait-loop That code simply stores the filename at $150 in drive RAM and tests the first byte of the filename. If the filename is null, the drive code un-installs itself (resumes normal ROM code). Otherwise the code returns to the main loop: .8:03e8 B9 50 01 LDA $0150,Y ;read first filename character .8:03eb C9 30 CMP #$30 ;is it ASCII zero? .8:03ed D0 02 BNE $03F1 ;no, skip ahead .8:03ef E8 INX ;yes, skip over it (.X) .8:03f0 C8 INY ;skip (.Y) .8:03f1 B9 50 01 LDA $0150,Y ;read first or second character of filename .8:03f4 C9 3A CMP #$3A ;is it ASCII colon (:)? .8:03f6 D0 01 BNE $03F9 ;no, skip ahead .8:03f8 E8 INX ;yes, skip over it (.X) [but not .Y = strange!] .8:03f9 8E CA 04 STX $04CA ;save 'start-of-filename' index .8:03fc 4C 1C 03 JMP $031C ;continue loader ~ .8:031c 8E F1 04 STX $04F1 ;save 'start-of-filename' index again! .8:031f 4C 27 03 JMP $0327 ;continue loader ~ .8:0327 20 45 07 JSR $0745 ;transmit buffer $600 (ID table) That part of the code checks if the filename begins with "0:" or simply ":" and skips past those characters (using X register) if so. The next thing it does is transmit a 256-byte table of data. It is a simple sequence of bytes running from 0 to 255 (in order). However, the hardware inversion of serial lines, and the bits for the UserPort line and VIC Bank cause the bytes to get scrambled. The C64 will read the scrambled version and build a decode table. This is a terribly slow way to build a decode table in my opinion, but maybe the code size is minimal? Let's take a quick peek at how the C64 builds its decode table before we get back to the drive code: ;wait loop .C:4480 2C 00 DD BIT $DD00 ;is CLK high? .C:4483 70 FB BVS $4480 ;yes, wait loop .C:4485 20 0E 45 JSR $450E ;read 256 bytes into $4600 buffer (scrambled data) .C:4488 A0 00 LDY #$00 ;reset index ;build decode table loop .C:448a BE 00 46 LDX $4600,Y ;read scrambled value .C:448d 98 TYA ;current index .C:448e 9D 00 47 STA $4700,X ;save in decode table .C:4491 C8 INY ;next index, all done? .C:4492 D0 F6 BNE $448A ;no, decode loop Not much to building the table, although transmitting 256 bytes is time-consuming. It needs to been done each time Load is called because the bits of the VIC Bank or User Port line might change. Anyway, let's see what the C1541 does now: .8:032a A9 EE LDA #$EE ;request byte-ready signal .8:032c 8D 0C 1C STA $1C0C ;from the disk read/write head .8:032f A9 01 LDA #$01 ;desired sector .8:0331 85 19 STA $19 .8:0333 A2 12 LDX #$12 ;desired track .8:0335 86 22 STX $22 .8:0337 E8 INX ;19 .8:0338 86 58 STX $58 ;?? .8:033a 20 95 05 JSR $0595 ;move disk head and set bit-rate .8:033d 4C B3 04 JMP $04B3 ;continue loader That code simply activates the drive-head read electronics and moves the disk head to the directory track. Also track 18 sector 1 (start of directory) is set for loading. The C1541 continues by reading the directory and searching for the requested filename: ;directory sector loop .8:04b3 20 E5 05 JSR $05E5 ;toggle drive light .8:04b6 20 C7 03 JSR $03C7 ;read sector (from same track) into $600 buffer .8:04b9 A9 03 LDA #$03 ;set pointer to $603 (track/sector of first file) .8:04bb 85 58 STA $58 .8:04bd A9 06 LDA #$06 .8:04bf 85 59 STA $59 ;start filename test .8:04c1 A0 02 LDY #$02 ;index filename (skip over track/sector) ;test filename loop .8:04c3 B1 58 LDA ($58),Y ;read directory data .8:04c5 C9 A0 CMP #$A0 ;end of directory filename? .8:04c7 F0 27 BEQ $04F0 ;yes, check end of requrested filename .8:04c9 D9 4E 01 CMP $014E,Y ;no, check current character .8:04cc D0 03 BNE $04D1 ;mismatch, skip to next directory entry .8:04ce C8 INY ;ok, next character in filename .8:04cf D0 F2 BNE $04C3 ;always, test filename loop ;skip to next directory entry .8:04d1 A5 58 LDA $58 ;directory pointer low .8:04d3 18 CLC .8:04d4 69 20 ADC #$20 ;size of directory entry .8:04d6 85 58 STA $58 ;update pointer low, end of buffer? .8:04d8 90 E7 BCC $04C1 ;no, start filename test .8:04da AD 01 06 LDA $0601 ;yes, get next sector of directory .8:04dd 85 19 STA $19 ;set for loader .8:04df AD 00 06 LDA $0600 ;next track of directory, is it valid? .8:04e2 D0 CF BNE $04B3 ;yes, directory sector loop ;file not found 😞 .8:04e4 8D 01 06 STA $0601 ;zero second byte of buffer (first already zero) .8:04e7 20 45 07 JSR $0745 ;transmit buffer to C64 .8:04ea 20 EE 05 JSR $05EE ;turn off drive light .8:04ed 4C 09 03 JMP $0309 ;build ID table and wait for filename ;check end of requested filename .8:04f0 B9 4E 01 LDA $014E,Y ;read filename requested .8:04f3 D0 DC BNE $04D1 ;not the end, skip to next directory entry ;file found! .8:04f5 A0 00 LDY #$00 ;index track# .8:04f7 B1 58 LDA ($58),Y ;get starting track# of file .8:04f9 85 22 STA $22 ;set for loader .8:04fb C8 INY ;index next .8:04fc B1 58 LDA ($58),Y ;get starting sector# of file .8:04fe 85 19 STA $19 ;set for loader That code is pretty straight-forward. Interestingly is does not check the file type (DEL/PRG/REL/SEQ/USR). It also does not allow the use of wildcards in the filename request. If the file is not found, the buffer is transmitted to the C64 with the first two bytes set to zero, and then the code jumps back to build the ID table and wait for another filename. If the file is found, the starting track and sector values are extracted from the directory entry and stored in zero page addresses $22 and $19 respectively for the file loader. Next we have the main C1541 loader loop: ;loop to load and transmit sectors .8:0500 20 95 05 JSR $0595 ;move head to track and set bit-rate ;retry sector read .8:0503 20 67 03 JSR $0367 ;read sector and decode it .8:0506 AD FF 05 LDA $05FF ;get leading byte .8:0509 C9 07 CMP #$07 ;correct sector-data mark? .8:050b D0 F6 BNE $0503 ;no, retry sector read .8:050d AD 00 07 LDA $0700 ;get trailing byte (checksum) .8:0510 A0 00 LDY #$00 ;reset index ;checksum loop .8:0512 59 00 06 EOR $0600,Y ;calculate checksum remainder .8:0515 C8 INY ;next byte, all 256 data bytes? .8:0516 D0 FA BNE $0512 ;no, checksum loop .8:0518 C9 00 CMP #$00 ;yes, is checksum valid? .8:051a D0 E7 BNE $0503 ;no, retry sector read .8:051c 20 45 07 JSR $0745 ;yes, transmit $600 buffer to C64 .8:051f AD 01 06 LDA $0601 ;file's next sector .8:0522 85 19 STA $19 ;set for loader .8:0524 AD 00 06 LDA $0600 ;file's next track, is it valid? .8:0527 F0 C1 BEQ $04EA ;no, exit (turn off drive light, build ID table, wait for filename) .8:0529 85 22 STA $22 ;yes, set for loader .8:052b 4C 00 05 JMP $0500 ;loop to load and transmit sectors There's not much to the main loop. It first positions the drive-head over the correct track, then keeps trying to read the desired sector forever. Once the sector is read, it is transmitted to the C64. Finally if the next track is not zero, the loop repeats. (When next track# = zero we've reached the end of file.) Before looking at how the C64 fast-loads the sector of data, let's see how the C1541 reads and decodes a sector: .8:0367 20 D7 07 JSR $07D7 ;find and read sector header ;wait for first byte .8:036a 50 FE BVC $036A ;wait for first byte .8:036c B8 CLV ;ready for next .8:036d AD 01 1C LDA $1C01 ;read byte from disk-head (and discard) .8:0370 A0 00 LDY #$00 ;reset buffer index ;wait for second byte .8:0372 50 FE BVC $0372 ;wait for second byte .8:0374 B8 CLV ;ready for next .8:0375 4C 7B 03 JMP $037B ;skip ahead for third byte .8:0378 20 47 03 JSR $0347 ;wait for sync mark (enter here to read normal directory sector) ;wait for third byte .8:037b 50 FE BVC $037B ;wait for third byte .8:037d B8 CLV ;ready for next .8:037e AD 01 1C LDA $1C01 ;get byte from disk-head .8:0381 8D FF 05 STA $05FF ;save as leading byte ;wait for $600 buffer byte .8:0384 50 FE BVC $0384 ;wait for byte .8:0386 B8 CLV ;ready for next .8:0387 AD 01 1C LDA $1C01 ;read byte from disk-head .8:038a 99 00 06 STA $0600,Y ;store in buffer (low) .8:038d C8 INY ;index next, all done? .8:038e D0 F4 BNE $0384 ;no, wait for $600 buffer byte .8:0390 A0 BC LDY #$BC ;index for $700 buffer (-68) ;wait for $700 buffer byte .8:0392 50 FE BVC $0392 ;wait for byte .8:0394 B8 CLV ;ready for next .8:0395 AD 01 1C LDA $1C01 ;read byte from disk-head .8:0398 99 44 06 STA $0644,Y ;store in $700 buffer ($700~743) .8:039b C8 INY ;index next and count, all done? .8:039c D0 F4 BNE $0392 ;no, wait for $700 buffer byte .8:039e 4C FF 03 JMP $03FF ;do sector decode Interestingly that code discards/ignores the first two bytes which come after the sector header. Normally gap bytes and another sync-mark would occur before the data block, but not in this custom sector / copy protection. Next it reads 325 GCR bytes into RAM. A leading byte goes into $5FF. 256 bytes go to $600~6FF, and 68 bytes go to $700~743. These GCR bytes need to be decoded into 260 regular bytes. I'll post the code below but won't comment most of it; it's a bit-twiddling mess. .8:03ff A9 FF LDA #$FF ;set read and write pointers to $5FF .8:0401 85 52 STA $52 ;read low .8:0403 85 54 STA $54 ;write low .8:0405 A9 05 LDA #$05 .8:0407 85 53 STA $53 ;read high .8:0409 85 55 STA $55 ;write high .8:040b A9 41 LDA #$41 ;65 groups of 5 GCR bytes = 325 GCR bytes .8:040d 85 57 STA $57 ;set counter (translate to $104 regular bytes) ;decode loop .8:040f A0 00 LDY #$00 ;reset index .8:0411 B1 52 LDA ($52),Y .8:0413 48 PHA .8:0414 4A LSR A .8:0415 4A LSR A .8:0416 4A LSR A .8:0417 AA TAX .8:0418 BD A0 F8 LDA $F8A0,X .8:041b 85 56 STA $56 .8:041d C8 INY .8:041e B1 52 LDA ($52),Y .8:0420 0A ASL A .8:0421 AA TAX .8:0422 68 PLA .8:0423 2A ROL A .8:0424 48 PHA .8:0425 8A TXA .8:0426 0A ASL A .8:0427 68 PLA .8:0428 2A ROL A .8:0429 29 1F AND #$1F .8:042b 88 DEY .8:042c AA TAX .8:042d BD C0 F8 LDA $F8C0,X .8:0430 05 56 ORA $56 .8:0432 91 54 STA ($54),Y .8:0434 C8 INY .8:0435 B1 52 LDA ($52),Y .8:0437 4A LSR A .8:0438 08 PHP .8:0439 29 1F AND #$1F .8:043b AA TAX .8:043c BD A0 F8 LDA $F8A0,X .8:043f 85 56 STA $56 .8:0441 C8 INY .8:0442 B1 52 LDA ($52),Y .8:0444 28 PLP .8:0445 6A ROR A .8:0446 4A LSR A .8:0447 4A LSR A .8:0448 4A LSR A .8:0449 AA TAX .8:044a BD C0 F8 LDA $F8C0,X .8:044d 05 56 ORA $56 .8:044f 88 DEY .8:0450 91 54 STA ($54),Y .8:0452 C8 INY .8:0453 B1 52 LDA ($52),Y .8:0455 AA TAX .8:0456 C8 INY .8:0457 B1 52 LDA ($52),Y .8:0459 0A ASL A .8:045a 8A TXA .8:045b 2A ROL A .8:045c 29 1F AND #$1F .8:045e AA TAX .8:045f BD A0 F8 LDA $F8A0,X .8:0462 85 56 STA $56 .8:0464 B1 52 LDA ($52),Y .8:0466 4A LSR A .8:0467 4A LSR A .8:0468 29 1F AND #$1F .8:046a AA TAX .8:046b BD C0 F8 LDA $F8C0,X .8:046e 05 56 ORA $56 .8:0470 88 DEY .8:0471 91 54 STA ($54),Y .8:0473 C8 INY .8:0474 B1 52 LDA ($52),Y .8:0476 0A ASL A .8:0477 29 06 AND #$06 .8:0479 85 56 STA $56 .8:047b C8 INY .8:047c B1 52 LDA ($52),Y .8:047e 29 E0 AND #$E0 .8:0480 05 56 ORA $56 .8:0482 AA TAX .8:0483 BD 00 03 LDA $0300,X .8:0486 85 56 STA $56 .8:0488 B1 52 LDA ($52),Y .8:048a 29 1F AND #$1F .8:048c AA TAX .8:048d BD C0 F8 LDA $F8C0,X .8:0490 05 56 ORA $56 .8:0492 88 DEY .8:0493 91 54 STA ($54),Y .8:0495 A5 52 LDA $52 ;read pointer low .8:0497 18 CLC .8:0498 69 05 ADC #$05 ;advance by five .8:049a 85 52 STA $52 ;update read low .8:049c 90 02 BCC $04A0 ;any carry? .8:049e E6 53 INC $53 ;yes, update read high .8:04a0 A5 54 LDA $54 ;write pointer low .8:04a2 18 CLC .8:04a3 69 04 ADC #$04 ;advance by four .8:04a5 85 54 STA $54 ;update write low .8:04a7 90 02 BCC $04AB ;any carry? .8:04a9 E6 55 INC $55 ;yes, update write high .8:04ab C6 57 DEC $57 ;count down .8:04ad F0 03 BEQ $04B2 ;all done? .8:04af 4C 0F 04 JMP $040F ;no, loop to decode .8:04b2 60 RTS ;yes, exit If I counted correctly, there are 289 cycles (typical) per loop. There are 65 loops so that's 18,785 cycles plus 21 cycles for initialization. That's a bit longer than a full NTSC VIC-II screen (a bit shorter than a PAL VIC-II screen). Although it's very time consuming, it's considerably faster than the C1541 ROM routines. Now let's look at how the NTSC C64 fast-loads each decoded sector into RAM $4600: .C:450e A0 3F LDY #$3F ;max index (64 loops) ;wait for CLK high (cycle times in [brackets]) .C:4510 AD 00 DD LDA $DD00 ;test serial lines .C:4513 10 D5 BPL $44EA ;abort load if DATA low .C:4515 09 30 ORA #$30 ;pull CLK and DATA low (eventually) .C:4517 AA TAX ;save for later .C:4518 29 40 AND #$40 ;test CLK status .C:451a F0 F4 BEQ $4510 ;low, loop ;loop to get 4 bytes [52] (cycle time in [brackets]) .C:451c 20 EF 45 JSR $45EF ;[28]wait for VIC if needed .C:451f 8E 00 DD STX $DD00 ;[4]pull CLK and DATA low .C:4522 8A TXA ;[2]CIA bits .C:4523 29 03 AND #$03 ;[2]mask VIC bank -- Bug! Should be AND #7 .C:4525 8D 00 DD STA $DD00 ;[4]update serial bus (and alter User-port line!) .C:4528 C6 B7 DEC $B7 ;[5]waste 12 cycles .C:452a 48 PHA ;[3] .C:452b 68 PLA ;[4] ;receive byte 1 [37] .C:452c AD 00 DD LDA $DD00 ;[4] get 2 bits .C:452f 4A LSR A ;[2] shift down 2 bits .C:4530 4A LSR A ;[2] .C:4531 4D 00 DD EOR $DD00 ;[4] merge 2 more bits .C:4534 4A LSR A ;[2] shift down 2 bits .C:4535 4A LSR A ;[2] .C:4536 4D 00 DD EOR $DD00 ;[4] merge 2 more bits .C:4539 4A LSR A ;[2] shfit down 2 bits .C:453a 4A LSR A ;[2] .C:453b 4D 00 DD EOR $DD00 ;[4] merge last 2 bits .C:453e 99 00 46 STA $4600,Y ;[5] save in buffer .C:4541 EA NOP ;[2] waste 4 cycles .C:4542 EA NOP ;[2] ;receive byte 2 [37] .C:4543 AD 00 DD LDA $DD00 .C:4546 4A LSR A .C:4547 4A LSR A .C:4548 4D 00 DD EOR $DD00 .C:454b 4A LSR A .C:454c 4A LSR A .C:454d 4D 00 DD EOR $DD00 .C:4550 4A LSR A .C:4551 4A LSR A .C:4552 4D 00 DD EOR $DD00 .C:4555 99 40 46 STA $4640,Y ;save in buffer .C:4558 EA NOP .C:4559 EA NOP ;receive byte 3 [37] .C:455a AD 00 DD LDA $DD00 .C:455d 4A LSR A .C:455e 4A LSR A .C:455f 4D 00 DD EOR $DD00 .C:4562 4A LSR A .C:4563 4A LSR A .C:4564 4D 00 DD EOR $DD00 .C:4567 4A LSR A .C:4568 4A LSR A .C:4569 4D 00 DD EOR $DD00 .C:456c 99 80 46 STA $4680,Y ;save in buffer .C:456f EA NOP .C:4570 EA NOP ;receive byte 4 [38] .C:4571 AD 00 DD LDA $DD00 .C:4574 4A LSR A .C:4575 4A LSR A .C:4576 4D 00 DD EOR $DD00 .C:4579 4A LSR A .C:457a 4A LSR A .C:457b 4D 00 DD EOR $DD00 .C:457e 4A LSR A .C:457f 4A LSR A .C:4580 4D 00 DD EOR $DD00 .C:4583 99 C0 46 STA $46C0,Y ;save in buffer .C:4586 88 DEY ;[2]index next, all done? .C:4587 10 93 BPL $451C ;[3]loop to get 4 bytes .C:4589 60 RTS ;not included in cycle time So once the drive is ready, the code reads 4 bytes in 52+37+37+37+38 = 201 cycles which averages out to about 50 cycles per byte -- assuming typical VIC timing. However you (or rather the C64) won't get typical VIC timing over the full 256-byte transfer! Naively this would take about 256 x 50 = 12,800 cycles for the full sector, but testing in VICE indicates a typical time of 15,300 cycles. Based on the VICE value, it works out to about 60 cycles per byte! (20% more than a naive calculation.) At any rate, the full sector is transferred in less than a full VIC screen (NTSC or PAL). I always assumed this code was using the clunky 'scrambled sector' code so that it would avoid altering (corrupting) the User-Port line ($DD00 bit 2), but due to a bug at $4523, this bit gets cleared (corrupted) anyway! Alas, so much effort down the drain! Even though the C64 has the sector in RAM, it is scrambled and needs to be decoded (unscrambled). This will take more time; let's look at the code: .C:4494 20 0E 45 JSR $450E ;read scrambled sector into $4600~46FF .C:4497 A5 B9 LDA $B9 ;custom load address? .C:4499 F0 10 BEQ $44AB ;yes, skip ahead .C:449b AE 02 46 LDX $4602 ;no, get scrambled address-low .C:449e BD 00 47 LDA $4700,X ;decode .C:44a1 85 C3 STA $C3 ;set address low .C:44a3 AE 03 46 LDX $4603 ;get scrambled address-high .C:44a6 BD 00 47 LDA $4700,X ;decode .C:44a9 85 C4 STA $C4 ;set address high .C:44ab A2 04 LDX #$04 ;starting offset (first sector) .C:44ad AC 00 46 LDY $4600 ;get scrambled next-track .C:44b0 B9 00 47 LDA $4700,Y ;decode, last sector? .C:44b3 F0 18 BEQ $44CD ;yes, skip ahead .C:44b5 4C C5 44 JMP $44C5 ;no, enter main loop ;main file loop .C:44b8 20 0E 45 JSR $450E ;read scrambled sector into $4600~46FF .C:44bb A2 02 LDX #$02 ;starting offset (not first sector) .C:44bd AC 00 46 LDY $4600 ;get scrambled track# .C:44c0 B9 00 47 LDA $4700,Y ;decode, is it zero? .C:44c3 F0 08 BEQ $44CD ;yes, last sector ;full sector byte loop .C:44c5 20 F1 44 JSR $44F1 ;decode byte and write to RAM .C:44c8 D0 FB BNE $44C5 ;not all of sector, full sector byte loop .C:44ca 4C B8 44 JMP $44B8 ;main file loop ;last sector .C:44cd 20 D0 45 JSR $45D0 ;pull DATA low, allow CLK high .C:44d0 AC 01 46 LDY $4601 ;get scrambled byte count .C:44d3 B9 00 47 LDA $4700,Y ;decode, is it zero? .C:44d6 F0 12 BEQ $44EA ;yes, file not found exit .C:44d8 85 B7 STA $B7 ;bytes in last sector ;last sector byte loop .C:44da 20 F1 44 JSR $44F1 ;decode byte and write to RAM .C:44dd C6 B7 DEC $B7 ;count down, finished? .C:44df D0 F9 BNE $44DA ;no, last sector byte loop .C:44e1 18 CLC ;flag no error ;restore sprites and exit .C:44e2 AD FE 45 LDA $45FE ;get sprite enable bits .C:44e5 8D 15 D0 STA $D015 ;update VIC .C:44e8 58 CLI ;enable enterupts .C:44e9 60 RTS ;exit loader ;file not found exit .C:44ea A9 42 LDA #$42 ;error code .C:44ec 85 90 STA $90 .C:44ee 38 SEC ;flag error .C:44ef D0 F1 BNE $44E2 ;always, restore sprites and exit ;decode byte and write to RAM .C:44f1 BC 00 46 LDY $4600,X ;get scrambled data byte .C:44f4 B9 00 47 LDA $4700,Y ;unscramble .C:44f7 A0 00 LDY #$00 ;no index .C:44f9 91 C3 STA ($C3),Y ;write to RAM .C:44fb E6 C3 INC $C3 ;index RAM pointer low .C:44fd D0 02 BNE $4501 ;carry? .C:44ff E6 C4 INC $C4 ;yes, index RAM pointer high .C:4501 E8 INX ;index next byte in sector buffer .C:4502 60 RTS
© Hydrophilic.net, 2026 |