Acorn NFS 3.34B

Updated 31 Mar 2026

← All Acorn NFS and Acorn ANFS versions

; Sideways ROM header
; NFS ROM 3.34B disassembly (Acorn Econet filing system)
; =====================================================
8000 .rom_header←2← 048B LDA← 9BB8 JSR
.language_entry←2← 048B LDA← 9BB8 JSR
.pydis_start←2← 048B LDA← 9BB8 JSR
JMP language_handler ; JMP language_handler
8003 .service_entry←1← 049A LDY
JMP service_handler ; JMP service_handler
8006 .rom_type←1← 0482 AND
EQUB &82
8007 .copyright_offset←1← 0487 LDX
EQUB copyright - rom_header
8008 .binary_version←2← 81A3 CMP← 81AC LDA
EQUB &03
8009 .title
EQUS "NET"
800C .copyright
EQUB &00
; The 'ROFF' suffix at copyright_string+3 is reused by
; the *ROFF command matcher (svc_4_star_command) — a
; space-saving trick that shares ROM bytes between the
; copyright string and the star command table.
800D .copyright_string
EQUS "(C)ROFF"
; Error message offset table (9 entries).
; Each byte is a Y offset into error_msg_table.
; Entry 0 (Y=0, "Line Jammed") doubles as the
; copyright string null terminator.
; Indexed by TXCB status (AND #7), or hardcoded 8.
8014 .error_offsets←1← 842E LDY
EQUB &00 ; "Line Jammed"
8015 EQUB &0D ; "Net Error"
8016 EQUB &18 ; "Not listening"
8017 EQUB &27 ; "No Clock"
8018 EQUB &31 ; "Bad Txcb"
8019 EQUB &3B ; "Escape"
801A EQUB &3B ; "Escape"
801B EQUB &43 ; "Bad Option"
801C EQUB &4F ; "No reply"
; Four bytes with unknown purpose.
801D EQUB &01 ; Purpose unknown
801E EQUB &00 ; Purpose unknown
801F EQUB &3B ; Purpose unknown
8020 EQUB &03 ; Purpose unknown
; Dispatch table: low bytes of (handler_address - 1)
; Each entry stores the low byte of a handler address minus 1,
; for use with the PHA/PHA/RTS dispatch trick at &809F.
; See dispatch_0_hi (&8045) for the corresponding high bytes.
; Five callers share this table via different Y base offsets:
; Y=&00 Service calls 0-12 (indices 1-13)
; Y=&0D Language entry reasons (indices 14-18)
; Y=&12 FSCV codes 0-7 (indices 19-26)
; Y=&16 FS reply handlers (indices 27-32)
; Y=&20 *NET1-4 sub-commands (indices 33-36)
8021 .dispatch_0_lo
EQUB <(return_2-1) ; lo - Svc 0: already claimed (no-op)
8022 EQUB <(svc_1_abs_workspace-1) ; lo - Svc 1: absolute workspace
8023 EQUB <(svc_2_private_workspace-1) ; lo - Svc 2: private workspace
8024 EQUB <(svc_3_autoboot-1) ; lo - Svc 3: auto-boot
8025 EQUB <(svc_4_star_command-1) ; lo - Svc 4: unrecognised star command
8026 EQUB <(svc_5_unknown_irq-1) ; lo - Svc 5: unrecognised interrupt
8027 EQUB <(return_2-1) ; lo - Svc 6: BRK (no-op)
8028 EQUB <(dispatch_net_cmd-1) ; lo - Svc 7: unrecognised OSBYTE
8029 EQUB <(svc_8_osword-1) ; lo - Svc 8: unrecognised OSWORD
802A EQUB <(svc_9_help-1) ; lo - Svc 9: *HELP
802B EQUB <(return_2-1) ; lo - Svc 10: static workspace (no-op)
802C EQUB <(svc_11_nmi_claim-1) ; lo - Svc 11: NMI release (reclaim NMIs)
802D EQUB <(svc_12_nmi_release-1) ; lo - Svc 12: NMI claim (save NMI state)
802E EQUB <(lang_0_insert_remote_key-1) ; lo - Lang 0: no language / Tube
802F EQUB <(lang_1_remote_boot-1) ; lo - Lang 1: normal startup
8030 EQUB <(lang_2_save_palette_vdu-1) ; lo - Lang 2: softkey byte (Electron)
8031 EQUB <(lang_3_execute_at_0100-1) ; lo - Lang 3: softkey length (Electron)
8032 EQUB <(lang_4_remote_validated-1) ; lo - Lang 4: remote validated
8033 EQUB <(fscv_0_opt-1) ; lo - FSCV 0: *OPT
8034 EQUB <(fscv_1_eof-1) ; lo - FSCV 1: EOF check
8035 EQUB <(fscv_3_star_cmd-1) ; lo - FSCV 2: */ (run)
8036 EQUB <(fscv_3_star_cmd-1) ; lo - FSCV 3: unrecognised star command
8037 EQUB <(fscv_3_star_cmd-1) ; lo - FSCV 4: *RUN
8038 EQUB <(fscv_5_cat-1) ; lo - FSCV 5: *CAT
8039 EQUB <(fscv_6_shutdown-1) ; lo - FSCV 6: shutdown
803A EQUB <(fscv_7_read_handles-1) ; lo - FSCV 7: read handle range
803B EQUB <(fsreply_0_print_dir-1) ; lo - FS reply: print directory name
803C EQUB <(fsreply_1_copy_handles_boot-1) ; lo - FS reply: copy handles + boot
803D EQUB <(fsreply_2_copy_handles-1) ; lo - FS reply: copy handles
803E EQUB <(fsreply_3_set_csd-1) ; lo - FS reply: set CSD handle
803F EQUB <(fsreply_4_notify_exec-1) ; lo - FS reply: notify + execute
8040 EQUB <(fsreply_5_set_lib-1) ; lo - FS reply: set library handle
8041 EQUB <(net_1_read_handle-1) ; lo - *NET1: read handle from packet
8042 EQUB <(net_2_read_handle_entry-1) ; lo - *NET2: read handle from workspace
8043 EQUB <(net_3_close_handle-1) ; lo - *NET3: close handle
8044 EQUB <(net_4_resume_remote-1) ; lo - *NET4: resume remote
; Dispatch table: high bytes of (handler_address - 1)
; Paired with dispatch_0_lo (&8021). Together they form a table
; of 37 handler addresses, used via the PHA/PHA/RTS trick at
; &809F.
8045 .dispatch_0_hi
EQUB >(return_2-1) ; hi - Svc 0: already claimed (no-op)
8046 EQUB >(svc_1_abs_workspace-1) ; hi - Svc 1: absolute workspace
8047 EQUB >(svc_2_private_workspace-1) ; hi - Svc 2: private workspace
8048 EQUB >(svc_3_autoboot-1) ; hi - Svc 3: auto-boot
8049 EQUB >(svc_4_star_command-1) ; hi - Svc 4: unrecognised star command
804A EQUB >(svc_5_unknown_irq-1) ; hi - Svc 5: unrecognised interrupt
804B EQUB >(return_2-1) ; hi - Svc 6: BRK (no-op)
804C EQUB >(dispatch_net_cmd-1) ; hi - Svc 7: unrecognised OSBYTE
804D EQUB >(svc_8_osword-1) ; hi - Svc 8: unrecognised OSWORD
804E EQUB >(svc_9_help-1) ; hi - Svc 9: *HELP
804F EQUB >(return_2-1) ; hi - Svc 10: static workspace (no-op)
8050 EQUB >(svc_11_nmi_claim-1) ; hi - Svc 11: NMI release (reclaim NMIs)
8051 EQUB >(svc_12_nmi_release-1) ; hi - Svc 12: NMI claim (save NMI state)
8052 EQUB >(lang_0_insert_remote_key-1) ; hi - Lang 0: no language / Tube
8053 EQUB >(lang_1_remote_boot-1) ; hi - Lang 1: normal startup
8054 EQUB >(lang_2_save_palette_vdu-1) ; hi - Lang 2: softkey byte (Electron)
8055 EQUB >(lang_3_execute_at_0100-1) ; hi - Lang 3: softkey length (Electron)
8056 EQUB >(lang_4_remote_validated-1) ; hi - Lang 4: remote validated
8057 EQUB >(fscv_0_opt-1) ; hi - FSCV 0: *OPT
8058 EQUB >(fscv_1_eof-1) ; hi - FSCV 1: EOF check
8059 EQUB >(fscv_3_star_cmd-1) ; hi - FSCV 2: */ (run)
805A EQUB >(fscv_3_star_cmd-1) ; hi - FSCV 3: unrecognised star command
805B EQUB >(fscv_3_star_cmd-1) ; hi - FSCV 4: *RUN
805C EQUB >(fscv_5_cat-1) ; hi - FSCV 5: *CAT
805D EQUB >(fscv_6_shutdown-1) ; hi - FSCV 6: shutdown
805E EQUB >(fscv_7_read_handles-1) ; hi - FSCV 7: read handle range
805F EQUB >(fsreply_0_print_dir-1) ; hi - FS reply: print directory name
8060 EQUB >(fsreply_1_copy_handles_boot-1) ; hi - FS reply: copy handles + boot
8061 EQUB >(fsreply_2_copy_handles-1) ; hi - FS reply: copy handles
8062 EQUB >(fsreply_3_set_csd-1) ; hi - FS reply: set CSD handle
8063 EQUB >(fsreply_4_notify_exec-1) ; hi - FS reply: notify + execute
8064 EQUB >(fsreply_5_set_lib-1) ; hi - FS reply: set library handle
8065 EQUB >(net_1_read_handle-1) ; hi - *NET1: read handle from packet
8066 EQUB >(net_2_read_handle_entry-1) ; hi - *NET2: read handle from workspace
8067 EQUB >(net_3_close_handle-1) ; hi - *NET3: close handle
8068 EQUB >(net_4_resume_remote-1) ; hi - *NET4: resume remote

*NET command dispatcher

Parses the character after *NET as '1'-'4', maps to table indices 33-36 via base offset Y=&20, and dispatches via &809F. Characters outside '1'-'4' fall through to return_1 (RTS).

These are internal sub-commands used only by the ROM itself, not user-accessible star commands. The MOS command parser requires a space or terminator after 'NET', so *NET1 typed at the command line does not match; these are reached only via OSCLI calls within the ROM.

*NET1 (&8DB0): read file handle from received packet (net_1_read_handle)

*NET2 (&8DCA): read handle entry from workspace (net_2_read_handle_entry)

*NET3 (&8DE0): close handle / mark as unused (net_3_close_handle)

*NET4 (&8DF3): resume after remote operation (net_4_resume_remote)

8069 .dispatch_net_cmd
LDA osbyte_a_copy ; Read command character following *NET
806B SBC #&31 ; Subtract ASCII '1' to get 0-based command index
806D BMI return_1 ; Negative: not a net command, exit
806F CMP #4 ; Command index >= 4: invalid *NET sub-command
8071 BCS return_1 ; Out of range: return via c80e3/RTS
8073 TAX ; X = command index (0-3)
8074 TYA ; Transfer Y to A for dispatch
8075 LDY #&20 ; Y=&20: base offset for *NET commands (index 33+)
8077 BNE dispatch ; ALWAYS branch

Forward unrecognised * command to fileserver (COMERR)

Copies command text from (fs_crc_lo) to &0F05+ via copy_filename, prepares an FS command with function code 0, and sends it to the fileserver to request decoding. The server returns a command code indicating what action to take (e.g. code 4=INFO, 7=DIR, 9=LIB, 5=load-as-command). This mechanism allows the fileserver to extend the client's command set without ROM updates. Called from the "I." and catch-all entries in the command match table at &8BD7, and from FSCV 2/3/4 indirectly. If CSD handle is zero (not logged in), returns without sending.

8079 .forward_star_cmd←1← 8D1D JMP
JSR copy_filename ; Copy command text to FS buffer
807C TAY ; Y=function code for HDRFN
807D .prepare_cmd_dispatch
JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8080 LDX fs_cmd_csd ; X=depends on function
8083 BEQ return_1 ; CSD handle zero: not logged in
8085 LDA fs_cmd_data ; A=function code (0-7)
8088 LDY #&16 ; Y=depends on function
808A BNE dispatch ; ALWAYS branch

FSCV dispatch entry

Entered via the extended vector table when the MOS calls FSCV. Stores A/X/Y via save_fscv_args, compares A (function code) against 8, and dispatches codes 0-7 via the shared dispatch table at &8020 with base offset Y=&12 (table indices 19-26). Function codes: 0=*OPT, 1=EOF, 2=*/, 3=unrecognised *, 4=*RUN, 5=*CAT, 6=shutdown, 7=read handles.

On EntryAfunction code (0-7)
Xdepends on function
Ydepends on function
On ExitAdepends on handler (preserved if A >= 8)
Xdepends on handler (preserved if A >= 8)
Ydepends on handler (preserved if A >= 8)
808C .fscv_handler
JSR save_fscv_args ; Store A/X/Y in FS workspace
808F CMP #8 ; Function code >= 8? Return (unsupported)
8091 BCS return_1 ; Function code >= 8? Return (unsupported)
8093 TAX ; X = function code for dispatch
8094 TYA ; Save Y (command text ptr hi)
8095 LDY #&12 ; Y=&12: base offset for FSCV dispatch (indices 19+)
8097 BNE dispatch ; ALWAYS branch

Language entry dispatcher

Called when the NFS ROM is entered as a language. Although rom_type (&82) does not set the language bit, the MOS enters this point after NFS claims service &FE (Tube post-init). X = reason code (0-4). Dispatches via table indices 14-18 (base offset Y=&0D).

8099 .language_handler←1← 8000 JMP
.lang_entry_dispatch←1← 8000 JMP
CPX #5 ; X >= 5: invalid reason code, return
809B .svc_dispatch_range
BCS return_1 ; Out of range: return via RTS
809D LDY #&0d ; Y=&0D: base offset for language handlers (index 14+)
fall through ↓

PHA/PHA/RTS computed dispatch

X = command index within caller's group (e.g. service number) Y = base offset into dispatch table (0, &0D, &20, etc.) The loop adds Y+1 to X, so final X = command index + base + 1. Then high and low bytes of (handler-1) are pushed onto the stack, and RTS pops them and jumps to handler_address.

This is a standard 6502 trick: RTS increments the popped address by 1 before jumping, so the table stores (address - 1) to compensate. Multiple callers share one table via different Y base offsets.

809F .dispatch←5← 8077 BNE← 808A BNE← 8097 BNE← 80A1 BPL← 8139 JSR
INX ; Add base offset Y to index X (loop: X += Y+1)
80A0 DEY ; Decrement base offset counter
80A1 BPL dispatch ; Loop until Y exhausted
80A3 TAY ; Y=&FF (no further use)
80A4 LDA dispatch_0_hi-1,x ; Load high byte of (handler - 1) from table
80A7 PHA ; Push high byte onto stack
80A8 LDA dispatch_0_lo-1,x ; Load low byte of (handler - 1) from table
80AB PHA ; Push low byte onto stack
80AC LDX fs_options ; Restore X (fileserver options) for use by handler
80AE .return_1←6← 806D BMI← 8071 BCS← 8083 BEQ← 8091 BCS← 809B BCS← 80B7 BEQ
RTS ; RTS pops address, adds 1, jumps to handler

Service handler entry

Intercepts three special service calls before normal dispatch: &FE: Tube init — explode character definitions (OSBYTE &14, X=6) &FF: Full init — set up WRCHV/RDCHV/BRKV/EVNTV, copy NMI handler code from ROM to RAM pages &04-&06, copy workspace init to &0016-&0076, then fall through to select NFS. &12 with Y=5: Select NFS as active filing system. All other service calls dispatch via dispatch_service (&8127).

80AF .service_handler←1← 8003 JMP
.service_handler_entry←1← 8003 JMP
.check_svc_high←1← 8003 JMP
CMP #&fe ; Service >= &FE?
80B1 BCC check_svc_12 ; Service < &FE: skip to &12/dispatch check
80B3 BNE init_vectors_and_copy ; Service &FF: full init (vectors + RAM copy)
80B5 CPY #0 ; Service &FE: Y=0?
80B7 BEQ return_1 ; Y=0: no Tube data, skip to &12 check
80B9 STX zp_temp_11 ; Save ROM number across OSBYTE
80BB STY zp_temp_10 ; Save Tube address across OSBYTE
80BD LDX #6 ; X=6 extra pages for char definitions
80BF LDA #osbyte_explode_chars ; OSBYTE &14: explode character RAM
80C1 JSR osbyte ; Explode character definition RAM (six extra pages), can redefine all characters 32-255 (X=6)
80C4 LDX zp_temp_11 ; Restore ROM number
80C6 BNE restore_y_check_svc ; Continue to vector setup
fall through ↓

NFS initialisation (service &FF: full reset)

Sets up OS vectors for Tube co-processor support: WRCHV = &051C (page 5 — WRCH handler) RDCHV = &04E7 (page 4 — RDCH handler) BRKV = &0016 (workspace — BRK/error handler) EVNTV = &06E8 (page 6 — event handler) Writes &8E to Tube control register (&FEE0). Then copies 3 pages of Tube host code from ROM (tube_code_page4, tube_dispatch_table, tube_code_page6) to RAM (&0400/&0500/&0600), calls tube_post_init (&0414), and copies 97 bytes of workspace init from ROM (nmi_workspace_start) to &0016-&0076.

80C8 .init_vectors_and_copy←1← 80B3 BNE
LDA #&1c ; Set WRCHV = &051C (Tube WRCH handler)
80CA STA wrchv ; Set WRCHV low byte
80CD LDA #5 ; A=5: WRCHV high byte
80CF STA wrchv+1 ; Set WRCHV high byte
80D2 LDA #&e7 ; Set RDCHV = &04E7 (Tube RDCH handler)
80D4 STA rdchv ; Set RDCHV low byte
80D7 LDA #4 ; A=4: RDCHV high byte
80D9 STA rdchv+1 ; Set RDCHV high byte
80DC LDA #&16 ; Set BRKV = &0016 (BRK handler in workspace)
80DE STA brkv ; Set BRKV low byte
80E1 LDA #0 ; A=0: BRKV high byte (page zero)
80E3 STA brkv+1 ; Set BRKV high byte
80E6 LDA #&e8 ; Set EVNTV = &06E8 (event handler in page 6)
80E8 STA evntv ; Set EVNTV low byte
80EB LDA #6 ; A=6: EVNTV high byte
80ED STA evntv+1 ; Set EVNTV high byte
80F0 LDA #&8e ; Write &8E to Tube control register
80F2 STA tube_status_1_and_tube_control ; Write &8E to Tube control register
80F5 STY zp_temp_10 ; Save Y to temporary
80F7 LDY #0 ; Y=0: start ROM-to-RAM copy loop
; Copy NMI handler code from ROM to RAM pages &04-&06
80F9 .cloop←1← 810C BNE
LDA reloc_p4_src,y ; Load ROM byte from page &93
80FC STA tube_code_page4,y ; Store to page &04 (Tube code)
80FF LDA l944d,y ; Load ROM byte from page &94
8102 STA tube_dispatch_table,y ; Store to page &05 (dispatch table)
8105 LDA c954d,y ; Load ROM byte from page &95
8108 STA tube_code_page6,y ; Store to page &06
810B DEY ; DEY wraps 0 -> &FF on first iteration
810C BNE cloop ; Loop until 256 bytes copied per page
810E JSR tube_post_init ; Run post-init routine in copied code
8111 LDX #&60 ; X=&60: copy 97 bytes (&60..&00)
; Copy NMI workspace initialiser from ROM to &0016-&0076
8113 .copy_nmi_workspace←1← 8119 BPL
LDA reloc_zp_src,x ; Load NMI workspace init byte from ROM
8116 STA nmi_workspace_start,x ; Store to zero page &16+X
8118 DEX ; Next byte
8119 BPL copy_nmi_workspace ; Loop until all workspace bytes copied
811B .restore_y_check_svc←1← 80C6 BNE
LDY zp_temp_10 ; Restore Y (ROM number)
811D .tube_chars_done
LDA #0 ; A=0: fall through to service &12 check
811F .check_svc_12←1← 80B1 BCC
CMP #&12 ; Is this service &12 (select FS)?
8121 BNE dispatch_service ; No: check if service < &0D
8123 CPY #5 ; Service &12: Y=5 (NFS)?
8125 BEQ select_nfs ; Y=5: select NFS
fall through ↓

Service call dispatcher

Dispatches MOS service calls 0-12 via the shared dispatch table. Uses base offset Y=0, so table index = service number + 1. Service numbers >= 13 are ignored (branch to return_2). Called via JSR &809F rather than fall-through, so it returns to &813C to restore saved registers.

8127 .dispatch_service←1← 8121 BNE
.not_svc_12_nfs←1← 8121 BNE
CMP #&0d ; Service >= &0D?
8129 .svc_unhandled_return
BCS return_2 ; Service >= &0D: not handled, return
812B .do_svc_dispatch
TAX ; X = service number (dispatch index)
812C LDA rom_svc_num ; Save &A9 (current service state)
812E PHA ; Push saved &A9
812F LDA nfs_temp ; Save &A8 (workspace page number)
8131 PHA ; Push saved &A8
8132 STX rom_svc_num ; Store service number to &A9
8134 STY nfs_temp ; Store Y (page number) to &A8
8136 TYA ; A = Y for dispatch table offset
8137 LDY #0 ; Y=0: base offset for service dispatch
8139 JSR dispatch ; JSR to dispatcher (returns here after handler completes)
813C LDX rom_svc_num ; Recover service claim status from &A9
813E PLA ; Restore saved &A8 from stack
813F STA nfs_temp ; Write back &A8
fall through ↓

Service dispatch epilogue

Common return path for all dispatched service handlers. Restores rom_svc_num from the stack (pushed by dispatch_service), transfers X (ROM number) to A, then returns via RTS.

8141 .svc_dispatch_epilogue
PLA ; Restore saved A from service dispatch
8142 STA rom_svc_num ; Save to workspace &A9
8144 TXA ; Return ROM number in A
8145 .return_2←2← 8129 BCS← 814A BEQ
RTS ; Return (not our command)

Resume after remote operation / *ROFF handler (NROFF)

Checks byte 4 of (net_rx_ptr): if non-zero, the keyboard was disabled during a remote operation (peek/poke/boot). Clears the flag, re-enables the keyboard via OSBYTE &C9, and sends function &0A to notify completion. Also handles *ROFF and the triple-plus escape sequence (+++), which resets system masks via OSBYTE &CE and returns control to the MOS, providing an escape route when a remote session becomes unresponsive.

8146 .resume_after_remote←2← 817B BEQ← 8DF3 JSR
LDY #4 ; Y=4: offset of keyboard disable flag
8148 LDA (net_rx_ptr),y ; Read flag from RX buffer
814A BEQ return_2 ; Zero: keyboard not disabled, skip
814C LDA #0 ; A=0: value to clear flag and re-enable
814E TAX ; X=&00
814F STA (net_rx_ptr),y ; Clear keyboard disable flag in buffer
8151 TAY ; Y=&00
8152 LDA #osbyte_read_write_econet_keyboard_disable ; OSBYTE &C9: Econet keyboard disable
8154 JSR osbyte ; Re-enable keyboard (X=0, Y=0) Enable keyboard (for Econet)
8157 LDA #&0a ; Function &0A: remote operation complete
8159 JSR setup_tx_and_send ; Send notification to controlling station
815C .clear_osbyte_ce_cf←1← 911C JSR
STX nfs_workspace ; Save X (return value from TX)
815E LDA #&ce ; OSBYTE &CE: first system mask to reset
8160 .clear_osbyte_masks←1← 816B BEQ
LDX nfs_workspace ; Restore X for OSBYTE call
8162 LDY #&7f ; Y=&7F: AND mask (clear bit 7)
8164 JSR osbyte ; Reset system mask byte
8167 ADC #1 ; Advance to next OSBYTE (&CE -> &CF)
8169 CMP #&d0 ; Reached &D0? (past &CF)
816B BEQ clear_osbyte_masks ; No: reset &CF too
816D .skip_kbd_reenable
LDA #0 ; A=0: clear remote state
816F STA nfs_workspace ; Clear &A9 (service dispatch state)
8171 RTS ; Return from workspace reset

Service 4: unrecognised * command

Matches the command text against ROM string table entries. Both entries reuse bytes from the ROM header to save space:

  X=8: matches "ROFF" at copyright_string+3 — the suffix
       of "(C)ROFF" → *ROFF (Remote Off,
       end remote session) — jumps to resume_after_remote
  X=1: matches "NET" at &8009 — the ROM title string
       → *NET (select NFS) — falls through to select_nfs

If neither matches, returns with the service call unclaimed.

8172 .svc_4_star_command
LDX #8 ; X=8: ROM offset for *ROFF match
8174 JSR match_rom_string ; Match command against ROM string
8177 BNE match_net_cmd ; No match: try *NET command
8179 STA rom_svc_num ; Match found: claim service (A=0)
817B BEQ resume_after_remote ; ALWAYS branch
817D .match_net_cmd←1← 8177 BNE
LDX #1 ; X=1: ROM offset for "NET" match
817F JSR match_rom_string ; Try matching *NET command
8182 BNE restore_y_return ; No match: return unclaimed
fall through ↓

Select NFS as active filing system (INIT)

Reached from service &12 (select FS) with Y=5, or when *NET command selects NFS. Notifies the current FS of shutdown via FSCV A=6 — this triggers the outgoing FS to save its context back to its workspace page, allowing restoration if re-selected later (the FSDIE handoff mechanism). Then sets up the standard OS vector indirections (FILEV through FSCV) to NFS entry points, claims the extended vector table entries, and issues service &0F (vectors claimed) to notify other ROMs. If nfs_temp is zero (auto-boot not inhibited), injects the synthetic command "I .BOOT" through the command decoder to trigger auto-boot login.

8184 .select_nfs←1← 8125 BEQ
JSR call_fscv_shutdown ; Notify current FS of shutdown (FSCV A=6)
8187 SEC ; C=1 for ROR
8188 ROR nfs_temp ; Set bit 7 of l00a8 (inhibit auto-boot)
818A JSR issue_vectors_claimed ; Claim OS vectors, issue service &0F
818D LDY #&1d ; Y=&1D: top of FS state range
818F .initl←1← 8197 BNE
LDA (net_rx_ptr),y ; Copy FS state from RX buffer...
8191 STA fs_state_deb,y ; ...to workspace (offsets &15-&1D)
8194 DEY ; Next byte (descending)
8195 CPY #&14 ; Loop until offset &14 done
8197 BNE initl ; Continue loop
8199 BEQ init_fs_vectors ; ALWAYS branch to init_fs_vectors ALWAYS branch

Match command text against ROM string table

Compares characters from (os_text_ptr)+Y against bytes starting at binary_version+X (&8008+X). Input is uppercased via AND &DF. Returns with Z=1 if the ROM string's NUL terminator was reached (match), or Z=0 if a mismatch was found. On match, Y points past the matched text; on return, skips trailing spaces.

819B .match_rom_string←2← 8174 JSR← 817F JSR
LDY nfs_temp ; Y = saved text pointer offset
819D .match_next_char←1← 81AA BNE
LDA (os_text_ptr),y ; Load next input character
819F AND #&df ; Force uppercase (clear bit 5)
81A1 BEQ cmd_name_matched ; Input char is NUL/space: check ROM byte
81A3 CMP binary_version,x ; Compare with ROM string byte
81A6 BNE cmd_name_matched ; Mismatch: check if ROM string ended
81A8 INY ; Advance input pointer
81A9 INX ; Advance ROM string pointer
81AA BNE match_next_char ; Continue matching (always taken)
81AC .cmd_name_matched←2← 81A1 BEQ← 81A6 BNE
LDA binary_version,x ; Load ROM string byte at match point
81AF BEQ skip_cmd_spaces ; Zero = end of ROM string = full match
81B1 RTS ; Non-zero = partial/no match; Z=0
81B2 .skpspi←1← 81B7 BEQ
INY ; Skip this space
81B3 .skip_cmd_spaces←1← 81AF BEQ
LDA (os_text_ptr),y ; Load next input character
81B5 CMP #&20 ; Is it a space?
81B7 BEQ skpspi ; Yes: keep skipping
81B9 EOR #&0d ; XOR with CR: Z=1 if end of line
81BB RTS ; Return (not our service call)

Service 9: *HELP

Prints the ROM identification string using print_inline.

81BC .svc_9_help
JSR print_inline ; Print inline ROM identification string
81BF EQUS ".NFS 3.34B."
81CA .restore_y_return←2← 8182 BNE← 81DF BNE
LDY nfs_temp ; Restore Y from temporary
81CC RTS ; Return (service not claimed)

Notify filing system of shutdown

Loads A=6 (FS shutdown notification) and JMP (FSCV). The FSCV handler's RTS returns to the caller of this routine (JSR/JMP trick saves one level of stack).

81CD .call_fscv_shutdown←2← 8184 JSR← 81D2 JSR
LDA #6 ; FSCV reason 6 = FS shutdown
81CF JMP (fscv) ; Tail-call via filing system control vector

Service 3: auto-boot

Notifies current FS of shutdown via FSCV A=6. Scans keyboard (OSBYTE &7A): if the 'N' key is pressed (matrix address &55), the keypress is forgotten via OSBYTE &78 and auto-boot proceeds. Any other key causes the auto-boot to be declined. If no key is pressed, auto-boot proceeds directly. Falls through to print_station_info, then init_fs_vectors.

81D2 .svc_3_autoboot
JSR call_fscv_shutdown ; Notify current FS of shutdown
81D5 LDA #osbyte_scan_keyboard_from_16 ; OSBYTE &7A: scan keyboard
81D7 JSR osbyte ; Keyboard scan starting from key 16
81DA TXA ; X is key number if key is pressed, or &ff otherwise
81DB BMI print_station_info ; No key pressed: proceed with auto-boot
fall through ↓

Check boot key

Checks if the pressed key (in A) is 'N' (matrix address &55). If not 'N', returns to the MOS without claiming the service call (another ROM may boot instead). If 'N', forgets the keypress via OSBYTE &78 and falls through to print_station_info.

81DD .check_boot_key
EOR #&55 ; XOR with &55: result=0 if key is 'N'
81DF BNE restore_y_return ; Not 'N': return without claiming
81E1 TAY ; Y=key
81E2 LDA #osbyte_write_keys_pressed ; OSBYTE &78: clear key-pressed state
81E4 JSR osbyte ; Write current keys pressed (X and Y)
fall through ↓

Print station identification

Prints "Econet Station <n>" using the station number from the net receive buffer, then tests ADLC SR2 for the network clock signal — prints " No Clock" if absent. Falls through to init_fs_vectors.

81E7 .print_station_info←1← 81DB BMI
JSR print_inline ; Print 'Econet Station ' banner
81EA EQUS "Econet Station "
81F9 LDA tx_clear_flag ; Load local station number
81FC JSR print_decimal ; Print station number as decimal
81FF LDA #&20 ; A=&20: test bit 5 of SR2 (clock)
8201 BIT econet_control23_or_status2 ; Test ADLC SR2 for network clock
8204 BEQ skip_no_clock_msg ; Clock present: skip warning msg
8206 JSR print_inline ; Print ' No Clock' warning
8209 EQUS " No Clock"
8212 NOP ; NOP (padding after inline string)
8213 .skip_no_clock_msg←1← 8204 BEQ
JSR print_inline ; Print two CRs (blank line)
8216 EQUS ".."
fall through ↓

Initialise filing system vectors

Copies 14 bytes from fs_vector_addrs (&824E) into FILEV-FSCV (&0212), setting all 7 filing system vectors to the extended vector dispatch addresses (&FF1B-&FF2D). Calls setup_rom_ptrs_netv to install the ROM pointer table entries with the actual NFS handler addresses. Also reached directly from select_nfs, bypassing the station display. Falls through to issue_vectors_claimed.

8218 .init_fs_vectors←1← 8199 BEQ
LDY #&0d ; Copy 14 bytes: FS vector addresses → FILEV-FSCV
821A .dofsl1←1← 8221 BPL
LDA fs_vector_addrs,y ; Load vector address from ROM table
821D STA filev,y ; Write to FILEV-FSCV vector table
8220 DEY ; Next byte (descending)
8221 BPL dofsl1 ; Loop until all 14 bytes copied
8223 JSR setup_rom_ptrs_netv ; Read ROM ptr table addr, install NETV
8226 LDY #&1b ; Install 7 handler entries in ROM ptr table
8228 LDX #7 ; 7 FS vectors to install
822A JSR store_rom_ptr_pair ; Install each 3-byte vector entry
822D STX rom_svc_num ; X=0 after loop; store as workspace offset
fall through ↓

Issue 'vectors claimed' service and optionally auto-boot

Issues service &0F (vectors claimed) via OSBYTE &8F, then service &0A. If nfs_temp is zero (auto-boot not inhibited), sets up the command string "I .BOOT" at &8246 and jumps to the FSCV 3 unrecognised-command handler (which matches against the command table at &8BD7). The "I." prefix triggers the catch-all entry which forwards the command to the fileserver. Falls through to run_fscv_cmd.

822F .issue_vectors_claimed←1← 818A JSR
LDA #osbyte_issue_service_request ; A=&8F: issue service request
8231 LDX #&0f ; X=&0F: 'vectors claimed' service
8233 JSR osbyte ; Issue paged ROM service call, Reason X=15 - Vectors claimed
8236 LDX #&0a ; X=&0A: service &0A
8238 JSR osbyte ; Issue service &0A
823B LDX nfs_temp ; Non-zero after hard reset: skip auto-boot
823D BNE return_3 ; Non-zero: skip auto-boot
823F LDX #&46 ; X = lo byte of auto-boot string at &8292
fall through ↓

Run FSCV command from ROM

Sets Y to the ROM page high byte (&82) and jumps to fscv_3_star_cmd to execute the command string at (X, Y). X is pre-loaded by the caller with the low byte of the string address. Also used as a data base address by store_rom_ptr_pair for Y-indexed access to the handler address table.

8241 .run_fscv_cmd←2← 82E6 LDA← 82EC LDA
LDY #&82 ; Y=&82: ROM page high byte
8243 JMP fscv_3_star_cmd ; Execute command string at (X, Y)
; Synthetic auto-boot command string. "I " does not match any
; entry in NFS's local command table — "I." requires a dot, and
; "I AM" requires 'A' after the space — so fscv_3_star_cmd
; forwards the entire string to the fileserver, which executes
; the .BOOT file.
8246 EQUS "I .BOOT." ; Auto-boot string tail / NETV handler data

FS vector dispatch and handler addresses (34 bytes)

Bytes 0-13: extended vector dispatch addresses, copied to FILEV-FSCV (&0212) by init_fs_vectors. Each 2-byte pair is a dispatch address (&FF1B-&FF2D) that the MOS uses to look up the handler in the ROM pointer table.

Bytes 14-33: handler address pairs read by store_rom_ptr_pair. Each entry has addr_lo, addr_hi, then a padding byte that is overwritten with the current ROM bank number at runtime. The last entry (FSCV) has no padding byte.

824E .fs_vector_addrs←1← 821A LDA
EQUB &1B ; FILEV dispatch lo
824F EQUB &FF ; FILEV dispatch hi
8250 EQUB &1E ; ARGSV dispatch lo
8251 EQUB &FF ; ARGSV dispatch hi
8252 EQUB &21 ; BGETV dispatch lo
8253 EQUB &FF ; BGETV dispatch hi
8254 EQUB &24 ; BPUTV dispatch lo
8255 EQUB &FF ; BPUTV dispatch hi
8256 EQUB &27 ; GBPBV dispatch lo
8257 EQUB &FF ; GBPBV dispatch hi
8258 EQUB &2A ; FINDV dispatch lo
8259 EQUB &FF ; FINDV dispatch hi
825A EQUB &2D ; FSCV dispatch lo
825B EQUB &FF ; FSCV dispatch hi
825C EQUB &95 ; FILEV handler lo (&8695)
825D EQUB &86 ; FILEV handler hi
825E EQUB &00 ; (ROM bank — overwritten)
825F EQUB &E2 ; ARGSV handler lo (&88E2)
8260 EQUB &88 ; ARGSV handler hi
8261 EQUB &00 ; (ROM bank — overwritten)
8262 EQUB &86 ; BGETV handler lo (&8486)
8263 EQUB &84 ; BGETV handler hi
8264 EQUB &00 ; (ROM bank — overwritten)
8265 EQUB &A3 ; BPUTV handler lo (&83A3)
8266 EQUB &83 ; BPUTV handler hi
8267 EQUB &00 ; (ROM bank — overwritten)
8268 EQUB &EB ; GBPBV handler lo (&89EB)
8269 EQUB &89 ; GBPBV handler hi
826A EQUB &00 ; (ROM bank — overwritten)
826B EQUB &4A ; FINDV handler lo (&894A)
826C EQUB &89 ; FINDV handler hi
826D EQUB &00 ; (ROM bank — overwritten)
826E EQUB &8C ; FSCV handler lo (&808C)
826F EQUB &80 ; FSCV handler hi

Service 1: claim absolute workspace

Claims pages up to &10 for NMI workspace (&0D), FS state (&0E), and FS command buffer (&0F). If Y >= &10, workspace already allocated — returns unchanged.

On EntryYcurrent top of absolute workspace
On ExitYupdated top of absolute workspace (max of input and &10)
8270 .svc_1_abs_workspace
CPY #&10 ; Compare Y against workspace boundary
8272 BCS return_3 ; Y>=&10: already allocated, return
8274 LDY #&10 ; Claim workspace up to page &10
8276 .return_3←2← 823D BNE← 8272 BCS
RTS ; Return to caller
8277 EQUB &08, &90

Service 2: claim private workspace and initialise NFS

Y = next available workspace page on entry. Sets up net_rx_ptr (Y) and nfs_workspace (Y+1) page pointers. On soft break (OSBYTE &FD returns 0): skips FS state init, preserving existing login state, file server selection, and control block configuration — this is why pressing BREAK keeps the user logged in. On power-up/CTRL-BREAK (result non-zero): - Sets FS server station to &FE (FS, the default; no server) - Sets printer server to &EB (PS, the default) - Clears FS handles, OPT byte, message flag, SEQNOS - Initialises all RXCBs with &3F flag (available) In both cases: reads station ID from &FE18 (only valid during reset), calls adlc_init, enables user-level RX (LFLAG=&40).

On EntryYnext available workspace page
On ExitYnext available workspace page after NFS (input + 2)
8279 .svc_2_private_workspace
STY net_rx_ptr_hi ; Store RX buffer page pointer
827B INY ; Advance to next page
827C STY nfs_workspace_hi ; Store workspace page pointer
827E LDA #0 ; A=0 for clearing workspace
8280 LDY #4 ; Y=4: remote status offset
8282 STA (net_rx_ptr),y ; Clear status byte in net receive buffer
8284 LDY #&ff ; Y=&FF: used for later iteration
8286 STA net_rx_ptr ; Clear RX ptr low byte
8288 STA nfs_workspace ; Clear workspace ptr low byte
828A STA nfs_temp ; Clear RXCB iteration counter
828C STA tx_ctrl_status ; Clear TX semaphore (no TX in progress)
828F TAX ; X=0 for OSBYTE X=&00
8290 LDA #osbyte_read_write_last_break_type ; OSBYTE &FD: read type of last reset
8292 JSR osbyte ; Read type of last reset
8295 TXA ; X = break type from OSBYTE result X=value of type of last reset
8296 BEQ read_station_id ; Soft break (X=0): skip FS init
8298 LDY #&15 ; Y=&15: printer station offset in RX buffer
829A LDA #&fe ; &FE = no server selected
829C STA fs_server_stn ; Station &FE = no server selected
829F STA (net_rx_ptr),y ; Store &FE at printer station offset
82A1 LDY #2 ; Y=2: printer server offset
82A3 LDA #&eb ; A=&EB: default printer server
82A5 STA (nfs_workspace),y ; Store printer server at offset 2
82A7 INY ; Y=&03
82A8 LDA #0 ; A=0: clear remaining fields
82AA STA fs_server_net ; Clear FS server network number
82AD STA (nfs_workspace),y ; Clear workspace byte at offset 3
82AF STA prot_status ; Clear protection status mask
82B2 STA fs_messages_flag ; Clear FS messages flag
82B5 .init_rxcb_entries←1← 82C2 BNE
LDA nfs_temp ; Load RXCB counter
82B7 JSR calc_handle_offset ; Convert to workspace byte offset
82BA BCS read_station_id ; C=1: past max handles, done
82BC LDA #&3f ; Mark RXCB as available
82BE STA (nfs_workspace),y ; Write &3F flag to workspace
82C0 INC nfs_temp ; Next RXCB number
82C2 BNE init_rxcb_entries ; Loop for all RXCBs
82C4 .read_station_id←2← 8296 BEQ← 82BA BCS
LDA station_id_disable_net_nmis ; Read station ID (also INTOFF)
82C7 STA tx_clear_flag ; Store station ID for TX scout
82CA JSR trampoline_adlc_init ; Initialise ADLC hardware
82CD LDA #&40 ; Enable user-level RX (LFLAG=&40)
82CF STA rx_status_flags ; Store to rx_flags
fall through ↓

Set up ROM pointer table and NETV

Reads the ROM pointer table base address via OSBYTE &A8, stores it in osrdsc_ptr (&F6). Sets NETV low byte to &36. Then copies one 3-byte extended vector entry (addr=&9008, rom=current) into the ROM pointer table at offset &36, installing osword_dispatch as the NETV handler.

82D2 .setup_rom_ptrs_netv←1← 8223 JSR
LDA #osbyte_read_rom_ptr_table_low ; OSBYTE &A8: read ROM pointer table address
82D4 LDX #0 ; X=0: read low byte
82D6 LDY #&ff ; Y=&FF: read high byte
82D8 JSR osbyte ; Returns table address in X (lo) Y (hi) Read address of ROM pointer table
82DB STX osrdsc_ptr ; Store table base address low byte X=value of address of ROM pointer table (low byte)
82DD STY osrdsc_ptr_hi ; Store table base address high byte Y=value of address of ROM pointer table (high byte)
82DF LDY #&36 ; NETV extended vector offset in ROM ptr table
82E1 STY netv ; Set NETV low byte = &36 (vector dispatch)
82E4 LDX #1 ; Install 1 entry (NETV) in ROM ptr table
82E6 .store_rom_ptr_pair←2← 822A JSR← 82F8 BNE
LDA run_fscv_cmd,y ; Load handler address low byte from table
82E9 STA (osrdsc_ptr),y ; Store to ROM pointer table
82EB INY ; Next byte
82EC LDA run_fscv_cmd,y ; Load handler address high byte from table
82EF STA (osrdsc_ptr),y ; Store to ROM pointer table
82F1 INY ; Next byte
82F2 LDA romsel_copy ; Write current ROM bank number
82F4 STA (osrdsc_ptr),y ; Store ROM number to ROM pointer table
82F6 INY ; Advance to next entry position
82F7 DEX ; Count down entries
82F8 BNE store_rom_ptr_pair ; Loop until all entries installed
82FA LDY nfs_workspace_hi ; Y = workspace high byte + 1 = next free page
82FC INY ; Advance past workspace page
82FD RTS ; Return; Y = page after NFS workspace

FSCV 6: Filing system shutdown / save state (FSDIE)

Called when another filing system (e.g. DFS) is selected. Saves the current NFS context (FSLOCN station number, URD/CSD/LIB handles, OPT byte, etc.) from page &0E into the dynamic workspace backup area. This allows the state to be restored when *NET is re-issued later, without losing the login session. Finally calls OSBYTE &7B (printer driver going dormant) to release the Econet network printer on FS switch.

82FE .fscv_6_shutdown
LDY #&1d ; Copy 10 bytes: FS state to workspace backup
8300 .fsdiel←1← 8308 BNE
LDA fs_state_deb,y ; Load FS state byte at offset Y
8303 STA (net_rx_ptr),y ; Store to workspace backup area
8305 DEY ; Next byte down
8306 CPY #&14 ; Offsets &15-&1D: server, handles, OPT, etc.
8308 BNE fsdiel ; Loop for offsets &1D..&15
830A LDA #osbyte_printer_driver_going_dormant ; A=&7B: printer driver going dormant
830C JMP osbyte ; Printer driver going dormant

Initialise TX control block for FS reply on port &90

Loads port &90 (PREPLY) into A, calls init_tx_ctrl_block to set up the TX control block, stores the port and control bytes, then decrements the control flag. Used by send_fs_reply_cmd to prepare for receiving the fileserver's reply.

830F .init_tx_ctrl_data←1← 8382 JSR
.init_tx_reply_port←1← 8382 JSR
LDA #&90 ; A=&90: FS reply port (PREPLY)
8311 .init_tx_ctrl_port←2← 880F JSR← 8F79 LDA
JSR init_tx_ctrl_block ; Init TXCB from template
8314 STA txcb_port ; Store port number in TXCB
8316 LDA #3 ; Control byte: 3 = transmit
8318 STA txcb_start ; Store control byte in TXCB
831A DEC txcb_ctrl ; Decrement TXCB flag to arm TX
831C RTS ; Return after port setup

Initialise TX control block at &00C0 from template

Copies 12 bytes from tx_ctrl_template (&8335) to &00C0. For the first 2 bytes (Y=0,1), also copies the fileserver station/network from &0E00/&0E01 to &00C2/&00C3. The template sets up: control=&80, port=&99 (FS command port), command data length=&0F, plus padding bytes.

831D .init_tx_ctrl_block←3← 8311 JSR← 8371 JSR← 83BA JSR
PHA ; Preserve A across call
831E LDY #&0b ; Copy 12 bytes (Y=11..0)
8320 .fstxl1←1← 8331 BPL
LDA tx_ctrl_template,y ; Load template byte
8323 STA txcb_ctrl,y ; Store to TX control block at &00C0
8326 CPY #2 ; Y < 2: also copy FS server station/network
8328 BPL fstxl2 ; Skip station/network copy for Y >= 2
832A LDA fs_server_stn,y ; Load FS server station (Y=0) or network (Y=1)
832D STA txcb_dest,y ; Store to dest station/network at &00C2
8330 .fstxl2←1← 8328 BPL
DEY ; Next byte (descending)
8331 BPL fstxl1 ; Loop until all 12 bytes copied
8333 PLA ; Restore A
8334 RTS ; Return

TX control block template (TXTAB, 12 bytes)

12-byte template copied to &00C0 by init_tx_ctrl. Defines the TX control block for FS commands: control flag, port, station/ network, and data buffer pointers (&0F00-&0FFF). The 4-byte Econet addresses use only the low 2 bytes; upper bytes are &FF.

8335 .tx_ctrl_template←1← 8320 LDA
EQUB &80 ; Control flag
8336 EQUB &99 ; Port (FS command = &99)
8337 EQUB &00 ; Station (filled at runtime)
8338 EQUB &00 ; Network (filled at runtime)
8339 EQUB &00 ; Buffer start low
833A EQUB &0F ; Buffer start high (page &0F)
833B .tx_ctrl_upper←3← 8890 BIT← 896A BIT← 915E BIT
EQUB &FF ; Buffer start pad (4-byte Econet addr)
833C EQUB &FF ; Buffer start pad
833D EQUB &FF ; Buffer end low
833E EQUB &0F ; Buffer end high (page &0F)
833F EQUB &FF ; Buffer end pad
8340 EQUB &FF ; Buffer end pad

Prepare FS command with carry set

Alternate entry to prepare_fs_cmd that pushes A, loads &2A into fs_error_ptr, and enters with carry set (SEC). The carry flag is later tested by build_send_fs_cmd to select the byte-stream (BSXMIT) transmission path.

On EntryAflag byte to include in FS command
Yfunction code for FS header
8341 .prepare_cmd_with_flag←1← 8A3C JSR
PHA ; Save flag byte for command
8342 LDA #&2a ; A=&2A: error ptr for retry
8344 SEC ; C=1: include flag in FS command
8345 BCS store_fs_hdr_fn ; ALWAYS branch to prepare_fs_cmd ALWAYS branch
8347 .prepare_cmd_clv←2← 86D8 JSR← 8781 JSR
CLV ; V=0: command has no flag byte
8348 BVC store_fs_hdr_clc ; ALWAYS branch to prepare_fs_cmd ALWAYS branch

*BYE handler (logoff)

Closes any open *SPOOL and *EXEC files via OSBYTE &77 (FXSPEX), then falls into prepare_fs_cmd with Y=&17 (FCBYE: logoff code). Dispatched from the command match table at &8BD7 for "BYE".

834A .bye_handler
LDA #osbyte_close_spool_exec ; A=&77: OSBYTE close spool/exec
834C JSR osbyte ; Close any *SPOOL and *EXEC files
834F LDY #&17 ; Y=function code for HDRFN
fall through ↓

Prepare FS command buffer (12 references)

Builds the 5-byte FS protocol header at &0F00: &0F00 HDRREP = reply port (set downstream, typically &90/PREPLY) &0F01 HDRFN = Y parameter (function code) &0F02 HDRURD = URD handle (from &0E02) &0F03 HDRCSD = CSD handle (from &0E03) &0F04 HDRLIB = LIB handle (from &0E04) Command-specific data follows at &0F05 (TXBUF). Also clears V flag. Called before building specific FS commands for transmission.

On EntryYfunction code for HDRFN
Xpreserved through header build
On ExitA0 on success (from build_send_fs_cmd)
X0 on success, &D6 on not-found
Y1 (offset past command code in reply)
8351 .prepare_fs_cmd←12← 807D JSR← 8835 JSR← 88AB JSR← 88FD JSR← 8924 JSR← 8997 JSR← 89C1 JSR← 8A9B JSR← 8B51 JSR← 8C19 JSR← 8C50 JSR← 8CC9 JSR
CLV ; V=0: standard FS command path
8352 .prepare_fs_cmd_v←2← 8893 JSR← 896D JSR
LDA fs_urd_handle ; Copy URD handle from workspace to buffer
8355 STA fs_cmd_urd ; Store URD at &0F02
8358 LDA #&2a ; A=&2A: error ptr for retry
835A .store_fs_hdr_clc←1← 8348 BVC
CLC ; CLC: no byte-stream path
835B .store_fs_hdr_fn←1← 8345 BCS
STY fs_cmd_y_param ; Store function code at &0F01
835E STA fs_error_ptr ; Store error ptr for TX poll
8360 LDY #1 ; Y=1: copy CSD (offset 1) then LIB (offset 0)
8362 .copy_dir_handles←1← 8369 BPL
LDA fs_csd_handle,y ; Copy CSD and LIB handles to command buffer A=timeout period for FS reply
8365 STA fs_cmd_csd,y ; Store at &0F03 (CSD) and &0F04 (LIB)
8368 DEY ; Y=function code
8369 BPL copy_dir_handles ; Loop for both handles
fall through ↓

Build and send FS command (DOFSOP)

Sets reply port to &90 (PREPLY) at &0F00, initialises the TX control block, then adjusts TXCB's high pointer (HPTR) to X+5 -- the 5-byte FS header (reply port, function code, URD, CSD, LIB) plus the command data -- so only meaningful bytes are transmitted, conserving Econet bandwidth. If carry is set on entry (DOFSBX byte-stream path), takes the alternate path through econet_tx_retry for direct BSXMIT transmission. Otherwise sets up the TX pointer via setup_tx_ptr_c0 and falls through to send_fs_reply_cmd for reply handling. The carry flag is the sole discriminator between byte-stream and standard FS protocol paths -- set by SEC at the BPUTV/BGETV entry points. On return from WAITFS/BSXMIT, Y=0; INY advances past the command code to read the return code. Error &D6 ("not found", on_entry={"x": "buffer extent (command-specific data bytes)", "y": "function code", "a": "timeout period for FS reply", "c": "0 for standard FS path, 1 for byte-stream (BSXMIT)"}, on_exit={"a": "0 on success", "x": "0 on success, &D6 on not-found", "y": "1 (offset past command code in reply)"}) is detected via ADC #(&100-&D6) with C=0 -- if the return code was exactly &D6, the result wraps to zero (Z=1). This is a branchless comparison returning C=1, A=0 as a soft error that callers can handle, vs hard errors which go through FSERR.

On EntryXbuffer extent (command-specific data bytes)
Yfunction code
Atimeout period for FS reply
C0 for standard FS path, 1 for byte-stream (BSXMIT)
On ExitA0 on success
X0 on success, &D6 on not-found
Y1 (offset past command code in reply)
836B .build_send_fs_cmd←1← 8AF4 JSR
PHP ; Save carry (FS path vs byte-stream)
836C LDA #&90 ; Reply port &90 (PREPLY)
836E STA fs_cmd_type ; Store at &0F00 (HDRREP)
8371 JSR init_tx_ctrl_block ; Copy TX template to &00C0
8374 TXA ; A = X (buffer extent)
8375 ADC #5 ; HPTR = header (5) + data (X) bytes to send
8377 STA txcb_end ; Store to TXCB end-pointer low
8379 PLP ; Restore carry flag
837A BCS dofsl5 ; C=1: byte-stream path (BSXMIT)
837C PHP ; Save flags for send_fs_reply_cmd
837D JSR setup_tx_ptr_c0 ; Point net_tx_ptr to &00C0; transmit
8380 PLP ; Restore flags
8381 .send_fs_reply_cmd←2← 8791 JSR← 8A78 JSR
PHP ; Save flags (V flag state)
8382 JSR init_tx_ctrl_data ; Set up RX wait for FS reply
8385 LDA fs_error_ptr ; Load error ptr for TX retry
8387 JSR send_to_fs ; Transmit and wait (BRIANX)
838A PLP ; Restore flags
838B .dofsl7←1← 83A1 BCC
INY ; Y=1: skip past command code byte
838C LDA (txcb_start),y ; Load return code from FS reply
838E TAX ; X = return code
838F BEQ return_dofsl7 ; Zero: success, return
8391 BVC check_fs_error ; V=0: standard path, error is fatal
8393 ADC #&2a ; ADC #&2A: test for &D6 (not found)
8395 .check_fs_error←1← 8391 BVC
BNE store_fs_error ; Non-zero: hard error, go to FSERR
8397 .return_dofsl7←1← 838F BEQ
RTS ; Return (success or soft &D6 error)
8398 .dofsl5←1← 837A BCS
PLA ; Discard saved flags from stack
8399 LDX #&c0 ; X=&C0: TXCB address for byte-stream TX
839B INY ; Y++ past command code
839C JSR econet_tx_retry ; Byte-stream transmit with retry
839F STA fs_load_addr_3 ; Store result to &B3
83A1 BCC dofsl7 ; C=0: success, check reply code
83A3 .bputv_handler
CLC ; CLC for address addition
fall through ↓

Handle BPUT/BGET file byte I/O

BPUTV enters at &83A3 (CLC; fall through) and BGETV enters at &8486 (SEC; JSR here). The carry flag is preserved via PHP/PLP through the call chain and tested later (BCS) to select byte-stream transmission (BSXMIT) vs normal FS transmission (FSXMIT) -- a control-flow encoding using processor flags to avoid an extra flag variable.

BSXMIT uses handle=0 for print stream transactions (which sidestep the SEQNOS sequence number manipulation) and non-zero handles for file operations. After transmission, the high pointer bytes of the CB are reset to &FF -- "The BGET/PUT byte fix" which prevents stale buffer pointers corrupting subsequent byte-level operations.

On EntryC0 for BPUT (write byte), 1 for BGET (read byte)
Abyte to write (BPUT only)
Yfile handle
On ExitApreserved
Xpreserved
Ypreserved
83A4 .handle_bput_bget←1← 8487 JSR
PHA ; Save A (BPUT byte) on stack
83A5 STA fs_error_flags ; Also save byte at &0FDF for BSXMIT
83A8 TXA ; Transfer X for stack save
83A9 PHA ; Save X on stack
83AA TYA ; Transfer Y (handle) for stack save
83AB PHA ; Save Y (handle) on stack
83AC PHP ; Save P (C = BPUT/BGET selector) on stack
83AD JSR handle_to_mask_clc ; Convert handle Y to single-bit mask
83B0 STY fs_handle_mask ; Store handle bitmask at &0FDE
83B3 STY fs_spool0 ; Store handle bitmask for sequence tracking
83B5 LDY #&90 ; &90 = data port (PREPLY)
83B7 STY fs_putb_buf ; Store reply port in command buffer
83BA JSR init_tx_ctrl_block ; Set up 12-byte TXCB from template
83BD LDA #&dc ; CB reply buffer at &0FDC
83BF STA txcb_start ; Store reply buffer ptr low in TXCB
83C1 LDA #&e0 ; Error buffer at &0FE0
83C3 STA txcb_end ; Store error buffer ptr low in TXCB
83C5 INY ; Y=1 (from init_tx_ctrl_block exit)
83C6 LDX #9 ; X=9: BPUT function code
83C8 PLP ; Restore C: selects BPUT (0) vs BGET (1)
83C9 BCC store_retry_count ; C=0 (BPUT): keep X=9
83CB DEX ; X=&08
83CC .store_retry_count←1← 83C9 BCC
STX fs_getb_buf ; Store function code at &0FDD
83CF LDA fs_handle_mask ; Load handle bitmask for BPUT/BGET
83D2 LDX #&c0 ; X=&C0: TXCB address for econet_tx_retry
83D4 JSR econet_tx_retry ; Transmit via byte-stream protocol
83D7 LDX fs_getb_buf ; Load reply byte from buffer
83DA BEQ update_sequence_return ; Zero reply = success, skip error handling
83DC LDY #&1f ; Copy 32-byte reply to error buffer at &0FE0
83DE .error1←1← 83E5 BPL
LDA fs_putb_buf,y ; Load reply byte at offset Y
83E1 STA fs_error_buf,y ; Store to error buffer at &0FE0+Y
83E4 DEY ; Next byte (descending)
83E5 BPL error1 ; Loop until all 32 bytes copied
83E7 TAX ; X=File handle
83E8 LDA #osbyte_read_write_spool_file_handle ; A=&C7: read *SPOOL file handle
83EA JSR osbyte ; Read/Write *SPOOL file handle
83ED TXA ; X=value of *SPOOL file handle
83EE JSR handle_to_mask_a ; Convert SPOOL handle to bitmask
83F1 CPY fs_spool0 ; Compare SPOOL mask with file mask
83F3 BNE dispatch_fs_error ; Not SPOOL file: dispatch FS error
83F5 LDX #<(sp_dot_string) ; Load '*SP.' command string low
83F7 LDY #>(sp_dot_string) ; Y=&85: high byte of OSCLI string in ROM
83F9 JSR oscli ; Close SPOOL/EXEC via "*SP." or "*E."
83FC .dispatch_fs_error←1← 83F3 BNE
LDA #&e0 ; Reset CB pointer to error buffer at &0FE0
83FE STA txcb_start ; Reset reply ptr to error buffer
8400 LDX fs_getb_buf ; Reload reply byte for error dispatch
fall through ↓

Handle fileserver error replies (FSERR)

The fileserver returns errors as: zero command code + error number + CR-terminated message string. This routine converts the reply buffer in-place to a standard MOS BRK error packet by: 1. Storing the error code at fs_last_error (&0E09) 2. Normalizing error codes below &A8 to &A8 (the standard FS error number), since the MOS error space below &A8 has other meanings 3. Scanning for the CR terminator and replacing it with &00 4. JMPing indirect through (l00c4) to execute the buffer as a BRK instruction — the zero command code serves as the BRK opcode N.B. This relies on the fileserver always returning a zero command code in position 0 of the reply buffer.

8403 .store_fs_error←1← 8395 BNE
STX fs_last_error ; Remember raw FS error code
8406 LDY #1 ; Y=1: point to error number byte in reply
8408 CPX #&a8 ; Clamp FS errors below &A8 to standard &A8
840A BCS find_cr_terminator ; Error >= &A8: keep original value
840C LDA #&a8 ; Error < &A8: override with standard &A8
840E STA (txcb_start),y ; Write clamped error number to reply buffer
8410 .find_cr_terminator←2← 840A BCS← 8415 BNE
INY ; Advance to next reply buffer byte
8411 LDA #&0d ; A=CR: terminator to search for
8413 EOR (txcb_start),y ; XOR with buffer byte (0 when CR)
8415 BNE find_cr_terminator ; Not CR: continue scanning
8417 STA (txcb_start),y ; Store 0 (from XOR) to replace CR
8419 JMP (txcb_start) ; Execute error via JMP indirect
841C .update_sequence_return←1← 83DA BEQ
STA fs_sequence_nos ; Save updated sequence number
841F PLA ; Restore Y from stack
8420 TAY ; Transfer A to Y for indexing
8421 PLA ; Restore X from stack
8422 TAX ; Transfer to X for return
8423 PLA ; Restore A from stack
8424 RTS ; Return to caller
8425 .error_not_listening←1← 8478 BEQ
LDA #8 ; Error code 8: "Not listening" error
8427 BNE set_listen_offset ; ALWAYS branch to set_listen_offset ALWAYS branch
8429 .nlistn←1← 868E JMP
LDA (net_tx_ptr,x) ; Load TX status byte for error lookup
842B .nlisne←2← 8484 BNE← 89B9 JMP
AND #7 ; Mask to 3-bit error code (0-7)
842D .set_listen_offset←1← 8427 BNE
TAX ; X = error code index
842E LDY error_offsets,x ; Look up error message offset from table
8431 LDX #0 ; X=0: start writing at &0101
8433 STX l0100 ; Store BRK opcode at &0100
8436 .copy_error_message←1← 8440 BNE
LDA error_msg_table,y ; Load error message byte
8439 STA l0101,x ; Build error message at &0101+
843C BEQ execute_brk_error ; Zero byte = end of message; go execute BRK
843E INX ; Advance output buffer position
843F INY ; Advance source string pointer
8440 BNE copy_error_message ; Continue copying message bytes
8442 .execute_brk_error←1← 843C BEQ
JMP l0100 ; Execute constructed BRK error
8445 .sp_dot_string
EQUS "SP."
8448 EQUB &0D
8449 .send_to_fs_star←3← 8731 JSR← 8FBC JSR← 927E JSR
LDA #&2a ; A=&2A: error ptr for FS retry
fall through ↓

Send command to fileserver and handle reply (WAITFS)

Performs a complete FS transaction: transmit then wait for reply. Sets bit 7 of rx_status_flags (mark FS transaction in progress), builds a TX frame from the data at (net_tx_ptr), and transmits it. The system RX flag (LFLAG bit 7) is only set when receiving into the page-zero control block — if RXCBP's high byte is non-zero, setting the system flag would interfere with other RX operations. The timeout counter uses the stack (indexed via TSX) rather than memory to avoid bus conflicts with Econet hardware during the tight polling loop. Handles multi-block replies and checks for escape conditions between blocks.

844B .send_to_fs←2← 8387 JSR← 881B JSR
PHA ; Save function code on stack
844C LDA rx_status_flags ; Load current rx_flags
844F PHA ; Save rx_flags on stack for restore
8450 ORA #&80 ; Set bit7: FS transaction in progress
8452 STA rx_status_flags ; Write back updated rx_flags
8455 .skip_rx_flag_set
LDA #0 ; Push two zero bytes as timeout counters
8457 PHA ; First zero for timeout
8458 PHA ; Second zero for timeout
8459 TAY ; Y=0: index for flag byte check Y=&00
845A TSX ; TSX: index stack-based timeout via X
845B .incpx←3← 8465 BNE← 846A BNE← 846F BNE
LDA (net_tx_ptr),y ; Load TX flag byte from ctrl block
845D BMI fs_wait_cleanup ; Bit 7 set: TX complete, clean up
845F JSR check_escape ; Check for Escape during TX wait
8462 DEC l0101,x ; Three-stage nested timeout: inner loop
8465 BNE incpx ; Inner not expired: keep polling
8467 DEC l0102,x ; Middle timeout loop
846A BNE incpx ; Middle not expired: keep polling
846C DEC l0104,x ; Outer timeout loop (slowest)
846F BNE incpx ; Outer not expired: keep polling
8471 .fs_wait_cleanup←1← 845D BMI
PLA ; Pop first timeout byte
8472 PLA ; Pop second timeout byte
8473 PLA ; Pop saved rx_flags into A
8474 STA rx_status_flags ; Restore saved rx_flags from stack
8477 PLA ; Pop saved function code
8478 BEQ error_not_listening ; A=saved func code; zero would mean no reply
847A RTS ; Return to caller

Check and handle escape condition (ESC)

Two-level escape gating: the MOS escape flag (&FF bit 7) is ANDed with the software enable flag ESCAP. Both must have bit 7 set for escape to fire. ESCAP is set non-zero during data port operations (LOADOP stores the data port &90, serving double duty as both the port number and the escape-enable flag). ESCAP is disabled via LSR in the ENTER routine, which clears bit 7 — PHP/PLP around the LSR preserves the carry flag since ENTER is called from contexts where carry has semantic meaning (e.g., PUTBYT vs BGET distinction). This architecture allows escape between retransmission attempts but prevents interruption during critical FS transactions. If escape fires: acknowledges via OSBYTE &7E, then checks whether the failing handle is the current SPOOL or EXEC handle (OSBYTE &C6/&C7); if so, issues "*SP." or "*E." via OSCLI to gracefully close the channel before raising the error — preventing the system from continuing to spool output to a broken file handle.

847B .check_escape←2← 845F JSR← 8675 JSR
LDA #&7e ; A=&7E: OSBYTE acknowledge escape
fall through ↓

Test MOS escape flag and abort if pending

Tests MOS escape flag (&FF bit 7). If escape is pending: acknowledges via OSBYTE &7E, writes &3F (deleted marker) into the control block via (net_tx_ptr),Y, and branches to the NLISTN error path. If no escape, returns immediately.

847D .check_escape_handler
BIT escape_flag ; Test escape flag (bit 7)
847F BPL return_bget ; Bit 7 clear: no escape, return
8481 JSR osbyte ; Acknowledge escape via OSBYTE &7E
8484 BNE nlisne ; Non-zero: report 'Not listening'
8486 .bgetv_handler
SEC ; C=1: flag for BGET mode
8487 JSR handle_bput_bget ; Handle BGET via FS command Handle BPUT/BGET file byte I/O
848A SEC ; SEC: set carry for error check
848B LDA #&fe ; A=&FE: mask for EOF check
848D BIT fs_error_flags ; BIT l0fdf: test error flags
8490 BVS return_bget ; V=1: error, return early
8492 CLC ; CLC: no error
8493 BMI tx_flow_control ; Bit 7 set: set EOF hint flag
8495 LDA fs_spool0 ; Load handle bitmask for flag op
8497 JSR clear_fs_flag ; Clear EOF hint flag for this handle
849A BCC tx_error_classify ; Flag cleared: load handle mask
849C .tx_flow_control←1← 8493 BMI
LDA fs_spool0 ; Load handle bitmask for flow control
849E JSR set_fs_flag ; Set EOF hint flag for this handle
84A1 .tx_error_classify←1← 849A BCC
LDA fs_handle_mask ; Load handle mask for return value
84A4 .return_bget←2← 847F BPL← 8490 BVS
RTS ; Return with handle mask in A
84A5 .add_5_to_y←1← 8761 JSR
INY ; Y += 5 Y += 5 (entry point)
84A6 .add_4_to_y←1← 8A4E JSR
INY ; Y += 4
84A7 INY ; (continued)
84A8 INY ; (continued)
84A9 INY ; (continued)
84AA RTS ; Return with Y adjusted
84AB .sub_4_from_y←1← 8750 JSR
DEY ; Y -= 4
84AC .sub_3_from_y←2← 885D JSR← 8A56 JSR
DEY ; Y -= 3
84AD DEY ; (continued)
84AE DEY ; (continued)
84AF .return_4
RTS ; Return with handle mask in A
; Econet error message table (ERRTAB, 8 entries).
; Each entry: error number byte followed by NUL-terminated ; string.
; &A0: "Line Jammed" &A1: "Net Error"
; &A2: "Not listening" &A3: "No Clock"
; &A4: "Bad Txcb" &11: "Escape"
; &CB: "Bad Option" &A5: "No reply"
; Indexed by the low 3 bits of the TXCB flag byte (AND #&07),
; which encode the specific Econet failure reason. The NREPLY
; and NLISTN routines build a MOS BRK error block at &100 on the
; stack page: NREPLY fires when the fileserver does not respond
; within the timeout period; NLISTN fires when the destination
; station actively refused the connection.
; Indexed via the error dispatch at c8424/c842c.
84B0 .error_msg_table←1← 8436 LDA
EQUB &A0
84B1 EQUS "Line Jammed."
84BD EQUB &A1
84BE EQUS "Net Error."
84C8 EQUB &A2
84C9 EQUS "Not listening."
84D7 EQUB &A3
84D8 EQUS "No Clock."
84E1 EQUB &A4
84E2 EQUS "Bad Txcb."
84EB EQUB &11
84EC EQUS "Escape."
84F3 EQUB &CB
84F4 EQUS "Bad Option."
84FF EQUB &A5
8500 EQUS "No reply."

Save FSCV/vector arguments

Stores A, X, Y into the filing system workspace. Called at the start of every FS vector handler (FILEV, ARGSV, BGETV, BPUTV, GBPBV, FINDV, FSCV). NFS repurposes CFS/RFS workspace locations: &BD (fs_last_byte_flag) = A (function code / command) &BB (fs_options) = X (control block ptr low) &BC (fs_block_offset) = Y (control block ptr high) &BE/&BF (fs_crc_lo/hi) = X/Y (duplicate for indexed access)

On EntryAfunction code
Xcontrol block pointer low
Ycontrol block pointer high
8509 .save_fscv_args←6← 808C JSR← 8695 JSR← 88E2 JSR← 894A JSR← 89EB JSR← 8B93 JSR
STA fs_last_byte_flag ; A = function code / command
850B STX fs_options ; X = control block ptr lo
850D STY fs_block_offset ; Y = control block ptr hi
850F STX fs_crc_lo ; X dup for indexed access via (fs_crc)
8511 STY fs_crc_hi ; Y dup for indexed access
8513 RTS ; Return

Decode file attributes: FS → BBC format (FSBBC, 6-bit variant)

Reads attribute byte at offset &0E from the parameter block, masks to 6 bits, then falls through to the shared bitmask builder. Converts fileserver protection format (5-6 bits) to BBC OSFILE attribute format (8 bits) via the lookup table at &8531. The two formats use different bit layouts for file protection attributes.

8514 .decode_attribs_6bit←2← 886F JSR← 889A JSR
LDY #&0e ; Y=&0E: attribute byte offset in param block
8516 LDA (fs_options),y ; Load FS attribute byte
8518 AND #&3f ; Mask to 6 bits (FS → BBC direction)
851A LDX #4 ; X=4: skip first 4 table entries (BBC→FS half)
851C BNE attrib_shift_bits ; ALWAYS branch to shared bitmask builder ALWAYS branch

Decode file attributes: BBC → FS format (BBCFS, 5-bit variant)

Masks A to 5 bits and builds an access bitmask via the lookup table at &8531. Each input bit position maps to a different output bit via the table. The conversion is done by iterating through the source bits and OR-ing in the corresponding destination bits from the table, translating between BBC (8-bit) and fileserver (5-bit) protection formats.

On EntryABBC attribute byte (bits 0-4 used)
On ExitAFS attribute bitmask (5-bit)
Xcorrupted
851E .decode_attribs_5bit←2← 879C JSR← 88B7 JSR
AND #&1f ; Mask to 5 bits (BBC → FS direction)
8520 LDX #&ff ; X=&FF: INX makes 0; start from table index 0
8522 .attrib_shift_bits←1← 851C BNE
STA fs_error_ptr ; Temp storage for source bitmask to shift out
8524 LDA #0 ; A=0: accumulate destination bits here
8526 .map_attrib_bits←1← 852E BNE
INX ; Next table entry
8527 LSR fs_error_ptr ; Shift out source bits one at a time
8529 BCC skip_set_attrib_bit ; Bit was 0: skip this destination bit
852B ORA access_bit_table,x ; OR in destination bit from lookup table
852E .skip_set_attrib_bit←1← 8529 BCC
BNE map_attrib_bits ; Loop while source bits remain (A != 0)
8530 RTS ; Return; A = converted attribute bitmask
8531 .access_bit_table←1← 852B ORA
EQUB &50, &20, &05, &02, &88, &04, &08, &80, &10, &01, &02

Print inline string, high-bit terminated (VSTRNG)

Pops the return address from the stack, prints each byte via OSASCI until a byte with bit 7 set is found, then jumps to that address. The high-bit byte serves as both the string terminator and the opcode of the first instruction after the string. N.B. Cannot be used for BRK error messages -- the stack manipulation means a BRK in the inline data would corrupt the stack rather than invoke the error handler.

On ExitAterminator byte (bit 7 set, also next opcode)
Xcorrupted (by OSASCI)
Y0
853C .print_inline←13← 81BC JSR← 81E7 JSR← 8206 JSR← 8213 JSR← 8C21 JSR← 8C2B JSR← 8C39 JSR← 8C44 JSR← 8C5E JSR← 8C6F JSR← 8C82 JSR← 8C96 JSR← 8CA3 JSR
PLA ; Pop return address (low) — points to last byte of JSR
853D STA fs_load_addr ; Store return addr low as string ptr
853F PLA ; Pop return address (high)
8540 STA fs_load_addr_hi ; Store return addr high as string ptr
8542 LDY #0 ; Y=0: offset for indirect load
8544 .print_inline_char←1← 8551 BNE
INC fs_load_addr ; Advance pointer past return address / to next char
8546 BNE print_next_char ; No page wrap: skip high byte inc
8548 INC fs_load_addr_hi ; Handle page crossing in pointer
854A .print_next_char←1← 8546 BNE
LDA (fs_load_addr),y ; Load next byte from inline string
854C BMI filev_attrib_code_check ; Bit 7 set? Done — this byte is the next opcode
854E JSR osasci ; Write character
8551 BNE print_inline_char ; Continue printing loop
8553 .filev_attrib_code_check←1← 854C BMI
JMP (fs_load_addr) ; Jump to address of high-bit byte (resumes code after string)

Skip leading spaces in parameter block

Advances Y past space characters in (fs_options),Y. Returns with the first non-space character in A. Sets carry if the character is >= 'A' (alphabetic).

8556 .skip_spaces←3← 855B BEQ← 8C0D JSR← 8D07 JSR
LDA (fs_options),y ; Load character from parameter string
8558 INY ; Advance to next character
8559 CMP #&20 ; Compare against space (ASCII &20)
855B BEQ skip_spaces ; Space found: keep scanning
855D DEY ; Back up one (first non-space char)
855E CMP #&41 ; Compare against 'A' for case flag
8560 RTS ; Return: A=char, C set if >= 'A'

Parse decimal number from (fs_options),Y (DECIN)

Reads ASCII digits and accumulates in &B2 (fs_load_addr_2). Multiplication by 10 uses the identity: n*10 = n*8 + n*2, computed as ASL &B2 (x2), then A = &B2*4 via two ASLs, then ADC &B2 gives x10. Terminates on "." (pathname separator), control chars, or space. The delimiter handling was revised to support dot-separated path components (e.g. "1.$.PROG", on_entry={"y": "offset into (fs_options) buffer"}, on_exit={"a": "parsed value (accumulated in &B2)", "x": "initial A value (saved by TAX)", "y": "offset past last digit parsed"}) >= &40 (any letter), but the revision allows numbers followed by dots.

On EntryYoffset into (fs_options) buffer
On ExitAparsed value (accumulated in &B2)
Xinitial A value (saved by TAX)
Yoffset past last digit parsed
8561 .parse_decimal←2← 8D0E JSR← 8D14 JSR
TAX ; Save A in X for caller
8562 LDA #0 ; Zero accumulator
8564 STA fs_load_addr_2 ; Clear accumulator workspace
8566 .scan_decimal_digit←1← 8583 BNE
LDA (fs_options),y ; Load next char from buffer
8568 CMP #&40 ; Letter or above?
856A BCS no_dot_exit ; Yes: not a digit, done
856C CMP #&2e ; Dot separator?
856E BEQ parse_decimal_rts ; Yes: exit with C=1 (dot found)
8570 BMI no_dot_exit ; Control char or space: done
8572 AND #&0f ; Mask ASCII digit to 0-9
8574 STA fs_load_addr_3 ; Save new digit
8576 ASL fs_load_addr_2 ; Running total * 2
8578 LDA fs_load_addr_2 ; A = running total * 2
857A ASL ; A = running total * 4
857B ASL ; A = running total * 8
857C ADC fs_load_addr_2 ; + total*2 = total * 10
857E ADC fs_load_addr_3 ; + digit = total*10 + digit
8580 STA fs_load_addr_2 ; Store new running total
8582 INY ; Advance to next char
8583 BNE scan_decimal_digit ; Loop (always: Y won't wrap to 0)
8585 .no_dot_exit←2← 856A BCS← 8570 BMI
CLC ; No dot found: C=0
8586 .parse_decimal_rts←1← 856E BEQ
LDA fs_load_addr_2 ; Return result in A
8588 RTS ; Return with result in A

Convert handle in A to bitmask

Transfers A to Y via TAY, then falls through to handle_to_mask_clc to clear carry and convert.

On EntryAfile handle number (&20-&27, or 0)
On ExitApreserved
Xpreserved
Ybitmask (single bit set) or &FF if invalid
8589 .handle_to_mask_a←3← 83EE JSR← 8A06 JSR← 8ECD JSR
TAY ; Handle number to Y for conversion
fall through ↓

Convert handle to bitmask (carry cleared)

Clears carry to ensure handle_to_mask converts unconditionally. Falls through to handle_to_mask.

On EntryYfile handle number (&20-&27, or 0)
On ExitApreserved
Xpreserved
Ybitmask (single bit set) or &FF if invalid
858A .handle_to_mask_clc←3← 83AD JSR← 8823 JSR← 88ED JSR
CLC ; Force unconditional conversion
fall through ↓

Convert file handle to bitmask (Y2FS)

Converts fileserver handles to single-bit masks segregated inside the BBC. NFS handles occupy the &20-&27 range (base HAND=&20), which cannot collide with local filing system or cassette handles -- the MOS routes OSFIND/OSBGET/OSBPUT to the correct filing system based on the handle value alone. The power-of-two encoding allows the EOF hint byte to track up to 8 files simultaneously with one bit per file, and enables fast set operations (ORA to add, EOR to toggle, AND to test) without loops. Handle 0 passes through unchanged (means "no file", on_entry={"y": "handle number", "c": "0: convert, 1 with Y=0: skip, 1 with Y!=0: convert"}, on_exit={"a": "preserved", "x": "preserved", "y": "bitmask (single bit set) or &FF if handle invalid"}) has a built-in validity check: if the handle is out of range, the repeated ASL shifts all bits out, leaving A=0, which is converted to Y=&FF as a sentinel -- bad handles fail gracefully rather than indexing into garbage. Callers needing to move the handle from A use handle_to_mask_a; callers needing carry cleared use handle_to_mask_clc.

On EntryYhandle number
C0: convert, 1 with Y=0: skip, 1 with Y!=0: convert
On ExitApreserved
Xpreserved
Ybitmask (single bit set) or &FF if handle invalid
858B .handle_to_mask←1← 894E JSR
PHA ; Save A (will be restored on exit)
858C TXA ; Save X (will be restored on exit)
858D PHA ; (second half of X save)
858E TYA ; A = handle from Y
858F BCC y2fsl5 ; C=0: always convert
8591 BEQ handle_mask_exit ; C=1 and Y=0: skip (handle 0 = none)
8593 .y2fsl5←1← 858F BCC
SEC ; C=1 and Y!=0: convert
8594 SBC #&1f ; A = handle - &1F (1-based bit position)
8596 TAX ; X = shift count
8597 LDA #1 ; Start with bit 0 set
8599 .y2fsl2←1← 859B BNE
ASL ; Shift bit left
859A DEX ; Count down
859B BNE y2fsl2 ; Loop until correct position
859D ROR ; Undo final extra shift
859E TAY ; Y = resulting bitmask
859F BNE handle_mask_exit ; Non-zero: valid mask, skip to exit
85A1 DEY ; Zero: invalid handle, set Y=&FF
85A2 .handle_mask_exit←2← 8591 BEQ← 859F BNE
PLA ; Restore X
85A3 TAX ; Restore X from stack
85A4 PLA ; Restore A
85A5 RTS ; Return with mask in X

Convert bitmask to handle number (FS2A)

Inverse of Y2FS. Converts from the power-of-two FS format back to a sequential handle number by counting right shifts until A=0. Adds &1E to convert the 1-based bit position to a handle number (handles start at &1F+1 = &20). Used when receiving handle values from the fileserver in reply packets.

On EntryAsingle-bit bitmask
On ExitAhandle number (&20-&27)
Xcorrupted
Ypreserved
85A6 .mask_to_handle←2← 8981 JSR← 8EE7 JSR
LDX #0 ; X = 0 (bit position counter)
85A8 .fs2al1←1← 85AA BNE
INX ; Count this bit position
85A9 LSR ; Shift mask right; C=0 when done
85AA BNE fs2al1 ; Loop until all bits shifted out
85AC TXA ; A = X = &1F + bit position = handle
85AD ADC #&1e ; Add &1E+C(=0) = &1E; handle=&1F+pos
85AF RTS ; Return with A=handle number

Print byte as 3-digit decimal number

Prints A as a decimal number using repeated subtraction for each digit position (100, 10, 1). Leading zeros are printed (no suppression). Used to display station numbers.

On EntryAbyte value to print
On ExitAlast digit character
Xcorrupted
Y0 (remainder after last division)
85B0 .print_decimal←2← 81FC JSR← 8C28 JSR
TAY ; Y=dividend, A=100: hundreds digit
85B1 LDA #&64 ; Print hundreds digit
85B3 JSR print_decimal_digit ; A=10: tens divisor
85B6 LDA #&0a ; Print tens digit
85B8 JSR print_decimal_digit ; Transfer remainder to A
85BB LDA #1 ; Convert to ASCII and print
fall through ↓

Print one decimal digit by repeated subtraction

Divides Y by A using repeated subtraction. Prints the quotient as an ASCII digit ('0'-'9'). Returns with the remainder in Y. X starts at &2F ('0'-1) and increments once per subtraction, giving the ASCII digit directly.

On EntryAdivisor (stored to &B8)
Ydividend
On ExitYremainder
85BD .print_decimal_digit←2← 85B3 JSR← 85B8 JSR
STA fs_error_ptr ; Store divisor in temporary
85BF TYA ; Transfer dividend (Y) to A
85C0 LDX #&2f ; X=&2F: ASCII '0'-1 (loop init)
85C2 SEC ; Set carry for subtraction
85C3 .decimal_divide_loop←1← 85C6 BCS
INX ; Increment digit (ASCII '0'..'9')
85C4 SBC fs_error_ptr ; Subtract divisor from remainder
85C6 BCS decimal_divide_loop ; Carry set: subtract again
85C8 ADC fs_error_ptr ; Add back divisor (undo last SBC)
85CA TAY ; Remainder to Y for next digit
85CB TXA ; Quotient digit (X) to A for print
85CC .print_via_osasci←1← 85FF BNE
JMP osasci ; Write character

Compare two 4-byte addresses

Compares bytes at &B0-&B3 against &B4-&B7 using EOR. Used by the OSFILE save handler to compare the current transfer address (&C8-&CB, copied to &B0) against the end address (&B4-&B7) during multi-block file data transfers.

On ExitAcorrupted (EOR result)
Xcorrupted
Ypreserved
85CF .compare_addresses←2← 8717 JSR← 87CA JSR
LDX #4 ; Compare 4 bytes (index 4,3,2,1)
85D1 .compare_addr_byte←1← 85D8 BNE
LDA addr_work,x ; Load byte from first address
85D3 EOR fs_load_addr_3,x ; XOR with corresponding byte
85D5 BNE return_compare ; Mismatch: Z=0, return unequal
85D7 DEX ; Next byte
85D8 BNE compare_addr_byte ; Continue comparing
85DA .return_compare←1← 85D5 BNE
RTS ; Return with Z flag result
85DB .fscv_7_read_handles
LDX #&20 ; X=first handle (&20)
85DD LDY #&27 ; Y=last handle (&27)
85DF .return_fscv_handles←1← 8604 BEQ
RTS ; Return (FSCV 7 read handles)

Clear bit(s) in FS flags (&0E07)

Inverts A (EOR #&FF), then ANDs into fs_work_0e07 to clear the specified bits. Falls through to set_fs_flag to store.

On EntryAbitmask of bits to clear
On ExitAupdated fs_eof_flags value
85E0 .clear_fs_flag←3← 8497 JSR← 883E JSR← 8A82 JSR
EOR #&ff ; Invert A (NOT mask)
85E2 AND fs_eof_flags ; AND inverted mask to clear bits
fall through ↓

Set bit(s) in FS flags (&0E07)

ORs A into fs_work_0e07 (EOF hint byte). Each bit represents one of up to 8 open file handles. When clear, the file is definitely NOT at EOF. When set, the fileserver must be queried to confirm EOF status. This negative-cache optimisation avoids expensive network round-trips for the common case. The hint is cleared when the file pointer is updated (since seeking away from EOF invalidates the hint) and set after BGET/OPEN/EOF operations that might have reached the end.

On EntryAbitmask of bits to set
On ExitAupdated fs_eof_flags value
85E5 .set_fs_flag←5← 849E JSR← 892A JSR← 8976 JSR← 899D JSR← 8A85 JSR
ORA fs_eof_flags ; OR mask into EOF flags
85E8 STA fs_eof_flags ; Store updated EOF flags
85EB RTS ; Return to caller

Print byte as two hex digits

Prints the high nibble first (via 4× LSR), then the low nibble. Each nibble is converted to ASCII '0'-'9' or 'A'-'F' and output via OSASCI.

On EntryAbyte to print as two hex digits
On ExitApreserved (original byte)
Xcorrupted (by OSASCI)
85EC .print_hex←2← 863A JSR← 8C6C JSR
PHA ; Save full byte on stack
85ED LSR ; Shift high nibble to low position
85EE LSR ; Continue shift (4 LSRs total)
85EF LSR ; Continue shift
85F0 LSR ; High nibble now in bits 0-3
85F1 JSR print_hex_nibble ; Print high nibble as hex
85F4 PLA ; Restore original byte
85F5 AND #&0f ; Mask to low nibble
85F7 .print_hex_nibble←1← 85F1 JSR
ORA #&30 ; Convert to ASCII digit ('0'-'9')
85F9 CMP #&3a ; Compare against ':' (past '9'?)
85FB BCC print_hex_digit ; Digit 0-9: skip A-F adjustment
85FD ADC #6 ; Add 7 to get ASCII 'A'-'F'
85FF .print_hex_digit←2← 85FB BCC← 8643 BNE
BNE print_via_osasci ; ALWAYS branch to print character

Print file catalogue line

Displays a formatted catalogue entry: filename (padded to 12 chars with spaces), load address (4 hex bytes at offset 5-2), exec address (4 hex bytes at offset 9-6), and file length (3 hex bytes at offset &0C-&0A), followed by a newline. Data is read from (fs_crc_lo) for the filename and from (fs_options) for the numeric fields. Returns immediately if fs_messages_flag is zero (no info available).

8601 .print_file_info←2← 8704 JSR← 8784 JSR
LDY fs_messages_flag ; Check if file info available
8604 BEQ return_fscv_handles ; No info available: return
8606 LDY #0 ; Y=0: start of filename string
8608 .print_filename_loop←1← 8616 BNE
LDA (fs_crc_lo),y ; Load filename character
860A CMP #&0d ; CR: end of filename
860C BEQ pad_filename_spaces ; CR: pad rest of filename field
860E CMP #&20 ; Also end name on space character
8610 BEQ pad_filename_spaces ; Space: also ends filename
8612 JSR osasci ; Write character
8615 INY ; Advance to next filename byte
8616 BNE print_filename_loop ; Loop until all chars printed
8618 .pad_filename_spaces←3← 860C BEQ← 8610 BEQ← 861E BCC
JSR print_space ; Print padding space
861B INY ; Advance past filename position
861C CPY #&0c ; Pad to 12 chars wide
861E BCC pad_filename_spaces ; Continue padding if < 12 chars
8620 LDY #5 ; Y=5: high byte of load address
8622 JSR print_hex_bytes ; Print load address as 2 hex bytes Print load address as 2 hex bytes
8625 JSR print_exec_and_len ; Print exec address and length
8628 JMP osnewl ; Write newline (characters 10 and 13)
862B .print_exec_and_len←1← 8625 JSR
LDY #9 ; Y=9: exec address offset
862D JSR print_hex_bytes ; Print exec address bytes
8630 LDY #&0c ; Y=&0C: file length offset
8632 LDX #3 ; X=3: print 3 bytes for file length
8634 BNE num01 ; ALWAYS branch
8636 .print_hex_bytes←2← 8622 JSR← 862D JSR
LDX #4 ; X=4: print 4 bytes for address
8638 .num01←2← 8634 BNE← 863F BNE
LDA (fs_options),y ; Load address/length byte
863A JSR print_hex ; Print as 2 hex digits
863D DEY ; Move to next lower address byte
863E DEX ; Decrement byte counter
863F BNE num01 ; Loop for remaining hex bytes
8641 .print_space←2← 8618 JSR← 8D5D JSR
LDA #&20 ; A=space: separator character
8643 BNE print_hex_digit ; ALWAYS branch

Set up TX pointer to control block at &00C0

Points net_tx_ptr to &00C0 where the TX control block has been built by init_tx_ctrl_block. Falls through to tx_poll_ff to initiate transmission with full retry.

8645 .setup_tx_ptr_c0←2← 837D JSR← 880A JSR
LDX #&c0 ; X=&C0: TX control block at &00C0
8647 STX net_tx_ptr ; Set TX pointer lo
8649 LDX #0 ; X=0: page zero
864B STX net_tx_ptr_hi ; Set TX pointer hi byte to page 0
fall through ↓

Transmit and poll for result (full retry)

Sets A=&FF (retry count) and Y=&60 (timeout parameter). Falls through to tx_poll_core.

864D .tx_poll_ff←3← 9002 JSR← 905C JSR← 925C JSR
LDA #&ff ; A=&FF: full retry count
864F .tx_poll_timeout←1← 8FA5 JSR
LDY #&60 ; Y=timeout parameter (&60 = standard)
fall through ↓

Core transmit and poll routine (XMIT)

Claims the TX semaphore (tx_ctrl_status) via ASL -- a busy-wait spinlock where carry=0 means the semaphore is held by another operation. Only after claiming the semaphore is the TX pointer copied to nmi_tx_block, ensuring the low-level transmit code sees a consistent pointer. Then calls the ADLC TX setup routine and polls the control byte for completion: bit 7 set = still busy (loop) bit 6 set = error (check escape or report) bit 6 clear = success (clean return) On error, checks for escape condition and handles retries. Two entry points: setup_tx_ptr_c0 (&8645) always uses the standard TXCB; tx_poll_core (&8651) is general-purpose.

On EntryAretry count (&FF = full retry)
Ytimeout parameter (&60 = standard)
On ExitAentry A (retry count, restored from stack)
X0
Y0
8651 .tx_poll_core←1← 8FE7 JSR
PHA ; Save retry count on stack
8652 TYA ; Transfer timeout to A
8653 PHA ; Save timeout on stack
8654 LDX #0 ; X=0 for (net_tx_ptr,X) indirect
8656 LDA (net_tx_ptr,x) ; Load TXCB byte 0 (control/status)
8658 .tx_retry←1← 868B BEQ
STA (net_tx_ptr,x) ; Write control byte to start TX
865A PHA ; Save control byte for retry
865B .tx_semaphore_spin←1← 865E BCC
ASL tx_ctrl_status ; Test TX semaphore (C=1 when free)
865E BCC tx_semaphore_spin ; Spin until semaphore released
8660 LDA net_tx_ptr ; Copy TX ptr lo to NMI block
8662 STA nmi_tx_block ; Store for NMI handler access
8664 LDA net_tx_ptr_hi ; Copy TX ptr hi to NMI block
8666 STA nmi_tx_block_hi ; Store for NMI handler access
8668 JSR trampoline_tx_setup ; Initiate ADLC TX via trampoline
866B .poll_txcb_status←1← 866D BMI
LDA (net_tx_ptr,x) ; Poll TXCB byte 0 for completion
866D BMI poll_txcb_status ; Bit 7 set: still busy, keep polling
866F ASL ; Shift bit 6 into bit 7 (error flag)
8670 BPL tx_success ; Bit 6 clear: success, clean return
8672 ASL ; Shift bit 5 into carry
8673 BEQ tx_not_listening ; Zero: fatal error, no escape
8675 JSR check_escape ; Check for user escape condition
8678 PLA ; Discard saved control byte
8679 TAX ; Save to X for retry delay
867A PLA ; Restore timeout parameter
867B TAY ; Back to Y
867C PLA ; Restore retry count
867D BEQ tx_not_listening ; No retries left: report error
867F SBC #1 ; Decrement retry count
8681 PHA ; Save updated retry count
8682 TYA ; Timeout to A for delay
8683 PHA ; Save timeout parameter
8684 TXA ; Control byte for delay duration
8685 .delay_1ms←2← 8686 BNE← 8689 BNE
DEX ; Inner delay loop
8686 BNE delay_1ms ; Spin until X=0
8688 DEY ; Outer delay loop
8689 BNE delay_1ms ; Continue delay
868B BEQ tx_retry ; ALWAYS branch
868D .tx_not_listening←2← 8673 BEQ← 867D BEQ
TAX ; Save error code in X
868E JMP nlistn ; Report 'Not listening' error
8691 .tx_success←1← 8670 BPL
PLA ; Discard saved control byte
8692 PLA ; Discard timeout parameter
8693 PLA ; Discard retry count
8694 RTS ; Return (success)

FILEV handler (OSFILE entry point)

Saves A/X/Y, copies the filename pointer from the parameter block to os_text_ptr, then uses GSINIT/GSREAD to parse the filename into &0FC5+. Sets fs_crc_lo/hi to point at the parsed filename buffer. Dispatches by function code A: A=&FF: load file (send_fs_examine at &86D1) A=&00: save file (filev_save at &8747) A=&01-&06: attribute operations (filev_attrib_dispatch at &8845) Other: restore_args_return (unsupported, no-op)

On EntryAfunction code (&FF=load, &00=save, &01-&06=attrs)
Xparameter block address low byte
Yparameter block address high byte
On ExitArestored
Xrestored
Yrestored
8695 .filev_handler
JSR save_fscv_args ; Load FILEV function code from A
fall through ↓

Copy filename pointer to os_text_ptr and parse

Copies the 2-byte filename pointer from (fs_options),Y into os_text_ptr (&F2/&F3), then falls through to parse_filename_gs to parse the filename via GSINIT/GSREAD into the &0E30 buffer.

8698 .copy_filename_ptr
LDY #1 ; Y=1: copy 2 bytes (high then low)
869A .file1←1← 86A0 BPL
LDA (fs_options),y ; Load filename ptr from control block
869C STA os_text_ptr,y ; Store to MOS text pointer (&F2/&F3)
869F DEY ; Next byte (descending)
86A0 BPL file1 ; Loop for both bytes
86A2 INY ; Y=1: offset past filename pointer
86A3 LDX #&ff ; X=&FF: parse all characters
86A5 CLC ; C=0: normal string parse entry
86A6 JSR gsinit ; Init string parsing via GSINIT
86A9 .quote1←1← 86B2 BCC
JSR gsread ; Read next character via GSREAD
86AC BCS tx_result_check ; C=1 from GSREAD: end of string reached
86AE INX ; Advance buffer index
86AF STA l0fc5,x ; Store parsed character to &0E30+X
86B2 BCC quote1 ; ALWAYS loop (GSREAD clears C on success) ALWAYS branch
86B4 .tx_result_check←1← 86AC BCS
LDA #&0d ; CR = &0D
86B6 STA l0fc6,x ; Store CR terminator at end of string
86B9 LDA #&c5 ; Point fs_crc_lo/hi at &0E30 parse buffer
86BB STA fs_crc_lo ; fs_crc_lo = &30
86BD LDA #&0f ; fs_crc_hi = &0E → buffer at &0E30
86BF STA fs_crc_hi ; Store high byte
86C1 LDA fs_last_byte_flag ; Recover function code from saved A
86C3 BPL saveop ; A >= 0: save (&00) or attribs (&01-&06)
86C5 CMP #&ff ; A=&FF? Only &FF is valid for load
86C7 BEQ loadop ; A=&FF: branch to load path
86C9 JMP restore_args_return ; Unknown negative code: no-op return
86CC .loadop←1← 86C7 BEQ
JSR copy_filename ; Copy parsed filename to cmd buffer
86CF LDY #2 ; Y=2: FS function code offset
fall through ↓

Send FS examine command

Sends FS command &03 (FCEXAM: examine file) to the fileserver. Sets &0F02=&03 and error pointer to '*'. Called for OSFILE &FF (load file) with the filename already in the command buffer. The FS reply contains load/exec addresses and file length which are used to set up the data transfer. The header URD field is repurposed to carry the Econet data port number (PLDATA=&92) for the subsequent block data transfer.

On EntryYFS function code (2=load, 5=examine)
XTX buffer extent
86D1 .send_fs_examine←1← 8D91 JSR
LDA #&92 ; Port &92 = PLDATA (data transfer port)
86D3 STA fs_cmd_urd ; Overwrite URD field with data port number
86D6 LDA #&2a ; A=&2A: error ptr for retry
86D8 JSR prepare_cmd_clv ; Build FS header (V=1: CLV path)
86DB LDY #6 ; Y=6: param block byte 6
86DD LDA (fs_options),y ; Byte 6: use file's own load address?
86DF BNE lodfil ; Non-zero: use FS reply address (lodfil)
86E1 JSR copy_load_addr_from_params ; Zero: copy caller's load addr first
86E4 JSR copy_reply_to_params ; Then copy FS reply to param block
86E7 BCC skip_lodfil ; Carry clear from prepare_cmd_clv: skip lodfil
86E9 .lodfil←1← 86DF BNE
JSR copy_reply_to_params ; Copy FS reply addresses to param block
86EC JSR copy_load_addr_from_params ; Then copy load addr from param block
86EF .skip_lodfil←1← 86E7 BCC
LDY #4 ; Compute end address = load + file length
86F1 .copy_load_end_addr←1← 86FC BNE
LDA fs_load_addr,x ; Load address byte
86F3 STA txcb_end,x ; Store as current transfer position
86F5 ADC fs_file_len,x ; Add file length byte
86F8 STA fs_work_4,x ; Store as end position
86FA INX ; Next address byte
86FB DEY ; Decrement byte counter
86FC BNE copy_load_end_addr ; Loop for all 4 address bytes
86FE SEC ; Adjust high byte for 3-byte length overflow
86FF SBC fs_file_len_3 ; Subtract 4th length byte from end addr
8702 STA fs_work_7 ; Store adjusted end address high byte
8704 JSR print_file_info ; Display file info after FS reply
8707 JSR send_data_blocks ; Transfer file data in &80-byte blocks
870A LDX #2 ; Copy 3-byte file length to FS reply cmd buffer
870C .floop←1← 8713 BPL
LDA fs_file_len_3,x ; Load file length byte
870F STA fs_cmd_data,x ; Store in FS command data buffer
8712 DEX ; Next byte (count down)
8713 BPL floop ; Loop for 3 bytes (X=2,1,0)
8715 BMI set_star_reply_port ; ALWAYS branch

Send file data in multi-block chunks

Repeatedly sends &80-byte (128-byte) blocks of file data to the fileserver using command &7F. Compares current address (&C8-&CB) against end address (&B4-&B7) via compare_addresses, and loops until the entire file has been transferred. Each block is sent via send_to_fs_star.

8717 .send_data_blocks←2← 8707 JSR← 8A71 JSR
JSR compare_addresses ; Compare two 4-byte addresses
871A BEQ return_lodchk ; Addresses match: transfer complete
871C LDA #&92 ; Port &92 for data block transfer
871E STA txcb_port ; Store port to TXCB command byte
8720 .send_block_loop←1← 873C BNE
LDX #3 ; Set up next &80-byte block for transfer
8722 .copy_block_addrs←1← 872B BPL
LDA txcb_end,x ; Swap: current addr -> source, end -> current
8724 STA txcb_start,x ; Source addr = current position
8726 LDA fs_work_4,x ; Load end address byte
8728 STA txcb_end,x ; Dest = end address (will be clamped)
872A DEX ; Next address byte
872B BPL copy_block_addrs ; Loop for all 4 bytes
872D LDA #&7f ; Command &7F = data block transfer
872F STA txcb_ctrl ; Store to TXCB control byte
8731 JSR send_to_fs_star ; Send this block to the fileserver
8734 LDY #3 ; Y=3: compare 4 bytes (3..0)
8736 .lodchk←1← 873F BPL
LDA txcb_end,y ; Compare current vs end address (4 bytes)
8739 EOR fs_work_4,y ; XOR with end address byte
873C BNE send_block_loop ; Not equal: more blocks to send
873E DEY ; Next byte
873F BPL lodchk ; Loop for all 4 address bytes
8741 .return_lodchk←1← 871A BEQ
RTS ; All equal: transfer complete
8742 .saveop←1← 86C3 BPL
BEQ filev_save ; A=0: SAVE handler
8744 JMP filev_attrib_dispatch ; A!=0: attribute dispatch (A=1-6)

OSFILE save handler (A=&00)

Copies 4-byte load/exec/length addresses from the parameter block to the FS command buffer, along with the filename. Sends FS command &91 with function &14 to initiate the save, then calls print_file_info to display the filename being saved. Handles both host and Tube-based data sources. When receiving the save acknowledgement, the RX low pointer is incremented by 1 to skip the command code (CC) byte, which indicates the FS type and must be preserved. N.B. this assumes the RX buffer does not cross a page boundary.

8747 .filev_save←1← 8742 BEQ
LDX #4 ; Process 4 address bytes (load/exec/start/end)
8749 LDY #&0e ; Y=&0E: start from end-address in param block
874B .savsiz←1← 8765 BNE
LDA (fs_options),y ; Read end-address byte from param block
874D STA port_ws_offset,y ; Save to port workspace for transfer setup
8750 JSR sub_4_from_y ; Y = Y-4: point to start-address byte
8753 SBC (fs_options),y ; end - start = transfer length byte
8755 STA fs_cmd_csd,y ; Store length byte in FS command buffer
8758 PHA ; Save length byte for param block restore
8759 LDA (fs_options),y ; Read corresponding start-address byte
875B STA port_ws_offset,y ; Save to port workspace
875E PLA ; Restore length byte from stack
875F STA (fs_options),y ; Replace param block entry with length
8761 JSR add_5_to_y ; Y = Y+5: advance to next address group
8764 DEX ; Decrement address byte counter
8765 BNE savsiz ; Loop for all 4 address bytes
8767 LDY #9 ; Copy load/exec addresses to FS command buffer
8769 .copy_save_params←1← 876F BNE
LDA (fs_options),y ; Read load/exec address byte from params
876B STA fs_cmd_csd,y ; Copy to FS command buffer
876E DEY ; Next byte (descending)
876F BNE copy_save_params ; Loop for bytes 9..1
8771 LDA #&91 ; Port &91 for save command
8773 STA fs_cmd_urd ; Overwrite URD field with port number
8776 STA fs_error_ptr ; Save port &91 for flow control ACK
8778 LDX #&0b ; Append filename at offset &0B in cmd buffer
877A JSR copy_string_to_cmd ; Append filename to cmd buffer at offset X
877D LDY #1 ; Y=1: function code for save
877F LDA #&14 ; A=&14: FS function code for SAVE
8781 JSR prepare_cmd_clv ; Build header and send FS save command
8784 JSR print_file_info ; Display filename being saved
8787 .save_csd_display
LDA fs_cmd_data ; Load CSD from FS reply
878A JSR transfer_file_blocks ; Transfer file data blocks to server
878D .set_star_reply_port←1← 8715 BMI
LDA #&2a ; A=&2A: error ptr for FS retry
878F STA fs_error_ptr ; Store error ptr for TX poll
8791 .send_fs_reply
JSR send_fs_reply_cmd ; Send FS reply acknowledgement
8794 .skip_catalogue_msg
STX fs_reply_cmd ; Store reply command for attr decode
8797 LDY #&0e ; Y=&0E: access byte offset in param block
8799 LDA fs_cmd_data ; Load access byte from FS reply
879C JSR decode_attribs_5bit ; Convert FS access to BBC attribute format
879F BEQ direct_attr_copy ; Z=1: first byte, use A directly
87A1 .copy_attr_loop←1← 87A9 BNE
LDA fs_reply_data,y ; Load attribute byte from FS reply
87A4 .direct_attr_copy←1← 879F BEQ
STA (fs_options),y ; Store decoded access in param block
87A6 INY ; Next attribute byte
87A7 CPY #&12 ; Copied all 4 bytes? (Y=&0E..&11)
87A9 BNE copy_attr_loop ; Loop for 4 attribute bytes
87AB JMP restore_args_return ; Restore A/X/Y and return to caller

Copy load address from parameter block

Copies 4 bytes from (fs_options)+2..5 (the load address in the OSFILE parameter block) to &AE-&B3 (local workspace).

87AE .copy_load_addr_from_params←2← 86E1 JSR← 86EC JSR
LDY #5 ; Start at offset 5 (top of 4-byte addr)
87B0 .lodrl1←1← 87B8 BCS
LDA (fs_options),y ; Read from parameter block
87B2 STA work_ae,y ; Store to local workspace
87B5 DEY ; Next byte (descending)
87B6 CPY #2 ; Copy offsets 5,4,3,2 (4 bytes)
87B8 BCS lodrl1 ; Loop while Y >= 2
87BA RTS ; Return

Copy FS reply data to parameter block

Copies bytes from the FS command reply buffer (&0F02+) into the parameter block at (fs_options)+2..13. Used to fill in the OSFILE parameter block with information returned by the fileserver.

On EntryXattribute byte (stored first at offset &0D)
87BB .copy_reply_to_params←2← 86E4 JSR← 86E9 JSR
LDY #&0d ; Start at offset &0D (top of range)
87BD TXA ; First store uses X (attrib byte)
87BE .lodrl2←1← 87C6 BCS
STA (fs_options),y ; Write to parameter block
87C0 LDA fs_cmd_urd,y ; Read next byte from reply buffer
87C3 DEY ; Next byte (descending)
87C4 CPY #2 ; Copy offsets &0D down to 2
87C6 BCS lodrl2 ; Loop until offset 2 reached
87C8 RTS ; Return to caller

Multi-block file data transfer

Manages the transfer of file data in chunks between the local machine and the fileserver. Entry conditions: WORK (&B0-&B3) and WORK+4 (&B4-&B7) hold the low and high addresses of the data being sent/received. Sets up source (&C4-&C7) and destination (&C8-&CB) from the FS reply, sends &80-byte (128-byte) blocks with command &91, and continues until all data has been transferred. Handles address overflow and Tube co-processor transfers. For SAVE, WORK+8 holds the port on which to receive byte-level ACKs for each data block (flow control).

87C9 .transfer_file_blocks←2← 878A JSR← 8A6C JSR
PHA ; Save FS command byte on stack
87CA JSR compare_addresses ; Compare two 4-byte addresses
87CD BEQ restore_ay_return ; Addresses equal: nothing to transfer
87CF .next_block←1← 881E BNE
LDX #0 ; X=0: clear hi bytes of block size
87D1 LDY #4 ; Y=4: process 4 address bytes
87D3 STX fs_reply_cmd ; Clear block size hi byte 1
87D6 STX fs_load_vector ; Clear block size hi byte 2
87D9 CLC ; CLC for ADC in loop
87DA .block_addr_loop←1← 87E7 BNE
LDA fs_load_addr,x ; Source = current position
87DC STA txcb_start,x ; Store source address byte
87DE ADC fs_func_code,x ; Add block size to current position
87E1 STA txcb_end,x ; Store dest address byte
87E3 STA fs_load_addr,x ; Advance current position
87E5 INX ; Next address byte
87E6 DEY ; Decrement byte counter
87E7 BNE block_addr_loop ; Loop for all 4 bytes
87E9 BCS clamp_dest_setup ; Carry: address overflowed, clamp
87EB SEC ; SEC for SBC in overshoot check
87EC .savchk←1← 87F4 BNE
LDA fs_load_addr,y ; Check if new pos overshot end addr
87EF SBC fs_work_4,y ; Subtract end address byte
87F2 INY ; Next byte
87F3 DEX ; Decrement counter
87F4 BNE savchk ; Loop for 4-byte comparison
87F6 BCC send_block ; C=0: no overshoot, proceed
87F8 .clamp_dest_setup←1← 87E9 BCS
LDX #3 ; Overshot: clamp dest to end address
87FA .clamp_dest_addr←1← 87FF BPL
LDA fs_work_4,x ; Load end address byte
87FC STA txcb_end,x ; Replace dest with end address
87FE DEX ; Next byte
87FF BPL clamp_dest_addr ; Loop for all 4 bytes
8801 .send_block←1← 87F6 BCC
PLA ; Recover original FS command byte
8802 PHA ; Re-push for next iteration
8803 PHP ; Save processor flags (C from cmp)
8804 STA txcb_port ; Store command byte in TXCB
8806 LDA #&80 ; 128-byte block size for data transfer
8808 STA txcb_ctrl ; Store size in TXCB control byte
880A JSR setup_tx_ptr_c0 ; Point TX ptr to &00C0; transmit
880D LDA fs_error_ptr ; ACK port for flow control
880F JSR init_tx_ctrl_port ; Set reply port for ACK receive
8812 PLP ; Restore flags (C=overshoot status)
8813 BCS restore_ay_return ; C=1: all data sent (overshot), done
8815 LDA #&91 ; Command &91 = data block transfer
8817 STA txcb_port ; Store command &91 in TXCB
8819 LDA #&2a ; A=&2A: error ptr for retry
881B JSR send_to_fs ; Transmit block and wait (BRIANX)
881E BNE next_block ; More blocks? Loop back
fall through ↓

FSCV 1: EOF handler

Checks whether a file handle has reached end-of-file. Converts the handle via handle_to_mask_clc, tests the result against the EOF hint byte (&0E07). If the hint bit is clear, returns X=0 immediately (definitely not at EOF — no network call needed). If the hint bit is set, sends FS command &11 (FCEOF) to query the fileserver for definitive EOF status. Returns X=&FF if at EOF, X=&00 if not. This two-level check avoids an expensive network round-trip when the file is known to not be at EOF.

On EntryXfile handle to check
On ExitX&FF if at EOF, &00 if not
8820 .fscv_1_eof
PHA ; Save A (function code)
8821 STY fs_block_offset ; Save handle for EOF check
8823 JSR handle_to_mask_clc ; Convert handle to bitmask in A
8826 TYA ; Y = handle bitmask from conversion
8827 AND fs_eof_flags ; Local hint: is EOF possible for this handle?
882A TAX ; X = result of AND (0 = not at EOF)
882B BEQ restore_ay_return ; Hint clear: definitely not at EOF
882D PHA ; Save bitmask for clear_fs_flag
882E STY fs_cmd_data ; Handle byte in FS command buffer
8831 LDY #&11 ; Y=&11: FS function code FCEOF Y=function code for HDRFN
8833 LDX #1 ; X=preserved through header build
8835 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8838 PLA ; Restore bitmask
8839 LDX fs_cmd_data ; FS reply: non-zero = at EOF
883C BNE restore_ay_return ; At EOF: skip flag clear
883E JSR clear_fs_flag ; Not at EOF: clear the hint bit
8841 .restore_ay_return←4← 87CD BEQ← 8813 BCS← 882B BEQ← 883C BNE
PLA ; Restore A
8842 LDY fs_block_offset ; Restore Y
8844 RTS ; Return; X=0 (not EOF) or X=&FF (EOF)

FILEV attribute dispatch (A=1-6)

Dispatches OSFILE operations by function code: A=1: write catalogue info (load/exec/length/attrs) — FS &14 A=2: write load address only A=3: write exec address only A=4: write file attributes A=5: read catalogue info, returns type in A — FS &12 A=6: delete named object — FS &14 (FCDEL) A>=7: falls through to restore_args_return (no-op) Each handler builds the appropriate FS command, sends it to the fileserver, and copies the reply into the parameter block. The control block layout uses dual-purpose fields: the 'data start' field doubles as 'length' and 'data end' doubles as 'protection' depending on whether reading or writing attrs.

On EntryAfunction code (1-6)
On ExitAobject type (A=5 read info) or restored
8845 .filev_attrib_dispatch←1← 8744 JMP
STA fs_cmd_data ; Store function code in FS cmd buffer
8848 CMP #6 ; A=6? (delete)
884A BEQ cha6 ; Yes: jump to delete handler
884C BCS check_attrib_result ; A>=7: unsupported, fall through to return
884E CMP #5 ; A=5? (read catalogue info)
8850 BEQ cha5 ; Yes: jump to read info handler
8852 CMP #4 ; A=4? (write attributes only)
8854 BEQ cha4 ; Yes: jump to write attrs handler
8856 CMP #1 ; A=1? (write all catalogue info)
8858 BEQ get_file_protection ; Yes: jump to write-all handler
885A ASL ; A=2 or 3: convert to param block offset
885B ASL ; A*4: 2->8, 3->12
885C TAY ; Y = A*4
885D JSR sub_3_from_y ; Y = A*4 - 3 (load addr offset for A=2)
8860 LDX #3 ; X=3: copy 4 bytes
8862 .chalp1←1← 8869 BPL
LDA (fs_options),y ; Load address byte from param block
8864 STA fs_func_code,x ; Store to FS cmd data area
8867 DEY ; Next source byte (descending)
8868 DEX ; Next dest byte
8869 BPL chalp1 ; Loop for 4 bytes
886B LDX #5 ; X=5: data extent for filename copy
886D BNE copy_filename_to_cmd ; ALWAYS branch
886F .get_file_protection←1← 8858 BEQ
JSR decode_attribs_6bit ; A=1: encode protection from param block
8872 STA fs_file_attrs ; Store encoded attrs at &0F0E
8875 LDY #9 ; Y=9: source offset in param block
8877 LDX #8 ; X=8: dest offset in cmd buffer
8879 .chalp2←1← 8880 BNE
LDA (fs_options),y ; Load byte from param block
887B STA fs_cmd_data,x ; Store to FS cmd buffer
887E DEY ; Next source byte (descending)
887F DEX ; Next dest byte
8880 BNE chalp2 ; Loop until X=0 (8 bytes copied)
8882 LDX #&0a ; X=&0A: data extent past attrs+addrs
8884 .copy_filename_to_cmd←2← 886D BNE← 88A2 BNE
JSR copy_string_to_cmd ; Append filename to cmd buffer
8887 LDY #&13 ; Y=&13: fn code for FCSAVE (write attrs)
8889 BNE send_fs_cmd_v1 ; ALWAYS branch to send command ALWAYS branch
888B .cha6←1← 884A BEQ
JSR copy_filename ; A=6: copy filename (delete)
888E LDY #&14 ; Y=&14: fn code for FCDEL (delete)
8890 .send_fs_cmd_v1←1← 8889 BNE
BIT tx_ctrl_upper ; Set V=1 (BIT trick: &B3 has bit 6 set)
8893 JSR prepare_fs_cmd_v ; Send via prepare_fs_cmd_v (V=1 path)
8896 .check_attrib_result←1← 884C BCS
BCS attrib_error_exit ; C=1: &D6 not-found, skip to return
8898 BCC argsv_check_return ; C=0: success, copy reply to param block ALWAYS branch
889A .cha4←1← 8854 BEQ
JSR decode_attribs_6bit ; A=4: encode attrs from param block
889D STA fs_func_code ; Store encoded attrs at &0F06
88A0 LDX #2 ; X=2: data extent (1 attr byte + fn)
88A2 BNE copy_filename_to_cmd ; ALWAYS branch to append filename ALWAYS branch
88A4 .cha5←1← 8850 BEQ
LDX #1 ; X=1: filename only, no data extent
88A6 JSR copy_string_to_cmd ; Copy filename to cmd buffer
88A9 LDY #&12 ; Y=&12: fn code for FCEXAM (read info) Y=function code for HDRFN
88AB JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
88AE LDA fs_obj_type ; Save object type from FS reply
88B1 STX fs_obj_type ; Clear reply byte (X=0 on success) X=0 on success, &D6 on not-found
88B4 STX fs_len_clear ; Clear length high byte in reply
88B7 JSR decode_attribs_5bit ; Decode 5-bit access byte from FS reply
88BA LDX fs_cmd_data ; Load FS command code from reply
88BD BEQ argsv_zero_length ; Zero: no attribute data returned
88BF LDY #&0e ; Y=&0E: attrs offset in param block
88C1 STA (fs_options),y ; Store decoded attrs at param block +&0E
88C3 DEY ; Y=&0D: start copy below attrs Y=&0d
88C4 LDX #&0c ; X=&0C: copy from reply offset &0C down
88C6 .copy_fs_reply_to_cb←1← 88CD BNE
LDA fs_cmd_data,x ; Load reply byte (load/exec/length)
88C9 STA (fs_options),y ; Store to param block
88CB DEY ; Next dest byte (descending)
88CC DEX ; Next source byte
88CD BNE copy_fs_reply_to_cb ; Loop until X=0 (12 bytes copied)
88CF INX ; X=0 -> X=2 for length high copy
88D0 INX ; INX again: X=2
88D1 LDY #&11 ; Y=&11: length high dest in param block
88D3 .cha5lp←1← 88DA BPL
LDA fs_access_level,x ; Load length high byte from reply
88D6 STA (fs_options),y ; Store to param block
88D8 DEY ; Next dest byte (descending)
88D9 DEX ; Next source byte
88DA BPL cha5lp ; Loop for 3 length-high bytes
88DC LDX fs_cmd_data ; Reload FS command code
88DF .argsv_zero_length←1← 88BD BEQ
TXA ; A = command code for exit test
88E0 .attrib_error_exit←1← 8896 BCS
BPL restore_xy_return ; A>=0: branch to restore_args_return
fall through ↓

ARGSV handler (OSARGS entry point)

A=0, Y=0: return filing system number (10 = network FS) A=0, Y>0: read file pointer via FS command &0A (FCRDSE) A=1, Y>0: write file pointer via FS command &14 (FCWRSE) A>=3 (ensure): silently returns -- NFS has no local write buffer to flush, since all data is sent to the fileserver immediately The handle in Y is converted via handle_to_mask_clc. For writes (A=1), the carry flag from the mask conversion is used to branch to save_args_handle, which records the handle for later use.

On EntryAfunction code (0=query, 1=write ptr, >=3=ensure)
Yfile handle (0=FS-level query, >0=per-file)
On ExitAfiling system number if A=0/Y=0 query, else restored
Xrestored
Yrestored
88E2 .argsv_handler
JSR save_fscv_args ; Save A/X/Y registers for later restore
88E5 CMP #3 ; Function >= 3?
88E7 BCS restore_args_return ; A>=3 (ensure/flush): no-op for NFS
88E9 CPY #0 ; Test file handle
88EB BEQ argsv_dispatch_a ; Y=0: FS-level query, not per-file
88ED JSR handle_to_mask_clc ; Convert handle to bitmask
88F0 STY fs_cmd_data ; Store bitmask as first cmd data byte
88F3 LSR ; LSR splits A: C=1 means write (A=1)
88F4 STA fs_func_code ; Store function code to cmd data byte 2
88F7 BCS save_args_handle ; C=1: write path, copy ptr from caller
88F9 LDY #&0c ; Y=&0C: FCRDSE (read sequential pointer) Y=function code for HDRFN
88FB LDX #2 ; X=2: 3 data bytes in command X=preserved through header build
88FD JSR prepare_fs_cmd ; Build and send FS command Prepare FS command buffer (12 references)
8900 STA fs_last_byte_flag ; Clear last-byte flag on success A=0 on success (from build_send_fs_cmd)
8902 LDX fs_options ; X = saved control block ptr low
8904 LDY #2 ; Y=2: copy 3 bytes of file pointer
8906 STA zp_work_3,x ; Zero high byte of 3-byte pointer
8908 .copy_fileptr_reply←1← 890F BPL
LDA fs_cmd_data,y ; Read reply byte from FS cmd data
890B STA zp_work_2,x ; Store to caller's control block
890D DEX ; Next byte (descending)
890E DEY ; Next source byte
890F BPL copy_fileptr_reply ; Loop for all 3 bytes
8911 .argsv_check_return←1← 8898 BCC
BCC restore_args_return ; C=0 (read): return to caller
8913 .save_args_handle←1← 88F7 BCS
TYA ; Save bitmask for set_fs_flag later
8914 PHA ; Push bitmask
8915 LDY #3 ; Y=3: copy 4 bytes of file pointer
8917 .copy_fileptr_to_cmd←1← 891E BPL
LDA zp_work_3,x ; Read caller's pointer byte
8919 STA fs_data_count,y ; Store to FS command data area
891C DEX ; Next source byte
891D DEY ; Next destination byte
891E BPL copy_fileptr_to_cmd ; Loop for all 4 bytes
8920 LDY #&0d ; Y=&0D: FCWRSE (write sequential pointer) Y=function code for HDRFN
8922 LDX #5 ; X=5: 6 data bytes in command X=preserved through header build
8924 JSR prepare_fs_cmd ; Build and send FS command Prepare FS command buffer (12 references)
8927 STX fs_last_byte_flag ; Save not-found status from X X=0 on success, &D6 on not-found
8929 PLA ; Recover bitmask for EOF hint update
892A JSR set_fs_flag ; Set EOF hint bit for this handle
fall through ↓

Restore arguments and return

Common exit point for FS vector handlers. Reloads A from fs_last_byte_flag (&BD), X from fs_options (&BB), and Y from fs_block_offset (&BC) — the values saved at entry by save_fscv_args — and returns to the caller.

892D .restore_args_return←8← 86C9 JMP← 87AB JMP← 88E7 BCS← 8911 BCC← 893C BNE← 89A0 BCC← 89F6 JMP← 8D00 JMP
LDA fs_last_byte_flag ; A = saved function code / command
892F .restore_xy_return←5← 88E0 BPL← 8939 BNE← 8948 BPL← 8970 BCS← 8984 BNE
LDX fs_options ; X = saved control block ptr low
8931 LDY fs_block_offset ; Y = saved control block ptr high
8933 RTS ; Return to MOS with registers restored
8934 .argsv_dispatch_a←1← 88EB BEQ
TAY ; Transfer A to Y for test
8935 BNE halve_args_a ; Non-zero: halve A
8937 LDA #5 ; A=5: default FS number
8939 BNE restore_xy_return ; ALWAYS branch
893B .halve_args_a←1← 8935 BNE
LSR ; Shared: halve A (A=0 or A=2 paths)
893C BNE restore_args_return ; Return with A = FS number or 1
893E .osarg1←1← 8944 BPL
LDA fs_context_hi,y ; Copy command context to caller's block
8941 STA (fs_options),y ; Store to caller's parameter block
8943 DEY ; Next byte (descending)
8944 BPL osarg1 ; Loop until all bytes copied
fall through ↓

Return with A=0 via register restore

Loads A=0 and branches (always taken) to the common register restore exit at restore_args_return. Used as a shared exit point by ARGSV, FINDV, and GBPBV when an operation is unsupported or should return zero.

8946 .return_a_zero←3← 8956 BNE← 8A96 JMP← 8B33 JMP
LDA #0 ; A=operation (0=close, &40=read, &80=write, &C0=R/W)
8948 BPL restore_xy_return ; ALWAYS branch

FINDV handler (OSFIND entry point)

A=0: close file -- delegates to close_handle (&8986) A>0: open file -- modes &40=read, &80=write/update, &C0=read/write For open: the mode byte is converted to the fileserver's two-flag format by flipping bit 7 (EOR #&80) and shifting. This produces Flag 1 (read/write direction) and Flag 2 (create/existing), matching the fileserver protocol. After a successful open, the new handle's bit is OR'd into the EOF hint byte (marks it as "might be at EOF, query the server", on_entry={"a": "operation (0=close, &40=read, &80=write, &C0=R/W)", "x": "filename pointer low (open)", "y": "file handle (close) or filename pointer high (open)"}, on_exit={"a": "handle on open, 0 on close-all, restored on close-one", "x": "restored", "y": "restored"}) number tracking byte for the byte-stream protocol.

On EntryAoperation (0=close, &40=read, &80=write, &C0=R/W)
Xfilename pointer low (open)
Yfile handle (close) or filename pointer high (open)
On ExitAhandle on open, 0 on close-all, restored on close-one
Xrestored
Yrestored
894A .findv_handler
JSR save_fscv_args ; Save A/X/Y and set up pointers
894D SEC ; SEC distinguishes open (A>0) from close
894E JSR handle_to_mask ; Convert file handle to bitmask (Y2FS)
8951 TAX ; A=preserved
8952 BEQ close_handle ; A=0: close file(s)
8954 AND #&3f ; Valid open modes: &40, &80, &C0 only
8956 BNE return_a_zero ; Invalid mode bits: return
8958 TXA ; A = original mode byte
8959 EOR #&80 ; Convert MOS mode to FS protocol flags
895B ASL ; ASL: shift mode bits left
895C STA fs_cmd_data ; Flag 1: read/write direction
895F ROL ; ROL: Flag 2 into bit 0
8960 STA fs_func_code ; Flag 2: create vs existing file
8963 LDX #2 ; X=2: copy after 2-byte flags
8965 JSR copy_string_to_cmd ; Copy filename to FS command buffer
8968 LDY #6 ; Y=6: FS function code FCOPEN
896A BIT tx_ctrl_upper ; Set V flag from l83b3 bit 6
896D JSR prepare_fs_cmd_v ; Build and send FS open command
8970 BCS restore_xy_return ; Error: restore and return
8972 LDA fs_cmd_data ; Load reply handle from FS
8975 TAX ; X = new file handle
8976 JSR set_fs_flag ; Set EOF hint + sequence bits
8979 TXA ; A = handle bitmask from set_fs_flag
897A ORA fs_sequence_nos ; Merge handle into sequence tracking
897D STA fs_sequence_nos ; Store updated sequence tracking
8980 TXA ; A=single-bit bitmask
8981 JSR mask_to_handle ; Convert bitmask to handle number (FS2A)
8984 BNE restore_xy_return ; ALWAYS branch to restore and return

Close file handle(s) (CLOSE)

  Y=0: close all files — first calls OSBYTE &77 (close SPOOL and
       EXEC files) to coordinate with the MOS before sending the
       close-all command to the fileserver. This ensures locally-
       managed file handles are released before the server-side
       handles are invalidated, preventing the MOS from writing to
       a closed spool file.
  Y>0: close single handle — sends FS close command and clears
       the handle's bit in both the EOF hint byte and the sequence
       number tracking byte.
On EntryYfile handle (0=close all, >0=close single)
8986 .close_handle←1← 8952 BEQ
TYA ; A = handle (Y preserved in A) Y=preserved
8987 BNE close_single_handle ; Y>0: close single file
8989 LDA #osbyte_close_spool_exec ; Close SPOOL/EXEC before FS close-all
898B JSR osbyte ; Close any *SPOOL and *EXEC files
898E LDY #0 ; Y=0: close all handles on server
8990 .close_single_handle←1← 8987 BNE
STY fs_cmd_data ; Handle byte in FS command buffer
8993 LDX #1 ; X=preserved through header build
8995 LDY #7 ; Y=function code for HDRFN
8997 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
899A LDA fs_cmd_data ; Reply handle for flag update
899D JSR set_fs_flag ; Update EOF/sequence tracking bits
89A0 .close_opt_return←1← 89C9 BCC
BCC restore_args_return ; C=0: restore A/X/Y and return
fall through ↓

FSCV 0: *OPT handler (OPTION)

Handles *OPT X,Y to set filing system options: *OPT 1,Y (Y=0/1): set local user option in &0E06 (OPT) *OPT 4,Y (Y=0-3): set boot option via FS command &16 (FCOPT) Other combinations generate error &CB (OPTER: "bad option").

On EntryXoption number (1 or 4)
Yoption value
89A2 .fscv_0_opt
CPX #4 ; Is it *OPT 4,Y?
89A4 BNE gbpbv_func_dispatch ; No: check for *OPT 1
89A6 CPY #4 ; Y must be 0-3 for boot option
89A8 BCC optl1 ; Y < 4: valid boot option
89AA .gbpbv_func_dispatch←1← 89A4 BNE
CPX #1 ; X=1? (*OPT 1: set messaging)
89AC BNE opter1 ; Not *OPT 1: bad option error
89AE CPY #2 ; Y < 2? (valid: 0=off, 1=on)
89B0 BCS opter1 ; Y >= 2: bad option value error
89B2 .set_messages_flag
STY fs_messages_flag ; Set local messages flag (*OPT 1,Y)
89B5 BCC opt_return ; Return via restore_args_return
89B7 .opter1←2← 89AC BNE← 89B0 BCS
LDA #7 ; Error index 7 (Bad option)
89B9 JMP nlisne ; Generate BRK error
89BC .optl1←1← 89A8 BCC
STY fs_cmd_data ; Boot option value in FS command
89BF LDY #&16 ; Y=&16: FS function code FCOPT Y=function code for HDRFN
89C1 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
89C4 LDY fs_block_offset ; Restore Y from saved value
89C6 STY fs_boot_option ; Cache boot option locally
89C9 .opt_return←1← 89B5 BCC
BCC close_opt_return ; Return via restore_args_return
89CB .adjust_addrs_9←1← 8A8A JSR
LDY #9 ; Y=9: adjust 9 address bytes
89CD JSR adjust_addrs_clc ; Adjust with carry clear
89D0 .adjust_addrs_1←1← 8B7A JSR
LDY #1 ; Y=1: adjust 1 address byte
89D2 .adjust_addrs_clc←1← 89CD JSR
CLC ; C=0 for address adjustment
fall through ↓

Bidirectional 4-byte address adjustment

Adjusts a 4-byte value in the parameter block at (fs_options)+Y: If fs_load_addr_2 (&B2) is positive: adds fs_lib_handle+X values If fs_load_addr_2 (&B2) is negative: subtracts fs_lib_handle+X Starting offset X=&FC means it reads from &0E06-&0E09 area. Used to convert between absolute and relative file positions.

On EntryYstarting offset into (fs_options) parameter block
On ExitAcorrupted (last adjusted byte)
X0
Yentry Y + 4
89D3 .adjust_addrs←2← 8A90 JSR← 8B86 JSR
LDX #&fc ; X=&FC: index into &0E06 area (wraps to 0)
89D5 .adjust_addr_byte←1← 89E8 BNE
LDA (fs_options),y ; Load byte from param block
89D7 BIT fs_load_addr_2 ; Test sign of adjustment direction
89D9 BMI subtract_adjust ; Negative: subtract instead
89DB ADC fs_cmd_context,x ; Add adjustment value
89DE JMP gbpbx ; Skip to store result
89E1 .subtract_adjust←1← 89D9 BMI
SBC fs_cmd_context,x ; Subtract adjustment value
89E4 .gbpbx←1← 89DE JMP
STA (fs_options),y ; Store adjusted byte back
89E6 INY ; Next param block byte
89E7 INX ; Next adjustment byte (X wraps &FC->&00)
89E8 BNE adjust_addr_byte ; Loop 4 times (X=&FC,&FD,&FE,&FF,done)
89EA RTS ; Return (unsupported function)

GBPBV handler (OSGBPB entry point)

A=1-4: file read/write operations (handle-based) A=5-8: info queries (disc title, current dir, lib, filenames) Calls 1-4 are standard file data transfers via the fileserver. Calls 5-8 were a late addition to the MOS spec and are the only NFS operations requiring Tube data transfer -- described in the original source as "untidy but useful in theory." The data format uses length-prefixed strings (<name length><object name>) rather than the CR-terminated strings used elsewhere in the FS.

On EntryAcall number (1-8)
Xparameter block address low byte
Yparameter block address high byte
On ExitA0 after FS operation, else restored
Xrestored
Yrestored
89EB .gbpbv_handler
JSR save_fscv_args ; Save A/X/Y to FS workspace
89EE TAX ; X = call number for range check
89EF BEQ gbpbx0 ; A=0: invalid, restore and return
89F1 DEX ; Convert to 0-based (A=0..7)
89F2 CPX #8 ; Range check: must be 0-7
89F4 BCC gbpbx1 ; In range: continue to handler
89F6 .gbpbx0←1← 89EF BEQ
JMP restore_args_return ; Out of range: restore args and return
89F9 .gbpbx1←1← 89F4 BCC
TXA ; Recover 0-based function code
89FA LDY #0 ; Y=0: param block byte 0 (file handle)
89FC PHA ; Save function code on stack
89FD CMP #4 ; A>=4: info queries, dispatch separately
89FF BCC gbpbe1 ; A<4: file read/write operations
8A01 JMP osgbpb_info ; Dispatch to OSGBPB 5-8 info handler
8A04 .gbpbe1←1← 89FF BCC
LDA (fs_options),y ; Get file handle from param block byte 0
8A06 JSR handle_to_mask_a ; Convert handle to bitmask for EOF flags
8A09 STY fs_cmd_data ; Store handle in FS command data
8A0C LDY #&0b ; Y=&0B: start at param block byte 11
8A0E LDX #6 ; X=6: copy 6 bytes of transfer params
8A10 .gbpbf1←1← 8A1C BNE
LDA (fs_options),y ; Load param block byte
8A12 STA fs_func_code,x ; Store to FS command buffer at &0F06+X
8A15 DEY ; Previous param block byte
8A16 CPY #8 ; Skip param block offset 8 (the handle)
8A18 BNE gbpbf2 ; Not at handle offset: continue
8A1A DEY ; Extra DEY to skip handle byte
8A1B .gbpbf2←1← 8A18 BNE
DEX ; Decrement copy counter
8A1C BNE gbpbf1 ; Loop for all 6 bytes
8A1E PLA ; Recover function code from stack
8A1F LSR ; LSR: odd=read (C=1), even=write (C=0)
8A20 PHA ; Save function code again (need C later)
8A21 BCC gbpbl1 ; Even (write): X stays 0
8A23 INX ; Odd (read): X=1
8A24 .gbpbl1←1← 8A21 BCC
STX fs_func_code ; Store FS direction flag
8A27 LDY #&0b ; Y=&0B: command data extent
8A29 LDX #&91 ; Command &91=put, &92=get
8A2B PLA ; Recover function code
8A2C PHA ; Save again for later direction check
8A2D BEQ gbpb_write_path ; Even (write): keep &91 and Y=&0B
8A2F LDX #&92 ; Odd (read): use &92 (get) instead
8A31 DEY ; Read: one fewer data byte in command Y=&0a
8A32 .gbpb_write_path←1← 8A2D BEQ
STX fs_cmd_urd ; Store port to FS command URD field
8A35 STX fs_error_ptr ; Save port for error recovery
8A37 LDX #8 ; X=8: command data bytes
8A39 LDA fs_cmd_data ; Load handle from FS command data
8A3C JSR prepare_cmd_with_flag ; Build FS command with handle+flag
8A3F LDA fs_load_addr_3 ; Save seq# for byte-stream flow control
8A41 STA fs_sequence_nos ; Store to FS sequence number workspace
8A44 LDX #4 ; X=4: copy 4 address bytes
8A46 .gbpbl3←1← 8A5A BNE
LDA (fs_options),y ; Set up source/dest from param block
8A48 STA addr_work,y ; Store as source address
8A4B STA txcb_pos,y ; Store as current transfer position
8A4E JSR add_4_to_y ; Skip 4 bytes to reach transfer length
8A51 ADC (fs_options),y ; Dest = source + length
8A53 STA addr_work,y ; Store as end address
8A56 JSR sub_3_from_y ; Back 3 to align for next iteration
8A59 DEX ; Decrement byte counter
8A5A BNE gbpbl3 ; Loop for all 4 address bytes
8A5C INX ; X=1 after loop
8A5D .gbpbf3←1← 8A64 BPL
LDA fs_cmd_csd,x ; Copy CSD data to command buffer
8A60 STA fs_func_code,x ; Store at &0F06+X
8A63 DEX ; Decrement counter
8A64 BPL gbpbf3 ; Loop for X=1,0
8A66 PLA ; Odd (read): send data to FS first
8A67 BNE gbpb_read_path ; Non-zero: skip write path
8A69 LDA fs_cmd_urd ; Load port for transfer setup
8A6C JSR transfer_file_blocks ; Transfer data blocks to fileserver
8A6F BNE findv_eof_check ; Non-zero: branch past error ptr
8A71 .gbpb_read_path←1← 8A67 BNE
JSR send_data_blocks ; Read path: receive data blocks from FS
8A74 .findv_eof_check←1← 8A6F BNE
LDA #&2a ; A=&2A: error ptr for FS retry
8A76 STA fs_error_ptr ; Store error ptr for TX poll
8A78 .wait_fs_reply
JSR send_fs_reply_cmd ; Wait for FS reply command
8A7B LDA (fs_options,x) ; Load handle mask for EOF flag update
8A7D BIT fs_cmd_data ; Check FS reply: bit 7 = not at EOF
8A80 BMI skip_clear_flag ; Bit 7 set: not EOF, skip clear
8A82 JSR clear_fs_flag ; At EOF: clear EOF hint for this handle
8A85 .skip_clear_flag←1← 8A80 BMI
JSR set_fs_flag ; Set EOF hint flag (may be at EOF)
8A88 STX fs_load_addr_2 ; Direction=0: forward adjustment
8A8A JSR adjust_addrs_9 ; Adjust param block addrs by +9 bytes
8A8D DEC fs_load_addr_2 ; Direction=&FF: reverse adjustment
8A8F SEC ; SEC for reverse subtraction
8A90 JSR adjust_addrs ; Adjust param block addrs (reverse)
8A93 ASL fs_cmd_data ; Shift bit 7 into C for return flag
8A96 JMP return_a_zero ; Return via restore_args path
8A99 .get_disc_title←1← 8AC9 BEQ
LDY #&15 ; Y=&15: function code for disc title Y=function code for HDRFN
8A9B JSR prepare_fs_cmd ; Build and send FS command Prepare FS command buffer (12 references)
8A9E LDA fs_boot_option ; Load boot option from FS workspace
8AA1 STA fs_boot_data ; Store boot option in reply area
8AA4 STX fs_load_addr ; X=0: reply data start offset X=0 on success, &D6 on not-found
8AA6 STX fs_load_addr_hi ; Clear reply buffer high byte
8AA8 LDA #&12 ; A=&12: 18 bytes of reply data
8AAA STA fs_load_addr_2 ; Store as byte count for copy
8AAC BNE copy_reply_to_caller ; ALWAYS branch to copy_reply_to_caller ALWAYS branch

OSGBPB 5-8 info handler (OSINFO)

Handles the "messy interface tacked onto OSGBPB" (original source). Checks whether the destination address is in Tube space by comparing the high bytes against TBFLAG. If in Tube space, data must be copied via the Tube FIFO registers (with delays to accommodate the slow 16032 co-processor) instead of direct memory access.

8AAE .osgbpb_info←1← 8A01 JMP
LDY #4 ; Y=4: check param block byte 4
8AB0 LDA tx_in_progress ; Check if destination is in Tube space
8AB3 BEQ store_tube_flag ; No Tube: skip Tube address check
8AB5 CMP (fs_options),y ; Compare Tube flag with addr byte 4
8AB7 BNE store_tube_flag ; Mismatch: not Tube space
8AB9 DEY ; Y=&03
8ABA SBC (fs_options),y ; Y=3: subtract addr byte 3 from flag
8ABC .store_tube_flag←2← 8AB3 BEQ← 8AB7 BNE
STA rom_svc_num ; Non-zero = Tube transfer required
8ABE .info2←1← 8AC4 BNE
LDA (fs_options),y ; Copy param block bytes 1-4 to workspace
8AC0 STA fs_last_byte_flag,y ; Store to &BD+Y workspace area
8AC3 DEY ; Previous byte
8AC4 BNE info2 ; Loop for bytes 3,2,1
8AC6 PLA ; Sub-function: AND #3 of (original A - 4)
8AC7 AND #3 ; Mask to 0-3 (OSGBPB 5-8 → 0-3)
8AC9 BEQ get_disc_title ; A=0 (OSGBPB 5): read disc title
8ACB LSR ; LSR: A=0 (OSGBPB 6) or A=1 (OSGBPB 7)
8ACC BEQ gbpb6_read_name ; A=0 (OSGBPB 6): read CSD/LIB name
8ACE BCS gbpb8_read_dir ; C=1 (OSGBPB 8): read filenames from dir
8AD0 .gbpb6_read_name←1← 8ACC BEQ
TAY ; Y=0 for CSD or carry for fn code select Y=function code
8AD1 LDA fs_csd_handle,y ; Get CSD/LIB/URD handles for FS command
8AD4 STA fs_cmd_csd ; Store CSD handle in command buffer
8AD7 LDA fs_lib_handle ; Load LIB handle from workspace
8ADA STA fs_cmd_lib ; Store LIB handle in command buffer
8ADD LDA fs_urd_handle ; Load URD handle from workspace
8AE0 STA fs_cmd_urd ; Store URD handle in command buffer
8AE3 LDX #&12 ; X=&12: buffer extent for command data X=buffer extent (command-specific data bytes)
8AE5 STX fs_cmd_y_param ; Store X as function code in header
8AE8 LDA #&0d ; &0D = 13 bytes of reply data expected
8AEA STA fs_func_code ; Store reply length in command buffer
8AED STA fs_load_addr_2 ; Store as byte count for copy loop
8AEF LSR ; LSR: &0D >> 1 = 6 A=timeout period for FS reply
8AF0 STA fs_cmd_data ; Store as command data byte
8AF3 CLC ; CLC for standard FS path
8AF4 JSR build_send_fs_cmd ; Build and send FS command (DOFSOP)
8AF7 STX fs_load_addr_hi ; X=0 on success, &D6 on not-found
8AF9 INX ; INX: X=1 after build_send_fs_cmd
8AFA STX fs_load_addr ; Store X as reply start offset
8AFC .copy_reply_to_caller←2← 8AAC BNE← 8B6F JSR
LDA rom_svc_num ; Copy FS reply to caller's buffer
8AFE BNE tube_transfer ; Non-zero: use Tube transfer path
8B00 LDX fs_load_addr ; X = reply start offset
8B02 LDY fs_load_addr_hi ; Y = reply buffer high byte
8B04 .copy_reply_bytes←1← 8B0D BNE
LDA fs_cmd_data,x ; Load reply data byte
8B07 STA (fs_crc_lo),y ; Store to caller's buffer
8B09 INX ; Next source byte
8B0A INY ; Next destination byte
8B0B DEC fs_load_addr_2 ; Decrement remaining bytes
8B0D BNE copy_reply_bytes ; Loop until all bytes copied
8B0F BEQ gbpb_done ; ALWAYS branch to exit ALWAYS branch
8B11 .tube_transfer←1← 8AFE BNE
JSR tube_claim_loop ; Claim Tube transfer channel
8B14 LDA #1 ; A=1: Tube claim type 1 (write)
8B16 LDX fs_options ; X = param block address low
8B18 LDY fs_block_offset ; Y = param block address high
8B1A INX ; INX: advance past byte 0
8B1B BNE no_page_wrap ; No page wrap: keep Y
8B1D INY ; Page wrap: increment high byte
8B1E .no_page_wrap←1← 8B1B BNE
JSR tube_addr_claim ; Claim Tube address for transfer
8B21 LDX fs_load_addr ; X = reply data start offset
8B23 .tbcop1←1← 8B2C BNE
LDA fs_cmd_data,x ; Load reply data byte
8B26 STA tube_data_register_3 ; Send byte to Tube via R3
8B29 INX ; Next source byte
8B2A DEC fs_load_addr_2 ; Decrement remaining bytes
8B2C BNE tbcop1 ; Loop until all bytes sent to Tube
8B2E LDA #&83 ; Release Tube after transfer complete
8B30 JSR tube_addr_claim ; Release Tube address claim
8B33 .gbpb_done←2← 8B0F BEQ← 8B89 BEQ
JMP return_a_zero ; Return via restore_args path
8B36 .gbpb8_read_dir←1← 8ACE BCS
LDY #9 ; OSGBPB 8: read filenames from dir
8B38 LDA (fs_options),y ; Byte 9: number of entries to read
8B3A STA fs_func_code ; Store as reply count in command buffer
8B3D LDY #5 ; Y=5: byte 5 = starting entry number
8B3F LDA (fs_options),y ; Load starting entry number
8B41 STA fs_data_count ; Store in command buffer
8B44 LDX #&0d ; X=&0D: command data extent X=preserved through header build
8B46 STX fs_reply_cmd ; Store extent in command buffer
8B49 LDY #2 ; Y=2: function code for dir read
8B4B STY fs_load_addr ; Store 2 as reply data start offset
8B4D STY fs_cmd_data ; Store 2 as command data byte
8B50 INY ; Y=3: function code for header read Y=function code for HDRFN Y=&03
8B51 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8B54 STX fs_load_addr_hi ; X=0 after FS command completes X=0 on success, &D6 on not-found
8B56 LDA fs_func_code ; Load reply entry count
8B59 STA (fs_options,x) ; Store at param block byte 0 (X=0)
8B5B LDA fs_cmd_data ; Load entries-read count from reply
8B5E LDY #9 ; Y=9: param block byte 9
8B60 ADC (fs_options),y ; Add to starting entry number
8B62 STA (fs_options),y ; Update param block with new position
8B64 LDA txcb_end ; Load total reply length
8B66 SBC #7 ; Subtract header (7 bytes) from reply len
8B68 STA fs_func_code ; Store adjusted length in command buffer
8B6B STA fs_load_addr_2 ; Store as byte count for copy loop
8B6D BEQ skip_copy_reply ; Zero bytes: skip copy
8B6F JSR copy_reply_to_caller ; Copy reply data to caller's buffer
8B72 .skip_copy_reply←1← 8B6D BEQ
LDX #2 ; X=2: clear 3 bytes
8B74 .zero_cmd_bytes←1← 8B78 BPL
STA fs_data_count,x ; Zero out &0F07+X area
8B77 DEX ; Next byte
8B78 BPL zero_cmd_bytes ; Loop for X=2,1,0
8B7A JSR adjust_addrs_1 ; Adjust pointer by +1 (one filename read)
8B7D SEC ; SEC for reverse adjustment
8B7E DEC fs_load_addr_2 ; Reverse adjustment for updated counter
8B80 LDA fs_cmd_data ; Load entries-read count
8B83 STA fs_func_code ; Store in command buffer
8B86 JSR adjust_addrs ; Adjust param block addresses
8B89 BEQ gbpb_done ; Z=1: all done, exit
8B8B .tube_claim_loop←3← 8B11 JSR← 8B90 BCC← 8DA1 JSR
LDA #&c3 ; A=&C3: Tube claim with retry
8B8D JSR tube_addr_claim ; Request Tube address claim
8B90 BCC tube_claim_loop ; C=0: claim failed, retry
8B92 RTS ; Tube claimed successfully

FSCV 2/3/4: unrecognised * command handler (DECODE)

CLI parser originally by Sophie Wilson (co-designer of ARM). Matches command text against the table at &8BD7 using case-insensitive comparison with abbreviation support — commands can be shortened with '.' (e.g. "I." for "INFO"). The "I." entry is a special fudge placed first in the table: since "I." could match multiple commands, it jumps to forward_star_cmd to let the fileserver resolve the ambiguity.

The matching loop compares input characters against table entries. On mismatch, it skips to the next entry. On match of all table characters, or when '.' abbreviation is found, it dispatches via PHA/PHA/RTS to the entry's handler address.

After matching, adjusts fs_crc_lo/fs_crc_hi to point past the matched command text.

8B93 .fscv_3_star_cmd←1← 8243 JMP
JSR save_fscv_args ; Save A/X/Y and set up command ptr
8B96 LDX #&ff ; X=&FF: table index (pre-incremented)
8B98 .scan_cmd_table←1← 8BB3 BNE
LDY #&ff ; Y=&FF: input index (pre-incremented)
8B9A .decfir←1← 8BA5 BEQ
INY ; Advance input pointer
8B9B INX ; Advance table pointer
8B9C .decmor←1← 8BB7 BCS
LDA fs_cmd_match_table,x ; Load table character
8B9F BMI dispatch_cmd ; Bit 7: end of name, dispatch
8BA1 EOR (fs_crc_lo),y ; XOR input char with table char
8BA3 AND #&df ; Case-insensitive (clear bit 5)
8BA5 BEQ decfir ; Match: continue comparing
8BA7 DEX ; Mismatch: back up table pointer
8BA8 .decmin←1← 8BAC BPL
INX ; Skip to end of table entry
8BA9 LDA fs_cmd_match_table,x ; Load table byte
8BAC BPL decmin ; Loop until bit 7 set (end marker)
8BAE LDA (fs_crc_lo),y ; Check input for '.' abbreviation
8BB0 INX ; Skip past handler high byte
8BB1 CMP #&2e ; Is input '.' (abbreviation)?
8BB3 BNE scan_cmd_table ; No: try next table entry
8BB5 INY ; Yes: skip '.' in input
8BB6 DEX ; Back to handler high byte
8BB7 BCS decmor ; ALWAYS branch; dispatch via BMI
8BB9 .dispatch_cmd←1← 8B9F BMI
PHA ; Push handler address high byte
8BBA LDA cmd_table_entry_1,x ; Load handler address low byte
8BBD PHA ; Push handler address low byte
8BBE CLC ; CLC for pointer calculation
8BBF TYA ; A = chars consumed from input
8BC0 LDX fs_crc_hi ; X = command text pointer high
8BC2 ADC fs_crc_lo ; Add chars consumed to pointer low
8BC4 STA fs_context_hi ; Store adjusted text pointer low
8BC7 STA fs_cmd_ptr ; Duplicate to second pointer copy
8BCA BCC cmd_match_retry ; No page overflow: skip INX
8BCC INX ; Adjust high byte for page crossing
8BCD .cmd_match_retry←1← 8BCA BCC
STX l0e0c ; Store high byte to context ptr 1
8BD0 STX l0e11 ; Store high byte to context ptr 2
8BD3 STX fs_work_16 ; Store high byte to context ptr 3
8BD6 RTS ; Dispatch via PHA/PHA/RTS

FS command match table (COMTAB)

Format: command letters (bit 7 clear), then dispatch address as two bytes: high|(bit 7 set), low. The PHA/PHA/RTS trick adds 1 to the stored (address-1). Matching is case-insensitive (AND &DF) and supports '.' abbreviation (standard Acorn pattern).

Entries: "I." → &8079 (forward_star_cmd) — placed first as a fudge to catch *I. abbreviation before matching *I AM "I AM" → &8D07 (i_am_handler: parse station.net, logon) "EX " → &8BF3 (ex_handler: extended catalogue) "EX"\r → &8BF3 (same, exact match at end of line) "BYE"\r → &834A (bye_handler: logoff) <catch-all> → &8079 (forward anything else to FS)

8BD7 .fs_cmd_match_table←2← 8B9C LDA← 8BA9 LDA
EOR #&2e ; XOR with '.' (abbreviation check)
8BD9 EQUB &80
8BDA EQUS "xI AM"
8BDF EQUB &8D, &06
8BE1 EQUS "EX "
8BE4 EQUB &8B, &F2, &45, &58, &0D, &8B, &F2
8BEB EQUS "BYE"
8BEE EQUB &0D, &83, &49, &80, &78
fall through ↓

*EX handler (extended catalogue)

Sets column width &B6=&50 (80 columns, one file per line with full details) and &B7=&01, then branches into fscv_5_cat at &8C08, bypassing fscv_5_cat's default 20-column setup.

8BF3 .ex_handler
DEY ; Pre-decrement Y for parameter
8BF4 LDX #1 ; X=1: boot option display field
8BF6 STX fs_work_7 ; Store to fs_work_7 (&B7)
8BF8 LDX #&50 ; X=&50: 80-column display width
8BFA STX l00b6 ; Store column width at &B6
8BFC BNE cat_init_display ; ALWAYS branch

*CAT handler (directory catalogue)

Sets column width &B6=&14 (20 columns, four files per 80-column line) and &B7=&03. The catalogue protocol is multi-step: first sends FCUSER (&15: read user environment) to get CSD, disc, and library names, then sends FCREAD (&12: examine) repeatedly to fetch entries in batches until zero are returned (end of dir). The receive buffer abuts the examine request buffer and ends at RXBUFE, allowing seamless data handling across request cycles.

The command code byte in the fileserver reply indicates FS type: zero means an old-format FS (client must format data locally), non-zero means new-format (server returns pre-formatted strings). This enables backward compatibility with older Acorn fileservers.

Display format: - Station number in parentheses - "Owner" or "Public" access level - Boot option with name (Off/Load/Run/Exec) - Current directory and library paths - Directory entries: CRFLAG (&CF) cycles 0-3 for multi-column layout; at count 0 a newline is printed, others get spaces. *EX sets CRFLAG=&FF to force one entry per line.

8BFE .fscv_5_cat
LDX #&14 ; X=&14 (20): column width for display
8C00 STX l00b6 ; Store column width for batch calc
8C02 LDX #3 ; X=3: column count for examine request
8C04 STX fs_work_7 ; Store column count
8C06 LDY #0 ; Y=0: initial entry start offset
8C08 .cat_init_display←1← 8BFC BNE
LDA #6 ; A=6: examine format type in command
8C0A STA fs_cmd_data ; Store format type at &0F05
8C0D JSR skip_spaces ; Skip spaces in dir name argument
8C10 STY fs_load_addr_3 ; Save parameter offset after spaces
8C12 LDX #1 ; X=1: copy dir name at cmd offset 1
8C14 JSR copy_string_from_offset ; Copy directory name to command buffer
8C17 LDY #&12 ; Y=function code for HDRFN
8C19 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8C1C LDX #3 ; X=3: start printing from reply offset 3
8C1E JSR print_reply_bytes ; Print directory title (10 chars)
8C21 JSR print_inline ; Print '('
8C24 EQUS "("
8C25 LDA fs_reply_stn ; Load station number from FS reply
8C28 JSR print_decimal ; Print station number as decimal
8C2B JSR print_inline ; Print ') '
8C2E EQUS ")"
8C2F LDX #5 ; X=5: space padding count
8C31 JSR print_spaces ; Print 5 spaces for alignment
8C34 LDX fs_access_level ; Access: 0=Owner, non-zero=Public
8C37 BNE print_public ; Non-zero: Public access
8C39 JSR print_inline ; Print 'Owner' + CR
8C3C EQUS "Owner."
8C42 BNE cat_access_setup ; Always taken (high-bit term. str)
8C44 .print_public←1← 8C37 BNE
JSR print_inline ; Print 'Public' + CR
8C47 EQUS "Public."
8C4E .cat_access_setup←1← 8C42 BNE
LDY #&15 ; Y=function code for HDRFN
8C50 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8C53 INX ; X=1: past command code byte
8C54 LDY #&10 ; Y=&10: print 16 characters
8C56 JSR print_reply_counted ; Print disc/CSD name from reply
8C59 LDX #4 ; X=4: space padding count
8C5B JSR print_spaces ; Print 4 spaces for alignment
8C5E JSR print_inline ; Print 'Option ' label
8C61 EQUS "Option "
8C68 LDA fs_boot_option ; Load boot option from workspace
8C6B TAX ; X = boot option for name table lookup
8C6C JSR print_hex ; Print boot option as hex digit
8C6F JSR print_inline ; Print ' ('
8C72 EQUS " ("
8C74 LDY option_name_offsets,x ; Load string offset for option name
8C77 .print_option_char←1← 8C80 BNE
LDA option_name_strings,y ; Load char from option name string
8C7A BEQ done_option_name ; Zero terminator: name complete
8C7C JSR osasci ; Write character
8C7F INY ; Next character
8C80 BNE print_option_char ; Continue printing option name
8C82 .done_option_name←1← 8C7A BEQ
JSR print_inline ; Print ')' + CR + 'Dir. '
8C85 EQUS ").Dir. "
8C8C LDX #&11 ; X=&11: CSD name offset in reply
8C8E JSR print_reply_bytes ; Print current directory name
8C91 LDX #5 ; X=5: space padding count
8C93 JSR print_spaces ; Print 5 spaces for alignment
8C96 JSR print_inline ; Print 'Lib. ' label
8C99 EQUS "Lib. "
8C9E LDX #&1b ; X=&1B: library name offset in reply
8CA0 JSR print_reply_bytes ; Print library name
8CA3 JSR print_inline ; Print two CRs (blank line)
8CA6 EQUS ".."
8CA8 STY fs_func_code ; Y=0: initial examine start position
8CAB STY fs_work_4 ; Save start offset in zero page for loop
8CAD TXA ; A = reply buffer bytes consumed
8CAE EOR #&ff ; Complement for divide-by-subtraction
8CB0 .count_columns_loop←1← 8CB4 BCS
SEC ; SEC for subtraction
8CB1 SBC l00b6 ; Subtract one column width (20)
8CB3 INY ; Count another entry that fits
8CB4 BCS count_columns_loop ; Loop while space remains
8CB6 STY fs_data_count ; Store entries per examine batch
8CB9 STY fs_work_5 ; Save batch size for loop reset
8CBB .cat_examine_continue←1← 8CE6 BNE
LDY fs_load_addr_3 ; Reload dir name offset for examine
8CBD .cat_examine_loop
LDX fs_work_7 ; Load column count for display format
8CBF STX fs_cmd_data ; Store column count in command data
8CC2 LDX #3 ; X=3: copy directory name at offset 3
8CC4 JSR copy_string_from_offset ; Append directory name to examine command
8CC7 LDY #3 ; Y=function code for HDRFN
8CC9 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8CCC LDA fs_cmd_data ; Load entry count from reply
8CCF BEQ set_handle_return ; Zero entries returned: catalogue done
8CD1 LDX #2 ; X=2: first entry offset in reply
8CD3 JSR print_dir_from_offset ; Print/format this directory entry
8CD6 CLC ; CLC for addition
8CD7 LDA fs_work_4 ; Load current examine start offset
8CD9 ADC fs_cmd_data ; Add entries returned this batch
8CDC STA fs_func_code ; Update next examine start offset
8CDF STA fs_work_4 ; Save updated start offset
8CE1 LDA fs_work_5 ; Reload batch size for next request
8CE3 STA fs_data_count ; Store batch size in command buffer
8CE6 BNE cat_examine_continue ; Loop for remaining characters
8CE8 JMP l212e ; Fallthrough (also boot string 'L.!')

Boot command strings for auto-boot

The four boot options use OSCLI strings at offsets within page &8C: Option 0 (Off): offset &F7 → &8CF7 = bare CR (empty command) Option 1 (Load): offset &E8 → &8CE8 = "L.!BOOT" (dual-purpose: the JMP &212E instruction at &8CE8 has opcode &4C='L' and operand bytes &2E='.' &21='!', forming the string "L.!") Option 2 (Run): offset &EA → boot_cmd_strings-1 = "!BOOT" (bare filename = *RUN) Option 3 (Exec): offset &F0 → &8CF0 = "E.!BOOT"

This is a classic BBC ROM space optimisation: the JMP instruction's bytes serve double duty as both executable code and ASCII text.

8CEB .boot_cmd_strings
EQUS "BOOT"
8CEF EQUB &0D
8CF0 EQUS "E.!BOOT"
8CF7 EQUB &0D

Set library handle

Stores Y into &0E04 (library directory handle in FS workspace). Falls through to set_handle_return (JMP restore_args_return) if Y is non-zero.

On EntryYlibrary handle from FS reply
8CF8 .fsreply_5_set_lib
STY fs_lib_handle ; Store Y (library handle) to &0E04
8CFB BNE set_handle_return ; Non-zero: continue to set handle
fall through ↓

Set CSD handle

Stores Y into &0E03 (current selected directory handle). Falls through to set_handle_return (JMP restore_args_return).

On EntryYCSD handle from FS reply
8CFD .fsreply_3_set_csd
STY fs_csd_handle ; Store Y (CSD handle) to &0E03
8D00 .set_handle_return←3← 8CCF BEQ← 8CFB BNE← 8D2E BCC
JMP restore_args_return ; Jump to restore_args_return

Boot option → OSCLI string offset table

Four bytes indexed by the boot option value (0-3). Each byte is the low byte of a pointer into page &8C, where the OSCLI command string for that boot option lives. See boot_cmd_strings.

8D03 .boot_option_offsets←1← 8D33 LDX
EQUB &F7 ; Opt 0 (Off): bare CR
8D04 EQUB &E8 ; Opt 1 (Load): L.!BOOT
8D05 EQUB &EA ; Opt 2 (Run): !BOOT
8D06 EQUB &F0 ; Opt 3 (Exec): E.!BOOT

"I AM" command handler

Dispatched from the command match table when the user types "*I AM <station>" or "*I AM <network>.<station>". Skips leading spaces via skip_spaces, then calls parse_decimal twice if a dot separator is present. The first number becomes the network (&0E01, via TAX pass-through in parse_decimal) and the second becomes the station (&0E00). With a single number, it is stored as the station and the network defaults to 0 (local). Then forwards the command to the fileserver via forward_star_cmd.

8D07 .i_am_handler
JSR skip_spaces ; Skip spaces before station number
8D0A BCS jmp_restore_args ; C=1: alphabetic, forward to FS
8D0C LDA #0 ; A=0: default network (local)
8D0E JSR parse_decimal ; Parse decimal number from (fs_options),Y (DECIN)
8D11 BCC fsreply_handle_copy ; C=0: no dot, single number only
8D13 INY ; Y=offset into (fs_options) buffer
8D14 JSR parse_decimal ; Parse decimal number from (fs_options),Y (DECIN)
8D17 .fsreply_handle_copy←1← 8D11 BCC
STA fs_server_stn ; A=parsed value (accumulated in &B2)
8D1A STX fs_server_net ; X=initial A value (saved by TAX)
8D1D .jmp_restore_args←1← 8D0A BCS
JMP forward_star_cmd ; Restore A/X/Y and return to caller

Copy FS reply handles to workspace and execute boot command

SEC entry (LOGIN): copies 4 bytes from &0F05-&0F08 (FS reply) to &0E02-&0E05 (URD, CSD, LIB handles and boot option), then looks up the boot option in boot_option_offsets to get the OSCLI command string and executes it via JMP oscli. The carry flag distinguishes LOGIN (SEC) from SDISC (CLC) — both share the handle-copying code, but only LOGIN executes the boot command. This use of the carry flag to select behaviour between two callers avoids duplicating the handle-copy loop.

8D20 .fsreply_1_copy_handles_boot
SEC ; Set carry: LOGIN path (copy + boot)
fall through ↓

Copy FS reply handles to workspace (no boot)

CLC entry (SDISC): copies handles only, then jumps to set_handle_return. Called when the FS reply contains updated handle values but no boot action is needed.

8D21 .fsreply_2_copy_handles
LDX #3 ; Copy 4 bytes: boot option + 3 handles
8D23 BCC logon3 ; SDISC: skip boot option, copy handles only
8D25 .logon2←1← 8D2C BPL
LDA fs_cmd_data,x ; Load from FS reply (&0F05+X)
8D28 STA fs_urd_handle,x ; Store to handle workspace (&0E02+X)
8D2B .logon3←1← 8D23 BCC
DEX ; Next handle (descending)
8D2C BPL logon2 ; Loop while X >= 0
8D2E BCC set_handle_return ; SDISC: done, restore args and return
fall through ↓

Execute boot command via OSCLI

Reached from fsreply_1_copy_handles_boot when carry is set (LOGIN path). Reads the boot option from fs_boot_option (&0E05), looks up the OSCLI command string offset from boot_option_offsets+1, and executes the boot command via JMP oscli with page &8D.

8D30 .boot_cmd_execute
LDY fs_boot_option ; Y = boot option from FS workspace
8D33 LDX boot_option_offsets,y ; X = command string offset from table
8D36 LDY #&8c ; Y = &8D (high byte of command address)
8D38 JMP oscli ; Execute boot command string via OSCLI

Option name strings

Null-terminated strings for the four boot option names: "Off", "Load", "Run", "Exec" Used by fscv_5_cat to display the current boot option setting.

8D3B .option_name_strings←1← 8C77 LDA
EQUS "Off"
8D3E EQUB &00
8D3F EQUS "Load"
8D43 EQUB &00
8D44 EQUS "Run"
8D47 EQUB &00
8D48 EQUS "Exec"

Option name offsets

Four-byte table of offsets into option_name_strings: 0, 4, 9, &0D — one per boot option value (0-3).

8D4C .option_name_offsets←1← 8C74 LDY
BRK ; Offset 0 (BRK opcode as zero byte)
8D4D EQUB &04, &09, &0D

Print reply buffer bytes

Prints Y characters from the FS reply buffer (&0F05+X) to the screen via OSASCI. X = starting offset, Y = count. Used by fscv_5_cat to display directory and library names.

8D50 .print_reply_bytes←3← 8C1E JSR← 8C8E JSR← 8CA0 JSR
LDY #&0a ; Y=10: default character count
8D52 .print_reply_counted←2← 8C56 JSR← 8D5A BNE
LDA fs_cmd_data,x ; Load character from reply buffer
8D55 JSR osasci ; Write character
8D58 INX ; Advance to next character
8D59 DEY ; Decrement remaining count
8D5A BNE print_reply_counted ; Loop until count exhausted
8D5C RTS ; Return to caller

Print spaces

Prints X space characters via print_space. Used by fscv_5_cat to align columns in the directory listing.

8D5D .print_spaces←4← 8C31 JSR← 8C5B JSR← 8C93 JSR← 8D61 BNE
JSR print_space ; Print one space character
8D60 DEX ; Decrement space count
8D61 BNE print_spaces ; Loop until all spaces printed
8D63 RTS ; Return to caller

Copy filename to FS command buffer

Entry with X=0: copies from (fs_crc_lo),Y to &0F05+X until CR. Used to place a filename into the FS command buffer before sending to the fileserver. Falls through to copy_string_to_cmd.

8D64 .copy_filename←3← 8079 JSR← 86CC JSR← 888B JSR
LDX #0 ; X=0: start of output buffer
fall through ↓

Copy string to FS command buffer

Entry with X and Y specified: copies bytes from (fs_crc_lo),Y to &0F05+X, stopping when a CR (&0D) is encountered. The CR itself is also copied. Returns with X pointing past the last byte written.

On EntryXdestination offset in fs_cmd_data (&0F05+X)
On ExitXnext free position past CR
Ystring length (incl CR)
A0 (from EOR &0D with final CR)
8D66 .copy_string_to_cmd←4← 877A JSR← 8884 JSR← 88A6 JSR← 8965 JSR
LDY #0 ; Start copying from offset 0
8D68 .copy_string_from_offset←3← 8C14 JSR← 8CC4 JSR← 8D71 BNE
LDA (fs_crc_lo),y ; Load next byte from source string
8D6A STA fs_cmd_data,x ; Store to command buffer
8D6D INX ; Advance write position
8D6E INY ; Advance read position
8D6F EOR #&0d ; XOR with CR: result=0 if byte was CR
8D71 BNE copy_string_from_offset ; Loop until CR copied
8D73 .return_copy_string←1← 8D79 BMI
RTS ; Return to caller

Print directory name from reply buffer

Prints characters from the FS reply buffer (&0F05+X onwards). Null bytes (&00) are replaced with CR (&0D) for display. Stops when a byte with bit 7 set is encountered (high-bit terminator). Used by fscv_5_cat to display Dir. and Lib. paths.

8D74 .fsreply_0_print_dir
LDX #0 ; X=0: start of reply buffer
8D76 .print_dir_from_offset←2← 8CD3 JSR← 8D83 BNE
LDA fs_cmd_data,x ; Load character from reply
8D79 BMI return_copy_string ; Bit 7 set: end of string
8D7B BNE infol2 ; Non-zero: printable character
8D7D LDA #&0d ; Replace null with CR
8D7F .infol2←1← 8D7B BNE
JSR osasci ; Write character 13
8D82 INX ; Advance to next character
8D83 BNE print_dir_from_offset ; Continue printing directory path
fall through ↓

Send FS load-as-command and execute response

Sets up an FS command with function code &05 (FCCMND: load as command) using send_fs_examine. If a Tube co-processor is present (tx_in_progress != 0), transfers the response data to the Tube via tube_addr_claim. Otherwise jumps via the indirect pointer at (&0F09) to execute at the load address.

8D85 .fsreply_4_notify_exec
LDX #&0e ; X=&0E: OSWORD &10 parameter block size
8D87 STX fs_block_offset ; Y=0: param block offset
8D89 LDA #&10 ; A=&10: OSWORD &10 (open RXCB)
8D8B STA fs_options ; Issue OSWORD &10 to open RXCB
8D8D LDX #&4a ; Load RXCB number from result
8D8F LDY #5 ; Zero: no free RXCB, skip
8D91 JSR send_fs_examine ; Store RXCB number in exec context
8D94 LDA tx_in_progress ; Y=&70: FS workspace offset Load reply byte from workspace
8D97 BEQ net_handle_validate ; Store to exec param area
8D99 ADC fs_load_upper ; Advance to next param byte Compare against boundary
8D9C ADC fs_addr_check ; Loop for all exec parameters
8D9F BCS net_handle_validate ; Store final byte
8DA1 JSR tube_claim_loop ; X=&16: OSBYTE param A=&C7: OSBYTE read/write flag
8DA4 LDX #9 ; Enable exec notification via OSBYTE
8DA6 LDY #&0f ; Y=&0F: workspace offset for params
8DA8 LDA #4 ; A=4: Tube claim type (multi-byte transfer)
8DAA JMP tube_addr_claim ; Claim Tube for address transfer
8DAD .net_handle_validate←2← 8D97 BEQ← 8D9F BCS
JMP (fs_load_vector) ; Execute at load address

*NET1: read file handle from received packet

Reads a file handle byte from offset &6F in the RX buffer (net_rx_ptr), stores it in &F0, then falls through to the common handle workspace cleanup at clear_svc_return.

8DB0 .net_1_read_handle
LDY #&6f ; Y=&6F: handle offset in RX buffer
8DB2 LDA (net_rx_ptr),y ; Load handle byte from RX data
8DB4 STA osword_pb_ptr ; Store handle to &F0
8DB6 BCC clear_svc_return ; Branch to cleanup path
fall through ↓

Calculate handle workspace offset

Converts a file handle number (in A) to a byte offset (in Y) into the NFS handle workspace. The calculation is A*12: ASL A (A*2), ASL A (A*4), PHA, ASL A (A*8), ADC stack (A*8 + A*4 = A*12). Validates that the offset is < &48 (max 6 handles × 12 bytes per handle entry = 72 bytes). If invalid (>= &48), returns with C set and Y=0, A=0 as an error indicator.

On EntryAfile handle number
On ExitAhandle*12 or 0 if invalid
Yworkspace offset or 0 if invalid
Cclear if valid, set if invalid
8DB8 .calc_handle_offset←5← 82B7 JSR← 8DCC JSR← 8DE2 JSR← 8F02 JSR← 8F1B JSR
ASL ; A = handle * 2
8DB9 ASL ; A = handle * 4
8DBA PHA ; Push handle*4 onto stack
8DBB ASL ; A = handle * 8
8DBC TSX ; X = stack pointer
8DBD ADC l0101,x ; A = handle*8 + handle*4 = handle*12
8DC0 TAY ; Y = offset into handle workspace
8DC1 PLA ; Clean up stack (discard handle*4)
8DC2 CMP #&48 ; Offset >= &48? (6 handles max)
8DC4 BCC return_calc_handle ; Valid: return with C clear
8DC6 LDY #0 ; Invalid: Y = 0
8DC8 TYA ; A = 0, C set (error) A=&00
8DC9 .return_calc_handle←1← 8DC4 BCC
RTS ; Return after calculation

*NET2: read handle entry from workspace

Looks up the handle in &F0 via calc_handle_offset. If the workspace slot contains &3F ('?', meaning unused/closed), returns 0. Otherwise returns the stored handle value. Clears rom_svc_num on exit.

8DCA .net_2_read_handle_entry
LDA osword_pb_ptr ; Load handle number from &F0
8DCC JSR calc_handle_offset ; Look up handle &F0 in workspace
8DCF BCS rxpol2 ; Invalid handle: return 0
8DD1 LDA (nfs_workspace),y ; Load stored handle value
8DD3 CMP #&3f ; &3F = unused/closed slot marker
8DD5 BNE store_handle_return ; Slot in use: return actual value
8DD7 .rxpol2←2← 8DCF BCS← 8DE5 BCS
LDA #0 ; Return 0 for closed/invalid handle
8DD9 .store_handle_return←1← 8DD5 BNE
STA osword_pb_ptr ; Store result back to &F0
8DDB .clear_svc_return←3← 8DB6 BCC← 8DF1 BCC← 8DF6 BVC
LDA #0 ; A=0: clear service claim
8DDD STA rom_svc_num ; Release ROM service number
8DDF RTS ; Return to caller

*NET3: close handle (mark as unused)

Looks up the handle in &F0 via calc_handle_offset. Writes &3F ('?') to mark the handle slot as closed in the NFS workspace. Preserves the carry flag state across the write using ROL/ROR on rx_status_flags. Clears rom_svc_num on exit.

8DE0 .net_3_close_handle
LDA osword_pb_ptr ; Load handle number from &F0
8DE2 JSR calc_handle_offset ; Look up handle &F0 in workspace
8DE5 BCS rxpol2 ; Invalid handle: return 0
8DE7 ROL rx_status_flags ; Preserve carry via ROL
8DEA LDA #&3f ; A=&3F: handle closed/unused marker
8DEC STA (nfs_workspace),y ; Mark handle as closed in workspace
fall through ↓

Restore RX flags after close handle

Performs ROR on rx_flags to restore the carry flag state that was preserved by the matching ROL in net_3_close_handle. Falls through to osword_12_handler (clearing fs_temp_ce).

8DEE .restore_rx_flags
ROR rx_status_flags ; Restore carry via ROR
8DF1 BCC clear_svc_return ; C=0: branch to cleanup exit
fall through ↓

*NET4: resume after remote operation

Calls resume_after_remote (&8146) to re-enable the keyboard and send a completion notification. The BVC always branches to clear_svc_return since resume_after_remote returns with V clear (from CLV in prepare_cmd_clv).

8DF3 .net_4_resume_remote
JSR resume_after_remote ; Jump to clear_svc_restore_args
8DF6 BVC clear_svc_return ; V always clear: branch to cleanup exit
fall through ↓

Filing system OSWORD entry

Subtracts &0F from the command code in &EF, giving a 0-4 index for OSWORD calls &0F-&13 (15-19). Falls through to the PHA/PHA/RTS dispatch at &8E02.

8DF8 .svc_8_osword
LDA osbyte_a_copy ; Command code from &EF
8DFA SBC #&0f ; Subtract &0F: OSWORD &0F-&13 become indices 0-4
8DFC BMI return_copy_param ; Outside our OSWORD range, exit
8DFE CMP #5 ; Only OSWORDs &0F-&13 (index 0-4)
8E00 BCS return_copy_param ; Index >= 5: not ours, return
fall through ↓

PHA/PHA/RTS dispatch for filing system OSWORDs

X = OSWORD number - &0F (0-4). Dispatches via the 5-entry table at &8E19 (low) / &8E1E (high).

8E02 .fs_osword_dispatch
TAX ; X = sub-function code for table lookup
8E03 LDA fs_osword_tbl_hi,x ; Load handler address high byte from table
8E06 PHA ; Push high byte for RTS dispatch
8E07 LDA fs_osword_tbl_lo,x ; Load handler address low byte from table
8E0A PHA ; Dispatch table: low bytes for OSWORD &0F-&13 handlers
8E0B LDY #2 ; Y=2: save 3 bytes (&AA-&AC)
8E0D .save1←1← 8E13 BPL
LDA fs_last_byte_flag,y ; Load param block pointer byte
8E10 STA (net_rx_ptr),y ; Save to NFS workspace via (net_rx_ptr)
8E12 DEY ; Next byte (descending)
8E13 BPL save1 ; Loop for all 3 bytes
8E15 INY ; Y=0 after BPL exit; INY makes Y=1
8E16 LDA (osword_pb_ptr),y ; Read sub-function code from (&F0)+1
8E18 RTS ; RTS dispatches to pushed handler
8E19 .fs_osword_tbl_lo←1← 8E07 LDA
EQUB <(osword_0f_handler-1) ; Dispatch table: low bytes for OSWORD &0F-&13 handlers
8E1A EQUB <(osword_10_handler-1)
8E1B EQUB <(osword_11_handler-1)
8E1C EQUB <(osword_12_handler-1)
8E1D EQUB <(econet_tx_rx-1)
8E1E .fs_osword_tbl_hi←1← 8E03 LDA
EQUB >(osword_0f_handler-1) ; Dispatch table: high bytes for OSWORD &0F-&13 handlers
8E1F EQUB >(osword_10_handler-1)
8E20 EQUB >(osword_11_handler-1)
8E21 EQUB >(osword_12_handler-1)
8E22 EQUB >(econet_tx_rx-1)

Bidirectional block copy between OSWORD param block and workspace.

C=1: copy X+1 bytes from (&F0),Y to (fs_crc_lo),Y (param to workspace) C=0: copy X+1 bytes from (fs_crc_lo),Y to (&F0),Y (workspace to param)

8E23 .copy_param_block←5← 8E31 BPL← 8E47 JSR← 8E63 JSR← 8E97 JSR← 8F32 JSR
BCC load_workspace_byte ; C=0: workspace to param direction
8E25 LDA (osword_pb_ptr),y ; Load byte from param block
8E27 STA (fs_crc_lo),y ; Store to workspace
8E29 BCS copyl3 ; Always taken (C still set) ALWAYS branch
8E2B .load_workspace_byte←1← 8E23 BCC
LDA (fs_crc_lo),y ; Load byte from workspace
8E2D STA (osword_pb_ptr),y ; Store to param block (no-op if C=1)
8E2F .copyl3←1← 8E29 BCS
INY ; Advance to next byte
8E30 DEX ; Decrement byte counter
8E31 BPL copy_param_block ; Loop while X >= 0
8E33 .return_copy_param←2← 8DFC BMI← 8E00 BCS
RTS ; Return after copy

OSWORD &0F handler: initiate transmit (CALLTX)

Checks the TX semaphore (TXCLR at &0D62) via ASL -- if carry is clear, a TX is already in progress and the call returns an error, preventing user code from corrupting a system transmit. Otherwise copies 16 bytes from the caller's OSWORD parameter block into the user TX control block (UTXCB) in static workspace. The TXCB pointer is copied to LTXCBP only after the semaphore is claimed, ensuring the low-level transmit code (BRIANX) sees a consistent pointer -- if copied before claiming, another transmitter could modify TXCBP between the copy and the claim.

On EntryXparameter block address low byte
Yparameter block address high byte
On ExitAcorrupted
Xcorrupted
Y&FF
8E34 .osword_0f_handler
ASL tx_ctrl_status ; ASL: set C if TX in progress
8E37 BCC osword_12_subfunc ; C=0: read path
8E39 LDA net_rx_ptr_hi ; User TX CB in workspace page (high byte)
8E3B STA fs_crc_hi ; Set param block high byte
8E3D STA nmi_tx_block_hi ; Set LTXCBP high byte for low-level TX
8E3F LDA #&6f ; &6F: offset into workspace for user TXCB
8E41 STA fs_crc_lo ; Set param block low byte
8E43 STA nmi_tx_block ; Set LTXCBP low byte for low-level TX
8E45 LDX #&0f ; X=15: copy 16 bytes (OSWORD param block)
8E47 JSR copy_param_block ; Copy param block to user TX control block
8E4A JSR trampoline_tx_setup ; Set up and start low-level transmit
8E4D JMP clear_svc_restore_args ; Exit: release service claim
8E50 .osword_12_subfunc←1← 8E37 BCC
SEC ; SEC: alternate entry for OSWORD &11
8E51 TYA ; A = param block high for branch
8E52 BCS readry ; ALWAYS branch

OSWORD &11 handler: read JSR arguments (READRA)

Copies the JSR (remote procedure call) argument buffer from the static workspace page back to the caller's OSWORD parameter block. Reads the buffer size from workspace offset JSRSIZ, then copies that many bytes. After the copy, clears the old LSTAT byte via CLRJSR to reset the protection status. Also provides READRB as a sub-entry (&8E6B) to return just the buffer size and args size without copying the data.

8E54 .osword_11_handler
LDA net_rx_ptr_hi ; Set source high byte from workspace page
8E56 STA fs_crc_hi ; Store as copy source high byte in &BF
8E58 LDY #&7f ; JSRSIZ at workspace offset &7F
8E5A LDA (net_rx_ptr),y ; Load buffer size from workspace
8E5C INY ; Y=&80: start of JSR argument data Y=&80
8E5D STY fs_crc_lo ; Store &80 as copy source low byte
8E5F TAX ; X = buffer size (loop counter)
8E60 DEX ; X = size-1 (0-based count for copy)
8E61 LDY #0 ; Y=0: start of destination param block
8E63 JSR copy_param_block ; Copy X+1 bytes from workspace to param
8E66 JSR clear_jsr_protection ; Reset JSR protection status
8E69 BCC set_carry_dispatch ; Branch to set carry and dispatch
8E6B .read_args_size←1← 8EBE BEQ
LDY #&7f ; Y=&7F: JSRSIZ offset (READRB entry)
8E6D LDA (net_rx_ptr),y ; Load buffer size from workspace
8E6F LDY #1 ; Y=1: param block offset for size byte
8E71 STA (osword_pb_ptr),y ; Store buffer size to (&F0)+1
8E73 INY ; Y=2: param block offset for args size Y=&02
8E74 LDA #&80 ; A=&80: argument data starts at offset &80
8E76 .readry←1← 8E52 BCS
STA (osword_pb_ptr),y ; Store args start offset to (&F0)+2
8E78 BCS carry_exit_or_read ; Always taken (SEC set above)
8E7A .osword_12_ws_offsets←1← 8E8E LDA
EQUB &FF, &01
fall through ↓

OSWORD &12 handler: read/set state information (RS)

Dispatches on the sub-function code (0-9): 0: read FS station (FSLOCN at &0E00) 1: set FS station 2: read printer server station (PSLOCN) 3: set printer server station 4: read protection masks (LSTAT at &D63) 5: set protection masks 6: read context handles (URD/CSD/LIB, converted from internal single-bit form back to handle numbers) 7: set context handles (converted to internal form) 8: read local station number 9: read JSR arguments buffer size Even-numbered sub-functions read; odd-numbered ones write. Uses the bidirectional copy at &8E23 for station read/set.

8E7C .osword_12_handler
CMP #6 ; Sub-function >= 6? (handle ops)
8E7E BCS rsl1 ; Sub >= 6: handle/station/error
8E80 CMP #4 ; Sub-function >= 4? (protection)
8E82 BCS rssl1 ; Sub-function 4 or 5: read/set protection
8E84 LSR ; LSR: 0->0, 1->0, 2->1, 3->1
8E85 LDX #&0d ; X=&0D: default to static workspace page
8E87 TAY ; Transfer LSR result to Y for indexing
8E88 BEQ set_workspace_page ; Y=0 (sub 0-1): use page &0D
8E8A LDX nfs_workspace_hi ; Y=1 (sub 2-3): use dynamic workspace
8E8C .set_workspace_page←1← 8E88 BEQ
STX fs_crc_hi ; Store workspace page in &BF (hi byte)
8E8E LDA osword_12_ws_offsets,y ; Load offset: &FF (sub 0-1) or &01 (sub 2-3)
8E91 STA fs_crc_lo ; Store offset in &BE (lo byte)
8E93 LDX #1 ; X=1: copy 2 bytes
8E95 LDY #1 ; Y=1: start at param block offset 1
8E97 JSR copy_param_block ; Copy station bytes to/from workspace
8E9A BNE set_carry_dispatch ; Always taken (Y=2 after copy)
8E9C .rssl1←1← 8E82 BCS
LSR ; LSR A: test bit 0 of sub-function
8E9D INY ; Y=1: offset for protection byte
8E9E LDA (osword_pb_ptr),y ; Load protection byte from param block
8EA0 BCS rssl2 ; C=1 (odd sub): set protection
8EA2 LDA prot_status ; C=0 (even sub): read current status
8EA5 STA (osword_pb_ptr),y ; Return current value to param block
8EA7 .rssl2←1← 8EA0 BCS
STA prot_status ; Update protection status
8EAA STA rx_ctrl_copy ; Also save as JSR mask backup
8EAD .set_carry_dispatch←2← 8E69 BCC← 8E9A BNE
SEC ; SEC: set carry for exit
8EAE BCS carry_exit_or_read ; ALWAYS branch
8EB0 .read_local_station←1← 8EBA BEQ
LDA tx_clear_flag ; Load local station number
8EB3 INY ; Y=1: param block offset for result
8EB4 STA (osword_pb_ptr),y ; Return station number to caller
8EB6 BCS carry_exit_or_read ; Always taken (C set above)
8EB8 .rsl1←1← 8E7E BCS
CMP #8 ; Sub-function 8: read FS handle
8EBA BEQ read_local_station ; Match: read handle from RX buffer
8EBC CMP #9 ; Sub-function 9: read args size
8EBE BEQ read_args_size ; Match: read ARGS buffer info
8EC0 BPL osword_12_error ; Sub >= 10 (bit 7 clear): read error
8EC2 LDY #3 ; Y=3: start from handle 3 (descending)
8EC4 LSR ; LSR: test read/write bit
8EC5 BCC readc1 ; C=0: read handles from workspace
8EC7 STY nfs_temp ; Init loop counter at Y=3
8EC9 .copy_handles_to_ws←1← 8ED8 BNE
LDY nfs_temp ; Reload loop counter
8ECB LDA (osword_pb_ptr),y ; Read handle from caller's param block
8ECD JSR handle_to_mask_a ; Convert handle number to bitmask
8ED0 TYA ; TYA: get bitmask result
8ED1 LDY nfs_temp ; Reload loop counter
8ED3 STA fs_server_net,y ; Store bitmask to FS server table
8ED6 DEC nfs_temp ; Next handle (descending)
8ED8 BNE copy_handles_to_ws ; Loop for handles 3,2,1
8EDA BEQ clear_svc_restore_args ; ALWAYS branch
8EDC .osword_12_error←1← 8EC0 BPL
INY ; Y=1: param block offset for error
8EDD LDA fs_last_error ; Load last FS error number
8EE0 STA (osword_pb_ptr),y ; Return error code to caller
8EE2 .carry_exit_or_read←3← 8E78 BCS← 8EAE BCS← 8EB6 BCS
BCS clear_svc_restore_args ; Exit with carry set
8EE4 .readc1←2← 8EC5 BCC← 8EED BNE
LDA fs_server_net,y ; A=single-bit bitmask
8EE7 JSR mask_to_handle ; Convert bitmask to handle number (FS2A)
8EEA STA (osword_pb_ptr),y ; A=handle number (&20-&27) Y=preserved
8EEC DEY ; Next handle (descending) Y=parameter block address high byte
8EED BNE readc1 ; Loop for handles 3,2,1
8EEF BEQ clear_svc_restore_args ; ALWAYS branch

OSWORD &10 handler: open/read RX control block (OPENRX)

If the first byte of the caller's parameter block is zero, scans for a free RXCB (flag byte = &3F = deleted) starting from RXCB #3 (RXCBs 0-2 are dedicated: printer, remote, FS). Returns the RXCB number in the first byte, or zero if none free. If the first byte is non-zero, reads the specified RXCB's data back into the caller's parameter block (12 bytes) and then deletes the RXCB by setting its flag byte to &3F -- a consume-once semantic so user code reads received data and frees the CB in a single atomic operation, preventing double-reads. The low-level user RX flag (LFLAG) is temporarily disabled via ROR/ROL during the operation to prevent the interrupt-driven receive code from modifying a CB that is being read or opened.

On EntryXparameter block address low byte
Yparameter block address high byte
On ExitAcorrupted
Xcorrupted
Y&FF
8EF1 .osword_10_handler
LDX nfs_workspace_hi ; Workspace page high byte
8EF3 STX fs_crc_hi ; Set up pointer high byte in &AC
8EF5 STY fs_crc_lo ; Save param block high byte in &AB
8EF7 ROR rx_status_flags ; Disable user RX during CB operation
8EFA LDA (osword_pb_ptr),y ; Read first byte of param block
8EFC STA fs_last_byte_flag ; Save: 0=open new, non-zero=read RXCB
8EFE .scan_or_read_rxcb
BNE read_rxcb ; Non-zero: read specified RXCB
8F00 LDA #3 ; Start scan from RXCB #3 (0-2 reserved)
8F02 .scan0←1← 8F14 BNE
JSR calc_handle_offset ; Convert RXCB number to workspace offset
8F05 BCS openl4 ; Invalid RXCB: return zero
8F07 LSR ; LSR twice: byte offset / 4
8F08 LSR ; Yields RXCB number from offset
8F09 TAX ; X = RXCB number for iteration
8F0A LDA (fs_crc_lo),y ; Read flag byte from RXCB workspace
8F0C BEQ openl4 ; Zero = end of CB list
8F0E CMP #&3f ; &3F = deleted slot, free for reuse
8F10 BEQ scan1 ; Found free slot
8F12 INX ; Try next RXCB
8F13 TXA ; A = next RXCB number
8F14 BNE scan0 ; Continue scan (always branches)
8F16 .scan1←1← 8F10 BEQ
TXA ; A = free RXCB number
8F17 LDX #0 ; X=0 for indexed indirect store
8F19 STA (osword_pb_ptr,x) ; Return RXCB number to caller's byte 0
8F1B .read_rxcb←1← 8EFE BNE
JSR calc_handle_offset ; Convert RXCB number to workspace offset
8F1E BCS openl4 ; Invalid: write zero to param block
8F20 DEY ; Y = offset-1: points to flag byte
8F21 STY fs_crc_lo ; Set &AB = workspace ptr low byte
8F23 LDA #&c0 ; &C0: test mask for flag byte
8F25 LDY #1 ; Y=1: flag byte offset in RXCB
8F27 LDX #&0b ; Enable interrupts before transmit
8F29 CPY fs_last_byte_flag ; Compare Y(1) with saved byte (open/read)
8F2B ADC (fs_crc_lo),y ; ADC flag: test if slot is in use
8F2D BEQ openl6 ; Dest station = &FFFF (accept reply from any station)
8F2F BMI openl7 ; Negative: slot has received data
8F31 .copy_rxcb_to_param←1← 8F41 BNE
CLC ; C=0: workspace-to-param direction
8F32 .openl6←1← 8F2D BEQ
JSR copy_param_block ; Copy RXCB data to param block
8F35 BCS reenable_rx ; Done: skip deletion on error
8F37 LDA #&3f ; Mark CB as consumed (consume-once)
8F39 LDY #1 ; Y=1: flag byte offset
8F3B STA (fs_crc_lo),y ; Write &3F to mark slot deleted
8F3D BNE reenable_rx ; Branch to exit (always taken) ALWAYS branch
8F3F .openl7←1← 8F2F BMI
ADC #1 ; Advance through multi-byte field
8F41 BNE copy_rxcb_to_param ; Loop until all bytes processed
8F43 DEY ; Y=-1 → Y=0 after STA below
8F44 .openl4←3← 8F05 BCS← 8F0C BEQ← 8F1E BCS
STA (osword_pb_ptr),y ; Return zero (no free RXCB found)
8F46 .reenable_rx←2← 8F35 BCS← 8F3D BNE
ROL rx_status_flags ; Re-enable user RX
fall through ↓

Clear service number and restore OSWORD args

Shared exit for OSWORD handlers. Zeros rom_svc_num to release the service claim, then copies 3 bytes from (net_rx_ptr) back to the fs_last_byte_flag area, restoring the OSWORD argument state saved at entry.

8F49 .clear_svc_restore_args←6← 8E4D JMP← 8EDA BEQ← 8EE2 BCS← 8EEF BEQ← 8FBF BNE← 9005 JMP
LDY #0 ; Y=0: clear service claim
8F4B STY rom_svc_num ; Release ROM service number
8F4D LDY #2 ; Y=2: copy 3 bytes (indices 2,1,0)
8F4F .rest1←1← 8F55 BPL
LDA (net_rx_ptr),y ; Load saved arg from (net_rx_ptr)+Y
8F51 STA fs_last_byte_flag,y ; Restore saved OSWORD argument byte
8F54 DEY ; Decrement byte counter
8F55 BPL rest1 ; Loop for bytes 2,1,0
8F57 RTS ; Return to caller

Set up RX buffer pointers in NFS workspace

Calculates the start address of the RX data area (&F0+1) and stores it at workspace offset &28. Also reads the data length from (&F0)+1 and adds it to &F0 to compute the end address at offset &2C.

On EntryCclear for ADC
8F58 .setup_rx_buffer_ptrs←1← 8F8B JSR
LDY #&28 ; Y=&28: RXCB template offset
8F5A LDA osword_pb_ptr ; A = base address low byte
8F5C ADC #1 ; A = base + 1 (skip length byte)
8F5E JSR store_16bit_at_y ; Receive data blocks until command byte = &00 or &0D
8F61 LDY #1 ; Read data length from (&F0)+1
8F63 LDA (osword_pb_ptr),y ; A = data length byte
8F65 LDY #&2c ; Workspace offset &20 = RX data end
8F67 ADC osword_pb_ptr ; A = base + length = end address low
8F69 .store_16bit_at_y←1← 8F5E JSR
STA (nfs_workspace),y ; Store low byte of 16-bit address
8F6B INY ; Advance to high byte offset
8F6C LDA osword_pb_ptr_hi ; A = high byte of base address
8F6E ADC #0 ; Add carry for 16-bit addition
8F70 STA (nfs_workspace),y ; Store high byte
8F72 RTS ; Return

Econet transmit/receive handler

A=0: Initialise TX control block from ROM template at &8311 (zero entries substituted from NMI workspace &0DDA), transmit it, set up RX control block, and receive reply. A>=1: Handle transmit result (branch to cleanup at &8F49).

On EntryA0=set up and transmit, >=1=handle TX result
8F73 .econet_tx_rx
CMP #1 ; A=0: set up and transmit; A>=1: handle result
8F75 BCS handle_tx_result ; A >= 1: handle TX result
8F77 LDY #&2f ; Y=&2F: start of template (descending)
8F79 .dofs01←1← 8F86 BNE
LDA init_tx_ctrl_port,y ; Load from ROM template (zero = use NMI workspace value)
8F7C BNE store_txcb_byte ; Non-zero = use ROM template byte as-is
8F7E LDA l0dda,y ; Zero = substitute from NMI workspace
8F81 .store_txcb_byte←1← 8F7C BNE
STA (nfs_workspace),y ; Store to dynamic workspace
8F83 DEY ; Descend through template
8F84 CPY #&23 ; Stop at offset &17
8F86 BNE dofs01 ; Loop until all bytes copied
8F88 INY ; Y=&18: TX block starts here
8F89 STY net_tx_ptr ; Point net_tx_ptr at workspace+&18
8F8B JSR setup_rx_buffer_ptrs ; Set up RX buffer start/end pointers
8F8E LDY #2 ; Y=2: port byte offset in RXCB
8F90 LDA #&90 ; A=&90: FS reply port
8F92 STA (osword_pb_ptr),y ; Store port &90 at (&F0)+2
8F94 INY ; Y=&03
8F95 INY ; Y=&04: advance to station address Y=&04
8F96 .copy_fs_addr←1← 8F9E BNE
LDA fs_context_base,y ; Copy FS station addr from workspace
8F99 STA (osword_pb_ptr),y ; Store to RX param block
8F9B INY ; Next byte
8F9C CPY #7 ; Done 3 bytes (Y=4,5,6)?
8F9E BNE copy_fs_addr ; No: continue copying
8FA0 LDA nfs_workspace_hi ; High byte of workspace for TX ptr
8FA2 STA net_tx_ptr_hi ; Store as TX pointer high byte
8FA4 CLI ; Enable interrupts before transmit
8FA5 JSR tx_poll_timeout ; Transmit with full retry
8FA8 LDY #&2c ; Y=&2C: RX end address offset
8FAA LDA #&ff ; Dest station = &FFFF (accept reply from any station)
8FAC STA (nfs_workspace),y ; Store end address low byte (&FF)
8FAE INY ; Y=&2d
8FAF STA (nfs_workspace),y ; Store end address high byte (&FF)
8FB1 LDY #&25 ; Y=&25: port byte in workspace RXCB
8FB3 LDA #&90 ; A=&90: FS reply port
8FB5 STA (nfs_workspace),y ; Store port to workspace RXCB
8FB7 DEY ; Y=&24
8FB8 LDA #&7f ; A=&7F: flag byte = waiting for reply
8FBA STA (nfs_workspace),y ; Store flag byte to workspace RXCB
8FBC JSR send_to_fs_star ; Initiate receive with timeout
8FBF BNE clear_svc_restore_args ; Non-zero = error/timeout: jump to cleanup
8FC1 .handle_tx_result←1← 8F75 BCS
PHP ; Save processor flags
8FC2 LDY #1 ; Y=1: first data byte offset
8FC4 LDA (osword_pb_ptr),y ; Load first data byte from RX buffer
8FC6 TAX ; X = first data byte (command code)
8FC7 INY ; Advance to next data byte Y=&02
8FC8 LDA (osword_pb_ptr),y ; Load station address high byte
8FCA INY ; Advance past station addr Y=&03
8FCB STY fs_crc_lo ; Save Y as data index
8FCD LDY #&72 ; Store station addr hi at (net_rx_ptr)+&72
8FCF STA (net_rx_ptr),y ; Store to workspace
8FD1 DEY ; Y=&71
8FD2 TXA ; A = command code (from X)
8FD3 STA (net_rx_ptr),y ; Store station addr lo at (net_rx_ptr)+&71
8FD5 PLP ; Restore flags from earlier PHP
8FD6 BNE dofs2 ; First call: adjust data length
8FD8 .send_data_bytes←1← 8FF2 BNE
LDY fs_crc_lo ; Receive data blocks until command byte = &00 or &0D
8FDA INC fs_crc_lo ; Advance data index for next iteration
8FDC LDA (osword_pb_ptr),y ; Load next data byte
8FDE LDY #&7d ; Y=&7D: store byte for TX at offset &7D
8FE0 STA (net_rx_ptr),y ; Store data byte at (net_rx_ptr)+&7D for TX
8FE2 PHA ; Save data byte for &0D check after TX
8FE3 JSR ctrl_block_setup_alt ; Set up TX control block
8FE6 CLI ; Enable interrupts for TX
8FE7 JSR tx_poll_core ; Enable IRQs and transmit Core transmit and poll routine (XMIT)
8FEA .delay_between_tx←1← 8FEB BNE
DEX ; Short delay loop between TX packets
8FEB BNE delay_between_tx ; Spin until X reaches 0
8FED PLA ; Restore data byte for terminator check
8FEE BEQ rx_first_packet ; Z=1: not intercepted, pass through
8FF0 EOR #&0d ; Test for end-of-data marker (&0D)
8FF2 BNE send_data_bytes ; Not &0D: continue with next byte
8FF4 .rx_first_packet←1← 8FEE BEQ
BEQ jmp_clear_svc_restore ; First packet: exit handler
8FF6 .dofs2←1← 8FD6 BNE
JSR ctrl_block_setup_alt ; First-packet: set up control block
8FF9 LDY #&7b ; Y=&7B: data length offset
8FFB LDA (net_rx_ptr),y ; Load current data length
8FFD ADC #3 ; Adjust data length by 3 for header bytes
8FFF STA (net_rx_ptr),y ; Store adjusted length
fall through ↓

Enable interrupts and transmit via tx_poll_ff

CLI to enable interrupts, then JMP tx_poll_ff. A short tail-call wrapper used after building the TX control block.

9001 .enable_irq_and_tx
CLI ; Enable interrupts
9002 JSR tx_poll_ff ; Poll TX until complete
9005 .jmp_clear_svc_restore←1← 8FF4 BEQ
JMP clear_svc_restore_args ; Transmit via tx_poll_ff

NETVEC dispatch handler (ENTRY)

Indirected from NETVEC at &0224. Saves all registers and flags, retrieves the reason code from the stacked A, and dispatches to one of 9 handlers (codes 0-8) via the PHA/PHA/RTS trampoline at &9021. Reason codes >= 9 are ignored.

Dispatch targets (from NFS09): 0: no-op (RTS) 1-3: PRINT -- chars in printer buffer / Ctrl-B / Ctrl-C 4: NWRCH -- write character to screen (net write char) 5: SELECT -- printer selection changed 6: no-op (net read char -- not implemented) 7: NBYTE -- remote OSBYTE call 8: NWORD -- remote OSWORD call

On EntryAreason code (0-8)
On ExitApreserved
Xpreserved
Ypreserved
9008 .osword_dispatch
PHP ; Save processor status
9009 PHA ; Save A (reason code)
900A TXA ; Save X
900B PHA ; Push X to stack
900C TYA ; Save Y
900D PHA ; Push Y to stack
900E TSX ; Get stack pointer for indexed access
900F LDA l0103,x ; Retrieve original A (function code) from stack
9012 CMP #9 ; Reason codes 0-8 only
9014 BCS entry1 ; Code >= 9: skip dispatch, restore regs
9016 TAX ; X = reason code for table lookup
9017 JSR osword_trampoline ; Dispatch to handler via trampoline
901A .entry1←1← 9014 BCS
PLA ; Restore Y
901B TAY ; Transfer to Y register
901C PLA ; Restore X
901D TAX ; Transfer to X register
901E PLA ; Restore A
901F PLP ; Restore processor status flags
9020 RTS ; Return with all registers preserved
9021 .osword_trampoline←1← 9017 JSR
LDA osword_tbl_hi,x ; PHA/PHA/RTS trampoline: push handler addr-1, RTS jumps to it
9024 PHA ; Push high byte of handler address
9025 LDA osword_tbl_lo,x ; Load handler low byte from table
9028 PHA ; Push low byte of handler address
9029 LDA osbyte_a_copy ; Load workspace byte &EF for handler
902B RTS ; RTS dispatches to pushed handler
902C .osword_tbl_lo←1← 9025 LDA
EQUB <(return_2-1)
902D EQUB <(remote_print_handler-1)
902E EQUB <(remote_print_handler-1)
902F EQUB <(remote_print_handler-1)
9030 EQUB <(net_write_char-1)
9031 EQUB <(printer_select_handler-1)
9032 EQUB <(return_2-1)
9033 EQUB <(remote_cmd_dispatch-1)
9034 EQUB <(remote_cmd_data-1)
9035 .osword_tbl_hi←1← 9021 LDA
EQUB >(return_2-1)
9036 EQUB >(remote_print_handler-1)
9037 EQUB >(remote_print_handler-1)
9038 EQUB >(remote_print_handler-1)
9039 EQUB >(net_write_char-1)
903A EQUB >(printer_select_handler-1)
903B EQUB >(return_2-1)
903C EQUB >(remote_cmd_dispatch-1)
903D EQUB >(remote_cmd_data-1)

Fn 4: net write character (NWRCH)

Writes a character (passed in Y) to the screen via OSWRITCH. Before the write, uses TSX to reach into the stack and zero the carry flag in the caller's saved processor status byte -- ROR followed by ASL on the stacked P byte (&0106,X) shifts carry out and back in as zero. This ensures the calling code's PLP restores carry=0, signalling "character accepted" without needing a separate CLC/PHP sequence. A classic 6502 trick for modifying return flags without touching the actual processor status.

On EntryYcharacter to write
On ExitA&3F
X0
Y0
903E .net_write_char
TSX ; Get stack pointer for flag access
903F ROR l0106,x ; ROR/ASL on stacked P: zeros carry to signal success
9042 ASL l0106,x ; ASL: restore P after ROR zeroed carry
9045 TYA ; Y = character to write
9046 LDY #&da ; Store character at workspace offset &DA
9048 STA (nfs_workspace),y ; Store char at workspace offset &DA
904A LDA #0 ; A=0: command type for net write char
fall through ↓

Set up TX control block and send

Builds a TX control block at (nfs_workspace)+&0C from the current workspace state, then initiates transmission via the ADLC TX path. This is the common send routine used after command data has been prepared. The exact control block layout and field mapping need further analysis.

On EntryAcommand type byte
904C .setup_tx_and_send←3← 8159 JSR← 9093 JSR← 90F4 JSR
LDY #&d9 ; Y=&D9: command type offset
904E STA (nfs_workspace),y ; Store command type at ws+&D9
9050 LDA #&80 ; Mark TX control block as active (&80)
9052 LDY #&0c ; Y=&0C: TXCB start offset
9054 STA (nfs_workspace),y ; Set TX active flag at ws+&0C
9056 STY net_tx_ptr ; Redirect net_tx_ptr low to workspace
9058 LDX nfs_workspace_hi ; Load workspace page high byte
905A STX net_tx_ptr_hi ; Complete ptr redirect
905C JSR tx_poll_ff ; Transmit with full retry
905F LDA #&3f ; Mark TXCB as deleted (&3F) after transmit
9061 STA (net_tx_ptr,x) ; Write &3F to TXCB byte 0
9063 RTS ; Return

Fn 7: remote OSBYTE handler (NBYTE)

Full RPC mechanism for OSBYTE calls across the network. When a machine is remoted, OSBYTE/OSWORD calls that affect terminal-side hardware (keyboard scanning, flash rates, etc.) must be indirected across the net. OSBYTE calls are classified into three categories: Y>0 (NCTBPL table): executed on BOTH machines (flash rates etc.) Y<0 (NCTBMI table): executed on terminal only, result sent back Y=0: not recognised, passed through unhandled Results returned via stack manipulation: the saved processor status byte at &0106 has V-flag (bit 6) forced on to tell the MOS the call was claimed (preventing dispatch to other ROMs), and the I-bit (bit 2) forced on to disable interrupts during register restoration, preventing race conditions. The carry flag in the saved P is also manipulated via ROR/ASL to zero it, signaling success to the caller. OSBYTE &81 (INKEY) gets special handling as it must read the terminal's keyboard.

9064 .remote_cmd_dispatch
LDY osword_pb_ptr_hi ; Load original Y (OSBYTE secondary param)
9066 CMP #&81 ; OSBYTE &81 (INKEY): always forward to terminal
9068 BEQ dispatch_remote_osbyte ; Forward &81 to terminal for keyboard read
906A LDY #1 ; Y=1: search NCTBPL table (execute on both)
906C LDX #7 ; X=7: 8-entry NCTBPL table size
906E JSR match_osbyte_code ; Search for OSBYTE code in NCTBPL table
9071 BEQ dispatch_remote_osbyte ; Match found: dispatch with Y=1 (both)
9073 DEY ; Y=-1: search NCTBMI table (terminal only)
9074 DEY ; Second DEY: Y=&FF (from 1 via 0)
9075 LDX #&0e ; X=&0E: 15-entry NCTBMI table size
9077 JSR match_osbyte_code ; Search for OSBYTE code in NCTBMI table
907A BEQ dispatch_remote_osbyte ; Match found: dispatch with Y=&FF (terminal)
907C INY ; Y=0: OSBYTE not recognised, ignore
907D .dispatch_remote_osbyte←3← 9068 BEQ← 9071 BEQ← 907A BEQ
LDX #2 ; X=2 bytes to copy (default for RBYTE)
907F TYA ; A=Y: check table match result
9080 BEQ return_nbyte ; Y=0: not recognised, return unhandled
9082 PHP ; Y>0 (NCTBPL): send only, no result expected
9083 BPL nbyte6 ; Y>0 (NCTBPL): no result expected, skip RX
9085 INX ; Y<0 (NCTBMI): X=3 bytes (result + P flags) X=&03
9086 .nbyte6←1← 9083 BPL
LDY #&dc ; Y=&DC: top of 3-byte stack frame region
9088 .nbyte1←1← 9090 BPL
LDA tube_claimed_id,y ; Copy OSBYTE args from stack frame to workspace
908B STA (nfs_workspace),y ; Store to NFS workspace for transmission
908D DEY ; Next byte (descending)
908E CPY #&da ; Copied all 3 bytes? (&DC, &DB, &DA)
9090 BPL nbyte1 ; Loop for remaining bytes
9092 TXA ; A = byte count for setup_tx_and_send
9093 JSR setup_tx_and_send ; Build TXCB and transmit to terminal
9096 PLP ; Restore N flag from table match type
9097 BPL return_nbyte ; Y was positive (NCTBPL): done, no result
9099 LDA #&7f ; Set up RX control block to wait for reply
909B STA (net_tx_ptr,x) ; Write &7F to RXCB (wait for reply)
909D .poll_rxcb_loop←1← 909F BPL
LDA (net_tx_ptr,x) ; Poll RXCB for completion (bit7)
909F BPL poll_rxcb_loop ; Bit7 clear: still waiting, poll again
90A1 TSX ; X = stack pointer for register restoration
90A2 LDY #&dd ; Y=&DD: saved P byte offset in workspace
90A4 LDA (nfs_workspace),y ; Load remote processor status from reply
90A6 ORA #&44 ; Force V=1 (claimed) and I=1 (no IRQ) in saved P
90A8 BNE nbyte5 ; ALWAYS branch (ORA #&44 never zero) ALWAYS branch
90AA .nbyte4←1← 90B3 BNE
DEY ; Previous workspace offset
90AB DEX ; Previous stack register slot
90AC LDA (nfs_workspace),y ; Load next result byte (X, then Y)
90AE .nbyte5←1← 90A8 BNE
STA l0106,x ; Write result bytes to stacked registers
90B1 CPY #&da ; Copied all result bytes? (P at &DA)
90B3 BNE nbyte4 ; Loop for remaining result bytes
90B5 .return_nbyte←2← 9080 BEQ← 9097 BPL
RTS ; Return to OSBYTE dispatcher

Search remote OSBYTE table for match (NCALLP)

Searches remote_osbyte_table for OSBYTE code A. X indexes the last entry to check (table is scanned X..0). Returns Z=1 if found. Called twice by remote_cmd_dispatch:

  X=7  -> first 8 entries (NCTBPL: execute on both machines)
  X=14 -> all 15 entries (NCTBMI: execute on terminal only)

The last 7 entries (&0B, &0C, &0F, &79, &7A, &E3, &E4) are terminal-only because they affect the local keyboard, buffers, or function keys.

On entry: A = OSBYTE code, X = table size - 1 On exit: Z=1 if match found, Z=0 if not

90B6 .match_osbyte_code←3← 906E JSR← 9077 JSR← 90BC BPL
CMP remote_osbyte_table,x ; Compare OSBYTE code with table entry
90B9 BEQ return_match_osbyte ; Match found: return with Z=1
90BB DEX ; Next table entry (descending)
90BC BPL match_osbyte_code ; Loop for remaining entries
90BE .return_match_osbyte←1← 90B9 BEQ
RTS ; Return; Z=1 if match, Z=0 if not
90BF .remote_osbyte_table←1← 90B6 CMP
EQUB &04 ; OSBYTE &04: cursor key status
90C0 EQUB &09 ; OSBYTE &09: flash duration (1st colour)
90C1 EQUB &0A ; OSBYTE &0A: flash duration (2nd colour)
90C2 EQUB &14 ; OSBYTE &14: explode soft character RAM
90C3 EQUB &9A ; OSBYTE &9A: video ULA control register
90C4 EQUB &9B ; OSBYTE &9B: video ULA palette
90C5 EQUB &9C ; OSBYTE &9C: ACIA control register
90C6 EQUB &E2 ; OSBYTE &E2: function key &D0-&DF
90C7 EQUB &0B ; OSBYTE &0B: auto-repeat delay
90C8 EQUB &0C ; OSBYTE &0C: auto-repeat rate
90C9 EQUB &0F ; OSBYTE &0F: flush buffer class
90CA EQUB &79 ; OSBYTE &79: keyboard scan from X
90CB EQUB &7A ; OSBYTE &7A: keyboard scan from &10
90CC EQUB &E3 ; OSBYTE &E3: function key &E0-&EF
90CD EQUB &E4 ; OSBYTE &E4: function key &F0-&FF

Fn 8: remote OSWORD handler (NWORD)

Only intercepts OSWORD 7 (make a sound) and OSWORD 8 (define an envelope). Unlike NBYTE which returns results, NWORD is entirely fire-and-forget — no return path is implemented. The developer explicitly noted this was acceptable since sound/envelope commands don't return meaningful results. Copies up to 14 parameter bytes from the RX buffer to workspace, tags the message as RWORD, and transmits.

90CE .remote_cmd_data
LDY #&0e ; Y=&0E: 14-byte parameter block
90D0 CMP #7 ; OSWORD 7? (make sound)
90D2 BEQ copy_params_rword ; OSWORD 7 (sound): handle via common path
90D4 CMP #8 ; OSWORD 8 = define an envelope
90D6 BNE return_remote_cmd ; Not OSWORD 7 or 8: ignore (BNE exits)
90D8 .copy_params_rword←1← 90D2 BEQ
LDX #&db ; Point workspace to offset &DB for params
90DA STX nfs_workspace ; Store workspace ptr offset &DB
90DC .copy_osword_params←1← 90E1 BPL
LDA (osword_pb_ptr),y ; Load param byte from OSWORD param block
90DE STA (nfs_workspace),y ; Write param byte to workspace
90E0 DEY ; Next byte (descending)
90E1 BPL copy_osword_params ; Loop for all parameter bytes
90E3 INY ; Y=0 after loop
90E4 DEC nfs_workspace ; Point workspace to offset &DA
90E6 LDA osbyte_a_copy ; Load original OSWORD code
90E8 STA (nfs_workspace),y ; Store OSWORD code at ws+0
90EA STY nfs_workspace ; Reset workspace ptr to base
90EC LDY #&14 ; Y=&14: command type offset
90EE LDA #&e9 ; Tag as RWORD (port &E9)
90F0 STA (nfs_workspace),y ; Store port tag at ws+&14
90F2 LDA #1 ; A=1: single-byte TX
90F4 JSR setup_tx_and_send ; Set up TX and send RWORD packet
90F7 STX nfs_workspace ; Restore workspace ptr
90F9 JSR ctrl_block_setup_alt ; Set up control block for reply
90FC .return_remote_cmd←1← 90D6 BNE
RTS ; Return from remote command handler

Remote boot/execute handler

Validates byte 4 of the RX control block (must be zero), copies the 2-byte execution address from RX offsets &80/&81 into NFS workspace, sets up a control block, disables keyboard (OSBYTE &C9), then falls through to lang_3_execute_at_0100.

90FD .lang_1_remote_boot
LDY #4 ; Y=4: RX control block byte 4
90FF LDA (net_rx_ptr),y ; Load first data byte from RX
9101 BEQ remot1 ; Zero: standard boot, skip code
9103 .rchex←1← 9149 BNE
JMP clear_jsr_protection ; Load language ROM number
9106 .remot1←2← 9101 BEQ← 913F BEQ
ORA #9 ; OR with 9: set remote boot bits
9108 STA (net_rx_ptr),y ; Store modified control byte
910A LDX #&80 ; X=&80: exec address offset lo
910C LDY #&80 ; Y=&80: exec address offset hi
910E LDA (net_rx_ptr),y ; Load exec address low byte
9110 PHA ; Save boot type on stack
9111 INY ; Y=&81
9112 LDA (net_rx_ptr),y ; Load exec address high byte
9114 LDY #&0f ; Y=&0F: workspace offset for hi
9116 STA (nfs_workspace),y ; Store filename offset at ws+&0F
9118 DEY ; Y=&0e
9119 PLA ; Restore boot type from stack
911A STA (nfs_workspace),y ; Copy command to &0100 area
911C JSR clear_osbyte_ce_cf ; Initialize OSBYTE vectors
911F JSR ctrl_block_setup ; Set up control block for boot
9122 LDX #1 ; X=1: enable parameter
9124 LDY #0 ; Y=0: second argument for OSBYTE
9126 LDA #osbyte_read_write_econet_keyboard_disable ; A=&C9: disable keyboard for boot
9128 JSR osbyte ; Disable keyboard (for Econet)
fall through ↓

Execute downloaded code at &0100

Zeroes &0100-&0102 (safe BRK default), restores the protection mask, and JMP &0100 to execute code received over the network.

912B .lang_3_execute_at_0100
LDX #2 ; X=2: zero 3 bytes (offsets 2,1,0)
912D LDA #0 ; A=0: zero / BRK opcode
912F .zero_0100_loop←1← 9133 BPL
STA l0100,x ; Store zero at &0100+X
9132 DEX ; Decrement byte counter
9133 BPL zero_0100_loop ; Loop until 3 bytes zeroed
9135 JSR clear_jsr_protection ; Release JSR protection mask
9138 JMP l0100 ; Execute downloaded code at &0100

Remote operation with source validation (REMOT)

Validates that the source station/network in the received packet matches the controlling station stored in the remote RXCB. This ensures that only the station that initiated the remote session can send commands — characters from other stations are rejected. Full init sequence: 1) disable keyboard, 2) set workspace ptr, 3) set status busy, 4) set R/W/byte/word masks, 5) set up CB, 6) set MODE 7 (the only mode guaranteed for terminal emulation), 7) set auto repeat rates, 8) enter current language. This is essentially a "thin terminal" setup — the local machine becomes a remote display/keyboard for the controlling station. Bit 0 of the status byte disallows further remote takeover attempts (preventing re-entrant remote control), while bit 3 marks the machine as currently remoted.

913B .lang_4_remote_validated
LDY #4 ; Y=4: validation byte offset
913D LDA (net_rx_ptr),y ; Load validation byte from RX data
913F BEQ remot1 ; Zero: validation passed, continue
9141 LDY #&80 ; Y=&80: source station offset
9143 LDA (net_rx_ptr),y ; Load source station from RX buffer
9145 LDY #&0e ; Y=&0E: controlling station offset
9147 CMP (nfs_workspace),y ; Compare with controlling station
9149 BNE rchex ; Mismatch: reject remote command
fall through ↓

Insert remote keypress

Reads a character from RX block offset &82 and inserts it into keyboard input buffer 0 via OSBYTE &99.

914B .lang_0_insert_remote_key
LDY #&82 ; Y=&82: character offset in RX data
914D LDA (net_rx_ptr),y ; Load remote keypress character
914F TAY ; Transfer character to Y
9150 LDX #0 ; X=0: keyboard input buffer
9152 JSR clear_jsr_protection ; Release JSR protection before call
9155 LDA #osbyte_insert_input_buffer ; A=&99: OSBYTE insert into buffer
9157 JMP osbyte ; Insert character Y into input buffer X

Alternate entry into control block setup

Sets X=&0D, Y=&7C. Tests bit 6 of &833B to choose target: V=0 (bit 6 clear): stores to (nfs_workspace) V=1 (bit 6 set): stores to (net_rx_ptr)

915A .ctrl_block_setup_alt←3← 8FE3 JSR← 8FF6 JSR← 90F9 JSR
LDX #&0d ; X=&0D: template offset for alt entry
915C LDY #&7c ; Y=&7C: target workspace offset for alt entry
915E BIT tx_ctrl_upper ; BIT test: V flag = bit 6 of &83B3
9161 BVS cbset2 ; V=1: store to (net_rx_ptr) instead
fall through ↓

Control block setup — main entry

Sets X=&1A, Y=&17, clears V (stores to nfs_workspace). Reads the template table at &918F indexed by X, storing each value into the target workspace at offset Y. Both X and Y are decremented on each iteration.

Template sentinel values: &FE = stop (end of template for this entry path) &FD = skip (leave existing value unchanged) &FC = use page high byte of target pointer

9163 .ctrl_block_setup←1← 911F JSR
LDY #&17 ; Y=&17: workspace target offset (main entry)
9165 LDX #&1a ; X=&1A: template table index (main entry)
9167 .ctrl_block_setup_clv←1← 922C JSR
CLV ; V=0: target is (nfs_workspace)
9168 .cbset2←2← 9161 BVS← 9189 BPL
LDA ctrl_block_template,x ; Load template byte from ctrl_block_template[X]
916B CMP #&fe ; &FE = stop sentinel
916D BEQ cb_template_tail ; End of template: jump to exit
916F CMP #&fd ; &FD = skip sentinel
9171 BEQ cb_template_main_start ; Skip: don't store, just decrement Y
9173 CMP #&fc ; &FC = page byte sentinel
9175 BNE cbset3 ; Not sentinel: store template value directly
9177 LDA net_rx_ptr_hi ; V=1: use (net_rx_ptr) page
9179 BVS rxcb_matched ; V=1: skip to net_rx_ptr page
917B LDA nfs_workspace_hi ; V=0: use (nfs_workspace) page
917D .rxcb_matched←1← 9179 BVS
STA net_tx_ptr_hi ; PAGE byte → Y=&02 / Y=&74
917F .cbset3←1← 9175 BNE
BVS cbset4 ; → Y=&04 / Y=&76
9181 STA (nfs_workspace),y ; PAGE byte → Y=&06 / Y=&78
9183 BVC cb_template_main_start ; → Y=&08 / Y=&7A ALWAYS branch
9185 .cbset4←1← 917F BVS
STA (net_rx_ptr),y ; Alt-path only → Y=&70
9187 .cb_template_main_start←2← 9171 BEQ← 9183 BVC
DEY ; → Y=&0C (main only)
9188 DEX ; → Y=&0D (main only)
9189 BPL cbset2 ; Loop until all template bytes done
918B .cb_template_tail←1← 916D BEQ
INY ; → Y=&10 (main only)
918C STY net_tx_ptr ; Store final offset as net_tx_ptr
918E RTS ; → Y=&07 / Y=&79

Control block initialisation template

Read by the loop at &9168, indexed by X from a starting value down to 0. Values are stored into either (nfs_workspace) or (net_rx_ptr) at offset Y, depending on the V flag.

Two entry paths read different slices of this table: ctrl_block_setup: X=&1A (26) down, Y=&17 (23) down, V=0 ctrl_block_setup_alt: X=&0D (13) down, Y=&7C (124) down, V from BIT &833B

Sentinel values: &FE = stop processing &FD = skip this offset (decrement Y but don't store) &FC = substitute the page byte (net_rx_ptr_hi or nfs_workspace_hi)

918F .ctrl_block_template←1← 9168 LDA
EQUB &85 ; Alt-path only → Y=&6F
9190 EQUB &00 ; Alt-path only → Y=&70
9191 EQUB &FD ; SKIP
9192 EQUB &FD ; SKIP
9193 EQUB &7D ; → Y=&01 / Y=&73
9194 EQUB &FC ; PAGE byte → Y=&02 / Y=&74
9195 EQUB &FF ; → Y=&03 / Y=&75
9196 EQUB &FF ; → Y=&04 / Y=&76
9197 EQUB &7E ; → Y=&05 / Y=&77
9198 EQUB &FC ; PAGE byte → Y=&06 / Y=&78
9199 EQUB &FF ; → Y=&07 / Y=&79
919A EQUB &FF ; → Y=&08 / Y=&7A
919B EQUB &00 ; → Y=&09 / Y=&7B
919C EQUB &00 ; → Y=&0A / Y=&7C
919D EQUB &FE ; STOP — main-path boundary
919E EQUB &80 ; → Y=&0C (main only)
919F EQUB &93 ; → Y=&0D (main only)
91A0 EQUB &FD ; SKIP (main only)
91A1 EQUB &FD ; SKIP (main only)
91A2 EQUB &D9 ; → Y=&10 (main only)
91A3 EQUB &FC ; PAGE byte → Y=&11 (main only)
91A4 EQUB &FF ; → Y=&12 (main only)
91A5 EQUB &FF ; → Y=&13 (main only)
91A6 EQUB &DE ; → Y=&14 (main only)
91A7 EQUB &FC ; PAGE byte → Y=&15 (main only)
91A8 EQUB &FF ; → Y=&16 (main only)
91A9 EQUB &FF ; → Y=&17 (main only)
91AA EQUB &FE, &D1, &FD, &FD, &1F, &FD, &FF, &FF, &FD, &FD, &FF, &FF

Fn 5: printer selection changed (SELECT)

Called when the printer selection changes. Compares the new selection (in PARMX) against the network printer (buffer 4). If it matches, initialises the printer buffer pointer (PBUFFP) and sets the initial flag byte (&41). Otherwise just updates the printer status flags (PFLAGS).

On EntryX1-based buffer number
91B6 .printer_select_handler
LDA #0 ; A=0: clear printer buffer state
91B8 DEX ; X-1: convert 1-based buffer to 0-based
91B9 CPX osword_pb_ptr ; Is this the network printer buffer?
91BB BNE setup1 ; No: skip printer init
91BD LDA #&1f ; &1F = initial buffer pointer offset
91BF STA printer_buf_ptr ; Reset printer buffer write position
91C2 LDA #&43 ; &41 = initial PFLAGS (bit 6 set, bit 0 set)
91C4 .setup1←1← 91BB BNE
STA l0d60 ; Store initial PFLAGS value
91C7 .return_printer_select←2← 91CA BNE← 91DE BCS
RTS ; Return

Fn 1/2/3: network printer handler (PRINT)

Handles network printer output. Reason 1 = chars in buffer (extract from MOS buffer 3 and accumulate), reason 2 = Ctrl-B (start print), reason 3 = Ctrl-C (end print). The printer status byte PFLAGS uses: bit 7 = sequence number (toggles per packet for dup detection) bit 6 = always 1 (validity marker) bit 0 = 0 when print active Print streams reuse the BSXMIT (byte-stream transmit) code with handle=0, which causes the AND SEQNOS to produce zero and sidestep per-file sequence tracking. After transmission, TXCB pointer bytes are filled with &FF to prevent stale values corrupting subsequent BGET/BPUT operations (a historically significant bug fix). N.B. The printer and REMOTE facility share the same dynamically allocated static workspace page via WORKP1 (&9E,&9F) — care must be taken to never leave the pointer corrupted, as corruption would cause one subsystem to overwrite the other's data. Only handles buffer 4 (network printer); others are ignored.

On EntryXreason code (1=chars, 2=Ctrl-B, 3=Ctrl-C)
Ybuffer number (must be 4 for network printer)
91C8 .remote_print_handler
CPY #4 ; Only handle buffer 4 (network printer)
91CA BNE return_printer_select ; Not buffer 4: ignore
91CC TXA ; A = reason code
91CD DEX ; Reason 1? (DEX: 1->0)
91CE BNE toggle_print_flag ; Not reason 1: handle Ctrl-B/C
91D0 TSX ; Get stack pointer for P register
91D1 ORA l0106,x ; Force I flag in stacked P to block IRQs
91D4 STA l0106,x ; Write back modified P register
91D7 .prlp1←2← 91E6 BCC← 91EB BNE
LDA #osbyte_read_buffer ; OSBYTE &91: extract char from MOS buffer
91D9 LDX #buffer_printer ; X=3: printer buffer number
91DB JSR osbyte ; Get character from input buffer (C is set if the buffer is empty, otherwise Y=extracted character)
91DE BCS return_printer_select ; Buffer empty: return
91E0 TYA ; Y = extracted character Y is the character extracted from the buffer
91E1 JSR store_output_byte ; Store char in output buffer
91E4 CPY #&6e ; Buffer nearly full? (&6E = threshold)
91E6 BCC prlp1 ; Not full: get next char
91E8 JSR flush_output_block ; Buffer full: flush to network
91EB BNE prlp1 ; Flush done: continue loop
fall through ↓

Store output byte to network buffer

Stores byte A at the current output offset in the RX buffer pointed to by (net_rx_ptr). Advances the offset counter and triggers a flush if the buffer is full.

On EntryAbyte to store
On ExitYbuffer offset before store
91ED .store_output_byte←2← 91E1 JSR← 91F7 JSR
LDY printer_buf_ptr ; Load current buffer offset
91F0 STA (net_rx_ptr),y ; Store byte at current position
91F2 INC printer_buf_ptr ; Advance buffer pointer
91F5 RTS ; Return; Y = buffer offset
91F6 .toggle_print_flag←1← 91CE BNE
PHA ; Save reason code
91F7 JSR store_output_byte ; Decrement transfer count low byte
91FA EOR l0d60 ; XOR with transfer count flags
91FD ROR ; Shift bit 0 into carry
91FE BCC rx_data_phase ; Data phase active: continue
9200 LDA l0d60 ; Load transfer count flags
9203 ROR ; Shift bit 0 into carry
9204 BCC rx_imm_discard ; Bit 0=0 (active): just flush
9206 ROL ; Rotate carry back
9207 AND #&7f ; Mask off control bits
9209 STA l0d60 ; Store updated flags
920C .rx_imm_discard←1← 9204 BCC
JSR flush_output_block ; Flush accumulated output to network
920F .rx_data_phase←1← 91FE BCC
ROR l0d60 ; Save PFLAGS bit 0 via carry
9212 PLA ; Restore original reason code
9213 ROR ; Old PFLAGS bit 0 to A bit 7
9214 ROL l0d60 ; Reason bit 0 into PFLAGS bit 0
9217 RTS ; Return

Flush output block

Sends the accumulated output block over the network, resets the buffer pointer, and prepares for the next block of output data.

9218 .flush_output_block←2← 91E8 JSR← 920C JSR
LDY #8 ; Store buffer length at workspace offset &08
921A LDA printer_buf_ptr ; Current buffer fill position
921D STA (nfs_workspace),y ; Write to workspace offset &08
921F LDA net_rx_ptr_hi ; Store page high byte at offset &09
9221 INY ; Y=&09
9222 STA (nfs_workspace),y ; Write page high byte at offset &09
9224 LDY #5 ; Also store at offset &05
9226 STA (nfs_workspace),y ; (end address high byte)
9228 LDY #&0b ; Y=&0B: flag byte offset
922A LDX #&26 ; X=&26: start from template entry &26
922C JSR ctrl_block_setup_clv ; Reuse ctrl_block_setup with CLV entry
922F DEY ; Y=&0A: sequence flag byte offset
9230 LDA l0d60 ; Load current PFLAGS
9233 PHA ; Save current PFLAGS
9234 ROL ; Carry = current sequence (bit 7)
9235 PLA ; Restore original PFLAGS
9236 EOR #&80 ; Toggle sequence number (bit 7 of PFLAGS)
9238 STA l0d60 ; Store toggled sequence number
923B ROL ; Old sequence bit into bit 0
923C STA (nfs_workspace),y ; Store sequence flag at offset &0A
923E LDY #&1f ; Y=&1F: buffer start offset
9240 STY printer_buf_ptr ; Reset printer buffer to start (&1F)
9243 LDA #0 ; A=0: printer output flag
9245 TAX ; X=0: workspace low byte X=&00
9246 LDY nfs_workspace_hi ; Y = workspace page high byte
9248 CLI ; Enable interrupts before TX
fall through ↓

Transmit with retry loop (XMITFS/XMITFY)

Calls the low-level transmit routine (BRIANX) with FSTRY (&FF = 255) retries and FSDELY (&60 = 96) ms delay between attempts. On each iteration, checks the result code: zero means success, non-zero means retry. After all retries exhausted, reports a 'Net error'. Entry point XMITFY allows a custom delay in Y.

On EntryAhandle bitmask (0=printer, non-zero=file)
XTX control block address low
YTX control block address high
9249 .econet_tx_retry←2← 839C JSR← 83D4 JSR
STX net_tx_ptr ; Set TX control block ptr low byte
924B STY net_tx_ptr_hi ; Set TX control block ptr high byte
924D PHA ; Save A (handle bitmask) for later
924E AND fs_sequence_nos ; Compute sequence bit from handle
9251 BEQ bsxl1 ; Zero: no sequence bit set
9253 LDA #1 ; Non-zero: normalise to bit 0
9255 .bsxl1←1← 9251 BEQ
LDY #0 ; Y=0: flag byte offset in TXCB
9257 ORA (net_tx_ptr),y ; Merge sequence into existing flag byte
9259 PHA ; Save merged flag byte
925A STA (net_tx_ptr),y ; Write flag+sequence to TXCB byte 0
925C JSR tx_poll_ff ; Transmit with full retry
925F LDA #&ff ; End address &FFFF = unlimited data length
9261 LDY #8 ; Y=8: end address low offset in TXCB
9263 STA (net_tx_ptr),y ; Store &FF to end addr low
9265 INY ; Y=&09
9266 STA (net_tx_ptr),y ; Store &FF to end addr high (Y=9)
9268 PLA ; Recover merged flag byte
9269 TAX ; Save in X for sequence compare
926A LDY #&d1 ; Y=&D1: printer port number
926C PLA ; Recover saved handle bitmask
926D PHA ; Re-save for later consumption
926E BEQ bspsx ; A=0: port &D1 (print); A!=0: port &90 (FS)
9270 LDY #&90 ; Y=&90: FS data port
9272 .bspsx←1← 926E BEQ
TYA ; A = selected port number
9273 LDY #1 ; Y=1: port byte offset in TXCB
9275 STA (net_tx_ptr),y ; Write port to TXCB byte 1
9277 TXA ; A = saved flag byte (expected sequence)
9278 DEY ; Y=&00
9279 PHA ; Push expected sequence for retry loop
927A .bsxl0←1← 9286 BCS
LDA #&7f ; Flag byte &7F = waiting for reply
927C STA (net_tx_ptr),y ; Write to TXCB flag byte (Y=0)
927E JSR send_to_fs_star ; Transmit and wait for reply (BRIANX)
9281 PLA ; Recover expected sequence
9282 PHA ; Keep on stack for next iteration
9283 EOR (net_tx_ptr),y ; Check if TX result matches expected sequence
9285 ROR ; Bit 0 to carry (sequence mismatch?)
9286 BCS bsxl0 ; C=1: mismatch, retry transmit
9288 PLA ; Clean up: discard expected sequence
9289 PLA ; Discard saved handle bitmask
928A TAX ; Transfer count to X
928B INX ; Test for retry exhaustion
928C BEQ return_bspsx ; X wrapped to 0: retries exhausted
928E EOR fs_sequence_nos ; Toggle sequence bit on success
9291 .return_bspsx←1← 928C BEQ
RTS ; Return

Save palette and VDU state (CVIEW)

Part of the VIEW facility (second iteration, started 27/7/82). Uses dynamically allocated buffer store. The WORKP1 pointer (&9E,&9F) serves double duty: non-zero indicates data ready AND provides the buffer address — an efficient use of scarce zero- page space. This code must be user-transparent as the NFS may not be the dominant filing system. Reads all 16 palette entries using OSWORD &0B (read palette) and stores the results. Then reads cursor position (OSBYTE &85), shadow RAM allocation (OSBYTE &C2), and screen start address (OSBYTE &C3) using the 3-entry table at &9305 (osbyte_vdu_table). On completion, restores the JSR buffer protection bits (LSTAT) from OLDJSR to re-enable JSR reception, which was disabled during the screen data capture to prevent interference.

9292 .lang_2_save_palette_vdu
LDA fs_load_addr_2 ; Save current table index
9294 PHA ; Push for later restore
9295 LDA #&e9 ; Point workspace to palette save area (&E9)
9297 STA nfs_workspace ; Set workspace low byte
9299 LDY #0 ; Y=0: first palette entry
929B STY fs_load_addr_2 ; Clear table index counter
929D LDA l0350 ; Save current screen MODE to workspace
92A0 STA (nfs_workspace),y ; Store MODE at workspace[0]
92A2 INC nfs_workspace ; Advance workspace pointer past MODE byte
92A4 LDA l0351 ; Read colour count (from &0351)
92A7 PHA ; Push for iteration count tracking
92A8 TYA ; A=0: logical colour number for OSWORD A=&00
92A9 .save_palette_entry←1← 92C8 BNE
STA (nfs_workspace),y ; Store logical colour at workspace[0]
92AB LDX nfs_workspace ; X = workspace ptr low (param block addr)
92AD LDY nfs_workspace_hi ; Y = workspace ptr high
92AF LDA #osword_read_palette ; OSWORD &0B: read palette for logical colour
92B1 JSR osword ; Read palette
92B4 PLA ; Recover colour count
92B5 LDY #0 ; Y=0: access workspace[0]
92B7 STA (nfs_workspace),y ; Write colour count back to workspace[0]
92B9 INY ; Y=1: access workspace[1] (palette result) Y=&01
92BA LDA (nfs_workspace),y ; Read palette value returned by OSWORD
92BC PHA ; Push palette value for next iteration
92BD LDX nfs_workspace ; X = current workspace ptr low
92BF INC nfs_workspace ; Advance workspace pointer
92C1 INC fs_load_addr_2 ; Increment table index
92C3 DEY ; Y=0 for next store Y=&00
92C4 LDA fs_load_addr_2 ; Load table index as logical colour
92C6 CPX #&f9 ; Loop until workspace wraps past &F9
92C8 BNE save_palette_entry ; Continue for all 16 palette entries
92CA PLA ; Discard last palette value from stack
92CB STY fs_load_addr_2 ; Reset table index to 0
92CD INC nfs_workspace ; Advance workspace past palette data
92CF JSR save_vdu_state ; Save cursor pos and OSBYTE state values
92D2 INC nfs_workspace ; Advance workspace past VDU state data
92D4 PLA ; Recover saved table index
92D5 STA fs_load_addr_2 ; Restore table index
92D7 .clear_jsr_protection←4← 8E66 JSR← 9103 JMP← 9135 JSR← 9152 JSR
LDA rx_ctrl_copy ; Restore LSTAT from saved OLDJSR value
92DA STA prot_status ; Write to protection status
92DD RTS ; Return

Save VDU workspace state

Stores the cursor position value from &0355 into NFS workspace, then reads cursor position (OSBYTE &85), shadow RAM (OSBYTE &C2), and screen start (OSBYTE &C3) via read_vdu_osbyte, storing each result into consecutive workspace bytes.

ROMExec
92DE .save_vdu_state←1← 92CF JSR
LDA l0355 ; Read cursor editing state
92E1 STA (nfs_workspace),y ; Store to workspace[Y]
92E3 TAX ; Preserve in X for OSBYTE
92E4 JSR read_vdu_osbyte ; OSBYTE &85: read cursor position
92E7 INC nfs_workspace ; Advance workspace pointer
92E9 TYA ; Y result from OSBYTE &85
92EA STA (nfs_workspace,x) ; Store Y pos to workspace (X=0)
92EC JSR read_vdu_osbyte_x0 ; Self-call trick: executes twice
92EF .read_vdu_osbyte_x0←1← 92EC JSR
LDX #0 ; X=0 for (zp,X) addressing
92F1 .read_vdu_osbyte←1← 92E4 JSR
LDY fs_load_addr_2 ; Index into OSBYTE number table
92F3 INC fs_load_addr_2 ; Next table entry next time
92F5 INC nfs_workspace ; Advance workspace pointer
92F7 LDA osbyte_vdu_table,y ; Read OSBYTE number from table
92FA LDY #&ff ; Y=&FF: read current value
92FC JSR osbyte ; Call OSBYTE
92FF TXA ; Result in X to A
9300 LDX #0 ; X=0 for indexed indirect store
9302 STA (nfs_workspace,x) ; Store result to workspace
9304 RTS ; Return after storing result
; 3-entry OSBYTE table for lang_2_save_palette_vdu (&9292)
9305 .osbyte_vdu_table←1← 92F7 LDA
EQUB &85 ; OSBYTE &85: read cursor position
9306 EQUB &C2 ; OSBYTE &C2: read shadow RAM allocation
9307 EQUB &C3 ; OSBYTE &C3: read screen start address
; Tube BRK handler (BRKV target) — reference: NFS11 NEWBR
; Sends error information to the Tube co-processor via R2 ; and R4:
; 1. Sends &FF to R4 (WRIFOR) to signal error
; 2. Reads R2 data (flush any pending byte)
; 3. Sends &00 via R2, then error number from (&FD),0
; 4. Loops sending error string bytes via R2 until zero terminator
; 5. Falls through to tube_reset_stack → tube_main_loop
; The main loop continuously polls R4 for WRCH requests ; (forwarded
; to OSWRITCH &FFCB) and R2 for command bytes (dispatched ; via the
; 14-entry table at &0500). The R2 command byte is stored ; at &55
; before dispatch via JMP (&0500).
9308 0016 .nmi_workspace_start←1← 8116 STA
.tube_brk_handler←1← 8116 STA
LDA #&ff ; A=&FF: signal error to co-processor via R4
930A 0018 JSR tube_send_r4 ; Send &FF error signal to Tube R4
930D 001B LDA tube_data_register_2 ; Flush any pending R2 byte
9310 001E LDA #0 ; A=0: send zero prefix to R2
9312 0020 .tube_send_zero_r2
JSR tube_send_r2 ; Send zero prefix byte via R2
9315 0023 TAY ; Y=0: start of error block at (&FD)
9316 0024 LDA (brk_ptr),y ; Load error number from (&FD),0
9318 0026 JSR tube_send_r2 ; Send error number via R2
931B 0029 .tube_brk_send_loop←1← 0030 BNE
INY ; Advance to next error string byte
931C 002A .tube_send_error_byte
LDA (brk_ptr),y ; Load next error string byte
931E 002C JSR tube_send_r2 ; Send error string byte via R2
9321 002F TAX ; Zero byte = end of error string
9322 0030 BNE tube_brk_send_loop ; Loop until zero terminator sent
9324 0032 .tube_reset_stack
LDX #&ff ; Reset stack pointer to top
9326 0034 TXS ; TXS: set stack pointer from X
9327 0035 CLI ; Enable interrupts for main loop
; Save registers and enter Tube polling loop
; Saves X and Y to zp_temp_11/zp_temp_10, then falls ; through
; to tube_main_loop which polls Tube R1 (WRCH) and R2 ; (command)
; registers in an infinite loop. Called from ; tube_init_reloc
; after ROM relocation and from tube_dispatch_table ; handlers
; that need to restart the main loop.
9328 0036 .tube_enter_main_loop←2← 04EC JMP← 053A JMP
STX zp_temp_11 ; Save X to temporary
932A 0038 STY zp_temp_10 ; Save Y to temporary
932C 003A .tube_main_loop←7← 0048 BPL← 05AE JMP← 05D5 JMP← 0623 JMP← 0638 JMP← 06A0 JMP← 06CD JMP
BIT tube_status_register_4_and_cpu_control ; BIT R4 status: check WRCH request
932F 003D BPL tube_poll_r2 ; R1 not ready: check R2 instead
9331 003F .tube_handle_wrch←1← 004D BMI
LDA tube_data_register_4 ; Read character from Tube R4 data
9334 0042 JSR nvwrch ; Write character
9337 0045 .tube_poll_r2←1← 003D BPL
BIT tube_status_register_2 ; BIT R2 status: check command byte
933A 0048 BPL tube_main_loop ; R2 not ready: loop back to R1 check
933C 004A BIT tube_status_register_4_and_cpu_control ; Re-check R4: WRCH has priority over R2
933F 004D BMI tube_handle_wrch ; R1 ready: handle WRCH first
9341 004F LDX tube_data_register_2 ; Read command byte from Tube R2 data
9344 0052 STX tube_dispatch_ptr_lo ; Self-modify JMP low byte for dispatch
9346 0054 .tube_dispatch_cmd
JMP (tube_dispatch_table) ; Dispatch to handler via indirect JMP
9349 0057 .tube_transfer_addr←2← 0478 STY← 0493 STA
EQUB &00
934A 0058 .tube_xfer_page←2← 047C STA← 0498 STA
EQUB &80
934B 0059 .tube_xfer_addr_2←1← 04A2 STY
EQUB &00
934C 005A .tube_xfer_addr_3←1← 04A0 STA
EQUB &00
; Tube host code page 4 — reference: NFS12 (BEGIN, ADRR, ; SENDW)
; Copied from ROM (tube_code_page4) during init. The first ; 28 bytes (&0400-&041B)
; overlap with the end of the ZP block (the same ROM bytes ; serve both
; the ZP copy at &005B-&0076 and this page at ; &0400-&041B). Contains:
; &0400: JMP &0473 (BEGIN — CLI parser / startup entry)
; &0403: JMP &06E2 (tube_escape_check)
; &0406: tube_addr_claim — Tube address claim protocol (ADRR)
; &0414: tube_post_init — called after ROM→RAM copy
; &0473: BEGIN — startup/CLI entry, break type check
; &04E7: tube_rdch_handler — RDCHV target
; &04EF: tube_restore_regs — restore X,Y, dispatch entry 6
; &04F7: tube_read_r2 — poll R2 status, read data byte to A
934D 0400 .tube_code_page4←1← 80FC STA
JMP tube_begin ; JMP to BEGIN startup entry
9350 0403 .tube_escape_entry
JMP tube_escape_check ; JMP to tube_escape_check (&06A7)
9353 0406 .tube_addr_claim←10← 04BC JSR← 04E4 JMP← 8B1E JSR← 8B30 JSR← 8B8D JSR← 8DAA JMP← 99F0 JSR← 9A3D JSR← 9F98 JSR← 9FA0 JSR
CMP #&80 ; A>=&80: address claim; A<&80: data transfer
9355 0408 BCC setup_data_transfer ; A<&80: data transfer setup (SENDW)
9357 040A CMP #&c0 ; A>=&C0: new address claim from another host
9359 040C BCS addr_claim_external ; C=1: external claim, check ownership
935B 040E ORA #&40 ; Map &80-&BF range to &C0-&FF for comparison
935D 0410 CMP tube_claimed_id ; Is this for our currently-claimed address?
935F 0412 BNE return_tube_init ; Not our address: return
9361 0414 .tube_post_init←1← 810E JSR
LDA #&80 ; &80 sentinel: clear address claim
9363 0416 STA tube_claim_flag ; Store to claim-in-progress flag
9365 0418 RTS ; Return from tube_post_init
9366 0419 .addr_claim_external←1← 040C BCS
ASL tube_claim_flag ; Another host claiming; check if we're owner
9368 041B BCS accept_new_claim ; C=1: we have an active claim
936A 041D CMP tube_claimed_id ; Compare with our claimed address
936C 041F BEQ return_tube_init ; Match: return (we already have it)
936E 0421 CLC ; Not ours: CLC = we don't own this address
936F 0422 RTS ; Return with C=0 (claim denied)
9370 0423 .accept_new_claim←1← 041B BCS
STA tube_claimed_id ; Accept new claim: update our address
9372 0425 .return_tube_init←2← 0412 BNE← 041F BEQ
RTS ; Return with address updated
9373 0426 .setup_data_transfer←1← 0408 BCC
STY tube_data_ptr_hi ; Save 16-bit transfer address from (X,Y)
9375 0428 STX tube_data_ptr ; Store address pointer low byte
9377 042A JSR tube_send_r4 ; Send transfer type byte to co-processor
937A 042D TAX ; X = transfer type for table lookup
937B 042E LDY #3 ; Y=3: send 4 bytes (address + claimed addr)
937D 0430 .send_xfer_addr_bytes←1← 0436 BPL
LDA (tube_data_ptr),y ; Load transfer address byte from (X,Y)
937F 0432 JSR tube_send_r4 ; Send address byte to co-processor via R4
9382 0435 DEY ; Previous byte (big-endian: 3,2,1,0)
9383 0436 BPL send_xfer_addr_bytes ; Loop for all 4 address bytes
9385 0438 LDY #8 ; Y=8: write to Tube control register
9387 043A STY tube_status_1_and_tube_control ; Configure Tube for data transfer
938A 043D LDY #&10 ; Y=&10: data transfer control value
938C 043F CPX #2 ; Check transfer type (X=2?)
938E 0441 BCC tube_ctrl_write_2 ; X<2: skip alternate control
9390 0443 LDY #&90 ; Y=&90: alternate control for X>=2
9392 0445 .tube_ctrl_write_2←1← 0441 BCC
STY tube_status_1_and_tube_control ; Write transfer control to Tube
9395 0448 JSR tube_send_r4 ; Send data byte via Tube R4
9398 044B LDY #&88 ; Y=&88: post-transfer control value
939A 044D TXA ; Transfer type to A for comparison
939B 044E BEQ flush_r3_nmi_check ; Type 0: go to NMI flush check
939D 0450 CMP #2 ; Check if type 2
939F 0452 BEQ flush_r3_nmi_check ; Type 2: go to NMI flush check
93A1 0454 STY tube_status_1_and_tube_control ; Write post-transfer control
93A4 0457 CMP #4 ; Check if type 4 (SENDW)
93A6 0459 BNE return_tube_xfer ; Not SENDW type: skip release path
93A8 045B PLA ; Discard return address (low byte)
93A9 045C PLA ; Discard return address (high byte)
93AA 045D .release_claim_restart←1← 04B8 BEQ
LDA #&80 ; A=&80: reset claim flag sentinel
93AC 045F STA tube_claim_flag ; Clear claim-in-progress flag
93AE 0461 JMP tube_reply_byte ; Restart Tube main loop
93B1 0464 .flush_r3_nmi_check←3← 044E BEQ← 0452 BEQ← 0467 BVC
BIT tube_status_register_4_and_cpu_control ; Poll R4 status: wait for transfer ready
93B4 0467 BVC flush_r3_nmi_check ; V=0: not ready, poll again
93B6 0469 BIT tube_data_register_3 ; Flush Tube R3 data register
93B9 046C BIT tube_data_register_3 ; Flush Tube R3 again
93BC 046F STY tube_status_1_and_tube_control ; Write final control value
93BF 0472 .return_tube_xfer←1← 0459 BNE
RTS ; Return from Tube data setup
93C0 0473 .tube_begin←1← 0400 JMP
CLI ; BEGIN: enable interrupts for Tube host code
93C1 0474 PHP ; Save processor status
93C2 0475 PHA ; Save A on stack
93C3 0476 LDY #0 ; Y=0: start at beginning of page
93C5 0478 STY tube_transfer_addr ; Store to zero page pointer low byte
; Initialise relocation address for ROM transfer
; Sets source page to &8000 and page counter to &80. ; Checks
; ROM type bit 5 for a relocation address in the ROM ; header;
; if present, extracts the 4-byte address from after the
; copyright string. Otherwise uses default &8000 start.
93C7 047A .tube_init_reloc
LDA #&80 ; Init: start sending from &8000
93C9 047C STA tube_xfer_page ; Store &80 as source page high byte
93CB 047E STA zp_ptr_hi ; Store &80 as page counter initial value
93CD 0480 LDA #&20 ; A=&20: bit 5 mask for ROM type check
93CF 0482 AND rom_type ; ROM type bit 5: reloc address in header?
93D2 0485 BEQ store_xfer_end_addr ; No reloc addr: use defaults
93D4 0487 LDX copyright_offset ; Skip past copyright string to find reloc addr
93D7 048A .scan_copyright_end←1← 048E BNE
INX ; Skip past null-terminated copyright string
93D8 048B LDA rom_header,x ; Load next byte from ROM header
93DB 048E BNE scan_copyright_end ; Loop until null terminator found
93DD 0490 LDA lang_entry_lo,x ; Read 4-byte reloc address from ROM header
93E0 0493 STA tube_transfer_addr ; Store reloc addr byte 1 as transfer addr
93E2 0495 LDA lang_entry_hi,x ; Load reloc addr byte 2
93E5 0498 STA tube_xfer_page ; Store as source page start
93E7 049A LDY service_entry,x ; Load reloc addr byte 3
93EA 049D LDA svc_entry_lo,x ; Load reloc addr byte 4 (highest)
93ED 04A0 .store_xfer_end_addr←1← 0485 BEQ
STA tube_xfer_addr_3 ; Store high byte of end address
93EF 04A2 STY tube_xfer_addr_2 ; Store byte 3 of end address
93F1 04A4 PLA ; Restore A from stack
93F2 04A5 PLP ; Restore processor status
93F3 04A6 BCS beginr ; Carry set: language entry (claim Tube)
93F5 04A8 TAX ; X = A (preserved from entry)
93F6 04A9 BNE begink ; Non-zero: check break type
93F8 04AB JMP tube_reply_ack ; A=0: acknowledge and return
93FB 04AE .begink←1← 04A9 BNE
LDX #0 ; X=0 for OSBYTE read
93FD 04B0 LDY #&ff ; Y=&FF for OSBYTE read
93FF 04B2 LDA #osbyte_read_write_last_break_type ; OSBYTE &FD: read last break type
9401 04B4 JSR osbyte ; Read type of last reset
9404 04B7 TXA ; X=value of type of last reset
9405 04B8 BEQ release_claim_restart ; Soft break (0): skip ROM transfer
9407 04BA .beginr←2← 04A6 BCS← 04BF BCC
LDA #&ff ; A=&FF: claim Tube for all operations
9409 04BC JSR tube_addr_claim ; Claim Tube address via R4
940C 04BF BCC beginr ; Not claimed: retry until claimed
940E 04C1 LDA #1 ; Transfer type 1 (parasite to host)
9410 04C3 JSR tube_setup_transfer ; Set up Tube transfer parameters
9413 04C6 LDY #0 ; Y=0: start at page boundary
9415 04C8 STY zp_ptr_lo ; Source ptr low = 0
9417 04CA LDX #&40 ; X=&40: 64 pages (16KB) to transfer
9419 04CC .send_rom_byte←2← 04D7 BNE← 04DC BNE
LDA (zp_ptr_lo),y ; Read byte from source address
941B 04CE STA tube_data_register_3 ; Send byte to Tube via R3
941E 04D1 .poll_r3_ready←1← 04D4 BVC
BIT tube_status_register_3 ; Check R3 status
9421 04D4 BVC poll_r3_ready ; Not ready: wait for Tube
9423 04D6 INY ; Next byte in page
9424 04D7 BNE send_rom_byte ; More bytes in page: continue
9426 04D9 INC zp_ptr_hi ; Next source page
9428 04DB DEX ; Decrement page counter
9429 04DC BNE send_rom_byte ; More pages: continue transfer
942B 04DE LDA #4 ; Transfer type 4 (host to parasite burst)
942D 04E0 .tube_setup_transfer←1← 04C3 JSR
LDY #0 ; Y=0: low byte of param block ptr
942F 04E2 LDX #&57 ; X=&57: param block at &0057
9431 04E4 JMP tube_addr_claim ; Claim Tube and start transfer
9434 04E7 .tube_rdch_handler
LDA #1 ; R2 command: OSRDCH request
9436 04E9 JSR tube_send_r2 ; Send OSRDCH request to host
9439 04EC JMP tube_enter_main_loop ; Jump to RDCH completion handler
943C 04EF .tube_restore_regs
LDY zp_temp_10 ; Restore saved Y register
943E 04F1 LDX zp_temp_11 ; Restore X from saved value
9440 04F3 JSR tube_read_r2 ; Read result byte from R2
9443 04F6 ASL ; Shift carry into C flag
9444 04F7 .tube_read_r2←22← 04F3 JSR← 04FA BPL← 0543 JSR← 0547 JSR← 0550 JSR← 0569 JSR← 0580 JSR← 058C JSR← 0592 JSR← 059B JSR← 05B5 JSR← 05DA JSR← 05EB JSR← 0604 JSR← 060C JSR← 0626 JSR← 062A JSR← 063B JSR← 063F JSR← 0643 JSR← 065D JSR← 06A5 JSR
BIT tube_status_register_2 ; Poll R2 status register
9447 04FA BPL tube_read_r2 ; Bit 7 clear: R2 not ready, wait
9449 04FC LDA tube_data_register_2 ; Read byte from R2 data register
944C 04FF RTS ; Return with pointers initialised
; Tube host code page 5 — reference: NFS13 (TASKS, ; BPUT-FILE)
; Copied from ROM (tube_dispatch_table) during init. ; Contains:
; &0500: tube_dispatch_table — 14-entry handler address table
; &051C: tube_wrch_handler — WRCHV target
; &051F: tube_send_and_poll — send byte via R2, poll for reply
; &0527: tube_poll_r4_wrch — service R4 WRCH while waiting for R2
; &053D: tube_release_return — restore regs and RTS
; &0543: tube_osbput — write byte to file
; &0550: tube_osbget — read byte from file
; &055B: tube_osrdch — read character
; &0569: tube_osfind — open file
; &0580: tube_osfind_close — close file (A=0)
; &058C: tube_osargs — file argument read/write
; &05B1: tube_read_string — read CR-terminated string into &0700
; &05C5: tube_oscli — execute * command
; &05CB: tube_reply_ack — send &7F acknowledge
; &05CD: tube_reply_byte — send byte and return to main loop
; &05D8: tube_osfile — whole file operation
944D 0500 .tube_dispatch_table←2← 0054 JMP← 8102 STA
EQUW tube_osrdch ; cmd 0: OSRDCH
944F 0502 EQUW tube_oscli ; cmd 1: OSCLI
9451 0504 EQUW tube_osbyte_short ; cmd 2: OSBYTE (2-param)
9453 0506 EQUW tube_osbyte_long ; cmd 3: OSBYTE (3-param)
9455 0508 EQUW tube_osword ; cmd 4: OSWORD
9457 050A EQUW tube_osword_rdln ; cmd 5: OSWORD 0 (read line)
9459 050C EQUW tube_restore_regs ; cmd 6: release/restore regs
945B 050E EQUW tube_release_return ; cmd 7: restore regs, RTS
945D 0510 EQUW tube_osargs ; cmd 8: OSARGS
945F 0512 EQUW tube_osbget ; cmd 9: OSBGET
9461 0514 EQUW tube_osbput ; cmd 10: OSBPUT
9463 0516 EQUW tube_osfind ; cmd 11: OSFIND
9465 0518 EQUW tube_osfile ; cmd 12: OSFILE
9467 051A EQUW tube_osgbpb ; cmd 13: OSGBPB
9469 051C .tube_wrch_handler
PHA ; Save character for WRCH echo
946A 051D LDA #0 ; A=0: send null prefix via R2
946C 051F .tube_send_and_poll
JSR tube_send_r2 ; Send prefix byte to co-processor
946F 0522 .poll_r2_reply←2← 052A BPL← 0532 JMP
BIT tube_status_register_2 ; Poll R2 for co-processor reply
9472 0525 BVS wrch_echo_reply ; R2 ready: go process reply
9474 0527 .tube_poll_r4_wrch
BIT tube_status_register_4_and_cpu_control ; Check R4 for pending WRCH request
9477 052A BPL poll_r2_reply ; No R4 data: back to polling R2
9479 052C LDA tube_data_register_4 ; Read WRCH character from R4
947C 052F JSR nvwrch ; Write character
947F 0532 .tube_resume_poll
JMP poll_r2_reply ; Resume R2 polling after servicing
9482 0535 .wrch_echo_reply←1← 0525 BVS
PLA ; Recover original character
9483 0536 STA tube_data_register_2 ; Echo character back via R2
9486 0539 PHA ; Push for dispatch loop re-entry
9487 053A JMP tube_enter_main_loop ; Enter main dispatch loop
948A 053D .tube_release_return
LDX zp_temp_11 ; Restore saved X
948C 053F LDY zp_temp_10 ; Restore saved Y from temporary
948E 0541 PLA ; Restore saved A
948F 0542 RTS ; Return to caller
9490 0543 .tube_osbput
JSR tube_read_r2 ; Read file handle from R2
9493 0546 TAY ; Y=channel handle from R2
9494 0547 JSR tube_read_r2 ; Read data byte from R2 for BPUT
9497 054A .tube_poll_r1_wrch
JSR osbput ; Write a single byte A to an open file Y
949A 054D JMP tube_reply_ack ; BPUT done: send acknowledge, return
949D 0550 .tube_osbget
JSR tube_read_r2 ; Read file handle from R2
94A0 0553 TAY ; Y=channel handle for OSBGET Y=file handle
94A1 0554 JSR osbget ; Read a single byte from an open file Y
94A4 0557 PHA ; Save byte read from file
94A5 0558 JMP send_reply_ok ; Send carry+byte reply (BGET result)
94A8 055B .tube_osrdch
JSR nvrdch ; Read a character from the current input stream
94AB 055E PHA ; A=character read
94AC 055F .send_reply_ok←1← 0558 JMP
ORA #&80 ; Set bit 7 (no-error flag)
94AE 0561 .tube_rdch_reply
ROR ; ROR A: encode carry (error flag) into bit 7
94AF 0562 JSR tube_send_r2 ; = JSR tube_send_r2 (overlaps &053D entry)
94B2 0565 PLA ; Restore read character/byte
94B3 0566 JMP tube_reply_byte ; Return to Tube main loop
94B6 0569 .tube_osfind
JSR tube_read_r2 ; Read open mode from R2
94B9 056C BEQ tube_osfind_close ; A=0: close file, else open with filename
94BB 056E PHA ; Save open mode while reading filename
94BC 056F JSR tube_read_string ; Read filename string from R2 into &0700
94BF 0572 PLA ; Recover open mode from stack
94C0 0573 JSR osfind ; Open or close file(s)
94C3 0576 PHA ; Save file handle result
94C4 0577 LDA #&ff ; A=&FF: success marker
94C6 0579 JSR tube_send_r2 ; Send success marker via R2
94C9 057C PLA ; Restore file handle
94CA 057D JMP tube_reply_byte ; Send file handle result to co-processor
94CD 0580 .tube_osfind_close←1← 056C BEQ
JSR tube_read_r2 ; OSFIND close: read handle from R2
94D0 0583 TAY ; Y=handle to close
94D1 0584 LDA #osfind_close ; A=0: close command for OSFIND
94D3 0586 JSR osfind ; Close one or all files
94D6 0589 JMP tube_reply_ack ; Close done: send acknowledge, return
94D9 058C .tube_osargs
JSR tube_read_r2 ; Read file handle from R2
94DC 058F TAY ; Y=file handle for OSARGS
94DD 0590 .tube_read_params
LDX #3 ; Read 4-byte arg + reason from R2 into ZP
94DF 0592 .read_osargs_params←1← 0598 BPL
JSR tube_read_r2 ; Read next param byte from R2
94E2 0595 STA zp_ptr_lo,x ; Params stored at &00-&03 (little-endian)
94E4 0597 DEX ; Decrement byte counter
94E5 0598 BPL read_osargs_params ; Loop until all 4 bytes read
94E7 059A INX ; X=0: reset index after loop
94E8 059B JSR tube_read_r2 ; Read OSARGS reason code from R2
94EB 059E JSR osargs ; Read or write a file's attributes
94EE 05A1 JSR tube_send_r2 ; Send result A back to co-processor
94F1 05A4 LDX #3 ; Return 4-byte result from ZP &00-&03
94F3 05A6 .send_osargs_result←1← 05AC BPL
LDA zp_ptr_lo,x ; Load result byte from zero page
94F5 05A8 JSR tube_send_r2 ; Send byte to co-processor via R2
94F8 05AB DEX ; Previous byte (count down)
94F9 05AC BPL send_osargs_result ; Loop for all 4 bytes
94FB 05AE JMP tube_main_loop ; Return to Tube main loop
94FE 05B1 .tube_read_string←3← 056F JSR← 05C5 JSR← 05E2 JSR
LDX #0 ; X=0: initialise string buffer index
9500 05B3 LDY #0 ; Y=0: string buffer offset 0
9502 05B5 .strnh←1← 05C0 BNE
JSR tube_read_r2 ; Read next string byte from R2
9505 05B8 STA l0700,y ; Store byte in string buffer at &0700+Y
9508 05BB INY ; Next buffer position
9509 05BC BEQ string_buf_done ; Y overflow: string too long, truncate
950B 05BE CMP #&0d ; Check for CR terminator
950D 05C0 BNE strnh ; Not CR: continue reading string
950F 05C2 .string_buf_done←1← 05BC BEQ
LDY #7 ; Y=7: set XY=&0700 for OSCLI/OSFIND
9511 05C4 RTS ; Return with XY pointing to &0700
9512 05C5 .tube_oscli
JSR tube_read_string ; Read command string into &0700
9515 05C8 JSR oscli ; Execute * command via OSCLI
9518 05CB .tube_reply_ack←3← 04AB JMP← 054D JMP← 0589 JMP
LDA #&7f ; &7F = success acknowledgement
951A 05CD .tube_reply_byte←5← 0461 JMP← 0566 JMP← 057D JMP← 05D0 BVC← 06B8 JMP
BIT tube_status_register_2 ; Poll R2 status until ready
951D 05D0 BVC tube_reply_byte ; Bit 6 clear: not ready, loop
951F 05D2 STA tube_data_register_2 ; Write byte to R2 data register
9522 05D5 .mj←1← 0600 BEQ
JMP tube_main_loop ; Return to Tube main loop
9525 05D8 .tube_osfile
LDX #&10 ; X=&10: read 16-byte control block
9527 05DA .argsw←1← 05E0 BNE
JSR tube_read_r2 ; Read next control block byte from R2
952A 05DD STA zp_ptr_hi,x ; Store at &01+X (descending)
952C 05DF DEX ; Decrement byte counter
952D 05E0 BNE argsw ; Loop for all 16 bytes
952F 05E2 JSR tube_read_string ; Read filename string from R2 into &0700
9532 05E5 STX zp_ptr_lo ; XY=&0700: filename pointer for OSFILE
9534 05E7 STY zp_ptr_hi ; Store Y=7 as pointer high byte
9536 05E9 LDY #0 ; Y=0 for OSFILE control block offset
9538 05EB JSR tube_read_r2 ; Read OSFILE reason code from R2
953B 05EE JSR osfile ; Execute OSFILE operation
953E 05F1 ORA #&80 ; Set bit 7: mark result as present
9540 05F3 JSR tube_send_r2 ; Send result A (object type) to co-processor
9543 05F6 LDX #&10 ; Return 16-byte control block to co-processor
9545 05F8 .send_osfile_ctrl_blk←1← 05FE BNE
LDA zp_ptr_hi,x ; Load control block byte
9547 05FA JSR tube_send_r2 ; Send byte to co-processor via R2
954A 05FD DEX ; Decrement byte counter
954B 05FE BNE send_osfile_ctrl_blk ; Loop for all 16 bytes
; Tube host code page 6 — reference: NFS13 (GBPB-ESCA)
; Copied from ROM (tube_code_page6) during init. ; &0600-&0601 is the tail
; of tube_osfile (BEQ to tube_reply_byte when done). ; Contains:
; &0602: tube_osgbpb — multi-byte file I/O
; &0626: tube_osbyte_short — 2-param OSBYTE (returns X)
; &063B: tube_osbyte_long — 3-param OSBYTE (returns carry+Y+X)
; &065D: tube_osword — variable-length OSWORD (buffer at &0130)
; &06A3: tube_osword_rdln — OSWORD 0 (read line, 5-byte params)
; &06BB: tube_rdln_send_line — send input line from &0700
; &06D0: tube_send_r2 — poll R2 status, write A to R2 data
; &06D9: tube_send_r4 — poll R4 status, write A to R4 data
; &06E2: tube_escape_check — check &FF, forward escape to R1
; &06E8: tube_event_handler — EVNTV: forward event (A,X,Y) via R1
; &06F7: tube_send_r1 — poll R1 status, write A to R1 data
954D 0600 .tube_code_page6←1← 8108 STA
BEQ mj ; OSGBPB done: return to main loop
954F 0602 .tube_osgbpb
LDX #&0c ; X=&0C: read 13-byte param block
9551 0604 .read_gbpb_params←1← 060A BPL
JSR tube_read_r2 ; Read param byte from Tube R2
9554 0607 STA zp_ptr_lo,x ; Store in zero page param block
9556 0609 DEX ; Next byte (descending)
9557 060A BPL read_gbpb_params ; Loop until all 13 bytes read
9559 060C JSR tube_read_r2 ; Read A (OSGBPB function code)
955C 060F INX ; X=0 after loop
955D 0610 LDY #0 ; Y=0 for OSGBPB call
955F 0612 JSR osgbpb ; Read or write multiple bytes to an open file
9562 0615 ROR ; Encode carry into result bit 7
9563 0616 JSR tube_send_r2 ; Send carry+result byte via R2
9566 0619 LDX #&0c ; X=12: send 13 updated param bytes
9568 061B .send_gbpb_params←1← 0621 BPL
LDA zp_ptr_lo,x ; Load updated param byte
956A 061D JSR tube_send_r2 ; Send param byte via R2
956D 0620 DEX ; Next byte (descending)
956E 0621 BPL send_gbpb_params ; Loop until all 13 bytes sent
9570 0623 JMP tube_main_loop ; Return to main event loop
9573 0626 .tube_osbyte_short
JSR tube_read_r2 ; Read X parameter from R2
9576 0629 TAX ; Save in X
9577 062A JSR tube_read_r2 ; Read A (OSBYTE function code)
957A 062D JSR osbyte ; Execute OSBYTE A,X
957D 0630 .tube_osbyte_send_x←2← 0633 BVC← 065B BVS
BIT tube_status_register_2 ; Poll R2 status (bit 6 = ready)
9580 0633 BVC tube_osbyte_send_x ; Not ready: keep polling
9582 0635 STX tube_data_register_2 ; Send X result for 2-param OSBYTE
9585 0638 .bytex←1← 064B BEQ
JMP tube_main_loop ; Return to main event loop
9588 063B .tube_osbyte_long
JSR tube_read_r2 ; Read X parameter from R2
958B 063E TAX ; Save in X
958C 063F JSR tube_read_r2 ; Read Y parameter from co-processor
958F 0642 TAY ; Save in Y
9590 0643 JSR tube_read_r2 ; Read A (OSBYTE function code)
9593 0646 JSR osbyte ; Execute OSBYTE A,X,Y
9596 0649 EOR #&9d ; Test for OSBYTE &9D (fast Tube BPUT)
9598 064B BEQ bytex ; OSBYTE &9D (fast Tube BPUT): no result needed
959A 064D LDA #&40 ; A=&40: high bit will hold carry
959C 064F ROR ; Encode carry (error flag) into bit 7
959D 0650 JSR tube_send_r2 ; Send carry+status byte via R2
95A0 0653 .tube_osbyte_send_y←1← 0656 BVC
BIT tube_status_register_2 ; Poll R2 status for ready
95A3 0656 BVC tube_osbyte_send_y ; Not ready: keep polling
95A5 0658 STY tube_data_register_2 ; Send Y result, then fall through to send X
95A8 065B BVS tube_osbyte_send_x ; ALWAYS branch
95AA 065D .tube_osword
JSR tube_read_r2 ; Read OSWORD number from R2
95AD 0660 TAY ; Save OSWORD number in Y
95AE 0661 .tube_osword_read←1← 0664 BPL
BIT tube_status_register_2 ; Poll R2 status for data ready
95B1 0664 BPL tube_osword_read ; Not ready: keep polling
95B3 0666 LDX tube_data_register_2 ; Read param block length from R2
95B6 0669 DEX ; DEX: length 0 means no params to read
95B7 066A BMI skip_param_read ; No params (length=0): skip read loop
95B9 066C .tube_osword_read_lp←2← 066F BPL← 0678 BPL
BIT tube_status_register_2 ; Poll R2 status for data ready
95BC 066F BPL tube_osword_read_lp ; Not ready: keep polling
95BE 0671 LDA tube_data_register_2 ; Read param byte from R2
95C1 0674 STA l0130,x ; Store param bytes into block at &0130
95C4 0677 DEX ; Next param byte (descending)
95C5 0678 BPL tube_osword_read_lp ; Loop until all params read
95C7 067A TYA ; Restore OSWORD number from Y
95C8 067B .skip_param_read←1← 066A BMI
LDX #<(l0130) ; XY=&0130: param block address for OSWORD
95CA 067D LDY #>(l0130) ; Y=&01: param block at &0130
95CC 067F JSR osword ; Execute OSWORD with XY=&0130
95CF 0682 LDA #&ff ; A=&FF: result marker for co-processor
95D1 0684 JSR tube_send_r2 ; Send result marker via R2
95D4 0687 .poll_r2_osword_result←1← 068A BPL
BIT tube_status_register_2 ; Poll R2 status for ready
95D7 068A BPL poll_r2_osword_result ; Not ready: keep polling
95D9 068C LDX tube_data_register_2 ; Read result block length from R2
95DC 068F DEX ; Decrement result byte counter
95DD 0690 BMI tube_return_main ; No results to send: return to main loop
95DF 0692 .tube_osword_write←1← 069E BPL
LDY l0130,x ; Send result block bytes from &0128 via R2
95E2 0695 .tube_osword_write_lp←1← 0698 BVC
BIT tube_status_register_2 ; Poll R2 status for ready
95E5 0698 BVC tube_osword_write_lp ; Not ready: keep polling
95E7 069A STY tube_data_register_2 ; Send result byte via R2
95EA 069D DEX ; Next result byte (descending)
95EB 069E BPL tube_osword_write ; Loop until all results sent
95ED 06A0 .tube_return_main←1← 0690 BMI
JMP tube_main_loop ; Return to main event loop
95F0 06A3 .tube_osword_rdln
LDX #4 ; X=4: read 5-byte RDLN ctrl block
95F2 06A5 .read_rdln_ctrl_block←1← 06AB BPL
JSR tube_read_r2 ; Read control block byte from R2
95F5 06A8 STA zp_ptr_lo,x ; Store in zero page params
95F7 06AA DEX ; Next byte (descending)
95F8 06AB BPL read_rdln_ctrl_block ; Loop until all 5 bytes read
95FA 06AD INX ; X=0 after loop, A=0 for OSWORD 0 (read line)
95FB 06AE LDY #0 ; Y=0 for OSWORD 0
95FD 06B0 TXA ; A=0: OSWORD 0 (read line)
95FE 06B1 JSR osword ; Read input line from keyboard
9601 06B4 BCC tube_rdln_send_line ; C=0: line read OK; C=1: escape pressed
9603 06B6 LDA #&ff ; &FF = escape/error signal to co-processor
9605 06B8 JMP tube_reply_byte ; Escape: send &FF error to co-processor
9608 06BB .tube_rdln_send_line←1← 06B4 BCC
LDX #0 ; X=0: start of input buffer at &0700
960A 06BD LDA #&7f ; &7F = line read successfully
960C 06BF JSR tube_send_r2 ; Send &7F (success) to co-processor
960F 06C2 .tube_rdln_send_loop←1← 06CB BNE
LDA l0700,x ; Load char from input buffer
9612 06C5 .tube_rdln_send_byte
JSR tube_send_r2 ; Send char to co-processor
9615 06C8 INX ; Next character
9616 06C9 CMP #&0d ; Check for CR terminator
9618 06CB BNE tube_rdln_send_loop ; Loop until CR terminator sent
961A 06CD JMP tube_main_loop ; Return to main event loop
961D 06D0 .tube_send_r2←18← 0020 JSR← 0026 JSR← 002C JSR← 04E9 JSR← 051F JSR← 0562 JSR← 0579 JSR← 05A1 JSR← 05A8 JSR← 05F3 JSR← 05FA JSR← 0616 JSR← 061D JSR← 0650 JSR← 0684 JSR← 06BF JSR← 06C5 JSR← 06D3 BVC
BIT tube_status_register_2 ; Poll R2 status (bit 6 = ready)
9620 06D3 BVC tube_send_r2 ; Not ready: keep polling
9622 06D5 STA tube_data_register_2 ; Write A to Tube R2 data register
9625 06D8 RTS ; Return to caller
9626 06D9 .tube_send_r4←5← 0018 JSR← 042A JSR← 0432 JSR← 0448 JSR← 06DC BVC
BIT tube_status_register_4_and_cpu_control ; Poll R4 status (bit 6 = ready)
9629 06DC BVC tube_send_r4 ; Not ready: keep polling
962B 06DE STA tube_data_register_4 ; Write A to Tube R4 data register
962E 06E1 RTS ; Return to caller
962F 06E2 .tube_escape_check←1← 0403 JMP
LDA escape_flag ; Check OS escape flag at &FF
9631 06E4 SEC ; SEC+ROR: put bit 7 of &FF into carry+bit 7
9632 06E5 ROR ; ROR: shift escape bit 7 to carry
9633 06E6 BMI tube_send_r1 ; Escape set: forward to co-processor via R1
9635 06E8 .tube_event_handler
PHA ; EVNTV: forward event A, Y, X to co-processor
9636 06E9 LDA #0 ; Send &00 prefix (event notification)
9638 06EB JSR tube_send_r1 ; Send zero prefix via R1
963B 06EE TYA ; Y value for event
963C 06EF JSR tube_send_r1 ; Send Y via R1
963F 06F2 TXA ; X value for event
9640 06F3 JSR tube_send_r1 ; Send X via R1
9643 06F6 PLA ; Restore A (event type)
9644 06F7 .tube_send_r1←5← 06E6 BMI← 06EB JSR← 06EF JSR← 06F3 JSR← 06FA BVC
BIT tube_status_1_and_tube_control ; Poll R1 status (bit 6 = ready)
9647 06FA BVC tube_send_r1 ; Not ready: keep polling
9649 06FC STA tube_data_register_1 ; Write A to Tube R1 data register
964C 06FF RTS ; Return to caller
964D EQUB &FF, &42, &FF, &00, &FF, &77, &FF, &FF, &FF, &DF, &FF, &00, &FF, &00, &FF, &00, &FF, &04, &FF
9660 .trampoline_tx_setup←2← 8668 JSR← 8E4A JSR
JMP tx_begin ; Trampoline: forward to tx_begin
9663 .trampoline_adlc_init←1← 82CA JSR
JMP adlc_init ; Trampoline: forward to adlc_init
9666 .svc_12_nmi_release
JMP save_econet_state ; Trampoline: forward to NMI release
9669 .svc_11_nmi_claim
JMP restore_econet_state ; Trampoline: forward to NMI claim
966C .svc_5_unknown_irq
JMP check_sr_irq ; Trampoline: forward to IRQ handler

ADLC initialisation

Reads station ID (INTOFF side effect), performs full ADLC reset, checks for Tube presence (OSBYTE &EA), then falls through to adlc_init_workspace.

966F .adlc_init←1← 9663 JMP
BIT station_id_disable_net_nmis ; INTOFF: read station ID, disable NMIs
9672 JSR adlc_full_reset ; Full ADLC hardware reset
9675 LDA #osbyte_read_tube_presence ; OSBYTE &EA: check Tube co-processor
9677 LDX #0 ; X=0 for OSBYTE
9679 LDY #&ff ; Y=&FF for OSBYTE
967B JSR osbyte ; Read Tube present flag
967E STX tx_in_progress ; X=value of Tube present flag
fall through ↓

Initialise NMI workspace

Copies NMI shim from ROM to &0D00, stores current ROM bank number into shim self-modifying code, sets TX status to &80 (idle/complete), saves station ID from &FE18 into TX scout buffer, re-enables NMIs by reading &FE20.

9681 .adlc_init_workspace←1← 96CA JMP
JSR install_nmi_shim ; Copy NMI shim from ROM to &0D00
9684 LDA romsel_copy ; Load current ROM bank number
9686 STA nmi_shim_07 ; Patch ROM bank into NMI shim
9689 LDA #&80 ; A=&80: TX idle/complete status
968B STA tx_ctrl_status ; Mark Econet as initialised
968E LDA station_id_disable_net_nmis ; Read station ID (&FE18 = INTOFF side effect)
9691 STA tx_src_stn ; Store our station ID in TX scout
9694 LDA #0 ; A=0: clear source network
9696 STA tx_src_net ; Clear TX source network byte
9699 BIT video_ula_control ; INTON: re-enable NMIs (&FE20 read side effect)
969C RTS ; Return to caller

Save Econet state to RX control block

Stores rx_status_flags, protection_mask, and tx_in_progress to (net_rx_ptr) at offsets 8-10. INTOFF side effect on entry.

969D .save_econet_state←1← 9666 JMP
BIT station_id_disable_net_nmis ; INTOFF: disable NMIs for state save
96A0 LDY #8 ; Y=8: RXCB offset for rx_status_flags
96A2 LDA rx_status_flags ; Load rx_status_flags
96A5 STA (net_rx_ptr),y ; Store to RXCB offset 8
96A7 INY ; Y=&09
96A8 LDA prot_status ; Load prot_status
96AB STA (net_rx_ptr),y ; Store to RXCB offset 9
96AD INY ; Y=&0a
96AE LDA tx_in_progress ; Load TX in-progress flag
96B1 STA (net_rx_ptr),y ; Store tx_in_progress to offset &0A
96B3 RTS ; Return to caller

Restore Econet state from RX control block

Loads rx_status_flags, protection_mask, and tx_in_progress from (net_rx_ptr) at offsets 8-10, then reinitialises via adlc_init_workspace.

96B4 .restore_econet_state←1← 9669 JMP
BIT station_id_disable_net_nmis ; INTOFF: disable NMIs for state restore
96B7 LDY #8 ; Y=8: workspace offset for flags
96B9 LDA (net_rx_ptr),y ; Load saved rx_status_flags
96BB STA rx_status_flags ; Restore rx_status_flags
96BE INY ; Y=&09
96BF LDA (net_rx_ptr),y ; Load saved protection mask
96C1 STA prot_status ; Restore prot_status
96C4 INY ; Y=&0a
96C5 LDA (net_rx_ptr),y ; Load saved tx_in_progress
96C7 STA tx_in_progress ; Restore TX state
96CA JMP adlc_init_workspace ; Reinitialise NMI workspace

Copy NMI shim from ROM (&9FCA) to RAM (&0D00)

Copies 32 bytes. Interrupts are enabled during the copy.

96CD .install_nmi_shim←1← 9681 JSR
PHP ; Save interrupt state on stack
96CE CLI ; Enable interrupts during copy
96CF LDY #&20 ; Y=&20: copy 32 bytes
96D1 .copy_nmi_shim_loop←1← 96D8 BNE
LDA nmi_shim_rom_src,y ; Load NMI shim byte from ROM
96D4 STA l0cff,y ; Store to NMI area at &0D00+Y
96D7 DEY ; Decrement byte counter
96D8 BNE copy_nmi_shim_loop ; Loop until all bytes copied
96DA PLP ; Restore interrupt state
96DB RTS ; Return from shim installation

ADLC full reset

Aborts all activity and returns to idle RX listen mode.

96DC .adlc_full_reset←3← 9672 JSR← 973E JSR← 9894 JSR
LDA #&c1 ; CR1=&C1: TX_RESET | RX_RESET | AC (both sections in reset, address control set)
96DE STA econet_control1_or_status1 ; Write CR1: full reset
96E1 LDA #&1e ; CR4=&1E (via AC=1): 8-bit RX word length, abort extend enabled, NRZ encoding
96E3 STA econet_data_terminate_frame ; Write CR4 via ADLC reg 3 (AC=1)
96E6 LDA #0 ; CR3=&00 (via AC=1): no loop-back, no AEX, NRZ, no DTR
96E8 STA econet_control23_or_status2 ; Write CR3=0: clear loop-back/AEX/DTR
fall through ↓

Enter RX listen mode

TX held in reset, RX active with interrupts. Clears all status.

96EB .adlc_rx_listen←1← 9A40 JSR
LDA #&82 ; CR1=&82: TX_RESET | RIE (TX in reset, RX interrupts enabled)
96ED STA econet_control1_or_status1 ; Write CR1: RIE | TX_RESET
96F0 LDA #&67 ; CR2=&67: CLR_TX_ST | CLR_RX_ST | FC_TDRA | 2_1_BYTE | PSE
96F2 STA econet_control23_or_status2 ; Write CR2: listen mode config
96F5 RTS ; Return

NMI RX scout handler (initial byte)

Default NMI handler for incoming scout frames. Checks if the frame is addressed to us or is a broadcast. Installed as the NMI target during idle RX listen mode. Tests SR2 bit0 (AP = Address Present) to detect incoming data. Reads the first byte (destination station) from the RX FIFO and compares against our station ID. Reading &FE18 also disables NMIs (INTOFF side effect).

96F6 .nmi_rx_scout←1← 9FD6 JMP
LDA #1 ; A=&01: mask for SR2 bit0 (AP = Address Present)
96F8 BIT econet_control23_or_status2 ; BIT SR2: Z = A AND SR2 -- tests if AP is set
96FB BEQ scout_error ; AP not set, no incoming data -- check for errors
96FD LDA econet_data_continue_frame ; Read first RX byte (destination station address)
9700 CMP station_id_disable_net_nmis ; Compare to our station ID (&FE18 read = INTOFF, disables NMIs)
9703 BEQ accept_frame ; Match -- accept frame
9705 CMP #&ff ; Check for broadcast address (&FF)
9707 BNE scout_reject ; Neither our address nor broadcast -- reject frame
9709 LDA #&40 ; Flag &40 = broadcast frame
970B STA tx_flags ; Store broadcast flag in TX flags
970E .accept_frame←1← 9703 BEQ
LDA #&15 ; Install next NMI handler at &9715 (RX scout second byte)
9710 LDY #&97 ; High byte of scout net handler
9712 JMP set_nmi_vector ; Install next handler and RTI

RX scout second byte handler

Reads the second byte of an incoming scout (destination network). Checks for network match: 0 = local network (accept), &FF = broadcast (accept and flag), anything else = reject. Installs the scout data reading loop handler at &9747.

9715 .nmi_rx_scout_net
BIT econet_control23_or_status2 ; BIT SR2: test for RDA (bit7 = data available)
9718 BPL scout_error ; No RDA -- check errors
971A LDA econet_data_continue_frame ; Read destination network byte
971D BEQ accept_local_net ; Network = 0 -- local network, accept
971F EOR #&ff ; EOR &FF: test if network = &FF (broadcast)
9721 BEQ accept_scout_net ; Broadcast network -- accept
9723 .scout_reject←1← 9707 BNE
LDA #&a2 ; Reject: wrong network. CR1=&A2: RIE|RX_DISCONTINUE
9725 STA econet_control1_or_status1 ; Write CR1 to discontinue RX
9728 JMP discard_after_reset ; Return to idle scout listening
972B .accept_local_net←1← 971D BEQ
STA tx_flags ; Network = 0 (local): clear tx_flags
972E .accept_scout_net←1← 9721 BEQ
STA port_buf_len ; Store Y offset for scout data buffer
9730 LDA #&47 ; Install scout data reading loop at &9747
9732 LDY #&97 ; High byte of scout data handler
9734 JMP set_nmi_vector ; Install scout data loop and RTI

Scout error/discard handler

Reached when the scout data loop sees no RDA (BPL at &974C) or when scout completion finds unexpected SR2 state. If SR2 & &81 is non-zero (AP or RDA still active), performs full ADLC reset and discards. If zero (clean end), discards via &9A40. This path is a common landing for any unexpected ADLC state during scout reception.

9737 .scout_error←5← 96FB BEQ← 9718 BPL← 974C BPL← 9780 BEQ← 9782 BPL
LDA econet_control23_or_status2 ; Read SR2
973A AND #&81 ; Test AP (b0) | RDA (b7)
973C BEQ scout_discard ; Neither set -- clean end, discard via &9744
973E JSR adlc_full_reset ; Unexpected data/status: full ADLC reset
9741 JMP discard_after_reset ; Discard and return to idle
9744 .scout_discard←1← 973C BEQ
JMP discard_listen ; Gentle discard: RX_DISCONTINUE

Scout data reading loop

Reads the body of a scout frame, two bytes per iteration. Stores bytes at &0D3D+Y (scout buffer: src_stn, src_net, ctrl, port, ...). Between each pair it checks SR2: - At entry (&9749): LDA SR2, BPL tests RDA (bit7) - No RDA (BPL) -> error (&9737) - RDA set (BMI) -> read byte - After first byte (&9755): LDA SR2 - RDA set (BMI) -> read second byte - SR2 non-zero (BNE) -> scout completion (&9771) This is the FV detection point: when FV is set (by inline refill of the last byte during the preceding RX FIFO read), SR2 is non-zero and the branch is taken. - SR2 = 0 -> RTI, wait for next NMI - After second byte (&9769): LDA SR2 - SR2 non-zero (BNE) -> loop back to &974C - SR2 = 0 -> RTI, wait for next NMI The loop ends at Y=&0C (12 bytes max in scout buffer).

9747 .scout_data_loop
LDY port_buf_len ; Y = buffer offset
9749 LDA econet_control23_or_status2 ; Read SR2
974C .scout_loop_rda←1← 976C BNE
BPL scout_error ; No RDA -- error handler &9737
974E LDA econet_data_continue_frame ; Read data byte from RX FIFO
9751 STA rx_src_stn,y ; Store at &0D3D+Y (scout buffer)
9754 INY ; Advance buffer index
9755 LDA econet_control23_or_status2 ; Read SR2 again (FV detection point)
9758 BMI scout_loop_second ; RDA set -- more data, read second byte
975A BNE scout_complete ; SR2 non-zero (FV or other) -- scout completion
975C .scout_loop_second←1← 9758 BMI
LDA econet_data_continue_frame ; Read second byte of pair
975F STA rx_src_stn,y ; Store at &0D3D+Y
9762 INY ; Advance and check buffer limit
9763 CPY #&0c ; Copied all 12 scout bytes?
9765 BEQ scout_complete ; Buffer full (Y=12) -- force completion
9767 STY port_buf_len ; Save Y for next iteration
9769 LDA econet_control23_or_status2 ; Read SR2 for next pair
976C BNE scout_loop_rda ; SR2 non-zero -- loop back for more bytes
976E JMP nmi_rti ; SR2 = 0 -- RTI, wait for next NMI

Scout completion handler

Reached from the scout data loop when SR2 is non-zero (FV detected). Disables PSE to allow individual SR2 bit testing: CR1=&00 (clear all enables) CR2=&84 (RDA_SUPPRESS_FV | FC_TDRA) -- no PSE, no CLR bits Then checks FV (bit1) and RDA (bit7): - No FV (BEQ) -> error &9737 (not a valid frame end) - FV set, no RDA (BPL) -> error &9737 (missing last byte) - FV set, RDA set -> read last byte, process scout After reading the last byte, the complete scout buffer (&0D3D-&0D48) contains: src_stn, src_net, ctrl, port [, extra_data...]. The port byte at &0D40 determines further processing: - Port = 0 -> immediate operation (&9A59) - Port non-zero -> check if it matches an open receive block

9771 .scout_complete←2← 975A BNE← 9765 BEQ
LDA #0 ; CR1=&00: disable all interrupts
9773 STA econet_control1_or_status1 ; Write CR1
9776 LDA #&84 ; CR2=&84: disable PSE, enable RDA_SUPPRESS_FV
9778 STA econet_control23_or_status2 ; Write CR2
977B LDA #2 ; A=&02: FV mask for SR2 bit1
977D BIT econet_control23_or_status2 ; BIT SR2: test FV (Z) and RDA (N)
9780 BEQ scout_error ; No FV -- not a valid frame end, error
9782 BPL scout_error ; FV set but no RDA -- missing last byte, error
9784 LDA econet_data_continue_frame ; Read last byte from RX FIFO
9787 STA rx_src_stn,y ; Store last byte at &0D3D+Y
978A LDA #&44 ; CR1=&44: RX_RESET | TIE (switch to TX for ACK)
978C STA econet_control1_or_status1 ; Write CR1: switch to TX mode
978F LDA rx_port ; Check port byte: 0 = immediate op, non-zero = data transfer
9792 BNE scout_match_port ; Port non-zero -- look for matching receive block
9794 JMP immediate_op ; Port = 0 -- immediate operation handler
9797 .scout_no_match←3← 97E2 BNE← 97E7 BVC← 9819 JMP
JMP rx_error ; Port = 0 -- immediate operation handler
979A .scout_match_port←1← 9792 BNE
BIT tx_flags ; Check if broadcast (bit6 of tx_flags)
979D BVC scan_port_list ; Not broadcast -- skip CR2 setup
979F LDA #7 ; CR2=&07: broadcast prep
97A1 STA econet_control23_or_status2 ; Write CR2: broadcast frame prep
97A4 .scan_port_list←1← 979D BVC
BIT rx_status_flags ; Check if RX port list active (bit7)
97A7 BPL scout_network_match ; No active ports -- try NFS workspace
97A9 LDA #&c0 ; Start scanning port list at page &C0
97AB .scan_nfs_port_list
STA port_ws_offset ; Store page to workspace pointer low
97AD LDA #0 ; A=0: no NFS workspace offset yet
97AF STA rx_buf_offset ; Clear NFS workspace search flag
97B1 .check_port_slot←1← 97DE BCC
LDY #0 ; Y=0: read control byte from start of slot
97B3 .scout_ctrl_check←1← 97F1 BEQ
LDA (port_ws_offset),y ; Read port control byte from slot
97B5 BEQ scout_station_check ; Zero = end of port list, no match
97B7 CMP #&7f ; &7F = any-port wildcard
97B9 BNE next_port_slot ; Not wildcard -- check specific port match
97BB INY ; Y=1: advance to port byte in slot
97BC LDA (port_ws_offset),y ; Read port number from slot (offset 1)
97BE BEQ check_station_filter ; Zero port in slot = match any port
97C0 CMP rx_port ; Check if port matches this slot
97C3 BNE next_port_slot ; Port mismatch -- try next slot
97C5 .check_station_filter←1← 97BE BEQ
INY ; Y=2: advance to station byte
97C6 LDA (port_ws_offset),y ; Read station filter from slot (offset 2)
97C8 BEQ scout_port_match ; Zero station = match any station, accept
97CA CMP rx_src_stn ; Check if source station matches
97CD BNE next_port_slot ; Station mismatch -- try next slot
97CF .scout_port_match←1← 97C8 BEQ
INY ; Y=3: advance to network byte
97D0 LDA (port_ws_offset),y ; Read network filter from slot (offset 3)
97D2 CMP rx_src_net ; Check if source network matches
97D5 BEQ scout_accept ; Network matches or zero = accept
97D7 .next_port_slot←3← 97B9 BNE← 97C3 BNE← 97CD BNE
LDA port_ws_offset ; Check if NFS workspace search pending
97D9 CLC ; CLC for 12-byte slot advance
97DA ADC #&0c ; Advance to next 12-byte port slot
97DC STA port_ws_offset ; Update workspace pointer to next slot
97DE BCC check_port_slot ; Always branches (page &C0 won't overflow)
97E0 .scout_station_check←1← 97B5 BEQ
LDA rx_buf_offset ; Check if NFS workspace already searched
97E2 BNE scout_no_match ; Already searched: no match found
97E4 .scout_network_match←1← 97A7 BPL
BIT rx_status_flags ; Try NFS workspace if paged list exhausted
97E7 BVC scout_no_match ; No NFS workspace RX (bit6 clear) -- discard
97E9 LDA nfs_workspace_hi ; Get NFS workspace page number
97EB STA rx_buf_offset ; Mark NFS workspace as search target
97ED LDY #0 ; Y=0: start at offset 0 in workspace
97EF STY port_ws_offset ; Reset slot pointer to start
97F1 BEQ scout_ctrl_check ; ALWAYS branch
97F3 .scout_accept←1← 97D5 BEQ
BIT tx_flags ; Check broadcast flag (bit 6)
97F6 BVC ack_scout_match ; Not broadcast: ACK and set up RX
97F8 JMP copy_scout_fields ; Broadcast: copy scout fields directly
97FB .ack_scout_match←2← 97F6 BVC← 9AC5 JMP
LDA #3 ; Match found: set scout_status = 3
97FD STA scout_status ; Record match for completion handler
9800 LDA nmi_tx_block ; Save current TX block ptr (low)
9802 PHA ; Push TX block low on stack
9803 LDA nmi_tx_block_hi ; Save current TX block ptr (high)
9805 PHA ; Push TX block high on stack
9806 LDA port_ws_offset ; Use port slot as temp RXCB ptr (lo)
9808 STA nmi_tx_block ; Set RXCB low for tx_calc_transfer
980A LDA rx_buf_offset ; Use workspace page as temp RXCB (hi)
980C STA nmi_tx_block_hi ; Set RXCB high for tx_calc_transfer
980E JSR tx_calc_transfer ; Calculate transfer parameters
9811 PLA ; Restore original TX block (high)
9812 STA nmi_tx_block_hi ; Restore TX block ptr (high)
9814 PLA ; Restore original TX block (low)
9815 STA nmi_tx_block ; Restore TX block ptr (low)
9817 BCS send_data_rx_ack ; Transfer OK: send data ACK
9819 JMP scout_no_match ; Broadcast: different completion path
981C .send_data_rx_ack←2← 9817 BCS← 9ABA JMP
LDA #&44 ; CR1=&44: RX_RESET | TIE
981E STA econet_control1_or_status1 ; Write CR1: TX mode for ACK
9821 LDA #&a7 ; CR2=&A7: RTS | CLR_TX_ST | FC_TDRA | PSE
9823 STA econet_control23_or_status2 ; Write CR2: enable TX with PSE
9826 LDA #&2d ; Install data_rx_setup at &982D
9828 LDY #&98 ; High byte of data_rx_setup handler
982A JMP ack_tx_write_dest ; Send ACK with data_rx_setup as next NMI
982D .data_rx_setup
LDA #&82 ; CR1=&82: TX_RESET | RIE (switch to RX for data frame)
982F STA econet_control1_or_status1 ; Write CR1: switch to RX for data frame
9832 LDA #&39 ; Install nmi_data_rx at &9839
9834 LDY #&98 ; High byte of nmi_data_rx handler
9836 JMP set_nmi_vector ; Install nmi_data_rx and return from NMI

Data frame RX handler (four-way handshake)

Receives the data frame after the scout ACK has been sent. First checks AP (Address Present) for the start of the data frame. Reads and validates the first two address bytes (dest_stn, dest_net) against our station address, then installs continuation handlers to read the remaining data payload into the open port buffer.

Handler chain: &9839 (AP+addr check) -> &984F (net=0 check) -> &9865 (skip ctrl+port) -> &989A (bulk data read) -> &98CE (completion)

9839 .nmi_data_rx
LDA #1 ; A=&01: mask for AP (Address Present)
983B BIT econet_control23_or_status2 ; BIT SR2: test AP bit
983E BEQ rx_error ; No AP: wrong frame or error
9840 LDA econet_data_continue_frame ; Read first byte (dest station)
9843 CMP station_id_disable_net_nmis ; Compare to our station ID (INTOFF)
9846 BNE rx_error ; Not for us: error path
9848 LDA #&4f ; Install net check handler at &984F
984A LDY #&98 ; High byte of nmi_data_rx_net handler
984C JMP set_nmi_vector ; Set NMI vector via RAM shim
984F .nmi_data_rx_net
BIT econet_control23_or_status2 ; Validate source network = 0
9852 BPL rx_error ; SR2 bit7 clear: no data ready -- error
9854 LDA econet_data_continue_frame ; Read dest network byte
9857 BNE rx_error ; Network != 0: wrong network -- error
9859 LDA #&65 ; Install skip handler at &9865
985B LDY #&98 ; High byte of &9865 handler
985D BIT econet_control1_or_status1 ; SR1 bit7: IRQ, data already waiting
9860 BMI nmi_data_rx_skip ; Data ready: skip directly, no RTI
9862 JMP set_nmi_vector ; Install handler and return via RTI
9865 .nmi_data_rx_skip←1← 9860 BMI
BIT econet_control23_or_status2 ; Skip control and port bytes (already known from scout)
9868 BPL rx_error ; SR2 bit7 clear: error
986A LDA econet_data_continue_frame ; Discard control byte
986D LDA econet_data_continue_frame ; Discard port byte
fall through ↓

Install data RX bulk or Tube handler

Selects either the normal bulk RX handler (&989A) or the Tube RX handler (&98F7) based on the Tube transfer flag in tx_flags, and installs the appropriate NMI handler.

9870 .install_data_rx_handler←1← 9F2F JMP
LDA #2 ; A=2: Tube transfer flag mask
9872 BIT tx_flags ; Check if Tube transfer active
9875 BNE install_tube_rx ; Tube active: use Tube RX path
9877 LDA #&9a ; Install bulk read at &989A
9879 LDY #&98 ; High byte of &989A handler
987B BIT econet_control1_or_status1 ; SR1 bit7: more data already waiting?
987E BMI nmi_data_rx_bulk ; Yes: enter bulk read directly
9880 JMP set_nmi_vector ; No: install handler and RTI
9883 .install_tube_rx←1← 9875 BNE
LDA #&f7 ; Tube: install Tube RX at &98F7
9885 LDY #&98 ; High byte of &98F7 handler
9887 JMP set_nmi_vector ; Install Tube handler and RTI

NMI error handler dispatch

Common error/abort entry used by 12 call sites. Checks tx_flags bit 7: if clear, does a full ADLC reset and returns to idle listen (RX error path); if set, jumps to tx_result_fail (TX not-listening path).

988A .rx_error←12← 9797 JMP← 983E BEQ← 9846 BNE← 9852 BPL← 9857 BNE← 9868 BPL← 98AD BEQ← 98DF BEQ← 98E5 BEQ← 9930 JMP← 99B8 JMP← 9A8C JMP
.nmi_error_dispatch←12← 9797 JMP← 983E BEQ← 9846 BNE← 9852 BPL← 9857 BNE← 9868 BPL← 98AD BEQ← 98DF BEQ← 98E5 BEQ← 9930 JMP← 99B8 JMP← 9A8C JMP
LDA tx_flags ; Check tx_flags for error path
988D BPL rx_error_reset ; Bit7 clear: RX error path
988F LDA #&41 ; A=&41: 'not listening' error
9891 JMP tx_store_result ; Bit7 set: TX result = not listening
9894 .rx_error_reset←1← 988D BPL
JSR adlc_full_reset ; Full ADLC reset on RX error
9897 JMP discard_reset_listen ; Discard and return to idle listen

Data frame bulk read loop

Reads data payload bytes from the RX FIFO and stores them into the open port buffer at (open_port_buf),Y. Reads bytes in pairs (like the scout data loop), checking SR2 between each pair. SR2 non-zero (FV or other) -> frame completion at &98CE. SR2 = 0 -> RTI, wait for next NMI to continue.

989A .nmi_data_rx_bulk←1← 987E BMI
LDY port_buf_len ; Y = buffer offset, resume from last position
989C LDA econet_control23_or_status2 ; Read SR2 for next pair
989F .data_rx_loop←1← 98C9 BNE
BPL data_rx_complete ; SR2 bit7 clear: frame complete (FV)
98A1 LDA econet_data_continue_frame ; Read first byte of pair from RX FIFO
98A4 STA (open_port_buf),y ; Store byte to buffer
98A6 INY ; Advance buffer offset
98A7 BNE read_sr2_between_pairs ; Y != 0: no page boundary crossing
98A9 INC open_port_buf_hi ; Crossed page: increment buffer high byte
98AB DEC port_buf_len_hi ; Decrement remaining page count
98AD BEQ rx_error ; No pages left: handle as complete
98AF .read_sr2_between_pairs←1← 98A7 BNE
LDA econet_control23_or_status2 ; Read SR2 between byte pairs
98B2 BMI read_second_rx_byte ; SR2 bit7 set: more data available
98B4 BNE data_rx_complete ; SR2 non-zero, bit7 clear: frame done
98B6 .read_second_rx_byte←1← 98B2 BMI
LDA econet_data_continue_frame ; Read second byte of pair from RX FIFO
98B9 STA (open_port_buf),y ; Store byte to buffer
98BB INY ; Advance buffer offset
98BC STY port_buf_len ; Save updated buffer position
98BE BNE check_sr2_loop_again ; Y != 0: no page boundary crossing
98C0 INC open_port_buf_hi ; Crossed page: increment buffer high byte
98C2 DEC port_buf_len_hi ; Decrement remaining page count
98C4 BEQ data_rx_complete ; No pages left: frame complete
98C6 .check_sr2_loop_again←1← 98BE BNE
LDA econet_control23_or_status2 ; Read SR2 for next iteration
98C9 BNE data_rx_loop ; SR2 non-zero: more data, loop back
98CB JMP nmi_rti ; SR2=0: no more data yet, wait for NMI

Data frame completion

Reached when SR2 non-zero during data RX (FV detected). Same pattern as scout completion (&9771): disables PSE (CR1=&00, CR2=&84), then tests FV and RDA. If FV+RDA, reads the last byte. If extra data available and buffer space remains, stores it. Proceeds to send the final ACK via &995E.

98CE .data_rx_complete←3← 989F BPL← 98B4 BNE← 98C4 BEQ
LDA #0 ; CR1=&00: disable all interrupts
98D0 STA econet_control1_or_status1 ; Write CR1
98D3 LDA #&84 ; CR2=&84: disable PSE for individual bit testing
98D5 STA econet_control23_or_status2 ; Write CR2
98D8 STY port_buf_len ; Save Y (byte count from data RX loop)
98DA LDA #2 ; A=&02: FV mask
98DC BIT econet_control23_or_status2 ; BIT SR2: test FV (Z) and RDA (N)
98DF BEQ rx_error ; No FV -- error
98E1 BPL send_ack ; FV set, no RDA -- proceed to ACK
98E3 LDA port_buf_len_hi ; Check if buffer space remains
98E5 .read_last_rx_byte
BEQ rx_error ; No buffer space: error/discard frame
98E7 LDA econet_data_continue_frame ; FV+RDA: read and store last data byte
98EA LDY port_buf_len ; Y = current buffer write offset
98EC STA (open_port_buf),y ; Store last byte in port receive buffer
98EE INC port_buf_len ; Advance buffer write offset
98F0 BNE send_ack ; No page wrap: proceed to send ACK
98F2 INC open_port_buf_hi ; Page boundary: advance buffer page
98F4 .send_ack←2← 98E1 BPL← 98F0 BNE
JMP ack_tx ; Send ACK frame to complete handshake
98F7 .nmi_data_rx_tube
LDA econet_control23_or_status2 ; Read SR2 for Tube data receive path
98FA .rx_tube_data←1← 992B BNE
BPL data_rx_tube_complete ; RDA clear: no more data, frame complete
98FC LDA econet_data_continue_frame ; Read data byte from ADLC RX FIFO
98FF INC port_buf_len ; Advance Tube transfer byte count
9901 STA tube_data_register_3 ; Send byte to Tube data register 3
9904 BNE rx_update_buf ; No overflow: read second byte
9906 INC port_buf_len_hi ; Carry to transfer count byte 2
9908 BNE rx_update_buf ; No overflow: read second byte
990A INC open_port_buf ; Carry to transfer count byte 3
990C BNE rx_update_buf ; No overflow: read second byte
990E INC open_port_buf_hi ; Carry to transfer count byte 4
9910 BEQ data_rx_tube_error ; All bytes zero: overflow error
9912 .rx_update_buf←3← 9904 BNE← 9908 BNE← 990C BNE
LDA econet_data_continue_frame ; Read second data byte (paired transfer)
9915 STA tube_data_register_3 ; Send second byte to Tube
9918 INC port_buf_len ; Advance count after second byte
991A BNE rx_check_error ; No overflow: check for more data
991C INC port_buf_len_hi ; Carry to count byte 2
991E BNE rx_check_error ; No overflow: check for more data
9920 INC open_port_buf ; Carry to count byte 3
9922 BNE rx_check_error ; No overflow: check for more data
9924 INC open_port_buf_hi ; Carry to count byte 4
9926 BEQ data_rx_tube_complete ; Zero: Tube transfer complete
9928 .rx_check_error←3← 991A BNE← 991E BNE← 9922 BNE
LDA econet_control23_or_status2 ; Re-read SR2 for next byte pair
992B BNE rx_tube_data ; More data available: continue loop
992D JMP nmi_rti ; Return from NMI, wait for data
9930 .data_rx_tube_error←3← 9910 BEQ← 9942 BEQ← 994E BEQ
JMP rx_error ; Unexpected end: return from NMI
9933 .data_rx_tube_complete←2← 98FA BPL← 9926 BEQ
LDA #0 ; CR1=&00: disable all interrupts
9935 STA econet_control1_or_status1 ; Write CR1 for individual bit testing
9938 LDA #&84 ; CR2=&84: disable PSE
993A STA econet_control23_or_status2 ; Write CR2: same pattern as main path
993D LDA #2 ; A=&02: FV mask for Tube completion
993F BIT econet_control23_or_status2 ; BIT SR2: test FV (Z) and RDA (N)
9942 BEQ data_rx_tube_error ; No FV: incomplete frame, error
9944 BPL ack_tx ; FV set, no RDA: proceed to ACK
9946 LDA port_buf_len ; Check if any buffer was allocated
9948 ORA port_buf_len_hi ; OR all 4 buffer pointer bytes together
994A ORA open_port_buf ; Check buffer low byte
994C ORA open_port_buf_hi ; Check buffer high byte
994E BEQ data_rx_tube_error ; All zero (null buffer): error
9950 LDA econet_data_continue_frame ; Read extra trailing byte from FIFO
9953 STA rx_extra_byte ; Save extra byte at &0D5D for later use
9956 LDA #&20 ; Bit5 = extra data byte available flag
9958 ORA tx_flags ; Set extra byte flag in tx_flags
995B STA tx_flags ; Store updated flags
fall through ↓

ACK transmission

Sends a scout ACK or final ACK frame as part of the four-way handshake. If bit7 of &0D4A is set, this is a final ACK -> completion (&9F39). Otherwise, configures for TX (CR1=&44, CR2=&A7) and sends the ACK frame (dst_stn, dst_net from &0D3D, src_stn from &FE18, src_net=0). The ACK frame has no data payload -- just address bytes.

After writing the address bytes to the TX FIFO, installs the next NMI handler from &0D4B/&0D4C (saved by the scout/data RX handler) and sends TX_LAST_DATA (CR2=&3F) to close the frame.

995E .ack_tx←2← 98F4 JMP← 9944 BPL
LDA tx_flags ; Load TX flags to check ACK type
9961 BPL ack_tx_configure ; Bit7 clear: normal scout ACK
9963 JMP tx_result_ok ; Jump to TX success result
9966 .ack_tx_configure←1← 9961 BPL
LDA #&44 ; CR1=&44: RX_RESET | TIE (switch to TX mode)
9968 STA econet_control1_or_status1 ; Write CR1: switch to TX mode
996B LDA #&a7 ; CR2=&A7: RTS|CLR_TX_ST|FC_TDRA|2_1_BYTE|PSE
996D STA econet_control23_or_status2 ; Write CR2: enable TX with status clear
9970 LDA #&bb ; Install saved next handler (&99BB for scout ACK)
9972 LDY #&99 ; High byte of post-ACK handler
9974 .ack_tx_write_dest←2← 982A JMP← 9B0F JMP
STA nmi_next_lo ; Store next handler low byte
9977 STY nmi_next_hi ; Store next handler high byte
997A LDA rx_src_stn ; Load dest station from RX scout buffer
997D BIT econet_control1_or_status1 ; BIT SR1: test TDRA (V=bit6)
9980 BVC tdra_error ; TDRA not ready -- error
9982 STA econet_data_continue_frame ; Write dest station to TX FIFO
9985 LDA rx_src_net ; Write dest network to TX FIFO
9988 STA econet_data_continue_frame ; Write dest net byte to FIFO
998B LDA #&92 ; Install handler at &9992 (write src addr)
998D LDY #&99 ; High byte of nmi_ack_tx_src
998F JMP set_nmi_vector ; Set NMI vector to ack_tx_src handler

ACK TX continuation

Writes source station and network to TX FIFO, completing the 4-byte ACK frame. Then sends TX_LAST_DATA (CR2=&3F) to close the frame.

9992 .nmi_ack_tx_src
LDA station_id_disable_net_nmis ; Load our station ID (also INTOFF)
9995 BIT econet_control1_or_status1 ; BIT SR1: test TDRA
9998 BVC tdra_error ; TDRA not ready -- error
999A STA econet_data_continue_frame ; Write our station to TX FIFO
999D LDA #0 ; Write network=0 to TX FIFO
999F STA econet_data_continue_frame ; Write network=0 (local) to TX FIFO
99A2 LDA tx_flags ; Check tx_flags for data phase
99A5 BMI start_data_tx ; bit7 set: start data TX phase
99A7 LDA #&3f ; CR2=&3F: TX_LAST_DATA | CLR_RX_ST | FLAG_IDLE | FC_TDRA | 2_1_BYTE | PSE
99A9 STA econet_control23_or_status2 ; Write CR2 to clear status after ACK TX
99AC LDA nmi_next_lo ; Install saved handler from &0D4B/&0D4C
99AF LDY nmi_next_hi ; Load saved next handler high byte
99B2 JMP set_nmi_vector ; Install next NMI handler
99B5 .start_data_tx←1← 99A5 BMI
JMP data_tx_begin ; Jump to start data TX phase
99B8 .tdra_error←2← 9980 BVC← 9998 BVC
JMP rx_error ; TDRA error: jump to error handler

Post-ACK scout processing

Called after the scout ACK has been transmitted. Processes the received scout data stored in the buffer at &0D3D-&0D48. Checks the port byte (&0D40) against open receive blocks to find a matching listener. If a match is found, sets up the data RX handler chain for the four-way handshake data phase. If no match, discards the frame.

99BB .post_ack_scout
LDA rx_port ; Check port byte from scout
99BE BNE advance_rx_buffer_ptr ; Non-zero port: advance RX buffer
99C0 .dispatch_nmi_error
JMP check_imm_op_ctrl ; Jump to error handler

Advance RX buffer pointer after transfer

Adds the transfer count to the RXCB buffer pointer (4-byte addition). If a Tube transfer is active, re-claims the Tube address and sends the extra RX byte via R3, incrementing the Tube pointer by 1.

99C3 .advance_rx_buffer_ptr←1← 99BE BNE
LDA #2 ; A=2: test bit1 of tx_flags
99C5 BIT tx_flags ; BIT tx_flags: check data transfer bit
99C8 BEQ add_buf_to_base ; Bit1 clear: no transfer -- return
99CA CLC ; CLC: init carry for 4-byte add
99CB PHP ; Save carry on stack for loop
99CC LDY #8 ; Y=8: RXCB high pointer offset
99CE .add_rxcb_ptr←1← 99DA BCC
LDA (port_ws_offset),y ; Load RXCB[Y] (buffer pointer byte)
99D0 PLP ; Restore carry from stack
99D1 ADC net_tx_ptr,y ; Add transfer count byte
99D4 STA (port_ws_offset),y ; Store updated pointer back to RXCB
99D6 INY ; Next byte
99D7 PHP ; Save carry for next iteration
99D8 CPY #&0c ; Done 4 bytes? (Y reaches &0C)
99DA BCC add_rxcb_ptr ; No: continue adding
99DC PLP ; Discard final carry
99DD LDA #&20 ; A=&20: test bit5 of tx_flags
99DF BIT tx_flags ; BIT tx_flags: check Tube bit
99E2 BEQ jmp_store_rxcb ; No Tube: skip Tube update
99E4 TXA ; Save X on stack
99E5 PHA ; Push X
99E6 LDA #8 ; A=8: offset for Tube address
99E8 CLC ; CLC for address calculation
99E9 ADC port_ws_offset ; Add workspace base offset
99EB TAX ; X = address low for Tube claim
99EC LDY rx_buf_offset ; Y = address high for Tube claim
99EE LDA #1 ; A=1: Tube claim type (read)
99F0 JSR tube_addr_claim ; Claim Tube address for transfer
99F3 LDA rx_extra_byte ; Load extra RX data byte
99F6 STA tube_data_register_3 ; Send to Tube via R3
99F9 PLA ; Restore X from stack
99FA TAX ; Transfer to X register
99FB LDY #8 ; Y=8: RXCB buffer ptr offset
99FD LDA (port_ws_offset),y ; Load current RXCB buffer ptr lo
99FF SEC ; SEC for ADC #0 = add carry
9A00 ADC #0 ; Increment by 1 (Tube extra byte)
9A02 STA (port_ws_offset),y ; Store updated ptr back to RXCB
9A04 .jmp_store_rxcb←1← 99E2 BEQ
JMP store_rxcb_completion ; Other port-0 ops: immediate dispatch
9A07 .add_buf_to_base←1← 99C8 BEQ
LDA port_buf_len ; Load buffer bytes remaining
9A09 CLC ; CLC for address add
9A0A ADC open_port_buf ; Add to buffer base address
9A0C BCC store_buf_ptr_lo ; No carry: skip high byte increment
9A0E .inc_rxcb_buf_hi←1← 9A87 LDA
INC open_port_buf_hi ; Carry: increment buffer high byte
9A10 .store_buf_ptr_lo←1← 9A0C BCC
LDY #8 ; Y=8: store updated buffer position
9A12 .store_rxcb_buf_ptr
STA (port_ws_offset),y ; Store updated low byte to RXCB
9A14 INY ; Y=9: buffer high byte offset
9A15 .load_rxcb_buf_hi
LDA open_port_buf_hi ; Load updated buffer high byte
9A17 .store_rxcb_buf_hi
STA (port_ws_offset),y ; Store high byte to RXCB
fall through ↓

Store RXCB completion fields from scout buffer

Writes source network, source station, port, and control byte from the scout buffer into the active RXCB. Sets bit 7 of the control byte to mark reception complete.

9A19 .store_rxcb_completion←2← 9A04 JMP← 9A56 JMP
LDA rx_src_net ; Load source network from scout buffer
9A1C LDY #3 ; Y=3: RXCB source network offset
9A1E STA (port_ws_offset),y ; Store source network to RXCB
9A20 DEY ; Y=2: source station offset Y=&02
9A21 LDA rx_src_stn ; Load source station from scout buffer
9A24 STA (port_ws_offset),y ; Store source station to RXCB
9A26 DEY ; Y=1: port byte offset Y=&01
9A27 LDA rx_port ; Load port byte
9A2A STA (port_ws_offset),y ; Store port to RXCB
9A2C DEY ; Y=0: control/flag byte offset Y=&00
9A2D LDA rx_ctrl ; Load control byte from scout
9A30 ORA #&80 ; Set bit7 = reception complete flag
9A32 STA (port_ws_offset),y ; Store to RXCB (marks CB as complete)
fall through ↓

Discard with full ADLC reset

Performs adlc_full_reset (CR1=&C1, reset both TX and RX sections), then falls through to discard_after_reset. Used when the ADLC is in an unexpected state and needs a hard reset before returning to idle listen mode. 5 references — the main error recovery path.

9A34 .discard_reset_listen←4← 9897 JMP← 9B4F JMP← 9E93 JMP← 9F48 JMP
LDA #2 ; Tube flag bit 1 AND tx_flags bit 1
9A36 BIT tx_flags ; Test tx_flags for Tube transfer
9A39 BEQ discard_listen ; No Tube transfer active -- skip release
9A3B LDA #&82 ; A=&82: Tube release claim type
9A3D JSR tube_addr_claim ; Release Tube claim before discarding
fall through ↓

Discard frame (gentle)

Sends RX_DISCONTINUE (CR1=&A2: RIE|RX_DISCONTINUE) to abort the current frame reception without a full reset, then falls through to discard_after_reset. Used for clean rejection of frames that are correctly formatted but not for us (wrong station/network).

9A40 .discard_listen←2← 9744 JMP← 9A39 BEQ
JSR adlc_rx_listen ; Re-enter idle RX listen mode
fall through ↓

Return to idle listen after reset/discard

Just calls adlc_rx_listen (CR1=&82, CR2=&67) to re-enter idle RX mode, then RTI. The simplest of the three discard paths — used as the tail of both discard_reset_listen and discard_listen.

9A43 .discard_after_reset←2← 9728 JMP← 9741 JMP
.install_rx_scout_handler←2← 9728 JMP← 9741 JMP
LDA #&f6 ; Install nmi_rx_scout (&96F6) as NMI handler
9A45 LDY #&96 ; High byte of nmi_rx_scout
9A47 JMP set_nmi_vector ; Set NMI vector and return
9A4A .copy_scout_fields←1← 97F8 JMP
LDY #4 ; Y=4: start at RX CB offset 4
9A4C .copy_scout_loop←1← 9A54 BNE
LDA rx_src_stn,y ; Load scout field (stn/net/ctrl/port)
9A4F STA (port_ws_offset),y ; Store to port workspace buffer
9A51 INY ; Advance buffer pointer
9A52 CPY #&0c ; All 8 fields copied?
9A54 BNE copy_scout_loop ; No: continue copy loop
9A56 JMP store_rxcb_completion ; Jump to completion handler

Immediate operation handler (port = 0)

Handles immediate (non-data-transfer) operations received via scout frames with port byte = 0. The control byte (&0D3F) determines the operation type: &81 = PEEK (read memory) &82 = POKE (write memory) &83 = JSR (remote procedure call) &84 = user procedure &85 = OS procedure &86 = HALT &87 = CONTINUE The protection mask (LSTAT at &D63) controls which operations are permitted — each bit enables or disables an operation type. If the operation is not permitted by the mask, it is silently ignored. LSTAT can be read/set via OSWORD &12 sub-functions 4/5.

9A59 .immediate_op←1← 9794 JMP
LDY rx_ctrl ; Control byte &81-&88 range check
9A5C CPY #&81 ; Below &81: not an immediate op
9A5E BCC imm_op_out_of_range ; Out of range low: jump to discard
9A60 CPY #&89 ; Above &88: not an immediate op
9A62 BCS imm_op_out_of_range ; Out of range high: jump to discard
9A64 CPY #&87 ; HALT(&87)/CONTINUE(&88) skip protection
9A66 BCS imm_op_dispatch ; Ctrl >= &87: dispatch without mask check
9A68 LDA rx_src_stn ; Load source station number
9A6B CMP #&f0 ; Station >= &F0? (privileged)
9A6D BCS imm_op_dispatch ; Privileged: skip protection check
9A6F TYA ; Convert ctrl byte to 0-based index for mask
9A70 SEC ; SEC for subtract
9A71 SBC #&81 ; A = ctrl - &81 (0-based operation index)
9A73 TAY ; Y = index for mask rotation count
9A74 LDA prot_status ; Load protection mask from LSTAT
9A77 .rotate_prot_mask←1← 9A79 BPL
ROR ; Rotate mask right by control byte index
9A78 DEY ; Decrement rotation counter
9A79 BPL rotate_prot_mask ; Loop until bit aligned
9A7B BCC imm_op_dispatch ; Carry clear: operation permitted
9A7D JMP imm_op_discard ; Operation blocked by LSTAT mask
9A80 .imm_op_dispatch←3← 9A66 BCS← 9A6D BCS← 9A7B BCC
LDY rx_ctrl ; Reload ctrl byte for dispatch table
9A83 LDA rxcb_buf_hi_operand,y ; Look up handler address high byte
9A86 PHA ; Push handler address high
9A87 LDA inc_rxcb_buf_hi,y ; Load handler low byte from jump table
9A8A PHA ; Push handler address low
9A8B RTS ; RTS dispatches to handler
9A8C .imm_op_out_of_range←2← 9A5E BCC← 9A62 BCS
JMP rx_error ; Jump to discard handler
9A8F EQUB <(rx_imm_peek-1)
9A90 EQUB <(rx_imm_poke-1)
9A91 EQUB <(rx_imm_exec-1)
9A92 EQUB <(rx_imm_exec-1)
9A93 EQUB <(rx_imm_exec-1)
9A94 EQUB <(rx_imm_halt_cont-1)
9A95 EQUB <(rx_imm_halt_cont-1)
9A96 EQUB <(rx_imm_machine_type-1)
9A97 EQUB >(rx_imm_peek-1)
9A98 EQUB >(rx_imm_poke-1)
9A99 EQUB >(rx_imm_exec-1)
9A9A EQUB >(rx_imm_exec-1)
9A9B EQUB >(rx_imm_exec-1)
9A9C EQUB >(rx_imm_halt_cont-1)
9A9D EQUB >(rx_imm_halt_cont-1)
9A9E EQUB >(rx_imm_machine_type-1)

RX immediate: JSR/UserProc/OSProc setup

Sets up the port buffer to receive remote procedure data. Copies the 4-byte remote address from rx_remote_addr into the execution address workspace, then jumps to the common receive path. Used for operation types &83-&85.

9A9F .rx_imm_exec
LDA #0 ; Buffer start lo = &00
9AA1 STA open_port_buf ; Set port buffer lo
9AA3 LDA #&82 ; Buffer length lo = &82
9AA5 STA port_buf_len ; Set buffer length lo
9AA7 LDA #1 ; Buffer length hi = 1
9AA9 STA port_buf_len_hi ; Set buffer length hi
9AAB LDA net_rx_ptr_hi ; Load RX page hi for buffer
9AAD STA open_port_buf_hi ; Set port buffer hi
9AAF LDY #3 ; Y=3: copy 4 bytes (3 down to 0)
9AB1 .copy_addr_loop←1← 9AB8 BPL
LDA rx_remote_addr,y ; Load remote address byte
9AB4 STA l0d58,y ; Store to exec address workspace
9AB7 DEY ; Next byte (descending)
9AB8 BPL copy_addr_loop ; Loop until all 4 bytes copied
9ABA JMP send_data_rx_ack ; Enter common data-receive path

RX immediate: POKE setup

Sets up workspace offsets for receiving POKE data, then jumps to the common data-receive path.

9ABD .rx_imm_poke
LDA #&3d ; Port workspace offset = &3D
9ABF STA port_ws_offset ; Store workspace offset lo
9AC1 LDA #&0d ; RX buffer page = &0D
9AC3 STA rx_buf_offset ; Store workspace offset hi
9AC5 JMP ack_scout_match ; Enter POKE data-receive path

RX immediate: machine type query

Sets up a buffer in high memory (length #&01FC) for the machine type query response. Returns system identification data to the remote station.

9AC8 .rx_imm_machine_type
LDA #1 ; Buffer length hi = 1
9ACA STA port_buf_len_hi ; Set buffer length hi
9ACC LDA #&fc ; Buffer length lo = &FC
9ACE STA port_buf_len ; Set buffer length lo
9AD0 LDA #&21 ; Buffer start lo = &25
9AD2 STA open_port_buf ; Set port buffer lo
9AD4 LDA #&7f ; Buffer hi = &7F (below screen)
9AD6 STA open_port_buf_hi ; Set port buffer hi
9AD8 JMP set_tx_reply_flag ; Enter reply build path

RX immediate: PEEK setup

Saves the current TX block pointer, replaces it with a pointer to the workspace, and prepares to send the PEEK response data back to the requesting station.

9ADB .rx_imm_peek
LDA nmi_tx_block ; Save current TX block low byte
9ADD PHA ; Push to stack
9ADE LDA nmi_tx_block_hi ; Save current TX block high byte
9AE0 PHA ; Push to stack
fall through ↓

RX immediate: PEEK setup

Writes &0D3D to port_ws_offset/rx_buf_offset, sets scout_status=2, then calls tx_calc_transfer to send the PEEK response data back to the requesting station. Uses workspace offsets (&A6/&A7) for nmi_tx_block.

9AE1 .rx_imm_peek_setup
LDA #&3d ; Port workspace offset = &3D
9AE3 STA nmi_tx_block ; Store workspace offset lo
9AE5 LDA #&0d ; RX buffer page = &0D
9AE7 STA nmi_tx_block_hi ; Store workspace offset hi
9AE9 LDA #2 ; Scout status = 2 (PEEK response)
9AEB STA scout_status ; Store scout status
9AEE JSR tx_calc_transfer ; Calculate transfer size for response
9AF1 PLA ; Restore saved nmi_tx_block_hi
9AF2 STA nmi_tx_block_hi ; Restore workspace ptr hi byte
9AF4 PLA ; Restore saved nmi_tx_block
9AF5 STA nmi_tx_block ; Restore workspace ptr lo byte
9AF7 BCC imm_op_discard ; C=0: transfer not set up, discard
9AF9 .set_tx_reply_flag←1← 9AD8 JMP
LDA tx_flags ; Mark TX flags bit 7 (reply pending)
9AFC ORA #&80 ; Set reply pending flag
9AFE STA tx_flags ; Store updated TX flags
9B01 .rx_imm_halt_cont
LDA #&44 ; CR1=&44: TIE | TX_LAST_DATA
9B03 STA econet_control1_or_status1 ; Write CR1: enable TX interrupts
9B06 .tx_cr2_setup
LDA #&a7 ; CR2=&A7: RTS|CLR_RX_ST|FC_TDRA|PSE
9B08 STA econet_control23_or_status2 ; Write CR2 for TX setup
9B0B .tx_nmi_setup
LDA #&2f ; NMI handler lo byte (self-modifying)
9B0D .tx_nmi_dispatch_page
LDY #&9b ; Y=&9B: dispatch table page
9B0F JMP ack_tx_write_dest ; Acknowledge and write TX dest

Check control byte for immediate operation type

Loads the RX control byte and compares against &82 (immediate HALT). If HALT, discards the frame via imm_op_discard. Otherwise falls through to imm_op_build_reply.

9B12 .check_imm_op_ctrl←1← 99C0 JMP
LDY rx_ctrl ; Load RX control byte
9B15 CPY #&82 ; Compare against &82 (HALT)
9B17 BEQ imm_op_discard ; HALT: discard frame
fall through ↓

Build immediate operation reply header

Stores data length, source station/network, and control byte into the RX buffer header area for port-0 immediate operations. Then disables SR interrupts and configures the VIA shift register for shift-in mode before returning to idle listen.

9B19 .imm_op_build_reply
LDA port_buf_len ; Get buffer position for reply header
9B1B CLC ; Clear carry for offset addition
9B1C ADC #&80 ; Data offset = buf_len + &80 (past header)
9B1E LDY #&7f ; Y=&7F: reply data length slot
9B20 STA (net_rx_ptr),y ; Store reply data length in RX buffer
9B22 LDY #&80 ; Y=&80: source station slot
9B24 LDA rx_src_stn ; Load requesting station number
9B27 STA (net_rx_ptr),y ; Store source station in reply header
9B29 INY ; Y=&81
9B2A LDA rx_src_net ; Load requesting network number
9B2D STA (net_rx_ptr),y ; Store source network in reply header
9B2F LDA rx_ctrl ; Load control byte from received frame
9B32 STA tx_work_57 ; Save ctrl byte for TX response
9B35 LDA #&84 ; IER bit 2: disable SR interrupt
9B37 STA system_via_ier ; Write IER to disable SR
9B3A LDA system_via_acr ; Read ACR for shift register config
9B3D AND #&1c ; Isolate shift register mode bits (2-4)
9B3F STA tx_work_51 ; Save original SR mode for later restore
9B42 LDA system_via_acr ; Reload ACR for modification
9B45 AND #&e3 ; Clear SR mode bits (keep other bits)
9B47 ORA #8 ; SR mode 2: shift in under φ2
9B49 STA system_via_acr ; Apply new shift register mode
9B4C BIT system_via_sr ; Read SR to clear pending interrupt
9B4F .imm_op_discard←3← 9A7D JMP← 9AF7 BCC← 9B17 BEQ
JMP discard_reset_listen ; Return to idle listen mode
9B52 .check_sr_irq←1← 966C JMP
LDA #4 ; A=&04: IFR bit 2 (SR) mask
9B54 BIT system_via_ifr ; Test SR interrupt pending
9B57 BNE tx_done_error ; SR fired: handle TX completion
9B59 LDA #5 ; A=5: no SR, return status 5
9B5B RTS ; Return (no SR interrupt)
9B5C .tx_done_error←1← 9B57 BNE
TXA ; Save X
9B5D PHA ; Push X
9B5E TYA ; Save Y
9B5F PHA ; Push Y
9B60 LDA system_via_acr ; Read ACR for shift register mode
9B63 AND #&e3 ; Clear SR mode bits (2-4)
9B65 ORA tx_work_51 ; Restore original SR mode
9B68 STA system_via_acr ; Write updated ACR
9B6B LDA system_via_sr ; Read SR to clear pending interrupt
9B6E LDA #4 ; A=&04: SR bit mask
9B70 STA system_via_ifr ; Clear SR in IFR
9B73 STA system_via_ier ; Disable SR in IER
9B76 LDY tx_work_57 ; Load ctrl byte for dispatch
9B79 CPY #&86 ; Ctrl >= &86? (HALT/CONTINUE)
9B7B BCS tx_done_classify ; Yes: skip protection mask save
9B7D LDA prot_status ; Load current protection mask
9B80 STA rx_ctrl_copy ; Save mask before JSR modification
9B83 ORA #&1c ; Enable bits 2-4 (allow JSR ops)
9B85 STA prot_status ; Store modified protection mask
9B88 .tx_done_classify←1← 9B7B BCS
LDA rx_ctrl_operand,y ; Load handler addr hi from table
9B8B PHA ; Push handler hi
9B8C LDA tx_dispatch_page_operand,y ; Load handler addr lo from table
9B8F PHA ; Push handler lo
9B90 RTS ; Dispatch via RTS (addr-1 on stack)
9B91 EQUB <(tx_done_jsr-1)
9B92 EQUB <(tx_done_user_proc-1)
9B93 EQUB <(tx_done_os_proc-1)
9B94 EQUB <(tx_done_halt-1)
9B95 EQUB <(tx_done_continue-1)
9B96 EQUB >(tx_done_jsr-1)
9B97 EQUB >(tx_done_user_proc-1)
9B98 EQUB >(tx_done_os_proc-1)
9B99 EQUB >(tx_done_halt-1)
9B9A EQUB >(tx_done_continue-1)

TX done: remote JSR execution

Pushes a return address on the stack (pointing to tx_done_exit), then does JMP indirect to call the remote JSR target routine. When that routine returns via RTS, control resumes at tx_done_exit.

9B9B .tx_done_jsr
LDA #&9b ; Push hi of (tx_done_exit-1)
9B9D PHA ; Push hi byte on stack
9B9E LDA #&dc ; Push lo of (tx_done_exit-1)
9BA0 PHA ; Push lo byte on stack
9BA1 JMP (l0d58) ; Call remote JSR; RTS to tx_done_exit

TX done: UserProc event

Generates a network event (event 8) via OSEVEN with the remote address. This notifies the user program that a UserProc operation has completed.

9BA4 .tx_done_user_proc
LDY #event_network_error ; Y=8: network event type
9BA6 LDX l0d58 ; X = remote address lo
9BA9 LDA l0d59 ; A = remote address hi
9BAC JSR oseven ; Generate event Y='Network error'
9BAF JMP tx_done_exit ; Exit TX done handler

TX done: OSProc call

Calls the ROM entry point at &8000 (rom_header) with X/Y from the remote address workspace. This invokes an OS-level procedure on behalf of the remote station.

9BB2 .tx_done_os_proc
LDX l0d58 ; X = remote address lo
9BB5 LDY l0d59 ; Y = remote address hi
9BB8 JSR rom_header ; Call ROM entry point at &8000
9BBB JMP tx_done_exit ; Exit TX done handler

TX done: HALT

Sets bit 2 of rx_flags, enables interrupts, and spin-waits until bit 2 is cleared (by a CONTINUE from the remote station). If bit 2 is already set, skips to exit.

9BBE .tx_done_halt
LDA #4 ; A=&04: bit 2 mask for rx_flags
9BC0 BIT rx_status_flags ; Test if already halted
9BC3 BNE tx_done_exit ; Already halted: skip to exit
9BC5 ORA rx_status_flags ; Set bit 2 in rx_flags
9BC8 STA rx_status_flags ; Store halt flag
9BCB LDA #4 ; A=4: re-load halt bit mask
9BCD CLI ; Enable interrupts during halt wait
9BCE .halt_spin_loop←1← 9BD1 BNE
BIT rx_status_flags ; Test halt flag
9BD1 BNE halt_spin_loop ; Still halted: keep spinning
9BD3 BEQ tx_done_exit ; ALWAYS branch

TX done: CONTINUE

Clears bit 2 of rx_flags, releasing any station that is halted and spinning in tx_done_halt.

9BD5 .tx_done_continue
LDA rx_status_flags ; Load current RX flags
9BD8 AND #&fb ; Clear bit 2: release halted station
9BDA STA rx_status_flags ; Store updated flags
9BDD .tx_done_exit←4← 9BAF JMP← 9BBB JMP← 9BC3 BNE← 9BD3 BEQ
PLA ; Restore Y from stack
9BDE TAY ; Transfer to Y register
9BDF PLA ; Restore X from stack
9BE0 TAX ; Transfer to X register
9BE1 LDA #0 ; A=0: success status
9BE3 RTS ; Return with A=0 (success)

Begin TX operation

Main TX initiation entry point (called via trampoline at &06CE). Copies dest station/network from the TXCB to the scout buffer, dispatches to immediate op setup (ctrl >= &81) or normal data transfer, calculates transfer sizes, copies extra parameters, then enters the INACTIVE polling loop.

9BE4 .tx_begin←1← 9660 JMP
TXA ; Save X on stack
9BE5 PHA ; Push X
9BE6 LDY #2 ; Y=2: TXCB offset for dest station
9BE8 LDA (nmi_tx_block),y ; Load dest station from TX control block
9BEA STA tx_dst_stn ; Store to TX scout buffer
9BED INY ; Y=&03
9BEE LDA (nmi_tx_block),y ; Load dest network from TX control block
9BF0 STA tx_dst_net ; Store to TX scout buffer
9BF3 LDY #0 ; Y=0: first byte of TX control block
9BF5 LDA (nmi_tx_block),y ; Load control/flag byte
9BF7 BMI tx_imm_op_setup ; Bit7 set: immediate operation ctrl byte
9BF9 JMP tx_active_start ; Bit7 clear: normal data transfer
9BFC .tx_imm_op_setup←1← 9BF7 BMI
STA tx_ctrl_byte ; Store control byte to TX scout buffer
9BFF TAX ; X = control byte for range checks
9C00 INY ; Y=1: port byte offset
9C01 LDA (nmi_tx_block),y ; Load port byte from TX control block
9C03 STA tx_port ; Store port byte to TX scout buffer
9C06 BNE tx_line_idle_check ; Port != 0: skip immediate op setup
9C08 CPX #&83 ; Ctrl < &83: PEEK/POKE need address calc
9C0A BCS check_imm_range ; Ctrl >= &83: skip to range check
9C0C SEC ; SEC: init borrow for 4-byte subtract
9C0D PHP ; Save carry on stack for loop
9C0E LDY #8 ; Y=8: high pointer offset in TXCB
9C10 .calc_peek_poke_size←1← 9C24 BCC
LDA (nmi_tx_block),y ; Load TXCB[Y] (end addr byte)
9C12 DEY ; Y -= 4: back to start addr offset
9C13 DEY ; (continued)
9C14 DEY ; (continued)
9C15 DEY ; (continued)
9C16 PLP ; Restore borrow from stack
9C17 SBC (nmi_tx_block),y ; end - start = transfer size byte
9C19 STA tx_data_start,y ; Store result to tx_data_start
9C1C INY ; Y += 5: advance to next end byte
9C1D INY ; (continued)
9C1E INY ; (continued)
9C1F INY ; (continued)
9C20 INY ; (continued)
9C21 PHP ; Save borrow for next byte
9C22 CPY #&0c ; Done all 4 bytes? (Y reaches &0C)
9C24 BCC calc_peek_poke_size ; No: next byte pair
9C26 PLP ; Discard final borrow
9C27 .check_imm_range←1← 9C0A BCS
CPX #&89 ; Ctrl >= &89: out of immediate range
9C29 BCS tx_active_start ; Above range: normal data transfer
9C2B LDY #&0c ; Y=&0C: start of extra data in TXCB
9C2D .copy_imm_params←1← 9C35 BCC
LDA (nmi_tx_block),y ; Load extra parameter byte from TXCB
9C2F STA nmi_shim_1a,y ; Copy to NMI shim workspace at &0D1A+Y
9C32 INY ; Next byte
9C33 CPY #&10 ; Done 4 bytes? (Y reaches &10)
9C35 BCC copy_imm_params ; No: continue copying
9C37 .tx_line_idle_check←1← 9C06 BNE
LDA #&20 ; A=&20: mask for SR2 INACTIVE bit
9C39 BIT econet_control23_or_status2 ; BIT SR2: test if line is idle
9C3C BNE tx_no_clock_error ; Line not idle: handle as line jammed
9C3E LDA #&fd ; A=&FD: high byte of timeout counter
9C40 PHA ; Push timeout high byte to stack
9C41 LDA #6 ; Scout frame = 6 address+ctrl bytes
9C43 STA tx_length ; Store scout frame length
9C46 LDA #0 ; A=0: init low byte of timeout counter
fall through ↓

INACTIVE polling loop

Polls SR2 for INACTIVE (bit2) to confirm the network line is idle before attempting transmission. Uses a 3-byte timeout counter on the stack. The timeout (~256^3 iterations) generates "Line Jammed" if INACTIVE never appears. The CTS check at &9C66-&9C6B works because CR2=&67 has RTS=0, so cts_input_ is always true, and SR1_CTS reflects presence of clock hardware.

9C48 .inactive_poll
STA tx_index ; Save TX index
9C4B PHA ; Push timeout byte 1 on stack
9C4C PHA ; Push timeout byte 2 on stack
9C4D LDY #&e7 ; Y=&E7: CR2 value for TX prep (RTS|CLR_TX_ST|CLR_RX_ST|FC_TDRA|2_1_BYTE| PSE)
9C4F .test_inactive_retry←3← 9C75 BNE← 9C7A BNE← 9C7F BNE
LDA #4 ; A=&04: INACTIVE mask for SR2 bit2
9C51 PHP ; Save interrupt state
9C52 SEI ; Disable interrupts for ADLC access
fall through ↓

Disable NMIs and test INACTIVE

Mid-instruction label within the INACTIVE polling loop. The operand byte of the LDA before tx_begin is referenced as a constant for self-modifying code. Disables NMIs twice (belt-and-braces) then tests SR2 for INACTIVE before proceeding with TX.

9C53 .intoff_test_inactive←1← 9CCF LDA
BIT station_id_disable_net_nmis ; INTOFF -- disable NMIs
9C56 BIT station_id_disable_net_nmis ; INTOFF again (belt-and-braces)
9C59 .test_line_idle
BIT econet_control23_or_status2 ; BIT SR2: Z = &04 AND SR2 -- tests INACTIVE
9C5C BEQ inactive_retry ; INACTIVE not set -- re-enable NMIs and loop
9C5E LDA econet_control1_or_status1 ; Read SR1 (acknowledge pending interrupt)
9C61 LDA #&67 ; CR2=&67: CLR_TX_ST|CLR_RX_ST|FC_ TDRA|2_1_BYTE|PSE
9C63 STA econet_control23_or_status2 ; Write CR2: clear status, prepare TX
9C66 LDA #&10 ; A=&10: CTS mask for SR1 bit4
9C68 BIT econet_control1_or_status1 ; BIT SR1: tests CTS present
9C6B BNE tx_prepare ; CTS set -- clock hardware detected, start TX
9C6D .inactive_retry←1← 9C5C BEQ
BIT video_ula_control ; INTON -- re-enable NMIs (&FE20 read)
9C70 PLP ; Restore interrupt state
9C71 TSX ; 3-byte timeout counter on stack
9C72 INC l0101,x ; Increment timeout counter byte 1
9C75 BNE test_inactive_retry ; Not overflowed: retry INACTIVE test
9C77 INC l0102,x ; Increment timeout counter byte 2
9C7A BNE test_inactive_retry ; Not overflowed: retry INACTIVE test
9C7C INC l0103,x ; Increment timeout counter byte 3
9C7F BNE test_inactive_retry ; Not overflowed: retry INACTIVE test
9C81 JMP tx_line_jammed ; All 3 bytes overflowed: line jammed
; TX_ACTIVE branch (A=&44 = CR1 value for TX active)
9C84 .tx_active_start←2← 9BF9 JMP← 9C29 BCS
LDA #&44 ; CR1=&44: TIE | TX_LAST_DATA
9C86 BNE store_tx_error ; ALWAYS branch

TX timeout error handler (Line Jammed)

Writes CR2=&07 to abort TX, cleans 3 bytes from stack (the timeout loop's state), then stores error code &40 ("Line Jammed") into the TX control block and signals completion.

9C88 .tx_line_jammed←1← 9C81 JMP
LDA #7 ; CR2=&07: FC_TDRA | 2_1_BYTE | PSE (abort TX)
9C8A STA econet_control23_or_status2 ; Write CR2 to abort TX
9C8D PLA ; Clean 3 bytes of timeout loop state
9C8E PLA ; Pop saved register
9C8F PLA ; Pop saved register
9C90 LDA #&40 ; Error &40 = 'Line Jammed'
9C92 BNE store_tx_error ; ALWAYS branch to shared error handler ALWAYS branch
9C94 .tx_no_clock_error←1← 9C3C BNE
LDA #&43 ; Error &43 = 'No Clock'
9C96 .store_tx_error←2← 9C86 BNE← 9C92 BNE
LDY #0 ; Offset 0 = error byte in TX control block
9C98 STA (nmi_tx_block),y ; Store error code in TX CB byte 0
9C9A LDA #&80 ; &80 = TX complete flag
9C9C STA tx_ctrl_status ; Signal TX operation complete
9C9F PLA ; Restore X saved by caller
9CA0 TAX ; Move to X register
9CA1 RTS ; Return to TX caller

TX preparation

Configures ADLC for transmission: asserts RTS via CR2, enables TIE via CR1, installs NMI TX handler at &9D4C, and re-enables NMIs.

9CA2 .tx_prepare←1← 9C6B BNE
STY econet_control23_or_status2 ; Write CR2 = Y (&E7: RTS|CLR_TX_ST|CLR_RX_ST| FC_TDRA|2_1_BYTE|PSE)
9CA5 LDX #&44 ; CR1=&44: RX_RESET | TIE (TX active, TX interrupts enabled)
9CA7 STX econet_control1_or_status1 ; Write to ADLC CR1
9CAA LDX #&4c ; Install NMI handler at &9D4C (TX data handler)
9CAC LDY #&9d ; High byte of NMI handler address
9CAE STX nmi_jmp_lo ; Write NMI vector low byte directly
9CB1 STY nmi_jmp_hi ; Write NMI vector high byte directly
9CB4 BIT video_ula_control ; INTON -- NMIs now fire for TDRA (&FE20 read)
9CB7 LDA tx_port ; Load destination port number
9CBA BNE setup_data_xfer ; Port != 0: standard data transfer
9CBC LDY tx_ctrl_byte ; Port 0: load control byte for table lookup
9CBF LDA tube_tx_byte4_operand,y ; Look up tx_flags from table
9CC2 STA tx_flags ; Store operation flags
9CC5 LDA tube_tx_byte2_operand,y ; Look up tx_length from table
9CC8 STA tx_length ; Store expected transfer length
9CCB LDA sr2_test_operand,y ; Load handler from dispatch table
9CCE PHA ; Push high byte for PHA/PHA/RTS dispatch
9CCF LDA intoff_test_inactive,y ; Look up handler address low from table
9CD2 PHA ; Push low byte for PHA/PHA/RTS dispatch
9CD3 RTS ; RTS dispatches to control-byte handler
9CD4 EQUB <(tx_ctrl_peek-1)
9CD5 EQUB <(tx_ctrl_poke-1)
9CD6 EQUB <(tx_ctrl_proc-1)
9CD7 EQUB <(tx_ctrl_proc-1)
9CD8 EQUB <(tx_ctrl_proc-1)
9CD9 EQUB <(tx_ctrl_exit-1)
9CDA EQUB <(tx_ctrl_exit-1)
9CDB EQUB <(imm_op_status3-1)
9CDC EQUB >(tx_ctrl_peek-1)
9CDD EQUB >(tx_ctrl_poke-1)
9CDE EQUB >(tx_ctrl_proc-1)
9CDF EQUB >(tx_ctrl_proc-1)
9CE0 EQUB >(tx_ctrl_proc-1)
9CE1 EQUB >(tx_ctrl_exit-1)
9CE2 EQUB >(tx_ctrl_exit-1)
9CE3 EQUB >(imm_op_status3-1)
9CE4 .imm_op_status3
LDA #3 ; A=3: scout_status for PEEK
9CE6 BNE store_status_calc_xfer ; ALWAYS branch

TX ctrl: PEEK transfer setup

Sets scout_status=3, then performs a 4-byte addition of bytes from the TX block into the transfer parameter workspace (with carry propagation). Calls tx_calc_transfer to finalise, then exits via tx_ctrl_exit.

9CE8 .tx_ctrl_peek
LDA #3 ; A=3: scout_status for PEEK op
9CEA BNE store_status_add4 ; ALWAYS branch

TX ctrl: POKE transfer setup

Sets scout_status=2 and shares the 4-byte addition and transfer calculation path with tx_ctrl_peek.

9CEC .tx_ctrl_poke
LDA #2 ; Scout status = 2 (POKE transfer)
9CEE .store_status_add4←1← 9CEA BNE
STA scout_status ; Store scout status
9CF1 CLC ; Clear carry for 4-byte addition
9CF2 PHP ; Save carry on stack
9CF3 LDY #&0c ; Y=&0C: start at offset 12
9CF5 .add_bytes_loop←1← 9D02 BCC
LDA l0d1e,y ; Load workspace address byte
9CF8 PLP ; Restore carry from previous byte
9CF9 ADC (nmi_tx_block),y ; Add TXCB address byte
9CFB STA l0d1e,y ; Store updated address byte
9CFE INY ; Next byte
9CFF PHP ; Save carry for next addition
fall through ↓

TX ctrl: JSR/UserProc/OSProc setup

Sets scout_status=2 and calls tx_calc_transfer directly (no 4-byte address addition needed for procedure calls). Shared by operation types &83-&85.

9D00 .tx_ctrl_add_done
CPY #&10 ; Compare Y with 16-byte boundary
9D02 BCC add_bytes_loop ; Below boundary: continue addition
9D04 PLP ; Restore processor flags
9D05 JSR tx_calc_transfer ; Calculate transfer byte count
9D08 JMP tx_ctrl_exit ; Jump to TX control exit

TX ctrl: JSR/UserProc/OSProc setup

Sets scout_status=2 and calls tx_calc_transfer directly (no 4-byte address addition needed for procedure calls). Shared by operation types &83-&85.

9D0B .tx_ctrl_proc
LDA #2 ; A=2: scout_status for procedure ops
9D0D .store_status_calc_xfer←1← 9CE6 BNE
STA scout_status ; Store scout status
9D10 JSR tx_calc_transfer ; Calculate transfer parameters
9D13 JMP tx_ctrl_exit ; Exit TX ctrl setup
9D16 .setup_data_xfer←1← 9CBA BNE
LDA tx_dst_stn ; Load dest station for broadcast check
9D19 AND tx_dst_net ; AND with dest network
9D1C CMP #&ff ; Both &FF = broadcast address?
9D1E BNE setup_unicast_xfer ; Not broadcast: unicast path
9D20 LDA #&0e ; Broadcast scout: 14 bytes total
9D22 STA tx_length ; Store broadcast scout length
9D25 LDA #&40 ; A=&40: broadcast flag
9D27 STA tx_flags ; Set broadcast flag in tx_flags
9D2A LDY #4 ; Y=4: start of address data in TXCB
9D2C .copy_bcast_addr←1← 9D34 BCC
LDA (nmi_tx_block),y ; Copy TXCB address bytes to scout buffer
9D2E STA tx_src_stn,y ; Store to TX source/data area
9D31 INY ; Next byte
9D32 CPY #&0c ; Done 8 bytes? (Y reaches &0C)
9D34 BCC copy_bcast_addr ; No: continue copying
9D36 BCS tx_ctrl_exit ; ALWAYS branch
9D38 .setup_unicast_xfer←1← 9D1E BNE
LDA #0 ; A=0: clear flags for unicast
9D3A STA tx_flags ; Clear tx_flags
9D3D .proc_op_status2
LDA #2 ; scout_status=2: data transfer pending
9D3F .store_status_copy_ptr
STA scout_status ; Store scout status
9D42 JSR tx_calc_transfer ; Calculate transfer size from RXCB
9D45 .tx_ctrl_exit←3← 9D08 JMP← 9D13 JMP← 9D36 BCS
PLP ; Restore processor status from stack
9D46 PLA ; Restore stacked registers (4 PLAs)
9D47 PLA ; Second PLA
9D48 PLA ; Third PLA
9D49 PLA ; Fourth PLA
9D4A TAX ; Restore X from A
9D4B RTS ; Return to caller

NMI TX data handler

Writes 2 bytes per NMI invocation to the TX FIFO at &FEA2. Uses the BIT instruction on SR1 to test TDRA (V flag = bit6) and IRQ (N flag = bit7). After writing 2 bytes, checks if the frame is complete. If more data, tests SR1 bit7 (IRQ) via BMI -- if IRQ still asserted, writes 2 more bytes without returning from NMI (tight loop). Otherwise returns via RTI.

9D4C .nmi_tx_data
LDY tx_index ; Load TX buffer index
9D4F BIT econet_control1_or_status1 ; BIT SR1: V=bit6(TDRA), N=bit7(IRQ)
9D52 .tx_fifo_write←1← 9D6D BMI
BVC tx_fifo_not_ready ; TDRA not set -- TX error
9D54 LDA tx_dst_stn,y ; Load byte from TX buffer
9D57 STA econet_data_continue_frame ; Write to TX_DATA (continue frame)
9D5A INY ; Next TX buffer byte
9D5B LDA tx_dst_stn,y ; Load second byte from TX buffer
9D5E INY ; Advance TX index past second byte
9D5F STY tx_index ; Save updated TX buffer index
9D62 STA econet_data_continue_frame ; Write second byte to TX_DATA
9D65 CPY tx_length ; Compare index to TX length
9D68 BCS tx_last_data ; Frame complete -- go to TX_LAST_DATA
9D6A BIT econet_control1_or_status1 ; Check if we can send another pair
9D6D BMI tx_fifo_write ; IRQ set -- send 2 more bytes (tight loop)
9D6F JMP nmi_rti ; RTI -- wait for next NMI
; TX error path
9D72 .tx_error←1← 9DB7 BEQ
LDA #&42 ; Error &42
9D74 BNE tx_store_error ; ALWAYS branch
9D76 .tx_fifo_not_ready←1← 9D52 BVC
LDA #&67 ; CR2=&67: clear status, return to listen
9D78 STA econet_control23_or_status2 ; Write CR2: clear status, idle listen
9D7B LDA #&41 ; Error &41 (TDRA not ready)
9D7D .tx_store_error←1← 9D74 BNE
LDY station_id_disable_net_nmis ; INTOFF (also loads station ID)
9D80 .delay_nmi_disable←1← 9D83 BNE
PHA ; PHA/PLA delay loop (256 iterations for NMI disable)
9D81 PLA ; PHA/PLA delay (~7 cycles each)
9D82 INY ; Increment delay counter
9D83 BNE delay_nmi_disable ; Loop 256 times for NMI disable
9D85 JMP tx_store_result ; Jump to error handler

TX_LAST_DATA and frame completion

Signals end of TX frame by writing CR2=&3F (TX_LAST_DATA). Then installs the TX completion NMI handler at &9D94 which switches to RX mode. CR2=&3F = 0011_1111: bit5: CLR_RX_ST -- clears fv_stored_ (prepares for RX of reply) bit4: TX_LAST_DATA -- tells ADLC this is the final data byte bit3: FLAG_IDLE -- send flags/idle after frame bit2: FC_TDRA -- force clear TDRA bit1: 2_1_BYTE -- two-byte transfer mode bit0: PSE -- prioritised status enable Note: NO CLR_TX_ST (bit6=0), NO RTS (bit7=0 -- drops RTS after frame)

9D88 .tx_last_data←1← 9D68 BCS
LDA #&3f ; CR2=&3F: TX_LAST_DATA | CLR_RX_ST | FLAG_IDLE | FC_TDRA | 2_1_BYTE | PSE
9D8A STA econet_control23_or_status2 ; Write to ADLC CR2
9D8D LDA #&94 ; Install NMI handler at &9D94 (TX completion)
9D8F LDY #&9d ; High byte of handler address
9D91 JMP set_nmi_vector ; Install and return via set_nmi_vector

TX completion: switch to RX mode

Called via NMI after the frame (including CRC and closing flag) has been fully transmitted. Switches from TX mode to RX mode by writing CR1=&82. CR1=&82 = 1000_0010: TX_RESET | RIE (listen for reply). Checks workspace flags to decide next action: - bit6 set at &0D4A -> completion at &9F39 - bit0 set at &0D4A -> four-way handshake data phase at &9EDD - Otherwise -> install RX reply handler at &9DB2

9D94 .nmi_tx_complete
LDA #&82 ; CR1=&82: TX_RESET | RIE (now in RX mode)
9D96 STA econet_control1_or_status1 ; Write CR1 to switch from TX to RX
9D99 BIT tx_flags ; Test workspace flags
9D9C BVC check_handshake_bit ; bit6 not set -- check bit0
9D9E JMP tx_result_ok ; bit6 set -- TX completion
9DA1 .check_handshake_bit←1← 9D9C BVC
LDA #1 ; A=1: mask for bit0 test
9DA3 BIT tx_flags ; Test tx_flags bit0 (handshake)
9DA6 BEQ install_reply_scout ; bit0 clear: install reply handler
9DA8 JMP handshake_await_ack ; bit0 set -- four-way handshake data phase
9DAB .install_reply_scout←1← 9DA6 BEQ
LDA #&b2 ; Install RX reply handler at &9DB2
9DAD LDY #&9d ; High byte of nmi_reply_scout addr
9DAF JMP set_nmi_vector ; Install handler and RTI

RX reply scout handler

Handles reception of the reply scout frame after transmission. Checks SR2 bit0 (AP) for incoming data, reads the first byte (destination station) and compares to our station ID via &FE18 (which also disables NMIs as a side effect).

9DB2 .nmi_reply_scout
LDA #1 ; A=&01: AP mask for SR2
9DB4 BIT econet_control23_or_status2 ; BIT SR2: test AP (Address Present)
9DB7 BEQ tx_error ; No AP -- error
9DB9 LDA econet_data_continue_frame ; Read first RX byte (destination station)
9DBC CMP station_id_disable_net_nmis ; Compare to our station ID (INTOFF side effect)
9DBF BNE reply_error ; Not our station -- error/reject
9DC1 LDA #&c8 ; Install next handler at &9DC8 (reply continuation)
9DC3 LDY #&9d ; High byte of nmi_reply_cont
9DC5 JMP set_nmi_vector ; Install continuation handler

RX reply continuation handler

Reads the second byte of the reply scout (destination network) and validates it is zero (local network). Installs &9DE3 for the remaining two bytes (source station and network). Optimisation: checks SR1 bit7 (IRQ still asserted) via BMI at &9DD9. If IRQ is still set, falls through directly to &9DE3 without an RTI, avoiding NMI re-entry overhead for short frames where all bytes arrive in quick succession.

9DC8 .nmi_reply_cont
BIT econet_control23_or_status2 ; BIT SR2: test for RDA (bit7 = data available)
9DCB BPL reply_error ; No RDA -- error
9DCD LDA econet_data_continue_frame ; Read destination network byte
9DD0 BNE reply_error ; Non-zero -- network mismatch, error
9DD2 LDA #&e3 ; Install next handler at &9DE3 (reply validation)
9DD4 LDY #&9d ; High byte of nmi_reply_validate
9DD6 BIT econet_control1_or_status1 ; BIT SR1: test IRQ (N=bit7) -- more data ready?
9DD9 BMI nmi_reply_validate ; IRQ set -- fall through to &9D5B without RTI
9DDB JMP set_nmi_vector ; IRQ not set -- install handler and RTI
9DDE .reply_error←7← 9DBF BNE← 9DCB BPL← 9DD0 BNE← 9DE6 BPL← 9DEE BNE← 9DF6 BNE← 9DFD BEQ
LDA #&41 ; A=&41: 'not listening' error code
9DE0 .reject_reply
JMP tx_store_result ; Store error and return to idle

RX reply validation (Path 2 for FV/PSE interaction)

Reads the source station and source network from the reply scout and validates them against the original TX destination (&0D20/&0D21). Sequence: 1. Check SR2 bit7 (RDA) at &9DE3 -- must see data available 2. Read source station at &9DE8, compare to &0D20 (tx_dst_stn) 3. Read source network at &9DF0, compare to &0D21 (tx_dst_net) 4. Check SR2 bit1 (FV) at &9DFA -- must see frame complete If all checks pass, the reply scout is valid and the ROM proceeds to send the scout ACK (CR2=&A7 for RTS, CR1=&44 for TX mode).

9DE3 .nmi_reply_validate←1← 9DD9 BMI
BIT econet_control23_or_status2 ; BIT SR2: test RDA (bit7). Must be set for valid reply.
9DE6 BPL reply_error ; No RDA -- error (FV masking RDA via PSE would cause this)
9DE8 LDA econet_data_continue_frame ; Read source station
9DEB CMP tx_dst_stn ; Compare to original TX destination station (&0D20)
9DEE BNE reply_error ; Mismatch -- not the expected reply, error
9DF0 LDA econet_data_continue_frame ; Read source network
9DF3 CMP tx_dst_net ; Compare to original TX destination network (&0D21)
9DF6 BNE reply_error ; Mismatch -- error
9DF8 LDA #2 ; A=&02: FV mask for SR2 bit1
9DFA BIT econet_control23_or_status2 ; BIT SR2: test FV -- frame must be complete
9DFD BEQ reply_error ; No FV -- incomplete frame, error
9DFF LDA #&a7 ; CR2=&A7: RTS|CLR_TX_ST|FC_TDRA|2_ 1_BYTE|PSE (TX in handshake)
9E01 STA econet_control23_or_status2 ; Write CR2: enable RTS for TX handshake
9E04 LDA #&44 ; CR1=&44: RX_RESET | TIE (TX active for scout ACK)
9E06 STA econet_control1_or_status1 ; Write CR1: reset RX, enable TX interrupt
9E09 LDA #&dd ; Install next handler at &9EDD (four-way data phase) into &0D4B/&0D4C
9E0B LDY #&9e ; High byte &9E of next handler address
9E0D STA nmi_next_lo ; Store low byte to nmi_next_lo
9E10 STY nmi_next_hi ; Store high byte to nmi_next_hi
9E13 LDA tx_dst_stn ; Load dest station for scout ACK TX
9E16 BIT econet_control1_or_status1 ; BIT SR1: test TDRA (V=bit6)
9E19 BVC data_tx_error ; TDRA not ready -- error
9E1B STA econet_data_continue_frame ; Write dest station to TX FIFO
9E1E LDA tx_dst_net ; Load dest network for scout ACK TX
9E21 STA econet_data_continue_frame ; Write dest network to TX FIFO
9E24 LDA #&2b ; Install handler at &9E2B (write src addr for scout ACK)
9E26 LDY #&9e ; High byte &9D of handler address
9E28 JMP set_nmi_vector ; Set NMI vector and return

TX scout ACK: write source address

Writes our station ID and network=0 to TX FIFO, completing the 4-byte scout ACK frame. Then proceeds to send the data frame.

9E2B .nmi_scout_ack_src
LDA station_id_disable_net_nmis ; Read our station ID (also INTOFF)
9E2E BIT econet_control1_or_status1 ; BIT SR1: check TDRA before writing
9E31 BVC data_tx_error ; TDRA not ready: TX error
9E33 STA econet_data_continue_frame ; Write our station to TX FIFO
9E36 LDA #0 ; Network = 0 (local network)
9E38 STA econet_data_continue_frame ; Write network byte to TX FIFO
9E3B .data_tx_begin←1← 99B5 JMP
LDA #2 ; Test bit 1 of tx_flags
9E3D BIT tx_flags ; Check if immediate-op or data-transfer
9E40 BNE install_imm_data_nmi ; Bit 1 set: immediate op, use alt handler
9E42 LDA #&50 ; Install nmi_data_tx at &9E50
9E44 LDY #&9e ; High byte of handler address
9E46 JMP set_nmi_vector ; Install and return via set_nmi_vector
9E49 .install_imm_data_nmi←1← 9E40 BNE
LDA #&a4 ; Install nmi_data_tx_tube at &9EA4
9E4B LDY #&9e ; High byte of handler address
9E4D JMP set_nmi_vector ; Install and return via set_nmi_vector

TX data phase: send payload

Sends the data frame payload from (open_port_buf),Y in pairs per NMI. Same pattern as the NMI TX handler at &9D4C but reads from the port buffer instead of the TX workspace. Writes two bytes per iteration, checking SR1 IRQ between pairs for tight looping.

9E50 .nmi_data_tx
LDY port_buf_len ; Y = buffer offset, resume from last position
9E52 BIT econet_control1_or_status1 ; BIT SR1: test TDRA (V=bit6)
9E55 .data_tx_check_fifo←1← 9E78 BMI
BVC data_tx_error ; TDRA not ready -- error
9E57 LDA (open_port_buf),y ; Write data byte to TX FIFO
9E59 STA econet_data_continue_frame ; Write first byte of pair to FIFO
9E5C INY ; Advance buffer offset
9E5D BNE write_second_tx_byte ; No page crossing
9E5F DEC port_buf_len_hi ; Page crossing: decrement page count
9E61 BEQ data_tx_last ; No pages left: send last data
9E63 INC open_port_buf_hi ; Increment buffer high byte
9E65 .write_second_tx_byte←1← 9E5D BNE
LDA (open_port_buf),y ; Load second byte of pair
9E67 STA econet_data_continue_frame ; Write second byte to FIFO
9E6A INY ; Advance buffer offset
9E6B STY port_buf_len ; Save updated buffer position
9E6D BNE check_irq_loop ; No page crossing
9E6F DEC port_buf_len_hi ; Page crossing: decrement page count
9E71 BEQ data_tx_last ; No pages left: send last data
9E73 INC open_port_buf_hi ; Increment buffer high byte
9E75 .check_irq_loop←1← 9E6D BNE
BIT econet_control1_or_status1 ; BIT SR1: test IRQ (N=bit7) for tight loop
9E78 BMI data_tx_check_fifo ; IRQ still set: write 2 more bytes
9E7A JMP nmi_rti ; No IRQ: return, wait for next NMI
9E7D .data_tx_last←4← 9E61 BEQ← 9E71 BEQ← 9EBD BEQ← 9ED3 BEQ
LDA #&3f ; CR2=&3F: TX_LAST_DATA (close data frame)
9E7F STA econet_control23_or_status2 ; Write CR2 to close frame
9E82 LDA tx_flags ; Check tx_flags for next action
9E85 BPL install_saved_handler ; Bit7 clear: error, install saved handler
9E87 LDA #&34 ; Install discard_reset_listen at &9A34
9E89 LDY #&9a ; High byte of &9A34 handler
9E8B JMP set_nmi_vector ; Set NMI vector and return
9E8E .data_tx_error←4← 9E19 BVC← 9E31 BVC← 9E55 BVC← 9EA7 BVC
LDA tx_flags ; Load saved next handler low byte
9E91 BPL nmi_tx_not_listening ; bit7 clear: error path
9E93 JMP discard_reset_listen ; ADLC reset and return to idle
9E96 .nmi_tx_not_listening←1← 9E91 BPL
LDA #&41 ; A=&41: 'not listening' error
9E98 .jmp_tx_result_fail
JMP tx_store_result ; Store result and return to idle
9E9B .install_saved_handler←1← 9E85 BPL
LDA nmi_next_lo ; Load saved handler low byte
9E9E LDY nmi_next_hi ; Load saved next handler high byte
9EA1 JMP set_nmi_vector ; Install saved handler and return
9EA4 .nmi_data_tx_tube
BIT econet_control1_or_status1 ; Tube TX: BIT SR1 test TDRA
9EA7 .tube_tx_fifo_write←1← 9ED8 BMI
BVC data_tx_error ; TDRA not ready -- error
9EA9 LDA tube_data_register_3 ; Read byte from Tube R3
9EAC STA econet_data_continue_frame ; Write to TX FIFO
9EAF INC port_buf_len ; Increment 4-byte buffer counter
9EB1 BNE write_second_tube_byte ; Low byte didn't wrap
9EB3 INC port_buf_len_hi ; Carry into second byte
9EB5 BNE write_second_tube_byte ; No further carry
9EB7 INC open_port_buf ; Carry into third byte
9EB9 BNE write_second_tube_byte ; No further carry
9EBB INC open_port_buf_hi ; Carry into fourth byte
9EBD BEQ data_tx_last ; Counter wrapped to zero: last data
9EBF .write_second_tube_byte←3← 9EB1 BNE← 9EB5 BNE← 9EB9 BNE
LDA tube_data_register_3 ; Read second Tube byte from R3
9EC2 STA econet_data_continue_frame ; Write second byte to TX FIFO
9EC5 INC port_buf_len ; Increment 4-byte counter (second byte)
9EC7 BNE check_tube_irq_loop ; Low byte didn't wrap
9EC9 .tube_tx_inc_byte2
INC port_buf_len_hi ; Carry into second byte
9ECB BNE check_tube_irq_loop ; No further carry
9ECD .tube_tx_inc_byte3
INC open_port_buf ; Carry into third byte
9ECF BNE check_tube_irq_loop ; No further carry
9ED1 .tube_tx_inc_byte4
INC open_port_buf_hi ; Carry into fourth byte
9ED3 BEQ data_tx_last ; Counter wrapped to zero: last data
9ED5 .check_tube_irq_loop←3← 9EC7 BNE← 9ECB BNE← 9ECF BNE
BIT econet_control1_or_status1 ; BIT SR1: test IRQ for tight loop
9ED8 BMI tube_tx_fifo_write ; IRQ still set: write 2 more bytes
9EDA JMP nmi_rti ; No IRQ: return, wait for next NMI

Four-way handshake: switch to RX for final ACK

After the data frame TX completes, switches to RX mode (CR1=&82) and installs &9EE9 to receive the final ACK from the remote station.

9EDD .handshake_await_ack←1← 9DA8 JMP
LDA #&82 ; CR1=&82: TX_RESET | RIE (switch to RX for final ACK)
9EDF STA econet_control1_or_status1 ; Write to ADLC CR1
9EE2 LDA #&e9 ; Install handler at &9EE9 (RX final ACK)
9EE4 LDY #&9e ; High byte of handler address
9EE6 JMP set_nmi_vector ; Install and return via set_nmi_vector

RX final ACK handler

Receives the final ACK in a four-way handshake. Same validation pattern as the reply scout handler (&9DB2-&9DE3): &9EE9: Check AP, read dest_stn, compare to our station &9EFF: Check RDA, read dest_net, validate = 0 &9F15: Check RDA, read src_stn/net, compare to TX dest &9F32: Check FV for frame completion On success, stores result=0 at &9F39. On any failure, error &41.

9EE9 .nmi_final_ack
LDA #1 ; A=&01: AP mask
9EEB BIT econet_control23_or_status2 ; BIT SR2: test AP
9EEE BEQ tx_result_fail ; No AP -- error
9EF0 LDA econet_data_continue_frame ; Read dest station
9EF3 CMP station_id_disable_net_nmis ; Compare to our station (INTOFF side effect)
9EF6 BNE tx_result_fail ; Not our station -- error
9EF8 LDA #&ff ; Install handler at &9EFF (final ACK continuation)
9EFA LDY #&9e ; High byte of handler address
9EFC JMP set_nmi_vector ; Install continuation handler
9EFF .nmi_final_ack_net
BIT econet_control23_or_status2 ; BIT SR2: test RDA
9F02 BPL tx_result_fail ; No RDA -- error
9F04 LDA econet_data_continue_frame ; Read dest network
9F07 BNE tx_result_fail ; Non-zero -- network mismatch, error
9F09 LDA #&15 ; Install handler at &9F15 (final ACK validation)
9F0B LDY #&9f ; High byte of handler address
9F0D BIT econet_control1_or_status1 ; BIT SR1: test IRQ -- more data ready?
9F10 BMI nmi_final_ack_validate ; IRQ set -- fall through to &9F15 without RTI
9F12 JMP set_nmi_vector ; Install handler and RTI

Final ACK validation

Reads and validates src_stn and src_net against original TX dest. Then checks FV for frame completion.

9F15 .nmi_final_ack_validate←1← 9F10 BMI
BIT econet_control23_or_status2 ; BIT SR2: test RDA
9F18 BPL tx_result_fail ; No RDA -- error
9F1A LDA econet_data_continue_frame ; Read source station
9F1D CMP tx_dst_stn ; Compare to TX dest station (&0D20)
9F20 BNE tx_result_fail ; Mismatch -- error
9F22 LDA econet_data_continue_frame ; Read source network
9F25 CMP tx_dst_net ; Compare to TX dest network (&0D21)
9F28 BNE tx_result_fail ; Mismatch -- error
9F2A LDA tx_flags ; Load TX flags for next action
9F2D BPL check_fv_final_ack ; bit7 clear: no data phase
9F2F JMP install_data_rx_handler ; Install data RX handler
9F32 .check_fv_final_ack←1← 9F2D BPL
LDA #2 ; A=&02: FV mask for SR2 bit1
9F34 BIT econet_control23_or_status2 ; BIT SR2: test FV -- frame must be complete
9F37 BEQ tx_result_fail ; No FV -- error
fall through ↓

TX completion handler

Stores result code 0 (success) into the first byte of the TX control block (nmi_tx_block),Y=0. Then sets &0D3A bit7 to signal completion and calls full ADLC reset + idle listen via &9A34.

9F39 .tx_result_ok←2← 9963 JMP← 9D9E JMP
LDA #0 ; A=0: success result code
9F3B BEQ tx_store_result ; BEQ: always taken (A=0) ALWAYS branch

TX failure: not listening

Loads error code &41 (not listening) and falls through to tx_store_result. The most common TX error path — reached from 11 sites across the final-ACK validation chain when the remote station doesn't respond or the frame is malformed.

9F3D .tx_result_fail←8← 9EEE BEQ← 9EF6 BNE← 9F02 BPL← 9F07 BNE← 9F18 BPL← 9F20 BNE← 9F28 BNE← 9F37 BEQ
LDA #&41 ; A=&41: not listening error code
fall through ↓

TX error handler

Stores error code (A) into the TX control block, sets &0D3A bit7 for completion, and returns to idle via &9A34. Error codes: &00=success, &40=line jammed, &41=not listening, &42=net error.

9F3F .tx_store_result←5← 9891 JMP← 9D85 JMP← 9DE0 JMP← 9E98 JMP← 9F3B BEQ
LDY #0 ; Y=0: index into TX control block
9F41 STA (nmi_tx_block),y ; Store result/error code at (nmi_tx_block),0
9F43 LDA #&80 ; &80: completion flag for &0D3A
9F45 STA tx_ctrl_status ; Signal TX complete
9F48 JMP discard_reset_listen ; Full ADLC reset and return to idle listen
; Unreferenced data block (purpose unknown)
9F4B EQUB &0E, &0E, &0A, &0A, &0A, &06, &06, &0A, &81, &00, &00, &00, &00, &01, &01, &81

Calculate transfer size

Computes the number of bytes actually transferred during a data frame reception. Subtracts the low pointer (LPTR, offset 4 in the RXCB) from the current buffer position to get the byte count, and stores it back into the RXCB's high pointer field (HPTR, offset 8). This tells the caller how much data was received.

9F5B .tx_calc_transfer←5← 980E JSR← 9AEE JSR← 9D05 JSR← 9D10 JSR← 9D42 JSR
LDY #6 ; Load RXCB[6] (buffer addr byte 2)
9F5D LDA (nmi_tx_block),y ; Load TX block byte at offset 6
9F5F INY ; Y=&07
9F60 AND (nmi_tx_block),y ; AND with TX block[7] (byte 3)
9F62 CMP #&ff ; Both &FF = no buffer?
9F64 BEQ fallback_calc_transfer ; Yes: fallback path
9F66 LDA tx_in_progress ; Tube transfer in progress?
9F69 BEQ fallback_calc_transfer ; No: fallback path
9F6B LDA tx_flags ; Load TX flags for transfer setup
9F6E ORA #2 ; Set bit 1 (transfer complete)
9F70 STA tx_flags ; Store with bit 1 set (Tube xfer)
9F73 SEC ; Init borrow for 4-byte subtract
9F74 PHP ; Save carry on stack
9F75 LDY #4 ; Y=4: start at RXCB offset 4
9F77 .calc_transfer_size←1← 9F89 BCC
LDA (nmi_tx_block),y ; Load RXCB[Y] (current ptr byte)
9F79 INY ; Y += 4: advance to RXCB[Y+4]
9F7A INY ; (continued)
9F7B INY ; (continued)
9F7C INY ; (continued)
9F7D PLP ; Restore borrow from previous byte
9F7E SBC (nmi_tx_block),y ; Subtract RXCB[Y+4] (start ptr byte)
9F80 STA net_tx_ptr,y ; Store result byte
9F83 DEY ; Y -= 3: next source byte
9F84 DEY ; (continued)
9F85 DEY ; (continued)
9F86 PHP ; Save borrow for next byte
9F87 CPY #8 ; Done all 4 bytes?
9F89 BCC calc_transfer_size ; No: next byte pair
9F8B PLP ; Discard final borrow
9F8C TXA ; A = saved X
9F8D PHA ; Save X
9F8E LDA #4 ; Compute address of RXCB+4
9F90 CLC ; CLC for base pointer addition
9F91 ADC nmi_tx_block ; Add RXCB base to get RXCB+4 addr
9F93 TAX ; X = low byte of RXCB+4
9F94 LDY nmi_tx_block_hi ; Y = high byte of RXCB ptr
9F96 LDA #&c2 ; Tube claim type &C2
9F98 JSR tube_addr_claim ; Claim Tube transfer address
9F9B BCC restore_x_and_return ; No Tube: skip reclaim
9F9D LDA scout_status ; Tube: reclaim with scout status
9FA0 JSR tube_addr_claim ; Reclaim with scout status type
9FA3 SEC ; C=1: Tube address claimed
9FA4 .restore_x_and_return←1← 9F9B BCC
PLA ; Restore X
9FA5 TAX ; Restore X from stack
9FA6 RTS ; Return with C = transfer status
9FA7 .fallback_calc_transfer←2← 9F64 BEQ← 9F69 BEQ
LDY #4 ; Y=4: RXCB current pointer offset
9FA9 LDA (nmi_tx_block),y ; Load RXCB[4] (current ptr lo)
9FAB LDY #8 ; Y=8: RXCB start address offset
9FAD SEC ; Set carry for subtraction
9FAE SBC (nmi_tx_block),y ; Subtract RXCB[8] (start ptr lo)
9FB0 STA port_buf_len ; Store transfer size lo
9FB2 LDY #5 ; Y=5: current ptr hi offset
9FB4 LDA (nmi_tx_block),y ; Load RXCB[5] (current ptr hi)
9FB6 SBC #0 ; Propagate borrow from lo subtraction
9FB8 STA open_port_buf_hi ; Temp store adjusted current ptr hi
9FBA LDY #8 ; Y=8: start address lo offset
9FBC LDA (nmi_tx_block),y ; Copy RXCB[8] to open port buffer lo
9FBE STA open_port_buf ; Store to scratch (side effect)
9FC0 LDY #9 ; Y=9: start address hi offset
9FC2 LDA (nmi_tx_block),y ; Load RXCB[9] (start ptr hi)
9FC4 SEC ; Set carry for subtraction
9FC5 SBC open_port_buf_hi ; start_hi - adjusted current_hi
9FC7 STA port_buf_len_hi ; Store transfer size hi
9FC9 SEC ; Return with C=1
9FCA .nmi_shim_rom_src←1← 96D1 LDA
RTS ; Return with C=1 (success)

Bootstrap NMI entry point (in ROM)

An alternate NMI handler that lives in the ROM itself rather than in the RAM workspace at &0D00. Unlike the RAM shim (which uses a self-modifying JMP to dispatch to different handlers), this one hardcodes JMP nmi_rx_scout (&96F6). Used as the initial NMI handler before the workspace has been properly set up during initialisation. Same sequence as the RAM shim: BIT &FE18 (INTOFF), PHA, TYA, PHA, LDA romsel, STA &FE30, JMP &96F6.

9FCB .nmi_bootstrap_entry
BIT station_id_disable_net_nmis ; INTOFF: disable NMIs while switching ROM
9FCE PHA ; Save A
9FCF TYA ; Transfer Y to A
9FD0 PHA ; Save Y (via A)
9FD1 LDA #0 ; ROM bank 0 (patched during init for actual bank)
9FD3 STA romsel ; Select Econet ROM bank via ROMSEL
9FD6 JMP nmi_rx_scout ; Jump to scout handler in ROM

ROM copy of set_nmi_vector + nmi_rti

A version of the NMI vector-setting subroutine and RTI sequence that lives in ROM. The RAM workspace copy at &0D0E/&0D14 is the one normally used at runtime; this ROM copy is used during early initialisation before the RAM workspace has been set up, and as the source for the initial copy to RAM.

9FD9 .rom_set_nmi_vector
STY nmi_jmp_hi ; Store handler high byte at &0D0D
9FDC STA nmi_jmp_lo ; Store handler low byte at &0D0C
9FDF LDA romsel_copy ; Restore NFS ROM bank
9FE1 STA romsel ; Page in via hardware latch
9FE4 PLA ; Restore Y from stack
9FE5 TAY ; Transfer ROM bank to Y
9FE6 PLA ; Restore A from stack
9FE7 BIT video_ula_control ; INTON: re-enable NMIs
9FEA RTI ; Return from interrupt
9FEB .rom_nmi_tail
LDA tx_flags ; Load current TX flags
9FEE ORA #2 ; Set bit 1 (transfer mode flag)
9FF0 STA tx_flags ; Store updated TX flags
9FF3 SEC ; SEC for subtraction
9FF4 PHP ; Save carry on stack
9FF5 LDY #4 ; Y=4: TXCB data start low offset
9FF7 LDA (nmi_tx_block),y ; Load data start low byte
9FF9 INY ; Y += 4: advance to data end low offset Y=&05
9FFA INY ; (continued) Y=&06
9FFB INY ; (continued) Y=&07
9FFC INY ; (continued) Y=&08
9FFD PLP ; Restore carry for subtraction
9FFE SBC (nmi_tx_block),y ; Subtract buffer end from start
fall through ↓