Acorn NFS 3.40

Updated 31 Mar 2026

← All Acorn NFS and Acorn ANFS versions

; Sideways ROM header
; NFS ROM 3.40 disassembly (Acorn Econet filing system)
; ====================================================
8000 .rom_header←3← 04A9 LDA← 04DF LDA← 9B9B JSR
.language_entry←3← 04A9 LDA← 04DF LDA← 9B9B JSR
.pydis_start←3← 04A9 LDA← 04DF LDA← 9B9B JSR
JMP language_handler ; JMP language_handler
8003 .service_entry←1← 04EE LDY
JMP service_handler ; JMP service_handler
8006 .rom_type←1← 04D3 AND
EQUB &82
8007 .copyright_offset←1← 04DB LDX
EQUB copyright - rom_header
8008 .binary_version←2← 836A CMP← 8373 LDA
EQUB &83
8009 .title
EQUS " NET"
8010 .copyright
EQUB &00
; The 'ROFF' suffix at &8014 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.
8011 .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.
8018 .error_offsets←1← 8500 LDY
EQUB &00 ; "Line Jammed"
8019 EQUB &0D ; "Net Error"
801A EQUB &18 ; "Not listening"
801B EQUB &27 ; "No Clock"
801C EQUB &31 ; "Escape"
801D EQUB &31 ; "Escape"
801E EQUB &31 ; "Escape"
801F EQUB &39 ; "Bad Option"
8020 EQUB &45 ; "No reply"
; Four bytes with unknown purpose.
8021 EQUB &01 ; Purpose unknown
8022 EQUB &00 ; Purpose unknown
8023 EQUB &40 ; Purpose unknown
8024 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 &80E7.
; See dispatch_0_hi (&804A) for the corresponding high bytes.
; Five callers share this table via different Y base offsets:
; Y=&00 Service calls 0-12 (indices 0-13)
; Y=&0E Language entry reasons (indices 14-18)
; Y=&13 FSCV codes 0-7 (indices 19-26)
; Y=&17 FS reply handlers (indices 27-32)
; Y=&21 *NET1-4 sub-commands (indices 33-36)
; Lo bytes for the last 6 entries (indices 31-36) occupy
; &8044-&8049, immediately before the hi bytes. Their hi
; bytes are at &8069-&806E, after dispatch_0_hi.
8025 .dispatch_0_lo
EQUB <(return_1-1) ; lo - Svc 0: already claimed (no-op)
8026 EQUB <(svc_1_abs_workspace-1) ; lo - Svc 1: absolute workspace
8027 EQUB <(svc_2_private_workspace-1) ; lo - Svc 2: private workspace
8028 EQUB <(svc_3_autoboot-1) ; lo - Svc 3: auto-boot
8029 EQUB <(svc_4_star_command-1) ; lo - Svc 4: unrecognised star command
802A EQUB <(svc_5_unknown_irq-1) ; lo - Svc 5: unrecognised interrupt
802B EQUB <(return_1-1) ; lo - Svc 6: BRK (no-op)
802C EQUB <(dispatch_net_cmd-1) ; lo - Svc 7: unrecognised OSBYTE
802D EQUB <(svc_8_osword-1) ; lo - Svc 8: unrecognised OSWORD
802E EQUB <(svc_9_help-1) ; lo - Svc 9: *HELP
802F EQUB <(return_1-1) ; lo - Svc 10: static workspace (no-op)
8030 EQUB <(svc_11_nmi_claim-1) ; lo - Svc 11: NMI release (reclaim NMIs)
8031 EQUB <(svc_12_nmi_release-1) ; lo - Svc 12: NMI claim (save NMI state)
8032 EQUB <(svc_13_select_nfs-1) ; lo - Svc 13: select NFS (intercepted before dispatch)
8033 EQUB <(lang_0_insert_remote_key-1) ; lo - Lang 0: no language / Tube
8034 EQUB <(lang_1_remote_boot-1) ; lo - Lang 1: normal startup
8035 EQUB <(lang_2_save_palette_vdu-1) ; lo - Lang 2: softkey byte (Electron)
8036 EQUB <(lang_3_execute_at_0100-1) ; lo - Lang 3: softkey length (Electron)
8037 EQUB <(lang_4_remote_validated-1) ; lo - Lang 4: remote validated
8038 EQUB <(fscv_0_opt_entry-1) ; lo - FSCV 0: *OPT
8039 EQUB <(fscv_1_eof-1) ; lo - FSCV 1: EOF check
803A EQUB <(fscv_2_star_run-1) ; lo - FSCV 2: */ (run)
803B EQUB <(fscv_3_star_cmd-1) ; lo - FSCV 3: unrecognised star command
803C EQUB <(fscv_2_star_run-1) ; lo - FSCV 4: *RUN
803D EQUB <(fscv_5_cat-1) ; lo - FSCV 5: *CAT
803E EQUB <(fscv_6_shutdown-1) ; lo - FSCV 6: shutdown
803F EQUB <(fscv_7_read_handles-1) ; lo - FSCV 7: read handle range
8040 EQUB <(fsreply_0_print_dir-1) ; lo - FS reply: print directory name
8041 EQUB <(fsreply_1_copy_handles_boot-1) ; lo - FS reply: copy handles + boot
8042 EQUB <(fsreply_2_copy_handles-1) ; lo - FS reply: copy handles
8043 EQUB <(fsreply_3_set_csd-1) ; lo - FS reply: set CSD handle
8044 EQUB <(fsreply_4_notify_exec-1) ; lo - FS reply: notify + execute
8045 EQUB <(fsreply_5_set_lib-1) ; lo - FS reply: set library handle
8046 EQUB <(net_1_read_handle-1) ; lo - *NET1: read handle from packet
8047 EQUB <(net_2_read_handle_entry-1) ; lo - *NET2: read handle from workspace
8048 EQUB <(net_3_close_handle-1) ; lo - *NET3: close handle
8049 EQUB <(net_4_resume_remote-1) ; lo - *NET4: resume remote
; Dispatch table: high bytes of (handler_address - 1)
; Paired with dispatch_0_lo (&8025). Together they form a table
; of 37 handler addresses, used via the PHA/PHA/RTS trick at
; &80E7.
804A .dispatch_0_hi
EQUB >(return_1-1) ; hi - Svc 0: already claimed (no-op)
804B EQUB >(svc_1_abs_workspace-1) ; hi - Svc 1: absolute workspace
804C EQUB >(svc_2_private_workspace-1) ; hi - Svc 2: private workspace
804D EQUB >(svc_3_autoboot-1) ; hi - Svc 3: auto-boot
804E EQUB >(svc_4_star_command-1) ; hi - Svc 4: unrecognised star command
804F EQUB >(svc_5_unknown_irq-1) ; hi - Svc 5: unrecognised interrupt
8050 EQUB >(return_1-1) ; hi - Svc 6: BRK (no-op)
8051 EQUB >(dispatch_net_cmd-1) ; hi - Svc 7: unrecognised OSBYTE
8052 EQUB >(svc_8_osword-1) ; hi - Svc 8: unrecognised OSWORD
8053 EQUB >(svc_9_help-1) ; hi - Svc 9: *HELP
8054 EQUB >(return_1-1) ; hi - Svc 10: static workspace (no-op)
8055 EQUB >(svc_11_nmi_claim-1) ; hi - Svc 11: NMI release (reclaim NMIs)
8056 EQUB >(svc_12_nmi_release-1) ; hi - Svc 12: NMI claim (save NMI state)
8057 EQUB >(svc_13_select_nfs-1) ; hi - Svc 13: select NFS (intercepted before dispatch)
8058 EQUB >(lang_0_insert_remote_key-1) ; hi - Lang 0: no language / Tube
8059 EQUB >(lang_1_remote_boot-1) ; hi - Lang 1: normal startup
805A EQUB >(lang_2_save_palette_vdu-1) ; hi - Lang 2: softkey byte (Electron)
805B EQUB >(lang_3_execute_at_0100-1) ; hi - Lang 3: softkey length (Electron)
805C EQUB >(lang_4_remote_validated-1) ; hi - Lang 4: remote validated
805D EQUB >(fscv_0_opt_entry-1) ; hi - FSCV 0: *OPT
805E EQUB >(fscv_1_eof-1) ; hi - FSCV 1: EOF check
805F EQUB >(fscv_2_star_run-1) ; hi - FSCV 2: */ (run)
8060 EQUB >(fscv_3_star_cmd-1) ; hi - FSCV 3: unrecognised star command
8061 EQUB >(fscv_2_star_run-1) ; hi - FSCV 4: *RUN
8062 EQUB >(fscv_5_cat-1) ; hi - FSCV 5: *CAT
8063 EQUB >(fscv_6_shutdown-1) ; hi - FSCV 6: shutdown
8064 EQUB >(fscv_7_read_handles-1) ; hi - FSCV 7: read handle range
8065 EQUB >(fsreply_0_print_dir-1) ; hi - FS reply: print directory name
8066 EQUB >(fsreply_1_copy_handles_boot-1) ; hi - FS reply: copy handles + boot
8067 EQUB >(fsreply_2_copy_handles-1) ; hi - FS reply: copy handles
8068 EQUB >(fsreply_3_set_csd-1) ; hi - FS reply: set CSD handle
8069 EQUB >(fsreply_4_notify_exec-1) ; hi - FS reply: notify + execute
806A EQUB >(fsreply_5_set_lib-1) ; hi - FS reply: set library handle
806B EQUB >(net_1_read_handle-1) ; hi - *NET1: read handle from packet
806C EQUB >(net_2_read_handle_entry-1) ; hi - *NET2: read handle from workspace
806D EQUB >(net_3_close_handle-1) ; hi - *NET3: close handle
806E 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=&21, and dispatches via &80E7. 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 (&8E59): read file handle from received packet (net_1_read_handle)

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

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

*NET4 (&81B8): resume after remote operation (net_4_resume_remote)

806F .dispatch_net_cmd
LDA osbyte_a_copy ; Read command character following *NET
8071 SBC #&31 ; Subtract ASCII '1' to get 0-based command index
8073 CMP #4 ; Command index >= 4: invalid *NET sub-command
8075 BCS svc_dispatch_range ; Out of range: return via c80e3/RTS
8077 TAX ; X = command index (0-3)
8078 LDA #0 ; Clear &A9 (used by dispatch)
807A STA svc_state ; Store zero to &A9
807C TYA ; Preserve A before dispatch
807D LDY #&21 ; Y=&21: base offset for *NET commands (index 33+)
807F BNE dispatch ; ALWAYS branch to dispatch ALWAYS branch
8081 .skip_cmd_spaces←1← 8086 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 for the first number. If a dot separator was found (carry set), it stores the result directly as the network (&0E01) and calls parse_decimal again for 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.

8082 .i_am_handler
LDA (fs_options),y ; Load next char from command line
8084 CMP #&20 ; Skip spaces
8086 BEQ skip_cmd_spaces ; Loop back to skip leading spaces
8088 CMP #&3a ; Colon = interactive remote command prefix
808A BCS skip_stn_parse ; Char >= ':': skip number parsing
808C JSR parse_decimal ; Parse decimal number from (fs_options),Y (DECIN)
808F BCC got_station_num ; C=1: dot found, first number was network
8091 STA fs_server_net ; Store network number (n.s = network.station) A=parsed value (accumulated in &B2)
8094 INY ; Y=offset into (fs_options) buffer
8095 JSR parse_decimal ; Parse decimal number from (fs_options),Y (DECIN)
8098 .got_station_num←1← 808F BCC
BEQ skip_stn_parse ; Z=1: no station parsed (empty or non-numeric)
809A STA fs_server_stn ; A=parsed value (accumulated in &B2)
809D .skip_stn_parse←2← 808A BCS← 8098 BEQ
JSR infol2 ; Copy command text to FS buffer
80A0 .scan_for_colon←2← 80A8 BNE← 80BF BNE
DEY ; Scan backward for ':' (interactive prefix)
80A1 BEQ prepare_cmd_dispatch ; Y=0: no colon found, send command
80A3 LDA fs_cmd_data,y ; Read char from FS command buffer
80A6 CMP #&3a ; Test for colon separator
80A8 BNE scan_for_colon ; Not colon: keep scanning backward
80AA JSR oswrch ; Echo colon, then read user input from keyboard Write character
80AD .read_remote_cmd_line←1← 80BA BNE
JSR osrdch ; Check for escape condition Read a character from the current input stream
80B0 JSR check_escape_handler ; Test escape flag before FS reply
80B3 STA fs_cmd_data,y ; Append typed character to command buffer
80B6 INY ; Advance write pointer
80B7 INX ; Increment character count
80B8 CMP #&0d ; Test for CR (end of line)
80BA BNE read_remote_cmd_line ; Not CR: continue reading input
80BC JSR osnewl ; Write newline (characters 10 and 13)
80BF BNE scan_for_colon ; After OSNEWL: loop back to scan for colon
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 &8C05, and from FSCV 2/3/4 indirectly. If CSD handle is zero (not logged in), returns without sending.

80C1 .forward_star_cmd
JSR infol2 ; Copy command text to FS buffer
80C4 TAY ; Y=function code for HDRFN
80C5 .prepare_cmd_dispatch←1← 80A1 BEQ
JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
80C8 LDX fs_cmd_csd ; X=depends on function
80CB BEQ return_1 ; CSD handle zero: not logged in
80CD LDA fs_cmd_data ; A=function code (0-7)
80D0 LDY #&17 ; Y=depends on function
80D2 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 &8024 with base offset Y=&13 (table indices 20-27). 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)
80D4 .fscv_handler
JSR save_fscv_args_with_ptrs ; Store A/X/Y in FS workspace
80D7 CMP #8 ; FSCV function >= 8?
80D9 BCS return_1 ; Function code >= 8? Return (unsupported)
80DB TAX ; X = function code for dispatch
80DC TYA ; Save Y (command text ptr hi)
80DD LDY #&13 ; Y=&13: base offset for FSCV dispatch (indices 20+)
80DF 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 15-19 (base offset Y=&0E).

80E1 .language_handler←1← 8000 JMP
.lang_entry_dispatch←1← 8000 JMP
CPX #5 ; X >= 5: invalid reason code, return
80E3 .svc_dispatch_range←1← 8075 BCS
BCS return_1 ; Out of range: return via RTS
80E5 LDY #&0e ; Y=&0E: base offset for language handlers (index 15+)
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, &0E, &13, &21, 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.

80E7 .dispatch←5← 807F BNE← 80D2 BNE← 80DF BNE← 80E9 BPL← 819D JSR
INX ; Add base offset Y to index X (loop: X += Y+1)
80E8 DEY ; Decrement base offset counter
80E9 BPL dispatch ; Loop until Y exhausted
80EB TAY ; Y=&FF (no further use)
80EC LDA dispatch_0_hi-1,x ; Load high byte of (handler - 1) from table
80EF PHA ; Push high byte onto stack
80F0 LDA dispatch_0_lo-1,x ; Load low byte of (handler - 1) from table
80F3 PHA ; Push low byte onto stack
80F4 LDX fs_options ; Restore X (fileserver options) for use by handler
80F6 .return_1←3← 80CB BEQ← 80D9 BCS← 80E3 BCS
RTS ; RTS pops address, adds 1, jumps to handler
80F7 .service_handler←1← 8003 JMP
NOP ; 9 NOPs: bus settling time for ADLC probe
80F8 NOP ; (bus settling continued)
80F9 NOP ; (bus settling continued)
80FA NOP ; (bus settling continued)
80FB NOP ; (bus settling continued)
80FC NOP ; (bus settling continued)
80FD NOP ; (bus settling continued)
80FE NOP ; (bus settling continued)
80FF NOP ; (bus settling continued)
8100 PHA ; Save service call number
8101 LDA rom_ws_table,x ; Load workspace byte for this ROM slot
8104 PHA ; Push detection flag
8105 BNE adlc_detect_done ; Non-zero: ROM already detected, skip probe
8107 INC rom_ws_table,x ; First call: mark ROM as present
810A LDA station_id_disable_net_nmis ; Read station ID (INTOFF side effect)
810D BEQ no_adlc_found ; Zero: no ADLC hardware, skip
810F CMP station_id_disable_net_nmis ; Second read: bus stability check
8112 BEQ adlc_detect_done ; Same value: ADLC present, continue
8114 .no_adlc_found←1← 810D BEQ
SEC ; C=1: prepare to set disable flag
8115 ROR rom_ws_table,x ; Bit 7 into workspace: disable this ROM
8118 .adlc_detect_done←2← 8105 BNE← 8112 BEQ
PLA ; Restore detection flag
8119 ASL ; C into bit 7 of A
811A PLA ; Restore service call number
811B BMI service_handler_entry ; Service >= &80: always handle (Tube/init)
811D BCS svc_unhandled_return ; C=1 (no ADLC): disable ROM, skip
fall through ↓

Service handler entry

Intercepts three service calls before normal dispatch: &FE: Tube init -- explode character definitions &FF: Full init -- vector setup, copy code to RAM, select NFS &12 (Y=5): Select NFS as active filing system All other service calls < &0D dispatch via dispatch.

811F .service_handler_entry←1← 811B BMI
.check_svc_high←1← 811B BMI
CMP #&fe ; Service >= &FE?
8121 BCC check_svc_12 ; Service < &FE: skip to &12/dispatch check
8123 BNE init_vectors_and_copy ; Service &FF: full init (vectors + RAM copy)
8125 CPY #0 ; Service &FE: Y=0?
8127 BEQ check_svc_12 ; Y=0: no Tube data, skip to &12 check
8129 LDX #6 ; X=6 extra pages for char definitions
812B LDA #osbyte_explode_chars ; OSBYTE &14: explode character RAM
812D JSR osbyte ; Explode character definition RAM (six extra pages), can redefine all characters 32-255 (X=6)
8130 .poll_tube_ready←2← 8133 BPL← 813D JMP
BIT tube_status_1_and_tube_control ; Poll Tube status register 1
8133 BPL poll_tube_ready ; Loop until Tube ready (bit 7 set)
8135 LDA tube_data_register_1 ; Read byte from Tube data register 1
8138 BEQ tube_chars_done ; Zero byte: Tube transfer complete
813A JSR oswrch ; Send Tube char to screen via OSWRCH Write character
813D JMP poll_tube_ready ; Loop for next Tube byte
8140 .init_vectors_and_copy←1← 8123 BNE
LDA #&ad ; EVNTV low = &AD (event handler address)
8142 STA evntv ; Set EVNTV low byte at &0220
8145 LDA #6 ; EVNTV high = &06 (page 6)
8147 STA evntv+1 ; Set EVNTV high byte at &0221
814A LDA #&16 ; BRKV low = &16 (NMI workspace)
814C STA brkv ; Set BRKV low byte at &0202
814F LDA #0 ; BRKV high = &00 (zero page)
8151 STA brkv+1 ; Set BRKV high byte at &0203
8154 LDA #&8e ; Tube control register init value &8E
8156 STA tube_status_1_and_tube_control ; Write to Tube control register
8159 LDY #0 ; Y=0: copy 256 bytes per page
; Copy NMI handler code from ROM to RAM pages &04-&06
815B .cloop←1← 816E BNE
LDA reloc_p4_src,y ; Load ROM byte from page &93
815E STA tube_code_page4,y ; Store to page &04 (Tube code)
8161 LDA reloc_p5_src,y ; Load ROM byte from page &94
8164 STA tube_dispatch_table,y ; Store to page &05 (dispatch table)
8167 LDA l9556,y ; Load ROM byte from page &95
816A STA tube_page6_start,y ; Store to page &06
816D DEY ; DEY wraps 0 -> &FF on first iteration
816E BNE cloop ; Loop until 256 bytes copied per page
8170 JSR tube_post_init ; Run post-init routine in copied code
8173 LDX #&60 ; X=&60: copy 97 bytes (&60..&00)
; Copy NMI workspace initialiser from ROM to &0016-&0076
8175 .copy_nmi_workspace←1← 817B BPL
LDA reloc_zp_src,x ; Load NMI workspace init byte from ROM
8178 STA nmi_workspace_start,x ; Store to zero page &16+X
817A DEX ; Next byte
817B BPL copy_nmi_workspace ; Loop until all workspace bytes copied
817D .tube_chars_done←1← 8138 BEQ
LDA #0 ; A=0: fall through to service &12 check
817F .check_svc_12←2← 8121 BCC← 8127 BEQ
CMP #&12 ; Is this service &12 (select FS)?
8181 BNE not_svc_12_nfs ; No: check if service < &0D
8183 CPY #5 ; Service &12: Y=5 (NFS)?
8185 BNE not_svc_12_nfs ; Not NFS: check if service < &0D
8187 LDA #&0d ; A=&0D: dispatch index for svc_13_select_nfs
8189 BNE do_svc_dispatch ; ALWAYS branch to dispatch ALWAYS branch
818B .not_svc_12_nfs←2← 8181 BNE← 8185 BNE
CMP #&0d ; Service >= &0D?
818D .svc_unhandled_return←1← 811D BCS
BCS return_2 ; Service >= &0D: not handled, return
818F .do_svc_dispatch←1← 8189 BNE
TAX ; X = service number (dispatch index)
8190 LDA svc_state ; Save &A9 (current service state)
8192 PHA ; Push saved &A9
8193 LDA ws_page ; Save &A8 (workspace page number)
8195 PHA ; Push saved &A8
8196 STX svc_state ; Store service number to &A9
8198 STY ws_page ; Store Y (page number) to &A8
819A TYA ; A = Y for dispatch table offset
819B LDY #0 ; Y=0: base offset for service dispatch
819D JSR dispatch ; Dispatch to service handler
81A0 LDX svc_state ; Recover service claim status from &A9
81A2 PLA ; Restore saved &A8 from stack
81A3 STA ws_page ; Write back &A8
fall through ↓

Service 4: unrecognised * command

The first 7 bytes (&81A5-&81AB) are the service handler epilogue: PLA/STA restores &A9, TXA/LDX retrieves romsel_copy, then RTS. This is the return path reached after any dispatched service handler completes.

The service 4 handler itself is dispatched via the table to &81B1 (after 5 NOPs of padding). It makes two match_rom_string calls against the ROM header, reusing header bytes as command strings:

  X=&0C: matches "ROFF" at &8014 — the suffix of the
         copyright string "(C)ROFF" → *ROFF (Remote Off,
         end remote session) — falls through to net_4_resume_remote
  X=5: matches "NET" at &800D — the ROM title suffix
       → *NET (select NFS) — falls through to svc_13_select_nfs

If neither matches, returns with the service call unclaimed.

81A5 .svc_star_command
PLA ; Restore saved A from service dispatch
81A6 STA svc_state ; Save to workspace &A9
81A8 TXA ; Return ROM number in A
81A9 LDX romsel_copy ; Restore X from MOS ROM select copy
81AB .return_2←1← 818D BCS
RTS ; Return to MOS service handler
81AC NOP ; Padding: dispatch targets &81B1
81AD NOP ; NOP padding for command table
81AE NOP ; NOP padding
81AF NOP ; NOP padding
81B0 NOP ; NOP padding
81B1 .svc_4_star_command
LDX #&0c ; ROM offset for "ROFF" (copyright suffix)
81B3 JSR match_rom_string ; Try matching *ROFF command
81B6 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.

81B8 .net_4_resume_remote
LDY #4 ; Y=4: offset of keyboard disable flag
81BA LDA (net_rx_ptr),y ; Read flag from RX buffer
81BC BEQ skip_kbd_reenable ; Zero: keyboard not disabled, skip
81BE LDA #0 ; A=0: value to clear flag and re-enable
81C0 TAX ; X=&00
81C1 STA (net_rx_ptr),y ; Clear keyboard disable flag in buffer
81C3 TAY ; Y=&00
81C4 LDA #osbyte_read_write_econet_keyboard_disable ; OSBYTE &C9: Econet keyboard disable
81C6 JSR osbyte ; Re-enable keyboard (X=0, Y=0) Enable keyboard (for Econet)
81C9 LDA #&0a ; Function &0A: remote operation complete
81CB JSR setup_tx_and_send ; Send notification to controlling station
81CE .clear_osbyte_ce_cf←1← 84B9 JSR
STX nfs_workspace ; Save X (return value from TX)
81D0 LDA #&ce ; OSBYTE &CE: first system mask to reset
81D2 .clear_osbyte_masks←1← 81DD BEQ
LDX nfs_workspace ; Restore X for OSBYTE call
81D4 LDY #&7f ; Y=&7F: AND mask (clear bit 7)
81D6 JSR osbyte ; Reset system mask byte
81D9 ADC #1 ; Advance to next OSBYTE (&CE -> &CF)
81DB CMP #&d0 ; Reached &D0? (past &CF)
81DD .cmd_name_matched
BEQ clear_osbyte_masks ; No: reset &CF too
81DF .skip_kbd_reenable←1← 81BC BEQ
LDA #0 ; A=0: clear remote state
81E1 STA svc_state ; Clear &A9 (service dispatch state)
81E3 .skpspi
STA nfs_workspace ; Clear workspace byte
81E5 RTS ; Return
81E6 .match_net_cmd←1← 81B6 BNE
LDX #5 ; X=5: ROM offset for "NET" match
81E8 JSR match_rom_string ; Try matching *NET command
81EB BNE return_service ; No match: return unclaimed
fall through ↓

Select NFS as active filing system (INIT)

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

81ED .svc_13_select_nfs
JSR call_fscv_shutdown ; Notify current FS of shutdown (FSCV A=6)
81F0 SEC ; C=1 for ROR
81F1 ROR ws_page ; Set bit 7 of l00a8 (inhibit auto-boot)
81F3 JSR issue_vectors_claimed ; Claim OS vectors, issue service &0F
81F6 LDY #&1d ; Y=&1D: top of FS state range
81F8 .initl←1← 8200 BNE
LDA (net_rx_ptr),y ; Copy FS state from RX buffer...
81FA STA fs_state_deb,y ; ...to workspace (offsets &15-&1D)
81FD DEY ; Next byte (descending)
81FE CPY #&14 ; Loop until offset &14 done
8200 BNE initl ; Continue loop
8202 BEQ init_fs_vectors ; ALWAYS branch to init_fs_vectors ALWAYS branch

Service 9: *HELP

Prints the ROM identification string using print_inline.

8204 .svc_9_help
JSR print_inline ; Print ROM identification string
8207 EQUS ".NFS 3.40."
8211 .return_service←2← 81EB BNE← 8226 BNE
LDY ws_page ; Load workspace page for printing
8213 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).

8214 .call_fscv_shutdown←2← 81ED JSR← 8219 JSR
LDA #6 ; FSCV reason 6 = FS shutdown
8216 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.

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

8224 .check_boot_key
EOR #&55 ; XOR with &55: result=0 if key is 'N'
8226 BNE return_service ; Not 'N': return without claiming
8228 TAY ; Y=key
8229 LDA #osbyte_write_keys_pressed ; OSBYTE &78: clear key-pressed state
822B 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.

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

Initialise filing system vectors

Copies 14 bytes from fs_vector_addrs (&8296) 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 svc_13_select_nfs, bypassing the station display. Falls through to issue_vectors_claimed.

8260 .init_fs_vectors←1← 8202 BEQ
LDY #&0d ; Copy 14 bytes: FS vector addresses → FILEV-FSCV
8262 .copy_vectors_loop←1← 8269 BPL
LDA fs_vector_addrs,y ; Load vector address from table
8265 STA filev,y ; Write to FILEV-FSCV vector table
8268 DEY ; Next byte (descending)
8269 BPL copy_vectors_loop ; Loop until all 14 bytes copied
826B JSR setup_rom_ptrs_netv ; Read ROM ptr table addr, install NETV
826E LDY #&1b ; Install 7 handler entries in ROM ptr table
8270 LDX #7 ; 7 FS vectors to install
8272 JSR store_rom_ptr_pair ; Install each 3-byte vector entry
8275 STX svc_state ; X=0 after loop; store as workspace offset
fall through ↓

Issue 'vectors claimed' service and optionally auto-boot

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

8277 .issue_vectors_claimed←1← 81F3 JSR
LDA #osbyte_issue_service_request ; A=&8F: issue service request
8279 LDX #&0f ; X=&0F: 'vectors claimed' service
827B JSR osbyte ; Issue paged ROM service call, Reason X=15 - Vectors claimed
827E LDX #&0a ; X=&0A: service &0A
8280 JSR osbyte ; Issue service &0A
8283 LDX ws_page ; Non-zero after hard reset: skip auto-boot
8285 BNE return_3 ; Non-zero: skip auto-boot
8287 LDX #&8e ; X = lo byte of auto-boot string at &828E
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.

8289 .run_fscv_cmd←2← 8335 LDA← 833B LDA
LDY #&82 ; Y=&82: ROM page high byte
828B 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.
828E 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.

8296 .fs_vector_addrs←1← 8262 LDA
EQUW &FF1B ; FILEV dispatch (&FF1B)
8298 EQUW &FF1E ; ARGSV dispatch (&FF1E) ARGSV dispatch lo
829A EQUW &FF21 ; BGETV dispatch (&FF21) BGETV dispatch hi
829C EQUW &FF24 ; BPUTV dispatch (&FF24) BPUTV dispatch lo
829E EQUW &FF27 ; GBPBV dispatch (&FF27) GBPBV dispatch lo GBPBV dispatch hi
82A0 EQUW &FF2A ; FINDV dispatch (&FF2A) FINDV dispatch lo FINDV dispatch hi
82A2 EQUW &FF2D ; FSCV dispatch (&FF2D) FSCV dispatch lo
82A4 EQUW &8705 ; FILEV handler (&8705)
82A6 EQUB &4A ; (ROM bank — not read)
82A7 EQUW &8924 ; ARGSV handler (&8924)
82A9 EQUB &44 ; (ROM bank — not read)
82AA EQUW &855C ; BGETV handler (&855C)
82AC EQUB &57 ; (ROM bank — not read)
82AD EQUW &840F ; BPUTV handler (&840F)
82AF EQUB &42 ; (ROM bank — not read)
82B0 EQUW &8A2E ; GBPBV handler (&8A2E)
82B2 EQUB &41 ; (ROM bank — not read)
82B3 EQUW &8994 ; FINDV handler (&8994)
82B5 EQUB &52 ; (ROM bank — not read)
82B6 EQUW &80D4 ; FSCV handler (&80D4) 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)
82B8 .svc_1_abs_workspace
CPY #&10 ; Already at page &10 or above?
82BA BCS return_3 ; Yes: nothing to claim
82BC LDY #&10 ; Claim pages &0D-&0F (3 pages)
82BE .return_3←2← 8285 BNE← 82BA BCS
RTS ; Return (workspace claim done)
82BF EQUB &76, &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)
82C1 .svc_2_private_workspace
STY net_rx_ptr_hi ; Store page as RX buffer high byte
82C3 INY ; Next page for NFS workspace
82C4 STY nfs_workspace_hi ; Store page as NFS workspace high
82C6 LDA #0 ; A=0 for clearing workspace
82C8 LDY #4 ; Y=4: remote status offset
82CA STA (net_rx_ptr),y ; Clear status byte in net receive buffer
82CC LDY #&ff ; Y=&FF: used for later iteration
82CE STA net_rx_ptr ; Clear RX ptr low byte
82D0 STA nfs_workspace ; Clear workspace ptr low byte
82D2 STA ws_page ; Clear RXCB iteration counter
82D4 STA tx_clear_flag ; Clear TX semaphore (no TX in progress)
82D7 TAX ; X=0 for OSBYTE X=&00
82D8 LDA #osbyte_read_write_last_break_type ; OSBYTE &FD: read type of last reset
82DA JSR osbyte ; Read type of last reset
82DD TXA ; X = break type from OSBYTE result X=value of type of last reset
82DE BEQ read_station_id ; Soft break (X=0): skip FS init
82E0 LDY #&15 ; Y=&15: printer station offset in RX buffer
82E2 LDA #&fe ; &FE = no server selected
82E4 STA fs_server_stn ; Station &FE = no server selected
82E7 STA (net_rx_ptr),y ; Store &FE at printer station offset
82E9 LDA #0 ; A=0 for clearing workspace fields
82EB STA fs_server_net ; Clear network number
82EE STA prot_status ; Clear protection status
82F1 STA fs_messages_flag ; Clear message flag
82F4 STA fs_boot_option ; Clear boot option
82F7 INY ; Y=&16
82F8 STA (net_rx_ptr),y ; Clear net number at RX buffer offset &16
82FA LDY #3 ; Init printer server: station &FE, net 0
82FC STA (nfs_workspace),y ; Store net 0 at workspace offset 3
82FE DEY ; Y=2: printer station offset Y=&02
82FF LDA #&eb ; &FE = no printer server
8301 STA (nfs_workspace),y ; Store &FE at printer station in workspace
8303 .init_rxcb_entries←1← 8310 BNE
LDA ws_page ; Load RXCB counter
8305 JSR calc_handle_offset ; Convert to workspace byte offset
8308 BCS read_station_id ; C=1: past max handles, done
830A LDA #&3f ; Mark RXCB as available
830C STA (nfs_workspace),y ; Write &3F flag to workspace
830E INC ws_page ; Next RXCB number
8310 BNE init_rxcb_entries ; Loop for all RXCBs
8312 .read_station_id←2← 82DE BEQ← 8308 BCS
LDA station_id_disable_net_nmis ; Read station ID (also INTOFF)
8315 LDY #&14 ; Y=&14: station ID offset in RX buffer
8317 STA (net_rx_ptr),y ; Store our station number
8319 JSR trampoline_adlc_init ; Initialise ADLC hardware
831C LDA #&40 ; Enable user-level RX (LFLAG=&40)
831E 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=&9076, rom=current) into the ROM pointer table at offset &36, installing osword_dispatch as the NETV handler.

8321 .setup_rom_ptrs_netv←1← 826B JSR
LDA #osbyte_read_rom_ptr_table_low ; OSBYTE &A8: read ROM pointer table address
8323 LDX #0 ; X=0: read low byte
8325 LDY #&ff ; Y=&FF: read high byte
8327 JSR osbyte ; Returns table address in X (lo) Y (hi) Read address of ROM pointer table
832A STX osrdsc_ptr ; Store table base address low byte X=value of address of ROM pointer table (low byte)
832C STY osrdsc_ptr_hi ; Store table base address high byte Y=value of address of ROM pointer table (high byte)
832E LDY #&36 ; NETV extended vector offset in ROM ptr table
8330 STY netv ; Set NETV low byte = &36 (vector dispatch)
8333 LDX #1 ; Install 1 entry (NETV) in ROM ptr table
8335 .store_rom_ptr_pair←2← 8272 JSR← 8347 BNE
LDA run_fscv_cmd,y ; Load handler address low byte from table
8338 STA (osrdsc_ptr),y ; Store to ROM pointer table
833A INY ; Next byte
833B LDA run_fscv_cmd,y ; Load handler address high byte from table
833E STA (osrdsc_ptr),y ; Store to ROM pointer table
8340 INY ; Next byte
8341 LDA romsel_copy ; Write current ROM bank number
8343 STA (osrdsc_ptr),y ; Store ROM number to ROM pointer table
8345 INY ; Advance to next entry position
8346 DEX ; Count down entries
8347 BNE store_rom_ptr_pair ; Loop until all entries installed
8349 LDY nfs_workspace_hi ; Y = workspace high byte + 1 = next free page
834B INY ; Advance past workspace page
834C 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 &77 (close SPOOL/EXEC files) to release the Econet network printer on FS switch.

834D .fscv_6_shutdown
LDY #&1d ; Copy 10 bytes: FS state to workspace backup
834F .fsdiel←1← 8357 BNE
LDA fs_state_deb,y ; Load FS state byte at offset Y
8352 STA (net_rx_ptr),y ; Store to workspace backup area
8354 DEY ; Next byte down
8355 CPY #&14 ; Offsets &15-&1D: server, handles, OPT, etc.
8357 BNE fsdiel ; Loop for offsets &1D..&15
8359 LDA #osbyte_close_spool_exec ; A=&77: OSBYTE close spool/exec
835B JMP osbyte ; Close any *SPOOL and *EXEC files
835E .match_rom_string←2← 81B3 JSR← 81E8 JSR
LDY ws_page ; Y = saved text pointer offset
8360 .match_cmd_chars←1← 8371 BNE
LDA (os_text_ptr),y ; Load next input character
8362 CMP #&2e ; Is it a '.' (abbreviation)?
8364 BEQ skip_space_next ; Yes: skip to space skipper (match)
8366 AND #&df ; Force uppercase (clear bit 5)
8368 BEQ check_rom_end ; Input char is NUL/space: check ROM byte
836A CMP binary_version,x ; Compare with ROM string byte
836D BNE check_rom_end ; Mismatch: check if ROM string ended
836F INY ; Advance input pointer
8370 INX ; Advance ROM string pointer
8371 BNE match_cmd_chars ; Continue matching (always taken)
8373 .check_rom_end←2← 8368 BEQ← 836D BNE
LDA binary_version,x ; Load ROM string byte at match point
8376 BEQ skip_spaces ; Zero = end of ROM string = full match
8378 RTS ; Non-zero = partial/no match; Z=0
8379 .skip_space_next←2← 8364 BEQ← 837E BEQ
INY ; Skip this space
837A .skip_spaces←2← 8376 BEQ← 8DE0 JSR
LDA (os_text_ptr),y ; Load next input character
837C CMP #&20 ; Is it a space?
837E BEQ skip_space_next ; Yes: keep skipping
8380 EOR #&0d ; XOR with CR: Z=1 if end of line
8382 RTS ; Return with Z flag result

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.

8383 .init_tx_reply_port←1← 83F0 JSR
LDA #&90 ; A=&90: FS reply port (PREPLY)
8385 .init_tx_ctrl_port←1← 885A JSR
JSR init_tx_ctrl_block ; Init TXCB from template
8388 STA txcb_port ; Store port number in TXCB
838A LDA #3 ; Control byte: 3 = transmit
838C STA txcb_start ; Store control byte in TXCB
838E DEC txcb_ctrl ; Decrement TXCB flag to arm TX
8390 RTS ; Return after port setup

Initialise TX control block at &00C0 from template

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

8391 .init_tx_ctrl_block←4← 8385 JSR← 83DF JSR← 8428 JSR← 8FEE LDA
PHA ; Preserve A across call
8392 LDY #&0b ; Copy 12 bytes (Y=11..0)
8394 .fstxl1←1← 83A5 BPL
LDA tx_ctrl_template,y ; Load template byte
8397 STA txcb_ctrl,y ; Store to TX control block at &00C0
839A CPY #2 ; Y < 2: also copy FS server station/network
839C BPL fstxl2 ; Skip station/network copy for Y >= 2
839E LDA fs_server_stn,y ; Load FS server station (Y=0) or network (Y=1)
83A1 STA txcb_dest,y ; Store to dest station/network at &00C2
83A4 .fstxl2←1← 839C BPL
DEY ; Next byte (descending)
83A5 BPL fstxl1 ; Loop until all 12 bytes copied
83A7 PLA ; Restore A
83A8 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.

83A9 .tx_ctrl_template←1← 8394 LDA
EQUB &80, &99, ; Control flag Port (FS command = &99) &00, &00, ; Buffer start low Buffer start high (page &00, &0F ; &0F)
83AF .tx_ctrl_upper←3← 88D8 BIT← 89B7 BIT← 9177 BIT
EQUB &FF, &FF, ; Buffer start pad (4-byte Econet addr) &FF, &0F, ; Buffer start pad Buffer end low Buffer end &FF, &FF ; high (page &0F) Buffer end pad 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
83B5 .prepare_cmd_with_flag←1← 8A7F JSR
PHA ; Save flag byte for command
83B6 SEC ; C=1: include flag in FS command
83B7 BCS store_fs_hdr_fn ; ALWAYS branch to prepare_fs_cmd ALWAYS branch
83B9 .prepare_cmd_clv←2← 8720 JSR← 87C7 JSR
CLV ; V=0: command has no flag byte
83BA 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 &8C05 for "BYE".

83BC .bye_handler
LDA #osbyte_close_spool_exec ; A=&77: OSBYTE close spool/exec
83BE JSR osbyte ; Close any *SPOOL and *EXEC files
83C1 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)
83C3 .prepare_fs_cmd←12← 80C5 JSR← 887D JSR← 88F3 JSR← 893F JSR← 8966 JSR← 89DD JSR← 8A04 JSR← 8ADA JSR← 8B95 JSR← 8C3C JSR← 8C73 JSR← 8CDE JSR
CLV ; V=0: standard FS command path
83C4 .init_tx_ctrl_data←2← 88DB JSR← 89BA JSR
.prepare_fs_cmd_v←2← 88DB JSR← 89BA JSR
LDA fs_urd_handle ; Copy URD handle from workspace to buffer
83C7 STA fs_cmd_urd ; Store URD at &0F02
83CA .store_fs_hdr_clc←1← 83BA BVC
CLC ; CLC: no byte-stream path
83CB .store_fs_hdr_fn←1← 83B7 BCS
STY fs_cmd_y_param ; Store function code at &0F01
83CE LDY #1 ; Y=1: copy CSD (offset 1) then LIB (offset 0)
83D0 .copy_dir_handles←1← 83D7 BPL
LDA fs_csd_handle,y ; Copy CSD and LIB handles to command buffer A=timeout period for FS reply
83D3 STA fs_cmd_csd,y ; Store at &0F03 (CSD) and &0F04 (LIB)
83D6 DEY ; Y=function code
83D7 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)
83D9 .build_send_fs_cmd←1← 8B33 JSR
PHP ; Save carry (FS path vs byte-stream)
83DA LDA #&90 ; Reply port &90 (PREPLY)
83DC STA fs_cmd_type ; Store at &0F00 (HDRREP)
83DF JSR init_tx_ctrl_block ; Copy TX template to &00C0
83E2 TXA ; A = X (buffer extent)
83E3 ADC #5 ; HPTR = header (5) + data (X) bytes to send
83E5 STA txcb_end ; Store to TXCB end-pointer low
83E7 PLP ; Restore carry flag
83E8 BCS dofsl5 ; C=1: byte-stream path (BSXMIT)
83EA PHP ; Save flags for send_fs_reply_cmd
83EB JSR setup_tx_ptr_c0 ; Point net_tx_ptr to &00C0; transmit
83EE PLP ; Restore flags
83EF .send_fs_reply_cmd←2← 87D3 JSR← 8AB7 JSR
PHP ; Save flags (V flag state)
83F0 JSR init_tx_reply_port ; Set up RX wait for FS reply
83F3 JSR send_to_fs_star ; Transmit and wait (BRIANX)
83F6 PLP ; Restore flags
83F7 .dofsl7←1← 840D BCC
INY ; Y=1: skip past command code byte
83F8 LDA (txcb_start),y ; Load return code from FS reply
83FA TAX ; X = return code
83FB BEQ return_dofsl7 ; Zero: success, return
83FD BVC check_fs_error ; V=0: standard path, error is fatal
83FF ADC #&2a ; ADC #&2A: test for &D6 (not found)
8401 .check_fs_error←1← 83FD BVC
BNE store_fs_error ; Non-zero: hard error, go to FSERR
8403 .return_dofsl7←1← 83FB BEQ
RTS ; Return (success or soft &D6 error)
8404 .dofsl5←1← 83E8 BCS
PLA ; Discard saved flags from stack
8405 LDX #&c0 ; X=&C0: TXCB address for byte-stream TX
8407 INY ; Y++ past command code
8408 JSR econet_tx_retry ; Byte-stream transmit with retry
840B STA fs_load_addr_3 ; Store result to &B3
840D BCC dofsl7 ; C=0: success, check reply code
840F .bputv_handler
CLC ; CLC for address addition
fall through ↓

Handle BPUT/BGET file byte I/O

BPUTV enters at &83DC (CLC; fall through) and BGETV enters at &855C (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
8410 .handle_bput_bget←1← 855D JSR
PHA ; Save A (BPUT byte) on stack
8411 STA fs_error_flags ; Also save byte at &0FDF for BSXMIT
8414 TXA ; Transfer X for stack save
8415 PHA ; Save X on stack
8416 TYA ; Transfer Y (handle) for stack save
8417 PHA ; Save Y (handle) on stack
8418 PHP ; Save P (C = BPUT/BGET selector) on stack
8419 STY fs_spool_handle ; Save handle for SPOOL/EXEC comparison later
841B JSR handle_to_mask_clc ; Convert handle Y to single-bit mask
841E STY fs_handle_mask ; Store handle bitmask at &0FDE
8421 STY fs_spool0 ; Store handle bitmask for sequence tracking
8423 LDY #&90 ; &90 = data port (PREPLY)
8425 STY fs_putb_buf ; Store reply port in command buffer
8428 JSR init_tx_ctrl_block ; Set up 12-byte TXCB from template
842B LDA #&dc ; CB reply buffer at &0FDC
842D STA txcb_start ; Store reply buffer ptr low in TXCB
842F LDA #&e0 ; Error buffer at &0FE0
8431 STA txcb_end ; Store error buffer ptr low in TXCB
8433 INY ; Y=1 (from init_tx_ctrl_block exit)
8434 LDX #9 ; X=9: BPUT function code
8436 PLP ; Restore C: selects BPUT (0) vs BGET (1)
8437 BCC store_retry_count ; C=0 (BPUT): keep X=9
8439 DEX ; X=&08
843A .store_retry_count←1← 8437 BCC
STX fs_getb_buf ; Store function code at &0FDD
843D LDA fs_spool0 ; Load handle bitmask for BSXMIT
843F LDX #&c0 ; X=&C0: TXCB address for econet_tx_retry
8441 JSR econet_tx_retry ; Transmit via byte-stream protocol
8444 LDX fs_getb_buf ; Load reply byte from buffer
8447 BEQ update_sequence_return ; Zero reply = success, skip error handling
8449 LDY #&1f ; Copy 32-byte reply to error buffer at &0FE0
844B .error1←1← 8452 BPL
LDA fs_putb_buf,y ; Load reply byte at offset Y
844E STA fs_error_buf,y ; Store to error buffer at &0FE0+Y
8451 DEY ; Next byte (descending)
8452 BPL error1 ; Loop until all 32 bytes copied
8454 TAX ; X=File handle
8455 LDA #osbyte_read_write_exec_file_handle ; A=&C6: read *EXEC file handle
8457 JSR osbyte ; Read/Write *EXEC file handle
845A LDA #&14 ; ')': offset into "SP." string at &8529
845C CPY fs_spool_handle ; Y=value of *SPOOL file handle
845E BEQ close_spool_exec ; Handle matches SPOOL -- close it
8460 LDA #&18 ; '-': offset into "E." string at &852D
8462 CPX fs_spool_handle ; X=value of *EXEC file handle
8464 BNE dispatch_fs_error ; No EXEC match -- skip close
8466 .close_spool_exec←1← 845E BEQ
TAX ; X = string offset for OSCLI close
8467 LDY #&85 ; Y=&85: high byte of OSCLI string in ROM
8469 JSR oscli ; Close SPOOL/EXEC via "*SP." or "*E."
846C .dispatch_fs_error←1← 8464 BNE
LDA #&e0 ; Reset CB pointer to error buffer at &0FE0
846E STA txcb_start ; Reset reply ptr to error buffer
8470 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.

8473 .store_fs_error←1← 8401 BNE
STX fs_last_error ; Remember raw FS error code
8476 LDY #1 ; Y=1: point to error number byte in reply
8478 CPX #&a8 ; Clamp FS errors below &A8 to standard &A8
847A BCS error_code_clamped ; Error >= &A8: keep original value
847C LDA #&a8 ; Error < &A8: override with standard &A8
847E STA (txcb_start),y ; Write clamped error number to reply buffer
8480 .error_code_clamped←1← 847A BCS
LDY #&ff ; Start scanning from offset &FF (will INY to 0)
8482 .copy_error_to_brk←1← 848A BNE
INY ; Next byte in reply buffer
8483 LDA (txcb_start),y ; Copy reply buffer to &0100 for BRK execution
8485 STA l0100,y ; Build BRK error block at &0100
8488 EOR #&0d ; Scan for CR terminator (&0D)
848A BNE copy_error_to_brk ; Continue until CR found
848C STA l0100,y ; Replace CR with zero = BRK error block end
848F BEQ execute_downloaded ; Execute as BRK error block at &0100; ALWAYS ALWAYS branch
8491 .update_sequence_return←1← 8447 BEQ
STA fs_sequence_nos ; Save updated sequence number
8494 PLA ; Restore Y from stack
8495 TAY ; Transfer A to Y for indexing
8496 PLA ; Restore X from stack
8497 TAX ; Transfer to X for return
8498 PLA ; Restore A from stack
8499 .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.

849A .lang_1_remote_boot
LDY #4 ; Y=4: remote status flag offset
849C LDA (net_rx_ptr),y ; Read remote status from RX CB
849E BEQ remot1 ; Zero: not remoted, set up session
84A0 .rchex←1← 84E6 BNE
JMP clear_jsr_protection ; Already remoted: clear and return
84A3 .remot1←2← 849E BEQ← 84DC BEQ
ORA #9 ; Set remote status: bits 0+3 (ORA #9)
84A5 STA (net_rx_ptr),y ; Store updated remote status
84A7 LDX #&80 ; X=&80: RX data area offset
84A9 LDY #&80 ; Y=&80: read source station low
84AB LDA (net_rx_ptr),y ; Read source station lo from RX data at &80
84AD PHA ; Save source station low byte
84AE INY ; Y=&81
84AF LDA (net_rx_ptr),y ; Read source station hi from RX data at &81
84B1 LDY #&0f ; Save controlling station to workspace &0E/&0F
84B3 STA (nfs_workspace),y ; Store station high to ws+&0F
84B5 DEY ; Y=&0E Y=&0e
84B6 PLA ; Restore source station low
84B7 STA (nfs_workspace),y ; Store station low to ws+&0E
84B9 JSR clear_osbyte_ce_cf ; Clear OSBYTE &CE/&CF flags
84BC JSR ctrl_block_setup ; Set up TX control block
84BF LDX #1 ; X=1: disable keyboard
84C1 LDY #0 ; Y=0 for OSBYTE
84C3 LDA #osbyte_read_write_econet_keyboard_disable ; Disable keyboard for remote session
84C5 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.

84C8 .lang_3_execute_at_0100
JSR clear_jsr_protection ; Allow JSR to page 1 (stack page)
84CB LDX #2 ; Zero bytes &0100-&0102
84CD LDA #0 ; A=0: zero execution header bytes
84CF .zero_exec_header←1← 84D3 BPL
STA l0100,x ; BRK at &0100 as safe default
84D2 DEX ; Next byte
84D3 BPL zero_exec_header ; Loop until all zeroed
84D5 .execute_downloaded←2← 848F BEQ← 850E 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.

84D8 .lang_4_remote_validated
LDY #4 ; Y=4: RX control block byte 4 (remote status)
84DA LDA (net_rx_ptr),y ; Read remote status flag
84DC BEQ remot1 ; Zero = not remoted; allow new session
84DE LDY #&80 ; Read source station from RX data at &80
84E0 LDA (net_rx_ptr),y ; A = source station number
84E2 LDY #&0e ; Compare against controlling station at &0E
84E4 CMP (nfs_workspace),y ; Check if source matches controller
84E6 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.

84E8 .lang_0_insert_remote_key
LDY #&82 ; Read keypress from RX data at &82
84EA LDA (net_rx_ptr),y ; Load character byte
84EC TAY ; Y = character to insert
84ED LDX #0 ; X = buffer 0 (keyboard input)
84EF JSR clear_jsr_protection ; Release JSR protection before inserting key
84F2 LDA #osbyte_insert_input_buffer ; OSBYTE &99: insert char into input buffer
84F4 JMP osbyte ; Tail call: insert character Y into buffer X Insert character Y into input buffer X
84F7 .error_not_listening←1← 854A BEQ
LDA #8 ; Error code 8: "Not listening" error
84F9 BNE set_listen_offset ; ALWAYS branch to set_listen_offset ALWAYS branch
84FB .nlistn←1← 86D0 JMP
LDA (net_tx_ptr,x) ; Load TX status byte for error lookup
84FD .nlisne←2← 855A BNE← 89FC JMP
AND #7 ; Mask to 3-bit error code (0-7)
84FF .set_listen_offset←1← 84F9 BNE
TAX ; X = error code index
8500 LDY error_offsets,x ; Look up error message offset from table
8503 LDX #0 ; X=0: start writing at &0101
8505 STX l0100 ; Store BRK opcode at &0100
8508 .copy_error_message←1← 8512 BNE
LDA error_msg_table,y ; Load error message byte
850B STA l0101,x ; Build error message at &0101+
850E BEQ execute_downloaded ; Zero byte = end of message; go execute BRK
8510 INY ; Next source byte
8511 INX ; Next dest byte
8512 BNE copy_error_message ; Continue copying message
8514 EQUS "SP."
8517 EQUB &0D, &45, &2E, &0D
851B .send_to_fs_star←5← 83F3 JSR← 8779 JSR← 8864 JSR← 9031 JMP← 9296 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.

851D .send_to_fs
PHA ; Save function code on stack
851E LDA rx_flags ; Load current rx_flags
8521 PHA ; Save rx_flags on stack for restore
8522 ORA #&80 ; Set bit7: FS transaction in progress
8524 STA rx_flags ; Write back updated rx_flags
8527 .skip_rx_flag_set
LDA #0 ; Push two zero bytes as timeout counters
8529 PHA ; First zero for timeout
852A PHA ; Second zero for timeout
852B TAY ; Y=0: index for flag byte check Y=&00
852C TSX ; TSX: index stack-based timeout via X
852D .fs_reply_poll←3← 8537 BNE← 853C BNE← 8541 BNE
JSR check_escape_handler ; Check for user escape condition
8530 .incpx
LDA (net_tx_ptr),y ; Read flag byte from TX control block
8532 BMI fs_wait_cleanup ; Bit 7 set = reply received
8534 DEC l0101,x ; Three-stage nested timeout: inner loop
8537 BNE fs_reply_poll ; Inner not expired: keep polling
8539 DEC l0102,x ; Middle timeout loop
853C BNE fs_reply_poll ; Middle not expired: keep polling
853E DEC l0104,x ; Outer timeout loop (slowest)
8541 BNE fs_reply_poll ; Outer not expired: keep polling
8543 .fs_wait_cleanup←1← 8532 BMI
PLA ; Pop first timeout byte
8544 PLA ; Pop second timeout byte
8545 PLA ; Pop saved rx_flags into A
8546 STA rx_flags ; Restore saved rx_flags from stack
8549 PLA ; Pop saved function code
854A BEQ error_not_listening ; A=saved func code; zero would mean no reply
854C RTS ; Return to caller

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.

854D .check_escape_handler←3← 80B0 JSR← 852D JSR← 86B7 JSR
BIT escape_flag ; Test escape flag (bit 7)
854F BPL return_4 ; Bit 7 clear: no escape, return
8551 LDA #osbyte_acknowledge_escape ; A=&7E: acknowledge escape OSBYTE
8553 JSR osbyte ; Clear escape condition and perform escape effects
8556 LSR ; LSR: get escape result bit
8557 STA (net_tx_ptr),y ; Store escape result to TXCB
8559 ASL ; Restore A
855A BNE nlisne ; Non-zero: report 'Not listening'
855C .bgetv_handler
SEC ; C=1: flag for BGET mode
855D JSR handle_bput_bget ; Handle BGET via FS command Handle BPUT/BGET file byte I/O
8560 SEC ; SEC: set carry for error check
8561 LDA #&fe ; A=&FE: mask for EOF check
8563 BIT fs_error_flags ; BIT l0fdf: test error flags
8566 BVS return_4 ; V=1: error, return early
8568 CLC ; CLC: no error
8569 PHP ; Save flags for EOF check
856A LDA fs_spool0 ; Load BGET result byte
856C PLP ; Restore flags
856D BMI bgetv_shared_jsr ; Bit7 set: skip FS flag clear
856F JSR clear_fs_flag ; Clear FS flag for handle
8572 .bgetv_shared_jsr←1← 856D BMI
JSR set_fs_flag ; Set EOF flag for this handle
8575 .load_handle_mask
LDA fs_handle_mask ; Load handle bitmask for caller
8578 .return_4←2← 854F BPL← 8566 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.
8579 .error_msg_table←1← 8508 LDA
EQUB &A0
857A EQUS "Line Jammed."
8586 EQUB &A1
8587 EQUS "Net Error."
8591 EQUB &A2
8592 EQUS "Not listening."
85A0 EQUB &A3
85A1 EQUS "No Clock."
85AA EQUB &11
85AB EQUS "Escape."
85B2 EQUB &CB
85B3 EQUS "Bad Option."
85BE EQUB &A5
85BF 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.

85C8 .save_fscv_args_with_ptrs←3← 80D4 JSR← 8994 JSR← 8BD7 JSR
STX os_text_ptr ; X to os_text_ptr (text ptr lo)
85CA STY os_text_ptr_hi ; Y to os_text_ptr hi
85CC STX fs_cmd_ptr ; X to FS command ptr lo
85CF 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
85D2 .save_fscv_args←3← 8705 JSR← 8924 JSR← 8A2E JSR
STA fs_last_byte_flag ; A = function code / command
85D4 STX fs_options ; X = control block ptr lo
85D6 STY fs_block_offset ; Y = control block ptr hi
85D8 STX fs_crc_lo ; X dup for indexed access via (fs_crc)
85DA STY fs_crc_hi ; Y dup for indexed access
85DC 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 &85FA. The two formats use different bit layouts for file protection attributes.

85DD .decode_attribs_6bit←2← 88B7 JSR← 88E2 JSR
LDY #&0e ; Y=&0E: attribute byte offset in param block
85DF LDA (fs_options),y ; Load FS attribute byte
85E1 AND #&3f ; Mask to 6 bits (FS → BBC direction)
85E3 LDX #4 ; X=4: skip first 4 table entries (BBC→FS half)
85E5 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 &85FA. 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
85E7 .decode_attribs_5bit←2← 87DE JSR← 88FF JSR
AND #&1f ; Mask to 5 bits (BBC → FS direction)
85E9 LDX #&ff ; X=&FF: INX makes 0; start from table index 0
85EB .attrib_shift_bits←1← 85E5 BNE
STA fs_error_ptr ; Temp storage for source bitmask to shift out
85ED LDA #0 ; A=0: accumulate destination bits here
85EF .map_attrib_bits←1← 85F7 BNE
INX ; Next table entry
85F0 LSR fs_error_ptr ; Shift out source bits one at a time
85F2 BCC skip_set_attrib_bit ; Bit was 0: skip this destination bit
85F4 ORA access_bit_table,x ; OR in destination bit from lookup table
85F7 .skip_set_attrib_bit←1← 85F2 BCC
BNE map_attrib_bits ; Loop while source bits remain (A != 0)
85F9 RTS ; Return; A = converted attribute bitmask
85FA .access_bit_table←1← 85F4 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
8605 .print_inline←13← 8204 JSR← 822E JSR← 824E JSR← 825B JSR← 8C44 JSR← 8C4E JSR← 8C5C JSR← 8C67 JSR← 8C7C JSR← 8C91 JSR← 8CA4 JSR← 8CB3 JSR← 8D9E JSR
PLA ; Pop return address (low) — points to last byte of JSR
8606 STA fs_load_addr ; Store return addr low as string ptr
8608 PLA ; Pop return address (high)
8609 STA fs_load_addr_hi ; Store return addr high as string ptr
860B LDY #0 ; Y=0: offset for indirect load
860D .print_inline_char←1← 861A JMP
INC fs_load_addr ; Advance pointer past return address / to next char
860F BNE print_next_char ; No page wrap: skip high byte inc
8611 INC fs_load_addr_hi ; Handle page crossing in pointer
8613 .print_next_char←1← 860F BNE
LDA (fs_load_addr),y ; Load next byte from inline string
8615 BMI jump_via_addr ; Bit 7 set? Done — this byte is the next opcode
8617 JSR osasci ; Write character
861A JMP print_inline_char ; Continue printing next character
861D .jump_via_addr←1← 8615 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)
Xpreserved
Yoffset past last digit parsed
8620 .parse_decimal←2← 808C JSR← 8095 JSR
LDA #0 ; Zero accumulator
8622 STA fs_load_addr_2 ; Initialise accumulator to zero
8624 .scan_decimal_digit←1← 863D BNE
LDA (fs_options),y ; Load next char from buffer
8626 CMP #&2e ; Dot separator?
8628 BEQ parse_decimal_rts ; Yes: exit with C=1 (dot found)
862A BCC no_dot_exit ; Control char or space: done
862C AND #&0f ; Mask ASCII digit to 0-9
862E STA fs_load_addr_3 ; Save new digit
8630 ASL fs_load_addr_2 ; Running total * 2
8632 LDA fs_load_addr_2 ; A = running total * 2
8634 ASL ; A = running total * 4
8635 ASL ; A = running total * 8
8636 ADC fs_load_addr_2 ; + total*2 = total * 10
8638 ADC fs_load_addr_3 ; + digit = total*10 + digit
863A STA fs_load_addr_2 ; Store new running total
863C INY ; Advance to next char
863D BNE scan_decimal_digit ; Loop (always: Y won't wrap to 0)
863F .no_dot_exit←1← 862A BCC
CLC ; No dot found: C=0
8640 .parse_decimal_rts←1← 8628 BEQ
LDA fs_load_addr_2 ; Return result in A
8642 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
8643 .handle_to_mask_a←3← 886B JSR← 8A49 JSR← 8F53 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
8644 .handle_to_mask_clc←2← 841B JSR← 892F 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
8645 .handle_to_mask←1← 8998 JSR
PHA ; Save A (will be restored on exit)
8646 TXA ; Save X (will be restored on exit)
8647 PHA ; (second half of X save)
8648 TYA ; A = handle from Y
8649 BCC y2fsl5 ; C=0: always convert
864B BEQ handle_mask_exit ; C=1 and Y=0: skip (handle 0 = none)
864D .y2fsl5←1← 8649 BCC
SEC ; C=1 and Y!=0: convert
864E SBC #&1f ; A = handle - &1F (1-based bit position)
8650 TAX ; X = shift count
8651 LDA #1 ; Start with bit 0 set
8653 .y2fsl2←1← 8655 BNE
ASL ; Shift bit left
8654 DEX ; Count down
8655 BNE y2fsl2 ; Loop until correct position
8657 ROR ; Undo final extra shift
8658 TAY ; Y = resulting bitmask
8659 BNE handle_mask_exit ; Non-zero: valid mask, skip to exit
865B DEY ; Zero: invalid handle, set Y=&FF
865C .handle_mask_exit←2← 864B BEQ← 8659 BNE
PLA ; Restore X
865D TAX ; Restore X from stack
865E PLA ; Restore A
865F 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
8660 .mask_to_handle←2← 89C7 JSR← 8F6B JSR
LDX #&1f ; X = &1F (handle base - 1)
8662 .fs2al1←1← 8664 BNE
INX ; Count this bit position
8663 LSR ; Shift mask right; C=0 when done
8664 BNE fs2al1 ; Loop until all bits shifted out
8666 TXA ; A = X = &1F + bit position = handle
8667 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
8668 .compare_addresses←2← 875F JSR← 8815 JSR
LDX #4 ; Compare 4 bytes (index 4,3,2,1)
866A .compare_addr_byte←1← 8671 BNE
LDA addr_work,x ; Load byte from first address
866C EOR fs_load_addr_3,x ; XOR with corresponding byte
866E BNE return_compare ; Mismatch: Z=0, return unequal
8670 DEX ; Next byte
8671 BNE compare_addr_byte ; Continue comparing
8673 .return_compare←1← 866E BNE
RTS ; Return with Z flag result
8674 .fscv_7_read_handles
LDX #&20 ; X=first handle (&20)
8676 LDY #&27 ; Y=last handle (&27)
8678 .return_fscv_handles
RTS ; Return (FSCV 7 read handles)

Set bit(s) in EOF hint flags (&0E07)

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

On EntryAbitmask of bits to set
On ExitAupdated fs_eof_flags value
8679 .set_fs_flag←5← 8572 JSR← 896C JSR← 89C3 JSR← 89E3 JSR← 8AC4 JSR
ORA fs_eof_flags ; Merge new bits into flags
867C BNE store_fs_flag ; Store updated flags (always taken)
fall through ↓

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

Inverts A (EOR #&FF), then ANDs the result into fs_eof_flags to clear the specified bits.

On EntryAbitmask of bits to clear
On ExitAupdated fs_eof_flags value
867E .clear_fs_flag←3← 856F JSR← 8886 JSR← 8AC1 JSR
EOR #&ff ; Invert mask: set bits become clear bits
8680 AND fs_eof_flags ; Clear specified bits in flags
8683 .store_fs_flag←1← 867C BNE
STA fs_eof_flags ; Write back updated flags
8686 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.

8687 .setup_tx_ptr_c0←2← 83EB JSR← 8855 JSR
LDX #&c0 ; X=&C0: TX control block at &00C0
8689 STX net_tx_ptr ; Set TX pointer lo
868B LDX #0 ; X=0: page zero
868D STX net_tx_ptr_hi ; Set TX pointer hi
fall through ↓

Transmit and poll for result (full retry)

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

868F .tx_poll_ff←4← 901A JSR← 9073 JMP← 90D0 JSR← 9274 JSR
LDA #&ff ; A=&FF: full retry count
8691 .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 (&8687) always uses the standard TXCB; tx_poll_core (&8693) is general-purpose.

On EntryAretry count (&FF = full retry)
Ytimeout parameter (&60 = standard)
On ExitAentry A (retry count, restored from stack)
X0
Y0
8693 .tx_poll_core
PHA ; Save retry count on stack
8694 TYA ; Transfer timeout to A
8695 PHA ; Save timeout on stack
8696 LDX #0 ; X=0 for (net_tx_ptr,X) indirect
8698 LDA (net_tx_ptr,x) ; Load TXCB byte 0 (control/status)
869A .tx_retry←1← 86CD BEQ
STA (net_tx_ptr,x) ; Write control byte to start TX
869C PHA ; Save control byte for retry
869D .tx_semaphore_spin←1← 86A0 BCC
ASL tx_clear_flag ; Test TX semaphore (C=1 when free)
86A0 BCC tx_semaphore_spin ; Spin until semaphore released
86A2 LDA net_tx_ptr ; Copy TX ptr lo to NMI block
86A4 STA nmi_tx_block ; Store for NMI handler access
86A6 LDA net_tx_ptr_hi ; Copy TX ptr hi to NMI block
86A8 STA nmi_tx_block_hi ; Store for NMI handler access
86AA JSR trampoline_tx_setup ; Initiate ADLC TX via trampoline
86AD .l4←1← 86AF BMI
.tx_poll_status←1← 86AF BMI
LDA (net_tx_ptr,x) ; Poll TXCB byte 0 for completion
86AF BMI l4 ; Bit 7 set: still busy, keep polling
86B1 ASL ; Shift bit 6 into bit 7 (error flag)
86B2 BPL tx_success ; Bit 6 clear: success, clean return
86B4 ASL ; Shift bit 5 into carry
86B5 BEQ tx_not_listening ; Zero: fatal error, no escape
86B7 JSR check_escape_handler ; Check for user escape condition
86BA PLA ; Discard saved control byte
86BB TAX ; Save to X for retry delay
86BC PLA ; Restore timeout parameter
86BD TAY ; Back to Y
86BE PLA ; Restore retry count
86BF BEQ tx_not_listening ; No retries left: report error
86C1 SBC #1 ; Decrement retry count
86C3 PHA ; Save updated retry count
86C4 TYA ; Timeout to A for delay
86C5 PHA ; Save timeout parameter
86C6 TXA ; Control byte for delay duration
86C7 .msdely←2← 86C8 BNE← 86CB BNE
DEX ; Inner delay loop
86C8 BNE msdely ; Spin until X=0
86CA DEY ; Outer delay loop
86CB BNE msdely ; Continue delay
86CD BEQ tx_retry ; ALWAYS branch
86CF .tx_not_listening←2← 86B5 BEQ← 86BF BEQ
TAX ; Save error code in X
86D0 JMP nlistn ; Report 'Not listening' error
86D3 .tx_success←1← 86B2 BPL
PLA ; Discard saved control byte
86D4 PLA ; Discard timeout parameter
86D5 PLA ; Discard retry count
86D6 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.

86D7 .copy_filename_ptr←1← 8708 JSR
LDY #1 ; Y=1: copy 2 bytes (high then low)
86D9 .file1←1← 86DF BPL
LDA (fs_options),y ; Load filename ptr from control block
86DB STA os_text_ptr,y ; Store to MOS text pointer (&F2/&F3)
86DE DEY ; Next byte (descending)
86DF 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
86E1 .parse_filename_gs←2← 89AD JSR← 8DCF 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.

86E3 .parse_filename_gs_y←1← 8C32 JSR
LDX #&ff ; X=&FF: next INX wraps to first char index
86E5 CLC ; C=0 for GSINIT: parse from current position
86E6 JSR gsinit ; Initialise GS string parser
86E9 BEQ terminate_filename ; Empty string: skip to CR terminator
86EB .quote1←1← 86F4 BCC
JSR gsread ; Read next character via GSREAD
86EE BCS terminate_filename ; C=1 from GSREAD: end of string reached
86F0 INX ; Advance buffer index
86F1 STA fs_filename_buf,x ; Store parsed character to &0E30+X
86F4 BCC quote1 ; ALWAYS loop (GSREAD clears C on success) ALWAYS branch
86F6 .terminate_filename←2← 86E9 BEQ← 86EE BCS
INX ; Terminate parsed string with CR
86F7 LDA #&0d ; CR = &0D
86F9 STA fs_filename_buf,x ; Store CR terminator at end of string
86FC LDA #&30 ; Point fs_crc_lo/hi at &0E30 parse buffer
86FE STA fs_crc_lo ; fs_crc_lo = &30
8700 LDA #&0e ; fs_crc_hi = &0E → buffer at &0E30
8702 STA fs_crc_hi ; Store high byte
8704 RTS ; Return; X = string length

FILEV handler (OSFILE entry point)

Calls save_fscv_args (&85D2) to preserve A/X/Y, then JSR &86D7 to copy the 2-byte filename pointer from the parameter block to os_text_ptr and fall through to parse_filename_gs (&86E1) 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 &871B) A=&00: save file (filev_save at &878F) A=&01-&06: attribute operations (filev_attrib_dispatch at &888D) 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
8705 .filev_handler
JSR save_fscv_args ; Save A/X/Y in FS workspace
8708 JSR copy_filename_ptr ; Copy filename ptr from param block to os_text_ptr
870B LDA fs_last_byte_flag ; Recover function code from saved A
870D BPL saveop ; A >= 0: save (&00) or attribs (&01-&06)
870F CMP #&ff ; A=&FF? Only &FF is valid for load
8711 BEQ loadop ; A=&FF: branch to load path
8713 JMP restore_args_return ; Unknown negative code: no-op return
8716 .loadop←1← 8711 BEQ
JSR infol2 ; Copy parsed filename to cmd buffer
8719 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
871B .send_fs_examine←1← 8E00 JSR
LDA #&92 ; Port &92 = PLDATA (data transfer port)
871D STA fs_cmd_urd ; Overwrite URD field with data port number
8720 JSR prepare_cmd_clv ; Build FS header (V=1: CLV path)
8723 LDY #6 ; Y=6: param block byte 6
8725 LDA (fs_options),y ; Byte 6: use file's own load address?
8727 BNE lodfil ; Non-zero: use FS reply address (lodfil)
8729 JSR copy_load_addr_from_params ; Zero: copy caller's load addr first
872C JSR copy_reply_to_params ; Then copy FS reply to param block
872F BCC skip_lodfil ; Carry clear from prepare_cmd_clv: skip lodfil
8731 .lodfil←1← 8727 BNE
JSR copy_reply_to_params ; Copy FS reply addresses to param block
8734 JSR copy_load_addr_from_params ; Then copy load addr from param block
8737 .skip_lodfil←1← 872F BCC
LDY #4 ; Compute end address = load + file length
8739 .copy_load_end_addr←1← 8744 BNE
LDA fs_load_addr,x ; Load address byte
873B STA txcb_end,x ; Store as current transfer position
873D ADC fs_file_len,x ; Add file length byte
8740 STA fs_work_4,x ; Store as end position
8742 INX ; Next address byte
8743 DEY ; Decrement byte counter
8744 BNE copy_load_end_addr ; Loop for all 4 address bytes
8746 SEC ; Adjust high byte for 3-byte length overflow
8747 SBC fs_file_len_3 ; Subtract 4th length byte from end addr
874A STA fs_work_7 ; Store adjusted end address high byte
874C JSR print_file_info ; Display file info after FS reply
874F JSR send_data_blocks ; Transfer file data in &80-byte blocks
8752 LDX #2 ; Copy 3-byte file length to FS reply cmd buffer
8754 .floop←1← 875B BPL
LDA fs_file_len_3,x ; Load file length byte
8757 STA fs_cmd_data,x ; Store in FS command data buffer
875A DEX ; Next byte (count down)
875B BPL floop ; Loop for 3 bytes (X=2,1,0)
875D 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.

875F .send_data_blocks←2← 874F JSR← 8AB4 JSR
JSR compare_addresses ; Compare two 4-byte addresses
8762 BEQ return_lodchk ; Addresses match: transfer complete
8764 LDA #&92 ; Port &92 for data block transfer
8766 STA txcb_port ; Store port to TXCB command byte
8768 .send_block_loop←1← 8784 BNE
LDX #3 ; Set up next &80-byte block for transfer
876A .copy_block_addrs←1← 8773 BPL
LDA txcb_end,x ; Swap: current addr -> source, end -> current
876C STA txcb_start,x ; Source addr = current position
876E LDA fs_work_4,x ; Load end address byte
8770 STA txcb_end,x ; Dest = end address (will be clamped)
8772 DEX ; Next address byte
8773 BPL copy_block_addrs ; Loop for all 4 bytes
8775 LDA #&7f ; Command &7F = data block transfer
8777 STA txcb_ctrl ; Store to TXCB control byte
8779 JSR send_to_fs_star ; Send this block to the fileserver
877C LDY #3 ; Y=3: compare 4 bytes (3..0)
877E .lodchk←1← 8787 BPL
LDA txcb_end,y ; Compare current vs end address (4 bytes)
8781 EOR fs_work_4,y ; XOR with end address byte
8784 BNE send_block_loop ; Not equal: more blocks to send
8786 DEY ; Next byte
8787 BPL lodchk ; Loop for all 4 address bytes
8789 .return_lodchk←1← 8762 BEQ
RTS ; All equal: transfer complete
878A .saveop←1← 870D BPL
BEQ filev_save ; A=0: SAVE handler
878C 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.

878F .filev_save←1← 878A BEQ
LDX #4 ; Process 4 address bytes (load/exec/start/end)
8791 LDY #&0e ; Y=&0E: start from end-address in param block
8793 .savsiz←1← 87AD BNE
LDA (fs_options),y ; Read end-address byte from param block
8795 STA port_ws_offset,y ; Save to port workspace for transfer setup
8798 JSR sub_4_from_y ; Y = Y-4: point to start-address byte
879B SBC (fs_options),y ; end - start = transfer length byte
879D STA fs_cmd_csd,y ; Store length byte in FS command buffer
87A0 PHA ; Save length byte for param block restore
87A1 LDA (fs_options),y ; Read corresponding start-address byte
87A3 STA port_ws_offset,y ; Save to port workspace
87A6 PLA ; Restore length byte from stack
87A7 STA (fs_options),y ; Replace param block entry with length
87A9 JSR add_5_to_y ; Y = Y+5: advance to next address group
87AC DEX ; Decrement address byte counter
87AD BNE savsiz ; Loop for all 4 address bytes
87AF LDY #9 ; Copy load/exec addresses to FS command buffer
87B1 .copy_save_params←1← 87B7 BNE
LDA (fs_options),y ; Read load/exec address byte from params
87B3 STA fs_cmd_csd,y ; Copy to FS command buffer
87B6 DEY ; Next byte (descending)
87B7 BNE copy_save_params ; Loop for bytes 9..1
87B9 LDA #&91 ; Port &91 for save command
87BB STA fs_cmd_urd ; Overwrite URD field with port number
87BE STA fs_error_ptr ; Save port &91 for flow control ACK
87C0 LDX #&0b ; Append filename at offset &0B in cmd buffer
87C2 JSR copy_string_to_cmd ; Append filename to cmd buffer at offset X
87C5 LDY #1 ; Y=1: function code for save
87C7 JSR prepare_cmd_clv ; Build header and send FS save command
87CA JSR print_file_info ; Display save info (addr/len)
87CD LDA fs_cmd_data ; Load reply byte for transfer
87D0 JSR transfer_file_blocks ; Print file length in hex
87D3 .send_fs_reply←1← 875D BMI
JSR send_fs_reply_cmd ; Send FS reply acknowledgement
87D6 .skip_catalogue_msg
STX fs_reply_cmd ; Store reply command for attr decode
87D9 LDY #&0e ; Y=&0E: access byte offset in param block
87DB LDA fs_cmd_data ; Load access byte from FS reply
87DE JSR decode_attribs_5bit ; Convert FS access to BBC attribute format
87E1 BEQ direct_attr_copy ; Z=1: first byte, use A directly
87E3 .copy_attr_loop←1← 87EB BNE
LDA fs_reply_data,y ; Load attribute byte from FS reply
87E6 .direct_attr_copy←1← 87E1 BEQ
STA (fs_options),y ; Store decoded access in param block
87E8 INY ; Next attribute byte
87E9 CPY #&12 ; Copied all 4 bytes? (Y=&0E..&11)
87EB BNE copy_attr_loop ; Loop for 4 attribute bytes
87ED 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).

87F0 .copy_load_addr_from_params←2← 8729 JSR← 8734 JSR
LDY #5 ; Start at offset 5 (top of 4-byte addr)
87F2 .lodrl1←1← 87FA BCS
LDA (fs_options),y ; Read from parameter block
87F4 STA work_ae,y ; Store to local workspace
87F7 DEY ; Next byte (descending)
87F8 CPY #2 ; Copy offsets 5,4,3,2 (4 bytes)
87FA BCS lodrl1 ; Loop while Y >= 2
87FC .add_5_to_y←1← 87A9 JSR
INY ; Y += 5
87FD .add_4_to_y←1← 8A91 JSR
INY ; Y += 4
87FE INY ; (continued)
87FF INY ; (continued)
8800 INY ; (continued)
8801 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)
8802 .copy_reply_to_params←2← 872C JSR← 8731 JSR
LDY #&0d ; Start at offset &0D (top of range)
8804 TXA ; First store uses X (attrib byte)
8805 .lodrl2←1← 880D BCS
STA (fs_options),y ; Write to parameter block
8807 LDA fs_cmd_urd,y ; Read next byte from reply buffer
880A DEY ; Next byte (descending)
880B CPY #2 ; Copy offsets &0D down to 2
880D BCS lodrl2 ; Loop until offset 2 reached
880F .sub_4_from_y←1← 8798 JSR
DEY ; Y -= 4
8810 .sub_3_from_y←2← 88A5 JSR← 8A99 JSR
DEY ; Y -= 3
8811 DEY ; (continued)
8812 DEY ; (continued)
8813 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).

8814 .transfer_file_blocks←2← 87D0 JSR← 8AAF JSR
PHA ; Save FS command byte on stack
8815 JSR compare_addresses ; Compare two 4-byte addresses
8818 BEQ restore_ay_return ; Addresses equal: nothing to transfer
881A .next_block←1← 8867 BNE
LDX #0 ; X=0: clear hi bytes of block size
881C LDY #4 ; Y=4: process 4 address bytes
881E STX fs_reply_cmd ; Clear block size hi byte 1
8821 STX fs_load_vector ; Clear block size hi byte 2
8824 CLC ; CLC for ADC in loop
8825 .block_addr_loop←1← 8832 BNE
LDA fs_load_addr,x ; Source = current position
8827 STA txcb_start,x ; Store source address byte
8829 ADC fs_func_code,x ; Add block size to current position
882C STA txcb_end,x ; Store dest address byte
882E STA fs_load_addr,x ; Advance current position
8830 INX ; Next address byte
8831 DEY ; Decrement byte counter
8832 BNE block_addr_loop ; Loop for all 4 bytes
8834 BCS clamp_dest_setup ; Carry: address overflowed, clamp
8836 SEC ; SEC for SBC in overshoot check
8837 .savchk←1← 883F BNE
LDA fs_load_addr,y ; Check if new pos overshot end addr
883A SBC fs_work_4,y ; Subtract end address byte
883D INY ; Next byte
883E DEX ; Decrement counter
883F BNE savchk ; Loop for 4-byte comparison
8841 BCC send_block ; C=0: no overshoot, proceed
8843 .clamp_dest_setup←1← 8834 BCS
LDX #3 ; Overshot: clamp dest to end address
8845 .clamp_dest_addr←1← 884A BPL
LDA fs_work_4,x ; Load end address byte
8847 STA txcb_end,x ; Replace dest with end address
8849 DEX ; Next byte
884A BPL clamp_dest_addr ; Loop for all 4 bytes
884C .send_block←1← 8841 BCC
PLA ; Recover original FS command byte
884D PHA ; Re-push for next iteration
884E PHP ; Save processor flags (C from cmp)
884F STA txcb_port ; Store command byte in TXCB
8851 LDA #&80 ; 128-byte block size for data transfer
8853 STA txcb_ctrl ; Store size in TXCB control byte
8855 JSR setup_tx_ptr_c0 ; Point TX ptr to &00C0; transmit
8858 LDA fs_error_ptr ; ACK port for flow control
885A JSR init_tx_ctrl_port ; Set reply port for ACK receive
885D PLP ; Restore flags (C=overshoot status)
885E BCS restore_ay_return ; C=1: all data sent (overshot), done
8860 LDA #&91 ; Command &91 = data block transfer
8862 STA txcb_port ; Store command &91 in TXCB
8864 JSR send_to_fs_star ; Transmit block and wait (BRIANX)
8867 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
8869 .fscv_1_eof
PHA ; Save A (function code)
886A TXA ; X = file handle to check
886B JSR handle_to_mask_a ; Convert handle to bitmask in A
886E TYA ; Y = handle bitmask from conversion
886F AND fs_eof_flags ; Local hint: is EOF possible for this handle?
8872 TAX ; X = result of AND (0 = not at EOF)
8873 BEQ restore_ay_return ; Hint clear: definitely not at EOF
8875 PHA ; Save bitmask for clear_fs_flag
8876 STY fs_cmd_data ; Handle byte in FS command buffer
8879 LDY #&11 ; Y=&11: FS function code FCEOF Y=function code for HDRFN
887B LDX #1 ; X=preserved through header build
887D JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8880 PLA ; Restore bitmask
8881 LDX fs_cmd_data ; FS reply: non-zero = at EOF
8884 BNE restore_ay_return ; At EOF: skip flag clear
8886 JSR clear_fs_flag ; Not at EOF: clear the hint bit
8889 .restore_ay_return←4← 8818 BEQ← 885E BCS← 8873 BEQ← 8884 BNE
PLA ; Restore A
888A LDY fs_block_offset ; Restore Y
888C 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
888D .filev_attrib_dispatch←1← 878C JMP
STA fs_cmd_data ; Store function code in FS cmd buffer
8890 CMP #6 ; A=6? (delete)
8892 BEQ cha6 ; Yes: jump to delete handler
8894 BCS check_attrib_result ; A>=7: unsupported, fall through to return
8896 CMP #5 ; A=5? (read catalogue info)
8898 BEQ cha5 ; Yes: jump to read info handler
889A CMP #4 ; A=4? (write attributes only)
889C BEQ cha4 ; Yes: jump to write attrs handler
889E CMP #1 ; A=1? (write all catalogue info)
88A0 BEQ get_file_protection ; Yes: jump to write-all handler
88A2 ASL ; A=2 or 3: convert to param block offset
88A3 ASL ; A*4: 2->8, 3->12
88A4 TAY ; Y = A*4
88A5 JSR sub_3_from_y ; Y = A*4 - 3 (load addr offset for A=2)
88A8 LDX #3 ; X=3: copy 4 bytes
88AA .chalp1←1← 88B1 BPL
LDA (fs_options),y ; Load address byte from param block
88AC STA fs_func_code,x ; Store to FS cmd data area
88AF DEY ; Next source byte (descending)
88B0 DEX ; Next dest byte
88B1 BPL chalp1 ; Loop for 4 bytes
88B3 LDX #5 ; X=5: data extent for filename copy
88B5 BNE copy_filename_to_cmd ; ALWAYS branch
88B7 .get_file_protection←1← 88A0 BEQ
JSR decode_attribs_6bit ; A=1: encode protection from param block
88BA STA fs_file_attrs ; Store encoded attrs at &0F0E
88BD LDY #9 ; Y=9: source offset in param block
88BF LDX #8 ; X=8: dest offset in cmd buffer
88C1 .chalp2←1← 88C8 BNE
LDA (fs_options),y ; Load byte from param block
88C3 STA fs_cmd_data,x ; Store to FS cmd buffer
88C6 DEY ; Next source byte (descending)
88C7 DEX ; Next dest byte
88C8 BNE chalp2 ; Loop until X=0 (8 bytes copied)
88CA LDX #&0a ; X=&0A: data extent past attrs+addrs
88CC .copy_filename_to_cmd←2← 88B5 BNE← 88EA BNE
JSR copy_string_to_cmd ; Append filename to cmd buffer
88CF LDY #&13 ; Y=&13: fn code for FCSAVE (write attrs)
88D1 BNE send_fs_cmd_v1 ; ALWAYS branch to send command ALWAYS branch
88D3 .cha6←1← 8892 BEQ
JSR infol2 ; A=6: copy filename (delete)
88D6 LDY #&14 ; Y=&14: fn code for FCDEL (delete)
88D8 .send_fs_cmd_v1←1← 88D1 BNE
BIT tx_ctrl_upper ; Set V=1 (BIT trick: &B3 has bit 6 set)
88DB JSR init_tx_ctrl_data ; Send via prepare_fs_cmd_v (V=1 path)
88DE .check_attrib_result←1← 8894 BCS
BCS attrib_error_exit ; C=1: &D6 not-found, skip to return
88E0 BCC argsv_check_return ; C=0: success, copy reply to param block ALWAYS branch
88E2 .cha4←1← 889C BEQ
JSR decode_attribs_6bit ; A=4: encode attrs from param block
88E5 STA fs_func_code ; Store encoded attrs at &0F06
88E8 LDX #2 ; X=2: data extent (1 attr byte + fn)
88EA BNE copy_filename_to_cmd ; ALWAYS branch to append filename ALWAYS branch
88EC .cha5←1← 8898 BEQ
LDX #1 ; X=1: filename only, no data extent
88EE JSR copy_string_to_cmd ; Copy filename to cmd buffer
88F1 LDY #&12 ; Y=&12: fn code for FCEXAM (read info) Y=function code for HDRFN
88F3 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
88F6 LDA fs_obj_type ; Save object type from FS reply
88F9 STX fs_obj_type ; Clear reply byte (X=0 on success) X=0 on success, &D6 on not-found
88FC STX fs_len_clear ; Clear length high byte in reply
88FF JSR decode_attribs_5bit ; Decode 5-bit access byte from FS reply
8902 LDY #&0e ; Y=&0E: attrs offset in param block
8904 STA (fs_options),y ; Store decoded attrs at param block +&0E
8906 DEY ; Y=&0D: start copy below attrs Y=&0d
8907 LDX #&0c ; X=&0C: copy from reply offset &0C down
8909 .copy_fs_reply_to_cb←1← 8910 BNE
LDA fs_cmd_data,x ; Load reply byte (load/exec/length)
890C STA (fs_options),y ; Store to param block
890E DEY ; Next dest byte (descending)
890F DEX ; Next source byte
8910 BNE copy_fs_reply_to_cb ; Loop until X=0 (12 bytes copied)
8912 INX ; X=0 -> X=2 for length high copy
8913 INX ; INX again: X=2
8914 LDY #&11 ; Y=&11: length high dest in param block
8916 .cha5lp←1← 891D BPL
LDA fs_access_level,x ; Load length high byte from reply
8919 STA (fs_options),y ; Store to param block
891B DEY ; Next dest byte (descending)
891C DEX ; Next source byte
891D BPL cha5lp ; Loop for 3 length-high bytes
891F LDA fs_cmd_data ; Return object type in A
8922 .attrib_error_exit←1← 88DE 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
8924 .argsv_handler
JSR save_fscv_args ; Save A/X/Y registers for later restore
8927 CMP #3 ; Function >= 3?
8929 BCS restore_args_return ; A>=3 (ensure/flush): no-op for NFS
892B CPY #0 ; Test file handle
892D BEQ argsv_fs_query ; Y=0: FS-level query, not per-file
892F JSR handle_to_mask_clc ; Convert handle to bitmask
8932 STY fs_cmd_data ; Store bitmask as first cmd data byte
8935 LSR ; LSR splits A: C=1 means write (A=1)
8936 STA fs_func_code ; Store function code to cmd data byte 2
8939 BCS save_args_handle ; C=1: write path, copy ptr from caller
893B LDY #&0c ; Y=&0C: FCRDSE (read sequential pointer) Y=function code for HDRFN
893D LDX #2 ; X=2: 3 data bytes in command X=preserved through header build
893F JSR prepare_fs_cmd ; Build and send FS command Prepare FS command buffer (12 references)
8942 STA fs_last_byte_flag ; Clear last-byte flag on success A=0 on success (from build_send_fs_cmd)
8944 LDX fs_options ; X = saved control block ptr low
8946 LDY #2 ; Y=2: copy 3 bytes of file pointer
8948 STA zp_work_3,x ; Zero high byte of 3-byte pointer
894A .copy_fileptr_reply←1← 8951 BPL
LDA fs_cmd_data,y ; Read reply byte from FS cmd data
894D STA zp_work_2,x ; Store to caller's control block
894F DEX ; Next byte (descending)
8950 DEY ; Next source byte
8951 BPL copy_fileptr_reply ; Loop for all 3 bytes
8953 .argsv_check_return←1← 88E0 BCC
BCC restore_args_return ; C=0 (read): return to caller
8955 .save_args_handle←1← 8939 BCS
TYA ; Save bitmask for set_fs_flag later
8956 PHA ; Push bitmask
8957 LDY #3 ; Y=3: copy 4 bytes of file pointer
8959 .copy_fileptr_to_cmd←1← 8960 BPL
LDA zp_work_3,x ; Read caller's pointer byte
895B STA fs_data_count,y ; Store to FS command data area
895E DEX ; Next source byte
895F DEY ; Next destination byte
8960 BPL copy_fileptr_to_cmd ; Loop for all 4 bytes
8962 LDY #&0d ; Y=&0D: FCWRSE (write sequential pointer) Y=function code for HDRFN
8964 LDX #5 ; X=5: 6 data bytes in command X=preserved through header build
8966 JSR prepare_fs_cmd ; Build and send FS command Prepare FS command buffer (12 references)
8969 STX fs_last_byte_flag ; Save not-found status from X X=0 on success, &D6 on not-found
896B PLA ; Recover bitmask for EOF hint update
896C 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.

896F .restore_args_return←7← 8713 JMP← 87ED JMP← 8929 BCS← 8953 BCC← 89E6 BCC← 8A39 JMP← 8E27 JMP
LDA fs_last_byte_flag ; A = saved function code / command
8971 .restore_xy_return←5← 8922 BPL← 8982 BNE← 8992 BPL← 89BD BCS← 89CA BNE
LDX fs_options ; X = saved control block ptr low
8973 LDY fs_block_offset ; Y = saved control block ptr high
8975 RTS ; Return to MOS with registers restored
8976 .argsv_fs_query←1← 892D BEQ
CMP #2 ; Y=0: FS-level queries (no file handle)
8978 BEQ halve_args_a ; A=2: FS-level ensure (write extent)
897A BCS return_a_zero ; A>=3: FS command (ARGSV write)
897C TAY ; Y = A = byte count for copy loop
897D BNE osarg1 ; A!=0: copy command context block
897F LDA #&0a ; FS number 5 (loaded as &0A, LSR'd)
8981 .halve_args_a←1← 8978 BEQ
LSR ; Shared: halve A (A=0 or A=2 paths)
8982 BNE restore_xy_return ; Return with A = FS number or 1
8984 .osarg1←2← 897D BNE← 898A BPL
LDA fs_cmd_context,y ; Copy command context to caller's block
8987 STA (fs_options),y ; Store to caller's parameter block
8989 DEY ; Next byte (descending)
898A BPL osarg1 ; Loop until all bytes copied
898C STY zp_work_2,x ; Y=&FF after loop; fill high bytes
898E STY zp_work_3,x ; Set 32-bit result bytes 2-3 to &FF
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.

8990 .return_a_zero←4← 897A BCS← 89A0 BNE← 8AD5 JMP← 8B77 JMP
LDA #0 ; A=operation (0=close, &40=read, &80=write, &C0=R/W)
8992 BPL restore_xy_return ; ALWAYS branch

FINDV handler (OSFIND entry point)

A=0: close file -- delegates to close_handle (&89CC) 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
8994 .findv_handler
JSR save_fscv_args_with_ptrs ; Save A/X/Y and set up pointers
8997 SEC ; SEC distinguishes open (A>0) from close
8998 JSR handle_to_mask ; Convert file handle to bitmask (Y2FS)
899B TAX ; A=preserved
899C BEQ close_handle ; A=0: close file(s)
899E AND #&3f ; Valid open modes: &40, &80, &C0 only
89A0 BNE return_a_zero ; Invalid mode bits: return
89A2 TXA ; A = original mode byte
89A3 EOR #&80 ; Convert MOS mode to FS protocol flags
89A5 ASL ; ASL: shift mode bits left
89A6 STA fs_cmd_data ; Flag 1: read/write direction
89A9 ROL ; ROL: Flag 2 into bit 0
89AA STA fs_func_code ; Flag 2: create vs existing file
89AD JSR parse_filename_gs ; Parse filename from command line
89B0 LDX #2 ; X=2: copy after 2-byte flags
89B2 JSR copy_string_to_cmd ; Copy filename to FS command buffer
89B5 LDY #6 ; Y=6: FS function code FCOPEN
89B7 BIT tx_ctrl_upper ; Set V flag from l83b3 bit 6
89BA JSR init_tx_ctrl_data ; Build and send FS open command
89BD BCS restore_xy_return ; Error: restore and return
89BF LDA fs_cmd_data ; Load reply handle from FS
89C2 TAX ; X = new file handle
89C3 JSR set_fs_flag ; Set EOF hint + sequence bits
; OR handle bit into fs_sequence_nos (&0E08) to prevent
; a newly opened file inheriting a stale sequence number
; from a previous file using the same handle.
89C6 TXA ; A=handle bitmask for new file A=single-bit bitmask
89C7 JSR mask_to_handle ; Convert bitmask to handle number (FS2A)
89CA 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)
89CC .close_handle←1← 899C BEQ
TYA ; A = handle (Y preserved in A) Y=preserved
89CD BNE close_single_handle ; Y>0: close single file
89CF LDA #osbyte_close_spool_exec ; Close SPOOL/EXEC before FS close-all
89D1 JSR osbyte ; Close any *SPOOL and *EXEC files
89D4 LDY #0 ; Y=0: close all handles on server
89D6 .close_single_handle←1← 89CD BNE
STY fs_cmd_data ; Handle byte in FS command buffer
89D9 LDX #1 ; X=preserved through header build
89DB LDY #7 ; Y=function code for HDRFN
89DD JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
89E0 LDA fs_cmd_data ; Reply handle for flag update
89E3 JSR set_fs_flag ; Update EOF/sequence tracking bits
89E6 .close_opt_return←1← 8A0C BCC
BCC restore_args_return ; C=0: restore A/X/Y and return
89E8 .fscv_0_opt_entry
BEQ set_messages_flag ; Entry from fscv_0_opt (close-all path)
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
89EA .fscv_0_opt
CPX #4 ; Is it *OPT 4,Y?
89EC BNE check_opt1 ; No: check for *OPT 1
89EE CPY #4 ; Y must be 0-3 for boot option
89F0 BCC optl1 ; Y < 4: valid boot option
89F2 .check_opt1←1← 89EC BNE
DEX ; Not *OPT 4: check for *OPT 1
89F3 BNE opter1 ; Not *OPT 1 either: bad option
89F5 .set_messages_flag←1← 89E8 BEQ
STY fs_messages_flag ; Set local messages flag (*OPT 1,Y)
89F8 BCC opt_return ; Return via restore_args_return
89FA .opter1←1← 89F3 BNE
LDA #7 ; Error index 7 (Bad option)
89FC JMP nlisne ; Generate BRK error
89FF .optl1←1← 89F0 BCC
STY fs_cmd_data ; Boot option value in FS command
8A02 LDY #&16 ; Y=&16: FS function code FCOPT Y=function code for HDRFN
8A04 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8A07 LDY fs_block_offset ; Restore Y from saved value
8A09 STY fs_boot_option ; Cache boot option locally
8A0C .opt_return←1← 89F8 BCC
BCC close_opt_return ; Return via restore_args_return
8A0E .adjust_addrs_9←1← 8AC9 JSR
LDY #9 ; Y=9: adjust 9 address bytes
8A10 JSR adjust_addrs_clc ; Adjust with carry clear
8A13 .adjust_addrs_1←1← 8BBE JSR
LDY #1 ; Y=1: adjust 1 address byte
8A15 .adjust_addrs_clc←1← 8A10 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
8A16 .adjust_addrs←2← 8ACF JSR← 8BCA JSR
LDX #&fc ; X=&FC: index into &0E06 area (wraps to 0)
8A18 .adjust_addr_byte←1← 8A2B BNE
LDA (fs_options),y ; Load byte from param block
8A1A BIT fs_load_addr_2 ; Test sign of adjustment direction
8A1C BMI subtract_adjust ; Negative: subtract instead
8A1E ADC fs_cmd_context,x ; Add adjustment value
8A21 JMP gbpbx ; Skip to store result
8A24 .subtract_adjust←1← 8A1C BMI
SBC fs_cmd_context,x ; Subtract adjustment value
8A27 .gbpbx←1← 8A21 JMP
STA (fs_options),y ; Store adjusted byte back
8A29 INY ; Next param block byte
8A2A INX ; Next adjustment byte (X wraps &FC->&00)
8A2B BNE adjust_addr_byte ; Loop 4 times (X=&FC,&FD,&FE,&FF,done)
8A2D 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
8A2E .gbpbv_handler
JSR save_fscv_args ; Save A/X/Y to FS workspace
8A31 TAX ; X = call number for range check
8A32 BEQ gbpb_invalid_exit ; A=0: invalid, restore and return
8A34 DEX ; Convert to 0-based (A=0..7)
8A35 CPX #8 ; Range check: must be 0-7
8A37 BCC gbpbx1 ; In range: continue to handler
8A39 .gbpb_invalid_exit←1← 8A32 BEQ
JMP restore_args_return ; Out of range: restore args and return
8A3C .gbpbx1←1← 8A37 BCC
TXA ; Recover 0-based function code
8A3D LDY #0 ; Y=0: param block byte 0 (file handle)
8A3F PHA ; Save function code on stack
8A40 CMP #4 ; A>=4: info queries, dispatch separately
8A42 BCC gbpbe1 ; A<4: file read/write operations
8A44 JMP osgbpb_info ; Dispatch to OSGBPB 5-8 info handler
8A47 .gbpbe1←1← 8A42 BCC
LDA (fs_options),y ; Get file handle from param block byte 0
8A49 JSR handle_to_mask_a ; Convert handle to bitmask for EOF flags
8A4C STY fs_cmd_data ; Store handle in FS command data
8A4F LDY #&0b ; Y=&0B: start at param block byte 11
8A51 LDX #6 ; X=6: copy 6 bytes of transfer params
8A53 .gbpbf1←1← 8A5F BNE
LDA (fs_options),y ; Load param block byte
8A55 STA fs_func_code,x ; Store to FS command buffer at &0F06+X
8A58 DEY ; Previous param block byte
8A59 CPY #8 ; Skip param block offset 8 (the handle)
8A5B BNE gbpbx0 ; Not at handle offset: continue
8A5D DEY ; Extra DEY to skip handle byte
8A5E .gbpbx0←1← 8A5B BNE
.gbpbf2←1← 8A5B BNE
DEX ; Decrement copy counter
8A5F BNE gbpbf1 ; Loop for all 6 bytes
8A61 PLA ; Recover function code from stack
8A62 LSR ; LSR: odd=read (C=1), even=write (C=0)
8A63 PHA ; Save function code again (need C later)
8A64 BCC gbpbl1 ; Even (write): X stays 0
8A66 INX ; Odd (read): X=1
8A67 .gbpbl1←1← 8A64 BCC
STX fs_func_code ; Store FS direction flag
8A6A LDY #&0b ; Y=&0B: command data extent
8A6C LDX #&91 ; Command &91=put, &92=get
8A6E PLA ; Recover function code
8A6F PHA ; Save again for later direction check
8A70 BEQ gbpb_write_path ; Even (write): keep &91 and Y=&0B
8A72 LDX #&92 ; Odd (read): use &92 (get) instead
8A74 DEY ; Read: one fewer data byte in command Y=&0a
8A75 .gbpb_write_path←1← 8A70 BEQ
STX fs_cmd_urd ; Store port to FS command URD field
8A78 STX fs_error_ptr ; Save port for error recovery
8A7A LDX #8 ; X=8: command data bytes
8A7C LDA fs_cmd_data ; Load handle from FS command data
8A7F JSR prepare_cmd_with_flag ; Build FS command with handle+flag
8A82 LDA fs_load_addr_3 ; Save seq# for byte-stream flow control
8A84 STA fs_sequence_nos ; Store to FS sequence number workspace
8A87 LDX #4 ; X=4: copy 4 address bytes
8A89 .gbpbl3←1← 8A9D BNE
LDA (fs_options),y ; Set up source/dest from param block
8A8B STA addr_work,y ; Store as source address
8A8E STA txcb_pos,y ; Store as current transfer position
8A91 JSR add_4_to_y ; Skip 4 bytes to reach transfer length
8A94 ADC (fs_options),y ; Dest = source + length
8A96 STA addr_work,y ; Store as end address
8A99 JSR sub_3_from_y ; Back 3 to align for next iteration
8A9C DEX ; Decrement byte counter
8A9D BNE gbpbl3 ; Loop for all 4 address bytes
8A9F INX ; X=1 after loop
8AA0 .gbpbf3←1← 8AA7 BPL
LDA fs_cmd_csd,x ; Copy CSD data to command buffer
8AA3 STA fs_func_code,x ; Store at &0F06+X
8AA6 DEX ; Decrement counter
8AA7 BPL gbpbf3 ; Loop for X=1,0
8AA9 PLA ; Odd (read): send data to FS first
8AAA BNE gbpb_read_path ; Non-zero: skip write path
8AAC LDA fs_cmd_urd ; Load port for transfer setup
8AAF JSR transfer_file_blocks ; Transfer data blocks to fileserver
8AB2 BCS wait_fs_reply ; Carry set: transfer error
8AB4 .gbpb_read_path←1← 8AAA BNE
JSR send_data_blocks ; Read path: receive data blocks from FS
8AB7 .wait_fs_reply←1← 8AB2 BCS
JSR send_fs_reply_cmd ; Wait for FS reply command
8ABA LDA (fs_options,x) ; Load handle mask for EOF flag update
8ABC BIT fs_cmd_data ; Check FS reply: bit 7 = not at EOF
8ABF BMI skip_clear_flag ; Bit 7 set: not EOF, skip clear
8AC1 JSR clear_fs_flag ; At EOF: clear EOF hint for this handle
8AC4 .skip_clear_flag←1← 8ABF BMI
JSR set_fs_flag ; Set EOF hint flag (may be at EOF)
8AC7 STX fs_load_addr_2 ; Direction=0: forward adjustment
8AC9 JSR adjust_addrs_9 ; Adjust param block addrs by +9 bytes
8ACC DEC fs_load_addr_2 ; Direction=&FF: reverse adjustment
8ACE SEC ; SEC for reverse subtraction
8ACF JSR adjust_addrs ; Adjust param block addrs (reverse)
8AD2 ASL fs_cmd_data ; Shift bit 7 into C for return flag
8AD5 JMP return_a_zero ; Return via restore_args path
8AD8 .get_disc_title←1← 8B08 BEQ
LDY #&15 ; Y=&15: function code for disc title Y=function code for HDRFN
8ADA JSR prepare_fs_cmd ; Build and send FS command Prepare FS command buffer (12 references)
8ADD LDA fs_boot_option ; Load boot option from FS workspace
8AE0 STA fs_boot_data ; Store boot option in reply area
8AE3 STX fs_load_addr ; X=0: reply data start offset X=0 on success, &D6 on not-found
8AE5 STX fs_load_addr_hi ; Clear reply buffer high byte
8AE7 LDA #&12 ; A=&12: 18 bytes of reply data
8AE9 STA fs_load_addr_2 ; Store as byte count for copy
8AEB 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.

8AED .osgbpb_info←1← 8A44 JMP
LDY #4 ; Y=4: check param block byte 4
8AEF LDA tube_flag ; Check if destination is in Tube space
8AF2 BEQ store_tube_flag ; No Tube: skip Tube address check
8AF4 CMP (fs_options),y ; Compare Tube flag with addr byte 4
8AF6 BNE store_tube_flag ; Mismatch: not Tube space
8AF8 DEY ; Y=&03
8AF9 SBC (fs_options),y ; Y=3: subtract addr byte 3 from flag
8AFB .store_tube_flag←2← 8AF2 BEQ← 8AF6 BNE
STA svc_state ; Non-zero = Tube transfer required
8AFD .info2←1← 8B03 BNE
LDA (fs_options),y ; Copy param block bytes 1-4 to workspace
8AFF STA fs_last_byte_flag,y ; Store to &BD+Y workspace area
8B02 DEY ; Previous byte
8B03 BNE info2 ; Loop for bytes 3,2,1
8B05 PLA ; Sub-function: AND #3 of (original A - 4)
8B06 AND #3 ; Mask to 0-3 (OSGBPB 5-8 → 0-3)
8B08 BEQ get_disc_title ; A=0 (OSGBPB 5): read disc title
8B0A LSR ; LSR: A=0 (OSGBPB 6) or A=1 (OSGBPB 7)
8B0B BEQ gbpb6_read_name ; A=0 (OSGBPB 6): read CSD/LIB name
8B0D BCS gbpb8_read_dir ; C=1 (OSGBPB 8): read filenames from dir
8B0F .gbpb6_read_name←1← 8B0B BEQ
TAY ; Y=0 for CSD or carry for fn code select Y=function code
8B10 LDA fs_csd_handle,y ; Get CSD/LIB/URD handles for FS command
8B13 STA fs_cmd_csd ; Store CSD handle in command buffer
8B16 LDA fs_lib_handle ; Load LIB handle from workspace
8B19 STA fs_cmd_lib ; Store LIB handle in command buffer
8B1C LDA fs_urd_handle ; Load URD handle from workspace
8B1F STA fs_cmd_urd ; Store URD handle in command buffer
8B22 LDX #&12 ; X=&12: buffer extent for command data X=buffer extent (command-specific data bytes)
8B24 STX fs_cmd_y_param ; Store X as function code in header
8B27 LDA #&0d ; &0D = 13 bytes of reply data expected
8B29 STA fs_func_code ; Store reply length in command buffer
8B2C STA fs_load_addr_2 ; Store as byte count for copy loop
8B2E LSR ; LSR: &0D >> 1 = 6 A=timeout period for FS reply
8B2F STA fs_cmd_data ; Store as command data byte
8B32 CLC ; CLC for standard FS path
8B33 JSR build_send_fs_cmd ; Build and send FS command (DOFSOP)
8B36 STX fs_load_addr_hi ; X=0 on success, &D6 on not-found
8B38 INX ; INX: X=1 after build_send_fs_cmd
8B39 STX fs_load_addr ; Store X as reply start offset
8B3B .copy_reply_to_caller←2← 8AEB BNE← 8BB3 JSR
LDA svc_state ; Copy FS reply to caller's buffer
8B3D BNE tube_transfer ; Non-zero: use Tube transfer path
8B3F LDX fs_load_addr ; X = reply start offset
8B41 LDY fs_load_addr_hi ; Y = reply buffer high byte
8B43 .copy_reply_bytes←1← 8B4C BNE
LDA fs_cmd_data,x ; Load reply data byte
8B46 STA (fs_crc_lo),y ; Store to caller's buffer
8B48 INX ; Next source byte
8B49 INY ; Next destination byte
8B4A DEC fs_load_addr_2 ; Decrement remaining bytes
8B4C BNE copy_reply_bytes ; Loop until all bytes copied
8B4E BEQ gbpb_done ; ALWAYS branch to exit ALWAYS branch
8B50 .tube_transfer←1← 8B3D BNE
JSR tube_claim_loop ; Claim Tube transfer channel
8B53 LDA #1 ; A=1: Tube claim type 1 (write)
8B55 LDX fs_options ; X = param block address low
8B57 LDY fs_block_offset ; Y = param block address high
8B59 INX ; INX: advance past byte 0
8B5A BNE no_page_wrap ; No page wrap: keep Y
8B5C INY ; Page wrap: increment high byte
8B5D .no_page_wrap←1← 8B5A BNE
JSR tube_addr_claim ; Claim Tube address for transfer
8B60 LDX fs_load_addr ; X = reply data start offset
8B62 .tbcop1←1← 8B70 BNE
LDA fs_cmd_data,x ; Load reply data byte
8B65 STA tube_data_register_3 ; Send byte to Tube via R3
8B68 INX ; Next source byte
8B69 LDY #6 ; Delay loop for slow Tube co-processor
8B6B .wait_tube_delay←1← 8B6C BNE
DEY ; Decrement delay counter
8B6C BNE wait_tube_delay ; Loop until delay complete
8B6E DEC fs_load_addr_2 ; Decrement remaining bytes
8B70 BNE tbcop1 ; Loop until all bytes sent to Tube
8B72 LDA #&83 ; Release Tube after transfer complete
8B74 JSR tube_addr_claim ; Release Tube address claim
8B77 .gbpb_done←2← 8B4E BEQ← 8BCD BEQ
JMP return_a_zero ; Return via restore_args path
8B7A .gbpb8_read_dir←1← 8B0D BCS
LDY #9 ; OSGBPB 8: read filenames from dir
8B7C LDA (fs_options),y ; Byte 9: number of entries to read
8B7E STA fs_func_code ; Store as reply count in command buffer
8B81 LDY #5 ; Y=5: byte 5 = starting entry number
8B83 LDA (fs_options),y ; Load starting entry number
8B85 STA fs_data_count ; Store in command buffer
8B88 LDX #&0d ; X=&0D: command data extent X=preserved through header build
8B8A STX fs_reply_cmd ; Store extent in command buffer
8B8D LDY #2 ; Y=2: function code for dir read
8B8F STY fs_load_addr ; Store 2 as reply data start offset
8B91 STY fs_cmd_data ; Store 2 as command data byte
8B94 INY ; Y=3: function code for header read Y=function code for HDRFN Y=&03
8B95 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8B98 STX fs_load_addr_hi ; X=0 after FS command completes X=0 on success, &D6 on not-found
8B9A LDA fs_func_code ; Load reply entry count
8B9D STA (fs_options,x) ; Store at param block byte 0 (X=0)
8B9F LDA fs_cmd_data ; Load entries-read count from reply
8BA2 LDY #9 ; Y=9: param block byte 9
8BA4 ADC (fs_options),y ; Add to starting entry number
8BA6 STA (fs_options),y ; Update param block with new position
8BA8 LDA txcb_end ; Load total reply length
8BAA SBC #7 ; Subtract header (7 bytes) from reply len
8BAC STA fs_func_code ; Store adjusted length in command buffer
8BAF STA fs_load_addr_2 ; Store as byte count for copy loop
8BB1 BEQ skip_copy_reply ; Zero bytes: skip copy
8BB3 JSR copy_reply_to_caller ; Copy reply data to caller's buffer
8BB6 .skip_copy_reply←1← 8BB1 BEQ
LDX #2 ; X=2: clear 3 bytes
8BB8 .zero_cmd_bytes←1← 8BBC BPL
STA fs_data_count,x ; Zero out &0F07+X area
8BBB DEX ; Next byte
8BBC BPL zero_cmd_bytes ; Loop for X=2,1,0
8BBE JSR adjust_addrs_1 ; Adjust pointer by +1 (one filename read)
8BC1 SEC ; SEC for reverse adjustment
8BC2 DEC fs_load_addr_2 ; Reverse adjustment for updated counter
8BC4 LDA fs_cmd_data ; Load entries-read count
8BC7 STA fs_func_code ; Store in command buffer
8BCA JSR adjust_addrs ; Adjust param block addresses
8BCD BEQ gbpb_done ; Z=1: all done, exit
8BCF .tube_claim_loop←3← 8B50 JSR← 8BD4 BCC← 8E10 JSR
LDA #&c3 ; A=&C3: Tube claim with retry
8BD1 JSR tube_addr_claim ; Request Tube address claim
8BD4 BCC tube_claim_loop ; C=0: claim failed, retry
8BD6 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 &8C05 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.

8BD7 .fscv_3_star_cmd←1← 828B JMP
JSR save_fscv_args_with_ptrs ; Save A/X/Y and set up command ptr
8BDA LDX #&ff ; X=&FF: table index (pre-incremented)
8BDC STX fs_crflag ; Disable column formatting
8BDE .scan_cmd_table←1← 8BF9 BNE
LDY #&ff ; Y=&FF: input index (pre-incremented)
8BE0 .decfir←1← 8BEB BEQ
INY ; Advance input pointer
8BE1 INX ; Advance table pointer
8BE2 .decmor←1← 8BFD BCS
LDA fs_cmd_match_table,x ; Load table character
8BE5 BMI dispatch_cmd ; Bit 7: end of name, dispatch
8BE7 EOR (fs_crc_lo),y ; XOR input char with table char
8BE9 AND #&df ; Case-insensitive (clear bit 5)
8BEB BEQ decfir ; Match: continue comparing
8BED DEX ; Mismatch: back up table pointer
8BEE .decmin←1← 8BF2 BPL
INX ; Skip to end of table entry
8BEF LDA fs_cmd_match_table,x ; Load table byte
8BF2 BPL decmin ; Loop until bit 7 set (end marker)
8BF4 LDA (fs_crc_lo),y ; Check input for '.' abbreviation
8BF6 INX ; Skip past handler high byte
8BF7 CMP #&2e ; Is input '.' (abbreviation)?
8BF9 BNE scan_cmd_table ; No: try next table entry
8BFB INY ; Yes: skip '.' in input
8BFC DEX ; Back to handler high byte
8BFD BCS decmor ; ALWAYS branch; dispatch via BMI
8BFF .dispatch_cmd←1← 8BE5 BMI
PHA ; Push handler address high byte
8C00 LDA fs_cmd_dispatch_hi,x ; Load handler address low byte
8C03 PHA ; Push handler address low byte
8C04 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." → &80C1 (forward_star_cmd) — placed first as a fudge to catch *I. abbreviation before matching *I AM "I AM" → &8082 (i_am_handler: parse station.net, logon) "EX" → &8C1B (ex_handler: embedded in table tail) "BYE"\r → &83BC (bye_handler: logoff) <catch-all> → &80C1 (forward anything else to FS)

8C05 .fs_cmd_match_table←2← 8BE2 LDA← 8BEF LDA
EOR #&2e ; Match last char against '.' for *I. abbreviation
8C07 EQUB &80, &C0
8C09 EQUS "I AM"
8C0D EQUB &80, &81, &45, &58, &8C, &1A
8C13 EQUS "BYE"
8C16 EQUB &0D, &83, &BB, &80, &C0
8C1B .ex_handler
LDX #1 ; X=1: *EX single-entry examine
8C1D LDA #3 ; A=3: column count for *EX mode
8C1F 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.

8C21 .fscv_5_cat
LDX #3 ; X=3: column count for multi-column layout
8C23 STX fs_crflag ; CRFLAG=3: first entry will trigger newline
8C25 LDY #0 ; Y=0: initialise column counter
8C27 LDA #&0b ; A=&0B: examine argument count
8C29 .init_cat_params←1← 8C1F BNE
STA fs_work_5 ; Store examine argument count
8C2B STX fs_work_7 ; Store column count
8C2D LDA #6 ; A=6: examine format type in command
8C2F STA fs_cmd_data ; Store format type at &0F05
8C32 JSR parse_filename_gs_y ; Set up command parameter pointers
8C35 LDX #1 ; X=1: copy dir name at cmd offset 1
8C37 JSR copy_string_to_cmd ; Copy directory name to command buffer
8C3A LDY #&12 ; Y=function code for HDRFN
8C3C JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8C3F LDX #3 ; X=3: start printing from reply offset 3
8C41 JSR print_reply_bytes ; Print directory title (10 chars)
8C44 JSR print_inline ; Print '('
8C47 EQUS "("
8C48 LDA fs_reply_stn ; Load station number from FS reply
8C4B JSR print_decimal ; Print station number as decimal
8C4E JSR print_inline ; Print ') '
8C51 EQUS ") "
8C57 LDX fs_access_level ; Load access level from reply
8C5A BNE print_public ; Non-zero: Public access
8C5C JSR print_inline ; Print 'Owner' + CR
8C5F EQUS "Owner."
8C65 BNE cat_check_access ; ALWAYS branch past 'Owner'
8C67 .print_public←1← 8C5A BNE
JSR print_inline ; Print 'Public' + CR
8C6A EQUS "Public."
8C71 .cat_check_access←1← 8C65 BNE
LDY #&15 ; Y=function code for HDRFN
8C73 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8C76 INX ; X=1: past command code byte
8C77 LDY #&10 ; Y=&10: print 16 characters
8C79 JSR print_reply_counted ; Print disc/CSD name from reply
8C7C JSR print_inline ; Print ' Option '
8C7F EQUS " Option "
8C8A LDA fs_boot_option ; Load boot option from reply
8C8D TAX ; X = boot option for name table lookup
8C8E JSR print_hex ; Print boot option as hex digit
8C91 JSR print_inline ; Print ' ('
8C94 EQUS " ("
8C96 LDY boot_option_text,x ; Y=string offset for this option
8C99 .cattxt←1← 8CA2 BNE
LDA boot_option_text,y ; Load next char of option name
8C9C BMI done_option_name ; Bit 7 set: end of option name
8C9E JSR osasci ; Write character
8CA1 INY ; Next character
8CA2 BNE cattxt ; Continue printing option name
8CA4 .done_option_name←1← 8C9C BMI
JSR print_inline ; Print ')' + CR + 'Dir. '
8CA7 EQUS ").Dir. "
8CAE LDX #&11 ; X=&11: Dir. name offset in reply
8CB0 JSR print_reply_bytes ; Print directory name (10 chars)
8CB3 JSR print_inline ; Print ' Lib. ' header
8CB6 EQUS " Lib. "
8CC0 LDX #&1b ; X=&1B: Lib. name offset in reply
8CC2 JSR print_reply_bytes ; Print library name
8CC5 JSR osnewl ; Print two CRs (blank line) Write newline (characters 10 and 13)
8CC8 .fetch_dir_batch←1← 8CF9 BNE
STY fs_func_code ; Store entry start offset for request
8CCB STY fs_work_4 ; Save start offset in zero page for loop
8CCD LDX fs_work_5 ; Load examine arg count for batch size
8CCF STX fs_data_count ; Store as request count at &0F07
8CD2 .cat_examine_loop
LDX fs_work_7 ; Load column count for display format
8CD4 STX fs_cmd_data ; Store column count in command data
8CD7 LDX #3 ; X=3: copy directory name at offset 3
8CD9 JSR copy_string_to_cmd ; Append directory name to examine command
8CDC LDY #3 ; Y=function code for HDRFN
8CDE JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8CE1 INX ; X past command code byte in reply
8CE2 LDA fs_cmd_data ; Load entry count from reply
8CE5 BEQ print_newline ; Zero entries: catalogue complete
8CE7 .process_entries
PHA ; Save entry count for batch processing
8CE8 .scan_entry_terminator←1← 8CEC BPL
INY ; Advance Y past entry data bytes
8CE9 LDA fs_cmd_data,y ; Read entry byte from reply buffer
8CEC BPL scan_entry_terminator ; Loop until high-bit terminator found
8CEE STA fs_cmd_lib,y ; Store terminator as print boundary
8CF1 JSR cat_column_separator ; Print/format this directory entry
8CF4 PLA ; Restore entry count from stack
8CF5 CLC ; CLC for addition
8CF6 ADC fs_work_4 ; Advance start offset by entry count
8CF8 TAY ; Y = new entry start offset
8CF9 BNE fetch_dir_batch ; More entries: fetch next batch
8CFB .print_reply_bytes←3← 8C41 JSR← 8CB0 JSR← 8CC2 JSR
LDY #&0a ; Y=&0A: default print 10 characters
8CFD .print_reply_counted←2← 8C79 JSR← 8D05 BNE
LDA fs_cmd_data,x ; Load reply byte at offset X
8D00 JSR osasci ; Write character
8D03 INX ; Next reply byte
8D04 DEY ; Decrement character count
8D05 BNE print_reply_counted ; Loop for remaining characters
; Option name encoding: 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 boot_option_text (&8D08), whose first four
; bytes are the offset table:
; &6A→&8D72 "Off", &7D→&8D85 "Load",
; &A5→&8DAD "Run", &18→&8D20 "Exec"
; Each string is terminated by the next instruction's opcode
; having bit 7 set (e.g. LDA #imm = &A9, RTS = &60).
8D07 .return_9
RTS ; Return from column separator
8D08 .boot_option_text←2← 8C96 LDY← 8C99 LDA
EQUB &6A, &7D, &A5, &18
8D0C EQUS "L.!"

Boot command strings for auto-boot

The four boot options use OSCLI strings at offsets within page &8D. The offset table at boot_option_offsets+1 (&8D1C) is indexed by the boot option value (0-3); each byte is the low byte of the string address, with the page high byte &8D loaded separately: Option 0 (Off): offset &1B → &8D1B = bare CR (empty command) Option 1 (Load): offset &0C → &8D0C = "L.!BOOT" (the bytes &4C='L', &2E='.', &21='!' precede "BOOT" + CR at &8D0F) Option 2 (Run): offset &0E → &8D0E = "!BOOT" (bare filename = *RUN) Option 3 (Exec): offset &14 → &8D14 = "E.!BOOT"

This is a classic BBC ROM space optimisation: the string data overlaps with other byte sequences to save space. The &0D byte at &8D1B terminates "E.!BOOT" AND doubles as the bare-CR command for boot option 0.

8D0F .boot_cmd_strings
EQUS "BOOT"
8D13 EQUB &0D
8D14 EQUS "E.!BOOT"

Boot option → OSCLI string offset table

Five bytes: the first byte (&0D) is the bare-CR target for boot option 0; bytes 1-4 are the offset table indexed by boot option (0-3). Each offset is the low byte of a pointer into page &8D. The code reads from boot_option_offsets+1 (&8D1C) via LDX l8d1c,Y with Y=boot_option, then LDY #&8D, JMP oscli. See boot_cmd_strings for the target strings.

8D1B .boot_option_offsets
EQUB &0D
8D1C .boot_oscli_offset←1← 8E3D LDX
EQUB &1B
8D1D EQUB &0C
8D1E EQUB &0E
8D1F EQUB &14
8D20 EQUB &45
8D21 SEI ; Data byte: boot_cmd_strings 'x'
8D22 ADC l0063 ; Data bytes: boot_cmd_strings 'ec'
fall through ↓

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

8D24 .print_file_info←2← 874C JSR← 87CA JSR
LDY fs_messages_flag ; Check if messages enabled
8D27 BEQ return_5 ; Zero: no info to display, return
8D29 LDY #0 ; Y=0: start of filename
8D2B LDX fs_cmd_csd ; Load current directory prefix flag
8D2E BEQ next_filename_char ; No prefix: skip directory display
8D30 JSR print_dir_from_offset ; Print directory name prefix
8D33 BMI print_hex_fields ; N=1: skip to hex fields after dir
8D35 .next_filename_char←2← 8D2E BEQ← 8D43 BNE
LDA (fs_crc_lo),y ; Load next filename character
8D37 CMP #&0d ; CR: end of filename
8D39 BEQ pad_filename_spaces ; CR found: pad remaining with spaces
8D3B CMP #&20 ; Space: end of name field
8D3D BEQ pad_filename_spaces ; Space found: pad with spaces
8D3F JSR osasci ; Write character
8D42 INY ; Advance to next character
8D43 BNE next_filename_char ; Continue printing filename
8D45 .pad_filename_spaces←3← 8D39 BEQ← 8D3D BEQ← 8D4B BCC
JSR print_space ; Print space for padding
8D48 INY ; Advance column counter
8D49 CPY #&0c ; Reached 12 columns?
8D4B BCC pad_filename_spaces ; No: continue padding
8D4D .print_hex_fields←1← 8D33 BMI
LDY #5 ; Y=5: load address offset (4 bytes)
8D4F JSR print_hex_bytes ; Print load address
8D52 JSR print_exec_and_len ; Print exec address and file length
8D55 .print_newline←1← 8CE5 BEQ
JMP osnewl ; Write newline (characters 10 and 13)
8D58 .print_exec_and_len←1← 8D52 JSR
LDY #9 ; Y=9: exec address offset (4 bytes)
8D5A JSR print_hex_bytes ; Print exec address
8D5D LDY #&0c ; Y=&0C: file length offset
8D5F LDX #3 ; X=3: print 3 bytes (24-bit length)
8D61 BNE num01 ; ALWAYS branch
8D63 .print_hex_bytes←2← 8D4F JSR← 8D5A JSR
LDX #4 ; X=4: print 4 hex bytes
8D65 .num01←2← 8D61 BNE← 8D6C BNE
LDA (fs_options),y ; Load byte from parameter block
8D67 JSR print_hex ; Print as two hex digits
8D6A DEY ; Next byte (descending)
8D6B DEX ; Count down
8D6C BNE num01 ; Loop until 4 bytes printed
8D6E .print_space←1← 8D45 JSR
LDA #&20 ; A=space character
8D70 BNE print_digit ; ALWAYS branch
8D72 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.

8D75 .infol2←5← 809D JSR← 80C1 JSR← 8716 JSR← 88D3 JSR← 8DD2 JSR
.copy_filename←5← 809D JSR← 80C1 JSR← 8716 JSR← 88D3 JSR← 8DD2 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)
8D77 .copy_string_to_cmd←6← 87C2 JSR← 88CC JSR← 88EE JSR← 89B2 JSR← 8C37 JSR← 8CD9 JSR
LDY #0 ; Start copying from offset 0
8D79 .copy_string_from_offset←1← 8D82 BNE
LDA (fs_crc_lo),y ; Load next byte from source string
8D7B STA fs_cmd_data,x ; Store to FS command buffer (&0F05+X)
8D7E INX ; Advance write position
8D7F INY ; Advance source pointer
8D80 EOR #&0d ; XOR with CR: result=0 if byte was CR
8D82 BNE copy_string_from_offset ; Loop until CR copied
8D84 .return_5←2← 8D27 BEQ← 8D8E BMI
RTS ; Return; X = next free position in buffer
8D85 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.

8D89 .fsreply_0_print_dir
LDX #0 ; X=0: start from first reply byte
8D8B .print_dir_from_offset←2← 8D30 JSR← 8DAB BNE
LDA fs_cmd_data,x ; Load byte from FS reply buffer
8D8E BMI return_5 ; Bit 7 set: end of string, return
8D90 BNE dir_print_char ; 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.

8D92 .cat_column_separator←1← 8CF1 JSR
LDY fs_crflag ; Null byte: check column counter
8D94 BMI dir_column_check ; Negative: print CR (no columns)
8D96 INY ; Advance column counter
8D97 TYA ; Transfer to A for modulo
8D98 AND #3 ; Modulo 4 columns
8D9A STA fs_crflag ; Update column counter
8D9C BEQ dir_column_check ; Column 0: start new line
8D9E JSR print_inline ; Print 2-space column separator
8DA1 EQUS " "
8DA3 BNE next_dir_entry ; More entries: skip final newline
8DA5 .dir_column_check←2← 8D94 BMI← 8D9C BEQ
LDA #&0d ; A=CR: print newline separator
8DA7 .dir_print_char←1← 8D90 BNE
JSR osasci ; Write character 13
8DAA .next_dir_entry←1← 8DA3 BNE
INX ; Next byte in reply buffer
8DAB BNE print_dir_from_offset ; Loop until end of buffer
8DAD 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)
8DB0 .print_decimal←2← 8244 JSR← 8C4B JSR
TAY ; Y = value to print
8DB1 LDA #&64 ; Divisor = 100 (hundreds digit)
8DB3 JSR print_decimal_digit ; Print hundreds digit
8DB6 LDA #&0a ; Divisor = 10 (tens digit)
8DB8 JSR print_decimal_digit ; Print tens digit
8DBB 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
8DBD .print_decimal_digit←2← 8DB3 JSR← 8DB8 JSR
STA fs_error_ptr ; Save divisor to workspace
8DBF TYA ; A = dividend (from Y)
8DC0 LDX #&2f ; X = &2F = ASCII '0' - 1
8DC2 SEC ; Prepare for subtraction
8DC3 .divide_subtract←1← 8DC6 BCS
INX ; Count one subtraction (next digit value)
8DC4 SBC fs_error_ptr ; A = A - divisor
8DC6 BCS divide_subtract ; Loop while A >= 0 (borrow clear)
8DC8 ADC fs_error_ptr ; Undo last subtraction: A = remainder
8DCA TAY ; Y = remainder for caller
8DCB TXA ; A = X = ASCII digit character
8DCC .print_digit←1← 8D70 BNE
JMP osasci ; Write character

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

Parses the filename via parse_filename_gs and calls infol2, then falls through to fsreply_4_notify_exec to set up and send the FS load-as-command request.

8DCF .fscv_2_star_run
JSR parse_filename_gs ; Parse filename from command line
8DD2 JSR infol2 ; Copy filename to FS command buffer
fall through ↓

FS reply 4: send FS load-as-command and execute response

Initialises a GS reader to skip past the filename and calculate the command context address, then 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.

8DD5 .fsreply_4_notify_exec
LDY #0 ; Y=0: start of text for GSINIT
8DD7 CLC ; CLC before GSINIT call
8DD8 JSR gsinit ; GSINIT/GSREAD: skip past the filename
8DDB .skip_gs_filename←1← 8DDE BCC
JSR gsread ; Read next filename character
8DDE BCC skip_gs_filename ; C=0: more characters, keep reading
8DE0 JSR skip_spaces ; Skip spaces after filename
8DE3 CLC ; Calculate context addr = text ptr + Y
8DE4 TYA ; Y = offset past filename end
8DE5 ADC os_text_ptr ; Add text pointer low byte
8DE7 STA fs_cmd_context ; Store context address low byte
8DEA LDA os_text_ptr_hi ; Load text pointer high byte
8DEC ADC #0 ; Add carry from low byte addition
8DEE STA fs_context_hi ; Store context address high byte
8DF1 LDX #&0e ; X=&0E: FS command buffer offset
8DF3 STX fs_block_offset ; Store block offset for FS command
8DF5 LDA #&10 ; A=&10: 16 bytes of command data
8DF7 STA fs_options ; Store options byte
8DF9 STA fs_work_16 ; Store to FS workspace
8DFC LDX #&4a ; X=&4A: TXCB size for load command
8DFE LDY #5 ; Y=5: FCCMND (load as command)
8E00 JSR send_fs_examine ; Send FS examine/load command
8E03 LDA tube_flag ; Check for Tube co-processor
8E06 BEQ exec_at_load_addr ; No Tube: execute locally
8E08 ADC fs_load_upper ; Check load address upper bytes
8E0B ADC fs_addr_check ; Continue address range check
8E0E BCS exec_at_load_addr ; Carry set: not Tube space, exec locally
8E10 JSR tube_claim_loop ; Claim Tube transfer channel
8E13 LDX #9 ; X=9: source offset in FS reply
8E15 LDY #&0f ; Y=&0F: page &0F (FS command buffer)
8E17 LDA #4 ; A=4: Tube transfer type 4 (256-byte)
8E19 JMP tube_addr_claim ; Transfer data to Tube co-processor
8E1C .exec_at_load_addr←2← 8E06 BEQ← 8E0E 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
8E1F .fsreply_5_set_lib
STY fs_lib_handle ; Save library handle from FS reply
8E22 BCC jmp_restore_args ; SDISC path: skip CSD, jump to return
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
8E24 .fsreply_3_set_csd
STY fs_csd_handle ; Store CSD handle from FS reply
8E27 .jmp_restore_args←2← 8E22 BCC← 8E38 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.

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

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

8E3A .boot_cmd_execute
LDY fs_boot_option ; Y = boot option from FS workspace
8E3D LDX boot_oscli_offset,y ; X = command string offset from table
8E40 LDY #&8d ; Y = &8D (high byte of command address)
8E42 JMP oscli ; Execute boot command string via OSCLI

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.

8E45 .load_handle_calc_offset←2← 8E5F JSR← 8E6F 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
8E47 .calc_handle_offset←3← 8305 JSR← 8F85 JSR← 8F9E JSR
ASL ; A = handle * 2
8E48 ASL ; A = handle * 4
8E49 PHA ; Push handle*4 onto stack
8E4A ASL ; A = handle * 8
8E4B TSX ; X = stack pointer
8E4C ADC l0101,x ; A = handle*8 + handle*4 = handle*12
8E4F TAY ; Y = offset into handle workspace
8E50 PLA ; Clean up stack (discard handle*4)
8E51 CMP #&48 ; Offset >= &48? (6 handles max)
8E53 BCC return_6 ; Valid: return with C clear
8E55 LDY #0 ; Invalid: Y = 0
8E57 TYA ; A = 0, C set (error) A=&00
8E58 .return_6←1← 8E53 BCC
.return_calc_handle←1← 8E53 BCC
RTS ; Return after calculation
; *NET1: read file handle from received packet.
; Reads a byte from offset &6F of the RX buffer (net_rx_ptr)
; and falls through to net_2_read_handle_entry's common path.
8E59 .net_1_read_handle
LDY #&6f ; Y=&6F: RX buffer handle offset
8E5B LDA (net_rx_ptr),y ; Read handle from RX packet
8E5D BCC store_handle_return ; Valid handle: store and return
fall through ↓

*NET2: read handle entry from workspace

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

8E5F .net_2_read_handle_entry
JSR load_handle_calc_offset ; Look up handle &F0 in workspace
8E62 BCS rxpol2 ; Invalid handle: return 0
8E64 LDA (nfs_workspace),y ; Load stored handle value
8E66 CMP #&3f ; &3F = unused/closed slot marker
8E68 BNE store_handle_return ; Slot in use: return actual value
8E6A .rxpol2←2← 8E62 BCS← 8E72 BCS
LDA #0 ; Return 0 for closed/invalid handle
8E6C .store_handle_return←2← 8E5D BCC← 8E68 BNE
STA osword_pb_ptr ; Store result back to &F0
8E6E RTS ; Return

*NET3: close handle (mark as unused)

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

8E6F .net_3_close_handle
JSR load_handle_calc_offset ; Look up handle &F0 in workspace
8E72 BCS rxpol2 ; Invalid handle: return 0
8E74 ROR rx_flags ; Save carry via rotate
8E77 LDA #&3f ; A=&3F: handle closed/unused marker
8E79 STA (nfs_workspace),y ; Write marker to handle slot
8E7B ROL rx_flags ; Restore carry from rotate
8E7E RTS ; Return

Filing system OSWORD entry

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

8E7F .svc_8_osword
LDA osbyte_a_copy ; Command code from &EF
8E81 SBC #&0f ; Subtract &0F: OSWORD &0F-&13 become indices 0-4
8E83 BMI return_7 ; Outside our OSWORD range, exit
fall through ↓

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

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

8E85 .osword_12_handler
CMP #5 ; Only OSWORDs &0F-&13 (index 0-4)
8E87 BCS return_7 ; Index >= 5: not ours, return
8E89 JSR fs_osword_dispatch ; Dispatch via PHA/PHA/RTS table
8E8C LDY #2 ; Y=2: restore 3 bytes (&AA-&AC)
8E8E .copy_param_ptr←1← 8E94 BPL
LDA (net_rx_ptr),y ; Load saved param block byte
8E90 STA osword_flag,y ; Restore to &AA-&AC
8E93 DEY ; Next byte (descending)
8E94 BPL copy_param_ptr ; Loop for all 3 bytes
8E96 RTS ; Return to service handler

PHA/PHA/RTS dispatch for filing system OSWORDs

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

8E97 .fs_osword_dispatch←1← 8E89 JSR
TAX ; X = sub-function code for table lookup
8E98 LDA fs_osword_tbl_hi,x ; Load handler address high byte from table
8E9B PHA ; Push high byte for RTS dispatch
8E9C LDA osword_handler_lo,x ; Load handler address low byte from table
8E9F .fs_osword_tbl_lo
PHA ; Dispatch table: low bytes for OSWORD &0F-&13 handlers
8EA0 LDY #2 ; Y=2: save 3 bytes (&AA-&AC)
8EA2 .save1←1← 8EA8 BPL
LDA osword_flag,y ; Load param block pointer byte
8EA5 STA (net_rx_ptr),y ; Save to NFS workspace via (net_rx_ptr)
8EA7 DEY ; Next byte (descending)
8EA8 BPL save1 ; Loop for all 3 bytes
8EAA INY ; Y=0 after BPL exit; INY makes Y=1
8EAB LDA (osword_pb_ptr),y ; Read sub-function code from (&F0)+1
8EAD STY svc_state ; Store Y=1 to &A9
8EAF .return_7←2← 8E83 BMI← 8E87 BCS
RTS ; RTS dispatches to pushed handler address
8EB0 .osword_handler_lo←1← 8E9C LDA
EQUB <(osword_0f_handler-1)
8EB1 EQUB <(osword_10_handler-1)
8EB2 EQUB <(osword_11_handler-1)
8EB3 .copyl3
EQUB <(rs-1)
8EB4 EQUB <(econet_tx_rx-1)
8EB5 .fs_osword_tbl_hi←1← 8E98 LDA
EQUB >(osword_0f_handler-1) ; Dispatch table: high bytes for OSWORD &0F-&13 handlers
8EB6 EQUB >(osword_10_handler-1)
8EB7 EQUB >(osword_11_handler-1)
8EB8 EQUB >(rs-1)
8EB9 EQUB >(econet_tx_rx-1)

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
8EBA .osword_0f_handler
ASL tx_clear_flag ; Test TX semaphore (bit 7 to C)
8EBD TYA ; Save Y for return value
8EBE BCC readry ; C=0: TX busy, return error
8EC0 LDA net_rx_ptr_hi ; User TX CB in workspace page (high byte)
8EC2 STA ws_ptr_hi ; Set param block high byte
8EC4 STA nmi_tx_block_hi ; Set LTXCBP high byte for low-level TX
8EC6 LDA #&6f ; &6F: offset into workspace for user TXCB
8EC8 STA ws_ptr_lo ; Set param block low byte
8ECA STA nmi_tx_block ; Set LTXCBP low byte for low-level TX
8ECC LDX #&0f ; X=15: copy 16 bytes (OSWORD param block)
8ECE JSR copy_param_workspace ; Copy param block to user TX control block
8ED1 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 (&8EE9) to return just the buffer size and args size without copying the data.

8ED4 .osword_11_handler
LDA net_rx_ptr_hi ; Set source high byte from workspace page
8ED6 STA ws_ptr_hi ; Store as copy source high byte in &AC
8ED8 LDY #&7f ; JSRSIZ at workspace offset &7F
8EDA LDA (net_rx_ptr),y ; Load buffer size from workspace
8EDC INY ; Y=&80: start of JSR argument data Y=&80
8EDD STY ws_ptr_lo ; Store &80 as copy source low byte
8EDF TAX ; X = buffer size (loop counter)
8EE0 DEX ; X = size-1 (0-based count for copy)
8EE1 LDY #0 ; Y=0: start of destination param block
8EE3 JSR copy_param_workspace ; Copy X+1 bytes from workspace to param
8EE6 JMP clear_jsr_protection ; Clear JSR protection status (CLRJSR)
8EE9 .read_args_size←1← 8F44 BEQ
LDY #&7f ; Y=&7F: JSRSIZ offset (READRB entry)
8EEB LDA (net_rx_ptr),y ; Load buffer size from workspace
8EED LDY #1 ; Y=1: param block offset for size byte
8EEF STA (osword_pb_ptr),y ; Store buffer size to (&F0)+1
8EF1 INY ; Y=2: param block offset for args size Y=&02
8EF2 LDA #&80 ; A=&80: argument data starts at offset &80
8EF4 .readry←1← 8EBE BCC
STA (osword_pb_ptr),y ; Store args start offset to (&F0)+2
8EF6 RTS ; Return
8EF7 .osword_12_offsets←1← 8F0B LDA
EQUB &FF, &01
8EF9 .rs
CMP #6 ; Sub-function >= 6?
8EFB BCS rsl1 ; Yes: jump to sub 6-9 handler
8EFD CMP #4 ; Sub-function >= 4?
8EFF BCS rssl1 ; Sub-function 4 or 5: read/set protection
8F01 LSR ; LSR: 0->0, 1->0, 2->1, 3->1
8F02 LDX #&0d ; X=&0D: default to static workspace page
8F04 TAY ; Transfer LSR result to Y for indexing
8F05 BEQ set_workspace_page ; Y=0 (sub 0-1): use page &0D
8F07 LDX nfs_workspace_hi ; Y=1 (sub 2-3): use dynamic workspace
8F09 .set_workspace_page←1← 8F05 BEQ
STX ws_ptr_hi ; Store workspace page in &AC (hi byte)
8F0B LDA osword_12_offsets,y ; Load offset: &FF (sub 0-1) or &01 (sub 2-3)
8F0E STA ws_ptr_lo ; Store offset in &AB (lo byte)
8F10 LDX #1 ; X=1: copy 2 bytes
8F12 LDY #1 ; Y=1: start at param block offset 1
8F14 .copy_param_workspace←4← 8ECE JSR← 8EE3 JSR← 8F20 BPL← 8FB5 JSR
BCC skip_param_write ; C=0: skip param-to-workspace copy
8F16 LDA (osword_pb_ptr),y ; C=1: copy from param to workspace
8F18 STA (ws_ptr_lo),y ; Store param byte to workspace
8F1A .skip_param_write←1← 8F14 BCC
LDA (ws_ptr_lo),y ; Load byte from workspace
fall through ↓

Bidirectional block copy between OSWORD param block and workspace.

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

8F1C .copy_param_block
STA (osword_pb_ptr),y ; Store to param block (no-op if C=1)
8F1E INY ; Advance to next byte
8F1F DEX ; Decrement remaining count
8F20 BPL copy_param_workspace ; Loop while bytes remain
8F22 .logon3
.return_copy_param
RTS ; Return
8F23 .rssl1←1← 8EFF BCS
LSR ; LSR A: test bit 0 of sub-function
8F24 INY ; Y=1: offset for protection byte
8F25 LDA (osword_pb_ptr),y ; Load protection byte from param block
8F27 BCS rssl2 ; C=1 (odd sub): set protection
8F29 LDA prot_status ; C=0 (even sub): read current status
8F2C STA (osword_pb_ptr),y ; Return current value to param block
8F2E .rssl2←1← 8F27 BCS
STA prot_status ; Update protection status
8F31 STA saved_jsr_mask ; Also save as JSR mask backup
8F34 RTS ; Return
8F35 .read_fs_handle←1← 8F40 BEQ
LDY #&14 ; Y=&14: RX buffer offset for FS handle
8F37 LDA (net_rx_ptr),y ; Read FS reply handle from RX data
8F39 LDY #1 ; Y=1: param block byte 1
8F3B STA (osword_pb_ptr),y ; Return handle to caller's param block
8F3D RTS ; Return
8F3E .rsl1←1← 8EFB BCS
CMP #8 ; Sub-function 8: read FS handle
8F40 BEQ read_fs_handle ; Match: read handle from RX buffer
8F42 CMP #9 ; Sub-function 9: read args size
8F44 BEQ read_args_size ; Match: read ARGS buffer info
8F46 BPL return_last_error ; Sub >= 10 (bit 7 clear): read error
8F48 LDY #3 ; Y=3: start from handle 3 (descending)
8F4A LSR ; LSR: test read/write bit
8F4B BCC readc1 ; C=0: read handles from workspace
8F4D STY ws_page ; Init loop counter at Y=3
8F4F .copy_handles_to_ws←1← 8F5E BNE
LDY ws_page ; Reload loop counter
8F51 LDA (osword_pb_ptr),y ; Read handle from caller's param block
8F53 JSR handle_to_mask_a ; Convert handle number to bitmask
8F56 TYA ; TYA: get bitmask result
8F57 LDY ws_page ; Reload loop counter
8F59 STA fs_server_net,y ; Store bitmask to FS server table
8F5C DEC ws_page ; Next handle (descending)
8F5E BNE copy_handles_to_ws ; Loop for handles 3,2,1
8F60 RTS ; Return
8F61 .return_last_error←1← 8F46 BPL
INY ; Y=1 (post-INY): param block byte 1
8F62 LDA fs_last_error ; Read last FS error code
8F65 STA (osword_pb_ptr),y ; Return error to caller's param block
8F67 RTS ; Return
8F68 .readc1←2← 8F4B BCC← 8F71 BNE
LDA fs_server_net,y ; A=single-bit bitmask
8F6B JSR mask_to_handle ; Convert bitmask to handle number (FS2A)
8F6E STA (osword_pb_ptr),y ; A=handle number (&20-&27) Y=preserved
8F70 DEY ; Next handle (descending)
8F71 BNE readc1 ; Loop for handles 3,2,1
8F73 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
8F74 .osword_10_handler
LDX nfs_workspace_hi ; Workspace page high byte
8F76 STX ws_ptr_hi ; Set up pointer high byte in &AC
8F78 STY ws_ptr_lo ; Save param block high byte in &AB
8F7A ROR rx_flags ; Disable user RX during CB operation
8F7D LDA (osword_pb_ptr),y ; Read first byte of param block
8F7F STA osword_flag ; Save: 0=open new, non-zero=read RXCB
8F81 BNE read_rxcb ; Non-zero: read specified RXCB
8F83 LDA #3 ; Start scan from RXCB #3 (0-2 reserved)
8F85 .scan0←1← 8F97 BNE
JSR calc_handle_offset ; Convert RXCB number to workspace offset
8F88 BCS openl4 ; Invalid RXCB: return zero
8F8A LSR ; LSR twice: byte offset / 4
8F8B LSR ; Yields RXCB number from offset
8F8C TAX ; X = RXCB number for iteration
8F8D LDA (ws_ptr_lo),y ; Read flag byte from RXCB workspace
8F8F BEQ openl4 ; Zero = end of CB list
8F91 CMP #&3f ; &3F = deleted slot, free for reuse
8F93 BEQ scan1 ; Found free slot
8F95 INX ; Try next RXCB
8F96 TXA ; A = next RXCB number
8F97 BNE scan0 ; Continue scan (always branches)
8F99 .scan1←1← 8F93 BEQ
TXA ; A = free RXCB number
8F9A LDX #0 ; X=0 for indexed indirect store
8F9C STA (osword_pb_ptr,x) ; Return RXCB number to caller's byte 0
8F9E .read_rxcb←1← 8F81 BNE
JSR calc_handle_offset ; Convert RXCB number to workspace offset
8FA1 BCS openl4 ; Invalid: write zero to param block
8FA3 DEY ; Y = offset-1: points to flag byte
8FA4 STY ws_ptr_lo ; Set &AB = workspace ptr low byte
8FA6 LDA #&c0 ; &C0: test mask for flag byte
8FA8 LDY #1 ; Y=1: flag byte offset in RXCB
8FAA LDX #&0b ; Enable interrupts before transmit
8FAC CPY osword_flag ; Compare Y(1) with saved byte (open/read)
8FAE ADC (ws_ptr_lo),y ; ADC flag: test if slot is in use
8FB0 BEQ openl6 ; Dest station = &FFFF (accept reply from any station)
8FB2 BMI openl7 ; Negative: slot has received data
8FB4 .copy_rxcb_to_param←1← 8FC4 BNE
CLC ; C=0: workspace-to-param direction
8FB5 .openl6←1← 8FB0 BEQ
JSR copy_param_workspace ; Copy RXCB data to param block
8FB8 BCS reenable_rx ; Done: skip deletion on error
8FBA LDA #&3f ; Mark CB as consumed (consume-once)
8FBC LDY #1 ; Y=1: flag byte offset
8FBE STA (ws_ptr_lo),y ; Write &3F to mark slot deleted
8FC0 BNE reenable_rx ; Branch to exit (always taken) ALWAYS branch
8FC2 .openl7←1← 8FB2 BMI
ADC #1 ; Advance through multi-byte field
8FC4 BNE copy_rxcb_to_param ; Loop until all bytes processed
8FC6 DEY ; Y=-1 → Y=0 after STA below
8FC7 .openl4←3← 8F88 BCS← 8F8F BEQ← 8FA1 BCS
STA (osword_pb_ptr),y ; Return zero (no free RXCB found)
8FC9 .reenable_rx←2← 8FB8 BCS← 8FC0 BNE
ROL rx_flags ; Re-enable user RX
8FCC RTS ; Return

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
8FCD .setup_rx_buffer_ptrs←1← 9000 JSR
LDY #&1c ; Y=&1C: workspace offset for RX data start
8FCF LDA osword_pb_ptr ; A = base address low byte
8FD1 ADC #1 ; A = base + 1 (skip length byte)
8FD3 JSR store_16bit_at_y ; Receive data blocks until command byte = &00 or &0D
8FD6 LDY #1 ; Read data length from (&F0)+1
8FD8 LDA (osword_pb_ptr),y ; A = data length byte
8FDA LDY #&20 ; Workspace offset &20 = RX data end
8FDC ADC osword_pb_ptr ; A = base + length = end address low
8FDE .store_16bit_at_y←1← 8FD3 JSR
STA (nfs_workspace),y ; Store low byte of 16-bit address
8FE0 INY ; Advance to high byte offset
8FE1 LDA osword_pb_ptr_hi ; A = high byte of base address
8FE3 ADC #0 ; Add carry for 16-bit addition
8FE5 STA (nfs_workspace),y ; Store high byte
8FE7 RTS ; Return

Econet transmit/receive handler

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

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

9072 .enable_irq_and_tx←1← 905B JSR
CLI ; Enable interrupts
9073 JMP tx_poll_ff ; Transmit via tx_poll_ff

NETVEC dispatch handler (ENTRY)

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

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

On EntryAreason code (0-8)
On ExitApreserved
Xpreserved
Ypreserved
9076 .osword_dispatch
PHP ; Save processor status
9077 PHA ; Save A (reason code)
9078 TXA ; Save X
9079 PHA ; Push X to stack
907A TYA ; Save Y
907B PHA ; Push Y to stack
907C TSX ; Get stack pointer for indexed access
907D LDA l0103,x ; Retrieve original A (reason code) from stack
9080 CMP #9 ; Reason codes 0-8 only
9082 BCS entry1 ; Code >= 9: skip dispatch, restore regs
9084 TAX ; X = reason code for table lookup
9085 JSR osword_trampoline ; Dispatch to handler via trampoline
9088 .entry1←1← 9082 BCS
PLA ; Restore Y
9089 TAY ; Transfer to Y register
908A PLA ; Restore X
908B TAX ; Transfer to X register
908C PLA ; Restore A
908D PLP ; Restore processor status flags
908E RTS ; Return with all registers preserved
908F .osword_trampoline←1← 9085 JSR
LDA netvec_handler_hi,x ; PHA/PHA/RTS trampoline: push handler addr-1, RTS jumps to it
9092 PHA ; Push high byte of handler address
9093 LDA osword_tbl_lo,x ; Load handler low byte from table
9096 PHA ; Push low byte of handler address
9097 LDA osbyte_a_copy ; Load workspace byte &EF for handler
9099 RTS ; RTS dispatches to pushed handler
909A .osword_tbl_lo←1← 9093 LDA
EQUB <(return_1-1)
909B EQUB <(remote_print_handler-1)
909C EQUB <(remote_print_handler-1)
909D EQUB <(remote_print_handler-1)
909E EQUB <(net_write_char_handler-1)
909F EQUB <(printer_select_handler-1)
90A0 EQUB <(return_1-1)
90A1 .osword_tbl_hi
EQUB <(remote_cmd_dispatch-1)
90A2 EQUB <(nword-1)
90A3 .netvec_handler_hi←1← 908F LDA
EQUB >(return_1-1)
90A4 EQUB >(remote_print_handler-1)
90A5 EQUB >(remote_print_handler-1)
90A6 EQUB >(remote_print_handler-1)
90A7 EQUB >(net_write_char_handler-1)
90A8 EQUB >(printer_select_handler-1)
90A9 EQUB >(return_1-1)
90AA EQUB >(remote_cmd_dispatch-1)
90AB EQUB >(nword-1)
90AC .net_write_char_handler
TSX ; Get stack pointer for P register access
90AD ROR l0106,x ; ROR/ASL on stacked P: zeros carry to signal success
90B0 ASL l0106,x ; ASL: restore P after ROR zeroed carry
90B3 TYA ; Y = character to write
90B4 LDY #&da ; Store character at workspace offset &DA
90B6 STA (nfs_workspace),y ; Store char at workspace offset &DA
90B8 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
90BA .setup_tx_and_send←3← 81CB JSR← 910D JSR← 916E JSR
LDY #&d9 ; Y=&D9: command type offset
90BC STA (nfs_workspace),y ; Store command type at ws+&D9
90BE LDA #&80 ; Mark TX control block as active (&80)
90C0 LDY #&0c ; Y=&0C: TXCB start offset
90C2 STA (nfs_workspace),y ; Set TX active flag at ws+&0C
90C4 LDA net_tx_ptr ; Save net_tx_ptr; redirect to workspace TXCB
90C6 PHA ; Save net_tx_ptr low
90C7 LDA net_tx_ptr_hi ; Load net_tx_ptr high
90C9 PHA ; Save net_tx_ptr high
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 PLA ; Restore net_tx_ptr high
90D8 STA net_tx_ptr_hi ; Write back
90DA PLA ; Restore net_tx_ptr low
90DB STA net_tx_ptr ; Write back
90DD 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.

90DE .remote_cmd_dispatch
LDY osword_pb_ptr_hi ; Load original Y (OSBYTE secondary param)
90E0 CMP #&81 ; OSBYTE &81 (INKEY): always forward to terminal
90E2 BEQ dispatch_remote_osbyte ; Forward &81 to terminal for keyboard read
90E4 LDY #1 ; Y=1: search NCTBPL table (execute on both)
90E6 LDX #7 ; X=7: 8-entry NCTBPL table size
90E8 JSR match_osbyte_code ; Search for OSBYTE code in NCTBPL table
90EB BEQ dispatch_remote_osbyte ; Match found: dispatch with Y=1 (both)
90ED DEY ; Y=-1: search NCTBMI table (terminal only)
90EE DEY ; Second DEY: Y=&FF (from 1 via 0)
90EF LDX #&0e ; X=&0E: 15-entry NCTBMI table size
90F1 JSR match_osbyte_code ; Search for OSBYTE code in NCTBMI table
90F4 BEQ dispatch_remote_osbyte ; Match found: dispatch with Y=&FF (terminal)
90F6 INY ; Y=0: OSBYTE not recognised, ignore
90F7 .dispatch_remote_osbyte←3← 90E2 BEQ← 90EB BEQ← 90F4 BEQ
LDX #2 ; X=2 bytes to copy (default for RBYTE)
90F9 TYA ; A=Y: check table match result
90FA BEQ return_nbyte ; Y=0: not recognised, return unhandled
90FC PHP ; Y>0 (NCTBPL): send only, no result expected
90FD BPL nbyte6 ; Y>0 (NCTBPL): no result expected, skip RX
90FF INX ; Y<0 (NCTBMI): X=3 bytes (result + P flags) X=&03
9100 .nbyte6←1← 90FD BPL
LDY #&dc ; Y=&DC: top of 3-byte stack frame region
9102 .nbyte1←1← 910A BPL
LDA tube_claimed_id,y ; Copy OSBYTE args from stack frame to workspace
9105 STA (nfs_workspace),y ; Store to NFS workspace for transmission
9107 DEY ; Next byte (descending)
9108 CPY #&da ; Copied all 3 bytes? (&DC, &DB, &DA)
910A BPL nbyte1 ; Loop for remaining bytes
910C TXA ; A = byte count for setup_tx_and_send
910D JSR setup_tx_and_send ; Build TXCB and transmit to terminal
9110 PLP ; Restore N flag from table match type
9111 BPL return_nbyte ; Y was positive (NCTBPL): done, no result
9113 LDA #&7f ; Set up RX control block to wait for reply
9115 STA (net_tx_ptr,x) ; Write &7F to RXCB (wait for reply)
9117 .poll_rxcb_loop←1← 9119 BPL
LDA (net_tx_ptr,x) ; Poll RXCB for completion (bit7)
9119 BPL poll_rxcb_loop ; Bit7 clear: still waiting, poll again
911B TSX ; X = stack pointer for register restoration
911C LDY #&dd ; Y=&DD: saved P byte offset in workspace
911E LDA (nfs_workspace),y ; Load remote processor status from reply
9120 ORA #&44 ; Force V=1 (claimed) and I=1 (no IRQ) in saved P
9122 BNE nbyte5 ; ALWAYS branch (ORA #&44 never zero) ALWAYS branch
9124 .nbyte4←1← 912D BNE
DEY ; Previous workspace offset
9125 DEX ; Previous stack register slot
9126 LDA (nfs_workspace),y ; Load next result byte (X, then Y)
9128 .nbyte5←1← 9122 BNE
STA l0106,x ; Write result bytes to stacked registers
912B CPY #&da ; Copied all result bytes? (P at &DA)
912D BNE nbyte4 ; Loop for remaining result bytes
912F .return_nbyte←2← 90FA BEQ← 9111 BPL
RTS ; Return to OSBYTE dispatcher
9130 .match_osbyte_code←3← 90E8 JSR← 90F1 JSR← 9136 BPL
CMP remote_osbyte_table,x ; Compare OSBYTE code with table entry
9133 BEQ return_match_osbyte ; Match found: return with Z=1
9135 DEX ; Next table entry (descending)
9136 BPL match_osbyte_code ; Loop for remaining entries
9138 .return_match_osbyte←2← 9133 BEQ← 9150 BNE
RTS ; Return; Z=1 if match, Z=0 if not
9139 .remote_osbyte_table←1← 9130 CMP
EQUB &04, &09, &0A, &14, &15, &9A, &9B, &E2, &0B, &0C, &0F, &79, &7A, &E3, &E4
9148 .nword
LDY #&0e ; Y=14: max OSWORD parameter bytes
914A CMP #7 ; OSWORD 7 = make a sound
914C BEQ copy_params_rword ; OSWORD 7 (sound): handle via common path
914E CMP #8 ; OSWORD 8 = define an envelope
fall through ↓

Fn 8: remote OSWORD handler (NWORD)

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

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

Alternate entry into control block setup

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

9173 .ctrl_block_setup_alt←2← 9058 JSR← 9067 JSR
LDX #&0d ; X=&0D: template offset for alt entry
9175 LDY #&7c ; Y=&7C: target workspace offset for alt entry
9177 BIT tx_ctrl_upper ; BIT test: V flag = bit 6 of &83AF
917A 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 &91A8 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

917C .ctrl_block_setup←1← 84BC JSR
LDY #&17 ; Y=&17: workspace target offset (main entry)
917E LDX #&1a ; X=&1A: template table index (main entry)
9180 .ctrl_block_setup_clv←1← 9244 JSR
CLV ; V=0: target is (nfs_workspace)
9181 .cbset2←1← 917A BVS
LDA ctrl_block_template,x ; Load template byte from ctrl_block_template[X]
9184 CMP #&fe ; &FE = stop sentinel
9186 BEQ cb_template_tail ; End of template: jump to exit
9188 CMP #&fd ; &FD = skip sentinel
918A BEQ cb_template_main_start ; Skip: don't store, just decrement Y
918C CMP #&fc ; &FC = page byte sentinel
918E BNE cbset3 ; Not sentinel: store template value directly
9190 LDA net_rx_ptr_hi ; V=1: use (net_rx_ptr) page
9192 BVS rxcb_matched ; V=1: skip to net_rx_ptr page
9194 LDA nfs_workspace_hi ; V=0: use (nfs_workspace) page
9196 .rxcb_matched←1← 9192 BVS
STA net_tx_ptr_hi ; PAGE byte → Y=&02 / Y=&74
9198 .cbset3←1← 918E BNE
BVS cbset4 ; → Y=&04 / Y=&76
919A STA (nfs_workspace),y ; PAGE byte → Y=&06 / Y=&78
919C EQUB &50 ; → Y=&08 / Y=&7A
919D EQUB &02
919E .cbset4←1← 9198 BVS
EQUB &91 ; Alt-path only → Y=&70
919F EQUB &9C
91A0 .cb_template_main_start←1← 918A BEQ
EQUB &88 ; SKIP
91A1 EQUB &CA ; → Y=&01 / Y=&73
91A2 EQUB &10
91A3 EQUB &DD
91A4 .cb_template_tail←1← 9186 BEQ
EQUB &C8
91A5 EQUB &84
91A6 EQUB &9A ; PAGE byte → Y=&06 / Y=&78
91A7 EQUB &60 ; → Y=&07 / Y=&79
fall through ↓

Control block initialisation template

Read by the loop at &9181, 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 &83AF

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)

91A8 .ctrl_block_template←1← 9181 LDA
EQUB &85 ; Alt-path only → Y=&6F
91A9 EQUB &00
91AA EQUB &FD ; SKIP
91AB EQUB &FD
91AC EQUB &7D ; → Y=&0C (main only)
91AD EQUB &FC ; → Y=&0D (main only)
91AE EQUB &FF ; → Y=&03 / Y=&75
91AF EQUB &FF ; SKIP (main only)
91B0 EQUB &7E ; → Y=&10 (main only)
91B1 EQUB &FC
91B2 EQUB &FF
91B3 EQUB &FF ; → Y=&08 / Y=&7A
91B4 EQUB &00 ; → Y=&09 / Y=&7B
91B5 EQUB &00 ; PAGE byte → Y=&15 (main only)
91B6 EQUB &FE ; → Y=&16 (main only)
91B7 EQUB &80
91B8 EQUB &93, &FD, &FD, &D9, &FC, ; SKIP (main only) PAGE byte &FF, &FF, &DE, &FC, &FF, ; → Y=&11 (main only) → Y=&12 &FF, &FE, &D1, &FD, &FD, ; (main only) → Y=&13 (main &1F, &FD, &FF, &FF, &FD, ; only) → Y=&14 (main only) → &FD, &FF, &FF ; Y=&17 (main only)

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
91CF .printer_select_handler
DEX ; X-1: convert 1-based buffer to 0-based
91D0 CPX osword_pb_ptr ; Is this the network printer buffer?
91D2 BNE setup1 ; No: skip printer init
91D4 LDA #&1f ; &1F = initial buffer pointer offset
91D6 STA printer_buf_ptr ; Reset printer buffer write position
91D9 LDA #&41 ; &41 = initial PFLAGS (bit 6 set, bit 0 set)
91DB .setup1←1← 91D2 BNE
STA l0d60 ; Store initial PFLAGS value
91DE .return_printer_select←2← 91E1 BNE← 91F5 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)
91DF .remote_print_handler
CPY #4 ; Only handle buffer 4 (network printer)
91E1 BNE return_printer_select ; Not buffer 4: ignore
91E3 TXA ; A = reason code
91E4 DEX ; Reason 1? (DEX: 1->0)
91E5 BNE toggle_print_flag ; Not reason 1: handle Ctrl-B/C
91E7 TSX ; Get stack pointer for P register
91E8 ORA l0106,x ; Force I flag in stacked P to block IRQs
91EB STA l0106,x ; Write back modified P register
91EE .prlp1←2← 91FD BCC← 9202 BCC
LDA #osbyte_read_buffer ; OSBYTE &91: extract char from MOS buffer
91F0 LDX #buffer_printer ; X=3: printer buffer number
91F2 JSR osbyte ; Get character from input buffer (C is set if the buffer is empty, otherwise Y=extracted character)
91F5 BCS return_printer_select ; Buffer empty: return
91F7 TYA ; Y = extracted character Y is the character extracted from the buffer
91F8 JSR store_output_byte ; Store char in output buffer
91FB CPY #&6e ; Buffer nearly full? (&6E = threshold)
91FD BCC prlp1 ; Not full: get next char
91FF JSR flush_output_block ; Buffer full: flush to network
9202 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
9204 .store_output_byte←2← 91F8 JSR← 9211 JSR
LDY printer_buf_ptr ; Load current buffer offset
9207 STA (net_rx_ptr),y ; Store byte at current position
9209 INC printer_buf_ptr ; Advance buffer pointer
920C RTS ; Return; Y = buffer offset
920D .toggle_print_flag←1← 91E5 BNE
PHA ; Save reason code
920E TXA ; A = reason code
920F EOR #1 ; EOR #1: toggle print-active flag (bit 0)
9211 JSR store_output_byte ; Store toggled flag as output byte
9214 EOR l0d60 ; XOR with current PFLAGS
9217 ROR ; Test if sequence changed (bit 7 mismatch)
9218 BCC pril1 ; Sequence unchanged: skip flush
921A ROL ; Undo ROR
921B STA l0d60 ; Store toggled PFLAGS
921E JSR flush_output_block ; Flush current output block
9221 .pril1←1← 9218 BCC
LDA l0d60 ; Reload current PFLAGS
9224 AND #&f0 ; Extract upper nibble of PFLAGS
9226 ROR ; Shift for bit extraction
9227 TAX ; Save in X
9228 PLA ; Restore original reason code
9229 ROR ; Merge print-active bit from original A
922A TXA ; Retrieve shifted PFLAGS
922B ROL ; Recombine into new PFLAGS value
922C STA l0d60 ; Store recombined PFLAGS value
922F 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.

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

92A6 .lang_2_save_palette_vdu
LDA table_idx ; Save current table index
92A8 PHA ; Push for later restore
92A9 LDA #&e9 ; Point workspace to palette save area (&E9)
92AB STA nfs_workspace ; Set workspace low byte
92AD LDY #0 ; Y=0: first palette entry
92AF STY table_idx ; Clear table index counter
92B1 LDA l0350 ; Save current screen MODE to workspace
92B4 STA (nfs_workspace),y ; Store MODE at workspace[0]
92B6 INC nfs_workspace ; Advance workspace pointer past MODE byte
92B8 LDA l0351 ; Read colour count (from &0351)
92BB PHA ; Push for iteration count tracking
92BC TYA ; A=0: logical colour number for OSWORD A=&00
92BD .save_palette_entry←1← 92DC BNE
STA (nfs_workspace),y ; Store logical colour at workspace[0]
92BF LDX nfs_workspace ; X = workspace ptr low (param block addr)
92C1 LDY nfs_workspace_hi ; Y = workspace ptr high
92C3 LDA #osword_read_palette ; OSWORD &0B: read palette for logical colour
92C5 JSR osword ; Read palette
92C8 PLA ; Recover colour count
92C9 LDY #0 ; Y=0: access workspace[0]
92CB STA (nfs_workspace),y ; Write colour count back to workspace[0]
92CD INY ; Y=1: access workspace[1] (palette result) Y=&01
92CE LDA (nfs_workspace),y ; Read palette value returned by OSWORD
92D0 PHA ; Push palette value for next iteration
92D1 LDX nfs_workspace ; X = current workspace ptr low
92D3 INC nfs_workspace ; Advance workspace pointer
92D5 INC table_idx ; Increment table index
92D7 DEY ; Y=0 for next store Y=&00
92D8 LDA table_idx ; Load table index as logical colour
92DA CPX #&f9 ; Loop until workspace wraps past &F9
92DC BNE save_palette_entry ; Continue for all 16 palette entries
92DE PLA ; Discard last palette value from stack
92DF STY table_idx ; Reset table index to 0
92E1 INC nfs_workspace ; Advance workspace past palette data
92E3 JSR save_vdu_state ; Save cursor pos and OSBYTE state values
92E6 INC nfs_workspace ; Advance workspace past VDU state data
92E8 PLA ; Recover saved table index
92E9 STA table_idx ; Restore table index
92EB .clear_jsr_protection←4← 84A0 JMP← 84C8 JSR← 84EF JSR← 8EE6 JMP
LDA saved_jsr_mask ; Restore LSTAT from saved OLDJSR value
92EE STA prot_status ; Write to protection status
92F1 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
92F2 .save_vdu_state←1← 92E3 JSR
LDA l0355 ; Read cursor editing state
92F5 STA (nfs_workspace),y ; Store to workspace[Y]
92F7 TAX ; Preserve in X for OSBYTE
92F8 JSR read_vdu_osbyte ; OSBYTE &85: read cursor position
92FB INC nfs_workspace ; Advance workspace pointer
92FD TYA ; Y result from OSBYTE &85
92FE STA (nfs_workspace,x) ; Store Y pos to workspace (X=0)
9300 JSR read_vdu_osbyte_x0 ; Self-call trick: executes twice
9303 .read_vdu_osbyte_x0←1← 9300 JSR
LDX #0 ; X=0 for (zp,X) addressing
9305 .read_vdu_osbyte←1← 92F8 JSR
LDY table_idx ; Index into OSBYTE number table
9307 INC table_idx ; Next table entry next time
9309 INC nfs_workspace ; Advance workspace pointer
930B LDA vdu_osbyte_table,y ; Read OSBYTE number from table
930E LDY #&ff ; Y=&FF: read current value
9310 JSR osbyte ; Call OSBYTE
9313 TXA ; Result in X to A
9314 LDX #0 ; X=0 for indexed indirect store
9316 STA (nfs_workspace,x) ; Store result to workspace
9318 RTS ; Return after storing result
9319 .vdu_osbyte_table←1← 930B LDA
EQUB &85, ; OSBYTE &85: read cursor position OSBYTE &C2, ; &C3: read screen start address &C3
; 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
; 12-entry table at &0500). The R2 command byte is stored ; at &55
; before dispatch via JMP (&0500).
931C 0016 .nmi_workspace_start←1← 8178 STA
.tube_brk_handler←1← 8178 STA
LDA #&ff ; A=&FF: signal error to co-processor via R4
931E 0018 JSR tube_send_r4 ; Send &FF error signal to Tube R4
9321 001B LDA tube_data_register_2 ; Flush any pending R2 byte
9324 001E LDA #0 ; A=0: send zero prefix to R2
9326 0020 .tube_send_zero_r2←1← 0626 CMP
JSR tube_send_r2 ; Send zero prefix byte via R2
9329 0023 TAY ; Y=0: start of error block at (&FD)
932A 0024 LDA (brk_ptr),y ; Load error number from (&FD),0
932C 0026 JSR tube_send_r2 ; Send error number via R2
932F 0029 .tube_brk_send_loop←1← 0030 BNE
INY ; Advance to next error string byte
9330 002A .tube_send_error_byte
LDA (brk_ptr),y ; Load next error string byte
9332 002C JSR tube_send_r2 ; Send error string byte via R2
9335 002F TAX ; Zero byte = end of error string
9336 0030 BNE tube_brk_send_loop ; Loop until zero terminator sent
9338 0032 .tube_reset_stack←1← 046A JMP
LDX #&ff ; Reset stack pointer to top
933A 0034 TXS ; TXS: set stack pointer from X
933B 0035 CLI ; Enable interrupts for main loop
933C 0036 .tube_main_loop←6← 0044 BPL← 057F JMP← 05A6 JMP← 0604 JMP← 0665 JMP← 0692 JMP
BIT tube_status_1_and_tube_control ; BIT R1 status: check WRCH request
933F 0039 BPL tube_poll_r2 ; R1 not ready: check R2 instead
9341 003B .tube_handle_wrch←1← 0049 BMI
LDA tube_data_register_1 ; Read character from Tube R1 data
9344 003E JSR oswrch ; Write character
9347 0041 .tube_poll_r2←1← 0039 BPL
BIT tube_status_register_2 ; BIT R2 status: check command byte
934A 0044 BPL tube_main_loop ; R2 not ready: loop back to R1 check
934C 0046 BIT tube_status_1_and_tube_control ; Re-check R1: WRCH has priority over R2
934F 0049 BMI tube_handle_wrch ; R1 ready: handle WRCH first
9351 004B LDX tube_data_register_2 ; Read command byte from Tube R2 data
9354 004E STX tube_jmp_target ; Self-modify JMP low byte for dispatch
9356 0050 .tube_dispatch_cmd
JMP (tube_dispatch_table) ; Dispatch to handler via indirect JMP
9359 0053 .tube_transfer_addr←2← 04D7 STY← 04E7 STA
EQUB &00
935A 0054 .tube_xfer_page←3← 04AF INC← 04CD STA← 04EC STA
EQUB &80
935B 0055 .tube_xfer_addr_2←2← 04B3 INC← 04F6 STY
EQUB &00
935C 0056 .tube_xfer_addr_3←2← 04B7 INC← 04F4 STA
EQUB &00
; Tube host code page 4 — reference: NFS12 (BEGIN, ADRR, ; SENDW)
; Copied from ROM at &935D during init. The first 28 bytes ; (&0400-&041B)
; overlap with the end of the ZP block (the same ROM bytes ; serve both
; the ZP copy at &005B-&0076 and this page at ; &0400-&041B). Contains:
; &0400: JMP &0473 (BEGIN — CLI parser / startup entry)
; &0403: JMP &06E2 (tube_escape_check)
; &0406: tube_addr_claim — Tube address claim protocol (ADRR)
; &0414: tube_post_init — called after ROM→RAM copy
; &0473: BEGIN — startup/CLI entry, break type check
; &04CB: tube_init_reloc — initialise relocation address for ROM transfer
935D 0400 .tube_code_page4←1← 815E STA
JMP tube_begin ; JMP to BEGIN startup entry
9360 0403 .tube_escape_entry
JMP tube_escape_check ; JMP to tube_escape_check (&06A7)
9363 0406 .tube_addr_claim←10← 0493 JSR← 04C8 JMP← 8B5D JSR← 8B74 JSR← 8BD1 JSR← 8E19 JMP← 99D1 JSR← 9A3A JSR← 9F75 JSR← 9F7D JSR
CMP #&80 ; A>=&80: address claim; A<&80: data transfer
9365 0408 BCC tube_transfer_setup ; A<&80: data transfer setup (SENDW)
9367 040A CMP #&c0 ; A>=&C0: new address claim from another host
9369 040C BCS addr_claim_external ; C=1: external claim, check ownership
936B 040E ORA #&40 ; Map &80-&BF range to &C0-&FF for comparison
936D 0410 CMP tube_claimed_id ; Is this for our currently-claimed address?
936F 0412 BNE return_tube_init ; Not our address: return
9371 0414 .tube_send_release←1← 0464 JSR
LDA #5 ; A=5: Tube release request code
9373 0416 JSR tube_send_r4 ; Send release code via R4
9376 0419 LDA tube_claimed_id ; Load current Tube claim ID
9378 041B JSR tube_send_r4 ; Send claim ID via R4
937B 041E .tube_post_init←1← 8170 JSR
LDA #&80 ; &80 sentinel: clear address claim
937D 0420 STA tube_claim_flag ; Store to claim-in-progress flag
937F 0422 RTS ; Return from tube_post_init
9380 0423 .addr_claim_external←1← 040C BCS
ASL tube_claim_flag ; Another host claiming; check if we're owner
9382 0425 BCS accept_new_claim ; C=1: we have an active claim
9384 0427 CMP tube_claimed_id ; Compare with our claimed address
9386 0429 BEQ return_tube_init ; Match: return (we already have it)
9388 042B CLC ; Not ours: CLC = we don't own this address
9389 042C RTS ; Return with C=0 (claim denied)
938A 042D .accept_new_claim←1← 0425 BCS
STA tube_claimed_id ; Accept new claim: update our address
938C 042F .return_tube_init←2← 0412 BNE← 0429 BEQ
RTS ; Return with address updated
938D 0430 .tube_transfer_setup←1← 0408 BCC
PHP ; PHP: save interrupt state
938E 0431 SEI ; SEI: disable interrupts for R4 protocol
938F 0432 .setup_data_transfer
STY tube_data_ptr_hi ; Save 16-bit transfer address from (X,Y)
9391 0434 STX tube_data_ptr ; Store address pointer low byte
9393 0436 JSR tube_send_r4 ; Send transfer type byte to co-processor
9396 0439 TAX ; X = transfer type for table lookup
9397 043A LDY #3 ; Y=3: send 4 bytes (address + claimed addr)
9399 043C LDA tube_claimed_id ; Send our claimed address + 4-byte xfer addr
939B 043E JSR tube_send_r4 ; Send transfer address byte
939E 0441 .send_xfer_addr_bytes←1← 0447 BPL
LDA (tube_data_ptr),y ; Load transfer address byte from (X,Y)
93A0 0443 JSR tube_send_r4 ; Send address byte to co-processor via R4
93A3 0446 DEY ; Previous byte (big-endian: 3,2,1,0)
93A4 0447 BPL send_xfer_addr_bytes ; Loop for all 4 address bytes
93A6 0449 JSR tube_send_r4 ; Send claimed address via R4
93A9 044C LDY #&18 ; Y=&18: enable Tube control register
93AB 044E STY tube_status_1_and_tube_control ; Enable Tube interrupt generation
93AE 0451 LDA tube_ctrl_values,x ; Look up Tube control bits for this xfer type
93B1 0454 STA tube_status_1_and_tube_control ; Apply transfer- specific control bits
93B4 0457 LSR ; LSR: check bit 2 (2-byte flush needed?)
93B5 0458 LSR ; LSR: shift bit 2 to carry
93B6 0459 .poll_r4_copro_ack←1← 045C BVC
BIT tube_status_register_4_and_cpu_control ; Poll R4 status for co- processo r response
93B9 045C BVC poll_r4_copro_ack ; Bit 6 clear: not ready, keep polling
93BB 045E BCS flush_r3_nmi_check ; R4 bit 7: co-processor acknowledged transfer
93BD 0460 CPX #4 ; Type 4 = SENDW (host-to-parasite word xfer)
93BF 0462 BNE skip_nmi_release ; Not SENDW type: skip release path
93C1 0464 .tube_sendw_complete←1← 048F BEQ
JSR tube_send_release ; SENDW complete: release, sync, restart
93C4 0467 JSR tube_send_r2 ; Sync via R2 send
93C7 046A JMP tube_reset_stack ; Restart Tube main loop
93CA 046D .flush_r3_nmi_check←1← 045E BCS
BIT tube_data_register_3 ; Flush R3 data (first byte)
93CD 0470 BIT tube_data_register_3 ; Flush R3 data (second byte)
93D0 0473 .copro_ack_nmi_check
LSR ; LSR: check bit 0 (NMI used?)
93D1 0474 BCC skip_nmi_release ; C=0: NMI not used, skip NMI release
93D3 0476 LDY #&88 ; Release Tube NMI (transfer used interrupts)
93D5 0478 STY tube_status_1_and_tube_control ; Write &88 to Tube control to release NMI
93D8 047B .skip_nmi_release←2← 0462 BNE← 0474 BCC
PLP ; Restore interrupt state
93D9 047C .return_tube_xfer
RTS ; Return from transfer setup
93DA 047D .tube_begin←1← 0400 JMP
CLI ; BEGIN: enable interrupts for Tube host code
93DB 047E BCS claim_addr_ff ; C=1: hard break, claim addr &FF
93DD 0480 BNE check_break_type ; C=0, A!=0: re-init path
93DF 0482 JMP tube_reply_ack ; Z=1 from C=0 path: just acknowledge
93E2 0485 .check_break_type←1← 0480 BNE
LDX #0 ; X=0 for OSBYTE
93E4 0487 LDY #&ff ; Y=&FF for OSBYTE
93E6 0489 LDA #osbyte_read_write_last_break_type ; OSBYTE &FD: what type of reset was this?
93E8 048B JSR osbyte ; Read type of last reset
93EB 048E TXA ; X=value of type of last reset
93EC 048F BEQ tube_sendw_complete ; Soft break (X=0): re-init Tube and restart
93EE 0491 .claim_addr_ff←2← 047E BCS← 0496 BCC
LDA #&ff ; Claim address &FF (startup = highest prio)
93F0 0493 JSR tube_addr_claim ; Request address claim from Tube system
93F3 0496 BCC claim_addr_ff ; C=0: claim failed, retry
93F5 0498 JSR tube_init_reloc ; Init reloc pointers from ROM header
93F8 049B .next_rom_page←1← 04BD BVC
LDA #7 ; R4 cmd 7: SENDW to send ROM to parasite
93FA 049D JSR tube_claim_default ; Set up Tube for SENDW transfer
93FD 04A0 LDY #0 ; Y=0: start at beginning of page
93FF 04A2 STY zp_ptr_lo ; Store to zero page pointer low byte
9401 04A4 .send_rom_page_bytes←1← 04AD BNE
LDA (zp_ptr_lo),y ; Send 256-byte page via R3, byte at a time
9403 04A6 STA tube_data_register_3 ; Write byte to Tube R3 data register
9406 04A9 LDA rom_header ; Load ROM header byte for TX
9409 04AC INY ; Next byte in page
940A 04AD BNE send_rom_page_bytes ; Loop for all 256 bytes
940C 04AF INC tube_xfer_page ; Increment 24-bit destination addr
940E 04B1 BNE skip_addr_carry ; No carry: skip higher bytes
9410 04B3 INC tube_xfer_addr_2 ; Carry into second byte
9412 04B5 BNE skip_addr_carry ; No carry: skip third byte
9414 04B7 INC tube_xfer_addr_3 ; Carry into third byte
9416 04B9 .skip_addr_carry←2← 04B1 BNE← 04B5 BNE
INC zp_ptr_hi ; Increment page counter
9418 04BB BIT zp_ptr_hi ; Bit 6 set = all pages transferred
941A 04BD BVC next_rom_page ; More pages: loop back to SENDW
941C 04BF JSR tube_init_reloc ; Re-init reloc pointers for final claim
941F 04C2 LDA #4 ; A=4: transfer type for final address claim
9421 04C4 .tube_claim_default←1← 049D JSR
LDY #0 ; Y=0: transfer address low byte
9423 04C6 LDX #&53 ; X=&53: transfer address high byte (&0053)
9425 04C8 JMP tube_addr_claim ; Claim Tube address for transfer
; 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.
9428 04CB .tube_init_reloc←2← 0498 JSR← 04BF JSR
LDA #&80 ; Init: start sending from &8000
942A 04CD STA tube_xfer_page ; Store &80 as source page high byte
942C 04CF STA zp_ptr_hi ; Store &80 as page counter initial value
942E 04D1 LDA #&20 ; A=&20: bit 5 mask for ROM type check
9430 04D3 AND rom_type ; ROM type bit 5: reloc address in header?
9433 04D6 TAY ; Y = 0 or &20 (reloc flag)
9434 04D7 STY tube_transfer_addr ; Store as transfer address selector
9436 04D9 BEQ store_xfer_end_addr ; No reloc addr: use defaults
9438 04DB LDX copyright_offset ; Skip past copyright string to find reloc addr
943B 04DE .scan_copyright_end←1← 04E2 BNE
INX ; Skip past null-terminated copyright string
943C 04DF LDA rom_header,x ; Load next byte from ROM header
943F 04E2 BNE scan_copyright_end ; Loop until null terminator found
9441 04E4 LDA lang_entry_lo,x ; Read 4-byte reloc address from ROM header
9444 04E7 STA tube_transfer_addr ; Store reloc addr byte 1 as transfer addr
9446 04E9 LDA lang_entry_hi,x ; Load reloc addr byte 2
9449 04EC STA tube_xfer_page ; Store as source page start
944B 04EE LDY service_entry,x ; Load reloc addr byte 3
944E 04F1 LDA svc_entry_lo,x ; Load reloc addr byte 4 (highest)
9451 04F4 .store_xfer_end_addr←1← 04D9 BEQ
STA tube_xfer_addr_3 ; Store high byte of end address
9453 04F6 STY tube_xfer_addr_2 ; Store byte 3 of end address
9455 04F8 RTS ; Return with pointers initialised
; Tube host code page 5 — reference: NFS13 (TASKS, ; BPUT-FILE)
; Copied from ROM at &9456 during init. Contains:
; &0500: tube_dispatch_table — 12-entry handler address table
; &0518: R2 command byte table — 8 even command bytes (&00-&0E)
; &0520: tube_osbput — write byte to file
; &052D: tube_osbget — read byte from file
; &0537: tube_osrdch — read character
; &053A: tube_rdch_reply — ROR carry into byte, send via R2
; &053D: tube_release_return — dead code (unreferenced)
; &0542: tube_osfind — open file
; &0552: tube_osfind_close — close file (A=0)
; &055E: tube_osargs — file argument read/write
; &0582: tube_read_string — read CR-terminated string into &0700
; &0596: tube_oscli — execute * command
; &059C: tube_reply_ack — send &7F acknowledge
; &059E: tube_reply_byte — send byte and return to main loop
; &05A9: tube_osfile — whole file operation
9456 0500 .tube_dispatch_table←2← 0050 JMP← 8164 STA
EQUW tube_osrdch ; R2 cmd 0: OSRDCH
9458 0502 EQUW tube_oscli ; R2 cmd 1: OSCLI
945A 0504 EQUW tube_osbyte_2param ; R2 cmd 2: OSBYTE (2-param)
945C 0506 EQUW tube_osbyte_long ; R2 cmd 3: OSBYTE (3-param)
945E 0508 EQUW tube_osword ; R2 cmd 4: OSWORD
9460 050A EQUW tube_osword_rdln ; R2 cmd 5: OSWORD 0 (read line)
9462 050C EQUW tube_osargs ; R2 cmd 6: OSARGS
9464 050E EQUW tube_osbget ; R2 cmd 7: OSBGET
9466 0510 EQUW tube_osbput ; R2 cmd 8: OSBPUT
9468 0512 EQUW tube_osfind ; R2 cmd 9: OSFIND
946A 0514 EQUW tube_osfile ; R2 cmd 10: OSFILE
946C 0516 EQUW tube_osgbpb ; R2 cmd 11: OSGBPB
; Tube ULA control register values, indexed by transfer
; type (0-7). Written to &FEE0 after clearing V+M with
; &18. Bit layout: S=set/clear, T=reset regs, P=PRST,
; V=2-byte R3, M=PNMI(R3), J=PIRQ(R4), I=PIRQ(R1),
; Q=HIRQ(R4). Bits 1-7 select flags; bit 0 (S) is the
; value to set or clear.
946E 0518 .tube_ctrl_values←1← 0451 LDA
EQUB &86 ; Type 0: set I+J (1-byte R3, parasite to host)
946F 0519 EQUB &88 ; Type 1: set M (1-byte R3, host to parasite)
9470 051A EQUB &96 ; Type 2: set V+I+J (2-byte R3, parasite to host)
9471 051B EQUB &98 ; Type 3: set V+M (2-byte R3, host to parasite)
9472 051C EQUB &18 ; Type 4: clear V+M (execute code at address)
9473 051D EQUB &18 ; Type 5: clear V+M (release address claim)
9474 051E EQUB &82 ; Type 6: set I (define event handler)
9475 051F EQUB &18 ; Type 7: clear V+M (transfer and release)
; Tube OSBPUT handler (R2 cmd 8)
; Reads file handle and data byte from R2, then
; calls OSBPUT (&FFD4) to write the byte. Falls through
; to tube_reply_ack to send &7F acknowledgement.
9476 0520 .tube_osbput
JSR tube_read_r2 ; Read channel handle from R2
9479 0523 TAY ; Y=channel handle from R2
947A 0524 JSR tube_read_r2 ; Read data byte from R2 for BPUT
947D 0527 .tube_poll_r1_wrch
JSR osbput ; Write a single byte A to an open file Y
9480 052A JMP tube_reply_ack ; BPUT done: send acknowledge, return
; Tube OSBGET handler (R2 cmd 7)
; Reads file handle from R2, calls OSBGET (&FFD7)
; to read a byte, then falls through to tube_rdch_reply
; which encodes the carry flag (error) into bit 7 and
; sends the result byte via R2.
9483 052D .tube_osbget
JSR tube_read_r2 ; Read channel handle from R2
9486 0530 TAY ; Y=channel handle for OSBGET Y=file handle
9487 0531 JSR osbget ; Read a single byte from an open file Y
948A 0534 JMP tube_rdch_reply ; Send carry+byte reply (BGET result)
; Tube OSRDCH handler (R2 cmd 0)
; Calls OSRDCH (&FFE0) to read a character from
; the current input stream, then falls through to
; tube_rdch_reply which encodes the carry flag (error)
; into bit 7 and sends the result byte via R2.
948D 0537 .tube_osrdch
JSR osrdch ; Read a character from the current input stream
9490 053A .tube_rdch_reply←2← 0534 JMP← 05EF JMP
ROR ; ROR A: encode carry (error flag) into bit 7
9491 053B JSR tube_send_r2 ; Send carry+data byte to Tube R2
9494 053E ROL ; ROL A: restore carry flag
9495 053F JMP tube_reply_byte ; Return to Tube main loop
; Tube OSFIND handler (R2 cmd 9)
; Reads open mode from R2. If zero, reads a file
; handle and closes that file. Otherwise saves the mode,
; reads a filename string into &0700 via tube_read_string,
; then calls OSFIND (&FFCE) to open the file. Sends the
; resulting file handle (or &00) via tube_reply_byte.
9498 0542 .tube_osfind
JSR tube_read_r2 ; Read open mode from R2
949B 0545 BEQ tube_osfind_close ; A=0: close file, else open with filename
949D 0547 PHA ; Save open mode while reading filename
949E 0548 JSR tube_read_string ; Read filename string from R2 into &0700
94A1 054B PLA ; Recover open mode from stack
94A2 054C JSR osfind ; Open or close file(s)
94A5 054F JMP tube_reply_byte ; Send file handle result to co-processor
94A8 0552 .tube_osfind_close←1← 0545 BEQ
JSR tube_read_r2 ; OSFIND close: read handle from R2
94AB 0555 TAY ; Y=handle to close
94AC 0556 LDA #osfind_close ; A=0: close command for OSFIND
94AE 0558 JSR osfind ; Close one or all files
94B1 055B JMP tube_reply_ack ; Close done: send acknowledge, return
; Tube OSARGS handler (R2 cmd 6)
; Reads file handle from R2 into Y, then reads
; a 4-byte argument and reason code into zero page.
; Calls OSARGS (&FFDA), sends the result A and 4-byte
; return value via R2, then returns to the main loop.
94B4 055E .tube_osargs
JSR tube_read_r2 ; Read file handle from R2
94B7 0561 TAY ; Y=file handle for OSARGS
94B8 0562 .tube_read_params
LDX #4 ; Read 4-byte arg + reason from R2 into ZP
94BA 0564 .read_osargs_params←1← 056A BNE
JSR tube_read_r2 ; Read next param byte from R2
94BD 0567 STA escape_flag,x ; Params stored at &00-&03 (little-endian)
94BF 0569 DEX ; Decrement byte counter
94C0 056A BNE read_osargs_params ; Loop for 4 bytes
94C2 056C JSR tube_read_r2 ; Read OSARGS reason code from R2
94C5 056F JSR osargs ; Read or write a file's attributes
94C8 0572 JSR tube_send_r2 ; Send result A back to co-processor
94CB 0575 LDX #3 ; Return 4-byte result from ZP &00-&03
94CD 0577 .send_osargs_result←1← 057D BPL
LDA zp_ptr_lo,x ; Load result byte from zero page
94CF 0579 JSR tube_send_r2 ; Send byte to co-processor via R2
94D2 057C DEX ; Previous byte (count down)
94D3 057D BPL send_osargs_result ; Loop for all 4 bytes
94D5 057F JMP tube_main_loop ; Return to Tube main loop
; Read string from Tube R2 into buffer
; Loops reading bytes from tube_read_r2 into the
; string buffer at &0700, storing at string_buf+Y.
; Terminates on CR (&0D) or when Y wraps to zero
; (256-byte overflow). Returns with X=0, Y=7 so that
; XY = &0700, ready for OSCLI or OSFIND dispatch.
; Called by the Tube OSCLI and OSFIND handlers.
94D8 0582 .tube_read_string←3← 0548 JSR← 0596 JSR← 05B3 JSR
LDX #0 ; X=0: initialise string buffer index
94DA 0584 LDY #0 ; Y=0: string buffer offset 0
94DC 0586 .strnh←1← 0591 BNE
JSR tube_read_r2 ; Read next string byte from R2
94DF 0589 STA l0700,y ; Store byte in string buffer at &0700+Y
94E2 058C INY ; Next buffer position
94E3 058D BEQ string_buf_done ; Y overflow: string too long, truncate
94E5 058F CMP #&0d ; Check for CR terminator
94E7 0591 BNE strnh ; Not CR: continue reading string
94E9 0593 .string_buf_done←1← 058D BEQ
LDY #7 ; Y=7: set XY=&0700 for OSCLI/OSFIND
94EB 0595 RTS ; Return with XY pointing to &0700
; Tube OSCLI handler (R2 cmd 1)
; Reads a command string from R2 into &0700 via
; tube_read_string, then calls OSCLI (&FFF7) to execute
; it. Falls through to tube_reply_ack to send &7F
; acknowledgement.
94EC 0596 .tube_oscli
JSR tube_read_string ; Read command string from R2
94EF 0599 JSR oscli ; Execute * command via OSCLI
94F2 059C .tube_reply_ack←3← 0482 JMP← 052A JMP← 055B JMP
LDA #&7f ; &7F = success acknowledgement
94F4 059E .tube_reply_byte←4← 053F JMP← 054F JMP← 05A1 BVC← 067D JMP
BIT tube_status_register_2 ; Poll R2 status until ready
94F7 05A1 BVC tube_reply_byte ; Bit 6 clear: not ready, loop
94F9 05A3 STA tube_data_register_2 ; Write byte to R2 data register
94FC 05A6 .mj←1← 05CF BEQ
JMP tube_main_loop ; Return to Tube main loop
; Tube OSFILE handler (R2 cmd 10)
; Reads a 16-byte control block into zero page,
; a filename string into &0700 via tube_read_string,
; and a reason code from R2. Calls OSFILE (&FFDD),
; then sends the result A and updated 16-byte control
; block back via R2. Returns to the main loop via mj.
94FF 05A9 .tube_osfile
LDX #&10 ; X=&10: 16 bytes for OSFILE CB
9501 05AB .argsw←1← 05B1 BNE
JSR tube_read_r2 ; Read next control block byte from R2
9504 05AE STA zp_ptr_hi,x ; Store at &01+X (descending)
9506 05B0 DEX ; Decrement byte counter
9507 05B1 BNE argsw ; Loop for all 16 bytes
9509 05B3 JSR tube_read_string ; Read filename string from R2 into &0700
950C 05B6 STX zp_ptr_lo ; XY=&0700: filename pointer for OSFILE
950E 05B8 STY zp_ptr_hi ; Store Y=7 as pointer high byte
9510 05BA LDY #0 ; Y=0 for OSFILE control block offset
9512 05BC JSR tube_read_r2 ; Read OSFILE reason code from R2
9515 05BF JSR osfile ; Execute OSFILE operation
9518 05C2 JSR tube_send_r2 ; Send result A (object type) to co-processor
951B 05C5 LDX #&10 ; Return 16-byte control block to co-processor
951D 05C7 .send_osfile_ctrl_blk←1← 05CD BNE
LDA zp_ptr_hi,x ; Load control block byte
951F 05C9 JSR tube_send_r2 ; Send byte to co-processor via R2
9522 05CC DEX ; Decrement byte counter
9523 05CD BNE send_osfile_ctrl_blk ; Loop for all 16 bytes
9525 05CF BEQ mj ; ALWAYS branch to main loop ALWAYS branch
; Tube OSGBPB handler (R2 cmd 11)
; Reads a 13-byte control block and reason code
; from R2 into zero page. Calls OSGBPB (&FFD1), then
; sends 12 result bytes and the carry+result byte
; (via tube_rdch_reply) back via R2.
9527 05D1 .tube_osgbpb
LDX #&0d ; Read 13-byte OSGBPB control block from R2
9529 05D3 .read_osgbpb_ctrl_blk←1← 05D9 BNE
JSR tube_read_r2 ; Read next control block byte from R2
952C 05D6 STA escape_flag,x ; Store at &FF+X (descending into &00-&0C)
952E 05D8 DEX ; Decrement byte counter
952F 05D9 BNE read_osgbpb_ctrl_blk ; Loop for all 13 bytes
9531 05DB JSR tube_read_r2 ; Read OSGBPB reason code from R2
9534 05DE LDY #0 ; Y=0 for OSGBPB control block
9536 05E0 JSR osgbpb ; Read or write multiple bytes to an open file
9539 05E3 PHA ; Save A (completion status) for later
953A 05E4 LDX #&0c ; Return 13-byte result block to co-processor
953C 05E6 .send_osgbpb_result←1← 05EC BPL
LDA zp_ptr_lo,x ; Load result byte from zero page
953E 05E8 JSR tube_send_r2 ; Send byte to co-processor via R2
9541 05EB DEX ; Decrement byte counter
9542 05EC BPL send_osgbpb_result ; Loop for 13 bytes (X=12..0)
9544 05EE PLA ; Recover completion status from stack
9545 05EF JMP tube_rdch_reply ; Send carry+status as RDCH-style reply
; Tube OSBYTE 2-param handler (R2 cmd 2)
; Reads X and A from R2, calls OSBYTE (&FFF4)
; with Y=0, then sends the result X via
; tube_reply_byte. Used for OSBYTE calls that take
; only A and X parameters.
9548 05F2 .tube_osbyte_2param
JSR tube_read_r2 ; Read OSWORD number from R2
954B 05F5 TAX ; X = first parameter
954C 05F6 JSR tube_read_r2 ; Read A (OSBYTE number) from R2
954F 05F9 JSR osbyte ; Execute OSBYTE call
9552 05FC .tube_poll_r2_result←1← 05FF ref
BIT tube_status_register_2 ; Poll R2 status for result send
9555 05FF EQUB &50
9556 0600 .tube_page6_start←1← 816A STA
EQUB &FB
9557 0601 STX tube_data_register_2 ; Send X result for 2-param OSBYTE
955A 0604 .bytex←1← 0617 BEQ
JMP tube_main_loop ; Return to main event loop
; Tube OSBYTE 3-param handler (R2 cmd 3)
; Reads X, Y, and A from R2, calls OSBYTE
; (&FFF4), then sends carry+Y and X as result bytes
; via R2. Used for OSBYTE calls needing all three
; parameters and returning both X and Y results.
955D 0607 .tube_osbyte_long
JSR tube_read_r2 ; Read X parameter from co-processor
9560 060A TAX ; Save in X
9561 060B JSR tube_read_r2 ; Read Y parameter from co-processor
9564 060E TAY ; Save in Y
9565 060F JSR tube_read_r2 ; Read A (OSBYTE function code)
9568 0612 JSR osbyte ; Execute OSBYTE A,X,Y
956B 0615 EOR #&9d ; Test for OSBYTE &9D (fast Tube BPUT)
956D 0617 BEQ bytex ; OSBYTE &9D (fast Tube BPUT): no result needed
956F 0619 ROR ; Encode carry (error flag) into bit 7
9570 061A JSR tube_send_r2 ; Send carry+status byte via R2
9573 061D .tube_osbyte_send_y←1← 0620 BVC
BIT tube_status_register_2 ; Poll R2 status for ready
9576 0620 BVC tube_osbyte_send_y ; Not ready: keep polling
9578 0622 STY tube_data_register_2 ; Send Y result, then fall through to send X
957B 0625 EQUB &70
; Tube OSWORD handler (R2 cmd 4)
; Reads OSWORD number A and in-length from R2,
; then reads the parameter block into &0128. Calls
; OSWORD (&FFF1), then sends the out-length result
; bytes from the parameter block back via R2.
; Returns to the main loop via tube_return_main.
957C 0626 .tube_osbyte_short
CMP tube_send_zero_r2,x ; Send X result via R2 (data overlap with &70=BVS)
957E 0628 CMP l0006 ; Read OSWORD number from R2
9580 062A TAY ; Save OSWORD number in Y Save OSWORD number in Y
9581 062B .tube_osword_read←1← 062E BPL
BIT tube_status_register_2 ; Poll R2 status for data ready
9584 062E BPL tube_osword_read ; Not ready: keep polling
9586 0630 .tube_osbyte_send_x
LDX tube_data_register_2 ; Read param block length from R2
9589 0633 DEX ; DEX: length 0 means no params to read
958A 0634 BMI skip_param_read ; No params (length=0): skip read loop
958C 0636 .tube_osword_read_lp←2← 0639 BPL← 0642 BPL
BIT tube_status_register_2 ; Poll R2 status for data ready
958F 0639 BPL tube_osword_read_lp ; Not ready: keep polling
9591 063B LDA tube_data_register_2 ; Read param byte from R2
9594 063E STA l0128,x ; Store param bytes into block at &0128
9597 0641 DEX ; Next param byte (descending)
9598 0642 BPL tube_osword_read_lp ; Loop until all params read
959A 0644 TYA ; Restore OSWORD number from Y
959B 0645 .skip_param_read←1← 0634 BMI
LDX #<(l0128) ; XY=&0128: param block address for OSWORD
959D 0647 LDY #>(l0128) ; Y=&01: param block at &0128
959F 0649 JSR osword ; Execute OSWORD with XY=&0128
95A2 064C .poll_r2_osword_result←1← 064F BPL
BIT tube_status_register_2 ; Poll R2 status for ready
95A5 064F BPL poll_r2_osword_result ; Not ready: keep polling
95A7 0651 LDX tube_data_register_2 ; Read result block length from R2
95AA 0654 DEX ; Decrement result byte counter
95AB 0655 BMI tube_return_main ; No results to send: return to main loop
95AD 0657 .tube_osword_write←1← 0663 BPL
LDY l0128,x ; Send result block bytes from &0128 via R2
95B0 065A .tube_osword_write_lp←1← 065D BVC
BIT tube_status_register_2 ; Poll R2 status for ready
95B3 065D BVC tube_osword_write_lp ; Not ready: keep polling
95B5 065F STY tube_data_register_2 ; Send result byte via R2
95B8 0662 DEX ; Next result byte (descending)
95B9 0663 BPL tube_osword_write ; Loop until all results sent
95BB 0665 .tube_return_main←1← 0655 BMI
JMP tube_main_loop ; Return to main event loop
; Tube OSWORD 0 handler (R2 cmd 5)
; Handles OSWORD 0 (read line) specially. Reads
; 4 parameter bytes from R2 into &0128 (max length,
; min char, max char, flags). Calls OSWORD 0 (&FFF1)
; to read a line, then sends &7F+CR or the input line
; byte-by-byte via R2, followed by &80 (error/escape)
; or &7F (success).
95BE 0668 .tube_osword_rdln
LDX #4 ; X=4: read 5 control block bytes
95C0 066A .read_rdln_ctrl_block←1← 0670 BPL
JSR tube_read_r2 ; Read control block byte from R2
95C3 066D STA zp_ptr_lo,x ; Store in zero page params
95C5 066F DEX ; Next byte (descending)
95C6 0670 BPL read_rdln_ctrl_block ; Loop until all 5 bytes read
95C8 0672 INX ; X=0 after loop, A=0 for OSWORD 0 (read line)
95C9 0673 LDY #0 ; Y=0 for OSWORD 0
95CB 0675 TXA ; A=0: OSWORD 0 (read line)
95CC 0676 JSR osword ; Read input line from keyboard
95CF 0679 BCC tube_rdln_send_line ; C=0: line read OK; C=1: escape pressed
95D1 067B LDA #&ff ; &FF = escape/error signal to co-processor
95D3 067D JMP tube_reply_byte ; Escape: send &FF error to co-processor
95D6 0680 .tube_rdln_send_line←1← 0679 BCC
LDX #0 ; X=0: start of input buffer at &0700
95D8 0682 LDA #&7f ; &7F = line read successfully
95DA 0684 JSR tube_send_r2 ; Send &7F (success) to co-processor
95DD 0687 .tube_rdln_send_loop←1← 0690 BNE
LDA l0700,x ; Load char from input buffer
95E0 068A .tube_rdln_send_byte
JSR tube_send_r2 ; Send char to co-processor
95E3 068D INX ; Next character
95E4 068E CMP #&0d ; Check for CR terminator
95E6 0690 BNE tube_rdln_send_loop ; Loop until CR terminator sent
95E8 0692 JMP tube_main_loop ; Return to main event loop
; Send byte to Tube data register R2
; Polls Tube status register 2 until bit 6 (TDRA)
; is set, then writes A to the data register. Uses a
; tight BIT/BVC polling loop. Called by 12 sites
; across the Tube host code for all R2 data
; transmission: command responses, file data, OSBYTE
; results, and control block bytes.
95EB 0695 .tube_send_r2←14← 0020 JSR← 0026 JSR← 002C JSR← 0467 JSR← 053B JSR← 0572 JSR← 0579 JSR← 05C2 JSR← 05C9 JSR← 05E8 JSR← 061A JSR← 0684 JSR← 068A JSR← 0698 BVC
BIT tube_status_register_2 ; Poll R2 status (bit 6 = ready)
95EE 0698 BVC tube_send_r2 ; Not ready: keep polling
95F0 069A STA tube_data_register_2 ; Write A to Tube R2 data register
95F3 069D RTS ; Return to caller
; Send byte to Tube data register R4
; Polls Tube status register 4 until bit 6 is set,
; then writes A to the data register. Uses a tight
; BIT/BVC polling loop. R4 is the command/control
; channel used for address claims (ADRR), data transfer
; setup (SENDW), and release commands. Called by 7
; sites, primarily during tube_release_claim and
; tube_transfer_setup sequences.
95F4 069E .tube_send_r4←8← 0018 JSR← 0416 JSR← 041B JSR← 0436 JSR← 043E JSR← 0443 JSR← 0449 JSR← 06A1 BVC
BIT tube_status_register_4_and_cpu_control ; Poll R4 status (bit 6 = ready)
95F7 06A1 BVC tube_send_r4 ; Not ready: keep polling
95F9 06A3 STA tube_data_register_4 ; Write A to Tube R4 data register
95FC 06A6 RTS ; Return to caller
95FD 06A7 .tube_escape_check←1← 0403 JMP
LDA escape_flag ; Check OS escape flag at &FF
95FF 06A9 SEC ; SEC+ROR: put bit 7 of &FF into carry+bit 7
9600 06AA ROR ; ROR: shift escape bit 7 to carry
9601 06AB BMI tube_send_r1 ; Escape set: forward to co-processor via R1
9603 06AD .tube_event_handler
PHA ; EVNTV: forward event A, Y, X to co-processor
9604 06AE LDA #0 ; Send &00 prefix (event notification)
9606 06B0 JSR tube_send_r1 ; Send zero prefix via R1
9609 06B3 TYA ; Y value for event
960A 06B4 JSR tube_send_r1 ; Send Y via R1
960D 06B7 TXA ; X value for event
960E 06B8 JSR tube_send_r1 ; Send X via R1
9611 06BB PLA ; Restore A (event type)
; Send byte to Tube data register R1
; Polls Tube status register 1 until bit 6 is set,
; then writes A to the data register. Uses a tight
; BIT/BVC polling loop. R1 is used for asynchronous
; event and escape notification to the co-processor.
; Called by tube_event_handler to forward event type,
; Y, and X parameters, and reached via BMI from
; tube_escape_check when the escape flag is set.
9612 06BC .tube_send_r1←5← 06AB BMI← 06B0 JSR← 06B4 JSR← 06B8 JSR← 06BF BVC
BIT tube_status_1_and_tube_control ; Poll R1 status (bit 6 = ready)
9615 06BF BVC tube_send_r1 ; Not ready: keep polling
9617 06C1 STA tube_data_register_1 ; Write A to Tube R1 data register
961A 06C4 RTS ; Return to caller
; Read a byte from Tube data register R2
; Polls Tube status register 2 until data is available
; (bit 7 set), then loads A from Tube data register 2.
; Called by all Tube dispatch handlers that receive data
; or parameters from the co-processor.
961B 06C5 .tube_read_r2←20← 0520 JSR← 0524 JSR← 052D JSR← 0542 JSR← 0552 JSR← 055E JSR← 0564 JSR← 056C JSR← 0586 JSR← 05AB JSR← 05BC JSR← 05D3 JSR← 05DB JSR← 05F2 JSR← 05F6 JSR← 0607 JSR← 060B JSR← 060F JSR← 066A JSR← 06C8 BPL
BIT tube_status_register_2 ; Poll R2 status (bit 7 = data ready)
961E 06C8 BPL tube_read_r2 ; Not ready: keep polling
9620 06CA LDA tube_data_register_2 ; Read byte from Tube R2 data register
9623 06CD RTS ; Return with byte in A
9624 06CE EQUS "Jes"
9627 06D1 EQUB &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF
9656 EQUB &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF
9660 .trampoline_tx_setup←2← 86AA JSR← 8ED1 JMP
JMP tx_begin ; Trampoline: forward to tx_begin
9663 .trampoline_adlc_init←1← 8319 JSR
JMP adlc_init ; Trampoline: forward to adlc_init
9666 .svc_12_nmi_release
JMP poll_nmi_ready ; Trampoline: forward to NMI release
9669 .svc_11_nmi_claim
JMP init_nmi_workspace ; 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 STX econet_init_flag ; Clear Econet init flag before setup
967C LDY #&ff ; Y=&FF for OSBYTE
967E JSR osbyte ; Read Tube present flag
9681 STX tube_flag ; X=value of Tube present flag
9684 LDA #osbyte_issue_service_request ; OSBYTE &8F: issue service request
9686 LDX #&0c ; X=&0C: NMI claim service
9688 LDY #&ff ; Y=&FF: pass to adlc_init_workspace
fall through ↓

Initialise NMI workspace

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 (&9FA8) 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).

968A .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.

968D .init_nmi_workspace←1← 9669 JMP
LDY #&20 ; Copy 32 bytes of NMI shim from ROM to &0D00
968F .copy_nmi_shim←1← 9696 BNE
LDA nmi_shim_rom_src,y ; Read byte from NMI shim ROM source
9692 STA l0cff,y ; Write to NMI shim RAM at &0D00
9695 DEY ; Next byte (descending)
9696 BNE copy_nmi_shim ; Loop until all 32 bytes copied
9698 LDA romsel_copy ; Patch current ROM bank into NMI shim
969A STA nmi_shim_07 ; Self-modifying code: ROM bank at &0D07
969D LDA #&80 ; &80 = Econet initialised
969F STA tx_clear_flag ; Mark TX as complete (ready)
96A2 STA econet_init_flag ; Mark Econet as initialised
96A5 LDA station_id_disable_net_nmis ; Read station ID (&FE18 = INTOFF side effect)
96A8 STA tx_src_stn ; Store our station ID in TX scout
96AB STY tx_src_net ; Y=0 after copy loop: net = local
96AE BIT video_ula_control ; BIT VULA: enable NMIs via latch
96B1 RTS ; Return from NMI workspace init
96B2 .poll_nmi_ready←1← 9666 JMP
BIT econet_init_flag ; Test Econet init complete flag
96B5 BPL enter_rx_listen ; Init done: enter RX listen mode
96B7 .nmi_vec_lo_match←2← 96BC BNE← 96C3 BNE
LDA nmi_jmp_lo ; Load NMI vector low byte
96BA CMP #&f2 ; Check if low byte is expected value
96BC BNE nmi_vec_lo_match ; Mismatch: keep polling
96BE LDA nmi_jmp_hi ; Load NMI vector high byte
96C1 CMP #&96 ; Check if high byte is &96
96C3 BNE nmi_vec_lo_match ; Mismatch: keep polling
96C5 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.

96C8 .save_econet_state
BIT station_id_disable_net_nmis ; INTOFF: disable NMIs
96CB LDA #0 ; A=0: clear TX and init flags
96CD STA tx_clear_flag ; Clear TX semaphore (allow new TX)
96D0 STA econet_init_flag ; Clear Econet init flag
96D3 LDY #5 ; Y=5: status flags offset
96D5 .enter_rx_listen←1← 96B5 BPL
JMP adlc_rx_listen ; Re-enter idle RX listen mode

ADLC full reset

Aborts all activity and returns to idle RX listen mode.

96D8 .adlc_full_reset←3← 9672 JSR← 973A JSR← 987A JSR
LDA #&c1 ; CR1=&C1: TX_RESET | RX_RESET | AC (both sections in reset, address control set)
96DA STA econet_control1_or_status1 ; Write CR1: full reset
96DD LDA #&1e ; CR4=&1E: 8-bit word, abort ext, NRZ
96DF STA econet_data_terminate_frame ; Write CR4 via ADLC reg 3 (AC=1)
96E2 LDA #0 ; CR3=&00 (via AC=1): no loop-back, no AEX, NRZ, no DTR
96E4 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.

96E7 .adlc_rx_listen←2← 96D5 JMP← 9A3D JSR
LDA #&82 ; CR1=&82: TX_RESET | RIE (TX in reset, RX interrupts enabled)
96E9 STA econet_control1_or_status1 ; Write CR1: RIE | TX_RESET
96EC LDA #&67 ; CR2=&67: CLR_TX_ST | CLR_RX_ST | FC_TDRA | 2_1_BYTE | PSE
96EE STA econet_control23_or_status2 ; Write CR2: listen mode config
96F1 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).

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

9711 .nmi_rx_scout_net
BIT econet_control23_or_status2 ; BIT SR2: test for RDA (bit7 = data available)
9714 BPL scout_error ; No RDA -- check errors
9716 LDA econet_data_continue_frame ; Read destination network byte
9719 BEQ accept_local_net ; Network = 0 -- local network, accept
971B EOR #&ff ; EOR &FF: test if network = &FF (broadcast)
971D BEQ accept_scout_net ; Broadcast network -- accept
971F .scout_reject←1← 9703 BNE
LDA #&a2 ; Reject: wrong network. CR1=&A2: RIE|RX_DISCONTINUE
9721 STA econet_control1_or_status1 ; Write CR1 to discontinue RX
9724 JMP install_rx_scout_handler ; Return to idle scout listening
9727 .accept_local_net←1← 9719 BEQ
STA tx_flags ; Network = 0 (local): clear tx_flags
972A .accept_scout_net←1← 971D BEQ
STA port_buf_len ; Store Y offset for scout data buffer
972C LDA #&43 ; Install scout data reading loop at &970E
972E LDY #&97 ; High byte of scout data handler
9730 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 &9748) 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.

9733 .scout_error←5← 96F7 BEQ← 9714 BPL← 9748 BPL← 977C BEQ← 977E BPL
LDA econet_control23_or_status2 ; Read SR2
9736 AND #&81 ; Test AP (b0) | RDA (b7)
9738 BEQ scout_discard ; Neither set -- clean end, discard via &9740
973A JSR adlc_full_reset ; Unexpected data/status: full ADLC reset
973D JMP install_rx_scout_handler ; Discard and return to idle
9740 .scout_discard←1← 9738 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 (&9745) - No RDA (BPL) -> error (&9733) - RDA set (BMI) -> read byte - After first byte (&9751): full SR2 tested - SR2 non-zero (BNE) -> scout completion (&976D) 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 (&9765): re-test SR2 for next pair - RDA set (BMI) -> loop back to &974A - Neither set -> RTI, wait for next NMI The loop ends at Y=&0C (12 bytes max in scout buffer).

9743 .scout_data_loop
LDY port_buf_len ; Y = buffer offset
9745 LDA econet_control23_or_status2 ; Read SR2
9748 .scout_loop_rda←1← 9768 BNE
BPL scout_error ; No RDA -- error handler &9733
974A LDA econet_data_continue_frame ; Read data byte from RX FIFO
974D STA rx_src_stn,y ; Store at &0D3D+Y (scout buffer)
9750 INY ; Advance buffer index
9751 LDA econet_control23_or_status2 ; Read SR2 again (FV detection point)
9754 BMI scout_loop_second ; RDA set -- more data, read second byte
9756 BNE scout_complete ; SR2 non-zero (FV or other) -- scout completion
9758 .scout_loop_second←1← 9754 BMI
LDA econet_data_continue_frame ; Read second byte of pair
975B STA rx_src_stn,y ; Store at &0D3D+Y
975E INY ; Advance and check buffer limit
975F CPY #&0c ; Copied all 12 scout bytes?
9761 BEQ scout_complete ; Buffer full (Y=12) -- force completion
9763 STY port_buf_len ; Save final buffer offset
9765 LDA econet_control23_or_status2 ; Read SR2 for next pair
9768 BNE scout_loop_rda ; SR2 non-zero -- loop back for more bytes
976A 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 &9733 (not a valid frame end) - FV set, no RDA (BPL) -> error &9733 (missing last byte) - FV set, RDA set -> read last byte, process scout After reading the last byte, the complete scout buffer (&0D3D-&0D48) contains: src_stn, src_net, ctrl, port [, extra_data...]. The port byte at &0D40 determines further processing: - Port = 0 -> immediate operation (&9A6F) - Port non-zero -> check if it matches an open receive block

976D .scout_complete←2← 9756 BNE← 9761 BEQ
LDA #0 ; CR1=&00: disable all interrupts
976F STA econet_control1_or_status1 ; Write CR1
9772 LDA #&84 ; CR2=&84: disable PSE, enable RDA_SUPPRESS_FV
9774 STA econet_control23_or_status2 ; Write CR2
9777 LDA #2 ; A=&02: FV mask for SR2 bit1
9779 BIT econet_control23_or_status2 ; BIT SR2: test FV (Z) and RDA (N)
977C BEQ scout_error ; No FV -- not a valid frame end, error
977E BPL scout_error ; FV set but no RDA -- missing last byte, error
9780 LDA econet_data_continue_frame ; Read last byte from RX FIFO
9783 STA rx_src_stn,y ; Store last byte at &0D3D+Y
9786 LDA #&44 ; CR1=&44: RX_RESET | TIE (switch to TX for ACK)
9788 STA econet_control1_or_status1 ; Write CR1: switch to TX mode
978B LDA rx_port ; Check port byte: 0 = immediate op, non-zero = data transfer
978E BNE scout_match_port ; Port non-zero -- look for matching receive block
9790 JMP immediate_op ; Port = 0 -- immediate operation handler
9793 .scout_no_match←3← 97DE BNE← 97E3 BVC← 9801 JMP
JMP nmi_error_dispatch ; Port = 0 -- immediate operation handler
9796 .scout_match_port←1← 978E BNE
BIT tx_flags ; Check if broadcast (bit6 of tx_flags)
9799 BVC scan_port_list ; Not broadcast -- skip CR2 setup
979B LDA #7 ; CR2=&07: broadcast prep
979D STA econet_control23_or_status2 ; Write CR2: broadcast frame prep
97A0 .scan_port_list←1← 9799 BVC
BIT rx_flags ; Check if RX port list active (bit7)
97A3 BPL scout_network_match ; No active ports -- try NFS workspace
97A5 LDA #&c0 ; Start scanning port list at page &C0
97A7 .scan_nfs_port_list
STA port_ws_offset ; Store page to workspace pointer low
97A9 LDA #0 ; A=0: no NFS workspace offset yet
97AB STA rx_buf_offset ; Clear NFS workspace search flag
97AD .check_port_slot←1← 97DA BCC
LDY #0 ; Y=0: read control byte from start of slot
97AF .scout_ctrl_check←1← 97ED BEQ
LDA (port_ws_offset),y ; Read port control byte from slot
97B1 BEQ scout_station_check ; Zero = end of port list, no match
97B3 CMP #&7f ; &7F = any-port wildcard
97B5 BNE next_port_slot ; Not wildcard -- check specific port match
97B7 INY ; Y=1: advance to port byte in slot
97B8 LDA (port_ws_offset),y ; Read port number from slot (offset 1)
97BA BEQ check_station_filter ; Zero port in slot = match any port
97BC CMP rx_port ; Check if port matches this slot
97BF BNE next_port_slot ; Port mismatch -- try next slot
97C1 .check_station_filter←1← 97BA BEQ
INY ; Y=2: advance to station byte
97C2 LDA (port_ws_offset),y ; Read station filter from slot (offset 2)
97C4 BEQ scout_port_match ; Zero station = match any station, accept
97C6 CMP rx_src_stn ; Check if source station matches
97C9 BNE next_port_slot ; Station mismatch -- try next slot
97CB .scout_port_match←1← 97C4 BEQ
INY ; Y=3: advance to network byte
97CC LDA (port_ws_offset),y ; Read network filter from slot (offset 3)
97CE CMP rx_src_net ; Check if source network matches
97D1 BEQ scout_accept ; Network matches or zero = accept
97D3 .next_port_slot←3← 97B5 BNE← 97BF BNE← 97C9 BNE
LDA port_ws_offset ; Check if NFS workspace search pending
97D5 CLC ; CLC for 12-byte slot advance
97D6 ADC #&0c ; Advance to next 12-byte port slot
97D8 STA port_ws_offset ; Update workspace pointer to next slot
97DA BCC check_port_slot ; Always branches (page &C0 won't overflow)
97DC .scout_station_check←1← 97B1 BEQ
LDA rx_buf_offset ; Check if NFS workspace already searched
97DE BNE scout_no_match ; Already searched: no match found
97E0 .scout_network_match←1← 97A3 BPL
BIT rx_flags ; Try NFS workspace if paged list exhausted
97E3 BVC scout_no_match ; No NFS workspace RX (bit6 clear) -- discard
97E5 LDA nfs_workspace_hi ; Get NFS workspace page number
97E7 STA rx_buf_offset ; Mark NFS workspace as search target
97E9 LDY #0 ; Y=0: start at offset 0 in workspace
97EB STY port_ws_offset ; Reset slot pointer to start
97ED BEQ scout_ctrl_check ; ALWAYS branch
97EF .scout_accept←1← 97D1 BEQ
BIT tx_flags ; Check broadcast flag (bit 6)
97F2 BVC ack_scout_match ; Not broadcast: ACK and set up RX
97F4 JMP copy_scout_fields ; Broadcast: copy scout fields directly
97F7 .ack_scout_match←2← 97F2 BVC← 9ABB JMP
LDA #3 ; Match found: set scout_status = 3
97F9 STA scout_status ; Record match for completion handler
97FC JSR tx_calc_transfer ; Calculate transfer parameters
97FF BCS send_data_rx_ack ; Transfer OK: send data ACK
9801 JMP scout_no_match ; Broadcast: different completion path
9804 .send_data_rx_ack←2← 97FF BCS← 9AB0 JMP
LDA #&44 ; CR1=&44: RX_RESET | TIE
9806 STA econet_control1_or_status1 ; Write CR1: TX mode for ACK
9809 LDA #&a7 ; CR2=&A7: RTS | CLR_TX_ST | FC_TDRA | PSE
980B STA econet_control23_or_status2 ; Write CR2: enable TX with PSE
980E LDA #&15 ; Install data_rx_setup at &97DC
9810 LDY #&98 ; High byte of data_rx_setup handler
9812 JMP ack_tx_write_dest ; Send ACK with data_rx_setup as next NMI
9815 .data_rx_setup
LDA #&82 ; CR1=&82: TX_RESET | RIE (switch to RX for data frame)
9817 STA econet_control1_or_status1 ; Write CR1: switch to RX for data frame
981A LDA #&21 ; Install nmi_data_rx at &9821
981C LDY #&98 ; High byte of nmi_data_rx handler
981E 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: &9821 (AP+addr check) -> &9837 (net=0 check) -> &984D (skip ctrl+port) -> &9880 (bulk data read) -> &98B4 (completion)

9821 .nmi_data_rx
LDA #1 ; A=&01: mask for AP (Address Present)
9823 BIT econet_control23_or_status2 ; BIT SR2: test AP bit
9826 BEQ nmi_error_dispatch ; No AP: wrong frame or error
9828 LDA econet_data_continue_frame ; Read first byte (dest station)
982B CMP station_id_disable_net_nmis ; Compare to our station ID (INTOFF)
982E BNE nmi_error_dispatch ; Not for us: error path
9830 LDA #&37 ; Install net check handler at &9837
9832 LDY #&98 ; High byte of nmi_data_rx handler
9834 JMP set_nmi_vector ; Set NMI vector via RAM shim
9837 .nmi_data_rx_net
BIT econet_control23_or_status2 ; Validate source network = 0
983A BPL nmi_error_dispatch ; SR2 bit7 clear: no data ready -- error
983C LDA econet_data_continue_frame ; Read dest network byte
983F BNE nmi_error_dispatch ; Network != 0: wrong network -- error
9841 LDA #&4d ; Install skip handler at &9810
9843 LDY #&98 ; High byte of &9810 handler
9845 BIT econet_control1_or_status1 ; SR1 bit7: IRQ, data already waiting
9848 BMI nmi_data_rx_skip ; Data ready: skip directly, no RTI
984A JMP set_nmi_vector ; Install handler and return via RTI
984D .nmi_data_rx_skip←1← 9848 BMI
BIT econet_control23_or_status2 ; Skip control and port bytes (already known from scout)
9850 BPL nmi_error_dispatch ; SR2 bit7 clear: error
9852 LDA econet_data_continue_frame ; Discard control byte
9855 LDA econet_data_continue_frame ; Discard port byte
fall through ↓

Install data RX bulk or Tube handler

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

9858 .install_data_rx_handler←1← 9F0C JMP
LDA #2 ; A=2: Tube transfer flag mask
985A BIT tx_flags ; Check if Tube transfer active
985D BNE install_tube_rx ; Tube active: use Tube RX path
985F LDA #&80 ; Install bulk read at &9843
9861 LDY #&98 ; High byte of &9843 handler
9863 BIT econet_control1_or_status1 ; SR1 bit7: more data already waiting?
9866 BMI nmi_data_rx_bulk ; Yes: enter bulk read directly
9868 JMP set_nmi_vector ; No: install handler and RTI
986B .install_tube_rx←1← 985D BNE
LDA #&dd ; Tube: install Tube RX at &98DD
986D LDY #&98 ; High byte of &98DD handler
986F 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).

9872 .nmi_error_dispatch←12← 9793 JMP← 9826 BEQ← 982E BNE← 983A BPL← 983F BNE← 9850 BPL← 9893 BEQ← 98C5 BEQ← 98CB BEQ← 9916 JMP← 99A1 JMP← 9A82 JMP
LDA tx_flags ; Check tx_flags for error path
9875 BPL rx_error ; Bit7 clear: RX error path
9877 JMP tx_result_fail ; Bit7 set: TX result = not listening
987A .rx_error←1← 9875 BPL
.rx_error_reset←1← 9875 BPL
JSR adlc_full_reset ; Full ADLC reset on RX error
987D 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 &98B4. SR2 = 0 -> RTI, wait for next NMI to continue.

9880 .nmi_data_rx_bulk←1← 9866 BMI
LDY port_buf_len ; Y = buffer offset, resume from last position
9882 LDA econet_control23_or_status2 ; Read SR2 for next pair
9885 .data_rx_loop←1← 98AF BNE
BPL data_rx_complete ; SR2 bit7 clear: frame complete (FV)
9887 LDA econet_data_continue_frame ; Read first byte of pair from RX FIFO
988A STA (open_port_buf),y ; Store byte to buffer
988C INY ; Advance buffer offset
988D BNE read_sr2_between_pairs ; Y != 0: no page boundary crossing
988F INC open_port_buf_hi ; Crossed page: increment buffer high byte
9891 DEC port_buf_len_hi ; Decrement remaining page count
9893 BEQ nmi_error_dispatch ; No pages left: handle as complete
9895 .read_sr2_between_pairs←1← 988D BNE
LDA econet_control23_or_status2 ; Read SR2 between byte pairs
9898 BMI read_second_rx_byte ; SR2 bit7 set: more data available
989A BNE data_rx_complete ; SR2 non-zero, bit7 clear: frame done
989C .read_second_rx_byte←1← 9898 BMI
LDA econet_data_continue_frame ; Read second byte of pair from RX FIFO
989F STA (open_port_buf),y ; Store byte to buffer
98A1 INY ; Advance buffer offset
98A2 STY port_buf_len ; Save updated buffer position
98A4 BNE check_sr2_loop_again ; Y != 0: no page boundary crossing
98A6 INC open_port_buf_hi ; Crossed page: increment buffer high byte
98A8 DEC port_buf_len_hi ; Decrement remaining page count
98AA BEQ data_rx_complete ; No pages left: frame complete
98AC .check_sr2_loop_again←1← 98A4 BNE
LDA econet_control23_or_status2 ; Read SR2 for next iteration
98AF BNE data_rx_loop ; SR2 non-zero: more data, loop back
98B1 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 (&976D): 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 &9944.

98B4 .data_rx_complete←3← 9885 BPL← 989A BNE← 98AA BEQ
LDA #0 ; CR1=&00: disable all interrupts
98B6 STA econet_control1_or_status1 ; Write CR1
98B9 LDA #&84 ; CR2=&84: disable PSE for individual bit testing
98BB STA econet_control23_or_status2 ; Write CR2
98BE STY port_buf_len ; Save Y (byte count from data RX loop)
98C0 LDA #2 ; A=&02: FV mask
98C2 BIT econet_control23_or_status2 ; BIT SR2: test FV (Z) and RDA (N)
98C5 BEQ nmi_error_dispatch ; No FV -- error
98C7 BPL send_ack ; FV set, no RDA -- proceed to ACK
98C9 LDA port_buf_len_hi ; Check if buffer space remains
98CB .read_last_rx_byte
BEQ nmi_error_dispatch ; No buffer space: error/discard frame
98CD LDA econet_data_continue_frame ; FV+RDA: read and store last data byte
98D0 LDY port_buf_len ; Y = current buffer write offset
98D2 STA (open_port_buf),y ; Store last byte in port receive buffer
98D4 INC port_buf_len ; Advance buffer write offset
98D6 BNE send_ack ; No page wrap: proceed to send ACK
98D8 INC open_port_buf_hi ; Page boundary: advance buffer page
98DA .send_ack←2← 98C7 BPL← 98D6 BNE
JMP ack_tx ; Send ACK frame to complete handshake
98DD .nmi_data_rx_tube
LDA econet_control23_or_status2 ; Read SR2 for Tube data receive path
98E0 .rx_tube_data←1← 9911 BNE
BPL data_rx_tube_complete ; RDA clear: no more data, frame complete
98E2 LDA econet_data_continue_frame ; Read data byte from ADLC RX FIFO
98E5 INC port_buf_len ; Advance Tube transfer byte count
98E7 STA tube_data_register_3 ; Send byte to Tube data register 3
98EA BNE rx_update_buf ; No overflow: read second byte
98EC INC port_buf_len_hi ; Carry to transfer count byte 2
98EE BNE rx_update_buf ; No overflow: read second byte
98F0 INC open_port_buf ; Carry to transfer count byte 3
98F2 BNE rx_update_buf ; No overflow: read second byte
98F4 INC open_port_buf_hi ; Carry to transfer count byte 4
98F6 BEQ data_rx_tube_error ; All bytes zero: overflow error
98F8 .rx_update_buf←3← 98EA BNE← 98EE BNE← 98F2 BNE
LDA econet_data_continue_frame ; Read second data byte (paired transfer)
98FB STA tube_data_register_3 ; Send second byte to Tube
98FE INC port_buf_len ; Advance count after second byte
9900 BNE rx_check_error ; No overflow: check for more data
9902 INC port_buf_len_hi ; Carry to count byte 2
9904 BNE rx_check_error ; No overflow: check for more data
9906 INC open_port_buf ; Carry to count byte 3
9908 BNE rx_check_error ; No overflow: check for more data
990A INC open_port_buf_hi ; Carry to count byte 4
990C BEQ data_rx_tube_complete ; Zero: Tube transfer complete
990E .rx_check_error←3← 9900 BNE← 9904 BNE← 9908 BNE
LDA econet_control23_or_status2 ; Re-read SR2 for next byte pair
9911 BNE rx_tube_data ; More data available: continue loop
9913 JMP nmi_rti ; Return from NMI, wait for data
9916 .data_rx_tube_error←3← 98F6 BEQ← 9928 BEQ← 9934 BEQ
JMP nmi_error_dispatch ; Unexpected end: return from NMI
9919 .data_rx_tube_complete←2← 98E0 BPL← 990C BEQ
LDA #0 ; CR1=&00: disable all interrupts
991B STA econet_control1_or_status1 ; Write CR1 for individual bit testing
991E LDA #&84 ; CR2=&84: disable PSE
9920 STA econet_control23_or_status2 ; Write CR2: same pattern as main path
9923 LDA #2 ; A=&02: FV mask for Tube completion
9925 BIT econet_control23_or_status2 ; BIT SR2: test FV (Z) and RDA (N)
9928 BEQ data_rx_tube_error ; No FV: incomplete frame, error
992A BPL ack_tx ; FV set, no RDA: proceed to ACK
992C LDA port_buf_len ; Check if any buffer was allocated
992E ORA port_buf_len_hi ; OR all 4 buffer pointer bytes together
9930 ORA open_port_buf ; Check buffer low byte
9932 ORA open_port_buf_hi ; Check buffer high byte
9934 BEQ data_rx_tube_error ; All zero (null buffer): error
9936 LDA econet_data_continue_frame ; Read extra trailing byte from FIFO
9939 STA rx_extra_byte ; Save extra byte at &0D5D for later use
993C LDA #&20 ; Bit5 = extra data byte available flag
993E ORA tx_flags ; Set extra byte flag in tx_flags
9941 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 (&9F16). 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.

9944 .ack_tx←2← 98DA JMP← 992A BPL
LDA tx_flags ; Load TX flags to check ACK type
9947 BPL ack_tx_configure ; Bit7 clear: normal scout ACK
9949 JSR advance_rx_buffer_ptr ; Final ACK: call completion handler
994C JMP tx_result_ok ; Jump to TX success result
994F .ack_tx_configure←1← 9947 BPL
LDA #&44 ; CR1=&44: RX_RESET | TIE (switch to TX mode)
9951 STA econet_control1_or_status1 ; Write CR1: switch to TX mode
9954 LDA #&a7 ; CR2=&A7: RTS|CLR_TX_ST|FC_TDRA|2_1_BYTE|PSE
9956 STA econet_control23_or_status2 ; Write CR2: enable TX with status clear
9959 LDA #&e8 ; Install saved next handler (&99BB for scout ACK)
995B LDY #&99 ; High byte of post-ACK handler
995D .ack_tx_write_dest←2← 9812 JMP← 9AF9 JMP
STA nmi_next_lo ; Store next handler low byte
9960 STY nmi_next_hi ; Store next handler high byte
9963 LDA rx_src_stn ; Load dest station from RX scout buffer
9966 BIT econet_control1_or_status1 ; BIT SR1: test TDRA (V=bit6)
9969 BVC dispatch_nmi_error ; TDRA not ready -- error
996B STA econet_data_continue_frame ; Write dest station to TX FIFO
996E LDA rx_src_net ; Write dest network to TX FIFO
9971 STA econet_data_continue_frame ; Write dest net byte to FIFO
9974 LDA #&7b ; Install nmi_ack_tx_src at &9925
9976 LDY #&99 ; High byte of nmi_ack_tx_src
9978 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.

997B .nmi_ack_tx_src
LDA station_id_disable_net_nmis ; Load our station ID (also INTOFF)
997E BIT econet_control1_or_status1 ; BIT SR1: test TDRA
9981 BVC dispatch_nmi_error ; TDRA not ready -- error
9983 STA econet_data_continue_frame ; Write our station to TX FIFO
9986 LDA #0 ; Write network=0 to TX FIFO
9988 STA econet_data_continue_frame ; Write network=0 (local) to TX FIFO
998B LDA tx_flags ; Check tx_flags for data phase
998E BMI start_data_tx ; bit7 set: start data TX phase
9990 LDA #&3f ; CR2=&3F: TX_LAST_DATA | CLR_RX_ST | FLAG_IDLE | FC_TDRA | 2_1_BYTE | PSE
fall through ↓

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.

9992 .post_ack_scout
STA econet_control23_or_status2 ; Write CR2 to clear status after ACK TX
9995 LDA nmi_next_lo ; Install saved handler from &0D4B/&0D4C
9998 LDY nmi_next_hi ; Load saved next handler high byte
999B JMP set_nmi_vector ; Install next NMI handler
999E .start_data_tx←1← 998E BMI
JMP data_tx_begin ; Jump to start data TX phase
99A1 .dispatch_nmi_error←2← 9969 BVC← 9981 BVC
JMP nmi_error_dispatch ; 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.

99A4 .advance_rx_buffer_ptr←2← 9949 JSR← 99F7 JSR
LDA #2 ; A=2: test bit1 of tx_flags
99A6 BIT tx_flags ; BIT tx_flags: check data transfer bit
99A9 BEQ return_10 ; Bit1 clear: no transfer -- return
99AB CLC ; CLC: init carry for 4-byte add
99AC PHP ; Save carry on stack for loop
99AD LDY #8 ; Y=8: RXCB high pointer offset
99AF .add_rxcb_ptr←1← 99BB BCC
LDA (port_ws_offset),y ; Load RXCB[Y] (buffer pointer byte)
99B1 PLP ; Restore carry from stack
99B2 ADC net_tx_ptr,y ; Add transfer count byte
99B5 STA (port_ws_offset),y ; Store updated pointer back to RXCB
99B7 INY ; Next byte
99B8 PHP ; Save carry for next iteration
99B9 CPY #&0c ; Done 4 bytes? (Y reaches &0C)
99BB BCC add_rxcb_ptr ; No: continue adding
99BD PLP ; Discard final carry
99BE LDA #&20 ; A=&20: test bit5 of tx_flags
99C0 BIT tx_flags ; BIT tx_flags: check Tube bit
99C3 BEQ skip_tube_update ; No Tube: skip Tube update
99C5 TXA ; Save X on stack
99C6 PHA ; Push X
99C7 LDA #8 ; A=8: offset for Tube address
99C9 CLC ; CLC for address calculation
99CA ADC port_ws_offset ; Add workspace base offset
99CC TAX ; X = address low for Tube claim
99CD LDY rx_buf_offset ; Y = address high for Tube claim
99CF LDA #1 ; A=1: Tube claim type (read)
99D1 JSR tube_addr_claim ; Claim Tube address for transfer
99D4 LDA rx_extra_byte ; Load extra RX data byte
99D7 STA tube_data_register_3 ; Send to Tube via R3
99DA PLA ; Restore X from stack
99DB TAX ; Transfer to X register
99DC LDY #8 ; Y=8: RXCB buffer ptr offset
99DE LDA (port_ws_offset),y ; Load current RXCB buffer ptr lo
99E0 SEC ; SEC for ADC #0 = add carry
99E1 ADC #0 ; Increment by 1 (Tube extra byte)
99E3 STA (port_ws_offset),y ; Store updated ptr back to RXCB
99E5 .skip_tube_update←1← 99C3 BEQ
LDA #&ff ; A=&FF: return value (transfer done)
99E7 .return_10←1← 99A9 BEQ
RTS ; Return
99E8 LDA rx_port ; Load received port byte
99EB BNE rx_complete_update_rxcb ; Port != 0: data transfer frame
99ED LDY rx_ctrl ; Port=0: load control byte
99F0 CPY #&82 ; Ctrl = &82 (POKE)?
99F2 BEQ rx_complete_update_rxcb ; Yes: POKE also needs data transfer
99F4 JMP imm_op_build_reply ; Other port-0 ops: immediate dispatch
99F7 .rx_complete_update_rxcb←2← 99EB BNE← 99F2 BEQ
JSR advance_rx_buffer_ptr ; Update buffer pointer and check for Tube
99FA BNE skip_buf_ptr_update ; Transfer not done: skip buffer update
99FC .add_buf_to_base
LDA port_buf_len ; Load buffer bytes remaining
99FE CLC ; CLC for address add
99FF ADC open_port_buf ; Add to buffer base address
9A01 BCC store_buf_ptr_lo ; No carry: skip high byte increment
9A03 .inc_rxcb_buf_hi
INC open_port_buf_hi ; Carry: increment buffer high byte
9A05 .store_buf_ptr_lo←1← 9A01 BCC
LDY #8 ; Y=8: store updated buffer position
9A07 .store_rxcb_buf_ptr
STA (port_ws_offset),y ; Store updated low byte to RXCB
9A09 INY ; Y=9: buffer high byte offset
9A0A LDA open_port_buf_hi ; Load updated buffer high byte
9A0C .store_rxcb_buf_hi←1← 9A79 LDA
STA (port_ws_offset),y ; Store high byte to RXCB
9A0E .skip_buf_ptr_update←2← 99FA BNE← 9A53 JMP
LDA rx_port ; Check port byte again
9A11 BEQ discard_reset_listen ; Port=0: immediate op, discard+listen
9A13 LDA rx_src_net ; Load source network from scout buffer
9A16 LDY #3 ; Y=3: RXCB source network offset
9A18 STA (port_ws_offset),y ; Store source network to RXCB
9A1A DEY ; Y=2: source station offset Y=&02
9A1B LDA rx_src_stn ; Load source station from scout buffer
9A1E STA (port_ws_offset),y ; Store source station to RXCB
9A20 DEY ; Y=1: port byte offset Y=&01
9A21 LDA rx_port ; Load port byte
9A24 STA (port_ws_offset),y ; Store port to RXCB
9A26 DEY ; Y=0: control/flag byte offset Y=&00
9A27 LDA rx_ctrl ; Load control byte from scout
9A2A ORA #&80 ; Set bit7 = reception complete flag
9A2C 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.

9A2E .discard_reset_listen←4← 987D JMP← 9A11 BEQ← 9E72 JMP← 9F25 JMP
LDA #2 ; Tube flag bit 1 AND tx_flags bit 1
9A30 AND tube_flag ; Check if Tube transfer active
9A33 BIT tx_flags ; Test tx_flags for Tube transfer
9A36 BEQ discard_listen ; No Tube transfer active -- skip release
9A38 LDA #&82 ; A=&82: Tube release claim type
9A3A 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).

9A3D .discard_listen←3← 9740 JMP← 9A36 BEQ← 9B32 JMP
JSR adlc_rx_listen ; Re-enter idle RX listen mode
fall through ↓

Install RX scout NMI handler

Installs nmi_rx_scout (&96F2) 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.

9A40 .install_rx_scout_handler←2← 9724 JMP← 973D JMP
LDA #&f2 ; Install nmi_rx_scout (&96F2) as NMI handler
9A42 LDY #&96 ; High byte of nmi_rx_scout
9A44 JMP set_nmi_vector ; Set NMI vector and return
9A47 .copy_scout_fields←1← 97F4 JMP
LDY #4 ; Y=4: start at RX CB offset 4
9A49 .copy_scout_loop←1← 9A51 BNE
LDA rx_src_stn,y ; Load scout field (stn/net/ctrl/port)
9A4C STA (port_ws_offset),y ; Store to port workspace buffer
9A4E INY ; Advance buffer pointer
9A4F CPY #&0c ; All 8 fields copied?
9A51 BNE copy_scout_loop ; No: continue copy loop
9A53 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.

9A56 .immediate_op←1← 9790 JMP
LDY rx_ctrl ; Control byte &81-&88 range check
9A59 CPY #&81 ; Below &81: not an immediate op
9A5B BCC imm_op_out_of_range ; Out of range low: jump to discard
9A5D CPY #&89 ; Above &88: not an immediate op
9A5F BCS imm_op_out_of_range ; Out of range high: jump to discard
9A61 CPY #&87 ; HALT(&87)/CONTINUE(&88) skip protection
9A63 BCS imm_op_dispatch ; Ctrl >= &87: dispatch without mask check
9A65 TYA ; Convert ctrl byte to 0-based index for mask
9A66 SEC ; SEC for subtract
9A67 SBC #&81 ; A = ctrl - &81 (0-based operation index)
9A69 TAY ; Y = index for mask rotation count
9A6A LDA prot_status ; Load protection mask from LSTAT
9A6D .rotate_prot_mask←1← 9A6F BPL
ROR ; Rotate mask right by control byte index
9A6E DEY ; Decrement rotation counter
9A6F BPL rotate_prot_mask ; Loop until bit aligned
9A71 BCC imm_op_dispatch ; Carry clear: operation permitted
9A73 JMP imm_op_discard ; Operation blocked by LSTAT mask
9A76 .imm_op_dispatch←2← 9A63 BCS← 9A71 BCC
LDY rx_ctrl ; Reload ctrl byte for dispatch table
9A79 LDA store_rxcb_buf_hi,y ; Look up handler address high byte
9A7C PHA ; Push handler address high
9A7D LDA imm_dispatch_lo,y ; Load handler low byte from jump table
9A80 PHA ; Push handler address low
9A81 RTS ; RTS dispatches to handler
9A82 .imm_op_out_of_range←2← 9A5B BCC← 9A5F BCS
JMP nmi_error_dispatch ; Jump to discard handler
9A85 EQUB <(rx_imm_peek-1)
9A86 EQUB <(rx_imm_poke-1)
9A87 EQUB <(rx_imm_exec-1)
9A88 EQUB <(rx_imm_exec-1)
9A89 EQUB <(rx_imm_exec-1)
9A8A EQUB <(rx_imm_halt_cont-1)
9A8B EQUB <(rx_imm_halt_cont-1)
9A8C EQUB <(rx_imm_machine_type-1)
9A8D EQUB >(rx_imm_peek-1)
9A8E EQUB >(rx_imm_poke-1)
9A8F EQUB >(rx_imm_exec-1)
9A90 EQUB >(rx_imm_exec-1)
9A91 EQUB >(rx_imm_exec-1)
9A92 EQUB >(rx_imm_halt_cont-1)
9A93 EQUB >(rx_imm_halt_cont-1)
9A94 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).

9A95 .rx_imm_exec
LDA #0 ; Buffer start lo = &00
9A97 STA open_port_buf ; Set port buffer lo
9A99 LDA #&82 ; Buffer length lo = &82
9A9B STA port_buf_len ; Set buffer length lo
9A9D LDA #1 ; Buffer length hi = 1
9A9F STA port_buf_len_hi ; Set buffer length hi
9AA1 LDA net_rx_ptr_hi ; Load RX page hi for buffer
9AA3 STA open_port_buf_hi ; Set port buffer hi
9AA5 LDY #3 ; Y=3: copy 4 bytes (3 down to 0)
9AA7 .copy_addr_loop←1← 9AAE BPL
LDA rx_remote_addr,y ; Load remote address byte
9AAA STA l0d58,y ; Store to exec address workspace
9AAD DEY ; Next byte (descending)
9AAE BPL copy_addr_loop ; Loop until all 4 bytes copied
9AB0 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 ack_scout_match.

9AB3 .rx_imm_poke
LDA #&3d ; Port workspace offset = &3D
9AB5 STA port_ws_offset ; Store workspace offset lo
9AB7 LDA #&0d ; RX buffer page = &0D
9AB9 STA rx_buf_offset ; Store workspace offset hi
9ABB JMP ack_scout_match ; Enter POKE data-receive path

RX immediate: machine type query

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

9ABE .rx_imm_machine_type
LDA #1 ; Buffer length hi = 1
9AC0 STA port_buf_len_hi ; Set buffer length hi
9AC2 LDA #&fc ; Buffer length lo = &FC
9AC4 STA port_buf_len ; Set buffer length lo
9AC6 LDA #&25 ; Buffer start lo = &25
9AC8 STA open_port_buf ; Set port buffer lo
9ACA LDA #&7f ; Buffer hi = &7F (below screen)
9ACC STA open_port_buf_hi ; Set port buffer hi
9ACE JMP set_tx_reply_flag ; Enter reply build path

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.

9AD1 .rx_imm_peek
LDA #&3d ; Port workspace offset = &3D
9AD3 STA port_ws_offset ; Store workspace offset lo
9AD5 LDA #&0d ; RX buffer page = &0D
9AD7 STA rx_buf_offset ; Store workspace offset hi
9AD9 LDA #2 ; Scout status = 2 (PEEK response)
9ADB STA scout_status ; Store scout status
9ADE JSR tx_calc_transfer ; Calculate transfer size for response
9AE1 BCC imm_op_discard ; C=0: transfer not set up, discard
9AE3 .set_tx_reply_flag←1← 9ACE JMP
LDA tx_flags ; Mark TX flags bit 7 (reply pending)
9AE6 ORA #&80 ; Set reply pending flag
9AE8 STA tx_flags ; Store updated TX flags
9AEB .rx_imm_halt_cont
LDA #&44 ; CR1=&44: TIE | TX_LAST_DATA
9AED STA econet_control1_or_status1 ; Write CR1: enable TX interrupts
9AF0 .tx_cr2_setup
LDA #&a7 ; CR2=&A7: RTS|CLR_RX_ST|FC_TDRA|PSE
9AF2 STA econet_control23_or_status2 ; Write CR2 for TX setup
9AF5 .tx_nmi_setup
LDA #&12 ; NMI handler lo byte (self-modifying)
9AF7 LDY #&9b ; Y=&9B: dispatch table page
9AF9 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.

9AFC .imm_op_build_reply←1← 99F4 JMP
LDA port_buf_len ; Get buffer position for reply header
9AFE CLC ; Clear carry for offset addition
9AFF ADC #&80 ; Data offset = buf_len + &80 (past header)
9B01 LDY #&7f ; Y=&7F: reply data length slot
9B03 STA (net_rx_ptr),y ; Store reply data length in RX buffer
9B05 LDY #&80 ; Y=&80: source station slot
9B07 LDA rx_src_stn ; Load requesting station number
9B0A STA (net_rx_ptr),y ; Store source station in reply header
9B0C INY ; Y=&81
9B0D LDA rx_src_net ; Load requesting network number
9B10 STA (net_rx_ptr),y ; Store source network in reply header
9B12 LDA rx_ctrl ; Load control byte from received frame
9B15 STA tx_work_57 ; Save ctrl byte for TX response
9B18 LDA #&84 ; IER bit 2: disable SR interrupt
9B1A STA system_via_ier ; Write IER to disable SR
9B1D LDA system_via_acr ; Read ACR for shift register config
9B20 AND #&1c ; Isolate shift register mode bits (2-4)
9B22 STA tx_work_51 ; Save original SR mode for later restore
9B25 LDA system_via_acr ; Reload ACR for modification
9B28 AND #&e3 ; Clear SR mode bits (keep other bits)
9B2A ORA #8 ; SR mode 2: shift in under φ2
9B2C STA system_via_acr ; Apply new shift register mode
9B2F BIT system_via_sr ; Read SR to clear pending interrupt
9B32 .imm_op_discard←2← 9A73 JMP← 9AE1 BCC
JMP discard_listen ; Return to idle listen mode
9B35 .check_sr_irq←1← 966C JMP
LDA #4 ; A=&04: IFR bit 2 (SR) mask
9B37 BIT system_via_ifr ; Test SR interrupt pending
9B3A BNE tx_done_error ; SR fired: handle TX completion
9B3C LDA #5 ; A=5: no SR, return status 5
9B3E RTS ; Return (no SR interrupt)
9B3F .tx_done_error←1← 9B3A BNE
TXA ; Save X
9B40 PHA ; Push X
9B41 TYA ; Save Y
9B42 PHA ; Push Y
9B43 LDA system_via_acr ; Read ACR for shift register mode
9B46 AND #&e3 ; Clear SR mode bits (2-4)
9B48 ORA tx_work_51 ; Restore original SR mode
9B4B STA system_via_acr ; Write updated ACR
9B4E LDA system_via_sr ; Read SR to clear pending interrupt
9B51 LDA #4 ; A=&04: SR bit mask
9B53 STA system_via_ifr ; Clear SR in IFR
9B56 STA system_via_ier ; Disable SR in IER
9B59 LDY tx_work_57 ; Load ctrl byte for dispatch
9B5C CPY #&86 ; Ctrl >= &86? (HALT/CONTINUE)
9B5E BCS tx_done_classify ; Yes: skip protection mask save
9B60 LDA prot_status ; Load current protection mask
9B63 STA saved_jsr_mask ; Save mask before JSR modification
9B66 ORA #&1c ; Enable bits 2-4 (allow JSR ops)
9B68 STA prot_status ; Store modified protection mask
9B6B .tx_done_classify←1← 9B5E BCS
LDA tx_done_handler_hi,y ; Load handler addr hi from table
9B6E PHA ; Push handler hi
9B6F LDA tx_done_handler_lo,y ; Load handler addr lo from table
9B72 PHA ; Push handler lo
9B73 RTS ; Dispatch via RTS (addr-1 on stack)
9B74 EQUB <(tx_done_jsr-1)
9B75 EQUB <(tx_done_user_proc-1)
9B76 EQUB <(tx_done_os_proc-1)
9B77 EQUB <(tx_done_halt-1)
9B78 EQUB <(tx_done_continue-1)
9B79 EQUB >(tx_done_jsr-1)
9B7A EQUB >(tx_done_user_proc-1)
9B7B EQUB >(tx_done_os_proc-1)
9B7C EQUB >(tx_done_halt-1)
9B7D EQUB >(tx_done_continue-1)

TX done: remote JSR execution

Pushes address &9BEB 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.

9B7E .tx_done_jsr
LDA #&9b ; Push hi of (tx_done_exit-1)
9B80 PHA ; Push hi byte on stack
9B81 LDA #&bf ; Push lo of (tx_done_exit-1)
9B83 PHA ; Push lo byte on stack
9B84 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.

9B87 .tx_done_user_proc
LDY #event_network_error ; Y=8: network event type
9B89 LDX l0d58 ; X = remote address lo
9B8C LDA l0d59 ; A = remote address hi
9B8F JSR oseven ; Generate event Y='Network error'
9B92 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.

9B95 .tx_done_os_proc
LDX l0d58 ; X = remote address lo
9B98 LDY l0d59 ; Y = remote address hi
9B9B JSR rom_header ; Call ROM entry point at &8000
9B9E 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.

9BA1 .tx_done_halt
LDA #4 ; A=&04: bit 2 mask for rx_flags
9BA3 BIT rx_flags ; Test if already halted
9BA6 BNE tx_done_exit ; Already halted: skip to exit
9BA8 ORA rx_flags ; Set bit 2 in rx_flags
9BAB STA rx_flags ; Store halt flag
9BAE LDA #4 ; A=4: re-load halt bit mask
9BB0 CLI ; Enable interrupts during halt wait
9BB1 .halt_spin_loop←1← 9BB4 BNE
BIT rx_flags ; Test halt flag
9BB4 BNE halt_spin_loop ; Still halted: keep spinning
9BB6 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.

9BB8 .tx_done_continue
LDA rx_flags ; Load current RX flags
9BBB AND #&fb ; Clear bit 2: release halted station
9BBD STA rx_flags ; Store updated flags
9BC0 .tx_done_exit←4← 9B92 JMP← 9B9E JMP← 9BA6 BNE← 9BB6 BEQ
PLA ; Restore Y from stack
9BC1 TAY ; Transfer to Y register
9BC2 PLA ; Restore X from stack
9BC3 TAX ; Transfer to X register
9BC4 LDA #0 ; A=0: success status
9BC6 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.

9BC7 .tx_begin←1← 9660 JMP
TXA ; Save X on stack
9BC8 PHA ; Push X
9BC9 LDY #2 ; Y=2: TXCB offset for dest station
9BCB LDA (nmi_tx_block),y ; Load dest station from TX control block
9BCD STA tx_dst_stn ; Store to TX scout buffer
9BD0 INY ; Y=&03
9BD1 LDA (nmi_tx_block),y ; Load dest network from TX control block
9BD3 STA tx_dst_net ; Store to TX scout buffer
9BD6 LDY #0 ; Y=0: first byte of TX control block
9BD8 LDA (nmi_tx_block),y ; Load control/flag byte
9BDA BMI tx_imm_op_setup ; Bit7 set: immediate operation ctrl byte
9BDC JMP tx_active_start ; Bit7 clear: normal data transfer
9BDF .tx_imm_op_setup←1← 9BDA BMI
STA tx_ctrl_byte ; Store control byte to TX scout buffer
9BE2 TAX ; X = control byte for range checks
9BE3 INY ; Y=1: port byte offset
9BE4 LDA (nmi_tx_block),y ; Load port byte from TX control block
9BE6 STA tx_port ; Store port byte to TX scout buffer
9BE9 BNE tx_line_idle_check ; Port != 0: skip immediate op setup
9BEB CPX #&83 ; Ctrl < &83: PEEK/POKE need address calc
9BED BCS tx_ctrl_range_check ; Ctrl >= &83: skip to range check
9BEF SEC ; SEC: init borrow for 4-byte subtract
9BF0 PHP ; Save carry on stack for loop
9BF1 LDY #8 ; Y=8: high pointer offset in TXCB
9BF3 .calc_peek_poke_size←1← 9C07 BCC
LDA (nmi_tx_block),y ; Load TXCB[Y] (end addr byte)
9BF5 DEY ; Y -= 4: back to start addr offset
9BF6 DEY ; (continued)
9BF7 DEY ; (continued)
9BF8 DEY ; (continued)
9BF9 PLP ; Restore borrow from stack
9BFA SBC (nmi_tx_block),y ; end - start = transfer size byte
9BFC STA tx_data_start,y ; Store result to tx_data_start
9BFF INY ; Y += 5: advance to next end byte
9C00 INY ; (continued)
9C01 INY ; (continued)
9C02 INY ; (continued)
9C03 INY ; (continued)
9C04 PHP ; Save borrow for next byte
9C05 CPY #&0c ; Done all 4 bytes? (Y reaches &0C)
9C07 BCC calc_peek_poke_size ; No: next byte pair
9C09 PLP ; Discard final borrow
9C0A .tx_ctrl_range_check←1← 9BED BCS
CPX #&81 ; Ctrl < &81: not an immediate op
9C0C BCC tx_active_start ; Below range: normal data transfer
9C0E .check_imm_range
CPX #&89 ; Ctrl >= &89: out of immediate range
9C10 BCS tx_active_start ; Above range: normal data transfer
9C12 LDY #&0c ; Y=&0C: start of extra data in TXCB
9C14 .copy_imm_params←1← 9C1C BCC
LDA (nmi_tx_block),y ; Load extra parameter byte from TXCB
9C16 STA nmi_shim_1a,y ; Copy to NMI shim workspace at &0D1A+Y
9C19 INY ; Next byte
9C1A CPY #&10 ; Done 4 bytes? (Y reaches &10)
9C1C BCC copy_imm_params ; No: continue copying
9C1E .tx_line_idle_check←1← 9BE9 BNE
LDA #&20 ; A=&20: mask for SR2 INACTIVE bit
9C20 BIT econet_control23_or_status2 ; BIT SR2: test if line is idle
9C23 BNE tx_no_clock_error ; Line not idle: handle as line jammed
9C25 LDA #&fd ; A=&FD: high byte of timeout counter
9C27 PHA ; Push timeout high byte to stack
9C28 LDA #6 ; Scout frame = 6 address+ctrl bytes
9C2A STA tx_length ; Store scout frame length
9C2D 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 &9C4D-&9C52 works because CR2=&67 has RTS=0, so cts_input_ is always true, and SR1_CTS reflects presence of clock hardware.

9C2F .inactive_poll
STA tx_index ; Save TX index
9C32 PHA ; Push timeout byte 1 on stack
9C33 PHA ; Push timeout byte 2 on stack
9C34 LDY #&e7 ; Y=&E7: CR2 value for TX prep (RTS|CLR_TX_ST|CLR_RX_ST|FC_TDRA|2_1_BYTE| PSE)
9C36 .test_inactive_retry←3← 9C5C BNE← 9C61 BNE← 9C66 BNE
LDA #4 ; A=&04: INACTIVE mask for SR2 bit2
9C38 PHP ; Save interrupt state
9C39 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.

9C3A .intoff_test_inactive←1← 9CB6 LDA
BIT station_id_disable_net_nmis ; INTOFF -- disable NMIs
9C3D BIT station_id_disable_net_nmis ; INTOFF again (belt-and-braces)
9C40 .test_line_idle
BIT econet_control23_or_status2 ; BIT SR2: Z = &04 AND SR2 -- tests INACTIVE
9C43 BEQ inactive_retry ; INACTIVE not set -- re-enable NMIs and loop
9C45 LDA econet_control1_or_status1 ; Read SR1 (acknowledge pending interrupt)
9C48 LDA #&67 ; CR2=&67: CLR_TX_ST|CLR_RX_ST|FC_ TDRA|2_1_BYTE|PSE
9C4A STA econet_control23_or_status2 ; Write CR2: clear status, prepare TX
9C4D LDA #&10 ; A=&10: CTS mask for SR1 bit4
9C4F BIT econet_control1_or_status1 ; BIT SR1: tests CTS present
9C52 BNE tx_prepare ; CTS set -- clock hardware detected, start TX
9C54 .inactive_retry←1← 9C43 BEQ
BIT video_ula_control ; INTON -- re-enable NMIs (&FE20 read)
9C57 PLP ; Restore interrupt state
9C58 TSX ; 3-byte timeout counter on stack
9C59 INC l0101,x ; Increment timeout counter byte 1
9C5C BNE test_inactive_retry ; Not overflowed: retry INACTIVE test
9C5E INC l0102,x ; Increment timeout counter byte 2
9C61 BNE test_inactive_retry ; Not overflowed: retry INACTIVE test
9C63 INC l0103,x ; Increment timeout counter byte 3
9C66 BNE test_inactive_retry ; Not overflowed: retry INACTIVE test
9C68 JMP tx_line_jammed ; All 3 bytes overflowed: line jammed
; TX_ACTIVE branch (A=&44 = CR1 value for TX active)
9C6B .tx_active_start←3← 9BDC JMP← 9C0C BCC← 9C10 BCS
LDA #&44 ; CR1=&44: TIE | TX_LAST_DATA
9C6D 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.

9C6F .tx_line_jammed←1← 9C68 JMP
LDA #7 ; CR2=&07: FC_TDRA | 2_1_BYTE | PSE (abort TX)
9C71 STA econet_control23_or_status2 ; Write CR2 to abort TX
9C74 PLA ; Clean 3 bytes of timeout loop state
9C75 PLA ; Pop saved register
9C76 PLA ; Pop saved register
9C77 LDA #&40 ; Error &40 = 'Line Jammed'
9C79 BNE store_tx_error ; ALWAYS branch to shared error handler ALWAYS branch
9C7B .tx_no_clock_error←1← 9C23 BNE
LDA #&43 ; Error &43 = 'No Clock'
9C7D .store_tx_error←2← 9C6D BNE← 9C79 BNE
LDY #0 ; Offset 0 = error byte in TX control block
9C7F STA (nmi_tx_block),y ; Store error code in TX CB byte 0
9C81 LDA #&80 ; &80 = TX complete flag
9C83 STA tx_clear_flag ; Signal TX operation complete
9C86 PLA ; Restore X saved by caller
9C87 TAX ; Move to X register
9C88 RTS ; Return to TX caller

TX preparation

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

9C89 .tx_prepare←1← 9C52 BNE
STY econet_control23_or_status2 ; Write CR2 = Y (&E7: RTS|CLR_TX_ST|CLR_RX_ST| FC_TDRA|2_1_BYTE|PSE)
9C8C LDX #&44 ; CR1=&44: RX_RESET | TIE (TX active, TX interrupts enabled)
9C8E STX econet_control1_or_status1 ; Write to ADLC CR1
9C91 LDX #&2d ; Install NMI handler at &9D2D (nmi_tx_data)
9C93 LDY #&9d ; High byte of NMI handler address
9C95 STX nmi_jmp_lo ; Write NMI vector low byte directly
9C98 STY nmi_jmp_hi ; Write NMI vector high byte directly
9C9B BIT video_ula_control ; INTON -- NMIs now fire for TDRA (&FE20 read)
9C9E LDA tx_port ; Load destination port number
9CA1 BNE setup_data_xfer ; Port != 0: standard data transfer
9CA3 LDY tx_ctrl_byte ; Port 0: load control byte for table lookup
9CA6 LDA tube_tx_count_4,y ; Look up tx_flags from table
9CA9 STA tx_flags ; Store operation flags
9CAC LDA tube_tx_count_2,y ; Look up tx_length from table
9CAF STA tx_length ; Store expected transfer length
9CB2 LDA sr2_idle_status,y ; Load handler from dispatch table
9CB5 PHA ; Push high byte for PHA/PHA/RTS dispatch
9CB6 LDA intoff_test_inactive,y ; Look up handler address low from table
9CB9 PHA ; Push low byte for PHA/PHA/RTS dispatch
9CBA RTS ; RTS dispatches to control-byte handler
9CBB EQUB <(tx_ctrl_peek-1)
9CBC EQUB <(tx_ctrl_poke-1)
9CBD EQUB <(proc_op_status2-1)
9CBE EQUB <(proc_op_status2-1)
9CBF EQUB <(proc_op_status2-1)
9CC0 EQUB <(tx_ctrl_exit-1)
9CC1 EQUB <(tx_ctrl_exit-1)
9CC2 EQUB <(imm_op_status3-1)
9CC3 EQUB >(tx_ctrl_peek-1)
9CC4 EQUB >(tx_ctrl_poke-1)
9CC5 EQUB >(proc_op_status2-1)
9CC6 EQUB >(proc_op_status2-1)
9CC7 EQUB >(proc_op_status2-1)
9CC8 EQUB >(tx_ctrl_exit-1)
9CC9 EQUB >(tx_ctrl_exit-1)
9CCA EQUB >(imm_op_status3-1)
9CCB .imm_op_status3
LDA #3 ; A=3: scout_status for PEEK
9CCD BNE store_status_copy_ptr ; 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.

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

9CD3 .tx_ctrl_poke
LDA #2 ; Scout status = 2 (POKE transfer)
9CD5 .store_status_add4←1← 9CD1 BNE
STA scout_status ; Store scout status
9CD8 CLC ; Clear carry for 4-byte addition
9CD9 PHP ; Save carry on stack
9CDA LDY #&0c ; Y=&0C: start at offset 12
9CDC .add_bytes_loop←1← 9CE9 BCC
LDA l0d1e,y ; Load workspace address byte
9CDF PLP ; Restore carry from previous byte
9CE0 ADC (nmi_tx_block),y ; Add TXCB address byte
9CE2 STA l0d1e,y ; Store updated address byte
9CE5 INY ; Next byte
9CE6 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.

9CE7 .tx_ctrl_proc
CPY #&10 ; Compare Y with 16-byte boundary
9CE9 BCC add_bytes_loop ; Below boundary: continue addition
9CEB PLP ; Restore processor flags
9CEC JMP skip_buf_setup ; Exit TX ctrl setup
9CEF .setup_data_xfer←1← 9CA1 BNE
LDA tx_dst_stn ; Load dest station for broadcast check
9CF2 AND tx_dst_net ; AND with dest network
9CF5 CMP #&ff ; Both &FF = broadcast address?
9CF7 BNE setup_unicast_xfer ; Not broadcast: unicast path
9CF9 LDA #&0e ; Broadcast scout: 14 bytes total
9CFB STA tx_length ; Store broadcast scout length
9CFE LDA #&40 ; A=&40: broadcast flag
9D00 STA tx_flags ; Set broadcast flag in tx_flags
9D03 LDY #4 ; Y=4: start of address data in TXCB
9D05 .copy_bcast_addr←1← 9D0D BCC
LDA (nmi_tx_block),y ; Copy TXCB address bytes to scout buffer
9D07 STA tx_src_stn,y ; Store to TX source/data area
9D0A INY ; Next byte
9D0B CPY #&0c ; Done 8 bytes? (Y reaches &0C)
9D0D BCC copy_bcast_addr ; No: continue copying
9D0F BCS tx_ctrl_exit ; ALWAYS branch
9D11 .setup_unicast_xfer←1← 9CF7 BNE
LDA #0 ; A=0: clear flags for unicast
9D13 STA tx_flags ; Clear tx_flags
9D16 .proc_op_status2
LDA #2 ; scout_status=2: data transfer pending
9D18 .store_status_copy_ptr←1← 9CCD BNE
STA scout_status ; Store scout status
9D1B .skip_buf_setup←1← 9CEC JMP
LDA nmi_tx_block ; Copy TX block pointer to workspace ptr
9D1D STA port_ws_offset ; Store low byte
9D1F LDA nmi_tx_block_hi ; Copy TX block pointer high byte
9D21 STA rx_buf_offset ; Store high byte
9D23 JSR tx_calc_transfer ; Calculate transfer size from RXCB
9D26 .tx_ctrl_exit←1← 9D0F BCS
PLP ; Restore processor status from stack
9D27 PLA ; Restore stacked registers (4 PLAs)
9D28 PLA ; Second PLA
9D29 PLA ; Third PLA
9D2A PLA ; Fourth PLA
9D2B TAX ; Restore X from A
9D2C 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.

9D2D .nmi_tx_data
LDY tx_index ; Load TX buffer index
9D30 BIT econet_control1_or_status1 ; BIT SR1: V=bit6(TDRA), N=bit7(IRQ)
9D33 .tx_fifo_write←1← 9D4E BMI
BVC tx_fifo_not_ready ; TDRA not set -- TX error
9D35 LDA tx_dst_stn,y ; Load byte from TX buffer
9D38 STA econet_data_continue_frame ; Write to TX_DATA (continue frame)
9D3B INY ; Next TX buffer byte
9D3C LDA tx_dst_stn,y ; Load second byte from TX buffer
9D3F INY ; Advance TX index past second byte
9D40 STY tx_index ; Save updated TX buffer index
9D43 STA econet_data_continue_frame ; Write second byte to TX_DATA
9D46 CPY tx_length ; Compare index to TX length
9D49 BCS tx_last_data ; Frame complete -- go to TX_LAST_DATA
9D4B BIT econet_control1_or_status1 ; Check if we can send another pair
9D4E BMI tx_fifo_write ; IRQ set -- send 2 more bytes (tight loop)
9D50 JMP nmi_rti ; RTI -- wait for next NMI
; TX error path
9D53 .tx_error←1← 9D98 BEQ
LDA #&42 ; Error &42
9D55 BNE tx_store_error ; ALWAYS branch
9D57 .tx_fifo_not_ready←1← 9D33 BVC
LDA #&67 ; CR2=&67: clear status, return to listen
9D59 STA econet_control23_or_status2 ; Write CR2: clear status, idle listen
9D5C LDA #&41 ; Error &41 (TDRA not ready)
9D5E .tx_store_error←1← 9D55 BNE
LDY station_id_disable_net_nmis ; INTOFF (also loads station ID)
9D61 .delay_nmi_disable←1← 9D64 BNE
PHA ; PHA/PLA delay loop (256 iterations for NMI disable)
9D62 PLA ; PHA/PLA delay (~7 cycles each)
9D63 INY ; Increment delay counter
9D64 BNE delay_nmi_disable ; Loop 256 times for NMI disable
9D66 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 &9D75 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)

9D69 .tx_last_data←1← 9D49 BCS
LDA #&3f ; CR2=&3F: TX_LAST_DATA | CLR_RX_ST | FLAG_IDLE | FC_TDRA | 2_1_BYTE | PSE
9D6B STA econet_control23_or_status2 ; Write to ADLC CR2
9D6E LDA #&75 ; Install NMI handler at &9D75 (nmi_tx_complete)
9D70 LDY #&9d ; High byte of handler address
9D72 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 &9F16 - bit0 set at &0D4A -> four-way handshake data phase at &9EBA - Otherwise -> install RX reply handler at &9D93

9D75 .nmi_tx_complete
LDA #&82 ; CR1=&82: TX_RESET | RIE (now in RX mode)
9D77 STA econet_control1_or_status1 ; Write CR1 to switch from TX to RX
9D7A BIT tx_flags ; Test workspace flags
9D7D BVC check_handshake_bit ; bit6 not set -- check bit0
9D7F JMP tx_result_ok ; bit6 set -- TX completion
9D82 .check_handshake_bit←1← 9D7D BVC
LDA #1 ; A=1: mask for bit0 test
9D84 BIT tx_flags ; Test tx_flags bit0 (handshake)
9D87 BEQ install_reply_scout ; bit0 clear: install reply handler
9D89 JMP handshake_await_ack ; bit0 set -- four-way handshake data phase
9D8C .install_reply_scout←1← 9D87 BEQ
LDA #&93 ; Install nmi_reply_scout at &9D30
9D8E LDY #&9d ; High byte of nmi_reply_scout addr
9D90 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).

9D93 .nmi_reply_scout
LDA #1 ; A=&01: AP mask for SR2
9D95 BIT econet_control23_or_status2 ; BIT SR2: test AP (Address Present)
9D98 BEQ tx_error ; No AP -- error
9D9A LDA econet_data_continue_frame ; Read first RX byte (destination station)
9D9D CMP station_id_disable_net_nmis ; Compare to our station ID (INTOFF side effect)
9DA0 BNE reject_reply ; Not our station -- error/reject
9DA2 LDA #&a9 ; Install nmi_reply_cont at &9DA9
9DA4 LDY #&9d ; High byte of nmi_reply_cont
9DA6 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 &9DC2 for the remaining two bytes (source station and network). Optimisation: checks SR1 bit7 (IRQ still asserted) via BMI at &9DBA. If IRQ is still set, falls through directly to &9DC2 without an RTI, avoiding NMI re-entry overhead for short frames where all bytes arrive in quick succession.

9DA9 .nmi_reply_cont
BIT econet_control23_or_status2 ; BIT SR2: test for RDA (bit7 = data available)
9DAC BPL reject_reply ; No RDA -- error
9DAE LDA econet_data_continue_frame ; Read destination network byte
9DB1 BNE reject_reply ; Non-zero -- network mismatch, error
9DB3 LDA #&c2 ; Install nmi_reply_validate at &9DC2
9DB5 LDY #&9d ; High byte of nmi_reply_validate
9DB7 BIT econet_control1_or_status1 ; BIT SR1: test IRQ (N=bit7) -- more data ready?
9DBA BMI nmi_reply_validate ; IRQ set -- fall through to &9DC2 without RTI
9DBC JMP set_nmi_vector ; IRQ not set -- install handler and RTI
9DBF .reject_reply←7← 9DA0 BNE← 9DAC BPL← 9DB1 BNE← 9DC5 BPL← 9DCD BNE← 9DD5 BNE← 9DDC BEQ
JMP tx_result_fail ; 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 &9DC2 -- must see data available 2. Read source station at &9DC7, compare to &0D20 (tx_dst_stn) 3. Read source network at &9DCF, compare to &0D21 (tx_dst_net) 4. Check SR2 bit1 (FV) at &9DD9 -- 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).

9DC2 .nmi_reply_validate←1← 9DBA BMI
BIT econet_control23_or_status2 ; BIT SR2: test RDA (bit7). Must be set for valid reply.
9DC5 BPL reject_reply ; No RDA -- error (FV masking RDA via PSE would cause this)
9DC7 LDA econet_data_continue_frame ; Read source station
9DCA CMP tx_dst_stn ; Compare to original TX destination station (&0D20)
9DCD BNE reject_reply ; Mismatch -- not the expected reply, error
9DCF LDA econet_data_continue_frame ; Read source network
9DD2 CMP tx_dst_net ; Compare to original TX destination network (&0D21)
9DD5 BNE reject_reply ; Mismatch -- error
9DD7 LDA #2 ; A=&02: FV mask for SR2 bit1
9DD9 BIT econet_control23_or_status2 ; BIT SR2: test FV -- frame must be complete
9DDC BEQ reject_reply ; No FV -- incomplete frame, error
9DDE LDA #&a7 ; CR2=&A7: RTS|CLR_TX_ST|FC_TDRA|2_ 1_BYTE|PSE (TX in handshake)
9DE0 STA econet_control23_or_status2 ; Write CR2: enable RTS for TX handshake
9DE3 LDA #&44 ; CR1=&44: RX_RESET | TIE (TX active for scout ACK)
9DE5 STA econet_control1_or_status1 ; Write CR1: reset RX, enable TX interrupt
9DE8 LDA #&ba ; Install next handler at &9EBA into &0D4B/&0D4C
9DEA LDY #&9e ; High byte &9E of next handler address
9DEC STA nmi_next_lo ; Store low byte to nmi_next_lo
9DEF STY nmi_next_hi ; Store high byte to nmi_next_hi
9DF2 LDA tx_dst_stn ; Load dest station for scout ACK TX
9DF5 BIT econet_control1_or_status1 ; BIT SR1: test TDRA (V=bit6)
9DF8 BVC data_tx_error ; TDRA not ready -- error
9DFA STA econet_data_continue_frame ; Write dest station to TX FIFO
9DFD LDA tx_dst_net ; Load dest network for scout ACK TX
9E00 STA econet_data_continue_frame ; Write dest network to TX FIFO
9E03 LDA #&0a ; Install nmi_scout_ack_src at &9E0A
9E05 LDY #&9e ; High byte &9D of handler address
9E07 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.

9E0A .nmi_scout_ack_src
LDA station_id_disable_net_nmis ; Read our station ID (also INTOFF)
9E0D BIT econet_control1_or_status1 ; BIT SR1: check TDRA before writing
9E10 BVC data_tx_error ; TDRA not ready: TX error
9E12 STA econet_data_continue_frame ; Write our station to TX FIFO
9E15 LDA #0 ; Network = 0 (local network)
9E17 STA econet_data_continue_frame ; Write network byte to TX FIFO
9E1A .data_tx_begin←1← 999E JMP
LDA #2 ; Test bit 1 of tx_flags
9E1C BIT tx_flags ; Check if immediate-op or data-transfer
9E1F BNE install_imm_data_nmi ; Bit 1 set: immediate op, use alt handler
9E21 LDA #&2f ; Install nmi_data_tx at &9E2F
9E23 LDY #&9e ; High byte of handler address
9E25 JMP set_nmi_vector ; Install and return via set_nmi_vector
9E28 .install_imm_data_nmi←1← 9E1F BNE
LDA #&81 ; Install nmi_data_tx_tube at &9E81
9E2A LDY #&9e ; High byte of handler address
9E2C 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 &9D2D but reads from the port buffer instead of the TX workspace. Writes two bytes per iteration, checking SR1 IRQ between pairs for tight looping.

9E2F .nmi_data_tx
LDY port_buf_len ; Y = buffer offset, resume from last position
9E31 BIT econet_control1_or_status1 ; BIT SR1: test TDRA (V=bit6)
9E34 .data_tx_check_fifo←1← 9E57 BMI
BVC data_tx_error ; TDRA not ready -- error
9E36 LDA (open_port_buf),y ; Write data byte to TX FIFO
9E38 STA econet_data_continue_frame ; Write first byte of pair to FIFO
9E3B INY ; Advance buffer offset
9E3C BNE write_second_tx_byte ; No page crossing
9E3E DEC port_buf_len_hi ; Page crossing: decrement page count
9E40 BEQ data_tx_last ; No pages left: send last data
9E42 INC open_port_buf_hi ; Increment buffer high byte
9E44 .write_second_tx_byte←1← 9E3C BNE
LDA (open_port_buf),y ; Load second byte of pair
9E46 STA econet_data_continue_frame ; Write second byte to FIFO
9E49 INY ; Advance buffer offset
9E4A STY port_buf_len ; Save updated buffer position
9E4C BNE check_irq_loop ; No page crossing
9E4E DEC port_buf_len_hi ; Page crossing: decrement page count
9E50 BEQ data_tx_last ; No pages left: send last data
9E52 INC open_port_buf_hi ; Increment buffer high byte
9E54 .check_irq_loop←1← 9E4C BNE
BIT econet_control1_or_status1 ; BIT SR1: test IRQ (N=bit7) for tight loop
9E57 BMI data_tx_check_fifo ; IRQ still set: write 2 more bytes
9E59 JMP nmi_rti ; No IRQ: return, wait for next NMI
9E5C .data_tx_last←4← 9E40 BEQ← 9E50 BEQ← 9E9A BEQ← 9EB0 BEQ
LDA #&3f ; CR2=&3F: TX_LAST_DATA (close data frame)
9E5E STA econet_control23_or_status2 ; Write CR2 to close frame
9E61 LDA tx_flags ; Check tx_flags for next action
9E64 BPL install_saved_handler ; Bit7 clear: error, install saved handler
9E66 LDA #&2e ; Install discard_reset_listen at &99DB
9E68 LDY #&9a ; High byte of &99DB handler
9E6A JMP set_nmi_vector ; Set NMI vector and return
9E6D .data_tx_error←4← 9DF8 BVC← 9E10 BVC← 9E34 BVC← 9E84 BVC
LDA tx_flags ; Load saved next handler low byte
9E70 BPL jmp_tx_result_fail ; bit7 clear: error path
9E72 JMP discard_reset_listen ; ADLC reset and return to idle
9E75 .jmp_tx_result_fail←1← 9E70 BPL
JMP tx_result_fail ; Store result and return to idle
9E78 .install_saved_handler←1← 9E64 BPL
LDA nmi_next_lo ; Load saved handler low byte
9E7B LDY nmi_next_hi ; Load saved next handler high byte
9E7E JMP set_nmi_vector ; Install saved handler and return
9E81 .nmi_data_tx_tube
BIT econet_control1_or_status1 ; Tube TX: BIT SR1 test TDRA
9E84 .tube_tx_fifo_write←1← 9EB5 BMI
BVC data_tx_error ; TDRA not ready -- error
9E86 LDA tube_data_register_3 ; Read byte from Tube R3
9E89 STA econet_data_continue_frame ; Write to TX FIFO
9E8C INC port_buf_len ; Increment 4-byte buffer counter
9E8E BNE write_second_tube_byte ; Low byte didn't wrap
9E90 INC port_buf_len_hi ; Carry into second byte
9E92 BNE write_second_tube_byte ; No further carry
9E94 INC open_port_buf ; Carry into third byte
9E96 BNE write_second_tube_byte ; No further carry
9E98 INC open_port_buf_hi ; Carry into fourth byte
9E9A BEQ data_tx_last ; Counter wrapped to zero: last data
9E9C .write_second_tube_byte←3← 9E8E BNE← 9E92 BNE← 9E96 BNE
LDA tube_data_register_3 ; Read second Tube byte from R3
9E9F STA econet_data_continue_frame ; Write second byte to TX FIFO
9EA2 INC port_buf_len ; Increment 4-byte counter (second byte)
9EA4 BNE check_tube_irq_loop ; Low byte didn't wrap
9EA6 .tube_tx_inc_byte2
INC port_buf_len_hi ; Carry into second byte
9EA8 BNE check_tube_irq_loop ; No further carry
9EAA .tube_tx_inc_byte3
INC open_port_buf ; Carry into third byte
9EAC BNE check_tube_irq_loop ; No further carry
9EAE .tube_tx_inc_byte4
INC open_port_buf_hi ; Carry into fourth byte
9EB0 BEQ data_tx_last ; Counter wrapped to zero: last data
9EB2 .check_tube_irq_loop←3← 9EA4 BNE← 9EA8 BNE← 9EAC BNE
BIT econet_control1_or_status1 ; BIT SR1: test IRQ for tight loop
9EB5 BMI tube_tx_fifo_write ; IRQ still set: write 2 more bytes
9EB7 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 &9EC6 to receive the final ACK from the remote station.

9EBA .handshake_await_ack←1← 9D89 JMP
LDA #&82 ; CR1=&82: TX_RESET | RIE (switch to RX for final ACK)
9EBC STA econet_control1_or_status1 ; Write to ADLC CR1
9EBF LDA #&c6 ; Install nmi_final_ack at &9E5C
9EC1 LDY #&9e ; High byte of handler address
9EC3 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 (&9D93-&9DC2): &9EC6: Check AP, read dest_stn, compare to our station &9EDC: Check RDA, read dest_net, validate = 0 &9EF2: Check RDA, read src_stn/net, compare to TX dest &9F0F: Check FV for frame completion On success, stores result=0 at &9F16. On any failure, error &41.

9EC6 .nmi_final_ack
LDA #1 ; A=&01: AP mask
9EC8 BIT econet_control23_or_status2 ; BIT SR2: test AP
9ECB BEQ tx_result_fail ; No AP -- error
9ECD LDA econet_data_continue_frame ; Read dest station
9ED0 CMP station_id_disable_net_nmis ; Compare to our station (INTOFF side effect)
9ED3 BNE tx_result_fail ; Not our station -- error
9ED5 LDA #&dc ; Install nmi_final_ack_net at &9E70
9ED7 LDY #&9e ; High byte of handler address
9ED9 JMP set_nmi_vector ; Install continuation handler
9EDC .nmi_final_ack_net
BIT econet_control23_or_status2 ; BIT SR2: test RDA
9EDF BPL tx_result_fail ; No RDA -- error
9EE1 LDA econet_data_continue_frame ; Read dest network
9EE4 BNE tx_result_fail ; Non-zero -- network mismatch, error
9EE6 LDA #&f2 ; Install nmi_final_ack_validate at &9E84
9EE8 LDY #&9e ; High byte of handler address
9EEA BIT econet_control1_or_status1 ; BIT SR1: test IRQ -- more data ready?
9EED BMI nmi_final_ack_validate ; IRQ set -- fall through to &9E84 without RTI
9EEF 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.

9EF2 .nmi_final_ack_validate←1← 9EED BMI
BIT econet_control23_or_status2 ; BIT SR2: test RDA
9EF5 BPL tx_result_fail ; No RDA -- error
9EF7 LDA econet_data_continue_frame ; Read source station
9EFA CMP tx_dst_stn ; Compare to TX dest station (&0D20)
9EFD BNE tx_result_fail ; Mismatch -- error
9EFF LDA econet_data_continue_frame ; Read source network
9F02 CMP tx_dst_net ; Compare to TX dest network (&0D21)
9F05 BNE tx_result_fail ; Mismatch -- error
9F07 LDA tx_flags ; Load TX flags for next action
9F0A BPL check_fv_final_ack ; bit7 clear: no data phase
9F0C JMP install_data_rx_handler ; Install data RX handler
9F0F .check_fv_final_ack←1← 9F0A BPL
LDA #2 ; A=&02: FV mask for SR2 bit1
9F11 BIT econet_control23_or_status2 ; BIT SR2: test FV -- frame must be complete
9F14 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 &9A2E.

9F16 .tx_result_ok←2← 994C JMP← 9D7F JMP
LDA #0 ; A=0: success result code
9F18 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.

9F1A .tx_result_fail←11← 9877 JMP← 9DBF JMP← 9E75 JMP← 9ECB BEQ← 9ED3 BNE← 9EDF BPL← 9EE4 BNE← 9EF5 BPL← 9EFD BNE← 9F05 BNE← 9F14 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 &9A2E. Error codes: &00=success, &40=line jammed, &41=not listening, &42=net error.

9F1C .tx_store_result←2← 9D66 JMP← 9F18 BEQ
LDY #0 ; Y=0: index into TX control block
9F1E STA (nmi_tx_block),y ; Store result/error code at (nmi_tx_block),0
9F20 LDA #&80 ; &80: completion flag for &0D3A
9F22 STA tx_clear_flag ; Signal TX complete
9F25 JMP discard_reset_listen ; Full ADLC reset and return to idle listen
; Unreferenced data block (purpose unknown)
9F28 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.

9F38 .tx_calc_transfer←3← 97FC JSR← 9ADE JSR← 9D23 JSR
LDY #6 ; Load RXCB[6] (buffer addr byte 2)
9F3A LDA (port_ws_offset),y ; Load workspace byte at offset Y
9F3C INY ; Y=&07
9F3D AND (port_ws_offset),y ; AND with TX block[7] (byte 3)
9F3F CMP #&ff ; Both &FF = no buffer?
9F41 BEQ fallback_calc_transfer ; Yes: fallback path
9F43 LDA tube_flag ; Tube transfer in progress?
9F46 BEQ fallback_calc_transfer ; No: fallback path
9F48 LDA tx_flags ; Load TX flags for transfer setup
9F4B ORA #2 ; Set bit 1 (transfer complete)
9F4D STA tx_flags ; Store with bit 1 set (Tube xfer)
9F50 SEC ; Init borrow for 4-byte subtract
9F51 PHP ; Save carry on stack
9F52 LDY #4 ; Y=4: start at RXCB offset 4
9F54 .calc_transfer_size←1← 9F66 BCC
LDA (port_ws_offset),y ; Load RXCB[Y] (current ptr byte)
9F56 INY ; Y += 4: advance to RXCB[Y+4]
9F57 INY ; (continued)
9F58 INY ; (continued)
9F59 INY ; (continued)
9F5A PLP ; Restore borrow from previous byte
9F5B SBC (port_ws_offset),y ; Subtract RXCB[Y+4] (start ptr byte)
9F5D STA net_tx_ptr,y ; Store result byte
9F60 DEY ; Y -= 3: next source byte
9F61 DEY ; (continued)
9F62 DEY ; (continued)
9F63 PHP ; Save borrow for next byte
9F64 CPY #8 ; Done all 4 bytes?
9F66 BCC calc_transfer_size ; No: next byte pair
9F68 PLP ; Discard final borrow
9F69 TXA ; A = saved X
9F6A PHA ; Save X
9F6B LDA #4 ; Compute address of RXCB+4
9F6D CLC ; CLC for base pointer addition
9F6E ADC port_ws_offset ; Add RXCB base to get RXCB+4 addr
9F70 TAX ; X = low byte of RXCB+4
9F71 LDY rx_buf_offset ; Y = high byte of RXCB ptr
9F73 LDA #&c2 ; Tube claim type &C2
9F75 JSR tube_addr_claim ; Claim Tube transfer address
9F78 BCC restore_x_and_return ; No Tube: skip reclaim
9F7A LDA scout_status ; Tube: reclaim with scout status
9F7D JSR tube_addr_claim ; Reclaim with scout status type
9F80 SEC ; C=1: Tube address claimed
9F81 .restore_x_and_return←1← 9F78 BCC
PLA ; Restore X
9F82 TAX ; Restore X from stack
9F83 RTS ; Return with C = transfer status
9F84 .fallback_calc_transfer←2← 9F41 BEQ← 9F46 BEQ
LDY #4 ; Y=4: RXCB current pointer offset
9F86 LDA (port_ws_offset),y ; Load RXCB[4] (current ptr lo)
9F88 LDY #8 ; Y=8: RXCB start address offset
9F8A SEC ; Set carry for subtraction
9F8B SBC (port_ws_offset),y ; Subtract RXCB[8] (start ptr lo)
9F8D STA port_buf_len ; Store transfer size lo
9F8F LDY #5 ; Y=5: current ptr hi offset
9F91 LDA (port_ws_offset),y ; Load RXCB[5] (current ptr hi)
9F93 SBC #0 ; Propagate borrow from lo subtraction
9F95 STA open_port_buf_hi ; Temp store adjusted current ptr hi
9F97 LDY #8 ; Y=8: start address lo offset
9F99 LDA (port_ws_offset),y ; Copy RXCB[8] to open port buffer lo
9F9B STA open_port_buf ; Store to scratch (side effect)
9F9D LDY #9 ; Y=9: start address hi offset
9F9F LDA (port_ws_offset),y ; Load RXCB[9] (start ptr hi)
9FA1 SEC ; Set carry for subtraction
9FA2 SBC open_port_buf_hi ; start_hi - adjusted current_hi
9FA4 STA port_buf_len_hi ; Store transfer size hi
9FA6 SEC ; Return with C=1
9FA7 .nmi_shim_rom_src←1← 968F 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 (&96F2). 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 &96F2.

9FA8 .nmi_bootstrap_entry
BIT station_id_disable_net_nmis ; INTOFF: disable NMIs while switching ROM
9FAB PHA ; Save A
9FAC TYA ; Transfer Y to A
9FAD PHA ; Save Y (via A)
9FAE LDA #0 ; ROM bank 0 (patched during init for actual bank)
9FB0 STA romsel ; Select Econet ROM bank via ROMSEL
9FB3 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.

9FB6 .rom_set_nmi_vector
STY nmi_jmp_hi ; Store handler high byte at &0D0D
9FB9 STA nmi_jmp_lo ; Store handler low byte at &0D0C
9FBC LDA romsel_copy ; Restore NFS ROM bank
9FBE STA romsel ; Page in via hardware latch
9FC1 PLA ; Restore Y from stack
9FC2 TAY ; Transfer ROM bank to Y
9FC3 PLA ; Restore A from stack
9FC4 BIT video_ula_control ; INTON: re-enable NMIs
9FC7 RTI ; Return from interrupt
9FC8 EQUS "Brian,Hugo,Jes and Roger"

Print byte as two hex digits

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

On EntryAbyte to print as two hex digits
On ExitApreserved (original byte)
Xcorrupted (by OSASCI)
9FE0 .print_hex←2← 8C8E JSR← 8D67 JSR
PHA ; Save original byte for low nibble
9FE1 LSR ; Shift high nibble right (4x LSR)
9FE2 LSR ; Shift high nibble to low
9FE3 LSR ; Shift high nibble to low
9FE4 LSR ; Shift high nibble to low
9FE5 JSR print_hex_nibble ; Print high nibble as hex
9FE8 PLA ; Restore byte; fall through for low nibble
fall through ↓

Print single hex nibble

Converts the low nibble of A to ASCII hex ('0'-'9' or 'A'-'F') and prints via OSASCI. Returns with carry set.

9FE9 .print_hex_nibble←1← 9FE5 JSR
AND #&0f ; Mask to low nibble (0-F)
9FEB CMP #&0a ; Digit A-F?
9FED BCC add_ascii_base ; No: skip letter offset
9FEF ADC #6 ; A-F: ADC #6 + ADC #&30 + C = &41-&46
9FF1 .add_ascii_base←1← 9FED BCC
ADC #&30 ; Add ASCII '0' base (with carry)
9FF3 JSR osasci ; Write character
9FF6 SEC ; C=1: callers use SEC as sentinel
9FF7 RTS ; Return
9FF8 EQUB &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF