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

A hardware-in-the-loop runtime for embedded firmware, in Rust, and the open half of
[Rigless](https://riglesslabs.com). 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](https://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:

```sh
cargo install rigless
```

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

```sh
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](https://gitlab.com/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`).

```sh
rigless run --machine <name> --until 2ms firmware.elf
```

```text
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:

```sh
rigless svd generate src/soc/<name>
```

The `trace` and `benchmark` commands come later.

## Building and testing

**[`.gitlab-ci.yml`](https://gitlab.com/rigless-labs/rigless/-/blob/main/.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](https://gitlab.com/rigless-labs/rigless/-/blob/main/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](https://gitlab.com/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](https://gitlab.com/rigless-labs/rigless/-/blob/main/LICENSE-MIT),
[LICENSE-APACHE](https://gitlab.com/rigless-labs/rigless/-/blob/main/LICENSE-APACHE) and
[NOTICE](https://gitlab.com/rigless-labs/rigless/-/blob/main/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.