Banking
This commit is contained in:
@@ -1 +1,2 @@
|
||||
build
|
||||
__pycache__/
|
||||
|
||||
@@ -9,7 +9,7 @@ METAL_RELFILES := $(METAL_ASM_SRCFILES:%.asm=$(BUILDDIR)/metal/%.rel) \
|
||||
METAL_CC := sdcc
|
||||
METAL_AS := sdasz80
|
||||
METAL_MAKEBIN := makebin
|
||||
METAL_FLAGS := -mz80 --no-std-crt0 --code-loc 0xC080 --code-size 0x0F80 --data-loc 0xF000
|
||||
METAL_FLAGS := -mz80 --no-std-crt0 --code-loc 0xC080 --code-size 0x0F80 --data-loc 0xF000 --opt-code-size
|
||||
METAL_CPPFLAGS := -I src -I $(BUILDDIR)/metal/generated --disable-warning 283
|
||||
METAL_BIN_SIZE := 32768
|
||||
METAL_RAM_SIZE := 16384
|
||||
@@ -48,7 +48,8 @@ $(BUILDDIR)/metal/$(TARGET).flat.bin: $(BUILDDIR)/metal/$(TARGET).ihx
|
||||
$(METAL_MAKEBIN) -s $(METAL_IMAGE_SIZE) $< $@
|
||||
|
||||
$(BUILDDIR)/metal/$(TARGET).prog.bin: $(BUILDDIR)/metal/$(TARGET).flat.bin $(TINYBASIC_BUILD)/tinybasic.bin
|
||||
@payload_end=$$(awk '$$1 == "DEF" && $$2 == "resident_image_end" { print $$3 }' $(BUILDDIR)/metal/$(TARGET).noi); \
|
||||
@set -e; \
|
||||
payload_end=$$(awk '$$1 == "DEF" && $$2 == "resident_image_end" { print $$3 }' $(BUILDDIR)/metal/$(TARGET).noi); \
|
||||
payload_size=$$((payload_end - $(RESIDENT_BASE))); \
|
||||
test $$payload_size -gt 0; \
|
||||
test $$payload_size -le $$(( $(TINYBASIC_ROM_OFFSET) - $(RESIDENT_ROM_OFFSET) )); \
|
||||
|
||||
@@ -4,10 +4,12 @@ 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 of RAM at `C000`. The BIOS initializes interrupt mode 2 and the UART,
|
||||
then 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`.
|
||||
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
|
||||
@@ -17,6 +19,7 @@ optional `0x` prefix is accepted.
|
||||
| --- | --- |
|
||||
| `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 |
|
||||
@@ -29,12 +32,41 @@ optional `0x` prefix is accepted.
|
||||
`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:
|
||||
|
||||
```sh
|
||||
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](https://pyserial.readthedocs.io/).
|
||||
|
||||
```sh
|
||||
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:
|
||||
|
||||
```sh
|
||||
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 image
|
||||
@@ -44,8 +76,8 @@ The outputs are:
|
||||
The current compact image is 6,157 bytes. Its ROM layout is:
|
||||
|
||||
- `0000-007F`: reset/copy bootstrap
|
||||
- `0080-0F9D`: packed BIOS+monitor image copied to RAM at `C000-CF1D`
|
||||
- `0F9E-0FFF`: unused space reserved for the resident image to grow
|
||||
- `0080-0FEA`: packed BIOS+monitor image copied to RAM at `C000-CF6A`
|
||||
- `0FEB-0FFF`: unused space reserved for the resident image to grow
|
||||
- `1000-180C`: Tiny BASIC, executed directly from ROM
|
||||
- `180D-7FFF`: unused space available for additional ROM content
|
||||
|
||||
@@ -58,13 +90,20 @@ 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-CF1D`: resident BIOS, vectors, services, and monitor
|
||||
- `CF1E-EFFF`: free space for resident-image growth
|
||||
- `C000-CF6A`: resident BIOS, vectors, services, and monitor
|
||||
- `CF6B-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:
|
||||
@@ -78,10 +117,25 @@ Assembly applications include `include/bios.inc` and call these fixed entries:
|
||||
| `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 glue
|
||||
|
||||
@@ -21,6 +21,11 @@ the `I` register, and the BIOS UART/CTC configuration intact. They may return
|
||||
normally if they preserve the caller's stack, or jump to `BIOS_MONITOR` for a
|
||||
non-returning monitor warm start after replacing the stack pointer.
|
||||
|
||||
Use `BIOS_BANK` and `BIOS_ROM_DISABLE` for the write-only banking latch rather
|
||||
than writing port `00` directly. The BIOS keeps the ROM-disable state when the
|
||||
RAM bank selection changes. A ROM application must not expect a normal `RET`
|
||||
after it maps out the ROM containing its return address.
|
||||
|
||||
To add an application, add its assembler target and ROM offset to the Makefile,
|
||||
overlay its binary in the `monitor.prog.bin` recipe, and add a monitor command
|
||||
or use `G address` to launch it.
|
||||
|
||||
@@ -11,6 +11,10 @@ char bios_getc(void);
|
||||
uint8_t bios_try_getc(void);
|
||||
void bios_flush(void);
|
||||
|
||||
/* Write-only banking latch services. */
|
||||
void bios_bank(uint8_t bank);
|
||||
void bios_rom_disable(void);
|
||||
|
||||
/* Low-level routines implemented by the resident assembly BIOS. */
|
||||
void io_out(uint8_t port, uint8_t value);
|
||||
uint8_t io_in(uint8_t port);
|
||||
|
||||
@@ -2,6 +2,12 @@ STACK_TOP = 0xFFFF
|
||||
DELAY_LOOPS_PER_MS = 40
|
||||
RESIDENT_BASE = 0xC000
|
||||
RESIDENT_ROM_SOURCE = 0x0080
|
||||
BANK_CONTROL_PORT = 0x00
|
||||
BOOT_BANK_MAPPING = 0x06 ; ROM on, banking on, upper 16K at C000-FFFF
|
||||
ROM_DISABLE_BIT = 0x01
|
||||
BANK_SELECT_BIT = 0x02
|
||||
BANK_ENABLE_BIT = 0x04
|
||||
BANK_MODE_CLEAR_MASK = 0xF9 ; preserve ROM-disable and auxiliary outputs
|
||||
|
||||
.include "bios.inc"
|
||||
|
||||
@@ -16,6 +22,9 @@ RESIDENT_ROM_SOURCE = 0x0080
|
||||
.globl _bios_try_getc
|
||||
.globl _bios_getc
|
||||
.globl _bios_init
|
||||
.globl _bios_bank
|
||||
.globl _bios_rom_disable
|
||||
.globl _bank_control_shadow
|
||||
.globl resident_image_end
|
||||
.globl s__DATA
|
||||
.globl l__DATA
|
||||
@@ -30,6 +39,10 @@ RESIDENT_ROM_SOURCE = 0x0080
|
||||
.org 0x08
|
||||
boot_copy:
|
||||
di
|
||||
; Keep ROM enabled while mapping the upper physical 16K at C000-FFFF.
|
||||
; The resident copy therefore survives either banked mode after boot.
|
||||
ld a, #BOOT_BANK_MAPPING
|
||||
out (BANK_CONTROL_PORT), a
|
||||
ld sp, #STACK_TOP
|
||||
ld hl, #RESIDENT_ROM_SOURCE
|
||||
ld de, #RESIDENT_BASE
|
||||
@@ -66,7 +79,8 @@ boot_copy:
|
||||
|
||||
; Stable BIOS jump table for ROM-resident applications.
|
||||
; C050 putc(A), C053 try_getc(), C056 getc(), C059 io_out(A,L),
|
||||
; C05C io_in(A), C05F delay_ms(HL), C062 monitor warm start.
|
||||
; C05C io_in(A), C05F delay_ms(HL), C062 monitor warm start,
|
||||
; C065 bank(A), C068 disable ROM.
|
||||
.org BIOS_PUTC
|
||||
jp bios_service_putc
|
||||
.org BIOS_TRY_GETC
|
||||
@@ -81,6 +95,17 @@ boot_copy:
|
||||
jp bios_service_delay_ms
|
||||
.org BIOS_MONITOR
|
||||
jp init
|
||||
.org BIOS_BANK
|
||||
jp _bios_bank
|
||||
.org BIOS_ROM_DISABLE
|
||||
jp _bios_rom_disable
|
||||
|
||||
; Port 00 is write-only. Keep its last BIOS-managed value outside the C data
|
||||
; areas so a monitor warm start does not reset it. The bootstrap establishes
|
||||
; this same value in the hardware before the resident image is copied.
|
||||
.org RESIDENT_BASE + 0x70
|
||||
_bank_control_shadow::
|
||||
.db BOOT_BANK_MAPPING
|
||||
|
||||
.area _HOME
|
||||
init:
|
||||
@@ -135,6 +160,41 @@ resident_image_end:
|
||||
|
||||
.area _CODE
|
||||
|
||||
; Select a RAM mapping while retaining the shadowed ROM-disable bit.
|
||||
; Input: A=0 selects lower 48K + upper 16K; any other value selects upper 64K.
|
||||
; Both modes keep banking enabled. A and flags are preserved.
|
||||
_bios_bank::
|
||||
push af
|
||||
or a
|
||||
jr z, bios_bank_split
|
||||
|
||||
ld a, (_bank_control_shadow)
|
||||
and #BANK_MODE_CLEAR_MASK
|
||||
or #BANK_ENABLE_BIT
|
||||
jr bios_bank_update
|
||||
|
||||
bios_bank_split:
|
||||
ld a, (_bank_control_shadow)
|
||||
and #BANK_MODE_CLEAR_MASK
|
||||
or #(BANK_ENABLE_BIT | BANK_SELECT_BIT)
|
||||
|
||||
bios_bank_update:
|
||||
ld (_bank_control_shadow), a
|
||||
out (BANK_CONTROL_PORT), a
|
||||
pop af
|
||||
ret
|
||||
|
||||
; Permanently hide ROM until reset, preserving the selected RAM mapping.
|
||||
; A and flags are preserved.
|
||||
_bios_rom_disable::
|
||||
push af
|
||||
ld a, (_bank_control_shadow)
|
||||
or #ROM_DISABLE_BIT
|
||||
ld (_bank_control_shadow), a
|
||||
out (BANK_CONTROL_PORT), a
|
||||
pop af
|
||||
ret
|
||||
|
||||
; BIOS ABI services preserve registers except documented return values.
|
||||
bios_service_putc:
|
||||
push af
|
||||
|
||||
@@ -6,3 +6,5 @@ BIOS_IO_OUT = 0xC059 ; A = port, L = value; preserves all registers
|
||||
BIOS_IO_IN = 0xC05C ; A = port; returns value in A
|
||||
BIOS_DELAY_MS = 0xC05F ; HL = milliseconds; preserves all registers
|
||||
BIOS_MONITOR = 0xC062 ; warm-start the monitor; does not return
|
||||
BIOS_BANK = 0xC065 ; A=0 split 48K/16K, otherwise upper 64K
|
||||
BIOS_ROM_DISABLE = 0xC068 ; disable ROM while preserving the bank selection
|
||||
|
||||
@@ -91,14 +91,15 @@ static uint8_t split_line(char *line, char **argv)
|
||||
static void print_help(void)
|
||||
{
|
||||
bios_puts(
|
||||
"Commands (hexadecimal numbers):\r\n"
|
||||
" D|DUMP address [length] Dump memory (default 0040, max 0100)\r\n"
|
||||
" W|WRITE address byte ... Write one or more memory bytes\r\n"
|
||||
" I|IN port Read an I/O port\r\n"
|
||||
" O|OUT port byte Write an I/O port\r\n"
|
||||
" G|GO address Execute code; RET returns here\r\n"
|
||||
" B|BASIC Start ROM Tiny BASIC at 1000\r\n"
|
||||
" H|HELP|? Show this help\r\n");
|
||||
"Commands (hex numbers):\r\n"
|
||||
" D addr [len] Dump memory\r\n"
|
||||
" C start end 16-bit checksum, inclusive\r\n"
|
||||
" W addr byte... Write memory\r\n"
|
||||
" I port Read I/O\r\n"
|
||||
" O port byte Write I/O\r\n"
|
||||
" G addr Execute\r\n"
|
||||
" B Tiny BASIC\r\n"
|
||||
" H Help\r\n");
|
||||
}
|
||||
|
||||
static void command_dump(uint8_t argc, char **argv)
|
||||
@@ -131,6 +132,28 @@ static void command_dump(uint8_t argc, char **argv)
|
||||
}
|
||||
}
|
||||
|
||||
static void command_checksum(uint8_t argc, char **argv)
|
||||
{
|
||||
uint16_t address;
|
||||
uint16_t end;
|
||||
uint16_t checksum = 0;
|
||||
|
||||
if (argc != 3 || !parse_hex(argv[1], &address) ||
|
||||
!parse_hex(argv[2], &end) || end < address) {
|
||||
bios_puts("Usage: C start end\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
checksum = (uint16_t)(checksum + *(volatile uint8_t *)address);
|
||||
if (address == end) break;
|
||||
++address;
|
||||
}
|
||||
|
||||
print_hex16(checksum);
|
||||
bios_puts("\r\n");
|
||||
}
|
||||
|
||||
static void command_write(uint8_t argc, char **argv)
|
||||
{
|
||||
uint16_t address;
|
||||
@@ -222,6 +245,8 @@ static void execute_line(char *line)
|
||||
print_help();
|
||||
} else if (strings_equal(argv[0], "d") || strings_equal(argv[0], "dump")) {
|
||||
command_dump(argc, argv);
|
||||
} else if (strings_equal(argv[0], "c") || strings_equal(argv[0], "checksum")) {
|
||||
command_checksum(argc, argv);
|
||||
} else if (strings_equal(argv[0], "w") || strings_equal(argv[0], "write")) {
|
||||
command_write(argc, argv);
|
||||
} else if (strings_equal(argv[0], "i") || strings_equal(argv[0], "in")) {
|
||||
|
||||
Executable
+296
@@ -0,0 +1,296 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Program an Intel HEX file through the Z80 UART monitor."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import serial
|
||||
except ImportError:
|
||||
print(
|
||||
"error: pySerial is required; install it with: "
|
||||
"python3 -m pip install pyserial",
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise SystemExit(2)
|
||||
|
||||
|
||||
MAX_BLOCK_SIZE = 24 # Largest W command that fits the monitor's 80-byte line.
|
||||
|
||||
|
||||
class ProgramError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def load_ihex(path: Path) -> dict[int, int]:
|
||||
"""Return the 16-bit-addressed data contained in an Intel HEX file."""
|
||||
memory: dict[int, int] = {}
|
||||
address_base = 0
|
||||
saw_eof = False
|
||||
|
||||
try:
|
||||
lines = path.read_text(encoding="ascii").splitlines()
|
||||
except (OSError, UnicodeError) as exc:
|
||||
raise ProgramError(f"cannot read {path}: {exc}") from exc
|
||||
|
||||
for line_number, text in enumerate(lines, 1):
|
||||
text = text.strip()
|
||||
if not text:
|
||||
continue
|
||||
if saw_eof:
|
||||
raise ProgramError(f"{path}:{line_number}: data after EOF record")
|
||||
if not text.startswith(":"):
|
||||
raise ProgramError(f"{path}:{line_number}: record does not start with ':'")
|
||||
try:
|
||||
record = bytes.fromhex(text[1:])
|
||||
except ValueError as exc:
|
||||
raise ProgramError(f"{path}:{line_number}: invalid hexadecimal record") from exc
|
||||
|
||||
if len(record) < 5 or len(record) != record[0] + 5:
|
||||
raise ProgramError(f"{path}:{line_number}: invalid record length")
|
||||
if sum(record) & 0xFF:
|
||||
raise ProgramError(f"{path}:{line_number}: Intel HEX checksum mismatch")
|
||||
|
||||
count = record[0]
|
||||
offset = (record[1] << 8) | record[2]
|
||||
record_type = record[3]
|
||||
data = record[4 : 4 + count]
|
||||
|
||||
if record_type == 0x00:
|
||||
start = address_base + offset
|
||||
if start + count > 0x10000:
|
||||
raise ProgramError(
|
||||
f"{path}:{line_number}: address is outside the Z80's 64 KiB space"
|
||||
)
|
||||
for index, value in enumerate(data):
|
||||
address = start + index
|
||||
old_value = memory.get(address)
|
||||
if old_value is not None and old_value != value:
|
||||
raise ProgramError(
|
||||
f"{path}:{line_number}: conflicting data at {address:04X}"
|
||||
)
|
||||
memory[address] = value
|
||||
elif record_type == 0x01:
|
||||
if count != 0:
|
||||
raise ProgramError(f"{path}:{line_number}: malformed EOF record")
|
||||
saw_eof = True
|
||||
elif record_type == 0x02:
|
||||
if count != 2:
|
||||
raise ProgramError(
|
||||
f"{path}:{line_number}: malformed extended segment address"
|
||||
)
|
||||
address_base = int.from_bytes(data, "big") << 4
|
||||
elif record_type == 0x04:
|
||||
if count != 2:
|
||||
raise ProgramError(
|
||||
f"{path}:{line_number}: malformed extended linear address"
|
||||
)
|
||||
address_base = int.from_bytes(data, "big") << 16
|
||||
elif record_type in (0x03, 0x05):
|
||||
# Start-address records do not contain bytes to program.
|
||||
if count != 4:
|
||||
raise ProgramError(
|
||||
f"{path}:{line_number}: malformed start-address record"
|
||||
)
|
||||
continue
|
||||
else:
|
||||
raise ProgramError(
|
||||
f"{path}:{line_number}: unsupported record type {record_type:02X}"
|
||||
)
|
||||
|
||||
if not saw_eof:
|
||||
raise ProgramError(f"{path}: missing Intel HEX EOF record")
|
||||
if not memory:
|
||||
raise ProgramError(f"{path}: file contains no data")
|
||||
return memory
|
||||
|
||||
|
||||
def make_blocks(memory: dict[int, int], block_size: int) -> list[tuple[int, bytes]]:
|
||||
"""Combine adjacent addresses without filling gaps from the HEX file."""
|
||||
blocks: list[tuple[int, bytes]] = []
|
||||
addresses = sorted(memory)
|
||||
index = 0
|
||||
|
||||
while index < len(addresses):
|
||||
start = addresses[index]
|
||||
data = bytearray([memory[start]])
|
||||
index += 1
|
||||
while (
|
||||
index < len(addresses)
|
||||
and len(data) < block_size
|
||||
and addresses[index] == start + len(data)
|
||||
):
|
||||
data.append(memory[addresses[index]])
|
||||
index += 1
|
||||
blocks.append((start, bytes(data)))
|
||||
|
||||
return blocks
|
||||
|
||||
|
||||
class Monitor:
|
||||
def __init__(self, port: str, baud: int, timeout: float):
|
||||
try:
|
||||
self.serial = serial.Serial(
|
||||
port=port,
|
||||
baudrate=baud,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
parity=serial.PARITY_NONE,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
timeout=timeout,
|
||||
write_timeout=timeout,
|
||||
)
|
||||
except serial.SerialException as exc:
|
||||
raise ProgramError(f"cannot open serial port {port}: {exc}") from exc
|
||||
|
||||
def close(self) -> None:
|
||||
self.serial.close()
|
||||
|
||||
def command(self, command: str) -> str:
|
||||
try:
|
||||
self.serial.write(command.encode("ascii") + b"\r")
|
||||
self.serial.flush()
|
||||
response = self.serial.read_until(b"> ")
|
||||
except serial.SerialException as exc:
|
||||
raise ProgramError(f"serial communication failed: {exc}") from exc
|
||||
if not response.endswith(b"> "):
|
||||
raise ProgramError(f"timeout waiting for monitor after '{command}'")
|
||||
return response.decode("ascii", errors="replace")
|
||||
|
||||
def synchronize(self) -> None:
|
||||
try:
|
||||
self.serial.reset_input_buffer()
|
||||
except serial.SerialException as exc:
|
||||
raise ProgramError(f"serial communication failed: {exc}") from exc
|
||||
response = self.command("")
|
||||
if not response.endswith("> "):
|
||||
raise ProgramError("monitor prompt not found")
|
||||
|
||||
|
||||
def response_has_ok(response: str) -> bool:
|
||||
return any(line.strip() == "OK" for line in response.splitlines())
|
||||
|
||||
|
||||
def response_checksum(response: str) -> int | None:
|
||||
matches = [
|
||||
int(line.strip(), 16)
|
||||
for line in response.splitlines()
|
||||
if re.fullmatch(r"[0-9A-Fa-f]{4}", line.strip())
|
||||
]
|
||||
return matches[-1] if matches else None
|
||||
|
||||
|
||||
def write_and_verify(monitor: Monitor, start: int, data: bytes) -> tuple[bool, str]:
|
||||
values = " ".join(f"{value:02X}" for value in data)
|
||||
response = monitor.command(f"W {start:04X} {values}")
|
||||
if not response_has_ok(response):
|
||||
return False, "monitor did not acknowledge the write"
|
||||
|
||||
end = start + len(data) - 1
|
||||
expected = sum(data) & 0xFFFF
|
||||
response = monitor.command(f"C {start:04X} {end:04X}")
|
||||
actual = response_checksum(response)
|
||||
if actual is None:
|
||||
return False, "monitor did not return a checksum"
|
||||
if actual != expected:
|
||||
return False, f"checksum mismatch (expected {expected:04X}, received {actual:04X})"
|
||||
return True, ""
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Program an Intel HEX file through the Z80 UART monitor."
|
||||
)
|
||||
parser.add_argument("port", help="serial port, for example /dev/ttyUSB0 or COM3")
|
||||
parser.add_argument("file", type=Path, help="Intel HEX input file")
|
||||
parser.add_argument("--baud", type=int, default=2400, help="baud rate (default: 2400)")
|
||||
parser.add_argument(
|
||||
"--block-size",
|
||||
type=int,
|
||||
default=16,
|
||||
help=f"bytes per verified block, 1-{MAX_BLOCK_SIZE} (default: 16)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--retries",
|
||||
type=int,
|
||||
default=3,
|
||||
help="retries after the first attempt (default: 3)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--timeout",
|
||||
type=float,
|
||||
default=5.0,
|
||||
help="serial response timeout in seconds (default: 5)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
if not 1 <= args.block_size <= MAX_BLOCK_SIZE:
|
||||
parser.error(f"--block-size must be between 1 and {MAX_BLOCK_SIZE}")
|
||||
if args.retries < 0:
|
||||
parser.error("--retries cannot be negative")
|
||||
if args.baud <= 0:
|
||||
parser.error("--baud must be positive")
|
||||
if args.timeout <= 0:
|
||||
parser.error("--timeout must be positive")
|
||||
return args
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
monitor: Monitor | None = None
|
||||
|
||||
try:
|
||||
memory = load_ihex(args.file)
|
||||
blocks = make_blocks(memory, args.block_size)
|
||||
print(
|
||||
f"Loaded {len(memory)} bytes in {len(blocks)} blocks "
|
||||
f"({min(memory):04X}-{max(memory):04X})"
|
||||
)
|
||||
|
||||
monitor = Monitor(args.port, args.baud, args.timeout)
|
||||
monitor.synchronize()
|
||||
|
||||
for number, (start, data) in enumerate(blocks, 1):
|
||||
end = start + len(data) - 1
|
||||
for attempt in range(args.retries + 1):
|
||||
try:
|
||||
if attempt:
|
||||
monitor.synchronize()
|
||||
success, reason = write_and_verify(monitor, start, data)
|
||||
except ProgramError as exc:
|
||||
success, reason = False, str(exc)
|
||||
if success:
|
||||
print(
|
||||
f"\rBlock {number}/{len(blocks)} {start:04X}-{end:04X} verified",
|
||||
end="",
|
||||
flush=True,
|
||||
)
|
||||
break
|
||||
if attempt == args.retries:
|
||||
raise ProgramError(
|
||||
f"block {start:04X}-{end:04X} failed after "
|
||||
f"{args.retries + 1} attempts: {reason}"
|
||||
)
|
||||
print(
|
||||
f"\nRetrying {start:04X}-{end:04X} "
|
||||
f"({attempt + 1}/{args.retries}): {reason}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
print(f"\nProgrammed and verified {len(memory)} bytes")
|
||||
return 0
|
||||
except ProgramError as exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
except KeyboardInterrupt:
|
||||
print("\nCancelled", file=sys.stderr)
|
||||
return 130
|
||||
finally:
|
||||
if monitor is not None:
|
||||
monitor.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user