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:

  1. Compiles the C# plugin (build-st7789-visual).
  2. Cross-builds the Blue Pill firmware (build-bluepill).
  3. Generates a .resc that loads the DLL, scans it via Renode’s TypeManager, instantiates the SPI peripheral, and launches the analyser window if requested.
  4. Pipes the entire console/monitor log to build/renode/st7789.log.
  5. Runs parse_spi_log.py to 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.zig so both host tests and firmware share the same API (@import("board_spi"), etc.).
  • Update src/firmware/main.zig to gate the display demo on console.boardName()—other boards remain unaffected.

2. Keep host unit tests first-class

  • All inline tests live under tests/; zig build test exercises the SPI contract, pin double, and driver semantics.
  • Additional parser/visualisation utilities live in renode/scripts/parse_spi_log.py and st7789_visualize.py.

3. Teach Renode about the peripheral

  • Implement the ST7789 as a plugin in renode/plugins/ST7789Visual.cs. The class derives from AutoRepaintingVideo (for framebuffers) and ISPIPeripheral (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 .resc launcher (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.

4. Automate with mise tasks

Key tasks in .mise.toml:

TaskPurpose
build-st7789-visualdotnet build the plugin using the pinned SDK (tools.dotnet).
sim-bluepill-displayCompile firmware, generate .resc, honour RENODE_OPTS, RENODE_VISUAL, RENODE_NO_QUIT, run Renode, tee output.
test-display-renodeDepends 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 — inject showAnalyzer sysbus.spi1.display and keep the analyser window alive.
  • RENODE_NO_QUIT=1 — comment out quit in the .resc so Renode keeps running for manual inspection.

5. Inspecting and capturing output

  • build/renode/st7789.log contains 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-display followed by RENODE_VISUAL=1 surfaces 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, …):

  1. Model the firmware contract in src/bsp/<board>/… and expose it through src/root.zig.
  2. Keep Zig tests adjacent to the implementation; zig build test remains the first line of defence.
  3. Emit simulator artefacts in build/renode/ and parse them with a small helper script.
  4. 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.
  5. 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.