diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..44901e7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "vs-cupl.PathOpenOcd": "/usr/sbin/openocd", + "vs-cupl.PathOpenOcdDl": "/usr/sbin/openocd" +} \ No newline at end of file diff --git a/Makefile b/Makefile index 3c862f2..e9f4be9 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,11 @@ TARGET := monitor 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_ASM_SRCFILES := boot.asm @@ -9,18 +15,16 @@ 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 --opt-code-size -METAL_CPPFLAGS := -I src -I $(BUILDDIR)/metal/generated --disable-warning 283 -METAL_BIN_SIZE := 32768 +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 $(LAYOUT_BUILD) --disable-warning 283 +METAL_BIN_SIZE := $(ROM_SIZE) METAL_RAM_SIZE := 16384 METAL_IMAGE_SIZE := 65536 -RESIDENT_BASE := 0xC000 -RESIDENT_ROM_OFFSET := 128 -TINYBASIC_BASE := 0x1000 -TINYBASIC_ROM_OFFSET := 4096 -TINYBASIC_SRC := apps/tinybasic.asm +TINYBASIC_SRC := apps/tinybasic/tinybasic.asm TINYBASIC_BUILD := $(BUILDDIR)/tinybasic +TAPE_LOADER_SRC := apps/casette/tape_loader.asm +TAPE_LOADER_BUILD := $(BUILDDIR)/casette .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 " Resident BIOS+monitor: %5u bytes (%5u code/vector + %5u static data)\n", code + data, code, data; \ }' $(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) @mkdir -p $(@D) @@ -47,31 +56,48 @@ $(BUILDDIR)/metal/$(TARGET).ihx: $(METAL_RELFILES) $(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 +$(BUILDDIR)/metal/$(TARGET).prog.bin: $(BUILDDIR)/metal/$(TARGET).flat.bin $(TINYBASIC_BUILD)/tinybasic.bin $(TAPE_LOADER_BUILD)/tape_loader.bin @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) )); \ - dd if=$< of=$@ bs=1 count=$(TINYBASIC_ROM_OFFSET) status=none; \ - dd if=$< of=$@ bs=1 skip=$$(( $(RESIDENT_BASE) )) seek=$(RESIDENT_ROM_OFFSET) count=$$payload_size conv=notrunc status=none; \ - dd if=$(TINYBASIC_BUILD)/tinybasic.bin of=$@ bs=1 seek=$(TINYBASIC_ROM_OFFSET) conv=notrunc status=none + payload_size=$$((payload_end - $(RESIDENT_RAM_BASE))); \ + payload_rom_end=$$(( $(RESIDENT_ROM_OFFSET) + payload_size )); \ + tiny_size=$$(wc -c < $(TINYBASIC_BUILD)/tinybasic.bin); \ + tape_size=$$(wc -c < $(TAPE_LOADER_BUILD)/tape_loader.bin); \ + if test $$payload_size -le 0; then \ + 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 dd if=$< of=$@ bs=$(METAL_BIN_SIZE) count=1 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) $(METAL_CC) $(METAL_FLAGS) $(METAL_CPPFLAGS) -o $@ -c $< -$(BUILDDIR)/metal/%.rel: %.asm +$(BUILDDIR)/metal/%.rel: %.asm $(LAYOUT_INCLUDE) @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) - $(METAL_AS) -Iinclude -plosgff -o $@ $< + $(METAL_AS) -Iinclude -I$(LAYOUT_BUILD) -plosgff -o $@ $< $(TINYBASIC_BUILD)/tinybasic.ihx: $(TINYBASIC_BUILD)/tinybasic.rel 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 @tiny_size=$$(awk '$$1 == "TINYBASIC0" { print $$5 + 0 }' $(TINYBASIC_BUILD)/tinybasic.map); \ 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: rm -rf $(BUILDDIR) diff --git a/README.md b/README.md index 8af579e..897df65 100644 --- a/README.md +++ b/README.md @@ -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 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`. +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 @@ -23,9 +23,14 @@ optional `0x` prefix is accepted. | `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 1000` | Start the ROM-resident Z80 Tiny BASIC interpreter | -| `B` or `BASIC` | Shortcut for `G 1000` | +| `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 @@ -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 -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 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 @@ -72,14 +82,17 @@ 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 6,157 bytes. Its ROM layout is: +The current compact image is 12,318 bytes. Its ROM layout is: - `0000-007F`: reset/copy bootstrap -- `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 +- `0080-1201`: packed BIOS+monitor image copied to RAM at `C000-D181` +- `1202-13FF`: unused space reserved for the resident image to grow +- `1400-1C0C`: Tiny BASIC, executed directly from ROM +- `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 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 - `B000-B077`: Tiny BASIC variables and input buffer - `B078-BFFF`: Tiny BASIC stack space -- `C000-CF6A`: resident BIOS, vectors, services, and monitor -- `CF6B-EFFF`: free space for resident-image growth +- `C000-D181`: resident BIOS, vectors, services, and monitor +- `D182-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 @@ -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 | | `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 | The service wrappers preserve working registers except for documented return 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` 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. +`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)`. 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. +`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 -- `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 - `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.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 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 `1000`. Start it from +Tiny BASIC is linked at and runs directly from ROM address `1400`. Start it from the monitor with: ```text 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 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 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. +monitor. The tape loader occupies the later ROM slot at `3000-301D`; the first +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. diff --git a/apps/README.md b/apps/README.md index 1c2fa9f..910576f 100644 --- a/apps/README.md +++ b/apps/README.md @@ -7,25 +7,29 @@ directory instead of accessing the UART hardware directly. The current allocation is: -- `1000-180C`: Tiny BASIC -- `180D-7FFF`: available for more applications (`1900` is the next convenient +- `1400-1C0C`: Tiny BASIC +- `1C0D-2FFF`: available +- `3000-301D`: tape loader +- `301E-7FFF`: available for more applications (`3100` is the next convenient aligned origin) 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 -BASIC's lower-RAM allocation because ROM applications run one at a time, but it -must leave `C000-FFFF` untouched. +`8000-BFFF` for its workspace and stack. Applications may reuse this lower-RAM +allocation because they run one at a time, but they must leave `C000-FFFF` +untouched. Applications using the interrupt-driven console must leave interrupt mode 2, 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. +Use `BIOS_BANK`, `BIOS_ROM_DISABLE`, and `BIOS_ROM_ENABLE` 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. +Application origins and the resident-memory layout are defined centrally in +`rom_layout.mk`. The build turns that file into C and assembly includes. To add +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. diff --git a/apps/casette/CUPL.PLD b/apps/casette/CUPL.PLD new file mode 100644 index 0000000..8ef1851 --- /dev/null +++ b/apps/casette/CUPL.PLD @@ -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; \ No newline at end of file diff --git a/apps/casette/casette.jed b/apps/casette/casette.jed new file mode 100644 index 0000000..4ae757a --- /dev/null +++ b/apps/casette/casette.jed @@ -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 \ No newline at end of file diff --git a/apps/casette/encode.py b/apps/casette/encode.py new file mode 100644 index 0000000..b29bc8d --- /dev/null +++ b/apps/casette/encode.py @@ -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("= 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(" bytes: + header = struct.pack(" 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() diff --git a/apps/casette/tape_loader.asm b/apps/casette/tape_loader.asm new file mode 100644 index 0000000..328a438 --- /dev/null +++ b/apps/casette/tape_loader.asm @@ -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 diff --git a/apps/tinybasic.asm b/apps/tinybasic/tinybasic.asm similarity index 99% rename from apps/tinybasic.asm rename to apps/tinybasic/tinybasic.asm index eb632af..a123fe0 100644 --- a/apps/tinybasic.asm +++ b/apps/tinybasic/tinybasic.asm @@ -42,7 +42,7 @@ ; Adapted to work with GNU z80asm ; Added interrupt handling ; 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. ; UART access uses the resident BIOS ABI at C050/C053. @@ -102,7 +102,7 @@ STKLMT = 0xB078 ;LOWEST ALLOWED STACK ADDRESS ;************************************************************* .area TINYBASIC (ABS) - .org 0x1000 + .org TINYBASIC_ROM_ADDRESS main: START: LD SP,#STACK ;*** COLD START *** diff --git a/bios.h b/bios.h index fc8c72b..6968a0e 100644 --- a/bios.h +++ b/bios.h @@ -14,6 +14,7 @@ void bios_flush(void); /* Write-only banking latch services. */ void bios_bank(uint8_t bank); void bios_rom_disable(void); +void bios_rom_enable(void); /* Low-level routines implemented by the resident assembly BIOS. */ void io_out(uint8_t port, uint8_t value); diff --git a/boot.asm b/boot.asm index 76b7af3..1434ffd 100644 --- a/boot.asm +++ b/boot.asm @@ -1,7 +1,5 @@ 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 @@ -24,6 +22,7 @@ BANK_MODE_CLEAR_MASK = 0xF9 ; preserve ROM-disable and auxiliary outputs .globl _bios_init .globl _bios_bank .globl _bios_rom_disable + .globl _bios_rom_enable .globl _bank_control_shadow .globl resident_image_end .globl s__DATA @@ -44,34 +43,34 @@ boot_copy: ld a, #BOOT_BANK_MAPPING out (BANK_CONTROL_PORT), a ld sp, #STACK_TOP - ld hl, #RESIDENT_ROM_SOURCE - ld de, #RESIDENT_BASE - ld bc, #(resident_image_end - RESIDENT_BASE) + ld hl, #RESIDENT_ROM_OFFSET + ld de, #RESIDENT_RAM_BASE + ld bc, #(resident_image_end - RESIDENT_RAM_BASE) ldir jp init .area _ITABLE (ABS) ; The executable vector page is part of the image copied to high RAM. -.org RESIDENT_BASE +.org RESIDENT_RAM_BASE jp init -.org RESIDENT_BASE + 0x08 +.org RESIDENT_RAM_BASE + 0x08 reti -.org RESIDENT_BASE + 0x10 +.org RESIDENT_RAM_BASE + 0x10 reti -.org RESIDENT_BASE + 0x18 +.org RESIDENT_RAM_BASE + 0x18 reti -.org RESIDENT_BASE + 0x20 +.org RESIDENT_RAM_BASE + 0x20 reti -.org RESIDENT_BASE + 0x28 +.org RESIDENT_RAM_BASE + 0x28 reti -.org RESIDENT_BASE + 0x30 +.org RESIDENT_RAM_BASE + 0x30 reti -.org RESIDENT_BASE + 0x38 +.org RESIDENT_RAM_BASE + 0x38 reti ; 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 1: vector 0x42 .dw ctc_isr ; CTC channel 2: vector 0x44 @@ -80,7 +79,7 @@ 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, -; C065 bank(A), C068 disable ROM. +; C065 bank(A), C068 disable ROM, C06B enable ROM. .org BIOS_PUTC jp bios_service_putc .org BIOS_TRY_GETC @@ -99,11 +98,13 @@ boot_copy: jp _bios_bank .org 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 ; 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 +.org RESIDENT_RAM_BASE + 0x70 _bank_control_shadow:: .db BOOT_BANK_MAPPING @@ -184,16 +185,21 @@ bios_bank_update: pop af ret -; Permanently hide ROM until reset, preserving the selected RAM mapping. +; Hide ROM while 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 + jr bios_bank_update + +; Make ROM visible again while preserving the selected RAM mapping. +; 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_service_putc: diff --git a/include/bios.inc b/include/bios.inc index 7de3f54..f651be8 100644 --- a/include/bios.inc +++ b/include/bios.inc @@ -1,10 +1,13 @@ -; Stable assembly ABI exported by the resident BIOS at C000. -BIOS_PUTC = 0xC050 ; A = character; preserves all registers -BIOS_TRY_GETC = 0xC053 ; returns A = character or 0; preserves BC..IY -BIOS_GETC = 0xC056 ; blocking; returns character in A -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 + .include "rom_layout.inc" + +; Stable assembly ABI exported by the resident BIOS. +BIOS_PUTC = RESIDENT_RAM_BASE + 0x50 ; A = character; preserves all registers +BIOS_TRY_GETC = RESIDENT_RAM_BASE + 0x53 ; returns A = character or 0; preserves BC..IY +BIOS_GETC = RESIDENT_RAM_BASE + 0x56 ; blocking; returns character in A +BIOS_IO_OUT = RESIDENT_RAM_BASE + 0x59 ; A = port, L = value; preserves all registers +BIOS_IO_IN = RESIDENT_RAM_BASE + 0x5C ; A = port; returns value in A +BIOS_DELAY_MS = RESIDENT_RAM_BASE + 0x5F ; HL = milliseconds; preserves all registers +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 diff --git a/monitor.c b/monitor.c index d919ec6..019d2d8 100644 --- a/monitor.c +++ b/monitor.c @@ -1,5 +1,6 @@ #include #include "bios.h" +#include "rom_layout.h" #define LINE_SIZE 80 #define MAX_ARGS 40 @@ -97,8 +98,11 @@ static void print_help(void) " W addr byte... Write memory\r\n" " I port Read 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" " B Tiny BASIC\r\n" + " T Tape loader\r\n" " H Help\r\n"); } @@ -204,6 +208,35 @@ static void command_out(uint8_t argc, char **argv) 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) { bios_puts("Executing at "); @@ -231,7 +264,16 @@ static void command_basic(uint8_t argc) bios_puts("Usage: B\r\n"); 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) @@ -253,10 +295,16 @@ static void execute_line(char *line) command_in(argc, argv); } else if (strings_equal(argv[0], "o") || strings_equal(argv[0], "out")) { 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")) { command_go(argc, argv); } else if (strings_equal(argv[0], "b") || strings_equal(argv[0], "basic")) { command_basic(argc); + } else if (strings_equal(argv[0], "t") || strings_equal(argv[0], "tape")) { + command_tape(argc); } else { bios_puts("Unknown command; type HELP\r\n"); } diff --git a/program_ihex.py b/program_ihex.py index 0cdf799..95372e0 100755 --- a/program_ihex.py +++ b/program_ihex.py @@ -6,6 +6,7 @@ from __future__ import annotations import argparse import re import sys +import time from pathlib import Path try: @@ -132,7 +133,8 @@ def make_blocks(memory: dict[int, int], block_size: int) -> list[tuple[int, byte 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: self.serial = serial.Serial( port=port, @@ -151,18 +153,32 @@ class Monitor: def command(self, command: str) -> str: try: - self.serial.write(command.encode("ascii") + b"\r") - self.serial.flush() + for value in command.encode("ascii") + b"\r": + self.serial.write(bytes((value,))) + self.serial.flush() + if self.char_delay: + time.sleep(self.char_delay) 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}'") + text = response.decode("ascii", errors="replace") + raise ProgramError( + f"timeout waiting for monitor after '{command}'; partial={text!r}" + ) return response.decode("ascii", errors="replace") def synchronize(self) -> None: try: 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: raise ProgramError(f"serial communication failed: {exc}") from exc 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) response = monitor.command(f"W {start:04X} {values}") 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 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" + return False, f"monitor did not return a checksum; response={response!r}" if actual != expected: return False, f"checksum mismatch (expected {expected:04X}, received {actual:04X})" return True, "" @@ -225,6 +241,12 @@ def parse_args() -> argparse.Namespace: default=5.0, 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() if not 1 <= args.block_size <= 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") if args.timeout <= 0: parser.error("--timeout must be positive") + if args.char_delay < 0: + parser.error("--char-delay cannot be negative") return args @@ -249,7 +273,7 @@ def main() -> int: 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() for number, (start, data) in enumerate(blocks, 1): @@ -263,9 +287,8 @@ def main() -> int: success, reason = False, str(exc) if success: print( - f"\rBlock {number}/{len(blocks)} {start:04X}-{end:04X} verified", - end="", - flush=True, + f"Block {number}/{len(blocks)} " + f"{start:04X}-{end:04X} verified" ) break if attempt == args.retries: @@ -274,12 +297,12 @@ def main() -> int: f"{args.retries + 1} attempts: {reason}" ) print( - f"\nRetrying {start:04X}-{end:04X} " + f"Retrying {start:04X}-{end:04X} " f"({attempt + 1}/{args.retries}): {reason}", file=sys.stderr, ) - print(f"\nProgrammed and verified {len(memory)} bytes") + print(f"Programmed and verified {len(memory)} bytes") return 0 except ProgramError as exc: print(f"error: {exc}", file=sys.stderr) diff --git a/rom_layout.mk b/rom_layout.mk new file mode 100644 index 0000000..424f48b --- /dev/null +++ b/rom_layout.mk @@ -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 diff --git a/test.wav b/test.wav new file mode 100644 index 0000000..2624c08 Binary files /dev/null and b/test.wav differ