Display Stack Validation Pipeline
The ST7789 display stack is more than a firmware driver—it is a renode-first validation workflow that proves the entire graphics path before any bytes touch flash. This document codifies the process so the next time we add a peripheral (display or otherwise) we can repeat the pattern with confidence.
Why simulate first?
- Zero hardware dependency: iterate when the target board is on the other side of the planet (or occupied by CI).
- Deterministic repros: simulator logs capture every byte; bugs are no longer “flaky hardware”.
- Tight driver feedback loop: firmware changes and peripheral emulation evolve together.
- Screenshot/video capture: documentation and demos without a camera pointed at a dev board.
Architecture overview
┌────────────────────────────────────────────────────────────┐
│ Zig firmware (src/firmware, src/drivers/st7789.zig) │
│ └─ Board-native SPI/pin contract (src/bsp/stm32f103_…) │
│ └─ Driver exercises display API │
│ │
│ Renode environment │
│ ├─ Platform (.repl) instantiates ST7789Visual │
│ ├─ C# plugin (renode/plugins/ST7789Visual.cs) │
│ ├─ Validation script (renode/scripts/test_st7789.resc) │
│ └─ mise tasks build/load everything │
│ │
│ Tooling │
│ ├─ parse_spi_log.py → verifies byte stream │
│ └─ st7789_visualize.py → renders captured framebuffers │
└────────────────────────────────────────────────────────────┘
When mise run test-display-renode executes, the pipeline:
- Compiles the C# plugin (
build-st7789-visual). - Cross-builds the Blue Pill firmware (
build-bluepill). - Generates a
.rescthat loads the DLL, scans it via Renode’sTypeManager, instantiates the SPI peripheral, and launches the analyser window if requested. - Pipes the entire console/monitor log to
build/renode/st7789.log. - Runs
parse_spi_log.pyto assert the init sequence matches the datasheet.
The net effect is a fully automated loop capable of catching regressions long before they hit hardware.
Step-by-step workflow
1. Expose firmware primitives
- Add board-specific SPI and pin backends (
src/bsp/stm32f103_bluepill/ spi1.zig,gpio.zig,display_bus.zig). - Re-export the abstractions from
src/root.zigso both host tests and firmware share the same API (@import("board_spi"), etc.). - Update
src/firmware/main.zigto gate the display demo onconsole.boardName()—other boards remain unaffected.
2. Keep host unit tests first-class
- All inline tests live under
tests/;zig build testexercises the SPI contract, pin double, and driver semantics. - Additional parser/visualisation utilities live in
renode/scripts/parse_spi_log.pyandst7789_visualize.py.
3. Teach Renode about the peripheral
- Implement the ST7789 as a plugin in
renode/plugins/ST7789Visual.cs. The class derives fromAutoRepaintingVideo(for framebuffers) andISPIPeripheral(for SPI traffic). It decodes CASET/RASET/RAMWR commands, tracks a window, converts RGB565 → RGB888, and writes into the simulator’s video buffer. - Describe the board in
renode/platforms/boards/bluepill-st7789.repl: instantiate the plugin, wire GPIO inputs for DC/CS/RESET, and leave the LED/button wiring intact for visibility. - Extend the
.resclauncher (renode/scripts/test_st7789.resc) to:- load the plugin (
clr.AddReferenceToFileAndPath), - register it with
TypeManager.Instance.ScanFile, - optionally open the framebuffer (
@@SHOW_ANALYZER@@placeholder), - quit or remain running based on
RENODE_NO_QUIT.
- load the plugin (
4. Automate with mise tasks
Key tasks in .mise.toml:
| Task | Purpose |
|---|---|
build-st7789-visual | dotnet build the plugin using the pinned SDK (tools.dotnet). |
sim-bluepill-display | Compile firmware, generate .resc, honour RENODE_OPTS, RENODE_VISUAL, RENODE_NO_QUIT, run Renode, tee output. |
test-display-renode | Depends on simulation, then runs parse_spi_log.py to assert the init sequence. |
Environment toggles:
RENODE_OPTS— override Renode flags. Unset →--disable-xwt --console; set to empty string → GUI mode; custom flags respected verbatim.RENODE_VISUAL=1— injectshowAnalyzer sysbus.spi1.displayand keep the analyser window alive.RENODE_NO_QUIT=1— comment outquitin the.rescso Renode keeps running for manual inspection.
5. Inspecting and capturing output
build/renode/st7789.logcontains the full console log. The parser looks for known command/data sequences and fails the task when anything is missing or reordered.mise run sim-bluepill-displayfollowed byRENODE_VISUAL=1surfaces the framebuffer window.- From the Renode monitor:
(bluepill) sysbus.spi1.display SaveScreenshot @output.png - For post-hoc rendering without a GUI, run
python3 renode/scripts/st7789_visualize.py --log build/renode/st7789.log --output frame.png.
6. Moving to hardware
Once the simulation is green we only need to flash the firmware:
zig build -Dboard=stm32f103-bluepill -Dtarget=thumb-freestanding-none -Dcpu=cortex_m3 -Doptimize=ReleaseSafe
Because the firmware/pin/SPI contracts are shared between host tests, firmware build, and the Renode platform, the behaviour observed in simulation is exactly what hits the MCU.
Reapplying the pattern
When adding a new peripheral (display, sensor, codec, …):
- Model the firmware contract in
src/bsp/<board>/…and expose it throughsrc/root.zig. - Keep Zig tests adjacent to the implementation;
zig build testremains the first line of defence. - Emit simulator artefacts in
build/renode/and parse them with a small helper script. - Write a Renode peripheral (C# or Python for prototyping) and load
it via
.resc. If the existing namespaces are insufficient, verify the type is auto-loaded and available to the REPL. - Document the knobs: how to enable the visualiser, how to keep the simulator alive, how to take screenshots.
Following these steps keeps the validation pipeline reproducible for the next maintainer—and guarantees we ship firmware that has already proven itself under simulation before the first flash attempt.