Interface profiles
Declare an interface's signals and what must be true of them, and check enforces that reading wherever the interface appears.
A bus has a fixed structure. CAN is a CANH/CANL pair with a termination resistor across it, driven by a transceiver with TXD and RXD. I2C is SCL and SDA, both pulled up.
An interface profile writes that structure down as YAML, and the engine compiles it into check rules.
Agni ships profiles for SPI-NOR, eMMC, SD, CAN, LIN, A2B, PCIe, SGMII and MDIO. Your own interfaces, and your
own reading of a standard one, go in a directory you hand to --profile-path. No Go.
Write a profile ¶
I2C is not one of the built-ins, and it makes a short first example. Two signals, both needing a pull-up:
# profiles/i2c.yaml
name: I2C
signals:
- {name: SCL, suffix: SCL, pullup: true, anchor: true}
- {name: SDA, suffix: SDA, pullup: true}
requirements:
- {type: signal-missing}
- {type: missing-pullup}
- {type: signal-dangling}
Point check at the directory holding it. --tag profile=I2C narrows the run to this
profile’s rules:
$ agni check cmd/agni/testdata/conformance/showcase.fires.kicad_pro --profile-path ./profiles/ --tag profile=I2C
findings by rule:
profile-overlay/i2c-missing-pullup 1
first 1:
[warning] profile-overlay/i2c-missing-pullup: /SCL (I2C signal net /SCL needs a pull-up but reaches no rail)
1 finding(s) total
5 subject(s) considered by 3 rule(s) (--verdicts for the detail)
Three rules came out of that declaration, one per requirement, and the last line shows that all three ran rather than one. On the variant of the same board with the pull-ups fitted, they run and report nothing:
$ agni check cmd/agni/testdata/conformance/showcase.passes.kicad_pro --profile-path ./profiles/ --tag profile=I2C
no findings (3 rule(s) run)
5 subject(s) considered by 3 rule(s) (--verdicts for the detail)
Rules from --profile-path are namespaced profile-overlay/, so an overlay rule is never
mistaken for a built-in one. A request can send a profile in AnalysisConfig.profile_uris. That
profile is namespaced request-profiles/, so a client choosing rules by name asks for
request-profiles/<name>-missing-pullup. Asking for profile/<name>-missing-pullup instead runs
nothing and reports no error today (#905).
flowchart LR
subgraph d["i2c.yaml"]
S["signals<br/>SCL: anchor, pullup<br/>SDA: pullup"]
R["requirements<br/>signal-missing<br/>missing-pullup<br/>signal-dangling"]
end
R --> C["compile,<br/>once at load"]
C --> R1["profile-overlay/<br/>i2c-signal-missing"]
C --> R2["profile-overlay/<br/>i2c-missing-pullup"]
C --> R3["profile-overlay/<br/>i2c-signal-dangling"]
R1 --> D["each rule binds<br/>its signals to nets<br/>by net name"]
R2 --> D
R3 --> D
S -.->|"the matchers"| D
The requirements decide how many rules there are. The signals decide what each one looks at.
The shape of the file ¶
| key | what it is |
|---|---|
name |
the profile’s name. It appears in rule names, in every finding’s message, and as the profile tag. |
signals |
the interface’s roles, each with a name (the role, e.g. SCL) and one matcher. |
requirements |
the profile’s checks, each naming a registered type. |
host |
optional. Binds the interface to the component that declares it. |
instance |
optional. anchor_stem judges each bus of the interface on its own, as described in One bus at a time. |
excludes |
optional, and only with instance. Signals whose net on a bus says that bus is some other interface. |
host takes {attr: <key>, value: <val>} to match a component attribute in the design
(interface=CAN), or {class: <device-class>} to match what a part’s datasheet says it is.
Either or both, and a component matching either one is a host. The class form needs a seeded
parameter set, so without --params it finds nothing rather than guessing.
The four matcher forms ¶
A signal is found by net name, and declares exactly one of these:
| form | example | when to use it |
|---|---|---|
suffix |
suffix: _CANH |
the readable default. The role is the tail of the net name. |
prefix + suffix |
prefix: PCIE_, suffix: _TXP |
the tail is shared with a foreign bus, and a bus prefix tells them apart. |
glob |
glob: ETH_SW*_A_H |
the identity is in the head of the name and the tail is not distinctive. |
regex |
regex: ^ETH_SW\d+_P\d+_.*_H$ |
multi-instance naming a glob cannot express. RE2, not auto-anchored, so anchor it yourself. |
A glob follows the same SQLite GLOB rules as str.glob in a query: * and ? as usual, and
[...] a character class, so a literal [ in a net name is written [[] (see
Querying).
Prefix and suffix are conjunctive and count as one form together. Declaring two forms on one signal is an error, because a signal that can be found two ways has no single convention to enforce.
Requirements ¶
| type | what it checks | params |
|---|---|---|
signal-missing |
every declared signal appears on a bus that is in use | |
host-incomplete |
a declared host is wired to all of the interface’s signals | |
missing-pullup |
a signal marked pullup: true reaches a rail |
|
signal-dangling |
a signal net has at least two connections | |
termination |
a terminating device bridges the bus pair | high, low |
esd |
a signal leaving through a connector has an ESD clamp |
Only termination takes params. They name the two bridged net-name suffixes. The
shipped CAN profile is the worked example:
# stdlib/profiles/builtins/can.yaml
name: CAN
host: {attr: interface, value: CAN}
signals:
- {name: CANH, suffix: _CANH, anchor: true}
- {name: CANL, suffix: _CANL}
- {name: TXD, suffix: _TXD}
- {name: RXD, suffix: _RXD}
requirements:
- {type: signal-missing}
- {type: host-incomplete}
- {type: termination, params: {high: _CANH, low: _CANL}}
- {type: signal-dangling}
- {type: esd}
Those files are the built-ins themselves, not copies of them, so they are safe to read as examples.
A profile with no host, and signals that differ from each other ¶
Suppose you check exported boards and nobody is going to annotate them for you. Then copy the
shipped MDIO profile. It declares no host: at all.
name: MDIO
signals:
- {name: MDIO, regex: '(?i)(^|[^A-Z])MDIO([^A-Z]|$)', pullup: true, anchor: true}
- {name: MDC, regex: '(?i)(^|[^A-Z])MDC([^A-Z]|$)'}
requirements:
- {type: signal-missing}
- {type: missing-pullup}
- {type: signal-dangling}
missing-pullup and signal-dangling gate on the in-use test below, not on a host part. That test
asks for two distinct signals matching by name. So this profile evaluates on any board whose nets
are named conventionally, with nothing asked of the design.
MDIO carries pullup and MDC does not, because they
are not the same kind of line. MDIO is bidirectional and open-drain, undriven between frames and
through every turnaround cycle, so a resistor holds it high and IEEE 802.3 clause 22 calls
for one. MDC is a clock sourced by the station side, push-pull, driven whenever it matters. Requiring
a pull-up on it would report a failure on every correctly-built board that omits one.
The same holds beyond Ethernet. pullup belongs on the signals that
float when nobody is driving, not on every signal of a bus that has a pull-up somewhere. If you mark
a whole bus, the profile starts producing failures, and a reviewer has to learn to ignore them. A
checklist whose failures are routinely ignored is worse than no checklist.
The pattern also shows why a signal’s match is bounded. MDCLK opens with the letters MDC and is
an unrelated clock, so both patterns require a non-letter on each side of the match. RE2 has no
lookaround, so the boundary is written as explicit character classes.
Why MDIO is a profile and not a wider built-in ¶
The engine had one pull-up rule, and it answered one bus family by name. Its pattern matched SDA and SCL at a token boundary, and nothing else looked at a management-interface name. Take a board with no pull-up on either line of its PHY bus. That rule reported nothing for it, though that failure is the rule’s entire subject (agni issue 516). MDIO asks the same electrical question as I2C. Both lines idle high only because a resistor returns them there, and both sit undriven between transactions.
If the built-in rule’s pattern were widened as well, two rules would report the same net, so the coverage is in this profile as data instead. That leaves one gap, the unresolved half of issue 516. The built-in I2C rule proves a pass with the resistor and rail it walked through, while a profile requirement’s finding names only the net. A reviewer asking “show me the pull-up” gets a path on I2C and a bare net name on MDIO.
The anchor, and when a profile decides it is looking at your board ¶
A profile that finds none of its signals has to report nothing. A board with no CAN on it is not a board with a broken CAN bus. Two gates decide this, and both have to clear before the completeness check runs.
The in-use gate wants at least two distinct signals of the profile to appear as nets. One
match is not evidence, because a real design has many _CS nets belonging to many things.
You mark one signal anchor: true, and that signal is the anchor. It is always present when
the interface is. Completeness is reported against it, so a finding reads CAN interface (anchored at net CAN_CANH) is missing required signal RXD rather than naming a net that is not
there. At most one signal may be the anchor, and a profile that declares none generates no
completeness rule at all.
flowchart TB
A["every signal matched<br/>against every net name"] --> B{"two or more distinct<br/>signals matched?"}
B -->|no| S["silent: no interface<br/>on this board"]
B -->|yes| C{"an anchor declared,<br/>and its net present?"}
C -->|no| E["pull-up and dangling<br/>rules evaluate, but no<br/>completeness finding"]
C -->|yes| F["those, plus missing<br/>signals reported<br/>against the anchor net"]
One bus at a time ¶
Without an instance key a profile treats every matching net on the design as one interface. That
is fine when a board carries a bus once. It goes wrong when a board carries two buses that share
suffixes, because each lends the other its signals. An SD card slot next to an eMMC is the usual
case on a SoC board, and an unused M.2 SMBus clock ending _CLK used to read as the eMMC’s clock.
Under instance: anchor_stem, the anchor’s matcher finds nets, and each of those nets founds its
own bus. The bus is named by the
anchor’s net name less the anchor’s suffix, so SD2_CMD founds bus SD2. A signal belongs to the
bus when its net is named from that stem. For a suffix signal that means exactly the stem and the
suffix (SD2_CLK). For a glob or regex signal it means the net matches and starts with the stem
and the anchor suffix’s first character, so bus SD never takes SD2_DATA0. Then each of these
runs per bus: the in-use gate, completeness, the pull-up and dangling checks, and the coverage panel.
A missing signal is reported against that bus’s anchor.
excludes lists signals whose presence on a bus says it is a different interface. The shipped eMMC
profile excludes a card detect line, because a removable SD card shares eMMC’s CMD, CLK and DAT
names but has four data lines and no reset. Judging it as an eMMC would report a list of missing
signals that an SD card never has:
# stdlib/profiles/builtins/emmc.yaml
instance: anchor_stem
signals:
- {name: CMD, suffix: _CMD, pullup: true, anchor: true}
- {name: DAT0, regex: '_DATA?0$'}
# ...
excludes:
- {name: CD, regex: '_(CD|CD_B|CD_N|CDn|nCD|DET)$'}
A bus carrying an excluded signal is not in use, so nothing on it is required or judged, and a
review item bound to the profile reads it as absent. The shipped SD profile is the mirror image. It
excludes an eighth data line and a reset, since those say a bus is an eMMC. So the two profiles
split one naming convention between them, and each judges its own bus. An instance key needs an
anchor declared by suffix, since the stem comes from taking that suffix off.
Every requirement runs per bus under an instance key. termination asks each bus’s own high net to
reach that bus’s low net, so one bus’s terminator no longer clears another, and esd judges an
exposed line only when it is on a bus in use. A review item bound to the profile still reads every
bus in use together, as DECISIONS.md records.
Re-bind a built-in instead of re-writing it ¶
When the engine already reads your interface and only the net naming differs, a naming map re-binds the roles and leaves the structure alone:
# profiles/can.yaml
override: CAN
suffixes:
CANH: _CAN_P
CANL: _CAN_N
Unmapped roles keep the built-in’s suffix. Requirements, host binding and anchor come across unchanged. A profile carrying a built-in’s name replaces that built-in’s rules rather than running beside it, and the CLI says so on stderr:
note: profile-overlay supersedes 5 rule(s): profile/can-signal-missing, profile/can-host-incomplete, profile/can-termination-missing, profile/can-signal-dangling, profile/can-esd-missing
If two readings of one interface ran together, they would invent failures rather than merely duplicate them, since the built-in still anchors on its own naming and reports each re-bound role as missing. Extending and embedding the engine covers the mechanism.
Where the flag reaches ¶
--profile-path takes a directory of these YAML files, and three surfaces accept it:
agni check, agni review, and agni serve, where the composed profiles reach the web check
panel too. The interface-coverage matrix in the viewer is the exception. It projects the
built-in profiles only, so an overlay profile’s findings appear while its coverage row does not.
What the tooling tells you when the declaration is wrong ¶
A profile that cannot do what it says is refused at load, with the error naming what was available.
The four load-time refusals, verbatim
A key missing from the file’s schema. The schema is the ProfileDef message in
protos/agni/v1/checks/ruledef.proto, so a misspelled key fails with its line rather than leaving
the signal without the requirement it was meant to carry:
error: profiles: i2c.yaml: profile: line 3: unknown key "pull_up" in "signals[0]" (known: anchor, glob, name, prefix, pullup, regex, suffix)
An unregistered requirement type:
error: profiles: i2c.yaml: profile "I2C": unknown requirement type "pullup" (known: esd, host-incomplete, missing-pullup, signal-dangling, signal-missing, termination)
A requirement missing a param it cannot work without:
error: profiles: mybus.yaml: profile "MYBUS": requirement "termination": needs the low param, naming the two bridged net-name suffixes (e.g. high: _CANH, low: _CANL); got map[high:_H]
A signal declaring more than one matcher form:
error: profiles: mybus.yaml: profile "MYBUS": signal "H" declares 2 matcher forms: set exactly one of suffix/prefix, glob, or regex
Load cannot catch a matcher that is merely too loose, because whether _H names one
role or half the board depends on the board. check judges that against the design in front of
it. A profile whose signals claim an implausible share of the nets:
warning: profile "ETH100": signal "AP" (regex "_H") matches 8 of 32 nets (25%) — the matcher looks too broad to name one role; narrow it with a prefix, a glob, or an anchored regex
And a profile that cannot tell two of its own roles apart:
warning: profile "MYBUS": signals "DATA" and "CLK" both match net "CAN_CANH" — the profile cannot tell these two roles apart, so whichever rule runs first claims the net
Both describe the profile you wrote, not the board, so neither is a finding. They go to stderr,
they never change the exit code, and they are left out of --format json.
What a profile cannot do ¶
A suffix-named profile only fires on designs that follow that naming. Rename the nets and the
interface goes invisible, silently. host binding exists to stop that, because a component that
declares interface=CAN anchors the check no matter what the nets are called. When the
naming is merely different rather than absent, a naming map is the cheaper fix.
Where to go next ¶
- Checks and reports shows that profile findings read like any other,
and
--fail-oncan gate on them. - Extending and embedding the engine covers shipping profiles alongside private readers and rules in your own module.
- CLI reference documents the
--profile-pathflag. - How a rule gets written explains where a profile sits among the four ways to author a rule, and why the shipped profiles use this same YAML format rather than a layer over something else.