Coverage for src / tracekit / analyzers / protocols / jtag.py: 85%
105 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 23:04 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 23:04 +0000
1"""JTAG protocol decoder.
3This module provides IEEE 1149.1 JTAG/Boundary-Scan protocol decoding
4with TAP state machine tracking and IR/DR data extraction.
7Example:
8 >>> from tracekit.analyzers.protocols.jtag import JTAGDecoder
9 >>> decoder = JTAGDecoder()
10 >>> for packet in decoder.decode(tck=tck, tms=tms, tdi=tdi, tdo=tdo):
11 ... print(f"State: {packet.annotations['tap_state']}")
13References:
14 IEEE 1149.1-2013 Standard Test Access Port and Boundary-Scan Architecture
15"""
17from __future__ import annotations
19from enum import Enum
20from typing import TYPE_CHECKING
22import numpy as np
24from tracekit.analyzers.protocols.base import (
25 AnnotationLevel,
26 ChannelDef,
27 SyncDecoder,
28)
29from tracekit.core.types import DigitalTrace, ProtocolPacket
31if TYPE_CHECKING:
32 from collections.abc import Iterator
34 from numpy.typing import NDArray
37class TAPState(Enum):
38 """JTAG TAP Controller states."""
40 TEST_LOGIC_RESET = "Test-Logic-Reset"
41 RUN_TEST_IDLE = "Run-Test/Idle"
42 SELECT_DR_SCAN = "Select-DR-Scan"
43 CAPTURE_DR = "Capture-DR"
44 SHIFT_DR = "Shift-DR"
45 EXIT1_DR = "Exit1-DR"
46 PAUSE_DR = "Pause-DR"
47 EXIT2_DR = "Exit2-DR"
48 UPDATE_DR = "Update-DR"
49 SELECT_IR_SCAN = "Select-IR-Scan"
50 CAPTURE_IR = "Capture-IR"
51 SHIFT_IR = "Shift-IR"
52 EXIT1_IR = "Exit1-IR"
53 PAUSE_IR = "Pause-IR"
54 EXIT2_IR = "Exit2-IR"
55 UPDATE_IR = "Update-IR"
58# Standard JTAG instructions
59JTAG_INSTRUCTIONS = {
60 0x00: "EXTEST",
61 0x01: "SAMPLE/PRELOAD",
62 0x02: "IDCODE",
63 0x03: "BYPASS",
64 0x04: "INTEST",
65 0x05: "RUNBIST",
66 0x06: "CLAMP",
67 0x07: "HIGHZ",
68}
71class JTAGDecoder(SyncDecoder):
72 """JTAG protocol decoder.
74 Decodes JTAG bus transactions including TAP state machine transitions,
75 IR/DR shift operations, and standard instruction identification.
77 Attributes:
78 id: "jtag"
79 name: "JTAG"
80 channels: [tck, tms, tdi] (required), [tdo] (optional)
82 Example:
83 >>> decoder = JTAGDecoder()
84 >>> for packet in decoder.decode(tck=tck, tms=tms, tdi=tdi, sample_rate=1e6):
85 ... print(f"IR: {packet.annotations.get('ir_value', 'N/A')}")
86 """
88 id = "jtag"
89 name = "JTAG"
90 longname = "Joint Test Action Group (IEEE 1149.1)"
91 desc = "JTAG/Boundary-Scan protocol decoder"
93 channels = [ # noqa: RUF012
94 ChannelDef("tck", "TCK", "Test Clock", required=True),
95 ChannelDef("tms", "TMS", "Test Mode Select", required=True),
96 ChannelDef("tdi", "TDI", "Test Data In", required=True),
97 ]
99 optional_channels = [ # noqa: RUF012
100 ChannelDef("tdo", "TDO", "Test Data Out", required=False),
101 ]
103 options = [] # noqa: RUF012
105 annotations = [ # noqa: RUF012
106 ("state", "TAP state"),
107 ("ir", "Instruction register"),
108 ("dr", "Data register"),
109 ("instruction", "Decoded instruction"),
110 ]
112 def __init__(self) -> None:
113 """Initialize JTAG decoder."""
114 super().__init__()
115 self._tap_state = TAPState.TEST_LOGIC_RESET
116 self._shift_bits_tdi: list[int] = []
117 self._shift_bits_tdo: list[int] = []
119 def decode( # type: ignore[override]
120 self,
121 trace: DigitalTrace | None = None,
122 *,
123 tck: NDArray[np.bool_] | None = None,
124 tms: NDArray[np.bool_] | None = None,
125 tdi: NDArray[np.bool_] | None = None,
126 tdo: NDArray[np.bool_] | None = None,
127 sample_rate: float = 1.0,
128 ) -> Iterator[ProtocolPacket]:
129 """Decode JTAG transactions.
131 Args:
132 trace: Optional primary trace.
133 tck: Test Clock signal.
134 tms: Test Mode Select signal.
135 tdi: Test Data In signal.
136 tdo: Test Data Out signal (optional).
137 sample_rate: Sample rate in Hz.
139 Yields:
140 Decoded JTAG operations as ProtocolPacket objects.
142 Example:
143 >>> decoder = JTAGDecoder()
144 >>> for pkt in decoder.decode(tck=tck, tms=tms, tdi=tdi, sample_rate=10e6):
145 ... print(f"State: {pkt.annotations['tap_state']}")
146 """
147 if tck is None or tms is None or tdi is None:
148 return
150 n_samples = min(len(tck), len(tms), len(tdi))
151 if tdo is not None:
152 n_samples = min(n_samples, len(tdo))
154 tck = tck[:n_samples]
155 tms = tms[:n_samples]
156 tdi = tdi[:n_samples]
157 if tdo is not None:
158 tdo = tdo[:n_samples]
160 # Find rising edges of TCK (state updates on rising edge)
161 rising_edges = np.where(~tck[:-1] & tck[1:])[0] + 1
163 if len(rising_edges) == 0:
164 return
166 state_start_idx = 0
167 trans_num = 0
169 for edge_idx in rising_edges:
170 # Sample TMS at rising edge
171 tms_val = bool(tms[edge_idx])
173 # Update TAP state
174 new_state = self._next_state(self._tap_state, tms_val)
176 # Handle state-specific actions
177 if self._tap_state in (TAPState.SHIFT_IR, TAPState.SHIFT_DR):
178 # Shift data
179 tdi_bit = 1 if tdi[edge_idx] else 0
180 self._shift_bits_tdi.append(tdi_bit)
182 if tdo is not None: 182 ↛ 183line 182 didn't jump to line 183 because the condition on line 182 was never true
183 tdo_bit = 1 if tdo[edge_idx] else 0
184 self._shift_bits_tdo.append(tdo_bit)
186 # Detect state transitions
187 if new_state != self._tap_state:
188 # Emit packet on state change if we have shifted data
189 if self._tap_state == TAPState.SHIFT_IR and len(self._shift_bits_tdi) > 0:
190 # Emit IR shift
191 ir_value = self._bits_to_value(self._shift_bits_tdi)
192 start_time = state_start_idx / sample_rate
193 end_time = edge_idx / sample_rate
195 instruction_name = JTAG_INSTRUCTIONS.get(ir_value, "UNKNOWN")
197 self.put_annotation(
198 start_time,
199 end_time,
200 AnnotationLevel.FIELDS,
201 f"IR: 0x{ir_value:02X} ({instruction_name})",
202 )
204 annotations = {
205 "transaction_num": trans_num,
206 "tap_state": self._tap_state.value,
207 "ir_value": ir_value,
208 "ir_bits": len(self._shift_bits_tdi),
209 "instruction": instruction_name,
210 }
212 packet = ProtocolPacket(
213 timestamp=start_time,
214 protocol="jtag",
215 data=bytes([ir_value]),
216 annotations=annotations,
217 errors=[],
218 )
220 yield packet
221 trans_num += 1
223 elif self._tap_state == TAPState.SHIFT_DR and len(self._shift_bits_tdi) > 0: 223 ↛ 225line 223 didn't jump to line 225 because the condition on line 223 was never true
224 # Emit DR shift
225 dr_value_tdi = self._bits_to_value(self._shift_bits_tdi)
226 start_time = state_start_idx / sample_rate
227 end_time = edge_idx / sample_rate
229 # Convert to bytes
230 byte_count = (len(self._shift_bits_tdi) + 7) // 8
231 dr_bytes = dr_value_tdi.to_bytes(byte_count, "little")
233 self.put_annotation(
234 start_time,
235 end_time,
236 AnnotationLevel.FIELDS,
237 f"DR: 0x{dr_value_tdi:X} ({len(self._shift_bits_tdi)} bits)",
238 )
240 annotations = {
241 "transaction_num": trans_num,
242 "tap_state": self._tap_state.value,
243 "dr_value_tdi": dr_value_tdi,
244 "dr_bits": len(self._shift_bits_tdi),
245 }
247 if tdo is not None and len(self._shift_bits_tdo) > 0:
248 dr_value_tdo = self._bits_to_value(self._shift_bits_tdo)
249 annotations["dr_value_tdo"] = dr_value_tdo
251 packet = ProtocolPacket(
252 timestamp=start_time,
253 protocol="jtag",
254 data=dr_bytes,
255 annotations=annotations,
256 errors=[],
257 )
259 yield packet
260 trans_num += 1
262 # Reset shift buffers on state change
263 if new_state not in [TAPState.SHIFT_IR, TAPState.SHIFT_DR]:
264 self._shift_bits_tdi = []
265 self._shift_bits_tdo = []
267 self._tap_state = new_state
268 state_start_idx = edge_idx
270 def _next_state(self, current: TAPState, tms: bool) -> TAPState:
271 """Compute next TAP state based on TMS value.
273 Args:
274 current: Current TAP state.
275 tms: TMS signal value.
277 Returns:
278 Next TAP state.
279 """
280 # TAP state machine (IEEE 1149.1 Figure 6-1)
281 transitions = {
282 TAPState.TEST_LOGIC_RESET: {
283 False: TAPState.RUN_TEST_IDLE,
284 True: TAPState.TEST_LOGIC_RESET,
285 },
286 TAPState.RUN_TEST_IDLE: {
287 False: TAPState.RUN_TEST_IDLE,
288 True: TAPState.SELECT_DR_SCAN,
289 },
290 TAPState.SELECT_DR_SCAN: {
291 False: TAPState.CAPTURE_DR,
292 True: TAPState.SELECT_IR_SCAN,
293 },
294 TAPState.CAPTURE_DR: {
295 False: TAPState.SHIFT_DR,
296 True: TAPState.EXIT1_DR,
297 },
298 TAPState.SHIFT_DR: {
299 False: TAPState.SHIFT_DR,
300 True: TAPState.EXIT1_DR,
301 },
302 TAPState.EXIT1_DR: {
303 False: TAPState.PAUSE_DR,
304 True: TAPState.UPDATE_DR,
305 },
306 TAPState.PAUSE_DR: {
307 False: TAPState.PAUSE_DR,
308 True: TAPState.EXIT2_DR,
309 },
310 TAPState.EXIT2_DR: {
311 False: TAPState.SHIFT_DR,
312 True: TAPState.UPDATE_DR,
313 },
314 TAPState.UPDATE_DR: {
315 False: TAPState.RUN_TEST_IDLE,
316 True: TAPState.SELECT_DR_SCAN,
317 },
318 TAPState.SELECT_IR_SCAN: {
319 False: TAPState.CAPTURE_IR,
320 True: TAPState.TEST_LOGIC_RESET,
321 },
322 TAPState.CAPTURE_IR: {
323 False: TAPState.SHIFT_IR,
324 True: TAPState.EXIT1_IR,
325 },
326 TAPState.SHIFT_IR: {
327 False: TAPState.SHIFT_IR,
328 True: TAPState.EXIT1_IR,
329 },
330 TAPState.EXIT1_IR: {
331 False: TAPState.PAUSE_IR,
332 True: TAPState.UPDATE_IR,
333 },
334 TAPState.PAUSE_IR: {
335 False: TAPState.PAUSE_IR,
336 True: TAPState.EXIT2_IR,
337 },
338 TAPState.EXIT2_IR: {
339 False: TAPState.SHIFT_IR,
340 True: TAPState.UPDATE_IR,
341 },
342 TAPState.UPDATE_IR: {
343 False: TAPState.RUN_TEST_IDLE,
344 True: TAPState.SELECT_DR_SCAN,
345 },
346 }
348 return transitions[current][tms]
350 def _bits_to_value(self, bits: list[int]) -> int:
351 """Convert bit list to integer (LSB first).
353 Args:
354 bits: List of bit values (0 or 1).
356 Returns:
357 Integer value.
358 """
359 value = 0
360 for i, bit in enumerate(bits):
361 value |= bit << i
362 return value
365def decode_jtag(
366 tck: NDArray[np.bool_],
367 tms: NDArray[np.bool_],
368 tdi: NDArray[np.bool_],
369 tdo: NDArray[np.bool_] | None = None,
370 sample_rate: float = 1.0,
371) -> list[ProtocolPacket]:
372 """Convenience function to decode JTAG transactions.
374 Args:
375 tck: Test Clock signal.
376 tms: Test Mode Select signal.
377 tdi: Test Data In signal.
378 tdo: Test Data Out signal (optional).
379 sample_rate: Sample rate in Hz.
381 Returns:
382 List of decoded JTAG transactions.
384 Example:
385 >>> packets = decode_jtag(tck, tms, tdi, tdo, sample_rate=10e6)
386 >>> for pkt in packets:
387 ... print(f"IR: {pkt.annotations.get('ir_value', 'N/A')}")
388 """
389 decoder = JTAGDecoder()
390 return list(decoder.decode(tck=tck, tms=tms, tdi=tdi, tdo=tdo, sample_rate=sample_rate))
393__all__ = ["JTAG_INSTRUCTIONS", "JTAGDecoder", "TAPState", "decode_jtag"]