Rig files
What a rig is, on disk: two files, boards/rigs/<name>/rig.yml (the
rig metadata file) and boards/rigs/<name>/<name>.yml (the
rig content file), plus the optional qualifier-delta fragments
that layer onto the content file. The authority is the loader itself —
scripts/rigc/loader/documents.py, axes.py, delta.py and
params.py — and every key below is read (or deliberately never read)
exactly as that code does today.
This page is reference, not narrative — Building a rig that already exists and Making the rig permanent teach the concept; this page looks up the facts. A shield can also become a rig of one instance without either file existing at all — see Promotion.
The two files
Metadata files are named after the entity type — rig.yml, the same
filename in every rig’s folder, the same way board.yml and
shield.yml are. Content files are named after the entity instance:
<name>.yml, constructed from the rig’s own name:, never read back
from the folder it happens to live in.
rig.ymlThe rig’s identity and its qualifier axes. Carries no hardware description whatsoever — not an instance, not a wire, not a board.
<name>.ymlThe assembly: instances, and any wires between them. Required — a rig with no content file is refused (
lang-content, naming the expected path) before anything else about it is read.
Both are parsed by the same mark-aware YAML reader
(loader/documents.py), with no rig: wrapper on the content side —
only rig.yml nests its keys under a top-level rig: mapping.
rig.yml — metadata
rig:
name: <string> # required
revision: # optional
format: letter | number | major.minor.patch | custom
default: "<id>" # optional
exact: true | false # optional, default false
revisions:
- name: "<id>"
variants: # optional
default: <name> # optional
list:
- <name> # or {name: <name>}
rig: name:The rig’s identity — its
rig.ymlname:field, not its folder name (the two conventionally match; nothing enforces it). Required; a missingrig:block or a missingname:inside it stops the load before anything else is read.rig: revision:The rig’s own revision axis, in upstream’s hwmv2 shape (
board.yml’s own block, copied key for key) — the singular keyrevision:, not the pluralrevisions:a shield’s own axis uses (see Shield template properties).format:is required whenever this block is present, one ofletter(a single uppercase letter),number(digits only),major.minor.patch(three dot-separated non-negative integers) orcustom— declaringcustomis legal YAML but rejected the moment the axis is actually resolved (rigc implements the first three formats only). Every declared id inrevisions:must be a quoted string matching the declared format — an unquoted numeric-looking id is rejected rather than silently read as a YAML number.default:, if given, must be one of the declared ids.exact: truedisables hwmv2’s own nearest-lower match: normally a requested revision that is not itself declared resolves down to the highest declared revision that is<=the request (comparing per-format —major.minor.patchalso zero-pads a short request,"1"becoming"1.0.0", before either comparison runs); withexact: true, any requested value that is not an exact declared member is refused instead.rig: variants:The rig’s own variant axis — topology alternates, not a hardware axis: a variant selects which qualifier-delta fragments apply (below), never a board or a socket mapping.
list:entries are either a bare name or{name: <name>}(any other key on a mapping entry is silently ignored — see What rig.yml never declares).default:, if given, must be one of the listed names. Resolution takes an exact declared member only; there is no hwmv2 machinery on this axis.
A rig with variants and revisions must not let any two distinct
selections — a variant alone, a revision alone, or a combined
(variant, revision) pair — construct the same delta-fragment stem
(see Qualifier delta fragments): a variant literally named the same as
a revision id is the single-axis case of the same collision, and both are
refused (lang-variant) at load time, before any fragment is read.
Combined selection resolves variant, then revision — both independently against the declarations above, an unselected axis taking its declared default (or being refused if it has none and nothing was requested). There is no interaction between the two beyond the shared-stem collision check: a rig may declare either axis, both, or neither.
What rig.yml never declares
No ``board:`` key exists anywhere in this grammar — not at the top
level, not per variant. A rig describes a topology; the
invocation coordinate (-b/-DBOARD=, see
Commands) is the only source of the board, for every rig, with
no exception — including a rig built against more than one real board
(the same content file resolves on either, provided both boards expose
the sockets it names; see Making the rig permanent
and the arduino_r3 alias convention it describes). A rig build with
no board given is a configure-time error, not a fallback.
A stray board: (or sockets:) key on a variants: list entry is
not rejected — it is silently ignored, the same as any other
unrecognized key in this grammar. Nothing in rig.yml today populates
a per-variant board or socket table.
<name>.yml — content
instances:
- name: <string> # required
shield: <string>[@<revision>] # required
socket: <label> # single-plug shields only
sockets: # OR, plural shields only
<slot>: <label>
invert: true | false # optional
config:
<config-element-label>: <value>
params:
<device-label>:
<property-name>: <value>
wires:
- from: <instance>.<node>
to: <instance>.<node>
route: adhoc # or: {via: <position-name>}
instances:Required, a list — an empty list is legal (a rig with no modules attached still configures, structurally identical to a plain board build). Each entry is one instance:
name:/shield:Both required.
shield:is the template’s own name, optionally qualified with@<revision>(the shield’s revision axis — see Shield template properties — never the rig’s own).socket:/sockets:Mutually exclusive, and each legal only for the matching shield shape: a single-plug shield takes bare
socket: <label>; a plural shield (more than one plug) takessockets:, a slot name → label map, one entry per plug the shield’s own plurality requires. Naming the wrong form for the shield’s own plurality is refused (lang-instance-socket). Either is optional — an omitted socket (a whole omitted slot, in the plural case) is left to the analyzer’s own unique-by-type inference, which succeeds only when exactly one board socket of the needed type exists.<label>may name a carrier’s own exposed socket as<carrier instance>.<socket>instead of a board socket directly (see carrier).invert:A boolean, default false.
config:One entry per routing jumper or strap this instance sets, keyed by the config element’s own devicetree label — never its node name, and with no hyphen/underscore normalization. The value is spelled as the config sheet spells it: a position name (
D2) for a jumper, an integer for a strap. Naming a label the shield has no config element for is refused (lang-config, listing the real labels).params:One entry per device whose
shield,paramsthis instance assigns, keyed by the device’s own devicetree label, each a property-name → value map. A bare integer literal is accepted directly; any other value must resolve (via the C preprocessor) as a token declared in that device’s ownshield,param-includes— the vocabulary is always the owning device’s, never somethingrig.ymlsupplies. Naming an undeclared device, or a property the device’s ownshield,paramsdoes not declare, is refused (lang-param) before any preprocessing runs; a value that fails to resolve islang-dt-include, naming the header to add it to. Every parameter a device declares with no authored default must end up assigned once every qualifier delta has applied, or the rig is refused (lang-param, “declares … as required”).
wires:Optional, a list of point-to-point jumper wires between two instances — a physical connection the shields’ own connectors don’t carry. No shield in the corpus ships one yet, so every rig on disk today omits this key; the grammar below is exercised only by fixtures.
from:/to:Both required, each a
<instance>.<node>dotted reference — the instance must exist in the effective topology at this point, and<node>must name exactly one referencable node of that instance’s own resolved shield (an unknown or ambiguous node islang-wire-ref).route:Required. Either a bare value (
adhoc— a hand-run wire, no further claim) or a mapping namingvia:— a connector position the wire is routed through. A mapping with novia:key is refused (lang-schema).
Qualifier delta fragments
A selected, non-default variant and/or revision may layer a delta
onto the base content — never required; a value that contributes nothing
at all is refused (see below). Stems are constructed from the rig’s own
name: and the selected value (a revision id normalized by
replacing . with _, e.g. 1.5 → 1_5 — never applied to a
variant name), in this fixed naming scheme:
Fragment |
Applies to |
|---|---|
|
the selected variant’s own content delta |
|
the selected variant’s own devicetree/Kconfig fragments (Zephyr’s
own |
|
the selected revision’s own content delta |
|
the selected revision’s own Kconfig fragment |
|
the combined (variant, revision) content delta — collected LAST |
A content delta (``.yml``) applies five operations, in this fixed order, every time:
instances:Patches an existing instance’s top-level keys — a given key replaces wholesale (never merges into what was there); an unspecified key inherits unchanged. Naming an instance the effective topology does not have is refused, naming the delta that already removed it if that is why. Two asymmetric resets apply on a shield change:
params:is always cleared (the old assignments are keyed to devices the new shield may not have); thesocket:/sockets:map carries forward unless the new shield’s slot names differ from the old map’s, in which case it resets the same way. A patch that suppliesparams:for an instance whose shield did not change must restate every property the effective topology already assigned — omitting one is refused (the restate rule: a wholesale replace that dropped a property would otherwise silently revert it to the shield’s own default).add-instances:Full instance declarations, same shape as the base
instances:entries. The named instance must not already exist.remove-instances:A list of instance names. Each must currently exist.
remove-wires:A list of
{from:, to:}pairs, matched against the effective wire list by their raw endpoint strings (a wire carries no other identity). Re-routing a wire is remove, then add — there is no in-place “replace”.add-wires:New wires, same shape as the base
wires:entries.
Every parameter invariant (params:’s own required-parameter rule, above) is re-checked over the
whole effective topology after each stage — a later stage may still
supply what an earlier one left unassigned, and a shield substituted
mid-chain is re-checked against its own requirements immediately.
A selected non-default variant or revision that contributes nothing
at all — no delta file, and (for a variant) no ``.overlay``/
``_defconfig`` either — is refused (lang-variant / lang-rev,
naming every filename that was looked for). The declared default of
an axis is exempt: the base content file already is what the
default means, so it needs no fragment of its own.