Acorn NFS 3.35D

Updated 31 Mar 2026

← All Acorn NFS and Acorn ANFS versions

; Sideways ROM header
; NFS ROM 3.35D 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← 81DE CMP← 81E7 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← 84DD 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 (&8E43): read file handle from received packet (net_1_read_handle)

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

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

*NET4 (&818A): 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 &8BE2, 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 ; Function code >= 8? Return (unsupported)
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← 816A 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←7← 806D BMI← 8071 BCS← 80BE BEQ← 80CC BCS← 80D6 BCS← 80F2 BCS← 80FC BEQ
RTS ; RTS pops address, adds 1, jumps to handler

Service handler entry

Checks per-ROM disable flag at &0DF0+X (new in 3.35D). If bit 7 is set, returns immediately; service calls &FE/&FF bypass this check. Intercepts three service calls: &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 c8150.

80EA .service_handler←1← 8003 JMP
.service_handler_entry←1← 8003 JMP
PHA ; Save A (service number) on stack
80EB LDA rom_ws_table,x ; Load per-ROM workspace flag
80EE ASL ; Test bit 7 (ROM disabled flag)
80EF PLA ; Restore A (service number)
80F0 BMI check_svc_high ; Service >= &80: always handle
80F2 BCS return_1 ; C=1 (disabled): skip this ROM
fall through ↓

Service handler: high service dispatch

Handles service calls >= &80. Compares A against &FE: &FF: Full init — jump to init_vectors_and_copy &FE: Tube init — explode character definitions (if Y != 0) < &FE: Fall through to check for service &12 and dispatch.

80F4 .check_svc_high←1← 80F0 BMI
CMP #&fe ; Service >= &FE?
80F6 BCC check_svc_12 ; Service < &FE: skip to &12/dispatch check
80F8 BNE init_vectors_and_copy ; Service &FF: full init (vectors + RAM copy)
80FA CPY #0 ; Service &FE: Y=0?
80FC BEQ return_1 ; Y=0: no Tube data, skip to &12 check
80FE STX zp_temp_11 ; Save ROM number across OSBYTE
8100 STY zp_temp_10 ; Save Tube address across OSBYTE
8102 LDX #6 ; X=6 extra pages for char definitions
8104 LDA #osbyte_explode_chars ; OSBYTE &14: explode character RAM
8106 JSR osbyte ; Explode character definition RAM (six extra pages), can redefine all characters 32-255 (X=6)
8109 LDX zp_temp_11 ; Restore ROM number
810B 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 &8177 (low byte, high byte, vector offset) and stores each 16-bit value at &0200+offset: EVNTV (&0220) = &06E5 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 / reloc_p5_src / reloc_p6_src) 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.

810D .init_vectors_and_copy←1← 80F8 BNE
STY zp_temp_10 ; Save Y (ROM number) for later
810F LDY #&0c ; Y=12: 4 triplets x 3 bytes each
8111 .init_vector_loop←1← 8123 BNE
LDX return_2,y ; Load vector offset from table
8114 DEY ; Previous table byte
8115 LDA return_2,y ; Load vector high byte from table
8118 STA userv+1,x ; Store high byte at &0201+X
811B DEY ; Previous table byte
811C LDA return_2,y ; Load vector low byte from table
811F STA userv,x ; Store low byte at &0200+X
8122 DEY ; Previous table byte
8123 BNE init_vector_loop ; Loop for all 4 vector pairs
8125 .init_tube_and_workspace
LDA #&8e ; A=&8E: Tube control register init value
8127 STA tube_status_1_and_tube_control ; Write to Tube control register
; Copy NMI handler code from ROM to RAM pages &04-&06
812A .cloop←1← 813D BNE
LDA reloc_p4_src,y ; Load ROM byte from page &93
812D STA tube_code_page4,y ; Store to page &04 (Tube code)
8130 LDA reloc_p5_src,y ; Load ROM byte from page &94
8133 STA tube_dispatch_table,y ; Store to page &05 (dispatch table)
8136 LDA reloc_p6_src,y ; Load ROM byte from page &95
8139 STA tube_code_page6,y ; Store to page &06
813C DEY ; DEY wraps 0 -> &FF on first iteration
813D BNE cloop ; Loop until 256 bytes copied per page
813F JSR tube_post_init ; Run post-init routine in copied code
8142 LDX #&60 ; X=&60: copy 97 bytes (&60..&00)
; Copy NMI workspace initialiser from ROM to &0016-&0076
8144 .copy_nmi_workspace←1← 814A BPL
LDA reloc_zp_src,x ; Load NMI workspace init byte from ROM
8147 STA nmi_workspace_start,x ; Store to zero page &16+X
8149 DEX ; Next byte
814A BPL copy_nmi_workspace ; Loop until all workspace bytes copied
814C .restore_y_check_svc←1← 810B BNE
LDY zp_temp_10 ; Restore Y (ROM number)
814E .tube_chars_done
LDA #0 ; A=0: fall through to service &12 check
8150 .check_svc_12←1← 80F6 BCC
CMP #&12 ; Is this service &12 (select FS)?
8152 BNE not_svc_12_nfs ; No: check if service < &0D
8154 CPY #5 ; Service &12: Y=5 (NFS)?
8156 BEQ select_nfs ; Y=5: select NFS
fall through ↓

Service call dispatcher

Dispatches MOS service calls 0-12 via the shared dispatch table. Uses base offset Y=0, so table index = service number + 1. Service numbers >= 13 are ignored (branch to return).

8158 .not_svc_12_nfs←1← 8152 BNE
.dispatch_service←1← 8152 BNE
CMP #&0d ; Service >= &0D?
815A .svc_unhandled_return
BCS return_2 ; Service >= &0D: not handled, return
815C .do_svc_dispatch
TAX ; X = service number (dispatch index)
815D LDA rom_svc_num ; Save &A9 (current service state)
815F PHA ; Push saved &A9
8160 LDA nfs_temp ; Save &A8 (workspace page number)
8162 PHA ; Push saved &A8
8163 STX rom_svc_num ; Store service number to &A9
8165 STY nfs_temp ; Store Y (page number) to &A8
8167 TYA ; A = Y for dispatch table offset
8168 LDY #0 ; Y=0: base offset for service dispatch
816A JSR dispatch ; JSR to dispatcher (returns here after handler completes)
816D LDX rom_svc_num ; Recover service claim status from &A9
816F PLA ; Restore saved &A8 from stack
8170 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.

8172 .svc_dispatch_epilogue
PLA ; Restore saved A from service dispatch
8173 STA rom_svc_num ; Save to workspace &A9
8175 TXA ; Return ROM number in A
8176 .return_2←4← 8111 LDX← 8115 LDA← 811C LDA← 815A BCS
RTS ; Return (not our command)
8177 EQUB &1C, &05, &0E, &E7, &04, &10, &16, &00, &02, &E5, &06, &20
8183 .svc_4_star_command
LDX #8 ; X=8: compare 8 chars for *ROFF
8185 JSR match_rom_string ; Try matching *ROFF command
8188 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.

818A .net_4_resume_remote
LDY #4 ; Y=4: offset of keyboard disable flag
818C LDA (net_rx_ptr),y ; Read flag from RX buffer
818E BEQ skip_kbd_reenable ; Zero: keyboard not disabled, skip
8190 LDA #0 ; A=0: value to clear flag and re-enable
8192 TAX ; X=&00
8193 STA (net_rx_ptr),y ; Clear keyboard disable flag in buffer
8195 TAY ; Y=&00
8196 LDA #osbyte_read_write_econet_keyboard_disable ; OSBYTE &C9: Econet keyboard disable
8198 JSR osbyte ; Re-enable keyboard (X=0, Y=0) Enable keyboard (for Econet)
819B LDA #&0a ; Function &0A: remote operation complete
819D JSR setup_tx_and_send ; Send notification to controlling station
81A0 .clear_osbyte_ce_cf←1← 8496 JSR
STX nfs_workspace ; Save X (return value from TX)
81A2 LDA #&ce ; OSBYTE &CE: first system mask to reset
81A4 .clear_osbyte_masks←1← 81AF BEQ
LDX nfs_workspace ; Restore X for OSBYTE call
81A6 LDY #&7f ; Y=&7F: AND mask (clear bit 7)
81A8 JSR osbyte ; Reset system mask byte
81AB ADC #1 ; Advance to next OSBYTE (&CE -> &CF)
81AD CMP #&d0 ; Reached &D0? (past &CF)
81AF BEQ clear_osbyte_masks ; No: reset &CF too
81B1 .skip_kbd_reenable←1← 818E BEQ
LDA #0 ; A=0: clear remote state
81B3 STA rom_svc_num ; Clear &A9 (service dispatch state)
81B5 STA nfs_workspace ; Clear workspace byte
81B7 RTS ; Return
81B8 .match_net_cmd←1← 8188 BNE
LDX #1 ; X=1: ROM offset for "NET" match
81BA JSR match_rom_string ; Try matching *NET command
81BD 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 fs_temp_cd is zero (auto-boot not inhibited), injects the synthetic command "I .BOOT" through the command decoder to trigger auto-boot login.

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

81D6 .match_rom_string←2← 8185 JSR← 81BA JSR
LDY nfs_temp ; Y = saved text pointer offset
81D8 .match_next_char←1← 81E5 BNE
LDA (os_text_ptr),y ; Load next input character
81DA AND #&df ; Force uppercase (clear bit 5)
81DC BEQ cmd_name_matched ; Input char is NUL/space: check ROM byte
81DE CMP binary_version,x ; Compare with ROM string byte
81E1 BNE cmd_name_matched ; Mismatch: check if ROM string ended
81E3 INY ; Advance input pointer
81E4 INX ; Advance ROM string pointer
81E5 BNE match_next_char ; Continue matching (always taken)
81E7 .cmd_name_matched←2← 81DC BEQ← 81E1 BNE
LDA binary_version,x ; Load ROM string byte at match point
81EA BEQ skip_cmd_spaces ; Zero = end of ROM string = full match
81EC RTS ; Non-zero = partial/no match; Z=0
81ED .skpspi←1← 81F2 BEQ
INY ; Skip this space
81EE .skip_cmd_spaces←1← 81EA BEQ
LDA (os_text_ptr),y ; Load next input character
81F0 CMP #&20 ; Is it a space?
81F2 BEQ skpspi ; Yes: keep skipping
81F4 EOR #&0d ; XOR with CR: Z=1 if end of line
81F6 RTS ; Return (not our service call)

Service 9: *HELP

Prints the ROM identification string using print_inline.

81F7 .svc_9_help
JSR print_inline ; Print inline ROM identification string
81FA EQUS ".NFS 3.35d."
8205 .restore_y_return←2← 81BD BNE← 821A BNE
LDY nfs_temp ; Reload saved character counter
8207 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).

8208 .call_fscv_shutdown←2← 81BF JSR← 820D JSR
LDA #6 ; FSCV reason 6 = FS shutdown
820A 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.

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

8218 .check_boot_key
EOR #&55 ; XOR with &55: result=0 if key is 'N'
821A BNE restore_y_return ; Not 'N': return without claiming
821C TAY ; Y=key
821D LDA #osbyte_write_keys_pressed ; OSBYTE &78: clear key-pressed state
821F 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.

8222 .print_station_info←1← 8216 BMI
JSR print_inline ; Print 'Econet Station ' banner
8225 EQUS "Econet Station "
8234 LDY #&14 ; Y=&14: offset for station number
8236 LDA (net_rx_ptr),y ; Load station number
8238 JSR print_decimal ; Print as 3-digit decimal
823B LDA #&20 ; BIT trick: bit 5 of SR2 = clock present
823D BIT econet_control23_or_status2 ; Test DCD: clock present if bit 5 clear
8240 .dofsl1
BEQ skip_no_clock_msg ; Clock present: skip warning
8242 JSR print_inline ; Print ' No Clock' warning
8245 EQUS " No Clock"
824E NOP ; NOP (padding after inline string)
824F .skip_no_clock_msg←1← 8240 BEQ
JSR print_inline ; Print two CRs (blank line)
8252 EQUS ".."
fall through ↓

Initialise filing system vectors

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

8254 .init_fs_vectors←1← 81D4 BEQ
LDY #&0d ; Copy 14 bytes: FS vector addresses → FILEV-FSCV
8256 .init_vector_copy_loop←1← 825D BPL
LDA fs_vector_addrs,y ; Load vector address from table
8259 STA filev,y ; Write to FILEV-FSCV vector table
825C DEY ; Next byte (descending)
825D BPL init_vector_copy_loop ; Loop until all 14 bytes copied
825F JSR setup_rom_ptrs_netv ; Read ROM ptr table addr, install NETV
8262 LDY #&1b ; Install 7 handler entries in ROM ptr table
8264 LDX #7 ; 7 FS vectors to install
8266 JSR store_rom_ptr_pair ; Install each 3-byte vector entry
8269 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 fs_temp_cd is zero (auto-boot not inhibited), sets up the command string "I .BOOT" at &8282 and jumps to the FSCV 3 unrecognised-command handler (which matches against the command table at &8BE2). The "I." prefix triggers the catch-all entry which forwards the command to the fileserver. Falls through to run_fscv_cmd.

826B .issue_vectors_claimed←1← 81C5 JSR
LDA #osbyte_issue_service_request ; A=&8F: issue service request
826D LDX #&0f ; X=&0F: 'vectors claimed' service
826F JSR osbyte ; Issue paged ROM service call, Reason X=15 - Vectors claimed
8272 LDX #&0a ; X=&0A: service &0A
8274 JSR osbyte ; Issue service &0A
8277 LDX nfs_temp ; Non-zero after hard reset: skip auto-boot
8279 BNE return_3 ; Non-zero: skip auto-boot
827B LDX #&82 ; 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.

827D .run_fscv_cmd←2← 8329 LDA← 832F LDA
LDY #&82 ; Y=&82: ROM page high byte
827F 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.
8282 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.

828A .fs_vector_addrs←1← 8256 LDA
EQUB &1B ; FILEV dispatch lo
828B EQUB &FF ; FILEV dispatch hi
828C EQUB &1E ; ARGSV dispatch lo
828D EQUB &FF ; ARGSV dispatch hi
828E EQUB &21 ; BGETV dispatch lo
828F EQUB &FF ; BGETV dispatch hi
8290 EQUB &24 ; BPUTV dispatch lo
8291 EQUB &FF ; BPUTV dispatch hi
8292 EQUB &27 ; GBPBV dispatch lo
8293 EQUB &FF ; GBPBV dispatch hi
8294 EQUB &2A ; FINDV dispatch lo
8295 EQUB &FF ; FINDV dispatch hi
8296 EQUB &2D ; FSCV dispatch lo
8297 EQUB &FF ; FSCV dispatch hi
8298 EQUB &E7 ; FILEV handler lo (&86E7)
8299 EQUB &86 ; FILEV handler hi
829A EQUB &4A ; (ROM bank — not read)
829B EQUB &0C ; ARGSV handler lo (&890C)
829C EQUB &89 ; ARGSV handler hi
829D EQUB &44 ; (ROM bank — not read)
829E EQUB &39 ; BGETV handler lo (&8539)
829F EQUB &85 ; BGETV handler hi
82A0 EQUB &57 ; (ROM bank — not read)
82A1 EQUB &EC ; BPUTV handler lo (&83EC)
82A2 EQUB &83 ; BPUTV handler hi
82A3 EQUB &42 ; (ROM bank — not read)
82A4 EQUB &10 ; GBPBV handler lo (&8A10)
82A5 EQUB &8A ; GBPBV handler hi
82A6 EQUB &41 ; (ROM bank — not read)
82A7 EQUB &78 ; FINDV handler lo (&8978)
82A8 EQUB &89 ; FINDV handler hi
82A9 EQUB &52 ; (ROM bank — not read)
82AA EQUB &C7 ; FSCV handler lo (&80C7)
82AB 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)
82AC .svc_1_abs_workspace
CPY #&10 ; Already at page &10 or above?
82AE BCS return_3 ; Yes: nothing to claim
82B0 LDY #&10 ; Claim pages &0D-&0F (3 pages)
82B2 .return_3←2← 8279 BNE← 82AE BCS
RTS ; Return (workspace claim done)
82B3 EQUB &7C, &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)
82B5 .svc_2_private_workspace
STY net_rx_ptr_hi ; Store page as RX buffer high byte
82B7 INY ; Next page for NFS workspace
82B8 STY nfs_workspace_hi ; Store page as NFS workspace high
82BA LDA #0 ; A=0 for clearing workspace
82BC LDY #4 ; Y=4: remote status offset
82BE STA (net_rx_ptr),y ; Clear status byte in net receive buffer
82C0 LDY #&ff ; Y=&FF: used for later iteration
82C2 STA net_rx_ptr ; Clear RX ptr low byte
82C4 STA nfs_workspace ; Clear workspace ptr low byte
82C6 STA nfs_temp ; Clear RXCB iteration counter
82C8 STA tx_clear_flag ; Clear TX semaphore (no TX in progress)
82CB TAX ; X=0 for OSBYTE X=&00
82CC LDA #osbyte_read_write_last_break_type ; OSBYTE &FD: read type of last reset
82CE JSR osbyte ; Read type of last reset
82D1 TXA ; X = break type from OSBYTE result X=value of type of last reset
82D2 BEQ read_station_id ; Soft break (X=0): skip FS init
82D4 LDY #&15 ; Y=&15: printer station offset in RX buffer
82D6 LDA #&fe ; &FE = no server selected
82D8 STA fs_server_stn ; Station &FE = no server selected
82DB STA (net_rx_ptr),y ; Store &FE at printer station offset
82DD LDA #0 ; A=0 for clearing workspace fields
82DF STA fs_server_net ; Clear network number
82E2 STA prot_status ; Clear protection status
82E5 STA fs_messages_flag ; Clear message flag
82E8 STA fs_boot_option ; Clear boot option
82EB INY ; Y=&16
82EC STA (net_rx_ptr),y ; Clear net number at RX buffer offset &16
82EE LDY #3 ; Init printer server: station &FE, net 0
82F0 STA (nfs_workspace),y ; Store net 0 at workspace offset 3
82F2 DEY ; Y=2: printer station offset Y=&02
82F3 LDA #&eb ; &FE = no printer server
82F5 STA (nfs_workspace),y ; Store &FE at printer station in workspace
82F7 .init_rxcb_entries←1← 8304 BNE
LDA nfs_temp ; Load RXCB counter
82F9 JSR calc_handle_offset ; Convert to workspace byte offset
82FC BCS read_station_id ; C=1: past max handles, done
82FE LDA #&3f ; Mark RXCB as available
8300 STA (nfs_workspace),y ; Write &3F flag to workspace
8302 INC nfs_temp ; Next RXCB number
8304 BNE init_rxcb_entries ; Loop for all RXCBs
8306 .read_station_id←2← 82D2 BEQ← 82FC BCS
LDA station_id_disable_net_nmis ; Read station ID (also INTOFF)
8309 LDY #&14 ; Y=&14: station ID offset in RX buffer
830B STA (net_rx_ptr),y ; Store our station number
830D JSR trampoline_adlc_init ; Initialise ADLC hardware
8310 LDA #&40 ; Enable user-level RX (LFLAG=&40)
8312 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=&9008, rom=current) into the ROM pointer table at offset &36, installing osword_dispatch as the NETV handler.

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

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

8352 .init_tx_reply_port←1← 83CB JSR
LDA #&90 ; A=&90: FS reply port (PREPLY)
8354 .init_tx_ctrl_port←1← 8840 JSR
JSR init_tx_ctrl_block ; Init TXCB from template
8357 STA txcb_port ; Store port number in TXCB
8359 LDA #3 ; Control byte: 3 = transmit
835B STA txcb_start ; Store control byte in TXCB
835D DEC txcb_ctrl ; Decrement TXCB flag to arm TX
835F RTS ; Return after port setup

Initialise TX control block at &00C0 from template

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

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

8378 .tx_ctrl_template←1← 8363 LDA
EQUB &80 ; Control flag
8379 EQUB &99 ; Port (FS command = &99)
837A EQUB &00 ; Station (filled at runtime)
837B EQUB &00 ; Network (filled at runtime)
837C EQUB &00 ; Buffer start low
837D EQUB &0F ; Buffer start high (page &0F)
837E .tx_ctrl_upper←3← 88C0 BIT← 899B BIT← 9171 BIT
EQUB &FF ; Buffer start pad (4-byte Econet addr)
837F EQUB &FF ; Buffer start pad
8380 EQUB &FF ; Buffer end low
8381 EQUB &0F ; Buffer end high (page &0F)
8382 EQUB &FF ; Buffer end pad
8383 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
8384 .prepare_cmd_with_flag←1← 8A61 JSR
PHA ; Save flag byte for command
8385 LDA #&2a ; A=&2A: error ptr for retry
8387 SEC ; C=1: include flag in FS command
8388 BCS store_fs_hdr_fn ; ALWAYS branch to prepare_fs_cmd ALWAYS branch
838A .prepare_cmd_clv←2← 8704 JSR← 87AD JSR
CLV ; V=0: command has no flag byte
838B 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 &8BE2 for "BYE".

838D .bye_handler
LDA #osbyte_close_spool_exec ; A=&77: OSBYTE close spool/exec
838F JSR osbyte ; Close any *SPOOL and *EXEC files
8392 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)
8394 .prepare_fs_cmd←12← 80B8 JSR← 8865 JSR← 88DB JSR← 8927 JSR← 894E JSR← 89C1 JSR← 89E6 JSR← 8ABC JSR← 8B72 JSR← 8C1B JSR← 8C52 JSR← 8CBF JSR
CLV ; V=0: standard FS command path
8395 .init_tx_ctrl_data←2← 88C3 JSR← 899E JSR
.prepare_fs_cmd_v←2← 88C3 JSR← 899E JSR
LDA fs_urd_handle ; Copy URD handle from workspace to buffer
8398 STA fs_cmd_urd ; Store URD at &0F02
839B LDA #&2a ; A=&2A: error ptr for retry
839D .store_fs_hdr_clc←1← 838B BVC
CLC ; CLC: no byte-stream path
839E .store_fs_hdr_fn←1← 8388 BCS
STY fs_cmd_y_param ; Store function code at &0F01
83A1 STA fs_error_ptr ; Store error ptr for TX poll
83A3 LDY #1 ; Y=1: copy CSD (offset 1) then LIB (offset 0)
83A5 .copy_dir_handles←1← 83AC BPL
LDA fs_csd_handle,y ; Copy CSD and LIB handles to command buffer A=timeout period for FS reply
83A8 STA fs_cmd_csd,y ; Store at &0F03 (CSD) and &0F04 (LIB)
83AB DEY ; Y=function code
83AC 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)
83AE .build_send_fs_cmd←1← 8B15 JSR
PHP ; Save carry (FS path vs byte-stream)
83AF LDA #&90 ; Reply port &90 (PREPLY)
83B1 STA fs_cmd_type ; Store at &0F00 (HDRREP)
83B4 JSR init_tx_ctrl_block ; Copy TX template to &00C0
83B7 TXA ; A = X (buffer extent)
83B8 ADC #5 ; HPTR = header (5) + data (X) bytes to send
83BA STA txcb_end ; Store to TXCB end-pointer low
83BC PLP ; Restore carry flag
83BD BCS dofsl5 ; C=1: byte-stream path (BSXMIT)
83BF PHP ; Save flags for send_fs_reply_cmd
83C0 JSR setup_tx_ptr_c0 ; Point net_tx_ptr to &00C0; transmit
83C3 PLP ; Restore flags
83C4 BCC send_fs_reply_cmd ; C=0: standard FS command path
fall through ↓

Send FS command with standard timeout

Wrapper for send_fs_reply_cmd that sets the timeout counter (fs_error_ptr at &B8) to &2A before falling through. The &2A value becomes the outer loop count in send_to_fs's 3-level polling loop (~42 x 65536 iterations). Called after file transfer operations to send the completion command to the fileserver. Eliminated in 3.35K where call sites inline the LDA #&2A / STA fs_error_ptr sequence directly.

83C6 .send_fs_reply_timed←2← 87B9 JSR← 8A99 JSR
LDA #&2a ; A=&2A: error ptr for retry
83C8 STA fs_error_ptr ; Store error ptr for TX poll
83CA .send_fs_reply_cmd←1← 83C4 BCC
PHP ; Save flags (V flag state)
83CB JSR init_tx_reply_port ; Set up RX wait for FS reply
83CE LDA fs_error_ptr ; Load error ptr for TX retry
83D0 JSR send_to_fs ; Transmit and wait (BRIANX)
83D3 PLP ; Restore flags
83D4 .dofsl7←1← 83EA BCC
INY ; Y=1: skip past command code byte
83D5 LDA (txcb_start),y ; Load return code from FS reply
83D7 TAX ; X = return code
83D8 BEQ return_dofsl7 ; Zero: success, return
83DA BVC check_fs_error ; V=0: standard path, error is fatal
83DC ADC #&2a ; ADC #&2A: test for &D6 (not found)
83DE .check_fs_error←1← 83DA BVC
BNE store_fs_error ; Non-zero: hard error, go to FSERR
83E0 .return_dofsl7←1← 83D8 BEQ
RTS ; Return (success or soft &D6 error)
83E1 .dofsl5←1← 83BD BCS
PLA ; Discard saved flags from stack
83E2 LDX #&c0 ; X=&C0: TXCB address for byte-stream TX
83E4 INY ; Y++ past command code
83E5 JSR econet_tx_retry ; Byte-stream transmit with retry
83E8 STA fs_load_addr_3 ; Store result to &B3
83EA BCC dofsl7 ; C=0: success, check reply code
83EC .bputv_handler
CLC ; CLC for address addition
fall through ↓

Handle BPUT/BGET file byte I/O

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

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

On EntryC0 for BPUT (write byte), 1 for BGET (read byte)
Abyte to write (BPUT only)
Yfile handle
On ExitApreserved
Xpreserved
Ypreserved
83ED .handle_bput_bget←1← 853A JSR
PHA ; Save A (BPUT byte) on stack
83EE STA fs_error_flags ; Also save byte at &0FDF for BSXMIT
83F1 TXA ; Transfer X for stack save
83F2 PHA ; Save X on stack
83F3 TYA ; Transfer Y (handle) for stack save
83F4 PHA ; Save Y (handle) on stack
83F5 PHP ; Save P (C = BPUT/BGET selector) on stack
83F6 STY fs_spool_handle ; Save handle for SPOOL/EXEC comparison later
83F8 JSR handle_to_mask_clc ; Convert handle Y to single-bit mask
83FB STY fs_handle_mask ; Store handle bitmask at &0FDE
83FE STY fs_spool0 ; Store handle bitmask for sequence tracking
8400 LDY #&90 ; &90 = data port (PREPLY)
8402 STY fs_putb_buf ; Store reply port in command buffer
8405 JSR init_tx_ctrl_block ; Set up 12-byte TXCB from template
8408 LDA #&dc ; CB reply buffer at &0FDC
840A STA txcb_start ; Store reply buffer ptr low in TXCB
840C LDA #&e0 ; Error buffer at &0FE0
840E STA txcb_end ; Store error buffer ptr low in TXCB
8410 INY ; Y=1 (from init_tx_ctrl_block exit)
8411 LDX #9 ; X=9: BPUT function code
8413 PLP ; Restore C: selects BPUT (0) vs BGET (1)
8414 BCC store_retry_count ; C=0 (BPUT): keep X=9
8416 DEX ; X=&08
8417 .store_retry_count←1← 8414 BCC
STX fs_getb_buf ; Store function code at &0FDD
841A LDA fs_spool0 ; Load handle bitmask for BSXMIT
841C LDX #&c0 ; X=&C0: TXCB address for econet_tx_retry
841E JSR econet_tx_retry ; Transmit via byte-stream protocol
8421 LDX fs_getb_buf ; Load reply byte from buffer
8424 BEQ update_sequence_return ; Zero reply = success, skip error handling
8426 LDY #&1f ; Copy 32-byte reply to error buffer at &0FE0
8428 .error1←1← 842F BPL
LDA fs_putb_buf,y ; Load reply byte at offset Y
842B STA fs_error_buf,y ; Store to error buffer at &0FE0+Y
842E DEY ; Next byte (descending)
842F BPL error1 ; Loop until all 32 bytes copied
8431 TAX ; X=File handle
8432 LDA #osbyte_read_write_exec_file_handle ; A=&C6: read *EXEC file handle
8434 JSR osbyte ; Read/Write *EXEC file handle
8437 LDA #&f1 ; A=&F1: OSCLI "SP." string at &84F1
8439 CPY fs_spool_handle ; Y=value of *SPOOL file handle
843B BEQ close_spool_exec ; Match: close SPOOL file
843D LDA #&f5 ; A=&F5: offset for 'E.' string
843F CPX fs_spool_handle ; X=value of *EXEC file handle
8441 BNE dispatch_fs_error ; No match: dispatch FS error
8443 .close_spool_exec←1← 843B BEQ
TAX ; X = string offset for OSCLI close
8444 LDY #&84 ; Y=&84: high byte of OSCLI string in ROM
8446 JSR oscli ; Close SPOOL/EXEC via "*SP." or "*E."
8449 .dispatch_fs_error←1← 8441 BNE
LDA #&e0 ; Reset CB pointer to error buffer at &0FE0
844B STA txcb_start ; Reset reply ptr to error buffer
844D 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.

8450 .store_fs_error←1← 83DE BNE
STX fs_last_error ; Remember raw FS error code
8453 LDY #1 ; Y=1: point to error number byte in reply
8455 CPX #&a8 ; Clamp FS errors below &A8 to standard &A8
8457 BCS error_code_clamped ; Error >= &A8: keep original value
8459 LDA #&a8 ; Error < &A8: override with standard &A8
845B STA (txcb_start),y ; Write clamped error number to reply buffer
845D .error_code_clamped←1← 8457 BCS
LDY #&ff ; Start scanning from offset &FF (will INY to 0)
845F .copy_error_to_brk←1← 8467 BNE
INY ; Next byte in reply buffer
8460 LDA (txcb_start),y ; Copy reply buffer to &0100 for BRK execution
8462 STA l0100,y ; Build BRK error block at &0100
8465 EOR #&0d ; Scan for CR terminator (&0D)
8467 BNE copy_error_to_brk ; Continue until CR found
8469 STA l0100,y ; Replace CR with zero = BRK error block end
846C BEQ execute_downloaded ; Execute as BRK error block at &0100; ALWAYS ALWAYS branch
846E .update_sequence_return←1← 8424 BEQ
STA fs_sequence_nos ; Save updated sequence number
8471 PLA ; Restore Y from stack
8472 TAY ; Transfer A to Y for indexing
8473 PLA ; Restore X from stack
8474 TAX ; Transfer to X for return
8475 PLA ; Restore A from stack
8476 .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.

8477 .lang_1_remote_boot
LDY #4 ; Y=4: remote status flag offset
8479 LDA (net_rx_ptr),y ; Read remote status from RX CB
847B BEQ remot1 ; Zero: not remoted, set up session
847D .rchex←1← 84C3 BNE
JMP clear_jsr_protection ; Already remoted: clear and return
8480 .remot1←2← 847B BEQ← 84B9 BEQ
ORA #9 ; Set remote status: bits 0+3 (ORA #9)
8482 STA (net_rx_ptr),y ; Store updated remote status
8484 LDX #&80 ; X=&80: RX data area offset
8486 LDY #&80 ; Y=&80: read source station low
8488 LDA (net_rx_ptr),y ; Read source station lo from RX data at &80
848A PHA ; Save source station low byte
848B INY ; Y=&81
848C LDA (net_rx_ptr),y ; Read source station hi from RX data at &81
848E LDY #&0f ; Save controlling station to workspace &0E/&0F
8490 STA (nfs_workspace),y ; Store station high to ws+&0F
8492 DEY ; Y=&0E Y=&0e
8493 PLA ; Restore source station low
8494 STA (nfs_workspace),y ; Store station low to ws+&0E
8496 JSR clear_osbyte_ce_cf ; Clear OSBYTE &CE/&CF flags
8499 JSR ctrl_block_setup ; Set up TX control block
849C LDX #1 ; X=1: disable keyboard
849E LDY #0 ; Y=0 for OSBYTE
84A0 LDA #osbyte_read_write_econet_keyboard_disable ; Disable keyboard for remote session
84A2 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.

84A5 .lang_3_execute_at_0100
JSR clear_jsr_protection ; Allow JSR to page 1 (stack page)
84A8 LDX #2 ; Zero bytes &0100-&0102
84AA LDA #0 ; A=0: zero execution header bytes
84AC .zero_exec_header←1← 84B0 BPL
STA l0100,x ; BRK at &0100 as safe default
84AF DEX ; Next byte
84B0 BPL zero_exec_header ; Loop until all zeroed
84B2 .execute_downloaded←2← 846C BEQ← 84EB 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.

84B5 .lang_4_remote_validated
LDY #4 ; Y=4: RX control block byte 4 (remote status)
84B7 LDA (net_rx_ptr),y ; Read remote status flag
84B9 BEQ remot1 ; Zero = not remoted; allow new session
84BB LDY #&80 ; Read source station from RX data at &80
84BD LDA (net_rx_ptr),y ; A = source station number
84BF LDY #&0e ; Compare against controlling station at &0E
84C1 CMP (nfs_workspace),y ; Check if source matches controller
84C3 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.

84C5 .lang_0_insert_remote_key
LDY #&82 ; Read keypress from RX data at &82
84C7 LDA (net_rx_ptr),y ; Load character byte
84C9 TAY ; Y = character to insert
84CA LDX #0 ; X = buffer 0 (keyboard input)
84CC JSR clear_jsr_protection ; Release JSR protection before inserting key
84CF LDA #osbyte_insert_input_buffer ; OSBYTE &99: insert char into input buffer
84D1 JMP osbyte ; Tail call: insert character Y into buffer X Insert character Y into input buffer X
84D4 .error_not_listening←1← 8527 BEQ
LDA #8 ; Error code 8: "Not listening" error
84D6 BNE set_listen_offset ; ALWAYS branch to set_listen_offset ALWAYS branch
84D8 .nlistn←1← 86B2 JMP
LDA (net_tx_ptr,x) ; Load TX status byte for error lookup
84DA .nlisne←2← 8537 BNE← 89DE JMP
AND #7 ; Mask to 3-bit error code (0-7)
84DC .set_listen_offset←1← 84D6 BNE
TAX ; X = error code index
84DD LDY error_offsets,x ; Look up error message offset from table
84E0 LDX #0 ; X=0: start writing at &0101
84E2 STX l0100 ; Store BRK opcode at &0100
84E5 .copy_error_message←1← 84EF BNE
LDA error_msg_table,y ; Load error message byte
84E8 STA l0101,x ; Build error message at &0101+
84EB BEQ execute_downloaded ; Zero byte = end of message; go execute BRK
84ED INY ; Next error message source byte
84EE INX ; Next error buffer position
84EF BNE copy_error_message ; Continue copying message
84F1 EQUS "SP."
84F4 EQUB &0D, &45, &2E, &0D
84F8 .send_to_fs_star←3← 875D JSR← 9036 JMP← 9290 JSR
LDA #&2a ; A=&2A: error ptr for FS send
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.

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

852A .check_escape←2← 850A JSR← 8699 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.

852C .check_escape_handler
BIT escape_flag ; Test escape flag (bit 7)
852E BPL return_4 ; Bit 7 clear: no escape, return
8530 JSR osbyte ; Acknowledge escape via OSBYTE &7E
8533 LSR ; LSR: get escape result bit
8534 STA (net_tx_ptr),y ; Store escape result to TXCB
8536 ASL ; Restore A
8537 BNE nlisne ; Non-zero: report 'Not listening'
8539 .bgetv_handler
SEC ; C=1: flag for BGET mode
853A JSR handle_bput_bget ; Handle BGET via FS command Handle BPUT/BGET file byte I/O
853D SEC ; SEC: set carry for error check
853E LDA #&fe ; A=&FE: mask for EOF check
8540 BIT fs_error_flags ; BIT l0fdf: test error flags
8543 BVS return_4 ; V=1: error, return early
8545 CLC ; CLC: no error
8546 PHP ; Save P (C=0 from CLC above)
8547 LDA fs_spool0 ; Load handle bitmask
8549 PLP ; Restore C flag for branch test
854A BMI bgetv_shared_jsr ; Bit 7 set: skip clear, just set
854C JSR clear_fs_flag ; Clear EOF hint for this handle
854F .bgetv_shared_jsr←1← 854A BMI
JSR set_fs_flag ; Set EOF flag for this handle
8552 .load_handle_mask
LDA fs_handle_mask ; Load handle bitmask for caller
8555 .return_4←2← 852E BPL← 8543 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.
8556 .error_msg_table←1← 84E5 LDA
EQUB &A0
8557 EQUS "Line Jammed."
8563 EQUB &A1
8564 EQUS "Net Error."
856E EQUB &A2
856F EQUS "Not listening."
857D EQUB &A3
857E EQUS "No Clock."
8587 EQUB &11
8588 EQUS "Escape."
858F EQUB &CB
8590 EQUS "Bad Option."
859B EQUB &A5
859C 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.

85A5 .save_fscv_args_with_ptrs←3← 80C7 JSR← 8978 JSR← 8BB4 JSR
STX os_text_ptr ; X to os_text_ptr (text ptr lo)
85A7 STY os_text_ptr_hi ; Y to os_text_ptr hi
85A9 STX fs_cmd_ptr ; X to FS command ptr lo
85AC 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
85AF .save_fscv_args←3← 86E7 JSR← 890C JSR← 8A10 JSR
STA fs_last_byte_flag ; A = function code / command
85B1 STX fs_options ; X = control block ptr lo
85B3 STY fs_block_offset ; Y = control block ptr hi
85B5 STX fs_crc_lo ; X dup for indexed access via (fs_crc)
85B7 STY fs_crc_hi ; Y dup for indexed access
85B9 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 &85D7. The two formats use different bit layouts for file protection attributes.

85BA .decode_attribs_6bit←2← 889F JSR← 88CA JSR
LDY #&0e ; Y=&0E: attribute byte offset in param block
85BC LDA (fs_options),y ; Load FS attribute byte
85BE AND #&3f ; Mask to 6 bits (FS → BBC direction)
85C0 LDX #4 ; X=4: skip first 4 table entries (BBC→FS half)
85C2 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 &85D7. 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
85C4 .decode_attribs_5bit←2← 87C4 JSR← 88E7 JSR
AND #&1f ; Mask to 5 bits (BBC → FS direction)
85C6 LDX #&ff ; X=&FF: INX makes 0; start from table index 0
85C8 .attrib_shift_bits←1← 85C2 BNE
STA fs_error_ptr ; Temp storage for source bitmask to shift out
85CA LDA #0 ; A=0: accumulate destination bits here
85CC .map_attrib_bits←1← 85D4 BNE
INX ; Next table entry
85CD LSR fs_error_ptr ; Shift out source bits one at a time
85CF BCC skip_set_attrib_bit ; Bit was 0: skip this destination bit
85D1 ORA access_bit_table,x ; OR in destination bit from lookup table
85D4 .skip_set_attrib_bit←1← 85CF BCC
BNE map_attrib_bits ; Loop while source bits remain (A != 0)
85D6 RTS ; Return; A = converted attribute bitmask
85D7 .access_bit_table←1← 85D1 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
85E2 .print_inline←14← 81F7 JSR← 8222 JSR← 8242 JSR← 824F JSR← 8C23 JSR← 8C2D JSR← 8C3B JSR← 8C46 JSR← 8C5B JSR← 8C70 JSR← 8C83 JSR← 8C92 JSR← 8CA4 JSR← 8D74 JSR
PLA ; Pop return address (low) — points to last byte of JSR
85E3 STA fs_load_addr ; Store return addr low as string ptr
85E5 PLA ; Pop return address (high)
85E6 STA fs_load_addr_hi ; Store return addr high as string ptr
85E8 LDY #0 ; Y=0: offset for indirect load
85EA .print_inline_char←1← 85F7 JMP
INC fs_load_addr ; Advance pointer past return address / to next char
85EC BNE print_next_char ; No page wrap: skip high byte inc
85EE INC fs_load_addr_hi ; Handle page crossing in pointer
85F0 .print_next_char←1← 85EC BNE
LDA (fs_load_addr),y ; Load next byte from inline string
85F2 BMI jump_via_addr ; Bit 7 set? Done — this byte is the next opcode
85F4 JSR osasci ; Write character
85F7 JMP print_inline_char ; Continue printing next character
85FA .jump_via_addr←1← 85F2 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
85FD .parse_decimal←2← 808A JSR← 8090 JSR
TAX ; Save A in X for caller
85FE LDA #0 ; Zero accumulator
8600 STA fs_load_addr_2 ; Clear accumulator workspace
8602 .scan_decimal_digit←1← 861F BNE
LDA (fs_options),y ; Load next char from buffer
8604 CMP #&40 ; Letter or above?
8606 BCS no_dot_exit ; Yes: not a digit, done
8608 CMP #&2e ; Dot separator?
860A BEQ parse_decimal_rts ; Yes: exit with C=1 (dot found)
860C BMI no_dot_exit ; Control char or space: done
860E AND #&0f ; Mask ASCII digit to 0-9
8610 STA fs_load_addr_3 ; Save new digit
8612 ASL fs_load_addr_2 ; Running total * 2
8614 LDA fs_load_addr_2 ; A = running total * 2
8616 ASL ; A = running total * 4
8617 ASL ; A = running total * 8
8618 ADC fs_load_addr_2 ; + total*2 = total * 10
861A ADC fs_load_addr_3 ; + digit = total*10 + digit
861C STA fs_load_addr_2 ; Store new running total
861E INY ; Advance to next char
861F BNE scan_decimal_digit ; Loop (always: Y won't wrap to 0)
8621 .no_dot_exit←2← 8606 BCS← 860C BMI
CLC ; No dot found: C=0
8622 .parse_decimal_rts←1← 860A BEQ
LDA fs_load_addr_2 ; Return result in A
8624 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
8625 .handle_to_mask_a←3← 8853 JSR← 8A2B JSR← 8F4D 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
8626 .handle_to_mask_clc←2← 83F8 JSR← 8917 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
8627 .handle_to_mask←1← 897C JSR
PHA ; Save A (will be restored on exit)
8628 TXA ; Save X (will be restored on exit)
8629 PHA ; (second half of X save)
862A TYA ; A = handle from Y
862B BCC y2fsl5 ; C=0: always convert
862D BEQ handle_mask_exit ; C=1 and Y=0: skip (handle 0 = none)
862F .y2fsl5←1← 862B BCC
SEC ; C=1 and Y!=0: convert
8630 SBC #&1f ; A = handle - &1F (1-based bit position)
8632 TAX ; X = shift count
8633 LDA #1 ; Start with bit 0 set
8635 .y2fsl2←1← 8637 BNE
ASL ; Shift bit left
8636 DEX ; Count down
8637 BNE y2fsl2 ; Loop until correct position
8639 ROR ; Undo final extra shift
863A TAY ; Y = resulting bitmask
863B BNE handle_mask_exit ; Non-zero: valid mask, skip to exit
863D DEY ; Zero: invalid handle, set Y=&FF
863E .handle_mask_exit←2← 862D BEQ← 863B BNE
PLA ; Restore X
863F TAX ; Restore X from stack
8640 PLA ; Restore A
8641 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
8642 .mask_to_handle←2← 89AB JSR← 8F65 JSR
LDX #&1f ; X = &1F (handle base - 1)
8644 .fs2al1←1← 8646 BNE
INX ; Count this bit position
8645 LSR ; Shift mask right; C=0 when done
8646 BNE fs2al1 ; Loop until all bits shifted out
8648 TXA ; A = X = &1F + bit position = handle
8649 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
864A .compare_addresses←2← 8743 JSR← 87FB JSR
LDX #4 ; Compare 4 bytes (index 4,3,2,1)
864C .compare_addr_byte←1← 8653 BNE
LDA addr_work,x ; Load byte from first address
864E EOR fs_load_addr_3,x ; XOR with corresponding byte
8650 BNE return_compare ; Mismatch: Z=0, return unequal
8652 DEX ; Next byte
8653 BNE compare_addr_byte ; Continue comparing
8655 .return_compare←1← 8650 BNE
RTS ; Return with Z flag result
8656 .fscv_7_read_handles
LDX #&20 ; X=first handle (&20)
8658 LDY #&27 ; Y=last handle (&27)
865A .return_fscv_handles
RTS ; Return (FSCV 7 read handles)

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

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

On EntryAbitmask of bits to set
On ExitAupdated fs_eof_flags value
865B .set_fs_flag←5← 854F JSR← 8954 JSR← 89A7 JSR← 89C7 JSR← 8AA6 JSR
ORA fs_eof_flags ; Set EOF hint bit(s) for handle mask
865E BNE store_fs_flag ; ALWAYS branch to store updated flags

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

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

On EntryAbitmask of bits to clear
On ExitAupdated fs_eof_flags value
8660 .clear_fs_flag←3← 854C JSR← 886E JSR← 8AA3 JSR
EOR #&ff ; Invert mask for AND clear
8662 AND fs_eof_flags ; Clear specified bits in flags
8665 .store_fs_flag←1← 865E BNE
STA fs_eof_flags ; Write back updated flags
8668 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.

8669 .setup_tx_ptr_c0←2← 83C0 JSR← 883B JSR
LDX #&c0 ; X=&C0: TX control block at &00C0
866B STX net_tx_ptr ; Set TX pointer lo
866D LDX #0 ; X=0: page zero
866F STX net_tx_ptr_hi ; Set TX pointer high = 0 (page zero)
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.

8671 .tx_poll_ff←4← 901F JSR← 9079 JMP← 90D0 JSR← 926E JSR
LDA #&ff ; A=&FF: full retry count
8673 .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 (&8669) always uses the standard TXCB; tx_poll_core (&8675) is general-purpose.

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

86B9 .copy_filename_ptr←1← 86EA JSR
LDY #1 ; Y=1: copy 2 bytes (high then low)
86BB .file1←1← 86C1 BPL
LDA (fs_options),y ; Load filename ptr from control block
86BD STA os_text_ptr,y ; Store to MOS text pointer (&F2/&F3)
86C0 DEY ; Next byte (descending)
86C1 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 &86C5 allows a non-zero starting Y offset.

On EntryYoffset into (os_text_ptr) buffer (0 at &86C3)
On ExitXlength of parsed string
Ypreserved
86C3 .parse_filename_gs←2← 8991 JSR← 8DC7 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.

86C5 .parse_filename_gs_y←1← 8C11 JSR
LDX #&ff ; X=&FF: next INX wraps to first char index
86C7 CLC ; C=0 for GSINIT: parse from current position
86C8 JSR gsinit ; Initialise GS string parser
86CB BEQ terminate_filename ; Empty string: skip to CR terminator
86CD .quote1←1← 86D6 BCC
JSR gsread ; Read next character via GSREAD
86D0 BCS terminate_filename ; C=1 from GSREAD: end of string reached
86D2 INX ; Advance buffer index
86D3 STA fs_filename_buf,x ; Store parsed character to &0E30+X
86D6 BCC quote1 ; ALWAYS loop (GSREAD clears C on success) ALWAYS branch
86D8 .terminate_filename←2← 86CB BEQ← 86D0 BCS
INX ; Terminate parsed string with CR
86D9 .tx_result_check
LDA #&0d ; CR = &0D
86DB STA fs_filename_buf,x ; Store CR terminator at end of string
86DE LDA #&30 ; Point fs_crc_lo/hi at &0E30 parse buffer
86E0 STA fs_crc_lo ; fs_crc_lo = &30
86E2 LDA #&0e ; fs_crc_hi = &0E → buffer at &0E30
86E4 STA fs_crc_hi ; Store high byte
86E6 RTS ; Return; X = string length

FILEV handler (OSFILE entry point)

Calls save_fscv_args (&85AF) to preserve A/X/Y, then JSR &86B9 to copy the 2-byte filename pointer from the parameter block to os_text_ptr and fall through to parse_filename_gs (&86C3) 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 &86FD) A=&00: save file (filev_save at &8773) A=&01-&06: attribute operations (filev_attrib_dispatch at &8875) 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
86E7 .filev_handler
JSR save_fscv_args ; Save A/X/Y in FS workspace
86EA JSR copy_filename_ptr ; Copy filename ptr from param block to os_text_ptr
86ED LDA fs_last_byte_flag ; Recover function code from saved A
86EF BPL saveop ; A >= 0: save (&00) or attribs (&01-&06)
86F1 CMP #&ff ; A=&FF? Only &FF is valid for load
86F3 BEQ loadop ; A=&FF: branch to load path
86F5 JMP restore_args_return ; Unknown negative code: no-op return
86F8 .loadop←1← 86F3 BEQ
JSR copy_filename ; Copy parsed filename to cmd buffer
86FB 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
86FD .send_fs_examine←1← 8DDC JSR
LDA #&92 ; Port &92 = PLDATA (data transfer port)
86FF STA fs_cmd_urd ; Overwrite URD field with data port number
8702 LDA #&2a ; A=&2A: error ptr for retry
8704 JSR prepare_cmd_clv ; Build FS header (V=1: CLV path)
8707 LDY #6 ; Y=6: param block byte 6
8709 LDA (fs_options),y ; Byte 6: use file's own load address?
870B BNE lodfil ; Non-zero: use FS reply address (lodfil)
870D JSR copy_load_addr_from_params ; Zero: copy caller's load addr first
8710 JSR copy_reply_to_params ; Then copy FS reply to param block
8713 BCC skip_lodfil ; Carry clear from prepare_cmd_clv: skip lodfil
8715 .lodfil←1← 870B BNE
JSR copy_reply_to_params ; Copy FS reply addresses to param block
8718 JSR copy_load_addr_from_params ; Then copy load addr from param block
871B .skip_lodfil←1← 8713 BCC
LDY #4 ; Compute end address = load + file length
871D .copy_load_end_addr←1← 8728 BNE
LDA fs_load_addr,x ; Load address byte
871F STA txcb_end,x ; Store as current transfer position
8721 ADC fs_file_len,x ; Add file length byte
8724 STA fs_work_4,x ; Store as end position
8726 INX ; Next address byte
8727 DEY ; Decrement byte counter
8728 BNE copy_load_end_addr ; Loop for all 4 address bytes
872A SEC ; Adjust high byte for 3-byte length overflow
872B SBC fs_file_len_3 ; Subtract 4th length byte from end addr
872E STA fs_work_7 ; Store adjusted end address high byte
8730 JSR print_file_info ; Display file info after FS reply
8733 JSR send_data_blocks ; Transfer file data in &80-byte blocks
8736 LDX #2 ; Copy 3-byte file length to FS reply cmd buffer
8738 .floop←1← 873F BPL
LDA fs_file_len_3,x ; Load file length byte
873B STA fs_cmd_data,x ; Store in FS command data buffer
873E DEX ; Next byte (count down)
873F BPL floop ; Loop for 3 bytes (X=2,1,0)
8741 BMI send_fs_reply ; 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.

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

8773 .filev_save←1← 876E BEQ
LDX #4 ; Process 4 address bytes (load/exec/start/end)
8775 LDY #&0e ; Y=&0E: start from end-address in param block
8777 .savsiz←1← 8791 BNE
LDA (fs_options),y ; Read end-address byte from param block
8779 STA port_ws_offset,y ; Save to port workspace for transfer setup
877C JSR sub_4_from_y ; Y = Y-4: point to start-address byte
877F SBC (fs_options),y ; end - start = transfer length byte
8781 STA fs_cmd_csd,y ; Store length byte in FS command buffer
8784 PHA ; Save length byte for param block restore
8785 LDA (fs_options),y ; Read corresponding start-address byte
8787 STA port_ws_offset,y ; Save to port workspace
878A PLA ; Restore length byte from stack
878B STA (fs_options),y ; Replace param block entry with length
878D JSR add_5_to_y ; Y = Y+5: advance to next address group
8790 DEX ; Decrement address byte counter
8791 BNE savsiz ; Loop for all 4 address bytes
8793 LDY #9 ; Copy load/exec addresses to FS command buffer
8795 .copy_save_params←1← 879B BNE
LDA (fs_options),y ; Read load/exec address byte from params
8797 STA fs_cmd_csd,y ; Copy to FS command buffer
879A DEY ; Next byte (descending)
879B BNE copy_save_params ; Loop for bytes 9..1
879D LDA #&91 ; Port &91 for save command
879F STA fs_cmd_urd ; Overwrite URD field with port number
87A2 STA fs_error_ptr ; Save port &91 for flow control ACK
87A4 LDX #&0b ; Append filename at offset &0B in cmd buffer
87A6 JSR copy_string_to_cmd ; Append filename to cmd buffer at offset X
87A9 LDY #1 ; Y=1: function code for save
87AB LDA #&14 ; A=&14: FS function code for SAVE
87AD JSR prepare_cmd_clv ; Build header and send FS save command
87B0 JSR print_file_info ; Display filename being saved
87B3 .save_csd_display
LDA fs_cmd_data ; Load CSD from FS reply
87B6 JSR transfer_file_blocks ; Transfer file data blocks to server
87B9 .send_fs_reply←1← 8741 BMI
JSR send_fs_reply_timed ; Send FS reply acknowledgement
87BC .skip_catalogue_msg
STX fs_reply_cmd ; Store reply command for attr decode
87BF LDY #&0e ; Y=&0E: access byte offset in param block
87C1 LDA fs_cmd_data ; Load access byte from FS reply
87C4 JSR decode_attribs_5bit ; Convert FS access to BBC attribute format
87C7 BEQ direct_attr_copy ; Z=1: first byte, use A directly
87C9 .copy_attr_loop←1← 87D1 BNE
LDA fs_reply_data,y ; Load attribute byte from FS reply
87CC .direct_attr_copy←1← 87C7 BEQ
STA (fs_options),y ; Store decoded access in param block
87CE INY ; Next attribute byte
87CF CPY #&12 ; Copied all 4 bytes? (Y=&0E..&11)
87D1 BNE copy_attr_loop ; Loop for 4 attribute bytes
87D3 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).

87D6 .copy_load_addr_from_params←2← 870D JSR← 8718 JSR
LDY #5 ; Start at offset 5 (top of 4-byte addr)
87D8 .lodrl1←1← 87E0 BCS
LDA (fs_options),y ; Read from parameter block
87DA STA work_ae,y ; Store to local workspace
87DD DEY ; Next byte (descending)
87DE CPY #2 ; Copy offsets 5,4,3,2 (4 bytes)
87E0 BCS lodrl1 ; Loop while Y >= 2
87E2 .add_5_to_y←1← 878D JSR
INY ; Y += 5
87E3 .add_4_to_y←1← 8A73 JSR
INY ; Y += 4
87E4 INY ; (continued)
87E5 INY ; (continued)
87E6 INY ; (continued)
87E7 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)
87E8 .copy_reply_to_params←2← 8710 JSR← 8715 JSR
LDY #&0d ; Start at offset &0D (top of range)
87EA TXA ; First store uses X (attrib byte)
87EB .lodrl2←1← 87F3 BCS
STA (fs_options),y ; Write to parameter block
87ED LDA fs_cmd_urd,y ; Read next byte from reply buffer
87F0 DEY ; Next byte (descending)
87F1 CPY #2 ; Copy offsets &0D down to 2
87F3 BCS lodrl2 ; Loop until offset 2 reached
87F5 .sub_4_from_y←1← 877C JSR
DEY ; Y -= 4
87F6 .sub_3_from_y←2← 888D JSR← 8A7B JSR
DEY ; Y -= 3
87F7 DEY ; (continued)
87F8 DEY ; (continued)
87F9 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).

87FA .transfer_file_blocks←2← 87B6 JSR← 8A91 JSR
PHA ; Save FS command byte on stack
87FB JSR compare_addresses ; Compare two 4-byte addresses
87FE BEQ restore_ay_return ; Addresses equal: nothing to transfer
8800 .next_block←1← 884F BNE
LDX #0 ; X=0: clear hi bytes of block size
8802 LDY #4 ; Y=4: process 4 address bytes
8804 STX fs_reply_cmd ; Clear block size hi byte 1
8807 STX fs_load_vector ; Clear block size hi byte 2
880A CLC ; CLC for ADC in loop
880B .block_addr_loop←1← 8818 BNE
LDA fs_load_addr,x ; Source = current position
880D STA txcb_start,x ; Store source address byte
880F ADC fs_func_code,x ; Add block size to current position
8812 STA txcb_end,x ; Store dest address byte
8814 STA fs_load_addr,x ; Advance current position
8816 INX ; Next address byte
8817 DEY ; Decrement byte counter
8818 BNE block_addr_loop ; Loop for all 4 bytes
881A BCS clamp_dest_setup ; Carry: address overflowed, clamp
881C SEC ; SEC for SBC in overshoot check
881D .savchk←1← 8825 BNE
LDA fs_load_addr,y ; Check if new pos overshot end addr
8820 SBC fs_work_4,y ; Subtract end address byte
8823 INY ; Next byte
8824 DEX ; Decrement counter
8825 BNE savchk ; Loop for 4-byte comparison
8827 BCC send_block ; C=0: no overshoot, proceed
8829 .clamp_dest_setup←1← 881A BCS
LDX #3 ; Overshot: clamp dest to end address
882B .clamp_dest_addr←1← 8830 BPL
LDA fs_work_4,x ; Load end address byte
882D STA txcb_end,x ; Replace dest with end address
882F DEX ; Next byte
8830 BPL clamp_dest_addr ; Loop for all 4 bytes
8832 .send_block←1← 8827 BCC
PLA ; Recover original FS command byte
8833 PHA ; Re-push for next iteration
8834 PHP ; Save processor flags (C from cmp)
8835 STA txcb_port ; Store command byte in TXCB
8837 LDA #&80 ; 128-byte block size for data transfer
8839 STA txcb_ctrl ; Store size in TXCB control byte
883B JSR setup_tx_ptr_c0 ; Point TX ptr to &00C0; transmit
883E LDA fs_error_ptr ; ACK port for flow control
8840 JSR init_tx_ctrl_port ; Set reply port for ACK receive
8843 PLP ; Restore flags (C=overshoot status)
8844 BCS restore_ay_return ; C=1: all data sent (overshot), done
8846 LDA #&91 ; Command &91 = data block transfer
8848 STA txcb_port ; Store command &91 in TXCB
884A LDA #&2a ; A=&2A: error ptr for retry
884C JSR send_to_fs ; Transmit block and wait (BRIANX)
884F 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
8851 .fscv_1_eof
PHA ; Save A (function code)
8852 TXA ; X = file handle to check
8853 JSR handle_to_mask_a ; Convert handle to bitmask in A
8856 TYA ; Y = handle bitmask from conversion
8857 AND fs_eof_flags ; Local hint: is EOF possible for this handle?
885A TAX ; X = result of AND (0 = not at EOF)
885B BEQ restore_ay_return ; Hint clear: definitely not at EOF
885D PHA ; Save bitmask for clear_fs_flag
885E STY fs_cmd_data ; Handle byte in FS command buffer
8861 LDY #&11 ; Y=&11: FS function code FCEOF Y=function code for HDRFN
8863 LDX #1 ; X=preserved through header build
8865 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8868 PLA ; Restore bitmask
8869 LDX fs_cmd_data ; FS reply: non-zero = at EOF
886C BNE restore_ay_return ; At EOF: skip flag clear
886E JSR clear_fs_flag ; Not at EOF: clear the hint bit
8871 .restore_ay_return←4← 87FE BEQ← 8844 BCS← 885B BEQ← 886C BNE
PLA ; Restore A
8872 LDY fs_block_offset ; Restore Y
8874 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
8875 .filev_attrib_dispatch←1← 8770 JMP
STA fs_cmd_data ; Store function code in FS cmd buffer
8878 CMP #6 ; A=6? (delete)
887A BEQ cha6 ; Yes: jump to delete handler
887C BCS check_attrib_result ; A>=7: unsupported, fall through to return
887E CMP #5 ; A=5? (read catalogue info)
8880 BEQ cha5 ; Yes: jump to read info handler
8882 CMP #4 ; A=4? (write attributes only)
8884 BEQ cha4 ; Yes: jump to write attrs handler
8886 CMP #1 ; A=1? (write all catalogue info)
8888 BEQ get_file_protection ; Yes: jump to write-all handler
888A ASL ; A=2 or 3: convert to param block offset
888B ASL ; A*4: 2->8, 3->12
888C TAY ; Y = A*4
888D JSR sub_3_from_y ; Y = A*4 - 3 (load addr offset for A=2)
8890 LDX #3 ; X=3: copy 4 bytes
8892 .chalp1←1← 8899 BPL
LDA (fs_options),y ; Load address byte from param block
8894 STA fs_func_code,x ; Store to FS cmd data area
8897 DEY ; Next source byte (descending)
8898 DEX ; Next dest byte
8899 BPL chalp1 ; Loop for 4 bytes
889B LDX #5 ; X=5: data extent for filename copy
889D BNE copy_filename_to_cmd ; ALWAYS branch
889F .get_file_protection←1← 8888 BEQ
JSR decode_attribs_6bit ; A=1: encode protection from param block
88A2 STA fs_file_attrs ; Store encoded attrs at &0F0E
88A5 LDY #9 ; Y=9: source offset in param block
88A7 LDX #8 ; X=8: dest offset in cmd buffer
88A9 .chalp2←1← 88B0 BNE
LDA (fs_options),y ; Load byte from param block
88AB STA fs_cmd_data,x ; Store to FS cmd buffer
88AE DEY ; Next source byte (descending)
88AF DEX ; Next dest byte
88B0 BNE chalp2 ; Loop until X=0 (8 bytes copied)
88B2 LDX #&0a ; X=&0A: data extent past attrs+addrs
88B4 .copy_filename_to_cmd←2← 889D BNE← 88D2 BNE
JSR copy_string_to_cmd ; Append filename to cmd buffer
88B7 LDY #&13 ; Y=&13: fn code for FCSAVE (write attrs)
88B9 BNE send_fs_cmd_v1 ; ALWAYS branch to send command ALWAYS branch
88BB .cha6←1← 887A BEQ
JSR copy_filename ; A=6: copy filename (delete)
88BE LDY #&14 ; Y=&14: fn code for FCDEL (delete)
88C0 .send_fs_cmd_v1←1← 88B9 BNE
BIT tx_ctrl_upper ; Set V=1 (BIT trick: &B3 has bit 6 set)
88C3 JSR init_tx_ctrl_data ; Send via prepare_fs_cmd_v (V=1 path)
88C6 .check_attrib_result←1← 887C BCS
BCS attrib_error_exit ; C=1: &D6 not-found, skip to return
88C8 BCC argsv_check_return ; C=0: success, copy reply to param block ALWAYS branch
88CA .cha4←1← 8884 BEQ
JSR decode_attribs_6bit ; A=4: encode attrs from param block
88CD STA fs_func_code ; Store encoded attrs at &0F06
88D0 LDX #2 ; X=2: data extent (1 attr byte + fn)
88D2 BNE copy_filename_to_cmd ; ALWAYS branch to append filename ALWAYS branch
88D4 .cha5←1← 8880 BEQ
LDX #1 ; X=1: filename only, no data extent
88D6 JSR copy_string_to_cmd ; Copy filename to cmd buffer
88D9 LDY #&12 ; Y=&12: fn code for FCEXAM (read info) Y=function code for HDRFN
88DB JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
88DE LDA fs_obj_type ; Save object type from FS reply
88E1 STX fs_obj_type ; Clear reply byte (X=0 on success) X=0 on success, &D6 on not-found
88E4 STX fs_len_clear ; Clear length high byte in reply
88E7 JSR decode_attribs_5bit ; Decode 5-bit access byte from FS reply
88EA LDY #&0e ; Y=&0E: attrs offset in param block
88EC STA (fs_options),y ; Store decoded attrs at param block +&0E
88EE DEY ; Y=&0D: start copy below attrs Y=&0d
88EF LDX #&0c ; X=&0C: copy from reply offset &0C down
88F1 .copy_fs_reply_to_cb←1← 88F8 BNE
LDA fs_cmd_data,x ; Load reply byte (load/exec/length)
88F4 STA (fs_options),y ; Store to param block
88F6 DEY ; Next dest byte (descending)
88F7 DEX ; Next source byte
88F8 BNE copy_fs_reply_to_cb ; Loop until X=0 (12 bytes copied)
88FA INX ; X=0 -> X=2 for length high copy
88FB INX ; INX again: X=2
88FC LDY #&11 ; Y=&11: length high dest in param block
88FE .cha5lp←1← 8905 BPL
LDA fs_access_level,x ; Load length high byte from reply
8901 STA (fs_options),y ; Store to param block
8903 DEY ; Next dest byte (descending)
8904 DEX ; Next source byte
8905 BPL cha5lp ; Loop for 3 length-high bytes
8907 LDA fs_cmd_data ; Return object type in A
890A .attrib_error_exit←1← 88C6 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
890C .argsv_handler
JSR save_fscv_args ; Save A/X/Y registers for later restore
890F CMP #3 ; Function >= 3?
8911 BCS restore_args_return ; A>=3 (ensure/flush): no-op for NFS
8913 CPY #0 ; Test file handle
8915 BEQ argsv_dispatch_a ; Y=0: FS-level query, not per-file
8917 JSR handle_to_mask_clc ; Convert handle to bitmask
891A STY fs_cmd_data ; Store bitmask as first cmd data byte
891D LSR ; LSR splits A: C=1 means write (A=1)
891E STA fs_func_code ; Store function code to cmd data byte 2
8921 BCS save_args_handle ; C=1: write path, copy ptr from caller
8923 LDY #&0c ; Y=&0C: FCRDSE (read sequential pointer) Y=function code for HDRFN
8925 LDX #2 ; X=2: 3 data bytes in command X=preserved through header build
8927 JSR prepare_fs_cmd ; Build and send FS command Prepare FS command buffer (12 references)
892A STA fs_last_byte_flag ; Clear last-byte flag on success A=0 on success (from build_send_fs_cmd)
892C LDX fs_options ; X = saved control block ptr low
892E LDY #2 ; Y=2: copy 3 bytes of file pointer
8930 STA zp_work_3,x ; Zero high byte of 3-byte pointer
8932 .copy_fileptr_reply←1← 8939 BPL
LDA fs_cmd_data,y ; Read reply byte from FS cmd data
8935 STA zp_work_2,x ; Store to caller's control block
8937 DEX ; Next byte (descending)
8938 DEY ; Next source byte
8939 BPL copy_fileptr_reply ; Loop for all 3 bytes
893B .argsv_check_return←1← 88C8 BCC
BCC restore_args_return ; C=0 (read): return to caller
893D .save_args_handle←1← 8921 BCS
TYA ; Save bitmask for set_fs_flag later
893E PHA ; Push bitmask
893F LDY #3 ; Y=3: copy 4 bytes of file pointer
8941 .copy_fileptr_to_cmd←1← 8948 BPL
LDA zp_work_3,x ; Read caller's pointer byte
8943 STA fs_data_count,y ; Store to FS command data area
8946 DEX ; Next source byte
8947 DEY ; Next destination byte
8948 BPL copy_fileptr_to_cmd ; Loop for all 4 bytes
894A LDY #&0d ; Y=&0D: FCWRSE (write sequential pointer) Y=function code for HDRFN
894C LDX #5 ; X=5: 6 data bytes in command X=preserved through header build
894E JSR prepare_fs_cmd ; Build and send FS command Prepare FS command buffer (12 references)
8951 STX fs_last_byte_flag ; Save not-found status from X X=0 on success, &D6 on not-found
8953 PLA ; Recover bitmask for EOF hint update
8954 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.

8957 .restore_args_return←7← 86F5 JMP← 87D3 JMP← 8911 BCS← 893B BCC← 89CA BCC← 8A1B JMP← 8E25 JMP
LDA fs_last_byte_flag ; A = saved function code / command
8959 .restore_xy_return←5← 890A BPL← 896A BNE← 8976 BPL← 89A1 BCS← 89AE BNE
LDX fs_options ; X = saved control block ptr low
895B LDY fs_block_offset ; Y = saved control block ptr high
895D RTS ; Return to MOS with registers restored
895E .argsv_dispatch_a←1← 8915 BEQ
CMP #2 ; OSARGS A=2: read filing system
8960 BEQ halve_args_a ; A=2: halve to get FS number
8962 BCS return_a_zero ; A>2: unsupported, return zero
8964 TAY ; Y=A for block copy index
8965 BNE osarg1 ; A=1: read command context block
8967 LDA #&0a ; A=&0A: NFS filing system number
8969 .halve_args_a←1← 8960 BEQ
LSR ; Shared: halve A (A=0 or A=2 paths)
896A BNE restore_xy_return ; Return with A = FS number or 1
896C .osarg1←2← 8965 BNE← 8972 BPL
LDA fs_cmd_context,y ; Copy command context to caller's block
896F STA (fs_options),y ; Store to caller's parameter block
8971 DEY ; Next byte (descending)
8972 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.

8974 .return_a_zero←4← 8962 BCS← 8984 BNE← 8AB7 JMP← 8B54 JMP
LDA #0 ; A=operation (0=close, &40=read, &80=write, &C0=R/W)
8976 BPL restore_xy_return ; ALWAYS branch

FINDV handler (OSFIND entry point)

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

On EntryAoperation (0=close, &40=read, &80=write, &C0=R/W)
Xfilename pointer low (open)
Yfile handle (close) or filename pointer high (open)
On ExitAhandle on open, 0 on close-all, restored on close-one
Xrestored
Yrestored
8978 .findv_handler
JSR save_fscv_args_with_ptrs ; Save A/X/Y and set up pointers
897B SEC ; SEC distinguishes open (A>0) from close
897C JSR handle_to_mask ; Convert file handle to bitmask (Y2FS)
897F TAX ; A=preserved
8980 BEQ close_handle ; A=0: close file(s)
8982 AND #&3f ; Valid open modes: &40, &80, &C0 only
8984 BNE return_a_zero ; Invalid mode bits: return
8986 TXA ; A = original mode byte
8987 EOR #&80 ; Convert MOS mode to FS protocol flags
8989 ASL ; ASL: shift mode bits left
898A STA fs_cmd_data ; Flag 1: read/write direction
898D ROL ; ROL: Flag 2 into bit 0
898E STA fs_func_code ; Flag 2: create vs existing file
8991 JSR parse_filename_gs ; Parse filename from command line
8994 LDX #2 ; X=2: copy after 2-byte flags
8996 JSR copy_string_to_cmd ; Copy filename to FS command buffer
8999 LDY #6 ; Y=6: FS function code FCOPEN
899B BIT tx_ctrl_upper ; Set V flag from l83b3 bit 6
899E JSR init_tx_ctrl_data ; Build and send FS open command
89A1 BCS restore_xy_return ; Error: restore and return
89A3 LDA fs_cmd_data ; Load reply handle from FS
89A6 TAX ; X = new file handle
89A7 JSR set_fs_flag ; Set EOF hint + sequence bits
89AA TXA ; A=single-bit bitmask
89AB JSR mask_to_handle ; Convert bitmask to handle number (FS2A)
89AE 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)
89B0 .close_handle←1← 8980 BEQ
TYA ; A = handle (Y preserved in A) Y=preserved
89B1 BNE close_single_handle ; Y>0: close single file
89B3 LDA #osbyte_close_spool_exec ; Close SPOOL/EXEC before FS close-all
89B5 JSR osbyte ; Close any *SPOOL and *EXEC files
89B8 LDY #0 ; Y=0: close all handles on server
89BA .close_single_handle←1← 89B1 BNE
STY fs_cmd_data ; Handle byte in FS command buffer
89BD LDX #1 ; X=preserved through header build
89BF LDY #7 ; Y=function code for HDRFN
89C1 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
89C4 LDA fs_cmd_data ; Reply handle for flag update
89C7 JSR set_fs_flag ; Update EOF/sequence tracking bits
89CA .close_opt_return←1← 89EE 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
89CC .fscv_0_opt
CPX #4 ; Is it *OPT 4,Y?
89CE BNE check_opt1 ; No: check for *OPT 1
89D0 CPY #4 ; Y must be 0-3 for boot option
89D2 BCC optl1 ; Y < 4: valid boot option
89D4 .check_opt1←1← 89CE BNE
DEX ; Not *OPT 4: check for *OPT 1
89D5 BNE opter1 ; Not *OPT 1 either: bad option
89D7 .set_messages_flag
STY fs_messages_flag ; Set local messages flag (*OPT 1,Y)
89DA BCC opt_return ; Return via restore_args_return
89DC .opter1←1← 89D5 BNE
LDA #7 ; Error index 7 (Bad option)
89DE JMP nlisne ; Generate BRK error
89E1 .optl1←1← 89D2 BCC
STY fs_cmd_data ; Boot option value in FS command
89E4 LDY #&16 ; Y=&16: FS function code FCOPT Y=function code for HDRFN
89E6 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
89E9 LDY fs_block_offset ; Restore Y from saved value
89EB STY fs_boot_option ; Cache boot option locally
89EE .opt_return←1← 89DA BCC
BCC close_opt_return ; Return via restore_args_return
89F0 .adjust_addrs_9←1← 8AAB JSR
LDY #9 ; Y=9: adjust 9 address bytes
89F2 JSR adjust_addrs_clc ; Adjust with carry clear
89F5 .adjust_addrs_1←1← 8B9B JSR
LDY #1 ; Y=1: adjust 1 address byte
89F7 .adjust_addrs_clc←1← 89F2 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
89F8 .adjust_addrs←2← 8AB1 JSR← 8BA7 JSR
LDX #&fc ; X=&FC: index into &0E06 area (wraps to 0)
89FA .adjust_addr_byte←1← 8A0D BNE
LDA (fs_options),y ; Load byte from param block
89FC BIT fs_load_addr_2 ; Test sign of adjustment direction
89FE BMI subtract_adjust ; Negative: subtract instead
8A00 ADC fs_cmd_context,x ; Add adjustment value
8A03 JMP gbpbx ; Skip to store result
8A06 .subtract_adjust←1← 89FE BMI
SBC fs_cmd_context,x ; Subtract adjustment value
8A09 .gbpbx←1← 8A03 JMP
STA (fs_options),y ; Store adjusted byte back
8A0B INY ; Next param block byte
8A0C INX ; Next adjustment byte (X wraps &FC->&00)
8A0D BNE adjust_addr_byte ; Loop 4 times (X=&FC,&FD,&FE,&FF,done)
8A0F 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
8A10 .gbpbv_handler
JSR save_fscv_args ; Save A/X/Y to FS workspace
8A13 TAX ; X = call number for range check
8A14 BEQ gbpb_invalid_exit ; A=0: invalid, restore and return
8A16 DEX ; Convert to 0-based (A=0..7)
8A17 CPX #8 ; Range check: must be 0-7
8A19 BCC gbpbx1 ; In range: continue to handler
8A1B .gbpb_invalid_exit←1← 8A14 BEQ
JMP restore_args_return ; Out of range: restore args and return
8A1E .gbpbx1←1← 8A19 BCC
TXA ; Recover 0-based function code
8A1F LDY #0 ; Y=0: param block byte 0 (file handle)
8A21 PHA ; Save function code on stack
8A22 CMP #4 ; A>=4: info queries, dispatch separately
8A24 BCC gbpbe1 ; A<4: file read/write operations
8A26 JMP osgbpb_info ; Dispatch to OSGBPB 5-8 info handler
8A29 .gbpbe1←1← 8A24 BCC
LDA (fs_options),y ; Get file handle from param block byte 0
8A2B JSR handle_to_mask_a ; Convert handle to bitmask for EOF flags
8A2E STY fs_cmd_data ; Store handle in FS command data
8A31 LDY #&0b ; Y=&0B: start at param block byte 11
8A33 LDX #6 ; X=6: copy 6 bytes of transfer params
8A35 .gbpbf1←1← 8A41 BNE
LDA (fs_options),y ; Load param block byte
8A37 STA fs_func_code,x ; Store to FS command buffer at &0F06+X
8A3A DEY ; Previous param block byte
8A3B CPY #8 ; Skip param block offset 8 (the handle)
8A3D BNE gbpbx0 ; Not at handle offset: continue
8A3F DEY ; Extra DEY to skip handle byte
8A40 .gbpbx0←1← 8A3D BNE
.gbpbf2←1← 8A3D BNE
DEX ; Decrement copy counter
8A41 BNE gbpbf1 ; Loop for all 6 bytes
8A43 PLA ; Recover function code from stack
8A44 LSR ; LSR: odd=read (C=1), even=write (C=0)
8A45 PHA ; Save function code again (need C later)
8A46 BCC gbpbl1 ; Even (write): X stays 0
8A48 INX ; Odd (read): X=1
8A49 .gbpbl1←1← 8A46 BCC
STX fs_func_code ; Store FS direction flag
8A4C LDY #&0b ; Y=&0B: command data extent
8A4E LDX #&91 ; Command &91=put, &92=get
8A50 PLA ; Recover function code
8A51 PHA ; Save again for later direction check
8A52 BEQ gbpb_write_path ; Even (write): keep &91 and Y=&0B
8A54 LDX #&92 ; Odd (read): use &92 (get) instead
8A56 DEY ; Read: one fewer data byte in command Y=&0a
8A57 .gbpb_write_path←1← 8A52 BEQ
STX fs_cmd_urd ; Store port to FS command URD field
8A5A STX fs_error_ptr ; Save port for error recovery
8A5C LDX #8 ; X=8: command data bytes
8A5E LDA fs_cmd_data ; Load handle from FS command data
8A61 JSR prepare_cmd_with_flag ; Build FS command with handle+flag
8A64 LDA fs_load_addr_3 ; Save seq# for byte-stream flow control
8A66 STA fs_sequence_nos ; Store to FS sequence number workspace
8A69 LDX #4 ; X=4: copy 4 address bytes
8A6B .gbpbl3←1← 8A7F BNE
LDA (fs_options),y ; Set up source/dest from param block
8A6D STA addr_work,y ; Store as source address
8A70 STA txcb_pos,y ; Store as current transfer position
8A73 JSR add_4_to_y ; Skip 4 bytes to reach transfer length
8A76 ADC (fs_options),y ; Dest = source + length
8A78 STA addr_work,y ; Store as end address
8A7B JSR sub_3_from_y ; Back 3 to align for next iteration
8A7E DEX ; Decrement byte counter
8A7F BNE gbpbl3 ; Loop for all 4 address bytes
8A81 INX ; X=1 after loop
8A82 .gbpbf3←1← 8A89 BPL
LDA fs_cmd_csd,x ; Copy CSD data to command buffer
8A85 STA fs_func_code,x ; Store at &0F06+X
8A88 DEX ; Decrement counter
8A89 BPL gbpbf3 ; Loop for X=1,0
8A8B PLA ; Odd (read): send data to FS first
8A8C BNE gbpb_read_path ; Non-zero: skip write path
8A8E LDA fs_cmd_urd ; Load port for transfer setup
8A91 JSR transfer_file_blocks ; Transfer data blocks to fileserver
8A94 BNE wait_fs_reply ; Non-zero: branch past error ptr
8A96 .gbpb_read_path←1← 8A8C BNE
JSR send_data_blocks ; Read path: receive data blocks from FS
8A99 .wait_fs_reply←1← 8A94 BNE
JSR send_fs_reply_timed ; Wait for FS reply command
8A9C LDA (fs_options,x) ; Load handle mask for EOF flag update
8A9E BIT fs_cmd_data ; Check FS reply: bit 7 = not at EOF
8AA1 BMI skip_clear_flag ; Bit 7 set: not EOF, skip clear
8AA3 JSR clear_fs_flag ; At EOF: clear EOF hint for this handle
8AA6 .skip_clear_flag←1← 8AA1 BMI
JSR set_fs_flag ; Set EOF hint flag (may be at EOF)
8AA9 STX fs_load_addr_2 ; Direction=0: forward adjustment
8AAB JSR adjust_addrs_9 ; Adjust param block addrs by +9 bytes
8AAE DEC fs_load_addr_2 ; Direction=&FF: reverse adjustment
8AB0 SEC ; SEC for reverse subtraction
8AB1 JSR adjust_addrs ; Adjust param block addrs (reverse)
8AB4 ASL fs_cmd_data ; Shift bit 7 into C for return flag
8AB7 JMP return_a_zero ; Return via restore_args path
8ABA .get_disc_title←1← 8AEA BEQ
LDY #&15 ; Y=&15: function code for disc title Y=function code for HDRFN
8ABC JSR prepare_fs_cmd ; Build and send FS command Prepare FS command buffer (12 references)
8ABF LDA fs_boot_option ; Load boot option from FS workspace
8AC2 STA fs_boot_data ; Store boot option in reply area
8AC5 STX fs_load_addr ; X=0: reply data start offset X=0 on success, &D6 on not-found
8AC7 STX fs_load_addr_hi ; Clear reply buffer high byte
8AC9 LDA #&12 ; A=&12: 18 bytes of reply data
8ACB STA fs_load_addr_2 ; Store as byte count for copy
8ACD 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.

8ACF .osgbpb_info←1← 8A26 JMP
LDY #4 ; Y=4: check param block byte 4
8AD1 LDA tx_in_progress ; Check if destination is in Tube space
8AD4 BEQ store_tube_flag ; No Tube: skip Tube address check
8AD6 CMP (fs_options),y ; Compare Tube flag with addr byte 4
8AD8 BNE store_tube_flag ; Mismatch: not Tube space
8ADA DEY ; Y=&03
8ADB SBC (fs_options),y ; Y=3: subtract addr byte 3 from flag
8ADD .store_tube_flag←2← 8AD4 BEQ← 8AD8 BNE
STA rom_svc_num ; Non-zero = Tube transfer required
8ADF .info2←1← 8AE5 BNE
LDA (fs_options),y ; Copy param block bytes 1-4 to workspace
8AE1 STA fs_last_byte_flag,y ; Store to &BD+Y workspace area
8AE4 DEY ; Previous byte
8AE5 BNE info2 ; Loop for bytes 3,2,1
8AE7 PLA ; Sub-function: AND #3 of (original A - 4)
8AE8 AND #3 ; Mask to 0-3 (OSGBPB 5-8 → 0-3)
8AEA BEQ get_disc_title ; A=0 (OSGBPB 5): read disc title
8AEC LSR ; LSR: A=0 (OSGBPB 6) or A=1 (OSGBPB 7)
8AED BEQ gbpb6_read_name ; A=0 (OSGBPB 6): read CSD/LIB name
8AEF BCS gbpb8_read_dir ; C=1 (OSGBPB 8): read filenames from dir
8AF1 .gbpb6_read_name←1← 8AED BEQ
TAY ; Y=0 for CSD or carry for fn code select Y=function code
8AF2 LDA fs_csd_handle,y ; Get CSD/LIB/URD handles for FS command
8AF5 STA fs_cmd_csd ; Store CSD handle in command buffer
8AF8 LDA fs_lib_handle ; Load LIB handle from workspace
8AFB STA fs_cmd_lib ; Store LIB handle in command buffer
8AFE LDA fs_urd_handle ; Load URD handle from workspace
8B01 STA fs_cmd_urd ; Store URD handle in command buffer
8B04 LDX #&12 ; X=&12: buffer extent for command data X=buffer extent (command-specific data bytes)
8B06 STX fs_cmd_y_param ; Store X as function code in header
8B09 LDA #&0d ; &0D = 13 bytes of reply data expected
8B0B STA fs_func_code ; Store reply length in command buffer
8B0E STA fs_load_addr_2 ; Store as byte count for copy loop
8B10 LSR ; LSR: &0D >> 1 = 6 A=timeout period for FS reply
8B11 STA fs_cmd_data ; Store as command data byte
8B14 CLC ; CLC for standard FS path
8B15 JSR build_send_fs_cmd ; Build and send FS command (DOFSOP)
8B18 STX fs_load_addr_hi ; X=0 on success, &D6 on not-found
8B1A INX ; INX: X=1 after build_send_fs_cmd
8B1B STX fs_load_addr ; Store X as reply start offset
8B1D .copy_reply_to_caller←2← 8ACD BNE← 8B90 JSR
LDA rom_svc_num ; Copy FS reply to caller's buffer
8B1F BNE tube_transfer ; Non-zero: use Tube transfer path
8B21 LDX fs_load_addr ; X = reply start offset
8B23 LDY fs_load_addr_hi ; Y = reply buffer high byte
8B25 .copy_reply_bytes←1← 8B2E BNE
LDA fs_cmd_data,x ; Load reply data byte
8B28 STA (fs_crc_lo),y ; Store to caller's buffer
8B2A INX ; Next source byte
8B2B INY ; Next destination byte
8B2C DEC fs_load_addr_2 ; Decrement remaining bytes
8B2E BNE copy_reply_bytes ; Loop until all bytes copied
8B30 BEQ gbpb_done ; ALWAYS branch to exit ALWAYS branch
8B32 .tube_transfer←1← 8B1F BNE
JSR tube_claim_loop ; Claim Tube transfer channel
8B35 LDA #1 ; A=1: Tube claim type 1 (write)
8B37 LDX fs_options ; X = param block address low
8B39 LDY fs_block_offset ; Y = param block address high
8B3B INX ; INX: advance past byte 0
8B3C BNE no_page_wrap ; No page wrap: keep Y
8B3E INY ; Page wrap: increment high byte
8B3F .no_page_wrap←1← 8B3C BNE
JSR tube_addr_claim ; Claim Tube address for transfer
8B42 LDX fs_load_addr ; X = reply data start offset
8B44 .tbcop1←1← 8B4D BNE
LDA fs_cmd_data,x ; Load reply data byte
8B47 STA tube_data_register_3 ; Send byte to Tube via R3
8B4A INX ; Next source byte
8B4B DEC fs_load_addr_2 ; Decrement remaining bytes
8B4D BNE tbcop1 ; Loop until all bytes sent to Tube
8B4F LDA #&83 ; Release Tube after transfer complete
8B51 JSR tube_addr_claim ; Release Tube address claim
8B54 .gbpb_done←2← 8B30 BEQ← 8BAA BEQ
JMP return_a_zero ; Return via restore_args path
8B57 .gbpb8_read_dir←1← 8AEF BCS
LDY #9 ; OSGBPB 8: read filenames from dir
8B59 LDA (fs_options),y ; Byte 9: number of entries to read
8B5B STA fs_func_code ; Store as reply count in command buffer
8B5E LDY #5 ; Y=5: byte 5 = starting entry number
8B60 LDA (fs_options),y ; Load starting entry number
8B62 STA fs_data_count ; Store in command buffer
8B65 LDX #&0d ; X=&0D: command data extent X=preserved through header build
8B67 STX fs_reply_cmd ; Store extent in command buffer
8B6A LDY #2 ; Y=2: function code for dir read
8B6C STY fs_load_addr ; Store 2 as reply data start offset
8B6E STY fs_cmd_data ; Store 2 as command data byte
8B71 INY ; Y=3: function code for header read Y=function code for HDRFN Y=&03
8B72 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8B75 STX fs_load_addr_hi ; X=0 after FS command completes X=0 on success, &D6 on not-found
8B77 LDA fs_func_code ; Load reply entry count
8B7A STA (fs_options,x) ; Store at param block byte 0 (X=0)
8B7C LDA fs_cmd_data ; Load entries-read count from reply
8B7F LDY #9 ; Y=9: param block byte 9
8B81 ADC (fs_options),y ; Add to starting entry number
8B83 STA (fs_options),y ; Update param block with new position
8B85 LDA txcb_end ; Load total reply length
8B87 SBC #7 ; Subtract header (7 bytes) from reply len
8B89 STA fs_func_code ; Store adjusted length in command buffer
8B8C STA fs_load_addr_2 ; Store as byte count for copy loop
8B8E BEQ skip_copy_reply ; Zero bytes: skip copy
8B90 JSR copy_reply_to_caller ; Copy reply data to caller's buffer
8B93 .skip_copy_reply←1← 8B8E BEQ
LDX #2 ; X=2: clear 3 bytes
8B95 .zero_cmd_bytes←1← 8B99 BPL
STA fs_data_count,x ; Zero out &0F07+X area
8B98 DEX ; Next byte
8B99 BPL zero_cmd_bytes ; Loop for X=2,1,0
8B9B JSR adjust_addrs_1 ; Adjust pointer by +1 (one filename read)
8B9E SEC ; SEC for reverse adjustment
8B9F DEC fs_load_addr_2 ; Reverse adjustment for updated counter
8BA1 LDA fs_cmd_data ; Load entries-read count
8BA4 STA fs_func_code ; Store in command buffer
8BA7 JSR adjust_addrs ; Adjust param block addresses
8BAA BEQ gbpb_done ; Z=1: all done, exit
8BAC .tube_claim_loop←3← 8B32 JSR← 8BB1 BCC← 8E0E JSR
LDA #&c3 ; A=&C3: Tube claim with retry
8BAE JSR tube_addr_claim ; Request Tube address claim
8BB1 BCC tube_claim_loop ; C=0: claim failed, retry
8BB3 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 &8BE2 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.

8BB4 .fscv_3_star_cmd←1← 827F JMP
JSR save_fscv_args_with_ptrs ; Save A/X/Y and set up command ptr
8BB7 LDX #&ff ; X=&FF: table index (pre-incremented)
8BB9 STX fs_crflag ; Disable column formatting
8BBB .scan_cmd_table←1← 8BD6 BNE
LDY #&ff ; Y=&FF: input index (pre-incremented)
8BBD .decfir←1← 8BC8 BEQ
INY ; Advance input pointer
8BBE INX ; Advance table pointer
8BBF .decmor←1← 8BDA BCS
LDA fs_cmd_match_table,x ; Load table character
8BC2 BMI dispatch_cmd ; Bit 7: end of name, dispatch
8BC4 EOR (fs_crc_lo),y ; XOR input char with table char
8BC6 AND #&df ; Case-insensitive (clear bit 5)
8BC8 BEQ decfir ; Match: continue comparing
8BCA DEX ; Mismatch: back up table pointer
8BCB .decmin←1← 8BCF BPL
INX ; Skip to end of table entry
8BCC LDA fs_cmd_match_table,x ; Load table byte
8BCF BPL decmin ; Loop until bit 7 set (end marker)
8BD1 LDA (fs_crc_lo),y ; Check input for '.' abbreviation
8BD3 INX ; Skip past handler high byte
8BD4 CMP #&2e ; Is input '.' (abbreviation)?
8BD6 BNE scan_cmd_table ; No: try next table entry
8BD8 INY ; Yes: skip '.' in input
8BD9 DEX ; Back to handler high byte
8BDA BCS decmor ; ALWAYS branch; dispatch via BMI
8BDC .dispatch_cmd←1← 8BC2 BMI
PHA ; Push handler address high byte
8BDD LDA cmd_table_entry_1,x ; Load handler address low byte
8BE0 PHA ; Push handler address low byte
8BE1 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" → &8BF8 (ex_handler: extended catalogue) "BYE"\r → &838D (bye_handler: logoff) <catch-all> → &80B4 (forward anything else to FS)

8BE2 .fs_cmd_match_table←2← 8BBF LDA← 8BCC LDA
EOR #&2e ; Match last char against '.' for *I. abbreviation
8BE4 EQUB &80, &B3
8BE6 EQUS "I AM"
8BEA EQUB &80
8BEB EQUS "}EX"
8BEE EQUB &8B, &F7
8BF0 EQUS "BYE"
8BF3 EQUB &0D, &83, &8C, &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).

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

8C00 .fscv_5_cat
LDX #3 ; X=3: column count for multi-column layout
8C02 STX fs_work_7 ; CRFLAG=3: first entry will trigger newline
8C04 LDY #0 ; Y=0: initialise column counter
8C06 STY fs_crflag ; Clear CRFLAG column counter
8C08 LDA #&0b ; A=&0B: examine argument count
8C0A .init_cat_params←1← 8BFE BNE
STA fs_work_5 ; Store examine argument count
8C0C LDA #6 ; A=6: examine format type in command
8C0E STA fs_cmd_data ; Store format type at &0F05
8C11 JSR parse_filename_gs_y ; Set up command parameter pointers
8C14 LDX #1 ; X=1: copy dir name at cmd offset 1
8C16 JSR copy_string_to_cmd ; Copy directory name to command buffer
8C19 LDY #&12 ; Y=function code for HDRFN
8C1B JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8C1E LDX #3 ; X=3: start printing from reply offset 3
8C20 JSR print_reply_bytes ; Print directory title (10 chars)
8C23 JSR print_inline ; Print '('
8C26 EQUS "("
8C27 LDA fs_reply_stn ; Load station number from FS reply
8C2A JSR print_decimal ; Print station number as decimal
8C2D JSR print_inline ; Print ') '
8C30 EQUS ") "
8C36 LDX fs_access_level ; Load access level from reply
8C39 BNE print_public ; Non-zero: Public access
8C3B JSR print_inline ; Print 'Owner' + CR
8C3E EQUS "Owner."
8C44 BNE cat_print_header ; ALWAYS branch past 'Owner'
8C46 .print_public←1← 8C39 BNE
JSR print_inline ; Print 'Public' + CR
8C49 EQUS "Public."
8C50 .cat_print_header←1← 8C44 BNE
LDY #&15 ; Y=function code for HDRFN
8C52 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8C55 INX ; X=1: past command code byte
8C56 LDY #&10 ; Y=&10: print 16 characters
8C58 JSR print_reply_counted ; Print disc/CSD name from reply
8C5B JSR print_inline ; Print ' Option '
8C5E EQUS " Option "
8C69 LDA fs_boot_option ; Load boot option from reply
8C6C TAX ; X = boot option for name table lookup
8C6D JSR print_hex ; Print boot option as hex digit
8C70 JSR print_inline ; Print ' ('
8C73 EQUS " ("
8C75 LDY boot_option_strings,x ; Y=string offset for this option
8C78 .print_option_char←1← 8C81 BNE
LDA boot_option_strings,y ; Load next char of option name
8C7B BMI done_option_name ; Bit 7 set: end of option name
8C7D JSR osasci ; Write character
8C80 INY ; Next character
8C81 BNE print_option_char ; Continue printing option name
8C83 .done_option_name←1← 8C7B BMI
JSR print_inline ; Print ')' + CR + 'Dir. '
8C86 EQUS ").Dir. "
8C8D LDX #&11 ; X=&11: Dir. name offset in reply
8C8F JSR print_reply_bytes ; Print directory name (10 chars)
8C92 JSR print_inline ; Print ' Lib. ' header
8C95 EQUS " Lib. "
8C9F LDX #&1b ; X=&1B: Lib. name offset in reply
8CA1 JSR print_reply_bytes ; Print library name
8CA4 JSR print_inline ; Print two CRs (blank line)
8CA7 EQUS ".."
8CA9 STY fs_func_code ; Store next examine start offset
8CAC STY fs_work_4 ; Save start offset in zero page for loop
8CAE LDX fs_work_5 ; Load examine arg count for batch size
8CB0 STX fs_data_count ; Store as request count at &0F07
8CB3 .cat_examine_loop←1← 8CDC BNE
LDX fs_work_7 ; Load column count for display format
8CB5 STX fs_cmd_data ; Store column count in command data
8CB8 LDX #3 ; X=3: copy directory name at offset 3
8CBA JSR copy_string_to_cmd ; Append directory name to examine command
8CBD LDY #3 ; Y=function code for HDRFN
8CBF JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8CC2 LDA fs_cmd_data ; Load entry count from reply
8CC5 BEQ print_newline ; Zero entries returned: catalogue done
8CC7 LDX #2 ; X=2: first entry offset in reply
8CC9 JSR print_dir_from_offset ; Print/format this directory entry
8CCC CLC ; CLC for addition
8CCD LDA fs_work_4 ; Load current examine start offset
8CCF ADC fs_cmd_data ; Add entries returned this batch
8CD2 STA fs_func_code ; Update next examine start offset
8CD5 STA fs_work_4 ; Save updated start offset
8CD7 LDA fs_work_5 ; Reload batch size for next request
8CD9 STA fs_data_count ; Store batch size in command buffer
8CDC 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 c8cde (&8CDE), whose first four bytes
; (starting with the ROR A opcode &6A) double as the offset ; table:
; &6A→&8D48 "Off", &7D→&8D5B "Load",
; &A5→&8D83 "Run", &18→&8CF6 "Exec"
; Each string is terminated by the next instruction's opcode
; having bit 7 set (e.g. LDA #imm = &A9, RTS = &60).
8CDE .boot_option_strings←2← 8C75 LDY← 8C78 LDA
ROR ; Boot option string offset table
8CDF ADC l18a5,x ; Dual-purpose: offset data + code
8CE2 JMP l212e ; Fallthrough (also boot string 'L.!')

Boot command strings for auto-boot

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

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

8CE5 .boot_cmd_strings
EQUS "BOOT"
8CE9 EQUB &0D
8CEA EQUS "E.!BOOT"
8CF1 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.

8CF2 .boot_option_offsets←1← 8E3B LDX
EQUB &F1 ; Opt 0 (Off): bare CR
8CF3 EQUB &E2 ; Opt 1 (Load): L.!BOOT
8CF4 EQUB &E4 ; Opt 2 (Run): !BOOT
8CF5 EQUB &EA ; Opt 3 (Exec): E.!BOOT
8CF6 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).

8CFA .print_file_info←2← 8730 JSR← 87B0 JSR
LDY fs_messages_flag ; Check if messages enabled
8CFD BEQ return_5 ; Zero: no info to display, return
8CFF LDY #0 ; Y=0: start of filename
8D01 LDX fs_cmd_csd ; Load CSD handle from reply
8D04 BEQ next_filename_char ; Zero: no dir prefix needed
8D06 JSR print_dir_from_offset ; Print directory prefix
8D09 BMI print_hex_fields ; ALWAYS: skip to hex field output
8D0B .next_filename_char←2← 8D04 BEQ← 8D19 BNE
LDA (fs_crc_lo),y ; Load next filename character
8D0D CMP #&0d ; CR: end of filename
8D0F BEQ pad_filename_spaces ; CR found: pad remaining with spaces
8D11 CMP #&20 ; Space: end of name field
8D13 BEQ pad_filename_spaces ; Space found: pad with spaces
8D15 JSR osasci ; Write character
8D18 INY ; Advance to next character
8D19 BNE next_filename_char ; Continue printing filename
8D1B .pad_filename_spaces←3← 8D0F BEQ← 8D13 BEQ← 8D21 BCC
JSR print_space ; Print space for padding
8D1E INY ; Advance column counter
8D1F CPY #&0c ; Reached 12 columns?
8D21 BCC pad_filename_spaces ; No: continue padding
8D23 .print_hex_fields←1← 8D09 BMI
LDY #5 ; Y=5: load address offset (4 bytes)
8D25 JSR print_hex_bytes ; Print load address
8D28 JSR print_exec_and_len ; Print exec address and file length
8D2B .print_newline←1← 8CC5 BEQ
JMP osnewl ; Write newline (characters 10 and 13)
8D2E .print_exec_and_len←1← 8D28 JSR
LDY #9 ; Y=9: exec address offset (4 bytes)
8D30 JSR print_hex_bytes ; Print exec address
8D33 LDY #&0c ; Y=&0C: file length offset
8D35 LDX #3 ; X=3: print 3 bytes (24-bit length)
8D37 BNE num01 ; ALWAYS branch
8D39 .print_hex_bytes←2← 8D25 JSR← 8D30 JSR
LDX #4 ; X=4: print 4 hex bytes
8D3B .num01←2← 8D37 BNE← 8D42 BNE
LDA (fs_options),y ; Load byte from parameter block
8D3D JSR print_hex ; Print as two hex digits
8D40 DEY ; Next byte (descending)
8D41 DEX ; Count down
8D42 BNE num01 ; Loop until 4 bytes printed
8D44 .print_space←1← 8D1B JSR
LDA #&20 ; A=space character
8D46 BNE print_char_always ; ALWAYS branch
8D48 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.

8D4B .copy_filename←4← 80B4 JSR← 86F8 JSR← 88BB JSR← 8DCA 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)
8D4D .copy_string_to_cmd←6← 87A6 JSR← 88B4 JSR← 88D6 JSR← 8996 JSR← 8C16 JSR← 8CBA JSR
LDY #0 ; Start copying from offset 0
8D4F .copy_string_from_offset←1← 8D58 BNE
LDA (fs_crc_lo),y ; Load next byte from source string
8D51 STA fs_cmd_data,x ; Store to FS command buffer (&0F05+X)
8D54 INX ; Advance write position
8D55 INY ; Advance source pointer
8D56 EOR #&0d ; XOR with CR: result=0 if byte was CR
8D58 BNE copy_string_from_offset ; Loop until CR copied
8D5A .return_5←2← 8CFD BEQ← 8D64 BMI
RTS ; Return; X = next free position in buffer
8D5B 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.

8D5F .fsreply_0_print_dir
LDX #0 ; X=0: start from first reply byte
8D61 .print_dir_from_offset←3← 8CC9 JSR← 8D06 JSR← 8D81 BNE
LDA fs_cmd_data,x ; Load byte from FS reply buffer
8D64 BMI return_5 ; Bit 7 set: end of string, return
8D66 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.

8D68 .cat_column_separator
LDY fs_crflag ; Null byte: check column counter
8D6A BMI print_cr_separator ; Negative: print CR (no columns)
8D6C INY ; Advance column counter
8D6D TYA ; Transfer to A for modulo
8D6E AND #3 ; Modulo 4 columns
8D70 STA fs_crflag ; Update column counter
8D72 BEQ print_cr_separator ; Column 0: start new line
8D74 JSR print_inline ; Print 2-space column separator
8D77 EQUS " "
8D79 BNE next_dir_entry ; More entries: skip final newline
8D7B .print_cr_separator←2← 8D6A BMI← 8D72 BEQ
LDA #&0d ; A=CR: print newline separator
8D7D .infol2←1← 8D66 BNE
JSR osasci ; Write character 13
8D80 .next_dir_entry←1← 8D79 BNE
INX ; Next byte in reply buffer
8D81 BNE print_dir_from_offset ; Loop until end of buffer
8D83 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)
8D86 .print_decimal←2← 8238 JSR← 8C2A JSR
TAY ; Y = value to print
8D87 LDA #&64 ; Divisor = 100 (hundreds digit)
8D89 JSR print_decimal_digit ; Print hundreds digit
8D8C LDA #&0a ; Divisor = 10 (tens digit)
8D8E JSR print_decimal_digit ; Print tens digit
8D91 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
8D93 .print_decimal_digit←2← 8D89 JSR← 8D8E JSR
STA fs_error_ptr ; Save divisor to workspace
8D95 TYA ; A = dividend (from Y)
8D96 LDX #&2f ; X = &2F = ASCII '0' - 1
8D98 SEC ; Prepare for subtraction
8D99 .divide_subtract←1← 8D9C BCS
INX ; Count one subtraction (next digit value)
8D9A SBC fs_error_ptr ; A = A - divisor
8D9C BCS divide_subtract ; Loop while A >= 0 (borrow clear)
8D9E ADC fs_error_ptr ; Undo last subtraction: A = remainder
8DA0 TAY ; Y = remainder for caller
8DA1 TXA ; A = X = ASCII digit character
8DA2 .print_digit←1← 8DB8 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)
8DA5 .print_hex←2← 8C6D JSR← 8D3D JSR
PHA ; Save byte for low nibble
8DA6 LSR ; Shift high nibble to low position
8DA7 LSR ; Shift high nibble to low position
8DA8 LSR ; Shift high nibble to low position
8DA9 LSR ; Shift high nibble to low position
8DAA JSR print_hex_nibble ; Print high nibble as hex digit
8DAD PLA ; Restore original byte
8DAE AND #&0f ; Mask to low nibble
8DB0 .print_hex_nibble←1← 8DAA JSR
ORA #&30 ; Convert to ASCII '0' base
8DB2 CMP #&3a ; Above '9'?
8DB4 BCC print_char_always ; No: digit 0-9, skip adjustment
8DB6 ADC #6 ; Add 7 (6+C) for 'A'-'F'
8DB8 .print_char_always←2← 8D46 BNE← 8DB4 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.

8DBA .print_reply_bytes←3← 8C20 JSR← 8C8F JSR← 8CA1 JSR
LDY #&0a ; Y=10: character count
8DBC .print_reply_counted←2← 8C58 JSR← 8DC4 BNE
LDA fs_cmd_data,x ; Load byte from FS reply buffer
8DBF JSR osasci ; Write character
8DC2 INX ; Next buffer offset
8DC3 DEY ; Decrement character counter
8DC4 BNE print_reply_counted ; Loop until all chars printed
8DC6 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.

8DC7 .fscv_2_star_run
JSR parse_filename_gs ; Parse filename from command line
8DCA 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.

8DCD .fsreply_4_notify_exec
LDX #&0e ; X=&0E: FS command buffer offset
8DCF STX fs_block_offset ; Store block offset for FS command
8DD1 LDA #&10 ; A=&10: 16 bytes of command data
8DD3 STA fs_options ; Store options byte
8DD5 STA fs_work_16 ; Store to FS workspace
8DD8 LDX #&4a ; X=&4A: TXCB size for load command
8DDA LDY #5 ; Y=5: FCCMND (load as command)
8DDC JSR send_fs_examine ; Send FS examine/load command
8DDF LDY #0 ; Y=0: init GSINIT string offset
8DE1 CLC ; CLC: no flags for GSINIT
8DE2 JSR gsinit ; Init string scanning state
8DE5 .gsread_scan_loop←1← 8DE8 BCC
JSR gsread ; Read next char via GSREAD
8DE8 BCC gsread_scan_loop ; More chars: continue scanning
8DEA DEY ; Back up Y to last valid char
8DEB .skip_filename_spaces←1← 8DF0 BEQ
INY ; Advance past current position
8DEC LDA (os_text_ptr),y ; Read char from command line
8DEE CMP #&20 ; Is it a space?
8DF0 BEQ skip_filename_spaces ; Skip leading spaces in filename
8DF2 CLC ; CLC for pointer addition
8DF3 TYA ; A = Y (offset past spaces)
8DF4 ADC os_text_ptr ; Add base pointer to get abs addr
8DF6 STA fs_cmd_context ; Store filename pointer (low)
8DF9 LDA os_text_ptr_hi ; Load text pointer high byte
8DFB ADC #0 ; Add carry from low byte addition
8DFD STA fs_context_hi ; Store filename pointer (high)
8E00 SEC ; SEC for address range test
8E01 LDA tx_in_progress ; Check for Tube co-processor
8E04 BEQ exec_at_load_addr ; No Tube: execute locally
8E06 ADC fs_load_upper ; Check load address upper bytes
8E09 ADC fs_addr_check ; Continue address range check
8E0C BCS exec_at_load_addr ; Carry set: not Tube space, exec locally
8E0E JSR tube_claim_loop ; Claim Tube transfer channel
8E11 LDX #9 ; X=9: source offset in FS reply
8E13 LDY #&0f ; Y=&0F: page &0F (FS command buffer)
8E15 LDA #4 ; A=4: Tube transfer type 4 (256-byte)
8E17 JMP tube_addr_claim ; Transfer data to Tube co-processor
8E1A .exec_at_load_addr←2← 8E04 BEQ← 8E0C 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
8E1D .fsreply_5_set_lib
STY fs_lib_handle ; Save library handle from FS reply
8E20 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
8E22 .fsreply_3_set_csd
STY fs_csd_handle ; Store CSD handle from FS reply
8E25 .jmp_restore_args←2← 8E20 BNE← 8E36 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.

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

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

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

8E43 .net_1_read_handle
LDY #&6f ; Y=&6F: offset to handle byte in RX buf
8E45 LDA (net_rx_ptr),y ; Load file handle from RX buffer
8E47 STA osword_pb_ptr ; Store in parameter block pointer (&F0)
8E49 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.

8E4A .load_handle_calc_offset←2← 8E5E JSR← 8E6E 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
8E4C .calc_handle_offset←3← 82F9 JSR← 8F7F JSR← 8F98 JSR
ASL ; A = handle * 2
8E4D ASL ; A = handle * 4
8E4E PHA ; Push handle*4 onto stack
8E4F ASL ; A = handle * 8
8E50 TSX ; X = stack pointer
8E51 ADC l0101,x ; A = handle*8 + handle*4 = handle*12
8E54 TAY ; Y = offset into handle workspace
8E55 PLA ; Clean up stack (discard handle*4)
8E56 CMP #&48 ; Offset >= &48? (6 handles max)
8E58 BCC return_6 ; Valid: return with C clear
8E5A LDY #0 ; Invalid: Y = 0
8E5C TYA ; A = 0, C set (error) A=&00
8E5D .return_6←1← 8E58 BCC
.return_calc_handle←1← 8E58 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 fs_temp_ce on exit.

8E5E .net_2_read_handle_entry
JSR load_handle_calc_offset ; Look up handle &F0 in workspace
8E61 BCS rxpol2 ; Invalid handle: return 0
8E63 LDA (nfs_workspace),y ; Load stored handle value
8E65 CMP #&3f ; &3F = unused/closed slot marker
8E67 BNE store_handle_return ; Slot in use: return actual value
8E69 .rxpol2←2← 8E61 BCS← 8E71 BCS
LDA #0 ; Return 0 for closed/invalid handle
8E6B .store_handle_return←1← 8E67 BNE
STA osword_pb_ptr ; Store result back to &F0
8E6D 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 fs_temp_ce on exit.

8E6E .net_3_close_handle
JSR load_handle_calc_offset ; Look up handle &F0 in workspace
8E71 BCS rxpol2 ; Invalid handle: return 0
8E73 ROL rx_flags ; Preserve carry via ROL
8E76 LDA #&3f ; A=&3F: handle closed/unused marker
8E78 STA (nfs_workspace),y ; Mark handle as closed in workspace
fall through ↓

Restore RX flags after close handle

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

; 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.
8E7A .restore_rx_flags
ROR rx_flags ; Restore carry via ROR
8E7D RTS ; Return
fall through ↓

Filing system OSWORD entry

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

8E7E .svc_8_osword
LDA osbyte_a_copy ; Command code from &EF
8E80 SBC #&0f ; Subtract &0F: OSWORD &0F-&13 become indices 0-4
8E82 BMI return_7 ; Outside our OSWORD range, exit
8E84 CMP #5 ; Only OSWORDs &0F-&13 (index 0-4)
8E86 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 &8EA7 (low) / &8EAC (high).

8E88 .fs_osword_dispatch
TAX ; X = sub-function code for table lookup
8E89 LDA #&8f ; Push return addr high (restore_args)
8E8B PHA ; Push return addr high byte
8E8C LDA #&c6 ; Push return addr low (restore_args)
8E8E PHA ; Push return addr low byte
8E8F LDA osword_0f_tbl_hi,x ; Load handler address high byte from table
8E92 PHA ; Push high byte for RTS dispatch
8E93 LDA osword_0f_tbl_lo,x ; Load handler address low byte from table
8E96 PHA ; Dispatch table: low bytes for OSWORD &0F-&13 handlers
8E97 LDY #2 ; Y=2: save 3 bytes (&AA-&AC)
8E99 .save1←1← 8E9F BPL
LDA osword_flag,y ; Load param block pointer byte
8E9C STA (net_rx_ptr),y ; Save to NFS workspace via (net_rx_ptr)
8E9E DEY ; Next byte (descending)
8E9F BPL save1 ; Loop for all 3 bytes
8EA1 INY ; Y=0 after BPL exit; INY makes Y=1
8EA2 LDA (osword_pb_ptr),y ; Read sub-function code from (&F0)+1
8EA4 STY rom_svc_num ; Store Y=1 to &A9
8EA6 RTS ; RTS dispatches to pushed handler address
8EA7 .osword_0f_tbl_lo←1← 8E93 LDA
EQUB <(osword_0f_handler-1) ; Dispatch table: low bytes for OSWORD &0F-&13 handlers
8EA8 EQUB <(osword_10_handler-1)
8EA9 .fs_osword_tbl_lo
EQUB <(osword_11_handler-1)
8EAA EQUB <(osword_12_dispatch-1)
8EAB EQUB <(econet_tx_rx-1)
8EAC .osword_0f_tbl_hi←1← 8E8F LDA
EQUB >(osword_0f_handler-1) ; Dispatch table: high bytes for OSWORD &0F-&13 handlers
8EAD EQUB >(osword_10_handler-1)
8EAE .fs_osword_tbl_hi
EQUB >(osword_11_handler-1)
8EAF EQUB >(osword_12_dispatch-1)
8EB0 EQUB >(econet_tx_rx-1)

Bidirectional block copy between OSWORD param block and workspace.

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

8EB1 .copy_param_block←5← 8EBD BPL← 8ED4 JSR← 8EE9 JSR← 8F1A JMP← 8FAF JSR
BCC load_workspace_byte ; C=0: skip param-to-workspace copy
8EB3 LDA (osword_pb_ptr),y ; Load byte from param block
8EB5 STA (ws_ptr_lo),y ; Store to workspace
8EB7 .load_workspace_byte←1← 8EB1 BCC
LDA (ws_ptr_lo),y ; Load byte from workspace
8EB9 STA (osword_pb_ptr),y ; Store to param block (no-op if C=1)
8EBB .copyl3
INY ; Advance to next byte
8EBC DEX ; Decrement byte counter
8EBD BPL copy_param_block ; Loop while X >= 0
8EBF .return_7←2← 8E82 BMI← 8E86 BCS
.logon3←2← 8E82 BMI← 8E86 BCS
.return_copy_param←2← 8E82 BMI← 8E86 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
8EC0 .osword_0f_handler
ASL tx_clear_flag ; ASL: set C if TX in progress
8EC3 TYA ; Save param block high byte to A
8EC4 BCC readry ; C=0: read path
8EC6 LDA net_rx_ptr_hi ; User TX CB in workspace page (high byte)
8EC8 STA ws_ptr_hi ; Set param block high byte
8ECA STA nmi_tx_block_hi ; Set LTXCBP high byte for low-level TX
8ECC LDA #&6f ; &6F: offset into workspace for user TXCB
8ECE STA ws_ptr_lo ; Set param block low byte
8ED0 STA nmi_tx_block ; Set LTXCBP low byte for low-level TX
8ED2 LDX #&0f ; X=15: copy 16 bytes (OSWORD param block)
8ED4 JSR copy_param_block ; Copy param block to user TX control block
8ED7 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 (&8E6B) to return just the buffer size and args size without copying the data.

8EDA .osword_11_handler
LDA net_rx_ptr_hi ; Set source high byte from workspace page
8EDC STA ws_ptr_hi ; Store as copy source high byte in &AC
8EDE LDY #&7f ; JSRSIZ at workspace offset &7F
8EE0 LDA (net_rx_ptr),y ; Load buffer size from workspace
8EE2 INY ; Y=&80: start of JSR argument data Y=&80
8EE3 STY ws_ptr_lo ; Store &80 as copy source low byte
8EE5 TAX ; X = buffer size (loop counter)
8EE6 DEX ; X = size-1 (0-based count for copy)
8EE7 LDY #0 ; Y=0: start of destination param block
8EE9 JSR copy_param_block ; Copy X+1 bytes from workspace to param
8EEC JMP clear_jsr_protection ; Clear JSR protection status (CLRJSR)
8EEF .read_args_size←1← 8F3E BEQ
LDY #&7f ; Y=&7F: JSRSIZ offset (READRB entry)
8EF1 LDA (net_rx_ptr),y ; Load buffer size from workspace
8EF3 LDY #1 ; Y=1: param block offset for size byte
8EF5 STA (osword_pb_ptr),y ; Store buffer size to (&F0)+1
8EF7 INY ; Y=2: param block offset for args size Y=&02
8EF8 LDA #&80 ; A=&80: argument data starts at offset &80
8EFA .readry←1← 8EC4 BCC
STA (osword_pb_ptr),y ; Store args start offset to (&F0)+2
8EFC RTS ; Return
8EFD .osword_12_offsets←1← 8F11 LDA
EQUB &FF, &01

OSWORD &12 sub-function dispatcher

Dispatches sub-functions 0-9 for OSWORD &12 (read/set FS state). Functions 0-3 read/set station and printer server addresses; 4-5 read/set protection masks; 6-7 read/set context handles (URD/CSD/LIB); 8 reads local station; 9 reads JSR argument buffer size.

8EFF .osword_12_dispatch
CMP #6 ; Sub-function >= 6?
8F01 BCS rsl1 ; Yes: jump to sub 6-9 handler
8F03 CMP #4 ; Sub-function >= 4?
8F05 BCS rssl1 ; Sub-function 4 or 5: read/set protection
8F07 LSR ; LSR: 0->0, 1->0, 2->1, 3->1
8F08 LDX #&0d ; X=&0D: default to static workspace page
8F0A TAY ; Transfer LSR result to Y for indexing
8F0B BEQ set_workspace_page ; Y=0 (sub 0-1): use page &0D
8F0D LDX nfs_workspace_hi ; Y=1 (sub 2-3): use dynamic workspace
8F0F .set_workspace_page←1← 8F0B BEQ
STX ws_ptr_hi ; Store workspace page in &AC (hi byte)
8F11 LDA osword_12_offsets,y ; Load offset: &FF (sub 0-1) or &01 (sub 2-3)
8F14 STA ws_ptr_lo ; Store offset in &AB (lo byte)
8F16 LDX #1 ; X=1: copy 2 bytes
8F18 LDY #1 ; Y=1: start at param block offset 1
8F1A JMP copy_param_block ; Jump to read/write workspace path
8F1D .rssl1←1← 8F05 BCS
LSR ; LSR A: test bit 0 of sub-function
8F1E INY ; Y=1: offset for protection byte
8F1F LDA (osword_pb_ptr),y ; Load protection byte from param block
8F21 BCS rssl2 ; C=1 (odd sub): set protection
8F23 LDA prot_status ; C=0 (even sub): read current status
8F26 STA (osword_pb_ptr),y ; Return current value to param block
8F28 .rssl2←1← 8F21 BCS
STA prot_status ; Update protection status
8F2B STA saved_jsr_mask ; Also save as JSR mask backup
8F2E RTS ; Return
8F2F .read_fs_handle←1← 8F3A BEQ
LDY #&14 ; Y=&14: RX buffer offset for FS handle
8F31 LDA (net_rx_ptr),y ; Read FS reply handle from RX data
8F33 LDY #1 ; Y=1: param block byte 1
8F35 STA (osword_pb_ptr),y ; Return handle to caller's param block
8F37 RTS ; Return
8F38 .rsl1←1← 8F01 BCS
CMP #8 ; Sub-function 8: read FS handle
8F3A BEQ read_fs_handle ; Match: read handle from RX buffer
8F3C CMP #9 ; Sub-function 9: read args size
8F3E BEQ read_args_size ; Match: read ARGS buffer info
8F40 BPL return_last_error ; Sub >= 10 (bit 7 clear): read error
8F42 LDY #3 ; Y=3: start from handle 3 (descending)
8F44 LSR ; LSR: test read/write bit
8F45 BCC readc1 ; C=0: read handles from workspace
8F47 STY nfs_temp ; Init loop counter at Y=3
8F49 .copy_handles_to_ws←1← 8F58 BNE
LDY nfs_temp ; Reload loop counter
8F4B LDA (osword_pb_ptr),y ; Read handle from caller's param block
8F4D JSR handle_to_mask_a ; Convert handle number to bitmask
8F50 TYA ; TYA: get bitmask result
8F51 LDY nfs_temp ; Reload loop counter
8F53 STA fs_server_net,y ; Store bitmask to FS server table
8F56 DEC nfs_temp ; Next handle (descending)
8F58 BNE copy_handles_to_ws ; Loop for handles 3,2,1
8F5A RTS ; Return
8F5B .return_last_error←1← 8F40 BPL
INY ; Y=1 (post-INY): param block byte 1
8F5C LDA fs_last_error ; Read last FS error code
8F5F STA (osword_pb_ptr),y ; Return error to caller's param block
8F61 RTS ; Return
8F62 .readc1←2← 8F45 BCC← 8F6B BNE
LDA fs_server_net,y ; A=single-bit bitmask
8F65 JSR mask_to_handle ; Convert bitmask to handle number (FS2A)
8F68 STA (osword_pb_ptr),y ; A=handle number (&20-&27) Y=preserved
8F6A DEY ; Next handle (descending)
8F6B BNE readc1 ; Loop for handles 3,2,1
8F6D 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
8F6E .osword_10_handler
LDX nfs_workspace_hi ; Workspace page high byte
8F70 STX ws_ptr_hi ; Set up pointer high byte in &AC
8F72 STY ws_ptr_lo ; Save param block high byte in &AB
8F74 ROR rx_flags ; Disable user RX during CB operation
8F77 LDA (osword_pb_ptr),y ; Read first byte of param block
8F79 STA osword_flag ; Save: 0=open new, non-zero=read RXCB
8F7B .scan_or_read_rxcb
BNE read_rxcb ; Non-zero: read specified RXCB
8F7D LDA #3 ; Start scan from RXCB #3 (0-2 reserved)
8F7F .scan0←1← 8F91 BNE
JSR calc_handle_offset ; Convert RXCB number to workspace offset
8F82 BCS openl4 ; Invalid RXCB: return zero
8F84 LSR ; LSR twice: byte offset / 4
8F85 LSR ; Yields RXCB number from offset
8F86 TAX ; X = RXCB number for iteration
8F87 LDA (ws_ptr_lo),y ; Read flag byte from RXCB workspace
8F89 BEQ openl4 ; Zero = end of CB list
8F8B CMP #&3f ; &3F = deleted slot, free for reuse
8F8D BEQ scan1 ; Found free slot
8F8F INX ; Try next RXCB
8F90 TXA ; A = next RXCB number
8F91 BNE scan0 ; Continue scan (always branches)
8F93 .scan1←1← 8F8D BEQ
TXA ; A = free RXCB number
8F94 LDX #0 ; X=0 for indexed indirect store
8F96 STA (osword_pb_ptr,x) ; Return RXCB number to caller's byte 0
8F98 .read_rxcb←1← 8F7B BNE
JSR calc_handle_offset ; Convert RXCB number to workspace offset
8F9B BCS openl4 ; Invalid: write zero to param block
8F9D DEY ; Y = offset-1: points to flag byte
8F9E STY ws_ptr_lo ; Set &AB = workspace ptr low byte
8FA0 LDA #&c0 ; &C0: test mask for flag byte
8FA2 LDY #1 ; Y=1: flag byte offset in RXCB
8FA4 LDX #&0b ; Enable interrupts before transmit
8FA6 CPY osword_flag ; Compare Y(1) with saved byte (open/read)
8FA8 ADC (ws_ptr_lo),y ; ADC flag: test if slot is in use
8FAA BEQ openl6 ; Dest station = &FFFF (accept reply from any station)
8FAC BMI openl7 ; Negative: slot has received data
8FAE .copy_rxcb_to_param←1← 8FBE BNE
CLC ; C=0: workspace-to-param direction
8FAF .openl6←1← 8FAA BEQ
JSR copy_param_block ; Copy RXCB data to param block
8FB2 BCS reenable_rx ; Done: skip deletion on error
8FB4 LDA #&3f ; Mark CB as consumed (consume-once)
8FB6 LDY #1 ; Y=1: flag byte offset
8FB8 STA (ws_ptr_lo),y ; Write &3F to mark slot deleted
8FBA BNE reenable_rx ; Branch to exit (always taken) ALWAYS branch
8FBC .openl7←1← 8FAC BMI
ADC #1 ; Advance through multi-byte field
8FBE BNE copy_rxcb_to_param ; Loop until all bytes processed
8FC0 DEY ; Y=-1 → Y=0 after STA below
8FC1 .openl4←3← 8F82 BCS← 8F89 BEQ← 8F9B BCS
STA (osword_pb_ptr),y ; Return zero (no free RXCB found)
8FC3 .reenable_rx←2← 8FB2 BCS← 8FBA BNE
ROL rx_flags ; Re-enable user RX
8FC6 RTS ; Return
8FC7 EQUB &A0, &02 ; Y=2: copy 3 bytes (indices 2,1,0)
8FC9 .rest1
EQUB &B1, &9C, &99, ; Load saved arg from (net_rx_ptr)+Y &AA, &00, &88, ; Restore saved OSWORD argument byte &10, &F8, &60 ; Decrement byte counter Loop for bytes ; 2,1,0 Return to caller

Set up RX buffer pointers in NFS workspace

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

On EntryCclear for ADC
8FD2 .setup_rx_buffer_ptrs←1← 9005 JSR
LDY #&1c ; Y=&1C: RXCB template offset
8FD4 LDA osword_pb_ptr ; A = base address low byte
8FD6 ADC #1 ; A = base + 1 (skip length byte)
8FD8 JSR store_16bit_at_y ; Receive data blocks until command byte = &00 or &0D
8FDB LDY #1 ; Read data length from (&F0)+1
8FDD LDA (osword_pb_ptr),y ; A = data length byte
8FDF LDY #&20 ; Workspace offset &20 = RX data end
8FE1 ADC osword_pb_ptr ; A = base + length = end address low
8FE3 .store_16bit_at_y←1← 8FD8 JSR
STA (nfs_workspace),y ; Store low byte of 16-bit address
8FE5 INY ; Advance to high byte offset
8FE6 LDA osword_pb_ptr_hi ; A = high byte of base address
8FE8 ADC #0 ; Add carry for 16-bit addition
8FEA STA (nfs_workspace),y ; Store high byte
8FEC RTS ; Return

Econet transmit/receive handler

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

On EntryA0=set up and transmit, >=1=handle TX result
8FED .econet_tx_rx
CMP #1 ; A=0: set up and transmit; A>=1: handle result
8FEF BCS handle_tx_result ; A >= 1: handle TX result
8FF1 LDY #&23 ; Y=&23: start of template (descending)
8FF3 .dofs01←1← 9000 BNE
LDA init_tx_ctrl_block,y ; Load from ROM template (zero = use NMI workspace value)
8FF6 BNE store_txcb_byte ; Non-zero = use ROM template byte as-is
8FF8 LDA nmi_sub_table,y ; Zero = substitute from NMI workspace
8FFB .store_txcb_byte←1← 8FF6 BNE
STA (nfs_workspace),y ; Store to dynamic workspace
8FFD DEY ; Descend through template
8FFE CPY #&17 ; Stop at offset &17
9000 BNE dofs01 ; Loop until all bytes copied
9002 INY ; Y=&18: TX block starts here
9003 STY net_tx_ptr ; Point net_tx_ptr at workspace+&18
9005 JSR setup_rx_buffer_ptrs ; Set up RX buffer start/end pointers
9008 LDY #2 ; Y=2: port byte offset in RXCB
900A LDA #&90 ; A=&90: FS reply port
900C STA (osword_pb_ptr),y ; Store port &90 at (&F0)+2
900E INY ; Y=&03
900F INY ; Y=&04: advance to station address Y=&04
9010 .copy_fs_addr←1← 9018 BNE
LDA fs_context_base,y ; Copy FS station addr from workspace
9013 STA (osword_pb_ptr),y ; Store to RX param block
9015 INY ; Next byte
9016 CPY #7 ; Done 3 bytes (Y=4,5,6)?
9018 BNE copy_fs_addr ; No: continue copying
901A LDA nfs_workspace_hi ; High byte of workspace for TX ptr
901C STA net_tx_ptr_hi ; Store as TX pointer high byte
901E CLI ; Enable interrupts before transmit
901F JSR tx_poll_ff ; Transmit with full retry
9022 LDY #&20 ; Y=&20: RX end address offset
9024 LDA #&ff ; Dest station = &FFFF (accept reply from any station)
9026 STA (nfs_workspace),y ; Store end address low byte (&FF)
9028 INY ; Y=&21
9029 STA (nfs_workspace),y ; Store end address high byte (&FF)
902B LDY #&19 ; Y=&19: port byte in workspace RXCB
902D LDA #&90 ; A=&90: FS reply port
902F STA (nfs_workspace),y ; Store port to workspace RXCB
9031 DEY ; Y=&18
9032 LDA #&7f ; A=&7F: flag byte = waiting for reply
9034 STA (nfs_workspace),y ; Store flag byte to workspace RXCB
9036 JMP send_to_fs_star ; Jump to RX poll (BRIANX)
9039 .handle_tx_result←1← 8FEF BCS
PHP ; Save processor flags
903A LDY #1 ; Y=1: first data byte offset Y=character to write
903C 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
903E .net_write_char
TAX ; X = first data byte (command code)
903F INY ; Advance to next data byte
9040 LDA (osword_pb_ptr),y ; Load station address high byte
9042 INY ; Advance past station addr
9043 STY ws_ptr_lo ; Save Y as data index
9045 LDY #&72 ; Store station addr hi at (net_rx_ptr)+&72
9047 STA (net_rx_ptr),y ; Store to workspace
9049 DEY ; Y=&71
904A TXA ; A = command code (from X)
904B STA (net_rx_ptr),y ; Store station addr lo at (net_rx_ptr)+&71
904D PLP ; Restore flags from earlier PHP
904E BNE dofs2 ; First call: adjust data length
9050 .send_data_bytes←1← 906A BNE
LDY ws_ptr_lo ; Receive data blocks until command byte = &00 or &0D
9052 INC ws_ptr_lo ; Advance data index for next iteration
9054 LDA (osword_pb_ptr),y ; Load next data byte
9056 LDY #&7d ; Y=&7D: store byte for TX at offset &7D
9058 STA (net_rx_ptr),y ; Store data byte at (net_rx_ptr)+&7D for TX
905A PHA ; Save data byte for &0D check after TX
905B JSR ctrl_block_setup_alt ; Set up TX control block
905E CLI ; Enable interrupts for TX
905F JSR tx_poll_core ; Enable IRQs and transmit Core transmit and poll routine (XMIT)
9062 .delay_between_tx←1← 9063 BNE
DEX ; Short delay loop between TX packets
9063 BNE delay_between_tx ; Spin until X reaches 0
9065 PLA ; Restore data byte for terminator check
9066 BEQ return_8 ; Z=1: not intercepted, pass through
9068 EOR #&0d ; Test for end-of-data marker (&0D)
906A BNE send_data_bytes ; Not &0D: continue with next byte
906C .return_8←1← 9066 BEQ
RTS ; Return (data complete)
906D .dofs2←1← 904E BNE
JSR ctrl_block_setup_alt ; First-packet: set up control block
9070 LDY #&7b ; Y=&7B: data length offset
9072 LDA (net_rx_ptr),y ; Load current data length
9074 ADC #3 ; Adjust data length by 3 for header bytes
9076 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.

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

90B2 .nwrch_handler
TSX ; Get stack pointer for P register
90B3 ROR l0106,x ; ROR/ASL on stacked P: zeros carry to signal success
90B6 ASL l0106,x ; ASL: restore P after ROR zeroed carry
90B9 TYA ; Y = character to write
90BA LDY #&da ; Store character at workspace offset &DA
90BC STA (nfs_workspace),y ; Store char at workspace offset &DA
90BE 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
90C0 .setup_tx_and_send←3← 819D JSR← 9107 JSR← 9168 JSR
LDY #&d9 ; Y=&D9: command type offset
90C2 STA (nfs_workspace),y ; Store command type at ws+&D9
90C4 LDA #&80 ; Mark TX control block as active (&80)
90C6 LDY #&0c ; Y=&0C: TXCB start offset
90C8 STA (nfs_workspace),y ; Set TX active flag at ws+&0C
90CA STY net_tx_ptr ; Redirect net_tx_ptr low to workspace
90CC LDX nfs_workspace_hi ; Load workspace page high byte
90CE STX net_tx_ptr_hi ; Complete ptr redirect
90D0 JSR tx_poll_ff ; Transmit with full retry
90D3 LDA #&3f ; Mark TXCB as deleted (&3F) after transmit
90D5 STA (net_tx_ptr,x) ; Write &3F to TXCB byte 0
90D7 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.

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

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

Fn 8: remote OSWORD handler (NWORD)

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

9142 .remote_cmd_data
LDY #&0e ; Y=14: max OSWORD parameter bytes
9144 CMP #7 ; OSWORD 7 = make a sound
9146 BEQ copy_params_rword ; OSWORD 7 (sound): handle via common path
9148 CMP #8 ; OSWORD 8 = define an envelope
914A BNE return_match_osbyte ; Not OSWORD 7 or 8: ignore (BNE exits)
914C .copy_params_rword←1← 9146 BEQ
LDX #&db ; Point workspace to offset &DB for params
914E STX nfs_workspace ; Store workspace ptr offset &DB
9150 .copy_osword_params←1← 9155 BPL
LDA (osword_pb_ptr),y ; Load param byte from OSWORD param block
9152 STA (nfs_workspace),y ; Write param byte to workspace
9154 DEY ; Next byte (descending)
9155 BPL copy_osword_params ; Loop for all parameter bytes
9157 INY ; Y=0 after loop
9158 DEC nfs_workspace ; Point workspace to offset &DA
915A LDA osbyte_a_copy ; Load original OSWORD code
915C STA (nfs_workspace),y ; Store OSWORD code at ws+0
915E STY nfs_workspace ; Reset workspace ptr to base
9160 LDY #&14 ; Y=&14: command type offset
9162 LDA #&e9 ; Tag as RWORD (port &E9)
9164 STA (nfs_workspace),y ; Store port tag at ws+&14
9166 LDA #1 ; A=1: single-byte TX
9168 JSR setup_tx_and_send ; Load template byte from ctrl_block_template[X]
916B STX nfs_workspace ; Restore workspace ptr
fall through ↓

Alternate entry into control block setup

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

916D .ctrl_block_setup_alt←2← 905B JSR← 906D JSR
LDX #&0d ; X=&0D: template offset for alt entry
916F LDY #&7c ; Y=&7C: target workspace offset for alt entry
9171 BIT tx_ctrl_upper ; BIT test: V flag = bit 6 of &837E
9174 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 &91A2 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

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

Control block initialisation template

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

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

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)

91A2 .ctrl_block_template←1← 917B LDA
EQUB &85 ; Alt-path only → Y=&6F
91A3 EQUB &00 ; Alt-path only → Y=&70
91A4 EQUB &FD ; SKIP
91A5 EQUB &FD ; SKIP
91A6 EQUB &7D ; → Y=&01 / Y=&73
91A7 EQUB &FC ; PAGE byte → Y=&02 / Y=&74
91A8 EQUB &FF ; → Y=&03 / Y=&75
91A9 EQUB &FF ; → Y=&04 / Y=&76
91AA EQUB &7E ; → Y=&05 / Y=&77
91AB EQUB &FC ; PAGE byte → Y=&06 / Y=&78
91AC EQUB &FF ; → Y=&07 / Y=&79
91AD EQUB &FF ; → Y=&08 / Y=&7A
91AE EQUB &00 ; → Y=&09 / Y=&7B
91AF EQUB &00 ; → Y=&0A / Y=&7C
91B0 EQUB &FE ; STOP — main-path boundary
91B1 EQUB &80 ; → Y=&0C (main only)
91B2 EQUB &93 ; → Y=&0D (main only)
91B3 EQUB &FD ; SKIP (main only)
91B4 EQUB &FD ; SKIP (main only)
91B5 EQUB &D9 ; → Y=&10 (main only)
91B6 EQUB &FC ; PAGE byte → Y=&11 (main only)
91B7 EQUB &FF ; → Y=&12 (main only)
91B8 EQUB &FF ; → Y=&13 (main only)
91B9 EQUB &DE ; → Y=&14 (main only)
91BA EQUB &FC ; PAGE byte → Y=&15 (main only)
91BB EQUB &FF ; → Y=&16 (main only)
91BC EQUB &FF ; → Y=&17 (main only)
91BD 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
91C9 .printer_select_handler
DEX ; X-1: convert 1-based buffer to 0-based
91CA CPX osword_pb_ptr ; Is this the network printer buffer?
91CC BNE setup1 ; No: skip printer init
91CE LDA #&1f ; &1F = initial buffer pointer offset
91D0 STA printer_buf_ptr ; Reset printer buffer write position
91D3 LDA #&41 ; &41 = initial PFLAGS (bit 6 set, bit 0 set)
91D5 .setup1←1← 91CC BNE
STA l0d60 ; Store initial PFLAGS value
91D8 .return_printer_select←2← 91DB BNE← 91EF 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)
91D9 .remote_print_handler
CPY #4 ; Only handle buffer 4 (network printer)
91DB BNE return_printer_select ; Not buffer 4: ignore
91DD TXA ; A = reason code
91DE DEX ; Reason 1? (DEX: 1->0)
91DF BNE toggle_print_flag ; Not reason 1: handle Ctrl-B/C
91E1 TSX ; Get stack pointer for P register
91E2 ORA l0106,x ; Force I flag in stacked P to block IRQs
91E5 STA l0106,x ; Write back modified P register
91E8 .prlp1←2← 91F7 BCC← 91FC BCC
LDA #osbyte_read_buffer ; OSBYTE &91: extract char from MOS buffer
91EA LDX #buffer_printer ; X=3: printer buffer number
91EC JSR osbyte ; Get character from input buffer (C is set if the buffer is empty, otherwise Y=extracted character)
91EF BCS return_printer_select ; Buffer empty: return
91F1 TYA ; Y = extracted character Y is the character extracted from the buffer
91F2 JSR store_output_byte ; Store char in output buffer
91F5 CPY #&6e ; Buffer nearly full? (&6E = threshold)
91F7 BCC prlp1 ; Not full: get next char
91F9 JSR flush_output_block ; Buffer full: flush to network
91FC 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
91FE .store_output_byte←2← 91F2 JSR← 920B JSR
LDY printer_buf_ptr ; Y = current printer buffer offset
9201 STA (net_rx_ptr),y ; Store byte at current position
9203 INC printer_buf_ptr ; Advance buffer pointer
9206 RTS ; Return; Y = buffer offset
9207 .toggle_print_flag←1← 91DF BNE
PHA ; Save reason code
9208 TXA ; A = reason code
9209 EOR #1 ; EOR #1: toggle print-active flag (bit 0)
920B JSR store_output_byte ; Store toggled flag as output byte
920E EOR l0d60 ; XOR with current PFLAGS
9211 ROR ; Test if sequence changed (bit 7 mismatch)
9212 BCC pril1 ; Sequence unchanged: skip flush
9214 ROL ; Undo ROR
9215 STA l0d60 ; Store toggled PFLAGS
9218 JSR flush_output_block ; Flush current output block
921B .pril1←1← 9212 BCC
LDA l0d60 ; Reload current PFLAGS
921E AND #&f0 ; Extract upper nibble of PFLAGS
9220 ROR ; Shift for bit extraction
9221 TAX ; Save in X
9222 PLA ; Restore original reason code
9223 ROR ; Merge print-active bit from original A
9224 TXA ; Retrieve shifted PFLAGS
9225 ROL ; Recombine into new PFLAGS value
9226 STA l0d60 ; Store recombined PFLAGS value
9229 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.

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

Save palette and VDU state (CVIEW)

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

92A4 .lang_2_save_palette_vdu
LDA table_idx ; Save current table index
92A6 PHA ; Push for later restore
92A7 LDA #&e9 ; Point workspace to palette save area (&E9)
92A9 STA nfs_workspace ; Set workspace low byte
92AB LDY #0 ; Y=0: first palette entry
92AD STY table_idx ; Clear table index counter
92AF LDA l0350 ; Save current screen MODE to workspace
92B2 STA (nfs_workspace),y ; Store MODE at workspace[0]
92B4 INC nfs_workspace ; Advance workspace pointer past MODE byte
92B6 LDA l0351 ; Read colour count (from &0351)
92B9 PHA ; Push for iteration count tracking
92BA TYA ; A=0: logical colour number for OSWORD A=&00
92BB .save_palette_entry←1← 92DA BNE
STA (nfs_workspace),y ; Store logical colour at workspace[0]
92BD LDX nfs_workspace ; X = workspace ptr low (param block addr)
92BF LDY nfs_workspace_hi ; Y = workspace ptr high
92C1 LDA #osword_read_palette ; OSWORD &0B: read palette for logical colour
92C3 JSR osword ; Read palette
92C6 PLA ; Recover colour count
92C7 LDY #0 ; Y=0: access workspace[0]
92C9 STA (nfs_workspace),y ; Write colour count back to workspace[0]
92CB INY ; Y=1: access workspace[1] (palette result) Y=&01
92CC LDA (nfs_workspace),y ; Read palette value returned by OSWORD
92CE PHA ; Push palette value for next iteration
92CF LDX nfs_workspace ; X = current workspace ptr low
92D1 INC nfs_workspace ; Advance workspace pointer
92D3 INC table_idx ; Increment table index
92D5 DEY ; Y=0 for next store Y=&00
92D6 LDA table_idx ; Load table index as logical colour
92D8 CPX #&f9 ; Loop until workspace wraps past &F9
92DA BNE save_palette_entry ; Continue for all 16 palette entries
92DC PLA ; Discard last palette value from stack
92DD STY table_idx ; Reset table index to 0
92DF INC nfs_workspace ; Advance workspace past palette data
92E1 JSR save_vdu_state ; Save cursor pos and OSBYTE state values
92E4 INC nfs_workspace ; Advance workspace past VDU state data
92E6 PLA ; Recover saved table index
92E7 STA table_idx ; Restore table index
92E9 .clear_jsr_protection←4← 847D JMP← 84A5 JSR← 84CC JSR← 8EEC JMP
LDA saved_jsr_mask ; Restore LSTAT from saved OLDJSR value
92EC STA prot_status ; Write to protection status
92EF 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
92F0 .save_vdu_state←1← 92E1 JSR
LDA l0355 ; Read cursor editing state
92F3 STA (nfs_workspace),y ; Store to workspace[Y]
92F5 TAX ; Preserve in X for OSBYTE
92F6 JSR read_vdu_osbyte ; OSBYTE &85: read cursor position
92F9 INC nfs_workspace ; Advance workspace pointer
92FB TYA ; Y result from OSBYTE &85
92FC STA (nfs_workspace,x) ; Store Y pos to workspace (X=0)
92FE JSR read_vdu_osbyte_x0 ; Self-call trick: executes twice
9301 .read_vdu_osbyte_x0←1← 92FE JSR
LDX #0 ; X=0 for (zp,X) addressing
9303 .read_vdu_osbyte←1← 92F6 JSR
LDY table_idx ; Index into OSBYTE number table
9305 INC table_idx ; Next table entry next time
9307 INC nfs_workspace ; Advance workspace pointer
9309 LDA osbyte_vdu_table,y ; Read OSBYTE number from table
930C LDY #&ff ; Y=&FF: read current value
930E JSR osbyte ; Call OSBYTE
9311 TXA ; Result in X to A
9312 LDX #0 ; X=0 for indexed indirect store
9314 STA (nfs_workspace,x) ; Store result to workspace
9316 RTS ; Return after storing result
; 3-entry OSBYTE table for lang_2_save_palette_vdu (&92A4)
9317 .osbyte_vdu_table←1← 9309 LDA
EQUB &85 ; OSBYTE &85: read cursor position
9318 EQUB &C2 ; OSBYTE &C2: read shadow RAM allocation
9319 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).
931A 0016 .nmi_workspace_start←1← 8147 STA
.tube_brk_handler←1← 8147 STA
LDA #&ff ; A=&FF: signal error to co-processor via R4
931C 0018 JSR tube_send_r4 ; Send &FF error signal to Tube R4
931F 001B LDA tube_data_register_2 ; Flush any pending R2 byte
9322 001E LDA #0 ; A=0: send zero prefix to R2
9324 0020 .tube_send_zero_r2
JSR tube_send_r2 ; Send zero prefix byte via R2
9327 0023 TAY ; Y=0: start of error block at (&FD)
9328 0024 LDA (brk_ptr),y ; Load error number from (&FD),0
932A 0026 JSR tube_send_r2 ; Send error number via R2
932D 0029 .tube_brk_send_loop←1← 0030 BNE
INY ; Advance to next error string byte
932E 002A .tube_send_error_byte
LDA (brk_ptr),y ; Load next error string byte
9330 002C JSR tube_send_r2 ; Send error string byte via R2
9333 002F TAX ; Zero byte = end of error string
9334 0030 BNE tube_brk_send_loop ; Loop until zero terminator sent
9336 0032 .tube_reset_stack
LDX #&ff ; Reset stack pointer to top
9338 0034 TXS ; TXS: set stack pointer from X
9339 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.
933A 0036 .tube_enter_main_loop←2← 04EC JMP← 053A JMP
STX zp_temp_11 ; Save X to temporary
933C 0038 STY zp_temp_10 ; Save Y to temporary
933E 003A .tube_main_loop←6← 0048 BPL← 05AE JMP← 05D5 JMP← 0635 JMP← 069D JMP← 06CA JMP
BIT tube_status_1_and_tube_control ; BIT R1 status: check WRCH request
9341 003D BPL tube_poll_r2 ; R1 not ready: check R2 instead
9343 003F .tube_handle_wrch←1← 004D BMI
LDA tube_data_register_1 ; Read character from Tube R1 data
9346 0042 JSR nvwrch ; Write character
9349 0045 .tube_poll_r2←1← 003D BPL
BIT tube_status_register_2 ; BIT R2 status: check command byte
934C 0048 BPL tube_main_loop ; R2 not ready: loop back to R1 check
934E 004A BIT tube_status_1_and_tube_control ; Re-check R1: WRCH has priority over R2
9351 004D BMI tube_handle_wrch ; R1 ready: handle WRCH first
9353 004F LDX tube_data_register_2 ; Read command byte from Tube R2 data
9356 0052 STX tube_dispatch_ptr_lo ; Self-modify JMP low byte for dispatch
9358 0054 .tube_dispatch_cmd
JMP (tube_dispatch_table) ; Dispatch to handler via indirect JMP
935B 0057 .tube_transfer_addr←2← 0478 STY← 0493 STA
EQUB &00
935C 0058 .tube_xfer_page←2← 047C STA← 0498 STA
EQUB &80
935D 0059 .tube_xfer_addr_2←1← 04A2 STY
EQUB &00
935E 005A .tube_xfer_addr_3←1← 04A0 STA
EQUB &00
; Tube host code page 4 — reference: NFS12 (BEGIN, ADRR, ; SENDW)
; Copied from ROM during init (reloc_p4_src-18). 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
935F 0400 .tube_code_page4←1← 812D STA
JMP tube_begin ; JMP to BEGIN startup entry
9362 0403 .tube_escape_entry
JMP tube_escape_check ; JMP to tube_escape_check (&06A7)
9365 0406 .tube_addr_claim←10← 04BC JSR← 04E4 JMP← 8B3F JSR← 8B51 JSR← 8BAE JSR← 8E17 JMP← 9A01 JSR← 9A53 JSR← 9FA7 JSR← 9FAF JSR
CMP #&80 ; A>=&80: address claim; A<&80: data transfer
9367 0408 BCC setup_data_transfer ; A<&80: data transfer setup (SENDW)
9369 040A CMP #&c0 ; A>=&C0: new address claim from another host
936B 040C BCS addr_claim_external ; C=1: external claim, check ownership
936D 040E ORA #&40 ; Map &80-&BF range to &C0-&FF for comparison
936F 0410 CMP tube_claimed_id ; Is this for our currently-claimed address?
9371 0412 BNE return_tube_init ; Not our address: return
9373 0414 .tube_post_init←1← 813F JSR
LDA #&80 ; &80 sentinel: clear address claim
9375 0416 STA tube_claim_flag ; Store to claim-in-progress flag
9377 0418 RTS ; Return from tube_post_init
9378 0419 .addr_claim_external←1← 040C BCS
ASL tube_claim_flag ; Another host claiming; check if we're owner
937A 041B BCS accept_new_claim ; C=1: we have an active claim
937C 041D CMP tube_claimed_id ; Compare with our claimed address
937E 041F BEQ return_tube_init ; Match: return (we already have it)
9380 0421 CLC ; Not ours: CLC = we don't own this address
9381 0422 RTS ; Return with C=0 (claim denied)
9382 0423 .accept_new_claim←1← 041B BCS
STA tube_claimed_id ; Accept new claim: update our address
9384 0425 .return_tube_init←2← 0412 BNE← 041F BEQ
RTS ; Return with address updated
9385 0426 .setup_data_transfer←1← 0408 BCC
STY tube_data_ptr_hi ; Save 16-bit transfer address from (X,Y)
9387 0428 STX tube_data_ptr ; Store address pointer low byte
9389 042A JSR tube_send_r4 ; Send transfer type byte to co-processor
938C 042D TAX ; X = transfer type for table lookup
938D 042E LDY #3 ; Y=3: send 4 bytes (address + claimed addr)
938F 0430 .send_xfer_addr_bytes←1← 0436 BPL
LDA (tube_data_ptr),y ; Load transfer address byte from (X,Y)
9391 0432 JSR tube_send_r4 ; Send address byte to co-processor via R4
9394 0435 DEY ; Previous byte (big-endian: 3,2,1,0)
9395 0436 BPL send_xfer_addr_bytes ; Loop for all 4 address bytes
9397 0438 JSR tube_send_r4 ; Send claimed address via R4
939A 043B LDY #&18 ; Y=&18: enable Tube control register
939C 043D STY tube_status_1_and_tube_control ; Enable Tube interrupt generation
939F 0440 LDA tube_xfer_ctrl_bits,x ; Look up Tube control bits for this xfer type
93A2 0443 STA tube_status_1_and_tube_control ; Apply transfer- specific control bits
93A5 0446 LSR ; LSR: check bit 2 (2-byte flush needed?)
93A6 0447 LSR ; LSR: shift bit 2 to carry
93A7 0448 .poll_r4_copro_ack←1← 044B BVC
BIT tube_status_register_4_and_cpu_control ; Poll R4 status for co- processo r response
93AA 044B BVC poll_r4_copro_ack ; Bit 6 clear: not ready, keep polling
93AC 044D BCS flush_r3_nmi_check ; R4 bit 7: co-processor acknowledged transfer
93AE 044F CPX #4 ; Type 4 = SENDW (host-to-parasite word xfer)
93B0 0451 BNE return_tube_xfer ; Not SENDW type: skip release path
93B2 0453 PLA ; Discard return address (low byte)
93B3 0454 PLA ; Discard return address (high byte)
93B4 0455 .release_claim_restart←1← 04B8 BEQ
LDA #&80 ; A=&80: reset claim flag sentinel
93B6 0457 STA tube_claim_flag ; Clear claim-in-progress flag
93B8 0459 JMP tube_reply_byte ; Restart Tube main loop
93BB 045C .flush_r3_nmi_check←1← 044D BCS
BIT tube_data_register_3 ; Flush R3 data (first byte)
93BE 045F BIT tube_data_register_3 ; Flush R3 data (second byte)
93C1 0462 .copro_ack_nmi_check
LSR ; LSR: check bit 0 (NMI used?)
93C2 0463 BCC return_tube_xfer ; C=0: NMI not used, skip NMI release
93C4 0465 LDY #&88 ; Release Tube NMI (transfer used interrupts)
93C6 0467 STY tube_status_1_and_tube_control ; Write &88 to Tube control to release NMI
93C9 046A .return_tube_xfer←2← 0451 BNE← 0463 BCC
RTS ; Return from transfer setup
93CA 046B .tube_xfer_ctrl_bits←1← 0440 LDA
EQUB &86, &88, &96, &98, &18, &18, &82, &18
93D2 0473 .tube_begin←1← 0400 JMP
CLI ; BEGIN: enable interrupts for Tube host code
93D3 0474 PHP ; Save processor status
93D4 0475 PHA ; Save A on stack
93D5 0476 LDY #0 ; Y=0: start at beginning of page
93D7 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.
93D9 047A .tube_init_reloc
LDA #&80 ; Init: start sending from &8000
93DB 047C STA tube_xfer_page ; Store &80 as source page high byte
93DD 047E STA zp_ptr_hi ; Store &80 as page counter initial value
93DF 0480 LDA #&20 ; A=&20: bit 5 mask for ROM type check
93E1 0482 AND rom_type ; ROM type bit 5: reloc address in header?
93E4 0485 BEQ store_xfer_end_addr ; No reloc addr: use defaults
93E6 0487 LDX copyright_offset ; Skip past copyright string to find reloc addr
93E9 048A .scan_copyright_end←1← 048E BNE
INX ; Skip past null-terminated copyright string
93EA 048B LDA rom_header,x ; Load next byte from ROM header
93ED 048E BNE scan_copyright_end ; Loop until null terminator found
93EF 0490 LDA lang_entry_lo,x ; Read 4-byte reloc address from ROM header
93F2 0493 STA tube_transfer_addr ; Store reloc addr byte 1 as transfer addr
93F4 0495 LDA lang_entry_hi,x ; Load reloc addr byte 2
93F7 0498 STA tube_xfer_page ; Store as source page start
93F9 049A LDY service_entry,x ; Load reloc addr byte 3
93FC 049D LDA svc_entry_lo,x ; Load reloc addr byte 4 (highest)
93FF 04A0 .store_xfer_end_addr←1← 0485 BEQ
STA tube_xfer_addr_3 ; Store high byte of end address
9401 04A2 STY tube_xfer_addr_2 ; Store byte 3 of end address
9403 04A4 PLA ; Restore A from stack
9404 04A5 PLP ; Restore processor status
9405 04A6 BCS beginr ; Carry set: language entry (claim Tube)
9407 04A8 TAX ; X = A (preserved from entry)
9408 04A9 BNE begink ; Non-zero: check break type
940A 04AB JMP tube_reply_ack ; A=0: acknowledge and return
940D 04AE .begink←1← 04A9 BNE
LDX #0 ; X=0 for OSBYTE read
940F 04B0 LDY #&ff ; Y=&FF for OSBYTE read
9411 04B2 LDA #osbyte_read_write_last_break_type ; OSBYTE &FD: read last break type
9413 04B4 JSR osbyte ; Read type of last reset
9416 04B7 TXA ; X=value of type of last reset
9417 04B8 BEQ release_claim_restart ; Soft break (0): skip ROM transfer
9419 04BA .beginr←2← 04A6 BCS← 04BF BCC
LDA #&ff ; A=&FF: claim Tube for all operations
941B 04BC JSR tube_addr_claim ; Claim Tube address via R4
941E 04BF BCC beginr ; Not claimed: retry until claimed
9420 04C1 LDA #1 ; Transfer type 1 (parasite to host)
9422 04C3 JSR tube_setup_transfer ; Set up Tube transfer parameters
9425 04C6 LDY #0 ; Y=0: start at page boundary
9427 04C8 STY zp_ptr_lo ; Source ptr low = 0
9429 04CA LDX #&40 ; X=&40: 64 pages (16KB) to transfer
942B 04CC .send_rom_byte←2← 04D7 BNE← 04DC BNE
LDA (zp_ptr_lo),y ; Read byte from source address
942D 04CE STA tube_data_register_3 ; Send byte to Tube via R3
9430 04D1 .poll_r3_ready←1← 04D4 BVC
BIT tube_status_register_3 ; Check R3 status
9433 04D4 BVC poll_r3_ready ; Not ready: wait for Tube
9435 04D6 INY ; Next byte in page
9436 04D7 BNE send_rom_byte ; More bytes in page: continue
9438 04D9 INC zp_ptr_hi ; Next source page
943A 04DB DEX ; Decrement page counter
943B 04DC BNE send_rom_byte ; More pages: continue transfer
943D 04DE LDA #4 ; Transfer type 4 (host to parasite burst)
943F 04E0 .tube_setup_transfer←1← 04C3 JSR
LDY #0 ; Y=0: low byte of param block ptr
9441 04E2 LDX #&57 ; X=&57: param block at &0057
9443 04E4 JMP tube_addr_claim ; Claim Tube and start transfer
9446 04E7 .tube_rdch_handler
LDA #1 ; R2 command: OSRDCH request
9448 04E9 JSR tube_send_r2 ; Send OSRDCH request to host
944B 04EC JMP tube_enter_main_loop ; Jump to RDCH completion handler
944E 04EF .tube_restore_regs
LDY zp_temp_10 ; Restore saved Y register
9450 04F1 LDX zp_temp_11 ; Restore X from saved value
9452 04F3 JSR tube_read_r2 ; Read result byte from R2
9455 04F6 ASL ; Shift carry into C flag
9456 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← 0623 JSR← 0627 JSR← 0638 JSR← 063C JSR← 0640 JSR← 065A JSR← 06A2 JSR
BIT tube_status_register_2 ; Poll R2 status register
9459 04FA BPL tube_read_r2 ; Bit 7 clear: R2 not ready, wait
945B 04FC LDA tube_data_register_2 ; Read byte from R2 data register
945E 04FF RTS ; Return with pointers initialised
; Tube host code page 5 — reference: NFS13 (TASKS, ; BPUT-FILE)
; Copied from ROM during init (reloc_p5_src-18). 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
945F 0500 .tube_dispatch_table←2← 0054 JMP← 8133 STA
EQUW tube_osrdch ; cmd 0: OSRDCH
9461 0502 EQUW tube_oscli ; cmd 1: OSCLI
9463 0504 EQUW tube_osbyte_short ; cmd 2: OSBYTE (2-param)
9465 0506 EQUW tube_osbyte_long ; cmd 3: OSBYTE (3-param)
9467 0508 EQUW tube_osword ; cmd 4: OSWORD
9469 050A EQUW tube_osword_rdln ; cmd 5: OSWORD 0 (read line)
946B 050C EQUW tube_restore_regs ; cmd 6: release/restore regs
946D 050E EQUW tube_release_return ; cmd 7: restore regs, RTS
946F 0510 EQUW tube_osargs ; cmd 8: OSARGS
9471 0512 EQUW tube_osbget ; cmd 9: OSBGET
9473 0514 EQUW tube_osbput ; cmd 10: OSBPUT
9475 0516 EQUW tube_osfind ; cmd 11: OSFIND
9477 0518 EQUW tube_osfile ; cmd 12: OSFILE
9479 051A EQUW tube_osgbpb ; cmd 13: OSGBPB
947B 051C .tube_wrch_handler
PHA ; Save A for WRCHV echo
947C 051D LDA #0 ; A=0: send null prefix via R2
947E 051F .tube_send_and_poll
JSR tube_send_r2 ; Send prefix byte to co-processor
9481 0522 .poll_r2_reply←2← 052A BPL← 0532 JMP
BIT tube_status_register_2 ; Poll R2 for co-processor reply
9484 0525 BVS wrch_echo_reply ; R2 ready: go process reply
9486 0527 .tube_poll_r1_wrch
BIT tube_status_1_and_tube_control ; Check R1 for pending WRCH request
9489 052A BPL poll_r2_reply ; No R1 data: back to polling R2
948B 052C LDA tube_data_register_1 ; Read WRCH character from R1
948E 052F JSR nvwrch ; Write character
9491 0532 .tube_resume_poll
JMP poll_r2_reply ; Resume R2 polling after servicing
9494 0535 .wrch_echo_reply←1← 0525 BVS
PLA ; Recover original character
9495 0536 STA tube_data_register_2 ; Echo character back via R2
9498 0539 PHA ; Push for dispatch loop re-entry
9499 053A JMP tube_enter_main_loop ; Enter main dispatch loop
949C 053D .tube_release_return
LDX zp_temp_11 ; Restore saved X from temporary
949E 053F LDY zp_temp_10 ; Restore saved Y from temporary
94A0 0541 PLA ; Restore saved A
94A1 0542 RTS ; Return to caller
94A2 0543 .tube_osbput
JSR tube_read_r2 ; Read channel handle from R2
94A5 0546 TAY ; Y=channel handle from R2
94A6 0547 JSR tube_read_r2 ; Read data byte from R2 for BPUT
94A9 054A JSR osbput ; Write a single byte A to an open file Y
94AC 054D JMP tube_reply_ack ; BPUT done: send acknowledge, return
94AF 0550 .tube_osbget
JSR tube_read_r2 ; Read channel handle from R2
94B2 0553 TAY ; Y=channel handle for OSBGET Y=file handle
94B3 0554 JSR osbget ; Read a single byte from an open file Y
94B6 0557 PHA ; Save byte read from file
94B7 0558 JMP send_reply_ok ; Send carry+byte reply (BGET result)
94BA 055B .tube_osrdch
JSR nvrdch ; Read a character from the current input stream
94BD 055E PHA ; A=character read
94BE 055F .send_reply_ok←2← 0558 JMP← 0620 JMP
ORA #&80 ; Set bit 7 (no-error flag)
94C0 0561 .tube_rdch_reply
ROR ; ROR A: encode carry (error flag) into bit 7
94C1 0562 JSR tube_send_r2 ; = JSR tube_send_r2 (overlaps &053D entry)
94C4 0565 PLA ; Restore read character/byte
94C5 0566 JMP tube_reply_byte ; Return to Tube main loop
94C8 0569 .tube_osfind
JSR tube_read_r2 ; Read open mode from R2
94CB 056C BEQ tube_osfind_close ; A=0: close file, else open with filename
94CD 056E PHA ; Save open mode while reading filename
94CE 056F JSR tube_read_string ; Read filename string from R2 into &0700
94D1 0572 PLA ; Recover open mode from stack
94D2 0573 JSR osfind ; Open or close file(s)
94D5 0576 PHA ; Save file handle result
94D6 0577 LDA #&ff ; A=&FF: success marker
94D8 0579 JSR tube_send_r2 ; Send success marker via R2
94DB 057C PLA ; Restore file handle
94DC 057D JMP tube_reply_byte ; Send file handle result to co-processor
94DF 0580 .tube_osfind_close←1← 056C BEQ
JSR tube_read_r2 ; OSFIND close: read handle from R2
94E2 0583 TAY ; Y=handle to close
94E3 0584 LDA #osfind_close ; A=0: close command for OSFIND
94E5 0586 JSR osfind ; Close one or all files
94E8 0589 JMP tube_reply_ack ; Close done: send acknowledge, return
94EB 058C .tube_osargs
JSR tube_read_r2 ; Read file handle from R2
94EE 058F TAY ; Y=file handle for OSARGS
94EF 0590 .tube_read_params
LDX #3 ; Read 4-byte arg + reason from R2 into ZP
94F1 0592 .read_osargs_params←1← 0598 BPL
JSR tube_read_r2 ; Read next param byte from R2
94F4 0595 STA zp_ptr_lo,x ; Params stored at &00-&03 (little-endian)
94F6 0597 DEX ; Decrement byte counter
94F7 0598 BPL read_osargs_params ; Loop until all 4 bytes read
94F9 059A INX ; X=0: reset index after loop
94FA 059B JSR tube_read_r2 ; Read OSARGS reason code from R2
94FD 059E JSR osargs ; Read or write a file's attributes
9500 05A1 JSR tube_send_r2 ; Send result A back to co-processor
9503 05A4 LDX #3 ; Return 4-byte result from ZP &00-&03
9505 05A6 .send_osargs_result←1← 05AC BPL
LDA zp_ptr_lo,x ; Load result byte from zero page
9507 05A8 JSR tube_send_r2 ; Send byte to co-processor via R2
950A 05AB DEX ; Previous byte (count down)
950B 05AC BPL send_osargs_result ; Loop for all 4 bytes
950D 05AE JMP tube_main_loop ; Return to Tube main loop
9510 05B1 .tube_read_string←3← 056F JSR← 05C5 JSR← 05E2 JSR
LDX #0 ; X=0: initialise string buffer index
9512 05B3 LDY #0 ; Y=0: string buffer offset 0
9514 05B5 .strnh←1← 05C0 BNE
JSR tube_read_r2 ; Read next string byte from R2
9517 05B8 STA l0700,y ; Store byte in string buffer at &0700+Y
951A 05BB INY ; Next buffer position
951B 05BC BEQ string_buf_done ; Y overflow: string too long, truncate
951D 05BE CMP #&0d ; Check for CR terminator
951F 05C0 BNE strnh ; Not CR: continue reading string
9521 05C2 .string_buf_done←1← 05BC BEQ
LDY #7 ; Y=7: set XY=&0700 for OSCLI/OSFIND
9523 05C4 RTS ; Return with XY pointing to &0700
9524 05C5 .tube_oscli
JSR tube_read_string ; Read command string from R2
9527 05C8 JSR oscli ; Execute * command via OSCLI
952A 05CB .tube_reply_ack←3← 04AB JMP← 054D JMP← 0589 JMP
LDA #&7f ; &7F = success acknowledgement
952C 05CD .tube_reply_byte←5← 0459 JMP← 0566 JMP← 057D JMP← 05D0 BVC← 06B5 JMP
BIT tube_status_register_2 ; Poll R2 status until ready
952F 05D0 BVC tube_reply_byte ; Bit 6 clear: not ready, loop
9531 05D2 STA tube_data_register_2 ; Write byte to R2 data register
9534 05D5 .mj←1← 0600 BEQ
JMP tube_main_loop ; Return to Tube main loop
9537 05D8 .tube_osfile
LDX #&10 ; X=&10: 16 bytes for OSFILE CB
9539 05DA .argsw←1← 05E0 BNE
JSR tube_read_r2 ; Read next control block byte from R2
953C 05DD STA zp_ptr_hi,x ; Store at &01+X (descending)
953E 05DF DEX ; Decrement byte counter
953F 05E0 BNE argsw ; Loop for all 16 bytes
9541 05E2 JSR tube_read_string ; Read filename string from R2 into &0700
9544 05E5 STX zp_ptr_lo ; XY=&0700: filename pointer for OSFILE
9546 05E7 STY zp_ptr_hi ; Store Y=7 as pointer high byte
9548 05E9 LDY #0 ; Y=0 for OSFILE control block offset
954A 05EB JSR tube_read_r2 ; Read OSFILE reason code from R2
954D 05EE JSR osfile ; Execute OSFILE operation
9550 05F1 ORA #&80 ; Set bit 7: mark result as present
9552 05F3 JSR tube_send_r2 ; Send result A (object type) to co-processor
9555 05F6 LDX #&10 ; Return 16-byte control block to co-processor
9557 05F8 .send_osfile_ctrl_blk←1← 05FE BNE
LDA zp_ptr_hi,x ; Load control block byte
9559 05FA JSR tube_send_r2 ; Send byte to co-processor via R2
955C 05FD DEX ; Decrement byte counter
955D 05FE BNE send_osfile_ctrl_blk ; Loop for all 16 bytes
; Tube host code page 6 — reference: NFS13 (GBPB-ESCA)
; Copied from ROM during init (reloc_p6_src-18). ; &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
955F 0600 .tube_code_page6←1← 8139 STA
BEQ mj ; OSGBPB done: return to main loop
9561 0602 .tube_osgbpb
LDX #&0c ; X=&0C: 13 bytes for OSGBPB CB
9563 0604 .read_gbpb_params←1← 060A BPL
JSR tube_read_r2 ; Read param byte from Tube R2
9566 0607 STA zp_ptr_lo,x ; Store in zero page param block
9568 0609 DEX ; Next byte (descending)
9569 060A BPL read_gbpb_params ; Loop until all 13 bytes read
956B 060C JSR tube_read_r2 ; Read A (OSGBPB function code)
956E 060F INX ; X=0 after loop
956F 0610 LDY #0 ; Y=0 for OSGBPB call
9571 0612 JSR osgbpb ; Read or write multiple bytes to an open file
9574 0615 PHA ; Save OSGBPB result on stack
9575 0616 LDX #&0c ; X=12: send 13 updated param bytes
9577 0618 .send_gbpb_params←1← 061E BPL
LDA zp_ptr_lo,x ; Load updated param byte
9579 061A JSR tube_send_r2 ; Send param byte via R2
957C 061D DEX ; Next byte (descending)
957D 061E BPL send_gbpb_params ; Loop until all 13 bytes sent
957F 0620 JMP send_reply_ok ; Return to main event loop
9582 0623 .tube_osbyte_short
JSR tube_read_r2 ; Read X parameter from R2
9585 0626 TAX ; Save in X
9586 0627 JSR tube_read_r2 ; Read A (OSBYTE function code)
9589 062A JSR osbyte ; Execute OSBYTE A,X
958C 062D .tube_osbyte_send_x←2← 0630 BVC← 0658 BVS
BIT tube_status_register_2 ; Poll R2 status (bit 6 = ready)
958F 0630 BVC tube_osbyte_send_x ; Not ready: keep polling
9591 0632 STX tube_data_register_2 ; Send X result for 2-param OSBYTE
9594 0635 .bytex←1← 0648 BEQ
JMP tube_main_loop ; Return to main event loop
9597 0638 .tube_osbyte_long
JSR tube_read_r2 ; Read X parameter from R2
959A 063B TAX ; Save in X
959B 063C JSR tube_read_r2 ; Read Y parameter from co-processor
959E 063F TAY ; Save in Y
959F 0640 JSR tube_read_r2 ; Read A (OSBYTE function code)
95A2 0643 JSR osbyte ; Execute OSBYTE A,X,Y
95A5 0646 EOR #&9d ; Test for OSBYTE &9D (fast Tube BPUT)
95A7 0648 BEQ bytex ; OSBYTE &9D (fast Tube BPUT): no result needed
95A9 064A LDA #&40 ; A=&40: high bit will hold carry
95AB 064C ROR ; Encode carry (error flag) into bit 7
95AC 064D JSR tube_send_r2 ; Send carry+status byte via R2
95AF 0650 .tube_osbyte_send_y←1← 0653 BVC
BIT tube_status_register_2 ; Poll R2 status for ready
95B2 0653 BVC tube_osbyte_send_y ; Not ready: keep polling
95B4 0655 STY tube_data_register_2 ; Send Y result, then fall through to send X
95B7 0658 BVS tube_osbyte_send_x ; ALWAYS branch
95B9 065A .tube_osword
JSR tube_read_r2 ; Read OSWORD number from R2
95BC 065D TAY ; Save OSWORD number in Y
95BD 065E .tube_osword_read←1← 0661 BPL
BIT tube_status_register_2 ; Poll R2 status for data ready
95C0 0661 BPL tube_osword_read ; Not ready: keep polling
95C2 0663 LDX tube_data_register_2 ; Read param block length from R2
95C5 0666 DEX ; DEX: length 0 means no params to read
95C6 0667 BMI skip_param_read ; No params (length=0): skip read loop
95C8 0669 .tube_osword_read_lp←2← 066C BPL← 0675 BPL
BIT tube_status_register_2 ; Poll R2 status for data ready
95CB 066C BPL tube_osword_read_lp ; Not ready: keep polling
95CD 066E LDA tube_data_register_2 ; Read param byte from R2
95D0 0671 STA l0128,x ; Store param bytes into block at &0128
95D3 0674 DEX ; Next param byte (descending)
95D4 0675 BPL tube_osword_read_lp ; Loop until all params read
95D6 0677 TYA ; Restore OSWORD number from Y
95D7 0678 .skip_param_read←1← 0667 BMI
LDX #<(l0128) ; XY=&0128: param block address for OSWORD
95D9 067A LDY #>(l0128) ; Y=&01: param block at &0128
95DB 067C JSR osword ; Execute OSWORD with XY=&0128
95DE 067F LDA #&ff ; A=&FF: result marker for co-processor
95E0 0681 JSR tube_send_r2 ; Send result marker via R2
95E3 0684 .poll_r2_osword_result←1← 0687 BPL
BIT tube_status_register_2 ; Poll R2 status for ready
95E6 0687 BPL poll_r2_osword_result ; Not ready: keep polling
95E8 0689 LDX tube_data_register_2 ; Read result block length from R2
95EB 068C DEX ; Decrement result byte counter
95EC 068D BMI tube_return_main ; No results to send: return to main loop
95EE 068F .tube_osword_write←1← 069B BPL
LDY l0128,x ; Send result block bytes from &0128 via R2
95F1 0692 .tube_osword_write_lp←1← 0695 BVC
BIT tube_status_register_2 ; Poll R2 status for ready
95F4 0695 BVC tube_osword_write_lp ; Not ready: keep polling
95F6 0697 STY tube_data_register_2 ; Send result byte via R2
95F9 069A DEX ; Next result byte (descending)
95FA 069B BPL tube_osword_write ; Loop until all results sent
95FC 069D .tube_return_main←1← 068D BMI
JMP tube_main_loop ; Return to main event loop
95FF 06A0 .tube_osword_rdln
LDX #4 ; X=4: read 5-byte RDLN ctrl block
9601 06A2 .read_rdln_ctrl_block←1← 06A8 BPL
JSR tube_read_r2 ; Read control block byte from R2
9604 06A5 STA zp_ptr_lo,x ; Store in zero page params
9606 06A7 DEX ; Next byte (descending)
9607 06A8 BPL read_rdln_ctrl_block ; Loop until all 5 bytes read
9609 06AA INX ; X=0 after loop, A=0 for OSWORD 0 (read line)
960A 06AB LDY #0 ; Y=0 for OSWORD 0
960C 06AD TXA ; A=0: OSWORD 0 (read line)
960D 06AE JSR osword ; Read input line from keyboard
9610 06B1 BCC tube_rdln_send_line ; C=0: line read OK; C=1: escape pressed
9612 06B3 LDA #&ff ; &FF = escape/error signal to co-processor
9614 06B5 JMP tube_reply_byte ; Escape: send &FF error to co-processor
9617 06B8 .tube_rdln_send_line←1← 06B1 BCC
LDX #0 ; X=0: start of input buffer at &0700
9619 06BA LDA #&7f ; &7F = line read successfully
961B 06BC JSR tube_send_r2 ; Send &7F (success) to co-processor
961E 06BF .tube_rdln_send_loop←1← 06C8 BNE
LDA l0700,x ; Load char from input buffer
9621 06C2 .tube_rdln_send_byte
JSR tube_send_r2 ; Send char to co-processor
9624 06C5 INX ; Next character
9625 06C6 CMP #&0d ; Check for CR terminator
9627 06C8 BNE tube_rdln_send_loop ; Loop until CR terminator sent
9629 06CA JMP tube_main_loop ; Return to main event loop
962C 06CD .tube_send_r2←17← 0020 JSR← 0026 JSR← 002C JSR← 04E9 JSR← 051F JSR← 0562 JSR← 0579 JSR← 05A1 JSR← 05A8 JSR← 05F3 JSR← 05FA JSR← 061A JSR← 064D JSR← 0681 JSR← 06BC JSR← 06C2 JSR← 06D0 BVC
BIT tube_status_register_2 ; Poll R2 status (bit 6 = ready)
962F 06D0 BVC tube_send_r2 ; Not ready: keep polling
9631 06D2 STA tube_data_register_2 ; Write A to Tube R2 data register
9634 06D5 RTS ; Return to caller
9635 06D6 .tube_send_r4←5← 0018 JSR← 042A JSR← 0432 JSR← 0438 JSR← 06D9 BVC
BIT tube_status_register_4_and_cpu_control ; Poll R4 status (bit 6 = ready)
9638 06D9 BVC tube_send_r4 ; Not ready: keep polling
963A 06DB STA tube_data_register_4 ; Write A to Tube R4 data register
963D 06DE RTS ; Return to caller
963E 06DF .tube_escape_check←1← 0403 JMP
LDA escape_flag ; Check OS escape flag at &FF
9640 06E1 SEC ; SEC+ROR: put bit 7 of &FF into carry+bit 7
9641 06E2 ROR ; ROR: shift escape bit 7 to carry
9642 06E3 BMI tube_send_r1 ; Escape set: forward to co-processor via R1
9644 06E5 .tube_event_handler
PHA ; EVNTV: forward event A, Y, X to co-processor
9645 06E6 LDA #0 ; Send &00 prefix (event notification)
9647 06E8 JSR tube_send_r1 ; Send zero prefix via R1
964A 06EB TYA ; Y value for event
964B 06EC JSR tube_send_r1 ; Send Y via R1
964E 06EF TXA ; X value for event
964F 06F0 JSR tube_send_r1 ; Send X via R1
9652 06F3 PLA ; Restore A (event type)
9653 06F4 .tube_send_r1←5← 06E3 BMI← 06E8 JSR← 06EC JSR← 06F0 JSR← 06F7 BVC
BIT tube_status_1_and_tube_control ; Poll R1 status (bit 6 = ready)
9656 06F7 BVC tube_send_r1 ; Not ready: keep polling
9658 06F9 STA tube_data_register_1 ; Write A to Tube R1 data register
965B 06FC RTS ; Return to caller
965C 06FD EQUS " NE"
965F EQUB &00
9660 .trampoline_tx_setup←2← 868C JSR← 8ED7 JMP
JMP tx_begin ; Trampoline: forward to tx_begin
9663 .trampoline_adlc_init←1← 830D 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 (&9FD9) 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 (via AC=1): 8-bit RX word length, abort extend enabled, NRZ encoding
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. Reads SR2 and tests AP|RDA bits. If non-zero, performs full ADLC reset and discards. If zero (clean end), discards via scout_discard. 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 (&9A59) - 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 &986F 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_store_rxcb
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 .load_rxcb_buf_hi
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
fall through ↓

Store RXCB completion fields from scout buffer

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

9A2F .store_rxcb_completion
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 receive buffer (length #&01FC) below the screen 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 .tx_nmi_dispatch_page
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 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 &9D4C, 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 &9D94 which switches to RX mode. CR2=&3F = 0011_1111: bit5: CLR_RX_ST -- clears fv_stored_ (prepares for RX of reply) bit4: TX_LAST_DATA -- tells ADLC this is the final data byte bit3: FLAG_IDLE -- send flags/idle after frame bit2: FC_TDRA -- force clear TDRA bit1: 2_1_BYTE -- two-byte transfer mode bit0: PSE -- prioritised status enable Note: NO CLR_TX_ST (bit6=0), NO RTS (bit7=0 -- drops RTS after frame)

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 &9DB2

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 &9D4C but reads from the port buffer instead of the TX workspace. Writes two bytes per iteration, checking SR1 IRQ between pairs for tight looping.

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 &9A4A 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 &9EE9 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 &9A34.

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 &9A34. 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 TX block byte at offset 6
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 .rom_nmi_tail
EQUB &AD, &4A, ; Load current TX flags Set bit 1 (transfer &0D, &09, ; mode flag) Store updated TX flags &02, &8D