rigless
A hardware-in-the-loop runtime for embedded firmware, in Rust, and the open half of Rigless. An unmodified firmware image runs on an emulated MCU: every peripheral, every chip on the board and every unit of virtual time is a Rust model, and a fork of QEMU built into the crate executes the instructions, calling those models from its own thread for every access.
It is written for any part and any board. An SoC is its vendor's SVD, the register tables generated from it, and the models written against those tables; a board is a struct that has its MCUs, its crystals and the chips on its buses. Each of those is a public trait, and implementing one is how a part or a board is added. The parts and boards this build carries are behind Cargo features, so wherever one is named it is an example and not the subject.
Documentation
rigless-labs.gitlab.io/rigless is the API
documentation of main, and the guide is its front page: what you implement, how a peripheral is
modeled, how a machine is composed, and how to add an SoC, a model, a chip, a machine or a core
family. Every seam is a trait with its contract in its own module.
Start there for anything beyond the setup and the two commands below.
Setup
The emulator is built in and is not optional, so building this needs a C compiler, python3 with its
venv and wheel packages, meson, ninja, pkg-config and the glib-2.0 headers, as well as a recent
stable Rust. rust-toolchain.toml pins the Rust version and rustup picks it up.
From crates.io:
From a checkout, which is what you want if you are changing the emulator:
Those are the fork and the two subprojects its build reads; --recursive would also fetch the
fifteen ROM submodules nothing here compiles. The two ways of installing differ only in where the
emulator's source comes from. A checkout builds the submodule at qemu/ and never reaches the
network. A crate from the registry cannot carry 169 MB of emulator, so
it fetches the source archive named in emulator.pin and checks it against the digest there before
unpacking it; RIGLESS_EMULATOR_TARBALL points that at a local copy for an offline or air-gapped
build. Either way the emulator is pinned to one commit of
rigless-labs/qemu, so which emulator goes with which crate
needs no table.
Usage
rigless run executes a firmware image on one MCU of a machine and prints every peripheral access,
then its stop, the addresses no register covers, and the interrupts it has no line for:
read t=20 n=5 pc=0x080001c0 at=0x40023830 w=32 v=0x00100000
write t=28 n=7 pc=0x080001c6 at=0x40023830 w=32 v=0x00100001
stop t=2000000 n=500000 reason=deadline
unmodeled at=0x40023800 pc=0x080001e0
| Flag | What it does |
|---|---|
--machine NAME |
The machine to run on. A build carries the machines machine::all() returns and no others |
--target MCU |
Which MCU to run, on a machine that has several. A process runs one, since the emulator has one set of state |
--until TIME |
How much virtual time to run, with a unit: ns, us, ms, s. Without it the run ends when you interrupt it |
--gdb PORT |
Serve GDB on that port while the target runs. --wait-for-debugger holds the core before its first instruction instead |
--coverage PATH |
Write what the run reached: every instance a model is bound to, and every register of one that was not touched |
--actuators PERIOD |
Print the board's outputs whenever one changes, read every period of virtual time |
--host WIRE=PORT |
Listen for a host on that wire at that port instead of the one the board names; 0 takes any free port |
--link |
Be the target of an engine on stdin and stdout, a step at a time, and print nothing else there |
Virtual time is not wall-clock time, so any deadline you can name passes faster than you can type. A wire the board says a host may sit on gets a TCP socket on the loopback interface, and the run says so before it starts; whatever connects there is the far end of that serial port.
rigless svd generate turns a vendor's CMSIS-SVD file into the register tables of an SoC. The
directory must hold exactly one .svd file, and the tables land beside it:
The trace and benchmark commands follow.
Building and testing
.gitlab-ci.yml is the source of truth. Every job's script is a command you can run yourself,
so this page does not copy them and cannot tell you something the pipeline has stopped doing. Your
merge request runs all of them anyway.
The emulator has one set of global state and so runs once per process, which is why the suite runs
under cargo nextest rather than cargo test. The tests that boot a firmware need one built under
firmware/, and fail rather than skip without it, saying so.
Contributing
CONTRIBUTING.md covers how to get set up, how we work, and what a change is expected to carry. Models come with a conformance check, and determinism is the product: no wall-clock time, no unseeded randomness, and no iteration over unordered collections in the emulation path.
What it is not
The open runtime boots firmware, runs unit tests in CI, serves GDB, writes traces, and carries the
seam a plant is stepped against. Stepping a plant in lockstep, injecting faults, and replaying runs
bit-for-bit is the Rigless engine, rigless-hil, which is a separate, commercial binary, and the
Plant trait it steps is rigless-plant in rigless-labs/hil.
Nor is it an emulator of one board: a part and a board are modules behind a feature, and the runtime
knows neither by name.
License
GPL-2.0-or-later. The emulator is built in and is not optional, so what you receive is a combined work with a program derived from QEMU and carries that licence.
The Rust sources here are themselves offered under MIT or Apache-2.0, at your option; see LICENSE-MIT, LICENSE-APACHE and NOTICE. That dual offer is what makes the combination lawful rather than a courtesy: Apache-2.0 adds a patent-retaliation condition that GPL-2.0 does not permit, so the GPL takes these sources under MIT instead. By submitting a contribution you license it under both, as section 5 of the Apache license states; no sign-off or paperwork is needed.
Rigless is a trademark of Rigless Labs.