Acorn NFS 3.35K

Updated 31 Mar 2026

← All Acorn NFS and Acorn ANFS versions

; Sideways ROM header
; NFS ROM 3.35K disassembly (Acorn Econet filing system)
; =====================================================
8000 .rom_header←2← 048B LDA← 9BC7 JSR
.language_entry←2← 048B LDA← 9BC7 JSR
.pydis_start←2← 048B LDA← 9BC7 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← 81D4 CMP← 81DD 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_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← 84D0 LDY
EQUB &00 ; "Line Jammed"
8015 EQUB &0D ; "Net Error"
8016 EQUB &18 ; "Not listening"
8017 EQUB &27 ; "No Clock"
8018 EQUB &31 ; "Escape"
8019 EQUB &31 ; "Escape"
801A EQUB &31 ; "Escape"
801B EQUB &39 ; "Bad Option"
801C EQUB &45 ; "No reply"
; Four bytes with unknown purpose.
801D EQUB &01 ; Purpose unknown
801E EQUB &00 ; Purpose unknown
801F EQUB &35 ; 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 &80DA.
; 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_2_star_run-1) ; lo - FSCV 2: */ (run)
8036 EQUB <(fscv_3_star_cmd-1) ; lo - FSCV 3: unrecognised star command
8037 EQUB <(fscv_2_star_run-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
; &80DA.
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_2_star_run-1) ; hi - FSCV 2: */ (run)
805A EQUB >(fscv_3_star_cmd-1) ; hi - FSCV 3: unrecognised star command
805B EQUB >(fscv_2_star_run-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 &80DA. 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 (&8E3B): read file handle from received packet (net_1_read_handle)

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

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

*NET4 (&8180): 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 LDA #0 ; Clear &A9 (used by dispatch)
8076 STA rom_svc_num ; Store zero to &A9
8078 TYA ; Preserve A before dispatch
8079 LDY #&20 ; Y=&20: base offset for *NET commands (index 33+)
807B BNE dispatch ; ALWAYS branch to dispatch ALWAYS branch
807D .skip_iam_spaces←1← 8082 BEQ
INY ; Advance past matched command text
fall through ↓

"I AM" command handler

Dispatched from the command match table when the user types "*I AM <station>" or "*I AM <network>.<station>". Also used as the station number parser for "*NET <network>.<station>". Skips leading 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). If a colon follows, reads interactive input via OSRDCH and appends it to the command buffer. Finally jumps to forward_star_cmd.

807E .i_am_handler
LDA (fs_options),y ; Load next char from command line
8080 CMP #&20 ; Skip spaces
8082 BEQ skip_iam_spaces ; Loop back to skip leading spaces
8084 CMP #&41 ; Colon = interactive remote command prefix
8086 BCS scan_for_colon ; Char >= ':': skip number parsing
8088 LDA #0 ; A=0: default network number
808A JSR parse_decimal ; Parse decimal number from (fs_options),Y (DECIN)
808D BCC store_station_net ; C=1: dot found, first number was network
808F INY ; Y=offset into (fs_options) buffer
8090 JSR parse_decimal ; Parse decimal number from (fs_options),Y (DECIN)
8093 .store_station_net←1← 808D BCC
STA fs_server_stn ; A=parsed value (accumulated in &B2)
8096 STX fs_server_net ; X=initial A value (saved by TAX)
8099 .scan_for_colon←2← 8086 BCS← 80A2 BNE
INY ; Skip past current character
809A LDA (fs_options),y ; Load next character from cmd line
809C CMP #&0d ; CR: end of command string?
809E BEQ forward_star_cmd ; Y=0: no colon found, send command
80A0 CMP #&3a ; Test for colon separator
80A2 BNE scan_for_colon ; Not colon: keep scanning backward
80A4 JSR oswrch ; Echo colon, then read user input from keyboard Write character
80A7 .read_remote_cmd_line←1← 80AF BNE
JSR osrdch ; Check for escape condition Read a character from the current input stream
80AA STA (fs_options),y ; A=character read
80AC INY ; Advance write pointer
80AD CMP #&0d ; Test for CR (end of line)
80AF BNE read_remote_cmd_line ; Not CR: continue reading input
80B1 JSR osnewl ; Write newline (characters 10 and 13)
fall through ↓

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

80B4 .forward_star_cmd←1← 809E BEQ
JSR copy_filename ; Copy command text to FS buffer
80B7 TAY ; Y=function code for HDRFN
80B8 .prepare_cmd_dispatch
JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
80BB LDX fs_cmd_csd ; X=depends on function
80BE BEQ return_1 ; CSD handle zero: not logged in
80C0 LDA fs_cmd_data ; A=function code (0-7)
80C3 LDY #&16 ; Y=depends on function
80C5 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)
80C7 .fscv_handler
JSR save_fscv_args_with_ptrs ; Store A/X/Y in FS workspace
80CA CMP #8 ; FSCV function >= 8?
80CC BCS return_1 ; Function code >= 8? Return (unsupported)
80CE TAX ; X = function code for dispatch
80CF TYA ; Save Y (command text ptr hi)
80D0 LDY #&12 ; Y=&12: base offset for FSCV dispatch (indices 19+)
80D2 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).

80D4 .language_handler←1← 8000 JMP
.lang_entry_dispatch←1← 8000 JMP
CPX #5 ; X >= 5: invalid reason code, return
80D6 .svc_dispatch_range
BCS return_1 ; Out of range: return via RTS
80D8 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.

80DA .dispatch←5← 807B BNE← 80C5 BNE← 80D2 BNE← 80DC BPL← 8160 JSR
INX ; Add base offset Y to index X (loop: X += Y+1)
80DB DEY ; Decrement base offset counter
80DC BPL dispatch ; Loop until Y exhausted
80DE TAY ; Y=&FF (no further use)
80DF LDA dispatch_0_hi-1,x ; Load high byte of (handler - 1) from table
80E2 PHA ; Push high byte onto stack
80E3 LDA dispatch_0_lo-1,x ; Load low byte of (handler - 1) from table
80E6 PHA ; Push low byte onto stack
80E7 LDX fs_options ; Restore X (fileserver options) for use by handler
80E9 .return_1←6← 806D BMI← 8071 BCS← 80BE BEQ← 80CC BCS← 80D6 BCS← 80F2 BEQ
RTS ; RTS pops address, adds 1, jumps to handler

Service handler entry

Intercepts three service calls before normal dispatch: &FE: Tube init -- explode character definitions &FF: Full init -- vector setup, copy code to RAM, select NFS &12 (Y=5): Select NFS as active filing system All other service calls < &0D dispatch via c8146. 3.35K removes the per-ROM disable flag check that 3.35D has.

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

NFS initialisation (service &FF: full reset)

New in 3.35D: table-driven vector initialisation replaces the hardcoded LDA/STA pairs of 3.34B. Reads 4 triplets from the data table at &816D (low byte, high byte, vector offset) and stores each 16-bit value at &0200+offset: EVNTV (&0220) = &06E8 BRKV (&0202) = &0016 RDCHV (&0210) = &04E7 WRCHV (&020E) = &051C Then writes &8E to Tube control register (&FEE0) and copies 3 pages of Tube host code from ROM (reloc_p4_src et seq.) to RAM (&0400/&0500/&0600), calls tube_post_init (&0414), and copies 97 bytes of workspace init from ROM (reloc_zp_src) to &0016-&0076.

8103 .init_vectors_and_copy←1← 80EE BNE
STY zp_temp_10 ; Save Y (ROM number) for later
8105 LDY #&0c ; Y=12: 4 triplets x 3 bytes each
8107 .init_vector_loop←1← 8119 BNE
LDX return_2,y ; Load vector offset from table
810A DEY ; Previous table byte
810B LDA return_2,y ; Load vector high byte from table
810E STA userv+1,x ; Store high byte at &0201+X
8111 DEY ; Previous table byte
8112 LDA return_2,y ; Load vector low byte from table
8115 STA userv,x ; Store low byte at &0200+X
8118 DEY ; Previous table byte
8119 BNE init_vector_loop ; Loop for all 4 vector pairs
811B .init_tube_and_workspace
LDA #&8e ; A=&8E: Tube control register init value
811D STA tube_status_1_and_tube_control ; Write to Tube control register
; Copy NMI handler code from ROM to RAM pages &04-&06
8120 .cloop←1← 8133 BNE
LDA reloc_p4_src,y ; Load ROM byte from page &93
8123 STA tube_code_page4,y ; Store to page &04 (Tube code)
8126 LDA l945a,y ; Load ROM byte from page &94
8129 STA tube_dispatch_table,y ; Store to page &05 (dispatch table)
812C LDA c955a,y ; Load ROM byte from page &95
812F STA tube_code_page6,y ; Store to page &06
8132 DEY ; DEY wraps 0 -> &FF on first iteration
8133 BNE cloop ; Loop until 256 bytes copied per page
8135 JSR tube_post_init ; Run post-init routine in copied code
8138 LDX #&60 ; X=&60: copy 97 bytes (&60..&00)
; Copy NMI workspace initialiser from ROM to &0016-&0076
813A .copy_nmi_workspace←1← 8140 BPL
LDA reloc_zp_src,x ; Load NMI workspace init byte from ROM
813D STA nmi_workspace_start,x ; Store to zero page &16+X
813F DEX ; Next byte
8140 BPL copy_nmi_workspace ; Loop until all workspace bytes copied
8142 .restore_y_check_svc←1← 8101 BNE
LDY zp_temp_10 ; Restore Y (ROM number)
8144 .tube_chars_done
LDA #0 ; A=0: fall through to service &12 check
8146 .check_svc_12←1← 80EC BCC
CMP #&12 ; Is this service &12 (select FS)?
8148 BNE not_svc_12_nfs ; No: check if service < &0D
814A CPY #5 ; Service &12: Y=5 (NFS)?
814C BEQ select_nfs ; Y=5: select NFS
814E .not_svc_12_nfs←1← 8148 BNE
CMP #&0d ; Service >= &0D?
8150 .svc_unhandled_return
BCS return_2 ; Service >= &0D: not handled, return
8152 .do_svc_dispatch
TAX ; X = service number (dispatch index)
8153 LDA rom_svc_num ; Save &A9 (current service state)
8155 PHA ; Push saved &A9
8156 LDA nfs_temp ; Save &A8 (workspace page number)
8158 PHA ; Push saved &A8
8159 STX rom_svc_num ; Store service number to &A9
815B STY nfs_temp ; Store Y (page number) to &A8
815D TYA ; A = Y for dispatch table offset
815E LDY #0 ; Y=0: base offset for service dispatch
8160 JSR dispatch ; Dispatch to service handler
8163 LDX rom_svc_num ; Recover service claim status from &A9
8165 PLA ; Restore saved &A8 from stack
8166 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.

8168 .svc_dispatch_epilogue
PLA ; Restore saved A from service dispatch
8169 STA rom_svc_num ; Save to workspace &A9
816B TXA ; Return ROM number in A
816C .return_2←4← 8107 LDX← 810B LDA← 8112 LDA← 8150 BCS
RTS ; Return (not our command)
816D EQUB &1C, &05, &0E, &E7, &04, &10, &16, &00, &02, &E8, &06, &20
8179 .svc_4_star_command
LDX #8 ; ROM offset for "ROFF" (copyright suffix)
817B JSR match_rom_string ; Try matching *ROFF command
817E BNE match_net_cmd ; No match: try *NET
fall through ↓

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.

8180 .net_4_resume_remote
LDY #4 ; Y=4: offset of keyboard disable flag
8182 LDA (net_rx_ptr),y ; Read flag from RX buffer
8184 BEQ skip_kbd_reenable ; Zero: keyboard not disabled, skip
8186 LDA #0 ; A=0: value to clear flag and re-enable
8188 TAX ; X=&00
8189 STA (net_rx_ptr),y ; Clear keyboard disable flag in buffer
818B TAY ; Y=&00
818C LDA #osbyte_read_write_econet_keyboard_disable ; OSBYTE &C9: Econet keyboard disable
818E JSR osbyte ; Re-enable keyboard (X=0, Y=0) Enable keyboard (for Econet)
8191 LDA #&0a ; Function &0A: remote operation complete
8193 JSR setup_tx_and_send ; Send notification to controlling station
8196 .clear_osbyte_ce_cf←1← 8489 JSR
STX nfs_workspace ; Save X (return value from TX)
8198 LDA #&ce ; OSBYTE &CE: first system mask to reset
819A .clear_osbyte_masks←1← 81A5 BEQ
LDX nfs_workspace ; Restore X for OSBYTE call
819C LDY #&7f ; Y=&7F: AND mask (clear bit 7)
819E JSR osbyte ; Reset system mask byte
81A1 ADC #1 ; Advance to next OSBYTE (&CE -> &CF)
81A3 CMP #&d0 ; Reached &D0? (past &CF)
81A5 BEQ clear_osbyte_masks ; No: reset &CF too
81A7 .skip_kbd_reenable←1← 8184 BEQ
LDA #0 ; A=0: clear remote state
81A9 STA rom_svc_num ; Clear &A9 (service dispatch state)
81AB STA nfs_workspace ; Clear workspace byte
81AD RTS ; Return
81AE .match_net_cmd←1← 817E BNE
LDX #1 ; X=1: ROM offset for "NET" match
81B0 JSR match_rom_string ; Try matching *NET command
81B3 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.

81B5 .select_nfs←1← 814C BEQ
JSR call_fscv_shutdown ; Notify current FS of shutdown (FSCV A=6)
81B8 SEC ; C=1 for ROR
81B9 ROR nfs_temp ; Set bit 7 of l00a8 (inhibit auto-boot)
81BB JSR issue_vectors_claimed ; Claim OS vectors, issue service &0F
81BE LDY #&1d ; Y=&1D: top of FS state range
81C0 .initl←1← 81C8 BNE
LDA (net_rx_ptr),y ; Copy FS state from RX buffer...
81C2 STA fs_state_deb,y ; ...to workspace (offsets &15-&1D)
81C5 DEY ; Next byte (descending)
81C6 CPY #&14 ; Loop until offset &14 done
81C8 BNE initl ; Continue loop
81CA 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.

81CC .match_rom_string←2← 817B JSR← 81B0 JSR
LDY nfs_temp ; Y = saved text pointer offset
81CE .match_next_char←1← 81DB BNE
LDA (os_text_ptr),y ; Load next input character
81D0 AND #&df ; Force uppercase (clear bit 5)
81D2 BEQ cmd_name_matched ; Input char is NUL/space: check ROM byte
81D4 CMP binary_version,x ; Compare with ROM string byte
81D7 BNE cmd_name_matched ; Mismatch: check if ROM string ended
81D9 INY ; Advance input pointer
81DA INX ; Advance ROM string pointer
81DB BNE match_next_char ; Continue matching (always taken)
81DD .cmd_name_matched←2← 81D2 BEQ← 81D7 BNE
LDA binary_version,x ; Load ROM string byte at match point
81E0 BEQ skip_cmd_spaces ; Zero = end of ROM string = full match
81E2 RTS ; Non-zero = partial/no match; Z=0
81E3 .skpspi←1← 81E8 BEQ
INY ; Skip this space
81E4 .skip_cmd_spaces←1← 81E0 BEQ
LDA (os_text_ptr),y ; Load next input character
81E6 CMP #&20 ; Is it a space?
81E8 BEQ skpspi ; Yes: keep skipping
81EA EOR #&0d ; XOR with CR: Z=1 if end of line
81EC RTS ; Return (not our service call)

Service 9: *HELP

Prints the ROM identification string using print_inline.

81ED .svc_9_help
JSR print_inline ; Print inline ROM identification string
81F0 EQUS ".NFS 3.35K."
81FB .restore_y_return←2← 81B3 BNE← 8210 BNE
LDY nfs_temp ; Load preserved Y from temp storage
81FD 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).

81FE .call_fscv_shutdown←2← 81B5 JSR← 8203 JSR
LDA #6 ; FSCV reason 6 = FS shutdown
8200 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 no key is pressed, auto-boot proceeds directly via print_station_info. If a key is pressed, falls through to check_boot_key: the 'N' key (matrix address &55) proceeds with auto-boot, any other key causes the auto-boot to be declined.

8203 .svc_3_autoboot
JSR call_fscv_shutdown ; Notify current FS of shutdown
8206 LDA #osbyte_scan_keyboard_from_16 ; OSBYTE &7A: scan keyboard
8208 JSR osbyte ; Keyboard scan starting from key 16
820B TXA ; X is key number if key is pressed, or &ff otherwise
820C 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.

820E .check_boot_key
EOR #&55 ; XOR with &55: result=0 if key is 'N'
8210 BNE restore_y_return ; Not 'N': return without claiming
8212 TAY ; Y=key
8213 LDA #osbyte_write_keys_pressed ; OSBYTE &78: clear key-pressed state
8215 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.

8218 .print_station_info←1← 820C BMI
JSR print_inline ; Print 'Econet Station ' banner
821B EQUS "Econet Station "
822A LDY #&14 ; Y=&14: OSBYTE for version number
822C LDA (net_rx_ptr),y ; Load station number
822E JSR print_decimal ; Print as 3-digit decimal
8231 LDA #&20 ; BIT trick: bit 5 of SR2 = clock present
8233 BIT econet_control23_or_status2 ; Test DCD: clock present if bit 5 clear
8236 BEQ skip_no_clock_msg ; Clock present: skip warning
8238 JSR print_inline ; Print ' No Clock' warning
823B EQUS " No Clock"
8244 NOP ; NOP (padding after inline string)
8245 .skip_no_clock_msg←1← 8236 BEQ
JSR print_inline ; Print two CRs (blank line)
8248 EQUS ".."
fall through ↓

Initialise filing system vectors

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

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

8261 .issue_vectors_claimed←1← 81BB JSR
LDA #osbyte_issue_service_request ; A=&8F: issue service request
8263 LDX #&0f ; X=&0F: 'vectors claimed' service
8265 JSR osbyte ; Issue paged ROM service call, Reason X=15 - Vectors claimed
8268 LDX #&0a ; X=&0A: service &0A
826A JSR osbyte ; Issue service &0A
826D LDX nfs_temp ; Non-zero after hard reset: skip auto-boot
826F BNE return_3 ; Non-zero: skip auto-boot
8271 LDX #&78 ; X = lo byte of auto-boot string at &8292
fall through ↓

Run FSCV command from ROM

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

8273 .run_fscv_cmd←2← 831F LDA← 8325 LDA
LDY #&82 ; Y=&82: ROM page high byte
8275 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.
8278 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 not read at runtime (store_rom_ptr_pair writes the current ROM bank number without reading). The last entry (FSCV) has no padding byte.

8280 .fs_vector_addrs←1← 824C LDA
EQUB &1B ; FILEV dispatch lo
8281 EQUB &FF ; FILEV dispatch hi
8282 EQUB &1E ; ARGSV dispatch lo
8283 EQUB &FF ; ARGSV dispatch hi
8284 EQUB &21 ; BGETV dispatch lo
8285 EQUB &FF ; BGETV dispatch hi
8286 EQUB &24 ; BPUTV dispatch lo
8287 EQUB &FF ; BPUTV dispatch hi
8288 EQUB &27 ; GBPBV dispatch lo
8289 EQUB &FF ; GBPBV dispatch hi
828A EQUB &2A ; FINDV dispatch lo
828B EQUB &FF ; FINDV dispatch hi
828C EQUB &2D ; FSCV dispatch lo
828D EQUB &FF ; FSCV dispatch hi
828E EQUB &DE ; FILEV handler lo (&86DE)
828F EQUB &86 ; FILEV handler hi
8290 EQUB &4A ; (ROM bank — not read)
8291 EQUB &07 ; ARGSV handler lo (&8907)
8292 EQUB &89 ; ARGSV handler hi
8293 EQUB &44 ; (ROM bank — not read)
8294 EQUB &2E ; BGETV handler lo (&852E)
8295 EQUB &85 ; BGETV handler hi
8296 EQUB &57 ; (ROM bank — not read)
8297 EQUB &DC ; BPUTV handler lo (&83DC)
8298 EQUB &83 ; BPUTV handler hi
8299 EQUB &42 ; (ROM bank — not read)
829A EQUB &0E ; GBPBV handler lo (&8A0E)
829B EQUB &8A ; GBPBV handler hi
829C EQUB &41 ; (ROM bank — not read)
829D EQUB &6F ; FINDV handler lo (&896F)
829E EQUB &89 ; FINDV handler hi
829F EQUB &52 ; (ROM bank — not read)
82A0 EQUB &C7 ; FSCV handler lo (&80C7)
82A1 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)
82A2 .svc_1_abs_workspace
CPY #&10 ; Already at page &10 or above?
82A4 BCS return_3 ; Yes: nothing to claim
82A6 LDY #&10 ; Claim pages &0D-&0F (3 pages)
82A8 .return_3←2← 826F BNE← 82A4 BCS
RTS ; Return (workspace claim done)
82A9 EQUB &74, &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)
82AB .svc_2_private_workspace
STY net_rx_ptr_hi ; Store RX buffer page high byte
82AD INY ; Next page for NFS workspace
82AE STY nfs_workspace_hi ; Store workspace page high byte
82B0 LDA #0 ; A=0 for clearing workspace
82B2 LDY #4 ; Y=4: remote status offset
82B4 STA (net_rx_ptr),y ; Clear status byte in net receive buffer
82B6 LDY #&ff ; Y=&FF: used for later iteration
82B8 STA net_rx_ptr ; Clear RX ptr low byte
82BA STA nfs_workspace ; Clear workspace ptr low byte
82BC STA nfs_temp ; Clear RXCB iteration counter
82BE STA tx_clear_flag ; Clear TX semaphore (no TX in progress)
82C1 TAX ; X=0 for OSBYTE X=&00
82C2 LDA #osbyte_read_write_last_break_type ; OSBYTE &FD: read type of last reset
82C4 JSR osbyte ; Read type of last reset
82C7 TXA ; X = break type from OSBYTE result X=value of type of last reset
82C8 BEQ read_station_id ; Soft break (X=0): skip FS init
82CA LDY #&15 ; Y=&15: printer station offset in RX buffer
82CC LDA #&fe ; &FE = no server selected
82CE STA fs_server_stn ; Station &FE = no server selected
82D1 STA (net_rx_ptr),y ; Store &FE at printer station offset
82D3 LDA #0 ; A=0 for clearing workspace fields
82D5 STA fs_server_net ; Clear network number
82D8 STA prot_status ; Clear protection status
82DB STA fs_messages_flag ; Clear message flag
82DE STA fs_boot_option ; Clear boot option
82E1 INY ; Y=&16
82E2 STA (net_rx_ptr),y ; Clear net number at RX buffer offset &16
82E4 LDY #3 ; Init printer server: station &FE, net 0
82E6 STA (nfs_workspace),y ; Store net 0 at workspace offset 3
82E8 DEY ; Y=2: printer station offset Y=&02
82E9 LDA #&eb ; &FE = no printer server
82EB STA (nfs_workspace),y ; Store &FE at printer station in workspace
82ED .init_rxcb_entries←1← 82FA BNE
LDA nfs_temp ; Load RXCB counter
82EF JSR calc_handle_offset ; Convert to workspace byte offset
82F2 BCS read_station_id ; C=1: past max handles, done
82F4 LDA #&3f ; Mark RXCB as available
82F6 STA (nfs_workspace),y ; Write &3F flag to workspace
82F8 INC nfs_temp ; Next RXCB number
82FA BNE init_rxcb_entries ; Loop for all RXCBs
82FC .read_station_id←2← 82C8 BEQ← 82F2 BCS
LDA station_id_disable_net_nmis ; Read station ID (also INTOFF)
82FF LDY #&14 ; Y=&14: station ID offset in RX buffer
8301 STA (net_rx_ptr),y ; Store our station number
8303 JSR trampoline_adlc_init ; Initialise ADLC hardware
8306 LDA #&40 ; Enable user-level RX (LFLAG=&40)
8308 STA rx_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=&9074, rom=current) into the ROM pointer table at offset &36, installing osword_dispatch as the NETV handler.

830B .setup_rom_ptrs_netv←1← 8255 JSR
LDA #osbyte_read_rom_ptr_table_low ; OSBYTE &A8: read ROM pointer table address
830D LDX #0 ; X=0: read low byte
830F LDY #&ff ; Y=&FF: read high byte
8311 JSR osbyte ; Returns table address in X (lo) Y (hi) Read address of ROM pointer table
8314 STX osrdsc_ptr ; Store table base address low byte X=value of address of ROM pointer table (low byte)
8316 STY osrdsc_ptr_hi ; Store table base address high byte Y=value of address of ROM pointer table (high byte)
8318 LDY #&36 ; NETV extended vector offset in ROM ptr table
831A STY netv ; Set NETV low byte = &36 (vector dispatch)
831D LDX #1 ; Install 1 entry (NETV) in ROM ptr table
831F .store_rom_ptr_pair←2← 825C JSR← 8331 BNE
LDA run_fscv_cmd,y ; Load handler address low byte from table
8322 STA (osrdsc_ptr),y ; Store to ROM pointer table
8324 INY ; Next byte
8325 LDA run_fscv_cmd,y ; Load handler address high byte from table
8328 STA (osrdsc_ptr),y ; Store to ROM pointer table
832A INY ; Next byte
832B LDA romsel_copy ; Write current ROM bank number
832D STA (osrdsc_ptr),y ; Store ROM number to ROM pointer table
832F INY ; Advance to next entry position
8330 DEX ; Count down entries
8331 BNE store_rom_ptr_pair ; Loop until all entries installed
8333 LDY nfs_workspace_hi ; Y = workspace high byte + 1 = next free page
8335 INY ; Advance past workspace page
8336 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.

8337 .fscv_6_shutdown
LDY #&1d ; Copy 10 bytes: FS state to workspace backup
8339 .fsdiel←1← 8341 BNE
LDA fs_state_deb,y ; Load FS state byte at offset Y
833C STA (net_rx_ptr),y ; Store to workspace backup area
833E DEY ; Next byte down
833F CPY #&14 ; Offsets &15-&1D: server, handles, OPT, etc.
8341 BNE fsdiel ; Loop for offsets &1D..&15
8343 LDA #osbyte_printer_driver_going_dormant ; A=&7B: printer driver going dormant
8345 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.

8348 .init_tx_reply_port←1← 83BB JSR
LDA #&90 ; A=&90: FS reply port (PREPLY)
834A .init_tx_ctrl_port←1← 883B JSR
JSR init_tx_ctrl_block ; Init TXCB from template
834D STA txcb_port ; Store port number in TXCB
834F LDA #3 ; Control byte: 3 = transmit
8351 STA txcb_start ; Store control byte in TXCB
8353 DEC txcb_ctrl ; Decrement TXCB flag to arm TX
8355 RTS ; Return after port setup

Initialise TX control block at &00C0 from template

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

8356 .init_tx_ctrl_block←4← 834A JSR← 83AA JSR← 83F5 JSR← 8FEB LDA
PHA ; Preserve A across call
8357 LDY #&0b ; Copy 12 bytes (Y=11..0)
8359 .fstxl1←1← 836A BPL
LDA tx_ctrl_template,y ; Load template byte
835C STA txcb_ctrl,y ; Store to TX control block at &00C0
835F CPY #2 ; Y < 2: also copy FS server station/network
8361 BPL fstxl2 ; Skip station/network copy for Y >= 2
8363 LDA fs_server_stn,y ; Load FS server station (Y=0) or network (Y=1)
8366 STA txcb_dest,y ; Store to dest station/network at &00C2
8369 .fstxl2←1← 8361 BPL
DEY ; Next byte (descending)
836A BPL fstxl1 ; Loop until all 12 bytes copied
836C PLA ; Restore A
836D 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.

836E .tx_ctrl_template←1← 8359 LDA
EQUB &80 ; Control flag
836F EQUB &99 ; Port (FS command = &99)
8370 EQUB &00 ; Station (filled at runtime)
8371 EQUB &00 ; Network (filled at runtime)
8372 EQUB &00 ; Buffer start low
8373 EQUB &0F ; Buffer start high (page &0F)
8374 .tx_ctrl_upper←3← 88BB BIT← 8992 BIT← 916C BIT
EQUB &FF ; Buffer start pad (4-byte Econet addr)
8375 EQUB &FF ; Buffer start pad
8376 EQUB &FF ; Buffer end low
8377 EQUB &0F ; Buffer end high (page &0F)
8378 EQUB &FF ; Buffer end pad
8379 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
837A .prepare_cmd_with_flag←1← 8A5F JSR
PHA ; Save flag byte for command
837B LDA #&2a ; A=&2A: error ptr for retry
837D SEC ; C=1: include flag in FS command
837E BCS store_fs_hdr_fn ; ALWAYS branch to prepare_fs_cmd ALWAYS branch
8380 .prepare_cmd_clv←2← 86FB JSR← 87A4 JSR
CLV ; V=0: command has no flag byte
8381 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 &8BE4 for "BYE".

8383 .bye_handler
LDA #osbyte_close_spool_exec ; A=&77: OSBYTE close spool/exec
8385 JSR osbyte ; Close any *SPOOL and *EXEC files
8388 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)
838A .prepare_fs_cmd←12← 80B8 JSR← 8860 JSR← 88D6 JSR← 8922 JSR← 8949 JSR← 89BF JSR← 89E4 JSR← 8ABE JSR← 8B74 JSR← 8C1D JSR← 8C54 JSR← 8CC1 JSR
CLV ; V=0: standard FS command path
838B .init_tx_ctrl_data←2← 88BE JSR← 8995 JSR
.prepare_fs_cmd_v←2← 88BE JSR← 8995 JSR
LDA fs_urd_handle ; Copy URD handle from workspace to buffer
838E STA fs_cmd_urd ; Store URD at &0F02
8391 LDA #&2a ; A=&2A: error ptr for retry
8393 .store_fs_hdr_clc←1← 8381 BVC
CLC ; CLC: no byte-stream path
8394 .store_fs_hdr_fn←1← 837E BCS
STY fs_cmd_y_param ; Store function code at &0F01
8397 STA fs_error_ptr ; Store error ptr for TX poll
8399 LDY #1 ; Y=1: copy CSD (offset 1) then LIB (offset 0)
839B .copy_dir_handles←1← 83A2 BPL
LDA fs_csd_handle,y ; Copy CSD and LIB handles to command buffer A=timeout period for FS reply
839E STA fs_cmd_csd,y ; Store at &0F03 (CSD) and &0F04 (LIB)
83A1 DEY ; Y=function code
83A2 BPL copy_dir_handles ; Loop for both handles
fall through ↓

Build and send FS command (DOFSOP)

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

On EntryXbuffer extent (command-specific data bytes)
Yfunction code
Atimeout period for FS reply
C0 for standard FS path, 1 for byte-stream (BSXMIT)
On ExitA0 on success
X0 on success, &D6 on not-found
Y1 (offset past command code in reply)
83A4 .build_send_fs_cmd←1← 8B17 JSR
PHP ; Save carry (FS path vs byte-stream)
83A5 LDA #&90 ; Reply port &90 (PREPLY)
83A7 STA fs_cmd_type ; Store at &0F00 (HDRREP)
83AA JSR init_tx_ctrl_block ; Copy TX template to &00C0
83AD TXA ; A = X (buffer extent)
83AE ADC #5 ; HPTR = header (5) + data (X) bytes to send
83B0 STA txcb_end ; Store to TXCB end-pointer low
83B2 PLP ; Restore carry flag
83B3 BCS dofsl5 ; C=1: byte-stream path (BSXMIT)
83B5 PHP ; Save flags for send_fs_reply_cmd
83B6 JSR setup_tx_ptr_c0 ; Point net_tx_ptr to &00C0; transmit
83B9 PLP ; Restore flags
83BA .send_fs_reply_cmd←2← 87B4 JSR← 8A9B JSR
PHP ; Save flags (V flag state)
83BB JSR init_tx_reply_port ; Set up RX wait for FS reply
83BE LDA fs_error_ptr ; Load error ptr for TX retry
83C0 JSR send_to_fs ; Transmit and wait (BRIANX)
83C3 PLP ; Restore flags
83C4 .dofsl7←1← 83DA BCC
INY ; Y=1: skip past command code byte
83C5 LDA (txcb_start),y ; Load return code from FS reply
83C7 TAX ; X = return code
83C8 BEQ return_dofsl7 ; Zero: success, return
83CA BVC check_fs_error ; V=0: standard path, error is fatal
83CC ADC #&2a ; ADC #&2A: test for &D6 (not found)
83CE .check_fs_error←1← 83CA BVC
BNE store_fs_error ; Non-zero: hard error, go to FSERR
83D0 .return_dofsl7←1← 83C8 BEQ
RTS ; Return (success or soft &D6 error)
83D1 .dofsl5←1← 83B3 BCS
PLA ; Discard saved flags from stack
83D2 LDX #&c0 ; X=&C0: TXCB address for byte-stream TX
83D4 INY ; Y++ past command code
83D5 JSR econet_tx_retry ; Byte-stream transmit with retry
83D8 STA fs_load_addr_3 ; Store result to &B3
83DA BCC dofsl7 ; C=0: success, check reply code
83DC .bputv_handler
CLC ; CLC for address addition
fall through ↓

Handle BPUT/BGET file byte I/O

BPUTV enters at &83DC (CLC; fall through) and BGETV enters at &852E (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
83DD .handle_bput_bget←1← 852F JSR
PHA ; Save A (BPUT byte) on stack
83DE STA fs_error_flags ; Also save byte at &0FDF for BSXMIT
83E1 TXA ; Transfer X for stack save
83E2 PHA ; Save X on stack
83E3 TYA ; Transfer Y (handle) for stack save
83E4 PHA ; Save Y (handle) on stack
83E5 PHP ; Save P (C = BPUT/BGET selector) on stack
83E6 STY fs_spool_handle ; Save handle for SPOOL/EXEC comparison later
83E8 JSR handle_to_mask_clc ; Convert handle Y to single-bit mask
83EB STY fs_handle_mask ; Store handle bitmask at &0FDE
83EE STY fs_spool0 ; Store handle bitmask for sequence tracking
83F0 LDY #&90 ; &90 = data port (PREPLY)
83F2 STY fs_putb_buf ; Store reply port in command buffer
83F5 JSR init_tx_ctrl_block ; Set up 12-byte TXCB from template
83F8 LDA #&dc ; CB reply buffer at &0FDC
83FA STA txcb_start ; Store reply buffer ptr low in TXCB
83FC LDA #&e0 ; Error buffer at &0FE0
83FE STA txcb_end ; Store error buffer ptr low in TXCB
8400 INY ; Y=1 (from init_tx_ctrl_block exit)
8401 LDX #9 ; X=9: BPUT function code
8403 PLP ; Restore C: selects BPUT (0) vs BGET (1)
8404 BCC store_retry_count ; C=0 (BPUT): keep X=9
8406 DEX ; X=&08
8407 .store_retry_count←1← 8404 BCC
STX fs_getb_buf ; Store function code at &0FDD
840A LDA fs_handle_mask ; Load handle mask for seq tracking
840D LDX #&c0 ; X=&C0: TXCB address for econet_tx_retry
840F JSR econet_tx_retry ; Transmit via byte-stream protocol
8412 LDX fs_getb_buf ; Load reply byte from buffer
8415 BEQ update_sequence_return ; Zero reply = success, skip error handling
8417 LDY #&1f ; Copy 32-byte reply to error buffer at &0FE0
8419 .error1←1← 8420 BPL
LDA fs_putb_buf,y ; Load reply byte at offset Y
841C STA fs_error_buf,y ; Store to error buffer at &0FE0+Y
841F DEY ; Next byte (descending)
8420 BPL error1 ; Loop until all 32 bytes copied
8422 TAX ; X=File handle
8423 LDA #osbyte_read_write_exec_file_handle ; A=&C6: read *EXEC file handle
8425 JSR osbyte ; Read/Write *EXEC file handle
8428 LDA #&ea ; Default A=&EA: OSCLI no-op (CR before send_to_fs_star)
842A CPY fs_spool_handle ; Y=value of *SPOOL file handle
842C BNE check_exec_handle ; Non-zero reply: error or done
842E LDA #&e4 ; A=&E4: OSCLI "SP." string at &84E4
8430 .check_exec_handle←1← 842C BNE
CPX fs_spool_handle ; X=value of *EXEC file handle
8432 BNE close_spool_exec ; No EXEC match -- skip close
8434 LDA #&e8 ; A=&E8: Tube OSWORD for BPUT
8436 .close_spool_exec←1← 8432 BNE
TAX ; X = string offset for OSCLI close
8437 LDY #&84 ; Y=&84: high byte of OSCLI string in ROM
8439 JSR oscli ; Close SPOOL/EXEC via "*SP." or "*E."
843C .dispatch_fs_error
LDA #&e0 ; Reset CB pointer to error buffer at &0FE0
843E STA txcb_start ; Reset reply ptr to error buffer
8440 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.

8443 .store_fs_error←1← 83CE BNE
STX fs_last_error ; Remember raw FS error code
8446 LDY #1 ; Y=1: point to error number byte in reply
8448 CPX #&a8 ; Clamp FS errors below &A8 to standard &A8
844A BCS error_code_clamped ; Error >= &A8: keep original value
844C LDA #&a8 ; Error < &A8: override with standard &A8
844E STA (txcb_start),y ; Write clamped error number to reply buffer
8450 .error_code_clamped←1← 844A BCS
LDY #&ff ; Start scanning from offset &FF (will INY to 0)
8452 .copy_error_to_brk←1← 845A BNE
INY ; Next byte in reply buffer
8453 LDA (txcb_start),y ; Copy reply buffer to &0100 for BRK execution
8455 STA l0100,y ; Build BRK error block at &0100
8458 EOR #&0d ; Scan for CR terminator (&0D)
845A BNE copy_error_to_brk ; Continue until CR found
845C STA l0100,y ; Replace CR with zero = BRK error block end
845F BEQ execute_downloaded ; Execute as BRK error block at &0100; ALWAYS ALWAYS branch
8461 .update_sequence_return←1← 8415 BEQ
STA fs_sequence_nos ; Save updated sequence number
8464 PLA ; Restore Y from stack
8465 TAY ; Transfer A to Y for indexing
8466 PLA ; Restore X from stack
8467 TAX ; Transfer to X for return
8468 PLA ; Restore A from stack
8469 .return_remote_cmd
RTS ; Return to caller

Remote boot/execute handler

Checks byte 4 of the RX control block (remote status flag). If zero (not currently remoted), falls through to remot1 to set up a new remote session. If non-zero (already remoted), jumps to clear_jsr_protection and returns.

846A .lang_1_remote_boot
LDY #4 ; Y=4: remote status flag offset
846C LDA (net_rx_ptr),y ; Read remote status from RX CB
846E BEQ remot1 ; Zero: not remoted, set up session
8470 .rchex←1← 84B6 BNE
JMP clear_jsr_protection ; Already remoted: clear and return
8473 .remot1←2← 846E BEQ← 84AC BEQ
ORA #9 ; Set remote status: bits 0+3 (ORA #9)
8475 STA (net_rx_ptr),y ; Store updated remote status
8477 LDX #&80 ; X=&80: RX data area offset
8479 LDY #&80 ; Y=&80: read source station low
847B LDA (net_rx_ptr),y ; Read source station lo from RX data at &80
847D PHA ; Save source station low byte
847E INY ; Y=&81
847F LDA (net_rx_ptr),y ; Read source station hi from RX data at &81
8481 LDY #&0f ; Save controlling station to workspace &0E/&0F
8483 STA (nfs_workspace),y ; Store station high to ws+&0F
8485 DEY ; Y=&0E Y=&0e
8486 PLA ; Restore source station low
8487 STA (nfs_workspace),y ; Store station low to ws+&0E
8489 JSR clear_osbyte_ce_cf ; Clear OSBYTE &CE/&CF flags
848C JSR ctrl_block_setup ; Set up TX control block
848F LDX #1 ; X=1: disable keyboard
8491 LDY #0 ; Y=0 for OSBYTE
8493 LDA #osbyte_read_write_econet_keyboard_disable ; Disable keyboard for remote session
8495 JSR osbyte ; Disable keyboard (for Econet)
fall through ↓

Execute code at &0100

Clears JSR protection, zeroes &0100-&0102 (creating a BRK instruction at &0100 as a safe default), then JMP &0100 to execute code received over the network. If no code was loaded, the BRK triggers an error handler.

8498 .lang_3_execute_at_0100
JSR clear_jsr_protection ; Allow JSR to page 1 (stack page)
849B LDX #2 ; Zero bytes &0100-&0102
849D LDA #0 ; A=0: zero execution header bytes
849F .zero_exec_header←1← 84A3 BPL
STA l0100,x ; BRK at &0100 as safe default
84A2 DEX ; Next byte
84A3 BPL zero_exec_header ; Loop until all zeroed
84A5 .execute_downloaded←2← 845F BEQ← 84DE BEQ
JMP l0100 ; Execute downloaded code

Remote operation with source validation

Validates that the source station in the received packet matches the controlling station stored in the NFS workspace. If byte 4 of the RX control block is zero (not currently remoted), allows the new remote session via remot1. If non-zero, compares the source station at RX offset &80 against workspace offset &0E -- rejects mismatched stations via clear_jsr_protection, accepts matching stations by falling through to lang_0_insert_remote_key.

84A8 .lang_4_remote_validated
LDY #4 ; Y=4: RX control block byte 4 (remote status)
84AA LDA (net_rx_ptr),y ; Read remote status flag
84AC BEQ remot1 ; Zero = not remoted; allow new session
84AE LDY #&80 ; Read source station from RX data at &80
84B0 LDA (net_rx_ptr),y ; A = source station number
84B2 LDY #&0e ; Compare against controlling station at &0E
84B4 CMP (nfs_workspace),y ; Check if source matches controller
84B6 BNE rchex ; Reject: source != controlling station
fall through ↓

Insert remote keypress

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

84B8 .lang_0_insert_remote_key
LDY #&82 ; Read keypress from RX data at &82
84BA LDA (net_rx_ptr),y ; Load character byte
84BC TAY ; Y = character to insert
84BD LDX #0 ; X = buffer 0 (keyboard input)
84BF JSR clear_jsr_protection ; Release JSR protection before inserting key
84C2 LDA #osbyte_insert_input_buffer ; OSBYTE &99: insert char into input buffer
84C4 JMP osbyte ; Tail call: insert character Y into buffer X Insert character Y into input buffer X
84C7 .error_not_listening←1← 851A BEQ
LDA #8 ; Error code 8: "Not listening" error
84C9 BNE set_listen_offset ; ALWAYS branch to set_listen_offset ALWAYS branch
84CB .nlistn←1← 86A9 JMP
LDA (net_tx_ptr,x) ; Load TX status byte for error lookup
84CD .nlisne←2← 852C BNE← 89DC JMP
AND #7 ; Mask to 3-bit error code (0-7)
84CF .set_listen_offset←1← 84C9 BNE
TAX ; X = error code index
84D0 LDY error_offsets,x ; Look up error message offset from table
84D3 LDX #0 ; X=0: start writing at &0101
84D5 STX l0100 ; Store BRK opcode at &0100
84D8 .copy_error_message←1← 84E2 BNE
LDA error_msg_table,y ; Load error message byte
84DB STA l0101,x ; Build error message at &0101+
84DE BEQ execute_downloaded ; Zero byte = end of message; go execute BRK
84E0 INX ; Next dest byte
84E1 INY ; Advance past saved flags
84E2 BNE copy_error_message ; Continue copying message
84E4 EQUS "SP."
84E7 EQUB &0D, &45, &2E, &0D
84EB .send_to_fs_star←3← 8754 JSR← 902E JMP← 928B JSR
LDA #&2a ; A=&2A: error ptr for 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.

84ED .send_to_fs←2← 83C0 JSR← 8847 JSR
PHA ; Save function code on stack
84EE LDA rx_flags ; Load current rx_flags
84F1 PHA ; Save rx_flags on stack for restore
84F2 ORA #&80 ; Set bit7: FS transaction in progress
84F4 STA rx_flags ; Write back updated rx_flags
84F7 .skip_rx_flag_set
LDA #0 ; Push two zero bytes as timeout counters
84F9 PHA ; First zero for timeout
84FA PHA ; Second zero for timeout
84FB TAY ; Y=0: index for flag byte check Y=&00
84FC TSX ; TSX: index stack-based timeout via X
84FD .fs_reply_poll←3← 8507 BNE← 850C BNE← 8511 BNE
JSR check_escape ; Check for user escape condition
8500 .incpx
LDA (net_tx_ptr),y ; Read flag byte from TX control block
8502 BMI fs_wait_cleanup ; Bit 7 set = reply received
8504 DEC l0101,x ; Three-stage nested timeout: inner loop
8507 BNE fs_reply_poll ; Inner not expired: keep polling
8509 DEC l0102,x ; Middle timeout loop
850C BNE fs_reply_poll ; Middle not expired: keep polling
850E DEC l0104,x ; Outer timeout loop (slowest)
8511 BNE fs_reply_poll ; Outer not expired: keep polling
8513 .fs_wait_cleanup←1← 8502 BMI
PLA ; Pop first timeout byte
8514 PLA ; Pop second timeout byte
8515 PLA ; Pop saved rx_flags into A
8516 STA rx_flags ; Restore saved rx_flags from stack
8519 PLA ; Pop saved function code
851A BEQ error_not_listening ; A=saved func code; zero would mean no reply
851C 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.

3.35K restructures the SPOOL/EXEC close logic: both handles are always checked (3.35D skipped EXEC if SPOOL matched), and OSCLI is always called (with a harmless "." default if neither matched).

851D .check_escape←2← 84FD JSR← 8690 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.

851F .check_escape_handler
BIT escape_flag ; Test escape flag (bit 7)
8521 BPL return_4 ; Bit 7 clear: no escape, return
8523 JSR osbyte ; Acknowledge escape via OSBYTE &7E
; 3.35K fix: initialise Y=0 before the indexed store.
; In 3.35D, Y could hold any value here after the
; OSBYTE escape acknowledge call.
8526 LDY #0 ; Y=0 for indexed store
8528 LSR ; LSR: get escape result bit
8529 STA (net_tx_ptr),y ; Store escape result to TXCB
852B ASL ; Restore A
852C BNE nlisne ; Non-zero: report 'Not listening'
852E .bgetv_handler
SEC ; C=1: flag for BGET mode
852F JSR handle_bput_bget ; Handle BGET via FS command Handle BPUT/BGET file byte I/O
8532 SEC ; SEC: set carry for error check
8533 LDA #&fe ; A=&FE: mask for EOF check
8535 BIT fs_error_flags ; BIT l0fdf: test error flags
8538 BVS return_4 ; V=1: error, return early
; 3.35K fix: EOF hint clear/set are now mutually
; exclusive. In 3.35D, both clear_fs_flag and
; set_fs_flag were called when N=0, with the clear
; immediately undone by the set — making the EOF
; hint always set regardless of file position.
853A CLC ; CLC: no error
853B BMI set_eof_flag ; N=1: at/past EOF, set EOF flag
853D LDA fs_spool0 ; Load SPOOL handle mask
853F JSR clear_fs_flag ; Clear EOF flag for this handle
8542 BCC load_handle_mask ; C=0: skip to handle mask load
8544 .set_eof_flag←1← 853B BMI
LDA fs_spool0 ; Load SPOOL handle mask
8546 .bgetv_shared_jsr
JSR set_fs_flag ; Set EOF flag for this handle
8549 .load_handle_mask←1← 8542 BCC
LDA fs_handle_mask ; Load handle bitmask for caller
854C .return_4←2← 8521 BPL← 8538 BVS
RTS ; Return with handle mask in A
; Econet error message table (ERRTAB, 7 entries).
; Each entry: error number byte followed by NUL-terminated ; string.
; &A0: "Line Jammed" &A1: "Net Error"
; &A2: "Not listening" &A3: "No Clock"
; &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.
854D .error_msg_table←1← 84D8 LDA
EQUB &A0
854E EQUS "Line Jammed."
855A EQUB &A1
855B EQUS "Net Error."
8565 EQUB &A2
8566 EQUS "Not listening."
8574 EQUB &A3
8575 EQUS "No Clock."
857E EQUB &11
857F EQUS "Escape."
8586 EQUB &CB
8587 EQUS "Bad Option."
8592 EQUB &A5
8593 EQUS "No reply."

Save FSCV arguments with text pointers

Extended entry used by FSCV, FINDV, and fscv_3_star_cmd. Copies X/Y into os_text_ptr/&F3 and fs_cmd_ptr/&0E11, then falls through to save_fscv_args to store A/X/Y in the FS workspace.

859C .save_fscv_args_with_ptrs←3← 80C7 JSR← 896F JSR← 8BB6 JSR
STX os_text_ptr ; X to os_text_ptr (text ptr lo)
859E STY os_text_ptr_hi ; Y to os_text_ptr hi
85A0 STX fs_cmd_ptr ; X to FS command ptr lo
85A3 STY l0e11 ; Y to FS command ptr hi
fall through ↓

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
85A6 .save_fscv_args←3← 86DE JSR← 8907 JSR← 8A0E JSR
STA fs_last_byte_flag ; A = function code / command
85A8 STX fs_options ; X = control block ptr lo
85AA STY fs_block_offset ; Y = control block ptr hi
85AC STX fs_crc_lo ; X dup for indexed access via (fs_crc)
85AE STY fs_crc_hi ; Y dup for indexed access
85B0 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 &85CE. The two formats use different bit layouts for file protection attributes.

85B1 .decode_attribs_6bit←2← 889A JSR← 88C5 JSR
LDY #&0e ; Y=&0E: attribute byte offset in param block
85B3 LDA (fs_options),y ; Load FS attribute byte
85B5 AND #&3f ; Mask to 6 bits (FS → BBC direction)
85B7 LDX #4 ; X=4: skip first 4 table entries (BBC→FS half)
85B9 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 &85CE. 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
85BB .decode_attribs_5bit←2← 87BF JSR← 88E2 JSR
AND #&1f ; Mask to 5 bits (BBC → FS direction)
85BD LDX #&ff ; X=&FF: INX makes 0; start from table index 0
85BF .attrib_shift_bits←1← 85B9 BNE
STA fs_error_ptr ; Temp storage for source bitmask to shift out
85C1 LDA #0 ; A=0: accumulate destination bits here
85C3 .map_attrib_bits←1← 85CB BNE
INX ; Next table entry
85C4 LSR fs_error_ptr ; Shift out source bits one at a time
85C6 BCC skip_set_attrib_bit ; Bit was 0: skip this destination bit
85C8 ORA access_bit_table,x ; OR in destination bit from lookup table
85CB .skip_set_attrib_bit←1← 85C6 BCC
BNE map_attrib_bits ; Loop while source bits remain (A != 0)
85CD RTS ; Return; A = converted attribute bitmask
85CE .access_bit_table←1← 85C8 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
85D9 .print_inline←14← 81ED JSR← 8218 JSR← 8238 JSR← 8245 JSR← 8C25 JSR← 8C2F JSR← 8C3D JSR← 8C48 JSR← 8C5D JSR← 8C72 JSR← 8C85 JSR← 8C94 JSR← 8CA6 JSR← 8D6C JSR
PLA ; Pop return address (low) — points to last byte of JSR
85DA STA fs_load_addr ; Store return addr low as string ptr
85DC PLA ; Pop return address (high)
85DD STA fs_load_addr_hi ; Store return addr high as string ptr
85DF LDY #0 ; Y=0: offset for indirect load
85E1 .print_inline_char←1← 85EE BNE
INC fs_load_addr ; Advance pointer past return address / to next char
85E3 BNE print_next_char ; No page wrap: skip high byte inc
85E5 INC fs_load_addr_hi ; Handle page crossing in pointer
85E7 .print_next_char←1← 85E3 BNE
LDA (fs_load_addr),y ; Load next byte from inline string
85E9 BMI jump_via_addr ; Bit 7 set? Done — this byte is the next opcode
85EB JSR osasci ; Write character
85EE BNE print_inline_char ; OSASCI preserves NZ; loop always
85F0 .jump_via_addr←1← 85E9 BMI
JMP (fs_load_addr) ; Jump to address of high-bit byte (resumes code after string)

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

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

On EntryYoffset into (fs_options) buffer
On ExitAparsed value (accumulated in &B2)
Xinitial A value (saved by TAX)
Yoffset past last digit parsed
85F3 .parse_decimal←2← 808A JSR← 8090 JSR
TAX ; Save A in X for caller
85F4 LDA #0 ; Zero accumulator
85F6 STA fs_load_addr_2 ; Clear accumulator workspace
85F8 .scan_decimal_digit←1← 8615 BNE
LDA (fs_options),y ; Load next char from buffer
85FA CMP #&40 ; Letter or above?
85FC BCS no_dot_exit ; Yes: not a digit, done
85FE CMP #&2e ; Dot separator?
8600 BEQ parse_decimal_rts ; Yes: exit with C=1 (dot found)
8602 BMI no_dot_exit ; Control char or space: done
8604 AND #&0f ; Mask ASCII digit to 0-9
8606 STA fs_load_addr_3 ; Save new digit
8608 ASL fs_load_addr_2 ; Running total * 2
860A LDA fs_load_addr_2 ; A = running total * 2
860C ASL ; A = running total * 4
860D ASL ; A = running total * 8
860E ADC fs_load_addr_2 ; + total*2 = total * 10
8610 ADC fs_load_addr_3 ; + digit = total*10 + digit
8612 STA fs_load_addr_2 ; Store new running total
8614 INY ; Advance to next char
8615 BNE scan_decimal_digit ; Loop (always: Y won't wrap to 0)
8617 .no_dot_exit←2← 85FC BCS← 8602 BMI
CLC ; No dot found: C=0
8618 .parse_decimal_rts←1← 8600 BEQ
LDA fs_load_addr_2 ; Return result in A
861A 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
861B .handle_to_mask_a←3← 884E JSR← 8A29 JSR← 8F45 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
861C .handle_to_mask_clc←2← 83E8 JSR← 8912 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
861D .handle_to_mask←1← 8973 JSR
PHA ; Save A (will be restored on exit)
861E TXA ; Save X (will be restored on exit)
861F PHA ; (second half of X save)
8620 TYA ; A = handle from Y
8621 BCC y2fsl5 ; C=0: always convert
8623 BEQ handle_mask_exit ; C=1 and Y=0: skip (handle 0 = none)
8625 .y2fsl5←1← 8621 BCC
SEC ; C=1 and Y!=0: convert
8626 SBC #&1f ; A = handle - &1F (1-based bit position)
8628 TAX ; X = shift count
8629 LDA #1 ; Start with bit 0 set
862B .y2fsl2←1← 862D BNE
ASL ; Shift bit left
862C DEX ; Count down
862D BNE y2fsl2 ; Loop until correct position
862F ROR ; Undo final extra shift
8630 TAY ; Y = resulting bitmask
8631 BNE handle_mask_exit ; Non-zero: valid mask, skip to exit
8633 DEY ; Zero: invalid handle, set Y=&FF
8634 .handle_mask_exit←2← 8623 BEQ← 8631 BNE
PLA ; Restore X
8635 TAX ; Restore X from stack
8636 PLA ; Restore A
8637 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
8638 .mask_to_handle←2← 89A9 JSR← 8F5D JSR
LDX #&1f ; X = &1F (handle base - 1)
863A .fs2al1←1← 863C BNE
INX ; Count this bit position
863B LSR ; Shift mask right; C=0 when done
863C BNE fs2al1 ; Loop until all bits shifted out
863E TXA ; A = X = &1F + bit position = handle
863F RTS ; Return with handle in A

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
8640 .compare_addresses←2← 873A JSR← 87F6 JSR
LDX #4 ; Compare 4 bytes (index 4,3,2,1)
8642 .compare_addr_byte←1← 8649 BNE
LDA addr_work,x ; Load byte from first address
8644 EOR fs_load_addr_3,x ; XOR with corresponding byte
8646 BNE return_compare ; Mismatch: Z=0, return unequal
8648 DEX ; Next byte
8649 BNE compare_addr_byte ; Continue comparing
864B .return_compare←1← 8646 BNE
RTS ; Return with Z flag result
864C .fscv_7_read_handles
LDX #&20 ; X=first handle (&20)
864E LDY #&27 ; Y=last handle (&27)
8650 .return_fscv_handles
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. JMPs to the shared STA at &865C, skipping the ORA in set_fs_flag.

On EntryAbitmask of bits to clear
On ExitAupdated fs_eof_flags value
8651 .clear_fs_flag←3← 853F JSR← 8869 JSR← 8AA5 JSR
EOR #&ff ; Invert mask: set bits become clear bits
8653 AND fs_eof_flags ; AND into FS flags to clear bits
8656 JMP store_fs_flag ; Jump to shared store handler

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

ORs A into fs_work_0e07 (EOF hint byte), then falls through to STA fs_eof_flags at &865C (shared with clear_fs_flag). 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
8659 .set_fs_flag←5← 8546 JSR← 894F JSR← 899E JSR← 89C5 JSR← 8AA8 JSR
ORA fs_eof_flags ; OR into FS flags to set bits
865C .store_fs_flag←1← 8656 JMP
STA fs_eof_flags ; Write back updated flags
865F RTS ; Return

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.

8660 .setup_tx_ptr_c0←2← 83B6 JSR← 8836 JSR
LDX #&c0 ; X=&C0: TX control block at &00C0
8662 STX net_tx_ptr ; Set TX pointer lo
8664 LDX #0 ; X=0: page zero
8666 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.

8668 .tx_poll_ff←4← 9017 JSR← 9071 JMP← 90C8 JSR← 9269 JSR
LDA #&ff ; A=&FF: full retry count
866A .tx_poll_timeout
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 (&8660) always uses the standard TXCB; tx_poll_core (&866C) is general-purpose.

On EntryAretry count (&FF = full retry)
Ytimeout parameter (&60 = standard)
On ExitAentry A (retry count, restored from stack)
X0
Y0
866C .tx_poll_core←1← 9057 JSR
PHA ; Save retry count on stack
866D TYA ; Transfer timeout to A
866E PHA ; Save timeout on stack
866F LDX #0 ; X=0 for (net_tx_ptr,X) indirect
8671 LDA (net_tx_ptr,x) ; Load TXCB byte 0 (control/status)
8673 .tx_retry←1← 86A6 BEQ
STA (net_tx_ptr,x) ; Write control byte to start TX
8675 PHA ; Save control byte for retry
8676 .tx_semaphore_spin←1← 8679 BCC
ASL tx_clear_flag ; Test TX semaphore (C=1 when free)
8679 BCC tx_semaphore_spin ; Spin until semaphore released
867B LDA net_tx_ptr ; Copy TX ptr lo to NMI block
867D STA nmi_tx_block ; Store for NMI handler access
867F LDA net_tx_ptr_hi ; Copy TX ptr hi to NMI block
8681 STA nmi_tx_block_hi ; Store for NMI handler access
8683 JSR trampoline_tx_setup ; Initiate ADLC TX via trampoline
8686 .poll_txcb_status←1← 8688 BMI
LDA (net_tx_ptr,x) ; Poll TXCB byte 0 for completion
8688 BMI poll_txcb_status ; Bit 7 set: still busy, keep polling
868A ASL ; Shift bit 6 into bit 7 (error flag)
868B BPL tx_success ; Bit 6 clear: success, clean return
868D ASL ; Shift bit 5 into carry
868E BEQ tx_not_listening ; Zero: fatal error, no escape
8690 JSR check_escape ; Check for user escape condition
8693 PLA ; Discard saved control byte
8694 TAX ; Save to X for retry delay
8695 PLA ; Restore timeout parameter
8696 TAY ; Back to Y
8697 PLA ; Restore retry count
8698 BEQ tx_not_listening ; No retries left: report error
869A SBC #1 ; Decrement retry count
869C PHA ; Save updated retry count
869D TYA ; Timeout to A for delay
869E PHA ; Save timeout parameter
869F TXA ; Control byte for delay duration
86A0 .msdely←2← 86A1 BNE← 86A4 BNE
DEX ; Inner delay loop
86A1 BNE msdely ; Spin until X=0
86A3 DEY ; Outer delay loop
86A4 BNE msdely ; Continue delay
86A6 BEQ tx_retry ; ALWAYS branch
86A8 .tx_not_listening←2← 868E BEQ← 8698 BEQ
TAX ; Save error code in X
86A9 JMP nlistn ; Report 'Not listening' error
86AC .tx_success←1← 868B BPL
PLA ; Discard saved control byte
86AD PLA ; Discard timeout parameter
86AE PLA ; Discard retry count
86AF RTS ; Return (success)

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.

86B0 .copy_filename_ptr←1← 86E1 JSR
LDY #1 ; Y=1: copy 2 bytes (high then low)
86B2 .file1←1← 86B8 BPL
LDA (fs_options),y ; Load filename ptr from control block
86B4 STA os_text_ptr,y ; Store to MOS text pointer (&F2/&F3)
86B7 DEY ; Next byte (descending)
86B8 BPL file1 ; Loop for both bytes
fall through ↓

Parse filename using GSINIT/GSREAD into &0E30

Uses the MOS GSINIT/GSREAD API to parse a filename string from (os_text_ptr),Y, handling quoted strings and |-escaped characters. Stores the parsed result CR-terminated at &0E30 and sets up fs_crc_lo/hi to point to that buffer. Sub-entry at &86BC allows a non-zero starting Y offset.

On EntryYoffset into (os_text_ptr) buffer (0 at &86BA)
On ExitXlength of parsed string
Ypreserved
86BA .parse_filename_gs←2← 8988 JSR← 8DBF JSR
LDY #0 ; Start from beginning of string
fall through ↓

Parse filename via GSINIT/GSREAD from offset Y

Sub-entry of parse_filename_gs that accepts a non-zero Y offset into the (os_text_ptr) string. Initialises GSINIT, reads chars via GSREAD into &0E30, CR-terminates the result, and sets up fs_crc_lo/hi to point at the buffer.

86BC .parse_filename_gs_y←1← 8C13 JSR
LDX #&ff ; X=&FF: next INX wraps to first char index
86BE CLC ; C=0 for GSINIT: parse from current position
86BF JSR gsinit ; Initialise GS string parser
86C2 BEQ terminate_filename ; Empty string: skip to CR terminator
86C4 .quote1←1← 86CD BCC
JSR gsread ; Read next character via GSREAD
86C7 BCS terminate_filename ; C=1 from GSREAD: end of string reached
86C9 INX ; Advance buffer index
86CA STA fs_filename_buf,x ; Store parsed character to &0E30+X
86CD BCC quote1 ; ALWAYS loop (GSREAD clears C on success) ALWAYS branch
86CF .terminate_filename←2← 86C2 BEQ← 86C7 BCS
INX ; Terminate parsed string with CR
86D0 LDA #&0d ; CR = &0D
86D2 STA fs_filename_buf,x ; Store CR terminator at end of string
86D5 LDA #&30 ; Point fs_crc_lo/hi at &0E30 parse buffer
86D7 STA fs_crc_lo ; fs_crc_lo = &30
86D9 LDA #&0e ; fs_crc_hi = &0E → buffer at &0E30
86DB STA fs_crc_hi ; Store high byte
86DD RTS ; Return; X = string length

FILEV handler (OSFILE entry point)

Calls save_fscv_args (&85A6) to preserve A/X/Y, then JSR &86B0 to copy the 2-byte filename pointer from the parameter block to os_text_ptr and fall through to parse_filename_gs (&86BA) which parses the filename into &0E30+. 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 &86F4) A=&00: save file (filev_save at &876A) A=&01-&06: attribute operations (filev_attrib_dispatch at &8870) 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
86DE .filev_handler
JSR save_fscv_args ; Save A/X/Y in FS workspace
86E1 JSR copy_filename_ptr ; Copy filename ptr from param block to os_text_ptr
86E4 LDA fs_last_byte_flag ; Recover function code from saved A
86E6 BPL saveop ; A >= 0: save (&00) or attribs (&01-&06)
86E8 CMP #&ff ; A=&FF? Only &FF is valid for load
86EA BEQ loadop ; A=&FF: branch to load path
86EC JMP restore_args_return ; Unknown negative code: no-op return
86EF .loadop←1← 86EA BEQ
JSR copy_filename ; Copy parsed filename to cmd buffer
86F2 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
86F4 .send_fs_examine←1← 8DD4 JSR
LDA #&92 ; Port &92 = PLDATA (data transfer port)
86F6 STA fs_cmd_urd ; Overwrite URD field with data port number
86F9 LDA #&2a ; A=&2A: error ptr for retry
86FB JSR prepare_cmd_clv ; Build FS header (V=1: CLV path)
86FE LDY #6 ; Y=6: param block byte 6
8700 LDA (fs_options),y ; Byte 6: use file's own load address?
8702 BNE lodfil ; Non-zero: use FS reply address (lodfil)
8704 JSR copy_load_addr_from_params ; Zero: copy caller's load addr first
8707 JSR copy_reply_to_params ; Then copy FS reply to param block
870A BCC skip_lodfil ; Carry clear from prepare_cmd_clv: skip lodfil
870C .lodfil←1← 8702 BNE
JSR copy_reply_to_params ; Copy FS reply addresses to param block
870F JSR copy_load_addr_from_params ; Then copy load addr from param block
8712 .skip_lodfil←1← 870A BCC
LDY #4 ; Compute end address = load + file length
8714 .copy_load_end_addr←1← 871F BNE
LDA fs_load_addr,x ; Load address byte
8716 STA txcb_end,x ; Store as current transfer position
8718 ADC fs_file_len,x ; Add file length byte
871B STA fs_work_4,x ; Store as end position
871D INX ; Next address byte
871E DEY ; Decrement byte counter
871F BNE copy_load_end_addr ; Loop for all 4 address bytes
8721 SEC ; Adjust high byte for 3-byte length overflow
8722 SBC fs_file_len_3 ; Subtract 4th length byte from end addr
8725 STA fs_work_7 ; Store adjusted end address high byte
8727 JSR print_file_info ; Display file info after FS reply
872A JSR send_data_blocks ; Transfer file data in &80-byte blocks
872D LDX #2 ; Copy 3-byte file length to FS reply cmd buffer
872F .floop←1← 8736 BPL
LDA fs_file_len_3,x ; Load file length byte
8732 STA fs_cmd_data,x ; Store in FS command data buffer
8735 DEX ; Next byte (count down)
8736 BPL floop ; Loop for 3 bytes (X=2,1,0)
8738 BMI setup_fs_reply_attrs ; 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.

873A .send_data_blocks←2← 872A JSR← 8A94 JSR
JSR compare_addresses ; Compare two 4-byte addresses
873D BEQ return_lodchk ; Addresses match: transfer complete
873F LDA #&92 ; Port &92 for data block transfer
8741 STA txcb_port ; Store port to TXCB command byte
8743 .send_block_loop←1← 875F BNE
LDX #3 ; Set up next &80-byte block for transfer
8745 .copy_block_addrs←1← 874E BPL
LDA txcb_end,x ; Swap: current addr -> source, end -> current
8747 STA txcb_start,x ; Source addr = current position
8749 LDA fs_work_4,x ; Load end address byte
874B STA txcb_end,x ; Dest = end address (will be clamped)
874D DEX ; Next address byte
874E BPL copy_block_addrs ; Loop for all 4 bytes
8750 LDA #&7f ; Command &7F = data block transfer
8752 STA txcb_ctrl ; Store to TXCB control byte
8754 JSR send_to_fs_star ; Send this block to the fileserver
8757 LDY #3 ; Y=3: compare 4 bytes (3..0)
8759 .lodchk←1← 8762 BPL
LDA txcb_end,y ; Compare current vs end address (4 bytes)
875C EOR fs_work_4,y ; XOR with end address byte
875F BNE send_block_loop ; Not equal: more blocks to send
8761 DEY ; Next byte
8762 BPL lodchk ; Loop for all 4 address bytes
8764 .return_lodchk←1← 873D BEQ
RTS ; All equal: transfer complete
8765 .saveop←1← 86E6 BPL
BEQ filev_save ; A=0: SAVE handler
8767 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.

876A .filev_save←1← 8765 BEQ
LDX #4 ; Process 4 address bytes (load/exec/start/end)
876C LDY #&0e ; Y=&0E: start from end-address in param block
876E .savsiz←1← 8788 BNE
LDA (fs_options),y ; Read end-address byte from param block
8770 STA port_ws_offset,y ; Save to port workspace for transfer setup
8773 JSR sub_4_from_y ; Y = Y-4: point to start-address byte
8776 SBC (fs_options),y ; end - start = transfer length byte
8778 STA fs_cmd_csd,y ; Store length byte in FS command buffer
877B PHA ; Save length byte for param block restore
877C LDA (fs_options),y ; Read corresponding start-address byte
877E STA port_ws_offset,y ; Save to port workspace
8781 PLA ; Restore length byte from stack
8782 STA (fs_options),y ; Replace param block entry with length
8784 JSR add_5_to_y ; Y = Y+5: advance to next address group
8787 DEX ; Decrement address byte counter
8788 BNE savsiz ; Loop for all 4 address bytes
878A LDY #9 ; Copy load/exec addresses to FS command buffer
878C .copy_save_params←1← 8792 BNE
LDA (fs_options),y ; Read load/exec address byte from params
878E STA fs_cmd_csd,y ; Copy to FS command buffer
8791 DEY ; Next byte (descending)
8792 BNE copy_save_params ; Loop for bytes 9..1
8794 LDA #&91 ; Port &91 for save command
8796 STA fs_cmd_urd ; Overwrite URD field with port number
8799 STA fs_error_ptr ; Save port &91 for flow control ACK
879B LDX #&0b ; Append filename at offset &0B in cmd buffer
879D JSR copy_string_to_cmd ; Append filename to cmd buffer at offset X
87A0 LDY #1 ; Y=1: function code for save
87A2 LDA #&14 ; A=&14: FS function code for SAVE
87A4 JSR prepare_cmd_clv ; Build header and send FS save command
87A7 JSR print_file_info ; Display filename being saved
87AA .save_csd_display
LDA fs_cmd_data ; Load CSD from FS reply
87AD JSR transfer_file_blocks ; Transfer file data blocks to server
87B0 .setup_fs_reply_attrs←1← 8738 BMI
LDA #&2a ; A=&2A: error ptr for retry
87B2 STA fs_error_ptr ; Store error pointer for TX poll
87B4 .send_fs_reply
JSR send_fs_reply_cmd ; Send FS reply acknowledgement
87B7 .skip_catalogue_msg
STX fs_reply_cmd ; Store reply command for attr decode
87BA LDY #&0e ; Y=&0E: access byte offset in param block
87BC LDA fs_cmd_data ; Load access byte from FS reply
87BF JSR decode_attribs_5bit ; Convert FS access to BBC attribute format
87C2 BEQ direct_attr_copy ; Z=1: first byte, use A directly
87C4 .copy_attr_loop←1← 87CC BNE
LDA fs_reply_data,y ; Load attribute byte from FS reply
87C7 .direct_attr_copy←1← 87C2 BEQ
STA (fs_options),y ; Store decoded access in param block
87C9 INY ; Next attribute byte
87CA CPY #&12 ; Copied all 4 bytes? (Y=&0E..&11)
87CC BNE copy_attr_loop ; Loop for 4 attribute bytes
87CE 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).

87D1 .copy_load_addr_from_params←2← 8704 JSR← 870F JSR
LDY #5 ; Start at offset 5 (top of 4-byte addr)
87D3 .lodrl1←1← 87DB BCS
LDA (fs_options),y ; Read from parameter block
87D5 STA work_ae,y ; Store to local workspace
87D8 DEY ; Next byte (descending)
87D9 CPY #2 ; Copy offsets 5,4,3,2 (4 bytes)
87DB BCS lodrl1 ; Loop while Y >= 2
87DD .add_5_to_y←1← 8784 JSR
INY ; Y += 5
87DE .add_4_to_y←1← 8A71 JSR
INY ; Y += 4
87DF INY ; (continued)
87E0 INY ; (continued)
87E1 INY ; (continued)
87E2 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)
87E3 .copy_reply_to_params←2← 8707 JSR← 870C JSR
LDY #&0d ; Start at offset &0D (top of range)
87E5 TXA ; First store uses X (attrib byte)
87E6 .lodrl2←1← 87EE BCS
STA (fs_options),y ; Write to parameter block
87E8 LDA fs_cmd_urd,y ; Read next byte from reply buffer
87EB DEY ; Next byte (descending)
87EC CPY #2 ; Copy offsets &0D down to 2
87EE BCS lodrl2 ; Loop until offset 2 reached
87F0 .sub_4_from_y←1← 8773 JSR
DEY ; Y -= 4
87F1 .sub_3_from_y←2← 8888 JSR← 8A79 JSR
DEY ; Y -= 3
87F2 DEY ; (continued)
87F3 DEY ; (continued)
87F4 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).

87F5 .transfer_file_blocks←2← 87AD JSR← 8A8F JSR
PHA ; Save FS command byte on stack
87F6 JSR compare_addresses ; Compare two 4-byte addresses
87F9 BEQ restore_ay_return ; Addresses equal: nothing to transfer
87FB .next_block←1← 884A BNE
LDX #0 ; X=0: clear hi bytes of block size
87FD LDY #4 ; Y=4: process 4 address bytes
87FF STX fs_reply_cmd ; Clear block size hi byte 1
8802 STX fs_load_vector ; Clear block size hi byte 2
8805 CLC ; CLC for ADC in loop
8806 .block_addr_loop←1← 8813 BNE
LDA fs_load_addr,x ; Source = current position
8808 STA txcb_start,x ; Store source address byte
880A ADC fs_func_code,x ; Add block size to current position
880D STA txcb_end,x ; Store dest address byte
880F STA fs_load_addr,x ; Advance current position
8811 INX ; Next address byte
8812 DEY ; Decrement byte counter
8813 BNE block_addr_loop ; Loop for all 4 bytes
8815 BCS clamp_dest_setup ; Carry: address overflowed, clamp
8817 SEC ; SEC for SBC in overshoot check
8818 .savchk←1← 8820 BNE
LDA fs_load_addr,y ; Check if new pos overshot end addr
881B SBC fs_work_4,y ; Subtract end address byte
881E INY ; Next byte
881F DEX ; Decrement counter
8820 BNE savchk ; Loop for 4-byte comparison
8822 BCC send_block ; C=0: no overshoot, proceed
8824 .clamp_dest_setup←1← 8815 BCS
LDX #3 ; Overshot: clamp dest to end address
8826 .clamp_dest_addr←1← 882B BPL
LDA fs_work_4,x ; Load end address byte
8828 STA txcb_end,x ; Replace dest with end address
882A DEX ; Next byte
882B BPL clamp_dest_addr ; Loop for all 4 bytes
882D .send_block←1← 8822 BCC
PLA ; Recover original FS command byte
882E PHA ; Re-push for next iteration
882F PHP ; Save processor flags (C from cmp)
8830 STA txcb_port ; Store command byte in TXCB
8832 LDA #&80 ; 128-byte block size for data transfer
8834 STA txcb_ctrl ; Store size in TXCB control byte
8836 JSR setup_tx_ptr_c0 ; Point TX ptr to &00C0; transmit
8839 LDA fs_error_ptr ; ACK port for flow control
883B JSR init_tx_ctrl_port ; Set reply port for ACK receive
883E PLP ; Restore flags (C=overshoot status)
883F BCS restore_ay_return ; C=1: all data sent (overshot), done
8841 LDA #&91 ; Command &91 = data block transfer
8843 STA txcb_port ; Store command &91 in TXCB
8845 LDA #&2a ; A=&2A: error ptr for retry
8847 JSR send_to_fs ; Transmit block and wait (BRIANX)
884A 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
884C .fscv_1_eof
PHA ; Save A (function code)
884D TXA ; X = file handle to check
884E JSR handle_to_mask_a ; Convert handle to bitmask in A
8851 TYA ; Y = handle bitmask from conversion
8852 AND fs_eof_flags ; Local hint: is EOF possible for this handle?
8855 TAX ; X = result of AND (0 = not at EOF)
8856 BEQ restore_ay_return ; Hint clear: definitely not at EOF
8858 PHA ; Save bitmask for clear_fs_flag
8859 STY fs_cmd_data ; Handle byte in FS command buffer
885C LDY #&11 ; Y=&11: FS function code FCEOF Y=function code for HDRFN
885E LDX #1 ; X=preserved through header build
8860 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8863 PLA ; Restore bitmask
8864 LDX fs_cmd_data ; FS reply: non-zero = at EOF
8867 BNE restore_ay_return ; At EOF: skip flag clear
8869 JSR clear_fs_flag ; Not at EOF: clear the hint bit
886C .restore_ay_return←4← 87F9 BEQ← 883F BCS← 8856 BEQ← 8867 BNE
PLA ; Restore A
886D LDY fs_block_offset ; Restore Y
886F 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
8870 .filev_attrib_dispatch←1← 8767 JMP
STA fs_cmd_data ; Store function code in FS cmd buffer
8873 CMP #6 ; A=6? (delete)
8875 BEQ cha6 ; Yes: jump to delete handler
8877 BCS check_attrib_result ; A>=7: unsupported, fall through to return
8879 CMP #5 ; A=5? (read catalogue info)
887B BEQ cha5 ; Yes: jump to read info handler
887D CMP #4 ; A=4? (write attributes only)
887F BEQ cha4 ; Yes: jump to write attrs handler
8881 CMP #1 ; A=1? (write all catalogue info)
8883 BEQ get_file_protection ; Yes: jump to write-all handler
8885 ASL ; A=2 or 3: convert to param block offset
8886 ASL ; A*4: 2->8, 3->12
8887 TAY ; Y = A*4
8888 JSR sub_3_from_y ; Y = A*4 - 3 (load addr offset for A=2)
888B LDX #3 ; X=3: copy 4 bytes
888D .chalp1←1← 8894 BPL
LDA (fs_options),y ; Load address byte from param block
888F STA fs_func_code,x ; Store to FS cmd data area
8892 DEY ; Next source byte (descending)
8893 DEX ; Next dest byte
8894 BPL chalp1 ; Loop for 4 bytes
8896 LDX #5 ; X=5: data extent for filename copy
8898 BNE copy_filename_to_cmd ; ALWAYS branch
889A .get_file_protection←1← 8883 BEQ
JSR decode_attribs_6bit ; A=1: encode protection from param block
889D STA fs_file_attrs ; Store encoded attrs at &0F0E
88A0 LDY #9 ; Y=9: source offset in param block
88A2 LDX #8 ; X=8: dest offset in cmd buffer
88A4 .chalp2←1← 88AB BNE
LDA (fs_options),y ; Load byte from param block
88A6 STA fs_cmd_data,x ; Store to FS cmd buffer
88A9 DEY ; Next source byte (descending)
88AA DEX ; Next dest byte
88AB BNE chalp2 ; Loop until X=0 (8 bytes copied)
88AD LDX #&0a ; X=&0A: data extent past attrs+addrs
88AF .copy_filename_to_cmd←2← 8898 BNE← 88CD BNE
JSR copy_string_to_cmd ; Append filename to cmd buffer
88B2 LDY #&13 ; Y=&13: fn code for FCSAVE (write attrs)
88B4 BNE send_fs_cmd_v1 ; ALWAYS branch to send command ALWAYS branch
88B6 .cha6←1← 8875 BEQ
JSR copy_filename ; A=6: copy filename (delete)
88B9 LDY #&14 ; Y=&14: fn code for FCDEL (delete)
88BB .send_fs_cmd_v1←1← 88B4 BNE
BIT tx_ctrl_upper ; Set V=1 (BIT trick: &B3 has bit 6 set)
88BE JSR init_tx_ctrl_data ; Send via prepare_fs_cmd_v (V=1 path)
88C1 .check_attrib_result←1← 8877 BCS
BCS attrib_error_exit ; C=1: &D6 not-found, skip to return
88C3 BCC argsv_check_return ; C=0: success, copy reply to param block ALWAYS branch
88C5 .cha4←1← 887F BEQ
JSR decode_attribs_6bit ; A=4: encode attrs from param block
88C8 STA fs_func_code ; Store encoded attrs at &0F06
88CB LDX #2 ; X=2: data extent (1 attr byte + fn)
88CD BNE copy_filename_to_cmd ; ALWAYS branch to append filename ALWAYS branch
88CF .cha5←1← 887B BEQ
LDX #1 ; X=1: filename only, no data extent
88D1 JSR copy_string_to_cmd ; Copy filename to cmd buffer
88D4 LDY #&12 ; Y=&12: fn code for FCEXAM (read info) Y=function code for HDRFN
88D6 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
88D9 LDA fs_obj_type ; Save object type from FS reply
88DC STX fs_obj_type ; Clear reply byte (X=0 on success) X=0 on success, &D6 on not-found
88DF STX fs_len_clear ; Clear length high byte in reply
88E2 JSR decode_attribs_5bit ; Decode 5-bit access byte from FS reply
88E5 LDY #&0e ; Y=&0E: attrs offset in param block
88E7 STA (fs_options),y ; Store decoded attrs at param block +&0E
88E9 DEY ; Y=&0D: start copy below attrs Y=&0d
88EA LDX #&0c ; X=&0C: copy from reply offset &0C down
88EC .copy_fs_reply_to_cb←1← 88F3 BNE
LDA fs_cmd_data,x ; Load reply byte (load/exec/length)
88EF STA (fs_options),y ; Store to param block
88F1 DEY ; Next dest byte (descending)
88F2 DEX ; Next source byte
88F3 BNE copy_fs_reply_to_cb ; Loop until X=0 (12 bytes copied)
88F5 INX ; X=0 -> X=2 for length high copy
88F6 INX ; INX again: X=2
88F7 LDY #&11 ; Y=&11: length high dest in param block
88F9 .cha5lp←1← 8900 BPL
LDA fs_access_level,x ; Load length high byte from reply
88FC STA (fs_options),y ; Store to param block
88FE DEY ; Next dest byte (descending)
88FF DEX ; Next source byte
8900 BPL cha5lp ; Loop for 3 length-high bytes
8902 LDA fs_cmd_data ; Return object type in A
8905 .attrib_error_exit←1← 88C1 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
8907 .argsv_handler
JSR save_fscv_args ; Save A/X/Y registers for later restore
890A CMP #3 ; Function >= 3?
890C BCS restore_args_return ; A>=3 (ensure/flush): no-op for NFS
890E CPY #0 ; Test file handle
8910 BEQ argsv_dispatch_a ; Y=0: FS-level query, not per-file
8912 JSR handle_to_mask_clc ; Convert handle to bitmask
8915 STY fs_cmd_data ; Store bitmask as first cmd data byte
8918 LSR ; LSR splits A: C=1 means write (A=1)
8919 STA fs_func_code ; Store function code to cmd data byte 2
891C BCS save_args_handle ; C=1: write path, copy ptr from caller
891E LDY #&0c ; Y=&0C: FCRDSE (read sequential pointer) Y=function code for HDRFN
8920 LDX #2 ; X=2: 3 data bytes in command X=preserved through header build
8922 JSR prepare_fs_cmd ; Build and send FS command Prepare FS command buffer (12 references)
8925 STA fs_last_byte_flag ; Clear last-byte flag on success A=0 on success (from build_send_fs_cmd)
8927 LDX fs_options ; X = saved control block ptr low
8929 LDY #2 ; Y=2: copy 3 bytes of file pointer
892B STA zp_work_3,x ; Zero high byte of 3-byte pointer
892D .copy_fileptr_reply←1← 8934 BPL
LDA fs_cmd_data,y ; Read reply byte from FS cmd data
8930 STA zp_work_2,x ; Store to caller's control block
8932 DEX ; Next byte (descending)
8933 DEY ; Next source byte
8934 BPL copy_fileptr_reply ; Loop for all 3 bytes
8936 .argsv_check_return←1← 88C3 BCC
BCC restore_args_return ; C=0 (read): return to caller
8938 .save_args_handle←1← 891C BCS
TYA ; Save bitmask for set_fs_flag later
8939 PHA ; Push bitmask
893A LDY #3 ; Y=3: copy 4 bytes of file pointer
893C .copy_fileptr_to_cmd←1← 8943 BPL
LDA zp_work_3,x ; Read caller's pointer byte
893E STA fs_data_count,y ; Store to FS command data area
8941 DEX ; Next source byte
8942 DEY ; Next destination byte
8943 BPL copy_fileptr_to_cmd ; Loop for all 4 bytes
8945 LDY #&0d ; Y=&0D: FCWRSE (write sequential pointer) Y=function code for HDRFN
8947 LDX #5 ; X=5: 6 data bytes in command X=preserved through header build
8949 JSR prepare_fs_cmd ; Build and send FS command Prepare FS command buffer (12 references)
894C STX fs_last_byte_flag ; Save not-found status from X X=0 on success, &D6 on not-found
894E PLA ; Recover bitmask for EOF hint update
894F 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.

8952 .restore_args_return←8← 86EC JMP← 87CE JMP← 890C BCS← 8936 BCC← 8961 BNE← 89C8 BCC← 8A19 JMP← 8E1D JMP
LDA fs_last_byte_flag ; A = saved function code / command
8954 .restore_xy_return←5← 8905 BPL← 895E BNE← 896D BPL← 8998 BCS← 89AC BNE
LDX fs_options ; X = saved control block ptr low
8956 LDY fs_block_offset ; Y = saved control block ptr high
8958 RTS ; Return to MOS with registers restored
8959 .argsv_dispatch_a←1← 8910 BEQ
TAY ; Y = A = byte count for copy loop
895A BNE halve_args_a ; A!=0: copy command context block
895C LDA #5 ; FS number 5 (loaded as &0A, LSR'd)
895E BNE restore_xy_return ; ALWAYS branch
8960 .halve_args_a←1← 895A BNE
LSR ; Shared: halve A (A=0 or A=2 paths)
8961 BNE restore_args_return ; Return with A = FS number or 1
8963 .osarg1←1← 8969 BPL
LDA fs_cmd_context,y ; Copy command context to caller's block
8966 STA (fs_options),y ; Store to caller's parameter block
8968 DEY ; Next byte (descending)
8969 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.

896B .return_a_zero←3← 897B BNE← 8AB9 JMP← 8B56 JMP
LDA #0 ; A=operation (0=close, &40=read, &80=write, &C0=R/W)
896D BPL restore_xy_return ; ALWAYS branch

FINDV handler (OSFIND entry point)

A=0: close file -- delegates to close_handle (&89AE) 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
896F .findv_handler
JSR save_fscv_args_with_ptrs ; Save A/X/Y and set up pointers
8972 SEC ; SEC distinguishes open (A>0) from close
8973 JSR handle_to_mask ; Convert file handle to bitmask (Y2FS)
8976 TAX ; A=preserved
8977 BEQ close_handle ; A=0: close file(s)
8979 AND #&3f ; Valid open modes: &40, &80, &C0 only
897B BNE return_a_zero ; Invalid mode bits: return
897D TXA ; A = original mode byte
897E EOR #&80 ; Convert MOS mode to FS protocol flags
8980 ASL ; ASL: shift mode bits left
8981 STA fs_cmd_data ; Flag 1: read/write direction
8984 ROL ; ROL: Flag 2 into bit 0
8985 STA fs_func_code ; Flag 2: create vs existing file
8988 JSR parse_filename_gs ; Parse filename from command line
898B LDX #2 ; X=2: copy after 2-byte flags
898D JSR copy_string_to_cmd ; Copy filename to FS command buffer
8990 LDY #6 ; Y=6: FS function code FCOPEN
8992 BIT tx_ctrl_upper ; Set V flag from l83b3 bit 6
8995 JSR init_tx_ctrl_data ; Build and send FS open command
8998 BCS restore_xy_return ; Error: restore and return
899A LDA fs_cmd_data ; Load reply handle from FS
899D TAX ; X = new file handle
899E JSR set_fs_flag ; Set EOF hint + sequence bits
; 3.35K fix: OR handle bit into fs_sequence_nos
; (&0E08). Without this, a newly opened file could
; inherit a stale sequence number from a previous
; file using the same handle, causing byte-stream
; protocol errors.
89A1 TXA ; A=handle bitmask for new file
89A2 ORA fs_sequence_nos ; OR new handle into seq tracker
89A5 STA fs_sequence_nos ; Store updated sequence byte
89A8 TXA ; A=single-bit bitmask
89A9 JSR mask_to_handle ; Convert bitmask to handle number (FS2A)
89AC 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)
89AE .close_handle←1← 8977 BEQ
TYA ; A = handle (Y preserved in A) Y=preserved
89AF BNE close_single_handle ; Y>0: close single file
89B1 LDA #osbyte_close_spool_exec ; Close SPOOL/EXEC before FS close-all
89B3 JSR osbyte ; Close any *SPOOL and *EXEC files
89B6 LDY #0 ; Y=0: close all handles on server
89B8 .close_single_handle←1← 89AF BNE
STY fs_cmd_data ; Handle byte in FS command buffer
89BB LDX #1 ; X=preserved through header build
89BD LDY #7 ; Y=function code for HDRFN
89BF JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
89C2 LDA fs_cmd_data ; Reply handle for flag update
89C5 JSR set_fs_flag ; Update EOF/sequence tracking bits
89C8 .close_opt_return←1← 89EC 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
89CA .fscv_0_opt
CPX #4 ; Is it *OPT 4,Y?
89CC BNE check_opt1 ; No: check for *OPT 1
89CE CPY #4 ; Y must be 0-3 for boot option
89D0 BCC optl1 ; Y < 4: valid boot option
89D2 .check_opt1←1← 89CC BNE
DEX ; Not *OPT 4: check for *OPT 1
89D3 BNE opter1 ; Not *OPT 1 either: bad option
89D5 .set_messages_flag
STY fs_messages_flag ; Set local messages flag (*OPT 1,Y)
89D8 BCC opt_return ; Return via restore_args_return
89DA .opter1←1← 89D3 BNE
LDA #7 ; Error index 7 (Bad option)
89DC JMP nlisne ; Generate BRK error
89DF .optl1←1← 89D0 BCC
STY fs_cmd_data ; Boot option value in FS command
89E2 LDY #&16 ; Y=&16: FS function code FCOPT Y=function code for HDRFN
89E4 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
89E7 LDY fs_block_offset ; Restore Y from saved value
89E9 STY fs_boot_option ; Cache boot option locally
89EC .opt_return←1← 89D8 BCC
BCC close_opt_return ; Return via restore_args_return
89EE .adjust_addrs_9←1← 8AAD JSR
LDY #9 ; Y=9: adjust 9 address bytes
89F0 JSR adjust_addrs_clc ; Adjust with carry clear
89F3 .adjust_addrs_1←1← 8B9D JSR
LDY #1 ; Y=1: adjust 1 address byte
89F5 .adjust_addrs_clc←1← 89F0 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
89F6 .adjust_addrs←2← 8AB3 JSR← 8BA9 JSR
LDX #&fc ; X=&FC: index into &0E06 area (wraps to 0)
89F8 .adjust_addr_byte←1← 8A0B BNE
LDA (fs_options),y ; Load byte from param block
89FA BIT fs_load_addr_2 ; Test sign of adjustment direction
89FC BMI subtract_adjust ; Negative: subtract instead
89FE ADC fs_cmd_context,x ; Add adjustment value
8A01 JMP gbpbx ; Skip to store result
8A04 .subtract_adjust←1← 89FC BMI
SBC fs_cmd_context,x ; Subtract adjustment value
8A07 .gbpbx←1← 8A01 JMP
STA (fs_options),y ; Store adjusted byte back
8A09 INY ; Next param block byte
8A0A INX ; Next adjustment byte (X wraps &FC->&00)
8A0B BNE adjust_addr_byte ; Loop 4 times (X=&FC,&FD,&FE,&FF,done)
8A0D 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
8A0E .gbpbv_handler
JSR save_fscv_args ; Save A/X/Y to FS workspace
8A11 TAX ; X = call number for range check
8A12 BEQ gbpb_invalid_exit ; A=0: invalid, restore and return
8A14 DEX ; Convert to 0-based (A=0..7)
8A15 CPX #8 ; Range check: must be 0-7
8A17 BCC gbpbx1 ; In range: continue to handler
8A19 .gbpb_invalid_exit←1← 8A12 BEQ
JMP restore_args_return ; Out of range: restore args and return
8A1C .gbpbx1←1← 8A17 BCC
TXA ; Recover 0-based function code
8A1D LDY #0 ; Y=0: param block byte 0 (file handle)
8A1F PHA ; Save function code on stack
8A20 CMP #4 ; A>=4: info queries, dispatch separately
8A22 BCC gbpbe1 ; A<4: file read/write operations
8A24 JMP osgbpb_info ; Dispatch to OSGBPB 5-8 info handler
8A27 .gbpbe1←1← 8A22 BCC
LDA (fs_options),y ; Get file handle from param block byte 0
8A29 JSR handle_to_mask_a ; Convert handle to bitmask for EOF flags
8A2C STY fs_cmd_data ; Store handle in FS command data
8A2F LDY #&0b ; Y=&0B: start at param block byte 11
8A31 LDX #6 ; X=6: copy 6 bytes of transfer params
8A33 .gbpbf1←1← 8A3F BNE
LDA (fs_options),y ; Load param block byte
8A35 STA fs_func_code,x ; Store to FS command buffer at &0F06+X
8A38 DEY ; Previous param block byte
8A39 CPY #8 ; Skip param block offset 8 (the handle)
8A3B BNE gbpbx0 ; Not at handle offset: continue
8A3D DEY ; Extra DEY to skip handle byte
8A3E .gbpbx0←1← 8A3B BNE
.gbpbf2←1← 8A3B BNE
DEX ; Decrement copy counter
8A3F BNE gbpbf1 ; Loop for all 6 bytes
8A41 PLA ; Recover function code from stack
8A42 LSR ; LSR: odd=read (C=1), even=write (C=0)
8A43 PHA ; Save function code again (need C later)
8A44 BCC gbpbl1 ; Even (write): X stays 0
8A46 INX ; Odd (read): X=1
8A47 .gbpbl1←1← 8A44 BCC
STX fs_func_code ; Store FS direction flag
8A4A LDY #&0b ; Y=&0B: command data extent
8A4C LDX #&91 ; Command &91=put, &92=get
8A4E PLA ; Recover function code
8A4F PHA ; Save again for later direction check
8A50 BEQ gbpb_write_path ; Even (write): keep &91 and Y=&0B
8A52 LDX #&92 ; Odd (read): use &92 (get) instead
8A54 DEY ; Read: one fewer data byte in command Y=&0a
8A55 .gbpb_write_path←1← 8A50 BEQ
STX fs_cmd_urd ; Store port to FS command URD field
8A58 STX fs_error_ptr ; Save port for error recovery
8A5A LDX #8 ; X=8: command data bytes
8A5C LDA fs_cmd_data ; Load handle from FS command data
8A5F JSR prepare_cmd_with_flag ; Build FS command with handle+flag
8A62 LDA fs_load_addr_3 ; Save seq# for byte-stream flow control
8A64 STA fs_sequence_nos ; Store to FS sequence number workspace
8A67 LDX #4 ; X=4: copy 4 address bytes
8A69 .gbpbl3←1← 8A7D BNE
LDA (fs_options),y ; Set up source/dest from param block
8A6B STA addr_work,y ; Store as source address
8A6E STA txcb_pos,y ; Store as current transfer position
8A71 JSR add_4_to_y ; Skip 4 bytes to reach transfer length
8A74 ADC (fs_options),y ; Dest = source + length
8A76 STA addr_work,y ; Store as end address
8A79 JSR sub_3_from_y ; Back 3 to align for next iteration
8A7C DEX ; Decrement byte counter
8A7D BNE gbpbl3 ; Loop for all 4 address bytes
8A7F INX ; X=1 after loop
8A80 .gbpbf3←1← 8A87 BPL
LDA fs_cmd_csd,x ; Copy CSD data to command buffer
8A83 STA fs_func_code,x ; Store at &0F06+X
8A86 DEX ; Decrement counter
8A87 BPL gbpbf3 ; Loop for X=1,0
8A89 PLA ; Odd (read): send data to FS first
8A8A BNE gbpb_read_path ; Non-zero: skip write path
8A8C LDA fs_cmd_urd ; Load port for transfer setup
8A8F JSR transfer_file_blocks ; Transfer data blocks to fileserver
8A92 BNE set_gbpb_error_ptr ; Non-zero: branch past error ptr
8A94 .gbpb_read_path←1← 8A8A BNE
JSR send_data_blocks ; Read path: receive data blocks from FS
8A97 .set_gbpb_error_ptr←1← 8A92 BNE
LDA #&2a ; A=&2A: error ptr for retry
8A99 STA fs_error_ptr ; Store error ptr for TX poll
8A9B .wait_fs_reply
JSR send_fs_reply_cmd ; Wait for FS reply command
8A9E LDA (fs_options,x) ; Load handle mask for EOF flag update
8AA0 BIT fs_cmd_data ; Check FS reply: bit 7 = not at EOF
8AA3 BMI skip_clear_flag ; Bit 7 set: not EOF, skip clear
8AA5 JSR clear_fs_flag ; At EOF: clear EOF hint for this handle
8AA8 .skip_clear_flag←1← 8AA3 BMI
JSR set_fs_flag ; Set EOF hint flag (may be at EOF)
8AAB STX fs_load_addr_2 ; Direction=0: forward adjustment
8AAD JSR adjust_addrs_9 ; Adjust param block addrs by +9 bytes
8AB0 DEC fs_load_addr_2 ; Direction=&FF: reverse adjustment
8AB2 SEC ; SEC for reverse subtraction
8AB3 JSR adjust_addrs ; Adjust param block addrs (reverse)
8AB6 ASL fs_cmd_data ; Shift bit 7 into C for return flag
8AB9 JMP return_a_zero ; Return via restore_args path
8ABC .get_disc_title←1← 8AEC BEQ
LDY #&15 ; Y=&15: function code for disc title Y=function code for HDRFN
8ABE JSR prepare_fs_cmd ; Build and send FS command Prepare FS command buffer (12 references)
8AC1 LDA fs_boot_option ; Load boot option from FS workspace
8AC4 STA fs_boot_data ; Store boot option in reply area
8AC7 STX fs_load_addr ; X=0: reply data start offset X=0 on success, &D6 on not-found
8AC9 STX fs_load_addr_hi ; Clear reply buffer high byte
8ACB LDA #&12 ; A=&12: 18 bytes of reply data
8ACD STA fs_load_addr_2 ; Store as byte count for copy
8ACF 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.

8AD1 .osgbpb_info←1← 8A24 JMP
LDY #4 ; Y=4: check param block byte 4
8AD3 LDA tx_in_progress ; Check if destination is in Tube space
8AD6 BEQ store_tube_flag ; No Tube: skip Tube address check
8AD8 CMP (fs_options),y ; Compare Tube flag with addr byte 4
8ADA BNE store_tube_flag ; Mismatch: not Tube space
8ADC DEY ; Y=&03
8ADD SBC (fs_options),y ; Y=3: subtract addr byte 3 from flag
8ADF .store_tube_flag←2← 8AD6 BEQ← 8ADA BNE
STA rom_svc_num ; Non-zero = Tube transfer required
8AE1 .info2←1← 8AE7 BNE
LDA (fs_options),y ; Copy param block bytes 1-4 to workspace
8AE3 STA fs_last_byte_flag,y ; Store to &BD+Y workspace area
8AE6 DEY ; Previous byte
8AE7 BNE info2 ; Loop for bytes 3,2,1
8AE9 PLA ; Sub-function: AND #3 of (original A - 4)
8AEA AND #3 ; Mask to 0-3 (OSGBPB 5-8 → 0-3)
8AEC BEQ get_disc_title ; A=0 (OSGBPB 5): read disc title
8AEE LSR ; LSR: A=0 (OSGBPB 6) or A=1 (OSGBPB 7)
8AEF BEQ gbpb6_read_name ; A=0 (OSGBPB 6): read CSD/LIB name
8AF1 BCS gbpb8_read_dir ; C=1 (OSGBPB 8): read filenames from dir
8AF3 .gbpb6_read_name←1← 8AEF BEQ
TAY ; Y=0 for CSD or carry for fn code select Y=function code
8AF4 LDA fs_csd_handle,y ; Get CSD/LIB/URD handles for FS command
8AF7 STA fs_cmd_csd ; Store CSD handle in command buffer
8AFA LDA fs_lib_handle ; Load LIB handle from workspace
8AFD STA fs_cmd_lib ; Store LIB handle in command buffer
8B00 LDA fs_urd_handle ; Load URD handle from workspace
8B03 STA fs_cmd_urd ; Store URD handle in command buffer
8B06 LDX #&12 ; X=&12: buffer extent for command data X=buffer extent (command-specific data bytes)
8B08 STX fs_cmd_y_param ; Store X as function code in header
8B0B LDA #&0d ; &0D = 13 bytes of reply data expected
8B0D STA fs_func_code ; Store reply length in command buffer
8B10 STA fs_load_addr_2 ; Store as byte count for copy loop
8B12 LSR ; LSR: &0D >> 1 = 6 A=timeout period for FS reply
8B13 STA fs_cmd_data ; Store as command data byte
8B16 CLC ; CLC for standard FS path
8B17 JSR build_send_fs_cmd ; Build and send FS command (DOFSOP)
8B1A STX fs_load_addr_hi ; X=0 on success, &D6 on not-found
8B1C INX ; INX: X=1 after build_send_fs_cmd
8B1D STX fs_load_addr ; Store X as reply start offset
8B1F .copy_reply_to_caller←2← 8ACF BNE← 8B92 JSR
LDA rom_svc_num ; Copy FS reply to caller's buffer
8B21 BNE tube_transfer ; Non-zero: use Tube transfer path
8B23 LDX fs_load_addr ; X = reply start offset
8B25 LDY fs_load_addr_hi ; Y = reply buffer high byte
8B27 .copy_reply_bytes←1← 8B30 BNE
LDA fs_cmd_data,x ; Load reply data byte
8B2A STA (fs_crc_lo),y ; Store to caller's buffer
8B2C INX ; Next source byte
8B2D INY ; Next destination byte
8B2E DEC fs_load_addr_2 ; Decrement remaining bytes
8B30 BNE copy_reply_bytes ; Loop until all bytes copied
8B32 BEQ gbpb_done ; ALWAYS branch to exit ALWAYS branch
8B34 .tube_transfer←1← 8B21 BNE
JSR tube_claim_loop ; Claim Tube transfer channel
8B37 LDA #1 ; A=1: Tube claim type 1 (write)
8B39 LDX fs_options ; X = param block address low
8B3B LDY fs_block_offset ; Y = param block address high
8B3D INX ; INX: advance past byte 0
8B3E BNE no_page_wrap ; No page wrap: keep Y
8B40 INY ; Page wrap: increment high byte
8B41 .no_page_wrap←1← 8B3E BNE
JSR tube_addr_claim ; Claim Tube address for transfer
8B44 LDX fs_load_addr ; X = reply data start offset
8B46 .tbcop1←1← 8B4F BNE
LDA fs_cmd_data,x ; Load reply data byte
8B49 STA tube_data_register_3 ; Send byte to Tube via R3
8B4C INX ; Next source byte
8B4D DEC fs_load_addr_2 ; Decrement remaining bytes
8B4F BNE tbcop1 ; Loop until all bytes sent to Tube
8B51 LDA #&83 ; Release Tube after transfer complete
8B53 JSR tube_addr_claim ; Release Tube address claim
8B56 .gbpb_done←2← 8B32 BEQ← 8BAC BEQ
JMP return_a_zero ; Return via restore_args path
8B59 .gbpb8_read_dir←1← 8AF1 BCS
LDY #9 ; OSGBPB 8: read filenames from dir
8B5B LDA (fs_options),y ; Byte 9: number of entries to read
8B5D STA fs_func_code ; Store as reply count in command buffer
8B60 LDY #5 ; Y=5: byte 5 = starting entry number
8B62 LDA (fs_options),y ; Load starting entry number
8B64 STA fs_data_count ; Store in command buffer
8B67 LDX #&0d ; X=&0D: command data extent X=preserved through header build
8B69 STX fs_reply_cmd ; Store extent in command buffer
8B6C LDY #2 ; Y=2: function code for dir read
8B6E STY fs_load_addr ; Store 2 as reply data start offset
8B70 STY fs_cmd_data ; Store 2 as command data byte
8B73 INY ; Y=3: function code for header read Y=function code for HDRFN Y=&03
8B74 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8B77 STX fs_load_addr_hi ; X=0 after FS command completes X=0 on success, &D6 on not-found
8B79 LDA fs_func_code ; Load reply entry count
8B7C STA (fs_options,x) ; Store at param block byte 0 (X=0)
8B7E LDA fs_cmd_data ; Load entries-read count from reply
8B81 LDY #9 ; Y=9: param block byte 9
8B83 ADC (fs_options),y ; Add to starting entry number
8B85 STA (fs_options),y ; Update param block with new position
8B87 LDA txcb_end ; Load total reply length
8B89 SBC #7 ; Subtract header (7 bytes) from reply len
8B8B STA fs_func_code ; Store adjusted length in command buffer
8B8E STA fs_load_addr_2 ; Store as byte count for copy loop
8B90 BEQ skip_copy_reply ; Zero bytes: skip copy
8B92 JSR copy_reply_to_caller ; Copy reply data to caller's buffer
8B95 .skip_copy_reply←1← 8B90 BEQ
LDX #2 ; X=2: clear 3 bytes
8B97 .zero_cmd_bytes←1← 8B9B BPL
STA fs_data_count,x ; Zero out &0F07+X area
8B9A DEX ; Next byte
8B9B BPL zero_cmd_bytes ; Loop for X=2,1,0
8B9D JSR adjust_addrs_1 ; Adjust pointer by +1 (one filename read)
8BA0 SEC ; SEC for reverse adjustment
8BA1 DEC fs_load_addr_2 ; Reverse adjustment for updated counter
8BA3 LDA fs_cmd_data ; Load entries-read count
8BA6 STA fs_func_code ; Store in command buffer
8BA9 JSR adjust_addrs ; Adjust param block addresses
8BAC BEQ gbpb_done ; Z=1: all done, exit
8BAE .tube_claim_loop←3← 8B34 JSR← 8BB3 BCC← 8E06 JSR
LDA #&c3 ; A=&C3: Tube claim with retry
8BB0 JSR tube_addr_claim ; Request Tube address claim
8BB3 BCC tube_claim_loop ; C=0: claim failed, retry
8BB5 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 &8BE4 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.

8BB6 .fscv_3_star_cmd←1← 8275 JMP
JSR save_fscv_args_with_ptrs ; Save A/X/Y and set up command ptr
8BB9 LDX #&ff ; X=&FF: table index (pre-incremented)
8BBB STX fs_crflag ; Disable column formatting
8BBD .scan_cmd_table←1← 8BD8 BNE
LDY #&ff ; Y=&FF: input index (pre-incremented)
8BBF .decfir←1← 8BCA BEQ
INY ; Advance input pointer
8BC0 INX ; Advance table pointer
8BC1 .decmor←1← 8BDC BCS
LDA fs_cmd_match_table,x ; Load table character
8BC4 BMI dispatch_cmd ; Bit 7: end of name, dispatch
8BC6 EOR (fs_crc_lo),y ; XOR input char with table char
8BC8 AND #&df ; Case-insensitive (clear bit 5)
8BCA BEQ decfir ; Match: continue comparing
8BCC DEX ; Mismatch: back up table pointer
8BCD .decmin←1← 8BD1 BPL
INX ; Skip to end of table entry
8BCE LDA fs_cmd_match_table,x ; Load table byte
8BD1 BPL decmin ; Loop until bit 7 set (end marker)
8BD3 LDA (fs_crc_lo),y ; Check input for '.' abbreviation
8BD5 INX ; Skip past handler high byte
8BD6 CMP #&2e ; Is input '.' (abbreviation)?
8BD8 BNE scan_cmd_table ; No: try next table entry
8BDA INY ; Yes: skip '.' in input
8BDB DEX ; Back to handler high byte
8BDC BCS decmor ; ALWAYS branch; dispatch via BMI
8BDE .dispatch_cmd←1← 8BC4 BMI
PHA ; Push handler address high byte
8BDF LDA cmd_table_entry_1,x ; Load handler address low byte
8BE2 PHA ; Push handler address low byte
8BE3 RTS ; Dispatch via RTS (addr-1 on stack)

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." → &80B4 (forward_star_cmd) — placed first as a fudge to catch *I. abbreviation before matching *I AM "I AM" → &807E (i_am_handler: parse station.net, logon) "EX" → &8BFA (ex_handler: extended catalogue) "BYE"\r → &8383 (bye_handler: logoff) <catch-all> → &80B4 (forward anything else to FS)

8BE4 .fs_cmd_match_table←2← 8BC1 LDA← 8BCE LDA
EOR #&2e ; Match last char against '.' for *I. abbreviation
8BE6 EQUB &80, &B3
8BE8 EQUS "I AM"
8BEC EQUB &80
8BED EQUS "}EX"
8BF0 EQUB &8B, &F9
8BF2 EQUS "BYE"
8BF5 EQUB &0D, &83, &82, &80, &B3
fall through ↓

*EX handler (extended catalogue)

Sets &B7=&01 and &B5=&03, then branches into fscv_5_cat at &8C0A, bypassing fscv_5_cat's default column setup. &B7=1 gives one entry per line with full details (vs &B7=3 for *CAT which gives multiple files per line).

8BFA .ex_handler
LDX #1 ; X=1: single-entry-per-line mode
8BFC STX fs_work_7 ; Store column format selector
8BFE LDA #3 ; A=3: FS function code for EX
8C00 BNE init_cat_params ; 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.

8C02 .fscv_5_cat
LDX #3 ; X=3: column count for multi-column layout
8C04 STX fs_work_7 ; CRFLAG=3: first entry will trigger newline
8C06 LDY #0 ; Y=0: initialise column counter
8C08 STY fs_crflag ; Clear CRFLAG column counter
8C0A LDA #&0b ; A=&0B: examine argument count
8C0C .init_cat_params←1← 8C00 BNE
STA fs_work_5 ; Store examine argument count
8C0E LDA #6 ; A=6: examine format type in command
8C10 STA fs_cmd_data ; Store format type at &0F05
8C13 JSR parse_filename_gs_y ; Set up command parameter pointers
8C16 LDX #1 ; X=1: copy dir name at cmd offset 1
8C18 JSR copy_string_to_cmd ; Copy directory name to command buffer
8C1B LDY #&12 ; Y=function code for HDRFN
8C1D JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8C20 LDX #3 ; X=3: start printing from reply offset 3
8C22 JSR print_reply_bytes ; Print directory title (10 chars)
8C25 JSR print_inline ; Print '('
8C28 EQUS "("
8C29 LDA fs_reply_stn ; Load station number from FS reply
8C2C JSR print_decimal ; Print station number as decimal
8C2F JSR print_inline ; Print ') '
8C32 EQUS ") "
8C38 LDX fs_access_level ; Load access level from FS reply
8C3B BNE print_public ; Non-zero: Public access
8C3D JSR print_inline ; Print 'Owner' + CR
8C40 EQUS "Owner."
8C46 BNE cat_print_header ; Skip past 'Owner' string
8C48 .print_public←1← 8C3B BNE
JSR print_inline ; Print 'Public' + CR
8C4B EQUS "Public."
8C52 .cat_print_header←1← 8C46 BNE
LDY #&15 ; Y=function code for HDRFN
8C54 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8C57 INX ; X=1: past command code byte
8C58 LDY #&10 ; Y=&10: print 16 characters
8C5A JSR print_reply_counted ; Print disc/CSD name from reply
8C5D JSR print_inline ; Print ' Option '
8C60 EQUS " Option "
8C6B LDA fs_boot_option ; Load boot option from FS workspace
8C6E TAX ; X = boot option for name table lookup
8C6F JSR print_hex ; Print boot option as hex digit
8C72 JSR print_inline ; Print ' ('
8C75 EQUS " ("
8C77 LDY return_9,x ; Y = option name offset from table
8C7A .print_option_char←1← 8C83 BNE
LDA return_9,y ; Load next char of option name
8C7D BMI done_option_name ; Bit 7 set: end of name string
8C7F JSR osasci ; Write character
8C82 INY ; Next character
8C83 BNE print_option_char ; Continue printing option name
8C85 .done_option_name←1← 8C7D BMI
JSR print_inline ; Print ')' + CR + 'Dir. '
8C88 EQUS ").Dir. "
8C8F LDX #&11 ; X=&11: CSD name offset in reply
8C91 JSR print_reply_bytes ; Print CSD name from reply buffer
8C94 JSR print_inline ; Print ' Lib. ' header
8C97 EQUS " Lib. "
8CA1 LDX #&1b ; X=&1B: library name offset in reply
8CA3 JSR print_reply_bytes ; Print library name
8CA6 JSR print_inline ; Print two CRs (blank line)
8CA9 EQUS ".."
8CAB STY fs_func_code ; Init examine start offset to 0
8CAE STY fs_work_4 ; Save start offset in zero page for loop
8CB0 LDX fs_work_5 ; Load examine arg count for batch size
8CB2 STX fs_data_count ; Store as request count at &0F07
8CB5 .cat_examine_loop←1← 8CDE BNE
LDX fs_work_7 ; Load column count for display format
8CB7 STX fs_cmd_data ; Store column count in command data
8CBA LDX #3 ; X=3: copy directory name at offset 3
8CBC JSR copy_string_to_cmd ; Append directory name to examine command
8CBF LDY #3 ; Y=function code for HDRFN
8CC1 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8CC4 LDA fs_cmd_data ; Load entry count from reply
8CC7 BEQ print_newline ; Zero entries returned: catalogue done
8CC9 LDX #2 ; X=2: first entry offset in reply
8CCB JSR print_dir_from_offset ; Print/format this directory entry
8CCE CLC ; CLC for addition
8CCF LDA fs_work_4 ; Load current examine start offset
8CD1 ADC fs_cmd_data ; Add entries returned this batch
8CD4 STA fs_func_code ; Update next examine start offset
8CD7 STA fs_work_4 ; Save updated start offset
8CD9 LDA fs_work_5 ; Reload batch size for next request
8CDB STA fs_data_count ; Store batch size in command buffer
8CDE BNE cat_examine_loop ; Loop for remaining characters
; Option name encoding: in 3.35, the boot option names ("Off",
; "Load", "Run", "Exec") are scattered through the code rather
; than stored as a contiguous table. They are addressed via
; base+offset from return_9 (&8CE0), whose first four bytes
; (starting with the RTS opcode &60) double as the offset table:
; &60→&8D40 "Off", &73→&8D53 "Load",
; &9B→&8D7B "Run", &18→&8CF8 "Exec"
; Each string is terminated by the next instruction's opcode
; having bit 7 set (e.g. LDA #imm = &A9, RTS = &60).
8CE0 .return_9←2← 8C77 LDY← 8C7A LDA
RTS ; RTS doubles as offset table byte
8CE1 EQUB &73, &9B, &18
8CE4 EQUS "L.!"

Boot command strings for auto-boot

The four boot options use OSCLI strings at offsets within page &8C: Option 0 (Off): offset &F3 → &8CF3 = bare CR (empty command) Option 1 (Load): offset &E4 → &8CE4 = "L.!BOOT" (dual-purpose: the bytes &4C='L', &2E='.', &21='!' at &8CE4 are followed by "BOOT" at &8CE7, forming the OSCLI string "L.!BOOT") Option 2 (Run): offset &E6 → boot_cmd_strings-1 = "!BOOT" (bare filename = *RUN) Option 3 (Exec): offset &EC → &8CEC = "E.!BOOT"

This is a classic BBC ROM space optimisation: the string data overlaps with other byte sequences to save space.

8CE7 .boot_cmd_strings
EQUS "BOOT"
8CEB EQUB &0D
8CEC EQUS "E.!BOOT"
8CF3 EQUB &0D

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. Referenced by fsreply_1_copy_handles_boot via LDX boot_option_offsets,Y.

8CF4 .boot_option_offsets←1← 8E33 LDX
EQUB &F3 ; Opt 0 (Off): bare CR
8CF5 EQUB &E4 ; Opt 1 (Load): L.!BOOT
8CF6 EQUB &E6 ; Opt 2 (Run): !BOOT
8CF7 EQUB &EC ; Opt 3 (Exec): E.!BOOT
8CF8 EQUS "Exec" ; Data bytes: boot_cmd_strings 'ec'

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

8CFC .print_file_info←2← 8727 JSR← 87A7 JSR
LDY fs_messages_flag ; Check if messages enabled
8CFF BEQ return_5 ; Zero: no info to display, return
8D01 LDY #0 ; Y=0: start of filename
8D03 .next_filename_char←1← 8D11 BNE
LDA (fs_crc_lo),y ; Load next filename character
8D05 CMP #&0d ; CR: end of filename
8D07 BEQ pad_filename_spaces ; CR found: pad remaining with spaces
8D09 CMP #&20 ; Space: end of name field
8D0B BEQ pad_filename_spaces ; Space found: pad with spaces
8D0D JSR osasci ; Write character
8D10 INY ; Advance to next character
8D11 BNE next_filename_char ; Continue printing filename
8D13 .pad_filename_spaces←3← 8D07 BEQ← 8D0B BEQ← 8D19 BCC
JSR print_space ; Print space for padding
8D16 INY ; Advance column counter
8D17 CPY #&0c ; Reached 12 columns?
8D19 BCC pad_filename_spaces ; No: continue padding
8D1B .print_hex_fields
LDY #5 ; Y=5: load address offset (4 bytes)
8D1D JSR print_hex_bytes ; Print load address
8D20 JSR print_exec_and_len ; Print exec address and file length
8D23 .print_newline←1← 8CC7 BEQ
JMP osnewl ; Write newline (characters 10 and 13)
8D26 .print_exec_and_len←1← 8D20 JSR
LDY #9 ; Y=9: exec address offset (4 bytes)
8D28 JSR print_hex_bytes ; Print exec address
8D2B LDY #&0c ; Y=&0C: file length offset
8D2D LDX #3 ; X=3: print 3 bytes (24-bit length)
8D2F BNE num01 ; ALWAYS branch
8D31 .print_hex_bytes←2← 8D1D JSR← 8D28 JSR
LDX #4 ; X=4: print 4 hex bytes
8D33 .num01←2← 8D2F BNE← 8D3A BNE
LDA (fs_options),y ; Load byte from parameter block
8D35 JSR print_hex ; Print as two hex digits
8D38 DEY ; Next byte (descending)
8D39 DEX ; Count down
8D3A BNE num01 ; Loop until 4 bytes printed
8D3C .print_space←1← 8D13 JSR
LDA #&20 ; A=space character
8D3E BNE print_char_always ; ALWAYS branch
8D40 EQUS "Off"

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.

8D43 .copy_filename←4← 80B4 JSR← 86EF JSR← 88B6 JSR← 8DC2 JSR
LDX #0 ; Start writing at &0F05 (after cmd header)
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)
8D45 .copy_string_to_cmd←6← 879D JSR← 88AF JSR← 88D1 JSR← 898D JSR← 8C18 JSR← 8CBC JSR
LDY #0 ; Start copying from offset 0
8D47 .copy_string_from_offset←1← 8D50 BNE
LDA (fs_crc_lo),y ; Load next byte from source string
8D49 STA fs_cmd_data,x ; Store to FS command buffer (&0F05+X)
8D4C INX ; Advance write position
8D4D INY ; Advance source pointer
8D4E EOR #&0d ; XOR with CR: result=0 if byte was CR
8D50 BNE copy_string_from_offset ; Loop until CR copied
8D52 .return_5←2← 8CFF BEQ← 8D5C BMI
RTS ; Return; X = next free position in buffer
8D53 EQUS "Load"

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.

8D57 .fsreply_0_print_dir
LDX #0 ; X=0: start from first reply byte
8D59 .print_dir_from_offset←2← 8CCB JSR← 8D79 BNE
LDA fs_cmd_data,x ; Load byte from FS reply buffer
8D5C BMI return_5 ; Bit 7 set: end of string, return
8D5E BNE infol2 ; Non-zero: print character
fall through ↓

Print catalogue column separator or newline

Handles column formatting for *CAT display. On a null byte separator, advances the column counter modulo 4: prints a 2-space separator between columns, or a CR at column 0. Called from fsreply_0_print_dir.

8D60 .cat_column_separator
LDY fs_crflag ; Null byte: check column counter
8D62 BMI print_cr_separator ; Negative: print CR (no columns)
8D64 INY ; Advance column counter
8D65 TYA ; Transfer to A for modulo
8D66 AND #3 ; Modulo 4 columns
8D68 STA fs_crflag ; Update column counter
8D6A BEQ print_cr_separator ; Column 0: start new line
8D6C JSR print_inline ; Print 2-space column separator
8D6F EQUS " "
8D71 BNE next_dir_entry ; Not last column: skip CR
8D73 .print_cr_separator←2← 8D62 BMI← 8D6A BEQ
LDA #&0d ; A=&0D: CR for column separator
8D75 .infol2←1← 8D5E BNE
JSR osasci ; Write character 13
8D78 .next_dir_entry←1← 8D71 BNE
INX ; Next byte in reply buffer
8D79 BNE print_dir_from_offset ; Loop until end of buffer
8D7B EQUS "Run"
fall through ↓

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)
8D7E .print_decimal←2← 822E JSR← 8C2C JSR
TAY ; Y = value to print
8D7F LDA #&64 ; Divisor = 100 (hundreds digit)
8D81 JSR print_decimal_digit ; Print hundreds digit
8D84 LDA #&0a ; Divisor = 10 (tens digit)
8D86 JSR print_decimal_digit ; Print tens digit
8D89 LDA #1 ; Divisor = 1; fall through to units
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') via OSASCI. 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
8D8B .print_decimal_digit←2← 8D81 JSR← 8D86 JSR
STA fs_error_ptr ; Save divisor to workspace
8D8D TYA ; A = dividend (from Y)
8D8E LDX #&2f ; X = &2F = ASCII '0' - 1
8D90 SEC ; Prepare for subtraction
8D91 .divide_subtract←1← 8D94 BCS
INX ; Count one subtraction (next digit value)
8D92 SBC fs_error_ptr ; A = A - divisor
8D94 BCS divide_subtract ; Loop while A >= 0 (borrow clear)
8D96 ADC fs_error_ptr ; Undo last subtraction: A = remainder
8D98 TAY ; Y = remainder for caller
8D99 TXA ; A = X = ASCII digit character
8D9A .print_digit←1← 8DB0 BNE
JMP osasci ; Write character

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)
8D9D .print_hex←2← 8C6F JSR← 8D35 JSR
PHA ; Save byte for low nibble
8D9E LSR ; Shift high nibble to low position
8D9F LSR ; Shift high nibble to low position
8DA0 LSR ; Shift high nibble to low position
8DA1 LSR ; Shift high nibble to low position
8DA2 JSR print_hex_nibble ; Print high nibble as hex digit
8DA5 PLA ; Restore original byte
8DA6 AND #&0f ; Mask to low nibble
8DA8 .print_hex_nibble←1← 8DA2 JSR
ORA #&30 ; Convert to ASCII '0' base
8DAA CMP #&3a ; Above '9'?
8DAC BCC print_char_always ; No: digit 0-9, skip adjustment
8DAE ADC #6 ; Add 7 (6+C) for 'A'-'F'
8DB0 .print_char_always←2← 8D3E BNE← 8DAC BCC
BNE print_digit ; ALWAYS branch to print character

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.

8DB2 .print_reply_bytes←3← 8C22 JSR← 8C91 JSR← 8CA3 JSR
LDY #&0a ; Y=10: character count
8DB4 .print_reply_counted←2← 8C5A JSR← 8DBC BNE
LDA fs_cmd_data,x ; Load byte from FS reply buffer
8DB7 JSR osasci ; Write character
8DBA INX ; Next buffer offset
8DBB DEY ; Decrement character counter
8DBC BNE print_reply_counted ; Loop until all chars printed
8DBE RTS ; Return to caller

FSCV 2/4: */ (run) and *RUN handler

Parses the filename via parse_filename_gs and copies it into the command buffer, then falls through to fsreply_4_notify_exec to send the FS load-as-command request.

8DBF .fscv_2_star_run
JSR parse_filename_gs ; Parse filename from command line
8DC2 JSR copy_filename ; Copy filename to FS command buffer
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.

8DC5 .fsreply_4_notify_exec
LDX #&0e ; X=&0E: FS command buffer offset
8DC7 STX fs_block_offset ; Store block offset for FS command
8DC9 LDA #&10 ; A=&10: 16 bytes of command data
8DCB STA fs_options ; Store options byte
8DCD STA fs_work_16 ; Store to FS workspace
8DD0 LDX #&4a ; X=&4A: TXCB size for load command
8DD2 LDY #5 ; Y=5: FCCMND (load as command)
8DD4 JSR send_fs_examine ; Send FS examine/load command
8DD7 LDY #0 ; Y=0: init GSINIT string offset
8DD9 CLC ; CLC: no flags for GSINIT
8DDA JSR gsinit ; Init string scanning state
8DDD .gsread_scan_loop←1← 8DE0 BCC
JSR gsread ; Read next char via GSREAD
8DE0 BCC gsread_scan_loop ; More chars: continue scanning
8DE2 DEY ; Back up Y to last valid char
8DE3 .skip_filename_spaces←1← 8DE8 BEQ
INY ; Advance past current position
8DE4 LDA (os_text_ptr),y ; Read char from command line
8DE6 CMP #&20 ; Is it a space?
8DE8 BEQ skip_filename_spaces ; Skip leading spaces in filename
8DEA CLC ; CLC for pointer addition
8DEB TYA ; A = Y (offset past spaces)
8DEC ADC os_text_ptr ; Add base pointer to get abs addr
8DEE STA fs_cmd_context ; Store filename pointer (low)
8DF1 LDA os_text_ptr_hi ; Load text pointer high byte
8DF3 ADC #0 ; Add carry from low byte addition
8DF5 STA fs_context_hi ; Store filename pointer (high)
8DF8 SEC ; SEC for address range test
8DF9 LDA tx_in_progress ; Check for Tube co-processor
8DFC BEQ exec_at_load_addr ; No Tube: execute locally
8DFE ADC fs_load_upper ; Check load address upper bytes
8E01 ADC fs_addr_check ; Continue address range check
8E04 BCS exec_at_load_addr ; Carry set: not Tube space, exec locally
8E06 JSR tube_claim_loop ; Claim Tube transfer channel
8E09 LDX #9 ; X=9: source offset in FS reply
8E0B LDY #&0f ; Y=&0F: page &0F (FS command buffer)
8E0D LDA #4 ; A=4: Tube transfer type 4 (256-byte)
8E0F JMP tube_addr_claim ; Transfer data to Tube co-processor
8E12 .exec_at_load_addr←2← 8DFC BEQ← 8E04 BCS
JMP (fs_load_vector) ; Execute at load address via indirect JMP

Set library handle

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

On EntryYlibrary handle from FS reply
8E15 .fsreply_5_set_lib
STY fs_lib_handle ; Save library handle from FS reply
8E18 BNE jmp_restore_args ; Non-zero: jump to restore exit
fall through ↓

Set CSD handle

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

On EntryYCSD handle from FS reply
8E1A .fsreply_3_set_csd
STY fs_csd_handle ; Store CSD handle from FS reply
8E1D .jmp_restore_args←2← 8E18 BNE← 8E2E BCC
JMP restore_args_return ; 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.

8E20 .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 jmp_restore_args. Called when the FS reply contains updated handle values but no boot action is needed.

8E21 .fsreply_2_copy_handles
LDX #3 ; Copy 4 bytes: boot option + 3 handles
8E23 BCC copy_handles_loop ; SDISC: skip boot option, copy handles only
8E25 .logon2←1← 8E2C BPL
LDA fs_cmd_data,x ; Load from FS reply (&0F05+X)
8E28 STA fs_urd_handle,x ; Store to handle workspace (&0E02+X)
8E2B .copy_handles_loop←1← 8E23 BCC
DEX ; Next handle (descending)
8E2C BPL logon2 ; Loop while X >= 0
8E2E BCC jmp_restore_args ; 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.

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

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

8E3B .net_1_read_handle
LDY #&6f ; Y=&6F: offset to handle byte in RX buf
8E3D LDA (net_rx_ptr),y ; Load file handle from RX buffer
8E3F STA osword_pb_ptr ; Store in parameter block pointer (&F0)
8E41 RTS ; Return to caller

Load handle from &F0 and calculate workspace offset

Loads the file handle byte from &F0, then falls through to calc_handle_offset which converts handle * 12 to a workspace byte offset. Validates offset < &48.

8E42 .load_handle_calc_offset←2← 8E56 JSR← 8E66 JSR
LDA osword_pb_ptr ; Load handle from &F0
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
8E44 .calc_handle_offset←3← 82EF JSR← 8F77 JSR← 8F90 JSR
ASL ; A = handle * 2
8E45 ASL ; A = handle * 4
8E46 PHA ; Push handle*4 onto stack
8E47 ASL ; A = handle * 8
8E48 TSX ; X = stack pointer
8E49 ADC l0101,x ; A = handle*8 + handle*4 = handle*12
8E4C TAY ; Y = offset into handle workspace
8E4D PLA ; Clean up stack (discard handle*4)
8E4E CMP #&48 ; Offset >= &48? (6 handles max)
8E50 BCC return_6 ; Valid: return with C clear
8E52 LDY #0 ; Invalid: Y = 0
8E54 TYA ; A = 0, C set (error) A=&00
8E55 .return_6←1← 8E50 BCC
.return_calc_handle←1← 8E50 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.

8E56 .net_2_read_handle_entry
JSR load_handle_calc_offset ; Look up handle &F0 in workspace
8E59 BCS rxpol2 ; Invalid handle: return 0
8E5B LDA (nfs_workspace),y ; Load stored handle value
8E5D CMP #&3f ; &3F = unused/closed slot marker
8E5F BNE store_handle_return ; Slot in use: return actual value
8E61 .rxpol2←2← 8E59 BCS← 8E69 BCS
LDA #0 ; Return 0 for closed/invalid handle
8E63 .store_handle_return←1← 8E5F BNE
STA osword_pb_ptr ; Store result back to &F0
8E65 RTS ; Return

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

8E66 .net_3_close_handle
JSR load_handle_calc_offset ; Look up handle &F0 in workspace
8E69 BCS rxpol2 ; Invalid handle: return 0
8E6B ROL rx_flags ; Preserve carry via ROL
8E6E LDA #&3f ; A=&3F: handle closed/unused marker
8E70 STA (nfs_workspace),y ; Mark handle as closed in workspace
8E72 ROR rx_flags ; Restore carry via ROR
8E75 RTS ; Return

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 &8E80.

8E76 .svc_8_osword
LDA osbyte_a_copy ; Command code from &EF
8E78 SBC #&0f ; Subtract &0F: OSWORD &0F-&13 become indices 0-4
8E7A BMI return_7 ; Outside our OSWORD range, exit
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 &8EB1 for station read/set.

8E7C .osword_12_handler
CMP #5 ; Only OSWORDs &0F-&13 (index 0-4)
8E7E BCS return_7 ; 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 &8E9F (low) / &8EA4 (high).

8E80 .fs_osword_dispatch
TAX ; X = sub-function code for table lookup
8E81 LDA #&8f ; Push return addr high (restore_args)
8E83 PHA ; Push return addr high byte
8E84 LDA #&be ; Push return addr low (restore_args)
8E86 PHA ; Push return addr low byte
8E87 LDA fs_osword_tbl_hi,x ; Load handler address high byte from table
8E8A PHA ; Push high byte for RTS dispatch
8E8B LDA fs_osword_tbl_lo,x ; Load handler address low byte from table
8E8E PHA ; Dispatch table: low bytes for OSWORD &0F-&13 handlers
8E8F LDY #2 ; Y=2: save 3 bytes (&AA-&AC)
8E91 .save1←1← 8E97 BPL
LDA fs_last_byte_flag,y ; Load param block pointer byte
8E94 STA (net_rx_ptr),y ; Save to NFS workspace via (net_rx_ptr)
8E96 DEY ; Next byte (descending)
8E97 BPL save1 ; Loop for all 3 bytes
8E99 INY ; Y=0 after BPL exit; INY makes Y=1
8E9A LDA (osword_pb_ptr),y ; Read sub-function code from (&F0)+1
8E9C STY rom_svc_num ; Store Y=1 to &A9
8E9E RTS ; RTS dispatches to pushed handler address
8E9F .fs_osword_tbl_lo←1← 8E8B LDA
EQUB <(osword_0f_handler-1) ; Dispatch table: low bytes for OSWORD &0F-&13 handlers
8EA0 EQUB <(osword_10_handler-1)
8EA1 EQUB <(osword_11_handler-1)
8EA2 EQUB <(osword_12_dispatch-1)
8EA3 EQUB <(econet_tx_rx-1)
8EA4 .fs_osword_tbl_hi←1← 8E87 LDA
EQUB >(osword_0f_handler-1) ; Dispatch table: high bytes for OSWORD &0F-&13 handlers
8EA5 EQUB >(osword_10_handler-1)
8EA6 EQUB >(osword_11_handler-1)
8EA7 EQUB >(osword_12_dispatch-1)
8EA8 EQUB >(econet_tx_rx-1)

Copy one byte between OSWORD param block and workspace

If C=1, copies one byte from (osword_pb_ptr),Y to (fs_crc_lo),Y (param to workspace). Always loads the workspace byte into A. Used as the inner body of copy_param_block's bidirectional copy loop, and called directly by OSWORD &0F/&10/&11 handlers to set up or retrieve workspace data.

8EA9 .copy_param_byte←5← 8EB5 BPL← 8ECC JSR← 8EE1 JSR← 8F12 JMP← 8FA7 JSR
BCC load_workspace_byte ; C=0: skip param-to-workspace copy
8EAB LDA (osword_pb_ptr),y ; Load byte from param block
8EAD STA (fs_crc_lo),y ; Store to workspace
8EAF .load_workspace_byte←1← 8EA9 BCC
LDA (fs_crc_lo),y ; Load byte from workspace
fall through ↓

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)

8EB1 .copy_param_block
STA (osword_pb_ptr),y ; Store to param block (no-op if C=1)
8EB3 .copyl3
INY ; Advance to next byte
8EB4 DEX ; Decrement byte counter
8EB5 BPL copy_param_byte ; Loop while X >= 0
8EB7 .return_7←2← 8E7A BMI← 8E7E BCS
.logon3←2← 8E7A BMI← 8E7E BCS
.return_copy_param←2← 8E7A BMI← 8E7E 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
8EB8 .osword_0f_handler
ASL tx_clear_flag ; ASL: set C if TX in progress
8EBB TYA ; Save param block high byte to A
8EBC BCC readry ; C=0: read path
8EBE LDA net_rx_ptr_hi ; User TX CB in workspace page (high byte)
8EC0 STA fs_crc_hi ; Set param block high byte
8EC2 STA nmi_tx_block_hi ; Set LTXCBP high byte for low-level TX
8EC4 LDA #&6f ; &6F: offset into workspace for user TXCB
8EC6 STA fs_crc_lo ; Set param block low byte
8EC8 STA nmi_tx_block ; Set LTXCBP low byte for low-level TX
8ECA LDX #&0f ; X=15: copy 16 bytes (OSWORD param block)
8ECC JSR copy_param_byte ; Copy param block to user TX control block
8ECF JMP trampoline_tx_setup ; Start user transmit via BRIANX

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 (&8EE7) to return just the buffer size and args size without copying the data.

8ED2 .osword_11_handler
LDA net_rx_ptr_hi ; Set source high byte from workspace page
8ED4 STA fs_crc_hi ; Store as copy source high byte in &BF
8ED6 LDY #&7f ; JSRSIZ at workspace offset &7F
8ED8 LDA (net_rx_ptr),y ; Load buffer size from workspace
8EDA INY ; Y=&80: start of JSR argument data Y=&80
8EDB STY fs_crc_lo ; Store &80 as copy source low byte
8EDD TAX ; X = buffer size (loop counter)
8EDE DEX ; X = size-1 (0-based count for copy)
8EDF LDY #0 ; Y=0: start of destination param block
8EE1 JSR copy_param_byte ; Copy X+1 bytes from workspace to param
8EE4 JMP clear_jsr_protection ; Clear JSR protection status (CLRJSR)
8EE7 .read_args_size←1← 8F36 BEQ
LDY #&7f ; Y=&7F: JSRSIZ offset (READRB entry)
8EE9 LDA (net_rx_ptr),y ; Load buffer size from workspace
8EEB LDY #1 ; Y=1: param block offset for size byte
8EED STA (osword_pb_ptr),y ; Store buffer size to (&F0)+1
8EEF INY ; Y=2: param block offset for args size Y=&02
8EF0 LDA #&80 ; A=&80: argument data starts at offset &80
8EF2 .readry←1← 8EBC BCC
STA (osword_pb_ptr),y ; Store args start offset to (&F0)+2
8EF4 RTS ; Return
8EF5 .osword_12_offsets←1← 8F09 LDA
EQUB &FF, &01

OSWORD &12 sub-function dispatch

Dispatches OSWORD &12 sub-functions 0-9. Sub-functions 0-3 read or write workspace paths (static page &0D or dynamic workspace). Sub-functions 4/5 read/set the JSR protection status byte. Sub-function 8 reads the FS handle from the RX buffer. Sub-function 9 reads ARGS buffer size info. Sub-functions >= 6 and unrecognised codes are handled via the shared rsl1 error path.

8EF7 .osword_12_dispatch
CMP #6 ; Sub-function >= 6?
8EF9 BCS rsl1 ; Yes: out of range, error
8EFB CMP #4 ; Sub-function 4 or 5?
8EFD BCS rssl1 ; Sub-function 4 or 5: read/set protection
8EFF LSR ; LSR: 0->0, 1->0, 2->1, 3->1
8F00 LDX #&0d ; X=&0D: default to static workspace page
8F02 TAY ; Transfer LSR result to Y for indexing
8F03 BEQ set_workspace_page ; Y=0 (sub 0-1): use page &0D
8F05 LDX nfs_workspace_hi ; Y=1 (sub 2-3): use dynamic workspace
8F07 .set_workspace_page←1← 8F03 BEQ
STX fs_crc_hi ; Store workspace page in &BF (hi byte)
8F09 LDA osword_12_offsets,y ; Load offset: &FF (sub 0-1) or &01 (sub 2-3)
8F0C STA fs_crc_lo ; Store offset in &BE (lo byte)
8F0E LDX #1 ; X=1: copy 2 bytes
8F10 LDY #1 ; Y=1: start at param block offset 1
8F12 JMP copy_param_byte ; Jump to read/write workspace path
8F15 .rssl1←1← 8EFD BCS
LSR ; LSR A: test bit 0 of sub-function
8F16 INY ; Y=1: offset for protection byte
8F17 LDA (osword_pb_ptr),y ; Load protection byte from param block
8F19 BCS rssl2 ; C=1 (odd sub): set protection
8F1B LDA prot_status ; C=0 (even sub): read current status
8F1E STA (osword_pb_ptr),y ; Return current value to param block
8F20 .rssl2←1← 8F19 BCS
STA prot_status ; Update protection status
8F23 STA saved_jsr_mask ; Also save as JSR mask backup
8F26 RTS ; Return
8F27 .read_fs_handle←1← 8F32 BEQ
LDY #&14 ; Y=&14: RX buffer offset for FS handle
8F29 LDA (net_rx_ptr),y ; Read FS reply handle from RX data
8F2B LDY #1 ; Y=1: param block byte 1
8F2D STA (osword_pb_ptr),y ; Return handle to caller's param block
8F2F RTS ; Return
8F30 .rsl1←1← 8EF9 BCS
CMP #8 ; Sub-function 8: read FS handle
8F32 BEQ read_fs_handle ; Match: read handle from RX buffer
8F34 CMP #9 ; Sub-function 9: read args size
8F36 BEQ read_args_size ; Match: read ARGS buffer info
8F38 BPL return_last_error ; Sub >= 10 (bit 7 clear): read error
8F3A LDY #3 ; Y=3: start from handle 3 (descending)
8F3C LSR ; LSR: test read/write bit
8F3D BCC readc1 ; C=0: read handles from workspace
8F3F STY nfs_temp ; Init loop counter at Y=3
8F41 .copy_handles_to_ws←1← 8F50 BNE
LDY nfs_temp ; Reload loop counter
8F43 LDA (osword_pb_ptr),y ; Read handle from caller's param block
8F45 JSR handle_to_mask_a ; Convert handle number to bitmask
8F48 TYA ; TYA: get bitmask result
8F49 LDY nfs_temp ; Reload loop counter
8F4B STA fs_server_net,y ; Store bitmask to FS server table
8F4E DEC nfs_temp ; Next handle (descending)
8F50 BNE copy_handles_to_ws ; Loop for handles 3,2,1
8F52 RTS ; Return
8F53 .return_last_error←1← 8F38 BPL
INY ; Y=1 (post-INY): param block byte 1
8F54 LDA fs_last_error ; Read last FS error code
8F57 STA (osword_pb_ptr),y ; Return error to caller's param block
8F59 RTS ; Return
8F5A .readc1←2← 8F3D BCC← 8F63 BNE
LDA fs_server_net,y ; A=single-bit bitmask
8F5D JSR mask_to_handle ; Convert bitmask to handle number (FS2A)
8F60 STA (osword_pb_ptr),y ; A=handle number (&20-&27) Y=preserved
8F62 DEY ; Next handle (descending)
8F63 BNE readc1 ; Loop for handles 3,2,1
8F65 RTS ; Return

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
8F66 .osword_10_handler
LDX nfs_workspace_hi ; Workspace page high byte
8F68 STX fs_crc_hi ; Set up pointer high byte in &AC
8F6A STY fs_crc_lo ; Save param block high byte in &AB
8F6C ROR rx_flags ; Disable user RX during CB operation
8F6F LDA (osword_pb_ptr),y ; Read first byte of param block
8F71 STA fs_last_byte_flag ; Save: 0=open new, non-zero=read RXCB
8F73 BNE read_rxcb ; Non-zero: read specified RXCB
8F75 LDA #3 ; Start scan from RXCB #3 (0-2 reserved)
8F77 .scan0←1← 8F89 BNE
JSR calc_handle_offset ; Convert RXCB number to workspace offset
8F7A BCS openl4 ; Invalid RXCB: return zero
8F7C LSR ; LSR twice: byte offset / 4
8F7D LSR ; Yields RXCB number from offset
8F7E TAX ; X = RXCB number for iteration
8F7F LDA (fs_crc_lo),y ; Read flag byte from RXCB workspace
8F81 BEQ openl4 ; Zero = end of CB list
8F83 CMP #&3f ; &3F = deleted slot, free for reuse
8F85 BEQ scan1 ; Found free slot
8F87 INX ; Try next RXCB
8F88 TXA ; A = next RXCB number
8F89 BNE scan0 ; Continue scan (always branches)
8F8B .scan1←1← 8F85 BEQ
TXA ; A = free RXCB number
8F8C LDX #0 ; X=0 for indexed indirect store
8F8E STA (osword_pb_ptr,x) ; Return RXCB number to caller's byte 0
8F90 .read_rxcb←1← 8F73 BNE
JSR calc_handle_offset ; Convert RXCB number to workspace offset
8F93 BCS openl4 ; Invalid: write zero to param block
8F95 DEY ; Y = offset-1: points to flag byte
8F96 STY fs_crc_lo ; Set &AB = workspace ptr low byte
8F98 LDA #&c0 ; &C0: test mask for flag byte
8F9A LDY #1 ; Y=1: flag byte offset in RXCB
8F9C LDX #&0b ; Enable interrupts before transmit
8F9E CPY fs_last_byte_flag ; Compare Y(1) with saved byte (open/read)
8FA0 ADC (fs_crc_lo),y ; ADC flag: test if slot is in use
8FA2 BEQ openl6 ; Dest station = &FFFF (accept reply from any station)
8FA4 BMI openl7 ; Negative: slot has received data
8FA6 .copy_rxcb_to_param←1← 8FB6 BNE
CLC ; C=0: workspace-to-param direction
8FA7 .openl6←1← 8FA2 BEQ
JSR copy_param_byte ; Copy RXCB data to param block
8FAA BCS reenable_rx ; Done: skip deletion on error
8FAC LDA #&3f ; Mark CB as consumed (consume-once)
8FAE LDY #1 ; Y=1: flag byte offset
8FB0 STA (fs_crc_lo),y ; Write &3F to mark slot deleted
8FB2 BNE reenable_rx ; Branch to exit (always taken) ALWAYS branch
8FB4 .openl7←1← 8FA4 BMI
ADC #1 ; Advance through multi-byte field
8FB6 BNE copy_rxcb_to_param ; Loop until all bytes processed
8FB8 DEY ; Y=-1 → Y=0 after STA below
8FB9 .openl4←3← 8F7A BCS← 8F81 BEQ← 8F93 BCS
STA (osword_pb_ptr),y ; Return zero (no free RXCB found)
8FBB .reenable_rx←2← 8FAA BCS← 8FB2 BNE
ROL rx_flags ; Re-enable user RX
8FBE RTS ; Return
8FBF EQUB &A0, &02 ; Y=2: copy 3 bytes (indices 2,1,0)
8FC1 .rest1
EQUB &B1, &9C, &99, &BD, &00, &88, &10, &F8, &60

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
8FCA .setup_rx_buffer_ptrs←1← 8FFD JSR
LDY #&1c ; Y=&1C: RXCB template offset
8FCC LDA osword_pb_ptr ; A = base address low byte
8FCE ADC #1 ; A = base + 1 (skip length byte)
8FD0 JSR store_16bit_at_y ; Receive data blocks until command byte = &00 or &0D
8FD3 LDY #1 ; Read data length from (&F0)+1
8FD5 LDA (osword_pb_ptr),y ; A = data length byte
8FD7 LDY #&20 ; Workspace offset &20 = RX data end
8FD9 ADC osword_pb_ptr ; A = base + length = end address low
8FDB .store_16bit_at_y←1← 8FD0 JSR
STA (nfs_workspace),y ; Store low byte of 16-bit address
8FDD INY ; Advance to high byte offset
8FDE LDA osword_pb_ptr_hi ; A = high byte of base address
8FE0 ADC #0 ; Add carry for 16-bit addition
8FE2 STA (nfs_workspace),y ; Store high byte
8FE4 RTS ; Return

Econet transmit/receive handler

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

On EntryA0=set up and transmit, >=1=handle TX result
8FE5 .econet_tx_rx
CMP #1 ; A=0: set up and transmit; A>=1: handle result
8FE7 BCS handle_tx_result ; A >= 1: handle TX result
8FE9 LDY #&23 ; Y=&23: start of template (descending)
8FEB .dofs01←1← 8FF8 BNE
LDA init_tx_ctrl_block,y ; Load ROM template byte
8FEE BNE store_txcb_byte ; Non-zero = use ROM template byte as-is
8FF0 LDA nmi_sub_table,y ; Zero = substitute from NMI workspace
8FF3 .store_txcb_byte←1← 8FEE BNE
STA (nfs_workspace),y ; Store to dynamic workspace
8FF5 DEY ; Descend through template
8FF6 CPY #&17 ; Stop at offset &17
8FF8 BNE dofs01 ; Loop until all bytes copied
8FFA INY ; Y=&18: TX block starts here
8FFB STY net_tx_ptr ; Point net_tx_ptr at workspace+&18
8FFD JSR setup_rx_buffer_ptrs ; Set up RX buffer start/end pointers
9000 LDY #2 ; Y=2: port byte offset in RXCB
9002 LDA #&90 ; A=&90: FS reply port
9004 STA (osword_pb_ptr),y ; Store port &90 at (&F0)+2
9006 INY ; Y=&03
9007 INY ; Y=&04: advance to station address Y=&04
9008 .copy_fs_addr←1← 9010 BNE
LDA fs_context_base,y ; Copy FS station addr from workspace
900B STA (osword_pb_ptr),y ; Store to RX param block
900D INY ; Next byte
900E CPY #7 ; Done 3 bytes (Y=4,5,6)?
9010 BNE copy_fs_addr ; No: continue copying
9012 LDA nfs_workspace_hi ; High byte of workspace for TX ptr
9014 STA net_tx_ptr_hi ; Store as TX pointer high byte
9016 CLI ; Enable interrupts before transmit
9017 JSR tx_poll_ff ; Transmit with full retry
901A LDY #&20 ; Y=&20: RX end address offset
901C LDA #&ff ; Set RX end address to &FFFF (accept any length)
901E STA (nfs_workspace),y ; Store end address low byte (&FF)
9020 INY ; Y=&21
9021 STA (nfs_workspace),y ; Store end address high byte (&FF)
9023 LDY #&19 ; Y=&19: port byte in workspace RXCB
9025 LDA #&90 ; A=&90: FS reply port
9027 STA (nfs_workspace),y ; Store port to workspace RXCB
9029 DEY ; Y=&18
902A LDA #&7f ; A=&7F: flag byte = waiting for reply
902C STA (nfs_workspace),y ; Store flag byte to workspace RXCB
902E JMP send_to_fs_star ; Jump to RX poll (BRIANX)
9031 .handle_tx_result←1← 8FE7 BCS
PHP ; Save processor flags
9032 LDY #1 ; Y=1: first data byte offset Y=character to write
9034 LDA (osword_pb_ptr),y ; Load first data byte from RX buffer
fall through ↓

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
9036 .net_write_char
TAX ; X = first data byte (command code)
9037 INY ; Advance to next data byte
9038 LDA (osword_pb_ptr),y ; Load station address high byte
903A INY ; Advance past station addr
903B STY fs_crc_lo ; Save Y as data index
903D LDY #&72 ; Store station addr hi at (net_rx_ptr)+&72
903F STA (net_rx_ptr),y ; Store to workspace
9041 DEY ; Y=&71
9042 TXA ; A = command code (from X)
9043 STA (net_rx_ptr),y ; Store station addr lo at (net_rx_ptr)+&71
9045 PLP ; Restore flags from earlier PHP
9046 BNE dofs2 ; First call: adjust data length
9048 .send_data_bytes←1← 9062 BNE
LDY fs_crc_lo ; Reload data index
904A INC fs_crc_lo ; Advance data index for next iteration
904C LDA (osword_pb_ptr),y ; Load next data byte
904E LDY #&7d ; Y=&7D: store byte for TX at offset &7D
9050 STA (net_rx_ptr),y ; Store data byte at (net_rx_ptr)+&7D for TX
9052 PHA ; Save data byte for &0D check after TX
9053 JSR ctrl_block_setup_alt ; Set up TX control block
9056 CLI ; Enable interrupts for TX
9057 JSR tx_poll_core ; Enable IRQs and transmit Core transmit and poll routine (XMIT)
905A .delay_between_tx←1← 905B BNE
DEX ; Short delay loop between TX packets
905B BNE delay_between_tx ; Spin until X reaches 0
905D PLA ; Restore data byte for terminator check
905E BEQ return_8 ; Z=1: not intercepted, pass through
9060 EOR #&0d ; Test for end-of-data marker (&0D)
9062 BNE send_data_bytes ; Not &0D: continue with next byte
9064 .return_8←1← 905E BEQ
RTS ; Return (data complete)
9065 .dofs2←1← 9046 BNE
JSR ctrl_block_setup_alt ; First-packet: set up control block
9068 LDY #&7b ; Y=&7B: data length offset
906A LDA (net_rx_ptr),y ; Load current data length
906C ADC #3 ; Adjust data length by 3 for header bytes
906E 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.

9070 .enable_irq_and_tx
CLI ; Enable interrupts
9071 JMP tx_poll_ff ; 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 &908D. 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
9074 .osword_dispatch
PHP ; Save processor status
9075 PHA ; Save A (reason code)
9076 TXA ; Save X
9077 PHA ; Push X to stack
9078 TYA ; Save Y
9079 PHA ; Push Y to stack
907A TSX ; Get stack pointer for indexed access
907B LDA l0103,x ; Retrieve original A (reason code) from stack
907E CMP #9 ; Reason codes 0-8 only
9080 BCS entry1 ; Code >= 9: skip dispatch, restore regs
9082 TAX ; X = reason code for table lookup
9083 JSR osword_trampoline ; Dispatch to handler via trampoline
9086 .entry1←1← 9080 BCS
PLA ; Restore Y
9087 TAY ; Transfer to Y register
9088 PLA ; Restore X
9089 TAX ; Transfer to X register
908A PLA ; Restore A
908B PLP ; Restore processor status flags
908C RTS ; Return with all registers preserved
908D .osword_trampoline←1← 9083 JSR
LDA osword_tbl_hi,x ; PHA/PHA/RTS trampoline: push handler addr-1, RTS jumps to it
9090 PHA ; Push high byte of handler address
9091 LDA osword_tbl_lo,x ; Load handler low byte from table
9094 PHA ; Push low byte of handler address
9095 LDA osbyte_a_copy ; Load workspace byte &EF for handler
9097 RTS ; RTS dispatches to pushed handler
9098 .osword_tbl_lo←1← 9091 LDA
EQUB <(return_2-1)
9099 EQUB <(remote_print_handler-1)
909A EQUB <(remote_print_handler-1)
909B EQUB <(remote_print_handler-1)
909C EQUB <(nwrch_handler-1)
909D EQUB <(printer_select_handler-1)
909E EQUB <(return_2-1)
909F EQUB <(remote_cmd_dispatch-1)
90A0 EQUB <(remote_osword_handler-1)
90A1 .osword_tbl_hi←1← 908D LDA
EQUB >(return_2-1)
90A2 EQUB >(remote_print_handler-1)
90A3 EQUB >(remote_print_handler-1)
90A4 EQUB >(remote_print_handler-1)
90A5 EQUB >(nwrch_handler-1)
90A6 EQUB >(printer_select_handler-1)
90A7 EQUB >(return_2-1)
90A8 EQUB >(remote_cmd_dispatch-1)
90A9 EQUB >(remote_osword_handler-1)

NETVEC reason 4: write character to network (NWRCH)

Handles remote character output over the network. Clears carry in the stacked processor status (via ROR/ASL on the stack frame) to signal success to the MOS dispatcher. Stores the character from Y into workspace offset &DA, then falls through to setup_tx_and_send with A=0 to transmit the character to the remote terminal.

90AA .nwrch_handler
TSX ; TSX: index stack for register fix
90AB ROR l0106,x ; ROR/ASL on stacked P: zeros carry to signal success
90AE ASL l0106,x ; ASL: restore P after ROR zeroed carry
90B1 TYA ; Y = character to write
90B2 LDY #&da ; Store character at workspace offset &DA
90B4 STA (nfs_workspace),y ; Store char at workspace offset &DA
90B6 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
90B8 .setup_tx_and_send←3← 8193 JSR← 90FF JSR← 9160 JSR
LDY #&d9 ; Y=&D9: command type offset
90BA STA (nfs_workspace),y ; Store command type at ws+&D9
90BC LDA #&80 ; Mark TX control block as active (&80)
90BE LDY #&0c ; Y=&0C: TXCB start offset
90C0 STA (nfs_workspace),y ; Set TX active flag at ws+&0C
90C2 STY net_tx_ptr ; Redirect net_tx_ptr low to workspace
90C4 LDX nfs_workspace_hi ; Load workspace page high byte
90C6 STX net_tx_ptr_hi ; Complete ptr redirect
90C8 JSR tx_poll_ff ; Transmit with full retry
90CB LDA #&3f ; Mark TXCB as deleted (&3F) after transmit
90CD STA (net_tx_ptr,x) ; Write &3F to TXCB byte 0
90CF 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.

90D0 .remote_cmd_dispatch
LDY osword_pb_ptr_hi ; Load original Y (OSBYTE secondary param)
90D2 CMP #&81 ; OSBYTE &81 (INKEY): always forward to terminal
90D4 BEQ dispatch_remote_osbyte ; Forward &81 to terminal for keyboard read
90D6 LDY #1 ; Y=1: search NCTBPL table (execute on both)
90D8 LDX #7 ; X=7: 8-entry NCTBPL table size
90DA JSR match_osbyte_code ; Search for OSBYTE code in NCTBPL table
90DD BEQ dispatch_remote_osbyte ; Match found: dispatch with Y=1 (both)
90DF DEY ; Y=-1: search NCTBMI table (terminal only)
90E0 DEY ; Second DEY: Y=&FF (from 1 via 0)
90E1 LDX #&0e ; X=&0E: 15-entry NCTBMI table size
90E3 JSR match_osbyte_code ; Search for OSBYTE code in NCTBMI table
90E6 BEQ dispatch_remote_osbyte ; Match found: dispatch with Y=&FF (terminal)
90E8 INY ; Y=0: OSBYTE not recognised, ignore
90E9 .dispatch_remote_osbyte←3← 90D4 BEQ← 90DD BEQ← 90E6 BEQ
LDX #2 ; X=2 bytes to copy (default for RBYTE)
90EB TYA ; A=Y: check table match result
90EC BEQ return_nbyte ; Y=0: not recognised, return unhandled
90EE PHP ; Y>0 (NCTBPL): send only, no result expected
90EF BPL nbyte6 ; Y>0 (NCTBPL): no result expected, skip RX
90F1 INX ; Y<0 (NCTBMI): X=3 bytes (result + P flags) X=&03
90F2 .nbyte6←1← 90EF BPL
LDY #&dc ; Y=&DC: top of 3-byte stack frame region
90F4 .nbyte1←1← 90FC BPL
LDA tube_claimed_id,y ; Copy OSBYTE args from stack frame to workspace
90F7 STA (nfs_workspace),y ; Store to NFS workspace for transmission
90F9 DEY ; Next byte (descending)
90FA CPY #&da ; Copied all 3 bytes? (&DC, &DB, &DA)
90FC BPL nbyte1 ; Loop for remaining bytes
90FE TXA ; A = byte count for setup_tx_and_send
90FF JSR setup_tx_and_send ; Build TXCB and transmit to terminal
9102 PLP ; Restore N flag from table match type
9103 BPL return_nbyte ; Y was positive (NCTBPL): done, no result
9105 LDA #&7f ; Set up RX control block to wait for reply
9107 STA (net_tx_ptr,x) ; Write &7F to RXCB (wait for reply)
9109 .poll_rxcb_loop←1← 910B BPL
LDA (net_tx_ptr,x) ; Poll RXCB for completion (bit7)
910B BPL poll_rxcb_loop ; Bit7 clear: still waiting, poll again
910D TSX ; X = stack pointer for register restoration
910E LDY #&dd ; Y=&DD: saved P byte offset in workspace
9110 LDA (nfs_workspace),y ; Load remote processor status from reply
9112 ORA #&44 ; Force V=1 (claimed) and I=1 (no IRQ) in saved P
9114 BNE nbyte5 ; ALWAYS branch (ORA #&44 never zero) ALWAYS branch
9116 .nbyte4←1← 911F BNE
DEY ; Previous workspace offset
9117 DEX ; Previous stack register slot
9118 LDA (nfs_workspace),y ; Load next result byte (X, then Y)
911A .nbyte5←1← 9114 BNE
STA l0106,x ; Write result bytes to stacked registers
911D CPY #&da ; Copied all result bytes? (P at &DA)
911F BNE nbyte4 ; Loop for remaining result bytes
9121 .return_nbyte←2← 90EC BEQ← 9103 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

9122 .match_osbyte_code←3← 90DA JSR← 90E3 JSR← 9128 BPL
CMP remote_osbyte_table,x ; Compare OSBYTE code with table entry
9125 BEQ return_match_osbyte ; Match found: return with Z=1
9127 DEX ; Next table entry (descending)
9128 BPL match_osbyte_code ; Loop for remaining entries
912A .return_match_osbyte←2← 9125 BEQ← 9142 BNE
RTS ; Return; Z=1 if match, Z=0 if not
912B .remote_osbyte_table←1← 9122 CMP
EQUB &04 ; OSBYTE &04: cursor key status
912C EQUB &09 ; OSBYTE &09: flash duration (1st colour)
912D EQUB &0A ; OSBYTE &0A: flash duration (2nd colour)
912E EQUB &14 ; OSBYTE &14: explode soft character RAM
912F EQUB &9A ; OSBYTE &9A: video ULA control register
9130 EQUB &9B ; OSBYTE &9B: video ULA palette
9131 EQUB &9C ; OSBYTE &9C: ACIA control register
9132 EQUB &E2 ; OSBYTE &E2: function key &D0-&DF
9133 EQUB &0B ; OSBYTE &0B: auto-repeat delay
9134 EQUB &0C ; OSBYTE &0C: auto-repeat rate
9135 EQUB &0F ; OSBYTE &0F: flush buffer class
9136 EQUB &79 ; OSBYTE &79: keyboard scan from X
9137 EQUB &7A ; OSBYTE &7A: keyboard scan from &10
9138 EQUB &E3 ; OSBYTE &E3: function key &E0-&EF
9139 EQUB &E4 ; OSBYTE &E4: function key &F0-&FF

NETVEC reason 8: remote OSWORD handler (NWORD)

Handles OSWORD 7 (sound) and OSWORD 8 (define envelope) across the network. Copies up to 14 parameter bytes from the RX buffer to workspace, tags the message as RWORD, and transmits. Fire-and-forget: no return value is sent back. Other OSWORD numbers are ignored.

913A .remote_osword_handler
LDY #&0e ; Y=&0E: 14 bytes for OSWORD 8
913C CMP #7 ; OSWORD 7 (sound)?
913E BEQ copy_params_rword ; OSWORD 7 (sound): handle via common path
9140 CMP #8 ; OSWORD 8 = define an envelope
fall through ↓

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.

9142 .remote_cmd_data
BNE return_match_osbyte ; Not OSWORD 7 or 8: ignore (BNE exits)
9144 .copy_params_rword←1← 913E BEQ
LDX #&db ; Point workspace to offset &DB for params
9146 STX nfs_workspace ; Store workspace ptr offset &DB
9148 .copy_osword_params←1← 914D BPL
LDA (osword_pb_ptr),y ; Load param byte from OSWORD param block
914A STA (nfs_workspace),y ; Write param byte to workspace
914C DEY ; Next byte (descending)
914D BPL copy_osword_params ; Loop for all parameter bytes
914F INY ; Y=0 after loop
9150 DEC nfs_workspace ; Point workspace to offset &DA
9152 LDA osbyte_a_copy ; Load original OSWORD code
9154 STA (nfs_workspace),y ; Store OSWORD code at ws+0
9156 STY nfs_workspace ; Reset workspace ptr to base
9158 LDY #&14 ; Y=&14: command type offset
915A LDA #&e9 ; Tag as RWORD (port &E9)
915C STA (nfs_workspace),y ; Store port tag at ws+&14
915E LDA #1 ; A=1: single-byte TX
9160 JSR setup_tx_and_send ; Load template byte from ctrl_block_template[X]
9163 STX nfs_workspace ; Restore workspace ptr
9165 JMP ctrl_block_setup_alt ; Set up alt control block

Alternate entry into control block setup

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

9168 .ctrl_block_setup_alt←3← 9053 JSR← 9065 JSR← 9165 JMP
LDX #&0d ; X=&0D: template offset for alt entry
916A LDY #&7c ; Y=&7C: target workspace offset for alt entry
916C BIT tx_ctrl_upper ; BIT test: V flag = bit 6 of &8374
916F 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 &919D 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

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

Control block initialisation template

Read by the loop at &9176, 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 &8374

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)

919D .ctrl_block_template←1← 9176 LDA
EQUB &85 ; Alt-path only → Y=&6F
919E EQUB &00 ; Alt-path only → Y=&70
919F EQUB &FD ; SKIP
91A0 EQUB &FD ; SKIP
91A1 EQUB &7D ; → Y=&01 / Y=&73
91A2 EQUB &FC ; PAGE byte → Y=&02 / Y=&74
91A3 EQUB &FF ; → Y=&03 / Y=&75
91A4 EQUB &FF ; → Y=&04 / Y=&76
91A5 EQUB &7E ; → Y=&05 / Y=&77
91A6 EQUB &FC ; PAGE byte → Y=&06 / Y=&78
91A7 EQUB &FF ; → Y=&07 / Y=&79
91A8 EQUB &FF ; → Y=&08 / Y=&7A
91A9 EQUB &00 ; → Y=&09 / Y=&7B
91AA EQUB &00 ; → Y=&0A / Y=&7C
91AB EQUB &FE ; STOP — main-path boundary
91AC EQUB &80 ; → Y=&0C (main only)
91AD EQUB &93 ; → Y=&0D (main only)
91AE EQUB &FD ; SKIP (main only)
91AF EQUB &FD ; SKIP (main only)
91B0 EQUB &D9 ; → Y=&10 (main only)
91B1 EQUB &FC ; PAGE byte → Y=&11 (main only)
91B2 EQUB &FF ; → Y=&12 (main only)
91B3 EQUB &FF ; → Y=&13 (main only)
91B4 EQUB &DE ; → Y=&14 (main only)
91B5 EQUB &FC ; PAGE byte → Y=&15 (main only)
91B6 EQUB &FF ; → Y=&16 (main only)
91B7 EQUB &FF ; → Y=&17 (main only)
91B8 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 X against the network printer buffer number (&F0). If it matches, initialises the printer buffer pointer (&0D61 = &1F) and sets the initial flag byte (&0D60 = &41). Otherwise falls through to return.

On EntryX1-based buffer number
91C4 .printer_select_handler
DEX ; X-1: convert 1-based buffer to 0-based
91C5 CPX osword_pb_ptr ; Is this the network printer buffer?
91C7 BNE setup1 ; No: skip printer init
91C9 LDA #&1f ; &1F = initial buffer pointer offset
91CB STA printer_buf_ptr ; Reset printer buffer write position
91CE LDA #&41 ; &41 = initial PFLAGS (bit 6 set, bit 0 set)
91D0 .setup1←1← 91C7 BNE
STA l0d60 ; Store initial PFLAGS value
91D3 .return_printer_select←2← 91D6 BNE← 91EA 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)
91D4 .remote_print_handler
CPY #4 ; Only handle buffer 4 (network printer)
91D6 BNE return_printer_select ; Not buffer 4: ignore
91D8 TXA ; A = reason code
91D9 DEX ; Reason 1? (DEX: 1->0)
91DA BNE toggle_print_flag ; Not reason 1: handle Ctrl-B/C
91DC TSX ; Get stack pointer for P register
91DD ORA l0106,x ; Force I flag in stacked P to block IRQs
91E0 STA l0106,x ; Write back modified P register
91E3 .prlp1←2← 91F2 BCC← 91F7 BCC
LDA #osbyte_read_buffer ; OSBYTE &91: extract char from MOS buffer
91E5 LDX #buffer_printer ; X=3: printer buffer number
91E7 JSR osbyte ; Get character from input buffer (C is set if the buffer is empty, otherwise Y=extracted character)
91EA BCS return_printer_select ; Buffer empty: return
91EC TYA ; Y = extracted character Y is the character extracted from the buffer
91ED JSR store_output_byte ; Store char in output buffer
91F0 CPY #&6e ; Buffer nearly full? (&6E = threshold)
91F2 BCC prlp1 ; Not full: get next char
91F4 JSR flush_output_block ; Buffer full: flush to network
91F7 BCC prlp1 ; Continue after flush
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
91F9 .store_output_byte←2← 91ED JSR← 9206 JSR
LDY printer_buf_ptr ; Load current buffer offset
91FC STA (net_rx_ptr),y ; Store byte at current position
91FE INC printer_buf_ptr ; Advance buffer pointer
9201 RTS ; Return; Y = buffer offset
9202 .toggle_print_flag←1← 91DA BNE
PHA ; Save reason code
9203 TXA ; A = reason code
9204 EOR #1 ; EOR #1: toggle print-active flag (bit 0)
9206 JSR store_output_byte ; Store toggled flag as output byte
9209 EOR l0d60 ; XOR with current PFLAGS
920C ROR ; Test if sequence changed (bit 7 mismatch)
920D BCC pril1 ; Sequence unchanged: skip flush
920F ROL ; Undo ROR
9210 STA l0d60 ; Store toggled PFLAGS
9213 JSR flush_output_block ; Flush current output block
9216 .pril1←1← 920D BCC
LDA l0d60 ; Reload current PFLAGS
9219 AND #&f0 ; Extract upper nibble of PFLAGS
921B ROR ; Shift for bit extraction
921C TAX ; Save in X
921D PLA ; Restore original reason code
921E ROR ; Merge print-active bit from original A
921F TXA ; Retrieve shifted PFLAGS
9220 ROL ; Recombine into new PFLAGS value
9221 STA l0d60 ; Store recombined PFLAGS value
9224 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.

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

929F .lang_2_save_palette_vdu
LDA fs_load_addr_2 ; Save current table index
92A1 PHA ; Push for later restore
92A2 LDA #&e9 ; Point workspace to palette save area (&E9)
92A4 STA nfs_workspace ; Set workspace low byte
92A6 LDY #0 ; Y=0: first palette entry
92A8 STY fs_load_addr_2 ; Clear table index counter
92AA LDA l0350 ; Save current screen MODE to workspace
92AD STA (nfs_workspace),y ; Store MODE at workspace[0]
92AF INC nfs_workspace ; Advance workspace pointer past MODE byte
92B1 LDA l0351 ; Read colour count (from &0351)
92B4 PHA ; Push for iteration count tracking
92B5 TYA ; A=0: logical colour number for OSWORD A=&00
92B6 .save_palette_entry←1← 92D5 BNE
STA (nfs_workspace),y ; Store logical colour at workspace[0]
92B8 LDX nfs_workspace ; X = workspace ptr low (param block addr)
92BA LDY nfs_workspace_hi ; Y = workspace ptr high
92BC LDA #osword_read_palette ; OSWORD &0B: read palette for logical colour
92BE JSR osword ; Read palette
92C1 PLA ; Recover colour count
92C2 LDY #0 ; Y=0: access workspace[0]
92C4 STA (nfs_workspace),y ; Write colour count back to workspace[0]
92C6 INY ; Y=1: access workspace[1] (palette result) Y=&01
92C7 LDA (nfs_workspace),y ; Read palette value returned by OSWORD
92C9 PHA ; Push palette value for next iteration
92CA LDX nfs_workspace ; X = current workspace ptr low
92CC INC nfs_workspace ; Advance workspace pointer
92CE INC fs_load_addr_2 ; Increment table index
92D0 DEY ; Y=0 for next store Y=&00
92D1 LDA fs_load_addr_2 ; Load table index as logical colour
92D3 CPX #&f9 ; Loop until workspace wraps past &F9
92D5 BNE save_palette_entry ; Continue for all 16 palette entries
92D7 PLA ; Discard last palette value from stack
92D8 STY fs_load_addr_2 ; Reset table index to 0
92DA INC nfs_workspace ; Advance workspace past palette data
92DC JSR save_vdu_state ; Save cursor pos and OSBYTE state values
92DF INC nfs_workspace ; Advance workspace past VDU state data
92E1 PLA ; Recover saved table index
92E2 STA fs_load_addr_2 ; Restore table index
92E4 .clear_jsr_protection←4← 8470 JMP← 8498 JSR← 84BF JSR← 8EE4 JMP
LDA saved_jsr_mask ; Restore LSTAT from saved OLDJSR value
92E7 STA prot_status ; Write to protection status
92EA 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
92EB .save_vdu_state←1← 92DC JSR
LDA l0355 ; Read cursor editing state
92EE STA (nfs_workspace),y ; Store to workspace[Y]
92F0 TAX ; Preserve in X for OSBYTE
92F1 JSR read_vdu_osbyte ; OSBYTE &85: read cursor position
92F4 INC nfs_workspace ; Advance workspace pointer
92F6 TYA ; Y result from OSBYTE &85
92F7 STA (nfs_workspace,x) ; Store Y pos to workspace (X=0)
92F9 JSR read_vdu_osbyte_x0 ; Self-call trick: executes twice
92FC .read_vdu_osbyte_x0←1← 92F9 JSR
LDX #0 ; X=0 for (zp,X) addressing
92FE .read_vdu_osbyte←1← 92F1 JSR
LDY fs_load_addr_2 ; Index into OSBYTE number table
9300 INC fs_load_addr_2 ; Next table entry next time
9302 INC nfs_workspace ; Advance workspace pointer
9304 LDA osbyte_vdu_table,y ; Read OSBYTE number from table
9307 LDY #&ff ; Y=&FF: read current value
9309 JSR osbyte ; Call OSBYTE
930C TXA ; Result in X to A
930D LDX #0 ; X=0 for indexed indirect store
930F STA (nfs_workspace,x) ; Store result to workspace
9311 RTS ; Return after storing result
; 3-entry OSBYTE table for lang_2_save_palette_vdu (&929F)
9312 .osbyte_vdu_table←1← 9304 LDA
EQUB &85 ; OSBYTE &85: read cursor position
9313 EQUB &C2 ; OSBYTE &C2: read shadow RAM allocation
9314 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).
9315 0016 .nmi_workspace_start←1← 813D STA
.tube_brk_handler←1← 813D STA
LDA #&ff ; A=&FF: signal error to co-processor via R4
9317 0018 JSR tube_send_r4 ; Send &FF error signal to Tube R4
931A 001B LDA tube_data_register_2 ; Flush any pending R2 byte
931D 001E LDA #0 ; A=0: send zero prefix to R2
931F 0020 .tube_send_zero_r2
JSR tube_send_r2 ; Send zero prefix byte via R2
9322 0023 TAY ; Y=0: start of error block at (&FD)
9323 0024 LDA (brk_ptr),y ; Load error number from (&FD),0
9325 0026 JSR tube_send_r2 ; Send error number via R2
9328 0029 .tube_brk_send_loop←1← 0030 BNE
INY ; Advance to next error string byte
9329 002A .tube_send_error_byte
LDA (brk_ptr),y ; Load next error string byte
932B 002C JSR tube_send_r2 ; Send error string byte via R2
932E 002F TAX ; Zero byte = end of error string
932F 0030 BNE tube_brk_send_loop ; Loop until zero terminator sent
9331 0032 .tube_reset_stack
LDX #&ff ; Reset stack pointer to top
9333 0034 TXS ; TXS: set stack pointer from X
9334 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.
9335 0036 .tube_enter_main_loop←2← 04EC JMP← 053A JMP
STX zp_temp_11 ; Save X to temporary
9337 0038 STY zp_temp_10 ; Save Y to temporary
9339 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
933C 003D BPL tube_poll_r2 ; R1 not ready: check R2 instead
933E 003F .tube_handle_wrch←1← 004D BMI
LDA tube_data_register_1 ; Read character from Tube R1 data
9341 0042 JSR nvwrch ; Write character
9344 0045 .tube_poll_r2←1← 003D BPL
BIT tube_status_register_2 ; BIT R2 status: check command byte
9347 0048 BPL tube_main_loop ; R2 not ready: loop back to R1 check
9349 004A BIT tube_status_1_and_tube_control ; Re-check R1: WRCH has priority over R2
934C 004D BMI tube_handle_wrch ; R1 ready: handle WRCH first
934E 004F LDX tube_data_register_2 ; Read command byte from Tube R2 data
9351 0052 STX tube_dispatch_ptr_lo ; Self-modify JMP low byte for dispatch
9353 0054 .tube_dispatch_cmd
JMP (tube_dispatch_table) ; Dispatch to handler via indirect JMP
9356 0057 .tube_transfer_addr←2← 0478 STY← 0493 STA
EQUB &00
9357 0058 .tube_xfer_page←2← 047C STA← 0498 STA
EQUB &80
9358 0059 .tube_xfer_addr_2←1← 04A2 STY
EQUB &00
9359 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 at ; &0400-&041B). Contains:
; &0400: JMP &0473 (BEGIN — CLI parser / startup entry)
; &0403: JMP &06E2 (tube_escape_check)
; &0406: tube_addr_claim — Tube address claim protocol (ADRR)
; &0414: tube_post_init — called after ROM→RAM copy
; &0473: BEGIN — startup/CLI entry, break type check
; &04E7: tube_rdch_handler — RDCHV target
; &04EF: tube_restore_regs — restore X,Y, dispatch entry 6
; &04F7: tube_read_r2 — poll R2 status, read data byte to A
935A 0400 .tube_code_page4←1← 8123 STA
JMP tube_begin ; JMP to BEGIN startup entry
935D 0403 .tube_escape_entry
JMP tube_escape_check ; JMP to tube_escape_check (&06A7)
9360 0406 .tube_addr_claim←10← 04BC JSR← 04E4 JMP← 8B41 JSR← 8B53 JSR← 8BB0 JSR← 8E0F JMP← 9A01 JSR← 9A53 JSR← 9FA7 JSR← 9FAF JSR
CMP #&80 ; A>=&80: address claim; A<&80: data transfer
9362 0408 BCC setup_data_transfer ; A<&80: data transfer setup (SENDW)
9364 040A CMP #&c0 ; A>=&C0: new address claim from another host
9366 040C BCS addr_claim_external ; C=1: external claim, check ownership
9368 040E ORA #&40 ; Map &80-&BF range to &C0-&FF for comparison
936A 0410 CMP tube_claimed_id ; Is this for our currently-claimed address?
936C 0412 BNE return_tube_init ; Not our address: return
936E 0414 .tube_post_init←1← 8135 JSR
LDA #&80 ; &80 sentinel: clear address claim
9370 0416 STA tube_claim_flag ; Store to claim-in-progress flag
9372 0418 RTS ; Return from tube_post_init
9373 0419 .addr_claim_external←1← 040C BCS
ASL tube_claim_flag ; Another host claiming; check if we're owner
9375 041B BCS accept_new_claim ; C=1: we have an active claim
9377 041D CMP tube_claimed_id ; Compare with our claimed address
9379 041F BEQ return_tube_init ; Match: return (we already have it)
937B 0421 CLC ; Not ours: CLC = we don't own this address
937C 0422 RTS ; Return with C=0 (claim denied)
937D 0423 .accept_new_claim←1← 041B BCS
STA tube_claimed_id ; Accept new claim: update our address
937F 0425 .return_tube_init←2← 0412 BNE← 041F BEQ
RTS ; Return with address updated
9380 0426 .setup_data_transfer←1← 0408 BCC
STY tube_data_ptr_hi ; Save 16-bit transfer address from (X,Y)
9382 0428 STX tube_data_ptr ; Store address pointer low byte
9384 042A JSR tube_send_r4 ; Send transfer type byte to co-processor
9387 042D TAX ; X = transfer type for table lookup
9388 042E LDY #3 ; Y=3: send 4 bytes (address + claimed addr)
938A 0430 .send_xfer_addr_bytes←1← 0436 BPL
LDA (tube_data_ptr),y ; Load transfer address byte from (X,Y)
938C 0432 JSR tube_send_r4 ; Send address byte to co-processor via R4
938F 0435 DEY ; Previous byte (big-endian: 3,2,1,0)
9390 0436 BPL send_xfer_addr_bytes ; Loop for all 4 address bytes
9392 0438 JSR tube_send_r4 ; Send claimed address via R4
9395 043B LDY #&18 ; Y=&18: enable Tube control register
9397 043D STY tube_status_1_and_tube_control ; Enable Tube interrupt generation
939A 0440 LDA tube_xfer_ctrl_bits,x ; Look up Tube control bits for this xfer type
939D 0443 STA tube_status_1_and_tube_control ; Apply transfer- specific control bits
93A0 0446 LSR ; LSR: check bit 2 (2-byte flush needed?)
93A1 0447 LSR ; LSR: shift bit 2 to carry
93A2 0448 .poll_r4_copro_ack←1← 044B BVC
BIT tube_status_register_4_and_cpu_control ; Poll R4 status for co- processo r response
93A5 044B BVC poll_r4_copro_ack ; Bit 6 clear: not ready, keep polling
93A7 044D BCS flush_r3_nmi_check ; R4 bit 7: co-processor acknowledged transfer
93A9 044F CPX #4 ; Type 4 = SENDW (host-to-parasite word xfer)
93AB 0451 BNE return_tube_xfer ; Not SENDW type: skip release path
93AD 0453 PLA ; Discard return address (low byte)
93AE 0454 PLA ; Discard return address (high byte)
93AF 0455 .release_claim_restart←1← 04B8 BEQ
LDA #&80 ; A=&80: reset claim flag sentinel
93B1 0457 STA tube_claim_flag ; Clear claim-in-progress flag
93B3 0459 JMP tube_reply_byte ; Restart Tube main loop
93B6 045C .flush_r3_nmi_check←1← 044D BCS
BIT tube_data_register_3 ; Flush R3 data (first byte)
93B9 045F BIT tube_data_register_3 ; Flush R3 data (second byte)
93BC 0462 .copro_ack_nmi_check
LSR ; LSR: check bit 0 (NMI used?)
93BD 0463 BCC return_tube_xfer ; C=0: NMI not used, skip NMI release
93BF 0465 LDY #&88 ; Release Tube NMI (transfer used interrupts)
93C1 0467 STY tube_status_1_and_tube_control ; Write &88 to Tube control to release NMI
93C4 046A .return_tube_xfer←2← 0451 BNE← 0463 BCC
RTS ; Return from transfer setup
93C5 046B .tube_xfer_ctrl_bits←1← 0440 LDA
EQUB &86, &88, &96, &98, &18, &18, &82, &18
93CD 0473 .tube_begin←1← 0400 JMP
CLI ; BEGIN: enable interrupts for Tube host code
93CE 0474 PHP ; Save processor status
93CF 0475 PHA ; Save A on stack
93D0 0476 LDY #0 ; Y=0: start at beginning of page
93D2 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.
93D4 047A .tube_init_reloc
LDA #&80 ; Init: start sending from &8000
93D6 047C STA tube_xfer_page ; Store &80 as source page high byte
93D8 047E STA zp_ptr_hi ; Store &80 as page counter initial value
93DA 0480 LDA #&20 ; A=&20: bit 5 mask for ROM type check
93DC 0482 AND rom_type ; ROM type bit 5: reloc address in header?
93DF 0485 BEQ store_xfer_end_addr ; No reloc addr: use defaults
93E1 0487 LDX copyright_offset ; Skip past copyright string to find reloc addr
93E4 048A .scan_copyright_end←1← 048E BNE
INX ; Skip past null-terminated copyright string
93E5 048B LDA rom_header,x ; Load next byte from ROM header
93E8 048E BNE scan_copyright_end ; Loop until null terminator found
93EA 0490 LDA lang_entry_lo,x ; Read 4-byte reloc address from ROM header
93ED 0493 STA tube_transfer_addr ; Store reloc addr byte 1 as transfer addr
93EF 0495 LDA lang_entry_hi,x ; Load reloc addr byte 2
93F2 0498 STA tube_xfer_page ; Store as source page start
93F4 049A LDY service_entry,x ; Load reloc addr byte 3
93F7 049D LDA svc_entry_lo,x ; Load reloc addr byte 4 (highest)
93FA 04A0 .store_xfer_end_addr←1← 0485 BEQ
STA tube_xfer_addr_3 ; Store high byte of end address
93FC 04A2 STY tube_xfer_addr_2 ; Store byte 3 of end address
93FE 04A4 PLA ; Restore A from stack
93FF 04A5 PLP ; Restore processor status
9400 04A6 BCS beginr ; Carry set: language entry (claim Tube)
9402 04A8 TAX ; X = A (preserved from entry)
9403 04A9 BNE begink ; Non-zero: check break type
9405 04AB JMP tube_reply_ack ; A=0: acknowledge and return
9408 04AE .begink←1← 04A9 BNE
LDX #0 ; X=0 for OSBYTE read
940A 04B0 LDY #&ff ; Y=&FF for OSBYTE read
940C 04B2 LDA #osbyte_read_write_last_break_type ; OSBYTE &FD: read last break type
940E 04B4 JSR osbyte ; Read type of last reset
9411 04B7 TXA ; X=value of type of last reset
9412 04B8 BEQ release_claim_restart ; Soft break (0): skip ROM transfer
9414 04BA .beginr←2← 04A6 BCS← 04BF BCC
LDA #&ff ; A=&FF: claim Tube for all operations
9416 04BC JSR tube_addr_claim ; Claim Tube address via R4
9419 04BF BCC beginr ; Not claimed: retry until claimed
941B 04C1 LDA #1 ; Transfer type 1 (parasite to host)
941D 04C3 JSR tube_setup_transfer ; Set up Tube transfer parameters
9420 04C6 LDY #0 ; Y=0: start at page boundary
9422 04C8 STY zp_ptr_lo ; Source ptr low = 0
9424 04CA LDX #&40 ; X=&40: 64 pages (16KB) to transfer
9426 04CC .send_rom_byte←2← 04D7 BNE← 04DC BNE
LDA (zp_ptr_lo),y ; Read byte from source address
9428 04CE STA tube_data_register_3 ; Send byte to Tube via R3
942B 04D1 .poll_r3_ready←1← 04D4 BVC
BIT tube_status_register_3 ; Check R3 status
942E 04D4 BVC poll_r3_ready ; Not ready: wait for Tube
9430 04D6 INY ; Next byte in page
9431 04D7 BNE send_rom_byte ; More bytes in page: continue
9433 04D9 INC zp_ptr_hi ; Next source page
9435 04DB DEX ; Decrement page counter
9436 04DC BNE send_rom_byte ; More pages: continue transfer
9438 04DE LDA #4 ; Transfer type 4 (host to parasite burst)
943A 04E0 .tube_setup_transfer←1← 04C3 JSR
LDY #0 ; Y=0: low byte of param block ptr
943C 04E2 LDX #&57 ; X=&57: param block at &0057
943E 04E4 JMP tube_addr_claim ; Claim Tube and start transfer
9441 04E7 .tube_rdch_handler
LDA #1 ; R2 command: OSRDCH request
9443 04E9 JSR tube_send_r2 ; Send OSRDCH request to host
9446 04EC JMP tube_enter_main_loop ; Jump to RDCH completion handler
9449 04EF .tube_restore_regs
LDY zp_temp_10 ; Restore Y from saved value
944B 04F1 LDX zp_temp_11 ; Restore X from saved value
944D 04F3 JSR tube_read_r2 ; Read result byte from R2
9450 04F6 ASL ; Shift carry into C flag
9451 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
9454 04FA BPL tube_read_r2 ; Bit 7 clear: R2 not ready, wait
9456 04FC LDA tube_data_register_2 ; Read byte from R2 data register
9459 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
945A 0500 .tube_dispatch_table←2← 0054 JMP← 8129 STA
EQUW tube_osrdch ; cmd 0: OSRDCH
945C 0502 EQUW tube_oscli ; cmd 1: OSCLI
945E 0504 EQUW tube_osbyte_short ; cmd 2: OSBYTE (2-param)
9460 0506 EQUW tube_osbyte_long ; cmd 3: OSBYTE (3-param)
9462 0508 EQUW tube_osword ; cmd 4: OSWORD
9464 050A EQUW tube_osword_rdln ; cmd 5: OSWORD 0 (read line)
9466 050C EQUW tube_restore_regs ; cmd 6: release/restore regs
9468 050E EQUW tube_release_return ; cmd 7: restore regs, RTS
946A 0510 EQUW tube_osargs ; cmd 8: OSARGS
946C 0512 EQUW tube_osbget ; cmd 9: OSBGET
946E 0514 EQUW tube_osbput ; cmd 10: OSBPUT
9470 0516 EQUW tube_osfind ; cmd 11: OSFIND
9472 0518 EQUW tube_osfile ; cmd 12: OSFILE
9474 051A EQUW tube_osgbpb ; cmd 13: OSGBPB
9476 051C .tube_wrch_handler
PHA ; Save character for WRCH output
9477 051D LDA #0 ; A=0: send null prefix via R2
9479 051F .tube_send_and_poll
JSR tube_send_r2 ; Send prefix byte to co-processor
947C 0522 .poll_r2_reply←2← 052A BPL← 0532 JMP
BIT tube_status_register_2 ; Poll R2 for co-processor reply
947F 0525 BVS wrch_echo_reply ; R2 ready: go process reply
9481 0527 .tube_poll_r1_wrch
BIT tube_status_1_and_tube_control ; Check R1 for pending WRCH request
9484 052A BPL poll_r2_reply ; No R1 data: back to polling R2
9486 052C LDA tube_data_register_1 ; Read WRCH character from R1
9489 052F JSR nvwrch ; Write character
948C 0532 .tube_resume_poll
JMP poll_r2_reply ; Resume R2 polling after servicing
948F 0535 .wrch_echo_reply←1← 0525 BVS
PLA ; Recover original character
9490 0536 STA tube_data_register_2 ; Echo character back via R2
9493 0539 PHA ; Push for dispatch loop re-entry
9494 053A JMP tube_enter_main_loop ; Enter main dispatch loop
9497 053D .tube_release_return
LDX zp_temp_11 ; Restore saved X
9499 053F LDY zp_temp_10 ; Restore saved Y
949B 0541 PLA ; Restore saved A
949C 0542 RTS ; Return to caller
949D 0543 .tube_osbput
JSR tube_read_r2 ; Read channel handle from R2
94A0 0546 TAY ; Y=channel handle from R2
94A1 0547 JSR tube_read_r2 ; Read data byte from R2 for BPUT
94A4 054A JSR osbput ; Write a single byte A to an open file Y
94A7 054D JMP tube_reply_ack ; BPUT done: send acknowledge, return
94AA 0550 .tube_osbget
JSR tube_read_r2 ; Read channel handle from R2
94AD 0553 TAY ; Y=channel handle for OSBGET Y=file handle
94AE 0554 JSR osbget ; Read a single byte from an open file Y
94B1 0557 PHA ; Save byte read from file
94B2 0558 JMP send_reply_ok ; Send carry+byte reply (BGET result)
94B5 055B .tube_osrdch
JSR nvrdch ; Read a character from the current input stream
94B8 055E PHA ; A=character read
94B9 055F .send_reply_ok←1← 0558 JMP
ORA #&80 ; Set bit 7 (no-error flag)
94BB 0561 .tube_rdch_reply
ROR ; ROR A: encode carry (error flag) into bit 7
94BC 0562 JSR tube_send_r2 ; = JSR tube_send_r2 (overlaps &053D entry)
94BF 0565 PLA ; Restore read character/byte
94C0 0566 JMP tube_reply_byte ; Return to Tube main loop
94C3 0569 .tube_osfind
JSR tube_read_r2 ; Read OSFIND open mode from R2
94C6 056C BEQ tube_osfind_close ; A=0: close file, else open with filename
94C8 056E PHA ; Save open mode while reading filename
94C9 056F JSR tube_read_string ; Read filename string from R2 into &0700
94CC 0572 PLA ; Recover open mode from stack
94CD 0573 JSR osfind ; Open or close file(s)
94D0 0576 PHA ; Save file handle result
94D1 0577 LDA #&ff ; A=&FF: success marker
94D3 0579 JSR tube_send_r2 ; Send success marker via R2
94D6 057C PLA ; Restore file handle
94D7 057D JMP tube_reply_byte ; Send file handle result to co-processor
94DA 0580 .tube_osfind_close←1← 056C BEQ
JSR tube_read_r2 ; OSFIND close: read handle from R2
94DD 0583 TAY ; Y=handle to close
94DE 0584 LDA #osfind_close ; A=0: close command for OSFIND
94E0 0586 JSR osfind ; Close one or all files
94E3 0589 JMP tube_reply_ack ; Close done: send acknowledge, return
94E6 058C .tube_osargs
JSR tube_read_r2 ; Read file handle from R2
94E9 058F TAY ; Y=file handle for OSARGS
94EA 0590 .tube_read_params
LDX #3 ; Read 4-byte arg + reason from R2 into ZP
94EC 0592 .read_osargs_params←1← 0598 BPL
JSR tube_read_r2 ; Read next param byte from R2
94EF 0595 STA zp_ptr_lo,x ; Params stored at &00-&03 (little-endian)
94F1 0597 DEX ; Decrement byte counter
94F2 0598 BPL read_osargs_params ; Loop until all 4 bytes read
94F4 059A INX ; X=0: reset index after loop
94F5 059B JSR tube_read_r2 ; Read OSARGS reason code from R2
94F8 059E JSR osargs ; Read or write a file's attributes
94FB 05A1 JSR tube_send_r2 ; Send result A back to co-processor
94FE 05A4 LDX #3 ; Return 4-byte result from ZP &00-&03
9500 05A6 .send_osargs_result←1← 05AC BPL
LDA zp_ptr_lo,x ; Load result byte from zero page
9502 05A8 JSR tube_send_r2 ; Send byte to co-processor via R2
9505 05AB DEX ; Previous byte (count down)
9506 05AC BPL send_osargs_result ; Loop for all 4 bytes
9508 05AE JMP tube_main_loop ; Return to Tube main loop
950B 05B1 .tube_read_string←3← 056F JSR← 05C5 JSR← 05E2 JSR
LDX #0 ; X=0: initialise string buffer index
950D 05B3 LDY #0 ; Y=0: string buffer offset 0
950F 05B5 .strnh←1← 05C0 BNE
JSR tube_read_r2 ; Read next string byte from R2
9512 05B8 STA l0700,y ; Store byte in string buffer at &0700+Y
9515 05BB INY ; Next buffer position
9516 05BC BEQ string_buf_done ; Y overflow: string too long, truncate
9518 05BE CMP #&0d ; Check for CR terminator
951A 05C0 BNE strnh ; Not CR: continue reading string
951C 05C2 .string_buf_done←1← 05BC BEQ
LDY #7 ; Y=7: set XY=&0700 for OSCLI/OSFIND
951E 05C4 RTS ; Return with XY pointing to &0700
951F 05C5 .tube_oscli
JSR tube_read_string ; Read * command string from R2
9522 05C8 JSR oscli ; Execute * command via OSCLI
9525 05CB .tube_reply_ack←3← 04AB JMP← 054D JMP← 0589 JMP
LDA #&7f ; &7F = success acknowledgement
9527 05CD .tube_reply_byte←5← 0459 JMP← 0566 JMP← 057D JMP← 05D0 BVC← 06B8 JMP
BIT tube_status_register_2 ; Poll R2 status until ready
952A 05D0 BVC tube_reply_byte ; Bit 6 clear: not ready, loop
952C 05D2 STA tube_data_register_2 ; Write byte to R2 data register
952F 05D5 .mj←1← 0600 BEQ
JMP tube_main_loop ; Return to Tube main loop
9532 05D8 .tube_osfile
LDX #&10 ; X=&10: read 16-byte ctrl block
9534 05DA .argsw←1← 05E0 BNE
JSR tube_read_r2 ; Read next control block byte from R2
9537 05DD STA zp_ptr_hi,x ; Store at &01+X (descending)
9539 05DF DEX ; Decrement byte counter
953A 05E0 BNE argsw ; Loop for all 16 bytes
953C 05E2 JSR tube_read_string ; Read filename string from R2 into &0700
953F 05E5 STX zp_ptr_lo ; XY=&0700: filename pointer for OSFILE
9541 05E7 STY zp_ptr_hi ; Store Y=7 as pointer high byte
9543 05E9 LDY #0 ; Y=0 for OSFILE control block offset
9545 05EB JSR tube_read_r2 ; Read OSFILE reason code from R2
9548 05EE JSR osfile ; Execute OSFILE operation
954B 05F1 ORA #&80 ; Set bit 7: mark result as present
954D 05F3 JSR tube_send_r2 ; Send result A (object type) to co-processor
9550 05F6 LDX #&10 ; Return 16-byte control block to co-processor
9552 05F8 .send_osfile_ctrl_blk←1← 05FE BNE
LDA zp_ptr_hi,x ; Load control block byte
9554 05FA JSR tube_send_r2 ; Send byte to co-processor via R2
9557 05FD DEX ; Decrement byte counter
9558 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
955A 0600 .tube_code_page6←1← 812F STA
BEQ mj ; OSGBPB done: return to main loop
955C 0602 .tube_osgbpb
LDX #&0c ; X=12: read 13 OSGBPB param bytes
955E 0604 .read_gbpb_params←1← 060A BPL
JSR tube_read_r2 ; Read param byte from Tube R2
9561 0607 STA zp_ptr_lo,x ; Store in zero page param block
9563 0609 DEX ; Next byte (descending)
9564 060A BPL read_gbpb_params ; Loop until all 13 bytes read
9566 060C JSR tube_read_r2 ; Read A (OSGBPB function code)
9569 060F INX ; X=0 after loop
956A 0610 LDY #0 ; Y=0 for OSGBPB call
956C 0612 JSR osgbpb ; Read or write multiple bytes to an open file
956F 0615 ROR ; 3.35K fix: send carry result to co-processor. 3.35D had PHA here (never sent, never popped).
9570 0616 JSR tube_send_r2 ; Send OSGBPB carry result via R2
9573 0619 LDX #&0c ; X=12: send 13 updated param bytes
9575 061B .send_gbpb_params←1← 0621 BPL
LDA zp_ptr_lo,x ; Load updated param byte
9577 061D JSR tube_send_r2 ; Send param byte via R2
957A 0620 DEX ; Next byte (descending)
957B 0621 BPL send_gbpb_params ; Loop until all 13 bytes sent
957D 0623 JMP tube_main_loop ; Return to main event loop
9580 0626 .tube_osbyte_short
JSR tube_read_r2 ; Read X parameter from co-processor
9583 0629 TAX ; Save in X
9584 062A JSR tube_read_r2 ; Read A (OSBYTE function code)
9587 062D JSR osbyte ; Execute OSBYTE A,X
958A 0630 .tube_osbyte_send_x←2← 0633 BVC← 065B BVS
BIT tube_status_register_2 ; Poll R2 status (bit 6 = ready)
958D 0633 BVC tube_osbyte_send_x ; Not ready: keep polling
958F 0635 STX tube_data_register_2 ; Send X result for 2-param OSBYTE
9592 0638 .bytex←1← 064B BEQ
JMP tube_main_loop ; Return to main event loop
9595 063B .tube_osbyte_long
JSR tube_read_r2 ; Read X parameter from co-processor
9598 063E TAX ; Save in X
9599 063F JSR tube_read_r2 ; Read Y parameter from co-processor
959C 0642 TAY ; Save in Y
959D 0643 JSR tube_read_r2 ; Read A (OSBYTE function code)
95A0 0646 JSR osbyte ; Execute OSBYTE A,X,Y
95A3 0649 EOR #&9d ; Test for OSBYTE &9D (fast Tube BPUT)
95A5 064B BEQ bytex ; OSBYTE &9D (fast Tube BPUT): no result needed
95A7 064D LDA #&40 ; A=&40: high bit will hold carry
95A9 064F ROR ; Encode carry (error flag) into bit 7
95AA 0650 JSR tube_send_r2 ; Send carry+status byte via R2
95AD 0653 .tube_osbyte_send_y←1← 0656 BVC
BIT tube_status_register_2 ; Poll R2 status for ready
95B0 0656 BVC tube_osbyte_send_y ; Not ready: keep polling
95B2 0658 STY tube_data_register_2 ; Send Y result, then fall through to send X
95B5 065B BVS tube_osbyte_send_x ; ALWAYS branch
95B7 065D .tube_osword
JSR tube_read_r2 ; Read OSWORD number from co-processor
95BA 0660 TAY ; Save OSWORD number in Y
95BB 0661 .tube_osword_read←1← 0664 BPL
BIT tube_status_register_2 ; Poll R2 status for data ready
95BE 0664 BPL tube_osword_read ; Not ready: keep polling
95C0 0666 LDX tube_data_register_2 ; Read param block length from R2
95C3 0669 DEX ; DEX: length 0 means no params to read
95C4 066A BMI skip_param_read ; No params (length=0): skip read loop
95C6 066C .tube_osword_read_lp←2← 066F BPL← 0678 BPL
BIT tube_status_register_2 ; Poll R2 status for data ready
95C9 066F BPL tube_osword_read_lp ; Not ready: keep polling
95CB 0671 LDA tube_data_register_2 ; Read param byte from R2
95CE 0674 STA l0128,x ; Store param bytes into block at &0128
95D1 0677 DEX ; Next param byte (descending)
95D2 0678 BPL tube_osword_read_lp ; Loop until all params read
95D4 067A TYA ; Restore OSWORD number from Y
95D5 067B .skip_param_read←1← 066A BMI
LDX #<(l0128) ; XY=&0128: param block address for OSWORD
95D7 067D LDY #>(l0128) ; Y=&01: param block at &0128
95D9 067F JSR osword ; Execute OSWORD with XY=&0128
95DC 0682 LDA #&ff ; A=&FF: result marker for co-processor
95DE 0684 JSR tube_send_r2 ; Send result marker via R2
95E1 0687 .poll_r2_osword_result←1← 068A BPL
BIT tube_status_register_2 ; Poll R2 status for ready
95E4 068A BPL poll_r2_osword_result ; Not ready: keep polling
95E6 068C LDX tube_data_register_2 ; Read result block length from R2
95E9 068F DEX ; Decrement result byte counter
95EA 0690 BMI tube_return_main ; No results to send: return to main loop
95EC 0692 .tube_osword_write←1← 069E BPL
LDY l0128,x ; Send result block bytes from &0128 via R2
95EF 0695 .tube_osword_write_lp←1← 0698 BVC
BIT tube_status_register_2 ; Poll R2 status for ready
95F2 0698 BVC tube_osword_write_lp ; Not ready: keep polling
95F4 069A STY tube_data_register_2 ; Send result byte via R2
95F7 069D DEX ; Next result byte (descending)
95F8 069E BPL tube_osword_write ; Loop until all results sent
95FA 06A0 .tube_return_main←1← 0690 BMI
JMP tube_main_loop ; Return to main event loop
95FD 06A3 .tube_osword_rdln
LDX #4 ; X=4: read 5-byte RDLN control block
95FF 06A5 .read_rdln_ctrl_block←1← 06AB BPL
JSR tube_read_r2 ; Read control block byte from R2
9602 06A8 STA zp_ptr_lo,x ; Store in zero page params
9604 06AA DEX ; Next byte (descending)
9605 06AB BPL read_rdln_ctrl_block ; Loop until all 5 bytes read
9607 06AD INX ; X=0 after loop, A=0 for OSWORD 0 (read line)
9608 06AE LDY #0 ; Y=0 for OSWORD 0
960A 06B0 TXA ; A=0: OSWORD 0 (read line)
960B 06B1 JSR osword ; Read input line from keyboard
960E 06B4 BCC tube_rdln_send_line ; C=0: line read OK; C=1: escape pressed
9610 06B6 LDA #&ff ; &FF = escape/error signal to co-processor
9612 06B8 JMP tube_reply_byte ; Escape: send &FF error to co-processor
9615 06BB .tube_rdln_send_line←1← 06B4 BCC
LDX #0 ; X=0: start of input buffer at &0700
9617 06BD LDA #&7f ; &7F = line read successfully
9619 06BF JSR tube_send_r2 ; Send &7F (success) to co-processor
961C 06C2 .tube_rdln_send_loop←1← 06CB BNE
LDA l0700,x ; Load char from input buffer
961F 06C5 .tube_rdln_send_byte
JSR tube_send_r2 ; Send char to co-processor
9622 06C8 INX ; Next character
9623 06C9 CMP #&0d ; Check for CR terminator
9625 06CB BNE tube_rdln_send_loop ; Loop until CR terminator sent
9627 06CD JMP tube_main_loop ; Return to main event loop
962A 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)
962D 06D3 BVC tube_send_r2 ; Not ready: keep polling
962F 06D5 STA tube_data_register_2 ; Write A to Tube R2 data register
9632 06D8 RTS ; Return to caller
9633 06D9 .tube_send_r4←5← 0018 JSR← 042A JSR← 0432 JSR← 0438 JSR← 06DC BVC
BIT tube_status_register_4_and_cpu_control ; Poll R4 status (bit 6 = ready)
9636 06DC BVC tube_send_r4 ; Not ready: keep polling
9638 06DE STA tube_data_register_4 ; Write A to Tube R4 data register
963B 06E1 RTS ; Return to caller
963C 06E2 .tube_escape_check←1← 0403 JMP
LDA escape_flag ; Check OS escape flag at &FF
963E 06E4 SEC ; SEC+ROR: put bit 7 of &FF into carry+bit 7
963F 06E5 ROR ; ROR: shift escape bit 7 to carry
9640 06E6 BMI tube_send_r1 ; Escape set: forward to co-processor via R1
9642 06E8 .tube_event_handler
PHA ; EVNTV: forward event A, Y, X to co-processor
9643 06E9 LDA #0 ; Send &00 prefix (event notification)
9645 06EB JSR tube_send_r1 ; Send zero prefix via R1
9648 06EE TYA ; Y value for event
9649 06EF JSR tube_send_r1 ; Send Y via R1
964C 06F2 TXA ; X value for event
964D 06F3 JSR tube_send_r1 ; Send X via R1
9650 06F6 PLA ; Restore A (event type)
9651 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)
9654 06FA BVC tube_send_r1 ; Not ready: keep polling
9656 06FC STA tube_data_register_1 ; Write A to Tube R1 data register
9659 06FF RTS ; Return to caller
965A EQUB &E5, &E5, &E5, &E5, &E5, &E5
9660 .trampoline_tx_setup←2← 8683 JSR← 8ECF JMP
JMP tx_begin ; Trampoline: forward to tx_begin
9663 .trampoline_adlc_init←1← 8303 JSR
JMP adlc_init ; Trampoline: forward to adlc_init
9666 .svc_12_nmi_release
JMP wait_nmi_ready ; 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
9681 LDA #osbyte_issue_service_request ; OSBYTE &8F: issue service request
9683 LDX #&0c ; X=&0C: NMI claim service
9685 LDY #&ff ; Y=&FF: pass to adlc_init_workspace
fall through ↓

Initialise NMI workspace

New in 3.35D: issues OSBYTE &8F with X=&0C (NMI claim service request) before copying the NMI shim. Sub-entry at &968A skips the service request for quick re-init. Then copies 32 bytes of NMI shim from ROM (&9FE8) to RAM (&0D00), patches the current ROM bank number into the shim's self-modifying code at &0D07, sets TX clear flag and econet_init_flag to &80, reads station ID from &FE18 (INTOFF side effect), stores it in the TX scout buffer, and re-enables NMIs by reading &FE20 (INTON side effect).

9687 .adlc_init_workspace
JSR osbyte ; Issue paged ROM service call, Reason X=12 - NMI claim
fall through ↓

Initialise NMI workspace (skip service request)

Sub-entry of adlc_init_workspace that skips the OSBYTE &8F service request. Copies 32 bytes of NMI shim from ROM to &0D00, patches the ROM bank number, sets init flags, reads station ID, and re-enables NMIs.

968A .init_nmi_workspace←1← 96E3 JMP
LDY #&20 ; Copy 32 bytes of NMI shim from ROM to &0D00
968C .copy_nmi_shim←1← 9693 BNE
LDA nmi_shim_rom_src,y ; Read byte from NMI shim ROM source
968F STA l0cff,y ; Write to NMI shim RAM at &0D00
9692 DEY ; Next byte (descending)
9693 BNE copy_nmi_shim ; Loop until all 32 bytes copied
9695 LDA romsel_copy ; Patch current ROM bank into NMI shim
9697 STA nmi_shim_07 ; Self-modifying code: ROM bank at &0D07
969A LDA #&80 ; &80 = Econet initialised
969C STA tx_clear_flag ; Mark TX as complete (ready)
969F STA econet_init_flag ; Mark Econet as initialised
96A2 LDA station_id_disable_net_nmis ; Read station ID (&FE18 = INTOFF side effect)
96A5 STA tx_src_stn ; Store our station ID in TX scout
96A8 LDA #0 ; A=0: clear source network
96AA STA tx_src_net ; Clear TX source network byte
96AD BIT video_ula_control ; INTON: re-enable NMIs (&FE20 read side effect)
96B0 RTS ; Return to caller

Wait for NMI subsystem ready and save Econet state

Clears the TX complete flag, then polls econet_init_flag until the NMI subsystem reports initialised. Validates the NMI jump vector points to the expected handler (&9700) by polling nmi_jmp_lo/hi in a tight loop. Once validated, disables NMIs via INTOFF and falls through to save_econet_state. Called from save_vdu_state during VDU state preservation.

96B1 .wait_nmi_ready←1← 9666 JMP
LDA #0 ; A=0: clear TX complete flag
96B3 STA tx_clear_flag ; Clear TX complete flag
96B6 .poll_nmi_ready
BIT econet_init_flag ; Poll Econet init status
96B9 BPL jmp_rx_listen ; Not initialised: skip to RX listen
96BB STA econet_init_flag ; Clear Econet init flag
96BE .nmi_vec_lo_match←2← 96C3 BNE← 96CA BNE
LDA nmi_jmp_lo ; Load NMI vector low byte
96C1 CMP #0 ; Check if low byte is expected value
96C3 BNE nmi_vec_lo_match ; Mismatch: keep polling
96C5 LDA nmi_jmp_hi ; Load NMI vector high byte
96C8 CMP #&97 ; Check if high byte is &97
96CA BNE nmi_vec_lo_match ; Mismatch: keep polling
96CC BIT station_id_disable_net_nmis ; BIT INTOFF: disable NMIs
fall through ↓

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.

96CF .save_econet_state
BIT station_id_disable_net_nmis ; INTOFF: disable NMIs
96D2 LDY #8 ; Y=8: RXCB offset for TX status
96D4 LDA tx_in_progress ; Load current TX status flag
96D7 STA (net_rx_ptr),y ; Save TX status in RXCB
96D9 .jmp_rx_listen←1← 96B9 BPL
JMP adlc_rx_listen ; Enter RX listen mode

Restore Econet TX state and re-enter RX listen

Loads the saved tx_in_progress flag from RXCB offset 8 (previously stored by save_econet_state) and restores it. Then jumps to init_nmi_workspace to re-initialise the NMI handler and return to idle RX listen mode. Called from save_vdu_state during VDU state restoration.

96DC .restore_econet_state←1← 9669 JMP
LDY #8 ; Y=8: RXCB offset for TX status
96DE LDA (net_rx_ptr),y ; Load saved TX status from RXCB
96E0 STA tx_in_progress ; Restore TX status flag
96E3 .enter_rx_listen
JMP init_nmi_workspace ; Re-enter idle RX listen mode

ADLC full reset

Aborts all activity and returns to idle RX listen mode.

96E6 .adlc_full_reset←3← 9672 JSR← 9748 JSR← 989E JSR
LDA #&c1 ; CR1=&C1: TX_RESET | RX_RESET | AC (both sections in reset, address control set)
96E8 STA econet_control1_or_status1 ; Write CR1: full reset
96EB LDA #&1e ; CR4=&1E: 8-bit word, abort ext, NRZ
96ED STA econet_data_terminate_frame ; Write CR4 via ADLC reg 3 (AC=1)
96F0 LDA #0 ; CR3=&00 (via AC=1): no loop-back, no AEX, NRZ, no DTR
96F2 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.

96F5 .adlc_rx_listen←2← 96D9 JMP← 9A56 JSR
LDA #&82 ; CR1=&82: TX_RESET | RIE (TX in reset, RX interrupts enabled)
96F7 STA econet_control1_or_status1 ; Write CR1: RIE | TX_RESET
96FA LDA #&67 ; CR2=&67: CLR_TX_ST | CLR_RX_ST | FC_TDRA | 2_1_BYTE | PSE
96FC STA econet_control23_or_status2 ; Write CR2: listen mode config
96FF 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).

9700 .nmi_rx_scout←1← 9FE5 JMP
LDA #1 ; A=&01: mask for SR2 bit0 (AP = Address Present)
9702 BIT econet_control23_or_status2 ; BIT SR2: Z = A AND SR2 -- tests if AP is set
9705 BEQ scout_error ; AP not set, no incoming data -- check for errors
9707 LDA econet_data_continue_frame ; Read first RX byte (destination station address)
970A CMP station_id_disable_net_nmis ; Compare to our station ID (&FE18 read = INTOFF, disables NMIs)
970D BEQ accept_frame ; Match -- accept frame
970F CMP #&ff ; Check for broadcast address (&FF)
9711 BNE scout_reject ; Neither our address nor broadcast -- reject frame
9713 LDA #&40 ; Flag &40 = broadcast frame
9715 STA tx_flags ; Store broadcast flag in TX flags
9718 .accept_frame←1← 970D BEQ
LDA #&1f ; Install next NMI handler at &971F (RX scout net byte)
971A LDY #&97 ; High byte of scout net handler
971C 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 &9751.

971F .nmi_rx_scout_net
BIT econet_control23_or_status2 ; BIT SR2: test for RDA (bit7 = data available)
9722 BPL scout_error ; No RDA -- check errors
9724 LDA econet_data_continue_frame ; Read destination network byte
9727 BEQ accept_local_net ; Network = 0 -- local network, accept
9729 EOR #&ff ; EOR &FF: test if network = &FF (broadcast)
972B BEQ accept_scout_net ; Broadcast network -- accept
972D .scout_reject←1← 9711 BNE
LDA #&a2 ; Reject: wrong network. CR1=&A2: RIE|RX_DISCONTINUE
972F STA econet_control1_or_status1 ; Write CR1 to discontinue RX
9732 JMP install_rx_scout_handler ; Return to idle scout listening
9735 .accept_local_net←1← 9727 BEQ
STA tx_flags ; Network = 0 (local): clear tx_flags
9738 .accept_scout_net←1← 972B BEQ
STA port_buf_len ; Store Y offset for scout data buffer
973A LDA #&51 ; Install scout data reading loop at &9751
973C LDY #&97 ; High byte of scout data handler
973E 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 &9756) 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 &9A56. This path is a common landing for any unexpected ADLC state during scout reception.

9741 .scout_error←5← 9705 BEQ← 9722 BPL← 9756 BPL← 978A BEQ← 978C BPL
LDA econet_control23_or_status2 ; Read SR2
9744 AND #&81 ; Test AP (b0) | RDA (b7)
9746 BEQ scout_discard ; Neither set -- clean end, discard via &974E
9748 JSR adlc_full_reset ; Unexpected data/status: full ADLC reset
974B JMP install_rx_scout_handler ; Discard and return to idle
974E .scout_discard←1← 9746 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: - SR2 read at entry (&9753) - No RDA (BPL) -> error (&9741) - RDA set (BMI) -> read byte - After first byte (&975F): full SR2 tested - SR2 non-zero (BNE) -> scout completion (&977B) 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 -> read second byte and loop - After second byte (&9773): re-test SR2 for next pair - RDA set (BMI) -> loop back to &9758 - Neither set -> RTI, wait for next NMI The loop ends at Y=&0C (12 bytes max in scout buffer).

9751 .scout_data_loop
LDY port_buf_len ; Y = buffer offset
9753 LDA econet_control23_or_status2 ; Read SR2
9756 .scout_loop_rda←1← 9776 BNE
BPL scout_error ; No RDA -- error handler &9741
9758 LDA econet_data_continue_frame ; Read data byte from RX FIFO
975B STA rx_src_stn,y ; Store at &0D3D+Y (scout buffer)
975E INY ; Advance buffer index
975F LDA econet_control23_or_status2 ; Read SR2 again (FV detection point)
9762 BMI scout_loop_second ; RDA set -- more data, read second byte
9764 BNE scout_complete ; SR2 non-zero (FV or other) -- scout completion
9766 .scout_loop_second←1← 9762 BMI
LDA econet_data_continue_frame ; Read second byte of pair
9769 STA rx_src_stn,y ; Store at &0D3D+Y
976C INY ; Advance and check buffer limit
976D CPY #&0c ; Copied all 12 scout bytes?
976F BEQ scout_complete ; Buffer full (Y=12) -- force completion
9771 STY port_buf_len ; Save final buffer offset
9773 LDA econet_control23_or_status2 ; Read SR2 for next pair
9776 BNE scout_loop_rda ; SR2 non-zero -- loop back for more bytes
9778 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 &9741 (not a valid frame end) - FV set, no RDA (BPL) -> error &9741 (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 (&9A6F) - Port non-zero -> check if it matches an open receive block

977B .scout_complete←2← 9764 BNE← 976F BEQ
LDA #0 ; CR1=&00: disable all interrupts
977D STA econet_control1_or_status1 ; Write CR1
9780 LDA #&84 ; CR2=&84: disable PSE, enable RDA_SUPPRESS_FV
9782 STA econet_control23_or_status2 ; Write CR2
9785 LDA #2 ; A=&02: FV mask for SR2 bit1
9787 BIT econet_control23_or_status2 ; BIT SR2: test FV (Z) and RDA (N)
978A BEQ scout_error ; No FV -- not a valid frame end, error
978C BPL scout_error ; FV set but no RDA -- missing last byte, error
978E LDA econet_data_continue_frame ; Read last byte from RX FIFO
9791 STA rx_src_stn,y ; Store last byte at &0D3D+Y
9794 LDA #&44 ; CR1=&44: RX_RESET | TIE (switch to TX for ACK)
9796 STA econet_control1_or_status1 ; Write CR1: switch to TX mode
9799 LDA rx_port ; Check port byte: 0 = immediate op, non-zero = data transfer
979C BNE scout_match_port ; Port non-zero -- look for matching receive block
979E JMP immediate_op ; Port = 0 -- immediate operation handler
97A1 .scout_no_match←3← 97EC BNE← 97F1 BVC← 9823 JMP
JMP nmi_error_dispatch ; Port = 0 -- immediate operation handler
97A4 .scout_match_port←1← 979C BNE
BIT tx_flags ; Check if broadcast (bit6 of tx_flags)
97A7 BVC scan_port_list ; Not broadcast -- skip CR2 setup
97A9 LDA #7 ; CR2=&07: broadcast prep
97AB STA econet_control23_or_status2 ; Write CR2: broadcast frame prep
97AE .scan_port_list←1← 97A7 BVC
BIT rx_flags ; Check if RX port list active (bit7)
97B1 BPL scout_network_match ; No active ports -- try NFS workspace
97B3 LDA #&c0 ; Start scanning port list at page &C0
97B5 .scan_nfs_port_list
STA port_ws_offset ; Store page to workspace pointer low
97B7 LDA #0 ; A=0: no NFS workspace offset yet
97B9 STA rx_buf_offset ; Clear NFS workspace search flag
97BB .check_port_slot←1← 97E8 BCC
LDY #0 ; Y=0: read control byte from start of slot
97BD .scout_ctrl_check←1← 97FB BEQ
LDA (port_ws_offset),y ; Read port control byte from slot
97BF BEQ scout_station_check ; Zero = end of port list, no match
97C1 CMP #&7f ; &7F = any-port wildcard
97C3 BNE next_port_slot ; Not wildcard -- check specific port match
97C5 INY ; Y=1: advance to port byte in slot
97C6 LDA (port_ws_offset),y ; Read port number from slot (offset 1)
97C8 BEQ check_station_filter ; Zero port in slot = match any port
97CA CMP rx_port ; Check if port matches this slot
97CD BNE next_port_slot ; Port mismatch -- try next slot
97CF .check_station_filter←1← 97C8 BEQ
INY ; Y=2: advance to station byte
97D0 LDA (port_ws_offset),y ; Read station filter from slot (offset 2)
97D2 BEQ scout_port_match ; Zero station = match any station, accept
97D4 CMP rx_src_stn ; Check if source station matches
97D7 BNE next_port_slot ; Station mismatch -- try next slot
97D9 .scout_port_match←1← 97D2 BEQ
INY ; Y=3: advance to network byte
97DA LDA (port_ws_offset),y ; Read network filter from slot (offset 3)
97DC CMP rx_src_net ; Check if source network matches
97DF BEQ scout_accept ; Network matches or zero = accept
97E1 .next_port_slot←3← 97C3 BNE← 97CD BNE← 97D7 BNE
LDA port_ws_offset ; Check if NFS workspace search pending
97E3 CLC ; CLC for 12-byte slot advance
97E4 ADC #&0c ; Advance to next 12-byte port slot
97E6 STA port_ws_offset ; Update workspace pointer to next slot
97E8 BCC check_port_slot ; Always branches (page &C0 won't overflow)
97EA .scout_station_check←1← 97BF BEQ
LDA rx_buf_offset ; Check if NFS workspace already searched
97EC BNE scout_no_match ; Already searched: no match found
97EE .scout_network_match←1← 97B1 BPL
BIT rx_flags ; Try NFS workspace if paged list exhausted
97F1 BVC scout_no_match ; No NFS workspace RX (bit6 clear) -- discard
97F3 LDA nfs_workspace_hi ; Get NFS workspace page number
97F5 STA rx_buf_offset ; Mark NFS workspace as search target
97F7 LDY #0 ; Y=0: start at offset 0 in workspace
97F9 STY port_ws_offset ; Reset slot pointer to start
97FB BEQ scout_ctrl_check ; ALWAYS branch
97FD .scout_accept←1← 97DF BEQ
BIT tx_flags ; Check broadcast flag (bit 6)
9800 BVC ack_scout_match ; Not broadcast: ACK and set up RX
9802 JMP copy_scout_fields ; Broadcast: copy scout fields directly
9805 .ack_scout_match←2← 9800 BVC← 9ADB JMP
LDA #3 ; Match found: set scout_status = 3
9807 STA scout_status ; Record match for completion handler
980A LDA nmi_tx_block ; Save current TX block ptr (low)
980C PHA ; Push TX block low on stack
980D LDA nmi_tx_block_hi ; Save current TX block ptr (high)
980F PHA ; Push TX block high on stack
9810 LDA port_ws_offset ; Use port slot as temp RXCB ptr (lo)
9812 STA nmi_tx_block ; Set RXCB low for tx_calc_transfer
9814 LDA rx_buf_offset ; Use workspace page as temp RXCB (hi)
9816 STA nmi_tx_block_hi ; Set RXCB high for tx_calc_transfer
9818 JSR tx_calc_transfer ; Calculate transfer parameters
981B PLA ; Restore original TX block (high)
981C STA nmi_tx_block_hi ; Restore TX block ptr (high)
981E PLA ; Restore original TX block (low)
981F STA nmi_tx_block ; Restore TX block ptr (low)
9821 BCS send_data_rx_ack ; Transfer OK: send data ACK
9823 JMP scout_no_match ; Broadcast: different completion path
9826 .send_data_rx_ack←2← 9821 BCS← 9AD0 JMP
LDA #&44 ; CR1=&44: RX_RESET | TIE
9828 STA econet_control1_or_status1 ; Write CR1: TX mode for ACK
982B LDA #&a7 ; CR2=&A7: RTS | CLR_TX_ST | FC_TDRA | PSE
982D STA econet_control23_or_status2 ; Write CR2: enable TX with PSE
9830 LDA #&37 ; Install data_rx_setup at &97DC
9832 LDY #&98 ; High byte of data_rx_setup handler
9834 JMP ack_tx_write_dest ; Send ACK with data_rx_setup as next NMI
9837 .data_rx_setup
LDA #&82 ; CR1=&82: TX_RESET | RIE (switch to RX for data frame)
9839 STA econet_control1_or_status1 ; Write CR1: switch to RX for data frame
983C LDA #&43 ; Install nmi_data_rx at &9843
983E LDY #&98 ; High byte of nmi_data_rx handler
9840 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: &9843 (AP+addr check) -> &9859 (net=0 check) -> &986F (skip ctrl+port) -> &98A4 (bulk data read) -> &98D8 (completion)

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

Install data RX bulk or Tube handler

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

987A .install_data_rx_handler←1← 9F3E JMP
LDA #2 ; A=2: Tube transfer flag mask
987C BIT tx_flags ; Check if Tube transfer active
987F BNE install_tube_rx ; Tube active: use Tube RX path
9881 LDA #&a4 ; Install bulk read at &9843
9883 LDY #&98 ; High byte of &9843 handler
9885 BIT econet_control1_or_status1 ; SR1 bit7: more data already waiting?
9888 BMI nmi_data_rx_bulk ; Yes: enter bulk read directly
988A JMP set_nmi_vector ; No: install handler and RTI
988D .install_tube_rx←1← 987F BNE
LDA #1 ; Tube: install Tube RX at &9901
988F LDY #&99 ; High byte of &9901 handler
9891 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).

9894 .nmi_error_dispatch←12← 97A1 JMP← 9848 BEQ← 9850 BNE← 985C BPL← 9861 BNE← 9872 BPL← 98B7 BEQ← 98E9 BEQ← 98EF BEQ← 993A JMP← 99C2 JMP← 9AA2 JMP
LDA tx_flags ; Check tx_flags for error path
9897 BPL rx_error ; Bit7 clear: RX error path
9899 LDA #&41 ; A=&41: 'not listening' error
989B JMP tx_store_result ; Bit7 set: TX result = not listening
989E .rx_error←1← 9897 BPL
.rx_error_reset←1← 9897 BPL
JSR adlc_full_reset ; Full ADLC reset on RX error
98A1 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 &98D8. SR2 = 0 -> RTI, wait for next NMI to continue.

98A4 .nmi_data_rx_bulk←1← 9888 BMI
LDY port_buf_len ; Y = buffer offset, resume from last position
98A6 LDA econet_control23_or_status2 ; Read SR2 for next pair
98A9 .data_rx_loop←1← 98D3 BNE
BPL data_rx_complete ; SR2 bit7 clear: frame complete (FV)
98AB LDA econet_data_continue_frame ; Read first byte of pair from RX FIFO
98AE STA (open_port_buf),y ; Store byte to buffer
98B0 INY ; Advance buffer offset
98B1 BNE read_sr2_between_pairs ; Y != 0: no page boundary crossing
98B3 INC open_port_buf_hi ; Crossed page: increment buffer high byte
98B5 DEC port_buf_len_hi ; Decrement remaining page count
98B7 BEQ nmi_error_dispatch ; No pages left: handle as complete
98B9 .read_sr2_between_pairs←1← 98B1 BNE
LDA econet_control23_or_status2 ; Read SR2 between byte pairs
98BC BMI read_second_rx_byte ; SR2 bit7 set: more data available
98BE BNE data_rx_complete ; SR2 non-zero, bit7 clear: frame done
98C0 .read_second_rx_byte←1← 98BC BMI
LDA econet_data_continue_frame ; Read second byte of pair from RX FIFO
98C3 STA (open_port_buf),y ; Store byte to buffer
98C5 INY ; Advance buffer offset
98C6 STY port_buf_len ; Save updated buffer position
98C8 BNE check_sr2_loop_again ; Y != 0: no page boundary crossing
98CA INC open_port_buf_hi ; Crossed page: increment buffer high byte
98CC DEC port_buf_len_hi ; Decrement remaining page count
98CE BEQ data_rx_complete ; No pages left: frame complete
98D0 .check_sr2_loop_again←1← 98C8 BNE
LDA econet_control23_or_status2 ; Read SR2 for next iteration
98D3 BNE data_rx_loop ; SR2 non-zero: more data, loop back
98D5 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 (&977B): 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 &9968.

98D8 .data_rx_complete←3← 98A9 BPL← 98BE BNE← 98CE BEQ
LDA #0 ; CR1=&00: disable all interrupts
98DA STA econet_control1_or_status1 ; Write CR1
98DD LDA #&84 ; CR2=&84: disable PSE for individual bit testing
98DF STA econet_control23_or_status2 ; Write CR2
98E2 STY port_buf_len ; Save Y (byte count from data RX loop)
98E4 LDA #2 ; A=&02: FV mask
98E6 BIT econet_control23_or_status2 ; BIT SR2: test FV (Z) and RDA (N)
98E9 BEQ nmi_error_dispatch ; No FV -- error
98EB BPL send_ack ; FV set, no RDA -- proceed to ACK
98ED LDA port_buf_len_hi ; Check if buffer space remains
98EF .read_last_rx_byte
BEQ nmi_error_dispatch ; No buffer space: error/discard frame
98F1 LDA econet_data_continue_frame ; FV+RDA: read and store last data byte
98F4 LDY port_buf_len ; Y = current buffer write offset
98F6 STA (open_port_buf),y ; Store last byte in port receive buffer
98F8 INC port_buf_len ; Advance buffer write offset
98FA BNE send_ack ; No page wrap: proceed to send ACK
98FC INC open_port_buf_hi ; Page boundary: advance buffer page
98FE .send_ack←2← 98EB BPL← 98FA BNE
JMP ack_tx ; Send ACK frame to complete handshake
9901 .nmi_data_rx_tube
LDA econet_control23_or_status2 ; Read SR2 for Tube data receive path
9904 .rx_tube_data←1← 9935 BNE
BPL data_rx_tube_complete ; RDA clear: no more data, frame complete
9906 LDA econet_data_continue_frame ; Read data byte from ADLC RX FIFO
9909 INC port_buf_len ; Advance Tube transfer byte count
990B STA tube_data_register_3 ; Send byte to Tube data register 3
990E BNE rx_update_buf ; No overflow: read second byte
9910 INC port_buf_len_hi ; Carry to transfer count byte 2
9912 BNE rx_update_buf ; No overflow: read second byte
9914 INC open_port_buf ; Carry to transfer count byte 3
9916 BNE rx_update_buf ; No overflow: read second byte
9918 INC open_port_buf_hi ; Carry to transfer count byte 4
991A BEQ data_rx_tube_error ; All bytes zero: overflow error
991C .rx_update_buf←3← 990E BNE← 9912 BNE← 9916 BNE
LDA econet_data_continue_frame ; Read second data byte (paired transfer)
991F STA tube_data_register_3 ; Send second byte to Tube
9922 INC port_buf_len ; Advance count after second byte
9924 BNE rx_check_error ; No overflow: check for more data
9926 INC port_buf_len_hi ; Carry to count byte 2
9928 BNE rx_check_error ; No overflow: check for more data
992A INC open_port_buf ; Carry to count byte 3
992C BNE rx_check_error ; No overflow: check for more data
992E INC open_port_buf_hi ; Carry to count byte 4
9930 BEQ data_rx_tube_complete ; Zero: Tube transfer complete
9932 .rx_check_error←3← 9924 BNE← 9928 BNE← 992C BNE
LDA econet_control23_or_status2 ; Re-read SR2 for next byte pair
9935 BNE rx_tube_data ; More data available: continue loop
9937 JMP nmi_rti ; Return from NMI, wait for data
993A .data_rx_tube_error←3← 991A BEQ← 994C BEQ← 9958 BEQ
JMP nmi_error_dispatch ; Unexpected end: return from NMI
993D .data_rx_tube_complete←2← 9904 BPL← 9930 BEQ
LDA #0 ; CR1=&00: disable all interrupts
993F STA econet_control1_or_status1 ; Write CR1 for individual bit testing
9942 LDA #&84 ; CR2=&84: disable PSE
9944 STA econet_control23_or_status2 ; Write CR2: same pattern as main path
9947 LDA #2 ; A=&02: FV mask for Tube completion
9949 BIT econet_control23_or_status2 ; BIT SR2: test FV (Z) and RDA (N)
994C BEQ data_rx_tube_error ; No FV: incomplete frame, error
994E BPL ack_tx ; FV set, no RDA: proceed to ACK
9950 LDA port_buf_len ; Check if any buffer was allocated
9952 ORA port_buf_len_hi ; OR all 4 buffer pointer bytes together
9954 ORA open_port_buf ; Check buffer low byte
9956 ORA open_port_buf_hi ; Check buffer high byte
9958 BEQ data_rx_tube_error ; All zero (null buffer): error
995A LDA econet_data_continue_frame ; Read extra trailing byte from FIFO
995D STA rx_extra_byte ; Save extra byte at &0D5D for later use
9960 LDA #&20 ; Bit5 = extra data byte available flag
9962 ORA tx_flags ; Set extra byte flag in tx_flags
9965 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 (&9F48). 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.

9968 .ack_tx←2← 98FE JMP← 994E BPL
LDA tx_flags ; Load TX flags to check ACK type
996B BPL ack_tx_configure ; Bit7 clear: normal scout ACK
996D JMP tx_result_ok ; Jump to TX success result
9970 .ack_tx_configure←1← 996B BPL
LDA #&44 ; CR1=&44: RX_RESET | TIE (switch to TX mode)
9972 STA econet_control1_or_status1 ; Write CR1: switch to TX mode
9975 LDA #&a7 ; CR2=&A7: RTS|CLR_TX_ST|FC_TDRA|2_1_BYTE|PSE
9977 STA econet_control23_or_status2 ; Write CR2: enable TX with status clear
997A LDA #&c5 ; Install saved next handler (&99C5 for scout ACK)
997C LDY #&99 ; High byte of post-ACK handler
997E .ack_tx_write_dest←2← 9834 JMP← 9B25 JMP
STA nmi_next_lo ; Store next handler low byte
9981 STY nmi_next_hi ; Store next handler high byte
9984 LDA rx_src_stn ; Load dest station from RX scout buffer
9987 BIT econet_control1_or_status1 ; BIT SR1: test TDRA (V=bit6)
998A BVC tdra_error ; TDRA not ready -- error
998C STA econet_data_continue_frame ; Write dest station to TX FIFO
998F LDA rx_src_net ; Write dest network to TX FIFO
9992 STA econet_data_continue_frame ; Write dest net byte to FIFO
9995 LDA #&9c ; Install nmi_ack_tx_src at &999C
9997 LDY #&99 ; High byte of nmi_ack_tx_src
9999 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.

999C .nmi_ack_tx_src
LDA station_id_disable_net_nmis ; Load our station ID (also INTOFF)
999F BIT econet_control1_or_status1 ; BIT SR1: test TDRA
99A2 BVC tdra_error ; TDRA not ready -- error
99A4 STA econet_data_continue_frame ; Write our station to TX FIFO
99A7 LDA #0 ; Write network=0 to TX FIFO
99A9 STA econet_data_continue_frame ; Write network=0 (local) to TX FIFO
99AC LDA tx_flags ; Check tx_flags for data phase
99AF BMI start_data_tx ; bit7 set: start data TX phase
99B1 LDA #&3f ; CR2=&3F: TX_LAST_DATA | CLR_RX_ST | FLAG_IDLE | FC_TDRA | 2_1_BYTE | PSE
99B3 STA econet_control23_or_status2 ; Write CR2 to clear status after ACK TX
99B6 LDA nmi_next_lo ; Install saved handler from &0D4B/&0D4C
99B9 LDY nmi_next_hi ; Load saved next handler high byte
99BC JMP set_nmi_vector ; Install next NMI handler
99BF .start_data_tx←1← 99AF BMI
JMP data_tx_begin ; Jump to start data TX phase
99C2 .tdra_error←2← 998A BVC← 99A2 BVC
JMP nmi_error_dispatch ; 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.

99C5 .post_ack_scout
LDA rx_port ; Check port byte from scout
99C8 BNE advance_rx_buffer_ptr ; Non-zero port: advance RX buffer
99CA LDY rx_ctrl ; Load control byte from scout
99CD CPY #&82 ; Is ctrl &82 (immediate peek)?
99CF BEQ advance_rx_buffer_ptr ; Yes: advance RX buffer for peek
99D1 .dispatch_nmi_error
JMP imm_op_build_reply ; 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.

99D4 .advance_rx_buffer_ptr←2← 99C8 BNE← 99CF BEQ
LDA #2 ; A=2: test bit1 of tx_flags
99D6 BIT tx_flags ; BIT tx_flags: check data transfer bit
99D9 BEQ add_buf_to_base ; Bit1 clear: no transfer -- return
99DB CLC ; CLC: init carry for 4-byte add
99DC PHP ; Save carry on stack for loop
99DD LDY #8 ; Y=8: RXCB high pointer offset
99DF .add_rxcb_ptr←1← 99EB BCC
LDA (port_ws_offset),y ; Load RXCB[Y] (buffer pointer byte)
99E1 PLP ; Restore carry from stack
99E2 ADC net_tx_ptr,y ; Add transfer count byte
99E5 STA (port_ws_offset),y ; Store updated pointer back to RXCB
99E7 INY ; Next byte
99E8 PHP ; Save carry for next iteration
99E9 CPY #&0c ; Done 4 bytes? (Y reaches &0C)
99EB BCC add_rxcb_ptr ; No: continue adding
99ED PLP ; Discard final carry
99EE LDA #&20 ; A=&20: test bit5 of tx_flags
99F0 BIT tx_flags ; BIT tx_flags: check Tube bit
99F3 BEQ skip_buf_ptr_update ; No Tube: skip Tube update
99F5 TXA ; Save X on stack
99F6 PHA ; Push X
99F7 LDA #8 ; A=8: offset for Tube address
99F9 CLC ; CLC for address calculation
99FA ADC port_ws_offset ; Add workspace base offset
99FC TAX ; X = address low for Tube claim
99FD LDY rx_buf_offset ; Y = address high for Tube claim
99FF LDA #1 ; A=1: Tube claim type (read)
9A01 JSR tube_addr_claim ; Claim Tube address for transfer
9A04 LDA rx_extra_byte ; Load extra RX data byte
9A07 STA tube_data_register_3 ; Send to Tube via R3
9A0A PLA ; Restore X from stack
9A0B TAX ; Transfer to X register
9A0C LDY #8 ; Y=8: RXCB buffer ptr offset
9A0E LDA (port_ws_offset),y ; Load current RXCB buffer ptr lo
9A10 SEC ; SEC for ADC #0 = add carry
9A11 ADC #0 ; Increment by 1 (Tube extra byte)
9A13 STA (port_ws_offset),y ; Store updated ptr back to RXCB
9A15 JMP skip_buf_ptr_update ; Other port-0 ops: immediate dispatch
9A18 .add_buf_to_base←1← 99D9 BEQ
LDA port_buf_len ; Load buffer bytes remaining
9A1A CLC ; CLC for address add
9A1B ADC open_port_buf ; Add to buffer base address
9A1D BCC store_buf_ptr_lo ; No carry: skip high byte increment
9A1F .inc_rxcb_buf_hi
INC open_port_buf_hi ; Carry: increment buffer high byte
9A21 .store_buf_ptr_lo←1← 9A1D BCC
LDY #8 ; Y=8: store updated buffer position
9A23 .store_rxcb_buf_ptr
STA (port_ws_offset),y ; Store updated low byte to RXCB
9A25 INY ; Y=9: buffer high byte offset
9A26 LDA open_port_buf_hi ; Load updated buffer high byte
9A28 .store_rxcb_buf_hi
STA (port_ws_offset),y ; Store high byte to RXCB
9A2A .skip_buf_ptr_update←3← 99F3 BEQ← 9A15 JMP← 9A6C JMP
LDA rx_port ; Check port byte again
9A2D BEQ discard_reset_listen ; Port=0: immediate op, discard+listen
9A2F LDA rx_src_net ; Load source network from scout buffer
9A32 LDY #3 ; Y=3: RXCB source network offset
9A34 STA (port_ws_offset),y ; Store source network to RXCB
9A36 DEY ; Y=2: source station offset Y=&02
9A37 LDA rx_src_stn ; Load source station from scout buffer
9A3A STA (port_ws_offset),y ; Store source station to RXCB
9A3C DEY ; Y=1: port byte offset Y=&01
9A3D LDA rx_port ; Load port byte
9A40 STA (port_ws_offset),y ; Store port to RXCB
9A42 DEY ; Y=0: control/flag byte offset Y=&00
9A43 LDA rx_ctrl ; Load control byte from scout
9A46 ORA #&80 ; Set bit7 = reception complete flag
9A48 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 install_rx_scout_handler. 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.

9A4A .discard_reset_listen←4← 98A1 JMP← 9A2D BEQ← 9EA2 JMP← 9F57 JMP
LDA #2 ; Tube flag bit 1 AND tx_flags bit 1
9A4C BIT tx_flags ; Test tx_flags for Tube transfer
9A4F BEQ discard_listen ; No Tube transfer active -- skip release
9A51 LDA #&82 ; A=&82: Tube release claim type
9A53 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 install_rx_scout_handler. Used for clean rejection of frames that are correctly formatted but not for us (wrong station/network).

9A56 .discard_listen←3← 974E JMP← 9A4F BEQ← 9B5E JMP
JSR adlc_rx_listen ; Re-enter idle RX listen mode
fall through ↓

Install RX scout NMI handler

Installs nmi_rx_scout (&9700) as the NMI handler via set_nmi_vector, without first calling adlc_rx_listen. Used when the ADLC is already in the correct RX mode.

9A59 .install_rx_scout_handler←2← 9732 JMP← 974B JMP
LDA #0 ; Install nmi_rx_scout (&9700) as NMI handler
9A5B LDY #&97 ; High byte of nmi_rx_scout
9A5D JMP set_nmi_vector ; Set NMI vector and return
9A60 .copy_scout_fields←1← 9802 JMP
LDY #4 ; Y=4: start at RX CB offset 4
9A62 .copy_scout_loop←1← 9A6A BNE
LDA rx_src_stn,y ; Load scout field (stn/net/ctrl/port)
9A65 STA (port_ws_offset),y ; Store to port workspace buffer
9A67 INY ; Advance buffer pointer
9A68 CPY #&0c ; All 8 fields copied?
9A6A BNE copy_scout_loop ; No: continue copy loop
9A6C JMP skip_buf_ptr_update ; 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.

9A6F .immediate_op←1← 979E JMP
LDY rx_ctrl ; Control byte &81-&88 range check
9A72 CPY #&81 ; Below &81: not an immediate op
9A74 BCC imm_op_out_of_range ; Out of range low: jump to discard
9A76 CPY #&89 ; Above &88: not an immediate op
9A78 BCS imm_op_out_of_range ; Out of range high: jump to discard
9A7A CPY #&87 ; HALT(&87)/CONTINUE(&88) skip protection
9A7C BCS imm_op_dispatch ; Ctrl >= &87: dispatch without mask check
9A7E LDA rx_src_stn ; Load source station number
9A81 CMP #&f0 ; Station >= &F0? (privileged)
9A83 BCS imm_op_dispatch ; Privileged: skip protection check
9A85 TYA ; Convert ctrl byte to 0-based index for mask
9A86 SEC ; SEC for subtract
9A87 SBC #&81 ; A = ctrl - &81 (0-based operation index)
9A89 TAY ; Y = index for mask rotation count
9A8A LDA prot_status ; Load protection mask from LSTAT
9A8D .rotate_prot_mask←1← 9A8F BPL
ROR ; Rotate mask right by control byte index
9A8E DEY ; Decrement rotation counter
9A8F BPL rotate_prot_mask ; Loop until bit aligned
9A91 BCC imm_op_dispatch ; Carry clear: operation permitted
9A93 JMP imm_op_discard ; Operation blocked by LSTAT mask
9A96 .imm_op_dispatch←3← 9A7C BCS← 9A83 BCS← 9A91 BCC
LDY rx_ctrl ; Reload ctrl byte for dispatch table
9A99 LDA rx_port_operand,y ; Look up handler address high byte
9A9C PHA ; Push handler address high
9A9D LDA store_rxcb_byte,y ; Load handler low byte from jump table
9AA0 PHA ; Push handler address low
9AA1 RTS ; RTS dispatches to handler
9AA2 .imm_op_out_of_range←2← 9A74 BCC← 9A78 BCS
JMP nmi_error_dispatch ; Jump to discard handler
9AA5 EQUB <(rx_imm_peek-1)
9AA6 EQUB <(rx_imm_poke-1)
9AA7 EQUB <(rx_imm_exec-1)
9AA8 EQUB <(rx_imm_exec-1)
9AA9 EQUB <(rx_imm_exec-1)
9AAA EQUB <(rx_imm_halt_cont-1)
9AAB EQUB <(rx_imm_halt_cont-1)
9AAC EQUB <(rx_imm_machine_type-1)
9AAD EQUB >(rx_imm_peek-1)
9AAE EQUB >(rx_imm_poke-1)
9AAF EQUB >(rx_imm_exec-1)
9AB0 EQUB >(rx_imm_exec-1)
9AB1 EQUB >(rx_imm_exec-1)
9AB2 EQUB >(rx_imm_halt_cont-1)
9AB3 EQUB >(rx_imm_halt_cont-1)
9AB4 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 at &0D58, then jumps to the common receive path at c9826. Used for operation types &83 (JSR), &84 (UserProc), and &85 (OSProc).

9AB5 .rx_imm_exec
LDA #0 ; Buffer start lo = &00
9AB7 STA open_port_buf ; Set port buffer lo
9AB9 LDA #&82 ; Buffer length lo = &82
9ABB STA port_buf_len ; Set buffer length lo
9ABD LDA #1 ; Buffer length hi = 1
9ABF STA port_buf_len_hi ; Set buffer length hi
9AC1 LDA net_rx_ptr_hi ; Load RX page hi for buffer
9AC3 STA open_port_buf_hi ; Set port buffer hi
9AC5 LDY #3 ; Y=3: copy 4 bytes (3 down to 0)
9AC7 .copy_addr_loop←1← 9ACE BPL
LDA rx_remote_addr,y ; Load remote address byte
9ACA STA l0d58,y ; Store to exec address workspace
9ACD DEY ; Next byte (descending)
9ACE BPL copy_addr_loop ; Loop until all 4 bytes copied
9AD0 JMP send_data_rx_ack ; Enter common data-receive path

RX immediate: POKE setup

Sets up workspace offsets for receiving POKE data. port_ws_offset=&3D, rx_buf_offset=&0D, then jumps to the common data-receive path at c9805.

9AD3 .rx_imm_poke
LDA #&3d ; Port workspace offset = &3D
9AD5 STA port_ws_offset ; Store workspace offset lo
9AD7 LDA #&0d ; RX buffer page = &0D
9AD9 STA rx_buf_offset ; Store workspace offset hi
9ADB JMP ack_scout_match ; Enter POKE data-receive path

RX immediate: machine type query

Sets up a buffer just below screen memory (length #&01FC) for the machine type query response, then jumps to the query handler at set_tx_reply_flag. Returns system identification data to the remote station.

9ADE .rx_imm_machine_type
LDA #1 ; Buffer length hi = 1
9AE0 STA port_buf_len_hi ; Set buffer length hi
9AE2 LDA #&fc ; Buffer length lo = &FC
9AE4 STA port_buf_len ; Set buffer length lo
9AE6 LDA #&21 ; Buffer start lo = &21
9AE8 STA open_port_buf ; Set port buffer lo
9AEA LDA #&7f ; Buffer hi = &7F (below screen)
9AEC STA open_port_buf_hi ; Set port buffer hi
9AEE 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 &0D3D, and prepares to send the PEEK response data back to the requesting station.

9AF1 .rx_imm_peek
LDA nmi_tx_block ; Save current TX block low byte
9AF3 PHA ; Push to stack
9AF4 LDA nmi_tx_block_hi ; Save current TX block high byte
9AF6 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.

9AF7 .rx_imm_peek_setup
LDA #&3d ; Port workspace offset = &3D
9AF9 STA nmi_tx_block ; Store workspace offset lo
9AFB LDA #&0d ; RX buffer page = &0D
9AFD STA nmi_tx_block_hi ; Store workspace offset hi
9AFF LDA #2 ; Scout status = 2 (PEEK response)
9B01 STA scout_status ; Store scout status
9B04 JSR tx_calc_transfer ; Calculate transfer size for response
9B07 PLA ; Restore saved nmi_tx_block_hi
9B08 STA nmi_tx_block_hi ; Restore workspace ptr hi byte
9B0A PLA ; Restore saved nmi_tx_block
9B0B STA nmi_tx_block ; Restore workspace ptr lo byte
9B0D BCC imm_op_discard ; C=0: transfer not set up, discard
9B0F .set_tx_reply_flag←1← 9AEE JMP
LDA tx_flags ; Mark TX flags bit 7 (reply pending)
9B12 ORA #&80 ; Set reply pending flag
9B14 STA tx_flags ; Store updated TX flags
9B17 .rx_imm_halt_cont
LDA #&44 ; CR1=&44: TIE | TX_LAST_DATA
9B19 STA econet_control1_or_status1 ; Write CR1: enable TX interrupts
9B1C .tx_cr2_setup
LDA #&a7 ; CR2=&A7: RTS|CLR_RX_ST|FC_TDRA|PSE
9B1E STA econet_control23_or_status2 ; Write CR2 for TX setup
9B21 .tx_nmi_setup
LDA #&3e ; NMI handler lo byte (self-modifying)
9B23 LDY #&9b ; Y=&9B: dispatch table page
9B25 JMP ack_tx_write_dest ; Acknowledge and write TX dest

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.

9B28 .imm_op_build_reply←1← 99D1 JMP
LDA port_buf_len ; Get buffer position for reply header
9B2A CLC ; Clear carry for offset addition
9B2B ADC #&80 ; Data offset = buf_len + &80 (past header)
9B2D LDY #&7f ; Y=&7F: reply data length slot
9B2F STA (net_rx_ptr),y ; Store reply data length in RX buffer
9B31 LDY #&80 ; Y=&80: source station slot
9B33 LDA rx_src_stn ; Load requesting station number
9B36 STA (net_rx_ptr),y ; Store source station in reply header
9B38 INY ; Y=&81
9B39 LDA rx_src_net ; Load requesting network number
9B3C STA (net_rx_ptr),y ; Store source network in reply header
9B3E LDA rx_ctrl ; Load control byte from received frame
9B41 STA tx_work_57 ; Save ctrl byte for TX response
9B44 LDA #&84 ; IER bit 2: disable SR interrupt
9B46 STA system_via_ier ; Write IER to disable SR
9B49 LDA system_via_acr ; Read ACR for shift register config
9B4C AND #&1c ; Isolate shift register mode bits (2-4)
9B4E STA tx_work_51 ; Save original SR mode for later restore
9B51 LDA system_via_acr ; Reload ACR for modification
9B54 AND #&e3 ; Clear SR mode bits (keep other bits)
9B56 ORA #8 ; SR mode 2: shift in under φ2
9B58 STA system_via_acr ; Apply new shift register mode
9B5B BIT system_via_sr ; Read SR to clear pending interrupt
9B5E .imm_op_discard←2← 9A93 JMP← 9B0D BCC
JMP discard_listen ; Return to idle listen mode
9B61 .check_sr_irq←1← 966C JMP
LDA #4 ; A=&04: IFR bit 2 (SR) mask
9B63 BIT system_via_ifr ; Test SR interrupt pending
9B66 BNE tx_done_error ; SR fired: handle TX completion
9B68 LDA #5 ; A=5: no SR, return status 5
9B6A RTS ; Return (no SR interrupt)
9B6B .tx_done_error←1← 9B66 BNE
TXA ; Save X
9B6C PHA ; Push X
9B6D TYA ; Save Y
9B6E PHA ; Push Y
9B6F LDA system_via_acr ; Read ACR for shift register mode
9B72 AND #&e3 ; Clear SR mode bits (2-4)
9B74 ORA tx_work_51 ; Restore original SR mode
9B77 STA system_via_acr ; Write updated ACR
9B7A LDA system_via_sr ; Read SR to clear pending interrupt
9B7D LDA #4 ; A=&04: SR bit mask
9B7F STA system_via_ifr ; Clear SR in IFR
9B82 STA system_via_ier ; Disable SR in IER
9B85 LDY tx_work_57 ; Load ctrl byte for dispatch
9B88 CPY #&86 ; Ctrl >= &86? (HALT/CONTINUE)
9B8A BCS tx_done_classify ; Yes: skip protection mask save
9B8C LDA prot_status ; Load current protection mask
9B8F STA saved_jsr_mask ; Save mask before JSR modification
9B92 ORA #&1c ; Enable bits 2-4 (allow JSR ops)
9B94 STA prot_status ; Store modified protection mask
9B97 .tx_done_classify←1← 9B8A BCS
LDA tx_nmi_lo_operand,y ; Load handler addr hi from table
9B9A PHA ; Push handler hi
9B9B LDA tx_cr2_operand,y ; Load handler addr lo from table
9B9E PHA ; Push handler lo
9B9F RTS ; Dispatch via RTS (addr-1 on stack)
9BA0 EQUB <(tx_done_jsr-1)
9BA1 EQUB <(tx_done_user_proc-1)
9BA2 EQUB <(tx_done_os_proc-1)
9BA3 EQUB <(tx_done_halt-1)
9BA4 EQUB <(tx_done_continue-1)
9BA5 EQUB >(tx_done_jsr-1)
9BA6 EQUB >(tx_done_user_proc-1)
9BA7 EQUB >(tx_done_os_proc-1)
9BA8 EQUB >(tx_done_halt-1)
9BA9 EQUB >(tx_done_continue-1)

TX done: remote JSR execution

Pushes address tx_done_exit-1 on the stack (so RTS returns to tx_done_exit), then does JMP (l0d58) to call the remote JSR target routine. When that routine returns via RTS, control resumes at tx_done_exit.

9BAA .tx_done_jsr
LDA #&9b ; Push hi of (tx_done_exit-1)
9BAC PHA ; Push hi byte on stack
9BAD LDA #&eb ; Push lo of (tx_done_exit-1)
9BAF PHA ; Push lo byte on stack
9BB0 JMP (l0d58) ; Call remote JSR; RTS to tx_done_exit

TX done: UserProc event

Generates a network event (event 8) via OSEVEN with X=l0d58, A=l0d59 (the remote address). This notifies the user program that a UserProc operation has completed.

9BB3 .tx_done_user_proc
LDY #event_network_error ; Y=8: network event type
9BB5 LDX l0d58 ; X = remote address lo
9BB8 LDA l0d59 ; A = remote address hi
9BBB JSR oseven ; Generate event Y='Network error'
9BBE JMP tx_done_exit ; Exit TX done handler

TX done: OSProc call

Calls the ROM entry point at &8000 (rom_header) with X=l0d58, Y=l0d59. This invokes an OS-level procedure on behalf of the remote station.

9BC1 .tx_done_os_proc
LDX l0d58 ; X = remote address lo
9BC4 LDY l0d59 ; Y = remote address hi
9BC7 JSR rom_header ; Call ROM entry point at &8000
9BCA JMP tx_done_exit ; Exit TX done handler

TX done: HALT

Sets bit 2 of rx_flags (&0D64), 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.

9BCD .tx_done_halt
LDA #4 ; A=&04: bit 2 mask for rx_flags
9BCF BIT rx_flags ; Test if already halted
9BD2 BNE tx_done_exit ; Already halted: skip to exit
9BD4 ORA rx_flags ; Set bit 2 in rx_flags
9BD7 STA rx_flags ; Store halt flag
9BDA LDA #4 ; A=4: re-load halt bit mask
9BDC CLI ; Enable interrupts during halt wait
9BDD .halt_spin_loop←1← 9BE0 BNE
BIT rx_flags ; Test halt flag
9BE0 BNE halt_spin_loop ; Still halted: keep spinning
9BE2 BEQ tx_done_exit ; ALWAYS branch

TX done: CONTINUE

Clears bit 2 of rx_flags (&0D64), releasing any station that is halted and spinning in tx_done_halt.

9BE4 .tx_done_continue
LDA rx_flags ; Load current RX flags
9BE7 AND #&fb ; Clear bit 2: release halted station
9BE9 STA rx_flags ; Store updated flags
9BEC .tx_done_exit←4← 9BBE JMP← 9BCA JMP← 9BD2 BNE← 9BE2 BEQ
PLA ; Restore Y from stack
9BED TAY ; Transfer to Y register
9BEE PLA ; Restore X from stack
9BEF TAX ; Transfer to X register
9BF0 LDA #0 ; A=0: success status
9BF2 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.

9BF3 .tx_begin←1← 9660 JMP
TXA ; Save X on stack
9BF4 PHA ; Push X
9BF5 LDY #2 ; Y=2: TXCB offset for dest station
9BF7 LDA (nmi_tx_block),y ; Load dest station from TX control block
9BF9 STA tx_dst_stn ; Store to TX scout buffer
9BFC INY ; Y=&03
9BFD LDA (nmi_tx_block),y ; Load dest network from TX control block
9BFF STA tx_dst_net ; Store to TX scout buffer
9C02 LDY #0 ; Y=0: first byte of TX control block
9C04 LDA (nmi_tx_block),y ; Load control/flag byte
9C06 BMI tx_imm_op_setup ; Bit7 set: immediate operation ctrl byte
9C08 JMP tx_active_start ; Bit7 clear: normal data transfer
9C0B .tx_imm_op_setup←1← 9C06 BMI
STA tx_ctrl_byte ; Store control byte to TX scout buffer
9C0E TAX ; X = control byte for range checks
9C0F INY ; Y=1: port byte offset
9C10 LDA (nmi_tx_block),y ; Load port byte from TX control block
9C12 STA tx_port ; Store port byte to TX scout buffer
9C15 BNE tx_line_idle_check ; Port != 0: skip immediate op setup
9C17 CPX #&83 ; Ctrl < &83: PEEK/POKE need address calc
9C19 BCS check_imm_range ; Ctrl >= &83: skip to range check
9C1B SEC ; SEC: init borrow for 4-byte subtract
9C1C PHP ; Save carry on stack for loop
9C1D LDY #8 ; Y=8: high pointer offset in TXCB
9C1F .calc_peek_poke_size←1← 9C33 BCC
LDA (nmi_tx_block),y ; Load TXCB[Y] (end addr byte)
9C21 DEY ; Y -= 4: back to start addr offset
9C22 DEY ; (continued)
9C23 DEY ; (continued)
9C24 DEY ; (continued)
9C25 PLP ; Restore borrow from stack
9C26 SBC (nmi_tx_block),y ; end - start = transfer size byte
9C28 STA tx_data_start,y ; Store result to tx_data_start
9C2B INY ; Y += 5: advance to next end byte
9C2C INY ; (continued)
9C2D INY ; (continued)
9C2E INY ; (continued)
9C2F INY ; (continued)
9C30 PHP ; Save borrow for next byte
9C31 CPY #&0c ; Done all 4 bytes? (Y reaches &0C)
9C33 BCC calc_peek_poke_size ; No: next byte pair
9C35 PLP ; Discard final borrow
9C36 .check_imm_range←1← 9C19 BCS
CPX #&89 ; Ctrl >= &89: out of immediate range
9C38 BCS tx_active_start ; Above range: normal data transfer
9C3A LDY #&0c ; Y=&0C: start of extra data in TXCB
9C3C .copy_imm_params←1← 9C44 BCC
LDA (nmi_tx_block),y ; Load extra parameter byte from TXCB
9C3E STA nmi_shim_1a,y ; Copy to NMI shim workspace at &0D1A+Y
9C41 INY ; Next byte
9C42 CPY #&10 ; Done 4 bytes? (Y reaches &10)
9C44 BCC copy_imm_params ; No: continue copying
9C46 .tx_line_idle_check←1← 9C15 BNE
LDA #&20 ; A=&20: mask for SR2 INACTIVE bit
9C48 BIT econet_control23_or_status2 ; BIT SR2: test if line is idle
9C4B BNE tx_no_clock_error ; Line not idle: handle as line jammed
9C4D LDA #&fd ; A=&FD: high byte of timeout counter
9C4F PHA ; Push timeout high byte to stack
9C50 LDA #6 ; Scout frame = 6 address+ctrl bytes
9C52 STA tx_length ; Store scout frame length
9C55 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 &9C75-&9C7A works because CR2=&67 has RTS=0, so cts_input_ is always true, and SR1_CTS reflects presence of clock hardware.

9C57 .inactive_poll
STA tx_index ; Save TX index
9C5A PHA ; Push timeout byte 1 on stack
9C5B PHA ; Push timeout byte 2 on stack
9C5C LDY #&e7 ; Y=&E7: CR2 value for TX prep (RTS|CLR_TX_ST|CLR_RX_ST|FC_TDRA|2_1_BYTE| PSE)
9C5E .test_inactive_retry←3← 9C84 BNE← 9C89 BNE← 9C8E BNE
LDA #4 ; A=&04: INACTIVE mask for SR2 bit2
9C60 PHP ; Save interrupt state
9C61 SEI ; Disable interrupts for ADLC access
fall through ↓

Disable NMIs and test INACTIVE

Mid-instruction label within the INACTIVE polling loop. The address &9BE2 is referenced as a constant for self-modifying code. Disables NMIs twice (belt-and-braces) then tests SR2 for INACTIVE before proceeding with TX.

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

9C97 .tx_line_jammed←1← 9C90 JMP
LDA #7 ; CR2=&07: FC_TDRA | 2_1_BYTE | PSE (abort TX)
9C99 STA econet_control23_or_status2 ; Write CR2 to abort TX
9C9C PLA ; Clean 3 bytes of timeout loop state
9C9D PLA ; Pop saved register
9C9E PLA ; Pop saved register
9C9F LDA #&40 ; Error &40 = 'Line Jammed'
9CA1 BNE store_tx_error ; ALWAYS branch to shared error handler ALWAYS branch
9CA3 .tx_no_clock_error←1← 9C4B BNE
LDA #&43 ; Error &43 = 'No Clock'
9CA5 .store_tx_error←2← 9C95 BNE← 9CA1 BNE
LDY #0 ; Offset 0 = error byte in TX control block
9CA7 STA (nmi_tx_block),y ; Store error code in TX CB byte 0
9CA9 LDA #&80 ; &80 = TX complete flag
9CAB STA tx_clear_flag ; Signal TX operation complete
9CAE PLA ; Restore X saved by caller
9CAF TAX ; Move to X register
9CB0 RTS ; Return to TX caller

TX preparation

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

9CB1 .tx_prepare←1← 9C7A BNE
STY econet_control23_or_status2 ; Write CR2 = Y (&E7: RTS|CLR_TX_ST|CLR_RX_ST| FC_TDRA|2_1_BYTE|PSE)
9CB4 LDX #&44 ; CR1=&44: RX_RESET | TIE (TX active, TX interrupts enabled)
9CB6 STX econet_control1_or_status1 ; Write to ADLC CR1
9CB9 LDX #&5b ; Install NMI handler at &9D5B (nmi_tx_data)
9CBB LDY #&9d ; High byte of NMI handler address
9CBD STX nmi_jmp_lo ; Write NMI vector low byte directly
9CC0 STY nmi_jmp_hi ; Write NMI vector high byte directly
9CC3 BIT video_ula_control ; INTON -- NMIs now fire for TDRA (&FE20 read)
9CC6 LDA tx_port ; Load destination port number
9CC9 BNE setup_data_xfer ; Port != 0: standard data transfer
9CCB LDY tx_ctrl_byte ; Port 0: load control byte for table lookup
9CCE LDA tube_tx_byte4_operand,y ; Look up tx_flags from table
9CD1 STA tx_flags ; Store operation flags
9CD4 LDA tube_tx_byte2_operand,y ; Look up tx_length from table
9CD7 STA tx_length ; Store expected transfer length
9CDA LDA sr2_test_operand,y ; Load handler from dispatch table
9CDD PHA ; Push high byte for PHA/PHA/RTS dispatch
9CDE LDA intoff_test_inactive,y ; Look up handler address low from table
9CE1 PHA ; Push low byte for PHA/PHA/RTS dispatch
9CE2 RTS ; RTS dispatches to control-byte handler
9CE3 EQUB <(tx_ctrl_peek-1)
9CE4 EQUB <(tx_ctrl_poke-1)
9CE5 EQUB <(tx_ctrl_proc-1)
9CE6 EQUB <(tx_ctrl_proc-1)
9CE7 EQUB <(tx_ctrl_proc-1)
9CE8 EQUB <(tx_ctrl_exit-1)
9CE9 EQUB <(tx_ctrl_exit-1)
9CEA EQUB <(imm_op_status3-1)
9CEB EQUB >(tx_ctrl_peek-1)
9CEC EQUB >(tx_ctrl_poke-1)
9CED EQUB >(tx_ctrl_proc-1)
9CEE EQUB >(tx_ctrl_proc-1)
9CEF EQUB >(tx_ctrl_proc-1)
9CF0 EQUB >(tx_ctrl_exit-1)
9CF1 EQUB >(tx_ctrl_exit-1)
9CF2 EQUB >(imm_op_status3-1)
9CF3 .imm_op_status3
LDA #3 ; A=3: scout_status for PEEK
9CF5 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 at &0D1E-&0D21 (with carry propagation). Calls tx_calc_transfer to finalise, then exits via tx_ctrl_exit.

9CF7 .tx_ctrl_peek
LDA #3 ; A=3: scout_status for PEEK op
9CF9 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.

9CFB .tx_ctrl_poke
LDA #2 ; Scout status = 2 (POKE transfer)
9CFD .store_status_add4←1← 9CF9 BNE
STA scout_status ; Store scout status
9D00 CLC ; Clear carry for 4-byte addition
9D01 PHP ; Save carry on stack
9D02 LDY #&0c ; Y=&0C: start at offset 12
9D04 .add_bytes_loop←1← 9D11 BCC
LDA l0d1e,y ; Load workspace address byte
9D07 PLP ; Restore carry from previous byte
9D08 ADC (nmi_tx_block),y ; Add TXCB address byte
9D0A STA l0d1e,y ; Store updated address byte
9D0D INY ; Next byte
9D0E 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.

9D0F .tx_ctrl_add_done
CPY #&10 ; Compare Y with 16-byte boundary
9D11 BCC add_bytes_loop ; Below boundary: continue addition
9D13 PLP ; Restore processor flags
9D14 JSR tx_calc_transfer ; Calculate transfer byte count
9D17 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.

9D1A .tx_ctrl_proc
LDA #2 ; A=2: scout_status for procedure ops
9D1C .store_status_calc_xfer←1← 9CF5 BNE
STA scout_status ; Store scout status
9D1F JSR tx_calc_transfer ; Calculate transfer parameters
9D22 JMP tx_ctrl_exit ; Exit TX ctrl setup
9D25 .setup_data_xfer←1← 9CC9 BNE
LDA tx_dst_stn ; Load dest station for broadcast check
9D28 AND tx_dst_net ; AND with dest network
9D2B CMP #&ff ; Both &FF = broadcast address?
9D2D BNE setup_unicast_xfer ; Not broadcast: unicast path
9D2F LDA #&0e ; Broadcast scout: 14 bytes total
9D31 STA tx_length ; Store broadcast scout length
9D34 LDA #&40 ; A=&40: broadcast flag
9D36 STA tx_flags ; Set broadcast flag in tx_flags
9D39 LDY #4 ; Y=4: start of address data in TXCB
9D3B .copy_bcast_addr←1← 9D43 BCC
LDA (nmi_tx_block),y ; Copy TXCB address bytes to scout buffer
9D3D STA tx_src_stn,y ; Store to TX source/data area
9D40 INY ; Next byte
9D41 CPY #&0c ; Done 8 bytes? (Y reaches &0C)
9D43 BCC copy_bcast_addr ; No: continue copying
9D45 BCS tx_ctrl_exit ; ALWAYS branch
9D47 .setup_unicast_xfer←1← 9D2D BNE
LDA #0 ; A=0: clear flags for unicast
9D49 STA tx_flags ; Clear tx_flags
9D4C .proc_op_status2
LDA #2 ; scout_status=2: data transfer pending
9D4E .store_status_copy_ptr
STA scout_status ; Store scout status
9D51 JSR tx_calc_transfer ; Calculate transfer size from RXCB
9D54 .tx_ctrl_exit←3← 9D17 JMP← 9D22 JMP← 9D45 BCS
PLP ; Restore processor status from stack
9D55 PLA ; Restore stacked registers (4 PLAs)
9D56 PLA ; Second PLA
9D57 PLA ; Third PLA
9D58 PLA ; Fourth PLA
9D59 TAX ; Restore X from A
9D5A 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.

9D5B .nmi_tx_data
LDY tx_index ; Load TX buffer index
9D5E BIT econet_control1_or_status1 ; BIT SR1: V=bit6(TDRA), N=bit7(IRQ)
9D61 .tx_fifo_write←1← 9D7C BMI
BVC tx_fifo_not_ready ; TDRA not set -- TX error
9D63 LDA tx_dst_stn,y ; Load byte from TX buffer
9D66 STA econet_data_continue_frame ; Write to TX_DATA (continue frame)
9D69 INY ; Next TX buffer byte
9D6A LDA tx_dst_stn,y ; Load second byte from TX buffer
9D6D INY ; Advance TX index past second byte
9D6E STY tx_index ; Save updated TX buffer index
9D71 STA econet_data_continue_frame ; Write second byte to TX_DATA
9D74 CPY tx_length ; Compare index to TX length
9D77 BCS tx_last_data ; Frame complete -- go to TX_LAST_DATA
9D79 BIT econet_control1_or_status1 ; Check if we can send another pair
9D7C BMI tx_fifo_write ; IRQ set -- send 2 more bytes (tight loop)
9D7E JMP nmi_rti ; RTI -- wait for next NMI
; TX error path
9D81 .tx_error←1← 9DC6 BEQ
LDA #&42 ; Error &42
9D83 BNE tx_store_error ; ALWAYS branch
9D85 .tx_fifo_not_ready←1← 9D61 BVC
LDA #&67 ; CR2=&67: clear status, return to listen
9D87 STA econet_control23_or_status2 ; Write CR2: clear status, idle listen
9D8A LDA #&41 ; Error &41 (TDRA not ready)
9D8C .tx_store_error←1← 9D83 BNE
LDY station_id_disable_net_nmis ; INTOFF (also loads station ID)
9D8F .delay_nmi_disable←1← 9D92 BNE
PHA ; PHA/PLA delay loop (256 iterations for NMI disable)
9D90 PLA ; PHA/PLA delay (~7 cycles each)
9D91 INY ; Increment delay counter
9D92 BNE delay_nmi_disable ; Loop 256 times for NMI disable
9D94 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 &9DA3 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)

9D97 .tx_last_data←1← 9D77 BCS
LDA #&3f ; CR2=&3F: TX_LAST_DATA | CLR_RX_ST | FLAG_IDLE | FC_TDRA | 2_1_BYTE | PSE
9D99 STA econet_control23_or_status2 ; Write to ADLC CR2
9D9C LDA #&a3 ; Install NMI handler at &9DA3 (nmi_tx_complete)
9D9E LDY #&9d ; High byte of handler address
9DA0 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 &9F48 - bit0 set at &0D4A -> four-way handshake data phase at &9EEC - Otherwise -> install RX reply handler at &9DC1

9DA3 .nmi_tx_complete
LDA #&82 ; CR1=&82: TX_RESET | RIE (now in RX mode)
9DA5 STA econet_control1_or_status1 ; Write CR1 to switch from TX to RX
9DA8 BIT tx_flags ; Test workspace flags
9DAB BVC check_handshake_bit ; bit6 not set -- check bit0
9DAD JMP tx_result_ok ; bit6 set -- TX completion
9DB0 .check_handshake_bit←1← 9DAB BVC
LDA #1 ; A=1: mask for bit0 test
9DB2 BIT tx_flags ; Test tx_flags bit0 (handshake)
9DB5 BEQ install_reply_scout ; bit0 clear: install reply handler
9DB7 JMP handshake_await_ack ; bit0 set -- four-way handshake data phase
9DBA .install_reply_scout←1← 9DB5 BEQ
LDA #&c1 ; Install nmi_reply_scout at &9DC1
9DBC LDY #&9d ; High byte of nmi_reply_scout addr
9DBE 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).

9DC1 .nmi_reply_scout
LDA #1 ; A=&01: AP mask for SR2
9DC3 BIT econet_control23_or_status2 ; BIT SR2: test AP (Address Present)
9DC6 BEQ tx_error ; No AP -- error
9DC8 LDA econet_data_continue_frame ; Read first RX byte (destination station)
9DCB CMP station_id_disable_net_nmis ; Compare to our station ID (INTOFF side effect)
9DCE BNE reply_error ; Not our station -- error/reject
9DD0 LDA #&d7 ; Install nmi_reply_cont at &9DD7
9DD2 LDY #&9d ; High byte of nmi_reply_cont
9DD4 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 &9DF2 for the remaining two bytes (source station and network). Optimisation: checks SR1 bit7 (IRQ still asserted) via BMI at &9DE8. If IRQ is still set, falls through directly to &9DF2 without an RTI, avoiding NMI re-entry overhead for short frames where all bytes arrive in quick succession.

9DD7 .nmi_reply_cont
BIT econet_control23_or_status2 ; BIT SR2: test for RDA (bit7 = data available)
9DDA BPL reply_error ; No RDA -- error
9DDC LDA econet_data_continue_frame ; Read destination network byte
9DDF BNE reply_error ; Non-zero -- network mismatch, error
9DE1 LDA #&f2 ; Install nmi_reply_validate at &9D5B
9DE3 LDY #&9d ; High byte of nmi_reply_validate
9DE5 BIT econet_control1_or_status1 ; BIT SR1: test IRQ (N=bit7) -- more data ready?
9DE8 BMI nmi_reply_validate ; IRQ set -- fall through to &9D5B without RTI
9DEA JMP set_nmi_vector ; IRQ not set -- install handler and RTI
9DED .reply_error←7← 9DCE BNE← 9DDA BPL← 9DDF BNE← 9DF5 BPL← 9DFD BNE← 9E05 BNE← 9E0C BEQ
LDA #&41 ; A=&41: 'not listening' error code
9DEF .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 &9DF2 -- must see data available 2. Read source station at &9DF7, compare to &0D20 (tx_dst_stn) 3. Read source network at &9DFF, compare to &0D21 (tx_dst_net) 4. Check SR2 bit1 (FV) at &9E09 -- 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).

9DF2 .nmi_reply_validate←1← 9DE8 BMI
BIT econet_control23_or_status2 ; BIT SR2: test RDA (bit7). Must be set for valid reply.
9DF5 BPL reply_error ; No RDA -- error (FV masking RDA via PSE would cause this)
9DF7 LDA econet_data_continue_frame ; Read source station
9DFA CMP tx_dst_stn ; Compare to original TX destination station (&0D20)
9DFD BNE reply_error ; Mismatch -- not the expected reply, error
9DFF LDA econet_data_continue_frame ; Read source network
9E02 CMP tx_dst_net ; Compare to original TX destination network (&0D21)
9E05 BNE reply_error ; Mismatch -- error
9E07 LDA #2 ; A=&02: FV mask for SR2 bit1
9E09 BIT econet_control23_or_status2 ; BIT SR2: test FV -- frame must be complete
9E0C BEQ reply_error ; No FV -- incomplete frame, error
9E0E LDA #&a7 ; CR2=&A7: RTS|CLR_TX_ST|FC_TDRA|2_ 1_BYTE|PSE (TX in handshake)
9E10 STA econet_control23_or_status2 ; Write CR2: enable RTS for TX handshake
9E13 LDA #&44 ; CR1=&44: RX_RESET | TIE (TX active for scout ACK)
9E15 STA econet_control1_or_status1 ; Write CR1: reset RX, enable TX interrupt
9E18 LDA #&ec ; Install next handler at &9EEC into &0D4B/&0D4C
9E1A LDY #&9e ; High byte &9E of next handler address
9E1C STA nmi_next_lo ; Store low byte to nmi_next_lo
9E1F STY nmi_next_hi ; Store high byte to nmi_next_hi
9E22 LDA tx_dst_stn ; Load dest station for scout ACK TX
9E25 BIT econet_control1_or_status1 ; BIT SR1: test TDRA (V=bit6)
9E28 BVC data_tx_error ; TDRA not ready -- error
9E2A STA econet_data_continue_frame ; Write dest station to TX FIFO
9E2D LDA tx_dst_net ; Load dest network for scout ACK TX
9E30 STA econet_data_continue_frame ; Write dest network to TX FIFO
9E33 LDA #&3a ; Install nmi_scout_ack_src at &9DA3
9E35 LDY #&9e ; High byte &9D of handler address
9E37 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.

9E3A .nmi_scout_ack_src
LDA station_id_disable_net_nmis ; Read our station ID (also INTOFF)
9E3D BIT econet_control1_or_status1 ; BIT SR1: check TDRA before writing
9E40 BVC data_tx_error ; TDRA not ready: TX error
9E42 STA econet_data_continue_frame ; Write our station to TX FIFO
9E45 LDA #0 ; Network = 0 (local network)
9E47 STA econet_data_continue_frame ; Write network byte to TX FIFO
9E4A .data_tx_begin←1← 99BF JMP
LDA #2 ; Test bit 1 of tx_flags
9E4C BIT tx_flags ; Check if immediate-op or data-transfer
9E4F BNE install_imm_data_nmi ; Bit 1 set: immediate op, use alt handler
9E51 LDA #&5f ; Install nmi_data_tx at &9E5F
9E53 LDY #&9e ; High byte of handler address
9E55 JMP set_nmi_vector ; Install and return via set_nmi_vector
9E58 .install_imm_data_nmi←1← 9E4F BNE
LDA #&b3 ; Install nmi_data_tx_tube at &9EB3
9E5A LDY #&9e ; High byte of handler address
9E5C 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 &9D5B but reads from the port buffer instead of the TX workspace. Writes two bytes per iteration, checking SR1 IRQ between pairs for tight looping.

9E5F .nmi_data_tx
LDY port_buf_len ; Y = buffer offset, resume from last position
9E61 BIT econet_control1_or_status1 ; BIT SR1: test TDRA (V=bit6)
9E64 .data_tx_check_fifo←1← 9E87 BMI
BVC data_tx_error ; TDRA not ready -- error
9E66 LDA (open_port_buf),y ; Write data byte to TX FIFO
9E68 STA econet_data_continue_frame ; Write first byte of pair to FIFO
9E6B INY ; Advance buffer offset
9E6C BNE write_second_tx_byte ; No page crossing
9E6E DEC port_buf_len_hi ; Page crossing: decrement page count
9E70 BEQ data_tx_last ; No pages left: send last data
9E72 INC open_port_buf_hi ; Increment buffer high byte
9E74 .write_second_tx_byte←1← 9E6C BNE
LDA (open_port_buf),y ; Load second byte of pair
9E76 STA econet_data_continue_frame ; Write second byte to FIFO
9E79 INY ; Advance buffer offset
9E7A STY port_buf_len ; Save updated buffer position
9E7C BNE check_irq_loop ; No page crossing
9E7E DEC port_buf_len_hi ; Page crossing: decrement page count
9E80 BEQ data_tx_last ; No pages left: send last data
9E82 INC open_port_buf_hi ; Increment buffer high byte
9E84 .check_irq_loop←1← 9E7C BNE
BIT econet_control1_or_status1 ; BIT SR1: test IRQ (N=bit7) for tight loop
9E87 BMI data_tx_check_fifo ; IRQ still set: write 2 more bytes
9E89 JMP nmi_rti ; No IRQ: return, wait for next NMI
9E8C .data_tx_last←4← 9E70 BEQ← 9E80 BEQ← 9ECC BEQ← 9EE2 BEQ
LDA #&3f ; CR2=&3F: TX_LAST_DATA (close data frame)
9E8E STA econet_control23_or_status2 ; Write CR2 to close frame
9E91 LDA tx_flags ; Check tx_flags for next action
9E94 BPL install_saved_handler ; Bit7 clear: error, install saved handler
9E96 LDA #&4a ; Install discard_reset_listen at &9A4A
9E98 LDY #&9a ; High byte of &99DB handler
9E9A JMP set_nmi_vector ; Set NMI vector and return
9E9D .data_tx_error←4← 9E28 BVC← 9E40 BVC← 9E64 BVC← 9EB6 BVC
LDA tx_flags ; Load saved next handler low byte
9EA0 BPL nmi_tx_not_listening ; bit7 clear: error path
9EA2 JMP discard_reset_listen ; ADLC reset and return to idle
9EA5 .nmi_tx_not_listening←1← 9EA0 BPL
LDA #&41 ; A=&41: 'not listening' error
9EA7 .jmp_tx_result_fail
JMP tx_store_result ; Store result and return to idle
9EAA .install_saved_handler←1← 9E94 BPL
LDA nmi_next_lo ; Load saved handler low byte
9EAD LDY nmi_next_hi ; Load saved next handler high byte
9EB0 JMP set_nmi_vector ; Install saved handler and return
9EB3 .nmi_data_tx_tube
BIT econet_control1_or_status1 ; Tube TX: BIT SR1 test TDRA
9EB6 .tube_tx_fifo_write←1← 9EE7 BMI
BVC data_tx_error ; TDRA not ready -- error
9EB8 LDA tube_data_register_3 ; Read byte from Tube R3
9EBB STA econet_data_continue_frame ; Write to TX FIFO
9EBE INC port_buf_len ; Increment 4-byte buffer counter
9EC0 BNE write_second_tube_byte ; Low byte didn't wrap
9EC2 INC port_buf_len_hi ; Carry into second byte
9EC4 BNE write_second_tube_byte ; No further carry
9EC6 INC open_port_buf ; Carry into third byte
9EC8 BNE write_second_tube_byte ; No further carry
9ECA INC open_port_buf_hi ; Carry into fourth byte
9ECC BEQ data_tx_last ; Counter wrapped to zero: last data
9ECE .write_second_tube_byte←3← 9EC0 BNE← 9EC4 BNE← 9EC8 BNE
LDA tube_data_register_3 ; Read second Tube byte from R3
9ED1 STA econet_data_continue_frame ; Write second byte to TX FIFO
9ED4 INC port_buf_len ; Increment 4-byte counter (second byte)
9ED6 BNE check_tube_irq_loop ; Low byte didn't wrap
9ED8 .tube_tx_inc_byte2
INC port_buf_len_hi ; Carry into second byte
9EDA BNE check_tube_irq_loop ; No further carry
9EDC .tube_tx_inc_byte3
INC open_port_buf ; Carry into third byte
9EDE BNE check_tube_irq_loop ; No further carry
9EE0 .tube_tx_inc_byte4
INC open_port_buf_hi ; Carry into fourth byte
9EE2 BEQ data_tx_last ; Counter wrapped to zero: last data
9EE4 .check_tube_irq_loop←3← 9ED6 BNE← 9EDA BNE← 9EDE BNE
BIT econet_control1_or_status1 ; BIT SR1: test IRQ for tight loop
9EE7 BMI tube_tx_fifo_write ; IRQ still set: write 2 more bytes
9EE9 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 &9EF8 to receive the final ACK from the remote station.

9EEC .handshake_await_ack←1← 9DB7 JMP
LDA #&82 ; CR1=&82: TX_RESET | RIE (switch to RX for final ACK)
9EEE STA econet_control1_or_status1 ; Write to ADLC CR1
9EF1 LDA #&f8 ; Install nmi_final_ack at &9E5C
9EF3 LDY #&9e ; High byte of handler address
9EF5 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 (&9DC1-&9DF2): &9EF8: Check AP, read dest_stn, compare to our station &9F0E: Check RDA, read dest_net, validate = 0 &9F24: Check RDA, read src_stn/net, compare to TX dest &9F41: Check FV for frame completion On success, stores result=0 at &9F48. On any failure, error &41.

9EF8 .nmi_final_ack
LDA #1 ; A=&01: AP mask
9EFA BIT econet_control23_or_status2 ; BIT SR2: test AP
9EFD BEQ tx_result_fail ; No AP -- error
9EFF LDA econet_data_continue_frame ; Read dest station
9F02 CMP station_id_disable_net_nmis ; Compare to our station (INTOFF side effect)
9F05 BNE tx_result_fail ; Not our station -- error
9F07 LDA #&0e ; Install nmi_final_ack_net at &9E70
9F09 LDY #&9f ; High byte of handler address
9F0B JMP set_nmi_vector ; Install continuation handler
9F0E .nmi_final_ack_net
BIT econet_control23_or_status2 ; BIT SR2: test RDA
9F11 BPL tx_result_fail ; No RDA -- error
9F13 LDA econet_data_continue_frame ; Read dest network
9F16 BNE tx_result_fail ; Non-zero -- network mismatch, error
9F18 LDA #&24 ; Install nmi_final_ack_validate at &9E84
9F1A LDY #&9f ; High byte of handler address
9F1C BIT econet_control1_or_status1 ; BIT SR1: test IRQ -- more data ready?
9F1F BMI nmi_final_ack_validate ; IRQ set -- fall through to &9E84 without RTI
9F21 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.

9F24 .nmi_final_ack_validate←1← 9F1F BMI
BIT econet_control23_or_status2 ; BIT SR2: test RDA
9F27 BPL tx_result_fail ; No RDA -- error
9F29 LDA econet_data_continue_frame ; Read source station
9F2C CMP tx_dst_stn ; Compare to TX dest station (&0D20)
9F2F BNE tx_result_fail ; Mismatch -- error
9F31 LDA econet_data_continue_frame ; Read source network
9F34 CMP tx_dst_net ; Compare to TX dest network (&0D21)
9F37 BNE tx_result_fail ; Mismatch -- error
9F39 LDA tx_flags ; Load TX flags for next action
9F3C BPL check_fv_final_ack ; bit7 clear: no data phase
9F3E JMP install_data_rx_handler ; Install data RX handler
9F41 .check_fv_final_ack←1← 9F3C BPL
LDA #2 ; A=&02: FV mask for SR2 bit1
9F43 BIT econet_control23_or_status2 ; BIT SR2: test FV -- frame must be complete
9F46 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 &9A4A.

9F48 .tx_result_ok←2← 996D JMP← 9DAD JMP
LDA #0 ; A=0: success result code
9F4A 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.

9F4C .tx_result_fail←8← 9EFD BEQ← 9F05 BNE← 9F11 BPL← 9F16 BNE← 9F27 BPL← 9F2F BNE← 9F37 BNE← 9F46 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 &9A4A. Error codes: &00=success, &40=line jammed, &41=not listening, &42=net error.

9F4E .tx_store_result←5← 989B JMP← 9D94 JMP← 9DEF JMP← 9EA7 JMP← 9F4A BEQ
LDY #0 ; Y=0: index into TX control block
9F50 STA (nmi_tx_block),y ; Store result/error code at (nmi_tx_block),0
9F52 LDA #&80 ; &80: completion flag for &0D3A
9F54 STA tx_clear_flag ; Signal TX complete
9F57 JMP discard_reset_listen ; Full ADLC reset and return to idle listen
; Unreferenced data block (purpose unknown)
9F5A 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.

9F6A .tx_calc_transfer←5← 9818 JSR← 9B04 JSR← 9D14 JSR← 9D1F JSR← 9D51 JSR
LDY #6 ; Load RXCB[6] (buffer addr byte 2)
9F6C LDA (nmi_tx_block),y ; Load RXCB[6] (buffer addr byte 2)
9F6E INY ; Y=&07
9F6F AND (nmi_tx_block),y ; AND with TX block[7] (byte 3)
9F71 CMP #&ff ; Both &FF = no buffer?
9F73 BEQ fallback_calc_transfer ; Yes: fallback path
9F75 LDA tx_in_progress ; Tube transfer in progress?
9F78 BEQ fallback_calc_transfer ; No: fallback path
9F7A LDA tx_flags ; Load TX flags for transfer setup
9F7D ORA #2 ; Set bit 1 (transfer complete)
9F7F STA tx_flags ; Store with bit 1 set (Tube xfer)
9F82 SEC ; Init borrow for 4-byte subtract
9F83 PHP ; Save carry on stack
9F84 LDY #4 ; Y=4: start at RXCB offset 4
9F86 .calc_transfer_size←1← 9F98 BCC
LDA (nmi_tx_block),y ; Load RXCB[Y] (current ptr byte)
9F88 INY ; Y += 4: advance to RXCB[Y+4]
9F89 INY ; (continued)
9F8A INY ; (continued)
9F8B INY ; (continued)
9F8C PLP ; Restore borrow from previous byte
9F8D SBC (nmi_tx_block),y ; Subtract RXCB[Y+4] (start ptr byte)
9F8F STA net_tx_ptr,y ; Store result byte
9F92 DEY ; Y -= 3: next source byte
9F93 DEY ; (continued)
9F94 DEY ; (continued)
9F95 PHP ; Save borrow for next byte
9F96 CPY #8 ; Done all 4 bytes?
9F98 BCC calc_transfer_size ; No: next byte pair
9F9A PLP ; Discard final borrow
9F9B TXA ; A = saved X
9F9C PHA ; Save X
9F9D LDA #4 ; Compute address of RXCB+4
9F9F CLC ; CLC for base pointer addition
9FA0 ADC nmi_tx_block ; Add RXCB base to get RXCB+4 addr
9FA2 TAX ; X = low byte of RXCB+4
9FA3 LDY nmi_tx_block_hi ; Y = high byte of RXCB ptr
9FA5 LDA #&c2 ; Tube claim type &C2
9FA7 JSR tube_addr_claim ; Claim Tube transfer address
9FAA BCC restore_x_and_return ; No Tube: skip reclaim
9FAC LDA scout_status ; Tube: reclaim with scout status
9FAF JSR tube_addr_claim ; Reclaim with scout status type
9FB2 SEC ; C=1: Tube address claimed
9FB3 .restore_x_and_return←1← 9FAA BCC
PLA ; Restore X
9FB4 TAX ; Restore X from stack
9FB5 RTS ; Return with C = transfer status
9FB6 .fallback_calc_transfer←2← 9F73 BEQ← 9F78 BEQ
LDY #4 ; Y=4: RXCB current pointer offset
9FB8 LDA (nmi_tx_block),y ; Load RXCB[4] (current ptr lo)
9FBA LDY #8 ; Y=8: RXCB start address offset
9FBC SEC ; Set carry for subtraction
9FBD SBC (nmi_tx_block),y ; Subtract RXCB[8] (start ptr lo)
9FBF STA port_buf_len ; Store transfer size lo
9FC1 LDY #5 ; Y=5: current ptr hi offset
9FC3 LDA (nmi_tx_block),y ; Load RXCB[5] (current ptr hi)
9FC5 SBC #0 ; Propagate borrow from lo subtraction
9FC7 STA open_port_buf_hi ; Temp store adjusted current ptr hi
9FC9 LDY #8 ; Y=8: start address lo offset
9FCB LDA (nmi_tx_block),y ; Copy RXCB[8] to open port buffer lo
9FCD STA open_port_buf ; Store to scratch (side effect)
9FCF LDY #9 ; Y=9: start address hi offset
9FD1 LDA (nmi_tx_block),y ; Load RXCB[9] (start ptr hi)
9FD3 SEC ; Set carry for subtraction
9FD4 SBC open_port_buf_hi ; start_hi - adjusted current_hi
9FD6 STA port_buf_len_hi ; Store transfer size hi
9FD8 SEC ; Return with C=1
9FD9 .nmi_shim_rom_src←1← 968C 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 (&9700). 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 &9700.

9FDA .nmi_bootstrap_entry
BIT station_id_disable_net_nmis ; INTOFF: disable NMIs while switching ROM
9FDD PHA ; Save A
9FDE TYA ; Transfer Y to A
9FDF PHA ; Save Y (via A)
9FE0 LDA #0 ; ROM bank 0 (patched during init for actual bank)
9FE2 STA romsel ; Select Econet ROM bank via ROMSEL
9FE5 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.

9FE8 .rom_set_nmi_vector
STY nmi_jmp_hi ; Store handler high byte at &0D0D
9FEB STA nmi_jmp_lo ; Store handler low byte at &0D0C
9FEE LDA romsel_copy ; Restore NFS ROM bank
9FF0 STA romsel ; Page in via hardware latch
9FF3 PLA ; Restore Y from stack
9FF4 TAY ; Transfer ROM bank to Y
9FF5 PLA ; Restore A from stack
9FF6 BIT video_ula_control ; INTON: re-enable NMIs
9FF9 RTI ; Return from interrupt
9FFA EQUB &AD, &4A, &0D, &09, &02, &8D