3. JTAG 状态机详解:从复位到数据移位的完整旅程

JTAG 的核心,说白了就是一台有 16 个状态的有限状态机。你每次在调试器上点“连接”或“单步”,背后都是这个状态机在按规则跳动。我刚开始学 JTAG 时,总觉得这 16 个状态太复杂,后来发现——你真正需要记住的,其实就 8 个关键状态。

今天我们就把这 8 个状态掰开揉碎,一个一个讲清楚。

核心观点:JTAG 状态机是 TAP(Test Access Port)控制器的心脏。TCK 的每个上升沿,状态机都会根据 TMS 的电平跳转到下一个状态。你控制 TMS,就能控制整个调试流程。

3.1 状态机全景图

先看一张总图,把 16 个状态的位置关系搞清楚。我习惯把状态机分成两条腿:一条是 DR(数据寄存器)路径,一条是 IR(指令寄存器)路径。两条路径的结构完全对称,只是操作的对象不同。

JTAG TAP 状态机全景图(16 状态) Test-Logic-Reset Run-Test/Idle Select-DR-Scan Select-IR-Scan Capture-DR Shift-DR Exit1-DR Pause-DR Capture-IR Shift-IR Exit1-IR Pause-IR TMS=1 时状态向上/右跳转,TMS=0 时向下/左跳转

嗯,这张图看着有点密,但你只要记住:从 Reset 出发,经过 Idle,然后选择走 DR 路还是 IR 路。两条路的结构一模一样,都是 Capture → Shift → Exit1 → Pause → Exit2 → Update 的流程。

3.2 八个关键状态详解

3.2.1 Test-Logic-Reset

这是状态机的起点,也是安全屋。上电后 TAP 控制器默认就在这个状态。此时 TDI 和 TDO 之间是直通的——不对,准确说是 TDO 处于高阻态,TDI 输入被忽略。整个测试逻辑被复位,所有寄存器清零。

我的习惯:每次开始新的调试会话前,我都会先发 5 个 TCK 时钟,TMS 保持高电平,确保状态机回到 Test-Logic-Reset。这相当于给 JTAG 链路做一次“硬复位”。有些芯片在异常状态卡住时,这招特别管用。

3.2.2 Run-Test/Idle

从 Reset 出来,TMS 拉低一个时钟,就进入 Idle。这是状态机的“待机室”。在这里,你可以选择什么都不做,也可以让某些测试逻辑运行(比如自测试)。

我个人习惯把 Idle 当作“安全过渡态”。比如在两次扫描操作之间,我会让状态机回到 Idle 待一两个时钟,确保前一次操作完全结束。

3.2.3 Select-DR-Scan

这是分岔路口。从 Idle 发一个 TMS=1,就进入 Select-DR-Scan。如果你紧接着发 TMS=0,就进入 Capture-DR;如果发 TMS=1,就跳到 Select-IR-Scan。

说白了,这个状态就是让你决定:接下来我要操作数据寄存器,还是指令寄存器?

3.2.4 Capture-DR

进入 Capture-DR 后,在 TCK 的上升沿,选中的数据寄存器会把当前值“捕获”到移位寄存器链中。比如你要读 IDCODE,芯片就会把 ID 值加载到移位链里。

我曾经踩过的坑:Capture-DR 只持续一个 TCK 周期。如果你在这个状态停留超过一个时钟,第二次捕获可能会覆盖第一次的数据。所以 Capture-DR 之后必须立刻进入 Shift-DR,不能犹豫。

3.2.5 Shift-DR

这是最忙的状态。在 Shift-DR 里,每个 TCK 上升沿,移位寄存器都会移动一位:TDI 上的数据移入,TDO 上的数据移出。你要读多少位,就在这个状态待多少个时钟。

举个例子,读 IDCODE 通常是 32 位,那就在 Shift-DR 里待 32 个时钟。第 1 个时钟移出最低位,第 32 个时钟移出最高位。

// 伪代码:在 Shift-DR 中移位 32 位
for (i = 0; i < 32; i++) {
    TDI = (data_to_send >> i) & 0x01;  // 逐位发送
    TCK = 1; delay();                  // 上升沿
    bit_received = TDO;                // 读取 TDO
    data_received |= (bit_received << i);
    TCK = 0; delay();                  // 下降沿
}

3.2.6 Exit1-DR

Shift-DR 结束后,你必须进入 Exit1-DR。这是一个临时暂停点。在这里你可以选择:

  • TMS=0 → 进入 Pause-DR,暂停移位
  • TMS=1 → 进入 Update-DR,结束本次扫描

我一般直接走 TMS=1 到 Update-DR,除非我需要暂停一下处理其他事情。

3.2.7 Pause-DR

Pause-DR 允许你暂停移位操作,同时保持移位寄存器中的数据不变。这在处理长数据流时很有用——比如你要读一个 1024 位的边界扫描链,中间可以去处理一下中断,再回来继续。

从 Pause-DR 出来有两个选择:

  • TMS=0 → 留在 Pause-DR
  • TMS=1 → 进入 Exit2-DR

3.2.8 Update-DR

这是 DR 路径的终点。在 Update-DR 的 TCK 下降沿,移位寄存器中的数据会被锁存到并行的输出寄存器中。对于写操作,数据在这一步真正生效。

关键区别:Capture-DR 是“读”的起点,Update-DR 是“写”的终点。Capture 把芯片内部状态搬到移位链,Update 把移位链的数据写到芯片内部。

3.3 状态转换速查表

当前状态 TMS=0 下一状态 TMS=1 下一状态 典型用途
Test-Logic-Reset Run-Test/Idle Test-Logic-Reset 上电复位、异常恢复
Run-Test/Idle Run-Test/Idle Select-DR-Scan 待机、等待
Select-DR-Scan Capture-DR Select-IR-Scan 选择数据路径
Capture-DR Shift-DR Exit1-DR 捕获数据到移位链
Shift-DR Shift-DR Exit1-DR 逐位移位(核心操作)
Exit1-DR Pause-DR Update-DR 暂停或结束移位
Pause-DR Pause-DR Exit2-DR 暂停移位
Exit2-DR Shift-DR Update-DR 从暂停恢复
Update-DR Run-Test/Idle Select-DR-Scan 数据生效、结束扫描

3.4 实战:一个完整的读 IDCODE 流程

光讲理论不过瘾,我们走一遍实际流程。假设你要读芯片的 IDCODE(32 位),状态机应该怎么跳?

  1. Test-Logic-Reset → 发 5 个 TMS=1 的 TCK,确保复位
  2. Run-Test/Idle → TMS=0,进入 Idle
  3. Select-DR-Scan → TMS=1,选择 DR 路径
  4. Capture-DR → TMS=0,捕获 IDCODE 到移位链
  5. Shift-DR → TMS=0,停留 32 个时钟,逐位移出数据
  6. Exit1-DR → TMS=1,结束移位
  7. Update-DR → TMS=1,更新(读操作其实不需要,但流程必须走完)
  8. Run-Test/Idle → TMS=0,回到 Idle,准备下一次操作
小技巧:在 Shift-DR 阶段,TMS 必须保持为 0,直到最后一位移出。如果你在中间误发了 TMS=1,状态机会提前跳到 Exit1-DR,数据就丢了。我早期调试时犯过这个错,查了半天才发现是 TMS 时序没控制好。

3.5 避坑指南

  • 不要在 Shift-DR 里多待时钟:移位位数必须精确等于寄存器长度。多一个时钟,数据就多移了一位,整个字节序就乱了。
  • 注意 TCK 的边沿:状态机在 TCK 上升沿跳转,数据在 TCK 下降沿变化。这个时序关系搞反了,数据就采不对。
  • Pause-DR 不是必须的:大多数调试场景根本用不到 Pause-DR。我只有在调试边界扫描链(几百位长)时才用过它。
  • IR 路径和 DR 路径完全对称:你学会了 DR 路径,IR 路径自然就会了。唯一的区别是 IR 路径操作的是指令寄存器,通常只有 4-8 位。

好了,JTAG 状态机的 8 个关键状态就讲到这里。你想想看,其实核心就是一条线:Reset → Idle → Select → Capture → Shift → Exit1 → Update → 回到 Idle。把这个循环刻在脑子里,JTAG 调试就入门了。