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

6.3 KiB

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

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.