深入解析Virtual JTAG的Verilog实现:FPGA调试核心代码剖析 1. 项目概述深入理解Virtual JTAG的Verilog实现在FPGA调试的世界里JTAG接口是连接物理世界与逻辑世界的桥梁。而Virtual JTAG则是Altera现Intel提供的一种强大工具它允许我们在FPGA内部“虚拟”出一个JTAG TAP控制器通过Quartus自带的System Console或用户自定义的TCL脚本就能像操作真实JTAG芯片一样去读写FPGA内部的任何寄存器或信号。上一期我们搭建了框架理解了信号流今天我们来啃最硬的骨头——亲手剖析并编写驱动这一切的Verilog代码。这段代码是Virtual JTAG应用的核心理解了它你就能随心所欲地通过一根下载线窥探或操控FPGA内部的运行状态这对于复杂逻辑的调试、系统初始状态加载、甚至是产品现场升级后的参数注入都至关重要。本文适合已经对FPGA开发流程、Verilog硬件描述语言以及JTAG协议基础有了解的工程师。我们将完全聚焦于如何解读和编写那个连接my_vji_a宏模块与用户逻辑的“胶水代码”。我会以一个经典的“采样与馈入”案例为例不仅逐行分析代码更会解释每个状态、每个判断背后的硬件时序考量并分享我在实际项目中积累的调试心得和避坑指南。目标是让你读完就能动手修改出适合自己项目的Virtual JTAG调试链路。2. 核心代码结构与信号解析首先我们把目光聚焦在那段承上启下的关键Verilog代码上。它的作用很明确实例化Altera提供的Virtual JTAG IP核my_vji_a并编写相应的状态机与数据通路来响应IP核发出的各种JTAG状态信号最终实现数据的输入FEED和输出SAMPLE。2.1 关键信号定义与IP核实例化让我们先看看代码的骨架部分这里定义了所有交互的信号并连接了IP核。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寄存器用于输出 /* instantiation of the vji mega function */ my_vji_a VJI_INST( .ir_out (2b0), // input to megafunction我们这里不输出IR故接地 .tdo (tdo), // input to mega function这是我们输出的串行数据 .ir_in (ir_in), // output from mega function这是IP核输出的当前指令 .tck (tck), // output from mega function虚拟JTAG时钟 .tdi (tdi), // output from megafunction串行输入数据 .virtual_state_cdr (cdr), // Capture_DR 状态信号 .virtual_state_e1dr(e1dr), // Exit1_DR .virtual_state_e2dr(e2dr), // Exit2_DR .virtual_state_pdr (pdr), // Pause_DR .virtual_state_sdr (sdr), // Shift_DR .virtual_state_udr (udr), // Update_DR .virtual_state_uir (uir), // Update_IR .virtual_state_cir (cir) // Capture_IR );信号深度解读ir_in[1:0]与指令译码这是整个逻辑的控制核心。my_vji_a模块内部有一个2位的指令寄存器IR。当我们在System Console中使用device_virtual_ir_dr命令时指定的指令值就会出现在这个端口上。代码中将其译码为两个使能信号sample ir_in[0]当ir_in为2‘b01时有效表示当前指令是“采样”即我们要从FPGA内部读取数据。feed ir_in[1]当ir_in为2’b10时有效表示当前指令是“馈入”即我们要向FPGA内部写入数据。为什么这样译码这是一种简化的设计。理论上IR可以有很多位对应很多条指令。这里只用2位IR和两种指令来演示最核心的输入输出操作。在实际项目中你可以扩展IR位数实现更复杂的多指令集例如3‘b001读状态寄存器、3’b010写控制寄存器等。虚拟状态信号cdr,sdr,udr...这些是Virtual JTAG IP核模拟出的JTAG TAP控制器状态机信号。它们与标准的JTAG状态图Test-Logic-Reset, Run-Test/Idle, Capture-DR, Shift-DR, Update-DR等一一对应。这些信号是“高电平有效”且“单周期脉冲”。例如当TAP控制器处于Shift-DR状态时sdr信号会在那个tck周期内保持高电平。我们的所有操作都必须严格跟随这些状态的节拍。数据寄存器feed_reg,offload_reg,bypass_regfeed_reg用于FEED指令。在移位阶段Shift-DR从tdi一位位串行接收数据暂存起来最后在合适时机如Update-DR并行输出给目标如counter1。offload_reg用于SAMPLE指令。在捕获阶段Capture-DR从counter1并行加载数据然后在移位阶段Shift-DR一位位串行移出到tdo。bypass_reg旁路寄存器。当IR指令不是sample也不是feed即对本模块无效时为了保证JTAG扫描链的连续性数据需要直接从tdi延时一个tck周期后从tdo输出。这是一个最简单的移位寄存器。tdo输出选择这是一个关键的多路选择器。根据当前有效的指令sample或feed选择将哪个寄存器的LSB因为JTAG是MSB先出移位时LSB在输出端连接到tdo回送给Virtual JTAG IP核。如果没有有效指令则输出bypass_reg。注意tck是Virtual JTAG内部产生的时钟它与FPGA的主时钟如clk_50m是异步的。所有使用posedge tck的触发器都构成了一个独立的、由JTAG协议驱动的时钟域。在设计时必须注意跨时钟域问题尤其是在capture阶段采样FPGA内部信号如counter1时可能会遇到亚稳态。通常对于调试观测可以接受偶尔的误采样对于可靠的数据加载可能需要同步器或握手机制。2.2 指令处理状态机详解接下来我们分指令来看具体的处理过程。这是最需要理解JTAG状态机时序的地方。2.2.1 SAMPLE指令处理流程并行加载串行输出/* 1. Sample Instruction Handler */ always (posedge tck) // 所有操作同步于虚拟JTAG时钟tck if ( sample cdr ) // 条件1当前指令是SAMPLE且处于Capture_DR状态 offload_reg counter1; // 动作并行捕获目标信号 else if ( sample sdr ) // 条件2当前指令是SAMPLE且处于Shift_DR状态 offload_reg {tdi, offload_reg[3:1]}; // 动作串行移位MSB to LSB时序与逻辑拆解Capture_DR阶段 (cdr有效)当TAP控制器进入Capture-DR状态时cdr信号会拉高一个tck周期。此时如果sample指令有效代码会将需要观测的外部信号counter1一个4位向量并行地加载到offload_reg寄存器中。这就完成了一次“快照”。Shift_DR阶段 (sdr有效)紧接着TAP控制器进入Shift-DR状态。在此状态下每一个tck的上升沿sdr都有效。代码执行移位操作{tdi, offload_reg[3:1]}。这个操作有两层含义移出将offload_reg的最低位offload_reg[0]连接到tdo通过后续的输出选择逻辑在下一个tck周期这个位就会被Virtual JTAG IP核读取。移入将来自IP核的串行输入tdi移入offload_reg的最高位offload_reg[3]。注意对于SAMPLE指令这个移入的数据通常是无用的因为我们的目的是读出但移位操作本身必须进行以维持链路的通畅。移入的数据会被后续的移位覆盖。后续状态在Exit1-DR、Pause-DR、Exit2-DR、Update-DR状态sample指令通常不做任何操作因为数据已经在上一步被持续移出了。实操心得这里有一个常见的理解误区认为在Shift-DR阶段offload_reg的值会随着移位改变导致读出的数据错误。实际上只要我们确保在Capture-DR时刻捕获到的counter1值是正确的那么即便offload_reg在移位过程中被tdi数据填充每次移出的位仍然是最初捕获的那个数据序列的位。因为我们是MSB先出第一个tck移出的是捕获时刻的offload_reg[3]即counter1[3]同时tdi移入成为新的offload_reg[3]下一个tck移出的是原来的offload_reg[2]即counter1[2]以此类推。所以关键是要确保cdr脉冲到来时counter1是你想采样的值。2.2.2 FEED指令处理流程串行输入并行更新/* 2. Feed Instruction Handler */ always (posedge tck) // 针对FEED指令的处理 if ( feed sdr ) // 条件当前指令是FEED且处于Shift_DR状态 feed_reg {tdi, feed_reg[3:1]}; // 动作串行移位输入时序与逻辑拆解Capture_DR阶段对于FEED指令在Capture-DR阶段通常不做任何操作代码中没有if (feed cdr)的条件分支。这是因为我们要输入的是外部数据内部没有需要捕获的状态。Shift_DR阶段 (sdr有效)这是核心阶段。在每一个tck上升沿将串行数据tdi移入feed_reg的最高位。通过连续4个tck周期一个完整的4位数据就从tdi串行地存入了feed_reg。Update_DR阶段 (udr有效)原示例代码在这里有一个重要的省略仔细观察示例代码它只实现了移位但没有实现“并行更新”。通常我们需要在Update-DR状态将移位完成的feed_reg值并行加载到目标寄存器比如counter1中。完整的逻辑应该补充如下/* 2. Feed Instruction Handler - 补充更新逻辑 */ always (posedge tck) begin if ( feed sdr ) feed_reg {tdi, feed_reg[3:1]}; // 串行移位 // 增加Update_DR状态的处理 if ( feed udr ) counter1 feed_reg; // 将移位好的数据并行加载到目标 end为什么在Update-DR更新这是JTAG的标准行为。Shift-DR阶段只是把数据移入边界扫描寄存器而Update-DR状态才将数据真正应用到器件的内部逻辑上这可以避免在移位过程中数据变化对系统产生干扰。避坑指南很多初学者按照原始示例代码实现FEED功能后发现数据写不进去就是因为缺失了udr状态下的并行更新操作。务必记住移位Shift是为了传输更新Update才是为了生效。2.2.3 旁路寄存器与TDO输出选择/* 3. Bypass register */ // 旁路寄存器 always (posedge tck) bypass_reg tdi; // 注意这里用的是阻塞赋值()强调组合逻辑行为虽然写在时序块里但不推荐。通常用非阻塞赋值()。 /* 4. Node TDO Output */ // TDO输出选择器 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. else tdo bypass_reg; end代码分析旁路寄存器 (bypass_reg)它就是一个简单的触发器在每个tck将tdi的值寄存一下。当IR指令对本模块无效时即sample和feed都为0tdo选择输出bypass_reg。这样tdi的数据在经过一个时钟周期延迟后从tdo流出实现了JTAG链的“透明”穿透保证了链路的完整性即使链上有多个Virtual JTAG节点也能正常工作。TDO输出选择这是一个组合逻辑的always块敏感列表是sample, feed...。它根据当前有效的指令实时选择将哪个数据源的LSB送到tdo端口。执行SAMPLE时输出offload_reg[0]正在被移出的数据位。执行FEED时输出feed_reg[0]。这一点至关重要即使在写入数据也必须输出feed_reg的最低位以维持扫描链的连续性。系统在移位输入的同时也在读取tdo上的输出。如果这里输出不对链就断了。否则输出bypass_reg。注意旁路寄存器的阻塞赋值bypass_reg tdi在仿真中可能没问题但在综合时写在(posedge tck)块中使用阻塞赋值可能产生非预期的结果取决于综合工具。为了代码清晰和可综合强烈建议统一使用非阻塞赋值。可以改为bypass_reg tdi;。TDO输出选择逻辑是组合逻辑使用阻塞或非阻塞赋值在always (*)块中区别不大但为了一致性通常也用非阻塞。3. Virtual JTAG状态机信号与硬件时序对应原作者的“sld_virtual_jtag的说明”部分点出了理解这些*dr,*ir信号的关键。我们来将其与JTAG标准状态机对应起来并解释在代码中如何运用。3.1 DR扫描状态信号详解JTAG对数据寄存器DR的操作遵循一个固定的状态序列Capture-DR - Shift-DR - Exit1-DR - Pause-DR - Exit2-DR - Update-DR然后回到Run-Test/Idle。Virtual JTAG将每个状态用一个高电平脉冲信号表示。cdr(Capture_DR)状态机进入Capture-DR状态的那个时钟周期此信号为高。用于并行加载数据到DR。对应代码中的if (sample cdr)。sdr(Shift_DR)状态机处于Shift-DR状态的每一个时钟周期此信号均为高。用于串行移位数据。对应代码中的if (sample sdr)和if (feed sdr)。e1dr,pdr,e2dr对应Exit1, Pause, Exit2状态。在简单的应用中这三个状态通常不执行操作直接穿过。它们用于更复杂的控制例如在Pause-DR暂停移位。udr(Update_DR)状态机进入Update-DR状态的那个时钟周期此信号为高。用于将移位后的DR数据并行更新到内部逻辑。这是FEED指令生效的关键如前文所述示例代码遗漏了对此信号的处理。一个完整的SAMPLE指令JTAG操作波形概念图TAP StateIdleCapture-DRShift-DRShift-DRShift-DRShift-DRExit1-DRUpdate-DRIdletck_/‾\__/‾\__/‾\__/‾\__/‾\__/‾\__/‾\__/‾\__/‾\_virtual_state_cdr010000000virtual_state_sdr001111000virtual_state_udr000000010动作捕获counter1到offload_reg移出Bit3移出Bit2移出Bit1移出Bit0tdoZZBit3Bit2Bit1Bit0ZZZ3.2 IR扫描状态信号与简化设计对于指令寄存器IR的扫描也有类似的状态序列Capture-IR - Shift-IR - Exit1-IR - Pause-IR - Exit2-IR - Update-IR。Virtual JTAG提供了cir(Capture_IR)和uir(Update_IR)信号。然而在这个示例中我们巧妙地避开了直接处理IR扫描的复杂性。我们只使用了ir_in这个2位宽的输出端口。my_vji_a模块内部已经帮我们完成了IR的扫描、捕获和更新操作。当我们在System Console中发送一条新指令如device_virtual_ir_dr -instance_index 0 -ir_value 2后IP核内部会在Update-IR状态将新指令值锁存并立即反映在ir_in端口上。我们的用户逻辑只需要读取ir_in的值就知道当前该执行哪条命令了。这就是Virtual JTAG“虚拟”和“方便”的精髓所在它把复杂的JTAG TAP控制器状态机、IR扫描协议都封装好了只给我们提供简洁的状态脉冲和指令端口让我们可以像处理普通同步逻辑一样去处理JTAG通信。3.3 综合视图与资源利用原作者提到了用LPM宏单元替换功能块后编译的RTL视图。在Quartus中编译后你可以通过RTL Viewer看到综合后的网表。my_vji_a会被综合成一个黑盒代表Intel的IP核。你编写的always块会被综合成几个触发器feed_reg,offload_reg,bypass_reg和一些多路选择器MUX。tdo输出选择逻辑会综合成一个由sample和feed信号控制的选择器。整个逻辑所占用的资源非常少主要是几个寄存器和少量逻辑单元。这意味着你可以在一个FPGA设计中实例化多个Virtual JTAG节点用于调试不同的模块而几乎不用担心资源开销。4. 完整代码实现与系统集成基于以上的分析我们来整合一份更完整、更健壮的Verilog代码并说明如何将其集成到你的系统中。4.1 增强版的Verilog核心代码module virtual_jtag_controller ( input wire [3:0] counter_to_sample, // 需要被采样的外部计数器 output reg [3:0] data_to_feed, // 需要馈入到外部逻辑的数据 input wire clk, // 系统主时钟可选用于同步 output reg data_updated // 数据已更新脉冲可选 ); // Virtual JTAG IP核接口信号 wire tdi, tck; wire cdr, cir, e1dr, e2dr, pdr, sdr, udr, uir; reg tdo; wire [1:0] ir_in; // 内部寄存器与译码 reg [3:0] feed_reg; reg [3:0] offload_reg; reg bypass_reg; wire sample (ir_in 2b01); wire feed (ir_in 2b10); // 实例化Virtual JTAG IP核 (在Quartus中通过MegaWizard生成) my_vji_a u_vji ( .ir_out (2b0), .tdo (tdo), .ir_in (ir_in), .tck (tck), .tdi (tdi), .virtual_state_cdr (cdr), .virtual_state_cir (cir), .virtual_state_e1dr(e1dr), .virtual_state_e2dr(e2dr), .virtual_state_pdr (pdr), .virtual_state_sdr (sdr), .virtual_state_udr (udr), .virtual_state_uir (uir) ); // --- 1. SAMPLE指令处理采样counter_to_sample --- always (posedge tck) begin if (sample cdr) begin // Capture阶段并行加载外部信号 offload_reg counter_to_sample; end else if (sample sdr) begin // Shift阶段串行移出同时移入无用tdi数据 offload_reg {tdi, offload_reg[3:1]}; end end // --- 2. FEED指令处理移位数据并更新到data_to_feed --- always (posedge tck) begin if (feed sdr) begin // Shift阶段串行移入数据 feed_reg {tdi, feed_reg[3:1]}; end // 重要在Update阶段将数据锁存到输出端口 if (feed udr) begin data_to_feed feed_reg; // 可选产生一个更新脉冲通知系统其他部分 // 注意tck与clk异步需要同步处理 end end // --- 3. 旁路寄存器 --- always (posedge tck) begin bypass_reg tdi; end // --- 4. TDO输出选择逻辑 (组合逻辑) --- always (*) begin case (1b1) // 优先级编码风格 sample: tdo offload_reg[0]; feed: tdo feed_reg[0]; default: tdo bypass_reg; endcase end // --- 可选跨时钟域同步将更新事件同步到系统时钟域 --- reg feed_updated_tck; reg feed_updated_sync1, feed_updated_sync2; always (posedge tck) begin if (feed udr) begin feed_updated_tck 1b1; end else begin feed_updated_tck 1b0; end end always (posedge clk) begin feed_updated_sync1 feed_updated_tck; feed_updated_sync2 feed_updated_sync1; data_updated feed_updated_sync1 !feed_updated_sync2; // 上升沿检测 end endmodule4.2 在Quartus中创建与连接IP核生成IP核在Quartus的IP Catalog中搜索“Virtual JTAG”运行MegaWizard插件。配置参数Instance ID设置为0如果有多个实例需不同。IR width设置为2对应我们的两条指令。其他选项保持默认。生成文件MegaWizard会生成一个.v或.vhd文件例如my_vji_a.v和一个.qip文件。将这两个文件添加到你的Quartus工程中。实例化与连接在你的顶层模块或调试模块中实例化上面编写的virtual_jtag_controller并将counter_to_sample连接到你想观察的信号如某个状态机的状态码、计数器值将data_to_feed连接到你想控制的寄存器如一个配置寄存器、一个PWM占空比设定值。4.3 System Console TCL脚本示例硬件代码就绪后你需要通过TCL脚本来操作它。下面是一个简单的脚本示例保存为debug.tcl。# 连接到JTAG链 open_device device_lock -timeout 10000 # 设置Virtual JTAG实例索引与IP核配置的Instance ID一致 set_instance 0 # 1. 使用SAMPLE指令ir_value1读取4位数据 device_virtual_ir_dr -instance_index 0 -ir_value 1 -dr_value 0 # 执行一次DR扫描数据会从offload_reg中移出 set sampled_data [device_virtual_dr_shift -instance_index 0 -dr_length 4 -value_in 0] puts Sampled Counter Value: $sampled_data # 2. 使用FEED指令ir_value2写入4位数据 0xA (1010) device_virtual_ir_dr -instance_index 0 -ir_value 2 -dr_value 0xA # 注意-dr_value 参数就是通过tdi移入的数据 # 执行DR扫描将0xA移位进去同时也会读出一个值来自feed_reg[0]的移位输出 set feedback [device_virtual_dr_shift -instance_index 0 -dr_length 4 -value_in 0xA] puts Feedback during FEED: $feedback # 可以再次采样确认数据是否写入成功假设data_to_feed连接回了某个可读端口 device_virtual_ir_dr -instance_index 0 -ir_value 1 -dr_value 0 set check_data [device_virtual_dr_shift -instance_index 0 -dr_length 4 -value_in 0] puts Check after FEED: $check_data device_unlock close_device在Quartus中打开System Console通过source debug.tcl命令运行此脚本就能完成一次完整的读写操作。5. 高级技巧、调试心得与常见问题排查掌握了基本操作后我们来看看如何用得更好、更稳以及出了问题怎么查。5.1 高级应用技巧扩展指令集将IR宽度从2位增加到N位如4位支持16条指令。在Verilog中ir_in端口相应变宽译码逻辑变为case(ir_in)可以定义更多操作如READ_STATUS,WRITE_CONFIG,TRIGGER_EVENT等。可变长度DR扫描上述例子DR长度固定为4。你可以通过指令来动态选择DR长度。例如定义一条指令READ_LONG_REG对应的DR长度是32位。在状态机中使用一个计数器在sdr有效时计数达到指定长度后自动退出。与片上逻辑深度交互data_to_feed可以不是一个简单的寄存器而是一个写使能信号。当feed udr时产生一个单周期脉冲write_enable同时将feed_reg作为数据总线写入到另一个由系统时钟clk驱动的双端口RAM或寄存器堆中。这样就能实现通过JTAG初始化片上存储器。多实例调试在一个大型设计中可以为每个核心模块实例化一个独立的Virtual JTAG控制器并赋予不同的Instance ID。在TCL脚本中通过set_instance命令切换实现分模块、非侵入式调试。5.2 实战调试心得仿真先行在烧录到板子前务必用ModelSim等工具进行仿真。编写一个简单的Testbench模拟tck、tdi并按照JTAG状态机序列驱动cdr,sdr,udr等信号。观察offload_reg,feed_reg,tdo的行为是否符合预期。这是排查逻辑错误最快的方法。SignalTap II是好朋友将tck,tdi,tdo,ir_in,cdr,sdr,udr以及关键的内部寄存器feed_reg,offload_reg添加到SignalTap中。在System Console中执行操作时实时捕获这些信号。你可以清晰地看到状态信号的跳变、数据的移位过程以及tdo的输出是否正确。这是硬件调试的利器。理解“链”的概念如果你的设计中只有一个Virtual JTAG节点那么链很简单。但如果有多个节点或与其他JTAG器件如ARM核、CPLD串联你必须清楚它们在链上的顺序。System Console的device_virtual_ir_dr和device_virtual_dr_shift命令中的-instance_index参数就是用来选择链上不同节点的。顺序错误会导致通信失败。注意复位Virtual JTAG IP核和你的用户逻辑可能需要复位。确保在系统上电或全局复位时feed_reg,offload_reg等寄存器被初始化为已知状态如全0。这可以避免不可预测的行为。5.3 常见问题排查速查表现象可能原因排查步骤System Console报错Can‘t lock device或Instance index X not found1. FPGA未正确编程或JTAG连接断开。2. Virtual JTAG IP核未正确例化或Instance ID不匹配。3. 代码编译后未包含IP核。1. 检查USB-Blaster连接重新编程FPGA。2. 检查Quartus编译报告确认IP核被综合。核对TCL脚本中的-instance_index与IP核配置是否一致。3. 确认.qip文件已加入工程并参与编译。能锁住设备但读写数据全为0或全为11. Verilog代码中状态信号判断逻辑错误如sample cdr写成了sample cdr。2.tdo输出选择逻辑错误始终输出固定值。3. 信号连接错误counter_to_sample始终为0。1. 使用SignalTap触发在cdr或sdr有效时检查offload_reg/feed_reg是否被正确加载/移位。2. 检查always (*)块看tdo的逻辑选择是否正确。3. 在SignalTap中查看counter_to_sample信号是否变化。写入FEED的数据读回不正确1.最常见缺失udr状态下的并行更新操作。2.feed_reg在非Shift阶段被意外清零或修改。3. 目标寄存器data_to_feed被系统其他逻辑覆盖。1.重点检查Verilog代码中是否有if (feed udr) data_to_feed feed_reg;。2. 检查feed_reg的赋值逻辑确保只在sdr有效时移位。3. 检查data_to_feed的驱动源确保没有多驱动冲突。操作速度慢默认的JTAG TCK频率较低通常几MHz到几十MHz。对于大数据量传输速度是瓶颈。1. 在System Console中可以尝试使用-dr_value一次性写入多位数据减少命令交互次数。2. 对于极高速需求考虑使用其他接口如PCIe、Ethernet。Virtual JTAG更适合低频、小批量的调试和配置。多实例时只有第一个实例能工作TCL脚本中set_instance切换错误或链顺序理解有误。1. 在Quartus Assignment Editor中确认每个my_vji_a实例的instance_id参数设置正确且唯一。2. 在TCL脚本中在操作每个实例前都使用正确的set_instance命令。最后我个人在多个大型FPGA项目中使用Virtual JTAG的体会是它就像嵌入在芯片里的一个“后门”调试串口。初期搭建和调试代码需要花费一些精力但一旦跑通它就成为了一个极其可靠的调试和配置通道。尤其是在系统无法启动或关键接口不通时通过Virtual JTAG去读取内部状态寄存器、强制修改某个配置位往往能起到“起死回生”的效果。把它作为你FPGA调试工具箱中的常备利器绝对物超所值。