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

1"""JTAG protocol decoder. 

2 

3This module provides IEEE 1149.1 JTAG/Boundary-Scan protocol decoding 

4with TAP state machine tracking and IR/DR data extraction. 

5 

6 

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']}") 

12 

13References: 

14 IEEE 1149.1-2013 Standard Test Access Port and Boundary-Scan Architecture 

15""" 

16 

17from __future__ import annotations 

18 

19from enum import Enum 

20from typing import TYPE_CHECKING 

21 

22import numpy as np 

23 

24from tracekit.analyzers.protocols.base import ( 

25 AnnotationLevel, 

26 ChannelDef, 

27 SyncDecoder, 

28) 

29from tracekit.core.types import DigitalTrace, ProtocolPacket 

30 

31if TYPE_CHECKING: 

32 from collections.abc import Iterator 

33 

34 from numpy.typing import NDArray 

35 

36 

37class TAPState(Enum): 

38 """JTAG TAP Controller states.""" 

39 

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" 

56 

57 

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} 

69 

70 

71class JTAGDecoder(SyncDecoder): 

72 """JTAG protocol decoder. 

73 

74 Decodes JTAG bus transactions including TAP state machine transitions, 

75 IR/DR shift operations, and standard instruction identification. 

76 

77 Attributes: 

78 id: "jtag" 

79 name: "JTAG" 

80 channels: [tck, tms, tdi] (required), [tdo] (optional) 

81 

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 """ 

87 

88 id = "jtag" 

89 name = "JTAG" 

90 longname = "Joint Test Action Group (IEEE 1149.1)" 

91 desc = "JTAG/Boundary-Scan protocol decoder" 

92 

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 ] 

98 

99 optional_channels = [ # noqa: RUF012 

100 ChannelDef("tdo", "TDO", "Test Data Out", required=False), 

101 ] 

102 

103 options = [] # noqa: RUF012 

104 

105 annotations = [ # noqa: RUF012 

106 ("state", "TAP state"), 

107 ("ir", "Instruction register"), 

108 ("dr", "Data register"), 

109 ("instruction", "Decoded instruction"), 

110 ] 

111 

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] = [] 

118 

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. 

130 

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. 

138 

139 Yields: 

140 Decoded JTAG operations as ProtocolPacket objects. 

141 

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 

149 

150 n_samples = min(len(tck), len(tms), len(tdi)) 

151 if tdo is not None: 

152 n_samples = min(n_samples, len(tdo)) 

153 

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] 

159 

160 # Find rising edges of TCK (state updates on rising edge) 

161 rising_edges = np.where(~tck[:-1] & tck[1:])[0] + 1 

162 

163 if len(rising_edges) == 0: 

164 return 

165 

166 state_start_idx = 0 

167 trans_num = 0 

168 

169 for edge_idx in rising_edges: 

170 # Sample TMS at rising edge 

171 tms_val = bool(tms[edge_idx]) 

172 

173 # Update TAP state 

174 new_state = self._next_state(self._tap_state, tms_val) 

175 

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) 

181 

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) 

185 

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 

194 

195 instruction_name = JTAG_INSTRUCTIONS.get(ir_value, "UNKNOWN") 

196 

197 self.put_annotation( 

198 start_time, 

199 end_time, 

200 AnnotationLevel.FIELDS, 

201 f"IR: 0x{ir_value:02X} ({instruction_name})", 

202 ) 

203 

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 } 

211 

212 packet = ProtocolPacket( 

213 timestamp=start_time, 

214 protocol="jtag", 

215 data=bytes([ir_value]), 

216 annotations=annotations, 

217 errors=[], 

218 ) 

219 

220 yield packet 

221 trans_num += 1 

222 

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 

228 

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") 

232 

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 ) 

239 

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 } 

246 

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 

250 

251 packet = ProtocolPacket( 

252 timestamp=start_time, 

253 protocol="jtag", 

254 data=dr_bytes, 

255 annotations=annotations, 

256 errors=[], 

257 ) 

258 

259 yield packet 

260 trans_num += 1 

261 

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 = [] 

266 

267 self._tap_state = new_state 

268 state_start_idx = edge_idx 

269 

270 def _next_state(self, current: TAPState, tms: bool) -> TAPState: 

271 """Compute next TAP state based on TMS value. 

272 

273 Args: 

274 current: Current TAP state. 

275 tms: TMS signal value. 

276 

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 } 

347 

348 return transitions[current][tms] 

349 

350 def _bits_to_value(self, bits: list[int]) -> int: 

351 """Convert bit list to integer (LSB first). 

352 

353 Args: 

354 bits: List of bit values (0 or 1). 

355 

356 Returns: 

357 Integer value. 

358 """ 

359 value = 0 

360 for i, bit in enumerate(bits): 

361 value |= bit << i 

362 return value 

363 

364 

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. 

373 

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. 

380 

381 Returns: 

382 List of decoded JTAG transactions. 

383 

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)) 

391 

392 

393__all__ = ["JTAG_INSTRUCTIONS", "JTAGDecoder", "TAPState", "decode_jtag"]