rigless 0.0.4

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; needs the rigless-sidecar package from rigless-labs/qemu
Documentation
# rigless

The open runtime of [Rigless](https://riglesslabs.com): a virtual hardware-in-the-loop rig for embedded control software. An unmodified firmware image runs on an emulated MCU, with every peripheral, chip model, and unit of virtual time owned by Rust, and a fork of QEMU executing instructions as a sidecar process, one per MCU on the machine.

```text
rigless run --machine pixracer --until 2ms firmware/stm32f427/bear-bones/target/thumbv7em-none-eabihf/release/bear-bones
```

That needs `rigless-sidecar`, the fork of QEMU that [rigless-labs/qemu](https://gitlab.com/rigless-labs/qemu) packages on its [releases page](https://gitlab.com/rigless-labs/qemu/-/releases), on `PATH`, or `--sidecar PATH` naming the binary.

## What is here

One Apache-2.0 crate, one tree, one version. The library is the runtime and the binary is how a person drives it; a change to either is one merge request against this repository.

```text
src/
├── lib.rs
├── runtime/                 what knows no SoC and no processor family, re-exported at the root
│   ├── device.rs
│   ├── mcu.rs
│   ├── time.rs
│   ├── session.rs
│   ├── sidecar.rs
│   └── sidecar/                 the wire, the backend, and the one unsafe file
├── soc.rs
├── soc/
│   ├── stm32f427.rs             one index per SoC, behind a Cargo feature
│   └── stm32f427/
│       ├── STM32F427.svd        the vendor's file, committed unchanged
│       ├── memory_map.rs        the map and the corrections, private to the module
│       ├── generated.rs, generated/
│       └── peripherals.rs, peripherals/     the MCU, and one model per layout: gpio, pwr, rcc, spi, tim
├── machine.rs
├── machine/
│   ├── armv7m.rs                one per core family
│   ├── pixracer.rs              one per board: what it has, and what it is made of
│   └── stm32f427_bench.rs       one per SoC, for proving it before a board
├── svd.rs, svd/
└── cli/
    ├── main.rs
    └── signal.rs
```

Each area is a directory with its index file beside it, and `runtime` is private and re-exported, so the paths stay flat: `rigless::device`, not `rigless::runtime::device`.

| Module | Role |
|---|---|
| `soc` | The static description of a system on chip: its CPU, its memory map, its peripheral instances, the register layouts the generator emits, and the `Bus` that decodes an address to a slot and holds that slot's registers. A bus owns no models |
| `soc::stm32f427` | The STM32F427, behind the feature of that name: ST's own SVD, the tables generated from it, the memory map and the corrections from RM0090 rev 22 in `memory_map`, and `Stm32f427` itself in `peripherals` — the whole MCU, holding a model per instance as a field |
| `device` | The `Device` trait a model implements against one register layout, and the `DeviceContext` through which it sees time and its own interrupts |
| `mcu` | The `Mcu` trait that joins a bus to the fields holding its models, with `read`, `write` and `advance_to` written once against it; plus `Single`, one model on a bus of its own, and `Bare`, a bus with none |
| `time` | `VirtualNanos`, the virtual clock every access and every event is stamped with |
| `sidecar` | The `CpuBackend` seam to whatever executes instructions, the shared-memory protocol to the QEMU sidecar, the `Sidecar` backend that runs it, and the `SidecarCommand` a machine module fills in to say what one target's engine is started as |
| `machine` | The `Board` trait a board implements, its `Target`s borrowed one per MCU, and every machine this build carries |
| `machine::armv7m` | The Rust side of the sidecar's ARMv7-M machine: one typed field per property that machine declares |
| `machine::pixracer` | The Pixracer: one STM32F427, its two crystals, and its rail |
| `machine::stm32f427_bench` | The board that proves the STM32F427 rather than being a board: the whole MCU, a rail, one crystal, and a peer on every serial interface |
| `coverage` | Which of an MCU's registers a run reached, resolved from the accesses a session already reports |
| `session` | The run loop that joins each target's MCU to its engine and steps every target of a board together |
| `svd` | The generator, behind the `svd` feature: a vendor's CMSIS-SVD file becomes the register tables an SoC is built from |

| Feature | Default | What it adds |
|---|---|---|
| `cli` | yes | The `rigless` binary and the argument parsing it needs; implies `svd` |
| `svd` | through `cli` | `rigless::svd`, so a build can generate tables as a library |
| `stm32f427` | yes | The STM32F427 SoC, its models, and the Pixracer machine |

The bare-metal test firmware lives under `firmware/`, one directory per SoC. The QEMU sidecar lives in [rigless-labs/qemu](https://gitlab.com/rigless-labs/qemu). The plant interface and the example projects for the commercial engine live in [rigless-labs/hil](https://gitlab.com/rigless-labs/hil). The API documentation of `main` is published at [rigless-labs.gitlab.io/rigless](https://rigless-labs.gitlab.io/rigless/).

## The command line

`run` executes a firmware image on each MCU of a machine and prints every peripheral access, then for each MCU its stop, the addresses no register covers, and the interrupts it has no line for:

```text
rigless run --machine pixracer [--until 10ms] [--gdb 3333] [--wait-for-debugger] [--sidecar PATH] [--coverage PATH] firmware.elf
```

`--until` is virtual time with a unit, `ns`, `us`, `ms`, or `s`. Without it the run has no deadline and ends when the firmware stops or you interrupt it; virtual time is not wall-clock time, so any deadline you can name passes faster than you can type. The sidecar is `rigless-sidecar` on `PATH` unless `--sidecar` names the binary; `--gdb` serves GDB on the port while the target runs, and `--wait-for-debugger` holds it before its first instruction instead.

`--coverage PATH` writes what the run reached when it ends: every instance a model is bound to with how many of its registers were touched, then every register of one that was not. It is what a bench run is read for, and `rigless run --machine stm32f427-bench` is that run.

A machine with more than one MCU takes an image for each by name, and a debugger port the same way; every line of its output gains a `target=<name>` column after its kind. A machine with one MCU takes the image on its own, as above, and names nothing, so its output is what it always was. Either machine also accepts the named form, so `--firmware stm32f427=firmware.elf` and `--gdb stm32f427=3333` drive a one-MCU machine as well.

```text
rigless run --machine <name> --until 10ms --firmware fc=fc.elf --firmware aux=aux.elf --gdb fc=3333
```

The sidecar is a separate GPL-2.0-or-later program, so it is not in this crate and cannot be. **Each release of this crate pairs with the sidecar release of the same number**: the fork is tagged `<qemu version>-rigless.<this version>`, so release 0.1.0 of the crate is built against `11.1.0-rigless.0.1.0` of the sidecar, and `publish` refuses to publish a crate whose pinned sidecar is anything else. There is no table to keep: the two numbers are one number. Between releases a branch may pin a snapshot, named `-rigless.snapshot.<commit>`, which is how a change to the fork is tried before it is cut; no published crate may name one. A sidecar speaking another protocol version is refused at the handshake with both named. The exit status is zero when the run reached its deadline and one when the backend failed; Ctrl-C ends the run through that same path, so the sidecar is killed and its shared files removed before the process dies of the signal.

`svd generate` turns a vendor's SVD into the register tables of an SoC:

```text
rigless svd generate src/soc/stm32f427
```

The `trace`, `benchmark`, and `gdb` commands follow.

## Modeling a peripheral

A model matches on the layout's registers. The register file has already stored the value, applied the access rules, and done the write-modify arithmetic, so the model writes only the side effects:

```rust
use rigless::device::{Device, DeviceContext, RegisterFile};
use rigless::mcu::{Mcu, Single};
use rigless::soc::{Access, Bus, Cpu, Layout, Peripheral, ReadAction, Region, RegisterSpec, Width, WriteModify};
use rigless::time::VirtualNanos;

// What the generator emits for one register layout.
static REGISTERS: [RegisterSpec; 2] = [
    RegisterSpec { name: "CR1", offset: 0x0, width: Width::W32, reset: 0, access: Access::ReadWrite, read: ReadAction::None, write: WriteModify::None, fields: &[] },
    RegisterSpec { name: "DR", offset: 0xC, width: Width::W32, reset: 0, access: Access::ReadWrite, read: ReadAction::None, write: WriteModify::None, fields: &[] },
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Register { Cr1, Dr }
impl Layout for Register {
    const REGISTERS: &'static [RegisterSpec] = &REGISTERS;
    fn from_index(index: usize) -> Option<Self> { [Self::Cr1, Self::Dr].get(index).copied() }
    fn index(self) -> usize { self as usize }
}
const SPI1: Peripheral = Peripheral { name: "SPI1", base: 0x4001_3000, size: 0x400, interrupts: &[35], registers: &REGISTERS };
static MEMORY: [Region; 1] = [Region::ram("sram", 0x2000_0000, 192 << 10)];

// What a person writes.
#[derive(Default)]
struct Spi { sent: Vec<u8> }
impl Device for Spi {
    type Register = Register;
    fn write(&mut self, register: Register, value: u64, _registers: &mut RegisterFile, _context: &mut DeviceContext<'_>) {
        if register == Register::Dr { self.sent.push(value as u8); }
    }
}

// A bus describes where things answer; the model goes on the MCU built over it. `Single` is one model
// on a bus of its own, which is what a test of one model wants.
let bus = Bus::builder(Cpu { name: "a-core", fpu: true, interrupt_lines: 91 }, &MEMORY, [SPI1]);
let mut soc = Single::new(bus, "SPI1", Spi::default())?;
let data_register = SPI1.base + u64::from(Register::Dr.spec().offset);
let now = VirtualNanos(0);
soc.write(now, data_register, Width::W32, 0x5A)?;
assert_eq!(soc.read(now, data_register, Width::W32)?, 0x5A);
# Ok::<(), Box<dyn std::error::Error>>(())
```

Nothing in `soc` itself knows which processor family the SoC belongs to: addresses and values are 64-bit, a `Cpu` is a name, an FPU flag, and an interrupt count, and the tables are whatever the vendor's SVD says. A bus access names its width, so a byte store into a 32-bit register touches only its lane, and a lane of a write-only register has nothing to merge with, so the rest lands as zero. Where the vendor's file disagrees with the manual the SoC corrects it on the builder: `reset` for a reset value and `write_modify` for a write rule, which replaces the register's and every field's, as a status register whose flags clear on a written zero needs. A model is reached as a field of the MCU that has it, so there is nothing to look up and nothing to downcast; an input the bus does not carry goes in through the MCU's `drive_` method for that field, which runs a closure over the model, its registers, and a `DeviceContext` at the instant given.

### Time and events

Every access reaches the bus at an instant of the virtual clock, the same clock the engine's `icount` runs under, and a model sees that instant and nothing else about time through the `DeviceContext` every method receives. An event is QEMU's virtual-clock timer in this vocabulary: nothing ticks and nothing polls. The `time` and `soc` modules document how events are ordered and how a model raises an interrupt it cannot know the number of.

## The sidecar

The two processes share one page of memory, the control block, and take turns writing to it: the sidecar writes a message into its slot and hands over the turn, the runtime reads it, writes its answer into the other slot, and hands the turn back. Seven messages, strict alternation, no rings and no sequence numbers. The page's layout, the hand-over, the wait, the hand-written C header the fork compiles against, and what `Sidecar::spawn` puts on QEMU's command line are the `sidecar` module's own documentation.

## The machine and the session

A machine module declares a struct whose fields are what is on the board and implements `Board`: its name, and one `Target` per MCU, each **borrowing** the MCU the board has, with the `SidecarCommand` its engine is started as and the route from that MCU's interrupt numbers to the engine's lines. The board keeps ownership throughout, so between rounds it is whole again and a caller holding it reaches any model on it by field. A `Session` runs one firmware per target over any engine behind `CpuBackend`, stepping every target in rounds on the earliest instant any of them is due at. The `machine` and `session` modules document the rest.

`rigless run` is the session as a command. It builds the machine by name, spawns one sidecar per target, runs to `--until` of virtual time, or until you interrupt it when `--until` is not given, and prints one line per access, then each target's stop, then its unmodeled and unrouted reports:

```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
unmodeled at=0x40023800 pc=0x080001e0
```

`t` is the virtual time in nanoseconds and `n` the instruction count, both as the engine reported them; the line is the access frame of the trace, which arrives with its hash in a later slice.

### The debugger

`--gdb PORT` serves GDB on that port and the target runs from reset, so a debugger attaches whenever it likes and detaches without ending the run. `--wait-for-debugger` holds every target that serves GDB before its first instruction instead, adding QEMU's `-S`; the wait for `Hello` then lasts until the debugger's first continue, which the wait allows since it only fails when the process exits or the run is cancelled. The command says which it did, `sidecar serving GDB on :PORT` or `sidecar waiting for a debugger on :PORT`, labelled with the target on a machine that has more than one.

```text
rigless run --machine <name> --gdb 3333 firmware.elf
gdb-multiarch firmware.elf
(gdb) target remote :3333
(gdb) break bear_bones::tim2_update
(gdb) continue
```

**Set a breakpoint before the first continue.** Virtual time is not wall-clock time: the idle warp skips every `wfi` instantly, so a `continue` with nothing to stop at reaches the deadline and ends the run before you can type again. That is why `--until` has no default. A breakpoint is the only thing that holds the CPU for you, and a name that does not resolve is not one: GDB prints `Function ... not defined.` and continues anyway. The firmware's symbols are `reset`, `bear_bones::tim2_update`, and `bear_bones::default_handler`; there is no `main`.

**`next` over an idle loop never returns.** `loop { wfi }` is one source line, so `next` has no other line to reach and steps the halted CPU forever, while the warp that would wake it only runs when the engine is free-running to a deadline. `stepi` returns; `next` does not. Step into the handler with a breakpoint instead.

A peripheral read the debugger makes itself, through `x` or `print`, crosses to the runtime like any other access but with a program counter of zero, since no instruction made it, and it appears in the run's output.

## The SoCs and the machines

Everything a firmware runs on: the SoCs, the models of their peripherals, the machines built from them, and the Rust side of the sidecar's machine for each core family.

| What | Where | Now |
|---|---|---|
| An SoC | one module under `soc`, behind a Cargo feature, on by default | `soc::stm32f427`: the STM32F427, RM0090 rev 22 and ES0206 rev 24 |
| Its models | one module each under the SoC's `peripherals` | `soc::stm32f427::peripherals::gpio`, the GPIO port of chapter 8; `pwr`, the power controller of section 5.5; `rcc`, the reset and clock controller of chapter 6; `spi`, the serial peripheral interface of chapter 28; `tim`, the general-purpose timer of chapter 18 against the `tim2` layout |
| A core family | one module under `machine`, compiled always, naming no SoC | `machine::armv7m`, mirroring the sidecar's `rigless-armv7m` machine |
| A machine | one module under `machine` declaring a board struct that implements `Board` | `machine::pixracer`, one STM32F427 with its crystals and its rail |

An SoC module holds the vendor's SVD, the register tables `rigless svd generate` emits from it, the memory map from the reference manual with every correction where the vendor's file disagrees with it, and **the MCU those add up to**. A model is written against one of the generated layouts; the MCU holds one per instance as a field, and reaching it is a field path:

```rust
# #[cfg(feature = "stm32f427")]
# fn main() -> Result<(), Box<dyn std::error::Error>> {
use rigless::mcu::Mcu;
use rigless::soc::{Width, stm32f427};
use rigless::soc::stm32f427::Stm32f427;
use rigless::time::VirtualNanos;

let mut mcu = Stm32f427::new()?;
assert_eq!(mcu.cpu(), stm32f427::CPU);
mcu.write(VirtualNanos(0), stm32f427::generated::GPIOA.base, Width::W32, 0)?;

// The models are fields. No name, no lookup, nothing that can be absent.
let (clocks, registers) = mcu.clocks();
assert_eq!(clocks.sysclk(registers), 16_000_000, "the part wakes on its own oscillator");
assert_eq!(mcu.spi.len(), stm32f427::SERIAL_INTERFACES.len());
# Ok(())
# }
# #[cfg(not(feature = "stm32f427"))]
# fn main() {}
```

### The models

Each model's module opens with **two** tables. The first is what it covers: `Register or section | Behaviour | Source | Test`, one row per register and per functional section of its chapter, with the section number it was read from and the test that proves it, and one row per erratum of the errata sheet that the model implements. The second is what it does not: `Behaviour | Source | Reason`, one row for each clause of the chapter and each applicable erratum the model leaves out, with why — a wire the runtime does not have and its ticket, an analogue effect with no register behaviour, or a simplification named as the model's own. A row belongs in the first table only if a test proves it, so no row there is ever empty.

A targeted peripheral is implemented in full, so nothing inside one is register-only. Where that is departed from, the departure goes in the second table with its reason and its ticket, and the model refuses to pretend: `spi` is the SPI half of a register block the I2S shares, and in the other mode it stores every write, starts nothing, and raises nothing.

A model's boundary is methods, because the runtime does not yet carry pin levels or the wires between peripherals. What the outside world puts in takes a `drive_` name and a level; what a board attaches takes an `attach_` or `set_` name and is given after the part is built; what nothing consumes yet is answered by a method so that the logic is complete and only the wiring waits.

`Gpio` takes `drive(pin, level)` for what the outside world puts on a pin and `drive_function(pin, level)` for what a peripheral puts on one, and answers `level(pin)` and `function(pin)`. `Tim` takes `with_clock(hz)` from the machine, `drive_input`, `drive_etr`, `drive_internal_trigger`, and `halt`, and answers `output(channel)`, `trigger_output`, and `dma_requests`; the counter itself is read the way the firmware reads it, through `CNT`. `Rcc` takes `attach_crystals(hse_hz, lse_hz)` for the two a board fits and `fail_hse` for one of them stopping, and answers the whole clock tree — `sysclk`, `hclk`, `pclk1`, `pclk2`, `timer_clock`, `pll48`, `rtc_clock`, `mco1`, `mco2`, `plli2s_clock`, `sai_clock`. `Pwr` takes `set_supply(millivolts)` for the board's rail, which is also how it moves, and answers `scale`. `Spi` takes `with_clock(hz)`, `drive_miso(words)` for what the device on the other side of the wires puts on them, and `drive_nss(level)`, and answers `shifted_out`. A test reaches a model as a field of the MCU that has it — `mcu.rcc`, `board.mcu.spi[0]` — and drives one of those inputs at an instant through the MCU's `drive_` method for that field.

### Machines

A machine is a module declaring a board struct that **has** its parts: `Pixracer { mcu: Stm32f427, hse: Crystal, lse: Crystal, rail: Supply }`. It implements `Board`, which hands the session one `Target` per MCU — the MCU borrowed, the `SidecarCommand` its engine takes, and the route from that MCU's interrupt numbers to the engine's lines.

**A machine composes, it does not choose.** Which of a part's own peripherals are modelled is the part's business: `Stm32f427::new()` gives a part with a model on every instance it has one for, each its own field, and that is the MCU, complete and runnable on its own. What a board adds is what a board adds — the crystals on the oscillator pins, the rail the regulator sees, and the integrated circuits on its buses — put on the part's fields when the board is built. A board that fits no crystal is a board with no crystal, not a part missing a clock tree.

The whole of it is a tree of concrete types, so every step of reaching anything is a field:

```text
Pixracer ── mcu ──▶ Stm32f427 ── rcc   ──▶ Rcc
         ├─ hse ──▶ Crystal    ├─ pwr  ──▶ Pwr
         ├─ lse ──▶ Crystal    ├─ spi  ──▶ [Spi; 6] ── peer ──▶ dyn Peer
         └─ rail ─▶ Supply     └─ tim2 ──▶ Tim
```

The bus underneath decodes an address to a slot and holds that slot's registers; it owns no models. `Mcu::split` is the one place a slot becomes a field, a `match` each SoC writes once, and it is why nothing after construction looks a peripheral up by name or downcasts to its type.

The command comes from the module of the MCU's core family. `armv7m` is the first: one typed field per property the sidecar's ARMv7-M machine declares, with `NonZero` where that machine refuses a zero, plus the core it runs, the instruction rate, and the region the SoC's boot configuration puts where the core fetches its reset vector. An SoC's numbers are therefore typed and named where the engine names them, and nothing is written twice: the Pixracer's interrupt count is its SoC's own.

`Pixracer` is the first board: one whole STM32F427 in its `mcu` field, the board's 24 MHz and 32.768 kHz crystals, and its 3.3 V rail, so a firmware can take the part from the internal oscillator it wakes on up to 168 MHz on the over-drive rail, the way the manual sets that out. Its sensors follow when there are chip models to hang on its buses.

`Stm32f427Bench` is the same MCU with as little fitted as still lets something happen: a rail, one crystal, and a peer on each serial interface that answers a known pattern and records every word it was sent. It is the part run standalone, which is what a conformance run wants — a board's crystal and its sensors are things that run should not depend on, and a part has to be provable before there is a board to put it on.

### Adding an SoC

1. Make the directory `src/soc/<name>/` and put the vendor's SVD in it, unchanged, with its origin, version, and license in `src/soc/<name>/README.md`.
2. Generate the tables from the repository root; the directory must hold exactly one `.svd` file, and `generated.rs` and `generated/` land beside it:

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

3. Write the two files beside it as `stm32f427` does: `src/soc/<name>.rs` is the index, declaring `generated` and `peripherals` publicly, `memory_map` privately, and re-exporting the surface a board uses; `src/soc/<name>/memory_map.rs` holds the map and every correction as a struct update with the manual's section named, and exposes `CPU`, `MEMORY`, and `builder()`.
4. Add the feature to `Cargo.toml`, `<name> = []` and a place in `default`, and the module to `src/soc.rs` under `#[cfg(feature = "<name>")]`.
5. Add the feature to the two places that name every SoC: the `extern crate self as rigless;` gate at the top of `src/lib.rs`, which is what the generated tables import through, and the `features` job in `.gitlab-ci.yml`, which builds each feature on its own.

CI regenerates every SoC and fails when the committed tables differ from the result; the tables are never edited by hand.

### Adding a model

1. Put it under its SoC's `peripherals`, `src/soc/<name>/peripherals/<peripheral>.rs`, written against one generated layout and named by it.
2. Open it with the two tables above: what it covers, each row naming the section it was read from and the test that proves it, and what it leaves out, each row naming why. Every erratum of the errata sheet that applies to the part goes in one table or the other. The manual is the source of truth and a citation names the section it was read from, never memory.
3. Give the inputs the runtime cannot yet deliver an explicit method, and the outputs nobody consumes yet a method as well, so the logic is complete and only the wiring waits.
4. Give the MCU a field for it in `src/soc/<name>/peripherals.rs`, a `Field` variant, an `inventory()` row per instance, and an arm in `split` and `layout`. The compiler will not let you forget one, and `Mcu::check` catches a field wired to the wrong slot when the part is built.

### Adding a machine

Declare a struct whose fields are what is on the board — its MCU or MCUs, its crystals, its rail, the chips on its buses — and put each on the part's own fields when the board is built. Implement `Board`: `targets` hands the session one `Target` per MCU, *borrowing* it, with a `SidecarCommand` from its core family's module and the interrupt route, which is `InterruptLine::new` wherever the controller is inside the core. Then add it to the table `machine::all()` returns. A board with two MCUs is two targets; the session runs one engine for each, and `targets` answers with the same MCUs in the same order every time.

### Adding a core family

One module beside `armv7m`, mirroring the machine the sidecar carries for that family: a typed field per property it declares, an enum of the cores it accepts, a `PROPERTIES` table of the names and their types, and a `command()` that renders them. Copy `machine_matches_the_fork` for it, so the sidecar itself says whether the two agree. Nothing in `sidecar` changes, and neither does any other family.

## The generator

`rigless::svd` turns a vendor's CMSIS-SVD file into plain Rust a person can implement a peripheral against. For each register layout it emits a `Register` enum to match on, a constant per field, and the table the runtime serves reads and writes from; `generated.rs` names every instance at its address. All of it uses the types in `soc`, and none of it knows which processor family the SoC belongs to.

What a layout file looks like, trimmed:

```rust,ignore
/// The registers by name; a variant's index is its row in [`REGISTERS`].
pub enum Register {
    /// `CR1` at offset `0x0`, resets to `0x0000_0000`, read-write.
    Cr1 = 0,
    /// `SR` at offset `0x8`, resets to `0x0000_0002`, read-write.
    Sr = 2,
}

/// The fields of `CR1`.
pub mod cr1 {
    /// `SPE`, bit 6.
    pub const SPE: &FieldSpec = &super::REGISTERS[0].fields[6];
}

pub static REGISTERS: [RegisterSpec; 9] = [ /* one register per line */ ];
```

Register and peripheral names that are Rust keywords get a trailing underscore, so a register called `LOOP` is the module `loop_` and the variant `Loop`. Not emitted: descriptions, enumerated values. The provenance line at the top of each file names the source file and its SHA-256.

The library is [`svd::generate`] for the tables of one SVD as strings, and [`svd::write_module`] for a whole SoC directory. The fixture under `tests/fixture` is an SoC directory of its own; after changing the emitter, regenerate it the same way and review the diff:

```text
rigless svd generate tests/fixture
```

## Tests

```text
cargo test
cargo test --test bear_bones_runs --test machine_matches_the_fork
RUSTFLAGS="--cfg loom" cargo test --lib --no-default-features --release
MIRIFLAGS=-Zmiri-disable-isolation cargo +nightly-2026-09-06 miri test --no-default-features
```

The second line needs `rigless-sidecar` on `PATH`; those two tests fail rather than skip without it, naming what to install. `bear_bones_runs` is the proof of the whole stack: the test firmware's setup writes checked against the register file, its TIM2 handler firing inside the window the manual puts the update in, a twin with the interrupt misrouted that proves the first would notice, and a machine whose two targets run two sidecars at once and repeat exactly. `machine_matches_the_fork` asks the installed sidecar for its own machine properties and its own core list and holds `armv7m` against both, so a property renamed on the C side fails here rather than in the field.

The last two run `--no-default-features`, which is the runtime and the protocol alone, with no SoC and no command line. Everything else runs with no QEMU: the protocol round-trips through a slot with proptest, two threads exchange a million messages in strict alternation, and `Link` drives a scripted sidecar thread. The loom model covers the atomics of the hand-over under every interleaving; it does not cover the spin-then-sleep wait, which has a second implementation under `cfg(loom)` that yields instead. Miri runs the ordinary tests with isolation off, all but the ones that spawn a process or map a file. The only unsafe code in the crate is the `mmap` and the cast to `&ControlBlock` in `runtime/sidecar/mapping.rs`.

## What it is not

The open runtime boots firmware, runs unit tests in CI, serves GDB, and writes traces. Coupling that firmware to a simulated plant in lockstep, injecting faults, and replaying runs bit-for-bit is the Rigless engine, `rigless-hil`, which is a separate, commercial binary.

## License

Apache-2.0; see [LICENSE](https://gitlab.com/rigless-labs/rigless/-/blob/main/LICENSE) and [NOTICE](https://gitlab.com/rigless-labs/rigless/-/blob/main/NOTICE). By submitting a contribution you license it under the same terms, as section 5 of the license states; no sign-off or paperwork is needed.

Rigless is a trademark of Rigless Labs.