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 the crate's own CPU core translates the firmware's instructions to host code and runs them, calling those models 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, each behind a feature of its own, 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 core is the crate's own, in Rust: a decoder and translator per CPU family that lowers blocks of the firmware's instructions to host code with Cranelift. 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, under MIT or Apache-2.0 at your option. 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
│ ├── snapshot.rs
│ ├── image.rs the images a run places, and where the core boots
│ ├── elf.rs
│ ├── symbols.rs the firmware's own names, which a fault can name an instruction by
│ ├── coverage.rs
│ ├── emulator.rs
│ ├── emulator/ the runner, the code generator and memory, which every family shares
│ ├── 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
│ ├── stm32h743.rs
│ └── stm32h743/ the vendor's file, the generated tables, the map and its corrections, and the MCU with one model per peripheral
├── cpu.rs
├── cpu/ one module per core family, the whole core: decode, translation, exceptions
├── 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
│ ├── flight.rs what every flight controller board senses and drives, so one plant flies any of them
│ ├── matekh743.rs, pixracer.rs one per board, behind a feature of its own: 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.rs the `rigless` command, over any table of machines
└── main.rs the `rigless` binary: the command over `machine::all()`
```
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 |
| `soc::stm32h743` | The STM32H743, behind the feature of that name: ST's own SVD, the tables generated from it, the memory map and the corrections from RM0433 rev 9 in `memory_map`, and `Stm32h743` itself in `peripherals`. That's the whole MCU, holding a model per instance as a field. A firmware's access to undefined space inside a peripheral reads zero and drops a write, as RM0433 says the part does |
| `cpu` | One module per core family, each the whole core: its decoder and translator, its exception model and interrupt controller, the address space the processor answers itself, its floating-point unit and MPU, how it boots and what a debugger sees, and the blocks the architecture fixes at an address, 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, the seam each kind attaches through: `spi::Peer`, `i2c::Peer`, `uart::Peer` and `sd::Card`, and `Place`, where a controller holds a chip, a board silences it, and a trigger counts its transactions and fires a flip or a drop. `uart::Errors` is how often words arrive garbled on a line, which a board sets and a serial port's model asks about each word |
| `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 and says what the part does at a reset and a stop, and `Operate`, with `read`, `write` and `advance_to` written once against it for every `Mcu`. `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 `CoreSpec` a machine module fills in to say what one target's core is, the `Processor` a core family implements and the `CoreBus` it's lent, and `run`, which runs the target's core |
| `link` | The step protocol between an engine and a target it spawned, version 5: the target says what it carries in `Manifest`, its conditions among it, then the two meet once per plant step on stdin and stdout, a fault the engine arms fires at an exact point the target counts to, and at any stop the engine can have the target write a snapshot of itself. `serve` is the target's side, for any board with an edge |
| `snapshot` | A target's whole state at a stop, as a file a new process starts from: the core's own state, every writable region of memory, and every model, chip and board through the `Store` trait each implements. `take` writes one and `Snapshot` reads one back |
| `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, and its own conditions, what can be wrong with the board itself. `Coupled` is the same edge as bytes, `Condition`, `Sensed` and `Reading` are what a stuck, biased or noisy chip reads through, `draw` and `normal` are every random draw a board makes, keyed by the run's seed, and `Pulse` is what a timer's pin carries |
| `machine::armv7m` | The ARMv7-M machine: one typed field per property of a target's, the `Config` the family makes its core from, and the vector table a run boots from |
| `machine::pixracer` | The Pixracer, behind the feature of that name: one STM32F427, its two crystals, its rail, the chips on its buses, and the six outputs and the mountings its description names |
| `machine::matekh743` | The Matek H743, behind the feature of that name: one STM32H743, its crystal and rail, the two inertial units, the barometer and the external compass on its buses, the GPS receiver, the battery's senses, and the thirteen outputs and the mountings its description names |
| `machine::flight` | What every flight controller board senses of its vehicle and drives on it: the `Signals` both boards take, `AT_REST`, and the `Actuator`s their outputs are, through `Actuate`, so one plant flies either |
| `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 I2C 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 |
| `image` | The images a run places in memory, where each section goes, where the core boots from, and every refusal before the core runs |
| `elf` | The smallest ELF reader the runtime needs: the symbols, and each section's bytes with where the file stores them |
| `symbols` | The firmware's own names for its code and data, read from its ELF symbol table, which a trigger names an instruction by |
| `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 |
| `cli` | The `rigless` command, behind the `cli` feature. `cli::main` runs it over a table of machines, so a binary of your own gives your boards everything `rigless run` has |
| Feature | Default | What it adds |
|---|---|---|
| `cli` | yes | The `rigless` binary, `cli::main` for a binary of your own, and the argument parsing both need, with the kernels `rigless-rtos` walks for its debugger. It turns on `svd` too |
| `svd` | through `cli` | `rigless::svd`, so a build can generate tables as a library |
| `stm32f427` | through `pixracer` | The STM32F427 SoC, its models, and the `stm32f427-bench` machine |
| `stm32h743` | no | The STM32H743 SoC, its tables, its memory map, its corrections and its models |
| `pixracer` | yes | The `pixracer` machine, and the STM32F427 it's built on |
| `matekh743` | no | The `matekh743` machine, and the STM32H743 it's built on |
The bare-metal test firmware lives under `firmware/`, one directory per SoC. 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, so what you write is a type the compiler checks against the runtime. The one downcast is a chip's: a controller holds its chip as the seam's trait object in a `chip::Place`, and its `chip_mut` hands the board back the type it hung, through `Any`.
| 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`, `chip::parallel::Peer` or `chip::sd::Card`, and setters in its datasheet's terms where the chip measures something | [A chip on a line of your own](#a-chip-on-a-line-of-your-own), [Adding a chip](#adding-a-chip) |
| A memory-mapped chip on a part you didn't write | `chip::parallel::Peer` in a `soc::Window` the part defines, attached with `mcu::Wired`. Add memory with `soc::Bus::add_region` | [Extending the STM32F427](#extending-the-stm32f427) |
| A host on a wire | `chip::uart::Peer` too: `chip::uart::TcpPeer` 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`, `Signals` and `Conditions` where it drives or senses something | [The board](#the-board), [The edge](#the-edge), [Adding a machine](#adding-a-machine) |
| What a snapshot keeps | `snapshot::Store`, on a board and on every chip, which `Board` and each chip seam require. Saving the MCU is one call, since every part and model this crate ships implements it already. `store!` implements it for your own part, model, chip or board, naming each field once as a value, a part or what construction built. `impl Store for MyBoard {}` says the board can't be kept, and its runs start from reset | the `snapshot` module |
| A core family | `emulator::Processor`, a function that makes one from the family's typed configuration, and a machine module whose `CoreSpec` carries that function, as `cpu::armv7m` and `machine::armv7m` do | [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. `cli::main` is the whole `rigless` command over a table of machines you hand it, `--link` and `--gdb` included. `emulator::run_printed` prints what `rigless run` prints, `link::serve` runs a target for an engine, and `emulator::run` with a `Driver` of your own does anything else. `rigless run` itself 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] [--quiet] [--link [--seed N | --restore PATH]] [--host WIRE=PORT] [--image PATH[@ADDRESS]]... [--boot ADDRESS] 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 press Ctrl-C. 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.
The firmware and every `--image` go into memory before the core's first instruction, the way a probe writes them. An ELF file goes where it stores its sections: each section's bytes at the load address of the segment that holds it, as objcopy writes them into a binary. Nothing it only zeroes is written, and neither are bytes outside every section, like the file's own headers a linker can map in front of the first one. Any other file goes whole at the address after its `@`, like `app.bin@0x08004000`. The core boots where its machine says it does on its own, and `--boot ADDRESS` boots it from somewhere else. A fault names an instruction by the firmware's symbols, not an `--image`'s.
That gives you two ways to run an application linked to sit behind a bootloader. Put the bootloader in front of it, and the run boots the way the board does, bootloader first:
```text
rigless run --machine pixracer --image bootloader.elf app.elf
```
Or run the application alone, from its own vector table:
```text
rigless run --machine pixracer --boot 0x08004000 app.elf
```
A run is refused before anything runs when an ELF file is built for another machine than the core runs or keeps the other byte order from the part's, two images overlap, a segment is outside the memory map, the core can't boot from the address, or nothing is placed there. The refusal names what it hit, so you don't get a core running from empty flash.
`--quiet` drops the line per access and prints the rest: the console, the outputs, the stop and the gaps the run ends with. A large firmware can make millions of accesses a second, and that's too many lines to read. The coverage is the same either way.
`--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. `--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 run is one MCU. 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 run, in this process or another, since each run's core is a thread of its own.
`--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, its ports, and its conditions, each resting at zero. Then once per plant step the engine sends `Advance`, asking it to reach an instant with some inputs and conditions changed and some bytes on its ports. The target answers `Reached` with every output, what the firmware sent, any resets, and each gap and each thing a model told, 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`, `--host`, `--semihosting`, `--trace` or `--rtt`. 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. `--seed N` keys every random draw the board makes, 0 when it's left out. The target hands the board the seed and the `Advance`'s index once per `Advance`, after the conditions and the inputs, so the same seed under the same steps is the same run. 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.
Between a `Reached` and the next `Advance` the engine can send `TakeSnapshot` with a path. The target writes a snapshot of itself there, the core's state, its memory, every model and chip, and what the link holds, and answers `Snapshotted` with its SHA-256. When it can't, at a stop where a reset the core or a model asked for hasn't happened yet or with a chip that can't be kept, it answers `Refused` with why, and the run goes on as it stood. `--restore PATH` starts a target from one instead of from reset. It has to be the same machine, target, images, boot address and runtime, and it names the snapshot in its `Manifest`. The engine's first `Advance` then carries on from the stop the snapshot was taken at, under the seed it was taken with, and from there it's the same run as one that never stopped. The core loads a snapshot as it stands, so only restore one you or your engine wrote. That's what lets cases that start the same way run the start once. The file is the target's part, and an engine appends its own after it: `rigless snapshot`, a `u16` format, then each part as a one-byte name length, the name, a `u64` length, the part's SHA-256 and its bytes.
An `Advance` can also arm faults, each with an id the engine picks, a trigger, and what fires. A trigger is a chain of links, each counting from where the one before it fired: `After` a length of time, the Nth `Access` to a register the `Manifest` lists, the Nth `Transaction` with a chip it lists, the Nth arrival at an instruction to `Execute` it, by its address or the firmware's own symbol for it, or the Nth instruction that makes a `Write` into a range of RAM. An access link counts the core's own reads or writes, optionally of a value under a mask, and fires right after the one before its count, so the one it counts to is the first to see the fault. A transaction link does the same with the transactions a chip's place counts: a select to its deselect, a card's command, or a word on a serial line. An `After` instant becomes a pause of the run's, between two instructions, which the models are never told of, and the step still ends where the engine asked. An instruction or a write is watched by the core only while a link counts it, and the core runs on through a hit short of the count as it would without the watch, so neither changes the run. An interrupt taken at a watched instruction before it runs brings the core back to it, and that's another arrival. A push of several registers into a watched range is one write. Write links of different faults watch one range between them, since the core reports a store to one range only. An instruction outside the part's memory, and a write range outside its RAM, are refused. What fires is the board's conditions, set right there, or a one-shot fault: the next word a chip answers flipped under a mask, its next transaction dropped, an interrupt line raised for an instant, a reset from the part's pin, or the core's next access to a register failed, which it takes as a bus error. `Reached` lists every fault that fired since the last answer, with its instant, its instruction count and the instruction the core was at, for one fired right after an access or on arriving at an instruction. A fault that never fires stays armed, and a snapshot keeps it with how far it has counted.
The crate is MIT or Apache-2.0, at your option, the core included. Cranelift and `rustc_apfloat`, which the core builds on, are Apache-2.0 with the LLVM exception. 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.
`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, Operate, Single};
use rigless::soc::{Access, Bus, Cpu, Endian, 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: "CTRL", offset: 0x0, width: Width::W32, reset: 0, access: Access::ReadWrite, read: ReadAction::None, write: WriteModify::None, fields: &[] },
RegisterSpec { name: "DATA", offset: 0x4, width: Width::W32, reset: 0, access: Access::ReadWrite, read: ReadAction::None, write: WriteModify::None, fields: &[] },
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Register { Ctrl, Data }
impl Layout for Register {
const REGISTERS: &'static [RegisterSpec] = ®ISTERS;
fn from_index(index: usize) -> Option<Self> { [Self::Ctrl, Self::Data].get(index).copied() }
fn index(self) -> usize { self as usize }
}
const SERIAL: Peripheral = Peripheral { name: "SERIAL", base: 0x4000_0000, size: 0x100, interrupts: &[5], registers: ®ISTERS };
static MEMORY: [Region; 1] = [Region::ram("sram", 0x2000_0000, 64 << 10)];
// What a person writes.
#[derive(Default)]
struct Serial { sent: Vec<u8> }
impl Device for Serial {
type Register = Register;
fn write(&mut self, register: Register, value: u64, _registers: &mut RegisterFile, _context: &mut DeviceContext<'_>) {
if register == Register::Data { 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::new("a-core", true, 16, Endian::Little), &MEMORY, [SERIAL]);
let mut soc = Single::new(bus, "SERIAL", Serial::default())?;
let data_register = SERIAL.base + u64::from(Register::Data.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, an interrupt count and a byte order, 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. Your own MCU writes one with `Bus::drive`, which hands over the registers and the context for a slot, as an access does.
### Time and events
Every access reaches the bus at an instant of the virtual clock, which the core's instruction count runs, and a model sees that instant and nothing else about time through the `DeviceContext` every method receives. An event is a timer on that clock: 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 core
The core is the crate's own. A family's module under `cpu` decodes the firmware's instructions and translates a block of them at a time to host code with Cranelift, quickly the first time and again with better code once the block turns out to run often. What knows no instruction set, the page table the translated code reads memory through, the cache of blocks and the code generator, is private to the crate. A family meets the runner through `emulator::Processor` and the `emulator::CoreBus` it's lent, whose documentation is the contract. `emulator::run` makes the target's processor with the constructor its `CoreSpec` carries, and runs it on a thread of its own. A processor is a plain value, so a process runs as many as it likes, one after another or at once. The run maps the target's memory, places the images and boots, then runs the core in stretches to the next instant the models asked for. A load or store to plain memory runs inline in the translated code. Every other access, a peripheral's or one the memory map doesn't answer, is a call into `Session::access` at its instant, with its instruction count and its program counter, before the next instruction, and the core carries on afterwards by itself when the run has nothing to do between. Every instruction counts exactly, so an access lands at the same instruction count on every host. A watched instruction is a breakpoint the core stops before, a watched range has its pages sent through the same calls, and a debugger is served between stretches. What the processor answers itself, its interrupt controller, its system control space and how it boots, is the family's module under `cpu`. The core maps memory a page at a time, so every region and alias starts and ends on one of its pages, and a run whose map doesn't is refused before the first instruction. A model's write to memory the guest can't write is refused as the guest's would be. A store to a region a model programs, flash behind its flash interface, goes to that model instead. The core's half of a snapshot is the processor's architectural state, with nothing of the process or of how the run was split into stretches in it, so a snapshot is the same bytes from every process and every host, and loads into a new core before its first instruction. A saved state the core couldn't have been in, with an active exception its table doesn't have say, is refused. Nothing crosses a process boundary.
## 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 `CoreSpec` that describes its core, 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::TcpPeer` 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: 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. `Conditions` is the third type, what can be wrong with the board itself: a chip that's stopped answering, one that senses a stuck, biased or noisy value while the chip beside it senses the truth, or a serial line that garbles what arrives. `Edge::condition` takes it and it takes effect at once, `Edge::CONDITIONS` says how it crosses, and every condition rests at zero, the board as built. A silence goes on the chip's `chip::Place` through its controller, so the controller takes the path it takes with nothing there, and a wrong reading is `sense` handing the chip what an `edge::Sensed` answers. The chip never learns of either. Anything random comes from `edge::draw`, a function of the run's seed, the name of what it's for and an index. No generator holds state, so setting one condition never moves another's noise. `Edge::draw` is where a board draws for the step about to run, and a board with nothing random leaves it out. 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 press Ctrl-C 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 core reported them. `pc` is the instruction that made the access, or `debugger` for a debugger's. 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.
Ctrl-C ends a run with no `--until` where it stands, between two instructions, and its stop line says where that is: `stop t=<ns> n=<count> reason=interrupted`. Run it again with `--until <ns>ns` and it prints the same accesses and stops after the same instruction, so you can replay the run you cut. Ctrl-C ends a run that's waiting for a debugger, or whose core a debugger holds, the same way, and the debugger hears the connection close. A second Ctrl-C ends the process at once.
### 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 IP address, IPv6 in brackets like `[::1]:3333`. `--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, until a debugger continues, steps, detaches or kills it. A connection that closes without a word, like a port probe's, leaves the core as it found it. The command says which it did and where once it's listening there, `serving GDB on 127.0.0.1:3333` or `waiting for a debugger on 127.0.0.1:3333`, so `--gdb 0` tells you the port the system picked.
```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`.
#### Connecting and detaching
The runtime serves the debugger itself, on the core's thread between stretches, so virtual time stands still while the debugger holds the core, and a debugger that attaches finds the core held where it was. It serves one debugger at a time, so one that connects while another is attached is refused and closed at once. Once that one goes, the next can connect straight away. `detach` lets the run go on. Under `target remote` that closes the connection, and under `target extended-remote` GDB stays connected, as it expects to, and `attach 1` holds the core again wherever it ran to. Until then the server answers nothing that asks about the core, so `monitor reset` asks you to attach first, while `monitor help`, `monitor halt`, `monitor peek on` and `monitor peek off` still answer. `disconnect` leaves the core held where it was for the next `target remote` to find, with your breakpoints and watchpoints taken out and its halting debug still on, as a probe that lost its host leaves it. A GDB that crashes leaves the core as it was too: held if it was stopped, and running on with its halting debug still on if it was running, so a `BKPT` it reaches then holds it for the next debugger. `kill` under `target remote` lets the run go on. Under `target extended-remote`, GDB's own `run` and `starti` reset the machine as `monitor reset` does and read everything again, and `kill` leaves the core held for the next `run` or `attach`, or lets it run on once GDB detaches or goes.
#### Breakpoints, watchpoints and stepping
Breakpoints, watchpoints on writes, reads and both (`watch`, `rwatch` and `awatch`), `stepi`, `finish`, `info registers` and `load` work as they would against a probe. The registers are the family's, its system registers among them and its floating-point registers when the core has them, and a step runs one instruction with interrupts held off, so one that comes due while you step is taken only once you continue. GDB's own `record full` works over the server, so `reverse-stepi` and `reverse-continue` go back through what GDB recorded.
**`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 CPU around it forever, and a step holds interrupts off, so the one that would take it somewhere else waits until the run goes on. `stepi` returns, and `next` doesn't until you press Ctrl-C, which stops it at its next step. Step into the handler with a breakpoint instead.
#### Halting debug
While a debugger is attached the core's halting debug is on, as a probe turns it on. The firmware reads `DHCSR.C_DEBUGEN` set, and a `BKPT` stops the core at itself, which GDB reports as `SIGTRAP`. Continue and it stops there again, so step past it with `set $pc = $pc + 2`. Once the debugger detaches or kills the run, a `BKPT` does what it does with no debugger, which is HardFault unless the firmware turned DebugMonitor on. One that disconnects leaves halting debug on, so a `BKPT` the core reaches after that holds it for the next debugger. Since that changes what the firmware does, the run prints `debugger attached t=<ns> n=<count>` where a debugger takes the core and `debugger detached t=<ns> n=<count>` where it lets it go or goes, and a run nobody debugs prints neither. In non-stop mode, a `detach` or a `kill` while the core runs holds it first, so it's let go at a stop, as it is in all-stop mode. A driver of your own hears both in `Driver::on_debugger_attached`.
#### Memory and registers
A debugger that stops the core and reads memory or the core's registers leaves the run the same run, a peripheral's registers included, since your reads are peeks. Writing a register or memory changes the run from that stop on, and the run's output doesn't show it, unless it reaches a peripheral, below. A run you can take back keeps it, to put it back where you made it. `load` writes the flash all the same. Where a model programs the flash, a snapshot keeps it as it keeps RAM. Anywhere else the run refuses a snapshot from then on, since a snapshot keeps that flash only as the images placed it. `compare-sections` checks what's in memory against the ELF with a CRC the server works out, which takes milliseconds where reading it all back took seconds.
A peripheral write the debugger makes itself, through `set`, reaches the models like any other access but with no program counter, since no instruction made it. It appears in the run's output with `pc=debugger`. An address nothing covers is an error to GDB, `Cannot access memory`, and no gap in the run's report, since the run never went there.
#### Monitor commands
`monitor reset` resets the machine as the firmware's own request does and holds the core at its reset handler. GDB keeps the registers it had already read, so ask again with `maintenance flush register-cache`. `monitor reset halt` and `monitor reset init`, which IDEs' launch templates send, and `monitor system_reset`, which other emulators' scripts send, do the same. `monitor halt` says whether the core is held, which it is whenever GDB has it, and `monitor help` lists every command. None of them writes a file on your machine or starts a server, so the debugger's port reaches the emulated core and nothing else.
#### Watchpoints and instruction counts
Set as many watchpoints as you like, up to 64 MiB of memory between them, where silicon gives you four comparators. Every access to each page of memory a watchpoint touches is checked while it's in, so one over a lot of memory slows the run, as `monitor help` says.
A watchpoint stops the core before the access it watches happens, with the instruction that makes it not run yet. GDB then takes the watchpoint out, steps that instruction and tells you, so you land right after the instruction that made the access, as on any target that stops before it. Some instructions make more than one access: a load or store multiple, an `LDRD` or `STRD`, an unaligned access split into bytes, and a store through a bit-band alias, which reads the word before it writes it back. One whose later access is watched runs again from its first access when you go on, as the architecture lets a core restart a load or store multiple, so its earlier accesses to plain memory happen twice, with the same values, and nothing sees it. Once one of its earlier accesses has reached a peripheral, though, the core doesn't stop before the watched one. The instruction finishes and the core stops right after it, so a peripheral never sees an access twice and the run's output never gains a line. GDB steps once more from there, as it does after every watch stop, so you land one instruction further on. A core family that doesn't ask before an access stops after it the same way, as [Adding a core family](#adding-a-core-family) says.
`monitor icount` prints how many instructions the core has retired and the virtual time it's at, like `icount 57594 at 2230340 ns`. GDB reads the same count as `$icount`, a register of the run's own after the family's, so `p $icount`, `display $icount` and `break bear_bones::tim2_update if $icount > 57600` work. You can't write it. The run is deterministic, so a count is a place you can get back to. `monitor until-icount 57594` and then `continue` stops the core once it has retired exactly that many instructions, and every access in the run's output carries its count as `n`, so take one from there. `monitor until-time 5300us` stops it at that instant of virtual time, waiting or not. GDB hears either stop as `SIGTRAP`, and the run with the stop is the run without it. A count or an instant the run has passed is refused.
#### Peeks
A read you make, through `x`, `print` or an IDE's register view, is a peek. It answers what the firmware's read would answer at that instant and changes nothing. A flag a read clears stays set, the byte waiting in a serial port's data register stays there for the firmware, and no model and no driver hears of it, so the run is the one it would be with nobody looking. A peripheral whose clock is off shows you what its registers hold, where the firmware's own read of it answers zero, and a write you make to one is dropped, as the firmware's is. `monitor peek off` makes your reads the firmware's for the rest of the run, side effects and all, each in the run's output with `pc=debugger`, for when you want a read to do what it does on silicon. `monitor peek on` makes them peeks again. The server's own reads, like the CRC `compare-sections` asks for, always peek.
#### Vector catch and the memory map
`monitor vector-catch hard bus` has the core halt when it takes a HardFault or a BusFault, before the handler's first instruction, with the faulting context on the stack where the handler would find it, rather than you finding the core spinning in the handler later. The words are `DEMCR`'s `VC_` bits: `hard`, `int` for a fault on exception entry or return or on the vector read, `bus`, `state` for an undefined instruction or a bad state, `check` for an unaligned access or a divide by zero, `nocp`, `mm`, and `reset`, which halts the core at the reset handler after the firmware's own reset. `all` or `none` stand alone and do what they say, and with no words it says what's caught. Writing `DEMCR` yourself does the same, and GDB hears the halt as `SIGTRAP`. `DFSR` says what halted the core: `BKPT` for a `BKPT` or a breakpoint, `DWTTRAP` for a watchpoint, `VCATCH` for a vector catch and `HALTED` for your own halt or step. Write one to a bit to clear it.
GDB reads a memory map from the run: the flash in the blocks it erases in, the RAM, the system memory as read-only, and every peripheral and the core's own space, so `info mem` lists them all. `load` then erases the flash a block at a time and writes it with `vFlashErase` and `vFlashWrite`, as it would through a probe, and a run whose flash no model programs refuses a snapshot after it, as it does after any write there. With the map GDB refuses an address it doesn't list, which is one nothing answers anyway, and writes the flash only through `load`, so a `set` there says writing to flash is forbidden.
You reach the bit-band aliases as the core does. A word at an alias reads as the one bit it stands for, and writing one sets or clears that bit. A write to a peripheral's alias peeks the register and writes it back once with the bit changed, so the model hears one write of the register, as it would from the firmware.
#### Host calls
`rigless run --semihosting` serves the firmware's semihosting calls, the ones of Arm's "Semihosting for AArch32 and AArch64" a firmware's console, clock, exit and files take: `SYS_OPEN`, `SYS_CLOSE`, `SYS_WRITEC`, `SYS_WRITE0`, `SYS_WRITE`, `SYS_READ`, `SYS_ISTTY`, `SYS_SEEK`, `SYS_FLEN`, `SYS_CLOCK`, `SYS_TIME`, `SYS_ERRNO`, `SYS_EXIT`, `SYS_EXIT_EXTENDED`, `SYS_ELAPSED` and `SYS_TICKFREQ`. Every other call, `SYS_READC`, `SYS_ISERROR`, `SYS_TMPNAM`, `SYS_REMOVE`, `SYS_RENAME`, `SYS_SYSTEM`, `SYS_GET_CMDLINE` and `SYS_HEAPINFO` among them, answers -1, as a call that failed does. What it writes to the console is printed as it runs, as `console t=<ns> n=<count> text=<bytes>` lines among the accesses, with a line break in what it wrote shown as `\n`, and with GDB attached GDB prints it too. Its clock and its time read virtual time, so two runs read the same, and its exit ends the run with its status, which the last line says as `exit code=N` and the process exits with. An attached GDB hears the firmware exit with that status. Its files are GDB's: with GDB attached, an open, read, write, seek or close happens on GDB's host, in GDB's working directory, and with no debugger attached it fails, so a firmware never reaches the files of the machine the emulator runs on. Without the flag a host call is the breakpoint instruction it is, which halts the core under a debugger and faults without one, as it does on a part with nothing serving it.
`Driver::on_console` hands a driver of your own each write to the console, and `Finished::exit_status` the status the firmware ended the run with. `Start::new` makes a run's start, and `Start::semihosting` turns host calls on.
#### Output ports
`--trace [HOST:]PORT` streams the core's own trace there, which on an ARMv7-M core is its ITM: what the firmware writes to a stimulus port it enabled goes out as the Instrumentation packet a viewer of the ITM's packet protocol reads, as it would from a probe on the trace pin. The port takes one client at a time, and with none connected the trace goes, so the run is the same run whether anyone's listening, and never waits on a slow client. A stimulus port reads ready whenever it's enabled, since the port takes every packet at once. `Start::trace` is the listener a run of your own streams it to. The ITM is the part's with or without the flag, so a firmware that turns it on and writes to it runs as it does on silicon, and the packets go nowhere.
`--rtt [HOST:]PORT` streams the firmware's RTT terminal, its up-channel 0, there. The run finds the control block by the firmware's symbol for it, or by its ID in RAM when the firmware has no symbols, and drains it every virtual millisecond, at whole milliseconds, writing the read offset back as a probe does. Writing the read offset is what makes room in the buffer, so a firmware that waits for room runs differently with the flag than without, as it does on a board with a probe attached and one without, and two runs with it are the same run. `Start::rtt` is the listener for a run of your own.
#### Conditions and dprintf
GDB hands the run a breakpoint's condition and a `dprintf`'s print to work out itself, as agent expressions, so the core never comes back to GDB for a pass that doesn't stop. `break bear_bones::tim2_update if (*(int *)0x40020014 & 0x20) == 0x20` stops only at the passes where the pin is high, and `info breakpoints` says `(target evals)`. Over 20 virtual seconds of bear-bones, about 20,000 passes, that took 1.6 s where GDB testing it at every pass took 4.8 s, and the run with no breakpoint at all 1.5 s. `set breakpoint condition-evaluation host` has GDB test it again. A condition reads registers, `$icount` among them, and memory through peeks, so it leaves the run the same run. One that can't be worked out, a read where nothing answers say, stops the core at the breakpoint and says why.
`set dprintf-style agent` then `dprintf bear_bones::tim2_update,"odr %#x\n", *(int *)0x40020014` prints at every pass and never stops the core. It takes `%d`, `%i`, `%u`, `%x`, `%X`, `%o`, `%c`, `%s`, `%p` and `%%`, with flags, a width, a precision and a length, and an `int` and a `long` are 32 bits, as on the targets that run here.
#### Non-stop and live reads
`set non-stop on` before `target remote` has GDB leave the core running while you look. `continue &` runs it in the background, and `x`, `print` and an IDE's live watch read memory while it runs, as peeks, so watching a value change leaves the run the same run. `interrupt` stops it, and a stop comes to GDB as it happens, without GDB waiting on the core. While the core runs, its registers, writes to memory and `monitor` wait for a stop, and a breakpoint you set goes in at once.
In non-stop mode the core is one thread, whatever kernel it runs, since the run can't stop one of a kernel's threads and leave the others going. The thread list comes back in all-stop mode. GDB doesn't wait on the core in non-stop mode, so it hears nothing of the firmware's host calls: the console still prints in the run's output, but not in GDB's, and a file call fails as it does with no debugger. What a `dprintf` prints, and why a breakpoint's condition couldn't be worked out, go to the run's output as `dprintf t=<ns> n=<count> text=<bytes>` lines, each once however often you take the run back over it. A driver of your own hears them in `Driver::on_debugger_print`, never in `Driver::on_console`.
#### More than one target
A run steps one target of a board, so each target of a board with more than one gets a run and a port of its own, `rigless run --target <name> --gdb <port>`, and one GDB holds them all as inferiors, each over its own connection:
```text
(gdb) target extended-remote :3333
(gdb) add-inferior -exec firmware.elf
(gdb) inferior 2
(gdb) target extended-remote :3334
(gdb) info inferiors
```
A breakpoint goes in every inferior whose program has it, and `continue` runs the inferior you're in. Under an engine's lockstep, with `--link`, a target you hold doesn't answer its step until you let it go, so the engine holds the others with it. One server that stops every core at the same instant needs a run of several targets, which the runtime doesn't do yet.
#### Going back
`monitor save NAME` keeps the run where it stands, in memory, for as long as your debugger's attached, and `monitor restore NAME` takes the run back there, or forward to it. `monitor snapshots` lists what you kept. GDB keeps the registers and memory it read across a monitor command, so after a restore ask again with `maintenance flush register-cache` and `maintenance flush dcache`.
`reverse-stepi`, `reverse-step`, `reverse-next`, `reverse-finish` and `reverse-continue` work with no `record` in GDB. The run keeps a checkpoint when you attach and every 4,194,304 instructions after, 64 at most, the older ones thinned out as it goes, and going back is putting the run back to one and running forward from there. `reverse-stepi` lands right before the instruction that came next, an exception the core took before it included. `reverse-continue` stops where going forward would have stopped: a breakpoint, a watchpoint, or a halt point, so a `monitor until-icount N` with an N the run has passed is a place it stops at. A watchpoint lands you on the store that last wrote the word, with its old value in it. With nothing to stop at, it goes back to where you attached, and GDB says it reached the end of the recorded history.
The run is deterministic, so going forward again is the same run, as long as what you did comes back where you did it: your writes, the registers you set, your steps, which hold interrupts off for their instruction, a reset, and what GDB answered the firmware's file calls with all do. So do the bytes a host wrote to a serial port, at the same instants. The run's output prints each instant once, so a session you went back and forth in prints what one that never went back prints, and the trace and RTT ports send nothing twice. Something the history doesn't have, a write or a step it never saw there, takes the run a new way from there: what came after it goes, the snapshots you saved after it with it, and the run's output carries on from there, its counts going back.
A run under `--link` can't go back, since the engine's inputs aren't kept, and the monitor says so. A driver of your own lets a debugger take the run back with `Driver::may_rewind`, which says so only when its calls change nothing in the run but which instant `on_stop` answers. A snapshot, the file an engine asks for, is still refused once a debugger has written memory the guest can't. A checkpoint keeps it as you wrote it, since it's yours and never a file.
#### Threads
`rigless run --gdb` shows GDB every thread of a firmware whose kernel [`rigless-rtos`](https://crates.io/crates/rigless-rtos) knows, ChibiOS/RT or FreeRTOS. GDB looks up the kernel's symbols for the run when it loads the firmware's, so there's nothing to set: `info threads` lists each thread with its name and what it's doing, like `(UART1, WTOREVT)` under ChibiOS or `(delay, Blocked)` under FreeRTOS, and `thread 3` then `bt` gives you that thread's stack. The thread that runs has the core's registers. Every other one has what its kernel saved when it switched away from it, and you can't write those, so a register the save doesn't keep reads as `<unavailable>` and `set $r4 = 1` there is refused. The core runs whichever thread the kernel runs, so `stepi`, `next` or `continue` in another thread steps or runs the one that runs, and the stop names it. The run walks the kernel's lists once a stop, through peeks, so looking at threads leaves the run the same run. Before the kernel starts, or when its memory doesn't check out, the core is the one thread, as it is with no kernel at all. GDB first meets the core as thread 1, before it's looked anything up, so it can show thread 1 beside the thread that runs until the next stop, when it says thread 1 exited.
The kernels are the binary's, not the runtime's, and `rigless-rtos`'s README says what each one needs of a build. A run of your own shows the threads of the kernels you put in `Start::kernels`, each a `WalkThreads`. A kernel names the symbols it asks GDB for, and from where GDB found them, the core's registers, the target's byte order and a way to peek at memory, it walks its threads: each one's id, name and state, and what its kernel saved of its registers, by the names the target description gives them. A register is its bytes in the target's order, both ways, so one wider than 64 bits crosses whole. The run asks for every kernel's symbols in order and walks the first whose required symbols GDB found. With no kernels the core is the one thread.
## 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. Its core is from the one family the crate carries today, ARMv7-M, so the board fills in `machine::armv7m::Core` and you build the firmware for that family's targets. Another family can live in a crate of your own, as [Adding a core family](#adding-a-core-family) says. 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 and boards we ship, and leaves you the runtime. The core is always built in, and it's Rust, so there's nothing else to install.
### 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.
The register table here is written by hand, the way the generator would emit it, which is fine for a register or three. For a part with more, write an SVD for it, a made-up part included, and `rigless svd generate` writes the tables, the `Register` enums and the field constants for you, as [the generator](#the-generator) says.
```rust
use rigless::device::{Device, DeviceContext, Level, RegisterFile};
use rigless::mcu::{Mcu, Operate, Single};
use rigless::soc::{Access, Bus, Cpu, Endian, 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); }
}
fn reset(&mut self) {
self.frame_end = None;
}
}
/// 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::new("lantern-core", false, 16, Endian::Little);
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. When the part resets, the reset drops every event, so `reset` forgets the frame the beacon was sending. Leave `reset` out of a model that keeps nothing a reset clears.
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.
A debugger's read is a peek instead, `Device::peek`, which answers what your `read` would with nothing changed. It takes `&self` and the register file shared, so it can't change a thing, and `None` serves what the file holds with no read action taken. The beacon needs none, since its `read` answers what `SR` holds. Write one only where your `read` answers something the file doesn't hold, like the byte at the front of a FIFO or a counter that runs on time.
### 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, Operate, Single};
# use rigless::soc::{Access, Bus, Cpu, Endian, 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::new("lantern-core", false, 16, Endian::Little);
# 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(BEACON.name)?;
let tally_slot = bus.slot(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 reset(&mut self, _at: VirtualNanos, _cause: rigless::device::ResetCause) { self.reset_every_model(); }
fn poll(&mut self, now: VirtualNanos) { self.poll_every_model(now); }
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` asks for five methods: the bus both ways, `split`, and what the part does at a reset and at a stop. `reset_every_model` and `poll_every_model` are what a part with nothing of its own to keep there says. `Operate` then 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.
A flash `Region` is erased whole unless you say how it erases. `Region::flash(...).erased_in(&[...])` gives its erase blocks, which add up to its size, and GDB's `load` then erases a block at a time, as it does through a probe. Without them the memory map a debugger reads has the flash as one block, so a `load` of a few bytes erases all of it.
A flash `Region` reads 0 wherever no image fills it unless you say what it erases to. Most flash erases to all ones, so give it `.erased_to(0xFF)`. It then starts that way past the image, and `vFlashErase` writes the same.
Firmware that writes its own flash, to keep its parameters say, does it through the part's flash interface. `.programmed_by(name)` on the region names that instance, and a store the firmware makes into the region then reaches its model at `Device::write_region`. The model decides what the store means and changes the flash only through `DeviceContext::program`, so what it programs lands at the next settle. A store the part ignores, or only flags as an error, the model still takes. One it doesn't take shows up in the run's output as a store nothing answered. A snapshot keeps a programmed region as it keeps RAM.
### 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 description of its core, and that comes from the core family module, `armv7m::Core`: one typed field for each thing the family needs to make a processor, so a board can't leave one out, and the family checks each value when the run starts.
```rust
# use rigless::device::{Device, DeviceContext, Level, RegisterFile};
# use rigless::mcu::{Mcu, Operate, Single};
# use rigless::soc::{Access, Bus, Cpu, Endian, 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::new("lantern-core", false, 16, Endian::Little);
# 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(BEACON.name)?;
# let tally_slot = bus.slot(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 reset(&mut self, _at: VirtualNanos, _cause: rigless::device::ResetCause) { self.reset_every_model(); }
# fn poll(&mut self, now: VirtualNanos) { self.poll_every_model(now); }
# 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::machine::{Board, Target, armv7m};
use rigless::session::Session;
use rigless::snapshot::Store;
/// The board: one Lantern and nothing else.
#[derive(Debug)]
struct LanternBoard { chip: Lantern }
// A board says how a snapshot keeps it. This one doesn't, so a snapshot of it is refused and its
// runs start from reset.
impl Store for LanternBoard {}
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 core = armv7m::Core {
variant: armv7m::Variant::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(Operate::cpu(&self.chip).interrupt_lines)).unwrap(),
priority_bits: 0, // zero leaves it to the core
refclk_divider: NonZeroU32::new(8).unwrap(),
instructions_per_second: NonZeroU64::new(250_000_000).unwrap(), // four nanoseconds each
boot: "flash", // the region at the reset vector
}
.spec(&cpu::armv7m::windows(Mcu::bus(&self.chip)));
vec![Target::new("lantern-core", &mut self.chip, core)]
}
}
// A session on the board's one target. Nothing runs yet, but the core's description 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. `instructions_per_second` is how fast the core runs against the run's virtual clock: 250 million is four nanoseconds an instruction, which every target we ship uses. The part's own clock, one instruction a cycle, is the other natural pick, and the virtual clock counts whole nanoseconds, rounding each instant down. `spec` gives the core an MPU of eight regions and the floating-point unit its variant has by default. When the part's manual says otherwise, call `spec_with_parts` with what it says. The core runs only what the variant and the part have, so a variant without the DSP extension takes its instructions as undefined, and a part without an FPU faults on every floating-point instruction. A unit or an MPU the variant can't have is refused when the run starts. What the architecture leaves to the implementation goes on `armv7m::Config`: the `CPUID` the part reports, its caches as their identification registers describe them, whether it predicts branches, the registers it has in the system control space's IMPLEMENTATION DEFINED range, and where its vector table is at reset when that isn't zero, in which case nothing answers at zero but what `MEMORY` puts there. The core caches nothing, so a firmware's cache maintenance is taken and does nothing. Leave them unset and the core reports the Cortex-M4's `CPUID`, has no caches, and boots from zero.
### 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. `Operate::drain_interrupts` is how a unit test sees what a model drove, as the peripheral block above does.
The part decides how its interrupts reach the core. With no interrupt controller in its description, an interrupt drives the emulator's line of its own number, which is right when the core's interrupt controller numbers its inputs the way the part does. A part with a controller on its own side, an interrupt matrix the firmware programs say, names it with `routes_interrupts`, and that model hears every interrupt at `Device::on_interrupt` and drives the core's lines with `DeviceContext::drive_line`. An interrupt with no line 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 of its channels 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 `Operate::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, Operate, Single};
# use rigless::soc::{Access, Bus, Cpu, Endian, 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::new("lantern-core", false, 16, Endian::Little);
# 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(BEACON.name)?;
# let tally_slot = bus.slot(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 reset(&mut self, _at: VirtualNanos, _cause: rigless::device::ResetCause) { self.reset_every_model(); }
# fn poll(&mut self, now: VirtualNanos) { self.poll_every_model(now); }
# 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. The shortest is the whole command over a table of your own machines. Turn on the `cli` feature, `cargo add rigless --no-default-features --features cli`, and the binary is one line:
```rust,no_run
# use rigless::device::{Device, DeviceContext, Level, RegisterFile};
# use rigless::mcu::{Mcu, Operate, Single};
# use rigless::soc::{Access, Bus, Cpu, Endian, 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::new("lantern-core", false, 16, Endian::Little);
# static MEMORY: [Region; 2] = [Region::flash("flash", 0x0800_0000, 64 << 10), Region::ram("sram", 0x2000_0000, 16 << 10)];
#
# use rigless::device::{ErasedDevice, RequestChange};
#
# /// 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(BEACON.name)?;
# let tally_slot = bus.slot(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 reset(&mut self, _at: VirtualNanos, _cause: rigless::device::ResetCause) { self.reset_every_model(); }
# fn poll(&mut self, now: VirtualNanos) { self.poll_every_model(now); }
# 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::machine::{Target, armv7m};
# use rigless::session::Session;
#
# /// The board: one Lantern and nothing else.
# #[derive(Debug)]
# struct LanternBoard { chip: Lantern }
# impl rigless::snapshot::Store for LanternBoard {}
# 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 core = armv7m::Core {
# variant: armv7m::Variant::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(Operate::cpu(&self.chip).interrupt_lines)).unwrap(),
# priority_bits: 0, // zero leaves it to the core
# refclk_divider: NonZeroU32::new(8).unwrap(),
# instructions_per_second: NonZeroU64::new(250_000_000).unwrap(), // four nanoseconds each
# boot: "flash", // the region at the reset vector
# }
# .spec(&cpu::armv7m::windows(Mcu::bus(&self.chip)));
# vec![Target::new("lantern-core", &mut self.chip, core)]
# }
# }
#
use std::process::ExitCode;
use rigless::machine::{Board, Constructor};
use rigless::soc::Error;
fn lantern() -> Result<Box<dyn Board>, Error> {
Ok(Box::new(LanternBoard { chip: Lantern::new()? }))
}
/// Each board by the name `--machine` takes, which is the name it calls itself.
const MACHINES: &[(&str, Constructor)] = &[("lantern", lantern)];
# #[cfg(feature = "cli")]
fn main() -> ExitCode {
rigless::cli::main(MACHINES)
}
# #[cfg(not(feature = "cli"))]
# fn main() -> ExitCode { let _ = MACHINES; ExitCode::SUCCESS }
```
That binary is `rigless` for your boards: `run --machine lantern --until 3ms firmware.elf` prints what `rigless run` prints, `--gdb 3333` serves a debugger, and `--coverage` writes the report. Once the board implements `Edge`, `--link` makes it an engine's target, and an engine spawns this binary wherever it would spawn `rigless`. `link::serve` is that target as a function, for a program that isn't a command.
To keep your own `main`, `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. `Accesses::Counted` prints what `--quiet` prints:
```rust,no_run
# use rigless::device::{Device, DeviceContext, Level, RegisterFile};
# use rigless::mcu::{Mcu, Operate, Single};
# use rigless::soc::{Access, Bus, Cpu, Endian, 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::new("lantern-core", false, 16, Endian::Little);
# 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(BEACON.name)?;
# let tally_slot = bus.slot(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 reset(&mut self, _at: VirtualNanos, _cause: rigless::device::ResetCause) { self.reset_every_model(); }
# fn poll(&mut self, now: VirtualNanos) { self.poll_every_model(now); }
# 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::machine::{Board, Target, armv7m};
# use rigless::session::Session;
#
# /// The board: one Lantern and nothing else.
# #[derive(Debug)]
# struct LanternBoard { chip: Lantern }
# impl rigless::snapshot::Store for LanternBoard {}
# 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 core = armv7m::Core {
# variant: armv7m::Variant::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(Operate::cpu(&self.chip).interrupt_lines)).unwrap(),
# priority_bits: 0, // zero leaves it to the core
# refclk_divider: NonZeroU32::new(8).unwrap(),
# instructions_per_second: NonZeroU64::new(250_000_000).unwrap(), // four nanoseconds each
# boot: "flash", // the region at the reset vector
# }
# .spec(&cpu::armv7m::windows(Mcu::bus(&self.chip)));
# vec![Target::new("lantern-core", &mut self.chip, core)]
# }
# }
#
use std::io;
use std::path::Path;
use rigless::emulator::{self, Start};
use rigless::image::Image;
let session = Session::new(Box::new(LanternBoard { chip: Lantern::new()? }), 0, VirtualNanos(3 * FRAME.0))?;
let firmware = Image::read(Path::new("firmware/template/target/thumbv7m-none-eabi/release/template"), None)?;
let images = [firmware];
let start = Start::new(&images);
let printed = emulator::run_printed(session, &start, None, emulator::Accesses::Printed, 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 `time::NO_DEADLINE` means run until interrupted. `debugger: Some(&listener)` in `Start` is `--gdb`, with a `TcpListener` you bound where the debugger connects, `127.0.0.1` and the port for a port alone. 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, Operate, Single};
# use rigless::soc::{Access, Bus, Cpu, Endian, 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::new("lantern-core", false, 16, Endian::Little);
# 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(BEACON.name)?;
# let tally_slot = bus.slot(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 reset(&mut self, _at: VirtualNanos, _cause: rigless::device::ResetCause) { self.reset_every_model(); }
# fn poll(&mut self, now: VirtualNanos) { self.poll_every_model(now); }
# 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::machine::{Board, Target, armv7m};
# use rigless::session::Session;
#
# /// The board: one Lantern and nothing else.
# #[derive(Debug)]
# struct LanternBoard { chip: Lantern }
# impl rigless::snapshot::Store for LanternBoard {}
# 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 core = armv7m::Core {
# variant: armv7m::Variant::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(Operate::cpu(&self.chip).interrupt_lines)).unwrap(),
# priority_bits: 0, // zero leaves it to the core
# refclk_divider: NonZeroU32::new(8).unwrap(),
# instructions_per_second: NonZeroU64::new(250_000_000).unwrap(), // four nanoseconds each
# boot: "flash", // the region at the reset vector
# }
# .spec(&cpu::armv7m::windows(Mcu::bus(&self.chip)));
# vec![Target::new("lantern-core", &mut self.chip, core)]
# }
# }
#
use std::path::Path;
use rigless::emulator::{self, Driver, Finished, Host, Settled, Start, Stop, StopReason};
use rigless::image::Image;
/// 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, a test asserts on them, and the link's
/// target counts toward the faults it armed after each one and takes a snapshot at a stop when the
/// engine asks, with the host it's lent there.
struct Recording(Vec<(emulator::Access, u64)>);
impl<D: Board + ?Sized> Driver<D> for Recording {
fn on_settled(&mut self, _session: &mut Session<D>, _host: &mut dyn Host, settled: Settled) {
if let Settled::Access { access, value } = settled { self.0.push((access, value)); }
}
fn on_stop(&mut self, _session: &mut Session<D>, _stop: Stop, _host: &mut dyn Host) -> Option<VirtualNanos> { None }
}
let session = Session::new(Box::new(LanternBoard { chip: Lantern::new()? }), 0, VirtualNanos(3 * FRAME.0))?;
let firmware = Image::read(Path::new("firmware/template/target/thumbv7m-none-eabi/release/template"), None)?;
let images = [firmware];
let start = Start::new(&images);
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_settled` sees every access, every hit on what the emulator watches and every stop once the models have settled after it. `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. Set `Start::interrupt` to a flag of your own and the run ends once you set it, with `Finished::interrupted` saying where it stood. `StopReason::Idle` means the core is halted waiting for an interrupt with nothing pending, and the emulator skips ahead instead of waiting.
**Runs share nothing.** Each `emulator::run` makes a core of its own on a thread of its own, so a process runs as many as you like, one after another or at once, and a test that boots a firmware runs under `cargo test` as well as `cargo nextest`.
### The edge
An engine steps a board in lockstep through its edge: what the firmware drives, read off the board, and what the world does to the board's transducers, handed to them. If your board drives or senses something a plant cares about, implement `edge::Edge` for it and answer `Board::edge` with it. The lantern drives nothing a plant reads, so here's a board that does: a made-up heater, whose firmware reads the water's temperature and drives a heating element. It's the heater the engine's own docs step against a tank of water, field for field.
```rust
use std::num::{NonZeroU32, NonZeroU64};
use rigless::cpu;
use rigless::device::{Device, DeviceContext, ErasedDevice, RegisterFile};
use rigless::edge::{Coupled, Edge, Field};
use rigless::machine::{Board, Target, armv7m};
use rigless::mcu::{Mcu, Operate};
use rigless::session::Session;
use rigless::snapshot::Store;
use rigless::soc::{Access, Bus, Cpu, Endian, Error, Layout, Peripheral, ReadAction, Region, RegisterSpec, Width, WriteModify};
use rigless::time::VirtualNanos;
// Two instances of one register each: the water the firmware reads, in thousandths of a degree, and
// the element's duty it writes, in thousandths of full power.
static THERMOMETER_REGISTERS: [RegisterSpec; 1] = [
RegisterSpec { name: "DATA", offset: 0x0, width: Width::W32, reset: 0, access: Access::ReadOnly, read: ReadAction::None, write: WriteModify::None, fields: &[] },
];
static ELEMENT_REGISTERS: [RegisterSpec; 1] = [
RegisterSpec { name: "DUTY", offset: 0x0, width: Width::W32, reset: 0, access: Access::ReadWrite, read: ReadAction::None, write: WriteModify::None, fields: &[] },
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Thermometer { Data }
impl Layout for Thermometer {
const REGISTERS: &'static [RegisterSpec] = &THERMOMETER_REGISTERS;
fn from_index(index: usize) -> Option<Self> { (index == 0).then_some(Self::Data) }
fn index(self) -> usize { 0 }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Element { Duty }
impl Layout for Element {
const REGISTERS: &'static [RegisterSpec] = &ELEMENT_REGISTERS;
fn from_index(index: usize) -> Option<Self> { (index == 0).then_some(Self::Duty) }
fn index(self) -> usize { 0 }
}
const THERMOMETER: Peripheral = Peripheral { name: "THERMOMETER", base: 0x4000_0000, size: 0x100, interrupts: &[], registers: &THERMOMETER_REGISTERS };
const ELEMENT: Peripheral = Peripheral { name: "ELEMENT", base: 0x4000_0100, size: 0x100, interrupts: &[], registers: &ELEMENT_REGISTERS };
const CPU: Cpu = Cpu::new("heater-core", false, 16, Endian::Little);
static MEMORY: [Region; 2] = [Region::flash("flash", 0x0800_0000, 64 << 10), Region::ram("sram", 0x2000_0000, 16 << 10)];
/// The probe: a read of `DATA` answers the water the board last handed it.
#[derive(Debug, Default)]
struct Probe { water: i32 }
impl Device for Probe {
type Register = Thermometer;
fn read(&mut self, _register: Thermometer, _registers: &mut RegisterFile, _context: &mut DeviceContext<'_>) -> Option<u64> {
Some(u64::from(self.water.cast_unsigned()))
}
}
/// The part: the probe on its bus, and the element, which is its register and nothing more.
#[derive(Debug)]
struct Boiler { bus: Bus, probe: Probe, probe_slot: usize, element_slot: usize }
impl Boiler {
fn new() -> Result<Self, Error> {
let bus = Bus::builder(CPU, &MEMORY, [THERMOMETER, ELEMENT]).build()?;
let (probe_slot, element_slot) = (bus.slot(THERMOMETER.name)?, bus.slot(ELEMENT.name)?);
let mut part = Self { bus, probe: Probe::default(), probe_slot, element_slot };
part.check()?;
Ok(part)
}
}
impl Mcu for Boiler {
fn bus(&self) -> &Bus { &self.bus }
fn bus_mut(&mut self) -> &mut Bus { &mut self.bus }
fn reset(&mut self, _at: VirtualNanos, _cause: rigless::device::ResetCause) { self.reset_every_model(); }
fn poll(&mut self, now: VirtualNanos) { self.poll_every_model(now); }
fn split(&mut self, slot: usize) -> (&mut Bus, Option<&mut dyn ErasedDevice>) {
let model: Option<&mut dyn ErasedDevice> = if slot == self.probe_slot { Some(&mut self.probe) } else { None };
(&mut self.bus, model)
}
}
/// The board: the part and nothing else.
#[derive(Debug)]
struct Heater { part: Boiler }
impl Heater {
/// The heater as built, its probe in water at rest.
fn new() -> Result<Self, Error> {
let mut board = Self { part: Boiler::new()? };
board.sense(&Self::REST);
Ok(board)
}
}
impl Store for Heater {}
impl Board for Heater {
fn name(&self) -> &'static str { "heater" }
fn target_names(&self) -> Vec<&'static str> { vec!["heater-core"] }
fn mcu(&mut self, target: usize) -> Option<&mut dyn Mcu> { (target == 0).then_some(&mut self.part) }
fn targets(&mut self) -> Vec<Target<'_>> {
let core = armv7m::Core {
variant: armv7m::Variant::M3,
clock_hz: NonZeroU64::new(16_000_000).unwrap(),
interrupt_lines: NonZeroU32::new(u32::from(CPU.interrupt_lines)).unwrap(),
priority_bits: 0,
refclk_divider: NonZeroU32::new(8).unwrap(),
instructions_per_second: NonZeroU64::new(250_000_000).unwrap(),
boot: "flash",
}
.spec(&cpu::armv7m::windows(Mcu::bus(&self.part)));
vec![Target::new("heater-core", &mut self.part, core)]
}
fn edge(&mut self) -> Option<&mut dyn Coupled> { Some(self) }
}
impl Edge for Heater {
/// The water, in thousandths of a degree.
type Signals = i32;
/// The element's duty, in thousandths of full power.
type Outputs = u16;
/// Nothing about the heater itself can go wrong.
type Conditions = ();
const FIELDS: &'static [Field] = &[Field::new("water", "m°C", 4, true)];
const OUTPUT_FIELDS: &'static [Field] = &[Field::new("ELEMENT.duty", "‰", 2, false)];
const REST: i32 = 20_000;
fn sense(&mut self, water: &i32) {
self.part.probe.water = *water;
}
fn condition(&mut self, _conditions: &()) {}
fn drive(&mut self) -> u16 {
let duty = self.part.bus.slots()[self.part.element_slot].registers().get(Element::Duty);
u16::try_from(duty.min(1_000)).unwrap_or(1_000)
}
}
// The session holds each declaration to its type before anything runs.
let session = Session::new(Box::new(Heater::new()?), 0, VirtualNanos(1_000_000))?;
assert_eq!(session.board().part.probe.water, 20_000);
# Ok::<(), Box<dyn std::error::Error>>(())
```
Five things to get right:
- **The types cross the link as bytes**, so each implements `link::Wire`. The integers and `()` already do. A struct of your own writes its fields in order, each as its integer, and `wire!` writes that for you from the fields' names.
- **Build the board sensing `REST`**, as `Heater::new` does. Nothing senses it for you, and an engine only sends what changes from it.
- **The fields describe those bytes.** `FIELDS`, `OUTPUT_FIELDS` and `CONDITIONS` list each value in the order its type writes it, with its unit, its width in bytes and its sign. `Session::new` holds every list to its type and refuses one that doesn't add up, naming the list and both numbers. An engine holds `FIELDS` and `OUTPUT_FIELDS` against its own declaration of the board, so a field renamed on one side is a run refused rather than a wrong result. It offers `CONDITIONS` as they're listed.
- **`sense` hands each value to what measures it**, a model or a chip, on the part's own field, and the firmware finds it there the next time it reads. A model that has to act at the instant an input changes, like a pin raising an interrupt, takes it through a `drive_` method of the part's, which `Bus::drive` writes, from a test or a driver that knows the instant.
- **`drive` reads what the firmware drives** off the registers or the models that hold it, as the board would read its pins.
If your board's chips can go wrong, list what can as its `Conditions`, each resting at zero: a chip that stops answering is a `chip::Place` its controller silences, and a reading that's stuck, biased or noisy is the board handing the chip what an `edge::Sensed` answers under an `edge::Condition`. Name those chips in `CHIPS` and hand out each one's place in `place`, so a fault can reach it. A session refuses a board that names a chip and has no place for it. The Pixracer's module, `machine::pixracer`, is a whole board that does all of it.
To run it, put the heater in the table of the binary from [Running it](#running-it), and `--link` makes it an engine's target. A plant on the engine's side declares the same machine and the same fields from its own types.
### A chip on a line of your own
The chips this crate ships hang on the controllers its parts ship. A part of your own hangs a chip on a controller of its own, and the controller holds it in a `chip::Place`, which knows whether the chip answers, counts its transactions for a trigger, and takes a flipped word or a lost one. Here's a serial line's controller with a made-up gauge on its far end, which says its reading every 10 ms:
```rust
use std::any::Any;
use rigless::chip::{Place, Timeline};
use rigless::chip::uart::Peer;
use rigless::device::{Device, DeviceContext, RegisterFile};
use rigless::mcu::{Mcu, Operate, Single};
use rigless::snapshot::Store;
use rigless::soc::{Access, Bus, Cpu, Endian, Layout, Peripheral, ReadAction, Region, RegisterSpec, Width, WriteModify};
use rigless::time::{EventId, VirtualNanos};
// `DR` takes a word to send and answers the last word received, which the controller holds apart.
// `SR` says one is waiting.
static LINE_REGISTERS: [RegisterSpec; 2] = [
RegisterSpec { name: "DR", 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: &[] },
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Line { Dr, Sr }
impl Layout for Line {
const REGISTERS: &'static [RegisterSpec] = &LINE_REGISTERS;
fn from_index(index: usize) -> Option<Self> { [Self::Dr, Self::Sr].get(index).copied() }
fn index(self) -> usize { self as usize }
}
const WAITING: u64 = 1;
/// The controller's one interrupt, by its position in the instance's list.
const RECEIVED: usize = 0;
/// A word on the line takes a millisecond.
const FRAME: VirtualNanos = VirtualNanos(1_000_000);
/// The controller, what's on the far end of its line, the word it's sending and the last it received.
#[derive(Debug, Default)]
struct Uart { far_end: Option<Place<dyn Peer>>, sending: Option<(EventId, u16)>, received: u16 }
impl Uart {
/// The chip on the far end as the type the board hung there, for the board to hand it what it
/// measures. This is the one downcast.
fn chip_mut<C: Peer>(&mut self) -> Option<&mut C> {
let chip: &mut dyn Any = self.far_end.as_mut()?.chip_mut();
chip.downcast_mut()
}
}
impl Device for Uart {
type Register = Line;
fn read(&mut self, register: Line, registers: &mut RegisterFile, context: &mut DeviceContext<'_>) -> Option<u64> {
if register != Line::Dr { return None; }
registers.set(Line::Sr, 0);
context.lower(RECEIVED);
Some(u64::from(self.received))
}
fn write(&mut self, register: Line, value: u64, _registers: &mut RegisterFile, context: &mut DeviceContext<'_>) {
if register == Line::Dr {
let end = context.schedule(VirtualNanos(context.now().0 + FRAME.0));
self.sending = Some((end, u16::from(value.to_le_bytes()[0])));
}
}
fn on_event(&mut self, event: EventId, _registers: &mut RegisterFile, context: &mut DeviceContext<'_>) {
let Some((_, word)) = self.sending.filter(|(end, _)| *end == event) else { return };
self.sending = None;
// The far end hears the word as its stop bit ends, unless it's silent.
if let Some(chip) = self.far_end.as_mut().and_then(Place::answering) {
chip.receive(word, &mut Timeline::new(context));
}
}
fn poll(&mut self, registers: &mut RegisterFile, context: &mut DeviceContext<'_>) {
let Some(place) = &mut self.far_end else { return };
let now = context.now();
// A silent far end's words are lost, as down a cut line.
place.lose_while_silent(now);
while let Some(word) = place.answering().and_then(|chip| chip.transmit(now)) {
// Each word is a transaction: a trigger counts it, and a flip or a drop lands on it.
if let Some(word) = place.sent(u32::from(word)) {
self.received = u16::try_from(word & 0xFFFF).unwrap_or_default();
registers.set(Line::Sr, WAITING);
context.raise(RECEIVED);
}
}
}
fn reset(&mut self) {
self.sending = None;
}
}
/// The gauge: it says its reading once every 10 ms, and keeps what it hears.
#[derive(Debug, Default)]
struct Gauge { reading: u16, due: u64, heard: Vec<u16> }
impl Store for Gauge {}
impl Peer for Gauge {
fn receive(&mut self, word: u16, _context: &mut Timeline<'_, '_>) {
self.heard.push(word);
}
fn transmit(&mut self, now: VirtualNanos) -> Option<u16> {
(now.0 >= self.due).then(|| {
self.due += 10_000_000;
self.reading
})
}
fn received(&self) -> &[u16] {
&self.heard
}
}
const LINE: Peripheral = Peripheral { name: "LINE", base: 0x4000_0000, size: 0x100, interrupts: &[7], registers: &LINE_REGISTERS };
static MEMORY: [Region; 1] = [Region::ram("sram", 0x2000_0000, 16 << 10)];
let cpu = Cpu::new("a-core", false, 16, Endian::Little);
let mut part = Single::new(Bus::builder(cpu, &MEMORY, [LINE]), "LINE", Uart::default())?;
let data = LINE.base + u64::from(Line::Dr.spec().offset);
// The board hangs the gauge, and reaches it again as a gauge to hand it its reading.
part.pair().0.far_end = Some(Place::new(Box::new(Gauge::default())));
part.pair().0.chip_mut::<Gauge>().unwrap().reading = 43;
// At a stop the controller asks the far end for what it has said by then.
part.poll(VirtualNanos(0));
assert_eq!(part.read(VirtualNanos(0), data, Width::W32)?, 43);
// The firmware's word reaches the gauge as its frame ends.
part.write(VirtualNanos(0), data, Width::W32, u64::from(b'?'))?;
part.advance_to(FRAME);
assert_eq!(part.pair().0.chip_mut::<Gauge>().unwrap().heard, [u16::from(b'?')]);
// Silenced, as a board condition does, the gauge's next word is lost on the line.
part.pair().0.far_end.as_mut().unwrap().silence(true);
part.pair().0.chip_mut::<Gauge>().unwrap().reading = 44;
part.poll(VirtualNanos(10_000_000));
assert_eq!(part.read(VirtualNanos(10_000_000), data, Width::W32)?, 43);
# Ok::<(), Box<dyn std::error::Error>>(())
```
The controller asks the far end at every stop of the session, through `Device::poll`, and the far end hands over what it has said by then. So a word a chip says lands at the first stop at or after its instant: a host on a socket at the stop its bytes are noticed at, and a chip's word at the next event of the part or the next step of an engine. A run with more stops, like one reading `--outputs` often, hands the firmware a chip's words sooner. Keep a chip's words on instants the run stops at anyway, or give its controller an event of its own to ask at.
Your board does the rest. Its `condition` silences the place with `Place::silence`, its `place` hands it out as a `chip::Placed`, so a trigger counts its words and a fault flips or drops one, and its `sense` reaches the chip through `chip_mut` to hand it what it measures. A snapshot keeps the place and the chip through their own `Store`, which a chip that can't be kept leaves empty, as the gauge does.
### Extending the STM32F427
Everything above is for writing a part from scratch. Most boards don't need that. They use the STM32F427 this crate ships and add what's on the PCB around it: memory and a display on the FMC, a chip on SPI, a file in flash. You can do all of that without writing an `Mcu` or forwarding any methods.
- **Memory**: call `Bus::add_region`. The emulator maps it like the part's own memory, you can load an image into it, DMA reaches it, and it survives a reset.
- **A memory-mapped chip**, like a display on an FMC bank, implements `chip::parallel::Peer`. It gets each access as an offset into its window, plus a `DeviceContext` for time, events and interrupt lines. It doesn't see the part's registers. The part's module lists its windows and what enables each one. `stm32f427::NOR_PSRAM` is NE1 to NE4, and each bank answers only while its `FMC_BCRx.MBKEN` is set. `mcu::Wired` puts the chip in a window and passes every other access to the part. `Wired` is an `Mcu` too, so your board holds it in place of the part. The part is `mcu.part` and the chip is `mcu.chip()` and `mcu.chip_mut()`. For two chips, wrap one `Wired` in another.
- **A chip on SPI or a UART** attaches to the part's controller with `part.spi[0].attach(chip)`, or `attach_selected` with a chip select, like the chips in [A chip on a line of your own](#a-chip-on-a-line-of-your-own).
- **A raw file** loads at an address with `Image::read(path, Some(address))`, like a data file in the NOR flash you added.
Here's a board with 16 MB of SDRAM on the first SDRAM bank, 16 MB of NOR flash on NE1, a made-up display on NE4 (A16 low is a command, A16 high is data), and an FRAM on SPI1:
```rust
# #[cfg(feature = "stm32f427")]
# fn main() -> Result<(), Box<dyn std::error::Error>> {
use std::num::{NonZeroU32, NonZeroU64};
use std::path::Path;
use std::sync::{Arc, Mutex};
use rigless::chip::fram::Fm25v02;
use rigless::chip::parallel::Peer;
use rigless::device::DeviceContext;
use rigless::emulator::{self, Driver, Host, Start, Stop};
use rigless::image::Image;
use rigless::machine::{Board, Target, armv7m};
use rigless::mcu::{Mcu, Operate, Wired};
use rigless::session::Session;
use rigless::snapshot::{Store, store};
use rigless::soc::stm32f427::{NOR_PSRAM, Stm32f427};
use rigless::soc::stm32f427::generated::FMC;
use rigless::soc::stm32f427::generated::fmc::{Register as Fmc, bcr4};
use rigless::soc::{Error, Layout, Region, Width};
use rigless::time::{NO_DEADLINE, VirtualNanos};
const SDRAM: Region = Region::ram("sdram", 0xC000_0000, 16 << 20);
const NOR: Region = Region::flash("nor", 0x6000_0000, 16 << 20).erased_to(0xFF);
/// A16 of the bank, which is byte address bit 17 on a 16-bit bus.
const DATA: u64 = 1 << 17;
/// The display keeps the last command and the data words sent after it.
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
struct Display { command: u64, data: Vec<u64> }
store!(Display);
impl Peer for Display {
fn read(&mut self, _offset: u64, _width: Width, _context: &mut DeviceContext<'_>) -> u64 {
0
}
fn write(&mut self, offset: u64, _width: Width, value: u64, _context: &mut DeviceContext<'_>) {
if offset & DATA == 0 {
self.command = value;
self.data.clear();
} else {
self.data.push(value);
}
}
}
#[derive(Debug)]
struct MyBoard { mcu: Wired<Stm32f427, Display> }
impl Store for MyBoard {
fn store(&self, to: &mut rigless::snapshot::Writer) -> Result<(), rigless::snapshot::Error> {
self.mcu.store(to)
}
fn load(&mut self, from: &mut rigless::snapshot::Reader) -> Result<(), rigless::snapshot::Error> {
self.mcu.load(from)
}
}
impl MyBoard {
fn new() -> Result<Self, Error> {
let mut part = Stm32f427::new()?;
part.bus_mut().add_region(SDRAM)?;
part.bus_mut().add_region(NOR)?;
part.spi[0].attach(Fm25v02::new());
let mcu = Wired::new(part, NOR_PSRAM[3], Display::default())?;
Ok(Self { mcu })
}
}
impl Board for MyBoard {
fn name(&self) -> &'static str { "my-board" }
fn target_names(&self) -> Vec<&'static str> { vec!["stm32f427"] }
fn mcu(&mut self, target: usize) -> Option<&mut dyn Mcu> { (target == 0).then_some(&mut self.mcu) }
fn targets(&mut self) -> Vec<Target<'_>> {
# let core = armv7m::Core {
# variant: armv7m::Variant::M4,
# clock_hz: NonZeroU64::new(168_000_000).unwrap(),
# interrupt_lines: NonZeroU32::new(u32::from(self.mcu.cpu().interrupt_lines)).unwrap(),
# priority_bits: 4,
# refclk_divider: NonZeroU32::new(8).unwrap(),
# instructions_per_second: NonZeroU64::new(250_000_000).unwrap(),
# boot: "flash",
# }
# .spec_with_parts(&rigless::cpu::armv7m::windows(self.mcu.bus()), armv7m::Fpu::FpV4Sp, armv7m::MpuRegions::Eight);
// Same core setup as in "The board" above, using the part's numbers.
vec![Target::new("stm32f427", &mut self.mcu, core)]
}
}
/// Copies the display's data to the window thread at each stop, and stops every 16.7 ms of
/// virtual time, about once per frame.
struct Presenter { frame: Arc<Mutex<Vec<u64>>> }
impl Driver<MyBoard> for Presenter {
fn on_stop(&mut self, session: &mut Session<MyBoard>, stop: Stop, _host: &mut dyn Host) -> Option<VirtualNanos> {
if let Ok(mut frame) = self.frame.lock() {
frame.clone_from(&session.board().mcu.chip().data);
}
Some(VirtualNanos(stop.at.0 + 16_666_667))
}
}
/// Runs the firmware on its own thread, with the data file in NOR flash. Your window loop reads
/// `frame` until the thread finishes.
fn start(firmware: &Path, data: &Path, frame: Arc<Mutex<Vec<u64>>>) -> Result<std::thread::JoinHandle<()>, Box<dyn std::error::Error>> {
let images = vec![Image::read(firmware, None)?, Image::read(data, Some(NOR.base))?];
let session = Session::new(Box::new(MyBoard::new()?), 0, NO_DEADLINE)?;
Ok(std::thread::spawn(move || {
let _finished = emulator::run(session, &Start::new(&images), Presenter { frame });
}))
}
# let _ = start;
let mut board = MyBoard::new()?;
let display = NOR_PSRAM[3].base;
let at = VirtualNanos(0);
// NE4 refuses accesses until the firmware enables it. Then the display takes a command and a data word.
assert!(board.mcu.write(at, display, Width::W16, 0x2C).is_err());
let bcr4 = FMC.base + u64::from(Fmc::Bcr4.spec().offset);
board.mcu.write(at, bcr4, Width::W32, bcr4::MBKEN.mask())?;
board.mcu.write(at, display, Width::W16, 0x2C)?;
board.mcu.write(at, display | DATA, Width::W16, 0xF800)?;
assert_eq!((board.mcu.chip().command, board.mcu.chip().data.as_slice()), (0x2C, [0xF800].as_slice()));
assert!(board.mcu.bus().memory().contains(&SDRAM));
# Ok(())
# }
# #[cfg(not(feature = "stm32f427"))]
# fn main() {}
```
A `Driver` runs on the core's thread during the run, so it and everything it holds must be `Send`. Share what the display shows through an `Arc<Mutex<..>>` like `Presenter` does, and keep your window on the main thread, where most windowing libraries want it.
### 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`, or `lantern boots lantern-core from a region called rom, but lantern-core has no such region (it has flash, sram)` when `armv7m::Config::vector_table` is set | `armv7m::Core::boot` has to be one of the `MEMORY` names |
| 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 |
| A table names a board by something other than what the board calls itself | `the machine built for lantern calls itself lamp, and a run names a machine by what it calls itself` from `cli::main` | The name in the table has to be what `Board::name` answers |
| `--link` on a board with no edge | `lantern has no edge a plant couples to, so it can't be an engine's target` | An engine steps a board through its `Edge`. Implement it, or run the board without `--link` |
| `--outputs` on a board with no edge | `lantern has no edge, so there are no outputs to read. ...` from `emulator::run_printed` | The outputs are the edge's. Implement `Edge`, or leave `--outputs` out |
| An edge's fields don't add up to its types | `heater's FIELDS add up to 2 bytes, and its Signals type takes 4 on the wire` from `Session::new` or `link::serve` | Each `Field::width` has to be the bytes its value takes on the wire, in the order the type writes them |
| A chip in `CHIPS` has no place | ``heater's CHIPS name gauge, and its `place` gives none for it`` from `Session::new` or `link::serve` | `Edge::place` answers the place of every chip `CHIPS` names, in that order |
## The SoCs and the machines
Everything a firmware runs on: the SoCs, the models of their peripherals, the machines built from them, and each core family.
| What | Where | Now |
|---|---|---|
| An SoC | one module under `soc`, behind a Cargo feature its boards turn on | `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 analog-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, and `chip::memory_card` is a 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. The LIS3MDL speaks SPI and, strapped by its `SA1` pin, I2C too. `chip::icm42688p` and `chip::icm42605` are two more inertial units, on a serial interface with a FIFO of packets, and `chip::dps310` is a barometer on an I2C bus that hands a driver two raw words and the coefficients to turn them into a pressure. `chip::qmc5883l` is a three-axis magnetometer on an I2C bus that a driver polls for a ready flag and six bytes. `chip::nmea` is a receiver that reports a position as NMEA sentences on a serial port |
| A core family | one module under `cpu` and one under `machine`, compiled always, naming no SoC | `cpu::armv7m` is the ARMv7-M core and the blocks the architecture fixes at an address, and `machine::armv7m` the typed fields a target of it fills in |
| A machine | one module under `machine` declaring a board struct that implements `Board`. A board is behind a feature of its own that turns on its SoC's, and an SoC's bench is behind the SoC's | `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, Operate};
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 | Behavior | 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: `Behavior | 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, an analog effect with no register behavior, 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 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 I2C 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 and the `CoreSpec` its emulator takes. `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 modeled 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 core's description comes from the module of the MCU's core family. `armv7m` is the first: one typed field for each thing the family needs to make a processor, with `NonZero` where a zero makes no core, 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 once, 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 I2C 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> = ["tables"]`, in `default` only when no board there turns it on, and the module to `src/soc.rs` under `#[cfg(feature = "<name>")]`. `tables` is what the generated tables import the crate through, so `src/lib.rs` needs nothing.
5. Add the feature to the checks in the `lint` 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. `Operate::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.
9. A controller that chips hang on marks what a trigger counts on each chip's `chip::Place`: `begin` before it reaches the chip for a transaction, `release` when the transaction ends, and every word the chip answers through `answered`, or through `sent` on a line where each word is a transaction of its own. Hand the place out with a `place_mut`. A controller that marks nothing has chips no trigger counts and no flip reaches.
### Adding a chip
Chips live under `src/chip`. Each one is written from its own datasheet and doesn't name an SoC, so any board can use it. A chip implements the trait its controller calls: `chip::spi::Peer` on SPI, `chip::uart::Peer` on a UART, `chip::sd::Card` on the SD interface, `chip::i2c::Peer` on I2C (the part's bus or one inside another chip), and `chip::parallel::Peer` in a window of the part's addresses. Those traits live next to the chips for the same reason. Every one but `parallel::Peer` hands the chip a `chip::Timeline` with each word, the controller's context narrowed to the time and its event queue, so a chip reads the time and schedules an event it hears at its own `on_event`, which the controller hands every chip it carries, silenced or not. An event a chip scheduled doesn't survive a reset of its part, though the chip does, so the controller calls its `restarted` at the reset's instant and a chip that keeps time schedules again there. A chip in a window gets the whole `DeviceContext`, since the window has interrupt lines of its own. An SPI word is a `u32` with the frame's width beside it, so a part with wider frames needs no other seam. A chip's state is what it was sent. A stand-in for a chip has no real state, so it keeps a record of what it saw. A chip's docs open with the same two tables as a peripheral's, with the same four columns in the first, so add the chip to the check that holds those tables to real tests. The board attaches the chip to its controller when the board is built. On SPI it goes behind the chip select the board names (a port, a pin and the active level), and that pin's level reaches the controller as its request line. On I2C its address selects it. Unit tests drive the chip directly through its trait, with a controller's context wrapped in a `Timeline`. The firmware the board runs tests the chip and its select together. A chip that measures something has setters named after its datasheet, like `set_rate`, `set_field` and `set_pressure`, each in the chip's own axes, and reports that value until it's set again. The board gets to them through the controller's `chip_mut`, or a card through `Sdio::card_mut`, which hands the chip back as the type it attached, and the board converts body axes to the chip's axes.
### 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 a `CoreSpec` from its core family's module, and `mcu` hands the same MCU back by index. Give it a feature of its own in `Cargo.toml` that turns on its SoC's, and name it in `default` if the default build should carry it. Gate its module, its row and every test that builds it on that feature, add the feature to the checks in the `lint` job of `.gitlab-ci.yml`, and add the row to the table `machine::all()` returns, which is how `rigless run` and `rigless run --link` both find it. An SoC's bench rides on the SoC's own feature instead. You hand a board outside this crate to `cli::main` in a table of your own instead. 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`, `Signals` and `Conditions`. Its `REST` is what the board senses when it's built, and all three types implement `link::Wire`, which is what lets `edge` answer with the board itself. `condition` turns each silence onto the chip's place with its controller's `silence`, which answers whether a chip hangs there, and senses what the board last sensed again, so a condition takes effect at once. `sense` hands each chip what its `Sensed` answers under its `Condition`, so one chip can read wrong while the one beside it reads the truth. `draw` has each `Sensed` draw its noise for the step, keyed by the noise condition's own name, and senses again when any moved. A serial line's error rates go on the port's `chip::uart::Errors` in `condition`, and its seed in `draw`. `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. `CHIPS` names every chip a trigger or a one-shot fault can reach, and `place` hands out each one's place by that name, through its controller's `place_mut`. Its `edge` answers itself.
### Adding a core family
A family is a core and the spec that makes it, and neither has to live in this crate. The core implements `emulator::Processor`: it runs a stretch of the family's instructions on the `emulator::CoreBus` it's lent, and between stretches it does what the runner, the models and a debugger ask of it. The trait's documentation says what the runner relies on, from how instructions are counted to when the core hands control back. A function makes the core from the family's own typed configuration, and refuses one the core can't be. A machine module renders the spec: a closure in `processor` that hands that configuration to the function, an enum of the variants it accepts, and the rate the core retires instructions at. `emulator::run` calls whatever constructor the spec carries, so no generic module changes and nothing in the runtime names your family. `cpu::armv7m::processor` and `machine::armv7m` are the family this crate carries. Its code generator, page table and block cache stay private, so a family of your own brings its own way to run instructions, an interpreter or a translator. A family names the ELF machine its core runs in `CoreSpec::elf`, so a firmware built for another is refused before it runs, and says in `CoreSpec::code_address` where a function's first instruction is from its symbol's value. Its processor answers `Processor::data_model`, how wide a pointer and a `long` are, which the run reads the firmware's RTT block, a `dprintf`'s `long`, a host call's elapsed time and a debugger's widest access by. A family that maps memory into its core says how big its pages are, and one that maps nothing takes regions of any size, every access reaching it through the bus. A family answers, address by address, whether it can stop before an instruction there, and the run refuses a debugger's breakpoint or a watch on an instruction it can't keep. One that never stops at an instruction writes none of the breakpoint methods. A family that asks `CoreBus::stops_before` before each of an instruction's accesses has a debugger's watchpoint stop the core before the access, and one that doesn't still stops for it, right after the instruction that made the access. A kernel's walk reads its switched-out threads back by the names the family's target description gives its registers, so a family writes nothing for GDB to see them. A board whose MCUs are of two families runs each as its own target.
`cpu::armv7m::processor` also runs the ARMv7-M core without a session: make a `machine::armv7m::Config` from a `machine::armv7m::Core`, map pages of your own memory into the processor, its `page_size` bytes each, boot it and run it on a `CoreBus` of your own.
## 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 {
/// `CTRL` at offset `0x0`, resets to `0x0000_0000`, read-write.
Ctrl = 0,
/// `STATUS` at offset `0x8`, resets to `0x0000_0002`, read-write.
Status = 2,
}
/// The fields of `CTRL`.
pub mod ctrl {
/// `EN`, bit 6.
pub const EN: &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.
**Sixteen test binaries boot a firmware** on the core built in, and they need one built under `firmware/`, bear-bones, conformance or the template, 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 moved to another line that proves the first would notice, two runs of the binary that print the same thing to the byte, three runs at once in one process that are the same run, a debugger that stops it at a breakpoint and a watchpoint without changing it, and one that steps it and writes its flash, after which the run takes no snapshot. `conformance` is the bench, every check the conformance firmware declares, held to a budget the job enforces. `debugger` holds bear-bones on its read and access watchpoints and restarts it with extended mode's `run`, and runs a program a few instructions long to show a `BKPT` halting the core for a debugger, a step over a faulting load stopping at its handler, memory nothing answers refused, `monitor reset`, the interrupt reaching a core that waits, the registers against the target description with and without a floating-point unit, the protocol's framing, and one debugger at a time. It also runs GDB itself, `gdb-multiarch`, against the binary on bear-bones, and fails rather than skipping when that isn't installed. `debugger_protocol` holds the server to "Debugging with GDB" for GDB 18.1, appendix E where it's particular, thread-ids in both forms among it, and runs GDB itself for what only GDB's use of the protocol shows, like its own `record full`. `debugger_stops` holds where a debugger stops the core and what that costs the run: a run serving GDB with nobody attached printing what a run without it prints, the interrupt reaching a core that has waited or slept a long way, watchpoints well past silicon's four that stop the core before the access they watch, or right after the instruction once a model has seen part of it, and halt points at an exact instruction count or instant, with GDB itself reading and breaking on `$icount`. `debugger_peeks` holds a debugger's reads to changing nothing: every model peeked on a busy part, bear-bones read in its TIM2 handler with the run it makes and the run it prints unchanged, `monitor peek off` bringing the firmware's read back, a peripheral whose clock is off, and a stop by the interrupt, a step or a halt point finding every model settled at its instant. `debugger_memory` holds what a debugger reads and writes of memory and flash, and what `DFSR` says halted the core. `debugger_live` holds conditions and `dprintf` worked out in the target, and non-stop mode reading memory while the firmware runs. `debugger_reverse` holds `monitor save`, `monitor restore`, `reverse-stepi` and `reverse-continue` to the run that never went back, by what it hears and what it prints. `two_targets` debugs both MCUs of a board with two, one connection each. `gdb_sessions` runs GDB's whole sessions under `tests/gdb` against bear-bones and its other programs and holds each to the transcript beside it. `semihosting` runs bear-bones' programs that print, exit, and write and read files on GDB's host, and `output_ports` the ones that write to the ITM and to RTT's terminal. `link` spawns the binary as an engine would and steps it across the link. `serve` serves a board from outside the crate's table of machines as an engine's target, on the template firmware. `snapshot` stops bear-bones and the conformance bench, saves each, and restores it into a new core that runs on as the run that never stopped did. `core` needs no firmware: it runs programs a few instructions long, to show a core that locked up resets and runs on, a floating-point exception pends no interrupt line, an exception's entry stores at the instruction it interrupts, and a stop saves the same bytes however the run was split into stretches on the way there. `outside_family` and `core_alone` need none either, and see only the public API: a toy family the test writes runs on the runner as this crate's own does, a debugger's watchpoint stopping it though it never asks before an access, and the ARMv7-M core runs on a bus of the test's own with no session around it. `cortex_m7` needs none either: a Cortex-M7 on a board of the test's own boots from a vector table away from zero and runs a start-up's cache enables and maintenance, its writes to registers of the part's own, and a double-precision divide.
**Everything else runs with no core**: the session driven by hand as the core 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 core absent, since it can't execute the code the core generates.
**The fuzz targets** under `fuzz/` take what comes from outside a run and the core itself: arbitrary code on the core, a processor's saved state, a snapshot file, what a debugger sends, the link's frames, a firmware image and its symbols, and an SVD for the generator. Each has to answer or refuse its input, and never panic, hang or touch memory it doesn't own. The `fuzz` job runs them on every merge request with `cargo fuzz` and its address sanitizer. To run one yourself, on the nightly toolchain `cargo fuzz` needs and naming your host's target, since the `cargo fuzz` you install may default to another:
```text
cargo +nightly fuzz run --target x86_64-unknown-linux-gnu core
```
## 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 each a module behind a feature of its own, and the runtime's generic modules know neither by name.