From 1d6961d03ce5c5676020e25013c5088b4116849b Mon Sep 17 00:00:00 2001 From: David Montero Date: Wed, 3 Jun 2026 13:31:26 +0200 Subject: [PATCH] fix(z80-cpu): map RAM over the whole 0x8000-0xFFFF so vanilla SDCC C runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SDCC's z80 crt0 sets SP=0x0000 and makes its first stack push at 0xFFFF. The chip only mapped RAM at 0x8000-0xBFFF (0xC000+ was MMIO/ignored), so the stack landed on unmapped memory and a plain C program crashed inside crt0 — before main — which is why z80-led-chaser-c compiled but drove nothing. Extend RAM to cover 0x8000-0xFFFF (32 KB) with the MMIO window 0xC000-0xC0FF carved out and checked first, in scripts/make-z80-cpu.py + regenerated z80-cpu.c. Now SDCC's default stack works and "write C from scratch, click Run" just works — no manual `LD SP` needed (dropped from chaser.c). Bumped the chip WASM initial memory to 4 pages to hold the larger RAM buffer. Larson (asm, SP=0xBFFF, LED at 0xC000) is unaffected. Co-Authored-By: Claude Opus 4.8 --- .../customChips/examples/intel/z80-cpu.c | 41 ++++++++++++------- frontend/src/data/examples-retro-intel.ts | 9 ---- .../src/simulation/customChips/ChipRuntime.ts | 5 ++- scripts/make-z80-cpu.py | 41 ++++++++++++------- 4 files changed, 58 insertions(+), 38 deletions(-) diff --git a/frontend/src/components/customChips/examples/intel/z80-cpu.c b/frontend/src/components/customChips/examples/intel/z80-cpu.c index bfe14b34..487c6938 100644 --- a/frontend/src/components/customChips/examples/intel/z80-cpu.c +++ b/frontend/src/components/customChips/examples/intel/z80-cpu.c @@ -82,7 +82,13 @@ static uint8_t read_data(void) { return 0; } /* ─── External ROM + internal RAM + MMIO state ───────────────────────── */ #define ROM_MAX 0x8000 #define RAM_BASE 0x8000 -#define RAM_SIZE 0x4000 +/* RAM spans 0x8000-0xFFFF (32 KB) so SDCC's default crt0 — which sets SP to + 0x0000 and makes its first push at 0xFFFF — lands in real RAM. Without this + a plain C program crashes in crt0 (before main) on this chip. The MMIO + window below is carved out of the RAM range and checked first. */ +#define RAM_SIZE 0x8000 +#define MMIO_BASE 0xC000 +#define MMIO_END 0xC0FF #define MMIO_LED_OUT 0xC000 #define MMIO_UART_DATA 0xC001 #define MMIO_UART_STAT 0xC002 @@ -130,24 +136,31 @@ static void drive_leds(uint8_t v) { static uint8_t bus_mem_read(uint16_t addr) { if (addr < ROMSZ) return ROMBUF[addr]; - if (addr >= RAM_BASE && addr < RAM_BASE + RAM_SIZE) return RAMBUF[addr - RAM_BASE]; - switch (addr) { - case MMIO_UART_DATA: return rx_has() ? rx_pop() : 0; - case MMIO_UART_STAT: { uint8_t s = 0x01; if (rx_has()) s |= 0x02; return s; } - case MMIO_BTN_IN: return read_btn_bitmap(); - case MMIO_EDGE_FLAGS: { uint8_t v = edge_latch; edge_latch = 0; return v; } - default: return 0xFF; + /* MMIO window has priority over RAM (it is carved out of the RAM range). */ + if (addr >= MMIO_BASE && addr <= MMIO_END) { + switch (addr) { + case MMIO_UART_DATA: return rx_has() ? rx_pop() : 0; + case MMIO_UART_STAT: { uint8_t s = 0x01; if (rx_has()) s |= 0x02; return s; } + case MMIO_BTN_IN: return read_btn_bitmap(); + case MMIO_EDGE_FLAGS: { uint8_t v = edge_latch; edge_latch = 0; return v; } + default: return 0xFF; + } } + if (addr >= RAM_BASE) return RAMBUF[addr - RAM_BASE]; /* 0x8000-0xFFFF */ + return 0xFF; } static void bus_mem_write(uint16_t addr, uint8_t v) { - if (addr >= RAM_BASE && addr < RAM_BASE + RAM_SIZE) { - RAMBUF[addr - RAM_BASE] = v; return; + /* MMIO window has priority over RAM (it is carved out of the RAM range). */ + if (addr >= MMIO_BASE && addr <= MMIO_END) { + switch (addr) { + case MMIO_LED_OUT: drive_leds(v); return; + case MMIO_UART_DATA: vx_uart_write(g_uart, &v, 1); return; + default: return; + } } - switch (addr) { - case MMIO_LED_OUT: drive_leds(v); return; - case MMIO_UART_DATA: vx_uart_write(g_uart, &v, 1); return; - default: return; + if (addr >= RAM_BASE) { /* 0x8000-0xFFFF */ + RAMBUF[addr - RAM_BASE] = v; return; } } diff --git a/frontend/src/data/examples-retro-intel.ts b/frontend/src/data/examples-retro-intel.ts index 7bf649fa..8b38749a 100644 --- a/frontend/src/data/examples-retro-intel.ts +++ b/frontend/src/data/examples-retro-intel.ts @@ -57,15 +57,6 @@ static void delay(unsigned int loops) { } void main(void) { - /* Park the stack at the top of the chip's 16 KB RAM (0x8000-0xBFFF). - SDCC's crt0 defaults SP to 0x0000; on this chip's memory map (RAM - 0x8000-0xBFFF, MMIO at 0xC000+) that would push the stack onto - unmapped high memory and crash on the first CALL. The asm Larson - example does the same with "LD SP, 0xBFFF". */ - __asm - ld sp, #0xBFFF - __endasm; - unsigned char bit = 0x01; char dir = 1; /* +1 = walking left, -1 = walking right */ while (1) { diff --git a/frontend/src/simulation/customChips/ChipRuntime.ts b/frontend/src/simulation/customChips/ChipRuntime.ts index 04901a67..1df4c9b5 100644 --- a/frontend/src/simulation/customChips/ChipRuntime.ts +++ b/frontend/src/simulation/customChips/ChipRuntime.ts @@ -208,7 +208,10 @@ export class ChipInstance { } private async _instantiate(): Promise { - this.memory = new WebAssembly.Memory({ initial: 2, maximum: 16 }); + // 4 pages (256 KB) initial: CPU-emulator chips like z80-cpu keep a 32 KB + // ROM + 32 KB RAM buffer as static data, which alone needs >2 pages once + // the WASM stack is added. Grows up to 16 pages on demand. + this.memory = new WebAssembly.Memory({ initial: 4, maximum: 16 }); this.wasi.setMemory(this.memory); const importObject: WebAssembly.Imports = { diff --git a/scripts/make-z80-cpu.py b/scripts/make-z80-cpu.py index 69063e43..2d2d3651 100644 --- a/scripts/make-z80-cpu.py +++ b/scripts/make-z80-cpu.py @@ -145,7 +145,13 @@ helpers = ''' /* ─── External ROM + internal RAM + MMIO state ───────────────────────── */ #define ROM_MAX 0x8000 #define RAM_BASE 0x8000 -#define RAM_SIZE 0x4000 +/* RAM spans 0x8000-0xFFFF (32 KB) so SDCC's default crt0 — which sets SP to + 0x0000 and makes its first push at 0xFFFF — lands in real RAM. Without this + a plain C program crashes in crt0 (before main) on this chip. The MMIO + window below is carved out of the RAM range and checked first. */ +#define RAM_SIZE 0x8000 +#define MMIO_BASE 0xC000 +#define MMIO_END 0xC0FF #define MMIO_LED_OUT 0xC000 #define MMIO_UART_DATA 0xC001 #define MMIO_UART_STAT 0xC002 @@ -193,24 +199,31 @@ static void drive_leds(uint8_t v) { static uint8_t bus_mem_read(uint16_t addr) { if (addr < ROMSZ) return ROMBUF[addr]; - if (addr >= RAM_BASE && addr < RAM_BASE + RAM_SIZE) return RAMBUF[addr - RAM_BASE]; - switch (addr) { - case MMIO_UART_DATA: return rx_has() ? rx_pop() : 0; - case MMIO_UART_STAT: { uint8_t s = 0x01; if (rx_has()) s |= 0x02; return s; } - case MMIO_BTN_IN: return read_btn_bitmap(); - case MMIO_EDGE_FLAGS: { uint8_t v = edge_latch; edge_latch = 0; return v; } - default: return 0xFF; + /* MMIO window has priority over RAM (it is carved out of the RAM range). */ + if (addr >= MMIO_BASE && addr <= MMIO_END) { + switch (addr) { + case MMIO_UART_DATA: return rx_has() ? rx_pop() : 0; + case MMIO_UART_STAT: { uint8_t s = 0x01; if (rx_has()) s |= 0x02; return s; } + case MMIO_BTN_IN: return read_btn_bitmap(); + case MMIO_EDGE_FLAGS: { uint8_t v = edge_latch; edge_latch = 0; return v; } + default: return 0xFF; + } } + if (addr >= RAM_BASE) return RAMBUF[addr - RAM_BASE]; /* 0x8000-0xFFFF */ + return 0xFF; } static void bus_mem_write(uint16_t addr, uint8_t v) { - if (addr >= RAM_BASE && addr < RAM_BASE + RAM_SIZE) { - RAMBUF[addr - RAM_BASE] = v; return; + /* MMIO window has priority over RAM (it is carved out of the RAM range). */ + if (addr >= MMIO_BASE && addr <= MMIO_END) { + switch (addr) { + case MMIO_LED_OUT: drive_leds(v); return; + case MMIO_UART_DATA: vx_uart_write(g_uart, &v, 1); return; + default: return; + } } - switch (addr) { - case MMIO_LED_OUT: drive_leds(v); return; - case MMIO_UART_DATA: vx_uart_write(g_uart, &v, 1); return; - default: return; + if (addr >= RAM_BASE) { /* 0x8000-0xFFFF */ + RAMBUF[addr - RAM_BASE] = v; return; } }