; ============================================================================
; 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
