modemwars/disassembly/drive/driveFastLoader.s
2026-08-23 02:09:40 -05:00

776 lines
58 KiB
ArmAsm

; ============================================================================
; 1541 fast loader (track 1 sectors 18-20, runs at $0300-$05FF in the drive)
; ============================================================================
; Commands from the C64 (received through the 2-bit protocol on the serial bus):
; $C0 = reset drive, $60 = write sector (receive 256 bytes, encrypt, write), anything else = read sector.
; Reads are done with the DOS ROM helpers (header search, GCR decode, checksum) into buffer 3 ($0600),
; decrypted with the rolling XOR key at $0531 (not applied to track 18) and sent back 2 bits at a time.
.setcpu "6502"
.include "drive1541.inc"
.include "drive_zp.inc"
; ---- references to code/data outside this file ----
D_0100 := $0100
D_0600 := $0600
; Contents
; --------
; $0300 fastloaderMain Entry point of the 1541-side fast loader.
; $0313 commandDispatchLoop The drive's command loop.
; $0342 enableByteReadySo Sets VIA2's PCR bits 1-3 to %111, i.e.
; $034B exchangeByteWithHost The slow, fully handshaked, full-duplex byte transfer with the C64.
; $0391 sendStatusAndLoop Sends the byte in A to the host as the operation's status and restarts the
; command loop.
; $0397 findHeaderAndDataSync Positions the head at the data field of the sector named in the header table:
; first waits for that sector's header to come round (findSectorHeader), then for the sync mark that introduces the data
; block (waitForSyncByte).
; $039D findSectorHeader Builds the header image DOS would have written for the wanted sector (disk id,
; track, sector and their XOR checksum), has the ROM turn it into GCR at zp_24, then compares the first eight GCR bytes
; of every header that passes under the head against it.
; $03E3 waitForSyncByte Waits for the next SYNC mark under the head, guarded by VIA1 timer 1 as a
; timeout of roughly 20 ms (about a tenth of a revolution, several sectors' worth).
; $03FB readSectorFromDisk Reads the sector named by zp_06/zp_07 into buffer 3 ($0600) and decrypts it.
; $044B sendSectorToHost Ships the 256 plaintext bytes of buffer 3 to the C64 over the fast path: no
; per-bit handshake, just four port writes per byte, two data bits at a time on CLK OUT and DATA OUT, encoded through
; nibbleSendTable.
; $0490 handleWriteCommand Handler for command $60: take the track and sector bytes from the host, write
; the sector, go back to the command loop.
; $04A0 writeSectorToDisk Receives 256 bytes from the host the slow handshaked way, encrypts them with
; the same rolling XOR that a read decrypts with, checksums them, refuses a write-protected disk ($08), lets the ROM
; turn the block into GCR, waits for the sector's header to come round, switches the head electronics to write, lays
; down five sync bytes and the 325 GCR bytes of the data field, switches back to read and reports success.
; $0525 prepareHeadAndVerifyId Enables BYTE READY -> SO and then checks the disk: zp_12 EOR zp_13 has to equal
; $15, and $15 is 'O' EOR 'Z' - the id of the SPORT OF WAR master.
; $0531 cryptSectorBuffer The disk cipher, and the reason a plain sector dump of this disk is unreadable.
; $0553 uploadBytesToDrive RAM injection.
; $0561 receiveCommand Waits for one command byte from the host.
; $05DE finishSendByte Two-instruction tail of sendSectorToHost, oddly parked out here in the third
; loader sector: it writes A ($08 = CLK pulled low) to the serial port and returns, leaving the bus in the state
; exchangeByteWithHost expects for the status exchange that follows.
.org $0300
; ----------------------------------------------------------------------
; fastloaderMain - Entry point of the 1541-side fast loader. The bootstrap sector (T1 s17) has
; already read T1 s18-20 into $0300-$05FF, i.e. over DOS buffers 0, 1 and 2, and jumps here. It idles
; the serial bus, points buffer 0's header table entry at track 18 sector 0 and issues a SEEK job so
; the DOS job controller parks the head on the directory track, then falls into the command loop.
; In: nothing. DOS has just been re-initialised by the bootstrap (it calls ROM_INIT_DRIVE at $063B),
; so zp_12/zp_13 hold the disk id and the controller interrupt is running.
; Out: serial bus idle, zp_06/zp_07 = 18/0, buffer 0's SEEK job run to completion (result ignored),
; zp_30-zp_33 left by the controller pointing at buffer 0 and at its header table entry; falls
; through into commandDispatchLoop.
; Called from: drive/driveBootstrap $063E (JMP $0300) - the only way into this file. XREF.txt has no
; entries for drive code: its $0300-$05FF addresses collide with C64 RAM, so the XREF rows for
; these addresses belong to unrelated C64 units.
; ----------------------------------------------------------------------
fastloaderMain:
lda #$00 ; 0300 $00 = release both serial outputs (CLK OUT and DATA OUT idle, ATNA off)
sta VIA1_PRB_SERIAL ; 0302 let go of the serial bus so the C64 sees CLK and DATA high
sta zp_07 ; 0305 sector byte of buffer 0's header table entry ($06/$07) = 0
lda #$12 ; 0307 $12 = 18, the directory track
sta zp_06 ; 0309 track byte of the same entry: the SEEK below steps the head to track 18
lda #$B0 ; 030B $B0 = DOS job code SEEK (step to the track in the header table and read a header there)
sta zp_00 ; 030D zp_00 is buffer 0's job byte - writing it hands the job to the controller
; ----------------------------------------------------------------------
; Wait for the seek. The DOS job controller runs off the VIA2 interrupt; when it is done it clears
; bit 7 of the job byte and leaves a result code (1 = ok) in its place. The result is never examined
; - all this needs is for the head to be somewhere known before the first command arrives.
; ----------------------------------------------------------------------
waitSeekJobAtBoot:
lda zp_00 ; 030F poll buffer 0's job byte
bmi waitSeekJobAtBoot ; 0311 bit 7 stays set while the job is pending: spin until the controller replaces it
; ----------------------------------------------------------------------
; commandDispatchLoop - The drive's command loop. It rebuilds the stack pointer the way the DOS idle
; loop does (every error path abandons the stack by jumping straight back here), receives one command
; byte and dispatches it: $C0 = cold start the drive, $60 = write a sector, anything else = read a
; sector (the C64 always sends $80). Read and write are each followed by a track byte and a sector
; byte, and every operation ends by sending a status byte, 0 meaning no error.
; In: the command byte returned by receiveCommand.
; Out: never returns. Each pass leaves a sector in buffer 3 ($0600) and a status byte on the wire.
; Called from: falls in from fastloaderMain; jumped back to by sendStatusAndLoop ($0394) and
; handleWriteCommand ($049D).
; ----------------------------------------------------------------------
commandDispatchLoop:
sei ; 0313 hold off the controller interrupt while the stack pointer is rebuilt
lda #$00 ; 0314 clear DOS work byte zp_50 (a controller state byte; exact ROM meaning uncertain)
sta zp_50 ; 0316 error exits abandon the stack, so start every command from a known state
ldx #$45 ; 0318 $45 is the stack pointer DOS itself uses: the stack stays inside $0100-$0145 and leaves $0146-$01FF free for the GCR overflow area used by the read and write paths
txs ; 031A throw away anything a previous error path left on the stack
cli ; 031B let the controller interrupt run again - the SEEK jobs below need it
jsr receiveCommand ; 031C LED off, then bit-bang one command byte in from the C64
cmp #$C0 ; 031F $C0 = reset the drive?
beq resetDrive ; 0321 yes: cold start and forget this loader
cmp #$60 ; 0323 $60 = write a sector?
bne handleReadCommand ; 0325 no: anything else means read a sector (the host only ever sends $80)
jmp handleWriteCommand ; 0327 the write handler receives its own track/sector bytes
; ----------------------------------------------------------------------
; Read command. The two bytes that follow name the sector; read it, decrypt it, ship it out over the
; fast path and acknowledge with status 0.
; ----------------------------------------------------------------------
handleReadCommand:
jsr exchangeByteWithHost ; 032A receive the track number (the byte handed back to the host is ignored)
sta zp_06 ; 032D track byte of buffer 0's header table entry
jsr exchangeByteWithHost ; 032F receive the sector number
sta zp_07 ; 0332 sector byte of the same entry - findSectorHeader reads both through (zp_32)
jsr readSectorFromDisk ; 0334 read it into buffer 3 ($0600) and decrypt it in place
jsr sendSectorToHost ; 0337 clock the 256 bytes over with the fast nibble protocol
lda #$00 ; 033A status 0 = no error; the C64 turns the border red and retries forever for anything else
jmp sendStatusAndLoop ; 033C send it and go back for the next command
resetDrive:
jmp (ROM_RESET_VECTOR) ; 033F $C0: jump through the drive's RESET vector at $FFFC - a full DOS cold start, which unloads this loader
; ----------------------------------------------------------------------
; enableByteReadySo - Sets VIA2's PCR bits 1-3 to %111, i.e. CA2 = manual output high. CA2 is the
; 1541's SOE line: while it is high the disk controller's BYTE READY pulse reaches the 6502's SO pin,
; which is what makes every 'bvc *' wait in this file (and in the DOS ROM helpers it calls) work. It
; does not select read mode - that is CB2, PCR bits 5-7, which writeSectorToDisk drives directly - so
; the survey name setHeadReadMode was misleading.
; In: VIA2_PCR.
; Out: PCR bits 1-3 set; from here on every assembled disk byte sets the V flag.
; Called from: prepareHeadAndVerifyId $0525, which both the read and the write path call.
; ----------------------------------------------------------------------
enableByteReadySo:
lda VIA2_PCR ; 0342 peripheral control register: CA1/CA2 in the low nibble, CB1/CB2 in the high nibble
ora #$0E ; 0345 bits 1-3 = %111: CA2 manual output high = SOE, so BYTE READY reaches the CPU's SO pin
sta VIA2_PCR ; 0347 the CB2 bits (read/write head select) are left exactly as they were
rts ; 034A back to prepareHeadAndVerifyId, which then checks the disk id
; ----------------------------------------------------------------------
; exchangeByteWithHost - The slow, fully handshaked, full-duplex byte transfer with the C64. Eight
; times over, the drive puts one outgoing bit (LSB first) on DATA, waits for the host to pull CLK,
; toggles DATA to say 'seen', waits for the host to release CLK, samples the host's bit off DATA,
; pulls CLK as its own acknowledgement and waits for the host to toggle DATA. So both directions move
; one bit per handshake and neither side has to be cycle-accurate. The host side is sendByteToDrive
; ($089C in game/mainProgram0800, bootExchangeByte $C03B in boot/fastLoaderC000). Note the inversion
; at the drive: writing a 1 to a VIA1 serial output bit PULLS that line low, and an input bit READS 1
; while the line is low. Bit 0 = DATA IN, bit 1 = DATA OUT, bit 2 = CLK IN, bit 3 = CLK OUT.
; In: A = byte to send (kept in zp_90).
; Out: A = the byte received from the host (zp_8F); zp_14 holds the port snapshot the toggle test
; compares against; X = 0.
; Called from: commandDispatchLoop $032A/$032F (track and sector), sendStatusAndLoop $0391 (status
; byte), handleWriteCommand $0490/$0495, writeSectorToDisk $04AE (all 256 data bytes) and $0522
; (final status), receiveCommand $056B, uploadBytesToDrive $0553.
; ----------------------------------------------------------------------
exchangeByteWithHost:
sta zp_90 ; 034B the byte to send goes into the output shift register
ldx #$08 ; 034D eight bits per byte
; ----------------------------------------------------------------------
; One bit per pass. Phase 1 puts our bit on DATA and waits for the host's CLK; phase 2 toggles DATA
; as an acknowledgement and waits for CLK to go away; phase 3 samples the host's bit; phase 4 pulls
; CLK as our acknowledgement and waits for the host to toggle DATA back.
; ----------------------------------------------------------------------
exchangeNextBit:
lsr zp_90 ; 034F shift the next outgoing bit (LSB first) into the carry
lda #$00 ; 0351 a 1 bit: leave DATA OUT clear so the line stays high
bcs L_0357 ; 0353 branch if the outgoing bit is 1
lda #$02 ; 0355 a 0 bit: set DATA OUT (bit 1), which pulls the DATA line low
L_0357:
sta VIA1_PRB_SERIAL ; 0357 drive the bit onto DATA and keep CLK OUT released; the host samples it at $08AE/$C04D
waitHostPullsClk:
lda VIA1_PRB_SERIAL ; 035A poll the serial port
and #$04 ; 035D bit 2 = CLK IN, which reads 1 while the host holds the CLK line low
beq waitHostPullsClk ; 035F wait until the host pulls CLK - that means it has taken our bit
lda VIA1_PRB_SERIAL ; 0361 read the port back (the two output bits read back as written)
eor #$02 ; 0364 flip our own DATA OUT bit
sta VIA1_PRB_SERIAL ; 0366 toggling DATA is the acknowledgement the host is waiting for in its 'cmp CIA2_PRA / beq' loop at $08BD/$C05C
lda #$04 ; 0369 $04 = the CLK IN mask, and also the value the BIT below tests against
waitHostReleasesClk:
bit VIA1_PRB_SERIAL ; 036B Z = 0 while CLK IN is still set
bne waitHostReleasesClk ; 036E wait for the host to release CLK; by then its own data bit is sitting on the DATA line
lda #$00 ; 0370 release both outputs
sta VIA1_PRB_SERIAL ; 0372 let go of DATA OUT so the line shows only what the C64 drives
lda VIA1_PRB_SERIAL ; 0375 sample the port with the host's bit on DATA IN (bit 0)
sta zp_14 ; 0378 remember the sampled port state for the toggle test at $0382 (zp_14/zp_15 are drive 1's disk id, which a single 1541 never uses)
lsr a ; 037A host's data bit (bit 0) into the carry
ror zp_8F ; 037B shift it into the receive register from the top; after eight bits the first bit sits in bit 0, i.e. LSB first
lda #$08 ; 037D $08 = CLK OUT
sta VIA1_PRB_SERIAL ; 037F pull CLK low as our acknowledgement; the host waits for exactly this at $08D6/$C075
waitHostTogglesData:
lda zp_14 ; 0382 the port state at sample time
eor VIA1_PRB_SERIAL ; 0384 against the port right now
and #$01 ; 0387 only DATA IN matters
beq waitHostTogglesData ; 0389 wait until the host toggles DATA (its 'eor #$20' at $08DB/$C07A) - it has seen the acknowledgement
dex ; 038B one bit done
bne exchangeNextBit ; 038C round again for all eight
lda zp_8F ; 038E return the byte received from the host
rts ; 0390 callers that only wanted to send simply ignore it
; ----------------------------------------------------------------------
; sendStatusAndLoop - Sends the byte in A to the host as the operation's status and restarts the
; command loop. 0 means success; the error paths arrive here with a DOS error code: $02 header not
; found (READ ERROR 20), $03 no sync (21), $05 data block checksum (23), $08 write protected (26) -
; the last one doubling as the wrong-disk-id code. The C64 treats any nonzero status as a failure,
; turns the border red and retries the same sector forever, so any of these hangs the load.
; In: A = status byte.
; Out: does not return; jumps to commandDispatchLoop, which rebuilds the stack pointer over whatever
; the abandoned call chain left behind.
; Called from: commandDispatchLoop $033C (status 0 after a read), findSectorHeader $03E0 (also the
; landing point for waitForSyncByte's timeout), readSectorFromDisk $0435, writeSectorToDisk $04CC,
; and the unreferenced helper at $05E2.
; ----------------------------------------------------------------------
sendStatusAndLoop:
jsr exchangeByteWithHost ; 0391 the host is exchanging a dummy byte to collect this ($0949/$C0E8)
jmp commandDispatchLoop ; 0394 abandon the stack and wait for the next command
; ----------------------------------------------------------------------
; findHeaderAndDataSync - Positions the head at the data field of the sector named in the header
; table: first waits for that sector's header to come round (findSectorHeader), then for the sync mark
; that introduces the data block (waitForSyncByte). It comes back with Y = 0 and V clear so the
; caller can fall straight into a byte-ready read loop. Survey name was seekSectorAndSyncData;
; nothing seeks here - the SEEK job in readSectorFromDisk did that.
; In: zp_12/zp_13 disk id, zp_3D drive number, (zp_32) = the header table entry holding track and
; sector.
; Out: the next BYTE READY is the first GCR byte of the data field; Y = 0, V clear. Header or sync
; failures never return - they go to sendStatusAndLoop.
; Called from: readSectorFromDisk $040C.
; ----------------------------------------------------------------------
findHeaderAndDataSync:
jsr findSectorHeader ; 0397 wait for the header of the wanted track/sector to pass under the head
jmp waitForSyncByte ; 039A then for the data block's sync mark; its RTS returns to readSectorFromDisk
; ----------------------------------------------------------------------
; findSectorHeader - Builds the header image DOS would have written for the wanted sector (disk id,
; track, sector and their XOR checksum), has the ROM turn it into GCR at zp_24, then compares the
; first eight GCR bytes of every header that passes under the head against it. It returns the moment
; one matches, with the head sitting just behind that header; after 90 headers (roughly four
; revolutions) it gives up with error $02.
; In: zp_3D drive number, zp_12/zp_13 disk id, (zp_32) pointing at the wanted track/sector pair.
; Out: RTS with the head just past the matching header. Clobbers zp_16-zp_1A (header image) and
; zp_24-zp_2D (its GCR form). On failure it jumps to sendStatusAndLoop with $02 and never
; returns.
; Called from: findHeaderAndDataSync $0397 on the read path and writeSectorToDisk $04D2 on the write
; path.
; ----------------------------------------------------------------------
findSectorHeader:
lda zp_3D ; 039D DOS drive number for the current job - always 0 in a single 1541
asl a ; 039F two id bytes per drive
tax ; 03A0 index 0 = drive 0's id in zp_12/zp_13, index 2 would be drive 1's in zp_14/zp_15
lda zp_12,x ; 03A1 first disk id character, as read by the last INITIALIZE
sta zp_16 ; 03A3 zp_16-zp_1A is the header image the ROM converter reads
lda zp_13,x ; 03A5 second disk id character
sta zp_17 ; 03A7 id byte 2 of the image
ldy #$00 ; 03A9 offset 0 of the header table entry
lda (zp_32),y ; 03AB the controller left zp_32/zp_33 pointing at this buffer's header table entry ($0006), so this is the track
sta zp_18 ; 03AD track of the header to look for
iny ; 03AF offset 1 of the same entry
lda (zp_32),y ; 03B0 and this is the sector
sta zp_19 ; 03B2 sector of the header to look for
lda #$00 ; 03B4 build the header checksum from scratch
eor zp_16 ; 03B6 id character 1
eor zp_17 ; 03B8 id character 2
eor zp_18 ; 03BA track
eor zp_19 ; 03BC sector - DOS stores exactly this XOR in the header
sta zp_1A ; 03BE expected header checksum
jsr ROM_MAKE_HEADER_GCR ; 03C0 $F934 packs the header block (block id $08, the checksum, sector, track, both id characters and two $0F gap bytes) into ten GCR bytes at zp_24-zp_2D
ldx #$5A ; 03C3 $5A = 90 headers to inspect, about four disk revolutions, before declaring the sector missing
; ----------------------------------------------------------------------
; Header search. Every sync mark on the track introduces either a header or a data block; read eight
; GCR bytes after each one and compare. A mismatch costs one of the 90 tries and the search waits for
; the next sync.
; ----------------------------------------------------------------------
tryNextHeader:
jsr waitForSyncByte ; 03C5 wait for the next sync mark
ldy #$00 ; 03C8 compare from the first GCR byte of the image
compareHeaderByte:
bvc compareHeaderByte ; 03CA wait for BYTE READY (SO sets V): the head has assembled another GCR byte
clv ; 03CC clear V so the next byte can set it
lda VIA2_PRA_DATA ; 03CD the raw GCR byte, straight off the read head
cmp a:zp_24,y ; 03D0 compare with the expected GCR image (absolute,Y - the 6502 has no zero page,Y form of CMP)
bne headerMismatch ; 03D3 one byte wrong means this is not our header
iny ; 03D5 next GCR byte
cpy #$08 ; 03D6 eight of the ten GCR bytes are enough: that covers block id, checksum, sector, track and both id characters
bne compareHeaderByte ; 03D8 keep comparing
rts ; 03DA matched - the head is now just behind this sector's header
headerMismatch:
dex ; 03DB one try used up
bne tryNextHeader ; 03DC keep looking while tries remain
lda #$02 ; 03DE DOS error 2 = header block not found (READ ERROR 20)
reportErrorAndLoop:
jmp sendStatusAndLoop ; 03E0 shared error exit; waitForSyncByte's timeout branches in here with A = $03 instead
; ----------------------------------------------------------------------
; waitForSyncByte - Waits for the next SYNC mark under the head, guarded by VIA1 timer 1 as a timeout
; of roughly 20 ms (about a tenth of a revolution, several sectors' worth). On sync it does one dummy
; read of the data port, which clears the pending BYTE READY, so the caller's next 'bvc' wait yields
; the first real byte after the sync mark. This is the DOS ROM routine at $F556 copied into RAM.
; In: VIA1 timer 1, VIA2 disk control port (bit 7 = SYNC, low while a sync mark is passing).
; Out: A = the byte read while still inside the sync (both callers discard it), V clear, Y = 0. On
; timeout it does not return: A is already $03 (DOS 'no sync', READ ERROR 21) and it branches into
; the shared error exit at $03E0.
; Called from: findHeaderAndDataSync $039A (the data block's sync) and findSectorHeader $03C5 (before
; every header comparison).
; ----------------------------------------------------------------------
waitForSyncByte:
lda #$D0 ; 03E3 timeout counter high byte
sta VIA1_T1C_HI ; 03E5 writing T1C-H starts VIA1 timer 1 counting down from $D0xx at 1 MHz
lda #$03 ; 03E8 preload the timeout status code: DOS error 3 = no sync (READ ERROR 21). Nothing in the loop below touches A, so it is still $03 at the error exit
; ----------------------------------------------------------------------
; Poll two things at once: the timeout timer and the SYNC line.
; ----------------------------------------------------------------------
waitSyncOrTimeout:
bit VIA1_T1C_HI ; 03EA N = bit 7 of the timer high byte
bpl reportErrorAndLoop ; 03ED high byte has fallen below $80, about 20 ms with no sync at all: give up with A = $03
bit VIA2_PRB_DISK_CTRL ; 03EF bit 7 of the disk control port is the SYNC input, low while a sync mark is under the head
bmi waitSyncOrTimeout ; 03F2 still no sync - back to watching the timer
lda VIA2_PRA_DATA ; 03F4 dummy read of the data port: it clears the pending BYTE READY so the next one is the first byte after the sync
clv ; 03F7 start the caller's wait with V clear
ldy #$00 ; 03F8 callers index their buffers from 0
rts ; 03FA back with the head just inside the sync mark
; ----------------------------------------------------------------------
; readSectorFromDisk - Reads the sector named by zp_06/zp_07 into buffer 3 ($0600) and decrypts it.
; It issues a SEEK job to get the head onto the track, takes the disk hardware over from the DOS
; controller (SEI), waits for the sector's data field, shovels 326 raw GCR bytes into $0600 and
; $01BA-$01FF, lets the ROM decode and checksum them, and tail-jumps into cryptSectorBuffer.
; In: zp_06 = track, zp_07 = sector, both just received from the host.
; Out: 256 plaintext bytes at $0600. Interrupts stay disabled until the command loop does CLI.
; Returns through cryptSectorBuffer's shared exit at $0581. A checksum failure reports $05 and
; never returns.
; Called from: commandDispatchLoop $0334.
; ----------------------------------------------------------------------
readSectorFromDisk:
clc ; 03FB leftover: nothing below depends on the carry
lda #$B0 ; 03FC $B0 = DOS job code SEEK
sta zp_00 ; 03FE ask the controller to step the head to the track now in zp_06 and read a header there
waitSeekJobBeforeRead:
lda zp_00 ; 0400 poll buffer 0's job byte
bmi waitSeekJobBeforeRead ; 0402 wait while bit 7 (job pending) is set; the result code is not looked at
sei ; 0404 from here on this code drives the disk hardware itself - no controller interrupt may disturb the byte-ready timing
jsr prepareHeadAndVerifyId ; 0405 enable BYTE READY -> SO, and refuse to go on unless the disk id is OZ
lda #$06 ; 0408 high byte of buffer 3
sta zp_31 ; 040A retarget the ROM helpers' buffer pointer zp_30/zp_31 from buffer 0 ($0300 - which holds this very code) to $0600; the low byte the controller left there is already 0
jsr findHeaderAndDataSync ; 040C wait for this sector's header, then for the data field's sync mark
; ----------------------------------------------------------------------
; Pull in the data field. The first 256 raw GCR bytes go into buffer 3 and the remaining 70 into
; $01BA-$01FF, which is where the ROM's GCR decoder expects the overflow - the layout is copied
; verbatim from the DOS read loop at $F556. $0100-$0145 is left alone because that is where the stack
; lives.
; ----------------------------------------------------------------------
readGcrIntoBuffer:
bvc readGcrIntoBuffer ; 040F wait for the next assembled GCR byte
clv ; 0411 re-arm the byte-ready flag
lda VIA2_PRA_DATA ; 0412 raw GCR byte off the head
sta D_0600,y ; 0415 store it in buffer 3 (Y came back as 0 from waitForSyncByte)
iny ; 0418 next byte
bne readGcrIntoBuffer ; 0419 256 GCR bytes fill the buffer
ldy #$BA ; 041B the remaining 70 GCR bytes of the data field go to $01BA-$01FF, above the stack area
readGcrOverflow:
bvc readGcrOverflow ; 041D wait for the next byte
clv ; 041F re-arm
lda VIA2_PRA_DATA ; 0420 raw GCR byte
sta D_0100,y ; 0423 into the overflow area
iny ; 0426 next
bne readGcrOverflow ; 0427 until Y wraps past $01FF - 326 GCR bytes in all
; ----------------------------------------------------------------------
; Decode and verify: 326 GCR bytes become the 256 data bytes plus the block's own checksum byte, and
; that checksum has to match a recomputed one.
; ----------------------------------------------------------------------
jsr ROM_GCR_TO_BIN ; 0429 $F8E0 converts the GCR back to binary in place and leaves the block's checksum byte in zp_3A
jsr ROM_DATA_CHECKSUM ; 042C $F5E9 EORs the 256 bytes at (zp_30) together
cmp zp_3A ; 042F compare with the checksum that was stored in the sector
beq L_0438 ; 0431 sector is good
lda #$05 ; 0433 DOS error 5 = data block checksum error (READ ERROR 23)
jmp sendStatusAndLoop ; 0435 report it and go back to the command loop
L_0438:
jmp cryptSectorBuffer ; 0438 decrypt the buffer in place; its shared exit RTSes to commandDispatchLoop for us
; nibbleSendTable - one byte per nibble value, used by sendSectorToHost; the index is the nibble to
; send, the entry is what gets written to the serial port. Only VIA1 bits 3 (CLK OUT) and 1 (DATA OUT)
; reach the C64, and writing a 1 pulls that line low, so the host reads back the complement. Each
; entry is written twice: once as it stands (bits 3 and 1 land on CLK and DATA) and once shifted left
; by one (bits 2 and 0 land there), which is how four data bits travel in two port writes. Working
; the encoding backwards from the host's receive loop, entry = the nibble complemented with bits 0 and
; 3 exchanged: $0 -> $0F, $1 -> $07, $2 -> $0D, $8 -> $0E, $F -> $00. The C64 loop at $0950 / $C0EF
; samples the (CLK,DATA) pair four times per byte and drops the pairs into bits 0-1, 2-3, 4-5 and 6-7,
; so the low nibble goes first.
nibbleSendTable:
.byte $0F,$07,$0D,$05,$0B,$03,$09,$01; 043B ........ entries for nibbles $0-$F; entry = nibble complemented with bits 0 and 3 swapped
.byte $0E,$06,$0C,$04,$0A,$02,$08,$00; 0443 ........
; ----------------------------------------------------------------------
; sendSectorToHost - Ships the 256 plaintext bytes of buffer 3 to the C64 over the fast path: no
; per-bit handshake, just four port writes per byte, two data bits at a time on CLK OUT and DATA OUT,
; encoded through nibbleSendTable. The only synchronisation is once per byte - the host releases the
; DATA line to ask for the next one and then samples the four writes at fixed cycle offsets. That is
; why the C64 side stops CIA1 timer A, blanks its sprites and waits for a raster line that is not
; about to be a bad line before each byte (receiveSectorData $0950 / bootReceiveSectorData $C0EF), and
; why the padding NOPs here matter.
; In: 256 decrypted bytes at $0600, nibbleSendTable at $043B.
; Out: 256 bytes clocked to the host; the bus is parked with CLK pulled low, ready for the status
; exchange. A, X and Y clobbered.
; Called from: commandDispatchLoop $0337.
; ----------------------------------------------------------------------
sendSectorToHost:
lda #$08 ; 044B $08 = CLK OUT
sta VIA1_PRB_SERIAL ; 044D pull CLK low: 'the sector is ready'. The C64 waits for exactly this at $0929 / $C0C8
ldy #$00 ; 0450 start at the first byte of the buffer
; ----------------------------------------------------------------------
; One pass per buffer byte: encode the low nibble, wait for the host to ask for the byte, then fire
; four port writes - low nibble bits 0-1 and 2-3, high nibble bits 4-5 and 6-7.
; ----------------------------------------------------------------------
sendNextByte:
lda D_0600,y ; 0452 the byte to send
lsr a ; 0455 shift the high nibble down (1 of 4)
lsr a ; 0456 (2 of 4)
lsr a ; 0457 (3 of 4)
lsr a ; 0458 (4 of 4) - A is now the high nibble
pha ; 0459 park it on the stack for the second half of the byte
lda D_0600,y ; 045A fetch the byte again
and #$0F ; 045D the low nibble travels first
tax ; 045F index the encoding table with it
lda nibbleSendTable,x ; 0460 the two port values for this nibble, packed into one byte
tax ; 0463 hold the encoded low nibble in X
lda #$01 ; 0464 bit 0 is an input, so this value releases both CLK OUT and DATA OUT - and it doubles as the DATA IN mask for the BIT below
sta VIA1_PRB_SERIAL ; 0466 let both lines go so the host can pull DATA
waitHostAsksForByte:
bit VIA1_PRB_SERIAL ; 0469 Z = 0 while DATA IN (bit 0) is set, i.e. while the host is holding DATA low
bne waitHostAsksForByte ; 046C wait until the C64 releases DATA - its 'stx CIA2_PRA' at $0966 / $C105 asking for a byte
stx VIA1_PRB_SERIAL ; 046E write 1: encoded bits 3 and 1 put data bits 1 and 0 on CLK and DATA
txa ; 0471 the same encoded nibble again
asl a ; 0472 shift its other two bits into the CLK/DATA positions
and #$0F ; 0473 keep only the four port bits (drop what fell into bit 4)
sta VIA1_PRB_SERIAL ; 0475 write 2: data bits 3 and 2
pla ; 0478 the high nibble saved above
tax ; 0479 index the table with it
lda nibbleSendTable,x ; 047A its two port values
sta VIA1_PRB_SERIAL ; 047D write 3: data bits 5 and 4
asl a ; 0480 shift the remaining two bits into place
and #$0F ; 0481 mask to the four port bits
nop ; 0483 two cycles of padding so write 4 lands where the host samples it
sta VIA1_PRB_SERIAL ; 0484 write 4: data bits 7 and 6 - the byte is gone
iny ; 0487 next buffer byte
bne sendNextByte ; 0488 all 256 of them
lda #$08 ; 048A $08 = CLK OUT again: park the bus with CLK low, the state exchangeByteWithHost starts from
nop ; 048C padding so the last write is clear of the host's final sample
jmp finishSendByte ; 048D the store itself lives at $05DE, out in the third loader sector
; ----------------------------------------------------------------------
; handleWriteCommand - Handler for command $60: take the track and sector bytes from the host, write
; the sector, go back to the command loop. The C64 only uses this to save game films and the setup -
; $7E85 and $8194 in the map generator overlay call writeSectors ($0843), which is the only code that
; sends $60.
; In: two bytes from the host.
; Out: zp_06/zp_07 set and the sector written; the status byte is sent by writeSectorToDisk itself.
; Called from: commandDispatchLoop $0327.
; ----------------------------------------------------------------------
handleWriteCommand:
jsr exchangeByteWithHost ; 0490 receive the track number
sta zp_06 ; 0493 track byte of buffer 0's header table entry
jsr exchangeByteWithHost ; 0495 receive the sector number
sta zp_07 ; 0498 sector byte of the same entry
jsr writeSectorToDisk ; 049A receive 256 bytes, encrypt them and write the sector; it sends its own status byte
jmp commandDispatchLoop ; 049D back for the next command
; ----------------------------------------------------------------------
; writeSectorToDisk - Receives 256 bytes from the host the slow handshaked way, encrypts them with the
; same rolling XOR that a read decrypts with, checksums them, refuses a write-protected disk ($08),
; lets the ROM turn the block into GCR, waits for the sector's header to come round, switches the head
; electronics to write, lays down five sync bytes and the 325 GCR bytes of the data field, switches
; back to read and reports success.
; In: zp_06/zp_07 = track/sector, 256 bytes arriving on the serial link.
; Out: the sector on disk, encrypted; zp_3A = the data block checksum. The status byte goes out from
; the tail call at $0522, whose RTS returns to handleWriteCommand.
; Called from: handleWriteCommand $049A.
; ----------------------------------------------------------------------
writeSectorToDisk:
lda #$B0 ; 04A0 $B0 = DOS job code SEEK
sta zp_00 ; 04A2 step the head to the track in zp_06
waitSeekJobBeforeWrite:
lda zp_00 ; 04A4 poll buffer 0's job byte
bmi waitSeekJobBeforeWrite ; 04A6 wait for the controller to finish the seek
sei ; 04A8 take the disk hardware over: the timing below is ours
jsr prepareHeadAndVerifyId ; 04A9 enable BYTE READY -> SO and check the disk id
ldy #$00 ; 04AC fill the buffer from the start
; ----------------------------------------------------------------------
; Take the 256 data bytes from the host. This uses the slow handshaked path in both directions -
; saving a game film happens once in a while, so a fast host-to-drive protocol was never written.
; ----------------------------------------------------------------------
receiveDataByte:
jsr exchangeByteWithHost ; 04AE receive one data byte (the byte handed back to the host is ignored)
sta D_0600,y ; 04B1 into buffer 3
iny ; 04B4 next
bne receiveDataByte ; 04B5 256 bytes
jsr cryptSectorBuffer ; 04B7 encrypt in place - the very same routine that decrypts on a read
lda #$06 ; 04BA high byte of buffer 3
sta zp_31 ; 04BC point the ROM helpers' buffer pointer at $0600
jsr ROM_DATA_CHECKSUM ; 04BE $F5E9 EORs the 256 bytes together
sta zp_3A ; 04C1 zp_3A is where the GCR encoder picks up the data block checksum
; ----------------------------------------------------------------------
; Refuse to write to a protected disk.
; ----------------------------------------------------------------------
lda VIA2_PRB_DISK_CTRL ; 04C3 disk control port
and #$10 ; 04C6 bit 4 = write protect sense; it reads 0 when the notch is covered
bne L_04CF ; 04C8 not protected - go ahead
writeProtectOrIdError:
lda #$08 ; 04CA DOS error 8 = write protect on (error 26); prepareHeadAndVerifyId reuses this exit for a wrong disk id
jmp sendStatusAndLoop ; 04CC report it and abandon the write
; ----------------------------------------------------------------------
; Encode the block and get into position: the write has to start in the gap right behind this sector's
; header.
; ----------------------------------------------------------------------
L_04CF:
jsr ROM_BIN_TO_GCR ; 04CF $F78F encodes the block id, the 256 data bytes and the checksum into 325 GCR bytes: the first 69 at $01BB-$01FF, the other 256 in buffer 3
jsr findSectorHeader ; 04D2 wait until this sector's header goes by
ldx #$09 ; 04D5 nine byte times of header gap between the header and the data field
skipHeaderGap:
bvc skipHeaderGap ; 04D7 wait one byte time
clv ; 04D9 re-arm
dex ; 04DA one gap byte gone
bne skipHeaderGap ; 04DB keep skipping
; ----------------------------------------------------------------------
; Switch the head electronics to write and lay down the sync mark that introduces the data field.
; ----------------------------------------------------------------------
lda #$FF ; 04DD all eight lines to output
sta VIA2_DDRA ; 04DF port A now feeds the write shift register instead of reading it
lda VIA2_PCR ; 04E2 peripheral control register
and #$1F ; 04E5 clear the CB2 control bits
ora #$C0 ; 04E7 CB2 = manual output low = write mode (the DOS $FE00 sequence)
sta VIA2_PCR ; 04E9 the head amplifier is now recording
lda #$FF ; 04EC $FF written continuously is a SYNC mark
ldx #$05 ; 04EE five sync bytes ahead of the data field
sta VIA2_PRA_DATA ; 04F0 hand the first one to the write shift register
clv ; 04F3 clear V before timing the rest
writeSyncBytes:
bvc writeSyncBytes ; 04F4 wait one byte time
clv ; 04F6 re-arm
dex ; 04F7 one sync byte written
bne writeSyncBytes ; 04F8 the latched $FF keeps going out, so this produces five of them
; ----------------------------------------------------------------------
; Write the data field: 69 GCR bytes from $01BB-$01FF first, then the 256 in buffer 3. That is the
; order the ROM's binary-to-GCR encoder produced them in, and it is the mirror image of the read
; layout above - each direction matches the ROM helper it feeds.
; ----------------------------------------------------------------------
ldy #$BB ; 04FA the encoded block starts with the 69 bytes at $01BB-$01FF
writeGcrOverflow:
lda D_0100,y ; 04FC next GCR byte
L_04FF:
bvc L_04FF ; 04FF wait until the head has taken the previous one
clv ; 0501 re-arm
sta VIA2_PRA_DATA ; 0502 hand it to the write shift register
iny ; 0505 next
bne writeGcrOverflow ; 0506 through $01FF
writeGcrFromBuffer:
lda (zp_30),y ; 0508 then the 256 GCR bytes in buffer 3 (Y has wrapped to 0)
L_050A:
bvc L_050A ; 050A wait for the head
clv ; 050C re-arm
sta VIA2_PRA_DATA ; 050D write it
iny ; 0510 next
bne writeGcrFromBuffer ; 0511 all 325 GCR bytes are now on the disk
; ----------------------------------------------------------------------
; Back to read mode and report success.
; ----------------------------------------------------------------------
L_0513:
bvc L_0513 ; 0513 wait for the last byte to be shifted out before killing write mode - cutting it short would corrupt the tail of the block
lda VIA2_PCR ; 0515 peripheral control register
eor #$E0 ; 0518 flip the CB2 control bits back out of 'manual output low', so the head stops recording
sta VIA2_PCR ; 051A read mode again
lda #$00 ; 051D A = 0 here, which is also the status byte sent below
sta VIA2_DDRA ; 051F port A back to input
jmp exchangeByteWithHost ; 0522 send status 0 = ok; its RTS returns to handleWriteCommand
; ----------------------------------------------------------------------
; prepareHeadAndVerifyId - Enables BYTE READY -> SO and then checks the disk: zp_12 EOR zp_13 has to
; equal $15, and $15 is 'O' EOR 'Z' - the id of the SPORT OF WAR master. Any other id (a disk
; formatted with id 'EA' XORs to $04) drops into the write-protect error exit, so the C64 loader spins
; forever with a red border. Cheap protection: a sector copy carries the id along, a file-by-file
; copy onto another blank does not. The survey noted the $15 as an unexplained DOS constant.
; In: zp_12/zp_13 = the disk id read by the last INITIALIZE (the bootstrap called ROM_INIT_DRIVE at
; $063B).
; Out: VIA2 PCR CA2 set. On a mismatch it jumps to $04CA (status $08) and never returns.
; Called from: readSectorFromDisk $0405 and writeSectorToDisk $04A9.
; ----------------------------------------------------------------------
prepareHeadAndVerifyId:
jsr enableByteReadySo ; 0525 let BYTE READY drive the CPU's SO pin
lda zp_12 ; 0528 first disk id character ('O' on the original)
eor zp_13 ; 052A second one ('Z')
eor #$15 ; 052C $4F EOR $5A = $15, so this comes out 0 only for an OZ disk
bne writeProtectOrIdError ; 052E wrong disk: report $08 and let the host spin on its red border
rts ; 0530 right disk - carry on with the read or write
; ----------------------------------------------------------------------
; cryptSectorBuffer - The disk cipher, and the reason a plain sector dump of this disk is unreadable.
; Track 18 is left in plain text (BAM, directory, the unit start-position templates, the tile rules
; and the save slots); every other sector is XORed with a rolling key seeded from its own track and
; sector, so the same plaintext encrypts differently in each sector: seed = ((track OR $C0) * 2) +
; sector + carry, key[0] = ROL(seed), key[y] = ROL(y EOR key[y-1]), the carry chaining from one ROL
; into the next. The current key byte lives in the operand of the EOR at $0542 and is rewritten every
; round. Being a pure XOR it is symmetric - the same code decrypts a read and encrypts a write.
; In: zp_06 = track, zp_07 = sector, 256 bytes at $0600.
; Out: $0600-$06FF transformed in place; leaves through the shared tail at $057D, so A comes back
; holding the last command byte.
; Called from: readSectorFromDisk $0438 (JMP, so this routine's exit is what returns to
; commandDispatchLoop) and writeSectorToDisk $04B7 (JSR). extracted/decryptDisk.py reimplements
; it; see overview section 4.
; ----------------------------------------------------------------------
cryptSectorBuffer:
lda zp_06 ; 0531 track the sector came from
cmp #$12 ; 0533 $12 = 18, the directory track
beq cryptExit ; 0535 track 18 is stored in the clear - skip the cipher entirely
ora #$C0 ; 0537 force bits 6 and 7 so the shift below always leaves carry set
asl a ; 0539 (track OR $C0) * 2, carry = 1
adc zp_07 ; 053A plus the sector and that carry; the carry out of this add feeds the first ROL
ldy #$00 ; 053C start at the first buffer byte
beq L_0543 ; 053E always taken (Y was just set to 0): the first key byte comes straight from the seed
; ----------------------------------------------------------------------
; The key stream: every round rotates (buffer index EOR previous key) left through the carry the
; previous round left behind, so the key depends on the whole history, not just the index.
; ----------------------------------------------------------------------
cryptNextByte:
tya ; 0540 every later round starts from the buffer index
cryptKeyEor:
eor #$00 ; 0541 EOR with the previous key byte - the operand at $0542 is rewritten below (self-modifying)
L_0543:
rol a ; 0543 rotate left, taking in the carry from the previous round
sta cryptKeyEor+1 ; 0544 that is the new key byte; keep it in the EOR operand at $0542 for the next round
eor D_0600,y ; 0547 XOR the buffer byte with the key
sta D_0600,y ; 054A in place: decrypts after a read, encrypts before a write
iny ; 054D next byte
bne cryptNextByte ; 054E all 256 of them
cryptExit:
sec ; 0550 force the branch below
bcs lastCommandLda ; 0551 always taken: leave through the shared exit, which reloads A with the last command byte and RTSes
; ----------------------------------------------------------------------
; uploadBytesToDrive - RAM injection. receiveCommand comes here when the host's command byte is >=
; $F0; that byte is really a negative count, so 256 minus it more bytes are received and poked into
; drive RAM starting at $0580, with both the destination low byte (the operand of the STA at $0556)
; and the counter (the operand of the LDA at $057D) self-modified as it goes. When the counter wraps
; to zero the code falls into receiveCommand for the real command - and by then the injected bytes
; have landed on the BEQ operand at $0580 and the RTS at $0581, so receiveCommand's own tail has
; become the injected code, which runs and returns whatever it likes in A. $0582-$05DD is prefilled
; with RTS so a short upload still ends cleanly, and the destination pointer is never reset, so
; successive uploads append.
; In: the count already patched into $057E by receiveCommand, then that many bytes on the serial
; link.
; Out: bytes stored from $0580 upward; $0557 and $057E left patched; falls into receiveCommand.
; Called from: receiveCommand $0573. Nothing in the boot or overlay-loading path ever sends a command
; byte >= $F0, but the modem driver does: a received packet whose control byte is >= $F0 is
; forwarded byte for byte to the drive by dispatchReceivedPacket ($EA0D-$EA29 in
; game/modemDriverE000), counter and all.
; ----------------------------------------------------------------------
uploadBytesToDrive:
jsr exchangeByteWithHost ; 0553 receive the next byte to inject
uploadStoreByte:
sta commandExitBeq+1 ; 0556 store it - this instruction's own operand starts at $0580 (the BEQ offset in receiveCommand's tail) and walks upward
inc uploadStoreByte+1 ; 0559 bump the destination low byte at $0557 for the next byte
inc lastCommandLda+1 ; 055C count the byte counter at $057E up (it started at $F0-$FF)
bne uploadBytesToDrive ; 055F keep receiving until the counter wraps to 0, then fall into receiveCommand for the real command
; ----------------------------------------------------------------------
; receiveCommand - Waits for one command byte from the host. It turns the drive LED off while idle -
; that is the flicker you see between sectors - exchanges a byte (it sends $01, which the host
; ignores) and stores what arrived in the operand of the LDA at $057D, which doubles as the upload
; counter. A value >= $F0 is not a command but the length preamble of a RAM upload, so control drops
; into uploadBytesToDrive; otherwise the LED goes back on and the byte is handed back through the
; shared exit at $057D-$0581.
; In: nothing.
; Out: A = the command byte; LED on; $057E patched with the same value.
; Called from: commandDispatchLoop $031C, and by falling out of uploadBytesToDrive.
; ----------------------------------------------------------------------
receiveCommand:
lda VIA2_PRB_DISK_CTRL ; 0561 disk control port: stepper, motor, LED, write protect sense, SYNC
and #$F7 ; 0564 clear bit 3 = drive LED off while the drive is idle
sta VIA2_PRB_DISK_CTRL ; 0566 read-modify-write, so the motor and stepper bits are untouched
lda #$01 ; 0569 the byte handed back to the host during the exchange; it ignores what it gets here and only checks the status exchange
jsr exchangeByteWithHost ; 056B receive the command byte
sta lastCommandLda+1 ; 056E keep it in the LDA operand at $057E, which is also the upload counter
cmp #$F0 ; 0571 $F0-$FF is not a command but 'here come 256 minus this many bytes for your RAM'
bcs uploadBytesToDrive ; 0573 go and swallow them, then come back here for the real command
lda VIA2_PRB_DISK_CTRL ; 0575 disk control port again
ora #$08 ; 0578 bit 3 = drive LED on for the duration of the command
sta VIA2_PRB_DISK_CTRL ; 057A write it back
lastCommandLda:
lda #$00 ; 057D operand at $057E is patched with the byte just received (or with the count during an upload); cryptSectorBuffer also exits through here
commandExitBeq:
beq commandExitRts ; 057F a branch to the next instruction as it stands - but its operand at $0580 is the first byte an upload overwrites, so after an upload this is where the injected code starts
commandExitRts:
rts ; 0581 shared exit, also used by cryptSectorBuffer; the second byte of an upload overwrites it
; driveUploadScratch - 92 bytes of $60 (RTS), $0582-$05DD. This is the landing area for
; uploadBytesToDrive: the first injected byte overwrites the BEQ operand at $0580 and the second the
; RTS at $0581, so injected code executes as the tail of receiveCommand and any byte it did not
; overwrite is a harmless return. One upload can carry at most 16 bytes ($F0 = -16), but the
; destination pointer at $0557 is never reset, so uploads chain - which is why the RTS field is 92
; bytes long.
driveUploadScratch:
.byte $60 ; 0582 ` $60 = RTS, so an under-filled upload still returns cleanly
.byte "````````````````" ; 0583
.byte "`````````````````````````````````"; 0593
.byte "````````````````````````````"; 05B4
.byte "```````" ; 05D0
.byte "```````" ; 05D7
; ----------------------------------------------------------------------
; finishSendByte - Two-instruction tail of sendSectorToHost, oddly parked out here in the third loader
; sector: it writes A ($08 = CLK pulled low) to the serial port and returns, leaving the bus in the
; state exchangeByteWithHost expects for the status exchange that follows. There was room to do it
; inline at $048F, so this looks like a leftover of how the loader was assembled rather than something
; deliberate.
; In: A = the port value to park ($08, loaded at $048A).
; Out: VIA1 serial port written; RTS to commandDispatchLoop.
; Called from: sendSectorToHost $048D (JMP).
; ----------------------------------------------------------------------
finishSendByte:
sta VIA1_PRB_SERIAL ; 05DE park the bus with CLK pulled low
rts ; 05E1 back to commandDispatchLoop, which then sends the status byte
; checksumBufferAndReport - 6502 code that the disassembler emits as data, because it sits behind an
; RTS and nothing in the drive reaches it. $05E2 is JSR $05E8 / JMP sendStatusAndLoop; $05E8 is LDA
; #$00 / TAY / CLC and then the loop ROL A / ADC $0600,Y / INY / BNE, ending in RTS - a rotate-and-add
; checksum over the whole of buffer 3, handed to the host as the operation's status byte. The only
; thing that can call it is code injected through uploadBytesToDrive (a JMP $05E2 is three bytes, well
; inside one upload), which fits a mastering or verification tool; failing that it is dead code.
; In: 256 bytes at $0600.
; Out: A = the rolling checksum, sent to the host, then back to the command loop.
; Called from: nothing in this file.
checksumBufferAndReport:
.byte $20,$E8,$05,$4C,$91,$03,$A9; 05E2 ..L... 20 E8 05 = JSR $05E8, 4C 91 03 = JMP sendStatusAndLoop, then the helper starts with A9 00 = LDA #$00
.byte $00,'('|$80,$18,"*y",$00,$06,'H'|$80,'P'|$80,'y'|$80,"`"; 05E9
; hiddenSignature - the last twelve bytes of the third loader sector: 44 61 4E 73 A9 55 F0 E2 53 61 52
; 61, which reads as 'DaNs' and 'SaRa' around four binary bytes. Nothing references the address and it
; is not reachable as code, so this is filler with a personal signature in it - the same kind of thing
; as the 1988 Electronic Arts message that fills the tail of the bootstrap sector. Read as
; instructions the middle bytes would be LDA #$55 / BEQ, which is almost certainly coincidence.
hiddenSignature:
.byte $44,$61,$4E,$73,$A9,$55 ; 05F4 DaNs.U 'DaNs' followed by $A9 $55; the last four bytes at $05FC are 'SaRa'
.byte 'p'|$80,'b'|$80,"SaRa" ; 05FA