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
|
||||
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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
+16
-12
@@ -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.
|
||||
|
||||
@@ -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
|
||||
; 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 ***
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
+13
-10
@@ -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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <stdint.h>
|
||||
#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");
|
||||
}
|
||||
|
||||
+35
-12
@@ -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)
|
||||
|
||||
@@ -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