consortium-cfg 0.5.0

Configuration subsystem support for the Consortium project.
Documentation

consortium-cfg

Turns a Consortium.toml manifest into validated configuration, generated Rust, and Linux integration artifacts.

This is the crate that makes Consortium's "explicit cross-core contracts" idea pay off. A manifest declares what the system is — which cores exist, which memory windows they share, which doorbell rings which — and this crate checks that against the chip's real capabilities, then emits the bring-up code and device-tree nodes that make it so. Firmware and application authors never write the address arithmetic, the UIO ids, or the doorbell wiring by hand.

Pipeline

Consortium.toml
  └─ session   parse + resolve the chip database + validate       → Session
       └─ bridge    lower into the components IR                  → ShuHan, BellA, …
            └─ components   each IR node generates its own output
                 ├─ Rust tokens ─ render ─→ src/consortium.gen.rs
                 ├─ DTS nodes   ────────→ merged tree or .dtso
                 └─ Kconfig / boot / systemd fragments
Stage Module Produces
Parse, validate session Session — manifest bound to its chip database
Lower bridge The components IR, addresses and UIO ids resolved
Generate components Rust tokens, DTS nodes, UIO/Kconfig fragments
Format render Formatted Rust source

Schema types come from consortium-cfg-common; chip databases, per-core chip maps, and vendor device trees come from consortium-data (re-exported here as database, chipmap, dts for historical paths). consortium-builder drives this and adds compilation, staging, and packaging.

What validation catches

Session::from_toml resolves [profile] chip through the embedded databases and then rejects manifests asking for things the chip cannot do. The CfgError variants are the interesting part of this crate's contract:

  • a doorbell the chip does not have, or a doorbell channel outside its range;
  • a channel whose party is not a visible core, or has no declared endpoint;
  • two channels claiming the same doorbell channel;
  • memory regions out of bounds, overlapping, or misaligned for the MPU;
  • debug regions colliding with IPC regions;
  • a peripheral owned by a core that cannot reach it — checked against the per-core chip map, so the filter is the peripherals that core's CMSIS view actually contains;
  • [peripheral.*] with secure = true, since the chip HAL's singletons carry only the non-secure alias.

Catching these at manifest-parse time is the whole point: each would otherwise surface as a silent bus error or a link that never wakes, on hardware, at 3am.

Where the fixed decisions live

bridge pins the AP-side address layout so the firmware, NVIC, and Linux DTS outputs cannot disagree:

  • every device gets its uioN id by ascending mapped base — the aggregate IPC shared-memory device, one device per distinct doorbell, and the aggregate debug-ring device — so ids follow the memory map the kernel enumerates;
  • within the IPC device, a region's mmap_id is its index in channel-declaration order, and a doorbell always exposes its register window as mmap_id 0.

These are conventions rather than discoveries, which is why they are written down in one place: three separate outputs have to agree on them.

The components IR

Codegen is deliberately not centralised. Each IR type emits its own output (ShuHan::initialize, BellA::nvic_enable, ToUio::to_uio), so a new component adds a module rather than a branch in a shared emitter.

Module Emits
ipc_shm Channel markers, instance metadata, and endpoint transport installation.
bella Type-A doorbell interrupt enablement.
peripheral The singleton allowlist, manifest constants, IRQ handlers, and NVIC setup.
time The sealed Embassy time-driver vector and defmt::timestamp!.
dbg Ring layout, AP console resources, and the builder's environment values.
uio generic-uio DTS nodes and their reserved-memory carveouts.
memory_to_uio Groups shared-memory regions onto one UIO device.
dts_render The Linux device tree, merged onto a vendor base or as an overlay.
address AP (uio_id, mmap_id) vs a controller's physical base.
region One shared-memory window's construction expression.

Two conventions to know before adding a component:

  • Emitted unsafe blocks carry their justification as a /// doc comment, not a // comment. proc_macro2 discards // comments because they are not tokens (a /// comment survives as a #[doc] attribute), so render rewrites the doc lines back into ordinary comments after formatting. That is what keeps the rationale attached and satisfies clippy::undocumented_unsafe_blocks in the generated crate.
  • Items referencing a chip HAL are gated to target_arch = "arm", because the host trybuild harness never links one.

Two design details worth understanding

The AP zeroes the shared window; controller cores do not. A carveout is no-map reserved memory that survives a warm reboot, so a previous run's descriptor and slot bytes could read as valid CIPC state. The AP is the primary in the descriptor handshake, so it takes the first touch (mmap_zeroed).

A doorbell singleton moves only once. Two transceivers may share one doorbell block (disjoint channels of the same MU or IPCC), so ipc_shm's DoorbellSource gives the first user the constructor parameter and has later ones borrow from it. The AP has no singletons at all — it reaches doorbells through UIO mmap.

Generated code

The output is src/consortium.gen.rs in each endpoint crate, which the crate include!s. It defines Config, Peripherals, Channels, and a synchronous init(config) that:

  1. resets the CDBG ring when the core has a [dbg.<core>] logger;
  2. applies the selected MPU, NVIC, tracing, UIO, and time-driver policy;
  3. installs each unsplit IPC transport behind its generated channel singleton;
  4. returns the endpoint's peripheral allowlist and channel bundle.

The user connects channels independently with Transceiver::connect(c.NAME).await or the deferred Endpoint::new(c.NAME).connect().await spelling.

Because a generated crate cannot be cargo checked before the pipeline has run, the melt-pot endpoint crates are excluded from the workspace.

Device-tree output

[dts] mode selects between two forms sharing one analysis pass:

Mode Output Can delete properties?
merge (default) a full tree yes
overlay a .dtso no

That difference is not cosmetic: stripping a remoteproc node's mboxes/mbox-names requires deletion, so a manifest needing that fixup must use merge mode. Overlay mode returns a warning when it has to skip one.

Adding to the schema

Work outward, because each step's tests depend on the previous: schema types in consortium-cfg-common → validation in session → lowering in bridge → codegen in components. Then update the snapshot tests under tests/.

Testing

cargo test -p consortium-cfg
just test cfg host

tests/ holds insta snapshots of generated output plus a trybuild UI harness. Update snapshots (cargo insta review) when generated output changes on purpose — the snapshot diff is how a codegen change becomes reviewable.

Consortium.example.mimx95.toml, Consortium.example.stm32mp257.toml, and src/session.rs's tests are the best references for current manifest syntax.

License

Apache-2.0. See LICENSE.