start of tape loading. hw works and have a generation script
This commit is contained in:
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"vs-cupl.PathOpenOcd": "/usr/sbin/openocd",
|
||||||
|
"vs-cupl.PathOpenOcdDl": "/usr/sbin/openocd"
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
@@ -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 ***
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user