P3: BSDL completeness check (missing device power/ground pins)

check_bsdl_completeness(System*): for each BSDL-attached part, re-parse the
.bsd and report the device power/ground ports with no matching pin on the
netlist part (matched by port name or physical pad) — a rail the schematic
symbol is missing. One aggregated BsdlPinMissing per part; restricted to
power/ground so unused I/O balls don't create noise. Surfaced as a 7th verify
pass and in the analyze/dashboard model counts. 76 cases / 327 assertions
green; the real 8-card system reports 0 (all FPGA rails present). This closes
out P3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 16:21:02 +02:00
parent c9ac186a20
commit a914b9d7e8
9 changed files with 100 additions and 3 deletions

View File

@@ -131,7 +131,7 @@ The explore screen shows the type in the signal detail header.
**Unified application (`apply_model`)**: connector layout and BSDL are two implementations of one `PinModel` interface (`src/system/pin_model.{hpp,cpp}`: `spec_for(pin_name)`, `layout()`, `source()`). `ConnectorModel` wraps `pin_role`/`pin_layout`; `BsdlPinModel` wraps a parsed `BsdlModel`, indexed by both port name and physical pad. A single `apply_model(Part*, const PinModel&)` materialises the layout pins missing from the netlist, then sets each pin's `spec` **only where the model speaks** (`spec.source != None`). Sources are ranked (`spec_source_rank` in `pin_spec.hpp`: UserOverride > Bsdl > ConnectorModel > Inferred > Imported) and apply_model refuses to overwrite a spec owned by a higher-rank source — so one source never clobbers a more authoritative one, which is also the basis for `check_source_conflicts`. `set-connector-type` and `attach-bsdl` both funnel through it (the latter via the thin `apply_bsdl` adapter); `verify` stays agnostic of where a spec came from. A future SPICE/Modelica source would be a third `PinModel`.
**`verify` (six passes)**: (1) typed pins — local mismatch between each pin's `expected_signal_type()` (derived from its `PinSpec`) and the actual signal type; (2) bridged nets — Power↔GndShield inconsistencies; (3) orphan summary `N orphan pin(s) at import (X imported NC, Y dropped singleton)` (filters out pins bridged via any `Connection::pin_map` — typically `FillIdentityNCs`-materialised); (4) **model-driven pin checks** (`check_pin_specs`): `DriveContention` (≥2 push-pull `Out` on a net), `UndrivenNet` (a **fully-modelled** net with input(s) but no driver — nets with any Unknown-direction pin are skipped, so un-modelled drivers don't cause false positives), `NcWired` (a no-connect pin on a multi-pin net); (5) **JTAG chain** (`check_jtag_chain`): collects TAP pins by `spec.function`, maps each to its net, emits `JtagTapIncomplete` / `JtagBusUnbridged` (TMS or TCK not common to all TAP devices) / `JtagChainBreak` (dangling TDO/TDI, chain fan-out, or not a single head→tail daisy chain); (6) **source conflicts** (`check_source_conflicts`): a pin the BSDL declares power/ground (a must-connect rail) that the netlist leaves unconnected — a rail floated in the schematic (`SourceConflict`; the reverse, a BSDL no-connect that *is* wired, is the `NcWired` check). The BFS-reached `(module, signal)` set for any signal is shown live in `explore`'s detail pane when a signal entry is selected.
**`verify` (seven passes)**: (1) typed pins — local mismatch between each pin's `expected_signal_type()` (derived from its `PinSpec`) and the actual signal type; (2) bridged nets — Power↔GndShield inconsistencies; (3) orphan summary `N orphan pin(s) at import (X imported NC, Y dropped singleton)` (filters out pins bridged via any `Connection::pin_map` — typically `FillIdentityNCs`-materialised); (4) **model-driven pin checks** (`check_pin_specs`): `DriveContention` (≥2 push-pull `Out` on a net), `UndrivenNet` (a **fully-modelled** net with input(s) but no driver — nets with any Unknown-direction pin are skipped, so un-modelled drivers don't cause false positives), `NcWired` (a no-connect pin on a multi-pin net); (5) **JTAG chain** (`check_jtag_chain`): collects TAP pins by `spec.function`, maps each to its net, emits `JtagTapIncomplete` / `JtagBusUnbridged` (TMS or TCK not common to all TAP devices) / `JtagChainBreak` (dangling TDO/TDI, chain fan-out, or not a single head→tail daisy chain); (6) **source conflicts** (`check_source_conflicts`): a pin the BSDL declares power/ground (a must-connect rail) that the netlist leaves unconnected — a rail floated in the schematic (`SourceConflict`; the reverse, a BSDL no-connect that *is* wired, is the `NcWired` check); (7) **BSDL completeness** (`check_bsdl_completeness`): device power/ground ports (from the attached `.bsd`, re-parsed) with no matching pin on the netlist part — a rail the schematic symbol is missing (`BsdlPinMissing`, one aggregated finding per part). The BFS-reached `(module, signal)` set for any signal is shown live in `explore`'s detail pane when a signal entry is selected.
**`analyze` (post-processing pass)**: `analyze_system(System*) → AnalysisReport` (`src/system/analysis.{hpp,cpp}`) is a stateless read-only pass that detects structural signal groups and anomalies. Per-module (signals are module-scoped):
@@ -299,7 +299,7 @@ The analyze screen additionally surfaces two "verify-class" issues, computed the
- **pin-role mismatch** — a pin whose `expected_signal_type()` (derived from its `PinSpec`, set by `set-connector-type` via `pin_role(connector_type, pin_name)`) disagrees with the actual signal type.
- **net-mix** — a bridged net (BFS over `Connection::pin_map`, ≥ 2 members) where `net_type_consistent(net, &dominant)` returns false. Specifically, the net contains both `Power` and `GndShield` signals.
The `verify` command (not the analyze screen, yet) also emits the **model-driven `AnomalyKind`s** from `bsdl_check.{hpp,cpp}`: `DriveContention` / `UndrivenNet` / `NcWired` (`check_pin_specs`) and `JtagTapIncomplete` / `JtagChainBreak` / `JtagBusUnbridged` (`check_jtag_chain`); and `SourceConflict` (`check_source_conflicts` — a BSDL power/ground pin the netlist leaves unconnected). They consume the BSDL-populated `PinSpec` plus `compute_all_nets`, and are surfaced in three places: the `verify` command, the analyze screen's Issues pane (counted as `… N model`), and a `model:` health row on the dashboard. `check_pin_specs`/`check_jtag_chain` accept an optional precomputed net list, so verify, analyze and the dashboard each compute the nets once and reuse them across checks.
The `verify` command (not the analyze screen, yet) also emits the **model-driven `AnomalyKind`s** from `bsdl_check.{hpp,cpp}`: `DriveContention` / `UndrivenNet` / `NcWired` (`check_pin_specs`) and `JtagTapIncomplete` / `JtagChainBreak` / `JtagBusUnbridged` (`check_jtag_chain`); `SourceConflict` (`check_source_conflicts` — a BSDL power/ground pin the netlist leaves unconnected); and `BsdlPinMissing` (`check_bsdl_completeness` — a device power/ground port absent from the netlist part, re-parsing the attached `.bsd`). They consume the BSDL-populated `PinSpec` plus `compute_all_nets`, and are surfaced in three places: the `verify` command, the analyze screen's Issues pane (counted as `… N model`), and a `model:` health row on the dashboard. `check_pin_specs`/`check_jtag_chain` accept an optional precomputed net list, so verify, analyze and the dashboard each compute the nets once and reuse them across checks.
### Component kind

View File

@@ -30,6 +30,7 @@ const char *anomaly_kind_name(AnomalyKind k) {
case AnomalyKind::JtagChainBreak: return "jtag-chain-break";
case AnomalyKind::JtagBusUnbridged: return "jtag-bus-unbridged";
case AnomalyKind::SourceConflict: return "source-conflict";
case AnomalyKind::BsdlPinMissing: return "bsdl-pin-missing";
}
return "?";
}

View File

@@ -38,6 +38,7 @@ enum class AnomalyKind {
JtagChainBreak, ///< The TDO→TDI daisy chain is broken / not a single path.
JtagBusUnbridged, ///< TMS or TCK is not common to all TAP devices.
SourceConflict, ///< A model contradicts the netlist (e.g. BSDL power pin left NC).
BsdlPinMissing, ///< A BSDL power/ground port has no pin on the netlist part.
};
struct Anomaly {

View File

@@ -1,5 +1,6 @@
#include "bsdl_check.hpp"
#include "bsdl_model.hpp"
#include "connect.hpp"
#include "modules.hpp"
#include "nets.hpp"
@@ -325,3 +326,54 @@ std::vector<Anomaly> check_source_conflicts(System *sys)
return out;
}
std::vector<Anomaly> check_bsdl_completeness(System *sys)
{
std::vector<Anomaly> out;
if (!sys)
return out;
for (auto &mkv : *sys->modules())
for (auto &pkv : *mkv.second) {
Part *part = pkv.second;
if (part->bsdl_path.empty())
continue;
BsdlModel model = BsdlModel::from_file(part->bsdl_path);
if (!model.valid())
continue;
std::vector<std::string> missing; // absent power/ground ports
for (const auto &port : model.ports()) {
if (port.function != PinFunction::Power &&
port.function != PinFunction::Ground)
continue; // only must-have rails — unused I/O may be absent
bool present = (!port.name.empty() && part->exists(port.name)) ||
(!port.pad.empty() && part->exists(port.pad));
if (!present) {
std::string lbl = port.name;
if (!port.pad.empty())
lbl += "@" + port.pad;
missing.push_back(lbl);
}
}
if (!missing.empty()) {
std::string examples;
for (size_t i = 0; i < missing.size() && i < 5; ++i)
examples += (examples.empty() ? "" : ", ") + missing[i];
if (missing.size() > 5)
examples += ", …";
Anomaly a;
a.kind = AnomalyKind::BsdlPinMissing;
a.module = part->prnt;
a.message = mkv.first + "/" + part->name + ": "
+ std::to_string(missing.size())
+ " device power/ground pin(s) absent from the netlist ("
+ examples + ")";
out.push_back(std::move(a));
}
}
return out;
}

View File

@@ -36,4 +36,11 @@ std::vector<Anomaly> check_jtag_chain(System *sys, const std::vector<Net> *nets
// schematic. The reverse (BSDL no-connect wired in the netlist) is `NcWired`.
std::vector<Anomaly> check_source_conflicts(System *sys);
// Completeness: for every part with an attached BSDL, the device's power/ground
// ports that have no matching pin on the netlist part (matched by port name or
// physical pad) — i.e. a power/ground pin the schematic symbol is missing. One
// aggregated anomaly per part. Re-parses each attached `.bsd` (no model cache on
// Part yet), so it's bounded by the number of BSDL-attached parts.
std::vector<Anomaly> check_bsdl_completeness(System *sys);
#endif // _BSDL_CHECK_HPP_

View File

@@ -319,6 +319,13 @@ void Tui::RegisterCommands() {
Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message);
Print("verify: " + std::to_string(conflict_anoms.size())
+ " source-conflict(s).");
// BSDL completeness: device power/ground pins missing from the netlist.
auto missing_anoms = check_bsdl_completeness(sys.get());
for (const auto &a : missing_anoms)
Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message);
Print("verify: " + std::to_string(missing_anoms.size())
+ " BSDL completeness issue(s).");
}, true,
"check pin roles, bridged-net consistency, and model-driven pin specs (contention/undriven/NC)" };

View File

@@ -106,9 +106,11 @@ Component Tui::BuildAnalyzeScreen() {
auto a1 = check_pin_specs(sys.get(), &nets);
auto a2 = check_jtag_chain(sys.get(), &nets);
auto a3 = check_source_conflicts(sys.get());
auto a4 = check_bsdl_completeness(sys.get());
model_anoms.insert(model_anoms.end(), a1.begin(), a1.end());
model_anoms.insert(model_anoms.end(), a2.begin(), a2.end());
model_anoms.insert(model_anoms.end(), a3.begin(), a3.end());
model_anoms.insert(model_anoms.end(), a4.begin(), a4.end());
}
for (const auto &a : model_anoms)
analyze_issues.push_back(std::string("[")

View File

@@ -154,7 +154,8 @@ Component Tui::BuildDashboardScreen() {
// reusing the nets computed above.
int n_model = (int)(check_pin_specs(sys.get(), &nets).size()
+ check_jtag_chain(sys.get(), &nets).size()
+ check_source_conflicts(sys.get()).size());
+ check_source_conflicts(sys.get()).size()
+ check_bsdl_completeness(sys.get()).size());
health_rows.push_back(health_line(n_model == 0,
"model: " + std::to_string(n_model) + " BSDL/JTAG anomaly(ies)"));

View File

@@ -1,5 +1,7 @@
#include <doctest/doctest.h>
#include "system/analysis.hpp"
#include "system/bsdl_check.hpp"
#include "system/bsdl_model.hpp"
#include "system/parts.hpp"
#include "system/pins.hpp"
@@ -141,3 +143,27 @@ TEST_CASE("attached BSDL path persists and re-applies on restore") {
std::remove(bsd);
std::remove(snap);
}
TEST_CASE("check_bsdl_completeness flags a device power pin absent from the part") {
const char *bsd = "test_complete_demo.bsd";
{ std::ofstream o(bsd); o << DEMO_BSDL; }
System sys;
Module *m = sys.modules()->merge("CARD");
Part *u = new Part("U1");
// All pins EXCEPT VDD (a power port at ball C1) → its port is unmatched.
for (const char *n : {"TCK", "TDI", "TDO", "TMS", "IO1", "GND"})
u->add(new Pin(n));
u->bsdl_path = bsd;
m->add(u);
auto a = check_bsdl_completeness(&sys);
REQUIRE(a.size() == 1);
CHECK(a[0].kind == AnomalyKind::BsdlPinMissing);
// With VDD present (by ball or by name), no completeness issue.
u->add(new Pin("VDD"));
CHECK(check_bsdl_completeness(&sys).empty());
std::remove(bsd);
}