Acorn NFS 3.34

Updated 31 Mar 2026

← All Acorn NFS and Acorn ANFS versions

; Sideways ROM header
; NFS ROM 3.34 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← 842D 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 &34 ; 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 (&8DAF): read file handle from received packet (net_1_read_handle)

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

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

*NET4 (&8DF2): 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 &8BD6, and from FSCV 2/3/4 indirectly. If CSD handle is zero (not logged in), returns without sending.

8079 .forward_star_cmd←1← 8D1C 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 reloc_p4_src to RAM (&0400/&0500/&0600), calls tube_post_init (&0414), and copies 97 bytes of workspace init from reloc_zp_src 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 l944c,y ; Load ROM byte from page &94
8102 STA tube_dispatch_table,y ; Store to page &05 (dispatch table)
8105 LDA c954c,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 calls (index 1+)
8139 JSR dispatch ; Dispatch to service handler
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← 8DF2 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← 911B 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.34."
81C9 .restore_y_return←2← 8182 BNE← 81DE BNE
LDY nfs_temp ; Reload character counter
81CB 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).

81CC .call_fscv_shutdown←2← 8184 JSR← 81D1 JSR
LDA #6 ; FSCV reason 6 = FS shutdown
81CE 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.

81D1 .svc_3_autoboot
JSR call_fscv_shutdown ; Notify current FS of shutdown
81D4 LDA #osbyte_scan_keyboard_from_16 ; OSBYTE &7A: scan keyboard
81D6 JSR osbyte ; Keyboard scan starting from key 16
81D9 TXA ; X is key number if key is pressed, or &ff otherwise
81DA 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.

81DC .check_boot_key
EOR #&55 ; XOR with &55: result=0 if key is 'N'
81DE BNE restore_y_return ; Not 'N': return without claiming
81E0 TAY ; Y=key
81E1 LDA #osbyte_write_keys_pressed ; OSBYTE &78: clear key-pressed state
81E3 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.

81E6 .print_station_info←1← 81DA BMI
JSR print_inline ; Print 'Econet Station ' banner
81E9 EQUS "Econet Station "
81F8 LDA tx_clear_flag ; Load local station number
81FB JSR print_decimal ; Print station number as decimal
81FE LDA #&20 ; A=&20: test bit 5 of SR2 (clock)
8200 BIT econet_control23_or_status2 ; Test ADLC SR2 for network clock
8203 BEQ skip_no_clock_msg ; Clock present: skip warning msg
8205 JSR print_inline ; Print ' No Clock' warning
8208 EQUS " No Clock"
8211 NOP ; NOP (padding after inline string)
8212 .skip_no_clock_msg←1← 8203 BEQ
JSR print_inline ; Print two CRs (blank line)
8215 EQUS ".."
fall through ↓

Initialise filing system vectors

Copies 14 bytes from fs_vector_addrs (&824D) 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.

8217 .init_fs_vectors←1← 8199 BEQ
LDY #&0d ; Copy 14 bytes: FS vector addresses → FILEV-FSCV
8219 .dofsl1←1← 8220 BPL
LDA fs_vector_addrs,y ; Load vector address from table
821C STA filev,y ; Write to FILEV-FSCV vector table
821F DEY ; Next byte (descending)
8220 BPL dofsl1 ; Loop until all 14 bytes copied
8222 JSR setup_rom_ptrs_netv ; Read ROM ptr table addr, install NETV
8225 LDY #&1b ; Install 7 handler entries in ROM ptr table
8227 LDX #7 ; 7 FS vectors to install
8229 JSR store_rom_ptr_pair ; Install each 3-byte vector entry
822C 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 &8245 and jumps to the FSCV 3 unrecognised-command handler (which matches against the command table at &8BD6). The "I." prefix triggers the catch-all entry which forwards the command to the fileserver. Falls through to run_fscv_cmd.

822E .issue_vectors_claimed←1← 818A JSR
LDA #osbyte_issue_service_request ; A=&8F: issue service request
8230 LDX #&0f ; X=&0F: 'vectors claimed' service
8232 JSR osbyte ; Issue paged ROM service call, Reason X=15 - Vectors claimed
8235 LDX #&0a ; X=&0A: service &0A
8237 JSR osbyte ; Issue service &0A
823A LDX nfs_temp ; Non-zero after hard reset: skip auto-boot
823C BNE return_3 ; Non-zero: skip auto-boot
823E LDX #&45 ; X = lo byte of auto-boot string at &8245
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.

8240 .run_fscv_cmd←2← 82E5 LDA← 82EB LDA
LDY #&82 ; Y=&82: ROM page high byte
8242 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.
8245 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.

824D .fs_vector_addrs←1← 8219 LDA
EQUB &1B ; FILEV dispatch lo
824E EQUB &FF ; FILEV dispatch hi
824F EQUB &1E ; ARGSV dispatch lo
8250 EQUB &FF ; ARGSV dispatch hi
8251 EQUB &21 ; BGETV dispatch lo
8252 EQUB &FF ; BGETV dispatch hi
8253 EQUB &24 ; BPUTV dispatch lo
8254 EQUB &FF ; BPUTV dispatch hi
8255 EQUB &27 ; GBPBV dispatch lo
8256 EQUB &FF ; GBPBV dispatch hi
8257 EQUB &2A ; FINDV dispatch lo
8258 EQUB &FF ; FINDV dispatch hi
8259 EQUB &2D ; FSCV dispatch lo
825A EQUB &FF ; FSCV dispatch hi
825B EQUB &94 ; FILEV handler lo (&8694)
825C EQUB &86 ; FILEV handler hi
825D EQUB &00 ; (ROM bank — overwritten)
825E EQUB &E1 ; ARGSV handler lo (&88E1)
825F EQUB &88 ; ARGSV handler hi
8260 EQUB &00 ; (ROM bank — overwritten)
8261 EQUB &85 ; BGETV handler lo (&8485)
8262 EQUB &84 ; BGETV handler hi
8263 EQUB &00 ; (ROM bank — overwritten)
8264 EQUB &A2 ; BPUTV handler lo (&83A2)
8265 EQUB &83 ; BPUTV handler hi
8266 EQUB &00 ; (ROM bank — overwritten)
8267 EQUB &EA ; GBPBV handler lo (&89EA)
8268 EQUB &89 ; GBPBV handler hi
8269 EQUB &00 ; (ROM bank — overwritten)
826A EQUB &49 ; FINDV handler lo (&8949)
826B EQUB &89 ; FINDV handler hi
826C EQUB &00 ; (ROM bank — overwritten)
826D EQUB &8C ; FSCV handler lo (&808C)
826E 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)
826F .svc_1_abs_workspace
CPY #&10 ; Check if page &10 or above
8271 BCS return_3 ; Not our workspace: return
8273 LDY #&10 ; Claim workspace up to page &10
8275 .return_3←2← 823C BNE← 8271 BCS
RTS ; Return to caller
8276 EQUB &07, &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)
8278 .svc_2_private_workspace
STY net_rx_ptr_hi ; Store RX buffer page
827A INY ; Advance to next page
827B STY nfs_workspace_hi ; Store workspace page
827D LDA #0 ; A=0 for clearing workspace
827F LDY #4 ; Y=4: remote status offset
8281 STA (net_rx_ptr),y ; Clear status byte in net receive buffer
8283 LDY #&ff ; Y=&FF: used for later iteration
8285 STA net_rx_ptr ; Clear RX ptr low byte
8287 STA nfs_workspace ; Clear workspace ptr low byte
8289 STA nfs_temp ; Clear RXCB iteration counter
828B STA tx_ctrl_status ; Clear TX semaphore (no TX in progress)
828E TAX ; X=0 for OSBYTE X=&00
828F LDA #osbyte_read_write_last_break_type ; OSBYTE &FD: read type of last reset
8291 JSR osbyte ; Read type of last reset
8294 TXA ; X = break type from OSBYTE result X=value of type of last reset
8295 BEQ read_station_id ; Soft break (X=0): skip FS init
8297 LDY #&15 ; Y=&15: printer station offset in RX buffer
8299 LDA #&fe ; &FE = no server selected
829B STA fs_server_stn ; Station &FE = no server selected
829E STA (net_rx_ptr),y ; Store &FE at printer station offset
82A0 LDY #2 ; Y=2: printer server offset
82A2 LDA #&eb ; A=&EB: default printer server
82A4 STA (nfs_workspace),y ; Store printer server at offset 2
82A6 INY ; Y=&03
82A7 LDA #0 ; A=0: clear remaining fields
82A9 STA fs_server_net ; Clear FS server network number
82AC STA (nfs_workspace),y ; Clear workspace byte at offset 3
82AE STA prot_status ; Clear protection status mask
82B1 STA fs_messages_flag ; Clear FS messages flag
82B4 .init_rxcb_entries←1← 82C1 BNE
LDA nfs_temp ; Load RXCB counter
82B6 JSR calc_handle_offset ; Convert to workspace byte offset
82B9 BCS read_station_id ; C=1: past max handles, done
82BB LDA #&3f ; Mark RXCB as available
82BD STA (nfs_workspace),y ; Write &3F flag to workspace
82BF INC nfs_temp ; Next RXCB number
82C1 BNE init_rxcb_entries ; Loop for all RXCBs
82C3 .read_station_id←2← 8295 BEQ← 82B9 BCS
LDA station_id_disable_net_nmis ; Read station ID (also INTOFF)
82C6 STA tx_clear_flag ; Store station ID for TX scout
82C9 JSR trampoline_adlc_init ; Initialise ADLC hardware
82CC LDA #&40 ; Enable user-level RX (LFLAG=&40)
82CE 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=&9007, rom=current) into the ROM pointer table at offset &36, installing osword_dispatch as the NETV handler.

82D1 .setup_rom_ptrs_netv←1← 8222 JSR
LDA #osbyte_read_rom_ptr_table_low ; OSBYTE &A8: read ROM pointer table address
82D3 LDX #0 ; X=0: read low byte
82D5 LDY #&ff ; Y=&FF: read high byte
82D7 JSR osbyte ; Returns table address in X (lo) Y (hi) Read address of ROM pointer table
82DA STX osrdsc_ptr ; Store table base address low byte X=value of address of ROM pointer table (low byte)
82DC STY osrdsc_ptr_hi ; Store table base address high byte Y=value of address of ROM pointer table (high byte)
82DE LDY #&36 ; NETV extended vector offset in ROM ptr table
82E0 STY netv ; Set NETV low byte = &36 (vector dispatch)
82E3 LDX #1 ; Install 1 entry (NETV) in ROM ptr table
82E5 .store_rom_ptr_pair←2← 8229 JSR← 82F7 BNE
LDA run_fscv_cmd,y ; Load handler address low byte from table
82E8 STA (osrdsc_ptr),y ; Store to ROM pointer table
82EA INY ; Next byte
82EB LDA run_fscv_cmd,y ; Load handler address high byte from table
82EE STA (osrdsc_ptr),y ; Store to ROM pointer table
82F0 INY ; Next byte
82F1 LDA romsel_copy ; Write current ROM bank number
82F3 STA (osrdsc_ptr),y ; Store ROM number to ROM pointer table
82F5 INY ; Advance to next entry position
82F6 DEX ; Count down entries
82F7 BNE store_rom_ptr_pair ; Loop until all entries installed
82F9 LDY nfs_workspace_hi ; Y = next workspace page for MOS
82FB INY ; Advance past workspace page
82FC 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.

82FD .fscv_6_shutdown
LDY #&1d ; Copy 10 bytes: FS state to workspace backup
82FF .fsdiel←1← 8307 BNE
LDA fs_state_deb,y ; Load FS state byte at offset Y
8302 STA (net_rx_ptr),y ; Store to workspace backup area
8304 DEY ; Next byte down
8305 CPY #&14 ; Offsets &15-&1D: server, handles, OPT, etc.
8307 BNE fsdiel ; Loop for offsets &1D..&15
8309 LDA #osbyte_printer_driver_going_dormant ; A=&7B: printer driver going dormant
830B 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.

830E .init_tx_ctrl_data←1← 8381 JSR
.init_tx_reply_port←1← 8381 JSR
LDA #&90 ; A=&90: FS reply port (PREPLY)
8310 .init_tx_ctrl_port←2← 880E JSR← 8F78 LDA
JSR init_tx_ctrl_block ; Init TXCB from template
8313 STA txcb_port ; Store port number in TXCB
8315 LDA #3 ; Control byte: 3 = transmit
8317 STA txcb_start ; Store control byte in TXCB
8319 DEC txcb_ctrl ; Decrement TXCB flag to arm TX
831B RTS ; Return after port setup

Initialise TX control block at &00C0 from template

Copies 12 bytes from tx_ctrl_template (&8334) 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.

831C .init_tx_ctrl_block←3← 8310 JSR← 8370 JSR← 83B9 JSR
PHA ; Preserve A across call
831D LDY #&0b ; Copy 12 bytes (Y=11..0)
831F .fstxl1←1← 8330 BPL
LDA tx_ctrl_template,y ; Load template byte
8322 STA txcb_ctrl,y ; Store to TX control block at &00C0
8325 CPY #2 ; Y < 2: also copy FS server station/network
8327 BPL fstxl2 ; Skip station/network copy for Y >= 2
8329 LDA fs_server_stn,y ; Load FS server station (Y=0) or network (Y=1)
832C STA txcb_dest,y ; Store to dest station/network at &00C2
832F .fstxl2←1← 8327 BPL
DEY ; Next byte (descending)
8330 BPL fstxl1 ; Loop until all 12 bytes copied
8332 PLA ; Restore A
8333 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.

8334 .tx_ctrl_template←1← 831F LDA
EQUB &80 ; Control flag
8335 EQUB &99 ; Port (FS command = &99)
8336 EQUB &00 ; Station (filled at runtime)
8337 EQUB &00 ; Network (filled at runtime)
8338 EQUB &00 ; Buffer start low
8339 EQUB &0F ; Buffer start high (page &0F)
833A .tx_ctrl_upper←3← 888F BIT← 8969 BIT← 915D BIT
EQUB &FF ; Buffer start pad (4-byte Econet addr)
833B EQUB &FF ; Buffer start pad
833C EQUB &FF ; Buffer end low
833D EQUB &0F ; Buffer end high (page &0F)
833E EQUB &FF ; Buffer end pad
833F 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
8340 .prepare_cmd_with_flag←1← 8A3B JSR
PHA ; Save flag byte for command
8341 LDA #&2a ; A=&2A: error ptr for retry
8343 SEC ; C=1: include flag in FS command
8344 BCS store_fs_hdr_fn ; ALWAYS branch to prepare_fs_cmd ALWAYS branch
8346 .prepare_cmd_clv←2← 86D7 JSR← 8780 JSR
CLV ; V=0: command has no flag byte
8347 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 &8BD6 for "BYE".

8349 .bye_handler
LDA #osbyte_close_spool_exec ; A=&77: OSBYTE close spool/exec
834B JSR osbyte ; Close any *SPOOL and *EXEC files
834E 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)
8350 .prepare_fs_cmd←12← 807D JSR← 8834 JSR← 88AA JSR← 88FC JSR← 8923 JSR← 8996 JSR← 89C0 JSR← 8A9A JSR← 8B50 JSR← 8C18 JSR← 8C4F JSR← 8CC8 JSR
CLV ; V=0: standard FS command path
8351 .prepare_fs_cmd_v←2← 8892 JSR← 896C JSR
LDA fs_urd_handle ; Copy URD handle from workspace to buffer
8354 STA fs_cmd_urd ; Store URD at &0F02
8357 LDA #&2a ; A=&2A: error ptr for retry
8359 .store_fs_hdr_clc←1← 8347 BVC
CLC ; CLC: no byte-stream path
835A .store_fs_hdr_fn←1← 8344 BCS
STY fs_cmd_y_param ; Store function code at &0F01
835D STA fs_error_ptr ; Store error ptr for TX poll
835F LDY #1 ; Y=1: copy CSD (offset 1) then LIB (offset 0)
8361 .copy_dir_handles←1← 8368 BPL
LDA fs_csd_handle,y ; Copy CSD and LIB handles to command buffer A=timeout period for FS reply
8364 STA fs_cmd_csd,y ; Store at &0F03 (CSD) and &0F04 (LIB)
8367 DEY ; Y=function code
8368 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", "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)
836A .build_send_fs_cmd←1← 8AF3 JSR
PHP ; Save carry (FS path vs byte-stream)
836B LDA #&90 ; Reply port &90 (PREPLY)
836D STA fs_cmd_type ; Store at &0F00 (HDRREP)
8370 JSR init_tx_ctrl_block ; Copy TX template to &00C0
8373 TXA ; A = X (buffer extent)
8374 ADC #5 ; HPTR = header (5) + data (X) bytes to send
8376 STA txcb_end ; Store to TXCB end-pointer low
8378 PLP ; Restore carry flag
8379 BCS dofsl5 ; C=1: byte-stream path (BSXMIT)
837B PHP ; Save flags for send_fs_reply_cmd
837C JSR setup_tx_ptr_c0 ; Point net_tx_ptr to &00C0; transmit
837F PLP ; Restore flags
8380 .send_fs_reply_cmd←2← 8790 JSR← 8A77 JSR
PHP ; Save flags (V flag state)
8381 JSR init_tx_ctrl_data ; Set up RX wait for FS reply
8384 LDA fs_error_ptr ; Load error ptr for TX retry
8386 JSR send_to_fs ; Transmit and wait (BRIANX)
8389 PLP ; Restore flags
838A .dofsl7←1← 83A0 BCC
INY ; Y=1: skip past command code byte
838B LDA (txcb_start),y ; Load return code from FS reply
838D TAX ; X = return code
838E BEQ return_dofsl7 ; Zero: success, return
8390 BVC check_fs_error ; V=0: standard path, error is fatal
8392 ADC #&2a ; ADC #&2A: test for &D6 (not found)
8394 .check_fs_error←1← 8390 BVC
BNE store_fs_error ; Non-zero: hard error, go to FSERR
8396 .return_dofsl7←1← 838E BEQ
RTS ; Return (success or soft &D6 error)
8397 .dofsl5←1← 8379 BCS
PLA ; Discard saved flags from stack
8398 LDX #&c0 ; X=&C0: TXCB address for byte-stream TX
839A INY ; Y++ past command code
839B JSR econet_tx_retry ; Byte-stream transmit with retry
839E STA fs_load_addr_3 ; Store result to &B3
83A0 BCC dofsl7 ; C=0: success, check reply code
83A2 .bputv_handler
CLC ; CLC for address addition
fall through ↓

Handle BPUT/BGET file byte I/O

BPUTV enters at &83A2 (CLC; fall through) and BGETV enters at &8485 (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
83A3 .handle_bput_bget←1← 8486 JSR
PHA ; Save A (BPUT byte) on stack
83A4 STA fs_error_flags ; Also save byte at &0FDF for BSXMIT
83A7 TXA ; Transfer X for stack save
83A8 PHA ; Save X on stack
83A9 TYA ; Transfer Y (handle) for stack save
83AA PHA ; Save Y (handle) on stack
83AB PHP ; Save P (C = BPUT/BGET selector) on stack
83AC JSR handle_to_mask_clc ; Convert handle Y to single-bit mask
83AF STY fs_handle_mask ; Store handle bitmask at &0FDE
83B2 STY fs_spool0 ; Store handle bitmask for sequence tracking
83B4 LDY #&90 ; &90 = data port (PREPLY)
83B6 STY fs_putb_buf ; Store reply port in command buffer
83B9 JSR init_tx_ctrl_block ; Set up 12-byte TXCB from template
83BC LDA #&dc ; CB reply buffer at &0FDC
83BE STA txcb_start ; Store reply buffer ptr low in TXCB
83C0 LDA #&e0 ; Error buffer at &0FE0
83C2 STA txcb_end ; Store error buffer ptr low in TXCB
83C4 INY ; Y=1 (from init_tx_ctrl_block exit)
83C5 LDX #9 ; X=9: BPUT function code
83C7 PLP ; Restore C: selects BPUT (0) vs BGET (1)
83C8 BCC store_retry_count ; C=0 (BPUT): keep X=9
83CA DEX ; X=&08
83CB .store_retry_count←1← 83C8 BCC
STX fs_getb_buf ; Store function code at &0FDD
83CE LDA fs_handle_mask ; Load handle bitmask for BPUT/BGET
83D1 LDX #&c0 ; X=&C0: TXCB address for econet_tx_retry
83D3 JSR econet_tx_retry ; Transmit via byte-stream protocol
83D6 LDX fs_getb_buf ; Load reply byte from buffer
83D9 BEQ update_sequence_return ; Zero reply = success, skip error handling
83DB LDY #&1f ; Copy 32-byte reply to error buffer at &0FE0
83DD .error1←1← 83E4 BPL
LDA fs_putb_buf,y ; Load reply byte at offset Y
83E0 STA fs_error_buf,y ; Store to error buffer at &0FE0+Y
83E3 DEY ; Next byte (descending)
83E4 BPL error1 ; Loop until all 32 bytes copied
83E6 TAX ; X=File handle
83E7 LDA #osbyte_read_write_spool_file_handle ; A=&C7: read *SPOOL file handle
83E9 JSR osbyte ; Read/Write *SPOOL file handle
83EC TXA ; X=value of *SPOOL file handle
83ED JSR handle_to_mask_a ; Convert SPOOL handle to bitmask
83F0 CPY fs_spool0 ; Compare SPOOL mask with file mask
83F2 BNE dispatch_fs_error ; Not SPOOL file: dispatch FS error
83F4 LDX #<(sp_dot_string) ; Load '*SP.' command string low
83F6 LDY #>(sp_dot_string) ; Y=&85: high byte of OSCLI string in ROM
83F8 JSR oscli ; Close SPOOL/EXEC via "*SP." or "*E."
83FB .dispatch_fs_error←1← 83F2 BNE
LDA #&e0 ; Reset CB pointer to error buffer at &0FE0
83FD STA txcb_start ; Reset reply ptr to error buffer
83FF 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.

8402 .store_fs_error←1← 8394 BNE
STX fs_last_error ; Remember raw FS error code
8405 LDY #1 ; Y=1: point to error number byte in reply
8407 CPX #&a8 ; Clamp FS errors below &A8 to standard &A8
8409 BCS find_cr_terminator ; Error >= &A8: keep original value
840B LDA #&a8 ; Error < &A8: override with standard &A8
840D STA (txcb_start),y ; Write clamped error number to reply buffer
840F .find_cr_terminator←2← 8409 BCS← 8414 BNE
INY ; Advance to next reply buffer byte
8410 LDA #&0d ; A=CR: terminator to search for
8412 EOR (txcb_start),y ; XOR with buffer byte (0 when CR)
8414 BNE find_cr_terminator ; Not CR: continue scanning
8416 STA (txcb_start),y ; Store 0 (from XOR) to replace CR
8418 JMP (txcb_start) ; Execute error via JMP indirect
841B .update_sequence_return←1← 83D9 BEQ
STA fs_sequence_nos ; Save updated sequence number
841E PLA ; Restore Y from stack
841F TAY ; Transfer A to Y for indexing
8420 PLA ; Restore X from stack
8421 TAX ; Transfer to X for return
8422 PLA ; Restore A from stack
8423 RTS ; Return to caller
8424 .error_not_listening←1← 8477 BEQ
LDA #8 ; Error code 8: "Not listening" error
8426 BNE set_listen_offset ; ALWAYS branch to set_listen_offset ALWAYS branch
8428 .nlistn←1← 868D JMP
LDA (net_tx_ptr,x) ; Load TX status byte for error lookup
842A .nlisne←2← 8483 BNE← 89B8 JMP
AND #7 ; Mask to 3-bit error code (0-7)
842C .set_listen_offset←1← 8426 BNE
TAX ; X = error code index
842D LDY error_offsets,x ; Look up error message offset from table
8430 LDX #0 ; X=0: start writing at &0101
8432 STX l0100 ; Store BRK opcode at &0100
8435 .copy_error_message←1← 843F BNE
LDA error_msg_table,y ; Load error message byte
8438 STA l0101,x ; Build error message at &0101+
843B BEQ execute_brk_error ; Zero byte = end of message; go execute BRK
843D INX ; Advance output buffer position
843E INY ; Advance source string pointer
843F BNE copy_error_message ; Continue copying message bytes
8441 .execute_brk_error←1← 843B BEQ
JMP l0100 ; Execute constructed BRK error
8444 .sp_dot_string
EQUS "SP."
8447 EQUB &0D
8448 .send_to_fs_star←3← 8730 JSR← 8FBB JSR← 927D 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.

844A .send_to_fs←2← 8386 JSR← 881A JSR
PHA ; Save function code on stack
844B LDA rx_status_flags ; Load current rx_flags
844E PHA ; Save rx_flags on stack for restore
844F ORA #&80 ; Set bit7: FS transaction in progress
8451 STA rx_status_flags ; Write back updated rx_flags
8454 .skip_rx_flag_set
LDA #0 ; Push two zero bytes as timeout counters
8456 PHA ; First zero for timeout
8457 PHA ; Second zero for timeout
8458 TAY ; Y=0: index for flag byte check Y=&00
8459 TSX ; TSX: index stack-based timeout via X
845A .incpx←3← 8464 BNE← 8469 BNE← 846E BNE
LDA (net_tx_ptr),y ; Load TX flag byte from ctrl block
845C BMI fs_wait_cleanup ; Bit 7 set: TX complete, clean up
845E JSR check_escape ; Check for Escape during TX wait
8461 DEC l0101,x ; Three-stage nested timeout: inner loop
8464 BNE incpx ; Inner not expired: keep polling
8466 DEC l0102,x ; Middle timeout loop
8469 BNE incpx ; Middle not expired: keep polling
846B DEC l0104,x ; Outer timeout loop (slowest)
846E BNE incpx ; Outer not expired: keep polling
8470 .fs_wait_cleanup←1← 845C BMI
PLA ; Pop first timeout byte
8471 PLA ; Pop second timeout byte
8472 PLA ; Pop saved rx_flags into A
8473 STA rx_status_flags ; Restore saved rx_flags from stack
8476 PLA ; Pop saved function code
8477 BEQ error_not_listening ; A=saved func code; zero would mean no reply
8479 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.

847A .check_escape←2← 845E JSR← 8674 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.

847C .check_escape_handler
BIT escape_flag ; Test escape flag (bit 7)
847E BPL return_bget ; Bit 7 clear: no escape, return
8480 JSR osbyte ; Acknowledge escape via OSBYTE &7E
8483 BNE nlisne ; Non-zero: report 'Not listening'
8485 .bgetv_handler
SEC ; C=1: flag for BGET mode
8486 JSR handle_bput_bget ; Handle BGET via FS command Handle BPUT/BGET file byte I/O
8489 SEC ; SEC: set carry for error check
848A LDA #&fe ; A=&FE: mask for EOF check
848C BIT fs_error_flags ; BIT l0fdf: test error flags
848F BVS return_bget ; V=1: error, return early
8491 CLC ; CLC: no error
8492 BMI tx_flow_control ; Bit 7 set: set EOF hint flag
8494 LDA fs_spool0 ; Load handle bitmask for flag op
8496 JSR clear_fs_flag ; C=0: no escape, test for retry
8499 BCC tx_error_classify ; Flag cleared: load handle mask
849B .tx_flow_control←1← 8492 BMI
LDA fs_spool0 ; Branch if flow control set
849D JSR set_fs_flag ; Error code: TX failed
84A0 .tx_error_classify←1← 8499 BCC
LDA fs_handle_mask ; Shift error bits right
84A3 .return_bget←2← 847E BPL← 848F BVS
RTS ; Return with handle mask in A
84A4 .add_5_to_y←1← 8760 JSR
INY ; Y += 5
84A5 .add_4_to_y←1← 8A4D JSR
INY ; Y += 4
84A6 INY ; (continued)
84A7 INY ; (continued)
84A8 INY ; (continued)
84A9 RTS ; Return with Y adjusted
84AA .sub_4_from_y←1← 874F JSR
DEY ; Y -= 4
84AB .sub_3_from_y←2← 885C JSR← 8A55 JSR
DEY ; Y -= 3
84AC DEY ; (continued)
84AD DEY ; (continued)
84AE .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.
84AF .error_msg_table←1← 8435 LDA
EQUB &A0
84B0 EQUS "Line Jammed."
84BC EQUB &A1
84BD EQUS "Net Error."
84C7 EQUB &A2
84C8 EQUS "Not listening."
84D6 EQUB &A3
84D7 EQUS "No Clock."
84E0 EQUB &A4
84E1 EQUS "Bad Txcb."
84EA EQUB &11
84EB EQUS "Escape."
84F2 EQUB &CB
84F3 EQUS "Bad Option."
84FE EQUB &A5
84FF 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
8508 .save_fscv_args←6← 808C JSR← 8694 JSR← 88E1 JSR← 8949 JSR← 89EA JSR← 8B92 JSR
STA fs_last_byte_flag ; A = function code / command
850A STX fs_options ; X = control block ptr lo
850C STY fs_block_offset ; Y = control block ptr hi
850E STX fs_crc_lo ; X dup for indexed access via (fs_crc)
8510 STY fs_crc_hi ; Y dup for indexed access
8512 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 &8530. The two formats use different bit layouts for file protection attributes.

8513 .decode_attribs_6bit←2← 886E JSR← 8899 JSR
LDY #&0e ; Y=&0E: attribute byte offset in param block
8515 LDA (fs_options),y ; Load FS attribute byte
8517 AND #&3f ; Mask to 6 bits (FS → BBC direction)
8519 LDX #4 ; X=4: skip first 4 table entries (BBC→FS half)
851B 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 &8530. 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
851D .decode_attribs_5bit←2← 879B JSR← 88B6 JSR
AND #&1f ; Mask to 5 bits (BBC → FS direction)
851F LDX #&ff ; X=&FF: INX makes 0; start from table index 0
8521 .attrib_shift_bits←1← 851B BNE
STA fs_error_ptr ; Temp storage for source bitmask to shift out
8523 LDA #0 ; A=0: accumulate destination bits here
8525 .map_attrib_bits←1← 852D BNE
INX ; Next table entry
8526 LSR fs_error_ptr ; Shift out source bits one at a time
8528 BCC skip_set_attrib_bit ; Bit was 0: skip this destination bit
852A ORA access_bit_table,x ; OR in destination bit from lookup table
852D .skip_set_attrib_bit←1← 8528 BCC
BNE map_attrib_bits ; Loop while source bits remain (A != 0)
852F RTS ; Return; A = converted attribute bitmask
8530 .access_bit_table←1← 852A 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
853B .print_inline←13← 81BC JSR← 81E6 JSR← 8205 JSR← 8212 JSR← 8C20 JSR← 8C2A JSR← 8C38 JSR← 8C43 JSR← 8C5D JSR← 8C6E JSR← 8C81 JSR← 8C95 JSR← 8CA2 JSR
PLA ; Pop return address (low) — points to last byte of JSR
853C STA fs_load_addr ; Store return addr low as string ptr
853E PLA ; Pop return address (high)
853F STA fs_load_addr_hi ; Store return addr high as string ptr
8541 LDY #0 ; Y=0: offset for indirect load
8543 .print_inline_char←1← 8550 BNE
INC fs_load_addr ; Advance pointer past return address / to next char
8545 BNE print_next_char ; No page wrap: skip high byte inc
8547 INC fs_load_addr_hi ; Handle page crossing in pointer
8549 .print_next_char←1← 8545 BNE
LDA (fs_load_addr),y ; Load next byte from inline string
854B BMI filev_attrib_code_check ; Bit 7 set? Done — this byte is the next opcode
854D JSR osasci ; Write character
8550 BNE print_inline_char ; Continue printing loop
8552 .filev_attrib_code_check←1← 854B 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).

8555 .skip_spaces←3← 855A BEQ← 8C0C JSR← 8D06 JSR
LDA (fs_options),y ; Load character from parameter string
8557 INY ; Advance to next character
8558 CMP #&20 ; Compare against space (ASCII &20)
855A BEQ skip_spaces ; Space found: keep scanning
855C DEY ; Back up one (first non-space char)
855D CMP #&41 ; Compare against 'A' for case flag
855F 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": "preserved", "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
8560 .parse_decimal←2← 8D0D JSR← 8D13 JSR
TAX ; Save A in X for caller
8561 LDA #0 ; Zero accumulator
8563 STA fs_load_addr_2 ; Clear accumulator workspace
8565 .scan_decimal_digit←1← 8582 BNE
LDA (fs_options),y ; Load next char from buffer
8567 CMP #&40 ; Letter or above?
8569 BCS no_dot_exit ; Yes: not a digit, done
856B CMP #&2e ; Dot separator?
856D BEQ parse_decimal_rts ; Yes: exit with C=1 (dot found)
856F BMI no_dot_exit ; Control char or space: done
8571 AND #&0f ; Mask ASCII digit to 0-9
8573 STA fs_load_addr_3 ; Save new digit
8575 ASL fs_load_addr_2 ; Running total * 2
8577 LDA fs_load_addr_2 ; A = running total * 2
8579 ASL ; A = running total * 4
857A ASL ; A = running total * 8
857B ADC fs_load_addr_2 ; + total*2 = total * 10
857D ADC fs_load_addr_3 ; + digit = total*10 + digit
857F STA fs_load_addr_2 ; Store new running total
8581 INY ; Advance to next char
8582 BNE scan_decimal_digit ; Loop (always: Y won't wrap to 0)
8584 .no_dot_exit←2← 8569 BCS← 856F BMI
CLC ; No dot found: C=0
8585 .parse_decimal_rts←1← 856D BEQ
LDA fs_load_addr_2 ; Return result in A
8587 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
8588 .handle_to_mask_a←3← 83ED JSR← 8A05 JSR← 8ECC 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
8589 .handle_to_mask_clc←3← 83AC JSR← 8822 JSR← 88EC 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
858A .handle_to_mask←1← 894D JSR
PHA ; Save A (will be restored on exit)
858B TXA ; Save X (will be restored on exit)
858C PHA ; (second half of X save)
858D TYA ; A = handle from Y
858E BCC y2fsl5 ; C=0: always convert
8590 BEQ handle_mask_exit ; C=1 and Y=0: skip (handle 0 = none)
8592 .y2fsl5←1← 858E BCC
SEC ; C=1 and Y!=0: convert
8593 SBC #&1f ; A = handle - &1F (1-based bit position)
8595 TAX ; X = shift count
8596 LDA #1 ; Start with bit 0 set
8598 .y2fsl2←1← 859A BNE
ASL ; Shift bit left
8599 DEX ; Count down
859A BNE y2fsl2 ; Loop until correct position
859C ROR ; Undo final extra shift
859D TAY ; Y = resulting bitmask
859E BNE handle_mask_exit ; Non-zero: valid mask, skip to exit
85A0 DEY ; Zero: invalid handle, set Y=&FF
85A1 .handle_mask_exit←2← 8590 BEQ← 859E BNE
PLA ; Restore X
85A2 TAX ; Restore X from stack
85A3 PLA ; Restore A
85A4 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
85A5 .mask_to_handle←2← 8980 JSR← 8EE6 JSR
LDX #0 ; X = 0 (bit position counter)
85A7 .fs2al1←1← 85A9 BNE
INX ; Count this bit position
85A8 LSR ; Shift mask right; C=0 when done
85A9 BNE fs2al1 ; Loop until all bits shifted out
85AB TXA ; A = bit position (1-based)
85AC ADC #&1e ; Add &1E+C(=0) = &1E; handle=&1F+pos
85AE 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)
85AF .print_decimal←2← 81FB JSR← 8C27 JSR
TAY ; Y=dividend, A=100: hundreds digit
85B0 LDA #&64 ; A=100: hundreds divisor
85B2 JSR print_decimal_digit ; Print hundreds digit
85B5 LDA #&0a ; A=10: tens divisor
85B7 JSR print_decimal_digit ; Print tens digit
85BA 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
85BC .print_decimal_digit←2← 85B2 JSR← 85B7 JSR
STA fs_error_ptr ; Store divisor in temporary
85BE TYA ; Transfer dividend (Y) to A
85BF LDX #&2f ; X=&2F: ASCII '0'-1 (loop init)
85C1 SEC ; Set carry for subtraction
85C2 .decimal_divide_loop←1← 85C5 BCS
INX ; Increment digit (ASCII '0'..'9')
85C3 SBC fs_error_ptr ; Subtract divisor from remainder
85C5 BCS decimal_divide_loop ; Carry set: subtract again
85C7 ADC fs_error_ptr ; Add back divisor (undo last SBC)
85C9 TAY ; Remainder to Y for next digit
85CA TXA ; Quotient digit (X) to A for print
85CB .print_via_osasci←1← 85FE 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
85CE .compare_addresses←2← 8716 JSR← 87C9 JSR
LDX #4 ; Compare 4 bytes (index 4,3,2,1)
85D0 .compare_addr_byte←1← 85D7 BNE
LDA addr_work,x ; Load byte from first address
85D2 EOR fs_load_addr_3,x ; XOR with corresponding byte
85D4 BNE return_compare ; Mismatch: Z=0, return unequal
85D6 DEX ; Next byte
85D7 BNE compare_addr_byte ; Continue comparing
85D9 .return_compare←1← 85D4 BNE
RTS ; Return with Z flag result
85DA .fscv_7_read_handles
LDX #&20 ; X=first handle (&20)
85DC LDY #&27 ; Y=last handle (&27)
85DE .return_fscv_handles←1← 8603 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
85DF .clear_fs_flag←3← 8496 JSR← 883D JSR← 8A81 JSR
EOR #&ff ; Invert A (NOT mask)
85E1 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
85E4 .set_fs_flag←5← 849D JSR← 8929 JSR← 8975 JSR← 899C JSR← 8A84 JSR
ORA fs_eof_flags ; OR mask into EOF flags
85E7 STA fs_eof_flags ; Store updated EOF flags
85EA 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)
85EB .print_hex←2← 8639 JSR← 8C6B JSR
PHA ; Save full byte on stack
85EC LSR ; Shift high nibble to low position
85ED LSR ; Continue shift (4 LSRs total)
85EE LSR ; Continue shift
85EF LSR ; High nibble now in bits 0-3
85F0 JSR print_hex_nibble ; Print high nibble as hex
85F3 PLA ; Restore original byte
85F4 AND #&0f ; Mask to low nibble
85F6 .print_hex_nibble←1← 85F0 JSR
ORA #&30 ; Convert to ASCII digit ('0'-'9')
85F8 CMP #&3a ; Compare against ':' (past '9'?)
85FA BCC print_hex_digit ; Digit 0-9: skip A-F adjustment
85FC ADC #6 ; Add 7 to get ASCII 'A'-'F'
85FE .print_hex_digit←2← 85FA BCC← 8642 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).

8600 .print_file_info←2← 8703 JSR← 8783 JSR
LDY fs_messages_flag ; Check if file info available
8603 BEQ return_fscv_handles ; No info available: return
8605 LDY #0 ; Y=0: start of filename string
8607 .print_filename_loop←1← 8615 BNE
LDA (fs_crc_lo),y ; Load filename character
8609 CMP #&0d ; Compare with CR (end of filename)
860B BEQ pad_filename_spaces ; CR: pad rest of filename field
860D CMP #&20 ; Also end name on space character
860F BEQ pad_filename_spaces ; Space: also ends filename
8611 JSR osasci ; Write character
8614 INY ; Advance to next filename char
8615 BNE print_filename_loop ; Loop until all chars printed
8617 .pad_filename_spaces←3← 860B BEQ← 860F BEQ← 861D BCC
JSR print_space ; Print padding space
861A INY ; Advance padding counter
861B CPY #&0c ; Pad to 12 chars wide
861D BCC pad_filename_spaces ; Continue padding if < 12 chars
861F LDY #5 ; Y=5: high byte of load address
8621 JSR print_hex_bytes ; Print load address as 2 hex bytes
8624 JSR print_exec_and_len ; Print exec address and length
8627 JMP osnewl ; Write newline (characters 10 and 13)
862A .print_exec_and_len←1← 8624 JSR
LDY #9 ; Y=9: exec address offset
862C JSR print_hex_bytes ; Print exec address bytes
862F LDY #&0c ; Y=&0C: file length offset
8631 LDX #3 ; X=3: print 3 bytes for file length
8633 BNE num01 ; ALWAYS branch
8635 .print_hex_bytes←2← 8621 JSR← 862C JSR
LDX #4 ; X=4: print 4 bytes for address
8637 .num01←2← 8633 BNE← 863E BNE
LDA (fs_options),y ; Load address/length byte
8639 JSR print_hex ; Print as 2 hex digits
863C DEY ; Move to next lower address byte
863D DEX ; Decrement byte counter
863E BNE num01 ; Loop for remaining hex bytes
8640 .print_space←2← 8617 JSR← 8D5C JSR
LDA #&20 ; A=space: separator character
8642 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.

8644 .setup_tx_ptr_c0←2← 837C JSR← 8809 JSR
LDX #&c0 ; X=&C0: TX control block at &00C0
8646 STX net_tx_ptr ; Set TX pointer lo
8648 LDX #0 ; X=0: page zero
864A STX net_tx_ptr_hi ; Set TX pointer hi
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.

864C .tx_poll_ff←3← 9001 JSR← 905B JSR← 925B JSR
LDA #&ff ; A=&FF: full retry count
864E .tx_poll_timeout←1← 8FA4 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 (&8644) always uses the standard TXCB; tx_poll_core (&8650) is general-purpose.

On EntryAretry count (&FF = full retry)
Ytimeout parameter (&60 = standard)
On ExitAentry A (retry count, restored from stack)
X0
Y0
8650 .tx_poll_core←1← 8FE6 JSR
PHA ; Save retry count on stack
8651 TYA ; Transfer timeout to A
8652 PHA ; Save timeout on stack
8653 LDX #0 ; X=0 for (net_tx_ptr,X) indirect
8655 LDA (net_tx_ptr,x) ; Load TXCB byte 0 (control/status)
8657 .tx_retry←1← 868A BEQ
STA (net_tx_ptr,x) ; Write control byte to start TX
8659 PHA ; Save control byte for retry
865A .tx_semaphore_spin←1← 865D BCC
ASL tx_ctrl_status ; Test TX semaphore (C=1 when free)
865D BCC tx_semaphore_spin ; Spin until semaphore released
865F LDA net_tx_ptr ; Copy TX ptr lo to NMI block
8661 STA nmi_tx_block ; Store for NMI handler access
8663 LDA net_tx_ptr_hi ; Copy TX ptr hi to NMI block
8665 STA nmi_tx_block_hi ; Store for NMI handler access
8667 JSR trampoline_tx_setup ; Initiate ADLC TX via trampoline
866A .poll_txcb_status←1← 866C BMI
LDA (net_tx_ptr,x) ; Poll TXCB byte 0 for completion
866C BMI poll_txcb_status ; Bit 7 set: still busy, keep polling
866E ASL ; Shift bit 6 into bit 7 (error flag)
866F BPL tx_success ; Bit 6 clear: success, clean return
8671 ASL ; Shift bit 5 into carry
8672 BEQ tx_not_listening ; Zero: fatal error, no escape
8674 JSR check_escape ; Check for user escape condition
8677 PLA ; Discard saved control byte
8678 TAX ; Save to X for retry delay
8679 PLA ; Restore timeout parameter
867A TAY ; Back to Y
867B PLA ; Restore retry count
867C BEQ tx_not_listening ; No retries left: report error
867E SBC #1 ; Decrement retry count
8680 PHA ; Save updated retry count
8681 TYA ; Timeout to A for delay
8682 PHA ; Save timeout parameter
8683 TXA ; Control byte for delay duration
8684 .delay_1ms←2← 8685 BNE← 8688 BNE
DEX ; Inner delay loop
8685 BNE delay_1ms ; Spin until X=0
8687 DEY ; Outer delay loop
8688 BNE delay_1ms ; Continue delay
868A BEQ tx_retry ; ALWAYS branch
868C .tx_not_listening←2← 8672 BEQ← 867C BEQ
TAX ; Save error code in X
868D JMP nlistn ; Report 'Not listening' error
8690 .tx_success←1← 866F BPL
PLA ; Discard saved control byte
8691 PLA ; Discard timeout parameter
8692 PLA ; Discard retry count
8693 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 &86D0) A=&00: save file (filev_save at &8746) A=&01-&06: attribute operations (filev_attrib_dispatch at &8844) 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
8694 .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.

8697 .copy_filename_ptr
LDY #1 ; Y=1: copy 2 bytes (high then low)
8699 .file1←1← 869F BPL
LDA (fs_options),y ; Load filename ptr from control block
869B STA os_text_ptr,y ; Store to MOS text pointer (&F2/&F3)
869E DEY ; Next byte (descending)
869F BPL file1 ; Loop for both bytes
86A1 INY ; Y=1: offset past filename pointer
86A2 LDX #&ff ; X=&FF: parse all characters
86A4 CLC ; C=0: normal string parse entry
86A5 JSR gsinit ; Init string parsing via GSINIT
86A8 .quote1←1← 86B1 BCC
JSR gsread ; Read next character via GSREAD
86AB BCS tx_result_check ; C=1 from GSREAD: end of string reached
86AD INX ; Advance buffer index
86AE STA l0fc5,x ; Store parsed character to &0E30+X
86B1 BCC quote1 ; ALWAYS loop (GSREAD clears C on success) ALWAYS branch
86B3 .tx_result_check←1← 86AB BCS
LDA #&0d ; CR = &0D
86B5 STA l0fc6,x ; Store CR terminator at end of string
86B8 LDA #&c5 ; Point fs_crc_lo/hi at &0E30 parse buffer
86BA STA fs_crc_lo ; fs_crc_lo = &30
86BC LDA #&0f ; fs_crc_hi = &0E → buffer at &0E30
86BE STA fs_crc_hi ; Store high byte
86C0 LDA fs_last_byte_flag ; Recover function code from saved A
86C2 BPL saveop ; A >= 0: save (&00) or attribs (&01-&06)
86C4 CMP #&ff ; A=&FF? Only &FF is valid for load
86C6 BEQ loadop ; A=&FF: branch to load path
86C8 JMP restore_args_return ; Unknown negative code: no-op return
86CB .loadop←1← 86C6 BEQ
JSR copy_filename ; Copy parsed filename to cmd buffer
86CE 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
86D0 .send_fs_examine←1← 8D90 JSR
LDA #&92 ; Port &92 = PLDATA (data transfer port)
86D2 STA fs_cmd_urd ; Overwrite URD field with data port number
86D5 LDA #&2a ; A=&2A: error ptr for retry
86D7 JSR prepare_cmd_clv ; Build FS header (V=1: CLV path)
86DA LDY #6 ; Y=6: param block byte 6
86DC LDA (fs_options),y ; Byte 6: use file's own load address?
86DE BNE lodfil ; Non-zero: use FS reply address (lodfil)
86E0 JSR copy_load_addr_from_params ; Zero: copy caller's load addr first
86E3 JSR copy_reply_to_params ; Then copy FS reply to param block
86E6 BCC skip_lodfil ; Carry clear from prepare_cmd_clv: skip lodfil
86E8 .lodfil←1← 86DE BNE
JSR copy_reply_to_params ; Copy FS reply addresses to param block
86EB JSR copy_load_addr_from_params ; Then copy load addr from param block
86EE .skip_lodfil←1← 86E6 BCC
LDY #4 ; Compute end address = load + file length
86F0 .copy_load_end_addr←1← 86FB BNE
LDA fs_load_addr,x ; Load address byte
86F2 STA txcb_end,x ; Store as current transfer position
86F4 ADC fs_file_len,x ; Add file length byte
86F7 STA fs_work_4,x ; Store as end position
86F9 INX ; Next address byte
86FA DEY ; Decrement byte counter
86FB BNE copy_load_end_addr ; Loop for all 4 address bytes
86FD SEC ; Adjust high byte for 3-byte length overflow
86FE SBC fs_file_len_3 ; Subtract 4th length byte from end addr
8701 STA fs_work_7 ; Store adjusted end address high byte
8703 JSR print_file_info ; Display file info after FS reply
8706 JSR send_data_blocks ; Transfer file data in &80-byte blocks
8709 LDX #2 ; Copy 3-byte file length to FS reply cmd buffer
870B .floop←1← 8712 BPL
LDA fs_file_len_3,x ; Load file length byte
870E STA fs_cmd_data,x ; Store in FS command data buffer
8711 DEX ; Next byte (count down)
8712 BPL floop ; Loop for 3 bytes (X=2,1,0)
8714 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.

8716 .send_data_blocks←2← 8706 JSR← 8A70 JSR
JSR compare_addresses ; Compare two 4-byte addresses
8719 BEQ return_lodchk ; Addresses match: transfer complete
871B LDA #&92 ; Port &92 for data block transfer
871D STA txcb_port ; Store port to TXCB command byte
871F .send_block_loop←1← 873B BNE
LDX #3 ; Set up next &80-byte block for transfer
8721 .copy_block_addrs←1← 872A BPL
LDA txcb_end,x ; Swap: current addr -> source, end -> current
8723 STA txcb_start,x ; Source addr = current position
8725 LDA fs_work_4,x ; Load end address byte
8727 STA txcb_end,x ; Dest = end address (will be clamped)
8729 DEX ; Next address byte
872A BPL copy_block_addrs ; Loop for all 4 bytes
872C LDA #&7f ; Command &7F = data block transfer
872E STA txcb_ctrl ; Store to TXCB control byte
8730 JSR send_to_fs_star ; Send this block to the fileserver
8733 LDY #3 ; Y=3: compare 4 bytes (3..0)
8735 .lodchk←1← 873E BPL
LDA txcb_end,y ; Compare current vs end address (4 bytes)
8738 EOR fs_work_4,y ; XOR with end address byte
873B BNE send_block_loop ; Not equal: more blocks to send
873D DEY ; Next byte
873E BPL lodchk ; Loop for all 4 address bytes
8740 .return_lodchk←1← 8719 BEQ
RTS ; All equal: transfer complete
8741 .saveop←1← 86C2 BPL
BEQ filev_save ; A=0: SAVE handler
8743 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.

8746 .filev_save←1← 8741 BEQ
LDX #4 ; Process 4 address bytes (load/exec/start/end)
8748 LDY #&0e ; Y=&0E: start from end-address in param block
874A .savsiz←1← 8764 BNE
LDA (fs_options),y ; Read end-address byte from param block
874C STA port_ws_offset,y ; Save to port workspace for transfer setup
874F JSR sub_4_from_y ; Y = Y-4: point to start-address byte
8752 SBC (fs_options),y ; end - start = transfer length byte
8754 STA fs_cmd_csd,y ; Store length byte in FS command buffer
8757 PHA ; Save length byte for param block restore
8758 LDA (fs_options),y ; Read corresponding start-address byte
875A STA port_ws_offset,y ; Save to port workspace
875D PLA ; Restore length byte from stack
875E STA (fs_options),y ; Replace param block entry with length
8760 JSR add_5_to_y ; Y = Y+5: advance to next address group
8763 DEX ; Decrement address byte counter
8764 BNE savsiz ; Loop for all 4 address bytes
8766 LDY #9 ; Copy load/exec addresses to FS command buffer
8768 .copy_save_params←1← 876E BNE
LDA (fs_options),y ; Read load/exec address byte from params
876A STA fs_cmd_csd,y ; Copy to FS command buffer
876D DEY ; Next byte (descending)
876E BNE copy_save_params ; Loop for bytes 9..1
8770 LDA #&91 ; Port &91 for save command
8772 STA fs_cmd_urd ; Overwrite URD field with port number
8775 STA fs_error_ptr ; Save port &91 for flow control ACK
8777 LDX #&0b ; Append filename at offset &0B in cmd buffer
8779 JSR copy_string_to_cmd ; Append filename to cmd buffer at offset X
877C LDY #1 ; Y=1: function code for save
877E LDA #&14 ; A=&14: FS function code for SAVE
8780 JSR prepare_cmd_clv ; Build header and send FS save command
8783 JSR print_file_info ; Send file data blocks to server
8786 .save_csd_display
LDA fs_cmd_data ; Save CSD from reply for catalogue display
8789 JSR transfer_file_blocks ; Print file length in hex
878C .set_star_reply_port←1← 8714 BMI
LDA #&2a ; A=&2A: error ptr for FS retry
878E STA fs_error_ptr ; Store error ptr for TX poll
8790 .send_fs_reply
JSR send_fs_reply_cmd ; Send FS reply acknowledgement
8793 .skip_catalogue_msg
STX fs_reply_cmd ; Store reply command for attr decode
8796 LDY #&0e ; Y=&0E: access byte offset in param block
8798 LDA fs_cmd_data ; Load access byte from FS reply
879B JSR decode_attribs_5bit ; Convert FS access to BBC attribute format
879E BEQ direct_attr_copy ; Z=1: first byte, use A directly
87A0 .copy_attr_loop←1← 87A8 BNE
LDA fs_reply_data,y ; Load attribute byte from FS reply
87A3 .direct_attr_copy←1← 879E BEQ
STA (fs_options),y ; Store decoded access in param block
87A5 INY ; Next attribute byte
87A6 CPY #&12 ; Copied all 4 bytes? (Y=&0E..&11)
87A8 BNE copy_attr_loop ; Loop for 4 attribute bytes
87AA 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).

87AD .copy_load_addr_from_params←2← 86E0 JSR← 86EB JSR
LDY #5 ; Start at offset 5 (top of 4-byte addr)
87AF .lodrl1←1← 87B7 BCS
LDA (fs_options),y ; Read from parameter block
87B1 STA work_ae,y ; Store to local workspace
87B4 DEY ; Next byte (descending)
87B5 CPY #2 ; Copy offsets 5,4,3,2 (4 bytes)
87B7 BCS lodrl1 ; Loop while Y >= 2
87B9 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)
87BA .copy_reply_to_params←2← 86E3 JSR← 86E8 JSR
LDY #&0d ; Start at offset &0D (top of range)
87BC TXA ; First store uses X (attrib byte)
87BD .lodrl2←1← 87C5 BCS
STA (fs_options),y ; Write to parameter block
87BF LDA fs_cmd_urd,y ; Read next byte from reply buffer
87C2 DEY ; Next byte (descending)
87C3 CPY #2 ; Copy offsets &0D down to 2
87C5 BCS lodrl2 ; Loop until offset 2 reached
87C7 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).

87C8 .transfer_file_blocks←2← 8789 JSR← 8A6B JSR
PHA ; Save FS command byte on stack
87C9 JSR compare_addresses ; Compare two 4-byte addresses
87CC BEQ restore_ay_return ; Addresses equal: nothing to transfer
87CE .next_block←1← 881D BNE
LDX #0 ; X=0: clear hi bytes of block size
87D0 LDY #4 ; Y=4: process 4 address bytes
87D2 STX fs_reply_cmd ; Clear block size hi byte 1
87D5 STX fs_load_vector ; Clear block size hi byte 2
87D8 CLC ; CLC for ADC in loop
87D9 .block_addr_loop←1← 87E6 BNE
LDA fs_load_addr,x ; Source = current position
87DB STA txcb_start,x ; Store source address byte
87DD ADC fs_func_code,x ; Add block size to current position
87E0 STA txcb_end,x ; Store dest address byte
87E2 STA fs_load_addr,x ; Advance current position
87E4 INX ; Next address byte
87E5 DEY ; Decrement byte counter
87E6 BNE block_addr_loop ; Loop for all 4 bytes
87E8 BCS clamp_dest_setup ; Carry: address overflowed, clamp
87EA SEC ; SEC for SBC in overshoot check
87EB .savchk←1← 87F3 BNE
LDA fs_load_addr,y ; Check if new pos overshot end addr
87EE SBC fs_work_4,y ; Subtract end address byte
87F1 INY ; Next byte
87F2 DEX ; Decrement counter
87F3 BNE savchk ; Loop for 4-byte comparison
87F5 BCC send_block ; C=0: no overshoot, proceed
87F7 .clamp_dest_setup←1← 87E8 BCS
LDX #3 ; Overshot: clamp dest to end address
87F9 .clamp_dest_addr←1← 87FE BPL
LDA fs_work_4,x ; Load end address byte
87FB STA txcb_end,x ; Replace dest with end address
87FD DEX ; Next byte
87FE BPL clamp_dest_addr ; Loop for all 4 bytes
8800 .send_block←1← 87F5 BCC
PLA ; Recover original FS command byte
8801 PHA ; Re-push for next iteration
8802 PHP ; Save processor flags (C from cmp)
8803 STA txcb_port ; Store command byte in TXCB
8805 LDA #&80 ; 128-byte block size for data transfer
8807 STA txcb_ctrl ; Store size in TXCB control byte
8809 JSR setup_tx_ptr_c0 ; Point TX ptr to &00C0; transmit
880C LDA fs_error_ptr ; ACK port for flow control
880E JSR init_tx_ctrl_port ; Set reply port for ACK receive
8811 PLP ; Restore flags (C=overshoot status)
8812 BCS restore_ay_return ; C=1: all data sent (overshot), done
8814 LDA #&91 ; Command &91 = data block transfer
8816 STA txcb_port ; Store command &91 in TXCB
8818 LDA #&2a ; A=&2A: error ptr for retry
881A JSR send_to_fs ; Transmit block and wait (BRIANX)
881D 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
881F .fscv_1_eof
PHA ; Save A (function code)
8820 STY fs_block_offset ; Save handle for EOF check
8822 JSR handle_to_mask_clc ; Convert handle to bitmask in A
8825 TYA ; Y = handle bitmask from conversion
8826 AND fs_eof_flags ; Local hint: is EOF possible for this handle?
8829 TAX ; X = result of AND (0 = not at EOF)
882A BEQ restore_ay_return ; Hint clear: definitely not at EOF
882C PHA ; Save bitmask for clear_fs_flag
882D STY fs_cmd_data ; Handle byte in FS command buffer
8830 LDY #&11 ; Y=&11: FS function code FCEOF Y=function code for HDRFN
8832 LDX #1 ; X=preserved through header build
8834 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8837 PLA ; Restore bitmask
8838 LDX fs_cmd_data ; FS reply: non-zero = at EOF
883B BNE restore_ay_return ; At EOF: skip flag clear
883D JSR clear_fs_flag ; Not at EOF: clear the hint bit
8840 .restore_ay_return←4← 87CC BEQ← 8812 BCS← 882A BEQ← 883B BNE
PLA ; Restore A
8841 LDY fs_block_offset ; Restore Y
8843 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
8844 .filev_attrib_dispatch←1← 8743 JMP
STA fs_cmd_data ; Store function code in FS cmd buffer
8847 CMP #6 ; A=6? (delete)
8849 BEQ cha6 ; Yes: jump to delete handler
884B BCS check_attrib_result ; A>=7: unsupported, fall through to return
884D CMP #5 ; A=5? (read catalogue info)
884F BEQ cha5 ; Yes: jump to read info handler
8851 CMP #4 ; A=4? (write attributes only)
8853 BEQ cha4 ; Yes: jump to write attrs handler
8855 CMP #1 ; A=1? (write all catalogue info)
8857 BEQ get_file_protection ; Yes: jump to write-all handler
8859 ASL ; A=2 or 3: convert to param block offset
885A ASL ; A*4: 2->8, 3->12
885B TAY ; Y = A*4
885C JSR sub_3_from_y ; Y = A*4 - 3 (load addr offset for A=2)
885F LDX #3 ; X=3: copy 4 bytes
8861 .chalp1←1← 8868 BPL
LDA (fs_options),y ; Load address byte from param block
8863 STA fs_func_code,x ; Store to FS cmd data area
8866 DEY ; Next source byte (descending)
8867 DEX ; Next dest byte
8868 BPL chalp1 ; Loop for 4 bytes
886A LDX #5 ; X=5: data extent for filename copy
886C BNE copy_filename_to_cmd ; ALWAYS branch
886E .get_file_protection←1← 8857 BEQ
JSR decode_attribs_6bit ; A=1: encode protection from param block
8871 STA fs_file_attrs ; Store encoded attrs at &0F0E
8874 LDY #9 ; Y=9: source offset in param block
8876 LDX #8 ; X=8: dest offset in cmd buffer
8878 .chalp2←1← 887F BNE
LDA (fs_options),y ; Load byte from param block
887A STA fs_cmd_data,x ; Store to FS cmd buffer
887D DEY ; Next source byte (descending)
887E DEX ; Next dest byte
887F BNE chalp2 ; Loop until X=0 (8 bytes copied)
8881 LDX #&0a ; X=&0A: data extent past attrs+addrs
8883 .copy_filename_to_cmd←2← 886C BNE← 88A1 BNE
JSR copy_string_to_cmd ; Append filename to cmd buffer
8886 LDY #&13 ; Y=&13: fn code for FCSAVE (write attrs)
8888 BNE send_fs_cmd_v1 ; ALWAYS branch to send command ALWAYS branch
888A .cha6←1← 8849 BEQ
JSR copy_filename ; A=6: copy filename (delete)
888D LDY #&14 ; Y=&14: fn code for FCDEL (delete)
888F .send_fs_cmd_v1←1← 8888 BNE
BIT tx_ctrl_upper ; Set V=1 (BIT trick: &B3 has bit 6 set)
8892 JSR prepare_fs_cmd_v ; Send via prepare_fs_cmd_v (V=1 path)
8895 .check_attrib_result←1← 884B BCS
BCS attrib_error_exit ; C=1: &D6 not-found, skip to return
8897 BCC argsv_check_return ; C=0: success, copy reply to param block ALWAYS branch
8899 .cha4←1← 8853 BEQ
JSR decode_attribs_6bit ; A=4: encode attrs from param block
889C STA fs_func_code ; Store encoded attrs at &0F06
889F LDX #2 ; X=2: data extent (1 attr byte + fn)
88A1 BNE copy_filename_to_cmd ; ALWAYS branch to append filename ALWAYS branch
88A3 .cha5←1← 884F BEQ
LDX #1 ; X=1: filename only, no data extent
88A5 JSR copy_string_to_cmd ; Copy filename to cmd buffer
88A8 LDY #&12 ; Y=&12: fn code for FCEXAM (read info) Y=function code for HDRFN
88AA JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
88AD LDA fs_obj_type ; Save object type from FS reply
88B0 STX fs_obj_type ; Clear reply byte (X=0 on success) X=0 on success, &D6 on not-found
88B3 STX fs_len_clear ; Clear length high byte in reply
88B6 JSR decode_attribs_5bit ; Decode 5-bit access byte from FS reply
88B9 LDX fs_cmd_data ; Load FS command code from reply
88BC BEQ argsv_zero_length ; Zero: no attribute data returned
88BE LDY #&0e ; Y=&0E: attrs offset in param block
88C0 STA (fs_options),y ; Store decoded attrs at param block +&0E
88C2 DEY ; Y=&0D: start copy below attrs Y=&0d
88C3 LDX #&0c ; X=&0C: copy from reply offset &0C down
88C5 .copy_fs_reply_to_cb←1← 88CC BNE
LDA fs_cmd_data,x ; Load reply byte (load/exec/length)
88C8 STA (fs_options),y ; Store to param block
88CA DEY ; Next dest byte (descending)
88CB DEX ; Next source byte
88CC BNE copy_fs_reply_to_cb ; Loop until X=0 (12 bytes copied)
88CE INX ; X=0 -> X=2 for length high copy
88CF INX ; INX again: X=2
88D0 LDY #&11 ; Y=&11: length high dest in param block
88D2 .cha5lp←1← 88D9 BPL
LDA fs_access_level,x ; Load length high byte from reply
88D5 STA (fs_options),y ; Store to param block
88D7 DEY ; Next dest byte (descending)
88D8 DEX ; Next source byte
88D9 BPL cha5lp ; Loop for 3 length-high bytes
88DB LDX fs_cmd_data ; Reload FS command code
88DE .argsv_zero_length←1← 88BC BEQ
TXA ; A = command code for exit test
88DF .attrib_error_exit←1← 8895 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
88E1 .argsv_handler
JSR save_fscv_args ; Save A/X/Y registers for later restore
88E4 CMP #3 ; Function >= 3?
88E6 BCS restore_args_return ; A>=3 (ensure/flush): no-op for NFS
88E8 CPY #0 ; Test file handle
88EA BEQ argsv_dispatch_a ; Y=0: FS-level query, not per-file
88EC JSR handle_to_mask_clc ; Convert handle to bitmask
88EF STY fs_cmd_data ; Store bitmask as first cmd data byte
88F2 LSR ; LSR splits A: C=1 means write (A=1)
88F3 STA fs_func_code ; Store function code to cmd data byte 2
88F6 BCS save_args_handle ; C=1: write path, copy ptr from caller
88F8 LDY #&0c ; Y=&0C: FCRDSE (read sequential pointer) Y=function code for HDRFN
88FA LDX #2 ; X=2: 3 data bytes in command X=preserved through header build
88FC JSR prepare_fs_cmd ; Build and send FS command Prepare FS command buffer (12 references)
88FF STA fs_last_byte_flag ; Clear last-byte flag on success A=0 on success (from build_send_fs_cmd)
8901 LDX fs_options ; X = saved control block ptr low
8903 LDY #2 ; Y=2: copy 3 bytes of file pointer
8905 STA zp_work_3,x ; Zero high byte of 3-byte pointer
8907 .copy_fileptr_reply←1← 890E BPL
LDA fs_cmd_data,y ; Read reply byte from FS cmd data
890A STA zp_work_2,x ; Store to caller's control block
890C DEX ; Next byte (descending)
890D DEY ; Next source byte
890E BPL copy_fileptr_reply ; Loop for all 3 bytes
8910 .argsv_check_return←1← 8897 BCC
BCC restore_args_return ; C=0 (read): return to caller
8912 .save_args_handle←1← 88F6 BCS
TYA ; Save bitmask for set_fs_flag later
8913 PHA ; Push bitmask
8914 LDY #3 ; Y=3: copy 4 bytes of file pointer
8916 .copy_fileptr_to_cmd←1← 891D BPL
LDA zp_work_3,x ; Read caller's pointer byte
8918 STA fs_data_count,y ; Store to FS command data area
891B DEX ; Next source byte
891C DEY ; Next destination byte
891D BPL copy_fileptr_to_cmd ; Loop for all 4 bytes
891F LDY #&0d ; Y=&0D: FCWRSE (write sequential pointer) Y=function code for HDRFN
8921 LDX #5 ; X=5: 6 data bytes in command X=preserved through header build
8923 JSR prepare_fs_cmd ; Build and send FS command Prepare FS command buffer (12 references)
8926 STX fs_last_byte_flag ; Save not-found status from X X=0 on success, &D6 on not-found
8928 PLA ; Recover bitmask for EOF hint update
8929 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.

892C .restore_args_return←8← 86C8 JMP← 87AA JMP← 88E6 BCS← 8910 BCC← 893B BNE← 899F BCC← 89F5 JMP← 8CFF JMP
LDA fs_last_byte_flag ; A = saved function code / command
892E .restore_xy_return←5← 88DF BPL← 8938 BNE← 8947 BPL← 896F BCS← 8983 BNE
LDX fs_options ; X = saved control block ptr low
8930 LDY fs_block_offset ; Y = saved control block ptr high
8932 RTS ; Return to MOS with registers restored
8933 .argsv_dispatch_a←1← 88EA BEQ
TAY ; Transfer A to Y for test
8934 BNE halve_args_a ; Non-zero: halve A
8936 LDA #5 ; A=5: default FS number
8938 BNE restore_xy_return ; ALWAYS branch
893A .halve_args_a←1← 8934 BNE
LSR ; Shared: halve A (A=0 or A=2 paths)
893B BNE restore_args_return ; Return with A = FS number or 1
893D .osarg1←1← 8943 BPL
LDA fs_context_hi,y ; Copy command context to caller's block
8940 STA (fs_options),y ; Store to caller's parameter block
8942 DEY ; Next byte (descending)
8943 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.

8945 .return_a_zero←3← 8955 BNE← 8A95 JMP← 8B32 JMP
LDA #0 ; A=operation (0=close, &40=read, &80=write, &C0=R/W)
8947 BPL restore_xy_return ; ALWAYS branch

FINDV handler (OSFIND entry point)

A=0: close file -- delegates to close_handle (&8985) 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
8949 .findv_handler
JSR save_fscv_args ; Save A/X/Y and set up pointers
894C SEC ; SEC distinguishes open (A>0) from close
894D JSR handle_to_mask ; Convert file handle to bitmask (Y2FS)
8950 TAX ; A=preserved
8951 BEQ close_handle ; A=0: close file(s)
8953 AND #&3f ; Valid open modes: &40, &80, &C0 only
8955 BNE return_a_zero ; Invalid mode bits: return
8957 TXA ; A = original mode byte
8958 EOR #&80 ; Convert MOS mode to FS protocol flags
895A ASL ; ASL: shift mode bits left
895B STA fs_cmd_data ; Flag 1: read/write direction
895E ROL ; ROL: Flag 2 into bit 0
895F STA fs_func_code ; Flag 2: create vs existing file
8962 LDX #2 ; X=2: copy after 2-byte flags
8964 JSR copy_string_to_cmd ; Copy filename to FS command buffer
8967 LDY #6 ; Y=6: FS function code FCOPEN
8969 BIT tx_ctrl_upper ; Set V flag from l83b3 bit 6
896C JSR prepare_fs_cmd_v ; Build and send FS open command
896F BCS restore_xy_return ; Error: restore and return
8971 LDA fs_cmd_data ; Load reply handle from FS
8974 TAX ; X = new file handle
8975 JSR set_fs_flag ; Set EOF hint + sequence bits
8978 TXA ; A = handle bitmask from set_fs_flag
8979 ORA fs_sequence_nos ; Merge handle into sequence tracking
897C STA fs_sequence_nos ; Store updated sequence tracking
897F TXA ; A=single-bit bitmask
8980 JSR mask_to_handle ; Convert bitmask to handle number (FS2A)
8983 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)
8985 .close_handle←1← 8951 BEQ
TYA ; A = handle (Y preserved in A) Y=preserved
8986 BNE close_single_handle ; Y>0: close single file
8988 LDA #osbyte_close_spool_exec ; Close SPOOL/EXEC before FS close-all
898A JSR osbyte ; Close any *SPOOL and *EXEC files
898D LDY #0 ; Y=0: close all handles on server
898F .close_single_handle←1← 8986 BNE
STY fs_cmd_data ; Handle byte in FS command buffer
8992 LDX #1 ; X=preserved through header build
8994 LDY #7 ; Y=function code for HDRFN
8996 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8999 LDA fs_cmd_data ; Reply handle for flag update
899C JSR set_fs_flag ; Update EOF/sequence tracking bits
899F .close_opt_return←1← 89C8 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
89A1 .fscv_0_opt
CPX #4 ; Is it *OPT 4,Y?
89A3 BNE gbpbv_func_dispatch ; No: check for *OPT 1
89A5 CPY #4 ; Y must be 0-3 for boot option
89A7 BCC optl1 ; Y < 4: valid boot option
89A9 .gbpbv_func_dispatch←1← 89A3 BNE
CPX #1 ; X=1? (*OPT 1: set messaging)
89AB BNE opter1 ; Not *OPT 1: bad option error
89AD CPY #2 ; Y < 2? (valid: 0=off, 1=on)
89AF BCS opter1 ; Y >= 2: bad option value error
89B1 .set_messages_flag
STY fs_messages_flag ; Set local messages flag (*OPT 1,Y)
89B4 BCC opt_return ; Return via restore_args_return
89B6 .opter1←2← 89AB BNE← 89AF BCS
LDA #7 ; Error index 7 (Bad option)
89B8 JMP nlisne ; Generate BRK error
89BB .optl1←1← 89A7 BCC
STY fs_cmd_data ; Boot option value in FS command
89BE LDY #&16 ; Y=&16: FS function code FCOPT Y=function code for HDRFN
89C0 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
89C3 LDY fs_block_offset ; Restore Y from saved value
89C5 STY fs_boot_option ; Cache boot option locally
89C8 .opt_return←1← 89B4 BCC
BCC close_opt_return ; Return via restore_args_return
89CA .adjust_addrs_9←1← 8A89 JSR
LDY #9 ; Y=9: adjust 9 address bytes
89CC JSR adjust_addrs_clc ; Adjust with carry clear
89CF .adjust_addrs_1←1← 8B79 JSR
LDY #1 ; Y=1: adjust 1 address byte
89D1 .adjust_addrs_clc←1← 89CC 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
89D2 .adjust_addrs←2← 8A8F JSR← 8B85 JSR
LDX #&fc ; X=&FC: index into &0E06 area (wraps to 0)
89D4 .adjust_addr_byte←1← 89E7 BNE
LDA (fs_options),y ; Load byte from param block
89D6 BIT fs_load_addr_2 ; Test sign of adjustment direction
89D8 BMI subtract_adjust ; Negative: subtract instead
89DA ADC fs_cmd_context,x ; Add adjustment value
89DD JMP gbpbx ; Skip to store result
89E0 .subtract_adjust←1← 89D8 BMI
SBC fs_cmd_context,x ; Subtract adjustment value
89E3 .gbpbx←1← 89DD JMP
STA (fs_options),y ; Store adjusted byte back
89E5 INY ; Next param block byte
89E6 INX ; Next adjustment byte (X wraps &FC->&00)
89E7 BNE adjust_addr_byte ; Loop 4 times (X=&FC,&FD,&FE,&FF,done)
89E9 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
89EA .gbpbv_handler
JSR save_fscv_args ; Save A/X/Y to FS workspace
89ED TAX ; X = call number for range check
89EE BEQ gbpbx0 ; A=0: invalid, restore and return
89F0 DEX ; Convert to 0-based (A=0..7)
89F1 CPX #8 ; Range check: must be 0-7
89F3 BCC gbpbx1 ; In range: continue to handler
89F5 .gbpbx0←1← 89EE BEQ
JMP restore_args_return ; Out of range: restore args and return
89F8 .gbpbx1←1← 89F3 BCC
TXA ; Recover 0-based function code
89F9 LDY #0 ; Y=0: param block byte 0 (file handle)
89FB PHA ; Save function code on stack
89FC CMP #4 ; A>=4: info queries, dispatch separately
89FE BCC gbpbe1 ; A<4: file read/write operations
8A00 JMP osgbpb_info ; Dispatch to OSGBPB 5-8 info handler
8A03 .gbpbe1←1← 89FE BCC
LDA (fs_options),y ; Get file handle from param block byte 0
8A05 JSR handle_to_mask_a ; Convert handle to bitmask for EOF flags
8A08 STY fs_cmd_data ; Store handle in FS command data
8A0B LDY #&0b ; Y=&0B: start at param block byte 11
8A0D LDX #6 ; X=6: copy 6 bytes of transfer params
8A0F .gbpbf1←1← 8A1B BNE
LDA (fs_options),y ; Load param block byte
8A11 STA fs_func_code,x ; Store to FS command buffer at &0F06+X
8A14 DEY ; Previous param block byte
8A15 CPY #8 ; Skip param block offset 8 (the handle)
8A17 BNE gbpbf2 ; Not at handle offset: continue
8A19 DEY ; Extra DEY to skip handle byte
8A1A .gbpbf2←1← 8A17 BNE
DEX ; Decrement copy counter
8A1B BNE gbpbf1 ; Loop for all 6 bytes
8A1D PLA ; Recover function code from stack
8A1E LSR ; LSR: odd=read (C=1), even=write (C=0)
8A1F PHA ; Save function code again (need C later)
8A20 BCC gbpbl1 ; Even (write): X stays 0
8A22 INX ; Odd (read): X=1
8A23 .gbpbl1←1← 8A20 BCC
STX fs_func_code ; Store FS direction flag
8A26 LDY #&0b ; Y=&0B: command data extent
8A28 LDX #&91 ; Command &91=put, &92=get
8A2A PLA ; Recover function code
8A2B PHA ; Save again for later direction check
8A2C BEQ gbpb_write_path ; Even (write): keep &91 and Y=&0B
8A2E LDX #&92 ; Odd (read): use &92 (get) instead
8A30 DEY ; Read: one fewer data byte in command Y=&0a
8A31 .gbpb_write_path←1← 8A2C BEQ
STX fs_cmd_urd ; Store port to FS command URD field
8A34 STX fs_error_ptr ; Save port for error recovery
8A36 LDX #8 ; X=8: command data bytes
8A38 LDA fs_cmd_data ; Load handle from FS command data
8A3B JSR prepare_cmd_with_flag ; Build FS command with handle+flag
8A3E LDA fs_load_addr_3 ; Save seq# for byte-stream flow control
8A40 STA fs_sequence_nos ; Store to FS sequence number workspace
8A43 LDX #4 ; X=4: copy 4 address bytes
8A45 .gbpbl3←1← 8A59 BNE
LDA (fs_options),y ; Set up source/dest from param block
8A47 STA addr_work,y ; Store as source address
8A4A STA txcb_pos,y ; Store as current transfer position
8A4D JSR add_4_to_y ; Skip 4 bytes to reach transfer length
8A50 ADC (fs_options),y ; Dest = source + length
8A52 STA addr_work,y ; Store as end address
8A55 JSR sub_3_from_y ; Back 3 to align for next iteration
8A58 DEX ; Decrement byte counter
8A59 BNE gbpbl3 ; Loop for all 4 address bytes
8A5B INX ; X=1 after loop
8A5C .gbpbf3←1← 8A63 BPL
LDA fs_cmd_csd,x ; Copy CSD data to command buffer
8A5F STA fs_func_code,x ; Store at &0F06+X
8A62 DEX ; Decrement counter
8A63 BPL gbpbf3 ; Loop for X=1,0
8A65 PLA ; Odd (read): send data to FS first
8A66 BNE gbpb_read_path ; Non-zero: skip write path
8A68 LDA fs_cmd_urd ; Load port for transfer setup
8A6B JSR transfer_file_blocks ; Transfer data blocks to fileserver
8A6E BNE findv_eof_check ; Non-zero: branch past error ptr
8A70 .gbpb_read_path←1← 8A66 BNE
JSR send_data_blocks ; Read path: receive data blocks from FS
8A73 .findv_eof_check←1← 8A6E BNE
LDA #&2a ; A=&2A: error ptr for FS retry
8A75 STA fs_error_ptr ; Store error ptr for TX poll
8A77 .wait_fs_reply
JSR send_fs_reply_cmd ; Wait for FS reply command
8A7A LDA (fs_options,x) ; Load handle mask for EOF flag update
8A7C BIT fs_cmd_data ; Check FS reply: bit 7 = not at EOF
8A7F BMI skip_clear_flag ; Bit 7 set: not EOF, skip clear
8A81 JSR clear_fs_flag ; At EOF: clear EOF hint for this handle
8A84 .skip_clear_flag←1← 8A7F BMI
JSR set_fs_flag ; Set EOF hint flag (may be at EOF)
8A87 STX fs_load_addr_2 ; Direction=0: forward adjustment
8A89 JSR adjust_addrs_9 ; Adjust param block addrs by +9 bytes
8A8C DEC fs_load_addr_2 ; Direction=&FF: reverse adjustment
8A8E SEC ; SEC for reverse subtraction
8A8F JSR adjust_addrs ; Adjust param block addrs (reverse)
8A92 ASL fs_cmd_data ; Shift bit 7 into C for return flag
8A95 JMP return_a_zero ; Return via restore_args path
8A98 .get_disc_title←1← 8AC8 BEQ
LDY #&15 ; Y=&15: function code for disc title Y=function code for HDRFN
8A9A JSR prepare_fs_cmd ; Build and send FS command Prepare FS command buffer (12 references)
8A9D LDA fs_boot_option ; Load boot option from FS workspace
8AA0 STA fs_boot_data ; Store boot option in reply area
8AA3 STX fs_load_addr ; X=0: reply data start offset X=0 on success, &D6 on not-found
8AA5 STX fs_load_addr_hi ; Clear reply buffer high byte
8AA7 LDA #&12 ; A=&12: 18 bytes of reply data
8AA9 STA fs_load_addr_2 ; Store as byte count for copy
8AAB 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.

8AAD .osgbpb_info←1← 8A00 JMP
LDY #4 ; Y=4: check param block byte 4
8AAF LDA tx_in_progress ; Check if destination is in Tube space
8AB2 BEQ store_tube_flag ; No Tube: skip Tube address check
8AB4 CMP (fs_options),y ; Compare Tube flag with addr byte 4
8AB6 BNE store_tube_flag ; Mismatch: not Tube space
8AB8 DEY ; Y=&03
8AB9 SBC (fs_options),y ; Y=3: subtract addr byte 3 from flag
8ABB .store_tube_flag←2← 8AB2 BEQ← 8AB6 BNE
STA rom_svc_num ; Non-zero = Tube transfer required
8ABD .info2←1← 8AC3 BNE
LDA (fs_options),y ; Copy param block bytes 1-4 to workspace
8ABF STA fs_last_byte_flag,y ; Store to &BD+Y workspace area
8AC2 DEY ; Previous byte
8AC3 BNE info2 ; Loop for bytes 3,2,1
8AC5 PLA ; Sub-function: AND #3 of (original A - 4)
8AC6 AND #3 ; Mask to 0-3 (OSGBPB 5-8 → 0-3)
8AC8 BEQ get_disc_title ; A=0 (OSGBPB 5): read disc title
8ACA LSR ; LSR: A=0 (OSGBPB 6) or A=1 (OSGBPB 7)
8ACB BEQ gbpb6_read_name ; A=0 (OSGBPB 6): read CSD/LIB name
8ACD BCS gbpb8_read_dir ; C=1 (OSGBPB 8): read filenames from dir
8ACF .gbpb6_read_name←1← 8ACB BEQ
TAY ; Y=0 for CSD or carry for fn code select Y=function code
8AD0 LDA fs_csd_handle,y ; Get CSD/LIB/URD handles for FS command
8AD3 STA fs_cmd_csd ; Store CSD handle in command buffer
8AD6 LDA fs_lib_handle ; Load LIB handle from workspace
8AD9 STA fs_cmd_lib ; Store LIB handle in command buffer
8ADC LDA fs_urd_handle ; Load URD handle from workspace
8ADF STA fs_cmd_urd ; Store URD handle in command buffer
8AE2 LDX #&12 ; X=&12: buffer extent for command data X=buffer extent (command-specific data bytes)
8AE4 STX fs_cmd_y_param ; Store X as function code in header
8AE7 LDA #&0d ; &0D = 13 bytes of reply data expected
8AE9 STA fs_func_code ; Store reply length in command buffer
8AEC STA fs_load_addr_2 ; Store as byte count for copy loop
8AEE LSR ; LSR: &0D >> 1 = 6 A=timeout period for FS reply
8AEF STA fs_cmd_data ; Store as command data byte
8AF2 CLC ; CLC for standard FS path
8AF3 JSR build_send_fs_cmd ; Build and send FS command (DOFSOP)
8AF6 STX fs_load_addr_hi ; X=0 on success, &D6 on not-found
8AF8 INX ; INX: X=1 after build_send_fs_cmd
8AF9 STX fs_load_addr ; Store X as reply start offset
8AFB .copy_reply_to_caller←2← 8AAB BNE← 8B6E JSR
LDA rom_svc_num ; Copy FS reply to caller's buffer
8AFD BNE tube_transfer ; Non-zero: use Tube transfer path
8AFF LDX fs_load_addr ; X = reply start offset
8B01 LDY fs_load_addr_hi ; Y = reply buffer high byte
8B03 .copy_reply_bytes←1← 8B0C BNE
LDA fs_cmd_data,x ; Load reply data byte
8B06 STA (fs_crc_lo),y ; Store to caller's buffer
8B08 INX ; Next source byte
8B09 INY ; Next destination byte
8B0A DEC fs_load_addr_2 ; Decrement remaining bytes
8B0C BNE copy_reply_bytes ; Loop until all bytes copied
8B0E BEQ gbpb_done ; ALWAYS branch to exit ALWAYS branch
8B10 .tube_transfer←1← 8AFD BNE
JSR tube_claim_loop ; Claim Tube transfer channel
8B13 LDA #1 ; A=1: Tube claim type 1 (write)
8B15 LDX fs_options ; X = param block address low
8B17 LDY fs_block_offset ; Y = param block address high
8B19 INX ; INX: advance past byte 0
8B1A BNE no_page_wrap ; No page wrap: keep Y
8B1C INY ; Page wrap: increment high byte
8B1D .no_page_wrap←1← 8B1A BNE
JSR tube_addr_claim ; Claim Tube address for transfer
8B20 LDX fs_load_addr ; X = reply data start offset
8B22 .tbcop1←1← 8B2B BNE
LDA fs_cmd_data,x ; Load reply data byte
8B25 STA tube_data_register_3 ; Send byte to Tube via R3
8B28 INX ; Next source byte
8B29 DEC fs_load_addr_2 ; Decrement remaining bytes
8B2B BNE tbcop1 ; Loop until all bytes sent to Tube
8B2D LDA #&83 ; Release Tube after transfer complete
8B2F JSR tube_addr_claim ; Release Tube address claim
8B32 .gbpb_done←2← 8B0E BEQ← 8B88 BEQ
JMP return_a_zero ; Return via restore_args path
8B35 .gbpb8_read_dir←1← 8ACD BCS
LDY #9 ; OSGBPB 8: read filenames from dir
8B37 LDA (fs_options),y ; Byte 9: number of entries to read
8B39 STA fs_func_code ; Store as reply count in command buffer
8B3C LDY #5 ; Y=5: byte 5 = starting entry number
8B3E LDA (fs_options),y ; Load starting entry number
8B40 STA fs_data_count ; Store in command buffer
8B43 LDX #&0d ; X=&0D: command data extent X=preserved through header build
8B45 STX fs_reply_cmd ; Store extent in command buffer
8B48 LDY #2 ; Y=2: function code for dir read
8B4A STY fs_load_addr ; Store 2 as reply data start offset
8B4C STY fs_cmd_data ; Store 2 as command data byte
8B4F INY ; Y=3: function code for header read Y=function code for HDRFN Y=&03
8B50 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8B53 STX fs_load_addr_hi ; X=0 after FS command completes X=0 on success, &D6 on not-found
8B55 LDA fs_func_code ; Load reply entry count
8B58 STA (fs_options,x) ; Store at param block byte 0 (X=0)
8B5A LDA fs_cmd_data ; Load entries-read count from reply
8B5D LDY #9 ; Y=9: param block byte 9
8B5F ADC (fs_options),y ; Add to starting entry number
8B61 STA (fs_options),y ; Update param block with new position
8B63 LDA txcb_end ; Load total reply length
8B65 SBC #7 ; Subtract header (7 bytes) from reply len
8B67 STA fs_func_code ; Store adjusted length in command buffer
8B6A STA fs_load_addr_2 ; Store as byte count for copy loop
8B6C BEQ skip_copy_reply ; Zero bytes: skip copy
8B6E JSR copy_reply_to_caller ; Copy reply data to caller's buffer
8B71 .skip_copy_reply←1← 8B6C BEQ
LDX #2 ; X=2: clear 3 bytes
8B73 .zero_cmd_bytes←1← 8B77 BPL
STA fs_data_count,x ; Zero out &0F07+X area
8B76 DEX ; Next byte
8B77 BPL zero_cmd_bytes ; Loop for X=2,1,0
8B79 JSR adjust_addrs_1 ; Adjust pointer by +1 (one filename read)
8B7C SEC ; SEC for reverse adjustment
8B7D DEC fs_load_addr_2 ; Reverse adjustment for updated counter
8B7F LDA fs_cmd_data ; Load entries-read count
8B82 STA fs_func_code ; Store in command buffer
8B85 JSR adjust_addrs ; Adjust param block addresses
8B88 BEQ gbpb_done ; Z=1: all done, exit
8B8A .tube_claim_loop←3← 8B10 JSR← 8B8F BCC← 8DA0 JSR
LDA #&c3 ; A=&C3: Tube claim with retry
8B8C JSR tube_addr_claim ; Request Tube address claim
8B8F BCC tube_claim_loop ; C=0: claim failed, retry
8B91 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 &8BD6 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.

8B92 .fscv_3_star_cmd←1← 8242 JMP
JSR save_fscv_args ; Save A/X/Y and set up command ptr
8B95 LDX #&ff ; X=&FF: table index (pre-incremented)
8B97 .scan_cmd_table←1← 8BB2 BNE
LDY #&ff ; Y=&FF: input index (pre-incremented)
8B99 .decfir←1← 8BA4 BEQ
INY ; Advance input pointer
8B9A INX ; Advance table pointer
8B9B .decmor←1← 8BB6 BCS
LDA fs_cmd_match_table,x ; Load table character
8B9E BMI dispatch_cmd ; Bit 7: end of name, dispatch
8BA0 EOR (fs_crc_lo),y ; XOR input char with table char
8BA2 AND #&df ; Case-insensitive (clear bit 5)
8BA4 BEQ decfir ; Match: continue comparing
8BA6 DEX ; Mismatch: back up table pointer
8BA7 .decmin←1← 8BAB BPL
INX ; Skip to end of table entry
8BA8 LDA fs_cmd_match_table,x ; Load table byte
8BAB BPL decmin ; Loop until bit 7 set (end marker)
8BAD LDA (fs_crc_lo),y ; Check input for '.' abbreviation
8BAF INX ; Skip past handler high byte
8BB0 CMP #&2e ; Is input '.' (abbreviation)?
8BB2 BNE scan_cmd_table ; No: try next table entry
8BB4 INY ; Yes: skip '.' in input
8BB5 DEX ; Back to handler high byte
8BB6 BCS decmor ; ALWAYS branch; dispatch via BMI
8BB8 .dispatch_cmd←1← 8B9E BMI
PHA ; Push handler address high byte
8BB9 LDA cmd_table_entry_1,x ; Load handler address low byte
8BBC PHA ; Push handler address low byte
8BBD CLC ; CLC for pointer calculation
8BBE TYA ; A = chars consumed from input
8BBF LDX fs_crc_hi ; X = command text pointer high
8BC1 ADC fs_crc_lo ; Add chars consumed to pointer low
8BC3 STA fs_context_hi ; Store adjusted text pointer low
8BC6 STA fs_cmd_ptr ; Duplicate to second pointer copy
8BC9 BCC cmd_match_retry ; No page overflow: skip INX
8BCB INX ; Adjust high byte for page crossing
8BCC .cmd_match_retry←1← 8BC9 BCC
STX l0e0c ; Store high byte to context ptr 1
8BCF STX l0e11 ; Store high byte to context ptr 2
8BD2 STX fs_work_16 ; Store high byte to context ptr 3
8BD5 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" → &8D06 (i_am_handler: parse station.net, logon) "EX " → &8BF2 (ex_handler: extended catalogue) "EX"\r → &8BF2 (same, exact match at end of line) "BYE"\r → &8349 (bye_handler: logoff) <catch-all> → &8079 (forward anything else to FS)

8BD6 .fs_cmd_match_table←2← 8B9B LDA← 8BA8 LDA
EOR #&2e ; XOR with '.' (abbreviation check)
8BD8 EQUB &80
8BD9 EQUS "xI AM"
8BDE EQUB &8D, &05
8BE0 EQUS "EX "
8BE3 EQUB &8B, &F1, &45, &58, &0D, &8B, &F1
8BEA EQUS "BYE"
8BED EQUB &0D, &83, &48, &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 &8C07, bypassing fscv_5_cat's default 20-column setup.

8BF2 .ex_handler
DEY ; Pre-decrement Y for parameter
8BF3 LDX #1 ; X=1: boot option display field
8BF5 STX fs_work_7 ; Store to fs_work_7 (&B7)
8BF7 LDX #&50 ; X=&50: 80-column display width
8BF9 STX l00b6 ; Store column width at &B6
8BFB 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.

8BFD .fscv_5_cat
LDX #&14 ; X=&14 (20): column width for display
8BFF STX l00b6 ; Store column width for batch calc
8C01 LDX #3 ; X=3: column count for examine request
8C03 STX fs_work_7 ; Store column count
8C05 LDY #0 ; Y=0: initial entry start offset
8C07 .cat_init_display←1← 8BFB BNE
LDA #6 ; A=6: examine format type in command
8C09 STA fs_cmd_data ; Store format type at &0F05
8C0C JSR skip_spaces ; Skip spaces in dir name argument
8C0F STY fs_load_addr_3 ; Save parameter offset after spaces
8C11 LDX #1 ; X=1: copy dir name at cmd offset 1
8C13 JSR copy_string_from_offset ; Copy directory name to command buffer
8C16 LDY #&12 ; Y=function code for HDRFN
8C18 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8C1B LDX #3 ; X=3: start printing from reply offset 3
8C1D JSR print_reply_bytes ; Print directory title (10 chars)
8C20 JSR print_inline ; Print '('
8C23 EQUS "("
8C24 LDA fs_reply_stn ; Load station number from FS reply
8C27 JSR print_decimal ; Print station number as decimal
8C2A JSR print_inline ; Print ') '
8C2D EQUS ")"
8C2E LDX #5 ; X=5: space padding count
8C30 JSR print_spaces ; Print 5 spaces for alignment
8C33 LDX fs_access_level ; Access: 0=Owner, non-zero=Public
8C36 BNE print_public ; Non-zero: Public access
8C38 JSR print_inline ; Print 'Owner' + CR
8C3B EQUS "Owner."
8C41 BNE cat_access_setup ; Always taken (high-bit term. str)
8C43 .print_public←1← 8C36 BNE
JSR print_inline ; Print 'Public' + CR
8C46 EQUS "Public."
8C4D .cat_access_setup←1← 8C41 BNE
LDY #&15 ; Y=function code for HDRFN
8C4F JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8C52 INX ; X=1: past command code byte
8C53 LDY #&10 ; Y=&10: print 16 characters
8C55 JSR print_reply_counted ; Print disc/CSD name from reply
8C58 LDX #4 ; X=4: space padding count
8C5A JSR print_spaces ; Print 4 spaces for alignment
8C5D JSR print_inline ; Print 'Option ' label
8C60 EQUS "Option "
8C67 LDA fs_boot_option ; Load boot option from workspace
8C6A TAX ; X = boot option for name table lookup
8C6B JSR print_hex ; Print boot option as hex digit
8C6E JSR print_inline ; Print ' ('
8C71 EQUS " ("
8C73 LDY option_name_offsets,x ; Load string offset for option name
8C76 .print_option_char←1← 8C7F BNE
LDA option_name_strings,y ; Load char from option name string
8C79 BEQ done_option_name ; Zero terminator: name complete
8C7B JSR osasci ; Write character
8C7E INY ; Next character
8C7F BNE print_option_char ; Continue printing option name
8C81 .done_option_name←1← 8C79 BEQ
JSR print_inline ; Print ')' + CR + 'Dir. '
8C84 EQUS ").Dir. "
8C8B LDX #&11 ; X=&11: CSD name offset in reply
8C8D JSR print_reply_bytes ; Print current directory name
8C90 LDX #5 ; X=5: space padding count
8C92 JSR print_spaces ; Print 5 spaces for alignment
8C95 JSR print_inline ; Print 'Lib. ' label
8C98 EQUS "Lib. "
8C9D LDX #&1b ; X=&1B: library name offset in reply
8C9F JSR print_reply_bytes ; Print library name
8CA2 JSR print_inline ; Print two CRs (blank line)
8CA5 EQUS ".."
8CA7 STY fs_func_code ; Y=0: initial examine start position
8CAA STY fs_work_4 ; Save start offset in zero page for loop
8CAC TXA ; A = reply buffer bytes consumed
8CAD EOR #&ff ; Complement for divide-by-subtraction
8CAF .count_columns_loop←1← 8CB3 BCS
SEC ; SEC for subtraction
8CB0 SBC l00b6 ; Subtract one column width (20)
8CB2 INY ; Count another entry that fits
8CB3 BCS count_columns_loop ; Loop while space remains
8CB5 STY fs_data_count ; Store entries per examine batch
8CB8 STY fs_work_5 ; Save batch size for loop reset
8CBA .cat_examine_continue←1← 8CE5 BNE
LDY fs_load_addr_3 ; Reload dir name offset for examine
8CBC .cat_examine_loop
LDX fs_work_7 ; Load column count for display format
8CBE STX fs_cmd_data ; Store column count in command data
8CC1 LDX #3 ; X=3: copy directory name at offset 3
8CC3 JSR copy_string_from_offset ; Append directory name to examine command
8CC6 LDY #3 ; Y=function code for HDRFN
8CC8 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8CCB LDA fs_cmd_data ; Load entry count from reply
8CCE BEQ set_handle_return ; Zero entries returned: catalogue done
8CD0 LDX #2 ; X=2: first entry offset in reply
8CD2 JSR print_dir_from_offset ; Print/format this directory entry
8CD5 CLC ; CLC for addition
8CD6 LDA fs_work_4 ; Load current examine start offset
8CD8 ADC fs_cmd_data ; Add entries returned this batch
8CDB STA fs_func_code ; Update next examine start offset
8CDE STA fs_work_4 ; Save updated start offset
8CE0 LDA fs_work_5 ; Reload batch size for next request
8CE2 STA fs_data_count ; Store batch size in command buffer
8CE5 BNE cat_examine_continue ; Loop for remaining characters
8CE7 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 &F6 → &8CF6 = bare CR (empty command) Option 1 (Load): offset &E7 → &8CE7 = "L.!BOOT" (dual-purpose: the JMP &212E instruction at &8CE7 has opcode &4C='L' and operand bytes &2E='.' &21='!', forming the string "L.!") Option 2 (Run): offset &E9 → boot_cmd_strings-1 = "!BOOT" (*RUN) Option 3 (Exec): offset &EF → &8CEF = "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.

8CEA .boot_cmd_strings
EQUS "BOOT"
8CEE EQUB &0D
8CEF EQUS "E.!BOOT"
8CF6 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
8CF7 .fsreply_5_set_lib
STY fs_lib_handle ; Store Y (library handle) to &0E04
8CFA 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
8CFC .fsreply_3_set_csd
STY fs_csd_handle ; Store Y (CSD handle) to &0E03
8CFF .set_handle_return←3← 8CCE BEQ← 8CFA BNE← 8D2D 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.

8D02 .boot_option_offsets←1← 8D32 LDX
EQUB &F6 ; Opt 0 (Off): bare CR
8D03 EQUB &E7 ; Opt 1 (Load): L.!BOOT
8D04 EQUB &E9 ; Opt 2 (Run): !BOOT
8D05 EQUB &EF ; 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.

8D06 .i_am_handler
JSR skip_spaces ; Skip spaces in command argument
8D09 BCS jmp_restore_args ; C=1: alphabetic, forward to FS
8D0B LDA #0 ; A=0: default network (local)
8D0D JSR parse_decimal ; Parse decimal number from (fs_options),Y (DECIN)
8D10 BCC fsreply_handle_copy ; C=0: no dot, single number only
8D12 INY ; Y=offset into (fs_options) buffer
8D13 JSR parse_decimal ; Parse decimal number from (fs_options),Y (DECIN)
8D16 .fsreply_handle_copy←1← 8D10 BCC
STA fs_server_stn ; A=parsed value (accumulated in &B2)
8D19 STX fs_server_net ; X=initial A value (saved by TAX)
8D1C .jmp_restore_args←1← 8D09 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.

8D1F .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.

8D20 .fsreply_2_copy_handles
LDX #3 ; Copy 4 bytes: boot option + 3 handles
8D22 BCC logon3 ; SDISC: skip boot option, copy handles only
8D24 .logon2←1← 8D2B BPL
LDA fs_cmd_data,x ; Load from FS reply (&0F05+X)
8D27 STA fs_urd_handle,x ; Store to handle workspace (&0E02+X)
8D2A .logon3←1← 8D22 BCC
DEX ; Next handle (descending)
8D2B BPL logon2 ; Loop while X >= 0
8D2D 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.

8D2F .boot_cmd_execute
LDY fs_boot_option ; Y = boot option from FS workspace
8D32 LDX boot_option_offsets,y ; X = command string offset from table
8D35 LDY #&8c ; Y = &8D (high byte of command address)
8D37 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.

8D3A .option_name_strings←1← 8C76 LDA
EQUS "Off"
8D3D EQUB &00
8D3E EQUS "Load"
8D42 EQUB &00
8D43 EQUS "Run"
8D46 EQUB &00
8D47 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).

8D4B .option_name_offsets←1← 8C73 LDY
BRK ; Offset 0 (BRK opcode as zero byte)
8D4C 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.

8D4F .print_reply_bytes←3← 8C1D JSR← 8C8D JSR← 8C9F JSR
LDY #&0a ; Y=10: default character count
8D51 .print_reply_counted←2← 8C55 JSR← 8D59 BNE
LDA fs_cmd_data,x ; Load character from reply buffer
8D54 JSR osasci ; Write character
8D57 INX ; Advance to next character
8D58 DEY ; Decrement remaining count
8D59 BNE print_reply_counted ; Loop until count exhausted
8D5B 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.

8D5C .print_spaces←4← 8C30 JSR← 8C5A JSR← 8C92 JSR← 8D60 BNE
JSR print_space ; Print one space character
8D5F DEX ; Decrement space count
8D60 BNE print_spaces ; Loop until all spaces printed
8D62 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.

8D63 .copy_filename←3← 8079 JSR← 86CB JSR← 888A 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)
8D65 .copy_string_to_cmd←4← 8779 JSR← 8883 JSR← 88A5 JSR← 8964 JSR
LDY #0 ; Start copying from offset 0
8D67 .copy_string_from_offset←3← 8C13 JSR← 8CC3 JSR← 8D70 BNE
LDA (fs_crc_lo),y ; Load next byte from source string
8D69 STA fs_cmd_data,x ; Store to command buffer
8D6C INX ; Advance write position
8D6D INY ; Advance read position
8D6E EOR #&0d ; XOR with CR: result=0 if byte was CR
8D70 BNE copy_string_from_offset ; Loop until CR copied
8D72 .return_copy_string←1← 8D78 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.

8D73 .fsreply_0_print_dir
LDX #0 ; X=0: start of reply buffer
8D75 .print_dir_from_offset←2← 8CD2 JSR← 8D82 BNE
LDA fs_cmd_data,x ; Load character from reply
8D78 BMI return_copy_string ; Bit 7 set: end of string
8D7A BNE infol2 ; Non-zero: printable character
8D7C LDA #&0d ; Replace null with CR
8D7E .infol2←1← 8D7A BNE
JSR osasci ; Write character 13
8D81 INX ; Advance to next character
8D82 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.

8D84 .fsreply_4_notify_exec
LDX #&0e ; X=&0E: OSWORD &10 parameter block size
8D86 STX fs_block_offset ; Y=0: param block offset
8D88 LDA #&10 ; A=&10: OSWORD &10 (open RXCB)
8D8A STA fs_options ; Issue OSWORD &10 to open RXCB
8D8C LDX #&4a ; X=&4A: parameter block offset
8D8E LDY #5 ; Y=5: command code offset
8D90 JSR send_fs_examine ; Send FS examine command
8D93 LDA tx_in_progress ; Y=&70: FS workspace offset
8D96 BEQ net_handle_validate ; No Tube: skip transfer setup
8D98 ADC fs_load_upper ; Add file load address upper byte
8D9B ADC fs_addr_check ; Add address check byte
8D9E BCS net_handle_validate ; Store final byte
8DA0 JSR tube_claim_loop ; X=&16: OSBYTE param
8DA3 LDX #9 ; X=9: Tube claim parameter
8DA5 LDY #&0f ; Y=&0F: Tube claim parameter
8DA7 LDA #4 ; Save original value on stack
8DA9 JMP tube_addr_claim ; Claim Tube for address transfer
8DAC .net_handle_validate←2← 8D96 BEQ← 8D9E BCS
JMP (fs_load_vector) ; Execute via indirect load vector

*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.

8DAF .net_1_read_handle
LDY #&6f ; Y=&6F: handle offset in RX buffer
8DB1 LDA (net_rx_ptr),y ; Load handle byte from RX data
8DB3 STA osword_pb_ptr ; Store handle to &F0
8DB5 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
8DB7 .calc_handle_offset←5← 82B6 JSR← 8DCB JSR← 8DE1 JSR← 8F01 JSR← 8F1A JSR
ASL ; A = handle * 2
8DB8 ASL ; A = handle * 4
8DB9 PHA ; Push handle*4 onto stack
8DBA ASL ; A = handle * 8
8DBB TSX ; X = stack pointer
8DBC ADC l0101,x ; A = handle*8 + handle*4 = handle*12
8DBF TAY ; Y = offset into handle workspace
8DC0 PLA ; Clean stack (discard handle*4)
8DC1 CMP #&48 ; Offset >= &48 (6 handles max)?
8DC3 BCC return_calc_handle ; No: valid handle, return with C=0
8DC5 LDY #0 ; Y=0: invalid handle error sentinel
8DC7 TYA ; A=0, C set = error indicator A=&00
8DC8 .return_calc_handle←1← 8DC3 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.

8DC9 .net_2_read_handle_entry
LDA osword_pb_ptr ; Load handle number from &F0
8DCB JSR calc_handle_offset ; Look up handle &F0 in workspace
8DCE BCS rxpol2 ; Invalid handle: return 0
8DD0 LDA (nfs_workspace),y ; Load stored handle value
8DD2 CMP #&3f ; &3F = unused/closed slot marker
8DD4 BNE store_handle_return ; Slot in use: return actual value
8DD6 .rxpol2←2← 8DCE BCS← 8DE4 BCS
LDA #0 ; Return 0 for closed/invalid handle
8DD8 .store_handle_return←1← 8DD4 BNE
STA osword_pb_ptr ; Store result back to &F0
8DDA .clear_svc_return←3← 8DB5 BCC← 8DF0 BCC← 8DF5 BVC
LDA #0 ; A=0: clear service claim
8DDC STA rom_svc_num ; Release ROM service number
8DDE 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.

8DDF .net_3_close_handle
LDA osword_pb_ptr ; Load handle number from &F0
8DE1 JSR calc_handle_offset ; Look up handle &F0 in workspace
8DE4 BCS rxpol2 ; Invalid handle: return 0
8DE6 ROL rx_status_flags ; Preserve carry via ROL
8DE9 LDA #&3f ; &3F = '?' marks slot as unused
8DEB 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).

8DED .restore_rx_flags
ROR rx_status_flags ; Restore carry via ROR
8DF0 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).

8DF2 .net_4_resume_remote
JSR resume_after_remote ; Jump to clear_svc_restore_args
8DF5 BVC clear_svc_return ; Enable remote operations flag
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 &8E01.

8DF7 .svc_8_osword
LDA osbyte_a_copy ; Command code from &EF
8DF9 SBC #&0f ; Subtract &0F: OSWORD &0F-&13 become indices 0-4
8DFB BMI return_copy_param ; Outside our OSWORD range, exit
8DFD CMP #5 ; Only OSWORDs &0F-&13 (index 0-4)
8DFF 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 &8E18 (low) / &8E1D (high).

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

8E22 .copy_param_block←5← 8E30 BPL← 8E46 JSR← 8E62 JSR← 8E96 JSR← 8F31 JSR
BCC load_workspace_byte ; C=0: workspace to param direction
8E24 LDA (osword_pb_ptr),y ; Load byte from param block
8E26 STA (fs_crc_lo),y ; Store to workspace
8E28 BCS copyl3 ; Always taken (C still set) ALWAYS branch
8E2A .load_workspace_byte←1← 8E22 BCC
LDA (fs_crc_lo),y ; Load byte from workspace
8E2C STA (osword_pb_ptr),y ; Store to param block (no-op if C=1)
8E2E .copyl3←1← 8E28 BCS
INY ; Advance to next byte
8E2F DEX ; Decrement byte counter
8E30 BPL copy_param_block ; Loop while X >= 0
8E32 .return_copy_param←2← 8DFB BMI← 8DFF 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
8E33 .osword_0f_handler
ASL tx_ctrl_status ; ASL: set C if TX in progress
8E36 BCC osword_12_subfunc ; C=0: read path
8E38 LDA net_rx_ptr_hi ; User TX CB in workspace page (high byte)
8E3A STA fs_crc_hi ; Set param block high byte
8E3C STA nmi_tx_block_hi ; Set LTXCBP high byte for low-level TX
8E3E LDA #&6f ; &6F: offset into workspace for user TXCB
8E40 STA fs_crc_lo ; Set param block low byte
8E42 STA nmi_tx_block ; Set LTXCBP low byte for low-level TX
8E44 LDX #&0f ; X=15: copy 16 bytes (OSWORD param block)
8E46 JSR copy_param_block ; Copy param block to user TX control block
8E49 JSR trampoline_tx_setup ; Set up and start low-level transmit
8E4C JMP clear_svc_restore_args ; Exit: release service claim
8E4F .osword_12_subfunc←1← 8E36 BCC
SEC ; SEC: alternate entry for OSWORD &11
8E50 TYA ; A = param block high for branch
8E51 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 (&8E6A) to return just the buffer size and args size without copying the data.

8E53 .osword_11_handler
LDA net_rx_ptr_hi ; Set source high byte from workspace page
8E55 STA fs_crc_hi ; Store as copy source high byte in &BF
8E57 LDY #&7f ; JSRSIZ at workspace offset &7F
8E59 LDA (net_rx_ptr),y ; Load buffer size from workspace
8E5B INY ; Y=&80: start of JSR argument data Y=&80
8E5C STY fs_crc_lo ; Store &80 as copy source low byte
8E5E TAX ; X = buffer size (loop counter)
8E5F DEX ; X = size-1 (0-based count for copy)
8E60 LDY #0 ; Y=0: start of destination param block
8E62 JSR copy_param_block ; Copy X+1 bytes from workspace to param
8E65 JSR clear_jsr_protection ; Reset JSR protection status
8E68 BCC set_carry_dispatch ; Branch to set carry and dispatch
8E6A .read_args_size←1← 8EBD BEQ
LDY #&7f ; Y=&7F: JSRSIZ offset (READRB entry)
8E6C LDA (net_rx_ptr),y ; Load buffer size from workspace
8E6E LDY #1 ; Y=1: param block offset for size byte
8E70 STA (osword_pb_ptr),y ; Store buffer size to (&F0)+1
8E72 INY ; Y=2: param block offset for args size Y=&02
8E73 LDA #&80 ; A=&80: argument data starts at offset &80
8E75 .readry←1← 8E51 BCS
STA (osword_pb_ptr),y ; Store args start offset to (&F0)+2
8E77 BCS carry_exit_or_read ; Always taken (SEC set above)
8E79 .osword_12_ws_offsets←1← 8E8D 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 &8E22 for station read/set.

8E7B .osword_12_handler
CMP #6 ; Sub-function >= 6? (handle ops)
8E7D BCS rsl1 ; Sub >= 6: handle/station/error
8E7F CMP #4 ; Sub-function >= 4? (protection)
8E81 BCS rssl1 ; Sub-function 4 or 5: read/set protection
8E83 LSR ; LSR: 0->0, 1->0, 2->1, 3->1
8E84 LDX #&0d ; X=&0D: default to static workspace page
8E86 TAY ; Transfer LSR result to Y for indexing
8E87 BEQ set_workspace_page ; Y=0 (sub 0-1): use page &0D
8E89 LDX nfs_workspace_hi ; Y=1 (sub 2-3): use dynamic workspace
8E8B .set_workspace_page←1← 8E87 BEQ
STX fs_crc_hi ; Store workspace page in &BF (hi byte)
8E8D LDA osword_12_ws_offsets,y ; Load offset: &FF (sub 0-1) or &01 (sub 2-3)
8E90 STA fs_crc_lo ; Store offset in &BE (lo byte)
8E92 LDX #1 ; X=1: copy 2 bytes
8E94 LDY #1 ; Y=1: start at param block offset 1
8E96 JSR copy_param_block ; Copy station bytes to/from workspace
8E99 BNE set_carry_dispatch ; Always taken (Y=2 after copy)
8E9B .rssl1←1← 8E81 BCS
LSR ; LSR A: test bit 0 of sub-function
8E9C INY ; Y=1: offset for protection byte
8E9D LDA (osword_pb_ptr),y ; Load protection byte from param block
8E9F BCS rssl2 ; C=1 (odd sub): set protection
8EA1 LDA prot_status ; C=0 (even sub): read current status
8EA4 STA (osword_pb_ptr),y ; Return current value to param block
8EA6 .rssl2←1← 8E9F BCS
STA prot_status ; Update protection status
8EA9 STA rx_ctrl_copy ; Also save as JSR mask backup
8EAC .set_carry_dispatch←2← 8E68 BCC← 8E99 BNE
SEC ; SEC: set carry for exit
8EAD BCS carry_exit_or_read ; ALWAYS branch
8EAF .read_local_station←1← 8EB9 BEQ
LDA tx_clear_flag ; Load local station number
8EB2 INY ; Y=1: param block offset for result
8EB3 STA (osword_pb_ptr),y ; Return station number to caller
8EB5 BCS carry_exit_or_read ; Always taken (C set above)
8EB7 .rsl1←1← 8E7D BCS
CMP #8 ; Sub-function 8: read FS handle
8EB9 BEQ read_local_station ; Match: read handle from RX buffer
8EBB CMP #9 ; Sub-function 9: read args size
8EBD BEQ read_args_size ; Match: read ARGS buffer info
8EBF BPL osword_12_error ; Sub >= 10 (bit 7 clear): read error
8EC1 LDY #3 ; Y=3: start from handle 3 (descending)
8EC3 LSR ; LSR: test read/write bit
8EC4 BCC readc1 ; C=0: read handles from workspace
8EC6 STY nfs_temp ; Init loop counter at Y=3
8EC8 .copy_handles_to_ws←1← 8ED7 BNE
LDY nfs_temp ; Reload loop counter
8ECA LDA (osword_pb_ptr),y ; Read handle from caller's param block
8ECC JSR handle_to_mask_a ; Convert handle number to bitmask
8ECF TYA ; TYA: get bitmask result
8ED0 LDY nfs_temp ; Reload loop counter
8ED2 STA fs_server_net,y ; Store bitmask to FS server table
8ED5 DEC nfs_temp ; Next handle (descending)
8ED7 BNE copy_handles_to_ws ; Loop for handles 3,2,1
8ED9 BEQ clear_svc_restore_args ; ALWAYS branch
8EDB .osword_12_error←1← 8EBF BPL
INY ; Y=1: param block offset for error
8EDC LDA fs_last_error ; Load last FS error number
8EDF STA (osword_pb_ptr),y ; Return error code to caller
8EE1 .carry_exit_or_read←3← 8E77 BCS← 8EAD BCS← 8EB5 BCS
BCS clear_svc_restore_args ; Exit with carry set
8EE3 .readc1←2← 8EC4 BCC← 8EEC BNE
LDA fs_server_net,y ; A=single-bit bitmask
8EE6 JSR mask_to_handle ; Convert bitmask to handle number (FS2A)
8EE9 STA (osword_pb_ptr),y ; A=handle number (&20-&27) Y=preserved
8EEB DEY ; Next handle (descending) Y=parameter block address high byte
8EEC BNE readc1 ; Loop for handles 3,2,1
8EEE 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
8EF0 .osword_10_handler
LDX nfs_workspace_hi ; Workspace page high byte
8EF2 STX fs_crc_hi ; Set up pointer high byte in &AC
8EF4 STY fs_crc_lo ; Save param block high byte in &AB
8EF6 ROR rx_status_flags ; Disable user RX during CB operation
8EF9 LDA (osword_pb_ptr),y ; Read first byte of param block
8EFB STA fs_last_byte_flag ; Save: 0=open new, non-zero=read RXCB
8EFD .scan_or_read_rxcb
BNE read_rxcb ; Non-zero: read specified RXCB
8EFF LDA #3 ; Start scan from RXCB #3 (0-2 reserved)
8F01 .scan0←1← 8F13 BNE
JSR calc_handle_offset ; Convert RXCB number to workspace offset
8F04 BCS openl4 ; Invalid RXCB: return zero
8F06 LSR ; LSR twice: byte offset / 4
8F07 LSR ; Yields RXCB number from offset
8F08 TAX ; X = RXCB number for iteration
8F09 LDA (fs_crc_lo),y ; Read flag byte from RXCB workspace
8F0B BEQ openl4 ; Zero = end of CB list
8F0D CMP #&3f ; &3F = deleted slot, free for reuse
8F0F BEQ scan1 ; Found free slot
8F11 INX ; Try next RXCB
8F12 TXA ; A = next RXCB number
8F13 BNE scan0 ; Continue scan (always branches)
8F15 .scan1←1← 8F0F BEQ
TXA ; A = free RXCB number
8F16 LDX #0 ; X=0 for indexed indirect store
8F18 STA (osword_pb_ptr,x) ; Return RXCB number to caller's byte 0
8F1A .read_rxcb←1← 8EFD BNE
JSR calc_handle_offset ; Convert RXCB number to workspace offset
8F1D BCS openl4 ; Invalid: write zero to param block
8F1F DEY ; Y = offset-1: points to flag byte
8F20 STY fs_crc_lo ; Set &AB = workspace ptr low byte
8F22 LDA #&c0 ; &C0: test mask for flag byte
8F24 LDY #1 ; Y=1: flag byte offset in RXCB
8F26 LDX #&0b ; X=11: copy 12 bytes from RXCB
8F28 CPY fs_last_byte_flag ; Compare Y(1) with saved byte (open/read)
8F2A ADC (fs_crc_lo),y ; ADC flag: test if slot is in use
8F2C BEQ openl6 ; Zero: slot open, do copy
8F2E BMI openl7 ; Negative: slot has received data
8F30 .copy_rxcb_to_param←1← 8F40 BNE
CLC ; C=0: workspace-to-param direction
8F31 .openl6←1← 8F2C BEQ
JSR copy_param_block ; Copy RXCB data to param block
8F34 BCS reenable_rx ; Done: skip deletion on error
8F36 LDA #&3f ; Mark CB as consumed (consume-once)
8F38 LDY #1 ; Y=1: flag byte offset
8F3A STA (fs_crc_lo),y ; Write &3F to mark slot deleted
8F3C BNE reenable_rx ; Branch to exit (always taken) ALWAYS branch
8F3E .openl7←1← 8F2E BMI
ADC #1 ; Advance through multi-byte field
8F40 BNE copy_rxcb_to_param ; Loop until all bytes processed
8F42 DEY ; Y=-1 → Y=0 after STA below
8F43 .openl4←3← 8F04 BCS← 8F0B BEQ← 8F1D BCS
STA (osword_pb_ptr),y ; Return zero (no free RXCB found)
8F45 .reenable_rx←2← 8F34 BCS← 8F3C 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.

8F48 .clear_svc_restore_args←6← 8E4C JMP← 8ED9 BEQ← 8EE1 BCS← 8EEE BEQ← 8FBE BNE← 9004 JMP
LDY #0 ; Y=0: clear service claim
8F4A STY rom_svc_num ; Release ROM service number
8F4C LDY #2 ; Y=2: copy 3 bytes (indices 2,1,0)
8F4E .rest1←1← 8F54 BPL
LDA (net_rx_ptr),y ; Load saved arg from (net_rx_ptr)+Y
8F50 STA fs_last_byte_flag,y ; Restore saved OSWORD argument byte
8F53 DEY ; Decrement byte counter
8F54 BPL rest1 ; Loop for bytes 2,1,0
8F56 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
8F57 .setup_rx_buffer_ptrs←1← 8F8A JSR
LDY #&28 ; Y=&28: RXCB template offset
8F59 LDA osword_pb_ptr ; A = base address low byte
8F5B ADC #1 ; A = base + 1 (skip length byte)
8F5D JSR store_16bit_at_y ; Store 16-bit start addr at ws+&1C/&1D
8F60 LDY #1 ; Read data length from (&F0)+1
8F62 LDA (osword_pb_ptr),y ; A = data length byte
8F64 LDY #&2c ; Workspace offset &20 = RX data end
8F66 ADC osword_pb_ptr ; A = base + length = end address low
8F68 .store_16bit_at_y←1← 8F5D JSR
STA (nfs_workspace),y ; Store low byte of 16-bit address
8F6A INY ; Advance to high byte offset
8F6B LDA osword_pb_ptr_hi ; A = high byte of base address
8F6D ADC #0 ; Add carry for 16-bit addition
8F6F STA (nfs_workspace),y ; Store high byte
8F71 RTS ; Return

Econet transmit/receive handler

A=0: Initialise TX control block from ROM template at &8310 (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 &8F48).

On EntryA0=set up and transmit, >=1=handle TX result
8F72 .econet_tx_rx
CMP #1 ; A=0: set up and transmit; A>=1: handle result
8F74 BCS handle_tx_result ; A >= 1: handle TX result
8F76 LDY #&2f ; Y=&2F: start of template (descending)
8F78 .dofs01←1← 8F85 BNE
LDA init_tx_ctrl_port,y ; Load from ROM template (zero = use NMI workspace value)
8F7B BNE store_txcb_byte ; Non-zero = use ROM template byte as-is
8F7D LDA l0dda,y ; Zero = substitute from NMI workspace
8F80 .store_txcb_byte←1← 8F7B BNE
STA (nfs_workspace),y ; Store to dynamic workspace
8F82 DEY ; Descend through template
8F83 CPY #&23 ; Stop at offset &17
8F85 BNE dofs01 ; Loop until all bytes copied
8F87 INY ; Y=&18: TX block starts here
8F88 STY net_tx_ptr ; Point net_tx_ptr at workspace+&18
8F8A JSR setup_rx_buffer_ptrs ; Set up RX buffer start/end pointers
8F8D LDY #2 ; Y=2: port byte offset in RXCB
8F8F LDA #&90 ; A=&90: FS reply port
8F91 STA (osword_pb_ptr),y ; Store port &90 at (&F0)+2
8F93 INY ; Y=&03
8F94 INY ; Y=&04: advance to station address Y=&04
8F95 .copy_fs_addr←1← 8F9D BNE
LDA fs_context_base,y ; Copy FS station addr from workspace
8F98 STA (osword_pb_ptr),y ; Store to RX param block
8F9A INY ; Next byte
8F9B CPY #7 ; Done 3 bytes (Y=4,5,6)?
8F9D BNE copy_fs_addr ; No: continue copying
8F9F LDA nfs_workspace_hi ; High byte of workspace for TX ptr
8FA1 STA net_tx_ptr_hi ; Store as TX pointer high byte
8FA3 CLI ; Enable interrupts before transmit
8FA4 JSR tx_poll_timeout ; Transmit with full retry
8FA7 LDY #&2c ; Y=&2C: RX end address offset
8FA9 LDA #&ff ; Set RX end address to &FFFF (accept any length)
8FAB STA (nfs_workspace),y ; Store end address low byte (&FF)
8FAD INY ; Y=&2d
8FAE STA (nfs_workspace),y ; Store end address high byte (&FF)
8FB0 LDY #&25 ; Y=&25: port byte in workspace RXCB
8FB2 LDA #&90 ; A=&90: FS reply port
8FB4 STA (nfs_workspace),y ; Store port to workspace RXCB
8FB6 DEY ; Y=&24
8FB7 LDA #&7f ; A=&7F: flag byte = waiting for reply
8FB9 STA (nfs_workspace),y ; Store flag byte to workspace RXCB
8FBB JSR send_to_fs_star ; Initiate receive with timeout
8FBE BNE clear_svc_restore_args ; Non-zero = error/timeout: jump to cleanup
8FC0 .handle_tx_result←1← 8F74 BCS
PHP ; Save processor flags
8FC1 LDY #1 ; Y=1: first data byte offset
8FC3 LDA (osword_pb_ptr),y ; Load first data byte from RX buffer
8FC5 TAX ; X = first data byte (command code)
8FC6 INY ; Advance to next data byte Y=&02
8FC7 LDA (osword_pb_ptr),y ; Load station address high byte
8FC9 INY ; Advance past station addr Y=&03
8FCA STY fs_crc_lo ; Save Y as data index
8FCC LDY #&72 ; Store station addr hi at (net_rx_ptr)+&72
8FCE STA (net_rx_ptr),y ; Store to workspace
8FD0 DEY ; Y=&71
8FD1 TXA ; A = command code (from X)
8FD2 STA (net_rx_ptr),y ; Store station addr lo at (net_rx_ptr)+&71
8FD4 PLP ; Restore flags from earlier PHP
8FD5 BNE dofs2 ; First call: adjust data length
8FD7 .send_data_bytes←1← 8FF1 BNE
LDY fs_crc_lo ; Reload data index
8FD9 INC fs_crc_lo ; Advance data index for next iteration
8FDB LDA (osword_pb_ptr),y ; Load next data byte
8FDD LDY #&7d ; Y=&7D: store byte for TX at offset &7D
8FDF STA (net_rx_ptr),y ; Store data byte at (net_rx_ptr)+&7D for TX
8FE1 PHA ; Save data byte for &0D check after TX
8FE2 JSR ctrl_block_setup_alt ; Set up TX control block
8FE5 CLI ; Enable interrupts for TX
8FE6 JSR tx_poll_core ; Enable IRQs and transmit Core transmit and poll routine (XMIT)
8FE9 .delay_between_tx←1← 8FEA BNE
DEX ; Short delay loop between TX packets
8FEA BNE delay_between_tx ; Spin until X reaches 0
8FEC PLA ; Restore data byte for terminator check
8FED BEQ rx_first_packet ; Z=1: not intercepted, pass through
8FEF EOR #&0d ; Test for end-of-data marker (&0D)
8FF1 BNE send_data_bytes ; Not &0D: continue with next byte
8FF3 .rx_first_packet←1← 8FED BEQ
BEQ jmp_clear_svc_restore ; First packet: exit handler
8FF5 .dofs2←1← 8FD5 BNE
JSR ctrl_block_setup_alt ; First-packet: set up control block
8FF8 LDY #&7b ; Y=&7B: data length offset
8FFA LDA (net_rx_ptr),y ; Load current data length
8FFC ADC #3 ; Adjust data length by 3 for header bytes
8FFE 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.

9000 .enable_irq_and_tx
CLI ; Enable interrupts
9001 JSR tx_poll_ff ; Poll TX until complete
9004 .jmp_clear_svc_restore←1← 8FF3 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 &9020. 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
9007 .osword_dispatch
PHP ; Save processor status
9008 PHA ; Save A (reason code)
9009 TXA ; Save X
900A PHA ; Push X to stack
900B TYA ; Save Y
900C PHA ; Push Y to stack
900D TSX ; Get stack pointer for indexed access
900E LDA l0103,x ; Retrieve original A (reason code) from stack
9011 CMP #9 ; Reason codes 0-8 only
9013 BCS entry1 ; Code >= 9: skip dispatch, restore regs
9015 TAX ; X = reason code for table lookup
9016 JSR osword_trampoline ; Dispatch to handler via trampoline
9019 .entry1←1← 9013 BCS
PLA ; Restore Y
901A TAY ; Transfer to Y register
901B PLA ; Restore X
901C TAX ; Transfer to X register
901D PLA ; Restore A
901E PLP ; Restore processor status flags
901F RTS ; Return with all registers preserved
9020 .osword_trampoline←1← 9016 JSR
LDA osword_tbl_hi,x ; PHA/PHA/RTS trampoline: push handler addr-1, RTS jumps to it
9023 PHA ; Push high byte of handler address
9024 LDA osword_tbl_lo,x ; Load handler low byte from table
9027 PHA ; Push low byte of handler address
9028 LDA osbyte_a_copy ; Load workspace byte &EF for handler
902A RTS ; RTS dispatches to pushed handler
902B .osword_tbl_lo←1← 9024 LDA
EQUB <(return_2-1)
902C EQUB <(remote_print_handler-1)
902D EQUB <(remote_print_handler-1)
902E EQUB <(remote_print_handler-1)
902F EQUB <(net_write_char-1)
9030 EQUB <(printer_select_handler-1)
9031 EQUB <(return_2-1)
9032 EQUB <(remote_cmd_dispatch-1)
9033 EQUB <(remote_cmd_data-1)
9034 .osword_tbl_hi←1← 9020 LDA
EQUB >(return_2-1)
9035 EQUB >(remote_print_handler-1)
9036 EQUB >(remote_print_handler-1)
9037 EQUB >(remote_print_handler-1)
9038 EQUB >(net_write_char-1)
9039 EQUB >(printer_select_handler-1)
903A EQUB >(return_2-1)
903B EQUB >(remote_cmd_dispatch-1)
903C 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
903D .net_write_char
TSX ; Get stack pointer for P register
903E ROR l0106,x ; ROR/ASL on stacked P: zeros carry to signal success
9041 ASL l0106,x ; ASL: restore P after ROR zeroed carry
9044 TYA ; Y = character to write
9045 LDY #&da ; Store character at workspace offset &DA
9047 STA (nfs_workspace),y ; Store char at workspace offset &DA
9049 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
904B .setup_tx_and_send←3← 8159 JSR← 9092 JSR← 90F3 JSR
LDY #&d9 ; Y=&D9: command type offset
904D STA (nfs_workspace),y ; Store command type at ws+&D9
904F LDA #&80 ; Mark TX control block as active (&80)
9051 LDY #&0c ; Y=&0C: TXCB start offset
9053 STA (nfs_workspace),y ; Set TX active flag at ws+&0C
9055 STY net_tx_ptr ; Redirect net_tx_ptr low to workspace
9057 LDX nfs_workspace_hi ; Load workspace page high byte
9059 STX net_tx_ptr_hi ; Complete ptr redirect
905B JSR tx_poll_ff ; Transmit with full retry
905E LDA #&3f ; Mark TXCB as deleted (&3F) after transmit
9060 STA (net_tx_ptr,x) ; Write &3F to TXCB byte 0
9062 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.

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

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

90CD .remote_cmd_data
LDY #&0e ; Y=&0E: 14-byte parameter block
90CF CMP #7 ; OSWORD 7? (make sound)
90D1 BEQ copy_params_rword ; OSWORD 7 (sound): handle via common path
90D3 CMP #8 ; OSWORD 8 = define an envelope
90D5 BNE return_remote_cmd ; Not OSWORD 7 or 8: ignore (BNE exits)
90D7 .copy_params_rword←1← 90D1 BEQ
LDX #&db ; Point workspace to offset &DB for params
90D9 STX nfs_workspace ; Store workspace ptr offset &DB
90DB .copy_osword_params←1← 90E0 BPL
LDA (osword_pb_ptr),y ; Load param byte from OSWORD param block
90DD STA (nfs_workspace),y ; Write param byte to workspace
90DF DEY ; Next byte (descending)
90E0 BPL copy_osword_params ; Loop for all parameter bytes
90E2 INY ; Y=0 after loop
90E3 DEC nfs_workspace ; Point workspace to offset &DA
90E5 LDA osbyte_a_copy ; Load original OSWORD code
90E7 STA (nfs_workspace),y ; Store OSWORD code at ws+0
90E9 STY nfs_workspace ; Reset workspace ptr to base
90EB LDY #&14 ; Y=&14: command type offset
90ED LDA #&e9 ; Tag as RWORD (port &E9)
90EF STA (nfs_workspace),y ; Store port tag at ws+&14
90F1 LDA #1 ; A=1: single-byte TX
90F3 JSR setup_tx_and_send ; Transmit via workspace TXCB
90F6 STX nfs_workspace ; Restore workspace ptr
90F8 JSR ctrl_block_setup_alt ; Set up control block for reply
90FB .return_remote_cmd←1← 90D5 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.

90FC .lang_1_remote_boot
LDY #4 ; Y=4: RX control block byte 4
90FE LDA (net_rx_ptr),y ; Load first data byte from RX
9100 BEQ remot1 ; Zero: standard boot, skip code
9102 .rchex←1← 9148 BNE
JMP clear_jsr_protection ; Load language ROM number
9105 .remot1←2← 9100 BEQ← 913E BEQ
ORA #9 ; OR with 9: set remote boot bits
9107 STA (net_rx_ptr),y ; Store modified control byte
9109 LDX #&80 ; X=&80: exec address offset lo
910B LDY #&80 ; Y=&80: exec address offset hi
910D LDA (net_rx_ptr),y ; Load exec address low byte
910F PHA ; Save exec lo on stack
9110 INY ; Non-zero: remote boot handler Y=&81
9111 LDA (net_rx_ptr),y ; Load exec address high byte
9113 LDY #&0f ; Y=&0F: workspace offset for hi
9115 STA (nfs_workspace),y ; Store exec hi byte to workspace
9117 DEY ; A=2: OSBYTE function code Y=&0e
9118 PLA ; Restore exec lo byte from stack
9119 STA (nfs_workspace),y ; Copy command to &0100 area
911B JSR clear_osbyte_ce_cf ; Initialize OSBYTE vectors
911E JSR ctrl_block_setup ; Y=0: command string high byte
9121 LDX #1 ; X=1: enable parameter
9123 LDY #0 ; Y=0: second argument for OSBYTE
9125 LDA #osbyte_read_write_econet_keyboard_disable ; Disable keyboard for Econet boot
9127 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.

912A .lang_3_execute_at_0100
LDX #2 ; X=2: zero 3 bytes (offsets 2,1,0)
912C LDA #0 ; A=0: zero / BRK opcode
912E .zero_0100_loop←1← 9132 BPL
STA l0100,x ; Store zero at &0100+X
9131 DEX ; Decrement byte counter
9132 BPL zero_0100_loop ; Loop until 3 bytes zeroed
9134 JSR clear_jsr_protection ; Release JSR protection mask
9137 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.

913A .lang_4_remote_validated
LDY #4 ; Y=4: validation byte offset
913C LDA (net_rx_ptr),y ; Load validation byte from RX data
913E BEQ remot1 ; Zero: validation passed, continue
9140 LDY #&80 ; Y=&80: source station offset
9142 LDA (net_rx_ptr),y ; Load source station from RX buffer
9144 LDY #&0e ; Y=&0E: controlling station offset
9146 CMP (nfs_workspace),y ; Compare with controlling station
9148 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.

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

Alternate entry into control block setup

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

9159 .ctrl_block_setup_alt←3← 8FE2 JSR← 8FF5 JSR← 90F8 JSR
LDX #&0d ; X=&0D: template offset for alt entry
915B LDY #&7c ; Y=&7C: target workspace offset for alt entry
915D BIT tx_ctrl_upper ; BIT test: V flag = bit 6 of &833A
9160 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 &918E 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

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

Control block initialisation template

Read by the loop at &9167, 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 &833A

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)

918E .ctrl_block_template←1← 9167 LDA
EQUB &85 ; Alt-path only → Y=&6F
918F EQUB &00 ; Alt-path only → Y=&70
9190 EQUB &FD ; SKIP
9191 EQUB &FD ; SKIP
9192 EQUB &7D ; → Y=&01 / Y=&73
9193 EQUB &FC ; PAGE byte → Y=&02 / Y=&74
9194 EQUB &FF ; → Y=&03 / Y=&75
9195 EQUB &FF ; → Y=&04 / Y=&76
9196 EQUB &7E ; → Y=&05 / Y=&77
9197 EQUB &FC ; PAGE byte → Y=&06 / Y=&78
9198 EQUB &FF ; → Y=&07 / Y=&79
9199 EQUB &FF ; → Y=&08 / Y=&7A
919A EQUB &00 ; → Y=&09 / Y=&7B
919B EQUB &00 ; → Y=&0A / Y=&7C
919C EQUB &FE ; STOP — main-path boundary
919D EQUB &80 ; → Y=&0C (main only)
919E EQUB &93 ; → Y=&0D (main only)
919F EQUB &FD ; SKIP (main only)
91A0 EQUB &FD ; SKIP (main only)
91A1 EQUB &D9 ; → Y=&10 (main only)
91A2 EQUB &FC ; PAGE byte → Y=&11 (main only)
91A3 EQUB &FF ; → Y=&12 (main only)
91A4 EQUB &FF ; → Y=&13 (main only)
91A5 EQUB &DE ; → Y=&14 (main only)
91A6 EQUB &FC ; PAGE byte → Y=&15 (main only)
91A7 EQUB &FF ; → Y=&16 (main only)
91A8 EQUB &FF ; → Y=&17 (main only)
91A9 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
91B5 .printer_select_handler
LDA #0 ; A=0: clear printer buffer state
91B7 DEX ; X-1: convert 1-based buffer to 0-based
91B8 CPX osword_pb_ptr ; Is this the network printer buffer?
91BA BNE setup1 ; No: skip printer init
91BC LDA #&1f ; &1F = initial buffer pointer offset
91BE STA printer_buf_ptr ; Reset printer buffer write position
91C1 LDA #&43 ; &41 = initial PFLAGS (bit 6 set, bit 0 set)
91C3 .setup1←1← 91BA BNE
STA l0d60 ; Store initial PFLAGS value
91C6 .return_printer_select←2← 91C9 BNE← 91DD 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)
91C7 .remote_print_handler
CPY #4 ; Only handle buffer 4 (network printer)
91C9 BNE return_printer_select ; Not buffer 4: ignore
91CB TXA ; A = reason code
91CC DEX ; Reason 1? (DEX: 1->0)
91CD BNE toggle_print_flag ; Not reason 1: handle Ctrl-B/C
91CF TSX ; Get stack pointer for P register
91D0 ORA l0106,x ; Force I flag in stacked P to block IRQs
91D3 STA l0106,x ; Write back modified P register
91D6 .prlp1←2← 91E5 BCC← 91EA BNE
LDA #osbyte_read_buffer ; OSBYTE &91: extract char from MOS buffer
91D8 LDX #buffer_printer ; X=3: printer buffer number
91DA JSR osbyte ; Get character from input buffer (C is set if the buffer is empty, otherwise Y=extracted character)
91DD BCS return_printer_select ; Buffer empty: return
91DF TYA ; Y = extracted character Y is the character extracted from the buffer
91E0 JSR store_output_byte ; Store char in output buffer
91E3 CPY #&6e ; Buffer nearly full? (&6E = threshold)
91E5 BCC prlp1 ; Not full: get next char
91E7 JSR flush_output_block ; Buffer full: flush to network
91EA 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
91EC .store_output_byte←2← 91E0 JSR← 91F6 JSR
LDY printer_buf_ptr ; Load current buffer offset
91EF STA (net_rx_ptr),y ; Store byte at current position
91F1 INC printer_buf_ptr ; Advance buffer pointer
91F4 RTS ; Return; Y = buffer offset
91F5 .toggle_print_flag←1← 91CD BNE
PHA ; Save reason code
91F6 JSR store_output_byte ; Decrement transfer count low byte
91F9 EOR l0d60 ; XOR with transfer count flags
91FC ROR ; Check if both bytes zero
91FD BCC rx_data_phase ; Data phase active: continue
91FF LDA l0d60 ; Load transfer count flags
9202 ROR ; Tube active: send via R3
9203 BCC rx_imm_discard ; No Tube transfer: discard
9205 ROL ; Decrement Tube count low
9206 AND #&7f ; Mask off control bits
9208 STA l0d60 ; Store updated flags
920B .rx_imm_discard←1← 9203 BCC
JSR flush_output_block ; Flush accumulated output to network
920E .rx_data_phase←1← 91FD BCC
ROR l0d60 ; Save PFLAGS bit 0 via carry
9211 PLA ; Restore original reason code
9212 ROR ; Old PFLAGS bit 0 to A bit 7
9213 ROL l0d60 ; Reason bit 0 into PFLAGS bit 0
9216 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.

9217 .flush_output_block←2← 91E7 JSR← 920B JSR
LDY #8 ; Store buffer length at workspace offset &08
9219 LDA printer_buf_ptr ; Current buffer fill position
921C STA (nfs_workspace),y ; Write to workspace offset &08
921E LDA net_rx_ptr_hi ; Store page high byte at offset &09
9220 INY ; Y=&09
9221 STA (nfs_workspace),y ; Write page high byte at offset &09
9223 LDY #5 ; Also store at offset &05
9225 STA (nfs_workspace),y ; (end address high byte)
9227 LDY #&0b ; Y=&0B: flag byte offset
9229 LDX #&26 ; X=&26: start from template entry &26
922B JSR ctrl_block_setup_clv ; Reuse ctrl_block_setup with CLV entry
922E DEY ; Y=&0A: sequence flag byte offset
922F LDA l0d60 ; Load current PFLAGS
9232 PHA ; Save current PFLAGS
9233 ROL ; Carry = current sequence (bit 7)
9234 PLA ; Restore original PFLAGS
9235 EOR #&80 ; Toggle sequence number (bit 7 of PFLAGS)
9237 STA l0d60 ; Store toggled sequence number
923A ROL ; Old sequence bit into bit 0
923B STA (nfs_workspace),y ; Store sequence flag at offset &0A
923D LDY #&1f ; Y=&1F: buffer start offset
923F STY printer_buf_ptr ; Reset printer buffer to start (&1F)
9242 LDA #0 ; A=0: printer output flag
9244 TAX ; X=0: workspace low byte X=&00
9245 LDY nfs_workspace_hi ; Y = workspace page high byte
9247 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
9248 .econet_tx_retry←2← 839B JSR← 83D3 JSR
STX net_tx_ptr ; Set TX control block ptr low byte
924A STY net_tx_ptr_hi ; Set TX control block ptr high byte
924C PHA ; Save A (handle bitmask) for later
924D AND fs_sequence_nos ; Compute sequence bit from handle
9250 BEQ bsxl1 ; Zero: no sequence bit set
9252 LDA #1 ; Non-zero: normalise to bit 0
9254 .bsxl1←1← 9250 BEQ
LDY #0 ; Y=0: flag byte offset in TXCB
9256 ORA (net_tx_ptr),y ; Merge sequence into existing flag byte
9258 PHA ; Save merged flag byte
9259 STA (net_tx_ptr),y ; Write flag+sequence to TXCB byte 0
925B JSR tx_poll_ff ; Transmit with full retry
925E LDA #&ff ; End address &FFFF = unlimited data length
9260 LDY #8 ; Y=8: end address low offset in TXCB
9262 STA (net_tx_ptr),y ; Store &FF to end addr low
9264 INY ; Y=&09
9265 STA (net_tx_ptr),y ; Store &FF to end addr high (Y=9)
9267 PLA ; Recover merged flag byte
9268 TAX ; Save in X for sequence compare
9269 LDY #&d1 ; Y=&D1: printer port number
926B PLA ; Recover saved handle bitmask
926C PHA ; Re-save for later consumption
926D BEQ bspsx ; A=0: port &D1 (print); A!=0: port &90 (FS)
926F LDY #&90 ; Y=&90: FS data port
9271 .bspsx←1← 926D BEQ
TYA ; A = selected port number
9272 LDY #1 ; Y=1: port byte offset in TXCB
9274 STA (net_tx_ptr),y ; Write port to TXCB byte 1
9276 TXA ; A = saved flag byte (expected sequence)
9277 DEY ; Y=&00
9278 PHA ; Push expected sequence for retry loop
9279 .bsxl0←1← 9285 BCS
LDA #&7f ; Flag byte &7F = waiting for reply
927B STA (net_tx_ptr),y ; Write to TXCB flag byte (Y=0)
927D JSR send_to_fs_star ; Transmit and wait for reply (BRIANX)
9280 PLA ; Recover expected sequence
9281 PHA ; Keep on stack for next iteration
9282 EOR (net_tx_ptr),y ; Check if TX result matches expected sequence
9284 ROR ; Bit 0 to carry (sequence mismatch?)
9285 BCS bsxl0 ; C=1: mismatch, retry transmit
9287 PLA ; Clean up: discard expected sequence
9288 PLA ; Discard saved handle bitmask
9289 TAX ; Transfer count to X
928A INX ; Test for retry exhaustion
928B BEQ return_bspsx ; X wrapped to 0: retries exhausted
928D EOR fs_sequence_nos ; Toggle sequence bit on success
9290 .return_bspsx←1← 928B 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 &9304 (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.

9291 .lang_2_save_palette_vdu
LDA fs_load_addr_2 ; Save current table index
9293 PHA ; Push for later restore
9294 LDA #&e9 ; Point workspace to palette save area (&E9)
9296 STA nfs_workspace ; Set workspace low byte
9298 LDY #0 ; Y=0: first palette entry
929A STY fs_load_addr_2 ; Clear table index counter
929C LDA l0350 ; Save current screen MODE to workspace
929F STA (nfs_workspace),y ; Store MODE at workspace[0]
92A1 INC nfs_workspace ; Advance workspace pointer past MODE byte
92A3 LDA l0351 ; Read colour count (from &0351)
92A6 PHA ; Push for iteration count tracking
92A7 TYA ; A=0: logical colour number for OSWORD A=&00
92A8 .save_palette_entry←1← 92C7 BNE
STA (nfs_workspace),y ; Store logical colour at workspace[0]
92AA LDX nfs_workspace ; X = workspace ptr low (param block addr)
92AC LDY nfs_workspace_hi ; Y = workspace ptr high
92AE LDA #osword_read_palette ; OSWORD &0B: read palette for logical colour
92B0 JSR osword ; Read palette
92B3 PLA ; Recover colour count
92B4 LDY #0 ; Y=0: access workspace[0]
92B6 STA (nfs_workspace),y ; Write colour count back to workspace[0]
92B8 INY ; Y=1: access workspace[1] (palette result) Y=&01
92B9 LDA (nfs_workspace),y ; Read palette value returned by OSWORD
92BB PHA ; Push palette value for next iteration
92BC LDX nfs_workspace ; X = current workspace ptr low
92BE INC nfs_workspace ; Advance workspace pointer
92C0 INC fs_load_addr_2 ; Increment table index
92C2 DEY ; Y=0 for next store Y=&00
92C3 LDA fs_load_addr_2 ; Load table index as logical colour
92C5 CPX #&f9 ; Loop until workspace wraps past &F9
92C7 BNE save_palette_entry ; Continue for all 16 palette entries
92C9 PLA ; Discard last palette value from stack
92CA STY fs_load_addr_2 ; Reset table index to 0
92CC INC nfs_workspace ; Advance workspace past palette data
92CE JSR save_vdu_state ; Save cursor pos and OSBYTE state values
92D1 INC nfs_workspace ; Advance workspace past VDU state data
92D3 PLA ; Recover saved table index
92D4 STA fs_load_addr_2 ; Restore table index
92D6 .clear_jsr_protection←4← 8E65 JSR← 9102 JMP← 9134 JSR← 9151 JSR
LDA rx_ctrl_copy ; Restore LSTAT from saved OLDJSR value
92D9 STA prot_status ; Write to protection status
92DC 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
92DD .save_vdu_state←1← 92CE JSR
LDA l0355 ; Read cursor editing state
92E0 STA (nfs_workspace),y ; Store to workspace[Y]
92E2 TAX ; Preserve in X for OSBYTE
92E3 JSR read_vdu_osbyte ; OSBYTE &85: read cursor position
92E6 INC nfs_workspace ; Advance workspace pointer
92E8 TYA ; Y result from OSBYTE &85
92E9 STA (nfs_workspace,x) ; Store Y pos to workspace (X=0)
92EB JSR read_vdu_osbyte_x0 ; Self-call trick: executes twice
92EE .read_vdu_osbyte_x0←1← 92EB JSR
LDX #0 ; X=0 for (zp,X) addressing
92F0 .read_vdu_osbyte←1← 92E3 JSR
LDY fs_load_addr_2 ; Index into OSBYTE number table
92F2 INC fs_load_addr_2 ; Next table entry next time
92F4 INC nfs_workspace ; Advance workspace pointer
92F6 LDA osbyte_vdu_table,y ; Read OSBYTE number from table
92F9 LDY #&ff ; Y=&FF: read current value
92FB JSR osbyte ; Call OSBYTE
92FE TXA ; Result in X to A
92FF LDX #0 ; X=0 for indexed indirect store
9301 STA (nfs_workspace,x) ; Store result to workspace
9303 RTS ; Return after storing result
; 3-entry OSBYTE table for lang_2_save_palette_vdu (&9291)
9304 .osbyte_vdu_table←1← 92F6 LDA
EQUB &85 ; OSBYTE &85: read cursor position
9305 EQUB &C2 ; OSBYTE &C2: read shadow RAM allocation
9306 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 R1 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).
9307 0016 .nmi_workspace_start←1← 8116 STA
.tube_brk_handler←1← 8116 STA
LDA #&ff ; A=&FF: signal error to co-processor via R4
9309 0018 JSR tube_send_r4 ; Send &FF error signal to Tube R4
930C 001B LDA tube_data_register_2 ; Flush any pending R2 byte
930F 001E LDA #0 ; A=0: send zero prefix to R2
9311 0020 .tube_send_zero_r2
JSR tube_send_r2 ; Send zero prefix byte via R2
9314 0023 TAY ; Y=0: start of error block at (&FD)
9315 0024 LDA (brk_ptr),y ; Load error number from (&FD),0
9317 0026 JSR tube_send_r2 ; Send error number via R2
931A 0029 .tube_brk_send_loop←1← 0030 BNE
INY ; Advance to next error string byte
931B 002A .tube_send_error_byte
LDA (brk_ptr),y ; Load next error string byte
931D 002C JSR tube_send_r2 ; Send error string byte via R2
9320 002F TAX ; Zero byte = end of error string
9321 0030 BNE tube_brk_send_loop ; Loop until zero terminator sent
9323 0032 .tube_reset_stack
LDX #&ff ; Reset stack pointer to top
9325 0034 TXS ; TXS: set stack pointer from X
9326 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.
9327 0036 .tube_enter_main_loop←2← 04EC JMP← 053A JMP
STX zp_temp_11 ; Save X to temporary
9329 0038 STY zp_temp_10 ; Save Y to temporary
932B 003A .tube_main_loop←7← 0048 BPL← 05AE JMP← 05D5 JMP← 0623 JMP← 0638 JMP← 06A0 JMP← 06CD JMP
BIT tube_status_1_and_tube_control ; BIT R1 status: check WRCH request
932E 003D BPL tube_poll_r2 ; R1 not ready: check R2 instead
9330 003F .tube_handle_wrch←1← 004D BMI
LDA tube_data_register_1 ; Read character from Tube R1 data
9333 0042 JSR nvwrch ; Write character
9336 0045 .tube_poll_r2←1← 003D BPL
BIT tube_status_register_2 ; BIT R2 status: check command byte
9339 0048 BPL tube_main_loop ; R2 not ready: loop back to R1 check
933B 004A BIT tube_status_1_and_tube_control ; Re-check R1: WRCH has priority over R2
933E 004D BMI tube_handle_wrch ; R1 ready: handle WRCH first
9340 004F LDX tube_data_register_2 ; Read command byte from Tube R2 data
9343 0052 STX tube_dispatch_ptr_lo ; Self-modify JMP low byte for dispatch
9345 0054 .tube_dispatch_cmd
JMP (tube_dispatch_table) ; Dispatch to handler via indirect JMP
9348 0057 .tube_transfer_addr←2← 0478 STY← 0493 STA
EQUB &00
9349 0058 .tube_xfer_page←2← 047C STA← 0498 STA
EQUB &80
934A 0059 .tube_xfer_addr_2←1← 04A2 STY
EQUB &00
934B 005A .tube_xfer_addr_3←1← 04A0 STA
EQUB &00
; Tube host code page 4 — reference: NFS12 (BEGIN, ADRR, ; SENDW)
; Copied from ROM at reloc_p4_src 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). 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
934C 0400 .tube_code_page4←1← 80FC STA
JMP tube_begin ; JMP to BEGIN startup entry
934F 0403 .tube_escape_entry
JMP tube_escape_check ; JMP to tube_escape_check (&06A7)
9352 0406 .tube_addr_claim←10← 04BC JSR← 04E4 JMP← 8B1D JSR← 8B2F JSR← 8B8C JSR← 8DA9 JMP← 99F0 JSR← 9A3D JSR← 9F98 JSR← 9FA0 JSR
CMP #&80 ; A>=&80: address claim; A<&80: data transfer
9354 0408 BCC setup_data_transfer ; A<&80: data transfer setup (SENDW)
9356 040A CMP #&c0 ; A>=&C0: new address claim from another host
9358 040C BCS addr_claim_external ; C=1: external claim, check ownership
935A 040E ORA #&40 ; Map &80-&BF range to &C0-&FF for comparison
935C 0410 CMP tube_claimed_id ; Is this for our currently-claimed address?
935E 0412 BNE return_tube_init ; Not our address: return
9360 0414 .tube_post_init←1← 810E JSR
LDA #&80 ; &80 sentinel: clear address claim
9362 0416 STA tube_claim_flag ; Store to claim-in-progress flag
9364 0418 RTS ; Return from tube_post_init
9365 0419 .addr_claim_external←1← 040C BCS
ASL tube_claim_flag ; Another host claiming; check if we're owner
9367 041B BCS accept_new_claim ; C=1: we have an active claim
9369 041D CMP tube_claimed_id ; Compare with our claimed address
936B 041F BEQ return_tube_init ; Match: return (we already have it)
936D 0421 CLC ; Not ours: CLC = we don't own this address
936E 0422 RTS ; Return with C=0 (claim denied)
936F 0423 .accept_new_claim←1← 041B BCS
STA tube_claimed_id ; Accept new claim: update our address
9371 0425 .return_tube_init←2← 0412 BNE← 041F BEQ
RTS ; Return with address updated
9372 0426 .setup_data_transfer←1← 0408 BCC
STY tube_data_ptr_hi ; Save 16-bit transfer address from (X,Y)
9374 0428 STX tube_data_ptr ; Store address pointer low byte
9376 042A JSR tube_send_r4 ; Send transfer type byte to co-processor
9379 042D TAX ; X = transfer type for table lookup
937A 042E LDY #3 ; Y=3: send 4 bytes (address + claimed addr)
937C 0430 .send_xfer_addr_bytes←1← 0436 BPL
LDA (tube_data_ptr),y ; Load transfer address byte from (X,Y)
937E 0432 JSR tube_send_r4 ; Send address byte to co-processor via R4
9381 0435 DEY ; Previous byte (big-endian: 3,2,1,0)
9382 0436 BPL send_xfer_addr_bytes ; Loop for all 4 address bytes
9384 0438 LDY #8 ; Y=8: write to Tube control register
9386 043A STY tube_status_1_and_tube_control ; Configure Tube for data transfer
9389 043D LDY #&10 ; Y=&10: data transfer control value
938B 043F CPX #2 ; Check transfer type (X=2?)
938D 0441 BCC tube_ctrl_write_2 ; X<2: skip alternate control
938F 0443 LDY #&90 ; Y=&90: alternate control for X>=2
9391 0445 .tube_ctrl_write_2←1← 0441 BCC
STY tube_status_1_and_tube_control ; Write transfer control to Tube
9394 0448 JSR tube_send_r4 ; Send data byte via Tube R4
9397 044B LDY #&88 ; Y=&88: post-transfer control value
9399 044D TXA ; Transfer type to A for comparison
939A 044E BEQ flush_r3_nmi_check ; Type 0: go to NMI flush check
939C 0450 CMP #2 ; Check if type 2
939E 0452 BEQ flush_r3_nmi_check ; Type 2: go to NMI flush check
93A0 0454 STY tube_status_1_and_tube_control ; Write post-transfer control
93A3 0457 CMP #4 ; Check if type 4 (SENDW)
93A5 0459 BNE return_tube_xfer ; Not SENDW type: skip release path
93A7 045B PLA ; Discard return address (low byte)
93A8 045C PLA ; Discard return address (high byte)
93A9 045D .release_claim_restart←1← 04B8 BEQ
LDA #&80 ; A=&80: reset claim flag sentinel
93AB 045F STA tube_claim_flag ; Clear claim-in-progress flag
93AD 0461 JMP tube_reply_byte ; Restart Tube main loop
93B0 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
93B3 0467 BVC flush_r3_nmi_check ; V=0: not ready, poll again
93B5 0469 BIT tube_data_register_3 ; Flush Tube R3 data register
93B8 046C BIT tube_data_register_3 ; Flush Tube R3 again
93BB 046F STY tube_status_1_and_tube_control ; Write final control value
93BE 0472 .return_tube_xfer←1← 0459 BNE
RTS ; Return from Tube data setup
93BF 0473 .tube_begin←1← 0400 JMP
CLI ; BEGIN: enable interrupts for Tube host code
93C0 0474 PHP ; Save processor status
93C1 0475 PHA ; Save A on stack
93C2 0476 LDY #0 ; Y=0: start at beginning of page
93C4 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.
93C6 047A .tube_init_reloc
LDA #&80 ; Init: start sending from &8000
93C8 047C STA tube_xfer_page ; Store &80 as source page high byte
93CA 047E STA zp_ptr_hi ; Store &80 as page counter initial value
93CC 0480 LDA #&20 ; A=&20: bit 5 mask for ROM type check
93CE 0482 AND rom_type ; ROM type bit 5: reloc address in header?
93D1 0485 BEQ store_xfer_end_addr ; No reloc addr: use defaults
93D3 0487 LDX copyright_offset ; Skip past copyright string to find reloc addr
93D6 048A .scan_copyright_end←1← 048E BNE
INX ; Skip past null-terminated copyright string
93D7 048B LDA rom_header,x ; Load next byte from ROM header
93DA 048E BNE scan_copyright_end ; Loop until null terminator found
93DC 0490 LDA lang_entry_lo,x ; Read 4-byte reloc address from ROM header
93DF 0493 STA tube_transfer_addr ; Store reloc addr byte 1 as transfer addr
93E1 0495 LDA lang_entry_hi,x ; Load reloc addr byte 2
93E4 0498 STA tube_xfer_page ; Store as source page start
93E6 049A LDY service_entry,x ; Load reloc addr byte 3
93E9 049D LDA svc_entry_lo,x ; Load reloc addr byte 4 (highest)
93EC 04A0 .store_xfer_end_addr←1← 0485 BEQ
STA tube_xfer_addr_3 ; Store high byte of end address
93EE 04A2 STY tube_xfer_addr_2 ; Store byte 3 of end address
93F0 04A4 PLA ; Restore A from stack
93F1 04A5 PLP ; Restore processor status
93F2 04A6 BCS beginr ; Carry set: language entry (claim Tube)
93F4 04A8 TAX ; X = A (preserved from entry)
93F5 04A9 BNE begink ; Non-zero: check break type
93F7 04AB JMP tube_reply_ack ; A=0: acknowledge and return
93FA 04AE .begink←1← 04A9 BNE
LDX #0 ; X=0 for OSBYTE read
93FC 04B0 LDY #&ff ; Y=&FF for OSBYTE read
93FE 04B2 LDA #osbyte_read_write_last_break_type ; OSBYTE &FD: read last break type
9400 04B4 JSR osbyte ; Read type of last reset
9403 04B7 TXA ; X=value of type of last reset
9404 04B8 BEQ release_claim_restart ; Soft break (0): skip ROM transfer
9406 04BA .beginr←2← 04A6 BCS← 04BF BCC
LDA #&ff ; A=&FF: claim Tube for all operations
9408 04BC JSR tube_addr_claim ; Claim Tube address via R4
940B 04BF BCC beginr ; Not claimed: retry until claimed
940D 04C1 LDA #1 ; Transfer type 1 (parasite to host)
940F 04C3 JSR tube_setup_transfer ; Set up Tube transfer parameters
9412 04C6 LDY #0 ; Y=0: start at page boundary
9414 04C8 STY zp_ptr_lo ; Source ptr low = 0
9416 04CA LDX #&40 ; X=&40: 64 pages (16KB) to transfer
9418 04CC .send_rom_byte←2← 04D7 BNE← 04DC BNE
LDA (zp_ptr_lo),y ; Read byte from source address
941A 04CE STA tube_data_register_3 ; Send byte to Tube via R3
941D 04D1 .poll_r3_ready←1← 04D4 BVC
BIT tube_status_register_3 ; Check R3 status
9420 04D4 BVC poll_r3_ready ; Not ready: wait for Tube
9422 04D6 INY ; Next byte in page
9423 04D7 BNE send_rom_byte ; More bytes in page: continue
9425 04D9 INC zp_ptr_hi ; Next source page
9427 04DB DEX ; Decrement page counter
9428 04DC BNE send_rom_byte ; More pages: continue transfer
942A 04DE LDA #4 ; Transfer type 4 (host to parasite burst)
942C 04E0 .tube_setup_transfer←1← 04C3 JSR
LDY #0 ; Y=0: low byte of param block ptr
942E 04E2 LDX #&57 ; X=&57: param block at &0057
9430 04E4 JMP tube_addr_claim ; Claim Tube and start transfer
9433 04E7 .tube_rdch_handler
LDA #1 ; R2 command: OSRDCH request
9435 04E9 JSR tube_send_r2 ; Send OSRDCH request to host
9438 04EC JMP tube_enter_main_loop ; Jump to RDCH completion handler
943B 04EF .tube_restore_regs
LDY zp_temp_10 ; Restore saved Y register
943D 04F1 LDX zp_temp_11 ; Restore X from saved value
943F 04F3 JSR tube_read_r2 ; Read result byte from R2
9442 04F6 ASL ; Shift carry into C flag
9443 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
9446 04FA BPL tube_read_r2 ; Bit 7 clear: R2 not ready, wait
9448 04FC LDA tube_data_register_2 ; Read byte from R2 data register
944B 04FF RTS ; Return with pointers initialised
; Tube host code page 5 — reference: NFS13 (TASKS, ; BPUT-FILE)
; Copied from ROM at reloc_p4_src+&100 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_r1_wrch — service R1 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
944C 0500 .tube_dispatch_table←2← 0054 JMP← 8102 STA
EQUW tube_osrdch ; cmd 0: OSRDCH
944E 0502 EQUW tube_oscli ; cmd 1: OSCLI
9450 0504 EQUW tube_osbyte_short ; cmd 2: OSBYTE (2-param)
9452 0506 EQUW tube_osbyte_long ; cmd 3: OSBYTE (3-param)
9454 0508 EQUW tube_osword ; cmd 4: OSWORD
9456 050A EQUW tube_osword_rdln ; cmd 5: OSWORD 0 (read line)
9458 050C EQUW tube_restore_regs ; cmd 6: release/restore regs
945A 050E EQUW tube_release_return ; cmd 7: restore regs, RTS
945C 0510 EQUW tube_osargs ; cmd 8: OSARGS
945E 0512 EQUW tube_osbget ; cmd 9: OSBGET
9460 0514 EQUW tube_osbput ; cmd 10: OSBPUT
9462 0516 EQUW tube_osfind ; cmd 11: OSFIND
9464 0518 EQUW tube_osfile ; cmd 12: OSFILE
9466 051A EQUW tube_osgbpb ; cmd 13: OSGBPB
9468 051C .tube_wrch_handler
PHA ; Save character for WRCH echo
9469 051D LDA #0 ; A=0: send null prefix via R2
946B 051F .tube_send_and_poll
JSR tube_send_r2 ; Send prefix byte to co-processor
946E 0522 .poll_r2_reply←2← 052A BPL← 0532 JMP
BIT tube_status_register_2 ; Poll R2 for co-processor reply
9471 0525 BVS wrch_echo_reply ; R2 ready: go process reply
9473 0527 .tube_poll_r1_wrch
BIT tube_status_1_and_tube_control ; Check R1 for pending WRCH request
9476 052A BPL poll_r2_reply ; No R1 data: back to polling R2
9478 052C LDA tube_data_register_1 ; Read WRCH character from R1
947B 052F JSR nvwrch ; Write character
947E 0532 .tube_resume_poll
JMP poll_r2_reply ; Resume R2 polling after servicing
9481 0535 .wrch_echo_reply←1← 0525 BVS
PLA ; Recover original character
9482 0536 STA tube_data_register_2 ; Echo character back via R2
9485 0539 PHA ; Push for dispatch loop re-entry
9486 053A JMP tube_enter_main_loop ; Enter main dispatch loop
9489 053D .tube_release_return
LDX zp_temp_11 ; Restore saved X
948B 053F LDY zp_temp_10 ; Restore saved Y from temporary
948D 0541 PLA ; Restore saved A
948E 0542 RTS ; Return to caller
948F 0543 .tube_osbput
JSR tube_read_r2 ; Read file handle from R2
9492 0546 TAY ; Y=channel handle from R2
9493 0547 JSR tube_read_r2 ; Read data byte from R2 for BPUT
9496 054A JSR osbput ; Write a single byte A to an open file Y
9499 054D JMP tube_reply_ack ; BPUT done: send acknowledge, return
949C 0550 .tube_osbget
JSR tube_read_r2 ; Read file handle from R2
949F 0553 TAY ; Y=channel handle for OSBGET Y=file handle
94A0 0554 JSR osbget ; Read a single byte from an open file Y
94A3 0557 PHA ; Save byte read from file
94A4 0558 JMP send_reply_ok ; Send carry+byte reply (BGET result)
94A7 055B .tube_osrdch
JSR nvrdch ; Read a character from the current input stream
94AA 055E PHA ; A=character read
94AB 055F .send_reply_ok←1← 0558 JMP
ORA #&80 ; Set bit 7 (no-error flag)
94AD 0561 .tube_rdch_reply
ROR ; ROR A: encode carry (error flag) into bit 7
94AE 0562 JSR tube_send_r2 ; = JSR tube_send_r2 (overlaps &053D entry)
94B1 0565 PLA ; Restore read character/byte
94B2 0566 JMP tube_reply_byte ; Return to Tube main loop
94B5 0569 .tube_osfind
JSR tube_read_r2 ; Read open mode from R2
94B8 056C BEQ tube_osfind_close ; A=0: close file, else open with filename
94BA 056E PHA ; Save open mode while reading filename
94BB 056F JSR tube_read_string ; Read filename string from R2 into &0700
94BE 0572 PLA ; Recover open mode from stack
94BF 0573 JSR osfind ; Open or close file(s)
94C2 0576 PHA ; Save file handle result
94C3 0577 LDA #&ff ; A=&FF: success marker
94C5 0579 JSR tube_send_r2 ; Send success marker via R2
94C8 057C PLA ; Restore file handle
94C9 057D JMP tube_reply_byte ; Send file handle result to co-processor
94CC 0580 .tube_osfind_close←1← 056C BEQ
JSR tube_read_r2 ; OSFIND close: read handle from R2
94CF 0583 TAY ; Y=handle to close
94D0 0584 LDA #osfind_close ; A=0: close command for OSFIND
94D2 0586 JSR osfind ; Close one or all files
94D5 0589 JMP tube_reply_ack ; Close done: send acknowledge, return
94D8 058C .tube_osargs
JSR tube_read_r2 ; Read file handle from R2
94DB 058F TAY ; Y=file handle for OSARGS
94DC 0590 .tube_read_params
LDX #3 ; Read 4-byte arg + reason from R2 into ZP
94DE 0592 .read_osargs_params←1← 0598 BPL
JSR tube_read_r2 ; Read next param byte from R2
94E1 0595 STA zp_ptr_lo,x ; Params stored at &00-&03 (little-endian)
94E3 0597 DEX ; Decrement byte counter
94E4 0598 BPL read_osargs_params ; Loop until all 4 bytes read
94E6 059A INX ; X=0: reset index after loop
94E7 059B JSR tube_read_r2 ; Read OSARGS reason code from R2
94EA 059E JSR osargs ; Read or write a file's attributes
94ED 05A1 JSR tube_send_r2 ; Send result A back to co-processor
94F0 05A4 LDX #3 ; Return 4-byte result from ZP &00-&03
94F2 05A6 .send_osargs_result←1← 05AC BPL
LDA zp_ptr_lo,x ; Load result byte from zero page
94F4 05A8 JSR tube_send_r2 ; Send byte to co-processor via R2
94F7 05AB DEX ; Previous byte (count down)
94F8 05AC BPL send_osargs_result ; Loop for all 4 bytes
94FA 05AE JMP tube_main_loop ; Return to Tube main loop
94FD 05B1 .tube_read_string←3← 056F JSR← 05C5 JSR← 05E2 JSR
LDX #0 ; X=0: initialise string buffer index
94FF 05B3 LDY #0 ; Y=0: string buffer offset 0
9501 05B5 .strnh←1← 05C0 BNE
JSR tube_read_r2 ; Read next string byte from R2
9504 05B8 STA l0700,y ; Store byte in string buffer at &0700+Y
9507 05BB INY ; Next buffer position
9508 05BC BEQ string_buf_done ; Y overflow: string too long, truncate
950A 05BE CMP #&0d ; Check for CR terminator
950C 05C0 BNE strnh ; Not CR: continue reading string
950E 05C2 .string_buf_done←1← 05BC BEQ
LDY #7 ; Y=7: set XY=&0700 for OSCLI/OSFIND
9510 05C4 RTS ; Return with XY pointing to &0700
9511 05C5 .tube_oscli
JSR tube_read_string ; Read command string into &0700
9514 05C8 JSR oscli ; Execute * command via OSCLI
9517 05CB .tube_reply_ack←3← 04AB JMP← 054D JMP← 0589 JMP
LDA #&7f ; &7F = success acknowledgement
9519 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
951C 05D0 BVC tube_reply_byte ; Bit 6 clear: not ready, loop
951E 05D2 STA tube_data_register_2 ; Write byte to R2 data register
9521 05D5 .mj←1← 0600 BEQ
JMP tube_main_loop ; Return to Tube main loop
9524 05D8 .tube_osfile
LDX #&10 ; X=&10: read 16-byte control block
9526 05DA .argsw←1← 05E0 BNE
JSR tube_read_r2 ; Read next control block byte from R2
9529 05DD STA zp_ptr_hi,x ; Store at &01+X (descending)
952B 05DF DEX ; Decrement byte counter
952C 05E0 BNE argsw ; Loop for all 16 bytes
952E 05E2 JSR tube_read_string ; Read filename string from R2 into &0700
9531 05E5 STX zp_ptr_lo ; XY=&0700: filename pointer for OSFILE
9533 05E7 STY zp_ptr_hi ; Store Y=7 as pointer high byte
9535 05E9 LDY #0 ; Y=0 for OSFILE control block offset
9537 05EB JSR tube_read_r2 ; Read OSFILE reason code from R2
953A 05EE JSR osfile ; Execute OSFILE operation
953D 05F1 ORA #&80 ; Set bit 7: mark result as present
953F 05F3 JSR tube_send_r2 ; Send result A (object type) to co-processor
9542 05F6 LDX #&10 ; Return 16-byte control block to co-processor
9544 05F8 .send_osfile_ctrl_blk←1← 05FE BNE
LDA zp_ptr_hi,x ; Load control block byte
9546 05FA JSR tube_send_r2 ; Send byte to co-processor via R2
9549 05FD DEX ; Decrement byte counter
954A 05FE BNE send_osfile_ctrl_blk ; Loop for all 16 bytes
; Tube host code page 6 — reference: NFS13 (GBPB-ESCA)
; Copied from ROM at reloc_p4_src+&200 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
954C 0600 .tube_code_page6←1← 8108 STA
BEQ mj ; OSGBPB done: return to main loop
954E 0602 .tube_osgbpb
LDX #&0c ; X=&0C: read 13-byte param block
9550 0604 .read_gbpb_params←1← 060A BPL
JSR tube_read_r2 ; Read param byte from Tube R2
9553 0607 STA zp_ptr_lo,x ; Store in zero page param block
9555 0609 DEX ; Next byte (descending)
9556 060A BPL read_gbpb_params ; Loop until all 13 bytes read
9558 060C JSR tube_read_r2 ; Read A (OSGBPB function code)
955B 060F INX ; X=0 after loop
955C 0610 LDY #0 ; Y=0 for OSGBPB call
955E 0612 JSR osgbpb ; Read or write multiple bytes to an open file
9561 0615 ROR ; Encode carry into result bit 7
9562 0616 JSR tube_send_r2 ; Send carry+result byte via R2
9565 0619 LDX #&0c ; X=12: send 13 updated param bytes
9567 061B .send_gbpb_params←1← 0621 BPL
LDA zp_ptr_lo,x ; Load updated param byte
9569 061D JSR tube_send_r2 ; Send param byte via R2
956C 0620 DEX ; Next byte (descending)
956D 0621 BPL send_gbpb_params ; Loop until all 13 bytes sent
956F 0623 JMP tube_main_loop ; Return to main event loop
9572 0626 .tube_osbyte_short
JSR tube_read_r2 ; Read X parameter from R2
9575 0629 TAX ; Save in X
9576 062A JSR tube_read_r2 ; Read A (OSBYTE function code)
9579 062D JSR osbyte ; Execute OSBYTE A,X
957C 0630 .tube_osbyte_send_x←2← 0633 BVC← 065B BVS
BIT tube_status_register_2 ; Poll R2 status (bit 6 = ready)
957F 0633 BVC tube_osbyte_send_x ; Not ready: keep polling
9581 0635 STX tube_data_register_2 ; Send X result for 2-param OSBYTE
9584 0638 .bytex←1← 064B BEQ
JMP tube_main_loop ; Return to main event loop
9587 063B .tube_osbyte_long
JSR tube_read_r2 ; Read X parameter from R2
958A 063E TAX ; Save in X
958B 063F JSR tube_read_r2 ; Read Y parameter from co-processor
958E 0642 TAY ; Save in Y
958F 0643 JSR tube_read_r2 ; Read A (OSBYTE function code)
9592 0646 JSR osbyte ; Execute OSBYTE A,X,Y
9595 0649 EOR #&9d ; Test for OSBYTE &9D (fast Tube BPUT)
9597 064B BEQ bytex ; OSBYTE &9D (fast Tube BPUT): no result needed
9599 064D LDA #&40 ; A=&40: high bit will hold carry
959B 064F ROR ; Encode carry (error flag) into bit 7
959C 0650 JSR tube_send_r2 ; Send carry+status byte via R2
959F 0653 .tube_osbyte_send_y←1← 0656 BVC
BIT tube_status_register_2 ; Poll R2 status for ready
95A2 0656 BVC tube_osbyte_send_y ; Not ready: keep polling
95A4 0658 STY tube_data_register_2 ; Send Y result, then fall through to send X
95A7 065B BVS tube_osbyte_send_x ; ALWAYS branch
95A9 065D .tube_osword
JSR tube_read_r2 ; Read OSWORD number from R2
95AC 0660 TAY ; Save OSWORD number in Y
95AD 0661 .tube_osword_read←1← 0664 BPL
BIT tube_status_register_2 ; Poll R2 status for data ready
95B0 0664 BPL tube_osword_read ; Not ready: keep polling
95B2 0666 LDX tube_data_register_2 ; Read param block length from R2
95B5 0669 DEX ; DEX: length 0 means no params to read
95B6 066A BMI skip_param_read ; No params (length=0): skip read loop
95B8 066C .tube_osword_read_lp←2← 066F BPL← 0678 BPL
BIT tube_status_register_2 ; Poll R2 status for data ready
95BB 066F BPL tube_osword_read_lp ; Not ready: keep polling
95BD 0671 LDA tube_data_register_2 ; Read param byte from R2
95C0 0674 STA l0130,x ; Store param bytes into block at &0130
95C3 0677 DEX ; Next param byte (descending)
95C4 0678 BPL tube_osword_read_lp ; Loop until all params read
95C6 067A TYA ; Restore OSWORD number from Y
95C7 067B .skip_param_read←1← 066A BMI
LDX #<(l0130) ; XY=&0130: param block address for OSWORD
95C9 067D LDY #>(l0130) ; Y=&01: param block at &0130
95CB 067F JSR osword ; Execute OSWORD with XY=&0130
95CE 0682 LDA #&ff ; A=&FF: result marker for co-processor
95D0 0684 JSR tube_send_r2 ; Send result marker via R2
95D3 0687 .poll_r2_osword_result←1← 068A BPL
BIT tube_status_register_2 ; Poll R2 status for ready
95D6 068A BPL poll_r2_osword_result ; Not ready: keep polling
95D8 068C LDX tube_data_register_2 ; Read result block length from R2
95DB 068F DEX ; Decrement result byte counter
95DC 0690 BMI tube_return_main ; No results to send: return to main loop
95DE 0692 .tube_osword_write←1← 069E BPL
LDY l0130,x ; Send result block bytes from &0128 via R2
95E1 0695 .tube_osword_write_lp←1← 0698 BVC
BIT tube_status_register_2 ; Poll R2 status for ready
95E4 0698 BVC tube_osword_write_lp ; Not ready: keep polling
95E6 069A STY tube_data_register_2 ; Send result byte via R2
95E9 069D DEX ; Next result byte (descending)
95EA 069E BPL tube_osword_write ; Loop until all results sent
95EC 06A0 .tube_return_main←1← 0690 BMI
JMP tube_main_loop ; Return to main event loop
95EF 06A3 .tube_osword_rdln
LDX #4 ; X=4: read 5-byte RDLN ctrl block
95F1 06A5 .read_rdln_ctrl_block←1← 06AB BPL
JSR tube_read_r2 ; Read control block byte from R2
95F4 06A8 STA zp_ptr_lo,x ; Store in zero page params
95F6 06AA DEX ; Next byte (descending)
95F7 06AB BPL read_rdln_ctrl_block ; Loop until all 5 bytes read
95F9 06AD INX ; X=0 after loop, A=0 for OSWORD 0 (read line)
95FA 06AE LDY #0 ; Y=0 for OSWORD 0
95FC 06B0 TXA ; A=0: OSWORD 0 (read line)
95FD 06B1 JSR osword ; Read input line from keyboard
9600 06B4 BCC tube_rdln_send_line ; C=0: line read OK; C=1: escape pressed
9602 06B6 LDA #&ff ; &FF = escape/error signal to co-processor
9604 06B8 JMP tube_reply_byte ; Escape: send &FF error to co-processor
9607 06BB .tube_rdln_send_line←1← 06B4 BCC
LDX #0 ; X=0: start of input buffer at &0700
9609 06BD LDA #&7f ; &7F = line read successfully
960B 06BF JSR tube_send_r2 ; Send &7F (success) to co-processor
960E 06C2 .tube_rdln_send_loop←1← 06CB BNE
LDA l0700,x ; Load char from input buffer
9611 06C5 .tube_rdln_send_byte
JSR tube_send_r2 ; Send char to co-processor
9614 06C8 INX ; Next character
9615 06C9 CMP #&0d ; Check for CR terminator
9617 06CB BNE tube_rdln_send_loop ; Loop until CR terminator sent
9619 06CD JMP tube_main_loop ; Return to main event loop
961C 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)
961F 06D3 BVC tube_send_r2 ; Not ready: keep polling
9621 06D5 STA tube_data_register_2 ; Write A to Tube R2 data register
9624 06D8 RTS ; Return to caller
9625 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)
9628 06DC BVC tube_send_r4 ; Not ready: keep polling
962A 06DE STA tube_data_register_4 ; Write A to Tube R4 data register
962D 06E1 RTS ; Return to caller
962E 06E2 .tube_escape_check←1← 0403 JMP
LDA escape_flag ; Check OS escape flag at &FF
9630 06E4 SEC ; SEC+ROR: put bit 7 of &FF into carry+bit 7
9631 06E5 ROR ; ROR: shift escape bit 7 to carry
9632 06E6 BMI tube_send_r1 ; Escape set: forward to co-processor via R1
9634 06E8 .tube_event_handler
PHA ; EVNTV: forward event A, Y, X to co-processor
9635 06E9 LDA #0 ; Send &00 prefix (event notification)
9637 06EB JSR tube_send_r1 ; Send zero prefix via R1
963A 06EE TYA ; Y value for event
963B 06EF JSR tube_send_r1 ; Send Y via R1
963E 06F2 TXA ; X value for event
963F 06F3 JSR tube_send_r1 ; Send X via R1
9642 06F6 PLA ; Restore A (event type)
9643 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)
9646 06FA BVC tube_send_r1 ; Not ready: keep polling
9648 06FC STA tube_data_register_1 ; Write A to Tube R1 data register
964B 06FF RTS ; Return to caller
964C EQUB &60, &FF, &42, &FF, &00, &FF, &77, &FF, &FF, &FF, &DF, &FF, &00, &FF, &00, &FF, &00, &FF, &04, &FF
9660 .trampoline_tx_setup←2← 8667 JSR← 8E49 JSR
JMP tx_begin ; Trampoline: forward to tx_begin
9663 .trampoline_adlc_init←1← 82C9 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 from RXCB 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 ; Load saved tx_in_progress from RXCB Y=&0a
96C5 LDA (net_rx_ptr),y ; Load saved TX-in-progress flag
96C7 STA tx_in_progress ; Restore TX-in-progress status
96CA JMP adlc_init_workspace ; Re-initialize ADLC and NMI

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 .poll_nmi_complete←1← 96D8 BNE
LDA nmi_shim_rom_src,y ; Load NMI shim byte from ROM table
96D4 STA l0cff,y ; Store to NMI area at &0D00+Y
96D7 DEY ; Decrement byte counter
96D8 BNE poll_nmi_complete ; 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 final buffer offset
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 &97DC
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 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 &9843
9879 LDY #&98 ; High byte of &9843 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 ; Next field
9A52 CPY #&0c ; All 8 fields copied?
9A54 BNE copy_scout_loop ; No page crossing
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 &9A as dispatch high byte
9A87 LDA inc_rxcb_buf_hi,y ; Load handler low byte from jump table
9A8A PHA ; Push handler low byte
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 just below the screen (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. sr2_test_operand is the self-modifying target for patching the SR2 test. 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 ; Store error and return to idle

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 &9DE3 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 ; Load 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_imm_data 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 ; Transmit 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 ↓