Virtual JTAG Verilog实现:FPGA片上调试核心代码详解
1. 项目概述深入理解Virtual JTAG的Verilog实现在FPGA调试的世界里Virtual JTAG虚拟JTAG是一个强大但常被低估的工具。它不像SignalTap II那样直观也不像外部逻辑分析仪那样“即插即用”但一旦你掌握了它的核心——即如何通过Verilog代码与TAP控制器状态机对话——你就获得了一种深度、灵活且不占用额外I/O引脚的片上调试能力。上一期我们讨论了Virtual JTAG的原理与MegaWizard配置今天我们把焦点完全放在代码上。我将带你逐行拆解一个经典的SAMPLE/FEED指令示例的Verilog实现这不仅仅是“照着写就能用”更重要的是理解每一个状态信号如cdr,sdr,udr在何时、为何被触发以及如何据此设计出稳定可靠的采样与加载逻辑。这对于实现自定义的调试指令、内建自测试BIST或动态重配置都至关重要。2. 核心代码结构与模块接口解析2.1 顶层信号定义与功能划分首先我们来看整个设计的信号框架。这段代码的核心是实例化了一个Altera提供的Virtual JTAG IP核my_vji_a并围绕它构建了用户逻辑。wire [3:0] counter1; // 这是一个需要被采样观测的4位计数器来自设计其他部分 reg [3:0] feed_reg; // 四位的DR寄存器用于加载输入值 wire tdi, tck, cdr, cir, e1dr, e2dr, pdr, sdr, udr, uir; reg tdo, bypass_reg; wire [1:0] ir_in; // 两位的IR寄存器输出来自my_vji_a wire sample ir_in[0]; // IR译码2b01表示SAMPLE命令 wire feed ir_in[1]; // IR译码2b10表示FEED命令 reg [3:0] offload_reg; // 四位的DR寄存器用于输出关键点解析ir_in[1:0]与指令译码这是理解整个设计的关键。Virtual JTAG IP核的输出ir_in直接反映了通过Quartus II的System Console或TCL脚本写入的指令。在这个例子中我们定义了两个指令sample ir_in[0]: 当ir_in为2‘b01时有效。这是一个“只读”指令用于将counter1的值采样并移出。feed ir_in[1]: 当ir_in为2’b10时有效。这是一个“只写”指令用于将数据移入并加载到feed_reg进而可能去控制counter1。这种直接位映射的译码方式非常高效但要求你在设计IP核时在MegaWizard中定义的指令码Instruction Register必须与此对应。例如你定义指令“SAMPLE”的码值为1二进制01“FEED”的码值为2二进制10。三组关键寄存器offload_reg[3:0]:输出移位寄存器。在SAMPLE指令下它先并行装载counter1的值然后在TCK驱动下将其数据一位一位地从offload_reg[0]移到tdo。feed_reg[3:0]:输入移位寄存器。在FEED指令下它通过tdi在TCK驱动下一位一位地串行装入数据装满4位后在特定时刻并行输出例如更新counter1的初始值。bypass_reg:旁路寄存器。当JTAG链上的指令不是针对本Virtual JTAG模块时即ir_in既不是SAMPLE也不是FEED数据流经此寄存器确保JTAG链的连续性。它本质上是一个单比特的移位寄存器tdi打一拍后直接给tdo。TAP控制器状态信号cdr,sdr,udr,cir,uir等。这些是Virtual JTAG IP核输出的、与IEEE 1149.1标准TAP控制器状态机严格同步的信号。它们是高电平有效的脉冲信号每个信号对应TAP状态机中的一个状态。你的所有用户逻辑都必须严格同步于tck并根据这些状态信号来执行相应操作。2.2 Virtual JTAG IP核实例化/* instantiation of the vji mega function */ my_vji_a VJI_INST( .ir_out (2‘b0), // input to megafunction .tdo (tdo), // input to mega function .ir_in (ir_in), // output from mega function .tck (tck), // output from mega function .tdi (tdi), // output from mgafunction .virtual_state_cdr (cdr), .virtual_state_e1dr(e1dr), .virtual_state_e2dr(e2dr), .virtual_state_pdr (pdr), .virtual_state_sdr (sdr), .virtual_state_udr (udr), .virtual_state_uir (uir), .virtual_state_cir (cir) );接口说明与实战技巧ir_out这是输入到IP核的指令寄存器值。在这个例子中我们固定为0意味着我们不主动通过FPGA逻辑去改变JTAG指令而是完全由外部的JTAG控制器如System Console来设置。这是一个典型用法。如果你想实现JTAG链的嵌套或动态重配才需要驱动这个端口。tdo这是整个Virtual JTAG模块的输出必须连接到IP核的tdo输入。这是一个易错点你需要设计一个多路选择器如下文的“Node TDO Output”根据当前指令选择offload_reg[0]、feed_reg[0]或bypass_reg驱动到tdo再反馈给这个端口。tck,tdi由IP核输出的JTAG时钟和数据输入。所有用户侧的移位操作都必须严格在tck的上升沿或下降沿需统一进行。ir_in当前生效的指令由IP核输出。virtual_state_*信号这是精髓所在。它们标志着TAP控制器进入了哪个状态。注意在Quartus的早期版本中这些状态信号可能命名略有不同如tck可能是tck_out。务必根据你所用IP核生成的模块声明文件.v或.vh来连接。实例化后最好做一个简单的仿真确认上电后这些信号能否被正确触发。3. 用户逻辑设计状态驱动的移位与捕获这部分是真正的用户自定义逻辑也是调试功能的具体实现。你必须深刻理解TAP状态机的流转。3.1 SAMPLE指令处理逻辑数据读出/* 1. Sample Instruction Handler */ always (posedge tck) // 针对SAMPLE指令的处理 if ( sample cdr ) offload_reg counter1; else if ( sample sdr ) offload_reg {tdi, offload_reg[3:1]}; // 典型的移位寄存器操作MSB to LSB操作流程与状态对应Capture-DR (cdr)状态当TAP控制器处于Capture-DR状态且当前指令是SAMPLE时这是一个并行加载时刻。我们将要观测的信号counter14位一次性捕获到offload_reg中。这是“采样”动作的发生点。为什么在cdr做根据JTAG标准Capture-DR状态就是用于将并行数据加载到数据寄存器DR的。在这里我们的DR就是offload_reg。Shift-DR (sdr)状态接下来进入Shift-DR状态。在这个状态下每一个tck上升沿都会发生一次移位操作。offload_reg的高3位(offload_reg[3:1])向右移动一位最低位(offload_reg[0])被输出到tdo通过后续的选择器。同时tdi线上的数据被移入offload_reg的最高位(offload_reg[3])。{tdi, offload_reg[3:1]}是Verilog的位拼接语法完美描述了一次从MSB向LSB的移位操作。注意在SAMPLE指令下我们虽然也接收tdi数据但通常忽略它因为目的是读出。这里移位操作主要是为了将offload_reg的数据逐位移出。后续状态 (e1dr,pdr,e2dr,udr)对于SAMPLE指令在数据移出完成后经过Exit1-DR、Pause-DR、Exit2-DR最终到达Update-DR状态。在这个例子中Update-DR状态没有执行任何操作因为数据已经读出不需要更新内部逻辑。实操心得这里有一个非常重要的细节offload_reg的捕获cdr和移位sdr是互斥的由if...else if保证。这意味着在cdr那个短暂的时钟周期我们捕获了counter1的瞬时值。如果counter1在cdr时刻正在变化比如也在tck边沿可能会产生亚稳态或捕获到不确定值。因此最好确保被采样的信号counter1与tck是异步的或者已经通过了同步器处理。对于高速或跨时钟域信号建议先用一个由tck时钟域触发的寄存器打一拍再用这个寄存器的输出连接到counter1的位置。3.2 FEED指令处理逻辑数据写入/* 2. Feed Instruction Handler */ always (posedge tck) // 针对FEED指令的处理 if ( feed sdr ) feed_reg {tdi, feed_reg[3:1]}; // 典型的移位寄存器操作MSB to LSB操作流程与状态对应Capture-DR (cdr)状态对于FEED指令cdr状态通常被忽略。因为我们要写入新数据不需要捕获旧数据。所以代码中没有对feed cdr的处理。Shift-DR (sdr)状态这是串行加载阶段。每一个tck上升沿外部通过tdi送入的数据位被移入feed_reg的最高位feed_reg原有数据右移。经过4个tck周期4位新数据就完全占据了feed_reg[3:0]。Update-DR (udr)状态关键代码示例中并没有直接显示在udr状态下的操作。这是原示例一个可以完善的点。通常在数据串行移位完成后TAP控制器进入Update-DR状态。此时应该将已经移入完毕的feed_reg的值并行加载到目标寄存器例如用来设置counter1的初始值。原示例可能将这部分逻辑放在了另一个always块或默认连接中。一个更完整的写法应该补充always (posedge tck) begin if (feed udr) begin counter1_load_value feed_reg; // 将移位好的数据锁存 end end // 或者用组合逻辑但要注意时序 // assign counter1 (some_condition) ? feed_reg : ...;为什么在udr做Update-DR状态的意义就是将移位寄存器中的数据“更新”到系统的并行接口上从而影响系统行为。在这里就是将feed_reg的值正式生效。注意事项数据移位的方向MSB first还是LSB first必须与上位机软件如System Console的发送顺序匹配。这个例子是MSB firsttdi进入最高位。如果你在System Console里用device_virtual_ir_shift或device_virtual_dr_shift命令需要清楚它的移位顺序。不匹配会导致读写的数据位序颠倒。3.3 旁路寄存器与TDO输出选择/* 3. Bypass register */ // 旁路寄存器没有针对这个virtual_jtag的操作就旁路 always (posedge tck) bypass_reg tdi; /* 4. Node TDO Output */ // TDO输出选择器根据IR的不同选择不同的信号输出 always ( sample, feed, feed_reg[0], offload_reg[0], bypass_reg ) begin if (sample) tdo offload_reg[0]; else if (feed) tdo feed_reg[0]; // Used to maintain the continuity of the scan chain. // 在移位输入时也要保证jtag链路有输出 else tdo bypass_reg; end设计解析旁路寄存器 (bypass_reg)它非常简单就是一个在tck驱动下将tdi直接延迟一个周期输出。它的存在至关重要保证了当JTAG链上其他器件或本器件执行其他指令时数据流可以无损通过这个Virtual JTAG模块维持整条JTAG链的完整性。TDO输出选择器这是一个组合逻辑的always块敏感列表是相关信号或者更严谨地可以用assign tdo sample ? offload_reg[0] : (feed ? feed_reg[0] : bypass_reg);来描述。它的功能是根据当前指令选择三路数据中的一路送到tdo输出。sample模式输出offload_reg的最低位即正在被移出的数据位。feed模式输出feed_reg的最低位。这一点非常巧妙且必要。在FEED指令的移位阶段sdr我们一边从tdi移入数据到feed_reg的高位同时需要将feed_reg的最低位通过tdo移出。移出的数据可能是之前残留的旧数据但这保证了JTAG链在移位过程中不断流符合标准。上位机在移位时也会同时读取tdo的数据只是我们通常不关心它。默认模式输出bypass_reg实现旁路。常见问题如果这个选择器逻辑设计错误例如在feed模式下错误地输出了offload_reg[0]会导致JTAG链数据混乱上位机读取的数据完全不对甚至导致链路卡死。务必仔细检查这里的条件判断和信号连接。建议为这个选择器编写一个简单的测试平台验证在三种指令模式下tdo的输出是否与预期一致。4. TAP控制器状态机深度解读与代码设计范式原作者的说明非常到位这里我再结合代码和状态机图进行深化。4.1 DR扫描Scan-DR状态序列与代码映射TAP控制器在执行数据寄存器扫描时会严格遵循以下状态序列Capture-DR-Shift-DR-Exit1-DR-Pause-DR-Exit2-DR-Update-DR-Run-Test/Idle在我们的代码中用到的状态信号和操作是cdr(Capture-DR): 用于SAMPLE指令的并行捕获 (offload_reg counter1)。对于FEED指令此状态通常空过。sdr(Shift-DR): 用于所有指令的串行移位。无论是SAMPLE指令移出数据还是FEED指令移入数据都在此状态持续发生。udr(Update-DR): 用于FEED指令的并行更新代码中隐含建议显式写出。对于SAMPLE指令此状态通常空过。e1dr,pdr,e2dr这几个状态在简单应用中通常不执行操作它们是状态机流转的必经之路为更复杂的操作如暂停扫描留出了钩子。代码设计范式你可以将你的用户DR操作逻辑看作是一个围绕这三个核心状态cdr,sdr,udr的有限状态机。always (posedge tck) begin case (ir_in) SAMPLE_INSTR: begin if (cdr) begin // 捕获阶段 dr_reg parallel_input_data; end else if (sdr) begin // 移位阶段 dr_reg {tdi, dr_reg[WIDTH-1:1]}; tdo_output dr_reg[0]; // 注意tdo逻辑可能需要独立处理 end // udr 状态无操作 end FEED_INSTR: begin // cdr 状态无操作 if (sdr) begin // 移位阶段 dr_reg {tdi, dr_reg[WIDTH-1:1]}; tdo_output dr_reg[0]; end else if (udr) begin // 更新阶段 parallel_output_data dr_reg; end end // ... 其他指令 default: begin // Bypass 或未定义指令 if (sdr) bypass_reg tdi; tdo_output bypass_reg; end endcase end这是一个更结构化的模板清晰地分离了不同指令和不同状态下的行为。4.2 IR扫描Scan-IR的简化处理代码中只使用了ir_in这个并行输出端口而没有使用cir(Capture-IR) 和uir(Update-IR) 信号。这是Virtual JTAG IP核提供的一大便利。在标准JTAG中指令也需要通过tdi串行移入指令寄存器IR过程与DR扫描类似涉及Capture-IR、Shift-IR、Update-IR等状态。Virtual JTAG IP核帮我们处理了这一切。当上位机发送指令切换命令时IP核内部完成了IR的移位和更新并直接将更新后的指令值呈现在ir_in端口上。我们的用户逻辑只需要读取ir_in即可知道当前指令无需关心IR扫描的时序细节。只有在需要动态改变JTAG指令例如实现一个可编程的JTAG路由器时才可能需要用到cir和uir信号并配合ir_out端口。4.3 综合视图与调试技巧将上述所有部分结合起来这个Virtual JTAG模块在FPGA内部形成了一个小的“从机”JTAG链段。它响应特定的指令SAMPLE, FEED在相应的DR扫描周期内执行我们定义的并行加载、串行移位和并行更新操作。调试技巧仿真先行在写System Console TCL脚本之前务必用ModelSim或QuestaSim等工具进行仿真。编写一个简单的testbench模拟tck、tdi并控制virtual_state_*信号按顺序跳变观察offload_reg、feed_reg和tdo的行为是否符合预期。这是排查逻辑错误最有效的方法。SignalTap验证如果设计已经下载到FPGA但System Console通信不正常可以用另一个SignalTap实例如果资源允许来抓取tck、tdi、tdo、ir_in以及关键状态信号。看看当你在Console里发送指令时这些信号是否有活动。这能帮你确定问题是出在FPGA逻辑侧还是在上位机通信侧。指令验证在System Console中先用get_device_info确认找到了设备。然后用device_virtual_ir_shift命令尝试写入一个已知指令如SAMPLE的代码再立刻用该命令读回看是否一致。这可以验证IR路径是否通畅。分步测试先测试BYPASS功能。如果BYPASS都不通可能是最基础的JTAG链或模块实例化有问题。再单独测试SAMPLE功能用一个固定的测试信号代替counter1看能否正确读出。最后测试FEED功能。5. 常见问题、排查与高级应用思路5.1 典型问题速查表问题现象可能原因排查步骤System Console连接失败找不到设备1. JTAG电缆连接或驱动问题。2. FPGA配置未完成或代码未正确运行。3. Virtual JTAG IP核未正确例化或参数如IR宽度设置错误。1. 检查电缆用Quartus Programmer确认JTAG链正常。2. 确认FPGA配置成功用简单LED代码测试。3. 检查编译报告确认Virtual JTAG IP核被综合。能连接但发送指令后无反应或返回错误数据1. 用户逻辑中ir_in译码错误。2. TDO输出选择逻辑错误。3. 移位方向MSB/LSB与上位机不匹配。4. 状态信号cdr,sdr,udr使用逻辑错误。1. 仿真确认在对应指令下用户逻辑块是否被激活。2. 用SignalTap抓取tdo对比预期输出。3. 确认System Console命令的移位顺序调整代码或命令参数。4. 仔细对照状态机时序图检查每个状态下的操作。只能读写一次后续操作失败1. 状态机未正确返回Run-Test/Idle。2. 用户逻辑在非预期状态下修改了关键寄存器导致状态紊乱。3. TDO在非移位期间输出高阻或错误值干扰链路过零检测。1. 确保逻辑只响应cdr/sdr/udr脉冲不要在tck每个周期都执行条件外的操作。2. 检查tdo输出选择逻辑确保在任何情况下包括idle都有确定的驱动源不能是z。读取的数据不稳定偶尔正确1. 被采样信号如counter1与tck时钟域异步在cdr时刻出现亚稳态。2.tck频率过高用户逻辑时序不满足。1. 对异步输入信号使用双寄存器同步器在tck域打两拍后再接入。2. 降低System Console的JTAG时钟频率如果支持或优化逻辑代码。5.2 从示例到实战扩展设计思路掌握了这个基本框架后你可以将其扩展成更强大的调试工具多寄存器与地址寻址定义更宽的IR如8位用高几位作为“地址”或“命令”低几位作为“子命令”。例如指令8‘h10代表“读GPIO状态”8’h20代表“写配置寄存器”。在用户逻辑中解析这些指令访问FPGA内部不同的寄存器或存储器块。构建内嵌逻辑分析仪利用SAMPLE机制创建一个深度采样FIFO。当触发条件满足时连续采样一组信号并存入Block RAM中。然后通过Virtual JTAG缓慢地将RAM中的数据读出。这样可以实现不占用额外I/O的深度跟踪。动态重配置与调试注入利用FEED机制向FPGA内部注入控制字。例如动态改变一个PID控制器的系数、调整通信协议的参数、或者注入一个特定的测试激励序列。多核调试接口在SoC FPGA中为每个处理器核如Nios II挂载一个Virtual JTAG从机。通过一条物理JTAG链上位机可以分别访问不同核的调试寄存器实现集中式但可区分的调试管理。与System Console TCL脚本深度集成将复杂的调试操作封装成TCL过程proc。例如一个read_memory过程内部自动完成指令发送、地址递增、数据连续读取等操作对上层提供简单的调用接口。Virtual JTAG的Verilog编码本质上是实现一个遵循JTAG标准的、可定制的串行通信从机。吃透状态机与代码的对应关系你就掌握了这把钥匙。剩下的就是如何用它去打开FPGA内部那扇丰富的调试与配置之门了。我个人的经验是从一个这样的小例子开始亲手写一遍仿真一遍再上板用System Console调通整个过程遇到的每一个错误和解决它的方法都会让你对JTAG和FPGA内部调试机制的理解加深一层。