Insertion¶
Building a network plan, then actually inserting it into a netlist.
warptap.sib_plan ¶
Pure planning phase for SIB network insertion (implementation_plan.md §7 Stage 4, §3.2).
Mirrors bsr_plan.py's split: this module only decides what the network looks like: one
top-level SIB per requested instrument or nested sub-network, chained TDI-side-first in
caller-given order. The impure surgery that actually inserts cells into a netlist lives in
sib_insert.py.
Unlike build_bsr_plan, this isn't derived from a target module's existing ports --
instruments are explicitly caller-specified, since warptap doesn't invent instrument content
(a real instrument is TAM/DFT logic outside this project's scope; v1 wraps a fixed-value stub,
see sib_insert.py).
HierarchySpec ¶
Bases: NamedTuple
A SIB that gates a nested sub-network instead of one instrument directly -- real IEEE
1687 (a SIB's fromSO binds to another SIB's own SO rather than an instrument's).
Its own SIB name is derived the same way an instrument's is (f"sib_{name}"), for
naming consistency and so :func:~warptap.icl_model.validate_physical_graph's global
uniqueness check treats both kinds of node the same way. Contributes no
:class:~warptap.icl_model.ModuleInstance node of its own -- only leaf instruments ever
do, flattened into one sibling list under the root regardless of nesting depth (SIBs
themselves were never part of the dotted-address space, matching v1's existing
sibling-addressing convention).
InstrumentSpec ¶
Bases: NamedTuple
build_sib_plan ¶
build_sib_plan(specs: Sequence[_Spec], *, top_name: str = 'top') -> tuple[PhysicalGraph, ModuleInstance]
One top-level SIB per spec, chained in specs order (index 0 nearest TDI --
PhysicalGraph.chain's own documented ordering). A :class:HierarchySpec recurses,
gating a nested sub-network instead of a single instrument -- built the same way at every
depth. Each SIB is named f"sib_{spec.name}", deterministic and human-legible in
emitted RTL/attributes.
Returns a flat :class:ModuleInstance tree regardless of nesting depth: every instrument
a direct sibling child of the root, matching the confirmed sibling-addressing convention
(§3.2) -- SIBs themselves aren't represented as ModuleInstance nodes, since dotted-address
resolution only ever needs to reach an instrument, and each instrument's gating SIB is
already recorded on its :class:~warptap.icl_model.SibNode in the returned graph.
top_name defaults to the placeholder "top" every prior stage's own tests already
use, but a real caller wanting the returned tree's root to name the actual target module
(implementation_plan.md §7 Stage 10's own need: icl_emit.to_icl() renders
root.name as the emitted ICL file's real Module <name> { ... } name) should pass
the same string given to sib_insert.insert_sib_network's own top argument --
nothing today enforces the two calls agree, since there is no single orchestration entry
point yet threading one value to both; document the invariant here rather than silently
assume it.
Raises :class:ValueError on a duplicate name within the same level -- silently letting
two specs share a name would make both resolve_dotted_address and
sib_retarget.open_path_to ambiguous. Raises
:class:~warptap.icl_model.ICLModelError (via :func:~warptap.icl_model.validate_physical_graph)
for a name that repeats across different levels, which this function's own per-level
check can't see.
warptap.sib_insert ¶
SIB network insertion: the impure surgery phase (implementation_plan.md §7 Stage 4).
Given a :class:~warptap.icl_model.PhysicalGraph (from sib_plan.build_sib_plan), imports
tap_core.v, sib_cell.v, bc1_shift_only.v, and instrument_write.v as
hierarchical submodules (the same "hand-author once, capture as JSON" mechanism
bsr_insert.py established), instantiates one SIB per :class:~warptap.icl_model.SibNode
plus one chained instrument-bit cell per instrument bit, and wires the whole network into
tap_core's external_dr_tdo input, exactly as insert_bsr() does for the
boundary-scan chain. Each instrument is either READ (a bc1_shift_only cell, pi tied to
a fixed-value stub or fanned out from a real host port) or WRITE (an instrument_write cell,
pin_out wired onto a real host port in place of that port's prior drivers) --
implementation_plan.md §7 Stage 9's real (non-stub) functional-instrument extension.
Deliberately fully independent of bsr_insert.py/tap_core.v (implementation_plan.md
§7 Stage 4's own scoping decision): reuses external_dr_tdo directly, no new TAP
instruction, zero edits to any Stage 1-3 file. _import_template is duplicated rather than
shared (exactly two occurrences -- the textbook "rule of three" case for not extracting yet).
The network is selected by EXTEST only. tap_core's capture_dr/shift_dr/
update_dr are per-state strobes that fire on every instruction's DR scan, so, like
bsr_insert.py's extest_mode, one $eq decode cell (warptap_ijtag_extest_decode)
compares the TAP's current instruction with EXTEST and drives warptap_ijtag_select,
every top-level slot's select. Nested slots and WRITE commits are gated through it (a
SIB's nested_active/nested_select both AND in its own select). Without it, a DR
scan under IDCODE (loaded by every reset) or BYPASS (a board chain passing through)
shifted, opened and committed the network: two all-ones scans raised every WRITE
instrument. bc1_shift_only/instrument_write shift registers still move on any DR
scan, which is harmless: they reach tdo only through an open SIB, and a commit needs one.
SAMPLE_PRELOAD, which tap_core also routes to external_dr_tdo, therefore has no
working data register in a SIB-only design (its model has none either).
With :func:insert_sib_network's ijtag_access_opcode, a dedicated IJTAG_ACCESS instruction
selects the network instead, so a board-level EXTEST can't reach it. tap_core is imported
with that opcode in both OPCODE_EXTEST and OPCODE_SAMPLE_PRELOAD
(:func:_import_tap_core): it is then the only opcode routed to external_dr_tdo, EXTEST's
and SAMPLE/PRELOAD's own opcodes match nothing at Update-IR and load BYPASS, and the decode
cell (warptap_ijtag_access_decode) compares with it.
A :class:~warptap.icl_model.ScanMuxNode slot instantiates rtl/scan_mux_cell.v instead,
one FRESHLY-IMPORTED, uniquely-named module PER mux instance -- NOT the shared-import-once
pattern every other template here uses (:func:_import_template), and not a per-instance
add_cell(parameters={...}) override either, even though Yosys JSON's own cell-parameters
mechanism (a fixed-width MSB-first binary string) looks like it should support this. Confirmed
empirically, not assumed: a module imported via plain ingest() (the hierarchy/proc
pipeline every other template already goes through once) permanently loses its own
overridability -- instantiating it with ANY parameter override afterward fails to compile
(iverilog reports "parameter ... not found"), regardless of whether that parameter affects
a port width or not. scan_mux_cell is the first template here that genuinely needs
different parameter values per instance (every other one is either unparameterized or, like
tap_core, instantiated with its own defaults), so this gap was never exposed before.
tap_core gets the same chparam treatment when :func:insert_sib_network is given a
non-default idcode_value (:func:_import_tap_core). :func:_import_scan_mux_template instead calls
:func:~warptap.yosys_io.ingest_with_params per mux node, baking ARMS/SEL_WIDTH/
ARM_VALUES in via Yosys's own chparam (a plain-decimal value syntax, a different
layer/format from JSON cell parameters) before hierarchy runs, under a module name
derived from the mux's own globally-unique mux_name (warptap_scan_mux_<mux_name>,
matching the prefix icl_import.py's own future detection logic looks for) -- one Yosys
module per distinct mux instance, instantiated with zero further overrides.
Each mux's own instance is tagged warptap_mux_name (not warptap_sib_name, so every
existing SIB-only structural test stays unaffected); each arm's own leaf-instrument wiring is
:func:_insert_leaf_instrument_bits, a deliberate duplicate of the SibNode leaf case below
(two occurrences; keeps this new code path from touching the already real-RTL-cross-sim-proven
SibNode one at all).
SibInsertError ¶
Bases: WarptapError
Raised when a :class:~warptap.icl_model.PhysicalGraph asks for something this
insertion pass doesn't support -- a SIB with neither an instrument nor a nested
sub-network, a zero-width instrument, a WRITE instrument with no signal_bits (nothing
for it to drive), or a signal_bits length that doesn't match the instrument's
width -- rather than silently emitting something structurally broken.
insert_sib_network ¶
insert_sib_network(netlist: Netlist, top: str, graph: PhysicalGraph, *, ir_width: int = DEFAULT_IR_WIDTH, opcode_extest: int = DEFAULT_OPCODE_EXTEST, idcode_value: int = IDCODE_VALUE, yosys_command: str | None = None, chip_reset: str | None = None, chip_reset_active_low: bool = True, ijtag_access_opcode: int | None = None) -> None
Insert a TAP + SIB network into netlist's top module, matching graph
(from sib_plan.build_sib_plan). Adds five new top-level JTAG ports (tck/tms/tdi/
trst_n/tdo) -- top must not already have a TAP inserted (this function doesn't
coexist with bsr_insert.insert_bsr on the same module; implementation_plan.md §7
Stage 4 §1's documented scoping decision).
top should be the same string passed as sib_plan.build_sib_plan's own
top_name when that graph's accompanying :class:~warptap.icl_model.ModuleInstance
tree was built (Stage 10's icl_emit.to_icl() renders that tree's root name as the
emitted ICL file's real module name) -- nothing here enforces that the two agree.
Each slot in graph.chain (and, recursively, in any nested sub-chain -- see
:func:_insert_chain) becomes one sib_cell instance plus, for a leaf slot,
slot.instrument.width chained instrument-bit cells wired between the SIB's
nested_si/nested_so (implementation_plan.md §7 Stage 9): a READ instrument gets
bc1_shift_only cells, each pi either tied to one bit of the fixed
capture_value stub (no signal_bits) or fanned out from one real host port bit
(signal_bits given) -- bc1_shift_only_sync instead, a two-TCK-flop synchronizer
before capture, when the instrument asks for capture_sync (a signal from another
clock domain); a WRITE instrument gets instrument_write cells, each
pin_out wired directly onto one bit of its target host port -- detach_port once
per distinct port name, so every existing internal driver/reader of that port's old bits
is now driven by JTAG instead. A WRITE cell's select is wired to its own SIB's
nested_select output (sib_cell.v's po & shift_ff -- open both before and
after the current edge) so its update-latch only ever commits on an edge that leaves
that SIB open, never one that opens or closes it -- without it, an unrelated
iApply's phase-1 retargeting shift (which walks every instrument cell's
capture_dr/shift_dr/update_dr unconditionally, matching every other cell)
would commit garbage into this instrument's real host signal, including while
retargeting away from it. Every top-level slot's select is the TAP's EXTEST
decode (IJTAG_ACCESS's, see below), so none of this moves under any other instruction
(see this module's docstring). Mutates
netlist in place and returns None -- matching insert_bsr()'s convention; the
caller already has the :class:~warptap.icl_model.ModuleInstance tree from
build_sib_plan for dotted-address resolution, since instrument naming (unlike Yosys
cell instance naming) is decided entirely at planning time, not here.
ir_width/opcode_extest must match rtl/tap_core.v's actual compiled
parameters (its own defaults, currently) since this function never overrides them on
the instance. opcode_extest is unused with ijtag_access_opcode.
idcode_value is baked into the inserted tap_core's IDCODE_VALUE (default:
:data:warptap.tap_model.IDCODE_VALUE, a placeholder). It must be a 32-bit value with bit 0
set, as IEEE 1149.1 requires, else :class:SibInsertError. Pass the same value to
:func:warptap.bsdl_emit.to_bsdl and to TapModel/TapConfig when modelling this TAP.
chip_reset names a single-bit input of top, the chip reset (active low unless
chip_reset_active_low is false). Every WRITE instrument then also clears on it, not
only on trst_n, so the signals it drives come up deasserted after a chip reset even
if TRST never pulsed: its cells are instrument_write_clr, cleared by tck_reset_sync,
which asserts with the chip reset and releases two TCK edges after it -- an Update-DR in
those two edges is lost. Off by default, and then nothing changes.
ijtag_access_opcode gives the network an instruction of its own, IJTAG_ACCESS, at that
opcode (:data:warptap.tap_model.OPCODE_IJTAG_ACCESS is the suggested one): it alone
selects the network, and EXTEST, SAMPLE and PRELOAD select BYPASS -- there is no boundary
register -- so a board-level EXTEST leaves the network, and every signal a WRITE instrument
drives, untouched. It must fit ir_width and not be EXTEST's, SAMPLE/PRELOAD's, IDCODE's
or BYPASS's opcode, else :class:SibInsertError. Give :func:warptap.bsdl_emit.to_bsdl,
TapModel and TapConfig the same value, and :func:warptap.icl_emit.to_icl
ijtag_access=True. Off by default, and then nothing changes.
warptap.bsr_insert ¶
Boundary-scan register insertion: the impure surgery phase (implementation_plan.md §7
Stage 3). Given a :class:~warptap.bsr_plan.BsrPlan for a target module, imports
tap_core.v and the BC cell templates as hierarchical submodules (plan §4.2's "hand-author
once, capture as JSON" mechanism realized as ordinary Yosys submodule instantiation; see
netlist.Netlist.add_module's docstring for why this is simpler than flattened cloning),
instantiates one cell per :class:~warptap.bsr_plan.BsrCell, stitches them into a single
shift chain, and wires the chain into tap_core's external_dr_tdo input.
BIDIR/BC_7 cells reuse the target design's own existing tri-state driver for their port
(func_in/enable), found by pattern-matching a $mux cell with one operand tied to the
constant "z": confirmed empirically that Yosys's default proc lowering of the standard
assign io = enable ? drive_val : 1'bz; idiom produces exactly this shape, not a dedicated
$tribuf cell (which only appears from passes this project's ingest pipeline doesn't run).
BsrInsertError ¶
Bases: WarptapError
Raised when the target design's structure doesn't match what a Stage 3 insertion step needs, e.g. no existing tri-state driver found for a bidir port, rather than guessing and silently emitting something wrong.
insert_bsr ¶
insert_bsr(netlist: Netlist, top: str, plan: BsrPlan, *, ir_width: int = DEFAULT_IR_WIDTH, opcode_extest: int = DEFAULT_OPCODE_EXTEST, yosys_command: str | None = None) -> None
Insert a TAP + boundary-scan chain into netlist's top module,
matching plan (from bsr_plan.build_bsr_plan). Adds five new top-level
JTAG ports (tck/tms/tdi/trst_n/tdo). Every scanned port's original bits stay
fully wired to whatever originally drove/read them: output3 pins get
detach_port-ed (the real pin becomes new bits driven through a tri-state
buffer), but their old bits remain the cell's functional passthrough input, so
normal-mode (trst_n asserted) behavior is provably unchanged; see
tests/test_bsr_insert_equivalence.py.
ir_width/opcode_extest must match rtl/tap_core.v's actual compiled
parameters (its own defaults, currently; see the module-level constants above)
since this function never overrides them on the instance.
warptap.pipeline ¶
End-to-end insertion convenience (library-quality pass, post-Stage 14). Extracts the exact
ingest -> build network -> insert -> re-emit sequence this project's own test suite otherwise
repeats by hand in every cross-sim test (e.g. tests/test_sib_insert_write_instrument_cross_sim.py's
own _build_and_insert helper, tests/test_mem_subsystem_mbist_pdl_verify.py's inline
equivalent) -- the single most common orchestration a real caller needs, now available as one
function instead of five.
Deliberately scoped to the SIB/IJTAG path only. Boundary-scan register insertion
(:func:~warptap.bsr_insert.insert_bsr) stays a separate, independent step -- this project has
always treated BSR and SIB/IJTAG insertion as two distinct concerns, and this function doesn't
change that.
insert_test_access ¶
insert_test_access(sources: List[Union[Path, str]], top_module: str, specs: List[TestAccessSpec], *, yosys_command: str | None = None, use_sv: bool = False, port_renames: dict[str, str] | None = None, idcode_value: int = IDCODE_VALUE, chip_reset: str | None = None, chip_reset_active_low: bool = True, ijtag_access_opcode: int | None = None) -> Tuple[str, PhysicalGraph, ModuleInstance]
Ingest sources, build the SIB/instrument network specs describes, insert it into
top_module, and return the synthesizable inserted Verilog text plus the
(graph, root) pair needed to drive a :class:~warptap.pdl_interpreter.PDLInterpreter
against that same network.
specs may freely mix plain :class:~warptap.sib_plan.InstrumentSpec leaves with
:class:~warptap.sib_plan.HierarchySpec entries (a SIB gating a nested sub-network instead
of one instrument, at any depth -- see docs/guide/nested-sib-networks.md); this function
was flat-only before this parameter widening, purely by never having offered the choice --
:func:~warptap.sib_plan.build_sib_plan (called below, unchanged) already accepted the
same union, so no behavioral change was needed to support this, only the type and this
docstring were out of date with what the function actually did.
Every leaf instrument's own name becomes one of root's children 1:1, flattened
regardless of nesting depth (:func:~warptap.sib_plan.build_sib_plan's own documented
sibling-addressing convention -- a HierarchySpec never contributes a
:class:~warptap.icl_model.ModuleInstance of its own), so the returned root is ready to
pass straight into PDLInterpreter(graph, root) -- no separate bookkeeping needed to keep
the module-instantiation tree in sync with the physical network, since both come from this
one call.
port_renames ({old_name: new_name}) is applied to top_module via
:meth:~warptap.netlist.Module.rename_port, right after ingest and BEFORE
insert_sib_network claims its own hardcoded top-level JTAG pin names
(tap_ports.TDI/TDO/etc.) -- needed when the design being wrapped already has a
top-level port under one of those names (e.g. a faultflow compression/compaction composed
netlist's own tdi/tdo channel bus). Every InstrumentSpec.signal_bits referencing
a renamed port must use the NEW (post-rename) name -- this function doesn't rewrite
specs for the caller.
idcode_value is the inserted TAP's IDCODE (see
:func:~warptap.sib_insert.insert_sib_network): a 32-bit value with bit 0 set, else
:class:~warptap.sib_insert.SibInsertError, raised before anything is ingested. Give
:func:~warptap.bsdl_emit.to_bsdl the same value.
chip_reset/chip_reset_active_low make every WRITE instrument also clear on the
chip reset input (see :func:~warptap.sib_insert.insert_sib_network); off by default.
ijtag_access_opcode gives the network its own instruction, IJTAG_ACCESS, at that opcode,
and EXTEST/SAMPLE/PRELOAD then select BYPASS (see
:func:~warptap.sib_insert.insert_sib_network); off by default. An opcode it can't take
raises :class:~warptap.sib_insert.SibInsertError before anything is ingested.