Rom Filing System — Developer's Guide

RFS Developer's Guide

This guide is a detailed walkthrough of the RFS source code and development environment. It explains Z80 assembly language concepts for developers who may not be familiar with the language, walks through every source module, documents the bank-switching architecture, and shows how to add new commands, modify existing modules, and port RFS to new hardware.
For hardware architecture and build system details see the Technical Guide. For user-facing operation see the User Manual.

Introduction to Z80 Assembly for Non-Assembly Programmers

The entire RFS firmware is written in Z80 assembly language — the native instruction language of the Zilog Z80 processor used in the Sharp MZ series. Unlike high-level languages, assembly maps almost directly to the physical hardware: every instruction translates to one or a few bytes that the CPU executes directly.

Registers
The Z80 has no "variables" — instead it has a small set of registers (fast storage locations inside the CPU). The most commonly used ones in RFS:
Register Size Role
A 8-bit Accumulator — the primary register for arithmetic, logic, and I/O operations. Almost every instruction involves A.
B, C 8-bit General purpose. BC together forms a 16-bit pair, commonly used as a loop counter or byte count.
D, E 8-bit General purpose. DE together is a 16-bit pair, commonly used as a source or destination pointer.
H, L 8-bit General purpose. HL together is the main 16-bit memory pointer — most memory read/write instructions use HL.
IX, IY 16-bit Index registers — used for base+offset memory addressing. Slower than HL but convenient for structured data.
SP 16-bit Stack Pointer — points to the top of the call stack. PUSH and POP use SP automatically.
PC 16-bit Program Counter — the address of the current instruction. Incremented automatically; modified by jumps and calls.
F 8-bit Flags register — individual bits set by arithmetic operations: Z (zero), C (carry), S (sign), P/V (parity/overflow).

Key Instructions
  • LD dest, src — Load (copy) data. LD A, B copies B into A. LD A, (HL) reads the byte at the memory address held in HL into A. LD (0x1200), A writes A to memory address 0x1200.
  • CALL addr — Call a subroutine. Pushes the return address (next instruction) onto the stack and jumps to addr. Equivalent to a function call.
  • RET — Return from subroutine. Pops the return address off the stack and jumps to it.
  • JP addr — Unconditional jump to addr. JP Z, addr jumps only if the Zero flag is set (i.e. the last operation produced zero).
  • JR offset — Short relative jump (−128 to +127 bytes). Faster and more compact than JP for nearby branches.
  • DJNZ offset — Decrement B and jump if Not Zero. The canonical Z80 loop instruction: LD B, 10 / LOOP: ... / DJNZ LOOP repeats 10 times.
  • ADD A, n — Add n to A. SUB n subtracts. AND n, OR n, XOR n — bitwise logic on A.
  • IN A, (port) — Read from I/O port into A. OUT (port), A — write A to I/O port. These are how the Z80 communicates with hardware (the WD1773, SPI controller, bank latch, etc.).
  • PUSH rr / POP rr — Save/restore a 16-bit register pair to/from the stack.
  • EI / DI — Enable / Disable interrupts. Code that must not be interrupted (e.g. time-critical tape operations) is wrapped between DI and EI.

Addressing Modes
The Z80 offers several ways to specify where data comes from or goes to:
  • Immediate: LD A, 42 — the value is embedded in the instruction bytes themselves.
  • Register: LD A, B — data comes from or goes to a register.
  • Indirect (via HL): LD A, (HL) — HL contains a memory address; data is read from that address.
  • Extended (direct address): LD A, (0x1200) — the address is a literal 16-bit constant in the instruction.
  • Indexed: LD A, (IX+5) — IX holds a base address; 5 is added to get the effective address. Used in RFS for accessing fields within fixed-format data structures.

GLASS Assembler Syntax
RFS uses the GLASS Z80 assembler. Key syntax features:
  • Comments begin with ; — everything to the right of a semicolon is ignored.
  • Labels are identifiers followed by :. A label at the start of a line names the address of the next instruction.
  • EQU defines a constant: BELL EQU 007H — the assembler replaces every occurrence of BELL with 0x07.
  • DB (Define Byte) inserts raw bytes: DB 0x41, 0x42 emits two bytes. Used for strings and lookup tables.
  • DW (Define Word) inserts 16-bit little-endian values: DW HANDLER emits the address of the HANDLER label.
  • ORG addr sets the assembly origin — subsequent code is assembled as if it lives at addr.
  • INCLUDE "file.asm" textually includes another file at the current position.
  • IF / ENDIF conditional assembly: IF BUILD_SFD700 = 1 ... ENDIF — the enclosed instructions are only assembled when the condition is true. This is how RFS builds four different firmware variants from one source tree.

Source Tree

Path Contents
asm/ All Z80 assembly source files
asm/include/ Shared definitions and configuration files
asm/dis/ Disassembled reference files for SA-5510 and XPATCH
tools/ Build scripts, GLASS assembler, utility binaries
MZF/ MZF format application files, organised by machine type
MZB/ Sector-padded binary applications (generated by build)
roms/ Build output — ROM images and SD card images
releases/ Pre-built release binaries
config/ CP/M disk format definitions (diskdefs)
cpmtools/ cpmtools source (submodule)
src/ Source for supporting tools

Configuration: rfs_definitions.asm

This is the central configuration file, included by every other source file via INCLUDE "rfs_definitions.asm". Every assembly-time option is controlled here. The key sections:

Build Target Flags
HW_SPI_ENA    EQU 1    ; 1 = hardware SPI on RomDisk v2+ PCB
SW_SPI_ENA    EQU 0    ; 1 = software bit-bang SPI (RomDisk v1)
PP_SPI_ENA    EQU 0    ; 1 = SPI via parallel port (RomDisk v1 alternative)
FUSIONX_ENA   EQU 0    ; 1 = running on tranZPUter FusionX
KUMA80_ENA    EQU 0    ; 1 = Kuma 40/80 upgrade present
VIDEOMODULE_ENA EQU 0  ; 1 = 40/80 colour video module present
BUILD_ROMDISK EQU 0    ; 1 = build for RomDisk card
BUILD_SFD700  EQU 0    ; 1 = build for SFD-700 floppy interface
BUILD_PICOZ80 EQU 1    ; 1 = build for picoZ80 board
ENADEBUG      EQU 0    ; 1 = enable debug output during assembly
Exactly one BUILD_* flag must be set to 1 at a time. All conditional assembly blocks throughout the source test these flags to include or exclude platform-specific code.

Address Constants
UROMADDR    EQU 0E800H   ; Base address of the User ROM window
UROMBSTBL   EQU UROMADDR + 020H   ; Bank-switch table entry point (fixed offset)
RFSJMPTABLE EQU UROMADDR + 0B0H   ; RFS jump table start
FDCROMADDR  EQU 0F000H   ; FDC ROM address (SFD-700 MROM location)

; SFD-700 specific bank defaults (only assembled when BUILD_SFD700 = 1):
BNKDEFMROM_MZ80A EQU 0   ; Default MROM bank for MZ-80A (AFI ROM)
BNKDEFMROM_MZ700 EQU 1   ; Default MROM bank for MZ-700 (AFI ROM)
BNKDEFUROM       EQU 2   ; Default UROM bank for RFS (starts at 8KB in Flash)
These constants define where in the Z80 address space each ROM window sits. Code compiled for the User ROM window always assembles with ORG 0xE800; code for the Monitor ROM window assembles at ORG 0x0000.

Character and Control Definitions
Standard ASCII control characters are defined as named constants to make the source self-documenting:
BELL    EQU 007H    ; Terminal bell
CR      EQU 00DH    ; Carriage return
LF      EQU 00AH    ; Line feed
CS      EQU 00CH    ; Clear screen
SPACE   EQU 020H    ; ASCII space
DELETE  EQU 07FH    ; Delete key

Bank Switching in Detail

Bank switching is the heart of the RFS architecture. Understanding it is essential before modifying any source file.

Why Banking is Needed
The Sharp MZ-80A gives User ROM only 2 KB of address space (0xE800–0xEFFF). 2 KB can hold only a few hundred instructions — nowhere near enough for a filing system, assembler, disassembler, tape controller, SD card driver, and CP/M CBIOS. The solution is to physically switch which 2 KB of a 512 KB Flash chip is visible at that address range. By storing 12 different 2 KB RFS banks (banks 0–11) in the Flash chip and switching between them on demand, RFS achieves effectively 24 KB of ROM code — with a further 4 banks (12–15) reserved for the CP/M CBIOS. In addition, three of the 16 Monitor ROM pages (banks 6, 7 and 9) hold the Z80 assembler/disassembler opcode tables and RFS message strings, extending the available ROM space without consuming any User ROM capacity.

The Bank-Switch Stub
Every bank begins with an identical copy of the bank-switching stub occupying the first 32 bytes of the bank (0xE800–0xE81F). This stub provides:
  • A standard call gateway: Any bank can call any routine in any other bank by calling the stub with the target bank number and target address. The stub writes the bank number to the hardware latch (typically an I/O port write), then calls the requested address. The callee runs in the new bank, and when it returns, the stub switches back to the original bank.
  • A consistent entry point: Because the stub is at a fixed offset (0xE800 + 0x20 for the bank-switch table), code in bank 0 can reliably find the stub in bank 3 even though it has never seen bank 3's internal addresses.
The coded latch on RomDisk v2+ boards adds a protection mechanism: the bank latch register only becomes writable after the CPU reads from the upper 8 bytes of User ROM space (0xEFF8–0xEFFF) a specific number of times in sequence. This prevents accidental bank switches caused by stray code that happens to write to the latch address. The stub handles this unlock sequence before every bank switch.

Command Table Format (rfs.asm)
The monitor command dispatcher in rfs.asm uses a compact command table. Each entry describes one command and is laid out as follows:
; One command table entry:
;
;   DB  FLAGS          ; 1 byte: END|MATCH|BANK[5:3]|SIZE[2:0]
;   DB  "COMMAND"      ; SIZE bytes: the command string (no null terminator)
;   DW  HANDLER_ADDR   ; 2 bytes: address of the handler routine in the named bank
;
; Flags byte:
;   Bit 7 = 1: End of table marker (last entry).
;   Bit 6 = 1: Exact match required (command must be entire input, no trailing chars).
;   Bits 5:3   Bank number where HANDLER_ADDR lives (0–11 for RFS, 12–15 for CBIOS).
;   Bits 2:0   Length of the command string in bytes.
;
; Example — the ASM command (all builds, lives in bank 6, 3-char string):
CMDTABLE:
    DB  000H | 000H | 030H | 003H    ; FLAGS: not-end, not-exact, bank 6, length 3
    DB  "ASM"                         ; Command string
    DW  ASM_MAIN                      ; Handler address in bank 6
The dispatcher reads the monitor input line, walks the table, and for each entry:
  1. Compares the input against the command string (case-insensitive on some builds).
  2. If matched, extracts the bank number and handler address from the table entry.
  3. Performs a bank switch to the target bank.
  4. Calls the handler with any remaining input (parameters) available in the monitor input buffer.
Two separate command tables exist: CMDTABLE2 for the SFD-700 build, and CMDTABLE for the RomDisk/picoZ80 build. Both are structured identically but contain different command sets — notably the SFD-700 table excludes SD card commands (IC, LC, SC, EC, DUC, T2SD, SD2T) since that hardware has no SD card. The ASM and DASM commands are present in both tables.

Module Walkthroughs

rfs.asm — Command Dispatcher (User ROM Bank 0)
Role: The entry point for all RFS functionality. When the SA-1510 monitor does not recognise a command, it passes control to the User ROM entry point at 0xE800. This is always bank 0.
Key sections:
  • Bank-switch stub (0xE800–0xE81F): The cross-bank call gateway described above. Every bank has an identical copy.
  • Bank-switch table (0xE800 + 0xB0): A jump table that maps bank numbers to their physical Flash ROM addresses. Modified at boot time if the hardware requires non-sequential bank addressing.
  • Command table (CMDTABLE / CMDTABLE2): The list of all RFS commands with their bank and handler address.
  • Main dispatcher loop: Reads the monitor's input buffer, walks the command table, performs the bank switch and calls the handler. If no command matches, returns to the SA-1510 monitor so it can print the "?" error.
  • RFS initialisation: On first entry after reset, RFS detects the hardware platform (from the MODE register on SFD-700, or from flags on RomDisk), initialises the SPI and SD card, and sets the initial drive to 0.
SFD-700 note: When BUILD_SFD700 = 1, rfs.asm assembles with ORG 0xE000 / ALIGN 0xE300 rather than ORG 0xE800, because the SFD-700 maps its User ROM window at 0xE300–0xEFFF (0xE000–0xE2FF is reserved for MZ-700 memory-mapped I/O).

rfs_bank1.asm — Floppy Disk Controller (User ROM Bank 1)
Role: Implements floppy disk commands — floppy boot (F / FL), floppy directory (FD) and the direct AFI jump (f). The full FDC command set is assembled on all builds.
Key functions:
  • FLOPPY (FL): Prompts for a drive number (if not supplied on the command line), initializes the disk, reads the boot sector, verifies the disk signature, extracts program info (name, load address, size, execution address), loads the program into memory and executes it.
  • FDDIR (FD): Lists the directory of files on a floppy disk. Accepts an optional drive number (1–4, default 1). Reads the boot sector, verifies MZ-700 disk format, then scans directory sectors displaying filenames, load addresses, execution addresses and file sizes.
  • FDCK: Reads the byte at 0xF000 to verify the AFI ROM is present and non-zero, then calls 0xF000 directly. This is the f (lowercase) command on all builds.
  • At the end of the bank, an ALIGN 0xF000 directive ensures the SFD-700 ROM image positions the AFI boot ROM precisely at 0xF000 in the Flash layout.

rfs_bank2.asm — SD Card Controller (User ROM Bank 2)
Role: The complete SD card subsystem — SPI initialisation, SD card command protocol, and the SDCFS directory and file I/O routines. On the SFD-700 build, the SD card controller code is not assembled (the SFD-700 hardware has no SD card interface); the bank slot is present in the ROM image but contains only the bank-switching stub.
Key functions:
  • SPI driver (hardware or software): Conditional assembly selects between hardware SPI (using the RomDisk v2 SPI controller registers) and software bit-bang SPI (toggling individual I/O port bits to clock the SPI bus). The hardware SPI path is substantially faster and used on all current boards (HW_SPI_ENA = 1).
  • SD card initialisation (SDINIT): Implements the SD card initialisation sequence — sends CMD0 (GO_IDLE), CMD8 (SEND_IF_COND), ACMD41 (SD_SEND_OP_COND) to switch the card from SPI mode to the active state. Handles both SD and SDHC/SDXC card types by checking the OCR response.
  • Sector read (SDREAD): Sends CMD17 (READ_SINGLE_BLOCK) with a 32-bit sector address, waits for the data start token (0xFE), then reads 512 bytes into a Z80 RAM buffer. Uses hardware SPI burst mode where available.
  • Sector write (SDWRITE): Sends CMD24 (WRITE_BLOCK), the data start token, 512 bytes of data, and the CRC. Waits for the write response and busy signal to clear.
  • SDCFS directory read (SDDIR): Reads the directory from the first 8 KB of the active drive image and builds a RAM-resident directory cache used by IC, LC, SC, and EC commands.
  • SDCFS file load (SDLOAD): Given a file number from the directory, calculates the sector address of the file's 64 KB block, reads the actual file size bytes, and loads them to the Z80 address specified in the directory entry's LOAD ADDR field.
  • SDCFS file save (SDSAVE): Allocates a new directory slot (or finds an existing entry with the same name to overwrite), sets the START SECTOR, SIZE, LOAD ADDR, and EXEC ADDR fields, then writes the file data to the appropriate 64 KB block.

rfs_bank3.asm — Memory Utilities (User ROM Bank 3)
Role: Implements the D (hex dump), M (memory edit), CP (memory copy), IN (I/O port read), and OUT (I/O port write) commands, which are available on all builds. The DUC (SD card file dump), T2SD (tape to SD) and SD2T (SD to tape) commands are also implemented here but are only assembled for the RomDisk / picoZ80 builds — the SFD-700 build excludes them as there is no SD card.
Hex dump (D): Reads up to 20 lines of 16 bytes each from the target address range. For each line it prints the 4-digit hex address, 16 hex byte values (with a space between every 4 bytes), and the 16 ASCII characters (using a dot for non-printable bytes). The Sharp MZ uses an unusual character encoding — bank 5 provides the Sharp-to-ASCII conversion table used here.
Memory editor (M): Presents each byte in turn, showing the address and current value. The user can type a new hex value (1 or 2 digits) and press Enter to write it, or press Enter alone to leave it unchanged. Pressing Ctrl+C or a specific escape sequence exits.
I/O port commands (IN / OUT): IN reads one or more Z80 I/O ports (2 or 4 hex digit addresses, comma-separated) using IN A,(C) and prints each value as 2-digit hex. OUT writes to one or more ports — each entry is a port address followed by a colon and a 2-digit hex value (e.g. OUTD0:01,D1:80), executed with OUT (C),A. Both commands support the full 16-bit Z80 I/O port address space.
T2SD and SD2T: These commands perform transparent bidirectional copy between tape (CMT) and the SD card. T2SD calls the CMT load routine from bank 4 to read a tape file into RAM, then calls the SD save routine from bank 2 to write it to the active drive. SD2T calls the SD load routine from bank 2 to put the file into RAM, then calls the CMT save routine from bank 4 to write it to tape. Both directions use the SDCFS directory to maintain filenames, sizes, and addresses.

rfs_bank4.asm — CMT Controller (User ROM Bank 4)
Role: Implements the L/LT, LTNX, S/ST, and V tape (CMT) commands.
The Sharp MZ-80A uses a 1200 baud Kansas City Standard cassette interface. Bytes are encoded as bursts of 1200 Hz (bit 0) or 2400 Hz (bit 1) tone. The tape routines are time-critical — they must read or write each bit within a tight timing window. They use the 8253 timer chip (or CPU cycle-counted loops on platforms without the timer) to measure the incoming tone frequency and generate the outgoing waveform. Interrupts are disabled (DI) throughout tape operations to prevent timing disruption.
The MZF tape format prefixes every program with a 128-byte header containing the file type, filename, data length, load address, and execution address — the same fields stored in the SDCFS directory entry. This is why SD↔tape copy is transparent: the header format is identical.

rfs_bank5.asm — Utility Functions (User ROM Bank 5)
Role: A library of shared routines called by other banks. Because bank-switching is expensive (requires the unlock sequence on v2+ boards), frequently used routines are centralised here to minimise switching overhead.
Key routines:
  • PRTHEX: Prints the A register as two hex digits to the screen.
  • PRTHL: Prints the HL register as four hex digits.
  • PRTSTR: Prints a null-terminated string from (HL) to the screen, handling the Sharp character encoding conversion.
  • INPHEX: Reads a hex number (up to 4 digits) from the keyboard, returning the value in HL.
  • STRCMP: Compares two null-terminated strings.
  • SUBSTR: Extracts a substring, used by the command dispatcher to separate command names from parameters.
  • WAITKEY: Waits for a keypress and returns the key code in A. Used for "press any key to continue" pauses in IC and IR directory listings.

rfs_bank6.asm — ASM/DASM Opcode Table 1 (User ROM Bank 6)
Role: Stores the first half of the Z80 opcode lookup tables used by both the assembler (ASM) and disassembler (DASM) commands, plus the PRINTMSG function and message string infrastructure. The opcode tables map Z80 mnemonic strings to opcode bytes and vice versa. The assembler and disassembler are available on all builds (RomDisk, picoZ80, SFD-700 and FusionX).
The PRINTMSG function reads message strings from MROM bank 9 into a RAM buffer before printing, since the monitor ROM functions (PRNT, ?DSP, etc.) require the monitor MROM bank to be active during character output. Corresponding MROM bank 6 also holds opcode table data in the 4 KB Monitor ROM space, providing additional room for the full Z80 instruction set including all prefix-byte variants (CB, DD, ED, FD).

rfs_bank7.asm — ASM/DASM Opcode Table 2, DASM, Tests (User ROM Bank 7)
Role: Stores the second half of the Z80 opcode lookup tables, the DASM disassembler main routine, the R DRAM test, and the T timer test.
Disassembler (DASM): Reads machine code bytes from the target address, decodes each instruction (using the opcode tables in banks 6 and 7), and prints the address, hex bytes, and mnemonic for each instruction. Handles all Z80 prefix bytes (CB, DD, ED, FD) and extended instructions.
DRAM test (R): Performs a walking-bit pattern write/verify across the full user RAM space (0x1200–0xCFFF). Reports any failed addresses. Useful for diagnosing faulty RAM chips — a common failure mode in vintage machines.

rfs_bank8.asm — Z80 Assembler (User ROM Bank 8)
Role: Implements the ASM interactive assembler command.
Interactive assembler (ASM): Presents a line-input prompt at the target address. The user types Z80 mnemonics (e.g. LD A, 42) which are parsed, assembled to machine code bytes, and written directly to the target address in RAM. The address advances by the size of each assembled instruction. This allows assembly of small routines directly on the hardware without an external PC. The opcode lookup tables in banks 6 and 7 (and MROM banks 6 and 7) are used for mnemonic-to-opcode translation.

rfs_bank9.asm — ROM Directory and File Functions (User ROM Bank 9)
Role: Contains ROM directory enumeration, file find, file load and print functions that were moved from bank 0 (the command dispatcher bank) to free space in bank 0 for additional command table entries and infrastructure. Key functions include DIRROM9 (ROM directory listing), FINDSDX9 (file search), ISMZF9 (MZF header validation), and _PRTMZF9 (MZF entry display).

rfs_bank11.asm — Help Screen (User ROM Bank 11)
Role: Stores the paginated help screen text (moved from bank 6 to make room for the opcode tables). The help screen is stored as a sequence of null-terminated strings (one per line). The H command prints them with automatic pagination, calling the WAITKEY routine from bank 5 at each screenful boundary.

rfs_mrom.asm — Monitor ROM Utilities (Monitor ROM Bank 3)
Role: Provides the ROM scanning and MZF file load routines that must run from Monitor ROM space rather than User ROM space.
Why a separate Monitor ROM bank? The IR and LR commands need to enumerate MZF files stored in the User ROM Flash chips (User ROM banks beyond 15 hold packed MZF programs). To scan a User ROM bank, the CPLD must switch the User ROM window to point at that bank. But the scanning code itself lives in the User ROM — if it switches the User ROM bank, it instantly replaces itself with a different bank and crashes.
The solution is to place the scanning loop in Monitor ROM bank 3. Monitor ROM bank switching is independent of User ROM bank switching. The MROM scanning routine can freely switch User ROM banks (to enumerate each bank's MZF header list) without disturbing its own execution context.
Key functions:
  • ROMDIR: Scans all User ROM banks above 15, reads each MZF header, and builds a RAM-resident ROM directory used by the IR command.
  • ROMLOAD: Given a file number from the ROM directory, reads the MZF data from the appropriate User ROM bank into the LOAD ADDR specified in the header, then optionally jumps to EXEC ADDR.

CP/M CBIOS Modules
The four CBIOS User ROM banks (12–15) and the CBIOS Monitor ROM bank (2) together implement the complete CP/M 2.2 CBIOS. Each bank provides one subsystem:
cbios.asm (Monitor ROM bank 2): The CBIOS entry point table — all 17 CP/M API vectors (BOOT through SECTRN) are jump addresses within this module. Also holds the disk parameter tables (DPH, DPB structures that tell CP/M the geometry of each disk drive), the cold/warm boot sequences, and the ROM disk controller (reads sectors from User ROM Flash).
cbios_bank1.asm (User ROM bank 12): Audio output (bell tone and melody using the MZ-80A's 8255 PPI and buzzer), real-time clock routines using the 8253 timer, and keyboard input with auto-repeat (key held for > 500 ms repeats at ~10 Hz, matching the behaviour users expect from a modern keyboard).
cbios_bank2.asm (User ROM bank 13): Screen driver — character output at the current cursor position, scrolling, clear screen, cursor positioning. Also the ANSI terminal emulator: a state machine that recognises VT52/VT100 escape sequences (CSI codes) and translates them into the equivalent MZ-80A screen operations. This makes CP/M applications that assume a smart terminal (WordStar, Turbo Pascal, etc.) work correctly without any modification to those applications.
cbios_bank3.asm (User ROM bank 14): SD card disk driver for CP/M. Translates CP/M 128-byte sector read/write requests into SDCFS operations on the CP/M disk images stored after the 256 MB boundary on the SD card. Includes the sector skew table used by SECTRN to improve disk access performance.
cbios_bank4.asm (User ROM bank 15): Floppy disk controller for CP/M. Uses the WD1773 (via SFD-700) or equivalent floppy controller to service CP/M disk read/write requests for physical floppy drives. Data is read un-inverted (unlike the MZ-80A AFI ROM which uses inverted data) because the CBIOS writes its own re-inverted format.

Adding a New Monitor Command

To add a new command FOO that lives in User ROM bank 3 (memory utilities):
  1. Write the handler in rfs_bank3.asm: Add a labeled routine FOO_CMD: that implements the command. Parameters are available in the monitor input buffer (HL points to the first character after the command name). Return with RET when done.
  2. Add an entry to the command table in rfs.asm: In the appropriate CMDTABLE (or CMDTABLE2 for SFD-700), add:
; FLAGS: not-end (bit7=0), not-exact (bit6=0), bank 3 (bits5:3 = 011 = 0x18), length 3 (bits2:0 = 011)
    DB  000H | 000H | 018H | 003H
    DB  "FOO"
    DW  FOO_CMD
  1. Update the help text in rfs_bank6.asm: Add a line describing the new command to the help screen text strings.
  2. Rebuild: Run ./build.sh. The assembler will report any size overflows if bank 3 is now larger than 2 KB — in that case, remove or compress other code in that bank.

Adding a New Hardware Platform

To port RFS to a new hardware target:
  1. Add a build flag in rfs_definitions.asm: BUILD_NEWBOARD EQU 0 (set to 1 when building for the new target).
  2. Add address constants if the new hardware maps ROM windows at different addresses.
  3. Add conditional assembly blocks throughout the source where hardware-specific behaviour differs — I/O port addresses for the bank latch, SPI controller register layout, SD card presence detection, etc. Follow the pattern of existing IF BUILD_ROMDISK = 1 ... ENDIF blocks.
  4. Add a new build script (or extend make_roms.sh) to package the ROM image for the new target's Flash chip layout.
  5. Update the prerequisite check in build.sh if the new target needs additional tools.

Debugging Tips

Enable debug output: Set ENADEBUG EQU 1 in rfs_definitions.asm before building. This includes additional diagnostic output at strategic points in the SD card initialisation and SDCFS routines.
Use the built-in disassembler: On SFD-700 builds, type DASM addr to disassemble assembled code in RAM. This is invaluable for verifying that a newly assembled routine was correctly encoded by the ASM command.
Use the hex dump: D E800 shows the first 320 bytes of the current User ROM bank — useful for verifying that the bank latch is selecting the expected bank.
DRAM test after any RAM modification: Type R to run a comprehensive DRAM test whenever adding new RAM-resident data structures. This catches addressing bugs early before they appear as mysterious crashes.
Bank-switch guard: On RomDisk v2+ boards, never place a DJNZ or any other looping instruction that spans 0xEFF8–0xEFFF. The coded latch interprets reads from that range as the unlock sequence and may switch banks unexpectedly.

Build Environment

The RFS build toolchain is straightforward — only a Java runtime and the GLASS Z80 assembler are required for the Z80 assembly components. The FusionX global build script build.sh automates the entire process.

Prerequisites
# Install Java runtime (required for the GLASS assembler)
sudo apt install -y default-jre git

# Clone the repository
git clone https://git.eaw.app/eaw/tzpuFusionX.git
cd tzpuFusionX

# Initialise git submodules
git submodule update --init --recursive
The GLASS Z80 assembler JAR file is bundled in the repository at software/tools/glass-0.5.1.jar — no separate download is needed. Any Java 8 or later runtime will work.

Building
# Build all Z80 assembly ROMs and MZF files (includes RFS)
./build.sh --asm

# Build output appears in software/roms/
ls software/roms/*.rom software/roms/*.mzf
The build script assembles each ROM and MZF target using the GLASS assembler, passing the appropriate include paths and build flags. MS BASIC variants (MZ-80A, MZ-700, TZ40, TZ80) are built from the same source file with different BUILD_VERSION flags set via generated include files.
Build output:
Output Description
monitor_sa1510.rom SA-1510 monitor ROM
monitor_80c_sa1510.rom SA-1510 monitor with 80-column support
monitor_1z-013a.rom 1Z-013A monitor ROM
mz80afi.rom MZ-80A AFI (floppy interface) ROM
mz2000_ipl_*.rom MZ-2000 IPL ROMs (original, TZPU, FusionX)
mz800_*.rom MZ-800 system ROMs
msbasic_*.mzf MS BASIC for each target
sa-5510_tzfs.mzf SA-5510 BASIC with TZFS support
sharpmz-test.mzf Hardware test MZF

Continuous Integration
What is CI/CD? Continuous Integration (CI) is a practice where a dedicated server automatically builds your project every time you push code changes. Instead of manually running the assembler on your development machine, packaging the ROM files, and uploading them to a download page, a CI server does all of this automatically. If the build breaks — for example, because of a syntax error or a missing include file — you receive an email notification immediately. This catches problems early and ensures that every published release was built from a clean, reproducible starting point.
The RFS ROMs are built as part of the FusionX Jenkins CI pipeline. Jenkins is a popular open-source automation server that runs on a VPS (Virtual Private Server) or any Linux machine. It watches the Gitea repository for pushes to the master branch and automatically triggers a full build of all project components.

How It Works
The automated build process for RFS follows these steps:
  1. You push code to the master branch of the Gitea repository.
  2. Gitea sends a webhook (an HTTP notification) to the Jenkins server.
  3. Jenkins clones the repository into a fresh, clean workspace.
  4. Jenkins runs ./build.sh --asm which assembles all Z80 monitor ROMs and MZF files using the GLASS assembler.
  5. Jenkins packages the assembled ROM and MZF files into a versioned tarball (FusionX-ROMs-v1.08.tar.gz).
  6. Jenkins creates a Gitea Release with the tarball attached as a downloadable asset.
  7. Jenkins sends an email reporting success or failure.
The entire process takes about one minute and requires no manual intervention after the initial push.

Setting Up Jenkins
Jenkins runs inside a Docker container for easy installation and portability. The minimum requirements are a Linux server with 2 GB RAM, Docker installed, and network access to your Gitea repository. On your server:
# Install Docker (Debian/Ubuntu)
sudo apt update && sudo apt install -y docker.io docker-compose
sudo systemctl enable docker && sudo systemctl start docker

# Create the Jenkins directory
sudo mkdir -p /srv/jenkins/data
cd /srv/jenkins
Create a docker-compose.yml file:
# /srv/jenkins/docker-compose.yml
version: '3.8'
services:
  jenkins:
    image: jenkins/jenkins:lts
    ports:
      - "8080:8080"
    volumes:
      - /srv/jenkins/data:/var/jenkins_home
    environment:
      - JAVA_OPTS=-Djenkins.install.runSetupWizard=false
    restart: unless-stopped
# Start Jenkins
docker-compose up -d

# Get the initial admin password (first run only)
docker-compose logs jenkins | grep "initial admin password" -A 2

# Open http://your-server:8080 in a browser
On first launch, Jenkins asks for the admin password shown in the logs. After logging in, install the "suggested plugins" and then add the Generic Webhook Trigger plugin via Manage Jenkins → Plugins → Available.

Creating the Pipeline
A Jenkins "Pipeline" job is defined by a Groovy script that tells Jenkins exactly what commands to run. To create an RFS build pipeline:
  1. Click New Item on the Jenkins dashboard.
  2. Enter a name (e.g. RFS-Build), select Pipeline, click OK.
  3. In the Pipeline section, set Definition to "Pipeline script" and paste this:
pipeline {
    agent any
    environment {
        GITEA_URL   = "https://git.eaw.app"
        REPO_URL    = "https://git.eaw.app/eaw/tzpuFusionX.git"
        GITEA_TOKEN = credentials('gitea-api-token')
        GITEA_OWNER = "eaw"
        GITEA_REPO  = "tzpuFusionX"
    }
    triggers {
        GenericTrigger(
            genericVariables: [[key: 'ref', value: '$.ref']],
            causeString: 'Triggered by Gitea push to $ref',
            token: 'rfs-build-trigger',
            regexpFilterText: '$ref',
            regexpFilterExpression: '^refs/heads/(main|master)$'
        )
    }
    stages {
        stage('Checkout') {
            steps {
                cleanWs()
                git url: "${REPO_URL}", branch: 'master'
                sh 'git submodule update --init --recursive'
            }
        }
        stage('Build Assembly ROMs') {
            steps {
                sh 'chmod +x build.sh && mkdir -p software/tmp software/roms'
                sh './build.sh --asm'
            }
        }
        stage('Package') {
            steps {
                script {
                    def ver = readFile('VERSION').trim()
                    sh "cd software/roms && tar czf ../../FusionX-ROMs-v${ver}.tar.gz *.rom *.mzf"
                    archiveArtifacts artifacts: "FusionX-ROMs-v${ver}.tar.gz"
                }
            }
        }
    }
    post {
        success { mail to: 'your-email@example.com', subject: "RFS Build - SUCCESS", body: "Build completed." }
        failure { mail to: 'your-email@example.com', subject: "RFS Build - FAILED", body: "Check console output." }
        always  { cleanWs() }
    }
}
This is a simplified pipeline that builds only the Z80 assembly ROMs. For the full FusionX pipeline — which also builds TZFS, CP/M, kernel modules, CPLD bitstreams, and creates Gitea releases — see the FusionX Developer's Guide — Continuous Integration section. That guide also covers the ARM cross-compiler setup, Quartus Docker installation, sibling container path translation, and the complete pipeline script.

Gitea Webhook
A webhook tells Gitea to notify Jenkins whenever code is pushed. In your Gitea repository, go to Settings → Webhooks → Add Webhook → Gitea and set:
  • Target URL: http://your-server:8080/generic-webhook-trigger/invoke?token=rfs-build-trigger
  • Content Type: application/json
  • Trigger On: Push Events
After saving, push a commit to master and check Jenkins — a new build should appear automatically. Click on the build number and then Console Output to watch the progress in real time.

Reference Sites

Resource Link
RFS project page /sharpmz-upgrades-rfs/
RFS User Manual /sharpmz-upgrades-rfs-usermanual/
RFS Technical Guide /sharpmz-upgrades-rfs-technicalguide/
RFS Gallery /sharpmz-upgrades-rfs-gallery/
SFD-700 mkII Developer’s Guide /sfd700-developersguide/
picoZ80 Developer’s Guide /picoz80-developersguide/
FusionX Developer’s Guide /tranzputer-fusionx-developersguide/
GLASS Z80 Assembler Bundled in tools/glass-0.5.1.jar
Zilog Z80 CPU User Manual Standard datasheet — bus timing, instruction set, register reference
CP/M 2.2 Alteration Guide Digital Research — CBIOS design reference