fix(z80-cpu): map RAM over the whole 0x8000-0xFFFF so vanilla SDCC C runs

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 <noreply@anthropic.com>
This commit is contained in:
David Montero 2026-06-03 13:31:26 +02:00
parent bf36487642
commit 1d6961d03c
4 changed files with 58 additions and 38 deletions

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -208,7 +208,10 @@ export class ChipInstance {
}
private async _instantiate(): Promise<void> {
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 = {

View File

@ -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;
}
}