From 952afe39796d6e7ffccd29cdd92329cf71f988d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Wed, 3 Jun 2026 15:24:00 +0200 Subject: [PATCH] verify: JTAG boundary-scan chain integrity (flagship) New check_jtag_chain(System*): collects TAP pins by PinSpec.function, resolves each to its net, and flags JtagTapIncomplete (a device missing TDI/TDO/TMS/ TCK), JtagBusUnbridged (TMS or TCK not common to every TAP device), and JtagChainBreak (dangling TDO/TDI, chain fan-out, or not a single head->tail daisy chain). Surfaced as a pass in `verify`; AnomalyKind extended. Covered by test_bsdl_check (healthy chain, broken chain + split bus, incomplete TAP). Co-Authored-By: Claude Opus 4.8 --- src/system/analysis.cpp | 3 + src/system/analysis.hpp | 3 + src/system/bsdl_check.cpp | 165 ++++++++++++++++++++++++++++++++++++++ src/system/bsdl_check.hpp | 9 +++ src/tui/commands.cpp | 7 ++ tests/test_bsdl_check.cpp | 69 ++++++++++++++++ 6 files changed, 256 insertions(+) diff --git a/src/system/analysis.cpp b/src/system/analysis.cpp index 7e1b2f8..44ee5d4 100644 --- a/src/system/analysis.cpp +++ b/src/system/analysis.cpp @@ -26,6 +26,9 @@ const char *anomaly_kind_name(AnomalyKind k) { case AnomalyKind::DriveContention: return "drive-contention"; case AnomalyKind::UndrivenNet: return "undriven-net"; case AnomalyKind::NcWired: return "nc-wired"; + case AnomalyKind::JtagTapIncomplete: return "jtag-tap-incomplete"; + case AnomalyKind::JtagChainBreak: return "jtag-chain-break"; + case AnomalyKind::JtagBusUnbridged: return "jtag-bus-unbridged"; } return "?"; } diff --git a/src/system/analysis.hpp b/src/system/analysis.hpp index 2cbabea..188ea8a 100644 --- a/src/system/analysis.hpp +++ b/src/system/analysis.hpp @@ -34,6 +34,9 @@ enum class AnomalyKind { DriveContention, ///< A net has ≥2 push-pull output (Out) drivers. UndrivenNet, ///< A net has input(s) but no driver (Out/Bidir/Power). NcWired, ///< A no-connect pin is wired onto a multi-pin net. + JtagTapIncomplete, ///< A TAP device is missing one of TDI/TDO/TMS/TCK. + JtagChainBreak, ///< The TDO→TDI daisy chain is broken / not a single path. + JtagBusUnbridged, ///< TMS or TCK is not common to all TAP devices. }; struct Anomaly { diff --git a/src/system/bsdl_check.cpp b/src/system/bsdl_check.cpp index 9da0009..16d07b1 100644 --- a/src/system/bsdl_check.cpp +++ b/src/system/bsdl_check.cpp @@ -9,10 +9,18 @@ #include "system.hpp" #include +#include +#include #include namespace { +std::string part_label(const Part *p) +{ + std::string mod = (p && p->prnt) ? p->prnt->name : std::string("?"); + return mod + "/" + (p ? p->name : std::string("?")); +} + std::string pin_label(const Pin *p) { std::string part = (p && p->prnt) ? p->prnt->name : std::string("?"); @@ -109,3 +117,160 @@ std::vector check_pin_specs(System *sys) return out; } + +std::vector check_jtag_chain(System *sys) +{ + std::vector out; + if (!sys) + return out; + + // Map every pin to the index of the net it sits on. + std::vector nets = compute_all_nets(sys); + std::unordered_map net_of; + for (size_t i = 0; i < nets.size(); ++i) + for (auto &mp : nets[i].members) + for (auto &kv : *mp.second) + net_of[kv.second] = (int)i; + auto net_id = [&](Pin *p) -> int { + if (!p) return -1; + auto it = net_of.find(p); + return it == net_of.end() ? -1 : it->second; + }; + + // Collect TAP devices: any part carrying ≥1 pin with a TAP function. + struct Tap { + Part *part = nullptr; + Pin *tdi = nullptr, *tdo = nullptr, *tms = nullptr, *tck = nullptr, *trst = nullptr; + }; + std::vector taps; + for (auto &mkv : *sys->modules()) + for (auto &pkv : *mkv.second) { + Tap t; + t.part = pkv.second; + bool any = false; + for (auto &nkv : *t.part) { + Pin *pin = nkv.second; + switch (pin->spec.function) { + case PinFunction::JtagTdi: t.tdi = pin; any = true; break; + case PinFunction::JtagTdo: t.tdo = pin; any = true; break; + case PinFunction::JtagTms: t.tms = pin; any = true; break; + case PinFunction::JtagTck: t.tck = pin; any = true; break; + case PinFunction::JtagTrst: t.trst = pin; any = true; break; + default: break; + } + } + if (any) taps.push_back(t); + } + + if (taps.empty()) + return out; + + // (1) every TAP device needs the four mandatory signals. + for (const Tap &t : taps) { + std::string miss; + auto need = [&](Pin *p, const char *n) { + if (!p) miss += (miss.empty() ? "" : ", ") + std::string(n); + }; + need(t.tdi, "TDI"); need(t.tdo, "TDO"); need(t.tms, "TMS"); need(t.tck, "TCK"); + if (!miss.empty()) { + Anomaly a; + a.kind = AnomalyKind::JtagTapIncomplete; + a.module = t.part ? t.part->prnt : nullptr; + a.message = part_label(t.part) + ": incomplete TAP, missing " + miss; + out.push_back(std::move(a)); + } + } + + // (2) TMS and TCK must each be one common net across all TAP devices. + auto check_bus = [&](const char *name, const std::vector> &pins) { + if (pins.size() < 2) + return; + int ref = -1; + for (const auto &pp : pins) { + int n = net_id(pp.second); + if (n >= 0) { ref = n; break; } + } + if (ref < 0) { + Anomaly a; + a.kind = AnomalyKind::JtagBusUnbridged; + a.message = std::string(name) + ": no TAP device has a connected " + name; + out.push_back(std::move(a)); + return; + } + std::string off; + for (const auto &pp : pins) + if (net_id(pp.second) != ref) + off += (off.empty() ? "" : ", ") + part_label(pp.first); + if (!off.empty()) { + Anomaly a; + a.kind = AnomalyKind::JtagBusUnbridged; + a.message = std::string(name) + " is not common to all TAP devices (off-bus: " + + off + ")"; + out.push_back(std::move(a)); + } + }; + std::vector> tms_pins, tck_pins; + for (const Tap &t : taps) { + if (t.tms) tms_pins.push_back({t.part, t.tms}); + if (t.tck) tck_pins.push_back({t.part, t.tck}); + } + check_bus("TMS", tms_pins); + check_bus("TCK", tck_pins); + + // (3) TDO→TDI daisy chain: a link is two devices sharing a net (A.tdo, B.tdi). + const int n = (int)taps.size(); + std::vector downstream(n, 0), upstream(n, 0); + for (int i = 0; i < n; ++i) { + if (taps[i].tdo && net_id(taps[i].tdo) < 0) { + Anomaly a; + a.kind = AnomalyKind::JtagChainBreak; + a.module = taps[i].part ? taps[i].part->prnt : nullptr; + a.message = part_label(taps[i].part) + "/TDO is not connected to any net"; + out.push_back(std::move(a)); + } + if (taps[i].tdi && net_id(taps[i].tdi) < 0) { + Anomaly a; + a.kind = AnomalyKind::JtagChainBreak; + a.module = taps[i].part ? taps[i].part->prnt : nullptr; + a.message = part_label(taps[i].part) + "/TDI is not connected to any net"; + out.push_back(std::move(a)); + } + } + for (int i = 0; i < n; ++i) + for (int j = 0; j < n; ++j) { + if (i == j) continue; + int a = net_id(taps[i].tdo), b = net_id(taps[j].tdi); + if (a >= 0 && a == b) { ++downstream[i]; ++upstream[j]; } + } + int heads = 0, tails = 0; + for (int i = 0; i < n; ++i) { + if (downstream[i] > 1) { + Anomaly a; + a.kind = AnomalyKind::JtagChainBreak; + a.module = taps[i].part ? taps[i].part->prnt : nullptr; + a.message = part_label(taps[i].part) + "/TDO drives " + + std::to_string(downstream[i]) + " TDIs (chain fan-out)"; + out.push_back(std::move(a)); + } + if (upstream[i] > 1) { + Anomaly a; + a.kind = AnomalyKind::JtagChainBreak; + a.module = taps[i].part ? taps[i].part->prnt : nullptr; + a.message = part_label(taps[i].part) + "/TDI is driven by " + + std::to_string(upstream[i]) + " TDOs"; + out.push_back(std::move(a)); + } + if (taps[i].tdi && net_id(taps[i].tdi) >= 0 && upstream[i] == 0) ++heads; + if (taps[i].tdo && net_id(taps[i].tdo) >= 0 && downstream[i] == 0) ++tails; + } + if (n > 1 && (heads != 1 || tails != 1)) { + Anomaly a; + a.kind = AnomalyKind::JtagChainBreak; + a.message = "JTAG chain is not a single daisy chain: " + std::to_string(heads) + + " head(s), " + std::to_string(tails) + " tail(s) over " + + std::to_string(n) + " TAP device(s)"; + out.push_back(std::move(a)); + } + + return out; +} diff --git a/src/system/bsdl_check.hpp b/src/system/bsdl_check.hpp index 221cc54..8144c8b 100644 --- a/src/system/bsdl_check.hpp +++ b/src/system/bsdl_check.hpp @@ -16,4 +16,13 @@ class System; // un-modelled parts). std::vector check_pin_specs(System *sys); +// 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: +// - JtagTapIncomplete : a TAP device missing TDI/TDO/TMS/TCK; +// - JtagBusUnbridged : TMS or TCK not common to every TAP device; +// - JtagChainBreak : the TDO→TDI daisy chain dangles, fans out, or is not a +// single path (≠1 head / ≠1 tail). +// Empty when the system has no TAP pins. +std::vector check_jtag_chain(System *sys); + #endif // _BSDL_CHECK_HPP_ diff --git a/src/tui/commands.cpp b/src/tui/commands.cpp index f43dd98..8cede1f 100644 --- a/src/tui/commands.cpp +++ b/src/tui/commands.cpp @@ -304,6 +304,13 @@ void Tui::RegisterCommands() { Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message); Print("verify: " + std::to_string(pin_anoms.size()) + " model-driven pin anomaly(ies)."); + + // JTAG boundary-scan chain integrity (TAP pins → nets). + auto jtag_anoms = check_jtag_chain(sys.get()); + for (const auto &a : jtag_anoms) + Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message); + Print("verify: " + std::to_string(jtag_anoms.size()) + + " JTAG chain anomaly(ies)."); }, true, "check pin roles, bridged-net consistency, and model-driven pin specs (contention/undriven/NC)" }; diff --git a/tests/test_bsdl_check.cpp b/tests/test_bsdl_check.cpp index a0abaa9..6186ba2 100644 --- a/tests/test_bsdl_check.cpp +++ b/tests/test_bsdl_check.cpp @@ -90,3 +90,72 @@ TEST_CASE("check_pin_specs stays silent on un-modelled nets") { auto anoms = check_pin_specs(&sys); CHECK(anoms.empty()); } + +namespace { + +// Add the four mandatory TAP pins to a part and return it. +Part *mk_tap(Module *m, const char *name) +{ + Part *p = new Part(name); + mkpin(p, "TDI", PinDirection::In, PinFunction::JtagTdi); + mkpin(p, "TDO", PinDirection::Out, PinFunction::JtagTdo); + mkpin(p, "TMS", PinDirection::In, PinFunction::JtagTms); + mkpin(p, "TCK", PinDirection::In, PinFunction::JtagTck); + m->add(p); + return p; +} + +} // namespace + +TEST_CASE("check_jtag_chain accepts a healthy daisy chain") { + System sys; + Module *m = sys.modules()->merge("M"); + Part *u1 = mk_tap(m, "U1"); + Part *u2 = mk_tap(m, "U2"); + Part *u3 = mk_tap(m, "U3"); + + wire(m, "TCK", {u1->get("TCK"), u2->get("TCK"), u3->get("TCK")}); + wire(m, "TMS", {u1->get("TMS"), u2->get("TMS"), u3->get("TMS")}); + wire(m, "TDI_HEAD", {u1->get("TDI")}); // from the controller + wire(m, "L1", {u1->get("TDO"), u2->get("TDI")}); // U1 → U2 + wire(m, "L2", {u2->get("TDO"), u3->get("TDI")}); // U2 → U3 + wire(m, "TDO_TAIL", {u3->get("TDO")}); // back to the controller + + CHECK(check_jtag_chain(&sys).empty()); +} + +TEST_CASE("check_jtag_chain flags a broken chain and a split bus") { + System sys; + Module *m = sys.modules()->merge("M"); + Part *u1 = mk_tap(m, "U1"); + Part *u2 = mk_tap(m, "U2"); + Part *u3 = mk_tap(m, "U3"); + + wire(m, "TCK", {u1->get("TCK"), u2->get("TCK"), u3->get("TCK")}); + // U3's TMS is on its own net → not bridged to the TMS bus. + wire(m, "TMS", {u1->get("TMS"), u2->get("TMS")}); + wire(m, "TMS_ALT", {u3->get("TMS")}); + // Chain link U2 → U3 is missing: U2.TDO and U3.TDI dangle. + wire(m, "TDI_HEAD", {u1->get("TDI")}); + wire(m, "L1", {u1->get("TDO"), u2->get("TDI")}); + wire(m, "U2_TDO", {u2->get("TDO")}); + wire(m, "U3_TDI", {u3->get("TDI")}); + wire(m, "TDO_TAIL", {u3->get("TDO")}); + + auto a = check_jtag_chain(&sys); + CHECK(count_kind(a, AnomalyKind::JtagChainBreak) >= 1); + CHECK(count_kind(a, AnomalyKind::JtagBusUnbridged) >= 1); +} + +TEST_CASE("check_jtag_chain reports an incomplete TAP") { + System sys; + Module *m = sys.modules()->merge("M"); + Part *u1 = new Part("U1"); + // only TDI + TDO, no TMS/TCK + mkpin(u1, "TDI", PinDirection::In, PinFunction::JtagTdi); + mkpin(u1, "TDO", PinDirection::Out, PinFunction::JtagTdo); + m->add(u1); + + auto a = check_jtag_chain(&sys); + CHECK(count_kind(a, AnomalyKind::JtagTapIncomplete) == 1); +}