Simulation Guide¶
This guide explains how to use ChipFlow’s simulation system to test your designs before committing to silicon.
Overview¶
ChipFlow uses CXXRTL (C++ RTL simulation) to create fast, compiled simulations of your designs. The simulation system:
Converts your Amaranth design to CXXRTL C++ code
Automatically instantiates C++ models for your peripherals (UART, SPI flash, GPIO)
Compiles everything into a standalone executable
Runs your firmware on the simulated SoC
This allows cycle-accurate testing with real firmware, interactive debugging, and automated integration testing.
Basic Workflow¶
The typical simulation workflow:
# Lock pins (required before simulation)
pdm run chipflow pin lock
# Build the simulation
pdm run chipflow sim build
# Run the simulation
pdm run chipflow sim run
# Run simulation and check against reference
pdm run chipflow sim check
What Happens During Simulation¶
Design Elaboration
ChipFlow elaborates your design and extracts:
Top-level I/O signatures (UART, GPIO, SPI, etc.)
Pin assignments from
pins.lockSoftware binaries to load (from
attach_data())Peripheral metadata (from
SoftwareDriverSignature)
CXXRTL Code Generation
Amaranth converts your design to C++ using CXXRTL:
design.py → Fragment → RTLIL → CXXRTL C++ → sim_soc.cc
Model Instantiation
For each interface with a
SimInterfaceannotation, ChipFlow:Looks up the corresponding C++ model (uart_model, spiflash_model, etc.)
Generates code to instantiate and wire it up
Configures the model based on signature parameters
Main.cc Generation
ChipFlow generates
main.ccthat:Instantiates your design (
p_sim__top)Instantiates peripheral models
Sets up the CXXRTL debugger agent
Loads software binaries into flash models
Runs the clock for the configured number of steps
Compilation
Everything is compiled together using Zig as the C++ compiler:
zig c++ -O3 -g -std=c++17 \\ sim_soc.cc main.cc models.cc \\ -o sim_soc
Execution
The resulting
sim_socexecutable runs your design.
SimPlatform Internals¶
The SimPlatform class is responsible for managing the simulation build process.
Automatic Model Matching¶
ChipFlow includes built-in models for common peripherals:
# From chipflow_lib/platform/sim.py
_COMMON_BUILDER = BasicCxxBuilder(
models=[
SimModel('spi', 'chipflow::models', SPISignature),
SimModel('spiflash', 'chipflow::models', QSPIFlashSignature, [SimModelCapability.LOAD_DATA]),
SimModel('uart', 'chipflow::models', UARTSignature),
SimModel('i2c', 'chipflow::models', I2CSignature),
SimModel('gpio', 'chipflow::models', GPIOSignature),
],
...
)
When you use UARTSignature() in your design, SimPlatform automatically:
Extracts the
SimInterfaceannotation with UID"com.chipflow.chipflow_lib.UARTSignature"Looks up the model in
_COMMON_BUILDER._tableGenerates:
chipflow::models::uart uart_0("uart_0", top.p_uart__0____tx____o, top.p_uart__0____rx____i)
Port Instantiation¶
SimPlatform creates SimulationPort objects for each pin in your design:
# Inside SimPlatform.instantiate_ports()
for name, port_desc in interface_desc.items():
self._ports[port_desc.port_name] = io.SimulationPort(
port_desc.direction,
port_desc.width,
invert=port_desc.invert,
name=port_desc.port_name
)
These ports become the top-level I/O of your simulated design.
Clock and Reset Handling¶
Clocks and resets receive special treatment:
Clocks: Connected to Amaranth
ClockDomainResets: Synchronized with
FFSynchronizerfor proper reset behavior
# Clock domain creation
setattr(m.domains, domain, ClockDomain(name=domain))
clk_buffer = io.Buffer(clock.direction, self._ports[clock.port_name])
m.d.comb += ClockSignal().eq(clk_buffer.i)
# Reset synchronization
rst_buffer = io.Buffer(reset.direction, self._ports[reset.port_name])
ffsync = FFSynchronizer(rst_buffer.i, ResetSignal())
Generated main.cc¶
The generated main.cc follows this structure:
#include <cxxrtl/cxxrtl.h>
#include <cxxrtl/cxxrtl_server.h>
#include "sim_soc.h"
#include "models.h"
int main(int argc, char **argv) {
// Instantiate design
p_sim__top top;
// Instantiate peripheral models
chipflow::models::spiflash flash("flash", top.p_flash____clk____o, ...);
chipflow::models::uart uart_0("uart_0", top.p_uart__0____tx____o, ...);
chipflow::models::gpio gpio_0("gpio_0", top.p_gpio__0____gpio____o, ...);
// Set up debugger
cxxrtl::agent agent(cxxrtl::spool("spool.bin"), top);
if (getenv("DEBUG"))
std::cerr << "Waiting for debugger on " << agent.start_debugging() << std::endl;
// Set up event logging
open_event_log("events.json");
// Clock tick function
auto tick = [&]() {
flash.step(timestamp);
uart_0.step(timestamp);
gpio_0.step(timestamp);
top.p_clk.set(false);
agent.step();
agent.advance(1_us);
++timestamp;
top.p_clk.set(true);
agent.step();
agent.advance(1_us);
++timestamp;
};
// Load software
flash.load_data("../software/software.bin", 0x00100000U);
// Reset sequence
top.p_rst.set(true);
tick();
top.p_rst.set(false);
// Run simulation
for (int i = 0; i < num_steps; i++)
tick();
close_event_log();
return 0;
}
Configuration¶
chipflow.toml Settings¶
[chipflow.simulation]
# Number of clock cycles to simulate (default: 3000000)
num_steps = 3000000
[chipflow.test]
# Reference event log for integration testing
event_reference = "design/tests/events_reference.json"
Simulation Commands¶
chipflow sim build¶
Builds the simulation executable:
Elaborates the design
Generates CXXRTL C++
Generates main.cc
Compiles to
build/sim/sim_soc
chipflow sim run¶
Runs the simulation:
Builds software (if needed)
Builds simulation (if needed)
Executes
build/sim/sim_soc
Output appears in the terminal, and events.json is written to build/sim/.
chipflow sim check¶
Runs simulation and validates output:
Runs
chipflow sim runCompares
build/sim/events.jsonagainst referenceReports pass/fail
Useful for regression testing in CI/CD.
Debugging with RTL Debugger¶
ChipFlow simulations integrate with the RTL Debugger VS Code extension.
Enable Debugging¶
DEBUG=1 pdm run chipflow sim run
This starts the CXXRTL debug server and prints:
Waiting for debugger on localhost:37268
Event Logging for Testing¶
Peripheral models can log events to events.json for automated testing.
Logging Events¶
UART model automatically logs received characters:
[
{"type": "uart_rx", "data": "H", "timestamp": 1234},
{"type": "uart_rx", "data": "e", "timestamp": 1256},
{"type": "uart_rx", "data": "l", "timestamp": 1278},
{"type": "uart_rx", "data": "l", "timestamp": 1300},
{"type": "uart_rx", "data": "o", "timestamp": 1322}
]
Creating Reference¶
Run simulation and capture good output:
pdm run chipflow sim run cp build/sim/events.json design/tests/events_reference.json
Configure in
chipflow.toml:[chipflow.test] event_reference = "design/tests/events_reference.json"
Use in testing:
pdm run chipflow sim check
Input Commands (Optional)¶
You can provide input commands via design/tests/input.json. To reduce test churn from timing changes, input files use output events as triggers rather than timestamps:
{
"commands": [
{"type": "action", "peripheral": "uart_0", "event": "tx", "payload": 72},
{"type": "wait", "peripheral": "uart_0", "event": "tx", "payload": 62},
{"type": "action", "peripheral": "uart_0", "event": "tx", "payload": 10}
]
}
Commands are processed sequentially:
actioncommands queue an action (like transmitting data) for a peripheralwaitcommands pause execution until the specified event occurs
See the mcu_soc example for a working input.json file.
Customizing Simulation¶
Adding Custom Models¶
ChipFlow’s built-in simulation models cover common peripherals (UART, SPI, I2C, GPIO, QSPI Flash). For custom peripherals, you’ll need to write C++ models that interact with the CXXRTL-generated design.
Warning
The custom simulation model interface is subject to change. Model APIs may be updated in future ChipFlow releases. Built-in models (UART, SPI, etc.) are stable, but custom model registration and integration mechanisms may evolve.
Learning Resources:
Study existing models: The best way to learn is to examine ChipFlow’s built-in implementations:
chipflow_lib/common/sim/models.h- Model interfaces and helper functionschipflow_lib/common/sim/models.cc- Complete implementations for:uart- UART transceiver with baud rate controlspiflash- QSPI flash memory with command processingspi- Generic SPI peripherali2c- I2C bus controller with start/stop detection
CXXRTL Runtime API: Models interact with the generated design using CXXRTL’s API:
CXXRTL Documentation - Command reference
CXXRTL runtime source:
yosys/backends/cxxrtl/runtime/(in Yosys repository)Key types:
cxxrtl::value<WIDTH>for signal access,.get()to read,.set()to write
Model Registration:
Once you’ve written a model (e.g., design/sim/my_model.h), register it with ChipFlow:
from chipflow_lib.platform import SimPlatform, SimModel, BasicCxxBuilder
from pathlib import Path
MY_BUILDER = BasicCxxBuilder(
models=[
SimModel('my_peripheral', 'my_design', MyPeripheralSignature),
],
hpp_files=[Path('design/sim/my_model.h')],
)
class MySimStep(SimStep):
def __init__(self, config):
super().__init__(config)
self.platform._builders.append(MY_BUILDER)
Then reference your custom step in chipflow.toml:
[chipflow.steps]
sim = "my_design.steps.sim:MySimStep"
Note
Comprehensive CXXRTL runtime documentation is planned for a future release. For now, refer to existing model implementations and the Yosys CXXRTL source code.
Performance Tips¶
Reduce sim cycles: Lower
num_stepsduring development[chipflow.simulation] num_steps = 100000 # Instead of 3000000
Use Release builds: Already enabled by default (
-O3)Disable debug server: Don’t set
DEBUG=1unless actively debuggingProfile your design: Use the RTL Debugger to find bottlenecks in your HDL
Common Issues¶
Incomplete Simulation Output¶
Symptom: Simulation completes but expected operations are incomplete
Note: The simulation will always stop after num_steps clock cycles, regardless of what the design or software is doing. If your firmware hasn’t completed by then, you’ll see incomplete output.
Causes:
- num_steps too low for the operations being performed
- Firmware stuck in infinite loop
- Waiting for peripheral that never responds
Solutions:
- Increase num_steps in chipflow.toml if legitimate operations need more time
- Enable DEBUG=1 and attach debugger to see where execution is stuck
- Add timeout checks in your firmware to detect hangs
- Use event logging to see how far the simulation progressed
No UART Output¶
Symptom: Expected UART output doesn’t appear
Causes: - UART baud rate misconfigured - UART peripheral not initialized - Software not running
Solutions:
- Check init_divisor matches clock frequency
- Verify UART initialization in firmware
- Check that flash model loaded software correctly
Model Not Found¶
Symptom: Unable to find a simulation model for 'com.chipflow.chipflow_lib.XXX'
Causes: - Using a signature without a corresponding model - Custom signature not registered in a builder
Solutions:
- Use built-in signatures (UART, GPIO, SPI, I2C, QSPIFlash)
- Or create a custom model and register it with a BasicCxxBuilder
Example: Complete Simulation Setup¶
Here’s a complete example showing simulation setup for a simple SoC:
Design (design/design.py)¶
from amaranth import Module
from amaranth.lib.wiring import Component, Out, connect, flipped
from amaranth_soc import csr
from chipflow_digital_ip.io import UARTPeripheral, GPIOPeripheral
from chipflow_digital_ip.memory import QSPIFlash
from chipflow_lib.platforms import (
UARTSignature, GPIOSignature, QSPIFlashSignature,
attach_data, SoftwareBuild
)
class MySoC(Component):
def __init__(self):
super().__init__({
"flash": Out(QSPIFlashSignature()),
"uart": Out(UARTSignature()),
"gpio": Out(GPIOSignature(pin_count=4)),
})
self.bios_offset = 0x100000
def elaborate(self, platform):
m = Module()
# CSR decoder
csr_decoder = csr.Decoder(addr_width=28, data_width=8)
m.submodules.csr_decoder = csr_decoder
# Flash
m.submodules.flash = flash = QSPIFlash()
csr_decoder.add(flash.csr_bus, name="flash", addr=0x00000000)
connect(m, flipped(self.flash), flash.pins)
# UART
m.submodules.uart = uart = UARTPeripheral(init_divisor=217)
csr_decoder.add(uart.bus, name="uart", addr=0x02000000)
connect(m, flipped(self.uart), uart.pins)
# GPIO
m.submodules.gpio = gpio = GPIOPeripheral(pin_count=4)
csr_decoder.add(gpio.bus, name="gpio", addr=0x01000000)
connect(m, flipped(self.gpio), gpio.pins)
# Attach software
from pathlib import Path
sw = SoftwareBuild(
sources=Path('design/software').glob('*.c'),
offset=self.bios_offset
)
attach_data(self.flash, flash, sw)
return m
Configuration (chipflow.toml)¶
[chipflow]
project_name = "my_soc"
clock_domains = ["sync"]
[chipflow.top]
soc = "design.design:MySoC"
[chipflow.silicon]
process = "sky130"
package = "pga144"
[chipflow.simulation]
num_steps = 1000000
[chipflow.test]
event_reference = "design/tests/events_reference.json"
Firmware (design/software/main.c)¶
#include "soc.h"
int main() {
// UART is auto-initialized by attach_data
// Print test message
puts("Hello from ChipFlow simulation!");
// Blink GPIO
for (int i = 0; i < 10; i++) {
UART->gpio_data = i & 0xF;
}
return 0;
}
Running¶
# Lock pins
pdm run chipflow pin lock
# Run simulation
pdm run chipflow sim run
Expected output:
Building simulation...
Building software...
🐱: nyaa~!
Hello from ChipFlow simulation!
See Also¶
ChipFlow Architecture Overview - Overall ChipFlow architecture
Using Pin Signatures and Software Drivers - Pin signature usage guide
The chipflow command - CLI command reference
RTL Debugger - Interactive debugging