Skip to content

Pattern Export

The shared, format-independent TAP-transaction op vocabulary (warptap.tap_ir), and the SVF/STAPL/STIL text emitters that render it.

A tester-ready STIL file

to_stil always declares TCK, TMS, TDI and TDO. Declare the DUT's other pins as well: inputs with the value each one holds, outputs (never compared), one signal per bit of a bus. Start the pattern with a reset lead-in that clocks TCK while TRST is low:

from warptap import GotoState, SetPins, select_instruction, to_stil
from warptap.tap_fsm import TapState
from warptap.tap_model import OPCODE_EXTEST

lead_in = [
    SetPins((("trst_n", 0), ("rst_n", 0))),  # assert TRST and the chip reset
    GotoState(TapState.TEST_LOGIC_RESET),    # five TCK cycles with TMS=1
    SetPins((("trst_n", 1), ("rst_n", 1))),  # release both
    GotoState(TapState.RUN_TEST_IDLE),       # one TMS=0 cycle
]
stil = to_stil(
    lead_in + select_instruction(OPCODE_EXTEST) + pdl.program,
    jtag_period="50ns",
    pulse_periods={"clk": "10ns"},
    inputs={"trst_n": 1, "rst_n": 1, "clk": 0, "func_addr[0]": 0, "func_addr[1]": 1},
    outputs=["func_dout[0]", "func_dout[1]"],
)

In each TCK cycle TMS and TDI change at 0, TDO is compared at a quarter period, and TCK rises at half and falls at three quarters; to_stil below says why. A Runtest or PulsePin of many cycles is written as one Loop. Releases up to 0.0.3 compared TDO after the rising edge, so on warptap's own TAP every compare checked the next bit.

warptap.tap_ir

TAP-transaction IR (implementation_plan.md §3.4, §7 Stage 5). One internal representation for a scan-test sequence, independent of output format -- built now, not deferred to Stage 7, because §3.4's own stated justification ("reduces header+payload+trailer composition from the SIB-tree retargeting result to shift/OR/mask arithmetic") describes exactly the composition problem pdl_interpreter.PDLInterpreter.iApply already has to solve. A Stage-5-private alternative would need the same fields anyway; Stage 7's SVF/STAPL emitters and Stage 5's own tap_ir_play.play() become two stateless backends walking the same list.

Scan data is an arbitrary-precision Python int with an explicit bits width and an optional mask (which bits of tdo to actually compare), not a bit-list -- matches SVF/ STAPL's own LSB-first hex conventions and keeps composition to shift/OR/mask arithmetic.

Bit-encoding convention for tdi/tdo/mask: bit i is the value shifted on cycle i + 1 of that op -- chronological/feed order, matching how this project's own tests already represent an observed shift-out sequence as an int (sum(bit << i for i, bit in enumerate(observed_bits))). Any code composing these ints from :mod:warptap.sib_layout's position-ordered bit lists (index 0 = the chain position nearest TDI, i.e. what that position ends up holding once the whole shift completes) must reverse the list first -- the two orders are exact reverses of each other, since the last bit fed ends up at the position nearest TDI (the same "last fed bit lands at position 0" rule every prior stage's directed tests already rely on).

GotoState/Runtest reuse :class:~warptap.tap_fsm.TapState directly rather than a parallel enum -- the same "one canonical table, never two that could drift apart" reasoning implementation_plan.md §7 Stage 2 already gives for sharing the FSM table between the RTL codegen and the Python model.

GotoState

Bases: NamedTuple

ShiftIR

Bases: NamedTuple

ShiftDR

Bases: NamedTuple

Runtest

Bases: NamedTuple

PulsePin

Bases: NamedTuple

Pulse a named signal OTHER than TCK/TMS/TDI/TDO (implementation_plan.md §7 Stage 13) -- e.g. a DUT's functional/system clock -- count times, independent of TCK's own timing domain. TAP pins (TCK/TMS/TDI) stay static throughout. hold_pins optionally drives other named signals (e.g. a faultflow pattern's capture_pi_values) to fixed values for the same duration -- both only ever meaningful together, at the moment of forcing a functional capture edge, hence one op rather than two.

Exists because gating this pulse from TCK itself (an earlier design considered and rejected) is unsound for a real class of external ATPG patterns: a transition/delay fault needs an at-speed capture edge, which TCK -- externally, slowly, ATE-driven -- cannot provide. Deliberately NOT expressible via SVF/STAPL (a JTAG-pins-only vocabulary, no concept of a second clock domain) -- both emitters raise their own existing TapIrSvfError/TapIrStaplError for it; only :mod:warptap.tap_ir_stil can render it, since STIL's own WaveformTable mechanism lets an independently-timed signal coexist with the JTAG pins in one pattern.

SetPins

Bases: NamedTuple

Drive DUT pins other than TCK/TMS/TDI/TDO to new values from the next cycle on, until another SetPins changes them: e.g. assert trst_n for a reset lead-in, clock TCK, then release it. Unlike hold_pins, the values stay after the op. Only :mod:warptap.tap_ir_stil renders it, and each pin must be one of its inputs; the SVF and STAPL emitters and :mod:warptap.tap_ir_play raise their unsupported-op errors for it, as they do for :class:PulsePin.

bits_from_int

bits_from_int(value: int, width: int) -> list[int]

Inverse of :func:bits_to_int: unpack value into a width-long chronological-order bit list.

bits_to_int

bits_to_int(bits: list[int]) -> int

Pack a chronological-order bit list (index 0 = cycle 1) into this module's LSB-first int convention.

warptap.tap_ir_svf

SVF (Serial Vector Format) text emitter (implementation_plan.md §3.4, §6, §7 Stage 7). Walks the same :mod:warptap.tap_ir ops list :func:warptap.tap_ir_play.play/to_cycles already walk, producing real SVF text -- the second of §3.4's "stateless pretty-printers over one IR." Grounded directly in the SVF Specification, Revision E (ASSET InterTech / Texas Instruments, 8 March 1999) -- freely copyable/quotable per its own copyright notice -- and cross-checked against OpenOCD's real svf.c parser/executor source.

Bit-encoding: SVF's own bit-order rule ("the least significant bit... is the first bit scanned into the hardware for TDI... and is the first bit scanned out for TDO", spec p.5) is identical to tap_ir.py's own chronological/LSB-first int convention -- confirmed by direct comparison against the spec's own worked example (SIR 8 TDI (41); shifts LSB-first, cycle 1 = bit 0 of 0x41, exactly matching bits_from_int(0x41, 8)). No bit reversal is needed anywhere in this module -- op.tdi/op.tdo/op.mask format directly as hex.

GotoState ops are dropped, not translated. SVF's own SIR/SDR statements implicitly handle the entire "navigate from the current stable state into Shift-IR/DR, shift, return to whatever ENDIR/ENDDR currently specifies" sequence -- confirmed directly by the spec's own worked example, which never precedes a scan with an explicit STATE command. Since every GotoState this project's own ops ever emit only ever brackets a shift (matching tap_ir_play.py's own narrow navigation model), an explicit STATE translation would be redundant, not merely an optimization -- this module emits a single ENDIR IDLE;/ ENDDR IDLE; header instead (SVF's own default, per spec p.8, but stated explicitly here to match this stage's "always emit every field, no sticky-default optimization yet" discipline), which is exactly what every GotoState(RUN_TEST_IDLE) in this project's own ops already wants. The one exception is GotoState(TEST_LOGIC_RESET), a reset that SIR/SDR navigation never performs: it becomes STATE RESET;.

Deliberately emits every TDI/TDO/MASK field on every SIR/SDR -- SVF's own field "stickiness" (reusing the last value for an omitted field) is a real, legal compression this module doesn't use, matching §7 Stage 7's explicit instruction.

HDR/HIR/TDR/TIR (multi-device chain header/trailer padding) are omitted entirely -- confirmed safe by both the spec's own semantics ("A header can be removed by setting length to 0") and OpenOCD's own svf_para_init, which statically zeroes them before any file is even read. TRST is likewise never emitted -- warptap's own tap_ir ops have no TRST-equivalent op type, matching tap_ir_play.play()/to_cycles()'s own "neither function drives a reset itself" precedent (Stage 5).

TapIrSvfError

Bases: WarptapError

Raised when to_svf is given an op it can't render.

to_svf

to_svf(ir_ops: List[_IrOp]) -> str

Render ir_ops as SVF text, one statement per line, prefixed by an explicit ENDIR IDLE;/ENDDR IDLE; header. Raises :class:TapIrSvfError for an op this emitter can't represent (currently none of the ops this project's own callers produce trigger this -- GotoState is unconditionally droppable, see module docstring -- but a genuinely unsupported future op type would land here rather than silently emitting something wrong).

warptap.tap_ir_stapl

STAPL (JEDEC JESD71 / IEEE 1532) text emitter (implementation_plan.md §3.4, §6, §7 Stage 7). Walks the same :mod:warptap.tap_ir ops list the SVF backend (:mod:warptap.tap_ir_svf) and :mod:warptap.tap_ir_play already walk -- the third "stateless pretty-printer over one IR" §3.4 promises. Grounded in the full JESD71-1999 spec text and Altera/Actel's real reference STAPL-player source (jamcrc.c), fetched and read directly during Stage 7's own implementation session.

File shape: NOTE fields, one ACTION, one PROCEDURE containing the lowered ops, ENDPROC, then a CRC statement computed over everything before it (:mod:warptap.stapl_crc16). STAPL mandates the CRC statement be the file's last statement, computed over the whole file's ASCII source text up to that point -- this module therefore builds the file in two passes: render everything through ENDPROC;, compute the CRC over that text, then append the CRC <hex4>; line.

GotoState ops are dropped, matching the SVF backend's own reasoning: STAPL's IRSCAN/DRSCAN statements implicitly handle the full "navigate from the current stable state into the shift state, shift, return to whatever IRSTOP/DRSTOP currently specifies" sequence -- IRSTOP/DRSTOP are STAPL's own sticky-end-state directives, directly analogous to SVF's ENDIR/ENDDR, confirmed to default to IDLE the same way. This module emits an explicit IRSTOP IDLE;/DRSTOP IDLE; header, matching every GotoState(RUN_TEST_IDLE) this project's own ops already target. As in SVF, GotoState(TEST_LOGIC_RESET) is the exception and becomes STATE RESET;.

Bit-encoding: STAPL's own array-index convention (JESD71 §6.4/§8.8 -- "array index 0 corresponds to the rightmost character... shifted in increasing order of the array index," an explicit "least significant bit to most significant bit" framing in Annex A's own worked example) matches tap_ir.py's chronological/LSB-first int convention exactly, the same as SVF's -- no bit reversal needed; a scan literal is a plain positional hex string ($<hex>), MSB on the left, LSB on the right.

COMPARE clause, for ops carrying tdo/mask. Grammar confirmed this session against five independently-converging real sources -- the full JESD71-1999 spec text (read directly, §8.8 DRSCAN/§8.18 IRSCAN), real vendor-generated .jam/.stp files (Altera Quartus Prime, Altera's own "Chain Interrogation" utility, Microsemi FlashPro), and two independent copies of the Altera "Jam STAPL Player" reference C source (jamexec.c, sibling of the jamcrc.c already used for :mod:warptap.stapl_crc16) -- all agreeing on the same grammar, unlike an earlier, lower-confidence secondary summary that wrongly implied a separate MASK keyword:

DRSCAN <length>, <data> [,COMPARE <compare array>, <mask array>, <result>];
IRSCAN <length>, <data> [,COMPARE <compare array>, <mask array>, <result>];

COMPARE is not a standalone statement -- it is a trailing clause on DRSCAN/IRSCAN, taking exactly three positional (unlabeled) arguments: the expected value, a mask (bit 1 = compare this position, 0 = don't-care), and a previously-declared scalar BOOLEAN variable that receives the result. <compare array>/<mask array> may be bare hex literals (confirmed in a real Microsemi FlashPro file: DRSCAN 8, $00,COMPARE $00,$80, PASS;) -- this emitter uses that form, avoiding any need to also declare/populate BOOLEAN array variables for the expected/mask data, matching this module's existing bare-hex-literal TDI convention.

Per JESD71's own text, a failed COMPARE sets <result> FALSE but does not abort execution by itself -- every real file this session found follows a COMPARE with an explicit check (IF <result> == 0 THEN ...) to actually act on failure, exactly the pattern this emitter reproduces (IF <result_var> == 0 THEN EXIT (1);, real syntax confirmed directly in Altera's IDCODE.JAM: IF (stuck_tdo_flag == 1) THEN EXIT (1);). Without this, STAPL's COMPARE would silently NOT enforce what SVF's TDO/MASK enforces automatically (SVF's own player checks and reports a mismatch with no extra statement needed in the file, confirmed directly against a real OpenOCD svf run this session) -- the two emitted formats would otherwise diverge in behavior for identical ir_ops input. Each COMPARE-bearing op gets its own uniquely-named result variable (compare_result_<n>), declared with a plain BOOLEAN <name>; statement immediately before its first use, directly inside PROCEDURE DO_RUN -- the Jam 1.1 language spec's own inline example (BOOLEAN verify_error; DRSCAN ..., COMPARE ..., verify_error;) shows a local declaration immediately preceding use with no surrounding DATA/USES block, which this module follows since its own PROCEDURE DO_RUN has no such block either; real vendor-generated files instead declare variables in a separate DATA block referenced via USES on the PROCEDURE line, an alternative, more elaborate structure not adopted here.

TapIrStaplError

Bases: WarptapError

Raised when to_stapl is given an op it can't render -- an unsupported op type, or a ShiftIR/ShiftDR with exactly one of tdo/mask set (STAPL's COMPARE clause needs both together, see module docstring).

to_stapl

to_stapl(ir_ops: List[_IrOp], *, device: str, date: str, creator: str = 'warptap', stapl_version: str = '1.01', alg_version: str = '1', stack_depth: str = '1', target: str = '1', max_freq: str = '10000000') -> str

Render ir_ops as a complete, single-PROCEDURE STAPL file, ending in a real, computed CRC statement. device/date are required (this project doesn't invent device-identifying data, matching e.g. Stage 4's InstrumentNode discipline).

target (JESD71 §10.2, Table 13) is not free text: it's a comma-separated list of 1-based chain-position numbers, counted from the TDI side toward TDO, naming which device(s) in the physical scan chain this file's operations address. It defaults to "1" (the first, and in warptap's v1 scope the only, device in the chain) -- a real, meaningful default, not an "unspecified" placeholder, since warptap only ever targets a standalone TAP in v1, never a multi-device daisy chain. The remaining NOTE fields default to explicitly-labeled placeholder values -- this project has no real frequency/version data to report either, and says so via these defaults rather than fabricating plausible-looking ones.

warptap.tap_ir_stil

STIL (IEEE Std 1450, "Standard Test Interface Language") text emitter (implementation_plan.md §7 Stage 13). The fourth "stateless pretty-printer over one IR" (:mod:warptap.tap_ir_svf, :mod:warptap.tap_ir_stapl, and this module all walk the same :mod:warptap.tap_ir ops list), built specifically to carry :class:~warptap.tap_ir.PulsePin -- a functional/system-clock pulse independent of TCK's own timing domain, which SVF/STAPL (a JTAG-pins-only vocabulary) cannot express at all. See :class:~warptap.tap_ir.PulsePin's own docstring for why this capability exists and why it isn't modeled as TCK gating a functional clock instead.

Grounded in real primary sources fetched directly this session: the IEEE 1450 working group's own "D03" STIL Reference Guide and a real worked 1450-1999 example file (both hosted at grouper.ieee.org/groups/1450) -- the actual ratified standard text is paywalled and was not independently read. Live-validated against a real, independent, pip-installable parser, Semi-ATE-STIL (github.com/Semi-ATE/STIL, Lark-based) -- despite its own "not yet ready for production" disclaimer, it both syntax- and semantic-validated real STIL text this session, including the exact multi-WaveformTable mechanism this emitter depends on.

Real, empirically-discovered requirements this module's output follows (found by actually feeding files through Semi-ATE-STIL, not derived from documentation alone -- the Reference Guide's own condensed worked example omits several of these):

  • A WaveformTable's per-signal event schedules must be wrapped in a Waveforms { ... } block -- the real worked 1450-1999 example this session's research found omits this wrapper entirely (an older/simplified presentation), but Semi-ATE-STIL's own grammar requires it.
  • Block order is significant and was not documented anywhere this session's research found: SignalGroups must precede Timing; PatternBurst and PatternExec must both precede any Pattern block they reference.
  • Every signal referenced in ANY vector under a given WaveformTable must have its OWN WFC (waveform character) defined in THAT table -- switching tables mid-pattern (this module's own mechanism for letting the TCK domain and a functional-clock domain coexist) means every signal needs an entry in EVERY table the pattern ever selects, even if that entry is just "hold" (P) or "don't-compare" (X) for a signal irrelevant to that table's own phase.
  • STILParser.parse_semantic()'s return value is always None regardless of outcome -- real success is parser.is_parsing_done is True with parser.err_msg == "" (confirmed by reading its own source directly; a naive if parser.parse_semantic(): would silently treat every file, valid or not, as failing).

No real, independent-tool-confirmed convention exists for encoding a JTAG SIR/SDR-style scan operation as STIL vectors -- this session's own research found no public example (an EDA vendor's own IJTAG-to-STIL translator was cited as existing, but its actual per-cycle convention isn't public). This emitter's own convention -- one V{} per TCK cycle (a Runtest or PulsePin run is one V{} in a Loop); TMS/TDI driven via :func:warptap.tap_ir_play.navigation_tms/:func:~warptap.tap_ir_play.shift_tms; TDO compared (H/L) when a bit falls within a ShiftIR/ShiftDR's own mask, don't-compared (X) otherwise -- is a reasoned construction from STIL's own confirmed primitives, not an established industry idiom, stated here rather than presented as settled convention.

Where each edge and strobe sits in a period is :func:to_stil's docstring. Semi-ATE-STIL's syntax and semantic checks cannot see it, so tests/test_tap_ir_stil_replay.py replays emitted files literally against rtl/tap_core.v and inserted designs in Icarus and counts TDO mismatches; it fails on the 0.0.3 strobe, which compared TDO after the rising TCK edge.

One WaveformTable per distinct :class:~warptap.tap_ir.PulsePin target port, each declaring hold/force/don't-compare WFCs for every OTHER known signal (TCK/TMS/TDI/TDO, every other pulse port, every hold_pins key seen anywhere in ir_ops, every declared input and output) -- the mechanism, confirmed empirically against Semi-ATE-STIL, that lets an independently-timed functional clock pulse coexist with the TAP's own JTAG-speed WaveformTable in one Pattern, switched via a plain W <name>; statement.

TapIrStilError

Bases: WarptapError

Raised when to_stil is given an op it can't render, a period it can't read, a pulse_periods mapping missing an entry for some :class:~warptap.tap_ir.PulsePin target this ir_ops list actually uses, or a pin it can't declare or drive as asked.

to_stil

to_stil(ir_ops: List[_IrOp], *, jtag_period: str = '100ns', pulse_periods: Optional[Dict[str, str]] = None, inputs: Optional[Mapping[str, int]] = None, outputs: Iterable[str] = ()) -> str

Render ir_ops as a complete STIL file: Signals/SignalGroups/Timing (one WaveformTable for ordinary JTAG cycles, one more per distinct :class:~warptap.tap_ir.PulsePin target port)/PatternBurst/PatternExec/ Pattern (block order matches this module's own empirically-confirmed requirement, see module docstring). jtag_period/pulse_periods are real caller inputs -- STIL always requires explicit timing, this project has no real frequency data to invent (matches :mod:warptap.tap_ir_stapl's own NOTE field discipline). Each is a positive number with a unit (fs, ps, ns, us, ms or s), e.g. '50ns'; the edge times are written in the same unit. Raises :class:TapIrStilError for an unsupported op, a period it can't read, or a PulsePin target port missing from pulse_periods.

Pins. TCK, TMS, TDI (In) and TDO (Out) are always declared. inputs declares the DUT's other inputs, each with the value (0 or 1) it holds: it is driven in every vector, except where a PulsePin pulses it or lists it in hold_pins. outputs declares the DUT's other outputs: Out, and never compared. A tester-ready file declares every DUT pin this way. A PulsePin port or hold_pins key left out of inputs is still declared In, but only a pulse that names it ever drives it: JTAG cycles and other pulses hold whatever it had (P), and nothing drives it before. Up to 0.0.3 a PulsePin drove every such pin it did not list to 0, which asserts an active-low reset. Naming a TAP pin, a pin in both, or an output in a PulsePin raises :class:TapIrStilError. A bus is one signal per bit, named like func_addr[0]; a name that is not a plain identifier is written in double quotes, as STIL requires, in every block and in a PulsePin's WaveformTable name.

Reset lead-in. :class:~warptap.tap_ir.SetPins changes declared inputs from the next vector on, so a pattern can reset the TAP itself, e.g. with inputs={"trst_n": 1, ...}::

[SetPins((("trst_n", 0),)),             # assert TRST
 GotoState(TapState.TEST_LOGIC_RESET),  # five TCK cycles with TMS=1
 SetPins((("trst_n", 1),)),             # release TRST
 GotoState(TapState.RUN_TEST_IDLE)]     # one TMS=0 cycle

TCK runs while TRST is low, which matters: tap_core and the network cells reset on negedge trst_n or on a TCK edge while it is low, so a TRST that is low from the start (in simulation, or a tester pin that powers up low) has no falling edge and needs those TCK edges. Without a TRST pin the two GotoState still reset the TAP through TMS, but not the SIB network, which only TRST resets. Without either, the ops must start where the TAP already is: in Run-Test/Idle.

Loops. A Runtest or PulsePin with count above 1 is one V inside Loop count { ... }, so an MBIST run loop of thousands of cycles is one statement. When such a run opens the pattern, its first cycle is written as a plain V and the rest loop: Semi-ATE-STIL's compiler, like a tester that loads the loop count on the vector before the loop, needs a vector first.

Timing. One jtag_wft vector is one TCK cycle of period T:

  • TMS and TDI change at 0;
  • TDO is compared at T/4;
  • TCK rises at T/2 and falls at 3T/4, so it idles low between cycles.

A vector's expected TDO bit is the one the TAP shows before that vector's rising edge, the edge that shifts it out: :meth:~warptap.tap_model.TapModel.tick returns it before it clocks, and ShiftIR/ShiftDR's tdo holds those bits. rtl/tap_core.v changes TDO on the rising edge, so that bit is valid from the previous rise (-T/2) to this one (T/2). A TAP that changes TDO on the falling edge, as IEEE 1149.1 requires, shows it from the previous fall (-T/4) to this cycle's fall (3T/4). A strobe at T/4 lies inside both windows and is at least T/4 from any TDO change in either. TMS and TDI get T/2 of setup before the rise and T/2 of hold after it. Up to 0.0.3, TCK rose at 1ns and TDO was compared at 2ns, after the rising edge, so on tap_core every compare checked the next bit.

A PulsePin's port has the same shape in its own period: low at 0, rising at T/2, falling at 3T/4. Its hold_pins change at 0, half a period before the rising edge.