Acorn NFS 3.65

Updated 31 Mar 2026

← All Acorn NFS and Acorn ANFS versions

; Sideways ROM header
; NFS ROM 3.65 disassembly (Acorn Econet filing system)
; ====================================================
8000 .rom_header←2← 04E6 LDA← 9B64 JSR
.language_entry←2← 04E6 LDA← 9B64 JSR
.pydis_start←2← 04E6 LDA← 9B64 JSR
JMP language_handler ; JMP language_handler
8003 .service_entry←1← 04F5 LDY
JMP service_handler ; JMP service_handler
8006 .rom_type←1← 04DA AND
EQUB &82 ; ROM type: service + language
8007 .copyright_offset←1← 04E2 LDX
EQUB copyright - rom_header ; Copyright string offset from &8000
8008 .binary_version←2← 835C CMP← 8365 LDA
EQUB &03 ; Binary version number
8009 .title
EQUS "NET" ; ROM title string "NET"
800C .copyright
EQUB &00 ; Null terminator before copyright
; 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.
800D .copyright_string
EQUS "(C)ROFF" ; Copyright string "(C)ROFF"
; Error message offset table (9 entries).
; Each byte is a Y offset into error_msg_table.
; Entry 0 (Y=0, "Line Jammed") doubles as the
; copyright string null terminator.
; Indexed by TXCB status (AND #7), or hardcoded 8.
8014 .error_offsets←1← 8503 LDY
EQUB &00 ; "Line Jammed"
8015 EQUB &0D ; "Net Error"
8016 EQUB &18 ; "Not listening"
8017 EQUB &27 ; "No Clock"
8018 EQUB &31 ; "Escape"
8019 EQUB &31 ; "Escape"
801A EQUB &31 ; "Escape"
801B EQUB &39 ; "Bad Option"
801C EQUB &45 ; "No reply"
; Four bytes with unknown purpose.
801D EQUB &01 ; Purpose unknown
801E EQUB &00 ; Purpose unknown
801F EQUB &65 ; Purpose unknown
8020 EQUB &03 ; Purpose unknown
; Dispatch table: low bytes of (handler_address - 1)
; Each entry stores the low byte of a handler address minus 1,
; for use with the PHA/PHA/RTS dispatch trick at &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
; &8040-&8045, immediately before the hi bytes. Their hi
; bytes are at &8065-&806A, after dispatch_0_hi.
8021 .dispatch_0_lo
EQUB <(return_1-1) ; lo - Svc 0: already claimed (no-op)
8022 EQUB <(svc_1_abs_workspace-1) ; lo - Svc 1: absolute workspace
8023 EQUB <(svc_2_private_workspace-1) ; lo - Svc 2: private workspace
8024 EQUB <(svc_3_autoboot-1) ; lo - Svc 3: auto-boot
8025 EQUB <(svc_4_star_command-1) ; lo - Svc 4: unrecognised star command
8026 EQUB <(svc5_irq_check-1) ; lo - Svc 5: unrecognised interrupt
8027 EQUB <(return_1-1) ; lo - Svc 6: BRK (no-op)
8028 EQUB <(dispatch_net_cmd-1) ; lo - Svc 7: unrecognised OSBYTE
8029 EQUB <(svc_8_osword-1) ; lo - Svc 8: unrecognised OSWORD
802A EQUB <(svc_9_help-1) ; lo - Svc 9: *HELP
802B EQUB <(return_1-1) ; lo - Svc 10: static workspace (no-op)
802C EQUB <(econet_restore-1) ; lo - Svc 11: NMI release (reclaim NMIs)
802D EQUB <(econet_save-1) ; lo - Svc 12: NMI claim (save NMI state)
802E EQUB <(svc_13_select_nfs-1) ; lo - Svc 13: select NFS (intercepted before dispatch)
802F EQUB <(lang_0_insert_remote_key-1) ; lo - Lang 0: no language / Tube
8030 EQUB <(lang_1_remote_boot-1) ; lo - Lang 1: normal startup
8031 EQUB <(lang_2_save_palette_vdu-1) ; lo - Lang 2: softkey byte (Electron)
8032 EQUB <(lang_3_execute_at_0100-1) ; lo - Lang 3: softkey length (Electron)
8033 EQUB <(lang_4_remote_validated-1) ; lo - Lang 4: remote validated
8034 EQUB <(fscv_0_opt_entry-1) ; lo - FSCV 0: *OPT
8035 EQUB <(fscv_1_eof-1) ; lo - FSCV 1: EOF check
8036 EQUB <(fscv_2_star_run-1) ; lo - FSCV 2: */ (run)
8037 EQUB <(fscv_3_star_cmd-1) ; lo - FSCV 3: unrecognised star command
8038 EQUB <(fscv_2_star_run-1) ; lo - FSCV 4: *RUN
8039 EQUB <(fscv_5_cat-1) ; lo - FSCV 5: *CAT
803A EQUB <(fscv_6_shutdown-1) ; lo - FSCV 6: shutdown
803B EQUB <(fscv_7_read_handles-1) ; lo - FSCV 7: read handle range
803C EQUB <(fsreply_0_print_dir-1) ; lo - FS reply: print directory name
803D EQUB <(fsreply_1_copy_handles_boot-1) ; lo - FS reply: copy handles + boot
803E EQUB <(fsreply_2_copy_handles-1) ; lo - FS reply: copy handles
803F EQUB <(fsreply_3_set_csd-1) ; lo - FS reply: set CSD handle
8040 EQUB <(fsreply_4_notify_exec-1) ; lo - FS reply: notify + execute
8041 EQUB <(fsreply_5_set_lib-1) ; lo - FS reply: set library handle
8042 EQUB <(net_1_read_handle-1) ; lo - *NET1: read handle from packet
8043 EQUB <(net_2_read_handle_entry-1) ; lo - *NET2: read handle from workspace
8044 EQUB <(net_3_close_handle-1) ; lo - *NET3: close handle
8045 EQUB <(net_4_resume_remote-1) ; lo - *NET4: resume remote
8046 .dispatch_0_hi
EQUB >(return_1-1) ; hi - Svc 0: already claimed (no-op)
8047 EQUB >(svc_1_abs_workspace-1) ; hi - Svc 1: absolute workspace
8048 EQUB >(svc_2_private_workspace-1) ; hi - Svc 2: private workspace
8049 EQUB >(svc_3_autoboot-1) ; hi - Svc 3: auto-boot
804A EQUB >(svc_4_star_command-1) ; hi - Svc 4: unrecognised star command
804B EQUB >(svc5_irq_check-1) ; hi - Svc 5: unrecognised interrupt
804C EQUB >(return_1-1) ; hi - Svc 6: BRK (no-op)
804D EQUB >(dispatch_net_cmd-1) ; hi - Svc 7: unrecognised OSBYTE
804E EQUB >(svc_8_osword-1) ; hi - Svc 8: unrecognised OSWORD
804F EQUB >(svc_9_help-1) ; hi - Svc 9: *HELP
8050 EQUB >(return_1-1) ; hi - Svc 10: static workspace (no-op)
8051 EQUB >(econet_restore-1) ; hi - Svc 11: NMI release (reclaim NMIs)
8052 EQUB >(econet_save-1) ; hi - Svc 12: NMI claim (save NMI state)
8053 EQUB >(svc_13_select_nfs-1) ; hi - Svc 13: select NFS (intercepted before dispatch)
8054 EQUB >(lang_0_insert_remote_key-1) ; hi - Lang 0: no language / Tube
8055 EQUB >(lang_1_remote_boot-1) ; hi - Lang 1: normal startup
8056 EQUB >(lang_2_save_palette_vdu-1) ; hi - Lang 2: softkey byte (Electron)
8057 EQUB >(lang_3_execute_at_0100-1) ; hi - Lang 3: softkey length (Electron)
8058 EQUB >(lang_4_remote_validated-1) ; hi - Lang 4: remote validated
8059 EQUB >(fscv_0_opt_entry-1) ; hi - FSCV 0: *OPT
805A EQUB >(fscv_1_eof-1) ; hi - FSCV 1: EOF check
805B EQUB >(fscv_2_star_run-1) ; hi - FSCV 2: */ (run)
805C EQUB >(fscv_3_star_cmd-1) ; hi - FSCV 3: unrecognised star command
805D EQUB >(fscv_2_star_run-1) ; hi - FSCV 4: *RUN
805E EQUB >(fscv_5_cat-1) ; hi - FSCV 5: *CAT
805F EQUB >(fscv_6_shutdown-1) ; hi - FSCV 6: shutdown
8060 EQUB >(fscv_7_read_handles-1) ; hi - FSCV 7: read handle range
8061 EQUB >(fsreply_0_print_dir-1) ; hi - FS reply: print directory name
8062 EQUB >(fsreply_1_copy_handles_boot-1) ; hi - FS reply: copy handles + boot
8063 EQUB >(fsreply_2_copy_handles-1) ; hi - FS reply: copy handles
8064 EQUB >(fsreply_3_set_csd-1) ; hi - FS reply: set CSD handle
8065 EQUB >(fsreply_4_notify_exec-1) ; hi - FS reply: notify + execute
8066 EQUB >(fsreply_5_set_lib-1) ; hi - FS reply: set library handle
8067 EQUB >(net_1_read_handle-1) ; hi - *NET1: read handle from packet
8068 EQUB >(net_2_read_handle_entry-1) ; hi - *NET2: read handle from workspace
8069 EQUB >(net_3_close_handle-1) ; hi - *NET3: close handle
806A 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 &80E3. 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 (&8E6A): read file handle from received packet (net_1_read_handle)

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

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

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

806B .dispatch_net_cmd
LDA osbyte_a_copy ; Read command character following *NET
806D SBC #&31 ; Subtract ASCII '1' to get 0-based command index
806F CMP #4 ; Command index >= 4: invalid *NET sub-command
8071 BCS svc_dispatch_range ; Out of range: return via c80e3/RTS
8073 TAX ; X = command index (0-3)
8074 LDA #0 ; Clear &A9 (used by dispatch)
8076 STA svc_state ; Store zero to &A9
8078 TYA ; Preserve A before dispatch
8079 LDY #&21 ; Y=&21: base offset for *NET commands (index 33+)
807B BNE dispatch ; ALWAYS branch to dispatch ALWAYS branch
807D .skip_cmd_spaces←1← 8082 BEQ
INY ; Advance past matched command text
fall through ↓

"I AM" command handler

Dispatched from the command match table when the user types "*I AM <station>" or "*I AM <network>.<station>". Also used as the station number parser for "*NET <network>.<station>". Skips leading spaces, then calls parse_decimal 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.

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

80BD .forward_star_cmd
JSR infol2 ; Copy command text to FS buffer
80C0 TAY ; Y=function code for HDRFN
80C1 .prepare_cmd_dispatch←1← 809D BEQ
JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
80C4 LDX fs_cmd_csd ; X=depends on function
80C7 BEQ return_1 ; CSD handle zero: not logged in
80C9 LDA fs_cmd_data ; A=function code (0-7)
80CC LDY #&17 ; Y=depends on function
80CE 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)
80D0 .fscv_handler
JSR save_fscv_args_with_ptrs ; Store A/X/Y in FS workspace
80D3 CMP #8 ; FSCV function >= 8?
80D5 BCS return_1 ; Function code >= 8? Return (unsupported)
80D7 TAX ; X = function code for dispatch
80D8 TYA ; Save Y (command text ptr hi)
80D9 LDY #&13 ; Y=&13: base offset for FSCV dispatch (indices 20+)
80DB 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).

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

80E3 .dispatch←5← 807B BNE← 80CE BNE← 80DB BNE← 80E5 BPL← 8194 JSR
INX ; Add base offset Y to index X (loop: X += Y+1)
80E4 DEY ; Decrement base offset counter
80E5 BPL dispatch ; Loop until Y exhausted
80E7 TAY ; Y=&FF (no further use)
80E8 LDA dispatch_0_hi-1,x ; Load high byte of (handler - 1) from table
80EB PHA ; Push high byte onto stack
80EC LDA dispatch_0_lo-1,x ; Load low byte of (handler - 1) from table
80EF PHA ; Push low byte onto stack
80F0 LDX fs_options ; Restore X (fileserver options) for use by handler
80F2 .return_1←3← 80C7 BEQ← 80D5 BCS← 80DF BCS
RTS ; RTS pops address, adds 1, jumps to handler
80F3 .service_handler←1← 8003 JMP
PHA ; Save service call number
80F4 CMP #1 ; Only probe ADLC on service 1 (workspace claim)
80F6 BNE check_disable_flag ; Not service 1: skip probe
80F8 LDA econet_control1_or_status1 ; Probe ADLC SR1: non-zero = absent (bus noise)
80FB AND #&ed ; Mask SR1 status bits (ignore bits 4,1)
80FD BNE set_adlc_disable ; Non-zero: ADLC absent, set disable flag
80FF LDA econet_control23_or_status2 ; Probe ADLC SR2 if SR1 was all zeros
8102 AND #&db ; Mask SR2 status bits (ignore bits 5,2)
8104 BEQ check_disable_flag ; Both zero: ADLC present, skip
8106 .set_adlc_disable←1← 80FD BNE
ROL rom_ws_table,x ; Set bit 7 of per-ROM workspace = disable flag
8109 SEC ; SEC for ROR to set bit 7
810A ROR rom_ws_table,x ; Rotate carry into bit 7 of workspace
810D .check_disable_flag←2← 80F6 BNE← 8104 BEQ
LDA rom_ws_table,x ; Read back flag; ASL puts bit 7 into carry
8110 ASL ; C into bit 7 of A
8111 PLA ; Restore service call number
8112 BMI check_svc_high ; Service >= &80: always handle (Tube/init)
8114 BCS svc_unhandled_return ; C=1 (no ADLC): disable ROM, skip
fall through ↓

Service handler entry

On service 1 only, probes ADLC status registers SR1 (&FEA0) and SR2 (&FEA1) to detect whether Econet hardware is present. Non-zero reads indicate bus noise from absent hardware; sets bit 7 of per-ROM workspace as a disable flag. For services < &80, the flag causes an early return (disabling this ROM). Services >= &80 (&FE, &FF) are always handled regardless.

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

8116 .check_svc_high←1← 8112 BMI
.service_handler_entry←1← 8112 BMI
CMP #&fe ; Service >= &FE?
8118 BCC check_svc_12 ; Service < &FE: skip to &12/dispatch check
811A BNE init_vectors_and_copy ; Service &FF: full init (vectors + RAM copy)
811C CPY #0 ; Service &FE: Y=0?
811E BEQ check_svc_12 ; Y=0: no Tube data, skip to &12 check
8120 LDX #6 ; X=6 extra pages for char definitions
8122 LDA #osbyte_explode_chars ; OSBYTE &14: explode character RAM
8124 JSR osbyte ; Explode character definition RAM (six extra pages), can redefine all characters 32-255 (X=6)
8127 .poll_tube_ready←2← 812A BPL← 8134 JMP
BIT tube_status_1_and_tube_control ; Poll Tube status register 1
812A BPL poll_tube_ready ; Loop until Tube ready (bit 7 set)
812C LDA tube_data_register_1 ; Read byte from Tube data register 1
812F BEQ tube_chars_done ; Zero byte: Tube transfer complete
8131 JSR oswrch ; Send Tube char to screen via OSWRCH Write character
8134 JMP poll_tube_ready ; Loop for next Tube byte
8137 .init_vectors_and_copy←1← 811A BNE
LDA #&ad ; EVNTV low = &AD (event handler address)
8139 STA evntv ; Set EVNTV low byte at &0220
813C LDA #6 ; EVNTV high = &06 (page 6)
813E STA evntv+1 ; Set EVNTV high byte at &0221
8141 LDA #&16 ; BRKV low = &16 (NMI workspace)
8143 STA brkv ; Set BRKV low byte at &0202
8146 LDA #0 ; BRKV high = &00 (zero page)
8148 STA brkv+1 ; Set BRKV high byte at &0203
814B LDA #&8e ; Tube control register init value &8E
814D STA tube_status_1_and_tube_control ; Write to Tube control register
8150 LDY #0 ; Y=0: copy 256 bytes per page
; Copy NMI handler code from ROM to RAM pages &04-&06
8152 .cloop←1← 8165 BNE
LDA reloc_p4_src,y ; Load ROM byte from page &93
8155 STA tube_code_page4,y ; Store to page &04 (Tube code)
8158 LDA reloc_p5_src,y ; Load ROM byte from page &94
815B STA tube_dispatch_table,y ; Store to page &05 (dispatch table)
815E LDA reloc_p6_src,y ; Load ROM byte from page &95
8161 STA tube_page6_start,y ; Store to page &06
8164 DEY ; DEY wraps 0 -> &FF on first iteration
8165 BNE cloop ; Loop until 256 bytes copied per page
8167 JSR tube_post_init ; Run post-init routine in copied code
816A LDX #&60 ; X=&60: copy 97 bytes (&60..&00)
; Copy NMI workspace initialiser from ROM to &0016-&0076
816C .copy_nmi_workspace←1← 8172 BPL
LDA reloc_zp_src,x ; Load NMI workspace init byte from ROM
816F STA nmi_workspace_start,x ; Store to zero page &16+X
8171 DEX ; Next byte
8172 BPL copy_nmi_workspace ; Loop until all workspace bytes copied
8174 .tube_chars_done←1← 812F BEQ
LDA #0 ; A=0: fall through to service &12 check
8176 .check_svc_12←2← 8118 BCC← 811E BEQ
CMP #&12 ; Is this service &12 (select FS)?
8178 BNE not_svc_12_nfs ; No: check if service < &0D
817A CPY #5 ; Service &12: Y=5 (NFS)?
817C BNE not_svc_12_nfs ; Not NFS: check if service < &0D
817E LDA #&0d ; A=&0D: dispatch index for svc_13_select_nfs
8180 BNE do_svc_dispatch ; ALWAYS branch to dispatch ALWAYS branch
8182 .not_svc_12_nfs←2← 8178 BNE← 817C BNE
CMP #&0d ; Service >= &0D?
8184 .svc_unhandled_return←1← 8114 BCS
BCS return_2 ; Service >= &0D: not handled, return
8186 .do_svc_dispatch←1← 8180 BNE
TAX ; X = service number (dispatch index)
8187 LDA svc_state ; Save &A9 (current service state)
8189 PHA ; Push saved &A9
818A LDA ws_page ; Save &A8 (workspace page number)
818C PHA ; Push saved &A8
818D STX svc_state ; Store service number to &A9
818F STY ws_page ; Store Y (page number) to &A8
8191 TYA ; A = Y for dispatch table offset
8192 LDY #0 ; Y=0: base offset for service dispatch
8194 JSR dispatch ; Dispatch to service handler
8197 LDX svc_state ; Recover service claim status from &A9
8199 PLA ; Restore saved &A8 from stack
819A STA ws_page ; Write back &A8
fall through ↓

Service dispatch epilogue

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

819C .svc_dispatch_epilogue
PLA ; Restore saved A from service dispatch
819D STA svc_state ; Save to workspace &A9
819F TXA ; Return ROM number in A
81A0 LDX romsel_copy ; Restore X from MOS ROM select copy
81A2 .return_2←1← 8184 BCS
RTS ; Return to MOS service handler
81A3 .svc_4_star_command
LDX #8 ; ROM offset for "ROFF" (copyright suffix)
81A5 JSR match_rom_string ; Try matching *ROFF command
81A8 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.

81AA .net_4_resume_remote
LDY #4 ; Y=4: offset of keyboard disable flag
81AC LDA (net_rx_ptr),y ; Read flag from RX buffer
81AE BEQ skip_kbd_reenable ; Zero: keyboard not disabled, skip
81B0 LDA #0 ; A=0: value to clear flag and re-enable
81B2 TAX ; X=&00
81B3 STA (net_rx_ptr),y ; Clear keyboard disable flag in buffer
81B5 TAY ; Y=&00
81B6 LDA #osbyte_read_write_econet_keyboard_disable ; OSBYTE &C9: Econet keyboard disable
81B8 JSR osbyte ; Re-enable keyboard (X=0, Y=0) Enable keyboard (for Econet)
81BB LDA #&0a ; Function &0A: remote operation complete
81BD JSR setup_tx_and_send ; Send notification to controlling station
81C0 .clear_osbyte_ce_cf←1← 84BC JSR
STX nfs_workspace ; Save X (return value from TX)
81C2 LDA #&ce ; OSBYTE &CE: first system mask to reset
81C4 .clear_osbyte_masks←1← 81CF BEQ
LDX nfs_workspace ; Restore X for OSBYTE call
81C6 LDY #&7f ; Y=&7F: AND mask (clear bit 7)
81C8 JSR osbyte ; Reset system mask byte
81CB ADC #1 ; Advance to next OSBYTE (&CE -> &CF)
81CD CMP #&d0 ; Reached &D0? (past &CF)
81CF .cmd_name_matched
BEQ clear_osbyte_masks ; No: reset &CF too
81D1 .skip_kbd_reenable←1← 81AE BEQ
LDA #0 ; A=0: clear remote state
81D3 STA svc_state ; Clear &A9 (service dispatch state)
81D5 .skpspi
STA nfs_workspace ; Clear workspace byte
81D7 RTS ; Return
81D8 .match_net_cmd←1← 81A8 BNE
LDX #1 ; X=1: ROM offset for "NET" match
81DA JSR match_rom_string ; Try matching *NET command
81DD BNE restore_ws_return ; No match: return unclaimed
fall through ↓

Select NFS as active filing system (INIT)

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

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

Service 9: *HELP

Prints the ROM identification string using print_inline.

On ExitYworkspace page number (from ws_page)
81F6 .svc_9_help
JSR print_inline ; Print ROM identification string
81F9 EQUS ".NFS 3.65." ; "NFS 3.65" version string + CRs
8203 .restore_ws_return←2← 81DD BNE← 8218 BNE
LDY ws_page ; Restore Y (workspace page number)
8205 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).

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

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

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

8220 .print_station_info←1← 8214 BMI
JSR print_inline ; Print 'Econet Station ' banner
8223 EQUS "Econet Station " ; "Econet Station " inline string data
8232 LDY #&14 ; Y=&14: station number offset in RX buf
8234 LDA (net_rx_ptr),y ; Load station number
8236 JSR print_decimal ; Print as 3-digit decimal
8239 LDA #&20 ; BIT trick: bit 5 of SR2 = clock present
823B BIT econet_control23_or_status2 ; Test DCD: clock present if bit 5 clear
823E .dofsl1
BEQ skip_no_clock_msg ; Clock present: skip warning
8240 JSR print_inline ; Print ' No Clock' warning
8243 EQUS " No Clock" ; " No Clock" inline string data
824C NOP ; NOP (padding after inline string)
824D .skip_no_clock_msg←1← 823E BEQ
JSR print_inline ; Print two CRs (blank line)
8250 EQUS ".." ; CR CR inline string data
fall through ↓

Initialise filing system vectors

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

8252 .init_fs_vectors←1← 81F4 BEQ
LDY #&0d ; Copy 14 bytes: FS vector addresses to FILEV-FSCV
8254 .copy_fs_vectors←1← 825B BPL
LDA fs_dispatch_addrs,y ; Load extended vector dispatch address
8257 STA filev,y ; Write to FILEV-FSCV vector table
825A DEY ; Next byte (descending)
825B BPL copy_fs_vectors ; Loop until all 14 bytes copied
825D JSR setup_rom_ptrs_netv ; Read ROM ptr table addr, install NETV
8260 LDY #&1b ; Install 7 handler entries in ROM ptr table
8262 LDX #7 ; 7 FS vectors to install
8264 JSR store_rom_ptr_pair ; Install each 3-byte vector entry
8267 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 l00a8 is zero (soft break — RXCBs already initialised), 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 &8C4B). The "I." prefix triggers the catch-all entry which forwards the command to the fileserver. Falls through to run_fscv_cmd.

8269 .issue_vectors_claimed←1← 81E5 JSR
LDA #osbyte_issue_service_request ; A=&8F: issue service request
826B LDX #&0f ; X=&0F: 'vectors claimed' service
826D JSR osbyte ; Issue paged ROM service call, Reason X=15 - Vectors claimed
8270 LDX #&0a ; X=&0A: service &0A
8272 JSR osbyte ; Issue service &0A
8275 LDX ws_page ; Non-zero after hard reset: skip auto-boot
8277 BNE return_3 ; Non-zero: skip auto-boot
8279 LDX #&80 ; X = lo byte of auto-boot string at &8292
fall through ↓

Run FSCV command from ROM

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

; 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.
827B .run_fscv_cmd←2← 8327 LDA← 832D LDA
LDY #&82 ; Y=&82: ROM page high byte
827D JMP fscv_3_star_cmd ; Execute command string at (X, Y)
8280 EQUS "I .BOO"

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.

8286 .fs_vector_addrs
EQUB &54, &0D ; Auto-boot string tail / vector table header
8288 .fs_dispatch_addrs←1← 8254 LDA
EQUW &FF1B ; FILEV dispatch (&FF1B)
828A EQUW &FF1E ; ARGSV dispatch (&FF1E)
828C EQUW &FF21 ; BGETV dispatch (&FF21)
828E EQUW &FF24 ; BPUTV dispatch (&FF24)
8290 EQUW &FF27 ; GBPBV dispatch (&FF27)
8292 EQUW &FF2A ; FINDV dispatch (&FF2A)
8294 EQUW &FF2D ; FSCV dispatch (&FF2D)
8296 EQUW &86FA ; FILEV handler (&86FA)
8298 EQUB &4A ; (ROM bank — not read)
8299 EQUW &8956 ; ARGSV handler (&8956)
829B EQUB &44 ; (ROM bank — not read)
829C EQUW &8551 ; BGETV handler (&8551)
829E EQUB &57 ; (ROM bank — not read)
829F EQUW &8401 ; BPUTV handler (&8401)
82A1 EQUB &42 ; (ROM bank — not read)
82A2 EQUW &8A60 ; GBPBV handler (&8A60)
82A4 EQUB &41 ; (ROM bank — not read)
82A5 EQUW &89C6 ; FINDV handler (&89C6)
82A7 EQUB &52 ; (ROM bank — not read)
82A8 EQUW &80D0 ; FSCV handler (&80D0)

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)
82AA .svc_1_abs_workspace
CPY #&10 ; Already at page &10 or above?
82AC BCS return_3 ; Yes: nothing to claim
82AE LDY #&10 ; Claim pages &0D-&0F (3 pages)
82B0 .return_3←2← 8277 BNE← 82AC BCS
RTS ; Return (workspace claim done)
82B1 EQUB &83, &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 (no server selected) - Sets printer server station to &FE (no server selected) - 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)
82B3 .svc_2_private_workspace
STY net_rx_ptr_hi ; RX buffer page = first claimed page
82B5 INY ; Advance to next page
82B6 STY nfs_workspace_hi ; Workspace page = second claimed page
82B8 LDA #0 ; A=0 for clearing workspace
82BA LDY #4 ; Y=4: remote status offset
82BC STA (net_rx_ptr),y ; Clear status byte in net receive buffer
82BE LDY #&ff ; Y=&FF: used for later iteration
82C0 STA net_rx_ptr ; Clear RX ptr low byte
82C2 STA nfs_workspace ; Clear workspace ptr low byte
82C4 STA ws_page ; Clear RXCB iteration counter
82C6 STA tx_clear_flag ; Clear TX semaphore (no TX in progress)
82C9 TAX ; X=0 for OSBYTE X=&00
82CA LDA #osbyte_read_write_last_break_type ; OSBYTE &FD: read type of last reset
82CC JSR osbyte ; Read type of last reset
82CF TXA ; X = break type from OSBYTE result X=value of type of last reset
82D0 BEQ read_station_id ; Soft break (X=0): skip FS init
82D2 LDY #&15 ; Y=&15: printer station offset in RX buffer
82D4 LDA #&fe ; &FE = no server selected
82D6 STA fs_server_stn ; Station &FE = no server selected
82D9 STA (net_rx_ptr),y ; Store &FE at printer station offset
82DB LDA #0 ; A=0 for clearing workspace fields
82DD STA fs_server_net ; Clear network number
82E0 STA prot_status ; Clear protection status
82E3 STA fs_messages_flag ; Clear message flag
82E6 STA fs_boot_option ; Clear boot option
82E9 INY ; Y=&16
82EA STA (net_rx_ptr),y ; Clear net number at RX buffer offset &16
82EC LDY #3 ; Init printer server: station &FE, net 0
82EE STA (nfs_workspace),y ; Store net 0 at workspace offset 3
82F0 DEY ; Y=2: printer station offset Y=&02
82F1 LDA #&eb ; &FE = no printer server
82F3 STA (nfs_workspace),y ; Store &FE at printer station in workspace
82F5 .init_rxcb_entries←1← 8302 BNE
LDA ws_page ; Load RXCB counter
82F7 JSR calc_handle_offset ; Convert to workspace byte offset
82FA BCS read_station_id ; C=1: past max handles, done
82FC LDA #&3f ; Mark RXCB as available
82FE STA (nfs_workspace),y ; Write &3F flag to workspace
8300 INC ws_page ; Next RXCB number
8302 BNE init_rxcb_entries ; Loop for all RXCBs
8304 .read_station_id←2← 82D0 BEQ← 82FA BCS
LDA station_id_disable_net_nmis ; Read station ID (also INTOFF)
8307 LDY #&14 ; Y=&14: station ID offset in RX buffer
8309 STA (net_rx_ptr),y ; Store our station number
830B JSR init_adlc_hw ; Initialise ADLC hardware
830E LDA #&40 ; Enable user-level RX (LFLAG=&40)
8310 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=&9080, rom=current) into the ROM pointer table at offset &36, installing osword_dispatch as the NETV handler.

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

833F .fscv_6_shutdown
LDY #&1d ; Copy 10 bytes: FS state to workspace backup
8341 .fsdiel←1← 8349 BNE
LDA fs_state_deb,y ; Load FS state byte at offset Y
8344 STA (net_rx_ptr),y ; Store to workspace backup area
8346 DEY ; Next byte down
8347 CPY #&14 ; Offsets &15-&1D: server, handles, OPT, etc.
8349 BNE fsdiel ; Loop for offsets &1D..&15
834B LDA #osbyte_close_spool_exec ; A=&77: OSBYTE close spool/exec
834D JMP osbyte ; Close any *SPOOL and *EXEC files
8350 .match_rom_string←2← 81A5 JSR← 81DA JSR
LDY ws_page ; Y = saved text pointer offset
8352 .match_cmd_chars←1← 8363 BNE
LDA (os_text_ptr),y ; Load next input character
8354 CMP #&2e ; Is it a '.' (abbreviation)?
8356 BEQ skip_space_next ; Yes: skip to space skipper (match)
8358 AND #&df ; Force uppercase (clear bit 5)
835A BEQ check_rom_end ; Input char is NUL/space: check ROM byte
835C CMP binary_version,x ; Compare with ROM string byte
835F BNE check_rom_end ; Mismatch: check if ROM string ended
8361 INY ; Advance input pointer
8362 INX ; Advance ROM string pointer
8363 BNE match_cmd_chars ; Continue matching (always taken)
8365 .check_rom_end←2← 835A BEQ← 835F BNE
LDA binary_version,x ; Load ROM string byte at match point
8368 BEQ skip_spaces ; Zero = end of ROM string = full match
836A RTS ; Non-zero = partial/no match; Z=0
836B .skip_space_next←2← 8356 BEQ← 8370 BEQ
INY ; Skip this space
fall through ↓

Skip spaces and test for end of line

Advances Y past leading spaces in the text at (os_text_ptr),Y. Returns Z=1 if the next non-space character is CR (end of line), Z=0 otherwise with A holding the character.

On EntryYoffset into (os_text_ptr) buffer
On ExitAcharacter EOR &0D (0 if CR)
Yoffset of first non-space character
Zset if end of line (CR)
836C .skip_spaces←2← 8368 BEQ← 8DF0 JSR
LDA (os_text_ptr),y ; Load next input character
836E CMP #&20 ; Is it a space?
8370 BEQ skip_space_next ; Yes: keep skipping
8372 EOR #&0d ; XOR with CR: Z=1 if end of line
8374 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.

8375 .init_tx_reply_port←1← 83E2 JSR
LDA #&90 ; A=&90: FS reply port (PREPLY)
8377 .init_tx_ctrl_port←1← 888A JSR
JSR init_tx_ctrl_block ; Init TXCB from template
837A STA txcb_port ; Store port number in TXCB
837C LDA #3 ; Control byte: 3 = transmit
837E STA txcb_start ; Store control byte in TXCB
8380 DEC txcb_ctrl ; Decrement TXCB flag to arm TX
8382 RTS ; Return after port setup

Initialise TX control block at &00C0 from template

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

On ExitApreserved
Y&FF (decremented past 0)
8383 .init_tx_ctrl_block←4← 8377 JSR← 83D1 JSR← 841D JSR← 8FF9 LDA
PHA ; Preserve A across call
8384 LDY #&0b ; Copy 12 bytes (Y=11..0)
8386 .fstxl1←1← 8397 BPL
LDA tx_ctrl_template,y ; Load template byte
8389 STA txcb_ctrl,y ; Store to TX control block at &00C0
838C CPY #2 ; Y < 2: also copy FS server station/network
838E BPL fstxl2 ; Skip station/network copy for Y >= 2
8390 LDA fs_server_stn,y ; Load FS server station (Y=0) or network (Y=1)
8393 STA txcb_dest,y ; Store to dest station/network at &00C2
8396 .fstxl2←1← 838E BPL
DEY ; Next byte (descending)
8397 BPL fstxl1 ; Loop until all 12 bytes copied
8399 PLA ; Restore A
839A 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.

839B .tx_ctrl_template←1← 8386 LDA
EQUB &80, &99, ; Control flag Port (FS command = &99) &00, &00, ; Buffer start low Buffer start high (page &00, &0F ; &0F)
83A1 .tx_ctrl_upper←3← 890A BIT← 89E9 BIT← 9186 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
83A7 .prepare_cmd_with_flag←1← 8AB1 JSR
PHA ; Save flag byte for command
83A8 SEC ; C=1: include flag in FS command
83A9 BCS store_fs_hdr_fn ; ALWAYS branch to prepare_fs_cmd ALWAYS branch
83AB .prepare_cmd_clv←2← 8717 JSR← 87BD JSR
CLV ; V=0: command has no flag byte
83AC 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 &8C4B for "BYE".

83AE .bye_handler
LDA #osbyte_close_spool_exec ; A=&77: OSBYTE close spool/exec
83B0 JSR osbyte ; Close any *SPOOL and *EXEC files
83B3 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)
83B5 .prepare_fs_cmd←12← 80C1 JSR← 88AF JSR← 8925 JSR← 8971 JSR← 8998 JSR← 8A0F JSR← 8A36 JSR← 8B0C JSR← 8BC7 JSR← 8C73 JSR← 8CAA JSR← 8D15 JSR
CLV ; V=0: standard FS command path
83B6 .init_tx_ctrl_data←2← 890D JSR← 89EC JSR
.prepare_fs_cmd_v←2← 890D JSR← 89EC JSR
LDA fs_urd_handle ; Copy URD handle from workspace to buffer
83B9 STA fs_cmd_urd ; Store URD at &0F02
83BC .store_fs_hdr_clc←1← 83AC BVC
CLC ; CLC: no byte-stream path
83BD .store_fs_hdr_fn←1← 83A9 BCS
STY fs_cmd_y_param ; Store function code at &0F01
83C0 LDY #1 ; Y=1: copy CSD (offset 1) then LIB (offset 0)
83C2 .copy_dir_handles←1← 83C9 BPL
LDA fs_csd_handle,y ; Copy CSD and LIB handles to command buffer
83C5 STA fs_cmd_csd,y ; Store at &0F03 (CSD) and &0F04 (LIB)
83C8 DEY ; Y=function code
83C9 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") 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
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)
83CB .build_send_fs_cmd←1← 8B65 JSR
PHP ; Save carry (FS path vs byte-stream)
83CC LDA #&90 ; Reply port &90 (PREPLY)
83CE STA fs_cmd_type ; Store at &0F00 (HDRREP)
83D1 JSR init_tx_ctrl_block ; Copy TX template to &00C0
83D4 TXA ; A = X (buffer extent)
83D5 ADC #5 ; HPTR = header (5) + data (X) bytes to send
83D7 STA txcb_end ; Store to TXCB end-pointer low
83D9 PLP ; Restore carry flag
83DA BCS dofsl5 ; C=1: byte-stream path (BSXMIT)
83DC PHP ; Save flags for send_fs_reply_cmd
83DD JSR setup_tx_ptr_c0 ; Point net_tx_ptr to &00C0; transmit
83E0 PLP ; Restore flags
83E1 .send_fs_reply_cmd←2← 87CA JSR← 8AE9 JSR
PHP ; Save flags (V flag state)
83E2 JSR init_tx_reply_port ; Set up RX wait for FS reply
83E5 JSR waitfs ; Transmit and wait (BRIANX)
83E8 PLP ; Restore flags
83E9 .dofsl7←1← 83FF BCC
INY ; Y=1: skip past command code byte
83EA LDA (txcb_start),y ; Load return code from FS reply
83EC TAX ; X = return code
83ED BEQ return_dofsl7 ; Zero: success, return
83EF BVC check_fs_error ; V=0: standard path, error is fatal
83F1 ADC #&2a ; ADC #&2A: test for &D6 (not found)
83F3 .check_fs_error←1← 83EF BVC
BNE store_fs_error ; Non-zero: hard error, go to FSERR
83F5 .return_dofsl7←1← 83ED BEQ
RTS ; Return (success or soft &D6 error)
83F6 .dofsl5←1← 83DA BCS
PLA ; Discard saved flags from stack
83F7 LDX #&c0 ; X=&C0: TXCB address for byte-stream TX
83F9 INY ; Y++ past command code
83FA JSR econet_tx_retry ; Byte-stream transmit with retry
83FD STA fs_load_addr_3 ; Store result to &B3
83FF BCC dofsl7 ; C=0: success, check reply code
8401 .bputv_handler
CLC ; CLC for address addition
fall through ↓

BGETV entry point

Clears the escapable flag via clear_escapable, then falls through to handle_bput_bget with carry set (SEC by caller) to indicate a BGET operation.

On EntryYfile handle
C1 (set by MOS before calling BGETV)
On ExitAbyte read from file
C1 if EOF, 0 otherwise
8402 .bgetv_entry←1← 8552 JSR
JSR clear_escapable ; Clear escapable flag before BGET
fall through ↓

Handle BPUT/BGET file byte I/O

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

8468 .store_fs_error←1← 83F3 BNE
STX fs_last_error ; Remember raw FS error code
846B LDY #1 ; Y=1: point to error number byte in reply
846D CPX #&a8 ; Clamp FS errors below &A8 to standard &A8
846F BCS error_code_clamped ; Error >= &A8: keep original value
8471 LDA #&a8 ; Error < &A8: override with standard &A8
8473 STA (txcb_start),y ; Write clamped error number to reply buffer
8475 .error_code_clamped←1← 846F BCS
LDY #&ff ; Start scanning from offset &FF (will INY to 0)
8477 .copy_error_to_brk←1← 847F BNE
INY ; Next byte in reply buffer
8478 LDA (txcb_start),y ; Copy reply buffer to &0100 for BRK execution
847A STA error_block,y ; Build BRK error block at &0100
847D EOR #&0d ; Scan for CR terminator (&0D)
847F BNE copy_error_to_brk ; Continue until CR found
8481 STA error_block,y ; Replace CR with zero = BRK error block end
8484 BEQ execute_downloaded ; Execute as BRK error block at &0100; ALWAYS ALWAYS branch
8486 .update_sequence_return←1← 843C BEQ
STA fs_sequence_nos ; Save updated sequence number
8489 PLA ; Restore Y from stack
848A TAY ; Transfer A to Y for indexing
848B PLA ; Restore X from stack
848C TAX ; Transfer to X for return
848D PLA ; Restore A from stack
848E .return_remote_cmd←1← 8493 BPL
RTS ; Return to caller

Check for pending escape condition

Tests bit 7 of the MOS escape flag (&FF) ANDed with the escapable flag. If no escape is pending, returns immediately. If escape is active, acknowledges it via OSBYTE &7E and jumps to the escape error handler.

On ExitAcorrupted (AND result on normal return)
848F .check_escape←2← 80A9 JSR← 8615 JSR
LDA escape_flag ; Read escape flag from MOS workspace
8491 AND escapable ; Mask with escapable: bit 7 set if active
8493 BPL return_remote_cmd ; No escape pending: return
8495 LDA #osbyte_acknowledge_escape ; OSBYTE &7E: acknowledge escape condition
8497 JSR osbyte ; Clear escape condition and perform escape effects
849A JMP nlisne ; Report escape error via error message table

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.

849D .lang_1_remote_boot
LDY #4 ; Y=4: remote status flag offset
849F LDA (net_rx_ptr),y ; Read remote status from RX CB
84A1 BEQ remot1 ; Zero: not remoted, set up session
84A3 .rchex←1← 84E9 BNE
JMP clear_jsr_protection ; Already remoted: clear and return
84A6 .remot1←2← 84A1 BEQ← 84DF BEQ
ORA #9 ; Set remote status: bits 0+3 (ORA #9)
84A8 STA (net_rx_ptr),y ; Store updated remote status
84AA LDX #&80 ; X=&80: RX data area offset
84AC LDY #&80 ; Y=&80: read source station low
84AE LDA (net_rx_ptr),y ; Read source station lo from RX data at &80
84B0 PHA ; Save source station low byte
84B1 INY ; Y=&81
84B2 LDA (net_rx_ptr),y ; Read source station hi from RX data at &81
84B4 LDY #&0f ; Save controlling station to workspace &0E/&0F
84B6 STA (nfs_workspace),y ; Store station high to ws+&0F
84B8 DEY ; Y=&0E Y=&0e
84B9 PLA ; Restore source station low
84BA STA (nfs_workspace),y ; Store station low to ws+&0E
84BC JSR clear_osbyte_ce_cf ; Clear OSBYTE &CE/&CF flags
84BF JSR ctrl_block_setup ; Set up TX control block
84C2 LDX #1 ; X=1: disable keyboard
84C4 LDY #0 ; Y=0 for OSBYTE
84C6 LDA #osbyte_read_write_econet_keyboard_disable ; Disable keyboard for remote session
84C8 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.

84CB .lang_3_execute_at_0100
JSR clear_jsr_protection ; Allow JSR to page 1 (stack page)
84CE LDX #2 ; Zero bytes &0100-&0102
84D0 LDA #0 ; A=0: zero execution header bytes
84D2 .zero_exec_header←1← 84D6 BPL
STA error_block,x ; BRK at &0100 as safe default
84D5 DEX ; Next byte
84D6 BPL zero_exec_header ; Loop until all zeroed
84D8 .execute_downloaded←2← 8484 BEQ← 8511 BEQ
JMP error_block ; 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.

84DB .lang_4_remote_validated
LDY #4 ; Y=4: RX control block byte 4 (remote status)
84DD LDA (net_rx_ptr),y ; Read remote status flag
84DF BEQ remot1 ; Zero = not remoted; allow new session
84E1 LDY #&80 ; Read source station from RX data at &80
84E3 LDA (net_rx_ptr),y ; A = source station number
84E5 LDY #&0e ; Compare against controlling station at &0E
84E7 CMP (nfs_workspace),y ; Check if source matches controller
84E9 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.

84EB .lang_0_insert_remote_key
LDY #&82 ; Read keypress from RX data at &82
84ED LDA (net_rx_ptr),y ; Load character byte
84EF TAY ; Y = character to insert
84F0 LDX #0 ; X = buffer 0 (keyboard input)
84F2 JSR clear_jsr_protection ; Release JSR protection before inserting key
84F5 LDA #osbyte_insert_input_buffer ; OSBYTE &99: insert char into input buffer
84F7 JMP osbyte ; Tail call: insert character Y into buffer X Insert character Y into input buffer X
84FA .error_not_listening←1← 854E BEQ
LDA #8 ; Error code 8: "Not listening" error
84FC BNE set_listen_offset ; ALWAYS branch to set_listen_offset ALWAYS branch
84FE .nlistn←1← 862E JMP
LDA (net_tx_ptr,x) ; Load TX status byte for error lookup
8500 .nlisne←2← 849A JMP← 8A2E JMP
AND #7 ; Mask to 3-bit error code (0-7)
8502 .set_listen_offset←1← 84FC BNE
TAX ; X = error code index
8503 LDY error_offsets,x ; Look up error message offset from table
8506 LDX #0 ; X=0: start writing at &0101
8508 STX error_block ; Store BRK opcode at &0100
850B .copy_error_message←1← 8515 BNE
LDA error_msg_table,y ; Load error message byte
850E STA error_text,x ; Build error message at &0101+
8511 BEQ execute_downloaded ; Zero byte = end of message; go execute BRK
8513 INY ; Next source byte
8514 INX ; Next dest byte
8515 BNE copy_error_message ; Continue copying message
8517 EQUS "SP." ; "SP." remote spool command string
851A EQUB &0D, &45, ; CR + "E." + CR remote exec strings &2E, &0D
fall through ↓

Load '*' prefix and send FS command (WAITFS)

Loads A with &2A ('*') as the FS command prefix byte, then falls through to send_to_fs to perform a full fileserver transaction: transmit and wait for reply.

On ExitAreply command code
851E .waitfs←5← 83E5 JSR← 876D JSR← 8896 JSR← 903E JMP← 929E JSR
LDA #&2a ; A = '*' for FS command prefix
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_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.

On EntryAfunction code / command prefix byte
On ExitAreply command code
8520 .send_to_fs
PHA ; Save function code on stack
8521 .send_to_fs_star
LDA rx_flags ; Load current rx_flags
8524 PHA ; Save rx_flags on stack for restore
8525 LDX net_tx_ptr_hi ; Only flag rx_flags if using page-zero CB
8527 BNE skip_rx_flag_set ; High byte != 0: skip flag set
8529 ORA #&80 ; Set bit7: FS transaction in progress
852B STA rx_flags ; Write back updated rx_flags
852E .skip_rx_flag_set←1← 8527 BNE
LDA #0 ; Push two zero bytes as timeout counters
8530 PHA ; First zero for timeout
8531 PHA ; Second zero for timeout
8532 TAY ; Y=0: index for flag byte check Y=&00
8533 TSX ; TSX: index stack-based timeout via X
8534 .incpx←3← 853B BNE← 8540 BNE← 8545 BNE
LDA (net_tx_ptr),y ; Read flag byte from TX control block
8536 BMI fs_wait_cleanup ; Bit 7 set = reply received
8538 DEC error_text,x ; Three-stage nested timeout: inner loop
853B BNE incpx ; Inner not expired: keep polling
853D DEC stk_timeout_mid,x ; Middle timeout loop
8540 BNE incpx ; Middle not expired: keep polling
8542 DEC stk_timeout_hi,x ; Outer timeout loop (slowest)
8545 BNE incpx ; Outer not expired: keep polling
8547 .fs_wait_cleanup←1← 8536 BMI
PLA ; Pop first timeout byte
8548 PLA ; Pop second timeout byte
8549 PLA ; Pop saved rx_flags into A
854A STA rx_flags ; Restore saved rx_flags from stack
854D PLA ; Pop saved function code
854E BEQ error_not_listening ; A=saved func code; zero would mean no reply
8550 RTS ; Return to caller
8551 .bgetv_handler
SEC ; C=1: flag for BGET mode
8552 JSR bgetv_entry ; Handle BGET via FS command
8555 SEC ; SEC: set carry for error check
8556 LDA #&fe ; A=&FE: mask for EOF check
8558 BIT fs_error_flags ; BIT l0fdf: test error flags
855B BVS return_4 ; V=1: error, return early
855D CLC ; CLC: no error
855E PHP ; Save flags for EOF check
855F LDA fs_spool0 ; Load BGET result byte
8561 PLP ; Restore flags
8562 BMI bgetv_shared_jsr ; Bit7 set: skip FS flag clear
8564 JSR clear_fs_flag ; Clear FS flag for handle
8567 .bgetv_shared_jsr←1← 8562 BMI
JSR set_fs_flag ; Set EOF flag for this handle
856A .load_handle_mask
LDA fs_handle_mask ; Load handle bitmask for caller
856D .return_4←1← 855B 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 c84FA/nlistn/nlisne at &84FA-&850B.
856E .error_msg_table←1← 850B LDA
EQUB &A0 ; Error &A0: Line Jammed
856F EQUS "Line Jamme" ; "Line Jammed" string
8579 EQUB &64 ; Terminator + error &A1
857A EQUS "." ; NUL terminator
857B EQUB &A1 ; Error &A1: Net Error
857C EQUS "Net Error." ; "Net Error" string
8586 EQUB &A2 ; Error &A2: Not listening
8587 EQUS "Not listening." ; "Not listening" string
8595 EQUB &A3 ; Error &A3: No Clock
8596 EQUS "No Clock." ; "No Clock" string
859F EQUB &11 ; Error &11: Escape
85A0 EQUS "Escape." ; "Escape" string
85A7 EQUB &CB ; Error &CB: Bad Option
85A8 EQUS "Bad Option." ; "Bad Option" string
85B3 EQUB &A5 ; Error &A5: No reply
85B4 EQUS "No reply." ; "No reply" string

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 &85DA. The two formats use different bit layouts for file protection attributes.

On ExitABBC attribute bitmask (8-bit)
Xcorrupted
Y&0E
85BD .decode_attribs_6bit←2← 88E9 JSR← 8914 JSR
LDY #&0e ; Y=&0E: attribute byte offset in param block
85BF LDA (fs_options),y ; Load FS attribute byte
85C1 AND #&3f ; Mask to 6 bits (FS → BBC direction)
85C3 LDX #4 ; X=4: skip first 4 table entries (BBC→FS half)
85C5 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 &85DA. 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
85C7 .decode_attribs_5bit←2← 880D JSR← 8931 JSR
AND #&1f ; Mask to 5 bits (BBC → FS direction)
85C9 LDX #&ff ; X=&FF: INX makes 0; start from table index 0
85CB .attrib_shift_bits←1← 85C5 BNE
STA fs_error_ptr ; Temp storage for source bitmask to shift out
85CD LDA #0 ; A=0: accumulate destination bits here
85CF .map_attrib_bits←1← 85D7 BNE
INX ; Next table entry
85D0 LSR fs_error_ptr ; Shift out source bits one at a time
85D2 BCC skip_set_attrib_bit ; Bit was 0: skip this destination bit
85D4 ORA access_bit_table,x ; OR in destination bit from lookup table
85D7 .skip_set_attrib_bit←1← 85D2 BCC
BNE map_attrib_bits ; Loop while source bits remain (A != 0)
85D9 RTS ; Return; A = converted attribute bitmask
85DA .access_bit_table←1← 85D4 ORA
EQUB &50, &20, &05, &02, &88, &04, ; Attribute bit mapping &08, &80, &10, &01, &02 ; table (11 entries)

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.

On ExitA&FF (retry count, restored)
X0
Y0
85E5 .setup_tx_ptr_c0←2← 83DD JSR← 8885 JSR
LDX #&c0 ; TX control block low byte
85E7 STX net_tx_ptr ; Set net_tx_ptr = &00C0
85E9 LDX #0 ; TX control block high byte
85EB STX net_tx_ptr_hi ; Set net_tx_ptr+1 = &00
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.

On ExitA&FF (retry count, restored)
X0
Y0
85ED .tx_poll_ff←4← 9027 JSR← 9080 JMP← 90DD JSR← 927C JSR
LDA #&ff ; A=&FF: full retry count
85EF .tx_poll_timeout
LDY #&60 ; Y=timeout parameter (&60 = standard)
fall through ↓

Core transmit and poll routine (XMIT)

Claims the TX semaphore (tx_clear_flag) 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 (&85E5) always uses the standard TXCB; tx_poll_core (&85F1) is general-purpose.

On EntryAretry count (&FF = full retry)
Ytimeout parameter (&60 = standard)
On ExitAentry A (retry count, restored from stack)
X0
Y0
85F1 .tx_poll_core
PHA ; Save retry count and timeout on stack
85F2 TYA ; Transfer timeout to A
85F3 PHA ; Push timeout parameter
85F4 LDX #0 ; X=0 for (zp,X) indirect addressing
85F6 LDA (net_tx_ptr,x) ; Read control byte from TX block
85F8 .rearm_tx_attempt←1← 862B BEQ
STA (net_tx_ptr,x) ; Write back control byte (re-arm for TX)
85FA PHA ; Save control byte for error recovery
85FB .poll_tx_semaphore←1← 85FE BCC
ASL tx_clear_flag ; Spin until TX semaphore is free (C=1)
85FE BCC poll_tx_semaphore ; C=0: still held, keep spinning
8600 LDA net_tx_ptr ; Copy TX pointer to NMI block while locked
8602 STA nmi_tx_block ; Store low byte to NMI TX block
8604 LDA net_tx_ptr_hi ; Load TX pointer high byte
8606 STA nmi_tx_block_hi ; Store high byte to NMI TX block
8608 JSR start_adlc_tx ; Initiate ADLC transmission
860B .poll_tx_complete←1← 860D BMI
LDA (net_tx_ptr,x) ; Poll: wait for bit 7 to clear (TX done)
860D BMI poll_tx_complete ; Bit 7 set: still busy, keep polling
860F ASL ; Bit 6 into sign: 0=success, 1=error
8610 BPL tx_success_exit ; Success: clean up stack and exit
8612 ASL ; Bit 5: escape condition?
8613 BEQ tx_abort ; Yes (Z=1): abort via nlistn
8615 JSR check_escape ; Check for escape key pressed
8618 PLA ; Recover saved control byte
8619 TAX ; Move to X for retry
861A PLA ; Recover timeout parameter
861B TAY ; Move to Y for delay loop
861C PLA ; Recover retry count
861D BEQ tx_abort ; Retries exhausted: abort via nlistn
861F SBC #1 ; Decrement retry count (C=1 from CMP)
8621 PHA ; Re-push retry count and timeout for retry
8622 TYA ; Transfer timeout to A
8623 PHA ; Push timeout for next attempt
8624 TXA ; Restore control byte for retry
8625 .tx_retry_delay←2← 8626 BNE← 8629 BNE
DEX ; Delay loop: X*Y iterations before retry
8626 BNE tx_retry_delay ; Inner loop: decrement X
8628 DEY ; Outer loop: decrement Y
8629 BNE tx_retry_delay ; Continue delay until Y=0
862B BEQ rearm_tx_attempt ; ALWAYS branch
862D .tx_abort←2← 8613 BEQ← 861D BEQ
TAX ; A = error code for nlistn
862E JMP nlistn ; Report net error via nlistn
8631 .tx_success_exit←1← 8610 BPL
PLA ; Success: discard 3 saved bytes from stack
8632 PLA ; Discard timeout
8633 PLA ; Discard retry count
8634 JMP clear_escapable ; Jump to clear escapable flag and return

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.

On EntryAfunction code
Xtext pointer low
Ytext pointer high
8637 .save_fscv_args_with_ptrs←3← 80D0 JSR← 89C6 JSR← 8C09 JSR
STX os_text_ptr ; Set os_text_ptr low = X
8639 STY os_text_ptr_hi ; Set os_text_ptr high = Y
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
863B .save_fscv_args←3← 86FA JSR← 8956 JSR← 8A60 JSR
STA fs_last_byte_flag ; Save A = function code / command
863D STX fs_options ; Save X = control block ptr low
863F STY fs_block_offset ; Save Y = control block ptr high
8641 STX fs_crc_lo ; Duplicate X for indirect indexed access
8643 STY fs_crc_hi ; Duplicate Y for indirect indexed access
fall through ↓

Clear escapable flag preserving processor status

PHP/LSR escapable/PLP: clears bit 7 of the escapable flag while preserving the processor status register. Used at the start of FS vector operations to mark them as not yet escapable.

8645 .clear_escapable←2← 8402 JSR← 8634 JMP
PHP ; Clear escapable flag, preserving processor flags
8646 LSR escapable ; Reset: this operation is not escapable yet
8648 PLP ; Restore flags (caller may need N/Z/C)
8649 RTS ; Return

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
864A .print_inline←13← 81F6 JSR← 8220 JSR← 8240 JSR← 824D JSR← 8C7B JSR← 8C85 JSR← 8C93 JSR← 8C9E JSR← 8CB3 JSR← 8CC8 JSR← 8CDB JSR← 8CEA JSR← 8D99 JSR
PLA ; Pop return address (low) — points to last byte of JSR
864B STA fs_load_addr ; Store return addr low as string ptr
864D PLA ; Pop return address (high)
864E STA fs_load_addr_hi ; Store return addr high as string ptr
8650 LDY #0 ; Y=0: offset for indirect load
8652 .print_inline_char←1← 865F JMP
INC fs_load_addr ; Advance pointer past return address / to next char
8654 BNE print_next_char ; No page wrap: skip high byte inc
8656 INC fs_load_addr_hi ; Handle page crossing in pointer
8658 .print_next_char←1← 8654 BNE
LDA (fs_load_addr),y ; Load next byte from inline string
865A BMI jump_via_addr ; Bit 7 set? Done — this byte is the next opcode
865C JSR osasci ; Write character
865F JMP print_inline_char ; Continue printing next character
8662 .jump_via_addr←1← 865A 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") -- originally stopped on any char >= &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
8665 .parse_decimal←2← 8088 JSR← 8091 JSR
LDA #0 ; Zero accumulator
8667 STA fs_load_addr_2 ; Initialise accumulator to zero
8669 .scan_decimal_digit←1← 8682 BNE
LDA (fs_options),y ; Load next char from buffer
866B CMP #&2e ; Dot separator?
866D BEQ parse_decimal_rts ; Yes: exit with C=1 (dot found)
866F BCC no_dot_exit ; Control char or space: done
8671 AND #&0f ; Mask ASCII digit to 0-9
8673 STA fs_load_addr_3 ; Save new digit
8675 ASL fs_load_addr_2 ; Running total * 2
8677 LDA fs_load_addr_2 ; A = running total * 2
8679 ASL ; A = running total * 4
867A ASL ; A = running total * 8
867B ADC fs_load_addr_2 ; + total*2 = total * 10
867D ADC fs_load_addr_3 ; + digit = total*10 + digit
867F STA fs_load_addr_2 ; Store new running total
8681 INY ; Advance to next char
8682 BNE scan_decimal_digit ; Loop (always: Y won't wrap to 0)
8684 .no_dot_exit←1← 866F BCC
CLC ; No dot found: C=0
8685 .parse_decimal_rts←1← 866D BEQ
LDA fs_load_addr_2 ; Return result in A
8687 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
8688 .handle_to_mask_a←3← 889D JSR← 8A7B JSR← 8F5E 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
8689 .handle_to_mask_clc←2← 8410 JSR← 8961 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"). The bit-shift conversion loop 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
868A .handle_to_mask←1← 89CA JSR
PHA ; Save A (will be restored on exit)
868B TXA ; Save X (will be restored on exit)
868C PHA ; (second half of X save)
868D TYA ; A = handle from Y
868E BCC y2fsl5 ; C=0: always convert
8690 BEQ handle_mask_exit ; C=1 and Y=0: skip (handle 0 = none)
8692 .y2fsl5←1← 868E BCC
SEC ; C=1 and Y!=0: convert
8693 SBC #&1f ; A = handle - &1F (1-based bit position)
8695 TAX ; X = shift count
8696 LDA #1 ; Start with bit 0 set
8698 .y2fsl2←1← 869A BNE
ASL ; Shift bit left
8699 DEX ; Count down
869A BNE y2fsl2 ; Loop until correct position
869C ROR ; Undo final extra shift
869D TAY ; Y = resulting bitmask
869E BNE handle_mask_exit ; Non-zero: valid mask, skip to exit
86A0 DEY ; Zero: invalid handle, set Y=&FF
86A1 .handle_mask_exit←2← 8690 BEQ← 869E BNE
PLA ; Restore X
86A2 TAX ; Restore X from stack
86A3 PLA ; Restore A
86A4 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
86A5 .mask_to_handle←2← 89F9 JSR← 8F76 JSR
LDX #&1f ; X = &1F (handle base - 1)
86A7 .fs2al1←1← 86A9 BNE
INX ; Count this bit position
86A8 LSR ; Shift mask right; C=0 when done
86A9 BNE fs2al1 ; Loop until all bits shifted out
86AB TXA ; A = X = &1F + bit position = handle
86AC 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
86AD .compare_addresses←2← 8753 JSR← 8842 JSR
LDX #4 ; Compare 4 bytes (index 4,3,2,1)
86AF .compare_addr_byte←1← 86B6 BNE
LDA addr_work,x ; Load byte from first address
86B1 EOR fs_load_addr_3,x ; XOR with corresponding byte
86B3 BNE return_compare ; Mismatch: Z=0, return unequal
86B5 DEX ; Next byte
86B6 BNE compare_addr_byte ; Continue comparing
86B8 .return_compare←1← 86B3 BNE
RTS ; Return with Z flag result
86B9 .fscv_7_read_handles
LDX #&20 ; X=first handle (&20)
86BB LDY #&27 ; Y=last handle (&27)
86BD .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
86BE .set_fs_flag←5← 8567 JSR← 899E JSR← 89F5 JSR← 8A15 JSR← 8AF6 JSR
ORA fs_eof_flags ; Merge new bits into flags
86C1 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
86C3 .clear_fs_flag←3← 8564 JSR← 88B8 JSR← 8AF3 JSR
EOR #&ff ; Invert mask: set bits become clear bits
86C5 AND fs_eof_flags ; Clear specified bits in flags
86C8 .store_fs_flag←1← 86C1 BNE
STA fs_eof_flags ; Write back updated flags
86CB RTS ; Return

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.

On ExitXlength of parsed string
Y0
86CC .copy_filename_ptr←1← 86FD JSR
LDY #1 ; Y=1: copy 2 bytes (high then low)
86CE .file1←1← 86D4 BPL
LDA (fs_options),y ; Load filename ptr from control block
86D0 STA os_text_ptr,y ; Store to MOS text pointer (&F2/&F3)
86D3 DEY ; Next byte (descending)
86D4 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 &86D8 allows a non-zero starting Y offset.

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

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

FILEV handler (OSFILE entry point)

Calls save_fscv_args (&863B) to preserve A/X/Y, then JSR &86CC to copy the 2-byte filename pointer from the parameter block to os_text_ptr and fall through to parse_filename_gs (&86D6) 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 &8710) A=&00: save file (filev_save at &8783) A=&01-&06: attribute operations (filev_attrib_dispatch at &88BF) 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
86FA .filev_handler
JSR save_fscv_args ; Save A/X/Y in FS workspace
86FD JSR copy_filename_ptr ; Copy filename ptr from param block to os_text_ptr
8700 LDA fs_last_byte_flag ; Recover function code from saved A
8702 BPL saveop ; A >= 0: save (&00) or attribs (&01-&06)
8704 CMP #&ff ; A=&FF? Only &FF is valid for load
8706 BEQ loadop ; A=&FF: branch to load path
8708 JMP restore_args_return ; Unknown negative code: no-op return
870B .loadop←1← 8706 BEQ
JSR infol2 ; Copy parsed filename to cmd buffer
870E LDY #2 ; Y=2: FS function code offset
fall through ↓

Send FS examine command

Sends an FS examine/load command to the fileserver. The function code in Y is set by the caller (Y=2 for load, Y=5 for examine). Overwrites fs_cmd_urd (&0F02) with &92 (PLDATA port number) to repurpose the URD header field for the data transfer port. Sets escapable to &92 so escape checking is active during the transfer. Calls prepare_cmd_clv to build the FS header (which skips the normal URD copy, preserving &92). The FS reply contains load/exec addresses and file length used to set up the data transfer. Byte 6 of the parameter block selects load address handling: non-zero uses the address from the FS reply (load to file's own address); zero uses the caller-supplied address.

On EntryYFS function code (2=load, 5=examine)
XTX buffer extent
8710 .send_fs_examine←1← 8E10 JSR
LDA #&92 ; Port &92 = PLDATA (data transfer port)
8712 STA escapable ; Mark transfer as escapable
8714 STA fs_cmd_urd ; Overwrite URD field with data port number
8717 JSR prepare_cmd_clv ; Build FS header (V=1: CLV path)
871A LDY #6 ; Y=6: param block byte 6
871C LDA (fs_options),y ; Byte 6: use file's own load address?
871E BNE lodfil ; Non-zero: use FS reply address (lodfil)
8720 JSR copy_load_addr_from_params ; Zero: copy caller's load addr first
8723 JSR copy_reply_to_params ; Then copy FS reply to param block
8726 BCC skip_lodfil ; Carry clear from prepare_cmd_clv: skip lodfil
8728 .lodfil←1← 871E BNE
JSR copy_reply_to_params ; Copy FS reply addresses to param block
872B JSR copy_load_addr_from_params ; Then copy load addr from param block
872E .skip_lodfil←1← 8726 BCC
LDY #4 ; Compute end address = load + file length
8730 .copy_load_end_addr←1← 873B BNE
LDA fs_load_addr,x ; Load address byte
8732 STA txcb_end,x ; Store as current transfer position
8734 ADC fs_file_len,x ; Add file length byte
8737 STA fs_work_4,x ; Store as end position
8739 INX ; Next address byte
873A DEY ; Decrement byte counter
873B BNE copy_load_end_addr ; Loop for all 4 address bytes
873D SEC ; Adjust high byte for 3-byte length overflow
873E SBC fs_file_len_3 ; Subtract 4th length byte from end addr
8741 STA fs_work_7 ; Store adjusted end address high byte
8743 JSR send_data_blocks ; Transfer file data in &80-byte blocks
8746 LDX #2 ; Copy 3-byte file length to FS reply cmd buffer
8748 .floop←1← 874F BPL
LDA fs_file_len_3,x ; Load file length byte
874B STA fs_cmd_data,x ; Store in FS command data buffer
874E DEX ; Next byte (count down)
874F BPL floop ; Loop for 3 bytes (X=2,1,0)
8751 BMI save_csd_display ; 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.

8753 .send_data_blocks←2← 8743 JSR← 8AE6 JSR
JSR compare_addresses ; Compare two 4-byte addresses
8756 BEQ return_lodchk ; Addresses match: transfer complete
8758 LDA #&92 ; Port &92 for data block transfer
875A STA txcb_port ; Store port to TXCB command byte
875C .send_block_loop←1← 8778 BNE
LDX #3 ; Set up next &80-byte block for transfer
875E .copy_block_addrs←1← 8767 BPL
LDA txcb_end,x ; Swap: current addr -> source, end -> current
8760 STA txcb_start,x ; Source addr = current position
8762 LDA fs_work_4,x ; Load end address byte
8764 STA txcb_end,x ; Dest = end address (will be clamped)
8766 DEX ; Next address byte
8767 BPL copy_block_addrs ; Loop for all 4 bytes
8769 LDA #&7f ; Command &7F = data block transfer
876B STA txcb_ctrl ; Store to TXCB control byte
876D JSR waitfs ; Send this block to the fileserver
8770 LDY #3 ; Y=3: compare 4 bytes (3..0)
8772 .lodchk←1← 877B BPL
LDA txcb_end,y ; Compare current vs end address (4 bytes)
8775 EOR fs_work_4,y ; XOR with end address byte
8778 BNE send_block_loop ; Not equal: more blocks to send
877A DEY ; Next byte
877B BPL lodchk ; Loop for all 4 address bytes
877D .return_lodchk←1← 8756 BEQ
RTS ; All equal: transfer complete
877E .saveop←1← 8702 BPL
BEQ filev_save ; A=0: SAVE handler
8780 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. The savsiz loop computes data-end minus data-start for each address byte to derive the transfer length, saving both the original address and the difference. Sends FS command with port &91, function code Y=1, and the filename via copy_string_to_cmd. After transfer_file_blocks sends the data, calls send_fs_reply_cmd for the final handshake. If fs_messages_flag is set, prints the catalogue line inline: filename (padded to 12 chars), load address, exec address, and file length. Finally decodes the FS attributes and copies the reply data back into the caller's parameter block.

8783 .filev_save←1← 877E BEQ
LDX #4 ; Process 4 address bytes (load/exec/start/end)
8785 LDY #&0e ; Y=&0E: start from end-address in param block
8787 .savsiz←1← 87A1 BNE
LDA (fs_options),y ; Read end-address byte from param block
8789 STA port_ws_offset,y ; Save to port workspace for transfer setup
878C JSR sub_4_from_y ; Y = Y-4: point to start-address byte
878F SBC (fs_options),y ; end - start = transfer length byte
8791 STA fs_cmd_csd,y ; Store length byte in FS command buffer
8794 PHA ; Save length byte for param block restore
8795 LDA (fs_options),y ; Read corresponding start-address byte
8797 STA port_ws_offset,y ; Save to port workspace
879A PLA ; Restore length byte from stack
879B STA (fs_options),y ; Replace param block entry with length
879D JSR add_5_to_y ; Y = Y+5: advance to next address group
87A0 DEX ; Decrement address byte counter
87A1 BNE savsiz ; Loop for all 4 address bytes
87A3 LDY #9 ; Copy load/exec addresses to FS command buffer
87A5 .copy_save_params←1← 87AB BNE
LDA (fs_options),y ; Read load/exec address byte from params
87A7 STA fs_cmd_csd,y ; Copy to FS command buffer
87AA DEY ; Next byte (descending)
87AB BNE copy_save_params ; Loop for bytes 9..1
87AD LDA #&91 ; Port &91 for save command
87AF STA escapable ; Mark as escapable during save
87B1 STA fs_cmd_urd ; Overwrite URD field with port number
87B4 STA fs_error_ptr ; Save port &91 for flow control ACK
87B6 LDX #&0b ; Append filename at offset &0B in cmd buffer
87B8 JSR copy_string_to_cmd ; Append filename to cmd buffer at offset X
87BB LDY #1 ; Y=1: function code for save
87BD JSR prepare_cmd_clv ; Build header and send FS save command
87C0 LDA fs_cmd_data ; Read FS reply command code for transfer type
87C3 JSR transfer_file_blocks ; Send file data blocks to server
87C6 .save_csd_display←1← 8751 BMI
LDA fs_cmd_csd ; Save CSD from reply for catalogue display
87C9 PHA ; Save CSD byte from reply for display
87CA JSR send_fs_reply_cmd ; Send final reply acknowledgement
87CD PLA ; Restore CSD byte after reply command
87CE LDY fs_messages_flag ; Check if file info messages enabled
87D1 BEQ skip_catalogue_msg ; Messages off: skip catalogue display
87D3 LDY #0 ; Y=0: start of filename in reply
87D5 TAX ; A = CSD; test for directory prefix
87D6 BEQ print_filename_char ; CSD=0: no directory prefix
87D8 JSR print_dir_from_offset ; Print directory prefix from reply
87DB BMI print_addresses ; Dir printed: skip to address display
87DD .print_filename_char←2← 87D6 BEQ← 87E7 BNE
LDA (fs_crc_lo),y ; Load filename character from reply
87DF CMP #&21 ; Check for control character or space
87E1 BCC pad_filename_space ; Below &21: pad with spaces to column 12
87E3 JSR osasci ; Write character
87E6 INY ; Next character in filename
87E7 BNE print_filename_char ; Loop for more filename characters
87E9 .pad_filename_space←2← 87E1 BCC← 87EF BCC
JSR print_space ; Print space to pad filename to 12 chars
87EC INY ; Advance column counter
87ED CPY #&0c ; Reached column 12?
87EF BCC pad_filename_space ; No: keep padding with spaces
87F1 .print_addresses←1← 87DB BMI
LDY #5 ; Y=5: load address offset in reply
87F3 JSR print_hex_bytes ; Print 4-byte load address in hex
87F6 LDY #9 ; Y=9: exec address offset in reply
87F8 JSR print_hex_bytes ; Print 4-byte exec address in hex
87FB LDY #&0c ; Y=&0C: file length offset in reply
87FD LDX #3 ; X=3: print 3 bytes of length
87FF JSR num01 ; Print file length in hex
8802 .send_fs_reply
JSR osnewl ; Send FS reply acknowledgement Write newline (characters 10 and 13)
8805 .skip_catalogue_msg←1← 87D1 BEQ
STX fs_reply_cmd ; Store reply command for attr decode
8808 LDY #&0e ; Y=&0E: access byte offset in param block
880A LDA fs_cmd_data ; Load access byte from FS reply
880D JSR decode_attribs_5bit ; Convert FS access to BBC attribute format
8810 .copy_attribs_reply←1← 8818 BNE
STA (fs_options),y ; Store decoded access in param block
8812 INY ; Next attribute byte
8813 LDA fs_reply_data,y ; Load remaining reply data for param block
8816 CPY #&12 ; Copied all 4 bytes? (Y=&0E..&11)
8818 BNE copy_attribs_reply ; Loop for 4 attribute bytes
881A 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).

On ExitY8 (final offset)
Alast byte copied
881D .copy_load_addr_from_params←2← 8720 JSR← 872B JSR
LDY #5 ; Start at offset 5 (top of 4-byte addr)
881F .lodrl1←1← 8827 BCS
LDA (fs_options),y ; Read from parameter block
8821 STA work_ae,y ; Store to local workspace
8824 DEY ; Next byte (descending)
8825 CPY #2 ; Copy offsets 5,4,3,2 (4 bytes)
8827 BCS lodrl1 ; Loop while Y >= 2
8829 .add_5_to_y←1← 879D JSR
INY ; Y += 5
882A .add_4_to_y←1← 8AC3 JSR
INY ; Y += 4
882B INY ; (continued)
882C INY ; (continued)
882D INY ; (continued)
882E 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)
882F .copy_reply_to_params←2← 8723 JSR← 8728 JSR
LDY #&0d ; Start at offset &0D (top of range)
8831 TXA ; First store uses X (attrib byte)
8832 .lodrl2←1← 883A BCS
STA (fs_options),y ; Write to parameter block
8834 LDA fs_cmd_urd,y ; Read next byte from reply buffer
8837 DEY ; Next byte (descending)
8838 CPY #2 ; Copy offsets &0D down to 2
883A BCS lodrl2 ; Loop until offset 2 reached
883C .sub_4_from_y←1← 878C JSR
DEY ; Y -= 4
883D .sub_3_from_y←2← 88D7 JSR← 8ACB JSR
DEY ; Y -= 3
883E DEY ; (continued)
883F DEY ; (continued)
8840 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).

8841 .transfer_file_blocks←2← 87C3 JSR← 8AE1 JSR
PHA ; Save FS command byte on stack
8842 JSR compare_addresses ; Compare two 4-byte addresses
8845 BEQ restore_ay_return ; Addresses equal: nothing to transfer
8847 .transfer_loop_top←1← 8899 BNE
LDA #0 ; A=0: high bytes of block size
8849 PHA ; Push 4-byte block size: 0, 0, hi, lo
884A PHA ; Push second zero byte
884B TAX ; X=&00
884C LDA fs_data_count ; Load block size high byte from &0F07
884F PHA ; Push block size high
8850 LDA fs_func_code ; Load block size low byte from &0F06
8853 PHA ; Push block size low
8854 LDY #4 ; Y=4: process 4 address bytes
8856 CLC ; CLC for ADC in loop
8857 .setup_block_addrs←1← 8864 BNE
LDA fs_load_addr,x ; Source = current position
8859 STA txcb_start,x ; Store source address byte
885B PLA ; Pop block size byte from stack
885C ADC fs_load_addr,x ; Dest = current pos + block size
885E STA txcb_end,x ; Store dest address byte
8860 STA fs_load_addr,x ; Advance current position
8862 INX ; Next address byte
8863 DEY ; Decrement byte counter
8864 BNE setup_block_addrs ; Loop for all 4 bytes
8866 SEC ; SEC for SBC in overshoot check
8867 .savchk←1← 886F BNE
LDA fs_load_addr,y ; Check if new pos overshot end addr
886A SBC fs_work_4,y ; Subtract end address byte
886D INY ; Next byte
886E DEX ; Decrement counter
886F BNE savchk ; Loop for 4-byte comparison
8871 BCC send_block ; C=0: no overshoot, proceed
8873 .clamp_dest_setup
LDX #3 ; Overshot: clamp dest to end address
8875 .clamp_dest_addr←1← 887A BPL
LDA fs_work_4,x ; Load end address byte
8877 STA txcb_end,x ; Replace dest with end address
8879 DEX ; Next byte
887A BPL clamp_dest_addr ; Loop for all 4 bytes
887C .send_block←1← 8871 BCC
PLA ; Recover original FS command byte
887D PHA ; Re-push for next iteration
887E PHP ; Save processor flags (C from cmp)
887F STA txcb_port ; Store command byte in TXCB
8881 LDA #&80 ; 128-byte block size for data transfer
8883 STA txcb_ctrl ; Store size in TXCB control byte
8885 JSR setup_tx_ptr_c0 ; Point TX ptr to &00C0; transmit
8888 LDA fs_error_ptr ; ACK port for flow control
888A JSR init_tx_ctrl_port ; Set reply port for ACK receive
888D PLP ; Restore flags (C=overshoot status)
888E BCS restore_ay_return ; C=1: all data sent (overshot), done
8890 LDA #&91 ; Command &91 = data block transfer
8892 STA txcb_port ; Store command &91 in TXCB
8894 INC txcb_start ; Skip command code byte in TX buffer
8896 JSR waitfs ; Transmit block and wait (BRIANX)
8899 BNE transfer_loop_top ; 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
889B .fscv_1_eof
PHA ; Save A (function code)
889C TXA ; X = file handle to check
889D JSR handle_to_mask_a ; Convert handle to bitmask in A
88A0 TYA ; Y = handle bitmask from conversion
88A1 AND fs_eof_flags ; Local hint: is EOF possible for this handle?
88A4 TAX ; X = result of AND (0 = not at EOF)
88A5 BEQ restore_ay_return ; Hint clear: definitely not at EOF
88A7 PHA ; Save bitmask for clear_fs_flag
88A8 STY fs_cmd_data ; Handle byte in FS command buffer
88AB LDY #&11 ; Y=&11: FS function code FCEOF Y=function code for HDRFN
88AD LDX #1 ; X=preserved through header build
88AF JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
88B2 PLA ; Restore bitmask
88B3 LDX fs_cmd_data ; FS reply: non-zero = at EOF
88B6 BNE restore_ay_return ; At EOF: skip flag clear
88B8 JSR clear_fs_flag ; Not at EOF: clear the hint bit
88BB .restore_ay_return←4← 8845 BEQ← 888E BCS← 88A5 BEQ← 88B6 BNE
PLA ; Restore A
88BC LDY fs_block_offset ; Restore Y
88BE 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
88BF .filev_attrib_dispatch←1← 8780 JMP
STA fs_cmd_data ; Store function code in FS cmd buffer
88C2 CMP #6 ; A=6? (delete)
88C4 BEQ cha6 ; Yes: jump to delete handler
88C6 BCS check_attrib_result ; A>=7: unsupported, fall through to return
88C8 CMP #5 ; A=5? (read catalogue info)
88CA BEQ cha5 ; Yes: jump to read info handler
88CC CMP #4 ; A=4? (write attributes only)
88CE BEQ cha4 ; Yes: jump to write attrs handler
88D0 CMP #1 ; A=1? (write all catalogue info)
88D2 BEQ get_file_protection ; Yes: jump to write-all handler
88D4 ASL ; A=2 or 3: convert to param block offset
88D5 ASL ; A*4: 2->8, 3->12
88D6 TAY ; Y = A*4
88D7 JSR sub_3_from_y ; Y = A*4 - 3 (load addr offset for A=2)
88DA LDX #3 ; X=3: copy 4 bytes
88DC .chalp1←1← 88E3 BPL
LDA (fs_options),y ; Load address byte from param block
88DE STA fs_func_code,x ; Store to FS cmd data area
88E1 DEY ; Next source byte (descending)
88E2 DEX ; Next dest byte
88E3 BPL chalp1 ; Loop for 4 bytes
88E5 LDX #5 ; X=5: data extent for filename copy
88E7 BNE copy_filename_to_cmd ; ALWAYS branch
88E9 .get_file_protection←1← 88D2 BEQ
JSR decode_attribs_6bit ; A=1: encode protection from param block
88EC STA fs_file_attrs ; Store encoded attrs at &0F0E
88EF LDY #9 ; Y=9: source offset in param block
88F1 LDX #8 ; X=8: dest offset in cmd buffer
88F3 .chalp2←1← 88FA BNE
LDA (fs_options),y ; Load byte from param block
88F5 STA fs_cmd_data,x ; Store to FS cmd buffer
88F8 DEY ; Next source byte (descending)
88F9 DEX ; Next dest byte
88FA BNE chalp2 ; Loop until X=0 (8 bytes copied)
88FC LDX #&0a ; X=&0A: data extent past attrs+addrs
88FE .copy_filename_to_cmd←2← 88E7 BNE← 891C BNE
JSR copy_string_to_cmd ; Append filename to cmd buffer
8901 LDY #&13 ; Y=&13: fn code for FCSAVE (write attrs)
8903 BNE send_fs_cmd_v1 ; ALWAYS branch to send command ALWAYS branch
8905 .cha6←1← 88C4 BEQ
JSR infol2 ; A=6: copy filename (delete)
8908 LDY #&14 ; Y=&14: fn code for FCDEL (delete)
890A .send_fs_cmd_v1←1← 8903 BNE
BIT tx_ctrl_upper ; Set V=1 (BIT trick: &B3 has bit 6 set)
890D JSR init_tx_ctrl_data ; Send via prepare_fs_cmd_v (V=1 path)
8910 .check_attrib_result←1← 88C6 BCS
BCS attrib_error_exit ; C=1: &D6 not-found, skip to return
8912 BCC argsv_check_return ; C=0: success, copy reply to param block ALWAYS branch
8914 .cha4←1← 88CE BEQ
JSR decode_attribs_6bit ; A=4: encode attrs from param block
8917 STA fs_func_code ; Store encoded attrs at &0F06
891A LDX #2 ; X=2: data extent (1 attr byte + fn)
891C BNE copy_filename_to_cmd ; ALWAYS branch to append filename ALWAYS branch
891E .cha5←1← 88CA BEQ
LDX #1 ; X=1: filename only, no data extent
8920 JSR copy_string_to_cmd ; Copy filename to cmd buffer
8923 LDY #&12 ; Y=&12: fn code for FCEXAM (read info) Y=function code for HDRFN
8925 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8928 LDA fs_obj_type ; Save object type from FS reply
892B STX fs_obj_type ; Clear reply byte (X=0 on success) X=0 on success, &D6 on not-found
892E STX fs_len_clear ; Clear length high byte in reply
8931 JSR decode_attribs_5bit ; Decode 5-bit access byte from FS reply
8934 LDY #&0e ; Y=&0E: attrs offset in param block
8936 STA (fs_options),y ; Store decoded attrs at param block +&0E
8938 DEY ; Y=&0D: start copy below attrs Y=&0d
8939 LDX #&0c ; X=&0C: copy from reply offset &0C down
893B .copy_fs_reply_to_cb←1← 8942 BNE
LDA fs_cmd_data,x ; Load reply byte (load/exec/length)
893E STA (fs_options),y ; Store to param block
8940 DEY ; Next dest byte (descending)
8941 DEX ; Next source byte
8942 BNE copy_fs_reply_to_cb ; Loop until X=0 (12 bytes copied)
8944 INX ; X=0 -> X=2 for length high copy
8945 INX ; INX again: X=2
8946 LDY #&11 ; Y=&11: length high dest in param block
8948 .cha5lp←1← 894F BPL
LDA fs_access_level,x ; Load length high byte from reply
894B STA (fs_options),y ; Store to param block
894D DEY ; Next dest byte (descending)
894E DEX ; Next source byte
894F BPL cha5lp ; Loop for 3 length-high bytes
8951 LDA fs_cmd_data ; Return object type in A
8954 .attrib_error_exit←1← 8910 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 (5 = network FS) A=0, Y>0: read file pointer via FS command &0C (FCRDSE) A=1, Y>0: write file pointer via FS command &0D (FCWRSE) A=2, Y=0: return &01 (command-line tail supported) 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
8956 .argsv_handler
JSR save_fscv_args ; Save A/X/Y registers for later restore
8959 CMP #3 ; Function >= 3?
895B BCS restore_args_return ; A>=3 (ensure/flush): no-op for NFS
895D CPY #0 ; Test file handle
895F BEQ argsv_fs_query ; Y=0: FS-level query, not per-file
8961 JSR handle_to_mask_clc ; Convert handle to bitmask
8964 STY fs_cmd_data ; Store bitmask as first cmd data byte
8967 LSR ; LSR splits A: C=1 means write (A=1)
8968 STA fs_func_code ; Store function code to cmd data byte 2
896B BCS save_args_handle ; C=1: write path, copy ptr from caller
896D LDY #&0c ; Y=&0C: FCRDSE (read sequential pointer) Y=function code for HDRFN
896F LDX #2 ; X=2: 3 data bytes in command X=preserved through header build
8971 JSR prepare_fs_cmd ; Build and send FS command Prepare FS command buffer (12 references)
8974 STA fs_last_byte_flag ; Clear last-byte flag on success A=0 on success (from build_send_fs_cmd)
8976 LDX fs_options ; X = saved control block ptr low
8978 LDY #2 ; Y=2: copy 3 bytes of file pointer
897A STA zp_work_3,x ; Zero high byte of 3-byte pointer
897C .copy_fileptr_reply←1← 8983 BPL
LDA fs_cmd_data,y ; Read reply byte from FS cmd data
897F STA zp_work_2,x ; Store to caller's control block
8981 DEX ; Next byte (descending)
8982 DEY ; Next source byte
8983 BPL copy_fileptr_reply ; Loop for all 3 bytes
8985 .argsv_check_return←1← 8912 BCC
BCC restore_args_return ; C=0 (read): return to caller
8987 .save_args_handle←1← 896B BCS
TYA ; Save bitmask for set_fs_flag later
8988 PHA ; Push bitmask
8989 LDY #3 ; Y=3: copy 4 bytes of file pointer
898B .copy_fileptr_to_cmd←1← 8992 BPL
LDA zp_work_3,x ; Read caller's pointer byte
898D STA fs_data_count,y ; Store to FS command data area
8990 DEX ; Next source byte
8991 DEY ; Next destination byte
8992 BPL copy_fileptr_to_cmd ; Loop for all 4 bytes
8994 LDY #&0d ; Y=&0D: FCWRSE (write sequential pointer) Y=function code for HDRFN
8996 LDX #5 ; X=5: 6 data bytes in command X=preserved through header build
8998 JSR prepare_fs_cmd ; Build and send FS command Prepare FS command buffer (12 references)
899B STX fs_last_byte_flag ; Save not-found status from X X=0 on success, &D6 on not-found
899D PLA ; Recover bitmask for EOF hint update
899E 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.

On ExitArestored from fs_last_byte_flag (&BD)
Xrestored from fs_options (&BB)
Yrestored from fs_block_offset (&BC)
89A1 .restore_args_return←7← 8708 JMP← 881A JMP← 895B BCS← 8985 BCC← 8A18 BCC← 8A6B JMP← 8E38 JMP
LDA fs_last_byte_flag ; A = saved function code / command
89A3 .restore_xy_return←5← 8954 BPL← 89B4 BNE← 89C4 BPL← 89EF BCS← 89FC BNE
LDX fs_options ; X = saved control block ptr low
89A5 LDY fs_block_offset ; Y = saved control block ptr high
89A7 RTS ; Return to MOS with registers restored
89A8 .argsv_fs_query←1← 895F BEQ
CMP #2 ; Y=0: FS-level queries (no file handle)
89AA BEQ halve_args_a ; A=2: FS-level ensure (write extent)
89AC BCS return_a_zero ; A>=3: FS command (ARGSV write)
89AE TAY ; Y = A = byte count for copy loop
89AF BNE osarg1 ; A!=0: copy command context block
89B1 LDA #&0a ; FS number 5 (loaded as &0A, LSR'd)
89B3 .halve_args_a←1← 89AA BEQ
LSR ; Shared: halve A (A=0 or A=2 paths)
89B4 BNE restore_xy_return ; Return with A = FS number or 1
89B6 .osarg1←2← 89AF BNE← 89BC BPL
LDA fs_cmd_context,y ; Copy command context to caller's block
89B9 STA (fs_options),y ; Store to caller's parameter block
89BB DEY ; Next byte (descending)
89BC BPL osarg1 ; Loop until all bytes copied
89BE STY zp_work_2,x ; Y=&FF after loop; fill high bytes
89C0 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.

On ExitA0
Xrestored from fs_options (&BB)
Yrestored from fs_block_offset (&BC)
89C2 .return_a_zero←4← 89AC BCS← 89D2 BNE← 8B07 JMP← 8BA9 JMP
LDA #0 ; A=operation (0=close, &40=read, &80=write, &C0=R/W)
89C4 BPL restore_xy_return ; ALWAYS branch

FINDV handler (OSFIND entry point)

A=0: close file -- delegates to close_handle (&89FE) 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"), and into the sequence 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
89C6 .findv_handler
JSR save_fscv_args_with_ptrs ; Save A/X/Y and set up pointers
89C9 SEC ; SEC distinguishes open (A>0) from close
89CA JSR handle_to_mask ; Convert file handle to bitmask (Y2FS)
89CD TAX ; A=preserved
89CE BEQ close_handle ; A=0: close file(s)
89D0 AND #&3f ; Valid open modes: &40, &80, &C0 only
89D2 BNE return_a_zero ; Invalid mode bits: return
89D4 TXA ; A = original mode byte
89D5 EOR #&80 ; Convert MOS mode to FS protocol flags
89D7 ASL ; ASL: shift mode bits left
89D8 STA fs_cmd_data ; Flag 1: read/write direction
89DB ROL ; ROL: Flag 2 into bit 0
89DC STA fs_func_code ; Flag 2: create vs existing file
89DF JSR parse_filename_gs ; Parse filename from command line
89E2 LDX #2 ; X=2: copy after 2-byte flags
89E4 JSR copy_string_to_cmd ; Copy filename to FS command buffer
89E7 LDY #6 ; Y=6: FS function code FCOPEN
89E9 BIT tx_ctrl_upper ; Set V flag from l83b3 bit 6
89EC JSR init_tx_ctrl_data ; Build and send FS open command
89EF BCS restore_xy_return ; Error: restore and return
89F1 LDA fs_cmd_data ; Load reply handle from FS
89F4 TAX ; X = new file handle
89F5 JSR set_fs_flag ; Set EOF hint + sequence bits
; OR handle bit into fs_sequence_nos
; (&0E08). Without this, a newly opened file could
; inherit a stale sequence number from a previous
; file using the same handle, causing byte-stream
; protocol errors.
89F8 TXA ; A=handle bitmask for new file A=single-bit bitmask
89F9 JSR mask_to_handle ; Convert bitmask to handle number (FS2A)
89FC 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)
89FE .close_handle←1← 89CE BEQ
TYA ; A = handle (Y preserved in A) Y=preserved
89FF BNE close_single_handle ; Y>0: close single file
8A01 LDA #osbyte_close_spool_exec ; Close SPOOL/EXEC before FS close-all
8A03 JSR osbyte ; Close any *SPOOL and *EXEC files
8A06 LDY #0 ; Y=0: close all handles on server
8A08 .close_single_handle←1← 89FF BNE
STY fs_cmd_data ; Handle byte in FS command buffer
8A0B LDX #1 ; X=preserved through header build
8A0D LDY #7 ; Y=function code for HDRFN
8A0F JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8A12 LDA fs_cmd_data ; Reply handle for flag update
8A15 JSR set_fs_flag ; Update EOF/sequence tracking bits
8A18 .close_opt_return←1← 8A3E BCC
BCC restore_args_return ; C=0: restore A/X/Y and return
8A1A .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
8A1C .fscv_0_opt
CPX #4 ; Is it *OPT 4,Y?
8A1E BNE check_opt1 ; No: check for *OPT 1
8A20 CPY #4 ; Y must be 0-3 for boot option
8A22 BCC optl1 ; Y < 4: valid boot option
8A24 .check_opt1←1← 8A1E BNE
DEX ; Not *OPT 4: check for *OPT 1
8A25 BNE opter1 ; Not *OPT 1 either: bad option
8A27 .set_messages_flag←1← 8A1A BEQ
STY fs_messages_flag ; Set local messages flag (*OPT 1,Y)
8A2A BCC opt_return ; Return via restore_args_return
8A2C .opter1←1← 8A25 BNE
LDA #7 ; Error index 7 (Bad option)
8A2E JMP nlisne ; Generate BRK error
8A31 .optl1←1← 8A22 BCC
STY fs_cmd_data ; Boot option value in FS command
8A34 LDY #&16 ; Y=&16: FS function code FCOPT Y=function code for HDRFN
8A36 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8A39 LDY fs_block_offset ; Restore Y from saved value
8A3B STY fs_boot_option ; Cache boot option locally
8A3E .opt_return←1← 8A2A BCC
BCC close_opt_return ; Return via restore_args_return
8A40 .adjust_addrs_9←1← 8AFB JSR
LDY #9 ; Y=9: adjust 9 address bytes
8A42 JSR adjust_addrs_clc ; Adjust with carry clear
8A45 .adjust_addrs_1←1← 8BF0 JSR
LDY #1 ; Y=1: adjust 1 address byte
8A47 .adjust_addrs_clc←1← 8A42 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
8A48 .adjust_addrs←2← 8B01 JSR← 8BFC JSR
LDX #&fc ; X=&FC: index into &0E06 area (wraps to 0)
8A4A .adjust_addr_byte←1← 8A5D BNE
LDA (fs_options),y ; Load byte from param block
8A4C BIT fs_load_addr_2 ; Test sign of adjustment direction
8A4E BMI subtract_adjust ; Negative: subtract instead
8A50 ADC fs_cmd_context,x ; Add adjustment value
8A53 JMP gbpbx ; Skip to store result
8A56 .subtract_adjust←1← 8A4E BMI
SBC fs_cmd_context,x ; Subtract adjustment value
8A59 .gbpbx←1← 8A53 JMP
STA (fs_options),y ; Store adjusted byte back
8A5B INY ; Next param block byte
8A5C INX ; Next adjustment byte (X wraps &FC->&00)
8A5D BNE adjust_addr_byte ; Loop 4 times (X=&FC,&FD,&FE,&FF,done)
8A5F 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
8A60 .gbpbv_handler
JSR save_fscv_args ; Save A/X/Y to FS workspace
8A63 TAX ; X = call number for range check
8A64 BEQ gbpb_invalid_exit ; A=0: invalid, restore and return
8A66 DEX ; Convert to 0-based (A=0..7)
8A67 CPX #8 ; Range check: must be 0-7
8A69 BCC gbpbx1 ; In range: continue to handler
8A6B .gbpb_invalid_exit←1← 8A64 BEQ
JMP restore_args_return ; Out of range: restore args and return
8A6E .gbpbx1←1← 8A69 BCC
TXA ; Recover 0-based function code
8A6F LDY #0 ; Y=0: param block byte 0 (file handle)
8A71 PHA ; Save function code on stack
8A72 CMP #4 ; A>=4: info queries, dispatch separately
8A74 BCC gbpbe1 ; A<4: file read/write operations
8A76 JMP osgbpb_info ; Dispatch to OSGBPB 5-8 info handler
8A79 .gbpbe1←1← 8A74 BCC
LDA (fs_options),y ; Get file handle from param block byte 0
8A7B JSR handle_to_mask_a ; Convert handle to bitmask for EOF flags
8A7E STY fs_cmd_data ; Store handle in FS command data
8A81 LDY #&0b ; Y=&0B: start at param block byte 11
8A83 LDX #6 ; X=6: copy 6 bytes of transfer params
8A85 .gbpbf1←1← 8A91 BNE
LDA (fs_options),y ; Load param block byte
8A87 STA fs_func_code,x ; Store to FS command buffer at &0F06+X
8A8A DEY ; Previous param block byte
8A8B CPY #8 ; Skip param block offset 8 (the handle)
8A8D BNE gbpbx0 ; Not at handle offset: continue
8A8F DEY ; Extra DEY to skip handle byte
8A90 .gbpbx0←1← 8A8D BNE
.gbpbf2←1← 8A8D BNE
DEX ; Decrement copy counter
8A91 BNE gbpbf1 ; Loop for all 6 bytes
8A93 PLA ; Recover function code from stack
8A94 LSR ; LSR: odd=read (C=1), even=write (C=0)
8A95 PHA ; Save function code again (need C later)
8A96 BCC gbpbl1 ; Even (write): X stays 0
8A98 INX ; Odd (read): X=1
8A99 .gbpbl1←1← 8A96 BCC
STX fs_func_code ; Store FS direction flag
8A9C LDY #&0b ; Y=&0B: command data extent
8A9E LDX #&91 ; Command &91=put, &92=get
8AA0 PLA ; Recover function code
8AA1 PHA ; Save again for later direction check
8AA2 BEQ gbpb_write_path ; Even (write): keep &91 and Y=&0B
8AA4 LDX #&92 ; Odd (read): use &92 (get) instead
8AA6 DEY ; Read: one fewer data byte in command Y=&0a
8AA7 .gbpb_write_path←1← 8AA2 BEQ
STX fs_cmd_urd ; Store port to FS command URD field
8AAA STX fs_error_ptr ; Save port for error recovery
8AAC LDX #8 ; X=8: command data bytes
8AAE LDA fs_cmd_data ; Load handle from FS command data
8AB1 JSR prepare_cmd_with_flag ; Build FS command with handle+flag
8AB4 LDA fs_load_addr_3 ; Save seq# for byte-stream flow control
8AB6 STA fs_sequence_nos ; Store to FS sequence number workspace
8AB9 LDX #4 ; X=4: copy 4 address bytes
8ABB .gbpbl3←1← 8ACF BNE
LDA (fs_options),y ; Set up source/dest from param block
8ABD STA addr_work,y ; Store as source address
8AC0 STA txcb_pos,y ; Store as current transfer position
8AC3 JSR add_4_to_y ; Skip 4 bytes to reach transfer length
8AC6 ADC (fs_options),y ; Dest = source + length
8AC8 STA addr_work,y ; Store as end address
8ACB JSR sub_3_from_y ; Back 3 to align for next iteration
8ACE DEX ; Decrement byte counter
8ACF BNE gbpbl3 ; Loop for all 4 address bytes
8AD1 INX ; X=1 after loop
8AD2 .gbpbf3←1← 8AD9 BPL
LDA fs_cmd_csd,x ; Copy CSD data to command buffer
8AD5 STA fs_func_code,x ; Store at &0F06+X
8AD8 DEX ; Decrement counter
8AD9 BPL gbpbf3 ; Loop for X=1,0
8ADB PLA ; Odd (read): send data to FS first
8ADC BNE gbpb_read_path ; Non-zero: skip write path
8ADE LDA fs_cmd_urd ; Load port for transfer setup
8AE1 JSR transfer_file_blocks ; Transfer data blocks to fileserver
8AE4 BCS wait_fs_reply ; Carry set: transfer error
8AE6 .gbpb_read_path←1← 8ADC BNE
JSR send_data_blocks ; Read path: receive data blocks from FS
8AE9 .wait_fs_reply←1← 8AE4 BCS
JSR send_fs_reply_cmd ; Wait for FS reply command
8AEC LDA (fs_options,x) ; Load handle mask for EOF flag update
8AEE BIT fs_cmd_data ; Check FS reply: bit 7 = not at EOF
8AF1 BMI skip_clear_flag ; Bit 7 set: not EOF, skip clear
8AF3 JSR clear_fs_flag ; At EOF: clear EOF hint for this handle
8AF6 .skip_clear_flag←1← 8AF1 BMI
JSR set_fs_flag ; Set EOF hint flag (may be at EOF)
8AF9 STX fs_load_addr_2 ; Direction=0: forward adjustment
8AFB JSR adjust_addrs_9 ; Adjust param block addrs by +9 bytes
8AFE DEC fs_load_addr_2 ; Direction=&FF: reverse adjustment
8B00 SEC ; SEC for reverse subtraction
8B01 JSR adjust_addrs ; Adjust param block addrs (reverse)
8B04 ASL fs_cmd_data ; Shift bit 7 into C for return flag
8B07 JMP return_a_zero ; Return via restore_args path
8B0A .get_disc_title←1← 8B3A BEQ
LDY #&15 ; Y=&15: function code for disc title Y=function code for HDRFN
8B0C JSR prepare_fs_cmd ; Build and send FS command Prepare FS command buffer (12 references)
8B0F LDA fs_boot_option ; Load boot option from FS workspace
8B12 STA fs_boot_data ; Store boot option in reply area
8B15 STX fs_load_addr ; X=0: reply data start offset X=0 on success, &D6 on not-found
8B17 STX fs_load_addr_hi ; Clear reply buffer high byte
8B19 LDA #&12 ; A=&12: 18 bytes of reply data
8B1B STA fs_load_addr_2 ; Store as byte count for copy
8B1D 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.

8B1F .osgbpb_info←1← 8A76 JMP
LDY #4 ; Y=4: check param block byte 4
8B21 LDA tube_flag ; Check if destination is in Tube space
8B24 BEQ store_tube_flag ; No Tube: skip Tube address check
8B26 CMP (fs_options),y ; Compare Tube flag with addr byte 4
8B28 BNE store_tube_flag ; Mismatch: not Tube space
8B2A DEY ; Y=&03
8B2B SBC (fs_options),y ; Y=3: subtract addr byte 3 from flag
8B2D .store_tube_flag←2← 8B24 BEQ← 8B28 BNE
STA svc_state ; Non-zero = Tube transfer required
8B2F .info2←1← 8B35 BNE
LDA (fs_options),y ; Copy param block bytes 1-4 to workspace
8B31 STA fs_last_byte_flag,y ; Store to &BD+Y workspace area
8B34 DEY ; Previous byte
8B35 BNE info2 ; Loop for bytes 3,2,1
8B37 PLA ; Sub-function: AND #3 of (original A - 4)
8B38 AND #3 ; Mask to 0-3 (OSGBPB 5-8 → 0-3)
8B3A BEQ get_disc_title ; A=0 (OSGBPB 5): read disc title
8B3C LSR ; LSR: A=0 (OSGBPB 6) or A=1 (OSGBPB 7)
8B3D BEQ gbpb6_read_name ; A=0 (OSGBPB 6): read CSD/LIB name
8B3F BCS gbpb8_read_dir ; C=1 (OSGBPB 8): read filenames from dir
8B41 .gbpb6_read_name←1← 8B3D BEQ
TAY ; Y=0 for CSD or carry for fn code select Y=function code
8B42 LDA fs_csd_handle,y ; Get CSD/LIB/URD handles for FS command
8B45 STA fs_cmd_csd ; Store CSD handle in command buffer
8B48 LDA fs_lib_handle ; Load LIB handle from workspace
8B4B STA fs_cmd_lib ; Store LIB handle in command buffer
8B4E LDA fs_urd_handle ; Load URD handle from workspace
8B51 STA fs_cmd_urd ; Store URD handle in command buffer
8B54 LDX #&12 ; X=&12: buffer extent for command data X=buffer extent (command-specific data bytes)
8B56 STX fs_cmd_y_param ; Store X as function code in header
8B59 LDA #&0d ; &0D = 13 bytes of reply data expected
8B5B STA fs_func_code ; Store reply length in command buffer
8B5E STA fs_load_addr_2 ; Store as byte count for copy loop
8B60 LSR ; LSR: &0D >> 1 = 6
8B61 STA fs_cmd_data ; Store as command data byte
8B64 CLC ; CLC for standard FS path
8B65 JSR build_send_fs_cmd ; Build and send FS command (DOFSOP)
8B68 STX fs_load_addr_hi ; X=0 on success, &D6 on not-found
8B6A INX ; INX: X=1 after build_send_fs_cmd
8B6B STX fs_load_addr ; Store X as reply start offset
8B6D .copy_reply_to_caller←2← 8B1D BNE← 8BE5 JSR
LDA svc_state ; Copy FS reply to caller's buffer
8B6F BNE tube_transfer ; Non-zero: use Tube transfer path
8B71 LDX fs_load_addr ; X = reply start offset
8B73 LDY fs_load_addr_hi ; Y = reply buffer high byte
8B75 .copy_reply_bytes←1← 8B7E BNE
LDA fs_cmd_data,x ; Load reply data byte
8B78 STA (fs_crc_lo),y ; Store to caller's buffer
8B7A INX ; Next source byte
8B7B INY ; Next destination byte
8B7C DEC fs_load_addr_2 ; Decrement remaining bytes
8B7E BNE copy_reply_bytes ; Loop until all bytes copied
8B80 BEQ gbpb_done ; ALWAYS branch to exit ALWAYS branch
8B82 .tube_transfer←1← 8B6F BNE
JSR tube_claim_loop ; Claim Tube transfer channel
8B85 LDA #1 ; A=1: Tube claim type 1 (write)
8B87 LDX fs_options ; X = param block address low
8B89 LDY fs_block_offset ; Y = param block address high
8B8B INX ; INX: advance past byte 0
8B8C BNE no_page_wrap ; No page wrap: keep Y
8B8E INY ; Page wrap: increment high byte
8B8F .no_page_wrap←1← 8B8C BNE
JSR tube_addr_claim ; Claim Tube address for transfer
8B92 LDX fs_load_addr ; X = reply data start offset
8B94 .tbcop1←1← 8BA2 BNE
LDA fs_cmd_data,x ; Load reply data byte
8B97 STA tube_data_register_3 ; Send byte to Tube via R3
8B9A INX ; Next source byte
8B9B LDY #6 ; Delay loop for slow Tube co-processor
8B9D .wait_tube_delay←1← 8B9E BNE
DEY ; Decrement delay counter
8B9E BNE wait_tube_delay ; Loop until delay complete
8BA0 DEC fs_load_addr_2 ; Decrement remaining bytes
8BA2 BNE tbcop1 ; Loop until all bytes sent to Tube
8BA4 LDA #&83 ; Release Tube after transfer complete
8BA6 JSR tube_addr_claim ; Release Tube address claim
8BA9 .gbpb_done←2← 8B80 BEQ← 8BFF BEQ
JMP return_a_zero ; Return via restore_args path
8BAC .gbpb8_read_dir←1← 8B3F BCS
LDY #9 ; OSGBPB 8: read filenames from dir
8BAE LDA (fs_options),y ; Byte 9: number of entries to read
8BB0 STA fs_func_code ; Store as reply count in command buffer
8BB3 LDY #5 ; Y=5: byte 5 = starting entry number
8BB5 LDA (fs_options),y ; Load starting entry number
8BB7 STA fs_data_count ; Store in command buffer
8BBA LDX #&0d ; X=&0D: command data extent X=preserved through header build
8BBC STX fs_reply_cmd ; Store extent in command buffer
8BBF LDY #2 ; Y=2: function code for dir read
8BC1 STY fs_load_addr ; Store 2 as reply data start offset
8BC3 STY fs_cmd_data ; Store 2 as command data byte
8BC6 INY ; Y=3: function code for header read Y=function code for HDRFN Y=&03
8BC7 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8BCA STX fs_load_addr_hi ; X=0 after FS command completes X=0 on success, &D6 on not-found
8BCC LDA fs_func_code ; Load reply entry count
8BCF STA (fs_options,x) ; Store at param block byte 0 (X=0)
8BD1 LDA fs_cmd_data ; Load entries-read count from reply
8BD4 LDY #9 ; Y=9: param block byte 9
8BD6 ADC (fs_options),y ; Add to starting entry number
8BD8 STA (fs_options),y ; Update param block with new position
8BDA LDA txcb_end ; Load total reply length
8BDC SBC #7 ; Subtract header (7 bytes) from reply len
8BDE STA fs_func_code ; Store adjusted length in command buffer
8BE1 STA fs_load_addr_2 ; Store as byte count for copy loop
8BE3 BEQ skip_copy_reply ; Zero bytes: skip copy
8BE5 JSR copy_reply_to_caller ; Copy reply data to caller's buffer
8BE8 .skip_copy_reply←1← 8BE3 BEQ
LDX #2 ; X=2: clear 3 bytes
8BEA .zero_cmd_bytes←1← 8BEE BPL
STA fs_data_count,x ; Zero out &0F07+X area
8BED DEX ; Next byte
8BEE BPL zero_cmd_bytes ; Loop for X=2,1,0
8BF0 JSR adjust_addrs_1 ; Adjust pointer by +1 (one filename read)
8BF3 SEC ; SEC for reverse adjustment
8BF4 DEC fs_load_addr_2 ; Reverse adjustment for updated counter
8BF6 LDA fs_cmd_data ; Load entries-read count
8BF9 STA fs_func_code ; Store in command buffer
8BFC JSR adjust_addrs ; Adjust param block addresses
8BFF BEQ gbpb_done ; Z=1: all done, exit
8C01 .tube_claim_loop←3← 8B82 JSR← 8C06 BCC← 8E20 JSR
LDA #&c3 ; A=&C3: Tube claim with retry
8C03 JSR tube_addr_claim ; Request Tube address claim
8C06 BCC tube_claim_loop ; C=0: claim failed, retry
8C08 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 &8C4B 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.

8C09 .fscv_3_star_cmd←1← 827D JMP
JSR save_fscv_args_with_ptrs ; Save A/X/Y and set up command ptr
8C0C LDX #&ff ; X=&FF: table index (pre-incremented)
8C0E STX fs_crflag ; Disable column formatting
8C10 STX escapable ; Enable escape checking
8C12 .scan_cmd_table←1← 8C2D BNE
LDY #&ff ; Y=&FF: input index (pre-incremented)
8C14 .decfir←1← 8C1F BEQ
INY ; Advance input pointer
8C15 INX ; Advance table pointer
8C16 .decmor←1← 8C31 BCS
LDA fs_cmd_match_table,x ; Load table character
8C19 BMI dispatch_cmd ; Bit 7: end of name, dispatch
8C1B EOR (fs_crc_lo),y ; XOR input char with table char
8C1D AND #&df ; Case-insensitive (clear bit 5)
8C1F BEQ decfir ; Match: continue comparing
8C21 DEX ; Mismatch: back up table pointer
8C22 .decmin←1← 8C26 BPL
INX ; Skip to end of table entry
8C23 LDA fs_cmd_match_table,x ; Load table byte
8C26 BPL decmin ; Loop until bit 7 set (end marker)
8C28 LDA (fs_crc_lo),y ; Check input for '.' abbreviation
8C2A INX ; Skip past handler high byte
8C2B CMP #&2e ; Is input '.' (abbreviation)?
8C2D BNE scan_cmd_table ; No: try next table entry
8C2F INY ; Yes: skip '.' in input
8C30 DEX ; Back to handler high byte
8C31 BCS decmor ; ALWAYS branch; dispatch via BMI
8C33 .dispatch_cmd←1← 8C19 BMI
PHA ; Push handler address high byte
8C34 LDA cmd_match_data,x ; Load handler address low byte
8C37 PHA ; Push handler address low byte
8C38 RTS ; Dispatch via RTS (addr-1 on stack)

FS command match table (COMTAB)

Format: command letters (bit 7 clear), then dispatch address as two big-endian bytes: high|(bit 7 set), low. The bit 7 set on the high byte marks the end of the command string. The PHA/PHA/RTS trick adds 1 to the stored (address-1). Matching is case-insensitive (AND &DF) and supports '.' abbreviation.

Entries: "I." → &80BD (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" → &8C4F (ex_handler: } terminator rejects *EXEC) "BYE"\r → &83AE (bye_handler: logoff) <catch-all> → &80BD (forward anything else to FS)

8C39 .fs_cmd_match_table←2← 8C16 LDA← 8C23 LDA
EOR #&2e ; Match last char against '.' for *I. abbreviation
8C3B EQUB &80 ; I. handler hi → &80BD (forward_star_cmd)
8C3C EQUB &BC ; I. handler lo
8C3D EQUS "I AM" ; "I AM" command string
8C41 EQUB &80 ; I AM handler hi (shared lo for I.)
8C42 EQUS "}EX" ; "}EX" command (} = special terminator)
8C45 EQUB &8C ; EX handler hi → &8C4F (ex_handler)
8C46 EQUS "NBYE" ; "NBYE" command (BYE with N prefix)
8C4A EQUB &0D ; CR terminator for BYE
8C4B EQUB &83 ; BYE handler hi → &83AE (bye_handler)
8C4C EQUB &AD ; BYE handler lo
8C4D EQUB &80 ; Catch-all hi → &80BD (forward_star_cmd)
8C4E EQUB &BC ; Catch-all lo
8C4F .ex_handler
LDX #1 ; X=1: force one entry per line for *EX
8C51 LDA #3 ; A=3: examine format code
8C53 BNE init_cat_params ; ALWAYS branch

*CAT handler (directory catalogue)

Initialises &B5=&0B (examine arg count) and &B7=&03 (column count). The catalogue protocol is multi-step: first sends FCREAD (&12: examine) to get the directory header, then sends FCUSER (&15: read user environment) to get CSD, disc, and library names, then sends FC &03 (examine entries) 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 (&B9) 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.

8C55 .fscv_5_cat
LDX #3 ; X=3: column count for multi-column layout
8C57 STX fs_crflag ; CRFLAG=3: first entry will trigger newline
8C59 LDY #&ff ; Y=&FF: mark as escapable
8C5B STY escapable ; Store escapable flag for Escape checking
8C5D INY ; Y=&00
8C5E LDA #&0b ; A=&0B: examine argument count
8C60 .init_cat_params←1← 8C53 BNE
STA fs_work_5 ; Store examine argument count
8C62 STX fs_work_7 ; Store column count
8C64 LDA #6 ; A=6: examine format type in command
8C66 STA fs_cmd_data ; Store format type at &0F05
8C69 JSR parse_filename_gs_y ; Set up command parameter pointers
8C6C LDX #1 ; X=1: copy dir name at cmd offset 1
8C6E JSR copy_string_to_cmd ; Copy directory name to command buffer
8C71 LDY #&12 ; Y=function code for HDRFN
8C73 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8C76 LDX #3 ; X=3: start printing from reply offset 3
8C78 JSR print_reply_bytes ; Print directory title (10 chars)
8C7B JSR print_inline ; Print '('
8C7E EQUS "(" ; "(" inline string data
8C7F LDA fs_reply_stn ; Load station number from FS reply
8C82 JSR print_decimal ; Print station number as decimal
8C85 JSR print_inline ; Print ') '
8C88 EQUS ") " ; ") " inline string data
8C8E LDY fs_access_level ; Access level byte: 0=Owner, non-zero=Public
8C91 BNE print_public ; Non-zero: Public access
8C93 JSR print_inline ; Print 'Owner' + CR
8C96 EQUS "Owner." ; "Owner" + CR inline string data
8C9C BNE print_user_env ; Always branches (print_inline sets N=1)
8C9E .print_public←1← 8C91 BNE
JSR print_inline ; Print 'Public' + CR
8CA1 EQUS "Public." ; "Public" + CR inline string data
8CA8 .print_user_env←1← 8C9C BNE
LDY #&15 ; Y=function code for HDRFN
8CAA JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8CAD INX ; X=1: past command code byte
8CAE LDY #&10 ; Y=&10: print 16 characters
8CB0 JSR print_reply_counted ; Print disc/CSD name from reply
8CB3 JSR print_inline ; Print ' Option '
8CB6 EQUS " Option " ; " Option " inline string data
8CC1 LDA fs_boot_option ; Load boot option from workspace
8CC4 TAX ; X = boot option for name table lookup
8CC5 JSR print_hex_byte ; Print boot option as hex digit
8CC8 JSR print_inline ; Print ' ('
8CCB EQUS " (" ; " (" inline string data
8CCD LDY option_name_offsets,x ; Load string offset for option name
8CD0 .print_option_name←1← 8CD9 BNE
LDA option_name_offsets,y ; Load character from option name string
8CD3 BMI done_option_name ; Bit7 set: string terminated, done
8CD5 JSR osasci ; Write character
8CD8 INY ; Next character
8CD9 BNE print_option_name ; Continue printing option name
8CDB .done_option_name←1← 8CD3 BMI
JSR print_inline ; Print ')' + CR + 'Dir. '
8CDE EQUS ").Dir. " ; ") CR Dir. " inline string data
8CE5 LDX #&11 ; X=&11: directory name offset in reply
8CE7 JSR print_reply_bytes ; Print current directory name
8CEA JSR print_inline ; Print ' Lib. '
8CED EQUS " Lib. " ; " Lib. " inline string data
8CF7 LDX #&1b ; X=&1B: library name offset in reply
8CF9 JSR print_reply_bytes ; Print library name
8CFC JSR osnewl ; Print two CRs (blank line) Write newline (characters 10 and 13)
8CFF .fetch_dir_batch←1← 8D33 BNE
STY fs_func_code ; Store entry start offset for request
8D02 STY fs_work_4 ; Save start offset in zero page for loop
8D04 LDX fs_work_5 ; Load examine arg count for batch size
8D06 STX fs_data_count ; Store as request count at &0F07
8D09 .cat_examine_loop
LDX fs_work_7 ; Load column count for display format
8D0B STX fs_cmd_data ; Store column count in command data
8D0E LDX #3 ; X=3: copy directory name at offset 3
8D10 JSR copy_string_to_cmd ; Append directory name to examine command
8D13 LDY #3 ; Y=function code for HDRFN
8D15 JSR prepare_fs_cmd ; Prepare FS command buffer (12 references)
8D18 INX ; X past command code byte in reply
8D19 LDA fs_cmd_data ; Load entry count from reply
8D1C BNE process_entries ; Zero entries returned = end of directory
8D1E JMP osnewl ; Write newline (characters 10 and 13)
8D21 .process_entries←1← 8D1C BNE
PHA ; Save entry count for batch processing
8D22 .scan_entry_terminator←1← 8D26 BPL
INY ; Advance Y past entry data bytes
8D23 LDA fs_cmd_data,y ; Read entry byte from reply buffer
8D26 BPL scan_entry_terminator ; Loop until high-bit terminator found
8D28 STA fs_cmd_lib,y ; Store terminator as print boundary
8D2B JSR cat_column_separator ; Print/format this directory entry
8D2E PLA ; Restore entry count from stack
8D2F CLC ; CLC for addition
8D30 ADC fs_work_4 ; Advance start offset by entry count
8D32 TAY ; Y = new entry start offset
8D33 BNE fetch_dir_batch ; More entries: fetch next batch
8D35 .print_reply_bytes←3← 8C78 JSR← 8CE7 JSR← 8CF9 JSR
LDY #&0a ; Y=&0A: default print 10 characters
8D37 .print_reply_counted←2← 8CB0 JSR← 8D3F BNE
LDA fs_cmd_data,x ; Load reply byte at offset X
8D3A JSR osasci ; Write character
8D3D INX ; Next reply byte
8D3E DEY ; Decrement character count
8D3F 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 option_name_offsets (&8D42), whose four bytes are ; offsets:
; &2B→option_name_offsets+&2B "Off",
; &3E→option_name_offsets+&3E "Load",
; &66→option_name_offsets+&66 "Run",
; &18→option_name_offsets+&18 "Exec"
; Each string is terminated by the next instruction's opcode
; having bit 7 set (e.g. LDA #imm = &A9, RTS = &60).
8D41 .return_9
RTS ; Return from column separator
8D42 .option_name_offsets←2← 8CCD LDY← 8CD0 LDA
EQUS "+>f" ; Boot option name offsets (4 entries)
8D45 EQUB &18 ; Offset &18: Exec option name start
8D46 EQUS "L.!" ; Boot option name string data "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 is indexed by the boot option value (0-3); each byte is the low byte of the string address, with the page high byte loaded separately: Option 0 (Off): offset &55 → boot_option_offsets = bare CR Option 1 (Load): offset &46 → "L.!BOOT" (the bytes &4C='L', &2E='.', &21='!' precede "BOOT" + CR at &8D4D) Option 2 (Run): offset &48 → boot_cmd_strings-1 = "!BOOT" Option 3 (Exec): offset &4E → &8D4E = "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 &8D55 terminates "E.!BOOT" AND doubles as the bare-CR command for boot option 0.

8D49 .boot_cmd_strings
EQUS "BOOT" ; "BOOT" command string tail
8D4D EQUB &0D ; CR terminator for BOOT string
8D4E EQUS "E.!BOOT" ; "E.!BOOT" exec boot command

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 (&8D68) via LDX l8d56,Y with Y=boot_option, then LDY #&8D, JMP oscli. See boot_cmd_strings for the target strings.

8D55 .boot_option_offsets
ORA l4655 ; Opt 0 (Off): bare CR at &8D55 Opt 1 (Load): L.!BOOT at &8D46
8D58 PHA ; Opt 2 (Run): !BOOT at boot_cmd_strings-1
8D59 LSR l7845 ; Opt 3 (Exec): E.!BOOT at &8D4E
8D5C ADC l0063 ; Boot string overlap: "ec" tail of "Exec"
8D5E .print_hex_bytes←2← 87F3 JSR← 87F8 JSR
LDX #4 ; X=4: print 4 hex bytes
8D60 .num01←1← 87FF JSR
LDA (fs_options),y ; Load byte from parameter block
8D62 JSR print_hex_byte ; Print as two hex digits
8D65 DEY ; Next byte (descending)
8D66 DEX ; Count down
8D67 EQUB &D0 ; Loop until 4 bytes printed
8D68 EQUB &F7 ; BNE operand (also boot offset data)
8D69 .print_space←1← 87E9 JSR
EQUB &A9 ; A=space character
8D6A EQUB &20 ; LDA #&20 operand (space)
8D6B EQUB &D0 ; BNE opcode (also string overlap)
8D6C EQUS "ZOff" ; Boot option name "Exec" starts here
fall through ↓

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.

On ExitXnext free position in cmd buffer
Ystring length (incl CR)
A0 (from EOR &0D with final CR)
8D70 .infol2←5← 8099 JSR← 80BD JSR← 870B JSR← 8905 JSR← 8DE2 JSR
.copy_filename←5← 8099 JSR← 80BD JSR← 870B JSR← 8905 JSR← 8DE2 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)
8D72 .copy_string_to_cmd←6← 87B8 JSR← 88FE JSR← 8920 JSR← 89E4 JSR← 8C6E JSR← 8D10 JSR
LDY #0 ; Start copying from offset 0
8D74 .copy_string_from_offset←1← 8D7D BNE
LDA (fs_crc_lo),y ; Load next byte from source string
8D76 STA fs_cmd_data,x ; Store to FS command buffer (&0F05+X)
8D79 INX ; Advance write position
8D7A INY ; Advance source pointer
8D7B EOR #&0d ; XOR with CR: result=0 if byte was CR
8D7D BNE copy_string_from_offset ; Loop until CR copied
8D7F .return_5←1← 8D89 BMI
RTS ; Return; X = next free position in buffer
8D80 EQUS "Load" ; "Load" boot option name string

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.

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

8D8D .cat_column_separator←1← 8D2B JSR
LDY fs_crflag ; Null byte: check column counter
8D8F BMI print_cr ; Negative: print CR (no columns)
8D91 INY ; Advance column counter
8D92 TYA ; Transfer to A for modulo
8D93 AND #3 ; Modulo 4 columns
8D95 STA fs_crflag ; Update column counter
8D97 BEQ print_cr ; Column 0: start new line
8D99 JSR print_inline ; Print 2-space column separator
8D9C EQUS " " ; " " column separator string
8D9E BNE next_dir_entry ; ALWAYS branch to next byte
8DA0 .print_cr←2← 8D8F BMI← 8D97 BEQ
LDA #&0d ; CR = carriage return
8DA2 .print_newline←1← 8D8B BNE
JSR osasci ; Write character 13
8DA5 .next_dir_entry←1← 8D9E BNE
INX ; Next byte in reply buffer
8DA6 BNE print_dir_from_offset ; Loop until end of buffer
8DA8 EQUS "Run" ; "Run" boot option name string
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)
8DAB .print_decimal←2← 8236 JSR← 8C82 JSR
TAY ; Y = value to print
8DAC LDA #&64 ; Divisor = 100 (hundreds digit)
8DAE JSR print_decimal_digit ; Print hundreds digit
8DB1 LDA #&0a ; Divisor = 10 (tens digit)
8DB3 JSR print_decimal_digit ; Print tens digit
8DB6 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
8DB8 .print_decimal_digit←2← 8DAE JSR← 8DB3 JSR
STA fs_error_ptr ; Save divisor to workspace
8DBA TYA ; A = dividend (from Y)
8DBB LDX #&2f ; X = &2F = ASCII '0' - 1
8DBD SEC ; Prepare for subtraction
8DBE .divide_subtract←1← 8DC1 BCS
INX ; Count one subtraction (next digit value)
8DBF SBC fs_error_ptr ; A = A - divisor
8DC1 BCS divide_subtract ; Loop while A >= 0 (borrow clear)
8DC3 ADC fs_error_ptr ; Undo last subtraction: A = remainder
8DC5 TAY ; Y = remainder for caller
8DC6 TXA ; A = X = ASCII digit character
8DC7 .print_digit←2← 8DD9 BCC← 8DDD BNE
JMP osasci ; Write character

Print A as two hexadecimal digits

Prints the byte in A as two hex digits (e.g. &3F prints "3F"). Saves A, shifts high nibble down, prints it via print_hex_nibble, then recovers the original A and prints the low nibble. The nibble-to-hex conversion uses ORA #&30 / CMP #&3A to distinguish 0-9 (ASCII &30-&39) from A-F (add 7 more). New in 3.65: moved inline from &9F9D (end of ROM in 3.62).

8DCA .print_hex_byte←2← 8CC5 JSR← 8D62 JSR
PHA ; Save original byte
8DCB LSR ; Shift high nibble to low position
8DCC LSR ; LSR (continued)
8DCD LSR ; LSR (continued)
8DCE LSR ; LSR (continued): A = high nibble
8DCF JSR print_hex_nibble ; Print high nibble as hex digit
8DD2 PLA ; Recover original byte
8DD3 AND #&0f ; Mask to low nibble
8DD5 .print_hex_nibble←1← 8DCF JSR
ORA #&30 ; Convert nibble to ASCII: &00-&09 -> '0'-'9'
8DD7 CMP #&3a ; Digit or letter? &3A = ':'
8DD9 BCC print_digit ; Digit 0-9: print directly
8DDB ADC #6 ; Letter A-F: add 7 (carry already set)
8DDD BNE print_digit ; ALWAYS branch to print_digit

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.

8DDF .fscv_2_star_run
JSR parse_filename_gs ; Parse filename from command line
8DE2 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 (tube_flag != 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.

8DE5 .fsreply_4_notify_exec
LDY #0 ; Y=0: start of text for GSINIT
8DE7 CLC ; CLC before GSINIT call
8DE8 JSR gsinit ; GSINIT/GSREAD: skip past the filename
8DEB .skip_gs_filename←1← 8DEE BCC
JSR gsread ; Read next filename character
8DEE BCC skip_gs_filename ; C=0: more characters, keep reading
8DF0 JSR skip_spaces ; Skip spaces after filename
8DF3 CLC ; Calculate context addr = text ptr + Y
8DF4 TYA ; Y = offset past filename end
8DF5 ADC os_text_ptr ; Add text pointer low byte
8DF7 STA fs_cmd_context ; Store context address low byte
8DFA LDA os_text_ptr_hi ; Load text pointer high byte
8DFC ADC #0 ; Add carry from low byte addition
8DFE STA fs_context_hi ; Store context address high byte
8E01 LDX #&0e ; X=&0E: FS command buffer offset
8E03 STX fs_block_offset ; Store block offset for FS command
8E05 LDA #&10 ; A=&10: 16 bytes of command data
8E07 STA fs_options ; Store options byte
8E09 STA fs_work_16 ; Store to FS workspace
8E0C LDX #&4a ; X=&4A: TXCB size for load command
8E0E LDY #5 ; Y=5: FCCMND (load as command)
8E10 JSR send_fs_examine ; Send FS examine/load command
8E13 LDA tube_flag ; Check for Tube co-processor
8E16 BEQ exec_local ; No Tube: execute locally
8E18 ADC fs_load_upper ; Check load address upper bytes
8E1B ADC fs_addr_check ; Continue address range check
8E1E BCS exec_local ; Carry set: not Tube space, exec locally
8E20 JSR tube_claim_loop ; Claim Tube transfer channel
8E23 LDX #9 ; X=9: source offset in FS reply
8E25 LDY #&0f ; Y=&0F: page &0F (FS command buffer)
8E27 LDA #4 ; A=4: Tube transfer type 4 (256-byte)
8E29 JMP tube_addr_claim ; Transfer data to Tube co-processor
8E2C .exec_local←2← 8E16 BEQ← 8E1E BCS
ROL ; ROL: restore A (undo ADC carry)
8E2D .exec_at_load_addr
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
8E30 .fsreply_5_set_lib
STY fs_lib_handle ; Save library handle from FS reply
8E33 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
8E35 .fsreply_3_set_csd
STY fs_csd_handle ; Store CSD handle from FS reply
8E38 .jmp_restore_args←2← 8E33 BCC← 8E49 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.

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

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

8E4B .boot_cmd_execute
LDY fs_boot_option ; Y = boot option from FS workspace
8E4E LDX boot_string_offsets,y ; X = command string offset from table
8E51 LDY #&8d ; Y = &8D (high byte of command address)
8E53 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.

On ExitAhandle*12 or 0 if invalid
Yworkspace offset or 0 if invalid
Cclear if valid, set if invalid
8E56 .load_handle_calc_offset←2← 8E70 JSR← 8E80 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
8E58 .calc_handle_offset←3← 82F7 JSR← 8F90 JSR← 8FA9 JSR
ASL ; A = handle * 2
8E59 ASL ; A = handle * 4
8E5A PHA ; Push handle*4 onto stack
8E5B ASL ; A = handle * 8
8E5C TSX ; X = stack pointer
8E5D ADC error_text,x ; A = handle*8 + handle*4 = handle*12
8E60 TAY ; Y = offset into handle workspace
8E61 PLA ; Clean up stack (discard handle*4)
8E62 CMP #&48 ; Offset >= &48? (6 handles max)
8E64 BCC return_6 ; Valid: return with C clear
8E66 LDY #0 ; Invalid: Y = 0
8E68 TYA ; A = 0, C set (error) A=&00
8E69 .return_6←1← 8E64 BCC
.return_calc_handle←1← 8E64 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.
8E6A .net_1_read_handle
LDY #&6f ; Y=&6F: RX buffer handle offset
8E6C LDA (net_rx_ptr),y ; Read handle from RX packet
8E6E 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.

On ExitAhandle value (0 if closed/invalid)
8E70 .net_2_read_handle_entry
JSR load_handle_calc_offset ; Look up handle &F0 in workspace
8E73 BCS rxpol2 ; Invalid handle: return 0
8E75 LDA (nfs_workspace),y ; Load stored handle value
8E77 CMP #&3f ; &3F = unused/closed slot marker
8E79 BNE store_handle_return ; Slot in use: return actual value
8E7B .rxpol2←2← 8E73 BCS← 8E83 BCS
LDA #0 ; Return 0 for closed/invalid handle
8E7D .store_handle_return←2← 8E6E BCC← 8E79 BNE
STA osword_pb_ptr ; Store result back to &F0
8E7F 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. Returns via RTS (earlier versions preserved the carry flag across the write using ROL/ROR on rx_flags, but 3.60 simplified this).

On ExitA&3F (close marker) or 0 if invalid
8E80 .net_3_close_handle
JSR load_handle_calc_offset ; Look up handle &F0 in workspace
8E83 BCS rxpol2 ; Invalid handle: return 0
8E85 LDA #&3f ; &3F = '?' marks slot as unused
8E87 STA (nfs_workspace),y ; Write close marker to workspace slot
8E89 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 range check and dispatch at osword_12_handler (&8E90).

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

OSWORD range check, dispatch, and register restore

Reached by fall-through from svc_8_osword with A = OSWORD number minus &0F. Rejects indices >= 5 (only OSWORDs &0F-&13 are handled). Dispatches to the appropriate handler via fs_osword_dispatch, then on return copies 3 bytes from (net_rx_ptr)+0..2 back to &AA-&AC (restoring the param block pointer that was saved by fs_osword_dispatch before dispatch).

The actual OSWORD &12 sub-function dispatch (read/set station, protection, handles etc.) lives in sub_c8f01.

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

PHA/PHA/RTS dispatch for filing system OSWORDs

Saves the param block pointer (&AA-&AC) to (net_rx_ptr) and reads the sub-function code from (&F0)+1, then dispatches via the 5-entry table at &8EB8 (low) / &8EBD (high) using PHA/PHA/RTS. The RTS at the end of the dispatched handler returns here, after which the caller restores &AA-&AC.

8EA2 .fs_osword_dispatch←1← 8E94 JSR
TAX ; X = sub-function code for table lookup
8EA3 LDA fs_osword_tbl_hi,x ; Load handler address high byte from table
8EA6 PHA ; Push high byte for RTS dispatch
8EA7 LDA osword_handler_lo,x ; Load handler address low byte from table
8EAA .fs_osword_tbl_lo
PHA ; Dispatch table: low bytes for OSWORD &0F-&13 handlers
8EAB LDY #2 ; Y=2: save 3 bytes (&AA-&AC)
8EAD .save1←1← 8EB3 BPL
LDA osword_flag,y ; Load param block pointer byte
8EB0 STA (net_rx_ptr),y ; Save to NFS workspace via (net_rx_ptr)
8EB2 DEY ; Next byte (descending)
8EB3 BPL save1 ; Loop for all 3 bytes
8EB5 INY ; Y=0 after BPL exit; INY makes Y=1
8EB6 LDA (osword_pb_ptr),y ; Read sub-function code from (&F0)+1
8EB8 STY svc_state ; Store Y=1 to &A9
8EBA .return_7←2← 8E8E BMI← 8E92 BCS
RTS ; RTS dispatches to pushed handler address
8EBB .osword_handler_lo←1← 8EA7 LDA
EQUB <(osword_0f_handler-1) ; lo(osword_0f_handler-1): OSWORD &0F
8EBC EQUB <(osword_10_handler-1) ; lo(osword_10_handler-1): OSWORD &10
8EBD EQUB <(osword_11_handler-1) ; lo(osword_11_handler-1): OSWORD &11
8EBE EQUB <(osword_12_dispatch-1) ; lo(osword_12_dispatch-1): OSWORD &12
8EBF EQUB <(econet_tx_rx-1) ; lo(econet_tx_rx-1): OSWORD &13
8EC0 .fs_osword_tbl_hi←1← 8EA3 LDA
EQUB >(osword_0f_handler-1) ; Dispatch table: high bytes for OSWORD &0F-&13 handlers
8EC1 EQUB >(osword_10_handler-1) ; hi(osword_10_handler-1): OSWORD &10
8EC2 EQUB >(osword_11_handler-1) ; hi(osword_11_handler-1): OSWORD &11
8EC3 EQUB >(osword_12_dispatch-1) ; hi(osword_12_dispatch-1): OSWORD &12
8EC4 EQUB >(econet_tx_rx-1) ; hi(econet_tx_rx-1): OSWORD &13

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
8EC5 .osword_0f_handler
ASL tx_clear_flag ; ASL TXCLR: C=1 means TX free to claim
8EC8 TYA ; Save Y (param block high) for later
8EC9 BCC readry ; C=0: TX busy, return error status
8ECB LDA net_rx_ptr_hi ; User TX CB in workspace page (high byte)
8ECD STA ws_ptr_hi ; Set param block high byte
8ECF STA nmi_tx_block_hi ; Set LTXCBP high byte for low-level TX
8ED1 LDA #&6f ; &6F: offset into workspace for user TXCB
8ED3 STA ws_ptr_lo ; Set param block low byte
8ED5 STA nmi_tx_block ; Set LTXCBP low byte for low-level TX
8ED7 LDX #&0f ; X=15: copy 16 bytes (OSWORD param block)
8ED9 JSR copy_param_workspace ; Copy param block to user TX control block
8EDC JMP start_adlc_tx ; Start user transmit via BRIANX

OSWORD &11 handler: read JSR arguments (READRA)

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

8EDF .osword_11_handler
LDA net_rx_ptr_hi ; Set source high byte from workspace page
8EE1 STA ws_ptr_hi ; Store as copy source high byte in &AC
8EE3 LDY #&7f ; JSRSIZ at workspace offset &7F
8EE5 LDA (net_rx_ptr),y ; Load buffer size from workspace
8EE7 INY ; Y=&80: start of JSR argument data Y=&80
8EE8 STY ws_ptr_lo ; Store &80 as copy source low byte
8EEA TAX ; X = buffer size (loop counter)
8EEB DEX ; X = size-1 (0-based count for copy)
8EEC LDY #0 ; Y=0: start of destination param block
8EEE JSR copy_param_workspace ; Copy X+1 bytes from workspace to param
8EF1 JMP clear_jsr_protection ; Clear JSR protection status (CLRJSR)
8EF4 .read_args_size←1← 8F4F BEQ
LDY #&7f ; Y=&7F: JSRSIZ offset (READRB entry)
8EF6 LDA (net_rx_ptr),y ; Load buffer size from workspace
8EF8 LDY #1 ; Y=1: param block offset for size byte
8EFA STA (osword_pb_ptr),y ; Store buffer size to (&F0)+1
8EFC INY ; Y=2: param block offset for args size Y=&02
8EFD LDA #&80 ; A=&80: argument data starts at offset &80
8EFF .readry←1← 8EC9 BCC
STA (osword_pb_ptr),y ; Store args start offset to (&F0)+2
8F01 RTS ; Return
8F02 .osword_12_offsets←1← 8F16 LDA
EQUB &FF, &01 ; OSWORD &12 workspace offset table

OSWORD &12 handler: dispatch sub-functions 0-5

Range-checks the sub-function code from the param block. Sub-functions 4-5 go to read/set station number. Sub-functions 0-3 select the appropriate workspace page (static &0D or dynamic) and offset, then fall through to the bidirectional param block copy loop.

8F04 .osword_12_dispatch
CMP #6 ; OSWORD &12: range check sub-function
8F06 BCS rsl1 ; Sub-function >= 6: not supported
8F08 CMP #4 ; Check for sub-functions 4-5
8F0A BCS rssl1 ; Sub-function 4 or 5: read/set protection
8F0C LSR ; LSR: 0->0, 1->0, 2->1, 3->1
8F0D LDX #&0d ; X=&0D: default to static workspace page
8F0F TAY ; Transfer LSR result to Y for indexing
8F10 BEQ set_workspace_page ; Y=0 (sub 0-1): use page &0D
8F12 LDX nfs_workspace_hi ; Y=1 (sub 2-3): use dynamic workspace
8F14 .set_workspace_page←1← 8F10 BEQ
STX ws_ptr_hi ; Store workspace page in &AC (hi byte)
8F16 LDA osword_12_offsets,y ; Load offset: &FF (sub 0-1) or &01 (sub 2-3)
8F19 STA ws_ptr_lo ; Store offset in &AB (lo byte)
8F1B LDX #1 ; X=1: copy 2 bytes
8F1D LDY #1 ; Y=1: start at param block offset 1
fall through ↓

Bidirectional copy loop between param block and workspace

If C=1, copies from OSWORD param block (&F0),Y to workspace (&AB),Y. In either case, loads from workspace and stores to param block. Loops for X+1 bytes. Used by OSWORD &0F, &10, &11, and &12 handlers.

On EntryC1=copy param to workspace first, 0=workspace to param only
Xbyte count minus 1
Ystarting offset
On ExitAlast byte copied
X&FF
Ystart + count + 1
8F1F .copy_param_workspace←4← 8ED9 JSR← 8EEE JSR← 8F2B BPL← 8FC0 JSR
BCC skip_param_write ; C=0: skip param-to-workspace copy
8F21 LDA (osword_pb_ptr),y ; C=1: copy from param to workspace
8F23 STA (ws_ptr_lo),y ; Store param byte to workspace
8F25 .skip_param_write←1← 8F1F 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 (&AB),Y (param to workspace) C=0: copy X+1 bytes from (&AB),Y to (&F0),Y (workspace to param)

On EntryC1=param to workspace, 0=workspace to param
Xbyte count minus 1
Ystarting offset
On ExitAlast byte copied
X&FF
Ystart + count + 1
8F27 .copy_param_block
STA (osword_pb_ptr),y ; Store to param block (no-op if C=1)
8F29 INY ; Advance to next byte
8F2A DEX ; Decrement remaining count
8F2B BPL copy_param_workspace ; Loop while bytes remain
8F2D .logon3
.return_copy_param
RTS ; Return
8F2E .rssl1←1← 8F0A BCS
LSR ; LSR A: test bit 0 of sub-function
8F2F INY ; Y=1: offset for protection byte
8F30 LDA (osword_pb_ptr),y ; Load protection byte from param block
8F32 BCS rssl2 ; C=1 (odd sub): set protection
8F34 LDA prot_status ; C=0 (even sub): read current status
8F37 STA (osword_pb_ptr),y ; Return current value to param block
8F39 .rssl2←1← 8F32 BCS
STA prot_status ; Update protection status
8F3C STA saved_jsr_mask ; Also save as JSR mask backup
8F3F RTS ; Return
8F40 .read_fs_handle←1← 8F4B BEQ
LDY #&14 ; Y=&14: RX buffer offset for FS handle
8F42 LDA (net_rx_ptr),y ; Read FS reply handle from RX data
8F44 LDY #1 ; Y=1: param block byte 1
8F46 STA (osword_pb_ptr),y ; Return handle to caller's param block
8F48 RTS ; Return
8F49 .rsl1←1← 8F06 BCS
CMP #8 ; Sub-function 8: read FS handle
8F4B BEQ read_fs_handle ; Match: read handle from RX buffer
8F4D CMP #9 ; Sub-function 9: read args size
8F4F BEQ read_args_size ; Match: read ARGS buffer info
8F51 BPL return_last_error ; Sub >= 10 (bit 7 clear): read error
8F53 LDY #3 ; Y=3: start from handle 3 (descending)
8F55 LSR ; LSR: test read/write bit
8F56 BCC readc1 ; C=0: read handles from workspace
8F58 STY ws_page ; Init loop counter at Y=3
8F5A .copy_handles_to_ws←1← 8F69 BNE
LDY ws_page ; Reload loop counter
8F5C LDA (osword_pb_ptr),y ; Read handle from caller's param block
8F5E JSR handle_to_mask_a ; Convert handle number to bitmask
8F61 TYA ; TYA: get bitmask result
8F62 LDY ws_page ; Reload loop counter
8F64 STA fs_server_net,y ; Store bitmask to FS server table
8F67 DEC ws_page ; Next handle (descending)
8F69 BNE copy_handles_to_ws ; Loop for handles 3,2,1
8F6B RTS ; Return
8F6C .return_last_error←1← 8F51 BPL
INY ; Y=1 (post-INY): param block byte 1
8F6D LDA fs_last_error ; Read last FS error code
8F70 STA (osword_pb_ptr),y ; Return error to caller's param block
8F72 RTS ; Return
8F73 .readc1←2← 8F56 BCC← 8F7C BNE
LDA fs_server_net,y ; A=single-bit bitmask
8F76 JSR mask_to_handle ; Convert bitmask to handle number (FS2A)
8F79 STA (osword_pb_ptr),y ; A=handle number (&20-&27) Y=preserved
8F7B DEY ; Next handle (descending)
8F7C BNE readc1 ; Loop for handles 3,2,1
8F7E 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
8F7F .osword_10_handler
LDX nfs_workspace_hi ; Workspace page high byte
8F81 STX ws_ptr_hi ; Set up pointer high byte in &AC
8F83 STY ws_ptr_lo ; Save param block high byte in &AB
8F85 ROR rx_flags ; Disable user RX during CB operation
8F88 LDA (osword_pb_ptr),y ; Read first byte of param block
8F8A STA osword_flag ; Save: 0=open new, non-zero=read RXCB
8F8C BNE read_rxcb ; Non-zero: read specified RXCB
8F8E LDA #3 ; Start scan from RXCB #3 (0-2 reserved)
8F90 .scan0←1← 8FA2 BNE
JSR calc_handle_offset ; Convert RXCB number to workspace offset
8F93 BCS openl4 ; Invalid RXCB: return zero
8F95 LSR ; LSR twice: byte offset / 4
8F96 LSR ; Yields RXCB number from offset
8F97 TAX ; X = RXCB number for iteration
8F98 LDA (ws_ptr_lo),y ; Read flag byte from RXCB workspace
8F9A BEQ openl4 ; Zero = end of CB list
8F9C CMP #&3f ; &3F = deleted slot, free for reuse
8F9E BEQ scan1 ; Found free slot
8FA0 INX ; Try next RXCB
8FA1 TXA ; A = next RXCB number
8FA2 BNE scan0 ; Continue scan (always branches)
8FA4 .scan1←1← 8F9E BEQ
TXA ; A = free RXCB number
8FA5 LDX #0 ; X=0 for indexed indirect store
8FA7 STA (osword_pb_ptr,x) ; Return RXCB number to caller's byte 0
8FA9 .read_rxcb←1← 8F8C BNE
JSR calc_handle_offset ; Convert RXCB number to workspace offset
8FAC BCS openl4 ; Invalid: write zero to param block
8FAE DEY ; Y = offset-1: points to flag byte
8FAF STY ws_ptr_lo ; Set &AB = workspace ptr low byte
8FB1 LDA #&c0 ; &C0: test mask for flag byte
8FB3 LDY #1 ; Y=1: flag byte offset in RXCB
8FB5 LDX #&0b ; Enable interrupts before transmit
8FB7 CPY osword_flag ; Compare Y(1) with saved byte (open/read)
8FB9 ADC (ws_ptr_lo),y ; ADC flag: test if slot is in use
8FBB BEQ openl6 ; Dest station = &FFFF (accept reply from any station)
8FBD BMI openl7 ; Negative: slot has received data
8FBF .copy_rxcb_to_param←1← 8FCF BNE
CLC ; C=0: workspace-to-param direction
8FC0 .openl6←1← 8FBB BEQ
JSR copy_param_workspace ; Copy RXCB data to param block
8FC3 BCS reenable_rx ; Done: skip deletion on error
8FC5 LDA #&3f ; Mark CB as consumed (consume-once)
8FC7 LDY #1 ; Y=1: flag byte offset
8FC9 STA (ws_ptr_lo),y ; Write &3F to mark slot deleted
8FCB BNE reenable_rx ; Branch to exit (always taken) ALWAYS branch
8FCD .openl7←1← 8FBD BMI
ADC #1 ; Advance through multi-byte field
8FCF BNE copy_rxcb_to_param ; Loop until all bytes processed
8FD1 DEY ; Y=-1 → Y=0 after STA below
8FD2 .openl4←3← 8F93 BCS← 8F9A BEQ← 8FAC BCS
STA (osword_pb_ptr),y ; Return zero (no free RXCB found)
8FD4 .reenable_rx←2← 8FC3 BCS← 8FCB BNE
ROL rx_flags ; Re-enable user RX
8FD7 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 &1C. Also reads the data length from (&F0)+1 and adds it to &F0 to compute the end address at offset &20.

On EntryCclear for ADC
8FD8 .setup_rx_buffer_ptrs←1← 900B JSR
LDY #&1c ; Y=&1C: workspace offset for RX data start
8FDA LDA osword_pb_ptr ; A = base address low byte
8FDC ADC #1 ; A = base + 1 (skip length byte)
8FDE JSR store_16bit_at_y ; Receive data blocks until command byte = &00 or &0D
8FE1 LDY #1 ; Read data length from (&F0)+1
8FE3 LDA (osword_pb_ptr),y ; A = data length byte
8FE5 LDY #&20 ; Workspace offset &20 = RX data end
8FE7 ADC osword_pb_ptr ; A = base + length = end address low
8FE9 .store_16bit_at_y←1← 8FDE JSR
STA (nfs_workspace),y ; Store low byte of 16-bit address
8FEB INY ; Advance to high byte offset
8FEC LDA osword_pb_ptr_hi ; A = high byte of base address
8FEE ADC #0 ; Add carry for 16-bit addition
8FF0 STA (nfs_workspace),y ; Store high byte
8FF2 RTS ; Return

Econet transmit/receive handler

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

On EntryA0=set up and transmit, >=1=handle TX result
8FF3 .econet_tx_rx
CMP #1 ; A=0: set up and transmit; A>=1: handle result
8FF5 BCS handle_tx_result ; A >= 1: handle TX result
8FF7 LDY #&23 ; Y=&23: start of template (descending)
8FF9 .dofs01←1← 9006 BNE
LDA init_tx_ctrl_block,y ; Load ROM template byte
8FFC BNE store_txcb_byte ; Non-zero = use ROM template byte as-is
8FFE LDA nmi_sub_table,y ; Zero = substitute from NMI workspace
9001 .store_txcb_byte←1← 8FFC BNE
STA (nfs_workspace),y ; Store to dynamic workspace
9003 DEY ; Descend through template
9004 CPY #&17 ; Stop at offset &17
9006 BNE dofs01 ; Loop until all bytes copied
9008 INY ; Y=&18: TX block starts here
9009 STY net_tx_ptr ; Point net_tx_ptr at workspace+&18
900B JSR setup_rx_buffer_ptrs ; Set up RX buffer start/end pointers
900E LDY #2 ; Y=2: port byte offset in RXCB
9010 LDA #&90 ; A=&90: FS reply port
9012 STA escapable ; Mark as escapable operation
9014 STA (osword_pb_ptr),y ; Store port &90 at (&F0)+2
9016 INY ; Y=&03
9017 INY ; Y=&04: advance to station address Y=&04
9018 .copy_fs_addr←1← 9020 BNE
LDA fs_context_base,y ; Copy FS station addr from workspace
901B STA (osword_pb_ptr),y ; Store to RX param block
901D INY ; Next byte
901E CPY #7 ; Done 3 bytes (Y=4,5,6)?
9020 BNE copy_fs_addr ; No: continue copying
9022 LDA nfs_workspace_hi ; High byte of workspace for TX ptr
9024 STA net_tx_ptr_hi ; Store as TX pointer high byte
9026 CLI ; Enable interrupts before transmit
9027 JSR tx_poll_ff ; Transmit with full retry
902A LDY #&20 ; Y=&20: RX end address offset
902C LDA #&ff ; Set RX end address to &FFFF (accept any length)
902E STA (nfs_workspace),y ; Store end address low byte (&FF)
9030 INY ; Y=&21
9031 STA (nfs_workspace),y ; Store end address high byte (&FF)
9033 LDY #&19 ; Y=&19: port byte in workspace RXCB
9035 LDA #&90 ; A=&90: FS reply port
9037 STA (nfs_workspace),y ; Store port to workspace RXCB
9039 DEY ; Y=&18
903A LDA #&7f ; A=&7F: flag byte = waiting for reply
903C STA (nfs_workspace),y ; Store flag byte to workspace RXCB
903E JMP waitfs ; Jump to RX poll (BRIANX)
9041 .handle_tx_result←1← 8FF5 BCS
PHP ; Save processor flags
9042 LDY #1 ; Y=1: first data byte offset
9044 LDA (osword_pb_ptr),y ; Load first data byte from RX buffer
fall through ↓

FS response data relay (DOFS)

Entered from the econet_tx_rx response handler at &903E after loading the first data byte from the RX buffer. Saves the command byte and station address from the received packet into (net_rx_ptr)+&71/&72, then iterates through remaining data bytes. Each byte is stored at (net_rx_ptr)+&7D, the control block is set up via ctrl_block_setup_alt, and the packet is transmitted. Loops until a &0D terminator or &00 null is found. The branch at &9053 (BNE dofs2) handles the first-packet case where the data length field at (net_rx_ptr)+&7B is adjusted.

9046 .net_write_char
TAX ; X = first data byte (command code)
9047 INY ; Advance to next data byte
9048 LDA (osword_pb_ptr),y ; Load station address high byte
904A INY ; Advance past station addr
904B STY ws_ptr_lo ; Save Y as data index
904D LDY #&72 ; Store station addr hi at (net_rx_ptr)+&72
904F STA (net_rx_ptr),y ; Store to workspace
9051 DEY ; Y=&71
9052 TXA ; A = command code (from X)
9053 STA (net_rx_ptr),y ; Store station addr lo at (net_rx_ptr)+&71
9055 PLP ; Restore flags from earlier PHP
9056 BNE dofs2 ; First call: adjust data length
9058 .send_data_bytes←1← 9071 BNE
LDY ws_ptr_lo ; Reload data index
905A INC ws_ptr_lo ; Advance data index for next iteration
905C LDA (osword_pb_ptr),y ; Load next data byte
905E BEQ return_8 ; Zero byte: end of data, return
9060 LDY #&7d ; Y=&7D: store byte for TX at offset &7D
9062 STA (net_rx_ptr),y ; Store data byte at (net_rx_ptr)+&7D for TX
9064 PHA ; Save data byte for &0D check after TX
9065 JSR ctrl_block_setup_alt ; Set up TX control block
9068 JSR enable_irq_and_tx ; Enable IRQs and transmit
906B .delay_between_tx←1← 906C BNE
DEX ; Short delay loop between TX packets
906C BNE delay_between_tx ; Spin until X reaches 0
906E PLA ; Restore data byte for terminator check
906F EOR #&0d ; Test for end-of-data marker (&0D)
9071 BNE send_data_bytes ; Not &0D: continue with next byte
9073 .return_8←1← 905E BEQ
RTS ; Return (data complete)
9074 .dofs2←1← 9056 BNE
JSR ctrl_block_setup_alt ; First-packet: set up control block
9077 LDY #&7b ; Y=&7B: data length offset
9079 LDA (net_rx_ptr),y ; Load current data length
907B ADC #3 ; Adjust data length by 3 for header bytes
907D 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.

907F .enable_irq_and_tx←1← 9068 JSR
CLI ; Enable interrupts
9080 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 &9099. 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
9083 .osword_dispatch
PHP ; Save processor status
9084 PHA ; Save A (reason code)
9085 TXA ; Save X
9086 PHA ; Push X to stack
9087 TYA ; Save Y
9088 PHA ; Push Y to stack
9089 TSX ; Get stack pointer for indexed access
908A LDA stk_frame_3,x ; Retrieve original A (reason code) from stack
908D CMP #9 ; Reason codes 0-8 only
908F BCS entry1 ; Code >= 9: skip dispatch, restore regs
9091 TAX ; X = reason code for table lookup
9092 JSR osword_trampoline ; Dispatch to handler via trampoline
9095 .entry1←1← 908F BCS
PLA ; Restore Y
9096 TAY ; Transfer to Y register
9097 PLA ; Restore X
9098 TAX ; Transfer to X register
9099 PLA ; Restore A
909A PLP ; Restore processor status flags
909B RTS ; Return with all registers preserved
909C .osword_trampoline←1← 9092 JSR
LDA osword_tbl_hi,x ; PHA/PHA/RTS trampoline: push handler addr-1, RTS jumps to it
909F PHA ; Push high byte of handler address
90A0 LDA osword_tbl_lo,x ; Load handler low byte from table
90A3 PHA ; Push low byte of handler address
90A4 LDA osbyte_a_copy ; Load workspace byte &EF for handler
90A6 RTS ; RTS dispatches to pushed handler
90A7 .osword_tbl_lo←1← 90A0 LDA
EQUB <(return_1-1) ; lo(return_1-1): fn 0 (null handler)
90A8 EQUB <(remote_print_handler-1) ; lo(remote_print_handler- 1): fn 1
90A9 EQUB <(remote_print_handler-1) ; lo(remote_print_handler- 1): fn 2
90AA EQUB <(remote_print_handler-1) ; lo(remote_print_handler- 1): fn 3
90AB EQUB <(net_write_char_handler-1) ; lo(net_write_char_ handler-1): fn 4
90AC EQUB <(printer_select_handler-1) ; lo(printer_select_ handler-1): fn 5
90AD EQUB <(return_1-1) ; lo(return_1-1): fn 6 (null handler)
90AE EQUB <(remote_cmd_dispatch-1) ; lo(remote_cmd_dispatch- 1): fn 7
90AF EQUB <(remote_osword_handler-1) ; lo(remote_osword_ handler-1): fn 8
90B0 .osword_tbl_hi←1← 909C LDA
EQUB >(return_1-1) ; hi(return_1-1): fn 0 (null handler)
90B1 EQUB >(remote_print_handler-1) ; hi(remote_print_handler- 1): fn 1
90B2 EQUB >(remote_print_handler-1) ; hi(remote_print_handler- 1): fn 2
90B3 EQUB >(remote_print_handler-1) ; hi(remote_print_handler- 1): fn 3
90B4 EQUB >(net_write_char_handler-1) ; hi(net_write_char_ handler-1): fn 4
90B5 EQUB >(printer_select_handler-1) ; hi(printer_select_ handler-1): fn 5
90B6 EQUB >(return_1-1) ; hi(return_1-1): fn 6 (null handler)
90B7 EQUB >(remote_cmd_dispatch-1) ; hi(remote_cmd_dispatch- 1): fn 7
90B8 EQUB >(remote_osword_handler-1) ; hi(remote_osword_ handler-1): fn 8

NETVEC fn 4: handle net write character (NWRCH)

Zeros the carry flag in the stacked processor status to signal success, stores the character from Y into workspace offset &DA, loads A=0 as the command type, and falls through to setup_tx_and_send.

On EntryYcharacter to write
90B9 .net_write_char_handler
TSX ; Get stack pointer for P register access
90BA ROR stk_frame_p,x ; ROR/ASL on stacked P: zeros carry to signal success
90BD ASL stk_frame_p,x ; ASL: restore P after ROR zeroed carry
90C0 TYA ; Y = character to write
90C1 LDY #&da ; Store character at workspace offset &DA
90C3 STA (nfs_workspace),y ; Store char at workspace offset &DA
90C5 LDA #0 ; A=0: command type for net write char
fall through ↓

Set up TX control block and send

Stores A at workspace offset &D9 (command type), then sets byte &0C to &80 (TX active flag). Saves the current net_tx_ptr, temporarily redirects it to (nfs_workspace)+&0C so tx_poll_ff transmits from the workspace TX control block. After transmission completes, writes &3F (TX deleted) at (net_tx_ptr)+&00 to mark the control block as free, then restores net_tx_ptr to its original value.

On EntryAcommand type byte
90C7 .setup_tx_and_send←3← 81BD JSR← 911A JSR← 917D JSR
LDY #&d9 ; Y=&D9: command type offset
90C9 STA (nfs_workspace),y ; Store command type at ws+&D9
90CB LDA #&80 ; Mark TX control block as active (&80)
90CD LDY #&0c ; Y=&0C: TXCB start offset
90CF STA (nfs_workspace),y ; Set TX active flag at ws+&0C
90D1 LDA net_tx_ptr ; Save net_tx_ptr; redirect to workspace TXCB
90D3 PHA ; Save net_tx_ptr low
90D4 LDA net_tx_ptr_hi ; Load net_tx_ptr high
90D6 PHA ; Save net_tx_ptr high
90D7 STY net_tx_ptr ; Redirect net_tx_ptr low to workspace
90D9 LDX nfs_workspace_hi ; Load workspace page high byte
90DB STX net_tx_ptr_hi ; Complete ptr redirect
90DD JSR tx_poll_ff ; Transmit with full retry
90E0 LDA #&3f ; Mark TXCB as deleted (&3F) after transmit
90E2 STA (net_tx_ptr,x) ; Write &3F to TXCB byte 0
90E4 PLA ; Restore net_tx_ptr high
90E5 STA net_tx_ptr_hi ; Write back
90E7 PLA ; Restore net_tx_ptr low
90E8 STA net_tx_ptr ; Write back
90EA 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.

90EB .remote_cmd_dispatch
LDY osword_pb_ptr_hi ; Load original Y (OSBYTE secondary param)
90ED CMP #&81 ; OSBYTE &81 (INKEY): always forward to terminal
90EF BEQ dispatch_remote_osbyte ; Forward &81 to terminal for keyboard read
90F1 LDY #1 ; Y=1: search NCTBPL table (execute on both)
90F3 LDX #9 ; X=9: 10-entry NCTBPL table size
90F5 JSR match_osbyte_code ; Search for OSBYTE code in NCTBPL table
90F8 BEQ dispatch_remote_osbyte ; Match found: dispatch with Y=1 (both)
90FA DEY ; Y=-1: search NCTBMI table (terminal only)
90FB DEY ; Second DEY: Y=&FF (from 1 via 0)
90FC LDX #&0e ; X=&0E: 15-entry NCTBMI table size
90FE JSR match_osbyte_code ; Search for OSBYTE code in NCTBMI table
9101 BEQ dispatch_remote_osbyte ; Match found: dispatch with Y=&FF (terminal)
9103 INY ; Y=0: OSBYTE not recognised, ignore
9104 .dispatch_remote_osbyte←3← 90EF BEQ← 90F8 BEQ← 9101 BEQ
LDX #2 ; X=2 bytes to copy (default for RBYTE)
9106 TYA ; A=Y: check table match result
9107 BEQ return_nbyte ; Y=0: not recognised, return unhandled
9109 PHP ; Y>0 (NCTBPL): send only, no result expected
910A BPL nbyte6 ; Y>0 (NCTBPL): no result expected, skip RX
910C INX ; Y<0 (NCTBMI): X=3 bytes (result + P flags) X=&03
910D .nbyte6←1← 910A BPL
LDY #&dc ; Y=&DC: top of 3-byte stack frame region
910F .nbyte1←1← 9117 BPL
LDA tube_claimed_id,y ; Copy OSBYTE args from stack frame to workspace
9112 STA (nfs_workspace),y ; Store to NFS workspace for transmission
9114 DEY ; Next byte (descending)
9115 CPY #&da ; Copied all 3 bytes? (&DC, &DB, &DA)
9117 BPL nbyte1 ; Loop for remaining bytes
9119 TXA ; A = byte count for setup_tx_and_send
911A JSR setup_tx_and_send ; Build TXCB and transmit to terminal
911D PLP ; Restore N flag from table match type
911E BPL return_nbyte ; Y was positive (NCTBPL): done, no result
9120 LDA #&7f ; Set up RX control block to wait for reply
9122 LDY #&0c ; Y=&0C: RX control block offset in workspace
9124 STA (nfs_workspace),y ; Write &7F (waiting) to RXCB flag byte
9126 .poll_rxcb_flag←1← 9128 BPL
LDA (nfs_workspace),y ; Poll for TX completion (wait for bit 7 set)
9128 BPL poll_rxcb_flag ; Bit7 clear: still waiting, poll again
912A TSX ; X = stack pointer for register restoration
912B LDY #&dd ; Y=&DD: saved P byte offset in workspace
912D LDA (nfs_workspace),y ; Load remote processor status from reply
912F ORA #&44 ; Force V=1 (claimed) and I=1 (no IRQ) in saved P
9131 BNE nbyte5 ; ALWAYS branch (ORA #&44 never zero) ALWAYS branch
9133 .nbyte4←1← 913C BNE
DEY ; Previous workspace offset
9134 DEX ; Previous stack register slot
9135 LDA (nfs_workspace),y ; Load next result byte (X, then Y)
9137 .nbyte5←1← 9131 BNE
STA stk_frame_p,x ; Write result bytes to stacked registers
913A CPY #&da ; Copied all result bytes? (P at &DA)
913C BNE nbyte4 ; Loop for remaining result bytes
913E .return_nbyte←2← 9107 BEQ← 911E BPL
RTS ; Return to OSBYTE dispatcher

Search remote OSBYTE table for match (NCALLP)

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

  X=9  → first 10 entries (NCTBPL: execute on both machines)
  X=14 → all 15 entries (NCTBMI: execute on terminal only)

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

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

913F .match_osbyte_code←3← 90F5 JSR← 90FE JSR← 9145 BPL
CMP remote_osbyte_table,x ; Compare OSBYTE code with table entry
9142 BEQ return_match_osbyte ; Match found: return with Z=1
9144 DEX ; Next table entry (descending)
9145 BPL match_osbyte_code ; Loop for remaining entries
9147 .return_match_osbyte←2← 9142 BEQ← 915F BNE
RTS ; Return; Z=1 if match, Z=0 if not
9148 .remote_osbyte_table←1← 913F CMP
EQUB &04 ; OSBYTE &04: cursor key status
9149 EQUB &09 ; OSBYTE &09: flash duration (1st colour)
914A EQUB &0A ; OSBYTE &0A: flash duration (2nd colour)
914B EQUB &15 ; OSBYTE &15: flush specific buffer
914C EQUB &9A ; OSBYTE &9A: video ULA control register
914D EQUB &9B ; OSBYTE &9B: video ULA palette
914E EQUB &E1 ; OSBYTE &E1: function key &C0-&CF
914F EQUB &E2 ; OSBYTE &E2: function key &D0-&DF
9150 EQUB &E3 ; OSBYTE &E3: function key &E0-&EF
9151 EQUB &E4 ; OSBYTE &E4: function key &F0-&FF
9152 EQUB &0B ; OSBYTE &0B: auto-repeat delay
9153 EQUB &0C ; OSBYTE &0C: auto-repeat rate
9154 EQUB &0F ; OSBYTE &0F: flush buffer class
9155 EQUB &79 ; OSBYTE &79: keyboard scan from X
9156 EQUB &7A ; OSBYTE &7A: keyboard scan from 16

NETVEC fn 8: remote OSWORD dispatch (NWORD)

Only accepts OSWORD 7 (make a sound) and OSWORD 8 (define an envelope), rejecting all others. Sets Y=14 as the maximum parameter byte count, then falls through to remote_cmd_data.

9157 .remote_osword_handler
LDY #&0e ; Y=&0E: max 14 parameter bytes for OSWORD
9159 CMP #7 ; OSWORD 7 = make a sound
915B BEQ copy_params_rword ; OSWORD 7 (sound): handle via common path
915D 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.

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

Alternate entry into control block setup

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

9182 .ctrl_block_setup_alt←2← 9065 JSR← 9074 JSR
LDX #&0d ; X=&0D: template offset for alt entry
9184 LDY #&7c ; Y=&7C: target workspace offset for alt entry
9186 BIT tx_ctrl_upper ; BIT test: V flag = bit 6 of &83B3
9189 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 &91B4 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

918B .ctrl_block_setup←1← 84BF JSR
LDY #&17 ; Y=&17: workspace target offset (main entry)
918D LDX #&1a ; X=&1A: template table index (main entry)
918F .ctrl_block_setup_clv←1← 924E JSR
CLV ; V=0: target is (nfs_workspace)
9190 .cbset2←2← 9189 BVS← 91B1 BPL
LDA ctrl_block_template,x ; Load template byte from ctrl_block_template[X]
9193 CMP #&fe ; &FE = stop sentinel
9195 BEQ cb_template_tail ; End of template: jump to exit
9197 CMP #&fd ; &FD = skip sentinel
9199 BEQ cb_template_main_start ; Skip: don't store, just decrement Y
919B CMP #&fc ; &FC = page byte sentinel
919D BNE cbset3 ; Not sentinel: store template value directly
919F LDA net_rx_ptr_hi ; V=1: use (net_rx_ptr) page
91A1 BVS rxcb_matched ; V=1: skip to net_rx_ptr page
91A3 LDA nfs_workspace_hi ; V=0: use (nfs_workspace) page
91A5 .rxcb_matched←1← 91A1 BVS
STA net_tx_ptr_hi ; PAGE byte → Y=&02 / Y=&74
91A7 .cbset3←1← 919D BNE
BVS cbset4 ; → Y=&04 / Y=&76
91A9 STA (nfs_workspace),y ; PAGE byte → Y=&06 / Y=&78
91AB BVC cb_template_main_start ; → Y=&08 / Y=&7A ALWAYS branch
91AD .cbset4←1← 91A7 BVS
STA (net_rx_ptr),y ; Alt-path only → Y=&70
91AF .cb_template_main_start←2← 9199 BEQ← 91AB BVC
DEY ; → Y=&0C (main only)
91B0 DEX ; → Y=&0D (main only)
91B1 BPL cbset2 ; Loop until all template bytes done
91B3 .cb_template_tail←1← 9195 BEQ
INY ; → Y=&10 (main only)
91B4 STY net_tx_ptr ; Store final offset as net_tx_ptr
91B6 RTS ; → Y=&07 / Y=&79

Control block initialisation template

Read by the loop at &918D, 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 &83B3

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)

91B7 .ctrl_block_template←1← 9190 LDA
STA zp_ptr_lo ; Alt-path only → Y=&6F
91B9 SBC l7dfd,x
91BC EQUB &FC, &FF, &FF, &7E, ; → Y=&0D (main only) → Y=&03 / &FC, &FF, &FF, &00, ; Y=&75 SKIP (main only) → Y=&10 &00, &FE, &80, &93, ; (main only) → Y=&08 / Y=&7A → &FD, &FD, &D9, &FC, ; Y=&09 / Y=&7B PAGE byte → Y=&15 &FF, &FF, &DE, &FC, ; (main only) → Y=&16 (main only) &FF, &FF, &FE, &D1, ; SKIP (main only) PAGE byte → &FD, &FD, &1F, &FD, ; Y=&11 (main only) → Y=&12 (main &FF, &FF, &FD, &FD, ; only) → Y=&13 (main only) → &FF, &FF ; Y=&14 (main only) → Y=&17 (main ; only)
fall through ↓

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
91DE .printer_select_handler
DEX ; X-1: convert 1-based buffer to 0-based
91DF CPX osword_pb_ptr ; Is this the network printer buffer?
91E1 BNE setup1 ; No: skip printer init
91E3 LDA #&1f ; &1F = initial buffer pointer offset
91E5 STA printer_buf_ptr ; Reset printer buffer write position
91E8 LDA #&41 ; &41 = initial PFLAGS (bit 6 set, bit 0 set)
91EA .setup1←1← 91E1 BNE
STA prot_flags ; Store A to printer status byte
91EC .return_printer_select←2← 91EF BNE← 9203 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)
91ED .remote_print_handler
CPY #4 ; Only handle buffer 4 (network printer)
91EF BNE return_printer_select ; Not buffer 4: ignore
91F1 TXA ; A = reason code
91F2 DEX ; Reason 1? (DEX: 1->0)
91F3 BNE toggle_print_flag ; Not reason 1: handle Ctrl-B/C
91F5 TSX ; Get stack pointer for P register
91F6 ORA stk_frame_p,x ; Force I flag in stacked P to block IRQs
91F9 STA stk_frame_p,x ; Write back modified P register
91FC .prlp1←2← 920B BCC← 9210 BCC
LDA #osbyte_read_buffer ; OSBYTE &91: extract char from MOS buffer
91FE LDX #buffer_printer ; X=3: printer buffer number
9200 JSR osbyte ; Get character from input buffer (C is set if the buffer is empty, otherwise Y=extracted character)
9203 BCS return_printer_select ; Buffer empty: return
9205 TYA ; Y = extracted character Y is the character extracted from the buffer
9206 JSR store_output_byte ; Store char in output buffer
9209 CPY #&6e ; Buffer nearly full? (&6E = threshold)
920B BCC prlp1 ; Not full: get next char
920D JSR flush_output_block ; Buffer full: flush to network
9210 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
9212 .store_output_byte←2← 9206 JSR← 921F JSR
LDY printer_buf_ptr ; Load current buffer offset
9215 STA (net_rx_ptr),y ; Store byte at current position
9217 INC printer_buf_ptr ; Advance buffer pointer
921A RTS ; Return; Y = buffer offset
921B .toggle_print_flag←1← 91F3 BNE
PHA ; Save reason code
921C TXA ; A = reason code
921D EOR #1 ; EOR #1: toggle print-active flag (bit 0)
921F JSR store_output_byte ; Store toggled flag as output byte
9222 EOR prot_flags ; XOR with PFLAGS
9224 ROR ; Test if sequence changed (bit 7 mismatch)
9225 BCC skip_flush ; Sequence unchanged: skip flush
9227 ROL ; Undo ROR
9228 STA prot_flags ; Update PFLAGS
922A JSR flush_output_block ; Flush current output block
922D .skip_flush←1← 9225 BCC
LDA prot_flags ; Load PFLAGS
922F AND #&f0 ; Extract upper nibble of PFLAGS
9231 ROR ; Shift for bit extraction
9232 TAX ; Save in X
9233 PLA ; Restore original reason code
9234 ROR ; Merge print-active bit from original A
9235 TXA ; Retrieve shifted PFLAGS
9236 ROL ; Recombine into new PFLAGS value
9237 STA prot_flags ; Update PFLAGS
9239 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.

923A .flush_output_block←2← 920D JSR← 922A JSR
LDY #8 ; Store buffer length at workspace offset &08
923C LDA printer_buf_ptr ; Current buffer fill position
923F STA (nfs_workspace),y ; Write to workspace offset &08
9241 LDA net_rx_ptr_hi ; Store page high byte at offset &09
9243 INY ; Y=&09
9244 STA (nfs_workspace),y ; Write page high byte at offset &09
9246 LDY #5 ; Also store at offset &05
9248 STA (nfs_workspace),y ; (end address high byte)
924A LDY #&0b ; Y=&0B: flag byte offset
924C LDX #&26 ; X=&26: start from template entry &26
924E JSR ctrl_block_setup_clv ; Reuse ctrl_block_setup with CLV entry
9251 DEY ; Y=&0A: sequence flag byte offset
9252 LDA prot_flags ; Load protocol flags (PFLAGS)
9254 PHA ; Save current PFLAGS
9255 ROL ; Carry = current sequence (bit 7)
9256 PLA ; Restore original PFLAGS
9257 EOR #&80 ; Toggle sequence number (bit 7 of PFLAGS)
9259 STA prot_flags ; Save toggled PFLAGS
925B ROL ; Old sequence bit into bit 0
925C STA (nfs_workspace),y ; Store sequence flag at offset &0A
925E LDY #&1f ; Y=&1F: buffer start offset
9260 STY printer_buf_ptr ; Reset printer buffer to start (&1F)
9263 LDA #0 ; A=0: printer output flag
9265 TAX ; X=0: workspace low byte X=&00
9266 LDY nfs_workspace_hi ; Y = workspace page high byte
9268 CLI ; Enable interrupts before TX
fall through ↓

Byte-stream transmit (BSXMIT/BSPSX)

Transmits a data packet over econet with sequence number tracking. Sets up the TX control block pointer from X/Y, computes the sequence bit from A AND fs_sequence_nos (handle-based tracking), merges it into the flag byte at (net_tx_ptr)+0, then initiates transmit via tx_poll_ff. Sets end addresses (offsets 8/9) to &FF to allow unlimited data. Selects port byte &D1 (print) or &90 (FS) based on the original A value. Polls the TX result in a loop via BRIANX (c8530), retrying while the result bit differs from the expected sequence. On success, toggles the sequence tracking bit in fs_sequence_nos.

On EntryAhandle bitmask (0=printer, non-zero=file)
XTX control block address low
YTX control block address high
9269 .econet_tx_retry←2← 83FA JSR← 8436 JSR
STX net_tx_ptr ; Set TX control block ptr low byte
926B STY net_tx_ptr_hi ; Set TX control block ptr high byte
926D PHA ; Save A (handle bitmask) for later
926E AND fs_sequence_nos ; Compute sequence bit from handle
9271 BEQ bsxl1 ; Zero: no sequence bit set
9273 LDA #1 ; Non-zero: normalise to bit 0
9275 .bsxl1←1← 9271 BEQ
LDY #0 ; Y=0: flag byte offset in TXCB
9277 ORA (net_tx_ptr),y ; Merge sequence into existing flag byte
9279 PHA ; Save merged flag byte
927A STA (net_tx_ptr),y ; Write flag+sequence to TXCB byte 0
927C JSR tx_poll_ff ; Transmit with full retry
927F LDA #&ff ; End address &FFFF = unlimited data length
9281 LDY #8 ; Y=8: end address low offset in TXCB
9283 STA (net_tx_ptr),y ; Store &FF to end addr low
9285 INY ; Y=&09
9286 STA (net_tx_ptr),y ; Store &FF to end addr high (Y=9)
9288 PLA ; Recover merged flag byte
9289 TAX ; Save in X for sequence compare
928A LDY #&d1 ; Y=&D1: printer port number
928C PLA ; Recover saved handle bitmask
928D PHA ; Re-save for later consumption
928E BEQ bspsx ; A=0: port &D1 (print); A!=0: port &90 (FS)
9290 LDY #&90 ; Y=&90: FS data port
9292 .bspsx←1← 928E BEQ
TYA ; A = selected port number
9293 LDY #1 ; Y=1: port byte offset in TXCB
9295 STA (net_tx_ptr),y ; Write port to TXCB byte 1
9297 TXA ; A = saved flag byte (expected sequence)
9298 DEY ; Y=&00
9299 PHA ; Push expected sequence for retry loop
929A .bsxl0←1← 92A6 BCS
LDA #&7f ; Flag byte &7F = waiting for reply
929C STA (net_tx_ptr),y ; Write to TXCB flag byte (Y=0)
929E JSR waitfs ; Transmit and wait for reply (BRIANX)
92A1 PLA ; Recover expected sequence
92A2 PHA ; Keep on stack for next iteration
92A3 EOR (net_tx_ptr),y ; Check if TX result matches expected sequence
92A5 ROR ; Bit 0 to carry (sequence mismatch?)
92A6 BCS bsxl0 ; C=1: mismatch, retry transmit
92A8 PLA ; Clean up: discard expected sequence
92A9 PLA ; Discard saved handle bitmask
92AA EOR fs_sequence_nos ; Toggle sequence bit on success
92AD .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 &931E. 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.

92AE .lang_2_save_palette_vdu
LDA table_idx ; Save current table index
92B0 PHA ; Push for later restore
92B1 LDA #&e9 ; Point workspace to palette save area (&E9)
92B3 STA nfs_workspace ; Set workspace low byte
92B5 LDY #0 ; Y=0: first palette entry
92B7 STY table_idx ; Clear table index counter
92B9 LDA vdu_screen_mode ; Save current screen MODE to workspace
92BC STA (nfs_workspace),y ; Store MODE at workspace[0]
92BE INC nfs_workspace ; Advance workspace pointer past MODE byte
92C0 LDA vdu_colours ; Read colour count (from &0351)
92C3 PHA ; Push for iteration count tracking
92C4 TYA ; A=0: logical colour number for OSWORD A=&00
92C5 .save_palette_entry←1← 92E4 BNE
STA (nfs_workspace),y ; Store logical colour at workspace[0]
92C7 LDX nfs_workspace ; X = workspace ptr low (param block addr)
92C9 LDY nfs_workspace_hi ; Y = workspace ptr high
92CB LDA #osword_read_palette ; OSWORD &0B: read palette for logical colour
92CD JSR osword ; Read palette
92D0 PLA ; Recover colour count
92D1 LDY #0 ; Y=0: access workspace[0]
92D3 STA (nfs_workspace),y ; Write colour count back to workspace[0]
92D5 INY ; Y=1: access workspace[1] (palette result) Y=&01
92D6 LDA (nfs_workspace),y ; Read palette value returned by OSWORD
92D8 PHA ; Push palette value for next iteration
92D9 LDX nfs_workspace ; X = current workspace ptr low
92DB INC nfs_workspace ; Advance workspace pointer
92DD INC table_idx ; Increment table index
92DF DEY ; Y=0 for next store Y=&00
92E0 LDA table_idx ; Load table index as logical colour
92E2 CPX #&f9 ; Loop until workspace wraps past &F9
92E4 BNE save_palette_entry ; Continue for all 16 palette entries
92E6 PLA ; Discard last palette value from stack
92E7 STY table_idx ; Reset table index to 0
92E9 INC nfs_workspace ; Advance workspace past palette data
92EB JSR save_vdu_state ; Save cursor pos and OSBYTE state values
92EE INC nfs_workspace ; Advance workspace past VDU state data
92F0 PLA ; Recover saved table index
92F1 STA table_idx ; Restore table index
92F3 .clear_jsr_protection←4← 84A3 JMP← 84CB JSR← 84F2 JSR← 8EF1 JMP
LDA saved_jsr_mask ; Restore LSTAT from saved OLDJSR value
92F6 STA prot_status ; Write to protection status
92F9 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. The JSR to read_vdu_osbyte_x0 is a self-calling trick: it executes read_vdu_osbyte twice (once for &C2, once for &C3) because the RTS returns to the instruction at read_vdu_osbyte_x0 itself.

ROMExec
92FA .save_vdu_state←1← 92EB JSR
LDA vdu_cursor_edit ; Read cursor editing state
92FD STA (nfs_workspace),y ; Store to workspace[Y]
92FF TAX ; Preserve in X for OSBYTE
9300 JSR read_vdu_osbyte ; OSBYTE &85: read cursor position
9303 INC nfs_workspace ; Advance workspace pointer
9305 TYA ; Y result from OSBYTE &85
9306 STA (nfs_workspace,x) ; Store Y pos to workspace (X=0)
9308 JSR read_vdu_osbyte_x0 ; Self-call trick: executes twice
930B .read_vdu_osbyte_x0←1← 9308 JSR
LDX #0 ; X=0 for (zp,X) addressing
930D .read_vdu_osbyte←1← 9300 JSR
LDY table_idx ; Index into OSBYTE number table
930F INC table_idx ; Next table entry next time
9311 INC nfs_workspace ; Advance workspace pointer
9313 LDA vdu_osbyte_table,y ; Read OSBYTE number from table
9316 LDY #&ff ; Y=&FF: read current value
9318 JSR osbyte ; Call OSBYTE
931B TXA ; Result in X to A
931C LDX #0 ; X=0 for indexed indirect store
931E STA (nfs_workspace,x) ; Store result to workspace
9320 RTS ; Return after storing result
9321 .vdu_osbyte_table←1← 9313 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 &51
; (self-modifying the JMP indirect low byte) before ; dispatch.
9324 0016 .nmi_workspace_start←1← 816F STA
.tube_brk_handler←1← 816F STA
LDA #&ff ; A=&FF: signal error to co-processor via R4
9326 0018 JSR tube_send_r4 ; Send &FF error signal to Tube R4
9329 001B LDA tube_data_register_2 ; Flush any pending R2 byte
932C 001E LDA #0 ; A=0: send zero prefix to R2
932E 0020 .tube_send_zero_r2
JSR tube_send_r2 ; Send zero prefix byte via R2
9331 0023 TAY ; Y=0: start of error block at (&FD)
9332 0024 LDA (brk_ptr),y ; Load error number from (&FD),0
9334 0026 JSR tube_send_r2 ; Send error number via R2
9337 0029 .tube_brk_send_loop←1← 0030 BNE
INY ; Advance to next error string byte
9338 002A .tube_send_error_byte
LDA (brk_ptr),y ; Load next error string byte
933A 002C JSR tube_send_r2 ; Send error string byte via R2
933D 002F TAX ; Zero byte = end of error string
933E 0030 BNE tube_brk_send_loop ; Loop until zero terminator sent
9340 0032 .tube_reset_stack←1← 0477 JMP
LDX #&ff ; Reset stack pointer to top
9342 0034 TXS ; TXS: set stack pointer from X
9343 0035 CLI ; Enable interrupts for main loop
9344 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
9347 0039 BPL tube_poll_r2 ; R1 not ready: check R2 instead
9349 003B .tube_handle_wrch←1← 0049 BMI
LDA tube_data_register_1 ; Read character from Tube R1 data
934C 003E JSR oswrch ; Write character
934F 0041 .tube_poll_r2←1← 0039 BPL
BIT tube_status_register_2 ; BIT R2 status: check command byte
9352 0044 BPL tube_main_loop ; R2 not ready: loop back to R1 check
9354 0046 BIT tube_status_1_and_tube_control ; Re-check R1: WRCH has priority over R2
9357 0049 BMI tube_handle_wrch ; R1 ready: handle WRCH first
9359 004B LDX tube_data_register_2 ; Read command byte from Tube R2 data
935C 004E STX tube_cmd_lo ; Self-modify JMP low byte for dispatch
935E 0050 .tube_dispatch_cmd
JMP (tube_dispatch_table) ; Dispatch to handler via indirect JMP
9361 0053 .tube_transfer_addr←2← 04DE STY← 04EE STA
EQUB &00 ; Tube transfer address low byte
9362 0054 .tube_xfer_page←3← 04B6 INC← 04D4 STA← 04F3 STA
EQUB &80 ; Tube transfer page (default &80)
9363 0055 .tube_xfer_addr_2←2← 04BA INC← 04FD STY
EQUB &00 ; Tube transfer address byte 2
9364 0056 .tube_xfer_addr_3←2← 04BE INC← 04FB STA
EQUB &00 ; Tube transfer address byte 3
; Tube host code page 4 — reference: NFS12 (BEGIN, ADRR, ; SENDW)
; Copied from ROM at reloc_p4_src during init. The first ; 28 bytes (&0400-&041B)
; overlap with the end of the ZP block (the same ROM bytes ; serve both
; the ZP copy at &005B-&0076 and this page at ; &0400-&041B). Contains:
; &0400: JMP &0484 (BEGIN — startup/CLI entry, break type check)
; &0403: JMP &06E2 (tube_escape_check)
; &0406: tube_addr_claim — Tube address claim (ADRR protocol)
; &0414: tube_release_claim — release address claim via R4 cmd 5
; &0421: tube_post_init — reset claimed-address state to &80
; &0435: data transfer setup (SENDW protocol) — &0435-&0483
; &0484: BEGIN — startup entry, sends ROM contents to Tube
; &04CB: tube_claim_default — claim default transfer address
; &04D2: tube_init_reloc — extract relocation address from ROM
9365 0400 .tube_code_page4←1← 8155 STA
JMP tube_begin ; JMP to BEGIN startup entry
9368 0403 .tube_escape_entry
JMP tube_escape_check ; JMP to tube_escape_check (&06A7)
936B 0406 .tube_addr_claim←10← 049A JSR← 04CF JMP← 8B8F JSR← 8BA6 JSR← 8C03 JSR← 8E29 JMP← 999D JSR← 9A53 JSR← 9F3A JSR← 9F42 JSR
CMP #&80 ; A>=&80: address claim; A<&80: data transfer
936D 0408 BCC tube_transfer_setup ; A<&80: data transfer setup (SENDW)
936F 040A CMP #&c0 ; A>=&C0: new address claim from another host
9371 040C BCS addr_claim_external ; C=1: external claim, check ownership
9373 040E ORA #&40 ; Map &80-&BF range to &C0-&FF for comparison
9375 0410 CMP tube_claimed_id ; Is this for our currently-claimed address?
9377 0412 BNE return_tube_init ; Not our address: return
; Release Tube address claim via R4 command 5
; Saves interrupt state (PHP/SEI), sends R4 command 5 ; (release)
; followed by the currently-claimed address, then restores
; interrupts. Falls through to tube_post_init to reset the
; claimed-address state to &80.
9379 0414 .tube_release_claim←1← 0471 JSR
PHP ; PHP: save interrupt state for release
937A 0415 SEI ; SEI: disable interrupts during R4 protocol
937B 0416 LDA #5 ; R4 cmd 5: release our address claim
937D 0418 JSR tube_send_r4 ; Send release command to co-processor
9380 041B LDA tube_claimed_id ; Load our currently-claimed address
9382 041D JSR tube_send_r4 ; Send our address as release parameter
9385 0420 PLP ; Restore interrupt state
9386 0421 .tube_post_init←1← 8167 JSR
LDA #&80 ; &80 sentinel: clear address claim
9388 0423 STA tube_claimed_id ; &80 sentinel = no address currently claimed
938A 0425 STA tube_claim_flag ; Store to claim-in-progress flag
938C 0427 RTS ; Return from tube_post_init
938D 0428 .addr_claim_external←1← 040C BCS
ASL tube_claim_flag ; Another host claiming; check if we're owner
938F 042A BCS accept_new_claim ; C=1: we have an active claim
9391 042C CMP tube_claimed_id ; Compare with our claimed address
9393 042E BEQ return_tube_init ; Match: return (we already have it)
9395 0430 CLC ; Not ours: CLC = we don't own this address
9396 0431 RTS ; Return with C=0 (claim denied)
9397 0432 .accept_new_claim←1← 042A BCS
STA tube_claimed_id ; Accept new claim: update our address
9399 0434 .return_tube_init←2← 0412 BNE← 042E BEQ
RTS ; Return with address updated
939A 0435 .tube_transfer_setup←1← 0408 BCC
PHP ; PHP: save interrupt state
939B 0436 SEI ; SEI: disable interrupts for R4 protocol
939C 0437 .setup_data_transfer
STY tube_data_ptr_hi ; Save 16-bit transfer address from (X,Y)
939E 0439 STX tube_data_ptr ; Store address pointer low byte
93A0 043B JSR tube_send_r4 ; Send transfer type byte to co-processor
93A3 043E TAX ; X = transfer type for table lookup
93A4 043F LDY #3 ; Y=3: send 4 bytes (address + claimed addr)
93A6 0441 LDA tube_claimed_id ; Send our claimed address + 4-byte xfer addr
93A8 0443 JSR tube_send_r4 ; Send transfer address byte
93AB 0446 .send_xfer_addr_bytes←1← 044C BPL
LDA (tube_data_ptr),y ; Load transfer address byte from (X,Y)
93AD 0448 JSR tube_send_r4 ; Send address byte to co-processor via R4
93B0 044B DEY ; Previous byte (big-endian: 3,2,1,0)
93B1 044C BPL send_xfer_addr_bytes ; Loop for all 4 address bytes
93B3 044E LDY #&18 ; Y=&18: enable Tube control register
93B5 0450 STY tube_status_1_and_tube_control ; Enable Tube interrupt generation
93B8 0453 LDA tube_ctrl_values,x ; Look up Tube control bits for this xfer type
93BB 0456 STA tube_status_1_and_tube_control ; Apply transfer- specific control bits
93BE 0459 LSR ; LSR: check bit 2 (2-byte flush needed?)
93BF 045A LSR ; LSR: shift bit 2 to carry
93C0 045B BCC skip_r3_flush ; C=0: no flush needed, skip R3 reads
93C2 045D BIT tube_data_register_3 ; Dummy R3 reads: flush for 2-byte transfers
93C5 0460 BIT tube_data_register_3 ; Second dummy read to flush R3 FIFO
93C8 0463 .skip_r3_flush←1← 045B BCC
JSR tube_send_r4 ; Trigger co-processor ack via R4
93CB 0466 .poll_r4_copro_ack←1← 0469 BVC
BIT tube_status_register_4_and_cpu_control ; Poll R4 status for co- processo r response
93CE 0469 BVC poll_r4_copro_ack ; Bit 6 clear: not ready, keep polling
93D0 046B BCS copro_ack_nmi_check ; R4 bit 7: co-processor acknowledged transfer
93D2 046D CPX #4 ; Type 4 = SENDW (host-to-parasite word xfer)
93D4 046F BNE skip_nmi_release ; Not SENDW type: skip release path
93D6 0471 .tube_sendw_complete←1← 0496 BEQ
JSR tube_release_claim ; SENDW complete: release, sync, restart
93D9 0474 JSR tube_send_r2 ; Sync via R2 send
93DC 0477 JMP tube_reset_stack ; Restart Tube main loop
93DF 047A .copro_ack_nmi_check←1← 046B BCS
LSR ; LSR: check bit 0 (NMI used?)
93E0 047B BCC skip_nmi_release ; C=0: NMI not used, skip NMI release
93E2 047D LDY #&88 ; Release Tube NMI (transfer used interrupts)
93E4 047F STY tube_status_1_and_tube_control ; Write &88 to Tube control to release NMI
93E7 0482 .skip_nmi_release←2← 046F BNE← 047B BCC
PLP ; Restore interrupt state
93E8 0483 .return_tube_xfer
RTS ; Return from transfer setup
; Tube host startup entry (BEGIN)
; Entry point via JMP from &0400. Enables interrupts, ; checks
; break type via OSBYTE &FD: soft break re-initialises ; Tube and
; restarts, hard break claims address &FF. Sends ROM ; contents
; to co-processor page by page via SENDW, then claims the ; final
; transfer address.
93E9 0484 .tube_begin←1← 0400 JMP
CLI ; BEGIN: enable interrupts for Tube host code
93EA 0485 BCS claim_addr_ff ; C=1: hard break, claim addr &FF
93EC 0487 BNE check_break_type ; C=0, A!=0: re-init path
93EE 0489 JMP tube_reply_ack ; Z=1 from C=0 path: just acknowledge
93F1 048C .check_break_type←1← 0487 BNE
LDX #0 ; X=0 for OSBYTE
93F3 048E LDY #&ff ; Y=&FF for OSBYTE
93F5 0490 LDA #osbyte_read_write_last_break_type ; OSBYTE &FD: what type of reset was this?
93F7 0492 JSR osbyte ; Read type of last reset
93FA 0495 TXA ; X=value of type of last reset
93FB 0496 BEQ tube_sendw_complete ; Soft break (X=0): re-init Tube and restart
93FD 0498 .claim_addr_ff←2← 0485 BCS← 049D BCC
LDA #&ff ; Claim address &FF (startup = highest prio)
93FF 049A JSR tube_addr_claim ; Request address claim from Tube system
9402 049D BCC claim_addr_ff ; C=0: claim failed, retry
9404 049F JSR tube_init_reloc ; Init reloc pointers from ROM header
9407 04A2 .next_rom_page←1← 04C4 BVC
LDA #7 ; R4 cmd 7: SENDW to send ROM to parasite
9409 04A4 JSR tube_claim_default ; Set up Tube for SENDW transfer
940C 04A7 LDY #0 ; Y=0: start at beginning of page
940E 04A9 STY zp_ptr_lo ; Store to zero page pointer low byte
9410 04AB .send_rom_page_bytes←1← 04B4 BNE
LDA (zp_ptr_lo),y ; Send 256-byte page via R3, byte at a time
9412 04AD STA tube_data_register_3 ; Write byte to Tube R3 data register
9415 04B0 NOP ; Timing delay: Tube data register needs NOPs
9416 04B1 NOP ; NOP delay (2)
9417 04B2 NOP ; NOP delay (3)
9418 04B3 INY ; Next byte in page
9419 04B4 BNE send_rom_page_bytes ; Loop for all 256 bytes
941B 04B6 INC tube_xfer_page ; Increment 24-bit destination addr
941D 04B8 BNE skip_addr_carry ; No carry: skip higher bytes
941F 04BA INC tube_xfer_addr_2 ; Carry into second byte
9421 04BC BNE skip_addr_carry ; No carry: skip third byte
9423 04BE INC tube_xfer_addr_3 ; Carry into third byte
9425 04C0 .skip_addr_carry←2← 04B8 BNE← 04BC BNE
INC zp_ptr_hi ; Increment page counter
9427 04C2 BIT zp_ptr_hi ; Bit 6 set = all pages transferred
9429 04C4 BVC next_rom_page ; More pages: loop back to SENDW
942B 04C6 JSR tube_init_reloc ; Re-init reloc pointers for final claim
942E 04C9 LDA #4 ; A=4: transfer type for final address claim
; Claim default Tube transfer address
; Sets Y=0, X=&53 (address &0053), then JMP ; tube_addr_claim
; to initiate a Tube address claim for the default ; transfer
; address. Called from the BEGIN startup path and after ; the
; page transfer loop completes.
9430 04CB .tube_claim_default←1← 04A4 JSR
LDY #0 ; Y=0: transfer address low byte
9432 04CD LDX #&53 ; X=&53: transfer address high byte (&0053)
9434 04CF 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.
9437 04D2 .tube_init_reloc←2← 049F JSR← 04C6 JSR
LDA #&80 ; Init: start sending from &8000
9439 04D4 STA tube_xfer_page ; Store &80 as source page high byte
943B 04D6 STA zp_ptr_hi ; Store &80 as page counter initial value
943D 04D8 LDA #&20 ; A=&20: bit 5 mask for ROM type check
943F 04DA AND rom_type ; ROM type bit 5: reloc address in header?
9442 04DD TAY ; Y = 0 or &20 (reloc flag)
9443 04DE STY tube_transfer_addr ; Store as transfer address selector
9445 04E0 BEQ store_xfer_end_addr ; No reloc addr: use defaults
9447 04E2 LDX copyright_offset ; Skip past copyright string to find reloc addr
944A 04E5 .scan_copyright_end←1← 04E9 BNE
INX ; Skip past null-terminated copyright string
944B 04E6 LDA rom_header,x ; Load next byte from ROM header
944E 04E9 BNE scan_copyright_end ; Loop until null terminator found
9450 04EB LDA language_handler_lo,x ; Read 4-byte reloc address from ROM header
9453 04EE STA tube_transfer_addr ; Store reloc addr byte 1 as transfer addr
9455 04F0 LDA language_handler_hi,x ; Load reloc addr byte 2
9458 04F3 STA tube_xfer_page ; Store as source page start
945A 04F5 LDY service_entry,x ; Load reloc addr byte 3
945D 04F8 LDA service_handler_lo,x ; Load reloc addr byte 4 (highest)
9460 04FB .store_xfer_end_addr←1← 04E0 BEQ
STA tube_xfer_addr_3 ; Store high byte of end address
9462 04FD STY tube_xfer_addr_2 ; Store byte 3 of end address
9464 04FF RTS ; Return with pointers initialised
; Tube host code page 5 — reference: NFS13 (TASKS, ; BPUT-FILE)
; Copied from ROM at reloc_p5_src during init. Contains:
; &0500: 12-entry dispatch table (&0500-&0517)
; &0518: 8-byte Tube control register value table
; &0520: tube_osbput — write byte to file
; &052D: tube_osbget — read byte from file
; &0537: tube_osrdch — read character
; &053A: tube_rdch_reply — send carry+data as reply
; &0542: tube_osfind — open/close file
; &055E: tube_osargs — file argument read/write
; &0582: tube_read_string — read string from R2 into &0700
; &0596: tube_oscli — execute * command
; &059C: tube_reply_ack — send &7F acknowledge
; &05A9: tube_osfile — whole file operation
; &05D1: tube_osgbpb — multi-byte file read/write
; Code continues seamlessly into page 6 (tube_osbyte_short ; at &05F2
; straddles the page boundary with a BVC at &05FF/&0600).
9465 0500 .tube_dispatch_table←2← 0050 JMP← 815B STA
EQUW tube_osrdch ; R2 cmd 0: OSRDCH
9467 0502 EQUW tube_oscli ; R2 cmd 1: OSCLI
9469 0504 EQUW tube_osbyte_2param ; R2 cmd 2: OSBYTE (2-param)
946B 0506 EQUW tube_osbyte_long ; R2 cmd 3: OSBYTE (3-param)
946D 0508 EQUW tube_osword ; R2 cmd 4: OSWORD
946F 050A EQUW tube_osword_rdln ; R2 cmd 5: OSWORD 0 (read line)
9471 050C EQUW tube_osargs ; R2 cmd 6: OSARGS
9473 050E EQUW tube_osbget ; R2 cmd 7: OSBGET
9475 0510 EQUW tube_osbput ; R2 cmd 8: OSBPUT
9477 0512 EQUW tube_osfind ; R2 cmd 9: OSFIND
9479 0514 EQUW tube_osfile ; R2 cmd 10: OSFILE
947B 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.
947D 0518 .tube_ctrl_values←1← 0453 LDA
EQUB &86 ; Type 0: set I+J (1-byte R3, parasite to host)
947E 0519 EQUB &88 ; Type 1: set M (1-byte R3, host to parasite)
947F 051A EQUB &96 ; Type 2: set V+I+J (2-byte R3, parasite to host)
9480 051B EQUB &98 ; Type 3: set V+M (2-byte R3, host to parasite)
9481 051C EQUB &18 ; Type 4: clear V+M (execute code at address)
9482 051D EQUB &18 ; Type 5: clear V+M (release address claim)
9483 051E EQUB &82 ; Type 6: set I (define event handler)
9484 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.
9485 0520 .tube_osbput
JSR tube_read_r2 ; Read channel handle from R2 for BPUT
9488 0523 TAY ; Y=channel handle from R2
9489 0524 JSR tube_read_r2 ; Read data byte from R2 for BPUT
948C 0527 .tube_poll_r1_wrch
JSR osbput ; Write a single byte A to an open file Y
948F 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.
9492 052D .tube_osbget
JSR tube_read_r2 ; Read channel handle from R2 for BGET
9495 0530 TAY ; Y=channel handle for OSBGET Y=file handle
9496 0531 JSR osbget ; Read a single byte from an open file Y
9499 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.
949C 0537 .tube_osrdch
JSR osrdch ; Read a character from the current input stream
949F 053A .tube_rdch_reply←2← 0534 JMP← 05EF JMP
ROR ; ROR A: encode carry (error flag) into bit 7
94A0 053B JSR tube_send_r2 ; Send carry+data byte to Tube R2
94A3 053E ROL ; ROL A: restore carry flag
94A4 053F JMP tube_reply_byte ; Return via tube_reply_byte
; 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.
94A7 0542 .tube_osfind
JSR tube_read_r2 ; Read open mode from R2 for OSFIND
94AA 0545 BEQ tube_osfind_close ; A=0: close file, else open with filename
94AC 0547 PHA ; Save open mode while reading filename
94AD 0548 JSR tube_read_string ; Read filename string from R2 into &0700
94B0 054B PLA ; Recover open mode from stack
94B1 054C JSR osfind ; Open or close file(s)
94B4 054F JMP tube_reply_byte ; Send file handle result to co-processor
94B7 0552 .tube_osfind_close←1← 0545 BEQ
JSR tube_read_r2 ; OSFIND close: read handle from R2
94BA 0555 TAY ; Y=handle to close
94BB 0556 LDA #osfind_close ; A=0: close command for OSFIND
94BD 0558 JSR osfind ; Close one or all files
94C0 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.
94C3 055E .tube_osargs
JSR tube_read_r2 ; Read file handle from R2 for OSARGS
94C6 0561 TAY ; Y=file handle for OSARGS
94C7 0562 .tube_read_params
LDX #4 ; Read 4-byte arg + reason from R2 into ZP
94C9 0564 .read_osargs_params←1← 056A BNE
JSR tube_read_r2 ; Read next param byte from R2
94CC 0567 STA escape_flag,x ; Params stored at &00-&03 (little-endian)
94CE 0569 DEX ; Decrement byte counter
94CF 056A BNE read_osargs_params ; Loop for 4 bytes
94D1 056C JSR tube_read_r2 ; Read OSARGS reason code from R2
94D4 056F JSR osargs ; Read or write a file's attributes
94D7 0572 JSR tube_send_r2 ; Send result A back to co-processor
94DA 0575 LDX #3 ; Return 4-byte result from ZP &00-&03
94DC 0577 .send_osargs_result←1← 057D BPL
LDA zp_ptr_lo,x ; Load result byte from zero page
94DE 0579 JSR tube_send_r2 ; Send byte to co-processor via R2
94E1 057C DEX ; Previous byte (count down)
94E2 057D BPL send_osargs_result ; Loop for all 4 bytes
94E4 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.
94E7 0582 .tube_read_string←3← 0548 JSR← 0596 JSR← 05B3 JSR
LDX #0 ; X=0: initialise string buffer index
94E9 0584 LDY #0 ; Y=0: string buffer offset 0
94EB 0586 .strnh←1← 0591 BNE
JSR tube_read_r2 ; Read next string byte from R2
94EE 0589 STA string_buf,y ; Store byte in string buffer at &0700+Y
94F1 058C INY ; Next buffer position
94F2 058D BEQ string_buf_done ; Y overflow: string too long, truncate
94F4 058F CMP #&0d ; Check for CR terminator
94F6 0591 BNE strnh ; Not CR: continue reading string
94F8 0593 .string_buf_done←1← 058D BEQ
LDY #7 ; Y=7: set XY=&0700 for OSCLI/OSFIND
94FA 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.
94FB 0596 .tube_oscli
JSR tube_read_string ; Read command string from R2 into &0700
94FE 0599 JSR oscli ; Execute * command via OSCLI
9501 059C .tube_reply_ack←3← 0489 JMP← 052A JMP← 055B JMP
LDA #&7f ; &7F = success acknowledgement
9503 059E .tube_reply_byte←4← 053F JMP← 054F JMP← 05A1 BVC← 067D JMP
BIT tube_status_register_2 ; Poll R2 status until ready
9506 05A1 BVC tube_reply_byte ; Bit 6 clear: not ready, loop
9508 05A3 STA tube_data_register_2 ; Write byte to R2 data register
950B 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.
950E 05A9 .tube_osfile
LDX #&10 ; Read 16-byte OSFILE control block from R2
9510 05AB .argsw←1← 05B1 BNE
JSR tube_read_r2 ; Read next control block byte from R2
9513 05AE STA zp_ptr_hi,x ; Store at &01+X (descending)
9515 05B0 DEX ; Decrement byte counter
9516 05B1 BNE argsw ; Loop for all 16 bytes
9518 05B3 JSR tube_read_string ; Read filename string from R2 into &0700
951B 05B6 STX zp_ptr_lo ; XY=&0700: filename pointer for OSFILE
951D 05B8 STY zp_ptr_hi ; Store Y=7 as pointer high byte
951F 05BA LDY #0 ; Y=0 for OSFILE control block offset
9521 05BC JSR tube_read_r2 ; Read OSFILE reason code from R2
9524 05BF JSR osfile ; Execute OSFILE operation
9527 05C2 JSR tube_send_r2 ; Send result A (object type) to co-processor
952A 05C5 LDX #&10 ; Return 16-byte control block to co-processor
952C 05C7 .send_osfile_ctrl_blk←1← 05CD BNE
LDA zp_ptr_hi,x ; Load control block byte
952E 05C9 JSR tube_send_r2 ; Send byte to co-processor via R2
9531 05CC DEX ; Decrement byte counter
9532 05CD BNE send_osfile_ctrl_blk ; Loop for all 16 bytes
9534 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.
9536 05D1 .tube_osgbpb
LDX #&0d ; Read 13-byte OSGBPB control block from R2
9538 05D3 .read_osgbpb_ctrl_blk←1← 05D9 BNE
JSR tube_read_r2 ; Read next control block byte from R2
953B 05D6 STA escape_flag,x ; Store at &FF+X (descending into &00-&0C)
953D 05D8 DEX ; Decrement byte counter
953E 05D9 BNE read_osgbpb_ctrl_blk ; Loop for all 13 bytes
9540 05DB JSR tube_read_r2 ; Read OSGBPB reason code from R2
9543 05DE LDY #0 ; Y=0 for OSGBPB control block
9545 05E0 JSR osgbpb ; Read or write multiple bytes to an open file
9548 05E3 PHA ; Save A (completion status) for later
9549 05E4 LDX #&0c ; Return 13-byte result block to co-processor
954B 05E6 .send_osgbpb_result←1← 05EC BPL
LDA zp_ptr_lo,x ; Load result byte from zero page
954D 05E8 JSR tube_send_r2 ; Send byte to co-processor via R2
9550 05EB DEX ; Decrement byte counter
9551 05EC BPL send_osgbpb_result ; Loop for 13 bytes (X=12..0)
9553 05EE PLA ; Recover completion status from stack
9554 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.
9557 05F2 .tube_osbyte_2param
JSR tube_read_r2 ; Read X param from R2 for 2-param OSBYTE
955A 05F5 TAX ; X = first parameter
955B 05F6 JSR tube_read_r2 ; Read A (OSBYTE number) from R2
955E 05F9 JSR osbyte ; Execute OSBYTE call
9561 05FC .tube_poll_r2_result←2← 05FF ref← 0625 BVS
BIT tube_status_register_2 ; Poll R2 status for result send
9564 05FF EQUB &50 ; BVC: page 5/6 boundary straddle
9565 0600 .tube_page6_start←1← 8161 STA
EQUB &FB ; Send carry+status to co-processor via R2
9566 0601 STX tube_data_register_2 ; Send X result for 2-param OSBYTE
9569 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.
956C 0607 .tube_osbyte_long
JSR tube_read_r2 ; Read X, Y, A from R2 for 3-param OSBYTE
956F 060A TAX ; Save in X
9570 060B JSR tube_read_r2 ; Read Y parameter from co-processor
9573 060E TAY ; Save in Y
9574 060F JSR tube_read_r2 ; Read A (OSBYTE function code)
9577 0612 JSR osbyte ; Execute OSBYTE A,X,Y
957A 0615 EOR #&9d ; Test for OSBYTE &9D (fast Tube BPUT)
957C 0617 BEQ bytex ; OSBYTE &9D (fast Tube BPUT): no result needed
957E 0619 ROR ; Encode carry (error flag) into bit 7
957F 061A JSR tube_send_r2 ; Send carry+status byte via R2
9582 061D .tube_osbyte_send_y←1← 0620 BVC
BIT tube_status_register_2 ; Poll R2 status for ready
9585 0620 BVC tube_osbyte_send_y ; Not ready: keep polling
9587 0622 STY tube_data_register_2 ; Send Y result, then fall through to send X
958A 0625 .tube_osbyte_short
BVS tube_poll_r2_result ; BVS &05FC: overlapping code — loops back to page 5 R2 poll to send X after Y
; 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.
958C 0627 .tube_osword
JSR tube_read_r2 ; Overlapping entry: &20 = JSR c06c5 (OSWORD)
958F 062A TAY ; Save OSWORD number in Y
9590 062B .tube_osword_read←1← 062E BPL
BIT tube_status_register_2 ; Poll R2 status for data ready
9593 062E BPL tube_osword_read ; Not ready: keep polling
9595 0630 .tube_osbyte_send_x
LDX tube_data_register_2 ; Read param block length from R2
9598 0633 DEX ; DEX: length 0 means no params to read
9599 0634 BMI skip_param_read ; No params (length=0): skip read loop
959B 0636 .tube_osword_read_lp←2← 0639 BPL← 0642 BPL
BIT tube_status_register_2 ; Poll R2 status for data ready
959E 0639 BPL tube_osword_read_lp ; Not ready: keep polling
95A0 063B LDA tube_data_register_2 ; Read param byte from R2
95A3 063E STA tube_osword_pb,x ; Store param bytes into block at &0128
95A6 0641 DEX ; Next param byte (descending)
95A7 0642 BPL tube_osword_read_lp ; Loop until all params read
95A9 0644 TYA ; Restore OSWORD number from Y
95AA 0645 .skip_param_read←1← 0634 BMI
LDX #<(tube_osword_pb) ; XY=&0128: param block address for OSWORD
95AC 0647 LDY #>(tube_osword_pb) ; Y=&01: param block at &0128
95AE 0649 JSR osword ; Execute OSWORD with XY=&0128
95B1 064C .poll_r2_osword_result←1← 064F BPL
BIT tube_status_register_2 ; Poll R2 status for ready
95B4 064F BPL poll_r2_osword_result ; Not ready: keep polling
95B6 0651 LDX tube_data_register_2 ; Read result block length from R2
95B9 0654 DEX ; Decrement result byte counter
95BA 0655 BMI tube_return_main ; No results to send: return to main loop
95BC 0657 .tube_osword_write←1← 0663 BPL
LDY tube_osword_pb,x ; Send result block bytes from &0128 via R2
95BF 065A .tube_osword_write_lp←1← 065D BVC
BIT tube_status_register_2 ; Poll R2 status for ready
95C2 065D BVC tube_osword_write_lp ; Not ready: keep polling
95C4 065F STY tube_data_register_2 ; Send result byte via R2
95C7 0662 DEX ; Next result byte (descending)
95C8 0663 BPL tube_osword_write ; Loop until all results sent
95CA 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).
95CD 0668 .tube_osword_rdln
LDX #4 ; Read 5-byte OSWORD 0 control block from R2
95CF 066A .read_rdln_ctrl_block←1← 0670 BPL
JSR tube_read_r2 ; Read control block byte from R2
95D2 066D STA zp_ptr_lo,x ; Store in zero page params
95D4 066F DEX ; Next byte (descending)
95D5 0670 BPL read_rdln_ctrl_block ; Loop until all 5 bytes read
95D7 0672 INX ; X=0 after loop, A=0 for OSWORD 0 (read line)
95D8 0673 LDY #0 ; Y=0 for OSWORD 0
95DA 0675 TXA ; A=0: OSWORD 0 (read line)
95DB 0676 JSR osword ; Read input line from keyboard
95DE 0679 BCC tube_rdln_send_line ; C=0: line read OK; C=1: escape pressed
95E0 067B LDA #&ff ; &FF = escape/error signal to co-processor
95E2 067D JMP tube_reply_byte ; Escape: send &FF error to co-processor
95E5 0680 .tube_rdln_send_line←1← 0679 BCC
LDX #0 ; X=0: start of input buffer at &0700
95E7 0682 LDA #&7f ; &7F = line read successfully
95E9 0684 JSR tube_send_r2 ; Send &7F (success) to co-processor
95EC 0687 .tube_rdln_send_loop←1← 0690 BNE
LDA string_buf,x ; Load char from input buffer
95EF 068A .tube_rdln_send_byte
JSR tube_send_r2 ; Send char to co-processor
95F2 068D INX ; Next character
95F3 068E CMP #&0d ; Check for CR terminator
95F5 0690 BNE tube_rdln_send_loop ; Loop until CR terminator sent
95F7 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.
95FA 0695 .tube_send_r2←14← 0020 JSR← 0026 JSR← 002C JSR← 0474 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)
95FD 0698 BVC tube_send_r2 ; Not ready: keep polling
95FF 069A STA tube_data_register_2 ; Write A to Tube R2 data register
9602 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.
9603 069E .tube_send_r4←8← 0018 JSR← 0418 JSR← 041D JSR← 043B JSR← 0443 JSR← 0448 JSR← 0463 JSR← 06A1 BVC
BIT tube_status_register_4_and_cpu_control ; Poll R4 status (bit 6 = ready)
9606 06A1 BVC tube_send_r4 ; Not ready: keep polling
9608 06A3 STA tube_data_register_4 ; Write A to Tube R4 data register
960B 06A6 RTS ; Return to caller
960C 06A7 .tube_escape_check←1← 0403 JMP
LDA escape_flag ; Check OS escape flag at &FF
960E 06A9 SEC ; SEC+ROR: put bit 7 of &FF into carry+bit 7
960F 06AA ROR ; ROR: shift escape bit 7 to carry
9610 06AB BMI tube_send_r1 ; Escape set: forward to co-processor via R1
9612 06AD .tube_event_handler
PHA ; EVNTV: forward event A, Y, X to co-processor
9613 06AE LDA #0 ; Send &00 prefix (event notification)
9615 06B0 JSR tube_send_r1 ; Send zero prefix via R1
9618 06B3 TYA ; Y value for event
9619 06B4 JSR tube_send_r1 ; Send Y via R1
961C 06B7 TXA ; X value for event
961D 06B8 JSR tube_send_r1 ; Send X via R1
9620 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.
9621 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)
9624 06BF BVC tube_send_r1 ; Not ready: keep polling
9626 06C1 STA tube_data_register_1 ; Write A to Tube R1 data register
9629 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.
962A 06C5 .tube_read_r2←21← 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← 0627 JSR← 066A JSR← 06C8 BPL
BIT tube_status_register_2 ; Poll R2 status (bit 7 = ready)
962D 06C8 BPL tube_read_r2 ; Not ready: keep polling
962F 06CA LDA tube_data_register_2 ; Read data byte from R2
9632 06CD RTS ; Return with byte in A
9633 06CE EQUB &FF, &FF, &FF, &FF, &FF, &FF, &FF, ; &FF padding &FF, &FF, &FF, &FF, &FF, &FF, &FF, ; (29 bytes &FF, &FF, &FF, &FF, &FF, &FF, &FF, ; before &FF, &FF, &FF, &FF, &FF, &FF, &FF, ; trampolines &FF ; )
9650 06EB .trampoline_tx_setup
JMP tx_begin ; Trampoline: begin TX operation
9653 06EE .trampoline_adlc_init
JMP adlc_init ; Trampoline: full ADLC init
9656 06F1 .svc_12_nmi_release
JMP wait_idle_and_reset ; Trampoline: wait idle and reset
9659 06F4 .svc_11_nmi_claim
JMP init_nmi_workspace ; Trampoline: init NMI workspace
965C 06F7 LDA #4 ; A=4: SR interrupt bit mask
965E 06F9 BIT system_via_ifr ; Test SR flag in VIA IFR
9661 06FC BNE l0701 ; SR active: handle interrupt
9663 06FE LDA #5 ; A=5: NMI not for us
9665 RTS ; RTS (end of save_vdu_state data)
9666 EQUB &8A, &48, &98, &48, &AD, ; Y >= &86: above &4B, &FE, &29, &E3, &0D, ; dispatch range Out of &51, &0D, &8D, &4B, &FE, ; range: skip &AD, &4A, &FE, &A9, &04, ; protection Save &8D, &4D, &FE, &8D, &4E, ; current JSR &FE, &AC, &57, &0D, &C0, ; protection mask &86, &B0, &0B, &AD, &63, ; Backup to &0D, &8D, &65, &0D, &09, ; saved_jsr_mask Set &1C, &8D, &63, &0D ; protection bits 2-4 ; Apply protection ; during dispatch
9692 .dispatch_svc5
EQUB &A9, &9B, ; Push return addr high (&9B) High &48, &B9, ; byte on stack for RTS Load dispatch &BF, &9A, ; target low byte Low byte on stack &48 ; for RTS
9699 .svc_5_unknown_irq
RTS ; RTS = dispatch to PHA'd address

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.

969A .adlc_init←1← 06EE JMP
BIT station_id_disable_net_nmis ; INTOFF: read station ID, disable NMIs
969D JSR adlc_full_reset ; Full ADLC hardware reset
96A0 LDA #osbyte_read_tube_presence ; OSBYTE &EA: check Tube co-processor
96A2 LDX #0 ; X=0 for OSBYTE
96A4 STX econet_init_flag ; Clear Econet init flag before setup
96A7 LDY #&ff ; Y=&FF for OSBYTE
96A9 JSR osbyte ; Read Tube present flag
96AC STX tube_flag ; X=value of Tube present flag
96AF LDA #osbyte_issue_service_request ; OSBYTE &8F: issue service request
96B1 LDX #&0c ; X=&0C: NMI claim service
96B3 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 &96B8 skips the service request for quick re-init. Then copies 32 bytes of NMI shim from ROM (&9FB2) 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).

96B5 .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.

96B8 .init_nmi_workspace←1← 06F4 JMP
LDY #&20 ; Copy 32 bytes of NMI shim from ROM to &0D00
96BA .copy_nmi_shim←1← 96C1 BNE
LDA listen_jmp_hi,y ; Read byte from NMI shim ROM source
96BD STA nmi_code_base,y ; Write to NMI shim RAM at &0D00
96C0 DEY ; Next byte (descending)
96C1 BNE copy_nmi_shim ; Loop until all 32 bytes copied
96C3 LDA romsel_copy ; Patch current ROM bank into NMI shim
96C5 STA nmi_shim_07 ; Self-modifying code: ROM bank at &0D07
96C8 LDA #&80 ; &80 = Econet initialised
96CA STA tx_clear_flag ; Mark TX as complete (ready)
96CD STA econet_init_flag ; Mark Econet as initialised
96D0 LDA station_id_disable_net_nmis ; Read station ID (&FE18 = INTOFF side effect)
96D3 STA tx_src_stn ; Store our station ID in TX scout
96D6 STY tx_src_net ; Y=0 after copy loop: net = local
96D9 STY need_release_tube ; Clear Tube release flag
96DB BIT video_ula_control ; INTON: re-enable NMIs (&FE20 read side effect)
96DE 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).

96DF .nmi_rx_scout←1← 9FBD JMP
LDA #1 ; A=&01: mask for SR2 bit0 (AP = Address Present)
96E1 BIT econet_control23_or_status2 ; BIT SR2: Z = A AND SR2 -- tests if AP is set
96E4 BEQ scout_error ; AP not set, no incoming data -- check for errors
96E6 LDA econet_data_continue_frame ; Read first RX byte (destination station address)
96E9 CMP station_id_disable_net_nmis ; Compare to our station ID (&FE18 read = INTOFF, disables NMIs)
96EC BEQ accept_frame ; Match -- accept frame
96EE CMP #&ff ; Check for broadcast address (&FF)
96F0 BNE scout_reject ; Neither our address nor broadcast -- reject frame
96F2 LDA #&40 ; Flag &40 = broadcast frame
96F4 STA tx_flags ; Store broadcast flag in TX flags
96F7 .accept_frame←1← 96EC BEQ
LDA #&fc ; Install next NMI handler at &9715 (RX scout second byte)
96F9 JMP install_nmi_handler ; 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 &972E.

96FC .nmi_rx_scout_net
BIT econet_control23_or_status2 ; BIT SR2: test for RDA (bit7 = data available)
96FF BPL scout_error ; No RDA -- check errors
9701 LDA econet_data_continue_frame ; Read destination network byte
9704 BEQ accept_local_net ; Network = 0 -- local network, accept
9706 EOR #&ff ; EOR &FF: test if network = &FF (broadcast)
9708 BEQ accept_scout_net ; Broadcast network -- accept
970A .scout_reject←1← 96F0 BNE
LDA #&a2 ; Reject: wrong network. CR1=&A2: RIE|RX_DISCONTINUE
970C STA econet_control1_or_status1 ; Write CR1 to discontinue RX
970F JMP install_rx_scout_handler ; Return to idle scout listening
9712 .accept_local_net←1← 9704 BEQ
STA tx_flags ; Network = 0 (local): clear tx_flags
9715 .accept_scout_net←1← 9708 BEQ
STA port_buf_len ; Store Y offset for scout data buffer
9717 LDA #&2e ; Install scout data reading loop at &972E
9719 LDY #&97 ; High byte of scout data handler
971B 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 &9733) or when scout completion finds unexpected SR2 state. If SR2 & &81 is non-zero (AP or RDA still active), performs full ADLC reset and discards. If zero (clean end), discards via &9A0A. This path is a common landing for any unexpected ADLC state during scout reception.

971E .scout_error←5← 96E4 BEQ← 96FF BPL← 9733 BPL← 9767 BEQ← 9769 BPL
LDA econet_control23_or_status2 ; Read SR2
9721 AND #&81 ; Test AP (b0) | RDA (b7)
9723 BEQ scout_discard ; Neither set -- clean end, discard via &972B
9725 JSR adlc_full_reset ; Unexpected data/status: full ADLC reset
9728 JMP install_rx_scout_handler ; Discard and return to idle
972B .scout_discard←1← 9723 BEQ
JMP discard_listen ; Gentle discard: RX_DISCONTINUE

Scout data reading loop

Reads the body of a scout frame, two bytes per iteration. Stores bytes at &0D3D+Y (scout buffer: src_stn, src_net, ctrl, port, ...). Between each pair it checks SR2: - At entry (&9730): SR2 read, BPL tests RDA (bit7) - No RDA (BPL) -> error (&971E) - RDA set (BMI) -> read byte - After first byte (&973C): full SR2 tested - SR2 non-zero (BNE) -> scout completion (&9758) 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 (&9750): re-test full SR2 - SR2 non-zero (BNE) -> loop back to &9733 - SR2 = 0 -> RTI, wait for next NMI The loop ends at Y=&0C (12 bytes max in scout buffer).

972E .scout_data_loop
LDY port_buf_len ; Y = buffer offset
9730 LDA econet_control23_or_status2 ; Read SR2
9733 .scout_loop_rda←1← 9753 BNE
BPL scout_error ; No RDA -- error handler &971E
9735 LDA econet_data_continue_frame ; Read data byte from RX FIFO
9738 STA rx_src_stn,y ; Store at &0D3D+Y (scout buffer)
973B INY ; Advance buffer index
973C LDA econet_control23_or_status2 ; Read SR2 again (FV detection point)
973F BMI scout_loop_second ; RDA set -- more data, read second byte
9741 BNE scout_complete ; SR2 non-zero (FV or other) -- scout completion
9743 .scout_loop_second←1← 973F BMI
LDA econet_data_continue_frame ; Read second byte of pair
9746 STA rx_src_stn,y ; Store at &0D3D+Y
9749 INY ; Advance and check buffer limit
974A CPY #&0c ; Copied all 12 scout bytes?
974C BEQ scout_complete ; Buffer full (Y=12) -- force completion
974E STY port_buf_len ; Save final buffer offset
9750 LDA econet_control23_or_status2 ; Read SR2 for next pair
9753 BNE scout_loop_rda ; SR2 non-zero -- loop back for more bytes
9755 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 &971E (not a valid frame end) - FV set, no RDA (BPL) -> error &971E (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 (&9A46) - Port non-zero -> check if it matches an open receive block

9758 .scout_complete←2← 9741 BNE← 974C BEQ
LDA #0 ; CR1=&00: disable all interrupts
975A STA econet_control1_or_status1 ; Write CR1
975D LDA #&84 ; CR2=&84: disable PSE, enable RDA_SUPPRESS_FV
975F STA econet_control23_or_status2 ; Write CR2
9762 LDA #2 ; A=&02: FV mask for SR2 bit1
9764 BIT econet_control23_or_status2 ; BIT SR2: test FV (Z) and RDA (N)
9767 BEQ scout_error ; No FV -- not a valid frame end, error
9769 BPL scout_error ; FV set but no RDA -- missing last byte, error
976B LDA econet_data_continue_frame ; Read last byte from RX FIFO
976E STA rx_src_stn,y ; Store last byte at &0D3D+Y
9771 LDA #&44 ; CR1=&44: RX_RESET | TIE (switch to TX for ACK)
9773 STA econet_control1_or_status1 ; Write CR1: switch to TX mode
9776 SEC ; Set bit7 of need_release_tube flag
9777 ROR need_release_tube ; Rotate C=1 into bit7: mark Tube release needed
9779 LDA rx_port ; Check port byte: 0 = immediate op, non-zero = data transfer
977C BNE scout_match_port ; Port non-zero -- look for matching receive block
977E .scout_no_match
JMP immediate_op ; Port = 0 -- immediate operation handler
9781 .scout_match_port←1← 977C BNE
BIT tx_flags ; Check if broadcast (bit6 of tx_flags)
9784 BVC scan_port_list ; Not broadcast -- skip CR2 setup
9786 LDA #7 ; CR2=&07: broadcast prep
9788 STA econet_control23_or_status2 ; Write CR2: broadcast frame prep
978B .scan_port_list←1← 9784 BVC
BIT rx_flags ; Check if RX port list active (bit7)
978E BPL try_nfs_port_list ; No active ports -- try NFS workspace
9790 LDA #&c0 ; Start scanning port list at page &C0
9792 LDY #0 ; Y=0: start offset within each port slot
9794 .scan_nfs_port_list←1← 97D7 BNE
STA port_ws_offset ; Store page to workspace pointer low
9796 STY rx_buf_offset ; Store page high byte for slot scanning
9798 .check_port_slot←1← 97C9 BCC
LDY #0 ; Y=0: read control byte from start of slot
979A .scout_ctrl_check
LDA (port_ws_offset),y ; Read port control byte from slot
979C BEQ discard_no_match ; Zero = end of port list, no match
979E CMP #&7f ; &7F = any-port wildcard
97A0 BNE next_port_slot ; Not wildcard -- check specific port match
97A2 INY ; Y=1: advance to port byte in slot
97A3 LDA (port_ws_offset),y ; Read port number from slot (offset 1)
97A5 BEQ check_station_filter ; Zero port in slot = match any port
97A7 CMP rx_port ; Check if port matches this slot
97AA BNE next_port_slot ; Port mismatch -- try next slot
97AC .check_station_filter←1← 97A5 BEQ
INY ; Y=2: advance to station byte
97AD LDA (port_ws_offset),y ; Read station filter from slot (offset 2)
97AF BEQ port_match_found ; Zero station = match any station, accept
97B1 CMP rx_src_stn ; Check if source station matches
97B4 BNE next_port_slot ; Station mismatch -- try next slot
97B6 .scout_port_match
INY ; Y=3: advance to network byte
97B7 LDA (port_ws_offset),y ; Read network filter from slot (offset 3)
97B9 CMP rx_src_net ; Check if source network matches
97BC BEQ port_match_found ; Network matches or zero = accept
97BE .next_port_slot←3← 97A0 BNE← 97AA BNE← 97B4 BNE
LDA rx_buf_offset ; Check if NFS workspace search pending
97C0 BEQ try_nfs_port_list ; No NFS workspace -- try fallback path
97C2 LDA port_ws_offset ; Load current slot base address
97C4 CLC ; CLC for 12-byte slot advance
97C5 ADC #&0c ; Advance to next 12-byte port slot
97C7 STA port_ws_offset ; Update workspace pointer to next slot
97C9 BCC check_port_slot ; Always branches (page &C0 won't overflow)
97CB .discard_no_match←2← 979C BEQ← 97D1 BVC
JMP nmi_error_dispatch ; No match found -- discard frame
97CE .try_nfs_port_list←2← 978E BPL← 97C0 BEQ
BIT rx_flags ; Try NFS workspace if paged list exhausted
97D1 BVC discard_no_match ; No NFS workspace RX (bit6 clear) -- discard
97D3 LDA #0 ; NFS workspace starts at offset 0 in page
97D5 LDY nfs_workspace_hi ; NFS workspace high byte for port list
97D7 BNE scan_nfs_port_list ; Scan NFS workspace port list
97D9 .port_match_found←3← 97AF BEQ← 97BC BEQ← 9AC9 JMP
LDA #3 ; Match found: set scout_status = 3
97DB STA scout_status ; Record match for completion handler
97DE JSR tx_calc_transfer ; Calculate transfer parameters
97E1 BCC nmi_error_dispatch ; C=0: no Tube claimed -- discard
97E3 BIT tx_flags ; Check broadcast flag for ACK path
97E6 BVC send_data_rx_ack ; Not broadcast -- normal ACK path
97E8 JMP copy_scout_to_buffer ; Broadcast: different completion path
97EB .send_data_rx_ack←2← 97E6 BVC← 9ABE JMP
LDA #&44 ; CR1=&44: RX_RESET | TIE
97ED STA econet_control1_or_status1 ; Write CR1: TX mode for ACK
97F0 LDA #&a7 ; CR2=&A7: RTS | CLR_TX_ST | FC_TDRA | PSE
97F2 STA econet_control23_or_status2 ; Write CR2: enable TX with PSE
97F5 LDA #&fc ; Install data_rx_setup at &97FC
97F7 LDY #&97 ; High byte of data_rx_setup handler
97F9 JMP ack_tx_write_dest ; Send ACK with data_rx_setup as next NMI
97FC .data_rx_setup
LDA #&82 ; CR1=&82: TX_RESET | RIE (switch to RX for data frame)
97FE STA econet_control1_or_status1 ; Write CR1: switch to RX for data frame
9801 LDA #8 ; Install nmi_data_rx at &9808
9803 LDY #&98 ; Y=&98: NMI handler high byte
9805 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: &9808 (AP+addr check) -> &981C (net=0 check) -> &9832 (skip ctrl+port) -> &9865 (bulk data read) -> &9899 (completion)

9808 .nmi_data_rx
LDA #1 ; A=&01: mask for AP (Address Present)
980A BIT econet_control23_or_status2 ; BIT SR2: test AP bit
980D BEQ nmi_error_dispatch ; No AP: wrong frame or error
980F LDA econet_data_continue_frame ; Read first byte (dest station)
9812 CMP station_id_disable_net_nmis ; Compare to our station ID (INTOFF)
9815 BNE nmi_error_dispatch ; Not for us: error path
9817 LDA #&1c ; Install net check handler at &981C
9819 JMP install_nmi_handler ; Set NMI vector via RAM shim
981C .nmi_data_rx_net
BIT econet_control23_or_status2 ; Validate source network = 0
981F BPL nmi_error_dispatch ; SR2 bit7 clear: no data ready -- error
9821 LDA econet_data_continue_frame ; Read dest network byte
9824 BNE nmi_error_dispatch ; Network != 0: wrong network -- error
9826 LDA #&32 ; Install skip handler at &9832
9828 LDY #&98 ; High byte of &9832 handler
982A BIT econet_control1_or_status1 ; SR1 bit7: IRQ, data already waiting
982D BMI nmi_data_rx_skip ; Data ready: skip directly, no RTI
982F JMP set_nmi_vector ; Install handler and return via RTI
9832 .nmi_data_rx_skip←1← 982D BMI
BIT econet_control23_or_status2 ; Skip control and port bytes (already known from scout)
9835 BPL nmi_error_dispatch ; SR2 bit7 clear: error
9837 LDA econet_data_continue_frame ; Discard control byte
983A LDA econet_data_continue_frame ; Discard port byte
fall through ↓

Install data RX bulk or Tube handler

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

983D .install_data_rx_handler←1← 9ED1 JMP
LDA #2 ; A=2: Tube transfer flag mask
983F BIT tx_flags ; Check if Tube transfer active
9842 BNE install_tube_rx ; Tube active: use Tube RX path
9844 LDA #&65 ; Install bulk read at &9865
9846 LDY #&98 ; High byte of &9865 handler
9848 BIT econet_control1_or_status1 ; SR1 bit7: more data already waiting?
984B BMI nmi_data_rx_bulk ; Yes: enter bulk read directly
984D JMP set_nmi_vector ; No: install handler and RTI
9850 .install_tube_rx←1← 9842 BNE
LDA #&c2 ; Tube: install Tube RX at &98C2
9852 LDY #&98 ; High byte of &98C2 handler
9854 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).

9857 .nmi_error_dispatch←12← 97CB JMP← 97E1 BCC← 980D BEQ← 9815 BNE← 981F BPL← 9824 BNE← 9835 BPL← 9878 BEQ← 98AA BEQ← 98B0 BEQ← 996D JMP← 9A98 JMP
LDA tx_flags ; Check tx_flags for error path
985A BPL rx_error ; Bit7 clear: RX error path
985C JMP tx_result_fail ; Bit7 set: TX result = not listening
985F .rx_error←1← 985A BPL
.rx_error_reset←1← 985A BPL
JSR adlc_full_reset ; Full ADLC reset on RX error
9862 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 &9899. SR2 = 0 -> RTI, wait for next NMI to continue.

9865 .nmi_data_rx_bulk←1← 984B BMI
LDY port_buf_len ; Y = buffer offset, resume from last position
9867 LDA econet_control23_or_status2 ; Read SR2 for next pair
986A .data_rx_loop←1← 9894 BNE
BPL data_rx_complete ; SR2 bit7 clear: frame complete (FV)
986C LDA econet_data_continue_frame ; Read first byte of pair from RX FIFO
986F STA (open_port_buf),y ; Store byte to buffer
9871 INY ; Advance buffer offset
9872 BNE read_sr2_between_pairs ; Y != 0: no page boundary crossing
9874 INC open_port_buf_hi ; Crossed page: increment buffer high byte
9876 DEC port_buf_len_hi ; Decrement remaining page count
9878 BEQ nmi_error_dispatch ; No pages left: handle as complete
987A .read_sr2_between_pairs←1← 9872 BNE
LDA econet_control23_or_status2 ; Read SR2 between byte pairs
987D BMI read_second_rx_byte ; SR2 bit7 set: more data available
987F BNE data_rx_complete ; SR2 non-zero, bit7 clear: frame done
9881 .read_second_rx_byte←1← 987D BMI
LDA econet_data_continue_frame ; Read second byte of pair from RX FIFO
9884 STA (open_port_buf),y ; Store byte to buffer
9886 INY ; Advance buffer offset
9887 STY port_buf_len ; Save updated buffer position
9889 BNE check_sr2_loop_again ; Y != 0: no page boundary crossing
988B INC open_port_buf_hi ; Crossed page: increment buffer high byte
988D DEC port_buf_len_hi ; Decrement remaining page count
988F BEQ data_rx_complete ; No pages left: frame complete
9891 .check_sr2_loop_again←1← 9889 BNE
LDA econet_control23_or_status2 ; Read SR2 for next iteration
9894 BNE data_rx_loop ; SR2 non-zero: more data, loop back
9896 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 (&9758): disables PSE (CR2=&84, CR1=&00), 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 &9910.

9899 .data_rx_complete←3← 986A BPL← 987F BNE← 988F BEQ
LDA #&84 ; CR2=&84: disable PSE for bit testing
989B STA econet_control23_or_status2 ; Write CR2
989E LDA #0 ; CR1=&00: disable all interrupts
98A0 STA econet_control1_or_status1 ; Write CR1
98A3 STY port_buf_len ; Save Y (byte count from data RX loop)
98A5 LDA #2 ; A=&02: FV mask
98A7 BIT econet_control23_or_status2 ; BIT SR2: test FV (Z) and RDA (N)
98AA BEQ nmi_error_dispatch ; No FV -- error
98AC BPL send_ack ; FV set, no RDA -- proceed to ACK
98AE LDA port_buf_len_hi ; Check if buffer space remains
98B0 .read_last_rx_byte←3← 98CD BEQ← 98F4 BEQ← 9900 BEQ
BEQ nmi_error_dispatch ; No buffer space: error/discard frame
98B2 LDA econet_data_continue_frame ; FV+RDA: read and store last data byte
98B5 LDY port_buf_len ; Y = current buffer write offset
98B7 STA (open_port_buf),y ; Store last byte in port receive buffer
98B9 INC port_buf_len ; Advance buffer write offset
98BB BNE send_ack ; No page wrap: proceed to send ACK
98BD INC open_port_buf_hi ; Page boundary: advance buffer page
98BF .send_ack←2← 98AC BPL← 98BB BNE
JMP ack_tx ; Send ACK frame to complete handshake
98C2 .nmi_data_rx_tube
LDA econet_control23_or_status2 ; Read SR2 for Tube data receive path
98C5 .rx_tube_data←1← 98E0 BNE
BPL data_rx_tube_complete ; RDA clear: no more data, frame complete
98C7 LDA econet_data_continue_frame ; Read data byte from ADLC RX FIFO
98CA JSR inc_buf_counter_32 ; Check buffer limits and transfer size
98CD BEQ read_last_rx_byte ; Zero: buffer full, handle as error
98CF STA tube_data_register_3 ; Send byte to Tube data register 3
98D2 LDA econet_data_continue_frame ; Read second data byte (paired transfer)
98D5 STA tube_data_register_3 ; Send second byte to Tube
98D8 JSR inc_buf_counter_32 ; Check limits after byte pair
98DB BEQ data_rx_tube_complete ; Zero: Tube transfer complete
98DD LDA econet_control23_or_status2 ; Re-read SR2 for next byte pair
98E0 BNE rx_tube_data ; More data available: continue loop
98E2 .data_rx_tube_error
JMP nmi_rti ; Unexpected end: return from NMI
98E5 .data_rx_tube_complete←2← 98C5 BPL← 98DB BEQ
LDA #0 ; CR1=&00: disable all interrupts
98E7 STA econet_control1_or_status1 ; Write CR1 for individual bit testing
98EA LDA #&84 ; CR2=&84: disable PSE
98EC STA econet_control23_or_status2 ; Write CR2: same pattern as main path
98EF LDA #2 ; A=&02: FV mask for Tube completion
98F1 BIT econet_control23_or_status2 ; BIT SR2: test FV (Z) and RDA (N)
98F4 BEQ read_last_rx_byte ; No FV: incomplete frame, error
98F6 BPL ack_tx ; FV set, no RDA: proceed to ACK
98F8 LDA port_buf_len ; Check if any buffer was allocated
98FA ORA port_buf_len_hi ; OR all 4 buffer pointer bytes together
98FC ORA open_port_buf ; Check buffer low byte
98FE ORA open_port_buf_hi ; Check buffer high byte
9900 BEQ read_last_rx_byte ; All zero (null buffer): error
9902 LDA econet_data_continue_frame ; Read extra trailing byte from FIFO
9905 STA rx_extra_byte ; Save extra byte at &0D5D for later use
9908 LDA #&20 ; Bit5 = extra data byte available flag
990A ORA tx_flags ; Set extra byte flag in tx_flags
990D 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 (&9EDB). 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.

9910 .ack_tx←2← 98BF JMP← 98F6 BPL
LDA tx_flags ; Load TX flags to check ACK type
9913 BPL ack_tx_configure ; Bit7 clear: normal scout ACK
9915 JSR advance_rx_buffer_ptr ; Final ACK: call completion handler
9918 JMP tx_result_ok ; Jump to TX success result
991B .ack_tx_configure←1← 9913 BPL
LDA #&44 ; CR1=&44: RX_RESET | TIE (switch to TX mode)
991D STA econet_control1_or_status1 ; Write CR1: switch to TX mode
9920 LDA #&a7 ; CR2=&A7: RTS|CLR_TX_ST|FC_TDRA|2_1_BYTE|PSE
9922 STA econet_control23_or_status2 ; Write CR2: enable TX with status clear
9925 LDA #&b7 ; Install saved next handler (&99B7 for scout ACK)
9927 LDY #&99 ; High byte of post-ACK handler
9929 .ack_tx_write_dest←2← 97F9 JMP← 9B06 JMP
STA nmi_next_lo ; Store next handler low byte
992C STY nmi_next_hi ; Store next handler high byte
992F LDA rx_src_stn ; Load dest station from RX scout buffer
9932 BIT econet_control1_or_status1 ; BIT SR1: test TDRA (V=bit6)
9935 BVC dispatch_nmi_error ; TDRA not ready -- error
9937 STA econet_data_continue_frame ; Write dest station to TX FIFO
993A LDA rx_src_net ; Write dest network to TX FIFO
993D STA econet_data_continue_frame ; Write dest net byte to FIFO
9940 LDA #&47 ; Install handler at &9992 (write src addr)
9942 LDY #&99 ; High byte of nmi_ack_tx_src
9944 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.

9947 .nmi_ack_tx_src
LDA station_id_disable_net_nmis ; Load our station ID (also INTOFF)
994A BIT econet_control1_or_status1 ; BIT SR1: test TDRA
994D BVC dispatch_nmi_error ; TDRA not ready -- error
994F STA econet_data_continue_frame ; Write our station to TX FIFO
9952 LDA #0 ; Write network=0 to TX FIFO
9954 STA econet_data_continue_frame ; Write network=0 (local) to TX FIFO
9957 LDA tx_flags ; Check tx_flags for data phase
995A BMI start_data_tx ; bit7 set: start data TX phase
995C 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.

995E .post_ack_scout
STA econet_control23_or_status2 ; Write CR2 to clear status after ACK TX
9961 LDA nmi_next_lo ; Install saved handler from &0D4B/&0D4C
9964 LDY nmi_next_hi ; Load saved next handler high byte
9967 JMP set_nmi_vector ; Install next NMI handler
996A .start_data_tx←1← 995A BMI
JMP data_tx_begin ; Jump to start data TX phase
996D .dispatch_nmi_error←2← 9935 BVC← 994D 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.

9970 .advance_rx_buffer_ptr←2← 9915 JSR← 99C6 JSR
LDA #2 ; A=2: test bit1 of tx_flags
9972 BIT tx_flags ; BIT tx_flags: check data transfer bit
9975 BEQ return_10 ; Bit1 clear: no transfer -- return
9977 CLC ; CLC: init carry for 4-byte add
9978 PHP ; Save carry on stack for loop
9979 LDY #8 ; Y=8: RXCB high pointer offset
997B .add_rxcb_ptr←1← 9987 BCC
LDA (port_ws_offset),y ; Load RXCB[Y] (buffer pointer byte)
997D PLP ; Restore carry from stack
997E ADC net_tx_ptr,y ; Add transfer count byte
9981 STA (port_ws_offset),y ; Store updated pointer back to RXCB
9983 INY ; Next byte
9984 PHP ; Save carry for next iteration
9985 CPY #&0c ; Done 4 bytes? (Y reaches &0C)
9987 BCC add_rxcb_ptr ; No: continue adding
9989 PLP ; Discard final carry
998A LDA #&20 ; A=&20: test bit5 of tx_flags
998C BIT tx_flags ; BIT tx_flags: check Tube bit
998F BEQ skip_tube_update ; No Tube: skip Tube update
9991 TXA ; Save X on stack
9992 PHA ; Push X
9993 LDA #8 ; A=8: offset for Tube address
9995 CLC ; CLC for address calculation
9996 ADC port_ws_offset ; Add workspace base offset
9998 TAX ; X = address low for Tube claim
9999 LDY rx_buf_offset ; Y = address high for Tube claim
999B LDA #1 ; A=1: Tube claim type (read)
999D JSR tube_addr_claim ; Claim Tube address for transfer
99A0 LDA rx_extra_byte ; Load extra RX data byte
99A3 STA tube_data_register_3 ; Send to Tube via R3
99A6 SEC ; SEC: init carry for increment
99A7 LDY #8 ; Y=8: start at high pointer
99A9 .inc_rxcb_ptr←1← 99B0 BCS
LDA #0 ; A=0: add carry only (increment)
99AB ADC (port_ws_offset),y ; Add carry to pointer byte
99AD STA (port_ws_offset),y ; Store back to RXCB
99AF INY ; Next byte
99B0 BCS inc_rxcb_ptr ; Keep going while carry propagates
99B2 PLA ; Restore X from stack
99B3 TAX ; Transfer to X register
99B4 .skip_tube_update←1← 998F BEQ
LDA #&ff ; A=&FF: return value (transfer done)
99B6 .return_10←1← 9975 BEQ
RTS ; Return
99B7 LDA rx_port ; Load received port byte
99BA BNE rx_complete_update_rxcb ; Port != 0: data transfer frame
99BC LDY rx_ctrl ; Port=0: load control byte
99BF CPY #&82 ; Ctrl = &82 (POKE)?
99C1 BEQ rx_complete_update_rxcb ; Yes: POKE also needs data transfer
99C3 JMP imm_op_build_reply ; Other port-0 ops: immediate dispatch

Complete RX and update RXCB

Post-scout completion for data transfer frames (port != 0) and POKE (ctrl=&82). Calls advance_rx_buffer_ptr, updates the open port buffer address, then writes source station/ network, port, and control byte into the RXCB.

99C6 .rx_complete_update_rxcb←3← 99BA BNE← 99C1 BEQ← 9A38 JMP
JSR advance_rx_buffer_ptr ; Update buffer pointer and check for Tube
99C9 BNE skip_buf_ptr_update ; Transfer not done: skip buffer update
99CB .add_buf_to_base
LDA port_buf_len ; Load buffer bytes remaining
99CD CLC ; CLC for address add
99CE ADC open_port_buf ; Add to buffer base address
99D0 BCC store_buf_ptr_lo ; No carry: skip high byte increment
99D2 .inc_rxcb_buf_hi
INC open_port_buf_hi ; Carry: increment buffer high byte
99D4 .store_buf_ptr_lo←1← 99D0 BCC
LDY #8 ; Y=8: store updated buffer position
99D6 .store_rxcb_buf_ptr
STA (port_ws_offset),y ; Store updated low byte to RXCB
99D8 INY ; Y=9: buffer high byte offset
99D9 LDA open_port_buf_hi ; Load updated buffer high byte
99DB .store_rxcb_buf_hi
STA (port_ws_offset),y ; Store high byte to RXCB
99DD .skip_buf_ptr_update←1← 99C9 BNE
LDA rx_port ; Check port byte again
99E0 BEQ discard_reset_listen ; Port=0: immediate op, discard+listen
99E2 LDA rx_src_net ; Load source network from scout buffer
99E5 LDY #3 ; Y=3: RXCB source network offset
99E7 STA (port_ws_offset),y ; Store source network to RXCB
99E9 DEY ; Y=2: source station offset Y=&02
99EA LDA rx_src_stn ; Load source station from scout buffer
99ED STA (port_ws_offset),y ; Store source station to RXCB
99EF DEY ; Y=1: port byte offset Y=&01
99F0 LDA rx_port ; Load port byte
99F3 STA (port_ws_offset),y ; Store port to RXCB
99F5 DEY ; Y=0: control/flag byte offset Y=&00
99F6 LDA rx_ctrl ; Load control byte from scout
99F9 ORA #&80 ; Set bit7 = reception complete flag
99FB STA (port_ws_offset),y ; Store to RXCB (marks CB as complete)
fall through ↓

Discard with Tube release

Conditionally releases the Tube co-processor before discarding. If tx_flags bit 1 is set (Tube transfer was active), calls sub_c9a2b to release the Tube claim, then falls through to discard_listen. The main teardown path for RX operations that used the Tube.

99FD .discard_reset_listen←4← 9862 JMP← 99E0 BEQ← 9E80 JMP← 9EEA JMP
LDA #2 ; Tube flag bit 1 AND tx_flags bit 1
99FF AND tube_flag ; Check if Tube transfer active
9A02 BIT tx_flags ; Test tx_flags for Tube transfer
9A05 BEQ discard_listen ; No Tube transfer active -- skip release
9A07 JSR release_tube ; Release Tube claim before discarding
fall through ↓

Discard frame and return to idle listen

Calls adlc_rx_listen to re-enter idle RX mode (CR1=&82, CR2=&67), then installs nmi_rx_scout (&96DF) as the NMI handler via set_nmi_vector. Returns to the caller's NMI context. Used as the common discard tail for both gentle rejection (wrong station/network) and error recovery paths.

9A0A .discard_listen←4← 972B JMP← 9A05 BEQ← 9A83 BCS← 9B3F JMP
JSR adlc_rx_listen ; Re-enter idle RX listen mode
fall through ↓

Install RX scout NMI handler

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

9A0D .install_rx_scout_handler←2← 970F JMP← 9728 JMP
LDA #&df ; Install nmi_rx_scout (&96DF) as NMI handler
9A0F LDY #&96 ; High byte of nmi_rx_scout
9A11 JMP set_nmi_vector ; Set NMI vector and return

Copy scout data to port buffer

Copies scout data bytes (offsets 4-11) from the RX scout buffer into the open port buffer, handling both direct memory and Tube R3 write paths.

9A14 .copy_scout_to_buffer←1← 97E8 JMP
TXA ; Save X on stack
9A15 PHA ; Push X
9A16 LDX #4 ; X=4: start at scout byte offset 4
9A18 LDA #2 ; A=2: Tube transfer check mask
9A1A BIT tx_flags ; BIT tx_flags: check Tube bit
9A1D BNE copy_scout_via_tube ; Tube active: use R3 write path
9A1F LDY port_buf_len ; Y = current buffer position
9A21 .copy_scout_bytes←1← 9A34 BNE
LDA rx_src_stn,x ; Load scout data byte
9A24 STA (open_port_buf),y ; Store to port buffer
9A26 INY ; Advance buffer pointer
9A27 BNE next_scout_byte ; No page crossing
9A29 INC open_port_buf_hi ; Page crossing: inc buffer high byte
9A2B DEC port_buf_len_hi ; Decrement remaining page count
9A2D BEQ scout_page_overflow ; No pages left: overflow
9A2F .next_scout_byte←1← 9A27 BNE
INX ; Next scout data byte
9A30 STY port_buf_len ; Save updated buffer position
9A32 CPX #&0c ; Done all scout data? (X reaches &0C)
9A34 BNE copy_scout_bytes ; No: continue copying
9A36 .scout_copy_done←2← 9A4B BEQ← 9A94 BEQ
PLA ; Restore X from stack
9A37 TAX ; Transfer to X register
9A38 JMP rx_complete_update_rxcb ; Jump to completion handler
9A3B .copy_scout_via_tube←2← 9A1D BNE← 9A49 BNE
LDA rx_src_stn,x ; Tube path: load scout data byte
9A3E STA tube_data_register_3 ; Send byte to Tube via R3
9A41 JSR inc_buf_counter_32 ; Increment buffer position counters
9A44 BEQ check_scout_done ; Counter overflow: handle end of buffer
9A46 INX ; Next scout data byte
9A47 CPX #&0c ; Done all scout data?
9A49 BNE copy_scout_via_tube ; No: continue Tube writes
9A4B BEQ scout_copy_done ; ALWAYS branch

Release Tube co-processor claim

If need_release_tube bit 7 is clear (Tube is claimed), calls tube_addr_claim with A=&82 to release it, then clears the release flag via LSR.

9A4D .release_tube←2← 9A07 JSR← 9F45 JSR
BIT need_release_tube ; Check if Tube needs releasing
9A4F BMI clear_release_flag ; Bit7 set: already released
9A51 LDA #&82 ; A=&82: Tube release claim type
9A53 JSR tube_addr_claim ; Release Tube address claim
9A56 .clear_release_flag←1← 9A4F BMI
LSR need_release_tube ; Clear release flag (LSR clears bit7)
9A58 RTS ; Return

Increment 32-bit buffer counter

Increments a 4-byte counter across port_buf_len / port_buf_len_hi / open_port_buf / open_port_buf_hi with carry propagation. Returns Z=1 if the counter wraps to zero.

9A59 .inc_buf_counter_32←3← 98CA JSR← 98D8 JSR← 9A41 JSR
INC port_buf_len ; Increment buffer position (4-byte)
9A5B BNE return_inc_port_buf ; Low byte didn't wrap: done
9A5D INC port_buf_len_hi ; Carry into second byte
9A5F BNE return_inc_port_buf ; No further carry: done
9A61 INC open_port_buf ; Carry into third byte
9A63 BNE return_inc_port_buf ; No further carry: done
9A65 INC open_port_buf_hi ; Carry into fourth byte
9A67 .return_inc_port_buf←3← 9A5B BNE← 9A5F BNE← 9A63 BNE
RTS ; Return

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.

9A68 .immediate_op←1← 977E JMP
LDY rx_ctrl ; Control byte &81-&88 range check
9A6B CPY #&81 ; Below &81: not an immediate op
9A6D BCC imm_op_out_of_range ; Out of range low: jump to discard
9A6F CPY #&89 ; Above &88: not an immediate op
9A71 BCS imm_op_out_of_range ; Out of range high: jump to discard
9A73 CPY #&87 ; HALT(&87)/CONTINUE(&88) skip protection
9A75 BCS dispatch_imm_op ; Ctrl >= &87: dispatch without mask check
9A77 TYA ; Convert ctrl byte to 0-based index for mask
9A78 SEC ; SEC for subtract
9A79 SBC #&81 ; A = ctrl - &81 (0-based operation index)
9A7B TAY ; Y = index for mask rotation count
9A7C LDA prot_status ; Load protection mask from LSTAT
9A7F .rotate_prot_mask←1← 9A81 BPL
ROR ; Rotate mask right by control byte index
9A80 DEY ; Decrement rotation counter
9A81 BPL rotate_prot_mask ; Loop until bit aligned
9A83 BCS discard_listen ; Bit set = operation disabled, discard
9A85 .dispatch_imm_op←1← 9A75 BCS
LDY rx_ctrl ; Reload ctrl byte for dispatch table
9A88 LDA #&9a ; Hi byte: all handlers are in page &9A
9A8A PHA ; Push hi byte for PHA/PHA/RTS dispatch
9A8B LDA imm_op_dispatch_lo-&81,y ; Load handler low byte from jump table
9A8E PHA ; Push handler low byte
9A8F RTS ; RTS dispatches to handler
9A90 .scout_page_overflow←1← 9A2D BEQ
INC port_buf_len ; Increment port buffer length
9A92 .check_scout_done←1← 9A44 BEQ
CPX #&0b ; Check if scout data index reached 11
9A94 BEQ scout_copy_done ; Yes: loop back to continue reading
9A96 PLA ; Restore A from stack
9A97 TAX ; Transfer to X
9A98 .imm_op_out_of_range←2← 9A6D BCC← 9A71 BCS
JMP nmi_error_dispatch ; Jump to discard handler
9A9B .imm_op_dispatch_lo
EQUB <(rx_imm_peek-1) ; Ctrl &81: PEEK
9A9C EQUB <(rx_imm_poke-1) ; Ctrl &82: POKE
9A9D EQUB <(rx_imm_exec-1) ; Ctrl &83: JSR
9A9E EQUB <(rx_imm_exec-1) ; Ctrl &84: UserProc
9A9F EQUB <(rx_imm_exec-1) ; Ctrl &85: OSProc
9AA0 EQUB <(rx_imm_halt_cont-1) ; Ctrl &86: HALT
9AA1 EQUB <(rx_imm_halt_cont-1) ; Ctrl &87: CONTINUE
9AA2 EQUB <(rx_imm_machine_type-1) ; Ctrl &88: machine type query

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

9AA3 .rx_imm_exec
LDA #0 ; A=0: port buffer lo at page boundary
9AA5 STA open_port_buf ; Set port buffer lo
9AA7 LDA #&82 ; Buffer length lo = &82
9AA9 STA port_buf_len ; Set buffer length lo
9AAB LDA #1 ; Buffer length hi = 1
9AAD STA port_buf_len_hi ; Set buffer length hi
9AAF LDA net_rx_ptr_hi ; Load RX page hi for buffer
9AB1 STA open_port_buf_hi ; Set port buffer hi
9AB3 LDY #3 ; Y=3: copy 4 bytes (3 down to 0)
9AB5 .copy_addr_loop←1← 9ABC BPL
LDA rx_remote_addr,y ; Load remote address byte
9AB8 STA l0d58,y ; Store to exec address workspace
9ABB DEY ; Next byte (descending)
9ABC BPL copy_addr_loop ; Loop until all 4 bytes copied
9ABE .sub_c9abe
JMP send_data_rx_ack ; Enter common data-receive path Svc 5 dispatch table low bytes

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

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

RX immediate: machine type query

Sets up a reply buffer (open_port_buf=&21, page &7F, length &01FC) for the machine type query response, then branches to set_tx_reply_flag. Returns system identification data to the remote station.

9ACC .rx_imm_machine_type
LDA #1 ; Buffer length hi = 1
9ACE STA port_buf_len_hi ; Set buffer length hi
9AD0 LDA #&fc ; Buffer length lo = &FC
9AD2 STA port_buf_len ; Set buffer length lo
9AD4 LDA #&21 ; Buffer start lo = &21
9AD6 STA open_port_buf ; Set port buffer lo
9AD8 LDA #&7f ; Buffer hi = &7F (below screen)
9ADA STA open_port_buf_hi ; Set port buffer hi
9ADC BNE set_tx_reply_flag ; ALWAYS branch

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.

9ADE .rx_imm_peek
LDA #&3d ; Port workspace offset = &3D
9AE0 STA port_ws_offset ; Store workspace offset lo
9AE2 LDA #&0d ; RX buffer page = &0D
9AE4 STA rx_buf_offset ; Store workspace offset hi
9AE6 LDA #2 ; Scout status = 2 (PEEK response)
9AE8 STA scout_status ; Store scout status
9AEB JSR tx_calc_transfer ; Calculate transfer size for response
9AEE BCC imm_op_discard ; C=0: transfer not set up, discard
9AF0 .set_tx_reply_flag←1← 9ADC BNE
LDA tx_flags ; Mark TX flags bit 7 (reply pending)
9AF3 ORA #&80 ; Set reply pending flag
9AF5 STA tx_flags ; Store updated TX flags
9AF8 .rx_imm_halt_cont
LDA #&44 ; CR1=&44: TIE | TX_LAST_DATA
9AFA STA econet_control1_or_status1 ; Write CR1: enable TX interrupts
9AFD .tx_cr2_setup
LDA #&a7 ; CR2=&A7: RTS|CLR_RX_ST|FC_TDRA|PSE
9AFF STA econet_control23_or_status2 ; Write CR2 for TX setup
9B02 .tx_nmi_setup
LDA #&1f ; NMI handler lo byte (self-modifying)
9B04 LDY #&9b ; Y=&9B: dispatch table page
9B06 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.

9B09 .imm_op_build_reply←1← 99C3 JMP
LDA port_buf_len ; Get buffer position for reply header
9B0B CLC ; Clear carry for offset addition
9B0C ADC #&80 ; Data offset = buf_len + &80 (past header)
9B0E LDY #&7f ; Y=&7F: reply data length slot
9B10 STA (net_rx_ptr),y ; Store reply data length in RX buffer
9B12 LDY #&80 ; Y=&80: source station slot
9B14 LDA rx_src_stn ; Load requesting station number
9B17 STA (net_rx_ptr),y ; Store source station in reply header
9B19 INY ; Y=&81
9B1A LDA rx_src_net ; Load requesting network number
9B1D STA (net_rx_ptr),y ; Store source network in reply header
9B1F LDA rx_ctrl ; Load control byte from received frame
9B22 STA tx_work_57 ; Save ctrl byte for TX response
9B25 LDA #&84 ; IER bit 2: disable SR interrupt
9B27 STA system_via_ier ; Write IER to disable SR
9B2A LDA system_via_acr ; Read ACR for shift register config
9B2D AND #&1c ; Isolate shift register mode bits (2-4)
9B2F STA tx_work_51 ; Save original SR mode for later restore
9B32 LDA system_via_acr ; Reload ACR for modification
9B35 AND #&e3 ; Clear SR mode bits (keep other bits)
9B37 ORA #8 ; SR mode 2: shift in under φ2
9B39 STA system_via_acr ; Apply new shift register mode
9B3C BIT system_via_sr ; Read SR to clear pending interrupt
9B3F .imm_op_discard←1← 9AEE BCC
JMP discard_listen ; Return to idle listen mode
9B42 EQUS "FO]i" ; Unreferenced data (reply tail bytes)
9B46 EQUB &80 ; Terminator byte (&80)

TX done: remote JSR execution

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

9B47 .tx_done_jsr
LDA #&9b ; Hi byte of tx_done_exit-1
9B49 PHA ; Push hi byte on stack
9B4A LDA #&88 ; Push lo of (tx_done_exit-1)
9B4C PHA ; Push lo byte on stack
9B4D JMP (l0d58) ; Call remote JSR; RTS to tx_done_exit ORA opcode (dead code / data overlap)

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.

9B50 .tx_done_user_proc
LDY #event_network_error ; Y=8: network event type
9B52 LDX l0d58 ; X = remote address lo
9B55 LDA l0d59 ; A = remote address hi
9B58 JSR oseven ; Generate event Y='Network error'
9B5B 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.

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

9B6A .tx_done_halt
LDA #4 ; A=&04: bit 2 mask for rx_flags
9B6C BIT rx_flags ; Test if already halted
9B6F BNE tx_done_exit ; Already halted: skip to exit
9B71 ORA rx_flags ; Set bit 2 in rx_flags
9B74 STA rx_flags ; Store halt flag
9B77 LDA #4 ; A=4: re-load halt bit mask
9B79 CLI ; Enable interrupts during halt wait
9B7A .halt_spin_loop←1← 9B7D BNE
BIT rx_flags ; Test halt flag
9B7D BNE halt_spin_loop ; Still halted: keep spinning
9B7F 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.

9B81 .tx_done_continue
LDA rx_flags ; Load current RX flags
9B84 AND #&fb ; Clear bit 2: release halted station
9B86 STA rx_flags ; Store updated flags
9B89 .tx_done_exit←4← 9B5B JMP← 9B67 JMP← 9B6F BNE← 9B7F BEQ
PLA ; Restore Y from stack
9B8A TAY ; Transfer to Y register
9B8B PLA ; Restore X from stack
9B8C TAX ; Transfer to X register
9B8D LDA #0 ; A=0: success status
9B8F 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.

9B90 .tx_begin←1← 06EB JMP
TXA ; Save X on stack
9B91 PHA ; Push X
9B92 LDY #2 ; Y=2: TXCB offset for dest station
9B94 LDA (nmi_tx_block),y ; Load dest station from TX control block
9B96 STA tx_dst_stn ; Store to TX scout buffer
9B99 INY ; Y=&03
9B9A LDA (nmi_tx_block),y ; Load dest network from TX control block
9B9C STA tx_dst_net ; Store to TX scout buffer
9B9F LDY #0 ; Y=0: first byte of TX control block
9BA1 LDA (nmi_tx_block),y ; Load control/flag byte
9BA3 BMI tx_imm_op_setup ; Bit7 set: immediate operation ctrl byte
9BA5 JMP tx_active_start ; Bit7 clear: normal data transfer
9BA8 .tx_imm_op_setup←1← 9BA3 BMI
STA tx_ctrl_byte ; Store control byte to TX scout buffer
9BAB TAX ; X = control byte for range checks
9BAC INY ; Y=1: port byte offset
9BAD LDA (nmi_tx_block),y ; Load port byte from TX control block
9BAF STA tx_port ; Store port byte to TX scout buffer
9BB2 BNE tx_line_idle_check ; Port != 0: skip immediate op setup
9BB4 CPX #&83 ; Ctrl < &83: PEEK/POKE need address calc
9BB6 BCS tx_ctrl_range_check ; Ctrl >= &83: skip to range check
9BB8 SEC ; SEC: init borrow for 4-byte subtract
9BB9 PHP ; Save carry on stack for loop
9BBA LDY #8 ; Y=8: high pointer offset in TXCB
9BBC .calc_peek_poke_size←1← 9BD0 BCC
LDA (nmi_tx_block),y ; Load TXCB[Y] (end addr byte)
9BBE DEY ; Y -= 4: back to start addr offset
9BBF DEY ; (continued)
9BC0 DEY ; (continued)
9BC1 DEY ; (continued)
9BC2 PLP ; Restore borrow from stack
9BC3 SBC (nmi_tx_block),y ; end - start = transfer size byte
9BC5 STA tx_data_start,y ; Store result to tx_data_start
9BC8 INY ; Y += 5: advance to next end byte
9BC9 INY ; (continued)
9BCA INY ; (continued)
9BCB INY ; (continued)
9BCC INY ; (continued)
9BCD PHP ; Save borrow for next byte
9BCE CPY #&0c ; Done all 4 bytes? (Y reaches &0C)
9BD0 BCC calc_peek_poke_size ; No: next byte pair
9BD2 PLP ; Discard final borrow
9BD3 .tx_ctrl_range_check←1← 9BB6 BCS
CPX #&81 ; Ctrl < &81: not an immediate op
9BD5 BCC tx_active_start ; Below range: normal data transfer
9BD7 .check_imm_range
CPX #&89 ; Ctrl >= &89: out of immediate range
9BD9 BCS tx_active_start ; Above range: normal data transfer
9BDB LDY #&0c ; Y=&0C: start of extra data in TXCB
9BDD .copy_imm_params←1← 9BE5 BCC
LDA (nmi_tx_block),y ; Load extra parameter byte from TXCB
9BDF STA nmi_shim_1a,y ; Copy to NMI shim workspace at &0D1A+Y
9BE2 INY ; Next byte
9BE3 CPY #&10 ; Done 4 bytes? (Y reaches &10)
9BE5 BCC copy_imm_params ; No: continue copying
9BE7 .tx_line_idle_check←1← 9BB2 BNE
LDA #&20 ; A=&20: mask for SR2 INACTIVE bit
9BE9 BIT econet_control23_or_status2 ; BIT SR2: test if line is idle
9BEC BNE tx_no_clock_error ; Line not idle: handle as line jammed
9BEE LDA #&fd ; A=&FD: high byte of timeout counter
9BF0 PHA ; Push timeout high byte to stack
9BF1 LDA #6 ; Scout frame = 6 address+ctrl bytes
9BF3 STA tx_length ; Store scout frame length
9BF6 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 &9C18-&9C1D works because CR2=&67 has RTS=0, so cts_input_ is always true, and SR1_CTS reflects presence of clock hardware.

9BF8 .inactive_poll
STA tx_index ; Save TX index
9BFB PHA ; Push timeout byte 1 on stack
9BFC PHA ; Push timeout byte 2 on stack
9BFD LDY #&e7 ; Y=&E7: CR2 value for TX prep (RTS|CLR_TX_ST|CLR_RX_ST|FC_TDRA|2_1_BYTE| PSE)
9BFF .test_inactive_retry←3← 9C2C BNE← 9C31 BNE← 9C36 BNE
PHP ; A=&04: INACTIVE mask for SR2 bit2
9C00 SEI ; Disable interrupts for ADLC access
9C01 LDA #&40 ; A=&40: BIT &FE18 becomes RTI (disable NMI)
9C03 STA l0d1c ; Self-modify NMI shim at &0D1C: disable
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.

9C06 .intoff_test_inactive
BIT station_id_disable_net_nmis ; INTOFF -- disable NMIs INTOFF again (belt-and-braces)
9C09 LDA #4 ; A=4: INACTIVE mask for SR2 bit 2
9C0B .test_line_idle
BIT econet_control23_or_status2 ; BIT SR2: Z = &04 AND SR2 -- tests INACTIVE
9C0E BEQ c9c1f ; INACTIVE not set -- re-enable NMIs and loop
9C10 LDA econet_control1_or_status1 ; Read SR1 (acknowledge pending interrupt)
9C13 LDA #&67 ; CR2=&67: CLR_TX_ST|CLR_RX_ST|FC_TDRA|2_1_BYTE|PSE
9C15 .c9c15←1← 9C91 LDA
STA econet_control23_or_status2 ; Write CR2: clear status, prepare TX
9C18 LDA #&10 ; A=&10: CTS mask for SR1 bit4
9C1A BIT econet_control1_or_status1 ; BIT SR1: tests CTS present
9C1D BNE c9c58 ; CTS set -- clock hardware detected, start TX
9C1F .c9c1f←1← 9C0E BEQ
LDA #&2c ; A=&2C: BIT opcode (re-enable NMI processing)
9C21 STA l0d1c ; Self-modify NMI shim at &0D1C: enable
9C24 .inactive_retry
BIT video_ula_control ; INTON -- re-enable NMIs (&FE20 read)
9C27 PLP ; Restore interrupt state
9C28 TSX ; 3-byte timeout counter on stack
9C29 INC error_text,x ; Increment timeout counter byte 1
9C2C BNE test_inactive_retry ; Not overflowed: retry INACTIVE test
9C2E INC stk_timeout_mid,x ; Increment timeout counter byte 2
9C31 BNE test_inactive_retry ; Not overflowed: retry INACTIVE test
9C33 INC stk_frame_3,x ; Increment timeout counter byte 3
9C36 BNE test_inactive_retry ; Not overflowed: retry INACTIVE test
9C38 BEQ tx_line_jammed ; ALWAYS branch
; TX_ACTIVE branch (A=&44 = CR1 value for TX active)
9C3A .tx_active_start←3← 9BA5 JMP← 9BD5 BCC← 9BD9 BCS
LDA #&44 ; CR1=&44: TIE | TX_LAST_DATA
9C3C 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.

9C3E .tx_line_jammed←1← 9C38 BEQ
LDA #7 ; CR2=&07: FC_TDRA | 2_1_BYTE | PSE (abort TX)
9C40 STA econet_control23_or_status2 ; Write CR2 to abort TX
9C43 PLA ; Clean 3 bytes of timeout loop state
9C44 PLA ; Pop saved register
9C45 PLA ; Pop saved register
9C46 LDA #&40 ; Error &40 = 'Line Jammed'
9C48 BNE store_tx_error ; ALWAYS branch to shared error handler ALWAYS branch
9C4A .tx_no_clock_error←1← 9BEC BNE
LDA #&43 ; Error &43 = 'No Clock'
9C4C .store_tx_error←2← 9C3C BNE← 9C48 BNE
LDY #0 ; Offset 0 = error byte in TX control block
9C4E STA (nmi_tx_block),y ; Store error code in TX CB byte 0
9C50 LDA #&80 ; &80 = TX complete flag
9C52 STA tx_clear_flag ; Signal TX operation complete
9C55 PLA ; Restore X saved by caller
9C56 TAX ; Move to X register
9C57 RTS ; Return to TX caller
9C58 .c9c58←1← 9C1D BNE
LDX #&c0 ; X=&C0: CR1 = AC | RX_RESET
9C5A STX econet_control1_or_status1 ; Write CR1: reset RX before TX (new in 3.65)
fall through ↓

TX preparation

Configures ADLC for transmission: asserts RTS via CR2, enables TIE via CR1, installs NMI TX handler at &9CFF (nmi_tx_data), and re-enables NMIs. For port-0 (immediate) operations, dispatches via a lookup table indexed by control byte to set tx_flags, tx_length, and a per-operation handler. For port non-zero, branches to c9c8e for standard data transfer setup.

9C5D .tx_prepare
STY econet_control23_or_status2 ; Write CR2 = Y (&E7: RTS|CLR_TX_ST|CLR_RX_ST| FC_TDRA|2_1_BYTE|PSE)
9C60 LDX #&44 ; CR1=&44: RX_RESET | TIE (TX active, TX interrupts enabled)
9C62 STX econet_control1_or_status1 ; Write to ADLC CR1
9C65 LDX #&ff ; Install NMI handler at &9D4C (TX data handler)
9C67 LDY #&9c ; High byte of NMI handler address
9C69 STX nmi_jmp_lo ; Write NMI vector low byte directly
9C6C STY nmi_jmp_hi ; Write NMI vector high byte directly
9C6F SEC ; Set need_release_tube flag (SEC/ROR = bit7)
9C70 ROR need_release_tube ; Rotate carry into bit 7 of flag
9C72 LDA #&2c ; A=&2C: BIT opcode (re-enable NMI processing)
9C74 STA l0d1c ; Self-modify NMI shim at &0D1C: enable
9C77 BIT video_ula_control ; INTON -- NMIs now fire for TDRA (&FE20 read)
9C7A LDA tx_port ; Load destination port number
9C7D BNE setup_data_xfer ; Port != 0: standard data transfer
9C7F LDY tx_ctrl_byte ; Port 0: load control byte for table lookup
9C82 LDA tube_tx_sr1_operand,y ; Look up tx_flags from table
9C85 STA tx_flags ; Store operation flags
9C88 LDA tube_tx_inc_operand,y ; Look up tx_length from table
9C8B STA tx_length ; Store expected transfer length
9C8E LDA #&9c ; Push high byte of return address (&9C)
9C90 PHA ; Push high byte for PHA/PHA/RTS dispatch
9C91 LDA c9c15,y ; Look up handler address low from table
9C94 PHA ; Push low byte for PHA/PHA/RTS dispatch
9C95 RTS ; RTS dispatches to control-byte handler
9C96 EQUB &A1, &A5, &E7, &E7, ; Control byte → CR2 value lookup &E7, &F7, &F7, &9D ; table
9C9E .imm_op_status3
EQUB &A9, &03, &D0, &48 ; A=3: scout_status for PEEK

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.

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

9CA6 .tx_ctrl_poke
LDA #2 ; Scout status = 2 (POKE transfer)
9CA8 .store_status_add4←1← 9CA4 BNE
STA scout_status ; Store scout status
9CAB CLC ; Clear carry for 4-byte addition
9CAC PHP ; Save carry on stack
9CAD LDY #&0c ; Y=&0C: start at offset 12
9CAF .add_bytes_loop←1← 9CBC BCC
LDA l0d1e,y ; Load workspace address byte
9CB2 PLP ; Restore carry from previous byte
9CB3 ADC (nmi_tx_block),y ; Add TXCB address byte
9CB5 STA l0d1e,y ; Store updated address byte
9CB8 INY ; Next byte
9CB9 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.

9CBA .tx_ctrl_proc
CPY #&10 ; Compare Y with 16-byte boundary
9CBC BCC add_bytes_loop ; Below boundary: continue addition
9CBE PLP ; Restore processor flags
9CBF BNE skip_buf_setup ; Skip buffer setup if transfer size is zero
9CC1 .setup_data_xfer←1← 9C7D BNE
LDA tx_dst_stn ; Load dest station for broadcast check
9CC4 AND tx_dst_net ; AND with dest network
9CC7 CMP #&ff ; Both &FF = broadcast address?
9CC9 BNE setup_unicast_xfer ; Not broadcast: unicast path
9CCB LDA #&0e ; Broadcast scout: 14 bytes total
9CCD STA tx_length ; Store broadcast scout length
9CD0 LDA #&40 ; A=&40: broadcast flag
9CD2 STA tx_flags ; Set broadcast flag in tx_flags
9CD5 LDY #4 ; Y=4: start of address data in TXCB
9CD7 .copy_bcast_addr←1← 9CDF BCC
LDA (nmi_tx_block),y ; Copy TXCB address bytes to scout buffer
9CD9 STA tx_src_stn,y ; Store to TX source/data area
9CDC INY ; Next byte
9CDD CPY #&0c ; Done 8 bytes? (Y reaches &0C)
9CDF BCC copy_bcast_addr ; No: continue copying
9CE1 BCS tx_ctrl_exit ; ALWAYS branch
9CE3 .setup_unicast_xfer←1← 9CC9 BNE
LDA #0 ; A=0: clear flags for unicast
9CE5 STA tx_flags ; Clear tx_flags
9CE8 .proc_op_status2
LDA #2 ; scout_status=2: data transfer pending
9CEA .store_status_copy_ptr
STA scout_status ; Store scout status
9CED .skip_buf_setup←1← 9CBF BNE
LDA nmi_tx_block ; Copy TX block pointer to workspace ptr
9CEF STA port_ws_offset ; Store low byte
9CF1 LDA nmi_tx_block_hi ; Copy TX block pointer high byte
9CF3 STA rx_buf_offset ; Store high byte
9CF5 JSR tx_calc_transfer ; Calculate transfer size from RXCB
9CF8 .tx_ctrl_exit←1← 9CE1 BCS
PLP ; Restore processor status from stack
9CF9 PLA ; Restore stacked registers (4 PLAs)
9CFA PLA ; Second PLA
9CFB PLA ; Third PLA
9CFC PLA ; Fourth PLA
9CFD TAX ; Restore X from A
9CFE 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.

9CFF .nmi_tx_data
LDY tx_index ; Load TX buffer index
9D02 BIT econet_control1_or_status1 ; BIT SR1: V=bit6(TDRA), N=bit7(IRQ)
9D05 .tx_fifo_write←1← 9D20 BMI
BVC tx_fifo_not_ready ; TDRA not set -- TX error
9D07 LDA tx_dst_stn,y ; Load byte from TX buffer
9D0A STA econet_data_continue_frame ; Write to TX_DATA (continue frame)
9D0D INY ; Next TX buffer byte
9D0E LDA tx_dst_stn,y ; Load second byte from TX buffer
9D11 INY ; Advance TX index past second byte
9D12 STY tx_index ; Save updated TX buffer index
9D15 STA econet_data_continue_frame ; Write second byte to TX_DATA
9D18 CPY tx_length ; Compare index to TX length
9D1B BCS tx_last_data ; Frame complete -- go to TX_LAST_DATA
9D1D BIT econet_control1_or_status1 ; Check if we can send another pair
9D20 BMI tx_fifo_write ; IRQ set -- send 2 more bytes (tight loop)
9D22 JMP nmi_rti ; RTI -- wait for next NMI
; TX error path
9D25 .tx_error←1← 9D68 BEQ
LDA #&42 ; Error &42
9D27 BNE tx_store_error ; ALWAYS branch
9D29 .tx_fifo_not_ready←1← 9D05 BVC
LDA #&67 ; CR2=&67: clear status, return to listen
9D2B STA econet_control23_or_status2 ; Write CR2: clear status, idle listen
9D2E LDA #&41 ; Error &41 (TDRA not ready)
9D30 .tx_store_error←1← 9D27 BNE
LDY station_id_disable_net_nmis ; INTOFF (also loads station ID)
9D33 .delay_nmi_disable←1← 9D36 BNE
PHA ; PHA/PLA delay loop (256 iterations for NMI disable)
9D34 PLA ; PHA/PLA delay (~7 cycles each)
9D35 INY ; Increment delay counter
9D36 BNE delay_nmi_disable ; Loop 256 times for NMI disable
9D38 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 &9D47 (nmi_tx_complete). 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)

9D3B .tx_last_data←1← 9D1B BCS
LDA #&3f ; CR2=&3F: TX_LAST_DATA | CLR_RX_ST | FLAG_IDLE | FC_TDRA | 2_1_BYTE | PSE
9D3D STA econet_control23_or_status2 ; Write to ADLC CR2
9D40 LDA #&47 ; Install NMI handler at &9D47 (TX completion)
9D42 LDY #&9d ; High byte of handler address
9D44 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 -> tx_result_ok at &9EDB - bit0 set at &0D4A -> handshake_await_ack at &9E83 - Otherwise -> install nmi_reply_scout at &9D63

9D47 .nmi_tx_complete
LDA #&82 ; CR1=&82: TX_RESET | RIE (now in RX mode)
9D49 STA econet_control1_or_status1 ; Write CR1 to switch from TX to RX
9D4C BIT tx_flags ; Test workspace flags
9D4F BVC check_handshake_bit ; bit6 not set -- check bit0
9D51 JMP tx_result_ok ; bit6 set -- TX completion
9D54 .check_handshake_bit←1← 9D4F BVC
LDA #1 ; A=1: mask for bit0 test
9D56 BIT tx_flags ; Test tx_flags bit0 (handshake)
9D59 BEQ install_reply_scout ; bit0 clear: install reply handler
9D5B JMP handshake_await_ack ; bit0 set -- four-way handshake data phase
9D5E .install_reply_scout←1← 9D59 BEQ
LDA #&63 ; Install RX reply handler at &9D63
9D60 JMP install_nmi_handler ; 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).

9D63 .nmi_reply_scout
LDA #1 ; A=&01: AP mask for SR2
9D65 BIT econet_control23_or_status2 ; BIT SR2: test AP (Address Present)
9D68 BEQ tx_error ; No AP -- error
9D6A LDA econet_data_continue_frame ; Read first RX byte (destination station)
9D6D CMP station_id_disable_net_nmis ; Compare to our station ID (INTOFF side effect)
9D70 BNE reject_reply ; Not our station -- error/reject
9D72 LDA #&77 ; Install next handler at &9D77 (reply continuation)
9D74 JMP install_nmi_handler ; 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 nmi_reply_validate (&9D8E) for the remaining two bytes (source station and network). Optimisation: checks SR1 bit7 (IRQ still asserted) via BMI at &9D86. If IRQ is still set, falls through directly to &9D8E without an RTI, avoiding NMI re-entry overhead for short frames where all bytes arrive in quick succession.

9D77 .nmi_reply_cont
BIT econet_control23_or_status2 ; BIT SR2: test for RDA (bit7 = data available)
9D7A BPL reject_reply ; No RDA -- error
9D7C LDA econet_data_continue_frame ; Read destination network byte
9D7F BNE reject_reply ; Non-zero -- network mismatch, error
9D81 LDA #&8e ; Install next handler at &9DE3 (reply validation)
9D83 BIT econet_control1_or_status1 ; BIT SR1: test IRQ (N=bit7) -- more data ready?
9D86 BMI nmi_reply_validate ; IRQ set: validate reply immediately
9D88 JMP install_nmi_handler ; IRQ not set -- install handler and RTI
9D8B .reject_reply←7← 9D70 BNE← 9D7A BPL← 9D7F BNE← 9D91 BPL← 9D99 BNE← 9DA1 BNE← 9DA8 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 &9D8E -- must see data available 2. Read source station at &9D93, compare to &0D20 (tx_dst_stn) 3. Read source network at &9D9B, compare to &0D21 (tx_dst_net) 4. Check SR2 bit1 (FV) at &9DA5 -- 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).

9D8E .nmi_reply_validate←1← 9D86 BMI
BIT econet_control23_or_status2 ; BIT SR2: test RDA (bit7). Must be set for valid reply.
9D91 BPL reject_reply ; No RDA -- error (FV masking RDA via PSE would cause this)
9D93 LDA econet_data_continue_frame ; Read source station
9D96 CMP tx_dst_stn ; Compare to original TX destination station (&0D20)
9D99 BNE reject_reply ; Mismatch -- not the expected reply, error
9D9B LDA econet_data_continue_frame ; Read source network
9D9E CMP tx_dst_net ; Compare to original TX destination network (&0D21)
9DA1 BNE reject_reply ; Mismatch -- error
9DA3 LDA #2 ; A=&02: FV mask for SR2 bit1
9DA5 BIT econet_control23_or_status2 ; BIT SR2: test FV -- frame must be complete
9DA8 BEQ reject_reply ; No FV -- incomplete frame, error
9DAA LDA #&a7 ; CR2=&A7: RTS|CLR_TX_ST|FC_TDRA|2_ 1_BYTE|PSE (TX in handshake)
9DAC STA econet_control23_or_status2 ; Write CR2: enable RTS for TX handshake
9DAF LDA #&44 ; CR1=&44: RX_RESET | TIE (TX active for scout ACK)
9DB1 STA econet_control1_or_status1 ; Write CR1: reset RX, enable TX interrupt
9DB4 LDA #&83 ; Install next handler at &9E83 into &0D4B/&0D4C
9DB6 LDY #&9e ; High byte &9E of next handler address
9DB8 STA nmi_next_lo ; Store low byte to nmi_next_lo
9DBB STY nmi_next_hi ; Store high byte to nmi_next_hi
9DBE LDA tx_dst_stn ; Load dest station for scout ACK TX
9DC1 BIT econet_control1_or_status1 ; BIT SR1: test TDRA (V=bit6)
9DC4 BVC data_tx_check_fifo ; TDRA not ready -- error
9DC6 STA econet_data_continue_frame ; Write dest station to TX FIFO
9DC9 LDA tx_dst_net ; Load dest network for scout ACK TX
9DCC STA econet_data_continue_frame ; Write dest network to TX FIFO
9DCF LDA #&d6 ; Install handler at &9DD6 (write src addr for scout ACK)
9DD1 LDY #&9d ; High byte &9D of handler address
9DD3 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.

9DD6 .nmi_scout_ack_src
LDA station_id_disable_net_nmis ; Load our station ID (also INTOFF)
9DD9 BIT econet_control1_or_status1 ; BIT SR1: check TDRA before writing
9DDC BVC data_tx_check_fifo ; TDRA not ready: TX error
9DDE STA econet_data_continue_frame ; Write our station to TX FIFO
9DE1 LDA #0 ; Network = 0 (local network)
9DE3 STA econet_data_continue_frame ; Write network byte to TX FIFO
9DE6 .data_tx_begin←1← 996A JMP
LDA #2 ; Test bit 1 of tx_flags
9DE8 BIT tx_flags ; Check if immediate-op or data-transfer
9DEB BNE install_imm_data_nmi ; Bit 1 set: immediate op, use alt handler
9DED LDA #&fb ; Install nmi_data_tx at &9DFB
9DEF LDY #&9d ; High byte of handler address
9DF1 JMP set_nmi_vector ; Install and return via set_nmi_vector
9DF4 .install_imm_data_nmi←1← 9DEB BNE
LDA #&42 ; Install nmi_imm_data at &9E42
9DF6 LDY #&9e ; High byte of handler address
9DF8 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 &9CFF but reads from the port buffer instead of the TX workspace. Writes two bytes per iteration, checking SR1 IRQ between pairs for tight looping.

9DFB .nmi_data_tx
LDY port_buf_len ; Y = buffer offset, resume from last position
9DFD BIT econet_control1_or_status1 ; BIT SR1: test TDRA (V=bit6)
9E00 .data_tx_check_fifo←3← 9DC4 BVC← 9DDC BVC← 9E23 BMI
BVC tx_tdra_error ; TDRA not ready -- error
9E02 LDA (open_port_buf),y ; Write data byte to TX FIFO
9E04 STA econet_data_continue_frame ; Write first byte of pair to FIFO
9E07 INY ; Advance buffer offset
9E08 BNE write_second_tx_byte ; No page crossing
9E0A DEC port_buf_len_hi ; Page crossing: decrement page count
9E0C BEQ data_tx_last ; No pages left: send last data
9E0E INC open_port_buf_hi ; Increment buffer high byte
9E10 .write_second_tx_byte←1← 9E08 BNE
LDA (open_port_buf),y ; Load second byte of pair
9E12 STA econet_data_continue_frame ; Write second byte to FIFO
9E15 INY ; Advance buffer offset
9E16 STY port_buf_len ; Save updated buffer position
9E18 BNE check_irq_loop ; No page crossing
9E1A DEC port_buf_len_hi ; Page crossing: decrement page count
9E1C BEQ data_tx_last ; No pages left: send last data
9E1E INC open_port_buf_hi ; Increment buffer high byte
9E20 .check_irq_loop←1← 9E18 BNE
BIT econet_control1_or_status1 ; BIT SR1: test IRQ (N=bit7) for tight loop
9E23 BMI data_tx_check_fifo ; IRQ still set: write 2 more bytes
9E25 JMP nmi_rti ; No IRQ: return, wait for next NMI
9E28 .data_tx_last←4← 9E0C BEQ← 9E1C BEQ← 9E5B BEQ← 9E71 BEQ
LDA #&3f ; CR2=&3F: TX_LAST_DATA (close data frame)
9E2A STA econet_control23_or_status2 ; Write CR2 to close frame
9E2D LDA tx_flags ; Check tx_flags for next action
9E30 BPL data_tx_error ; Bit7 clear: error, install saved handler
9E32 LDA #&fd ; Install discard_reset_listen at &99DB
9E34 LDY #&99 ; High byte of &99DB handler
9E36 JMP set_nmi_vector ; Set NMI vector and return
9E39 .data_tx_error←1← 9E30 BPL
.install_saved_handler←1← 9E30 BPL
LDA nmi_next_lo ; Load saved next handler low byte
9E3C LDY nmi_next_hi ; Load saved next handler high byte
9E3F JMP set_nmi_vector ; Install saved handler and return
9E42 .nmi_data_tx_tube
BIT econet_control1_or_status1 ; Tube TX: BIT SR1 test TDRA
9E45 .tube_tx_fifo_write←1← 9E76 BMI
BVC tx_tdra_error ; TDRA not ready -- error
9E47 LDA tube_data_register_3 ; Read byte from Tube R3
9E4A STA econet_data_continue_frame ; Write to TX FIFO
9E4D INC port_buf_len ; Increment 4-byte buffer counter
9E4F BNE write_second_tube_byte ; Low byte didn't wrap
9E51 INC port_buf_len_hi ; Carry into second byte
9E53 BNE write_second_tube_byte ; No further carry
9E55 INC open_port_buf ; Carry into third byte
9E57 BNE write_second_tube_byte ; No further carry
9E59 INC open_port_buf_hi ; Carry into fourth byte
9E5B BEQ data_tx_last ; Counter wrapped to zero: last data
9E5D .write_second_tube_byte←3← 9E4F BNE← 9E53 BNE← 9E57 BNE
LDA tube_data_register_3 ; Read second Tube byte from R3
9E60 STA econet_data_continue_frame ; Write second byte to TX FIFO
9E63 INC port_buf_len ; Increment 4-byte counter (second byte)
9E65 BNE check_tube_irq_loop ; Low byte didn't wrap
9E67 .tube_tx_inc_byte2
INC port_buf_len_hi ; Carry into second byte
9E69 BNE check_tube_irq_loop ; No further carry
9E6B .tube_tx_inc_byte3
INC open_port_buf ; Carry into third byte
9E6D BNE check_tube_irq_loop ; No further carry
9E6F .tube_tx_inc_byte4
INC open_port_buf_hi ; Carry into fourth byte
9E71 BEQ data_tx_last ; Counter wrapped to zero: last data
9E73 .check_tube_irq_loop←3← 9E65 BNE← 9E69 BNE← 9E6D BNE
BIT econet_control1_or_status1 ; BIT SR1: test IRQ for tight loop
9E76 BMI tube_tx_fifo_write ; IRQ still set: write 2 more bytes
9E78 JMP nmi_rti ; No IRQ: return, wait for next NMI
9E7B .tx_tdra_error←2← 9E00 BVC← 9E45 BVC
LDA tx_flags ; TX error: check flags for path
9E7E BPL tx_result_fail ; Bit7 clear: TX result = not listening
9E80 JMP discard_reset_listen ; Bit7 set: discard and return to listen

Four-way handshake: switch to RX for final ACK

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

9E83 .handshake_await_ack←1← 9D5B JMP
LDA #&82 ; CR1=&82: TX_RESET | RIE (switch to RX for final ACK)
9E85 STA econet_control1_or_status1 ; Write to ADLC CR1
9E88 LDA #&8f ; Install handler at &9E8F (RX final ACK)
9E8A LDY #&9e ; High byte of handler address
9E8C 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 (&9D63-&9D8E): &9E8F: Check AP, read dest_stn, compare to our station &9EA3: Check RDA, read dest_net, validate = 0 &9EB7: Check RDA, read src_stn/net, compare to TX dest &9ED6: Check FV for frame completion On success, stores result=0 at tx_result_ok. On failure, error &41.

9E8F .nmi_final_ack
LDA #1 ; A=&01: AP mask
9E91 BIT econet_control23_or_status2 ; BIT SR2: test AP
9E94 BEQ tx_result_fail ; No AP -- error
9E96 LDA econet_data_continue_frame ; Read dest station
9E99 CMP station_id_disable_net_nmis ; Compare to our station (INTOFF side effect)
9E9C BNE tx_result_fail ; Not our station -- error
9E9E LDA #&a3 ; Install handler at &9EFF (final ACK continuation)
9EA0 JMP install_nmi_handler ; Install continuation handler
9EA3 .nmi_final_ack_net
BIT econet_control23_or_status2 ; BIT SR2: test RDA
9EA6 BPL tx_result_fail ; No RDA -- error
9EA8 LDA econet_data_continue_frame ; Read dest network
9EAB BNE tx_result_fail ; Non-zero -- network mismatch, error
9EAD LDA #&b7 ; Install handler at &9F15 (final ACK validation)
9EAF BIT econet_control1_or_status1 ; BIT SR1: test IRQ -- more data ready?
9EB2 BMI nmi_final_ack_validate ; IRQ set: validate final ACK immediately
9EB4 JMP install_nmi_handler ; 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.

9EB7 .nmi_final_ack_validate←1← 9EB2 BMI
BIT econet_control23_or_status2 ; BIT SR2: test RDA
9EBA BPL tx_result_fail ; No RDA -- error
9EBC LDA econet_data_continue_frame ; Read source station
9EBF CMP tx_dst_stn ; Compare to TX dest station (&0D20)
9EC2 BNE tx_result_fail ; Mismatch -- error
9EC4 LDA econet_data_continue_frame ; Read source network
9EC7 CMP tx_dst_net ; Compare to TX dest network (&0D21)
9ECA BNE tx_result_fail ; Mismatch -- error
9ECC LDA tx_flags ; Load TX flags for next action
9ECF BPL check_fv_final_ack ; bit7 clear: no data phase
9ED1 JMP install_data_rx_handler ; Install data RX handler
9ED4 .check_fv_final_ack←1← 9ECF BPL
LDA #2 ; A=&02: FV mask for SR2 bit1
9ED6 BIT econet_control23_or_status2 ; BIT SR2: test FV -- frame must be complete
9ED9 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 discard_reset_listen to return to idle.

9EDB .tx_result_ok←2← 9918 JMP← 9D51 JMP
LDA #0 ; A=0: success result code
9EDD 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.

9EDF .tx_result_fail←11← 985C JMP← 9D8B JMP← 9E7E BPL← 9E94 BEQ← 9E9C BNE← 9EA6 BPL← 9EAB BNE← 9EBA BPL← 9EC2 BNE← 9ECA BNE← 9ED9 BEQ
LDA #&41 ; A=&41: not listening error code
fall through ↓

TX result store and completion

Stores result code (A) into the TX control block at (nmi_tx_block),0 and sets bit 7 of &0D3A to signal completion. Returns to idle via discard_reset_listen. Reached from tx_result_ok (A=0, success), tx_result_fail (A=&41, not listening), and directly with other codes (A=&40 line jammed, A=&42 net error).

9EE1 .tx_store_result←2← 9D38 JMP← 9EDD BEQ
LDY #0 ; Y=0: index into TX control block
9EE3 STA (nmi_tx_block),y ; Store result/error code at (nmi_tx_block),0
9EE5 LDA #&80 ; &80: completion flag for &0D3A
9EE7 STA tx_clear_flag ; Signal TX complete
9EEA JMP discard_reset_listen ; Full ADLC reset and return to idle listen
; Unreferenced data block (purpose unknown)
9EED EQUB &0E, &0E, &0A, &0A, &0A, &06, ; Unreferenced data &06, &0A, &81, &00, &00, &00, ; block (purpose &00, &01, &01, &81 ; unknown)

Calculate transfer size

Computes the number of bytes actually transferred during a data frame reception by subtracting RXCB[8..11] (start address) from RXCB[4..7] (current pointer), giving the byte count. Two paths: the main path performs a 4-byte subtraction for Tube transfers, storing results to port_buf_len..open_port_buf_hi (&A2-&A5). The fallback path (no Tube or buffer addr = &FFFF) does a 2-byte subtraction using open_port_buf/open_port_buf_hi (&A4/&A5) as scratch. Both paths clobber &A4/&A5 as a side effect of the result area overlapping open_port_buf.

On ExitC1 if transfer set up, 0 if not
Xpreserved
9EFD .tx_calc_transfer←3← 97DE JSR← 9AEB JSR← 9CF5 JSR
LDY #6 ; Load RXCB[6] (buffer addr byte 2)
9EFF LDA (port_ws_offset),y ; Load workspace byte at offset Y
9F01 INY ; Y=&07
9F02 AND (port_ws_offset),y ; AND with TX block[7] (byte 3)
9F04 CMP #&ff ; Both &FF = no buffer?
9F06 BEQ fallback_calc_transfer ; Yes: fallback path
9F08 LDA tube_flag ; Tube transfer in progress?
9F0B BEQ fallback_calc_transfer ; No: fallback path
9F0D LDA tx_flags ; Load TX flags for transfer setup
9F10 ORA #2 ; Set bit 1 (transfer complete)
9F12 STA tx_flags ; Store with bit 1 set (Tube xfer)
9F15 SEC ; Init borrow for 4-byte subtract
9F16 PHP ; Save carry on stack
9F17 LDY #4 ; Y=4: start at RXCB offset 4
9F19 .calc_transfer_size←1← 9F2B BCC
LDA (port_ws_offset),y ; Load RXCB[Y] (current ptr byte)
9F1B INY ; Y += 4: advance to RXCB[Y+4]
9F1C INY ; (continued)
9F1D INY ; (continued)
9F1E INY ; (continued)
9F1F PLP ; Restore borrow from previous byte
9F20 SBC (port_ws_offset),y ; Subtract RXCB[Y+4] (start ptr byte)
9F22 STA net_tx_ptr,y ; Store result byte
9F25 DEY ; Y -= 3: next source byte
9F26 DEY ; (continued)
9F27 DEY ; (continued)
9F28 PHP ; Save borrow for next byte
9F29 CPY #8 ; Done all 4 bytes?
9F2B BCC calc_transfer_size ; No: next byte pair
9F2D PLP ; Discard final borrow
9F2E TXA ; A = saved X
9F2F PHA ; Save X
9F30 LDA #4 ; Compute address of RXCB+4
9F32 CLC ; CLC for base pointer addition
9F33 ADC port_ws_offset ; Add RXCB base to get RXCB+4 addr
9F35 TAX ; X = low byte of RXCB+4
9F36 LDY rx_buf_offset ; Y = high byte of RXCB ptr
9F38 LDA #&c2 ; Tube claim type &C2
9F3A JSR tube_addr_claim ; Claim Tube transfer address
9F3D BCC restore_x_and_return ; No Tube: skip reclaim
9F3F LDA scout_status ; Tube: reclaim with scout status
9F42 JSR tube_addr_claim ; Reclaim with scout status type
9F45 JSR release_tube ; Release Tube claim after reclaim
9F48 SEC ; C=1: Tube address claimed
9F49 .restore_x_and_return←1← 9F3D BCC
PLA ; Restore X
9F4A TAX ; Restore X from stack
9F4B RTS ; Return with C = transfer status
9F4C .fallback_calc_transfer←2← 9F06 BEQ← 9F0B BEQ
LDY #4 ; Y=4: RXCB current pointer offset
9F4E LDA (port_ws_offset),y ; Load RXCB[4] (current ptr lo)
9F50 LDY #8 ; Y=8: RXCB start address offset
9F52 SEC ; Set carry for subtraction
9F53 SBC (port_ws_offset),y ; Subtract RXCB[8] (start ptr lo)
9F55 STA port_buf_len ; Store transfer size lo
9F57 LDY #5 ; Y=5: current ptr hi offset
9F59 LDA (port_ws_offset),y ; Load RXCB[5] (current ptr hi)
9F5B SBC #0 ; Propagate borrow from lo subtraction
9F5D STA open_port_buf_hi ; Temp store adjusted current ptr hi
9F5F LDY #8 ; Y=8: start address lo offset
9F61 LDA (port_ws_offset),y ; Copy RXCB[8] to open port buffer lo
9F63 STA open_port_buf ; Store to scratch (side effect)
9F65 LDY #9 ; Y=9: start address hi offset
9F67 LDA (port_ws_offset),y ; Load RXCB[9] (start ptr hi)
9F69 SEC ; Set carry for subtraction
9F6A SBC open_port_buf_hi ; start_hi - adjusted current_hi
9F6C STA port_buf_len_hi ; Store transfer size hi
9F6E SEC ; Return with C=1
9F6F .nmi_shim_rom_src
RTS ; Return with C=1 (success)

ADLC full reset

Aborts all activity and returns to idle RX listen mode.

On ExitA0
9F70 .adlc_full_reset←3← 969D JSR← 9725 JSR← 985F JSR
LDA #&c1 ; CR1=&C1: TX_RESET | RX_RESET | AC (both sections in reset, address control set)
9F72 STA econet_control1_or_status1 ; Write CR1 to ADLC register 0
9F75 LDA #&1e ; CR4=&1E (via AC=1): 8-bit RX word length, abort extend enabled, NRZ encoding
9F77 STA econet_data_terminate_frame ; Write CR4 to ADLC register 3
9F7A LDA #0 ; CR3=&00 (via AC=1): no loop-back, no AEX, NRZ, no DTR
9F7C STA econet_control23_or_status2 ; Write CR3 to ADLC register 1
fall through ↓

Enter RX listen mode

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

On ExitA&67
9F7F .adlc_rx_listen←2← 9A0A JSR← 9FAF JMP
LDA #&82 ; CR1=&82: TX_RESET | RIE (TX in reset, RX interrupts enabled)
9F81 STA econet_control1_or_status1 ; Write to ADLC CR1
9F84 LDA #&67 ; CR2=&67: CLR_TX_ST | CLR_RX_ST | FC_TDRA | 2_1_BYTE | PSE
9F86 STA econet_control23_or_status2 ; Write to ADLC CR2
9F89 RTS ; Return; ADLC now in RX listen mode

Wait for idle NMI state and reset Econet

Called via svc_12_nmi_release (&06D4). Checks if Econet has been initialised; if not, skips to adlc_rx_listen. If initialised, spins until the NMI handler is idle (pointing at nmi_rx_scout), then falls through to save_econet_state to clear flags and re-enter RX listen mode.

9F8A .wait_idle_and_reset←1← 06F1 JMP
BIT econet_init_flag ; Econet not initialised -- skip to adlc_rx_listen
9F8D BPL reset_enter_listen ; Not initialised: skip to RX listen
9F8F .poll_nmi_idle←2← 9F94 BNE← 9F9B BNE
LDA nmi_jmp_lo ; Spin until NMI handler = &96DF (nmi_rx_scout)
9F92 CMP #&df ; Expected: &DF (nmi_rx_scout low)
9F94 BNE poll_nmi_idle ; Not idle: spin and wait
9F96 LDA nmi_jmp_hi ; Read current NMI handler high byte
9F99 CMP #&96 ; Expected: &96 (nmi_rx_scout high)
9F9B BNE poll_nmi_idle ; Not idle: spin and wait
9F9D LDA #&40 ; A=&40: RTI opcode (disable NMI processing)
9F9F STA l0d1c ; Self-modify NMI shim at &0D1C: disable
fall through ↓

Reset Econet flags and enter RX listen

Disables NMIs via INTOFF (BIT &FE18), clears tx_clear_flag and econet_init_flag to zero, then falls through to adlc_rx_listen with Y=5.

9FA2 .save_econet_state
BIT station_id_disable_net_nmis ; INTOFF: disable NMIs
9FA5 LDA #0 ; Clear both flags
9FA7 STA tx_clear_flag ; TX not in progress
9FAA STA econet_init_flag ; Econet not initialised
9FAD LDY #5 ; Y=5: service call workspace page
9FAF .reset_enter_listen←1← 9F8D BPL
JMP adlc_rx_listen ; Set ADLC to RX listen mode

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 (&96DF). 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 &96DF.

9FB2 .nmi_bootstrap_entry
BIT station_id_disable_net_nmis ; INTOFF: disable NMIs while switching ROM
9FB5 PHA ; Save A
9FB6 TYA ; Transfer Y to A
9FB7 PHA ; Save Y (via A)
9FB8 LDA #0 ; ROM bank 0 (patched during init for actual bank)
9FBA STA romsel ; Select Econet ROM bank via ROMSEL
9FBD 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.

9FC0 .rom_set_nmi_vector
STY nmi_jmp_hi ; Store handler high byte at &0D0D
9FC3 STA nmi_jmp_lo ; Store handler low byte at &0D0C
9FC6 LDA romsel_copy ; Restore NFS ROM bank
9FC8 STA romsel ; Page in via hardware latch
9FCB PLA ; Restore Y from stack
9FCC TAY ; Transfer ROM bank to Y
9FCD PLA ; Restore A from stack
9FCE BIT video_ula_control ; INTON: re-enable NMIs
9FD1 RTI ; Return from interrupt
9FD2 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 padding (unused ROM space)