Skip to content

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.