| 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 Super Cycle by Epyx. I believe its name is Vorpal Loader, but I'm not sure. 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. It loads into RAM region $128~205 and works by over-writing the CPU stack with 2's. When the system LOAD routine tries to return to its caller, the CPU will instead execute the code at $203 (i.e., $202+1). Let's look at the code: .C:0203 4C 28 01 JMP $0128 ~ .C:0128 EA NOP .C:0129 EA NOP .C:012a EA NOP .C:012b EA NOP .C:012c EA NOP .C:012d EA NOP .C:012e EA NOP .C:012f EA NOP .C:0130 EA NOP .C:0131 EA NOP .C:0132 EA NOP .C:0133 EA NOP .C:0134 EA NOP .C:0135 EA NOP .C:0136 EA NOP .C:0137 EA NOP .C:0138 EA NOP .C:0139 EA NOP .C:013a EA NOP .C:013b EA NOP .C:013c A9 00 LDA #$00 ;turn off messages (like LOADING) .C:013e 20 90 FF JSR $FF90 ;KERNAL SetMsg .C:0141 A9 0F LDA #$0F ;filename length .C:0143 A2 64 LDX #$64 ;.YX = $164 (address of "VLOADER ") .C:0145 A0 01 LDY #$01 .C:0147 20 BD FF JSR $FFBD ;KERNAL SetNam .C:014a A9 50 LDA #$50 ;file# .C:014c A6 BA LDX $BA ;device# (usually 8) .C:014e A0 01 LDY #$01 ;channel# .C:0150 20 BA FF JSR $FFBA ;KERNAL SetLFS .C:0153 A9 00 LDA #$00 ;load (not verify) .C:0155 A2 FF LDX #$FF ;.YX = dummy address (ignored) .C:0157 A0 FF LDY #$FF .C:0159 20 D5 FF JSR $FFD5 ;KERNAL Load .C:015c A9 26 LDA #$26 ;KERNAL ROM and I/O (no BASIC ROM) .C:015e 85 01 STA $01 ;set memory configuration .C:0160 EA NOP .C:0161 4C 98 BD JMP $BD98 ;start up code I don't know why it starts with so many NOPs, but after that it turns off KERNAL messages and (slow) loads the file "VLOADER " (with a bunch of non-breaking spaces) into the file-indicated load-address of $BD70 (to $BFFF). Then it changes memory configuration (no BASIC ROM) and jumps to $BD98; let's have a peek: .C:bd98 A9 BD LDA #$BD ;set KERNAL NMI vector to $BDDC .C:bd9a 8D 19 03 STA $0319 ;which contains an RTI instruction .C:bd9d A9 DC LDA #$DC .C:bd9f 8D 18 03 STA $0318 .C:bda2 A9 00 LDA #$00 ;set TimerA high .C:bda4 8D 05 DD STA $DD05 .C:bda7 A9 1E LDA #$1E ;set TimerA low (about 31 microseconds) .C:bda9 8D 04 DD STA $DD04 .C:bdac A9 81 LDA #$81 .C:bdae 8D 0D DD STA $DD0D ;enable TimerA interrupts .C:bdb1 8D 0E DD STA $DD0E ;set TOD to PAL and start TimerA .C:bdb4 20 F1 BF JSR $BFF1 ;indirect call $BECE ~ .C:bece 2C FD BE BIT $BEFD ;do nothing .C:bed1 A9 4C LDA #$4C ;opcode JMP absolute .C:bed3 8D CE BE STA $BECE ;modify code (next call to $BFF1 will JMP directly to $BEFD) .C:bed6 AD A6 02 LDA $02A6 ;NTSC/PAL flag .C:bed9 C9 01 CMP #$01 ;is this PAL .C:bedb D0 20 BNE $BEFD ;no, skip ahead ;(a PAL machine will modify several bytes here -- not shown) .C:befd 20 FB BD JSR $BDFB ;effectively OPEN 2,8,2,"#" (note device 8 is hard-coded) .C:bf00 20 ED BD JSR $BDED ;effectively OPEN 15,8,15,"B-E 2 0 18 6" .C:bf03 78 SEI .C:bf04 4C BF BE JMP $BEBF ~ .C:bebf AD 00 DD LDA $DD00 ;get serial I/O bits .C:bec2 29 0F AND #$0F ;allow CLK and DATA to go high .C:bec4 8D 00 DD STA $DD00 ;update serial lines .C:bec7 AE 76 BE LDX $BE76 ;255 .C:beca AC 77 BE LDY $BE77 ;255 .C:becd 60 RTS ~ .C:bdb7 A9 00 LDA #$00 .C:bdb9 85 FE STA $FE .C:bdbb A9 40 LDA #$40 .C:bdbd 85 FF STA $FF .C:bdbf A2 19 LDX #$19 ;2nd command byte: starting track# .C:bdc1 A0 00 LDY #$00 ;3rd command byte: starting sector# .C:bdc3 A9 63 LDA #$63 ;1st command byte: # sectors to load .C:bdc5 20 F4 BF JSR $BFF4 ;call fast load (JMP $BF07) The first thing the code does is set the NMI vector to an RTI instruction and sets CIA2 (the NMI source) to run its timer and trigger an NMI. The NMI will return immediately without clearing the trigger. This means further NMIs (from the Restore key, for example) are not possible. Next it calls an initialize routine $BFF1. The first time it is called it will modify itself so it is faster in the future. But the first time it will check NTSC/PAL system flag. If it is PAL, several code bytes will be updated (not very important for us). More importantly, it will send a Block-Execute (B-E) command to the disk drive. We'll look at that momentarily. But first the routine allows the serial CLK and DATA lines to go high. Finally it goes to load the first file. Before looking at the C64 side of the fast-loader, let's see what the disk drive is doing: .8:0500 78 SEI ;disable controller .8:0501 D8 CLD ;clear BCD mode (these guys are serious or silly, you decide) .8:0502 A9 08 LDA #$08 ;pull CLK low, allow DATA high .8:0504 8D 00 18 STA $1800 ;update serial lines .8:0507 A9 60 LDA #$60 ;RTS opcode .8:0509 8D 00 03 STA $0300 ;store in RAM (this is silly, RTS opcodes occur throught ROM) .8:050c 20 00 03 JSR $0300 ;immediate return (with return address on stack) .8:050f BA TSX ;stack pointer → X register .8:0510 BD 00 01 LDA $0100,X ;get address high .8:0513 85 15 STA $15 ;set pointer high .8:0515 A0 00 LDY #$00 ;address low (and index zero) .8:0517 84 14 STY $14 ;set pointer low ;copy loop .8:0519 B1 14 LDA ($14),Y ;read from whichever buffer we're in .8:051b 99 00 07 STA $0700,Y ;store to known location .8:051e C8 INY ;index next, all done? .8:051f D0 F8 BNE $0519 ;no, copy loop .8:0521 4C 24 07 JMP $0724 ;yes, (re)initialize RAM ~ ;(re)initialize RAM .8:0724 A2 0E LDX #$0E ;index last (15 bytes to copy) ;setup read blocks .8:0726 BD 45 07 LDA $0745,X ;read data table .8:0729 95 00 STA $00,X ;store for controller .8:072b CA DEX ;index prior, all done? .8:072c 10 F8 BPL $0726 ;no, setup read blocks .8:072e 58 CLI ;allow controller to run ;wait loop .8:072f A5 00 LDA $00 ;get buffer $300 status .8:0731 05 01 ORA $01 ;merge buffer $400 status .8:0733 05 02 ORA $02 ;merge buffer $500 status .8:0735 05 03 ORA $03 ;merge buffer $600 status .8:0737 30 F6 BMI $072F ;any command still running? yes, wait loop .8:0739 C9 01 CMP #$01 ;no, any error? .8:073b D0 E7 BNE $0724 ;yes, (re)initialize RAM .8:073d A9 00 LDA #$00 ;allow CLK and DATA to go high .8:073f 8D 00 18 STA $1800 ;update serial bus lines .8:0742 4C 03 04 JMP $0403 ;start loader (get 'filename') ~ >8:0745 80 80 80 80 00 00 12 09 ........ >8:074d 12 0c 12 0f 12 12 ...... The first thing done by the code (besides SEI and CLD) is to update the serial bus. Next the code does a trick to copy itself into $700~7FF. You see, this code could be in any buffer ($300, $400, $500, or $600), but we don't know which. So we JSR to an RTS instruction (which immediately returns) with the side-effect of leaving the return address (our location) on the stack. So we examine the stack to determine where the code is currently at. Once we know, we setup a pointer and copy this (wherever this is) buffer to a known location (buffer $700). Then we jump to the copy at $724. Next we copy commands and track/sector pairs into low RAM for the drive's controller. This instructs the controller to load 4 buffers as follows:
The code then waits for all four commands to execute and checks if there is any error. The code loops to re-load if there was an error. Otherwise it updates the serial bus lines and jumps to $403. This whole process (load four buffers) only takes about 1 second. The code continues at $403: .8:0403 A2 45 LDX #$45 ;reset CPU stack .8:0405 9A TXS .8:0406 AD 00 1C LDA $1C00 ;multi-use register .8:0409 29 F7 AND #$F7 ;turn off drive's LED .8:040b 8D 00 1C STA $1C00 .8:040e 58 CLI ;enable interrupts .8:040f 20 A5 06 JSR $06A5 ;read a byte from C64 .8:0412 78 SEI ;disable interrupts .8:0413 48 PHA ;save command .8:0414 20 A5 06 JSR $06A5 ;read a byte from C64 .8:0417 85 0E STA $0E ;save track# .8:0419 20 A5 06 JSR $06A5 ;read a byte from C64 .8:041c 85 8D STA $8D ;save sector# (for us) .8:041e 85 0F STA $0F ;save it again (for controller) .8:0420 AD 00 1C LDA $1C00 ;multi-use register .8:0423 09 08 ORA #$08 ;turn on drive's LED .8:0425 8D 00 1C STA $1C00 .8:0428 68 PLA ;get back command .8:0429 85 8B STA $8B ;save as # sectors .8:042b C9 FF CMP #$FF ;is command 'read sector'? .8:042d F0 12 BEQ $0441 ;yes, go do it .8:042f C9 FC CMP #$FC ;is command 'custom code'? (read code into $700 buffer and execute) .8:0431 F0 24 BEQ $0457 ;yes, go do it .8:0433 C9 FE CMP #$FE ;is command 'write sector'? .8:0435 F0 07 BEQ $043E ;yes, go do it .8:0437 C9 FD CMP #$FD ;is command 'uninstall'? .8:0439 D0 25 BNE $0460 ;no, do fast-load .8:043b 4C 4B EB JMP $EB4B ;yes, exit to ROM: init RAM and controller wait .8:043e 4C 30 03 JMP $0330 ;do 'write sector' That code is the (re)start of a custom command loop. It turns off the drive LED and waits for 3 bytes (a command) to be received from the C64. Once it gets them, it turns on the drive's LED and then branches to a routine based on the command# (the first byte received). For fast-load, the first byte is the number of sectors to load. Let's look at how the C64 transmits one of those command bytes: .C:be1c 8D 7D BE STA $BE7D ;save byte to transmit .C:be1f A2 08 LDX #$08 ;#bits to transmit ;wait for CLK low -- loop for bits .C:be21 2C 00 DD BIT $DD00 ;[4]is CLK low? <--+-- these execute 3 times (20 cycles) .C:be24 50 FB BVC $BE21 ;[2 or 3]no, wait <--+ .C:be26 AD 00 DD LDA $DD00 ;[4] .C:be29 29 DF AND #$DF ;[2]allow DATA high .C:be2b 8D 00 DD STA $DD00 ;[4]update serial bus .C:be2e AD 00 DD LDA $DD00 ;[4] .C:be31 09 10 ORA #$10 ;[2]pull CLK low .C:be33 8D 00 DD STA $DD00 ;[4]update serial bus .C:be36 C9 80 CMP #$80 ;[2]copy DATA-in to carry flag .C:be38 6E 7E BE ROR $BE7E ;[5]save DATA bit .C:be3b 29 BF AND #$BF ;[2]clear CLK-in, .A = $97 (assuming VIC Bank 0 and UserPort bit 1) ;wait for DATA low .C:be3d CD 00 DD CMP $DD00 ;[4]is DATA high? <--+-- these execute 3 times (20 cycles) .C:be40 F0 FB BEQ $BE3D ;[2 or 3]yes, wait <--+ .C:be42 4E 7D BE LSR $BE7D ;[5]get low bit of data into carry flag .C:be45 A9 00 LDA #$00 ;[2]CLK high, assume DATA high (hardware inversion) .C:be47 90 02 BCC $BE4B ;[3.5]good assumption .C:be49 A9 20 LDA #$20 ;[0]CLK high, DATA low (hardware inversion) .C:be4b 0D 00 DD ORA $DD00 ;[4]merge with VIC Bank and UserPort bits .C:be4e 8D 00 DD STA $DD00 ;[4]update serial lines .C:be51 29 EF AND #$EF ;[2]CLK high (again) .C:be53 8D 00 DD STA $DD00 ;[4]update serial lines ;wait for CLK low .C:be56 2C 00 DD BIT $DD00 ;[4]is CLK high? <--+-- these execute 4 times (27 cycles) .C:be59 70 FB BVS $BE56 ;[2 or 3]yes, wait <--+ .C:be5b 49 20 EOR #$20 ;[2]toggle DATA out bit .C:be5d 8D 00 DD STA $DD00 ;[4]update serial lines .C:be60 CA DEX ;[2]count bits, all done? .C:be61 D0 BE BNE $BE21 ;[3]no, loop for bits .C:be63 AD 7E BE LDA $BE7E ;get DATA received .C:be66 60 RTS That code takes an average 131.5 cycles per loop for a total of 1051 cycles per byte. That's slow for a custom transfer routine (similar to standard ROM), but if you notice: the code not only transmits but receives a byte too! Another nice thing is the code can run without the screen being blanked. Anyway, let's look at what the drive does when it receives a fast-load command: .8:0460 20 86 05 JSR $0586 ;use ROM to read sector into $700 (just for track movement?) .8:0463 20 31 05 JSR $0531 ;set bit-rate, sectors/track, and sector interleave ;loop for sectors .8:0466 A5 8D LDA $8D ;physical sector # .8:0468 18 CLC .8:0469 6D 15 02 ADC $0215 ;add interleave value .8:046c C5 15 CMP $15 ;greater/equal to # sectors/track ? .8:046e 90 02 BCC $0472 ;no, continue .8:0470 E5 15 SBC $15 ;yes, subtract #sectors in track .8:0472 85 0F STA $0F ;logical sector# to read .8:0474 20 B5 05 JSR $05B5 ;read sector to $1B0~1FF, $700~7EF (320 GCR bytes, no decoding) ;synchronize drive with C64 .8:0477 A9 00 LDA #$00 ;allow CLK and DATA high .8:0479 8D 00 18 STA $1800 ;update serial lines .8:047c EA NOP ;delay 4 cycles .8:047d EA NOP .8:047e A9 08 LDA #$08 ;pull CLK low, allow DATA high .8:0480 8D 00 18 STA $1800 ;update serial lines .8:0483 24 80 BIT $80 ;delay 3 cycles .8:0485 A9 00 LDA #$00 ;allow CLK and DATA high .8:0487 8D 00 18 STA $1800 ;update serial lines .8:048a EA NOP ;delay 2 cycles .8:048b A9 08 LDA #$08 ;pull CLK low, allow DATA high .8:048d 8D 00 18 STA $1800 ;update serial lines .8:0490 A0 4F LDY #$4F ;80 loops ;loop to send bytes .8:0492 A9 00 LDA #$00 .8:0494 8D 00 18 STA $1800 .8:0497 BE B0 01 LDX $01B0,Y ;byte 1 low 2 bits .8:049a 8E 00 18 STX $1800 ;update serial lines .8:049d BD 00 03 LDA $0300,X ;byte 1 bits 2~5 (4 bits) .8:04a0 8D 00 18 STA $1800 ;update serial lines .8:04a3 0A ASL A ;next two bits .8:04a4 29 0F AND #$0F ;isolate serial line bits .8:04a6 8D 00 18 STA $1800 ;update serial lines .8:04a9 BE 00 07 LDX $0700,Y ;bit 1 high 2 bits .8:04ac 8E 00 18 STX $1800 ;update serial lines .8:04af BD 00 03 LDA $0300,X ;byte 2 bits 0~3 (4 bits) .8:04b2 8D 00 18 STA $1800 ;update serial lines .8:04b5 0A ASL A ;next two bits .8:04b6 29 0F AND #$0F ;isolate serial line bits .8:04b8 8D 00 18 STA $1800 ;update serial lines .8:04bb BE 50 07 LDX $0750,Y ;byte 2 bits 5,6 .8:04be 8E 00 18 STX $1800 ;update serial lines .8:04c1 BD 00 03 LDA $0300,X ;byte 2 bits 6,7 and byte 3 bits 0,1 .8:04c4 8D 00 18 STA $1800 ;update serial lines .8:04c7 0A ASL A ;next two bits .8:04c8 29 0F AND #$0F ;isolate serial line bits .8:04ca 8D 00 18 STA $1800 ;update serial lines .8:04cd BE A0 07 LDX $07A0,Y ;byte 3 bits 2,3 .8:04d0 8E 00 18 STX $1800 ;update serial lines .8:04d3 BD 00 03 LDA $0300,X ;byte 3 bits 4~7 (4 bits) .8:04d6 8D 00 18 STA $1800 ;update serial lines .8:04d9 0A ASL A ;next two bits .8:04da 29 0F AND #$0F ;isolate serial lines .8:04dc 8D 00 18 STA $1800 ;update serial lines .8:04df A9 08 LDA #$08 ;pull CLK low, allow DATA high .8:04e1 8D 00 18 STA $1800 ;update serial lines .8:04e4 88 DEY ;countdown byte triples, all done? .8:04e5 10 AB BPL $0492 ;no, loop to send bytes .8:04e7 A5 8D LDA $8D ;physical sector# .8:04e9 18 CLC .8:04ea 69 02 ADC #$02 ;add constant two .8:04ec C5 15 CMP $15 ;greater/equal to #sectors per track? .8:04ee 90 06 BCC $04F6 ;no skip ahead .8:04f0 E5 15 SBC $15 ;subtract #sectors per track .8:04f2 45 8C EOR $8C ;apply correction, all sectors in track done? .8:04f4 F0 0C BEQ $0502 ;yes, next track ;resume after next track .8:04f6 85 8D STA $8D ;update physical sector# .8:04f8 C6 8B DEC $8B ;countdown # sectors in file, all done? .8:04fa F0 03 BEQ $04FF ;yes, return to main command loop .8:04fc 4C 66 04 JMP $0466 ;no, loop for sectors .8:04ff 4C 03 04 JMP $0403 ;go to main command loop ;next track .8:0502 A2 02 LDX #$02 ;2 half-track steps ;move head loop .8:0504 AD 00 1C LDA $1C00 ;this is a convoluted way .8:0507 38 SEC ;to add 1 to the stepper motor .8:0508 2A ROL A .8:0509 29 03 AND #$03 ;isolate scrambled stepper value .8:050b 4D 00 1C EOR $1C00 ;unscramble stepper bits and merge non-stepper bits .8:050e 8D 00 1C STA $1C00 ;set new head position .8:0511 A0 0A LDY #$0A ;setup delay for about 10,000 cycles ;outer delay .8:0513 A9 C7 LDA #$C7 .8:0515 38 SEC ;inner delay .8:0516 E9 01 SBC #$01 ;subtract 1, inner delay done? .8:0518 B0 FC BCS $0516 ;no, loop to inner delay .8:051a 88 DEY ;subtract 1, outer delay done? .8:051b D0 F6 BNE $0513 ;no, loop to outer delay .8:051d CA DEX ;countdown half-track steps, done? .8:051e D0 E4 BNE $0504 ;no, move head loop .8:0520 E6 0E INC $0E ;next (greater) track# .8:0522 A5 0E LDA $0E ;get new track# .8:0524 C9 12 CMP #$12 ;is it directory track 18? .8:0526 F0 DA BEQ $0502 ;yes, next track .8:0528 E6 22 INC $22 ;next track# for look-up table .8:052a 20 31 05 JSR $0531 ;set bit-rate, sectors/track, and sector interleave .8:052d A9 00 LDA #$00 ;next sector# .8:052f F0 C5 BEQ $04F6 ;always, resume after next track Wow, that's quite a lot! Let's break it down. First (and strangely) it uses ROM to read the requested track and sector, but that data is never used! As far as I can tell, it's just a cheap way (fewest code bytes) to get the ROM to move the disk head to the desired track. Then that code sets the head bit-rate, # sectors per track, and sector interleave according to a table:
Next the code calculates the next logical sector# (sector# stored in sector header) based on the current physical sector# and the sector interleave for the track. A variety of sector interleaves are used as (presumably) a form of data obfusication. The code then calls $525 to read in the raw GCR bytes into two buffers (320 bytes total). This scheme uses a custom sector header with a 3-byte gap before the data. Interestingly, the routine doesn't do any GCR decoding before returning to the main loader. Next the code toggles the CLK line twice with a decreasing number of cycles between each transition. This allows the C64 to "zero in" or "lock on" to the timing of the disk drive. They need to be aligned pretty close because the following code uses minimal synchronization. The bulk of the code decodes the GCR data and transmits it to the C64... at the same time! It uses a custom 4-byte to 3-byte (32-bit to 24-bit) decoding scheme. This has a 33% overhead which is significantly more than the 25% overhead of standard GCR coding. (However, it's not as wasteful as some schemes which have a 50% overhead.) The codes sends a whopping 240 bytes with only minimal synchronization (occassional 1-cycle delay)! This is done by 80 loops of 3 bytes each. Since there is no separate routine for decoding, and the byte transmission is done in bulk, this loader is extremely fast. (Unfortunately the screen must be blanked.) After transmitting the data, the code calculates the next physical sector# using a constant interleave value of 2. If this results in sector zero, the head is moved to the next greater track. Either way, the # sectors requested is decremented and the code loops if sectors remain (if none remain, it jumps back to the command loop at $403). In case it isn't obvious, the code does not perform any type of data validation! If the data is corrupt due to a bad read, it will be transmitted to the C64 anyway. The poor C64 (and ultimately the user) will have to deal with the consequences... Anyway, let's look at how the NTSC C64 handles the data stream: .C:bf41 A9 07 LDA #$07 ;allow CLK and DATA high (VIC Bank 0, UserPort bit 1) .C:bf43 8D 00 DD STA $DD00 ;update serial lines ;loop for sectors .C:bf46 2C 00 DD BIT $DD00 ;is CLK low? .C:bf49 50 FB BVC $BF46 ;yes, wait .C:bf4b 2C 00 DD BIT $DD00 ;is CLK low? .C:bf4e 50 03 BVC $BF53 ;yes, skip ahead .C:bf50 24 80 BIT $80 ;no, delay 5 cycles .C:bf52 EA NOP .C:bf53 2C 00 DD BIT $DD00 ;is CLK high? .C:bf56 70 02 BVS $BF5A ;yes, skip ahead .C:bf58 24 80 BIT $80 ;no, delay 3 cycles .C:bf5a 2C 00 DD BIT $DD00 ;is CLK high? .C:bf5d 70 00 BVS $BF5F ;yes, delay 1 cycle .C:bf5f EA NOP ;delay 12 cycles .C:bf60 EA NOP .C:bf61 EA NOP .C:bf62 EA NOP .C:bf63 EA NOP .C:bf64 EA NOP .C:bf65 A0 4F LDY #$4F ;80 passes (for 240 bytes) ;loop for bytes (cycle times in [brackets], avg. 115.5 per pass) .C:bf67 AD 00 DD LDA $DD00 ;[4]get 2 bits .C:bf6a 4A LSR A ;[2]shift down two bits .C:bf6b 4A LSR A ;[2] .C:bf6c 4D 00 DD EOR $DD00 ;[4]merge 2 bits .C:bf6f 4A LSR A ;[2]shift down two bits .C:bf70 4A LSR A ;[2] .C:bf71 4D 00 DD EOR $DD00 ;[4]merge 2 bits .C:bf74 4A LSR A ;[2]shift down two bits .C:bf75 4A LSR A ;[2] .C:bf76 4D 00 DD EOR $DD00 ;[4]merge 2 bits .C:bf79 48 PHA ;[3]save byte .C:bf7a EA NOP ;[2] .C:bf7b AD 00 DD LDA $DD00 ;[4]get 2 bits .C:bf7e 4A LSR A ;[2]shift down two bits .C:bf7f 4A LSR A ;[2] .C:bf80 4D 00 DD EOR $DD00 ;[4]merge 2 bits .C:bf83 4A LSR A ;[2]shift down two bits .C:bf84 4A LSR A ;[2] .C:bf85 4D 00 DD EOR $DD00 ;[4]merge 2 bits .C:bf88 4A LSR A ;[2]shift down two bits .C:bf89 4A LSR A ;[2] .C:bf8a 4D 00 DD EOR $DD00 ;[4]merge 2 bits .C:bf8d 48 PHA ;[3]save byte .C:bf8e EA NOP ;[2] .C:bf8f AD 00 DD LDA $DD00 ;[4]get 2 bits .C:bf92 4A LSR A ;[2]shift down two bits .C:bf93 4A LSR A ;[2] .C:bf94 4D 00 DD EOR $DD00 ;[4]merge 2 bits .C:bf97 4A LSR A ;[2]shift down two bits .C:bf98 4A LSR A ;[2] .C:bf99 4D 00 DD EOR $DD00 ;[4]merge 2 bits .C:bf9c 4A LSR A ;[2]shift down two bits .C:bf9d 4A LSR A ;[2] .C:bf9e 4D 00 DD EOR $DD00 ;[4]merge 2 bits .C:bfa1 48 PHA ;[3]save byte .C:bfa2 EA NOP ;[2] .C:bfa3 24 80 BIT $80 ;[3]delay .C:bfa5 2C 00 DD BIT $DD00 ;[4]is CLK low? .C:bfa8 50 00 BVC $BFAA ;[2.5] yes, delay 1 cycle .C:bfaa EA NOP ;[2] .C:bfab 88 DEY ;[2]coundown passes, all done? .C:bfac 10 B9 BPL $BF67 ;[3]no, loop for bytes .C:bfae A5 01 LDA $01 ;get memory config .C:bfb0 8D 7C BE STA $BE7C ;save it .C:bfb3 29 FC AND #$FC ;all RAM .C:bfb5 85 01 STA $01 ;update memory configuration .C:bfb7 A0 F0 LDY #$F0 ;#bytes to write (240) ;loop to store bytes .C:bfb9 68 PLA ;get data .C:bfba 49 F9 EOR #$F9 ;unscramble (due to hardware inversion and VIC Bank and UserPort bits) .C:bfbc 88 DEY ;index and countdown, all done? .C:bfbd 91 FE STA ($FE),Y ;(store data) .C:bfbf D0 F8 BNE $BFB9 ;no, loop to store bytes .C:bfc1 AD 7C BE LDA $BE7C ;orig. mem. config. .C:bfc4 85 01 STA $01 ;restore memory configuration .C:bfc6 A5 FE LDA $FE ;load address low .C:bfc8 18 CLC .C:bfc9 69 F0 ADC #$F0 ;add 240 .C:bfcb 85 FE STA $FE ;update low, any carry? .C:bfcd 90 02 BCC $BFD1 ;no, countdown sectors .C:bfcf E6 FF INC $FF ;update address high ;countdown sectors .C:bfd1 CE 78 BE DEC $BE78 ;countdown, all done? .C:bfd4 F0 03 BEQ $BFD9 ;yes, finish up .C:bfd6 4C 46 BF JMP $BF46 ;no, loop for sectors ;finish up .C:bfd9 AD 7A BE LDA $BE7A ;orig $DD00 value .C:bfdc 8D 00 DD STA $DD00 .C:bfdf AD 79 BE LDA $BE79 ;orig $D015 value .C:bfe2 8D 15 D0 STA $D015 .C:bfe5 AD 7B BE LDA $BE7B ;orig $D011 value .C:bfe8 29 7F AND #$7F ;clear raster bit 8 .C:bfea 4C 70 BD JMP $BD70 ;restore VIC $D011 and exit Since we know what the drive is doing, the C64 code above shouldn't be too hard to understand. It begins by setting the serial lines, VIC Bank, and UserPort bit to a known state. Next it repeatedly checks the CLK line on the serial bus, narrowing the gap/delay between attempts to get the computer in-sync with the drive. Next it fast-loads 240 bytes of data, pushing each byte onto the stack. This means only 16 stack bytes are allowed for the CPU/program to use. This process takes a 'mere' 9239 cycles on average. This is noticibly less than a full VIC screen (even on the shorter NTSC). Next it pulls the data off the stack and stores it in the appropriate RAM location. The time needed for this is not counted towards the loading speed because it occurs in parallel with the drive loading the next sector. In other words, by the time the drive has a new sector ready, this 'unloading' has already finished. Note there is no provision for the number of bytes in the last sector. It always loads data in chunks of 240 bytes. This would be a problem for a general-purpose loader. Anyway, after all sectors are loaded, it restores hardware registers and exits.
© Hydrophilic.net, 2026 |