Silent-path census
Measured on branch chore/silent-path-census, whose merge base is
d503501d323fe25fb9dc2dd87481806e37beeb67 (origin/main); host
aarch64-apple-darwin, rustc 1.97.1.
Every number here is from a full re-measurement after counter (b) was corrected (see below). Counters (a), (c) and (b1) reproduced the first run exactly, entry for entry, which is what establishes that the (b2) figures are a correction rather than run-to-run drift.
This document is a measurement. Nothing is fixed, escalated, or gated here. It exists to scope three follow-up repairs that could not be sized without data.
Correction (counter b)
The first version of this document reported
nvicon 22 labs andscbon 3 as live stubs, and called them the only entries that were "not intentional". That was wrong. Both are replaced by real models before a single instruction executes, and the conclusion was published without being checked.The cause was measuring at the wrong moment: counter (b) fired inside
bus/from_config.rs, which is construction time but not the end of construction.system::cortex_m::configure_cortex_mruns afterwards on every ARM path —cli/commands/test.rs,wasm/lib.rs,run.rs,machine.rs,debug_probe.rs,dap/adapter.rs,python/lib.rs,system/node.rs— finds the entry matchingname == "nvic" || base == 0xE000_E100and swaps in a realNvic, does the same forScbat0xE000_ED00andDwtat0xE000_1000, and rewrites their names while doing it. A factory-time counter cannot see any of that. It was counting what the factory emitted, not what the machine ran.Counter (b) is now two clearly separated numbers: (b1) how often the factory fallthrough was taken, and (b2) what is still a
StubPeripheralon a fully assembled machine. (b2) is the actionable one; (b1) is retained because "how often did the_other =>arm fire" is a legitimate, separate statistic about the factory. They are not interchangeable and must never be read off one another.A guard that proves the real
Nvic/Scbare installed is #877. The corrected mechanism also has a gate in this branch:crates/core/tests/census_probe.rs::nvic_and_scb_trip_the_factory_counter_but_are_not_live_stubs, which runs the realfrom_configon a committed chip file, asserts NVIC/SCB are stubs straight out of the factory, then asserts they are not afterconfigure_cortex_m.Generalising the lesson: a counter placed inside a construction step measures that step, not construction. Before trusting one, find every pass that mutates the same data afterwards. The audit of those passes is in Other post-factory mutation passes.
Counters (a) and (c) instrument runtime access, not construction, so they should be unaffected. That was checked, not assumed — see (a) and (c) are unchanged.
What was counted, and how it is gated
All counters live behind the silent-census Cargo feature
(crates/core/src/census.rs). It is off by default, is not implied by any other
feature, and even when compiled in it writes nothing unless LABWIRED_CENSUS_OUT
names an output path at runtime. Both the compile-time feature and the env var are
required, so it cannot be turned on by accident.
cargo build -p labwired-cli --features silent-census
LABWIRED_CENSUS_OUT=census.json ./target/debug/labwired test --script <lab>.yaml ...
| # | Path | Instrumented at | Sites |
|---|---|---|---|
| a | Dropped Cortex-M memory errors | cpu/cortex_m.rs - every let _ = bus.write_* and every discarded bus.read_*, wrapped in census_bus! |
64 (25 write, 39 read) |
| b1 | Stub-peripheral factory fallthrough (construction statistic) | bus/from_config.rs - the _other => arm ending the factory chain |
1 |
| b2 | Live stub peripherals, post-construction (the actionable number) | Machine::new - a sweep of bus.peripherals after the finished bus is handed over |
1 |
| c | Undecoded register access | peripherals/** - catch-all _ => {} / _ => 0 decode arms, via census_reg! |
312 |
| c' | Undecoded register access, declarative models | peripherals/declarative.rs - the reg_index_at miss fallthrough |
2 |
How (b2) decides that something is a stub
Not by name. PeripheralEntry::name is rewritten by the very replacement passes
that make (b1) misleading, so a name records what the manifest asked for, never
what the machine ended up holding.
The sweep uses the type system: Peripheral::as_any() followed by
dyn Any::is::<StubPeripheral>(). That is a TypeId comparison against the
concrete type — it cannot false-positive on some other model, and it cannot
false-negative on a real stub. Name and base address are still reported,
because they are how a human finds the entry again; they are output, not the
predicate.
The one production-type change this needs is a Peripheral::as_any override on
StubPeripheral, and it is itself #[cfg(feature = "silent-census")]: with the
feature off the type is byte-for-byte what it was. With the feature on it returns
Some(self) where it previously returned None, and every existing consumer of
Peripheral::as_any in the tree immediately downcast_refs to a concrete model
type that a stub is not — so Some(stub) and None are indistinguishable to all
of them.
Machine::new is the sweep point because it is the single choke that every
runner (CLI lab runner, multi-node environment runner, wasm, DAP, python,
system::node) passes through holding a finished bus. It already walks
bus.peripherals for exactly this kind of TypeId lookup. The census publishes
machines_swept alongside the table so multi-node runs and any double
construction are visible rather than silently inflating the count.
Why c' exists, and what the audit's arm count really contains
The audit sized (c) as "~204 _ => {} and ~173 _ => 0 arms". A grep at this SHA
finds 201 and 176 (377 total). Classifying each by the subject of its enclosing
match shows they are not all register decodes:
| Arm population | Count |
|---|---|
Total _ => {} / _ => 0, arms |
377 |
... in #[cfg(test)] / mod tests code |
19 |
... matching on a register offset (offset, reg, reg_off, word_off, ...) - instrumented |
312 |
... matching on something else entirely (cmd, dest, src, self.state, self.pointer, upper.as_str(), ...) - not instrumented, not a register decode |
46 |
Instrumenting the last group would have produced numbers that look like register gaps but are not. They are excluded deliberately.
Separately, the _ => grep structurally cannot see the declarative/SVD-driven
models (GenericPeripheral), whose decode is
if let Some(idx) = self.reg_index_at(offset) { ... } with a bare Ok(0) / Ok(())
fallthrough - the same silent path in a different shape. 138 of the 1,186 peripheral
instances across the runnable corpus are declarative, so omitting them would have made
a near-zero (c) result misleading. They are counted and reported separately as
shape: declarative_miss.
Raw counts are not one-per-register unless the model stored a word
Peripheral::read/write are byte-granular, and the trait default splits
read_u32/write_u32 into four byte accesses. Several models also
read-modify-write each byte, so one 32-bit store can cost 4 write hits and 4
read hits. RCC does not: an aligned write_u32 is one word store, because a
byte split re-latches write-1-to-clear bits (RCC_CSR.RMVF). That path records
one write and no read, pinned by
crates/core/tests/census_probe.rs::aligned_rcc_u32_write_is_one_census_hit.
Divide a raw count by four only when the model still takes the byte path.
Coverage
| Count | |
|---|---|
Test scripts discovered under examples/ (any YAML with an assertions: block) |
97 |
| Ran | 68 |
| Skipped - firmware artifact absent | 27 |
| Ran but produced no census file | 2 |
Of the 68 runs: 25 clean (counters a, b2 and c all zero), 43 hot.
The 68 runs built 70 machines — ci-multiarch/two-riscv-test and
ci/two-node-inputs-env are two-node environments and are swept twice, once per
node. Every other lab reports machines_swept: 1.
Assertion outcome 58 pass / 10 fail; the failures are pre-existing at this SHA and are not caused by the census - see Behavioural neutrality, which proves byte-identical output on failing labs as well as passing ones.
Under the old, factory-time (b) the split was 19 clean / 49 hot. Six labs (
feather-f405x2,nucleo-f401rex2,nucleo-f767zix2) were counted as hot solely because their manifest declarestype: "nvic". They have no live stub, no dropped memory error and no undecoded register access, and are now where they belong: clean.
On the brief's denominator
The brief describes the corpus as "89 projects and 82 committed .elf files" in
examples/. The 89 directories are right; the 82 ELFs are repo-wide. Only 8
committed ELFs are under examples/ - 67 are under tests/fixtures/. The runnable
unit is a test script, not a project: the 89 directories yield 97 scripts, and most
firmware comes from cross-compiling workspace members (33 of 38 ARM build units built
cleanly here; the 5 failures are 3 nested workspaces that do not build on this
toolchain and 2 crate names that are [[bin]] targets inside other packages, both of
which were then built via their real package).
Aggregate: counter (a) - dropped Cortex-M memory errors
2 hits, 2 distinct (pc, addr, kind), in 1 of 68 runs.
| count | pc | addr | kind |
|---|---|---|---|
| 1 | 0x00000000 |
0x00000001 |
read |
| 1 | 0x00000000 |
0x00000004 |
read |
Both hits are in examples/ci/dummy-memory-violation.yaml - a fixture whose purpose
is to provoke a memory violation. No shipped lab drops a single Cortex-M memory error.
Cross-checked against the independent, always-on fidelity::unmapped_mmio log, which
records the same bus rejections one layer lower (per byte, at the point the error is
created rather than dropped):
| lab | fidelity unmapped_mmio (bytes rejected) |
census (a) (errors dropped) |
|---|---|---|
ci/dummy-memory-violation |
7 | 2 |
nucleo-l073rz/io-smoke |
1 | 0 |
pico2/io-smoke |
1 | 0 |
pico2/uart-smoke |
1 | 0 |
| total | 10 | 2 |
The gap is the point: 8 of the 10 rejections were propagated, not dropped - they
hit the instruction-fetch path (bus.read_u16(fetch_pc)?, one of the 9 places
cortex_m.rs does use ?), which already faults correctly. Counter (a) is measuring
the drop sites specifically, and they are cold.
Aggregate: counter (b2) - live stub peripherals (the actionable number)
147 live stubs, 34 distinct (name, base), in 41 of 68 runs, across 70 machines.
This is what is still a StubPeripheral when the machine actually runs. Every one
of the 147 was traced back to the manifest entry that produced it, and every one
is a stub the manifest or the factory deliberately asked for:
| count | asked for by | reading |
|---|---|---|
| 120 | manifest type: "stub" |
Declared. The manifest literally says type: stub; the factory is doing what it was asked. |
| 18 | manifest type: "nrf54l_*_stub" (7 strings) |
Declared by naming convention. dppic x4, wdt x4, ficr/regulators/rramc/tampc/uicr x2 each. Intentional, but they still reach the factory's _other => arm by failing to match, so the factory cannot tell them from a typo. |
| 7 | manifest type: "icache" |
Declared, and explicitly handled. peripherals/generic_factory.rs maps "icache" \| "dcache" to a read-as-zero stub on purpose: Zephyr's SoC init writes ICACHE_CR.EN and never polls a completion flag, and the simulator has flat memory, so there is no cache behaviour to model. |
| 2 | manifest type: "syscfg" |
Declared, and explicitly handled. Same file: a read-0 stub with CCCSR @ 0x20 seeded to 0x0000_0100 so the H7 HAL's I/O-compensation-cell READY poll exits. |
| 0 | anything else | Nothing unaccounted for. |
(b2) is benign on the current corpus. There is no live stub that anybody asked
for by accident: no unmatched manifest type: survives to run time, and in
particular nvic and scb — the two entries the first version of this document
flagged — do not appear here at all, because they are not stubs by the time the
machine exists.
Aggregate: counter (b1) - factory fallthrough (construction statistic)
163 instantiations, 10 distinct type: strings, in 47 of 68 runs.
How often bus/from_config.rs's _other => arm was taken. Useful for scoping a
change to the factory, and for nothing else — it is not a statement about the
running machine.
| count | type: string |
still a stub at run time? |
|---|---|---|
| 120 | stub |
yes - all 120 |
| 22 | nvic |
no - all 22 replaced by a real Nvic in configure_cortex_m |
| 4 | nrf54l_dppic_stub |
yes |
| 4 | nrf54l_wdt_stub |
yes |
| 3 | scb |
no - all 3 replaced by a real Scb in configure_cortex_m |
| 2 | nrf54l_ficr_stub |
yes |
| 2 | nrf54l_regulators_stub |
yes |
| 2 | nrf54l_rramc_stub |
yes |
| 2 | nrf54l_tampc_stub |
yes |
| 2 | nrf54l_uicr_stub |
yes |
Reconciling (b1) and (b2)
The two numbers differ by 16, and the difference is exactly accounted for:
(b1) factory fallthrough 163
- nvic (22) replaced by a real Nvic before run time -22
- scb ( 3) replaced by a real Scb before run time -3
+ icache( 7) an explicit generic_factory arm, so it
never reaches `_other =>` and is invisible
to (b1) - but it IS a live stub + 7
+ syscfg( 2) same + 2
----
(b2) live stubs 147
That the difference runs in both directions is the point. (b1) over-reports by counting entries that were replaced afterwards, and also under-reports by missing stubs the factory installs from a named arm rather than by falling off the end. Neither number is a proxy for the other.
Delta against the first published table
| first version | corrected | change | |
|---|---|---|---|
| headline count | 163 "live stubs" | 147 live stubs (b2); 163 factory fallthroughs (b1) | the 163 was never a live-stub count |
| entries called "not intentional" | nvic x22, scb x3 |
none | both are real models at run time |
| labs with a live stub | 47 | 41 | 6 labs were hot only because of nvic |
| clean labs | 19 | 25 | same 6 labs |
| stub sources not previously visible | - | icache x7, syscfg x2 |
explicit factory arms, missed by a fallthrough-only counter |
| (a) dropped memory errors | 2 | 2 | unchanged |
| (c) undecoded register access | 11 raw / 5 distinct | 11 raw / 5 distinct | unchanged |
Scoping consequence. The follow-up repair is unchanged in shape but smaller in substance, and it is a factory hygiene change rather than a fidelity gap:
- Nothing on the current corpus runs with an accidentally stubbed peripheral, so there is no chip-model repair to schedule from (b2). No datasheet look is owed.
- Turning
_other =>into a hard error still breaks 47 of 68 labs, because 138 of the 163 fallthroughs are intentional stubbing that is inferred from falling off the end of the match rather than declared. The fix remains a migration: make the factory explicitly matchtype: stuband the*_stubconvention, then hard-error the remainder — which, after that migration, is the empty set on this corpus. nvicandscbwant a tidy-up for a different reason than the first version claimed: they are not a fidelity problem, they are dead manifest entries. The factory builds a stub thatconfigure_cortex_mimmediately throws away. Removing them from the chip yamls, or giving the factory a real arm for them, would make the fallthrough histogram mean what a reader assumes it means.
Other post-factory mutation passes
configure_cortex_m is not the only thing that touches peripherals after the
factory runs. Every pass found in the tree, and whether it can produce the same
false positive:
| pass | what it does | affected? |
|---|---|---|
system/cortex_m.rs::configure_cortex_m |
replaces the entries at 0xE000_ED00 (SCB), 0xE000_E100 (NVIC), 0xE000_1000 (DWT) with real models, rewriting name, base, size and irq |
yes - this is the one that caused the wrong number. Runs before Machine::new, so (b2) sees the result. |
cli/commands/esp32_boot_state.rs::install_esp32c3_fast_boot |
replace_or_add_peripheral for systimer, rmt, usb_serial_jtag, plus 6 add_peripheral installs |
yes, same mechanism - a chip-yaml entry can be replaced by a behavioural twin. Runs at test.rs:1352, before the machine is built at test.rs:1467, so (b2) already covers it. |
system/xtensa/{mod,esp32,esp32s3}.rs |
~40 add_peripheral calls building the ESP32/S3 bus |
no - additive, and these buses are assembled before Machine::new. |
boot/esp32c3_rom::inject_rom_regions |
injects ROM windows, then build_rom_boot_machine |
no - additive, and it constructs the machine itself, so the sweep still runs. |
system/riscv.rs::configure_riscv |
14 lines; mandates no peripherals at all | no. There is no RISC-V equivalent of the Cortex-M replacement pass. |
bus/tick.rs (3 sites) and peripherals/esp32s3/gdma.rs (2 sites) |
temporarily swap entry.dev out for a placeholder while lending a model to a bus-aware tick, then swap it back |
no - transient, mid-run, and restored. Not construction. |
bus/construct.rs::replace_or_add_peripheral |
the generic replacement primitive the passes above use | covered by whoever calls it, all of which run before Machine::new. |
The sweep point is therefore after every construction-time pass in the tree today.
It is not immune by design: a pass that replaced a peripheral after Machine::new
would be missed. machines_swept and this table are what a future reader should
re-check before trusting the number again.
(a) and (c) are unchanged
Counters (a) and (c) instrument runtime access, so they should be unaffected by a construction-time error. That was verified rather than assumed: the corpus was re-run in full and both counters reproduced exactly, entry for entry —
- (a): 2 hits, the same 2 distinct
(pc, addr, kind)triples, in the same single lab, and the samefidelity::unmapped_mmiocross-check (7 + 1 + 1 + 1 = 10 bytes rejected against 2 errors dropped). - (c): 11 raw hits, the same 5 distinct
(peripheral, offset, kind)triples, in the same 5 labs, withdeclarative_missstill at zero. - (b1) also reproduced exactly (163 / 10 distinct / 47 labs), which is what establishes that this re-run and the original measured the same corpus under the same conditions — so the (b2) delta is the correction and not run-to-run drift.
Aggregate: counter (c) - undecoded register access
11 raw hits, 5 distinct (peripheral, offset, kind), in 5 of 68 runs.
Applying the multiplier per entry rather than in bulk: the two 4-hit entries are one 32-bit write each; the three 1-hit entries are single byte writes. So the corpus performs 5 register-level undecoded accesses in total, all writes.
Fewer than 20 distinct pairs exist, so this is the complete list, not a top-20:
| count | peripheral | offset | kind | shape |
|---|---|---|---|---|
| 4 | fdcan:Fdcan |
0x0010 |
write | match_arm |
| 4 | iwdg:Iwdg |
0x0008 |
write | match_arm |
| 1 | nrf54l.twim:Nrf54lTwim |
0x0508 |
write | match_arm |
| 1 | nrf54l.twim:Nrf54lTwim |
0x050c |
write | match_arm |
| 1 | nrf52.twim:Nrf52Twim |
0x0510 |
write | match_arm |
Shape split: match_arm 11 hits, declarative_miss 0 hits.
The declarative path recorded zero hits across the entire corpus. That is a
measured zero from live instrumentation, not an untested assumption - the counter is
proven capable of firing by census_probe.rs.
Scoping consequence. Three findings are twim 0x508/0x50c/0x510 (nRF52 and
nRF54L), each a single byte write; one is fdcan 0x010 and one is iwdg 0x008,
each a single 32-bit write. The WB55 precedent in rcc.rs is real, but
nothing in the current shipped corpus reproduces it: no RCC/clock-enable offset is
undecoded on any lab that runs. Escalating undecoded writes to a fault would break 5
labs, each for one register.
Per-lab table
a = dropped Cortex-M memory errors, b1 = factory stub fallthroughs (construction
statistic), b2 = live stubs on the assembled machine (the actionable column),
c = undecoded register hits (raw, pre-divide). Sorted hot first.
Where b1 and b2 differ the difference is always nvic/scb (counted by b1,
replaced before run time) or icache/syscfg (a live stub b1 cannot see).
| script | status | steps | a | b1 | b2 | c | detail |
|---|---|---|---|---|---|---|---|
examples/stm32f411ceu6-blackpill/io-smoke.yaml |
pass | 5,000,000 | 0 | 13 | 13 | 4 | live stub: crc@0x40023000, dbg@0xe0042000, dma1@0x40026000, dma2@0x40026400, i2s2ext@0x40003400, i2s3ext@0x40004000, otg_fs_device@0x50000800, otg_fs_global@0x50000000, otg_fs_host@0x50000400, otg_fs_pwrclk@0x50000e00, sdio@0x40012c00, syscfg@0x40013800, wwdg@0x40002c00; reg: iwdg:Iwdg@0x0008 writex4 |
examples/stm32f401cdu6-blackpill/i2c-smoke.yaml |
fail | 4,096 | 0 | 15 | 15 | 0 | live stub: crc@0x40023000, dbg@0xe0042000, dma1@0x40026000, dma2@0x40026400, i2s2ext@0x40003400, i2s3ext@0x40004000, iwdg@0x40003000, otg_fs_device@0x50000800, otg_fs_global@0x50000000, otg_fs_host@0x50000400, otg_fs_pwrclk@0x50000e00, rtc@0x40002800, sdio@0x40012c00, syscfg@0x40013800, wwdg@0x40002c00 |
examples/stm32f401cdu6-blackpill/io-smoke.yaml |
pass | 64 | 0 | 15 | 15 | 0 | live stub: crc@0x40023000, dbg@0xe0042000, dma1@0x40026000, dma2@0x40026400, i2s2ext@0x40003400, i2s3ext@0x40004000, iwdg@0x40003000, otg_fs_device@0x50000800, otg_fs_global@0x50000000, otg_fs_host@0x50000400, otg_fs_pwrclk@0x50000e00, rtc@0x40002800, sdio@0x40012c00, syscfg@0x40013800, wwdg@0x40002c00 |
examples/stm32f401cdu6-blackpill/trace-smoke.yaml |
pass | 64 | 0 | 15 | 15 | 0 | live stub: crc@0x40023000, dbg@0xe0042000, dma1@0x40026000, dma2@0x40026400, i2s2ext@0x40003400, i2s3ext@0x40004000, iwdg@0x40003000, otg_fs_device@0x50000800, otg_fs_global@0x50000000, otg_fs_host@0x50000400, otg_fs_pwrclk@0x50000e00, rtc@0x40002800, sdio@0x40012c00, syscfg@0x40013800, wwdg@0x40002c00 |
examples/stm32f401cdu6/uart-smoke.yaml |
pass | 64 | 0 | 15 | 15 | 0 | live stub: crc@0x40023000, dbg@0xe0042000, dma1@0x40026000, dma2@0x40026400, i2s2ext@0x40003400, i2s3ext@0x40004000, iwdg@0x40003000, otg_fs_device@0x50000800, otg_fs_global@0x50000000, otg_fs_host@0x50000400, otg_fs_pwrclk@0x50000e00, rtc@0x40002800, sdio@0x40012c00, syscfg@0x40013800, wwdg@0x40002c00 |
examples/nrf54l15-smart-ring/io-smoke.yaml |
pass | 500,000 | 0 | 9 | 9 | 2 | live stub: dppic20@0x500c2000, dppic30@0x50102000, ficr@0x00ffc000, regulators@0x50120000, rramc@0x5004b000, tampc@0x500dc000, uicr@0x00ffd000, wdt30@0x50108000, wdt31@0x50109000; reg: nrf54l.twim:Nrf54lTwim@0x0508 writex1, nrf54l.twim:Nrf54lTwim@0x050c writex1 |
examples/nrf54l15-dk/io-smoke.yaml |
pass | 200,000 | 0 | 9 | 9 | 0 | live stub: dppic20@0x500c2000, dppic30@0x50102000, ficr@0x00ffc000, regulators@0x50120000, rramc@0x5004b000, tampc@0x500dc000, uicr@0x00ffd000, wdt30@0x50108000, wdt31@0x50109000 |
examples/h563-uds-ecu/uds-session-smoke.yaml |
pass | 2,000,000 | 0 | 1 | 1 | 2 | live stub: icache@0x40030400; reg: fdcan:Fdcan@0x0010 writex2 |
examples/h563-uds-ecu/uds-smoke.yaml |
pass | 2,000,000 | 0 | 1 | 1 | 2 | live stub: icache@0x40030400; reg: fdcan:Fdcan@0x0010 writex2 |
examples/nucleo-l073rz/io-smoke.yaml |
fail | 0 | 0 | 5 | 3 | 0 | live stub: lcd@0x40002400, syscfg@0x40010000, usb_fs@0x40005c00 |
examples/ads1115-adc-lab/io-smoke.yaml |
pass | 500,000 | 0 | 2 | 2 | 0 | live stub: bkp@0x40006c00, usb_dev@0x40005c00 |
examples/ads1115-adc-lab/stimuli-smoke.yaml |
pass | 2,000,000 | 0 | 2 | 2 | 0 | live stub: bkp@0x40006c00, usb_dev@0x40005c00 |
examples/adxl345-sensor-lab/io-smoke.yaml |
pass | 200,000 | 0 | 2 | 2 | 0 | live stub: bkp@0x40006c00, usb_dev@0x40005c00 |
examples/bme280-weather-lab/io-smoke.yaml |
pass | 200,000 | 0 | 2 | 2 | 0 | live stub: bkp@0x40006c00, usb_dev@0x40005c00 |
examples/ci/dummy-memory-violation.yaml |
pass | 0 | 2 | 0 | 0 | 0 | arm: pc=0x00000000 addr=0x00000001 readx1, pc=0x00000000 addr=0x00000004 readx1 |
examples/demo-blinky/io-smoke.yaml |
pass | 10,000 | 0 | 2 | 2 | 0 | live stub: bkp@0x40006c00, usb_dev@0x40005c00 |
examples/ds3231-rtc-lab/io-smoke.yaml |
pass | 500,000 | 0 | 2 | 2 | 0 | live stub: bkp@0x40006c00, usb_dev@0x40005c00 |
examples/ds3231-rtc-lab/stimuli-smoke.yaml |
pass | 2,000,000 | 0 | 2 | 2 | 0 | live stub: bkp@0x40006c00, usb_dev@0x40005c00 |
examples/f103-fidelity-bench/gpiobug-smoke.yaml |
fail | 200,000 | 0 | 2 | 2 | 0 | live stub: bkp@0x40006c00, usb_dev@0x40005c00 |
examples/f103-i2c-silicon/io-smoke.yaml |
pass | 50,000,000 | 0 | 2 | 2 | 0 | live stub: bkp@0x40006c00, usb_dev@0x40005c00 |
examples/ili9341-tft-lab/io-smoke.yaml |
pass | 20,000,000 | 0 | 2 | 2 | 0 | live stub: bkp@0x40006c00, usb_dev@0x40005c00 |
examples/ina219-power-lab/io-smoke.yaml |
pass | 500,000 | 0 | 2 | 2 | 0 | live stub: bkp@0x40006c00, usb_dev@0x40005c00 |
examples/ina219-power-lab/stimuli-smoke.yaml |
pass | 2,000,000 | 0 | 2 | 2 | 0 | live stub: bkp@0x40006c00, usb_dev@0x40005c00 |
examples/max31855-thermocouple-lab/io-smoke.yaml |
fail | 200,000 | 0 | 2 | 2 | 0 | live stub: bkp@0x40006c00, usb_dev@0x40005c00 |
examples/mpu6050-sensor-lab/io-smoke.yaml |
pass | 200,000 | 0 | 2 | 2 | 0 | live stub: bkp@0x40006c00, usb_dev@0x40005c00 |
examples/neo6m-gps-lab/io-smoke.yaml |
pass | 200,000 | 0 | 2 | 2 | 0 | live stub: bkp@0x40006c00, usb_dev@0x40005c00 |
examples/ntc-thermistor-lab/io-smoke.yaml |
fail | 200,000 | 0 | 2 | 2 | 0 | live stub: bkp@0x40006c00, usb_dev@0x40005c00 |
examples/pico2/io-smoke.yaml |
fail | 0 | 0 | 3 | 2 | 0 | live stub: powman@0x40100000, tbman@0x40160000 |
examples/pico2/uart-smoke.yaml |
fail | 0 | 0 | 3 | 2 | 0 | live stub: powman@0x40100000, tbman@0x40160000 |
examples/ssd1306-hello-lab/io-smoke.yaml |
pass | 200,000 | 0 | 2 | 2 | 0 | live stub: bkp@0x40006c00, usb_dev@0x40005c00 |
examples/vl53l1x-tof-lab/io-smoke.yaml |
pass | 200,000 | 0 | 2 | 2 | 0 | live stub: bkp@0x40006c00, usb_dev@0x40005c00 |
examples/ci/l476-bldc-stall.yaml |
pass | 1,265,348 | 0 | 3 | 1 | 0 | live stub: syscfg@0x40010000 |
examples/h735-telematics-lab/io-smoke.yaml |
pass | 840,000 | 0 | 1 | 1 | 0 | live stub: syscfg@0x58000400 |
examples/hil-displacement-showcase/io-smoke.yaml |
pass | 0 | 0 | 1 | 1 | 0 | live stub: icache@0x40030400 |
examples/hil-displacement-showcase/showcase-test.yaml |
pass | 0 | 0 | 1 | 1 | 0 | live stub: icache@0x40030400 |
examples/nokia5110-invaders-lab/io-smoke.yaml |
pass | 5,000,000 | 0 | 3 | 1 | 0 | live stub: syscfg@0x40010000 |
examples/nucleo-h563zi/fullchip-smoke.yaml |
pass | 2,000 | 0 | 1 | 1 | 0 | live stub: icache@0x40030400 |
examples/nucleo-h563zi/io-smoke.yaml |
pass | 5,000,000 | 0 | 1 | 1 | 0 | live stub: icache@0x40030400 |
examples/nucleo-h563zi/uart-smoke.yaml |
pass | 64 | 0 | 1 | 1 | 0 | live stub: icache@0x40030400 |
examples/rp2040-pio/asm-smoke.yaml |
pass | 10 | 0 | 2 | 1 | 0 | live stub: tbman@0x4006c000 |
examples/rp2040-pio/io-smoke.yaml |
fail | 10,000 | 0 | 2 | 1 | 0 | live stub: tbman@0x4006c000 |
examples/seeed-xiao-nrf52840-sense/uart-gpio-spi-smoke.yaml |
pass | 20,000 | 0 | 0 | 0 | 1 | reg: nrf52.twim:Nrf52Twim@0x0510 writex1 |
examples/stm32h735-smoke/io-smoke.yaml |
pass | 5,000,000 | 0 | 1 | 1 | 0 | live stub: syscfg@0x58000400 |
Clean labs (a, b2 and c all zero)
b1 is shown so the six labs that were previously listed as hot purely because
their chip yaml declares type: "nvic" are visible as exactly that.
| script | status | steps | b1 |
|---|---|---|---|
examples/ci-multiarch/two-riscv-test.yaml |
pass | 2,000 | 0 |
examples/ci/dummy-fail-uart.yaml |
fail | 10 | 0 |
examples/ci/dummy-max-cycles.yaml |
pass | 10 | 0 |
examples/ci/dummy-max-steps.yaml |
pass | 10 | 0 |
examples/ci/dummy-max-uart-bytes.yaml |
pass | 10,000 | 0 |
examples/ci/dummy-no-progress.yaml |
pass | 125 | 0 |
examples/ci/dummy-wall-time.yaml |
pass | 0 | 0 |
examples/ci/two-node-inputs-env.yaml |
pass | 10 | 0 |
examples/ci/uart-inject-echo.yaml |
pass | 20,000 | 0 |
examples/ci/uart-ok.yaml |
pass | 1,000 | 0 |
examples/esp32c3-blinky/test-blink.yaml |
pass | 800,000 | 0 |
examples/esp32c3-leo-airquality/test-fresh.yaml |
pass | 24,000,000 | 0 |
examples/esp32c3-leo-airquality/test-stuffy.yaml |
pass | 28,000,000 | 0 |
examples/esp32c3-leo-airquality/test.yaml |
pass | 28,000,000 | 0 |
examples/feather-f405/io-smoke.yaml |
pass | 64 | 1 |
examples/feather-f405/uart-smoke.yaml |
pass | 64 | 1 |
examples/kw41z-cow-activity/calm.yaml |
pass | 6,000,000 | 0 |
examples/kw41z-cow-activity/stimulus-shake.yaml |
pass | 6,000,000 | 0 |
examples/nrf52840-proximity-lab/proximity-smoke.yaml |
fail | 6,000,000 | 0 |
examples/nrf52840-secure-boot-lab/secure-boot-smoke.yaml |
pass | 30,000,000 | 0 |
examples/nucleo-f401re/io-smoke.yaml |
pass | 64 | 1 |
examples/nucleo-f401re/uart-smoke.yaml |
pass | 64 | 1 |
examples/nucleo-f767zi/io-smoke.yaml |
pass | 64 | 1 |
examples/nucleo-f767zi/uart-smoke.yaml |
pass | 64 | 1 |
examples/simctl-selftest/simctl-selftest.yaml |
pass | 7,210 | 0 |
Skipped - could not run
Every script that did not run gets a row. None is omitted.
| script | missing artifact | why |
|---|---|---|
examples/canmod-gps-sim/canmod-smoke.yaml |
./firmware/build/canmod_gps_sim.elf |
C/Makefile firmware (arm-none-eabi-gcc / riscv32-esp-elf-gcc); ELF not committed |
examples/ci/riscv-uart-ok.yaml |
../../target/riscv32i-unknown-none-elf/release/riscv-ci-fixture |
riscv32 rustup target not installed on this host |
examples/esp32-bay-occupancy/tests/test-debounce.yaml |
../firmware/.pio/build/adafruit_feather_esp32_v2/firmware.elf |
PlatformIO/Arduino output; needs pio run, not committed |
examples/esp32-bay-occupancy/tests/test-fault-and-display.yaml |
../firmware/.pio/build/adafruit_feather_esp32_v2/firmware.elf |
PlatformIO/Arduino output; needs pio run, not committed |
examples/esp32-bay-occupancy/tests/test-nonblocking.yaml |
../firmware/.pio/build/adafruit_feather_esp32_v2/firmware.elf |
PlatformIO/Arduino output; needs pio run, not committed |
examples/esp32-bay-occupancy/tests/test-occupancy-combinations.yaml |
../firmware/.pio/build/adafruit_feather_esp32_v2/firmware.elf |
PlatformIO/Arduino output; needs pio run, not committed |
examples/esp32-bay-occupancy/tests/test-thresholds-hysteresis.yaml |
../firmware/.pio/build/adafruit_feather_esp32_v2/firmware.elf |
PlatformIO/Arduino output; needs pio run, not committed |
examples/esp32c3-mlx90640-thermal/test-fault.yaml |
./firmware/thermal_fingerprint.elf |
C/Makefile firmware (arm-none-eabi-gcc / riscv32-esp-elf-gcc); ELF not committed |
examples/esp32c3-mlx90640-thermal/test-iolink-fault.yaml |
./firmware/thermal_fingerprint.elf |
C/Makefile firmware (arm-none-eabi-gcc / riscv32-esp-elf-gcc); ELF not committed |
examples/esp32c3-mlx90640-thermal/test-iolink.yaml |
./firmware/thermal_fingerprint.elf |
C/Makefile firmware (arm-none-eabi-gcc / riscv32-esp-elf-gcc); ELF not committed |
examples/esp32c3-mlx90640-thermal/test.yaml |
./firmware/thermal_fingerprint.elf |
C/Makefile firmware (arm-none-eabi-gcc / riscv32-esp-elf-gcc); ELF not committed |
examples/f103-fidelity-bench/clockbug-nogate-smoke.yaml |
./firmware/build/clockbug.elf |
C/Makefile firmware (arm-none-eabi-gcc / riscv32-esp-elf-gcc); ELF not committed |
examples/f103-fidelity-bench/clockbug-smoke.yaml |
./firmware/build/clockbug.elf |
C/Makefile firmware (arm-none-eabi-gcc / riscv32-esp-elf-gcc); ELF not committed |
examples/f103-fidelity-bench/control-smoke.yaml |
./firmware/build/control.elf |
C/Makefile firmware (arm-none-eabi-gcc / riscv32-esp-elf-gcc); ELF not committed |
examples/f103-fidelity-bench/rambug-smoke.yaml |
./firmware/build/rambug.elf |
C/Makefile firmware (arm-none-eabi-gcc / riscv32-esp-elf-gcc); ELF not committed |
examples/f103-j1939-monitor/j1939-replay.yaml |
./firmware/build/j1939_monitor.elf |
C/Makefile firmware (arm-none-eabi-gcc / riscv32-esp-elf-gcc); ELF not committed |
examples/f103-uds-ecu/firmware/diff/diff-smoke.yaml |
/tmp/lw-deploy/core/examples/f103-uds-ecu/firmware/diff/build/f103_uds_diff.elf |
absolute path into an external HIL deploy tree |
examples/f103-uds-ecu/uds-reset-smoke.yaml |
./firmware/build/f103_uds_ecu.elf |
C/Makefile firmware (arm-none-eabi-gcc / riscv32-esp-elf-gcc); ELF not committed |
examples/f103-uds-ecu/uds-session-smoke.yaml |
./firmware/build/f103_uds_ecu.elf |
C/Makefile firmware (arm-none-eabi-gcc / riscv32-esp-elf-gcc); ELF not committed |
examples/f103-uds-ecu/uds-smoke.yaml |
./firmware/build/f103_uds_ecu.elf |
C/Makefile firmware (arm-none-eabi-gcc / riscv32-esp-elf-gcc); ELF not committed |
examples/h563-uds-bootloader/ota-smoke.yaml |
../../../udslib/examples/h563_uds_bootloader/bootloader/build/h563_uds_bootloader_sim.elf |
sibling udslib checkout not present in this repo |
examples/iolink-dido/test.yaml |
./firmware/iolink_dido.elf |
C/Makefile firmware (arm-none-eabi-gcc / riscv32-esp-elf-gcc); ELF not committed |
examples/marketplace-arduino-c3/stimuli-smoke.yaml |
../../platformio/marketplace-arduino-c3/.pio/build/marketplace/firmware.elf |
PlatformIO/Arduino output; needs pio run, not committed |
examples/mb1355c/uart-smoke.yaml |
./board_firmware/target/thumbv7em-none-eabi/release/firmware-mb1355c-demo |
nested cargo workspace; host build fails (unwinding panics are not supported without std) |
examples/nucleo-h563zi/golden-reference/dummy_test.yaml |
target/thumbv7em-none-eabihf/release/firmware-h563-demo |
firmware artifact absent |
examples/nucleo_g474re/uart-smoke.yaml |
./board_firmware/target/thumbv7em-none-eabi/release/firmware-nucleo_g474re-demo |
nested cargo workspace; host build fails (unwinding panics are not supported without std) |
examples/nucleo_wba52cg/uart-smoke.yaml |
./board_firmware/target/thumbv8m.main-none-eabi/release/firmware-nucleo_wba52cg-demo |
nested cargo workspace; host build fails (unwinding panics are not supported without std) |
Ran, but produced no census file
| script | why |
|---|---|
examples/ci/benchmark.yaml |
exited before the census dump point (pre-existing config error) |
examples/stm32f103-integrated-test/stm32f103_integrated_test.yaml |
exited before the census dump point (pre-existing config error) |
Behavioural neutrality
The instrumentation must not change what the simulator does, or the census is worthless. Two independent arguments:
1. By construction. With the feature off, every recording site expands back to the code it wraps:
| site | feature off expands to |
|---|---|
census_bus!(self, kind, expr) |
expr - the bare expression |
census_reg!(name, off, kind) |
() - so _ => { census_reg!(..); } is _ => {} and _ => { census_reg!(..); 0 } is _ => 0 |
census::record_stub / record_live_stubs / record_undecoded_reg_named / dump_if_requested |
empty #[inline(always)] fns |
StubPeripheral::as_any |
does not exist - the whole override is #[cfg(feature = "silent-census")], so the type is unchanged |
The macro arguments are not even evaluated when the feature is off, so a site cannot
introduce a side effect, a panic, or a borrow. record_live_stubs takes the bus by
shared reference and the off-build never touches it, so the single call site in
Machine::new needs no cfg attribute and cannot perturb construction. With the
feature on, every arm still performs its original action and then records; no
control flow changed anywhere.
The one thing that is observable with the feature on is StubPeripheral::as_any()
returning Some(self) instead of None. Every consumer of Peripheral::as_any in
the tree — Machine::new's four index scans, Machine's accessors, world.rs's
UART lookup, inspect.rs's mux lookup, cpu/riscv.rs's XIP lookup — immediately
downcast_refs to a concrete model type, and a stub is none of them. Some(stub)
and None produce the same None from every one of those chains.
2. Empirically. The entire runnable corpus - all 70 labs - was run twice, once
with a feature-off binary and once with a feature-on binary that was actively
recording, and result.json (status, steps, cycles, assertions, full CPU register
state, UART bytes, peripheral inspection, fidelity gaps) compared byte-for-byte:
| result | |
|---|---|
| labs run under both binaries | 70 |
result.json byte-identical |
70 / 70 |
| exit-code mismatches | 0 |
| ON runs that actively recorded (wrote a census file) | 68 |
| ON runs where at least one counter fired | 43 |
The proof is non-vacuous: all four counters fired somewhere in the set that was
compared — (a) on dummy-memory-violation, (b1) on 47 labs, (b2) on 41 labs,
(c) on 5 — and both pass and fail outcomes are represented (58 pass, 10 fail, 2
config errors), yet not one output byte moved. This is a strict superset of the
8-lab spot check the first version of this document relied on.
Both gates were also checked independently:
| binary | LABWIRED_CENSUS_OUT |
census file written? |
|---|---|---|
| feature-off | set | no |
| feature-on | unset | no |
| feature-on | set | yes |
What was NOT measured
- 46 catch-all arms that match on something other than a register offset -
cmd,dest,self.state,upper.as_str()and similar. Not register decodes; counting them would fabricate gaps. - 19 catch-all arms inside
#[cfg(test)]/mod tests. - 27 scripts whose firmware could not be produced on this host - PlatformIO/Arduino
builds, C/Makefile firmware needing
arm-none-eabi-gccorriscv32-esp-elf-gcc, one riscv32 target not installed, three nested cargo workspaces that fail to build on this toolchain, and two paths pointing outside the repo. Every one is a row above. - Xtensa and ESP32 ROM-boot paths. No
examples/esp32s3-*directory has a test script at all, and the C3 Arduino lab needsLABWIRED_ESP32C3_*flash/ROM images. - The RISC-V CPU's own error handling.
cpu/riscv.rspropagates ~32 bus results with?and discards none, so it has no (a)-equivalent to count. Unverified beyond reading the code. Machine::runbatched orchestration. Every run here used the CLI's default per-instruction path. The browser's batched path was not measured.- Any peripheral replacement that happens after
Machine::new. (b2) sweeps at machine construction, which is after every construction-time pass in the tree today (audited above), but it is not immune by design. A future pass that swapped a peripheral out later would be invisible to it, exactly asconfigure_cortex_mwas invisible to (b1). Re-check that table before trusting the number again. - Whether an undecoded offset is actually wrong. The census says the model did not decode an offset the firmware touched. Deciding whether that matters needs the datasheet, one register at a time. That is the follow-up work this table scopes.
Belongs to another task
- Fixing any of the three paths. Explicitly out of scope here.
- Removing the dead
type: "nvic"/type: "scb"entries from the chip yamls (or giving the factory a real arm for them). They cost aStubPeripheralallocation thatconfigure_cortex_mimmediately discards, and they are the reason the fallthrough histogram does not mean what a reader assumes. Not a fidelity bug. examples/ci/benchmark.yamldeclaresmax_steps: 1000000000, above the CLI'sMAX_ALLOWED_STEPSof 50000000, so it cannot run as committed.examples/stm32f103-integrated-testpoints at a system manifest missing achip:field and fails to load.examples/f103-uds-ecu/firmware/diff/diff-smoke.yamlhardcodes absolute/tmp/lw-deploy/...paths and can never run from a clean checkout.- 10 of the 68 runnable labs fail their own assertions at this SHA. Pre-existing, unrelated to this work, and not investigated.
scripts/example_smokes.shglobsexamples/*/*smoke*.yaml examples/*/test*.yaml, which misses ~7 scripts one level deeper plus everyexamples/ci/*.yaml. Widening it is a separate change.