2026-10-05 10:07:47 +02:00
2026-10-05 10:07:47 +02:00
2026-10-04 13:53:22 +02:00
2026-10-01 12:44:11 +02:00
2026-10-03 17:30:43 +02:00
2026-10-04 13:53:22 +02:00
2026-09-28 13:17:35 +02:00
2026-09-28 13:36:52 +02:00
2026-10-03 17:30:43 +02:00
2026-10-04 13:53:22 +02:00
2026-10-04 17:23:39 +02:00

Z80 UART monitor

This project provides a resident BIOS, an interactive monitor, and ROM-resident applications for a Z80 system with a UART/CTC card at I/O base 20.

At reset, a small ROM bootstrap copies only the BIOS and monitor into the top 16 KiB at C000. Before copying, it writes 06 to banking port 00: ROM remains enabled, banking is enabled, and C000-FFFF maps to the upper physical 16 KiB. The BIOS then initializes interrupt mode 2 and the UART and starts the monitor. Applications remain in ROM and execute in place while calling the stable BIOS jump table in high RAM. Tiny BASIC is linked at ROM address 1400, and the tape loader is linked at 3000.

Connect a terminal at 2400 baud, 8 data bits, no parity, 1 stop bit. Commands are case-insensitive and all numeric arguments are hexadecimal; an optional 0x prefix is accepted.

Command Action
D 8000 Dump 40 bytes starting at address 8000
D 8000 20 Dump 20 bytes starting at address 8000
C 8000 BFFF Compute the 16-bit additive checksum of an inclusive range
W 8000 3E 41 D3 25 C9 Write bytes starting at address 8000
I 20 Read I/O port 20
O 20 57 Write 57 to I/O port 20
K 0 or BANK 0 Select lower physical 48K plus upper physical 16K
K 1 or BANK 1 Select the complete upper physical 64K
R ON or ROM ON Enable ROM while preserving the RAM bank selection
R OFF or ROM OFF Disable ROM while preserving the RAM bank selection
G 8000 Execute code at address 8000
G 1400 Start the ROM-resident Z80 Tiny BASIC interpreter
B or BASIC Shortcut for G 1400
T or TAPE Start the ROM-resident tape loader at 3000
HELP Show the command summary

G transfers control with a return address on the stack, so code ending in RET returns to the monitor. Code that changes the stack pointer, interrupt mode, or UART/CTC setup must restore it before returning.

C adds every byte from the start through the end address, inclusive, and prints the low 16 bits of the sum as four hexadecimal digits. The end address must be greater than or equal to the start address.

D prints 16 bytes per row followed by an ASCII column. Bytes outside the printable ASCII range are shown as ..

Build the ROM and programmer-format images with:

make

Uploading Intel HEX files

program_ihex.py can write an Intel HEX file to memory through the running monitor. It sends data in small blocks with W, checks every block with C, and retries a failed write or checksum before giving up. It requires Python 3 and pySerial.

python3 -m pip install pyserial
python3 program_ihex.py /dev/ttyUSB0 program.hex

The default connection is 2400 baud, the default block size is 16 bytes, and three retries are allowed after the initial attempt. Characters are paced with a 5 ms delay because the monitor UART can lose bytes from an uninterrupted USB serial burst. For example:

python3 program_ihex.py /dev/ttyUSB0 program.hex --retries 5 --block-size 24

Use --char-delay 0.01 if commands still arrive with missing characters. A smaller value uploads faster; --char-delay 0 disables host-side pacing.

Use a Windows port such as COM3 in place of /dev/ttyUSB0. Run python3 program_ihex.py --help for all options. The monitor must be sitting at its > prompt before starting the upload. Only data records present in the HEX file are written; gaps are left unchanged. The HEX addresses must refer to writable memory and must not overwrite the running monitor at C000-FFFF.

The outputs are:

  • build/metal/monitor.bin: complete 32 KiB ROM image
  • build/metal/monitor.prog.bin: compact image ending at the last occupied byte
  • build/tinybasic/tinybasic.bin: standalone relocated Tiny BASIC payload
  • build/casette/tape_loader.bin: standalone relocated tape-loader payload

The current compact image is 12,967 bytes. Its ROM layout is:

  • 0000-007F: reset/copy bootstrap
  • 0080-12DD: packed BIOS+monitor image copied to RAM at C000-D25D
  • 12DE-13FF: unused space reserved for the resident image to grow
  • 1400-1C0C: Tiny BASIC, executed directly from ROM
  • 1C0D-2FFF: unused ROM
  • 3000-32A6: tape loader, executed directly from ROM
  • 32A7-7FFF: unused space available for additional ROM content

The resident image starts at C000, its IM 2 vector table is at C040, and linked code starts at C080. The bootstrap obtains the resident copy length from a linker symbol, so it tracks BIOS and monitor changes automatically.

The resulting RAM layout is:

  • 8000-AFFF: Tiny BASIC workspace and BASIC program text
  • B000-B077: Tiny BASIC variables and input buffer
  • B078-BFFF: Tiny BASIC stack space
  • C000-D25D: resident BIOS, vectors, services, and monitor
  • D25E-EFFF: free space for resident-image growth
  • F000-FFFF: BIOS globals, UART buffers, and monitor stack

The BIOS and monitor continue to work if low ROM is disabled. ROM-resident applications naturally require ROM to remain mapped while they execute.

The banking latch bits used by the bootstrap are bit 0 ROM_DISABLE, bit 1 BANK_SELECT, and bit 2 BANK_ENABLE. Its initial value 06 leaves ROM on and selects the lower physical 48 KiB plus the upper physical 16 KiB. Writing 07 later disables ROM without changing that split mapping. Writing 05 selects the complete upper physical 64 KiB; the monitor remains available because its C000-FFFF image was placed in that upper bank during boot.

BIOS interface

Assembly applications include include/bios.inc and call these fixed entries:

Address Service Interface
C050 BIOS_PUTC Write character in A
C053 BIOS_TRY_GETC Return character in A, or zero
C056 BIOS_GETC Wait for and return a character in A
C059 BIOS_IO_OUT Write L to port A
C05C BIOS_IO_IN Read port A, returning the value in A
C05F BIOS_DELAY_MS Delay approximately HL milliseconds
C062 BIOS_MONITOR Warm-start the monitor; does not return
C065 BIOS_BANK A=0: split lower 48K/upper 16K; otherwise select upper 64K
C068 BIOS_ROM_DISABLE Disable ROM while retaining the selected RAM mapping
C06B BIOS_ROM_ENABLE Enable ROM while retaining the selected RAM mapping
C06E BIOS_FLUSH Wait until the UART output queue is empty

The service wrappers preserve working registers except for documented return values. The monitor uses the corresponding C API declared in bios.h.

The banking latch at port 00 is write-only, so the BIOS keeps a persistent shadow byte initialized to 06 by the copied resident image. BIOS_BANK always leaves banking enabled and changes only the bank selection while retaining the shadowed ROM-disable bit and auxiliary-output bits. BIOS_ROM_DISABLE sets the ROM-disable bit and BIOS_ROM_ENABLE clears it; both are idempotent. Code should not write port 00 through BIOS_IO_OUT or the monitor's O command after using these services, because such writes bypass the BIOS shadow state. Use the monitor's BANK and ROM commands instead.

The equivalent C API is bios_bank(uint8_t bank) and bios_rom_disable(void) / bios_rom_enable(void). A ROM-resident caller cannot return normally after selecting the complete upper 64 KiB or disabling ROM, because its return address is no longer mapped; it must arrange its continuation in RAM.

Project organization:

  • boot.asm: ROM bootstrap, high vectors, BIOS jump table, and assembly glue
  • bios.c / bios.h: UART, buffers, interrupts, and resident BIOS API
  • monitor.c: command interpreter implemented on the BIOS API
  • rom_layout.mk: authoritative ROM and resident-memory addresses
  • apps/tinybasic/tinybasic.asm: Tiny BASIC ROM application
  • apps/casette/tape_loader.asm: tape-loader ROM application
  • apps/README.md: ROM application memory and integration contract
  • include/bios.inc: stable assembly interface for ROM applications

The build generates rom_layout.h for C and rom_layout.inc for assembly from rom_layout.mk. Change application origins and resident-memory addresses only in rom_layout.mk; the generated files are removed by make clean.

Tiny BASIC

Tiny BASIC is built from apps/tinybasic/tinybasic.asm. The port replaces fixed low-page RST calls with relocatable calls and uses a marker-based command table so it can be linked at a nonzero ROM address.

Tiny BASIC is linked at and runs directly from ROM address 1400. Start it from the monitor with:

B

BASIC and G 1400 are equivalent alternatives.

Tiny BASIC calls the BIOS at C050 for output and C053 for non-blocking input. Tiny BASIC keeps interrupts enabled so it can share the BIOS-configured interrupt-driven UART.

Line editing accepts either BS (08) or DEL (7F) for backspace and emits BS SPACE BS so ANSI-style terminals erase the character visually. Ctrl-U clears the complete input line.

During execution, Ctrl-O toggles output suppression without stopping the BASIC program. Press Ctrl-O again to resume output. Ctrl-C stops execution and returns to the Tiny BASIC prompt.

Tiny BASIC resets the stack pointer when it starts, so it does not currently return through the monitor's G call. Reset the machine to return to the monitor. The tape loader occupies the later ROM slot at 3000-32A6; the first unused byte after it is 32A7.

Tape loader

The tape-loader image is built from apps/casette/tape_loader.asm, linked at 3000, and included in both ROM images. Start it with T, TAPE, or G 3000. The current image polls the comparator input and prints its state through the resident BIOS console service. Signal acquisition is bounded; a missing, unstable, or permanently high signal reports TAPE SIGNAL TIMEOUT and returns to the monitor instead of waiting forever with interrupts disabled.

S
Description
No description provided
Readme
12 MiB
Languages
Assembly 98.8%
Python 0.8%
Makefile 0.4%