Changelog¶
Format loosely follows Keep a Changelog, one entry per implementation stage.
[Unreleased]¶
[0.0.5] - 2026-10-15¶
Behavior changes¶
polynomial_from_manifestrefuses a manifest whosearchitectureis not"one_shot". faultflow retired its one-shot decompressor (commitc665de9): every compressed manifest it writes now says"architecture": "continuous".retarget_compressed_faultflow_patternsmodels only the one-shot decompressor and never sees the manifest, so given the polynomial and phase shifter of a continuous chip it produced wrong ops without an error. Reading the polynomial throughpolynomial_from_manifestnow raisesFaultflowCompressionErrorand namesretarget_decompressed_faultflow_patterns, which picks the retarget from the manifest. A manifest with noarchitectureis still the one-shot decompressor.continuous_from_manifestreads the polynomial itself.
Not changed¶
- The one-shot compression cross-sim needs a faultflow from before
c665de9. It builds the chip with faultflow'sinsert_compression, which that commit removed, so against a current faultflow checkout it skips and says so. The continuous decompressor's cross-sim and cross-check run against current faultflow.
Added¶
- faultflow's continuous-flow decompressor (
faultflow_continuous.py, beside the one-shot port infaultflow_compression.py): the ring generator cleared on the scan-enable rise and fednum_channelschannel bits on every shift, so a pattern has channels x shifts free bits.injector_stagesandcontinuous_scan_in_rowsport its model;solve_pattern_channelssolves a pattern's whole load at once, strictly (a don't-care is aload_carethe pattern names), for the channel bit of every channel at every shift;continuous_from_manifestreads the decompressor frommanifest["compression"](architecture,polynomial,phase_shifter_taps,warmup, andinjectorsas a check);retarget_continuous_faultflow_patternspulses the clock once with scan enable low, then per shift writes that shift's channel word and pulses once with scan enable high, then captures and unloads as the one-shot flow does;retarget_decompressed_faultflow_patternspicks the retarget from the manifest'sarchitecture(absent: the one-shot decompressor). Tests: an independent numeric model of the ring, a cross-check of every scan-in row against faultflow's own model, and an Icarus Verilog run on a realinsert_continuous_compressionnetlist behind warptap's SIB network. A transition (launch) pattern is not retargeted, as in the one-shot flow. - IEEE 1500 wrappers and their wrapper serial port (WSP), the counterpart of the TAP and IJTAG support, for a block wrapped to IEEE 1500 or a SoC that chains such blocks. A wrapper's access logic -- the WIR, WBY and their decode -- is a test instrument: tested with an ICL description and a PDL-retargeted program, not with scan ATPG. The pieces:
wsp1500_model:Wrapper1500(boundary cells in wrapper chains, the WIR's width and instructions, port names),Wsp1500Sim/Wsp1500Chain(the registers cycle by cycle: WIR shift stage on the rising edge of WRCK and update stage on the falling edge, WRSTN, WBY, the boundary register, the safe / hold / functional cell modes, an unused code acting as WS_BYPASS, wrappers chained WSI to WSO), andWspChainDesignfor a SoC top.to_icl_1500/to_icl_1500_chain: the ICL of a wrapper and of wrappers chained on a SoC, in the standard shape the vendoredicl_parseraccepts (the ICL value of an instruction is its code with the bits reversed, since aScanRegisterenters its MSB);import_icl_1500rebuilds the model from thewarptap_*attributes and requires the regenerated ICL to equal the text byte for byte.WspResetandto_wsp_rows: theShiftIR/ShiftDR/Runtestoperations of a TAP played on the WSP (a WIR scan, a data scan with a CaptureWR cycle and an UpdateWR cycle, idle cycles).Wsp1500Interpreter(iReset,iTarget,iWrite,iRead,iRunLoop,iApply) withto_pdl_1500/import_pdl_1500: oneiApplyis one WIR scan through every wrapper of the chain, far wrapper's bits first, then one data scan through whatever each wrapper's instruction selects.build_wsp_integrity_program: reset, WIR length, every WIR code (a boundary register scan three times, each starting with a capture from the driven ports and leaving a tail of 0, 1, 0, so a cell that follows its port instead of holding shows; WBY; an unused code), WBY held across a WIR scan, a held update that shifts in the complement of the code in force, and a CaptureWR with the WIR selected, each cycle carrying what the model expects at WSO, at each wrapped output and at each wrapper chain's scan output; what the core decides is no care, and so is an input cell on a port the tester doesn't drive (drive=,()on a SoC). The program is awarptap-wsp-program(version 1) JSON a tool without warptap can play;check_wsp_integritycompares measured values with it andreplay_wsp_programplays it on a model.
[0.0.4] - 2026-10-04¶
Fixed¶
to_stilcompared TDO after the rising TCK edge, so each compare checked the next bit. In 0.0.3 and earlier every JTAG vector drove TCK low at 0 and high at 1ns, and compared TDO at 2ns.rtl/tap_core.vchanges TDO on the rising edge, so by 2ns TDO already showed the next bit (on a scan's last cycle, the 0 of Exit1). A STIL file from those releases that compares TDO against a warptap TAP fails wherever two consecutive expected bits differ: an IDCODE read-back fails 15 of its 32 compares, and a 1-bit read that should see 1 sees 0. Files without compares are unaffected. Each period T now drives TMS and TDI at 0, compares TDO at T/4, raises TCK at T/2 and lowers it at 3T/4. T/4 is inside the window where TDO shows the current bit both ontap_coreand on a TAP that changes TDO on the falling edge, as IEEE 1149.1 requires, and at least T/4 from any TDO change in either. The Semi-ATE-STIL syntax and semantic tests could not see where a strobe sits. New replay tests (tests/stil_replay.py,tests/test_tap_ir_stil_replay.py) apply every event of an emitted file at its time againsttap_coreand inserted designs in Icarus and count TDO mismatches. They fail on the 0.0.3 timing, and pass on it with only the strobe moved before the rise.- A
PulsePindrove every pin it did not list to 0. A pin named only byPulsePins (another pulse port, or another pulse'shold_pinskey) kept its value through JTAG cycles but was forced to 0 by any pulse that did not list it, which asserts an active-low reset such asrst_n. It now holds its value (P).
Behavior changes¶
Upgrading from 0.0.3 changes the STIL to_stil writes, beyond the fix above.
- Edge times follow the period.
jtag_periodand eachpulse_periodsvalue must be a positive number with a unit ('50ns','0.1us'); anything else, such as a STIL spec variable, raisesTapIrStilError. Edge times are written in the same unit, e.g.'12.5ns'. APulsePin's clock now rises at half its period and falls at three quarters (it rose at 1ns and stayed high), so like TCK it idles low, and itshold_pinsget half a period of setup. - A
RuntestorPulsePinof N > 1 cycles is oneVinsideLoop N, not NVstatements. When the run opens the pattern, its first cycle is a plainVbefore theLoop, which Semi-ATE-STIL's compiler requires. Anything that countedVstatements sees fewer. - Signals beyond TCK/TMS/TDI/TDO sort by bit number (
bus[2]beforebus[10]), and a name that is not a plain identifier is quoted. 0.0.3 wrote such names unquoted, which is not valid STIL. - SVF and STAPL write
GotoState(TEST_LOGIC_RESET)asSTATE RESET;instead of dropping it; they still drop every otherGotoState. Before,navigation_tmscould not reach Test-Logic-Reset, soplay(),to_cycles()andto_stil()raised for it.
Added¶
to_stil(..., inputs=..., outputs=...)declares every DUT pin.inputsmaps each other DUT input to the value (0 or 1) it holds in every vector; aPulsePinthat pulses it or lists it inhold_pinsoverrides it for that pulse.outputsare declaredOutand never compared. A pin named only by aPulsePinand left out ofinputsbehaves as before: declaredIn, driven only by the pulses that name it.SetPins, for a reset lead-in. A newtap_irop that changes declared inputs from the next vector on: asserttrst_n(and a chip reset), runGotoState(TEST_LOGIC_RESET), release them, runGotoState(RUN_TEST_IDLE). TCK keeps running while TRST is low, sotap_coreand the SIB network reset even when TRST has no falling edge (a tester pin that powers up low, or a simulation that starts it at 0); without TRST, a TMS reset leaves the SIB network unknown. Onlyto_stilrendersSetPins; SVF, STAPL,play()andto_cycles()raise for it.GotoState(TapState.TEST_LOGIC_RESET).navigation_tmsreaches it with five TMS=1 cycles from any state, andGotoState(RUN_TEST_IDLE)settles from there with one TMS=0.- Per-bit pins. A bus is one signal per bit, such as
func_addr[0], quoted inSignals,SignalGroups, everyWaveformTable(and a per-bit pulse port's table name),WandV. A counter fixture's replay proves each bit reaches its own DUT bit. - The
devextra installs Semi-ATE-STIL (pinned to 0.3.2) and lark, which the STIL tests and the replay need.docs/reference/pattern-export.mdshows a tester-readyto_stilcall. capture_syncon a READ instrument (InstrumentSpec(..., capture_sync=True)): its bits capture through a two-TCK-flop synchronizer (rtl/bc1_shift_only_sync.v, reset bytrst_n), for a signal from another clock domain. A capture shows the signal as it was two TCK edges earlier, never a value caught changing. Each bit has its own synchronizer, so a multi-bit value is coherent only if it holds still for those two edges, as a settled status does. Off by default: a network without it is inserted byte for byte as before. A WRITE instrument can't ask for it.chip_resetoninsert_sib_network/insert_test_access: every WRITE instrument also clears on the chip reset input (active low unlesschip_reset_active_low=False), not only ontrst_n, so the signals it drives come up deasserted after a chip reset even when TRST never pulsed. Its cells arertl/instrument_write_clr.v, cleared throughrtl/tck_reset_sync.v, which asserts with the chip reset and releases two TCK edges after it: the release is a TCK-domain path, never an asynchronous one near a TCK edge. An Update-DR within those two edges is lost, and with TCK stopped the clear holds. The model doesn't simulate a chip reset: a program must leave it inactive after its TRST lead-in. Off by default, byte for byte as before.- A dedicated IJTAG_ACCESS instruction (
ijtag_access_opcodeoninsert_sib_network/insert_test_access;tap_model.OPCODE_IJTAG_ACCESS,1100, is the suggested opcode, the only one two bits from every other instruction). It alone selects the IJTAG network, and EXTEST, SAMPLE and PRELOAD select BYPASS, since there is no boundary register: a board-level EXTEST and its DR scans leave the network, and every signal a WRITE instrument drives, untouched.rtl/tap_core.vis unchanged: the inserted copy has the opcode in itsOPCODE_EXTESTandOPCODE_SAMPLE_PRELOADparameters, so it is the only one routed to the network, and the select decode compares with it.TapModel,TapConfig(and so the integrity program, which then also tests EXTEST and SAMPLE/PRELOAD as BYPASS),to_bsdl(anIJTAG_ACCESSinstruction;REGISTER_ACCESSgives EXTEST, SAMPLE and PRELOAD the BYPASS register) take the same opcode, andto_icl(..., ijtag_access=True)namesIJTAG_ACCESSin the AccessLink. The program JSON'stapcarries the opcode only when set. Off by default, byte for byte as before.
Not changed¶
rtl/tap_core.vstill changes TDO on the rising TCK edge and drives it low outside the shift states. IEEE 1149.1 has TDO change on the falling edge and float when not shifting, and the BSDL's DESIGN_WARNING already says warptap's TAP doesn't. The new STIL timing passes on both kinds of TAP; a falling-edge TDO is a separate RTL change.
[0.0.3] - 2026-09-29¶
Behavior changes¶
Upgrading from 0.0.2 changes what the inserted TAP does. Check these first.
- The IJTAG network moves only while EXTEST (
0000) is loaded. In 0.0.2 every top-level SIB'sselectwas tied to 1, so the network captured, shifted, opened and committed on a DR scan under any instruction: SAMPLE/PRELOAD, IDCODE (loaded by every reset) and BYPASS included. Anything that relied on SAMPLE/PRELOAD, IDCODE or BYPASS moving the network will break; load EXTEST first. Under SAMPLE/PRELOAD the network's tail still reaches TDO, but the network holds still. - A pattern with no instruction load no longer reaches the network.
PDLInterpreter.programand the faultflow retargeting functions emit DR scans only. Played straight after a reset (IDCODE loaded), a write-only program took effect by accident in 0.0.2 and now does nothing: prependselect_instruction(OPCODE_EXTEST). No warptap test, fixture or library code path relied on the old behavior;docs/quickstart.mdand the faultflow guide omitted the EXTEST load and now include it, and thePDLInterpreterdocstring says it is required. - Entering Test-Logic-Reset through TMS reloads IDCODE. In 0.0.2 only
trst_nreset the instruction, so a 5xTMS=1 reset left the previous instruction (e.g. EXTEST) loaded. Anything that relied on the instruction surviving a TMS reset will break.
Added¶
-
Stage 24: faultflow compression/compaction-aware pattern retargeting. Closes a gap
faultflow_retarget.py's (Stage 14) naivechain_to_instrumentmodel couldn't express: a faultflow design with[compression]and/or[compaction]enabled has its rawscan_in_N/scan_out_Nchain ports become internal wires on the composed netlist, with the real external pins becoming a K-widetdi/tdochannel bus feeding a Galois-form LFSR ring generator (load side) or a registerless XOR-tree (unload side). New modulesfaultflow_compression.py/faultflow_compaction.pyaddretarget_compressed_faultflow_patterns/retarget_compacted_faultflow_patterns, each replacing only the side its own transform actually touches (the other side still goes through the existing per-chainchain_to_instrumentmechanism unchanged). Compression's own GF(2) math (care_bit_rows, the Gauss-Jordansolve_xor_broadcast, the curated LFSR polynomial table) is natively ported from real faultflow source, per this project's own "port understanding, not code" boundary with that project -- never imported or called.Module.rename_port(netlist.py) plus a newport_renamesparameter oninsert_test_access(pipeline.py) fix a real naming collision: warptap's own hardcodedtdi/tdoTAP pin names would otherwise crash against a compression/compaction composed netlist's own channel ports of the same name. Unit-tested (hand-derived GF(2) traces, op sequence assertions) plus a skip-if-absent cross-check against faultflow's own real polynomial table, PLUS a real cross-sim tier: both retargeting functions driven through real Icarus Verilog against a realinsert_compression/insert_compaction-produced, warptap-SIB- inserted netlist, sky130 gates and all. Caught two real gaps in the test fixtures themselves (not the retargeting code) along the way: the real sky130 behavioral models need`define FUNCTIONALto bypass their ownspecify/timing-check blocks under a crude bit-banged testbench (mirrorsfaultflow/verify/gate.py's own-DFUNCTIONAL), and a scan chain's FF needs genuine scan-enable-gated hold behavior (a plaindfxtpre-samples the ring generator's still-evolving output on the retargeting function's own capture edge and silently clobbers the just-loaded value) -- both fixed in the fixtures, zero changes needed tofaultflow_compression.py/faultflow_compaction.pythemselves. A later faultflow change (commit26e1f96, branchcompress) closed the curated-polynomial-table drift risk directly:manifest["compression"]["polynomial"]now serializes the campaign's real LFSR polynomial, sofaultflow_compression.py's newpolynomial_from_manifestreads it straight from the manifest instead of guessing via the curated table, which now survives only as a fallback for a manifest captured before that field existed; extendedtest_faultflow_compression_polynomial_table.pycovers the new path with a hand-built manifest, independent of whether a faultflow checkout is present. A second later faultflow change (commit9b670df, branchcompress) closed this module's other flagged limitation: an exported pattern now carriesload_care(the exact(chain_id, cycle)positions ATPG proved necessary for detection), sosolve_pattern_seed's new optionalload_careparameter solves against only those positions instead of every specifiedload_seqsposition, letting a genuine don't-care position's arbitrary fill value stop causing false "unsatisfiable" results; falls back to the original every-position behavior when a manifest has noload_care(older faultflow, random-fill patterns, non-compression campaigns). -
Stage 25:
insert_test_access()acceptsHierarchySpec. Itsspecsparameter was typedList[InstrumentSpec]since the Library-quality pass (0.0.1), predating this project's own nested-SIB/ScanMux work (Stage 4'sSibNode.nested, and the mux-arm generalization that followed) -- purely stale, not a deliberate restriction: the function's own body already forwardedspecsstraight tobuild_sib_plan()unchanged, which has accepted aHierarchySpecin the mix since that same nested-network work landed. Widened to a new publicTestAccessSpec = Union[InstrumentSpec, HierarchySpec]alias (re-exported fromwarptap/__init__.pyalongsideinsert_test_access), docstring corrected to stop calling the built network "flat", and a new test (tests/test_pipeline.py::test_insert_test_access_accepts_a_hierarchy_spec) proves the pipeline entry point specifically -- not justbuild_sib_plan/insert_sib_networkdirectly, which is all the existing nested-SIB test suite had ever exercised -- correctly threads a nestedHierarchySpecend to end (structure, flat sibling addressing, and a workingPDLInterpreter.iApply()against the nested leaf). No RTL or behavioral change: this closes a gap in what the convenience wrapper's own type/docs claimed it could do, not in what the underlying insertion machinery could already do. -
Stage 26: BSDL emitter for the TAP.
to_bsdl(entity_name, *, tck_max_freq_hz, idcode_value=IDCODE_VALUE)(bsdl_emit.py, exported withBsdlEmitError) writes a BSDL file for the TAP warptap inserts: the entity, the five TAP pins, theTAP_SCAN_*attributes,INSTRUCTION_LENGTH/OPCODE/CAPTURE,IDCODE_REGISTER,REGISTER_ACCESSand aDESIGN_WARNING. Every value comes fromtap_model/tap_ports; none is duplicated. It is TAP-only: it declares noBOUNDARY_LENGTHorBOUNDARY_REGISTER, so it is not a chip-level BSDL and a tool that requires a boundary register will reject it (theDESIGN_WARNINGsays so). EXTEST (0000) and SAMPLE/PRELOAD (0010) are declared as usual; the warning states that in a SIB design neither selects a boundary register: both put the IJTAG network's tail on TDO (rtl/tap_core.vroutes it for every instruction other than IDCODE and BYPASS), and only EXTEST moves the network (see the behavior changes above).tap_model.NETWORK_ACCESS_INSTRUCTION(EXTEST) names the instruction a tester loads to reach the network;to_iclandto_bsdlboth read it from there. The emitted text is read back and compared withtap_model, and every behavioral claim is checked on the realtap_core.vunder Icarus Verilog: capture pattern, IR length, IDCODE value and width, BYPASS depth, EXTEST/SAMPLE/PRELOAD routing, undeclared opcodes acting as BYPASS, and theDESIGN_WARNINGclaims (a TMS reset andtrst_nboth select IDCODE, TDO is low outside the shift states and changes only on the rising edge of TCK). These tests were mutation-checked against wrong BSDL claims and RTL bugs. No independent BSDL parser validates the output. Three were installed and run: UrJTAG 0.10 (Ubuntu apt) fails on every input, valid or not;bsdl-parsercan't import on Python 3.10+ (grako);cb_bsdl_parserraises on every input, valid or not (evidence intests/test_bsdl_emit.py).to_icl(..., include_access_link=False)output is now pinned byte-for-byte by golden files (.gitattributesmarks them-textsocore.autocrlfcannot alter them). -
A design's own IDCODE:
idcode_value.insert_test_access,insert_sib_networkandto_bsdltakeidcode_value(defaulttap_model.IDCODE_VALUE, the placeholder). The insertedtap_coregets the value baked into itsIDCODE_VALUEparameter, imported through Yosyschparamasscan_mux_cellalready is. The default keeps the previous import path, becausechparamchanges Yosys's output even at the default value: default output is byte-identical to before (the inserted Verilog forreal_signal.vhas the same sha256; the BSDL and ICL golden files still match).tap_model.idcode_value_errorenforces IEEE 1149.1's 32 bits with bit 0 set, andTapModel(TapModelError),TapConfig(TapIntegrityError),to_bsdl(BsdlEmitError) and both insertion functions (SibInsertError, before anything is ingested) reject anything else; Yosys would otherwise silently truncate a wider value.to_bsdlcalls only the default value a placeholder. Cross-simulated on real inserted RTL: a custom value shifted out after TRST equals the BSDL'sIDCODE_REGISTER, andbuild_integrity_program(tap=TapConfig(idcode_value=V))passes on that RTL while a program for the default fails atreset_instruction; the test fails if the value is ignored. -
TAP and IJTAG network-integrity patterns (
warptap.tap_integrity).build_integrity_program(graph, root)builds the TCK-level program a production flow plays to test the TAP and the network through TCK, with the TDO it expects fromTapModel+SibNetworkRegister: IDCODE after TRST, the IR's capture pattern and length, BYPASS, every unimplemented opcode (exhaustive_opcodes), an explicit IDCODE load, EXTEST with every SIB closed and then each SIB and ScanMux arm opened alone (over-shift probes, each from an all-closed network), each WRITE instrument written with P, ~P and 0 and read back (write_readback), and a TMS reset. READ instruments bound to design signals are don't-care wherever their captured value reaches TDO (two lockstep models, capturing all 0s and all 1s;live_valuespins them).check_integrity(program, observed)names the first failing test;IntegrityProgram.to_json()/from_json()carry the program as a self-contained file (warptap-tck-programv1). Cross-simulated against real inserted RTL (flat with a live instrument, nested, ScanMux), including programs built for the wrong network or TAP, which fail in the tests that see the difference; on the RTL from before the fixes below, the program failsnetwork_closedandtms_reset. - The integrity program tests the TAP's state machine and the network holding still.
build_integrity_programgains two test groups, both on by default:tap_paths(raw TCK sequences taking every one of the 32 TAP state transitions -- Pause-DR/IR, Exit2, Update -> Select-DR, Capture -> Exit1 without a shift, Select-IR -> Test-Logic-Reset -- with IDCODE, BYPASS and the IR each held in Pause at 0 and at 1 in every bit, and IDCODE and BYPASS captured over their complement) andnetwork_hold(SAMPLE/PRELOAD, whose TDO is the network's tail while the network holds still, then one EXTEST scan through the open network paused holding an alternating pattern and its complement, ending with every SIB closed).SibNetworkRegistergainstail()andcapture_unselected(): every instrument leaf captures on every Capture-DR, whatever the instruction, which cross-simulating SAMPLE/PRELOAD on a ScanMux network showed (its tail is a leaf's bit). Graded by FaultFlow's fault simulator (stuck-at, sky130 gate level) on autoMBIST's JTAG-wrapped designs, the program alone now detects 95.6% of the TAP's faults instead of 72.7% (dedicated; 95.7% instead of 72.5% on self-repair) and 94.6% of the IJTAG TDRs' instead of 86.5% (92.8% instead of 82.0%); an 8000-cycle random TCK walk on top of it finds 12 more faults on the dedicated design. What stays undetected is almost all out of TCK's reach: a register's behaviour under another instruction (every DR read starts with a capture), decodes of opcodes Update-IR never latches, TDO outside the shift states, and most of a SIB's capture logic.
Fixed¶
- The IJTAG network moved on every DR scan, not just under EXTEST.
insert_sib_networktied every top-level SIB'sselectto 1, andtap_core's capture/shift/update strobes fire for every instruction's DR scan. So the network shifted, opened and committed under IDCODE (the instruction after every reset) and BYPASS (a board chain passing through): two all-ones DR scans drove every WRITE instrument (test_mode,bist_start, ...) to 1. A$eqdecode (warptap_ijtag_extest_decode, likeinsert_bsr'sextest_mode) now selects the network only while EXTEST is loaded, matching the Python model, which already drove it only under EXTEST.insert_sib_networkgainsopcode_extest(default0b0000, which must matchtap_core). Consequence: in a SIB-only design, SAMPLE/PRELOAD still puts the network's tail on TDO but no longer moves the network (it has no data register there; the model already raised for it). The BSDLDESIGN_WARNING,NETWORK_ACCESS_INSTRUCTION's comment and thebsdl_emit/icl_emitdocstrings, which said SAMPLE/PRELOAD reaches the network, say so. - Test-Logic-Reset didn't reload the instruction. IEEE 1149.1 requires Test-Logic-Reset to
load IDCODE (BYPASS without one), so five TMS=1 cycles deselect a test-mode instruction
without a TRST pin;
tap_coreonly did it ontrst_n.tap_core.vandTapModelnow both reload it on every TCK edge taken in Test-Logic-Reset. The BSDLDESIGN_WARNINGno longer lists this deviation, andtest_bsdl_emit_cross_sim, which pinned it, checks the reload. tap_core's IDCODE/BYPASS shift registers had no reset. Invisible at TDO (Capture-DR always loads them first), but they started unknown and fed the TDO mux, so a gate-level X check couldn't cleartdo. Both now clear ontrst_n;TapModel.reset()clears its built-in BYPASS/IDCODE registers to match (BypassRegister/IdcodeRegistergainreset()).to_icl'sAccessLinknamed an instruction the TAP does not have. The block used the instruction namewdr_select, copied from the IJTAG benchmark set's example chip, whose instruction blocks are namedwir_select/wdr_select/clk_select; it is not part of ICL's syntax, and the docstrings wrongly said the block namedEXTEST. It now namesEXTEST, whichto_bsdldeclares, so the ICL and the BSDL agree (checked bytests/test_icl_bsdl_agreement.py).to_iclgainsbsdl_entity_name(defaultroot.name) soBSDLEntitycan match the BSDL file's entity, andinclude_access_link=Truewith an empty chain now raisesIclEmitErrorinstead of emitting anAccessLinkwith no instruction block, which the grammar rejects.include_access_link=Falseoutput is unchanged. The syntax was confirmed from the vendored grammar (icl.g4), the IJTAG benchmark set (E30.icl, from IEEE 1687 Annex E example E.30), MAST's ICL grammar and test files, and ASSET InterTech's IJTAG article; the normative IEEE 1687 text is paywalled and was not read. The vendored parser cannot parse any ordinarily formattedAccessLink, published ones included, becauseAccessLinkGeneric_defis a lexer rule inicl.g4, so the earlier claim that the emitted block was "grammatically valid" had never been checked by that tool.tests/test_icl_emit_access_link_syntax.pynow checks it through the tool's raw ANTLR parser with the header's whitespace rewritten around that bug, with positive and negative controls.IDCODE_VALUE's comment was wrong.tap_model.pysaid the placeholder was not a real registered JEDEC manufacturer ID; its manufacturer field decodes to JEP106 bank 1, code 0x01, which is an assigned code as far as we know (the JEP106 table was not checked). The value is unchanged, andtap_core.v's default must move with it if it is ever replaced.- Tests backed by the vendored
icl_parserskipped silently without it. A checkout without thethird_party/icl_parsersubmodule (everygit worktree, until initialized) skipped 77 grammar, ICL and PDL tests. They now fail with the fix-it command (git submodule update --init third_party/icl_parser);WARPTAP_ALLOW_MISSING_ICL_PARSER=1restores the skip. Thedevextra now installs the parser's packages (antlr4-python3-runtime==4.7.2,z3-solver,sympy,networkx), the README documents the setup, and the ICL/PDL import guide points at the fork the submodule pins (upstream lacks the compiled PDL parserimport_pdlneeds).
Explicitly out of scope¶
- A boundary-scan register in the BSDL: designs with one, from
bsr_insert.py, get noBOUNDARY_REGISTERdescription, andinsert_bsrdoes not takeidcode_value. - A dedicated instruction for the IJTAG network: it still shares opcode 0000 with EXTEST.
- The
AccessLink'sScanInterfacelist still names only the chain's first top-level slot, as a bare instance name. Reading the MAST project's retargeter source (not running it) indicates it needs exactly one entry, the chain's last slot as<instance>.client, to reach the whole chain; that has not been verified by running a retargeter, so the emitter is unchanged. - Width>1 instrument registers in the emitted ICL source scan-out from the bit scan-in enters
(
ScanRegister DR[w-1:0]withSource DR[w-1]), which the published ICL convention and the vendored parser read as disconnecting the register's other bits. The RTL is unaffected; the ICL description of it is suspect. Not yet verified with a retargeter, so unchanged. - The IEEE 1149.1 deviations in
rtl/tap_core.vtheDESIGN_WARNINGreports: TDO changes on the rising edge of TCK and is driven low, not tri-stated, outside the shift states.
[0.0.2] - 2026-09-16¶
Added¶
- Stage 15: Field/alias addressing. Real PDL/ICL named sub-field addressing (
Alias) insideiWrite/iRead, letting a caller target a bit-range within an instrument by name instead of the whole register;iRunLoop's-sckselector for pulsing a real system-clock port independent of TCK's own timing domain. - Nested/hierarchical SIB networks. A SIB gating a nested sub-network (not just a leaf instrument) at arbitrary depth: layout/retargeting, the Python cross-sim oracle, real RTL insertion, PDL retargeting, and ICL emit/import all generalized to recurse; validated end to end with a 3-level-deep integration scenario against real RTL and the vendored ICL parser.
- Multi-arm ScanMux. Real IEEE 1687 N-arm, multi-bit-select
ScanMuxsupport (not just the binary self-select SIB case): a new RTL primitive, per-instance-parameterized Yosys insertion, a genuine direct arm-to-arm switch in PDL retargeting, and ICL emit/import; validated end to end including a network mixing SIB-nests-mux and mux-nests-SIB in one real-RTL-validated network. - PDL grammar validation. The vendored
icl_parsersubmodule bundled a real PDL grammar (pdl_parser/pdl.g4, a "PDL0 grammar v20130806") that had never been compiled into a working parser: fixed 4 real bugs in it (an unterminated rule, an undefined rule reference, two rule names colliding with Python builtins, a missing token) and compiled it via ANTLR 4.7.2 (fix landed in the vendored fork, upstream PR opened). Immediately caught a real bug in Stage 11's own PDL emission:-sckwas rendered as a bare flag with no port name, which the real grammar requires: now renders-sck <port>. Proves PDL emission is valid syntax, not retargeting-semantics correctness (confirmed separately that the vendored parser's own retargeting engine doesn't support the register kind a WRITE instrument uses, so it can't serve as an oracle for that either). - Fixed: width>1 instrument/ScanMux emission used a non-standard scan-port convention.
render_instrument_module()(since Stage 10) andrender_scan_mux_module()(this session's own ScanMux work) both widened their ownScanInPort/ScanOutPort/ScanInSourceto match their underlying register's width. Confirmed real (checked everyScanRegister-sourcedScanOutPort/ScanInSourcein the vendoredicl_parser's own fixture corpus, 5-for-5 plus all 29ScanInSourceclauses, zero exceptions, including a 32-bitIDCODEregister fed by a 1-bit source): real ICL keeps these ports scalar always, letting the register's own internal shift chain move other bits into position. A real upstream maintainer's own PR review caught this, not this project's own live-validation (the vendored checker's ownport_size == source_sizeassertion is too permissive to catch it). Fixed to source exactly one bit each, confirmed against each construct's own real RTL wiring rather than assumed by convention (the instrument case sources the MSB, matchingsib_insert.py's own last-chained- cell; the ScanMux case sources bit 0, matchingscan_mux_cell.v's ownshift_ff[0]: two different real answers, not one rule).CaptureSource/WriteDataSource/DataOutPort(parallel, functional-level connections, not the bit-serial scan path) were already correct and are unaffected. This also resolved the retargeting-graph crash a since-questioned upstream PR had been working around: confirmed the crash never happens for genuinely standard-shaped ICL, only for the non-standard wide-port shape this fix removes. - PDL import.
to_pdl()'s own natural companion, the "other direction"icl_import.pyalready plays for ICL: but procedural, not declarative: replays real PDL text against a livePDLInterpreter's own publiciTarget/iWrite/iRead/iApply/iRunLoopmethods, in order, rather than reconstructing a new object.iTargetis hand-recognized (the compiled PDL0 grammar has no such construct at all, see "PDL grammar validation" above); every other supported statement is genuinely grammar-parsed, walking the real ANTLR parse tree. v1 scope is the 5 statement kindsto_pdl()actually emits: everything else the real grammar recognizes (iProc/iCall,iScan,iState, etc.) is rejected with a specific, namedPdlImportError, matchingicl_import.py's own discipline. Round-trips real, multi- statement histories (write, settle via both-tckand-sck, read, write again; alias- addressed fields) cleanly on the first pass. OneHotDataGroup(address-decoded register-file bus). Closes the "indirect/paged addressing" gap: but with real ICL's own genuinely different shape, not the originally- assumed JTAG-window-into-memory pattern (that pattern turns out to need no new ICL vocabulary at all).OneHotDataGroupis a parallel, non-scan, address-decoded register-file bus: a bounded set of individually-addressedDataRegisters sharing oneAddressPort/WriteEnPort/ReadEnPort/DataInPort/DataOutPortquintet. Scope is ICL emit/import only: never touches the scan chain, RTL insertion, or retargeting. No real fixture exists anywhere in the vendoredicl_parser's own test corpus for this construct, unlike every other ICL feature this project has built; it was closed with a permanent, gating live-validation spike before any production code, confirming (among other things) that exactly oneOneHotDataGroupper Module is a hard v1 constraint, and thatwritable/readableare a whole-group property in real ICL, not a per-register one (an early design draft got this wrong, caught the same way: a live-rendered example checked against the real vendoredIjtag, not just reasoned from the grammar). Live- validated at both this project's usual tiers (pure-Python shape assertions, and the real vendored checker withbuild_register_model=False, required because this construct is scan-free by construction and the checker's default crashes unconditionally for that shape), plus a full emit→import round trip.- Retargeting shift-length optimization.
sib_retarget.stage_open_sequencenever looked at what's already open: everyPDLInterpreter.iApplycall recomputed a full cold-start staged- opening sequence sized to the new target's own tree depth, even when a prefix of that path was already open from the previous call. Confirmed empirically: two sibling instruments 2 levels deep under a shared hierarchy, targeted back to back, used to cost 4+4 totalShiftDRrounds: now 4+2, since the shared ancestor prefix is reused. Adds a keyword-onlycurrently_openparameter, defaulting to reproduce every existing caller's exact output; wired into both real callers (PDLInterpreter.iApplyandsib_overshift.build_overshift_ops). As a direct, proven side effect, also fixes a previously-documented v1 footgun: retargeting to the exact same still-open WRITE instrument twice in a row (no intervening different target) used to clobber the value via a redundant round's own zero-fill commit: confirmed as a real bug on real RTL before this fix, now a permanent passing regression test. An earlier memory entry had mischaracterized this gap as needing "Keim's ring-by-ring hierarchical search": that technique (real primary source: Dr. Martin Keim, Nordic Test Forum 2017) is for fault localization in an unknown-structure network, already correctly ruled out as inapplicable by this project's own earliersib_overshift.pyresearch; not chased here. - Fixed: a
WRITE-direction instrument gated by aScanMuxNodearm didn't correctly round-trip its own value on readback. Found as a side effect of strengthening a previously- weak test assertion (it only ever checked shift-op count, never the actual value, despite a comment falsely claiming otherwise), and initially mischaracterized (in this file and the project's own memory) as asib_model.pybug: direct re-investigation found its own shift/ capture/update simulation matches real RTL exactly, including a_read-time redirect a blanket-removal patch confirmed is load-bearing (broke 4 real, previously-passing tests, 2 of them real RTL). The real bug:PDLInterpreter._target_layout/iApply's pending-read block computed the expected comparison value by reusingcompose_bits' position-order-then- reversed layout, which assumes a uniform linear cascade through the wholewidth + select_widthblock: wrong for a matched mux arm. Confirmed by readingrtl/scan_mux_cell.vdirectly and a dedicated RTL spike (tests/test_scan_mux_write_arm_readback_cross_sim.py, using a real multi-bit host port so MSB-first vs LSB-first is actually distinguishable, unlike every existing 1-bit fixture): while an arm stays matched,sois a combinational passthrough of the matched arm's ownsofor the round's entire duration (matched_old_ccan't change mid-round), so its content surfaces on the round's own firstwidthchronological cycles, MSB-first: not wherever the reused linear layout would place it. Fixed with a dedicated_mux_arm_read_chronological_bits, confirmed on real RTL in the two tests that had deliberately documented-but-skipped this exact check (test_sib_insert_scan_mux_cross_sim.py). The plain-SibNodecase was never affected. - Fixed: a read-only
iApplyof an already-openWRITEinstrument silently zeroed its own stored value for any later read. Broader than the "same instrument twice with no intervening target" footgun the retargeting shift-length optimization plan already fixed: phase 2 always deliverspayload_value(defaulting to0absent a freshiWrite), andstays_matched/stays_opennaturally holds for a round that doesn't change what's selected, so that0committed via Update-DR regardless of mux involvement or intervening targets. Not mux-specific: the plain-SibNodecase was affected too. A second, closely related bug with the same root cause, found while fixing the first:iWrite's own sub-field (field=) merge had an identical gap: writing one field in a fresh apply cycle (no other field of the same instrument also queued that batch) silently zeroed every other field instead of preserving its own real last-committed value. Fixed with one new piece of state,PDLInterpreter._committed_writes, tracking eachWRITE-direction instrument's own last-known committed value acrossiApplycalls: bothiApply's own phase-2 payload selection andiWrite's own sub-field merge fall back to it instead of a bare0. Confirmed as real bugs on real RTL before the fix (both plain-SibNodeandScanMuxNode-arm cases), now permanent passing regression tests. - Second real-external-design validation:
alexforencich/verilog-uart'suart_tx.v. Closes the "only ever proven against one real external design" gap (openMBIST'smem_subsystem_mbist) with a second, unrelated, MIT-licensed real design (583-star, actively maintained), read live from a sibling checkout exactly like openMBIST already is (verilog_uart_dirfixture, mirroringopenmbist_dir's own convention). Unlike everymem_subsystem_mbisttest,uart_tx.vneeds its own functional clock pulsed a real, precise number of times (81 cycles for a full byte atprescale=1) with no useful relationship to how many TCK cycles JTAG shifting happens to take: the first real use ofPDLInterpreter.iRunLoop'ssck_portparameter (Stage 15's own-sckselector) end to end, rather than the lower-levelretarget_faultflow_patternsAPI Stage 14's own cross-sim tests used to prove the same underlyingPulsePinprimitive. OneWRITEinstrument drivess_axis_tdata+s_axis_tvalidtogether (9 bits, one atomiciWrite/iApply); a second setsprescale; aREADinstrument confirmsbusytoggles 0→1→0 across a transmission: and, the strong end-to-end proof, a new testbench (tb_uart_tx.v) tracestxddirectly every functional-clock pulse, never through JTAG, letting the test decode the actual transmitted byte in Python and confirm it matches what was written via JTAG. Two real findings surfaced independently verifying the RTL before writing any warptap-side code (a standalone spike, not trusted from a web summary): the stop-bit bit-time is 9 cycles, one longer than every other bit (bit_cnt==1's own branch setsprescale_reg <= (prescale<<3)with no-1), andbusynever visibly dips between back-to-back transmissions unlesss_axis_tvalidis explicitly deasserted before the frame completes (treadypulses for exactly one cycle at frame-end with no separate same-cycle check gating a fresh start). Also confirmed, the hard way (a same-sessionValueErroron realx-valued trace output):uart_tx.v's ownreg = <literal>initial-value declarations do not survive this project's Yosys JSON netlist round-trip, and its reset is synchronous-only (unlike every prior fixture's DUT reset): so the new testbench's own reset lead-in needed a real pulse row, not just a held level, to commit a known state before anything is sampled. - Fixed:
iWrite/iRead's ownfield=(named sub-field /Alias) resolution crashed or silently misresolved for any instrument not gated by a plain top-levelSibNode._resolve_field(pdl_interpreter.py) resolvedinstrument_namevia_instrument_for, a shallow, top-level-only walk ofgraph.chainthat unconditionally accessednode.instrumenton every entry: a realScanMuxNodehas no such attribute at all, so afield=lookup against any graph containing one anywhere raised a bare, unhelpfulAttributeError: 'ScanMuxNode' object has no attribute 'instrument', regardless of whether the mux itself gated the target. For a nested (HierarchySpec/SibNode.nested) instrument, the same top-level-only walk instead silently returnedNone, misreporting a real, existing instrument asPDLError("no instrument named ... in this network"). A real, previously documented gap:_instrument_for's own docstring (added alongside the write-instrument- payload-defaulting fix) explicitly named both failure modes and deliberately deferred fixing them, pre-identifying the fix as routing through the module's own already-correct, properly recursive_find_instrumentinstead (added that same session, previously used only byiApply) rather than conflating two unrelated fixes. Confirmed no existing test combinedfield=with either a mux-gated or nested instrument. Fixed exactly as pre-identified:_instrument_fordeleted outright (zero other callers),_resolve_fieldnow calls_find_instrument. New pure-Python regression tests confirmfield=now resolves correctly through both aScanMuxNodearm (tests/test_pdl_interpreter_scan_mux.py) and a nestedHierarchySpecinstrument (tests/test_pdl_interpreter.py).
Explicitly out of scope¶
Dynamic existPr-conditional PDL reachability, any conformance claim against a specific IEEE
standard edition, and PDL import for anything beyond to_pdl()'s own output shape (arbitrary
hand-authored .pdl files: multiple statements per line, statements spanning lines, real
iProc definitions; see pdl_import.py's own module docstring for the exact boundary). No
independent oracle exists for PDL retargeting semantics (as opposed to grammar): confirmed
by a thorough external search (implementation_plan.md's own Stage 23), not just the one
vendored tool: every real, accessible, maintained open IJTAG tool found deliberately excludes
retargeting semantics from its own scope.
[0.0.1] - 2026-09-07¶
Added¶
- Stage 1: Ingest/surgery/re-synth pipeline skeleton. Yosys JSON ingest, a
Netlistmodel, and re-emission back to Verilog, forming the surgery pipeline every later stage builds on. - Stage 2: TAP FSM. A table-driven, shared 16-state IEEE 1149.1 transition function, used identically by the RTL codegen and the Python behavioral model.
- Stage 3: BSR insertion. Boundary-scan register insertion over pre-synthesis netlists, covering plain, full, and bidirectional (BC_7) cell types.
- Stage 4: ICL model + SIB primitive. The physical shift-topology graph and module-instantiation tree data model, and the SIB (segment insertion bit) primitive.
- Stage 5: PDL interpreter + static retargeting.
iWrite/iRead/iApplyover a flat/ static SIB network, with correct phase-1/phase-2 bit composition. - Stage 6: ICL connectivity check. The "over-shifting" structural-consistency technique, confirming an inserted network matches its own declared topology.
- Stage 7: TAP-transaction IR + SVF/STAPL emitters. A shared, format-independent op
vocabulary (
tap_ir.py), with SVF and STAPL text emitters live-validated against real OpenOCD and a real Jam STAPL Player build. - Stage 9: Real functional instruments + PDL functional verification. A genuine
(non-stub) write-target TDR with a real update latch, and
pdl_verify.py's real-RTLiRead-expectation checking: proven against a real external design (openMBIST'smem_subsystem_mbist). - Stage 10: ICL emission. Real ICL network-topology text emission, live-validated against
a vendored, independent ICL parser (
Honza255/icl_parser). - Stage 11: PDL emission. Real PDL statement text emission, backed by a new additive
PDLInterpreter.historyrecord; validated self-consistently after confirming no independent PDL parser exists anywhere to validate against. - Stage 12: ICL import. Parses real
.iclfiles entirely through the same vendored ICL parser (never a self-written parser), recognizing warptap's own canonical SIB-network shape; round-trips Stage 10's own emission and correctly rejects every fixture in that parser's own real test corpus that isn't warptap-shaped. - Stage 13: STIL emission. This project's first new output format since SVF/STAPL. A new
PulsePinop models a functional/system-clock pulse independent of TCK's own timing domain (SVF/STAPL cannot express this at all); live-validated against a real, independent, pip-installable STIL parser (Semi-ATE-STIL). - Stage 14: faultflow pattern retargeting. Consumes a real
faultflow--export-patternsJSON export and retargets it through warptap's own SIB/TAP network (never faultflow's own SoC chain-offset table), validated at the gold-standard tier: a real Icarus cross-sim provingPulsePinpulses a genuinely independent clock domain against real simulated hardware. - Library-quality pass. A shared
WarptapErrorexception base (every specific error now subclasses it, fully backward compatible), a PEP 561py.typedmarker, a curated top-levelwarptappublic API (warptap/__init__.py), andinsert_test_access(): a single convenience function for the ingest → build-network → insert → re-emit sequence every cross-sim test previously hand-rolled.
Explicitly out of scope¶
Nested/hierarchical SIB trees, dynamic existPr-conditional PDL reachability, PDL import (no
real parser exists anywhere to import via), any conformance claim against a specific IEEE
standard edition.