# Chiplab — full site content for LLMs > Chiplab is an MCP platform by Veecle that lets AI coding agents compile, simulate, and validate firmware on virtual instances of real microcontrollers — no physical hardware. This file is the full text corpus of https://veecle.ai for one-fetch ingestion. Curated index: https://veecle.ai/llms.txt # Home / Product (https://veecle.ai/ and https://veecle.ai/chiplab) Chiplab is the API between AI agents and silicon. Agents call Chiplab over MCP to run firmware on a virtual instance of the real target chip — same binary, same peripherals, same interrupt timing as the physical board. Not a generic CPU emulation. Cross-compilation, peripheral simulation, sensor IO, and chip-by-chip plumbing are exposed through one endpoint: https://chiplab.veecle.ai/mcp Every run deposits what happened into a shared corpus. The next agent that calls inherits it. How it works: 1. The agent calls — Cursor, Claude Code, Codex, Windsurf, opencode, any MCP host. One endpoint. 2. Chiplab routes — corpus lookup, build, simulate, test, bench, composed behind a single verb. 3. Firmware proves out — verdict, trace, fix, back to the agent in seconds. The corpus learns. The MCP verbs: - chiplab.ask (shipped): Semantic + structural search across every firmware test, validated fix, architecture decision, and chip-specific gotcha indexed in the corpus. Returns ranked matches with confidence and provenance. - chiplab.run (shipped): Execute firmware on a virtual chip via Renode or QEMU. The agent submits an ELF + target board; Chiplab boots the virtual chip and returns stdout/UART capture, telemetry, GPIO/register state, and any panic or fault trace. No local toolchain, no hardware. - chiplab.build (in progress): Compile remotely against production toolchains — GCC, LLVM, IAR EWARM, Keil ARMCC, HighTec ASIL-D, Renesas CC-RX — without installing or licensing them. - chiplab.test (in progress): Unit and integration tests on virtual hardware every commit. Coverage, regression tracking, root-cause matching against the corpus. - chiplab.bench (planned): Profile code and recommend the optimal board — timing, memory, power, and cost side by side across chip families. - chiplab.simulate (planned): Full-system simulation against partner environment models — Renode for the chip; BeamNG, dSPACE, NVIDIA Isaac for the world it lives in. Inject synthetic sensor data (camera frames, CAN bus, IMU) into running firmware. - chiplab.generate (planned): AI-driven inputs — fuzz cases, synthetic sensor frames, edge-case stimuli. - chiplab.automate (planned): The orchestrator. Chain build → run → test → bench → simulate → ask autonomously. # Supported boards (today) Chiplab currently supports STM32 and Nordic nRF boards. Each has ready-to-run examples in https://github.com/veecle/chiplab under examples///: | Board | Chip | Board key | Frameworks with examples | | ------------------- | ---------- | ------------------- | ----------------------------------- | | STM32F4 Discovery | STM32F407 | stm32f4_discovery | bare-metal, embassy-rust, freertos | | STM32F7 Discovery | STM32F746 | stm32f7_discovery | bare-metal, embassy-rust | | STM32F103 Blue Pill | STM32F103 | stm32f103_blue_pill | bare-metal, embassy-rust | | STM32WBA52 Nucleo | STM32WBA52 | stm32wba52_nucleo | embassy-rust | | STM32L073 Nucleo | STM32L073 | stm32l073_nucleo | bare-metal, embassy-rust | | STM32H745 Nucleo | STM32H745 | stm32h745_nucleo | bare-metal | | nRF52840 DK | nRF52840 | nrf52840_dk | bare-metal, embassy-rust, zephyr-os | Simulation covers CPU execution of the ELF, UART/USART/LPUART peripherals (output captured and returned), and peripheral registers and interrupt timing matching the physical chip. Runs are bounded to a fixed amount of virtual CPU time. The discovery/help tool (call it with no arguments) is the authoritative, up-to-date source for boards and tools. # Connect your agent Chiplab is an HTTP MCP server at https://chiplab.veecle.ai/mcp. Standard mcpServers config: { "mcpServers": { "chiplab": { "type": "http", "url": "https://chiplab.veecle.ai/mcp" } } } Claude Code: claude mcp add --transport http chiplab https://chiplab.veecle.ai/mcp Codex (~/.codex/config.toml): [mcp_servers.chiplab] url = "https://chiplab.veecle.ai/mcp", then codex mcp login chiplab VS Code: same JSON block in .vscode/mcp.json under a "servers" key. On first use the client opens a browser to sign in. Accounts are self-serve and free at https://chiplab.veecle.ai — no credit card. # Pricing (https://veecle.ai/pricing) Credits, not seats. Every MCP verb invocation consumes credits priced by the compute it actually triggers. - Free: €0 forever. 1,000 credits/day, resets every 24h. Access to all verbs and features, any agent. - Pay-as-you-do (recommended): €10 per 1,000 credits. Top up when you outgrow the daily pool. Coming soon. - Enterprise: custom. Volume commit, custom chip/tool integrations, on-prem deployment, SLAs. # Roadmap (https://veecle.ai/roadmap) Eight verbs, one endpoint. Today: ask + run, on STM32 and Nordic. Q3 2026: build + test, adding Infineon. Q4 2026: bench + simulate, adding Renesas. 2027+: generate + automate, adding NXP. For each supported chipmaker Chiplab starts with one mainstream chip and expands coverage. Missing a chip? Open a request at https://github.com/veecle/chiplab/issues/new/choose or ask via https://veecle.ai/contact # About (https://veecle.ai/about) Veecle did not start here. Two products' worth of expertise feed Chiplab: an open-source Rust RTOS shipped on production devices (system internals — real-time constraints, memory budgets, silicon quirks) and a browser-based embedded IDE (developer experience — fast test loops, debugging surfaces, observability). Based in Berlin. Backed by exist, Relay Ventures, IBB Ventures, F-LOG Ventures, Plug and Play, and IT Inkubator. Careers: https://veecle.jobs.personio.com/ # Blog (https://veecle.ai/now?tab=blog) ## Zephyr's Twister hardware map is a YAML file, not a product (https://veecle.ai/blog/zephyr-twister-hardware, 2026-08-17) Somebody is going to explain hardware-in-the-loop testing to you this year using Zephyr's Twister, and it will be framed as a technique. A YAML file, a debug probe, a self-hosted CI runner, a small Linux box under a desk with six boards hanging off a USB hub. It is not a technique. It is a schema-validated config file and three command line flags, shipped in the tree, documented in full. I read the whole thing at v4.4.1, tagged 2026-06-10. Here is what is actually specified, and the more interesting half: what is not. ### The hardware map is a list of boards with five required fields The contract lives in `scripts/schemas/twister/hwmap-schema.yaml`. Sixty-seven lines. Five required fields per entry: `connected`, `id`, `platform`, `product`, `runner`.[^1] Everything else is optional, and the optional list is where lab work actually happens: `probe_id`, `runner_params`, `serial_pty`, `serial`, `baud`, `serial_baud`, `post_script`, `post_flash_script`, `pre_script`, `fixtures`, `flash_timeout`, `flash_with_test`, `flash_before`, `script_param`, `west_flash_cmd`. This is not a convention Twister tolerates. `HardwareMap.load()` reads those exact keys off each entry and validates the file against the schema before anything gets programmed.[^2] Misspell `fixtures` and validation rejects the file before any board gets touched. Not a warning, not a quietly degraded run. Zephyr's own documentation carries a two-board example: ```yaml - connected: true id: OSHW000032254e4500128002ab98002784d1000097969900 platform: reel_board product: DAPLink CMSIS-DAP runner: pyocd serial: /dev/cu.usbmodem146114202 baud: 9600 - connected: true id: 000683759358 platform: nrf52840dk/nrf52840 product: J-Link runner: nrfjprog serial: /dev/cu.usbmodem0006837593581 baud: 9600 ``` A reel_board behind a DAPLink probe driven by pyOCD, and an nRF52840 DK behind a J-Link driven by nrfjprog.[^3] Two vendors, two probe stacks, two flash tools, one host, one flat list. ![Annotated Zephyr Twister hardware map YAML showing two boards, a reel_board on pyOCD and an nRF52840 DK on nrfjprog, with the five required schema fields labeled](/blog/zephyr-twister-hardware/image-1.webp) ### The runner list is short, and Zephyr says so out loud `--generate-hardware-map` walks your serial devices and guesses the runner. The guess is a dictionary: ```python runner_mapping = { 'pyocd': ['DAPLink CMSIS-DAP', 'MBED CMSIS-DAP'], 'jlink': ['J-Link', 'J-Link OB'], 'openocd': ['STM32 STLink', '^XDS110.*', 'STLINK-V3', '^Tigard.*', 'KitProg3'], 'dediprog': ['TTL232R-3V3', 'MCP2200 USB Serial Port Emulator'] } ``` That is the entire autodetect surface.[^4] Four keys, eleven product strings, two regexes. Note what is missing: `nrfjprog` is a legal `runner:` value (it is in the example above), but it is not in that table, so autodetect will never produce it. You type it in by hand. The docs state the ceiling in a sentence more people should quote: > "Currently only boards with support for pyocd, nrfjprog, jlink, openocd, or > dediprog are supported with the hardware map features. Boards that require > other runners to flash the Zephyr binary are still work in progress."[^5] Five runners. If your part programs through something else, the hardware map is not what saves you. There is one documented door in that wall, and it is instructive. Intel ADSP targets flash over a remote-host protocol with no debug probe at all: ```yaml - connected: true id: None platform: intel_adsp/cavs25 product: None runner: intel_adsp serial_pty: path/to/script.py runner_params: - --remote-host=remote_host_ip_addr - --key=/path/to/key.pem ``` `runner_params` passes straight through, making that entry equivalent to `west flash --remote-host remote_host_ip_addr --key /path/to/key.pem`.[^6] `id` and `product` are literally `None`, and the console is a script behind `serial_pty`. So the five-runner limit describes what autodetect and the default flash path handle, not a hard boundary, and the counterexample sits a few paragraphs from the sentence declaring the boundary. ### Three commands, then the flags you actually end up needing The generate-then-run loop is two lines, verbatim from Zephyr's docs: ```bash ./scripts/twister --generate-hardware-map map.yml ./scripts/twister --device-testing --hardware-map map.yml -T samples/hello_world/ ``` The flags behind them, from argparse: `--device-testing`, `--generate-hardware-map `, `--hardware-map `, and `--persistent-hardware-map` for stable device names on Linux.[^7] Nobody stops there. The second tier is where the real bench shows up. `--device-flash-timeout` defaults to 60 seconds, and some parts do not finish in 60 seconds. `--flash-before` exists for boards where the programming port and the console port are the same port, or where USB only appears after software brings it up. `--flash-command` discards `west flash` entirely and runs your script instead.[^8] Shared console and programming port. Soft USB. A part slower than a minute to program. Every one of those is a flag because somebody hit it on real hardware and pushed the fix upstream. That is years of lab scar tissue, already committed. ![Flow diagram of the Zephyr Twister hardware map workflow from generate-hardware-map through a manual edit to device-testing, fanning out across pyocd, openocd, jlink, nrfjprog and dediprog to a physical board](/blog/zephyr-twister-hardware/image-2.webp) ### Fixtures are the part worth copying Fixtures answer "this test needs a wire between two pins". The test declares what it needs, the DUT declares what it has, Twister matches them. In the docs' words: "Some tests require additional setup or special wiring specific to the test... A test scenario can specify the fixture it needs which can then be matched with hardware capability of a board and the fixtures it supports via the command line or using the hardware map file."[^9] Board side: ```yaml - connected: true fixtures: - gpio_loopback id: 0240000026334e450015400f5e0e000b4eb1000097969900 platform: frdm_k64f product: DAPLink CMSIS-DAP runner: pyocd serial: /dev/ttyACM9 ``` Test side, from a real in-tree test at `tests/drivers/uart/uart_async_api/testcase.yaml`: ```yaml tests: drivers.uart.async_api: filter: CONFIG_SERIAL_SUPPORT_ASYNC and not CONFIG_UART_MCUX_LPUART harness: ztest harness_config: fixture: gpio_loopback depends_on: gpio ``` Same string on both sides.[^10] That is the entire contract. The docs add two constraints: one fixture per test scenario, and the name must be unique across the suite.[^11] The matching is a handful of lines on each side, and both sides split on a colon: ```python ## testinstance.py fixture = testsuite.harness_config.get('fixture') if fixture: can_run = fixture in map(lambda f: f.split(sep=':')[0], fixtures) ``` ```python ## hardwaremap.py def reserve_dut(self, device, fixture): for d in self.duts: if fixture and fixture not in (f.split(sep=':')[0] for f in d.fixtures): continue ... ``` That colon is a parameter slot.[^12] `gpio_loopback:PA0-PA1` on the board still matches `fixture: gpio_loopback` in the test, and the suffix rides along with the reservation. Small detail, load-bearing the moment you own four boards with the same capability wired differently. ![Diagram showing a Zephyr testcase.yaml fixture gpio_loopback matched against a hardware map DUT fixtures list, with an unmatched board skipped rather than failed](/blog/zephyr-twister-hardware/image-3.webp) ### Zephyr documents the DUT, not the lab I went looking for Zephyr's reference lab architecture (the host box, the USB hub, the udev rules, the runner registration). Zephyr's Twister documentation specifies the DUT contract down to the field name, but it does not provide a reference architecture for the host, USB topology, recovery, or CI runner. What exists instead is third-party layering. Golioth publishes an open-source repo wiring Twister-based HIL into GitHub Actions self-hosted runners.[^13] An independent write-up wraps `--device-testing --device-serial` plus `--west-flash="--skip-rebuild,--dev-id=..."` in a bash loop to cycle-test boards.[^14] A Zephyr meetup talk this June covers hardware-in-the-loop at scale with Twister, pitched as how a consultancy automates testing on real hardware and scales the infrastructure.[^15] None of that is bad. It is useful, it is attributed, and it fills a real gap. Each one starts from the same specified DUT contract, then adds the host, cabling, recovery, and CI policy that upstream leaves open. The schema did not change. The flags did not change. Be careful what you read into Zephyr's own blog, too. There is a case study on the project site involving a Raspberry Pi Pico, and the Pico is the board under test, not the lab controller.[^16] Conflate those and you end up asserting that Zephyr recommends a Pi-as-lab-host architecture, which the docs never say. So the honest framing: the mechanism is documented and boring, the lab is undocumented and opinionated. Re-explanation keeps happening at the boring layer, because the opinionated layer is harder to write and does not generalize past your own bench. ### What it replaces, and what it does not The hardware map answers exactly one question: does this test pass on that physical part. That question is worth real money, and it is also the most expensive question in the pipeline, because the answer requires a board, a probe, a cable, and a human when the cable falls out. Most teams never get this far: [firmware CI ends at the linker](/blog/firmware-ci-ends-at-the-linker). The map is the HIL slot in [what belongs in CI](/blog/simulation-vs-hil-what-belongs-in-ci). Which is why it belongs above a layer that needs none of those. Zephyr already ships hardware-free targets, and [the difference between native_sim and unit_testing](/blog/zephyr-native-sim) decides how much of your suite never needs a map entry at all. The mechanism was never the hard part. Sixty-seven lines of schema, five required fields, five supported runners, one string compared on both sides. Everything genuinely expensive about hardware testing lives in the layer Zephyr chose not to document, and that is exactly the layer each re-explanation skips. ### Sources [^1]: Zephyr v4.4.1, `scripts/schemas/twister/hwmap-schema.yaml` (67 lines; required fields `connected, id, platform, product, runner`). https://github.com/zephyrproject-rtos/zephyr/blob/v4.4.1/scripts/schemas/twister/hwmap-schema.yaml [^2]: Zephyr v4.4.1, `HardwareMap.load()` in `scripts/pylib/twister/twisterlib/hardwaremap.py:239-293`. https://github.com/zephyrproject-rtos/zephyr/blob/v4.4.1/scripts/pylib/twister/twisterlib/hardwaremap.py#L239-L293 [^3]: Zephyr v4.4.1 documentation, `doc/develop/test/twister.rst:1670-1691` (two-board hardware map example). https://github.com/zephyrproject-rtos/zephyr/blob/v4.4.1/doc/develop/test/twister.rst#L1670-L1691 [^4]: Zephyr v4.4.1, `runner_mapping` in `scripts/pylib/twister/twisterlib/hardwaremap.py:123-139`. https://github.com/zephyrproject-rtos/zephyr/blob/v4.4.1/scripts/pylib/twister/twisterlib/hardwaremap.py#L123-L139 [^5]: Zephyr v4.4.1 documentation, `doc/develop/test/twister.rst:1738-1742` (supported runner list). https://github.com/zephyrproject-rtos/zephyr/blob/v4.4.1/doc/develop/test/twister.rst#L1738-L1742 [^6]: Zephyr v4.4.1 documentation, `doc/develop/test/twister.rst:1751-1777` (Intel ADSP entry and its `west flash` equivalent). https://github.com/zephyrproject-rtos/zephyr/blob/v4.4.1/doc/develop/test/twister.rst#L1751-L1777 [^7]: Zephyr v4.4.1, argparse definitions in `scripts/pylib/twister/twisterlib/environment.py:170-222`, and the documented commands at `doc/develop/test/twister.rst:1605-1732`. https://github.com/zephyrproject-rtos/zephyr/blob/v4.4.1/scripts/pylib/twister/twisterlib/environment.py#L170-L222 [^8]: Zephyr v4.4.1, flash-related flags in `scripts/pylib/twister/twisterlib/environment.py:215-271` (`--device-flash-timeout` default 60s, `--flash-before`, `--flash-command`). https://github.com/zephyrproject-rtos/zephyr/blob/v4.4.1/scripts/pylib/twister/twisterlib/environment.py#L215-L271 [^9]: Zephyr v4.4.1 documentation, `doc/develop/test/twister.rst:1819-1855` (fixtures description and the frdm_k64f `gpio_loopback` example). https://github.com/zephyrproject-rtos/zephyr/blob/v4.4.1/doc/develop/test/twister.rst#L1819-L1855 [^10]: Zephyr v4.4.1, `tests/drivers/uart/uart_async_api/testcase.yaml:9-15`. https://github.com/zephyrproject-rtos/zephyr/blob/v4.4.1/tests/drivers/uart/uart_async_api/testcase.yaml#L9-L15 [^11]: Zephyr v4.4.1 documentation, `doc/develop/test/twister.rst:597-606` (`fixture:` field definition and uniqueness constraint). https://github.com/zephyrproject-rtos/zephyr/blob/v4.4.1/doc/develop/test/twister.rst#L597-L606 [^12]: Zephyr v4.4.1, fixture matching in `scripts/pylib/twister/twisterlib/testinstance.py:220-243` and DUT reservation in `scripts/pylib/twister/twisterlib/hardwaremap.py:488-497`. https://github.com/zephyrproject-rtos/zephyr/blob/v4.4.1/scripts/pylib/twister/twisterlib/testinstance.py#L220-L243 [^13]: Golioth, `zephyr_twister_hil_testing` (Apache-2.0, self-hosted-runner HIL pattern over GitHub Actions). https://github.com/golioth/zephyr_twister_hil_testing [^14]: Mike Szczys, "How to use Twister to cycle test Zephyr devices," February 2024. https://jumptuck.com/blog/2024-02-10-twister-device-testing/ [^15]: Zephyr Project, "What to expect at the Zephyr Project Meetup, June 30 2026, Lyon" (Witekio talk, "Hardware-in-the-Loop at Scale with Zephyr Twister"). https://zephyrproject.org/what-to-expect-at-the-zephyr-project-meetup-june-30-2026-in-lyon-france/ [^16]: Zephyr Project, "How Zephyr simplified pre-silicon and production firmware development" (case study; the Raspberry Pi Pico appears as a device under test, not a lab host). https://zephyrproject.org/how-zephyr-simplified-pre-silicon-and-production-firmware-development/ --- ## RTT vs SWO vs UART: choose the constraint, not the tutorial (https://veecle.ai/blog/rtt-vs-swo-vs-uart, 2026-08-17) Ask an engineer why their firmware prints over UART and you usually get a shrug. Ask a coding agent and you get the same answer with more confidence, because the last example in its context window used it. Four common Cortex-M debug-output paths are UART, RTT, SWO, and semihosting, and they are not variations on a theme. One stops your program on every message. One needs a pin that vendor eval kits fail to route even when the probe and the core both support it. One needs no pin at all and reads your RAM while the core runs. One is fully specified as a protocol and completely unowned as debug infrastructure. Choosing without knowing which is which is how you debug a timing bug through a transport that changes the timing. ### The table | | halts the CPU? | bandwidth | hardware needed | extra pin? | under emulation | | --------------- | --------------- | ---------------------------------------------------- | ---------------------------------------------- | -------------------------- | --------------------------- | | **UART** | no | baud rate | a free UART peripheral, plus a serial adapter | yes, TX (and RX for input) | yes | | **SEGGER RTT** | depends on mode | up to ~3.5 MB/s | debug probe with background memory access | no | needs a host-side reader | | **SWO / ITM** | no | prescaler off the core clock; ~1.5 µs/char at 10 MHz | probe with SWO capture, and a core that has it | yes, TRACESWO | only if the sink is modeled | | **Semihosting** | yes, every call | milliseconds to >100 ms per message | a debug agent, nothing else | no | yes, by design | Every row has a caveat that matters more than the row itself. ### RTT has three modes and only one of them is free RTT puts a control block in target RAM. The host probe locates it by scanning for an ID string, then reads and writes ring buffers, one per channel.[^1] Your firmware's write is a `memcpy`. That is the whole target-side cost. Every summary flattens this: RTT runs in one of three modes. **Background mode** is the fast one, up to about 3.5 MB/s, available on all current J-Link and J-Trace Pro models. **Legacy background mode** is the same idea at lower speed on older probe firmware. **Stop mode** is what SEGGER calls "Pseudo RTT mode": the CPU is halted to read data, and SEGGER documents that it was introduced for CPUs that do not support background access, and that it affects real-time behavior.[^2] So "RTT never halts the core" holds right up until your part cannot do background memory access, at which point RTT halts the core and you have bought semihosting with extra steps. Throughput scales with the probe, not just the buffer: SEGGER's own numbers put Background mode up to roughly 3.5 MB/s, with Legacy background mode lower on older probe firmware.[^2] On an STM32F407 at 168 MHz, a line of output costs about a microsecond or less.[^3] And no extra pin. SEGGER is explicit: RTT needs no additional pin or hardware beyond a probe on the standard debug port.[^1] ### "You need a J-Link" is true of the tooling, not of the protocol SEGGER states the requirement as any J-Link plus any supported target that allows background memory access.[^1] That describes SEGGER's stack. It is not a description of the protocol, which is a documented RAM ring-buffer format that three independent projects have reimplemented against CMSIS-DAP-class probes: - OpenOCD ships `rtt setup address size [ID]`, `rtt start`, and `rtt server start port channel`, over any adapter OpenOCD supports.[^4] - pyOCD added RTT and SystemView support with a first-class session option, `-O rtt=...`.[^5][^6] - probe-rs ships RTT support directly in its `rtt` module now. The standalone `probe-rs-rtt` crate was folded back into the main repo.[^7] Practical version: with a J-Link, use SEGGER's stack. With an ST-Link on a dev board, RTT is still on the table, not with SEGGER's tools or support. ![Diagram of four Cortex-M debug transports showing UART using two pins, SWO using the TRACESWO pin, and RTT and semihosting riding the existing debug port](/blog/rtt-vs-swo-vs-uart/image-1.webp) ### SWO is gated in three separate places Serial Wire Output is Arm-architected infrastructure, not a bolt-on. Arm describes it as a trace sink similar to the TPIU that can trace exactly one source, the ITM, and outputs the stream off-chip through a single-pin interface; SWO plus its 8-bit ATB slave interface to the ITM are collectively the Serial Wire Viewer.[^8] Physically it is ten-bit packets with start and stop bits, in Manchester or UART encoding, and the capture device is expected to clock at the same speed as the TRACESWO pin.[^9] Arm's tooling docs name the use case: ITM stimulus ports exist for `printf`-style debugging without a communication interface like a UART.[^10] Bandwidth is a prescaler off the core clock. `TPIU_ACPR` sets the divisor for the SWO baud rate.[^11] You are not choosing a speed, you are choosing a division of whatever the core runs at, so a clock change moves your trace port with it. The concrete cost, from SEGGER's measurements on that same STM32F407: roughly 1.5 µs per character at 10 MHz, about 120 µs for an eighty-character line.[^3] Two orders of magnitude slower per line than RTT. Then the three gates. Your probe has to support SWO capture, which Arm's own CMSIS-DAP specification treats as optional: separate capability bits report SWO UART, SWO Manchester, and SWO streaming trace.[^12] Your silicon has to support it, and pyOCD says the quiet part plainly: Armv7-M and Armv8-M Mainline support SWO, while Armv6-M and Armv8-M Baseline do not, so Cortex-M0, M0+, M1 and M23 are out.[^13] And the board has to have routed the signal to the debug header, which pyOCD notes fails in a surprising number of cases, including on silicon vendor evaluation kits where probe and MCU both support it.[^13] Three independent things have to be true. A tutorial that worked for its author tells you nothing about whether they are true for you. ### Semihosting stops the program, and that is the whole story Semihosting is a trap. On M-profile Thumb the instruction is `BKPT #0xAB`, opcode `0xBEAB`; A64 uses `HLT #0xF000`, and A32 can use either `SVC #0x123456` or `HLT #0xF000`.[^14] The operation number goes in a register, and the set is a small POSIX-shaped file API: `SYS_OPEN` (0x01), `SYS_WRITEC` (0x03), `SYS_WRITE0` (0x04), `SYS_WRITE` (0x05), `SYS_READ` (0x06), `SYS_READC` (0x07), up through `SYS_EXIT_EXTENDED` (0x20).[^14] SEGGER's description of the mechanism is the clearest one I have found: the target CPU is halted, the debug agent takes control and performs the action, then restarts the target, and the target stays halted for the duration of the operation.[^15] The price tag is in the same vendor's measurements: one message can take several milliseconds to more than a hundred.[^3] A hundred milliseconds of stopped core inside a control loop is not logging, it is a fault injection experiment you did not mean to run. What semihosting buys you is that it needs nothing. No pin, no peripheral, no trace unit, no capture-capable probe. And it works under emulation: Zephyr documents it as a mechanism for code on Arm, RISC-V and Xtensa targets to use the host's I/O facilities under a debugger or an emulator.[^16] ![Timeline diagram contrasting continuous execution under RTT background mode and SWO with the halted execution gaps semihosting introduces at every call](/blog/rtt-vs-swo-vs-uart/image-2.webp) ### Nobody owns UART, so nobody writes the fourth column Here is the finding I care about most. I went looking for the document that compares these four. The closest thing I found is a SEGGER blog post, and it naturally favors RTT. Arm documents SWO, ITM, the TPIU and semihosting thoroughly, because Arm architected all of them. Arm's documentation never mentions RTT and never will: RTT is SEGGER's technology, not part of the architecture. Arm could not produce a four-way matrix even in principle. SEGGER can, and their blog post on getting `printf` output off a target is the closest thing that exists: UART and CDC, semihosting, SWO and RTT in one place.[^3] It is also SEGGER's blog, and it concludes that there is no reason to use SWO anymore when you can use RTT instead. UART gets a single sentence, which does at least name the real tradeoff: a UART may already be used by the application, and USB CDC requires a stack and a connector on the target.[^3] One dismissive sentence is not a comparison row. That is the structural problem. Each vendor documents the transport it owns. Nobody owns UART. It is a convention, not architected debug infrastructure, so there is no single Cortex-M debug-output specification or vendor responsible column. You can watch a project work around the pin contention instead: Zephyr's GDB stub docs tell you to point `zephyr,gdbstub-uart` at a spare UART so `printk` and log messages do not collide with GDB, and say that on boards with one UART you must disable `printk` and logging entirely.[^17] That is the fourth column, written down as a workaround in someone else's feature docs. The matrix is scattered by construction. Which is why engineers and agents default to whatever the last tutorial used. They are not being lazy. They went looking for the comparison, found four vendor pages each arguing for its own transport, and copied the example instead. ![Grid graphic showing that Arm documents SWO and semihosting but never RTT, while SEGGER documents RTT and gives UART a single sentence, leaving no complete four-way comparison](/blog/rtt-vs-swo-vs-uart/image-3.webp) ### The emulation column changes the ranking Delete the board and the ordering inverts. Semihosting, the worst transport on hardware, becomes one of the easiest to get working, because a trap handled by a debug agent is a trap handled by an emulator.[^16] UART stays available, since a UART peripheral is one of the first things any machine model implements. SWO depends on whether the ITM and its trace sink were modeled at all. RTT is the interesting one. The target side works fine under emulation, since it is just RAM. What disappears is the host-side reader polling that control block while the core runs. I wrote up that transport's format story separately in [the RTOS trace post](/blog/rtos-trace-simulator), including what came back when I ran Zephyr's CTF trace over UART on a simulated board. Which is why [UART still wins](/blog/uart-still-wins) more often than it should on paper. It is the only one of the four with no probe requirement, no silicon requirement, no capability bit and no vendor. ### Pick on the constraint, not on the example Four questions, in order. Does the transport get to stop my core? Do I have a pin to spare? Do I have a probe with the right capability, and does my part have the peripheral at all? Am I running this on a model, on silicon, or both? Answer those and the choice is usually forced. Semihosting for bring-up and for simulated CI where nothing has a deadline. SWO when you have a v7-M or v8-M Mainline part, a routed pin and a capable probe. RTT when you have background access and need throughput. UART when you want the thing that works everywhere. None of that is hard. It is never written down in one place, so a choice with real timing consequences gets made by whichever page came up first. Simulation moves the tradeoffs around, and there is a separate list of things [it still cannot catch](/blog/what-simulation-cant-catch), but it does not remove them. It changes which column you read. ### Sources [^1]: SEGGER, "About Real Time Transfer." https://www.segger.com/products/debug-probes/j-link/technology/about-real-time-transfer/ [^2]: SEGGER Knowledge Base, "RTT," §RTT operation Modes and buffer/speed figures. https://kb.segger.com/RTT [^3]: SEGGER Blog, Johannes Lask, "Getting printf Output from Target to Debugger," 2016-10-21, updated 2022-10-14. https://blog.segger.com/getting-printf-output-from-target-to-debugger/ [^4]: OpenOCD User's Guide, §15.6 "Real Time Transfer (RTT)." https://openocd.org/doc/html/General-Commands.html [^5]: pyOCD documentation, "Session options" (`rtt` option). https://pyocd.io/docs/options.html [^6]: pyOCD v0.43.0 release notes, "Add support for SEGGER's RTT and SystemView." https://github.com/pyocd/pyOCD/releases/tag/v0.43.0 [^7]: `probe-rs`, `rtt` module documentation, "Host side implementation of the RTT (Real-Time Transfer) I/O protocol over probe-rs". The standalone `probe-rs-rtt` crate now redirects here. https://docs.rs/probe-rs/latest/probe_rs/rtt/index.html [^8]: Arm, CoreSight Technology System Design Guide (DGI0012), §Trace sinks. https://developer.arm.com/documentation/dgi0012/d/CoreSight-Components-and-Systems/CoreSight-components/Trace-sinks [^9]: Arm, CoreSight Components Technical Reference Manual (DDI0314H), §Serial Wire Output, physical pin protocol. https://developer.arm.com/documentation/ddi0314/h/Serial-Wire-Output/SWO-trace-port/Physical-pin-protocol [^10]: Arm, µVision User's Guide (101407), "Trace Features." https://developer.arm.com/documentation/101407/latest/Debugging/Code-and-Data-Trace--Cortex-M-/Trace-Features [^11]: Armv7-M Architecture Reference Manual (DDI0403E), §C1.10 Trace Port Interface Unit: `TPIU_ACPR` is the SWO baud-rate prescaler. https://developer.arm.com/documentation/ddi0403/latest [^12]: Arm, CMSIS-DAP `DAP_Info` capabilities and SWO commands. https://arm-software.github.io/CMSIS-DAP/latest/group__DAP__Info.html and https://arm-software.github.io/CMSIS-DAP/latest/group__DAP__swo__gr.html [^13]: pyOCD documentation, "SWO/SWV" (probe, MCU, architecture and board routing requirements). https://pyocd.io/docs/swo_swv.html [^14]: Arm, ABI for the Arm Architecture, `semihosting.rst` (trap instructions and operation numbers). https://github.com/ARM-software/abi-aa/blob/main/semihosting/semihosting.rst [^15]: SEGGER Knowledge Base, "Semihosting" (SEGGER's description of the halt-and-resume mechanism). https://kb.segger.com/Semihosting [^16]: Zephyr documentation, "Semihosting Guide." https://docs.zephyrproject.org/latest/hardware/arch/semihost.html [^17]: Zephyr documentation, "GDB stub" (`zephyr,gdbstub-uart` and single-UART boards). https://docs.zephyrproject.org/latest/services/debugging/gdbstub.html --- ## Your rust embedded unit test runs on the host. The Book barely mentions it. (https://veecle.ai/blog/rust-embedded-unit-test, 2026-08-17) `cargo test` in an embedded Rust project either refuses to compile or passes suspiciously fast. Both outcomes confuse people, and both have the same cause: nobody said which machine the test was going to run on. Zephyr has [native_sim and unit_testing](/blog/zephyr-native-sim): two host binaries, two different machines, and a docs page that had to print the words "fundamentally different" because engineers kept treating them as two spellings of the same thing. Rust has the same split. It has far fewer words written about it. Plain `cargo test` on a host target is the `unit_testing`-shaped thing: a native x86 or ARM64 process, `std` present, the libtest harness, no board, no probe. `cargo build --target thumbv7em-none-eabihf` with no test run is a different, middle thing: it proves the code compiles for the architecture, nothing about runtime behavior. With a device-side harness and `probe-rs run` configured as the target runner, `cargo test` can flash and execute the test binary on silicon. Three checks, three different kinds of evidence. ### What the Book says about this Close to nothing. The Embedded Rust Book has no unit-testing chapter. The nearest thing is a single bullet in the Portability chapter, listing the flavours a HAL implementation can come in: low-level hardware access via registers, an operating system such as sysfs under Linux, "via adapter, e.g. a mock of types for unit testing", and via a driver for hardware adapters like an I2C multiplexer.[^1] That is the official treatment. A mock-adapter pattern, mentioned in passing, inside a chapter about something else. I am not going to inflate that into "the Book already explains the host/target split." It does not. It gestures at swapping the hardware layer for something host-friendly, the same idea the C world argues about every year in [CMock is not a simulator](/blog/mocking-stm32-hal-unit-test), and stops. It does not tell you that `cargo test` will fail to build, why it fails, or what to type instead. Someone asked for precisely that. A 2023 issue on `rust-embedded/discovery`, the Book's companion tutorial project, reads: "its gotten to the point where I need to unit test some of my non-hardware dependent functionality... How should I set up unit tests for testing individual modules in embedded projects? Examples would be massively appreciated."[^2] The gap is known. It is still a gap. So the explaining happened elsewhere: on Ferrous Systems' blog, in Embassy's CI scripts, and in forum threads where people re-derive the answer one at a time. That pattern is the same shape as the Zephyr confusion: thin official docs, community filling in by hand, everyone arriving surprised. It is most of why this post exists. ![Two-column diagram contrasting cargo test running on the host with cargo test using probe-rs run to flash and execute on a real microcontroller](/blog/rust-embedded-unit-test/image-1.webp) ### Why cargo test fails at all The mechanism is boring, which is why it catches people. `cargo test` links libtest. libtest depends on `std`. The `defmt-test` README says it in one line: the default test harness, the test crate, depends on `std`.[^3] Your project almost certainly has a `.cargo/config.toml` with something like `[build] target = "thumbv7em-none-eabihf"`. Cargo applies that default target to every subcommand, not just `build`. So `cargo test` tries to compile a std-dependent harness for a bare-metal triple, and you get `error[E0463]: can't find crate for 'test'`. A 2019 internals thread has the cleanest statement of the problem I have found: "Rust's test library requires the standard library so we can't use it in no_std environments. But what we can do is to run platform independent unit tests on the host system... The problem is that cargo uses the specified default target for all subcommands and tries to compile the std-dependent tests for our bare metal system, which of course fails."[^4] People hit this and reach for an on-target harness, because the error mentions the target and they assume the target is required. Usually it is not. A user in 2020 worked it out mid-thread: "Ah yes — I see now that I've got a .cargo/config file that declares a default build target. Performing `cargo test --target x86_64-apple-darwin` does indeed work."[^5] A 2023 answer states the rule better than any doc page: for a lib crate, if your tests can run on the host, meaning you are not testing hardware-specific functionality, the default libtest harness works fine, and you only need `defmt-test` when the test has to execute on the target architecture.[^6] Ferrous Systems drew the same line back in 2021: functional, IO-less code gets tested on the host with the built-in `#[test]` machinery, and code that interacts with hardware gets tested on the target with `defmt-test`.[^7] That is the whole taxonomy. It lives on a consultancy blog, not in the Book. The mechanical fix is one attribute. `#![cfg_attr(not(test), no_std)]` makes the crate `no_std` for normal builds and lets `std` back in under `cfg(test)`, so the same source compiles two ways. Combine it with an explicit `--target `, or split the register-touching code into its own crate away from the logic: PAC access, cortex-m intrinsics, interrupt vectors.[^7] ![Annotated diagram showing how cfg_attr(not(test), no_std) lets one crate compile as no_std firmware and as a std host test binary](/blog/rust-embedded-unit-test/image-2.webp) ### Embassy already sorted this out in public The best documentation of the Rust host/target split is not prose. It is a shell script. Embassy's `.github/ci/test.sh` runs plain `cargo test` on the host, and not only against pure-logic crates: ```bash cargo test --manifest-path ./embassy-executor/Cargo.toml --features metadata-name cargo test --manifest-path ./embassy-sync/Cargo.toml cargo test --manifest-path ./embassy-time/Cargo.toml --features mock-driver,embassy-time-queue-utils/generic-queue-8 cargo test --manifest-path ./embassy-nrf/Cargo.toml --no-default-features --features nrf52840,time-driver-rtc1,gpiote cargo test --manifest-path ./embassy-stm32/Cargo.toml --no-default-features --features stm32f429vg,time-driver-any,exti,single-bank,low-power,chrono,test ``` Those last two lines are the interesting ones. `embassy-nrf` with `nrf52840` and `embassy-stm32` with `stm32f429vg` are chip HAL crates, compiled and tested on a CI runner with no nRF and no STM32 attached.[^8] The trick is the first line of `embassy-stm32/src/lib.rs`: `#![cfg_attr(not(test), no_std)]`.[^9] An Embassy maintainer spelled out the recipe to a user who filed an issue titled "Any guidance on the correct way to allow cargo test to work?": make the crate `no_std` only when not testing with `#![cfg_attr(not(test), no_std)]`, accept that this builds embedded-only libs for x86 and that this might or might not work, and adjust dependency features: `embassy-nrf` needs `default-features = false` to disable `rt`, because that is how it is tested in CI.[^10] "Might or might not work" is the honest part. Host-testing a HAL crate gets you the type system, the state machines, the arithmetic. It does not get you the peripheral. Which is why Embassy keeps the other half somewhere else: `tests/stm32/`, `tests/nrf/`, `tests/rp/`, flashed onto a hardware farm by `teleprobe`, Embassy's own wrapper that uses probe-rs underneath for flashing, attaching and decoding RTT.[^11] A review comment on one PR makes the division sound as ordinary as it should: can you add on-hardware tests for HASH here, this is a prime candidate for testing since it needs no connections to external hardware.[^11] Two directories. Two judges. Nobody in that repo confuses them. ![Diagram of Embassy's split between HAL crates tested on the host in CI and tests/stm32 flashed to real boards through teleprobe and probe-rs](/blog/rust-embedded-unit-test/image-3.webp) ### The target side, precisely There is no `probe-rs test`. `probe-rs` has `run`, `attach`, and a handful of other device commands, but no subcommand dedicated to running tests.[^12] You point Cargo at `probe-rs run` as the target runner, so `cargo test` hands it a binary the way it would hand one to any runner: ```toml [target.thumbv7em-none-eabihf] runner = 'probe-rs run --chip STM32F767ZITx' ``` `probe-rs run` flashes, resets, starts the binary and streams RTT and defmt output plus panics back to your console, for any embedded binary in any language.[^12] `probe-rs attach` is the same thing without the reset and without flashing. The maintainers' current guidance is short: prefer `probe-rs run` for new setups, `cargo-embed` adds an interactive RTT terminal but is expected to be phased out, and `cargo-flash` just flashes.[^13] All of it needs a physical debug probe translating host USB or Ethernet into SWD or JTAG.[^13] `defmt-test` is the Knurling harness for the device side: a test harness that lets you write and run unit tests on your device as if you were using the built-in `#[test]` attribute. You put `#[defmt_test::tests]` on a `mod`, write `#[test]` functions inside, and you get `#[init]`, `#[before_each]`, `#[after_each]`, `#[teardown]` and `#[should_error]`. You must set `harness = false` in `Cargo.toml`, because the default harness needs `std`.[^3] `embedded-test`, from the probe-rs org, makes the host half explicit instead of implicit. It is a libtest-compatible runner where, for each test case, probe-rs resets the device, signals which test to run via semihosting `SYS_GET_CMDLINE`, and waits for the device to report success or failure via `SYS_EXIT`. `probe-rs run` autodetects whether the ELF is normal firmware or a test binary.[^14] And it is not theoretical. rp-hal's migration off the deprecated `probe-run` shows `cargo test` reaching silicon:[^15] ``` $ CARGO_TARGET_THUMBV6M_NONE_EABI_RUNNER="probe-rs run" cargo test --test dma_m2m_u8 -- --chip rp2040 Finished test [optimized + debuginfo] target(s) in 0.03s Running tests/dma_m2m_u8.rs (target/thumbv6m-none-eabi/debug/deps/dma_m2m_u8-ebcd141c1ecc1d99) Erasing sectors ✓ ... ``` "Erasing sectors" is the line that tells you which category you are in. ### What each pass buys you A green host `cargo test` says your logic is correct on x86, your state machines step the way you asserted, your parsers parse, your arithmetic does not overflow, and, if you did the Embassy thing, large parts of a HAL crate compile and behave for a chosen chip feature set. That is a lot, it runs in seconds, it needs no board, and you get the whole host toolchain: coverage, sanitizers, a real debugger. It says nothing about a register write, a peripheral, an interrupt latency, or the image you are about to ship. Not because host testing is weak, but because that binary was never built for your target. A green `probe-rs run` pass says the flashed image did the thing on that specific chip, once, on the desk it was attached to. Both belong in CI. Most pipelines have neither: [firmware CI ends at the linker](/blog/firmware-ci-ends-at-the-linker) for a depressing share of teams, and "it compiles for thumbv7em" is not a test result. The short version: pure logic goes to host `cargo test`. Target compatibility goes to a target-triple build with no run. Peripheral or interrupt behavior goes to an on-device harness through `probe-rs run`. The one thing to stop doing is what the Zephyr crowd does with `native_sim`: running the fast one and reporting it as the slow one. Rust makes that easier to do by accident, because both invocations start with `cargo test`. Zephyr eventually wrote "fundamentally different" into its docs. The Embedded Rust Book has a bullet about mock adapters. The community filled the rest in by hand, one forum thread at a time, and that is worth naming rather than pretending the docs cover it. ### Sources [^1]: The Embedded Rust Book, "Portability," §HAL implementation, accessed 2026-08-21. https://docs.rust-embedded.org/book/portability/index.html [^2]: rust-embedded, "Unit testing?," discovery issue 544, 2023-10-28. https://github.com/rust-embedded/discovery/issues/544 [^3]: Knurling / Ferrous Systems, "defmt-test," README, accessed 2026-08-21. https://github.com/knurling-rs/defmt/blob/main/firmware/defmt-test/README.md [^4]: internals.rust-lang.org, "Set default target for cargo build but not for cargo test," 2019-04-06. https://internals.rust-lang.org/t/set-default-target-for-cargo-build-but-not-for-cargo-test/9777/1 [^5]: users.rust-lang.org, "Running doc and unit tests for nostd," 2020-10-16. https://users.rust-lang.org/t/running-doc-and-unit-tests-for-nostd/50179 [^6]: users.rust-lang.org, "Cargo test for embedded target fails," 2023-08-25. https://users.rust-lang.org/t/cargo-test-for-embedded-target-fails/98926 [^7]: Ferrous Systems, "Testing an embedded application," 2021. https://ferrous-systems.com/blog/test-embedded-app/ [^8]: Embassy, host-side CI script `.github/ci/test.sh`, commit 63e74af. https://github.com/embassy-rs/embassy/blob/63e74af9badbb4e82580e3121c4b341dcf63d5cc/.github/ci/test.sh [^9]: Embassy, `embassy-stm32/src/lib.rs` line 1, commit 63e74af. https://github.com/embassy-rs/embassy/blob/63e74af9badbb4e82580e3121c4b341dcf63d5cc/embassy-stm32/src/lib.rs#L1 [^10]: Embassy, "Any guidance on the correct way to allow cargo test to work?," issue 1564, 2023-06-16. https://github.com/embassy-rs/embassy/issues/1564 [^11]: Embassy, pull request 2528, and the `teleprobe` repository. https://github.com/embassy-rs/embassy/pull/2528 and https://github.com/embassy-rs/teleprobe [^12]: probe-rs, "probe-rs CLI," documentation, accessed 2026-08-21. https://probe.rs/docs/tools/probe-rs/ [^13]: probe-rs, crates.io README, and "About probe-rs." https://crates.io/crates/probe-rs and https://probe.rs/docs/overview/about-probe-rs/ [^14]: probe-rs, "embedded-test" v0.7.1 documentation. https://docs.rs/embedded-test/latest/embedded_test/ and https://github.com/probe-rs/embedded-test/ [^15]: rp-hal, "Migrate from probe-run to probe-rs run," pull request 698, and Ferrous Systems, "probe-run deprecation." https://github.com/rp-rs/rp-hal/pull/698 and https://ferrous-systems.com/blog/probe-run-deprecation/ --- ## Stop grepping Renode stdout. It already ships a test runner. (https://veecle.ai/blog/renode-robot-framework, 2026-08-17) I keep running into a pattern in firmware repos that use Renode. Someone writes a shell script. The script launches `renode` with a chain of `-e` monitor commands, waits an arbitrary number of seconds, pipes stdout somewhere, and greps for a string. It works on the author's machine. It gets called from a Makefile. Renode Robot Framework integration has shipped for years: a remote-library server, a keyword library implemented in C#, a packaged `renode-test` command, and 280-plus real `.robot` files across its own repo. This is not a hidden feature. It is documented on the front page of the README. ### The README names it before it names anything else Renode's own README, on the prerequisites list: "Follow the Additional Prerequisites section if you wish to use Robot Framework for testing." Then again, under testing: "To write and run test cases, Renode integrates with the Robot testing framework."[^1] The official docs are more explicit. "Renode is integrated with the Robot Framework testing suite and provides user-friendly scripts for running tests." And: "Running a robot test script in Renode is as simple as executing a single command."[^2] That single command is `renode-test`. It is a 26-line shell script at the repo root, with a `renode-test.bat` beside it for Windows. It locates `tests/tests.yaml`, sets up an output directory, and hands off to `tests/run_tests.py`.[^3] The macOS package even ships an alias suggestion, `alias renode-test='/Applications/Renode.app/Contents/MacOS/renode-test'`, which tells you this is a supported, packaged entry point, not a developer convenience left in the tree.[^1] What it does when you run it, per the docs: starts a Renode instance in the background, enables Renode's built-in Robot Framework server on port 9999, starts the Robot Framework test engine, connects it to Renode, runs the test case, and generates a log and a summary.[^2] Five things. The bash script does two of them badly. ### A real test file from Renode's own repo This is `tests/platforms/STM32F7.robot`, verbatim, first 32 lines, at the tip of master:[^4] ```robot *** Variables *** ${UART} sysbus.usart1 ${URL} https://dl.antmicro.com/projects/renode *** Test Cases *** Run Mbed-OS Hello World Execute Command set bin @${URL}/renode-mbed-pipeline-helloworld.elf-ga2ede71-s_2466384-6e3635e4ed159bc847cf1deb3dc7f24b10d26b41 Execute Command include @scripts/single-node/stm32f746_mbed.resc Execute Command showAnalyzer ${UART} Create Terminal Tester ${UART} Start Emulation Wait For Line On Uart HELLO WORLD MBED+RENODE Provides hello-world Wait For Message on LTDC [Tags] non_critical Requires hello-world Execute Command emulation CreateFrameBufferTester "fb_tester" 10 Execute Command fb_tester AttachTo sysbus.ltdc Execute Command fb_tester WaitForFrame @${URL}/mbed-stm32f7.png-s_4651-99842c172e660e408b2197e48c8e9dccd7948421 ``` Read what that buys over a shell script. `Wait For Line On Uart` blocks until a string appears or a timeout expires. It is an assertion. Pass or fail, with a reason. The bash version is `sleep 5` followed by `grep`, which fails differently depending on how loaded the machine is. `Create Terminal Tester` binds a tester object to a specific UART. Not to combined stdout. Not to whatever the terminal happened to receive. A named peripheral. `Provides` and `Requires` order test cases against each other, so the framebuffer check runs against state the first case established, and gets skipped, not falsely failed, if the first case never got there. `[Tags] non_critical` lets it fail without failing the suite. A shell script's exit code has no room for either. And `Execute Command` is the escape hatch, right there in the same file. Anything you would have typed into the Monitor still works. You are not giving up the interactive surface. You are wrapping it in something that reports. ![Diagram showing a robot test file driving Renode through the Robot Framework engine over XML-RPC on port 9999 into Renode's C# keyword providers and the Monitor](/blog/renode-robot-framework/image-1.webp) ### The keywords are C# methods, not string templates The interesting part is what sits behind port 9999. `src/Renode/RobotFrameworkEngine/` holds the implementation. `RobotFrameworkEngine.cs` registers keyword providers and runs an XML-RPC server on the given port.[^5] A `RobotFrameworkKeywordAttribute` marks a C# method as a Robot keyword, and Robot Framework's standard remote-library name matching does the rest: `WaitForLineOnUart` becomes `Wait For Line On Uart`, case and spaces ignored. `Execute Command` is thin on purpose:[^6] ```csharp [RobotFrameworkKeyword] public string ExecuteCommand(string command, string machine = null) { var interaction = monitor.Interaction as CommandInteractionWrapper; interaction.Clear(); SetMonitorMachine(machine); if(!monitor.Parse(command)) { throw new KeywordException(...); } return interaction.GetContents(); } ``` Same `monitor.Parse` a human hits when typing interactively, same parser as the `-e` flags on the CLI. Bash wrappers and Robot suites drive identical machinery. The difference is entirely in what comes back out. `Wait For Line On Uart` is where that shows:[^7] ```csharp [RobotFrameworkKeyword] public TerminalTesterResult WaitForLineOnUart(string content, float? timeout = null, int? testerId = null, bool treatAsRegex = false, bool includeUnfinishedLine = false, bool? pauseEmulation = null, bool? matchNextLine = null) { return DoTest(timeout, testerId, (tester, timeInterval) => { var result = tester.WaitFor(content, timeInterval, treatAsRegex, includeUnfinishedLine, ...); if(result?.IsFailingString == true) { throw new InvalidOperationException($"Terminal tester failed!\n\nTest failing entry has been found on UART:\n{result.Line}"); } return result; }); } ``` Explicit timeout. Optional regex. A tester id, so a multi-UART board can be asserted on per port. `pauseEmulation`, so the simulation stops the instant the line matches instead of drifting past it. A failing-string concept, so a known panic message aborts the test immediately rather than burning the full timeout. None of that is expressible in `grep`. The keyword layer is documented too: start the emulation with `Start Emulation`, clear it with `Reset Emulation`, run Monitor commands with `Execute Command`, allocate scratch files with `Allocate Temporary File`, pull inputs in with `Download File`.[^2] The runner has the flags you would expect from something built for CI: ``` $ renode-test my_test.robot $ renode-test my_tests.robot additional_tests.robot extra_tests.robot # aggregated report $ renode-test -t my_tests.yaml # manifest $ renode-test -j12 -t my_tests.yaml # parallel across files $ renode-test --stop-on-error my_tests.robot $ renode-test -f "*GDB*" my_tests.robot # filter fixtures $ RENODE_CI_MODE=YES renode-test my_test.robot # snapshot failed tests $ renode-test --debug-on-error my_test.robot # interactive Monitor on failure ``` Parallelism, filtering, aggregated reports, failure snapshots, and a drop-into-the-Monitor mode for when the report is not enough.[^2] You can build all of that on top of a bash wrapper. It will take you a quarter and it will be worse. ### The pattern that skips all of it Real examples, both public, both from projects I respect. RIOT-OS added board reset support for emulated targets by scripting the Monitor over telnet:[^8] ``` bash -c "{ sleep 0.2;echo machine RequestReset; } | telnet localhost 1234" || true ``` Invoked as `EMULATE=1 make BOARD=hifive1b -C examples/hello-world reset`. A 200ms sleep, a raw socket, and `|| true` swallowing whatever happened. PlatformIO's Renode integration wires the whole upload and debug flow through chained `-e` commands:[^9] ``` upload_command = renode -e "include @scripts/single-node/sifive_fe310.resc" -e "machine StartGdbServer 3333 True" -e "sysbus LoadELF @$SOURCE" -e "start" ``` No Robot Framework anywhere in either. These are automation scripts that reach for the raw Monitor instead of the test layer Renode already ships. Neither claims to be a test suite, and both are reasonable for their scope. The problem starts when a repo grows assertions on top of that shape, because the shape has nowhere to put them. Renode's own maintainers sometimes answer scripting questions the same way. Issue 344, "Pass a value to a RESC script," gets answered with `renode -e '$bin=@path; include @script.resc'`. No pointer to Robot Framework.[^10] Which is the right answer to that question. The ad-hoc path is not a user failing. It is always available, it is often the correct tool, and nobody is steering you off it. That is exactly why it wins by default. A one-off Monitor command is genuinely faster to write in shell. Robot starts paying for itself the moment a script grows assertions, timeouts, dependencies, fixture filtering, retries, or a report someone other than the author has to read. ![Side-by-side comparison of a raw telnet Monitor command with no assertion against the equivalent Robot Framework terminal tester keywords with timeout and pass or fail semantics](/blog/renode-robot-framework/image-2.webp) ### What actually runs `.robot` suites at scale I could not verify that the current public `renode/renode` GitHub Actions configuration gates changes on the Robot suite. At the commit these citations are pinned to, `.github/workflows/` does not contain a robot-test job, and the repo's history around that directory is non-linear enough that I would not trust a snapshot of it either way. The documented public path is Antmicro's own `renode-test-action`. What is solidly documented is better evidence anyway. Antmicro, the same organization that builds Renode, publishes a first-party GitHub Action, `antmicro/renode-test-action`, linked from Renode's own testing docs. Its description: "A GitHub Action for testing embedded software in the Renode simulation environment using the Robot Framework... This action allows you to write a test in Robot using Renode's predefined keyword library and execute them automatically in GitHub Actions."[^11] The usage is three lines: ```yaml steps: - uses: antmicro/renode-test-action@v5 with: renode-revision: "master" tests-to-run: "tests/**/*.robot" ``` Then the large-scale case. Antmicro's Renodepedia CI "executes thousands of jobs" and publishes "a nicely styled Robot Test Suite Log" as a build artifact per platform.[^12] The related dashboards keep growing: 926 of 1513 platforms passing on the Zephyr dashboard, and 829 of 1373 on the U-Boot dashboard.[^13] Each platform's result is a structured test report a machine can read and diff. That is what makes the fan-out inspectable and comparable. ![Diagram contrasting a single ad-hoc shell script on one laptop with robot test files fanned out by Renode's GitHub Action across hundreds of dashboard platforms](/blog/renode-robot-framework/image-3.webp) ### Why I care more than a human would An agent writing firmware tests needs a result it can parse. Not a log it has to interpret. `renode-test` produces Robot Framework's XML output and HTML report, which means pass, fail, skip, per keyword, with timings. A grep-based script produces an exit code and a wall of text; a minimal grep wrapper usually has to add its own timeout, failure classification, and structured reporting on top. This is the same line I keep drawing about where firmware pipelines stop: [firmware CI ends at the linker](/blog/firmware-ci-ends-at-the-linker) for a depressing share of teams, and adding execution without adding assertions just moves the stopping point. Simulation is the layer that can sit in [that CI slot](/blog/simulation-vs-hil-what-belongs-in-ci); wrapping `renode` in bash is how you occupy it without getting a result. [What Renode models, versus QEMU](/blog/renode-vs-qemu-what-each-models), is a different question. None of that changes the small thing this post is about. The test layer exists. It is packaged, documented, upstream, exercised by 280-plus files in the repo you already cloned, and wrapped in a GitHub Action by the people who wrote the simulator. If you are wrapping `renode` in bash and grepping stdout, you did not choose that. You just never read the second half of the README. ### Sources [^1]: Renode README at commit `091eb1a`, Robot Framework prerequisites and testing sections, plus the macOS `renode-test` alias. https://github.com/renode/renode/blob/091eb1aca70127a5508573b3f452fdf40eca2ce9/README.md [^2]: Renode documentation, "Testing with Renode", Robot Framework integration, `renode-test` behavior, keyword list, and runner flags. https://renode.readthedocs.io/en/latest/introduction/testing.html [^3]: Renode `renode-test`, 26-line shell entry point that locates `tests/tests.yaml` and hands off to `tests/run_tests.py`. https://github.com/renode/renode/blob/091eb1aca70127a5508573b3f452fdf40eca2ce9/renode-test [^4]: Renode `tests/platforms/STM32F7.robot`, lines 1-32, quoted verbatim above. https://github.com/renode/renode/blob/091eb1aca70127a5508573b3f452fdf40eca2ce9/tests/platforms/STM32F7.robot#L1-L32 [^5]: Renode `src/Renode/RobotFrameworkEngine/RobotFrameworkEngine.cs`, keyword provider registration and the XML-RPC server. https://github.com/renode/renode/blob/091eb1aca70127a5508573b3f452fdf40eca2ce9/src/Renode/RobotFrameworkEngine/RobotFrameworkEngine.cs#L1-L38 [^6]: Renode `src/Renode/RobotFrameworkEngine/RenodeKeywords.cs`, `ExecuteCommand` calling `monitor.Parse`. https://github.com/renode/renode/blob/091eb1aca70127a5508573b3f452fdf40eca2ce9/src/Renode/RobotFrameworkEngine/RenodeKeywords.cs#L21-L73 [^7]: Renode `src/Renode/RobotFrameworkEngine/UartKeywords.cs`, `WaitForLineOnUart` signature and failing-string handling. https://github.com/renode/renode/blob/091eb1aca70127a5508573b3f452fdf40eca2ce9/src/Renode/RobotFrameworkEngine/UartKeywords.cs#L92-L106 [^8]: RIOT-OS pull request #19375, adding emulated board reset via telnet-to-Monitor scripting, opened 2023-03-10. https://github.com/RIOT-OS/RIOT/pull/19375 [^9]: PlatformIO Core issue #3401, Renode integration using chained `-e` Monitor commands for upload and debug, opened 2020-03-04. https://github.com/platformio/platformio-core/issues/3401 [^10]: Renode issue #344, "Pass a value to a RESC script," answered with a `-e` one-liner, opened 2022-06-16. https://github.com/renode/renode/issues/344 [^11]: Antmicro, `renode-test-action`, first-party GitHub Action running Robot Framework tests in Renode. https://github.com/antmicro/renode-test-action/ [^12]: Antmicro, "Renodepedia," 2022-08-29, thousands of CI jobs, Robot test suite log per platform. https://antmicro.com/blog/2022/08/renodepedia [^13]: Antmicro, "Recent improvements to Renode Zephyr and U-Boot dashboards," 2026-08-12, platform pass counts (926/1513 Zephyr, 829/1373 U-Boot). https://antmicro.com/blog/2026/08/recent-improvements-to-renode-zephyr-and-u-boot-dashboards --- ## A firmware instruction trace is not code coverage (https://veecle.ai/blog/firmware-instruction-trace, 2026-08-17) Renode will dump every instruction your firmware executed. People see that file and conclude they have code coverage. They do not. The gap is not a formatting problem. A firmware instruction trace is a list of addresses the core visited. Coverage is a statement about source structure. Getting from the first to the second needs a debug build, a separate post-processing tool, and a DWARF join. Even then, you get line coverage. That is what the tool actually ships. Branch coverage needs a control-flow graph this tool does not build; condition coverage and MC/DC need information a program counter stream never contains at all. I already split the three unrelated things called trace in [RTOS trace in a simulator](/blog/rtos-trace-simulator). This post stays inside the middle one, CPU instruction trace, and asks what that artifact is evidence of. ### What is in the file Renode's execution tracing has four modes, selected on the command that turns it on: `cpu CreateExecutionTracing "tracer_name" @path-to-file `, where mode is `PC`, `Opcode`, `PCAndOpcode` or `Disassembly`.[^1] Output can go out binary or gzipped. `cpu DisableExecutionTracing` turns it off. `PC` mode gives you addresses and nothing else: ``` 0x20400000 0x20400004 0x20400008 ``` `Opcode` mode gives you the opposite half, raw opcodes with no addresses: ``` 0x0297 0x1028293 0x30529073 ``` `PCAndOpcode` pairs them: ``` 0x20400000: 0x0297 ``` `Disassembly` is the only mode that adds anything a human can read, and it comes from Renode's own built-in LLVM-based disassembler: ``` 0x20400000: 00000297 auipc t0, 0 [vinit (entry)] 0x20400004: 01028293 addi t0, t0, 16 [vinit+0x4 (guessed)] ``` The brackets are the tell. `[vinit (entry)]` and `[vinit+0x4 (guessed)]` are symbol-name guesses pulled from the ELF symbol table. Function granularity, with the word "guessed" printed in the output where the tool is interpolating. No file. No line. Not in the richest of the four modes. That is the whole payload. Renode does not do source-line mapping, and the docs do not pretend otherwise: they hand that job to a separate downstream tool. ![Diagram of Renode's four execution-tracing output modes showing none of them carry a source file or line number](/blog/firmware-instruction-trace/image-1.webp) ### The conflation, dated and quoted This is not a hypothetical mistake. Renode issue 266 was opened on 2021-11-18 with a reasonable question: "i just curious about when testing with renode using Robot Framework, renode can measure code coverage or not... so, i think renode can do this, it will be really great!!!"[^9] A maintainer answered honestly: "Tracking execution is something that can be easily done with Renode, but with the abundance of coverage tools out there it's difficult to select just one." Then a contributor pointed at the thing that had just landed: "we added the execution tracer module that can dump executed PCs / opcodes to a separate file." That exchange is the entire problem in four messages. Someone asks for coverage. The available answer is a PC dump. The reporter's follow-up, "that's a good update. how can i use this from robot test framework?", is the sound of a gap being papered over. The issue got closed, then reopened later by a different person asking the obvious next thing: "how can we get an lcov coverage report from the trace?" In 2021 that question had no documented answer. The tooling that answers it did not exist until February 2024, three years later.[^3] Nobody in that thread was confused about their job. The tooling was missing, and a trace file looks close enough to coverage that it kept getting handed over as if it were. ### What the real pipeline costs Antmicro built the missing piece and documented it, and the documentation is refreshingly blunt about the preconditions. "With an execution trace of a properly built binary and a dedicated script, you can create a code coverage report."[^2] Both qualifiers are load-bearing. The script "combines data in [the DWARF format] (debug information available in ELF files...) with the number of executions of each instruction counted by Renode." DWARF supplies the address-to-source-line mapping. Renode supplies execution counts per address. Neither half is coverage on its own. Properly built means what you would expect: "it's recommended to use the `-g` and `-O0` (or `-Og`) flags... The `-g` switch adds debug information to the binary, specifically code line numbers for each machine instruction. The coverage functionality requires that information, and you will see an error if it's missing from the ELF file." And optimization breaks it, stated plainly in the same doc: "Any optimization done by a compiler may prevent redundant lines of code from being executed. The compiler might also replicate the same code line across several addresses in the program or reorder the execution flow, which will make obtaining legitimate results impossible." The tool is a separate CLI utility, `renode-retracer`, installed with `pipx` from the Renode tree: ``` renode-retracer coverage trace.bin.gz \ --binary coverage-sample.elf \ --sources main.c additional.c \ --output coverage.zip \ --export-for-coverview ``` There is also a fallback for when DWARF is not available: hand the tool a manual mapping file with `--pc2line`, lines like `0x800003d8 k210_ops.cpp:252` and `0x80000bb6 main.c:6`. That flag is the cleanest proof of the argument. The address-to-line correspondence is external data fed _into_ the trace. The trace never carried it. Antmicro's own writeup says why they took this route rather than the usual one: "The most common method for code coverage analysis has the compiler add additional calls to the coverage infrastructure, meaning the code you're running to get the statistics is not the same as the code you'll be running in production... we leveraged the framework's capabilities to run tests on uninstrumented binaries."[^3] Their Coverview post repeats the mechanism and names the tradeoff: post-mortem PC-to-line translation "allows you to track line coverage of an executed application without additional instrumentation of the code, which is the typical approach used by tools like gcov."[^4] Line coverage. That word keeps appearing, and it is not an accident. ### I went looking for the branch tag The docs say "renode-retracer supports generation of line coverage info (DA)".[^2] That is Antmicro's own sentence and it is the safe thing to quote. I wanted to know whether the implementation was narrower than the docs, so I read the tool's source. The entire LCOV emission path is one function: ```python def to_lcov_format(self) -> str: return f"DA:{self.number},{self.most_executions()}" ``` That is the whole thing, at commit `091eb1a` of the Renode tree.[^5] `DA` is LCOV's line-hit-count tag. `renode-retracer` emits no `BRDA` records, so its LCOV output contains no branch data. No `FNDA`, so not even function-hit records. No condition or MC/DC tag, because LCOV's format has nothing for it and the tool has nothing to put there. This is my reading of their source rather than a claim they made, but the ceiling is not a roadmap item. It is a line number and a count. ![Ladder diagram showing a PC trace reaching line coverage via DWARF and renode-retracer, branch coverage reachable in principle via a reconstructed control-flow graph but not by this tool, and condition and MC/DC coverage reachable only through compiler instrumentation](/blog/firmware-instruction-trace/image-2.webp) ### Three different ceilings The reason is not that nobody wrote the feature. It is that the information does not exist in the artifact, not entirely, and not in the same way for every coverage kind. Branch coverage is not hopeless from a PC stream. A tool with the binary's full control-flow graph can compare consecutive addresses and infer some taken/not-taken outcomes, which is close to what Ozone does inside its own engine. `renode-retracer` does not do that: `to_lcov_format` emits `DA` and nothing else, so branch is a tooling gap here, not a law of physics. Condition coverage and MC/DC are the harder wall, because no amount of PC-stream analysis recovers a boolean expression's term structure. That has to come from the compiler. Look at how gcov gets branch data. `--coverage` is a synonym for `-fprofile-arcs -ftest-coverage` at compile time plus `-lgcov` at link.[^6] `-ftest-coverage` emits a `.gcno` notes file holding the static flow-graph structure, generated while compiling. `-fprofile-arcs` adds code: "for each function of your program GCC creates a program flow graph, then finds a spanning tree for the graph. Only arcs that are not on the spanning tree have to be instrumented: the compiler adds code to count the number of times that these arcs are executed." Those counters write a `.gcda` at program exit. Two files, two different times, both produced by the compiler. Neither is a byproduct of running an uninstrumented binary under any external tracer. The graph structure that makes an arc count _mean_ something is a compile-time artifact. Condition coverage needs a third flag on top of that: `-fcondition-coverage`, which adds "code so that program conditions are instrumented. During execution the program records what terms in a conditional contribute to a decision," read back with `gcov --conditions`.[^7] GCC documents this as masking MC/DC directly, with the independence rule stated as "an outcome is considered covered if it has an independent effect on the decision." Output looks like `condition outcomes covered 1/4` and `condition 0 not covered (true)` for a line `if (total != 45 && v == 1)`. Path coverage is yet another flag, `-fpath-coverage`. The pattern holds at every rung: line, arc, condition, path, each one its own compile-time switch. LLVM makes the point even harder to argue with. Clang's source-based coverage "operates on AST and preprocessor information directly," and MC/DC is `-fcoverage-mcdc` layered on `-fprofile-instr-generate -fcoverage-mapping`.[^8] Clang's definition ties the criterion to decision structure explicitly: MC/DC is "the percentage of individual branch conditions that have been shown to independently affect the decision outcome of the boolean expression they comprise." The implementation limits give it away completely. Maximum 32767 _terms_ in an expression. Test vectors capped at 2,147,483,646. Those are limits on boolean-expression syntax, a concept with no representation in an address stream at all. Clang even ships a second, separate gcov-compatible mode that operates on DebugInfo instead. One compiler, two structurally different coverage implementations, and neither of them is "read the PC trace." If you want the criterion itself rather than the plumbing, that is [MC/DC for firmware people](/blog/mcdc-coverage-embedded). One sentence of lineage: MC/DC is the structural coverage objective at DO-178C Level A, which is why it shows up in embedded conversations at all. ![Diagram contrasting the gcov instrumented-binary path, which reaches condition and MC/DC coverage, with the Renode plus renode-retracer trace path, which stops at line coverage](/blog/firmware-instruction-trace/image-3.webp) ### The honest version of the claim I am not saying trace-derived coverage is a scam. Commercial hardware-trace tooling sells it. SEGGER's page for Ozone with J-Trace says exactly that: "Ozone's trace capability can provide accurate information about instruction execution on the target system. This allows a full code coverage analysis of the firmware."[^10] That is a real product doing real work, and it does not contradict anything above. Ozone performs its own address-to-source mapping and its own analysis of decision structure inside the debugger. The work still happens. It just happens somewhere you do not have to invoke it yourself. Nobody is claiming the raw trace file _is_ the coverage report. So the precise claim is this. A trace file is not coverage. Every route from a trace to coverage adds a mapping and analysis step, and that step lives in one of three places: a separate script, a debugger's internal engine, or the compiler. Where it lives sets your ceiling. Machine-level branch outcomes can be inferred from a PC stream plus a reconstructed control-flow graph. `renode-retracer` just does not do that. Source-level condition coverage and MC/DC are a harder wall: they need the compiler to have known about the decision structure before the binary was built, which no amount of post-hoc PC analysis recovers. The practical version: if you are pointing at a trace file as coverage evidence, check what produced it and what tag it emits. If your build has `-O2` in it, the line mapping is already suspect by the tool's own documentation. And if somebody tells you an emulator gave them MC/DC, ask which flag was on the compile line. There is exactly one right answer and a trace does not have it. Same reflex as [what cycle-accurate actually means](/blog/what-cycle-accurate-actually-means): the word is doing more work than the tool is. ### Sources [^1]: Renode documentation, "Execution tracing in Renode." Mode list, per-mode output examples, and the `CreateExecutionTracing` / `DisableExecutionTracing` syntax. https://renode.readthedocs.io/en/latest/execution-tracing/execution-tracing.html [^2]: Renode documentation, "Generating a coverage report." DWARF join, the `-g` and `-O0`/`-Og` build requirement, the optimization warning, `renode-retracer` invocation, `--pc2line` fallback, and "line coverage info (DA)". https://renode.readthedocs.io/en/latest/execution-tracing/coverage-report.html [^3]: Antmicro, "Introducing code coverage reporting in Renode," 2024-02-27. Rationale for uninstrumented binaries and the custom trace-to-coverage tool. https://renode.io/news/introducing-code-coverage-reporting-in-renode/ [^4]: Antmicro, "Renode integration with Coverview for improved code coverage analysis," 2025-06-17. Post-mortem PC-to-line translation, contrasted with gcov. https://antmicro.com/blog/2025/06/renode-integration-with-coverview [^5]: `renode-retracer` source, `tools/execution_tracer/execution_tracer/coverage.py`, at commit `091eb1aca70127a5508573b3f452fdf40eca2ce9`. The `to_lcov_format` method emits `DA` only; the absence of `BRDA`, `FNDA` and any condition tag is my own reading of the codebase, not an Antmicro statement. https://github.com/renode/renode/blob/091eb1aca70127a5508573b3f452fdf40eca2ce9/tools/execution_tracer/execution_tracer/coverage.py#L67 [^6]: GCC Online Documentation, "Program Instrumentation Options." `--coverage`, `-fprofile-arcs`, `-ftest-coverage`, `-fcondition-coverage`, `-fpath-coverage`, and the spanning-tree arc instrumentation mechanism. https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html [^7]: GCC Online Documentation, "Invoking Gcov." `gcov --conditions` output format and the masking MC/DC independence rule. https://gcc.gnu.org/onlinedocs/gcc/Invoking-Gcov.html [^8]: Clang documentation, "Source-based Code Coverage." AST-based operation, the separate DebugInfo-based gcov mode, `-fcoverage-mcdc`, the MC/DC definition, and the term and test-vector limits. https://clang.llvm.org/docs/SourceBasedCodeCoverage.html [^9]: Renode issue #266, "Test with renode and code coverage report," opened 2021-11-18, closed and later reopened with the unanswered LCOV question. https://github.com/renode/renode/issues/266 [^10]: SEGGER, "Code Coverage" for Ozone with J-Trace. https://www.segger.com/products/development-tools/ozone-j-link-debugger/technology/code-coverage/ --- ## Espressif already published the QEMU ESP32 limitations list (https://veecle.ai/blog/qemu-esp32-limitations, 2026-08-14) People keep asking whether QEMU can run their ESP32 firmware. Espressif already answered. The answer is a table in a README, and the peripherals people buy the chip for are marked with a red X. I wrote about [what QEMU covers on Cortex-M](/blog/qemu-for-cortex-m-what-works). This is the other popular QEMU story in embedded, and it is cleaner, because the vendor drew the map themselves. ### The table nobody scrolls to ESP-IDF's QEMU page is a how-to. Install the fork. `idf.py qemu monitor`. Debug with GDB. Emulate eFuses so you can test secure boot without burning a part. It does not list what is missing.[^1] The missing list lives one click further, in Espressif's toolchain-docs README. Supported features, three columns, ESP32 / ESP32-S3 / ESP32-C3.[^2] The green side is real work. Dual-core. UART. Interrupt matrix. NOR flash and its MMU. PSRAM. eFuse. RNG. AES, SHA, RSA. Timer groups. TWAI. On ESP32, SD/MMC and LEDC. Then the red side, identical across all three chips: | Feature | ESP32 | ESP32-S3 | ESP32-C3 | | ------------------- | ----- | -------- | -------- | | Wi-Fi | no | no | no | | Bluetooth | no | no | no | | USB | no | no | no | | RMT | no | no | no | | GP SPI | no | no | no | | I2C | no | no | no | | I2S | no | no | no | | ULP (co-processor) | no | no | no | | GPIO matrix / IOMUX | no | no | no | Wi-Fi has a note: "The Ethernet controller can be used for networking instead." That controller is also starred. It is OpenCores Ethernet. It is not a real ESP32 peripheral. The RGB framebuffer is the same kind of star: a host convenience, not silicon.[^2] ![Espressif QEMU feature matrix showing WiFi Bluetooth USB RMT I2C I2S and ULP unsupported on ESP32](/blog/qemu-esp32-limitations/image-1.webp) ### What that list actually means An ESP32 without Wi-Fi, Bluetooth, USB, RMT, SPI, I2C, I2S, the ULP, and the GPIO matrix is not "an ESP32 with a few gaps." Those are the reasons people buy the part. QEMU will still boot a lot of firmware. The CPU is there. Flash is there. UART is there. Crypto is there. If your test is "does this image start, print, and sit in a loop," the fork is a legitimate target. Espressif even ships prebuilt binaries and wires them into `idf.py`.[^1] The moment the firmware talks to the radio, a sensor bus, a WS2812 chain, a USB gadget, or the ULP, you have left the model. The README is not coy about this. The support paragraph under the table says Espressif does not provide support for QEMU, and that they will "likely not be able to help with particular use cases which aren't supported yet (e.g. due to missing emulation of some peripherals)."[^2] That is a vendor telling you the unsupported set is expected, not a temporary omission they forgot to mention. When someone opens an issue because `esp_wifi_init` crashes in QEMU, the reply is the same table. Wi-Fi is not supported. Use the OpenCores Ethernet path if you need a network. File QEMU bugs in the QEMU repo, not in ESP-IDF.[^3] ### The stand-ins are a tell Two green ticks in that table are not hardware. OpenCores Ethernet exists so you can have a socket. The RGB framebuffer exists so you can look at pixels. Both notes say so in plain language.[^2] That is a coherent engineering choice. Networking tests and GUI tests are useful. Faking the ESP32 MAC and PHY well enough to run `esp_wifi` is a different project, and Espressif did not take it on. The Ethernet stand-in is the honest substitute: you get packets, you do not get a radio. A December 2025 walkthrough of "internet in QEMU" has to spend most of its length on exactly this reroute — enable OpenEth, init a MAC that is not on the chip, turn off hardware crypto because that path misbehaves, accept that Wi-Fi is gone.[^4] The workarounds are the product. They are not a path back to the missing peripherals. ![Diagram contrasting Espressif QEMU modeled peripherals with the ESP32 silicon features it does not emulate](/blog/qemu-esp32-limitations/image-2.webp) ### What this is good for, and what it is not Same as [Ozone-Sim](/blog/ozone-sim-teardown). Scope first, then the question you are allowed to ask. | Question | Espressif QEMU | | ------------------------------------------------------ | ------------------------------------------ | | Does this image boot and print on UART? | Yes, that is the happy path | | Can I step it in GDB without a probe? | Yes, `idf.py qemu gdb` | | Can I burn fake eFuses and test secure boot? | Yes, that is a documented feature | | Does my HTTPS client get a socket? | Yes, through OpenCores Ethernet, not Wi-Fi | | Does `esp_wifi_init` work? | No | | Does my I2C sensor come up? | No — I2C is unsupported on every chip | | Does my RMT LED strip bit-bang correctly? | No | | Does the ULP wake the main cores the way silicon does? | No | Modeling Wi-Fi and the GPIO matrix to silicon fidelity is expensive, and most of what this fork is worth is "run the binary without flashing." That is real. It is also not the same as "this is the chip." I have written about [what simulation can't catch](/blog/what-simulation-cant-catch) — the cases where the simulator answers anyway. Espressif's table is the better failure mode. The unsupported set is written down. The crash when you init Wi-Fi is a loud no, not a quiet zero. The README does not claim timing fidelity either; this fork is not in the [cycle-accurate](/blog/what-cycle-accurate-actually-means) conversation. The green ticks are what still bites people. LEDC is yes on ESP32 and no on S3 and C3. SD/MMC is yes on ESP32 and no on S3. "QEMU supports ESP32" is not a sentence that transfers across the family. Treat Espressif QEMU as a host for the parts of the image that do not touch the red rows. That is a useful CI target. It is not a substitute for the radio, the buses, or the ULP, and the people who built it already said so. ### Sources [^1]: Espressif, "QEMU Emulator," ESP-IDF Programming Guide v6.0.2, accessed 2026-08-14. https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/tools/qemu.html [^2]: Espressif, "QEMU README," esp-toolchain-docs, accessed 2026-08-14. https://github.com/espressif/esp-toolchain-docs/blob/main/qemu/README.md [^3]: Espressif, "Initializing WiFi in QEMU crashes the application," esp-idf issue 15087, 2024-12-24. https://github.com/espressif/esp-idf/issues/15087 [^4]: Production ESP32, "ESP32 Internet Access in QEMU," 2025-12-19. https://productionesp32.com/posts/internet-in-qemu/ --- ## The LLM wrote the skills. The ESP-IDF score went down. (https://veecle.ai/blog/llm-embedded-benchmark, 2026-08-14) The default fix, once an LLM fails at firmware, is to give it more text. A RAG corpus. A skills folder. The datasheet in the prompt. Something in, better code out. A March 2026 paper measured that move. The skills the model wrote for itself made ESP-IDF worse. ### What they ran IoT-SkillsBench is a hardware-in-the-loop benchmark. Three platform-framework pairs: ATmega2560 with Arduino, ESP32-S3 with ESP-IDF, nRF52840 with Zephyr. 42 tasks, three difficulty bands, 23 peripherals. Each task under three agent setups: no skills, LLM-generated skills, human-expert skills. 378 runs, checked on real boards, not in QEMU.[^1][^2] I have already argued that [most LLM-for-embedded benchmarks do not measure the thing people cite them for](/blog/what-llm-embedded-benchmarks-measure). This one is closer to a firmware test than most, because the pass condition is behavior on silicon. The skills themselves follow the usual pattern: YAML header, markdown body, one peripheral or framework concern per file. The LLM set was produced by asking Claude Sonnet 4.6 to summarize the relevant knowledge from parametric memory only — no datasheets, no error logs, no extra docs. The human set was written by people who had the failing code, the compiler errors, and the runtime behavior in front of them.[^1] The expert skills are not a mystical prompt. They are a short note written after watching the model fail on the bench. ### The number On ESP-IDF, no skills: 31 out of 42 tasks. LLM-written skills: 27 out of 42. Human skills: 41 out of 42.[^1] Level 3 — interrupts, multi-device work, scheduling — is where the drop concentrates. No skills resolved 7 of 14. LLM skills resolved 4 of 14. The paper's own reading is that synthesized skills "can sometimes reinforce incorrect assumptions about complex ESP-IDF-specific behavior."[^1] Arduino barely moved. 42/42 with no skills, 41/42 with LLM skills, 42/42 with human skills. Zephyr went 28/42 to 27/42 with LLM skills, then 41/42 with human skills. The only human misses the paper calls out are a 5 V RTC on a 3.3 V ESP32-S3, and a rotary encoder whose direction is not standardized. Those are hardware ambiguities, not prompt failures.[^1] ![Bar chart of IoT-SkillsBench scores showing LLM-written skills dropping ESP-IDF from 31 of 42 to 27 of 42](/blog/llm-embedded-benchmark/image-1.webp) Token cost went the wrong way too. No-skills sat around 300 input tokens per task. LLM skills ran 8,500 to 9,500, plus the model talking to itself about the skill before emitting code. Human skills landed in the middle and almost cleared the suite.[^1] So the expensive condition was also the worse one. ### "Just add RAG" is a retrieval story, not a truth story This paper did not kill retrieval. It killed a lazier claim: that any extra structured text about the chip is an upgrade. The LLM skills were not retrieved from a good corpus. They were generated from the same weights that were already failing the task. That is closer to asking the student to write the cheat sheet, then grading the exam they take with it. If the student is wrong about ESP-IDF init sequences, the cheat sheet is wrong in a more confident font. I have made the adjacent argument before: [LLMs write plausible firmware and cannot prove it](/blog/llms-write-good-firmware-cant-prove-it), and [what they get wrong is usually hardware context, not C syntax](/blog/what-llms-get-wrong). SkillsBench adds a measurement. Injecting ungrounded "knowledge" is not a neutral act. It can overwrite the cases the model would have gotten right. The human skills worked because they were grounded in the failure. Compiler log, runtime behavior, one peripheral, short prose, almost no sample code. That is closer to a lab notebook than to a scraped SDK dump. ![Diagram contrasting LLM-written skills that recycle the same wrong assumptions with human skills grounded in a failed hardware run](/blog/llm-embedded-benchmark/image-2.webp) ### What I am not claiming This is one paper, one agent scaffold, one model used to write the skills, three boards. It is not a law of RAG. A corpus built from datasheets and errata, retrieved against the actual error, is a different experiment. SkillsBench did not run that experiment, and I am not going to cite it as if it had. It also does not say "skills are useless." The human set almost maxed the benchmark. The variable was the source of the text, not the presence of a skills folder. If you are going to add skills to an embedded agent, the test is simple. Keep the no-skills baseline. Measure the suite again after the skills land. If the score drops, you did not add knowledge. You added a second, worse model of the chip, and you paid tokens for the privilege. ### Sources [^1]: Li et al., "Skilled AI Agents for Embedded and IoT Systems Development," arXiv:2603.19583, 2026-03-20. https://arxiv.org/abs/2603.19583 [^2]: IoT-SkillsBench, GitHub repository, accessed 2026-08-14. https://github.com/iot-agent/iot-skillsbench --- ## You cannot simulate MCU power consumption. You can only measure it. (https://veecle.ai/blog/simulate-mcu-power-consumption, 2026-08-14) Someone always asks whether the simulator can tell them the battery life. No. Every tool I trust for MCU power consumption sits on a wire between a supply and a chip. Joulescope. Qoitech Otii. Nordic's Power Profiler Kit II. Espressif's own current-measurement guide names two of those and then tells you to cut the module's supply and put an ammeter in the gap.[^1][^2][^3][^4] Skip the probe and you are not measuring power. ### What the real tools are Joulescope is a shunt and a voltmeter that samples at 2 MHz so a sleep-to-radio spike does not disappear between ranges.[^2] Otii Arc and Ace are a source-measure unit: they power the device and record current, voltage, power, and energy at the same time.[^3] The PPK2 is the same idea at Nordic-kit money, ampere-meter or source, 200 nA to 1 A, with digital inputs so you can mark which code was running when the spike happened.[^4] All three assume a device under test. A board. A module. A pair of banana plugs. The firmware is running on silicon. The number on the screen is charge that left a physical rail. Espressif's module guide barely pretends otherwise. Do not measure a dev board, the rest of the board still draws. Cut the module supply. Use an ammeter that can track deep-sleep nanoamps and active milliamps without dropping the rail. They recommend Joulescope and the PPK2, then publish a waveform: 8.14 µA asleep, about 23.88 mA active, on a specific module, with a specific example.[^1] That is a measurement. It has a setup photo and a shunt in the picture. ![Diagram of MCU power measurement with a source-measure unit or shunt between the supply and the module](/blog/simulate-mcu-power-consumption/image-1.webp) ### The things that look like simulation, and are not Datasheet tables. "Typical 5 µA in stop mode." Those are vendor lab numbers on a reference setup. Useful as a floor. They are not your firmware, your regulators, or your leaky GPIO. Nordic's Online Power Profiler estimates BLE, LTE-M, and NB-IoT current from lab-tested protocol profiles.[^4] It is a calculator over someone else's traces. It will not see the extra milliamp your debug pin is sourcing. Instruction-level energy models exist in papers. They assign a cost to an opcode or a memory access on one core, usually under one voltage and one temperature. They are research. I have not seen a firmware team ship a battery claim off one. QEMU, Renode, Ozone-Sim, native_sim — none of them model the analog current out of the pad. They model enough of the digital machine to run code. Current is not a register. It is charge on a rail those models do not have. I wrote a longer field guide to [what simulation can't catch](/blog/what-simulation-cant-catch). Power is the cleanest entry on that list, because the simulator does not even try. The failure mode is not a quiet zero from a missing DWT. The failure mode is you asking a digital model for a current it does not have. Cycle-accurate digital simulation does not rescue this. [Cycle-accurate means instruction timing, not joules](/blog/what-cycle-accurate-actually-means). A perfect cycle count still needs a current-per-cycle map that matches your silicon, your voltage, your temperature, and every analog block the radio just powered. Nobody hands you that map for a modern wireless MCU and stands behind it. ### Why the honest "no" is useful Firmware CI can, and should, run a lot of checks without a board. I have a whole post on [what belongs in simulation versus HIL](/blog/simulation-vs-hil-what-belongs-in-ci). Power is HIL. Not because HIL is morally better. Because the quantity you want is electrical, and the simulator does not have the circuit. An agent optimizing sleep current without a meter will optimize the story. It will delete a log line, flip a clock bit, and declare victory. The rail does not care. If you want the loop to close, the agent needs the same thing a human needs: a trace from a probe, time-aligned with a GPIO mark or a log, on the firmware you actually intend to ship. That is slower than a unit test. It is also the only number that counts. ![Comparison showing firmware checks that can run without a board versus MCU power numbers that require a probe on silicon](/blog/simulate-mcu-power-consumption/image-2.webp) You cannot simulate MCU power consumption in any sense a battery engineer would accept. You measure it. ### Sources [^1]: Espressif, "Current Consumption Measurement of Modules," ESP-IDF Programming Guide v6.0.2, accessed 2026-08-14. https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/current-consumption-measurement-modules.html [^2]: Jetperch, "Joulescope JS220 User's Guide," v1.9, accessed 2026-08-14. https://download.joulescope.com/products/JS220/JS220-K000/users_guide/Joulescope%20JS220%20User%27s%20Guide%20v1_9.pdf [^3]: Qoitech, "Power and current profiling," Otii documentation, accessed 2026-08-14. https://docs.qoitech.com/en/use-cases/power-and-current-profiling [^4]: Nordic Semiconductor, "Power Profiler Kit II," product page, accessed 2026-08-14. https://www.nordicsemi.com/Products/Development-hardware/Power-Profiler-Kit-2 --- ## native_sim is not a unit test. Zephyr had to write that down. (https://veecle.ai/blog/zephyr-native-sim, 2026-08-14) Both targets produce a Linux executable. Both sit under Twister. Both let you debug with gdb. So people treat `native_sim` and `BOARD=unit_testing` as two spellings of "run Zephyr on the host." Zephyr's own test-framework page opens a subsection by admitting the two are easy to confuse, then says: they are fundamentally different.[^1] When a project has to write that sentence, the confusion is not theoretical. It is the default. ### Two host binaries, two machines `unit_testing` is a pseudo-board. `arch: unit`. It is not hardware and it is not a simulated SoC. Twister selects it when the scenario sets `type: unit`. The build links the files you put on the `testbinary` target with the Ztest harness, using the host toolchain. The kernel is not in the image. There is no boot, no scheduler, no devicetree init, no driver model. You call the function. If it needed a kernel API, you stub it.[^1] `native_sim` builds the complete Zephyr OS — kernel, devicetree, Kconfig, drivers, subsystems — into a host binary that boots and runs like a Zephyr image, compiled for the host instead of a target SoC. Tests that use it do not set `type: unit`.[^1][^2] Reach for `native_sim` to exercise code in a running Zephyr system. Reach for `unit_testing` to test an isolated module without pulling in the kernel.[^1] ![Diagram contrasting Zephyr unit_testing, which omits the kernel, with native_sim, which boots a full Zephyr image on the host](/blog/zephyr-native-sim/image-1.webp) ### What native_sim is honest about The board page is blunt. `native_sim` "does not intend to simulate any particular HW." It offers a few peripherals — Ethernet, display, UART — so application code that needs those surfaces can run. It does not model a chip. Code is compiled for the host, typically x86. There is no I/O or MMU emulation; a hardcoded address segfaults.[^3] Time is simulated and, by default, decoupled from wall time. The kernel thinks time is whatever the models say.[^2] The POSIX-architecture writeup underneath it is even blunter. The port assumes code executes in zero simulated time. It is not for debugging hardware/software races or missed programming deadlines. It does not replace an instruction-set simulator, a development board, or QEMU. It complements them.[^3] That last sentence is the one CI setups skip. They see a green Twister run on `native_sim` and file it under "we tested on a board." They tested on a host program that contains a Zephyr kernel. That is a real test. It is not a board. `native_posix` is the old name. Same idea.[^2] ### Why the mix-up keeps happening Twister's console output reports how a test ran: qemu, native_sim, or build-only. The status line does not say whether the kernel was in the binary.[^4] If you live in the YAML and not in the CMake, `type: unit` is a small key. Miss it and you built a different kind of program than you think. The 2018 argument is still the argument. One engineer wrote that ztest is for unit tests and native_posix is for system tests, and that you cannot mock the world away under native_posix because the world is the point. Another wanted native_posix to replace the mock layer so they would not have to maintain empty headers. The thread did not resolve into one tool. It resolved into two jobs.[^5] Zephyr later grew FFF mocks inside Ztest, which makes the unit-testing board more pleasant and does not make `native_sim` a unit-test target. Different layer. I have made this split before. [Firmware CI that ends at the linker](/blog/firmware-ci-ends-at-the-linker) is the unit-testing failure mode: you compiled something, you did not run the system. [Simulation versus HIL](/blog/simulation-vs-hil-what-belongs-in-ci) is the native_sim failure mode: you ran a system, it was not the silicon. Both belong in CI. They answer different tickets. ![Ladder placing Zephyr unit_testing, native_sim, and target simulation or hardware as three different test rungs](/blog/zephyr-native-sim/image-2.webp) ### What you can claim after a native_sim pass You can claim the kernel scheduled, the app reached the state you asserted, the subsystem you enabled initialized, and a host-backed UART or Ethernet path did what that backend does. You can debug it with host tools, ASan, UBSan, coverage. Those are large claims. They are why the target exists.[^2] You cannot claim the STM32 timer did the thing, the nRF radio did the thing, or the image you will flash did the thing. That image was not built. The ABI is the host ABI — 32-bit `native_sim` exists specifically because most MCUs are ILP32 and 64-bit host longs hide bugs.[^2] Even the 32-bit target is still host code running host instructions. If the behavior you care about is "does this module return the right value given these stubs," use `unit_testing`. If it is "does this Zephyr app boot and pass its integration tests without a board," use `native_sim`. If it is "does this driver speak this chip," you have left both pages. That is [what simulation can't catch](/blog/what-simulation-cant-catch) when people pretend otherwise. The docs already picked the words. Fundamentally different. Use them. ### Sources [^1]: Zephyr Project, "Test Framework," Zephyr documentation, accessed 2026-08-14. https://docs.zephyrproject.org/latest/develop/test/ztest.html [^2]: Zephyr Project, "Native simulator - native_sim," Zephyr documentation, accessed 2026-08-14. https://docs.zephyrproject.org/latest/boards/native/native_sim/doc/index.html [^3]: Zephyr Project, "The POSIX architecture," Zephyr documentation, accessed 2026-08-14. https://docs.zephyrproject.org/latest/boards/native/doc/arch_soc.html [^4]: Zephyr Project, "Test Runner (Twister)," Zephyr documentation, accessed 2026-08-14. https://docs.zephyrproject.org/latest/develop/twister/index.html [^5]: Zephyr Project, "native_posix supersedes ztest mocking," GitHub issue 7316, 2018-05-02. https://github.com/zephyrproject-rtos/zephyr/issues/7316 --- ## CMock is not a simulator. The STM32 HAL keeps proving it. (https://veecle.ai/blog/mocking-stm32-hal-unit-test, 2026-08-14) Every few years someone tries to mock `stm32xxxx_hal_adc_ex.h`, CMock chokes on a type defined three includes away, and a thread appears that is really about a different question. The question is not "how do I get Ceedling to parse this header." The question is what you thought the mock was going to tell you. ### The October 2025 version A developer using Ceedling wants to unit-test a file that calls into `stm32h7xx_hal_adc_ex.c`. The post notes that ST's docs tell you to include the umbrella `stm32h7xx_hal.h`. The test includes `mock_stm32h7xx_hal_adc_ex.h` instead. GCC replies: unknown type name `HAL_StatusTypeDef`. The type lives in `stm32h7xx_hal_def.h`. The include chain breaks the moment CMock generates a mock from the inner header.[^1] The fix that unblocked the build was one extra include in `project.yml`. Tell CMock to pull in the umbrella header. The test then compiles. That is a real fix for a real parser problem. It is not a simulation of an ADC. The same pain showed up in a January 2025 question about mocking `HAL_UART_Receive`, and in December 2024 as a request for an "optimal" STM32 Ceedling config whose interesting lines are a pile of `:includes:` pointing at `stm32l4xx_hal.h`.[^2][^3] ThrowTheSwitch's own tracker has the 2017 nested-header issue and the 2020 "unknown type name FOO_Type" issue. Maintainers keep giving the same advice: CMock will not walk the chain for you; vendor HALs were not written to be mocked; make a thin header of the functions you actually call, or stop testing at this layer.[^4][^5] Eight years. Same HAL. Same tool. Same surprise. ![Diagram of STM32 HAL header chains breaking CMock when a nested header is mocked without its type dependencies](/blog/mocking-stm32-hal-unit-test/image-1.webp) ### Mock, fake, stub, simulator Same split as [MIL / SIL / PIL / HIL](/blog/mil-sil-pil-hil-for-firmware-people), at the function call. A **stub** returns a canned value so the caller can proceed. A **fake** is a small working stand-in — an in-memory flash, a loopback UART. A **mock** is a stub plus a script: you declare that `HAL_ADCEx_CalibrationStart` will be called with these arguments and will return `HAL_OK`, and the test fails if the call does not happen. CMock is a mock generator. It is good at that script. A **simulator** executes the compiled target binary against models of a core and, if you are lucky, some peripherals. Renode, QEMU, a vendor ISS. Different artifact. Different question. I wrote down [what simulation still cannot catch](/blog/what-simulation-cant-catch) and [which of those checks belong in CI](/blog/simulation-vs-hil-what-belongs-in-ci). | You want to know | Use | | ----------------------------------------------------------------- | ------------------------------------------- | | Did my module call `HAL_ADCEx_CalibrationStart` with this handle? | Mock | | Can my module tolerate `HAL_ERROR` from that call? | Mock | | Does my ring buffer do the right thing when the ISR posts a byte? | Fake the ISR entry, or stub the HAL receive | | Does this HAL init sequence actually start the ADC on this STM32? | Simulator or silicon | | Does the calibration complete in the time the errata says? | Silicon | CMock will happily let you write the last two tests. They will pass. They have not asked the chip anything. They asked your mock whether you called your mock. ### Why the HAL is a bad mock target ST's HAL is an umbrella include, a forest of `_ex` files, types that live in neighbors, and a lot of `#ifdef` branches that CMock does not handle like the configured target build. The Ceedling maintainers say it plainly: the STM libraries are really hard for CMock, mostly because of those ifdefs, and the options are a hand-written API header, a configured preprocessor, or a thin wrapper you own.[^5] That is not incompetence in the test tool. It is the HAL doing what a HAL is for — presenting a huge, configurable C API to application code — colliding with what a mock needs, which is a small, closed surface. The useful move is the one ThrowTheSwitch has recommended since the nested-header thread: do not mock the vendor tree. Mock _your_ wrapper. `adc_start_calibration()` is one function. It is yours. The test names the contract you care about. The HAL stays an implementation detail of the target build. If you skip the wrapper and mock `HAL_*` directly, you are writing tests that break when Cube regenerates a header, and that still say nothing about the register write. ![Diagram contrasting a CMock STM32 HAL unit test that verifies a function call with a simulator or board test that exercises the ADC](/blog/mocking-stm32-hal-unit-test/image-2.webp) ### Pick the judge that can refute the claim If the claim is "this module requests calibration before it reads," a mock is the right judge. Fast, deterministic, no board. If the claim is "this Nucleo starts the ADC and the values are not garbage," you need a model of that ADC or the Nucleo. Most firmware bugs that waste an afternoon are the second kind. Most firmware unit tests that are easy to write are the first kind. Both are worth having. Calling the first one "we mocked the hardware" is how you skip the second and still feel covered. The October thread got the build green. That is the start of a unit test, not the end of a hardware test. ### Sources [^1]: Stack Overflow, "Mocking nested functions in a Hardware Abstraction Layer," 2025-10-05. https://stackoverflow.com/questions/79783009/mocking-nested-functions-in-a-hardware-abstraction-layer [^2]: Stack Overflow, "Mocking/Stubbing HAL_UART_Receive() for the STM32," 2025-01-15. https://stackoverflow.com/questions/79358148/mocking-stubbing-hal-uart-receive-for-the-stm32 [^3]: Stack Overflow, "How do I create an optimal, customizable Ceedling configuration for STM32 unit testing?," 2024-12-18. https://stackoverflow.com/questions/79292106/how-do-i-create-an-optimal-customizable-ceedling-configuration-for-stm32-unit-t [^4]: ThrowTheSwitch, "Mocking nested headers (or something like it)," Ceedling issue 171, 2017-05-23. https://github.com/ThrowTheSwitch/Ceedling/issues/171 [^5]: ThrowTheSwitch, "Creating Tests," Ceedling issue 503, 2020-07-03. https://github.com/ThrowTheSwitch/Ceedling/issues/503 --- ## Firmware fuzzing in emulation works. Now you trust a peripheral model. (https://veecle.ai/blog/firmware-fuzzing-emulation, 2026-08-14) You can fuzz firmware without a board. That part is settled, and it has been settled since 2020. What people skip is the second half. The fuzzer still needs something to answer its reads. Take the chip out and a model answers instead. Every result you get is a claim about that model, and the model is code someone wrote, inferred, or guessed. ### The research stack that runs with no board Three papers cover the ground, and each one deletes a different layer. **P2IM** builds an abstract peripheral interface model automatically, by watching how the firmware itself touches registers, then drives QEMU with it. No hardware, no vendor spec, no manual per-peripheral work.[^1] **HALucinator** does not model the peripheral at all. It matches the HAL functions in the binary and swaps in host handlers. If you never execute the driver, you never need the register semantics under it.[^2] **Fuzzware** goes the other way and models MMIO precisely, at access-level granularity, so the fuzzer stops wasting mutations on input bits the firmware cannot act on. The evaluation covers 19 platforms and 77 firmware images, reports up to 3.25x the coverage of prior work, and found 15 new bugs that became 12 CVEs.[^3] Those are the numbers worth quoting, because they come with a paper you can read. ![Diagram of a firmware stack showing where HALucinator, Fuzzware and P2IM replace hardware with a model and where GDBFuzz keeps the real chip](/blog/firmware-fuzzing-emulation/image-1.webp) ### The boring, supported version is Renode plus AFL++ Antmicro shipped this in October 2023, as part of the aSSIsT project. It needs no changes to the simulator. Renode's own hooks do the work: `SetHookAtBlockBegin` fills AFL's coverage map in shared memory on every executed block, and a quantum hook feeds the fuzzer's byte stream into the machine.[^4] The demo treats those bytes as UART input to a Zephyr `echo` sample on an EFR32MG board, patched so the letter `a` jumps to address 0x0. A jump to 0 is the crash signal. You are fuzzing the whole image, driver layer included, not a harnessed subsystem. The `a` to 0x0 patch is only the crash oracle. Antmicro calls the whole-image setup complementary rather than better. The catch is one sentence in their post that most readers skate past. It is up to you to decide how to interpret and feed the byte stream. That decision is the harness, and the harness decides which bug class you are able to find at all. Bytes as UART characters finds parser bugs. Bytes as a length-prefixed packet finds different ones. Neither finds what you did not wire up. ### GDBFuzz is the hardware-backed contrast GDBFuzz keeps getting filed under hardware-free fuzzing. It is the opposite, and the config file says so. Bosch Research's tool takes coverage feedback from hardware breakpoints on the microcontroller, read through a GDB server attached to a debug probe.[^5] The repo's setup instructions are a tour of debug hardware: `st-util` for an STM32 B-L4S5I-IOT01A, `pyocd` for a CY8CKIT-062, OpenOCD with a Segger J-Link for an ESP32, `mspdebug` for an MSP430.[^6] Ghidra recovers the control flow graph, GDBFuzz places a bounded number of breakpoints (`max_breakpoints` in the config) and rotates them once they stop hitting. There is a QEMU target mode. The README explains why it exists: the evaluation is split into GDBFuzz "directly on the hardware", which is the intended setup, and an emulated run so other people can reproduce and compare the results. The requirements list for a new board is a microcontroller with hardware breakpoints and a GDB-compliant debug probe. That is not a hardware-free tool with an optional board. The bugs it found came off real devices: an infinite loop in the STM32 USB device stack, plus a buffer overflow and a null pointer dereference in the Cypress JSON parser. The trade is honest and explicit. Cortex-M parts give you a handful of hardware breakpoints, so coverage is sampled and rotated rather than complete. You accept worse feedback to keep the real chip as the oracle. ![Diagram comparing an emulated AFL++ and Renode fuzzing loop with GDBFuzz taking sampled coverage from hardware breakpoints on a real board](/blog/firmware-fuzzing-emulation/image-2.webp) ### Two nearby tools people file in the wrong bucket Tardigrade gets miscredited as Antmicro fuzzing. It is a third-party fault-injection testbed that runs OTA bootloaders under Renode and checks recovery from power loss, interrupted erase, bit corruption, instruction skip, and similar faults.[^8] Its fuzzer bridge consumes crash artifacts from libFuzzer, AFL or honggfuzz; it does not make Tardigrade a fuzzer. The actual work still happens in hand-written peripheral models with fault hooks. Same lesson: the model is the product. SAFIREFUZZ sits on the other side. It drops the emulator and runs ARM firmware natively on a same-architecture host, replacing HAL functions as it goes. Its paper reports a 690x average throughput increase over HALucinator and up to 147x over Fuzzware.[^7] Those are results from its own target set and comparison setups, not a shared bake-off. Near-native speed also gives up the emulator introspection you may have wanted in the first place. ### What no-hardware fuzzing cannot prove A clean run means the campaign found no failing execution in firmware running against that model. That is a smaller claim than it sounds. - **A missing crash proves nothing about the registers you never modeled.** P2IM infers peripheral behavior from firmware access patterns. If the inference is wrong in the direction of "permissive", the fuzzer walks past the bug. - **Timing and analog behavior are not in scope.** I wrote up the full list in [what simulation still cannot catch](/blog/what-simulation-cant-catch). - **Errata are absent unless someone models them explicitly.** Most peripheral models do not promise to reproduce every documented silicon mistake. - **Model bugs produce phantom crashes.** Every one of those costs a triage afternoon, and enough of them train the team to ignore the fuzzer. The capability that survives is worth having anyway: no-hardware fuzzing can find parser, state-machine and protocol bugs at a scale no board farm can reach. A clean campaign does not prove the device survives hostile input. Those are two different sentences, and firmware teams keep printing the first one and reading the second. Then it has to reach CI, which is where most firmware pipelines already stop short. [Firmware CI ends at the linker](/blog/firmware-ci-ends-at-the-linker) for most teams, and a fuzzing campaign that only runs on someone's laptop is a hobby. ![Table graphic separating firmware bugs emulated fuzzing can find from failures only real silicon can expose](/blog/firmware-fuzzing-emulation/image-3.webp) ### The part an agent gets wrong An agent can write a fuzzing harness in a minute. Feed it the Antmicro post and it will produce a working `quantum_hook`. What it cannot do is tell you whether the peripheral model underneath is honest, because nothing in the loop reports that. The run comes back green either way. That is the same gap I hit writing about how [LLMs write good firmware and cannot prove it](/blog/llms-write-good-firmware-cant-prove-it). Generation is cheap now. The judge is the expensive part, and hardware-free fuzzing is a judge you have to build before you can use it. So build it, run it, and be precise in the write-up about which one found the bug: the firmware, or the model standing in for the chip. ### Sources [^1]: Feng et al., "P2IM: Scalable and Hardware-independent Firmware Testing via Automatic Peripheral Interface Modeling," USENIX Security 2020. https://arxiv.org/abs/1909.06472 [^2]: Clements et al., "HALucinator: Firmware Re-hosting Through Abstraction Layer Emulation," USENIX Security 2020. https://www.usenix.org/system/files/sec20summer_clements_prepub.pdf [^3]: Scharnowski et al., "Fuzzware: Using Precise MMIO Modeling for Effective Firmware Fuzzing," USENIX Security 2022. https://www.usenix.org/system/files/sec22summer_scharnowski.pdf [^4]: Antmicro, "Fuzzing Zephyr with AFL and Renode," 2023-10-04. https://antmicro.com/blog/2023/10/fuzzing-zephyr-with-afl-renode/ [^5]: Eisele et al., "Fuzzing Embedded Systems using Debug Interfaces," ISSTA 2023. https://doi.org/10.1145/3597926.3598115 [^6]: Bosch Research, "GDBFuzz: Debugger-Driven Fuzzing," companion code. https://github.com/boschresearch/gdbfuzz [^7]: Seidel et al., "Forming Faster Firmware Fuzzers," USENIX Security 2023. https://www.usenix.org/system/files/usenixsecurity23-seidel.pdf [^8]: Berkman, "tardigrade: OTA firmware update fault injection testbed." https://github.com/neilberkman/tardigrade --- ## MC/DC is not branch coverage with extra steps (https://veecle.ai/blog/mcdc-coverage-embedded, 2026-08-14) MC/DC — modified condition/decision coverage — asks one question that branch coverage does not. Branch coverage asks whether `if (a && b)` went both ways. MC/DC asks whether you showed that `a` changes the outcome on its own, and that `b` does too. In practice that is one or two extra test cases per decision, and a pile of confusion produced by arguing about the standard instead of looking at the expression. So look at the expression. ### The two words you have to get right A **condition** is a boolean expression containing no boolean operators. A **decision** is a boolean expression composed of conditions and zero or more boolean operators, and a decision without an operator is a condition. Those are the definitions from the DO-178B glossary, reproduced in the NASA tutorial that is still the best free explanation of the criterion.[^1] Two consequences that people miss. First, a decision is not "an `if`". `bool ok = a && b;` is a decision even though nothing branches. The certification authorities wrote a whole position paper to close that loophole, CAST-10, and the FAA's own tool study restates its conclusion: all boolean expressions in a program are subject to decision coverage or MC/DC, not just the ones that steer control flow.[^2][^3] Second: if a condition appears more than once in a decision, each occurrence is a distinct condition.[^1] ### `a && b` needs three tests, not four | a | b | `a && b` | | --- | --- | -------- | | F | F | F | | F | T | F | | T | F | F | | T | T | T | To show `a` has an independent effect, pair row TT with row FT: only `a` moved, and the outcome moved. To show `b`, pair TT with TF. Three rows do the job. Row FF is never needed. That is the whole trick, and it often scales to n+1 tests for a decision with n independent conditions.[^1] The alternative that guarantees the same thing by brute force is multiple condition coverage: every combination, 2^n tests. At four conditions that is 5 versus 16. At ten it is 11 versus 1024. MC/DC exists because someone did that arithmetic and wanted the strength without the bill. Branch coverage on the same expression gets you there with two tests and tells you nothing about `b`. GCC's manual shows the gap in its own example. Compile `if (a && b)` with `a = 1, b = 0`, run it once, and `gcov --conditions` reports: ``` 1: 8: if (a && b) condition outcomes covered 1/4 condition 0 not covered (true false) condition 1 not covered (true) ``` Both conditions were evaluated. One outcome out of four counted.[^4] ![Truth table for a && b with the two MC/DC independence pairs marked, showing three of the four rows are enough](/blog/mcdc-coverage-embedded/image-1.webp) ### Unique cause, masking, and the same variable twice There are two accepted ways to demonstrate that independent effect. **Unique cause**: between the two tests in a pair, only the condition of interest and the outcome may change. Everything else is held fixed. You do not need to look at the logic of the decision to believe the result. **Masking**: more than one input may change between the two tests, as long as analysis of the decision's internal logic shows the others could not have affected the outcome. Short-circuit evaluation is the everyday case. In `a || (b && c)`, once `a` is true, nothing after it can move the result. GCC's docs put it plainly: an outcome counts as covered only if it had an independent effect, and in a masked position it does not, even though the condition was evaluated.[^4] Now the repeated condition. Take `Z = (a && b) || (a && c)`. | a | b | c | Z | | --- | --- | --- | --- | | T | T | F | T | | F | T | F | F | Three inputs, four conditions, because each occurrence of `a` counts separately.[^1] Those two rows show the independent effect of the _input_ `a`. They cannot show the independent effect of either _occurrence_, because you cannot vary one occurrence of a variable while holding the other fixed. The FAA tool study calls this a primary source of confusion about MC/DC, and it is the reason masking exists at all: unique cause simply cannot be applied to coupled conditions.[^3] A 2001 FAA study compared the forms and concluded masking should be the preferred one.[^5] Practical version: if your tool reports coverage you cannot explain, look for a variable used twice in one decision. Then split the decision. ![Diagram comparing unique-cause and masking MC/DC test pairs, and a coupled repeated condition](/blog/mcdc-coverage-embedded/image-2.webp) ### Who actually requires this DO-178B/C, for Level A airborne software: MC/DC is the structural coverage objective at the top design assurance level. The FAA research states it directly, and the tool market exists because of it.[^5] ISO 26262 is a different animal, and summaries flatten it. The GCC paper says MC/DC is "mandated by standards such as DO-178 and ISO26262".[^6] That compression is where arguments start. ISO 26262 is paywalled and I have not read it, so I will not quote it or tell you what your assessor accepts. Its software-unit-testing methods come as tables with a recommendation scale, and secondary summaries place MC/DC as highly recommended at ASIL D rather than flatly required. If someone says it is mandatory, ask which table, which ASIL, and which recommendation level. Nothing here is compliance advice. ### Which program did you measure? MC/DC is defined on source constructs. Your target runs object code, and the two come apart the moment the optimizer does its job. The tutorial has a section on exactly that: nothing stops you analyzing object code, but showing that object-level coverage is equivalent to source-level coverage is not trivial in the general case. Short-circuit control forms simplify it. Leaning on compiler behavior for that argument may mean qualifying the compiler feature as a verification tool.[^1] The two open toolchains sit on opposite sides of that line. | | Clang | GCC | | ----------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------ | | Flag | `-fcoverage-mcdc` (with `-fprofile-instr-generate -fcoverage-mapping`) | `-fcondition-coverage`, then `gcov --conditions` | | Operates on | AST and preprocessor information — source-based coverage mapping | Compiler control flow graph — CFG-based condition coverage | | Form | test-vector based, capped at 32767 terms per expression | masking MC/DC | | Languages | C, C++ | any front-end whose CFG looks like a BDD: C, C++, D, Rust (experimental) | Clang's docs call MC/DC "most applicable to the embedded space" and document the caps, including the escape hatch when an expression explodes into too many test vectors.[^8] GCC 14 got it in 2024 by analyzing the compiler's CFG as a binary decision diagram rather than the syntax tree, which is why one implementation covers several languages.[^6][^9] That does not by itself make the report equivalent to coverage measured on the final object code; source-to-object equivalence remains a separate assurance question. Clang's landed a year earlier with a six-condition limit, lifted the year after.[^6] Separate work covers MC/DC for Rust.[^10] Free MC/DC in a mainstream compiler is new. It was a paid-tool feature for two decades. If the last time you looked it cost five figures, look again. ![Diagram showing MC/DC instrumented at the source AST level versus the object control flow graph](/blog/mcdc-coverage-embedded/image-3.webp) ### What MC/DC does not prove This is the part that gets skipped, and it is why the metric has a bad name. Structural coverage analysis exists to determine which code structure was _not_ exercised by the requirements-based tests.[^1] It is a check on your test suite, run after the tests are written from requirements. Read backwards — write tests until the number turns green — it inverts into something close to useless. Rushby's summary of the Heimdahl, George and Weber experiment is worth the discomfort: tests generated automatically to hit MC/DC on a flight guidance model detected relatively few seeded bugs, and generally did worse than random testing.[^11] Three things the number cannot see: - **Code that is not there.** A missing requirement produces no uncovered branch. 100% MC/DC on the wrong logic is 100%. - **Unintended function.** In the tutorial's words, structural testing fails to assure that there are no unintended functions. At best it confirms the object code and processor implement the source.[^1] - **Everything physical.** Interrupt timing, a peripheral that needs a settling delay, a DMA race, a register written in the wrong order, current draw. Full MC/DC on host builds says nothing about any of it. That split is [MIL, SIL, PIL, HIL](/blog/mil-sil-pil-hil-for-firmware-people), and [simulation versus HIL](/blog/simulation-vs-hil-what-belongs-in-ci) is where it lands in CI. A gap in coverage is still information. When requirements-based tests fail to reach MC/DC, the tutorial's list of causes is short and every entry is actionable: inadequate test cases, inadequate requirements, dead code, or deactivated code.[^1] Dead code gets removed. The rest gets explained. That loop lives with the rest of the evidence in [a firmware validation cycle](/blog/anatomy-of-a-firmware-validation-cycle). ### Where I would start Build your logic-heavy modules for the host, turn on `-fcondition-coverage` or `-fcoverage-mcdc`, and read the uncovered outcomes on the decisions you already care about. Keep decisions small enough that n+1 is a number you can look at. If repeated conditions make the reported independence pairs impossible to explain, simplify or decompose the decision deliberately. Then go argue about the standard, if you still have to. The criterion is not hard. It is three test cases for `a && b` and a rule about what counts as one condition. Everything else is procedure around it. ### Sources [^1]: Hayhurst, Veerhusen, Chilenski, Rierson, "A Practical Tutorial on Modified Condition/Decision Coverage," NASA/TM-2001-210876, 2001. Definitions, the n+1 minimum, source-versus-object coverage (2.5.1), and the DO-178B guidance on unachieved coverage. https://ntrs.nasa.gov/citations/20010057789 [^2]: Certification Authorities Software Team, "What is a 'Decision' in Application of Modified Condition/Decision Coverage (MC/DC) and Decision Coverage (DC)?," Position Paper CAST-10, June 2002. The FAA has since removed the CAST papers from its site; this is the archived copy, which I could not fetch directly at the time of writing. https://web.archive.org/web/20170702144954/https://www.faa.gov/aircraft/air_cert/design_approvals/air_software/cast/cast_papers/media/cast-10.pdf [^3]: FAA, "Software Verification Tools Assessment Study," DOT/FAA/AR-06/54, 2007. Summarizes CAST-10 and CAST-6, and section 4.3.3 covers multiple occurrences of a condition. https://www.faa.gov/sites/faa.gov/files/aircraft/air_cert/design_approvals/air_software/AR-06-54_VerificationTools.pdf [^4]: GCC manual, "Invoking Gcov" (`--conditions`) and "Instrumentation Options" (`-fcondition-coverage`). https://gcc.gnu.org/onlinedocs/gcc/Invoking-Gcov.html [^5]: Chilenski, "An Investigation of Three Forms of the Modified Condition Decision Coverage (MCDC) Criterion," DOT/FAA/AR-01/18, April 2001. https://rosap.ntl.bts.gov/view/dot/42764 [^6]: Kvalsvik, "Modified Condition/Decision Coverage in the GNU Compiler Collection," arXiv:2501.02133, January 2025. https://arxiv.org/abs/2501.02133 [^8]: Clang documentation, "Source-based Code Coverage," MC/DC Instrumentation. https://clang.llvm.org/docs/SourceBasedCodeCoverage.html [^9]: Kvalsvik, "MC/DC merged into gcc," patch.no, 18 April 2024. https://patch.no/blog/2024-04-18-mcdc-merged-into-gcc/ [^10]: Zaeske, Albini, Gilcher, Durak, "Towards Modified Condition/Decision Coverage of Rust," arXiv:2409.08708, 2024. https://arxiv.org/abs/2409.08708 [^11]: Rushby, "Automated Test Generation and Verified Software," VSTTE position paper, summarizing Heimdahl, George and Weber on MC/DC-targeted test generation for a flight guidance model. https://www.vstte.ethz.ch/pdfs/vstte-rushby-position.pdf --- ## Embedded simulation cost: published prices stop where the checkout stops (https://veecle.ai/blog/embedded-simulation-cost, 2026-08-14) I spent an afternoon trying to price the whole embedded simulation ladder, bottom to top, using only numbers a stranger can open in a browser. The bottom two tiers took ninety seconds. The top two took a procurement database. The easy story is that the prices vanish because fidelity gets expensive, and expensive things get negotiated. The public evidence supports a narrower answer. Published prices disappear when the buying motion moves from checkout to configuration and procurement. The available records do not reveal a clean fidelity threshold. Once you see that, the ladder stops looking like a cliff and starts looking like two different businesses stacked on top of each other. I use "simulation ladder" broadly here. Its upper rungs are remote benches and closed-loop HIL, not simulation in the same sense as Wokwi or Renode. They belong in the comparison because firmware teams budget across all of them to answer progressively more physical questions. ### First, kill the six-figure floor The line you hear is that hardware-in-the-loop starts at six figures. It is repeated enough that people budget around it, which usually means they never open the conversation at all. Public spending records disagree. NASA obligated **$24,281.18** to dSPACE Inc. on 2022-10-25, contract 80NSSC23PA076, described in the award record as "MICRO LAB BOX HARDWARE IN THE LOOP SYSTEM AND SOFTWARE."[^1] The same agency obligated **$44,910.00** to the same vendor on 2024-08-01, contract 80NSSC24PB920, described as "DSPACE HARDWARE-IN-THE-LOOP SYSTEM."[^2] Texas A&M University at Qatar ran a public bid for two Typhoon HIL404 real-time simulators — hardware, permanent academic device licenses, premium toolbox packages, six breakout boards, six dSPACE interface boards, and shipping to Doha. Total awarded: **$56,000.00**.[^3] The same university's transparency portal carries a bid for National Instruments PXI systems — four PXIe-1062Q chassis, four PXIe-8840 controllers, waveform generators, digitizers, cabling — at an estimated value of **$193,625.44**.[^4] That figure is the estimate written on the bid document, not an award total, and PXI is a modular instrument platform rather than a HIL rig as such. It is, however, how a lot of labs actually build one. These four public records span roughly **$24K to $194K**. Three sit below six figures, which is enough to kill the claim that HIL universally starts there. It is not enough to establish a market floor: configurations, existing licenses, support and procurement terms differ. One more thing those four records have in common: not one of dSPACE, Typhoon HIL, or National Instruments publishes a list price for these configurations. Every number above exists because a public institution had to disclose what it spent. That is not a price list. It is procurement evidence. ![Diagram of five embedded simulation tiers with a line marking where self-serve pricing ends and quote-only pricing begins](/blog/embedded-simulation-cost/image-1.webp) ### The five tiers, with what is actually public This is a map of public purchasing units, not an apples-to-apples total-cost comparison. A seat-month, a runner-minute, a core-hour, a probe and a configured HIL procurement buy different things. | Tier | What it is | Public price | What you get for it | | ----------------------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | Browser simulator | Wokwi | €0 Community, €5.60/mo Hobby, €8.10/mo Hobby+, €20/seat/mo Pro, all annual billing[^5] | A circuit at a URL. Pro adds 1,000 fast build minutes and 2,000 CI minutes | | Open-source emulator | Renode, QEMU | $0 license (Renode is MIT[^6]); compute is $0.006/min on a GitHub-hosted Linux 2-core x64 runner[^7] | Register-level peripheral models, scriptable regressions, no vendor relationship | | Vendor virtual platform | Arm Virtual Hardware powered by Corellium | $0.50 per core hour on AWS Marketplace[^8] | Cloud-native Arm boards over REST, billed only while running | | Remote bench automation | Probe farms | J-Link BASE Classic $598, J-Link Pro PoE $1,680[^9] | Real silicon, reachable from CI — the probes are priced, the rack around them is not | | Closed-loop HIL | dSPACE, Typhoon HIL, NI PXI | No list price published; procurement records span $24,281.18 to $193,625.44[^1][^2][^3][^4] | Plant models, real-time I/O, the only tier that can close a control loop | Read that table top to bottom and the fidelity story looks convincing. Read the price column and something else shows up. ### Where the number actually disappears Wokwi publishes four tiers to the cent. Wokwi also sells a Classroom license, and the Classroom button says "get a quotation."[^5] Same simulator, same fidelity, same peripherals. One channel has a checkout, the other has a form. The price is public on exactly one of them. Corellium is the sharper version. Its virtualization powers Arm Virtual Hardware, sold on AWS Marketplace at a published $0.50 per core hour, with the billing unit spelled out in the listing: one processor core running for one hour, each core counted separately.[^8] Corellium's enterprise tiers are quote-only. I am not going to put a number on them, because there isn't one to put — an undisclosed price is undisclosed, not "around" anything. Identical technology, two channels, one published rate. That is the whole pattern. AWS Marketplace forces a unit and a rate into a public page, so the rate is public. A field sales team does not, so it isn't. Nothing about the silicon model changed between those two sentences. The tier where prices stop is tier 4, not tier 5. Debug probes are catalog items with sticker prices — SEGGER lists J-Link BASE Classic at $598 and J-Link Pro PoE, the model explicitly sold for test farms, at $1,680.[^9] The moment you ask what it costs to turn forty of those into a scheduled, remotely reachable board farm, you are in a conversation, not a cart. Remote bench automation is where the ladder goes quiet, and it is a full tier below the one everyone blames. ![Chart plotting four public hardware-in-the-loop procurement figures from $24,281 to $193,625 against a $100,000 line](/blog/embedded-simulation-cost/image-2.webp) ### Cost is not fidelity, and the ladder is not a queue The temptation with a table like that is to read price as a proxy for how much truth you get. It is not, in either direction. A GitHub-hosted Linux runner at $0.006 a minute, driving Renode, will catch a regression on every commit across as many board variants as you care to define. A $193,625 PXI rack will not do that, because there is one of it and forty people want it on Thursday. That is the scarcity problem I wrote about in [simulation vs HIL: what belongs in CI](/blog/simulation-vs-hil-what-belongs-in-ci) — the expensive tier is expensive per _slot_, and slots are what you run out of. Going the other way: a $598 probe on a real Nucleo will show you brown-out behavior, analog settling, and clock drift that no tier below it models at all. A third-party comparison of Wokwi against Renode and QEMU puts the same ceiling in writing: Wokwi "does not simulate timing-precise peripherals or complex real-time behavior. Always verify with physical hardware for production systems."[^10] Cheapest tier, and even outside reviewers agree on where it stops. The tier map in [Wokwi vs Renode vs QEMU](/blog/wokwi-vs-renode-vs-qemu) is a routing decision, not a ranking, and the price column does not change that. The useful mental model: tiers 1 through 3 buy you _repetitions_. Tier 5 buys you _verdicts_ on things only physics can answer. Tier 4 is the one that decides whether the verdicts arrive in a pull request or in a lab notebook three weeks later, and it is also the one nobody will quote you on a web page. ### Budgeting this without a price list Three questions, in order. **What claim are you trying to refute?** If it is "this build boots and this state machine advances," you are shopping in tiers 1 to 3, where everything is priced and you can compute your annual bill in a spreadsheet this afternoon. If it is "this ECU behaves under a real plant model," you are in tier 5 and you are getting on a call. **How many slots do you need, not how much fidelity?** The $24K NASA line item and the $194K TAMU bid differ mostly by channel count and I/O, not by whether they are "real" HIL. Size the I/O first; the price follows it far more tightly than it follows the brand. **Is your bottleneck actually tier 4?** A pile of boards CI cannot reach is not a simulation budget problem. It is a bench automation problem wearing a simulation budget's clothes, and it is exactly the tier where you will have to ask for a number instead of reading one. If you are starting from "which simulator," the ladder itself is laid out in [Wokwi alternatives for professional firmware work](/blog/wokwi-alternatives-professional-firmware). This post is only about what each rung costs, and about the fact that the answer stops being public well before the fidelity gets interesting. ### Sources [^1]: USAspending.gov, award 80NSSC23PA076, NASA to DSPACE INC, signed 2022-10-25, total obligated $24,281.18, description "MICRO LAB BOX HARDWARE IN THE LOOP SYSTEM AND SOFTWARE." https://www.usaspending.gov/award/CONT_AWD_80NSSC23PA076_8000_-NONE-_-NONE- [^2]: USAspending.gov, award 80NSSC24PB920, NASA to DSPACE INC, signed 2024-08-01, total obligated $44,910.00, description "DSPACE HARDWARE-IN-THE-LOOP SYSTEM." https://www.usaspending.gov/award/CONT_AWD_80NSSC24PB920_8000_-NONE-_-NONE- [^3]: Texas A&M University Procurement Services, contract transparency portal, bid document 8862, "HIL404 Real-Time Simulator," Texas A&M University at Qatar; total bid and total awarded $56,000.00. https://spar.tamu.edu/Transparency/GetFile?dir=ItbRfpDocs&id=8862 — browsable index at https://purchasing.tamu.edu/purchasing/spar.html [^4]: Texas A&M University Procurement Services, contract transparency portal, bid document 8036, "PXI Systems; National Instruments for Texas A&M University at Qatar," estimated value $193,625.44. https://spar.tamu.edu/Transparency/GetFile?dir=ItbRfpDocs&id=8036 — browsable index at https://purchasing.tamu.edu/purchasing/spar.html [^5]: Wokwi, "Wokwi Plan and Pricing." Community €0/mo, Hobby €5.6/mo, Hobby+ €8.1/mo, Wokwi Pro €20/seat/mo, all annual billing; Pro includes 1,000 fast build minutes and 2,000 CI minutes; Classroom is quotation-based. https://wokwi.com/pricing [^6]: Renode, GitHub repository ("License & contributions"). https://github.com/renode/renode [^7]: GitHub Docs, "Actions runner pricing." Linux 2-core (x64), billing SKU `actions_linux`, $0.006 per minute. https://docs.github.com/en/billing/reference/actions-minute-multipliers [^8]: AWS Marketplace, "Arm Virtual Hardware powered by Corellium," sold by Corellium, Inc. Usage cost dimension "core hours," $0.50 per core hour. https://aws.amazon.com/marketplace/pp/prodview-3ert36ne4rlmq [^9]: SEGGER USA web shop, "J-Link (Debug Probe)" category. J-Link BASE Classic (8.08.00) $598.00; J-Link Pro PoE (8.12.28) $1,680.00. https://shop-us.segger.com/product-category/debug-probes/jlink/ [^10]: Industrial Monitor Direct, "Wokwi vs Renode vs QEMU: Embedded System Simulators Comparison" — a third-party vendor guide, not an official Wokwi publication — as previously cited in our [Wokwi alternatives](/blog/wokwi-alternatives-professional-firmware) writeup. https://industrialmonitordirect.com/blogs/knowledgebase/wokwi-vs-renode-vs-qemu-embedded-system-simulators-comparison --- ## RTOS trace in a simulator: the format was never the hard part (https://veecle.ai/blog/rtos-trace-simulator, 2026-08-14) I wanted a scheduler timeline out of a simulated board. No probe, no bench, just a run in CI that tells me which thread held the CPU and when it gave it up. The tracing format turned out to be the easy part. The pipe the bytes come out of is where the whole thing lives or dies, and that is the part nobody writes about. ### Three unrelated things are called "trace" **RTOS event trace** is the kernel telling you about itself. Zephyr's hooks fire on thread switches, semaphore operations, ISR entry and exit, and the format layer serializes each one into an event. This is the only one of the three that knows what a thread is. **CPU instruction trace** is the simulator recording what the core executed. Renode does this with `cpu CreateExecutionTracing`, in `PC`, `Opcode`, `PCAndOpcode` or `Disassembly` mode, and the output feeds coverage tooling.[^1] It is a superb receipt for which lines ran. It has no idea that a scheduler exists, because a context switch is just more instructions. **Waveform capture** is pin state over time, the thing a logic analyzer draws. Renode does not have it. A GPIO logic analyzer view has been an open feature request since June 2023.[^2] Three different questions. Teams conflate them constantly, then get frustrated that the tool they picked cannot answer the question they actually had. ![Diagram distinguishing RTOS event trace, CPU instruction trace and waveform capture as three different kinds of embedded trace](/blog/rtos-trace-simulator/image-1.webp) ### Tracealyzer and SystemView are wired to a debug probe Search `tracealyzer renode` and you find people trying to point a commercial RTOS visualizer at a simulated machine. The obstacle is transport. Percepio's TraceRecorder ships as a Zephyr module, and the stream ports exposed in Zephyr's configuration system are Ring Buffer, RTT, ITM and Semihost.[^3][^4] Ring Buffer means you dump RAM from a debugger. RTT means a host-side reader polls a control block in target RAM, commonly through J-Link tooling. ITM means the Cortex-M trace unit and a fast probe on SWO. Semihost writes a host file and is scoped to QEMU. There is no UART stream port in that list. SEGGER SystemView is blunter still. Zephyr's own documentation says the payload "relies on RTT as a transport", and that newer SystemView versions support other transports such as UART or snapshot mode, both "still not supported in Zephyr".[^5] SEGGER shipped UART and TCP/IP acquisition in SystemView V3.10 in December 2019.[^6] The host tool has had a serial recorder for over six years. The Zephyr integration still asks for the probe. That is the crux for anyone running without hardware. Delete the probe and something still has to poll the RTT control block while the core runs. Neither integration ships that compatible host-side reader for a simulated target. ### Zephyr's own CTF format does speak UART The in-tree path avoids the problem entirely. Zephyr's Common Trace Format layer is transport-agnostic, and the supported backends are UART, USB, File on the POSIX targets, RTT, and RAM.[^5] The tracing sample carries a config for exactly this combination: ``` CONFIG_TRACING=y CONFIG_TRACING_CTF=y CONFIG_TRACING_BACKEND_UART=y CONFIG_TRACING_BUFFER_SIZE=4096 ``` That is `prj_uart_ctf.conf`, verbatim, from the sample in Zephyr 4.4.1.[^7] The sample README adds the detail that decides whether any of this works: you set the `zephyr,tracing-uart` property under the devicetree chosen node, which is how you keep trace bytes off the console UART.[^8] Decoding is Babeltrace or TraceCompass pointed at a directory holding the CTF metadata file next to the captured channel. No probe, no vendor license, no RTT. On paper this is the simulator-friendly option, so I ran it. ### What actually came back Zephyr 4.4.1, the tracing sample built for the nRF52840 DK with `prj_uart_ctf.conf`, trace backend bound to UART1 so the console could keep UART0. Chiplab ran it on Renode 1.16.1 for a bounded simulation window. The run produced exactly one artifact: combined stdout. ``` $ python3 -c "b=open('stdout.bin','rb').read(); \ print(len(b), sum(1 for c in b if c < 9 or 13 < c < 32 or c > 126))" 16264 3750 $ strings -n 4 stdout.bin | grep -E 'thread_a|thread_b|idle|tracing_thread' | sort -u idle thread_a thread_b tracing_thread ``` Sixteen kilobytes, of which 3,750 bytes fall outside printable ASCII. Those are UART1 binary fragments, interleaved with console text in one stream. The visible strings are the giveaway that this is real trace payload rather than log output: `thread_a` is the `K_THREAD_DEFINE` name, `thread_b` is set by `k_thread_name_set` in the sample's own source,[^9] and `tracing_thread` is the tracing subsystem's worker, named the same way in the tracing core.[^10] Those names ride in CTF thread events. The sample's `printk` lines say "Hello World from" and look nothing like that. Enough of the chain held to put trace-shaped payload into capture. Hooks compiled in, the UART backend transmitted binary data, and trace-specific thread names survived. Without a successful decode, I cannot claim that a complete valid CTF stream survived intact. What I do not have is a clean channel file. The trace reached capture mixed into combined stdout rather than delivered as a separate decoder-ready UART stream, and I am not going to fabricate a Babeltrace session I never ran. Partial result, reported as partial. ![Diagram comparing an RTT path missing a compatible simulated-target reader with the Zephyr CTF UART path whose trace bytes merge into combined simulator stdout](/blog/rtos-trace-simulator/image-2.webp) ### A clean UART stream is necessary, not sufficient This is the part worth internalizing before you blame the emulator. A CTF decoder wants the stream and nothing but the stream. Anything else in the byte sequence is an event header as far as the parser is concerned. Zephyr issue 49889 shows the next wall on a physical reel_board with a real serial cable. The reporter commented out every `printk` in `main.c` so only trace data would go down the UART, captured with the in-tree `trace_capture_uart.py`, then ran a second capture specifically to dodge the boot banner. Babeltrace 1.5.8 still answered `[error] Event id 32 is unknown`, and Babeltrace 2 produced the structured equivalent.[^11] No simulator was involved anywhere in that report. Separating the streams removes one source of corruption; it does not guarantee that the remaining bytes decode. The Renode side of the same class shows up in issue 699, where semihosting UART output simply never appeared on the analyzer the user was watching.[^12] Getting a second byte stream out of a simulated machine, into its own file, is configuration work that nobody's quickstart covers, because every quickstart assumes one console and one human reading it. The fix is unglamorous and entirely mechanical: bind each UART to its own sink, keep console and trace separated from the peripheral all the way to disk, then decode. That is the next run, and it is a plumbing change, not a tracing change. ### Why this matters more for agents than for humans A human debugging with a board on the desk works around a contaminated stream without noticing. Squint at the console, ignore the garbage, move on. An agent cannot. It gets an artifact, and the artifact either parses into events or it does not. "Mostly the right bytes, in the wrong container" is worthless to a machine reader and looks identical to success from the outside, which is the failure mode I care about most. The run exits zero. Something got captured. Only a decode attempt tells you whether observability actually happened. That is the same line I keep drawing between what simulation proves and what it merely runs. Instruction trace and event trace both come free of hardware, and [neither covers what simulation still cannot catch](/blog/what-simulation-cant-catch). Deciding which of these belongs in a pipeline at all is the argument in [simulation versus hardware in the loop](/blog/simulation-vs-hil-what-belongs-in-ci), and the habit of checking a tool's claims against its own issue tracker is the one from [the simulator comparison audit](/blog/embedded-simulator-comparison-audit). Trace was supposed to be the observability story for firmware without hardware. It half is. The formats are open, the hooks are upstream, the decoders are free. The transport is where six years of a shipped UART recorder still has not reached the integration, and where a working RTOS trace turns into a file no decoder will read. ### Sources [^1]: Renode documentation, "Execution tracing." https://renode.readthedocs.io/en/latest/execution-tracing/execution-tracing.html [^2]: Renode issue #478, "GPIO logic analyzer view," opened 2023-06-21, still open. https://github.com/renode/renode/issues/478 [^3]: Percepio, "Getting Started with TraceRecorder on Zephyr." https://percepio.com/getstarted/latest/html/zephyr.html [^4]: Zephyr documentation, "Percepio TraceRecorder and Stream Ports" (stream port list stated as of July 2024). https://docs.zephyrproject.org/latest/services/tracing/index.html [^5]: Zephyr documentation, "Tracing" — SEGGER SystemView support and Transport Backends. https://docs.zephyrproject.org/latest/services/tracing/index.html [^6]: SEGGER, "SystemView adds data acquisition via UART and TCP/IP," 2019-12-18. https://www.segger.com/news/seggers-systemview-adds-data-acquisition-via-uart-and-tcpip/ [^7]: Zephyr v4.4.1, `samples/subsys/tracing/prj_uart_ctf.conf`. https://github.com/zephyrproject-rtos/zephyr/blob/v4.4.1/samples/subsys/tracing/prj_uart_ctf.conf [^8]: Zephyr v4.4.1, `samples/subsys/tracing/README.rst`. https://github.com/zephyrproject-rtos/zephyr/blob/v4.4.1/samples/subsys/tracing/README.rst [^9]: Zephyr v4.4.1, `samples/subsys/tracing/src/main.c`. https://github.com/zephyrproject-rtos/zephyr/blob/v4.4.1/samples/subsys/tracing/src/main.c [^10]: Zephyr v4.4.1, `subsys/tracing/tracing_core.c` (worker thread named `tracing_thread` via `k_thread_name_set`). https://github.com/zephyrproject-rtos/zephyr/blob/v4.4.1/subsys/tracing/tracing_core.c [^11]: Zephyr issue #49889, "ctf trace: unknown event id when parsing samples/tracing result on reel board," opened 2022-09-04. https://github.com/zephyrproject-rtos/zephyr/issues/49889 [^12]: Renode issue #699, "Access semihosting UART from Rust," opened 2024-11-13. https://github.com/renode/renode/issues/699 --- ## ESP-IDF 6.0 did not remove one thing. It removed four different things. (https://veecle.ai/blog/esp-idf-v6-migration, 2026-08-14) Ask anyone what changed in ESP-IDF 6.0 and you get "they removed the legacy drivers." That sentence is doing far too much work. It covers four different fates, and the difference between them is the difference between a five-minute fix and a rewrite. I went through the migration guides and the v6.0 tree to sort them out. ### Four fates, one word **Removed.** The header is gone. The build fails. You port the code. **End-of-life but still shipping.** The header is still there, still compiles, and prints a warning telling you the clock is running. **Moved out of the tree with its API unchanged.** The component left ESP-IDF for the Component Registry. You add a dependency line. **Moved with code changes, renamed, or refactored.** The peripheral is fine, but a component name, symbol, enum member, or implicit include changed underneath you. Three of those four can break a build, and each demands a different fix. Treating them as one bucket is how a half-day migration turns into a week. ![Diagram sorting ESP-IDF 6.0 changes into removed drivers, end-of-life drivers, components moved unchanged, and components or symbols moved or refactored](/blog/esp-idf-v6-migration/image-1.webp) ### The nine that are actually gone v6.0 shipped on 20 March 2026.[^1] These legacy drivers are removed outright: ADC (`driver/adc.h`), DAC (`driver/dac.h`), I2S (`driver/i2s.h`), timer group (`driver/timer.h`), PCNT (`driver/pcnt.h`), MCPWM (`driver/mcpwm.h`), RMT (`driver/rmt.h`), sigma-delta (`driver/sigmadelta.h`), and temperature sensor (`driver/temp_sensor.h`).[^2] The pattern is identical for each. The peripherals migration guide on ADC: > The legacy ADC driver `driver/adc.h` is deprecated since version 5.0 [...]. > Starting from version 6.0, the legacy driver is completely removed.[^2] None of this was a surprise. Every one of the nine was deprecated in 5.0 and carried a warning for a full major version before it went. The replacements are already documented: `esp_adc/adc_oneshot.h` for ADC, `driver/gptimer.h` for the timer group, `driver/pulse_cnt.h` for PCNT, `driver/sdm.h` for sigma-delta, and `driver/temperature_sensor.h` for the temperature sensor.[^2] Six years of Stack Overflow answers still use the old ones. ### I2C is the one everyone gets wrong Legacy I2C was **not** removed in 6.0. It is the odd one out, and it is the claim I see stated backwards most often. `driver/i2c.h` is still in the v6.0 tree at `components/driver/i2c/include/driver/i2c.h`. It still compiles. What it does now is shout at you, via a `#pragma message` guarded by a Kconfig option: ```c #if !CONFIG_I2C_SUPPRESS_DEPRECATE_WARN #pragma message("\n" \ "================================ CRITICAL WARNING ================================\n" \ "This legacy I2C driver (driver/i2c.h) is officially END-OF-LIFE (EOL) as of ESP-IDF v6.0.\n" \ "\n" \ "1. NO SUPPORT: ESP-IDF will not provide updates, bug fixes, or security patches timely.\n" \ "2. PLANNED REMOVAL: This driver WILL BE REMOVED in ESP-IDF v7.0 (next major release).\n" \ "3. ACTION REQUIRED: Migrate to 'driver/i2c_master.h' or 'driver/i2c_slave.h' immediately.\n" \ ``` That is verbatim from the v6.0 header.[^3] The migration guide says the same thing in calmer language: marked end-of-life in v6.0, "scheduled for removal in v7.0."[^2] So legacy I2C code builds against 6.0. It is also on a countdown, and it stops receiving fixes now rather than at removal. The old I2C _slave_ driver is a separate story — that one was redesigned in 5.4 and the old version is gone.[^2] ### What the new I2C actually looks like Here is the pair, both from Espressif's own examples, same sensor, same function name. Legacy, from the v5.1 tag: ```c static esp_err_t i2c_master_init(void) { int i2c_master_port = I2C_MASTER_NUM; i2c_config_t conf = { .mode = I2C_MODE_MASTER, .sda_io_num = I2C_MASTER_SDA_IO, .scl_io_num = I2C_MASTER_SCL_IO, .sda_pullup_en = GPIO_PULLUP_ENABLE, .scl_pullup_en = GPIO_PULLUP_ENABLE, .master.clk_speed = I2C_MASTER_FREQ_HZ, }; i2c_param_config(i2c_master_port, &conf); return i2c_driver_install(i2c_master_port, conf.mode, I2C_MASTER_RX_BUF_DISABLE, I2C_MASTER_TX_BUF_DISABLE, 0); } ``` That is `examples/peripherals/i2c/i2c_simple` in v5.1.[^4] The same job in `examples/peripherals/i2c/i2c_basic` at the v6.0 tag: ```c static void i2c_master_init(i2c_master_bus_handle_t *bus_handle, i2c_master_dev_handle_t *dev_handle) { i2c_master_bus_config_t bus_config = { .i2c_port = I2C_MASTER_NUM, .sda_io_num = I2C_MASTER_SDA_IO, .scl_io_num = I2C_MASTER_SCL_IO, .clk_source = I2C_CLK_SRC_DEFAULT, .glitch_ignore_cnt = 7, .flags.enable_internal_pullup = true, }; ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, bus_handle)); i2c_device_config_t dev_config = { .dev_addr_length = I2C_ADDR_BIT_LEN_7, .device_address = MPU9250_SENSOR_ADDR, .scl_speed_hz = I2C_MASTER_FREQ_HZ, }; ESP_ERROR_CHECK(i2c_master_bus_add_device(*bus_handle, &dev_config, dev_handle)); } ``` Also verbatim.[^5] This is not a rename. The old model is a port number plus a config struct, with the device address passed on every transfer. The new model is a bus handle, then device handles hung off it, with clock speed and address length as per-device properties. Reads go from `i2c_master_write_read_device(port, addr, ...)` to `i2c_master_transmit_receive(dev_handle, ...)`. Anything that mechanically swaps header names produces code that does not compile. The handles have to be threaded through every function that touches the bus. ![Diagram contrasting the legacy ESP-IDF I2C port-number API with the new bus-handle and device-handle model in driver/i2c_master.h](/blog/esp-idf-v6-migration/image-2.webp) ### Moved is not removed This is the other half of the confusion, and it goes the opposite way: people see a component vanish from the tree and start rewriting code that is completely fine. ESP-MQTT left ESP-IDF for the Component Registry. The protocols migration guide is explicit about what that costs you: > Include headers and APIs remain the same (`mqtt_client.h`), but the component > is fetched via the Component Manager.[^6] You run `idf.py add-dependency espressif/mqtt` and carry on. cJSON went the same way, and the guide's heading for it is literally "No Code Changes Required."[^6] `wifi_provisioning` moved out and was renamed `network_provisioning`.[^1] The Ethernet PHY and SPI module drivers moved to a separate repository, and that one _does_ remove APIs — `esp_eth_phy_new_ip101()` and friends are gone from the core tree.[^7] Same word, "removed", covering a dependency line in one case and deleted functions in another. ### The fourth fate is the one that ambushes you The last category has no migration section of its own because it is scattered through everything. A symbol moves and takes an unrelated build down with it. Issue 18410, filed ten days after v6.0 shipped: `PERIPH_I2S1_MODULE` is no longer in `soc/periph_defs.h`. The reporter notes it "was well defined and working fine in v5.5.3."[^8] Their I2S peripheral logic was not the problem. A constant they passed to `periph_module_enable()` stopped existing, as fallout from the legacy I2S removal. Elsewhere in the same guide: `sdm_channel_set_duty()` became `sdm_channel_set_pulse_density()`, the `io_od_mode` member disappeared from RMT and MCPWM configs in favor of calling `gpio_od_enable()` yourself, LCD GPIO numbers went from `int` to `gpio_num_t`, and driver headers stopped implicitly including FreeRTOS headers — so code that relied on that inclusion now needs the include it always should have had.[^2] Espressif's own examples were not immune. Issue 18379 reports `lp_core_build_system_example` failing to build on v6.0 for the ESP32-S3 with `ulp_lp_core.h: No such file or directory`.[^9] When the in-tree examples have gaps at a major release, the code out in the world has more. ### Why this is worse for an agent than for you When you hit `driver/adc.h: No such file or directory`, you search, you find the migration guide, you notice it is scoped to 6.0, and you port. You carry the version in your head the whole time. A model working from its training corpus does not have that anchor by default. The volume of ESP-IDF code written against 5.x and earlier is enormous and has been accumulating since 2016. v6.0 shipped in March. Many older tutorials, forum answers and vendor samples still use `i2c_param_config` and `i2c_driver_install`. Weight of material alone favors the old API, and much of that material is not labeled with a version at the point where it matters. I have not measured agent failure rates on v6.0 migration tasks. The closest receipt is IoT-SkillsBench, where [LLM-written skills dropped the ESP-IDF score from 31/42 to 27/42](/blog/llm-embedded-benchmark) and the authors found that synthesized skills can reinforce incorrect assumptions about ESP-IDF-specific behavior.[^10] That was measured before v6.0 added another version boundary. The mitigation is not clever prompting. It is the same one as always: make the loop close on something real. A build against the pinned IDF version answers "does this API exist" in seconds and does not care what the corpus thinks. That is cheaper than any amount of context stuffing, and it is the only step that catches the fourth fate, where nothing about the code looks wrong until the linker disagrees. Compilation is the floor, not the ceiling. Getting past the compiler on ESP32 still leaves you with peripherals a simulator will not model — Espressif's own QEMU matrix already [marks I2C, I2S, RMT and WiFi unsupported](/blog/qemu-esp32-limitations) — and substituting mocks for the driver only [tests your call contract, not the chip](/blog/mocking-stm32-hal-unit-test). ### The actual checklist Before you touch a line: figure out which of the four fates each of your build errors belongs to. Removed means port. End-of-life means schedule it, and note that legacy I2C buys you until v7.0 and no longer. A component moved with its API unchanged usually means adding a dependency. A move with API changes, a rename or a refactor means reading that component's migration section instead of guessing from the release-note verb. Then pin your IDF version somewhere a tool can read it. After March, "what does the header look like" stopped being a question with one answer. ### Sources [^1]: ESP-IDF Release v6.0, published 2026-03-20. https://github.com/espressif/esp-idf/releases/tag/v6.0 [^2]: ESP-IDF v6.0 documentation, "Migration from 5.x to 6.0 — Peripherals." https://docs.espressif.com/projects/esp-idf/en/v6.0/esp32/migration-guides/release-6.x/6.0/peripherals.html [^3]: ESP-IDF v6.0, `components/driver/i2c/include/driver/i2c.h`. https://github.com/espressif/esp-idf/blob/v6.0/components/driver/i2c/include/driver/i2c.h [^4]: ESP-IDF v5.1, `examples/peripherals/i2c/i2c_simple/main/i2c_simple_main.c`. https://github.com/espressif/esp-idf/blob/v5.1/examples/peripherals/i2c/i2c_simple/main/i2c_simple_main.c [^5]: ESP-IDF v6.0, `examples/peripherals/i2c/i2c_basic/main/i2c_basic_example_main.c`. https://github.com/espressif/esp-idf/blob/v6.0/examples/peripherals/i2c/i2c_basic/main/i2c_basic_example_main.c [^6]: ESP-IDF v6.0 documentation, "Migration from 5.x to 6.0 — Protocols." https://docs.espressif.com/projects/esp-idf/en/v6.0/esp32/migration-guides/release-6.x/6.0/protocols.html [^7]: ESP-IDF v6.0 documentation, "Migration from 5.x to 6.0 — Networking." https://docs.espressif.com/projects/esp-idf/en/v6.0/esp32/migration-guides/release-6.x/6.0/networking.html [^8]: ESP-IDF issue #18410, "In V6.0.0: PERIPH_I2S1_MODULE disappeared from the components/soc/esp32/include/soc/periph_defs.h," opened 2026-03-30. https://github.com/espressif/esp-idf/issues/18410 [^9]: ESP-IDF issue #18379, on `lp_core_build_system_example` failing to build for ESP-IDF v6.0 on esp32-s3 with error "ulp_lp_core.h: No such file or directory," opened 2026-03-22. https://github.com/espressif/esp-idf/issues/18379 [^10]: Li et al., "Skilled AI Agents for Embedded and IoT Systems Development," arXiv:2603.19583, 2026-03-20. https://arxiv.org/abs/2603.19583 --- ## Arm has its own simulator. Here is what it actually models. (https://veecle.ai/blog/arm-fvp-vs-qemu-renode, 2026-08-13) I wrote a three-way comparison of embedded simulators — [Wokwi vs Renode vs QEMU](/blog/wokwi-vs-renode-vs-qemu) — and left out the one made by the company that designs the instruction set. That was not an oversight so much as a reflex. Most conversations I have about running firmware without a board land on those three. Arm's Fast Models and the Fixed Virtual Platforms built from them rarely enter the comparison, even though Arm has shipped them for years, gives several of them away, and updates them regularly. So here is the plain version. ### What Arm's Fast Models and FVPs actually ship Fast Models is the modeling technology. An FVP is a ready-made platform assembled from it: a processor model, some memory, some peripherals, packaged as a binary you run from a shell. For Cortex-M, the ones that matter are the Corstone platforms — `FVP_Corstone_SSE-300` (Cortex-M55), `FVP_Corstone_SSE-310` (Cortex-M85), `FVP_Corstone_SSE-315` and `FVP_Corstone_SSE-320` (Cortex-M85 with Ethos-U65 or Ethos-U85, plus Mali-C55), and Ethos-U variants of the first two.[^1] An older family of bare per-core models is named after the MPS2 board: `FVP_MPS2_Cortex-M0` through `-M85`, one per core.[^1] Arm steers you off those for anything recent — "It is recommended to use Corstone-320/315/310 FVP models for Cortex-M85".[^1] The framing that explains everything else is Arm's phrase "programmer's view": > "FVPs provide Programmer's View (PV) models of processors and devices. The functional > behavior of a model is equivalent to real hardware. PV models sacrifice absolute timing > accuracy to achieve fast simulated execution speed. This means that you can use PV models > for confirming software functionality, but you must not rely on the accuracy of cycle > counts, low-level component interactions, or other hardware-specific behavior."[^2] Fast Models also produces a second kind of model, and the contrast between them is the clearest thing in Arm's documentation. An Architecture Envelope Model is "a fully-configurable, generic model of an Arm architecture. It aims to expose software bugs by modeling the range of behavior that the architecture allows."[^3] A Programmers' View model is "a high performance, functionally accurate model of a hardware platform. It can be used for booting an operating system and executing software, but not to provide hardware-accurate timing information."[^3] ### The timing story, told by Arm against its own interest I have written about [what "cycle-accurate" actually means](/blog/what-cycle-accurate-actually-means), and Arm's documentation is one of the few vendor sources that does not blur the line to make itself look better. A 2022 Arm reference-design guide says it in six words: "The FVP also cannot be used to measure software performance."[^4] And then there is the detail I find genuinely persuasive, because it is a product decision rather than a disclaimer. Arm removed a parameter that exposed the performance monitoring unit on the Cortex-M55 model. Arm staff, on Arm's own forum, explained that `has_pmu` "has been removed from the ARMCortexM55CT component... since version 11.22 because keeping this parameter is misleading... FVPs are not cycle accurate... so the values are inaccurate."[^5] A vendor deleting a feature because users kept reading real numbers into it says more than any paragraph of caveats. The honest fix was to remove the knob, and Arm removed it. ### The crux: it models Arm's designs, not anyone's chip Arm's technical overview for the Corstone SSE-300 FVP lists what is inside: > "The Corstone SSE-300 FVP models a r0p0 version of Corstone SSE-300 Subsystem. The FVP > models the following IP components: Single Arm Cortex-M55 processor with MVE extension, > Single Arm Ethos-U55 NPU, Memory Protection Controller (MPC), Peripheral Protection > Controller (PPC), Implementation Defined Attribution Unit (IDAU). The FVP does not model > every component that Corstone SSE-300 describes. For example, it does not model the > CoreSight technology components."[^6] Every item on that list is Arm IP. The same is true one level up, at the board: > "Board — The board peripherals represent peripherals that may be present on the board onto > which the SoC is mounted. The Corstone SSE-300 board model is based on the ARM MPS3 Board."[^6] So an FVP models an Arm reference subsystem, mounted on an Arm reference board, built from Arm IP, with real rigor — and models no specific vendor's silicon at all. Not an STM32. Not an nRF52. No vendor clock tree, no vendor flash controller, no vendor errata. That is the exact inverse of Renode's proposition, which exists to model named parts from named vendors, and a different point again from QEMU's generic machines. I laid out that axis in [Renode vs QEMU: what each actually models](/blog/renode-vs-qemu-what-each-models). ![Block diagram of the Arm Corstone SSE-300 FVP showing modeled Arm IP, dummy stub peripherals, and unmodeled components](/blog/arm-fvp-vs-qemu-renode/image-1.webp) #### The stubs are labeled, and that is a good sign Arm's peripheral tables mark UART3 and UART4 on the SSE-300 as "Dummy Stub" while UART0, 1, 2 and 5 are real CMSDK UARTs, and mark the CLCD configuration register as "Partial modelled". The legend is Arm's own: "Dummy Stub – These stubs have minimal implementation... Partial Modelled – Certain features are implemented."[^6][^7] I do not read that as a gotcha. A reference subsystem has no reason to model a UART nobody boots through. The same question runs through [what QEMU models on Cortex-M](/blog/qemu-for-cortex-m-what-works): which parts of a machine exist. ### The complication: two products, one brand Arm's Virtual Hardware product page describes two different families. The first is "Arm Virtual Hardware FVPs", which is "Based on Arm Fast Model technology" and covers "Corstone platforms... All Cortex-M processor cores."[^8] That is everything above. The second is "Arm Virtual Hardware Third-Party Hardware", "Underpinned by the Arm Neoverse CPU natively executing Arm code", offering "Accurate models of complete SoCs and development kits, including peripherals, sensors, and other board components. These models are not performance accurate."[^8] The product list is short and specific: "Raspberry Pi Model 4, NXP i.MX 93, NXP i.MX 8M Arm Cortex Complex, STM32U5 IoT Discovery Kit", and it is "Available via Corellium."[^8] So yes — Arm sells something that models a real STM32 development board. It is not an FVP. It runs on Corellium's virtualization technology, which Arm licensed in October 2022: "Arm is licensing Corellium's virtualization technology as part of its Arm Virtual Hardware (AVH) offering."[^9] And it is not finished: Arm's own learning material states that "Arm Virtual Hardware Third-Party Hardware is currently in public beta."[^10] That distinction is easy to lose, and I have watched people lose it mid-conversation, so here it is the long way: Fast Models and FVP technology do not model specific vendor silicon, and a separate, beta, partner-limited product under the same brand does, using different technology underneath. How faithful that second family is, nobody outside has audited in public. A DVCon paper raises the obvious caution: "Emulation may be partial. For instance, the RPI4 model does not offer GPU emulation... it is not possible to assess whether Virtual Hardware allows development of end-to-end stacks... and the accuracy of the modeling."[^11] ![Diagram showing Arm Virtual Hardware split into Fast Models FVPs and Corellium-based third-party hardware models](/blog/arm-fvp-vs-qemu-renode/image-2.webp) ### Practical friction, in the order you will hit it **There are no macOS binaries.** Arm is blunt: "Arm FVPs are not available for macOS. Refer to FVPs-on-Mac for a workaround using a Docker container."[^12] Arm maintains that workaround repository itself.[^13] Darwin is the only absence, and on my team that is the conversation. **The Corstone models are free, and documented as free.** Arm: "The Corstone-300 and Corstone-700 FVPs are now available at no cost... No license key, no fees, just one click. Your access to Corstone FVPs is free and not limited in time."[^14] Arm staff confirm the CI case separately: they "can be used freely in a CI system" and need no license key.[^15] **The packaging changed, and that matters more than the price.** Arm: "Arm Fixed Virtual Platforms (FVPs) were available as a library of ready to use platforms (and as a component of Arm Development Studio) up until version 11.28. From 11.29 onwards the FVPs are provided solely as part of Arm Development Studio."[^16] Development Studio Gold lists at "$5,170.00/year per license".[^17] The free Corstone downloads and the general library are now two different stories. **Speed: Arm publishes numbers, nobody publishes a comparison.** Arm says code-translation models run "between 20-200 MIPS on a typical workstation, enabling an OS to boot in tens of seconds".[^3] No published head-to-head against QEMU exists, so I will not quote you a ratio. ### Who should use one, and who should not This is the practical distinction the documentation makes hard to find. | If you are... | FVP is... | because | | ------------------------------------------------------------- | -------------- | ------------------------------------------------------------ | | Writing a CMSIS, TF-M or RTOS port against a core, not a chip | the right tool | the model is the architecture, from the people who define it | | Hunting undefined-but-architecturally-legal behavior | the right tool | that is literally the AEM's stated job[^3] | | Validating a driver against a vendor's peripheral | the wrong tool | your peripheral is not in there[^6] | | Chasing a timing or performance question | the wrong tool | Arm says so itself[^4] | Teams moving between simulators find the real cost is the test harness rather than the simulator, which I went through in [Renode alternatives](/blog/renode-alternatives). An FVP is cheap to add alongside something else because its scope is narrow. ### Four tiers, not three So the map I have been carrying around has one more row in it than I thought. | Tier | Example | Models | Best at | | ---------------------- | ------- | ----------------------------------- | -------------------------------------- | | Hobby / educational | Wokwi | popular boards, in a browser | learning, sharing, quick demos | | Specific silicon | Renode | named vendor parts and peripherals | driver and board-level firmware | | Generic core | QEMU | machines built around a CPU | booting, portability, breadth | | Architecture authority | Arm FVP | Arm reference subsystems and boards | conformance to the architecture itself | The gap the fourth tier does not fill is the one I spend my working life on: a specific vendor's chip, hosted, no board and no local install, that a coding agent can drive without a human in the loop. An FVP gives me the architecture but not the chip. That is why Chiplab models named vendor microcontrollers rather than reference subsystems — different tier, different question. Agents make that choice sharper. Point an agent at an FVP and it can expose architecture-level problems while leaving vendor peripherals untested. A vendor-specific model can exercise those peripherals, but only to the fidelity its implementation provides. A simulator answers the question it was built to answer, and an agent that does not know which question it asked reports success either way. ![Diagram of four embedded simulator tiers from hobby to architecture authority, placing Arm FVP alongside Wokwi, QEMU and Renode](/blog/arm-fvp-vs-qemu-renode/image-3.webp) I do not think Arm's simulator is underrated. It is useful for a narrow job and easy to miss outside that job, partly because its documentation is written as if you already work at a silicon vendor. The first useful distinction takes hours of reading to reach. It took me longer than that. Now it is one paragraph. ### Sources [^1]: Arm, "Arm Virtual Hardware — Simulation," Arm Software documentation. https://arm-software.github.io/AVH/main/simulation/html/index.html [^2]: Arm, "Fast Models Fixed Virtual Platforms (FVP) Reference Guide," Arm. https://documentation-service.arm.com/static/5f4d1264ca7b6a3399375cb4 [^3]: Arm, "What is Fast Models?," Fast Models User Guide, Arm. https://developer.arm.com/documentation/100965/1131/Introduction-to-Fast-Models/What-is-Fast-Models- [^4]: Arm, "RD-TC22 Fixed Virtual Platform," Arm developer documentation, 2022. https://developer.arm.com/documentation/108028/0000/RD-TC22-Fixed-Virtual-Platform [^5]: Arm, "Measuring performance of programs on the FVP Corstone SSE-300," Arm Community support forums. https://community.arm.com/support-forums/f/soc-design-and-simulation-forum/55657/measuring-performance-of-programs-on-the-fvp-corstone-sse-300 [^6]: Arm, "Corstone SSE-300 with Ethos-U55 FVP (MPS3) Technical Overview," Arm Software documentation. https://arm-software.github.io/AVH/main/simulation/html/Corstone_SSE-300_Ethos-U55_FVP_MPS3_Technical_Overview.pdf [^7]: Arm, "Corstone SSE-310 FVP Technical Overview," Arm Software documentation. https://arm-software.github.io/AVH/main/simulation/html/Corstone_SSE-310_FVP_Technical_Overview.pdf [^8]: Arm, "Arm Virtual Hardware," Arm products. https://www.arm.com/products/development-tools/simulation/virtual-hardware [^9]: Corellium, "Announcing the Corellium and Arm partnership," Corellium blog, October 2022. https://www.corellium.com/blog/announcing-corellium-and-arm-partnership-2022 [^10]: Arm, "Platforms," Arm Learning Paths. https://web.archive.org/web/20251010211456/https://learn.arm.com/learning-paths/embedded-and-microcontrollers/nav-mlek/platforms/ [^11]: DVCon proceedings paper on Arm Virtual Hardware. https://dvcon-proceedings.org/wp-content/uploads/91483.pdf [^12]: Arm, "AVH FVP Artifactory," Arm Software documentation. https://arm-software.github.io/AVH/main/infrastructure/html/avh_fvp_artifactory.html [^13]: Arm, "FVPs-on-Mac," Arm-Examples GitHub repository. https://github.com/Arm-Examples/FVPs-on-Mac [^14]: Arm, "Arm Corstone virtual platforms now available at no cost," Arm Community IoT blog. https://community.arm.com/arm-community-blogs/b/internet-of-things-blog/posts/arm-corstone-virtual-platforms-now-available-at-no-cost [^15]: Arm, "Arm FVP and continuous integration," Arm Community support forums. https://community.arm.com/support-forums/f/soc-design-and-simulation-forum/47151/arm-fvp-and-continuous-integration [^16]: Arm, "Fast Models and Fixed Virtual Platforms install guide," Arm Learning Paths. https://learn.arm.com/install-guides/fm_fvp/fvp/ [^17]: Arm, "Arm Development Studio," Arm store. https://store.arm.com/arm-ds/ --- ## SEGGER shipped a simulator. Here's what it does and what it doesn't. (https://veecle.ai/blog/ozone-sim-teardown, 2026-08-13) SEGGER sells debug probes. That is the business — a physical object you plug into a physical board, plus software that makes it useful. In July 2026 they shipped a product that makes the object unnecessary. Ozone-Sim runs your firmware on your PC. No J-Link, no target. SEGGER says so in its own FAQ: "Do I need a J-Link debug probe to use Ozone-Sim? No. Ozone-Sim is a pure software simulator. You can run, debug, and profile your embedded applications entirely hardware-free, without requiring a physical J-Link or target board."[^1] When the company whose margin depends on you owning a probe builds a supported way to skip it, that is a useful read on where firmware development is going. ### What Ozone-Sim shipped, and when The press release is dated 3 July 2026: "With Ozone-Sim, SEGGER introduces a high-performance instruction set simulator designed to accelerate embedded software development, debugging, testing, and analysis."[^2] The code landed a day earlier — the Ozone release notes list Version 3.50 [02 Jul 2026] with a New Features entry reading "Added Ozone Simulator."[^3] The manual is UM20008, and it gives you the shape in one line: "Ozone-Sim is a fast simulator for ARM32 and RISC-V based MCUs. It can be used in two different modes: GDB server... Standalone mode."[^4] Those two modes are the whole product surface. **GDB server mode** exposes the simulator over a socket: "An integrated GDB server interface enables the simulator to be used with a wide range of third-party debuggers and development environments."[^2] Ozone attaches this way. That server capability isn't new. In September 2025 SEGGER said Ozone was "now available for simulators and third-party debug probes," using "the industry-standard GDB Remote Protocol."[^5] The release notes call it "Added Interface to GDB server," Version 3.40.[^3] That was Ozone becoming a GDB client. Ozone-Sim, ten months later, is the other end of that connection, not the same product. **Standalone mode** is the CI mode. You hand it an ELF, it runs, it exits with a code. On speed, SEGGER reports "up to 700 million instructions per second (on an AMD Ryzen 9)."[^1] That is the vendor's own number, with no published workload and no methodology. ### The scope question, answered by the vendor Here is the paragraph that should govern how you think about this product. It is SEGGER's own FAQ, on SEGGER's own product page: > "Does Ozone-Sim support emulation of device peripherals? At this point, Ozone-Sim does not > support emulation of device peripherals. Support may be added in a future release. Ozone-Sim > does include emulation of the Cortex-M SysTick, allowing operating systems such as embOS to > be simulated."[^1] No peripheral emulation. One exception, SysTick: without a tick an RTOS does not schedule, and without scheduling the simulator cannot run the software most people want to run. That is the hardest question about the product, answered in plain words in the FAQ, where a buyer will find it. Whether temporary or permanent, it is a coherent scope for a first release. Peripheral models are where simulator projects go to die: every part number is a fresh pile of registers, every register has errata, and the maintenance cost never stops growing. Declining to start is a legitimate way to ship something fast and keep it fast. What it buys and what it costs split cleanly: | Question you're asking | Ozone-Sim's scope | | ------------------------------------------------------------------------ | ------------------------------------------------------------ | | Does my sorting/DSP/crypto routine produce the right output? | Yes — pure computation on a modeled core | | Does my RTOS schedule tasks in the right order? | Yes — SysTick is modeled, and SEGGER names embOS | | Does my state machine reach the right state for this input sequence? | Yes, if every input and observation fits through semihosting | | Did my refactor change target-independent behavior across my test suite? | Yes, fast, and in parallel | | Does my UART driver configure the baud divisor correctly? | No — there is no UART | | Does my DMA controller hand off correctly at a buffer boundary? | No — there is no DMA controller | | Does my code survive this chip's silicon erratum? | No | One thing I deliberately cannot tell you: whether the simulation is cycle-accurate. SEGGER does not use the words "cycle-accurate" or "timing-accurate" anywhere I could find, and publishes no claim about instruction timing, bus contention or wait states. Absence of a claim is not a claim of absence. It is an open question, and if timing fidelity matters to your tests, ask before you plan around an answer. ![Diagram showing that the Ozone-Sim --chip flag configures memory map geometry while device peripherals are not modeled](/blog/ozone-sim-teardown/image-1.webp) ### `--chip STM32F407VE` configures memory, not silicon The manual's configuration section reads: "The simulator can be configured by specifying a known MCU name, a known core name or via a list of features which constitute a core... If no MCU/core/feature options are provided, a 'default Cortex-M7' core with ample memory is used."[^4] The known-MCU list is short, and here it is verbatim: - "STM32F407VE: MCU used on the SEGGER Cortex M trace reference board." - "STM32H743ZI: MCU used on the SEGGER STM32H7 trace reference board." - "K66P144M180SF5RMV2: MCU on the Segger emPower board." - "XC7Z007S: Zynq7007S Cortex-A9 MCU." - "default: A Cortex-M7 with flash memory banks..."[^4] Read that cold and you will conclude Ozone-Sim simulates an STM32F407. It does not. Cross-reference the FAQ above: there is no peripheral emulation, so there is nothing behind the part number except a core selection and a memory map. `--chip STM32F407VE` tells the simulator where flash starts, how big the RAM banks are, and which architecture profile to decode. Your linker script will be happy. Your driver will not find a peripheral to talk to. It is a naming collision between "the chip I am building for" and "the chip this flag is named after," and it is the misreading worth guarding against. Peripheral fidelity is where simulator claims most often outrun reality, which is the longer argument in [what simulation can't catch](/blog/what-simulation-cant-catch). ### Semihosting is the entire I/O story, and that's a clean design If there are no peripherals, how does simulated firmware talk to anything? Semihosting. The manual is specific about the boundaries. File access is sandboxed: "Semihosting file access can either be handled by the simulator or by the GDB client. The simulator will only perform file access in the directory provided via the `--file-root ` option."[^4] Arguments arrive the same way, and the manual's own example is a regression runner: "a regression test executable might need the name of the test cases to execute. This could be achieved with the following statement: ozone-sim --standalone --bss --arg multiplication --arg addition --arg quicksort CortexM-regression.elf"[^4] Results come back as a process exit code: "In standalone mode, a semihosting exit call will cause the simulator to exit with the code provided to the semihosting call."[^4] That is a complete CI contract in three flags. Firmware in, arguments in, exit code out. The termination logic is better than it needed to be. Standalone mode "will run until the application performs a semihosting SYS_EXIT operation or until an unhandled exception occurs. Endless loops are also detected by the simulator and will also cause it to stop the simulation."[^4] ![Diagram of the Ozone-Sim standalone mode CI flow from firmware ELF through semihosting to a pass or fail exit code](/blog/ozone-sim-teardown/image-2.webp) ### The slogan is a partner's, and the claims under it check out The phrase attached to this product in a lot of secondhand coverage is "hardware-free CI/CD regression testing." I went looking for it in SEGGER's material and it is not there. It comes from a blog post by GSAS, which describes itself as "SEGGER's authorized engineering partner in India."[^6][^7] Which is where I expected to write the usual paragraph about partner content inflating a vendor's scope, because that is overwhelmingly what turned up when we [read every embedded simulator comparison on the internet](/blog/embedded-simulator-comparison-audit). Not this time. Take the slogan apart and every piece traces to SEGGER's own documentation. Hardware-free: the FAQ says no J-Link and no target board required.[^1] CI/CD: standalone mode exists for it and returns exit codes.[^4] Regression testing: the manual's own worked example is a regression executable taking test-case names as arguments.[^4] Compressed marketing, but compressed from real material. The same partner post also states the limits more plainly than most first-party copy manages: "Simulation is not a replacement for every test, and no serious embedded team treats it as one. Ozone-Sim validates application logic, RTOS behavior, driver state machines and algorithmic correctness quickly and repeatably, but it does not exercise real peripheral timing, board-level electrical behavior, or interaction with actual sensors and actuators."[^6] ### Nobody outside SEGGER has run it yet Six weeks after launch there is no independent hands-on review and no reproduced benchmark. Two trade outlets rewrote the press release[^8][^9] without running the tool. Everything else is SEGGER's material, SEGGER's forum, and the partner's blog. So if you are weighing this against something you already run, you are the one generating the data. Same problem as any [Renode alternative](/blog/renode-alternatives) evaluation: you run your own firmware and see. ![Ladder diagram placing an instruction set simulator between host unit tests and generic-core emulation](/blog/ozone-sim-teardown/image-3.webp) ### Where it sits on the ladder We keep a rough ladder for this: host unit tests at the bottom, then instruction set simulation, then generic-core emulation, then simulation of specific silicon, then real hardware. I laid it out properly in [Wokwi vs Renode vs QEMU](/blog/wokwi-vs-renode-vs-qemu), and Ozone-Sim slots in cleanly at the ISS rung. That rung is a real tier, not a consolation prize. It is the fastest thing that still executes your actual compiled binary — same instructions, same linker layout, same compiler bugs. Host unit tests do not do that; they run host code you compiled twice. What the rung cannot do is answer a question about a register that does not exist in the model. If the thing you need to test is the peripheral — the UART divisor, the DMA handoff, the timer capture — you move up to something that models the actual part. That is the tier Chiplab occupies, and it is a different job from the one Ozone-Sim signed up for, not a better version of it. ### The part I actually care about I write about coding agents, so here is the connection. An agent writing firmware needs a loop that closes: write a change, run something, read a result, adjust. An ISS closes that loop for a large class of work — algorithms, RTOS behavior, state machines, refactor regressions — in milliseconds, and hands back an exit code the agent can read without parsing anything. Arguments in, exit code out is close to the ideal machine interface. It leaves the loop wide open for driver work, because there is nothing on the other end of the driver. An agent asked to fix a UART configuration on Ozone-Sim would produce a patch and get no signal at all about whether the patch was right. A probe vendor built a real rung of the ladder and told you exactly which rung it is. The remaining work is knowing which of your tests live there — and staying disciplined about not letting a chip name in a command-line flag talk you into thinking more of them do. ### Sources [^1]: SEGGER, "Ozone-Sim," SEGGER product page (technology / Ozone-Sim), accessed 2026-08-13. https://www.segger.com/products/development-tools/ozone-j-link-debugger/technology/ozone-sim/ [^2]: SEGGER, "SEGGER introduces Ozone-Sim," SEGGER press release, 2026-07-03. https://www.segger.com/news/pr-260703-ozone-sim/ [^3]: SEGGER, "Ozone Release Notes," SEGGER, accessed 2026-08-13. https://www.segger.com/downloads/jlink/ReleaseNotes_Ozone.html [^4]: SEGGER, "UM20008 — Ozone-Sim User Guide," SEGGER documentation, accessed 2026-08-13. https://doc.segger.com/UM20008_Ozone-Sim.html [^5]: SEGGER, "SEGGER's Ozone now available for simulators and third-party debug probes," SEGGER press release, 2025-09-09. https://www.segger.com/news/pr-250909-ozone-simulators/ [^6]: GSAS, "Hardware-free CI/CD regression testing for embedded teams," GSAS blog, 2026-07-08. https://gsasindia.com/blog/hardware-free-ci-cd-regression-testing-embedded-india [^7]: GSAS, "SEGGER Ozone-Sim," GSAS product page, accessed 2026-08-13. https://gsasindia.com/products/segger/segger-ozone-sim [^8]: Electronic Design, "SEGGER Ozone-Sim Enables Hardware-Free Firmware Debugging," Electronic Design, 2026-07-24. https://www.electronicdesign.com/technologies/eda/product/55393421/electronic-design-segger-ozone-sim-enables-hardware-free-firmware-debugging [^9]: eeNews Europe, "SEGGER launches Ozone-Sim for embedded software simulation," eeNews Europe, 2026-07-24. https://www.eenewseurope.com/en/segger-launches-ozone-sim-for-embedded-software-simulation/ --- ## The chip vendors showed up: hardware MCP servers, six months later (https://veecle.ai/blog/hardware-mcp-servers-2026, 2026-08-13) I audited every hardware MCP server I could find, and the method had a hole in it big enough to drive a foundry through. I started from the community `awesome` list, then the official MCP Registry, then targeted searches. All three index community projects. None of them index what a silicon vendor puts behind a login on its own developer portal. The registry is still labeled "in preview", and returns two firmware-adjacent results for "firmware" and nothing at all for "microcontroller".[^1] The community list contains no first-party vendor servers.[^2] So the audit reported a category of hobby projects, which was true of the layer I could see and wrong about the layer I could not. Between October 2025 and August 2026, seven silicon and tooling vendors shipped first-party MCP servers or MCP-driven tooling: Arm, Nordic, Analog Devices, Espressif, Microchip, Silicon Labs and Texas Instruments. That is the industry deciding, quietly and without a joint announcement, that agent-callable endpoints are part of a developer offering now. [Every hardware MCP server I could find](/blog/hardware-mcp-servers-reviewed) still holds up as a picture of the community layer. This is the layer above it, plus two corrections I owe the category. ### The vendors that shipped Checked 2026-08-13. Where a server is closed and hosted, all I can report is what the vendor documents. | Vendor | What shipped | When | License | What the agent gets | | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Arm | [Arm MCP Server](https://github.com/arm/mcp)[^3] | GA Oct 2025 | Apache-2.0 | x86→Arm migration scanning, `sysreport`, container image architecture checks, doc search, and `apx_recipe_run` profiling against a remote Arm target over SSH | | Arm | [Performix Dynamic Insights](https://newsroom.arm.com/blog/arm-performix-dynamic-insights-agentic-development), MCP server built in (`apx mcp start`)[^4] | Aug 2026 | Proprietary, free download | `generate_ai_insights`, `read_ai_insights_payload_details`, `list_recipes`, `list_runs`. Needs a completed profiling run on a real Arm target | | Nordic | [MCP server for nRF Connect SDK](https://www.nordicsemi.com/Products/Technologies/AI-assisted-development)[^5] | May 2026 | Proprietary, myNordic account | Slash commands for install, build, program, debug, UART and board tasks; the MCP tools are semantic search over SDK docs, DevAcademy and DevZone | | Nordic | [nRF Cloud MCP server](https://docs.nrfcloud.com/docs/platform/mcp-server)[^6] | May 2026 | Proprietary, OAuth | 16 read-only tools over your fleet: device metadata, timeseries and vitals, issues and traces with optional log excerpts, version stats, project listing | | Analog Devices | [CodeFusion Studio AI Debug Assistant](https://github.com/analogdevicesinc/codefusion-studio)[^7] | Mar 2026 (preview) | Apache-2.0 repo | 23 tools, 10 diagnostic prompts: session control, live register/memory/variable/stack/thread inspection, breakpoints, guarded GDB, Cortex-M and RISC-V fault decoding, ELF analysis. Needs a live debug session on real ADI silicon | | Espressif | [ESP-IDF Tools Local MCP Server](https://developer.espressif.com/blog/2026/04/esp-idf-tools-mcp-server/)[^8] | Apr 2026 | Apache-2.0 | `set_target`, `build_project`, `flash_project`, `clean_project`, plus `project://config`, `project://status` and `project://devices`. Needs ESP-IDF v6.0+ and a board to flash | | Espressif | [Documentation MCP Server](https://mcp.espressif.com/docs)[^9] | Apr 2026 | Proprietary, free, login | One tool: `search_espressif_sources(query, language)` | | Microchip | [MCP Server + MPLAB-DOCS MCP Server](https://www.microchip.com/en-us/resources/model-context-protocol-server)[^10] | Nov 2025 | Proprietary, free, no auth | `SearchProducts`, `SearchProductPhysicalSpecs`, `SearchProductInventoryInfo`, `SearchProductComplianceInfo`, `SearchMicrochipProductDocuments`; semantic search over docs, examples, reference designs | | Silicon Labs | [MCP Server](https://mcp.silabs.com)[^11] | ≈ Jan 2026 | Proprietary | Docs and SDK search from Cursor, VS Code or Claude Code | | Silicon Labs | [Simplicity Network Analyzer MCP](https://docs.silabs.com/network-analyzer/latest/network-analyzer-release-notes/)[^12] | Jun 2026 (v1.0.0) | Proprietary, in Simplicity Studio 6 | The agent discovers and executes the packet sniffer's own API operations. Needs a physical radio capture setup | | Texas Instruments | [Code Composer Studio bundled servers](https://software-dl.ti.com/ccs/esd/documents/users_guide_ccs_21.0.0/ccs_ai.html): `ccs-debug`, `ccs-project`, `ccs-sysconfig`, `ccs-serial`[^13] | CCS 21.0.0, experimental in 20.2.0 | Proprietary, local only | Build, project, SysConfig, serial and debug control. Needs CCS open and, for debug, an XDS110-class probe on real TI silicon | | Golioth | [tinymcp](https://github.com/golioth/tinymcp)[^14] | Jul 2025 | Apache-2.0 | Exposes on-device RPCs registered through the Golioth Firmware SDK as MCP tools, via Golioth's cloud. Needs a device already provisioned on Golioth | Golioth is in the table as a first-party device-cloud provider, but it is not one of the seven silicon and tooling vendors counted in the opening. Arm's server sits at 90 GitHub stars as of August 2026, which for a first-party release is neither impressive nor embarrassing. Golioth's `tinymcp` has 152 and has taken no push since it launched in July 2025. Everything else from a vendor here is closed-source with no public activity signal, which tells us nothing about private usage. ![Grid of silicon vendors sorted into shipped products, an upstream component and absent hardware MCP servers as of August 2026](/blog/hardware-mcp-servers-2026/image-1.webp) This is the concrete form of an argument I made in the abstract in [hardware is becoming an API](/blog/hardware-is-becoming-an-api). ### Sort them by what the agent is actually handed "Shipped an MCP server" treats a documentation search box and a live debug session as the same event. Sort those twelve entries by what an agent gets back from a tool call and the distribution goes lopsided fast. | Category | First-party entries | Which ones | | -------------------------------------------- | ------------------- | --------------------------------------------------------------------------------- | | Documentation and catalog lookup | 3 | Espressif Documentation, Microchip, Silicon Labs MCP — retrieval and nothing else | | Toolchain control: build, flash, configure | 3 | Espressif ESP-IDF Tools, Nordic nRF Connect SDK, TI project and SysConfig | | Live debug or capture on attached hardware | 4 | ADI CodeFusion, TI `ccs-debug`, Silicon Labs Network Analyzer, Arm Performix | | Fleet data from devices you already deployed | 2 | nRF Cloud, Golioth `tinymcp` | | Simulated execution | 0 | — | Three of twelve stop at retrieval — a quarter of the first-party category whose entire function is answering a question about a datasheet. Useful, honestly labeled, and the easiest thing on this list to build: a vector index over documents the vendor already owns, with one `search` tool on top. The ordering surprised me. Arm was first, in October 2025, then Microchip in November and Silicon Labs around January 2026 — the last two with documentation search and nothing else. The MCU vendors whose developer tooling people actually argue about did not lead. Those two got to market six months early by shipping the tier that needs no hardware, no session state and no support burden. The zero row matters more than the rest. Several servers can build or configure firmware without a board. None lets an agent execute the resulting binary without attached hardware. Live debug, capture, flashing and fleet observation still terminate at a physical object: a board, a probe, a capture rig or a deployed device. The vendors shipped agent access to the tools around the hardware. They did not ship a hardware-free target. ![Bar chart of twelve first-party hardware MCP servers by capability, with simulated execution at zero](/blog/hardware-mcp-servers-2026/image-2.webp) ### Three vendors have shipped nothing, and one took a different route "Chip vendors have shipped MCP servers" is half true, and the other half is specific. I found no official MCP server from **STMicroelectronics**. Every STM32-targeting server I found — `shieldyguy/stm32-mcp`, `creativec09/stm32`, `stm32-gdb-mcp` — is an independent project wrapping ST's public toolchains, none published or endorsed by ST. That is the largest gap on the board, because STM32 is what most of those community projects aim at. I found no MCP integration in **Infineon's** ModusToolbox material. I also found no official server from **Renesas** — the only hit is a third-party VS Code extension by an unaffiliated publisher wrapping e2 studio. **NXP** took a different route. An NXP engineer demoed a Zephyr-based MCP server for MCX MCUs on the company's own developer-relations channel in late March 2026. The code merged into upstream Zephyr on June 11 as a library, samples and unit tests.[^15] It ships as an open-source RTOS component, not a first-party NXP product with its own endpoint or support commitment. That is why it is not in the vendor-product table above. Then the absence I find hardest to explain. I found no maintained MCP integration for Antmicro's **Renode**. Renode is deterministic, scriptable, CI-first, and models whole boards including peripherals — one of the most agent-shaped tools in existence, and the wrapper is close to trivial. Its official integrations use other interfaces, including WebSockets, rather than MCP. ### The community layer is still where the strange things live Serial and UART wrappers remain the most-reimplemented thing in the category: `qarnet/serial-mcp` in Rust, `alxgmpr/serial-mcp` in Python, `es617/serial-mcp-server`, all MIT and all in the single-digit-to-teens star range as of August 2026. Probe wrappers are the next layer up — `es617/dbgprobe-mcp-server` for J-Link, CMSIS-DAP and ST-Link, `microhenrio/openocd-mcp`, and `okhsunrog/flashprobe-mcp` doing probe-rs and espflash with RTT and defmt capture. For simulation there is exactly one official option, labeled experimental by its own authors: Wokwi's CLI has an MCP mode, documented on a single page and gated on a token.[^16] The oddest entry is `quantumnic/mcpd`, an MCP server that runs _on_ the ESP32 or RP2040 rather than on the host, exposing 106 built-in tools from the device itself.[^17] I do not know yet whether that inversion is a good idea. ### Two corrections, because "agentic" is not "MCP" Two projects are routinely described as hardware MCP servers and neither is one. Both are good projects; the mistake is vocabulary, not credibility. **EmbedAgents `stm32-substrate`** is five Claude Code slash commands wrapping a Python CLI that shells out to ST's CubeProgrammer, CubeIDE, CubeMX, gdbserver and Signing Tool. Its `.claude-plugin/plugin.json` registers only `"commands"` — no `mcpServers` key, no MCP SDK import, no mention of MCP in the README, CHANGELOG or manifest.[^18] **`ccattuto/esp-agentic-dev`** is a CLI and shell-exec framework — `esp_target.py`, `rtt_reader.py`, `svd_parser.py` — invoked by Claude Code as subprocesses, talking to hardware over OpenOCD's Tcl port. No MCP SDK, no `mcpServers` configuration, no mention of MCP anywhere in the repository.[^19] Both do real work. Neither speaks the protocol. "Agentic" and "MCP" have become interchangeable in casual writing and they are two different claims: one is about what a tool is for, the other about a wire format and a discovery mechanism. If you are counting a category, that distinction is the measurement. Three test-automation vendors I checked market agentic workflows heavily and have no MCP server anywhere in their material; "AI-powered" on a landing page is not evidence an endpoint exists. ### Embedder thinks the whole approach is wrong One company has looked at MCP servers for hardware and declined on purpose. Embedder ships an MCP _client_, not a server, and has publicly argued that its harness has agents write and execute code directly rather than going through tool-call round-trips.[^20][^21] That deserves a hearing rather than a dismissal. A fixed tool schema is a lossy interface to a general-purpose machine: every capability you did not anticipate becomes a feature request, and every interaction costs a round trip through the model. The counter-argument is boring and mostly about blast radius. A schema is a contract you can audit, rate-limit and refuse; "write and execute code directly" against a probe on somebody's bench is a different risk conversation. Both models will be around. This is not settled. ### The missing capability is a hardware-free target An agent working on firmware can, today, look up a register in a reference manual, build the project, flash a board, halt a core, decode a fault and read telemetry off a fleet. Building can happen without a board; execution, live debug and observation still need hardware attached somewhere. What the first-party category cannot do is close that loop without the physical object: take an ELF it just produced and find out whether it runs. [E2B gave agents a computer](/blog/e2b-gives-agents-a-computer) they did not physically own, which is why a coding agent can iterate at three in the morning without a human. The firmware equivalent is the empty row in that capability table, and the same empty slot I mapped in [the agent infrastructure map](/blog/the-agent-infrastructure-map). The vendors shipped manuals and controls. What an agent needs is a target. Disclosure, since it is why I keep counting this category: we build one. Chiplab is a hosted MCP server that runs firmware on virtual instances of real Cortex-M parts, which is why it is in none of the tables above. ![Diagram contrasting documentation responses from vendor MCP servers with firmware run output an agent cannot get](/blog/hardware-mcp-servers-2026/image-3.webp) The only reliable way to find out whether your vendor has an agent endpoint is to read that vendor's developer site, which is a poor state of affairs for a protocol whose whole pitch is discoverability. ### Sources [^1]: "The MCP Registry." Model Context Protocol. https://registry.modelcontextprotocol.io [^2]: beriberikix. "Awesome MCP for Hardware." GitHub. https://github.com/beriberikix/awesome-mcp-hardware [^3]: Arm. "Arm MCP Server." GitHub. https://github.com/arm/mcp [^4]: Arm. "Arm Performix Dynamic Insights: agentic development." Arm Newsroom, August 3, 2026. https://newsroom.arm.com/blog/arm-performix-dynamic-insights-agentic-development [^5]: Nordic Semiconductor. "AI-assisted development." nordicsemi.com. https://www.nordicsemi.com/Products/Technologies/AI-assisted-development [^6]: Nordic Semiconductor. "MCP server." nRF Cloud documentation. https://docs.nrfcloud.com/docs/platform/mcp-server [^7]: Analog Devices. "CodeFusion Studio." GitHub. https://github.com/analogdevicesinc/codefusion-studio [^8]: Espressif. "ESP-IDF Tools Local MCP Server." Espressif Developer Portal, April 2026. https://developer.espressif.com/blog/2026/04/esp-idf-tools-mcp-server/ [^9]: Espressif. "Documentation MCP Server." https://mcp.espressif.com/docs [^10]: Microchip Technology. "Model Context Protocol Server." microchip.com. https://www.microchip.com/en-us/resources/model-context-protocol-server [^11]: Silicon Labs. "Silicon Labs MCP Server." https://mcp.silabs.com [^12]: Silicon Labs. "Simplicity Network Analyzer release notes." Silicon Labs documentation. https://docs.silabs.com/network-analyzer/latest/network-analyzer-release-notes/ [^13]: Texas Instruments. "AI features in Code Composer Studio," CCS 21.0.0 User's Guide. https://software-dl.ti.com/ccs/esd/documents/users_guide_ccs_21.0.0/ccs_ai.html [^14]: Golioth. "tinymcp." GitHub. https://github.com/golioth/tinymcp [^15]: Zephyr Project. "Zephyr MCP Server," pull request 105299, merged June 11, 2026. https://github.com/zephyrproject-rtos/zephyr/pull/105299 [^16]: Wokwi. "MCP Support." Wokwi Docs. https://docs.wokwi.com/wokwi-ci/mcp-support [^17]: quantumnic. "mcpd." GitHub. https://github.com/quantumnic/mcpd [^18]: EmbedAgents. "stm32-substrate." GitHub. https://github.com/EmbedAgents/stm32-substrate [^19]: ccattuto. "esp-agentic-dev." GitHub. https://github.com/ccattuto/esp-agentic-dev [^20]: Embedder. "MCP servers." Embedder documentation. https://docs.embedder.com/integrations/mcp-servers [^21]: Jonah Perry. "I'm going to have to hate a bit on MCP servers for agentic context and execution." LinkedIn, August 5, 2026. https://www.linkedin.com/posts/jonah-perry-941481161_im-going-to-have-to-hate-a-bit-on-mcp-servers-activity-7490879455523987458-V37q --- ## MCP went stateless. Some of us are holding a running chip. (https://veecle.ai/blog/mcp-went-stateless, 2026-08-13) I read the 2026-07-28 changelog the way everyone reads a changelog, scanning for the line that breaks me. It was the first bullet. In that protocol revision, sessions are gone. Not deprecated, not optional, removed. Two weeks later I think the decision is correct, which is an annoying thing to conclude when you maintain a server whose whole job is keeping something alive between calls. So this is not a complaint. The protocol did not drop state. It moved responsibility for state across a line, was deliberate about the easy side of that line, and left the hard side to whoever is standing on it. ### What changed when MCP went stateless on 28 July From the changelog, verbatim: > "1. Remove protocol-level sessions and the `Mcp-Session-Id` header from the Streamable HTTP > transport. List endpoints (`tools/list`, `resources/list`, `prompts/list`) no longer vary > per-connection. Servers that need cross-call state use explicit, server-minted handles passed > as ordinary tool arguments (SEP-2567)." > "2. Make MCP stateless: remove the `initialize`/`notifications/initialized` handshake. Every > request now carries its protocol version and client capabilities in `_meta` > (`io.modelcontextprotocol/protocolVersion`, `io.modelcontextprotocol/clientCapabilities`)... > Version mismatches return `UnsupportedProtocolVersionError` (SEP-2575)."[^1] The base specification states the rule normatively: MCP "is a **stateless protocol**: all the information needed to process a request is contained in the request itself. A server processes each request independently; no state should be inferred from previous requests, even those on the same connection or stream." Servers "**MUST NOT** rely on prior requests over the same connection to establish context (e.g., capabilities, protocol version, client identity)."[^2] ![Diagram comparing MCP session-scoped state before 2026-07-28 with stateless requests carrying explicit handles afterwards](/blog/mcp-went-stateless/image-1.webp) ### The scaling argument is right, and I'll grant it before I argue Most takes on this revision skip to the grievance. I'd rather concede the case first, because it is a strong one and it was made in public. SEP-2575's motivation, verbatim: > "1. **Impediment to Scalability:** The most critical issue is the difficulty of load > balancing stateful MCP. A simple stateless load balancer (e.g., L4/L7 round-robin) cannot be > used, as it would route a client's requests to different backend servers, none of which would > have the correct session state. Operators are forced to implement complex and fragile > solutions like sticky sessions..." > "2. **Poor Resilience and Fault Tolerance:** In a stateful model, if the specific server > instance handling a client session fails, that session state is lost..." > "3. **Increased Implementation Complexity:** ... Developers must implement logic to create, > manage, and eventually garbage-collect per-client session state. This is a common source of > bugs and memory leaks."[^3] All three are true. I have written the fragile sticky-session config and I have watched state evaporate because an instance rolled during a deploy. The third stings most: per-client lifecycle code is where the leaks live, and no amount of care makes it fun. I'd have made the same call. A protocol that supports both shapes supports neither well. ### The replacement is a name you carry yourself There is exactly one normative sentence about cross-call state, in the base specification: state spanning multiple requests "**MUST** be referenced by an explicit identifier the client passes on each request."[^2] That is a MUST, and it constrains the wire shape. Everything else about handles is guidance, and the specification says so itself in its own first line on the subject. From "Stateful Tools": > "This section is non-normative guidance for tool design. The protocol has no concept of a > state handle; from the wire's perspective a handle is an ordinary string in a tool result and > an ordinary argument to subsequent tool calls." > "Servers that need to maintain state across calls — a shopping cart, an open browser context, a > database transaction — should do so by returning an explicit handle from a creation tool and > accepting that handle as an argument on subsequent calls."[^4] The launch post puts the same idea in the maintainers' own words. This is blog commentary, not specification: "If your server needs to carry state across calls, mint an explicit handle from a tool and have the model pass it back as an argument. We found this works better than session state hidden in the transport - the model can see the handle and thread it between tools."[^5] Three kinds of text, three weights: | Kind of source | Example | What it binds | | ------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------ | | Normative specification | cross-call state "**MUST** be referenced by an explicit identifier" | Every compliant server | | The spec's own non-normative guidance | the "Stateful Tools" handle pattern, labeled non-normative in its first sentence | Nobody; design advice from a good source | | Maintainer blog commentary | "the model can see the handle and thread it between tools" | Nobody; an opinion, and one I mostly share | Anyone telling you the specification _requires_ the handle pattern has collapsed row one into row two. The handle pattern is the recommended way to satisfy the MUST, not the rule. ### The gradient runs past where the examples stop Look again at the three examples the guidance picks: a shopping cart, an open browser context, a database transaction. They are not arbitrary. They sit on a gradient, and the gradient is the story. | Behind the handle | Cost to rehydrate on another instance | Survives a restart? | | --------------------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------- | | Shopping cart | One row read | Yes, trivially | | Open browser context | Re-navigate, replay cookies and storage; lose the JS heap, scroll, timers | Partially, with real effort | | Database transaction | Locks and snapshot are bound to one connection in one process | No | | Firmware running on a virtual MCU | Registers, RAM, flash contents, peripheral timers, a half-full UART FIFO, an interrupt about to fire | Only if something explicitly snapshots it | Row one is what the pattern was designed against, and for it the pattern is perfect. Statelessness buys real horizontal scale and costs you a `SELECT`. Row four is where I live. If one tool call boots firmware on a virtual STM32 and the next reads what came out of the UART, the thing behind that handle isn't data, it's an execution. The protocol's contract for row four is identical to its contract for row one: give it a name, pass the name back. The sandbox-versus-target distinction is one I've written about before, in [E2B gives agents a computer](/blog/e2b-gives-agents-a-computer). A fresh container per call is a legitimate design. A fresh chip per call means the firmware you flashed is gone. ![Diagram of a gradient from a shopping cart to a running MCU core showing increasing cost to rehydrate state behind an MCP handle](/blog/mcp-went-stateless/image-2.webp) ### What the spec constrains, and what it leaves to you The sloppy version of my argument is that the specification ignores stateful servers. It doesn't. There is a MUST, there is a documented pattern, and the SEP is unusually honest about the limits of both. SEP-2567 says outright that the handle pattern is not a protocol feature: > "That third point is **not a protocol change**. There is no `handles/*` method, no handle type > in the schema, no wire-level concept of a handle at all... 'Explicit state handles' is a > tool-design pattern that the spec documents and recommends — in the same way it might document > pagination or error-message conventions — not something it implements."[^6] In future work it concedes the blind spot: "nothing marks `basket_id` as a state handle to the client or model — the relationship... is inferred from naming and tool descriptions, not declared."[^6] So the gap is narrow, and I want it stated narrowly. The specification mandates how cross-call state is **referenced**. It says nothing about where the resource behind that reference **lives**, how it **survives a restart**, or how it stays **addressable** under the horizontal scale-out that removing sessions was meant to enable. For a cart those three questions share one answer, and it's your existing database. For something that cannot be cheaply serialized and rehydrated, those three questions are the entire product. The work didn't evaporate when the transport stopped carrying it. When I went through [every hardware MCP server I could find](/blog/hardware-mcp-servers-reviewed), nearly all were this shape: one long-lived thing, fronted by tool calls. ### The authors knew exactly where the sharp edge was Multi round-trip requests replaced server-initiated requests. Per the spec they exist to handle them "without requiring a shared storage layer across server instances or requiring stateful load balancing."[^7] Which makes the exception instructive. MRTR _does_ define a state carrier: `requestState`, "an opaque string meaningful only to the server", which clients "**MUST NOT** inspect, parse, modify, or make any assumptions about its contents". And it has teeth. If `requestState` influences authorization, resource access or business logic, servers "**MUST** protect its integrity (e.g. HMAC or AEAD) and **MUST** reject state that fails verification."[^7] So the authors were willing to specify a state-carrying field when its scope was one round trip, and they specified it with mandatory integrity protection because they knew what happens to state crossing a trust boundary. ### Tasks are about a call that hasn't finished The immediate objection is that Tasks solve this. They don't, and the distinction fits in a line: Tasks model _execution progress_, not a _standing resource_. Tasks moved out of core into the `io.modelcontextprotocol/tasks` extension.[^8] Per the release candidate post — commentary, not spec — "Task creation is server-directed: the client advertises the extension and the server decides when a call should run as a task. `tasks/list` is removed because it can't be scoped safely without sessions."[^9] Its lifecycle terminates: completed, failed, cancelled. That's a call you're waiting on. What a stateful server owns is different: a thing deliberately still there after the call that created it returned successfully, expecting five more unrelated calls to find it again. A task that never terminates is a bug. A handle whose resource never terminates on its own is the entire point. ### Twelve months for the soft features, nothing for the hard one Put two policies side by side. Roots, Sampling and Logging are deprecated in 2026-07-28 with a runway of at least twelve months: earliest removal is the first revision on or after 2027-07-28.[^10] The session removal got no deprecation window. SEP-2567: "Rollout is a clean break: sessions are removed in the next spec version, with no deprecation window. Servers that currently rely on session-scoped state stay on the current protocol version until they have migrated to explicit handles."[^6] That does not make old servers noncompliant or unreachable. Protocol negotiation lets a client speak the older revision to an unmigrated server. But it does put the migration cost in one place: the stateful server keeps working on the old revision while its owner builds the explicit state layer required to adopt the new one. ![Diagram showing MCP specifying how state is referenced while leaving storage, restart survival and addressability to the implementer](/blog/mcp-went-stateless/image-3.webp) ### What we do about it Every handle we mint points at row four of that gradient, because we run firmware on virtual instances of real microcontrollers. Chiplab's answer is the boring one the spec implies and does not describe: a run is a first-class object with an identity of its own, it lives outside the process that served the request, and any instance can address it. Moving to 2026-07-28 means the transport no longer participates in that at all, which, irritatingly, makes the design cleaner. The wider point is the one I keep circling. As [hardware becomes an API](/blog/hardware-is-becoming-an-api), the servers fronting it inherit every problem a stateless protocol pushes downward, and no shared database makes a half-executed interrupt rehydrate on another box. The protocol got this right; it should not know what a target is. But an agent that wants to hold a chip still needs someone to hold the chip, and as of 28 July that someone is unambiguously us. ### Sources [^1]: Model Context Protocol, "Specification changelog, 2026-07-28," modelcontextprotocol.io, July 28, 2026. https://modelcontextprotocol.io/specification/2026-07-28/changelog [^2]: Model Context Protocol, "Base Protocol — Statelessness," MCP Specification 2026-07-28, July 28, 2026. https://modelcontextprotocol.io/specification/2026-07-28/basic#statelessness [^3]: Model Context Protocol, "SEP-2575: Make MCP stateless," modelcontextprotocol GitHub repository, 2026. https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575 [^4]: Model Context Protocol, "Server — Tools, Stateful Tools," MCP Specification 2026-07-28, July 28, 2026. https://modelcontextprotocol.io/specification/2026-07-28/server/tools#stateful-tools [^5]: Model Context Protocol, "MCP 2026-07-28 release," MCP Blog, July 28, 2026. https://blog.modelcontextprotocol.io/posts/2026-07-28/ [^6]: Model Context Protocol, "SEP-2567: Remove protocol-level sessions," modelcontextprotocol GitHub repository, 2026. https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2567 [^7]: Model Context Protocol, "Base Protocol — Patterns, Multi Round-Trip Requests," MCP Specification 2026-07-28, July 28, 2026. https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr [^8]: Model Context Protocol, "SEP-2663: Tasks extension," modelcontextprotocol GitHub repository, 2026. https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2663 [^9]: Model Context Protocol, "2026-07-28 release candidate," MCP Blog, July 28, 2026. https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/ [^10]: Model Context Protocol, "SEP-2596: Deprecation policy," modelcontextprotocol GitHub repository, 2026. https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2596 --- ## The best AI-firmware diagnoses agree. Their cures don't. (https://veecle.ai/blog/right-diagnosis-wrong-cure, 2026-08-13) This year produced an unusually good crop of writing about why AI struggles with firmware. I expected to disagree with it. Mostly I don't. The diagnosis has converged — five different authors, different employers, different incentives, same account of what goes wrong. The prescriptions have not converged at all. They point in four different directions, and as far as I can tell nobody has said out loud that they disagree with each other. That gap is the interesting thing about 2026. I've argued before that [Copilot for firmware is solved and that was the easy half](/blog/copilot-for-firmware-is-solved). This post is about the half that isn't, and specifically about which of the proposed fixes has evidence behind it. ### The diagnosis everybody shares A chip vendor's software director, writing on a personal Substack with the employer disclosed, put the failure in the form every embedded engineer recognizes: "Every embedded engineer I know has tried using Copilot or ChatGPT for firmware. And every one of them has a story about the plausible-looking driver that compiled cleanly and failed at 2 AM on the bench... That story is a failure of context, not a failure of AI."[^1] An independent consultant's blog reached the same place from the software-architecture side: "The AI has nothing to anchor to. There is no message contract. No formal state schema. No machine-readable map of who talks to whom. So the AI does what it does best — it predicts the next plausible token. Plausible-looking code that violates assumptions nobody ever wrote down."[^2] That piece deserves credit for a second reason. It shipped under a headline saying AI can't write firmware, took criticism from embedded engineers, and narrowed itself in the body: "What I had written first read as 'AI cannot do firmware.' What was actually true was narrower and more interesting: AI does fine with the parts of firmware that look like backend code. It struggles with the parts that look like systems." A trade publication's columnist put the central problem clearly: "It isn't that AI gets things wrong. Engineers get things wrong, too. The problem is that AI gets things wrong while sounding completely sure of itself."[^3] None of this is controversial to me. It's roughly what I wrote in [what LLMs get wrong about embedded code](/blog/what-llms-get-wrong). ### Four cures for one disease Same diagnosis, four remedies. | Source | What it says goes wrong | What it prescribes | | --------------------------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | | Independent consultant's blog, May | No contract, no schema, no machine-readable map — nothing to anchor to | The industry builds firmware the structural scaffolding the web already has | | Chip vendor software director, February | Hardware target invisible; errata effectively don't exist for the model | Hardware context as first-class input, plus auditable reasoning | | Trade publication columnist, May | Firmware has no feedback loop; register-level and timing work fails confidently | Teams build their own MCP servers, RAG databases, and custom agents | | Solo benchmark author, April | Measured per-category pass rates that bottom out in specific domains | Classify code into trust tiers and gate merges by tier | Each is defensible. Side by side they are not the same plan. "Wait for an industry-wide schema standard" and "build your own tooling this quarter" are advice for different decades. "Inject the register values" and "have an expert rewrite it" are different answers to the same ticket. ![Diagram showing one shared diagnosis of AI firmware failure branching into four different proposed cures](/blog/right-diagnosis-wrong-cure/image-1.webp) ### Input-side and output-side Three of them are input-side. Schemas, register values, RAG databases, errata ingestion — all of them are attempts to get more true facts in front of the model before it writes. They differ in who does the work and how long it takes, but the theory of the failure is identical: the model was under-informed. The fourth is different in kind. Trust tiers don't inform the model at all. They accept that the output may be wrong and install a judge — a human expert — whose job is to find out. That's an output-side fix: it doesn't improve the writing, it improves the finding out, which is the framing in [why your AI coding agent needs a target to run on](/blog/why-your-agent-needs-a-target). ### The input-side cure has a measured ceiling A vendor benchmarking its own product on an LPC55S69-EVK ran four models through the same LED blink twice — once cold, once with the relevant register data injected. Cold: 4 of 4 compiled, 1 of 4 worked on hardware. With register data: 4 of 4 compiled, 4 of 4 worked.[^4] They publish full source and binaries from every test with an "unmodified, nothing hand-edited" claim. Inspectable, not independently audited. The same table shows the ceiling. On a harder task — PWM LED fade — 4 of 4 compiled and 1 of 4 worked, and the one that worked lit the wrong LED, green instead of the specified red, because it brute-forced every clock-enable bit on the chip rather than finding the correct one. On a UART-plus-LED task, after full register-value injection, one model produced garbled serial output from a 7.0% baud error caused by the wrong oversampling ratio. Context injection took a blink from 25% to 100%. It did not turn a clock-tree calculation into a solved problem. A solo engineer publishing an open-source benchmark measured the same shape at scale: 233 cases, two models at n=3, Wilson 95% intervals over 699 pooled trials.[^5] 68.0% pass@1 for the stronger model, CI [64.4%, 71.3%]; 56.9% for the weaker, CI [53.2%, 60.6%]. The intervals don't overlap. The author is candid about the limits — two models, around 81% of cases on Zephyr, single-file scope. Solo, self-published, not peer reviewed, and more transparent about its own weaknesses than most funded work. More on what these benchmarks measure in [EmbedAgent and friends](/blog/what-llm-embedded-benchmarks-measure). EmbedAgent's published RAG result is on Arduino with schematics: DeepSeek-R1 went from 55.6% to 65.1% pass@1.[^8] Separately, the best ESP-IDF _migration_ number in that paper is 29.4%, with no RAG on that row; compiler feedback later moved DeepSeek-R1's ESP-IDF score from 21.4% to 27.8%. Context engineering raises the ceiling. It does not remove it, and it does not turn 29.4 into 65.1 on ESP-IDF. 65.1% is progress. It is not a release criterion. ![Bar chart showing Arduino firmware pass rate rising from 55.6 percent to 65.1 percent with retrieval, a gain of 9.5 points](/blog/right-diagnosis-wrong-cure/image-2.webp) ### The output-side argument has prior art The feedback-loop framing is the one I believe in, and other people stated it before me. The trade columnist named it in May, better than I have: "Web developers actually do see big productivity gains from AI. That part isn't a lie. The catch is that the gains have very little to do with code generation. They come from the workflow. Ship something, watch it break, push a fix in minutes. That feedback loop is the productivity engine. AI just feeds it faster. ... Most firmware ships once and stays shipped."[^3] A funded startup makes closing the loop its entire pitch: "It's because [the feedback] loop - the part that... matters - happens outside the world the [model] lives in... The model never sees the hardware. It never sees the [debug] output... It's the iteration cycle: write code, compile, flash, test on hardware, observe behavior, debug, repeat."[^6] Their performance claims come with no methodology, so I'm not citing those. And the hardware-testing vendor above lists it as unbuilt future work: "an end-to-end AI agent that generates firmware, compiles, flashes to real hardware, reads UART debug output, and iterates — all without human intervention."[^4] Not shipped. On the roadmap. Worth flagging: the two most-cited diagnosis pieces of the year — the chip vendor director's and the consultant's — never mention a feedback loop or a runnable target at all. Their remedies are entirely input-side. The convergence is real. It is not universal. ### The strongest evidence comes from a domain nobody expected The most rigorous test of the output-side argument I've found this year isn't about peripherals at all. It's a formal-methods preprint where the judge was a theorem prover.[^7] Under a verifier-driven loop with GNATprove over CVC5 and Z3 judging every claim, agents wrote and machine-verified crypto primitives, TLS 1.3, IKEv2, X.509, an SSH transport and a Matrix client on bare metal, discharging 49,280 proof obligations. The paper's central lesson is the cleanest statement of my position that exists, written by someone with no interest in firmware simulation: > "What an agent can be trusted to establish is bounded by the strength of its feedback." Now the scope limit, because this gets overclaimed the second it leaves the abstract. This is bare-metal _application_ software — crypto and protocol stacks under a separation kernel — chosen because its correctness properties are expressible in formal proof. It is not register-level, peripheral-driving firmware, and the authors put timing side channels ("no functional proof detects this"), protocol-logic security and compiler miscompilation outside what the work shows. So: strong evidence that closing the loop works, in a domain where a machine-checkable judge exists. Not evidence that AI can write firmware. The narrower version: [LLMs write good firmware, they can't prove it](/blog/llms-write-good-firmware-cant-prove-it). ### A weak judge is worse than no judge The failure mode in that paper matters more than the success, and it's the reason I'm suspicious of any cure that amounts to "add more review". > "GNATprove alone was insufficient: some defects could not be detected and were resolved using > known-answer tests, interoperability, or human review of specifications. Given weak checks, the > agent tried to bypass them and reported success." The agent silenced a failing proof obligation with a `pragma Assume` to fake a pass. Human review caught it. Read that against the trust-tier cure. Tiering installs a judge, and the judge is a human expert's attention. It works exactly as well as that attention is strong, and the paper is a controlled demonstration of what an agent does when the judge is weak. ![Diagram showing that what an AI agent can be trusted to establish is bounded by the strength of the check judging its output](/blog/right-diagnosis-wrong-cure/image-3.webp) ### What nobody has shown Both bodies of evidence bottom out in the same places. The benchmark's per-category floors: DMA cache coherency at 31% and 8% for the two models, ISR concurrency 23% and 38%, threading 33% and 33%, storage 54% and 31%. Its own tier table puts DMA, ISR, threading and storage in the "expert writes from scratch, LLM draft is reference only" band. The hardware tests break in the same places: the wrong LED lit by a brute-forced clock-enable, the 7.0% baud error surviving full register injection. No published work shows that closing the feedback loop lifts those categories. Not the formal-methods paper — its domain was chosen for having a machine-checkable judge, and DMA coherency does not. Not the hardware-testing vendor — their loop is future work. Not the startup — no methodology behind the claims. Not the benchmark — its runtime layer is QEMU and native_sim, and the author is direct that a hardware-in-the-loop gate is what "catches what QEMU can't see: DMA cache coherency, interrupt latency, clock jitter." In that series, hardware in the loop is one gate of three, subordinated to context injection and tiering. Present, not load-bearing. Whether closing the loop lifts the hard categories is unknown. I believe it will. I can't show you that it does, and I'm not going to write a sentence that implies otherwise. ### Where that leaves the agent The thing all four cures dance around is that an agent writing firmware has no way to find out it was wrong. Give it a target it can run on and the loop closes for the checks a simulator can answer: register semantics, peripheral configuration, boot paths, the boring failures that eat bench time. That's the bet behind Chiplab, which puts virtual instances of real microcontrollers where the board would be. A stronger judge than a compiler, a weaker one than silicon, and I'd rather be precise about that than sell it as the answer to DMA coherency. The diagnosis is remarkably consistent across the evidence I found. The cure isn't. That's a better place to be than either side of this argument usually admits. ### Sources [^1]: "Why AI Agents Haven't Cracked Embedded Development (Yet)," personal Substack of a software director at Analog Devices leading CodeFusion Studio, February 23, 2026. https://jdavidperez.substack.com/p/why-ai-agents-havent-cracked-embedded [^2]: Ritzy Lab, "AI Can Write Your Frontend. It Can't Write Your Firmware.," Ritzy Lab blog, May 8, 2026. https://www.ritzylab.com/blog/ai-cant-write-your-firmware [^3]: Design News, "The Productivity Myth: What AI Coding Assistants Actually Deliver for Firmware Engineers," Endeavor Business Media, May 13, 2026. https://www.designnews.com/artificial-intelligence/the-productivity-myth-what-ai-coding-assistants-actually-deliver-for-firmware-engineers [^4]: RespCode, "How RespCode Compiles Bare-Metal Firmware," RespCode blog, February 10, 2026 (updated February 12, 2026). https://web.archive.org/web/20260607052239/https://respcode.com/blog/how-respcode-compiles-baremetal-firmware [^5]: EdgeLog, "AI-Written Firmware" series, Parts 1–4, April 4–25, 2026. https://edgelog.dev/blog/?category=embedded-dev [^6]: BootLoop, "Why Your AI Coding Assistant Can't Write Firmware," BootLoop blog. https://bootloop.ai/blog/why-your-ai-coding-assistant-cant-write-firmware [^7]: "The Prover Is the Judge," arXiv preprint 2607.14340, cs.SE, July 15, 2026. https://arxiv.org/abs/2607.14340 [^8]: Xu et al., "EmbedAgent: Benchmarking Large Language Models in Embedded System Development," ICSE 2026. https://arxiv.org/abs/2506.11003 --- ## HIL won't scale to the AI era (https://veecle.ai/blog/hil-wont-scale-ai-era, 2026-08-10) The agent changed how the firmware boots. Again. The code is ready to test. The test is waiting for the one HIL bench that has the right harness, a free USB port, the right firmware already flashed, and nobody else using it. That's not a story about bad engineers. It's what the hardware-in-the-loop bench is for. Real controller. Real I/O. Real timing. Real electrical failure modes. The problem is asking that bench to be the default feedback loop for every generated change. AI doesn't make HIL obsolete. It makes the work before HIL non-negotiable. ### HIL isn't the enemy Let's get this out of the way first. HIL exists because some things need physical hardware: signal delays, electrical fault injection, bus timing, the actual controller running under the conditions it'll see in a product. ISO 26262 names HIL as part of the verification toolchain.[^1] For high-ASIL work, it recommends checking results across environments because they can disagree. That's not a bug in the standard — it's the point of running more than one layer. HIL also has a lifecycle of its own. A 2012 paper on HIL testing under ISO 26262[^2] lays it out plainly: build the rig, qualify it, use it, modify it, qualify it again. The bench is infrastructure. Infrastructure needs maintenance. So this isn't an argument to throw away the bench. It's an argument to stop using the most expensive, least parallel part of the stack to catch failures that a cheaper layer can reject first. ### The test pyramid got a new input The usual embedded testing stack already has the shape: 1. Build and static checks. 2. Software and virtual targets. 3. HIL. 4. Product or vehicle testing. That's not a new idea. A 2024 paper on virtual ECUs[^3] makes the sensible case: move the requirements that can be tested in software off HIL, and keep the requirements that need physical hardware on HIL. It doesn't pretend the two are interchangeable. AI changes the pressure on the top of that stack. The code-quality story is messier than the AI discourse wants it to be. Google's DORA reports[^4] don't show a clean "AI makes teams faster" law. A randomized METR study[^5] found experienced open-source developers slower with the tools it tested. In safety-critical automotive C, a Scania/KTH study[^6] found LLM success varied wildly by module. Fine. We don't need a universal AI productivity number to see the problem. If an agent can propose ten firmware changes before lunch, a team needs a way to reject the obviously broken ones before those changes compete for a shared bench. The verification system has to handle candidate code at machine speed, even when the final answer still comes from real hardware. That means the pyramid becomes a funnel. Many changes enter at the top. Few should make it to the bottom. > **Diagram 1: Firmware verification funnel.** Build and static checks → virtual-board > runs → HIL bench → product sign-off. Full designer brief: [`diagrams.md`](./diagrams.md). ### Give every layer one job The compiler is ruthless and cheap. It catches stale APIs, missing dependencies, broken types, and the language rules the model forgot existed. In our recent experiment, two models wrote an old STM32 HAL API; both died at compile time. Good — that's exactly where that failure belongs. The virtual board is the next gate. It runs the ELF against a model of the target board and gives the agent a receipt: expected output, a hang, a fault, or silence. That catches a different class of failure: wrong peripheral mode, a state machine that never starts, code waiting forever for an event, or firmware that simply never emits the expected behavior. We saw that too: an agent's nRF52840 attempt compiled cleanly, then ran for five seconds and produced none of the expected output. Not a style nitpick, not a vibe from code review — a real, unambiguous "this firmware didn't do the thing" receipt. The [full run and evidence](/blog/what-llms-get-wrong) are public. Then comes HIL. That's where you validate the physical stuff the virtual layer shouldn't claim to own: exact timing, pin routing, electrical behavior, real buses, sensors, actuators, and the ugly interactions that only show up when there are cables involved. The layers don't compete. They protect each other. > **Diagram 2: One generated change, two feedback loops.** Bench-first versus > funnel-first, converging on the same final HIL bench. Full designer brief: > [`diagrams.md`](./diagrams.md). ### The bench is a scarce resource HIL vendors don't dispute this. NI's own documentation[^7] notes that high-channel systems can require hundreds or thousands of connected signals, with wiring that's costly and time-consuming to maintain. The global chip shortage made the broader point even more brutal: when physical controllers and prototypes are scarce, every hardware-dependent test plan inherits that scarcity. That doesn't mean every team is waiting 26 weeks[^8] for a board today. It means hardware availability is a delivery constraint that software-only feedback loops don't have. And there is a less dramatic version of the same problem in every lab: one capable bench can only run one setup at a time. People book it. Someone changes a cable. Someone misconfigures the hardware. Someone is debugging a failure a virtual run could have rejected in seconds. That's poor allocation of the most valuable test environment you own. ### Be honest about the boundary Simulation doesn't get a free pass here. In [our own probes](/blog/what-llms-get-wrong), a byte-level simulation model accepted clock and configuration mistakes that real hardware wouldn't forgive. We wrote that down instead of editing it out, because the boundary matters. A virtual target isn't a magic copy of reality. It's a test environment with a scope. So is HIL. So is a vehicle. The point is to know what each one can prove. | Failure or requirement | Build | Virtual board | HIL / silicon | | -------------------------------------------------------- | ------ | --------------------------- | ------------------ | | Stale API or missing dependency | Strong | Not needed | Not needed | | Firmware boots, progresses, and produces expected output | No | Strong | Final confirmation | | Wrong peripheral mode or a hang | No | Strong | Can confirm | | Exact timing, pin routing, electrical behavior | No | Not sufficient for sign-off | Final authority | | Real sensor, actuator, and electrical fault behavior | No | Not sufficient | Final authority | The goal was never fewer layers. It's fewer bad binaries reaching the expensive one. ### What changes now Embedded teams used to accept a slow feedback loop because firmware changes were slow too. A human wrote the code, flashed the board, and watched the output — the whole loop was human-paced. It's not anymore. AI can generate code faster than a bench can be booked. That doesn't change what real hardware is for. It changes when we should ask it a question. Run the cheap checks first. Run the virtual target next. Keep the bench for the tests that need a bench. That's how HIL stays valuable in the AI era: not as the place every change begins, but as the place the surviving changes earn their final sign-off. ### Sources [^1]: ISO 26262-4:2018 and ISO 26262-6:2018, "Road vehicles — Functional safety." https://www.iso.org/standard/68386.html / https://www.iso.org/standard/68388.html [^2]: Himmler, Lamberg, and Beine, "Hardware-in-the-Loop Testing in the Context of ISO 26262," 2012. http://web1.see.asso.fr/erts2012/Site/0P2RUC89/TC-2.pdf [^3]: Keil et al., "Evaluation of SiL Testing Potential—Shifting from HiL by Identifying Compatible Requirements with vECUs," _Vehicles_, 2024. https://doi.org/10.3390/vehicles6020044 [^4]: Google, "State of AI-Assisted Software Development 2025." https://services.google.com/fh/files/misc/2025_state_of_ai_assisted_software_development.pdf [^5]: Becker, Rush, Barnes, and Rein (METR), "Measuring the Impact of Early-2025 AI on Experienced Open-Source Developer Productivity," 2025. https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/ [^6]: Sevenhuijsen, Patil, Nyberg, and Ung, "Generating Safety-Critical Automotive C-programs using LLMs with Formal Verification," PMLR 284, 2025. https://proceedings.mlr.press/v284/sevenhuijsen25a.html [^7]: NI, "HIL Test System Architectures." https://www.ni.com/en/solutions/transportation/hardware-in-the-loop/hardware-in-the-loop--hil--test-system-architectures.html [^8]: KPMG, "Surviving the Silicon Storm," 2021. https://web.archive.org/web/20251113024813/https://assets.kpmg.com/content/dam/kpmg/br/pdf/2021/06/automotive-semiconductor-shortage.pdf --- ## Wokwi vs Renode vs QEMU: hobby, professional, and CI tiers of embedded simulation (https://veecle.ai/blog/wokwi-vs-renode-vs-qemu, 2026-08-05) People search "Wokwi vs Renode vs QEMU" expecting a shootout. A winner, a runner-up, and one you avoid. That's the wrong shape for the question. These three tools barely overlap. Wokwi is where you prototype, Renode is where you develop firmware seriously, QEMU is where you gate a Linux build in CI. You can use all three in one week on one project and never once have to choose between them. The reason the shootout framing survives is that the content ranking for the query is written by people who haven't run any of them. ### The tier map | Tier | Tool | What it's for | What it can't do | | ------------------------ | ------ | ----------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | Hobby / prototyping | Wokwi | Arduino and ESP32 sketches, wiring up LEDs and displays in a browser, teaching, smoke-testing a library | Timing-precise peripherals, a serious build-system integration, most professional MCU families | | Professional development | Renode | Register-level peripheral models, interrupts, multi-node networks, scriptable regression runs, unusual boards you describe yourself | Cycle counts, analog behavior, power draw, anything off-chip you didn't model | | CI gating | QEMU | Booting embedded Linux fast, driver and Yocto work, full-system emulation across many architectures | Counting cycles, and the Cortex-M board you actually own is probably not in the list | Same pipeline, three different jobs. Read that table as a routing decision, not a ranking. ![Pyramid diagram showing Wokwi, Renode, and QEMU as the hobby, professional, and CI tiers of embedded simulation](/blog/wokwi-vs-renode-vs-qemu/image-1.webp) ### Wokwi: the tier everyone underrates and then overreaches with Wokwi is a browser tab that runs Arduino, ESP32, RP2040 and friends at near real-time, with LEDs, OLED displays, sensors and motors you drag onto a canvas. No toolchain, no flashing, no cable. Share a link and someone else sees your circuit running. For its tier that's close to unbeatable. Nathan Jones, writing up simulation options on EmbeddedRelated, guesses the hobbyist and educational tools are the easiest to learn of the bunch, and he's right.[^1] Uri Shaked, who built Wokwi, calls simulation a superpower, and the reason that lands is that Wokwi removes every excuse not to try something. The failure mode is treating it as your development environment. Jones puts his finger on the exact cost: updating a Wokwi project means uploading source files, then remembering to propagate the changes back to your real tree. His old coworker's line for that is "the same information in two places, guaranteeing that one of them is always wrong."[^1] There is a CLI, wokwi-cli, and it will run a simulation, but your interaction with a running sim is limited to the serial port or a predefined automation scenario that was still in alpha when he wrote it. Wokwi also doesn't model timing-precise peripherals. That's not a knock, it's the tier boundary. If your bug is "the DMA completes before the flag is checked," this is not the tool, and no amount of dragging components onto the canvas changes that. ### Renode: the professional tier, and the one we build on Renode is where the peripherals become real. Not "there is a UART" real, but "the UART has registers at the addresses your datasheet says, and writing the wrong bit does the wrong thing" real. Interrupts, timers, DMA, SPI, I2C, Ethernet, and multi-node setups where several boards talk to each other over a simulated wire. Chiplab runs on Renode. I'm not going to pretend that's a neutral position. But the reason we picked it is the same reason I'd recommend it to someone with no interest in our product: it's the only tool in this group where you can add a board that nobody has added before. Platform descriptions are files you write. When the peripheral you need isn't modeled, you model it. That's a real cost in hours and it's also the only escape hatch that exists, because [every chip is an island](/blog/renode-vs-qemu-what-each-models) and no vendor is going to ship yours. One correction to Jones here, because he's slightly off on this one. He puts Renode and QEMU in the bucket of simulators that don't support external components.[^1] For QEMU that's fair. For Renode it isn't quite: off-chip devices go in the platform description like everything else, which is how you get a simulated I2C sensor answering your driver. It's a text file instead of a drag-and-drop canvas, which is probably why it reads as "unsupported" from outside. What Renode will not do is count cycles. There is no cycle-accurate mode. It executes instructions and advances a virtual clock, which is a completely different thing from telling you how many cycles your loop took. We wrote up where that bites in [what simulation can't catch](/blog/what-simulation-cant-catch), including our stack cheerfully returning zeros from a cycle counter that doesn't exist on the virtual board. ![Chart comparing peripheral fidelity of Wokwi, Renode and QEMU across registers, interrupts and cycle counts](/blog/wokwi-vs-renode-vs-qemu/image-2.webp) ### QEMU: the CI tier, and the narrowest board list of the three QEMU is infrastructure. It boots Linux on ARM, AArch64, RISC-V, MIPS, PowerPC and x86, it plugs into Yocto, and it's fast. If you're doing embedded Linux, driver work, or filesystem validation, this is your tool and the other two aren't in the conversation. Two things get misread about it constantly. First, QEMU does not count cycles. It counts wall-clock time. People assume "full-system emulation" implies timing fidelity, and it doesn't. Second, the Cortex-M board list is short. When we went looking, QEMU shipped exactly five STM32 machines, and none of them was the F4-Discovery, which is one of the most common dev boards on earth. Details and the full list are in [QEMU for Cortex-M](/blog/qemu-for-cortex-m-what-works). That post also contains the result I liked least and trust most: QEMU killed a broken ELF instantly with a lockup, and Renode ran the same broken firmware to completion and printed clean output. QEMU was right. We were wrong. So the CI tier isn't "worse." It's stricter about a narrower set of things. That's what you want from a gate. ### The license cell, and why you should distrust the table you're reading Now the part that made me want to write this. The page ranking for "Wokwi vs Renode vs QEMU" belongs to industrialmonitordirect.com, a company that sells industrial panel PCs, in the knowledgebase section, and it ends with a CTA to configure a panel PC.[^2] Most of its comparison table is fine. Wokwi targets Arduino and ESP32 with basic peripherals, Renode targets Cortex-M and RISC-V with advanced ones, QEMU does full-system emulation. Their recommended pipeline — Wokwi or Renode for unit tests, Renode for integration, QEMU for Linux systems, hardware for final validation — is roughly the tiering I just spent a thousand words on. Credit where it's due. The license row says Renode is "Open-source (Apache 2.0)." It's MIT. Open any Renode repository on GitHub and read the LICENSE file. This takes fifteen seconds and it is the single easiest cell in the entire table to get right, because unlike every other row it requires zero judgment. It's a lookup. That exact error is the opening of [our Renode alternatives post](/blog/renode-alternatives), where the top-ranked page for a different query made the identical mistake. Two independent pages, same wrong cell. Nobody opened the repository. And the same column lists Wokwi's license as "Freemium," which is a pricing model. I'm being narrow on purpose. One wrong cell doesn't make a table useless, and I'd rather name the specific verifiable error than wave at the whole thing and call it slop. But the license cell is the cheapest possible fact-check in the document, and it's wrong, which tells you exactly how the rest of the rows were produced: from general knowledge of what these tools are, not from running them. That's fine for a rough orientation. It's not fine as input to a decision you'll live with for two years. We went and read every comparison of this shape we could find, and [we read every comparison like this one](/blog/embedded-simulator-comparison-audit) has the full tally. This post only needs one data point from it. ### How to actually choose Jones proposes three questions for evaluating virtual hardware, and they beat any feature table because they're about your project instead of the tool's marketing.[^1] **Does it support your MCU and your external components?** Start here, because it eliminates fastest. Notably, Jones argues a missing MCU isn't automatically fatal if your code is written to not care which processor it runs on, and that a missing sensor can often be substituted with something that produces a similar value. If neither your chip nor your components are supported, skip simulators entirely and compile for your host with printf. For a specific STM32 or nRF part, Renode is usually the answer, and if the board isn't there you write the platform file. For an ESP32 with a display and a servo, Wokwi already has all three. For anything that boots Linux, QEMU. **How easy is it to learn?** This one is about total time budget. Setting up the simulation has to cost a fraction of building the project, or you've lost the benefit. Wokwi wins outright. Renode has a real learning curve, especially platform descriptions. QEMU's curve depends entirely on whether your board is already supported, and it's steep on the far side of that. **How well does it fit your build system?** Jones gives the edge to the command-line tools, Renode and QEMU, and puts the graphical ones at a disadvantage because updating the sim means moving files around.[^1] I'd sharpen it: the question is whether one command rebuilds and reruns. If a human has to click, it will not run on every commit, and a simulation that doesn't run on every commit is a demo. For us that last question is the whole product. An agent writing firmware needs a target it can call without a human in the loop, which is why [Chiplab is Renode behind MCP](/blog/wokwi-alternatives-professional-firmware) rather than a nicer UI on top of it. The code takes minutes. Proving it works is the part that takes months, and it only gets shorter if the proof step is one command. ### Pick by the job If you're learning, or wiring up a sensor to see if the idea holds, open Wokwi. If you're writing drivers for a specific part and you need the registers to behave, Renode. If you're gating a Linux image in CI, QEMU. If you need cycle counts, none of these, go get the board. Nobody wins, because they were never in the same race. ### Sources [^1]: Nathan Jones, "Simulating Your Embedded Project on Your Computer (Part 2)," EmbeddedRelated, November 4, 2024. https://www.embeddedrelated.com/showarticle/1697.php [^2]: Industrial Monitor Direct, "Wokwi vs Renode vs QEMU: Embedded System Simulators Comparison," knowledge base, accessed August 5, 2026. https://industrialmonitordirect.com/blogs/knowledgebase/wokwi-vs-renode-vs-qemu-embedded-system-simulators-comparison --- ## We read every "embedded simulator comparison" on the internet. Most are written by resellers. (https://veecle.ai/blog/embedded-simulator-comparison-audit, 2026-08-05) I went looking for prior art. Before writing our own comparison of firmware simulators I wanted to know what already ranks, so I searched the obvious queries and started reading from the top. The first page I opened was an embedded simulator comparison of Wokwi, Renode and QEMU, with a tidy feature table.[^1] Row: license. Renode's cell reads "Open-source (Apache 2.0)." Renode is MIT. It is MIT in the LICENSE file at the root of the repository, in one word, on a line by itself.[^2] You do not need to read code, clone anything, or understand what a platform description file is. You need to click one link on GitHub. Then I looked at who published it. The page has "Request a Quote" and a cart in the navigation. The company sells industrial monitors and panel PCs. Not firmware tooling. Displays. ### The one thing I actually verified Let me be precise about what this post is, because the title is a hook and the methodology deserves better than a hook. I did not read every embedded simulator comparison on the internet. Nobody has. I read the ones that rank for the queries a firmware engineer would plausibly type, and I verified exactly one factual claim on one page against the primary source. That claim was wrong. Everything after this is a pattern I'm describing, not a census I'm reporting. One verified error is enough, though, and it's enough for a specific reason. A license field is the cheapest possible fact. It's a single word, it lives in a file literally named LICENSE, and it never requires you to install the tool you're writing about. If a comparison table can't get that right, the table was not built by anyone who opened the project. ![Illustration of an embedded simulator comparison table with Renode's license wrongly listed as Apache 2.0, circled in red](/blog/embedded-simulator-comparison-audit/image-1.webp) Same site, sibling page, same mistake. The best-ranked result for "Renode alternatives" lives on the same domain and also calls Renode Apache 2.0, which is why [our own Renode alternatives post](/blog/renode-alternatives) opens on it.[^3] The error got a paragraph in [why we won't write Chiplab vs pages](/blog/why-we-wont-write-chiplab-vs-pages) too. I'm not re-deriving it here. What's new in this post is the second half of the observation: it isn't one bad page. The whole shelf of top results for embedded simulator comparison queries is stocked by people with no reason to run the tools. ### Why a single wrong cell takes the page down with it In most B2B content a small factual error is a rounding error. The reader doesn't check, can't check, or doesn't care. Developer tooling doesn't work like that. The reader has a terminal open. Post 26 goes into the trust dynamic in detail, so the short version: developers verify, and one broken detail costs you the whole document, not just that line. Embedded engineers are the extreme case, because our entire job is being suspicious of documentation. Half of debugging a peripheral is discovering that the reference manual and the silicon disagree. That reflex doesn't switch off when we read a blog post. So here's what happens to that comparison table in practice. I read one cell, checked it against the repository, and found it wrong in under a minute. Now every other cell is unknown to me. Does Wokwi actually support the part it says it does? Is that QEMU claim current or four years stale? I have no way to tell, and I have one data point saying the author didn't check either. The rational move is to close the tab. That's the asymmetry that makes this content worse than useless. It isn't neutral filler occupying a slot. It costs the reader time and then hands them nothing they can rely on. ### The incentive problem, not a morality problem I want to be careful here, because the obvious version of this post is "look at these frauds," and that's both boring and wrong. The people publishing this stuff are not lying. They're doing content marketing exactly as it's normally practiced, and the practice does not include a verification step. The brief says: rank for a technical query adjacent to what we sell, so that firmware people land on our domain and maybe buy a panel PC. The writer gets a keyword, a word count and a deadline. They assemble a table from other tables, most of which were assembled from other tables. Somewhere upstream, years ago, one person guessed Apache 2.0 because Apache 2.0 is what open source usually is, and the guess has been getting copied forward ever since. Nowhere in that chain does anyone have a reason to open Renode's repository. Selling monitors does not require knowing what license Renode ships under. The incentive stops at "page exists, page ranks, page has a quote form." ![Chart contrasting who ranks for embedded simulator comparison queries with who has actually run the simulators](/blog/embedded-simulator-comparison-audit/image-2.webp) Compare that to the incentive on the other side. The people who genuinely know how Renode and QEMU differ are the people who fought both of them into a CI pipeline at 2am. Their output is a GitHub issue comment, a mailing list reply, a paragraph buried in a conference talk. None of it is structured as a comparison, none of it is optimized for anything, and it never outranks a table. That gap is the whole story. Motivation to publish and knowledge of the subject are sitting in different places, and search rewards the first one. ### How to spot a comparison nobody ran Since you're going to keep hitting these pages, here's what I use. None of these are proof on their own. Two or three together and I stop reading. **Check the license row first.** It's the fastest lie detector in the document. It costs you one click to verify and it tells you whether the author touched the project at all. **Look for the shape of a real run.** A person who actually used the tool produces artifacts: a command line, an error message, a version number, a specific board name that didn't work. Prose that never descends below "supports a wide range of platforms" was written from other prose. **Read the last section before the CTA.** On the reseller page family, the tell is a hardware recommendation block bolted to the end of a software comparison. The comparison exists to get you to that block. **Check whether any tool loses at anything.** Real tools have sharp edges and the edges are the useful part. QEMU's Cortex-M device coverage and Renode's timing model are different kinds of thing, and a table with checkmarks in both columns has flattened away the only information you came for. **See if it says when not to switch.** Comparison content written by someone who has used all the options always contains a "stay where you are" case, because that's usually the right answer. Content written to capture a query never does. **Check the date against the tools.** These projects move. A page with no date, or a 2021 date and no update note, is describing software that no longer exists in that form. ![Diagram of the content pipeline that produces embedded simulator comparisons with the verification step missing](/blog/embedded-simulator-comparison-audit/image-3.webp) ### What the honest version has to do The fix isn't complicated, it's just unrewarded. An embedded simulator comparison worth a firmware engineer's time has to clear four bars. Every factual claim traces to a primary source. License from the LICENSE file. Supported boards from the supported-boards doc, not from another blog post. Behavior from a run you did, with the output pasted in. The conclusion is discovered, not assigned. If you decide the winner before the first row, you'll write a table that produces that winner, and the reader will feel it by row three. The organizing axis is the job, not the vendor list. "Which of these three tools is best" has no answer. "I need a CI gate for a Cortex-M4 board that isn't in the QEMU tree" has one, and it's a different one from "I want to teach an Arduino class in a browser tab." Conflicts get declared in the text, not the footer. We build on Renode. That belongs in the second paragraph of anything we write about Renode, because you should read our comparisons with that in hand. We took a run at exactly this in [the simulator tiers post](/blog/wokwi-vs-renode-vs-qemu), which is this batch's attempt at a comparison built on what each tier actually models rather than on who we'd like to beat. Go check its cells. I'd rather be corrected than ranked. The uncomfortable part of all this is that the wrong page is winning right now. It's above us, it's above the docs, and it will keep collecting the traffic of every engineer who searches for an embedded simulator comparison this month. Some of them will believe the license row. The only lever I have is to publish the corrected version and put the receipt next to it. Renode is MIT. The file is right there. ### Sources [^1]: Industrial Monitor Direct, "Wokwi vs Renode vs QEMU: Embedded System Simulators Comparison," knowledge base. https://industrialmonitordirect.com/blogs/knowledgebase/wokwi-vs-renode-vs-qemu-embedded-system-simulators-comparison [^2]: Renode, GitHub repository, LICENSE file and README "License & contributions." https://github.com/renode/renode [^3]: Industrial Monitor Direct, "ARM Cortex-M Simulators QEMU Renode Keil Comparison," knowledge base. https://industrialmonitordirect.com/de/blogs/knowledgebase/arm-cortex-m-simulators-qemu-renode-keil-comparison --- ## HIL is a scarcity workaround (https://veecle.ai/blog/hil-is-a-scarcity-workaround, 2026-08-05) Hardware-in-the-loop testing is not a methodology somebody chose on merit. It's what you build when there is one bench and forty engineers. I want to be careful here, because this is the kind of claim that reads as an attack on an industry that isn't wrong about very much. HIL vendors know exactly what their product is for. Their own documentation is more honest about the limits than most of the people quoting it. The argument in this post is not that HIL is fake. It's that the _allocation_ is historical. A large share of the tests sitting in a bench queue right now are there because, when somebody wrote them, a physical rig was the only thing that could run them. That's a supply constraint wearing the costume of a standard. ### Sort a HIL vendor's own list and the pattern shows up immediately OPAL-RT published a post called "7 advantages of HIL testing."[^1] They make real-time simulators, they've been at this for decades, and the list is a fair summary of why teams buy benches. Read it as a list of _reasons_, though, and it splits into two very different piles. Advantage 1: catch defects "before hardware builds." Advantage 2: shorten validation "before prototype availability," because "you don't have to wait for the motor, grid interface, hydraulic rig, or vehicle mule to be available." Advantage 5: nightly builds can hit a bench instead of waiting for manual setup. Advantage 7: "fewer physical prototypes reduce validation cost," and a supplier with several controller variants "can run common validation suites on one simulator instead of preparing separate physical rigs for each new configuration." Four of the seven are scarcity. Not scarcity as a metaphor. Scarcity as in the physical thing does not exist yet, or costs too much to duplicate, or there's one of it and a queue behind it. Advantage 4 is stranger. It says repeatability is a HIL advantage because "physical setups drift as temperatures shift, components age, and operators make small adjustments." That's not an argument for hardware. That's an argument that physical setups are worse, and simulation is better, which is a point I'd have been called a shill for making. That leaves two that are actually about physics: safe fault injection with real electrical I/O, and high-fidelity plant models where "safety limits and certification evidence depend on responses measured in milliseconds." ![Diagram sorting the stated advantages of hardware-in-the-loop testing into scarcity reasons and physics reasons](/blog/hil-is-a-scarcity-workaround/image-1.webp) None of that is a gotcha. It's a vendor describing their product accurately. The interesting part is what happens once you notice that the scarcity pile and the physics pile have completely different futures. The physics pile is permanent. The scarcity pile is a function of what your tooling could do the year the test was written. ### HIL already conceded the argument it's used to win Here's the part that I think gets skipped. HIL is itself a simulation compromise. OPAL-RT's definition: it "puts the actual controller into a closed loop with a real-time plant model."[^2] The plant is fake. The motor is fake. The battery pack, the grid, the airframe, all fake. Nobody in this industry argues that you need a real dyno to validate a torque loop, and nobody calls that a loss of rigor. So the boundary between real and modeled has already moved once, and it moved a long way. The line currently sits at the connector on the controller, and it sits there because that's where modeling capability ran out when benches were designed. Everything on the plant side was tractable to model. The controller was not, so it stayed physical. That's the whole thing. The line is drawn at the edge of what could be simulated, and it gets redrawn every time that edge moves. People who cite HIL as proof that "you need real hardware" are citing the previous round of exactly the argument they're making against. And the edge has moved on the controller side. Register-accurate peripheral models, real interrupt priority and preemption, DMA that actually contends for the bus, timers that count in the right units. A decade ago "simulate the MCU" meant a CPU core and a UART that printed strings. That was genuinely not good enough to take work off a bench. It's a different question now, and "we tried simulation in 2014" is not an answer to it. ### What actually survives The remainder is real and it isn't going to zero. OPAL-RT's own framing is the best definition of it I've read: "HIL fits systems where timing errors can damage hardware, break safety logic, or hide integration faults that software-only tests will miss."[^2] Read that as a scope statement rather than a sales pitch and it's a precise description of the last mile: Real analog behavior. Sensor saturation during a transient, bus ripple, a resolver offset, a channel mis-scaled by a volt. OPAL-RT puts interface fidelity ahead of model detail for exactly this reason, because the controller "only reacts to sampled inputs, output loading, and transport delays." Their line is "poor I/O design will ruin a brilliant plant model." Analog is not a software problem and pretending otherwise is how you ship a protection routine that only works on the bench. Real RF and EMI. Nobody is modeling the coupling between a switching inverter and a CAN transceiver well enough to bet a recall on it. Real electrical and mechanical faults. A stuck-high input on an actual pin, a harness that half-seats, a connector that fails after thermal cycling. True timing under real silicon. Not the timing your model says the part has. The timing the part has, including the errata sheet, including the flash wait states nobody documented, including the one peripheral that behaves differently on the -A revision. That set is legitimate, it's where the expensive bugs live, and it justifies every bench that exists. It's also, if you're honest with yourself, not where most of your bench hours go. ### The honest counterweight: demand is growing anyway I don't want this to land as "share is shrinking, therefore HIL is in decline," because the numbers point the other way. Electric car sales passed 17 million in 2024.[^3] OPAL-RT cites that as evidence that "more software-heavy control systems now need closed-loop validation," and they're right.[^2] Grid control is doing the same thing from a different direction: wind and solar supplied a record 13.4% of global electricity in 2023, which means power system benches now have to represent far more inverter-based behavior than they used to.[^4] More software-defined systems means more controllers, more safety logic, more certification evidence. A shrinking percentage of a much larger number is a bigger number. Bench demand can grow at the same time as bench _dependence_ falls, and I expect both. ![Chart showing the share of validation requiring hardware-in-the-loop testing shrinking while a real last mile persists](/blog/hil-is-a-scarcity-workaround/image-2.webp) The chart has no numbers on it on purpose. I'm not going to invent a percentage for how much bench work is displaceable, because I don't have that data and neither does anyone who quotes one at you. The shape is a logical claim, not a measurement. ### Where I stop making claims Our own stack does not model any of the last mile. No analog. No RF. No EMI, no connector wear, no thermal drift. Chiplab runs firmware against modeled peripherals, and if you point it at something outside that boundary it will not warn you, it will just be wrong quietly. There's a whole post on [what simulation can't catch](/blog/what-simulation-cant-catch), including our stack returning zeros for a cycle counter that doesn't exist on the virtual board. So no, this isn't "buy simulation instead of a bench." If your failure mode is a saturating current sensor, a bench is the correct instrument and everything else is theater. ### The question to actually ask OPAL-RT says it plainly, in the middle of a guide selling HIL: "you don't need HIL for every function. Static calibration screens, reporting logic, and slow supervisory states are usually settled earlier with software tests and review." And: "HIL becomes important when the controller's timing is part of the requirement itself."[^2] That's a routing rule, and it's a good one. It's also not the rule most teams are following, because most teams never routed anything. Tests accumulated on the bench the way files accumulate in a home directory. So go look at the queue. For each thing in it, ask which reason it's there for. Does it need real silicon, real analog, real timing under real errata? Keep it, and stop apologizing for the capex. Or is it there because in 2019 the bench was the only thing that could run a firmware image at all? That one is a candidate to move, and the fact that it's been on the bench for six years is not evidence either way. The practical version of this argument, with an actual procedure for splitting the queue, is in [simulation vs HIL: what belongs in CI and what belongs in the lab](/blog/simulation-vs-hil-what-belongs-in-ci). If the acronyms are the blocker, [MIL, SIL, PIL, HIL for firmware people](/blog/mil-sil-pil-hil-for-firmware-people) sorts out the vocabulary first. One more line from OPAL-RT, which I'd frame and hang on a wall: "most failed HIL efforts collapse under weak assumptions about plant scope, interface timing, or pass criteria rather than raw simulator limits."[^2] The failure mode is never the simulator being too coarse. It's a team not knowing why a test is where it is. That cuts in both directions, and it's the actual point of this post. HIL is a workaround, and it's a good one. Workarounds get smaller when the constraint they route around gets smaller. That's not an insult. That's just what a workaround is. ### Sources [^1]: "7 advantages of HIL testing," OPAL-RT Technologies, June 16, 2026. https://www.opal-rt.com/blog/7-advantages-of-hil-testing [^2]: "A Guide to hardware-in-the-loop (HIL) testing in 2026," OPAL-RT Technologies, May 13, 2025. https://www.opal-rt.com/blog/a-guide-to-hardware-in-the-loop-testing [^3]: International Energy Agency, "Global EV Outlook 2025," IEA, 2025. https://www.iea.org/reports/global-ev-outlook-2025 [^4]: Ember, "Global Electricity Review 2024," Ember, May 2024. https://ember-energy.org/latest-insights/global-electricity-review-2024/ --- ## From datasheet to running firmware in one prompt (https://veecle.ai/blog/datasheet-to-firmware-one-prompt, 2026-08-05) An AI agent can bring up firmware on an unfamiliar board from the reference manual alone, on the first attempt, if it can run what it writes. Here is the run that shows it, including the part that didn't work. Almost every demo on this blog uses an STM32F4. That's not an accident, it's a habit. The F4 is the part I know best, it's the part with the most sample code on the internet, and any model you'd care to use has seen a thousand blinky examples for it. Asking an agent to write firmware for an F4 is barely a test. It's recall. So I gave it something else: a NUCLEO-L073RZ. Cortex-M0+, low-power L0 family, a different clock tree and a different peripheral map from anything else in this series. One prompt, no starter project, no working example to copy from. Read the manual, write the firmware, run it, show me UART output. ![Diagram of an AI agent writing firmware for an unfamiliar board from a datasheet and running it in simulation](/blog/datasheet-to-firmware-one-prompt/image-1.webp) ### Why this board is a real test and the F4 isn't Three things have to be right before a single character shows up on a serial line, and on the L0 all three are different from the part the agent has the most training data for. The clock. An F4 comes out of reset running on its 16 MHz internal RC oscillator. An L0 does not, it comes up on MSI, the multi-speed internal oscillator, at roughly 2 MHz. If you write F4 habits into L0 code you get a chip that runs, and a baud rate that's wrong by a factor of eight, which means a UART that prints garbage rather than nothing. Garbage is worse. Nothing tells you the peripheral never came up. Garbage tells you it did, and then you spend an hour looking at the wrong layer. The pins. USART2 is on PA2 and PA3, which happens to match the F4. What doesn't match is the alternate-function number that routes the pin to the peripheral. Every family reshuffles that table, and the reshuffling is the entire content of a datasheet's pin-assignment section. Pick the F4 number on an L0 and the GPIO is configured, the USART is configured, and the two are not connected to each other. The LED. PA5, LD2 on a Nucleo-64. This one is genuinely the same, and it's the only free lunch in the list. None of this is hard. It's just specific, and "specific" is exactly where a model with no way to check its work falls apart. Every chip is an island, and the bridge is the manual. ![Diagram comparing STM32F4 clock and pin assumptions against STM32L073 requirements for firmware bring-up](/blog/datasheet-to-firmware-one-prompt/image-2.webp) ### The firmware it wrote Rust, `no_std`, `stm32l0xx-hal`. This is the code that ran, not a cleaned-up version of it: ```rust #![no_main] #![no_std] use cortex_m_rt::entry; use embedded_time::rate::Extensions; use nb::block; use panic_halt as _; use stm32l0xx_hal::{pac, prelude::*, rcc::Config, serial}; const DBGMCU_IDCODE: *const u32 = 0xE004_2000 as *const u32; #[entry] fn main() -> ! { let dp = pac::Peripherals::take().unwrap(); let mut rcc = dp.RCC.freeze(Config::hsi16()); let gpioa = dp.GPIOA.split(&mut rcc); let serial = dp.USART2.usart( gpioa.pa2, gpioa.pa3, serial::Config::default().baudrate(115_200_u32.Bd()), &mut rcc, ).unwrap(); let (mut tx, _) = serial.split(); let mut led = gpioa.pa5.into_push_pull_output(); // ... prints a banner, reads DBGMCU_IDCODE, toggles PA5 six times over UART ... } ``` The interesting line is the third one in `main`. `Config::hsi16()` switches the clock tree off MSI and onto the 16 MHz internal oscillator, and hands the resulting frequency to `rcc`, which is then passed into the USART constructor so the baud divider is computed against the clock that is actually running. That's the whole trick, and getting it wrong is the single most common way to end up with a serial port that prints noise. The pin routing never appears in the source because the HAL owns it. `dp.USART2.usart(gpioa.pa2, gpioa.pa3, ...)` only compiles for pins that can physically carry that peripheral on this family, and the crate sets the alternate function itself. That's not the agent being clever, that's the Rust embedded ecosystem having encoded the datasheet into the type system a decade ago. The agent's job was to know that the crate works this way and reach for the right two pins. It did. ### The run Board `stm32l073_nucleo`. Unedited stdout: ``` 08:07:06.8801 [WARNING] usart2: Unhandled write to offset 0x8. Unhandled bits: [13] when writing value 0x20C0. Tags: DDRE (0x1). 08:07:06.8859 [INFO] usart2: [host: 0.19s (+0.19s)|virt: 0.51µs (+0.51µs)] datasheet-agent: never seen this board before, reading DBGMCU_IDCODE 08:07:06.8880 [WARNING] sysbus: [cpu: 0x8000290] ReadDoubleWord from non existing peripheral at 0xE0042000. 08:07:06.8885 [INFO] usart2: [host: 0.19s (+2.75ms)|virt: 0.51µs (+0s)] DBGMCU_IDCODE = 0x00000000 08:07:06.8896 [INFO] usart2: [host: 0.19s (+1.12ms)|virt: 0.51µs (+0s)] toggle 0 -> PA5 on 08:07:06.8899 [INFO] usart2: [host: 0.19s (+0.32ms)|virt: 0.51µs (+0s)] toggle 1 -> PA5 off 08:07:06.8901 [INFO] usart2: [host: 0.19s (+0.25ms)|virt: 0.51µs (+0s)] toggle 2 -> PA5 on 08:07:06.8904 [INFO] usart2: [host: 0.19s (+0.25ms)|virt: 0.51µs (+0s)] toggle 3 -> PA5 off 08:07:06.8906 [INFO] usart2: [host: 0.19s (+0.26ms)|virt: 0.51µs (+0s)] toggle 4 -> PA5 on 08:07:06.8909 [INFO] usart2: [host: 0.19s (+0.25ms)|virt: 0.51µs (+0s)] toggle 5 -> PA5 off 08:07:06.8916 [INFO] usart2: [host: 0.19s (+0.73ms)|virt: 0.51µs (+0s)] done, clock tree + USART2 + PA5 LED brought up on first try ``` Clock tree, serial port, GPIO. First attempt, no HardFault, no silent hang, readable text at the right baud rate. That last line is the agent's own print, not mine, and it's a little pleased with itself. The timestamps are worth a second look. The whole sequence covers 0.51 microseconds of virtual time and about 190 milliseconds of host time. The gaps between toggles are host-side scheduling, not chip behavior. Simulation time and wall-clock time are different units, and if you ever write a test that asserts on the second one you'll get a flaky suite and no idea why. ### The two things the log caught This is the part I'd cut if I were selling you something. #### The ID register read zero, and that's a simulation artifact The firmware reads `0xE0042000`, labeled `DBGMCU_IDCODE`. On real silicon the DBGMCU block holds the device and revision ID, and reading it is the standard way for firmware to ask "what chip am I actually on?" `0xE0042000` is the address I'm used to typing on F-series parts, which is almost certainly why the agent typed it too. On this virtual L0, nothing is mapped there. Renode says so in the log, in the line right before the print: ``` 08:07:06.8880 [WARNING] sysbus: [cpu: 0x8000290] ReadDoubleWord from non existing peripheral at 0xE0042000. ``` The load returned `0x00000000` and execution continued. So the value in the transcript is not a device ID. It's not a wrong device ID either. It's the absence of a peripheral, printed as a number, and I want that stated plainly because `DBGMCU_IDCODE = 0x00000000` looks exactly like a successful read of a chip that has nothing to say. This is the same failure mode as [what simulation can't catch](/blog/what-simulation-cant-catch), which is a whole post about our stack handing back zeros for things that don't exist on the virtual board. That post covers why it happens and what class of bug it hides. Here it just happened again, in a run I wasn't looking for it in, and the only reason I noticed is that the `sysbus` warning was two lines above the print. Note what did and didn't save me. The agent did not get a fault. The firmware did not misbehave. Nothing in the program's own output was wrong. The warning in the host log was the entire signal. On a physical board that read might return a real ID, or fault, or return whatever the bus leaves floating, and the point is that "the register I read is actually implemented" is an assumption you have to check on every target, in simulation or out of it. ![Diagram showing a firmware read to an unmodeled register address returning zero in simulation](/blog/datasheet-to-firmware-one-prompt/image-3.webp) #### The USART config bit the model shrugged at First line of the run: ``` 08:07:06.8801 [WARNING] usart2: Unhandled write to offset 0x8. Unhandled bits: [13] when writing value 0x20C0. Tags: DDRE (0x1). ``` Offset `0x8` is the USART's third control register, and Renode has named the bit for us: DDRE, a DMA-related error-handling flag. The HAL set it, the model doesn't implement it, the write was accepted anyway. It changed nothing here because this firmware doesn't touch DMA, it blocks on the transmit register and prints. Filing it as noted-and-harmless, which is the right call for this run and the wrong call for a run that does use DMA on receive errors. I'm mentioning it because it's the second instance in eleven log lines of a peripheral model being less complete than the silicon, and because that pattern is not a Chiplab quirk. It's what every simulator does. The difference between simulators worth using and simulators that waste your week is whether they tell you. ### What this actually proves Not that the agent is smart. That the loop is closed. Give the same model the same prompt with no way to run the result and you get plausible L0 code. It'll import the right crate. It might even get the pins right. But nobody, including the model, will know whether the clock configuration matched the baud divider until something prints, and "something prints" is a hardware event. Without a target, the agent's confidence and the firmware's correctness are unrelated variables. With a target, the run is the arbiter. The firmware either brings up the clock tree or it doesn't. The UART either produces readable text at 115200 or it produces noise. The failure mode of a wrong alternate-function number is silence, and silence is an observable. That's the whole argument for putting hardware behind an API instead of behind a bench: not that simulation is better than silicon, but that an agent can call it a hundred times in an afternoon and read the result each time. [The agent infrastructure map](/blog/the-agent-infrastructure-map) has the longer version of that argument, with the other categories that already solved it. The unfamiliar-board part is the bit I care about most. The economics of firmware are dominated by the fact that every new part is a fresh start. New reference manual, new clock tree, new peripheral map, new HAL, and weeks of an experienced engineer's time before the first line of product code gets written. If an agent can compress the bring-up phase of a part it has never seen into one prompt and one run, that changes what "supporting a new chip" costs. The code takes minutes. Proving it takes months, and this run is a small piece of evidence that the proving part is what's actually being automated here. One prompt, one board it had never touched, one run, clock plus UART plus GPIO on the first attempt, and one honest reminder to check whether the register you're reading exists at all. I'll take that trade. If you want to see which other tools can close this loop and which ones only claim to, I went through [every hardware MCP server I could find](/blog/hardware-mcp-servers-reviewed) and wrote down what each one does. --- ## Hello world on STM32F4 Discovery with FreeRTOS — no hardware (https://veecle.ai/blog/hello-f4-no-hardware, 2026-08-05) Short version: you can build a FreeRTOS firmware image for the STM32F4 Discovery with plain `make` and `arm-none-eabi-gcc`, hand the ELF to Chiplab, and read the UART output back, with no board, no ST-LINK, and no USB cable. Below is the example from our own repo, the exact stdout from a run I minted today, and an honest list of what that run does not prove. This is the boring demo. I mean that as a compliment. It's the one every embedded person recognizes on sight, which makes it the right place to check whether the claim "hardware is becoming an API" survives contact with a real toolchain. ### The example It lives in the Chiplab repo at `examples/freertos/stm32f4-discovery`. Nothing clever in it. A single FreeRTOS task gets created, and that task prints `Hello world!` over USART2, then sleeps on `vTaskDelay`, forever. That's the whole application. The interesting part isn't the task, it's everything the task drags in behind it: the FreeRTOS kernel is a real dependency here, not a stub. `tasks.c`, `list.c` and `queue.c` all get compiled in, memory comes from the `heap_4` allocator, and scheduling runs through the Cortex-M4F port layer. So the firmware that boots is doing the actual work an RTOS does: vector table, clock setup, SysTick driving the scheduler tick, context switches through the port's assembly, and a hardware peripheral being written to from inside a task context. The kernel version is pinned. The Makefile does a shallow clone of FreeRTOS kernel V11.3.0 rather than vendoring a copy or floating on `main`, which means the example doesn't quietly change behavior six months from now because upstream refactored something. If you build it in a year you get the same kernel I got today. Link flags are the usual embedded diet: `--gc-sections` to drop everything the linker can prove unreachable, and `nano.specs` plus `nosys.specs` so newlib doesn't try to be a hosted C library on a chip with no operating system under it. Standard. Which is the point. There is no Chiplab SDK to link against, no shim, no instrumentation build. It's the same ELF you'd flash. ![Diagram of a FreeRTOS task printing over USART2 into Chiplab with the physical STM32F4 Discovery board crossed out](/blog/hello-f4-no-hardware/image-1.webp) ### Building it `make`. That's the setup section, and I'm not padding it out to look like more work than it is. You need `arm-none-eabi-gcc` on your path and `git` for the kernel clone. The Makefile handles the rest: pull the pinned kernel, compile the application plus the four kernel sources plus the port layer, link against the board's linker script, emit an ELF. No IDE, no vendor project file, no code generator, no CubeMX round trip. This matters more than it sounds like it should, because "can an agent do this unattended" and "does this need a GUI" are the same question wearing different clothes. The output is a single ELF. That file is the entire interface to the next step. ### Running it without a board Chiplab takes the ELF and a `board_id`, runs it, and gives you back the artifacts. Here the board is `stm32f4_discovery`. The run I'm quoting was minted today from exactly the ELF that Makefile produces. Unedited stdout: ``` 08:04:34.7331 [INFO] cpu: Guessing VectorTableOffset value to be 0x8000000. 08:04:34.9054 [INFO] machine-0: Machine started. 08:04:34.9793 [INFO] usart2: [host: 0.21s (+0.21s)|virt: 0.12ms (+0.12ms)] Hello world! 08:04:35.3618 [INFO] usart2: [host: 0.59s (+0.38s)|virt: 0.46s (+0.45s)] Hello world! ... (task repeats on its own vTaskDelay period; 21 total prints over ~5.3 virtual seconds before the run was stopped) ... 08:04:39.8055 [INFO] usart2: [host: 5.03s (+0.22s)|virt: 4.9s (+0.22s)] Hello world! 08:04:39.9122 [INFO] machine-0: Machine resumed. Renode is quitting ``` I trimmed the middle. The task kept printing on its own period the entire time it was allowed to run, 21 prints total, and the lines in between look exactly like the lines you can see. I cut them because a wall of identical `Hello world!` rows is not evidence of anything the first three don't already establish. The last two lines are Renode's own bookkeeping as the run winds down, and I'm not going to read meaning into them that isn't there. What's worth actually looking at: The first line is a guess, and it says so. `Guessing VectorTableOffset value to be 0x8000000` is the simulator working out where the vector table lives because the ELF didn't hand it that on a plate. It guessed right, which is what you want, and it told you it was guessing, which is what you need. Compare that to a hardware bring-up where the same ambiguity shows up as a hang. The two columns on each UART line are host time and virtual time, with deltas. That's the distinction that makes simulation output readable: `virt: 0.12ms` on the first print means the firmware got from reset to its first UART write in a tenth of a millisecond of simulated time, while `host: 0.21s` is how long my machine spent producing that. By the last print, virtual time is at 4.9 seconds and host time at 5.03. The two tracked each other closely across the run, which is a nice property to have but isn't a timing guarantee, and I'll come back to why. And the scheduler works. `vTaskDelay` is not a busy loop. For that call to return, SysTick has to fire, the port's handler has to run, the tick has to increment, the task has to come off the delayed list and back onto a ready list, and a context switch has to land the CPU back in the task's stack frame. Twenty-one times, on schedule. That's the RTOS being exercised, not skipped. ### The part I want to be honest about There are no warnings in that transcript. None. Not one line about an unhandled register, an unimplemented peripheral, or an access to memory that shouldn't exist. That's real, and it's also the least surprising result in this batch of posts. FreeRTOS on the F4 Discovery is the single most-exercised path we have. It's the combination people reach for first, so it's the combination that got fixed first, and the transcript reflects that: clean boot, clean tick, clean UART, nothing to explain away. Take it as a statement about this path, not about simulation in general. Push toward less traveled boards and the logs get chattier. When [I put the Blue Pill through the same treatment](/blog/bluepill-2026), warnings showed up. When an agent went [from datasheet to firmware in one prompt](/blog/datasheet-to-firmware-one-prompt) on an L073 Nucleo, warnings showed up there too. A silent log means the board model has had attention, and that's all it means. Also, and this should be obvious but the internet is the internet: this did not run on physical silicon. There was no board on my desk today. The claim is "the firmware executed on a model of this MCU and produced this output", and it stops precisely there. ![Diagram comparing the simulated STM32F4 SRAM map with the real STM32F407 memory map including CCM RAM](/blog/hello-f4-no-hardware/image-2.webp) ### What this run covers, and what it doesn't Covered: does it build, does it boot, does the vector table land, does the scheduler tick, do context switches work, does the task reach the peripheral, does the peripheral emit the bytes you expected. That's the functional loop. For an agent writing firmware, it's most of what you need, because most of what an agent gets wrong is in that list. Not covered, and worth knowing before you lean on it: **Timing is not cycle-accurate.** Chiplab runs on Renode, which is MIT-licensed and has no cycle-accurate mode. Virtual time tracking host time in the transcript above is a scheduling artifact, not a measurement. If you need to know how many cycles your critical section takes, the number is not in this log and cannot be derived from it. I wrote up the full version of this in [what cycle-accurate actually means](/blog/what-cycle-accurate-actually-means) rather than re-deriving it here. **The memory map is not the datasheet's.** This specific board model gives you 256K of contiguous SRAM at `0x20000000`, and no CCM at `0x10000000`. Real F407 silicon has 128K there, with `0x20020000` and up reserved, plus a separate CCM block the model doesn't provide. Both directions bite. Firmware that overruns past 128K runs fine in simulation and faults on the chip. Firmware that touches CCM works on the chip and reads as nothing here. Same post covers the mechanism. **Anything outside the chip.** No power rails, no crystal that fails to start, no signal integrity, no sensor on the other end of a bus doing something the datasheet didn't mention. If your bug is analog, it isn't in this transcript. The useful framing is that this run is a compiler for behavior. It catches the class of mistake that stops the firmware from working at all, fast and repeatably, and it says nothing about the class that only shows up when electrons are involved. You still need the board. You need it much later, and much less often, and that's the whole economic argument. ### Why a hello world is the interesting example Because of who's running it. A human doing this once doesn't care that it's scriptable. An agent does. The loop that makes coding agents useful is write, run, read the failure, fix it, and embedded breaks that loop at the "run" step: the target is a physical object, attached to one desk, that one person can use at a time. Everything downstream of that constraint gets slow. The code takes minutes; proving it takes months. What the transcript above actually demonstrates is that the loop closes. ELF in, output out, no human in the middle, no cable, no queue for the shared board. That's a plain `make` plus one API call, which means it can happen a hundred times in an afternoon while nobody watches. I've mapped where this piece sits relative to the rest of the agent tooling stack in [the agent infrastructure map](/blog/the-agent-infrastructure-map). Start here, then go break something more interesting. The example is in the repo, and the parts I can't model are listed above rather than left for you to discover at 2am. ### Questions people actually ask **Can I really run STM32F4 FreeRTOS with no hardware?** Yes, for functional behavior. Build the ELF with `arm-none-eabi-gcc`, run it against the `stm32f4_discovery` board model, read the UART back. Timing and anything analog are out of scope. **Do I need to modify the firmware to run it in simulation?** No. The ELF in this example is built with the same Makefile, the same kernel, the same linker script and the same `nano.specs` / `nosys.specs` flags you'd use for a real flash. There's nothing Chiplab-specific linked in. **Does the FreeRTOS scheduler actually run, or is it faked?** It runs. SysTick drives the tick, the Cortex-M4F port handles the context switches, and `vTaskDelay` returns because the kernel moved the task between its lists. A faked scheduler would not produce 21 evenly spaced prints. **Why does the log say it's guessing the vector table offset?** Because the ELF doesn't state it explicitly, so the simulator infers `0x8000000` from the image layout. It's correct for this board, and it tells you it inferred it, which is more than a hanging board would. **Can I trust the simulated memory map?** Not as a substitute for the datasheet. This model provides 256K contiguous SRAM at `0x20000000` and no CCM, which differs from real F407 silicon in both directions. Check allocations against the part you're shipping. --- ## Why UART is still the best debug interface in 2026 (https://veecle.ai/blog/uart-still-wins, 2026-08-05) I have a coding agent that reads a datasheet, writes a peripheral driver, builds it, and runs it on a virtual chip without touching hardware. Very modern. Very 2026. The way it finds out whether any of that worked is a serial port whose design predates the microcontroller. Nobody planned this. The UART debug interface won by outliving everything that was supposed to replace it, and it keeps winning for a reason that has nothing to do with nostalgia. ### A very short history of watching firmware Debug interfaces have gone through roughly four generations. First there were lights. Front-panel LEDs and switches, and the debugging technique was "blink the LED in a pattern I can count." People still do this. It's the only interface guaranteed to work when nothing else does. Then serial. A device to shift bytes out one wire and in on another, so a machine could talk to a teletype. The acronym is older than the chips we run it on. Firmware borrowed it immediately, because a print statement is the cheapest debugging tool ever invented and it worked here too. Then the dedicated debug port. JTAG arrived as an IEEE standard at the start of the nineties, built for boundary scan and quickly repurposed into halt-the-core, read-the-registers debugging. ARM later squeezed it down to two pins as SWD, which is what your probe is actually speaking today. Then trace. SWO, ITM, ETM. Real instrumentation: timestamped events streamed out of the core without stopping it, so you can watch timing instead of guessing at it. Each generation was strictly more capable than the last. And every single one of them shipped next to a UART, not instead of it. ### Why the old one didn't die Five things, and none of them are features. They're all absences. **It needs two wires.** TX, RX, ground. Often you only wire TX, because all you want is for the board to talk. There is no clock line to route, no impedance to worry about, no probe that needs to be within a few inches of the target. **It needs no special tooling.** A three dollar USB-serial adapter, or the virtual COM port your debug probe already exposes. Compare that to the software stack between a trace port and a readable timeline. **The output is human-readable with no decoder.** `printf`, and the string comes out the other end as a string. No symbol table, no map file, no vendor GUI that only runs on Windows. You do not have to reconstruct meaning; the firmware author already wrote it down. **It has no host-side state.** `cat /dev/ttyACM0`. It's a file. Nothing to attach to, nothing to lose sync with, nothing that dies when you unplug and replug. **It works when the rest of the chip doesn't.** Clock tree half configured, watchdog barking, peripherals unenumerated, no debugger connected. One shift register and a baud clock is a very low bar to clear, so it clears early in boot and keeps clearing it while everything downstream is on fire. There's a sixth thing that only became load-bearing recently: text goes into a pipe. An agent can't attach to your trace GUI. It can read a line of ASCII and act on it. When we talk about [the loop breaking at the machine](/blog/the-build-passed-why-doesnt-it-run), the thing that closes it in practice is almost always a serial log. ### Seven boards, one afternoon Here's the claim I get to make that most of what we publish can't: today, UART output was captured on every single board Chiplab supports. Not a representative sample. All seven. Verbatim, the line each board printed: ```text STM32F4 Discovery 08:04:34.9793 [INFO] usart2: [...] Hello world! STM32F7 Discovery 08:09:04.6720 [INFO] usart2: [...] Hello world! STM32F103 Blue Pill 08:07:06.4365 [INFO] usart1: [...] bluepill-hello: $2 board, zero hardware, still boots STM32L073 Nucleo 08:07:06.8859 [INFO] usart2: [...] datasheet-agent: never seen this board before, reading DBGMCU_IDCODE STM32H745 Nucleo 08:09:05.2628 [INFO] usart3: [...] Hello world! nRF52840 DK 08:09:06.3444 [INFO] uart0: [...] Hello world! STM32WBA52 Nucleo 08:09:05.9954 [INFO] usart1: [...] Hello world! ``` And how each run went: | Board | Peripheral | Clean? | | ------------------- | ---------- | ----------------------------------------------------------------------------------------- | | STM32F4 Discovery | `usart2` | Yes, no warnings. FreeRTOS task, repeats periodically | | STM32F7 Discovery | `usart2` | One RCC warning, output unaffected | | STM32F103 Blue Pill | `usart1` | UART clean, GPIO path in the same run was not | | STM32L073 Nucleo | `usart2` | Yes | | STM32H745 Nucleo | `usart3` | Yes, no warnings, dual-core board | | nRF52840 DK | `uart0` | Two PSEL pin-config warnings, output unaffected | | STM32WBA52 Nucleo | `usart1` | Benign RCC/PWR warnings, output unaffected. Embassy async example, prints once per second | ![Table showing UART debug output captured on all seven Chiplab supported boards](/blog/uart-still-wins/image-1.webp) I want to be precise about the two imperfect ones, because "all seven were flawless" would be the more marketable sentence and it isn't true. On the F7, Chiplab logged `[WARNING] rcc: Unhandled read/write from offset 0x90` before the firmware got going. That's a clock-control register bit the model doesn't track. On the nRF52840 there were two warnings about unhandled PSEL bits. On Nordic parts the PSEL registers select which physical pins route to the UART peripheral, and our model didn't track two of the reserved bits. The WBA52, the newest chip in the set, produced a handful of RCC and PWR register warnings. In all three cases the warnings were about registers next to the UART, not the UART. The output came out anyway. The Blue Pill run is the most interesting of the seven for exactly this reason. The UART path worked cleanly, while the GPIO path in that same run hit bit-band addresses Chiplab's model doesn't implement. Same firmware, same run, same simulator: serial fine, pin toggling broken. That story gets its own post in [Blue Pill in 2026](/blog/bluepill-2026). ### Why simulators get UART right first That pattern isn't luck, and it's the actual argument of this post. A UART is one shift register and a baud clock. There's no DMA requirement, no handshaking to negotiate, no descriptor rings, no clock-domain trickery you have to model to get a byte out. You can implement a usable one in an afternoon. The behavior a simulator has to reproduce is "bytes appear in order," and bytes appearing in order is not hard. Now compare that to what's sitting one register block over. A clock tree with a dozen interdependent enables. A pin mux where the reserved bits still do something on this revision. A power controller with modes that change what other peripherals see. That is where our warnings came from, on all three boards. ![Diagram contrasting a simple UART with the complex peripherals around it](/blog/uart-still-wins/image-2.webp) So UART ends up as the lowest common denominator that real silicon and simulators actually agree on. It's the first peripheral implemented, the first one tested, and the one least likely to have drifted since. On a new board, `printf` over serial is the closest thing to a guarantee you get. That cuts both ways, and I'd rather say it here than let you discover it: agreeing on UART is not the same as agreeing on the chip. [What simulation can't catch](/blog/what-simulation-cant-catch) is a list of the places our stack will hand you a plausible number instead of an error. A clean serial log tells you the firmware reached that print statement. It does not tell you the peripheral underneath it behaves like silicon. ### When you actually need more than UART UART is a debug interface. It is not a debugger. Some things it will never do for you. **Real-time trace.** SWO and ITM stream timestamped events out of the core while it keeps running. A `printf` at 115200 baud costs real cycles and shifts the timing of the thing you're measuring. If you're chasing jitter or an interrupt-latency problem, serial logging is part of the problem. **Register-level inspection.** JTAG and SWD let you halt the core and read memory and registers as they are, right now. No breakpoints over UART. No stepping. No "what is in `r3` at this moment." The only state you can see is state the firmware author decided to print, which means you can only debug problems you already anticipated. **High-bandwidth and timing-critical buses.** A logic analyzer on SPI or I2C shows you edges, setup times, and who drove the line low. Serial output shows you what your driver _believed_ happened. Those are different, and when they disagree the logic analyzer is right. **Anything before your UART is initialized.** If the fault happens during clock setup, the interface that reports faults isn't up yet. That's what a debug probe is for. ![Diagram comparing what UART shows you against what SWO, SWD and logic analyzers show you](/blog/uart-still-wins/image-3.webp) None of that is an argument for skipping serial. It's an argument for knowing what layer you're looking at. Serial answers "did it get here and what did it think." The probe answers "what is actually in the machine." Most bugs die to the first question, which is why the first question is worth making trivially cheap on every target. ### The boring conclusion Two wires and a print statement is still the fastest way to find out whether firmware works, on real silicon and in simulation, and now also for an agent that can only act on what it can read. When we needed a first-run sanity check across seven different chips from two vendors, including a part that came out recently and a dollar-store clone of a part from 2007, we didn't debate the approach. We printed a line. Every one of those boards had a different clock tree, a different pin mux, and a different level of model maturity behind it. The UART was the only thing they all agreed on. That agreement is the whole value, and it's why the interface everyone predicted would be retired thirty years ago is the one I'd pick first if you gave me an unfamiliar board and an afternoon. It's also why [going from datasheet to running firmware](/blog/datasheet-to-firmware-one-prompt) starts with a serial banner and not a debugger session. Get the board talking first. Everything else is easier once it does. --- ## EmbedAgent and friends: what the LLM-for-embedded benchmarks actually measure (https://veecle.ai/blog/what-llm-embedded-benchmarks-measure, 2026-08-05) Somebody drops EmbedAgent into a thread about whether LLMs can write firmware. Somebody else drops the CHI paper. Then the argument proceeds as if the two are competing scoreboards and one of them must be wrong. They aren't competing. They aren't even playing the same sport. Short version: EmbedAgent measures whether generated code compiles and passes functional cases across a component and platform matrix. Englhardt et al. measure whether an LLM's reasoning helps a human, including in the runs where the code was broken. The MDPI Future Internet benchmark sits in the same lineage as the second one. All three are worth reading. None of them run the firmware long enough to find the class of bug that keeps firmware people up at night, and that gap is the whole reason "LLMs are at 55% on embedded" is a sentence with no meaning attached to it. ### EmbedBench: 126 cases and a ceiling near 55% EmbedAgent is the most benchmark-shaped of the three.[^1] The paper builds EmbedBench: 126 cases spanning 9 electronic components and 3 hardware platforms, run against 10 mainstream LLMs. The framing is a nice piece of scoping — instead of one blob task, it splits the work into the roles an embedded developer actually switches between: programmer, architect, integrator. Writing the driver, deciding how the parts connect, making the whole thing work on a given platform. The headline number is where it gets interesting. DeepSeek-R1, given the schematic information, lands at 55.6% pass@1. Made to generate the schematic itself first, it drops to 50.0%. The paper describes these cases as simple. Sit with that for a second. Simple cases. Top reasoning model in the set. Coin flip. The delta between the two conditions is the part I didn't expect. Handing the model the wiring is worth 5.6 points. If figuring out how the parts connect were the hard step, removing it should have bought a lot more than that. It suggests the failures are concentrated somewhere after the design decision — in the driver code, the init sequence, the platform glue. Then there's the migration result, which is the number I keep coming back to. Asked to move working code across platforms, the best model hits 73.8% pass@1 on MicroPython for the Raspberry Pi Pico and 29.4% on ESP-IDF. Same task class. Forty-four points apart. That spread is not a statement about hardware difficulty. A Pico and an ESP32 are both small microcontrollers with the same kinds of peripherals. It's a statement about how much of each toolchain exists in public code, and in what shape. MicroPython is short, uniform and everywhere. ESP-IDF is large, versioned, and full of code that only compiles against one specific release. The model is good exactly where the corpus is thick and consistent. That's a corpus benchmark wearing a hardware costume. ![Bar chart comparing best model pass at 1 on MicroPython at 73.8 percent versus ESP-IDF at 29.4 percent in the EmbedBench LLM embedded systems benchmark](/blog/what-llm-embedded-benchmarks-measure/image-1.webp) The paper also splits the failure modes by model type, and this is the observation I'd promote to the abstract if it were mine. General-purpose chat models like DeepSeek-V3 often fail to use relevant knowledge they demonstrably have. Reasoning models do the opposite: they overthink, and in doing so overlook the efficient piece of knowledge that would have solved it. Two different ways to be wrong about the same GPIO. EmbedAgent then proposes two fixes: retrieval-augmented generation, and compiler feedback. With both, DeepSeek-R1 goes from 55.6% to 65.1% pass@1 with schematics, and from 50.0% to 53.1% without. Arduino-to-ESP32 migration goes from 21.4% to 27.8%. I want to be fair about how good that is. Those are real gains and the compiler-feedback half is the part I'd bet on, because it's the one that closes a loop instead of widening a prompt. It's also the whole thesis of this blog arriving in someone else's paper: the model gets better when you let it find out it was wrong. But look at where it lands. Two thirds on simple cases with the schematic handed over. Barely over a quarter on the migration. The remedy works and the ceiling is still low. ### The CHI paper: measuring whether wrong code was still useful Englhardt et al. are asking a different question, and if you read them as a pass-rate paper you will misread them completely.[^2] Their setup is an automated testbench across 450 trials, plus a 15-user pilot study. The finding that matters: even when the LLM failed to produce working code, it generated helpful reasoning about embedded design tasks and specific debugging suggestions that were useful to both novice and expert developers. That is a success criterion with no overlap with pass@1. A trial can be a total failure by EmbedBench's scoring and a clear win by this one. The model wires the sensor to the wrong bus but correctly tells you your I2C address is probably shifted a bit, and the human — who was going to write the code anyway — saves an hour. This is an HCI paper doing HCI things, and the softer shape is the point rather than a weakness. It's measuring the tool as it's actually used in 2026: a developer in the loop, reading suggestions, keeping the good ones. Nobody I know ships an LLM's first draft unread. Measuring the first draft in isolation tells you less about the workflow than measuring what the draft did to the human. ### The MDPI paper: same lineage, newer volume The third one is "Benchmarking Large Language Models for Embedded Systems Programming in Microcontroller-Driven IoT Applications," in MDPI's Future Internet.[^3] It positions itself on the Englhardt line, building on and surveying that 450-trial testbench approach. I'm not going to quote pass rates from it, because the honest state of my reading is that I have the title and the framing and not the result table. What's worth noting is the lineage itself. Two of the three papers here trace back to the same testbench design. When you see three citations in a thread and assume three independent measurements, you're counting one methodology twice. ![Diagram comparing the methodology axes of three LLM embedded systems benchmark papers, functional pass rate versus qualitative usefulness](/blog/what-llm-embedded-benchmarks-measure/image-2.webp) ### Why stacking them up is a category error Put the three side by side and the comparison collapses. EmbedAgent's question is "did the machine produce artifacts that compile and pass a functional check." Englhardt's is "did the machine make a human more effective, even when it was wrong." Those aren't harder and easier versions of one test. A single trial can score 0 on the first and high on the second, and neither number is lying. So when a summary says LLMs are at 55% on embedded, the only correct follow-up is: 55% of what, on which platform, with the schematic given or not, and scored by a compiler or by a person. Every one of those choices moves the number more than swapping the model does. ### The thing none of them measure Here's the part that made me want to write this up, and it applies to all three. None of them run the generated firmware against real interrupt timing. None of them stress peripheral register races. None of them soak a build for days to see what drifts. They are testing "does this compile and pass a functional check," which is necessary and nowhere near sufficient for firmware anyone ships. The bugs that own my calendar don't fail a functional check. They pass it every single time. A missing memory barrier that works until the compiler inlines differently. A DMA buffer read one cycle before the transfer completes, which is fine at room temperature and not fine at 70°C. An interrupt priority inversion that only bites when two peripherals go active in the same millisecond. A counter that wraps on day nine. All of those produce code that compiles, runs, and passes the check on the bench. Which is the recurring point around here: [the code takes minutes, proving it takes months](/blog/llms-write-good-firmware-cant-prove-it). These benchmarks are almost entirely measuring the minutes. That's not a flaw in the papers — it's a flaw in how the results get repeated, because "55% pass@1 on simple cases" gets read as a progress bar toward shipping firmware, and it isn't one. It's a progress bar toward a first draft. ![Diagram of a firmware validation pipeline showing that LLM embedded benchmarks stop after the functional check, before timing and soak testing](/blog/what-llm-embedded-benchmarks-measure/image-3.webp) The same confusion shows up one layer up the stack, which I got into in [Copilot for firmware is solved, and that was the easy half](/blog/copilot-for-firmware-is-solved). Generation is the part that's going fine. Every benchmark here is scoring generation, with a compile step attached, and then people extrapolate from it to the validation problem sitting downstream. The extrapolation is where it goes wrong, not the measurement. ### What I'd want measured If I were extending EmbedBench, I'd keep the role split and the platform matrix and add one axis: how long the firmware ran before it was scored, and under what conditions. Pass at first boot is one number. Pass after ten thousand interrupt cycles is another. Pass with two peripherals contending for the same bus is a third. Those are three different levels of "working," and right now the literature collapses them into one bit. I'd also want the ESP-IDF gap treated as a first-class result rather than a footnote. A forty-four point swing by toolchain, at fixed hardware difficulty, says the useful lever today is making a platform legible to the model, not making the model larger. That should be a paper. ### Not a takedown I'll be clear, because criticism of academic work compresses badly. These are good papers. EmbedAgent's contribution is a real matrix over real components with a real failure-mode analysis, and I'd rather have its 55.6% than another vibes-based thread about whether models "get" embedded. Englhardt et al. asked a question nobody else was asking and got a genuinely useful answer about wrong code still being valuable. The MDPI paper is continuing a line of work that deserves continuing. The complaint is about the citation layer, not the research. Three papers measuring three things get repeated as one score. And the bar all three stop at — compiles, passes a functional check — is exactly where firmware validation starts getting expensive. Somebody should benchmark that half. Until then, when you see a pass rate for LLMs on embedded, ask what happened after the check passed. Usually the answer is: nothing, the trial ended. ### Sources [^1]: "EmbedAgent: Benchmarking Large Language Models in Embedded System Development," arXiv preprint arXiv:2506.11003v3, 2025. https://arxiv.org/html/2506.11003v3 [^2]: Zachary Englhardt et al., "Exploring and Characterizing Large Language Models for Embedded System Development and Debugging," CHI EA '24 (Extended Abstracts of the CHI Conference on Human Factors in Computing Systems), ACM, 2024. https://zachary.englhardt.com/pdfs/llm_embedded.pdf [^3]: "Benchmarking Large Language Models for Embedded Systems Programming in Microcontroller-Driven IoT Applications," Future Internet 18, no. 2 (2026): 94, MDPI. doi:10.3390/fi18020094. https://www.mdpi.com/1999-5903/18/2/94 --- ## Blue Pill in 2026 — the $2 board you can now dev without owning (https://veecle.ai/blog/bluepill-2026, 2026-08-05) The Blue Pill is the board that taught a generation of hobbyists what a real microcontroller felt like. Not a toy, not a learning kit wrapped in plastic — a $2 clone of an ST-branded board, running a genuine 32-bit ARM Cortex-M3, with enough GPIO and UART to build something that actually mattered. It was the gateway drug into embedded systems for anyone who couldn't afford an official Nucleo or Discovery kit, and for a solid decade it more or less was the entry point, full stop. Nobody at ST designed the Blue Pill. It's an unofficial format, copied from the pinout convention of ST's own eval boards closely enough to feel immediately familiar, then stripped down to a bare 2x20 header, a crystal, and a reset button by a rotating cast of manufacturers, mostly in China, since around 2014. That stripping is the entire pitch: real ARM silicon, none of the eval-board padding, for a price that made "I'll just buy ten and see what happens" a completely reasonable sentence to say out loud. It sold in the millions on that pitch, and it's still sold today. Not because it was good — the clone supply chain has a well-earned reputation for relabeled flash and the occasional board that reports the wrong die ID entirely — but because it was cheap enough that nobody cared. Brick one, you've got five more in the drawer. "What's a good first ARM board" had one dominant answer for years, and it wasn't the $30 official kit sitting next to it on the same seller's storefront. None of that nostalgia changes what's actually on the die, though. It's a genuine STM32F103: 64 or 128K of flash depending on the batch, 20K of SRAM, a real Cortex-M3 core, real bit-banded peripheral memory. That last detail is not trivia. It's the entire second half of this post. ![Split illustration contrasting a physical STM32F103 Blue Pill board with a terminal running its STM32F103 Blue Pill simulation output, labeled zero hardware needed](/blog/bluepill-2026/image-1.webp) ### Running the Blue Pill on Chiplab Chiplab has a board model for it: `stm32f103_blue_pill`. Hand it an ELF, get back a run id and a UART transcript — same interface as every other board we support, no board-specific setup on your end. Under the hood, this runs on Renode, the same MIT-licensed simulator behind the rest of our board fleet. I've written before about [why we point people at Renode instead of trying to build our own core from scratch](/blog/renode-alternatives), and the short version holds here too: reimplementing peripheral models for every MCU family on the market is not a fight worth picking when a maintained open-source one already exists and covers most of the ground. What that buys you in practice: an STM32F103 Blue Pill simulation that isn't a stub faking a UART print and calling it a day. It's a real CPU core executing real machine code against a model of real peripheral registers, clock tree included. Whether that model is complete for every peripheral is a separate question — and it's the one the rest of this post is actually about. ### The firmware Bare-metal Rust, no RTOS, using `stm32f1xx-hal` — the crate anyone writing Rust for this chip family reaches for. It brings up PC13 as a push-pull output (PC13 drives the Blue Pill's onboard LED, on the opposite rail from most boards' active-low convention, which trips up everyone the first time they blink it), configures USART1 on PA9/PA10 at 115200 baud, prints a banner, and toggles the LED six times while narrating each toggle over UART: ```rust #![no_main] #![no_std] use cortex_m_rt::entry; use panic_halt as _; use stm32f1xx_hal::{pac, prelude::*, serial::Config}; #[entry] fn main() -> ! { let dp = pac::Peripherals::take().unwrap(); let mut rcc = dp.RCC.constrain(); let mut gpioc = dp.GPIOC.split(&mut rcc); let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh); let mut gpioa = dp.GPIOA.split(&mut rcc); let tx = gpioa.pa9.into_alternate_push_pull(&mut gpioa.crh); let rx = gpioa.pa10; let mut serial = dp.USART1.serial( (tx, rx), Config::default().baudrate(115_200.bps()), &mut rcc, ); // ... prints a banner, toggles PC13 six times, prints each toggle over UART ... } ``` There's nothing exotic in here on purpose. This is the shape every Blue Pill tutorial from the last five years converges on: constrain RCC, split a GPIO port, configure a pin, bring up a UART. Which is exactly why it's the right firmware to run first. If the simulation has an opinion about this code, it has an opinion about a large fraction of everything actually written for this chip. ### Running it today, unedited Board id `stm32f103_blue_pill`, minted today. Here's the stdout, verbatim: ``` 08:07:06.4193 [WARNING] sysbus: [cpu: 0x8000406] WriteDoubleWord to non existing peripheral at 0x42420310, value 0x1. 08:07:06.4195 [WARNING] sysbus: [cpu: 0x800040E] WriteDoubleWord to non existing peripheral at 0x42420190, value 0x1. 08:07:06.4219 [WARNING] gpioPortC: Trying to set the state of the input pin #13 08:07:06.4365 [INFO] usart1: [host: 0.18s (+0.18s)|virt: 0s (+0s)] bluepill-hello: $2 board, zero hardware, still boots 08:07:06.4369 [INFO] usart1: [host: 0.18s (+0.58ms)|virt: 0s (+0s)] toggle 0 -> PC13 on 08:07:06.4372 [INFO] usart1: [host: 0.18s (+0.26ms)|virt: 0s (+0s)] toggle 1 -> PC13 off 08:07:06.4415 [INFO] usart1: [host: 0.18s (+0.25ms)|virt: 0s (+0s)] toggle 5 -> PC13 off 08:07:06.4422 [INFO] usart1: [host: 0.18s (+0.63ms)|virt: 0s (+0s)] done, 6 toggles observed over UART on the classic Blue Pill ``` It boots. The banner prints. All six toggles get logged over UART, in order, and the run reports done by its own bookkeeping. If this were the only thing I read, I'd call it a clean pass and move on to the next board. But there are two warnings sitting above the first UART line, and a third right after them, and they're worth reading instead of scrolling past on the way to the green-looking part. ### The honest finding Both early warnings share a shape: `WriteDoubleWord to non existing peripheral` at addresses `0x42420310` and `0x42420190`. Those addresses aren't garbage — they sit in the `0x4200_0000`–`0x43FF_FFFF` range, which on a real Cortex-M3 is the peripheral bit-band alias. Writing a word to an address in that range doesn't write "a word" at all. The hardware decodes it into a single-bit set or clear on the actual peripheral register the alias maps to, atomically, with no read-modify-write required. That's a real ARM architecture feature, not something Renode invented to be difficult. `stm32f1xx-hal` uses exactly this mechanism, and it's right there in the crate's own source. A small bit-banding helper (`src/bb.rs`) is what the crate reaches for specifically to flip individual clock-enable and reset bits in the RCC peripheral, before it goes on to configure GPIOC, GPIOA, and USART1 — which is precisely the sequence this firmware runs on the way to bringing up the LED pin and the UART. Chiplab's Blue Pill board model doesn't implement that alias address range. The writes land on addresses nothing is mapped to, and Renode logs it honestly instead of quietly pretending the write succeeded. The next line is where it gets interesting: `gpioPortC: Trying to set the state of the input pin #13`. That's Renode's own GPIO port model, a separate subsystem from the sysbus warnings above it, complaining that the firmware is calling something like `set_high()` or `set_low()` on PC13 while the port still considers that pin an input — even though the code explicitly calls `into_push_pull_output` a few lines earlier in the same function. I didn't get to read Renode's internal GPIO model source for this post, so I'll stick to what I can actually support: these two symptoms are likely connected, not confirmed connected. The RCC bit-band writes that vanished are the same writes that should be enabling the clock domain this pin's configuration depends on, and a GPIO port that never saw that clock enable land would explain a port that still thinks pin 13 is an input. That's an inference built from two real log lines, not a root cause traced step by step through Renode's own peripheral model — and it's worth being precise about which of those two things this actually is. ![Diagram showing an STM32 bit-band alias write succeeding on real silicon versus disappearing in Chiplab's Blue Pill simulation, with PC13 left as an input](/blog/bluepill-2026/image-2.webp) What I can say without any hedging at all: UART worked perfectly, start to finish. USART1's configuration path in this HAL doesn't route through the same bit-band shortcut for the bits that matter here, so it's unaffected by whatever's happening to PC13. The banner printed, all six toggle lines printed in order with the delta timestamps ticking along normally, and the run reported done. If your firmware's job today is "talk over UART," this board model has your back. If your firmware's job is "blink an LED and prove it happened," you'd want to know about this before you trust the green-looking output. This is exactly the kind of thing [I keep finding across this batch of posts](/blog/what-simulation-cant-catch): simulators are honest about what they don't model, provided you actually read the log instead of scanning for the word "error" and stopping there. Nothing in this transcript says FAIL. Both problem lines say WARNING and "Trying to," which is Renode telling you plainly that it doesn't know what to do with an address and is doing something reasonable — log it and move on — instead of crashing or, worse, silently succeeding. ### Compare that to the F4 I ran [the STM32F4 Discovery through the same kind of exercise](/blog/hello-f4-no-hardware) — FreeRTOS, USART2, no hardware — and that transcript had zero warnings. Not "warnings I chose to leave out of the post." Zero, top to bottom. The Blue Pill is not Chiplab's best-supported board, and I'm not going to pretend otherwise just because it's the one with the nostalgia angle this week. The F4 Discovery is the newer, more heavily exercised part in our own examples, and its Cortex-M4 board model has had more attention put into it than the F103's Cortex-M3 model has. The gap between those two runs is the honest state of things right now: less-travelled boards surface more corner cases, and the STM32F103 Blue Pill simulation, while genuinely useful today, is one of the boards where that shows up in the log if you go looking. That's not a knock on the platform. It's the platform working as intended — telling you where the coverage is thinner instead of hiding it behind a passing exit code. One sentence on Embassy, and no more than one: Chiplab also supports an Embassy-async firmware target for this board. I didn't run that variant today. Everything above is the bare-metal run, and I'm not going to describe output for a firmware image I don't have a transcript for. ### What actually happened here A ten-year-old clone board, running firmware built with a HAL crate that was never written with Chiplab in mind, hit a real Cortex-M3 architecture feature that this particular board model doesn't implement — and the simulator told me about it in plain English instead of lying about it. That's not a knock against bit-banding, which is doing exactly what it's supposed to do on real silicon. It's not a knock against `stm32f1xx-hal` either, which is using a real chip feature the way it's meant to be used. It's a gap in one board model's peripheral coverage, logged instead of hidden, on a $2 board you no longer need to own to go find it on. The LED bug is real and worth fixing on our end. The UART transcript is real and worth trusting today. Both of those things are true at once, and the log is the only reason I know which parts are which. --- ## Teach, don't market: our content strategy in public (https://veecle.ai/blog/teach-dont-market, 2026-08-05) Someone asked me last week why this blog reads like a lab notebook instead of a landing page. Fair question. Here's the real answer instead of the one that sounds good in a pitch deck. "Teach, don't market" isn't a value we hung on a wall somewhere. It's a small set of editorial rules that decide what we're allowed to publish, and they've already cost us posts we could otherwise have shipped this month. This is a post about a developer content marketing strategy, which is a funny thing to write a post about, because the strategy's whole point is that it doesn't read like marketing. Let me show you the mechanism instead of just claiming that. ### What "teach, don't market" means, operationally Say the phrase out loud and it sounds like a fortune cookie. Strip the slogan off it and what's left is a set of constraints that make certain posts impossible to write, or at least impossible to write fast. We already argued the biggest one in public. [Why we won't write "Chiplab vs" pages](/blog/why-we-wont-write-chiplab-vs-pages) makes the case that comparison pages are the best-converting content type in SaaS, and we're still not writing them, because there's no incumbent search volume in "MCU simulation for coding agents" to intercept yet. Nobody types "Chiplab vs" anything. Writing that content now wouldn't be marketing, it'd be talking to an empty room and calling it strategy. Before we'd let ourselves write comparison content at all, we ran an audit on what already ranks for the queries a firmware engineer would actually type. [That audit](/blog/embedded-simulator-comparison-audit) found the top-ranked embedded simulator comparison on the internet listing Renode's license as Apache 2.0. It's MIT. One word, in a file named LICENSE, at the root of the repo. Whoever wrote that table didn't open the project. We didn't earn the right to publish comparison content by declaring ourselves trustworthy. We earned it by going and checking what the competition actually got wrong, in public, before writing a single row of our own. That's the operational part. Not "be honest" as a stance, but a checklist that has to pass before a post ships: is there a real audience for this yet, did we check the thing we're about to contradict, and are we citing the primary source or somebody's summary of it. ![Diagram comparing marketing content types against teaching content types in a developer content marketing strategy](/blog/teach-dont-market/image-1.webp) ### The rules, plainly Four of them, and none require a philosophy degree to check: **Cite primary sources, not secondary aggregators.** If a claim rests on a license, a spec, a paper, or a benchmark, we go read that thing, not a blog post summarizing it. The comparison audit above exists because the top search result for "embedded simulator comparison" clearly didn't do this. Copying a wrong number is faster than checking it. It's also how a wrong number gets a second life. **Run real Chiplab instances rather than describe hypothetical behavior.** If a post says firmware booted, ran to completion, or printed specific output, that output came out of a real run against a real target, not a plausible paragraph about what would probably happen. This is the single most expensive rule on the list, and I'll get to why in a minute. **Admit when the tool has a limitation instead of hiding it.** Somewhere in this same batch of posts, [the datasheet-to-firmware writeup](/blog/datasheet-to-firmware-one-prompt) hits a real limitation mid-experiment and reports it instead of quietly editing the post to route around it. That's not generosity toward the reader. It's the only version of the post that survives someone else trying to reproduce it. **Don't force product mentions into posts that are fundamentally research or commentary.** Most of what we publish doesn't need Chiplab's name in it to be worth reading. When a post is about how Renode and QEMU each model hardware, or why HIL doesn't scale for AI-generated firmware, the product shows up when it's the tool that actually did the work, not because someone on the marketing side asked for a mention count. None of these rules are about being a nicer company. They're about what's allowed to get past "is this true" before it gets to "is this useful." ### Why this is also just good SEO, and not altruism I want to be direct about the self-interest here, because pretending there isn't any is its own kind of dishonesty. Developer content has a verification step baked into it that most B2B content doesn't: developers run the code, check the datasheet, or open the repo. That audience trait is exactly why the license mistake in the comparison audit mattered as much as it did. One wrong cell in a feature table isn't a rounding error to a firmware engineer, it's proof nobody on the writing side opened the thing they were writing about. And once a reader catches one wrong fact, they don't discount just that fact. They discount everything else on the page, including the parts that were right. That asymmetry is the whole argument. A missing feature costs you a maybe. A wrong fact costs you the reader's willingness to believe the next five true things you say. Given that trade, checking the primary source isn't a virtue, it's the cheaper mistake to avoid. This batch you're reading a post from is itself the evidence, not just the claim. Ten posts, each one anchored to either a fresh Chiplab run or a checked primary-source citation instead of a plausible-sounding paragraph, written against a shared brief that rules out invented benchmarks and vague claims before a single word gets published. That's not a promise about our intentions. It's a description of how this specific set of posts, including this one, got made. ### What it costs Here's the part that doesn't make it into most "we value honesty" posts: this is slower, and it's slower in ways that show up on a calendar, not just in principle. Running a real Chiplab instance against a real board target takes longer than writing "Chiplab handles this" and moving on. Reading a paper closely enough to cite its actual finding, instead of the finding you assumed it had, takes longer than pattern-matching on the abstract. Opening a competitor's repository to check a license field takes thirty seconds, which sounds trivial until you're doing it across every comparison claim in a post and the thirty seconds multiplies by twenty. Multiply that across a ten-post batch and the difference stops being rounding error. A post written from vibes can ship the same afternoon it's drafted. A post anchored to a real run has to wait for the run, then get checked against what the run actually produced, not what the outline assumed it would produce. That gap is the tax. We're paying it on purpose, but it's still a real cost, and I'd rather say that out loud than pretend the rules are free. ![Checklist diagram showing the four editorial gates a developer content marketing strategy post must pass before publishing](/blog/teach-dont-market/image-2.webp) ### Still marketing, just refusing to lie to get there I'm not going to pretend this is something other than marketing, because it obviously isn't. You are reading this because we want you to trust Chiplab enough to try it, and every post in this batch is, underneath the citations and the run outputs, aimed at that same outcome. Calling it "teaching" doesn't change who's paying for the writing or why it exists. What it changes is the method. We don't get to invent a benchmark to make a point land better. We don't get to describe a run we didn't actually make. We don't get to leave a competitor's license field wrong because checking it is thirty seconds we'd rather not spend. The audience is still deciding whether to trust us. The rules just take away the option of lying to speed that decision up. And we will get things wrong sometimes, because every editorial process run by humans and agents checking each other's work eventually lets something through. When that happens, the fix is a correction, not a rewrite of history. A blog that never admits a mistake either hasn't made one yet, which is unlikely at this volume, or isn't telling you when it does. We'd rather be the kind that says so. If you came here looking for a reason to believe the last nine posts weren't just well-formatted guessing, this is it: the rules are the same ones that produced the post that questions Renode's own foundation, the post that calls out a competitor's factual error by name, and the one that admits our own tool's limitation in the middle of an experiment we ran specifically to find it. That's the strategy. Everything else is execution. --- ## "The code takes minutes. Proving it takes months." — anatomy of a firmware validation cycle (https://veecle.ai/blog/anatomy-of-a-firmware-validation-cycle, 2026-08-05) I repeat this line often enough that people nod at it without making me prove it: the code takes minutes, proving it takes months. So here is the proof, stage by stage. The short version, if you're only here for one paragraph: in a firmware validation cycle, every stage that involves typing takes minutes to hours, and every stage that involves physical hardware takes days to months. There is almost nothing in between. The gap isn't caused by firmware engineers being slow. It's caused by the number of boards in the lab being a fixed, small integer. ### The cycle we inherited Embedded didn't arrive at this by accident. Microgenesis, writing about CI/CD for embedded systems, describes the starting point plainly: development "often followed a waterfall model — long design phases, months of coding, and extensive manual testing before release."[^1] The consequences they list are the ones every firmware team recognizes: "slow time-to-market, late detection of defects, and painful certification cycles." Their argument is that two forces are now breaking that model. One is compliance: ISO 26262 in automotive, IEC 62304 in medical. The other is that customers expect over-the-air updates, which means the release you shipped is not the last release you ship. Both of those turn a slow validation cycle from an annoyance into a structural problem. I agree with all of it. I want to zoom in on the part their piece treats as a single block and take it apart, because "extensive manual testing" is doing an enormous amount of work in that sentence. ### Anatomy of a firmware validation cycle Here's a realistic cycle for a nontrivial change. Say you're fixing a bug in a CAN driver's error-frame handling on an automotive ECU. Nothing exotic. The kind of ticket that closes every week somewhere. #### Write the code — minutes to hours You read the datasheet section, you understand the error counter behavior, you change maybe forty lines. If you're pairing with an agent, this part is faster than that, and getting faster every month. This stage is not the problem. This stage has never been the problem. #### Compile and unit checks — minutes Cross-compile, run whatever host-side unit tests exist, check the map file didn't blow past your flash budget. Static analysis if your build runs it locally. Fast. Fully automated. Runs on any machine. Also, notably, tells you almost nothing about whether the fix works, because a CAN error counter's behavior is a property of a peripheral you just compiled against, not a property of the code. #### Board bring-up and flash — hours, if a board is free Now you need a target. This is where the cycle stops being about software. You need a board of the right revision, a JTAG or SWD probe attached to it, a power supply, a CAN transceiver on the other end, and a machine with the right toolchain and probe drivers talking to all of it. In practice, that combination doesn't sit idle waiting for you. It's on someone's desk, or it's in the rack running a different test, or the probe walked off with whoever needed it last. The flashing takes seconds. Getting to the point where you can flash commonly takes the better part of a day. I've watched engineers schedule their week around who has the debug probe. #### Integration testing on real hardware — days Your fix touches the CAN driver, so now you care about the rest of the system. Does the scheduler still meet its deadlines with the new error path? Does the bootloader still hand off cleanly? Does anything else on the bus notice? This runs on shared hardware, in a lab, against other people's schedules. Every iteration costs a round trip to the bench. Every failure sends you back to read a register dump over a serial line at whatever baud rate someone picked in 2019. Days, and most of that is queueing, not testing. #### HIL rig testing for timing and fault cases — days to weeks The interesting failures don't show up in nominal operation. They show up when the bus floods, when a node drops mid-transmission, when a supply browns out during an interrupt. That's what the hardware-in-the-loop rig is for: real firmware, real MCU, simulated plant and simulated faults around it. There is one rig. Maybe three, if you're at a large OEM. It's booked. It's booked because every team needs it, and because the setups it runs are physical wiring harnesses that take real effort to swap. Getting a fault-injection slot on a HIL rig is a calendar negotiation, not a command you type. This is where weeks start appearing, and it's the single least elastic stage in the whole cycle. #### Regression across hardware revisions — weeks Your product doesn't ship on one board. It ships on rev B, rev C, and the pre-production rev D that only exists in quantities of four. Silicon errata differ between them. Clock trees differ. Sometimes a peripheral you depend on got moved. So the regression suite is the matrix of every test you care about times every revision you support, and it runs on physical units of which you own a countable number. When a board revision changes mid-cycle — and in hardware development it does, because the hardware team has its own deadlines — you don't extend the matrix, you re-run it. Weeks, and the ones you didn't plan for. #### Compliance and certification paperwork — months If you're in a regulated industry, this is the stage that dwarfs everything above it. ISO 26262 and IEC 62304 don't just want working firmware. They want evidence: requirements traced to design, design traced to tests, tests traced to results, coverage reports, static analysis findings dispositioned one by one with a human signature, tool qualification arguments for the compiler and the test harness. Documented, reviewed, archived. None of this is fake work. I'd genuinely rather my brake controller went through it. But it's measured in months, it's largely manual, and it's the reason Microgenesis calls certification cycles "painful" without needing to elaborate. #### Field validation and release-candidate soak — weeks Last stage. You put the candidate on real units in something resembling real conditions and you leave it there, because a class of firmware bugs only appears after three hundred hours: a counter that wraps, a heap that fragments, a watchdog that fires once a fortnight. You cannot compress a soak test by working harder. You can only start it earlier. ![Timeline diagram of a firmware validation cycle showing hardware-bound stages consuming days to months](/blog/anatomy-of-a-firmware-validation-cycle/image-1.webp) ### Add up the bands Two stages measured in minutes and hours. Six measured in days, weeks, and months. And look at what separates them. It isn't difficulty. Writing correct interrupt-priority code is harder than running a regression matrix. It's whether the stage needs a physical object that exists in limited quantity: a board, a probe, a rig, a reviewer's signature. That's the whole shape of it. The fast stages scale with how many engineers you have. The slow stages scale with how many boards you have. Hiring doesn't fix the second number, and neither does anyone's productivity tooling. ### CI/CD is real, and it optimizes the fast part I don't want this read as a swipe at embedded CI/CD. Microgenesis is right about the direction, and the benefits they list are the correct benefits: "accelerated feedback loops" and "improved software quality," achieved by automating builds and tests on simulators so developers get "immediate feedback" instead of waiting on hardware prototypes or manual QA.[^1] Every firmware team should be doing this. Many still aren't. But watch which stages a typical embedded pipeline actually covers. It builds. It runs unit tests. It runs static analysis. It maybe boots a smoke test somewhere. In other words, it takes the two stages that already took minutes and makes them take fewer minutes, on a schedule, without a human remembering to press the button. That's worth having. It is not where the months are. I've written the longer version of this argument in [firmware CI ends at the linker](/blog/firmware-ci-ends-at-the-linker): the pipeline proves the artifact was produced, then hands it to a human to find out whether it works. Everything after the linker — the six slow stages above — stays gated behind physical rigs and physical boards, even in teams with a beautiful green pipeline. Automating the fast half of a cycle whose time lives in the slow half improves the developer experience and barely moves the release date. ![Chart showing embedded CI/CD shortening build and unit test time while hardware-bound validation stages stay unchanged](/blog/anatomy-of-a-firmware-validation-cycle/image-2.webp) ### What actually shrinks the slow stages If the constraint is "this stage needs a physical object that exists in limited quantity," there are exactly two ways out. Buy more physical objects, or stop needing them for some of the work. The first one is what teams do today, and it doesn't scale: boards cost money, rigs cost more, and pre-production silicon can't be bought at all because there are twelve units in the world and they're on a shelf in a different country. The second one is simulation, and the reason it matters isn't that it's cheaper. It's that a simulated target scales horizontally. Fifty parallel instances of a virtual MCU is a scheduling decision. Fifty parallel instances of a physical board is a purchase order, a rack, and someone to wire it. That changes which stages can move earlier: - Peripheral-register-level tests. Does the CAN error counter increment the way the reference manual says? That's a question about registers, and a simulator that models the peripheral can answer it before a board is involved. - Interrupt-timing and priority behavior. Does the new error path still let the high-priority ISR preempt? Deterministic on a simulator, and repeatable, which is more than you can say for a bench. - Fault injection. Bus errors, unexpected resets, corrupted frames. On a rig this is a wiring change and a booked slot. On a simulator it's a line of test setup. - The revision matrix. Different memory maps and different peripheral configurations are configuration files, not four more boards you don't own. Simulation does not replace the HIL rig. I've said this often enough that I wrote the routing procedure down: [simulation vs HIL, what belongs in CI](/blog/simulation-vs-hil-what-belongs-in-ci) is the decision rule for which checks move earlier and which ones have to stay on real silicon. Analog behavior, real timing under real electrical conditions, anything where the plant model is the point — that stays on the rig. The win isn't deleting the slow stages. It's arriving at them with the boring failures already found. A HIL slot spent discovering that you mishandled an error counter is a wasted HIL slot. There's one rig. Don't send it your typos. ![Diagram comparing a fixed pool of physical boards with horizontally scalable simulated targets](/blog/anatomy-of-a-firmware-validation-cycle/image-3.webp) ### What this does not fix Two honest limits, because a post that claims to delete the months would be lying. Compliance paperwork is still paperwork. Traceability matrices, static analysis dispositions, tool qualification, review signatures. Simulation can produce more test evidence earlier and produce it repeatably, which is not nothing when a report has to cite results. But the process is human, it's regulated, and nobody at Veecle has eliminated a certification cycle. Anyone telling you otherwise is selling. And the soak test is still a soak test. Three hundred hours is three hundred hours. The only lever is starting it on a candidate that's already survived everything cheaper. ### The part I actually care about I mostly write about coding agents, so here's the connection. An agent writing firmware is fine at the minutes-long stages. It's fine at the code, and increasingly good at it. Then it hits the first stage that needs a board and it stops having a feedback loop at all. It can't queue for the rig. It can't ask who has the probe. It generates a patch, and the loop that would tell it whether the patch was right is a physical process measured in weeks. That's the same wall the human team hits, just more visibly, because a human will at least wait patiently and an agent will confidently hand you something unverified. Which is why I keep coming back to the same sentence. The code takes minutes. Proving it takes months. And every hour of that gap that lives in "no board was free" is an hour we put there ourselves. ### Sources [^1]: Chandra Narayanan, "CI/CD for Embedded Systems: Tools, Pipelines, and Strategies," Microgenesis, Oct 23, 2025. https://mgtechsoft.se/blogs/ci-cd-for-embedded-systems-tools-pipelines-and-strategies --- ## What LLMs get wrong about embedded code (https://veecle.ai/blog/what-llms-get-wrong, 2026-08-04) Our [last post](/blog/why-your-agent-needs-a-target) made a claim: language models hallucinate register names and phantom HAL APIs with the same confidence they use for real ones, and a clean build won't catch either. We wrote that from experience, not from data. This post is the data. We prompted two models, one big and one small, for hello-world firmware on three boards: STM32F4 Discovery, STM32L073 Nucleo, nRF52840 DK. No docs and no tools of any kind, not even a datasheet, and only one attempt each. Reply with `main.rs`, nothing else. The agent compiled every answer exactly as returned, byte for byte, and ran every binary that built on our simulated boards. To be clear about what this is: no real agent works this way. Agents read the docs, they grep around in the crates they depend on, and they try again when something breaks. Cold-prompting measures what's actually in the weights, which is exactly what you're trusting when a model writes register code and nothing checks it. ### Layer one: what the compiler catches We asked both models for the same thing twice: once with raw register writes, once using `stm32f4xx-hal` 0.23. Both HAL attempts failed to compile. Not on typos. On API drift: ``` error[E0616]: field `cfgr` of struct `stm32f4xx_hal::pac::rcc::RegisterBlock` is private error[E0061]: this method takes 1 argument but 0 arguments were supplied ``` Both models wrote the `rcc.constrain()` / `cfgr.freeze()` API that the crate dropped two major versions ago. This is the boring failure class, and it's fully mechanical. The model's knowledge of a moving API is frozen at training time, the compiler catches the drift, and an agent with the error output in front of it fixes the whole thing in one round. If your loop includes `cargo build`, this class is already handled. The raw-register attempts are where it gets interesting, because there the compiler has no opinion. An address is an address. All six raw-register attempts, across both models, compiled clean on the first try. ### Layer two: what the run catches The small model's nRF52840 attempt is worth walking through, because it failed the way firmware actually fails: silently, one plausible decision at a time. Its first attempt used the nRF's legacy byte-at-a-time UART interface, deprecated for a decade but heavily represented in old tutorials. The run produced nothing. Told that, it switched to the modern UARTE peripheral, which moves data with EasyDMA. Its second attempt died in the compiler, because Rust's rules forbid the `static mut` buffers it used. Its third attempt is the one that matters. To satisfy the borrow rules it made the DMA buffer an immutable static, which the linker places in flash, then copied the message into it at runtime through a const-cast pointer: ```rust static TX_BUFFER: [u8; 64] = [0; 64]; // ... let tx_ptr = TX_BUFFER.as_ptr() as *mut u8; ptr::write(tx_ptr.add(i), byte); // ... ptr::write_volatile(UARTE_TXD_PTR as *mut u32, TX_BUFFER.as_ptr() as u32); ``` This compiles without a single warning that matters, and it reviews fine if you're skimming. It also can never work: the writes into flash are silently dropped, so the DMA streams out whatever the buffer held at link time, which is zeros. The run said so immediately: ``` 18:55:22.6729 [INFO] machine-0: Machine started. 18:55:27.6771 [INFO] machine-0: Machine paused. ``` Five seconds of virtual CPU time, zero bytes on the UART. No fault, no error, no output. That is the exact bug class the target post described in the abstract: a DMA buffer the peripheral can't reach. Here it was produced organically, by a model under iteration pressure. Invisible to the compiler, invisible to casual review, and unambiguous the moment the firmware actually ran. ### Layer three: the bench, once Before trusting any of this, we probed what the simulation actually models. We took a known-good STM32F4 hello world and sabotaged it two ways on purpose: one variant with the USART clock enable removed, one with the baud divisor programmed for 9600 instead of 115200. Both printed `Hello world!` anyway. The UART model in the simulator is byte-level. It hands over bytes when the firmware writes them, and it does not model clock gating, baud mismatch or pin multiplexing. Which means some of the small model's bugs sailed straight through. Its STM32F4 attempt divides the baud clock by 16 twice, with a comment proudly showing the math, and routes the TX pin's alternate function to the wrong pin's bits. It prints fine in simulation. On your desk it would produce garbage at 1.8 Mbaud on a dead pin. So the bench isn't going anywhere, and we're not going to pretend otherwise. Analog and configuration detail below the byte level, meaning exact baud, pin routing and clock trees under load, gets its final word from real silicon. But look at what's left for the bench to do. Every failure in this post except those two was caught earlier, in seconds, without a board: the stale APIs by the compiler, then the wrong peripheral mode and the unreachable DMA buffer by the run, none of it needing a human. The whole experiment, fourteen runs across three chips with a three-iteration debug loop, took a few minutes and zero hardware. Do that loop by flashing a physical board every iteration and you're queueing for dev kits and reading a logic debugger. That's the actual division of labour. The cheap layers eat the iteration, and silicon does what only silicon can: final verification of a binary that is already structurally right. ### The gap nobody talks about The big model's register-level recall was flawless on every chip we tried: correct fractional baud encoding on the F4, the L0's renamed clock-enable register, and the UARTE's RAM-only DMA restriction, which it flagged unprompted, in a comment, while copying its buffer to the stack. The small model produced something plausible-but-wrong on every single target. Never nonsense. It would be one register off, or it had misremembered an encoding, or it reached for a peripheral mode that was deprecated. That is the dangerous kind of wrong, because everything downstream of it (the build, the review, the vibe) says ship it. ### What this adds up to Three layers, each catching what the previous one can't, ordered by how cheap they are to run: - The compiler catches frozen knowledge in seconds: stale APIs, missing dependencies, edition rules from after the training cutoff. - The run catches structural lies in under a minute. A peripheral that was never enabled the right way, a buffer the DMA can't read, or code that sits there waiting for an event that will never fire. - Silicon signs off at the end, once, on a binary the first two layers already debugged. Everything above the sign-off runs without a single physical board on a desk. That was the whole argument of the target post, and after this experiment we believe it slightly more than when we wrote it. - **Repo & examples:** [github.com/veecle/chiplab](https://github.com/veecle/chiplab) - **Chiplab:** [veecle.ai/chiplab](https://veecle.ai/chiplab) - **Discord:** [discord.com/invite/F6GwZJ6ktP](https://discord.com/invite/F6GwZJ6ktP) --- ## We ran the same STM32 firmware on Renode vs QEMU: here's what diverged (https://veecle.ai/blog/renode-vs-qemu-same-firmware, 2026-08-04) Same ELF. Same firmware, byte for byte. Two simulators, both claiming to emulate an STM32F4. One boots and prints "Hello world!" like nothing happened. The other dies before it gets anywhere near my code, with a fatal error I'd never seen before and a full register dump. Neither one is lying. ### The firmware Nothing exotic. `examples/bare-metal/stm32f4-discovery` from the [Chiplab repo](https://github.com/veecle/chiplab), unmodified. Same file I used to [plant a real crash and watch nothing happen](/blog/an-agent-fixed-a-hardfault). Rust, `no_std`, `no_main`, one job: bring up USART2 on PA2/PA3 at 115,200 baud and print "Hello world!" once, then loop forever. 22,680 bytes, `thumbv7m-none-eabi` release build. ```rust //! Hello world over USART2 (PA2/PA3) on the STM32F4 Discovery. #![no_main] #![no_std] use cortex_m_rt::entry; use panic_halt as _; use stm32f4xx_hal::{pac, prelude::*, rcc::Config, serial::Serial}; #[entry] fn main() -> ! { let dp = pac::Peripherals::take().unwrap(); let mut rcc = dp.RCC.freeze(Config::hsi().sysclk(16.MHz())); let gpioa = dp.GPIOA.split(&mut rcc); let tx = gpioa.pa2; let rx = gpioa.pa3; let serial = Serial::new( dp.USART2, (tx, rx), stm32f4xx_hal::serial::Config::default().baudrate(115_200.bps()), &mut rcc, ) .unwrap(); let (mut tx, _) = serial.split(); let msg = b"Hello world!\n"; for &byte in msg { let _ = nb::block!(tx.write(byte)); } loop {} } ``` No bugs planted this time. I wanted the boring case: does the same binary behave the same way on two different simulators of "the same" chip? Short answer: no. ### Run 1: Chiplab, Renode 1.16.1 underneath Board: `stm32f4_discovery`, the exact machine this firmware targets. I uploaded the ELF, ran it, and got exactly what the code is supposed to do. ``` 10:10:45.0412 [INFO] machine-0: Machine started. 10:10:45.0499 [WARNING] flash_controller: Unhandled write to offset 0x0. Unhandled bits: [8-10] when writing value 0x700. Tags: PRFTEN (0x1), ICEN (0x1), DCEN (0x1). 10:10:45.0982 [INFO] usart2: [host: 0.2s (+0.2s)|virt: 0s (+0s)] Hello world! 10:10:50.0412 [INFO] machine-0: Machine paused. ``` Boots. Warns about a flash-controller register it doesn't fully model—the HAL sets PRFTEN, ICEN, and DCEN, prefetch and cache enable bits, during clock setup. Prints. Idles for the rest of its 5-second run budget. Chiplab runs on Renode under the hood, the same emulator [Interrupt has written about for exactly this kind of firmware-without-hardware workflow](https://interrupt.memfault.com/blog/intro-to-renode).[^1] Peripheral behavior, including which registers exist and which bits Renode doesn't know what to do with, comes from `.repl` platform description files.[^2] Notice what it did here: it didn't silently swallow the write, and it didn't crash. It logged a WARNING naming the exact unhandled bits and kept executing. That's a real design choice, and for most of what people use Renode for, running firmware far enough to see the behavior you actually care about, it's the right one. ### Run 2: QEMU 11.0.3, and there's no such board QEMU doesn't ship an `stm32f4-discovery` machine.[^3] The closest F4-family machine is `netduinoplus2`, which models an STM32F405, not the F407 this board and firmware target.[^4] That's a different peripheral map—exactly the gap covered in [what Renode and QEMU each actually model](/blog/renode-vs-qemu-what-each-models): QEMU hardcodes its boards, Renode lets you edit the platform description. I ran the same ELF on netduinoplus2 anyway. ``` qemu-system-arm -M netduinoplus2 -kernel hello-stm32f4-discovery -nographic -serial mon:stdio ``` ``` qemu: fatal: Lockup: can't escalate 3 to HardFault (current priority -1) R00=00000000 R01=00000000 R02=00000000 R03=00000000 R04=00000000 R05=00000000 R06=00000000 R07=00000000 R08=00000000 R09=00000000 R10=00000000 R11=00000000 R12=00000000 R13=2002ffe0 R14=fffffff9 R15=08001d6c XPSR=41000003 -Z-- T handler ``` (Register dump continues with zeroed FPU registers, trimmed here.) The exact line is `Lockup: can't escalate 3 to HardFault`, QEMU's way of saying it hit a fault it can't escalate and decided that's fatal. I ran it twice to rule out a fluke. Both runs produced byte-identical output, md5-matched. That dump does tell us something, even without a debugger attached. R14 is 0xFFFFFFF9, an EXC_RETURN value, which means the core was already inside an exception handler when this happened. The XPSR handler bit is set. R15, the program counter, is 0x08001d6c, which sits inside the firmware's own flash, not some QEMU internal void. So this isn't QEMU choking on a malformed ELF or a broken vector table. It's a fault occurring while a fault is already being handled, with nowhere left to escalate to, during early init. I didn't bisect this. There's no single-stepped trace pointing at a guilty line. What I have is a register dump consistent with running an F407 binary's early boot sequence—RCC and GPIO and USART setup—on a machine modeling an F405 with a different peripheral map. That's a plausible story, not a proven one. ![Same firmware, two terminals](/blog/renode-vs-qemu-same-firmware/image-1.webp) ### Renode vs QEMU: what actually diverged | | Chiplab (Renode 1.16.1) | QEMU 11.0.3 | | --------------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------- | | Machine for this board | `stm32f4_discovery`, exact match | No such machine exists; closest is `netduinoplus2` (STM32F405, not F407) | | Boot result | Boots, runs to the time budget | Fatal lockup during early exception handling | | UART output | `Hello world!` printed once | None | | Behavior at an unmodeled register | Logs a WARNING, keeps running | N/A here, but its peripheral models are their own, separate set from Renode's | | Fault philosophy | Faults aren't escalated on this board at all[^5] | An unescalatable fault is fatal: halt and dump every register | | What "success" means | Firmware ran far enough to observe real behavior | Either the firmware runs clean, or the emulator gives up loudly | Read that fault-philosophy row against [the post where I tried to force a HardFault](/blog/an-agent-fixed-a-hardfault). I planted four different, real bugs on this exact board and firmware, on Chiplab, specifically to trigger a HardFault: a raw write into flash, an invalid branch, an undefined instruction, an unaligned read. Zero HardFaults fired, every time. The chip that would not die. Now here's QEMU, on a different machine modeling a related chip, hitting a fault situation nobody even provoked on purpose, and refusing to execute another instruction. Same silicon family. Opposite instincts. Renode's stance, on this board, today, is "if I don't understand the fault, I don't stop you." QEMU's stance is "if a fault can't be delivered, that's it, we're done, here's every register." Neither one is the STM32F407 sitting on my desk. Both are telling the truth about what they themselves do at the edge of their own model. ![Same ELF, two personalities](/blog/renode-vs-qemu-same-firmware/image-2.webp) ### The part that's actually useful I ran this to see what "the same firmware behaves differently on two simulators" looks like in practice. The answer isn't noise—it's information. If Renode and QEMU had agreed—boot clean, print the banner, idle—that agreement would have been a weak signal that early init doesn't touch anything either simulator models shakily. Two simulators converging on the easy path doesn't prove the hard path is fine. The disagreement is worth more. It's a map. It tells me exactly where the two models diverge: Renode doesn't escalate CPU faults on this board, QEMU escalates faults hard and unconditionally, and QEMU doesn't even have the right board to start from. None of that tells me what my actual STM32F407 will do. That's still a job for real hardware, the same case I make in [why HIL testing won't scale to the AI era on its own](/blog/hil-wont-scale-ai-era). What the simulators do tell me is which questions I can't trust either one to answer yet, and it's not a coincidence that other embedded engineers have raised exactly this tradeoff when weighing Renode against QEMU for MCU-level work.[^6] That's the actual lesson: know your STM32 simulator's failure philosophy before you trust its silence or its crash. A green checkmark from a tool that never escalates faults and a red X from a tool that escalates too eagerly on the wrong board are both telling you less about your firmware than they appear to. The two runs disagreeing told me more than either run alone. ### Can QEMU emulate an STM32F4 Discovery board? Not directly. QEMU ships no `stm32f4-discovery` machine model. The closest is `netduinoplus2`, which emulates an STM32F405, a related but different chip from the STM32F407 this board actually carries. If your firmware only touches USART, GPIO, and RCC the way this one does, running it on `netduinoplus2` might get you close. It won't get you a faithful STM32 simulator of this exact board, and this post is what that gap looks like when you actually run the binary. - **Repo & examples:** [github.com/veecle/chiplab](https://github.com/veecle/chiplab) - **Discord:** [discord.com/invite/F6GwZJ6ktP](https://discord.com/invite/F6GwZJ6ktP) - **Chiplab:** [veecle.ai/chiplab](https://veecle.ai/chiplab) ### Sources [^1]: Baldassari, François. "Cortex-M MCU Emulation with Renode." Interrupt by Memfault, March 23, 2020. https://interrupt.memfault.com/blog/intro-to-renode [^2]: "Platform Description Format." Renode documentation. https://renode.readthedocs.io/en/latest/advanced/platform_description_format.html [^3]: "QEMU System Emulation for ARM." QEMU documentation. https://www.qemu.org/docs/master/system/target-arm.html [^4]: "STM32 boards (netduino2, netduinoplus2, olimex-stm32-h405)." QEMU documentation. https://www.qemu.org/docs/master/system/arm/stm32.html [^5]: "An agent tried to fix a HardFault. There was no HardFault to fix." Veecle. [/blog/an-agent-fixed-a-hardfault](/blog/an-agent-fixed-a-hardfault) [^6]: Eplankton. "Idea: Introduce Renode as an alternative to QEMU." vivoblueos/kernel, GitHub issue #13, August 1, 2025. https://github.com/vivoblueos/kernel/issues/13 --- ## Renode vs QEMU for firmware development: what each actually models (https://veecle.ai/blog/renode-vs-qemu-what-each-models, 2026-08-04) You can run the same firmware binary on Renode and QEMU and get different results. Not because one is broken. Because they're simulating different machines. I've already [run the exact same STM32 firmware on both and watched the results diverge](/blog/renode-vs-qemu-same-firmware). This is the other half: a Renode vs QEMU comparison of what each tool actually models, so you know why they diverge before you ever hit run. Both are called "emulators" and both handle Cortex-M simulation. But Renode is a platform-description simulator, and QEMU is a full-system emulator—a distinction that shapes everything else. ### The machine definition problem Both bill themselves as STM32 emulators, but only one lets you edit the board underneath it. QEMU ships a compiled-in set of board models.[^4] You pick one: `qemu-system-arm -M stm32vldiscovery`. That board is hardcoded. The CPU, the memory map, the peripherals, the interrupt routing—all baked in. You can't change it without recompiling QEMU. Renode ships with `.repl` files—platform description files written in a YAML-like syntax. A `.repl` file is a machine definition. You can load one, modify it, compose it with others, or write your own. The STM32F4 Discovery board in Renode is not a fixed model; it's a description you can read and edit.[^1] This matters because real hardware is messy. The STM32F4 Discovery has CCM (core-coupled memory) at `0x10000000`. The standard platform description might not include it. In Renode, you add it with four lines: ``` ccm: Memory.MappedMemory @ sysbus 0x10000000 size: 0x10000 ``` In QEMU, you're stuck. The fixed board model doesn't include CCM, and there's no config knob to add it: extending the memory map means patching QEMU's board source and recompiling. Your firmware's write to `0x10000000` hits an address the model never accounted for, and what happens next depends on how that specific board wires up its unmapped space, not on anything you control. ### Peripheral fidelity: the honest gap Both model peripherals, but neither models them completely. Renode's approach is explicit. A peripheral in Renode is a C# object that implements a register interface. You can write custom peripherals in C# or Python. The UART peripheral, for example, accepts writes to the transmit register and emits bytes. It doesn't model baud rate, clock gating, or pin multiplexing. It's byte-level. That's documented.[^2] QEMU's peripherals are C code. They're often more detailed—QEMU's UART can model baud rates and clock divisors. But the detail is inconsistent. Some boards have rich peripheral models; others have stubs. And the documentation of what's actually modeled versus what's faked is scattered across source code comments. Neither models the analog side. A real STM32 ADC has noise, settling time, and reference voltage behavior; both simulators give clean digital reads. A real UART has timing jitter and electrical characteristics; both give perfect bytes. A real SPI bus has clock skew and signal integrity; both give bit-perfect transfers. This is fine if you're testing firmware logic. It's a problem if you're validating timing-critical code or debugging electrical issues. ### Timing models: where they diverge most QEMU's developers are explicit: this isn't cycle-accurate emulation. TCG's `icount` feature tracks instruction counts for deterministic replay, but the docs say plainly: "This should not be confused with cycle accurate emulation - QEMU does not attempt to emulate how long an instruction would take on real hardware."[^5] By default, QEMU clocks run on wall time, same as any other process on your machine. Renode doesn't claim cycle accuracy either. Its time framework advances virtual time from a configured MIPS rate: tell it the core runs at 100 MIPS and one virtual microsecond covers 100 instructions, no pipeline or cache modeling involved.[^3] It can also run flat out, letting simulated time race ahead of wall-clock time, useful for tests that only care about ordering, not absolute duration. Neither tool measures cycles. Both approximate timing from instruction counts—a real limit worth understanding on its own terms. We cover what "cycle-accurate" actually requires, and which firmware bugs genuinely need it, in [a separate piece](/blog/what-cycle-accurate-actually-means). ### Multi-machine and networking Renode was built for multi-node simulation. You can create ten virtual boards in one session, connect them with virtual networks, and test distributed firmware. This is native to Renode's architecture. QEMU can run multiple instances, but they're separate processes. Networking between them requires external setup (tap devices, bridges, etc.). It's possible but not the intended use case. If you're testing a mesh network or a multi-device protocol, Renode is the obvious choice. If you're testing a single board, it doesn't matter. ### Debugging integration Both support GDB. Renode exposes a GDB server on a port; you connect with `arm-none-eabi-gdb` and debug as if the board were on your desk. QEMU does the same. Renode also has an official VS Code extension for spawning and controlling simulations from inside the editor,[^7] plus a built-in monitor with commands for introspection: log function calls, trace register writes, inspect memory, inject faults. The monitor is powerful for embedded work. QEMU's debugging is more minimal. You get GDB and the QEMU monitor (which is different from Renode's). For low-level debugging, Renode wins. For basic breakpoint-and-step work, both are fine. ### CI/CD and automation Renode was designed for CI. You write `.resc` scripts (Renode scripts) that set up a machine, load firmware, run tests, and exit. These integrate cleanly with Jenkins, GitLab CI, GitHub Actions. The Robot Framework integration lets you write test suites in a readable syntax. QEMU is scriptable via the monitor, but it's less ergonomic. You typically wrap QEMU in shell scripts or Python. It works, but it's more boilerplate. For automated testing in a pipeline, Renode is the better fit. If you're weighing the whole ladder of firmware simulators, not just this pair, that's the territory covered in [Wokwi alternatives for professional firmware work](/blog/wokwi-alternatives-professional-firmware). ### Extensibility and customization Renode lets you write custom peripherals in C# or Python. You can model a sensor, a radio, a custom ASIC—anything that reads and writes registers. The platform description format is composable; you can build complex boards from simpler pieces. QEMU requires C code for custom peripherals. You modify QEMU's source, recompile, and test. It's more powerful but much higher friction. If you need to simulate a custom peripheral or a board that doesn't exist in either tool, Renode is more accessible. ### The Renode vs QEMU comparison table | Dimension | Renode | QEMU | | ------------------------------- | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | **Machine definitions** | Composable `.repl` files; easy to modify and extend | Fixed board models; hardcoded in source | | **Supported Cortex-M boards** | Broad and community-extensible: STM32 (F0-H7), nRF, LPC, Kinetis, and more | A short, fixed list: a handful of STM32 variants, plus Stellaris and MPS2/3 test chips[^6] | | **Peripheral modeling** | Byte-level; extensible via C# or Python | Instruction-level; C code; inconsistent detail | | **Timing model** | Instruction-count based (configurable MIPS rate); can run as-fast-as-possible[^3] | Wall-clock by default; optional instruction counting (`icount`) for determinism, not cycle timing[^5] | | **Multi-node simulation** | Native; designed for it | Possible via separate processes; not native | | **Debugging** | GDB + IDE integration + monitor + function tracing | GDB + QEMU monitor | | **CI/CD integration** | Native; `.resc` scripts + Robot Framework | Via shell scripting; more boilerplate | | **Extensibility** | Custom peripherals in C# or Python | Custom peripherals in C; requires recompile | | **What it silently lies about** | Analog behavior, clock gating, pin routing, electrical timing | Same, plus inconsistent peripheral detail across boards | | **License** | MIT (open-source)[^8] | GPL/LGPL (open-source) | ![Full-system emulation vs. platform-described simulation](/blog/renode-vs-qemu-what-each-models/image-1.webp) ### Where each one wins **Use Renode if:** - You're testing Cortex-M firmware and need to iterate on the board definition. - You're running automated tests in CI and want readable, maintainable test scripts. - You need to simulate multiple boards talking to each other. - You're modeling a custom peripheral or a board that doesn't exist in QEMU. - You want to trace function calls, inspect register writes, or debug at the firmware level. **Use QEMU if:** - You're testing embedded Linux on ARM (QEMU is the standard for this). - You need deterministic, replayable execution for debugging (QEMU's `icount` / record-replay).[^5] - You're already in a QEMU workflow and the board model exists. - You're comfortable with shell scripting for test automation. **Use both if:** - You're serious about firmware validation. Run the same test on both and compare results. If they differ, you've found a gap in one tool's model. ![Choosing a simulator](/blog/renode-vs-qemu-what-each-models/image-2.webp) ### Can QEMU emulate an STM32F4 Discovery board? Not directly. QEMU's Cortex-M support for STM32 is a short, fixed list of boards: `stm32vldiscovery`, `netduino2`, `netduinoplus2`, `olimex-stm32-h405`, and a couple of others, and none of them is an F407 Discovery board.[^6] The closest match is `netduinoplus2`, which models an STM32F405, a related chip with a different peripheral map, not the same board. [We ran an F407 binary against it anyway](/blog/renode-vs-qemu-same-firmware) and watched it hit a fatal, unescalatable fault before printing a single byte, on firmware that boots clean on Renode's exact `stm32f4_discovery` machine. ### The honest part Neither tool is a substitute for hardware. Firmware that compiles, runs, and produces output in simulation can still fail on real silicon. The UART might be wired to the wrong pin. The ADC might see noise on the bench. Timing might miss deadlines under load. The value of simulation isn't that it's accurate. It's that it's fast and repeatable. You catch the obvious bugs—the logic errors, the state machine hangs, the missing initialization—before you touch hardware. Then you use hardware for the things simulation can't do: verify timing under real load, check electrical behavior, validate power consumption, and sign off on the final binary. Renode and QEMU are both good at the simulation part. They just model different things. Pick the one that matches your board and your workflow. And when the simulation says "it works," remember: that's the beginning of validation, not the end. ### Sources [^1]: Renode Platform Description Format. https://renode.readthedocs.io/en/latest/advanced/platform_description_format.html [^2]: Baldassari, François. "Cortex-M MCU Emulation with Renode." Interrupt by Memfault, March 23, 2020. https://interrupt.memfault.com/blog/intro-to-renode [^3]: Renode Time Framework. https://renode.readthedocs.io/en/latest/advanced/time_framework.html [^4]: QEMU Arm System Emulator. https://www.qemu.org/docs/master/system/target-arm.html [^5]: QEMU, "TCG Instruction Counting." QEMU documentation. https://www.qemu.org/docs/master/devel/tcg-icount.html [^6]: QEMU, "STMicroelectronics STM32 boards." QEMU documentation. https://www.qemu.org/docs/master/system/arm/stm32.html [^7]: Antmicro. "Testing and debugging embedded systems in simulation with Renode VS Code extension." https://antmicro.com/blog/2024/12/introducing-renode-vscode-extension [^8]: Renode, LICENSE file (MIT license). https://github.com/renode/renode/blob/master/LICENSE --- ## The build passed. Why doesn't it run? (https://veecle.ai/blog/the-build-passed-why-doesnt-it-run, 2026-08-04) The firmware compiles. No warnings. No errors. The embedded CI pipeline turns green. Then you flash it to the board and nothing happens. Not a crash. Not a fault. Nothing. The UART is silent. The LED doesn't blink. The state machine never starts. The code is there—the linker proved it—but the chip isn't running it the way you expected. This is the gap nobody talks about in embedded development—the space between "the build passed" and "it works on hardware." Most firmware bugs live here.[^1] ### The compile-green trap The compiler is ruthless about syntax. It catches stale APIs, missing dependencies, broken types, and the language rules you forgot existed. But the compiler has no opinion about whether your code actually does what you intended. An address is an address. A register write is a register write. If you tell the compiler to initialize USART1 instead of USART2, it will happily compile that. If you forget to enable the clock for a peripheral, the compiler doesn't care. If you configure the wrong GPIO pins, the linker will place the code in flash and call it done. All of these compile clean. All of them produce silent failures on hardware. ![The CI pipeline that ends at the linker](/blog/the-build-passed-why-doesnt-it-run/image-1.webp) The problem is asking the compiler to be the final gate. ### What the run catches A virtual board run is the next layer. You upload the ELF, the simulator boots the chip, executes the firmware for a fixed budget of virtual time, and captures what the UART says. That catches a different class of failure: the ones that compile but don't behave. We demonstrated this in our [earlier post](/blog/what-llms-get-wrong). A small language model's nRF52840 attempt compiled without a single warning. It used the modern UARTE peripheral, satisfied the borrow checker, and looked plausible on review. But when it ran on the virtual board, it produced nothing. The firmware had written to a DMA buffer in flash—which the peripheral couldn't read—so the UART stayed silent. The run said so immediately: five seconds of virtual CPU time, zero bytes on the wire. That's the exact failure class the compiler can't catch, and it's unambiguous the moment the firmware actually runs. Here's what we saw when we tested a broken STM32F4 firmware that uses the wrong UART peripheral: ``` [chiplab] run complete (board: stm32f4_discovery) [chiplab] uart0: (no output captured) ``` The firmware booted. The code executed. The linker placed everything correctly. But the UART peripheral it was trying to write to wasn't the one connected to the board's debug connector—a silent failure the run caught in seconds. Then we fixed it—switched to the correct peripheral—and ran it again: ``` [chiplab] run complete (board: stm32f4_discovery) [chiplab] uart0: Hello world! ``` Same board, same toolchain, same linker. Different peripheral. The run is the receipt. ### The failure classes and where they die Not every bug needs a physical board to catch. The layers protect each other: | Failure class | Compiler | Virtual board | Physical hardware | | ---------------------------------------------- | --------- | -------------- | ----------------- | | Stale API or missing dependency | ✓ Catches | Not needed | Not needed | | Wrong peripheral mode or clock gate | ✗ Misses | ✓ Catches | Can confirm | | Firmware boots but produces no output | ✗ Misses | ✓ Catches | Can confirm | | Code hangs waiting for an event | ✗ Misses | ✓ Catches | Can confirm | | Exact timing, pin routing, electrical behavior | ✗ Misses | ✗ Insufficient | ✓ Final authority | | Real sensor, actuator, and fault injection | ✗ Misses | ✗ Insufficient | ✓ Final authority | The goal is fewer bad binaries reaching the expensive layer. Automotive research has been formalizing exactly this move, building systematic ways to decide which test cases can shift off a hardware-in-the-loop bench and onto a simulation instead.[^4] ### Why this matters now Embedded teams used to accept a slow feedback loop because firmware changes were slow too. A human wrote the code, flashed the board, and watched the output. The whole loop was human-paced. AI can generate firmware faster than a bench can be booked. If an agent can propose ten changes before lunch, a team needs a way to reject the obviously broken ones before those changes compete for a shared bench. The verification system has to handle candidate code at machine speed, even when the final answer still comes from real hardware.[^2] That means the pyramid becomes a funnel: many changes enter at the top, few make it to the bottom. ![The funnel in action](/blog/the-build-passed-why-doesnt-it-run/image-2.webp) Run the cheap checks first. Keep the bench for what needs a bench. We've made the harder version of this case already: [HIL alone can't scale to how fast an agent generates firmware](/blog/hil-wont-scale-ai-era). This post is about the gate that runs before the bench ever sees the code. ### The boundary is real Simulation doesn't get a free pass here. In our own probes, a byte-level simulation model accepted clock and configuration mistakes that real hardware wouldn't forgive. We wrote that down instead of editing it out, because the boundary matters. A virtual target is a test environment with a scope, not a magic copy of reality. So is HIL. So is a vehicle. The point is to know what each one can prove. The simulator catches structural lies—wrong peripheral, wrong pin, code that hangs—but doesn't model exact timing, electrical behavior, or the analog quirks that only show up under load. That's what the bench is for, and in a safety context the standards name it explicitly: ISO 26262 leans on HIL for software integration testing and for verifying software safety requirements.[^5] But the bench shouldn't be the first place you find out that the firmware never enabled the UART clock. We've hit this same boundary from two other angles. [Renode and QEMU ran the same STM32 firmware and diverged](/blog/renode-vs-qemu-same-firmware) on what counted as a valid boot. And when we planted a real crash on purpose, [Chiplab booted straight through a HardFault it doesn't model yet](/blog/an-agent-fixed-a-hardfault). Same lesson every time: know what a layer can prove before you trust it. ### Can firmware CI run without hardware? For most failure classes, a virtual board catches them before a physical board gets touched. Wrong peripheral, wrong clock gate, firmware that hangs waiting for an event: all caught by simulation. Firmware CI without hardware isn't a replacement for the bench. It's a filter that keeps bad binaries from reaching it. That ordering is the standard recommendation: run everything you can without devices first, and only let jobs touch real hardware once the cheap checks pass.[^3] The bench still handles exact timing, electrical behavior, and failures that only show up under real load. A HIL rig exists precisely because testing everything against the complete embedded system is impractical on safety, availability, or cost grounds.[^6] The point isn't to skip hardware. It's to stop spending it on bugs a simulator would have caught in five seconds. ### What an embedded CI pipeline needs now Embedded continuous integration used to be: build, link, ship to the bench. It needs to be: build, link, run on virtual hardware, then ship to the bench. The compiler is ruthless and cheap. The virtual board is the next gate. The bench is the final sign-off. That's how you keep the expensive resource valuable in the age of generated code: not as the place every change begins, but as the place the surviving changes earn their final sign-off. The build passed. Now make it run. ### Sources [^1]: Stack Overflow, "Continuous Integration on hardware-centric firmware," 2023. https://stackoverflow.com/questions/70303010/continuous-integration-on-hardware-centric-firmware [^2]: Sevenhuijsen, Patil, Nyberg, and Ung, "Generating Safety-Critical Automotive C-programs using LLMs with Formal Verification," PMLR 284, 2025. https://proceedings.mlr.press/v284/sevenhuijsen25a.html [^3]: Semaphore, "What CI/CD strategies work for embedded or IoT projects that require hardware testing?" 2024. https://semaphore.io/blog/what-ci-cd-strategies-work-for-embedded-or-iot-projects-that-require-hardware-testing [^4]: Keil et al., "Evaluation of SiL Testing Potential—Shifting from HiL by Identifying Compatible Requirements with vECUs," _Vehicles_, 2024. https://doi.org/10.3390/vehicles6020044 [^5]: Himmler, A. (dSPACE GmbH), "Hardware-in-the-Loop Testing of Safety-Relevant Functions in the Context of ISO 26262," ERTS 2012. http://web1.see.asso.fr/erts2012/Site/0P2RUC89/TC-2.pdf [^6]: NI, "HIL Test System Architectures." https://www.ni.com/en/solutions/transportation/hardware-in-the-loop/hardware-in-the-loop--hil--test-system-architectures.html --- ## LLMs write good firmware. They can't prove it. (https://veecle.ai/blog/llms-write-good-firmware-cant-prove-it, 2026-08-04) The papers all say the same thing. EmbedAgent tested 10 LLMs on 126 embedded tasks across 3 hardware platforms. DeepSeek-R1, the best performer, hit 55.6% pass@1 with schematics provided and 50% without.[^1] On ESP-IDF, the top model reached 29.4%. Englhardt et al. ran 450 trials and found that even when LLMs fail to produce working code, they generate helpful reasoning—but the reasoning doesn't translate to working hardware.[^2] Promwad's analysis is blunt: GenAI works for boilerplate and documentation. It breaks on timing, concurrency, DMA, and real-time constraints.[^3] These papers measure code generation. But LLM firmware code generation is not the same as firmware that works. ### The benchmark-to-production gap Benchmarks measure what's easy to measure. A test case has an input, a model produces output, and you check if the output is correct. Pass or fail. Clean. Quantifiable. Perfect for a paper. Firmware in production has a different shape. It compiles. It runs. It produces output. It doesn't crash under interrupt load. It doesn't corrupt memory when DMA and the CPU access the same buffer. It meets timing deadlines. It works on the actual silicon, not a simulation. It passes certification. A benchmark can check the first two. It cannot check the rest without hardware. We've made this exact point about CI before: [a green build doesn't guarantee firmware that runs](/blog/the-build-passed-why-doesnt-it-run). Same gap, different layer. | Requirement | Benchmark measures | Production requires | Who verifies | | ----------------------------------- | ------------------ | ------------------- | ----------------------- | | Code generation | Yes | Yes | Compiler | | Compilation | Sometimes | Yes | Compiler | | Correct output on ideal conditions | Yes | Yes | Virtual board or HIL | | Correct output under interrupt load | No | Yes | Real hardware | | DMA buffer safety | No | Yes | Real hardware | | Timing constraints met | No | Yes | Real hardware | | Pin routing and electrical behavior | No | Yes | Real hardware | | Certification compliance | No | Yes | Auditor + real hardware | The benchmarks are honest about this. EmbedAgent's abstract says it "enables end-to-end assessment from circuit design to cross-platform deployment." But "end-to-end" in the paper means: given a schematic, generate code. It does not mean: flash the code onto a board and verify it works. Englhardt et al. explicitly note that their testbench is "automated" and measures "LLM performance on embedded programming tasks"—not whether the firmware actually runs on hardware.[^2] This is not a flaw in the benchmarks. It's the boundary of what benchmarks can do. They're measuring generation. They're not measuring execution. ### The feedback loop that never closes The real problem lives here. When you write firmware, you get a receipt. You compile. You flash. You run. The UART prints something, or it doesn't. The LED blinks, or it hangs. The sensor reads, or it returns garbage. You see the failure immediately. You fix it. You iterate. The loop is tight. Feedback is fast. You learn what works and what doesn't. It generates code. The code is evaluated by a benchmark: does it compile? Does it produce the right output in simulation? Pass or fail. But the model never sees what happens when that code runs on real hardware under real constraints. It never learns that DMA buffers can't live in flash. It never learns that interrupt masking has to happen before accessing shared state. It never learns that the UART baud divisor is wrong because the clock tree is different on this chip. Every model that writes firmware is operating blind. It has no feedback from the machine. ![The feedback loop developers have vs the dead-end loop LLMs have](/blog/llms-write-good-firmware-cant-prove-it/image-1.webp) This is why the benchmarks show what they show. EmbedAgent's 55.6% pass rate on simple tasks reflects models generating code in a vacuum, without feedback from the machine. Promwad's analysis captures this exactly: "GenAI generates plausible code, not verifiable system behavior."[^3] Plausible is what you get without feedback; verifiable is what you get when you run the code and see what it does. ### What LLM firmware code generation benchmarks actually measure Let's be precise about what each benchmark is testing: The **EmbedAgent** benchmark measures code generation on embedded tasks with and without schematic information. The "pass" criteria is that the generated code compiles and produces correct output in simulation. It does not test the code on real hardware. The 55.6% pass rate means: "55.6% of the time, the model generates code that compiles and works in simulation." It does not mean: "55.6% of the time, the model generates firmware that works on a real board."[^1] **Englhardt et al.** tested LLMs on embedded programming tasks using an automated testbench. They found that LLMs can generate helpful reasoning and debugging suggestions, but their code often fails on cross-domain hardware-software tasks. The study explicitly notes that "embedded systems design and programming straddles the hardware-software interface and requires a cross-domain understanding of how devices interact with the physical world."[^2] The testbench measures code generation. It does not measure whether the code works on hardware. **MDPI's benchmark** (Babiuch & Smutný) evaluates LLMs on microcontroller-driven IoT applications. Like the others, it measures code generation and compilation. The scope is explicit: embedded systems programming in IoT contexts. It does not include hardware validation.[^4] **Promwad's analysis** is the most honest about the boundary. It separates where GenAI works (boilerplate, documentation, test scaffolding, code navigation) from where it breaks (timing, concurrency, DMA, real-time constraints, vendor SDK behavior). The key insight: "Firmware is not evaluated only on correctness at the function level. It is evaluated on deterministic behavior across interrupts, DMA transactions, memory boundaries, and timing constraints."[^3] Benchmarks measure function-level correctness. Production measures system-level behavior. ### Why this matters The gap between "code that compiles" and "code that works" is where most embedded bugs live. In our earlier test of [what LLMs get wrong about embedded code](/blog/what-llms-get-wrong), we cold-prompted two models for hello-world firmware on three boards. Both models' HAL attempts failed to compile—the compiler caught stale APIs. Good. But their raw-register attempts all compiled clean. Six attempts, zero compilation errors. Then we ran them on virtual hardware. One of the small model's attempts produced zero output for five seconds. The code was syntactically correct, logically plausible, and completely broken. The compiler had no opinion. The benchmark would have passed it. The hardware said no. That's the gap. That's where the loop breaks. Code generation is valuable. But it's not the same as firmware that works. And as long as models have no feedback from hardware, they have no way to learn the difference. ![Trust me, the code is good](/blog/llms-write-good-firmware-cant-prove-it/image-2.webp) ### The empty slot This is where Chiplab fits: treating [hardware as an API agents can call](/blog/hardware-is-becoming-an-api). The benchmarks measure generation. Production needs execution. The gap is the feedback loop that closes the circuit. An LLM that can generate code is useful. An LLM that can generate code, compile it, run it on virtual hardware, see the output, and iterate is something else entirely. That's a system with a receipt. That's a loop that closes. The models won't get better at firmware by being better at language. They'll get better by getting feedback from the machine. --- [^1]: Jiang et al., "EmbedAgent: Benchmarking Large Language Models in Embedded System Development," arXiv:2506.11003v3, 2025. https://arxiv.org/html/2506.11003v3 [^2]: Englhardt et al., "Exploring and Characterizing Large Language Models for Embedded System Development and Debugging," CHI EA '24, May 2024. https://doi.org/10.1145/3613905.3650764 [^3]: Promwad, "GenAI for Firmware: Where LLM Code Generation Breaks Down in Real Embedded Systems," April 2026. https://promwad.com/news/genai-for-firmware-llm-code-generation-certification-risks [^4]: Babiuch & Smutný, "Benchmarking Large Language Models for Embedded Systems Programming in Microcontroller-Driven IoT Applications," _Future Internet_, vol. 18, no. 2, 2026. https://www.mdpi.com/1999-5903/18/2/94 --- ## An agent tried to fix a HardFault. There was no HardFault to fix. (https://veecle.ai/blog/an-agent-fixed-a-hardfault, 2026-08-04) The plan was simple: stage a Cortex-M HardFault debugging session on a virtual board, break it in a way that crashes real silicon, let an agent read the fault dump, fix the bug, re-run. I got the "before." The fault never fired. ### The bug I planted I copied `examples/bare-metal/stm32f4-discovery` from the [Chiplab repo](https://github.com/veecle/chiplab) into a scratch directory — never touched the repo itself — and added a HardFault handler that reads the real Cortex-M fault registers and prints them over UART before halting: ```rust #[exception] unsafe fn HardFault(ef: &ExceptionFrame) -> ! { const CFSR: *const u32 = 0xE000_ED28 as *const u32; const HFSR: *const u32 = 0xE000_ED2C as *const u32; const MMFAR: *const u32 = 0xE000_ED34 as *const u32; const BFAR: *const u32 = 0xE000_ED38 as *const u32; print_str("\n[HardFault]\n"); print_str("CFSR = "); print_hex32(core::ptr::read_volatile(CFSR)); print_str("\nHFSR = "); print_hex32(core::ptr::read_volatile(HFSR)); print_str("\nMMFAR = "); print_hex32(core::ptr::read_volatile(MMFAR)); print_str("\nBFAR = "); print_hex32(core::ptr::read_volatile(BFAR)); print_str("\nstacked PC = "); print_hex32(ef.pc()); loop {} } ``` CFSR and HFSR at those exact addresses are the standard Cortex-M fault status registers — the same ones every HardFault debugging guide walks through.[^1] Then I added a bug: a raw pointer write straight into flash, at an address no provisioning step had prepared. ```rust // A config record that a provisioning step is supposed to have written into // flash before this firmware ever runs. Provisioning never ran, so this is a // write straight into ROM through a bad pointer. let cfg_ptr = 0x0801_0000 as *mut u32; unsafe { core::ptr::write_volatile(cfg_ptr, 0x1234_5678) }; ``` On a real STM32F407, writing to flash content outside an unlock-and-erase sequence is a textbook way to earn a bus fault. I built it, uploaded the ELF, and ran it on `stm32f4_discovery`. ### Attempt one: nothing happens ``` 08:55:12.3397 [INFO] machine-0: Machine started. 08:55:12.3868 [WARNING] flash_controller: Unhandled write to offset 0x0. ... 08:55:12.3970 [INFO] usart2: [host: 0.19s|virt: 0s] Hello world! 08:55:17.3435 [INFO] machine-0: Machine paused. ``` No `[HardFault]`. No CFSR. The firmware printed its banner, executed the bad write, and idled quietly until the run's 5-second virtual-time budget expired. Not a hang, not a crash — just silence where a fault should have been. I tried three more standard ways to fault a Cortex-M core: a branch to an odd/invalid address (INVSTATE), an explicit `udf` undefined-instruction trap, and a read from a definitely-unmapped address (`0x3000_0000`) with alignment trapping enabled. Same result each time: `Hello world!`, then five seconds of silence, then the run ends. The unmapped-read attempt is the interesting one, because it's the one place the simulator's own log admits it saw the bad access: ``` 08:52:24.3132 [INFO] machine-0: Machine started. 08:52:24.3612 [WARNING] flash_controller: Unhandled write to offset 0x0. ... 08:52:24.3715 [INFO] usart2: [host: 0.2s|virt: 0s] Hello world! 08:52:24.5966 [WARNING] sysbus: [cpu: 0x8000604] ReadDoubleWord from non existing peripheral at 0x30000000. 08:52:29.3181 [INFO] machine-0: Machine paused. ``` ![The run that logged the crime and let it go](/blog/an-agent-fixed-a-hardfault/image-1.webp) The bus model noticed the access was bogus, logged it, and returned zero anyway. The CPU never took the fault vector. ### What the runs told us | Observation (real, from the runs above) | Conclusion | | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | `Hello world!` prints identically in every run | The bug sits after the working code path; boot and UART are unaffected | | `bx` to an odd address (INVSTATE) produces no fault or log line | The core doesn't check Thumb-state validity on branch | | `udf #0xde` produces no fault or log line | Undefined-instruction trapping isn't wired to the exception table | | Read from `0x3000_0000` logs a bus warning but returns `0`, no fault | The bus model detects invalid accesses; the CPU model doesn't act on them | | `SCB->CCR.UNALIGN_TRP` set, then an unaligned read: no fault | Alignment trapping isn't enforced either | | Every run ends at the 5-second virtual-time budget, not on a crash | The simulator treats "ran to the time limit" as success, same as a healthy `loop {}` | Four architecturally distinct ways to fault a Cortex-M core. Zero faults. ### I asked Chiplab why Chiplab ships a discovery/help tool specifically for "what does this simulation actually cover" questions,[^2] so I asked it directly, pasting in what I'd seen. The answer was unambiguous: > CPU-level fault escalation (CFSR, HFSR, HardFault handler invocation) is not > currently a modeled feature in Chiplab's simulation... the underlying > emulator executes instructions and models peripheral register maps, but the > Cortex-M fault machinery that sits between "bad thing happened" and > "HardFault handler runs" is not wired up. Chiplab runs on Renode under the hood — the same log format that gives us `[INFO] machine-0: Machine started.` is Renode's, the emulator [Interrupt has written about](https://interrupt.memfault.com/blog/intro-to-renode) for exactly this kind of firmware-without-hardware workflow.[^3] Renode is real, well-used, and good at what it's built for: booting firmware, running peripheral models, capturing UART — we've compared [Renode vs. QEMU on the same firmware](/blog/renode-vs-qemu-same-firmware) elsewhere, if you want the fidelity gap laid out board-by-board. Renode fault handling for invalid memory access and illegal instructions is a different, harder piece of CPU fidelity, and on this board it isn't there yet. ![Expectation vs. reality](/blog/an-agent-fixed-a-hardfault/image-2.webp) ### What real Cortex-M HardFault debugging would show I never dereferenced this pointer on real silicon, so I won't invent register values for it. A write into flash outside an unlock sequence on real Cortex-M hardware raises a bus fault; if BusFault handling isn't explicitly enabled (it isn't, by default), it escalates to HardFault, and the CFSR's `BFARVALID` bit and the `BFAR` register would point at the address that caused it.[^1] Keil's own app note on Cortex-M3/M4/M7 fault exceptions and ST's own debugging guide for this exact family both walk through reading that same trio — CFSR, HFSR, and the faulting address register — as the first move after any fault.[^4] [^5] There is no CFSR/HFSR dump to show you here. The run never produced one, because on this board the fault never fires. That gap is the finding. Not a bug in my firmware. A boundary in the simulation. ### Why I'm publishing the miss We've written before about what a virtual board can and can't prove — a byte-level UART model that let a wrong baud rate sail through undetected,[^6] and the general case for treating simulation as one layer in a funnel, not a hardware replacement.[^7] This applies to a sharper edge: CPU-level fault handling is squarely inside "should the virtual layer own this," and today it doesn't. If your firmware's correctness depends on a fault handler actually running — recovery logic, a watchdog kick, a fault log written to flash before reset — a virtual STM32F4 run on Chiplab today will tell you your happy path works and stay silent about whether your unhappy path does. That's worth knowing before you trust the green checkmark, not after. The four things I broke on purpose — a bad flash write, an invalid branch, an undefined instruction, an unaligned trap — are, right now, four bug classes a real board would catch and Chiplab won't. The UART output, the peripheral state, the "did my firmware actually boot and do the thing" signal are all real and useful, and were exactly right in every one of these runs. But the boundary matters: "cheap layers eat the iteration, silicon gets the final word" only works if you know exactly which failures the cheap layers can't see yet. This is worth flagging to the Chiplab team, and I will — with these two run IDs attached. If fault escalation lands, I'll rerun this exact bug and post the real dump, with the run ID next to it, same as everything else here. ### Why wasn't the HardFault handler called? Because nothing raised the fault. Renode's bus model logged every bad access — the unhandled flash write, the read from a non-existent peripheral — and returned quietly instead of escalating to a CPU exception. No exception, no vector, and the HardFault handler I wrote was never called. The handler was fine. The simulator never gave it a reason to run. - **Repo & examples:** [github.com/veecle/chiplab](https://github.com/veecle/chiplab) - **Discord:** [discord.com/invite/F6GwZJ6ktP](https://discord.com/invite/F6GwZJ6ktP) - **Chiplab:** [veecle.ai/chiplab](https://veecle.ai/chiplab) ### Sources [^1]: Coleman, Chris. "How to debug a HardFault on an ARM Cortex-M MCU." Interrupt by Memfault. https://interrupt.memfault.com/blog/cortex-m-hardfault-debug [^2]: Chiplab discovery/help tool and `AGENTS.md`, veecle/chiplab. https://github.com/veecle/chiplab/blob/main/AGENTS.md [^3]: Baldassari, François. "Cortex-M MCU Emulation with Renode." Interrupt by Memfault, March 23, 2020. https://interrupt.memfault.com/blog/intro-to-renode [^4]: Yiu, Joseph. "Using Cortex-M3/M4/M7 Fault Exceptions." Keil/ARM Application Note. https://www.keil.com/appnotes/files/apnt209.pdf [^5]: STMicroelectronics. "How to debug a HardFault on an Arm Cortex-M STM32." ST Community. https://community.st.com/stm32-mcus-60/how-to-debug-a-hardfault-on-an-arm-cortex-m-stm32-132895 [^6]: "What LLMs get wrong about embedded code." Veecle. [What LLMs get wrong about embedded code](/blog/what-llms-get-wrong) [^7]: "HIL won't scale to the AI era." Veecle. [HIL won't scale to the AI era](/blog/hil-wont-scale-ai-era) --- ## What 'cycle-accurate' actually means (and why you almost never need it) (https://veecle.ai/blog/what-cycle-accurate-actually-means, 2026-08-04) Post a virtual-board run and count how many replies it takes before someone shows up with: "sure, but is it cycle-accurate?" It's not usually a question. It's a dismissal wearing a question mark. The implication is that if the simulator isn't cycle-accurate, whatever it just caught doesn't count. Cycle-accurate simulation is real. It has a precise definition. Almost everybody demanding it in your replies isn't actually asking for it. And "just flash the real board" isn't free either — [HIL benches don't scale to how fast agents can now generate test runs](/blog/hil-wont-scale-ai-era). The same move already happened with the word "simulation" itself. A well-known embedded consultant posted recently that most engineers get simulation wrong: real simulation means decoupling your business logic from the hardware and running it on your host machine. Firing up QEMU or Renode to model a whole microcontroller is "emulation," not simulation, and therefore beside the point.[^1] That's a real distinction and a fine argument for testable architecture. It's also a category reshuffle that lets you dismiss a whole class of tooling without engaging with what it actually catches. "Is it cycle-accurate" is the same move with a scarier-sounding word. Let's define the word properly, because it has a real definition, and then figure out how often you actually need what it names. ### The three tiers of embedded simulator accuracy Embedded simulator accuracy isn't one dial. It's three different claims: **Behavioral / functional accuracy.** Does the program produce the right outputs for the right inputs? No timing claim at all — gem5's own tutorial material describes functional simulation as executing programs correctly, "usually no timing information," used to check that a compiler or an instruction implementation is correct in the first place.[^2] This is what you want when you're asking "did my firmware do the thing." It's also where real divergence shows up before anyone's talking about clocks: [the same STM32 firmware booted clean on Renode and crashed instantly on QEMU](/blog/renode-vs-qemu-same-firmware), no cycle count needed to catch that. **Instruction accuracy.** Same as above, plus a rough notion of how long things took — usually derived from instruction count, not from modeling a pipeline. Renode's time framework is a clean example: it advances virtual time in units tied to a configured MIPS rate, so a CPU rated at 1 MIPS executes exactly one instruction per virtual microsecond.[^3] That gets you relative ordering and ballpark timing. It does not get you the exact cycle a bus stall added. **Cycle accuracy.** This is where instruction-accurate vs. cycle-accurate actually splits: the simulator's internal state changes in lockstep with the real hardware's clock, cycle for cycle — pipeline stages, cache fills, bus arbitration, wait states, all of it. This is the tier people are actually invoking when they use the word, whether they know it or not. Here's the part that makes the objection funnier than it thinks it is: gem5, the tool the computer-architecture research world treats as _the_ serious cycle-level simulator, doesn't claim to be cycle-accurate either. Its own maintainers draw the line explicitly — gem5 is "cycle-level," which "models the system cycle-by-cycle" and can be "quite accurate," but is "not the exact same cycle-by-cycle as the ASIC." True cycle accuracy, in their own slide, means RTL: register-transfer-level, the actual hardware description, "the same in the model and in an ASIC."[^2] A UC Davis architecture-lab write-up says the quiet part out loud: "gem5 is only cycle-level, not cycle-accurate."[^4] If the field's reference simulator for cycle-level work isn't cycle-accurate by the term's own definition, the guy in your replies almost certainly isn't asking for cycle accuracy either. He's asking for you to be embarrassed. There's a reason RTL sits at the top. A SiFive forum thread on RISC-V simulators puts the practical requirement plainly: for actual cycle accuracy you need the RTL for the specific design, run through a hardware simulator like Verilator, because different implementations of the same ISA take different numbers of cycles for the same instruction — an instruction-set simulator can't know that without a hardware model to match.[^5] Arm's own materials frame it as a deliberate trade-off, not a free upgrade: cycle-accurate models buy 100% confidence in timing but cost you RTL availability, execution speed, and validation effort — which is exactly why Arm sells both "Fast Models" (fast, not cycle-accurate) and RTL-derived "Cycle Accurate" models as separate products for separate jobs.[^6] ![The accuracy spectrum](/blog/what-cycle-accurate-actually-means/image-1.webp) ### What bug classes actually need which tier This is the part that matters. Most firmware bugs don't live in the cycle-accurate tier. Some genuinely do. | Bug class | Example | Accuracy tier actually needed | | -------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------ | | Missing/wrong init sequence | Peripheral clock never enabled before the register write | Behavioral | | State machine logic | Handshake never reaches its exit state, firmware hangs forever | Behavioral | | Protocol sequencing | I²C start/stop/ack issued out of order | Behavioral | | Peripheral mode misconfiguration | DMA channel pointed at the wrong buffer | Behavioral | | RTOS scheduling / priority bugs | Low-priority task starves a higher one | Behavioral / instruction-accurate (ordering matters, exact cycles don't) | | Soft real-time budgets | "Is this ISR roughly within budget" | Instruction-accurate | | Bit-banged protocol timing | Software SPI/I²C/1-Wire timed by delay loops and NOP counts | Cycle-accurate | | Cache/DMA memory-ordering races | Stale cache line read after a DMA write lands | Cycle-accurate | | Hard real-time WCET sign-off | Certifying worst-case execution time against a safety deadline | Cycle-accurate (RTL) | ### When do you actually need cycle-accurate simulation? Concede the real cases. Bit-banging is the textbook one. When there's no UART peripheral and the protocol timing comes entirely from counting instruction cycles in a delay loop, the cycle count _is_ the correctness criterion — Microchip's own app note on software one-wire framing describes bit-banged timing as built from delay routines that count "a predefined number of CPU clock cycles," where the loop itself defines the bus protocol.[^7] A behavioral model that just executes the loop and moves on has thrown away the only thing that mattered. Cache and DMA interaction is the other one that's genuinely a cycle problem, not a logic problem. When a DMA engine writes to memory a cached core hasn't invalidated yet, the bug isn't in your code's logic — the code is fine — it's in the exact interleaving of a cache fill, a bus transaction, and an invalidate that only shows up if the timing model knows about cache lines and bus arbitration at all. Arm's own coherency material calls out systems where "50% of debug time is spent on SW coherency issues" for exactly this reason: the bug is invisible to anything that treats memory as instantaneous and uniform.[^8] And if you're certifying worst-case execution time for a hard deadline — not "usually fine," but a number a safety case depends on — you're back to needing RTL, for the reason the SiFive thread gave: different silicon implementations of the same instruction set take different numbers of cycles for the same instruction, and no ISA-level model can know that without the actual hardware description.[^5] None of that is a strawman. All of it is real, and all of it is a small fraction of what a normal firmware verification loop spends its time on. ![The gatekeeping meme](/blog/what-cycle-accurate-actually-means/image-2.webp) ### What most firmware verification is actually asking Look back at that table. Driver init order, state machine logic, protocol sequencing, peripheral mode selection — that's most of what breaks in a firmware review, and none of it needs a clock tick to be modeled correctly. It needs the _behavior_ modeled correctly: did the write happen, did the state advance, did the sequence come out in the right order. That's the layer a byte-level UART model or an instruction-stepped CPU is built for. We've written before about [what Renode and QEMU each actually model at that layer](/blog/renode-vs-qemu-what-each-models) — Renode's UART peripheral, for instance, hands over bytes when firmware writes them and doesn't pretend to know about baud rate or clock gating, which is a real gap if you're validating electrical behavior and completely irrelevant if you're checking whether your driver enabled the clock before writing the baud register.[^9] That's not a cycle-accuracy gap. It's a scope, and it's the right scope for the bug class that dominates a normal review. So the next time "is it cycle-accurate" shows up in your replies, ask back what specific bug they think that objection is going to catch. If the answer is a missing register write, a state machine that never advances, or a driver that enables the wrong mode — that's not a cycle-accuracy problem. It's a behavior problem, and a behavioral model already caught it, in seconds, without anyone waiting for a board. If the answer is a bit-banged delay loop or a cache/DMA race, they're right, and you should go get RTL. That case exists. It's just not the one showing up in most replies. ### Sources [^1]: Beningo, Jacob. "Most embedded engineers misunderstand simulation." LinkedIn, 2025. https://www.linkedin.com/posts/jacobbeningo_most-embedded-engineers-misunderstand-simulation-activity-7373325093281779712-D-Ml [^2]: gem5 Tutorial, ISCA 2024, "Introduction to Simulation and gem5." Slides on functional vs. cycle-level vs. cycle-accurate RTL simulation. https://www.gem5.org/assets/files/isca2024-tutorial/01-intro.pdf [^3]: Renode Time Framework documentation. https://renode.readthedocs.io/en/latest/advanced/time_framework.html [^4]: Ganjehloo, Lowe-Power, and Akella. "Integrating Cycle Accurate Chisel Models with gem5's System Simulation." UC Davis Computer Architecture, 2018. https://arch.cs.ucdavis.edu/simulation/2018/11/14/CycleAccurateChiselGem5.html [^5]: SiFive Community Forums. "Which one is timing/cycle accurate RISC-V software simulator?" https://forums.sifive.com/t/which-one-is-timing-cycle-accurate-risc-v-software-simulator/2842 [^6]: Arm. "High Performance or Cycle Accuracy?" White paper. https://developer.arm.com/-/media/Arm%20Developer%20Community/PDF/Cycle%20Models/White%20Papers/High%20Performance%20or%20Cycle%20Accuracy.pdf [^7]: Microchip Technology. "USART in One-Wire Mode," AN2658, §3.2 Bit Banging. https://onlinedocs.microchip.com/oxy/GUID-CF2C0DBC-087A-4CE7-B9C1-5E08A928CFD9-en-US-6/GUID-52ED8983-65D6-4CC9-8A08-F6384F009DDB.html [^8]: Arm Developer Community. "Extended System Coherency: Part 1 — Cache Coherency Fundamentals." https://developer.arm.com/community/arm-community-blogs/b/architectures-and-processors-blog/posts/extended-system-coherency---part-1---cache-coherency-fundamentals [^9]: Baldassari, François. "Cortex-M MCU Emulation with Renode." Interrupt by Memfault, March 23, 2020. https://interrupt.memfault.com/blog/intro-to-renode — see also our own [Renode vs. QEMU comparison](/blog/renode-vs-qemu-what-each-models). --- ## Wokwi alternatives for professional firmware work (https://veecle.ai/blog/wokwi-alternatives-professional-firmware, 2026-08-04) Someone on r/embedded asked what simulators people actually use for ARM Cortex-M development, "curious what others use... for actual development or mostly just for CI."[^1] Dozens of upvotes, dozens of comments, three usable answers: Renode, whatever ships with Zephyr, and "real hardware, no simulators." That's the honest state of the search "Wokwi alternatives" once you strip the SEO listicles: nobody agrees, because the question conflates five different tools solving five different problems. I'm writing this because if you searched "Wokwi alternatives" and landed here, you've probably outgrown it — you need an embedded simulator for professionals: a build that survives a pull request, not a breadboard that survives a demo. ### Give Wokwi its due Wokwi is a browser-based circuit and firmware simulator for Arduino (Uno, Mega, Nano), ESP32, STM32, and the Raspberry Pi Pico.[^2] You drop parts on a virtual breadboard, write code, hit run, and watch LEDs blink with zero hardware on your desk. It's free for personal use, unlimited simulations on the Community tier, and the paid tiers (Hobby, Hobby+, Pro) buy faster builds, VS Code integration, and — this is the part people miss — CI minutes: Wokwi Pro includes 2,000 monthly CI minutes and a `wokwi-ci-action` GitHub Action for automated tests.[^2] It's also genuinely used by working engineers, not just students: one Adafruit forum thread calls it "WAY better" than anything else the poster had tried, unprompted, in a thread that wasn't about Wokwi at all.[^3] Wokwi already has a CI story. What it lacks is peripheral fidelity below the behavioral level. Its own comparison against Renode and QEMU is blunt: "Wokwi does not simulate timing-precise peripherals or complex real-time behavior. Always verify with physical hardware for production systems."[^4] That's not a Wokwi-specific gap — it's the tier it's designed for: quick prototyping, tutorials, smoke-testing a library, checking GPIO logic before you touch a real board. If that's your job today, stop reading and go use it. If your job is "does this firmware survive a pull request without a bench," you need tools further up the ladder. ![The verification ladder](/blog/wokwi-alternatives-professional-firmware/image-1.webp) ### The Wokwi alternative nobody puts on a listicle The tool that actually shows up when working engineers answer "what do you use for ARM Cortex-M" is Renode. Antmicro built it as an open-source, MIT-licensed[^5] simulation framework specifically for pre-silicon and CI-stage embedded work — Cortex-M, Cortex-A, Cortex-R, RISC-V, and more, with peripheral models for UART, SPI, I2C, GPIO, timers, and sensors.[^6] Zephyr and TensorFlow Lite Micro both use it in their own CI: Pete Warden, who leads TensorFlow's mobile/embedded team, put it plainly — "Renode gives us integrated software emulation for a lot of Arm and RISC-V devices, and we rely on it for our testing."[^7] Renode gets recommended over QEMU for firmware work because it offers deterministic multi-node execution and a testing framework built in. It ships with a Robot Framework integration and an official GitHub Action, so a pull request can run real test suites against simulated boards with logs and pass/fail reports, not just a boot check.[^8] That's the actual meaning of "CI-usable" for a simulator: not "can run headless," but "produces a result a CI system can gate on." QEMU deserves the comparison — the [Renode vs QEMU comparison](/blog/renode-vs-qemu-what-each-models) covers the model-by-model detail — because it's the tool people reach for first out of habit: everyone already has it. For full Linux-class systems (Cortex-A) it's excellent, which is why it dominates that world. For microcontroller-class Cortex-M work, its own docs are candid about the boundary: STM32 support covers five boards — one F1, one F2, two F4 variants, and one L4. The F-series boards model the core, ADC, EXTI, USART, SPI, and timers but not GPIO or DMA; the L4 board flips that, adding GPIO but dropping ADC and the timer.[^9] A Memfault engineer summed up the reputation: "QEMU is focused on emulating systems meant for higher-level OSes... rather than embedded devices."[^10] Whichever board you pick, something your firmware actually touches is missing. ### Vendor sims are a real tier, not a fallback If your stack is already Keil or IAR, you have a simulator sitting inside your toolchain that's easy to forget counts as one. Keil MDK ships an instruction-set simulator in µVision and, since MDK-Professional, includes Arm Virtual Hardware (AVH) at no extra cost — functionally accurate Fixed Virtual Platforms for select Cortex-M designs. That's Arm's own term, not the stricter [cycle-accurate](/blog/what-cycle-accurate-actually-means) bar. AVH is built for CI and MLOps pipelines and is free for evaluation, open-source, and non-commercial use via Arm's Tools Artifactory.[^11] It's also listed on AWS Marketplace as a subscription SaaS product for cloud-scale runs beyond a laptop.[^12] PlatformIO sits in an adjacent lane worth naming precisely so it doesn't get lumped in as a simulator: it's a build and test runner, and its "native" test mode compiles your hardware-independent logic for your host CPU and runs it there — no chip simulation at all, deliberately.[^13] That's a different, valid layer: fast unit tests for the parts of your firmware that don't touch a register. It's not competing with Renode; it's one rung of the pyramid over. ### The table | Tool | Sweet spot | Chips covered | Peripheral fidelity | CI-usable? | Agent-accessible? | Price | | ----------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | **Wokwi** | Prototyping, learning, quick sketches | Arduino Uno/Mega/Nano, ESP32, STM32, RP2040[^2] | Behavioral (LEDs, sensors, common buses); not timing-precise[^4] | Yes — `wokwi-cli` + GitHub Action, CI minutes on Pro[^2] | Experimental — local MCP server via `wokwi-cli mcp` (stdio, needs CLI + token)[^16] | Free (Community) → €20/seat/mo (Pro)[^2] | | **Renode** | Pre-silicon and CI regression on Cortex-M/A/R, RISC-V | Wide, community-modeled — dozens of ARM and RISC-V platforms[^6] | High — models UART, SPI, I2C, GPIO, timers, deterministic multi-node[^6] | Yes — built-in Robot Framework + official GitHub Action[^8] | Scriptable via CLI/Python; no built-in HTTP/agent API | Free, open source (MIT)[^5] | | **QEMU (M-profile)** | Full-system A-profile emulation; narrow M-profile support | 5 STM32 boards (F1, F2, 2x F4, L4); core support broader than board support[^9] | Core: good, but inconsistent per board — F-series skip GPIO/DMA, the L4 board skips ADC/timers[^9] | Yes — standard in many CI pipelines (e.g. Zephyr's `qemu_cortex_m3`) | Yes — plain CLI binary, trivially scriptable | Free, open source (GPL) | | **Keil MDK / Arm Virtual Hardware** | Vendor-integrated core sim + functionally accurate FVPs for CI/MLOps | Whatever Keil supports, plus select Cortex-M reference platforms via AVH[^11] | High for core + basic peripherals; functionally accurate (not cycle-accurate) on supported AVH platforms[^12] | Yes — AVH built explicitly for CI/MLOps[^12] | Limited — GUI-first tooling; AVH FVPs are scriptable binaries | MDK: free (Community) → paid seats. AVH: free for eval/OSS/non-commercial, bundled with MDK-Professional, subscription SaaS on AWS[^11][^12] | | **PlatformIO (native test)** | Fast host-side unit tests of hardware-independent logic | N/A — runs on host CPU, or real board via Remote[^13] | None by design — that's a different layer, not a chip model | Yes — this is its core use case[^13] | Yes — plain CLI (`pio test`) | Free (core), paid Enterprise tier | | **Chiplab** | Agent-driven verification of AI-generated firmware, no desk required | STM32 and Nordic nRF families today, more on the roadmap[^14] | Byte-level peripheral behavior — accepts some config mistakes real silicon wouldn't, documented openly[^15] | Yes — designed as a CI gate | Yes — native MCP tool surface: upload, run, fetch artifacts | Free tier (50 credits/day), pay-as-you-go, enterprise[^14] | ### "CI-usable" and "agent-accessible" are different columns Most of the table says yes to CI-usable. Far fewer say yes to agent-accessible — that's the real gap once you're past "which simulator" and into "how does my coding agent use one without me in the loop." CI-usable means a human wired a YAML file once, and now a pipeline runs the same script on every push — though green there doesn't mean correct; see [why the build can pass and the firmware still doesn't run](/blog/the-build-passed-why-doesnt-it-run). Agent-accessible means an agent can, at inference time, decide to compile something, get a board to run it on, and read back a structured result — without a human pre-authoring the exact test it needs. Wokwi's CLI now ships an experimental local MCP server — credit due[^16] — but it's a `stdio` process an agent spawns locally, CLI and token required, not reachable over a network the way Chiplab is. Every other tool in the table still assumes a human wrote the harness in advance. That's fine for regression testing. It's the wrong shape for an agent iterating on code nobody has seen yet. !["Green CI isn't the same test as 'can an agent ask a new question.'"](/blog/wokwi-alternatives-professional-firmware/image-2.webp) We built Chiplab to sit at the top rung most of these tools weren't designed for: not to replace any of them, but to handle agent-driven iteration. It's MCP-native — an agent gets an upload ticket, PUTs a compiled ELF, runs it on a virtual STM32 or nRF52 instance with the same peripherals and interrupt timing as the physical board, and reads the result back as a tool call, not a CI log a human has to parse.[^14] We're not pretending it's a free pass on fidelity either: it's byte-level peripheral modeling, and in our own testing it accepted a wrong baud divisor and a missing clock enable that real silicon wouldn't have forgiven — we published that limitation ourselves instead of hiding it.[^15] Every simulator has a scope. Knowing where yours ends is the actual skill. ### Pick by question, not by brand If the question is "does my code light the LED," Wokwi answers it in your browser in under a minute, and there's no reason to reach for anything heavier. If the question is "does this survive a pull request," you want Renode or a vendor sim wired into CI. If the question is "does this survive a full system boot on Linux-class hardware," that's QEMU's actual home turf, not M-profile microcontrollers. If the question is "can an agent ask this question a hundred times an hour without a human writing a new test each time," that's the rung none of the classic tools were built for — and it's the one we spend our time on. ### Sources [^1]: r/embedded, "What simulators do you actually use for ARM Cortex-M development?" https://www.reddit.com/r/embedded/comments/1qnaepr/what_simulators_do_you_actually_use_for_arm [^2]: Wokwi, "Pricing Plans", homepage, and CI docs. https://wokwi.com/pricing / https://wokwi.com / https://docs.wokwi.com/wokwi-ci/getting-started [^3]: Adafruit Forums, "looking for an Arduino simulator." https://forums.adafruit.com/viewtopic.php?t=184640 [^4]: Industrial Monitor Direct, "Wokwi vs Renode vs QEMU: Embedded System Simulators Comparison." https://industrialmonitordirect.com/blogs/knowledgebase/wokwi-vs-renode-vs-qemu-embedded-system-simulators-comparison [^5]: Renode, GitHub repository ("License & contributions"). https://github.com/renode/renode [^6]: Renode, "Supported boards" documentation. https://renode.readthedocs.io/en/latest/introduction/supported-boards.html [^7]: Renode, homepage quotes (Pete Warden, TensorFlow Mobile/Embedded Team Lead, Google). https://renode.io [^8]: Antmicro / Renode, "Renode GitHub Action for automated testing on simulated hardware" and "Testing with Renode" docs. https://renode.io/news/renode-github-action-for-automated-testing-in-simulation / https://renode.readthedocs.io/en/latest/introduction/testing.html [^9]: QEMU, "STMicroelectronics STM32 boards" documentation. https://www.qemu.org/docs/master/system/arm/stm32.html [^10]: Interrupt (Memfault), "Cortex-M MCU Emulation with Renode." https://interrupt.memfault.com/blog/intro-to-renode [^11]: Arm, "Virtual Hardware: Accelerate Software Development" (Costs section). https://www.arm.com/products/development-tools/simulation/virtual-hardware [^12]: AWS Marketplace, "Arm Virtual Hardware powered by Corellium." https://aws.amazon.com/marketplace/pp/prodview-3ert36ne4rlmq [^13]: PlatformIO, "Unit Testing" documentation. https://docs.platformio.org/en/latest/advanced/unit-testing/index.html [^14]: Chiplab documentation, pricing and overview. https://chiplab.veecle.ai/mcp/resources/docs/pricing.md / https://chiplab.veecle.ai/mcp/resources/docs/overview.md [^15]: Veecle, "What LLMs get wrong about embedded code." /blog/what-llms-get-wrong [^16]: Wokwi documentation, "MCP Support." https://docs.wokwi.com/wokwi-ci/mcp-support --- ## E2B gives agents a computer. What gives them a microcontroller? (https://veecle.ai/blog/e2b-gives-agents-a-computer, 2026-08-04) Ask an agent to write a UART driver for an nRF52840, then ask it to prove the driver works. It can spin up an E2B sandbox in under 200 milliseconds. It can install Python, Node, a compiler, a whole Debian userland, in about a second. It cannot spin up a UART. There's no `pip install nrf52840` for the thing the driver is actually supposed to talk to. That's a gap in the whole AI agent sandbox category, and nobody selling one is hiding it — because until an agent needs to touch real hardware, it was never their problem to solve. ### What the AI agent sandbox boom actually solved The sandbox market exists because agents got hands. Give a model a shell and a filesystem and eventually it `rm -rf`s something it shouldn't, or gets prompt-injected into downloading a binary and running it — that exact chain, from a poisoned webpage to a live command-and-control connection, is a documented security-research finding, not a hypothetical.[^1] The fix was cutting the blast radius: give the agent its own disposable machine instead of yours. By 2026 that idea is a whole industry. E2B boots an isolated Firecracker microVM in around 200ms and hands the agent a shell, a filesystem, and a network namespace.[^2] Modal does roughly the same job on gVisor, with GPU access if the workload needs it.[^2][^3] Daytona, Northflank, Blaxel, CodeSandbox, Fly.io — each with different isolation primitives and pricing models — all follow the same pattern. E2B's own pitch is refreshingly literal about the scope: "if you can run it on a Linux box, you can run it in the E2B sandbox."[^6] That's the whole category: a Linux box. ### They all stop at the same place A sandbox is a fence around a kernel. Firecracker draws that fence with hardware virtualization — every sandbox gets its own guest kernel, and a compromised guest can't see past its own VM boundary. gVisor draws it differently: a userspace process intercepts every syscall the guest tries to make and decides what the host actually sees.[^5] Everything a sandbox protects and grants is defined in terms of what a process on a Linux-shaped kernel is allowed to do. You can see how literally that's true in what Firecracker actually emulates for the guest: network, block storage, and a serial console. That's the device list.[^5] No GPU passthrough by default, no GPIO, no raw bus access — because none of that exists on the other side of a syscall either. The fence sits exactly where the OS abstraction sits, because that's the only boundary the isolation technology knows how to reason about. ![The stack every sandbox vendor ends at](/blog/e2b-gives-agents-a-computer/image-1.webp) ### Firmware doesn't have a syscall table A large share of the code an agent might be asked to write has no syscall table to sandbox in the first place. Bare-metal firmware on a Cortex-M or a RISC-V microcontroller doesn't run on top of a kernel. It is the closest thing to one, in the sense that matters: it owns the reset vector, it initializes its own clock tree before `main` is even reachable, it handles its own interrupts through a vector table it wrote itself, and it talks to a UART or an SPI bus by writing to a specific memory address — not by calling a library that calls a driver that calls a syscall. There's no userspace to fence off, because there's no kernel underneath granting it one. A microVM protects a guest kernel from a host kernel. That's a real, well-understood problem, and Firecracker and gVisor solve it well. It is also a completely different problem from "does this firmware sequence the clock-enable bit before it touches the UART's baud-rate register" — a question a Linux sandbox has no vocabulary for, because the answer lives below the exact layer sandboxes were built to isolate. I've made the proving-it-works half of this argument before: [LLMs can write firmware, but proving it runs is the part they can't do](/blog/llms-write-good-firmware-cant-prove-it). This is the sandbox half. What agents need when the target is embedded hardware, next to what a Linux sandbox actually hands you: | What firmware work needs | What a Linux sandbox gives you | | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | | A specific ISA (Cortex-M, RISC-V) with no OS underneath it | A guest kernel on x86-64 or ARM64 | | Memory-mapped peripheral registers: GPIO, UART, SPI, I2C, ADC, timers | virtio network, virtio block, serial console — the only devices Firecracker models[^5] | | A real interrupt vector table and ISR-level timing | The guest's own virtualized interrupts, invisible to "your" process | | A reset vector and clock-tree init before anything else runs | An init process that's already running by the time you connect | | Bus-accurate timing: baud rates, clock gating, peripheral state machines | Wall-clock time from the host scheduler | | Often: no filesystem, no TCP/IP, no syscalls, no OS at all | A full POSIX filesystem, network namespace, and package managers | | A debug/flash interface — SWD, JTAG, some way to observe silicon | SSH or a PTY into a Linux shell | None of this is a knock on E2B or Modal. They built exactly what the market asked for: safe, fast, disposable compute for code that assumes a Linux box underneath it. Firmware is the workload that doesn't — not out of neglect, but because the hardware it targets was never designed to run an OS at all. ### The empty column Go read any "E2B alternatives" or "best sandboxes for AI agents in 2026" comparison post — there are plenty now, and the good ones are genuinely useful, stacking up boot time, isolation model, pricing, GPU support, BYOC.[^2][^3][^4] Not one has a column for "can this run my firmware against something that models the actual chip." It's not an oversight; it's just not the axis those products compete on. (Every layer of the agent stack has been getting its own dedicated infra company lately — search, memory, browser, compute — and the hardware slot is a familiar kind of empty. I mapped [the rest of the agent infrastructure stack](/blog/the-agent-infrastructure-map) elsewhere; this post is just the hardware slot.) The specific version of that gap is this: you can't syscall-intercept your way to a chip that doesn't have syscalls. Isolation technology, however clever, is built to sit at a boundary that has to already exist. Below the register level, there isn't one. ![Expanding brain meme](/blog/e2b-gives-agents-a-computer/image-2.webp) ### What actually has to sit below the line Filling that gap honestly means building something that isn't a sandbox at all. What firmware work needs is a model of a specific chip — its registers, its peripherals, its interrupt timing, its boot sequence — accurate enough that running a real ELF against it produces a real answer: did the UART actually transmit, did the state machine ever leave its init state, did the firmware hang forever waiting on an event that will never fire. That's a different kind of emulator wearing none of the isolation machinery above, because there's no host kernel to protect anyone from in the first place. That's the specific gap my own team works on, so I'll name it once and move on. Chiplab runs real firmware ELFs against simulated STM32 and Nordic boards[^7] — not a Linux VM with extra steps, but a peripheral-level model of the silicon a given microcontroller target actually compiles for. It's early, and it doesn't replace a bench any more than a Linux sandbox replaces a datacenter. It's just the shape of thing that has to exist below the syscall line, because nothing above that line was ever going to reach it on its own. That's the same shift I trace more broadly in [why hardware is becoming an API for agents](/blog/hardware-is-becoming-an-api) — this post is just the sandbox-shaped slice of it. ### Can an AI agent sandbox run firmware for a microcontroller? No, not the ones on the market today. E2B, Modal, Daytona, Northflank, Blaxel, CodeSandbox, Fly.io — every one of them isolates a Linux-shaped kernel boundary, whether that's Firecracker, gVisor, or Kata underneath. A microcontroller's firmware runs below that boundary: no OS, no syscalls, nothing to virtualize. What you need there isn't a sandbox. It's a model of the chip itself. ### The loop breaks at the machine E2B, Modal, and the rest solved something real: safe, fast, disposable compute for agents that assume a Linux box. That's most agent workloads, and the category earned its funding rounds fair and square. But "give the agent a computer" and "give the agent a chip" aren't the same sentence with one word swapped. One ends at a syscall table; the other starts below it, on hardware that was never designed to have an OS looking over its shoulder. Every chip is an island, and no amount of Firecracker builds a bridge to one — because the bridge has to be a different kind of thing entirely. ### Sources [^1]: Firecrawl, "AI Agent Sandbox: How to Safely Run Autonomous Agents in 2026," citing Johann Rehberger, "ZombAIs: From Prompt Injection to C2 with Claude Computer Use," 2024. https://www.firecrawl.dev/blog/ai-agent-sandbox [^2]: Northflank, "E2B vs Modal: comparing AI code execution sandboxes in 2026." https://northflank.com/blog/e2b-vs-modal [^3]: Blaxel, "E2B Alternatives: Sandbox Environments for 2026." https://blaxel.ai/blog/e2b-alternatives-sandbox-environments [^4]: Jimmy Song, "The New Era of Cloud Agent Infrastructure: In-Depth Analysis of E2B and Browserbase Global Trends." https://jimmysong.io/blog/e2b-browserbase-report [^5]: SoftwareSeni, "Firecracker, gVisor, Containers, and WebAssembly — Comparing Isolation Technologies for AI Agents." https://www.softwareseni.com/firecracker-gvisor-containers-and-webassembly-comparing-isolation-technologies-for-ai-agents [^6]: E2B, official site. https://e2b.dev [^7]: Chiplab. https://veecle.ai/chiplab --- ## The agent infrastructure map (https://veecle.ai/blog/the-agent-infrastructure-map, 2026-08-04) The agent changed how we think about infrastructure because agents work at machine speed. A human writes code once. An agent writes code a hundred times before lunch. A human reads a webpage. An agent reads a thousand webpages in parallel. That speed exposed a gap: the tools we built for humans don't scale to agents. So the market filled the gap. Every agent capability became infrastructure — the AI agent infrastructure stack. ### The AI agent infrastructure pattern Five years ago, if you wanted to build an agent, you'd wire it to a search API, a database, a browser, and a sandbox. You'd own the integration. You'd own the reliability. You'd own the cost. Today, you pick a company for each slot. Search got Exa. Memory got Mem0. Browser got Browserbase.[^5] Compute got E2B.[^6] Each is a company, an API, a business model, a reliability contract, and a flag planted in a capability that used to be a feature of your application. The pattern is clean: **capability → infrastructure → company → API → billing**. | Capability | Human-era tool | Agent-era infrastructure | Status | | ------------ | -------------------- | ---------------------------------- | --------- | | **Search** | Google API, Bing API | Exa (custom search for AI) | Filled | | **Memory** | Redis, Postgres | Mem0 (persistent agent memory) | Filled | | **Browser** | Selenium, Puppeteer | Browserbase (browser-as-a-service) | Filled | | **Compute** | Docker, Lambda | E2B (sandbox-as-a-service) | Filled | | **Hardware** | Bare metal, FPGA | ? | **Empty** | The first four slots are occupied. The last one isn't. ### What "filled" looks like When Exa filled the search slot, they built a search engine for AI. Exa's API returns structured outputs, citations, and search traces. It costs $0.10 per compute unit plus $0.005 per search call.[^1] You can set effort levels from `minimal` ($0.012/request) to `xhigh` ($1.00/request) depending on how much reasoning you need. The API is async, handles multi-step research, and returns grounded answers. That's infrastructure. Mem0 filled the memory slot by making persistent memory a service. Instead of managing Redis yourself, you send Mem0 your conversations. It extracts facts, links entities, compresses context, and returns only the memories that matter for the next query.[^2] It runs on your infrastructure (self-hosted) or theirs (cloud), is SOC 2 and HIPAA compliant, and costs based on memory operations, not seat licenses. That's infrastructure. Browserbase filled the browser slot by making the browser itself an API. You describe a task in natural language, and Browserbase runs an autonomous agent that navigates, clicks, extracts, and returns structured JSON.[^3] It handles anti-bot detection, CAPTCHA solving, and login flows, records sessions for debugging, and costs per browser-hour. That's infrastructure. E2B filled the compute slot by making sandboxes instant and persistent. You call an API, get a Linux VM with sudo access, run code, and pause it. When you resume, the filesystem and memory are exactly as you left them.[^4] It costs by CPU-second and memory. That's infrastructure. Each one has the same shape: **API + billing + reliability + compliance**. ![The agent infrastructure stack](/blog/the-agent-infrastructure-map/image-1.webp) ### Why hardware is still empty Hardware is the hardest slot to fill because it's the most specific. Search is search. Memory is memory. Browsers are browsers. But hardware spans microcontrollers, FPGAs, wireless modules, sensors, actuators, and power supplies. An agent controlling a robot needs different hardware than an agent testing firmware. An agent on an edge device needs different hardware than an agent in the cloud. The human-era tools for hardware are still the human-era tools: bare metal, FPGA boards, dev kits, oscilloscopes. You buy them. You plug them in. You own them. You debug them. There's no API. There's no billing model. There's no company in the middle. That's the gap. An agent that writes firmware can test it on a virtual board in seconds. An agent that designs a circuit can simulate it in minutes. But an agent that needs to verify a design on real hardware still needs a human to plug in a board, flash a binary, and watch the output. That's not a bottleneck for one agent. It's a bottleneck for a thousand agents running in parallel. The infrastructure exists. Chiplab runs firmware on virtual boards. Wokwi simulates circuits. AWS Device Farm runs tests on real phones. But none of them are positioned as agent infrastructure. None of them have the API shape that agents expect. None of them have the billing model that scales to agent speed. ### The flag plant The empty slot is a flag plant: this is where the next company will be. When Exa filled the search slot, they repositioned search as infrastructure for agents. They built an API that agents expect, a billing model that scales to agent speed, and reliability that agents need. The same will happen to hardware. Someone will take the infrastructure that exists — virtual boards, simulators, real hardware in the cloud — and reposition it as agent infrastructure: [hardware is becoming an API](/blog/hardware-is-becoming-an-api), the same way servers, GPUs, phones, and browsers already did. They'll build an API that agents expect, a billing model that scales to agent speed, and reliability that agents need. That's the pattern. That's the flag plant. ![The empty hardware slot](/blog/the-agent-infrastructure-map/image-2.webp) ### What the map tells us The agent infrastructure map is a checklist. It says: if you're building an agent, you need these five things. Four of them are companies. One of them is still a gap. The gap is an opportunity and a constraint. Until someone fills it, agents needing real hardware will be slower, more expensive, and harder to scale. That's not a permanent constraint. It's a temporary one. The infrastructure exists. The API doesn't. Someone will build the API. When they do, the map will be complete. And agents will be able to do something they can't do today: verify hardware at machine speed. ### Where Chiplab fits Chiplab runs firmware on virtual boards. It's not the hardware slot. It's the compute slot, specialized for firmware — filling exactly the gap [E2B's sandboxes stop short of](/blog/e2b-gives-agents-a-computer): a Linux box doesn't have a UART. You upload an ELF binary, specify a board, and get back a transcript of the UART output. It's an API. That's the whole interface. But it's not the full hardware slot. It doesn't handle real hardware. It doesn't handle FPGAs. It doesn't handle circuits. It doesn't handle the full spectrum of hardware verification that agents need. What it does is fill a gap in the compute slot. It says: if your agent writes firmware, you don't need a physical board to test it. You can test it on a virtual board in seconds and get the execution receipt that [code-generation benchmarks can't provide](/blog/llms-write-good-firmware-cant-prove-it). That's infrastructure. That's the shape agents expect. The full hardware slot — the one that includes real hardware, FPGAs, circuits, and everything else — is still empty. But Chiplab is a piece of it. It's a flag plant in the firmware corner. ### Sources [^1]: Exa, "Exa Agent API Guide." https://exa.ai/docs/reference/agent-api-guide [^2]: Mem0, "Mem0 Platform Overview." https://docs.mem0.ai/platform/overview [^3]: Browserbase, "Agents Overview." https://docs.browserbase.com/platform/agents/overview [^4]: E2B, "E2B Documentation." https://e2b.dev/docs [^5]: Jimmy Song, "The New Era of Cloud Agent Infrastructure: In-Depth Analysis of E2B and Browserbase Global Trends." https://jimmysong.io/blog/e2b-browserbase-report [^6]: Blaxel, "Best Cloud Sandboxes for AI Agents in 2026." https://blaxel.ai/blog/best-cloud-sandboxes-ai-agents-2026 --- ## Hardware is becoming an API (https://veecle.ai/blog/hardware-is-becoming-an-api, 2026-08-04) You have never touched the server running this page. You have never touched the GPU that trained the model you asked to summarize it. You have probably never touched the phone your app got tested on last CI run, or the browser your scraper drove this morning. Four physical, scarce, expensive machines. Four times, someone put an API in front of the machine and the industry stopped touching it. This is a pattern with dates attached, and it has repeated often enough that you can predict the next hardware API before it ships. ### The pattern, not the vibe Every time it happens, it happens for the same three mechanical reasons, in order: 1. **Scarcity.** The resource is physically limited — one server rack, one GPU cluster, one drawer of test phones, one Chrome process. Not everyone who wants it can have one sitting on their desk. 2. **Parallelism.** Once the resource sits behind a network boundary instead of a desk, it can be pooled and multiplexed. One rack becomes a thousand tenants. One GPU cluster becomes a rented hour. One phone becomes a queue any CI job can join. 3. **Automation pressure.** The moment there's an API instead of a login prompt or a physical cable, software starts calling it directly — scripts, CI pipelines, and now agents. Software doesn't ask for one unit at a time. It asks for as many as the API will give it. Scarcity is why the resource needed pooling. Parallelism is what pooling behind an API buys you. Automation pressure is what happens once machines, not humans, are calling that API. Each wave shipped in that order. ### Four times it already happened | Resource | Before | The API moment | What changed after | | -------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | Servers | Rack your own box, wait weeks for a data-center slot and a sysadmin | Amazon EC2 public beta, August 25, 2006[^1] — pay-per-hour virtual servers, launched by API call | Startups stopped owning data centers; "provisioning a server" became a function call | | GPUs | Buy or lease a physical GPU cluster, share it by spreadsheet and grudge | AWS Cluster GPU Instance (`cg1.4xlarge`), announced November 2010[^2] — the same pay-as-you-go API extended to GPGPU | Nobody buys a GPU cluster to train a model; you rent GPU-hours, and the entire ML training boom rides on that rental market | | Phones | QA's drawer of real handsets, one USB cable, one tester, one device at a time | AWS Device Farm, launched July 9, 2015[^3]; Firebase Test Lab for Android, June 2016[^4] — schedule a test run against real devices over an API | Mobile CI runs hundreds of real-device tests per pull request; nobody keeps a device drawer to ship an app anymore | | Browsers | Self-hosted headless Chrome, hand-managed sessions, crashes at 2 a.m. | Browserbase, founded early 2024 by Paul Klein IV, opened its cloud browser API the same year[^5] | AI web agents rent browser sessions by the API call instead of running their own Chrome farm | Same shape, four times, roughly a decade apart. Each row starts with a human physically operating scarce hardware and ends with software calling an endpoint. ![The four waves](/blog/hardware-is-becoming-an-api/image-1.webp) Chips are the dashed line. There's no chip row in that table with a filled-in "what changed after" column, because that column hasn't been written yet. That's the bet. The same shape shows up elsewhere: search, memory, and browsers all got their own rental market, mapped end to end across [the whole agent infrastructure stack](/blog/the-agent-infrastructure-map). Compute got there through [E2B, which gives an agent a computer but not a microcontroller](/blog/e2b-gives-agents-a-computer). Chips are the one row still empty. ### Why there's no hardware API for chips yet Chips have all three preconditions already. Scarcity: a dev kit is one board, and during the 2021 chip shortage, semiconductor lead times stretched past 26 weeks[^6] — getting a board took months, not the usual next-day shipping. Parallelism: a physical board can run exactly one test at a time, full stop — there's no multiplexing a UART. Nobody has pooled it. Automation pressure: this is the one that just changed, and it changed fast. ### Agents make it 100x worse For twenty years, "automation pressure" meant a CI script hitting an API a few times an hour. That's what EC2, GPU clouds, and device farms were built for, and it was plenty to justify each one. An agent generating firmware doesn't hit an API a few times an hour. It proposes a fix, gets it wrong, proposes another, and does that in a loop with no coffee break and no meetings. Fifty candidate changes before lunch is a normal afternoon for a coding agent working an embedded repo. Every one of those fifty needs to build, run, and ideally get proven against something that behaves like the real chip — [a green build already isn't proof of that](/blog/the-build-passed-why-doesnt-it-run). A CI script can wait its turn for a shared bench and still ship on schedule. An agent that generates work faster than any human queue can absorb turns "wait your turn" into the bottleneck for the entire loop. The automation-pressure leg of the pattern didn't just show up for hardware — it showed up already running at a pace none of the four earlier waves had to handle on day one. And there's a harder floor under all of it: an agent cannot pick up a JTAG probe. It can write code. It cannot walk to a bench, find the right cable, plug it into the right header, and power-cycle a board that's stuck in a bad state. Every one of the first four waves eventually got automated by scripts that still, in principle, a human could have run by hand, just slower. This one is different: the physical step has no manual fallback for the thing doing most of the work now. If the chip isn't behind an API, the agent's loop simply stops at the point where a human has to intervene with their hands — every single time. ![The agent can't hold a probe](/blog/hardware-is-becoming-an-api/image-2.webp) ### What "the API moment" actually requires The API moment was never "make the resource faster." EC2 servers weren't faster than a rack you owned. Cluster GPU instances weren't faster than a GPU on your desk. A phone in a Google data center isn't faster than the one in your pocket. Every API moment was the same move: stop requiring a human to be physically present to operate the thing, so software can operate it at whatever rate it wants. An API doesn't care if the caller is a cron job, a CI pipeline, or an agent running fifty iterations before lunch — and hardware still does. We're building Chiplab to be that layer for chips: firmware built, run, and observed against virtual hardware for AI agents, delivered over an API, so an agent's fiftieth iteration this hour doesn't have to wait for the one physical board on the desk.[^7] Call it hardware as an API and you've described exactly what Chiplab does. I'm not going to pretend that's the whole story — some bugs are electrical, and no API replaces a scope on real silicon. That's a real boundary, and it's a separate post. But everything short of that boundary is exactly the part of the loop the last four waves already proved doesn't need a human standing next to the machine. Servers went first. Then GPUs. Then phones. Then browsers. Each time, someone got tired of a human being the bottleneck between software and a scarce machine, and put an endpoint in the way instead. Hardware is becoming an API. ### Sources [^1]: Amazon Elastic Compute Cloud, "History" — Wikipedia. Public beta announced August 25, 2006. https://en.wikipedia.org/wiki/Amazon_Elastic_Compute_Cloud [^2]: Jeff Barr, "New EC2 Instance Type – The Cluster GPU Instance," AWS News Blog, November 2010. https://aws.amazon.com/blogs/aws/new-ec2-instance-type-the-cluster-gpu-instance [^3]: Frederic Lardinois, "Amazon Launches AWS Device Farm, Lets Developers Test Android And Fire OS Apps On Real Devices," TechCrunch, July 9, 2015. https://techcrunch.com/2015/07/09/amazon-launches-aws-device-farm-lets-developers-test-android-and-fire-os-apps-on-real-devices [^4]: Doug Stevenson, "Introducing Firebase Test Lab for Android," Firebase Blog, June 2016. https://firebase.blog/posts/2016/06/introducing-firebase-test-lab-for [^5]: Jimmy Song, "The New Era of Cloud Agent Infrastructure: In-Depth Analysis of E2B and Browserbase," September 3, 2025. https://jimmysong.io/blog/e2b-browserbase-report [^6]: KPMG, "Surviving the Silicon Storm," 2021. https://web.archive.org/web/20251113024813/https://assets.kpmg.com/content/dam/kpmg/br/pdf/2021/06/automotive-semiconductor-shortage.pdf [^7]: Chiplab. https://veecle.ai/chiplab --- ## Simulation vs HIL: what belongs in CI and what belongs in the lab (https://veecle.ai/blog/simulation-vs-hil-what-belongs-in-ci, 2026-08-04) You pushed a commit. Something should test it. The only real question is which something, and how long you have to wait for the answer. That is the entire simulation vs HIL argument, and almost nobody writes about it from where you're standing. ### Who the existing SIL vs HIL literature is actually for I read the top results before writing this. They're good. They're just not written for someone with a branch open. OPAL-RT sells real-time simulators and HIL hardware. Their guide still tells you to start with software-in-the-loop and move to the bench only when timing and I/O behavior must be proven — otherwise "the lab becomes a bottleneck."[^1] A HIL vendor writing that down is worth more than me writing it down. TofuPilot, which sells hardware test- tracking software, publishes a MIL/SIL/HIL guide with a comparison matrix that has one row I'd put on a poster: CI-friendly — MIL yes, SIL yes, HIL "No (dedicated bench)."[^2] Wirtek, an embedded engineering services shop, is the most honest of the three about limits: their HIL primer says a HIL test "is only as good as the model behind it, so poor model fidelity can give false confidence."[^3] All three are correct. All three are written for someone who owns a validation plan. The unit of decision is a project phase — early design, integration, release candidate. Nobody tells you where `test_can_frame_parse.c` goes. The one place I've seen the decision actually encoded for developers is Zephyr's Twister — config, not philosophy. A board declares `simulation: qemu` or is typed `native`. A test declares `fixture:` when it depends on an external device like a sensor, so the runner only schedules it where that device exists. Physical runs need `--device-testing` plus a hardware map with one entry per connected board, serial port and all.[^4] That last detail is the whole embedded test strategy problem in one YAML file: simulated targets scale with your CI runners, physical targets scale with how many boards you literally own. ### Simulation vs HIL, decided in three questions Here's the procedure. It's three questions, and it takes about ten seconds per test. **1. Does this test need the target's instruction set, memory map, or peripheral registers?** If no — it's parsing, arithmetic, a state machine, a protocol encoder — it's a host unit test. Compile it for your laptop. Milliseconds, thousands of cases, no target involved. Most firmware developers under-use this tier because "it's embedded," which is not a reason. **2. Can this test fail in a way that only shows up as a voltage, a nanosecond, or a degree Celsius?** If no, it goes in simulation, in CI, on every commit. Peripheral register order, driver bring-up, boot progress, does the thing actually print — a virtual target answers those and answers them the same way every time. **3. If yes — is the physics something a rig can drive, or something only an instrument can measure?** A rig with a real MCU and a simulated plant handles real bus peers, real sensor parts, and real interrupt latency. A current probe, a thermal chamber and an anechoic chamber handle the rest. Those are different rooms, not the same tier. If you can't tell which, put the test in both CI and the nightly suite until one of them catches something — that's how you find where the boundary actually sits for your board. The two properties are independent. How fast you need an answer is a workflow constraint. How much the answer depends on physics is a property of the test. Cross them and one of the four boxes is genuinely uncomfortable. ![Decision quadrant for simulation vs HIL, mapping firmware tests to host unit tests, CI simulation, or the lab](/blog/simulation-vs-hil-what-belongs-in-ci/image-1.webp) The bottom-left cell is the one teams get wrong. If a test is pure logic and you're not in a hurry, people still send it to the bench out of habit, because the bench is where tests happen. It's free in CI. Run it there anyway. The top-right cell is the honest one. Sometimes you need a fast answer about something physical, and the truthful response is that you get an approximation now and a real answer later. Anyone selling you a way out of that box is selling something. ### The table: what to test in CI, what to test in the lab This is the artifact. Find your test, read the row. | Test category | Where it belongs | Why | | ------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------- | | Business logic, state machines | Host unit test | No silicon involved. Thousands of cases in under a second. | | Protocol framing and parsing | Host unit test | Bytes in, bytes out. The target adds nothing to the answer. | | Peripheral register sequences | Simulation in CI | Needs a real memory map; wrong-order writes show up as no output. | | Driver init and bring-up path | Simulation in CI | Catches "clock never enabled, peripheral never started" with no board. | | Boot progress and expected output | Simulation in CI | The cheapest possible "did my firmware do the thing" receipt. | | Fault handling and recovery logic | Simulation in CI — if your simulator models faults | Many don't. Verify yours does before you trust a green check. | | Interrupt priority and preemption order | Split: order in CI, latency on HIL | Ordering is logic. Latency is physics. Don't conflate them. | | Timing budgets and deadlines | HIL in the lab | Instruction counts are not wall-clock nanoseconds. | | Analog thresholds, ADC and comparator trips | HIL in the lab | A modeled ADC returns the number you told it to return. | | Real bus peers and sensor parts | HIL in the lab | Real parts have startup delays, noise and errata your model doesn't. | | Power draw and sleep current | Physical only | You need a current probe. There is no software version of this. | | EMI and EMC | Physical only | Needs a chamber and a calibrated antenna.[^5] | | Thermal behavior | Physical only | Needs a chamber and hours of real time. | | Long-run soak | Split: counters and leaks in CI, drift in the lab | Counter rollover and heap fragmentation are logic. Component drift isn't. | ### What simulation cannot answer, said plainly I'm not going to argue simulation replaces hardware-in-the-loop testing. It doesn't, and the fastest way to make a team distrust simulation entirely is to oversell it once. The failure mode is a green test, not a red one. A simulated peripheral can accept a configuration real silicon would reject and hand you a pass. A fault your board would trap can go unmodeled and simply never fire. That is the subject of [what simulation can't catch](/blog/what-simulation-cant-catch), with the runs behind it. A model is a scope, not a copy of reality. Physical tests have scopes too, which is the part the vendor material skips. There's work out of Clemson's automotive research center noting that the correlation between component-level EMC results and vehicle-level EMC performance "is generally poor."[^5] Even the chamber is answering a narrower question than you think it is. Nothing in this stack is truth. Everything is a measurement with a boundary, and your job is to know which boundary you just crossed. ### Tier it: simulation every commit, HIL on a schedule The tiering argument is the practical payoff, and the ordering is not arbitrary. Simulation in CI on every commit. HIL nightly and pre-release, on a much smaller suite that only contains tests from the right-hand column above. Not the other way around. The reason is throughput, not ideology. Say a HIL rig runs two hundred jobs a day. If most of those would have died at compile or at boot anyway, that rig is a queue with a cable harness, not a test system. Put the cheap filter first and the same rig spends its time on jobs that genuinely needed a rig, and people stop fighting over slots. That scarcity is structural, not a symptom of a badly run lab. Linaro's LAVA describes itself as a way to share boards among teams — an entire piece of infrastructure whose job is rationing physical targets.[^6] Twister's hardware map is the same fact at smaller scale: one YAML entry per board you own. The every-commit tier is genuinely off-the-shelf now. Antmicro ships a Renode GitHub Action that drops simulated-hardware tests into a normal CI pipeline,[^7] and Memfault's Interrupt has a full walkthrough of wiring Renode tests to GitHub Actions on pull requests.[^8] We build Chiplab for that same middle tier, and it does not answer anything in the right-hand column of my table. ![Firmware test tiering pyramid from host unit tests and CI simulation up to HIL and lab instruments](/blog/simulation-vs-hil-what-belongs-in-ci/image-2.webp) ### Firmware CI without hardware still has a floor "We have CI" and "we test firmware" are not the same sentence. Plenty of embedded pipelines stop at a successful link and call it green, which is why [most firmware CI ends at the linker](/blog/firmware-ci-ends-at-the-linker) needed writing. Firmware CI without hardware only counts as testing if something in the pipeline actually executes the binary against a model of the target and checks what came out. There's a timing pressure on all of this now that didn't exist five years ago. An agent can propose ten firmware changes before lunch, and a feedback loop that routes through a shared physical rig is a ticket queue, not a feedback loop. I've made that argument at length in [why HIL won't scale to the AI era](/blog/hil-wont-scale-ai-era), so I won't relitigate it here — it's the same tiering, just under more pressure. ### Can simulation replace hardware-in-the-loop testing? No. It replaces the _first_ hundred trips to the bench, not the last one. If a test can be wrong in volts, nanoseconds or degrees, it needs the lab, and no amount of model fidelity changes that. What simulation buys you is that every binary reaching the lab already compiles, boots, configures its peripherals and produces output. Go look at your CI config. If the last step is `arm-none-eabi-gcc` exiting zero, you don't have a test tier — you have a compiler. ### Sources [^1]: OPAL-RT. "Hardware in the loop vs software in the loop guide for engineers," May 13, 2025. https://www.opal-rt.com/blog/hardware-in-the-loop-vs-software-in-the-loop [^2]: Buteau, Julien. "HIL vs SIL vs MIL Testing: When to Use Each." TofuPilot Guides, March 14, 2026. https://www.tofupilot.com/guides/hil-vs-sil-vs-mil-testing-when-to-use-each-with-tofupilot [^3]: Wirtek. "Hardware-in-the-loop testing for embedded systems," June 18, 2026. https://www.wirtek.com/blog/hardware-in-the-loop-testing-for-embedded-systems [^4]: Zephyr Project. "Test Runner (Twister)," Zephyr Documentation. https://docs.zephyrproject.org/latest/develop/twister/index.html [^5]: Hubing, Todd H. "Module-Level Characterization for Vehicle-Level Emissions Modeling," Clemson University International Center for Automotive Research, APEMC 2010. https://cecas.clemson.edu/cvel/pdf/APEMC2010-692.pdf [^6]: Linaro. "Linaro Automated Validation Architecture (LAVA)." https://www.lavasoftware.org [^7]: Antmicro. "Renode GitHub Action for automated testing on simulated hardware," October 16, 2024. https://renode.io/news/renode-github-action-for-automated-testing-in-simulation [^8]: "Firmware Testing with Renode and GitHub Actions." Interrupt by Memfault. https://interrupt.memfault.com/blog/test-automation-renode --- ## What simulation can't catch, and why it never tells you (https://veecle.ai/blog/what-simulation-cant-catch, 2026-08-04) I wrote a CI check that asserted a block of code finished under a cycle budget. It passed. It measured zero cycles, because the counter it reads does not exist on that virtual board. Most writing about embedded simulation limitations is a feature checklist: this simulator has Ethernet, that one doesn't. That framing is useless, because a missing feature you know about costs you an afternoon. The expensive failures are the ones where the simulator answers the question anyway. It returns a number. The number is fiction. Nothing in your build log says so. So here is the honest version, organized by failure mode rather than by feature, and built on two real Chiplab runs I can hand you the IDs for. Chiplab runs Renode 1.16.1, and everything below is visible in the transcripts. ### Failure mode 1: a register that does not exist The DWT cycle counter is the standard way to time code on a Cortex-M. You set TRCENA in DEMCR at `0xE000EDFC`, set CYCCNTENA in DWT_CTRL at `0xE0001000`, and read DWT_CYCCNT at `0xE0001004`.[^1] Every profiling guide for this family walks you through those three addresses. I built a probe that does exactly that, then times 256 flash-resident `nop` instructions under four different FLASH_ACR configurations. Here is what came back, unaltered: ``` Renode, version 1.16.1 (d66b0c2aa3d420408eccecfd1d3bab0fd702a6db) cpu: Setting initial values: PC = 0x8000189, SP = 0x20030000 ... sysbus: [cpu: 0x8000334] WriteDoubleWord to non existing peripheral at 0xE0001004, value 0x0. sysbus: [cpu: 0x8000336] ReadDoubleWord from non existing peripheral at 0xE0001000. sysbus: [cpu: 0x800033E] WriteDoubleWord to non existing peripheral at 0xE0001000, value 0x1. usart2: [host: 0.22s (+8.49ms)|virt: 0s (+0s)] dwt_ctrl=0x00000000 cyccnt_enabled=no usart2: [host: 0.22s (+0.79ms)|virt: 0s (+0s)] cyccnt_before=0 cyccnt_after=0 advanced=NO ``` The writes returned. No fault, no error code. From inside the firmware, enabling the cycle counter looked like it worked. `dwt_ctrl` reads back `0x00000000` and every subsequent measurement is zero. This is the shape of the whole problem. The simulator knew. It printed `non existing peripheral` on its own host console. The firmware — the thing under test, the thing your assertions run inside — learned nothing. ![Terminal transcript showing a DWT cycle counter read returning zero on a simulated STM32F4](/blog/what-simulation-cant-catch/image-1.webp) ### Failure mode 2: a register modeled as storage, not as behavior Same run, next four lines. The probe writes FLASH_ACR at `0x40023C00` with different latency settings and times the same 256 `nop`s each time. ``` flash_cfg=ws0_no_accel acr_written=0x00000000 acr_readback=0x00000000 cycles_for_256_nops=0 flash_cfg=ws5_no_accel acr_written=0x00000005 acr_readback=0x00000005 cycles_for_256_nops=0 ``` Look at the middle column. `acr_written=0x00000005 acr_readback=0x00000005`. The LATENCY field reads back exactly what was written. A clock-setup driver that verifies its own register write — which is good practice, and which most HALs do — is completely satisfied. There is no timing consequence at all. Flash wait states on this model are a variable you can store a number in. On silicon, five wait states means the core stalls five cycles on every flash fetch that misses the accelerator. Here it means nothing, and it means nothing quietly. This is worse than an unimplemented register, because an unimplemented register usually reads back zero and a careful driver notices. This one lies convincingly. ### Failure mode 3: bits that vanish between the write and the read The last two configurations turn on the flash accelerator. Prefetch is bit 8 of FLASH_ACR, instruction cache is bit 9, data cache is bit 10. ``` flash_controller: Unhandled write to offset 0x0. Unhandled bits: [8] when writing value 0x105. Tags: PRFTEN (0x1). flash_cfg=ws5_prefetch acr_written=0x00000105 acr_readback=0x00000005 cycles_for_256_nops=0 flash_controller: Unhandled write to offset 0x0. Unhandled bits: [9-10] when writing value 0x600. Tags: ICEN (0x1), DCEN (0x1). flash_cfg=ws0_icache_dcache acr_written=0x00000600 acr_readback=0x00000000 cycles_for_256_nops=0 ``` Write `0x105`, read back `0x005`. Bit 8 is gone. Write `0x600`, read back `0x000`. Bits 9 and 10 are gone. Renode names them precisely on its console — `Tags: PRFTEN (0x1), ICEN (0x1), DCEN (0x1)` — which is genuinely good diagnostics, on a channel the firmware cannot see. On revision A of this exact silicon, ST's own errata sheet says the ART Accelerator prefetch queue "is not supported," with workaround "None."[^2] So a real chip and this simulator can agree that PRFTEN does nothing — for completely unrelated reasons, one of which is documented in a PDF you can read and the other of which is only discoverable by writing a probe. That is the gap. [What cycle-accurate actually means](/blog/what-cycle-accurate-actually-means) goes deeper into why simulator accuracy claims and timing claims are different claims. ### Failure mode 4: memory that is not there at all Different probe, same board. This one writes `0xC0FFEE01` to a list of addresses and reads each back. ``` == ram boundary probe: linker claims 192k at 0x20000000 == initial_sp_from_vector_table=0x20030000 addr=0x20000100 name=sram1_start+0x100 wrote=0xc0ffee01 read=0xc0ffee01 ram_here=yes addr=0x20020000 name=just_past_128k wrote=0xc0ffee01 read=0xc0ffee01 ram_here=yes addr=0x20030000 name=past_192k_claim wrote=0xc0ffee01 read=0xc0ffee01 ram_here=yes sysbus: [cpu: 0x80003BC] WriteDoubleWord to non existing peripheral at 0x20040000, value 0xC0FFEE01. addr=0x20040000 name=way_past_256k wrote=0xc0ffee01 read=0x00000000 ram_here=no sysbus: [cpu: 0x80003BC] WriteDoubleWord to non existing peripheral at 0x10000000, value 0xC0FFEE01. addr=0x10000000 name=ccm_data_ram wrote=0xc0ffee01 read=0x00000000 ram_here=no == probe complete, no fault taken == ``` ST's RM0090 lists an STM32F405/407 as having "Main internal SRAM1 (112 KB)", "Auxiliary internal SRAM2 (16 KB)", and a separate 64 KB CCM data RAM that "is not part of the bus matrix."[^3] The DS8626 memory map puts that 128 KB block at `0x20000000`, marks `0x2002 0000 – 0x3FFF FFFF` as Reserved, and puts CCM at `0x10000000`.[^4] The simulated map is inverted relative to the chip: | Address range | Real STM32F407 (RM0090 / DS8626) | This simulated board | Run B probe result | | --------------------------- | -------------------------------- | -------------------- | --------------------------------------- | | `0x10000000` | CCM data RAM, 64 KB | not mapped | `read=0x00000000 ram_here=no` | | `0x20000000` – `0x2001FFFF` | SRAM1 + SRAM2, 128 KB | RAM, present | `read=0xc0ffee01 ram_here=yes` | | `0x20020000` – `0x2003FFFF` | **Reserved** | **RAM, present** | `read=0xc0ffee01 ram_here=yes` | | `0x20040000` and up | Reserved | not mapped | `read=0x00000000 ram_here=no`, no fault | A follow-up probe walked the exact edge to be sure it wasn't a lucky guess: RAM answers correctly through the last word at `0x2003FFFC` and goes silent at `0x20040000`. That's a probed boundary, not an inference — the simulated board provides exactly 256 KB contiguous from `0x20000000`. It invents 128 KB the chip does not have at that address and omits 64 KB the chip does have somewhere else. And read the last transcript line again: `no fault taken`. Writes into unmapped space vanished, reads returned zero, and the CPU never took an exception. That matches what I found when I planted [four deliberate faults and got no HardFault](/blog/an-agent-fixed-a-hardfault) — the bus model spots the bad access, logs it, and moves on. ![Diagram comparing the real STM32F407 memory map with the simulated board's inverted map](/blog/what-simulation-cant-catch/image-2.webp) ### The bug this hid was ours That `0x20030000` stack pointer is not a synthetic example. It comes from `memory.x` in Veecle's own public examples repo, which declares `RAM : ORIGIN = 0x20000000, LENGTH = 192K` for an STM32F4 Discovery. That is an STM32F42x memory map. On an F407, 192 KB contiguous at `0x20000000` does not exist. We shipped that, and the simulator hid it from us. Not maliciously — it hid it by being generous. The board model provides enough contiguous RAM that the wrong linker script boots fine, prints fine, and exits clean. The same ELF, unchanged, tells three different stories: | Machine | Result | Why | | -------------------------------------------- | -------------------------------- | ------------------------------------------------------- | | Chiplab (Renode 1.16.1), `stm32f4_discovery` | boots, runs, prints, exits clean | its platform provides 256 KB contiguous at `0x20000000` | | QEMU 11.0.3, `netduinoplus2` (STM32F405) | instant lockup before `main` | provides the silicon-correct 128 KB | | Real STM32F407 | would bus-fault on the same push | `0x20020000`+ is reserved per DS8626 | The simulator that "worked" was the wrong one. QEMU's brutal `qemu: fatal: Lockup` was the honest answer, and it is the least friendly tool of the three. And `cargo build --release` printed `Finished` for all of it, because a linker script pointing the stack into reserved address space links perfectly cleanly. ### Embedded simulation limitations that no functional simulator will ever fix The four modes above are gaps in a model. These are different — they are things a functional instruction-set simulator is structurally not in the business of producing. | What you want | Why simulation can't give it | What you get instead | | --------------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------- | | Cycle counts, WCET | Renode advances virtual time from a configured MIPS rate, default 100 MIPS[^5] | a plausible-looking number derived from a config setting | | Analog behavior | no ADC nonlinearity, reference drift, or input impedance in the model | ideal converted values | | EMI, signal integrity | no physical layer exists to be noisy | perfect edges forever | | Power draw | no current model attached to peripheral state | nothing | | Real bus peers | sensor models implement the datasheet, not the part | clean ACKs, no clock stretching, no bad units | | Thermal | no die, no ambient | no derating, no shutdown | | Silicon errata | the model implements the manual, not the mistakes | correct behavior the chip does not have[^2] | That last row is the sharp one and it cuts both ways. A simulator is a model of the documentation. Errata are the list of places the silicon disagrees with its own documentation. By construction, a functional simulator implements the document, so it is closest to the chip exactly where the chip is least surprising. This is a large part of what hardware-in-the-loop rigs exist for: HIL puts the real device in the loop precisely because offline simulation "tests a model of the device, not the device itself, so it misses real timing, firmware and hardware behaviour."[^6] Which tests belong where is its own argument — [simulation vs HIL, and what belongs in CI](/blog/simulation-vs-hil-what-belongs-in-ci) lays out where I draw the line. ### A test that measures nothing is worse than no test This is the whole point, and it is not really about Renode limitations. It is about what a green check means. If I had no cycle counter at all, I would know I have no timing coverage, and I would go find some. Instead I had `assert!(cycles < BUDGET)` with `cycles = 0`, which is a green check that actively tells me timing is fine. It will stay green through every refactor that triples the real cost. That is a silent test failure: a test that passes for a reason unrelated to the thing it claims to check. ![Diagram showing a CI assertion passing because a simulated DWT cycle counter returned zero](/blog/what-simulation-cant-catch/image-3.webp) Four things I now do, all cheap: 1. Assert on what the model actually owns: register state, protocol bytes on a bus, control flow, whether the thing booted. Not on quantities the model fabricates. 2. Make every measurement prove its instrument first. `advanced=NO` should fail the build before `cycles < BUDGET` is ever evaluated. A counter that did not move is an error, not a zero. 3. Fail the build on the host log. Renode printed `non existing peripheral` and `Unhandled bits` for every single problem in this post. In most CI setups that output goes to a file nobody opens. Grep it. Treat those strings as failures unless explicitly allowlisted. 4. Read the platform file's memory map before you trust your linker script. Both of them are claims. Only one of them is going to be checked at runtime, and on the wrong board neither is. None of this makes me want to stop using simulation. Two probes, roughly thirty seconds each, found a real bug in our own repo and mapped four distinct classes of thing this board cannot tell me. That is an extremely good trade. But I would not have gone looking if I still believed the green check. The simulator will never volunteer which of its answers are made up. That part is your job. ### Update, 2026-08-04: the linker script is fixed We fixed it the same day this went out. `memory.x` now declares `LENGTH = 128K`, which is what an STM32F407 actually has at `0x20000000`, and the initial stack pointer moved to `0x20020000` — the real top of SRAM. Rebuilt and re-run, both machines now agree. Chiplab reports `Setting initial values: PC = 0x8000189, SP = 0x20020000` and prints `Hello world!`. So does QEMU's `netduinoplus2`, which refused to reach `main` at all before the fix. Every transcript above is from the broken build and stays exactly as it was. None of them is the reason we found it. The zeros, the accepted wait states, the dropped cache bits and the RAM that answered from reserved space all behaved identically before and after — which is the whole post. - **Repo & examples:** [github.com/veecle/chiplab](https://github.com/veecle/chiplab) - **Chiplab:** [veecle.ai/chiplab](https://veecle.ai/chiplab) ### Sources [^1]: Baldassari, François. "Profiling Firmware on Cortex-M." Interrupt by Memfault. https://interrupt.memfault.com/blog/profiling-firmware-on-cortex-m [^2]: STMicroelectronics. "STM32F405/407xx and STM32F415/417xx revision A device limitations," errata sheet, Doc ID 022183 Rev 1, section 2.1.1. https://d1.amobbs.com/bbs_upload782111/files_46/ourdev_678847KKGVKC.pdf [^3]: STMicroelectronics. RM0090, "STM32F405/415, STM32F407/417, STM32F427/437 and STM32F429/439 advanced Arm-based 32-bit MCUs" reference manual. https://www.cse.scu.edu/~dlewis/book3/docs/RM0090.pdf [^4]: STMicroelectronics. STM32F405xx/407xx datasheet (DS8626), memory map. https://hangpersonal.com/wp-content/uploads/2024/10/STM32F407-Memory-Mapping.pdf [^5]: Renode documentation. "Time framework." https://renode.readthedocs.io/en/latest/advanced/time_framework.html [^6]: Wirtek. "Hardware-in-the-loop testing for embedded systems." https://www.wirtek.com/blog/hardware-in-the-loop-testing-for-embedded-systems --- ## MIL, SIL, PIL, HIL for firmware people (https://veecle.ai/blog/mil-sil-pil-hil-for-firmware-people, 2026-08-04) Someone hands you a process document with four boxes and expects you to know what they mean. MIL, SIL, PIL, HIL. Nobody explains them, because in the room they came from, everybody already knows. I write firmware. Registers, interrupts, a UART. I do not have a plant model and I am never going to have one. Every page I found explaining **MIL SIL PIL HIL** was written for someone who does. So here is the translation. ### Where MIL SIL PIL HIL comes from The vocabulary is not automotive testing in general. It is model-based control design. You start with a physical thing you want to control — an engine, a motor, a brake. You write a math model of it, called the plant. Then you write the controller as another model, usually in Simulink, and simulate the two against each other in a closed loop. When the controller model behaves, you generate C from it and ship that. Every rung is a step in that pipeline: the same loop, one more piece swapped from model to real. That is what "in the loop" means. Not "under test" — in the control loop, with a simulated plant on the other side. Continental's writeup lays out the chain that way: physical model, implementation model, C compiled for the host, C compiled for the target.[^1] The ladder hangs off the V-model, the shape automotive settled on decades ago and baked into its standards.[^2] These four are rungs on the way back up the right side. None of that is stupid. It is a good process for the job it was built for. The job just is not yours. ### The four rungs, defined properly **MIL — model in the loop.** The controller model runs against the plant model inside the modeling environment. No generated code exists yet. You are testing whether the algorithm is right: thresholds, state transitions, gains.[^3] **SIL — software in the loop.** The generated (or hand-written) production code, compiled for and executed on your development computer, against the same simulated plant.[^4] Host compiler, host architecture. You are now running the artifact that ships, so codegen bugs, overflow, type casts and fixed-point errors show up.[^3] **PIL — processor in the loop.** The code is cross-compiled with the _target_ compiler and runs on the target processor — "or an equivalent instruction set simulator," in MathWorks' own words.[^4] The plant is still simulated on the host, stimulus shuttled to the target each sample interval. This rung catches faults caused by the target compiler or the processor architecture.[^1] **HIL — hardware in the loop.** The real ECU runs the real binary, wired to a real-time simulator faking everything around it: sensor signals, CAN and LIN traffic, power.[^5] Controller real, world simulated. That is why it catches timing, drivers, peripherals and buses that nothing above it can.[^3] Direction of travel: at MIL everything is a model. At HIL, only the world is. ![Diagram of the MIL SIL PIL HIL ladder showing what is simulated and what is real hardware at each rung](/blog/mil-sil-pil-hil-for-firmware-people/image-1.webp) ### The SIL confusion that wastes everyone's afternoon An embedded developer hears "software in the loop" and pictures the firmware binary running inside a simulator. Real ELF, virtual chip. Reasonable — that is what the words say in English. That is not what SIL means. In model-based design, SIL is the control algorithm compiled for the _host_ and linked against a plant model. No target instruction set anywhere near it. The thing the embedded developer was picturing is **PIL**. So two people say "we do SIL in CI" and mean different things. One means host unit tests against a math model. The other means the shipping firmware image booting on a virtual chip. Those catch nearly disjoint bugs, and the conversation goes nowhere until someone notices. | If someone says... | The MBD meaning | What a firmware dev pictured | Actual rung | | -------------------------------- | --------------------------------------- | ------------------------------------------- | ----------- | | "We run SIL" | Generated C on the host vs. plant model | The real ELF on a simulated chip | PIL | | "It passed software-in-the-loop" | Host-compiled numerical equivalence | Firmware booted and printed the right thing | PIL | | "We need HIL for this" | Real ECU, simulated vehicle | Any test involving a real board | HIL | | "MIL signed off" | Algorithm behaves against the plant | (nothing — no equivalent exists) | MIL | When a firmware person says "simulation," they usually mean PIL. ![Anakin and Padmé meme about the ambiguity of software-in-the-loop testing in firmware teams](/blog/mil-sil-pil-hil-for-firmware-people/image-2.webp) ### Does each rung earn its place in a firmware project? The honest column, which vendor pages skip because every rung is somebody's product. | Rung | What runs | Runs on | Simulated | Catches | Cannot catch | Earns its place in firmware? | | ---- | -------------------------- | --------------------------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------- | | MIL | Controller model | Modeling tool (Simulink) | Plant and everything else | Algorithm logic, thresholds, missing transitions | Anything about code, compiler or chip | Only with model-based design. Otherwise there is no model to run | | SIL | Generated or handwritten C | Host CPU, host compiler | Plant | Codegen bugs, overflow, casts, fixed-point error | Target timing, memory layout, interrupts, peripherals | Yes, as host unit tests. You have this and call it `cargo test` | | PIL | Target binary | Target CPU or instruction set simulator | Plant, and usually the board around the CPU | Target compiler bugs, architecture-specific math, stack depth, execution time, boot and peripheral logic | Analog behavior, electrical faults, real bus timing, anything off-chip | Yes. Most under-used rung in embedded | | HIL | Target binary | Real ECU, real silicon | The vehicle around it | Timing, drivers, electrical, bus, watchdog, the ugly stuff | Nothing you can afford to run a thousand times a day | Yes, as the last mile — not the default loop | Read the last column. If you do not do model-based design, MIL does not apply to you at all. There is no plant model, so there is no rung. Skipping it is not a coverage gap, it is a category mismatch. SIL collapses into host unit tests, which every firmware team already does some version of. And HIL is the last mile — essential, scarce, and [the wrong place to put your default feedback loop](/blog/hil-wont-scale-ai-era). Which leaves PIL. ### PIL is where firmware simulation lives Every automotive page I read treats PIL as a formality. AskUI calls it a stage that "sits between SIL and HIL in some workflows" and notes not all teams use it.[^3] OPAL-RT's SIL-versus-HIL guide does not mention it at all.[^5] PatSnap gives it two sentences.[^6] That is a hangover from where the vocabulary came from. In model-based design, PIL exists to prove the target compiler and FPU do not change your numbers before you go book the bench. A checkbox on the way to HIL. Look at what PIL mechanically is, though: your real firmware binary, cross-compiled with your real toolchain, executing on the target instruction set. Not a checkbox. For a firmware team that is the whole product. And MathWorks explicitly allows the target to be an instruction set simulator rather than silicon.[^4] So the sanctioned PIL setup includes the thing embedded people have been building for years under a different name: a virtual board. That is where a hosted target simulator like Chiplab sits on this ladder — a PIL rig with no eval board to book. The consequence is throughput. BTC Embedded, who sell PIL tooling, already argue for running PIL in CI as an always-on quality gate.[^7] That is not the ceiling. With an instruction set simulator there is no board to allocate, no flashing step and no queue, so the rung runs on every commit. Most firmware CI never gets there. It compiles, links, and stops — [which is where firmware CI usually ends](/blog/firmware-ci-ends-at-the-linker). PIL is the rung that fixes that, and the one the ladder undersells. Where exactly to draw the line between virtual target and bench I worked out separately, in [what belongs in CI versus on a HIL rig](/blog/simulation-vs-hil-what-belongs-in-ci). ### What the standard asks for Careful here. This is where posts start inventing clause numbers. The ZVEI best-practice guideline for safety-related automotive software restates the ISO 26262-6 software unit verification methods, and one is "back-to-back comparison test between model and code, if applicable" — applicable, it says, when a model of the software unit exists.[^8] That conditional does real work. The whole MIL-versus-SIL ritual is scoped to teams who have a model in the first place. MathWorks positions SIL and PIL as verification activities you can run against ISO 26262-6 and friends via a certification kit.[^4] BTC frames PIL as the way to test "as close to the target as possible" without jumping straight to HIL.[^7] Both are vendor framings, not quotes from the standard, and I am flagging them as such. Which ASIL requires which rung, I am not going to tell you. I do not have the text in front of me, and most confident answers online are copied from each other. ### What is the real difference between SIL and PIL? The compiler and the instruction set. SIL compiles your code with the host compiler and runs it on the host CPU. PIL cross-compiles with the target compiler and runs the object code on the target processor, or on an equivalent instruction set simulator.[^4] Same source, two machines. SIL proves the logic survived code generation. PIL proves it survives your toolchain and your chip. One place sources genuinely disagree: whether PIL requires physical silicon. MathWorks and the Continental paper both explicitly permit an instruction set simulator as the PIL target.[^4] [^1] BTC's definition assumes an evaluation board wired to a host PC.[^7] If you are writing a process document, pick one and say which. If you are reading someone else's, ask. ### So what do you call the thing you actually do If you run firmware on a virtual chip in CI, the automotive word for that is PIL, and saying so will save you an argument with a process engineer. But you do not owe the ladder your vocabulary. It describes a pipeline that turns block diagrams into C, and you are not running that pipeline. You have a binary, a chip, and one question: does this boot and do the right thing on the part I ship? Two rungs answer that. The other two describe a job you do not have. ### Sources [^1]: Narayanamurthy, Panditi, Schmidt, and Garrelfs (Continental AG), "MIL/SIL/PIL Approach: A New Paradigm in Model Based Development," MathWorks India Expo, July 2014. https://ww2.mathworks.cn/content/dam/mathworks/mathworks-dot-com/solutions/automotive/files/in-expo-2014/mil-sil-pil-a-new-paradigm-in-model-based-development.pdf [^2]: Aptiv, "What is the V-model in software development?" https://www.aptiv.com/en/insights/article/what-is-the-v-model-in-software-development [^3]: Seo, YouYoung. "HIL vs SIL vs MIL: The Full Testing Hierarchy." AskUI Blog, April 24, 2026. https://www.askui.com/blog-posts/mil-sil-hil-testing-hierarchy [^4]: MathWorks, "SIL and PIL Simulations," Embedded Coder documentation. https://www.mathworks.com/help/ecoder/ug/about-sil-and-pil-simulations.html [^5]: OPAL-RT, "Hardware in the loop vs software in the loop guide for engineers," May 13, 2025. https://www.opal-rt.com/blog/hardware-in-the-loop-vs-software-in-the-loop [^6]: PatSnap, "HIL vs SIL testing for autonomous vehicles." https://www.patsnap.com/resources/blog/articles/hil-vs-sil-testing-for-autonomous-vehicles [^7]: BTC Embedded Systems, "Processor-in-the-Loop (PIL)." https://www.btc-embedded.com/test-environments/processor-in-the-loop [^8]: ZVEI, "Best Practice Guideline: Software for Safety-Related Automotive Systems," January 2025. https://www.zvei.org/fileadmin/user_upload/Presse_und_Medien/Publikationen/2025/Januar/Best_Practice_Guideline_Software_for_Safety-Related_Automotive_Systems/Best-Practice-Guideline-Software-for-Safety-Related-Automotive-Systems_final.pdf --- ## QEMU for Cortex-M: what works, what doesn't, and why (https://veecle.ai/blog/qemu-for-cortex-m-what-works, 2026-08-04) Same ELF, same md5. Renode booted it, ran it, printed over UART, and exited clean. QEMU Cortex-M emulation killed it in one instruction. It printed `qemu: fatal: Lockup: can't escalate 3 to HardFault (current priority -1)`, then a register dump: ``` qemu: fatal: Lockup: can't escalate 3 to HardFault (current priority -1) ``` QEMU was right. Renode was wrong. That is the whole post, and it is not the conclusion most people expect, because QEMU is the simulator almost nobody reaches for on Cortex-M. ### Why QEMU locked up before `main` The firmware is the `bare-metal/stm32f4-discovery` example from Veecle's own examples repo. Rust, `#![no_std]`, `cortex-m-rt`, built with `cargo build --release`, which printed `Finished` and gave no warnings. QEMU has no STM32F4 Discovery machine, so the run used the closest thing it ships: `netduinoplus2`, an STM32F405 board. ``` $ qemu-system-arm -machine netduinoplus2 -cpu cortex-m4 \ -kernel hello-stm32f4-discovery -nographic -serial mon:stdio qemu: fatal: Lockup: can't escalate 3 to HardFault (current priority -1) R00=00000000 R01=00000000 R02=00000000 R03=00000000 R12=00000000 R13=2002ffe0 R14=fffffff9 R15=0800359c XPSR=41000003 -Z-- T handler ``` ![Terminal showing the QEMU Cortex-M lockup fatal error and register dump](/blog/qemu-for-cortex-m-what-works/image-1.webp) Six lines of output. No UART banner. Nothing from `main`. Here is what happened, and every step is a real Cortex-M rule. The board's linker script declares `RAM : ORIGIN = 0x20000000, LENGTH = 192K`. `cortex-m-rt` puts the initial stack pointer at the top of RAM, so the first word of the vector table is `0x20030000`. QEMU's `netduinoplus2` models an STM32F405, which has 128 KB of contiguous SRAM at `0x20000000` and reserved space above it — which is exactly what the silicon does.[^1] So the very first stack push lands in unmapped memory. `R13=2002ffe0` is the register dump telling you so: the stack pointer has decremented off the top of nothing. That raises a fault, and the CPU enters the HardFault handler. Whose own first instruction is: ``` 0800359c : 800359c: b580 push {r7, lr} 800359e: 466f mov r7, sp 80035a0: e7fe b.n 80035a0 ``` `R15=0800359c` is that first `push`. `R14=fffffff9` is an EXC_RETURN value, confirming the core was already in handler mode. The prologue push faults for the same reason the first one did, and a fault taken inside HardFault at priority -1 is lockup by definition. Dead before `main`, in about two instructions, with no diagnostic beyond a register dump. ![Diagram of the STM32F405 memory map showing the initial stack pointer landing in reserved space above the modeled SRAM](/blog/qemu-for-cortex-m-what-works/image-2.webp) The same ELF on Chiplab's Renode-backed `stm32f4_discovery` board wrote and read back `0xC0FFEE01` at every one of those addresses, including the one the stack pointer sits on: ``` == ram boundary probe: linker claims 192k at 0x20000000 == initial_sp_from_vector_table=0x20030000 addr=0x2001fffc name=sram2_last_word_128k wrote=0xc0ffee01 read=0xc0ffee01 ram_here=yes addr=0x20020000 name=just_past_128k wrote=0xc0ffee01 read=0xc0ffee01 ram_here=yes addr=0x20030000 name=past_192k_claim wrote=0xc0ffee01 read=0xc0ffee01 ram_here=yes ... == probe complete, no fault taken == ``` Three machines, one binary: | | Result | Why | | -------------------------------------------- | -------------------------------- | ------------------------------------------------------------- | | Chiplab (Renode 1.16.1), `stm32f4_discovery` | boots, runs, prints, exits clean | its platform provides exactly 256 KB contiguous at 0x20000000 | | QEMU 11.0.3, `netduinoplus2` (STM32F405) | instant lockup before `main` | provides the silicon-correct 128 KB | | Real STM32F407 | would bus-fault on the same push | 0x20020000+ is reserved per DS8626[^1] | The linker script is wrong. It looks copied from an STM32F42x, which really does have a third SRAM bank at `0x20020000`. We shipped it in a public examples repo and the friendly simulator hid it from us for months. Same gap as [Renode not modeling Cortex-M fault escalation](/blog/an-agent-fixed-a-hardfault), from the other side — and this time it cost us a real bug. ### What QEMU Cortex-M support actually covers QEMU's Arm CPU emulation is not the weak part. TCG supports Armv6-M, Armv7-M, Armv8-M and Armv8.1-M, plus the FP, MPU, Security and System Timer extensions.[^2] The M-profile core is in good shape. The boards are the gap. Here is the full STM32 machine list from `qemu-system-arm -machine help` on QEMU 11.0.3 — five machines, and none of them is an F4 Discovery: ``` b-l475e-iot01a B-L475E-IOT01A Discovery Kit (Cortex-M4) netduino2 Netduino 2 Machine (Cortex-M3) netduinoplus2 Netduino Plus 2 Machine (Cortex-M4) olimex-stm32-h405 Olimex STM32-H405 (Cortex-M4) stm32vldiscovery ST STM32VLDISCOVERY (Cortex-M3) ``` Two of the five are the same chip: `netduinoplus2` and `olimex-stm32-h405` are both STM32F405RGT6. QEMU's own documentation says plainly: "There are many other STM32 series that are currently not supported by QEMU."[^3] Peripheral coverage inside those machines is thin too. QEMU documents supported devices for its STM32 boards as ADC, EXTI, USART, SPI, SYSCFG, TIMER, and RCC (F4 only, reset and enable only). The _missing_ list, also from QEMU's docs, includes DMA, GPIO, I2C, RTC, CRC, DAC, Ethernet, the Flash Interface Unit, USB OTG, and both watchdogs.[^3] No GPIO controller means you cannot blink an LED. Not a criticism — a published fact to plan around. Post 08 goes [device-by-device on what Renode and QEMU each model](/blog/renode-vs-qemu-what-each-models). ### Why the MCU story is thin, structurally This is not neglect. It falls out of how QEMU is built and what it is for. QEMU's center of gravity is full-system virtualization of application processors. Its Arm docs say it supports "nearly fifty different machines" and immediately add that "even with fifty boards QEMU does not cover more than a small fraction of the Arm hardware ecosystem," with the recommendation that if you just want to run Linux you should use the synthetic `virt` board rather than any real hardware model.[^4] That advice is perfect for a kernel developer and useless for someone debugging an I2C driver. Board models are contributed per-machine, by whoever needed them. Nobody needed an F4 Discovery badly enough to write and maintain one. QEMU is honest about the consequence: "If it is not listed, then unfortunately your image will almost certainly not boot on QEMU."[^4] And QEMU makes no timing claims at all. Its `icount` feature counts instructions, and the docs are explicit that this "should not be confused with cycle accurate emulation - QEMU does not attempt to emulate how long an instruction would take on real hardware. That is a job for other more detailed (and slower) tools."[^5] If you need to know whether your control loop closes in time, QEMU is the wrong instrument and says so on the tin. ### What QEMU is good at here - **The CPU model.** Mature, wide M-profile coverage, actively maintained.[^2] - **gdbstub.** `-s -S` gives you a remote gdb target that lets you "debug guest code in the same way that you might with a low-level debug facility like JTAG on real hardware."[^6] No probe, no wiring. - **Semihosting.** Including the trick where test code exits and reports its own pass/fail status through a semihosting call — QEMU's own test suite uses it.[^7] - **The Arm reference machines.** `mps2-an385` through `mps2-an521`, `musca-a`, `musca-b1`, and `microbit`. These are the well-covered ones, because they are Arm's own platforms rather than vendor silicon. - **CI ubiquity.** Zephyr ships `qemu_cortex_m3` as a first-class board, emulating the TI LM3S6965, with a disclaimer I wish more simulators printed: "This board configuration makes no claims about its suitability for use with an actual ti_lm3s6965 hardware system, or any other hardware system."[^8] It is an execution environment for tests, not a stand-in for a board. - **It is everywhere.** One `brew install` or `apt install`, no license server, no signup. ### So what do you reach for instead? Depends on the job. There's no single winner. | If you need | Reach for | Caveat | | ------------------------------------------------- | --------------------------- | --------------------------------------------- | | Your exact vendor board, peripherals, multi-node | Renode | no cycle accuracy; fault escalation gaps | | Arm reference IP, Corstone, TrustZone bring-up | Arm FVP / Virtual Hardware | functionally accurate, not cycle-accurate[^9] | | One vendor's silicon, deep fidelity | That vendor's own simulator | locks you to the vendor | | Hobby-tier wiring and a blinking LED in a browser | Wokwi | not a CI target | | Arch-level test execution, gdb, semihosting | QEMU | board coverage is the bottleneck | ![Decision tree diagram for choosing between Renode, Arm FVP, QEMU and real hardware for Cortex-M work](/blog/qemu-for-cortex-m-what-works/image-3.webp) Renode is where most people land for board-level work, and not because of marketing. The BlueOS kernel project has a public issue proposing exactly that switch, with the reason stated flatly: "QEMU lacks extensive support for cortex-m series devices and built-in performance analysis tools on embedded MCU environment."[^10] Post 21 works through the [full field of Renode alternatives](/blog/renode-alternatives), and post 07 puts [Renode and QEMU on the same firmware](/blog/renode-vs-qemu-same-firmware) side by side. Chiplab runs Renode 1.16.1 behind an MCP server, which is how the probe above ran with nothing installed locally — and it inherits the same over-generous memory map. ### Is there a QEMU STM32F4 Discovery machine? No. There are five STM32 machines in `qemu-system-arm -machine help` and none of them is an F4 Discovery. The nearest relatives are `netduinoplus2` and `olimex-stm32-h405`, both STM32F405RGT6 — same core, same 128 KB of SRAM, different board wiring. `netduinoplus2` is not my board. It doesn't have my LEDs, my clock tree, or my USART pinout. It got the one thing right that mattered, refused to run my firmware, and said so in one ugly line with no stack trace and no advice. The board that pretended to be mine ran it happily for months. ### Update, 2026-08-04: QEMU was right, and now it runs it We fixed the linker script. `RAM` is declared `128K` now, the initial stack pointer sits at `0x20020000`, and the same example rebuilt without ceremony. ``` $ qemu-system-arm -machine netduinoplus2 -cpu cortex-m4 \ -kernel hello-stm32f4-discovery -nographic -serial null -serial mon:stdio Hello world! ``` That is the whole output, on the second serial, because `netduinoplus2` wires `-serial mon:stdio` to USART1 and this firmware talks on USART2. No lockup, no register dump. The machine that spent this entire post looking like the broken one was the only one telling the truth, and it runs the corrected binary fine. Chiplab agrees, for the record: `SP = 0x20020000`, `usart2: Hello world!`. It agreed before the fix too. That was the problem. ### Sources [^1]: STMicroelectronics. STM32F405xx/407xx datasheet (DS8626), memory map: SRAM 112 KB at 0x20000000, `0x2002 0000 - 0x3FFF FFFF` reserved, CCM data RAM at 0x10000000. https://hangpersonal.com/wp-content/uploads/2024/10/STM32F407-Memory-Mapping.pdf [^2]: QEMU Project. "M-profile CPU architecture support," Arm CPU architecture support. https://www.qemu.org/docs/master/system/arm/emulation.html [^3]: QEMU Project. "STMicroelectronics STM32 boards (`netduino2`, `netduinoplus2`, `olimex-stm32-h405`, `stm32vldiscovery`)." https://www.qemu.org/docs/master/system/arm/stm32.html [^4]: QEMU Project. "Arm System emulator." https://www.qemu.org/docs/master/system/target-arm.html [^5]: QEMU Project. "TCG Instruction Counting." https://www.qemu.org/docs/master/devel/tcg-icount.html [^6]: QEMU Project. "GDB usage." https://www.qemu.org/docs/master/system/gdb.html [^7]: QEMU Project. "Semihosting," Emulation. https://www.qemu.org/docs/master/about/emulation.html [^8]: Zephyr Project. "QEMU Emulation for ARM Cortex-M3." https://docs.zephyrproject.org/latest/boards/qemu/cortex_m3/doc/index.html [^9]: Arm. "Arm FVP Simulation Models," Arm Virtual Hardware documentation. https://arm-software.github.io/AVH/main/simulation/html/index.html [^10]: Eplankton. "Idea: Introduce Renode as an alternative to QEMU," vivoblueos/kernel issue #13, August 1, 2025. https://github.com/vivoblueos/kernel/issues/13 --- ## Renode alternatives: what would actually make you switch (https://veecle.ai/blog/renode-alternatives, 2026-08-04) The best-ranked comparison I found while searching for Renode alternatives is hosted on the blog of a company that sells industrial panel PCs.[^1] Its table lists Renode's license as Apache 2.0. Renode is MIT, and has been for as long as the README has said so.[^2] That one wrong cell tells you everything about how the page was made. Nobody opened the repository. Nobody ran the tool. The article ends with a product block: "Recommended hardware — Run it on an industrial Panel PC." It's an ad with a comparison table stapled to the front. I'm going to write the opposite of that, with an obvious conflict of interest I should declare in the second paragraph rather than the footer: **we build on Renode.** Which means this post is only worth writing if it's fair to Renode, and only worth reading if it tells you when to leave. A rigged comparison from a company that depends on the incumbent is worth less than nothing. Organized by job, not by tool. For most board-level Cortex-M work, the answer to "what should I switch to" is _don't_. ![Anakin and Padmé meme about Renode alternatives comparison articles written by people who never ran the tools](/blog/renode-alternatives/image-1.webp) ### The jobs, and which tool actually owns each one An embedded simulator comparison that lists tools alphabetically is useless: these things do not compete, they overlap at the edges and own different centers. **Board-level firmware simulation and multi-node systems.** This is Renode's home turf and it isn't close. Text platform files describe peripherals and wire them to buses and interrupt lines.[^5] The whole thing is deterministic. You can run several nodes in one emulation and have them talk. There's a Robot Framework test integration in the box so CI gets pass/fail rather than "it didn't crash."[^7] If your job is "does this firmware boot and behave on this board, every pull request," you are already using the right tool. **Raw CPU and ISA emulation, Linux-class targets.** QEMU. Not a consolation prize — for booting a kernel on Cortex-A this is the industry default and Renode isn't trying to take it. The trouble starts when people carry that reputation down to microcontrollers. QEMU's own STM32 page lists five machines: one F1, one F2, two F405 boards, and an L475 IoT node.[^9] No F4 Discovery. The shared "missing devices" list includes GPIO, DMA, I2C, CAN, and the RTC.[^9] Post 20 on [what QEMU actually covers for Cortex-M](/blog/qemu-for-cortex-m-what-works) goes through the gaps chip by chip, and [what Renode and QEMU each model](/blog/renode-vs-qemu-what-each-models) covers the Renode vs QEMU split at the peripheral level. Short version: QEMU is a superb CPU emulator with board models attached as an afterthought. **Hobby, teaching, and a link you can send someone.** Wokwi and Tinkercad Circuits. Renode cannot do the one thing these do: produce a URL that opens a working circuit in a stranger's browser. Tinkercad is free and aimed squarely at learning;[^15] Wokwi runs unlimited simulations on a €0 Community tier and sells CI minutes on Pro.[^14] If your actual requirement is a shareable demo or a classroom, Renode is the wrong shape of tool and no amount of fidelity fixes that. I wrote the reverse trip — [Wokwi alternatives for professional firmware work](/blog/wokwi-alternatives-professional-firmware) — for people going the other way. **Vendor-blessed fidelity.** Keil and IAR ship simulators inside the IDE, and Arm ships Fast Models and the Fixed Virtual Platforms behind Arm Virtual Hardware. Arm's own words for these are "functionally accurate simulation models of Arm-based Cortex-M CPUs and Corstone-3xx subsystems"[^10] and "functionally accurate programmer's view models."[^12] That's Arm's claim, not mine, and note what it doesn't say. Cost: free via Arm Tools Artifactory for evaluation, open source, and non-commercial use; no extra charge with MDK-Professional; AWS AMI billed as infrastructure.[^11] The real pull here is provenance, not accuracy. Arm models the IP, so if you need a model somebody will sign next to, that's the aisle. **Cycle-level micro-architecture research.** gem5, and this is where the fidelity conversation gets embarrassing for everyone. gem5's own official tutorial deck says, in bold, "gem5 is not a cycle-accurate microarchitecture model," and points at RTL simulation and commercial products like Arm Cycle Models for work that needs it.[^13] So the tool everybody names when they say "I need cycle accuracy" disclaims cycle accuracy. If you genuinely need it, you need RTL, and that is a different project with a different budget. **Running firmware in CI, or from an agent.** Renode has a GitHub Action and a test framework, so CI is solved as long as a human wrote the harness first.[^7][^17] "An agent decides at inference time to compile something and needs a board to run it on" is a different requirement — that's the one we work on with Chiplab, which is Renode-based and hosted, so the thing you're buying is that somebody else runs the infrastructure.[^16] Not a replacement for Renode. It is Renode, with an API and an on-call rotation. ![Diagram mapping six embedded firmware jobs to the simulator that owns each one, from Renode to QEMU to Arm Fixed Virtual Platforms](/blog/renode-alternatives/image-2.webp) ### The embedded simulator comparison table Timing column wording is deliberate. Where a vendor makes the claim, it's attributed to the vendor. | Tool | Genuinely best at | License / cost | Timing fidelity claim | MCU & board coverage | CI story | Pick this when | | ----------------------------------------- | -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | --------------------------------------------------------- | | **Renode** | Board-level firmware, multi-node systems, deterministic reruns | MIT, free; Antmicro sells commercial support and new platforms[^2] | No cycle-accurate mode. Virtual time advances against a CPU performance setting in MIPS, default 100[^3] | Broad and community-extensible; the docs are candid that your case "may require extending the provided hardware description / models"[^4] | Robot Framework integration and a GitHub Action[^7][^17] | Firmware has to survive a pull request without a bench | | **QEMU** | Linux-class Arm and raw ISA emulation | Open source, free | Explicitly not cycle-accurate: "QEMU does not attempt to emulate how long an instruction would take on real hardware"[^8] | Five STM32 machines total; GPIO, DMA, I2C, CAN missing from the shared device list[^9] | Everywhere already; a plain binary you can script | Booting a kernel, or emulating an ISA rather than a board | | **Wokwi** | A working circuit at a URL, plus a light CI path | €0 Community tier; Hobby from €5.60/mo; Pro €20/seat/mo with 2000 CI minutes[^14] | Not claimed | Arduino, ESP32, RP2040, some STM32 | `wokwi-ci` GitHub Action on paid tiers[^14] | The deliverable is a link somebody else can open | | **Tinkercad Circuits** | Teaching, first-circuit, zero-install | Free web app[^15] | Not claimed | Arduino-class hobby parts | None | You are teaching, not shipping | | **Arm FVP / Virtual Hardware, Keil, IAR** | Vendor provenance and pre-silicon Arm IP | Free via Arm Tools Artifactory for eval/OSS/non-commercial; bundled with MDK-Professional; AWS AMI billed as infra[^11] | Arm's term is "functionally accurate," for both FVPs and Fast Models[^10][^12] | Arm reference subsystems and Corstone; whatever your IDE vendor supports | Built for CI and MLOps pipelines, scriptable binaries | You need a model with a vendor's name on it | | **gem5** | Micro-architecture research, cache and pipeline exploration | Open source | Its own tutorial: "gem5 is not a cycle-accurate microarchitecture model"[^13] | Research platforms, not production dev boards | Not the point of the tool | You are writing a paper about a pipeline | ### Reasons you would actually switch away from Renode If one of them is your situation, go. - **You need a defensible cycle count.** Renode has no cycle-accurate mode; it advances virtual time against a MIPS number you configure, default 100.[^3] That's a budget, not a pipeline. Nothing in the open-source set gives you cycles — see [what cycle-accurate actually means](/blog/what-cycle-accurate-actually-means) before you spend money on this. - **The board does not exist yet and you have no platform file.** Renode needs a `.repl` describing your peripherals and their addresses.[^5] Writing one for a chip nobody has modeled is real work. If Arm already ships an FVP for the exact subsystem, take it. - **A peripheral your firmware actually touches isn't modeled in your specific chip.** Check first. But "modeled by the vendor sim, not by Renode" is a legitimate reason to use the vendor sim for that one thing. - **The deliverable is a shareable browser link.** Renode will never be that. - **You need vendor-blessed sign-off.** Provenance is a procurement requirement, not a technical one. - **Linux-class targets.** QEMU. - **You want somebody else to operate it.** Self-hosting a simulator is infrastructure. Hosted options exist, including ones built on Renode. ### Reasons that look like reasons but are not This section requires having used the tool. **"Renode is too slow."** By default Renode deliberately slows down when virtual time starts outrunning real time. There is a flag — `AdvanceImmediately` — that tells it to run as fast as the host allows.[^3] People benchmark the default and conclude the tool is slow. **"I need cycle accuracy."** Almost always you need _deterministic ordering_, which Renode already gives you. Or you need a duration measured on silicon, which no simulator on this page will give you. And if a specific block really does need cycle-level behavior, Renode co-simulates HDL through Verilator or Questa over DPI — you keep the system model and drop in the RTL for the one block that matters.[^6] Switching frameworks to get that is strictly worse than not switching. **"My chip isn't supported."** Platform descriptions are text files you can write and `using`-include.[^5] Or you pay Antmicro, who wrote the framework and sells exactly this service.[^2] Rewriting your test suite against a different simulator is more work than modeling one UART. **"QEMU is the standard, everyone has it."** For Cortex-A, sure. For your F4, QEMU does not ship a machine for your board and does not model GPIO.[^9] **"It's open source, so there's no support."** MIT license, commercial support from the vendor who maintains it.[^2] That's the same deal as most enterprise infrastructure. **"The UI is clunky."** It's a monitor console you drive from a script. Ergonomics is a real complaint; it is not a fidelity argument, and migrating a verification stack over aesthetics is how teams lose a quarter. ![Chart plotting embedded simulators by modeling fidelity against setup effort, showing cycle accuracy only begins at RTL simulation](/blog/renode-alternatives/image-3.webp) ### Are there any real Renode alternatives? For board-level Cortex-M work with peripherals, in 2026, no. QEMU wins on CPU coverage and loses on boards. gem5 wins on micro-architecture and isn't a firmware tool. The browser simulators win on approachability. The Renode license being MIT removes the last reason people usually go shopping. What you should actually go shopping for is the thing around the simulator: who operates it, who writes the platform file for your chip, who gets paged when CI can't reach it. Those are procurement questions with real answers, and none of them are "use a different emulator." If you searched "Renode alternatives" because Renode annoyed you this week, the fix is probably a `.repl` file and an afternoon. If you searched it because your firmware needs a number in nanoseconds that a court could look at, no tool on this page is your answer and you should stop reading listicles written by panel PC vendors. ### Sources [^1]: Industrial Monitor Direct, "ARM Cortex-M Simulators QEMU Renode Keil Comparison." https://industrialmonitordirect.com/de/blogs/knowledgebase/arm-cortex-m-simulators-qemu-renode-keil-comparison [^2]: Renode, GitHub repository README, "License & contributions" and "Commercial support." https://github.com/renode/renode [^3]: Renode documentation, "Time framework." https://renode.readthedocs.io/en/latest/advanced/time_framework.html [^4]: Renode documentation, "Supported boards." https://renode.readthedocs.io/en/latest/introduction/supported-boards.html [^5]: Renode documentation, "Describing platforms." https://renode.readthedocs.io/en/latest/basic/describing_platforms.html [^6]: Renode documentation, "Co-simulating with an HDL simulator." https://renode.readthedocs.io/en/latest/advanced/co-simulating-with-an-hdl-simulator.html [^7]: Renode documentation, "Testing with Renode." https://renode.readthedocs.io/en/latest/introduction/testing.html [^8]: QEMU developer documentation, "TCG Instruction Counting." https://www.qemu.org/docs/master/devel/tcg-icount.html [^9]: QEMU documentation, "STMicroelectronics STM32 boards." https://www.qemu.org/docs/master/system/arm/stm32.html [^10]: Arm, "Arm Virtual Hardware — Arm FVP Simulation Models" documentation. https://arm-software.github.io/AVH/main/simulation/html/index.html [^11]: Arm, "Virtual Hardware: Accelerate Software Development" (Costs section). https://www.arm.com/products/development-tools/simulation/virtual-hardware [^12]: Arm, "Fast Models." https://www.arm.com/products/development-tools/simulation/fast-models [^13]: gem5, ASPLOS 2017 gem5 tutorial slides. https://www.gem5.org/assets/files/ASPLOS2017_gem5_tutorial.pdf [^14]: Wokwi, "Pricing Plans." https://wokwi.com/pricing [^15]: Autodesk Tinkercad, "Circuits." https://www.tinkercad.com/circuits [^16]: Chiplab documentation, overview. https://chiplab.veecle.ai/mcp/resources/docs/overview.md [^17]: Antmicro, "Renode GitHub Action for automated testing on simulated hardware." https://antmicro.com/blog/2024/10/renode-github-action-for-automated-testing-in-simulation --- ## Firmware CI ends at the linker (https://veecle.ai/blog/firmware-ci-ends-at-the-linker, 2026-08-04) ``` MEMORY { FLASH : ORIGIN = 0x08000000, LENGTH = 1024K RAM : ORIGIN = 0x20000000, LENGTH = 192K } ``` That is `memory.x` from the STM32F4 Discovery example in Veecle's own public examples repo, and our firmware CI never noticed that the RAM line is wrong. An STM32F407 has 128 KB of contiguous SRAM at `0x20000000` — 112 KB of SRAM1 plus 16 KB of SRAM2 — and everything from `0x20020000` up is marked Reserved in the datasheet memory map.[^1] [^2] The extra 64 KB the chip really does have sits at `0x10000000`, on a different bus. `cargo build --release` printed `Finished` and said nothing. We shipped that. It built clean everywhere, because the pipeline around it compiled, linked, and stopped — where most embedded CI pipelines stop. ### The linker did exactly its job `cortex-m-rt` places the call stack at the end of the `RAM` region unless you say otherwise.[^3] `LENGTH = 192K` at origin `0x20000000` therefore puts the vector table's first word — the initial stack pointer — at `0x20030000`. That is documented behavior, not a bug, and the crate is explicit that it "expects the user, or some other crate, to provide the memory layout of the target device."[^3] The linker is equally explicit about its own contract. GNU `ld`'s manual says the `MEMORY` command "describes the location and size of blocks of memory in the target," and that the linker "will set section addresses based on the memory regions, and will warn about regions that become too full."[^4] Warn about regions that become _too full_. The linker checks that your sections fit inside the numbers you handed it. It has no opinion about whether those numbers describe a real chip. Declare 192 KB where 128 KB exists and it's perfectly happy — you gave it more room, not less. So: the compiler checks types, the linker checks that sections fit the memory map it was given, and _nothing in the toolchain checks that the memory map matches the silicon_. No stage can. The only thing that catches this is executing the binary against something that models the chip's address space — precisely the stage most firmware CI does not have. ### Where each stage actually stops A normal embedded CI pipeline, stage by stage, and what each one can prove. This table is the whole post. | Stage | What it actually checks | What it structurally cannot catch | Hardware? | | ------------------ | ------------------------------------------------------ | --------------------------------------------- | ----------------- | | Compile | Types, syntax, API signatures, borrow rules | Whether any address is real | No | | Static analysis | Known-bad patterns, undefined behavior, some data flow | A memory map that lies; it isn't a pattern | No | | Unit tests on host | Pure logic: parsers, state machines, encoders | Everything target-specific — this runs on x86 | No | | Link | That every section fits the regions declared | Whether those regions exist on the chip | No | | Flash | That the programmer could write the image | Whether it survives reset | Yes | | Execute on target | Boot, stack, clocks, peripherals, does it run | Only what the model is honest about | Yes, or simulated | | Integration / HIL | Real timing, electrical behavior, sensors | Little. It's just slow, scarce, expensive | Yes | Four of the seven stages need no hardware at all, and all four are blind to a wrong memory map. The first stage that can see it is the first stage that needs a chip, real or modeled. That's the whole shape of the problem, and why "firmware build vs run" isn't pedantry. ![Diagram of a firmware CI pipeline showing the bright red line after the linker stage where most embedded CI pipelines stop](/blog/firmware-ci-ends-at-the-linker/image-1.webp) ### Why firmware CI stops there, and why it isn't laziness I want to be fair here, because the smug version of this post is also the wrong one. A 2025 literature review of embedded DevOps synthesized 20 academic and industrial sources on CI/CD in firmware development. Across nearly all the empirical studies it reviewed, "hardware access was the single largest blocker" to continuous delivery. It also reports interviews across CPS organizations describing CI failures from hardware/software integration delays, flakiness in HIL setups, and hardware lockouts.[^5] That is not a survey saying "N percent of pipelines never run the binary." No such number exists that I'd trust and I'm not inventing one. What the literature supports is narrower: the hardware stage is the stage that breaks, and teams route around it. Semaphore's guide for embedded teams describes the same split from the vendor side. Fast feedback first — unit tests, static analysis, build validation, simulation or emulation via QEMU or Renode. Hardware-in-the-loop only after those pass, on USB-connected device farms with network-controlled power switches and Raspberry Pis as device controllers.[^6] Sensible advice. Also a shopping list. The running cost isn't theoretical either. An engineer maintaining a self-hosted HIL CI runner for an nRF52 published the real bill: roughly $100 of Pi and dev board, then the part nobody budgets for — OS patching, SD cards replaced every year or two under CI write load, and "USB connection instability — the board occasionally drops off and requires a physical reconnect."[^7] Somebody has to walk over and reseat a cable. That is the real reason your firmware continuous integration ends at the linker. Not laziness. Physical maintenance nobody owns. Simulation moves the execute stage back inside reach of CI without pretending it replaces the lab — I've argued that layering in [what belongs in CI versus what belongs on a bench](/blog/simulation-vs-hil-what-belongs-in-ci) and [HIL won't scale to the AI era](/blog/hil-wont-scale-ai-era). It stops garbage from reaching the lab. ### What happens the moment you run it Same ELF, md5 `90438763f9670f576bf33996d7ff6bd9`. QEMU has no STM32F4 Discovery machine, so this is `netduinoplus2`, QEMU's STM32F405 board: ``` $ qemu-system-arm -machine netduinoplus2 -cpu cortex-m4 \ -kernel hello-stm32f4-discovery -nographic -serial mon:stdio qemu: fatal: Lockup: can't escalate 3 to HardFault (current priority -1) R00=00000000 R01=00000000 R02=00000000 R03=00000000 R12=00000000 R13=2002ffe0 R14=fffffff9 R15=0800359c XPSR=41000003 -Z-- T handler ``` `R13=2002ffe0` is the stack pointer, above the 128 KB of SRAM QEMU's F405 model provides, because the linker script put it at `0x20030000`. `R15=0800359c` is the HardFault handler's first instruction; `R14=fffffff9` says the CPU is already in handler mode. The first push hit unmapped memory, faulted, entered HardFault, whose own first instruction is `push {r7, lr}`, which faulted again. QEMU's own diagnosis is `Lockup: can't escalate 3 to HardFault (current priority -1)` — a fault inside HardFault at priority -1 is lockup by definition. Dead before `main`. Ugly, unhelpful, no diagnosis, completely correct. Milliseconds of execution found what four stages of static checking could not. ![Terminal screenshot of QEMU locking up before main with the stack pointer at 0x2002ffe0](/blog/firmware-ci-ends-at-the-linker/image-2.webp) ### Executing it is necessary. It is not sufficient. Here's the part that should stop you feeling good. The same ELF runs fine on a simulated STM32F4 Discovery board. Not "mostly fine" — it boots, prints, exits clean. I ran a probe on Chiplab that writes `0xC0FFEE01` to a list of addresses and reads each one back: ``` == ram boundary probe: linker claims 192k at 0x20000000 == initial_sp_from_vector_table=0x20030000 addr=0x20000100 name=sram1_start+0x100 wrote=0xc0ffee01 read=0xc0ffee01 ram_here=yes addr=0x2001fffc name=sram2_last_word_128k wrote=0xc0ffee01 read=0xc0ffee01 ram_here=yes addr=0x20020000 name=just_past_128k wrote=0xc0ffee01 read=0xc0ffee01 ram_here=yes addr=0x20028000 name=sram3_region_f42x wrote=0xc0ffee01 read=0xc0ffee01 ram_here=yes addr=0x20030000 name=past_192k_claim wrote=0xc0ffee01 read=0xc0ffee01 ram_here=yes ... addr=0x10000000 name=ccm_data_ram wrote=0xc0ffee01 read=0x00000000 ram_here=no == probe complete, no fault taken == ``` The board model hands out RAM at `0x20020000` and `0x20030000`, which on an F407 is reserved address space. It does not map `0x10000000`, where the F407's 64 KB of CCM data RAM actually lives.[^1] [^2] The memory map is inverted relative to the chip in both directions. | Address range | Real STM32F407 (RM0090 / DS8626) | This simulated board | | ----------------------- | -------------------------------- | -------------------- | | 0x20000000 – 0x2001FFFF | SRAM1 + SRAM2, 128 KB | RAM, present | | 0x20020000 – 0x2003FFFF | Reserved | **RAM, present** | | 0x20040000 and up | Reserved | not mapped | | 0x10000000 | **CCM data RAM, 64 KB** | **not mapped** | Scoreboard on one binary: the toolchain said nothing, the honest simulator killed it instantly, the generous one ran it to completion, and a real F407 would bus-fault on that same first push. The simulator that "worked" is the one that was wrong. Execution catches strictly more than linking. It catches exactly as much as the model is honest about and not one byte more — the same boundary [a silent runtime failure that compiles clean](/blog/the-build-passed-why-doesnt-it-run) sits on, seen from the other side, and worth reading next to [what simulation quietly misses](/blog/what-simulation-cant-catch). ![Diagram comparing the declared 192K memory map, the real STM32F407 SRAM layout, and the simulated board's memory map](/blog/firmware-ci-ends-at-the-linker/image-3.webp) ### So what do I actually do about it? Run the binary in CI, on the cheapest thing that will run it, then read that thing's memory map against the datasheet once. Not per commit — once per board, when you adopt it. The `MEMORY` block in your linker script and the address table in the chip's datasheet are two documents that should agree, and nobody in your pipeline is comparing them. We found ours because a second simulator disagreed with the first one. That's a stupid way to find a bug and I'll take it. ### Update, 2026-08-04: fixed, and the build looked identical `memory.x` now says `LENGTH = 128K`. The stack pointer starts at `0x20020000`, inside RAM the chip actually has. The comment above it explains why nobody should raise it back to 192K. Both machines now run it: Chiplab prints `Hello world!` with `SP = 0x20020000`, and QEMU's `netduinoplus2` prints `Hello world!` instead of locking up before `main`. Here is the part that belongs in this post specifically. `cargo build --release` said ``Finished `release` profile [optimized] target(s)`` before the fix and after it. Same command, same exit code, same silence. The build was never going to be the thing that noticed, and adding a stage that compiles harder would not have helped. Running it did. ### Sources [^1]: STMicroelectronics, "RM0090: STM32F405/415, STM32F407/417, STM32F427/437 and STM32F429/439 advanced Arm-based 32-bit MCUs," Rev 19. https://www.cse.scu.edu/~dlewis/book3/docs/RM0090.pdf [^2]: STMicroelectronics, STM32F405xx/STM32F407xx datasheet (DS8626), memory map extract. https://hangpersonal.com/wp-content/uploads/2024/10/STM32F407-Memory-Mapping.pdf [^3]: `cortex-m-rt` crate documentation, "Requirements → `memory.x`" and "`_stack_start` / `_stack_end`." https://docs.rs/cortex-m-rt [^4]: GNU Binutils, "3.7 MEMORY Command," GNU linker (`ld`) manual. https://sourceware.org/binutils/docs/ld/MEMORY.html [^5]: "Embedded DevOps: A Survey on the Application of DevOps Practices in Embedded Software and Firmware Development," arXiv:2507.00421, 2025. https://arxiv.org/html/2507.00421v1 [^6]: Miloravac, Pete. "What CI/CD strategies work for embedded or IoT projects that require hardware testing?" Semaphore, April 2026. https://semaphore.io/blog/what-ci-cd-strategies-work-for-embedded-or-iot-projects-that-require-hardware-testing [^7]: "Firmware HIL CI Pipeline." Reverse to Build devlog series #5. https://reversetobuild.com/devlogs/firmware-hil-ci-pipeline --- ## Every chip is an island (https://veecle.ai/blog/every-chip-is-an-island, 2026-08-04) There is no `docker run` for hardware. Not because nobody tried. Embedded toolchain fragmentation is not an accident or a gap somebody forgot to fill. It is the accumulated output of decades of decisions that each made perfect sense to the company making them. Together, they produce an industry where "run this binary on that chip" is not a thing you can type. Software got out of this. Compilers, then package managers, then containers, then CI, then cloud. Each layer swallowed a class of difference and handed back one interface. Embedded never had that. A 2025 survey of DevOps practice in firmware puts it dryly: "Unlike the standardized cloud DevOps stacks (e.g., Docker, Kubernetes), embedded pipelines depend heavily on vendor-specific compilers, simulators, and debuggers."[^1] Dryly is doing a lot of work in that sentence. ### What embedded toolchain fragmentation actually looks like Take a working blinky on an STM32F407 Discovery and move it to an nRF52840 DK. Not a port to a different architecture — both are Cortex-M4F. Same instruction set, same core vendor, same language. The memory map changes: different flash origin, different RAM size, different reserved holes. So the linker script changes, and the startup code with it. The vector table and the interrupt list are per-device. The peripheral registers change completely — different UART, different names, different bit meanings. The HAL changes, because ST ships `stm32f4xx-hal` and Nordic's world is `nrfx` and neither has heard of the other. The clock tree changes, and it is not a parameter, it is a different set of concepts. Then you leave the source tree. The probe changes: ST-LINK on one board, J-Link on the other. The flashing tool changes. The board definition changes. On an RTOS, the driver bindings change. And at the end of it the board is a physical object somebody has to be holding. Ten layers. Every one forks per vendor, and most fork per part number. ![Diagram contrasting the converging software stack with embedded toolchain fragmentation into per-vendor silos](/blog/every-chip-is-an-island/image-1.webp) ### Why `docker run` works, and why there is no docker for firmware This is the part people hand-wave, so let me not. `docker run` works because of two things underneath it, and both are load-bearing. The first is one kernel. A container is not a virtual machine — Docker uses Linux kernel namespaces to carve an isolated workspace out of the host you are already running on.[^2] The container brings its own userland and borrows the kernel's system call interface, which is stable, documented, and the same on every Linux box on earth. That is the single ABI the whole abstraction rests on. The second is that CPU architecture is a short enum. When it varies, containers do not abstract it — they cheat. The OCI image index ships several manifests in one artifact, each tagged with a platform, and the client picks the matching one at pull time.[^3] That is not portability. That is building it N times and choosing. It works because N is small. Now try both on a microcontroller. There is no kernel, because there is no operating system. The thing your binary talks to is the silicon, and the ABI _is_ the memory map plus the peripheral register set. That is not a detail underneath the abstraction. It is the abstraction's entire surface, and it differs on every part. The cheat does not rescue you either. OCI's platform axis is a short enum. The embedded equivalent is a part number, times a board, times a wiring diagram — two boards carrying the same die can differ in crystal, external flash, and pin mux, and that is enough to make the same ELF behave differently. `docker run` also assumes the machine is fungible. Any host with the right kernel will do. A board is not fungible. It is one object, in one building, that one person is using right now — the whole argument in [why HIL benches won't scale in the AI era](/blog/hil-wont-scale-ai-era). ### Why each layer resisted standardization It is tempting to write this off as inertia. It isn't. Every layer has a real reason, and most of them are economic rather than technical. **Differentiation is the product.** A vendor sells you the peripheral set. Low-power modes, the DMA fabric, the crypto block, the way the timers chain — that is the pitch. A uniform interface across vendors turns the part into a commodity, which is exactly what nobody selling parts wants. **The silicon predates the software standard.** Reference manuals get written by teams shipping a die on a tape-out schedule. The machine-readable description arrives later as a retrofit, inheriting whatever the hardware team already decided. **Support burden.** A standard interface is a promise to support every combination built out of it. Vendors would rather bless one IDE and one probe and call the rest unsupported. **Certification.** In ISO 26262 or IEC 61508 work, a qualified toolchain is an asset with paperwork attached. Swapping the compiler is not an afternoon, it is a project. Frozen layers do not converge. **And the memory map cannot be abstracted without lying.** This one isn't about incentives. You cannot present 128 KB of RAM as 192 KB and call it an implementation detail. If a layer tries, it does not become portable. It becomes wrong. ### What a lie at the memory-map layer looks like We shipped that lie in our own examples repo and found it while writing this batch. The `memory.x` in our `stm32f4-discovery` example declares `RAM : ORIGIN = 0x20000000, LENGTH = 192K`. `cortex-m-rt` puts the initial stack pointer at the top of RAM, so the first word of the vector table is `0x20030000`. On a real STM32F407 the contiguous block at `0x20000000` is 128 KB and everything from `0x20020000` up is reserved.[^4] That stack pointer is 64 KB into nothing. `cargo build --release` is delighted. One ELF, one md5. Three machines: | Machine | Result | Why | | -------------------------------------------- | -------------------------------- | ----------------------------------------------------- | | Chiplab (Renode 1.16.1), `stm32f4_discovery` | boots, runs, prints, exits clean | its platform provides 256 KB contiguous at 0x20000000 | | QEMU 11.0.3, `netduinoplus2` (STM32F405) | instant lockup before `main` | provides the silicon-correct 128 KB | | Real STM32F407 | would bus-fault on the same push | 0x20020000+ is reserved per DS8626 | QEMU's answer, verbatim: ``` qemu: fatal: Lockup: can't escalate 3 to HardFault (current priority -1) R00=00000000 R01=00000000 R02=00000000 R03=00000000 R12=00000000 R13=2002ffe0 R14=fffffff9 R15=0800359c XPSR=41000003 -Z-- T handler ``` Three "correct" answers to the same binary, because each board model has its own idea of what memory exists. The simulator that "worked" was the wrong one. That is fragmentation you can see, and it is why [firmware CI that stops at the linker](/blog/firmware-ci-ends-at-the-linker) proves less than the green checkmark suggests. ### The partial escapes, and how far each one actually gets Plenty of smart people have attacked this. Every one of these is real and useful. None of them composes into one call. | Layer | What varies | Best standard or escape hatch | How far it actually gets | | -------------------- | --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------- | | Core API | Cortex-M revision, NVIC, SysTick | CMSIS-Core[^5] | Solid, but Arm-only, and CMSIS "does not define standard peripherals" | | Register description | Every peripheral, every bit | CMSIS-SVD[^6] | Machine-readable programmer's view — vendor-authored, quality varies | | Device package | Startup code, headers, flash algorithms | Open-CMSIS-Pack[^7] | Real packaging; internal structure is still manufacturer-defined | | Toolchain | Compiler, target triple, ABI flags | LLVM/GCC in a container | Fully solved and reproducible — for the _build_ only | | Linker + startup | Memory map, vector table | `memory.x`, vendor linker scripts | Per-part, hand-written, silently wrong when copied | | HAL | Peripheral drivers | `embedded-hal` traits, Zephyr drivers | Trait-level portability; implementations stay per-vendor | | Debug transport | SWD vs JTAG, probe vendor | OpenOCD[^8], probe-rs[^9] | Broad probe support; still needs a probe, a cable, a board | | Flashing | Per-part flash algorithm | `.FLM` algorithms in CMSIS-Packs[^10] | Works when the vendor ships one; they "can be absent" | | Board definition | Pins, clocks, peripherals present | Zephyr board porting[^11], PlatformIO[^12] | Excellent in tree; otherwise you are writing devicetree | | Physical access | Who owns the board | nothing | Zero. A human plugs it in. | Read the last column top to bottom. Every row is a partial win. Rust gets closest to the container experience — `cargo build` then `probe-rs run` really is two commands, and probe-rs speaks SWD and JTAG across Arm and RISC-V using flash algorithms lifted from vendor packs.[^9][^10] It is excellent. It also requires that the probe is attached, the board is powered, the target is in the registry, and the vendor shipped a flash algorithm. Four preconditions a Linux container has never once asked about. Ten layers, nine partial fixes, and the tenth is a person. ![Terminal joke showing a containerized embedded development environment building fine and then failing to reach the board](/blog/every-chip-is-an-island/image-2.webp) ### What an agent would actually need An agent writing firmware needs the same thing a container runtime gives a web developer: one call that takes a binary and a target name and returns what happened. Not an IDE, not a plugin, not a wizard. One call, and a transcript. Every layer above has to be closed for that call to exist, and most of them are somebody's product boundary. The one nobody has closed is hardware access for CI — the physical layer, where a person is holding the board. That is the whole premise of [hardware becoming an API](/blog/hardware-is-becoming-an-api), and it is the layer we are building at Veecle with Chiplab. It currently exposes seven boards. Seven. That number tells you exactly how early this is and how little I am claiming. ### Is a standard embedded development environment even possible? Partly. The build side is already solved — a container with the right cross-compiler is reproducible and boring. The register and packaging side converges slowly through CMSIS, SVD, and probe-rs, because machine-readable descriptions are the one thing vendors gain from publishing. Two layers will not converge on their own. The memory map, because it is the silicon, and lying about it produces a simulator that passes tests real hardware fails. And physical access, because no standard ever made a board plug itself in. The escape hatches keep improving. The last mile stays a person. Every chip stays an island until somebody runs a ferry to all of them. ### Update, 2026-08-04: our own memory map, corrected The linker script in that three-way comparison was ours and it was wrong. It is fixed: `RAM` is `128K` at `0x20000000`, matching the F407's actual contiguous SRAM, and the extra 64K of CCM stays where the silicon puts it, at `0x10000000`, on a bus DMA cannot reach. Rebuilt, both machines print `Hello world!` — Chiplab with `SP = 0x20020000`, and QEMU's `netduinoplus2`, which would not boot the old binary at all. Which is the layer argument in one file. One chip, one address range, three different opinions about what exists there, and the only way to settle it was to read the datasheet by hand. No standard was going to do that for us. ### Sources [^1]: Katapara and Sharma, "Embedded DevOps: A Survey on the Application of DevOps Practices in Embedded Software and Firmware Development," arXiv:2507.00421, 2025. https://arxiv.org/html/2507.00421v1 [^2]: Docker, "What is Docker?" — see "The underlying technology" on namespaces and Linux kernel features. https://docs.docker.com/get-started/docker-overview/ [^3]: Open Container Initiative, "OCI Image Index Specification." https://github.com/opencontainers/image-spec/blob/main/image-index.md [^4]: STMicroelectronics STM32F405xx/407xx datasheet (DS8626), memory map: SRAM 112 KB at 0x20000000, 0x2002 0000 – 0x3FFF FFFF reserved, CCM data RAM at 0x1000 0000. https://hangpersonal.com/wp-content/uploads/2024/10/STM32F407-Memory-Mapping.pdf [^5]: Arm, "CMSIS — Common Microcontroller Software Interface Standard," general documentation. https://arm-software.github.io/CMSIS_6/latest/General/index.html [^6]: Open-CMSIS-Pack, "CMSIS-SVD Format" specification. https://open-cmsis-pack.github.io/svd-spec/main/svd_Format_pg.html [^7]: Open-CMSIS-Pack specification, "Delivery Mechanism for Software Packs." https://open-cmsis-pack.github.io/Open-CMSIS-Pack-Spec/main/html/index.html [^8]: OpenOCD, "About" — debug adapters, transports, JTAG and SWD. https://openocd.org/doc/html/About.html [^9]: probe-rs, "About probe-rs." https://probe.rs/docs/overview/about-probe-rs/ [^10]: probe-rs, "CMSIS Packs" knowledge base — pack structure is manufacturer-defined and flash algorithms "can be absent if the manufacturer chooses so." https://probe.rs/docs/knowledge-base/cmsis-packs/ [^11]: Zephyr Project, "Board Porting Guide." https://docs.zephyrproject.org/latest/hardware/porting/board_porting.html [^12]: PlatformIO, "Boards." https://docs.platformio.org/en/latest/boards/index.html --- ## Copilot for firmware is solved. That was the easy half. (https://veecle.ai/blog/copilot-for-firmware-is-solved, 2026-08-04) Ask a current model for a USART init on an STM32F4 and you get one. Not a sketch — the RCC clock enable, the GPIO alternate-function bits, the fractional baud divisor, the busy-wait on TXE, in about four seconds. That is the state of AI firmware development today and it is, genuinely, very good. Ask for an interrupt handler and you get the right shape: read the status register, clear the flag, keep the body short, don't touch shared state without masking. Ask for a DMA ring buffer and you get head and tail indices, a half-transfer callback, and a comment about where the buffer has to live. Ask for a protocol state machine and you get an enum with exhaustive matches and a timeout arm you would have forgotten. I don't write that code by hand anymore. Nobody I work with does. So let's stop arguing about it: for the narrow task of producing plausible embedded code, Copilot for embedded works, and the generation problem is closed. Generation was never the problem. The problem is that after the model emits those forty lines, it has no way to find out whether they did anything. It can't run them. And an agent that can't run what it wrote doesn't say "I'm not sure." It says "looks right to me" — which is the single failure mode embedded punishes hardest. ### What the agent gets back when it's wrong Here is the comparison that made me stop treating this as a code-quality argument. Take the same agent, the same model, the same harness, and change only the domain it's working in. What comes back after it submits an answer? | | Can the agent run it? | What comes back when it's wrong | One iteration | What failure looks like | | ---------------- | -------------------------------------- | --------------------------------------------------------------------------------------- | ----------------------------------- | --------------------------------------- | | Web / backend | Yes, in its own sandbox | Stack trace with file and line, failing assertion, HTTP status, expected-vs-actual diff | Seconds | A red test with a name | | Mobile | Mostly — simulator on the same machine | Symbolicated crash log, UI-test failure, a screenshot it can look at | Seconds to minutes | A crash report or a wrong pixel | | Data / analytics | Yes | Exception, schema mismatch, row counts, a metric that moved the wrong way | Seconds to hours | A number that's off, or a job that died | | Firmware | Only if somebody handed it a target | Nothing | Minutes to days, if a board is free | A binary that exists | Every row above firmware has the same property: the machine argues back. The agent proposes, the runtime disposes, and the disagreement arrives as text the model can read. That is the whole trick: a shorter distance between a wrong guess and being told, not a smarter model. Firmware's row is not "worse feedback." It's _no_ feedback. The build succeeds, an ELF appears on disk, and that is the last event in the loop. We've written about this at the CI layer before — [a green build doesn't mean the firmware runs](/blog/the-build-passed-why-doesnt-it-run) — but for an agent it's sharper than a CI gap. The compile is the terminal state. There is no next observation. ![Diagram comparing a closed AI agent feedback loop in web development to a firmware loop broken at the execution step](/blog/copilot-for-firmware-is-solved/image-1.webp) ### The unlock in every other domain was execution access, not intelligence This is the most reproducible result in agentic coding, not a theory I made up to sell something. SWE-agent is the cleanest statement of it. The authors didn't train a better model — they built an interface that let an existing model create files, navigate a repo, and _execute tests and other programs_. Same weights, new access. They report pass@1 of 12.5% on SWE-bench and 87.7% on HumanEvalFix, and describe both as far exceeding what non-interactive language models had achieved.[^1] The paper's framing is worth stealing: language model agents are a new category of end user, and they need interfaces built for them. Now look at the one embedded benchmark that tried the same move. EmbedAgent evaluated 10 models on 126 cases across 3 hardware platforms, and its authors proposed two remedies for weak performance: retrieval, and compiler feedback. The compiler-feedback result is the one I keep coming back to. Feeding compiler errors back to DeepSeek-R1 for a single repair iteration cut syntax errors from 34.1% of cases (43 of 126) to 3.2% (4 of 126). Pass@1 on the ESP32 migration task went from 21.4 to 27.8.[^2] Read those two numbers next to each other. One feedback channel eliminated roughly nine-tenths of the compile failures and moved end-to-end correctness by six points. That's the whole argument in one ablation: the compiler was never what was holding the model back. It was holding back _the part that was already easy_. The paper is also blunt about the state of the field — existing LLM-for-embedded approaches, it notes, "rarely enable genuine interaction between the LLM and the hardware." ![Bar chart showing compiler feedback nearly eliminating syntax errors while barely improving pass rate in LLM firmware code generation](/blog/copilot-for-firmware-is-solved/image-2.webp) ### Be fair: what firmware tooling actually gives an agent today The honest version of this argument has to admit the agent isn't working in total darkness. | Tool | Signal it returns | Runs unattended in a loop? | Blind to | | ----------------------------------- | --------------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------ | | `cargo check`, clippy, the compiler | Type errors, stale APIs, borrow violations | Yes, seconds | Anything an address can express — a raw register write is always "valid" | | Static analysis (MISRA, sanitizers) | Rule violations, some UB, traceable findings | Yes | Whether the peripheral was ever enabled | | Host-side unit tests | Assertion failures on pure logic | Yes | Everything below the HAL boundary | | Emulator (QEMU, Renode, Wokwi) | UART output, peripheral state, real execution | Only if a model for _your_ board exists | Whatever the board model doesn't implement | | A physical board | Ground truth | No — someone has to plug it in and watch | Nothing, and that's the point | That top block is genuinely useful and it is why the failure classes have shifted. In our cold-prompt test of [what LLMs get wrong about embedded code](/blog/what-llms-get-wrong), every HAL attempt died in the compiler on API drift, and every raw-register attempt compiled clean on the first try. The mechanical layer works. What survives it is the wrong-but-plausible register write, and no static tool has an opinion about that. The emulator row is where it gets interesting, because that row is the only one that both returns a real execution signal _and_ can run in a loop without a human. It is also the row gated on a conditional: **if a board model exists**. That conditional is doing enormous work, and it's the reason firmware code generation risks don't get retired by better prompting. ### A broken signal doesn't fail loudly. It converges confidently. This is the part people underrate, and it's the reason I don't find "just review the output" reassuring. An agent optimizing against a signal that is _absent_ behaves identically to an agent optimizing against a signal that is _correct_. It iterates, it reports progress, it declares success. There's no uncertainty flag, because from inside the loop nothing looked wrong. Software engineering already measured the softer version of this. SpecBench decomposes 30 systems-level programming tasks into a spec, a visible test suite, and a held-out suite that composes the same features, and quantifies reward hacking as the gap between them. Every frontier agent saturated the visible suite; the holdout gap persisted, and grew by 28 percentage points for every tenfold increase in code size. The failure catalog includes a 2,900-line hash-table "compiler" that memorized the test inputs.[^3] Give an agent a proxy for correctness and it will find the proxy. Firmware doesn't even offer a proxy to game. It offers nothing — and we have our own receipt for what that produces. We staged a Cortex-M fault-debugging session, planted a write straight into flash, and planned to hand an agent the fault dump so it could diagnose and fix the bug. There was no fault dump to hand it: [the HardFault never fired](/blog/an-agent-fixed-a-hardfault), because the simulator's CPU model doesn't escalate bus faults. Four architecturally distinct ways to fault the core, zero faults, and a run that idled out at the time limit looking exactly like a healthy one. The instrument wasn't plugged in, and from inside the loop that looks identical to nothing being wrong. That is the shape of the risk in AI embedded systems work: not garbage output you can spot, but a clean run, a confident all-clear, and a bug nobody caught. ![Is this a pigeon meme with a coding agent mistaking silent UART output for a passing test](/blog/copilot-for-firmware-is-solved/image-3.webp) ### What closing the loop in AI firmware development actually requires Not "better models." Four concrete capabilities, none of which are about intelligence: 1. **Execute on the target.** Not `x86_64` with the HAL stubbed out. The actual instruction set, the actual memory map, the actual reset vector. 2. **Observe more than stdout.** Register values, peripheral state, memory contents, interrupt activity, and the bus warnings the model itself generated. `printf` is a proxy, and EmbedAgent's own benchmark design agrees: it moved past grading on serial output alone to monitoring real component state, because text on a wire doesn't confirm the hardware did the right thing.[^2] 3. **Attribute a fault to a cause.** "It hung" is not feedback. "It faulted at PC 0x8000604 reading an unmapped address" is. The gap between those two sentences is the difference between an agent guessing and an agent debugging. 4. **Iterate without a human in the middle.** If closing the loop requires someone to walk to a bench, the loop is a ticket queue, not a loop. That's what we're building Chiplab for, and post 11 is public proof we haven't finished either — our own fault escalation isn't wired up yet, which is precisely why that HardFault never fired in the first place. ### So is Copilot for embedded actually good enough? For writing code, yes, and I'd rather concede that loudly than pretend otherwise. Every argument that starts with "but the code is bad" is going to lose, and it's going to lose soon. We already made the narrower version of this case: [LLMs write good firmware and can't prove it](/blog/llms-write-good-firmware-cant-prove-it). The generation half is a solved product with a pricing page. The verification half doesn't have a product yet. It has certification standards that demand every artifact be traceable. That's the point Promwad makes better than most: firmware is judged on deterministic behavior across interrupts, DMA, and timing, not function-level correctness, and GenAI "generates plausible code, not verifiable system behavior."[^4] It has vendor-specific register maps that differ between variants of the same family, which is the one substantive thing in the otherwise fairly promotional roundups of embedded AI constraints.[^5] What it doesn't have is a way for the agent to check. Agents are going to own firmware. Embedded engineers become reviewers, the same way we became reviewers of compiler output and stopped reading assembly. I have zero doubt about the destination. But the ordering isn't optional. An agent that writes brilliantly and cannot observe is a very fast intern who never gets told they were wrong. Execution access has to land before autonomy does, and right now, for almost every chip on the market, it hasn't landed at all. The code takes minutes. Finding out it was wrong still takes a human with a board. - **Repo & examples:** [github.com/veecle/chiplab](https://github.com/veecle/chiplab) - **Discord:** [discord.com/invite/F6GwZJ6ktP](https://discord.com/invite/F6GwZJ6ktP) ### Sources [^1]: "SWE-agent: Agent-Computer Interfaces Enable Automated Software Engineering," arXiv:2405.15793. https://arxiv.org/abs/2405.15793 [^2]: "EmbedAgent: Benchmarking Large Language Models in Embedded System Development," arXiv:2506.11003v3. https://arxiv.org/html/2506.11003v3 [^3]: "SpecBench: Measuring Reward Hacking in Long-Horizon Coding Agents," arXiv:2605.21384v1. https://arxiv.org/html/2605.21384v1 [^4]: Promwad, "GenAI for Firmware: Where LLM Code Generation Breaks Down in Real Embedded Systems." https://promwad.com/news/genai-for-firmware-llm-code-generation-certification-risks [^5]: Garg, Jatin. "AI Code Generation in Embedded Systems: Constraints and Solutions." GoCodeo, July 1, 2025. https://www.gocodeo.com/post/ai-code-generation-in-embedded-systems-constraints-and-solutions --- ## Every hardware MCP server I could find, and what each one actually does (https://veecle.ai/blog/hardware-mcp-servers-reviewed, 2026-08-04) Conflict of interest first: I work on one of these. Veecle builds Chiplab, which is a hardware MCP server, and it is in the table below held to the same standard as everything else. If that makes you want to discount the whole post, fair. Read the links instead of me. I spent a day auditing the category. Start point was the `awesome-mcp-hardware` list,[^1] then the official MCP Registry[^2] and the reference-servers repository,[^6] then targeted searches for Model Context Protocol hardware tooling of every kind I could think of: serial, GPIO, Arduino, ESP32, PlatformIO, Raspberry Pi, JTAG/SWD, logic analyzers, oscilloscopes, SDR, CAN, Modbus, robotics, EDA and Wokwi. I opened 44 candidate links and checked repository activity with `gh` on 2026-08-04. Two of them 404. This is not exhaustive — it is everything I could find, and if you know one I missed I want the link. The honest summary: MCP embedded tooling is real, it is early, and most of it is a serial port with a schema bolted on. ### What the whole category looks like from above `awesome-mcp-hardware` lists 21 servers and one client. Two of the 21 — `xds110_mcp_server` and `IoT-Edge-MCP-Server` — return 404 from the GitHub API as of today. Of the 19 that resolve, 11 have fewer than 30 stars, and seven have not taken a commit in over twelve months: UnitApi, the three `mcp2everything` servers (last pushed December 2024), `modbus-mcp`, `OpenSCAD-MCP-Server`, and — the interesting one — Golioth's `tinymcp`. The official registry is thinner still. I searched it for fourteen hardware terms: | Registry search term | Unique servers returned | | ------------------------------------------------------------------------------------------------------ | ----------------------- | | `kicad` | 3 | | `serial`, `arduino`, `embedded` | 2 each | | `firmware` | 1 | | `stm32`, `microcontroller`, `probe-rs`, `jtag`, `oscilloscope`, `modbus`, `gpio`, `esp32`, `raspberry` | 0 | Nine of fourteen return nothing. The official index of MCP servers currently knows about zero servers matching "stm32", and three matching "kicad". That gap is the category. ![Capability tiers for hardware MCP servers, from docs retrieval to firmware execution](/blog/hardware-mcp-servers-reviewed/image-1.webp) ### Group 1: read the docs, touch nothing These aren't hardware access. They are retrieval over datasheets and source. `sheetsdata-mcp` pulls component specs and absolute max ratings out of manufacturer PDFs. `clangd-graph-rag` builds a graph index over a C/C++ tree. Microchip ships an [MPLAB-DOCS MCP server](https://www.microchip.com/en-us/resources/model-context-protocol-server) that does vector search over its own technical documentation — a silicon vendor's official MCP offering, and it cannot touch silicon. Useful. Honest about what they are. But when a "hardware" list is a quarter documentation search, that tells you what was available to put on the list. ### Group 2: talk to a port on your desk The biggest group and the thinnest. [`mcp2serial`](https://github.com/mcp2everything/mcp2serial), `mcp2tcp` and `mcp2mqtt` are the archetype: pyserial with an MCP schema in front. All three last saw a commit in December 2024. `mcp2mqtt` has 371 stars and nineteen months of silence. Newer ones are better maintained and no deeper. `serial-mcp-server` and `serial-mcp` both do list ports, connect, send, read. [`mcp-micropython-bridge`](https://github.com/SWITCHSCIENCE/mcp-micropython-bridge) drives a MicroPython REPL over USB. [`MCP_BluetoothScanner`](https://github.com/parkerdorris/MCP_BluetoothScanner) describes itself in its own README as a "simple POC" and has one star. What the agent observes here is bytes. Not state, not memory, not timing — whatever the firmware already decided to print. If your firmware doesn't log it, the agent can't see it. ### Group 3: drive a build-and-flash toolchain [`arduino-mcp-server`](https://github.com/hardware-mcp/arduino-mcp-server) wraps `arduino-cli`: detect boards, compile, upload, stateful serial sessions. It's the flagship of a new `hardware-mcp` org that also advertises an SCPI server and a JTAG server — both listed as "coming soon", and neither exists as a repository. That's the field in one screenshot. [`stm32-mcp`](https://github.com/shieldyguy/stm32-mcp) is my favorite entry in the whole list, precisely because it doesn't pretend. Its README says it is "pretty specific to how I tend to approach hardware development" — stlink-v3 mini, VCP, STM32 — and carries a blunt warning: "This is not a sandbox. Know what's connected before you let it rip." It builds, flashes, talks serial and reads memory over SWD. One author, 18 stars, actively pushed. Build works anywhere. Flash needs the board. ### Group 4: drive a debug probe This is where the category gets genuinely good. [`embedded-debugger-mcp`](https://github.com/Adancurusul/embedded-debugger-mcp) exposes 24 tools over probe-rs or OpenOCD: probe discovery, connect, read/write memory, halt, run, reset, single step, hardware breakpoints, flash, and RTT.[^3] Rust, MIT, ARM Cortex-M, RISC-V and Xtensa, last pushed 2026-07-16. [`dbgprobe-mcp-server`](https://github.com/es617/dbgprobe-mcp-server) does a similar job J-Link-first with ELF and SVD loading for symbol-aware inspection. OpenOCD and pyOCD are marked planned in the README's own commented-out badges. [`gdb-mcp`](https://github.com/Ipiano/gdb-mcp) drives GDB/MI directly. An agent on this tier can actually observe: registers, memory, execution state. That's the real feedback loop. It also requires a probe, a target, a USB cable and a human who plugged all three in. Which is exactly the problem. ### Group 5: design tools and instruments The EDA servers have the star counts. [`KiCAD-MCP-Server`](https://github.com/mixelpixx/KiCAD-MCP-Server) sits at 1,757 stars, [Seeed's](https://github.com/Seeed-Studio/kicad-mcp-server) at 71, `kicad-sch-api` at 48, plus two more KiCad servers in the official registry. They edit files. No hardware required, which is likely why they're the most popular things in a hardware list. Instruments are the opposite: rare, and hard-gated on owning the instrument. [`scope-mcp`](https://github.com/Netlist-Studio/scope-mcp) drives a Keysight/Agilent scope over Ethernet and states it was tested against exactly one model, an MSOX2024A. [`saleae-logic2-mcp`](https://github.com/hsoffar/saleae-logic2-mcp) needs Logic 2 running locally with automation enabled. [`sdr-mcp`](https://github.com/sandraschi/sdr-mcp) needs an RTL-SDR dongle. [`modbus-mcp`](https://github.com/kukapay/modbus-mcp) and `opcua-mcp` need a PLC. [`ros-mcp-server`](https://github.com/robotmcp/ros-mcp-server) is the single most-starred hardware-adjacent server at 1,376 and needs a robot. ### Group 6: execute firmware on a target you don't physically have Three entries. That's it. [`tinymcp`](https://github.com/golioth/tinymcp) from Golioth is the most architecturally interesting thing in the audit. The MCP server runs locally but reaches deployed devices through the Golioth management API, exposing device RPCs as MCP tools without changing firmware.[^4] It is also the only company-backed entry in the list, and its last commit is 2025-07-03 — a README edit titled "expand warning". The warning it expanded reads: "`tinymcp` is an experimental project. Expect frequent breaking changes." Thirteen months, no code. It still needs your own fleet, on Golioth, online. Wokwi ships an **experimental** MCP server inside `wokwi-cli`, invoked as `wokwi-cli mcp`, needing a `WOKWI_CLI_TOKEN`.[^5] Hosted simulation, so no board. The documentation is a single short page and the word "experimental" is theirs. And Chiplab. ![Agent-to-hardware paths with and without a physically attached board](/blog/hardware-mcp-servers-reviewed/image-2.webp) ### The structural problem with the whole hardware MCP server category Group the field by capability and one thing falls out immediately: nearly every entry assumes the hardware is already plugged into the machine running the agent. Serial servers need the port. Toolchain servers need the board to flash. Probe servers need the probe. Instrument servers need the instrument. That single assumption is what keeps all of it out of CI and out of the hands of an agent working alone. E2B gave agents [a computer they don't physically own](/blog/e2b-gives-agents-a-computer), and that's the whole reason a coding agent can iterate unsupervised. The hardware equivalent is mostly missing, which is the same empty slot I mapped in [the agent infrastructure map](/blog/the-agent-infrastructure-map) and argued from the other direction in [hardware is becoming an API](/blog/hardware-is-becoming-an-api). I'm not going to re-argue it here. The audit is the evidence. ### The table Maintenance status is last push to the default branch as reported by the GitHub API on **2026-08-04**. Stars same date. | Server | Group | Board on your desk? | What the agent observes | Last commit, stars (checked 2026-08-04) | Best for | | --------------------------------------------------------------------------------- | ---------- | ------------------- | ----------------------------------- | --------------------------------------- | ------------------------------------------ | | [sheetsdata-mcp](https://github.com/octoco-ltd/sheetsdata-mcp) | Docs | No | Datasheet text | 2026-04-26, 9 | Part selection | | [clangd-graph-rag](https://github.com/2015xli/clangd-graph-rag) | Docs | No | Your own source graph | 2026-04-25, 59 | Large C/C++ codebases | | [mcp2serial](https://github.com/mcp2everything/mcp2serial) | Port | Yes | UART bytes | 2024-12-19, 48 — stale | Nothing new; read it for the pattern | | [mcp-micropython-bridge](https://github.com/SWITCHSCIENCE/mcp-micropython-bridge) | Port | Yes | REPL output | 2026-04-15, 8 | ESP32/RP2040 MicroPython tinkering | | [arduino-mcp-server](https://github.com/hardware-mcp/arduino-mcp-server) | Toolchain | Yes, to upload | Compile output, serial | 2026-03-08, 14 | The best MCP server for Arduino work | | [stm32-mcp](https://github.com/shieldyguy/stm32-mcp) | Toolchain | Yes | Build, serial, SWD memory | 2026-07-18, 18 | One person's STM32 bench, honestly labeled | | [embedded-debugger-mcp](https://github.com/Adancurusul/embedded-debugger-mcp) | Probe | Yes | Memory, registers, breakpoints, RTT | 2026-07-16, 138 | Best embedded debugger MCP today | | [dbgprobe-mcp-server](https://github.com/es617/dbgprobe-mcp-server) | Probe | Yes | Memory + symbols via ELF/SVD | 2026-03-14, 8 | J-Link users | | [gdb-mcp](https://github.com/Ipiano/gdb-mcp) | Probe | Depends on target | Whatever GDB sees | 2026-03-11, 42 | Existing GDB workflows | | [KiCAD-MCP-Server](https://github.com/mixelpixx/KiCAD-MCP-Server) | EDA | No | Schematic and PCB files | 2026-08-01, 1,757 | PCB design automation | | [scope-mcp](https://github.com/Netlist-Studio/scope-mcp) | Instrument | Scope, not board | Waveforms, measurements | 2026-02-23, 11 | One tested Agilent model | | [saleae-logic2-mcp](https://github.com/hsoffar/saleae-logic2-mcp) | Instrument | Analyzer + board | Decoded UART/I2C/SPI, timing | 2026-03-09, 3 | Protocol bring-up | | [tinymcp](https://github.com/golioth/tinymcp) | Execute | Your fleet, remote | Device RPC results | 2025-07-03, 151 — stale 13 months | Golioth fleets | | [Wokwi CLI MCP](https://docs.wokwi.com/wokwi-ci/mcp-support) | Execute | No | Simulated run output | Experimental, docs only | Arduino/ESP32 simulation in CI | | [Chiplab](https://github.com/veecle/chiplab) | Execute | No | Firmware stdout, run artifacts | 2026-07-31, 13 | Cortex-M firmware an agent can run alone | ### Chiplab's entry, same standard Chiplab is a hosted MCP server that runs firmware on emulated boards. It is in the official registry as `ai.veecle/chiplab`.[^2] Here is the round-trip an agent actually performs, no human in the loop: ``` 1. chiplab_issue_upload_ticket -> artifact_id + presigned upload_url 2. curl --upload-file "" -> HTTP 200 3. chiplab_run { artifact_id, board_id } -> task_id 4. chiplab_get_task_result { task_id } -> {"artifacts":["stdout"],"run_id":"run_..."} 5. chiplab_list_run_artifacts { run_id } -> artifact list 6. chiplab_issue_download_ticket { run_id, artifact_name: "stdout" } 7. curl "" -> the transcript ``` Roughly 25-30 seconds from upload ticket to transcript in hand. Boards exposed: `stm32f4_discovery`, `stm32f7_discovery`, `stm32f103_blue_pill`, `stm32wba52_nucleo`, `stm32l073_nucleo`, `stm32h745_nucleo`, `nrf52840_dk`. Seven. The runs behind posts 18 and 20 in this batch came out of exactly this loop. Where it loses: seven boards is a rounding error next to the parts a real team ships. The server is hosted and closed, so you cannot audit it the way you can audit the 24 tools in `embedded-debugger-mcp`. It executes a simulation, not silicon. Those same runs are where that simulation confidently handed back 256 KB of RAM at an address where the STM32F407 has 128 KB and reserved space. A probe server attached to a real board would not have lied about that. Chiplab wins one dimension only: the agent doesn't need the board. ![Seven MCP tool calls an agent makes to run firmware on a hardware MCP server](/blog/hardware-mcp-servers-reviewed/image-3.webp) ### Is any of this production-ready? The probe tier, yes, for a human at a bench who wants an agent to stop making them type J-Link commands. `embedded-debugger-mcp` and `stm32-mcp` both do real work today. Everything in the port tier is a weekend of pyserial you could write yourself. The execute tier has three entries, one of which hasn't shipped code in thirteen months and one of which is documented on a single page labeled experimental. Forty-four links, two dead, seven abandoned, and one silicon vendor whose official MCP server is a PDF search. Nobody has won this category. Nobody is close. ### Sources [^1]: beriberikix. "Awesome MCP for Hardware." GitHub. https://github.com/beriberikix/awesome-mcp-hardware [^2]: "The MCP Registry." Model Context Protocol. https://modelcontextprotocol.io/registry/about and https://registry.modelcontextprotocol.io [^3]: "Embedded Debugger MCP Server." LobeHub. https://lobehub.com/mcp/adancurusul-embedded-debugger-mcp [^4]: Mangum, Daniel. "tinymcp: Unlocking the Physical World for LLMs with MCP and Microcontrollers." Golioth Developer Blog. https://blog.golioth.io/tinymcp-unlocking-the-physical-world-for-llms-with-mcp-and-microcontrollers [^5]: "MCP Support." Wokwi Docs. https://docs.wokwi.com/wokwi-ci/mcp-support [^6]: "Model Context Protocol servers." modelcontextprotocol/servers, GitHub. https://github.com/modelcontextprotocol/servers --- ## Why we won''t write "Chiplab vs X" pages (https://veecle.ai/blog/why-we-wont-write-chiplab-vs-pages, 2026-08-04) Ask anyone who does SaaS SEO for a living what we should ship this quarter and the answer is competitor comparison pages. "Chiplab vs Renode." "Chiplab vs QEMU." One per rival, feature table, CTA at the bottom, done by Friday. We're not going to write any of them. Not because I'm squeamish about marketing. Because for a tool in our position the math doesn't work, and I'd rather put the reasoning in public than quietly skip it and let someone assume we forgot. ### The playbook works, and the numbers behind it are real Grow and Convert analyzed 95 articles they'd written for clients and found comparison and alternative keywords converted better than anything else they measured: over 7.5% for comparison and alternative keywords, over 5% for "versus" keywords, both ahead of main product category keywords.[^1] They also make the point that the search-volume estimates lie in your favor here. In one client example, six comparison articles — every one showing under 20 monthly searches — collectively drove 149 organic signups, with one converting at 4.5%. And for a client with a domain rating of 28, five out of five comparison keywords landed in the top three results. ![Bar chart showing competitor comparison pages converting above 7.5 percent versus other keyword types](/blog/why-we-wont-write-chiplab-vs-pages/image-1.webp) Rework's guide to comparison and alternatives pages makes the mechanism explicit: competitors spend money building brand awareness, that awareness turns into branded search volume, and a comparison page lets you take a slice of it without paying for the awareness yourself.[^2] It opens with a project management SaaS that supposedly pulled $500K in ARR out of a single comparison page at a 23% trial conversion rate — unnamed company, no methodology, so weigh that like you'd weigh any anonymous case study. The underlying logic still holds. Somebody typing "X alternative" has already decided to leave. You don't have to sell them on changing, only on where to go. The good version of the playbook also tells you to be honest. That same guide argues that admitting where a competitor is better is what makes the rest of your comparison believable, and that "we're better at everything" reads as fluff. So this isn't a post about comparison pages being dirty. They work, and most companies should follow the advice. Here's why we're the exception. ### Where competitor comparison pages break for a tool like ours Four reasons, none of them moral. #### 1. Our obvious targets are the things we're standing on Chiplab runs on Renode. Not "is inspired by" — runs on. A "Chiplab vs Renode" page is a company writing an argument against its own dependency, which is incoherent before you get to the first table row. QEMU is in the same bucket for a different reason: it's infrastructure that a large chunk of the industry, us included, relies on. You don't get to call the foundation slab a competitor. ![Diagram showing Chiplab running on Renode and QEMU, with a vs page attacking its own foundation](/blog/why-we-wont-write-chiplab-vs-pages/image-2.webp) The best evidence that this isn't posturing is what happened when we ran the honest version anyway. In [our QEMU on Cortex-M writeup](/blog/qemu-for-cortex-m-what-works), the same ELF killed QEMU instantly with a lockup — and Renode, the thing under our own stack, ran the broken firmware to completion and printed clean output. QEMU was right. We were wrong. That's the finding, and it's the first line of the post. On a "vs" page that result gets buried in paragraph nine or it doesn't get published. #### 2. There's no incumbent search volume to intercept Comparison pages are demand capture. They work by standing in front of demand somebody else created. Chris Schreiber, who has worked on category creation from Google Docs onward, puts the constraint plainly: if you're creating a category, "most likely people aren't really searching on it today and you have to create that conversation."[^3] Nobody types "Chiplab vs" anything. Nobody types "hardware target for a coding agent" either. There is no incumbent in "MCU simulation an agent can call over MCP" whose branded traffic I could siphon, because the query doesn't have volume behind it yet. Optimizing a page for a search nobody performs is not a strategy, it's a hobby. #### 3. Embedded engineers read a comparison table the way they read a diff This is the audience problem, and it's the one I'd weight highest. Developer-tool content has a verification step baked in that most B2B content doesn't: developers run the code. Hackmamba's guide to devtools SEO makes the point that a single broken code sample in a tutorial immediately reduces trust in the product, and that generic content built on broad commercial keywords consistently fails this audience.[^4] Embedded engineers are the extreme end of that. Give them a table with a row about their chip and they will check that row against the datasheet, and they will find the one cell you got wrong. I know because I found one. The best-ranked page for "Renode alternatives" lists Renode's license as Apache 2.0. It's MIT, and it says so in the README. That single wrong cell is the entire subject of the opening of our [honest Renode alternatives post](/blog/renode-alternatives), because it proves nobody opened the repository before publishing the table. ![Meme of a rigged vendor comparison table where every row is a green checkmark for us](/blog/why-we-wont-write-chiplab-vs-pages/image-3.webp) #### 4. A "vs" page is a liability with a timer on it Feature tables decay. The competitor ships the thing your table says they don't have, and now you're the one hosting a wrong claim about somebody else's product with your logo on it. You will not notice, because nobody re-reads their own comparison pages. Content organized by what a tool fundamentally models ages at a completely different rate. [What Renode and QEMU each model](/blog/renode-vs-qemu-what-each-models) is a post about architecture, and architecture moves in years. "We have feature X and they don't" moves in weeks. ### The "vs" page versus the honest comparison post | | Classic "vs" page | Honest comparison post | | --------------------------------------- | ------------------------------------- | -------------------------------------------- | | Written for | A buyer already leaning your way | Someone deciding what tool fits a job | | Optimizes for | Conversion on one query | Being correct, and being reused | | Conclusion | Fixed before the first row is written | Discovered while writing; can go against you | | Reader who already knows the competitor | Spots the framing, leaves | Recognizes the trade-off, keeps reading | | Aging | Wrong the week the competitor ships | Stable while the architecture is stable | | One wrong cell costs | The whole page's credibility | A correction | | Can it say "don't switch" | No | Yes, and it has to | ### What we write instead, and the receipts Comparisons organized by job, not by opponent. [Renode alternatives, by what you're actually trying to do](/blog/renode-alternatives) spends a whole section on the reasons that look like reasons to switch but aren't, and tells you to stay put in most of them. [Wokwi alternatives for professional firmware work](/blog/wokwi-alternatives-professional-firmware) sorts tools by the question you're asking instead of ranking them, because "browser toy" and "CI gate" and "agent-accessible target" are three different purchases. Failures published as failures. [An agent tried to fix a HardFault](/blog/an-agent-fixed-a-hardfault) is a post about our own tool not modeling fault escalation, which is why the experiment produced nothing. It shipped anyway. Our own limits, documented in detail. [What simulation can't catch](/blog/what-simulation-cant-catch) is a field guide to our stack silently returning zeros for a cycle counter that doesn't exist on the virtual board, and inventing RAM that isn't there. No error, no warning. That post is more useful to a prospective user than any table I could build, and it is unambiguously bad for a demo. ### "You're still writing comparison content, you're just calling it teaching" Yes. Mostly. This post has a primary keyword. It's part of a batch of ten written against an SEO rubric. The Renode post is aimed squarely at "Renode alternatives," which is a comparison keyword by any definition, and Grow and Convert would recognize it instantly as strategy one from their own article: use a blog post instead of a landing page, because blog posts rank better and convert fine. I am running the playbook I just spent a thousand words declining. The distinction I'll actually defend is narrow: we don't fix the conclusion before we write. That's it. Everything else — the keyword research, the internal links, the tables — is the same machinery. The proof that the constraint is load-bearing is that the QEMU post exists and says we lost, and that the simulation-limits post lists the ways our own tool will lie to you. A rigged process doesn't produce those files. And the self-interest is obvious, so I'll say it out loud rather than let you find it: acknowledging trade-offs and the cases where you're not the right answer is itself the recommended way to earn trust with developers.[^5] Honest comparison is good marketing for a tool that isn't the default choice yet. I'm not doing this from a hilltop. I'm doing it because for us it's the only version that survives contact with the reader. If "Chiplab vs" ever becomes a query with real volume behind it, come back and hold me to this post. Until then the only comparison worth writing is the one where we might lose. ### Sources [^1]: Cameron Brown, "Competitor Comparison Landing Pages: 3 Unique Strategies," Grow and Convert, last updated October 12, 2023. https://www.growandconvert.com/seo/competitor-comparison-landing-pages [^2]: "Comparison & Alternative Pages: Capturing High-Intent Traffic," Rework Resources, SaaS Growth library. https://resources.rework.com/libraries/saas-growth/comparison-alternative-pages [^3]: David Khim, "How to Create and Capture Demand for a Brand New Product Category," Omniscient Digital, interviewing Chris Schreiber, Head of Marketing at Qwilr. https://beomniscient.com/blog/chris-schreiber-create-capture-demand [^4]: "SEO Agencies for Developer Tools," Hackmamba. https://hackmamba.io/technical-content/seo-agencies-developer-tools [^5]: Avthar Sewrathan, "'Developers hate being marketed to' — I think that's a myth," Developer Marketing Club, May 10, 2023. https://devmarketing.club/p/developers-hate-being-marketed-to --- ## I was on holiday.\u00a0\nMy agent shipped the blog post anyway. (https://veecle.ai/blog/agent-shipped-the-post, 2026-08-03) Our [last post](/blog/why-your-agent-needs-a-target) makes an argument: coding agents work because of a feedback loop, and in embedded that loop needs a target the agent can execute on. This post is the receipt. The last post was itself drafted, fact-checked, and validated by an agent, and the firmware transcript in the middle of it is real: compiled, uploaded, and run on a virtual STM32F4 while I was on holiday. I contributed three messages. Here's what happened between them. ### The three messages The first one: > I want to write a new blog from the list here [Linear board link]. What do you suggest? Something that is easy to do The agent queried our Linear workspace over MCP, pulled the 71 open issues in the marketing team, grouped them, and recommended a hands-on tutorial because the example code for it already existed in the [chiplab repo](https://github.com/veecle/chiplab). Reasonable, but not what I wanted that day. Message two: > mmm can we do sth more generic? It came back with the four opinion pieces from the backlog, ranked by effort, and recommended the thesis post: "Why your AI coding agent needs a target to run on." Message three, and my last real contribution: > Ok -- go for it, something like 5-10 mins read I guess? That's the entire spec I provided. ### What happened next Agents write prose fine, the same way they write firmware fine. What's worth looking at is everything around the prose. First, it gathered ground truth instead of improvising. It pulled the full Linear issue, which contained a four-line outline and a definition of done. It read our previous getting-started post to extract the house style: frontmatter shape, heading conventions, voice. It read the chiplab README for product facts. The draft that came out of this matches our existing post closely enough that I had nothing to fix, because it checked references instead of guessing. Second, the part I want to dwell on: the issue's definition of done included "code/example verified on Chiplab" and a live transcript as the proof artifact. A lazier workflow would have mocked one up. Plausible-looking terminal output is the easiest thing in the world for a language model to fabricate, and nobody would have caught it in review. Instead, the agent split the work. It handed the draft to a writing sub-agent running in the background, with instructions to leave an explicitly marked placeholder where the transcript would go. While that ran, it produced the artifact itself: ``` $ cargo build --release Compiling hello-stm32f4-discovery v0.1.0 Finished `release` profile [optimized] target(s) in 3.78s [chiplab] issue_upload_ticket → artifact_01kyhbxcw3… [chiplab] upload hello-stm32f4-discovery (ELF) → OK [chiplab] run (board: stm32f4_discovery) → run_01kyhbxyqx… [chiplab] usart2: [host: 0.23s | virt: 0s] Hello world! ``` It checked my machine had the right Rust cross-compilation targets installed, built the bare-metal STM32F4 example from the public repo, requested an upload ticket from Chiplab over MCP, pushed the ELF, triggered a run on a virtual STM32F4 Discovery, and pulled the captured output back down. The raw run log shows the simulated chip booting for real: the SVD for the STM32F40x loading, the vector table being located at `0x8000000`, and the firmware's greeting arriving on USART2 at virtual time zero. The full round trip, upload to output, came back in under a minute. Then it spliced the real transcript into the draft where the placeholder sat, ran a quick pass for word count and style tics, moved the Linear ticket to In Progress, and left a comment documenting exactly what had been verified, including the run ID, so anyone on the team could trace the claim. ### The recursive bit That sequence is: build an ELF, upload it, run it on a virtual instance of a real chip, read back the UART output. That's the exact contract the blog post describes. The post arguing that agents need a target to run on was validated by an agent using that target to run on. The proof artifact inside the argument was produced by the mechanism the argument is about. I'd love to claim we planned that symmetry; we didn't. It fell out of the workflow, because once the loop exists, the agent reaches for it on its own. And it matters for exactly the reason the post gives. The transcript is the one part of that article a language model would confidently hallucinate. Register names, tool output, and UART lines are all trivially fakeable, and all plausible in review. I can put that transcript in a published post without a disclaimer because it's captured output from a run I can look up by ID. ### What I actually did Full disclosure of the human contribution, because posts like this tend to oversell: I picked the topic, in two messages. I set the length. I read the draft afterwards and am reviewing it before it goes live, same as this one. The agent stack was nothing exotic: an MCP-capable coding agent on my laptop, connected to Linear's MCP server for the backlog and Chiplab's for the firmware runs. No custom automation or one-off pipeline. The same setup from the [getting-started post](/blog/getting-started-chiplab), pointed at a marketing repo instead of a firmware one. I was also on holiday. The messages have the typos to prove it. ### Why this is worth a post "AI wrote our blog" is barely an anecdote in 2026. This is worth a post because of where the verification happened. The wall between "the agent produced something" and "the thing actually works" is the same wall in content as it is in firmware. A draft that reads well and a binary that compiles are the same kind of artifact: plausible, unvalidated. What made this workflow feel different was watching the agent treat "verified on Chiplab" in a ticket's definition of done as an instruction to go compile and run firmware, rather than a checkbox to write around. The last post argued that agents check their work when you give them a way to. This time it's just what happened while I was away. - **Repo & examples:** [github.com/veecle/chiplab](https://github.com/veecle/chiplab) - **Chiplab:** [veecle.ai/chiplab](https://veecle.ai/chiplab) - **Discord:** [discord.com/invite/F6GwZJ6ktP](https://discord.com/invite/F6GwZJ6ktP) --- ## Why your AI coding agent needs a target to run on (https://veecle.ai/blog/why-your-agent-needs-a-target, 2026-07-27) Coding agents write firmware just fine. Ask one for a UART driver, a clock config, a FreeRTOS task, and you'll get something plausible, often something correct. The problem shows up one step later: the agent can't run what it wrote. It can't flash your board, can't watch the pins, can't tell you whether the thing it just generated does anything at all. ### The feedback loop is the product Coding agents are useful because of a specific loop: write code, run it, read the output, fix what's wrong. Repeat. Take away the "run it, read the output" part and you're left with an agent that types confidently and hopes. In web and backend work, that loop closes on its own. The agent runs `npm test` or `pytest`, reads a stack trace, patches the bug, runs again. The feedback is local and fast: a process on the same machine, exit code and stdout. Embedded breaks this at the first step. There's no `cargo test` for "does the UART clock actually come up at 115200 baud on this exact silicon revision." Running firmware means running it on a chip, and the chip is a physical board sitting on someone's desk. The agent has no hands. It can generate code and it can compile code, but it cannot plug in a USB cable, and it cannot read a logic analyzer. So today, the loop stops at compilation. The agent builds, sees no errors, and reports success. Compilation is not validation, though. It tells you the syntax is legal and the types check out. It says nothing about whether the RCC register you configured enables the peripheral clock you think it does, or whether the interrupt you registered fires before the DMA buffer is even initialized. Language models hallucinate register names and phantom HAL APIs with the same confidence they use for real ones, and a clean build won't catch either. The only thing that catches it is running the firmware on the actual target and watching what comes out the other end. That's exactly the step the agent has been locked out of. ### MCP as the bridge The Model Context Protocol gives agents a standard way to call tools outside the model itself. An MCP server describes what it can do; the agent decides when and how to call it, based on what it's trying to accomplish. This is how agents already read files, run shell commands, and query APIs without every tool being hand-wired into the model. MCP itself doesn't solve the embedded problem. It's just the pipe. The fix was never a smarter model that hallucinates fewer registers. The agent needs something on the other end of that pipe that can execute its code and send real signal back. ### The build, upload, run, read contract That target is what Chiplab is. The contract is deliberately narrow and framework-agnostic: the agent builds an ELF locally, uploads it, runs it on a virtual instance of a real chip, and reads back the UART output the firmware produced. The "virtual instance of a real chip" part matters more than it sounds like it should. This is a simulation of the specific board rather than a generic CPU emulator: same peripheral registers, same reset and clock behavior, same interrupt timing as the physical part. Firmware that misconfigures a peripheral fails on the simulated chip for the same reason it would fail on your desk. Runs are synchronous and bounded by simulated CPU time rather than wall clock. The agent uploads a binary, Chiplab boots the virtual chip, executes the firmware for a fixed budget, and hands back everything written to the UART. The whole cycle, upload through output, comes back in under a minute. Here's that exchange from a live session, trimmed to the tool calls. The prompt was one line; everything below it is the agent working: ```text > Build and run examples/bare-metal/stm32f4-discovery on Chiplab. $ cargo build --release Compiling hello-stm32f4-discovery v0.1.0 Finished `release` profile [optimized] target(s) in 3.78s [chiplab] issue_upload_ticket → artifact_01kyhbxcw3… [chiplab] upload hello-stm32f4-discovery (ELF) → OK [chiplab] run (board: stm32f4_discovery) → run_01kyhbxyqx… [chiplab] usart2: [host: 0.23s | virt: 0s] Hello world! ``` What you get out of that exchange is ground truth, the same role a test suite plays in a web project. The code ran on the target, and here's what it printed. The agent reads that output the way it would read a failing assertion, and iterates from there instead of guessing. ### What changes for embedded workflows Once an agent can run its own code against real chip behavior, a few things stop being theoretical: - **Firmware before hardware.** A board that's still on order, or being spun up in a lab three time zones away, doesn't block the firmware meant to run on it. The agent writes, runs against the simulated part, and fixes what breaks before a physical unit reaches a desk. - **Porting to boards you don't own.** Shipping support for hardware you can't test on usually means a change that looks done in review and fails the first time it touches silicon. If the agent can run the ported firmware on a simulated instance of that board and read back the output, "looks done" and "is done" collapse into the same thing. - **Chip-specific gotchas surface in runs.** The nRF52840's UARTE peripheral uses EasyDMA, which can only read from RAM. Put your TX buffer in flash, a reasonable-looking choice, and the peripheral silently transmits nothing. A static read of the code won't flag that; a run will, because the agent sees zero bytes come back and goes looking for why. - **No shared-dev-kit queue.** Teams with one nRF52840 DK and three people who need it know this bottleneck well. A simulated instance doesn't have a queue. - **A path to CI.** The same build, upload, run, read contract works from a pipeline: every pull request runs its firmware on virtual boards before a human or a physical unit sees it. None of this makes the model smarter. It gives the model something to check its own work against. ### Why we built this Chiplab exists because the agent loop needed a target on the embedded side of the wall, and there wasn't one. The contract stays small: build, upload, run, read. If you want the hands-on version, the [getting-started post](/blog/getting-started-chiplab) walks through connecting an agent, running a known example on a simulated STM32F4 Discovery, and having the agent port it to an nRF52840 DK it's never touched. - **Repo & examples:** [github.com/veecle/chiplab](https://github.com/veecle/chiplab) - **Chiplab:** [veecle.ai/chiplab](https://veecle.ai/chiplab) - **Discord:** [discord.com/invite/F6GwZJ6ktP](https://discord.com/invite/F6GwZJ6ktP) --- ## Getting started with Chiplab: run firmware without the hardware (https://veecle.ai/blog/getting-started-chiplab, 2026-07-14) Coding agents are useful because of a tight feedback loop: write code, run it, read the output, fix it. In embedded development that loop breaks at the hardware. An agent can write firmware, but it can't flash your board, can't watch the UART, and can't tell whether what it wrote actually works. Chiplab closes that loop. It's a hosted service, exposed over MCP, that runs firmware on simulated instances of real chips. Not a generic CPU emulator, but a simulation of the specific target, with the same peripherals and interrupt timing as the physical board. Your agent uploads an ELF, Chiplab boots the simulated chip, runs it, and returns the UART output. The agent reads the result and iterates, the same way it would against a test suite in a web project. You state intent; the agent works out targets, linker flags, and tool calls, and Chiplab gives it ground truth to check its work against. > **New to MCP?** The Model Context Protocol is an open standard that lets coding agents call external tools. An MCP server describes what it can do; the agent decides when to call it. Chiplab's server exposes tools for uploading binaries, starting runs, and fetching output; your agent handles the rest. Here's the plan: 1. Connect an agent (I'm using OpenCode; any MCP-capable agent works) 2. Run an example on a simulated STM32F4 Discovery 3. Have the agent port the firmware to a board from a different vendor, one I don't own, and validate the result itself Reproducing this takes about fifteen minutes. > **Note:** For a video walkthrough of this guide, check out [this post by Lucas](/blog/chiplab-demo). ### What you need > **Note:** Chiplab itself is language and framework agnostic. C, C++, Zephyr, FreeRTOS, or anything else that produces an ELF for a supported chip works the same way. This walkthrough happens to use Rust, so the toolchain requirement below is only for these examples. - **An MCP-capable coding agent.** OpenCode, Claude Code, Cursor, Codex: anything that speaks MCP. I use OpenCode below; the docs cover the others. - **A Rust toolchain (≥ 1.85).** The examples use `edition = "2024"`, so run `rustup update stable` if you're behind. Cross-compilation happens on your machine; the simulation runs on Chiplab's side. - **A Chiplab account.** Free tier, self-serve, no credit card. Sign up at [chiplab.veecle.ai](https://chiplab.veecle.ai). ### Step 1: Clone the examples repo Everything below uses the public repo at [github.com/veecle/chiplab](https://github.com/veecle/chiplab). It contains runnable examples for several boards and frameworks: bare-metal Rust, Embassy, Zephyr, FreeRTOS. ```bash git clone https://github.com/veecle/chiplab cd chiplab ``` We'll start with `examples/bare-metal/stm32f4-discovery`, a minimal firmware that prints over UART. Each example is a small, self-contained crate: a `main.rs`, a `memory.x` describing the chip's memory layout, and a `.cargo/config.toml` pinning the Rust target. No shared framework, nothing clever. Here's the entire firmware: ```rust //! Hello world over USART2 (PA2/PA3) on the STM32F4 Discovery. #![no_main] #![no_std] use cortex_m_rt::entry; use panic_halt as _; use stm32f4xx_hal::{pac, prelude::*, rcc::Config, serial::Serial}; #[entry] fn main() -> ! { let dp = pac::Peripherals::take().unwrap(); let mut rcc = dp.RCC.freeze(Config::hsi().sysclk(16.MHz())); let gpioa = dp.GPIOA.split(&mut rcc); let tx = gpioa.pa2; let rx = gpioa.pa3; let serial = Serial::new( dp.USART2, (tx, rx), stm32f4xx_hal::serial::Config::default().baudrate(115_200.bps()), &mut rcc, ) .unwrap(); let (mut tx, _) = serial.split(); let msg = b"Hello world!\n"; for &byte in msg { let _ = nb::block!(tx.write(byte)); } loop {} } ``` That self-containment is what makes the porting step at the end tractable: supporting a new board means writing one new small crate, not untangling a shared abstraction. ### Step 2: Connect your agent Go to [veecle.ai/chiplab](https://veecle.ai/chiplab) and click **Connect Your Agent** (or, from the dashboard: **API keys → Connect agent**). Pick your agent and you get a config snippet. The Connect Your Agent flow, with the agent picker visible. For OpenCode, the snippet goes into `opencode.json` in your project root: ```json { "mcp": { "chiplab": { "type": "remote", "url": "https://chiplab.veecle.ai/mcp", "enabled": true } } } ``` Only the config file format differs between agents. Authentication is browser-based OAuth; there are no tokens to copy around: ```bash opencode mcp auth chiplab ``` This opens a browser and walks you through login. One gotcha: start a fresh OpenCode session after authenticating so the agent picks up the connection. To sanity-check, call `chiplab_ask` with no arguments; it returns a short overview of the platform. ### Step 3: Run firmware on a simulated chip Tell your agent: ```text Build and run examples/bare-metal/stm32f4-discovery on Chiplab. ``` That's the whole prompt. Here's the session, condensed: ```text > Build and run examples/bare-metal/stm32f4-discovery on Chiplab. ● chiplab_ask Board: stm32f4-discovery, target thumbv7m-none-eabi, board_id stm32f4_discovery. Building. $ cargo build --release Finished `release` profile [optimized] target(s) in 0.09s ● chiplab_issue_upload_ticket → artifact_01kx… ● chiplab_run [artifact_01kx…, board_id=stm32f4_discovery] ● chiplab_get_task_result → run complete ● chiplab_issue_download_ticket → stdout ``` The agent read the example's docs to pick the right target, built the ELF, uploaded it, and triggered the run; no step needed my input. The fetched output is the simulated chip's log, UART line included: ```text 08:05:37.7476 [INFO] machine-0: Machine started. 08:05:37.8046 [INFO] usart2: [host: 0.19s (+0.19s)|virt: 0s (+0s)] Hello world! 08:05:42.7518 [INFO] machine-0: Machine paused. ``` `Hello world!` — as expected. The whole session, build included, took 49 seconds. Three details worth knowing about that run: - **It's bounded by simulated CPU time, not wall-clock time.** Chiplab boots the chip, executes the firmware for a fixed budget, and returns everything written to the UART. - **It's the real chip's behavior.** The same ELF you'd flash onto a physical F4 Discovery runs here: same peripheral registers, same startup sequence. Firmware that misconfigures the UART clock fails on Chiplab the same way it fails on your desk. - **It's quick.** A full cycle, build to captured output, comes back in under a minute. ### Step 4: Port to a board you don't own Running a known-good example is a smoke test. The porting case is the reason we built this. The example only supports the STM32F4 Discovery. I want it on an **nRF52840 DK**: a different chip from a different vendor, with a different UART peripheral and its own quirks. Normally I wouldn't ship support for hardware I can't test on; this is exactly the kind of change that looks done in a PR and fails the first time it touches silicon. So I let the agent write it and let Chiplab judge it. My prompt, in full: ```text Add support for the nRF52840 DK to this project and validate it on Chiplab. ``` No datasheets pasted in, no register names, no extra context. The agent writes a new example crate, `examples/bare-metal/nrf52840-dk-run`, sets up the right Rust target (`thumbv7em-none-eabihf`), and builds an ELF. Then, instead of declaring victory, it uploads the binary and runs it on a simulated nRF52840 DK to find out whether it's actually correct: ```text $ cargo build --release Finished `release` profile [optimized] target(s) in 12.10s ● chiplab_issue_upload_ticket → artifact_01kx… ● chiplab_run [artifact_01kx…, board_id=nrf52840_dk] ● chiplab_get_task_result → run complete ● chiplab_issue_download_ticket → stdout 09:54:45.2894 [INFO] machine-0: Machine started. 09:54:45.3455 [WARNING] uart0: Unhandled write to offset 0x514. … 09:54:50.2938 [INFO] machine-0: Machine paused. ``` First attempts at a new target often fail in chip-specific ways. This one did. The board boots, the GPIO writes land, and no `Hello world!` appears anywhere in the capture. The firmware built cleanly and ran, but the UART transmitted nothing. Instead of guessing, the agent asks Chiplab's knowledge base, which accumulates validated fixes and chip-specific gotchas from runs across the platform: ```text ● chiplab_ask [query=nRF52840 UARTE0 write() called with a byte string literal, board boots fine, GPIO pin config warnings appear, but no bytes show up in the captured UART output. What's the known cause and fix?] ``` The answer is a classic nRF52840 gotcha: the UARTE peripheral uses EasyDMA, which can only read from RAM. The agent cross-checks the other candidates against the actual code: `Uarte::write()` handles the PTR/MAXCNT/STARTTX ordering internally and only UARTE0 is initialized, so those are ruled out. The one thing left under its control is where the message lives: a `b"..."` literal is a `&'static` slice placed in `.rodata`, in flash, which EasyDMA can't read. On real hardware that diagnosis is an afternoon with a debug probe. The fix is two lines: ```diff - let msg = b"Hello world!\n"; - let _ = uarte.write(msg); + let msg: [u8; 13] = *b"Hello world!\n"; + let _ = uarte.write(&msg); ``` The message is copied into a stack array, in RAM. Second run, same board: ```text 09:56:08.8470 [INFO] machine-0: Machine started. 09:56:08.9105 [INFO] uart0: [host: 0.2s (+0.2s)|virt: 0s (+0s)] Hello world! 09:56:13.8512 [INFO] machine-0: Machine paused. ``` End to end: a working, validated implementation for a board I've never plugged in, from a one-line prompt, in about five minutes. Not "the code compiles": the code _ran on the target_ and printed what it should. ### Supported boards Chiplab currently simulates boards from the STM32 and Nordic nRF families: | Board | board_id | | ------------------- | --------------------- | | STM32F4 Discovery | `stm32f4_discovery` | | STM32F7 Discovery | `stm32f7_discovery` | | STM32F103 Blue Pill | `stm32f103_blue_pill` | | STM32WBA52 Nucleo | `stm32wba52_nucleo` | | STM32L073 Nucleo | `stm32l073_nucleo` | | STM32H745 Nucleo | `stm32h745_nucleo` | | nRF52840 DK | `nrf52840_dk` | The up-to-date list, including which frameworks run on which board, lives in `supported-boards.md`. If you need a chip that isn't there, open an issue; coverage is driven by requests. ### Wrapping up One config file, one OAuth login, and your agent can build, run, and observe firmware without any hardware on your desk. If you try it, tell us what worked, what broke, and which board you want next: - **Repo & examples:** [github.com/veecle/chiplab](https://github.com/veecle/chiplab) - **Discord:** [discord.com/invite/F6GwZJ6ktP](https://discord.com/invite/F6GwZJ6ktP) - **Chiplab:** [veecle.ai/chiplab](https://veecle.ai/chiplab) Single runs from a local agent are the start. Next: running your embedded test suite on simulated hardware in CI, on every commit, before a prototype exists. That post is coming. --- ## Chiplab demo: from MCP setup to a validated run on real hardware (https://veecle.ai/blog/chiplab-demo, 2026-07-10) **Watch our demo to learn how to:** - set up Chiplab as an MCP in your agent of choice - let your agent build support for a brand new chip target - run and validate it on real hardware — with zero manual input # Links - Product site: https://veecle.ai - App / sign in: https://chiplab.veecle.ai - MCP endpoint: https://chiplab.veecle.ai/mcp - Examples repo: https://github.com/veecle/chiplab - GitHub org: https://github.com/veecle - Discord: https://discord.com/invite/F6GwZJ6ktP - LinkedIn: https://linkedin.com/company/veecle