tranZPUter Filing System — Developer's Guide
TZFS Developer's Guide
This guide is a detailed walkthrough of the TZFS (tranZPUter Filing System) 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 TZMM bank-switching architecture, and shows how to add new commands, modify existing modules, and port TZFS to new hardware platforms.
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 TZFS 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 TZFS:
| 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). |
LD dest, src— Load (copy) data.LD A, Bcopies B into A.LD A, (HL)reads the byte at the memory address held in HL into A.LD (0x1200), Awrites A to memory address 0x1200.CALL addr— Call a subroutine. Pushes the return address (next instruction) onto the stack and jumps toaddr. Equivalent to a function call.RET— Return from subroutine. Pops the return address off the stack and jumps to it.JP addr— Unconditional jump toaddr.JP Z, addrjumps 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 LOOPrepeats 10 times.ADD A, n— Add n to A.SUB nsubtracts.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 MMCFG mode register, K64F service port, 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.
The Z80 offers several ways to specify where data comes from or goes to:
GLASS Assembler Syntax
- 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 TZFS for accessing fields within fixed-format data structures such as the K64F service block.
TZFS 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. EQUdefines a constant:MMCFG EQU 060H— the assembler replaces every occurrence of MMCFG with 0x60.DB(Define Byte) inserts raw bytes:DB 0x41, 0x42emits two bytes. Used for strings and lookup tables.DW(Define Word) inserts 16-bit little-endian values:DW HANDLERemits the address of the HANDLER label.ORG addrsets the assembly origin — subsequent code is assembled as if it lives ataddr.INCLUDE "file.asm"textually includes another file at the current position.IF / ENDIFconditional assembly:IF BUILD_FUSIONX = 1 ... ENDIF— the enclosed instructions are only assembled when the condition is true. This is how TZFS builds platform-specific firmware variants from one source tree.
Source Tree
| Path | Contents |
|---|---|
asm/ |
All Z80 assembly source files |
asm/tzfs.asm |
Bank 0: primary entry point, cold-start initialisation, command dispatcher, jump tables |
asm/tzfs_bank2.asm |
Bank 1 (TZMM_TZFS2): messages, help screen, print routines, Sharp↔ASCII conversion |
asm/tzfs_bank3.asm |
Bank 2 (TZMM_TZFS3): memory utilities, I/O port R/W, tape compensation, CPU/emulation commands |
asm/tzfs_bank4.asm |
Bank 3 (TZMM_TZFS4): full Z80 assembler and disassembler (occupies 52 KB of user RAM) |
asm/include/ |
Shared definitions and configuration files |
asm/include/tzfs_definitions.asm |
All configuration constants and I/O port definitions |
asm/include/tzfs_variables.asm |
Z80 variable declarations |
asm/include/tzfs_mondef.asm |
Monitor-specific definitions |
asm/include/tzfs_svcstruct.asm |
K64F service command and structure definitions |
asm/include/tzfs_utilities.asm |
Inline utility macros |
asm/include/macros.asm |
Common macros shared across modules |
asm/monitor_SA1510.asm |
SA-1510 monitor firmware for the MZ-80A |
asm/monitor_80c_SA1510.asm |
SA-1510 patched for 80-column mode |
asm/monitor_1Z-013A.asm |
MZ-700 1Z-013A monitor firmware |
asm/monitor_80c_1Z-013A.asm |
1Z-013A patched for 80-column mode |
asm/MZ80B_IPL.asm |
MZ-80B IPL (Initial Program Loader) |
build.sh |
Top-level build script |
tools/ |
Build tools including glass.jar (GLASS Z80 assembler) |
config/ |
CP/M disk format definitions |
releases/ |
Pre-built release binaries |
Configuration: tzfs_definitions.asm
This is the central configuration file, included by every other source file. Every assembly-time option is controlled here. The key sections:
Build Target Flags
BUILD_FUSIONX EQU 0 ; 1 = running on tranZPUter FusionX hardware BUILD_PICOZ80 EQU 1 ; 1 = running on picoZ80 board ENADEBUG EQU 0 ; 1 = enable additional diagnostic output during operation
The combination of
Address Constants
BUILD_FUSIONX and BUILD_PICOZ80 controls which command set is assembled. When both are 1 (FusionX + picoZ80 combined build), the MZ hardware emulation commands (MZ80K, MZ700, etc.), the CPU switching commands (T80, Z80, ZPU), and the video control commands (VBORDER, VMODE, VGA) are excluded, because those commands are not meaningful on that combined target.
UROMADDR EQU 0E800H ; Base address of the TZFS entry point (User ROM window) TZVARMEM EQU 0EC80H ; TZFS variable block (WARMSTART flag, model, tape comp. value) TZSVCMEM EQU 0ED80H ; K64F service communication block (shared memory API) BANKRAMADDR EQU 02000H ; Base address for bank4 assembler/disassembler tables
The most important address distinction in TZFS:
TZMM Mode Constants
UROMADDR (0xE800) is the fixed entry point that the native monitor calls, while TZVARMEM (0xEC80) and TZSVCMEM (0xED80) live in the tranZPUter SRAM area above the monitor RAM, always visible regardless of which TZMM mode is active.
MMCFG EQU 060H ; I/O port address of the memory management configuration register TZMM_TZFS EQU 022H ; Mode 0x22: bank 0 — main TZFS code at 0xE800-0xFFFF TZMM_TZFS2 EQU 023H ; Mode 0x23: bank 1 — tzfs_bank2 mapped at 0xF000-0xFFFF TZMM_TZFS3 EQU 024H ; Mode 0x24: bank 2 — tzfs_bank3 mapped at 0xF000-0xFFFF TZMM_TZFS4 EQU 025H ; Mode 0x25: bank 3 — tzfs_bank4 mapped over full user range
A single
Character and Control Definitions
OUT (MMCFG), A instruction selects which 64 KB SRAM block is visible in the Z80 address space. This is the entire bank-switching mechanism — there is no coded latch, no unlock sequence, and no multi-step protocol as in some other designs.
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
These named constants appear throughout the source wherever the code needs to output control characters. Using named constants rather than raw hex values keeps the source readable and makes it straightforward to locate all uses of, for example, the carriage return character with a simple search.
Bank Switching in Detail
Bank switching is the core architectural mechanism of TZFS. Understanding it is essential before modifying any source file. The TZFS approach is architecturally different from RFS — larger, simpler, and more powerful.
Why Banking is Needed
The Sharp MZ's native monitor ROM occupies 0x0000–0xDFFF. The User ROM window at 0xE800–0xEFFF gives TZFS only 2 KB of permanently visible address space — far too small for a full filing system, interactive assembler, disassembler, memory editor, hardware emulation controller, and K64F service API. Banking allows TZFS to present different code modules in the same address range on demand, multiplying the effective code space well beyond the 2 KB physical window.
The TZMM Switching Mechanism
The tranZPUter hardware contains 512 KB of SRAM organised as eight 64 KB blocks. A hardware register — the Memory Management Configuration register at I/O port 0x60 — selects which SRAM block (or blocks) are visible in the Z80 address space and at which addresses. Writing a TZMM mode value to this port takes effect immediately on the next memory cycle.
TZFS uses four TZMM modes. Their effect on the address space is:
| TZMM Mode | Value | 0xE800–0xEFFF | 0xF000–0xFFFF | Used by |
|---|---|---|---|---|
| TZMM_TZFS | 0x22 | TZFS core (bank 0) | TZFS core (bank 0) | Normal operation; command dispatch |
| TZMM_TZFS2 | 0x23 | TZFS core (bank 0) | tzfs_bank2 (bank 1) | Help, messages, print routines |
| TZMM_TZFS3 | 0x24 | TZFS core (bank 0) | tzfs_bank3 (bank 2) | Memory utils, I/O, emulation |
| TZMM_TZFS4 | 0x25 | — | tzfs_bank4 (bank 3) | Assembler/disassembler (full user RAM) |
A critical point: in modes TZMM_TZFS, TZMM_TZFS2, and TZMM_TZFS3, the 0xE800–0xEFFF window always maps to the same TZFS core code (bank 0). Only the 0xF000–0xFFFF window switches. This means the bank-switching stubs in
Comparison with RFS Bank Switching
tzfs.asm are always reachable regardless of which mode is active — there is no risk of the switching code disappearing when a bank switch occurs.
TZMM_TZFS4 is the exception: it maps bank 3 over the entire user RAM area including 0x1200–0xCFFF, which is used for the assembler/disassembler's large opcode and instruction tables. While TZMM_TZFS4 is active, normal user program RAM is not accessible. The dispatcher restores TZMM_TZFS before returning to the monitor.
RFS switches pages within a single 512 KB Flash ROM using a 2 KB window — each bank is exactly 2 KB. The RomDisk v2+ board adds a coded latch that requires a multi-step unlock sequence before the bank number can be written. This protects against accidental switches but adds code overhead to every cross-bank call.
TZFS switches entire 64 KB SRAM blocks using a single I/O port write. There is no coded latch, no unlock sequence, and no minimum bank size constraint. A TZFS bank can be 4 KB (tzfs_bank2, tzfs_bank3) or 52 KB (tzfs_bank4). The trade-off is that TZFS requires the tranZPUter hardware's memory management unit — it cannot run on a plain RomDisk card.
Command Table Format (tzfs.asm)
The monitor command dispatcher in
tzfs.asm uses a compact command table with the same format as RFS. Each entry describes one command:
; One TZFS 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: 0=TZMM_TZFS, 1=TZMM_TZFS2, 2=TZMM_TZFS3, 3=TZMM_TZFS4.
; Bits 2:0 Length of the command string in bytes.
;
; Example — the DASM command (lives in bank 3 / TZMM_TZFS4, 4-char string):
CMDTABLE:
DB 000H | 000H | 018H | 004H ; FLAGS: not-end, not-exact, bank 3 (0x18), length 4
DB "DASM" ; Command string
DW ?DASM ; Handler — the inter-bank stub for DASM_MAIN
The dispatcher reads the monitor input line, walks the table, and for each entry:
- Compares the input against the command string.
- If matched, extracts the bank number (TZMM mode index) and handler address from the table entry.
- Performs the TZMM mode switch by writing the appropriate value to port 0x60.
- Calls the handler with any remaining input (parameters) available in the monitor input buffer.
- Restores TZMM_TZFS on return and passes control back to the monitor.
Jump Tables
TZFS uses two distinct jump table mechanisms. Both are fixed-address tables of
External Jump Table at 0xE880 (TZFSJMPTABLE)
JP instructions whose addresses never change regardless of which TZMM mode is active.
The native monitor firmware (SA-1510 for the MZ-80A, 1Z-013A for the MZ-700, and the MZ-80B IPL) need to call TZFS services — specifically the tape/SD I/O routines that replace the hardware CMT interface. These monitor ROMs were not written to know TZFS's internal structure. Instead, TZFS publishes a set of fixed-address entry points starting at 0xE880 (UROMADDR + 0x80). Each entry is a single
JP instruction:
CMT_RDINF EQU UROMADDR+80H ; 0xE880 — Read tape/SD file header CMT_RDDATA EQU UROMADDR+83H ; 0xE883 — Read tape/SD file data body CMT_WRINF EQU UROMADDR+86H ; 0xE886 — Write tape/SD file header CMT_WRDATA EQU UROMADDR+89H ; 0xE889 — Write tape/SD file data body CMT_VERIFY EQU UROMADDR+8CH ; 0xE88C — Verify tape data against memory CMT_DIR EQU UROMADDR+8FH ; 0xE88F — SD card directory listing CMT_CD EQU UROMADDR+92H ; 0xE892 — SD card change directory SET_FREQ EQU UROMADDR+95H ; 0xE895 — Set CPU operating frequency
Each
Inter-Bank Function Stubs (? prefix)
JP at these addresses dispatches to the real TZFS implementation, which may itself perform a TZMM bank switch to reach the appropriate handler. Monitor firmware that previously called its internal tape routines now calls these fixed addresses instead. This is how TZFS transparently intercepts all tape operations and redirects them to the SD card.
Every function that needs to be called from a bank other than the one it lives in has a corresponding ?-prefixed stub in the jump table section of
tzfs.asm. For example, PRINTMSG lives in tzfs_bank2 (TZMM_TZFS2), but the command dispatcher in bank 0 needs to call it. The stub ?PRINTMSG in bank 0:
- Preserves the current TZMM mode (reads and saves the active mode value).
- Writes TZMM_TZFS2 to port 0x60, mapping tzfs_bank2 into 0xF000–0xFFFF.
- Calls the actual
PRINTMSGroutine at its address in tzfs_bank2. - Restores the saved TZMM mode by writing it back to port 0x60.
- Returns to the original caller.
CALL ?PRINTMSG
and the bank switch is handled transparently. The
? prefix is a naming convention — not special assembler syntax — that visually identifies every cross-bank call in the source.
Because the stubs live in bank 0 at fixed addresses in the 0xE800–0xEFFF window, they are reachable from any TZMM mode — even TZMM_TZFS4, which replaces most of user RAM. This is why the E800–EFFF window is never switched: it must always contain the stubs and jump tables.
Module Walkthroughs
tzfs.asm — Command Dispatcher (Bank 0, TZMM_TZFS)
Role: The entry point for all TZFS functionality. When the native monitor (SA-1510, 1Z-013A, etc.) encounters a command it does not recognise, it passes control to the User ROM entry point at 0xE800. This module is always bank 0, always mapped, and always reachable.
Cold start vs warm start: At 0xE800, TZFS first checks the WARMSTART flag in TZVARMEM (0xEC80). If the flag is clear (power-on or hard reset), TZFS performs a full cold-start sequence:
tzfs_bank2.asm — Messages and Help (Bank 1, TZMM_TZFS2)
- Writes TZMM_TZFS to port 0x60 to ensure the correct SRAM block is active.
- Calls the K64F service to load the appropriate monitor firmware (LOAD40ABIOS or LOAD700BIOS40) into tranZPUter SRAM at 0x0000.
- Clears the TZFS variable block at TZVARMEM.
- Sets the WARMSTART flag so subsequent entries skip initialisation.
- Cold starts the loaded monitor firmware.
- External jump table stubs (0xE880–0xE8xx): The
CMT_RDINF,CMT_RDDATA,CMT_WRINF,CMT_WRDATA,CMT_VERIFY,CMT_DIR,CMT_CD, andSET_FREQJP instructions called by monitor firmware. - Inter-bank stub table (TZFSJMPTABLE): All
?-prefixed stubs — one per cross-bank function. New functions added to tzfs_bank2/3/4 must have their stub added here. - Command table (CMDTABLE): The list of all TZFS commands with their bank index and handler address. Platform-specific commands are conditionally assembled.
- Main dispatcher loop: Reads the monitor's input buffer, walks CMDTABLE, performs the TZMM mode switch, and calls the handler. If no command matches, returns to the native monitor to print its error response.
- Variable storage: The WARMSTART flag, current machine model code, and tape compensation value are stored in the TZVARMEM block. The K64F service shared memory lives at TZSVCMEM (0xED80).
Role: All user-facing text output: character set conversion, string printing, filename display, and the help screen. Assembled with
tzfs_bank3.asm — Utilities (Bank 2, TZMM_TZFS3)
ORG 0xF000 — it occupies 0xF000–0xFFFF when TZMM_TZFS2 is active.
Key functions:
- PRINTASCII: Converts Sharp MZ character codes to ASCII before output. Codes in the range 0x00–0x7F are looked up in the
ATBLconversion table; codes ≥ 0x80 and the carriage return pass through unchanged. This is necessary because the Sharp MZ series uses a non-standard character encoding where many printable characters have different code points from ASCII. - PRINTMSG: Prints a string with embedded escape codes that allow formatted multi-value output: code 0xFF pushes a value from the stack, 0xFE/0xFD/0xFC print 1/2/3 values respectively from the stack as hex, and 0xFB prints the BC register pair. This eliminates the need for bespoke print routines for every message that includes a variable value.
- PRTFN: Prints a Sharp MZF filename — a fixed 17-byte field stored in Sharp character encoding. Calls PRINTASCII for each character.
- PRTSTR: Prints a null-terminated string directly to the screen.
- HELPSCR: The complete help screen text stored as CR-terminated strings, one per line. Conditionally assembled: lines describing hardware emulation, CPU switching, and video control commands are excluded when both BUILD_FUSIONX and BUILD_PICOZ80 are 1.
Role: The main utilities bank — memory editing, hex dump, block copy, fill, I/O port access, tape compensation, hardware emulation control, CPU switching, and video mode control. Assembled with
tzfs_bank4.asm — Assembler / Disassembler (Bank 3, TZMM_TZFS4)
ORG 0xF000 — it occupies 0xF000–0xFFFF when TZMM_TZFS3 is active.
Key functions:
- SKIPCOMMA: Scans the monitor input buffer for a comma delimiter. Returns with the Z flag set if no comma is found, with HL pointing past the comma if one is found. Used by all commands that accept multiple parameters separated by commas.
- CHECKMODEL: Maps machine model character codes — K (MZ-80K), C (MZ-80C), 1 (MZ-1200), A (MZ-80A), 7 (MZ-700), 8 (MZ-800), B (MZ-80B), 2 (MZ-2000) — to binary model numbers 0–7. Used by the hardware emulation commands.
- MCORX (?MCORX): The interactive memory editor (M command). Presents each byte at the current address, shows the address and current value in hex, and accepts a new hex byte or Enter to leave unchanged. Advances through memory byte by byte. Ctrl+C exits.
- DUMP* (?DUMPX): The hex dump (D command). Reads the start and end addresses from the input, then for each 16-byte line prints the 4-digit hex address, 16 hex byte values, and 16 ASCII character equivalents (dot for non-printable). Calls PRINTASCII for character display.
- COPYM (?COPYM): Block memory copy (CP command). Accepts source address, destination address, and byte count. Performs a safe overlapping-aware copy.
- FILL (?FILL): Fill a memory range with a constant byte (FILL command). Accepts start address, end address, and fill value.
- TAPECOMP (?TAPECOMP): Tape delay compensation (TC command). Reads or writes the compensation value stored in TZVARMEM. Increasing this value lengthens the bit-timing windows to compensate for mechanical wear on vintage tape decks with slow or uneven motor speed.
- READIO (?READIO): Read an I/O port (RIO command). Accepts a port address, performs
IN A, (C), and prints the result as hex. - WRITEIO (?WRITEIO): Write an I/O port (WIO command). Accepts a port address and byte value, performs
OUT (C), A. - Hardware emulation handlers (?SETMZ80K etc.): Each handler sets the appropriate K64F service command (e.g. TZSVC_CMD_EMU_SETMZ80K) and triggers the service request. Conditionally assembled — excluded when BUILD_FUSIONX + BUILD_PICOZ80 = 1.
- CPU switch handlers (?SETZ80, ?SETT80, ?SETZPUEVO): Ask the K64F to load any required firmware then signal the FPGA to activate the requested CPU. T80 is a soft-core Z80 implemented in the FPGA; ZPU Evolution is also an FPGA soft CPU. Conditionally assembled.
- Video control handlers (?SETVMODE, ?SETVGAMODE, ?SETVBORDER): Call K64F video control services. Conditionally assembled.
Role: A full interactive Z80 assembler and disassembler. This bank is architecturally unique: it does not fit in the 4 KB 0xF000–0xFFFF window. Instead, TZMM_TZFS4 maps bank 3 over the entire user RAM area (0x1200–0xCFFF) as well as 0xF000–0xFFFF. The assembler/disassembler opcode and instruction tables require this 52 KB space; no user program RAM is available while this bank is active.
Assembled with
ORG 0x2000 (BANKRAMADDR), the code physically occupies two regions: 0x1200–0xCFFF for the large lookup tables, and 0xF000–0xFFFF for the executable code itself.
Key functions:
- DASM_MAIN (?DASM): Full Z80 disassembler (DASM command). Reads start and end addresses from the input buffer. For each instruction, displays the hex address, the raw machine code bytes, and the decoded mnemonic with operands. Handles all Z80 prefix bytes (0xCB, 0xDD, 0xED, 0xFD) and all extended instruction groups, including the full IX/IY displacement addressing modes. The opcode decoding tables are derived from the TASM Z80 assembler reference.
- ASM_MAIN (?ASM): Full interactive Z80 assembler (ASM command). Prompts the user at each successive address. The user types a Z80 mnemonic with operands (e.g.
LD HL, 1234H), the assembler parses the mnemonic and operand fields, looks up the encoding in the instruction tables, assembles the machine code bytes, writes them to the target RAM address, and advances to the next address. Pressing Enter on a blank line exits. - Local workspace: COUNT_C, ADDR_LO/HI, ASM_ADDR (8 bytes), ASM_BUF (16 bytes), and various parameter and value buffers occupy fixed addresses within the bank's address space. These are separate from the TZVARMEM variable block and exist only while TZMM_TZFS4 is active.
K64F Service Calls
The tranZPUter hardware contains a Kinetis K64F ARM microcontroller that manages SD card access and monitor firmware loading. The Z80 communicates with the K64F through a shared memory block at TZSVCMEM (0xED80) and a single I/O port (SVCREQ, 0x68). The service API is defined in
tzfs_svcstruct.asm. For CPU switching and hardware emulation, the K64F's role is limited to loading the required firmware or ROM before the FPGA activates the mode — the T80, ZPU Evolution, and all Sharp MZ hardware emulation are implemented entirely in the FPGA, not in the K64F.
The call sequence from Z80 code is:
; Step 1 — Set the command and any parameters in the service block.
LD A, TZSVC_CMD_LOADFILE
LD (TZSVCCMD), A ; Write the command byte
; Step 2 — Signal request to the K64F.
LD A, TZSVC_STATUS_REQUEST
LD (TZSVCRESULT), A ; Set result field to "request pending"
OUT (SVCREQ), A ; Trigger K64F interrupt — the K64F polls this port
; Step 3 — Poll the result field until the K64F completes the operation.
WAIT:
LD A, (TZSVCRESULT)
CP TZSVC_STATUS_REQUEST ; Still pending?
JR Z, WAIT ; Yes — keep waiting
; Step 4 — Check result. TZSVC_STATUS_OK (0x00) = success, non-zero = error.
LD A, (TZSVCRESULT)
OR A
JR NZ, ERROR
The K64F responds within milliseconds for SD card operations (file load) and somewhat faster for firmware loading operations that precede a CPU or emulation switch. The polling loop is safe with interrupts enabled — the Z80 is not doing useful work during the wait, and the K64F's interrupt is edge-triggered on the port write, not level-sensitive.
Key service commands (from tzfs_svcstruct.asm):
TZSVC_CMD_LOADFILE ; Load a named file from SD card into Z80 RAM TZSVC_CMD_SAVEFILE ; Save Z80 RAM region to a named file on SD card TZSVC_CMD_DIR ; Read directory listing from SD card TZSVC_CMD_SD_RDPAGE ; Read raw SD sector TZSVC_CMD_SD_WRPAGE ; Write raw SD sector TZSVC_CMD_EMU_SETMZ80K ; Switch hardware emulation to MZ-80K mode TZSVC_CMD_EMU_SETMZ80A ; Switch hardware emulation to MZ-80A mode TZSVC_CMD_EMU_SETMZ700 ; Switch hardware emulation to MZ-700 mode TZSVC_CMD_EMU_SETMZ800 ; Switch hardware emulation to MZ-800 mode TZSVC_CMD_EMU_SETMZ80B ; Switch hardware emulation to MZ-80B mode TZSVC_CMD_CPU_SETZ80 ; Set active CPU to hardware Z80 TZSVC_CMD_CPU_SETT80 ; Load firmware then signal FPGA to activate T80 (FPGA soft-core Z80) TZSVC_CMD_CPU_SETZPUEVO ; Load firmware then signal FPGA to activate ZPU Evolution (FPGA soft CPU) TZSVC_CMD_SET_VIDMODE ; Set video output mode TZSVC_CMD_SET_VGAMODE ; Set VGA output mode TZSVC_CMD_SET_VBORDER ; Set video border colour
Parameters and return values are passed through additional fields in the TZSVCMEM block. The layout of this block is defined by the
TZSVCSTRUCT structure in tzfs_svcstruct.asm. For file operations, the filename is written as a null-terminated ASCII string at the TZSVCFNM offset before issuing the command.
Booting Floppy Disks from TZFS (GETBOOTDSK)
From TZFS v1.8.3, TZFS can boot an MZ-80A / MZ-700 / MZ-800 floppy disk — including MZ-800 CP/M — directly from within the filing system, and return to TZFS on the hardware RESET switch after the floppy-booted OS has run. The routine that does this is
Machine-ID Acceptance and IPLPRO Detection
GETBOOTDSK. It re-implements, in the User ROM window, the behaviour of the machine's own IPL (the native 9Z-504M disk IPL) closely enough that a second-stage boot loader on the disk cannot tell it is running under TZFS rather than the factory monitor. This section documents the developer-level detail of that routine and the memory modes that support it.
The first sector of a bootable Sharp disk carries a machine identifier and the ASCII signature
Block-7 Pre-Page for OSes that Load Below 1000H
"IPLPRO". Earlier TZFS accepted only a single hard-wired machine id; GETBOOTDSK now accepts any valid Sharp machine id — 01 (MZ-2000), 02 (MZ-80A/MZ-80K) and 03 (MZ-700/MZ-800) — and validates the boot sector by matching the "IPLPRO" signature rather than by assuming a fixed machine. This is what lets one TZFS build boot the disk sets of all three machines: the machine-id byte selects the load/run behaviour, and the signature match is the go/no-go test for "this is a bootable disk". A sector that carries neither a recognised id nor the signature is rejected and control returns to the TZFS prompt.
Some disk OSes — MZ-800 CP/M in particular — load their first stage below address
BC = 0200H Hand-off
1000H, into the region that normally holds the monitor's work area and the low DRAM. Before handing over, GETBOOTDSK pre-pages block 7 of DRAM into 0000H-0FFFH so that the loaded program sees native read/write DRAM there instead of the monitor image. This mirrors the machine's own IPL, which unmaps the boot ROM and exposes DRAM before the loaded program begins executing, and it is the same low-RAM aliasing fix applied generally on the picoZ80 (mapping 0000H-0FFFH from bank 7 under TZFS) so that a program which LDIRs code into low RAM and then switches video mode sees identical memory on both the native and the TZFS paths.
The native
Taming the 8253 Clock (SORES) Before JP (HL)
9Z-504M IPL hands the loaded program control with the register pair BC = 0200H — a second-stage loader relies on this value (a 512-byte sector count / directory-read parameter) to drive its own directory read. GETBOOTDSK reproduces the hand-off exactly: it sets BC = 0200H immediately before the JP (HL) that transfers control to the freshly loaded first stage, so a loader that reads its directory using the value it finds in BC behaves identically under TZFS. Getting this wrong is the difference between a disk that boots and one that stalls at "no system file" because its loader read the wrong number of sectors.
The Sharp's 8253 programmable interval timer drives a periodic interrupt. A disk loader typically re-enables interrupts (
Running the OS: TZMM_DSKLOAD / TZMM_DSKRUN
EI) early in its own start-up, and if the 8253 is still free-running at its native rate the resulting stream of RST 38H interrupts can flood the Z80 and abort the loader before it finishes. The native IPL avoids this in its SORES (sound reset) routine; GETBOOTDSK mirrors it. Immediately before the JP (HL) hand-off it:
- Reprograms 8253 counter 2 to roughly 1 Hz, so the interrupt source ticks slowly instead of at its native frequency, and
- Masks 8255 port C bit 2 (PC2), the gate that lets the counter output reach the interrupt line.
EI in the CP/M loader previously let a fast-running counter interrupt it to death before it could read its system file.
Two dedicated memory-management modes carry the boot through to a running OS. Under the picoZ80 firmware, TZMM_DSKLOAD and TZMM_DSKRUN load and then run a floppy-booted OS in the machine's native block-0 DRAM — exactly the memory layout the machine's own IPL would use — rather than in a TZFS SRAM bank. TZFS
Returning to TZFS on Hardware Reset
DSKREAD streams the boot sectors into that DRAM; on the MZ-800 this is helped by the MZ-1E05 MZ1E05_IO_A10Toggle FDC hardware-acceleration fetch handler, which ORs the FDC DRQ line with address bit A10 at 0xF3FE / 0xF7FE so that DSKREAD can stream a boot sector on the MZ-800 exactly as it already does on the MZ-80A. Once the sectors are in native DRAM, TZMM_DSKRUN is selected and control is transferred, so the OS runs against real block-0 memory with the monitor and TZFS User ROM out of the way.
Because block-0 ROM and RAM alias the same PSRAM on the picoZ80, an OS running under
TZMM_DSKRUN clobbers the block-0 monitor and TZFS User ROM images as it uses low memory. When the hardware /RESET switch is pressed, the firmware's reset-restore path re-loads the pristine block-0 monitor plus the TZFS UROM and then cold-boots TZFS, so the reset switch returns the user to the TZFS prompt instead of dropping to the bare 1Z-013A monitor. Two picoZ80 details keep the video correct across this transition: the MZ-800 cgWindow tracking lets an MZ-800-mode OS that loads a PCG font (via IN 0xE0) reach the real CG-ROM / CG-RAM under TZFS — fixing garbled disk-BASIC text while leaving native games such as Flappy unaffected — and the reset path re-runs initVideoText once /RESET releases (after a ~10 ms settle delay for the on-board GDG / 8255 to finish their hardware reset) so the GDG rescans and the monitor's HBLK sync completes cleanly.
The Quick Disk Engine (Q and QD Commands)
From TZFS v1.8.4, TZFS drives a Sharp Quick Disk over its Z80 SIO protocol. Two monitor commands expose it:
SIO Init, Hunt and Read Sequence
QD lists the Quick Disk directory, and Q loads and runs the first file on the disk. Both commands work identically against a real MZ-1F11 QD drive and against the picoZ80 virtual QD (an SD-backed image / virtual disk). The engine is ported from the RFS / MZ-1E14 QD engine and then hardened for real hardware — a real MZ-1F11 has none of the tolerance of an emulator, so the port adds a strict init/hunt/read discipline, a whole-operation interrupt lockout, and deferred screen output. This section documents that engine at the developer level; the TZFS help screen also lists Q and QD.
The QD interface is a Z80 SIO on ports
Whole-Operation DI and 8253 Post-DI Taming
F4-F7 (channel A data/command, channel B data/command). The engine's read path is a fixed init → hunt → read sequence, each step tuned to the way a real drive presents its bitstream:
- SIO interrupt-disable baseline: the SIO is first programmed to a known state with all SIO-generated interrupts disabled, so no channel interrupt can fire mid-transfer. This is the register baseline every subsequent step builds on.
- Pass-start (#HOME edge) alignment: before hunting for data the engine waits for the #HOME edge (channel B RR0 reports HOME) so that it begins a read at the start of a disk pass rather than at an arbitrary point in the media rotation. Starting mid-pass is the classic cause of a hunt that never syncs.
- First hunt without channel re-reset: the very first hunt after alignment does not re-reset the SIO channel — re-issuing the channel reset at that point drops the receiver out of hunt phase and loses the leading sync bytes. The engine enters hunt directly and lets the SIO's own sync-hunt logic lock on.
- Full-pass hunt window with A5 address-mark validation: the hunt is allowed to run for a whole disk pass rather than a short fixed timeout, and a candidate sync is only accepted when the A5 address mark is seen — an A5 validation gate that rejects false syncs from noise or partial marks before the engine commits to reading the block.
qdprobe debug-shell command reports them: channel A RR0 carries the receive/HDST/hunt bits, channel B RR0 carries the HOME bit. A developer validating real-hardware timing can replay this exact init/hunt/read sequence at a settable inter-access pace with qdprobe eng [pace_us] from the picoZ80 debug shell.
Quick Disk transfer is bit-timing critical: a single
Deferred Prints (No Screen I/O Mid-Stream)
RST 38H interrupt taken mid-block corrupts the read. The engine therefore wraps the entire QD operation — not just the innermost loop — between DI and EI, so no interrupt is serviced for the whole init/hunt/read span. Because the Sharp's 8253 programmable interval timer is still free-running while interrupts are disabled, its output can latch a pending interrupt that fires the instant EI is reached. The engine applies the same 8253 taming used by GETBOOTDSK's SORES path (see Taming the 8253 Clock): after the DI window it reprograms / masks the counter so that the interrupt source cannot swamp the Z80 as normal interrupt handling resumes. This is the "8253 post-DI taming" step — DI protects the transfer, and the post-DI taming protects the hand-back.
Screen output on the Sharp is not free — a
TZMM_QDLOAD Block-0 Mapping
PRINTASCII / monitor print touches video RAM and can take long enough to miss the next QD bit. The engine therefore performs no screen I/O mid-stream: filenames, progress, and directory entries are accumulated during the DI-protected read and printed only after the transfer completes and interrupts are safely restored. All prints are deferred to the tail of the operation. This is why the QD directory listing appears as a batch when the pass finishes rather than scrolling as each entry is read, and it is essential on a real MZ-1F11 where a mid-stream print would drop the receiver out of sync.
The
Bank-4 F000 Placement and tcycwait Pacing
Q command loads the first file and then hands off to the loaded program's exec address. For that hand-off to run the program correctly, the loaded bytes must land in the memory the exec address expects — native low RAM, not a TZFS SRAM bank. The engine selects a dedicated memory-management mode, TZMM_QDLOAD: a TZFS4-style map with low RAM present in block 0. Loading under TZMM_QDLOAD places the program where the exec hand-off runs it — the same block-0 DRAM aliasing principle used by the floppy boot path's TZMM_DSKLOAD/TZMM_DSKRUN modes, applied to the Quick Disk load so a QD program sees identical low memory on the native and TZFS paths.
The QD engine code lives in the bank-4 0xF000 window and is deployed at file position
Real-Mode SIO Passthrough Pacing
0xF600 in that ROM region. This region carries a per-region wait-state setting: the QD engine ROM region is deployed with tcycwait=1 so the Z80 runs the SIO accesses with an authentic real-Z80 memory-cycle pace, which the real SIO recovery timing depends on. This is deliberately not applied everywhere — the CMT-calibrated bank-2 slice and the DASM / ASM region stay at zero wait states so their timing-sensitive tape work and the interactive assembler/disassembler run at full speed. When porting the engine or relocating it, preserve both the 0xF600 file position and the tcycwait=1 attribute on that region.
When the QD interface is switched to Real mode (pure passthrough to a physically attached MZ-1F11, with the firmware snooping read-only), the real QD SIO passthrough is paced: a 10 µs inter-access recovery floor is enforced on ports
F4-F7. The Z80 SIO needs several clock cycles of recovery between accesses, and without the floor a back-to-back passthrough access can be issued before the device has recovered. Virtual mode (firmware emulates the drive from an SD-backed image) does not need the floor because the emulated SIO has no recovery constraint. The same qdprobe eng [pace_us] replay is the tool for finding and confirming the minimum safe pace on a given real drive.
Adding a New Monitor Command
To add a new command — for example, PROBE that reads a range of I/O ports and prints their values — follow these steps:
- Write the handler in the appropriate bank file. For a utility command, add a labeled routine in
tzfs_bank3.asm:
PROBE:
; HL points to parameters in the monitor input buffer on entry.
CALL SKIPCOMMA ; Parse start port
... ; implementation
RET
- Add a ?-prefix stub in tzfs.asm if the handler needs to be called from bank 0 or from any other bank. In the TZFSJMPTABLE section, add:
?PROBE:
PUSH AF
LD A, TZMM_TZFS3
OUT (MMCFG), A ; Switch to bank3
CALL PROBE ; Call the handler
LD A, TZMM_TZFS
OUT (MMCFG), A ; Restore bank0
POP AF
RET
- Add an entry to CMDTABLE in tzfs.asm. Bank 2 corresponds to TZMM_TZFS3 (bits 5:3 = 010 = 0x10), length 5:
; FLAGS: not-end (bit7=0), not-exact (bit6=0), bank 2 = 0x10, length 5 = 0x05
DB 000H | 000H | 010H | 005H
DB "PROBE"
DW ?PROBE
- Add help text to HELPSCR in tzfs_bank2.asm. Add a CR-terminated string describing the new command. If the command is platform-specific, wrap it with the appropriate conditional assembly guard (
IF BUILD_FUSIONX = 1 ... ENDIF). - Run
./build.sh. The assembler will report any size overflows — if tzfs_bank3 now exceeds 0xF000–0xFFFF, remove or compress other code in that bank or move less-used routines to tzfs_bank2.
Adding a New Hardware Platform
To port TZFS to a new hardware target (for example, a new tranZPUter board revision with different I/O port assignments or additional memory management modes):
- Add a build flag in
tzfs_definitions.asm:
BUILD_NEWBOARD EQU 0 ; 1 = build for the new target board
- Add I/O port and TZMM mode constants as needed. If the new board uses a different port address for MMCFG or defines additional TZMM modes, add them in the definitions file under a conditional block:
IF BUILD_NEWBOARD = 1 MMCFG EQU 070H ; New board uses port 0x70 for memory management TZMM_TZFS5 EQU 026H ; Additional bank mode for new board ENDIF
- Add conditional assembly blocks throughout the source where the new board's behaviour differs — I/O port sequences for bank switching, K64F service command numbers, or monitor firmware identifiers. Follow the existing pattern of
IF BUILD_FUSIONX = 1 ... ENDIFblocks. - Implement the K64F service API on the new platform. The new board's co-processor must respond to the SVCREQ port write (or an equivalent interrupt mechanism), read the command and parameters from TZSVCMEM, execute the service, and write the result back. The Z80-side service call sequence requires no changes — only the ARM-side firmware changes.
- Stub out unsupported service commands initially. Have the K64F return TZSVC_STATUS_OK immediately for commands the new hardware does not yet implement. This lets the Z80 side compile and run while firmware support is developed incrementally.
- Update
build.shto add a build target for the new board, settingBUILD_NEWBOARD=1and any other required flags on the GLASS assembler command line.
Debugging Tips
Enable debug output: Set
ENADEBUG EQU 1 in tzfs_definitions.asm before building. This includes additional diagnostic messages at strategic points — particularly during cold-start initialisation, K64F service calls, and TZMM mode switches.
Probe I/O ports directly with RIO/WIO: The RIO port command reads any I/O port and prints its value as hex. WIO port,value writes a byte to any port. These commands are invaluable for verifying that the MMCFG register (port 0x60) and the K64F service port (0x68) are responding as expected, without writing test code.
Inspect the TZFS variable block: Type D EC80 to hex-dump the TZVARMEM area. The WARMSTART flag is the first byte — a non-zero value confirms TZFS has completed cold-start initialisation. The tape compensation value and current model code follow immediately.
Inspect the K64F service block: Type D ED80 to hex-dump the TZSVCMEM area. The command byte and result byte are at the start of this block. After a failed service call, the result byte will be non-zero and can be cross-referenced against the TZSVC_STATUS_* constants in tzfs_svcstruct.asm.
Inspect the TZFS entry code and jump tables: Type D E800 to dump the first 320 bytes of the TZFS core — this shows the external jump table at 0xE880 (verify the JP instructions are intact) and the beginning of the inter-bank stub table.
Verify assembled code with DASM: After using the ASM command to enter a routine interactively, use DASM addr,endaddr immediately to disassemble the freshly written bytes. Encoding errors in operand types (immediate vs indirect, 8-bit vs 16-bit) are immediately visible in the disassembly output.
Tape read failures: If tape load operations fail on vintage hardware, use the TC (tape compensation) command to increase the timing window. Start by increasing the value in steps of 10 and attempting each load until successful, then note the working value. Values that are too large cause false bit detections; too small causes missed bits on slow or worn tape mechanisms.
Bank switching verification: If a cross-bank call crashes or produces unexpected output, check that the ?-prefixed stub correctly saves and restores the TZMM mode. A common mistake is returning from the stub with the wrong mode active, causing the next memory access to read from the wrong SRAM block.
"SD Read error" on large directories: An intermittent TZFS SD Read error when listing large directories is fixed. The cause was O(N²) directory rescans that, on a cold SD cache, overran the FSPI handshake window and returned a spurious read error. The fix replaces the rescans with an ESP32 MZFDIR persistent directory cursor plus an RP2350 dir-cache retry, so a large listing no longer re-reads the whole directory per entry. If you see this error resurface after touching the directory-listing path, check that the persistent cursor and the dir-cache retry are both still in place.
Build Environment
The TZFS build uses the GLASS Z80 assembler (bundled in the repository) and the FusionX global build script. Only a Java runtime and
Prerequisites
git are needed to build the Z80 assembly components. The recommended way to build is the automated setup script for your platform (see Automated setup and build below); the manual and FusionX build steps that follow are for advanced users and partial rebuilds. For a complete build including kernel modules, CPLD bitstreams, and the full Linux image, see the FusionX Developer's Guide — Development Environment Setup.
# 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 --recursiveAutomated setup and build (recommended)
The recommended way to build TZFS is the self-contained setup script for your platform. It installs the prerequisites (Java for the GLASS Z80 assembler, plus
git/perl/coreutils; on Windows, Git Bash + Java), clones the repository, fetches the content bundle, and can build the ROM images — all interactively, with sensible defaults you can accept by pressing Enter. Copy just the single file for your platform and run it.
macOS / Linux / WSL — setup_TZFS.sh
chmod +x setup_TZFS.sh
./setup_TZFS.sh
The script installs Java (which runs the GLASS assembler,
tools/glass-0.5.1.jar), a C toolchain + make (to build the cpmtools submodule), perl and git; on macOS it also installs GNU coreutils and bash 4, adding them to PATH via a generated tzfs_env.sh. When absent, it fetches the content bundle TZFS_Files.zip (~110 MB of MZF/DSK/CPM/CAS/BAS/BASIC content).
Windows 10 / 11 — setup_TZFS_windows.ps1 (native Git Bash, no WSL). From PowerShell:
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
.\setup_TZFS_windows.ps1
The Windows script uses winget to install Git for Windows (which provides bash, coreutils, perl and curl) and the Temurin 17 JRE (Java), clones the repository, fetches the content bundle, and runs
./build.sh through Git Bash. A prebuilt tools\cpmcp.exe is bundled, so no C compiler is required on Windows.
Questions the setup asks, and what to do. Every prompt has a safe default in brackets — the capital letter is the default, so pressing Enter accepts it.
macOS / Linux / WSL (setup_TZFS.sh):
| Prompt | Default | What to do |
|---|---|---|
Install now? [Y/n] (for missing packages) |
Yes | Enter — installs the missing prerequisites; may ask for your sudo password. |
Repo URL [https://git.eaw.app/eaw/TZFS.git] |
public repo | Enter for the public repo, or paste a different (private) URL. |
Install directory [~/TZFS] |
~/TZFS |
Enter for ~/TZFS, or type a path. |
Download and install them now? [Y/n] (content bundle) |
Yes | Enter to fetch the MZF/DSK/CPM/CAS/BAS/BASIC content needed for a full build. |
Run the build now (./build.sh …)? [Y/n] |
Yes | Enter to build immediately (verifies the environment). |
Windows (setup_TZFS_windows.ps1):
| Prompt | Default | What to do |
|---|---|---|
Repo URL [https://git.eaw.app/eaw/TZFS.git] |
public repo | Enter for the public repo, or paste a different URL. |
TZFS checkout directory [%USERPROFILE%\TZFS] |
%USERPROFILE%\TZFS |
Enter for the default, or type a path. |
Download and install them … now? [Y/n] (content bundle) |
Yes | Enter to fetch the content bundle. |
Build the TZFS firmware now (runs ./build.sh via Git Bash)? [Y/n] |
Yes | Enter to build immediately through Git Bash. |
Set the environment variable
TZFS_REPO_URL to override the default repository URL without editing the script.
Output and rebuilding. ROM images are written to
Building (manual / FusionX)
roms/ — for example tzfs.rom, the FusionX variants, the monitor ROMs, and the CP/M binaries. To rebuild later, change into the checkout (cd ~/TZFS); on macOS first run source ./tzfs_env.sh to put GNU coreutils and bash 4 on PATH; then run ./build.sh (or ./build.sh -m to also reprocess the MZF sources). On Windows, run ./build.sh from Git Bash inside the checkout.
# Build TZFS ROMs for all target machines ./build.sh --tzfs # Build CP/M (CBIOS is part of the TZFS source tree) ./build.sh --cpm # Build everything (ROMs, TZFS, CP/M, drivers, CPLD) ./build.sh --all
The TZFS build assembles firmware for three target machines: MZ-80A, MZ-700, and MZ-2000. Each target produces a ROM file (
tzfs_<target>_fusionx.rom) and a test MZF file (testtz_<target>_fusionx.mzf). The assembler is invoked with target-specific flags that select the appropriate BUILD_* settings in tzfs_definitions.asm.
Build output:
| Output | Description |
|---|---|
software/roms/tzfs_mz80a_fusionx.rom |
TZFS firmware for MZ-80A |
software/roms/tzfs_mz700_fusionx.rom |
TZFS firmware for MZ-700 |
software/roms/tzfs_mz2000_fusionx.rom |
TZFS firmware for MZ-2000 |
software/roms/testtz_*.mzf |
TZFS test MZF files |
software/roms/cpm223_mz80a_80c.bin |
CP/M 2.2 for MZ-80A (80-column) |
software/roms/cpm223_mz80a_std.bin |
CP/M 2.2 for MZ-80A (standard) |
software/roms/cpm223_mz700_80c.bin |
CP/M 2.2 for MZ-700 (80-column) |
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, 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.
TZFS and CP/M 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
How It Works
master branch and automatically triggers a full build.
The automated build process for TZFS follows these steps:
- You push code to the
masterbranch of the Gitea repository. - Gitea sends a webhook (an HTTP notification) to the Jenkins server.
- Jenkins clones the repository into a fresh, clean workspace.
- Jenkins runs
./build.sh --tzfswhich assembles TZFS firmware for all target machines (MZ-80A, MZ-700, MZ-2000). - Jenkins runs
./build.sh --cpmwhich builds CP/M 2.2 binaries for each target. - Jenkins packages the output into versioned tarballs (
FusionX-TZFS-v1.08.tar.gz,FusionX-CPM-v1.08.tar.gz). - Jenkins creates a Gitea Release with the tarballs attached as downloadable assets.
- 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 a TZFS build pipeline:
- Click New Item on the Jenkins dashboard.
- Enter a name (e.g.
TZFS-Build), select Pipeline, click OK. - 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: 'tzfs-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 TZFS ROMs') {
steps {
sh 'chmod +x build.sh && mkdir -p software/tmp software/roms'
sh './build.sh --tzfs'
}
}
stage('Build CP/M') {
steps {
sh './build.sh --cpm'
}
}
stage('Package') {
steps {
script {
def ver = readFile('VERSION').trim()
sh """
mkdir -p release/tzfs release/cpm
cp software/roms/tzfs_*.rom software/roms/testtz_*.mzf release/tzfs/ 2>/dev/null || true
cp software/roms/cpm223_*.bin release/cpm/ 2>/dev/null || true
cd release/tzfs && tar czf ../../FusionX-TZFS-v${ver}.tar.gz * && cd ../..
cd release/cpm && tar czf ../../FusionX-CPM-v${ver}.tar.gz * && cd ../..
"""
archiveArtifacts artifacts: "FusionX-*-v${ver}.tar.gz"
}
}
}
}
post {
success { mail to: 'your-email@example.com', subject: "TZFS Build - SUCCESS", body: "Build completed." }
failure { mail to: 'your-email@example.com', subject: "TZFS Build - FAILED", body: "Check console output." }
always { cleanWs() }
}
}
This is a simplified pipeline that builds only the TZFS and CP/M components. For the full FusionX pipeline — which also builds monitor ROMs, kernel modules, CPLD bitstreams, and creates Gitea releases with all assets — 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=tzfs-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 |
|---|---|
| TZFS project page | /sharpmz-upgrades-tzfs/ |
| TZFS User Manual | /sharpmz-upgrades-tzfs-usermanual/ |
| TZFS Technical Guide | /sharpmz-upgrades-tzfs-technicalguide/ |
| tranZPUter project page | /transzputer/ |
| FusionX Developer’s Guide | /tranzputer-fusionx-developersguide/ |
| RFS Developer’s Guide | /sharpmz-upgrades-rfs-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 |
| Sharp MZ-80A Service Manual | Hardware reference — memory map, I/O port assignments, monitor ROM entry points |
| Sharp MZ-700 Technical Manual | Hardware reference for MZ-700 monitor ROM and memory layout |