New -> currentt

This commit is contained in:
Joppe Blondel
2026-10-07 14:26:23 +02:00
parent 6a56786b86
commit da02d8c855
41 changed files with 46 additions and 18344 deletions
-1
View File
@@ -1,2 +1 @@
build build
__pycache__/
+46 -148
View File
@@ -1,157 +1,55 @@
TARGET := monitor .SILENT:
include memory.mk
ASM := zasm
ASMFLAGS := -w -b
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) FILES := bios
METAL_C_SRCFILES := $(wildcard *.c) SOURCES := $(addprefix ./, $(addsuffix .asm, $(FILES)))
METAL_ASM_SRCFILES := boot.asm TARGETS := $(addprefix $(BUILDDIR)/, $(addsuffix .bin, $(FILES)))
METAL_RELFILES := $(METAL_ASM_SRCFILES:%.asm=$(BUILDDIR)/metal/%.rel) \
$(METAL_C_SRCFILES:%.c=$(BUILDDIR)/metal/%.rel)
METAL_CC := sdcc TOPTARGETS := all clean size
METAL_AS := sdasz80 $(TOPTARGETS): $(SUBDIRS)
METAL_MAKEBIN := makebin
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
TINYBASIC_SRC := apps/tinybasic/tinybasic.asm all: $(BUILDDIR) $(BUILDDIR)/rom.bin size
TINYBASIC_BUILD := $(BUILDDIR)/tinybasic
TAPE_LOADER_SRC := apps/casette/tape_loader.asm
TAPE_LOADER_BUILD := $(BUILDDIR)/casette
NASCOM_BASIC_SRC := apps/nascom_basic/basic.asm
NASCOM_BASIC_BUILD := $(BUILDDIR)/nascom_basic
.DEFAULT_GOAL := metal
.PHONY: all metal clean
all: metal
metal: $(BUILDDIR)/metal/$(TARGET).ihx $(BUILDDIR)/metal/$(TARGET).bin $(BUILDDIR)/metal/$(TARGET).prog.bin
@stored=$$(wc -c < $(BUILDDIR)/metal/$(TARGET).prog.bin); \
awk -v rom_capacity=$(METAL_BIN_SIZE) -v stored=$$stored -v ram_capacity=$(METAL_RAM_SIZE) '\
$$1 ~ /^_(CODE|HOME|GSINIT|GSFINAL|INITIALIZER)$$/ || $$1 ~ /^_ITABLE/ { code += $$5 } \
$$1 ~ /^_(DATA|INITIALIZED|BSEG|BSS|HEAP)$$/ { data += $$5 } \
END { \
printf "\nMemory usage:\n"; \
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 %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) ))
@printf " NASCOM BASIC: %5u ROM bytes at %04X\n" \
$$(wc -c < $(NASCOM_BASIC_BUILD)/nascom_basic.bin) \
$$(( $(NASCOM_BASIC_ROM_ADDRESS) ))
$(BUILDDIR)/metal/$(TARGET).ihx: $(METAL_RELFILES)
@mkdir -p $(@D)
$(METAL_CC) $(METAL_FLAGS) -o $@ $(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 $(TAPE_LOADER_BUILD)/tape_loader.bin $(NASCOM_BASIC_BUILD)/nascom_basic.bin
@set -e; \
payload_end=$$(awk '$$1 == "DEF" && $$2 == "resident_image_end" { print $$3 }' $(BUILDDIR)/metal/$(TARGET).noi); \
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); \
nascom_size=$$(wc -c < $(NASCOM_BASIC_BUILD)/nascom_basic.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 $$(( $(NASCOM_BASIC_ROM_ADDRESS) )); then \
printf "error: tape loader ends at %04X, beyond NASCOM BASIC origin %04X\n" \
$$(( $(TAPE_LOADER_ROM_ADDRESS) + tape_size )) $$(( $(NASCOM_BASIC_ROM_ADDRESS) )) >&2; exit 1; \
fi; \
if test $$(( $(NASCOM_BASIC_ROM_ADDRESS) + nascom_size )) -gt $$(( $(ROM_SIZE) )); then \
printf "error: NASCOM BASIC ends at %04X, beyond ROM size %04X\n" \
$$(( $(NASCOM_BASIC_ROM_ADDRESS) + nascom_size )) $$(( $(ROM_SIZE) )) >&2; exit 1; \
fi; \
dd if=$< of=$@ bs=1 count=$$(( $(NASCOM_BASIC_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; \
dd if=$(NASCOM_BASIC_BUILD)/nascom_basic.bin of=$@ bs=1 seek=$$(( $(NASCOM_BASIC_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 $(LAYOUT_HEADER)
@mkdir -p $(@D)
$(METAL_CC) $(METAL_FLAGS) $(METAL_CPPFLAGS) -o $@ -c $<
$(BUILDDIR)/metal/%.rel: %.asm $(LAYOUT_INCLUDE)
@mkdir -p $(@D)
$(METAL_AS) -Iinclude -I$(LAYOUT_BUILD) -plosgff -o $@ $<
$(TINYBASIC_BUILD)/tinybasic.rel: $(TINYBASIC_SRC) $(LAYOUT_INCLUDE)
@mkdir -p $(@D)
$(METAL_AS) -Iinclude -I$(LAYOUT_BUILD) -plosgff -o $@ $<
$(TINYBASIC_BUILD)/tinybasic.ihx: $(TINYBASIC_BUILD)/tinybasic.rel
sdldz80 -n -i -m -j $(basename $@) $<
$(TINYBASIC_BUILD)/tinybasic.flat.bin: $(TINYBASIC_BUILD)/tinybasic.ihx
$(METAL_MAKEBIN) -s $(METAL_IMAGE_SIZE) $< $@
$(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_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
$(NASCOM_BASIC_BUILD)/nascom_basic.bin: $(NASCOM_BASIC_SRC)
@mkdir -p $(@D)
zasm --z80 $< -o $@ -l0
$(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)
-include $(EMU_DEPFILES) $(BUILDDIR):
mkdir -p $(BUILDDIR)
$(BUILDDIR)/%.bin: ./%.asm $(BUILDDIR)/memory.inc
echo '>$<'
$(ASM) $(ASMFLAGS) $< $@
size:
@for file in $(TARGETS); do \
if [ -f "$$file" ]; then \
echo "$$file: $$(wc -c < "$$file") bytes"; \
else \
echo "$$file: Not built yet (run 'make')"; \
fi \
done
$(MAKE) -C apps/basic size --no-print-directory
$(BUILDDIR)/basic.bin:
$(MAKE) -C apps/basic $(or $(MAKECMDGOALS),all) --no-print-directory
$(BUILDDIR)/memory.inc: memory.mk
@mkdir -p $(@D)
@awk 'BEGIN { print "; Generated from rom_layout.mk; do not edit." } \
/^[A-Z][A-Z0-9_]*[[:space:]]*:=[[:space:]]*/ { print $$1 " .equ " $$3 }' $< > $@
$(BUILDDIR)/rom.bin: $(BUILDDIR)/bios.bin $(BUILDDIR)/basic.bin
objcopy --pad-to=$$(($(BIOS_ROM_START)+$(BIOS_ROM_SIZE))) \
-I binary $(BUILDDIR)/bios.bin -O binary $(BUILDDIR)/bios_padded.bin
objcopy --pad-to=$(BASIC_ROM_SIZE) \
-I binary $(BUILDDIR)/basic.bin -O binary $(BUILDDIR)/basic_padded.bin
cat $(BUILDDIR)/bios_padded.bin $(BUILDDIR)/basic_padded.bin > $(BUILDDIR)/rom.bin
.PHONY: $(TOPTARGETS) $(SUBDIRS)
View File
-214
View File
@@ -1,214 +0,0 @@
# Z80 UART monitor
This project provides a resident BIOS, an interactive monitor, and ROM-resident
applications for a Z80 system with a UART/CTC card at I/O base `20`.
At reset, a small ROM bootstrap copies only the BIOS and monitor into the top
16 KiB at `C000`. Before copying, it writes `06` to banking port `00`: ROM
remains enabled, banking is enabled, and `C000-FFFF` maps to the upper physical
16 KiB. The BIOS then initializes interrupt mode 2 and the UART and starts the
monitor. Applications remain in ROM and execute in place while calling the
stable BIOS jump table in high RAM. Tiny BASIC is 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
optional `0x` prefix is accepted.
| Command | Action |
| --- | --- |
| `D 8000` | Dump 40 bytes starting at address 8000 |
| `D 8000 20` | Dump 20 bytes starting at address 8000 |
| `C 8000 BFFF` | Compute the 16-bit additive checksum of an inclusive range |
| `W 8000 3E 41 D3 25 C9` | Write bytes starting at address 8000 |
| `I 20` | Read I/O port 20 |
| `O 20 57` | Write 57 to I/O port 20 |
| `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 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
`RET` returns to the monitor. Code that changes the stack pointer, interrupt
mode, or UART/CTC setup must restore it before returning.
`C` adds every byte from the start through the end address, inclusive, and
prints the low 16 bits of the sum as four hexadecimal digits. The end address
must be greater than or equal to the start address.
`D` prints 16 bytes per row followed by an ASCII column. Bytes outside the
printable ASCII range are shown as `.`.
Build the ROM and programmer-format images with:
```sh
make
```
## Uploading Intel HEX files
`program_ihex.py` can write an Intel HEX file to memory through the running
monitor. It sends data in small blocks with `W`, checks every block with `C`,
and retries a failed write or checksum before giving up. It requires Python 3
and [pySerial](https://pyserial.readthedocs.io/).
```sh
python3 -m pip install pyserial
python3 program_ihex.py /dev/ttyUSB0 program.hex
```
The default connection is 2400 baud, the default block size is 16 bytes, and
three retries are allowed after the initial attempt. 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
file are written; gaps are left unchanged. The HEX addresses must refer to
writable memory and must not overwrite the running monitor at `C000-FFFF`.
The outputs are:
- `build/metal/monitor.bin`: complete 32 KiB ROM image
- `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 12,967 bytes. Its ROM layout is:
- `0000-007F`: reset/copy bootstrap
- `0080-12DD`: packed BIOS+monitor image copied to RAM at `C000-D25D`
- `12DE-13FF`: unused space reserved for the resident image to grow
- `1400-1C0C`: Tiny BASIC, executed directly from ROM
- `1C0D-2FFF`: unused ROM
- `3000-32A6`: tape loader, executed directly from ROM
- `32A7-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
from a linker symbol, so it tracks BIOS and monitor changes automatically.
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-D25D`: resident BIOS, vectors, services, and monitor
- `D25E-EFFF`: free space for resident-image growth
- `F000-FFFF`: BIOS globals, UART buffers, and monitor stack
The BIOS and monitor continue to work if low ROM is disabled. ROM-resident
applications naturally require ROM to remain mapped while they execute.
The banking latch bits used by the bootstrap are bit 0 `ROM_DISABLE`, bit 1
`BANK_SELECT`, and bit 2 `BANK_ENABLE`. Its initial value `06` leaves ROM on and
selects the lower physical 48 KiB plus the upper physical 16 KiB. Writing `07`
later disables ROM without changing that split mapping. Writing `05` selects
the complete upper physical 64 KiB; the monitor remains available because its
`C000-FFFF` image was placed in that upper bank during boot.
## BIOS interface
Assembly applications include `include/bios.inc` and call these fixed entries:
| Address | Service | Interface |
| --- | --- | --- |
| `C050` | `BIOS_PUTC` | Write character in `A` |
| `C053` | `BIOS_TRY_GETC` | Return character in `A`, or zero |
| `C056` | `BIOS_GETC` | Wait for and return a character in `A` |
| `C059` | `BIOS_IO_OUT` | Write `L` to port `A` |
| `C05C` | `BIOS_IO_IN` | Read port `A`, returning the value in `A` |
| `C05F` | `BIOS_DELAY_MS` | Delay approximately `HL` milliseconds |
| `C062` | `BIOS_MONITOR` | Warm-start the monitor; does not return |
| `C065` | `BIOS_BANK` | `A=0`: split lower 48K/upper 16K; otherwise select upper 64K |
| `C068` | `BIOS_ROM_DISABLE` | Disable ROM while retaining the selected RAM mapping |
| `C06B` | `BIOS_ROM_ENABLE` | Enable ROM while retaining the selected RAM mapping |
| `C06E` | `BIOS_FLUSH` | Wait until the UART output queue is empty |
The service wrappers preserve working registers except for documented return
values. The monitor uses the corresponding C API declared in `bios.h`.
The banking latch at port `00` is write-only, so the BIOS keeps a persistent
shadow byte initialized to `06` by the copied resident image. `BIOS_BANK`
always leaves banking enabled and changes only the bank selection while
retaining the shadowed ROM-disable bit and auxiliary-output bits.
`BIOS_ROM_DISABLE` sets the ROM-disable bit and `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)` / `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
- `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/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 `1400`. Start it from
the monitor with:
```text
B
```
`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
interrupt-driven UART.
Line editing accepts either `BS` (`08`) or `DEL` (`7F`) for backspace and emits
`BS SPACE BS` so ANSI-style terminals erase the character visually. Ctrl-U
clears the complete input line.
During execution, Ctrl-O toggles output suppression without stopping the BASIC
program. Press Ctrl-O again to resume output. Ctrl-C stops execution and
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 tape loader occupies the later ROM slot at `3000-32A6`; the first
unused byte after it is `32A7`.
## 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. Signal acquisition is bounded; a missing,
unstable, or permanently high signal reports `TAPE SIGNAL TIMEOUT` and returns
to the monitor instead of waiting forever with interrupts disabled.
-35
View File
@@ -1,35 +0,0 @@
# ROM applications
Applications in this directory execute directly from ROM; the reset bootstrap
does not copy them. Each application must have its own non-overlapping `ORG`
below `8000` and should include `bios.inc` from the project's `include`
directory instead of accessing the UART hardware directly.
The current allocation is:
- `1400-1C0C`: Tiny BASIC
- `1C0D-2FFF`: available
- `3000-32A6`: tape loader
- `32A7-7FFF`: available for more applications (`3300` 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. 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`, `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.
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.
-199
View File
@@ -1,199 +0,0 @@
Name casette;
PartNo 00;
Date 2026-10-03;
Revision 01;
Designer joppe;
Company unknown;
Assembly None;
Location unknown;
Device g22v10;
/* ------------------------------------------------------------
Constants
------------------------------------------------------------ */
ar = 'b'0;
sp = 'b'0;
/* ------------------------------------------------------------
Pin assignments
Physical signals IOSEL, RD and WR are active-low.
Their polarity is expressed in the equations with !.
------------------------------------------------------------ */
/* inputs */
PIN 1 = CLK;
PIN 2 = A1;
PIN 3 = A2;
PIN 4 = IOSEL;
PIN 5 = RD;
PIN 6 = A0;
PIN 7 = COMP;
PIN 8 = WR;
/* data bus */
PIN 14 = D0;
PIN 15 = D1;
PIN 16 = D2;
PIN 17 = D3;
PIN 18 = D4;
PIN 19 = D5;
PIN 20 = D6;
PIN 21 = D7;
/* registered output */
PIN 22 = LED;
/* ------------------------------------------------------------
Read register at offset 0
Conditions:
/IOSEL = 0
/RD = 0
/WR = 1
A2:A0 = 000
Result:
D0 = COMP
D7..D1 = 0
------------------------------------------------------------ */
D0 = COMP;
D1 = 'b'0;
D2 = 'b'0;
D3 = 'b'0;
D4 = 'b'0;
D5 = 'b'0;
D6 = 'b'0;
D7 = 'b'0;
D0.OE =
!IOSEL
& !RD
& WR
& !A2
& !A1
& !A0;
D1.OE =
!IOSEL
& !RD
& WR
& !A2
& !A1
& !A0;
D2.OE =
!IOSEL
& !RD
& WR
& !A2
& !A1
& !A0;
D3.OE =
!IOSEL
& !RD
& WR
& !A2
& !A1
& !A0;
D4.OE =
!IOSEL
& !RD
& WR
& !A2
& !A1
& !A0;
D5.OE =
!IOSEL
& !RD
& WR
& !A2
& !A1
& !A0;
D6.OE =
!IOSEL
& !RD
& WR
& !A2
& !A1
& !A0;
D7.OE =
!IOSEL
& !RD
& WR
& !A2
& !A1
& !A0;
/* ------------------------------------------------------------
Writable LED register
Write conditions:
/IOSEL = 0
/RD = 1
/WR = 0
A2:A0 = 000
During such a write:
LED.D = D0
Otherwise:
LED.D = LED
Pin 1 (CLK) clocks the registered macrocell.
------------------------------------------------------------ */
LED.D =
(
!IOSEL
& RD
& !WR
& !A2
& !A1
& !A0
& D0
)
#
(
(
IOSEL
# !RD
# WR
# A2
# A1
# A0
)
& LED
);
LED.ar = ar;
LED.sp = sp;
-54
View File
@@ -1,54 +0,0 @@

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
-135
View File
@@ -1,135 +0,0 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import struct
import wave
from pathlib import Path
class PulseWriter:
def __init__(
self,
sample_rate: int,
t_short_us: float,
t_long_us: float,
amplitude: float,
) -> None:
self.sample_rate = sample_rate
self.short_samples = self._duration_samples(t_short_us)
self.long_samples = self._duration_samples(t_long_us)
self.high = round(32767 * amplitude)
self.low = -self.high
self.samples = bytearray()
self.space(0.1)
def space(self, time: float) -> None:
self._level(0, int(time*self.sample_rate))
def _duration_samples(self, duration_us: float) -> int:
sample_count = round(duration_us * self.sample_rate / 1_000_000)
if sample_count < 1:
raise ValueError(
f"pulse duration {duration_us:g} us is shorter than one sample"
)
return sample_count
def _level(self, value: int, sample_count: int) -> None:
self.samples.extend(struct.pack("<h", value) * sample_count)
def short_pulse(self) -> None:
self._level(self.high, self.short_samples)
self._level(self.low, self.long_samples)
def long_pulse(self) -> None:
self._level(self.high, self.long_samples)
self._level(self.low, self.short_samples)
def marker(self) -> None:
self._level(self.high, 2*self.long_samples)
self._level(self.low, 2*self.long_samples)
def bit(self, value: int) -> None:
if value:
self.long_pulse()
else:
self.short_pulse()
def byte(self, value: int) -> None:
# Transmit bytes most-significant bit first.
for bit_number in range(7, -1, -1):
self.bit((value >> bit_number) & 1)
def bytes(self, value: bytes) -> None:
for byte in value:
self.byte(byte)
def leader(self) -> None:
self.space(0.2)
for _ in range(50):
self._level(self.high, self.short_samples)
self._level(self.low, self.short_samples)
for _ in range(50):
self._level(self.high, self.long_samples)
self._level(self.low, self.long_samples)
self.marker()
for value in (0x55, 0x5A, 0xC3):
self.byte(value)
def main() -> None:
parser = argparse.ArgumentParser(
description="Generate the pulse-width encoded tape leader"
)
parser.add_argument("output", type=Path, help="output WAV file")
parser.add_argument("--sample-rate", type=int, default=48_000)
parser.add_argument(
"--t-short",
type=float,
default=250.0,
metavar="MICROSECONDS",
help="short pulse interval in microseconds (default: 250)",
)
parser.add_argument(
"--t-long",
type=float,
default=500.0,
metavar="MICROSECONDS",
help="long pulse interval in microseconds (default: 500)",
)
parser.add_argument("--amplitude", type=float, default=0.75)
args = parser.parse_args()
if args.sample_rate <= 0:
parser.error("--sample-rate must be positive")
if args.t_short <= 0 or args.t_long <= 0:
parser.error("pulse intervals must be positive")
if args.t_short >= args.t_long:
parser.error("--t-short must be less than --t-long")
if not 0.0 <= args.amplitude <= 1.0:
parser.error("--amplitude must be between 0 and 1")
try:
pulses = PulseWriter(
args.sample_rate,
args.t_short,
args.t_long,
args.amplitude,
)
except ValueError as error:
parser.error(str(error))
pulses.leader()
pulses.bytes(b'Hello world!\0')
pulses.space(1.0)
with wave.open(str(args.output), "wb") as wav:
wav.setnchannels(1)
wav.setsampwidth(2)
wav.setframerate(args.sample_rate)
wav.writeframes(pulses.samples)
if __name__ == "__main__":
main()
-569
View File
@@ -1,569 +0,0 @@
.include "bios.inc"
.area TAPELOAD (ABS)
.org TAPE_LOADER_ROM_ADDRESS
.macro print_var arg1
ld a, (arg1)
ei
call print_hex8
call print_crlf
di
.endm
.macro error_ret arg1,arg2
arg1:
ld a,#arg2
scf
ret
.endm
TAPE_PORT = 0x18
MAX_BLOCK_SIZE = 0x100
STACK_TOP = 0xBFFF
WORK_BASE = 0xB000
OLD_SP = WORK_BASE + 0x00 ; word
OLD_IFF = WORK_BASE + 0x02 ; byte
SHORT_AVG = WORK_BASE + 0x03 ; byte
SHORT_MAX = WORK_BASE + 0x04 ; byte
SHORT_MIN = WORK_BASE + 0x05 ; byte
LONG_AVG = WORK_BASE + 0x06 ; byte
LONG_MAX = WORK_BASE + 0x07 ; byte
LONG_MIN = WORK_BASE + 0x08 ; byte
BLOCK_BUFFER = WORK_BASE + 0x100 ; MAX_BLOCK_SIZE
; #################################################3
; Entry
; #################################################3
tape_loader:
ld (OLD_SP),sp
; Save interrupt state.
ld a,i
jp po,iff_was_off
ld a,#1
jr save_iff
iff_was_off:
xor a
save_iff:
ld (OLD_IFF),a
di
ld sp,#STACK_TOP
main:
; Initial message.
ld hl,#msg_start
call print_string_ei
; Calibrate reader
call calibrate
jr nc,1$
ei
call print_hex8
call print_crlf
ld hl,#msg_cal_fail
call print_string
di
jp exit
1$:
; try to read the sync bytes
call read_sync
jr nc,2$
ld hl,#msg_sync_error
call print_string_ei
jp exit
2$:
; Test, read Hello World!\0 (13 bytes)
exx
ld e,#0
ld hl,#BLOCK_BUFFER
exx
4$:
call read_byte
jr nc,5$
ld hl,#msg_read_error
call print_string_ei
exx
ei
call print_hex16
call print_crlf
di
exx
jp exit
5$:
exx
ld (hl),a
inc hl
inc e
ld a,e
cp a,#13
exx
jr nz,4$
; #################################################3
; Exit
; #################################################3
exit:
ei
call BIOS_FLUSH
di
ld sp,(OLD_SP)
; Return code 0
ld a,#0
ld c,a
; Restore caller's interrupt state.
ld a,(OLD_IFF)
or a
ld a,c
jr z,return_di
ei
ret
return_di:
ret
; #################################################3
; Measure edge-to-edge pulse
; needs d (comparator state)
; returns b (loop count)
; carry is timeout
; uses a
; loop is 36T, extra 22T (0.6 loops)
; #################################################3
measure_pulse:
ld b,#0 ; 4T
1$:
; Use b as timeout counter (256 loops)
inc b ; 4T
jr z,9$ ; 12/7T
in a,(TAPE_PORT) ; 11T
cp a,d ; 4T
jp z,1$ ; 10T
; loop has 36T
; Edge found
ld d,a ; 4T
or a,a ; 4T
ret ; 10T
9$:
scf
ret
; #################################################3
; Ignore one pulse then measure next pulse
; needs d (comparator state)
; returns b (loop count)
; carry is timeout
; uses a
; loop is 36T, extra 26T (0.7 loops)
; #################################################3
sample_pulse:
; wait for first edge
ld b,#0 ; 4T
1$:
; Use b as timeout counter (256 loops)
inc b ; 4T
jr z,9$ ; 12/7T
in a,(TAPE_PORT) ; 11T
cp a,d ; 4T
jp z,1$ ; 10T
; loop has 36T
; Edge found
ld d,a ; 4T
; start timing pulse length
ld b,#0 ; 4T
2$:
; Use b as timeout counter (256 loops)
inc b ; 4T
jr z,9$ ; 12/7T
in a,(TAPE_PORT) ; 11T
cp a,d ; 4T
jp z,2$ ; 10T
; loop has 36T
; Edge found
ld d,a ; 4T
or a,a ; 4T
ret ; 10T
9$:
scf
ret
; #################################################3
; Average 8 pulse measurements
; needs d (comparator state)
; returns a (average)
; carry is timeout
; uses b,e,h
; #################################################3
average8:
ld e,#0
ld h,#8
1$:
; synchronize to next pulse
; ignore first measurement
; contains part of the calculations
call sample_pulse
call sample_pulse
; when timeout just return with carry
ret c
; Add loop count to e
ld a,e
add a,b
ld e,a
; repeat 8 times
dec h
jr nz,1$
; divide by 8
ld a,e
srl a
srl a
srl a
; clear carry
or a,a
ret
; #################################################3
; Make +/-25% window around a
; needs a (average)
; returns b (minimum) c (maximum)
; uses e,h
; NOTE: USES 8 BIT COUNTER
; @1MHZ t_long<1.14ms
; @2MHZ t_long<571us
; @4MHZ t_long<285us
; TODO: Check feasibility of using 16bit accumulator
; #################################################3
make_window:
ld e,a
; get 25%
srl a
srl a
ld h,a
; a-25%
ld a,e
sub h
ld b,a
; a+25%
ld a,e
add a,h
ld c,a
ret
; #################################################3
; Calibration
; #################################################3
calibrate:
; Get current comparator state
in a,(TAPE_PORT)
ld d,a
; make sure we start in silence
call sample_pulse
; should timeout
jp nc,cal_fail_no_silence
; Wait for first edge
ld l,#0
1$:
call sample_pulse
jr nc,2$
; Use b as timeout counter (256 loops)
; inc l
; jr nz,1$
jr 1$
; timeout
;jp cal_fail_no_short_leader ; TODO IGNORED TIMEOUT FOR NOW HERE
2$:
; edge found
; get short leader timing
call average8
jr c,cal_fail_short
ld (SHORT_AVG),a
call make_window
ld a,b
ld (SHORT_MIN),a
ld a,c
ld (SHORT_MAX),a
; find long leader
; need 4 consecutive pulses > short*1.5
ld a,(SHORT_AVG)
ld c,a
srl a
add a,c
ld c,a
; c = short*1.5
; TODO no overal timeout here. a continuous
; short pulse leader will keep on waiting
; forever
3$:
ld e,#4
4$:
; ignore first to get accurate measurement
call sample_pulse
call sample_pulse
jr c,cal_fail_find_long
ld a,b
; if it is no long pulse try again
cp a,c
jr c,3$
; long pulse found
dec e
jr nz,4$
; 4 (or well... 8) long pulses found
; get long leader timning
call average8
jr c,cal_fail_long
ld (LONG_AVG),a
call make_window
ld a,b
ld (LONG_MIN),a
ld a,c
ld (LONG_MAX),a
; A quick sanity check
; long > 1.5*short
ld a,(SHORT_AVG)
ld c,a
srl a
add a,c
ld c,a
; c = 1.5*short
ld a,(LONG_AVG)
cp a,c
jr c,cal_fail_sanity
; Find the marker
; marker is 2*long high 2*long low
; threshold is 1.5*long
ld a,(LONG_AVG)
ld c,a
srl a
add a,c
ld c,a
; c = 1.5*long
; Find two consecutive marker-width intervals.
5$:
ld e,#0
6$:
call measure_pulse
jr c,cal_fail
ld a,b
cp a,c
jr c,5$
inc e
ld a,e
cp a,#2
jr c,6$
; Both marker halves consumed. We are now at the
; beginning of the first data pulse.
; next pulse is first pulse of training byte
; must decode as 0x55
call read_byte
jr c,cal_fail_read_error
cp a,#0x55
jr nz,cal_fail_training_byte
ld a,#0
or a,a
ret
error_ret cal_fail 0
error_ret cal_fail_short 1
error_ret cal_fail_find_long 2
error_ret cal_fail_long 3
error_ret cal_fail_sanity 4
error_ret cal_fail_training_byte 5
error_ret cal_fail_no_silence 6
error_ret cal_fail_no_short_leader 7
error_ret cal_fail_read_error 8
; #################################################3
; Read single bit
; needs d (comparator state)
; valid: z=1 c=bit
; error: z=0
; uses a,b,c
; #################################################3
read_bit:
; entry may be late into first pulse
; wait for first edge to measure
ld b,#0
1$:
inc b
jr z,bit_bad
in a,(TAPE_PORT)
cp a,d
jp z,1$
ld d,a
; ignored first edge
; measure second pulse
ld b,#0
2$:
inc b
jr z,bit_bad
in a,(TAPE_PORT)
cp a,d
jp z,2$
ld d,a
; if in short window (second half is short pulse)
; 1
ld a,(SHORT_MIN)
ld c,a
ld a,b
cp a,c
jr c,bit_bad ; pulse<SHORT_MIN
ld a,(SHORT_MAX)
ld c,a
ld a,b
cp a,c
jr c,bit_one
jr z,bit_one ; pulse<=SHORT_MAX
; test long window (second half is long pulse)
ld a,(LONG_MIN)
ld c,a
ld a,b
cp a,c
jr c,bit_bad ; pulse<LONG_MAX
ld a,(LONG_MAX)
ld c,a
ld a,b
cp a,c
jr c,bit_zero
jr z,bit_zero ; pulse<=LONG_MAX
bit_bad:
ld a,#1
or a,a
ret
bit_zero:
xor a,a
ret
bit_one:
xor a,a
scf
ret
; #################################################3
; Read byte, MSB first
; returns a (byte)
; carry means error
; needs d (comparator state)
; uses e,h,b,c
; #################################################3
read_byte:
ld e,#0
ld h,#8
1$:
call read_bit
jr nz,9$
; shift in the carry bit
rl e
; repeat 8 times
dec h
jr nz,1$
; byte done,
ld a,e
or a
ret
9$:
scf
ret
; #################################################3
; Read and check sync bytes
; carry is error
; #################################################3
read_sync:
call read_byte
ret c
cp a,#0x5a
jr nz,9$
call read_byte
ret c
cp a,#0xc3
jr nz,9$
or a,a
ret
9$:
scf
ret
; #################################################3
; IO Helper functions
; #################################################3
print_string_ei:
ei
call print_string
call BIOS_FLUSH
di
ret
print_string:
ld a,(hl)
or a
ret z
call BIOS_PUTC
inc hl
jr print_string
print_crlf:
ld a,#13
call BIOS_PUTC
ld a,#10
jp BIOS_PUTC
print_hex16:
push hl
ld a,h
call print_hex8
pop hl
ld a,l
call print_hex8
ret
print_hex8:
push af
rrca
rrca
rrca
rrca
and #0x0F
call print_hex_nibble
pop af
and #0x0F
; fall through
print_hex_nibble:
cp #10
jr c,print_hex_digit
add a,#('A' - 10)
jp BIOS_PUTC
print_hex_digit:
add a,#'0'
jp BIOS_PUTC
; #################################################3
; Strings
; #################################################3
msg_start:
.ascii "TAPE DECODE TEST - PLAY TAPE"
.db 13,10,0
msg_cal_fail:
.ascii "Calibration failed"
.db 13,10,0
msg_sync_error:
.ascii "Sync bytes not found"
.db 13,10,0
msg_read_error:
.ascii "read error"
.db 13,10,0
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-2
View File
@@ -1,2 +0,0 @@
*.bin
*.lst
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
View File
-124
View File
@@ -1,124 +0,0 @@
#include <stdint.h>
#include "bios.h"
#include "circ_buf.h"
#define UART_CTC_CARD_BASE 0x20
#define CTC_BASE (UART_CTC_CARD_BASE + 0x00)
#define CTC_CHANNEL_0 (CTC_BASE + 0)
#define CTC_CHANNEL_1 (CTC_BASE + 1)
#define CTC_CHANNEL_2 (CTC_BASE + 2)
#define CTC_CHANNEL_3 (CTC_BASE + 3)
#define UART_BASE (UART_CTC_CARD_BASE + 0x04)
#define UART_CONTROL (UART_BASE + 0)
#define UART_DATA (UART_BASE + 1)
static volatile uint8_t uart_tx_busy;
static volatile circular_buffer_t rx_buf;
static volatile circular_buffer_t tx_buf;
static void uart_kick(void)
{
char c;
disable_interrupts();
if (!uart_tx_busy && buffer_dequeue(&tx_buf, &c)) {
uart_tx_busy = 1;
io_out(UART_DATA, c);
io_out(UART_CONTROL, 0b10110101);
}
enable_interrupts();
}
void bios_putc(char c)
{
uint8_t queued;
do {
disable_interrupts();
queued = buffer_enqueue(&tx_buf, c);
enable_interrupts();
if (!queued) uart_kick();
} while (!queued);
uart_kick();
}
void bios_puts(const char *s)
{
while (*s) bios_putc(*s++);
}
uint8_t bios_try_getc(void)
{
char c;
uint8_t received;
disable_interrupts();
received = buffer_dequeue(&rx_buf, &c);
enable_interrupts();
return received ? (uint8_t)c : 0;
}
char bios_getc(void)
{
uint8_t c;
do {
c = bios_try_getc();
} while (!c);
return (char)c;
}
void bios_flush(void)
{
while (uart_tx_busy || !buffer_is_empty(&tx_buf)) uart_kick();
}
void bios_init(void)
{
buffer_init(&rx_buf);
buffer_init(&tx_buf);
/* A CPU-only reset does not necessarily reset the external CTC. Stop and
disable every channel before rebuilding the UART clock/IRQ path. */
io_out(CTC_CHANNEL_0, 0x03);
io_out(CTC_CHANNEL_1, 0x03);
io_out(CTC_CHANNEL_2, 0x03);
io_out(CTC_CHANNEL_3, 0x03);
/* The 6850 has no reset input; CR1:CR0 = 11 is its master reset. */
io_out(UART_CONTROL, 0b00000011);
io_out(CTC_CHANNEL_0, 0x57);
io_out(CTC_CHANNEL_0, 13);
io_out(CTC_CHANNEL_3, 0b11001111);
io_out(CTC_CHANNEL_3, 1);
io_out(CTC_CHANNEL_0, 0x00);
io_out(CTC_CHANNEL_0, 0x40);
uart_tx_busy = 0;
io_out(UART_CONTROL, 0b10010101);
enable_interrupts();
}
void uart_interrupt(void)
{
uint8_t status = io_in(UART_CONTROL);
if ((status & 0x21) != 0) {
char c = io_in(UART_DATA);
if ((status & 0x01) != 0) buffer_enqueue(&rx_buf, c);
}
if ((status & 0x02) != 0 && uart_tx_busy) {
char c;
if (buffer_dequeue(&tx_buf, &c)) {
io_out(UART_DATA, c);
} else {
io_out(UART_CONTROL, 0b10010101);
uart_tx_busy = 0;
}
}
}
void ctc_interrupt(void)
{
}
-27
View File
@@ -1,27 +0,0 @@
#ifndef BIOS_H
#define BIOS_H
#include <stdint.h>
/* Resident BIOS API used by the monitor. */
void bios_init(void);
void bios_putc(char c);
void bios_puts(const char *s);
char bios_getc(void);
uint8_t bios_try_getc(void);
void bios_flush(void);
/* Write-only banking latch services. */
void bios_bank(uint8_t bank);
void bios_rom_disable(void);
void bios_rom_enable(void);
/* Low-level routines implemented by the resident assembly BIOS. */
void io_out(uint8_t port, uint8_t value);
uint8_t io_in(uint8_t port);
void delay_ms(uint16_t milliseconds);
void enable_interrupts(void);
void disable_interrupts(void);
void jump_to(uint16_t address);
#endif
View File
-413
View File
@@ -1,413 +0,0 @@
STACK_TOP = 0xFFFF
DELAY_LOOPS_PER_MS = 40
BANK_CONTROL_PORT = 0x00
BOOT_BANK_MAPPING = 0x06 ; ROM on, banking on, upper 16K at C000-FFFF
ROM_DISABLE_BIT = 0x01
BANK_SELECT_BIT = 0x02
BANK_ENABLE_BIT = 0x04
BANK_MODE_CLEAR_MASK = 0xF9 ; preserve ROM-disable and auxiliary outputs
.include "bios.inc"
.globl _main
.globl _delay_ms
.globl _io_out
.globl _io_in
.globl _enable_interrupts
.globl _disable_interrupts
.globl _jump_to
.globl _bios_putc
.globl _bios_try_getc
.globl _bios_getc
.globl _bios_flush
.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
.globl l__DATA
.globl s__INITIALIZER
.globl s__INITIALIZED
.globl l__INITIALIZER
.area _BOOT (ABS)
.org 0x00
jp boot_copy
.org 0x08
boot_copy:
di
; Keep ROM enabled while mapping the upper physical 16K at C000-FFFF.
; The resident copy therefore survives either banked mode after boot.
ld a, #BOOT_BANK_MAPPING
out (BANK_CONTROL_PORT), a
ld sp, #STACK_TOP
ld hl, #RESIDENT_ROM_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_RAM_BASE
jp init
.org RESIDENT_RAM_BASE + 0x08
reti
.org RESIDENT_RAM_BASE + 0x10
reti
.org RESIDENT_RAM_BASE + 0x18
reti
.org RESIDENT_RAM_BASE + 0x20
reti
.org RESIDENT_RAM_BASE + 0x28
reti
.org RESIDENT_RAM_BASE + 0x30
reti
.org RESIDENT_RAM_BASE + 0x38
reti
; IM 2 table: the CTC supplies vectors 40, 42, 44, and 46.
.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
.dw uart_isr ; CTC channel 3: vector 0x46
; 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, C06B enable ROM, C06E flush output.
.org BIOS_PUTC
jp bios_service_putc
.org BIOS_TRY_GETC
jp bios_service_try_getc
.org BIOS_GETC
jp bios_service_getc
.org BIOS_IO_OUT
jp bios_service_io_out
.org BIOS_IO_IN
jp bios_service_io_in
.org BIOS_DELAY_MS
jp bios_service_delay_ms
.org BIOS_MONITOR
jp init
.org BIOS_BANK
jp _bios_bank
.org BIOS_ROM_DISABLE
jp _bios_rom_disable
.org BIOS_ROM_ENABLE
jp _bios_rom_enable
.org BIOS_FLUSH
jp bios_service_flush
; 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_RAM_BASE + 0x73
_bank_control_shadow::
.db BOOT_BANK_MAPPING
.area _HOME
init:
di
ld sp, #STACK_TOP
; A reset may have interrupted a CTC service routine without executing
; RETI. Release any stale daisy-chain in-service latches before the
; channels are reinitialized. RETI acts as RET for each CALL while
; also signalling end-of-interrupt to daisy-chained peripherals.
call release_daisy_chain
call release_daisy_chain
call release_daisy_chain
call release_daisy_chain
call gsinit
; Setup interrupt mode 2
ld a, #0xC0
ld i, a
im 2
call _bios_init
call _main
hang:
jr hang
release_daisy_chain:
reti
.area _GSINIT
gsinit:
ld bc, #l__INITIALIZER
ld a, b
or c
jr z, zero_data
ld de, #s__INITIALIZED
ld hl, #s__INITIALIZER
ldir
zero_data:
ld bc, #l__DATA
ld a, b
or c
jr z, gsinit_done
ld hl, #s__DATA
xor a
clear_loop:
xor a ; BC loop test below changes A
ld (hl), a
inc hl
dec bc
ld a, b
or c
jr nz, clear_loop
gsinit_done:
.area _GSFINAL
ret
resident_image_end:
.area _CODE
; Select a RAM mapping while retaining the shadowed ROM-disable bit.
; Input: A=0 selects lower 48K + upper 16K; any other value selects upper 64K.
; Both modes keep banking enabled. A and flags are preserved.
_bios_bank::
push af
or a
jr z, bios_bank_split
ld a, (_bank_control_shadow)
and #BANK_MODE_CLEAR_MASK
or #BANK_ENABLE_BIT
jr bios_bank_update
bios_bank_split:
ld a, (_bank_control_shadow)
and #BANK_MODE_CLEAR_MASK
or #(BANK_ENABLE_BIT | BANK_SELECT_BIT)
bios_bank_update:
ld (_bank_control_shadow), a
out (BANK_CONTROL_PORT), a
pop af
ret
; 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
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:
push af
push bc
push de
push hl
push ix
push iy
call _bios_putc
pop iy
pop ix
pop hl
pop de
pop bc
pop af
ret
bios_service_try_getc:
push bc
push de
push hl
push ix
push iy
call _bios_try_getc
pop iy
pop ix
pop hl
pop de
pop bc
ret
bios_service_getc:
push bc
push de
push hl
push ix
push iy
call _bios_getc
pop iy
pop ix
pop hl
pop de
pop bc
ret
bios_service_flush:
push af
push bc
push de
push hl
push ix
push iy
call _bios_flush
pop iy
pop ix
pop hl
pop de
pop bc
pop af
ret
bios_service_io_out:
push af
push bc
push de
push hl
push ix
push iy
call _io_out
pop iy
pop ix
pop hl
pop de
pop bc
pop af
ret
bios_service_io_in:
push bc
push de
push hl
push ix
push iy
call _io_in
pop iy
pop ix
pop hl
pop de
pop bc
ret
bios_service_delay_ms:
push af
push bc
push de
push hl
push ix
push iy
call _delay_ms
pop iy
pop ix
pop hl
pop de
pop bc
pop af
ret
.macro MAKE_ISR entry, handler
.globl handler
entry:
push af
push bc
push de
push hl
push ix
push iy
ex af, af'
push af
exx
push bc
push de
push hl
call handler
pop hl
pop de
pop bc
exx
pop af
ex af, af'
pop iy
pop ix
pop hl
pop de
pop bc
pop af
ei
reti
.endm
MAKE_ISR ctc_isr, _ctc_interrupt
MAKE_ISR uart_isr, _uart_interrupt
_enable_interrupts::
ei
ret
_disable_interrupts::
di
ret
; Write a byte to an I/O port.
; Input: A = port address, L = data byte.
_io_out::
ld c, a
ld a, l
out (c), a
ret
; Read a byte from an I/O port.
; Input: A = port address.
; Return: A = data byte.
_io_in::
ld c, a
in a, (c)
ret
; Execute code at HL. Because this routine is entered with CALL, a RET in the
; target returns to the monitor immediately after jump_to().
_jump_to::
jp (hl)
; Approximate millisecond delay.
; Input: HL = delay in milliseconds.
_delay_ms::
ld a, h
or l
ret z
delay_ms_outer$:
push hl
ld hl, #DELAY_LOOPS_PER_MS
delay_ms_inner$:
dec hl
ld a, h
or l
jr nz, delay_ms_inner$
pop hl
dec hl
ld a, h
or l
jr nz, delay_ms_outer$
ret
-33
View File
@@ -1,33 +0,0 @@
#include "circ_buf.h"
void buffer_init(volatile circular_buffer_t* b){
b->count = 0;
b->head = 0;
b->tail = 0;
}
uint8_t buffer_is_full(volatile circular_buffer_t* b){
return b->count == BUFFER_SIZE;
}
uint8_t buffer_is_empty(volatile circular_buffer_t* b){
return b->count == 0;
}
uint8_t buffer_enqueue(volatile circular_buffer_t* b, char c){
if(buffer_is_full(b))
return 0;
b->data[b->head] = c;
b->head = (b->head+1)%BUFFER_SIZE;
b->count++;
return 1;
}
uint8_t buffer_dequeue(volatile circular_buffer_t* b, char* c){
if(buffer_is_empty(b))
return 0;
*c = b->data[b->tail];
b->tail = (b->tail+1)%BUFFER_SIZE;
b->count--;
return 1;
}
-21
View File
@@ -1,21 +0,0 @@
#ifndef __H_CIRC_BUF
#define __H_CIRC_BUF
#include <stdint.h>
#define BUFFER_SIZE 64
typedef struct {
char data[BUFFER_SIZE];
uint8_t head;
uint8_t tail;
uint8_t count;
} circular_buffer_t;
void buffer_init(volatile circular_buffer_t* b);
uint8_t buffer_is_full(volatile circular_buffer_t* b);
uint8_t buffer_is_empty(volatile circular_buffer_t* b);
uint8_t buffer_enqueue(volatile circular_buffer_t* b, char c);
uint8_t buffer_dequeue(volatile circular_buffer_t* b, char* c);
#endif
View File
-14
View File
@@ -1,14 +0,0 @@
.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
BIOS_FLUSH = RESIDENT_RAM_BASE + 0x6E ; wait until all queued UART output is sent
View File
-362
View File
@@ -1,362 +0,0 @@
#include <stdint.h>
#include "bios.h"
#include "rom_layout.h"
#define LINE_SIZE 80
#define MAX_ARGS 40
#define DEFAULT_DUMP_LENGTH 0x40
#define MAX_DUMP_LENGTH 0x100
static char hex_digit(uint8_t value)
{
value &= 0x0f;
return value < 10 ? (char)('0' + value) : (char)('A' + value - 10);
}
static void print_hex8(uint8_t value)
{
bios_putc(hex_digit(value >> 4));
bios_putc(hex_digit(value));
}
static void print_hex16(uint16_t value)
{
print_hex8((uint8_t)(value >> 8));
print_hex8((uint8_t)value);
}
static uint8_t hex_value(char c, uint8_t *value)
{
if (c >= '0' && c <= '9') {
*value = (uint8_t)(c - '0');
return 1;
}
if (c >= 'a' && c <= 'f') {
*value = (uint8_t)(c - 'a' + 10);
return 1;
}
if (c >= 'A' && c <= 'F') {
*value = (uint8_t)(c - 'A' + 10);
return 1;
}
return 0;
}
static uint8_t parse_hex(const char *text, uint16_t *result)
{
uint16_t value = 0;
uint8_t digit;
uint8_t digits = 0;
if (text[0] == '0' && (text[1] == 'x' || text[1] == 'X')) text += 2;
while (*text) {
if (digits == 4 || !hex_value(*text++, &digit)) return 0;
value = (uint16_t)((value << 4) | digit);
++digits;
}
if (digits == 0) return 0;
*result = value;
return 1;
}
static uint8_t strings_equal(const char *a, const char *b)
{
char ca;
char cb;
do {
ca = *a++;
cb = *b++;
if (ca >= 'A' && ca <= 'Z') ca += 'a' - 'A';
if (cb >= 'A' && cb <= 'Z') cb += 'a' - 'A';
if (ca != cb) return 0;
} while (ca);
return 1;
}
static uint8_t split_line(char *line, char **argv)
{
uint8_t argc = 0;
while (*line) {
while (*line == ' ' || *line == '\t') ++line;
if (!*line) break;
if (argc == MAX_ARGS) return argc;
argv[argc++] = line;
while (*line && *line != ' ' && *line != '\t') ++line;
if (*line) *line++ = 0;
}
return argc;
}
static void print_help(void)
{
bios_puts(
"Commands (hex numbers):\r\n"
" D addr [len] Dump memory\r\n"
" C start end 16-bit checksum, inclusive\r\n"
" W addr byte... Write memory\r\n"
" I port Read I/O\r\n"
" O port byte Write I/O\r\n"
" 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");
}
static void command_dump(uint8_t argc, char **argv)
{
uint16_t address;
uint16_t length = DEFAULT_DUMP_LENGTH;
uint16_t offset = 0;
if (argc < 2 || argc > 3 || !parse_hex(argv[1], &address) ||
(argc == 3 && !parse_hex(argv[2], &length))) {
bios_puts("Usage: D address [length]\r\n");
return;
}
if (length > MAX_DUMP_LENGTH) {
bios_puts("Length must be 0100 or less\r\n");
return;
}
while (offset < length) {
uint8_t column;
uint8_t row_length = (uint8_t)((length - offset) > 16 ? 16 : (length - offset));
print_hex16((uint16_t)(address + offset));
bios_puts(": ");
for (column = 0; column < row_length; ++column) {
print_hex8(*(volatile uint8_t *)(address + offset + column));
bios_putc(' ');
}
for (; column < 16; ++column) {
bios_puts(" ");
}
bios_putc('|');
for (column = 0; column < row_length; ++column) {
uint8_t value = *(volatile uint8_t *)(address + offset + column);
bios_putc(value >= ' ' && value <= '~' ? (char)value : '.');
}
bios_putc('|');
bios_puts("\r\n");
offset += row_length;
}
}
static void command_checksum(uint8_t argc, char **argv)
{
uint16_t address;
uint16_t end;
uint16_t checksum = 0;
if (argc != 3 || !parse_hex(argv[1], &address) ||
!parse_hex(argv[2], &end) || end < address) {
bios_puts("Usage: C start end\r\n");
return;
}
for (;;) {
checksum = (uint16_t)(checksum + *(volatile uint8_t *)address);
if (address == end) break;
++address;
}
print_hex16(checksum);
bios_puts("\r\n");
}
static void command_write(uint8_t argc, char **argv)
{
uint16_t address;
uint16_t value;
uint8_t i;
if (argc < 3 || !parse_hex(argv[1], &address)) {
bios_puts("Usage: W address byte [byte ...]\r\n");
return;
}
for (i = 2; i < argc; ++i) {
if (!parse_hex(argv[i], &value) || value > 0xff) {
bios_puts("Bad byte: ");
bios_puts(argv[i]);
bios_puts("\r\n");
return;
}
*(volatile uint8_t *)address++ = (uint8_t)value;
}
bios_puts("OK\r\n");
}
static void command_in(uint8_t argc, char **argv)
{
uint16_t port;
if (argc != 2 || !parse_hex(argv[1], &port) || port > 0xff) {
bios_puts("Usage: I port\r\n");
return;
}
print_hex8((uint8_t)port);
bios_puts(": ");
print_hex8(io_in((uint8_t)port));
bios_puts("\r\n");
}
static void command_out(uint8_t argc, char **argv)
{
uint16_t port;
uint16_t value;
if (argc != 3 || !parse_hex(argv[1], &port) || port > 0xff ||
!parse_hex(argv[2], &value) || value > 0xff) {
bios_puts("Usage: O port byte\r\n");
return;
}
io_out((uint8_t)port, (uint8_t)value);
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 ");
print_hex16(address);
bios_puts("...\r\n");
bios_flush();
jump_to(address);
bios_puts("Returned to monitor\r\n");
}
static void command_go(uint8_t argc, char **argv)
{
uint16_t address;
if (argc != 2 || !parse_hex(argv[1], &address)) {
bios_puts("Usage: G address\r\n");
return;
}
execute_address(address);
}
static void command_basic(uint8_t argc)
{
if (argc != 1) {
bios_puts("Usage: B\r\n");
return;
}
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)
{
char *argv[MAX_ARGS];
uint8_t argc = split_line(line, argv);
if (!argc) return;
if (strings_equal(argv[0], "h") || strings_equal(argv[0], "help") ||
strings_equal(argv[0], "?")) {
print_help();
} else if (strings_equal(argv[0], "d") || strings_equal(argv[0], "dump")) {
command_dump(argc, argv);
} else if (strings_equal(argv[0], "c") || strings_equal(argv[0], "checksum")) {
command_checksum(argc, argv);
} else if (strings_equal(argv[0], "w") || strings_equal(argv[0], "write")) {
command_write(argc, argv);
} else if (strings_equal(argv[0], "i") || strings_equal(argv[0], "in")) {
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");
}
}
void main(void)
{
char line[LINE_SIZE];
uint8_t length = 0;
uint8_t ignore_lf = 0;
bios_puts("\r\nZ80 monitor - v1\r\nType HELP for commands.\r\n> ");
for (;;) {
char c = bios_getc();
if (c == '\n' && ignore_lf) {
ignore_lf = 0;
continue;
}
ignore_lf = 0;
if (c == '\r' || c == '\n') {
ignore_lf = (c == '\r');
line[length] = 0;
bios_puts("\r\n");
execute_line(line);
length = 0;
bios_puts("> ");
} else if (c == '\b' || c == 0x7f) {
if (length) {
--length;
bios_puts("\b \b");
}
} else if (c == 0x15) {
while (length) {
--length;
bios_puts("\b \b");
}
} else if (c >= ' ' && c <= '~' && length < LINE_SIZE - 1) {
line[length++] = c;
bios_putc(c);
} else if (c >= ' ' && c <= '~') {
bios_putc('\a');
}
}
}
-1
View File
@@ -1 +0,0 @@
build
-55
View File
@@ -1,55 +0,0 @@
.SILENT:
include memory.mk
ASM := zasm
ASMFLAGS := -w -b
BUILDDIR := build
FILES := bios
SOURCES := $(addprefix ./, $(addsuffix .asm, $(FILES)))
TARGETS := $(addprefix $(BUILDDIR)/, $(addsuffix .bin, $(FILES)))
TOPTARGETS := all clean size
$(TOPTARGETS): $(SUBDIRS)
all: $(BUILDDIR) $(BUILDDIR)/rom.bin size
clean:
rm -rf $(BUILDDIR)
$(BUILDDIR):
mkdir -p $(BUILDDIR)
$(BUILDDIR)/%.bin: ./%.asm $(BUILDDIR)/memory.inc
echo '>$<'
$(ASM) $(ASMFLAGS) $< $@
size:
@for file in $(TARGETS); do \
if [ -f "$$file" ]; then \
echo "$$file: $$(wc -c < "$$file") bytes"; \
else \
echo "$$file: Not built yet (run 'make')"; \
fi \
done
$(MAKE) -C apps/basic size --no-print-directory
$(BUILDDIR)/basic.bin:
$(MAKE) -C apps/basic $(or $(MAKECMDGOALS),all) --no-print-directory
$(BUILDDIR)/memory.inc: memory.mk
@mkdir -p $(@D)
@awk 'BEGIN { print "; Generated from rom_layout.mk; do not edit." } \
/^[A-Z][A-Z0-9_]*[[:space:]]*:=[[:space:]]*/ { print $$1 " .equ " $$3 }' $< > $@
$(BUILDDIR)/rom.bin: $(BUILDDIR)/bios.bin $(BUILDDIR)/basic.bin
objcopy --pad-to=$$(($(BIOS_ROM_START)+$(BIOS_ROM_SIZE))) \
-I binary $(BUILDDIR)/bios.bin -O binary $(BUILDDIR)/bios_padded.bin
objcopy --pad-to=$(BASIC_ROM_SIZE) \
-I binary $(BUILDDIR)/basic.bin -O binary $(BUILDDIR)/basic_padded.bin
cat $(BUILDDIR)/bios_padded.bin $(BUILDDIR)/basic_padded.bin > $(BUILDDIR)/rom.bin
.PHONY: $(TOPTARGETS) $(SUBDIRS)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-202
View File
@@ -1,202 +0,0 @@
;==================================================================================
; Contents of this file are copyright Grant Searle
;
; You have permission to use this for NON COMMERCIAL USE ONLY
; If you wish to use it elsewhere, please include an acknowledgement to myself.
;
; http://searle.hostei.com/grant/index.html
;
; eMail: home.micros01@btinternet.com
;
; If the above don't work, please perform an Internet search to see if I have
; updated the web page hosting service.
;
;==================================================================================
; Minimum 6850 ACIA interrupt driven serial I/O to run modified NASCOM Basic 4.7
; Full input buffering with incoming data hardware handshaking
; Handshake shows full before the buffer is totally filled to allow run-on from the sender
SER_BUFSIZE .EQU 3FH
SER_FULLSIZE .EQU 30H
SER_EMPTYSIZE .EQU 5
RTS_HIGH .EQU 0D6H
RTS_LOW .EQU 096H
serBuf .EQU $2000
serInPtr .EQU serBuf+SER_BUFSIZE
serRdPtr .EQU serInPtr+2
serBufUsed .EQU serRdPtr+2
basicStarted .EQU serBufUsed+1
TEMPSTACK .EQU $20ED ; Top of BASIC line input buffer so is "free ram" when BASIC resets
CR .EQU 0DH
LF .EQU 0AH
CS .EQU 0CH ; Clear screen
.ORG $0000
;------------------------------------------------------------------------------
; Reset
RST00 DI ;Disable interrupts
JP INIT ;Initialize Hardware and go
;------------------------------------------------------------------------------
; TX a character over RS232
.ORG 0008H
RST08 JP TXA
;------------------------------------------------------------------------------
; RX a character over RS232 Channel A [Console], hold here until char ready.
.ORG 0010H
RST10 JP RXA
;------------------------------------------------------------------------------
; Check serial status
.ORG 0018H
RST18 JP CKINCHAR
;------------------------------------------------------------------------------
; RST 38 - INTERRUPT VECTOR [ for IM 1 ]
.ORG 0038H
RST38 JR serialInt
;------------------------------------------------------------------------------
serialInt: PUSH AF
PUSH HL
IN A,($80)
AND $01 ; Check if interupt due to read buffer full
JR Z,rts0 ; if not, ignore
IN A,($81)
PUSH AF
LD A,(serBufUsed)
CP SER_BUFSIZE ; If full then ignore
JR NZ,notFull
POP AF
JR rts0
notFull: LD HL,(serInPtr)
INC HL
LD A,L ; Only need to check low byte becasuse buffer<256 bytes
CP (serBuf+SER_BUFSIZE) & $FF
JR NZ, notWrap
LD HL,serBuf
notWrap: LD (serInPtr),HL
POP AF
LD (HL),A
LD A,(serBufUsed)
INC A
LD (serBufUsed),A
CP SER_FULLSIZE
JR C,rts0
LD A,RTS_HIGH
OUT ($80),A
rts0: POP HL
POP AF
EI
RETI
;------------------------------------------------------------------------------
RXA:
waitForChar: LD A,(serBufUsed)
CP $00
JR Z, waitForChar
PUSH HL
LD HL,(serRdPtr)
INC HL
LD A,L ; Only need to check low byte becasuse buffer<256 bytes
CP (serBuf+SER_BUFSIZE) & $FF
JR NZ, notRdWrap
LD HL,serBuf
notRdWrap: DI
LD (serRdPtr),HL
LD A,(serBufUsed)
DEC A
LD (serBufUsed),A
CP SER_EMPTYSIZE
JR NC,rts1
LD A,RTS_LOW
OUT ($80),A
rts1:
LD A,(HL)
EI
POP HL
RET ; Char ready in A
;------------------------------------------------------------------------------
TXA: PUSH AF ; Store character
conout1: IN A,($80) ; Status byte
BIT 1,A ; Set Zero flag if still transmitting character
JR Z,conout1 ; Loop until flag signals ready
POP AF ; Retrieve character
OUT ($81),A ; Output the character
RET
;------------------------------------------------------------------------------
CKINCHAR LD A,(serBufUsed)
CP $0
RET
PRINT: LD A,(HL) ; Get character
OR A ; Is it $00 ?
RET Z ; Then RETurn on terminator
RST 08H ; Print it
INC HL ; Next Character
JR PRINT ; Continue until $00
RET
;------------------------------------------------------------------------------
INIT:
LD HL,TEMPSTACK ; Temp stack
LD SP,HL ; Set up a temporary stack
LD HL,serBuf
LD (serInPtr),HL
LD (serRdPtr),HL
XOR A ;0 to accumulator
LD (serBufUsed),A
LD A,RTS_LOW
OUT ($80),A ; Initialise ACIA
IM 1
EI
LD HL,SIGNON1 ; Sign-on message
CALL PRINT ; Output string
LD A,(basicStarted); Check the BASIC STARTED flag
CP 'Y' ; to see if this is power-up
JR NZ,COLDSTART ; If not BASIC started then always do cold start
LD HL,SIGNON2 ; Cold/warm message
CALL PRINT ; Output string
CORW:
CALL RXA
AND %11011111 ; lower to uppercase
CP 'C'
JR NZ, CHECKWARM
RST 08H
LD A,$0D
RST 08H
LD A,$0A
RST 08H
COLDSTART: LD A,'Y' ; Set the BASIC STARTED flag
LD (basicStarted),A
JP $0150 ; Start BASIC COLD
CHECKWARM:
CP 'W'
JR NZ, CORW
RST 08H
LD A,$0D
RST 08H
LD A,$0A
RST 08H
JP $0153 ; Start BASIC WARM
SIGNON1: .BYTE CS
.BYTE "Z80 SBC By Grant Searle",CR,LF,0
SIGNON2: .BYTE CR,LF
.BYTE "Cold or warm start (C or W)? ",0
.END
-319
View File
@@ -1,319 +0,0 @@
#!/usr/bin/env python3
"""Program an Intel HEX file through the Z80 UART monitor."""
from __future__ import annotations
import argparse
import re
import sys
import time
from pathlib import Path
try:
import serial
except ImportError:
print(
"error: pySerial is required; install it with: "
"python3 -m pip install pyserial",
file=sys.stderr,
)
raise SystemExit(2)
MAX_BLOCK_SIZE = 24 # Largest W command that fits the monitor's 80-byte line.
class ProgramError(Exception):
pass
def load_ihex(path: Path) -> dict[int, int]:
"""Return the 16-bit-addressed data contained in an Intel HEX file."""
memory: dict[int, int] = {}
address_base = 0
saw_eof = False
try:
lines = path.read_text(encoding="ascii").splitlines()
except (OSError, UnicodeError) as exc:
raise ProgramError(f"cannot read {path}: {exc}") from exc
for line_number, text in enumerate(lines, 1):
text = text.strip()
if not text:
continue
if saw_eof:
raise ProgramError(f"{path}:{line_number}: data after EOF record")
if not text.startswith(":"):
raise ProgramError(f"{path}:{line_number}: record does not start with ':'")
try:
record = bytes.fromhex(text[1:])
except ValueError as exc:
raise ProgramError(f"{path}:{line_number}: invalid hexadecimal record") from exc
if len(record) < 5 or len(record) != record[0] + 5:
raise ProgramError(f"{path}:{line_number}: invalid record length")
if sum(record) & 0xFF:
raise ProgramError(f"{path}:{line_number}: Intel HEX checksum mismatch")
count = record[0]
offset = (record[1] << 8) | record[2]
record_type = record[3]
data = record[4 : 4 + count]
if record_type == 0x00:
start = address_base + offset
if start + count > 0x10000:
raise ProgramError(
f"{path}:{line_number}: address is outside the Z80's 64 KiB space"
)
for index, value in enumerate(data):
address = start + index
old_value = memory.get(address)
if old_value is not None and old_value != value:
raise ProgramError(
f"{path}:{line_number}: conflicting data at {address:04X}"
)
memory[address] = value
elif record_type == 0x01:
if count != 0:
raise ProgramError(f"{path}:{line_number}: malformed EOF record")
saw_eof = True
elif record_type == 0x02:
if count != 2:
raise ProgramError(
f"{path}:{line_number}: malformed extended segment address"
)
address_base = int.from_bytes(data, "big") << 4
elif record_type == 0x04:
if count != 2:
raise ProgramError(
f"{path}:{line_number}: malformed extended linear address"
)
address_base = int.from_bytes(data, "big") << 16
elif record_type in (0x03, 0x05):
# Start-address records do not contain bytes to program.
if count != 4:
raise ProgramError(
f"{path}:{line_number}: malformed start-address record"
)
continue
else:
raise ProgramError(
f"{path}:{line_number}: unsupported record type {record_type:02X}"
)
if not saw_eof:
raise ProgramError(f"{path}: missing Intel HEX EOF record")
if not memory:
raise ProgramError(f"{path}: file contains no data")
return memory
def make_blocks(memory: dict[int, int], block_size: int) -> list[tuple[int, bytes]]:
"""Combine adjacent addresses without filling gaps from the HEX file."""
blocks: list[tuple[int, bytes]] = []
addresses = sorted(memory)
index = 0
while index < len(addresses):
start = addresses[index]
data = bytearray([memory[start]])
index += 1
while (
index < len(addresses)
and len(data) < block_size
and addresses[index] == start + len(data)
):
data.append(memory[addresses[index]])
index += 1
blocks.append((start, bytes(data)))
return blocks
class Monitor:
def __init__(self, port: str, baud: int, timeout: float, char_delay: float):
self.char_delay = char_delay
try:
self.serial = serial.Serial(
port=port,
baudrate=baud,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=timeout,
write_timeout=timeout,
)
except serial.SerialException as exc:
raise ProgramError(f"cannot open serial port {port}: {exc}") from exc
def close(self) -> None:
self.serial.close()
def command(self, command: str) -> str:
try:
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"> "):
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("")
if not response.endswith("> "):
raise ProgramError("monitor prompt not found")
def response_has_ok(response: str) -> bool:
return any(line.strip() == "OK" for line in response.splitlines())
def response_checksum(response: str) -> int | None:
matches = [
int(line.strip(), 16)
for line in response.splitlines()
if re.fullmatch(r"[0-9A-Fa-f]{4}", line.strip())
]
return matches[-1] if matches else None
def write_and_verify(monitor: Monitor, start: int, data: bytes) -> tuple[bool, str]:
values = " ".join(f"{value:02X}" for value in data)
response = monitor.command(f"W {start:04X} {values}")
if not response_has_ok(response):
return False, 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, 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, ""
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Program an Intel HEX file through the Z80 UART monitor."
)
parser.add_argument("port", help="serial port, for example /dev/ttyUSB0 or COM3")
parser.add_argument("file", type=Path, help="Intel HEX input file")
parser.add_argument("--baud", type=int, default=2400, help="baud rate (default: 2400)")
parser.add_argument(
"--block-size",
type=int,
default=16,
help=f"bytes per verified block, 1-{MAX_BLOCK_SIZE} (default: 16)",
)
parser.add_argument(
"--retries",
type=int,
default=3,
help="retries after the first attempt (default: 3)",
)
parser.add_argument(
"--timeout",
type=float,
default=5.0,
help="serial response timeout in seconds (default: 5)",
)
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}")
if args.retries < 0:
parser.error("--retries cannot be negative")
if args.baud <= 0:
parser.error("--baud must be positive")
if args.timeout <= 0:
parser.error("--timeout must be positive")
if args.char_delay < 0:
parser.error("--char-delay cannot be negative")
return args
def main() -> int:
args = parse_args()
monitor: Monitor | None = None
try:
memory = load_ihex(args.file)
blocks = make_blocks(memory, args.block_size)
print(
f"Loaded {len(memory)} bytes in {len(blocks)} blocks "
f"({min(memory):04X}-{max(memory):04X})"
)
monitor = Monitor(args.port, args.baud, args.timeout, args.char_delay)
monitor.synchronize()
for number, (start, data) in enumerate(blocks, 1):
end = start + len(data) - 1
for attempt in range(args.retries + 1):
try:
if attempt:
monitor.synchronize()
success, reason = write_and_verify(monitor, start, data)
except ProgramError as exc:
success, reason = False, str(exc)
if success:
print(
f"Block {number}/{len(blocks)} "
f"{start:04X}-{end:04X} verified"
)
break
if attempt == args.retries:
raise ProgramError(
f"block {start:04X}-{end:04X} failed after "
f"{args.retries + 1} attempts: {reason}"
)
print(
f"Retrying {start:04X}-{end:04X} "
f"({attempt + 1}/{args.retries}): {reason}",
file=sys.stderr,
)
print(f"Programmed and verified {len(memory)} bytes")
return 0
except ProgramError as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
except KeyboardInterrupt:
print("\nCancelled", file=sys.stderr)
return 130
finally:
if monitor is not None:
monitor.close()
if __name__ == "__main__":
raise SystemExit(main())
-16
View File
@@ -1,16 +0,0 @@
# 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
NASCOM_BASIC_ROM_ADDRESS := 0x4000
BIN
View File
Binary file not shown.