Skip to content

Faultflow

Retargeting a real faultflow --export-patterns export through an inserted network; see Faultflow pattern retargeting for a full example.

warptap.faultflow_retarget

faultflow pattern retargeting (implementation_plan.md ยง7 Stage 14, built on Stage 13's PulsePin/STIL emission). Consumes faultflow's real --export-patterns JSON (a flat array of {"load_seqs": {chain_id: [bool,...]}, "capture_pi_values": {port: bool}, "expected_unload": {chain_id: [bool,...]}} records, faultflow/scan/pattern_export.py's own scan_pattern_to_dict) and remaps each pattern through warptap's own SIB/TAP network model, driving a real :class:~warptap.pdl_interpreter.PDLInterpreter -- never faultflow's own SoC chain-offset table, and never faultflow's own retargeting algorithm (this project's established "port understanding, not code" boundary with that project).

A faultflow chain id carries no inherent name (a bare int assigned during block-level scan stitching) -- chain_to_instrument is a REQUIRED caller input, since no shared naming convention exists between the two tools. The separate, structurally different faultflow_soc_access_v1 manifest (a flat, always-present SoC chain addressed by offset, not a TAP-instruction-selected network) is not consumed here.

Bit order -- independently re-derived and verified twice this session (once by hand with a non-palindromic worked example, once by an independent review given the same primitives): both tools define physical "position 0" as nearest-SI identically, but faultflow's own exported sequence is index-reversed relative to position (faultflow/retarget/transform.py's own s[k] == V[L-1-k]), while warptap's own payload_value bit k IS position k directly (:mod:warptap.sib_layout's compose_bits). This means payload_bit[k] == sequence[width-1-k] -- the exported bit list must be REVERSED before packing into an int via :func:warptap.tap_ir.bits_to_int. Not a new convention: tap_ir.py's own module docstring already states the identical rule for composing sib_layout's position-ordered bit lists into a chronological int.

Padding, confirmed directly against the real source (faultflow/scan/protocol.py's serialize_vector): when a pattern's chains differ in length, load_seqs is padded at the FRONT and expected_unload at the BACK to a shared max_chain_length -- neither the padding amount nor max_chain_length itself is present in the exported JSON, so padding is stripped here using each chain's REAL width (from chain_to_instrument -> graph -> InstrumentNode.width): the last width elements of load_seqs, the first width elements of expected_unload.

One PulsePin per pattern, not per chain -- a real correctness requirement, not a simplification: faultflow's ScanPattern represents ONE fault-detection attempt where every referenced chain is loaded, a SINGLE functional capture edge is applied (with capture_pi_values held throughout), and every chain is then unloaded/compared against that SAME capture. Interleaving load/pulse/unload per chain would compare a chain's response before other chains in the same pattern were even loaded, or before any capture happened at all -- this module loads every chain first, pulses once, then unloads every chain.

clock_port/clock_pulse_count are REQUIRED caller inputs: faultflow's own schema carries no clock/timing field at all (confirmed directly against transform.py/emit.py -- zero references to any clock concept in the real SoC-pattern-emission code). Making the caller supply a real period for that pin is standard STIL authoring, not a burden unique to this module -- see :mod:warptap.tap_ir_stil's own pulse_periods requirement.

FaultflowRetargetError

Bases: WarptapError

Raised when a pattern references a faultflow chain id absent from chain_to_instrument, or that mapping names an instrument not present in graph.

retarget_faultflow_patterns

retarget_faultflow_patterns(patterns: List[Dict[str, Any]], chain_to_instrument: Dict[int, str], graph: PhysicalGraph, root: ModuleInstance, *, clock_port: str, clock_pulse_count: int) -> List[_IrOp]

Retarget every pattern in patterns (the parsed --export-patterns JSON array) through a real :class:~warptap.pdl_interpreter.PDLInterpreter built against graph/root, returning the accumulated :mod:warptap.tap_ir ops -- feed straight into :func:warptap.tap_ir_stil.to_stil (the only backend that can render the :class:~warptap.tap_ir.PulsePin ops this function inserts).

Raises :class:FaultflowRetargetError naming the offending chain id if chain_to_instrument doesn't cover every chain a pattern references, or names an instrument absent from graph.

Continuous-flow decompressor

A faultflow design whose [compression] entry says architecture = "continuous" loads its chains from a ring generator that takes num_channels fresh channel bits on every shift, not one seed. retarget_decompressed_faultflow_patterns picks the retarget from the manifest's architecture (an entry without one is the older one-shot decompressor); solve_pattern_channels solves a pattern's load over every free bit (num_channels x warmup + max_chain_length) strictly, and the retarget writes one channel word per shift to the channel instrument, with one clock pulse each.

warptap.faultflow_continuous

faultflow's continuous-flow scan decompressor: its model and pattern retargeting (W1, next to faultflow_compression.py's one-shot port).

The decompressor (faultflow/scan/ring_generator.py and compression.py in faultflow; ported here as understanding, not as an import): a Galois ring generator of poly.width stages, cleared on the scan enable's rise (the first shift of a load, after a clock with scan enable low) and fed num_channels channel bits on every shift -- channel c is XORed into ring stage c * width // num_channels -- and a phase shifter driving each chain's scan-in from the ring. Shift t's scan-ins read the ring as shift t - 1 left it (shift 0's: the cleared ring, 0). A load is warmup + max_chain_length shifts, so a pattern has num_channels x shifts free bits instead of one seed's: the first warmup shifts only fill the ring (their scan-ins fall off the chains' far ends), and a pattern's load_seqs[chain][t] is the scan-in of shift warmup + t.

A free bit is named by its shift and channel: bit t * num_channels + c is channel c of the word applied at shift t. :func:continuous_scan_in_rows gives every scan-in as a GF(2) mask over them; :func:solve_pattern_channels solves a pattern's load over all of them at once, strictly -- a don't-care is a load_care the pattern names, nothing else -- and returns each channel's bit for each shift. :func:retarget_continuous_faultflow_patterns drives them through the JTAG network: one clock pulse with scan enable low, then for each shift a write of that shift's channel word and one pulse with scan enable high, then the capture and the unload as in the one-shot flow.

manifest["compression"] describes the decompressor: architecture ("continuous"; absent or "one_shot" is the older decompressor, which :func:retarget_decompressed_faultflow_patterns hands to faultflow_compression), num_channels, polynomial ({"width", "taps"}), phase_shifter_taps, warmup and, as a check, injectors.

Not retargeted here: a transition (launch) pattern, as in the one-shot flow.

ContinuousDecompressor dataclass

What a pattern's load is made of: the ring (poly), the phase shifter (one tap list of ring stages per chain), the channel count and the warm-up shifts.

shifts

shifts(max_chain_length: int) -> int

How many shifts a load takes: the warm-up and the longest chain's.

continuous_from_manifest

continuous_from_manifest(compression: dict) -> ContinuousDecompressor

The decompressor manifest["compression"] describes (see this module's docstring); a missing field, or injectors that are not the ones this model computes, is refused.

continuous_scan_in_rows

continuous_scan_in_rows(poly: LfsrPolynomial, phase_shifter_taps: List[List[int]], num_channels: int, cycles: int) -> List[List[int]]

rows[t][chain]: chain's scan-in at shift t, for t in range(cycles), as a GF(2) bitmask over the channel bits (bit t * num_channels + c: channel c of the word applied at shift t) -- ported from faultflow/scan/ring_generator.py::continuous_scan_in_rows.

injector_stages

injector_stages(width: int, num_channels: int) -> List[int]

The ring stage each channel bit XORs into on every shift: channel c into stage c * width // num_channels, spread evenly -- ported from faultflow/scan/ring_generator.py::injector_stages.

solve_pattern_channels

solve_pattern_channels(load_seqs: dict, decompressor: ContinuousDecompressor, max_chain_length: int, pattern_index: int, load_care: 'Optional[set[tuple[int, int]]]' = None, launch_scan_in: 'Optional[dict[int, bool]]' = None) -> dict

One joint GF(2) system over every free bit of the load (num_channels x shifts), solved once: {channel: [bit for each shift]} driving the chains to load_seqs. load_seqs[chain][t] is shift warmup + t's scan-in; load_care, when not None, keeps only the (chain, t) positions it names; launch_scan_in ({chain: bit}, a launch-on-shift pattern) constrains the scan-in of the shift after the load. Unconstrained bits are 0.

retarget_continuous_faultflow_patterns

retarget_continuous_faultflow_patterns(patterns: List[dict], chain_to_instrument: dict, compression_channel_instrument: str, graph: PhysicalGraph, root: ModuleInstance, *, decompressor: ContinuousDecompressor, max_chain_length: int, clock_port: str, scan_enable_port: str, clock_pulse_count: int = 1) -> list

Retarget every pattern through a continuous-decompressor composed netlist's JTAG network: solve the pattern's channel words (:func:solve_pattern_channels), pulse the clock once with scan enable low (so the next edge's scan-enable rise clears the ring), then for each shift write that shift's channel word to compression_channel_instrument (a num_channels-bit WRITE instrument on the composed netlist's channel port) and pulse the clock once with scan enable high. The unload side is :mod:warptap.faultflow_compression's: capture pulse (scan enable low, the pattern's capture_pi_values), then a read of each raw scan_out_N chain.

retarget_decompressed_faultflow_patterns

retarget_decompressed_faultflow_patterns(patterns: List[dict], chain_to_instrument: dict, compression_channel_instrument: str, graph: PhysicalGraph, root: ModuleInstance, *, compression: dict, max_chain_length: int, clock_port: str, scan_enable_port: str, clock_pulse_count: int = 1) -> list

Retarget through whichever decompressor manifest["compression"] says is on the chip: architecture == "continuous" the continuous one, anything else (absent: the older one) :func:~warptap.faultflow_compression.retarget_compressed_faultflow_patterns.