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
# Contributing to Rigless

Thanks for your interest in Rigless. Whether you're fixing a typo, modeling a peripheral, or bringing up a whole board, we're glad you're here. Rigless is one crate in one tree: the runtime, the SoCs, the machines, the generator, and the command line all live here, and one merge request can change any of them together. This guide covers how to get set up, how we work, and where to ask when something is unclear.

## Ways to help

- **Report a bug.** Open an issue with what you ran, what you expected, and what happened. The firmware image and the `rigless run` command line that reproduce it are gold.
- **Improve the docs.** If something confused you, it'll confuse the next person too. Small doc fixes are some of the most valuable merge requests we get.
- **Model a peripheral or a chip.** Every MCU block and sensor chip that gets modeled lets more firmware run unmodified. Each model's module opens with two tables, what it covers and what it leaves out. An instance the MCU has no model for answers from its register file alone, and a run reports each register the firmware reaches there as an unimplemented gap. A new MCU peripheral becomes a field of its SoC's MCU type, one per instance of its layout, and comes with a check in that SoC's conformance firmware. See below.
- **Add an SoC or a machine.** An SoC is a vendor's SVD, the tables generated from it, and an MCU type holding a model per instance as a field. A machine is a board struct that **has** one or more of those MCUs plus the crystals, rail and chips the board fits. They live in `src/soc` and `src/machine`, and the guide at [rigless-labs.gitlab.io/rigless](https://rigless-labs.gitlab.io/rigless/) walks through adding one.
- **Ask questions.** Open an issue with the question label. There are no dumb questions about emulators. They're full of surprises.

## Getting set up

You need a recent stable Rust. `rust-toolchain.toml` pins the exact version and rustup picks it up automatically.

The emulator is built into the crate and isn't optional, so the machine needs a C compiler, python3 with its `venv` and `wheel` packages, meson, ninja, pkg-config and the glib-2.0 headers too.

Before your first build, fetch the fork of QEMU, the submodule at `qemu/`, and the two subprojects its build reads:

```sh
git submodule update --init --depth 1 qemu
git -C qemu submodule update --init --depth 1 subprojects/dtc subprojects/keycodemapdb
```

Skip them and the build fetches the pinned tarball over the network instead, with curl. `--recursive` would also fetch the fork's other fifteen submodules, ROM sources and CI tooling that nothing here compiles. The submodule's commit is the emulator this branch is tested against, and building it in is what makes the crate a GPL-2.0-only combined work.

A few cargo tools make the checks faster to run locally:

```sh
cargo install cargo-binstall
cargo binstall cargo-nextest cargo-deny cargo-machete cargo-llvm-cov
```

The tests that run firmware need that firmware built under `firmware/`, and they fail rather than skip without it, saying so. Add the targets and build both images:

```sh
rustup target add thumbv7em-none-eabihf thumbv7m-none-eabi
(cd firmware/stm32f427/bear-bones && cargo build --release)
(cd firmware/stm32f427/conformance && cargo build --release)
```

Then build and run the tests. They run under `cargo nextest`, since the emulator runs once per process.

```sh
cargo build
cargo nextest run --all-features
```

## Finding something to work on

Issues labeled `good first issue` are scoped so you can finish one in an afternoon. If you want to take on something larger, say so on the issue first so we can talk through the approach together and nobody duplicates work. For anything that touches the emulator's seam, the link, virtual time, or the shape of a machine, an issue discussion before code saves everyone a rewrite.

## Making a change

1. Fork the repository and create a branch for your change.
2. Keep the change focused. One fix or one feature per merge request is much easier to review, and gets merged faster.
3. Add a test for behavior you add or fix. If you're not sure what to test, ask on the issue and we'll help.
4. Run the checks before you push. **`.gitlab-ci.yml` is the list.** Every job's `script` is a command you can run locally. This guide doesn't copy them, so it can't tell you something the pipeline has stopped doing. Your merge request runs them anyway.
5. Open a merge request. Describe what changed and why, and mention the issue it relates to. If it isn't ready yet, mark it as a draft. Early feedback is welcome.

We aim to respond to every merge request within a few days. Reviews are conversations: if a comment is unclear, ask, and if you disagree, say so. We're all trying to get to the best version of the change.

## Models come with a conformance check

A unit test proves what somebody thought of. Beside them, each SoC has a **conformance firmware** in `firmware/<soc>/conformance` that runs on a bench machine under a real core and drives the part through the manual's own numbered sequences, and a gate in `tests/conformance.rs` that holds it to two things:

- **Breadth.** Every register of every instance the MCU has a model for is reached by the run. The report names the ones that weren't, and a non-empty list fails. `rigless run --machine <soc>-bench --coverage PATH` writes it.
- **Depth.** Every check passes, and every check asserts *what came back* (a value, or an instant against the clock the firmware just configured) rather than that a flag moved.

So a merge request that adds or changes a model adds or changes a check, and the check has to be shown to die when the behaviour behind it is broken. Break the model by hand, watch the check go red, put it back. A check nobody can break is a finding, not a check.

Two rules keep the conformance gate inside its budget, which is ten minutes for the whole gate. Every wait in the firmware is bounded and reports a timeout for its own check, so one broken model costs one red check rather than a hung job. And a check picks the fastest configuration that still proves the behaviour, because in virtual time a check is free and waiting isn't.

For a bus, whether it's SPI, I2C or a UART, there are seven things a check has to cover before that model is done. They're listed in `firmware/<soc>/conformance/README.md`, with where the SPI stands against them. The seventh is the one people skip: assert the peer's whole recorded sequence, byte for byte and in order, and give it a peer that answers something known and **different** from what it was sent. A loopback that echoes hides a reversed bit order.

One thing in that firmware looks like duplication and isn't: its register addresses and field positions are hand-written from the manual even though the generated tables hold the same numbers. The two are separate witnesses, a test holds them against each other, and generating both from one table would quietly destroy that. The exception is written where the addresses are.

## A few things about the code

- **Determinism is the product.** The same firmware, plant, and seed must produce the same trace every time. That means no wall-clock time, no unseeded randomness, and no iteration over unordered collections in the emulation path. A machine with several MCUs lists them in the same order on every run, and a session runs one of them by its place in that order, so that's fixed too. Clippy is configured to catch the common cases, and reviewers will point out the rest kindly.
- **Comments explain why.** The code says what it does. A comment earns its place by saying why, citing a manual's section, or noting a deliberate shortcut. Public items get a one-line doc comment, private ones a plain `//` where they need one at all.
- **Small and boring wins.** If the standard library or a crate we already depend on does the job, use it. New dependencies are welcome when they carry their weight. Mention them in the merge request so we can check the license together.
- **Every Rust source file under `src`, `tests` and `firmware` starts with the two-line license header** you'll see at the top of any file there, the generated tables aside. CI checks for it with `ci/headers.sh`, so copy it into new files.

## Commit messages

A short imperative sentence that says what the commit does, such as `Add the SPI status flag model`, is all we need. The merge request description is the place for the why and the details.

## License

What you receive is GPL-2.0-only, because the fork of QEMU is built in and makes the crate a combined work. The Rust sources here are licensed under the MIT license or the Apache License 2.0, at the user's option, except the generated register tables, which carry their vendor's license. 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. There's no paperwork to sign.

## Security

If you find a security problem, please email wezley.sherman@riglesslabs.com instead of opening a public issue, and we'll work with you on a fix and credit.

## Community

Be kind, assume good intent, and remember there's a person on the other side of every comment. We want Rigless to be a place where someone modeling their first peripheral feels as welcome as someone who has shipped flight software for a decade.