This guide shows how to approach a new emulator project with proto-gen and where to look in the repository for working patterns.
Most projects follow this order:
- Define clocks.
- Define memory regions.
- Define one or more CPU cores.
- Define peripheral chips and their register blocks.
- Define buses, controllers, banking, signals, and ports.
- Assemble the board.
- Generate C.
- Add a handwritten
main()or generate an SDL host.
Begin by modeling the machine's instruction set with CPUDefinition.
Typical tasks:
- add general-purpose registers,
- define register pairs if the ISA has them,
- define the flag register layout,
- add a few opcodes first,
- use raw C for unusual instructions if needed.
Good first milestone:
- enough opcodes to load immediates,
- move values between registers,
- read and write memory,
- jump,
- halt.
That is exactly how the smaller examples are structured.
Define storage with MemoryRegion.
Common patterns:
- static RAM or VRAM with a fixed size,
- dynamic cartridge ROM with
size_in_bytes=0, - banked windows using
MemoryBank, - mappers using
MemoryController.
Then route everything through a MemoryBus.
Ask yourself:
- which ranges are fixed,
- which are banked,
- which are register-mapped,
- which writes should be intercepted,
- what fallback value should unmapped reads return.
Peripheral chips usually start with:
- state fields,
- one or more
RegisterBlockinstances, - an optional tick handler.
Examples:
- timer registers and overflow logic,
- PPU control/status and framebuffer memory,
- joypad state and read multiplexing,
- APU channel registers and sample generation.
Use:
SignalLinefor interrupts and events,Portfor mailbox-style communication,- shared bus mappings for memory-visible peripherals.
This is where the board becomes a system instead of a CPU core with RAM.
Do not wait until the entire system is modeled before generating code.
A productive rhythm is:
- add a small slice,
- generate C,
- inspect the output,
- compile,
- run a focused test ROM or micro-program.
That is how the repository examples are structured.
File:
What it teaches:
- minimal CPU definition,
- simple memory map,
- register block used as an output device,
- generated C plus a handwritten
main().
Use it when:
- you want the smallest complete end-to-end reference.
File:
What it teaches:
- banked ROM,
- memory controller state and write handlers,
- bank resolvers,
- timer peripheral with tick handler,
- interrupt-style signaling,
- a more realistic single-CPU console architecture.
Use it when:
- you are building a cartridge-based 8-bit machine.
File:
What it teaches:
- multiple CPUs,
- separate buses,
- derived clocks,
- latched ports between chips,
- catch-up scheduling across chips.
Use it when:
- your machine has a main CPU plus co-processor or sound CPU.
File:
What it teaches:
- bus
access_cycles, internal_op()timing,- synchronized peripheral ticking during execution,
- DMA-aware scheduling hooks.
Use it when:
- timing behavior matters more than simple per-opcode cycle totals.
Files:
What they teach:
- large-scale board composition,
- heavy use of transpiled register handlers,
- cartridge mapper logic,
- PPU/APU/timer/joypad chips,
- SDL host generation and ROM loading.
Use them when:
- you want the most complete reference for real project structure.
If you are starting a new machine, this order usually works well:
- Bootable CPU plus RAM and ROM.
- Enough opcodes to run a micro-test program.
- Memory-mapped I/O skeleton.
- Interrupt wiring.
- Banking or DMA if the target machine needs it.
- Video or audio peripherals.
- Host integration.
Keep each milestone independently runnable if possible.
Prefer transpiled Python when:
- the logic is straight-line and register-centric,
- you want readability at the system-model level,
- you want opcode families or shared helper patterns.
Prefer raw C when:
- the transpiler subset gets in your way,
- you need precise control over emitted code,
- you are copying known-good logic from an existing emulator,
- the handler is awkward to express in the supported subset.
Most successful uses of this framework will likely mix both styles.
For a board-only project:
python examples/fibonacci.py
gcc -O2 -o fibonacci examples/fibonacci.c
./fibonacciFor test-driven development:
pytestFor a host-enabled project:
- generate the board C or SDL host C,
- compile with your SDL3 toolchain,
- run against a test ROM,
- inspect both generated C and runtime behavior.
The generated file is long but regular. A good reading order is:
- board and chip structs,
- bus read/write functions,
- register block dispatchers,
- controller resolvers,
- CPU step function,
- board init and board step.
That mirrors the code generator organization in src/proto/codegen.py.
- Forgetting to assign a bus to a CPU chip with
chip.set_bus(...). - Forgetting to add a master clock to the board.
- Using Python features outside the transpiler subset.
- Expecting dynamic Python behavior at runtime instead of generated C behavior.
- Defining handler logic that would be clearer as raw C.
- Assuming all data-model features are already wired into code generation without checking examples or tests.
For modeling patterns:
For exact behavior:
For generator implementation details: