跳转到主要内容

VIO 那些不得不说的坑

由 judy 提交于

文章来源:FPGA入门到精通

本文收获

分清 VIO 和 ILA 的能力边界,调试核不再选错

提前避开五个坑,不用拿返工换经验

照抄一段代码,五个坑当场就能绕开

一、VIO与ILA的区别

VIO 和 ILA 的差别一句话说得完。ILA 是示波器,VIO 是遥控器。

示波器只能看,遥控器能改。「能改」是 VIO 省下综合时间的唯一原因,也是它全部坑的来源。

传统改一轮要改代码、综合、实现、生成 bit 流、下载,小设计几分钟,大设计半小时往上;VIO 改一个值,下一个时钟周期设计里就拿到了。

官方文档给这个核举的用例,就是「板子在远端、人不在板子跟前」。这正是它存在的理由。

例化本身没有技术含量,方向别接反就行。

vio_0 u_vio ( 

    .clk         (sys_clk),      // 必须是一直运行的时钟,见坑三 

    // ---- Output Probe:PC 写到 FPGA,在线改参数 ---- 

    .probe_out0  (force_rst),    // 1 位,软件复位 

    .probe_out1  (threshold),    // 16 位,动态阈值 

    .probe_out2  (work_mode),    // 2 位,工作模式 

    // ---- Input Probe:FPGA 读到 PC,实时观察 ---- 

    .probe_in0   (state_reg),    // 状态机当前状态 

    .probe_in1   (err_cnt),      // 错误计数 

    .probe_in2   (adc_data)      // ADC 采样值 

); 

wire detect = (adc_data > threshold);   // 直接用 VIO 送下来的阈值

二、第一个坑,是把方向记反

probe_out 是从 PC 写到 FPGA,probe_in 是从 FPGA 读到 PC。

字面直觉正好是反的。很多人看到 Output,第一反应是「FPGA 输出给 PC」。不是。它是 VIO 输出给设计的一条控制线,你在电脑上拖滑条,板子上那条线跟着变。

官方文档写得很清楚。probe_in 由你的设计驱动,VIO 只做一次快照,软件读取远慢于设计时钟,两次读取之间信号可能已经变化多次;probe_out 反过来,由 Vivado 或 ChipScoPy 驱动到设计里,编号 0 到 1023。

方向搞反的后果不止「读不到」。你要是把想读的状态配成 probe_out,那条线会被 VIO 反向驱动,你的状态寄存器等于被人从外面按着走。

你以为在读数,其实你在改值。这两件事在 VIO 里只差一个方向。

三、五个坑,一个比一个贵

下面逐个拆,每个都是同一套路,现象、根因、修法、判据。有的坑看一眼就明白,有的一条我当初也是找了半天才想通。

坑一,上电初值是 0。

现象是上电那几秒行为不对,手动拖一下滑条又正常了。

根因在初值上,它在 IP 生成时就能设,不改就是 0,从 bit 流下载那一刻起一直挂在你的设计上,直到你第一次拖滑条。所以「上电必须有效」或「上电必须无效」的控制信号,都不能直连 probe_out。

修法两条,省事的是在 IP 配置里把初值设对(这一栏很多人没注意到),稳一点的是加启动延时选择器,上电先强制一个安全值,等系统稳了再交给 VIO。

reg init_done = 1'b0; 

reg [23:0] init_cnt; 

always @(posedge clk) begin 

    if (init_cnt < 24'hFFFFFF) begin 

        init_cnt  <= init_cnt + 1'b1; 

        init_done <= 1'b0; 

    end else begin 

        init_done <= 1'b1; 

    end 

end 

wire force_rst_effective = init_done ? force_rst_from_vio : 1'b1;

判据是断开 VIO 面板再上电,行为应该和连着调试器时一致。

坑二,跨时钟域没同步。

现象是阈值改了偶尔生效、偶尔不生效,或者状态机跳到不该去的状态。

根因是 probe_out 在 VIO 的时钟域里变化,你直接送进了另一个时钟域的模块。两级同步器是标配;两个时钟频率差得大,还要先把变化展宽或加握手,因为两级寄存器解决不了「脉冲太窄」。

// VIO 挂在 clk_a 域,threshold 要在 clk_b 域使用 

reg [15:0] thresh_sync0, thresh_sync1; 

always @(posedge clk_b) begin 

    thresh_sync0 <= threshold_a; 

    thresh_sync1 <= thresh_sync0;   // 用 thresh_sync1 

end

判据是连改二十次不出一次异常。

坑三,时钟被门控,VIO 直接失联。

现象是设计一进低功耗态,或者主状态机停摆,Hardware Manager 里的 VIO 面板整个不动,你怎么拖都没反应。

根因在它的工作方式上。VIO 和设计同步,设计时钟的约束同样作用于它内部逻辑,所以它需要一个一直在跑的稳定时钟。你把被调试逻辑自己产生的门控时钟接给它,逻辑一停 VIO 跟着停,JTAG 那边看起来就像断线。

修法是换一个常跑的稳定时钟。频率也别太高,社区里常建议 50MHz 以下,这条是工程经验,官方文档没写死。

VIO 失联的时候,你第一反应会去查 JTAG 线。但八成是它自己的时钟停了。

坑四,只看值,判不出「动没动」。

现象是连续读两次值一样,于是判断这个信号死了,其实它只是在你两次读之间翻完了一大轮。

根因是读取速度,软件读一次的时间尺度跟设计时钟不是一个数量级,两次快照之间的翻转你全都看不见。

修法是用 Activity Detector。它返回四种状态,R 是上升沿、F 是下降沿、B 是双边沿、N 是没有沿,不占 BRAM 也不需要深度存储,只回答一个问题,这个信号到底动没动。注意它不是默认开的,得在 IP 配置里勾上 Enable Input Probe Activity Detectors。输出端口读活动返回 X,官方的说法是这个端口不支持活动检测。

判据是想确认「动没动」的时候用它,别用两次快照比大小。

坑五,调试核留在生产版里。

现象是资源残留,同时多了一个能通过 JTAG 读写内部信号的口子。根因很朴素,调试核是真实存在的硬件,不是软件里的开关。

修法是定版前把 VIO 和 ILA 一起去掉,写进发布检查清单;别靠人记,用条件编译或单独的 top 文件把它隔离出去。判据是生产 bit 流里查不到 debug hub。

四、接成一条流程,再交给脚本

VIO 单独用只解决一半问题,它只能告诉你「现在是什么值」,告诉不了你「刚才发生了什么」。

把它和 ILA 接在一起才是完整的一轮。动态阈值既送给设计,也送给 ILA 当触发条件,再用一个 probe_out 手动触发。

vio_0 u_vio ( 

    .clk        (adc_clk), 

    .probe_out0 (threshold),    // 动态阈值,同时送给 ILA 

    .probe_out1 (manual_trig),  // 手动触发 

    .probe_in0  (adc_value) 

); 

  

ila_0 u_ila ( 

    .clk     (adc_clk), 

    .probe0  (adc_value), 

    .probe1  (threshold), 

    .trig_in (manual_trig) 

);

流程是,面板上先看实时值的范围,设一个初值,点一下手动触发,ILA 立刻给你一段波形,判断阈值切得合不合理,不满意就改值再触发,三五次收敛。

再往上一步,这些滑条能被脚本拖动。ChipScoPy 的 VIO API 就三个方法,read_probes 读、write_probes 写、read_probe_activity 读活动,配上 LTX 文件可以按 HDL 里的层级名直接定位探针。走 Tcl 也一样,set_property OUTPUT_VALUE 加 commit_hw_vio,一次改一批。

能被人拖动的滑条,才能被脚本拖动。