velxio/scripts/make-z80-cpu.py

282 lines
10 KiB
Python

"""Generate z80-cpu.c from z80.c — strip external bus pins, swap in internal
RAM + ROM + memory-mapped LED/BTN/UART peripherals.
Run from repo root: `python scripts/make-z80-cpu.py`.
"""
from __future__ import annotations
import re
from pathlib import Path
SRC = Path('frontend/src/components/customChips/examples/intel/z80.c')
OUT = Path('frontend/src/components/customChips/examples/intel/z80-cpu.c')
src = SRC.read_text(encoding='utf-8')
# ─── helpers ─────────────────────────────────────────────────────────────
def replace_func(s: str, signature: str, body_replacement: str) -> str:
"""Replace a C function definition (signature + braced body) with a new
one-liner body. Uses a brace-depth scanner so nested `{` don't trip us."""
idx = s.find(signature)
if idx < 0:
return s
open_b = s.find('{', idx + len(signature))
if open_b < 0:
return s
depth = 1
i = open_b + 1
while i < len(s) and depth > 0:
if s[i] == '{':
depth += 1
elif s[i] == '}':
depth -= 1
i += 1
return s[:idx] + signature + ' { ' + body_replacement + ' }' + s[i:]
# ─── 1) Replace top-of-file comment ──────────────────────────────────────
src = re.sub(
r'/\*\n \* Zilog Z80 emulator.+?\*/\n',
'''/*
* z80-cpu.c — programmable Zilog Z80 chip for Velxio.
*
* Generated from z80.c by scripts/make-z80-cpu.py. The Z80 CPU emulation
* is the same clean-room implementation validated by test_z80/z80.test.js
* (passes ZEXDOC end-to-end). The external pin/bus protocol is replaced
* with internal RAM + ROM + memory-mapped LED/BTN/UART peripherals so the
* chip is drop-and-go on the Velxio canvas.
*
* The ROM image is loaded at chip_setup via vx_rom_size / vx_rom_read,
* sourced from the chip's romBytes property (typed in a project file,
* compiled by POST /api/compile-rom).
*
* Memory map:
* 0x0000..0x7FFF ROM (up to 32 KB, external)
* 0x8000..0xBFFF RAM (16 KB internal)
* 0xC000 LED_OUT
* 0xC001 UART_DATA
* 0xC002 UART_STAT (bit 0 = TX ready, bit 1 = RX has byte)
* 0xC003 BTN_IN
* 0xC004 EDGE_FLAGS (read = rising-edge latch; cleared on read)
*
* IN/OUT port instructions mirror MMIO at 0xC000+port_lo.
*/
''',
src,
count=1,
flags=re.DOTALL,
)
# ─── 2) Strip pin fields from cpu_t ──────────────────────────────────────
src = re.sub(
r' /\* Pins \*/\n'
r' vx_pin apin\[16\], dpin\[8\];\n'
r' vx_pin m1, mreq, iorq, rd, wr, rfsh, halt_, wait_;\n'
r' vx_pin intn, nmi, reset_, busreq, busack, clk;\n'
r' vx_pin vcc, gnd;\n'
r' vx_timer cycle_timer;\n',
' vx_timer cycle_timer;\n',
src,
)
# ─── 3) Reduce the four pin-driving helpers to no-ops ───────────────────
src = replace_func(src, 'static void drive_addr(uint16_t a)', '(void)a;')
src = replace_func(src, 'static void release_data(void)', '/* noop */')
src = replace_func(src, 'static uint8_t read_data(void)', 'return 0;')
src = replace_func(src, 'static void drive_data(uint8_t v)', '(void)v;')
# ─── 4) Strip every remaining vx_pin_* on G.<bus pin> ───────────────────
PIN_NAMES = (
r'(?:m1|mreq|iorq|rd|wr|rfsh|halt_|wait_|intn|nmi|reset_|busreq|busack|clk|vcc|gnd'
r'|apin\[[^\]]*\]|dpin\[[^\]]*\])'
)
src = re.sub(rf'^\s*vx_pin_write\(G\.{PIN_NAMES}\s*,[^;]*;\n', '', src, flags=re.MULTILINE)
src = re.sub(rf'^\s*vx_pin_set_mode\(G\.{PIN_NAMES}\s*,[^;]*;\n', '', src, flags=re.MULTILINE)
src = re.sub(r'vx_pin_read\(G\.busreq\)', '1', src)
src = re.sub(r'vx_pin_read\(G\.wait_\)', '1', src)
src = re.sub(r'vx_pin_read\(G\.(intn|nmi|reset_)\)', '0', src)
src = re.sub(r'\s*vx_pin_watch\(G\.(reset_|intn|nmi)[^;]*;\n', '\n', src)
# ─── 5) Drop the original pin-registration loops + lines (we add our own) ──
src = re.sub(
r'/\* A0\.\.A15.+?G\.apin\[i\] = vx_pin_register\([^)]*\);\s*}\n',
'', src, flags=re.DOTALL,
)
src = re.sub(
r'/\* D0\.\.D7.+?G\.dpin\[i\] = vx_pin_register\([^)]*\);\s*}\n',
'', src, flags=re.DOTALL,
)
src = re.sub(
rf' G\.{PIN_NAMES}\s*=\s*vx_pin_register\([^)]*\);\s*\n',
'', src,
)
# ─── 6) Replace each bus function in place ──────────────────────────────
src = replace_func(
src, 'static uint8_t opcode_fetch(uint16_t addr)',
'G.r = (G.r & 0x80) | ((G.r + 1) & 0x7F); return bus_mem_read(addr);',
)
src = replace_func(
src, 'static uint8_t mem_read(uint16_t addr)',
'return bus_mem_read(addr);',
)
src = replace_func(
src, 'static void mem_write(uint16_t addr, uint8_t data)',
'bus_mem_write(addr, data);',
)
src = replace_func(
src, 'static uint8_t io_read(uint16_t addr)',
'return bus_mem_read(0xC000 + (addr & 0xFF));',
)
src = replace_func(
src, 'static void io_write(uint16_t addr, uint8_t data)',
'bus_mem_write(0xC000 + (addr & 0xFF), data);',
)
# ─── 7) Inject helper state + bus_mem_read / bus_mem_write + UART hooks ──
helpers = '''
/* ─── External ROM + internal RAM + MMIO state ───────────────────────── */
#define ROM_MAX 0x8000
#define RAM_BASE 0x8000
/* 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
#define MMIO_BTN_IN 0xC003
#define MMIO_EDGE_FLAGS 0xC004
static uint8_t ROMBUF[ROM_MAX];
static uint32_t ROMSZ = 0;
static uint8_t RAMBUF[RAM_SIZE];
#define RX_BUFSZ 64
static uint8_t rx_buf[RX_BUFSZ];
static volatile uint32_t rx_head = 0, rx_tail = 0;
static bool rx_has(void) { return rx_head != rx_tail; }
static uint8_t rx_pop(void) {
if (rx_head == rx_tail) return 0;
uint8_t v = rx_buf[rx_tail];
rx_tail = (rx_tail + 1) % RX_BUFSZ;
return v;
}
static void rx_push(uint8_t b) {
uint32_t n = (rx_head + 1) % RX_BUFSZ;
if (n == rx_tail) return;
rx_buf[rx_head] = b; rx_head = n;
}
static vx_pin g_led[8];
static vx_pin g_btn[8];
static volatile uint8_t edge_latch = 0;
static vx_uart g_uart;
static void on_btn_rising(void* ud, vx_pin pin, int value) {
(void)pin; (void)value;
uint32_t idx = (uintptr_t)ud;
if (idx < 8) edge_latch |= (uint8_t)(1u << idx);
}
static uint8_t read_btn_bitmap(void) {
uint8_t b = 0;
for (int i = 0; i < 8; i++) if (vx_pin_read(g_btn[i])) b |= (uint8_t)(1u << i);
return b;
}
static void drive_leds(uint8_t v) {
for (int i = 0; i < 8; i++) vx_pin_write(g_led[i], (v >> i) & 1);
}
static uint8_t bus_mem_read(uint16_t addr) {
if (addr < ROMSZ) return ROMBUF[addr];
/* 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) {
/* 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;
}
}
if (addr >= RAM_BASE) { /* 0x8000-0xFFFF */
RAMBUF[addr - RAM_BASE] = v; return;
}
}
static void on_uart_rx(void* ud, uint8_t byte) { (void)ud; rx_push(byte); }
static void on_uart_tx_done(void* ud) { (void)ud; }
'''
# Insert just before the first use of opcode_fetch so bus_mem_read is defined
# before opcode_fetch references it.
src = src.replace('static uint8_t opcode_fetch', helpers + 'static uint8_t opcode_fetch', 1)
# ─── 8) Replace on_clock + chip_setup with chip-specific versions ───────
src = replace_func(
src, 'static void on_clock(void* user_data)',
'(void)user_data; if (ROMSZ == 0) return; for (int i = 0; i < 200; i++) step();',
)
new_setup = (
'char name[8]; '
'for (int i = 0; i < 8; i++) { '
'name[0] = \'L\'; name[1] = \'E\'; name[2] = \'D\'; '
'name[3] = (char)(\'0\' + i); name[4] = 0; '
'g_led[i] = vx_pin_register(name, VX_OUTPUT_LOW); '
'} '
'for (int i = 0; i < 8; i++) { '
'name[0] = \'B\'; name[1] = \'T\'; name[2] = \'N\'; '
'name[3] = (char)(\'0\' + i); name[4] = 0; '
'g_btn[i] = vx_pin_register(name, VX_INPUT_PULLDOWN); '
'vx_pin_watch(g_btn[i], VX_EDGE_RISING, on_btn_rising, (void*)(uintptr_t)i); '
'} '
'vx_uart_config cfg = { '
'.rx = vx_pin_register("RX", VX_INPUT), '
'.tx = vx_pin_register("TX", VX_OUTPUT_HIGH), '
'.baud_rate = 9600, '
'.on_rx_byte = on_uart_rx, '
'.on_tx_done = on_uart_tx_done, '
'.user_data = 0, '
'}; '
'g_uart = vx_uart_attach(&cfg); '
'vx_pin_register("VCC", VX_INPUT); '
'vx_pin_register("GND", VX_INPUT); '
'uint32_t n = vx_rom_size(); '
'if (n > ROM_MAX) n = ROM_MAX; '
'if (n > 0) { vx_rom_read(0, ROMBUF, n); ROMSZ = n; } '
'else { vx_log("z80-cpu: no romBytes attached. Compile a .s/.hex/.bin file."); } '
'reset_state(); '
'G.cycle_timer = vx_timer_create(on_clock, 0); '
'vx_timer_start(G.cycle_timer, 1000000, true); '
)
src = replace_func(src, 'void chip_setup(void)', new_setup)
OUT.write_text(src, encoding='utf-8')
print(f'wrote {OUT} ({len(src)} chars)')