Compare commits

..

3 Commits

Author SHA1 Message Date
a914b9d7e8 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>
2026-06-03 16:21:02 +02:00
c9ac186a20 P3.3: surface model anomalies in analyze + dashboard
The analyze screen's Issues pane now lists the model-driven checks
(check_pin_specs / check_jtag_chain / check_source_conflicts) alongside the
pin-role, net-mix and structural ones, with an "N model" count in the header;
the dashboard gains a "model:" health row. check_pin_specs/check_jtag_chain
take an optional precomputed net list, so verify, analyze and the dashboard
each compute the nets once and reuse them across checks instead of redoing the
transitive closure per check. Unit tests (75) green; verify output unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 16:14:41 +02:00
fe5b2c3d96 P3.2: source precedence + model-vs-netlist conflict check
Rank the spec sources (spec_source_rank: UserOverride > Bsdl > ConnectorModel
> Inferred > Imported); apply_model now refuses to overwrite a spec owned by a
higher-rank source, so one model never clobbers a more authoritative one. New
check_source_conflicts(System*) emits SourceConflict for a pin the BSDL
declares power/ground (a must-connect rail) that the netlist leaves
unconnected — a rail floated in the schematic; surfaced as a sixth `verify`
pass. Unit tests (75 cases) green; the real 8-card system reports 0 conflicts
(its rails are all connected) while the JTAG findings remain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 16:08:28 +02:00
13 changed files with 267 additions and 20 deletions

View File

@@ -129,9 +129,9 @@ The explore screen shows the type in the signal detail header.
**BSDL models (`attach-bsdl`)**: `attach-bsdl <module> <part> <file.bsd>` parses a BSDL device through `libbsdl` (wrapped by `BsdlModel`, `src/system/bsdl_model.{hpp,cpp}`), then `apply_bsdl(part, model)` binds each port to a Pin **by port name first, then by physical pad** — so a netlist that names IC pins either by signal or by package ball both bind. Each bound pin gets its `spec` set: `direction` (BSDL in/out/inout/linkage), `function` (TAP role → Jtag\*, `linkage` → Power/Ground/NoConnect by name, else Signal), `pad` (PIN_MAP ball), `source = Bsdl`. The `.bsd` path is stored on `Part::bsdl_path`, persisted via the `B` tag and re-applied on `restore`. Real-world check: an `xcku15p` FPGA in a VPX system binds 1517/1517 ports. **BSDL models (`attach-bsdl`)**: `attach-bsdl <module> <part> <file.bsd>` parses a BSDL device through `libbsdl` (wrapped by `BsdlModel`, `src/system/bsdl_model.{hpp,cpp}`), then `apply_bsdl(part, model)` binds each port to a Pin **by port name first, then by physical pad** — so a netlist that names IC pins either by signal or by package ball both bind. Each bound pin gets its `spec` set: `direction` (BSDL in/out/inout/linkage), `function` (TAP role → Jtag\*, `linkage` → Power/Ground/NoConnect by name, else Signal), `pad` (PIN_MAP ball), `source = Bsdl`. The `.bsd` path is stored on `Part::bsdl_path`, persisted via the `B` tag and re-applied on `restore`. Real-world check: an `xcku15p` FPGA in a VPX system binds 1517/1517 ports.
**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`) — so one source never clobbers another's. `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`. **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` (five 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). 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): **`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):
@@ -167,7 +167,7 @@ Exposed as the `analyze` shell command which prints groups (sorted by module + l
**Analyze screen** (`screen_analyze.cpp`, dashboard shortcut `a`, `screen_idx = 7`): unified **verify + analyze** view with a tabbed layout — horizontal tab bar at the top (`Issues (…) │ Groups (…) │ Types: …`), and a single scrollable detail panel below showing the active tab's list. Tab swap is handled at the outer `CatchEvent` (`Tab` / `→` cycle forward, `Shift-Tab` / `←` cycle back). The detail uses `Container::Tab({issues_menu, groups_menu, types_menu}, &analyze_focus_idx)` so `↑/↓` always navigate the visible list; each tab preserves its own selection idx. **Analyze screen** (`screen_analyze.cpp`, dashboard shortcut `a`, `screen_idx = 7`): unified **verify + analyze** view with a tabbed layout — horizontal tab bar at the top (`Issues (…) │ Groups (…) │ Types: …`), and a single scrollable detail panel below showing the active tab's list. Tab swap is handled at the outer `CatchEvent` (`Tab` / `→` cycle forward, `Shift-Tab` / `←` cycle back). The detail uses `Container::Tab({issues_menu, groups_menu, types_menu}, &analyze_focus_idx)` so `↑/↓` always navigate the visible list; each tab preserves its own selection idx.
- **Issues** pane merges: pin-role mismatches (typed pins whose actual signal type disagrees with the role from `connector_type`), bridged-net Power↔Gnd inconsistencies (the BFS check formerly in `verify` pass 2), and the structural anomalies from `analyze_system` (`DiffPairOrphan`, `BusGap`, `DiffBusGap`). Header counts each category. - **Issues** pane merges: pin-role mismatches (typed pins whose actual signal type disagrees with the role from `connector_type`), bridged-net Power↔Gnd inconsistencies (the BFS check formerly in `verify` pass 2), the structural anomalies from `analyze_system` (`DiffPairOrphan`, `BusGap`, `DiffBusGap`), and the model-driven checks (`check_pin_specs` / `check_jtag_chain` / `check_source_conflicts`, tagged `model` in the header). Header counts each category.
- **Groups** pane lists every detected `SignalGroup` sorted by `module / label` with kind tag and member count. - **Groups** pane lists every detected `SignalGroup` sorted by `module / label` with kind tag and member count.
- **Types** pane lists per-signal Power decisions (`[Power confirmed]` / `[Power REFUTED]` / `[Gnd]`) plus a trailing `[NC]` orphan rollup line. The pane header summarises counts (`N pwr-ok, M refuted, K gnd`). - **Types** pane lists per-signal Power decisions (`[Power confirmed]` / `[Power REFUTED]` / `[Gnd]`) plus a trailing `[NC]` orphan rollup line. The pane header summarises counts (`N pwr-ok, M refuted, K gnd`).
@@ -197,7 +197,7 @@ Today the only caller is `export` (`{"CSV", ".csv"}, {"ODS", ".ods"}` filters, k
**Command palette** (`screen_palette.cpp`): a global modal launcher attached to the whole tab tree via `tab | Modal(BuildPaletteModal(), &palette_open)` in `Run()`. Trigger: `Event::CtrlP` (FTXUI Input does not consume Ctrl-P, so the outer `CatchEvent` reliably picks it up first). Behaviour: a single Input bound to `palette_query` plus a result list rebuilt on every frame. Indexes three kinds of entries: commands (from the `commands` map), modules and per-module signals (qualified as `module/signal`). Fuzzy match is subsequence-based, case-insensitive: lower score wins, computed as `first_match_position * 100 + sum_of_gaps`. Kinds are biased by a constant offset (commands +0, modules +1000, signals +2000) so command matches come first when scores tie. Output capped at 20 rows to keep render cheap on big systems. Activation (`Enter`): commands → `Dispatch(name)` (which dispatches like the shell, including opening interactive screens), module → prefill `explore_*` state and jump to `screen_idx = 4`, signal → prefill `net_modules` + seed `net_sig_filter` to the exact signal name and jump to `screen_idx = 5`. `Esc` closes the palette. While the palette is open, the outer `CatchEvent` cedes events to it so Tab/Esc/etc. don't leak into the underlying screen. **Command palette** (`screen_palette.cpp`): a global modal launcher attached to the whole tab tree via `tab | Modal(BuildPaletteModal(), &palette_open)` in `Run()`. Trigger: `Event::CtrlP` (FTXUI Input does not consume Ctrl-P, so the outer `CatchEvent` reliably picks it up first). Behaviour: a single Input bound to `palette_query` plus a result list rebuilt on every frame. Indexes three kinds of entries: commands (from the `commands` map), modules and per-module signals (qualified as `module/signal`). Fuzzy match is subsequence-based, case-insensitive: lower score wins, computed as `first_match_position * 100 + sum_of_gaps`. Kinds are biased by a constant offset (commands +0, modules +1000, signals +2000) so command matches come first when scores tie. Output capped at 20 rows to keep render cheap on big systems. Activation (`Enter`): commands → `Dispatch(name)` (which dispatches like the shell, including opening interactive screens), module → prefill `explore_*` state and jump to `screen_idx = 4`, signal → prefill `net_modules` + seed `net_sig_filter` to the exact signal name and jump to `screen_idx = 5`. `Esc` closes the palette. While the palette is open, the outer `CatchEvent` cedes events to it so Tab/Esc/etc. don't leak into the underlying screen.
**Dashboard** (`screen_dashboard.cpp`, `dashboard` command, `screen_idx = 4`): read-only system overview. Single Renderer, no Input child. Recomputes everything per frame (cheap on realistic sizes): counters (modules/parts/signals/connections), three health rows (verify pin-role mismatches, bridged-net inconsistencies, NC orphans — green check / yellow warning prefix), an analysis summary line (diff pairs / buses / anomaly count, coloured if non-zero), and a per-module table (parts / signals / `connector_type`-tagged parts). Letter shortcuts handled in the outer `CatchEvent`: `c`=console, `p`=plug (connect), `t`=set-connector-type, `e`=explore, `a`=analyze, `q`=quit. `Esc` is swallowed on the dashboard (home). The dashboard is `interactive = true`, `scriptable = false`; running `dashboard` inside `source` aborts the script. **Dashboard** (`screen_dashboard.cpp`, `dashboard` command, `screen_idx = 4`): read-only system overview. Single Renderer, no Input child. Recomputes everything per frame (cheap on realistic sizes): counters (modules/parts/signals/connections), four health rows (verify pin-role mismatches, bridged-net inconsistencies, NC orphans, and BSDL/JTAG model anomalies — green check / yellow warning prefix), an analysis summary line (diff pairs / buses / anomaly count, coloured if non-zero), and a per-module table (parts / signals / `connector_type`-tagged parts). Letter shortcuts handled in the outer `CatchEvent`: `c`=console, `p`=plug (connect), `t`=set-connector-type, `e`=explore, `a`=analyze, `q`=quit. `Esc` is swallowed on the dashboard (home). The dashboard is `interactive = true`, `scriptable = false`; running `dashboard` inside `source` aborts the script.
**Screen titles** (shared idiom): every interactive screen renders a top bar in the form `" essim "` (bold) + `"→ "` (dim) + `"<screen-name>"` (bold) + `" — <short description>"` (dim), followed by a `separator()`. The main screen has its own variant that adds a live `N module(s), M connection(s)` counter on the right. Aim is to make the breadcrumb between essim and the current mode visible at all times. **Screen titles** (shared idiom): every interactive screen renders a top bar in the form `" essim "` (bold) + `"→ "` (dim) + `"<screen-name>"` (bold) + `" — <short description>"` (dim), followed by a `separator()`. The main screen has its own variant that adds a live `N module(s), M connection(s)` counter on the right. Aim is to make the breadcrumb between essim and the current mode visible at all times.
@@ -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. - **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. - **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`). They consume the BSDL-populated `PinSpec` plus `compute_all_nets`. Surfacing them in the analyze/dashboard Issues pane is a TODO. 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 ### Component kind

View File

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

View File

@@ -37,6 +37,8 @@ enum class AnomalyKind {
JtagTapIncomplete, ///< A TAP device is missing one of TDI/TDO/TMS/TCK. JtagTapIncomplete, ///< A TAP device is missing one of TDI/TDO/TMS/TCK.
JtagChainBreak, ///< The TDO→TDI daisy chain is broken / not a single path. JtagChainBreak, ///< The TDO→TDI daisy chain is broken / not a single path.
JtagBusUnbridged, ///< TMS or TCK is not common to all TAP devices. 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 { struct Anomaly {

View File

@@ -1,5 +1,7 @@
#include "bsdl_check.hpp" #include "bsdl_check.hpp"
#include "bsdl_model.hpp"
#include "connect.hpp"
#include "modules.hpp" #include "modules.hpp"
#include "nets.hpp" #include "nets.hpp"
#include "parts.hpp" #include "parts.hpp"
@@ -10,6 +12,7 @@
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <unordered_set>
#include <utility> #include <utility>
#include <vector> #include <vector>
@@ -37,13 +40,16 @@ std::string join_labels(const std::vector<Pin *> &pins)
} // namespace } // namespace
std::vector<Anomaly> check_pin_specs(System *sys) std::vector<Anomaly> check_pin_specs(System *sys, const std::vector<Net> *nets)
{ {
std::vector<Anomaly> out; std::vector<Anomaly> out;
if (!sys) if (!sys)
return out; return out;
for (const Net &net : compute_all_nets(sys)) { std::vector<Net> local;
if (!nets)
local = compute_all_nets(sys);
for (const Net &net : (nets ? *nets : local)) {
std::vector<Pin *> pins; std::vector<Pin *> pins;
std::vector<Signal *> sigs; std::vector<Signal *> sigs;
Module *mod = nullptr; Module *mod = nullptr;
@@ -121,14 +127,17 @@ std::vector<Anomaly> check_pin_specs(System *sys)
return out; return out;
} }
std::vector<Anomaly> check_jtag_chain(System *sys) std::vector<Anomaly> check_jtag_chain(System *sys, const std::vector<Net> *nets_in)
{ {
std::vector<Anomaly> out; std::vector<Anomaly> out;
if (!sys) if (!sys)
return out; return out;
// Map every pin to the index of the net it sits on. // Map every pin to the index of the net it sits on.
std::vector<Net> nets = compute_all_nets(sys); std::vector<Net> local;
if (!nets_in)
local = compute_all_nets(sys);
const std::vector<Net> &nets = nets_in ? *nets_in : local;
std::unordered_map<Pin *, int> net_of; std::unordered_map<Pin *, int> net_of;
for (size_t i = 0; i < nets.size(); ++i) for (size_t i = 0; i < nets.size(); ++i)
for (auto &mp : nets[i].members) for (auto &mp : nets[i].members)
@@ -277,3 +286,94 @@ std::vector<Anomaly> check_jtag_chain(System *sys)
return out; return out;
} }
std::vector<Anomaly> check_source_conflicts(System *sys)
{
std::vector<Anomaly> out;
if (!sys)
return out;
// Pins bridged to a peer signal through a connection count as connected.
std::unordered_set<Pin *> bridged;
for (auto &ckv : *sys->connections())
for (auto &wp : ckv.second->pin_map) {
if (wp.first) bridged.insert(wp.first);
if (wp.second) bridged.insert(wp.second);
}
for (auto &mkv : *sys->modules())
for (auto &pkv : *mkv.second)
for (auto &nkv : *pkv.second) {
Pin *pin = nkv.second;
if (pin->spec.source != SpecSource::Bsdl)
continue;
PinFunction f = pin->spec.function;
if (f != PinFunction::Power && f != PinFunction::Ground)
continue; // only must-connect rails are a clear defect
if (pin->connected() || bridged.count(pin))
continue;
Anomaly a;
a.kind = AnomalyKind::SourceConflict;
a.module = (pin->prnt) ? pin->prnt->prnt : nullptr;
a.message = pin_label(pin) + ": BSDL says "
+ (f == PinFunction::Power ? "power" : "ground")
+ " but the netlist leaves it unconnected"
+ (pin->nc_origin == NcOrigin::ImportedUnconnected
? " (marked NC at import)" : "");
out.push_back(std::move(a));
}
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

@@ -2,11 +2,16 @@
#define _BSDL_CHECK_HPP_ #define _BSDL_CHECK_HPP_
#include "analysis.hpp" // Anomaly, AnomalyKind #include "analysis.hpp" // Anomaly, AnomalyKind
#include "nets.hpp" // Net
#include <vector> #include <vector>
class System; class System;
// The net checks below accept an optional precomputed net list: callers that
// already have one (verify, the analyze screen, the dashboard) pass it so the
// transitive-closure pass isn't redone. Pass nullptr to compute it internally.
// Model-driven pin checks over the system's nets, using the PinSpec // Model-driven pin checks over the system's nets, using the PinSpec
// direction/function populated by connector or BSDL models. Emits: // direction/function populated by connector or BSDL models. Emits:
// - DriveContention : a net with ≥2 push-pull output drivers; // - DriveContention : a net with ≥2 push-pull output drivers;
@@ -14,7 +19,7 @@ class System;
// - NcWired : a no-connect pin wired onto a multi-pin net. // - NcWired : a no-connect pin wired onto a multi-pin net.
// Read-only; nets with no direction data are skipped (no false positives on // Read-only; nets with no direction data are skipped (no false positives on
// un-modelled parts). // un-modelled parts).
std::vector<Anomaly> check_pin_specs(System *sys); std::vector<Anomaly> check_pin_specs(System *sys, const std::vector<Net> *nets = nullptr);
// JTAG boundary-scan chain integrity, using pins whose PinSpec.function is a TAP // JTAG boundary-scan chain integrity, using pins whose PinSpec.function is a TAP
// role (JtagTdi/Tdo/Tms/Tck/Trst). Resolves each TAP pin to its net and checks: // role (JtagTdi/Tdo/Tms/Tck/Trst). Resolves each TAP pin to its net and checks:
@@ -23,6 +28,19 @@ std::vector<Anomaly> check_pin_specs(System *sys);
// - JtagChainBreak : the TDO→TDI daisy chain dangles, fans out, or is not a // - JtagChainBreak : the TDO→TDI daisy chain dangles, fans out, or is not a
// single path (≠1 head / ≠1 tail). // single path (≠1 head / ≠1 tail).
// Empty when the system has no TAP pins. // Empty when the system has no TAP pins.
std::vector<Anomaly> check_jtag_chain(System *sys); std::vector<Anomaly> check_jtag_chain(System *sys, const std::vector<Net> *nets = nullptr);
// Conflicts between a device model and the netlist's own view of a pin. Today:
// a pin the BSDL declares power/ground (a must-connect rail) that the netlist
// leaves unconnected (no signal and not bridged) — i.e. a rail floated in the
// 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_ #endif // _BSDL_CHECK_HPP_

View File

@@ -30,14 +30,17 @@ ApplyReport apply_model(Part *part, const PinModel &model)
} }
} }
// 2. Set each pin's spec where the model speaks for it. // 2. Set each pin's spec where the model speaks for it — but never overwrite
// a spec already owned by a higher-precedence source (see spec_source_rank).
r.pins_total = (int)part->size(); r.pins_total = (int)part->size();
for (auto &kv : *part) { for (auto &kv : *part) {
PinSpec s = model.spec_for(kv.first); PinSpec s = model.spec_for(kv.first);
if (s.source != SpecSource::None) { if (s.source == SpecSource::None)
kv.second->spec = s; continue;
++r.set; if (spec_source_rank(s.source) < spec_source_rank(kv.second->spec.source))
} continue;
kv.second->spec = s;
++r.set;
} }
return r; return r;
} }

View File

@@ -51,4 +51,21 @@ inline PinFunction function_from_signal_type(SignalType t)
} }
} }
// Precedence of spec sources: a higher rank wins when two sources speak for the
// same pin. A user override beats any model; a device model (BSDL) beats a
// connector layout; both beat plain import / inference. Used by apply_model to
// avoid clobbering a more authoritative spec.
inline int spec_source_rank(SpecSource s)
{
switch (s) {
case SpecSource::None: return 0;
case SpecSource::Imported: return 1;
case SpecSource::Inferred: return 2;
case SpecSource::ConnectorModel: return 3;
case SpecSource::Bsdl: return 4;
case SpecSource::UserOverride: return 5;
}
return 0;
}
#endif // _PIN_SPEC_HPP_ #endif // _PIN_SPEC_HPP_

View File

@@ -300,18 +300,32 @@ void Tui::RegisterCommands() {
// Model-driven pin checks (drive contention / undriven net / NC-wired) // Model-driven pin checks (drive contention / undriven net / NC-wired)
// from the PinSpec direction/function populated by connector/BSDL models. // from the PinSpec direction/function populated by connector/BSDL models.
auto pin_anoms = check_pin_specs(sys.get()); auto pin_anoms = check_pin_specs(sys.get(), &nets);
for (const auto &a : pin_anoms) for (const auto &a : pin_anoms)
Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message); Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message);
Print("verify: " + std::to_string(pin_anoms.size()) Print("verify: " + std::to_string(pin_anoms.size())
+ " model-driven pin anomaly(ies)."); + " model-driven pin anomaly(ies).");
// JTAG boundary-scan chain integrity (TAP pins → nets). // JTAG boundary-scan chain integrity (TAP pins → nets).
auto jtag_anoms = check_jtag_chain(sys.get()); auto jtag_anoms = check_jtag_chain(sys.get(), &nets);
for (const auto &a : jtag_anoms) for (const auto &a : jtag_anoms)
Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message); Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message);
Print("verify: " + std::to_string(jtag_anoms.size()) Print("verify: " + std::to_string(jtag_anoms.size())
+ " JTAG chain anomaly(ies)."); + " JTAG chain anomaly(ies).");
// Model-vs-netlist conflicts (e.g. a BSDL power pin left unconnected).
auto conflict_anoms = check_source_conflicts(sys.get());
for (const auto &a : conflict_anoms)
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, }, true,
"check pin roles, bridged-net consistency, and model-driven pin specs (contention/undriven/NC)" }; "check pin roles, bridged-net consistency, and model-driven pin specs (contention/undriven/NC)" };

View File

@@ -2,6 +2,7 @@
#include "tui/tui_helpers.hpp" #include "tui/tui_helpers.hpp"
#include "system/analysis.hpp" #include "system/analysis.hpp"
#include "system/bsdl_check.hpp"
#include "system/connect.hpp" #include "system/connect.hpp"
#include "system/modules.hpp" #include "system/modules.hpp"
#include "system/nets.hpp" #include "system/nets.hpp"
@@ -99,16 +100,35 @@ Component Tui::BuildAnalyzeScreen() {
+ anomaly_kind_name(a.kind) + "] " + anomaly_kind_name(a.kind) + "] "
+ a.message); + a.message);
// Model-driven checks (same as `verify`), reusing the nets above.
std::vector<Anomaly> model_anoms;
{
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("[")
+ anomaly_kind_name(a.kind) + "] "
+ a.message);
int n_model = (int)model_anoms.size();
if (analyze_issues.empty()) analyze_issues.push_back("(no issue found)"); if (analyze_issues.empty()) analyze_issues.push_back("(no issue found)");
if (analyze_issue_idx >= (int)analyze_issues.size()) if (analyze_issue_idx >= (int)analyze_issues.size())
analyze_issue_idx = (int)analyze_issues.size() - 1; analyze_issue_idx = (int)analyze_issues.size() - 1;
std::string issues_header = "Issues (" std::string issues_header = "Issues ("
+ std::to_string(n_role_mismatches + n_inconsistent + std::to_string(n_role_mismatches + n_inconsistent
+ (int)rep.anomalies.size()) + (int)rep.anomalies.size() + n_model)
+ ": " + std::to_string(n_role_mismatches) + " pin-role, " + ": " + std::to_string(n_role_mismatches) + " pin-role, "
+ std::to_string(n_inconsistent) + " net-mix, " + std::to_string(n_inconsistent) + " net-mix, "
+ std::to_string(rep.anomalies.size()) + " struct.)"; + std::to_string(rep.anomalies.size()) + " struct, "
+ std::to_string(n_model) + " model)";
// ============================================================ Groups // ============================================================ Groups
analyze_groups.clear(); analyze_groups.clear();

View File

@@ -2,6 +2,7 @@
#include "tui/tui_helpers.hpp" #include "tui/tui_helpers.hpp"
#include "system/analysis.hpp" #include "system/analysis.hpp"
#include "system/bsdl_check.hpp"
#include "system/connect.hpp" #include "system/connect.hpp"
#include "system/modules.hpp" #include "system/modules.hpp"
#include "system/nets.hpp" #include "system/nets.hpp"
@@ -149,6 +150,15 @@ Component Tui::BuildDashboardScreen() {
+ std::to_string(orph_imported) + " imported, " + std::to_string(orph_imported) + " imported, "
+ std::to_string(orph_dropped) + " dropped)")); + std::to_string(orph_dropped) + " dropped)"));
// Model-driven checks (BSDL pin specs, JTAG chain, source conflicts),
// 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_bsdl_completeness(sys.get()).size());
health_rows.push_back(health_line(n_model == 0,
"model: " + std::to_string(n_model) + " BSDL/JTAG anomaly(ies)"));
// ---- analysis summary ---- // ---- analysis summary ----
AnalysisReport rep = analyze_system(sys.get()); AnalysisReport rep = analyze_system(sys.get());
int n_diff = 0, n_diff_bus = 0, n_bus = 0; int n_diff = 0, n_diff_bus = 0, n_bus = 0;

View File

@@ -1,5 +1,7 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include "system/analysis.hpp"
#include "system/bsdl_check.hpp"
#include "system/bsdl_model.hpp" #include "system/bsdl_model.hpp"
#include "system/parts.hpp" #include "system/parts.hpp"
#include "system/pins.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(bsd);
std::remove(snap); 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);
}

View File

@@ -159,3 +159,23 @@ TEST_CASE("check_jtag_chain reports an incomplete TAP") {
auto a = check_jtag_chain(&sys); auto a = check_jtag_chain(&sys);
CHECK(count_kind(a, AnomalyKind::JtagTapIncomplete) == 1); CHECK(count_kind(a, AnomalyKind::JtagTapIncomplete) == 1);
} }
TEST_CASE("check_source_conflicts flags a BSDL rail left unconnected") {
System sys;
Module *m = sys.modules()->merge("M");
Part *u = new Part("U1");
m->add(u);
// A BSDL power pin with no signal → conflict (a rail floated in the netlist).
Pin *vcc = new Pin("VCC");
vcc->spec.function = PinFunction::Power;
vcc->spec.source = SpecSource::Bsdl;
u->add(vcc);
// A BSDL ground pin that IS connected → no conflict.
Pin *gnd = mkpin(u, "GND", PinDirection::Power, PinFunction::Ground);
wire(m, "GNDNET", {gnd, mkpin(u, "X", PinDirection::Out, PinFunction::Signal)});
auto a = check_source_conflicts(&sys);
CHECK(count_kind(a, AnomalyKind::SourceConflict) == 1);
}

View File

@@ -68,3 +68,18 @@ TEST_CASE("apply_model does not overwrite a spec the model is silent about") {
CHECK(part.get("DATA")->spec.function == PinFunction::Signal); CHECK(part.get("DATA")->spec.function == PinFunction::Signal);
CHECK(part.get("DATA")->spec.source == SpecSource::Bsdl); CHECK(part.get("DATA")->spec.source == SpecSource::Bsdl);
} }
TEST_CASE("apply_model never overwrites a higher-precedence source") {
Part part("U3");
Pin *p = new Pin("VCC");
p->spec.function = PinFunction::Ground; // user-set, deliberately != the model
p->spec.source = SpecSource::UserOverride;
part.add(p);
FakeModel m; // would set VCC = Power / Bsdl
apply_model(&part, m);
// UserOverride (rank 5) outranks Bsdl (rank 4): kept untouched.
CHECK(part.get("VCC")->spec.source == SpecSource::UserOverride);
CHECK(part.get("VCC")->spec.function == PinFunction::Ground);
}