Acorn Econet Bridge 1
Updated 18 Apr 2026
← All Acorn Econet Bridge versions
- Disassembly source on GitHub
- Acorn Econet Bridge in The BBC Micro ROM Library
- Found a mistake or a comment that could be clearer? Report an issue.
| E000 | .reset←7← F2A9 DEC← F2AC DEC← F2AF DEC← F2B2 DEC← F2B5 DEC← F2B8 DEC← F2BB DEC |
| .pydis_start←7← F2A9 DEC← F2AC DEC← F2AF DEC← F2B2 DEC← F2B5 DEC← F2B8 DEC← F2BB DEC | |
| CLI ; Enable IRQs (self-test button wired to ~IRQ) | |
| E001 | CLD ; Clear decimal mode (6502 arithmetic in binary) |
| E002 | JSR init_reachable_nets ; Initialise reachable_via_a/b tables for routing |
| E005 | JSR adlc_a_full_reset ; Reset ADLC A through its full CR1/CR2/CR3/CR4 sequence |
| E008 | JSR adlc_b_full_reset ; Reset ADLC B through its full CR1/CR2/CR3/CR4 sequence |
Scan pages from &1800 upward; record top of RAMProbes pages upward from &1800 by writing &AA and &55 patterns through mem_ptr_lo/mem_ptr_hi (&80/&81) and verifying each. The highest page that verifies is stored in top_ram_page (&82), used downstream by workspace initialisation. The Bridge can be built with either one 8 KiB 6264 chip or four 2 KiB 6116 chips (chosen by soldered links), so RAM size must be discovered at power-on. The routine looks like a textbook two-pattern memory test but is considerably more robust than a naive STA/LDA/CMP would be. Three independent mechanisms have to fail simultaneously for it to report RAM where none exists: 1. The INC on zero-page &00 between each write and its matching
read is an anti-bus-residue defence. When the 6502 writes to
an unmapped address, no chip latches the value, but the data
bus capacitance can hold the written byte long enough for
the subsequent LDA to sample its own ghost. INC $00 is a
read-modify-write that drives the data bus three times with
values unrelated to the test pattern (the cycle-4 dummy
write is a classic NMOS 6502 quirk that is exploited here),
clobbering any residue of &AA or &55.
2. The choice of &00 specifically is an alias tripwire. If the
address decoder is miswired and the target address aliases
into zero page, the obvious alias landing point is &00 — so
disturbing &00 between write and read forces any alias-based
false-positive to fail the CMP.
3. The two patterns &AA and &55 are bitwise complements: a
stuck bit is detected on whichever pattern it contradicts,
and a single-value bus residue cannot spoof both checks
simultaneously.
See docs/analysis/ram-test-anti-aliasing.md for the full cycle-level analysis. |
|
| E00B | .ram_test |
| LDY #0 ; Y = 0: ZP offset used by (mem_ptr_lo),Y throughout | |
| E00D | STY mem_ptr_lo ; Clear mem_ptr_lo so every probe is page-aligned |
| E00F | LDA #&17 ; A = &17: seed for mem_ptr_hi (first probe will be page &18) |
| E011 | STA mem_ptr_hi ; Commit mem_ptr_hi; first INC at loop head advances to &18 |
| E013 | .ram_test_loop←1← E02B BEQ |
| INC mem_ptr_hi ; Step up to the next candidate page | |
| E015 | LDA #&aa ; Pattern 1: &AA (1010_1010) -- half the bits set |
| E017 | STA (mem_ptr_lo),y ; Write &AA to (mem_ptr_lo) indirect |
| E019 | INC l0000 ; INC $00: read-modify-write disturbs the data bus... |
| E01B | LDA (mem_ptr_lo),y ; ...then read the probe byte back |
| E01D | CMP #&aa ; Did &AA survive the disturbance? |
| E01F | BNE ram_test_done ; Mismatch -> this page isn't real RAM; back off |
| E021 | LDA #&55 ; Pattern 2: &55 (0101_0101) -- exact complement of &AA |
| E023 | STA (mem_ptr_lo),y ; Write &55 to (mem_ptr_lo) indirect |
| E025 | INC l0000 ; INC $00 again -- anti-aliasing tripwire |
| E027 | LDA (mem_ptr_lo),y ; Read pattern 2 back |
| E029 | CMP #&55 ; Did &55 survive? |
| E02B | BEQ ram_test_loop ; Both patterns held -- real RAM, try next page |
| E02D | .ram_test_done←1← E01F BNE |
| DEC mem_ptr_hi ; Step back one: last-probed page failed, prior page was OK | |
| E02F | LDA mem_ptr_hi ; Read the highest-verified page number |
| E031 | STA top_ram_page ; Save as top_ram_page; workspace init caps buffers here |
| fall through ↓ | |
Emit the boot-time BridgeReset pair on both Econet sidesSecond half of the reset handler. Clears announce_flag so the idle-path re-announcer starts quiescent, then builds a single BridgeReset template (ctrl=&80, port=&9C, payload=net_num_b) and transmits it twice: first on side A with net_num_b in the payload, then on side B after patching the payload to net_num_a. The two wait_adlc_?_idle calls gate each transmit on carrier-sense; either can escape to main_loop if the line never goes idle. Falls through to main_loop on success. A clean reset therefore emits exactly two frames before steady-state polling begins. See two-broadcasts-one-template.md for why one template suffices. |
|
| E033 | .reset_announce_broadcasts |
| LDA #0 ; A = 0: clear announce_flag (idle path stays quiet initially) | |
| E035 | STA announce_flag ; Commit announce_flag = 0 to workspace |
| E038 | JSR build_announce_b ; Build the BridgeReset scout template into &045A-&0460 |
| E03B | JSR wait_adlc_a_idle ; CSMA: wait for side A's line to go idle |
| E03E | JSR transmit_frame_a ; Transmit first broadcast (announcing net_num_b to side A) |
| E041 | LDA net_num_a ; Load net_num_a -- the payload byte for the B-side broadcast |
| E044 | STA tx_data0 ; Patch payload byte 0 of the template in-place |
| E047 | LDA #4 ; A = &04: reset mem_ptr_hi to the template's base page... |
| E049 | STA mem_ptr_hi ; ...so transmit_frame_b re-reads from &045A |
| E04B | JSR wait_adlc_b_idle ; CSMA: wait for side B's line to go idle |
| E04E | JSR transmit_frame_b ; Transmit second broadcast (announcing net_num_a to side B) |
| fall through ↓ | |
Main Bridge loop: re-arm ADLCs, poll for frames, re-announceThe Bridge's continuous-operation entry point. Reached by fall- through from the reset handler once startup completes, and by JMP from fourteen other sites — every routine that takes an "escape to main" path (wait_adlc_a_idle, transmit_frame_a/b, etc.) lands here, so main_loop is the anchor of every packet-processing cycle. The header (&E051-&E078) forces each ADLC into a known RX-listening state: if SR2 bit 0 or 7 (AP or RDA) is already set from a partial or aborted previous operation, CR1 is cycled through &C2 (reset TX, leave RX running) before setting it to &82 (TX in reset, RX IRQs enabled). CR2 is set to &67 — the standard listen-mode value used throughout the firmware. The inner poll loop at main_loop_poll (&E079) tests SR1 bit 7 (IRQ summary) on each ADLC in turn, with side B checked first. If either chip has a pending IRQ, control jumps straight to the corresponding frame handler; otherwise the idle path at main_loop_idle (&E089) runs the periodic re-announcement. The re-announce scheme uses three bytes of workspace: announce_flag enables re-announce (bit 7 additionally selects
which side the re-announce goes out on)
announce_tmr_ 16-bit countdown, decremented every idle-path
lo/hi iteration; zero triggers the re-announce
announce_count remaining re-announce cycles; when this hits
zero, announce_flag is cleared and re-announce
stops until something else re-enables it
The re-announce path (&E098) rebuilds the announcement frame, sets tx_ctrl to &81 (distinguishing it from the reset-time &80 first announcement), then dispatches to side A or side B based on announce_flag bit 7. The timer is re-armed to &8000 (32768 idle iterations) after each announce, giving a roughly constant cadence regardless of how busy the ADLCs are with other traffic. |
|
| E051 | .main_loop←14← E0BF JMP← E0C7 JMP← E13C JMP← E1D3 JMP← E260 JMP← E2BD JMP← E354 JMP← E3E1 JMP← E4D6 JMP← E52D JMP← E5B3 JMP← E644 JMP← E6D0 JMP← E71C JMP |
| LDA adlc_a_cr2 ; Read ADLC A's SR2 | |
| E054 | AND #&81 ; Mask AP/RDA bits to test for any stale RX state |
| E056 | BEQ main_loop_arm_a ; Clean -> skip the TX reset |
| E058 | LDA #&c2 ; Mask: reset TX, leave RX running |
| E05A | STA adlc_a_cr1 ; Clear any stale TX state on ADLC A |
| E05D | .main_loop_arm_a←1← E056 BEQ |
| LDX #&82 ; X = &82: listen-mode CR1 (TX reset, RX IRQ) | |
| E05F | STX adlc_a_cr1 ; Commit CR1 on ADLC A |
| E062 | LDY #&67 ; Y = &67: listen-mode CR2 (status-clear pattern) |
| E064 | STY adlc_a_cr2 ; Commit CR2 on ADLC A |
| E067 | LDA adlc_b_cr2 ; Read ADLC B's SR2 |
| E06A | AND #&81 ; Mask AP/RDA to test for any stale RX state |
| E06C | BEQ main_loop_arm_b ; Clean -> skip the TX reset on B |
| E06E | LDA #&c2 ; Mask: reset TX, leave RX running |
| E070 | STA adlc_b_cr1 ; Clear any stale TX state on ADLC B |
| E073 | .main_loop_arm_b←1← E06C BEQ |
| STX adlc_b_cr1 ; Commit CR1 on ADLC B (X still = &82) | |
| E076 | STY adlc_b_cr2 ; Commit CR2 on ADLC B (Y still = &67) |
| E079 | .main_loop_poll←5← E08C BEQ← E091 BNE← E096 BNE← E144 JMP← E2C5 JMP |
| BIT adlc_b_cr1 ; BIT ADLC B's SR1 -- N <- bit 7 (IRQ summary) | |
| E07C | BPL main_loop_poll_a ; B quiet -> check A |
| E07E | JMP rx_frame_b ; B has an event -> dispatch to rx_frame_b |
| E081 | .main_loop_poll_a←1← E07C BPL |
| BIT adlc_a_cr1 ; BIT ADLC A's SR1 -- N <- bit 7 (IRQ summary) | |
| E084 | BPL main_loop_idle ; A quiet -> nothing to do; maybe re-announce |
| E086 | JMP rx_frame_a ; A has an event -> dispatch to rx_frame_a |
| E089 | .main_loop_idle←1← E084 BPL |
| LDA announce_flag ; Read announce_flag -- is a re-announce burst pending? | |
| E08C | BEQ main_loop_poll ; No burst in progress -> straight back to polling |
| E08E | DEC announce_tmr_lo ; Tick the 16-bit re-announce countdown, low byte |
| E091 | BNE main_loop_poll ; Low byte didn't wrap -> keep polling |
| E093 | DEC announce_tmr_hi ; Low byte wrapped -> tick the high byte too |
| E096 | BNE main_loop_poll ; Timer hasn't expired yet -> keep polling |
| fall through ↓ | |
Emit one BridgeReply in an in-progress response burstReached from main_loop_idle once the 16-bit announce_tmr has ticked down to zero *and* announce_flag is non-zero. Both conditions are only met after rx_?_handle_80 has set the flag in response to a BridgeReset received from another bridge. This routine is the per-tick action of that response burst -- it is NOT a self-scheduled periodic announcement. Rebuilds the outbound template via build_announce_b and patches tx_ctrl from &80 (the BridgeReset value the builder writes) to &81 (BridgeReply), distinguishing the follow-up announcements from the initial one that triggered the burst. Which side to transmit on is selected by announce_flag bit 7: bit 7 clear (flag = 1..&7F) -> transmit via ADLC A (side A)
bit 7 set (flag = &80..FF) -> transmit via ADLC B, after
patching tx_data0 with
net_num_a, mirroring the
reset-time dual-broadcast.
Each invocation decrements announce_count. When it hits zero, announce_flag is cleared (re_announce_done); the burst is complete and the idle path goes quiet until another BridgeReset arrives. Otherwise the timer is re-armed to &8000 and control returns to main_loop (re_announce_rearm). Before transmitting on one side, the routine resets the OTHER ADLC's TX path (CR1 = &C2) to prevent the opposite side from inadvertently transmitting a colliding frame while we're busy. |
|
| E098 | .re_announce |
| JSR build_announce_b ; Rebuild the frame template from scratch (ctrl=&80 default) | |
| E09B | LDA #&81 ; A = &81: the BridgeReply control byte |
| E09D | STA tx_ctrl ; Patch tx_ctrl to &81 -- this announcement is a reply |
| E0A0 | BIT announce_flag ; Test announce_flag bit 7 via BIT |
| E0A3 | BMI re_announce_side_b ; Bit 7 set -> send via ADLC B (re_announce_side_b) |
| E0A5 | LDA #&c2 ; Side-A path: silence B's TX first |
| E0A7 | STA adlc_b_cr1 ; Reset ADLC B's TX to avoid a cross-side collision |
| E0AA | JSR wait_adlc_a_idle ; CSMA wait on A before transmitting |
| E0AD | JSR transmit_frame_a ; Send the BridgeReply on ADLC A |
| E0B0 | DEC announce_count ; Decrement burst-remaining count |
| E0B3 | BEQ re_announce_done ; Count hit zero -> clear announce_flag |
| E0B5 | .re_announce_rearm←1← E0E0 BNE |
| LDA #&80 ; A = &80: reseed timer high byte | |
| E0B7 | STA announce_tmr_hi ; Store new timer_hi |
| E0BA | LDA #0 ; A = 0: timer low byte |
| E0BC | STA announce_tmr_lo ; Store timer_lo; next firing in ~&8000 idle iterations |
| E0BF | JMP main_loop ; Continue the main loop |
| E0C2 | .re_announce_done←2← E0B3 BEQ← E0DE BEQ |
| LDA #0 ; A = 0: 'burst complete' marker | |
| E0C4 | STA announce_flag ; Clear announce_flag; re-announce stops until next BridgeReset |
| E0C7 | JMP main_loop ; Continue the main loop |
| E0CA | .re_announce_side_b←1← E0A3 BMI |
| LDA net_num_a ; Fetch our side-A network number | |
| E0CD | STA tx_data0 ; Patch tx_data0: this frame announces net_num_a to side B |
| E0D0 | LDA #&c2 ; Mask: reset TX, RX going |
| E0D2 | STA adlc_a_cr1 ; Silence ADLC A's TX to avoid collision while we send on B |
| E0D5 | JSR wait_adlc_b_idle ; CSMA wait on B |
| E0D8 | JSR transmit_frame_b ; Send the BridgeReply on ADLC B |
| E0DB | DEC announce_count ; Decrement burst-remaining count |
| E0DE | BEQ re_announce_done ; Count hit zero -> clear announce_flag |
| E0E0 | BNE re_announce_rearm ; Not exhausted -> re-arm timer and continue (ALWAYS branch) |
Drain and dispatch an inbound frame on ADLC AReached from main_loop_poll when ADLC A raises SR1 bit 7. Drains the incoming scout frame from the RX FIFO into the rx_* buffer at &023C-&024F, runs two levels of filtering, and then dispatches on the control byte to per-message handlers. Filtering stage 1 — addressing: Expect SR2 bit 0 (AP: Address Present) -- if missing, bail to main_loop (spurious IRQ). Read byte 0 (rx_dst_stn) and byte 1 (rx_dst_net). If rx_dst_net is zero (local net) or reachable_via_b[rx_dst_net] is zero (unknown network), jump to rx_a_not_for_us (&E13F): ignore the frame, re-listen, drop back to main_loop_poll without a full main_loop re-init. Draining: Read the rest of the frame in byte-pairs into &023C+Y up to Y=20 (the Bridge only keeps the first 20 bytes). After the drain, force CR1=0 and CR2=&84 to halt the chip and test SR2 bit 1 (FV, Frame Valid). If FV is clear, the frame was corrupt or short -- bail to main_loop. If SR2 bit 7 (RDA) is also set, read one trailing byte. Filtering stage 2 — broadcast check: Only frames with dst_stn == dst_net == &FF (full broadcast) proceed to the bridge-protocol dispatcher. Everything else falls to rx_a_forward (&E208), the cross-network forwarding path (not yet analysed). Dispatch on rx_ctrl (after verifying rx_port == &9C = bridge protocol): &80 -> rx_a_handle_80 (&E1D6) - initial bridge announcement &81 -> rx_a_handle_81 (&E1EE) - re-announcement &82 -> rx_a_handle_82 (&E19D) - bridge query (tentative) &83 -> rx_a_handle_83 (&E195) - bridge query, known-station other -> rx_a_forward (&E208) - forward or discard The side-B handler at &E263 is the mirror of this routine. |
|
| E0E2 | .rx_frame_a←1← E086 JMP |
| LDA #1 ; A = &01: mask SR2 bit 0 (AP = Address Present) | |
| E0E4 | BIT adlc_a_cr2 ; BIT SR2 -- confirm the IRQ was a frame start |
| E0E7 | BEQ rx_frame_a_bail ; AP not set -> spurious IRQ, return to main_loop |
| E0E9 | LDA adlc_a_tx ; Read FIFO byte 0: destination station |
| E0EC | STA rx_dst_stn ; Stage dst_stn into the rx header buffer |
| E0EF | JSR wait_adlc_a_irq ; Block until ADLC A IRQs again (byte 1 ready) |
| E0F2 | BIT adlc_a_cr2 ; BIT SR2 -- RDA still set for the next byte? |
| E0F5 | BPL rx_frame_a_bail ; RDA cleared: frame truncated before dst_net, bail |
| E0F7 | LDY adlc_a_tx ; Read byte 1 into Y: destination network |
| E0FA | BEQ rx_a_not_for_us ; dst_net == 0 means 'local net of sender' -- not for us |
| E0FC | LDA reachable_via_b,y ; Probe reachable_via_b[dst_net] for a route via side B |
| E0FF | BEQ rx_a_not_for_us ; No route -> frame isn't ours to drain, re-listen |
| E101 | STY rx_dst_net ; Commit dst_net now that it has passed filtering |
| E104 | LDY #2 ; Y = 2: resume drain at offset 2 (after header) |
| E106 | .rx_frame_a_drain←1← E11E BCC |
| JSR wait_adlc_a_irq ; Wait for the next FIFO byte IRQ | |
| E109 | BIT adlc_a_cr2 ; BIT SR2 -- RDA still asserted? |
| E10C | BPL rx_frame_a_end ; RDA cleared mid-body -> go to FV check |
| E10E | LDA adlc_a_tx ; Read byte Y of payload from TX/RX FIFO |
| E111 | STA rx_dst_stn,y ; Store into rx_dst_stn+Y (buffer grows into rx_*) |
| E114 | INY ; Advance Y to the next slot |
| E115 | LDA adlc_a_tx ; Read byte Y+1 (pair-read without an IRQ wait) |
| E118 | STA rx_dst_stn,y ; Store the second byte of the pair |
| E11B | INY ; Advance Y past the pair |
| E11C | CPY #&14 ; Cap at 20 bytes (6-byte header + up to 14 payload) |
| E11E | BCC rx_frame_a_drain ; Under cap -> keep draining |
| E120 | .rx_frame_a_end←1← E10C BPL |
| LDA #0 ; A = &00: halt ADLC A | |
| E122 | STA adlc_a_cr1 ; CR1 = 0: disable TX and RX IRQs |
| E125 | LDA #&84 ; A = &84: clear-RX-status + FV-clear bits |
| E127 | STA adlc_a_cr2 ; Commit CR2: acknowledge end-of-frame |
| E12A | LDA #2 ; A = &02: mask SR2 bit 1 (FV: Frame Valid) |
| E12C | BIT adlc_a_cr2 ; BIT SR2 -- test FV and RDA |
| E12F | BEQ rx_frame_a_bail ; FV clear -> frame corrupt or short, bail |
| E131 | BPL rx_frame_a_dispatch ; FV set + no RDA -> clean end; go to dispatch |
| E133 | LDA adlc_a_tx ; FV + RDA: one trailing byte still in FIFO |
| E136 | STA rx_dst_stn,y ; Store the odd trailing byte |
| E139 | INY ; Advance Y to count that final byte |
| E13A | BNE rx_frame_a_dispatch ; Unconditional: continue to dispatch |
| E13C | .rx_frame_a_bail←4← E0E7 BEQ← E0F5 BPL← E12F BEQ← E14F BCC |
| JMP main_loop ; Bail: restart from main_loop (full ADLC re-init) | |
| E13F | .rx_a_not_for_us←2← E0FA BEQ← E0FF BEQ |
| LDA #&a2 ; A = &A2: RX on, IRQ enabled, TX in reset | |
| E141 | STA adlc_a_cr1 ; Re-arm ADLC A to listen for the next frame |
| E144 | JMP main_loop_poll ; Skip main_loop re-init; go straight back to polling |
| E147 | .rx_a_to_forward←2← E171 BNE← E180 BNE |
| JMP rx_a_forward ; Out-of-range JMP to rx_a_forward (JSR can't reach &E208) | |
| E14A | .rx_frame_a_dispatch←2← E131 BPL← E13A BNE |
| STY rx_len ; Save final byte count (even if 0 bytes of payload) | |
| E14D | CPY #6 ; Compare to 6 -- minimum valid scout header |
| E14F | BCC rx_frame_a_bail ; Shorter than header -> bail |
| E151 | LDA rx_src_net ; Load src_net from the drained frame |
| E154 | BNE rx_a_src_net_resolved ; Non-zero -> sender supplied src_net, keep it |
| E156 | LDA net_num_a ; Sender left src_net = 0 ('my local net') |
| E159 | STA rx_src_net ; ...substitute our own A-side network number |
| E15C | .rx_a_src_net_resolved←1← E154 BNE |
| LDA net_num_b ; Load our B-side network number for comparison | |
| E15F | CMP rx_dst_net ; Compare against the incoming dst_net |
| E162 | BNE rx_a_broadcast_check ; Not for side B -> skip the local rewrite |
| E164 | LDA #0 ; dst_net names our B-side network... |
| E166 | STA rx_dst_net ; ...normalise dst_net to 0 (local on B) |
| E169 | .rx_a_broadcast_check←1← E162 BNE |
| LDA rx_dst_stn ; Load dst_stn for the broadcast test | |
| E16C | AND rx_dst_net ; AND with dst_net (both &FF only if full broadcast) |
| E16F | CMP #&ff ; Compare result to &FF |
| E171 | BNE rx_a_to_forward ; Not a full broadcast -> forward path |
| E173 | JSR adlc_a_listen ; Broadcast: re-arm A's listen mode for any follow-up |
| E176 | LDA #&c2 ; A = &C2: reset TX, enable RX |
| E178 | STA adlc_a_cr1 ; Commit CR1 while we process the bridge-protocol frame |
| E17B | LDA rx_port ; Load the port byte from the drained frame |
| E17E | CMP #&9c ; Compare with &9C (bridge-protocol port) |
| E180 | BNE rx_a_to_forward ; Not our port -> drop into forward path |
| E182 | LDA rx_ctrl ; Load ctrl byte for the per-type dispatch |
| E185 | CMP #&81 ; Test &81 (BridgeReply: re-announcement) |
| E187 | BEQ rx_a_handle_81 ; Match -> rx_a_handle_81 |
| E189 | CMP #&80 ; Test &80 (BridgeReset: initial announcement) |
| E18B | BEQ rx_a_handle_80 ; Match -> rx_a_handle_80 |
| E18D | CMP #&82 ; Test &82 (WhatNet: general query) |
| E18F | BEQ rx_a_handle_82 ; Match -> rx_a_handle_82 |
| E191 | CMP #&83 ; Test &83 (IsNet: targeted query) |
| E193 | BNE rx_a_forward ; Unknown ctrl -> forward path (fall through to rx_a_handle_83 on match) |
| fall through ↓ | |
Side-A IsNet query (ctrl=&83): targeted network lookupCalled when a received frame on side A is broadcast + port=&9C + ctrl=&83. In JGH's BRIDGE.SRC this query type is named "IsNet" — the querier is asking "can you reach network X?", where X is the byte at offset 13 of the payload (rx_query_net). Consults reachable_via_b[rx_query_net]. If the entry is zero, we have no route to that network so the query is silently dropped (JMP main_loop via &E1D3). If non-zero, falls through to the shared response body at rx_a_handle_82 to transmit the reply -- so IsNet is effectively WhatNet with an up-front routing filter. |
|
| E195 | .rx_a_handle_83 |
| LDY rx_query_net ; Y = the queried network number | |
| E198 | LDA reachable_via_b,y ; Check if we have a route via the other side |
| E19B | BEQ rx_a_query_done ; Unknown -> silently drop this IsNet query |
| fall through ↓ | |
Side-A WhatNet query (ctrl=&82); also the IsNet response pathCalled when a received frame on side A is broadcast + port=&9C + ctrl=&82 (named "WhatNet" in JGH's BRIDGE.SRC — a general bridge query asking "which networks do you reach?"), or when rx_a_handle_83 has verified that a specific IsNet queried network is in fact reachable via side B and is re-using this response path. The response is a complete four-way handshake transaction, which the Bridge drives from the responder side as two transmissions (scout, then data) with an inbound ACK after each: 1. Build a reply-scout template via build_query_response,
addressed back to the querier on its local network with
tx_src_net patched to our net_num_b.
2. Stagger the scout transmission via stagger_delay, seeded
from net_num_b. Multiple bridges on the same segment will
all react to a broadcast query, and without the stagger
their responses would overlap on the wire; seeding from the
network number gives each bridge a deterministic but
distinct delay.
3. CSMA, transmit the scout, then handshake_rx_a to receive
the scout-ACK.
4. Rebuild the frame via build_query_response again -- this
time to be a *data* frame following the scout we just
exchanged, not a new scout. The patches that follow populate
the first two payload bytes of that data frame (at the byte
positions labelled tx_ctrl and tx_port, but those names
refer to scout semantics -- in a data frame those slots are
payload, not header, and the bytes are:
data0 = net_num_a ... the Bridge's side-A network
data1 = rx_query_net ... echo of the queried network
The answer thus consists of the dst/src quad plus two
payload bytes, packed into the smallest Econet frame that
can carry it.
5. Transmit the data frame, then handshake_rx_a for the final
data-ACK. JMP main_loop on completion.
Either handshake_rx_a call can escape to main_loop if the querier doesn't keep up the handshake, aborting the conversation cleanly. |
|
| ; Re-arm A for listen after the received query | |
| E19D | .rx_a_handle_82←1← E18F BEQ |
| JSR adlc_a_listen ; Re-arm ADLC A into listen mode before replying | |
| E1A0 | JSR build_query_response ; Build reply-scout template addressed at the querier |
| E1A3 | LDA net_num_b ; Fetch our side-B network number |
| E1A6 | STA tx_src_net ; Patch src_net so the scout names us by net_num_b |
| E1A9 | STA ctr24_lo ; Copy it into the stagger-delay counter too |
| E1AC | JSR stagger_delay ; Busy-wait for (net_num_b * ~50us) + 160us |
| E1AF | JSR wait_adlc_a_idle ; CSMA wait on A so we don't collide with live traffic |
| E1B2 | JSR transmit_frame_a ; Transmit the reply scout |
| E1B5 | JSR handshake_rx_a ; Wait for the querier's scout-ACK on A |
| E1B8 | JSR build_query_response ; Rebuild template -- next frame is the data response |
| E1BB | LDA net_num_b ; Fetch net_num_b |
| E1BE | STA tx_src_net ; Re-patch src_net (rebuilt block needs it again) |
| E1C1 | LDA net_num_a ; Fetch net_num_a |
| E1C4 | STA tx_ctrl ; Write it as data-frame payload byte 0 (tx_ctrl slot) |
| E1C7 | LDA rx_query_net ; Fetch the network the querier asked about |
| E1CA | STA tx_port ; Write it as data-frame payload byte 1 (tx_port slot) |
| E1CD | JSR transmit_frame_a ; Transmit the data frame |
| E1D0 | JSR handshake_rx_a ; Wait for the querier's final data-ACK |
| E1D3 | .rx_a_query_done←1← E19B BEQ |
| JMP main_loop ; Transaction complete -> back to main_loop | |
Side-A BridgeReset (ctrl=&80): learn topology from scratchCalled when a received frame on side A is broadcast + port=&9C + ctrl=&80. In JGH's BRIDGE.SRC this control byte is named "BridgeReset" -- a bridge on the far side is advertising a fresh topology, likely because it has itself just come up. We: 1. Wipe all learned routing state via init_reachable_nets. The
topology may have changed non-monotonically, so accumulated
reachable_via_? entries are suspect and the safe move is to
discard them and relearn.
2. Schedule a burst of our own re-announcements: ten cycles with
a staggered initial timer value seeded from net_num_b. Using
the local network number as the timer's phase means bridges
on different segments aren't all re-announcing at the same
millisecond. announce_flag is set to &40 (enable, bit 7
clear = next outbound on side A).
3. Fall through to rx_a_handle_81 (the same payload-processing
loop runs for both BridgeReset and BridgeReply) to mark the
sender's known networks as reachable-via-A.
This is one of only two places in the ROM that sets announce_flag non-zero (the other is the mirror rx_b_handle_80). Receiving a BridgeReply (ctrl=&81) does not trigger the burst; only receiving a BridgeReset does. A solo bridge therefore stays silent after its boot-time BridgeReset pair, because nothing comes back to trigger a response. See the event-driven-reannouncement writeup. |
|
| E1D6 | .rx_a_handle_80←1← E18B BEQ |
| JSR init_reachable_nets ; Wipe all learned routing state (topology reset) | |
| E1D9 | LDA net_num_b ; Fetch our side-B network number |
| E1DC | STA announce_tmr_hi ; Use it as the re-announce timer's high byte (stagger) |
| E1DF | LDA #0 ; A = 0: timer low byte |
| E1E1 | STA announce_tmr_lo ; Store timer_lo; first fire in (net_num_b * 256) idle ticks |
| E1E4 | LDA #&0a ; A = 10: number of BridgeReplies to emit |
| E1E6 | STA announce_count ; Store the burst count |
| E1E9 | LDA #&40 ; A = &40: enable re-announce, bit 7 clear = send via A |
| E1EB | STA announce_flag ; Set announce_flag; main loop will now schedule the burst |
| fall through ↓ | |
Side-A BridgeReply (ctrl=&81): learn and re-broadcastReached either directly as the ctrl=&81 handler ("BridgeReply" / "ResetReply" in JGH's source — the re-announcement that follows a BridgeReset) or via fall-through from rx_a_handle_80 (which additionally wipes routing state before the learn loop). Processes the announcement payload: each byte from offset 6 up to rx_len is a network number that the announcer says it can reach. Since the announcer is on side A, we can reach those networks via side A ourselves -- mark each in reachable_via_a. After the learn loop, append our own net_num_a to the payload and bump rx_len. Falling through to rx_a_forward re-broadcasts the augmented frame out of ADLC B, so any bridges beyond us on that side hear about the announced networks plus us as one further hop along the route. This is classic distance-vector flooding. A subtlety: JGH's BRIDGE.SRC memory-layout comments describe the payload as sometimes starting with the literal ASCII string "BRIDGE" at bytes 6-11 (in query frames). Our handler makes no such check -- it treats every byte from offset 6 up as a network number. A frame from a "newer" variant that prepended "BRIDGE" would have bytes &42 &52 &49 &44 &47 &45 erroneously marked as reachable network numbers. No evidence that any in-the-wild variant does this for ctrl=&80/&81; our own ROM doesn't emit the string in any outbound frame. |
|
| E1EE | .rx_a_handle_81←1← E187 BEQ |
| LDY #6 ; Y = 6: skip past the 6-byte scout header | |
| E1F0 | .rx_a_learn_loop←1← E1FD BNE |
| LDA rx_dst_stn,y ; Fetch next announced network number from payload | |
| E1F3 | TAX ; X = the network to record |
| E1F4 | LDA #&ff ; A = &FF: 'route known' marker |
| E1F6 | STA reachable_via_a,x ; Remember that network X is reachable via side A |
| E1F9 | INY ; Advance to next payload byte |
| E1FA | CPY rx_len ; Have we reached the end of the payload? |
| E1FD | BNE rx_a_learn_loop ; No -- keep learning |
| E1FF | LDA net_num_a ; Load our own side-A network number |
| E202 | STA rx_dst_stn,y ; Append it to the payload for the onward broadcast |
| E205 | INC rx_len ; Payload grew by one byte; record the new length |
| fall through ↓ | |
Forward an A-side frame to B, completing the 4-way handshakeEntry point for cross-network forwarding of frames received on side A. Reached from three places: * rx_a_to_forward (&E147): the A-side frame is addressed to a remote station (not a full broadcast), and we have accepted it via the routing filter. * rx_frame_a ctrl dispatch fall-through (&E193): the frame is broadcast + port &9C but has a control byte outside the recognised bridge-protocol set (&80-&83). * Fall-through from rx_a_handle_81 (&E207): we've learned from the announcement and appended net_num_a to the payload; now propagate it onward. The routine bridges the complete Econet four-way handshake by alternating direct-forward, receive-on-one-side, and re-transmit: Stage 1 (SCOUT, A -> B): the inbound scout already sits in the rx_* buffer (&023C..). Round rx_len down to even, wait for B to be idle, then push the bytes directly into adlc_b_tx in pairs (odd-length frames send the trailing byte as a single write). Terminate by writing CR2=&3F (end-of-burst). Stage 2 (ACK1, B -> A): handshake_rx_b drains the receiver's ACK from ADLC B into the &045A staging buffer. transmit_frame_a forwards it to the originator. Stage 3 (DATA, A -> B): handshake_rx_a drains the sender's data frame from ADLC A into &045A. transmit_frame_b forwards it to the destination. Stage 4 (ACK2, B -> A): handshake_rx_b drains the receiver's final ACK. transmit_frame_a forwards it to the originator. Each handshake_rx_? call can escape to main_loop (PLA/PLA/JMP) if the expected frame doesn't arrive, cleanly aborting the bridged conversation without further work on either side. The A-B-A transmit pattern that appears at the routine's tail is therefore the natural shape of a bridged four-way handshake when the initial scout came from side A: two frames travel A -> B (scout and data) and two travel B -> A (two ACKs). |
|
| E208 | .rx_a_forward←2← E147 JMP← E193 BNE |
| LDA rx_len ; Read rx_len into A | |
| E20B | TAX ; Preserve original length in X for odd-parity check |
| E20C | AND #&fe ; Mask low bit to round DOWN to even byte count |
| E20E | STA rx_len ; Store the rounded count for the pair loop |
| E211 | JSR wait_adlc_b_idle ; CSMA wait on B before transmitting the forwarded scout |
| E214 | LDY #0 ; Y = 0: start at byte 0 of the rx_* buffer |
| E216 | .rx_a_forward_pair_loop←1← E22F BCC |
| JSR wait_adlc_b_irq ; Wait for ADLC B's TDRA | |
| E219 | BIT adlc_b_cr1 ; BIT SR1 -- V <- bit 6 (TDRA) |
| E21C | BVC rx_a_forward_done ; TDRA clear -> chip lost sync, escape to main_loop |
| E21E | LDA rx_dst_stn,y ; Load byte Y of the received scout |
| E221 | STA adlc_b_tx ; Push it to ADLC B's TX FIFO |
| E224 | INY ; Advance Y |
| E225 | LDA rx_dst_stn,y ; Load byte Y+1 |
| E228 | STA adlc_b_tx ; Push the second byte of the pair |
| E22B | INY ; Advance Y again |
| E22C | CPY rx_len ; Have we reached the even-rounded length yet? |
| E22F | BCC rx_a_forward_pair_loop ; No -> keep looping |
| E231 | TXA ; Recover original length from X for parity check |
| E232 | ROR ; ROR: carry <- bit 0 (= original length was odd?) |
| E233 | BCC rx_a_forward_ack_round ; Even -> skip the trailing-byte path |
| E235 | JSR wait_adlc_b_irq ; Odd: wait for TDRA once more for the last byte |
| E238 | LDA rx_dst_stn,y ; Load the trailing byte |
| E23B | STA adlc_b_tx ; Push it to the TX FIFO |
| E23E | .rx_a_forward_ack_round←1← E233 BCC |
| LDA #&3f ; A = &3F: end-of-burst CR2 value | |
| E240 | STA adlc_b_cr2 ; Commit CR2 -- ADLC B flushes the scout |
| E243 | JSR wait_adlc_b_irq ; Wait for the frame-complete IRQ |
| E246 | LDA #&5a ; A = &5A: reset mem_ptr_lo for the handshake stages below |
| E248 | STA mem_ptr_lo ; Store mem_ptr_lo |
| E24A | LDA #4 ; A = 4: reset mem_ptr_hi |
| E24C | STA mem_ptr_hi ; Store mem_ptr_hi -- handshake_rx_? will write here |
| E24E | JSR handshake_rx_b ; Stage 2: drain ACK1 from B into &045A... |
| E251 | JSR transmit_frame_a ; ...and retransmit it on A so the originator hears its ACK |
| E254 | JSR handshake_rx_a ; Stage 3: drain DATA from A into &045A... |
| E257 | JSR transmit_frame_b ; ...and retransmit it on B to the destination |
| E25A | JSR handshake_rx_b ; Stage 4: drain ACK2 from B into &045A... |
| E25D | JSR transmit_frame_a ; ...and retransmit it on A as the final ACK |
| E260 | .rx_a_forward_done←1← E21C BVC |
| JMP main_loop ; 4-way handshake bridged; back to main_loop | |
Drain and dispatch an inbound frame on ADLC BByte-for-byte mirror of rx_frame_a (&E0E2): same three-stage structure (addressing filter, drain, broadcast + bridge-protocol check), same control-byte dispatch, with `adlc_a_*` replaced by `adlc_b_*`, `reachable_via_b` by `reachable_via_a`, and the side-selector value swaps (`net_num_a` ↔ `net_num_b`) where appropriate. Bridge-protocol dispatch for this side: &80 -> rx_b_handle_80 (&E357) - initial bridge announcement &81 -> rx_b_handle_81 (&E36F) - re-announcement &82 -> rx_b_handle_82 (&E31E) - bridge query (shared &83 path) &83 -> rx_b_handle_83 (&E316) - bridge query, known-station other -> rx_b_forward (&E389) - forward or discard See rx_frame_a for the full per-instruction explanation. |
|
| E263 | .rx_frame_b←1← E07E JMP |
| LDA #1 ; A = &01: mask SR2 bit 0 (AP = Address Present) | |
| E265 | BIT adlc_b_cr2 ; BIT SR2 -- confirm the IRQ was a frame start |
| E268 | BEQ rx_frame_b_bail ; AP not set -> spurious IRQ, return to main_loop |
| E26A | LDA adlc_b_tx ; Read FIFO byte 0: destination station |
| E26D | STA rx_dst_stn ; Stage dst_stn into the rx header buffer |
| E270 | JSR wait_adlc_b_irq ; Block until ADLC B IRQs again (byte 1 ready) |
| E273 | BIT adlc_b_cr2 ; BIT SR2 -- RDA still set for the next byte? |
| E276 | BPL rx_frame_b_bail ; RDA cleared: frame truncated before dst_net, bail |
| E278 | LDY adlc_b_tx ; Read byte 1 into Y: destination network |
| E27B | BEQ rx_b_not_for_us ; dst_net == 0 means 'local net of sender' -- not for us |
| E27D | LDA reachable_via_a,y ; Probe reachable_via_a[dst_net] for a route via side A |
| E280 | BEQ rx_b_not_for_us ; No route -> frame isn't ours to drain, re-listen |
| E282 | STY rx_dst_net ; Commit dst_net now that it has passed filtering |
| E285 | LDY #2 ; Y = 2: resume drain at offset 2 (after header) |
| E287 | .rx_frame_b_drain←1← E29F BCC |
| JSR wait_adlc_b_irq ; Wait for the next FIFO byte IRQ | |
| E28A | BIT adlc_b_cr2 ; BIT SR2 -- RDA still asserted? |
| E28D | BPL rx_frame_b_end ; RDA cleared mid-body -> go to FV check |
| E28F | LDA adlc_b_tx ; Read byte Y of payload from TX/RX FIFO |
| E292 | STA rx_dst_stn,y ; Store into rx_dst_stn+Y (buffer grows into rx_*) |
| E295 | INY ; Advance Y to the next slot |
| E296 | LDA adlc_b_tx ; Read byte Y+1 (pair-read without an IRQ wait) |
| E299 | STA rx_dst_stn,y ; Store the second byte of the pair |
| E29C | INY ; Advance Y past the pair |
| E29D | CPY #&14 ; Cap at 20 bytes (6-byte header + up to 14 payload) |
| E29F | BCC rx_frame_b_drain ; Under cap -> keep draining |
| E2A1 | .rx_frame_b_end←1← E28D BPL |
| LDA #0 ; A = &00: halt ADLC B | |
| E2A3 | STA adlc_b_cr1 ; CR1 = 0: disable TX and RX IRQs |
| E2A6 | LDA #&84 ; A = &84: clear-RX-status + FV-clear bits |
| E2A8 | STA adlc_b_cr2 ; Commit CR2: acknowledge end-of-frame |
| E2AB | LDA #2 ; A = &02: mask SR2 bit 1 (FV: Frame Valid) |
| E2AD | BIT adlc_b_cr2 ; BIT SR2 -- test FV and RDA |
| E2B0 | BEQ rx_frame_b_bail ; FV clear -> frame corrupt or short, bail |
| E2B2 | BPL rx_frame_b_dispatch ; FV set + no RDA -> clean end; go to dispatch |
| E2B4 | LDA adlc_b_tx ; FV + RDA: one trailing byte still in FIFO |
| E2B7 | STA rx_dst_stn,y ; Store the odd trailing byte |
| E2BA | INY ; Advance Y to count that final byte |
| E2BB | BNE rx_frame_b_dispatch ; Unconditional: continue to dispatch |
| E2BD | .rx_frame_b_bail←4← E268 BEQ← E276 BPL← E2B0 BEQ← E2D0 BCC |
| JMP main_loop ; Bail: restart from main_loop (full ADLC re-init) | |
| E2C0 | .rx_b_not_for_us←2← E27B BEQ← E280 BEQ |
| LDA #&a2 ; A = &A2: RX on, IRQ enabled, TX in reset | |
| E2C2 | STA adlc_b_cr1 ; Re-arm ADLC B to listen for the next frame |
| E2C5 | JMP main_loop_poll ; Skip main_loop re-init; go straight back to polling |
| E2C8 | .rx_b_to_forward←2← E2F2 BNE← E301 BNE |
| JMP rx_b_forward ; Out-of-range JMP to rx_b_forward (JSR can't reach &E389) | |
| E2CB | .rx_frame_b_dispatch←2← E2B2 BPL← E2BB BNE |
| STY rx_len ; Save final byte count (even if 0 bytes of payload) | |
| E2CE | CPY #6 ; Compare to 6 -- minimum valid scout header |
| E2D0 | BCC rx_frame_b_bail ; Shorter than header -> bail |
| E2D2 | LDA rx_src_net ; Load src_net from the drained frame |
| E2D5 | BNE rx_b_src_net_resolved ; Non-zero -> sender supplied src_net, keep it |
| E2D7 | LDA net_num_b ; Sender left src_net = 0 ('my local net') |
| E2DA | STA rx_src_net ; ...substitute our own B-side network number |
| E2DD | .rx_b_src_net_resolved←1← E2D5 BNE |
| LDA net_num_a ; Load our A-side network number for comparison | |
| E2E0 | CMP rx_dst_net ; Compare against the incoming dst_net |
| E2E3 | BNE rx_b_broadcast_check ; Not for side A -> skip the local rewrite |
| E2E5 | LDA #0 ; dst_net names our A-side network... |
| E2E7 | STA rx_dst_net ; ...normalise dst_net to 0 (local on A) |
| E2EA | .rx_b_broadcast_check←1← E2E3 BNE |
| LDA rx_dst_stn ; Load dst_stn for the broadcast test | |
| E2ED | AND rx_dst_net ; AND with dst_net (both &FF only if full broadcast) |
| E2F0 | CMP #&ff ; Compare result to &FF |
| E2F2 | BNE rx_b_to_forward ; Not a full broadcast -> forward path |
| E2F4 | JSR adlc_b_listen ; Broadcast: re-arm B's listen mode for any follow-up |
| E2F7 | LDA #&c2 ; A = &C2: reset TX, enable RX |
| E2F9 | STA adlc_b_cr1 ; Commit CR1 while we process the bridge-protocol frame |
| E2FC | LDA rx_port ; Load the port byte from the drained frame |
| E2FF | CMP #&9c ; Compare with &9C (bridge-protocol port) |
| E301 | BNE rx_b_to_forward ; Not our port -> drop into forward path |
| E303 | LDA rx_ctrl ; Load ctrl byte for the per-type dispatch |
| E306 | CMP #&81 ; Test &81 (BridgeReply: re-announcement) |
| E308 | BEQ rx_b_handle_81 ; Match -> rx_b_handle_81 |
| E30A | CMP #&80 ; Test &80 (BridgeReset: initial announcement) |
| E30C | BEQ rx_b_handle_80 ; Match -> rx_b_handle_80 |
| E30E | CMP #&82 ; Test &82 (WhatNet: general query) |
| E310 | BEQ rx_b_handle_82 ; Match -> rx_b_handle_82 |
| E312 | CMP #&83 ; Test &83 (IsNet: targeted query) |
| E314 | BNE rx_b_forward ; Unknown ctrl -> forward path (fall through to rx_b_handle_83 on match) |
| fall through ↓ | |
Side-B IsNet query (ctrl=&83): targeted network lookupMirror of rx_a_handle_83 (&E195) with A/B swapped: consults reachable_via_a (not _b) because the frame arrived on side B. Falls through to rx_b_handle_82 when the queried network is known. |
|
| E316 | .rx_b_handle_83 |
| LDY rx_query_net ; Y = the queried network number | |
| E319 | LDA reachable_via_a,y ; Check if we have a route via the other side |
| E31C | BEQ rx_b_query_done ; Unknown -> silently drop this IsNet query |
| fall through ↓ | |
Side-B WhatNet query (ctrl=&82); also IsNet response pathMirror of rx_a_handle_82 (&E19D) with A/B swapped throughout: stagger seeded from net_num_a, transmit via ADLC B, tx_src_net patched to net_num_a, response-data's first payload byte (at the tx_ctrl slot) encodes net_num_b. See rx_a_handle_82 for the full protocol description. |
|
| E31E | .rx_b_handle_82←1← E310 BEQ |
| JSR adlc_b_listen ; Re-arm ADLC B into listen mode before replying | |
| E321 | JSR build_query_response ; Build reply-scout template addressed at the querier |
| E324 | LDA net_num_a ; Fetch our side-A network number |
| E327 | STA tx_src_net ; Patch src_net so the scout names us by net_num_a |
| E32A | STA ctr24_lo ; Copy it into the stagger-delay counter too |
| E32D | JSR stagger_delay ; Busy-wait for (net_num_a * ~50us) + 160us |
| E330 | JSR wait_adlc_b_idle ; CSMA wait on B |
| E333 | JSR transmit_frame_b ; Transmit the reply scout |
| E336 | JSR handshake_rx_b ; Wait for the querier's scout-ACK on B |
| E339 | JSR build_query_response ; Rebuild template -- next frame is the data response |
| E33C | LDA net_num_a ; Fetch net_num_a |
| E33F | STA tx_src_net ; Re-patch src_net |
| E342 | LDA net_num_b ; Fetch net_num_b |
| E345 | STA tx_ctrl ; Write as data-frame payload byte 0 |
| E348 | LDA rx_query_net ; Fetch the network the querier asked about |
| E34B | STA tx_port ; Write as data-frame payload byte 1 |
| E34E | JSR transmit_frame_b ; Transmit the data frame |
| E351 | JSR handshake_rx_b ; Wait for final data-ACK |
| E354 | .rx_b_query_done←1← E31C BEQ |
| JMP main_loop ; Transaction complete -> back to main_loop | |
Side-B BridgeReset (ctrl=&80): learn topology from scratchMirror of rx_a_handle_80 (&E1D6): wipe reachable_via_* via init_reachable_nets, seed the re-announce timer's high byte from net_num_a (mirror of A-side seeding from net_num_b), set announce_count = 10 and announce_flag = &80 (bit 7 set = next outbound on side B). Falls through to rx_b_handle_81. The other of the two places in the ROM that sets announce_flag non-zero; all other writes to that byte clear it. |
|
| E357 | .rx_b_handle_80←1← E30C BEQ |
| JSR init_reachable_nets ; Wipe all learned routing state (topology reset) | |
| E35A | LDA net_num_a ; Fetch our side-A network number |
| E35D | STA announce_tmr_hi ; Use as re-announce timer high byte (stagger) |
| E360 | LDA #0 ; A = 0: timer low byte |
| E362 | STA announce_tmr_lo ; Store timer_lo; first fire in (net_num_a * 256) ticks |
| E365 | LDA #&0a ; A = 10: number of BridgeReplies to emit |
| E367 | STA announce_count ; Store the burst count |
| E36A | LDA #&80 ; A = &80: enable re-announce, bit 7 set = send via B |
| E36C | STA announce_flag ; Set announce_flag; main loop will now schedule the burst |
| fall through ↓ | |
Side-B BridgeReply (ctrl=&81): learn and re-broadcastMirror of rx_a_handle_81 (&E1EE): reads each payload byte from offset 6 onward as a network number reachable via side B, marks reachable_via_b[x] = &FF for each (mirror of the A-side writing reachable_via_a). Appends net_num_b to the payload and falls through to rx_b_forward for re-broadcast onto side A. |
|
| E36F | .rx_b_handle_81←1← E308 BEQ |
| LDY #6 ; Y = 6: skip past the 6-byte scout header | |
| E371 | .rx_b_learn_loop←1← E37E BNE |
| LDA rx_dst_stn,y ; Fetch next announced network number from payload | |
| E374 | TAX ; X = the network to record |
| E375 | LDA #&ff ; A = &FF: 'route known' marker |
| E377 | STA reachable_via_b,x ; Remember that network X is reachable via side B |
| E37A | INY ; Advance to next payload byte |
| E37B | CPY rx_len ; Have we reached the end of the payload? |
| E37E | BNE rx_b_learn_loop ; No -- keep learning |
| E380 | LDA net_num_b ; Load our own side-B network number |
| E383 | STA rx_dst_stn,y ; Append it to the payload for the onward broadcast |
| E386 | INC rx_len ; Payload grew by one byte; record the new length |
| fall through ↓ | |
Forward a B-side frame to A, completing the 4-way handshakeByte-for-byte mirror of rx_a_forward (&E208) with A and B swapped throughout: the inbound scout is pushed via adlc_a_tx, and the B-A-B tail bridges the four-way handshake the other direction. Reached from rx_b_to_forward (&E2C8), from rx_frame_b's ctrl dispatch fall-through (&E314), and from rx_b_handle_81's fall-through at &E387. See rx_a_forward for the full per-stage explanation. |
|
| E389 | .rx_b_forward←2← E2C8 JMP← E314 BNE |
| LDA rx_len ; Read rx_len into A | |
| E38C | TAX ; Preserve original length in X for odd-parity check |
| E38D | AND #&fe ; Mask low bit to round DOWN to even byte count |
| E38F | STA rx_len ; Store the rounded count for the pair loop |
| E392 | JSR wait_adlc_a_idle ; CSMA wait on A before transmitting the forwarded scout |
| E395 | LDY #0 ; Y = 0: start at byte 0 of the rx_* buffer |
| E397 | .rx_b_forward_pair_loop←1← E3B0 BCC |
| JSR wait_adlc_a_irq ; Wait for ADLC A's TDRA | |
| E39A | BIT adlc_a_cr1 ; BIT SR1 -- V <- bit 6 (TDRA) |
| E39D | BVC rx_b_forward_done ; TDRA clear -> chip lost sync, escape to main_loop |
| E39F | LDA rx_dst_stn,y ; Load byte Y of the received scout |
| E3A2 | STA adlc_a_tx ; Push it to ADLC A's TX FIFO |
| E3A5 | INY ; Advance Y |
| E3A6 | LDA rx_dst_stn,y ; Load byte Y+1 |
| E3A9 | STA adlc_a_tx ; Push the second byte of the pair |
| E3AC | INY ; Advance Y again |
| E3AD | CPY rx_len ; Have we reached the even-rounded length yet? |
| E3B0 | BCC rx_b_forward_pair_loop ; No -> keep looping |
| E3B2 | TXA ; Recover original length from X for parity check |
| E3B3 | ROR ; ROR: carry <- bit 0 (= original length was odd?) |
| E3B4 | BCC rx_b_forward_ack_round ; Even -> skip the trailing-byte path |
| E3B6 | JSR wait_adlc_a_irq ; Odd: wait for TDRA once more for the last byte |
| E3B9 | LDA rx_dst_stn,y ; Load the trailing byte |
| E3BC | STA adlc_a_tx ; Push it to the TX FIFO |
| E3BF | .rx_b_forward_ack_round←1← E3B4 BCC |
| LDA #&3f ; A = &3F: end-of-burst CR2 value | |
| E3C1 | STA adlc_a_cr2 ; Commit CR2 -- ADLC A flushes the scout |
| E3C4 | JSR wait_adlc_a_irq ; Wait for the frame-complete IRQ |
| E3C7 | LDA #&5a ; A = &5A: reset mem_ptr_lo for the handshake stages below |
| E3C9 | STA mem_ptr_lo ; Store mem_ptr_lo |
| E3CB | LDA #4 ; A = 4: reset mem_ptr_hi |
| E3CD | STA mem_ptr_hi ; Store mem_ptr_hi -- handshake_rx_? will write here |
| E3CF | JSR handshake_rx_a ; Stage 2: drain ACK1 from A into &045A... |
| E3D2 | JSR transmit_frame_b ; ...and retransmit it on B so the originator hears its ACK |
| E3D5 | JSR handshake_rx_b ; Stage 3: drain DATA from B into &045A... |
| E3D8 | JSR transmit_frame_a ; ...and retransmit it on A to the destination |
| E3DB | JSR handshake_rx_a ; Stage 4: drain ACK2 from A into &045A... |
| E3DE | JSR transmit_frame_b ; ...and retransmit it on B as the final ACK |
| E3E1 | .rx_b_forward_done←1← E39D BVC |
| JMP main_loop ; 4-way handshake bridged; back to main_loop | |
Wait for ADLC A IRQ (polled)Spin reading SR1 of ADLC A until the IRQ bit (bit 7) is set. Called from 19 sites where the code needs to wait for the ADLC to signal an event (frame complete, RX data available, TX ready, etc.). The Bridge does not route the ADLC ~IRQ output to the 6502 ~IRQ line (that pin is used for the self-test push-button), so ADLC attention is obtained by polling. |
|
| E3E4 | .wait_adlc_a_irq←19← E0EF JSR← E106 JSR← E397 JSR← E3B6 JSR← E3C4 JSR← E3E7 BPL← E523 JSR← E550 JSR← E562 JSR← E575 JSR← E583 JSR← E593 JSR← F125 JSR← F15C JSR← F1DA JSR← F1EA JSR← F20D JSR← F22A JSR← F242 JSR |
| BIT adlc_a_cr1 ; Peek ADLC A status, testing the IRQ-summary bit | |
| E3E7 | BPL wait_adlc_a_irq ; Spin while the chip has nothing to report |
| E3E9 | RTS ; Event pending; return to caller to handle it |
Wait for ADLC B IRQ (polled)As wait_adlc_a_irq but for ADLC B. |
|
| E3EA | .wait_adlc_b_irq←19← E216 JSR← E235 JSR← E243 JSR← E270 JSR← E287 JSR← E3ED BPL← E4CC JSR← E4F9 JSR← E50B JSR← E606 JSR← E614 JSR← E624 JSR← F139 JSR← F149 JSR← F16C JSR← F189 JSR← F1A1 JSR← F1C6 JSR← F1FD JSR |
| BIT adlc_b_cr1 ; Peek ADLC B status, testing the IRQ-summary bit | |
| E3ED | BPL wait_adlc_b_irq ; Spin while the chip has nothing to report |
| E3EF | RTS ; Event pending; return to caller to handle it |
ADLC A full reset, then enter RX listenAborts all ADLC A activity and returns it to idle RX listen mode. Falls through to adlc_a_listen. Called from the reset handler. |
|
| E3F0 | .adlc_a_full_reset←1← E005 JSR |
| LDA #&c1 ; Mask: reset TX and RX, unlock CR3/CR4 via AC=1 | |
| E3F2 | STA adlc_a_cr1 ; Drop ADLC A into full reset |
| E3F5 | LDA #&1e ; Mask: 8-bit RX word length, abort-extend, NRZ |
| E3F7 | STA adlc_a_tx2 ; Program CR4 (reached via tx2 slot while AC=1) |
| E3FA | LDA #0 ; Mask: no loopback, DTR released, NRZ encoding |
| E3FC | STA adlc_a_cr2 ; Program CR3 (reached via cr2 slot while AC=1); fall through |
| fall through ↓ | |
Enter ADLC A RX listen modeTX held in reset, RX active. IRQs are generated internally by the chip but the ~IRQ output is not wired; see wait_adlc_a_irq. |
|
| E3FF | .adlc_a_listen←2← E173 JSR← E19D JSR |
| LDA #&82 ; Mask: keep TX in reset, enable RX IRQs, AC=0 | |
| E401 | STA adlc_a_cr1 ; Commit CR1; subsequent cr2/tx writes hit CR2/TX again |
| E404 | LDA #&67 ; Mask: clear status flags, FC_TDRA, 2/1-byte, PSE |
| E406 | STA adlc_a_cr2 ; Commit CR2; ADLC A now listening for incoming frames |
| E409 | RTS ; Return; Econet side A is idle-listen |
ADLC B full reset, then enter RX listenByte-for-byte mirror of adlc_a_full_reset, targeting ADLC B's register set at &D800-&D803. Falls through to adlc_b_listen. CR3=&00 also puts the LOC/DTR pin high, so the front-panel LED is dark after this runs -- the distinguishing feature from self_test_reset_adlcs. |
|
| E40A | .adlc_b_full_reset←1← E008 JSR |
| LDA #&c1 ; Mask: reset TX and RX, unlock CR3/CR4 via AC=1 | |
| E40C | STA adlc_b_cr1 ; Drop ADLC B into full reset |
| E40F | LDA #&1e ; Mask: 8-bit RX word length, abort-extend, NRZ |
| E411 | STA adlc_b_tx2 ; Program CR4 (reached via tx2 slot while AC=1) |
| E414 | LDA #0 ; Mask: CR3 bit 7 clear -> LOC/DTR high -> status LED OFF |
| E416 | STA adlc_b_cr2 ; Program CR3; fall through into listen mode |
| fall through ↓ | |
Enter ADLC B RX listen modeMirror of adlc_a_listen for ADLC B. |
|
| E419 | .adlc_b_listen←2← E2F4 JSR← E31E JSR |
| LDA #&82 ; Mask: keep TX in reset, enable RX IRQs, AC=0 | |
| E41B | STA adlc_b_cr1 ; Commit CR1; subsequent cr2/tx writes hit CR2/TX again |
| E41E | LDA #&67 ; Mask: clear status flags, FC_TDRA, 2/1-byte, PSE |
| E420 | STA adlc_b_cr2 ; Commit CR2; ADLC B now listening for incoming frames |
| E423 | RTS ; Return; Econet side B is idle-listen |
Reset both routing tables to the directly-attached networksZeroes the two 256-entry routing tables (reachable_via_a at &035A and reachable_via_b at &025A), then writes &FF to four slots that are true by virtue of the Bridge's immediate topology: reachable_via_a[net_num_a] -- side A's own network is reachable
via side A (trivially)
reachable_via_b[net_num_b] -- side B's own network is reachable
via side B (trivially)
reachable_via_a[255] -- broadcast network reachable both
reachable_via_b[255] ways
Everything else starts at zero and is populated later by bridge- protocol announcements learned in the rx handlers (see rx_a_handle_80 / rx_b_handle_80). Called from the reset handler and also re-invoked from the two rx_?_handle_80 paths -- receiving an initial bridge announcement indicates a topology change that invalidates the learned state, so the Bridge forgets everything and starts accumulating again. |
|
| E424 | .init_reachable_nets←3← E002 JSR← E1D6 JSR← E357 JSR |
| LDY #0 ; Y: walks every network number 0..255 | |
| E426 | LDA #0 ; A = 0: 'route not known' marker |
| E428 | .init_reachable_nets_clear←1← E42F BNE |
| STA reachable_via_b,y ; Clear side-A handler's entry for network Y | |
| E42B | STA reachable_via_a,y ; Clear side-B handler's entry for network Y |
| E42E | INY ; Step to next network number |
| E42F | BNE init_reachable_nets_clear ; Loop back until Y wraps through all 256 slots |
| E431 | LDA #&ff ; A = &FF: 'route known' marker for the writes below |
| E433 | LDY net_num_a ; Y = net_num_a: our own side-A network number |
| E436 | STA reachable_via_a,y ; side-B handler can reach net_num_a via side A |
| E439 | LDY net_num_b ; Y = net_num_b: our own side-B network number |
| E43C | STA reachable_via_b,y ; side-A handler can reach net_num_b via side B |
| E43F | LDY #&ff ; Y = 255: the Econet broadcast network |
| E441 | STA reachable_via_b,y ; Broadcasts reachable for side-A handler's traffic |
| E444 | STA reachable_via_a,y ; Broadcasts reachable for side-B handler's traffic |
| E447 | RTS ; Tables primed; return to caller |
Fixed prelude + per-count delay scaled by ctr24_loA calibrated busy-wait used by the query-response paths to stagger their transmissions. Called from rx_a_handle_82 (&E1AC) and rx_b_handle_82 (&E32D), in each case with ctr24_lo pre-loaded with the bridge's opposite-side network number (net_num_b for A-side responses, net_num_a for B-side responses). Two phases: Prelude (~&40 * (dey/bne) cycles): a fixed settling delay, the same regardless of caller. Roughly &40 * 5 = 320 cycles = ~160 us at 2 MHz. Per-count loop (ctr24_lo iterations * (&14 * (dey/bne) + dec/bne) cycles): roughly ctr24_lo * 110 cycles. For a typical network number of ~24, that's ~2600 cycles = ~1.3 ms. For the range of network numbers permitted (1-127), the total delay runs from ~215 us to ~7 ms. This spread means multiple bridges on the same segment responding to a broadcast query (ctrl=&82) transmit their responses at measurably different times, reducing the chance of collisions on the shared medium. Bridges with higher network numbers back off longer -- a cheap deterministic priority scheme that requires no coordination. |
|
| E448 | .stagger_delay←2← E1AC JSR← E32D JSR |
| LDY #&40 ; Y = &40: seed for the fixed-length settling delay | |
| E44A | .stagger_delay_prelude←1← E44B BNE |
| DEY ; Tight DEY/BNE loop -- burns ~160 us regardless of caller | |
| E44B | BNE stagger_delay_prelude ; Spin until the prelude counter hits zero |
| E44D | .stagger_delay_outer←1← E455 BNE |
| LDY #&14 ; Y = &14: seed for one inner-loop iteration | |
| E44F | .stagger_delay_inner←1← E450 BNE |
| DEY ; Tight DEY/BNE -- ~50 us per outer iteration | |
| E450 | BNE stagger_delay_inner ; Spin until the inner counter hits zero |
| E452 | DEC ctr24_lo ; One tick of the caller's network-number count |
| E455 | BNE stagger_delay_outer ; Loop until ctr24_lo reaches zero (net_num_? ticks) |
| E457 | RTS ; Delay complete; return so caller can transmit |
Build a BridgeReset scout carrying net_num_b as payloadPopulates the outbound frame control block at &045A-&0460 with an all-broadcast "BridgeReset" scout (JGH's term) -- ctrl=&80, port=&9C, payload = net_num_b. At reset time this is transmitted via ADLC A first (announcing "network net_num_b is reachable through me" to side A's stations), then tx_data0 is patched to net_num_a and the same frame is re-transmitted via ADLC B. tx_dst_stn = &FF broadcast station tx_dst_net = &FF broadcast network tx_src_stn = &18 firmware marker (see below) tx_src_net = &18 firmware marker (see below) tx_ctrl = &80 initial-announcement ctrl tx_port = &9C bridge-protocol port tx_data0 = net_num_b network number on side B The src_stn/src_net fields are both set to the constant &18. The Bridge has no station number of its own (only network numbers, per the Installation Guide) so these fields are not real addresses. Receivers do not use them for routing -- rx_a_handle_81 reads the payload starting at offset 6 and ignores bytes 2-3 entirely. The most plausible role for &18 is defensive redundancy: together with dst=(&FF,&FF), ctrl=&80/&81 and port=&9C it gives a receiver multiple ways to confirm that a received frame is a well-formed bridge announcement. Also writes &06 to tx_end_lo and &04 to tx_end_hi (so the transmit routine sends bytes &045A..&0460 inclusive = 7 bytes when X=1), loads X=1 (trailing-byte flag for transmit_frame_a), and points mem_ptr at the frame block (&045A). Called from the reset handler at &E038 and again from &E098 (the main-loop periodic re-announce path). A structurally identical cousin builder lives at sub_ce48d (&E48D) and is called from four sites; it populates the same fields with values drawn from RAM variables at rx_src_stn and rx_query_net rather than baked-in constants. |
|
| E458 | .build_announce_b←2← E038 JSR← E098 JSR |
| LDA #&ff ; Broadcast marker &FF for dst station AND network | |
| E45A | STA tx_dst_stn ; Write dst_stn = 255 into the frame header |
| E45D | STA tx_dst_net ; Write dst_net = 255 into the frame header |
| E460 | LDA #&18 ; Firmware marker &18 for src fields (no station id) |
| E462 | STA tx_src_stn ; Write src_stn = &18 |
| E465 | STA tx_src_net ; Write src_net = &18 |
| E468 | LDA #&9c ; Bridge-protocol port number |
| E46A | STA tx_port ; Write port = &9C into the frame header |
| E46D | LDA #&80 ; Control byte: &80 = BridgeReset (initial announcement) |
| E46F | STA tx_ctrl ; Write ctrl = &80 into the frame header |
| E472 | LDA net_num_b ; Payload: our side-B network number to announce |
| E475 | STA tx_data0 ; Write as data byte 0 (trailing byte after header) |
| E478 | LDX #1 ; X = 1: ask transmit_frame_? to send the trailing byte too |
| E47A | LDA #6 ; Low byte of tx-end: &06 == 6 header bytes |
| E47C | STA tx_end_lo ; Store low byte of tx_end |
| E47F | LDA #4 ; High byte of tx-end: &04 matches mem_ptr_hi below |
| E481 | STA tx_end_hi ; Store high byte of tx_end (end pair = &0406) |
| E484 | LDA #&5a ; Low byte of mem_ptr: frame starts at &045A |
| E486 | STA mem_ptr_lo ; Store mem_ptr_lo |
| E488 | LDA #4 ; High byte of mem_ptr: page &04 |
| E48A | STA mem_ptr_hi ; Store mem_ptr_hi (pointer = &045A) |
| E48C | RTS ; Return; caller may now transmit the BridgeReset scout |
Build a reply template for WhatNet/IsNet query responsesA second frame-builder (sibling of build_announce_b) used by the bridge-query response path. Called *twice* per response: once to build the reply scout (ctrl=&80 + reply_port as the port), then after the querier's scout-ACK has been received, called again to rebuild the buffer as a data frame -- the caller then patches bytes 4 and 5 (labelled tx_ctrl and tx_port but genuinely payload in a data frame) with the routing answer. Where build_announce_b writes a broadcast-addressed template, this one builds a unicast reply: tx_dst_stn = rx_src_stn station that sent the query tx_dst_net = 0 local network tx_src_stn = 0 Bridge has no station tx_src_net = 0 (caller patches to net_num_?) tx_ctrl = &80 scout control byte tx_port = rx_query_port response port from byte 12 of query X = 0 no trailing payload Also writes tx_end_lo=&06 / tx_end_hi=&04 and points mem_ptr at &045A so a subsequent transmit_frame_? sends the 6-byte scout. Called from the two query-response paths (&E1A0 and &E1B8 on side A; &E321 and &E339 on side B). Each caller then patches a subset of the fields before calling transmit_frame_? -- the idiomatic second call in particular overwrites tx_ctrl and tx_port to carry the bridge's routing answer. |
|
| E48D | .build_query_response←4← E1A0 JSR← E1B8 JSR← E321 JSR← E339 JSR |
| LDA rx_src_stn ; Load querier's station from the received scout | |
| E490 | STA tx_dst_stn ; Target the reply back at them as dst_stn |
| E493 | LDA #0 ; A = 0: local network marker |
| E495 | STA tx_dst_net ; dst_net = 0: answer on the querier's local net |
| E498 | LDA #0 ; A = 0: Bridge has no station identity |
| E49A | STA tx_src_stn ; src_stn = 0 in the reply (unused by Econet routing) |
| E49D | STA tx_src_net ; src_net = 0 for now (caller patches to net_num_?) |
| E4A0 | LDA #&80 ; ctrl = &80: this is a scout, not a data frame |
| E4A2 | STA tx_ctrl ; Write ctrl into the frame header |
| E4A5 | LDA rx_query_port ; Fetch the reply_port the querier asked for |
| E4A8 | STA tx_port ; Write it as the outbound scout's port |
| E4AB | LDX #0 ; X = 0: transmit_frame_? should send 6 bytes exactly |
| E4AD | LDA #6 ; Low byte of tx_end: 6-byte frame |
| E4AF | STA tx_end_lo ; Store tx_end_lo |
| E4B2 | LDA #4 ; High byte of tx_end: page &04 |
| E4B4 | STA tx_end_hi ; Store tx_end_hi (end pair = &0406) |
| E4B7 | LDA #&5a ; Low byte of mem_ptr: &045A |
| E4B9 | STA mem_ptr_lo ; Store mem_ptr_lo |
| E4BB | LDA #4 ; High byte of mem_ptr: page &04 |
| E4BD | STA mem_ptr_hi ; Store mem_ptr_hi; pointer = &045A |
| E4BF | RTS ; Return; caller patches src_net and ctrl/port as needed |
Send the frame at mem_ptr out through ADLC B's TX FIFOByte-for-byte mirror of transmit_frame_a (&E517) with adlc_a_* replaced by adlc_b_*. Everything there applies here — same entry conditions, same end-pointer semantics (tx_end_lo/hi), same X=0/1 trailing-byte flag, same escape-to-main-loop on unexpected SR1 state, same normal exit that resets mem_ptr to &045A. Called from seven sites: reset (&E04E), &E0D8, &E257, &E333, &E34E, &E3D2, &E3DE. |
|
| E4C0 | .transmit_frame_b←7← E04E JSR← E0D8 JSR← E257 JSR← E333 JSR← E34E JSR← E3D2 JSR← E3DE JSR |
| LDA #&e7 ; A = &E7: prime CR2 for TX (FC_TDRA, 2/1-byte, PSE) | |
| E4C2 | STA adlc_b_cr2 ; Commit CR2 on ADLC B |
| E4C5 | LDA #&44 ; A = &44: arm CR1 for TX (TX on, RX off for now) |
| E4C7 | STA adlc_b_cr1 ; Commit CR1 on ADLC B |
| E4CA | LDY #0 ; Y = 0: byte offset into the frame buffer |
| E4CC | .transmit_frame_b_pair_loop←2← E4EC BNE← E4F3 BCC |
| JSR wait_adlc_b_irq ; Wait for ADLC B IRQ (TDRA = FIFO ready for bytes) | |
| E4CF | BIT adlc_b_cr1 ; BIT SR1 -- V flag <- bit 6 (TDRA) |
| E4D2 | BVS transmit_frame_b_send_pair ; TDRA set -> FIFO has room, send the next pair |
| E4D4 | .transmit_frame_b_escape←1← E4FF BVC |
| PLA ; TDRA clear -> ADLC state bad; drop return address... | |
| E4D5 | PLA ; ...(second PLA completes the drop) |
| E4D6 | JMP main_loop ; ...and escape to main_loop |
| E4D9 | .transmit_frame_b_send_pair←1← E4D2 BVS |
| LDA (mem_ptr_lo),y ; Load frame byte at (mem_ptr),Y | |
| E4DB | STA adlc_b_tx ; Push to ADLC B's TX FIFO |
| E4DE | INY ; Advance Y within page |
| E4DF | LDA (mem_ptr_lo),y ; Load the next frame byte |
| E4E1 | STA adlc_b_tx ; Push the second byte of the pair |
| E4E4 | INY ; Advance Y again |
| E4E5 | BNE transmit_frame_b_end_check ; Non-zero Y -> stay on current page |
| E4E7 | INC mem_ptr_hi ; Y wrapped to zero -> bump mem_ptr to next page |
| E4E9 | .transmit_frame_b_end_check←1← E4E5 BNE |
| CPY tx_end_lo ; Compare Y with tx_end_lo | |
| E4EC | BNE transmit_frame_b_pair_loop ; Still short of end-of-frame low byte -> more to send |
| E4EE | LDA mem_ptr_hi ; Load current mem_ptr_hi |
| E4F0 | CMP tx_end_hi ; Compare with tx_end_hi |
| E4F3 | BCC transmit_frame_b_pair_loop ; Still on a lower page than the end -> more to send |
| E4F5 | TXA ; Recover X (trailing-byte flag) from before the loop |
| E4F6 | ROR ; Rotate bit 0 into carry |
| E4F7 | BCC transmit_frame_b_finish ; X was even -> no trailing byte, skip ahead |
| E4F9 | JSR wait_adlc_b_irq ; X was odd -> wait for TDRA once more |
| E4FC | BIT adlc_b_cr1 ; BIT SR1 to test TDRA again |
| E4FF | BVC transmit_frame_b_escape ; TDRA clear -> escape (mirror of &E4D4) |
| E501 | LDA (mem_ptr_lo),y ; Load the extra trailing byte |
| E503 | STA adlc_b_tx ; Push trailing byte to TX FIFO |
| E506 | .transmit_frame_b_finish←1← E4F7 BCC |
| LDA #&3f ; A = &3F: signal end-of-burst via CR2 | |
| E508 | STA adlc_b_cr2 ; Commit CR2 -- ADLC flushes and flags frame-complete |
| E50B | JSR wait_adlc_b_irq ; Wait for the frame-complete IRQ |
| E50E | LDA #&5a ; A = &5A: reset mem_ptr_lo to &045A base |
| E510 | STA mem_ptr_lo ; Store mem_ptr_lo |
| E512 | LDA #4 ; A = 4: reset mem_ptr_hi to page &04 |
| E514 | STA mem_ptr_hi ; Store mem_ptr_hi -- pointer ready for next builder |
| E516 | RTS ; Return; the frame has left ADLC B |
Send the frame at mem_ptr out through ADLC A's TX FIFOSends the frame starting at mem_ptr (&80/&81 — normally pointing at the outbound control block &045A) through ADLC A's TX FIFO. Termi- nation is controlled by the 16-bit pointer tx_end_lo/tx_end_hi (&0200/&0201): the loop sends byte pairs until mem_ptr + Y reaches or passes (tx_end_hi:tx_end_lo). X is a flag — non-zero means send one extra trailing byte after the terminator (used by builders that append a payload like build_announce_b's net_num_b at &0460). On entry: mem_ptr_lo/hi start address of frame tx_end_lo/hi end address (exclusive pair) X 0 = no trailing byte, 1 = send one trailing byte ADLC A must already be primed by a frame builder On exit (normal RTS): mem_ptr_lo/hi reset to &045A ready for next builder ADLC A's TX FIFO flushed, CR2 = &3F Abnormal exit: if any of the three wait_adlc_a_irq polls returns with SR1's V-bit clear instead of set (meaning the ADLC didn't reach the expected TDRA state), the routine drops the caller's return address from the stack and JMP's into the main loop at &E051 — the same escape-to-main pattern used by wait_adlc_a_idle. Called from seven sites: reset (&E03E), &E0AD, &E1B2, &E1CD, &E251, &E25D, &E3D8. |
|
| E517 | .transmit_frame_a←7← E03E JSR← E0AD JSR← E1B2 JSR← E1CD JSR← E251 JSR← E25D JSR← E3D8 JSR |
| LDA #&e7 ; A = &E7: prime CR2 for TX (FC_TDRA, 2/1-byte, PSE) | |
| E519 | STA adlc_a_cr2 ; Commit CR2 on ADLC A |
| E51C | LDA #&44 ; A = &44: arm CR1 for TX (TX on, RX off for now) |
| E51E | STA adlc_a_cr1 ; Commit CR1 on ADLC A |
| E521 | LDY #0 ; Y = 0: byte offset into the frame buffer |
| E523 | .transmit_frame_a_pair_loop←2← E543 BNE← E54A BCC |
| JSR wait_adlc_a_irq ; Wait for ADLC A IRQ (TDRA = FIFO ready for bytes) | |
| E526 | BIT adlc_a_cr1 ; BIT SR1 -- V flag <- bit 6 (TDRA) |
| E529 | BVS transmit_frame_a_send_pair ; TDRA set -> FIFO has room, send the next pair |
| E52B | .transmit_frame_a_escape←1← E556 BVC |
| PLA ; TDRA clear -> ADLC state bad; drop return address... | |
| E52C | PLA ; ...(second PLA completes the drop) |
| E52D | JMP main_loop ; ...and escape to main_loop |
| E530 | .transmit_frame_a_send_pair←1← E529 BVS |
| LDA (mem_ptr_lo),y ; Load frame byte at (mem_ptr),Y | |
| E532 | STA adlc_a_tx ; Push to ADLC A's TX FIFO |
| E535 | INY ; Advance Y within page |
| E536 | LDA (mem_ptr_lo),y ; Load the next frame byte |
| E538 | STA adlc_a_tx ; Push the second byte of the pair |
| E53B | INY ; Advance Y again |
| E53C | BNE transmit_frame_a_end_check ; Non-zero Y -> stay on current page |
| E53E | INC mem_ptr_hi ; Y wrapped to zero -> bump mem_ptr to next page |
| E540 | .transmit_frame_a_end_check←1← E53C BNE |
| CPY tx_end_lo ; Compare Y with tx_end_lo | |
| E543 | BNE transmit_frame_a_pair_loop ; Still short of end-of-frame low byte -> more to send |
| E545 | LDA mem_ptr_hi ; Load current mem_ptr_hi |
| E547 | CMP tx_end_hi ; Compare with tx_end_hi |
| E54A | BCC transmit_frame_a_pair_loop ; Still on a lower page than the end -> more to send |
| E54C | TXA ; Recover X (trailing-byte flag) from before the loop |
| E54D | ROR ; Rotate bit 0 into carry |
| E54E | BCC transmit_frame_a_finish ; X was even -> no trailing byte, skip ahead |
| E550 | JSR wait_adlc_a_irq ; X was odd -> wait for TDRA once more |
| E553 | BIT adlc_a_cr1 ; BIT SR1 to test TDRA again |
| E556 | BVC transmit_frame_a_escape ; TDRA clear -> escape (mirror of &E52B) |
| E558 | LDA (mem_ptr_lo),y ; Load the extra trailing byte (tx_data0 in announce frames) |
| E55A | STA adlc_a_tx ; Push trailing byte to TX FIFO |
| E55D | .transmit_frame_a_finish←1← E54E BCC |
| LDA #&3f ; A = &3F: signal end-of-burst via CR2 | |
| E55F | STA adlc_a_cr2 ; Commit CR2 -- ADLC flushes and flags frame-complete |
| E562 | JSR wait_adlc_a_irq ; Wait for the frame-complete IRQ |
| E565 | LDA #&5a ; A = &5A: reset mem_ptr_lo to &045A base |
| E567 | STA mem_ptr_lo ; Store mem_ptr_lo |
| E569 | LDA #4 ; A = 4: reset mem_ptr_hi to page &04 |
| E56B | STA mem_ptr_hi ; Store mem_ptr_hi -- pointer ready for next builder |
| E56D | RTS ; Return; the frame has left ADLC A |
Receive a handshake frame on ADLC A and stage it for forwardThe receive half of four-way-handshake bridging for the A side. Enables RX on ADLC A, drains an inbound frame byte-by-byte into the outbound buffer starting at tx_dst_stn (&045A), then sets up tx_end_lo/hi so the next call to transmit_frame_b transmits the just-received frame out of the other port verbatim. The drain is capped at `top_ram_page` (set by the boot RAM test) so very long frames fill available RAM and no further. After the drain, does three pieces of address fix-up on the now-staged frame: * If tx_src_net (byte 3 of the frame) is zero, fill it with
net_num_a. Many Econet senders leave src_net as zero to mean
"my local network"; the Bridge makes that explicit before
forwarding.
* Reject the frame if tx_dst_net is zero (no destination
network declared) or if reachable_via_b has no entry for
that network (we don't know a route).
* If tx_dst_net equals net_num_b (our own B-side network),
normalise it to zero -- from side B's perspective the frame
is now "local".
On any of the "reject" paths above, and on any sub-step that fails (no AP/RDA, no Frame Valid, no response at all), takes the standard escape-to-main-loop exit: PLA/PLA/JMP main_loop. On success, return to the caller with mem_ptr / tx_end_lo / tx_end_hi ready for transmit_frame_b (or transmit_frame_a in the reverse direction for queries). Mirror of handshake_rx_b (&E5FF). Called from five sites: &E1B5 and &E1D0 (rx_a_handle_82/83 query paths), &E254 and &E3DB (forward tails), and &E3CF (also a forward tail). |
|
| E56E | .handshake_rx_a←5← E1B5 JSR← E1D0 JSR← E254 JSR← E3CF JSR← E3DB JSR |
| LDA #&82 ; A = &82: TX in reset, RX IRQs enabled | |
| E570 | STA adlc_a_cr1 ; Re-arm ADLC A for the incoming handshake frame |
| E573 | LDA #1 ; A = &01: SR2 mask for AP (Address Present) |
| E575 | JSR wait_adlc_a_irq ; Block until ADLC A raises its first IRQ |
| E578 | BIT adlc_a_cr2 ; BIT SR2 -- test AP bit against mask in A |
| E57B | BEQ handshake_rx_a_escape ; No AP: nothing arrived, escape to main |
| E57D | LDA adlc_a_tx ; Read byte 0 of the handshake frame (dst_stn) |
| E580 | STA tx_dst_stn ; Stage into tx buffer for onward transmission |
| E583 | JSR wait_adlc_a_irq ; Wait for the second RX IRQ (next byte ready) |
| E586 | BIT adlc_a_cr2 ; BIT SR2 -- RDA (bit 7) still set? |
| E589 | BPL handshake_rx_a_escape ; RDA cleared mid-frame: truncated, escape |
| E58B | LDA adlc_a_tx ; Read byte 1: destination network |
| E58E | STA tx_dst_net ; Stage dst_net into the forward buffer |
| E591 | LDY #2 ; Y = 2: start draining pair-payload into (mem_ptr_lo),Y |
| E593 | .handshake_rx_a_pair_loop←2← E5A7 BNE← E5AF BCC |
| JSR wait_adlc_a_irq ; Wait for the next RX byte | |
| E596 | BIT adlc_a_cr2 ; BIT SR2 -- RDA still asserted? |
| E599 | BPL handshake_rx_a_drained ; End-of-frame detected mid-pair -- jump to FV check |
| E59B | LDA adlc_a_tx ; Read even-indexed payload byte |
| E59E | STA (mem_ptr_lo),y ; Store into (mem_ptr_lo)+Y in the staging buffer |
| E5A0 | INY ; Advance Y to odd slot |
| E5A1 | LDA adlc_a_tx ; Read odd-indexed payload byte (no IRQ wait: paired) |
| E5A4 | STA (mem_ptr_lo),y ; Store into (mem_ptr_lo)+Y |
| E5A6 | INY ; Advance Y; wraps to 0 after 256 bytes |
| E5A7 | BNE handshake_rx_a_pair_loop ; Y didn't wrap -- stay in this page |
| E5A9 | INC mem_ptr_hi ; Y wrapped: advance mem_ptr_hi to next page |
| E5AB | LDA mem_ptr_hi ; Reload new page number for bounds test |
| E5AD | CMP top_ram_page ; Compare against top_ram_page (set by boot RAM test) |
| E5AF | BCC handshake_rx_a_pair_loop ; Still room -- keep draining into the next page |
| E5B1 | .handshake_rx_a_escape←5← E57B BEQ← E589 BPL← E5C5 BEQ← E5E4 BEQ← E5E9 BEQ |
| PLA ; Drop caller's return address (lo) | |
| E5B2 | PLA ; Drop caller's return address (hi) |
| E5B3 | JMP main_loop ; Abandon handshake and rejoin main_loop |
| E5B6 | .handshake_rx_a_drained←1← E599 BPL |
| LDA #0 ; A = &00: halt ADLC A | |
| E5B8 | STA adlc_a_cr1 ; CR1 = 0: disable TX and RX IRQs |
| E5BB | LDA #&84 ; A = &84: clear-RX-status + FV-clear bits |
| E5BD | STA adlc_a_cr2 ; Commit CR2: acknowledge the end-of-frame |
| E5C0 | LDA #2 ; A = &02: mask SR2 bit 1 (Frame Valid) |
| E5C2 | BIT adlc_a_cr2 ; BIT SR2 -- test FV and RDA bits together |
| E5C5 | BEQ handshake_rx_a_escape ; FV clear -> frame was corrupt/short, escape |
| E5C7 | BPL handshake_rx_a_finalise_len ; FV set but no RDA -> clean end, finalise length |
| E5C9 | LDA adlc_a_tx ; FV+RDA both set: one trailing byte still pending |
| E5CC | STA (mem_ptr_lo),y ; Store the odd trailing byte into the staging buffer |
| E5CE | INY ; Advance Y to cover that final byte |
| E5CF | .handshake_rx_a_finalise_len←1← E5C7 BPL |
| TYA ; A = Y (current byte offset in page) | |
| E5D0 | TAX ; X = A: preserve raw length for odd-length callers |
| E5D1 | AND #&fe ; Mask low bit: round length DOWN to even |
| E5D3 | STA tx_end_lo ; Store rounded tx_end_lo |
| E5D6 | LDA tx_src_net ; Load src_net from the just-drained frame |
| E5D9 | BNE handshake_rx_a_route_check ; Non-zero -> sender supplied src_net, keep it |
| E5DB | LDA net_num_a ; Sender left src_net as 0 ('my local net') |
| E5DE | STA tx_src_net ; Substitute our own A-side network number |
| E5E1 | .handshake_rx_a_route_check←1← E5D9 BNE |
| LDY tx_dst_net ; Load dst_net into Y for routing lookup | |
| E5E4 | BEQ handshake_rx_a_escape ; dst_net = 0 (unspecified) -> reject, escape |
| E5E6 | LDA reachable_via_b,y ; Probe reachable_via_b[dst_net] |
| E5E9 | BEQ handshake_rx_a_escape ; No route via side B -> reject, escape |
| E5EB | CPY net_num_b ; Compare dst_net with our B-side net number |
| E5EE | BNE handshake_rx_a_end ; Not us -> leave dst_net as-is, skip the rewrite |
| E5F0 | LDA #0 ; Frame is for the B-side's local network... |
| E5F2 | STA tx_dst_net ; ...normalise dst_net to 0 for the outbound header |
| E5F5 | .handshake_rx_a_end←1← E5EE BNE |
| LDA mem_ptr_hi ; Read final mem_ptr_hi (last page written) | |
| E5F7 | STA tx_end_hi ; Record as tx_end_hi (multi-page frames need this) |
| E5FA | LDA #4 ; A = &04: reset mem_ptr_hi back to &045A page... |
| E5FC | STA mem_ptr_hi ; ...so the transmit path walks the buffer from byte 0 |
| E5FE | RTS ; Return: frame staged, transmitter can send it verbatim |
Receive a handshake frame on ADLC B and stage it for forwardByte-for-byte mirror of handshake_rx_a (&E56E) with adlc_a_* replaced by adlc_b_* and the A/B network-number swaps in the address normalisation: src_net defaults to net_num_b, and the forwardability check is against reachable_via_a. Called from five sites: &E24E, &E25A, &E336, &E351, &E3D5. See handshake_rx_a for the per-instruction explanation. |
|
| E5FF | .handshake_rx_b←5← E24E JSR← E25A JSR← E336 JSR← E351 JSR← E3D5 JSR |
| LDA #&82 ; A = &82: TX in reset, RX IRQs enabled | |
| E601 | STA adlc_b_cr1 ; Re-arm ADLC B for the incoming handshake frame |
| E604 | LDA #1 ; A = &01: SR2 mask for AP (Address Present) |
| E606 | JSR wait_adlc_b_irq ; Block until ADLC B raises its first IRQ |
| E609 | BIT adlc_b_cr2 ; BIT SR2 -- test AP bit against mask in A |
| E60C | BEQ handshake_rx_b_escape ; No AP: nothing arrived, escape to main |
| E60E | LDA adlc_b_tx ; Read byte 0 of the handshake frame (dst_stn) |
| E611 | STA tx_dst_stn ; Stage into tx buffer for onward transmission |
| E614 | JSR wait_adlc_b_irq ; Wait for the second RX IRQ (next byte ready) |
| E617 | BIT adlc_b_cr2 ; BIT SR2 -- RDA (bit 7) still set? |
| E61A | BPL handshake_rx_b_escape ; RDA cleared mid-frame: truncated, escape |
| E61C | LDA adlc_b_tx ; Read byte 1: destination network |
| E61F | STA tx_dst_net ; Stage dst_net into the forward buffer |
| E622 | LDY #2 ; Y = 2: start draining pair-payload into (mem_ptr_lo),Y |
| E624 | .handshake_rx_b_pair_loop←2← E638 BNE← E640 BCC |
| JSR wait_adlc_b_irq ; Wait for the next RX byte | |
| E627 | BIT adlc_b_cr2 ; BIT SR2 -- RDA still asserted? |
| E62A | BPL handshake_rx_b_drained ; End-of-frame detected mid-pair -- jump to FV check |
| E62C | LDA adlc_b_tx ; Read even-indexed payload byte |
| E62F | STA (mem_ptr_lo),y ; Store into (mem_ptr_lo)+Y in the staging buffer |
| E631 | INY ; Advance Y to odd slot |
| E632 | LDA adlc_b_tx ; Read odd-indexed payload byte (no IRQ wait: paired) |
| E635 | STA (mem_ptr_lo),y ; Store into (mem_ptr_lo)+Y |
| E637 | INY ; Advance Y; wraps to 0 after 256 bytes |
| E638 | BNE handshake_rx_b_pair_loop ; Y didn't wrap -- stay in this page |
| E63A | INC mem_ptr_hi ; Y wrapped: advance mem_ptr_hi to next page |
| E63C | LDA mem_ptr_hi ; Reload new page number for bounds test |
| E63E | CMP top_ram_page ; Compare against top_ram_page (set by boot RAM test) |
| E640 | BCC handshake_rx_b_pair_loop ; Still room -- keep draining into the next page |
| E642 | .handshake_rx_b_escape←5← E60C BEQ← E61A BPL← E656 BEQ← E675 BEQ← E67A BEQ |
| PLA ; Drop caller's return address (lo) | |
| E643 | PLA ; Drop caller's return address (hi) |
| E644 | JMP main_loop ; Abandon handshake and rejoin main_loop |
| E647 | .handshake_rx_b_drained←1← E62A BPL |
| LDA #0 ; A = &00: halt ADLC B | |
| E649 | STA adlc_b_cr1 ; CR1 = 0: disable TX and RX IRQs |
| E64C | LDA #&84 ; A = &84: clear-RX-status + FV-clear bits |
| E64E | STA adlc_b_cr2 ; Commit CR2: acknowledge the end-of-frame |
| E651 | LDA #2 ; A = &02: mask SR2 bit 1 (Frame Valid) |
| E653 | BIT adlc_b_cr2 ; BIT SR2 -- test FV and RDA bits together |
| E656 | BEQ handshake_rx_b_escape ; FV clear -> frame was corrupt/short, escape |
| E658 | BPL handshake_rx_b_finalise_len ; FV set but no RDA -> clean end, finalise length |
| E65A | LDA adlc_b_tx ; FV+RDA both set: one trailing byte still pending |
| E65D | STA (mem_ptr_lo),y ; Store the odd trailing byte into the staging buffer |
| E65F | INY ; Advance Y to cover that final byte |
| E660 | .handshake_rx_b_finalise_len←1← E658 BPL |
| TYA ; A = Y (current byte offset in page) | |
| E661 | TAX ; X = A: preserve raw length for odd-length callers |
| E662 | AND #&fe ; Mask low bit: round length DOWN to even |
| E664 | STA tx_end_lo ; Store rounded tx_end_lo |
| E667 | LDA tx_src_net ; Load src_net from the just-drained frame |
| E66A | BNE handshake_rx_b_route_check ; Non-zero -> sender supplied src_net, keep it |
| E66C | LDA net_num_b ; Sender left src_net as 0 ('my local net') |
| E66F | STA tx_src_net ; Substitute our own B-side network number |
| E672 | .handshake_rx_b_route_check←1← E66A BNE |
| LDY tx_dst_net ; Load dst_net into Y for routing lookup | |
| E675 | BEQ handshake_rx_b_escape ; dst_net = 0 (unspecified) -> reject, escape |
| E677 | LDA reachable_via_a,y ; Probe reachable_via_a[dst_net] |
| E67A | BEQ handshake_rx_b_escape ; No route via side A -> reject, escape |
| E67C | CPY net_num_a ; Compare dst_net with our A-side net number |
| E67F | BNE handshake_rx_b_end ; Not us -> leave dst_net as-is, skip the rewrite |
| E681 | LDA #0 ; Frame is for the A-side's local network... |
| E683 | STA tx_dst_net ; ...normalise dst_net to 0 for the outbound header |
| E686 | .handshake_rx_b_end←1← E67F BNE |
| LDA mem_ptr_hi ; Read final mem_ptr_hi (last page written) | |
| E688 | STA tx_end_hi ; Record as tx_end_hi (multi-page frames need this) |
| E68B | LDA #4 ; A = &04: reset mem_ptr_hi back to &045A page... |
| E68D | STA mem_ptr_hi ; ...so the transmit path walks the buffer from byte 0 |
| E68F | RTS ; Return: frame staged, transmitter can send it verbatim |
Wait for ADLC B's line to go idle (CSMA) or escape |
|
| E690 | .wait_adlc_b_idle←4← E04B JSR← E0D5 JSR← E211 JSR← E330 JSR |
| LDA #0 ; A = 0: seed the 24-bit timeout counter | |
| E692 | STA ctr24_lo ; Clear timeout counter low byte |
| E695 | STA ctr24_mid ; Clear timeout counter mid byte |
| E698 | LDA #&fe ; A = &FE: seed for the high byte (~131K iterations) |
| E69A | STA ctr24_hi ; Store timeout high; counter = &00_00_FE counting up |
| E69D | LDA adlc_b_cr2 ; Read SR2 (result discarded; flags irrelevant here) |
| E6A0 | LDY #&e7 ; Y = &E7: CR2 value to arm the chip on Rx-Idle exit |
| E6A2 | .wait_adlc_b_idle_loop←3← E6C2 BNE← E6C7 BNE← E6CC BNE |
| LDA #&67 ; A = &67: standard listen-mode CR2 value | |
| E6A4 | STA adlc_b_cr2 ; Re-prime CR2 -- clears any stale status bits |
| E6A7 | LDA #4 ; A = &04: mask for SR2 bit 2 (Rx Idle / line quiet) |
| E6A9 | BIT adlc_b_cr2 ; Test SR2 bit 2 via BIT |
| E6AC | BNE wait_adlc_b_idle_ready ; Bit set -> line idle; we can transmit (exit) |
| E6AE | LDA adlc_b_cr2 ; Read SR2 into A for the mask test below |
| E6B1 | AND #&81 ; Mask AP (bit 0) + RDA (bit 7) -- someone else talking? |
| E6B3 | BEQ wait_adlc_b_idle_tick ; Neither set -> still quiet-ish, just increment counter |
| E6B5 | LDA #&c2 ; Mask: reset TX, RX active |
| E6B7 | STA adlc_b_cr1 ; Abort our pending TX on ADLC B (yield to other station) |
| E6BA | LDA #&82 ; Mask: TX still reset, RX IRQ enabled |
| E6BC | STA adlc_b_cr1 ; Keep CR1 in TX-reset state for another pass |
| E6BF | .wait_adlc_b_idle_tick←1← E6B3 BEQ |
| INC ctr24_lo ; Bump timeout counter (LSB first) | |
| E6C2 | BNE wait_adlc_b_idle_loop ; Low byte didn't wrap -> keep polling |
| E6C4 | INC ctr24_mid ; Bump mid byte |
| E6C7 | BNE wait_adlc_b_idle_loop ; Mid byte didn't wrap -> keep polling |
| E6C9 | INC ctr24_hi ; Bump high byte |
| E6CC | BNE wait_adlc_b_idle_loop ; High byte didn't wrap -> keep polling |
| E6CE | PLA ; Counter overflowed -- drop caller's return address... |
| E6CF | PLA ; ...(second PLA completes the return-address drop) |
| E6D0 | JMP main_loop ; ...and escape to main_loop without returning |
| E6D3 | .wait_adlc_b_idle_ready←1← E6AC BNE |
| STY adlc_b_cr2 ; STY: arm CR2 with &E7 (from Y) -- TX-ready listen state | |
| E6D6 | LDA #&44 ; Mask: arm CR1 for transmit (TX on, IRQ off) |
| E6D8 | STA adlc_b_cr1 ; Commit CR1; ADLC B ready to send |
| E6DB | RTS ; Normal return: caller transmits the frame |
Wait for ADLC A's line to go idle (CSMA) or escapePre-transmit carrier-sense: polls ADLC A's SR2 until the Rx Idle bit goes high (SR2 bit 2 = 15+ consecutive 1s received, i.e. the line is quiet and it is safe to start a frame). A 24-bit timeout counter at ctr24_lo/mid/hi (&0214-&0216) starts at &00_00_FE and increments LSB-first; overflow takes ~131K iterations, a few seconds at typical bus speeds. Each iteration re-primes CR2 with &67 (clear TX/RX status, FC_TDRA, 2/1-byte, PSE) then reads SR2. Three outcomes: * SR2 bit 2 set (Rx Idle): line is quiet. Arm CR2=&E7 and
CR1=&44, RTS -- caller proceeds to transmit.
* SR2 bit 0 or bit 7 set (AP or RDA): another station is
sending into this ADLC. Back off by cycling CR1 through
&C2 -> &82 (reset TX without touching RX) and keep polling.
The Bridge is not the right place to assert on a busy line.
* Timeout (counter overflows without ever seeing Rx Idle):
PLA/PLA discards the caller's saved return address from the
stack and JMP &E051 escapes into the main Bridge loop. The
code between the caller's JSR and the main loop is skipped
entirely. See docs/analysis/escape-to-main-control-flow.md.
Called from four sites, always immediately before a transmit: reset (&E03B, before transmit_frame_a), &E0AA, &E1AF, &E392. |
|
| E6DC | .wait_adlc_a_idle←4← E03B JSR← E0AA JSR← E1AF JSR← E392 JSR |
| LDA #0 ; A = 0: seed the 24-bit timeout counter | |
| E6DE | STA ctr24_lo ; Clear timeout counter low byte |
| E6E1 | STA ctr24_mid ; Clear timeout counter mid byte |
| E6E4 | LDA #&fe ; A = &FE: seed for the high byte (gives ~131K iterations) |
| E6E6 | STA ctr24_hi ; Store timeout high; counter = &00_00_FE counting up |
| E6E9 | LDA adlc_a_cr2 ; Read SR2 (result discarded; flags irrelevant here) |
| E6EC | LDY #&e7 ; Y = &E7: CR2 value arm the chip with on Rx-Idle exit |
| E6EE | .wait_adlc_a_idle_loop←3← E70E BNE← E713 BNE← E718 BNE |
| LDA #&67 ; A = &67: standard listen-mode CR2 value | |
| E6F0 | STA adlc_a_cr2 ; Re-prime CR2 -- clears any stale status bits |
| E6F3 | LDA #4 ; A = &04: mask for SR2 bit 2 (Rx Idle / line quiet) |
| E6F5 | BIT adlc_a_cr2 ; Test SR2 bit 2 via BIT |
| E6F8 | BNE wait_adlc_a_idle_ready ; Bit set -> line idle; we can transmit (exit) |
| E6FA | LDA adlc_a_cr2 ; Read SR2 into A for the mask test below |
| E6FD | AND #&81 ; Mask AP (bit 0) + RDA (bit 7) -- someone else talking? |
| E6FF | BEQ wait_adlc_a_idle_tick ; Neither set -> still quiet-ish, just increment counter |
| E701 | LDA #&c2 ; Mask: reset TX, RX active |
| E703 | STA adlc_a_cr1 ; Abort our pending TX on ADLC A (yield to the other station) |
| E706 | LDA #&82 ; Mask: TX still reset, RX IRQ enabled |
| E708 | STA adlc_a_cr1 ; Keep CR1 in TX-reset state for another pass |
| E70B | .wait_adlc_a_idle_tick←1← E6FF BEQ |
| INC ctr24_lo ; Bump timeout counter (LSB first) | |
| E70E | BNE wait_adlc_a_idle_loop ; Low byte didn't wrap -> keep polling |
| E710 | INC ctr24_mid ; Bump mid byte |
| E713 | BNE wait_adlc_a_idle_loop ; Mid byte didn't wrap -> keep polling |
| E715 | INC ctr24_hi ; Bump high byte |
| E718 | BNE wait_adlc_a_idle_loop ; High byte didn't wrap -> keep polling |
| E71A | PLA ; Counter overflowed -- drop caller's return address... |
| E71B | PLA ; ...(second PLA completes the return-address drop) |
| E71C | JMP main_loop ; ...and escape to main_loop without returning |
| E71F | .wait_adlc_a_idle_ready←1← E6F8 BNE |
| STY adlc_a_cr2 ; STY: arm CR2 with &E7 (from Y) -- TX-ready listen state | |
| E722 | LDA #&44 ; Mask: arm CR1 for transmit (TX on, IRQ off) |
| E724 | STA adlc_a_cr1 ; Commit CR1; ADLC A ready to send |
| E727 | RTS ; Normal return: caller transmits the frame |
| E728 | FILL 2264 × &FF |
Self-test entry (IRQ/BRK vector target)Invoked by pressing the self-test push-button on the 6502 ~IRQ line (and, implicitly, by any BRK instruction in the ROM). Runs through a sequence of hardware checks, signalling any failure via self_test_fail at &F2C7 with an error code in A. Not to be pressed while the Bridge is connected to a live network: the self-test reconfigures the ADLCs and drives their control registers in ways that will disturb any in-flight frames. Typical usage is with a loopback cable between the two Econet ports. |
|
| F000 | .self_test |
| SEI ; Mask IRQs -- this routine polls and must not re-enter | |
| F001 | LDA #0 ; A = 0: initial value for the scratch pass-phase flag |
| F003 | STA l0003 ; &03 = pass-phase; toggled by self_test_pass_done |
| fall through ↓ | |
Reset both ADLCs and light the status LEDByte-for-byte identical to the adlc_*_full_reset pair except for one crucial detail: CR3 is programmed to &80 (bit 7 set) instead of &00. CR3 bit 7 is the MC6854's LOC/DTR control bit — but the pin it drives is inverted: when the control bit is HIGH, the pin output goes LOW. On ADLC B (IC18) that pin sinks the low side of the front-panel status LED (which has its high side tied through a resistor to Vcc), so CR3 bit 7 = 1 pulls current through the LED and lights it. ADLC A's LOC/DTR pin is not wired and gets the same write for code symmetry only. Re-entered at &F26C after certain test paths need to reset the chips again; the LED stays lit until a normal reset runs adlc_b_full_reset and clears CR3. |
|
| F005 | .self_test_reset_adlcs←1← F26C JMP |
| LDA #&c1 ; Mask: reset TX+RX, AC=1 to reach CR3/CR4 | |
| F007 | STA adlc_a_cr1 ; Drop ADLC A into full reset |
| F00A | STA adlc_b_cr1 ; Drop ADLC B into full reset |
| F00D | LDA #&1e ; Mask: 8-bit RX, abort-extend, NRZ encoding |
| F00F | STA adlc_a_tx2 ; Program ADLC A's CR4 (via tx2 while AC=1) |
| F012 | STA adlc_b_tx2 ; Program ADLC B's CR4 |
| F015 | LDA #&80 ; Mask &80: CR3 bit 7 = light the LED via LOC/DTR |
| F017 | STA adlc_a_cr2 ; Program ADLC A's CR3 (pin not wired; no effect) |
| F01A | LDA #&80 ; Mask &80 again (separate load for symmetry) |
| F01C | STA adlc_b_cr2 ; Program ADLC B's CR3 -- lights the status LED |
| F01F | LDA #&82 ; Mask: TX in reset, RX IRQ enabled, AC=0 |
| F021 | STA adlc_a_cr1 ; Release CR1 AC bit on ADLC A (CR3 value sticks) |
| F024 | STA adlc_b_cr1 ; Release CR1 AC bit on ADLC B (CR3 value sticks) |
| F027 | LDA #&67 ; Mask: clear status, FC_TDRA, 2/1-byte, PSE |
| F029 | STA adlc_a_cr2 ; Commit CR2 on ADLC A |
| F02C | STA adlc_b_cr2 ; Commit CR2 on ADLC B; falls through to ZP test |
| fall through ↓ | |
Zero-page integrity test (&00-&02)Writes &55 to &00, &01, &02 and reads them back; then &AA and reads back. Failure jumps to self_test_fail with A=1. Tests only the three ZP bytes that are used as scratch by the later self-test stages (ROM checksum, RAM scan). A full ZP test isn't needed — the main reset handler has already exercised ZP indirectly via the RAM test. |
|
| F02F | .self_test_zp←1← F289 JMP |
| LDA #&55 ; First test pattern = &55 (0101_0101) | |
| F031 | .self_test_zp_write_read←1← F049 JMP |
| STA l0000 ; Write pattern to scratch byte &00 | |
| F033 | STA l0001 ; Write pattern to scratch byte &01 |
| F035 | STA l0002 ; Write pattern to scratch byte &02 |
| F037 | CMP l0000 ; Check &00 still reads as pattern |
| F039 | BNE self_test_ram_fail_jump ; Mismatch -> ram_test_fail (distinct blink pattern) |
| F03B | CMP l0001 ; Check &01 still reads as pattern |
| F03D | BNE self_test_ram_fail_jump ; Mismatch -> ram_test_fail |
| F03F | CMP l0002 ; Check &02 still reads as pattern |
| F041 | BNE self_test_ram_fail_jump ; Mismatch -> ram_test_fail |
| F043 | CMP #&aa ; Was the pattern &AA? then both halves passed |
| F045 | BEQ self_test_rom_checksum ; Yes -> continue to ROM checksum |
| F047 | LDA #&aa ; Second test pattern = &AA (1010_1010) |
| F049 | JMP self_test_zp_write_read ; Loop back to rerun the three-byte check |
ROM checksumSums every byte of the 8 KiB ROM modulo 256 using a running A accumulator. Expected total is &55; on mismatch, jumps to self_test_fail with A=2. Runtime pointer in &00/&01 starts at &E000; &02 holds the page counter (32 pages = 8 KiB). |
|
| F04C | .self_test_rom_checksum←1← F045 BEQ |
| LDA #0 ; A = 0: low byte of the ROM pointer | |
| F04E | STA l0000 ; Store pointer_lo = 0 |
| F050 | LDA #&20 ; A = &20: 32 pages remaining to sum |
| F052 | STA l0002 ; Store page counter |
| F054 | LDA #&e0 ; A = &E0: pointer_hi starts at ROM base &E000 |
| F056 | STA l0001 ; Store pointer_hi = &E0 |
| F058 | LDY #0 ; Y = 0: within-page byte offset |
| F05A | TYA ; A = 0: seed the running sum |
| F05B | .self_test_rom_checksum_loop←2← F05F BNE← F065 BNE |
| CLC ; Clear carry before the addition | |
| F05C | ADC (l0000),y ; Add next ROM byte at (pointer),Y into running sum |
| F05E | INY ; Advance to next byte within the page |
| F05F | BNE self_test_rom_checksum_loop ; Loop 256 times through the current page |
| F061 | INC l0001 ; Roll the pointer to the next 256-byte page |
| F063 | DEC l0002 ; One page done; decrement the page counter |
| F065 | BNE self_test_rom_checksum_loop ; Loop until all 32 ROM pages have been summed |
| F067 | CMP #&55 ; Compare running sum with the expected &55 |
| F069 | BEQ self_test_ram_pattern ; Match -> ROM is intact, proceed to RAM test |
| F06B | LDA #2 ; Mismatch: load error code 2 (ROM checksum fail) |
| F06D | JMP self_test_fail ; Jump to the countable-blink failure handler |
RAM pattern test: write &55/&AA to every byte, verifyStarting at address &0004 (skipping the three zero-page bytes reserved for the self-test workspace at &00/&01/&02), iterates through the full 8 KiB of RAM and checks that each byte can store both &55 and &AA. Pointer in (&00,&01) = &0000, Y starts at 4 and wraps, page count in &02 = &20 (32 pages = 8 KiB). On mismatch, jumps to ram_test_fail at &F28C (note: a *different* failure handler from self_test_fail, because a broken RAM cannot use the normal blink-code loop which needs RAM workspace). |
|
| F070 | .self_test_ram_pattern←1← F069 BEQ |
| LDA #0 ; A = 0: low byte of the RAM-test indirect pointer | |
| F072 | STA l0000 ; Store pointer_lo |
| F074 | LDA #0 ; A = 0: high byte -- start scanning at RAM base |
| F076 | STA l0001 ; Store pointer_hi |
| F078 | LDA #&20 ; A = &20: 32 pages to cover (the full 8 KiB) |
| F07A | STA l0002 ; Store page counter |
| F07C | LDY #4 ; Y = 4: skip &0000-&0003 (self-test scratch) |
| F07E | .self_test_ram_pattern_loop←2← F093 BNE← F099 BNE |
| LDA #&55 ; First pattern = &55 (alternating 1-0 nibbles) | |
| F080 | STA (l0000),y ; Write pattern to the current RAM byte |
| F082 | LDA (l0000),y ; Read the same byte back |
| F084 | CMP #&55 ; Verify the cell held the written pattern |
| F086 | BNE self_test_ram_fail_jump ; Mismatch -> ram_test_fail (unreliable storage) |
| F088 | LDA #&aa ; Second pattern = &AA (the bitwise complement) |
| F08A | STA (l0000),y ; Write complement to catch stuck-bit faults |
| F08C | LDA (l0000),y ; Read it back |
| F08E | CMP #&aa ; Verify |
| F090 | BNE self_test_ram_fail_jump ; Mismatch -> ram_test_fail |
| F092 | INY ; Advance to next byte within the page |
| F093 | BNE self_test_ram_pattern_loop ; Loop 256 times through the current page |
| F095 | INC l0001 ; Advance to the next page |
| F097 | DEC l0002 ; One page done; decrement the remaining-page count |
| F099 | BNE self_test_ram_pattern_loop ; Continue until all 32 pages verified |
| F09B | BEQ self_test_ram_incr ; All 8 KiB good -- fall through to the incrementing test |
| F09D | .self_test_ram_fail_jump←6← F039 BNE← F03D BNE← F041 BNE← F086 BNE← F090 BNE← F0C9 BNE |
| JMP ram_test_fail ; Any RAM check mismatch lands here; forward to blinker | |
RAM incrementing-pattern test: fill with X, read backSecond RAM test. Fills the whole 8 KiB with an incrementing byte pattern (X register cycles through 0..&FF and then reinitialised each page with a different offset, giving a distinctive pattern across the RAM that catches address-line faults). Then reads back and verifies. Catches failures that a plain &55/&AA pattern would miss: particularly address-line shorts, where writing to (say) &0410 and &0420 would land at the same cell and produce the same bytes under a uniform pattern but different bytes under this one. On mismatch, jumps to ram_test_fail at &F28C. |
|
| F0A0 | .self_test_ram_incr←1← F09B BEQ |
| LDA #0 ; A = 0: low byte of the pointer stays zero | |
| F0A2 | STA l0001 ; Reset pointer_hi to RAM base for the fill phase |
| F0A4 | LDA #&20 ; A = &20: full 32-page coverage again |
| F0A6 | STA l0002 ; Store the page counter |
| F0A8 | LDY #4 ; Y = 4: skip the self-test scratch bytes |
| F0AA | LDX #0 ; X = 0: seed the fill value |
| F0AC | .self_test_ram_incr_fill←2← F0B1 BNE← F0B8 BNE |
| TXA ; A = X: the current fill value | |
| F0AD | STA (l0000),y ; Write it to RAM via the indirect pointer |
| F0AF | INX ; Increment fill value (wraps naturally at 256) |
| F0B0 | INY ; Advance to next byte in the page |
| F0B1 | BNE self_test_ram_incr_fill ; Loop 256 times through the page |
| F0B3 | INC l0001 ; Advance to next page |
| F0B5 | INX ; Bump fill value by one extra per page -- different offset |
| F0B6 | DEC l0002 ; Decrement page counter |
| F0B8 | BNE self_test_ram_incr_fill ; Continue filling all 32 pages |
| F0BA | LDA #0 ; Fill done; now reset state for the verify phase |
| F0BC | STA l0001 ; pointer_hi back to RAM base |
| F0BE | LDA #&20 ; A = &20: 32 pages again |
| F0C0 | STA l0002 ; Store page counter |
| F0C2 | LDY #4 ; Y = 4: skip scratch bytes |
| F0C4 | LDX #0 ; X = 0: expected value follows the same sequence |
| F0C6 | .self_test_ram_incr_verify←2← F0CD BNE← F0D4 BNE |
| TXA ; A = X: expected byte value | |
| F0C7 | CMP (l0000),y ; Compare with what we actually wrote and read back |
| F0C9 | BNE self_test_ram_fail_jump ; Mismatch -> ram_test_fail (via &F09D) |
| F0CB | INX ; Step expected value |
| F0CC | INY ; Step byte offset |
| F0CD | BNE self_test_ram_incr_verify ; Loop through the page |
| F0CF | INC l0001 ; Advance to next page |
| F0D1 | INX ; Bump offset between pages (match fill pattern) |
| F0D2 | DEC l0002 ; One page verified; decrement |
| F0D4 | BNE self_test_ram_incr_verify ; Continue through all 32 pages; falls through on success |
| fall through ↓ | |
Verify both ADLCs' register state after resetChecks that both ADLCs show the expected register state after self_test_reset_adlcs has configured them. Tests specific bits of SR1 and SR2 on each chip (ADLC A bits from &C800/&C801, ADLC B bits from &D800/&D801). Failure paths: Code 3 (at &F107): ADLC A register-state mismatch Code 4 (at &F102): ADLC B register-state mismatch |
|
| F0D6 | .self_test_adlc_state |
| LDA #&10 ; Mask bit 4 (CTS bit of SR1): expect 1 after reset | |
| F0D8 | BIT adlc_a_cr1 ; Test on ADLC A |
| F0DB | BEQ self_test_fail_adlc_a ; CTS clear -> ADLC A misconfigured (fail code 3) |
| F0DD | LDA #4 ; Mask bit 2 (OVRN bit of SR2): expect 1 (idle, no OVRN) |
| F0DF | BIT adlc_a_cr2 ; Test on ADLC A |
| F0E2 | BEQ self_test_fail_adlc_a ; Bit clear -> unexpected state, fail |
| F0E4 | LDA #&20 ; Mask bit 5 (DCD of SR2): expect 0 (no carrier) |
| F0E6 | BIT adlc_a_cr2 ; Test on ADLC A |
| F0E9 | BNE self_test_fail_adlc_a ; Bit set -> unexpected carrier; fail code 3 |
| F0EB | LDA #&10 ; Same CTS check for ADLC B |
| F0ED | BIT adlc_b_cr1 ; Test on ADLC B |
| F0F0 | BEQ self_test_fail_adlc_b ; Clear -> fail code 4 |
| F0F2 | LDA #4 ; Same OVRN check for ADLC B |
| F0F4 | BIT adlc_b_cr2 ; Test on ADLC B |
| F0F7 | BEQ self_test_fail_adlc_b ; Clear -> fail code 4 |
| F0F9 | LDA #&20 ; Same DCD check for ADLC B |
| F0FB | BIT adlc_b_cr2 ; Test on ADLC B |
| F0FE | BEQ self_test_loopback_a_to_b ; Clear -> all checks passed, proceed to loopback test |
| F100 | .self_test_fail_adlc_b←2← F0F0 BEQ← F0F7 BEQ |
| LDA #4 ; Fail code 4: ADLC B register state wrong | |
| F102 | JMP self_test_fail ; Jump to countable-blink failure handler |
| F105 | .self_test_fail_adlc_a←3← F0DB BEQ← F0E2 BEQ← F0E9 BNE |
| LDA #3 ; Fail code 3: ADLC A register state wrong | |
| F107 | JMP self_test_fail ; Jump to countable-blink failure handler |
Loopback test: transmit on ADLC A, receive on ADLC BAssumes a loopback cable is connected between the two Econet ports. Reconfigures ADLC A for transmit (CR1=&44) and ADLC B for receive (CR1=&82), then sends a 256-byte sequence (0,1,2,...,255) out of A and verifies each byte is received on B in order by incrementing X alongside the sender's Y. Four phases: 1. Pre-fill the A TX FIFO with bytes 0-7 (Y=0..7) while B is still settling -- priming the pipeline before any RX checks begin. 2. Wait for B's first RX IRQ, verify AP, read and match bytes 0 and 1. This is the special "opening" case because the AP/RDA transitions happen on the first two bytes only. 3. Streaming loop: repeatedly send a pair via A, read a pair via B, compare against X (increments in lockstep), and loop until Y wraps to 0 (256 bytes sent). 4. Program CR2=&3F on A to flush the final byte with an end-of-frame marker. Drain the remaining bytes on B (another 255 iterations to empty B's FIFO), then wait for the Frame Valid bit to confirm a clean end-of-frame. Every mismatch or missing status bit jumps to the shared fail target at &F151 which loads code 5 and hands off to self_test_fail. Falls through to self_test_loopback_b_to_a on success. |
|
| F10A | .self_test_loopback_a_to_b←1← F0FE BEQ |
| LDA #&c0 ; A = &C0: ADLC full reset | |
| F10C | STA adlc_a_cr1 ; Reset ADLC A |
| F10F | STA adlc_b_cr1 ; Reset ADLC B |
| F112 | LDA #&82 ; A = &82: CR1 for receive (TX reset, RX IRQ enabled) |
| F114 | STA adlc_b_cr1 ; B becomes the receiver |
| F117 | LDA #&e7 ; A = &E7: CR2 for active TX (listen + IRQs armed) |
| F119 | STA adlc_a_cr2 ; Program CR2 on ADLC A |
| F11C | LDA #&44 ; A = &44: CR1 for active TX (TX on, IRQ off) |
| F11E | STA adlc_a_cr1 ; A becomes the transmitter |
| F121 | LDY #0 ; Y = 0: outbound byte counter / data value |
| F123 | LDX #0 ; X = 0: expected RX byte on B |
| F125 | .loopback_a_to_b_prefill←1← F137 BNE |
| JSR wait_adlc_a_irq ; Wait for A's TDRA IRQ | |
| F128 | BIT adlc_a_cr1 ; BIT SR1 (read CR1 addr) -- test V = TDRA (bit 6) |
| F12B | BVC loopback_a_to_b_fail ; Not TDRA -> A's TX stalled; fail |
| F12D | STY adlc_a_tx ; Push Y into A's TX FIFO (even byte of pair) |
| F130 | INY ; Advance Y |
| F131 | STY adlc_a_tx ; Push Y into A's TX FIFO (odd byte of pair) |
| F134 | INY ; Advance Y past the pair |
| F135 | CPY #8 ; Pre-filled 8 bytes yet? |
| F137 | BNE loopback_a_to_b_prefill ; Keep prefilling |
| F139 | JSR wait_adlc_b_irq ; Wait for B's first RX IRQ |
| F13C | LDA #1 ; A = &01: SR2 mask for AP (Address Present) |
| F13E | BIT adlc_b_cr2 ; BIT SR2 -- first byte should assert AP |
| F141 | BEQ loopback_a_to_b_fail ; No AP on first byte -> fail |
| F143 | CPX adlc_b_tx ; Compare B's FIFO byte against X (expect 0) |
| F146 | BNE loopback_a_to_b_fail ; Mismatch -> fail |
| F148 | INX ; Advance X past the first byte |
| F149 | JSR wait_adlc_b_irq ; Wait for B's next RX IRQ |
| F14C | BIT adlc_b_cr2 ; BIT SR2 -- RDA (bit 7) asserted? |
| F14F | BMI loopback_a_to_b_head_ok ; RDA set -> good, compare second byte |
| F151 | .loopback_a_to_b_fail←12← F12B BVC← F141 BEQ← F146 BNE← F159 BNE← F162 BVC← F172 BPL← F177 BNE← F17D BNE← F18F BPL← F194 BNE← F19A BNE← F1A9 BEQ |
| LDA #5 ; A = 5: error code for A-to-B loopback failure | |
| F153 | JMP self_test_fail ; Hand off to countable-blink failure handler |
| F156 | .loopback_a_to_b_head_ok←1← F14F BMI |
| CPX adlc_b_tx ; Compare B's second FIFO byte against X (expect 1) | |
| F159 | BNE loopback_a_to_b_fail ; Mismatch -> fail |
| F15B | INX ; Advance X past the second byte |
| F15C | .loopback_a_to_b_stream_loop←1← F182 BNE |
| JSR wait_adlc_a_irq ; Wait for A's TDRA IRQ (TX slot ready) | |
| F15F | BIT adlc_a_cr1 ; BIT SR1 -- test V = TDRA |
| F162 | BVC loopback_a_to_b_fail ; TX stalled mid-stream -> fail |
| F164 | STY adlc_a_tx ; Push even byte (Y) into A's TX FIFO |
| F167 | INY ; Advance Y |
| F168 | STY adlc_a_tx ; Push odd byte (Y) into A's TX FIFO |
| F16B | INY ; Advance Y past the pair |
| F16C | JSR wait_adlc_b_irq ; Wait for B's RX IRQ (pair received) |
| F16F | BIT adlc_b_cr2 ; BIT SR2 -- RDA still asserted? |
| F172 | BPL loopback_a_to_b_fail ; RDA cleared early -> fail |
| F174 | CPX adlc_b_tx ; Compare B's even byte against X |
| F177 | BNE loopback_a_to_b_fail ; Mismatch -> fail |
| F179 | INX ; Advance X |
| F17A | CPX adlc_b_tx ; Compare B's odd byte against X |
| F17D | BNE loopback_a_to_b_fail ; Mismatch -> fail |
| F17F | INX ; Advance X past the pair |
| F180 | CPY #0 ; Y wrapped back to 0 -> all 256 bytes sent |
| F182 | BNE loopback_a_to_b_stream_loop ; Not done -> keep streaming |
| F184 | LDA #&3f ; A = &3F: CR2 end-of-frame-with-flush |
| F186 | STA adlc_a_cr2 ; Commit: A pushes the final byte and closes the frame |
| F189 | .loopback_a_to_b_flush_loop←1← F19F BNE |
| JSR wait_adlc_b_irq ; Wait for B's remaining RX IRQ | |
| F18C | BIT adlc_b_cr2 ; BIT SR2 -- RDA still asserted? |
| F18F | BPL loopback_a_to_b_fail ; Drain interrupted -> fail |
| F191 | CPX adlc_b_tx ; Compare B's residual byte against X |
| F194 | BNE loopback_a_to_b_fail ; Mismatch -> fail |
| F196 | INX ; Advance X |
| F197 | CPX adlc_b_tx ; Compare B's next residual byte against X |
| F19A | BNE loopback_a_to_b_fail ; Mismatch -> fail |
| F19C | INX ; Advance X past the pair |
| F19D | CPX #0 ; X wrapped to 0 -> B has drained all 256 bytes |
| F19F | BNE loopback_a_to_b_flush_loop ; Not done -> keep draining |
| F1A1 | JSR wait_adlc_b_irq ; Wait for the trailing end-of-frame IRQ on B |
| F1A4 | LDA #2 ; A = &02: SR2 mask for FV (Frame Valid) |
| F1A6 | BIT adlc_b_cr2 ; BIT SR2 -- confirm FV is set |
| F1A9 | BEQ loopback_a_to_b_fail ; FV missing -> malformed frame, fail |
| fall through ↓ | |
Loopback test: transmit on ADLC B, receive on ADLC AMirror of self_test_loopback_a_to_b with adlc_a_* and adlc_b_* swapped: ADLC B becomes transmitter (CR1=&44), ADLC A the receiver (CR1=&82), and the same 256-byte sequence is sent and verified. Fail target loads code 6 (instead of 5). See self_test_loopback_a_to_b for the four-phase breakdown. |
|
| F1AB | .self_test_loopback_b_to_a |
| LDA #&c0 ; A = &C0: ADLC full reset | |
| F1AD | STA adlc_a_cr1 ; Reset ADLC A |
| F1B0 | STA adlc_b_cr1 ; Reset ADLC B |
| F1B3 | LDA #&82 ; A = &82: CR1 for receive (TX reset, RX IRQ enabled) |
| F1B5 | STA adlc_a_cr1 ; A becomes the receiver |
| F1B8 | LDA #&e7 ; A = &E7: CR2 for active TX (listen + IRQs armed) |
| F1BA | STA adlc_b_cr2 ; Program CR2 on ADLC B |
| F1BD | LDA #&44 ; A = &44: CR1 for active TX (TX on, IRQ off) |
| F1BF | STA adlc_b_cr1 ; B becomes the transmitter |
| F1C2 | LDY #0 ; Y = 0: outbound byte counter / data value |
| F1C4 | LDX #0 ; X = 0: expected RX byte on A |
| F1C6 | .loopback_b_to_a_prefill←1← F1D8 BNE |
| JSR wait_adlc_b_irq ; Wait for B's TDRA IRQ | |
| F1C9 | BIT adlc_b_cr1 ; BIT SR1 (read CR1 addr) -- test V = TDRA (bit 6) |
| F1CC | BVC loopback_b_to_a_fail ; Not TDRA -> B's TX stalled; fail |
| F1CE | STY adlc_b_tx ; Push Y into B's TX FIFO (even byte of pair) |
| F1D1 | INY ; Advance Y |
| F1D2 | STY adlc_b_tx ; Push Y into B's TX FIFO (odd byte of pair) |
| F1D5 | INY ; Advance Y past the pair |
| F1D6 | CPY #8 ; Pre-filled 8 bytes yet? |
| F1D8 | BNE loopback_b_to_a_prefill ; Keep prefilling |
| F1DA | JSR wait_adlc_a_irq ; Wait for A's first RX IRQ |
| F1DD | LDA #1 ; A = &01: SR2 mask for AP (Address Present) |
| F1DF | BIT adlc_a_cr2 ; BIT SR2 -- first byte should assert AP |
| F1E2 | BEQ loopback_b_to_a_fail ; No AP on first byte -> fail |
| F1E4 | CPX adlc_a_tx ; Compare A's FIFO byte against X (expect 0) |
| F1E7 | BNE loopback_b_to_a_fail ; Mismatch -> fail |
| F1E9 | INX ; Advance X past the first byte |
| F1EA | JSR wait_adlc_a_irq ; Wait for A's next RX IRQ |
| F1ED | BIT adlc_a_cr2 ; BIT SR2 -- RDA (bit 7) asserted? |
| F1F0 | BMI loopback_b_to_a_head_ok ; RDA set -> good, compare second byte |
| F1F2 | .loopback_b_to_a_fail←12← F1CC BVC← F1E2 BEQ← F1E7 BNE← F1FA BNE← F203 BVC← F213 BPL← F218 BNE← F21E BNE← F230 BPL← F235 BNE← F23B BNE← F24A BEQ |
| LDA #6 ; A = 6: error code for B-to-A loopback failure | |
| F1F4 | JMP self_test_fail ; Hand off to countable-blink failure handler |
| F1F7 | .loopback_b_to_a_head_ok←1← F1F0 BMI |
| CPX adlc_a_tx ; Compare A's second FIFO byte against X (expect 1) | |
| F1FA | BNE loopback_b_to_a_fail ; Mismatch -> fail |
| F1FC | INX ; Advance X past the second byte |
| F1FD | .loopback_b_to_a_stream_loop←1← F223 BNE |
| JSR wait_adlc_b_irq ; Wait for B's TDRA IRQ (TX slot ready) | |
| F200 | BIT adlc_b_cr1 ; BIT SR1 -- test V = TDRA |
| F203 | BVC loopback_b_to_a_fail ; TX stalled mid-stream -> fail |
| F205 | STY adlc_b_tx ; Push even byte (Y) into B's TX FIFO |
| F208 | INY ; Advance Y |
| F209 | STY adlc_b_tx ; Push odd byte (Y) into B's TX FIFO |
| F20C | INY ; Advance Y past the pair |
| F20D | JSR wait_adlc_a_irq ; Wait for A's RX IRQ (pair received) |
| F210 | BIT adlc_a_cr2 ; BIT SR2 -- RDA still asserted? |
| F213 | BPL loopback_b_to_a_fail ; RDA cleared early -> fail |
| F215 | CPX adlc_a_tx ; Compare A's even byte against X |
| F218 | BNE loopback_b_to_a_fail ; Mismatch -> fail |
| F21A | INX ; Advance X |
| F21B | CPX adlc_a_tx ; Compare A's odd byte against X |
| F21E | BNE loopback_b_to_a_fail ; Mismatch -> fail |
| F220 | INX ; Advance X past the pair |
| F221 | CPY #0 ; Y wrapped back to 0 -> all 256 bytes sent |
| F223 | BNE loopback_b_to_a_stream_loop ; Not done -> keep streaming |
| F225 | LDA #&3f ; A = &3F: CR2 end-of-frame-with-flush |
| F227 | STA adlc_b_cr2 ; Commit: B pushes the final byte and closes the frame |
| F22A | .loopback_b_to_a_flush_loop←1← F240 BNE |
| JSR wait_adlc_a_irq ; Wait for A's remaining RX IRQ | |
| F22D | BIT adlc_a_cr2 ; BIT SR2 -- RDA still asserted? |
| F230 | BPL loopback_b_to_a_fail ; Drain interrupted -> fail |
| F232 | CPX adlc_a_tx ; Compare A's residual byte against X |
| F235 | BNE loopback_b_to_a_fail ; Mismatch -> fail |
| F237 | INX ; Advance X |
| F238 | CPX adlc_a_tx ; Compare A's next residual byte against X |
| F23B | BNE loopback_b_to_a_fail ; Mismatch -> fail |
| F23D | INX ; Advance X past the pair |
| F23E | CPX #0 ; X wrapped to 0 -> A has drained all 256 bytes |
| F240 | BNE loopback_b_to_a_flush_loop ; Not done -> keep draining |
| F242 | JSR wait_adlc_a_irq ; Wait for the trailing end-of-frame IRQ on A |
| F245 | LDA #2 ; A = &02: SR2 mask for FV (Frame Valid) |
| F247 | BIT adlc_a_cr2 ; BIT SR2 -- confirm FV is set |
| F24A | BEQ loopback_b_to_a_fail ; FV missing -> malformed frame, fail |
| fall through ↓ | |
Verify jumper-set network numbers match self-test expectationsChecks that net_num_a == 1 and net_num_b == 2. The self-test presumes a standard loopback-test configuration: the jumpers on the bridge board should be set for 1 and 2 respectively before the self-test button is pressed, so that the network numbers are predictable and the loopback tests can complete without colliding with anything else a tester might leave plugged in. Failure paths: Code 7 at &F255: net_num_a != 1 Code 8 at &F261: net_num_b != 2 |
|
| F24C | .self_test_check_netnums |
| LDA net_num_a ; Fetch the side-A jumper setting | |
| F24F | CMP #1 ; Expected self-test value = 1 |
| F251 | BEQ self_test_check_netnum_b ; Match -> move on to check side B |
| F253 | LDA #7 ; Mismatch: load error code 7 |
| F255 | JMP self_test_fail ; Jump to countable-blink failure handler |
| F258 | .self_test_check_netnum_b←1← F251 BEQ |
| LDA net_num_b ; Fetch the side-B jumper setting | |
| F25B | CMP #2 ; Expected self-test value = 2 |
| F25D | BEQ self_test_pass_done ; Match -> end-of-pass bookkeeping |
| F25F | LDA #8 ; Mismatch: load error code 8 |
| F261 | JMP self_test_fail ; Jump to countable-blink failure handler |
End-of-pass: toggle scratch flag and loop for another passReached when every test in a pass has succeeded. The self-test doesn't stop -- it loops indefinitely until reset. Toggles bit 7 of &0003 (the self-test scratch byte) via EOR #&FF; if bit 7 is set after the toggle, JMPs to self_test_reset_adlcs for another full pass. Otherwise falls through to a slower test variant that resets ADLCs differently before re-entering the ZP test. Two-pass structure lets the operator see continuous LED activity (via the self-test ADLC reset's CR3=&80) for as long as the test is running, with minor variation between passes catching some intermittent faults. |
|
| F264 | .self_test_pass_done←1← F25D BEQ |
| LDA l0003 ; Read the pass-phase flag at &03 | |
| F266 | EOR #&ff ; Invert it so we alternate between passes |
| F268 | STA l0003 ; Store the flipped phase back |
| F26A | BMI self_test_alt_pass ; If bit 7 set, start a full self_test_reset_adlcs pass |
| F26C | JMP self_test_reset_adlcs ; Jump up to redo from the top |
| F26F | .self_test_alt_pass←1← F26A BMI |
| LDA #&c1 ; Alt-pass: full reset first but CR3=&00 only on A | |
| F271 | STA adlc_a_cr1 ; ADLC A CR1 = &C1 (reset + AC=1) |
| F274 | LDA #0 ; A = 0: CR3=&00 for A (LED state unchanged on B) |
| F276 | STA adlc_a_cr2 ; Program CR3 on A only this pass |
| F279 | LDA #&82 ; Mask: back to normal listen-mode CR1 |
| F27B | STA adlc_a_cr1 ; Commit CR1 on ADLC A |
| F27E | STA adlc_b_cr1 ; Commit CR1 on ADLC B |
| F281 | LDA #&67 ; Mask: standard listen-mode CR2 |
| F283 | STA adlc_a_cr2 ; Commit CR2 on ADLC A |
| F286 | STA adlc_b_cr2 ; Commit CR2 on ADLC B |
| F289 | JMP self_test_zp ; Enter the ZP test again (skip the ADLC reset) |
RAM-failure blink pattern (does not use RAM)Reached from any of the three RAM tests on failure -- ZP test, pattern RAM test, incrementing RAM test. This handler can't use RAM for counting blinks (if RAM is broken, reading/writing RAM is exactly what's untrustworthy), so it generates its blink pattern from ROM-based DEC abs,X instructions that exercise the CPU for timing without touching RAM. Sets CR1=1 (AC=1) so writes to adlc_a_cr2 target CR3. Alternates CR3 between &00 (LED off) and &80 (LED on) in an infinite loop paced by DEX/DEY delays and by seven DEC instructions that read-modify-write (but actually just read, since writes to ROM are ignored) bytes in the ROM starting at the reset vector. Continues forever; the operator infers "the RAM is bad" from the fact that the LED is blinking but no specific error code can be counted out -- distinct from the more structured blink patterns produced by self_test_fail with codes 2-8. |
|
| F28C | .ram_test_fail←1← F09D JMP |
| LDX #1 ; CR1 = 1: enable AC so cr2 writes hit CR3 | |
| F28E | STX adlc_a_cr1 ; Commit CR1 on ADLC A |
| F291 | .ram_test_fail_loop←1← F2C4 JMP |
| LDX #0 ; CR3 = 0 -> LED off on ADLC B (LOC/DTR pin high) | |
| F293 | STX adlc_a_cr2 ; Commit CR3 |
| F296 | LDX #0 ; X = 0: inner delay counter |
| F298 | LDY #0 ; Y = 0: outer delay counter |
| F29A | .ram_test_fail_short_delay←2← F29B BNE← F29E BNE |
| DEX ; Pure-register busy-wait (no RAM access) | |
| F29B | BNE ram_test_fail_short_delay ; Spin through X's 256 values |
| F29D | DEY ; Bump Y |
| F29E | BNE ram_test_fail_short_delay ; Spin through Y's 256 values |
| F2A0 | LDX #&80 ; CR3 = &80 -> LED on (LOC/DTR pin driven low) |
| F2A2 | STX adlc_a_cr2 ; Commit CR3 |
| F2A5 | LDY #0 ; Y = 0 for the longer delay phase |
| F2A7 | LDX #0 ; X = 0 |
| F2A9 | .ram_test_fail_long_delay←2← F2BF BNE← F2C2 BNE |
| DEC reset,x ; DEC of ROM (writes ignored); seven of them in a row... | |
| F2AC | DEC reset,x ; ...pace the LED-on interval without RAM writes |
| F2AF | DEC reset,x ; (all seven DECs hit the same RO address) |
| F2B2 | DEC reset,x ; DEC reset,X again -- 4 cycles, no side effect |
| F2B5 | DEC reset,x ; DEC reset,X again -- 4 cycles, no side effect |
| F2B8 | DEC reset,x ; DEC reset,X again -- 4 cycles, no side effect |
| F2BB | DEC reset,x ; Last of the seven; together they lengthen the inner tick |
| F2BE | DEX ; Step X |
| F2BF | BNE ram_test_fail_long_delay ; Spin through X's 256 values |
| F2C1 | DEY ; Step Y |
| F2C2 | BNE ram_test_fail_long_delay ; Spin through Y's 256 values |
| F2C4 | JMP ram_test_fail_loop ; Loop forever; LED alternates at an uncountable pace |
Self-test failure — signal error code via the LEDCommon failure exit for every non-RAM self-test stage. Called with the error code in A. Saves two copies of the code in &00/&01 then enters an infinite loop that blinks the LED (via CR3 bit 7 on ADLC B, which is the pin that drives the front-panel LED) a count of times equal to the error code, separated by longer gaps. Error code table: 2 ROM checksum mismatch (self_test_rom_checksum at &F04C) 3 ADLC A register state wrong (self_test_adlc_state, &F107) 4 ADLC B register state wrong (self_test_adlc_state, &F102) 5 A-to-B loopback fail (self_test_loopback_a_to_b, &F153) 6 B-to-A loopback fail (self_test_loopback_b_to_a, &F1F4) 7 net_num_a != 1 (self_test_check_netnums, &F255) 8 net_num_b != 2 (self_test_check_netnums, &F261) (Code 1 is not used: the zero-page integrity test's failure path routes to ram_test_fail via cf09d, not here, because any failure of the first three RAM tests means normal counting loops can't be trusted. ram_test_fail at &F28C uses a distinct ROM-only blink instead.) Blink pattern: CR1=1 sets the ADLC's AC bit so writes to CR2's address hit CR3. The handler alternates CR3=&00 (LED off) and CR3=&80 (LED on) N times, where N = error code held in &01, with delay loops between each pulse. After each N-pulse burst, a fixed 8-pulse spacer pattern runs before the outer loop repeats. The operator counts pulses to identify the failed test. |
|
| F2C7 | .self_test_fail←7← F06D JMP← F102 JMP← F107 JMP← F153 JMP← F1F4 JMP← F255 JMP← F261 JMP |
| STA l0000 ; Save error code to &00 (the restart value) | |
| F2C9 | STA l0001 ; ...and to &01 (the per-burst countdown) |
| F2CB | LDX #1 ; X = 1: enable AC on ADLC A |
| F2CD | STX adlc_a_cr1 ; Commit CR1 so cr2 writes hit CR3 from here on |
| F2D0 | .self_test_fail_pulse←2← F2F0 BNE← F308 JMP |
| LDX #0 ; X = 0: CR3 off -> LED dark | |
| F2D2 | STX adlc_a_cr2 ; Commit CR3 = 0 |
| F2D5 | LDY #0 ; Y = 0: outer loop counter for the dark phase |
| F2D7 | LDX #0 ; X = 0: inner loop counter |
| F2D9 | .self_test_fail_dark_delay←2← F2DA BNE← F2DD BNE |
| DEX ; DEX -- tick the inner counter | |
| F2DA | BNE self_test_fail_dark_delay ; Inner spin through X's 256 values |
| F2DC | DEY ; Step Y |
| F2DD | BNE self_test_fail_dark_delay ; Outer spin: Y cycles give ~65K iterations of dark |
| F2DF | LDX #&80 ; X = &80: CR3 bit 7 set -> LED lit |
| F2E1 | STX adlc_a_cr2 ; Commit CR3 = &80 |
| F2E4 | LDY #0 ; Y = 0 |
| F2E6 | LDX #0 ; X = 0 |
| F2E8 | .self_test_fail_lit_delay←2← F2E9 BNE← F2EC BNE |
| DEX ; DEX -- tick the inner counter | |
| F2E9 | BNE self_test_fail_lit_delay ; Inner spin through X's 256 values (LED lit) |
| F2EB | DEY ; Step Y |
| F2EC | BNE self_test_fail_lit_delay ; Outer spin: Y cycles give the same length as the dark phase |
| F2EE | DEC l0001 ; One pulse done; decrement the burst counter |
| F2F0 | BNE self_test_fail_pulse ; Loop until we've emitted N pulses |
| F2F2 | LDA #8 ; A = 8: spacer count between bursts |
| F2F4 | STA l0001 ; Seed the spacer loop counter |
| F2F6 | LDY #0 ; Y = 0 |
| F2F8 | LDX #0 ; X = 0 |
| F2FA | .self_test_fail_spacer_delay←3← F2FB BNE← F2FE BNE← F302 BNE |
| DEX ; DEX -- tick the inner spacer counter | |
| F2FB | BNE self_test_fail_spacer_delay ; Inner spin through X's 256 values (LED off) |
| F2FD | DEY ; Step Y |
| F2FE | BNE self_test_fail_spacer_delay ; Outer spin: 8x this pair keeps the gap audibly long |
| F300 | DEC l0001 ; Decrement spacer loop counter |
| F302 | BNE self_test_fail_spacer_delay ; Repeat eight times total |
| F304 | LDA l0000 ; Reload the N-pulse counter with the saved error code |
| F306 | STA l0001 ; Store into &01 for the next burst |
| F308 | JMP self_test_fail_pulse ; Jump back to start another N-pulse burst forever |
| F30B | FILL 3301 × &FF |
| ; Checksum-tuning byte: balances the ROM sum to &55 | |
| FFF0 | .rom_checksum_adjust |
| EQUB &46 | |
| FFF1 | FILL 9 × &FF |
| FFFA | EQUW &FFFF ; NMI vector |
| FFFC | EQUW reset ; RESET vector |
| FFFE | EQUW self_test ; IRQ/BRK vector |
