# 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.
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 all three:
```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)
(cd firmware/template && cargo build --release)
```
The debugger's tests also run GDB itself against the binary, and fail rather than skip without it. You need `gdb-multiarch` 16.3, the version the pipeline installs, since the GDB sessions below are held to what that version prints and another prints them differently. On Debian or Ubuntu that's:
```sh
sudo apt-get install gdb-multiarch=16.3-*
```
Then build and run the tests. The pipeline runs them under `cargo nextest`, and `cargo test` runs them too.
```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 core, 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 behavior 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 behavior, 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.
## GDB sessions are held to their transcripts
`tests/gdb` holds whole GDB sessions, each a `.gdb` file of commands, one to a line, and the `.expected` transcript `gdb-multiarch` 16.3 printed for it against `rigless run --wait-for-debugger` on bear-bones, on one of its other programs, or on a few hand-assembled instructions. The transcripts quote the firmwares' source lines and addresses, so they hold for firmware the toolchain `rust-toolchain.toml` pins builds, and a different toolchain moves them. `tests/gdb_sessions.rs` runs each one as a test of its own, so a change to anything GDB sees fails the one session that sees it. When a transcript differs, the test writes what GDB printed this time beside the session as `.actual` and shows you where the two part. Read the difference, and if it's what you meant, copy the `.actual` over the `.expected` and commit it with the change that made it. A new session is a `.gdb` file and a line in `tests/gdb_sessions.rs` that names it and the firmware it runs on. `tests/gdb/packets` holds what GDB sent in some of the sessions, and a few packets written by hand, and the `fuzz` job starts the `gdb-packets` target from them.
## 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`, `firmware` and `fuzz`, and `build.rs`, 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
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 MIT and Apache-2.0, 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.