hilt
Test your embedded Rust firmware with plain cargo test — no hardware on the
desk, no flashing, no probe.
hilt boots your compiled firmware inside a Renode
simulation running in a Podman/Docker container, captures everything it does,
and hands the result back to your test to assert on. It works natively on
both x86-64 and arm64 hosts, so the same test suite runs on the laptop where
you develop — Apple Silicon included — and in CI, with no code changes. The
whole point is making embedded development and testing a local, everyday
cargo test loop instead of a board-farm ritual.
use ;
Zero dependencies. The only things it shells out to are a container runtime and cargo's own binutils.
What you need
- Podman or Docker — auto-detected, or pick one with
HILT_CONTAINER_RUNTIME=docker. This is where Renode runs; you never install Renode itself. - cargo binutils —
cargo install cargo-binutils && rustup component add llvm-tools. Used to read the vector table out of your firmware ELF. - A firmware ELF cross-compiled for your target (your normal embedded build).
On Apple Silicon the first run also builds a small native Renode image (one-time, needs network); see Image selection below.
Your first test
The simplest style is self-reporting firmware: the firmware checks itself and signals success, and the host test just watches for that signal.
1. In your firmware, add an empty marker function and call it when your checks pass:
pub extern "C"
// ... in your test firmware's main, after asserting whatever you care about:
hil_marker;
That's the whole firmware-side contract. hilt finds the hil_marker symbol
in the ELF, sets a CPU hook on its address, and logs HIL OK the moment
execution reaches it. No UART driver, no semihosting, no test framework in
the firmware.
2. In your host test, point hilt at the ELF (build it yourself, or let
GuestBuild run the cross-compile for you):
use ;
3. Run cargo test. Behind the scenes, hilt generates a Renode script,
stages it with your ELF in a work directory, runs Renode in a container with
a hard timeout (killed and cleaned up if the firmware hangs), and returns a
HilOutput. If the test fails, the full simulation log is printed so you can
see what the firmware actually did. output.contains("...") asserts on any
log line, not just the marker.
Talking to the firmware while it runs
Self-reporting covers a lot, but sometimes the host needs to be in the conversation — sending bytes or bus frames and asserting on the replies.
UART ↔ TCP (works on Linux, macOS, and Windows): expose any machine's UART as a TCP socket on localhost and speak raw bytes to it:
use ;
let machine = new
.with_uart_bridge;
let runner = new;
spawn;
// connect to 127.0.0.1:3456 and exchange bytes with the firmware's serial line
CAN ↔ SocketCAN (Linux only): wire multiple machines into a shared CAN
bus and bridge it to a host vcan interface, so host code exchanges frames
with the simulated network:
let machines = vec!;
// hilt::setup_vcan("vcan0"); // once, needs privileges
let runner = new;
spawn;
// open vcan0 with your favorite SocketCAN library and talk to the firmware
All three styles mix freely in one run.
Supported boards
| Constructor | Chip | Notes |
|---|---|---|
Platform::rp2040() |
RP2040 (Cortex-M0+) | e.g. Raspberry Pi Pico |
Platform::nrf52840() |
nRF52840 (Cortex-M4F) | |
Platform::stm32f3() |
STM32F303xB (Cortex-M4F) | e.g. ZSA Voyager |
Platform::stm32f4() |
STM32F411 (Cortex-M4F) | |
Platform::stm32h7() |
STM32H753ZI Nucleo | full board model, CAN-ready |
Not listed? Platform::custom(..) takes any Renode platform description
(.repl) — embed your own string or reference one bundled in the Renode
image. Adding a board is usually a dozen lines; the built-ins are good
templates.
Beyond the basics
- Simulated key matrices —
matrix::MatrixTestPlangenerates a Python Renode peripheral that "presses" keys on a GPIO row/column matrix, plus parsers (parse_key_log,parse_tapdance_log) for asserting the firmware saw the right keys. Built for keyboard firmware, reusable for any scanned matrix. - Hand-rolled runs — everything the runner does is exposed as building
blocks (
extract_elf_info,run_renode_container,run_with_timeout_and_cleanup,generate_matrix_test_resc) if you need a custom flow. - Environment probes —
container_runtime_available(),renode_available(),interface_available()let tests skip gracefully on machines without the needed setup;detect_container_runtime()names the runtime that will be used.
Run cargo doc --open for the full API reference.
Renode image selection
The container image is picked by host architecture, so the same tests run on an Apple-Silicon laptop and amd64 CI unchanged:
- x86-64 hosts use
docker.io/antmicro/renode:latest, Antmicro's official (amd64-only) image. - aarch64 hosts get
localhost/hilt-renode:arm64, a native imagehiltbuilds on demand the first time it's needed — the official amd64 image never finishes booting under qemu emulation. First run downloads the portable Renode release and builds; later runs reuse the cached image.
Override with HILT_RENODE_IMAGE=<ref> or per-config via
HilConfig::image.
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.