Acorn Econet Bridge 1

Updated 18 Apr 2026

← All Acorn Econet Bridge versions

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 RAM

Probes 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 sides

Second 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-announce

The 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 burst

Reached 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 A

Reached 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 lookup

Called 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 path

Called 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 scratch

Called 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-broadcast

Reached 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 handshake

Entry 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 B

Byte-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 lookup

Mirror 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 path

Mirror 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 scratch

Mirror 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-broadcast

Mirror 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 handshake

Byte-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 listen

Aborts 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 mode

TX 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 listen

Byte-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 mode

Mirror 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 networks

Zeroes 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_lo

A 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 payload

Populates 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 responses

A 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 FIFO

Byte-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 FIFO

Sends 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 forward

The 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 forward

Byte-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

Byte-for-byte mirror of wait_adlc_a_idle (&E6DC) with adlc_a_* replaced by adlc_b_*. Same pre-transmit carrier-sense semantics: wait for SR2 bit 2 (Rx Idle), back off on AP/RDA, escape to main loop on ~131K-iteration timeout.

Called from four sites: reset (&E04B), &E0D5, &E211, &E330.

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 escape

Pre-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 LED

Byte-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 checksum

Sums 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, verify

Starting 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 back

Second 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 reset

Checks 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 B

Assumes 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 A

Mirror 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 expectations

Checks 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 pass

Reached 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 LED

Common 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