Compare commits
5 Commits
c2b1f4c4ae
...
dceb61237d
| Author | SHA1 | Date | |
|---|---|---|---|
| dceb61237d | |||
| 0b10e1c1b7 | |||
| 9cf43696a2 | |||
| e914c84c18 | |||
| 1943f1f88a |
@@ -36,6 +36,7 @@ LoadResult load_module(System *sys, const std::string &module_name,
|
||||
r.power = inf.power;
|
||||
r.gnd = inf.gnd;
|
||||
r.kept_other = inf.kept_other;
|
||||
r.mgmt = inf.mgmt;
|
||||
r.ok = true;
|
||||
} catch (const std::exception &e) {
|
||||
r.error = e.what();
|
||||
|
||||
@@ -23,6 +23,7 @@ struct LoadResult {
|
||||
int power = 0; ///< signals inferred Power (name + structure)
|
||||
int gnd = 0; ///< signals inferred GndShield (name)
|
||||
int kept_other = 0; ///< name said Power but evidence too weak → kept Other
|
||||
int mgmt = 0; ///< power-management signal (rail + SENSE/EN/PG/… token) → Other, not suspect
|
||||
};
|
||||
|
||||
// Import a module from a netlist/pinout file into `sys`, drop singleton signals,
|
||||
|
||||
@@ -150,6 +150,7 @@ private:
|
||||
grp(r.jtag_anomalies, " JTAG chain anomaly(ies).");
|
||||
grp(r.conflict_anomalies, " source-conflict(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.
|
||||
@@ -189,7 +190,9 @@ private:
|
||||
+ " singleton/NC signal(s))" : ""));
|
||||
emit(" types: " + std::to_string(r.power) + " power, "
|
||||
+ std::to_string(r.gnd) + " gnd, " + std::to_string(r.kept_other)
|
||||
+ " suspect Power (name only — kept as Other)");
|
||||
+ " suspect Power (name only — kept as Other), "
|
||||
+ std::to_string(r.mgmt)
|
||||
+ " power-management (control/measure — kept as Other)");
|
||||
return true;
|
||||
}
|
||||
if (cmd == "connect" || cmd == "plug") {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "core/domain/bsdl_check.hpp"
|
||||
#include "core/domain/connect.hpp"
|
||||
#include "core/domain/diff_check.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/nets.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});
|
||||
}
|
||||
|
||||
// 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.jtag_anomalies = check_jtag_chain(sys, &nets);
|
||||
r.conflict_anomalies = check_source_conflicts(sys);
|
||||
r.completeness_anomalies = check_bsdl_completeness(sys);
|
||||
r.diff_anomalies = check_diff_crossings(sys, &nets);
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -52,11 +52,13 @@ struct VerifyReport {
|
||||
std::vector<Anomaly> jtag_anomalies; ///< check_jtag_chain
|
||||
std::vector<Anomaly> conflict_anomalies; ///< check_source_conflicts
|
||||
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 model_total() const {
|
||||
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::SourceConflict: return "source-conflict";
|
||||
case AnomalyKind::BsdlPinMissing: return "bsdl-pin-missing";
|
||||
case AnomalyKind::DiffPolaritySwap: return "diff-polarity-swap";
|
||||
case AnomalyKind::DiffCrossIncomplete: return "diff-cross-incomplete";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// 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.
|
||||
// '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) {
|
||||
if (name.size() < 3) return false;
|
||||
char last = name.back();
|
||||
@@ -52,6 +52,29 @@ bool diff_suffix(const std::string &name, std::string &stem, char &pol) {
|
||||
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:
|
||||
// - bracketed: NAME[12] → stem "NAME", idx 12
|
||||
// - 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;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// ---- Pass 1: diff pairs ----
|
||||
std::unordered_map<std::string, std::pair<Signal *, Signal *>> dp; // stem -> {P, N}
|
||||
@@ -276,13 +278,21 @@ SignalTypeInferenceStats infer_signal_types(System *sys) {
|
||||
Module *mod = mkv.second;
|
||||
for (auto &skv : *mod->signals) {
|
||||
Signal *s = skv.second;
|
||||
SignalType named = infer_signal_type(s->name);
|
||||
if (named == SignalType::GndShield) {
|
||||
NameClassification ncl = classify_signal_name(s->name);
|
||||
if (ncl.verdict == NameVerdict::GndShield) {
|
||||
s->type = SignalType::GndShield;
|
||||
++st.gnd;
|
||||
continue;
|
||||
}
|
||||
if (named == SignalType::Power) {
|
||||
if (ncl.verdict == NameVerdict::PowerMgmt) {
|
||||
// A rail token next to a control token (SENSE, EN, PG, …):
|
||||
// a signal about a rail, confidently NOT the rail — never
|
||||
// suspect, whatever the fan-out.
|
||||
s->type = SignalType::Other;
|
||||
++st.mgmt;
|
||||
continue;
|
||||
}
|
||||
if (ncl.verdict == NameVerdict::Rail) {
|
||||
int fanout = (int)s->size();
|
||||
// Hard rule: a "power" net that touches fewer than three
|
||||
// pins cannot physically be a rail (a real rail goes to
|
||||
|
||||
@@ -39,6 +39,8 @@ enum class AnomalyKind {
|
||||
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.
|
||||
DiffPolaritySwap, ///< A diff pair crosses a connection with P and N swapped.
|
||||
DiffCrossIncomplete, ///< A diff pair/bus only partially crosses a connection.
|
||||
};
|
||||
|
||||
struct Anomaly {
|
||||
@@ -62,6 +64,8 @@ struct SignalTypeInferenceStats {
|
||||
int power = 0; ///< Signals promoted to Power (name + structural).
|
||||
int gnd = 0; ///< Signals promoted to GndShield (name only).
|
||||
int kept_other = 0; ///< Name said Power but structural evidence too weak.
|
||||
int mgmt = 0; ///< Power-management signal (rail + SENSE/EN/PG/…
|
||||
///< token) → confidently Other, never suspect.
|
||||
};
|
||||
|
||||
// Thresholds used by `infer_signal_types` (re-exposed so the analyze screen
|
||||
@@ -71,6 +75,15 @@ inline constexpr int POWER_FANOUT_CONFIRM_MIN = 4; ///< ≥ this confirms Powe
|
||||
|
||||
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`:
|
||||
// - GndShield when the name unambiguously matches GND/SHIELD/CHASSIS/EARTH.
|
||||
// - 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_
|
||||
@@ -5,8 +5,25 @@
|
||||
|
||||
enum class SignalType { Power, GndShield, Other };
|
||||
|
||||
// Name-level verdict, richer than SignalType. `PowerMgmt` is the key
|
||||
// addition: a name holding BOTH a rail token (VCC/VDD/PWR/…) and a control
|
||||
// token (SENSE/EN/PG/FB/…) is a power-management signal — measurement,
|
||||
// enable, power-good — not the rail itself. Its non-Power classification is
|
||||
// therefore confident, where a bare rail name without structural evidence
|
||||
// stays suspect.
|
||||
enum class NameVerdict { Rail, GndShield, PowerMgmt, Other };
|
||||
|
||||
struct NameClassification {
|
||||
NameVerdict verdict = NameVerdict::Other;
|
||||
std::string token; ///< PowerMgmt only: the control token that decided it.
|
||||
};
|
||||
|
||||
NameClassification classify_signal_name(const std::string &signal_name);
|
||||
|
||||
const char *signal_type_name(SignalType t);
|
||||
bool signal_type_from_name(const std::string &s, SignalType &out);
|
||||
// Thin wrapper over classify_signal_name: Rail → Power, GndShield → GndShield,
|
||||
// PowerMgmt/Other → Other.
|
||||
SignalType infer_signal_type(const std::string &signal_name);
|
||||
SignalType next_signal_type(SignalType t); // Power → GndShield → Other → Power
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
const char *signal_type_name(SignalType t) {
|
||||
@@ -36,11 +37,74 @@ bool signal_type_from_name(const std::string &s, SignalType &out) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Heuristic. Names like GND, GNDA, SHIELD, CHASSIS → GndShield.
|
||||
// Names containing PWR/POWER/VCC/VDD/VEE/VSS, or matching V±N or +N.NV
|
||||
// patterns, or starting with VS_/VS3_ → Power. Else Other.
|
||||
SignalType infer_signal_type(const std::string &name) {
|
||||
if (name.empty()) return SignalType::Other;
|
||||
namespace {
|
||||
|
||||
// Control/monitoring vocabulary: a name holding both a rail token and one of
|
||||
// these is a signal ABOUT a rail (feedback, enable, power-good, fault, …) —
|
||||
// not the rail itself. Matched against whole separator-delimited tokens
|
||||
// (uppercase, trailing digits stripped so EN1/PG0 still hit). Entries of
|
||||
// length ≥ 4 also match as a token *suffix*, catching fused (VSENSE, PWRGOOD)
|
||||
// and active-low (NFAULT) forms; short entries match exactly, so GREEN or
|
||||
// SENSOR never trip on EN / SENSE.
|
||||
const char *const kPowerControlTokens[] = {
|
||||
"SENSE", "SNS", "KELVIN", // remote / Kelvin sense
|
||||
"FB", "FBK", "FDB", "FDBK", "VFB", // regulator feedback
|
||||
"FEEDBACK",
|
||||
"EN", "ENA", "ENABLE", "INH", "INHIBIT", // enable / inhibit
|
||||
"PG", "PGOOD", "PWRGD", "PWROK", // power-good
|
||||
"GOOD", "OK", "FAIL", "FAULT", "FLT", // status / fault
|
||||
"ALERT", "ALRT", "WARN",
|
||||
"MON", "IMON", "VMON", "PMON", // monitoring
|
||||
"DET", "DETECT", "PRSNT", "PRESENT", // presence detection
|
||||
"OC", "OCP", "OV", "OVP", "UV", "UVP", // protection trips
|
||||
"TRIP", "SHDN", "SHUTDOWN",
|
||||
"ADJ", "ADJUST", "VADJ", "TRIM", // regulator adjust / trim
|
||||
"MARG", "MARGIN", // voltage margining
|
||||
"SET", "VSET", "ISET", // set-point pins
|
||||
"SEQ", "CTRL", "CTL", "CMD", // sequencing / control / command
|
||||
"STAT", "STATUS",
|
||||
"ON", "OFF", "BTN", // on/off request
|
||||
"REF", "VREF", // voltage reference
|
||||
"LED", // indicator drive
|
||||
"CS", "IRQ",
|
||||
};
|
||||
|
||||
bool is_power_control_token(std::string tok) {
|
||||
while (!tok.empty() && std::isdigit((unsigned char)tok.back()))
|
||||
tok.pop_back(); // EN1, PG0, FB2 …
|
||||
if (tok.empty()) return false;
|
||||
for (const char *lex : kPowerControlTokens) {
|
||||
size_t n = std::strlen(lex);
|
||||
if (tok == lex) return true;
|
||||
if (n >= 4 && tok.size() > n
|
||||
&& tok.compare(tok.size() - n, n, lex) == 0)
|
||||
return true; // VSENSE, PWRGOOD, NFAULT …
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Split on every non-alphanumeric character. `u` is already uppercase.
|
||||
std::vector<std::string> alnum_tokens(const std::string &u) {
|
||||
std::vector<std::string> out;
|
||||
std::string cur;
|
||||
for (char c : u) {
|
||||
if (std::isalnum((unsigned char)c)) { cur += c; continue; }
|
||||
if (!cur.empty()) { out.push_back(std::move(cur)); cur.clear(); }
|
||||
}
|
||||
if (!cur.empty()) out.push_back(std::move(cur));
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Heuristic. Names like GND, GNDA, SHIELD, CHASSIS → GndShield (name alone is
|
||||
// reliable there — left out of the control-token logic on purpose). Names
|
||||
// containing PWR/POWER/VCC/VDD/VEE/VSS, or starting with VS_/VBAT/+/− → rail
|
||||
// candidates; a rail candidate whose tokens include a control word (SENSE,
|
||||
// EN, PG, …) is downgraded to PowerMgmt. Else Other.
|
||||
NameClassification classify_signal_name(const std::string &name) {
|
||||
NameClassification out;
|
||||
if (name.empty()) return out;
|
||||
std::string u = name;
|
||||
std::transform(u.begin(), u.end(), u.begin(),
|
||||
[](unsigned char c) { return std::toupper(c); });
|
||||
@@ -52,14 +116,14 @@ SignalType infer_signal_type(const std::string &name) {
|
||||
return u.rfind(needle, 0) == 0;
|
||||
};
|
||||
|
||||
if (u == "GND" || u == "GROUND") return SignalType::GndShield;
|
||||
if (starts_with("GND_")
|
||||
if (u == "GND" || u == "GROUND"
|
||||
|| starts_with("GND_")
|
||||
|| (starts_with("GND") && u.size() >= 4
|
||||
&& std::isalpha((unsigned char)u[3]))) {
|
||||
return SignalType::GndShield;
|
||||
&& std::isalpha((unsigned char)u[3]))
|
||||
|| contains("SHIELD") || contains("CHASSIS") || contains("EARTH")) {
|
||||
out.verdict = NameVerdict::GndShield;
|
||||
return out;
|
||||
}
|
||||
if (contains("SHIELD") || contains("CHASSIS") || contains("EARTH"))
|
||||
return SignalType::GndShield;
|
||||
|
||||
if (contains("PWR") || contains("POWER")
|
||||
|| contains("VCC") || contains("VDD") || contains("VEE") || contains("VSS")
|
||||
@@ -67,7 +131,25 @@ SignalType infer_signal_type(const std::string &name) {
|
||||
|| starts_with("VS3_") || starts_with("VS4_")
|
||||
|| starts_with("VBAT") || starts_with("VBUS")
|
||||
|| starts_with("+") || starts_with("-")) {
|
||||
return SignalType::Power;
|
||||
for (const std::string &tok : alnum_tokens(u)) {
|
||||
if (is_power_control_token(tok)) {
|
||||
out.verdict = NameVerdict::PowerMgmt;
|
||||
out.token = tok;
|
||||
return out;
|
||||
}
|
||||
}
|
||||
out.verdict = NameVerdict::Rail;
|
||||
return out;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
SignalType infer_signal_type(const std::string &name) {
|
||||
switch (classify_signal_name(name).verdict) {
|
||||
case NameVerdict::Rail: return SignalType::Power;
|
||||
case NameVerdict::GndShield: return SignalType::GndShield;
|
||||
case NameVerdict::PowerMgmt:
|
||||
case NameVerdict::Other: break;
|
||||
}
|
||||
return SignalType::Other;
|
||||
}
|
||||
|
||||
@@ -152,7 +152,9 @@ void Tui::RegisterCommands() {
|
||||
Print(" types: " + std::to_string(r.power) + " power, "
|
||||
+ std::to_string(r.gnd) + " gnd, "
|
||||
+ std::to_string(r.kept_other)
|
||||
+ " suspect Power (name only — kept as Other)");
|
||||
+ " suspect Power (name only — kept as Other), "
|
||||
+ std::to_string(r.mgmt)
|
||||
+ " power-management (control/measure — kept as Other)");
|
||||
},
|
||||
/*prompt_for_missing=*/ true,
|
||||
"load a module from a netlist / pinout file (mentor, altium, ods)",
|
||||
@@ -257,6 +259,7 @@ void Tui::RegisterCommands() {
|
||||
render(r.jtag_anomalies, " JTAG chain anomaly(ies).");
|
||||
render(r.conflict_anomalies, " source-conflict(s).");
|
||||
render(r.completeness_anomalies, " BSDL completeness issue(s).");
|
||||
render(r.diff_anomalies, " diff-pair crossing anomaly(ies).");
|
||||
}, true,
|
||||
"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.conflict_anomalies);
|
||||
push_anoms(vr.completeness_anomalies);
|
||||
push_anoms(vr.diff_anomalies);
|
||||
int n_model = vr.model_total();
|
||||
|
||||
if (analyze_issues.empty()) analyze_issues.push_back("(no issue found)");
|
||||
@@ -131,29 +132,39 @@ Component Tui::BuildAnalyzeScreen() {
|
||||
// ============================================================= Types
|
||||
// Power decisions (confirmed / refuted) and NC orphan breakdown.
|
||||
analyze_types.clear();
|
||||
int conf_pwr = 0, ref_pwr = 0, gnd = 0;
|
||||
struct Row { char kind; std::string mod, sig; int fanout; bool voltage; };
|
||||
int conf_pwr = 0, ref_pwr = 0, mgmt = 0, gnd = 0;
|
||||
struct Row { char kind; std::string mod, sig; int fanout; bool voltage;
|
||||
std::string token; };
|
||||
std::vector<Row> rows;
|
||||
for (auto &mkv : *sys->modules()) {
|
||||
Module *mod = mkv.second;
|
||||
for (auto &skv : *mod->signals) {
|
||||
Signal *s = skv.second;
|
||||
SignalType named = infer_signal_type(s->name);
|
||||
NameClassification ncl = classify_signal_name(s->name);
|
||||
char kind = 0;
|
||||
if (named == SignalType::GndShield && s->type == SignalType::GndShield) {
|
||||
std::string token;
|
||||
if (ncl.verdict == NameVerdict::GndShield && s->type == SignalType::GndShield) {
|
||||
kind = 'G'; ++gnd;
|
||||
} else if (named == SignalType::Power && s->type == SignalType::Power) {
|
||||
} else if (ncl.verdict == NameVerdict::Rail && s->type == SignalType::Power) {
|
||||
kind = 'P'; ++conf_pwr;
|
||||
} else if (named == SignalType::Power && s->type == SignalType::Other) {
|
||||
} else if (ncl.verdict == NameVerdict::Rail && s->type == SignalType::Other) {
|
||||
kind = 'R'; ++ref_pwr;
|
||||
} else if (ncl.verdict == NameVerdict::PowerMgmt) {
|
||||
kind = 'M'; ++mgmt; token = ncl.token;
|
||||
} else continue;
|
||||
rows.push_back({kind, mod->name, s->name,
|
||||
(int)s->size(), has_voltage_pattern(s->name)});
|
||||
(int)s->size(), has_voltage_pattern(s->name),
|
||||
token});
|
||||
}
|
||||
}
|
||||
// Deliberate display order: confirmed rails, then the suspects (the
|
||||
// actionable residue), then the power-management signals, gnd last.
|
||||
auto rank = [](char k) {
|
||||
return k == 'P' ? 0 : k == 'R' ? 1 : k == 'M' ? 2 : 3;
|
||||
};
|
||||
std::sort(rows.begin(), rows.end(),
|
||||
[](const Row &a, const Row &b) {
|
||||
if (a.kind != b.kind) return a.kind < b.kind;
|
||||
[&](const Row &a, const Row &b) {
|
||||
if (a.kind != b.kind) return rank(a.kind) < rank(b.kind);
|
||||
if (a.mod != b.mod) return a.mod < b.mod;
|
||||
return a.sig < b.sig;
|
||||
});
|
||||
@@ -181,6 +192,10 @@ Component Tui::BuildAnalyzeScreen() {
|
||||
else reason = "name only — fan-out "
|
||||
+ std::to_string(r.fanout)
|
||||
+ ", no voltage";
|
||||
} else if (r.kind == 'M') {
|
||||
tag = "[Power mgmt] ";
|
||||
reason = "control token '" + r.token
|
||||
+ "' in name — kept as Other";
|
||||
} else {
|
||||
tag = "[Gnd] ";
|
||||
reason = "name match (fan-out " + std::to_string(r.fanout) + ")";
|
||||
@@ -200,7 +215,8 @@ Component Tui::BuildAnalyzeScreen() {
|
||||
|
||||
std::string types_header = "Types: " + std::to_string(conf_pwr)
|
||||
+ " Power, " + std::to_string(ref_pwr)
|
||||
+ " Suspect, " + std::to_string(gnd)
|
||||
+ " Suspect, " + std::to_string(mgmt)
|
||||
+ " Pwr-mgmt, " + std::to_string(gnd)
|
||||
+ " Gnd";
|
||||
|
||||
// Tab bar — horizontal headers, active one inverted.
|
||||
@@ -240,8 +256,14 @@ Component Tui::BuildAnalyzeScreen() {
|
||||
"Name suggests Power AND structure agrees: fan-out ≥ 4 pins, "
|
||||
"or a voltage pattern in the name (e.g. 3V3, 5V, 12V)."),
|
||||
term("Suspect Power",
|
||||
"Name suggests Power but the structural check failed — "
|
||||
"fan-out too low and no voltage in the name."),
|
||||
"Name suggests Power, no control token explains it, but the "
|
||||
"structural check failed — fan-out too low and no voltage "
|
||||
"in the name."),
|
||||
term("Power mgmt",
|
||||
"Name holds a rail token AND a control token (SENSE, EN, PG, "
|
||||
"FB, …): a power-management signal — measurement or control "
|
||||
"of a rail — not the rail itself. Confidently Other, never "
|
||||
"suspect."),
|
||||
term("Hard floor",
|
||||
"Fan-out below 3 pins forces Other regardless of the name. "
|
||||
"A real rail physically cannot live on 1-2 pads."),
|
||||
|
||||
@@ -281,20 +281,29 @@ void Tui::Source(const std::string &filename) {
|
||||
if (const char *home = std::getenv("HOME"))
|
||||
expanded = std::string(home) + expanded.substr(1);
|
||||
}
|
||||
if (source_stack.size() >= 32) { // same depth guard as the core engine
|
||||
Print("source: nesting too deep, skipping " + filename);
|
||||
return;
|
||||
}
|
||||
std::ifstream f(expanded);
|
||||
if (!f) { Print("source failed: cannot open " + filename); return; }
|
||||
|
||||
// Slurp the whole file so we can drive line-by-line processing from the
|
||||
// event loop (one line per posted task). This lets the screen redraw
|
||||
// between lines and surface the "Computing…" modal.
|
||||
loading_lines.clear();
|
||||
SourceFrame fr;
|
||||
fr.filename = filename;
|
||||
std::string line;
|
||||
while (std::getline(f, line)) loading_lines.push_back(line);
|
||||
while (std::getline(f, line)) fr.lines.push_back(std::move(line));
|
||||
|
||||
// Nested source (a sourced line is itself `source …`): just stack the
|
||||
// frame — the driver already running (ticker thread or headless drain)
|
||||
// picks it up on the next ProcessNextSourceLine, and the caller's frame
|
||||
// resumes when it finishes.
|
||||
bool nested = !source_stack.empty();
|
||||
source_stack.push_back(std::move(fr));
|
||||
if (nested) return;
|
||||
|
||||
loading_filename = filename;
|
||||
loading_idx = 0;
|
||||
loading_executed = 0;
|
||||
loading_lineno = 0;
|
||||
loading_prev_in_source = in_source;
|
||||
source_origin_screen = screen_idx; // a sourced line that leaves this screen aborts
|
||||
in_source = true;
|
||||
@@ -332,9 +341,18 @@ void Tui::Source(const std::string &filename) {
|
||||
|
||||
void Tui::ProcessNextSourceLine() {
|
||||
if (!loading.load()) return;
|
||||
while (loading_idx < loading_lines.size()) {
|
||||
const std::string &raw = loading_lines[loading_idx++];
|
||||
++loading_lineno;
|
||||
while (!source_stack.empty()) {
|
||||
if (source_stack.back().idx >= source_stack.back().lines.size()) {
|
||||
// Frame done: summarise it and resume the caller's frame.
|
||||
const SourceFrame &done = source_stack.back();
|
||||
Print("source: " + done.filename
|
||||
+ " (" + std::to_string(done.executed) + " line(s))");
|
||||
source_stack.pop_back();
|
||||
continue;
|
||||
}
|
||||
SourceFrame &fr = source_stack.back();
|
||||
const std::string raw = fr.lines[fr.idx++];
|
||||
++fr.lineno;
|
||||
size_t start = raw.find_first_not_of(" \t");
|
||||
if (start == std::string::npos) continue;
|
||||
if (raw[start] == '#') continue;
|
||||
@@ -343,28 +361,26 @@ void Tui::ProcessNextSourceLine() {
|
||||
trimmed.pop_back();
|
||||
if (trimmed.empty()) continue;
|
||||
|
||||
++fr.executed;
|
||||
int lineno = fr.lineno; // copies: Submit can push a nested frame,
|
||||
// which may reallocate and invalidate `fr`.
|
||||
input = trimmed;
|
||||
cursor_pos = (int)input.size();
|
||||
Submit();
|
||||
++loading_executed;
|
||||
|
||||
if (screen_idx != source_origin_screen) {
|
||||
Print("source: line " + std::to_string(loading_lineno)
|
||||
Print("source: line " + std::to_string(lineno)
|
||||
+ " is interactive (would open a screen) — aborting.");
|
||||
screen_idx = source_origin_screen;
|
||||
loading.store(false);
|
||||
computing_open = false;
|
||||
tick_in_flight.store(false);
|
||||
in_source = loading_prev_in_source;
|
||||
return;
|
||||
source_stack.clear(); // an abort cancels the whole chain
|
||||
break;
|
||||
}
|
||||
// One effective line per tick — ack so the ticker can pace the next.
|
||||
tick_in_flight.store(false);
|
||||
return;
|
||||
}
|
||||
|
||||
Print("source: " + loading_filename
|
||||
+ " (" + std::to_string(loading_executed) + " line(s))");
|
||||
// Stack drained (or aborted): close up.
|
||||
loading.store(false);
|
||||
computing_open = false;
|
||||
tick_in_flight.store(false);
|
||||
|
||||
@@ -12,7 +12,6 @@ using namespace ftxui;
|
||||
Tui::Tui()
|
||||
: cursor_pos(0), history_idx(-1), scroll_offset(0), quit(false), in_source(false),
|
||||
loading(false), tick_in_flight(false),
|
||||
loading_idx(0), loading_executed(0), loading_lineno(0),
|
||||
loading_prev_in_source(false), screen_ptr(nullptr),
|
||||
screen_idx(4), // boot to the dashboard; console (screen 0) is now a sub-screen
|
||||
connect_m1_idx(0), connect_m2_idx(0),
|
||||
@@ -64,12 +63,17 @@ void Tui::Run() {
|
||||
// script is opened from the dashboard. The Renderer re-reads the live
|
||||
// progress every frame.
|
||||
auto computing_modal = Renderer([this] {
|
||||
std::string progress = std::to_string(loading_executed) + " / "
|
||||
+ std::to_string((int)loading_lines.size()) + " lines";
|
||||
std::string fname, progress;
|
||||
if (!source_stack.empty()) { // top frame = the file currently running
|
||||
const SourceFrame &fr = source_stack.back();
|
||||
fname = fr.filename;
|
||||
progress = std::to_string(fr.executed) + " / "
|
||||
+ std::to_string((int)fr.lines.size()) + " lines";
|
||||
}
|
||||
return vbox({
|
||||
text(" Computing… ") | bold | center,
|
||||
separator(),
|
||||
text(loading_filename) | center,
|
||||
text(fname) | center,
|
||||
text(progress) | center,
|
||||
}) | borderDouble | size(WIDTH, GREATER_THAN, 40);
|
||||
});
|
||||
|
||||
@@ -99,11 +99,16 @@ class Tui : public Frontend {
|
||||
// ---- Source-file loading (event-driven, one line per tick) ----
|
||||
std::atomic<bool> loading; ///< true while a script is being processed; read by tick thread.
|
||||
std::atomic<bool> tick_in_flight; ///< main thread acks each tick by clearing this; ticker waits.
|
||||
std::string loading_filename;
|
||||
std::vector<std::string> loading_lines;
|
||||
size_t loading_idx;
|
||||
int loading_executed;
|
||||
int loading_lineno;
|
||||
// One script being processed. Nested `source` pushes a frame so the
|
||||
// caller resumes where it left off — the stack IS the call chain.
|
||||
struct SourceFrame {
|
||||
std::string filename;
|
||||
std::vector<std::string> lines;
|
||||
size_t idx = 0; ///< next line to process
|
||||
int executed = 0; ///< effective (non-blank, non-comment) lines run
|
||||
int lineno = 0; ///< current 1-based line, for messages
|
||||
};
|
||||
std::vector<SourceFrame> source_stack;
|
||||
bool loading_prev_in_source;
|
||||
int source_origin_screen = 0; ///< screen a `source` started from; a sourced line that navigates away (opens an interactive screen) aborts it.
|
||||
bool computing_open = false; ///< drives the global "Computing…" progress modal while a script loads.
|
||||
|
||||
@@ -352,9 +352,9 @@ void EssimFrame::OnLoad(wxCommandEvent &) {
|
||||
}
|
||||
Log(wxString::Format(
|
||||
"loaded '%s' from %s — %d part(s), %d signal(s)"
|
||||
" (dropped %d; types: %d power / %d gnd / %d suspect)",
|
||||
" (dropped %d; types: %d power / %d gnd / %d suspect / %d pwr-mgmt)",
|
||||
modname, path, r.parts, r.signals, r.dropped, r.power, r.gnd,
|
||||
r.kept_other));
|
||||
r.kept_other, r.mgmt));
|
||||
RebuildModelView();
|
||||
}
|
||||
|
||||
@@ -647,6 +647,7 @@ void EssimFrame::OnVerify(wxCommandEvent &) {
|
||||
log_anoms(r.jtag_anomalies, "JTAG chain anomaly(ies)");
|
||||
log_anoms(r.conflict_anomalies, "source-conflict(s)");
|
||||
log_anoms(r.completeness_anomalies, "BSDL completeness issue(s)");
|
||||
log_anoms(r.diff_anomalies, "diff-pair crossing anomaly(ies)");
|
||||
|
||||
RebuildModelView();
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@ TEST_CASE("infer_signal_types: Power requires name+structural agreement") {
|
||||
|
||||
Signal *p_3v3 = m->signals->merge("PWR_3V3"); fan_out(p_3v3, 3); // voltage + ≥ floor → Power
|
||||
Signal *vcc = m->signals->merge("VCC"); fan_out(vcc, 5); // fan-out ≥ 4 → Power
|
||||
Signal *pwr_ok = m->signals->merge("PWR_OK"); fan_out(pwr_ok, 1); // < 3 → hard floor → Other
|
||||
Signal *pwr_ok = m->signals->merge("PWR_OK"); fan_out(pwr_ok, 1); // control token → pwr-mgmt
|
||||
Signal *pwr_2 = m->signals->merge("PWR_2"); fan_out(pwr_2, 2); // < 3 → hard floor → Other
|
||||
Signal *gnd = m->signals->merge("GND"); fan_out(gnd, 1); // gnd: name alone
|
||||
Signal *clk = m->signals->merge("CLK_50MHZ"); fan_out(clk, 3); // not power-ish → Other
|
||||
@@ -243,7 +243,8 @@ TEST_CASE("infer_signal_types: Power requires name+structural agreement") {
|
||||
auto st = infer_signal_types(sys.get());
|
||||
CHECK(st.power == 2); // PWR_3V3, VCC
|
||||
CHECK(st.gnd == 1); // GND (name alone)
|
||||
CHECK(st.kept_other == 2); // PWR_OK, PWR_2 below the hard floor
|
||||
CHECK(st.kept_other == 1); // PWR_2 below the hard floor
|
||||
CHECK(st.mgmt == 1); // PWR_OK: power-good control, not suspect
|
||||
|
||||
CHECK(p_3v3->type == SignalType::Power);
|
||||
CHECK(vcc->type == SignalType::Power);
|
||||
@@ -253,6 +254,27 @@ TEST_CASE("infer_signal_types: Power requires name+structural agreement") {
|
||||
CHECK(clk->type == SignalType::Other);
|
||||
}
|
||||
|
||||
TEST_CASE("infer_signal_types: power-management beats fan-out — a big sense net is still Other") {
|
||||
auto sys = std::make_unique<System>();
|
||||
Module *m = sys->modules()->merge("M");
|
||||
Part *p = new Part("U1"); m->add(p);
|
||||
|
||||
// VDD_CORE_SENSE with fan-out 5: structure alone would confirm Power,
|
||||
// but the control token settles it as a measurement net → Other,
|
||||
// counted mgmt (not suspect, not power).
|
||||
Signal *s = m->signals->merge("VDD_CORE_SENSE");
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
Pin *pin = new Pin("p" + std::to_string(i));
|
||||
p->add(pin); s->add(pin); pin->connect(s);
|
||||
}
|
||||
|
||||
auto st = infer_signal_types(sys.get());
|
||||
CHECK(st.power == 0);
|
||||
CHECK(st.kept_other == 0);
|
||||
CHECK(st.mgmt == 1);
|
||||
CHECK(s->type == SignalType::Other);
|
||||
}
|
||||
|
||||
TEST_CASE("infer_signal_types: fan-out hard floor overrides voltage in name") {
|
||||
auto sys = std::make_unique<System>();
|
||||
Module *m = sys->modules()->merge("M");
|
||||
|
||||
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());
|
||||
}
|
||||
@@ -51,7 +51,52 @@ TEST_CASE("infer_signal_type: power family") {
|
||||
CHECK(infer_signal_type("VS3_5V0") == SignalType::Power);
|
||||
CHECK(infer_signal_type("+5V") == SignalType::Power);
|
||||
CHECK(infer_signal_type("-12V") == SignalType::Power);
|
||||
CHECK(infer_signal_type("VBAT_SENSE") == SignalType::Power);
|
||||
// Rail token + control token → power-management, mapped to Other by the
|
||||
// wrapper (it is a sense line ABOUT VBAT, not the rail).
|
||||
CHECK(infer_signal_type("VBAT_SENSE") == SignalType::Other);
|
||||
}
|
||||
|
||||
TEST_CASE("classify_signal_name: rail vs power-management signals") {
|
||||
CHECK(classify_signal_name("VCC").verdict == NameVerdict::Rail);
|
||||
CHECK(classify_signal_name("VDD_3V3").verdict == NameVerdict::Rail);
|
||||
CHECK(classify_signal_name("+5V").verdict == NameVerdict::Rail);
|
||||
|
||||
NameClassification c = classify_signal_name("VDD_CORE_SENSE");
|
||||
CHECK(c.verdict == NameVerdict::PowerMgmt);
|
||||
CHECK(c.token == "SENSE");
|
||||
|
||||
CHECK(classify_signal_name("VBAT_SENSE").verdict == NameVerdict::PowerMgmt);
|
||||
CHECK(classify_signal_name("VCC_EN").verdict == NameVerdict::PowerMgmt);
|
||||
CHECK(classify_signal_name("VCC_EN1").verdict == NameVerdict::PowerMgmt); // trailing digit
|
||||
CHECK(classify_signal_name("VDD_FB").verdict == NameVerdict::PowerMgmt);
|
||||
CHECK(classify_signal_name("PWR_GOOD").verdict == NameVerdict::PowerMgmt);
|
||||
CHECK(classify_signal_name("PWR_OK").verdict == NameVerdict::PowerMgmt);
|
||||
CHECK(classify_signal_name("VBUS_DET").verdict == NameVerdict::PowerMgmt);
|
||||
CHECK(classify_signal_name("POWER_FAIL").verdict == NameVerdict::PowerMgmt);
|
||||
CHECK(classify_signal_name("VDD_VSENSE").verdict == NameVerdict::PowerMgmt); // fused suffix
|
||||
CHECK(classify_signal_name("PWR_NFAULT").verdict == NameVerdict::PowerMgmt); // active-low
|
||||
CHECK(classify_signal_name("VDD_ADJ").verdict == NameVerdict::PowerMgmt); // regulator adjust
|
||||
CHECK(classify_signal_name("VCC_TRIM").verdict == NameVerdict::PowerMgmt);
|
||||
CHECK(classify_signal_name("VDD_VTRIM").verdict == NameVerdict::PowerMgmt); // fused suffix
|
||||
CHECK(classify_signal_name("VCC_VSET").verdict == NameVerdict::PowerMgmt);
|
||||
CHECK(classify_signal_name("VCC_FBK").verdict == NameVerdict::PowerMgmt); // feedback variants
|
||||
CHECK(classify_signal_name("VDD_FDB").verdict == NameVerdict::PowerMgmt);
|
||||
CHECK(classify_signal_name("PWR_CMD").verdict == NameVerdict::PowerMgmt); // command
|
||||
CHECK(classify_signal_name("PWR_LED").verdict == NameVerdict::PowerMgmt); // indicator
|
||||
CHECK(classify_signal_name("VDD_REF").verdict == NameVerdict::PowerMgmt); // reference
|
||||
CHECK(classify_signal_name("VCC_VREF").verdict == NameVerdict::PowerMgmt);
|
||||
|
||||
// Whole-token matching: SENSOR is not SENSE, GREEN is not EN —
|
||||
// these stay genuine rails.
|
||||
CHECK(classify_signal_name("VDD_SENSOR").verdict == NameVerdict::Rail);
|
||||
CHECK(classify_signal_name("VCC_GREEN").verdict == NameVerdict::Rail);
|
||||
|
||||
// No rail token at all → Other, even with a control word.
|
||||
CHECK(classify_signal_name("SPI_CS").verdict == NameVerdict::Other);
|
||||
CHECK(classify_signal_name("FAN_SENSE").verdict == NameVerdict::Other);
|
||||
|
||||
// GND family is deliberately left out of the control-token logic.
|
||||
CHECK(classify_signal_name("GND_RET").verdict == NameVerdict::GndShield);
|
||||
}
|
||||
|
||||
TEST_CASE("infer_signal_type: other (data signals)") {
|
||||
|
||||
71
tests/tui/test_source.cpp
Normal file
71
tests/tui/test_source.cpp
Normal file
@@ -0,0 +1,71 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "frontends/tui/tui.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
// Tui::Source nesting — regression for the bug where a nested `source`
|
||||
// overwrote the single loading state, so the CALLING script's remaining
|
||||
// lines never ran. Headless path (no screen): BootDispatch drains
|
||||
// synchronously, exactly like `essim --batch --source`.
|
||||
|
||||
namespace {
|
||||
|
||||
std::string run_boot(const std::string &cmd) {
|
||||
Tui t;
|
||||
t.BootDispatch(cmd);
|
||||
std::ostringstream oss;
|
||||
t.DumpOutput(oss);
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("source: lines after a nested source still run") {
|
||||
const char *inner = "test_src_inner.essim";
|
||||
const char *outer = "test_src_outer.essim";
|
||||
{
|
||||
std::ofstream f(inner);
|
||||
f << "new\n";
|
||||
}
|
||||
{
|
||||
std::ofstream f(outer);
|
||||
f << "source " << inner << "\n"
|
||||
"verify\n";
|
||||
}
|
||||
|
||||
std::string out = run_boot(std::string("source ") + outer);
|
||||
|
||||
// The inner script ran and was summarised…
|
||||
CHECK(out.find("system created.") != std::string::npos);
|
||||
CHECK(out.find(std::string("source: ") + inner) != std::string::npos);
|
||||
// …and the OUTER script kept going after it: verify executed…
|
||||
CHECK(out.find("verify: 0 local mismatch(es)") != std::string::npos);
|
||||
// …and the outer summary counts its 2 effective lines.
|
||||
CHECK(out.find(std::string("source: ") + outer + " (2 line(s))")
|
||||
!= std::string::npos);
|
||||
|
||||
std::remove(inner);
|
||||
std::remove(outer);
|
||||
}
|
||||
|
||||
TEST_CASE("source: self-recursion stops at the depth guard") {
|
||||
const char *loop = "test_src_loop.essim";
|
||||
{
|
||||
std::ofstream f(loop);
|
||||
f << "source " << loop << "\n";
|
||||
}
|
||||
|
||||
std::string out = run_boot(std::string("source ") + loop);
|
||||
|
||||
CHECK(out.find("source: nesting too deep, skipping")
|
||||
!= std::string::npos);
|
||||
// Every frame still closes with its own summary.
|
||||
CHECK(out.find(std::string("source: ") + loop + " (1 line(s))")
|
||||
!= std::string::npos);
|
||||
|
||||
std::remove(loop);
|
||||
}
|
||||
Reference in New Issue
Block a user