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