; ============================================================================= ; a9m0331_30.bin -- Apple ADB Mouse model A9M0331 firmware v3.0 ; ============================================================================= ; Copyright (C) 1986 Logitech S.A., R. Sommer, Switzerland ; Manufactured for Apple Computer under OEM agreement ; ; Chip: Motorola MC6805E2 (HMOS 8-bit microcontroller) ; - 11-bit address space: $000-$7FF ; - 64-byte RAM: $040-$07F ; - 2KB mask ROM: $080-$7FF ; - I/O registers: $000-$00F ; - Unused: $010-$03F (reads as $FF) ; - SCI (serial), timer with overflow interrupt ; - External IRQ pin = ADB bus line sense ; ; Protocol: Apple Desktop Bus (ADB) -- single-wire, open-drain, 10 kbps ; ; Disassembly generated by Claude Code using MAME unidasm ; Edited and sanity-checked by R. Belmont, April 2026 ; ; Errors may exist in Claude's analysis of the code, but everything I've examined ; seems reasonable. If you find errors, email messdrivers [at] gmail dot com. ; ============================================================================= ; ============================================================================= ; I/O REGISTER MAP ($000-$00F) ; ============================================================================= PORTA equ $00 ; Port A data register (DDR-A = $04, init $0F) ; PA7 = button (right) -- INPUT ; PA6 = button (left/single) -- INPUT ; PA5 = unused (input, ignored) ; PA4 = button (startup self-test sample) -- INPUT ; PA3-PA0 = outputs (driven low; unused/NC) PORTB equ $01 ; Port B data register (DDR-B = $05, init $F0) ; PB7-PB4 = unused outputs (always driven; not ADB) ; PB3-PB0 = ADB line open-drain driver ; DDRB $F0 = bits 3:0 inputs = line released (pulled high externally) ; DDRB $FF = bits 3:0 outputs driving $00 = line pulled low PORTC equ $02 ; Port C data register (DDR-C = $06, varies) ; When DDR-C = $00 (all inputs, during encoder read): ; PC7-PC4 = tied high (external pullups → always $F) ; PC3 = Y encoder channel B (Y1) -- INPUT ; PC2 = Y encoder channel A (Y0) -- INPUT ; PC1 = X encoder channel B (X1) -- INPUT ; PC0 = X encoder channel A (X0) -- INPUT ; When DDR-C = $0F (bits 3:0 outputs): ; PC7-PC4 still read as inputs ($F from pullups) ; PC3-PC0 driven as outputs (value = $00) ; SCI mode: PC0=RxD, PC1=TxD (shared with encoder) PORTD equ $03 ; Port D data register (unused) DDRA equ $04 ; Data direction register A (init $0F: bits 3:0 outputs) DDRB equ $05 ; Data direction register B (init $F0: bits 7:4 outputs) ; Note: also used as in-progress flag by SCI handler ; ($FF = ADB transmit in progress; $F0 = idle) DDRC equ $06 ; Data direction register C (varies: $00 or $0F) TCSR equ $08 ; Timer control/status register TCNT equ $09 ; Timer counter register (8-bit, counts up, overflows → ISR) SCICR1 equ $0B ; SCI control register 1 SCICR2 equ $0C ; SCI control register 2 SCISR equ $0D ; SCI status register SCIDR equ $0E ; SCI data register ; ============================================================================= ; RAM VARIABLE MAP ($040-$07F) ; ============================================================================= ; $040-$048 IRQ dispatch stub (loaded from ROM at reset, JMP instruction) ; $040: BE 08 LDX $08 (read TCSR into X) ; $042: A6 FF LDA #$FF ; $044: B7 08 STA $08 (clear TCSR flags) ; $046: A3 00 CPX #$00 (was TCSR zero?) ; $048: BC ?? JMP $00?? (dispatch; high byte = $00, ; low byte at $049 self-modified) ; $049 IRQ dispatch low-byte target (self-modified by state handlers) ; Initial value: $AF (RTI stub -- idle) ; $04A ADB state: packet-building parameter (low nibble = encoder bits) ; $04B ADB state counter / boot self-test counter ; $04C ADB command high nibble (saved from received command) ; $04D ADB command low nibble ; $04E ADB command extension parameter ; $04F Button state snapshot: PA7:6 (previous reading; $C0 = both released) ; $050 ADB device address register (from ADB LISTEN Register 3) ; $051 X delta accumulator (signed, incremented by quadrature decode) ; $052 Y delta accumulator (signed, incremented by quadrature decode) ; $053 X delta carry/overflow byte (for acceleration scaling) ; $054 Y delta carry/overflow byte ; $055 ADB flags byte (bit 0 = SRQ / have-data flag for polling) ; $056 DDR-B saved value (used during ADB transmission) ; $057 ADB bit counter / current phase ; $058 Previous Port C encoder value (for quadrature change detection) ; $059 Encoder scratch / intermediate ; $05A Encoder speed register (accumulated, bit 7 = motion detected) ; $05B Data-ready-to-send flag (bit 0 set = have new data for host) ; $05C ADB Register 0 high byte (Y delta 6:0 | button-right in bit 7) ; $05D ADB Register 0 low byte (X delta 6:0 | button-left in bit 7) ; $05E ADB bit transmit counter (counts bits during serial output) ; $05F Generic delay counter ; $060 Encoder scan count ; ============================================================================= ; ADB STATE MACHINE DISPATCH TARGETS ; ($049 is self-modified; JMP lands at one of these ROM addresses) ; ============================================================================= ; $0080 dispatch_main -- default / re-enter main ADB poll loop ; $0083 dispatch_table -- indexed dispatch via table at $031B ; $009D dispatch_9D -- receive ADB bit (set state for next bit) ; $00AF dispatch_idle -- RTI (idle, no action) ; $00B0 dispatch_B0 -- receive bit, update shift register, next state ; $00B7 dispatch_B7 -- receive bit (alternate path) ; $00BE dispatch_transmit -- ADB transmit start state ; $00C1 dispatch_C1 -- transmit bit state (mark pulse done) ; $00C4 dispatch_C4 -- transmit bit state (space pulse done) ; $00CD dispatch_tx_bit -- ADB transmit bit handler: JMP $0709 ; ; NOTE: The encoder scan subroutine at $00D0 is NOT a dispatch target; ; it is called directly via JSR $D0 from the main loop and timer ISR. ; Its entry sequence (CLR $06; CLR $01; DEC $5A; JMP $075F) prepares ; Port C for reading and runs the quadrature decode. ; ============================================================================= ; CODE ($0080 onwards) ; ============================================================================= ; ----------------------------------------------------------------------------- ; $0080 dispatch_main ; Entry: called when IRQ dispatch target = $80. ; Re-enters the main ADB polling loop. ; ----------------------------------------------------------------------------- $0080: jmp $0508 ; → main_poll_loop ; ----------------------------------------------------------------------------- ; $0083 dispatch_table ; Entry: called when IRQ dispatch target = $83. ; Indexed dispatch: reads from table at $031B using $4B (ADB state counter) ; as X-register index. All table entries are $00, so X=0, BPL taken → timer_isr. ; This path handles "no pending ADB command" fall-through. ; ----------------------------------------------------------------------------- $0083: ldx $031B,x ; X = dispatch_table[$4B] (all zeros → X=0) $0086: bpl $009A ; if X >= 0 (bit 7 clear): → timer_isr entry $0088: stx $47 ; X < 0: save to $47 (command code) $008A: lda #$9D $008C: sta $49 ; set dispatch target → dispatch_9D $008E: cli ; enable interrupts $008F: ldx #$F0 $0091: stx $01 ; PORTB = $F0: release ADB line $0093: ldx #$0F $0095: stx $06 ; DDRC = $0F: bits 3:0 outputs $0097: jmp $0590 ; → adb_response_handler $009A: jmp $04B4 ; → timer_isr (common return) ; ----------------------------------------------------------------------------- ; $009D dispatch_9D ; Entry: IRQ fired during ADB command receive -- sample incoming bit. ; Shifts current ADB line state (via ROL $57) into the receive shift register. ; ----------------------------------------------------------------------------- $009D: rol $57 ; shift ADB bit into $57 (C = line level sampled by IRQ) $009F: bcc $00AF ; if bit = 0: → dispatch_idle (RTI) $00A1: ldx #$F0 $00A3: lda $57 ; load current bit pattern $00A5: cmpa $4D ; compare with expected command pattern $00A7: bne $00AB $00A9: stx $56 ; match: save $F0 to $56 (DDR-B marker) $00AB: ldx #$B0 $00AD: stx $49 ; next dispatch target → dispatch_B0 $00AF: rti ; ← also: dispatch_idle / swi_handler target ; ----------------------------------------------------------------------------- ; $00B0 dispatch_B0 ; Entry: IRQ fired, continue ADB bit receive. ; ----------------------------------------------------------------------------- $00B0: rol $57 ; shift next bit $00B2: ldx #$B7 $00B4: stx $49 ; next dispatch target → dispatch_B7 $00B6: rti ; ----------------------------------------------------------------------------- ; $00B7 dispatch_B7 ; Entry: IRQ fired, ADB command reception complete -- respond. ; ----------------------------------------------------------------------------- $00B7: ldx $56 ; load DDR-B saved value ($F0 = normal) $00B9: stx $05 ; restore DDRB $00BB: jmp $059B ; → adb_response_handler ; ----------------------------------------------------------------------------- ; $00BE dispatch_transmit ; Entry: IRQ dispatch target set to $BE -- start ADB data transmission. ; Sets up for ADB Register 0 transmit sequence. ; ----------------------------------------------------------------------------- $00BE: jmp $070D ; → adb_tx_start ; ----------------------------------------------------------------------------- ; $00C1 dispatch_C1 ; Entry: IRQ dispatch target = $C1 -- ADB transmit mark-pulse complete. ; Redirects to adb_bit_timing_handler_B ($0712) which advances the bit phase. ; ----------------------------------------------------------------------------- $00C1: jmp $0712 ; → adb_bit_timing_handler_B ; ----------------------------------------------------------------------------- ; $00C4 dispatch_C4 ; Entry: IRQ dispatch target = $C4 -- ADB transmit space-pulse complete. ; Shifts next bit out of $5D:$5C (the ADB packet), then dispatches based on ; carry (bit value): carry clear = branch back to idle ($00AF = RTI), ; carry set = JMP $071E (continue transmit sequence). ; ----------------------------------------------------------------------------- $00C4: rol $5D ; shift next data bit out of X-delta byte $00C6: rol $5C ; rotate carry into Y-delta byte (MSB first) $00C8: bcc $00AF ; carry=0 (bit=0): → dispatch_idle (RTI) $00CA: jmp $071E ; carry=1 (bit=1): → adb_receive_decode ; ----------------------------------------------------------------------------- ; $00CD dispatch_tx_bit ; Entry: IRQ dispatch target = $CD -- redirect to ADB transmit bit handler. ; ----------------------------------------------------------------------------- $00CD: jmp $0709 ; → adb_bit_timing_handler ; ----------------------------------------------------------------------------- ; $00D0 encoder_scan (subroutine -- called via JSR $D0, NOT an IRQ dispatch) ; Prepares Port C for encoder read, decrements speed register, runs quad decode. ; Called frequently from the main loop and timer ISR to service the encoder ; even during ADB communication. ; ----------------------------------------------------------------------------- $00D0: clr $06 ; DDRC = $00: all Port C pins = inputs $00D2: clr $01 ; PORTB = $00: drive ADB line low (SRQ assertion) $00D4: dec $5A ; decrement encoder speed register $00D6: jmp $075F ; → quad_scan (tail call) ; -- padding / unused bytes $00D7-$00D8 (zeros) -- ; ============================================================================= ; QUADRATURE DECODE SECOND-LEVEL LOOKUP TABLE ($00E0-$00FF) ; ============================================================================= ; Indexed by (first-level XOR result). Returns jump-table offset for $0437. ; Key: X=bits 1:0 (X encoder), Y=bits 3:2 (Y encoder), upper nibble=$F. ; ; Input byte = 0xF? where ? = (Y1 Y0 X1 X0): ; $F0: prev=00,curr=00 → mem[$F0]=$10 → XOR=$E0 → offset=$00 = no-move ; $F1: prev=00,curr=01 → mem[$F1]=$12 → XOR=$E3 → offset=$00 = no-move (still) ; (full 16x16 table; all 256 entries handle simultaneous X+Y transitions) ; ; Table layout matches the ROM at addresses $00E0-$00FF: ; $E0-$EF: dispatch offsets for first-level results $E0-$EF (Y-axis dominated) ; $F0-$FF: primary X/Y decode offsets for normal port C values $F0-$FF ; $F0=$10(-X), $F1=$12(no), $F2=$11(?), $F3=$13(-X-Y) ; $F4=$18(+X), $F5=$1A(?), $F6=$19(?), $F7=$1B(-X) ; $F8=$14(?), $F9=$16(?), $FA=$15(-Y), $FB=$17(no) ; $FC=$1C(?), $FD=$1E(+Y),$FE=$1D(?), $FF=$1F(no) ; ; For normal quadrature (upper nibble=$F, one step at a time): ; X forward step (X0X1: 00→01→11→10) → dispatch offset $18 = INC $51 (+X) ; Y forward step (Y0Y1: 00→01→11→10) → dispatch offset $21 = DEC $52 (-Y sign) ; ============================================================================= $00E0: fcb 0 $00E1: fcb $18 $00E2: fcb $1B $00E3: fcb 0 $00E4: fcb $21 $00E5: fcb 9 $00E6: fcb $13 $00E7: fcb $21 $00E8: fcb $1E $00E9: fcb 4 $00EA: fcb $E $00EB: fcb $1E $00EC: fcb 0 $00ED: fcb $18 $00EE: fcb $1B $00EF: fcb 0 $00F0: fcb $10 $00F1: fcb $12 $00F2: fcb $11 $00F3: fcb $13 $00F4: fcb $18 $00F5: fcb $1A $00F6: fcb $19 $00F7: fcb $1B $00F8: fcb $14 $00F9: fcb $16 $00FA: fcb $15 $00FB: fcb $17 $00FC: fcb $1C $00FD: fcb $1E $00FE: fcb $1D $00FF: fcb $1F ; ============================================================================= ; TIMING TABLE ($03C0-$03F3) -- 52 entries ; ============================================================================= ; 8-bit timer preload values, indexed by $5A (encoder speed register), ; used to generate ADB bit-timing pulses. ; Values range from $C1 (short pulse, ~57 µs) to $E3 (long pulse, ~127 µs) ; covering ADB's '0' cell and '1' cell timings at the 2.048 MHz CPU clock. $03C0: fcb $C1 $03C1: fcb $C1 $03C2: fcb $C2 $03C3: fcb $C3 $03C4: fcb $C3 $03C5: fcb $C4 $03C6: fcb $C5 $03C7: fcb $C5 $03C8: fcb $C6 $03C9: fcb $C7 $03CA: fcb $C7 $03CB: fcb $C8 $03CC: fcb $C9 $03CD: fcb $C9 $03CE: fcb $CA $03CF: fcb $CB $03D0: fcb $CB $03D1: fcb $CC $03D2: fcb $CD $03D3: fcb $CD $03D4: fcb $CE $03D5: fcb $CF $03D6: fcb $CF $03D7: fcb $D0 $03D8: fcb $D1 $03D9: fcb $D1 $03DA: fcb $D2 $03DB: fcb $D3 $03DC: fcb $D3 $03DD: fcb $D4 $03DE: fcb $D5 $03DF: fcb $D5 $03E0: fcb $D6 $03E1: fcb $D7 $03E2: fcb $D7 $03E3: fcb $D8 $03E4: fcb $D9 $03E5: fcb $D9 $03E6: fcb $DA $03E7: fcb $DB $03E8: fcb $DB $03E9: fcb $DC $03EA: fcb $DD $03EB: fcb $DD $03EC: fcb $DE $03ED: fcb $DF $03EE: fcb $DF $03EF: fcb $E0 $03F0: fcb $E1 $03F1: fcb $E1 $03F2: fcb $E2 $03F3: fcb $E3 ; ============================================================================= ; FIRMWARE VERSION STRING ($03F4-$042C) ; ============================================================================= ; "3.0 860922 R. Sommer, Switzerland Copyright LOGITECH 1986" ; Null-terminated at $042B. $03F4: fcc "3.0 860922 R. Sommer, Switzerland Copyright LOGITECH 1986" ; ============================================================================= ; RESET INIT DATA TABLE ($042D-$047C) ; ============================================================================= ; 80-byte table copied to addresses $00-$4F at reset. ; First 16 bytes initialize I/O registers ($00-$0F): ; addr $00 (PORTA): $00 addr $01 (PORTB): $F0 addr $02 (PORTC): $F0 ; addr $03 (PORTD): $00 addr $04 (DDRA): $0F addr $05 (DDRB): $F0 ; addr $06 (DDRC): $FF addr $07: $00 addr $08 (TCSR): $00 ; addr $09 (TCNT): $50 $042D: fcb 0 ; PORT A init $042E: fcb $F0 ; PORT B init $042F: fcb $F0 ; PORT C init $0430: fcb 0 ; unused $0431: fcb $F ; DDR A init $0432: fcb $F0 ; DDR B init $0433: fcb $FF ; DDR C init $0434: fcb 0 ; unused $0435: fcb 0 ; TCSR $0436: fcb $50 ; TCNT ; ============================================================================= ; JUMP TABLE for quadrature motion dispatch ($0437-$045A) ; Indexed as: JMP/JSR $0437 + X, where X = second-level decode result. ; Each offset corresponds to a movement action: ; offset $00-$02: TSTX (no-op chain, for no-movement) ; offset $03: RTS -- no movement ; offset $04: INC $51; INC $52; RTS -- X+1, Y+1 ; offset $06: INC $52; RTS -- Y+1 only ; offset $08: RTS -- no movement (alt) ; offset $09: DEC $52; INC $51; RTS -- Y-1, X+1 ; offset $0B: INC $51; RTS -- X+1 only ; offset $0D: RTS -- no movement (alt) ; offset $0E: INC $52; DEC $51; RTS -- Y+1, X-1 ; offset $10: DEC $51; RTS -- X-1 only ; offset $12: RTS -- no movement (alt) ; offset $13: DEC $51; DEC $52; RTS -- X-1, Y-1 ; offset $15: DEC $52; RTS -- Y-1 only ; offset $17: RTS -- no movement (alt) ; offset $18: INC $51; RTS (alt +X via BRA) ; offset $1B: DEC $51; RTS (alt -X via BRA) ; offset $1E: INC $52; RTS (alt +Y via BRA) ; offset $21: DEC $52; RTS (alt -Y via BRA) ; ============================================================================= $0437: tstx ; no-movement padding (offset $00) $0438: tstx ; (offset $01) $0439: tstx ; (offset $02) $043A: rts ; (offset $03) no movement, return $043B: inc $51 ; (offset $04) X += 1 $043D: inc $52 ; (offset $06, also reached from $04 fall-through) Y += 1 $043F: rts $0440: dec $52 ; (offset $09) Y -= 1 $0442: inc $51 ; (offset $0B) X += 1 $0444: rts $0445: inc $52 ; (offset $0E) Y += 1 $0447: dec $51 ; (offset $10) X -= 1 $0449: rts $044A: dec $51 ; (offset $13) X -= 1 $044C: dec $52 ; (offset $15) Y -= 1 $044E: rts $044F: nop ; (offset $18) alt +X $0450: bra $0442 ; → INC $51; RTS $0452: nop ; (offset $1B) alt -X $0453: bra $0447 ; → DEC $51; RTS $0455: nop ; (offset $1E) alt +Y $0456: bra $043D ; → INC $52; RTS $0458: nop ; (offset $21) alt -Y $0459: bra $044C ; → DEC $52; RTS ; ============================================================================= ; RESET HANDLER ($047D) ; ============================================================================= $047D: lda #$F0 $047F: sta $01 ; PORTB = $F0: release ADB line immediately $0481: ldx #$FF $0483: stx $05 ; DDRB = $FF: all B outputs (brief override) $0485: lda #$14 ; outer loop counter = 20 ; ~25 ms startup delay: shifts $00 and $50 right (scrambles them), ; runs inner loop decrementing X from $FF to $00 $048A: lsr $00 ; shift I/O regs (harmless noise on Port A outputs) $048C: ror $50 $048E: decx $048F: bne $048A ; inner: X == 0? $0491: deca $0492: bne $048A ; outer: A == 0? ; --- Initialize registers and RAM --- $0494: sei ; disable interrupts $0495: rsp ; reset stack pointer to top of stack ($7F) $0496: clrx ; X = 0 $0497: lda $042D,x ; read byte N from init table (ROM $042D+X) $049A: sta ,x ; write to address X ($00, $01, ... $4F) $049B: clr $51,x ; zero RAM byte at $51+X (clears $51-$7F range) $049D: incx $049E: cpx #$50 ; done when X == $50? $04A0: bne $0497 ; loop until X = $50 ; --- Wait for ADB bus idle --- $04A2: bil $04A2 ; spin: BIL = branch if IRQ input low (ADB busy) ; wait until ADB line goes high (bus released) ; --- Check for startup button press (self-test trigger) --- $04A4: brclr 4, $00, $04A9 ; branch if PA4 clear (button not pressed) $04A7: inc $4B ; button held: increment self-test counter ; --- Pre-set PORTB = $00 (data register, not yet driving ADB line) --- $04A9: clr $06 ; DDRC = $00: all Port C inputs $04AB: clr $01 ; PORTB = $00: pre-set data register (DDRB = $F0, bits 0-3 still inputs; not asserting ADB line) $04AD: ldx #$80 $04AF: brclr 7, $50, $045B ; wait for $50 bit 7 to be set (timer-driven) ; --- Sample initial Port C encoder state --- $04B2: tstx ; (NOP effect; X still $80) $04B3: lda $02 ; read Port C $04B5: sta $58 ; store as previous encoder state ; --- Fall through to timer_isr --- ; ============================================================================= ; TIMER OVERFLOW ISR ($04B4) [also main loop return point] ; Entered from: interrupt vector $07F8, or JMP from many places. ; Samples button state, runs encoder scan, clamps motion accumulators. ; ============================================================================= $04B4: sei ; disable interrupts $04B5: rsp ; reset stack pointer $04B6: lda #$F0 $04B8: sta $01 ; PORTB = $F0: release ADB line $04BA: lda #$0F $04BC: sta $06 ; DDRC = $0F: bits 3:0 output $04BE: jsr $00D0 ; call encoder_scan subroutine ; --- Load timer for next period --- $04C0: lda #$50 $04C2: sta $09 ; TCNT = $50 (timer preload, ~ADB poll rate) $04C4: lda #$80 $04C6: sta $49 ; IRQ dispatch → dispatch_main ($0080) $04C8: cli ; enable interrupts ; --- Clamp X delta: if |$51| > $C8, cap to $64 or $9C --- $04C9: jsr $00D0 ; encoder_scan $04CB: lda $51 $04CD: adda #$64 ; bias up by $64 (to make unsigned comparison easy) $04CF: cmpa #$C8 ; > $C8? (original $51 in range -$64..$64) $04D1: bcs $04DD ; no overflow → skip clamp $04D3: ldx #$64 $04D5: cmpa #$E4 ; > $E4? (original > $80 = max speed) $04D7: bcs $04DB $04D9: ldx #$9C $04DB: stx $51 ; clamp X delta to speed limit ; --- Clamp Y delta same way --- $04DD: jsr $00D0 ; encoder_scan $04DF: lda $52 $04E1: adda #$64 $04E3: cmpa #$C8 $04E5: bcs $04F1 $04E7: ldx #$64 $04E9: cmpa #$E4 $04EB: bcs $04EF $04ED: ldx #$9C $04EF: stx $52 ; clamp Y delta to speed limit ; --- Sample encoder speed, update $5A --- $04F1: brclr 7, $5A, $04C9 ; if $5A bit 7 clear: keep scanning (no timeout) $04F4: lda #$3F $04F6: adda $5A ; $5A += $3F (accumulate timer ticks into speed reg) $04F8: sta $5A ; --- Sample button state (PA7:6) --- $04FA: lda $00 ; read Port A $04FC: anda #$C0 ; mask to bits 7:6 (both button inputs) $04FE: cmpa $4F ; same as previous reading? $0500: beq $04C9 ; yes: no change, keep scanning $0502: sta $4F ; save new button state $0504: bset 0, $5B ; set bit 0 of $5B = "data ready to send" $0506: bra $04C9 ; loop back ; ============================================================================= ; MAIN ADB POLL LOOP ($0508) ; Entered from: dispatch_main ($0080) → JMP $0508. ; Drives ADB communication: checks for host poll, assembles and sends data. ; ============================================================================= $0508: ldx #$F0 $050A: stx $01 ; PORTB = $F0: release ADB line $050C: ldx #$0F $050E: stx $06 ; DDRC = $0F $0510: jsr $00D0 ; encoder scan $0512: clr $08 ; clear TCSR (cancel any pending timer flags) $0514: lda #$10 $0516: sta $09 ; TCNT = $10 (short timer interval) $0518: rsp ; reset stack pointer $0519: cli ; enable interrupts ; --- Decide: does host want data (SRQ sent)? Assert SRQ if data ready --- $051A: ldx #$F0 $051C: brclr 5, $4A, $0521 ; if bit 5 of $4A clear: no SRQ → X stays $F0 $051F: ldx #$FF ; SRQ requested: X = $FF (drive line for SRQ pulse) ; --- Check if there's any data at all --- $0521: lda $51 ; X delta $0523: ora $52 ; Y delta $0525: ora $5B ; or data-ready flag $0527: bne $052B ; any non-zero → we have data to report $0529: ldx #$F0 ; nothing to send → X = $F0 (release) $052B: stx $56 ; save DDR-B value ($F0=idle or $FF=SRQ) $052D: jsr $00D0 ; encoder scan $052F: lda #$08 $0531: sta $60 ; scan iteration count = 8 $0533: lda #$04 $0535: sta $57 ; ADB bit phase counter = 4 ; --- Build the X-delta byte with acceleration scaling --- $0537: ldx #$FF $0539: lda $51 ; load X accumulator $053B: adda #$01 ; add 1 (sign-extend for ADB 7-bit format) $053D: cmpa #$03 $053F: bcc $0543 $0541: ldx #$FC ; clamp if near zero $0543: lda $52 ; Y accumulator (mirrors X path below) $0545: adda #$01 $0547: cmpa #$03 $0549: txa ; move clamp value to A $054A: bcc $054E $054C: anda #$F3 ; apply mask $054E: sta $59 ; save scaled value to scratch $0550: jsr $00D0 ; encoder scan ; --- Wait for Timer Overflow flag (TCSR bit 7) --- $0552: lda $08 ; read TCSR $0554: bne $0598 ; if non-zero (TOF set): ADB event, skip to handler $0556: lda #$83 $0558: sta $49 ; IRQ dispatch → dispatch_table ($0083) $055A: jsr $00D0 ; encoder scan (3x while waiting for timer) $055C: jsr $00D0 $055E: jsr $00D0 $0560: lda #$04 $0562: sta $5F ; delay counter = 4 $0564: jsr $00D0 ; encoder scan (delay loop) $0566: dec $5F $0568: bne $0564 $056A: sei ; disable interrupts $056B: ldx #$57 $056D: brset 7, $08, $0598 ; check TCSR bit 7 again $0570: decx $0571: bne $056D ; wait up to $57 timer ticks $0573: jmp $0491 ; timeout: restart (back to reset delay loop) ; --- Do another encoder scan before assembling packet --- $0576: lda $59 ; load scaled value $0578: ldx #$F0 $057A: anda $02 ; mask with Port C $057C: stx $01 ; PORTB = $F0: release line $057E: ora $58 ; OR with previous Port C state $0580: anda $02 ; mask to current $0582: ldx #$0F $0584: stx $06 ; DDRC = $0F $0586: ldx $58 ; X = previous Port C $0588: sta $58 ; save current as new previous $058A: eora ,x ; XOR with mem[prev] (quadrature decode, first level) $058B: tax $058C: ldx ,x ; second-level indirection (dispatch offset) $058D: jsr $0437,x ; call motion handler (INC/DEC $51/$52) $0590: clr $06 ; DDRC = $00: Port C all inputs $0592: clr $01 ; PORTB = $00: drive ADB low (assert SRQ) $0594: dec $60 ; decrement scan iteration count $0596: bne $0576 ; loop 8 times $0598: jmp $04B4 ; → timer_isr (common return/entry point) ; ============================================================================= ; ADB RESPONSE HANDLER ($059B) ; Entry from: dispatch_B7, dispatch_main (after receiving ADB command byte). ; Interprets received ADB command, prepares and sends response. ; ============================================================================= $059B: rol $57 ; shift command byte $059D: lda #$F0 $059F: sta $01 ; PORTB = $F0: release ADB line $05A1: lda #$0F $05A3: sta $06 ; DDRC = $0F $05A5: lda $47 ; load received command byte $05A7: adda #$1E ; add $1E to normalize address field $05A9: adda #$03 $05AB: bmi $05A9 ; loop until positive (clamp negative) $05AD: bih $05C3 ; if ADB line high: line is ours → proceed $05AF: jsr $00D0 ; encoder scan while waiting $05B1: swi ; software interrupt (self-test checkpoint) ; --- Confirm ADB line idle before transmitting --- $05B2: lda #$0A ; retry count = 10 $05B4: ldx #$F0 $05B6: stx $05 ; DDRB = $F0 (normal output) $05B8: bih $05C3 ; line high? → can transmit $05BA: suba #$01 ; decrement retry $05BC: bih $05C3 $05BE: bne $05B8 ; try again $05C0: jmp $04B4 ; timeout: back to timer_isr ; --- Begin response: set timer and state --- $05C3: lda #$CD $05C5: sta $49 ; IRQ dispatch → dispatch_tx_bit ($00CD = JMP $0709) $05C7: lda #$50 $05C9: sta $09 ; TCNT = $50 (reset timer) $05CB: rsp $05CC: lda $57 ; load command shift register $05CE: cmpa $4E ; compare to expected full command $05D0: beq $0628 ; match → full command received, assemble packet $05D2: anda #$F0 ; mask to high nibble (command class) $05D4: cmpa $4C ; compare to stored command class $05D6: bne $0625 ; mismatch → discard $05D8: eora $57 ; XOR to isolate low nibble difference $05DA: cmpa #$0B ; TALK Register 3 (address reassign)? $05DC: beq $05E8 ; yes → handle TALK R3 $05DE: cmpa #$0F ; LISTEN Register 3 (set address)? $05E0: beq $0603 ; yes → handle LISTEN R3 $05E2: cmpa #$01 ; flush? $05E4: beq $061F ; yes → flush accumulators $05E6: bra $0625 ; unknown command: discard ; --- Handle TALK Register 3: respond with device address --- $05E8: clr $08 ; clear TCSR $05EA: lda #$10 $05EC: sta $09 ; reset timer $05EE: lda #$BE $05F0: sta $49 ; IRQ dispatch → dispatch_transmit ($00BE) $05F2: lda #$D6 $05F4: sta $5F ; delay counter $05F6: clr $5C ; clear high byte of packet ($5C = $00) $05F8: lda #$01 $05FA: sta $5D ; low byte of packet = $01 (Register 3 default) $05FC: cli ; enable interrupts $05FD: dec $5F $05FF: bne $05FD ; wait delay $0601: bra $0625 ; fall through to discard path (packet sent via IRQ) ; --- Handle LISTEN Register 3: set new ADB address --- $0603: cli $0604: lda $4B ; load ADB state counter (new address from host) $0606: sta $5D ; store as packet low byte $0608: lda $5A ; encoder speed register $060A: anda #$0F ; mask to low nibble $060C: eora $4A ; XOR with $4A (command byte fragment) $060E: sta $5C ; store as packet high byte $0610: lda $5A $0612: anda #$03 ; mask to bits 1:0 $0614: suba #$01 $0616: bpl $0614 ; delay loop (motor-step timing) $0618: ldx #$04 $061A: decx $061B: bne $061A ; short delay $061D: bra $0678 ; → adb_transmit_setup ; --- Handle Flush: clear all motion accumulators --- $061F: clr $51 ; X delta = 0 $0621: clr $52 ; Y delta = 0 $0623: clr $5B ; data-ready flag = 0 $0625: jmp $04B4 ; → timer_isr ; ============================================================================= ; ADB PACKET ASSEMBLER ($0628) ; Assembles ADB Register 0 (mouse data) from $51 (X delta), $52 (Y delta), ; and $4F (button state). ; ADB Register 0 format (2 bytes): ; Byte 0 ($5C): bit7=button0(right), bits6:0 = Y delta (7-bit 2's complement) ; Byte 1 ($5D): bit7=button1(left), bits6:0 = X delta (7-bit 2's complement) ; ============================================================================= $0628: cli $0629: ldx $4F ; X = button state snapshot (PA7:6, $C0=released) ; --- Determine which accumulator path: check button state --- $062B: brset 0, $4B, $0654 ; if $4B bit 0: use raw accumulator path ; --- Normal path: apply acceleration to Y delta --- $062E: lda $52 ; Y delta $0630: cmpa #$C0 ; negative saturated? $0632: bpl $063C $0634: cmpa #$80 $0636: lda #$3F ; max positive $0638: bcs $063C $063A: lda #$C0 ; max negative $063C: asla ; shift Y delta left (clears bit 0) $063D: aslx ; shift X (button) register left; C = old PA7 $063E: rora ; PA7 → bit 7 of $5C (button-right into MSB) $063F: sta $5C ; store Y byte with button ; --- Apply acceleration to X delta --- $0641: lda $51 ; X delta $0643: cmpa #$C0 $0645: bpl $064F $0647: cmpa #$80 $0649: lda #$3F $064B: bcs $064F $064D: lda #$C0 $064F: asla ; shift X delta left $0650: aslx ; C = old PA6 $0651: rora ; PA6 → bit 7 of $5D (button-left into MSB) $0652: bra $066A ; → store X byte ; --- Alternate path: use raw carry-extended accumulator --- $0654: lda $52 ; Y delta $0656: lsr $54 ; shift Y carry byte right $0658: adca #$00 ; add carry into delta (round) $065A: aslx ; C = PA7 (button-right) $065B: rora ; PA7 → bit 7 $065C: sta $5C ; store Y byte with button $065E: rol $54 $0660: lda $51 ; X delta $0662: lsr $53 ; shift X carry byte right $0664: adca #$00 $0666: aslx ; C = PA6 (button-left) $0667: rora ; PA6 → bit 7 $0668: rol $53 $066A: sta $5D ; store X byte with button $066C: clr $51 ; clear X delta (consumed) $066E: clr $52 ; clear Y delta (consumed) $0670: ora $5C ; OR X and Y bytes together $0672: anda #$7F ; mask off button bits $0674: ora $5B ; OR in data-ready flag $0676: beq $0625 ; if all zero (no motion, no button change): discard ; ============================================================================= ; ADB SERIAL TRANSMIT SETUP ($0678) ; Serializes $5C:$5D (16 bits) MSB-first as ADB Register 0 response. ; Uses open-drain bit-bang on PORTB, timer-driven. ; ============================================================================= $0678: ldx #$FF $067A: sei $067B: stx $05 ; DDRB = $FF (all outputs, marks "tx in progress") $067D: bclr 0, $55 ; clear SRQ flag $067F: ldx #$06 $0681: stx $5E ; bit counter = 6 (will transmit 16+overhead bits) $0683: ldx #$F0 $0685: bra $0694 ; → transmit_bit_loop ; --- per-bit ADB transmit loop --- $0687: lda #$FF $0689: sta $05 ; DDRB = $FF: maintain "tx in progress" $068B: asl $5D ; shift X-delta byte left (LSB first... or MSB?) $068D: rol $5C ; rotate Y-delta byte through carry $068F: ldx #$F0 $0691: nop $0692: bcc $06FC ; carry=0: transmit '1' bit (line released = high) $0694: stx $05 ; DDRB = $F0: normal output $0696: bil $0709 ; if ADB line low (collision): → abort_tx $0698: clr $06 ; DDRC = $00: Port C all inputs $069A: clr $01 ; PORTB = $00: drive ADB line low (mark pulse start) $069C: lda $58 ; load previous Port C value $069E: stx $05 ; DDRB = $F0 $06A0: ldx #$F0 $06A2: bil $0709 ; check for collision $06A4: ora $0002 ; OR with Port C (current encoder state) $06A7: stx $01 ; PORTB = $F0: release line (mark pulse complete) $06A9: nop $06AA: ldx #$FF $06AC: stx $05 ; DDRB = $FF (tx in progress) $06AE: asl $5D ; shift next bit $06B0: rol $5C $06B2: nop $06B3: bcc $06BB ; if '1' bit: skip space-pulse ; --- space pulse (drive line low for '0' bit) --- $06B5: ldx #$F0 $06B7: stx $05 ; DDRB = $F0 $06B9: bil $0709 ; collision check ; --- inter-bit / post-space --- $06BB: anda $02 ; mask Port C encoder bits $06BD: tstx $06BE: ldx $58 ; X = previous Port C $06C0: sta $58 ; save current as new previous $06C2: eora ,x ; quadrature first-level decode $06C3: bcs $06CB ; carry set: skip no-motion check $06C5: ldx #$F0 $06C7: stx $05 ; DDRB = $F0 $06C9: bil $0709 $06CB: ldx #$0F $06CD: stx $06 ; DDRC = $0F $06CF: tax $06D0: ldx ,x ; second-level quadrature decode $06D1: lda #$FF $06D3: sta $0005 ; DDRB = $FF (still tx in progress) $06D6: asl $5D ; shift another bit $06D8: rol $5C $06DA: lda #$F0 $06DC: bcc $0700 ; '1' bit path $06DE: nop $06DF: sta $05 ; DDRB = $F0 $06E1: bil $0709 $06E3: nop $06E4: jsr $0437,x ; ← run quadrature motion handler mid-transmit $06E7: dec $5E ; decrement bit counter $06E9: bne $0687 ; more bits to send: loop ; --- Transmission complete: clean up --- $06EB: lda $5A ; encoder speed register $06ED: suba #$07 ; decay speed by 7 $06EF: sta $5A $06F1: lda $57 ; load command phase $06F3: cmpa $4E ; compare to expected phase $06F5: bne $06F9 $06F7: clr $5B ; clear data-ready flag (packet sent successfully) $06F9: jmp $04B4 ; → timer_isr $06FC: sta $05 ; '1' bit path: DDRB = ? (A = last Port C masked) $06FE: bra $0698 ; → back into transmit loop $0700: jsr $0437,x ; run quadrature handler ('1' bit path) $0703: sta $05 ; restore DDRB $0705: bil $0709 ; collision check $0707: bra $06E7 ; → decrement bit counter ; --- Abort: ADB collision / bus taken by host --- $0709: bset 0, $55 ; set SRQ flag $070B: bra $06F9 ; → timer_isr ; ============================================================================= ; ADB BIT-TIMING HANDLERS ($070D-$075E) ; Called from dispatch targets $C1/$C4 (transmit timing) and during ADB decode. ; ============================================================================= $070D: lda #$C1 $070F: sta $49 ; IRQ dispatch → dispatch_C1 $0711: rti $0712: ldx $031B,x ; indexed dispatch (second-level ADB command decode) $0715: bpl $06F9 ; positive X: → timer_isr $0717: stx $47 ; save command code $0719: lda #$C4 $071B: sta $49 ; IRQ dispatch → dispatch_C4 $071D: rti ; --- ADB receive decode: check $5D for command type --- $071E: lda $5D ; load ADB command receive register $0720: beq $0730 ; $00: → handle null command $0722: cmpa #$FE $0724: beq $073A ; $FE: → check SRQ flag $0726: cmpa #$FD $0728: beq $0751 ; $FD: → check button double-click state $072A: cmpa #$03 $072C: bcs $075B ; < $03: → save as ADB state counter $072E: bra $06F9 ; else: → timer_isr ; --- $5D == $00: assemble ADB timing data from $5C --- $0730: lda $5C $0732: anda #$2F ; mask low bits $0734: ora #$40 ; set bit 6 $0736: sta $4A ; → ADB state parameter $0738: bra $073D $073A: brset 0, $55, $06F9 ; if SRQ set: skip this path → timer_isr ; --- Assemble command nibbles from $5C for ADB reply --- $073D: lda $5C $073F: anda #$0F ; low nibble $0741: asla $0742: asla ; shift to bits 3:2 $0743: ora #$03 ; OR in low 2 bits $0745: sta $4D ; save to command low byte $0747: asla $0748: asla ; shift to bits 5:4 $0749: sta $4E ; save to command extension $074B: anda #$F0 ; high nibble only $074D: sta $4C ; save to command high byte $074F: bra $06F9 ; → timer_isr ; --- $5D == $FD: check button state for double-click detect --- $0751: lda $00 ; read Port A $0753: anda #$C0 ; mask PA7:6 (button inputs) $0755: cmpa #$C0 ; both buttons released? $0757: bne $073D ; not both released: → normal command assembly $0759: bra $06F9 ; both released: → timer_isr (ignore) ; --- $5D < $03: save value directly as ADB state --- $075B: sta $4B ; store in ADB state counter $075D: bra $06F9 ; → timer_isr ; ============================================================================= ; QUADRATURE ENCODER SCAN ($075F) ; Called via JSR $D0 (encoder_scan) and directly from various places in the ADB loop. ; ; Algorithm: double-indirection Gray-code decode. ; - Port C bits 7:4 = $F (tied high; upper nibble) ; - Port C bit 3 = Y1, bit 2 = Y0, bit 1 = X1, bit 0 = X0 ; - $58 = previous Port C reading ; ; Step 1: Read current Port C into A, OR with $02 (force stable read). ; Step 2: ANDA $02 (mask to current Port C, DDR-C = $00 so reads live pins). ; Step 3: LDX $58 (X = previous Port C); STA $58 (save current as new previous). ; Step 4: EORA ,X (A = curr XOR ROM[prev]) -- first-level lookup. ; The ROM bytes at $F0-$FF map the combined prev+curr pair into ; a dispatch-table offset selector. ; Step 5: TAX; LDX ,X -- second-level indirection resolves to jump-table offset. ; Step 6: JMP $0437,X -- dispatch to INC/DEC $51/$52 motion handler. ; ============================================================================= $075F: ldx #$F0 $0761: lda $58 ; load previous Port C value $0763: ora $02 ; OR with current Port C (DDR-C=$00 = all inputs) $0765: stx $01 ; PORTB = $F0: release ADB line $0767: anda $02 ; AND with current Port C -- leaves current state in A $0769: ldx #$0F $076B: stx $06 ; DDRC = $0F: switch encoder pins to outputs (PC3:0) $076D: ldx $58 ; X = previous Port C value $076F: sta $58 ; save current Port C as new $58 (previous) $0771: eora ,x ; A = curr XOR ROM[prev_PortC] (first-level decode) $0772: tax ; X = result $0773: ldx ,x ; X = ROM[result] (second-level decode → jump offset) $0774: jmp $0437,x ; dispatch to jump table → INC/DEC $51/$52 or no-op ; (execution continues in jump table, ends with RTS back to caller) ; ============================================================================= ; SCI RECEIVE HANDLER ($0784) ; Entered from interrupt vector $07F6. ; Handles SCI (serial communications interface) receive. ; Also used for ADB line sense via Port C complement. ; ============================================================================= $0784: rsp ; reset stack pointer $0785: com $02 ; complement Port C (toggles all encoder bits) $0787: bne $0787 ; spin if result non-zero (wait for bus idle) $0789: bset 0, $06 ; DDRC bit 0 = 1 (drive PC0 = SCI TxD) $078B: clr $09 ; clear timer counter $078D: swi ; software interrupt (SCI self-test / sync point) $078E: ldx #$01 $0790: lda #$F0 $0792: bsr $07EB ; call sci_bit_delay_A $0794: adca #$BA ; add $BA to A with carry (bit-timing adjustment) $0796: bsr $07EF ; call sci_bit_delay_B $0798: decx $0799: beq $0790 ; loop once (X=1→0→re-enter outer) $079B: clra $079C: swi $079D: ldx #$40 $079F: sta ,x ; store $00 to $40 (clear ADB receive buffer) $07A0: rora ; rotate A right through carry $07A1: incx $07A2: bpl $079F ; loop while X positive ($40-$7F) $07A4: rola ; restore A $07A5: ldx #$40 $07A7: eora ,x ; XOR with received byte in buffer $07A8: bne $07A8 ; spin if mismatch (wait for sync) $07AA: lda ,x ; load matched byte $07AB: rora ; bit-reverse rotation (SCI bit order) $07AC: incx $07AD: bpl $07A7 ; loop through buffer $07AF: bcc $079D ; carry clear: redo receive $07B1: swi ; checkpoint $07B2: ldx #$C8 $07B4: stx $40 ; init SCI receive state machine $07B6: rol $43 ; shift SCI status bit $07B8: jsr $40 ; call via RAM (ADB timing trampoline) $07BA: inc $42 $07BC: bhi $07B8 ; loop while above $07BE: inc $41 $07C0: brclr 3, $41, $07B8 ; loop until $41 bit 3 set (8 bits received) $07C3: nega ; negate accumulated byte $07C4: sbca #$00 ; subtract borrow $07C6: bne $07C6 ; spin if framing error $07C8: swi $07C9: cli $07CA: sei $07CB: bmc $07CB ; spin while carry mask set $07CD: suba $7C ; subtract received byte from $7C $07CF: bne $07CF ; spin if mismatch $07D1: cpx $7D ; compare X with $7D $07D3: bne $07D3 ; spin if mismatch $07D5: brclr 5, $4B, $07D5 ; wait for $4B bit 5 to clear $07D8: jmp $06C3,x ; indexed jump back into transmit handler ; -- data bytes $07DB-$07E6 (part of SCI handler / lookup data) -- $07DB: .byte $E7,$04,$A6,$55 ; SCI timing/decode data $07DF: .byte $F7,$F1 ; (literal bytes, not executed as instructions) ; ============================================================================= ; SHORT ISR STUBS ($07E0-$07EE) ; ============================================================================= ; --- stub_irq1 ($07E7) -- INC $02 / increment Port C, then RTI --- $07E7: inc $02 ; increment Port C (used as SCI sync) $07E9: rti ; --- stub_irq3 ($07EA) -- BCLR 7,$09; ASR $4B; RTI --- $07EA: bclr 7, $09 ; clear timer bit 7 (TOF flag) $07EC: asr $4B ; arithmetic shift right $4B (decay state counter) $07EE: rti ; ============================================================================= ; QUADRATURE DECODE FIRST-LEVEL LOOKUP TABLE ($07EB-$07EF overlap) ; These two BSR targets within the SCI handler are also part of the ; second-level decode table used by the quadrature scan: ; $07EB: .byte $26 $FE (BSR target: "bne $07EB" -- SCI bit delay loop A) ; $07EF: .byte $81 (BSR target: $81 = RTS -- SCI bit delay loop B) ; In quadrature context: ; addr $07EB = dispatch offset $26 → alt -X path ($07EB→0x1B) [not reachable] ; addr $07EF = $81 = no-movement (RTS, offset $17-equivalent) ; ============================================================================= $07EB: bne $07EB ; SCI: spin until Port C = 0 (sync pulse) $07ED: rts ; SCI: bit delay A exits here $07EF: rts ; SCI: bit delay B (immediate return) ; ============================================================================= ; INTERRUPT VECTORS ($07F0-$07FF) ; ============================================================================= $07F0: .word $07EA ; IRQ3 vector → stub_irq3 $07F2: .word $07EC ; IRQ2 vector → stub_irq2 (ASR $4B; RTI) $07F4: .word $07E7 ; IRQ1 vector → stub_irq1 (INC $02; RTI) $07F6: .word $0784 ; SCI vector → sci_handler $07F8: .word $04B4 ; Timer vector → timer_isr $07FA: .word $0040 ; IRQ vector → irq_stub (RAM) $07FC: .word $00AF ; SWI vector → swi_handler (RTI at $00AF) $07FE: .word $047D ; Reset vector → reset