Changes from ANFS 4.18

← Acorn ANFS 4.21 (variant 1) disassembly

Changes from ANFShref="glossary.html#term-nfs" class="glossary-ref" data-tip="Network Filing System: The Econet filing system ROM that provides remote file access over the Econet network.">NFS 4.18 to ANFS 4.21 (variant 1)

ANFS 4.18 was the last ANFS to run on a Model B. ANFS 4.21 (variant 1) is the first to run on a Master 128 – and the first to refuse to run on anything else. Between the two ROMs sits Acorn's biggest 8-bit hardware reshuffle, and ANFS's rewrite is shaped by it through and through.

The two 16 KB sideways ROMs share 86.7 % of their opcode structure but only 1.7 % of their bytes match at the same offset. Almost every operand has shifted, because the workspace has moved off-page and the ROM title gained a few bytes. The interesting changes lie underneath that arithmetic: the Model B's constraints – no hidden RAM, plain 6502 instruction set, direct VIA access – pushed 4.18 into workarounds the Master makes unnecessary, and 4.21 takes them all apart.

The host-OS gate

The first thing the 4.21 service handler does, before dispatching anything, is ask the OS what it's running on. Pre-Master OSes, and the Master Compact, are sent away unclaimed:

OS version Hardware Action
1.x BBC Model B reject
2.x BBC Model B+ reject
3.x Master 128 (MOS 3.20 / 3.50) proceed
4.0 Master Econet Terminal proceed
5.0 Master Compact reject

A reject silently returns service unclaimed, so MOS keeps looking for a filing system without ANFS interfering with anything it shouldn't. There was no equivalent gate in 4.18 – service_handler dispatched directly. The service entry-point itself shifts from &8A15 (4.18) to &8A54, most of which is the gate code.

"Variant 1" isn't part of the official name – the ROM image itself just calls itself "Acorn ANFS 4.21". Several distinct ROM images claiming to be ANFS 4.21 have turned up over the years; this project labels its first specimen "variant 1" to keep them apart. The relationship between the variants – their relative ordering, if any – is not known.

HAZEL: hidden RAM under the OS

The structural change with the broadest consequences is the move from MOS workspace in main RAM to HAZEL, the Master 128's 8 KB of hidden RAM at &C000&DFFF. HAZEL sits underneath the MOS VDU drivers in the address map and is reached by setting bit Y (bit 3) of the ACCCON register at &FE34 – when ACCCON.Y is set, reads and writes to &C000&DFFF go to HAZEL instead of the OS ROM. Filing systems are HAZEL's intended client; ANFS occupies the first 768 bytes (&C000&C2FF).

The 4.18 → 4.21 workspace mapping is uniform: every byte that lived in the page-&0E&10 main-RAM area shifts by +&B200.

Workspace data 4.18 4.21
Parse buffer &0E30 &C030
TX buffer base &0F00 &C100
FS lib flags &1071 &C271
FCB attributes &10C9 &C2C9
Saved catalogue buffer &10D9 &C2D9

In 4.18 the Model B had no spare RAM for a 768-byte filing-system workspace. The ROM coped by stealing pages 4–6 of main RAM and populating them on first selection from a copy loop in the main-ROM tail – about 130 bytes of code around &BE94. In 4.21 that whole apparatus is gone: the workspace is just there whenever ACCCON.Y is set, no copy required.

The hazel_minus_2 trick

The 6502's indexed addressing modes are cheap, but there is no STA (zp),Y – you need a zero-page pointer pair to do indirect- indexed writes. ANFS routines that copy small blocks through HAZEL frequently want to start at &C000, but a typical zero-page layout already has every byte spoken for, and burning two more for "the HAZEL base" is wasteful when only Y ever varies.

The 4.21 ROM solves this by putting the labels just below HAZEL. The ROM's last 64 bytes are &FF padding (read-only and otherwise unused), but two of those bytes are deliberately labelled:

hazel_minus_2 = &BFFE
hazel_minus_1 = &BFFF

Now STA hazel_minus_2,Y with Y ≥ 2 lands at &BFFE + Y ≥ &C000 – inside HAZEL. The instruction is three bytes, no zero-page pointer needed, no setup. Routines like loop_copy_fs_ctx use the pattern with just LDA src,X / STA hazel_minus_2,Y; the loop's CPY guard simply stops Y before it could underflow back into the ROM tail.

It's a piece of code economy that becomes possible only because the ROM author controls both endpoints – the data layout in the tail and the access patterns in the routines. A few bytes of &FF padding are invisible to the user; what matters is that the labels at the right offsets exist so the assembler can resolve hazel_minus_2,Y to &BFFE,Y.

65C02 instructions

Because the host-OS gate guarantees a Master, the assembler runs in 65C02 mode. About a dozen new instructions show up across the ROM:

The savings aren't dramatic per call site, but they accumulate. The 4.21 ROM has 81 more instructions than 4.18 (8270 vs 8189) while implementing strictly more functionality – because the new instructions paid for the new code.

Interrupts, deferred work, and the System VIA

NFS 3.65 and ANFS 4.18 both poked the System VIA directly to drive the ADLC clock. 3.65's imm_op_setup is the textbook example – it writes IER bit 2 to mask the SR interrupt, reads-modifies- writes ACR to set shift-register mode 2, then reads SR to clear the pending interrupt – and 4.18 carries an analogous sequence at &8036&8049 and a second one in setup_sr_tx. Eleven direct System-VIA accesses in 4.18 in total.

In ANFS 4.21 these are all gone. A bytewise scan finds zero direct accesses to &FE40&FE4F anywhere in the 16 KB ROM. setup_sr_tx survives in name and updates a pair of ACR/SR-format bytes at &0D68 / &0D69, but those bytes are read and written only by ANFS itself – nothing in this ROM ever flushes them to the live VIA registers.

Why this works on a Master without the per-packet SR setup 4.18 needed is a deeper question than this article can answer. The plausible explanation is that the Master's NMI claim path uses different facilities entirely (the master_intoff register at &FE38 rather than the Model B's &FE18 station-ID read), and that the ADLC clocking the SR-mode-2 setup arranged is either configured once at NMI claim time or supplied by Master MOS without ANFS having to reach for it. The factual delta from 4.18 is the easier claim: eleven VIA writes became zero.

The IRQ entry svc5_irq_check lives at the same address &8028 in both ROMs, but the body is unrecognisable:

4.18:                         4.21:
    LDA &FE4D    ; VIA IFR        LDY &0D65    ; deferred-work flag
    AND #&02     ; SR IRQ?        BEQ no_deferred
    BEQ no_sr_irq                 TRB ACCCON   ; release shadow-RAM
    ...                           STZ &0D65    ; consume the flag
                                  BMI dispatch_svc5
                                  LDA #&FE     ; fire RX event
                                  ...

The deferred-work flag at &0D65 is set by IRQ and NMI paths when they spot work that can't be done in the interrupt context – because MOS may be holding the VIA, or shadow RAM is paged the wrong way for the address the handler wanted to touch. The next service-handler entry, which runs with the interrupt cleared and the OS in a known state, consumes the flag, restores ACCCON, and dispatches the deferred work. It's the standard raise a flag in the ISR, drain it in non-IRQ code pattern, but the architectural reason for it is explicit: the Master's memory paging makes some operations unsafe inside the ISR.

The Master's &27 service call ("Reset has occurred") follows the same discipline. ANFS uses it specifically to claim NMIs for Econet receive – because the Master MOS no longer offers workspace on a soft BREAK, a 4.18 lifecycle assumption that 4.21 has to abandon.

OSWORD &13: clean errors instead of quiet zeroes

OSWORD &13 is ANFS's API surface – clients use it to read and write the saved file-server station, the receive-channel handles, the bridge state, and so on. In 4.18 each sub-handler began with a BIT &0D6C / BPL <return> that quietly bailed if ANFS wasn't the active filing system, returning a zeroed parameter block. A client that expected a station back and got 0.0 had no way to tell whether the FS was inactive or whether the FS was on station 0.0.

4.21 wraps every OSWORD &13 sub-handler in an ensure_fs_selected prologue. It tests bit 7 of fs_flags and, if clear, calls cmd_net_fs to make ANFS the active filing system. On selection failure it raises a 'net checksum' error (&AA).

The new behaviour is more aggressive than the old quiet-zero, but it's more honest: the client either gets real data or a real error, never a confusable success with a misleading payload.

Protection moves into CMOS

*PROT and *UNPROT in 4.18 took protection-type keywords (Halt, Peek, Poke, Jsr, Proc, Utils) and accumulated the parsed bits into a workspace mask – one bit per remote operation the station would refuse over Econet, with no keyword meaning "protect against all of them". The keyword-parser table and the per-keyword bit-encoding ran to fifty-odd bytes; the parser body itself was more. The mask was lost on BREAK.

cmd_prot and cmd_unprot in 4.21 take no arguments. They're each a four-instruction toggle of bit 6 of CMOS RAM byte &11 (the Master's Econet station-flags byte) via OSBYTE &A1 (read CMOS) and OSBYTE &A2 (write CMOS), with the new value also mirrored into a workspace byte pair at &0D68 / &0D69:

cmd_prot:
    LDA #&11                ; CMOS byte: station flags
    JSR osbyte_a1           ; read into Y
    TYA
    ORA #&40                ; set bit 6
    JSR osbyte_a2           ; write back
    JMP set_ws_pair_0d68_0d69

Storing the state in CMOS rather than volatile RAM means it survives BREAK, soft reset, and power-cycle – ANFS doesn't have to manage persistence at all. The keyword parser and its encoding table are gone. What's left is a routine the user could read and understand in thirty seconds.

What 4.21 sheds

Some 4.18 features simply don't appear in 4.21, because the Master MOS already provides them or because the wrapper was vestigial:

Together these account for several hundred bytes – roughly the budget that the host-OS gate, the 65C02-prologue rewrites, the shadow-VIA path, the deferred-work IRQ machinery, and ensure_fs_selected reclaim.

In sum

4.21 v1 is a leaner client running on a richer host. The Model B's constraints pushed 4.18 into workarounds: relocated workspace pages, hand-rolled push/pop sequences, direct VIA writes, persistent state held in volatile RAM. The Master 128 dissolves all of those constraints, and 4.21 takes them apart in favour of HAZEL workspace, 65C02 push/pull, shadow-VIA reconciliation through the IRQ path, and CMOS-backed configuration. What's left is a ROM that's tightly bound to its host – which is exactly why it gates so aggressively against being run anywhere else.