Home > CBM > Disk > Fast Load > GEOS | ||||||
This fast-loader (and fast-saver) was used by Berkley Softwork's GEOS (at least v1.5). The copy protection is in the boot loader. The boot-loader code is very similar to the "main" fast-load routines. I'm only going to document the "main" routines for clarity and brevity. Before a disk-command is issued, GEOS will call the following routine to prepare the C64 for disk operations. .C:9048 08 PHP ;save CPU status on stack .C:9049 68 PLA ;get CPU status into .A .C:904a 8D 33 9D STA $9D33 ;save for exit .C:904d 78 SEI ;disable interrupts .C:904e A5 01 LDA $01 ;get memory configuration .C:9050 8D 35 9D STA $9D35 ;save for exit .C:9053 A9 36 LDA #$36 ;I/O and KERNAL ROM .C:9055 85 01 STA $01 ;set memory configuration .C:9057 AD 1A D0 LDA $D01A ;get VIC IRQ enables .C:905a 8D 34 9D STA $9D34 ;save for exit .C:905d A0 00 LDY #$00 .C:905f 8C 1A D0 STY $D01A ;disable all VIC IRQ sources .C:9062 A9 7F LDA #$7F ;multi-use constant .C:9064 8D 19 D0 STA $D019 ;clear any pending VIC IRQs .C:9067 8D 0D DC STA $DC0D ;disable all CIA1 interrupt sources .C:906a 8D 0D DD STA $DD0D ;disable all CIA2 interrupt sources .C:906d A9 90 LDA #$90 ;set KERNAL IRQ vector to $90C4 .C:906f 8D 15 03 STA $0315 .C:9072 A9 C4 LDA #$C4 .C:9074 8D 14 03 STA $0314 .C:9077 A9 90 LDA #$90 ;set KERNAL NMI vector to $90C9 .C:9079 8D 19 03 STA $0319 .C:907c A9 C9 LDA #$C9 .C:907e 8D 18 03 STA $0318 .C:9081 A9 3F LDA #$3F ;standard port direction .C:9083 8D 02 DD STA $DD02 ;for serial bus, UserPort bit, and VIC Bank bits .C:9086 AD 15 D0 LDA $D015 ;sprite enables .C:9089 8D 36 9D STA $9D36 ;save for exit .C:908c 8C 15 D0 STY $D015 ;disable all sprites .C:908f 8C 05 DD STY $DD05 ;zero CIA2 TimerA high .C:9092 C8 INY ;1 .C:9093 8C 04 DD STY $DD04 ;set CIA2 TimerA low .C:9096 A9 81 LDA #$81 ;enable TimerA interrupt (NMI) .C:9098 8D 0D DD STA $DD0D .C:909b A9 09 LDA #$09 ;start timer, one-shot mode .C:909d 8D 0E DD STA $DD0E ;set TimerA control .C:90a0 A0 2C LDY #$2C ;44 rasters ;loop for next raster .C:90a2 AD 12 D0 LDA $D012 ;get current raster .C:90a5 C5 8F CMP $8F ;same as before? .C:90a7 F0 F9 BEQ $90A2 ;yes, loop for next raster .C:90a9 85 8F STA $8F ;temp = current raster .C:90ab 88 DEY ;countdown, all rasters checked? .C:90ac D0 F4 BNE $90A2 ;no, loop for next raster .C:90ae AD 00 DD LDA $DD00 ;serial lines and more .C:90b1 29 07 AND #$07 ;mask UserPort bit and VIC Bank bits -- allow CLK and DATA to go high .C:90b3 85 8E STA $8E ;save for quick access .C:90b5 8D 3C 9D STA $9D3C ;save for slow access (really) .C:90b8 09 30 ORA #$30 ;value to pull CLK and DATA low .C:90ba 85 8F STA $8F ;save in ZP for quick access .C:90bc A5 8E LDA $8E ;value for CLK and DATA high .C:90be 09 10 ORA #$10 ;value for CLK low .C:90c0 8D 3D 9D STA $9D3D ;save for slow access .C:90c3 60 RTS .C:90c4 68 PLA ;temporary KERNAL IRQ code .C:90c5 A8 TAY ;restore Y .C:90c6 68 PLA .C:90c7 AA TAX ;restore X .C:90c8 68 PLA ;restore A .C:90c9 40 RTI ;temporary KERNAL NMI code -- return from interrupt The code first saves the CPU status (including interrupt disables) and then disables IRQs with SEI. Then it saves the current memory configuration and enables I/O registers and KERNAL ROM in the memory configuration. It clears VIC IRQ sources (mask) and then clears any pending interrupts from the VIC and both CIAs. Interestingly it sets KERNAL IRQ and NMI values to dummy code -- in case an interrupt somehow happens (the standard serial ROM routines generally end by enabling interrupts). Actually, it sets CIA2 to generate an NMI via TimerA but the dummy code doesn't clear it, so no NMIs can happen later. While setting TimerA, it also disables all sprites. Next the code waits for 44 rasters to pass. This allows any pending sprites to finish rendering (including Y-expanded ones). Finally it calculates a few values to use with the serial port. Note the code at $90CA (not shown) will restore the memory configuration, hardware registers, and CPU status (i.e., it will re-enable interrupts if they were active before). GOES has a routine (named EnterTurbo) which initializes the drive's RAM with custom code. Let's look at the core part of this routine: .C:943b 20 5C C2 JSR $C25C ;call $9048 (see above) to prepare for I/O .C:943e A9 96 LDA #$96 ;set pointer $8D~8E to $9609 (source) .C:9440 85 8E STA $8E .C:9442 A9 09 LDA #$09 .C:9444 85 8D STA $8D .C:9446 A9 03 LDA #$03 ;initial pointer $94A5~94A6 to $300 (destination) .C:9448 8D A6 94 STA $94A6 .C:944b A9 00 LDA #$00 .C:944d 8D A5 94 STA $94A5 .C:9450 A9 1A LDA #$1A ;27 chunks of 32 bytes (total 864 bytes) .C:9452 85 8F STA $8F ;save counter ;loop to transmit chunks .C:9454 20 7A 94 JSR $947A ;send M-W command to drive (using standard/slow transfer) .C:9457 8A TXA ;test result, okay? .C:9458 D0 1D BNE $9477 ;no, error exit .C:945a 18 CLC .C:945b A9 20 LDA #$20 ;add 32 .C:945d 65 8D ADC $8D ;to source low .C:945f 85 8D STA $8D .C:9461 90 02 BCC $9465 .C:9463 E6 8E INC $8E ;carry to sourch high .C:9465 18 CLC .C:9466 A9 20 LDA #$20 ;add 32 .C:9468 6D A5 94 ADC $94A5 ;to destination low .C:946b 8D A5 94 STA $94A5 .C:946e 90 03 BCC $9473 .C:9470 EE A6 94 INC $94A6 ;carry to destination high .C:9473 C6 8F DEC $8F ;countdown chunks, all done? .C:9475 10 DD BPL $9454 ;no, loop to transmit chunks .C:9477 4C 5F C2 JMP $C25F ;done with I/O, call $90CA (see above) That code transfers 864 bytes of code ($9609~9968) from the C64 to the C1541 ($300~65F) which takes about 1.5 seconds. Thankfully this is only done once -- when the device is first accessed (early in the boot process). Immediately after the code is written to the drive, a Memory-Execute (M-E) command is sent to the drive to run code at $3DC. Let's take a look: .8:03dc 08 PHP ;save interrupt status .8:03dd 78 SEI ;disable interrupts .8:03de A5 49 LDA $49 ;temporary stack pointer .8:03e0 48 PHA ;save on stack .8:03e1 A0 00 LDY #$00 ;256 loops to delay (256*5 = 1280 cycles) .8:03e3 88 DEY ;delay loop .8:03e4 D0 FD BNE $03E3 .8:03e6 20 B8 03 JSR $03B8 ;pull DATA low, allow CLK high .8:03e9 A9 04 LDA #$04 ;CLK input bit ;wait for CLK low .8:03eb 2C 00 18 BIT $1800 ;is CLK high? .8:03ee F0 FB BEQ $03EB ;yes, wait for CLK low ;main loop .8:03f0 20 40 04 JSR $0440 ;drive LED off .8:03f3 A9 06 LDA #$06 ;set pointer $73~74 to $64A (for code vector) .8:03f5 85 74 STA $74 .8:03f7 A9 4A LDA #$4A .8:03f9 85 73 STA $73 .8:03fb 20 75 03 JSR $0375 ;read a byte-count and then that many bytes to $64A+ .8:03fe 20 44 04 JSR $0444 ;drive LED on .8:0401 A9 07 LDA #$07 ;set pointer $73~74 to $700 .8:0403 85 74 STA $74 .8:0405 A9 00 LDA #$00 .8:0407 85 73 STA $73 .8:0409 A9 03 LDA #$03 ;push return 'main loop' address $3F0 onto stack .8:040b 48 PHA .8:040c A9 EF LDA #$EF .8:040e 48 PHA .8:040f 6C 4A 06 JMP ($064A) ;execute command; usually $64C has track#, $64D has sector# .8:0412 20 8F F9 JSR $F98F ;drive motor off .8:0415 A9 00 LDA #$00 ;allow CLK and DATA to go high .8:0417 8D 00 18 STA $1800 ;update serial bus .8:041a 85 33 STA $33 ;clear high-byte for controller .8:041c A9 EC LDA #$EC ;turn off byte-ready request .8:041e 8D 0C 1C STA $1C0C .8:0421 68 PLA ;discard $3F0 address .8:0422 68 PLA .8:0423 68 PLA ;recall .8:0424 85 49 STA $49 ;temporary stack pointer .8:0426 28 PLP ;restore interrupt status .8:0427 60 RTS ;exit That code basically pulls the serial DATA-line low and waits for the serial CLK-line to go low, turns off the drive LED, and waits for a command. Every command begins with a byte count and an execution address. Most commands also include a track# and sector#. Once a command is received, the drive turns on its LED and executes the requested code. When the requested code exits with RTS, control will return to the main loop ($3F0). The code at $412 is executed to 'uninstall' the fast code; this happens when you switch drives on the GEOS desktop. Now let's look at how GEOS sends data (such as a command) to the drive: .C:919c 20 85 95 JSR $9585 ;wait for drive ~ .C:9585 78 SEI ;disable interrupts .C:9586 A5 8E LDA $8E .C:9588 8D 00 DD STA $DD00 ;allow CLK and DATA high ;wait for drive .C:958b 2C 00 DD BIT $DD00 ;is DATA low? .C:958e 10 FB BPL $958B ;yes, wait for drive .C:9590 60 RTS ~ .C:919f 98 TYA ;# bytes to send .C:91a0 48 PHA ;save count .C:91a1 A0 00 LDY #$00 ;flag end of data .C:91a3 20 B3 91 JSR $91B3 ;call ourself to send 1 byte (the count) .C:91a6 68 PLA ;# bytes to send .C:91a7 A8 TAY ;set counter/index .C:91a8 20 85 95 JSR $9585 ;wait for drive (see above) ;loop to send bytes (cycle counts in [brackets]) .C:91ab 88 DEY ;[2]decrement count/index .C:91ac B1 8B LDA ($8B),Y ;[5.5]read data .C:91ae A6 8E LDX $8E ;[3]allow CLK and DATA high .C:91b0 8E 00 DD STX $DD00 ;[4]update serial lines ;(called with Y=0 to send one byte) .C:91b3 AA TAX ;[2]save data byte .C:91b4 29 0F AND #$0F ;[2]mask low nibble .C:91b6 85 8D STA $8D ;[3]save for later .C:91b8 38 SEC ;[2]prepare to subtract ;loop for VIC .C:91b9 AD 12 D0 LDA $D012 ;[4]get VIC raster# .C:91bc E9 31 SBC #$31 ;[2]calc offset from border, are we in border? .C:91be 90 04 BCC $91C4 ;[2]yes, skip ahead .C:91c0 29 06 AND #$06 ;[2]no, are we near bad-line? .C:91c2 F0 F5 BEQ $91B9 ;[2]yes, loop for VIC .C:91c4 8A TXA ;[2]get data byte .C:91c5 A6 8F LDX $8F ;[3]pull CLK and DATA low .C:91c7 8E 00 DD STX $DD00 ;[4]update serial lines .C:91ca 29 F0 AND #$F0 ;[2]isolate high nibble .C:91cc 05 8E ORA $8E ;[3]merge UserPort bit and VIC Bank bits .C:91ce 8D 00 DD STA $DD00 ;[4]update serial lines (and UserPort,VIC) .C:91d1 6A ROR A ;[2]next two bits .C:91d2 6A ROR A ;[2] .C:91d3 29 F0 AND #$F0 ;[2]isolate CLK and DATA (should be AND #$30) .C:91d5 0D 3C 9D ORA $9D3C ;[4]merge UserPort bit and VIC Bank bits .C:91d8 8D 00 DD STA $DD00 ;[4]update serial lines .C:91db A6 8D LDX $8D ;[3]get low nibble .C:91dd BD 3D 91 LDA $913D,X ;[4]look-up encoding value .C:91e0 05 8E ORA $8E ;[3]merge UserPort bit and VIC Bank bits .C:91e2 8D 00 DD STA $DD00 ;[4]update serial lines .C:91e5 6A ROR A ;[2]next two bits .C:91e6 6A ROR A ;[2] .C:91e7 29 F0 AND #$F0 ;[2]isolate CLK and DATA (should be AND #$30) .C:91e9 05 8E ORA $8E ;[3]merge UserPort bit and VIC Bank bits .C:91eb C0 00 CPY #$00 ;[2]test count/index, all done? .C:91ed 8D 00 DD STA $DD00 ;[4](update serial lines) .C:91f0 D0 B9 BNE $91AB ;[3]no, loop to send bytes .C:91f2 EA NOP .C:91f3 EA NOP .C:91f4 F0 9F BEQ $9195 ;yes, exit ~ .C:9195 AE 3D 9D LDX $9D3D ;CLK low, DATA high .C:9198 8E 00 DD STX $DD00 ;update serial bus .C:919b 60 RTS ;encoding values for low nibble >C:913d 00 80 20 a0 40 c0 60 e0 .. .@.`. >C:9145 10 90 30 b0 50 d0 70 f0 ..0.P.p. The code begins by waiting for the drive then calling (part of) itself to send a single byte (the # bytes to be transmitted next). Next it enters a loop to fast-send the data bytes. A byte transmission takes an average of 99.5 cycles, assuming the VIC is not in the border nor near a bad-line. The routine ends by pulling CLK low and allowing DATA high. I should mention if the byte count is zero, the code will send 256 bytes. Now let's see what the drive does when GEOS wants to a read a sector: .8:058e 20 6B 04 JSR $046B ;drive motor on, move head to desired track# ($64C) .8:0591 A9 00 LDA #$00 ;read code .8:0593 A6 00 LDX $00 ;1 .8:0595 CA DEX ;0 .8:0596 F0 03 BEQ $059B ;always .8:0598 60 RTS ~ .8:059b 85 45 STA $45 ;save command code bits .8:059d A9 06 LDA #$06 ;set pointer $32~33 to $64C (track# followed by sector#) .8:059f 85 33 STA $33 .8:05a1 A9 4C LDA #$4C .8:05a3 85 32 STA $32 .8:05a5 A9 07 LDA #$07 ;set pointer $30~31 to $700 .8:05a7 85 31 STA $31 .8:05a9 BA TSX ;current stack pointer .8:05aa 86 49 STX $49 ;temp stack pointer .8:05ac A2 01 LDX #$01 ;okay code .8:05ae 86 00 STX $00 ;save for buffer zero .8:05b0 CA DEX ;zero .8:05b1 86 3F STX $3F ;current buffer# (result code to $00) .8:05b3 A9 EE LDA #$EE .8:05b5 8D 0C 1C STA $1C0C ;request byte-ready signal .8:05b8 A5 45 LDA $45 ;get command code .8:05ba C9 10 CMP #$10 ;is it write sector? .8:05bc F0 0A BEQ $05C8 ;yes, do it .8:05be C9 30 CMP #$30 ;is it look for sector header? .8:05c0 F0 03 BEQ $05C5 ;yes, do it .8:05c2 4C CA F4 JMP $F4CA ;no, read sector The code starts by turning on the drive motor and moving the head (if needed) to the desired track# (stored at $64C from the command). For some reason (code obfusication?) the code is using controller address $00 for the job code being executed, which is normally for the buffer at $300. Instead that code sets the memory pointer ($30~31) to the buffer at $700. The important thing is that the code ends by calling the ROM to read the sector. That is, GEOS uses a standard sector layout and standard/slow GCR decoding. Before looking at how the computer/GEOS receives the data from the drive, let's take a look at the head-stepping routine. .8:0497 48 PHA ;save .A on stack .8:0498 A5 22 LDA $22 ;current track# .8:049a A2 FF LDX #$FF ;-1 (assume step-out = smaller track#) .8:049c 38 SEC .8:049d ED 4C 06 SBC $064C ;subtract desired track#, is difference zero? .8:04a0 F0 13 BEQ $04B5 ;yes, exit -- else is differnce positive? .8:04a2 B0 06 BCS $04AA ;yes, move head .8:04a4 49 FF EOR #$FF ;no, negate part 1 .8:04a6 69 01 ADC #$01 ;negate part 2 .8:04a8 A2 01 LDX #$01 ;+1 (step-in = greater track#) ;move head (.A contains the absolute value of the difference between current and desired track) .8:04aa 20 BC 04 JSR $04BC ;move head .8:04ad AD 4C 06 LDA $064C ;desired track# is now .8:04b0 85 22 STA $22 ;current track# .8:04b2 20 40 05 JSR $0540 ;set #sectors per track and bit-rate .8:04b5 68 PLA ;restore .A from stack .8:04b6 60 RTS ;move head .8:04bc 86 4A STX $4A ;save delta (plus or minus one) .8:04be 0A ASL A ;accumulator times two .8:04bf A8 TAY ;counter = # half-track steps .8:04c0 AD 00 1C LDA $1C00 ;stepper bits and other stuff .8:04c3 29 FE AND #$FE ;keep top stepper bit and other stuff .8:04c5 85 70 STA $70 ;shadow stepper bits and other stuff .8:04c7 A9 1E LDA #$1E ;initial delay value (about 7.5 milliseconds) .8:04c9 85 71 STA $71 ;save timing value ;loop to move head .8:04cb A5 70 LDA $70 ;shadow stepper bits and other stuff .8:04cd 18 CLC .8:04ce 65 4A ADC $4A ;add delta .8:04d0 45 70 EOR $70 ;scramble stepper bits and other stuff .8:04d2 29 03 AND #$03 ;isolate scrambled stepper bits .8:04d4 45 70 EOR $70 ;unscramble stepper bits, merge other stuff .8:04d6 85 70 STA $70 ;update shadow .8:04d8 8D 00 1C STA $1C00 ;update stepper bits (other stuff unchanged) .8:04db A5 71 LDA $71 ;get delay value .8:04dd 20 FB 04 JSR $04FB ;delay via hardware timer .8:04e0 A5 71 LDA $71 ;get delay value .8:04e2 C0 05 CPY #$05 ;remaining half-steps >= 5 ? .8:04e4 90 08 BCC $04EE ;no, apply breaking .8:04e6 C9 11 CMP #$11 ;is delay value >= 17 ? .8:04e8 90 0A BCC $04F4 ;no (max speed = min delay of 16 → 4.0 milliseconds) so update delay .8:04ea E9 02 SBC #$02 ;accelerate (faster speed, shorter delay) .8:04ec D0 06 BNE $04F4 ;always, update delay ;apply breaking .8:04ee C9 1C CMP #$1C ;is delay value >= 28 .8:04f0 B0 02 BCS $04F4 ;yes (min speed = max delay of 28 → 7.0 milliseconds) so update delay .8:04f2 69 04 ADC #$04 ;slow down (slower speed, longer delay) ;update delay .8:04f4 85 71 STA $71 .8:04f6 88 DEY ;test # half-track steps, all done? .8:04f7 D0 D2 BNE $04CB ;no, loop to move head .8:04f9 A9 4B LDA #$4B ;head-settling time (about 19.0 milliseconds) ;delay via hardware timer .8:04fb 8D 05 18 STA $1805 ;timer high-byte ;wait for timer .8:04fe AD 05 18 LDA $1805 ;is timer down to zero? .8:0501 D0 FB BNE $04FE ;no, wait for timer .8:0503 60 RTS GEOS always impressed me with its fast head movement. The code starts by calculating two values: the absolute value between the current track and target track, and a delta value (plus or minus one) that depends on which way the head needs to move (either inward toward the hole or outward towards the rim of the disk). Next it calls the 'move head' routine and then finishes by updating the current track# and setting some variables (number of sectors per track, and disk head's data bit-rate). The magic happens in 'move head' where it first calculates the number of half-track steps (the track difference times two), initializes a delay value, and sets up a 'shadow register' of $1C00 (which contains the stepper bits and other things). Next it enters a loop which updates both the stepper motor and the delay value. If the head neads to move more than 2.5 tracks, the code will accelarate the stepping speed (decrease the timing delay) until a maximum speed (minimum delay) is reached. If the head is closer (less than 2.5 tracks) then it will apply braking: it will decrease stepping spead (increase the delay) until a minimum speed is reached. This is neccessary (at least at these 'high' speeds) because the disk-head has inertia: it can't instantly go from a stopped state to maximum speed. If you try to move the head too fast, the stepper motor will 'jam' (not move at all). Also, at high speed, the head can't instantly stop: it needs to slow down or the head will over-shoot the desired track. Finally, if the disk head had to move, a rather lengthy delay of about 19 milliseconds is imposed. This is known as a head-settling delay. Most fast-loaders don't use this. I believe GEOS does because you want the head stable before it begins writing data, and the head-movement code doesn't know if data is about to be written. (There's an optimization for you to implement.) Anyway, let's take a look at how GEOS receives data from the drive: .C:914d 20 85 95 JSR $9585 ;allow CLK and DATA high, wait for DATA high (see far above) .C:9150 48 PHA ;waste 14 cycles .C:9151 68 PLA .C:9152 48 PHA .C:9153 68 PLA .C:9154 84 8D STY $8D ;save byte-count ;loop to get bytes (cycle counts in [brackets]) .C:9156 38 SEC ;[2] ;wait for VIC .C:9157 AD 12 D0 LDA $D012 ;[4]get VIC raster# .C:915a E9 31 SBC #$31 ;[2]is it in the border? .C:915c 90 04 BCC $9162 ;[2]yes, skip ahead .C:915e 29 06 AND #$06 ;[2]is it near a bad-line? .C:9160 F0 F5 BEQ $9157 ;[2]yes, wait for VIC .C:9162 A5 8F LDA $8F ;[3]value for CLK and DATA low .C:9164 8D 00 DD STA $DD00 ;[4]update serial lines .C:9167 A5 8B LDA $8B ;[3]waste time .C:9169 A5 8E LDA $8E ;[3]allow CLK and DATA high .C:916b 8D 00 DD STA $DD00 ;[4]update serial lines .C:916e C6 8D DEC $8D ;[5]countdown bytes (set index) .C:9170 EA NOP ;[2] .C:9171 EA NOP ;[2] .C:9172 EA NOP ;[2] .C:9173 AD 00 DD LDA $DD00 ;[4]get two bits .C:9176 4A LSR A ;[2]shift down two bits .C:9177 4A LSR A ;[2] .C:9178 EA NOP ;[2] .C:9179 0D 00 DD ORA $DD00 ;[4]merge two more bits .C:917c 4A LSR A ;[2]shift down four bits .C:917d 4A LSR A ;[2] .C:917e 4A LSR A ;[2] .C:917f 4A LSR A ;[2] .C:9180 AC 00 DD LDY $DD00 ;[4]get two bits .C:9183 AA TAX ;[2]low nibble as table index .C:9184 98 TYA ;[2]high bits .C:9185 4A LSR A ;[2]shift down two bits .C:9186 4A LSR A ;[2] .C:9187 0D 00 DD ORA $DD00 ;[4]merge two more bits .C:918a 29 F0 AND #$F0 ;[2]isolate high nibble .C:918c 1D 2E 91 ORA $912E,X ;[4]merge low nibble .C:918f A4 8D LDY $8D ;[3]get index, is this the last byte? .C:9191 91 8B STA ($8B),Y ;[6](save data) .C:9193 D0 C1 BNE $9156 ;[3]no, loop to get bytes .C:9195 AE 3D 9D LDX $9D3D ;pull CLK low, allow DATA high .C:9198 8E 00 DD STX $DD00 ;update serial lines .C:919b 60 RTS ;decode low nibble table >C:912e 0f 07 0d 05 0b 03 09 01 ........ >C:9136 0e 06 0c 04 0a 02 08 00 ........ That code is pretty simple (all things considered). First it allows both the CLK and DATA lines to go high and ensures the DATA line actually is high (drive ready). Then it saves the byte-transfer count (in the .Y register) to zero page and enters a loop to read that many bytes from the drive. This code takes 98 cycles per byte assuming the VIC is not in the border nor near a bad-line. This is much faster than the standard ROM transfer, but actually slow compared to other fast-loaders. But most others don't have to deal with saving data -- in fact GEOS might be the fastest at sending data to the drive.
© Hydrophilic.net, 2026 |