velxio/test/test_intel/autosearch/03_emulation_strategy.md

170 lines
6.3 KiB
Markdown
Raw Normal View History

# Emulation strategy: mapping a CPU onto a velxio custom chip
The velxio runtime is **reactive** — there is no `loop()` hook. Every
chip is woken by either a pin edge or a timer fire. This document
describes how to fit an instruction-level CPU emulator into that model.
## Two viable execution models
### A. Timer-driven step (recommended starting point)
```
chip_setup() {
register all pins
reset_cpu_state()
timer = vx_timer_create()
vx_timer_start(timer, INSTR_PERIOD_NS, /* repeating */ true,
on_clock_tick, NULL)
}
on_clock_tick() {
step_one_instruction() // or step_one_machine_cycle()
drive_bus_pins_for_phase() // RD/WR/MREQ/ALE/etc.
}
```
- One timer fires at a chosen period (e.g. 250 ns for a 4 MHz Z80, or
coarser if we step a whole instruction per tick).
- The callback runs CPU work, sets the appropriate output pins for the
bus phase, and returns.
- Memory and I/O reads happen in two passes: in callback `N`, the CPU
drives the address pins and asserts `RD`; in callback `N+1`, the CPU
reads the data pins (which the memory chip on the canvas has had a
full tick to settle).
- This is **not cycle-accurate** with respect to the real silicon
unless the period equals one T-state. For most retro-computing demos
that's fine.
**Pro:** simple, deterministic, easy to reason about, easy to slow
down for visualization.
**Con:** can't react instantly to a wait state — has to wait for the
next tick. Mitigated by adding a pin watch on `WAIT`/`READY` that
stalls the timer-driven step.
### B. Pin-edge-driven step
```
chip_setup() {
register pins
vx_pin_watch(CLK_PIN, VX_RISING, on_clk_rising, NULL)
}
on_clk_rising() {
advance_one_t_state()
}
```
- The chip itself does **not** generate the clock; an external clock
chip on the canvas drives `CLK` and the CPU advances one T-state per
rising edge.
- Closer to how the real silicon behaves — the user can plug in a slow
manual clock and single-step.
- More expensive per second (one host callback per clock edge) but
matches user expectations and lets wait-state logic just be more pin
watches.
**Recommendation:** ship model A first for each chip (proves the ISA
implementation), then optionally upgrade to model B for chips where
single-stepping is part of the demo (Z80, 8080).
## Bus-cycle implementation
For separate-bus CPUs (8080, Z80) one machine read cycle looks like:
```
T1: drive A0..A15 with addr; assert MREQ; assert RD; release D0..D7
T2: (wait one tick for memory to respond)
T3: sample D0..D7 → byte = vx_pin_read_byte(D0..D7)
T4: deassert RD, MREQ
```
In the timer-driven model we expose this as a small state machine
inside `step_one_instruction()`. We do **not** need the runtime to
know anything about it — to the runtime we are just toggling pins.
For multiplexed-bus CPUs (4004, 8086) the chip drives a richer phase
sequence:
```
8086 T1: drive AD0..AD15 with low addr, A16..A19 with high addr,
assert ALE high then low (latches address into external 8282)
8086 T2: switch AD0..AD15 to data direction (input for read,
output for write), assert RD or WR
8086 T3: sample/drive data
8086 T4: deassert RD/WR
```
The chip does not need to *implement* the 8282 latch — it just needs to
drive `ALE` faithfully. A separate "address latch" custom chip on the
canvas (or a built-in primitive — see open question Q3) consumes
`ALE`+`AD0..15` and produces stable demultiplexed `A0..15`.
## Memory and I/O on the canvas
Each CPU needs at minimum a ROM holding the program. Three options,
in increasing order of effort:
1. **Bake the ROM into the CPU chip itself.** Simplest, allows zero
external wiring for a "hello world" demo. Bad for the long-term
pedagogical goal (the user can't see the bus working).
2. **Author a separate `rom-32k.c` custom chip** with 16 address pins,
8 data pins, `OE̅`, `CS̅`. Reads the ROM image from a chip-config
blob. Reusable across CPUs.
3. **Author a separate `ram-64k.c` custom chip** with the same pinout
plus `WE̅`. Even more reusable.
The right call is to do (2) and (3) once, in a sibling `test_buses/`
folder, and reuse them across all five CPUs. Tracked as Q1 in
`05_open_questions.md`.
## Decoder representation
For the 8-bit CPUs, the conventional approach is a 256-entry
`dispatch[]` table whose entries are small functions or compact
microcode steps. Andre Weissflog's `chips/z80.h` uses a flat
table-driven decoder generated by an offline script — porting that
script into our build pipeline is feasible but non-trivial.
**Pragmatic call:** for the first cut, use a giant `switch (opcode)`
in C. WASI-SDK's clang compiles big switches into reasonable jump
tables. Premature optimization is the enemy here.
## Timing and the simulated clock
`vx_sim_now_nanos()` is monotonic *in simulated time*. The host
advances simulated time as fast as it can, throttled by frame rendering.
This means:
- A 4 MHz Z80 emulator running 1 instruction per 1 µs of simulated
time will *appear* to run at whatever speed the host can sustain in
wall time — could be slower than 4 MHz, could be faster.
- Don't use `vx_sim_now_nanos` for "realistic" wall-clock pacing.
Use it for ordering events.
- For pedagogy, expose a "clock rate" config knob in the chip JSON so
users can set their Z80 to 1 Hz and watch instructions execute one
at a time.
## Determinism
All the CPUs in scope are deterministic given the same inputs (no
async errata of consequence). Two important invariants:
1. Avoid host-side wall-clock dependence. Only `vx_sim_now_nanos` is
safe; never call out to anything that observes real time.
2. Avoid floating-point in the CPU core unless we explicitly want it
(none of these chips have hardware FP). Stick to integer `uint8_t`
/ `uint16_t` / `uint32_t`.
## Suggested implementation order
1. **8080 first.** Cleanest bus, smallest ISA per pin, MIT-licensed
reference emulator (`superzazu/8080`) available for porting.
2. **Z80 second.** Same bus shape as 8080 + extensions; reuse what we
learned. Reference emulator: `floooh/chips/z80.h`.
3. **4004 third.** Different in shape (4-bit, multiplexed) — proves
the multiplexed-bus pattern that 8086 will need.
4. **4040 fourth.** Increment on 4004.
5. **8086 last.** Most complex; benefits from everything learned above.
This ordering also matches what reference material we already trust.