A framework for implementing custom ARM hardware devices in QEMU, with register logic and interrupt firing modelled in external processes. A generic QEMU SysBus device (mmio-sockdev) proxies MMIO reads/writes, IRQ lines, reset requests, and fabric bus-master transactions to/from Python or SystemVerilog/Verilator device models over TCP. Timed Python callbacks use the separate shared device_event service.
This repository is a chip-function validation environment, not a full cross-domain cycle-accurate simulator. QEMU provides a CPU/software behavioural model for firmware execution. Python devices provide fast functional peripheral models. SystemVerilog devices keep their own local RTL clock and are connected to QEMU through transaction boundaries such as MMIO reads/writes and IRQs.
For QEMU and Python timed devices, time is chip virtual time, not wall-clock time. QEMU is run with -icount shift=5 so QEMU_CLOCK_VIRTUAL = instruction_count × 32 ns — matching the KX6625 at 48 MHz / CPI≈2. A WDT set to 100 ms means 100 ms of simulated chip time, independent of how long the host machine takes to emulate it. SystemVerilog devices are intentionally modelled as independent clock domains; their local pclk/cycle count is not automatically back-annotated into QEMU CPU cycles.
This environment has two primary goals:
- Fast software prototyping: develop and debug firmware, drivers, RTOS integration, and application logic against a realistic SoC memory map and interrupt topology before hardware is available.
- Fast RTL device validation: connect selected SystemVerilog peripherals to firmware running on QEMU, exercise their register-level behaviour and IRQ paths, and compare them against Python reference models when useful.
The intended abstraction boundary is:
| Domain | Role | Time/clock model |
|---|---|---|
| QEMU CPU/SoC | Behavioural CPU, NVIC, memory map, firmware execution | QEMU virtual time with optional icount; MMIO callbacks are synchronous from the guest CPU's perspective |
| Python devices | Fast functional models and reference/checker models | Use shared device_event callbacks for deterministic QEMU virtual-time events |
| SystemVerilog devices | RTL device models with local state machines and registers | Independent local clock maintained by the SV host shell; no claim of cycle-accurate CPU/APB alignment |
MMIO access to an SV device is a synchronous transaction boundary: QEMU blocks while the bridge completes the APB/RTL operation, then the guest continues. The elapsed host time and the number of SV pclk cycles consumed by that transaction do not automatically advance QEMU guest cycles. This makes the environment well suited for software bring-up and device functional validation, while keeping the boundary honest about what is not modelled: CPU bus wait-state timing and exact 48 MHz : 16 MHz cross-domain cycle alignment.
This project implements:
- Custom QEMU Device (
mmio-sockdev): Generic SysBus proxy — 4 KB MMIO, IRQ line, optional fabric bus-master channel, optional system-reset channel (rst-chardev). One instance per device on the QEMU command line. - Python Device Domain (
device_model/soc_top.py,device_model/soc_config.py,device_model/soc_assembly.py):spec/devices.yamlandspec/soc.yamldescribe device instances, capabilities, dependencies, event IDs, and bus masters.PythonDeviceDomainloads that topology, then factory-based assembly wires transport servers, thePeripheralBus,BusMasterAddressSpace, reset manager,device_event, and abstractMMIODeviceinstances.SoCTopremains as a compatibility alias.kx6625_default()returns the canonical KX6625 device-domain map. - Transport Boundary (
device_model/mmio_device_server.py): TCP servers formmio-sockdevchannels — R/W, IRQ, device-event timing, fabric bus-master access, and reset. Each peripheral model is aMMIODevicesubclass withread(),write(), and optionalon_event(). - Native SYSCTRL block: QEMU-native system controller at
0x4000A000for CPU identity, CPU1 reset release, boot status, device clock/reset policy state, and SYSCTRL-mediated indirect device-register access. - Ten socket-backed peripherals: Console UART, multi-channel DMA controller, countdown timer, DMA client demo peripheral, CRC-32 hardware accelerator, Watchdog Timer (WDT), SystemVerilog APB timer/DMA subsystem, an HSM crypto accelerator with AES-128/CMAC support, an OTP controller with 1-to-0 programming and ECC, and an RGB565 display controller with optional two-layer composition. See
spec/README.mdfor register maps. - SystemVerilog Device Prototype (
sv_device/): A Verilator-built APB peripheral subsystem listening on TCP ports 7906/7907/7912. QEMU drives it through a normalmmio-sockdevinstance at0x4000B000; the SV subsystem contains an APB timer at offset0x000and a first-version SV DMA at offset0x100, with completion IRQs returned through NVIC IRQ5. - KX6625 Custom SoC (
scripts/qemu-fork/hw/arm/kx6625.c): Dual Cortex-M4 @ 48 MHz, 512 KB FLASH @0x00000000, 128 KB SRAM @0x20000000, NVIC with 16 external IRQs per ARMv7-M container. - FreeRTOS Cortex-M4F Firmware: CPU0 boots FreeRTOS using the official GCC
ARM_CM4Fport and Cortex-M SysTick; CPU1 runs a lightweight bare-metal IPC loop. The demo task exercises UART, DMA M2M, DMA peripheral DREQ/DACK, CRC-32, dual-core IPC, SV timer IRQ, SV DMA M2M copy, OTP programming/read protection, HSM AES-CBC/CMAC including OTP-backedKEY_ID, SYSCTRL, and WDT countdown-reset warm-boot detection. - End-to-End Smoke Test (
scripts/e2e_test.sh): Starts Python server and the SV host shell, boots QEMU withicount shift=5, exercises all devices including SV timer/DMA, OTP/HSM direct key wiring, HSM crypto, SYSCTRL indirect register access, and a WDT-triggered system reset, asserts firmware output, and generates an HTML trace report. - Event Tracer (
device_model/tracer.py): Non-blocking JSONL event trace for every device — records virtual time, wall time, and device-specific fields. Written by a background thread so device models never block on I/O. Visualised as a self-contained HTML report (build/trace_report.html) byscripts/visualize_trace.py.
flowchart TB
subgraph QEMU["QEMU Native Domain"]
direction TB
FW["Firmware / FreeRTOS"]
CPU["Dual Cortex-M4\nNVIC + SysTick"]
MEM["FLASH / SRAM\nQEMU physical memory"]
NATIVE["Native QEMU blocks\nSYSCTRL 0x4000A000 / CRU 0x4000F000"]
SOCK["mmio-sockdev instances\nMMIO proxy + IRQ + fabric + reset"]
DEVT["device-event\nQEMU_CLOCK_VIRTUAL timers"]
FW <--> CPU
CPU <--> MEM
CPU <--> NATIVE
CPU <--> SOCK
NATIVE -->|"clock/reset guard"| SOCK
DEVT --> SOCK
SOCK --> CPU
end
subgraph PY["Python Device Domain"]
direction TB
PYDOM["PythonDeviceDomain\ntransport + topology wiring"]
PYSRV["Transport servers\nRW / IRQ / Event / Fabric / Reset"]
PYBUS["PeripheralBus\nMMIODevice dispatcher"]
PYDEV["MMIODevice instances\nabstract device models"]
PYADDR["BusMasterAddressSpace\nPython MMIO or QEMU fabric"]
PYDMA["Bus-master device models\nDMA / HSM / flash"]
PYRST["SystemResetManager"]
PYDOM --> PYSRV
PYDOM --> PYBUS
PYDOM --> PYADDR
PYBUS <--> PYDEV
PYDEV --> PYDMA
PYDMA <--> PYADDR
PYADDR <--> PYBUS
PYDEV --> PYRST
end
subgraph SV["SystemVerilog / RTL Domain"]
direction TB
SVBR["Verilator host shell\nsv_host_shell.cpp"]
SVTOP["sv_device_top.sv\nAPB ingress + decoder + RTL slaves"]
SVCLOCK["Local RTL clock"]
SVROUTER["sv_master_router.sv\ncurrent pass-through router"]
SVEGRESS["sv_fabric_egress_dpi.sv\nDPI fabric endpoint"]
SVDMA["SV bus-master RTL"]
SVBR <-->|"host_req / host_rsp"| SVTOP
SVCLOCK --> SVTOP
SVTOP <--> SVDMA
SVDMA <--> SVROUTER
SVROUTER <--> SVEGRESS
SVEGRESS --> SVBR
end
SOCK <-->|"MMIO R/W TCP"| PYSRV
SOCK <-->|"MMIO/APB TCP"| SVBR
PYSRV -->|"IRQ TCP"| SOCK
SVBR -->|"IRQ TCP"| SOCK
SOCK <-->|"device_event virtual-time events"| PYSRV
PYADDR <-->|"fabric-chardev DMA"| SOCK
SVBR <-->|"fabric-chardev"| SOCK
PYRST -->|"rst-chardev"| SOCK
The standalone source for this diagram is kept in doc/architecture_diagram.md.
The normal Python timing path is the shared device_event service. Python devices schedule virtual-time callbacks, QEMU owns the QEMU_CLOCK_VIRTUAL timers, and QEMU fires events back to Python when each deadline arrives:
Python -> QEMU: 'S' | device_id | event_id | delay_ns
QEMU -> Python: 'F' | device_id | event_id | vtime_ns
Display scanout, DMA completion, Timer expiry, WDT timeout, and the Python fabric smoke master use device_event.
- QEMU virtual time remains authoritative, so events stop when QEMU is paused.
- Devices can re-arm themselves from an event handler without requiring a new MMIO write.
- The shared 1 ms broadcast tick is no longer part of the default QEMU command line.
QEMU_CLOCK_VIRTUAL
│ 'F'|device_id|event_id|vtime_ns (TCP :7925)
▼
DeviceEventServer
├── DisplayController.on_event() → scan one line / frame done IRQ
├── DmaController.on_event() → complete transfer / IRQ
├── TimerDevice.on_event() → expire / periodic re-arm / IRQ
├── WdtDevice.on_event() → timeout / reset request
└── PythonFabricMasterDemo.on_event() → fabric smoke probes
QEMU is started with -icount shift=5,sleep=off,align=off:
QEMU_CLOCK_VIRTUAL = executed_instruction_count × 32 ns
This matches the KX6625 at 48 MHz with CPI ≈ 2 (1 instruction ≈ 41.6 ns real; 32 ns virtual gives ~23% speedup, well within functional accuracy bounds).
Key properties:
| Property | Behaviour |
|---|---|
| WDT 100 ms timeout | Fires after firmware executes ~3.1 M instructions — chip time, not wall time |
WFI instruction |
CPU halts; QEMU jumps vtime directly to the next timer_mod deadline — zero host wait |
| MMIO R/W TCP latency | Host TCP round-trip (~50 µs real) counts as 0 ns virtual time — bus accesses are instantaneous from the chip's perspective |
| DMA 512 ns latency | device_event fires after exactly 16 virtual instructions — QEMU wakes from WFI immediately |
| Debug pause (gdb) | QEMU_CLOCK_VIRTUAL freezes — no spurious event fires |
Running with icount (recommended):
ICOUNT_SHIFT=5 bash scripts/e2e_test.sh # tests + HTML trace report
ICOUNT_SHIFT=5 bash scripts/run_interactive.shWithout ICOUNT_SHIFT, QEMU falls back to realtime wall-clock mode (functional but non-deterministic).
KX6625 is configured as a dual Cortex-M4 SoC. Both cores are instantiated by QEMU as ARMv7-M containers using the generated KX6625_CPU_TYPE_STR from spec/soc.yaml.
The firmware uses an asymmetric boot model:
- CPU0 runs the KX6625 startup code, initialises
.data/.bss, creates theapp_task, and starts the FreeRTOS scheduler withvTaskStartScheduler(). - CPU1 is held in reset until CPU0 writes
SYSCTRL.CPU1RST; it then switches to its own stack and runscpu1_main(), a bare-metal shared-SRAM IPC polling loop. - This is not SMP FreeRTOS. FreeRTOS scheduling is active on CPU0 only.
The FreeRTOS port is the official GCC Cortex-M4F port:
freertos/FreeRTOS-Kernel/portable/GCC/ARM_CM4F/port.c
The scheduler tick uses the Cortex-M architecture SysTick exception, not the KX6625 timer0 peripheral. timer0 remains an external IRQ2 device available to firmware and tests. The vector table in firmware/start.S routes the core scheduling exceptions directly to the FreeRTOS port:
SVCall -> vPortSVCHandler
PendSV -> xPortPendSVHandler
SysTick -> xPortSysTickHandler
Timed devices use the shared device_event protocol to schedule callbacks with nanosecond precision in QEMU virtual time:
Firmware writes register (e.g. DMA_CTRL.START):
QEMU → Python: 'W' | offset(4B) | size(1B) | data(sizeB)
Python computes transfer latency (e.g. 512 ns)
Python → QEMU device_event: 'S' | device_id | event_id | delay_ns
Firmware executes WFI
icount: vtime jumps to deadline
QEMU → Python device_event: 'F' | device_id | event_id | vtime_ns
Python on_event() executes DMA transfer, asserts IRQ
Firmware wakes from WFI, runs ISR
The R/W channel still returns an 8-byte reserved write response for wire compatibility, but QEMU no longer interprets it as a timing request.
MMIODevice.irq_controller.set_irq(idx, level)
│ 'I'(1B) | idx(1B) | level(1B) (TCP irq-port)
▼
mmio-sockdev (QEMU) ──► NVIC (IRQ line) ──► Cortex-M4
NVIC pulse pattern: assert then immediately deassert so the NVIC edge-trigger does not re-fire.
DMA-capable models act as bus masters: they read and write platform addresses
without involving the firmware CPU. This is modelled through the shared
fabric-chardev transaction channel:
FabricChannel.read(master_id, address, length)
│ 'F'|'R'|master_id|flags|address|length → QEMU fabric
│ status|data(length) ← QEMU fabric
▼
FabricChannel.write(master_id, address, data)
│ 'F'|'W'|master_id|flags|address|length|data → QEMU fabric
│ status ← QEMU fabric
The same fabric frame is used by Python masters, SV masters, and native QEMU masters. Status codes let device models distinguish a successful bus write from simulated bus faults, protection failures, or unmapped-address responses without changing the packet framing again.
sv_device/sv_device_top.sv is the Verilated top for the SV island region at 0x4000B000. It composes APB ingress, an APB decoder, APB timer, split DMA register/core blocks, GPIO, and an outbound fabric endpoint:
| Module | Responsibility |
|---|---|
sv_apb_decoder |
Decode QEMU-visible APB register windows and mux APB responses |
sv_apb_ingress |
Convert host shell requests into APB setup/access cycles |
sv_timer_apb |
APB timer smoke test and IRQ generation at offset 0x000-0x0ff |
sv_dma_regs |
DMA APB register file, CH0/CH1 start/clear pulses, and status presentation |
sv_dma_core |
CH0 M2M transfer state machine using a generic master request/response interface |
sv_dma_core |
Parameterized DMA channel engine for legacy M2M and request-driven M2P fabric transfers |
sv_gpio_apb |
GPIO APB prototype with output/input simulation and change IRQ |
sv_master_router |
Route SV master transactions; first version forwards all requests to the external fabric endpoint, later can target local APB/FIFO windows |
sv_spi_tx_apb |
SPI transmit-only master with CPU-fed TX FIFO and simulation frame logs |
sv_fabric_egress_dpi |
Keep SV master response timing in SV and call C++ DPI helpers at the fabric boundary |
The public APB register map remains:
| Offset window | Device | Purpose |
|---|---|---|
0x000-0x0ff |
sv_timer_apb |
APB timer smoke test and IRQ generation |
0x100-0x1ff |
sv_dma_apb |
First-version 32-bit aligned M2M DMA prototype |
0x200-0x2ff |
sv_gpio_apb |
GPIO output/input simulation and change IRQ prototype |
0x300-0x3ff |
sv_spi_tx_apb |
SPI TX master CPU FIFO path and frame logging |
The SV DMA keeps its own local RTL clock inside sv_host_shell.cpp. Firmware configures the DMA through MMIO/APB registers; sv_apb_ingress.sv performs the APB setup/access cycles inside SV, and the host shell returns the MMIO response once SV produces host_rsp. When the DMA needs memory access, sv_dma_core.sv sends a generic master transaction through sv_master_router.sv into sv_fabric_egress_dpi.sv, which calls the host shell's timing-independent DPI helpers to emit fabric-chardev reads/writes into QEMU fabric. Completion is signalled by the shared SV IRQ line through NVIC IRQ5.
DmaController (dma_controller.py) is the single MMIO-mapped DMA IP. It supports two independent channels:
| Channel | Mode | How triggered | Completion |
|---|---|---|---|
| CH0 | Memory-to-memory (M2M) or M2P | Firmware writes CH0_CTRL.START |
Pulses DMA IRQ (NVIC) |
| CH1 | Peripheral DREQ/DACK (M2M/P2M/M2P) | DmaClientHandle.transfer() call |
Calls peripheral's on_complete callback |
Firmware path (CH0) — device-event scheduling:
write(CH0_CTRL.START)
→ _firmware_start() → schedules delay_ns (e.g. 512)
→ QEMU device_event timer arms at vtime + 512 ns
→ firmware executes WFI
→ icount: vtime jumps to deadline
→ on_event(vtime_now + 512 ns) → fabric copy → pulse IRQ 1
No background thread is needed. The virtual-time deadline is guaranteed by icount — the transfer completes at exactly arm_vtime + transfer_ns in chip time.
Peripheral path (CH1) — background thread (cross-device scheduling):
DmaClientHandle.transfer() → _peripheral_request() → _arm_channel()
→ background thread waits for DREQ acknowledgment
→ _tick_channel() after N ticks → fabric copy → on_complete() callback
→ DmaClientDemoDevice sets STATUS.DONE → pulses IRQ 3
device_model/mmio_base.py provides the shared building blocks used by every Python device model. These helpers eliminate per-device boilerplate and encode common hardware access patterns as reusable, testable units.
A TCP server that forwards the firmware UART byte stream to external terminal clients. It multiplexes over any number of simultaneously connected clients and handles disconnection transparently.
ConsoleUartDevice.write(TXDATA)
│ raw byte (LF → CRLF for terminal)
▼
UartChannel.send(data) — called in device R/W thread
├──► client socket 1 (e.g. nc 127.0.0.1 7904)
└──► client socket 2 (e.g. python3 scripts/uart_console.py)
UartChannel is transport-layer infrastructure (like IRQController, FabricChannel, RstController), not a device model. It lives in mmio_base.py and is wired by PythonDeviceDomain (via UartCfg.term_port). You can also wire it manually:
uart_channel = UartChannel(port=7904)
uart_channel.start() # starts daemon accept thread; non-blocking
uart_dev = ConsoleUartDevice(..., uart_channel=uart_channel)| Method | Description |
|---|---|
start() |
Bind port, start accept-loop daemon thread |
stop() |
Close server socket |
send(data: bytes) |
Broadcast to all connected clients; removes dead connections |
connected |
True if at least one client is connected |
The send() call is fire-and-forget with non-blocking sendall — a slow or disconnected client never blocks the device model thread.
Replaces the raw bytearray + threading.Lock + manual bounds-check pattern that every device would otherwise repeat.
self._regs = RegisterBank(
size,
initial=bytes(init_values), # optional reset snapshot
policies={ # optional per-register access policies
_STATUS: RegAccess.READ_ONLY,
_VALUE: RegAccess.READ_ONLY,
_INTCLR: RegAccess.WRITE_ONLY,
},
)Key methods:
| Method | Description |
|---|---|
read(offset, size) → bytes |
CPU-side read; applies access policy |
write(offset, size, data) |
CPU-side write; applies access policy |
get32(offset) → int |
32-bit LE read, bypasses policy (device-internal) |
set32(offset, value) |
32-bit LE write, bypasses policy |
set_bits(offset, mask) |
Atomic OR, bypasses policy |
clear_bits(offset, mask) |
Atomic AND-NOT, bypasses policy |
reset(initial=None) |
Restore to construction-time snapshot |
with self._regs: |
Acquire internal lock for atomic multi-register operations |
get32_nolock / set32_nolock |
No-lock variants for use inside the context manager |
self._regs[byte_offset] |
Direct byte access inside context manager |
RegAccess is an enum.Flag whose members describe how a register behaves when the CPU reads or writes it. Policies apply only to the external CPU path (read()/write()). Internal device helpers (get32, set_bits, __setitem__, etc.) always bypass policies so the device hardware can freely update its own state.
| Flag | CPU Read | CPU Write | Typical Use |
|---|---|---|---|
| (none) | returns stored value | stores value | normal R/W register |
WRITE_ONLY |
returns 0 | stores normally | pulse/strobe registers (INTCLR, KICK, SWRESET) |
READ_ONLY |
returns stored value | dropped silently | STATUS, VALUE, hardware-computed registers |
READ_CLEAR |
returns value then clears to 0 | stores normally | latching event / error registers |
W1C |
returns stored value | bits written 1 → cleared | IRQ status (ARM convention): firmware acks by writing bit mask |
W1S |
returns stored value | bits written 1 → set | set-only enable registers |
Flags can be combined with |.
# Example: standard ARM interrupt status register
policies={
_STATUS: RegAccess.W1C, # firmware clears individual IRQ bits
_INTCLR: RegAccess.WRITE_ONLY, # reads return 0
_VALUE: RegAccess.READ_ONLY, # hardware-computed; CPU writes dropped
}
# Example: self-clearing event latch
policies={
_EVENTS: RegAccess.READ_CLEAR, # read returns accumulated flags and zeroes the register
}Encapsulates an IRQController + line index and provides named operations matching Cortex-M NVIC semantics.
self._irq = IrqLine(irq_controller, idx=0)
self._irq.assert_() # level = 1 (stays high; use for level-triggered, e.g. timer)
self._irq.deassert() # level = 0
self._irq.pulse() # assert then immediately deassert (edge-trigger; NVIC won't re-fire)
self._irq.wait_connected(timeout) # block until QEMU IRQ channel connects
self._irq.idx # read-only: line indexpulse() is the correct primitive for most peripherals — the NVIC latches the rising edge as pending; the level must return low before the handler returns to prevent the NVIC re-pending the interrupt on exception return.
If irq_controller is None all methods are silent no-ops, so devices remain constructible without an IRQ channel.
Encapsulates the _start_vtime_ns / _last_vtime_ns countdown pattern shared by the Timer and WDT.
self._clock = VirtualClock()
# In on_event():
self._clock.update(vtime_ns) # record latest timestamp
if self._clock.is_expired(load_ms):
self._clock.disarm() # one-shot
# or:
self._clock.rearm_periodic(load_ms * 1_000_000) # periodic — advances start by one period (no drift)
# Arm (e.g. when CTRL.ENABLE written):
self._clock.arm() # from most-recent tick
self._clock.arm(vtime_ns) # from explicit timestamp
# Read remaining time:
remaining = self._clock.remaining_ms(load_ms)The clock is correct across QEMU debug pauses: virtual time stops, update() stops being called, is_expired() stays False.
device_model/tracer.py provides lightweight, non-blocking event tracing for every Python device model. Records are written in JSONL format (one JSON object per line) by a background thread, so device R/W and event handlers never block on file I/O.
from device_model.tracer import Tracer, NULL_DEVICE_TRACER
# In mmio_device_server.py main():
tracer = Tracer('build/device_trace.jsonl') # starts background writer thread
# Each device gets a bound handle:
self._tr = tracer.context(self.name) # DeviceTracer
# In on_event() — update virtual-time context (call first):
def on_event(self, event_id: int, vtime_ns: int) -> None:
self._tr.tick(vtime_ns) # updates _vtime_ns context
...
# Anywhere in the device — emit an event:
self._tr.emit('EXPIRE', load_ms=100) # always non-blocking
self._tr.emit('TX', ch=65, ascii='A')JSONL record format (flat, compact, deterministic field order):
{"seq":362,"t_wall_ns":1714400362000000000,"t_virt_ns":2007975928,"dev":"DmaController(2ch)","event":"CH_DONE","ch":0,"ok":true}
{"seq":671,"t_wall_ns":1714400362050000000,"t_virt_ns":0,"dev":"CRC-32","event":"RESULT","crc32":"0xcbf43926"}
{"seq":1219,"t_wall_ns":1714400362100000000,"t_virt_ns":2346054174,"dev":"wdt","event":"TIMEOUT","timeout_cnt":1}The first record in every trace file is a HEADER sentinel:
{"seq":0,"t_wall_ns":..."dev":"__tracer__","event":"HEADER","version":"1","pid":12345,"path":"build/device_trace.jsonl"}Events emitted per device:
| Device | Events |
|---|---|
ConsoleUart |
TX (ch, ascii), IRQ_FIRE (irq_idx), RESET |
TimerDevice |
ARM (load_ms), DISARM, EXPIRE (load_ms), IRQ_ASSERT (irq_idx), INTCLR (irq_idx), RESET |
WdtDevice |
LOAD (load_ms), ARM (load_ms), DISARM, KICK, IRQ_PULSE (irq_idx), TIMEOUT (timeout_cnt), RESET (reset_reason, timeout_cnt) |
DmaController |
CH_START (ch, src, dst, length, mode), CH_DREQ (ch, src, dst, length, mode), CH_DONE (ch, ok[, path]), IRQ_PULSE (ch, irq_idx), RESET |
DmaClientDemo |
START (src, dst, length), NACK, DONE (ok), IRQ_PULSE (irq_idx), RESET |
CRC-32 |
DATA_WRITE (length), RESULT (crc32), RESET |
CLI flags (passed to mmio_device_server.py):
| Flag | Default | Description |
|---|---|---|
--trace-file PATH |
build/device_trace.jsonl |
Output file path |
--no-trace |
off | Disable tracing entirely |
NULL_DEVICE_TRACER is a module-level singleton where every method is a no-op. When tracing is disabled all devices silently receive NULL_DEVICE_TRACER — no conditional checks needed in device code.
Offline analysis examples:
# Count events by type
jq -r .event build/device_trace.jsonl | sort | uniq -c | sort -rn
# Show all TIMEOUT events with virtual time
jq 'select(.event == "TIMEOUT")' build/device_trace.jsonl
# Plot TX byte timing
jq 'select(.event == "TX") | [.seq, .t_virt_ns, .ascii]' build/device_trace.jsonl
# IRQ latency: time between CH_DONE and IRQ_PULSE for DMA
jq 'select(.event == "CH_DONE" or .event == "IRQ_PULSE")' build/device_trace.jsonlAbstract base class for the peripheral-to-DMA-controller handshake. Device models that need DMA accept this interface type instead of the concrete DmaClientHandle, decoupling them from the DMA controller implementation.
class MyDevice(MMIODevice):
def __init__(self, dma: DmaRequestInterface, ...):
self._dma = dma
def _start_transfer(self):
ok = self._dma.transfer(
src, dst, length, callback=self._on_done,
)
# True = DACK (accepted), False = NACK (channel busy)
def _on_done(self, success: bool): ...Abstract members: transfer(src, dst, length, callback, *, src_fixed, dst_fixed) → bool, busy → bool, channel_id → int.
src_fixed=True / dst_fixed=True model peripheral register addresses that do not auto-increment (e.g. reading a FIFO or writing to the CRC DATA register).
DmaClientHandle in dma_controller.py is the concrete implementation.
All protocols are binary, little-endian.
Binary, little-endian, sent by mmio-sockdev on each guest MMIO access:
Read: 'R'(1B) | offset(4B LE) | size(1B) QEMU → Python
data(sizeB LE) Python → QEMU
Write: 'W'(1B) | offset(4B LE) | size(1B) | data(sizeB LE) QEMU → Python
reserved(8B LE) Python → QEMU
offset is relative to the device base address (addr= property).
The write response is reserved for protocol compatibility. Timed devices schedule callbacks through the device_event channel.
'I'(1B) | irq_idx(1B) | level(1B)
irq_idx is 0-based index of the IRQ output on the mmio-sockdev instance. level = 1 assert, 0 deassert.
Schedule: 'S'(1B) | device_id(2B LE) | event_id(2B LE) | delay_ns(8B LE)
Cancel: 'C'(1B) | device_id(2B LE) | event_id(2B LE)
Cancel device: 'X'(1B) | device_id(2B LE)
Fire: 'F'(1B) | device_id(2B LE) | event_id(2B LE) | vtime_ns(8B LE)
Python sends schedule/cancel commands; QEMU fires events according to QEMU_CLOCK_VIRTUAL.
Allows Python and SystemVerilog master devices to read/write absolute SoC addresses through the QEMU fabric. The target can be QEMU physical memory, QEMU-native MMIO, Python MMIO, or SV APB, depending on address decode. Maximum single Python transfer chunk: 64 KB.
Write: 'F'(1B) | 'W'(1B) | master_id(1B) | flags(1B)
| addr(8B LE) | length(4B LE) | data(lengthB)
QEMU responds: status(1B)
Read: 'F'(1B) | 'R'(1B) | master_id(1B) | flags(1B)
| addr(8B LE) | length(4B LE)
QEMU responds: status(1B) | data(lengthB)
flags is reserved and currently zero. Non-zero status means the fabric saw a
decode, access, slave, or transport error; device models should reflect that in
their own status/error registers.
Allows a Python device model to trigger a subsystem-level QEMU system reset without exiting the emulator. Used by the WDT to reboot the firmware while keeping all TCP connections open.
Python → QEMU: any single byte (e.g. 'R')
QEMU action: qemu_system_reset_request(SHUTDOWN_CAUSE_SUBSYSTEM_RESET)
— CPU resets, vector table re-fetched, firmware restarts.
— All chardev TCP sockets remain connected.
— Python device model instance continues running;
volatile registers cleared by on_reset(), retention registers preserved.
The rst-chardev property is optional. If omitted, the device operates normally without reset capability.
The Python device server binds all TCP ports first; QEMU connects to them as a client. Each device needs one mmio-sockdev instance. Run with -icount shift=5,sleep=off,align=off for deterministic chip-time simulation.
SYSCTRL is part of the KX6625 machine itself, so it does not need a -device mmio-sockdev command-line entry.
qemu-system-arm -M kx6625 -smp 2 -nographic -no-reboot \
-icount shift=5,sleep=off,align=off \
-device mmio-sockdev,...,addr=0x40004000,irq-num=0 \
-device mmio-sockdev,...,addr=0x40005000,irq-num=1 \
-device mmio-sockdev,...,addr=0x40006000,irq-num=2 \
-device mmio-sockdev,...,addr=0x40007000,irq-num=3 \
-device mmio-sockdev,...,addr=0x40008000 \
-device mmio-sockdev,...,addr=0x40009000,irq-num=4 \
-device mmio-sockdev,...,addr=0x4000B000,irq-num=5 \
-device mmio-sockdev,...,addr=0x4000C000,irq-num=6 \
-device mmio-sockdev,...,addr=0x4000D000,irq-num=7 \
-device mmio-sockdev,...,addr=0x40011000,irq-num=9 \
-kernel build/firmware.hexThe full command line, including all -chardev socket wiring, is maintained in scripts/e2e_test.sh and scripts/run_interactive.sh.
The KX6625 QEMU machine treats -kernel build/firmware.hex as a flash preload image, not as a direct ELF boot. During machine initialisation it fills FLASH with erased bytes (0xFF), parses the Intel HEX records into the FLASH ROM backing store, then lets the Cortex-M reset sequence fetch MSP/PC from address 0x00000000. Keep build/firmware.elf for symbols and debugging.
- Ubuntu/Debian (or compatible Linux)
- ARM cross-compiler:
sudo apt install gcc-arm-none-eabi - Verilator:
sudo apt install verilatoror another recent Verilator installation - Python 3 (standard library only)
- Build tools:
sudo apt install build-essential ninja-build pkg-config libglib2.0-dev libpixman-1-dev
make fwOutput: build/firmware.elf, build/firmware.bin, and build/firmware.hex. QEMU boots from build/firmware.hex; keep the ELF for symbols and post-build inspection. This also runs make gen to regenerate build/generated/mmio_devices.h from spec/devices.yaml.
make svOutput: sv_device/build/sv_host_shell, a Verilator-backed host shell process used by the e2e and interactive runners. The host shell is built with VCD trace support and dumps waveforms by default; pass --wave-file PATH to choose the output file or --no-wave to disable dumping.
make qemuOutput: scripts/qemu-fork/build/qemu-system-arm
ICOUNT_SHIFT=5 bash scripts/e2e_test.shThis single command:
- Starts the Python device server and the SV host shell
- Waits for the UART port to be ready
- Starts QEMU (
-M kx6625 -icount shift=5,sleep=off,align=off) with allmmio-sockdevinstances and preloadsbuild/firmware.hexinto FLASH - Polls firmware output in the server log for up to 120 s
- Asserts all expected log lines are present and prints PASS or FAIL
- Generates
build/e2e_sv_host_shell.vcdfor SV waveform inspection - Generates
build/trace_report.html— a self-contained HTML visualizer of all device events
Without ICOUNT_SHIFT, the test still runs in realtime wall-clock mode (functional, but non-deterministic timing).
Logs are written to build/e2e_server.log, build/e2e_qemu.log, and build/e2e_sv_host_shell.log for post-mortem inspection. The SV waveform is written to build/e2e_sv_host_shell.vcd; interactive runs write build/interactive_sv_host_shell.vcd.
Expected output:
[PASS] Found: "MMIO SockDev Interrupt Demo"
[PASS] Found: "NVIC initialised"
[PASS] Found: "IRQs enabled"
[PASS] Found: "UART interrupt handled"
[PASS] Found: "DMA demo"
[PASS] Found: "DMA started"
[PASS] Found: "Verification PASSED"
[PASS] Found: "Demo complete"
[PASS] Found: "DMA client test"
[PASS] Found: "DMA client transfer started"
[PASS] Found: "Transfer verified PASSED"
[PASS] Found: "All demos complete"
[PASS] Found: "CRC test"
[PASS] Found: "0xCBF43926 PASSED"
[PASS] Found: "DMA-CRC test"
[PASS] Found: "DMA-CRC] Result 0xCBF43926 PASSED"
[PASS] Found: "All tests done"
[PASS] Found: "Power-on reset (RESET_REASON=POR)"
[PASS] Found: "Kick 1"
[PASS] Found: "Kick 2"
[PASS] Found: "Waiting for WDT timeout"
[PASS] Found: "WDT] TIMEOUT"
[PASS] Found: "Warm boot detected: RESET_REASON=WDT"
[PASS] Found: "WDT demo complete"
[PASS] End-to-end IRQ test PASSED
Troubleshooting:
| Symptom | Likely cause | Fix |
|---|---|---|
Required file not found: …/qemu-system-arm |
QEMU not built yet | make qemu |
Required file not found: …/firmware.hex |
Firmware not built | make fw |
| Port already in use | Leftover process | fuser -k 7890/tcp 7891/tcp |
| Timeout / FAIL | IRQ not firing | Check build/e2e_server.log and build/e2e_qemu.log |
Option A — one-command interactive demo (recommended)
Opens the Python device server, QEMU, and a dedicated UART terminal window in one step. Close the terminal window to stop everything:
ICOUNT_SHIFT=5 bash scripts/run_interactive.shWhen a graphical terminal is available, an xterm window titled KX6625 UART Console appears showing only the clean firmware UART output. All Python device-model debug logs (DMA, IRQ, tick messages) stay in build/interactive_server.log.
In VS Code, SSH, or other environments where the spawned terminal may be hidden, run the console inline in the current terminal:
RUN_INLINE=1 ICOUNT_SHIFT=5 bash scripts/run_interactive.shThis script is intentionally interactive: it waits until you close the UART terminal window, or press Ctrl-C / Ctrl-] in inline mode. Use scripts/e2e_test.sh for an automated pass/fail test that exits on its own.
Option B — manual (three terminals)
Terminal 1 — Python device server:
python3 device_model/mmio_device_server.pyTerminal 2 — QEMU:
bash scripts/run_interactive.shTerminal 3 — UART terminal (type commands at the # prompt):
python3 scripts/uart_console.py
# or: nc 127.0.0.1 7904The display controller is a Python device model at 0x40011000 with NVIC IRQ9. Firmware writes RGB565 pixels into SRAM, then programs the display registers. The display model reads framebuffer memory through the QEMU fabric as a bus master, renders a host Tk window when a GUI display is available, computes FRAME_CRC, and pulses the frame-done IRQ.
Run the default single-layer display smoke test:
DISPLAY_KEEPALIVE=0 ICOUNT_SHIFT=5 bash scripts/display_interactive.shExpected firmware output includes:
[DISPLAY] Frame done IRQ PASSED!
[DISPLAY] Frame CRC 0xD57022DF PASSED!
Run the interactive display session and leave the Tk window open for inspection:
ICOUNT_SHIFT=5 bash scripts/display_interactive.shRun the AWTK RGB565 demo on the two-layer display path:
DISPLAY_KEEPALIVE=0 AWTK_DEMO=1 ICOUNT_SHIFT=5 bash scripts/display_interactive.shIn AWTK mode, the firmware build is selected with AWTK_DEMO=1. AWTK draws into a foreground RGB565 framebuffer, firmware programs layer0/layer1 scanout, and the display model composes each scanline by reading layer0 then layer1 through the same display bus master. The project-owned AWTK port glue lives under awtk/port/; the upstream AWTK source tree is expected under third_party/awtk.
The FreeRTOS application task (firmware/main.c) executes ten demos from the UART menu. Command a runs them back-to-back:
- Initialises NVIC (16 external IRQs armed, IRQs 0–7 enabled).
- Enables IRQs and waits (
WFI) for IRQ 0. - Python server fires the UART IRQ ~2 s after connecting; firmware acknowledges and prints
[FW] UART interrupt handled successfully!.
- Fills SRAM source buffer
0x20001000with bytes0x01..0x20(32 bytes). - Programs DMA CH0 registers:
CH0_SRC_ADDR=0x20001000,CH0_DST_ADDR=0x20002000,CH0_LENGTH=32,CH0_CTRL=START. - DMA controller (Python) reads the source and writes the destination through
BusMasterAddressSpace, which routes SRAM accesses overfabric-chardev, then pulses IRQ 1 at the modelled virtual-time deadline. - Firmware handles IRQ 1, then verifies the destination buffer matches the source.
- Prints
[DMA] Verification PASSED!and[FW] Demo complete.
- Firmware programs
DMA_CLIENT_DEMOregisters (SRC_ADDR,DST_ADDR,LENGTH) and writesCTRL.START. DmaClientDemoDevicecallsdma_handle.transfer()— DREQ to CH1.- DMA controller accepts (DACK), performs the copy, then calls the demo device's
on_completecallback. - Demo device sets
STATUS.DONEand pulses IRQ 3. - Firmware handles IRQ 3, verifies buffer, prints
[DMA] Transfer verified PASSED!and[FW] All demos complete.
- Direct test: firmware writes
CRC_CTRL=RESET, then feeds the 9 bytes of"123456789"directly toCRC_DATA, readsCRC_RESULT. Asserts result equals0xCBF43926. - DMA-fed test: firmware sets up a DMA M2P transfer from SRAM (containing
"123456789") toCRC_DATAoffset0x40008000, starts the transfer, waits for IRQ 1, readsCRC_RESULT. Asserts result equals0xCBF43926. - Prints
[CRC] Result 0xCBF43926 PASSED!,[DMA-CRC] Result 0xCBF43926 PASSED!, and[FW] All tests done.
- CPU0 writes request data into the shared SRAM IPC window, then marks
IPC_STATUS=PENDING. - CPU1 is already running
cpu1_main()after CPU0 released it throughSYSCTRL.CPU1RST; it polls the shared IPC status word. - CPU1 computes
IPC_ARG0 ^ 0xCAFEBABE, writesIPC_RESP, and marksIPC_STATUS=DONE. - CPU0 verifies the response and prints
[IPC] Dual-CPU IPC PASS.
- Firmware writes
SV_ISLAND_LOAD=8, then starts the SV timer with IRQ enabled. - QEMU forwards the MMIO writes to the SV host shell at TCP port 7906.
sv_apb_ingress.svturns the host request into APB setup/access cycles onsv_timer_apb.sv, advances the timer in the SV device's local pclk domain, and the host shell observesirq_o.- The host shell sends an IRQ message on port 7907; QEMU raises NVIC IRQ5.
- Firmware handles IRQ5, writes
SV_ISLAND_IRQ_CLEAR, and prints[SVTIMER] IRQ observed and cleared PASSED!.
This phase validates the QEMU-to-SV transaction path and RTL interrupt behaviour. It does not model CPU bus wait states or force the SV pclk to remain cycle-aligned with QEMU's 48 MHz CPU clock.
- Firmware reads
SYSCTRL_ID,SYSCTRL_BOOT_STATUS, andSYSCTRL_CPU_STATUSfrom the QEMU-native controller at0x4000A000. - Firmware writes the device clock/reset policy registers and confirms the reset request is latched in
DEVICE_RST_STATUS. - Firmware programs
DEVCTL_ADDR=CONSOLE_UART_STATUS, writesDEVCTL_CTRL=START|READ, and verifiesDEVCTL_RDATAreports UART TX-ready. - This validates that SYSCTRL can act as a central system-control block and as a controlled bus master for device register access.
- Firmware programs OTP key slot 0 rows with the AES-128 test key after writing the required unlock words.
- Firmware attempts a CPU
READof key row 0 and expectsERROR.READ_PROTECTED. - Firmware programs and reads a non-secret customer row, then attempts an invalid 0 to 1 program and expects rejection.
- Firmware runs HSM AES-CBC with
KEY_ID=0; HSM obtains the key through the direct OTP provider rather than CPU-readable registers.
- POR boot: firmware reads
WDT_RESET_REASON_REG; value = 0 (REASON_POR) → first boot path. - Sets
WDT_LOAD = 200 ms, prints"Kick 1"and"Kick 2"(twoKICKregister writes). - Stops kicking, prints
"Waiting for WDT timeout". PythonWdtDevice.on_event()fires after 200 ms virtual time. - Python calls
SystemResetManager.wdt_reset(): all bus deviceson_reset()called (volatile state cleared, retention registers preserved), thenRstController.send_reset()sends one byte to QEMU over TCP:7903. - QEMU receives the byte →
qemu_system_reset_request(SHUTDOWN_CAUSE_SUBSYSTEM_RESET)— CPU reboots, TCP sockets stay open. - Warm boot: firmware reads
WDT_RESET_REASON_REG= 1 (REASON_WDT) → warm boot path. - Reads
WDT_TIMEOUT_CNT_REG(= 1), prints"Warm boot detected: RESET_REASON=WDT","WDT demo complete", disables WDT.
qemu_device/
├── Makefile # Top-level build system
├── README.md # This file
├── spec/ # Device specifications (single source of truth)
│ ├── README.md # Memory map + per-device register tables (human-readable)
│ ├── devices.yaml # Platform memory map + IRQ + TCP port topology
│ ├── uart.yaml # Console UART register map
│ ├── dma.yaml # DMA controller CH0/CH1 register map
│ ├── dma_client_demo.yaml # DMA client demo register map
│ ├── timer.yaml # Timer 0 register map
│ ├── crc.yaml # CRC-32 engine register map
│ ├── hsm.yaml # HSM AES/CMAC accelerator register map
│ ├── otp.yaml # OTP controller register map and HSM direct-key provider
│ ├── display.yaml # RGB565 display controller + two-layer composition register map
│ ├── sysctrl.yaml # Native SYSCTRL register map
│ ├── wdt.yaml # Watchdog Timer register map
│ └── sv_device.yaml # SystemVerilog APB timer/GPIO/DMA prototype register map
├── firmware/ # FreeRTOS Cortex-M4F firmware
│ ├── FreeRTOSConfig.h # KX6625 FreeRTOS config (48 MHz, 1 kHz SysTick)
│ ├── start.S # Vector table, Reset_Handler, CPU0/CPU1 split
│ ├── main.c # CPU0 FreeRTOS task + demo menu (UART/DMA/CRC/IPC/SV/OTP/HSM/SYSCTRL/WDT)
│ ├── cpu1_main.c # CPU1 bare-metal IPC polling loop
│ ├── runtime.c # Minimal freestanding memset/memcpy for -nostdlib
│ ├── linker.ld # Memory layout (FLASH @ 0x00000000, SRAM @ 0x20000000)
│ ├── drivers/display/ # Display controller firmware driver and CRC smoke test
│ ├── drivers/awtk_demo/ # Optional AWTK RGB565/two-layer pointer demo
│ └── Makefile # Runs gen_device_code.py then compiles FreeRTOS firmware
├── awtk/ # Project-owned AWTK port glue for KX6625/QEMU
│ └── port/ # awtk_config.h + raw main-loop/platform shims
├── freertos/
│ └── FreeRTOS-Kernel/ # Vendored FreeRTOS kernel source + GCC ARM_CM4F port
├── sv_device/ # SystemVerilog device prototypes
│ ├── sv_device_top.sv # Verilated SV device-island top
│ ├── sv_apb_ingress.sv # Host request to APB setup/access bridge
│ ├── sv_timer_apb.sv # APB register timer RTL
│ ├── sv_dma_apb.sv # APB-visible DMA registers + core wrapper
│ ├── sv_master_router.sv # SV master request/response routing point
│ ├── sv_fabric_egress_dpi.sv # SV master egress to C++ DPI fabric helpers
│ ├── sv_host_shell.cpp # Verilator host shell: sockets, clock, IRQ, DPI
│ └── Makefile # Builds sv_device/build/sv_host_shell
├── device_model/ # Python device emulation layer
│ ├── mmio_base.py # MMIODevice ABC; IRQController; FabricChannel;
│ │ # RstController; UartChannel;
│ │ # RegisterBank (+ RegAccess policies); IrqLine;
│ │ # VirtualClock; DmaRequestInterface
│ ├── mmio_device_server.py # Transport servers + PeripheralBus + main()
│ ├── soc_config.py # YAML topology normalization + dependency ordering
│ ├── soc_assembly.py # Device factories + capability/dependency wiring
│ ├── soc_top.py # PythonDeviceDomain facade; kx6625_default(); SoCTop alias
│ ├── tracer.py # Non-blocking JSONL event tracer (Tracer, DeviceTracer,
│ │ # NULL_DEVICE_TRACER; background writer thread)
│ ├── uart_model.py # Console UART (character output + demo IRQ)
│ ├── dma_controller.py # DMA controller (multi-channel M2M + DREQ/DACK)
│ ├── dma_client_demo.py # DMA client demo peripheral (DREQ/DACK to DMA CH1)
│ ├── timer_model.py # Countdown timer (virtual-clock, one-shot + periodic)
│ ├── crc_device.py # CRC-32/ISO-HDLC hardware accelerator
│ ├── hsm_model.py # HSM AES/CMAC accelerator with direct OTP key provider
│ ├── otp_model.py # File-backed OTP controller with ECC and protected key rows
│ ├── display_model.py # RGB565 display bus-master model + host Tk output
│ ├── wdt_model.py # Watchdog Timer (countdown reset + retention registers)
│ └── generated/ # Auto-generated constants (make gen / make fw)
│ └── device_consts.py # Python constants mirroring mmio_devices.h
├── scripts/
│ ├── build_qemu.sh # QEMU configure + ninja build
│ ├── gen_device_code.py # Code generator: spec/ → C header + Python consts
│ ├── run_interactive.sh # One-command demo: server + QEMU + UART console
│ ├── display_interactive.sh # Display-focused validation + optional AWTK demo
│ ├── uart_console.py # Bidirectional UART terminal client (port 7904)
│ ├── visualize_trace.py # Generate self-contained HTML trace report from JSONL
│ ├── e2e_test.sh # Automated end-to-end smoke test → trace_report.html
│ └── qemu-fork/ # Modified QEMU 8.1.0 source tree (build target)
│ └── hw/
│ ├── misc/mmio_sockdev.c # Generic SysBus mmio-sockdev (chardev/irq/tick/fabric/rst)
│ └── arm/kx6625.c # KX6625 custom SoC definition
└── build/ # Build artifacts (gitignored)
├── firmware.elf / firmware.bin / firmware.hex
├── device_trace.jsonl # Device event trace (JSONL; created at server runtime)
├── trace_report.html # HTML trace visualizer (generated after e2e_test.sh)
└── generated/
└── mmio_devices.h # Auto-generated C header (make gen / make fw)
| Target | Description |
|---|---|
make gen |
Generate C header + Python consts from spec/ |
make fw |
Generate constants, then build firmware (build/firmware.elf, .bin, .hex) |
make sv |
Build Verilator-backed SV device prototypes |
make qemu |
Copy mmio_sockdev.c to qemu-fork, then build QEMU |
make run |
Print interactive run instructions |
make clean |
Remove all build artifacts |
| Parameter | Value |
|---|---|
| Machine | kx6625 (dual Cortex-M4 @ 48 MHz) |
| NVIC external IRQs | 16 |
| UART IRQ | 0 (irq-num=0) |
| DMA IRQ | 1 (irq-num=1) |
| Timer 0 IRQ | 2 (irq-num=2) |
| DMA Client Demo IRQ | 3 (irq-num=3) |
| WDT pre-reset IRQ | 4 (irq-num=4) |
IRQ pulse pattern: assert then immediately deassert to edge-trigger the NVIC without re-firing.
- Define the spec: add an entry in
spec/devices.yamland createspec/<name>.yamlwith the register map. Put instance-level topology inspec/devices.yaml:kind,domain,capabilities,dependencies, optionalevent.device_id, optionalbus_master, and optionalfabric_endpoint. Add SoC-wide bus-master IDs inspec/soc.yaml. - Write the model: create
device_model/<name>_model.pysubclassingMMIODevice. Overrideread(),write(), and optionallyon_event()for timed behavior throughdevice_event. UseRegisterBank(withRegAccesspolicies) for register storage,IrqLinefor interrupt injection,VirtualClockfor countdown timing, andDmaRequestInterfacefor bus-master DMA requests. For watchdog-style resets, instantiate aRstControllerand pass aSystemResetManager.wdt_resetcallback. - Add a factory: add a small factory in
device_model/soc_assembly.pyand register it in_FACTORIESunder the devicekind. The factory should construct the model and useSoCAssemblyContexthelpers such asbind_mmio(),bind_irq(),bind_event(),bind_fabric_endpoint(), andaddress_space()instead of editingsoc_top.py. - Wire the tracer: accept
tracer: Optional[Tracer] = Nonein__init__, assignself._tr = tracer.context(self.name) if tracer else NULL_DEVICE_TRACER, callself._tr.tick(vtime_ns)at the top ofon_event()for timed callbacks, and emit events withself._tr.emit('EVENT_NAME', **fields). Passtracer=tracerwhen constructing the device. - Let assembly bind transports:
PythonDeviceDomainhandles servers automatically from YAML capabilities and factory bindings. Timed Python devices register withDeviceEventServer. Bus-master devices useFabricServerplusFabricChannel; devices that trigger system resets get anRstServerinstance wired to aRstController. - Extend the QEMU command line: add a
mmio-sockdevinstance withchardev,irq-chardev,addr,irq-num. Addfabric-chardevfor bus-master access andrst-chardevfor system-reset capability. The shareddevice-eventservice is already part of the default command lines. - Regenerate constants with
make gen.
| Symptom | Fix |
|---|---|
"chardev not connected" |
Start Python server before QEMU |
"Connection refused" on port |
Check server is running: lsof -i :7890 |
| Firmware never prints | Verify build/firmware.hex exists and QEMU was rebuilt (make fw && make qemu) |
| IRQ never fires | Check build/e2e_server.log; confirm IRQ port not blocked |
| DMA never completes | Check build/e2e_server.log for [FABRIC] and [TICK] connection lines |
| WDT timeout never fires | Confirm WDT CTRL.ENABLE is set; check [TICK] connection in server log |
| QEMU never resets after WDT | Verify QEMU was rebuilt (make qemu) with rst-chardev support |
| Warm boot not detected | Python server was restarted (clears retention registers); re-run the full test |
"Property 'mmio-sockdev.rst-chardev' not found" |
QEMU binary is stale; run make qemu |
| QEMU build fails | Install missing libs: sudo apt install libglib2.0-dev libpixman-1-dev |
| ARM toolchain missing | sudo apt install gcc-arm-none-eabi |
"Parameter 'driver' expects a pluggable device type" |
QEMU binary is stale; run make qemu |
| Firmware stuck at "Waiting for UART interrupt" | Check NVIC configuration in nvic_init(); ensure IRQ 0 is enabled |
Stop QEMU: Ctrl+C (script) or Ctrl+A X (nographic monitor).
This project is provided as educational material for understanding QEMU device development and ARM system emulation.