Verify: check diff pairs/buses crossing connections (swap, incomplete)
New pass 8 (core/domain/diff_check.{hpp,cpp}): every complete local diff
pair (X_P/X_N, name-based) resolves its legs to two bridged nets; peer
pairs on those nets must match leg for leg.
- DiffPolaritySwap: P legs meet N legs across a connection (sometimes
intentional - reported for review), or both legs joined onto one net.
- DiffCrossIncomplete: pairs sharing only one leg, and diff-bus lanes
crossing NOWHERE while sibling lanes cross (distributed/fan-out buses
stay silent - validated against the real 7-card VPX system: 21 noisy
findings down to 3 genuine dangling-lane reports, 0 false swaps).
diff_suffix/split_trailing_index/is_internal_name promoted out of
analysis.cpp's anonymous namespace for reuse. VerifyReport.diff_anomalies
wired into model_total() and all four renderers (TUI verify, script
engine, wx log, analyze Issues). 8 new test cases (466 assertions).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -150,6 +150,7 @@ private:
|
|||||||
grp(r.jtag_anomalies, " JTAG chain anomaly(ies).");
|
grp(r.jtag_anomalies, " JTAG chain anomaly(ies).");
|
||||||
grp(r.conflict_anomalies, " source-conflict(s).");
|
grp(r.conflict_anomalies, " source-conflict(s).");
|
||||||
grp(r.completeness_anomalies, " BSDL completeness issue(s).");
|
grp(r.completeness_anomalies, " BSDL completeness issue(s).");
|
||||||
|
grp(r.diff_anomalies, " diff-pair crossing anomaly(ies).");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute one already-trimmed line. Returns false on a hard error.
|
// Execute one already-trimmed line. Returns false on a hard error.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "core/domain/bsdl_check.hpp"
|
#include "core/domain/bsdl_check.hpp"
|
||||||
#include "core/domain/connect.hpp"
|
#include "core/domain/connect.hpp"
|
||||||
|
#include "core/domain/diff_check.hpp"
|
||||||
#include "core/domain/modules.hpp"
|
#include "core/domain/modules.hpp"
|
||||||
#include "core/domain/nets.hpp"
|
#include "core/domain/nets.hpp"
|
||||||
#include "core/domain/parts.hpp"
|
#include "core/domain/parts.hpp"
|
||||||
@@ -93,11 +94,12 @@ VerifyReport verify(System *sys)
|
|||||||
r.orphans.push_back({mkv.first, pkv.first, nkv.first, dropped});
|
r.orphans.push_back({mkv.first, pkv.first, nkv.first, dropped});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Passes 4-7 — model-driven checks (reuse the nets from pass 2).
|
// Passes 4-8 — model-driven checks (reuse the nets from pass 2).
|
||||||
r.pin_anomalies = check_pin_specs(sys, &nets);
|
r.pin_anomalies = check_pin_specs(sys, &nets);
|
||||||
r.jtag_anomalies = check_jtag_chain(sys, &nets);
|
r.jtag_anomalies = check_jtag_chain(sys, &nets);
|
||||||
r.conflict_anomalies = check_source_conflicts(sys);
|
r.conflict_anomalies = check_source_conflicts(sys);
|
||||||
r.completeness_anomalies = check_bsdl_completeness(sys);
|
r.completeness_anomalies = check_bsdl_completeness(sys);
|
||||||
|
r.diff_anomalies = check_diff_crossings(sys, &nets);
|
||||||
|
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,11 +52,13 @@ struct VerifyReport {
|
|||||||
std::vector<Anomaly> jtag_anomalies; ///< check_jtag_chain
|
std::vector<Anomaly> jtag_anomalies; ///< check_jtag_chain
|
||||||
std::vector<Anomaly> conflict_anomalies; ///< check_source_conflicts
|
std::vector<Anomaly> conflict_anomalies; ///< check_source_conflicts
|
||||||
std::vector<Anomaly> completeness_anomalies; ///< check_bsdl_completeness
|
std::vector<Anomaly> completeness_anomalies; ///< check_bsdl_completeness
|
||||||
|
std::vector<Anomaly> diff_anomalies; ///< check_diff_crossings
|
||||||
|
|
||||||
int orphan_total() const { return orphan_imported + orphan_dropped; }
|
int orphan_total() const { return orphan_imported + orphan_dropped; }
|
||||||
int model_total() const {
|
int model_total() const {
|
||||||
return (int)(pin_anomalies.size() + jtag_anomalies.size()
|
return (int)(pin_anomalies.size() + jtag_anomalies.size()
|
||||||
+ conflict_anomalies.size() + completeness_anomalies.size());
|
+ conflict_anomalies.size() + completeness_anomalies.size()
|
||||||
|
+ diff_anomalies.size());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -31,15 +31,15 @@ const char *anomaly_kind_name(AnomalyKind k) {
|
|||||||
case AnomalyKind::JtagBusUnbridged: return "jtag-bus-unbridged";
|
case AnomalyKind::JtagBusUnbridged: return "jtag-bus-unbridged";
|
||||||
case AnomalyKind::SourceConflict: return "source-conflict";
|
case AnomalyKind::SourceConflict: return "source-conflict";
|
||||||
case AnomalyKind::BsdlPinMissing: return "bsdl-pin-missing";
|
case AnomalyKind::BsdlPinMissing: return "bsdl-pin-missing";
|
||||||
|
case AnomalyKind::DiffPolaritySwap: return "diff-polarity-swap";
|
||||||
|
case AnomalyKind::DiffCrossIncomplete: return "diff-cross-incomplete";
|
||||||
}
|
}
|
||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
// Diff-pair suffix detection. Returns true and fills <stem, polarity> if
|
// Diff-pair suffix detection. Returns true and fills <stem, polarity> if
|
||||||
// `name` ends with one of {_P, _N, _p, _n} preceded by a non-suffix char.
|
// `name` ends with one of {_P, _N, _p, _n} preceded by a non-suffix char.
|
||||||
// 'P' / 'N' result is normalised to uppercase.
|
// 'P' / 'N' result is normalised to uppercase. Shared with diff_check.cpp.
|
||||||
bool diff_suffix(const std::string &name, std::string &stem, char &pol) {
|
bool diff_suffix(const std::string &name, std::string &stem, char &pol) {
|
||||||
if (name.size() < 3) return false;
|
if (name.size() < 3) return false;
|
||||||
char last = name.back();
|
char last = name.back();
|
||||||
@@ -52,6 +52,29 @@ bool diff_suffix(const std::string &name, std::string &stem, char &pol) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tool-internal net names we never want to surface to the user (Mentor's
|
||||||
|
// `$Nxxxx` convention, ODS placeholders, etc.). Cheap prefix check.
|
||||||
|
bool is_internal_name(const std::string &n) {
|
||||||
|
return !n.empty() && n[0] == '$';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trailing-integer split: "MDI0" → ("MDI", 0); "PCIE_TX_3" → ("PCIE_TX_", 3);
|
||||||
|
// "USB" → false (no trailing digits). Used for diff-bus aggregation only —
|
||||||
|
// the strict `_` rule from `numeric_suffix` does NOT apply here because the
|
||||||
|
// caller has already stripped a `_P` / `_N` polarity suffix, so we know the
|
||||||
|
// remaining digits are an index rather than part of a longer name.
|
||||||
|
bool split_trailing_index(const std::string &s, std::string &outer, int &idx) {
|
||||||
|
if (s.empty()) return false;
|
||||||
|
size_t i = s.size();
|
||||||
|
while (i > 0 && std::isdigit((unsigned char)s[i - 1])) --i;
|
||||||
|
if (i == s.size() || i == 0) return false;
|
||||||
|
idx = std::atoi(s.c_str() + i);
|
||||||
|
outer = s.substr(0, i);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
// Bus suffix detection. Two accepted forms:
|
// Bus suffix detection. Two accepted forms:
|
||||||
// - bracketed: NAME[12] → stem "NAME", idx 12
|
// - bracketed: NAME[12] → stem "NAME", idx 12
|
||||||
// - underscore: NAME_12 → stem "NAME_", idx 12 (underscore is REQUIRED
|
// - underscore: NAME_12 → stem "NAME_", idx 12 (underscore is REQUIRED
|
||||||
@@ -81,27 +104,6 @@ bool numeric_suffix(const std::string &name, std::string &stem, int &idx,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tool-internal net names we never want to surface to the user (Mentor's
|
|
||||||
// `$Nxxxx` convention, ODS placeholders, etc.). Cheap prefix check.
|
|
||||||
bool is_internal_name(const std::string &n) {
|
|
||||||
return !n.empty() && n[0] == '$';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trailing-integer split: "MDI0" → ("MDI", 0); "PCIE_TX_3" → ("PCIE_TX_", 3);
|
|
||||||
// "USB" → false (no trailing digits). Used for diff-bus aggregation only —
|
|
||||||
// the strict `_` rule from `numeric_suffix` does NOT apply here because the
|
|
||||||
// caller has already stripped a `_P` / `_N` polarity suffix, so we know the
|
|
||||||
// remaining digits are an index rather than part of a longer name.
|
|
||||||
bool split_trailing_index(const std::string &s, std::string &outer, int &idx) {
|
|
||||||
if (s.empty()) return false;
|
|
||||||
size_t i = s.size();
|
|
||||||
while (i > 0 && std::isdigit((unsigned char)s[i - 1])) --i;
|
|
||||||
if (i == s.size() || i == 0) return false;
|
|
||||||
idx = std::atoi(s.c_str() + i);
|
|
||||||
outer = s.substr(0, i);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void analyse_module(Module *mod, AnalysisReport &out) {
|
void analyse_module(Module *mod, AnalysisReport &out) {
|
||||||
// ---- Pass 1: diff pairs ----
|
// ---- Pass 1: diff pairs ----
|
||||||
std::unordered_map<std::string, std::pair<Signal *, Signal *>> dp; // stem -> {P, N}
|
std::unordered_map<std::string, std::pair<Signal *, Signal *>> dp; // stem -> {P, N}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ enum class AnomalyKind {
|
|||||||
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).
|
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.
|
BsdlPinMissing, ///< A BSDL power/ground port has no pin on the netlist part.
|
||||||
|
DiffPolaritySwap, ///< A diff pair crosses a connection with P and N swapped.
|
||||||
|
DiffCrossIncomplete, ///< A diff pair/bus only partially crosses a connection.
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Anomaly {
|
struct Anomaly {
|
||||||
@@ -73,6 +75,15 @@ inline constexpr int POWER_FANOUT_CONFIRM_MIN = 4; ///< ≥ this confirms Powe
|
|||||||
|
|
||||||
bool has_voltage_pattern(const std::string &name);
|
bool has_voltage_pattern(const std::string &name);
|
||||||
|
|
||||||
|
// Name-parsing helpers shared with the diff-crossing check (diff_check.cpp).
|
||||||
|
// diff_suffix: true if `name` ends with _P/_N (case-insensitive); fills the
|
||||||
|
// stem and the polarity normalised to uppercase 'P'/'N'.
|
||||||
|
// split_trailing_index: "MDI0" → ("MDI", 0); false without trailing digits.
|
||||||
|
// is_internal_name: tool-internal net names never surfaced ($Nxxxx …).
|
||||||
|
bool diff_suffix(const std::string &name, std::string &stem, char &pol);
|
||||||
|
bool split_trailing_index(const std::string &s, std::string &outer, int &idx);
|
||||||
|
bool is_internal_name(const std::string &n);
|
||||||
|
|
||||||
// Best-effort signal-type inference. Sets `Signal::type`:
|
// Best-effort signal-type inference. Sets `Signal::type`:
|
||||||
// - GndShield when the name unambiguously matches GND/SHIELD/CHASSIS/EARTH.
|
// - GndShield when the name unambiguously matches GND/SHIELD/CHASSIS/EARTH.
|
||||||
// - Power when the name suggests Power AND there is structural evidence
|
// - Power when the name suggests Power AND there is structural evidence
|
||||||
|
|||||||
194
src/core/domain/diff_check.cpp
Normal file
194
src/core/domain/diff_check.cpp
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
#include "diff_check.hpp"
|
||||||
|
|
||||||
|
#include "modules.hpp"
|
||||||
|
#include "signals.hpp"
|
||||||
|
#include "system.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <map>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// One complete local diff pair with the net ids of its two legs.
|
||||||
|
struct LocalPair {
|
||||||
|
Module *mod = nullptr;
|
||||||
|
std::string stem;
|
||||||
|
Signal *p = nullptr, *n = nullptr;
|
||||||
|
int np = -1, nn = -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string pair_label(const LocalPair &lp) {
|
||||||
|
return lp.mod->name + "/" + lp.stem + "_P/N";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::vector<Anomaly> check_diff_crossings(System *sys,
|
||||||
|
const std::vector<Net> *nets)
|
||||||
|
{
|
||||||
|
std::vector<Anomaly> out;
|
||||||
|
if (!sys || !nets) return out;
|
||||||
|
|
||||||
|
// Signal → net id (compute_all_nets covers every signal, singletons too).
|
||||||
|
std::unordered_map<Signal *, int> net_of;
|
||||||
|
for (size_t i = 0; i < nets->size(); ++i)
|
||||||
|
for (const auto &mp : (*nets)[i].members)
|
||||||
|
net_of[mp.second] = (int)i;
|
||||||
|
|
||||||
|
// Complete local pairs, module by module. Orphan halves (X_P without
|
||||||
|
// X_N) are analysis's DiffPairOrphan business — skipped here.
|
||||||
|
std::vector<LocalPair> pairs;
|
||||||
|
std::unordered_map<Signal *, int> pair_of; // leg signal → index in `pairs`
|
||||||
|
for (auto &mkv : *sys->modules()) {
|
||||||
|
Module *mod = mkv.second;
|
||||||
|
std::map<std::string, LocalPair> by_stem;
|
||||||
|
for (auto &skv : *mod->signals) {
|
||||||
|
if (is_internal_name(skv.first)) continue;
|
||||||
|
std::string stem; char pol;
|
||||||
|
if (!diff_suffix(skv.first, stem, pol)) continue;
|
||||||
|
LocalPair &lp = by_stem[stem];
|
||||||
|
lp.mod = mod; lp.stem = stem;
|
||||||
|
if (pol == 'P') lp.p = skv.second;
|
||||||
|
else lp.n = skv.second;
|
||||||
|
}
|
||||||
|
for (auto &kv : by_stem) {
|
||||||
|
LocalPair lp = kv.second;
|
||||||
|
if (!lp.p || !lp.n) continue;
|
||||||
|
auto ip = net_of.find(lp.p), in = net_of.find(lp.n);
|
||||||
|
if (ip == net_of.end() || in == net_of.end()) continue;
|
||||||
|
lp.np = ip->second;
|
||||||
|
lp.nn = in->second;
|
||||||
|
int idx = (int)pairs.size();
|
||||||
|
pairs.push_back(lp);
|
||||||
|
pair_of[lp.p] = idx;
|
||||||
|
pair_of[lp.n] = idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 1 — pair against pair. Each unordered couple of pairs is judged
|
||||||
|
// once (dedup set), so A↔B is never also reported as B↔A.
|
||||||
|
std::set<std::pair<int, int>> seen;
|
||||||
|
for (int i = 0; i < (int)pairs.size(); ++i) {
|
||||||
|
const LocalPair &a = pairs[i];
|
||||||
|
if (a.np == a.nn) {
|
||||||
|
// Degenerate: both legs land on one net — only the connections
|
||||||
|
// can do that (two module-local signals are distinct by nature).
|
||||||
|
Anomaly an;
|
||||||
|
an.kind = AnomalyKind::DiffPolaritySwap;
|
||||||
|
an.module = a.mod;
|
||||||
|
an.message = a.mod->name + ": " + a.stem + "_P and " + a.stem
|
||||||
|
+ "_N join the same net (through the connections)";
|
||||||
|
an.involved = {a.p, a.n};
|
||||||
|
out.push_back(std::move(an));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Candidate peers: pairs of OTHER modules with a leg on net np or nn.
|
||||||
|
std::set<int> cands;
|
||||||
|
for (int net : {a.np, a.nn})
|
||||||
|
for (const auto &mp : (*nets)[net].members) {
|
||||||
|
auto it = pair_of.find(mp.second);
|
||||||
|
if (it == pair_of.end()) continue;
|
||||||
|
const LocalPair &b = pairs[it->second];
|
||||||
|
if (b.mod == a.mod) continue; // intra-module: nothing to say
|
||||||
|
if (b.np == b.nn) continue; // degenerate: own anomaly above
|
||||||
|
cands.insert(it->second);
|
||||||
|
}
|
||||||
|
for (int j : cands) {
|
||||||
|
std::pair<int, int> key = std::minmax(i, j);
|
||||||
|
if (!seen.insert(key).second) continue;
|
||||||
|
const LocalPair &b = pairs[j];
|
||||||
|
if (a.np == b.np && a.nn == b.nn) continue; // straight: all good
|
||||||
|
Anomaly an;
|
||||||
|
an.module = a.mod;
|
||||||
|
an.involved = {a.p, a.n, b.p, b.n};
|
||||||
|
if (a.np == b.nn && a.nn == b.np) {
|
||||||
|
an.kind = AnomalyKind::DiffPolaritySwap;
|
||||||
|
an.message = pair_label(a) + " <-> " + pair_label(b)
|
||||||
|
+ ": polarity swapped (P legs meet N legs)";
|
||||||
|
} else {
|
||||||
|
an.kind = AnomalyKind::DiffCrossIncomplete;
|
||||||
|
std::string how;
|
||||||
|
if (a.np == b.np) how = "only the P legs are bridged";
|
||||||
|
else if (a.nn == b.nn) how = "only the N legs are bridged";
|
||||||
|
else if (a.np == b.nn) how = "P leg bridged to N leg; "
|
||||||
|
"the other legs are not";
|
||||||
|
else how = "N leg bridged to P leg; "
|
||||||
|
"the other legs are not";
|
||||||
|
an.message = pair_label(a) + " <-> " + pair_label(b)
|
||||||
|
+ ": " + how;
|
||||||
|
}
|
||||||
|
out.push_back(std::move(an));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 2 — diff buses at a crossing. Lanes grouped by outer stem. A lane
|
||||||
|
// is "dangling" when it crosses to NO module at all while sibling lanes
|
||||||
|
// do cross — distributed buses (lanes fanned out to different peers, a
|
||||||
|
// backplane classic) are legitimate and stay silent. Lanes crossing
|
||||||
|
// partially are already reported above, so they don't count as dangling.
|
||||||
|
// One aggregated anomaly per bus, per side (each side names its lanes).
|
||||||
|
std::map<std::pair<Module *, std::string>, std::map<int, int>> groups;
|
||||||
|
for (int i = 0; i < (int)pairs.size(); ++i) {
|
||||||
|
std::string outer; int idx;
|
||||||
|
if (!split_trailing_index(pairs[i].stem, outer, idx)) continue;
|
||||||
|
groups[{pairs[i].mod, outer}][idx] = i;
|
||||||
|
}
|
||||||
|
for (auto &gkv : groups) {
|
||||||
|
auto &lanes = gkv.second; // lane index → pair index
|
||||||
|
if (lanes.size() < 2) continue;
|
||||||
|
std::set<int> touching_any; // lanes sharing ≥1 net with a peer
|
||||||
|
std::set<int> complete_any; // lanes fully crossing somewhere
|
||||||
|
std::set<Module *> reached;
|
||||||
|
for (auto &lkv : lanes) {
|
||||||
|
const LocalPair &a = pairs[lkv.second];
|
||||||
|
if (a.np == a.nn) continue;
|
||||||
|
for (int net : {a.np, a.nn})
|
||||||
|
for (const auto &mp : (*nets)[net].members) {
|
||||||
|
auto it = pair_of.find(mp.second);
|
||||||
|
if (it == pair_of.end()) continue;
|
||||||
|
const LocalPair &b = pairs[it->second];
|
||||||
|
if (b.mod == a.mod) continue;
|
||||||
|
touching_any.insert(lkv.first);
|
||||||
|
bool straight = (a.np == b.np && a.nn == b.nn);
|
||||||
|
bool swapped = (a.np == b.nn && a.nn == b.np);
|
||||||
|
if (straight || swapped) {
|
||||||
|
complete_any.insert(lkv.first);
|
||||||
|
reached.insert(b.mod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (complete_any.empty()) continue; // fully local bus: fine
|
||||||
|
std::vector<int> dangling;
|
||||||
|
for (auto &lkv : lanes)
|
||||||
|
if (!touching_any.count(lkv.first))
|
||||||
|
dangling.push_back(lkv.first);
|
||||||
|
if (dangling.empty()) continue;
|
||||||
|
int lo = lanes.begin()->first;
|
||||||
|
int hi = lanes.rbegin()->first;
|
||||||
|
Anomaly an;
|
||||||
|
an.kind = AnomalyKind::DiffCrossIncomplete;
|
||||||
|
an.module = gkv.first.first;
|
||||||
|
std::string m = gkv.first.first->name + ": " + gkv.first.second
|
||||||
|
+ "[" + std::to_string(lo) + ".."
|
||||||
|
+ std::to_string(hi) + "]_P/N: lane(s)";
|
||||||
|
for (int ix : dangling) m += " " + std::to_string(ix);
|
||||||
|
m += " do not cross (others reach";
|
||||||
|
std::vector<std::string> names;
|
||||||
|
for (Module *mod : reached) names.push_back(mod->name);
|
||||||
|
std::sort(names.begin(), names.end());
|
||||||
|
for (const std::string &nm : names) m += " " + nm;
|
||||||
|
m += ")";
|
||||||
|
an.message = std::move(m);
|
||||||
|
for (auto &lkv : lanes) {
|
||||||
|
an.involved.push_back(pairs[lkv.second].p);
|
||||||
|
an.involved.push_back(pairs[lkv.second].n);
|
||||||
|
}
|
||||||
|
out.push_back(std::move(an));
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
27
src/core/domain/diff_check.hpp
Normal file
27
src/core/domain/diff_check.hpp
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#ifndef _DIFF_CHECK_HPP_
|
||||||
|
#define _DIFF_CHECK_HPP_
|
||||||
|
|
||||||
|
#include "analysis.hpp" // Anomaly, diff_suffix, split_trailing_index
|
||||||
|
#include "nets.hpp" // Net
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class System;
|
||||||
|
|
||||||
|
// Differential-pair crossing checks. Every complete local diff pair
|
||||||
|
// (X_P / X_N, name-based) resolves its two legs to two bridged nets; any
|
||||||
|
// other module whose own pair sits on those nets must match them leg for
|
||||||
|
// leg. Findings:
|
||||||
|
// - DiffPolaritySwap: the peer pair is wired P→N / N→P, or a pair's two
|
||||||
|
// legs end up joined onto one single net through the connections.
|
||||||
|
// - DiffCrossIncomplete: the two pairs share only one leg (the other does
|
||||||
|
// not cross), or some lanes of a diff bus do not reach a module the
|
||||||
|
// other lanes reach.
|
||||||
|
// Name-based on BOTH sides: a peer whose signals carry no _P/_N suffix is
|
||||||
|
// not judged (silent). Polarity swaps are sometimes intentional (routing
|
||||||
|
// compensation, SerDes with configurable polarity) — these are findings to
|
||||||
|
// review, not hard errors. `nets` must come from compute_all_nets(sys).
|
||||||
|
std::vector<Anomaly> check_diff_crossings(System *sys,
|
||||||
|
const std::vector<Net> *nets);
|
||||||
|
|
||||||
|
#endif // _DIFF_CHECK_HPP_
|
||||||
@@ -259,6 +259,7 @@ void Tui::RegisterCommands() {
|
|||||||
render(r.jtag_anomalies, " JTAG chain anomaly(ies).");
|
render(r.jtag_anomalies, " JTAG chain anomaly(ies).");
|
||||||
render(r.conflict_anomalies, " source-conflict(s).");
|
render(r.conflict_anomalies, " source-conflict(s).");
|
||||||
render(r.completeness_anomalies, " BSDL completeness issue(s).");
|
render(r.completeness_anomalies, " BSDL completeness issue(s).");
|
||||||
|
render(r.diff_anomalies, " diff-pair crossing anomaly(ies).");
|
||||||
}, true,
|
}, true,
|
||||||
"check pin roles, power/gnd net consistency, and (with BSDL) pin and JTAG checks" };
|
"check pin roles, power/gnd net consistency, and (with BSDL) pin and JTAG checks" };
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ Component Tui::BuildAnalyzeScreen() {
|
|||||||
push_anoms(vr.jtag_anomalies);
|
push_anoms(vr.jtag_anomalies);
|
||||||
push_anoms(vr.conflict_anomalies);
|
push_anoms(vr.conflict_anomalies);
|
||||||
push_anoms(vr.completeness_anomalies);
|
push_anoms(vr.completeness_anomalies);
|
||||||
|
push_anoms(vr.diff_anomalies);
|
||||||
int n_model = vr.model_total();
|
int n_model = vr.model_total();
|
||||||
|
|
||||||
if (analyze_issues.empty()) analyze_issues.push_back("(no issue found)");
|
if (analyze_issues.empty()) analyze_issues.push_back("(no issue found)");
|
||||||
|
|||||||
@@ -647,6 +647,7 @@ void EssimFrame::OnVerify(wxCommandEvent &) {
|
|||||||
log_anoms(r.jtag_anomalies, "JTAG chain anomaly(ies)");
|
log_anoms(r.jtag_anomalies, "JTAG chain anomaly(ies)");
|
||||||
log_anoms(r.conflict_anomalies, "source-conflict(s)");
|
log_anoms(r.conflict_anomalies, "source-conflict(s)");
|
||||||
log_anoms(r.completeness_anomalies, "BSDL completeness issue(s)");
|
log_anoms(r.completeness_anomalies, "BSDL completeness issue(s)");
|
||||||
|
log_anoms(r.diff_anomalies, "diff-pair crossing anomaly(ies)");
|
||||||
|
|
||||||
RebuildModelView();
|
RebuildModelView();
|
||||||
}
|
}
|
||||||
|
|||||||
184
tests/test_diff_check.cpp
Normal file
184
tests/test_diff_check.cpp
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
#include <doctest/doctest.h>
|
||||||
|
|
||||||
|
#include "core/app/verify.hpp"
|
||||||
|
#include "core/domain/analysis.hpp"
|
||||||
|
#include "core/domain/connect.hpp"
|
||||||
|
#include "core/domain/modules.hpp"
|
||||||
|
#include "core/domain/parts.hpp"
|
||||||
|
#include "core/domain/pins.hpp"
|
||||||
|
#include "core/domain/signals.hpp"
|
||||||
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
|
// check_diff_crossings: a complete local diff pair (X_P / X_N) must cross a
|
||||||
|
// connection leg for leg. Both sides are judged by name only.
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// New pin on `part`, wired to (or creating) module signal `sig_name`.
|
||||||
|
Pin *wire(Module *m, Part *p, const std::string &pin_name,
|
||||||
|
const std::string &sig_name)
|
||||||
|
{
|
||||||
|
Pin *pin = new Pin(pin_name);
|
||||||
|
p->add(pin);
|
||||||
|
Signal *s = m->signals->merge(sig_name);
|
||||||
|
s->add(pin);
|
||||||
|
pin->connect(s);
|
||||||
|
return pin;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Rig {
|
||||||
|
System sys;
|
||||||
|
Module *a, *b;
|
||||||
|
Part *ja, *jb;
|
||||||
|
Rig() {
|
||||||
|
a = sys.modules()->merge("A");
|
||||||
|
b = sys.modules()->merge("B");
|
||||||
|
ja = new Part("J1"); a->add(ja);
|
||||||
|
jb = new Part("P1"); b->add(jb);
|
||||||
|
}
|
||||||
|
void bridge(std::initializer_list<std::pair<Pin *, Pin *>> wires) {
|
||||||
|
Connection *c = new Connection("A/J1 <-> B/P1", a, ja, b, jb);
|
||||||
|
c->transform_name = "identity";
|
||||||
|
for (const auto &w : wires) c->pin_map.push_back(w);
|
||||||
|
sys.connections()->add(c);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("diff crossing: straight P↔P / N↔N is silent") {
|
||||||
|
Rig r;
|
||||||
|
Pin *ap = wire(r.a, r.ja, "1", "TX_P");
|
||||||
|
Pin *an = wire(r.a, r.ja, "2", "TX_N");
|
||||||
|
Pin *bp = wire(r.b, r.jb, "1", "RX_P");
|
||||||
|
Pin *bn = wire(r.b, r.jb, "2", "RX_N");
|
||||||
|
r.bridge({{ap, bp}, {an, bn}});
|
||||||
|
|
||||||
|
app::VerifyReport vr = app::verify(&r.sys);
|
||||||
|
CHECK(vr.diff_anomalies.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("diff crossing: swapped legs report ONE polarity-swap anomaly") {
|
||||||
|
Rig r;
|
||||||
|
Pin *ap = wire(r.a, r.ja, "1", "TX_P");
|
||||||
|
Pin *an = wire(r.a, r.ja, "2", "TX_N");
|
||||||
|
Pin *bp = wire(r.b, r.jb, "1", "RX_P");
|
||||||
|
Pin *bn = wire(r.b, r.jb, "2", "RX_N");
|
||||||
|
r.bridge({{ap, bn}, {an, bp}}); // crossed on purpose
|
||||||
|
|
||||||
|
app::VerifyReport vr = app::verify(&r.sys);
|
||||||
|
REQUIRE(vr.diff_anomalies.size() == 1); // deduped: not once per side
|
||||||
|
CHECK(vr.diff_anomalies[0].kind == AnomalyKind::DiffPolaritySwap);
|
||||||
|
CHECK(vr.diff_anomalies[0].message.find("polarity swapped")
|
||||||
|
!= std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("diff crossing: a single bridged leg reports incomplete") {
|
||||||
|
Rig r;
|
||||||
|
Pin *ap = wire(r.a, r.ja, "1", "TX_P");
|
||||||
|
wire(r.a, r.ja, "2", "TX_N"); // N leg stays local
|
||||||
|
Pin *bp = wire(r.b, r.jb, "1", "RX_P");
|
||||||
|
wire(r.b, r.jb, "2", "RX_N"); // peer N leg exists, unbridged
|
||||||
|
r.bridge({{ap, bp}});
|
||||||
|
|
||||||
|
app::VerifyReport vr = app::verify(&r.sys);
|
||||||
|
REQUIRE(vr.diff_anomalies.size() == 1);
|
||||||
|
CHECK(vr.diff_anomalies[0].kind == AnomalyKind::DiffCrossIncomplete);
|
||||||
|
CHECK(vr.diff_anomalies[0].message.find("only the P legs")
|
||||||
|
!= std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("diff crossing: an unsuffixed peer is not judged") {
|
||||||
|
Rig r;
|
||||||
|
Pin *ap = wire(r.a, r.ja, "1", "TX_P");
|
||||||
|
Pin *an = wire(r.a, r.ja, "2", "TX_N");
|
||||||
|
Pin *b1 = wire(r.b, r.jb, "1", "RXP"); // no _P/_N suffix
|
||||||
|
Pin *b2 = wire(r.b, r.jb, "2", "RXN");
|
||||||
|
r.bridge({{ap, b2}, {an, b1}}); // even crossed: silent
|
||||||
|
|
||||||
|
app::VerifyReport vr = app::verify(&r.sys);
|
||||||
|
CHECK(vr.diff_anomalies.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("diff crossing: P and N joined onto one net is flagged") {
|
||||||
|
Rig r;
|
||||||
|
Pin *ap = wire(r.a, r.ja, "1", "TX_P");
|
||||||
|
Pin *an = wire(r.a, r.ja, "2", "TX_N");
|
||||||
|
Pin *b1 = wire(r.b, r.jb, "1", "X");
|
||||||
|
Pin *b2 = wire(r.b, r.jb, "2", "X"); // same peer signal
|
||||||
|
r.bridge({{ap, b1}, {an, b2}});
|
||||||
|
|
||||||
|
app::VerifyReport vr = app::verify(&r.sys);
|
||||||
|
REQUIRE(vr.diff_anomalies.size() == 1);
|
||||||
|
CHECK(vr.diff_anomalies[0].kind == AnomalyKind::DiffPolaritySwap);
|
||||||
|
CHECK(vr.diff_anomalies[0].message.find("join the same net")
|
||||||
|
!= std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("diff bus crossing: a dangling lane is listed, once per side") {
|
||||||
|
Rig r;
|
||||||
|
// Two lanes on each side; only lane 0 is bridged — lane 1 crosses nowhere.
|
||||||
|
Pin *a0p = wire(r.a, r.ja, "1", "TX0_P");
|
||||||
|
Pin *a0n = wire(r.a, r.ja, "2", "TX0_N");
|
||||||
|
wire(r.a, r.ja, "3", "TX1_P");
|
||||||
|
wire(r.a, r.ja, "4", "TX1_N");
|
||||||
|
Pin *b0p = wire(r.b, r.jb, "1", "RX0_P");
|
||||||
|
Pin *b0n = wire(r.b, r.jb, "2", "RX0_N");
|
||||||
|
wire(r.b, r.jb, "3", "RX1_P");
|
||||||
|
wire(r.b, r.jb, "4", "RX1_N");
|
||||||
|
r.bridge({{a0p, b0p}, {a0n, b0n}});
|
||||||
|
|
||||||
|
app::VerifyReport vr = app::verify(&r.sys);
|
||||||
|
// One aggregated anomaly per side (each names its own lane signals).
|
||||||
|
REQUIRE(vr.diff_anomalies.size() == 2);
|
||||||
|
for (const auto &an : vr.diff_anomalies) {
|
||||||
|
CHECK(an.kind == AnomalyKind::DiffCrossIncomplete);
|
||||||
|
CHECK(an.message.find("lane(s) 1 do not cross") != std::string::npos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("diff bus crossing: a distributed bus (lanes fanned out) is silent") {
|
||||||
|
// A's two lanes go to two DIFFERENT modules — legitimate backplane fan-out.
|
||||||
|
System sys;
|
||||||
|
Module *a = sys.modules()->merge("A");
|
||||||
|
Module *b = sys.modules()->merge("B");
|
||||||
|
Module *c = sys.modules()->merge("C");
|
||||||
|
Part *ja = new Part("J1"); a->add(ja);
|
||||||
|
Part *jb = new Part("P1"); b->add(jb);
|
||||||
|
Part *jc = new Part("P1"); c->add(jc);
|
||||||
|
Pin *a0p = wire(a, ja, "1", "TX0_P");
|
||||||
|
Pin *a0n = wire(a, ja, "2", "TX0_N");
|
||||||
|
Pin *a1p = wire(a, ja, "3", "TX1_P");
|
||||||
|
Pin *a1n = wire(a, ja, "4", "TX1_N");
|
||||||
|
Pin *b0p = wire(b, jb, "1", "RX0_P");
|
||||||
|
Pin *b0n = wire(b, jb, "2", "RX0_N");
|
||||||
|
Pin *c0p = wire(c, jc, "1", "RX0_P");
|
||||||
|
Pin *c0n = wire(c, jc, "2", "RX0_N");
|
||||||
|
|
||||||
|
Connection *cb = new Connection("A/J1 <-> B/P1", a, ja, b, jb);
|
||||||
|
cb->transform_name = "identity";
|
||||||
|
cb->pin_map.emplace_back(a0p, b0p);
|
||||||
|
cb->pin_map.emplace_back(a0n, b0n);
|
||||||
|
sys.connections()->add(cb);
|
||||||
|
Connection *cc = new Connection("A/J1 <-> C/P1", a, ja, c, jc);
|
||||||
|
cc->transform_name = "identity";
|
||||||
|
cc->pin_map.emplace_back(a1p, c0p);
|
||||||
|
cc->pin_map.emplace_back(a1n, c0n);
|
||||||
|
sys.connections()->add(cc);
|
||||||
|
|
||||||
|
app::VerifyReport vr = app::verify(&sys);
|
||||||
|
CHECK(vr.diff_anomalies.empty()); // every lane crosses somewhere
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("diff crossing: empty / unconnected systems are silent") {
|
||||||
|
System sys;
|
||||||
|
app::VerifyReport vr = app::verify(&sys);
|
||||||
|
CHECK(vr.diff_anomalies.empty());
|
||||||
|
|
||||||
|
// A pair that never crosses anything: silent (local pairs are fine).
|
||||||
|
Rig r;
|
||||||
|
wire(r.a, r.ja, "1", "TX_P");
|
||||||
|
wire(r.a, r.ja, "2", "TX_N");
|
||||||
|
vr = app::verify(&r.sys);
|
||||||
|
CHECK(vr.diff_anomalies.empty());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user