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. The traits each of those implements are this crate's public surface, documented in their modules and walked in the sections below. The STM32F427 and the Pixracer are the first part and the first board it carries, behind a feature, so wherever a name of either appears below it's an example and not the subject.
```text
rigless run --machine pixracer --until 2ms firmware/stm32f427/bear-bones/target/thumbv7em-none-eabihf/release/bear-bones
```
The emulator is the fork of QEMU at [rigless-labs/qemu](https://gitlab.com/rigless-labs/qemu), built into the crate and not optional, which is why this is a GPL-2.0-only combined work. What to install and how to build it is in the [repository's README](https://gitlab.com/rigless-labs/rigless/-/blob/main/README.md), which is where setup is written down once rather than here as well.
## What is here
One crate, one tree, one version: GPL-2.0-only as you receive it, over Rust sources that are themselves MIT or Apache-2.0. The register tables `rigless svd generate` wrote under `src/soc/` are the exception, and each carries the license of the vendor file it came from. The library is the runtime and the binary is how you drive it. A change to either is one merge request against this repository.
```text
src/
├── lib.rs
├── lib.md this page: the crate's documentation, which `lib.rs` includes
├── runtime.rs
├── runtime/ what knows no SoC and no processor family, re-exported at the root
│ ├── device.rs
│ ├── mcu.rs
│ ├── time.rs
│ ├── session.rs
│ ├── coverage.rs
│ ├── emulator.rs
│ ├── emulator/ the emulator, and the one unsafe file
│ ├── link.rs
│ ├── link/ the target's side of the link
│ ├── version.rs what the runtime calls itself
│ └── version/ the rule `build.rs` writes it by
├── 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 peripheral: adc, dbg, dma, flash, gpio, i2c, iwdg, otg, pwr, rcc, rtc, sdio, spi, tim, usart
├── cpu.rs
├── cpu/ the blocks a processor architecture fixes at an address, one module per family
├── chip.rs
├── chip/ the chips a board puts on its buses, and the seam each kind attaches through
├── edge.rs the board's edge: what crosses it, and the `Edge` trait a board with a world implements
├── 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
```
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`. That's the whole MCU, holding a model per instance as a field |
| `cpu` | The blocks a processor architecture fixes at an address, one module per core family, each a `Device` like any peripheral |
| `chip` | The chips a board puts on a part's buses, each written against its own datasheet and naming no SoC, and the seam each kind attaches through: `spi::Peer`, `i2c::Peer`, `uart::Peer` and `sd::Card` |
| `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. `Single` is one model on a bus of its own, and `Bare` is a bus with none |
| `time` | `VirtualNanos`, the virtual clock every access and every event is stamped with |
| `emulator` | The seam between the emulator and the models: `Access` and `Stop` as the emulator reports them, the `Host` it lends the models, the `Driver` a process lends the run, the `EmulatorCommand` a machine module fills in to say what one target's emulator is started as, and `run`, which starts the emulator built into the crate |
| `link` | The step protocol between an engine and a target it spawned, version 2: the target says what it carries in `Manifest`, then the two meet once per plant step on stdin and stdout |
| `machine` | The `Board` trait a board implements, its `Target`s borrowed one per MCU, its edge, and every machine this build carries |
| `edge` | `Edge`, the trait a board that drives or senses something implements with its own outputs and signals types, the two halves of the seam a plant is stepped against. `Coupled` is the same edge as bytes, and `Pulse` is what a timer's pin carries |
| `machine::armv7m` | The Rust side of the emulator's ARMv7-M machine: one typed field per property that machine declares |
| `machine::pixracer` | The Pixracer: one STM32F427, its two crystals, its rail, the chips on its buses, and the six outputs and the mountings its description names |
| `machine::stm32f427_bench` | The board that proves the STM32F427 rather than being a board: the whole MCU, a rail, one crystal, a peer on every serial interface, serial port and the first I²C interface, known voltages on the converters' pins, and a memory card |
| `coverage` | Which of an MCU's registers a run reached, resolved from the accesses a session already reports |
| `session` | One firmware on one target: what the emulator calls at every access and every stop, and what the run records |
| `version` | What this runtime calls itself: its version alone for an official release, and `X.Y.Z+<commit>`, `X.Y.Z+<commit>.dirty` or `X.Y.Z+unknown` for any other build, so a result from one can't pass for a release |
| `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. It turns on `svd` too |
| `svd` | through `cli` | `rigless::svd`, so a build can generate tables as a library |
| `stm32f427` | yes | The STM32F427 SoC, its models, and the `pixracer` and `stm32f427-bench` machines |
The bare-metal test firmware lives under `firmware/`, one directory per SoC. The fork of QEMU lives in [rigless-labs/qemu](https://gitlab.com/rigless-labs/qemu) and is the submodule at `qemu/`. The plant interface and a reference plant for the commercial engine live in [rigless-labs/hil](https://gitlab.com/rigless-labs/hil), under the Rigless License. The API documentation of `main` is published at [rigless-labs.gitlab.io/rigless](https://rigless-labs.gitlab.io/rigless/).
## What you implement
Every seam is a public trait with the contract in its documentation. No model behind one is looked up by name or downcast, so what you write is a type the compiler checks against the runtime. A chip on a wire comes back to the board that hung it as the type it hung, through the interface's `chip_mut`.
| You are writing | Implement | Walked in |
|---|---|---|
| A peripheral of an SoC | `device::Device` against a `soc::Layout` the generator emits. The `DeviceContext` it's handed is its time, its events, its interrupt lines, its request lines, its clocks, the accesses it asks the runtime to make, a reset of the whole part, and what it tells the run about its own instance | [Modeling a peripheral](#modeling-a-peripheral), [Adding a model](#adding-a-model) |
| A block the processor architecture fixes | the same `Device`, beside its core family under `cpu` | [Adding a core family](#adding-a-core-family) |
| An SoC | `mcu::Mcu`: a struct holding one model per instance as a field, `bus`, `bus_mut` and `split`, over a `soc::Bus` built from the generated tables with the manual's corrections | [Adding an SoC](#adding-an-soc) |
| A chip on a bus | `chip::spi::Peer`, `chip::i2c::Peer`, `chip::uart::Peer` or `chip::sd::Card`, and setters in its datasheet's terms where the chip measures something | [Adding a chip](#adding-a-chip) |
| A host on a wire | `chip::uart::Peer` too: `chip::uart::Host` is one over a socket, and a board names its wires through `machine::HostWire` | [The machine and the session](#the-machine-and-the-session) |
| A board | `machine::Board`: `name`, `targets`, `target_names` and `mcu`, then `hosts`, `attach_host` and `host_mut` where a host may sit on one of its wires, and `edge` where it has one. `edge::Edge` with the board's own `Outputs` and `Signals` where it drives or senses something | [Adding a machine](#adding-a-machine) |
| A core family | a module mirroring the emulator's machine for that family, as `machine::armv7m` does | [Adding a core family](#adding-a-core-family) |
| What a run does at every access, and at each instant it was asked to reach | `emulator::Driver`. `rigless run` prints, the engine's target answers the link, and a test records | the `emulator` module |
| What a plant reads and writes | nothing. The board's own `Edge::Outputs` and `Edge::Signals` are the two halves of the seam, and the board is what turns its pins and chips into them | [The machine and the session](#the-machine-and-the-session) |
A board written outside this crate runs from a binary of your own: `emulator::run_printed` prints what `rigless run` prints, and `emulator::run` with a `Driver` of your own does anything else. `rigless run` carries the machines in the table `machine::all()` returns and no others, which is what keeps the runtime free of any registry. [Building your own device](#building-your-own-device) walks the whole path, from `cargo add` to a firmware running on your own part.
## The command line
`run` executes a firmware image on one MCU of a machine. It prints every peripheral access, then its stop, any resets with what caused each, the addresses no register covers, the accesses the bus refused for a reason other than a shut gate, the registers no model implements, the interrupts it has no line for, and what a model told the run about its own instance:
```text
rigless run --machine pixracer [--target MCU] [--until 10ms] [--gdb 3333] [--wait-for-debugger] [--coverage PATH] [--outputs 20ms] [--link] [--host WIRE=PORT] firmware.elf
serial1 on :5760
```
`--until` is virtual time with a unit, `ns`, `us`, `ms`, or `s`. Without it the run has no deadline and ends when you interrupt it. 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. `--gdb` serves GDB at that address, the loopback interface for a port alone, 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 wasn't. After those it names every instance the run reached with no model behind it, and every address it reached outside every instance. It's what a bench run is read for, and `rigless run --machine stm32f427-bench` is that run.
A wire the board says a host may sit on gets a TCP socket on the loopback interface at the port the board names, and the run says so before it starts, `serial1 on :5760` above. Whatever connects there is the far end of that serial port: bytes it writes reach the firmware as frames at the port's baud, from the stop the runtime noticed them at, and bytes the firmware sends reach it as each frame ends. Nothing waits for it, and a run with no one connected is a run whose wire hears nothing (ADR-0027). `--host WIRE=PORT` moves that socket to another port, and a port of 0 takes any free one and says which.
`--outputs PERIOD` reads the board's outputs every period of virtual time and prints each field that changed since the last read, in the unit the board declares it in. The period has to be above zero. The first read prints every field. Here's how the Pixracer's first read starts, with one pin driven:
```text
output t=20000000 PWM1.width=1000000
output t=20000000 PWM1.period=20000000
output t=20000000 PWM1.command=0
output t=20000000 PWM2.width=0
```
A process runs one MCU, since the emulator has one set of state. A machine with more than one names the one to run with `--target`. A machine with one needs no name. Every other MCU of such a machine is another process.
`--link` makes the run an engine's target. The engine that spawned it speaks the `link` module's frames on its stdin and stdout, and nothing else is printed there. The target answers the engine's `Open` with `Manifest`, which names the board's inputs with their units and widths, its outputs and its ports. Then once per plant step the engine sends `Advance`, asking it to reach an instant with some inputs changed and some bytes on its ports. The target answers `Reached` with every output, what the firmware sent, any resets, and each gap the first time the run meets it. `Shutdown` ends the run, and the target answers it with a `Report`. `--link` doesn't take `--until`, `--outputs`, `--coverage` or `--host`. The target refuses an `Advance` for an instant behind where the run stands or past where the virtual clock runs, since it could never answer it. The engine picks the instants and reads the outputs, and the board's ports cross the link and open no socket. The engine is the commercial `rigless-hil`, and `rigless-hil run` is the command that spawns this one.
QEMU as a whole is GPL version 2, the build links files of the fork that are GPL-2.0-only, and the fork is always linked in, so `rigless` is a combined work under GPL-2.0-only. The Rust sources are offered under MIT as well as Apache-2.0 so that it may be: Apache-2.0 adds a patent-retaliation condition that GPL-2.0 doesn't permit, so the GPL takes them under MIT instead. The register tables `rigless svd generate` wrote under `src/soc/` are the exception, and carry their vendor's copyright and license lines in their headers. The emulator is pinned to one commit of the fork: the submodule's in a checkout, and `emulator.pin`'s in a published crate. The `check` job fails when the two disagree, so which emulator goes with which crate needs no table.
`svd generate` turns a vendor's SVD into the register tables of an SoC:
```text
rigless svd generate src/soc/stm32f427
```
## 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] = ®ISTERS;
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: ®ISTERS };
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, and 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, `write_modify` for a write rule, `field` for a field's shape, and `declare` for a field the file leaves out. A write rule 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's nothing to look up and nothing to downcast. An input the bus doesn't 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 emulator'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 can't know the number of.
## The emulator
The emulator is the fork of QEMU built into the crate: `build.rs` builds the submodule at `qemu/`, or the pinned source it fetches when there is no submodule, and bundles the objects the fork links whole into one archive, and links it and the fork's other libraries into every binary of the crate, its tests included, with the fork's own `main` and its stand-in core left out. `emulator::run` is that `main` in this process: it prepares the session, starts the emulator with the arguments the target's machine module renders, runs its main loop, cleans up, and returns. The fork's proxy device covers every address nothing else claims and calls four functions of this crate for what lands there, one at a time under the emulator's big lock: an access, at its instant, with its instruction count and its program counter, a stop at every instant the models asked for, and the two that take up the session and let it go. A debugger's access comes from the main loop rather than the core's thread. Nothing crosses a process boundary. The emulator has global state, so it runs once per process, and a second `emulator::run` in the same process returns `Error::AlreadyRan`. Every test that boots a firmware needs its own process. This crate runs them under `cargo nextest`, which does that, and yours should too, or keep one firmware test per test binary.
## 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 `EmulatorCommand` its emulator is started as and the route from that MCU's interrupt numbers to the emulator's lines, and `mcu`, the same MCU reached without building anything, which is how the session reaches it on every access. The board keeps ownership throughout, so once the run has ended a caller holding it reaches any model on it by field. A `Session` is one firmware on one of those targets: the emulator calls `Session::access` for every peripheral access and `Session::stop` at every instant the models asked for, and the answer to each names the next. The `machine` and `session` modules document the rest.
A board may also have wires a host sits on. `Board::hosts` names each with the port `rigless run` listens on for it, and `Board::attach_host` hangs whatever the caller puts there on the serial port the board's description wires it to: a `chip::uart::Host` over a socket from the command line, a test's stand-in from a test, or bytes at chosen instants from a test framework. A serial port's far end is a `chip::uart::Peer` whichever it is, the port keeps its own time against its baud whatever is behind it, and the session asks every model to look outside at every stop of its target, so a byte a host wrote lands at the stop it was noticed at and wakes a firmware asleep on the line. That is the whole of the runtime's part in it, ADR-0027: what feeds a wire decides whether a run is reproducible, and the runtime makes no mode of it.
The board's other edge faces the world. A board that drives or senses something implements `Edge` with two types of its own. `Outputs` is what the firmware drives, which `Edge::drive` reads off the pins, and `Signals` is what the world does to the board's transducers, which `Edge::sense` hands to each chip through the chip's own setters on the chip's own axes. A flight controller's outputs are pulses off its timer pins and what each commands on its speed controllers' curve. A pump controller's could be a valve's position and a relay. `Edge::OUTPUT_FIELDS` and `Edge::FIELDS` say how each crosses the link, field by field. Whatever steps a plant, in lockstep or otherwise, holds the board as its own type, runs the session to an instant, reads the one and writes the other, and runs on. The runtime itself steps no plant and names none. The session and `rigless run` reach a board's edge only as bytes, through `edge::Coupled`. `Board::edge` answers it, and a board that drives and senses nothing has no edge.
`rigless run` is the session as a command. It builds the machine by name, starts the emulator on one of its MCUs, runs to `--until` of virtual time, or until you interrupt it when `--until` isn't given, and prints one line per access, then the stop, the resets, and the unmapped, refused, unimplemented, unrouted and told 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
unmapped at=0x4002381c pc=0x080001e0
```
`t` is the virtual time in nanoseconds and `n` the instruction count, both as the emulator reported them. The reports come from the session, and your own binary or test gets them the same way. `Session::unmapped` is a list of `Unmapped` entries that print as the line above, in hex, so an address no model claims reads as an address. A reset prints as `reset t=<ns> n=<count> cause=core` when the core asked for it, and names the instance when a model did, like `cause=IWDG` for the watchdog. A `told` line names the instance, the register, the instant and the manual's reason, once for each.
### The debugger
`--gdb [HOST:]PORT` serves GDB at that address and the target runs from reset, so a debugger attaches whenever it likes and detaches without ending the run. A port alone is on the loopback interface, and a host is an IPv4 address, since the emulator's stub takes no other. `--gdb 0.0.0.0:3333` serves every interface, and then anyone who can reach the machine can read and write the emulated core, so an SSH tunnel to the loopback is usually the better way in from another machine. `--wait-for-debugger` holds the core before its first instruction instead, adding QEMU's `-S`. The command says which it did and where, `serving GDB on 127.0.0.1:3333` or `waiting for a debugger on 127.0.0.1:3333`.
```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 isn't wall-clock time: the idle warp skips every idle wait 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 doesn't resolve isn't one: GDB prints `Function ... not defined.` and continues anyway. The bear-bones firmware's symbols are `reset`, `bear_bones::tim2_update`, and `bear_bones::default_handler`. There's no `main`.
**`next` over an idle loop never returns.** A loop around an idle wait 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 emulator is free-running to a deadline. `stepi` returns. `next` doesn't. Step into the handler with a breakpoint instead.
A peripheral read the debugger makes itself, through `x` or `print`, reaches the models like any other access but with a program counter of zero, since no instruction made it, and it appears in the run's output.
## Building your own device
Here's how to put your own part on rigless with nothing but the crate as a dependency: a peripheral, an SoC that holds it, a board, and a firmware that runs on it. The part is made up on purpose, so nothing here is specific to any real silicon. Every Rust block for the host side is a doctest, so it compiles against this crate on every test run. The two that boot the firmware are `no_run`, so build the image first. The firmware blocks are `ignore`, since they're built for the core, not the host.
```text
cargo add rigless --no-default-features
```
Turn the default features off. That drops the CLI, the SVD generator and the SoCs we ship, and leaves you the runtime. The emulator is always built in and isn't optional, so you need what the [README's setup](https://gitlab.com/rigless-labs/rigless/-/blob/main/README.md#setup) lists to build it. The first build downloads the emulator source, checks it against the digest pinned in the crate, and compiles it. After that it's cached.
### The peripheral
A peripheral is a `Device` written against a register layout. The register file already stores what the firmware wrote and applies the access rules, so your model only does the parts storage can't: what a write to the data register means, a status bit the firmware isn't allowed to write, and something happening later.
```rust
use rigless::device::{Device, DeviceContext, Level, RegisterFile};
use rigless::mcu::{Mcu, Single};
use rigless::soc::{Access, Bus, Cpu, Layout, Peripheral, ReadAction, Region, RegisterSpec, Width, WriteModify};
use rigless::time::{EventId, VirtualNanos};
// The register table, the way the generator would emit it from a vendor's SVD. Written by hand here
// because there's no vendor. `SR` is read-only to the firmware: the model sets and clears it.
static REGISTERS: [RegisterSpec; 3] = [
RegisterSpec { name: "CR", offset: 0x0, width: Width::W32, reset: 0, access: Access::ReadWrite, read: ReadAction::None, write: WriteModify::None, fields: &[] },
RegisterSpec { name: "SR", offset: 0x4, width: Width::W32, reset: 0, access: Access::ReadOnly, read: ReadAction::None, write: WriteModify::None, fields: &[] },
RegisterSpec { name: "DR", offset: 0x8, width: Width::W32, reset: 0, access: Access::ReadWrite, read: ReadAction::None, write: WriteModify::None, fields: &[] },
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Register { Cr, Sr, Dr }
impl Layout for Register {
const REGISTERS: &'static [RegisterSpec] = ®ISTERS;
fn from_index(index: usize) -> Option<Self> { [Self::Cr, Self::Sr, Self::Dr].get(index).copied() }
fn index(self) -> usize { self as usize }
}
const EN: u64 = 1;
const IE: u64 = 2;
const BUSY: u64 = 1;
const DONE: u64 = 2;
/// The beacon's one interrupt, by its position in the instance's list.
const FRAME_DONE: usize = 0;
/// The beacon's one request output, in its own numbering: high while a byte is on the wire.
const SENT: usize = 0;
/// One byte occupies the beacon for a millisecond of virtual time.
const FRAME: VirtualNanos = VirtualNanos(1_000_000);
/// The model keeps what the registers can't: the bytes it sent, and the event that ends the frame.
#[derive(Debug, Default)]
struct Beacon { sent: Vec<u8>, dropped: u32, frame_end: Option<EventId> }
impl Device for Beacon {
type Register = Register;
fn read(&mut self, register: Register, registers: &mut RegisterFile, context: &mut DeviceContext<'_>) -> Option<u64> {
if register != Register::Sr { return None; }
let status = registers.get(Register::Sr);
if status & DONE != 0 { registers.set(Register::Sr, status & !DONE); context.lower(FRAME_DONE); }
Some(status)
}
fn write(&mut self, register: Register, value: u64, registers: &mut RegisterFile, context: &mut DeviceContext<'_>) {
if register != Register::Dr { return; }
let enabled = registers.get(Register::Cr) & EN != 0;
let busy = registers.get(Register::Sr) & BUSY != 0;
if !enabled || busy { self.dropped += 1; return; }
self.sent.push(value as u8);
registers.set(Register::Sr, BUSY);
context.request(SENT, Level::High);
self.frame_end = Some(context.schedule(VirtualNanos(context.now().0 + FRAME.0)));
}
fn on_event(&mut self, event: EventId, registers: &mut RegisterFile, context: &mut DeviceContext<'_>) {
if self.frame_end != Some(event) { return; }
self.frame_end = None;
registers.set(Register::Sr, DONE);
context.request(SENT, Level::Low);
if registers.get(Register::Cr) & IE != 0 { context.raise(FRAME_DONE); }
}
}
/// Where the one instance lives. A bus is a core, a memory map, and the instances at their addresses.
const BEACON: Peripheral = Peripheral { name: "BEACON", base: 0x4000_0000, size: 0x100, interrupts: &[3], registers: ®ISTERS };
const CPU: Cpu = Cpu { name: "lantern-core", fpu: false, interrupt_lines: 16 };
static MEMORY: [Region; 2] = [Region::flash("flash", 0x0800_0000, 64 << 10), Region::ram("sram", 0x2000_0000, 16 << 10)];
// Drive it directly, like a unit test of one model would: a byte goes out, the beacon is busy for a
// frame, and a second byte written in the meantime gets dropped.
let mut soc = Single::new(Bus::builder(CPU, &MEMORY, [BEACON]), "BEACON", Beacon::default())?;
let at = |register: Register| BEACON.base + u64::from(register.spec().offset);
let start = VirtualNanos(0);
soc.write(start, at(Register::Cr), Width::W32, EN)?;
soc.write(start, at(Register::Dr), Width::W32, u64::from(b'h'))?;
assert_eq!(soc.read(start, at(Register::Sr), Width::W32)?, BUSY);
soc.write(start, at(Register::Dr), Width::W32, u64::from(b'i'))?;
soc.advance_to(FRAME);
// A read can have side effects too: `DONE` is set when the frame ends and cleared by reading `SR`.
assert_eq!(soc.read(FRAME, at(Register::Sr), Width::W32)?, DONE, "the frame is over");
assert_eq!(soc.read(FRAME, at(Register::Sr), Width::W32)?, 0, "reading SR cleared DONE");
{
let (beacon, _registers) = soc.pair();
assert_eq!((beacon.sent.as_slice(), beacon.dropped), (b"h".as_slice(), 1));
}
// With `IE` set, the end of a frame raises the beacon's interrupt, and reading `SR` lowers it. The
// read above already drove the line low once, and the runtime reports every level a model drives.
soc.drain_interrupts();
soc.write(FRAME, at(Register::Cr), Width::W32, EN | IE)?;
soc.write(FRAME, at(Register::Dr), Width::W32, u64::from(b'i'))?;
let second = VirtualNanos(2 * FRAME.0);
soc.advance_to(second);
let raised: Vec<(u16, Level)> = soc.drain_interrupts().into_iter().map(|change| (change.interrupt, change.level)).collect();
assert_eq!(raised, [(3, Level::High)], "line 3 of the part, as BEACON declares it");
soc.read(second, at(Register::Sr), Width::W32)?;
let lowered: Vec<Level> = soc.drain_interrupts().into_iter().map(|change| change.level).collect();
assert_eq!(lowered, [Level::Low]);
# Ok::<(), Box<dyn std::error::Error>>(())
```
Three things to notice. `SR` is `Access::ReadOnly`, so the register file rejects the firmware's writes and only `registers.set` can move the flag. `context.schedule` takes an absolute instant and gives you an `EventId`. `on_event` fires at exactly that instant with the same id, whether or not the firmware touched the peripheral in the meantime. There's no cancel. Keep the last id you scheduled and ignore anything else. And asking for an instant you already have an event at gives you the same id back, not a second event, so re-scheduling on every access is free.
Reads work the same way. `Device::read` returns `Some(value)` to answer a read yourself, or `None` to let the register file serve what it holds. Return `None` for anything the file already has right, which is most registers. Where a read has a side effect on the whole register, say so in the layout instead: `ReadAction::Clear` or `ReadAction::Set`, and the file does it when the model returns `None`. Anything narrower is the model's job, like the `DONE` bit above, which clears on a read of `SR` while `BUSY` stays: read the file, change what changed, and hand back what the firmware should see.
### The SoC
An SoC is a struct with your models as fields, plus a `Bus` that knows which address belongs to which instance and holds the registers. The bus doesn't own models. `split` is where a slot number turns into a field, and `check` makes sure each field's layout matches its slot. If you wire a slot to the wrong field it fails when you build the part, not on the first access.
```rust
# use rigless::device::{Device, DeviceContext, Level, RegisterFile};
# use rigless::mcu::{Mcu, Single};
# use rigless::soc::{Access, Bus, Cpu, Layout, Peripheral, ReadAction, Region, RegisterSpec, Width, WriteModify};
# use rigless::time::{EventId, VirtualNanos};
#
# // The register table, the way the generator would emit it from a vendor's SVD. Written by hand here
# // because there's no vendor. `SR` is read-only to the firmware: the model sets and clears it.
# static REGISTERS: [RegisterSpec; 3] = [
# RegisterSpec { name: "CR", offset: 0x0, width: Width::W32, reset: 0, access: Access::ReadWrite, read: ReadAction::None, write: WriteModify::None, fields: &[] },
# RegisterSpec { name: "SR", offset: 0x4, width: Width::W32, reset: 0, access: Access::ReadOnly, read: ReadAction::None, write: WriteModify::None, fields: &[] },
# RegisterSpec { name: "DR", offset: 0x8, width: Width::W32, reset: 0, access: Access::ReadWrite, read: ReadAction::None, write: WriteModify::None, fields: &[] },
# ];
# #[derive(Debug, Clone, Copy, PartialEq, Eq)]
# enum Register { Cr, Sr, Dr }
# impl Layout for Register {
# const REGISTERS: &'static [RegisterSpec] = ®ISTERS;
# fn from_index(index: usize) -> Option<Self> { [Self::Cr, Self::Sr, Self::Dr].get(index).copied() }
# fn index(self) -> usize { self as usize }
# }
# const EN: u64 = 1;
# const IE: u64 = 2;
# const BUSY: u64 = 1;
# const DONE: u64 = 2;
# /// The beacon's one interrupt, by its position in the instance's list.
# const FRAME_DONE: usize = 0;
# /// The beacon's one request output, in its own numbering: high while a byte is on the wire.
# const SENT: usize = 0;
# /// One byte occupies the beacon for a millisecond of virtual time.
# const FRAME: VirtualNanos = VirtualNanos(1_000_000);
#
# /// The model keeps what the registers can't: the bytes it sent, and the event that ends the frame.
# #[derive(Debug, Default)]
# struct Beacon { sent: Vec<u8>, dropped: u32, frame_end: Option<EventId> }
# impl Device for Beacon {
# type Register = Register;
# fn read(&mut self, register: Register, registers: &mut RegisterFile, context: &mut DeviceContext<'_>) -> Option<u64> {
# if register != Register::Sr { return None; }
# let status = registers.get(Register::Sr);
# if status & DONE != 0 { registers.set(Register::Sr, status & !DONE); context.lower(FRAME_DONE); }
# Some(status)
# }
# fn write(&mut self, register: Register, value: u64, registers: &mut RegisterFile, context: &mut DeviceContext<'_>) {
# if register != Register::Dr { return; }
# let enabled = registers.get(Register::Cr) & EN != 0;
# let busy = registers.get(Register::Sr) & BUSY != 0;
# if !enabled || busy { self.dropped += 1; return; }
# self.sent.push(value as u8);
# registers.set(Register::Sr, BUSY);
# context.request(SENT, Level::High);
# self.frame_end = Some(context.schedule(VirtualNanos(context.now().0 + FRAME.0)));
# }
# fn on_event(&mut self, event: EventId, registers: &mut RegisterFile, context: &mut DeviceContext<'_>) {
# if self.frame_end != Some(event) { return; }
# self.frame_end = None;
# registers.set(Register::Sr, DONE);
# context.request(SENT, Level::Low);
# if registers.get(Register::Cr) & IE != 0 { context.raise(FRAME_DONE); }
# }
# }
#
# /// Where the one instance lives. A bus is a core, a memory map, and the instances at their addresses.
# const BEACON: Peripheral = Peripheral { name: "BEACON", base: 0x4000_0000, size: 0x100, interrupts: &[3], registers: ®ISTERS };
# const CPU: Cpu = Cpu { name: "lantern-core", fpu: false, interrupt_lines: 16 };
# static MEMORY: [Region; 2] = [Region::flash("flash", 0x0800_0000, 64 << 10), Region::ram("sram", 0x2000_0000, 16 << 10)];
#
use rigless::device::{ErasedDevice, RequestChange};
use rigless::soc::Error;
/// A second peripheral: one read-only register that counts the bytes the beacon has sent. It never
/// touches the beacon's registers. It listens to the beacon's request line, in the next sections.
static TALLY_REGISTERS: [RegisterSpec; 1] = [
RegisterSpec { name: "COUNT", offset: 0x0, width: Width::W32, reset: 0, access: Access::ReadOnly, read: ReadAction::None, write: WriteModify::None, fields: &[] },
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TallyRegister { Count }
impl Layout for TallyRegister {
const REGISTERS: &'static [RegisterSpec] = &TALLY_REGISTERS;
fn from_index(index: usize) -> Option<Self> { (index == 0).then_some(Self::Count) }
fn index(self) -> usize { 0 }
}
const TALLY: Peripheral = Peripheral { name: "TALLY", base: 0x4000_0100, size: 0x100, interrupts: &[], registers: &TALLY_REGISTERS };
#[derive(Debug, Default)]
struct Tally;
impl Device for Tally {
type Register = TallyRegister;
fn on_request(&mut self, change: RequestChange, registers: &mut RegisterFile, _context: &mut DeviceContext<'_>) {
// A sink hears every request on the part. This one wants the beacon's, rising.
if change.instance == BEACON.name && change.request == SENT && change.level == Level::High {
registers.set(TallyRegister::Count, registers.get(TallyRegister::Count) + 1);
}
}
}
/// The part, complete: the bus, and the models on it as fields.
#[derive(Debug)]
struct Lantern { bus: Bus, beacon: Beacon, beacon_slot: usize, tally: Tally, tally_slot: usize }
impl Lantern {
fn new() -> Result<Self, Error> {
// `reads_requests` is the wiring: the tally hears what the beacon drives.
let bus = Bus::builder(CPU, &MEMORY, [BEACON, TALLY]).reads_requests(TALLY.name).build()?;
let beacon_slot = bus.slot_of(BEACON.name).ok_or(Error::UnknownInstance { instance: BEACON.name })?;
let tally_slot = bus.slot_of(TALLY.name).ok_or(Error::UnknownInstance { instance: TALLY.name })?;
let mut chip = Self { bus, beacon: Beacon::default(), beacon_slot, tally: Tally, tally_slot };
chip.check()?;
Ok(chip)
}
}
impl Mcu for Lantern {
fn bus(&self) -> &Bus { &self.bus }
fn bus_mut(&mut self) -> &mut Bus { &mut self.bus }
fn split(&mut self, slot: usize) -> (&mut Bus, Option<&mut dyn ErasedDevice>) {
let Self { bus, beacon, beacon_slot, tally, tally_slot } = self;
let model: Option<&mut dyn ErasedDevice> = if slot == *beacon_slot { Some(beacon) } else if slot == *tally_slot { Some(tally) } else { None };
(bus, model)
}
}
let mut chip = Lantern::new()?;
let at = |register: Register| BEACON.base + u64::from(register.spec().offset);
chip.write(VirtualNanos(0), at(Register::Cr), Width::W32, EN)?;
chip.write(VirtualNanos(0), at(Register::Dr), Width::W32, u64::from(b'h'))?;
chip.advance_to(FRAME);
assert_eq!(chip.read(FRAME, at(Register::Sr), Width::W32)?, DONE);
assert_eq!(chip.beacon.sent, b"h");
# Ok::<(), Box<dyn std::error::Error>>(())
```
`Mcu` gives you `read`, `write` and `advance_to` at whatever instant you pick, which is all a unit test of the part needs. The tally is what a second peripheral costs: another field, another slot, another arm in `split`. That's it.
### The board
A board is what a `Session` runs. The chip is complete on its own. The board adds whatever sits around it: crystals, a supply rail, chips on the buses. This one adds nothing. The one thing every board has to provide is the command the emulator starts with, and that comes from the core family module, `armv7m::Core`: one typed field per property the emulator's machine declares, so the values get checked instead of being passed around as strings.
```rust
# use rigless::device::{Device, DeviceContext, Level, RegisterFile};
# use rigless::mcu::{Mcu, Single};
# use rigless::soc::{Access, Bus, Cpu, Layout, Peripheral, ReadAction, Region, RegisterSpec, Width, WriteModify};
# use rigless::time::{EventId, VirtualNanos};
#
# // The register table, the way the generator would emit it from a vendor's SVD. Written by hand here
# // because there's no vendor. `SR` is read-only to the firmware: the model sets and clears it.
# static REGISTERS: [RegisterSpec; 3] = [
# RegisterSpec { name: "CR", offset: 0x0, width: Width::W32, reset: 0, access: Access::ReadWrite, read: ReadAction::None, write: WriteModify::None, fields: &[] },
# RegisterSpec { name: "SR", offset: 0x4, width: Width::W32, reset: 0, access: Access::ReadOnly, read: ReadAction::None, write: WriteModify::None, fields: &[] },
# RegisterSpec { name: "DR", offset: 0x8, width: Width::W32, reset: 0, access: Access::ReadWrite, read: ReadAction::None, write: WriteModify::None, fields: &[] },
# ];
# #[derive(Debug, Clone, Copy, PartialEq, Eq)]
# enum Register { Cr, Sr, Dr }
# impl Layout for Register {
# const REGISTERS: &'static [RegisterSpec] = ®ISTERS;
# fn from_index(index: usize) -> Option<Self> { [Self::Cr, Self::Sr, Self::Dr].get(index).copied() }
# fn index(self) -> usize { self as usize }
# }
# const EN: u64 = 1;
# const IE: u64 = 2;
# const BUSY: u64 = 1;
# const DONE: u64 = 2;
# /// The beacon's one interrupt, by its position in the instance's list.
# const FRAME_DONE: usize = 0;
# /// The beacon's one request output, in its own numbering: high while a byte is on the wire.
# const SENT: usize = 0;
# /// One byte occupies the beacon for a millisecond of virtual time.
# const FRAME: VirtualNanos = VirtualNanos(1_000_000);
#
# /// The model keeps what the registers can't: the bytes it sent, and the event that ends the frame.
# #[derive(Debug, Default)]
# struct Beacon { sent: Vec<u8>, dropped: u32, frame_end: Option<EventId> }
# impl Device for Beacon {
# type Register = Register;
# fn read(&mut self, register: Register, registers: &mut RegisterFile, context: &mut DeviceContext<'_>) -> Option<u64> {
# if register != Register::Sr { return None; }
# let status = registers.get(Register::Sr);
# if status & DONE != 0 { registers.set(Register::Sr, status & !DONE); context.lower(FRAME_DONE); }
# Some(status)
# }
# fn write(&mut self, register: Register, value: u64, registers: &mut RegisterFile, context: &mut DeviceContext<'_>) {
# if register != Register::Dr { return; }
# let enabled = registers.get(Register::Cr) & EN != 0;
# let busy = registers.get(Register::Sr) & BUSY != 0;
# if !enabled || busy { self.dropped += 1; return; }
# self.sent.push(value as u8);
# registers.set(Register::Sr, BUSY);
# context.request(SENT, Level::High);
# self.frame_end = Some(context.schedule(VirtualNanos(context.now().0 + FRAME.0)));
# }
# fn on_event(&mut self, event: EventId, registers: &mut RegisterFile, context: &mut DeviceContext<'_>) {
# if self.frame_end != Some(event) { return; }
# self.frame_end = None;
# registers.set(Register::Sr, DONE);
# context.request(SENT, Level::Low);
# if registers.get(Register::Cr) & IE != 0 { context.raise(FRAME_DONE); }
# }
# }
#
# /// Where the one instance lives. A bus is a core, a memory map, and the instances at their addresses.
# const BEACON: Peripheral = Peripheral { name: "BEACON", base: 0x4000_0000, size: 0x100, interrupts: &[3], registers: ®ISTERS };
# const CPU: Cpu = Cpu { name: "lantern-core", fpu: false, interrupt_lines: 16 };
# static MEMORY: [Region; 2] = [Region::flash("flash", 0x0800_0000, 64 << 10), Region::ram("sram", 0x2000_0000, 16 << 10)];
#
# use rigless::device::{ErasedDevice, RequestChange};
# use rigless::soc::Error;
#
# /// A second peripheral: one read-only register that counts the bytes the beacon has sent. It never
# /// touches the beacon's registers. It listens to the beacon's request line, in the next sections.
# static TALLY_REGISTERS: [RegisterSpec; 1] = [
# RegisterSpec { name: "COUNT", offset: 0x0, width: Width::W32, reset: 0, access: Access::ReadOnly, read: ReadAction::None, write: WriteModify::None, fields: &[] },
# ];
# #[derive(Debug, Clone, Copy, PartialEq, Eq)]
# enum TallyRegister { Count }
# impl Layout for TallyRegister {
# const REGISTERS: &'static [RegisterSpec] = &TALLY_REGISTERS;
# fn from_index(index: usize) -> Option<Self> { (index == 0).then_some(Self::Count) }
# fn index(self) -> usize { 0 }
# }
# const TALLY: Peripheral = Peripheral { name: "TALLY", base: 0x4000_0100, size: 0x100, interrupts: &[], registers: &TALLY_REGISTERS };
# #[derive(Debug, Default)]
# struct Tally;
# impl Device for Tally {
# type Register = TallyRegister;
# fn on_request(&mut self, change: RequestChange, registers: &mut RegisterFile, _context: &mut DeviceContext<'_>) {
# // A sink hears every request on the part. This one wants the beacon's, rising.
# if change.instance == BEACON.name && change.request == SENT && change.level == Level::High {
# registers.set(TallyRegister::Count, registers.get(TallyRegister::Count) + 1);
# }
# }
# }
#
# /// The part, complete: the bus, and the models on it as fields.
# #[derive(Debug)]
# struct Lantern { bus: Bus, beacon: Beacon, beacon_slot: usize, tally: Tally, tally_slot: usize }
# impl Lantern {
# fn new() -> Result<Self, Error> {
# // `reads_requests` is the wiring: the tally hears what the beacon drives.
# let bus = Bus::builder(CPU, &MEMORY, [BEACON, TALLY]).reads_requests(TALLY.name).build()?;
# let beacon_slot = bus.slot_of(BEACON.name).ok_or(Error::UnknownInstance { instance: BEACON.name })?;
# let tally_slot = bus.slot_of(TALLY.name).ok_or(Error::UnknownInstance { instance: TALLY.name })?;
# let mut chip = Self { bus, beacon: Beacon::default(), beacon_slot, tally: Tally, tally_slot };
# chip.check()?;
# Ok(chip)
# }
# }
# impl Mcu for Lantern {
# fn bus(&self) -> &Bus { &self.bus }
# fn bus_mut(&mut self) -> &mut Bus { &mut self.bus }
# fn split(&mut self, slot: usize) -> (&mut Bus, Option<&mut dyn ErasedDevice>) {
# let Self { bus, beacon, beacon_slot, tally, tally_slot } = self;
# let model: Option<&mut dyn ErasedDevice> = if slot == *beacon_slot { Some(beacon) } else if slot == *tally_slot { Some(tally) } else { None };
# (bus, model)
# }
# }
#
use std::num::{NonZeroU32, NonZeroU64};
use rigless::cpu;
use rigless::emulator::InterruptLine;
use rigless::machine::{Board, Target, armv7m};
use rigless::session::Session;
/// The board: one Lantern and nothing else.
#[derive(Debug)]
struct LanternBoard { chip: Lantern }
impl Board for LanternBoard {
fn name(&self) -> &'static str { "lantern" }
fn target_names(&self) -> Vec<&'static str> { vec!["lantern-core"] }
fn mcu(&mut self, target: usize) -> Option<&mut dyn Mcu> { (target == 0).then_some(&mut self.chip) }
fn targets(&mut self) -> Vec<Target<'_>> {
let emulator = armv7m::Core {
model: armv7m::Model::M3, // the part declares no FPU
clock_hz: NonZeroU64::new(16_000_000).unwrap(), // drives the core's own timers
interrupt_lines: NonZeroU32::new(u32::from(Mcu::cpu(&self.chip).interrupt_lines)).unwrap(),
priority_bits: 0, // zero leaves it to the core
refclk_divider: NonZeroU32::new(8).unwrap(),
icount_shift: 2, // four nanoseconds an instruction
boot: "flash", // the region at the reset vector
}
.command(&cpu::armv7m::windows(Mcu::bus(&self.chip)));
vec![Target { name: "lantern-core", mcu: &mut self.chip, emulator, interrupt_line: InterruptLine::new }]
}
}
// A session on the board's one target. Nothing runs yet, but the emulator command is built and
// checked against the part: a `boot` that isn't in `MEMORY` gets rejected right here.
let board = LanternBoard { chip: Lantern::new()? };
let session = Session::new(Box::new(board), 0, VirtualNanos(3 * FRAME.0))?;
assert_eq!(session.until(), VirtualNanos(3 * FRAME.0));
# Ok::<(), Box<dyn std::error::Error>>(())
```
`boot` is the region the emulator also maps at address zero, where the core fetches its reset vector. That's why the firmware below links its vector table at the start of `flash`. Name a region that isn't in `MEMORY` and `Session::new` rejects it and tells you which names exist. The interrupt count comes from the `Cpu`, so you write it once. `icount_shift` of 2 is four nanoseconds per instruction. Every target we ship uses that.
### Interrupts
A peripheral's interrupt lines are declared on the instance, not on the model: `interrupts: &[3]` on `BEACON` says the beacon's one interrupt is line 3 of the part. The model raises it by position in that list, `context.raise(FRAME_DONE)` with `FRAME_DONE` at 0, so a model written once serves every instance of its layout whatever lines those instances have. Two beacons at two addresses are two `Peripheral`s with two `interrupts` tables and one `Beacon` type.
An interrupt is a level, not a pulse. The model raises it when the flag goes up and lowers it when the firmware clears the flag, which for the beacon is a read of `SR`. Drive the same level twice and the runtime reports it twice, so a model that should only report changes keeps the last level it drove. `Mcu::drain_interrupts` is how a unit test sees what a model drove, as the peripheral block above does.
On the board, `Target::interrupt_line` turns the part's interrupt number into the emulator's line. `InterruptLine::new` is the identity, which is right when the core's interrupt controller numbers its inputs the way the part does. An interrupt the route returns `None` for isn't dropped silently: `Session::unrouted` lists it. In the firmware, line 3 is row 3 of the interrupt table, right after the sixteen exceptions. To take the beacon's interrupt instead of polling for it, enable the line in the core's interrupt controller, set `IE`, and put a handler in that row:
```rust,ignore
const NVIC_ISER0: *mut u32 = 0xE000_E100 as *mut u32;
const BEACON_LINE: u32 = 3;
extern "C" fn beacon_done() {
read(BEACON_SR); // clears DONE, which lowers the line
}
#[unsafe(link_section = ".vector_table.interrupts")]
#[used]
static INTERRUPTS: [extern "C" fn(); INTERRUPT_POSITIONS] = {
let mut table = [default_handler as extern "C" fn(); INTERRUPT_POSITIONS];
table[BEACON_LINE as usize] = beacon_done;
table
};
// In `reset`, before the first byte:
write(NVIC_ISER0, 1 << BEACON_LINE);
write(BEACON_CR, EN | IE);
```
Interrupts go from a peripheral to the core. When one peripheral needs to tell another something, that's a request line, which is the next section.
### Request lines
A request line is how one peripheral tells another something. It's a level the driving model owns, like an interrupt, but it goes to other instances on the part instead of to the core. The beacon drives its request 0 high while a byte is on the wire and low when the frame ends: `context.request(SENT, Level::High)` in `write`, `Level::Low` in `on_event`, in the beacon's own numbering.
Who hears it is the bus description's business, not the beacon's. `reads_requests(TALLY.name)` in `Lantern::new` makes the tally a sink, and a sink gets every request on the part in `Device::on_request` as a `RequestChange`: the driving instance's name, its request number, and the level. The tally checks for the beacon's request 0 going high, counts it, and ignores everything else. That's how a DMA controller on a real part knows which stream fired: it knows its own wiring and reads the instance name off the change. Nothing reaches into another model's registers.
Requests are delivered by `Mcu::settle`, which carries out everything the models asked for after an access or an event: requests to their sinks, clocks to their readers, and any bus access a model asked for, which is why it takes the emulator's memory. A session settles after every access and every stop. In a unit test you call it yourself:
```rust
# use rigless::device::{Device, DeviceContext, Level, RegisterFile};
# use rigless::mcu::{Mcu, Single};
# use rigless::soc::{Access, Bus, Cpu, Layout, Peripheral, ReadAction, Region, RegisterSpec, Width, WriteModify};
# use rigless::time::{EventId, VirtualNanos};
#
# // The register table, the way the generator would emit it from a vendor's SVD. Written by hand here
# // because there's no vendor. `SR` is read-only to the firmware: the model sets and clears it.
# static REGISTERS: [RegisterSpec; 3] = [
# RegisterSpec { name: "CR", offset: 0x0, width: Width::W32, reset: 0, access: Access::ReadWrite, read: ReadAction::None, write: WriteModify::None, fields: &[] },
# RegisterSpec { name: "SR", offset: 0x4, width: Width::W32, reset: 0, access: Access::ReadOnly, read: ReadAction::None, write: WriteModify::None, fields: &[] },
# RegisterSpec { name: "DR", offset: 0x8, width: Width::W32, reset: 0, access: Access::ReadWrite, read: ReadAction::None, write: WriteModify::None, fields: &[] },
# ];
# #[derive(Debug, Clone, Copy, PartialEq, Eq)]
# enum Register { Cr, Sr, Dr }
# impl Layout for Register {
# const REGISTERS: &'static [RegisterSpec] = ®ISTERS;
# fn from_index(index: usize) -> Option<Self> { [Self::Cr, Self::Sr, Self::Dr].get(index).copied() }
# fn index(self) -> usize { self as usize }
# }
# const EN: u64 = 1;
# const IE: u64 = 2;
# const BUSY: u64 = 1;
# const DONE: u64 = 2;
# /// The beacon's one interrupt, by its position in the instance's list.
# const FRAME_DONE: usize = 0;
# /// The beacon's one request output, in its own numbering: high while a byte is on the wire.
# const SENT: usize = 0;
# /// One byte occupies the beacon for a millisecond of virtual time.
# const FRAME: VirtualNanos = VirtualNanos(1_000_000);
#
# /// The model keeps what the registers can't: the bytes it sent, and the event that ends the frame.
# #[derive(Debug, Default)]
# struct Beacon { sent: Vec<u8>, dropped: u32, frame_end: Option<EventId> }
# impl Device for Beacon {
# type Register = Register;
# fn read(&mut self, register: Register, registers: &mut RegisterFile, context: &mut DeviceContext<'_>) -> Option<u64> {
# if register != Register::Sr { return None; }
# let status = registers.get(Register::Sr);
# if status & DONE != 0 { registers.set(Register::Sr, status & !DONE); context.lower(FRAME_DONE); }
# Some(status)
# }
# fn write(&mut self, register: Register, value: u64, registers: &mut RegisterFile, context: &mut DeviceContext<'_>) {
# if register != Register::Dr { return; }
# let enabled = registers.get(Register::Cr) & EN != 0;
# let busy = registers.get(Register::Sr) & BUSY != 0;
# if !enabled || busy { self.dropped += 1; return; }
# self.sent.push(value as u8);
# registers.set(Register::Sr, BUSY);
# context.request(SENT, Level::High);
# self.frame_end = Some(context.schedule(VirtualNanos(context.now().0 + FRAME.0)));
# }
# fn on_event(&mut self, event: EventId, registers: &mut RegisterFile, context: &mut DeviceContext<'_>) {
# if self.frame_end != Some(event) { return; }
# self.frame_end = None;
# registers.set(Register::Sr, DONE);
# context.request(SENT, Level::Low);
# if registers.get(Register::Cr) & IE != 0 { context.raise(FRAME_DONE); }
# }
# }
#
# /// Where the one instance lives. A bus is a core, a memory map, and the instances at their addresses.
# const BEACON: Peripheral = Peripheral { name: "BEACON", base: 0x4000_0000, size: 0x100, interrupts: &[3], registers: ®ISTERS };
# const CPU: Cpu = Cpu { name: "lantern-core", fpu: false, interrupt_lines: 16 };
# static MEMORY: [Region; 2] = [Region::flash("flash", 0x0800_0000, 64 << 10), Region::ram("sram", 0x2000_0000, 16 << 10)];
#
# use rigless::device::{ErasedDevice, RequestChange};
# use rigless::soc::Error;
#
# /// A second peripheral: one read-only register that counts the bytes the beacon has sent. It never
# /// touches the beacon's registers. It listens to the beacon's request line, in the next sections.
# static TALLY_REGISTERS: [RegisterSpec; 1] = [
# RegisterSpec { name: "COUNT", offset: 0x0, width: Width::W32, reset: 0, access: Access::ReadOnly, read: ReadAction::None, write: WriteModify::None, fields: &[] },
# ];
# #[derive(Debug, Clone, Copy, PartialEq, Eq)]
# enum TallyRegister { Count }
# impl Layout for TallyRegister {
# const REGISTERS: &'static [RegisterSpec] = &TALLY_REGISTERS;
# fn from_index(index: usize) -> Option<Self> { (index == 0).then_some(Self::Count) }
# fn index(self) -> usize { 0 }
# }
# const TALLY: Peripheral = Peripheral { name: "TALLY", base: 0x4000_0100, size: 0x100, interrupts: &[], registers: &TALLY_REGISTERS };
# #[derive(Debug, Default)]
# struct Tally;
# impl Device for Tally {
# type Register = TallyRegister;
# fn on_request(&mut self, change: RequestChange, registers: &mut RegisterFile, _context: &mut DeviceContext<'_>) {
# // A sink hears every request on the part. This one wants the beacon's, rising.
# if change.instance == BEACON.name && change.request == SENT && change.level == Level::High {
# registers.set(TallyRegister::Count, registers.get(TallyRegister::Count) + 1);
# }
# }
# }
#
# /// The part, complete: the bus, and the models on it as fields.
# #[derive(Debug)]
# struct Lantern { bus: Bus, beacon: Beacon, beacon_slot: usize, tally: Tally, tally_slot: usize }
# impl Lantern {
# fn new() -> Result<Self, Error> {
# // `reads_requests` is the wiring: the tally hears what the beacon drives.
# let bus = Bus::builder(CPU, &MEMORY, [BEACON, TALLY]).reads_requests(TALLY.name).build()?;
# let beacon_slot = bus.slot_of(BEACON.name).ok_or(Error::UnknownInstance { instance: BEACON.name })?;
# let tally_slot = bus.slot_of(TALLY.name).ok_or(Error::UnknownInstance { instance: TALLY.name })?;
# let mut chip = Self { bus, beacon: Beacon::default(), beacon_slot, tally: Tally, tally_slot };
# chip.check()?;
# Ok(chip)
# }
# }
# impl Mcu for Lantern {
# fn bus(&self) -> &Bus { &self.bus }
# fn bus_mut(&mut self) -> &mut Bus { &mut self.bus }
# fn split(&mut self, slot: usize) -> (&mut Bus, Option<&mut dyn ErasedDevice>) {
# let Self { bus, beacon, beacon_slot, tally, tally_slot } = self;
# let model: Option<&mut dyn ErasedDevice> = if slot == *beacon_slot { Some(beacon) } else if slot == *tally_slot { Some(tally) } else { None };
# (bus, model)
# }
# }
#
use rigless::emulator::{Error as EmulatorError, Memory};
// Nothing here asks for a memory access, so the memory a settle is handed can refuse everything.
struct NoMemory;
impl Memory for NoMemory {
fn read(&mut self, address: u64, width: Width) -> Result<u64, EmulatorError> { Err(EmulatorError::Memory { address, width }) }
fn write(&mut self, address: u64, width: Width, _value: u64) -> Result<(), EmulatorError> { Err(EmulatorError::Memory { address, width }) }
}
let mut chip = Lantern::new()?;
let at = |register: Register| BEACON.base + u64::from(register.spec().offset);
let count = TALLY.base + u64::from(TallyRegister::Count.spec().offset);
chip.write(VirtualNanos(0), at(Register::Cr), Width::W32, EN)?;
chip.write(VirtualNanos(0), at(Register::Dr), Width::W32, u64::from(b'h'))?;
let _refused = chip.settle(&mut NoMemory)?;
assert_eq!(chip.read(VirtualNanos(0), count, Width::W32)?, 1, "the tally saw the beacon's request go high");
chip.advance_to(FRAME);
let _refused = chip.settle(&mut NoMemory)?;
chip.write(FRAME, at(Register::Dr), Width::W32, u64::from(b'i'))?;
let _refused = chip.settle(&mut NoMemory)?;
assert_eq!(chip.read(FRAME, count, Width::W32)?, 2, "and go high again for the second byte");
# Ok::<(), Box<dyn std::error::Error>>(())
```
Clocks work the same way: `Builder::clocked_by` says which instance runs on which output of another, `context.clock` drives a rate, and `Device::on_clock` receives it. A bit of one instance that another answers behind is `Builder::gate`, or `Builder::gate_writes` for writes to named registers. The `soc` module docs cover all three.
### The firmware
The firmware is a normal bare-metal image: `no_std`, your own vector table, your own linker script, built for the core's target. It's in the repository as [`firmware/template/`](https://gitlab.com/rigless-labs/rigless/-/tree/main/firmware/template), ready to copy. This one sends "hi" on the beacon the way a real driver would: wait until the last byte is done, write the next. Then it sleeps. That wait is the whole point of the test. A model that just stored registers would never clear `BUSY`, and the firmware would spin forever.
```rust,ignore
#![no_std]
#![no_main]
use core::arch::asm;
use core::ptr::{read_volatile, write_volatile};
// The beacon's registers, at the address the SoC put them.
const BEACON_CR: *mut u32 = 0x4000_0000 as *mut u32;
const BEACON_SR: *mut u32 = 0x4000_0004 as *mut u32;
const BEACON_DR: *mut u32 = 0x4000_0008 as *mut u32;
const EN: u32 = 1;
const BUSY: u32 = 1;
/// One row per interrupt line the core declares. None is used, but the table has them.
const INTERRUPT_POSITIONS: usize = 16;
fn read(register: *mut u32) -> u32 {
// SAFETY: a word-aligned register on the part's map, and this is the only code running.
unsafe { read_volatile(register) }
}
fn write(register: *mut u32, value: u32) {
// SAFETY: as for the read.
unsafe { write_volatile(register, value) };
}
/// Sends "hi" the way a driver would: wait until the last byte is done, write the next. Then sleeps.
#[unsafe(no_mangle)]
pub extern "C" fn reset() -> ! {
write(BEACON_CR, EN);
for &byte in b"hi" {
while read(BEACON_SR) & BUSY != 0 {
core::hint::spin_loop();
}
write(BEACON_DR, u32::from(byte));
}
loop {
// SAFETY: WFI sleeps until an interrupt. It touches no memory or register.
unsafe { asm!("wfi", options(nomem, nostack, preserves_flags)) };
}
}
extern "C" fn default_handler() {
loop { core::hint::spin_loop(); }
}
#[unsafe(link_section = ".vector_table.reset")]
#[used]
static RESET_VECTOR: extern "C" fn() -> ! = reset;
/// The fourteen exception vectors after the reset vector.
#[unsafe(link_section = ".vector_table.exceptions")]
#[used]
static EXCEPTIONS: [extern "C" fn(); 14] = [default_handler; 14];
#[unsafe(link_section = ".vector_table.interrupts")]
#[used]
static INTERRUPTS: [extern "C" fn(); INTERRUPT_POSITIONS] = [default_handler; INTERRUPT_POSITIONS];
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo<'_>) -> ! {
loop { core::hint::spin_loop(); }
}
```
The interrupt table needs one entry per line the core declares (`CPU.interrupt_lines`). The linker script puts the vector table at the start of flash, and its `MEMORY` has to use the same origins and lengths as the SoC's, because the emulator loads the image by the SoC's map:
```text
MEMORY
{
FLASH : ORIGIN = 0x08000000, LENGTH = 64K /* as MEMORY declares them, or the loader and the */
SRAM : ORIGIN = 0x20000000, LENGTH = 16K /* linker disagree about where the code is */
}
ENTRY(reset);
_stack_start = ORIGIN(SRAM) + LENGTH(SRAM);
SECTIONS
{
.vector_table ORIGIN(FLASH) :
{
LONG(_stack_start);
KEEP(*(.vector_table.reset));
KEEP(*(.vector_table.exceptions));
KEEP(*(.vector_table.interrupts));
} > FLASH
.text : { *(.text .text.*); } > FLASH
.rodata : { *(.rodata .rodata.*); } > FLASH
.bss (NOLOAD) : { *(.bss .bss.*); } > SRAM
/DISCARD/ : { *(.ARM.exidx .ARM.exidx.*); }
}
```
The firmware is its own Cargo project, not a workspace member, because it's built for a different target. `Cargo.toml` gets an empty `[workspace]` and `panic = "abort"` in both profiles. `.cargo/config.toml` sets the target and the linker script:
```toml
[build]
target = "thumbv7m-none-eabi" # a core without an FPU. thumbv7em-none-eabihf for one with
[target.thumbv7m-none-eabi]
rustflags = ["-C", "link-arg=-Tlink.x", "-C", "link-arg=--nmagic"]
```
```text
rustup target add thumbv7m-none-eabi
cargo build --release
```
### Running it
`rigless run` only knows the machines in `machine::all()`, so for your own board you run it from a binary of your own. `emulator::run_printed` does what the command does: it takes a `Session` on one of the board's targets and a `Start` naming the image, and prints every access, the stop and the reports, the same lines `rigless run` prints, to whatever you hand it:
```rust,no_run
# use rigless::device::{Device, DeviceContext, Level, RegisterFile};
# use rigless::mcu::{Mcu, Single};
# use rigless::soc::{Access, Bus, Cpu, Layout, Peripheral, ReadAction, Region, RegisterSpec, Width, WriteModify};
# use rigless::time::{EventId, VirtualNanos};
#
# // The register table, the way the generator would emit it from a vendor's SVD. Written by hand here
# // because there's no vendor. `SR` is read-only to the firmware: the model sets and clears it.
# static REGISTERS: [RegisterSpec; 3] = [
# RegisterSpec { name: "CR", offset: 0x0, width: Width::W32, reset: 0, access: Access::ReadWrite, read: ReadAction::None, write: WriteModify::None, fields: &[] },
# RegisterSpec { name: "SR", offset: 0x4, width: Width::W32, reset: 0, access: Access::ReadOnly, read: ReadAction::None, write: WriteModify::None, fields: &[] },
# RegisterSpec { name: "DR", offset: 0x8, width: Width::W32, reset: 0, access: Access::ReadWrite, read: ReadAction::None, write: WriteModify::None, fields: &[] },
# ];
# #[derive(Debug, Clone, Copy, PartialEq, Eq)]
# enum Register { Cr, Sr, Dr }
# impl Layout for Register {
# const REGISTERS: &'static [RegisterSpec] = ®ISTERS;
# fn from_index(index: usize) -> Option<Self> { [Self::Cr, Self::Sr, Self::Dr].get(index).copied() }
# fn index(self) -> usize { self as usize }
# }
# const EN: u64 = 1;
# const IE: u64 = 2;
# const BUSY: u64 = 1;
# const DONE: u64 = 2;
# /// The beacon's one interrupt, by its position in the instance's list.
# const FRAME_DONE: usize = 0;
# /// The beacon's one request output, in its own numbering: high while a byte is on the wire.
# const SENT: usize = 0;
# /// One byte occupies the beacon for a millisecond of virtual time.
# const FRAME: VirtualNanos = VirtualNanos(1_000_000);
#
# /// The model keeps what the registers can't: the bytes it sent, and the event that ends the frame.
# #[derive(Debug, Default)]
# struct Beacon { sent: Vec<u8>, dropped: u32, frame_end: Option<EventId> }
# impl Device for Beacon {
# type Register = Register;
# fn read(&mut self, register: Register, registers: &mut RegisterFile, context: &mut DeviceContext<'_>) -> Option<u64> {
# if register != Register::Sr { return None; }
# let status = registers.get(Register::Sr);
# if status & DONE != 0 { registers.set(Register::Sr, status & !DONE); context.lower(FRAME_DONE); }
# Some(status)
# }
# fn write(&mut self, register: Register, value: u64, registers: &mut RegisterFile, context: &mut DeviceContext<'_>) {
# if register != Register::Dr { return; }
# let enabled = registers.get(Register::Cr) & EN != 0;
# let busy = registers.get(Register::Sr) & BUSY != 0;
# if !enabled || busy { self.dropped += 1; return; }
# self.sent.push(value as u8);
# registers.set(Register::Sr, BUSY);
# context.request(SENT, Level::High);
# self.frame_end = Some(context.schedule(VirtualNanos(context.now().0 + FRAME.0)));
# }
# fn on_event(&mut self, event: EventId, registers: &mut RegisterFile, context: &mut DeviceContext<'_>) {
# if self.frame_end != Some(event) { return; }
# self.frame_end = None;
# registers.set(Register::Sr, DONE);
# context.request(SENT, Level::Low);
# if registers.get(Register::Cr) & IE != 0 { context.raise(FRAME_DONE); }
# }
# }
#
# /// Where the one instance lives. A bus is a core, a memory map, and the instances at their addresses.
# const BEACON: Peripheral = Peripheral { name: "BEACON", base: 0x4000_0000, size: 0x100, interrupts: &[3], registers: ®ISTERS };
# const CPU: Cpu = Cpu { name: "lantern-core", fpu: false, interrupt_lines: 16 };
# static MEMORY: [Region; 2] = [Region::flash("flash", 0x0800_0000, 64 << 10), Region::ram("sram", 0x2000_0000, 16 << 10)];
#
# use rigless::device::{ErasedDevice, RequestChange};
# use rigless::soc::Error;
#
# /// A second peripheral: one read-only register that counts the bytes the beacon has sent. It never
# /// touches the beacon's registers. It listens to the beacon's request line, in the next sections.
# static TALLY_REGISTERS: [RegisterSpec; 1] = [
# RegisterSpec { name: "COUNT", offset: 0x0, width: Width::W32, reset: 0, access: Access::ReadOnly, read: ReadAction::None, write: WriteModify::None, fields: &[] },
# ];
# #[derive(Debug, Clone, Copy, PartialEq, Eq)]
# enum TallyRegister { Count }
# impl Layout for TallyRegister {
# const REGISTERS: &'static [RegisterSpec] = &TALLY_REGISTERS;
# fn from_index(index: usize) -> Option<Self> { (index == 0).then_some(Self::Count) }
# fn index(self) -> usize { 0 }
# }
# const TALLY: Peripheral = Peripheral { name: "TALLY", base: 0x4000_0100, size: 0x100, interrupts: &[], registers: &TALLY_REGISTERS };
# #[derive(Debug, Default)]
# struct Tally;
# impl Device for Tally {
# type Register = TallyRegister;
# fn on_request(&mut self, change: RequestChange, registers: &mut RegisterFile, _context: &mut DeviceContext<'_>) {
# // A sink hears every request on the part. This one wants the beacon's, rising.
# if change.instance == BEACON.name && change.request == SENT && change.level == Level::High {
# registers.set(TallyRegister::Count, registers.get(TallyRegister::Count) + 1);
# }
# }
# }
#
# /// The part, complete: the bus, and the models on it as fields.
# #[derive(Debug)]
# struct Lantern { bus: Bus, beacon: Beacon, beacon_slot: usize, tally: Tally, tally_slot: usize }
# impl Lantern {
# fn new() -> Result<Self, Error> {
# // `reads_requests` is the wiring: the tally hears what the beacon drives.
# let bus = Bus::builder(CPU, &MEMORY, [BEACON, TALLY]).reads_requests(TALLY.name).build()?;
# let beacon_slot = bus.slot_of(BEACON.name).ok_or(Error::UnknownInstance { instance: BEACON.name })?;
# let tally_slot = bus.slot_of(TALLY.name).ok_or(Error::UnknownInstance { instance: TALLY.name })?;
# let mut chip = Self { bus, beacon: Beacon::default(), beacon_slot, tally: Tally, tally_slot };
# chip.check()?;
# Ok(chip)
# }
# }
# impl Mcu for Lantern {
# fn bus(&self) -> &Bus { &self.bus }
# fn bus_mut(&mut self) -> &mut Bus { &mut self.bus }
# fn split(&mut self, slot: usize) -> (&mut Bus, Option<&mut dyn ErasedDevice>) {
# let Self { bus, beacon, beacon_slot, tally, tally_slot } = self;
# let model: Option<&mut dyn ErasedDevice> = if slot == *beacon_slot { Some(beacon) } else if slot == *tally_slot { Some(tally) } else { None };
# (bus, model)
# }
# }
#
# use std::num::{NonZeroU32, NonZeroU64};
# use rigless::cpu;
# use rigless::emulator::InterruptLine;
# use rigless::machine::{Board, Target, armv7m};
# use rigless::session::Session;
#
# /// The board: one Lantern and nothing else.
# #[derive(Debug)]
# struct LanternBoard { chip: Lantern }
# impl Board for LanternBoard {
# fn name(&self) -> &'static str { "lantern" }
# fn target_names(&self) -> Vec<&'static str> { vec!["lantern-core"] }
# fn mcu(&mut self, target: usize) -> Option<&mut dyn Mcu> { (target == 0).then_some(&mut self.chip) }
# fn targets(&mut self) -> Vec<Target<'_>> {
# let emulator = armv7m::Core {
# model: armv7m::Model::M3, // the part declares no FPU
# clock_hz: NonZeroU64::new(16_000_000).unwrap(), // drives the core's own timers
# interrupt_lines: NonZeroU32::new(u32::from(Mcu::cpu(&self.chip).interrupt_lines)).unwrap(),
# priority_bits: 0, // zero leaves it to the core
# refclk_divider: NonZeroU32::new(8).unwrap(),
# icount_shift: 2, // four nanoseconds an instruction
# boot: "flash", // the region at the reset vector
# }
# .command(&cpu::armv7m::windows(Mcu::bus(&self.chip)));
# vec![Target { name: "lantern-core", mcu: &mut self.chip, emulator, interrupt_line: InterruptLine::new }]
# }
# }
#
use std::io;
use std::path::Path;
use rigless::emulator::{self, Start};
let session = Session::new(Box::new(LanternBoard { chip: Lantern::new()? }), 0, VirtualNanos(3 * FRAME.0))?;
let start = Start {
firmware: Path::new("firmware/template/target/thumbv7m-none-eabi/release/template"),
debugger: None,
wait_for_debugger: false,
};
let printed = emulator::run_printed(session, &start, None, io::stdout())?;
assert_eq!(printed.session.board().chip.beacon.sent, b"hi");
# Ok::<(), Box<dyn std::error::Error>>(())
```
That's a whole `main`. The flags map straight across: the session's `until` is `--until`, and `emulator::NO_DEADLINE` means run until interrupted. `debugger: Some(address)` in `Start` is `--gdb`, where a port alone is that port on `127.0.0.1`. The third argument is `--outputs`, the period to read the board's outputs at. `printed.coverage` is what `--coverage` writes, so `fs::write(path, printed.coverage.to_string())` gives you the same file. A host on one of the board's wires is `board.attach_host(...)` before you make the session, which `rigless run` does for every wire `Board::hosts` names, at the board's port or the one `--host` gives. `printed.session` hands the board back, so you can look at your models once the run is over.
To do something other than print, write your own `Driver` and call `emulator::run`. A `Driver` gets every access as it happens, and every stop at an instant the run was asked to reach. That's what a test wants to assert on:
```rust,no_run
# use rigless::device::{Device, DeviceContext, Level, RegisterFile};
# use rigless::mcu::{Mcu, Single};
# use rigless::soc::{Access, Bus, Cpu, Layout, Peripheral, ReadAction, Region, RegisterSpec, Width, WriteModify};
# use rigless::time::{EventId, VirtualNanos};
#
# // The register table, the way the generator would emit it from a vendor's SVD. Written by hand here
# // because there's no vendor. `SR` is read-only to the firmware: the model sets and clears it.
# static REGISTERS: [RegisterSpec; 3] = [
# RegisterSpec { name: "CR", offset: 0x0, width: Width::W32, reset: 0, access: Access::ReadWrite, read: ReadAction::None, write: WriteModify::None, fields: &[] },
# RegisterSpec { name: "SR", offset: 0x4, width: Width::W32, reset: 0, access: Access::ReadOnly, read: ReadAction::None, write: WriteModify::None, fields: &[] },
# RegisterSpec { name: "DR", offset: 0x8, width: Width::W32, reset: 0, access: Access::ReadWrite, read: ReadAction::None, write: WriteModify::None, fields: &[] },
# ];
# #[derive(Debug, Clone, Copy, PartialEq, Eq)]
# enum Register { Cr, Sr, Dr }
# impl Layout for Register {
# const REGISTERS: &'static [RegisterSpec] = ®ISTERS;
# fn from_index(index: usize) -> Option<Self> { [Self::Cr, Self::Sr, Self::Dr].get(index).copied() }
# fn index(self) -> usize { self as usize }
# }
# const EN: u64 = 1;
# const IE: u64 = 2;
# const BUSY: u64 = 1;
# const DONE: u64 = 2;
# /// The beacon's one interrupt, by its position in the instance's list.
# const FRAME_DONE: usize = 0;
# /// The beacon's one request output, in its own numbering: high while a byte is on the wire.
# const SENT: usize = 0;
# /// One byte occupies the beacon for a millisecond of virtual time.
# const FRAME: VirtualNanos = VirtualNanos(1_000_000);
#
# /// The model keeps what the registers can't: the bytes it sent, and the event that ends the frame.
# #[derive(Debug, Default)]
# struct Beacon { sent: Vec<u8>, dropped: u32, frame_end: Option<EventId> }
# impl Device for Beacon {
# type Register = Register;
# fn read(&mut self, register: Register, registers: &mut RegisterFile, context: &mut DeviceContext<'_>) -> Option<u64> {
# if register != Register::Sr { return None; }
# let status = registers.get(Register::Sr);
# if status & DONE != 0 { registers.set(Register::Sr, status & !DONE); context.lower(FRAME_DONE); }
# Some(status)
# }
# fn write(&mut self, register: Register, value: u64, registers: &mut RegisterFile, context: &mut DeviceContext<'_>) {
# if register != Register::Dr { return; }
# let enabled = registers.get(Register::Cr) & EN != 0;
# let busy = registers.get(Register::Sr) & BUSY != 0;
# if !enabled || busy { self.dropped += 1; return; }
# self.sent.push(value as u8);
# registers.set(Register::Sr, BUSY);
# context.request(SENT, Level::High);
# self.frame_end = Some(context.schedule(VirtualNanos(context.now().0 + FRAME.0)));
# }
# fn on_event(&mut self, event: EventId, registers: &mut RegisterFile, context: &mut DeviceContext<'_>) {
# if self.frame_end != Some(event) { return; }
# self.frame_end = None;
# registers.set(Register::Sr, DONE);
# context.request(SENT, Level::Low);
# if registers.get(Register::Cr) & IE != 0 { context.raise(FRAME_DONE); }
# }
# }
#
# /// Where the one instance lives. A bus is a core, a memory map, and the instances at their addresses.
# const BEACON: Peripheral = Peripheral { name: "BEACON", base: 0x4000_0000, size: 0x100, interrupts: &[3], registers: ®ISTERS };
# const CPU: Cpu = Cpu { name: "lantern-core", fpu: false, interrupt_lines: 16 };
# static MEMORY: [Region; 2] = [Region::flash("flash", 0x0800_0000, 64 << 10), Region::ram("sram", 0x2000_0000, 16 << 10)];
#
# use rigless::device::{ErasedDevice, RequestChange};
# use rigless::soc::Error;
#
# /// A second peripheral: one read-only register that counts the bytes the beacon has sent. It never
# /// touches the beacon's registers. It listens to the beacon's request line, in the next sections.
# static TALLY_REGISTERS: [RegisterSpec; 1] = [
# RegisterSpec { name: "COUNT", offset: 0x0, width: Width::W32, reset: 0, access: Access::ReadOnly, read: ReadAction::None, write: WriteModify::None, fields: &[] },
# ];
# #[derive(Debug, Clone, Copy, PartialEq, Eq)]
# enum TallyRegister { Count }
# impl Layout for TallyRegister {
# const REGISTERS: &'static [RegisterSpec] = &TALLY_REGISTERS;
# fn from_index(index: usize) -> Option<Self> { (index == 0).then_some(Self::Count) }
# fn index(self) -> usize { 0 }
# }
# const TALLY: Peripheral = Peripheral { name: "TALLY", base: 0x4000_0100, size: 0x100, interrupts: &[], registers: &TALLY_REGISTERS };
# #[derive(Debug, Default)]
# struct Tally;
# impl Device for Tally {
# type Register = TallyRegister;
# fn on_request(&mut self, change: RequestChange, registers: &mut RegisterFile, _context: &mut DeviceContext<'_>) {
# // A sink hears every request on the part. This one wants the beacon's, rising.
# if change.instance == BEACON.name && change.request == SENT && change.level == Level::High {
# registers.set(TallyRegister::Count, registers.get(TallyRegister::Count) + 1);
# }
# }
# }
#
# /// The part, complete: the bus, and the models on it as fields.
# #[derive(Debug)]
# struct Lantern { bus: Bus, beacon: Beacon, beacon_slot: usize, tally: Tally, tally_slot: usize }
# impl Lantern {
# fn new() -> Result<Self, Error> {
# // `reads_requests` is the wiring: the tally hears what the beacon drives.
# let bus = Bus::builder(CPU, &MEMORY, [BEACON, TALLY]).reads_requests(TALLY.name).build()?;
# let beacon_slot = bus.slot_of(BEACON.name).ok_or(Error::UnknownInstance { instance: BEACON.name })?;
# let tally_slot = bus.slot_of(TALLY.name).ok_or(Error::UnknownInstance { instance: TALLY.name })?;
# let mut chip = Self { bus, beacon: Beacon::default(), beacon_slot, tally: Tally, tally_slot };
# chip.check()?;
# Ok(chip)
# }
# }
# impl Mcu for Lantern {
# fn bus(&self) -> &Bus { &self.bus }
# fn bus_mut(&mut self) -> &mut Bus { &mut self.bus }
# fn split(&mut self, slot: usize) -> (&mut Bus, Option<&mut dyn ErasedDevice>) {
# let Self { bus, beacon, beacon_slot, tally, tally_slot } = self;
# let model: Option<&mut dyn ErasedDevice> = if slot == *beacon_slot { Some(beacon) } else if slot == *tally_slot { Some(tally) } else { None };
# (bus, model)
# }
# }
#
# use std::num::{NonZeroU32, NonZeroU64};
# use rigless::cpu;
# use rigless::emulator::InterruptLine;
# use rigless::machine::{Board, Target, armv7m};
# use rigless::session::Session;
#
# /// The board: one Lantern and nothing else.
# #[derive(Debug)]
# struct LanternBoard { chip: Lantern }
# impl Board for LanternBoard {
# fn name(&self) -> &'static str { "lantern" }
# fn target_names(&self) -> Vec<&'static str> { vec!["lantern-core"] }
# fn mcu(&mut self, target: usize) -> Option<&mut dyn Mcu> { (target == 0).then_some(&mut self.chip) }
# fn targets(&mut self) -> Vec<Target<'_>> {
# let emulator = armv7m::Core {
# model: armv7m::Model::M3, // the part declares no FPU
# clock_hz: NonZeroU64::new(16_000_000).unwrap(), // drives the core's own timers
# interrupt_lines: NonZeroU32::new(u32::from(Mcu::cpu(&self.chip).interrupt_lines)).unwrap(),
# priority_bits: 0, // zero leaves it to the core
# refclk_divider: NonZeroU32::new(8).unwrap(),
# icount_shift: 2, // four nanoseconds an instruction
# boot: "flash", // the region at the reset vector
# }
# .command(&cpu::armv7m::windows(Mcu::bus(&self.chip)));
# vec![Target { name: "lantern-core", mcu: &mut self.chip, emulator, interrupt_line: InterruptLine::new }]
# }
# }
#
use std::path::Path;
use rigless::emulator::{self, Driver, Finished, Start, Stop, StopReason};
/// What the run does on every access and every stop. This one records accesses and lets the run
/// reach its deadline. `rigless run` prints them instead, and a test asserts on them.
struct Recording(Vec<(emulator::Access, u64)>);
impl<D: Board + ?Sized> Driver<D> for Recording {
fn on_access(&mut self, access: &emulator::Access, value: u64) { self.0.push((*access, value)); }
fn on_stop(&mut self, _session: &mut Session<D>, _stop: Stop) -> Option<VirtualNanos> { None }
}
let session = Session::new(Box::new(LanternBoard { chip: Lantern::new()? }), 0, VirtualNanos(3 * FRAME.0))?;
let start = Start {
firmware: Path::new("firmware/template/target/thumbv7m-none-eabi/release/template"),
debugger: None,
wait_for_debugger: false,
};
let Finished { session, driver, stop } = emulator::run(session, &start, Recording(Vec::new()))?;
// What the beacon saw, and what the core did.
assert!(session.unmapped().is_empty(), "an address no model claims: {:?}", session.unmapped());
assert_eq!(session.board().chip.beacon.sent, b"hi");
let writes = driver.0.iter().filter(|(access, _)| access.address == BEACON.base + 0x8).count();
assert_eq!(writes, 2, "one write per byte, the second only once the first's frame had passed");
let stop = stop.expect("the driver let the run reach its deadline");
assert_eq!(stop.reason, StopReason::Idle, "the firmware sleeps once it has said hi");
# Ok::<(), Box<dyn std::error::Error>>(())
```
`Driver::on_stop` returns the next instant to run to, or `None` to stop. `Finished` gives you back the session (board included), the driver (with whatever it recorded), and the stop that ended the run. `StopReason::Idle` means the core is halted waiting for an interrupt with nothing pending, and the emulator skips ahead instead of waiting.
**The emulator runs once per process.** It has global state, so a second `emulator::run` in the same process returns `Error::AlreadyRan`. Every test that boots a firmware needs its own process: run them with `cargo nextest`, which does exactly that, or keep one firmware test per test binary.
### What goes wrong, and what it says
| What happened | What you see | What it means |
|---|---|---|
| `boot` names a region that isn't in `MEMORY` | `lantern puts a region called rom at 0x0, but lantern-core has no such region (it has flash, sram)` from `Session::new` | `armv7m::Core::boot` has to be one of the `MEMORY` names |
| A second run in the same process | `the emulator has already run in this process, and it runs once: ...` from `emulator::run` | One firmware test per process. Use `cargo nextest` |
| The firmware hit an address no model claims | `unmapped at=0x40000200 pc=0x08000098` in `Session::unmapped` | Wrong address in the firmware, or the instance isn't on the bus |
| `link.x` and `MEMORY` disagree | The core fetches something that isn't the vector table | `link.x` has to use the SoC's origins and lengths |
## 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 emulator'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` is the GPIO port of chapter 8. `pwr` is the power controller of section 5.5, and `rcc` the reset and clock controller of chapter 6. `spi` is the serial peripheral interface of chapter 28. `tim` is the general-purpose timer of chapter 18 against the `tim2` layout, answering for all four of them, and through its `Advanced` family it's the advanced-control timer of chapter 17, answering for TIM1 and TIM8. `dma` is the direct memory access controller of chapter 10, answering for both of them. `sdio` is the card interface of chapter 31. `usart` is the serial port of chapter 30, answering for all eight from the chapter's two tables. `otg` is the reset register of the USB core of chapter 34 and nothing else, which is a partial and says so. `adc` is the analogue-to-digital converter of chapter 13, answering for all three of them and for the common block they share. `rtc` is the backup domain of chapter 26 as far as the reset that clears it, a partial for the same reason and saying so. `i2c` is the inter-integrated circuit interface of chapter 27 as a controller, answering for all three, with target mode left out and saying so. `flash` is the flash memory interface of chapter 3, its registers and not the array behind them. `iwdg` is the independent watchdog of chapter 21, whose end of count restarts the part. `dbg` is the MCU debug component of section 38.16 |
| A chip | one module under `chip`, naming no SoC | `chip::sd` is the card end of a secure digital bus, with a memory card that answers it. `chip::fram` is a ferroelectric memory. `chip::mpu9250` with the `chip::ak8963` compass die inside it, `chip::icm20608`, `chip::lis3mdl` and `chip::ms5611` are the inertial units, the magnetometer and the barometer of the first board, each written against its own datasheet. `chip::nmea` is a receiver that reports a position as NMEA sentences on a serial port |
| A core family | one module under `machine`, compiled always, naming no SoC | `machine::armv7m`, mirroring the emulator's `rigless-armv7m` machine. Under `cpu`, `cpu::armv7m` holds the blocks the architecture fixes at an address |
| A machine | one module under `machine` declaring a board struct that implements `Board` | `machine::pixracer` is one STM32F427 with its crystals, its rail, the chips on its buses and its six outputs. `machine::stm32f427_bench` is the part alone, for proving it |
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::{Layout, Width, stm32f427};
use rigless::soc::stm32f427::Stm32f427;
use rigless::time::VirtualNanos;
let mut mcu = Stm32f427::new()?;
assert_eq!(mcu.cpu(), stm32f427::CPU);
// An instance answers behind whatever the part says has to be true elsewhere. Here that is the port's
// clock enable, RM0090 rev 22 section 2.1.11, so a firmware turns it on first and so does this.
use stm32f427::generated::{GPIOA, RCC, rcc};
let enables = RCC.base + u64::from(rcc::Register::Ahb1enr.spec().offset);
mcu.write(VirtualNanos(0), enables, Width::W32, rcc::ahb1enr::GPIOAEN.mask())?;
mcu.write(VirtualNanos(0), 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 doesn't: `Behaviour | Source | Reason`, one row for each clause of the chapter and each applicable erratum the model leaves out. Each row says why. That's a wire the runtime doesn't 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.
**Two instances of one part are joined by four things and nothing else**, because a model may not reach another model. A **gate** is a bit of one instance that another answers behind, which is how a peripheral whose clock is off stops answering its registers. A **request line** is a level one drives and another reads, which is how a serial interface tells a DMA controller it has a byte. A **clock** is a rate one drives and another runs on, which is how the tree tells a timer what it counts, and the timer keeps its count across the change. A **requested access** is a model asking the runtime to make a bus or memory access on its behalf and being handed the answer, which is how that controller then moves the byte. The first three are declared in the part's own description beside its corrections, and `rigless::soc`'s documentation is where all four are written down. A model that needs none of them declares none.
A model's boundary to the world **outside** the part is still methods, because the runtime carries no pin levels. 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 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<General>` and `Tim<Advanced>` take `with_clock(hz)` from the part for the rate they count until the tree drives another, `drive_input`, `drive_etr`, `drive_internal_trigger`, and on the advanced family `drive_break` for the `BKIN` pin, and answer `output(channel)`, `complementary_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. It answers the whole clock tree: `sysclk`, `hclk`, `pclk1`, `pclk2`, `timer_clock`, `pll48`, `rtc_clock`, `mco1`, `mco2`, `plli2s_clock` and `sai_clock`. It drives the seven outputs something on the part runs on as clocks, so a timer, a serial interface or port, an I²C interface, the converters, the card interface, the flash interface, the watchdog and the processor's counter follow a firmware's retuning without naming the tree. The board's rail goes in through the MCU's own `set_supply(millivolts)`, which is also how it moves, and feeds both the regulator, which answers `scale`, and the flash interface's latency check. `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, like `mcu.rcc` or `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. A target is the MCU borrowed, the `EmulatorCommand` its emulator takes, and the route from that MCU's interrupt numbers to the emulator's lines. `Board::mcu` is the same MCU by index, which is how the session reaches it on every access.
**A machine doesn't choose what the part has.** 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's the MCU, complete and runnable on its own. A board adds the crystals on the oscillator pins, the rail the regulator sees, and the integrated circuits on its buses, and puts them on the part's fields when the board is built. A board that fits no crystal has no crystal. The part still has its whole clock tree.
The whole of it is a tree of concrete types, so every step of reaching anything is a field, up to a chip on a wire, which the interface's `chip_mut` hands back as the type that was hung:
```text
Pixracer ── mcu ──▶ Stm32f427 ── rcc ──▶ Rcc
├─ hse ──▶ Crystal ├─ pwr ──▶ Pwr
├─ lse ──▶ Crystal ├─ spi ──▶ [Spi; 6] ── chip_mut::<P>(n) ──▶ P
└─ rail ─▶ Supply └─ tim ──▶ [Tim<General>; 4]
```
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 emulator'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 emulator 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 FRAM and its barometer hang on the second serial interface and its two inertial units and its magnetometer on the first, each behind the select the board description names. Its `serial1`, the telemetry port a ground station talks to, is `USART2`, and `rigless run` listens for one on port 5760. Its `serial3`, `UART4`, has a satellite receiver on the far end that reports where the world says the board is, in NMEA sentences. Its six outputs are the description's `PWM(1)` to `PWM(6)`, four channels of the first advanced-control timer and two of a general-purpose one, read on the speed controllers' curve. Its chips read the body's axes as their own, since the firmware's driver undoes the description's rotation on the way in, and it's built sensing a level vehicle at rest.
`Stm32f427Bench` is the same MCU with as little fitted as still lets something happen: a rail, one crystal, a peer on each serial interface and each serial port, and a seven-bit and a ten-bit one on the first I²C interface, each answering a known pattern and recording every word it was sent, known voltages on the converters' pins, and a memory card on the card interface. It's the part run standalone, which is what a conformance run wants. A board's crystal and its sensors are things that run shouldn't depend on, and a part has to be provable before there's 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, each a call on the builder or a struct update over a generated constant, 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 feature checks in the `check` job of `.gitlab-ci.yml`, which build 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 for the peripheral rather than for that layout: `tim.rs` is written against `tim2`, `gpio.rs` against `gpioa`.
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 can't 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, a slot in `Slots`, a row in `Field::all`, and an arm in `Field::instance`, in the slot-resolving match, and in `split`. Only `Field::all` isn't a match the compiler checks, so it's the one to remember. `Mcu::check` catches a field wired to the wrong slot when the part is built.
5. Where the vendor gives one instance a shorter table than the layout the model is written against, and that shorter table is a prefix of it, override `Device::layout` to return the instance's own. That is what lets `tim` answer for all four of chapter 18's timers across two tables, and it is what the wiring check holds each slot to.
6. Add the model and its test file to `every_test_a_models_table_names_is_a_test_that_exists`, which is what holds the tables of step 2 to naming tests that exist. A model missing from that list is a model whose tables nothing checks.
7. Add the instance's register offsets to the bench firmware's walk. **Binding a model claims the whole instance**, so the coverage gate wants every register of it reached, not only the ones the model answers for. A partial model is still walked whole and accounts for the rest in its second table.
8. Expect the part's own checks to fire, and answer each with a reason rather than by loosening it. A model with no clock enable behind it, or an instance the manual gives no interrupt line, is a real thing the part does. Each has an exception table next to the rule it breaks, so the reason is found where the rule is.
### Adding a chip
A chip is a board's, not a part's. It lives under `src/chip`, is written against its own datasheet, and names no SoC. It implements the seam the controller it hangs on calls: `chip::spi::Peer` for a serial interface, `chip::uart::Peer` for a serial port, `chip::sd::Card` for the card interface, and `chip::i2c::Peer` for an I²C bus, whether the part's or one inside another chip. The seam lives beside the chips, since a chip names no SoC. Its own state is its account of what it was sent. A stand-in such as `Pattern`, having none, keeps a record instead. It opens with the two tables above, and the first has the same four columns, so the same check that holds a peripheral's tables to naming real tests holds a chip's. Add it to that check's list. The board hangs it in `fit`. On a serial interface it hangs behind a `Select` naming the port, the pin and the level that selects it, and the pin's level reaches the interface as the port's request line (ADR-0023). On an I²C bus its address is what selects it. A test drives it directly through the seam, taking the instant from a controller's context. ArduPilot, or whatever firmware the board runs, is what proves the chip and the select together. A chip that measures something has setters in its datasheet's terms, like `set_rate`, `set_field` and `set_pressure`, each on its own axes, and keeps what its transducer measures from then on. The board reaches them through the interface's `chip_mut`, asking for the chip back as the type it hung, and it's the board that turns the body's axes into the chip's.
### Adding a machine
Declare a struct whose fields are what's on the board: its MCU or MCUs, its crystals, its rail and the chips on its buses. 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 an `EmulatorCommand` from its core family's module and the interrupt route, which is `InterruptLine::new` wherever the controller is inside the core, and `mcu` hands the same MCU back by index. Then add it to the table `machine::all()` returns, which is how `rigless run` and `rigless run --link` both find it. A board with two MCUs is two targets. A process runs one of them, `--target` says which, and `targets` answers with the same MCUs in the same order every time. A board that drives or senses something implements `Edge` with its own `Outputs` and `Signals`. Its `REST` is what the board senses when it's built, and both types implement `link::Wire`, which is what lets `edge` answer with the board itself. `drive` reads each output off the pin its description wires it to, as whatever that pin carries means to the thing on it, and `sense` turns the body's axes into each chip's own before handing the signals over. Its `edge` answers itself.
### Adding a core family
One module beside `armv7m`, mirroring the machine the emulator 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. A property the machine doesn't declare fails `qemu_init` in every test that boots, which is what pins the two together. Nothing in `emulator` changes, and neither does any other family's module.
## 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, where 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[4];
}
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
```
## Building and testing
`.gitlab-ci.yml` is the source of truth for how this is built and tested. Every job's `script` is a command you can run yourself, so neither this page nor the README copies them, and neither can then tell you something the pipeline has stopped doing. What belongs here is what the suite proves.
**Three test binaries boot a firmware** on the emulator built in, and they need one built under `firmware/`, failing rather than skipping without it and saying so. `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 two runs of the binary that print the same thing to the byte. `conformance` is the bench, every check the conformance firmware declares, held to a budget the job enforces. `link` spawns the binary as an engine would and steps it across the link. The emulator has one set of global state and so runs once per process, which is why these run under `cargo nextest` rather than `cargo test`.
**Everything else runs with no emulator**: the session driven by hand as the emulator drives it, the link's frames read back as written and refused when they aren't, and every model against its manual. The runtime alone, with no SoC and no command line, is what the `--no-default-features` checks build. Miri runs it too, with the emulator absent, since it can't execute linked C.
## 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. 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.