rigless 0.0.6

Virtual hardware-in-the-loop runtime: run unmodified firmware on an emulated MCU with every peripheral, chip model and unit of virtual time in Rust, and a fork of QEMU built in to execute the instructions
Documentation
rigless-0.0.6 has been yanked.

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's 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 you add a part or a board by implementing one. The parts and boards this build carries are behind Cargo features, so wherever one is named it's 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. Building your own device takes you from cargo add rigless to a firmware running on your own part.

Setup

The emulator is built in and isn't 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, and curl to install from crates.io. You also need Rust 1.98 or later. In a checkout, rust-toolchain.toml pins the version and rustup picks it up.

From crates.io:

cargo install rigless

From a checkout, which is what you want if you're changing the emulator:

git submodule update --init --depth 1 qemu
git -C qemu submodule update --init --depth 1 subprojects/dtc subprojects/keycodemapdb
cargo install --path .

Those are the fork and the two subprojects its build reads. --recursive would also fetch the fork's other fifteen submodules, ROM sources and CI tooling that nothing here compiles. The two ways of installing differ in where the emulator's source comes from. A checkout with the submodule fetched builds it at qemu/ and never reaches the network. A crate from the registry can't carry 169 MB of emulator, so it fetches the source archive named in emulator.pin with curl and checks it against the digest there before unpacking it. A checkout whose submodule you haven't fetched takes that path too. RIGLESS_EMULATOR_TARBALL points the fetch 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.

rigless --version says which you've got. A release, from crates.io or a clean checkout of its tag, says X.Y.Z. Anything else says X.Y.Z+<commit>, with .dirty on the end when the tree had changes the commit doesn't hold. A tree that's neither a package nor a git checkout says X.Y.Z+unknown. A target says the same in its Manifest.

Usage

rigless run executes a firmware image on one MCU of a machine and prints every peripheral access. Then it prints the stop, each reset with what asked for it (the core, or a model like the watchdog), and every gap, the places the emulator couldn't stand in for the part: the addresses no register covers (unmapped), the accesses the bus refused for a reason other than a shut gate (refused), the registers no model implements (unimplemented) and the interrupts it has no line for (unrouted). Last comes anything a model told the run about a state the manual forbids (told).

rigless run --machine <name> --until 2ms firmware.elf
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
unmapped at=0x4002381c pc=0x080001e0
Flag What it does
--machine NAME The machine to run on. A build carries the machines machine::all() returns and no others, and the default build carries pixracer and stm32f427-bench
--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 [HOST:]PORT Serve GDB at that address while the target runs. A port alone is on the loopback interface, and a host is an IPv4 address. Add --wait-for-debugger to hold the core before its first instruction until the debugger continues
--coverage PATH Write what the run reached: every instance a model is bound to, every register of one that wasn't touched, and every instance it reached that has no model
--outputs PERIOD Print each of the board's outputs when it changes, read every period of virtual time. The period has to be above zero
--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 an engine's target on stdin and stdout, a step at a time in version 2 of the step protocol, and print nothing else there. It takes a machine with an edge, which here is pixracer, and it doesn't go with --until, --outputs, --coverage or --host. An instant the run can't reach, behind where it stands or past where the virtual clock runs, is refused

Virtual time isn't wall-clock time. A firmware waiting for an interrupt jumps straight to its next event, so a deadline can pass in a blink, and one that keeps the core busy runs nearer the clock on the wall. 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:

rigless svd generate src/soc/<name>

The trace and benchmark commands come later.

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 doesn't copy them and can't tell you something the pipeline has stopped doing. Your merge request runs 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 they 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, prints every peripheral access, 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, a separate, closed, commercial binary. The Plant trait it steps is in rigless-plant, in rigless-labs/hil, under the Rigless License. rigless-plant is never on crates.io, and it links no runtime (ADR-0033). Nor is this 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-only. The emulator is built in and isn't optional, so what you receive is a combined work with a program derived from QEMU, which is GPL-2.0 as a whole, and it carries that license.

The Rust sources here are themselves offered under MIT or Apache-2.0, at your option. The register tables rigless svd generate wrote under src/soc/ are the exception. They carry the copyright and license of the vendor's SVD they came from. 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 doesn't permit, so the GPL takes these sources under MIT instead. Unless you say otherwise, a contribution you submit for inclusion here, as the Apache license defines one, is licensed under both, with no additional terms. You don't need a sign-off or any paperwork.

Rigless is a trademark of Rigless Labs.