7.8 KiB
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 the first such application,
linked at ROM address 1000.
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 |
G 8000 |
Execute code at address 8000 |
G 1000 |
Start the ROM-resident Z80 Tiny BASIC interpreter |
B or BASIC |
Shortcut for G 1000 |
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.
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. For example:
python3 program_ihex.py /dev/ttyUSB0 program.hex --retries 5 --block-size 24
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 imagebuild/metal/monitor.prog.bin: compact image ending at the last occupied bytebuild/tinybasic/tinybasic.bin: standalone relocated Tiny BASIC payload
The current compact image is 6,157 bytes. Its ROM layout is:
0000-007F: reset/copy bootstrap0080-0FEA: packed BIOS+monitor image copied to RAM atC000-CF6A0FEB-0FFF: unused space reserved for the resident image to grow1000-180C: Tiny BASIC, executed directly from ROM180D-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 textB000-B077: Tiny BASIC variables and input bufferB078-BFFF: Tiny BASIC stack spaceC000-CF6A: resident BIOS, vectors, services, and monitorCF6B-EFFF: free space for resident-image growthF000-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 |
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 is 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.
The equivalent C API is bios_bank(uint8_t bank) and
bios_rom_disable(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 gluebios.c/bios.h: UART, buffers, interrupts, and resident BIOS APImonitor.c: command interpreter implemented on the BIOS APIapps/tinybasic.asm: first ROM-resident applicationapps/README.md: ROM application memory and integration contractinclude/bios.inc: stable assembly interface for ROM applications
Tiny BASIC
Tiny BASIC is built from apps/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 1000. Start it from
the monitor with:
B
BASIC and G 1000 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 first currently unused ROM byte is 180D; 1900 is a convenient
aligned starting point for another ROM payload.