start of tape loading. hw works and have a generation script

This commit is contained in:
Joppe Blondel
2026-10-03 16:29:41 +02:00
parent 47fffc2e71
commit 5897e55094
16 changed files with 784 additions and 106 deletions
+4
View File
@@ -0,0 +1,4 @@
{
"vs-cupl.PathOpenOcd": "/usr/sbin/openocd",
"vs-cupl.PathOpenOcdDl": "/usr/sbin/openocd"
}
+74 -22
View File
@@ -1,5 +1,11 @@
TARGET := monitor TARGET := monitor
BUILDDIR := build BUILDDIR := build
LAYOUT_CONFIG := rom_layout.mk
LAYOUT_BUILD := $(BUILDDIR)/generated
LAYOUT_HEADER := $(LAYOUT_BUILD)/rom_layout.h
LAYOUT_INCLUDE := $(LAYOUT_BUILD)/rom_layout.inc
include $(LAYOUT_CONFIG)
METAL_C_SRCFILES := $(wildcard *.c) METAL_C_SRCFILES := $(wildcard *.c)
METAL_ASM_SRCFILES := boot.asm METAL_ASM_SRCFILES := boot.asm
@@ -9,18 +15,16 @@ METAL_RELFILES := $(METAL_ASM_SRCFILES:%.asm=$(BUILDDIR)/metal/%.rel) \
METAL_CC := sdcc METAL_CC := sdcc
METAL_AS := sdasz80 METAL_AS := sdasz80
METAL_MAKEBIN := makebin METAL_MAKEBIN := makebin
METAL_FLAGS := -mz80 --no-std-crt0 --code-loc 0xC080 --code-size 0x0F80 --data-loc 0xF000 --opt-code-size METAL_FLAGS := -mz80 --no-std-crt0 --code-loc $(RESIDENT_CODE_BASE) --code-size $(RESIDENT_CODE_SIZE) --data-loc $(RESIDENT_DATA_BASE) --opt-code-size
METAL_CPPFLAGS := -I src -I $(BUILDDIR)/metal/generated --disable-warning 283 METAL_CPPFLAGS := -I src -I $(LAYOUT_BUILD) --disable-warning 283
METAL_BIN_SIZE := 32768 METAL_BIN_SIZE := $(ROM_SIZE)
METAL_RAM_SIZE := 16384 METAL_RAM_SIZE := 16384
METAL_IMAGE_SIZE := 65536 METAL_IMAGE_SIZE := 65536
RESIDENT_BASE := 0xC000
RESIDENT_ROM_OFFSET := 128
TINYBASIC_BASE := 0x1000 TINYBASIC_SRC := apps/tinybasic/tinybasic.asm
TINYBASIC_ROM_OFFSET := 4096
TINYBASIC_SRC := apps/tinybasic.asm
TINYBASIC_BUILD := $(BUILDDIR)/tinybasic TINYBASIC_BUILD := $(BUILDDIR)/tinybasic
TAPE_LOADER_SRC := apps/casette/tape_loader.asm
TAPE_LOADER_BUILD := $(BUILDDIR)/casette
.DEFAULT_GOAL := metal .DEFAULT_GOAL := metal
@@ -38,7 +42,12 @@ metal: $(BUILDDIR)/metal/$(TARGET).ihx $(BUILDDIR)/metal/$(TARGET).bin $(BUILDDI
printf " ROM storage: %5u / %5u bytes (%5.1f%%)\n", stored, rom_capacity, 100 * stored / rom_capacity; \ printf " ROM storage: %5u / %5u bytes (%5.1f%%)\n", stored, rom_capacity, 100 * stored / rom_capacity; \
printf " Resident BIOS+monitor: %5u bytes (%5u code/vector + %5u static data)\n", code + data, code, data; \ printf " Resident BIOS+monitor: %5u bytes (%5u code/vector + %5u static data)\n", code + data, code, data; \
}' $(BUILDDIR)/metal/$(TARGET).map }' $(BUILDDIR)/metal/$(TARGET).map
@printf " Tiny BASIC: %5u ROM bytes at 1000\n" $$(wc -c < $(TINYBASIC_BUILD)/tinybasic.bin) @printf " Tiny BASIC: %5u ROM bytes at %04X\n" \
$$(wc -c < $(TINYBASIC_BUILD)/tinybasic.bin) \
$$(( $(TINYBASIC_ROM_ADDRESS) ))
@printf " Tape loader: %5u ROM bytes at %04X\n" \
$$(wc -c < $(TAPE_LOADER_BUILD)/tape_loader.bin) \
$$(( $(TAPE_LOADER_ROM_ADDRESS) ))
$(BUILDDIR)/metal/$(TARGET).ihx: $(METAL_RELFILES) $(BUILDDIR)/metal/$(TARGET).ihx: $(METAL_RELFILES)
@mkdir -p $(@D) @mkdir -p $(@D)
@@ -47,31 +56,48 @@ $(BUILDDIR)/metal/$(TARGET).ihx: $(METAL_RELFILES)
$(BUILDDIR)/metal/$(TARGET).flat.bin: $(BUILDDIR)/metal/$(TARGET).ihx $(BUILDDIR)/metal/$(TARGET).flat.bin: $(BUILDDIR)/metal/$(TARGET).ihx
$(METAL_MAKEBIN) -s $(METAL_IMAGE_SIZE) $< $@ $(METAL_MAKEBIN) -s $(METAL_IMAGE_SIZE) $< $@
$(BUILDDIR)/metal/$(TARGET).prog.bin: $(BUILDDIR)/metal/$(TARGET).flat.bin $(TINYBASIC_BUILD)/tinybasic.bin $(BUILDDIR)/metal/$(TARGET).prog.bin: $(BUILDDIR)/metal/$(TARGET).flat.bin $(TINYBASIC_BUILD)/tinybasic.bin $(TAPE_LOADER_BUILD)/tape_loader.bin
@set -e; \ @set -e; \
payload_end=$$(awk '$$1 == "DEF" && $$2 == "resident_image_end" { print $$3 }' $(BUILDDIR)/metal/$(TARGET).noi); \ payload_end=$$(awk '$$1 == "DEF" && $$2 == "resident_image_end" { print $$3 }' $(BUILDDIR)/metal/$(TARGET).noi); \
payload_size=$$((payload_end - $(RESIDENT_BASE))); \ payload_size=$$((payload_end - $(RESIDENT_RAM_BASE))); \
test $$payload_size -gt 0; \ payload_rom_end=$$(( $(RESIDENT_ROM_OFFSET) + payload_size )); \
test $$payload_size -le $$(( $(TINYBASIC_ROM_OFFSET) - $(RESIDENT_ROM_OFFSET) )); \ tiny_size=$$(wc -c < $(TINYBASIC_BUILD)/tinybasic.bin); \
dd if=$< of=$@ bs=1 count=$(TINYBASIC_ROM_OFFSET) status=none; \ tape_size=$$(wc -c < $(TAPE_LOADER_BUILD)/tape_loader.bin); \
dd if=$< of=$@ bs=1 skip=$$(( $(RESIDENT_BASE) )) seek=$(RESIDENT_ROM_OFFSET) count=$$payload_size conv=notrunc status=none; \ if test $$payload_size -le 0; then \
dd if=$(TINYBASIC_BUILD)/tinybasic.bin of=$@ bs=1 seek=$(TINYBASIC_ROM_OFFSET) conv=notrunc status=none echo "error: resident image is empty or has an invalid end symbol" >&2; exit 1; \
fi; \
if test $$payload_rom_end -gt $$(( $(TINYBASIC_ROM_ADDRESS) )); then \
printf "error: resident ROM image ends at %04X, beyond Tiny BASIC origin %04X\n" \
$$payload_rom_end $$(( $(TINYBASIC_ROM_ADDRESS) )) >&2; exit 1; \
fi; \
if test $$(( $(TINYBASIC_ROM_ADDRESS) + tiny_size )) -gt $$(( $(TAPE_LOADER_ROM_ADDRESS) )); then \
printf "error: Tiny BASIC ends at %04X, beyond tape-loader origin %04X\n" \
$$(( $(TINYBASIC_ROM_ADDRESS) + tiny_size )) $$(( $(TAPE_LOADER_ROM_ADDRESS) )) >&2; exit 1; \
fi; \
if test $$(( $(TAPE_LOADER_ROM_ADDRESS) + tape_size )) -gt $$(( $(ROM_SIZE) )); then \
printf "error: tape loader ends at %04X, beyond ROM size %04X\n" \
$$(( $(TAPE_LOADER_ROM_ADDRESS) + tape_size )) $$(( $(ROM_SIZE) )) >&2; exit 1; \
fi; \
dd if=$< of=$@ bs=1 count=$$(( $(TAPE_LOADER_ROM_ADDRESS) )) status=none; \
dd if=$< of=$@ bs=1 skip=$$(( $(RESIDENT_RAM_BASE) )) seek=$$(( $(RESIDENT_ROM_OFFSET) )) count=$$payload_size conv=notrunc status=none; \
dd if=$(TINYBASIC_BUILD)/tinybasic.bin of=$@ bs=1 seek=$$(( $(TINYBASIC_ROM_ADDRESS) )) conv=notrunc status=none; \
dd if=$(TAPE_LOADER_BUILD)/tape_loader.bin of=$@ bs=1 seek=$$(( $(TAPE_LOADER_ROM_ADDRESS) )) conv=notrunc status=none
$(BUILDDIR)/metal/$(TARGET).bin: $(BUILDDIR)/metal/$(TARGET).flat.bin $(BUILDDIR)/metal/$(TARGET).prog.bin $(BUILDDIR)/metal/$(TARGET).bin: $(BUILDDIR)/metal/$(TARGET).flat.bin $(BUILDDIR)/metal/$(TARGET).prog.bin
dd if=$< of=$@ bs=$(METAL_BIN_SIZE) count=1 status=none dd if=$< of=$@ bs=$(METAL_BIN_SIZE) count=1 status=none
dd if=$(BUILDDIR)/metal/$(TARGET).prog.bin of=$@ conv=notrunc status=none dd if=$(BUILDDIR)/metal/$(TARGET).prog.bin of=$@ conv=notrunc status=none
$(BUILDDIR)/metal/%.rel: %.c $(BUILDDIR)/metal/%.rel: %.c $(LAYOUT_HEADER)
@mkdir -p $(@D) @mkdir -p $(@D)
$(METAL_CC) $(METAL_FLAGS) $(METAL_CPPFLAGS) -o $@ -c $< $(METAL_CC) $(METAL_FLAGS) $(METAL_CPPFLAGS) -o $@ -c $<
$(BUILDDIR)/metal/%.rel: %.asm $(BUILDDIR)/metal/%.rel: %.asm $(LAYOUT_INCLUDE)
@mkdir -p $(@D) @mkdir -p $(@D)
$(METAL_AS) -Iinclude -I$(BUILDDIR)/metal/generated -plosgff -o $@ $< $(METAL_AS) -Iinclude -I$(LAYOUT_BUILD) -plosgff -o $@ $<
$(TINYBASIC_BUILD)/tinybasic.rel: $(TINYBASIC_SRC) $(TINYBASIC_BUILD)/tinybasic.rel: $(TINYBASIC_SRC) $(LAYOUT_INCLUDE)
@mkdir -p $(@D) @mkdir -p $(@D)
$(METAL_AS) -Iinclude -plosgff -o $@ $< $(METAL_AS) -Iinclude -I$(LAYOUT_BUILD) -plosgff -o $@ $<
$(TINYBASIC_BUILD)/tinybasic.ihx: $(TINYBASIC_BUILD)/tinybasic.rel $(TINYBASIC_BUILD)/tinybasic.ihx: $(TINYBASIC_BUILD)/tinybasic.rel
sdldz80 -n -i -m -j $(basename $@) $< sdldz80 -n -i -m -j $(basename $@) $<
@@ -82,7 +108,33 @@ $(TINYBASIC_BUILD)/tinybasic.flat.bin: $(TINYBASIC_BUILD)/tinybasic.ihx
$(TINYBASIC_BUILD)/tinybasic.bin: $(TINYBASIC_BUILD)/tinybasic.flat.bin $(TINYBASIC_BUILD)/tinybasic.bin: $(TINYBASIC_BUILD)/tinybasic.flat.bin
@tiny_size=$$(awk '$$1 == "TINYBASIC0" { print $$5 + 0 }' $(TINYBASIC_BUILD)/tinybasic.map); \ @tiny_size=$$(awk '$$1 == "TINYBASIC0" { print $$5 + 0 }' $(TINYBASIC_BUILD)/tinybasic.map); \
test $$tiny_size -gt 0; \ test $$tiny_size -gt 0; \
dd if=$< of=$@ bs=1 skip=$$(( $(TINYBASIC_BASE) )) count=$$tiny_size status=none dd if=$< of=$@ bs=1 skip=$$(( $(TINYBASIC_ROM_ADDRESS) )) count=$$tiny_size status=none
$(TAPE_LOADER_BUILD)/tape_loader.rel: $(TAPE_LOADER_SRC) $(LAYOUT_INCLUDE)
@mkdir -p $(@D)
$(METAL_AS) -Iinclude -I$(LAYOUT_BUILD) -plosgff -o $@ $<
$(TAPE_LOADER_BUILD)/tape_loader.ihx: $(TAPE_LOADER_BUILD)/tape_loader.rel
sdldz80 -n -i -m -j $(basename $@) $<
$(TAPE_LOADER_BUILD)/tape_loader.flat.bin: $(TAPE_LOADER_BUILD)/tape_loader.ihx
$(METAL_MAKEBIN) -s $(METAL_IMAGE_SIZE) $< $@
$(TAPE_LOADER_BUILD)/tape_loader.bin: $(TAPE_LOADER_BUILD)/tape_loader.flat.bin
@loader_size=$$(awk '$$1 == "TAPELOAD0" { print $$5 + 0 }' $(TAPE_LOADER_BUILD)/tape_loader.map); \
test $$loader_size -gt 0; \
dd if=$< of=$@ bs=1 skip=$$(( $(TAPE_LOADER_ROM_ADDRESS) )) count=$$loader_size status=none
$(LAYOUT_HEADER): $(LAYOUT_CONFIG)
@mkdir -p $(@D)
@awk 'BEGIN { print "#ifndef ROM_LAYOUT_H"; print "#define ROM_LAYOUT_H"; print "" } \
/^[A-Z][A-Z0-9_]*[[:space:]]*:=[[:space:]]*/ { print "#define " $$1 " " $$3 } \
END { print ""; print "#endif" }' $< > $@
$(LAYOUT_INCLUDE): $(LAYOUT_CONFIG)
@mkdir -p $(@D)
@awk 'BEGIN { print "; Generated from rom_layout.mk; do not edit." } \
/^[A-Z][A-Z0-9_]*[[:space:]]*:=[[:space:]]*/ { print $$1 " = " $$3 }' $< > $@
clean: clean:
rm -rf $(BUILDDIR) rm -rf $(BUILDDIR)
+56 -26
View File
@@ -8,8 +8,8 @@ At reset, a small ROM bootstrap copies only the BIOS and monitor into the top
remains enabled, banking is enabled, and `C000-FFFF` maps to the upper physical 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 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 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, stable BIOS jump table in high RAM. Tiny BASIC is linked at ROM address `1400`,
linked at ROM address `1000`. and the tape loader is linked at `3000`.
Connect a terminal at **2400 baud, 8 data bits, no parity, 1 stop bit**. 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 Commands are case-insensitive and all numeric arguments are hexadecimal; an
@@ -23,9 +23,14 @@ optional `0x` prefix is accepted.
| `W 8000 3E 41 D3 25 C9` | Write bytes starting at address 8000 | | `W 8000 3E 41 D3 25 C9` | Write bytes starting at address 8000 |
| `I 20` | Read I/O port 20 | | `I 20` | Read I/O port 20 |
| `O 20 57` | Write 57 to 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 8000` | Execute code at address 8000 |
| `G 1000` | Start the ROM-resident Z80 Tiny BASIC interpreter | | `G 1400` | Start the ROM-resident Z80 Tiny BASIC interpreter |
| `B` or `BASIC` | Shortcut for `G 1000` | | `B` or `BASIC` | Shortcut for `G 1400` |
| `T` or `TAPE` | Start the ROM-resident tape loader at `3000` |
| `HELP` | Show the command summary | | `HELP` | Show the command summary |
`G` transfers control with a return address on the stack, so code ending in `G` transfers control with a return address on the stack, so code ending in
@@ -55,12 +60,17 @@ python3 program_ihex.py /dev/ttyUSB0 program.hex
``` ```
The default connection is 2400 baud, the default block size is 16 bytes, and The default connection is 2400 baud, the default block size is 16 bytes, and
three retries are allowed after the initial attempt. For example: 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:
```sh ```sh
python3 program_ihex.py /dev/ttyUSB0 program.hex --retries 5 --block-size 24 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 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 `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 its `>` prompt before starting the upload. Only data records present in the HEX
@@ -72,14 +82,17 @@ The outputs are:
- `build/metal/monitor.bin`: complete 32 KiB ROM image - `build/metal/monitor.bin`: complete 32 KiB ROM image
- `build/metal/monitor.prog.bin`: compact image ending at the last occupied byte - `build/metal/monitor.prog.bin`: compact image ending at the last occupied byte
- `build/tinybasic/tinybasic.bin`: standalone relocated Tiny BASIC payload - `build/tinybasic/tinybasic.bin`: standalone relocated Tiny BASIC payload
- `build/casette/tape_loader.bin`: standalone relocated tape-loader payload
The current compact image is 6,157 bytes. Its ROM layout is: The current compact image is 12,318 bytes. Its ROM layout is:
- `0000-007F`: reset/copy bootstrap - `0000-007F`: reset/copy bootstrap
- `0080-0FEA`: packed BIOS+monitor image copied to RAM at `C000-CF6A` - `0080-1201`: packed BIOS+monitor image copied to RAM at `C000-D181`
- `0FEB-0FFF`: unused space reserved for the resident image to grow - `1202-13FF`: unused space reserved for the resident image to grow
- `1000-180C`: Tiny BASIC, executed directly from ROM - `1400-1C0C`: Tiny BASIC, executed directly from ROM
- `180D-7FFF`: unused space available for additional ROM content - `1C0D-2FFF`: unused ROM
- `3000-301D`: tape loader, executed directly from ROM
- `301E-7FFF`: unused space available for additional ROM content
The resident image starts at `C000`, its IM 2 vector table is at `C040`, and 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 linked code starts at `C080`. The bootstrap obtains the resident copy length
@@ -90,8 +103,8 @@ The resulting RAM layout is:
- `8000-AFFF`: Tiny BASIC workspace and BASIC program text - `8000-AFFF`: Tiny BASIC workspace and BASIC program text
- `B000-B077`: Tiny BASIC variables and input buffer - `B000-B077`: Tiny BASIC variables and input buffer
- `B078-BFFF`: Tiny BASIC stack space - `B078-BFFF`: Tiny BASIC stack space
- `C000-CF6A`: resident BIOS, vectors, services, and monitor - `C000-D181`: resident BIOS, vectors, services, and monitor
- `CF6B-EFFF`: free space for resident-image growth - `D182-EFFF`: free space for resident-image growth
- `F000-FFFF`: BIOS globals, UART buffers, and monitor stack - `F000-FFFF`: BIOS globals, UART buffers, and monitor stack
The BIOS and monitor continue to work if low ROM is disabled. ROM-resident The BIOS and monitor continue to work if low ROM is disabled. ROM-resident
@@ -119,6 +132,7 @@ Assembly applications include `include/bios.inc` and call these fixed entries:
| `C062` | `BIOS_MONITOR` | Warm-start the monitor; does not return | | `C062` | `BIOS_MONITOR` | Warm-start the monitor; does not return |
| `C065` | `BIOS_BANK` | `A=0`: split lower 48K/upper 16K; otherwise select upper 64K | | `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 | | `C068` | `BIOS_ROM_DISABLE` | Disable ROM while retaining the selected RAM mapping |
| `C06B` | `BIOS_ROM_ENABLE` | Enable ROM while retaining the selected RAM mapping |
The service wrappers preserve working registers except for documented return The service wrappers preserve working registers except for documented return
values. The monitor uses the corresponding C API declared in `bios.h`. values. The monitor uses the corresponding C API declared in `bios.h`.
@@ -127,38 +141,47 @@ 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` shadow byte initialized to `06` by the copied resident image. `BIOS_BANK`
always leaves banking enabled and changes only the bank selection while always leaves banking enabled and changes only the bank selection while
retaining the shadowed ROM-disable bit and auxiliary-output bits. retaining the shadowed ROM-disable bit and auxiliary-output bits.
`BIOS_ROM_DISABLE` sets the ROM-disable bit and is idempotent. Code should not `BIOS_ROM_DISABLE` sets the ROM-disable bit and `BIOS_ROM_ENABLE` clears it;
write port `00` through `BIOS_IO_OUT` or the monitor's `O` command after using both are idempotent. Code should not write port `00` through `BIOS_IO_OUT` or
these services, because such writes bypass the BIOS shadow state. 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 The equivalent C API is `bios_bank(uint8_t bank)` and
`bios_rom_disable(void)`. A ROM-resident caller cannot return normally after `bios_rom_disable(void)` / `bios_rom_enable(void)`. A ROM-resident caller cannot
selecting the complete upper 64 KiB or disabling ROM, because its return return normally after selecting the complete upper 64 KiB or disabling ROM,
address is no longer mapped; it must arrange its continuation in RAM. because its return address is no longer mapped; it must arrange its
continuation in RAM.
Project organization: Project organization:
- `boot.asm`: ROM bootstrap, high vectors, BIOS jump table, and assembly glue - `boot.asm`: ROM bootstrap, high vectors, BIOS jump table, and assembly glue
- `bios.c` / `bios.h`: UART, buffers, interrupts, and resident BIOS API - `bios.c` / `bios.h`: UART, buffers, interrupts, and resident BIOS API
- `monitor.c`: command interpreter implemented on the BIOS API - `monitor.c`: command interpreter implemented on the BIOS API
- `apps/tinybasic.asm`: first ROM-resident application - `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 - `apps/README.md`: ROM application memory and integration contract
- `include/bios.inc`: stable assembly interface for ROM applications - `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
Tiny BASIC is built from `apps/tinybasic.asm`. The port replaces fixed low-page Tiny BASIC is built from `apps/tinybasic/tinybasic.asm`. The port replaces fixed
`RST` calls with relocatable calls and uses a marker-based command table so it low-page `RST` calls with relocatable calls and uses a marker-based command
can be linked at a nonzero ROM address. 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 Tiny BASIC is linked at and runs directly from ROM address `1400`. Start it from
the monitor with: the monitor with:
```text ```text
B B
``` ```
`BASIC` and `G 1000` are equivalent alternatives. `BASIC` and `G 1400` are equivalent alternatives.
Tiny BASIC calls the BIOS at `C050` for output and `C053` for non-blocking 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 input. Tiny BASIC keeps interrupts enabled so it can share the BIOS-configured
@@ -174,5 +197,12 @@ returns to the Tiny BASIC prompt.
Tiny BASIC resets the stack pointer when it starts, so it does not currently 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 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 monitor. The tape loader occupies the later ROM slot at `3000-301D`; the first
aligned starting point for another ROM payload. unused byte after it is `301E`.
## 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.
+16 -12
View File
@@ -7,25 +7,29 @@ directory instead of accessing the UART hardware directly.
The current allocation is: The current allocation is:
- `1000-180C`: Tiny BASIC - `1400-1C0C`: Tiny BASIC
- `180D-7FFF`: available for more applications (`1900` is the next convenient - `1C0D-2FFF`: available
- `3000-301D`: tape loader
- `301E-7FFF`: available for more applications (`3100` is the next convenient
aligned origin) aligned origin)
The resident BIOS and monitor own high RAM at `C000-FFFF`. Tiny BASIC owns The resident BIOS and monitor own high RAM at `C000-FFFF`. Tiny BASIC owns
`8000-BFFF` for its workspace and stack. A future application may reuse Tiny `8000-BFFF` for its workspace and stack. Applications may reuse this lower-RAM
BASIC's lower-RAM allocation because ROM applications run one at a time, but it allocation because they run one at a time, but they must leave `C000-FFFF`
must leave `C000-FFFF` untouched. untouched.
Applications using the interrupt-driven console must leave interrupt mode 2, Applications using the interrupt-driven console must leave interrupt mode 2,
the `I` register, and the BIOS UART/CTC configuration intact. They may return 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 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. non-returning monitor warm start after replacing the stack pointer.
Use `BIOS_BANK` and `BIOS_ROM_DISABLE` for the write-only banking latch rather Use `BIOS_BANK`, `BIOS_ROM_DISABLE`, and `BIOS_ROM_ENABLE` for the write-only
than writing port `00` directly. The BIOS keeps the ROM-disable state when the banking latch rather than writing port `00` directly. The BIOS keeps the
RAM bank selection changes. A ROM application must not expect a normal `RET` ROM-disable state when the RAM bank selection changes. A ROM application must
after it maps out the ROM containing its return address. 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, Application origins and the resident-memory layout are defined centrally in
overlay its binary in the `monitor.prog.bin` recipe, and add a monitor command `rom_layout.mk`. The build turns that file into C and assembly includes. To add
or use `G address` to launch it. an application, add its address there, add its build and overlay rules to the
Makefile, and add a monitor command or use `G address` to launch it.
+199
View File
@@ -0,0 +1,199 @@
Name casette;
PartNo 00;
Date 2026-10-03;
Revision 01;
Designer joppe;
Company unknown;
Assembly None;
Location unknown;
Device g22v10;
/* ------------------------------------------------------------
Constants
------------------------------------------------------------ */
ar = 'b'0;
sp = 'b'0;
/* ------------------------------------------------------------
Pin assignments
Physical signals IOSEL, RD and WR are active-low.
Their polarity is expressed in the equations with !.
------------------------------------------------------------ */
/* inputs */
PIN 1 = CLK;
PIN 2 = A1;
PIN 3 = A2;
PIN 4 = IOSEL;
PIN 5 = RD;
PIN 6 = A0;
PIN 7 = COMP;
PIN 8 = WR;
/* data bus */
PIN 14 = D0;
PIN 15 = D1;
PIN 16 = D2;
PIN 17 = D3;
PIN 18 = D4;
PIN 19 = D5;
PIN 20 = D6;
PIN 21 = D7;
/* registered output */
PIN 22 = LED;
/* ------------------------------------------------------------
Read register at offset 0
Conditions:
/IOSEL = 0
/RD = 0
/WR = 1
A2:A0 = 000
Result:
D0 = COMP
D7..D1 = 0
------------------------------------------------------------ */
D0 = COMP;
D1 = 'b'0;
D2 = 'b'0;
D3 = 'b'0;
D4 = 'b'0;
D5 = 'b'0;
D6 = 'b'0;
D7 = 'b'0;
D0.OE =
!IOSEL
& !RD
& WR
& !A2
& !A1
& !A0;
D1.OE =
!IOSEL
& !RD
& WR
& !A2
& !A1
& !A0;
D2.OE =
!IOSEL
& !RD
& WR
& !A2
& !A1
& !A0;
D3.OE =
!IOSEL
& !RD
& WR
& !A2
& !A1
& !A0;
D4.OE =
!IOSEL
& !RD
& WR
& !A2
& !A1
& !A0;
D5.OE =
!IOSEL
& !RD
& WR
& !A2
& !A1
& !A0;
D6.OE =
!IOSEL
& !RD
& WR
& !A2
& !A1
& !A0;
D7.OE =
!IOSEL
& !RD
& WR
& !A2
& !A1
& !A0;
/* ------------------------------------------------------------
Writable LED register
Write conditions:
/IOSEL = 0
/RD = 1
/WR = 0
A2:A0 = 000
During such a write:
LED.D = D0
Otherwise:
LED.D = LED
Pin 1 (CLK) clocks the registered macrocell.
------------------------------------------------------------ */
LED.D =
(
!IOSEL
& RD
& !WR
& !A2
& !A1
& !A0
& D0
)
#
(
(
IOSEL
# !RD
# WR
# A2
# A1
# A0
)
& LED
);
LED.ar = ar;
LED.sp = sp;
+54
View File
@@ -0,0 +1,54 @@

CUPL(WM) 5.0a Serial# MW-10400000
Device g22v10 Library DLIB-h-40-1
Created Sat Oct 03 16:22:02 2026
Name casette
Partno 00
Revision 01
Date 2026-10-03
Designer joppe
Company unknown
Assembly None
Location unknown
*QP24
*QF5892
*G1
*F0
*L00416 00000000000000000000000011111111
*L00448 11111111111111111111111111111111
*L00480 11111111101110111011011110111111
*L00512 10111111110111111111111011110111
*L00544 11111111111111111111111111111111
*L00576 11101111111110111111111111111111
*L00608 11111111111111101111111111111111
*L00640 11110111111111111111111111100111
*L00672 11111111111111111111111111111111
*L00704 11110110111111111111111111111111
*L00736 11111111111111111110111111111111
*L00768 01111111111111111111111100000000
*L00896 00000000000000000000000000001111
*L00928 10111011101110111011111101111111
*L00960 11111111000000000000000000000000
*L01472 00000000000000000000000011111011
*L01504 10111011101110111111011111111111
*L01536 11110000000000000000000000000000
*L02144 00000000000011111011101110111011
*L02176 10111111011111111111111100000000
*L02880 00000000000000000000000011111011
*L02912 10111011101110111111011111111111
*L02944 11110000000000000000000000000000
*L03648 00001111101110111011101110111111
*L03680 01111111111111110000000000000000
*L04288 00000000000000000000000011111011
*L04320 10111011101110111111011111111111
*L04352 11110000000000000000000000000000
*L04864 00000000000000000000111110111011
*L04896 10111011101111110111111111111111
*L05344 00000000000000000000000011111011
*L05376 10111011101110111111011111111111
*L05408 11111111111111111111111111110111
*L05440 11111111111111110000000000000000
*L05792 00000000000000000010111111111111
*L05824 11110011000000110000000000000000
*C5BE4
*6A87
+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env python3
"""
z80_tape_encode.py
Encode a binary file as a mono PCM WAV using robust two-tone Manchester FSK.
Defaults:
- 48 kHz sample rate
- 600 bit/s
- 2 kHz / 4 kHz tones
- Manchester FSK:
0 -> low tone for half a bit, then high tone
1 -> high tone for half a bit, then low tone
- 1 second 4 kHz leader
- 64 bytes of 0x55 preamble
- 256-byte CRC-protected blocks
- CRC-16/CCITT-FALSE
"""
from __future__ import annotations
import argparse
import math
import struct
import wave
from pathlib import Path
SYNC = b"\xD3\x91"
PREAMBLE_BYTE = 0x55
def crc16_ccitt_false(data: bytes) -> int:
crc = 0xFFFF
for byte in data:
crc ^= byte << 8
for _ in range(8):
crc = ((crc << 1) ^ 0x1021) & 0xFFFF if (crc & 0x8000) else (crc << 1) & 0xFFFF
return crc
class FSKWriter:
def __init__(self, sample_rate: int, baud: int, f_low: float, f_high: float, amplitude: float):
if sample_rate % (2 * baud) != 0:
raise ValueError("sample_rate must be divisible by 2*baud")
self.sample_rate = sample_rate
self.baud = baud
self.f_low = f_low
self.f_high = f_high
self.amplitude = amplitude
self.samples_per_half_bit = sample_rate // (2 * baud)
self.phase = 0.0
self.samples = bytearray()
def _tone(self, frequency: float, sample_count: int) -> None:
phase_step = 2.0 * math.pi * frequency / self.sample_rate
scale = int(32767 * self.amplitude)
phase = self.phase
out = self.samples
for _ in range(sample_count):
sample = int(round(scale * math.sin(phase)))
out.extend(struct.pack("<h", sample))
phase += phase_step
if phase >= 2.0 * math.pi:
phase -= 2.0 * math.pi
self.phase = phase
def leader(self, seconds: float) -> None:
self._tone(self.f_high, round(seconds * self.sample_rate))
def bit(self, value: int) -> None:
# Manchester-FSK:
# 0 = low -> high
# 1 = high -> low
if value:
first, second = self.f_high, self.f_low
else:
first, second = self.f_low, self.f_high
self._tone(first, self.samples_per_half_bit)
self._tone(second, self.samples_per_half_bit)
def byte(self, value: int) -> None:
# MSB first
for bit_no in range(7, -1, -1):
self.bit((value >> bit_no) & 1)
def bytes(self, data: bytes) -> None:
for value in data:
self.byte(value)
def make_block(sequence: int, payload: bytes) -> bytes:
header = struct.pack("<HH", sequence & 0xFFFF, len(payload))
crc = crc16_ccitt_false(header + payload)
return SYNC + header + payload + struct.pack("<H", crc)
def make_eof_block(sequence: int) -> bytes:
header = struct.pack("<HH", sequence & 0xFFFF, 0)
crc = crc16_ccitt_false(header)
return SYNC + header + struct.pack("<H", crc)
def encode(
input_path: Path,
output_path: Path,
*,
sample_rate: int = 48_000,
baud: int = 600,
f_low: float = 2_000.0,
f_high: float = 4_000.0,
leader_seconds: float = 1.0,
preamble_bytes: int = 64,
block_size: int = 256,
amplitude: float = 0.75,
) -> None:
data = input_path.read_bytes()
if not 0 < amplitude <= 1.0:
raise ValueError("amplitude must be > 0 and <= 1")
if not 1 <= block_size <= 0xFFFF:
raise ValueError("block_size must be in 1..65535")
writer = FSKWriter(sample_rate, baud, f_low, f_high, amplitude)
writer.leader(leader_seconds)
writer.bytes(bytes([PREAMBLE_BYTE]) * preamble_bytes)
sequence = 0
for start in range(0, len(data), block_size):
payload = data[start:start + block_size]
writer.bytes(make_block(sequence, payload))
sequence = (sequence + 1) & 0xFFFF
writer.bytes(make_eof_block(sequence))
with wave.open(str(output_path), "wb") as wav:
wav.setnchannels(1)
wav.setsampwidth(2)
wav.setframerate(sample_rate)
wav.writeframes(writer.samples)
duration = len(writer.samples) / 2 / sample_rate
print(f"Input: {input_path}")
print(f"Output: {output_path}")
print(f"Input bytes: {len(data)}")
print(f"Bit rate: {baud} bit/s")
print(f"Tones: {f_low:g} Hz / {f_high:g} Hz")
print(f"Sample rate: {sample_rate} Hz")
print(f"Half-bit: {writer.samples_per_half_bit} samples")
print(f"Block size: {block_size} bytes")
print(f"WAV duration: {duration:.2f} s")
def main() -> None:
parser = argparse.ArgumentParser(description="Encode a binary file as Manchester two-tone FSK WAV.")
parser.add_argument("input", type=Path, help="binary input file")
parser.add_argument("output", type=Path, nargs="?", help="output WAV; default: INPUT.wav")
parser.add_argument("--baud", type=int, default=600)
parser.add_argument("--low", type=float, default=2000.0, help="low tone in Hz")
parser.add_argument("--high", type=float, default=4000.0, help="high tone in Hz")
parser.add_argument("--sample-rate", type=int, default=48000)
parser.add_argument("--leader", type=float, default=1.0, help="leader seconds")
parser.add_argument("--preamble", type=int, default=64, help="number of 0x55 preamble bytes")
parser.add_argument("--block-size", type=int, default=64)
parser.add_argument("--amplitude", type=float, default=0.75)
args = parser.parse_args()
output = args.output or args.input.with_name(args.input.name + ".wav")
encode(
args.input,
output,
sample_rate=args.sample_rate,
baud=args.baud,
f_low=args.low,
f_high=args.high,
leader_seconds=args.leader,
preamble_bytes=args.preamble,
block_size=args.block_size,
amplitude=args.amplitude,
)
if __name__ == "__main__":
main()
+50
View File
@@ -0,0 +1,50 @@
.include "bios.inc"
.area TAPELOAD (ABS)
.org TAPE_LOADER_ROM_ADDRESS
COMP_PORT = 0x18
WORK_BASE = 0x0B000
LOAD_START = 0x08000
LOAD_LIMIT_HI = 0x0B0
STACK_TOP = 0x0BFFF
OLD_SP = WORK_BASE+0x00
start:
ld (OLD_SP), sp
ld sp, #STACK_TOP
di
poll_loop:
; Read from the hardware port COMP_PORT
in a, (COMP_PORT)
out (COMP_PORT), a
; Save the raw port status in register B
ld b, a
; Print a carriage return '\r' (ASCII 0x0D)
ld a, #0x0D
call BIOS_PUTC
; Restore the port status and isolate the relevant bit
; (Assuming bit 0 determines 0 or 1; adjust 'and' mask if it is a different bit)
ld a, b
and #0x01
; Convert the binary 0 or 1 into ASCII '0' (0x30) or '1' (0x31)
add a, #0x30
; Print the '0' or '1' character
call BIOS_PUTC
; Infinite loop: poll continuously
jr poll_loop
ld sp, (OLD_SP)
ei
ret
@@ -42,7 +42,7 @@
; Adapted to work with GNU z80asm ; Adapted to work with GNU z80asm
; Added interrupt handling ; Added interrupt handling
; Notes for this monitor port: ; Notes for this monitor port:
; Converted to sdasz80 syntax and linked as a ROM application at 1000. ; Converted to sdasz80 syntax and linked as a ROM application at 2200.
; Fixed-page RST calls were converted to local CALLs. ; Fixed-page RST calls were converted to local CALLs.
; UART access uses the resident BIOS ABI at C050/C053. ; UART access uses the resident BIOS ABI at C050/C053.
@@ -102,7 +102,7 @@ STKLMT = 0xB078 ;LOWEST ALLOWED STACK ADDRESS
;************************************************************* ;*************************************************************
.area TINYBASIC (ABS) .area TINYBASIC (ABS)
.org 0x1000 .org TINYBASIC_ROM_ADDRESS
main: main:
START: START:
LD SP,#STACK ;*** COLD START *** LD SP,#STACK ;*** COLD START ***
+1
View File
@@ -14,6 +14,7 @@ void bios_flush(void);
/* Write-only banking latch services. */ /* Write-only banking latch services. */
void bios_bank(uint8_t bank); void bios_bank(uint8_t bank);
void bios_rom_disable(void); void bios_rom_disable(void);
void bios_rom_enable(void);
/* Low-level routines implemented by the resident assembly BIOS. */ /* Low-level routines implemented by the resident assembly BIOS. */
void io_out(uint8_t port, uint8_t value); void io_out(uint8_t port, uint8_t value);
+27 -21
View File
@@ -1,7 +1,5 @@
STACK_TOP = 0xFFFF STACK_TOP = 0xFFFF
DELAY_LOOPS_PER_MS = 40 DELAY_LOOPS_PER_MS = 40
RESIDENT_BASE = 0xC000
RESIDENT_ROM_SOURCE = 0x0080
BANK_CONTROL_PORT = 0x00 BANK_CONTROL_PORT = 0x00
BOOT_BANK_MAPPING = 0x06 ; ROM on, banking on, upper 16K at C000-FFFF BOOT_BANK_MAPPING = 0x06 ; ROM on, banking on, upper 16K at C000-FFFF
ROM_DISABLE_BIT = 0x01 ROM_DISABLE_BIT = 0x01
@@ -24,6 +22,7 @@ BANK_MODE_CLEAR_MASK = 0xF9 ; preserve ROM-disable and auxiliary outputs
.globl _bios_init .globl _bios_init
.globl _bios_bank .globl _bios_bank
.globl _bios_rom_disable .globl _bios_rom_disable
.globl _bios_rom_enable
.globl _bank_control_shadow .globl _bank_control_shadow
.globl resident_image_end .globl resident_image_end
.globl s__DATA .globl s__DATA
@@ -44,34 +43,34 @@ boot_copy:
ld a, #BOOT_BANK_MAPPING ld a, #BOOT_BANK_MAPPING
out (BANK_CONTROL_PORT), a out (BANK_CONTROL_PORT), a
ld sp, #STACK_TOP ld sp, #STACK_TOP
ld hl, #RESIDENT_ROM_SOURCE ld hl, #RESIDENT_ROM_OFFSET
ld de, #RESIDENT_BASE ld de, #RESIDENT_RAM_BASE
ld bc, #(resident_image_end - RESIDENT_BASE) ld bc, #(resident_image_end - RESIDENT_RAM_BASE)
ldir ldir
jp init jp init
.area _ITABLE (ABS) .area _ITABLE (ABS)
; The executable vector page is part of the image copied to high RAM. ; The executable vector page is part of the image copied to high RAM.
.org RESIDENT_BASE .org RESIDENT_RAM_BASE
jp init jp init
.org RESIDENT_BASE + 0x08 .org RESIDENT_RAM_BASE + 0x08
reti reti
.org RESIDENT_BASE + 0x10 .org RESIDENT_RAM_BASE + 0x10
reti reti
.org RESIDENT_BASE + 0x18 .org RESIDENT_RAM_BASE + 0x18
reti reti
.org RESIDENT_BASE + 0x20 .org RESIDENT_RAM_BASE + 0x20
reti reti
.org RESIDENT_BASE + 0x28 .org RESIDENT_RAM_BASE + 0x28
reti reti
.org RESIDENT_BASE + 0x30 .org RESIDENT_RAM_BASE + 0x30
reti reti
.org RESIDENT_BASE + 0x38 .org RESIDENT_RAM_BASE + 0x38
reti reti
; IM 2 table: the CTC supplies vectors 40, 42, 44, and 46. ; IM 2 table: the CTC supplies vectors 40, 42, 44, and 46.
.org RESIDENT_BASE + 0x40 .org RESIDENT_RAM_BASE + 0x40
.dw ctc_isr ; CTC channel 0: vector 0x40 .dw ctc_isr ; CTC channel 0: vector 0x40
.dw ctc_isr ; CTC channel 1: vector 0x42 .dw ctc_isr ; CTC channel 1: vector 0x42
.dw ctc_isr ; CTC channel 2: vector 0x44 .dw ctc_isr ; CTC channel 2: vector 0x44
@@ -80,7 +79,7 @@ boot_copy:
; Stable BIOS jump table for ROM-resident applications. ; Stable BIOS jump table for ROM-resident applications.
; C050 putc(A), C053 try_getc(), C056 getc(), C059 io_out(A,L), ; 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. ; C065 bank(A), C068 disable ROM, C06B enable ROM.
.org BIOS_PUTC .org BIOS_PUTC
jp bios_service_putc jp bios_service_putc
.org BIOS_TRY_GETC .org BIOS_TRY_GETC
@@ -99,11 +98,13 @@ boot_copy:
jp _bios_bank jp _bios_bank
.org BIOS_ROM_DISABLE .org BIOS_ROM_DISABLE
jp _bios_rom_disable jp _bios_rom_disable
.org BIOS_ROM_ENABLE
jp _bios_rom_enable
; Port 00 is write-only. Keep its last BIOS-managed value outside the C data ; 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 ; 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. ; this same value in the hardware before the resident image is copied.
.org RESIDENT_BASE + 0x70 .org RESIDENT_RAM_BASE + 0x70
_bank_control_shadow:: _bank_control_shadow::
.db BOOT_BANK_MAPPING .db BOOT_BANK_MAPPING
@@ -184,16 +185,21 @@ bios_bank_update:
pop af pop af
ret ret
; Permanently hide ROM until reset, preserving the selected RAM mapping. ; Hide ROM while preserving the selected RAM mapping.
; A and flags are preserved. ; A and flags are preserved.
_bios_rom_disable:: _bios_rom_disable::
push af push af
ld a, (_bank_control_shadow) ld a, (_bank_control_shadow)
or #ROM_DISABLE_BIT or #ROM_DISABLE_BIT
ld (_bank_control_shadow), a jr bios_bank_update
out (BANK_CONTROL_PORT), a
pop af ; Make ROM visible again while preserving the selected RAM mapping.
ret ; A and flags are preserved.
_bios_rom_enable::
push af
ld a, (_bank_control_shadow)
and #0xFE
jr bios_bank_update
; BIOS ABI services preserve registers except documented return values. ; BIOS ABI services preserve registers except documented return values.
bios_service_putc: bios_service_putc:
+13 -10
View File
@@ -1,10 +1,13 @@
; Stable assembly ABI exported by the resident BIOS at C000. .include "rom_layout.inc"
BIOS_PUTC = 0xC050 ; A = character; preserves all registers
BIOS_TRY_GETC = 0xC053 ; returns A = character or 0; preserves BC..IY ; Stable assembly ABI exported by the resident BIOS.
BIOS_GETC = 0xC056 ; blocking; returns character in A BIOS_PUTC = RESIDENT_RAM_BASE + 0x50 ; A = character; preserves all registers
BIOS_IO_OUT = 0xC059 ; A = port, L = value; preserves all registers BIOS_TRY_GETC = RESIDENT_RAM_BASE + 0x53 ; returns A = character or 0; preserves BC..IY
BIOS_IO_IN = 0xC05C ; A = port; returns value in A BIOS_GETC = RESIDENT_RAM_BASE + 0x56 ; blocking; returns character in A
BIOS_DELAY_MS = 0xC05F ; HL = milliseconds; preserves all registers BIOS_IO_OUT = RESIDENT_RAM_BASE + 0x59 ; A = port, L = value; preserves all registers
BIOS_MONITOR = 0xC062 ; warm-start the monitor; does not return BIOS_IO_IN = RESIDENT_RAM_BASE + 0x5C ; A = port; returns value in A
BIOS_BANK = 0xC065 ; A=0 split 48K/16K, otherwise upper 64K BIOS_DELAY_MS = RESIDENT_RAM_BASE + 0x5F ; HL = milliseconds; preserves all registers
BIOS_ROM_DISABLE = 0xC068 ; disable ROM while preserving the bank selection BIOS_MONITOR = RESIDENT_RAM_BASE + 0x62 ; warm-start the monitor; does not return
BIOS_BANK = RESIDENT_RAM_BASE + 0x65 ; A=0 split 48K/16K, otherwise upper 64K
BIOS_ROM_DISABLE = RESIDENT_RAM_BASE + 0x68 ; disable ROM while preserving the bank selection
BIOS_ROM_ENABLE = RESIDENT_RAM_BASE + 0x6B ; enable ROM while preserving the bank selection
+49 -1
View File
@@ -1,5 +1,6 @@
#include <stdint.h> #include <stdint.h>
#include "bios.h" #include "bios.h"
#include "rom_layout.h"
#define LINE_SIZE 80 #define LINE_SIZE 80
#define MAX_ARGS 40 #define MAX_ARGS 40
@@ -97,8 +98,11 @@ static void print_help(void)
" W addr byte... Write memory\r\n" " W addr byte... Write memory\r\n"
" I port Read I/O\r\n" " I port Read I/O\r\n"
" O port byte Write I/O\r\n" " O port byte Write I/O\r\n"
" K bank Bank: 0=split, 1=upper 64K\r\n"
" R ON|OFF Enable or disable ROM\r\n"
" G addr Execute\r\n" " G addr Execute\r\n"
" B Tiny BASIC\r\n" " B Tiny BASIC\r\n"
" T Tape loader\r\n"
" H Help\r\n"); " H Help\r\n");
} }
@@ -204,6 +208,35 @@ static void command_out(uint8_t argc, char **argv)
bios_puts("OK\r\n"); bios_puts("OK\r\n");
} }
static void command_bank(uint8_t argc, char **argv)
{
uint16_t bank;
if (argc != 2 || !parse_hex(argv[1], &bank) || bank > 1) {
bios_puts("Usage: BANK 0|1\r\n");
return;
}
bios_bank((uint8_t)bank);
bios_puts("OK\r\n");
}
static void command_rom(uint8_t argc, char **argv)
{
if (argc != 2) {
bios_puts("Usage: ROM ON|OFF\r\n");
return;
}
if (strings_equal(argv[1], "on")) {
bios_rom_enable();
} else if (strings_equal(argv[1], "off")) {
bios_rom_disable();
} else {
bios_puts("Usage: ROM ON|OFF\r\n");
return;
}
bios_puts("OK\r\n");
}
static void execute_address(uint16_t address) static void execute_address(uint16_t address)
{ {
bios_puts("Executing at "); bios_puts("Executing at ");
@@ -231,7 +264,16 @@ static void command_basic(uint8_t argc)
bios_puts("Usage: B\r\n"); bios_puts("Usage: B\r\n");
return; return;
} }
execute_address(0x1000); execute_address(TINYBASIC_ROM_ADDRESS);
}
static void command_tape(uint8_t argc)
{
if (argc != 1) {
bios_puts("Usage: T\r\n");
return;
}
execute_address(TAPE_LOADER_ROM_ADDRESS);
} }
static void execute_line(char *line) static void execute_line(char *line)
@@ -253,10 +295,16 @@ static void execute_line(char *line)
command_in(argc, argv); command_in(argc, argv);
} else if (strings_equal(argv[0], "o") || strings_equal(argv[0], "out")) { } else if (strings_equal(argv[0], "o") || strings_equal(argv[0], "out")) {
command_out(argc, argv); command_out(argc, argv);
} else if (strings_equal(argv[0], "k") || strings_equal(argv[0], "bank")) {
command_bank(argc, argv);
} else if (strings_equal(argv[0], "r") || strings_equal(argv[0], "rom")) {
command_rom(argc, argv);
} else if (strings_equal(argv[0], "g") || strings_equal(argv[0], "go")) { } else if (strings_equal(argv[0], "g") || strings_equal(argv[0], "go")) {
command_go(argc, argv); command_go(argc, argv);
} else if (strings_equal(argv[0], "b") || strings_equal(argv[0], "basic")) { } else if (strings_equal(argv[0], "b") || strings_equal(argv[0], "basic")) {
command_basic(argc); command_basic(argc);
} else if (strings_equal(argv[0], "t") || strings_equal(argv[0], "tape")) {
command_tape(argc);
} else { } else {
bios_puts("Unknown command; type HELP\r\n"); bios_puts("Unknown command; type HELP\r\n");
} }
+35 -12
View File
@@ -6,6 +6,7 @@ from __future__ import annotations
import argparse import argparse
import re import re
import sys import sys
import time
from pathlib import Path from pathlib import Path
try: try:
@@ -132,7 +133,8 @@ def make_blocks(memory: dict[int, int], block_size: int) -> list[tuple[int, byte
class Monitor: class Monitor:
def __init__(self, port: str, baud: int, timeout: float): def __init__(self, port: str, baud: int, timeout: float, char_delay: float):
self.char_delay = char_delay
try: try:
self.serial = serial.Serial( self.serial = serial.Serial(
port=port, port=port,
@@ -151,18 +153,32 @@ class Monitor:
def command(self, command: str) -> str: def command(self, command: str) -> str:
try: try:
self.serial.write(command.encode("ascii") + b"\r") for value in command.encode("ascii") + b"\r":
self.serial.flush() self.serial.write(bytes((value,)))
self.serial.flush()
if self.char_delay:
time.sleep(self.char_delay)
response = self.serial.read_until(b"> ") response = self.serial.read_until(b"> ")
except serial.SerialException as exc: except serial.SerialException as exc:
raise ProgramError(f"serial communication failed: {exc}") from exc raise ProgramError(f"serial communication failed: {exc}") from exc
if not response.endswith(b"> "): if not response.endswith(b"> "):
raise ProgramError(f"timeout waiting for monitor after '{command}'") text = response.decode("ascii", errors="replace")
raise ProgramError(
f"timeout waiting for monitor after '{command}'; partial={text!r}"
)
return response.decode("ascii", errors="replace") return response.decode("ascii", errors="replace")
def synchronize(self) -> None: def synchronize(self) -> None:
try: try:
self.serial.reset_input_buffer() self.serial.reset_input_buffer()
# reset_input_buffer() cannot discard bytes which are still queued
# in the computer or USB adapter. Read until the line has stayed
# quiet before generating a fresh, unambiguous prompt.
normal_timeout = self.serial.timeout
self.serial.timeout = min(normal_timeout, 0.2)
while self.serial.read(256):
pass
self.serial.timeout = normal_timeout
except serial.SerialException as exc: except serial.SerialException as exc:
raise ProgramError(f"serial communication failed: {exc}") from exc raise ProgramError(f"serial communication failed: {exc}") from exc
response = self.command("") response = self.command("")
@@ -187,14 +203,14 @@ def write_and_verify(monitor: Monitor, start: int, data: bytes) -> tuple[bool, s
values = " ".join(f"{value:02X}" for value in data) values = " ".join(f"{value:02X}" for value in data)
response = monitor.command(f"W {start:04X} {values}") response = monitor.command(f"W {start:04X} {values}")
if not response_has_ok(response): if not response_has_ok(response):
return False, "monitor did not acknowledge the write" return False, f"monitor did not acknowledge the write; response={response!r}"
end = start + len(data) - 1 end = start + len(data) - 1
expected = sum(data) & 0xFFFF expected = sum(data) & 0xFFFF
response = monitor.command(f"C {start:04X} {end:04X}") response = monitor.command(f"C {start:04X} {end:04X}")
actual = response_checksum(response) actual = response_checksum(response)
if actual is None: if actual is None:
return False, "monitor did not return a checksum" return False, f"monitor did not return a checksum; response={response!r}"
if actual != expected: if actual != expected:
return False, f"checksum mismatch (expected {expected:04X}, received {actual:04X})" return False, f"checksum mismatch (expected {expected:04X}, received {actual:04X})"
return True, "" return True, ""
@@ -225,6 +241,12 @@ def parse_args() -> argparse.Namespace:
default=5.0, default=5.0,
help="serial response timeout in seconds (default: 5)", help="serial response timeout in seconds (default: 5)",
) )
parser.add_argument(
"--char-delay",
type=float,
default=0.005,
help="delay after each transmitted character in seconds (default: 0.005)",
)
args = parser.parse_args() args = parser.parse_args()
if not 1 <= args.block_size <= MAX_BLOCK_SIZE: if not 1 <= args.block_size <= MAX_BLOCK_SIZE:
parser.error(f"--block-size must be between 1 and {MAX_BLOCK_SIZE}") parser.error(f"--block-size must be between 1 and {MAX_BLOCK_SIZE}")
@@ -234,6 +256,8 @@ def parse_args() -> argparse.Namespace:
parser.error("--baud must be positive") parser.error("--baud must be positive")
if args.timeout <= 0: if args.timeout <= 0:
parser.error("--timeout must be positive") parser.error("--timeout must be positive")
if args.char_delay < 0:
parser.error("--char-delay cannot be negative")
return args return args
@@ -249,7 +273,7 @@ def main() -> int:
f"({min(memory):04X}-{max(memory):04X})" f"({min(memory):04X}-{max(memory):04X})"
) )
monitor = Monitor(args.port, args.baud, args.timeout) monitor = Monitor(args.port, args.baud, args.timeout, args.char_delay)
monitor.synchronize() monitor.synchronize()
for number, (start, data) in enumerate(blocks, 1): for number, (start, data) in enumerate(blocks, 1):
@@ -263,9 +287,8 @@ def main() -> int:
success, reason = False, str(exc) success, reason = False, str(exc)
if success: if success:
print( print(
f"\rBlock {number}/{len(blocks)} {start:04X}-{end:04X} verified", f"Block {number}/{len(blocks)} "
end="", f"{start:04X}-{end:04X} verified"
flush=True,
) )
break break
if attempt == args.retries: if attempt == args.retries:
@@ -274,12 +297,12 @@ def main() -> int:
f"{args.retries + 1} attempts: {reason}" f"{args.retries + 1} attempts: {reason}"
) )
print( print(
f"\nRetrying {start:04X}-{end:04X} " f"Retrying {start:04X}-{end:04X} "
f"({attempt + 1}/{args.retries}): {reason}", f"({attempt + 1}/{args.retries}): {reason}",
file=sys.stderr, file=sys.stderr,
) )
print(f"\nProgrammed and verified {len(memory)} bytes") print(f"Programmed and verified {len(memory)} bytes")
return 0 return 0
except ProgramError as exc: except ProgramError as exc:
print(f"error: {exc}", file=sys.stderr) print(f"error: {exc}", file=sys.stderr)
+15
View File
@@ -0,0 +1,15 @@
# Authoritative ROM and resident-memory layout.
#
# Keep assignments in the simple "NAME := value" form: the build generates
# equivalent C and sdasz80 include files from these definitions.
ROM_SIZE := 32768
RESIDENT_RAM_BASE := 0xC000
RESIDENT_ROM_OFFSET := 0x0080
RESIDENT_CODE_BASE := 0xC080
RESIDENT_CODE_SIZE := 0x0F80
RESIDENT_DATA_BASE := 0xF000
TINYBASIC_ROM_ADDRESS := 0x1400
TAPE_LOADER_ROM_ADDRESS := 0x3000
BIN
View File
Binary file not shown.