{ "chunk": "drive_fastloader_T1S18_20_0300", "unit": "drive/fastloader_T1S18_20", "routines": [ {"addr":"0300","name":"fastloaderMain","summary":"Entry point of the 1541 fast loader; the bootstrap sector (T1 s17) reads T1 s18-20 to $0300-$05FF and JMPs here. Idles the serial lines, sets the default track/sector ($06=18,$07=0), issues disk-controller job $B0 on buffer 0 and waits for it to finish, then falls into commandDispatchLoop.","inputs":"none (entered from the bootstrap)","outputs":"zp_06=$12, zp_07=$00, zp_00 job issued; falls through to $0313","confidence":"high"}, {"addr":"0313","name":"commandDispatchLoop","summary":"Top of the drive command loop. Resets the stack (SP=$45), receives a command byte from the host via receiveCommand, and dispatches: $C0 resets the drive (JMP through reset vector), $60 goes to handleWriteCommand, anything else is a read (receive track and sector, read the sector, send it back, then report status 0). Re-entered after every operation.","inputs":"command byte from receiveCommand","outputs":"zp_50=0; branches to read/write/reset handlers","confidence":"high"}, {"addr":"0342","name":"setHeadReadMode","summary":"Sets VIA2 PCR bits 1-3 (ORA #$0E) to put the read/write head electronics into read mode. Called by prepareHeadAndVerifyId before reading or writing a sector.","inputs":"VIA2_PCR","outputs":"VIA2_PCR updated","confidence":"medium"}, {"addr":"034B","name":"exchangeByteWithHost","summary":"Core full-duplex serial byte transfer with the C64. Clocks the byte in A out to the host one bit at a time (LSB first) on VIA1 serial bit1 while simultaneously shifting a received byte in on bit0, using a two-phase CLK/ACK handshake on bits 2/3. This is the drive-side counterpart of the C64 routine sendByteToDrive ($089C). Used for all command/parameter bytes, the status byte, and (in the write path) the 256 data bytes from the host.","inputs":"A = byte to send (into zp_90)","outputs":"A = byte received from host (zp_8F); uses zp_14 as handshake temp; X clobbered","confidence":"high"}, {"addr":"0391","name":"sendStatusAndLoop","summary":"Sends the status/error byte in A to the host via exchangeByteWithHost, then JMPs back to commandDispatchLoop. Every error path (header not found=$02, checksum=$05, write protect/id error=$08) jumps here with its code in A; the normal read path jumps here with A=0.","inputs":"A = status byte","outputs":"status sent to host; loops to $0313","confidence":"high"}, {"addr":"0397","name":"seekSectorAndSyncData","summary":"Finds the requested sector header on disk (findSectorHeader) then waits for and reads into the data-block sync (waitForSyncByte). Called at the start of readSectorFromDisk to position the read at the sector's data field.","inputs":"zp_3D drive index, zp_12/zp_13, zp_32 header pointer","outputs":"positioned at data field; V clear, Y=0; error $02 via sendStatusAndLoop","confidence":"medium"}, {"addr":"039D","name":"findSectorHeader","summary":"Builds the expected 8-byte GCR header image (from the id bytes zp_12/zp_13, the track/sector bytes via (zp_32), and their XOR checksum) with ROM_MAKE_HEADER_GCR into the zp_24 buffer, then reads raw GCR bytes from VIA2 comparing 8 bytes at a time. Returns when the matching header passes under the head; after $5A failed tries reports error $02. Also used by writeSectorToDisk to locate the sector before writing.","inputs":"zp_3D drive index, zp_12/zp_13 disk id bytes, zp_32 header ptr","outputs":"header located; zp_16-zp_1A header scratch; error $02 via sendStatusAndLoop","confidence":"medium"}, {"addr":"03E3","name":"waitForSyncByte","summary":"Arms VIA1 timer 1 as a timeout, waits for a SYNC mark on VIA2 (bit 7 of the disk-control port), then reads the first byte after sync from VIA2 and returns it. On timeout it branches to the error path ($02 via sendStatusAndLoop). Clears V and Y before returning.","inputs":"VIA1_T1, VIA2 disk port","outputs":"A = byte after sync, V clear, Y=0; timeout -> error","confidence":"medium"}, {"addr":"03FB","name":"readSectorFromDisk","summary":"Reads the sector named by zp_06/zp_07 from disk into buffer $0600: waits for the buffer-0 job, verifies the head/id (prepareHeadAndVerifyId), seeks to the data field (seekSectorAndSyncData), reads 256 GCR data bytes into $0600 plus the GCR overflow into $0100, converts GCR->binary and verifies the data checksum against zp_3A (error $05 on mismatch), then falls through to cryptSectorBuffer to decrypt. Invoked by the read branch of the command loop.","inputs":"zp_06 track, zp_07 sector, zp_31 buffer page","outputs":"decrypted sector in $0600; zp_3A checksum; error $05 via sendStatusAndLoop","confidence":"high"}, {"addr":"044B","name":"sendSectorToHost","summary":"Sends the 256 decrypted bytes in buffer $0600 back to the C64. Each byte is split into two nibbles, each nibble translated through nibbleSendTable and clocked out 4 bits at a time on the VIA1 serial port with a handshake (bit0 = clock). This bulk path is received by the C64 routine receiveSectorData ($0950). Ends via finishSendByte.","inputs":"$0600 buffer (256 decrypted bytes), nibbleSendTable","outputs":"256 bytes clocked to host; VIA1 serial port","confidence":"high"}, {"addr":"0490","name":"handleWriteCommand","summary":"Write-command ($60) handler: receives the track and sector bytes from the host into zp_06/zp_07, calls writeSectorToDisk, then returns to commandDispatchLoop.","inputs":"track/sector from host via exchangeByteWithHost","outputs":"sector written; loops to $0313","confidence":"high"}, {"addr":"04A0","name":"writeSectorToDisk","summary":"Receives 256 data bytes from the host into $0600, encrypts them in place (cryptSectorBuffer, symmetric with decrypt), computes the data checksum into zp_3A, checks the write-protect sense (VIA2 bit 4; error $08 if protected), converts binary->GCR, positions on the header (findSectorHeader), switches VIA2 to write mode, writes the GCR overflow ($0100) and data (zp_30) fields, restores read mode, and sends the status byte. Called by handleWriteCommand.","inputs":"256 bytes from host, zp_06/zp_07, zp_30 buffer ptr","outputs":"sector written to disk, zp_3A checksum; error $08 via sendStatusAndLoop","confidence":"high"}, {"addr":"0525","name":"prepareHeadAndVerifyId","summary":"Selects read mode (setHeadReadMode) and validates the drive id bytes: if zp_12 EOR zp_13 EOR $15 is nonzero it branches to the write-protect/id error path ($08). Called at the start of both readSectorFromDisk and writeSectorToDisk. The exact meaning of the $15 constant is a DOS id/consistency check.","inputs":"zp_12, zp_13","outputs":"read mode set; error $08 via sendStatusAndLoop on mismatch","confidence":"low"}, {"addr":"0531","name":"cryptSectorBuffer","summary":"The sector cipher: XORs the 256 bytes in $0600 with a self-modifying rolling key (key0 = ROL((track|$C0)<<1 + sector + carry); key[y]=ROL(y EOR key)). Track 18 ($12) is left in plaintext (BAM/directory/map/setup). Symmetric, so the same routine decrypts on read and encrypts on write. This is the cipher documented in overview section 4.","inputs":"$0600 buffer, zp_06 track, zp_07 sector","outputs":"$0600 XORed in place (except track 18)","confidence":"high"}, {"addr":"0553","name":"uploadBytesToDrive","summary":"Code/data injection loop reached from receiveCommand when the host sends a count byte >= $F0: receives (256 - count) bytes and stores them consecutively into drive RAM starting at $0580 (the RTS-filled scratch area) using self-modifying stores, then falls through into receiveCommand again to fetch the real command. Lets the host push a small routine or table into the drive.","inputs":"count in patched $057E, bytes from host","outputs":"bytes written to $0580+; re-enters receiveCommand","confidence":"medium"}, {"addr":"0561","name":"receiveCommand","summary":"Receives one command/parameter byte from the host: clears a VIA2 status bit, sends $01 and receives a byte via exchangeByteWithHost, patches it into the LDA at $057D. If the byte is >= $F0 it is a byte-count preamble and control drops to uploadBytesToDrive to inject that many bytes into $0580 before looping back for the real command; otherwise the received byte is returned in A.","inputs":"host serial input","outputs":"A = command byte","confidence":"medium"}, {"addr":"05DE","name":"finishSendByte","summary":"Tail of sendSectorToHost: writes the final value in A to the VIA1 serial port and returns (leaves the serial lines in the idle state after a bulk read).","inputs":"A, VIA1 serial port","outputs":"VIA1 serial port set; RTS","confidence":"medium"}, {"addr":"05E2","name":"checksumBufferAndReport","summary":"Small 6502 routine that the disassembler classified as data (it sits after an RTS and has no in-file caller). $05E2 does JSR $05E8 / JMP sendStatusAndLoop; $05E8 sums the 256 bytes of $0600 with a ROL+ADC accumulator and returns. Most likely a verify/checksum helper invoked by code injected via uploadBytesToDrive, or dead code. Followed by a hidden signature.","inputs":"$0600 buffer","outputs":"A = rolling checksum, reported to host via sendStatusAndLoop","confidence":"low"} ], "variables": [ {"addr":"00","scope":"zp","name":"jobCodeBuffer0","meaning":"1541 disk-controller job code for buffer 0; $B0 issued at startup (and $B0 before read/write), the code waits for bit 7 to clear (job done). Standard DOS job slot.","confidence":"medium"}, {"addr":"06","scope":"zp","name":"currentTrack","meaning":"Track of the sector being read/written (buffer-0 track). Initialised to $12 (18); compared to $12 by the cipher to skip encryption on track 18.","confidence":"high"}, {"addr":"07","scope":"zp","name":"currentSector","meaning":"Sector of the sector being read/written (buffer-0 sector); used as an operand of the cipher key.","confidence":"high"}, {"addr":"12","scope":"zp","name":"diskIdByte0","meaning":"DOS disk/header id byte 0, used to build the expected header GCR image and in the id-consistency check (zp_12 EOR zp_13 EOR $15).","confidence":"medium"}, {"addr":"13","scope":"zp","name":"diskIdByte1","meaning":"DOS disk/header id byte 1, paired with zp_12 in the header image and id check.","confidence":"medium"}, {"addr":"14","scope":"zp","name":"serialHandshakeTemp","meaning":"Scratch used by exchangeByteWithHost to hold the last VIA1 serial read for the bit handshake compare.","confidence":"high"}, {"addr":"16","scope":"zp","name":"headerScratch16","meaning":"Header-build scratch in findSectorHeader (id byte / GCR image assembly).","confidence":"low"}, {"addr":"1A","scope":"zp","name":"headerChecksum","meaning":"XOR checksum byte of the header fields, computed in findSectorHeader before building the GCR header image.","confidence":"medium"}, {"addr":"24","scope":"zp","name":"headerGcrImage","meaning":"8-byte expected GCR header image built by ROM_MAKE_HEADER_GCR and compared byte-for-byte against the raw header read from disk.","confidence":"medium"}, {"addr":"30","scope":"zp","name":"bufferPtr","meaning":"Pointer (lo) to the active disk buffer; the data field is read/written through (zp_30). Points into $0600.","confidence":"medium"}, {"addr":"31","scope":"zp","name":"bufferPage","meaning":"High byte / page of the active buffer, set to $06 so the buffer is at $0600 for both read and write.","confidence":"high"}, {"addr":"32","scope":"zp","name":"headerPtr","meaning":"Pointer to the current buffer's header (track/sector) bytes, dereferenced in findSectorHeader.","confidence":"medium"}, {"addr":"3A","scope":"zp","name":"dataChecksum","meaning":"Data-block checksum: compared against the ROM-computed checksum after a read (error $05), and stored as the checksum to write on a write.","confidence":"high"}, {"addr":"3D","scope":"zp","name":"driveIndex","meaning":"Drive/side selector; doubled to index the id-byte table (zp_12,x / zp_13,x) in findSectorHeader.","confidence":"low"}, {"addr":"50","scope":"zp","name":"loopStatus","meaning":"Cleared to 0 at the top of every command-loop iteration; DOS status/counter slot.","confidence":"low"}, {"addr":"8F","scope":"zp","name":"recvShiftReg","meaning":"Receive shift register in exchangeByteWithHost; bits from the host are rotated in here and the completed byte is returned in A.","confidence":"high"}, {"addr":"90","scope":"zp","name":"sendShiftReg","meaning":"Send shift register in exchangeByteWithHost; the byte to transmit is loaded here and shifted out LSB-first.","confidence":"high"} ], "dataBlocks": [ {"addr":"043B","length":16,"type":"byteTable","name":"nibbleSendTable","description":"16-entry nibble translation table used by sendSectorToHost. Entry n = complement of the nibble n with its two outer bits swapped (0->$0F,1->$07,2->$0D,...). Encodes each nibble into the 4 serial bits sent to the C64 during a bulk read."}, {"addr":"0582","length":92,"type":"byteTable","name":"driveUploadScratch","description":"$0582-$05DD filled with $60 (RTS). This is the RTS-filled scratch/patch area; uploadBytesToDrive stores injected bytes starting at $0580 into this region. Pre-filling with RTS makes any un-overwritten byte a harmless return."}, {"addr":"05F4","length":12,"type":"text","name":"hiddenSignature","description":"Hidden signature bytes at the very end of the loader: ASCII fragments 'DaN' and 'SaRa' (Dan Bunten / family names) mixed with a couple of opcode-looking bytes. Not referenced; an easter-egg credit like the EA message in the bootstrap sector."} ], "misclassified": [ {"addr":"05E2","length":18,"actual":"code","evidence":"Bytes $20,$E8,$05,$4C,$91,$03 disassemble as JSR $05E8 / JMP $0391 (sendStatusAndLoop), and $05E8-$05F3 as LDA #$00/TAY/CLC/ROL A/ADC $0600,Y/INY/BNE/RTS - a valid rolling checksum over the $0600 buffer. Classified as .byte because it follows an RTS ($05E1) with no in-file caller (likely a target for injected code, or dead)."} ], "insights": [ "This is the 1541-side fast loader (runs in the drive at $0300-$05FF). Its entry $0300 is JMPed to by the bootstrap sector (drive/bootstrap_T1S17.s at $063E) after that sector queues T1 s18-20 -> $0300-$05FF.", "Command protocol (drive side of overview section 3): the drive loops at $0313 receiving a command byte via receiveCommand. $C0 = reset (JMP reset vector), $60 = write sector, any other value = read sector. Read/write are followed by track then sector bytes; every operation ends by sending a status byte (0 = ok, else error code) back to the host.", "Error codes returned to the host: $02 = header not found (findSectorHeader/waitForSyncByte timeout), $05 = data checksum mismatch on read, $08 = write protected or id-check failure. The C64 side turns the border red and retries forever on any nonzero status.", "Two distinct serial transports are used. Command/parameter/status bytes and the write-path data bytes go through exchangeByteWithHost ($034B), a full-duplex bit-banged handshake on the VIA1 serial port (LSB first) that mirrors the C64's sendByteToDrive ($089C). The read-path bulk data uses the faster nibble-encoded path sendSectorToHost ($044B) via nibbleSendTable, received by the C64's receiveSectorData ($0950).", "cryptSectorBuffer ($0531) is the canonical implementation of the disk cipher documented in overview section 4: a self-modifying rolling XOR keyed on track and sector, skipped for track 18. The same routine encrypts on write and decrypts on read (symmetric).", "Code-injection facility: if the host sends a command byte >= $F0, receiveCommand treats it as a count and uploadBytesToDrive ($0553) pokes (256 - count) bytes into drive RAM at $0580 (the RTS-filled scratch area) before fetching the real command. The comm module exercises this same >=$F0 path from the C64 side (ovl_E000 around $EA0D-$EA29, sending through sendByteToDrive), so the game can patch/extend the drive at run time - probably part of the modem-play disk handling.", "Because the drive's address space ($0300-$05FF) overlaps C64 addresses, XREF.txt entries for $0300/$0313/$04A0 refer to unrelated C64 code (game/setup_0200, table_0400), not to this drive routine - do not cross-reference them." ] }