FPGA调试本身就是挺辛苦的一件事情,尤其是在刚开始调试FPGA的时候,无论培训的时候如何强调一些注意事项,如跨时钟域问题,如接口问题,以及RAM读写冲突问题,但一旦做起项目来,每每还是有同学必须要亲自往这些坑里面跳一次才真正懂得这些BUG的含义。如双口RAM在功能仿真时没有出现问题,但上板调试过程中运行很久才偶尔出现一次BUG,这时就需要花费大量的时间去追溯问题的源头,最后花一周甚至更长的时间才能找到是双口RAM读写冲突的问题,时间早早的就浪费掉了。事实上,上面说跨时钟域或者双口RAM读写冲突的这些问题是可以通过时序仿真仿真出来的。
FPGA验证在芯片设计流程中具有重要的作用,有时候为了找到某些BUG,不得不对FPGA综合出来的网表进行后仿真。后仿真又叫时序仿真,跟课程前面介绍的对写出来的Verilog hdl设计代码和testbench代码建工程进行的功能仿真不同,时序仿真是把综合出来电路中的时延信息加入到仿真的过程中,模拟出跟更接近于在FPGA上真实运行的情况。本文以Quartus II软件为例进行介绍后仿真的步骤和流程。ISE或VIVADO流程类似或关联ModelSim后更自动化。
什么是功能仿真?什么是时序仿真
仿真过程是正确实现设计的关键环节,用来验证设计者的设计思想是否正确,及在设计实现过程中各种分布参数引入后,其设计的功能是否依然正确无误。仿真主要分为功能仿真和时序仿真。功能仿真是在设计输入后进行; 时序仿真是在逻辑综合后或布局布线后进行。
1. 功能仿真 ( 前仿真 )
功能仿真是指在一个设计中, 在设计实现前对所创建的逻辑进行的验证其功能是否正确的过程。 布局布线以前的仿真都称作功能仿真, 它包括综合前仿真( Pre-Synthesis Simulation )和综合后仿真( Post-Synthesis Simulation )。 综合前仿真主要针对原理框图的设计 ; 综合后仿真既适合原理图设计 , 也适合基于 HDL 语言的设计。
2. 时序仿真(后仿真)
时序仿真使用布局布线后器件给出的模块和连线的延时信息, 在最坏的情况下对电路的行为作出实际地估价。 时序仿真使用的仿真器和功能仿真使用的仿真器是相同的, 所需的流程和激励也是相同的; 惟一的差别是为时序仿真加载到仿真器的设计包括基于实际布局布线设计的最坏情况的布局布线延时, 并且在仿真结果波形图中,时序仿真后的信号加载了时延, 而功能仿真没有。
一、用Quartus II建立工程。
具体过程可参考如下链接:
https://jingyan.baidu.com/article/cbcede07ef59cf02f40b4ddb.html。建立工程后的文件如下图所示。我们以一个简单的8位计数器为例进行说明。
建立工程的过程中可以配置好综合之后要产生用来做后仿真的网表文件,如果没有提前配置,也可以建好工程后再配置,具体如下图,点击右键,选择settings.
settings打开后出来如下窗口,在左侧选择Simulation,在右侧EDA Netlist Writer settings里面选择门级网表产生的语言以及路径,之后确定即可。
如下图,点击编译按钮,开始进行综合。
综合后的界面如下。
找到刚设置的网表文件的输出目录,在该目录下要选择两个文件,一个是.vo的网表文件,另一个是.sdo的时延信息文件。
打开.vo的文件可以看到里面有如下图的三行,就是把时延信息文件.sdo反标到网表文件中。没有这一句或者时延信息反标不成功,是做不成后仿真的。
用ModelSim进行后仿真
把上述过程中产生的.vo和.sdo文件拷贝到要做后仿真的工程文件夹下建立仿真工程,除了测试文件采用前仿真的测试文件外,还要添加一些Quartus的库文件,主要有三个,220model.v,altera_mf.v和cyclone_atoms(这个文件需要根据进行综合时选择的FPGA的型号来定)。注意,此工程是不能添加原始设计代码的,而是用综合后的网表文件替代。建立工程后的截图如下。
时序仿真的工程
功能仿真的工程
从上面两图可以明显对比出,功能仿真和时序仿真工程中的代码仅有count.v(功能仿真)和count.vo(时序仿真)不同,其余都完全一样。
编译成功后开始仿真。
右键点击下图中tb_count选择信号到波形文件的窗口。
运行后的结果截图,可以看出计数器是按照时钟的上升沿在自加1跳变的。
选择局部,计数数据由7跳变到8的时刻进行放大,可以看到,7状态后不是立即跳变到8的,而是经过了很多个中间状态。同时开始跳变的时刻也不是时钟的上升沿到来后立即跳变的,而是延迟了一段时间才开始的。
再进行放大,可以看到7变为8中间还经历了11和9两个中间状态,同时观察下面每Bit信号的跳变,可以发现每1位信号的跳变时刻也是不同的,这是因为在FPGA内部,总线型信号布局布线后每bit的走的路径是不同的,连线引入的时延也是不同的,这也进一步证明了后仿真的时延信息是成功的反标到了网表文件中了。同时,这种总线型信号跳变有很多中间状态的特点,也是区别于前仿真或者叫功能仿真的一种最直接的标记。
我们回过头来对比一下功能仿真的波形,如下图,很明显的看出来在计数跳变的过程中,没有任何中间状态的改变。
动图介绍ModelSim建工程到仿真
使用Modelsim建立仿真环境进行仿真的操作步骤动图如下:
补充知识--前仿真和后仿真的概念
功能仿真和时序仿真常常又分别被叫做前仿真和后仿真。
前仿真和后仿真的区别:前仿真就是指综合前的仿真,也就是行为级的仿真,如你在Modelsim直接写代码的仿真。后仿真指的是综合后的仿真,也就是功能仿真。比如你在Modelsim中用VHDL写了个计数器,行为级得仿真通过了,你把它加到quartus中或者其他的综合工具进行综合,综合完后生成功能网表,它把行为语言变成寄存器传送级语言,这时候你把它加到Modelsim中仿真叫后仿真,后仿真成功后,你还要在quartus中进行映射,布局布线,完后进行时序分析,生成时序网表,描述器件里门或者布线的延时,最后把延时网表和功能网表一起加到Modelsim中仿真叫门级仿真。
门级仿真和时序仿真的区别:门级仿真是quartus生成的网表文件.vo。门级则不考虑互联延迟,二只考虑了器件的延迟。时序仿真是选择具体器件并布局布线后进行的包含定时关系的仿真,主要验证是否满足时间约束关系、延时、最大工作频率和消耗的资源等。时序仿真是需添加时延文件.sdo。
modelsim 是专门进行仿真的软件,可以分别进行前仿真和后仿真。前仿真也称为功能仿真,主旨在于验证电路的功能是否符合设计要求,其特点是不考虑电路门延迟与线延迟,主要是验证电路与理想情况是否一致。可综合FPGA代码是用RTL级代码语言描述的,其输入为RTL级代码与testbench。后仿真也称为时序仿真或者布局布线后仿真,是指电路已经映射到特定的工艺环境以后,综合考虑电路的路径延迟与门延迟的影响,验证电路能否在一定时序条件下满足设计构想的过程,是否存在时序违规。其输入文件为从布局布线结果抽象出来的门级网表、testbench和扩展为sdo或sdf的标准时延文件。sdo、sdf的标准时延文件不仅包含门延迟,还包括实际布线延迟,能较好地反映芯片的实际工作情况。一般来说后仿真是必选的,检查设计时序与实际的FPGA运行情况是否一致,确保设计的可靠性和稳定性。
在进行网表仿真时,经常会遇到一些不定态问题(红线的说法稍显不专业),通常一个模块中某个寄存器出现X状态,就会造成整个仿真都出现X状态,仿真无法进行。这时我们需要找出最先出现X态的逻辑,找出错误的原因并消除后才能重新进行仿真。
a. 首先应该查看是否所有的RAM都进行了初始化操作。虽然很多RAM在实现功能时并不需要初始化,但后仿时没有初始化的RAM读出X态,X会蔓延出去,造成整个系统都出现X态。可采用verilog中系统任务readmemh对RAM进行初始化操作。
b. 异步时钟域信号赋值很可能会导致后仿出现红线,但这并不能说明系统错误,通常我们让仿真平台不去检查异步时钟域赋值导致的时序违例,从而消除这部分红线。
c. DUT与外部testbench模型接口时序不匹配导致,系统采进来数据错误导致红线。比如以太网的GMII接口,最开始建立ephy模型时是在RTL仿真环境下描述的,在Rx_clk上升沿发出Rxd数据;但在做后仿真时由于时延等的影响,按上升沿进来的数据就会出现时序违例。这时就需要我们修改ephy模型,让数据Rxd在上升沿后延迟一段时间再送出,使GMII接口满足时序要求。
d. 组合逻辑路径过长导致的时序为例。如下图所示是读取RAM的时序波形,读地址ADR,读使能ME,时钟CLK,当时钟上升沿到来时,应该读取ADR为0x40地址的数据,但是由于两个寄存器之间的组合逻辑时延很大,在读RAM的上升沿到来时RAM地址没有保持稳定,出现时序为例,导致输出数据线是X态。如果是在代码设计时,我们应该尽量避免这种情况的发生,减小组合逻辑的复杂度以减小时延。
后仿中时序问题
加快后仿真的方法
在做后仿时,跑1ms的数据通常是前仿的几十倍,甚至几百倍,所以如果要把前仿的所有测试例都跑完通常几天几夜也不一定能走完所有流程,而且仿真过程中还可能经常遇到一些问题,阻止仿真进行下去。如果能够加快仿真的速度,将会大大提高我们的工作效率。
a. 通常测试过程中,我们会把互相不影响的测试例分开运行,这样既可以同时运行多个测试例,又可以在某一个测试例发生错误时,减少重新测试需要的时间。
b. 在运行某一个测试例时,如果这个测试例需要运行的模块可以完全独立于其它模块,那么我们就可以利用force语句将其它模块的时钟强制拉低,这样其它模块就不会运行。例如在测试CPU的外设时,并不会对设计模块部分进行任何操作,如果把设计模块的时钟拉低,会大大缩短外设测试的时间。但在做后仿时force语句应慎用,如果把设计模块里的时钟拉低,跟这一模块相连的时钟信号也可能被拉低,造成需要测试的部分无法运行,只有把测试部分模块的时钟重新释放(release)掉才能正常运行。
c. 有些需要CPU初始化的RAM,如流控、队列门限等RAM,在CPU写RAM的过程需要大量的时间,如果RAM读写测试已经通过,没有必要再通过CPU写RAM,可以利用上面RAM初始化文件的方法,把需要配置的值写进RAM中,可以大大缩短运行时间。
应用实战
双口RAM的读写冲突问题在FPGA调试中经常遇到,并且,往往是那种费了好大劲追信号追到吐血后才确认到的问题。在初学FPGA调试中,常常为了所谓的省事,在写代码设计仿真阶段就忽略了双口RAM的读写冲突问题(读和写对同一个地址同时进行操作,功能仿真时可能没有出现,但因为有时延的原因,在时序仿真时就出现了),导致在FPGA上板调试中浪费大量的时间。在进行FPGA工程上板调试前,如果能够进行双口RAM的后仿真,就可以避免后期无穷尽的追信号最后定位到双口RAM读写冲突上了。希望能够给大家提个醒,内容虽然简单,但的确是不容忽视的一个隐藏很深的大问题。
FPGA 内部块RAM 的读时序如下图:
可知,块RAM的读延时为两个时钟周期。
FPGA 内部块RAM 的写时序如下图:
可知,块RAM 的写延时为0,但是RAM 中的内容是在写的下一个时钟改变。
在进行代码设计时应该尽量避免对一块RAM的同一个地址同时进行读写操作,防止读写冲突时读出数据是X态。
对于单端口RAM不能对同一RAM进行读写,对于双端口RAM可以从两个端口对同一地址进行读操作,但不能同时进行写操作也不能同时进行读写操作。
Ø 问题:
队列长度信息RAM a b口读写异常,更新出错。
Ø 现象:
端口卡死,某队列长度达到最大门限,但是发送调度显示队列为空,新数据帧入队申请,不满足门限要求而丢弃,输出没有调度结果,也不能出队操作;
Ø 分析定位:
根据现象中停止发送,对队列长度信息更新相关信号进行Debug测试,定位出问题的根源位置如图a所示,该队里进行入队操作后,队列长度信息被入队调度通过队列信息RAM a口更新写入长度13,此时出队操作正在执行,在获取队列长度信时,在a口刚写入后的一个clk,读取得到队列长度信息为12,再经过1个clk,数据稳定在13;但是异常数据12被出队操作获取到并用于出队号队列长度的更新,队列长度更新出错,若干次操作后,当最后一帧出队完成后,会将队列长度更新为负数,如图b所示,FPGA中不操作负数,即二进制中很大的正数,远大于队列最大门限,后面再进行入队操作时,入队操作不满足门限要求,无法入队操作,同时出队操作认为队列为空,不会调度该队列出队操作,进入卡死状态;队列长度13的二进制表示为1101’b,12的二进制表示为1100’b,说明出现了单bit翻转错误的问题。
图a 双口RAM a b口读写异常
图b 队列长度信息更新出现负数
Ø 解决方法:
首先考虑对RAM输出加寄存操作,但是这样会整体引入操作时延,即使入队和出队操作不是同一队列,每次在RAM读数据情况下都需要多等一个clk,为了解决该问题,采用的方法是在出队操作需要读队列长度时,如果前一个时刻a口刚更新该队列,此时a口晚一个clk再用数据,保证第二个clk读出的数据是a口写入稳定输出的,其他情况下不引入时延。
源码
1、tb_count.v
`timescale 1ns/100ps
module tb_count;
reg clk;
reg reset;
reg d;
wire [7:0] q;
initial
begin
clk=1"b0;
reset = 1"b0;
d = 1"b0;
#10 reset = 1"b1;
#1 d = 1"b1;
#10 reset = 1"b0;
#34 d = 1"b0;
#1000 $stop;
end
always #10
clk = ~clk;
counter u_counter(
.clk(clk),
.reset(reset),
.d(d),
.q(q)
);
endmodule
2、counter.v
`timescale 1ns/100ps
module counter(
clk,
reset,
d,
q );
input clk;
input d;
input reset;
output [7:0] q;
reg [7:0] q;
reg [7:0] q_ff1;
reg [7:0] q_ff2;
reg d_ff1;
reg d_ff2;
wire d_pos;
wire d_neg;
always @(posedge clk or posedge reset)
begin
if(reset == 1"b1)
d_ff1<= 1"b0;
else
d_ff1<=#1 d;
end
always @(posedge clk or posedge reset)
begin
if(reset == 1"b1)
d_ff2<= 1"b0;
else
d_ff2<=#1 d_ff1;
end
assign d_pos = d & (~d_ff1);
assign d_neg = ~d & d_ff1;
always @(posedge clk or posedge reset)
begin
if(reset == 1"b1)
q<= 8"b0;
else
q<= q + 1"b1;
end
always @(p