作者: 阵列,文章来源: 阵列微信公众号
FPGA程序的调试,尤其是大型程序,一直都是耗时耗力的工作。首先是因为HDL语言沿空间并发扩展的特性不同于一般基于按时间线性叙事的计算机语言,各个元素之间的逻辑关系更加紧密,不易理解和思考,debug手段也不多。同时,FPGA程序的综合布线过程都比较缓慢,经常一个中等大小的程序会需要等待数十分钟才能得到输出文件,大型设计run隔夜是常见的事情,随便一个笔误都会带来很高的沉没成本。每次说到这里,都不由得回忆起多年以前某次出差做实验,邂逅过一位研究所大哥玩笑说最爱领导指派FPGA任务,因为只要按下综合按钮,便能安心歇息半个午后。那时候,天地初开,摸鱼这个词还没有发明,也没有想到即便是当下最强最fancy的CPU也没能拯救这种等待。
言归正传。在逻辑构思基本正确,程序编写大致规范的前提下,大多数的问题,都只是写代码时的一些小疏忽。通过实施功能仿真,其实可以把多数逻辑问题都找出来。但是,实际工作中,经常没有时间或耐心做仿真,而是直接编码后下硬件调试了。此时,通过仔细阅读综合器给出的警告信息,就可以在最耗时的布局布线之前把各种小问题找出来并修正,从而有效地提高开发效率。在调试阶段,如果遇到了难以理解的现象,回过头去分析警告信息也是一种有效的办法。在本文中,我们以vivado自带综合器为例、以verilog为编程语言,看看如何理解和利用警告信息排除代码中的小bug。
(以上图片来自网络: My digital designing diary by Mandapati)
为了方便叙述,我们先建一个样例工程,包含模块top和adder。为了避免像很多经典教科书中的 “a=(b++)+(++c)” 那样被指为“例子代码不注重软件工程”,特意把这个程序写的尽量贴近工程实际一些(除了没有注释)。首先它具有特定的功能,对两路并发输入的数据流先相加再累加。其次,数据端口定义采用时下常用的AXI-Stream风格。
模块top是顶层模块,其源码Top.v如下图。该模块具备时钟信号clk和异步复位信号rst。输入数据端口din_tdata[31:0],配套流控握手信号din_tvalid和din_tready。在模块内部,输入数据首先被劈成2个16bit数据,代表要相加的两路数据流,并注入加法器模块adder。加法器的输出数据是adder_out[15:0]。第31行开始的always语句则完成对加法结果的累加操作。累加结果从端口acc_tdata输出,配套数据有效标志acc_tvalid。
再看看加法器的源码Adder.v,如下图。两路数据din1和din2,共享同一组流控握手信号din_tvalid和din_tready。加法结果从端口dout输出。在模块内部,第14行的always语句负责完成所有逻辑处理。核心语句在第21行:当输入数据有效而且后级设备准备好时,进行一次加法操作。写到这里作者也是十分感慨,三十多行代码就是为了伺候第23行的这个“+”号。
至此,例子程序搭建好了。程序比较小,按下Run Synthesis按钮等待半分钟左右就能看到综合结果。下面我们一起一边修改代码一边看看常见的警告信息都有哪些。
1. 常数驱动
警告之所以是警告,是因为综合器分不清它是否真的有问题。很多警告是可以忽略的。例如上述例程,看上去很完美,但综合完成后仍然会得到如下的警告信息:
这里,综合器提醒我们,top模块的端口din_tready被驱动为常数1,可能是一个潜在的问题。在top的逻辑中,din_tready的功能是提醒模块外部的前级数据源“是否准备好接收数据”。被驱动为1则表示“永远都准备好接收数据”。用顺藤摸瓜的方法分析代码,可以看到din_tready是被加法器实例adder1驱动的,而在加法器内部(adder.v的第31行),该信号来自于加法器输出端的tready。再看回top.v的第17行,果然加法器的tready被置为常数1,根源在此。具体到这个例子,此逻辑本身没有大问题,因为top模块的输入端口只有数据有效信号acc_tvalid,并没有配套的tready,说明该端口是强行输出的,并不考虑后级没有准备好的情况。
所以,在此例中,这个常数驱动警告“基本上”可以被忽略。但是,问题禁不住细琢磨,比如此例程并没有考虑在复位信号rst有效期间din_tready应当拉低来禁止数据输入,不太周全。进一步的,我们还能联想到,在真实的系统中,这个接口定义是否存在隐患? 即,后级模块是否真的可以无条件接收数据? 这些都是警告信息带来的福利。
2. 无用信号
下面开始折腾代码。首先把top.v line27处的端口连接去掉,只留下空括号。这样,adder1实例的dout_tvalid输出就悬空了。
综合之后,得到如下图的警告。综合器告知adder1中的dout_tvalid所对应的寄存器资源被移除。
显然,这是由于在top中断开了信号连接,于是dout_tvalid信号在adder内部虽然被赋值,但是在整个逻辑中没有被任何其它地方使用,也没有输出,于是综合器在给出警告后就将其删除了。从这个例子可以看到,如果一个信号被自动移除了,应当首先应当考虑它是否没有在别处被用到。不过,在下一个例子里马上可以看到这并不是信号被优化掉的唯一的原因。
3. 无源信号
首先,先把源码复原,然后试着把Top.v第17行注释掉:
综合之后,得到如下警告信息:
第一条信息直奔主题:adder_tready信号没有被驱动。这显然是前述修改带来的,源程序里缺乏对adder_tready的赋值操作。第二条以及随后更多的信息则会让人困惑:adder1/dout[15:0]被从逻辑中移除了。这些信号明明都有被后续的累加操作用到,为什么还会被优化掉? 通过分析adder中的逻辑关系可以知道,这仍然是因为adder_tready没有被驱动,于是综合器认为凡是依赖于adder_tready的后续信号都已经没有存在的意义,于是一股脑全拿掉了。这就提示我们,如果发现有大片的逻辑消失了,不但要往后寻找看是否缺乏最终的输出,而且要往前寻找看是否存在不确定或者无驱动的输入。当然,对于各种异常情况,不同的综合器以及同一个综合器的不同的参数,会表现很大的差异。比如作者也见过有的综合器会直接给无驱动信号赋值为0,这种好心好意的掩饰反而导致有时候问题很难查找。
4. 多重驱动
在top.v的第16行,把原先的adder_d2改成adder_d1,形成一个典型的笔误。本来是要分别给信号addr_d1和addr_d2赋值,一不小心变成了给信号adder_d1赋值两次。
对于上述情况,综合器明确指出了有信号被multi-driven了,如下图。
但是,它指出的对象却并不是addr_d2,而是我数据源din_tdata。这是因为,在综合器看来,din_tdata[15:0]和din_tdata[31:16]都连接到了addr_d2[15:0],其实就是din_tdata[15:0]与din_tdata[31:16]直接点对点短接了,所以它们本身就面临多驱动问题,addr_d2此时只是一个“别名”而已。好比你出门忘记戴帽子,而综合器告诉你:请注意冷风已经接触头皮。这种机器式的叙述风格,有时候着实会带来一些小麻烦,不过习惯了就好了。
5. 复位缺失
top.v第31行的always语句采用了异步复位。复位信号rst与时钟clk一起作为语句触发条件,在语句内部先按判断rst是否为真来选择执行复位操作。这是verilog典型的异步复位语句写法。这里,尝试把第36行注释掉,如下图。
综合器会给出如下的警告。
字面背后的意思可以理解为:语句中存在复位语段,但是并没有对acc_tvalid信号做复位操作,导致逻辑缺失,或者说综合器分不清应该set还是reset,于是担心综合结果会与仿真结果不符。
这类警告可以帮助我们找出因为忘了写复位而初始值不确定的寄存器,这往往是很多重大bug的来源。如果存在某些寄存器的确不需要复位操作,则应当单独写一个只有clk做触发的always句段,就能避免上述警告。
那么,此时综合结果有没有生成期望的逻辑呢? 打开综合输出的逻辑图(如下),可以看到acc_tvalid由一个没有复位和置位的D触发器驱动,符合修改后的语句原意。然而,我们仍然应该设法避免这类不太规范的写法。尤其是对于新手,务必要了解语言与真实逻辑的映射关系,谨记verilog就那么几种常见的语句套路。新奇的写法,可能导致完全不可预期的综合结果。
6. 位宽失配
修改adder模块的端口声明,如下图,把din1和din2的位宽从16bit分别改为17和15。
如下图,综合器会明确指出在top.v中实现adder模块时遇到了端口宽度不匹配的问题。
需要指出的是,至少对于vivado + verilog,位宽失配警告只对模块端口连接有用。如果是两个位宽不同的信号赋值,综合器将会直接做高位截断或者高位补零,而不给任何警告,除非截断操作触发了无用信号警告。所以,不论是wire类型还是reg类型,赋值时的位宽对齐问题,完全需要编程者自行关注。例如下面的语段,16bit的src被赋值给16bit的dst1和15bit的dst2,显然赋值给dst2时最高位会丢失,但是此时综合器不会给出警告,这是verilog语言本身的特点,改不了。而且,因为dst1用到了src的所有bit位,所以在综合器看来src里也不存在无用的bit位,也不会触发无用信号警告。最终结果就是,可能你就是笔误给dst2少写了1位,但这个错误要到后期调试时通过各种故障才被发现。这里并不是综合器犯懒,而是verilog语言本身就是这样设计的,相比之下VHDL就要严格的多,不同位宽信号互相赋值不给警告,而是直接报错。
7. 不应有的锁存器
把top.v中对adder_d1和adder_d2的直接赋值语句改为always句段,如下图:
上述修改将产生如下图的警告信息:adder_d1和adder_d2变量引入了锁存器(latch)。
分析上述语句,可以看到din_tvalid的确相当于锁存使能信号,当它为1时din_tdata可穿透到adder_d1和adder_d2。如果打开schematic观察综合结果,会发现此处使用了一个名为LDCE的锁存器元件。
我们知道,FPGA公认的基础逻辑资源是查找表和D触发器,是否具备锁存器要看具体的FPGA型号和综合器的算法,所以在HDL语言中书写锁存器风格的语句并不是好办法,也是这条警告存在的意义。
至此,我们简单介绍了在综合阶段常见的一些警告问题。当然,在后续的implementation操作中,还会有很多更难理解的提示和警告出现,它们更加地与具体器件的内部结构和元素有关。到了这些阶段,更加需要去关注XDC文件、关注物理和时序约束,而不是HDL语言本身。