• Vivado(Vitis)版本:2020.2
  • FPGA开发板:Microphase Z7-Lite 7020开发板

FPGA设计调试流程

FPGA开发是一个不断迭代的过程,一般的FPGA设计流程一般包含下面几个步骤:

  • 硬件架构和算法验证:实现需要的功能需要哪几个模块,模块和模块之间如何进行通信和连接;硬件算法是否可行和稳定(以图像处理算法为例,一般可以采用MATLAB进行算法验证);
  • RTL代码编写;
  • 硬件调试与验证:一般这个过程会耗费大量的时间,如果没有一定的经验以及技巧,有可能会使得开发时间延长几倍,甚至开发失败;


调试,即Debug,有一定开发经验的人一定会明确这是设计中最复杂最磨人的部分。对于一个庞大复杂的FPGA工程而言,出现问题的概率极大,这时如果没有一个清晰的Debug思路,调试过程只能是像无头苍蝇一样四处乱撞。在FPGA设计中一般的调试思路如下所示:

首先排查硬件问题:在出现问题时,首先怀疑并排除硬件问题。首先检查开发板的供电和连接是否正常,是否有电子元件被烧毁,是否出现元件虚焊等问题。确认开发板以及供电没有问题后,使用例程或者已有的程序或者工程对出现问题的核心部件进行测试。例如,在读写DDR时,如果DDR没有反应,可以通过网络查找例程,或者使用开发板官方提供的例程对DDR读写进行测试,确认DDR可以正常工作;在读写SD卡时,可以尝试换一张SD卡操作,或者通过将SD卡切换到其他设备上,确保SD卡没有损坏等。实际工程应用中,需要灵活选择测试和排查方案,但是目的基本都是相同的。

其次排查全局信号:确认硬件连接没有问题后,排查全局信号可能出现的问题。全局信号一般指接在内部所有模块的信号,例如i_sys_clki_sys_rst_n等。需要确保这些信号正常工作,之后的RTL排查才有意义。

最后排查RTL代码:在确保硬件和全局信号没有问题后,再开始排查RTL代码。在RTL代码排查中也有一定的顺序可以参考,一般可以参考下面的顺序:

  • 检查主从设备(模块)之间的握手机制,或者说检查主从设备之间是否正常连接。很多时候可以参考设备的官方Datasheet检查主从模块之间的初始化指令是否书写正确。
  • 检查状态跳转是否正常:在初始化过程中,经常使用状态机进行RTL编程。
  • 检查读写数据是否正常:可以设计一些“假数据”,例如人为规定的一些有规律的数据,检查这些数据在从设备中的地址是否正常,数据是否正确。
  • 检查执行操作的触发信号:检查信号的Trigger是否正常工作。


总之,RTL调试是最枯燥的部分,很多时候需要“抽丝剥茧”、“追本溯源”才能找到问题所在。但是笔者认为这恰恰是体现一个FPGA工程师硬实力的必要技能和心境。

Vivado ILA IP 的使用

ILA,全称Integrated Logic Analyzer,是Xilinx FPGA芯片中设计的芯片内部集成逻辑分析仪。它可以在一定程度上替代外部的传统逻辑分析仪的作用。ILA通常和VIO(Vritual Input/Output)结合使用,VIO不仅可以实时监控内部的逻辑信号和端口信号,还可以充当模拟输入驱动内部端口。ILA监控内部信号输出给PC端,而VIO接收PC端的实时指令从而给内部端口提供输入信号。

ILA调试有多种方法,可以直接在代码中通过原语添加,也可以在原理图中通过Debug添加,也可以在网络列表Netlist中添加。

在这里先创建一个示例工程,使用一个呼吸灯模块作为顶层代码:

module Breath_LED (
    input sys_clk,
    input sys_rst_n,
    output reg led
);  
    parameter CNT_2US_MAX = 7'd100;
    parameter CNT_2MS_MAX = 10'd1000;
    parameter CNT_2S_MAX = 10'd1000;

    reg [6:0] cnt_2us;  // sys_clk = 50MHz, T = 20ns, cnt_2us: 0 ~ 99
    reg [9:0] cnt_2ms;
    reg [9:0] cnt_2s;  // cnt_2ms, cnt_2s: 0 ~ 999
    reg inc_dec_flag;   // 0: increase, 1: decrease

    // count to 2us 
    always @(posedge sys_clk or negedge sys_rst_n) begin
        if (!sys_rst_n)
            cnt_2us <= 7'd0;
        else if (cnt_2us == (CNT_2US_MAX - 7'd1))
            cnt_2us <= 7'd0;
        else 
            cnt_2us <= cnt_2us + 7'd1;
    end

    // count to 2ms by cnt_2us
    always @(posedge sys_clk or negedge sys_rst_n) begin
        if (!sys_rst_n)
            cnt_2ms <= 10'd0;
        else if ((cnt_2us == (CNT_2US_MAX - 7'd1)) && 
            (cnt_2ms == (CNT_2MS_MAX - 10'd1)))
            cnt_2ms <= 10'd0;
        else if (cnt_2us == (CNT_2US_MAX - 7'd1))
            cnt_2ms <= cnt_2ms + 10'd1;
    end

    // count to 2s by cnt_2ms
    always @(posedge sys_clk or negedge sys_rst_n) begin
        if (!sys_rst_n)
            cnt_2s <= 10'd0;
        else if ((cnt_2us == (CNT_2US_MAX - 7'd1)) && 
            (cnt_2ms == (CNT_2MS_MAX - 10'd1)) && 
            (cnt_2s == (CNT_2S_MAX - 10'd1)))
            cnt_2s <= 10'd0;
        else if ((cnt_2us == (CNT_2US_MAX - 7'd1)) && 
            (cnt_2ms == (CNT_2MS_MAX - 10'd1)))
            cnt_2s <= cnt_2s + 10'd1;
    end

    // inc_dec_flag
    always @(posedge sys_clk or negedge sys_rst_n) begin
        if (!sys_rst_n)
            inc_dec_flag <= 1'b0;
        else if ((cnt_2us == (CNT_2US_MAX - 7'd1)) && 
            (cnt_2ms == (CNT_2MS_MAX - 10'd1)) && 
            (cnt_2s == (CNT_2S_MAX - 10'd1)))
            inc_dec_flag <= ~inc_dec_flag;
        else 
            inc_dec_flag <= inc_dec_flag;
    end

    // led PWM configuration
    always @(posedge sys_clk or negedge sys_rst_n) begin
        if (!sys_rst_n)
            led <= 1'b0;
        else if ((inc_dec_flag == 1'b0) && (cnt_2ms <= cnt_2s)) // increase
            led <= 1'b1;
        else if ((inc_dec_flag == 1'b1) && (cnt_2ms >= cnt_2s)) // decrease
            led <= 1'b1;
        else 
            led <= 1'b0;
    end

endmodule

IP核配置

在IP Catalog中搜索ILA即可找到ILA IP核,双击之后就会跳出配置界面。

设置部件名称、探针数量(需要测量信号的数量)和采样深度。clk信号一般是系统的时钟信号,ILA IP会在每个clk上升沿(或下降沿、具体有待考量)采一次对应信号的数据,直到采信号的次数达到采样深度为止。

在这里,假设代码出现了问题,我们需要检测sys_clkledsys_rst_ncnt_2uscnt_2ms四个信号。四个信号的宽度分别为1,1,7,10。

完成设置后,点击OK,出现下面界面。如果综合选项选择Global,代码会在每次综合时都对ILA进行综合;如果选择Out of conext per IP(OOC模式),代码只会在ILA设置发生改变时对ILA进行综合。一般选择后者即可,可以加快综合速度。

IP例化

首先在ila_0.veo文件中找到例化模板。将其复制到工程中需要的模块。在本例中,复制到顶层文件Breath_LED.v。(_所有的IP核例化都可以使用这个方法来获取例化模板_)

例化并连接相应的信号:

添加约束文件,生成Bit流文件,烧录进开发板。

波形分析

烧录完成后,可以看到板子上的LED灯已经开始呼吸了。此时Vivado会自动弹出ILA波形调试界面:

通过采样即可看到波形。在实际开发过程中,很有可能遇到在一次采样过程中关心的值没有变化的情况。这时一般需要连续采样,或者多采样几次,因为触发一次采样很肯发生在FPGA的Bank预充电期间、或者外部器件初始化期间。多采样几次,即使是设计Bug也要努力找到其中的规律。

手动采样只能显示部分波形,显示的时间很短。但是很多时候我们关心的信号出现的时间很短,并不能保证每一次触发都能找到需要的信号或者需要的值。这时候就可以在右下角的窗口设置触发条件。点击单次采样后,ILA会尝试帮助我们抓取特定的信号状态。如果没有抓到,抓取过程不会结束。

那么如果面对一个实际的、满是Bug的复杂工程,应该怎么添加触发信号呢?应该从输出开始,检查该模块的输出有没有问题。如果没有问题,那么就找到输出的上一级,即输出由哪个信号产生,查看产生机制是否出了问题。如果输出的触发信号或者计数器有问题,则再检查该触发信号或者计数器如何产生信号,是否存在Bug……就这样一路倒追上去,抽丝剥茧,大多数情况下都可以保证找到Bug,使功能正常。

添加ILA IP和标记Debug信号后,约束文件中会自动生成对应的内容。不要删除这些有关Debug的约束,否则会导致报错。

在原理图中Debug信号

经过博主测试,这种方法并不好用,偶可能会遇到Vivado报错或者找不到Debug的ltx文件的问题。不推荐使用这种方法。

通过原理图的方式对信号进行Debug实际上和添加ILA IP的效果是相同的,二者的区别仅仅在于对信号进行Debug的操作不同。在原理图中添加Debug信号不需要创建和例化IP核,在对代码进行综合后直接打开原理图窗口,找到Debug视图。

也可以在Set Up Debug向导中添加Debug信号。

可以直接在网表Netlist中将信号标记为Debug信号。但是需要注意:对于输入输出信号,不能将原来的信号标记为Debug信号,一定要将该信号对应的IBUF或者OBUF标记为Debug信号,否则Vivado可能直接报错

如果要添加的信号没有对应的IBUF或者OBUF,例如需要将内部的一个计数器标记为Debug信号,在网表中标记后,还需要在源码中的信号定义处对信号进行标记,否则Vivado可能将该信号在综合中优化(消除),因为这个信号没有通过IBUF和OBUF,即使这个信号是内部信号。添加标记后,Vivado就不会将该信号在综合时优化了,而是会作保留。

在Set Up Debug向导中设置时钟域,即触发ILA采样的时钟。

点击Next,设置采样深度和逻辑等级。

之后的操作和使用IP核调试没有区别。