hilt 0.1.0

Renode-based hardware-in-the-loop test fixtures for embedded Rust projects
Documentation
  • Coverage
  • 100%
    123 out of 123 items documented1 out of 69 items with examples
  • Size
  • Source code size: 100.1 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 1.4 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 2s Average build duration of successful builds.
  • all releases: 2s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • comploplo/hilt
    1 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • comploplo

hilt

crates.io docs.rs license

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 hilt::{HilConfig, Platform, RenodeRunner, assert_hil_ok};

#[test]
fn firmware_boots_and_scans_the_matrix() {
    let output = RenodeRunner::new(HilConfig::single(
        Platform::rp2040(),
        "target/thumbv6m-none-eabi/release/examples/hil_keyboard",
    ))
    .run();
    assert_hil_ok!(output);
}

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:

#[no_mangle]
#[inline(never)]
pub extern "C" fn hil_marker() {}

// ... 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 hilt::{GuestBuild, HilConfig, Platform, RenodeRunner, assert_hil_ok};

#[test]
fn firmware_self_checks_pass() {
    let elf = GuestBuild::example("my-firmware", "hil_smoke")
        .target("thumbv6m-none-eabi")
        .build();
    let output = RenodeRunner::new(HilConfig::single(Platform::rp2040(), elf)).run();
    assert_hil_ok!(output);
}

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 hilt::{HilConfig, MachineSpec, Platform, RenodeRunner};

let machine = MachineSpec::new("dev", "firmware.elf", Platform::stm32f3())
    .with_uart_bridge(3456, "sysbus.usart1");
let runner = RenodeRunner::new(HilConfig::multi(vec![machine]));
std::thread::spawn(move || runner.run());
// 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![
    MachineSpec::new("controller", "controller.elf", Platform::stm32h7())
        .with_socketcan_bridge(),
    MachineSpec::new("sensor", "sensor.elf", Platform::stm32h7()),
];
// hilt::setup_vcan("vcan0");  // once, needs privileges
let runner = RenodeRunner::new(HilConfig::multi(machines));
std::thread::spawn(move || runner.run());
// 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::MatrixTestPlan generates 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 image hilt builds 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.