From d19ffc70a123ba8c6dd78e358a63e9d7f9df6297 Mon Sep 17 00:00:00 2001 From: Joppe Blondel Date: Thu, 1 Oct 2026 12:17:47 +0200 Subject: [PATCH] new memory and project layout --- Makefile | 31 ++-- README.md | 94 +++++++---- apps/README.md | 26 ++++ tinybasic_sdas.asm => apps/tinybasic.asm | 31 ++-- bios.c | 113 ++++++++++++++ bios.h | 22 +++ boot.asm | 142 ++++++++++++----- boot.h | 13 -- include/bios.inc | 8 + main.c => monitor.c | 189 +++++------------------ 10 files changed, 397 insertions(+), 272 deletions(-) create mode 100644 apps/README.md rename tinybasic_sdas.asm => apps/tinybasic.asm (98%) create mode 100644 bios.c create mode 100644 bios.h delete mode 100644 boot.h create mode 100644 include/bios.inc rename main.c => monitor.c (61%) diff --git a/Makefile b/Makefile index b971da6..4ed3ea9 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ TARGET := monitor BUILDDIR := build METAL_C_SRCFILES := $(wildcard *.c) -METAL_ASM_SRCFILES := $(wildcard *.asm) +METAL_ASM_SRCFILES := boot.asm METAL_RELFILES := $(METAL_ASM_SRCFILES:%.asm=$(BUILDDIR)/metal/%.rel) \ $(METAL_C_SRCFILES:%.c=$(BUILDDIR)/metal/%.rel) @@ -14,12 +14,12 @@ METAL_CPPFLAGS := -I src -I $(BUILDDIR)/metal/generated --disable-warning 2 METAL_BIN_SIZE := 32768 METAL_RAM_SIZE := 16384 METAL_IMAGE_SIZE := 65536 -MONITOR_BASE := 0xC000 -MONITOR_ROM_OFFSET := 128 +RESIDENT_BASE := 0xC000 +RESIDENT_ROM_OFFSET := 128 -TINYBASIC_BASE := 0xD000 +TINYBASIC_BASE := 0x1000 TINYBASIC_ROM_OFFSET := 4096 -TINYBASIC_SRC := tinybasic_sdas.asm +TINYBASIC_SRC := apps/tinybasic.asm TINYBASIC_BUILD := $(BUILDDIR)/tinybasic .DEFAULT_GOAL := metal @@ -36,9 +36,9 @@ metal: $(BUILDDIR)/metal/$(TARGET).ihx $(BUILDDIR)/metal/$(TARGET).bin $(BUILDDI END { \ printf "\nMemory usage:\n"; \ printf " ROM storage: %5u / %5u bytes (%5.1f%%)\n", stored, rom_capacity, 100 * stored / rom_capacity; \ - printf " Monitor linked: %5u bytes (%5u code/vector + %5u static data)\n", code + data, code, data; \ + printf " Resident BIOS+monitor: %5u bytes (%5u code/vector + %5u static data)\n", code + data, code, data; \ }' $(BUILDDIR)/metal/$(TARGET).map - @printf " Tiny BASIC: %5u bytes at D000\n" $$(wc -c < $(TINYBASIC_BUILD)/tinybasic.bin) + @printf " Tiny BASIC: %5u ROM bytes at 1000\n" $$(wc -c < $(TINYBASIC_BUILD)/tinybasic.bin) $(BUILDDIR)/metal/$(TARGET).ihx: $(METAL_RELFILES) @mkdir -p $(@D) @@ -48,11 +48,12 @@ $(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 - @payload_end=$$(awk '$$1 == "DEF" && $$2 == "monitor_image_end" { print $$3 }' $(BUILDDIR)/metal/$(TARGET).noi); \ - payload_size=$$((payload_end - $(MONITOR_BASE))); \ + @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=$$(( $(MONITOR_BASE) )) seek=$(MONITOR_ROM_OFFSET) count=$$payload_size conv=notrunc 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 $(BUILDDIR)/metal/$(TARGET).bin: $(BUILDDIR)/metal/$(TARGET).flat.bin $(BUILDDIR)/metal/$(TARGET).prog.bin @@ -65,13 +66,11 @@ $(BUILDDIR)/metal/%.rel: %.c $(BUILDDIR)/metal/%.rel: %.asm @mkdir -p $(@D) - $(METAL_AS) -I$(BUILDDIR)/metal/generated -plosgff -o $@ $< - -$(BUILDDIR)/metal/boot.rel: $(BUILDDIR)/metal/generated/tinybasic.inc + $(METAL_AS) -Iinclude -I$(BUILDDIR)/metal/generated -plosgff -o $@ $< $(TINYBASIC_BUILD)/tinybasic.rel: $(TINYBASIC_SRC) @mkdir -p $(@D) - $(METAL_AS) -plosgff -o $@ $< + $(METAL_AS) -Iinclude -plosgff -o $@ $< $(TINYBASIC_BUILD)/tinybasic.ihx: $(TINYBASIC_BUILD)/tinybasic.rel sdldz80 -n -i -m -j $(basename $@) $< @@ -84,10 +83,6 @@ $(TINYBASIC_BUILD)/tinybasic.bin: $(TINYBASIC_BUILD)/tinybasic.flat.bin test $$tiny_size -gt 0; \ dd if=$< of=$@ bs=1 skip=$$(( $(TINYBASIC_BASE) )) count=$$tiny_size status=none -$(BUILDDIR)/metal/generated/tinybasic.inc: $(TINYBASIC_BUILD)/tinybasic.ihx - @mkdir -p $(@D) - @awk '$$1 == "TINYBASIC0" { printf "TINYBASIC_SIZE = 0x%04X\n", $$5 }' $(TINYBASIC_BUILD)/tinybasic.map > $@ - clean: rm -rf $(BUILDDIR) diff --git a/README.md b/README.md index d75fb11..14a1313 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ # Z80 UART monitor -This ROM provides a small interactive monitor and a bundled Z80 Tiny BASIC -interpreter on the UART/CTC card at I/O base `20`. At reset, a small ROM -bootstrap copies both programs into the top 16 KiB of RAM and starts the -monitor. Only occupied code/vector bytes are stored and copied; uninitialized -data is cleared directly in high RAM. The low ROM can subsequently be disabled -without taking away the monitor, Tiny BASIC, monitor data, or interrupt table. -No runtime RAM below `C000` is used. +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 of RAM at `C000`. The BIOS initializes interrupt mode 2 and the UART, +then 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`. 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 @@ -20,8 +21,8 @@ optional `0x` prefix is accepted. | `I 20` | Read I/O port 20 | | `O 20 57` | Write 57 to I/O port 20 | | `G 8000` | Execute code at address 8000 | -| `G D000` | Start the bundled Z80 Tiny BASIC interpreter | -| `B` or `BASIC` | Shortcut for `G D000` | +| `G 1000` | Start the ROM-resident Z80 Tiny BASIC interpreter | +| `B` or `BASIC` | Shortcut for `G 1000` | | `HELP` | Show the command summary | `G` transfers control with a return address on the stack, so code ending in @@ -43,45 +44,70 @@ The outputs are: The current compact image is 6,157 bytes. Its ROM layout is: - `0000-007F`: reset/copy bootstrap -- `0080-0F6E`: monitor image copied to RAM at `C000-CEEE` -- `0F6F-0FFF`: unused space reserved for monitor growth -- `1000-180C`: Tiny BASIC image copied to RAM at `D000-D80C` +- `0080-0F9D`: packed BIOS+monitor image copied to RAM at `C000-CF1D` +- `0F9E-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 -The monitor executable starts at `C080`; its IM 2 vector table is at `C040`. -The bootstrap obtains both copy lengths from generated/linker symbols, so the -copied sizes track changes to the programs automatically. +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 high-RAM layout is: +The resulting RAM layout is: -- `C000-CEEE`: monitor vectors, services, and code -- `CEEF-CFFF`: free -- `D000-D80C`: Tiny BASIC code -- `D80D-DFFF`: free -- `E000-E7FF`: Tiny BASIC workspace and BASIC program text -- `E800-E877`: Tiny BASIC variables and input buffer -- `E878-EFFF`: Tiny BASIC stack space -- `F000-FFFF`: monitor globals and monitor stack +- `8000-AFFF`: Tiny BASIC workspace and BASIC program text +- `B000-B077`: Tiny BASIC variables and input buffer +- `B078-BFFF`: Tiny BASIC stack space +- `C000-CF1D`: resident BIOS, vectors, services, and monitor +- `CF1E-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. + +## 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 | + +The service wrappers preserve working registers except for documented return +values. The monitor uses the corresponding C API declared in `bios.h`. + +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 +- `apps/README.md`: ROM application memory and integration contract +- `include/bios.inc`: stable assembly interface for ROM applications ## Tiny BASIC -The original GNU-z80asm listing is retained as `tinybasic/tinybasic.asm`. Its -`sdasz80` port is `tinybasic/tinybasic_sdas.asm` and is built automatically. -The port replaces fixed low-page `RST` calls with relocatable calls and uses a -marker-based command table so addresses in the `D000` page work correctly. +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. -At reset the bootstrap copies Tiny BASIC from ROM offset `1000` to `D000`. -Start it from the monitor with: +Tiny BASIC is linked at and runs directly from ROM address `1000`. Start it from +the monitor with: ```text B ``` -`BASIC` and `G D000` are equivalent alternatives. +`BASIC` and `G 1000` are equivalent alternatives. -Tiny BASIC calls the monitor ABI at `C050` for output and `C053` for -non-blocking input. The assembly ABI wrappers preserve the working registers, -and Tiny BASIC keeps interrupts enabled so it can share the monitor's configured +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 diff --git a/apps/README.md b/apps/README.md new file mode 100644 index 0000000..1fe8a18 --- /dev/null +++ b/apps/README.md @@ -0,0 +1,26 @@ +# 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: + +- `1000-180C`: Tiny BASIC +- `180D-7FFF`: available for more applications (`1900` 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. + +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. + +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. diff --git a/tinybasic_sdas.asm b/apps/tinybasic.asm similarity index 98% rename from tinybasic_sdas.asm rename to apps/tinybasic.asm index 2c0c861..eb632af 100644 --- a/tinybasic_sdas.asm +++ b/apps/tinybasic.asm @@ -42,13 +42,11 @@ ; Adapted to work with GNU z80asm ; Added interrupt handling ; Notes for this monitor port: -; Converted to sdasz80 syntax and relocated to D000. +; Converted to sdasz80 syntax and linked as a ROM application at 1000. ; Fixed-page RST calls were converted to local CALLs. -; UART access uses the monitor ABI at C050/C053. +; UART access uses the resident BIOS ABI at C050/C053. -; Monitor ABI jump table. These entry points remain stable across monitor builds. -MONITOR_PUTC = 0xC050 -MONITOR_GETC = 0xC053 + .include "bios.inc" SPACE = 0x020 ; Space @@ -67,9 +65,10 @@ CTRLU = 0x015 ; Control "U" ESC = 0x01B ; Escape DEL = 0x07F ; Delete -; Adjust to fit RAM mapping -STACK = 0xEFFF ; Tiny BASIC stack grows down below monitor data -OCSW = 0xE000 ;SWITCH FOR OUTPUT +; Tiny BASIC owns lower RAM 8000-BFFF. The resident BIOS and monitor own +; C000-FFFF, so BASIC cannot overwrite them even when its stack is deep. +STACK = 0xBFFF ; Tiny BASIC stack grows down through B078-BFFF +OCSW = 0x8000 ;SWITCH FOR OUTPUT CURRNT = OCSW+1 ;POINTS FOR OUTPUT STKGOS = OCSW+3 ;SAVES SP IN 'GOSUB' VARNXT = OCSW+5 ;TEMP STORAGE @@ -83,11 +82,11 @@ RANPNT = OCSW+19 ;RANDOM NUMBER POINTER TXTUNF = OCSW+21 ;->UNFILLED TEXT AREA TXTBGN = OCSW+23 ;TEXT SAVE AREA BEGINS -TXTEND = 0xE800 ;TEXT SAVE AREA ENDS -VARBGN = 0xE800 ;VARIABLE @(0) -BUFFER = 0xE837 ;55-byte variable area ends here -BUFEND = 0xE877 ;64-byte input buffer ends here -STKLMT = 0xE878 ;LOWEST ALLOWED STACK ADDRESS +TXTEND = 0xB000 ;TEXT SAVE AREA ENDS +VARBGN = 0xB000 ;VARIABLE @(0) +BUFFER = 0xB037 ;55-byte variable area ends here +BUFEND = 0xB077 ;64-byte input buffer ends here +STKLMT = 0xB078 ;LOWEST ALLOWED STACK ADDRESS ;************************************************************* @@ -103,7 +102,7 @@ STKLMT = 0xE878 ;LOWEST ALLOWED STACK ADDRESS ;************************************************************* .area TINYBASIC (ABS) - .org 0xD000 + .org 0x1000 main: START: LD SP,#STACK ;*** COLD START *** @@ -1601,7 +1600,7 @@ OUTC: OUTC2: CALL TX_RDY ;SEE IF TRANSMIT IS AVAILABLE POP AF ;RESTORE THE REGISTER - CALL MONITOR_PUTC ;SEND THE BYTE THROUGH MONITOR UART + CALL BIOS_PUTC ;SEND THE BYTE THROUGH BIOS UART CP #CR RET NZ LD A,#LF @@ -1826,7 +1825,7 @@ RX_RDY: ; However, if a character is not available, it returns with the ; Z-flag set. - CALL MONITOR_GETC + CALL BIOS_TRY_GETC OR A RET ;------------------------------------------------------------------------------- diff --git a/bios.c b/bios.c new file mode 100644 index 0000000..3d9d55e --- /dev/null +++ b/bios.c @@ -0,0 +1,113 @@ +#include +#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_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); + + 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, 0b00000011); + 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) +{ +} diff --git a/bios.h b/bios.h new file mode 100644 index 0000000..51bb4e8 --- /dev/null +++ b/bios.h @@ -0,0 +1,22 @@ +#ifndef BIOS_H +#define BIOS_H + +#include + +/* 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); + +/* 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 diff --git a/boot.asm b/boot.asm index 2dd08e9..c531770 100644 --- a/boot.asm +++ b/boot.asm @@ -1,11 +1,9 @@ STACK_TOP = 0xFFFF DELAY_LOOPS_PER_MS = 40 -MONITOR_BASE = 0xC000 -MONITOR_ROM_SOURCE = 0x0080 -TINYBASIC_BASE = 0xD000 -TINYBASIC_ROM_SOURCE = 0x1000 +RESIDENT_BASE = 0xC000 +RESIDENT_ROM_SOURCE = 0x0080 - .include "tinybasic.inc" + .include "bios.inc" .globl _main .globl _delay_ms @@ -14,9 +12,11 @@ TINYBASIC_ROM_SOURCE = 0x1000 .globl _enable_interrupts .globl _disable_interrupts .globl _jump_to - .globl _monitor_uart_putc - .globl _monitor_uart_getc - .globl monitor_image_end + .globl _bios_putc + .globl _bios_try_getc + .globl _bios_getc + .globl _bios_init + .globl resident_image_end .globl s__DATA .globl l__DATA .globl s__INITIALIZER @@ -31,50 +31,56 @@ TINYBASIC_ROM_SOURCE = 0x1000 boot_copy: di ld sp, #STACK_TOP - ld hl, #MONITOR_ROM_SOURCE - ld de, #MONITOR_BASE - ld bc, #(monitor_image_end - MONITOR_BASE) - ldir - - ld hl, #TINYBASIC_ROM_SOURCE - ld de, #TINYBASIC_BASE - ld bc, #TINYBASIC_SIZE + ld hl, #RESIDENT_ROM_SOURCE + ld de, #RESIDENT_BASE + ld bc, #(resident_image_end - RESIDENT_BASE) ldir jp init .area _ITABLE (ABS) ; The executable vector page is part of the image copied to high RAM. -.org MONITOR_BASE +.org RESIDENT_BASE jp init -.org MONITOR_BASE + 0x08 +.org RESIDENT_BASE + 0x08 reti -.org MONITOR_BASE + 0x10 +.org RESIDENT_BASE + 0x10 reti -.org MONITOR_BASE + 0x18 +.org RESIDENT_BASE + 0x18 reti -.org MONITOR_BASE + 0x20 +.org RESIDENT_BASE + 0x20 reti -.org MONITOR_BASE + 0x28 +.org RESIDENT_BASE + 0x28 reti -.org MONITOR_BASE + 0x30 +.org RESIDENT_BASE + 0x30 reti -.org MONITOR_BASE + 0x38 +.org RESIDENT_BASE + 0x38 reti ; IM 2 table: the CTC supplies vectors 40, 42, 44, and 46. -.org MONITOR_BASE + 0x40 +.org RESIDENT_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 entry points for programs living alongside the monitor. -; C050: write the character in A. C053: return a character in A, or 0. -.org MONITOR_BASE + 0x50 - jp monitor_service_putc -.org MONITOR_BASE + 0x53 - jp monitor_service_getc +; 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. +.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 .area _HOME init: @@ -87,6 +93,7 @@ init: ld a, #0xC0 ld i, a im 2 + call _bios_init call _main hang: @@ -124,20 +131,19 @@ clear_loop: gsinit_done: .area _GSFINAL ret -monitor_image_end: +resident_image_end: .area _CODE -; Monitor ABI services preserve all working registers. PUTC also preserves AF; -; GETC returns the received byte (or zero) in A. -monitor_service_putc: +; 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 _monitor_uart_putc + call _bios_putc pop iy pop ix pop hl @@ -146,13 +152,13 @@ monitor_service_putc: pop af ret -monitor_service_getc: +bios_service_try_getc: push bc push de push hl push ix push iy - call _monitor_uart_getc + call _bios_try_getc pop iy pop ix pop hl @@ -160,6 +166,66 @@ monitor_service_getc: 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_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: diff --git a/boot.h b/boot.h deleted file mode 100644 index 4336c9d..0000000 --- a/boot.h +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef __H_BOOT -#define __H_BOOT - -#include - -void io_out(uint8_t a, uint8_t d); -uint8_t io_in(uint8_t d); -void delay_ms(uint16_t t); -void enable_interrupts(void); -void disable_interrupts(void); -void jump_to(uint16_t address); - -#endif diff --git a/include/bios.inc b/include/bios.inc new file mode 100644 index 0000000..bae00d5 --- /dev/null +++ b/include/bios.inc @@ -0,0 +1,8 @@ +; 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 diff --git a/main.c b/monitor.c similarity index 61% rename from main.c rename to monitor.c index 773c0a5..d6b3b0c 100644 --- a/main.c +++ b/monitor.c @@ -1,113 +1,11 @@ #include -#include "boot.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_3 (CTC_BASE + 3) -#define UART_BASE (UART_CTC_CARD_BASE + 0x04) -#define UART_CONTROL (UART_BASE + 0) -#define UART_DATA (UART_BASE + 1) +#include "bios.h" #define LINE_SIZE 80 #define MAX_ARGS 40 #define DEFAULT_DUMP_LENGTH 0x40 #define MAX_DUMP_LENGTH 0x100 -volatile uint8_t uart_tx_busy; -volatile circular_buffer_t rx_buf; -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(); -} - -static void uart_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(); -} - -/* Stable monitor ABI implementations, reached through the jump table at C050. */ -void monitor_uart_putc(char c) -{ - uart_putc(c); -} - -uint8_t monitor_uart_getc(void) -{ - char c; - uint8_t received; - - disable_interrupts(); - received = buffer_dequeue(&rx_buf, &c); - enable_interrupts(); - return received ? (uint8_t)c : 0; -} - -static void uart_puts(const char *s) -{ - while (*s) uart_putc(*s++); -} - -static char uart_getc(void) -{ - char c; - uint8_t received; - - do { - disable_interrupts(); - received = buffer_dequeue(&rx_buf, &c); - enable_interrupts(); - } while (!received); - return c; -} - -static void uart_flush(void) -{ - while (uart_tx_busy || !buffer_is_empty(&tx_buf)) uart_kick(); -} - -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) -{ -} - static char hex_digit(uint8_t value) { value &= 0x0f; @@ -116,8 +14,8 @@ static char hex_digit(uint8_t value) static void print_hex8(uint8_t value) { - uart_putc(hex_digit(value >> 4)); - uart_putc(hex_digit(value)); + bios_putc(hex_digit(value >> 4)); + bios_putc(hex_digit(value)); } static void print_hex16(uint16_t value) @@ -192,14 +90,14 @@ static uint8_t split_line(char *line, char **argv) static void print_help(void) { - uart_puts( + bios_puts( "Commands (hexadecimal numbers):\r\n" " D|DUMP address [length] Dump memory (default 0040, max 0100)\r\n" " W|WRITE address byte ... Write one or more memory bytes\r\n" " I|IN port Read an I/O port\r\n" " O|OUT port byte Write an I/O port\r\n" " G|GO address Execute code; RET returns here\r\n" - " B|BASIC Start Tiny BASIC at D000\r\n" + " B|BASIC Start ROM Tiny BASIC at 1000\r\n" " H|HELP|? Show this help\r\n"); } @@ -211,11 +109,11 @@ static void command_dump(uint8_t argc, char **argv) if (argc < 2 || argc > 3 || !parse_hex(argv[1], &address) || (argc == 3 && !parse_hex(argv[2], &length))) { - uart_puts("Usage: D address [length]\r\n"); + bios_puts("Usage: D address [length]\r\n"); return; } if (length > MAX_DUMP_LENGTH) { - uart_puts("Length must be 0100 or less\r\n"); + bios_puts("Length must be 0100 or less\r\n"); return; } while (offset < length) { @@ -223,12 +121,12 @@ static void command_dump(uint8_t argc, char **argv) uint8_t row_length = (uint8_t)((length - offset) > 16 ? 16 : (length - offset)); print_hex16((uint16_t)(address + offset)); - uart_puts(": "); + bios_puts(": "); for (column = 0; column < row_length; ++column) { print_hex8(*(volatile uint8_t *)(address + offset + column)); - uart_putc(' '); + bios_putc(' '); } - uart_puts("\r\n"); + bios_puts("\r\n"); offset += row_length; } } @@ -240,19 +138,19 @@ static void command_write(uint8_t argc, char **argv) uint8_t i; if (argc < 3 || !parse_hex(argv[1], &address)) { - uart_puts("Usage: W address byte [byte ...]\r\n"); + bios_puts("Usage: W address byte [byte ...]\r\n"); return; } for (i = 2; i < argc; ++i) { if (!parse_hex(argv[i], &value) || value > 0xff) { - uart_puts("Bad byte: "); - uart_puts(argv[i]); - uart_puts("\r\n"); + bios_puts("Bad byte: "); + bios_puts(argv[i]); + bios_puts("\r\n"); return; } *(volatile uint8_t *)address++ = (uint8_t)value; } - uart_puts("OK\r\n"); + bios_puts("OK\r\n"); } static void command_in(uint8_t argc, char **argv) @@ -260,13 +158,13 @@ static void command_in(uint8_t argc, char **argv) uint16_t port; if (argc != 2 || !parse_hex(argv[1], &port) || port > 0xff) { - uart_puts("Usage: I port\r\n"); + bios_puts("Usage: I port\r\n"); return; } print_hex8((uint8_t)port); - uart_puts(": "); + bios_puts(": "); print_hex8(io_in((uint8_t)port)); - uart_puts("\r\n"); + bios_puts("\r\n"); } static void command_out(uint8_t argc, char **argv) @@ -276,21 +174,21 @@ static void command_out(uint8_t argc, char **argv) if (argc != 3 || !parse_hex(argv[1], &port) || port > 0xff || !parse_hex(argv[2], &value) || value > 0xff) { - uart_puts("Usage: O port byte\r\n"); + bios_puts("Usage: O port byte\r\n"); return; } io_out((uint8_t)port, (uint8_t)value); - uart_puts("OK\r\n"); + bios_puts("OK\r\n"); } static void execute_address(uint16_t address) { - uart_puts("Executing at "); + bios_puts("Executing at "); print_hex16(address); - uart_puts("...\r\n"); - uart_flush(); + bios_puts("...\r\n"); + bios_flush(); jump_to(address); - uart_puts("Returned to monitor\r\n"); + bios_puts("Returned to monitor\r\n"); } static void command_go(uint8_t argc, char **argv) @@ -298,7 +196,7 @@ static void command_go(uint8_t argc, char **argv) uint16_t address; if (argc != 2 || !parse_hex(argv[1], &address)) { - uart_puts("Usage: G address\r\n"); + bios_puts("Usage: G address\r\n"); return; } execute_address(address); @@ -307,10 +205,10 @@ static void command_go(uint8_t argc, char **argv) static void command_basic(uint8_t argc) { if (argc != 1) { - uart_puts("Usage: B\r\n"); + bios_puts("Usage: B\r\n"); return; } - execute_address(0xd000); + execute_address(0x1000); } static void execute_line(char *line) @@ -335,7 +233,7 @@ static void execute_line(char *line) } else if (strings_equal(argv[0], "b") || strings_equal(argv[0], "basic")) { command_basic(argc); } else { - uart_puts("Unknown command; type HELP\r\n"); + bios_puts("Unknown command; type HELP\r\n"); } } @@ -345,25 +243,10 @@ void main(void) uint8_t length = 0; uint8_t ignore_lf = 0; - buffer_init(&rx_buf); - buffer_init(&tx_buf); - - 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, 0b00000011); - io_out(UART_CONTROL, 0b10010101); - enable_interrupts(); - - uart_puts("\r\nZ80 monitor - v1\r\nType HELP for commands.\r\n> "); + bios_puts("\r\nZ80 monitor - v1\r\nType HELP for commands.\r\n> "); for (;;) { - char c = uart_getc(); + char c = bios_getc(); if (c == '\n' && ignore_lf) { ignore_lf = 0; @@ -373,25 +256,25 @@ void main(void) if (c == '\r' || c == '\n') { ignore_lf = (c == '\r'); line[length] = 0; - uart_puts("\r\n"); + bios_puts("\r\n"); execute_line(line); length = 0; - uart_puts("> "); + bios_puts("> "); } else if (c == '\b' || c == 0x7f) { if (length) { --length; - uart_puts("\b \b"); + bios_puts("\b \b"); } } else if (c == 0x15) { while (length) { --length; - uart_puts("\b \b"); + bios_puts("\b \b"); } } else if (c >= ' ' && c <= '~' && length < LINE_SIZE - 1) { line[length++] = c; - uart_putc(c); + bios_putc(c); } else if (c >= ' ' && c <= '~') { - uart_putc('\a'); + bios_putc('\a'); } } }