加载中…
个人资料
  • 博客等级:
  • 博客积分:
  • 博客访问:
  • 关注人气:
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

对Verilog仿真过程的理解

(2021-02-21 22:04:07)
分类: FGPA时序
前言
顺序执行的仿真器如何仿真并行的Verilog语言?仿真器中Verilog的并行性是通过其语义(Verilog的语言含义)仿真出来的,Verilog语言的语义是专门为仿真定义的。如果设计的Verilog源代码不符合Verilog仿真语义,对Verilog代码的仿真可能出现仿真歧义,也就是代码仿真结果会与综合后时间的门级网表功能不一致。

一、概念理解:
1、仿真时间
  • 仿真时间是指由仿真器维护的时间值,用来对仿真电路所用的真实时间进行建模。仿真时间由硬件电路的延时参数决定,也是硬件电路的实际工作时间,所有仿真工作都是严格按仿真时间向前推进的,在什么时间执行什么操作。仿真时间与仿真软件在计算机上的运行时间没有任何关系。
  • 如果在当前仿真时间有多个事件需要执行,那么首先要根据它们的优先级(在事件队列中的优先级)来判定谁先谁后。若优先级相同,则执行顺序随机(不同仿真器的行为可能不同)
2、事件驱动
  • 功能仿真是一种事件驱动类型的仿真。Verilog语言用来对数字系统的功能和时序进行建模,模型的仿真过程是围绕时间来组织的。
  • 事件是指在特定时刻,模型中值的变化。一个更新事件被执行后,所有对改事件敏感的进程都将以随机的顺序计算。比如,在被仿真电路中,线网或寄存器的值的任何改变被认为是一个更新事件。
  • 进程对更新事件敏感,进程(门或行为模型)的计算也是一个事件,叫做计算事件。计算事件和更新事件之间循环往复的互相触发,推动了仿真时间向前推进
3、进程
  • 进程是Verilog语言中的独立执行单元,用Verilog描述的数字系统正式由一个个进程组成的。
  • 进程包括原语、模块(module)、initial过程块、always过程块、连续赋值语句、异步任务、过程赋值语句等。
  • 进程可以被激活或被挂起。仿真器 总是在处理被激活的一个进程,而其他的所有进程则处在挂起状态。
  • 在仿真时,所有进程都是并行执行的,它们之间的顺序随机,仿真工具可根据自身原则安排它们的执行顺序。比如在下面例子中,0仿真时刻的所有进程都是按它们在代码中出现的先后顺序执行的。但仿真器的这种行为并不违背Verilog语言的并行性,因为在执行完所有进程前,仿真时间是不会向前推进的,仿真时刻仍然是0时刻
  • 在执行一个进程时,如果遇到一个事件语句(“@”)延时预计(“#”)、其表达式为FALSE的等待语句(wait),则进程将被挂起,直到发生该事件已经过延时中的时间单位数等待语句表达式变为真时,进程才会重新被激活
  • 在Verilog语言中,进程之间是并行独立执行的,但它们在执行的过程中右交织在一起。一个进程被挂起,另一个进程被激活;改进程被挂起,另一个进程又开始执行,这就是Verilog语言在仿真时的一大特点。
  • Verilog也会出现进程锁死的现象。比如若将下面代码中forever语句改为“forever sclk_i = ~sclk_i;”仿真时间会一直停留在0仿真时刻而不会向前推进。这是由于该initial进程已经被锁死,永远在执行“sclk_i = ~sclk_i;”语句。该进程永远不会挂起,其他进程就永远无法执行,仿真时刻就无法向前推进。
4、调度
  • 调度实际上就是安排事件的执行顺序。
  • 在Verilog中,同一仿真时刻也许有很多事件需要执行,不同的事件之间有一个先后的优先级关系,事件的执行也会产生新的当前时刻事件和将来时刻事件,这都需要被合理调度。
5、时序控制
  • 在Verilog仿真时,主要通过3种方法来进行时序控制:事件语句(“@”)、延时语句(“#”)、等待语句。
  • 时序控制总是伴随着进程的激活和挂起。
6、进程、事件和仿真时间的关系
  • 事件会在不同的时间发生。
  • Verilog是一种并行语言,允许描述多个同时发生的动作。但执行这些操作时要求将它们序列化,因为计算机不同于所建模的硬件,它不是并行的。
7、Verilog语言的不确定性
  • 在Verilog仿真时,有2个不确定的行为来源:一个是0时刻的任意执行顺序;一个是进程之间语句的随机交叉。
  • 仿真器执行被调度到同一时刻的一组事件时,也许需要几个仿真周期来完成,因为一个事件可能产生本时刻的其他事件。这里所说的执行同一时刻的事件,是指在长度为0的时间内执行他们并不是说执行这些事件不需要时间而是说所有的事件的发生并不会促使仿真时间向前推进。这种0长度时间内事件的任意执行顺序是仿真语言不确定性的根源。
二、时序模型与延时
1、仿真时序模型
(1)仿真模型
  • 在Verilog语言中,将数字硬件的模型称为仿真模型。比如module块与其中的always语句块都可被称作仿真模型。
  • 时序模型是仿真器的时间推进模型,反映了推进仿真时间和调度事件的方式。时序模型分为门级时序模型、过程时序模型2种。
(2)2个概念:敏感表、扇出表。
  • 敏感表:是仿真模型的输入表,由接收新值的元素组成,它告诉我们当输入发生变化时,哪些输入的变化会导致仿真模型的执行。
  • 扇出表:由将产生新值的元素组成,它告诉我们当一个事件发生时需要计算哪些元素。
(3)门级时序模型
  • Verilog门级时序模型主要用于分析连续赋值语句、过程连续赋值语句、门级原语、用户自定义原语等。
  • 改模型特点是,任意时刻、任意输入发生变化,门示例模型都将重新计算其输出。所有仿真模型都对输入变化敏感,而这种变换右会导致仿真模型的执行。
  • Verilog的门级模型具有惯性延时的特性,门级时序模型非常精确的模拟了电路中的惯性延时特性
  • 一般来说,门级时序模型对应于组合逻辑建模。
(4)过程时序模型
  • Verilog中过程时序模型不同于门级时序模型,它不是对任何输入的变化都敏感,它的敏感依赖于控制的上下文。一般来说,initial、always语句只对输入的一个子集敏感。比如,always语句只对@符号后面括号中的变量敏感。
  • 一般来说,过程时序模型对应时序逻辑建模。
(5)示例
对于语句:assign #8000000 sclk_dely = sclk; 时间单位`timescale为1ns,sclk信号是周期为488ns的时钟信号(远小于8ms的延时值)。原意是将sclk信号延时8ms赋值个sclk_dely,但仿真的结果是sclk_dely信号一直未发生变化仅延时值小于244ns时(assign #235 sclk_dely = sclk; ),sclk_dely信号才会正常变化
  • 该语法中,无论sclk信号何时发生变化,都会立即执行一个计算事件计算等号右边的表达式,同时产生一个更新事件(把计算出的值赋给sclk_dely),但该更新事件并未立即执行,而是被调度到当前仿真时间之后8ms再执行。
  • 根据门级时序模型的特征,在上一更新事件执行前还会有新的更新事件产生。因为sclk的周期为488ns,每个244ns会变化一次。这样,在每个244ns以后,Verilog调度器就会撤销先前已调度的事件,而调度新的事件。因此更新事件被不断产生,右不断被撤销,最后导致没有一条更新事件被真正执行,因此出现sclk_dely一直未初始化的现象。
2、在Verilog语言中增加延时
(1)电路的2种延时
  • 惯性延时:由输入变化过快(小于门本身延时值),而导致输出无响应的特性,称为电路的惯性延时。也就是说电路存在一定的惯性,或者说是惰性。例如在与非门电路中,门延时为5ns,因此任何小于这个延时值的输入变化都不会对输出造成影响。如:语句assign #5 B = ~A;模拟了电路的惯性延时,A信号任何小于5ns的变化都将被过滤,而不会反映到B信号的变化中。
  • 传导延时:信号A经过5ns的传输线到达另一端B,信号A的任何变化都将在5ns后体现在B端,这种延时被称为传导延时
(2)在阻塞赋值语句中增加延时(不推荐)
  • 在阻塞赋值中增加延时,不能模拟任何电路中的延时类型(惯性延时和传导延时),因此不推荐使用。
(3)在非阻塞赋值语句中增加延时(只在“<=”右边表达式中有意义)
1
2
3
always @(a or b) begin
    sum <= #5 a+b;
end
  • 比如上面语句可用于传导延时模型建模。当某个T时刻a发生变化,导致always语句开始执行。执行“sum <= #5 a+b;”时,首先计算a+b的值,然后将相加结果赋值给sum的更新事件调度到T+5ns之后执行。此后,任何a、b的变化都将导致always语句的执行,也就是说a和b上的任何变化都不会被忽略,而总是在5ns以后体现在sum上
  • 在非阻塞赋值语句的右表达式中增加延时,可以精确的模拟电路中的传导延时。
(4)在连续赋值语句中增加延时
  • 在连续赋值语句中,只有一种延时是合法的:assign #5 B = ~A;,A信号上的任何小于5ns的变化都将被过滤,不会体现到B的变化上这种延时精确的模拟了电路中的惯性延时。
三、如何提高代码仿真效率
  • 代码仿真精度越高,仿真效率越低;
  • 减少层次结构;
  • 进程越少,放着效率越高;
  • 减少啊门级原语使用,尽量采用行为描述;
  • 尽量使用case语句,而不是if...else语句;
  • 减少begin...end语句块的使用
  • 减少仿真输出显示。
四、防止仿真和综合结果不一致
  • 不完全敏感列表:
  • case语句:
  • 初始化:
  • 代码中的延时参数:
五、以如下代码示例对Verilog仿真过程进行理解
1、源代码如下
INV_DFF.v
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module INV_DFF(
    
input   wire    sclk,
    
input   wire    rst_n,
    
input   wire    DataIn,
    
output  reg     DataOut
);
wire DataInv;

always @(posedge sclk or negedge rst_n) begin
    
if(1'b0 == rst_n)
        DataOut <= 
1'b0;
    
else
        DataOut <= DataInv;
end 

assign #3 DataInv ~DataIn;

endmodule
TestBench模块:
TB_Adder.v
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
`timescale 1ns/100ps
module TB_Adder();
reg sclk_i, rst_n_i, Din_i;
wire Dout_i;

//时钟产生
initial begin 
    sclk_i 
0;
    
forever
        #10 sclk_i ~sclk_i;
end

//复位操作
initial begin
    rst_n_i 
1;
    #5 rst_n_i 
0;
    #55 rst_n_i 
1;
end 

//产生输入
initial begin
    Din_i 
0;
    #80 Din_i 
1;
    #40 Din_i 
0;
end 

//实例化模块
INV_DFF INV_DFF_inst(
    .sclk(sclk_i),
    .rst_n(rst_n_i),
    .DataIn(Din_i),
    .DataOut(Dout_i)
);

endmodule
2、仿真时序和波形如下
对Verilog仿真过程的理解
3、下面针对以上代码,对各个仿真时刻进行分析:
(1)0仿真时刻:
  • 在顶层仿真TB中,4个进程同时在0时刻执行,包括3个initial进程和一个INV_DFF实例化进程INV_DFF_inst;
  • 同时执行的进程其执行顺序是不固定的,跟其在代码中出现的顺序无关,但某些仿真器将它们在代码中出现的顺序作为执行顺序;
  • 首先执行时钟产生语句中的“sclk_i = 0;”,然后执行“forever #10”语句,当仿真器遇到“#10”语句时将该进程挂起,转而执行其他进程。这是由于后面语句是在10ns仿真时刻所需执行的语句,而当前的仿真时刻还是0,没有向前推进;
  • 然后执行复位操作中的语句“rst_n_i = 1;”,当仿真器遇到“#5”时将该进程挂起;
  • 再执行产生输入中的语句“Din_i = 0;”,当仿真器遇到“#80”时,将该进程挂起;
  • 在执行实例INV_DFF_inst时,虽然always语句也是在0时刻开始执行,但它仅对sclk_i的上升沿和rst_n_i的下降沿敏感,而在这时(0时刻)并没有这2个时间发生,因此不会执行该always语句;
  • 在执行实例INV_DFF_inst中的语句“assign #3 DataInv = ~DataIn;”时,需要分2部分进行分析:即计算事件和更新事件在0时刻首先计算“DataInv = ~DataIn;”,但真正更新DataInv变量的事件却在3ns以后处理。这就是为何在仿真波形上看DataInv,3ns前是“X”,3ns以后才更新为1的原因。
  • 至此,0仿真时刻的语句全部执行完毕,仿真时间轴向前推进。由于仿真时间3ns以前已经没有需要执行的任务了,因此仿真器会将时间轴推进到3ns时刻。
(2)3ns仿真时刻:
  • 在3ns仿真时刻,只有一个任务需要执行,就是将DataInv更新为1。3ns仿真时刻的语句全部执行完毕后,仿真时间轴向前推进。由于在仿真时间3ns时刻以前已经没有需要执行的任务了,因此仿真器将时间轴推进到5ns时刻。
(3)5ns仿真时刻:
  • 在5ns仿真时刻,在0时刻被挂起的复位操作进程将被重新唤醒,开始执行“rst_n_i = 0;”语句。当遇到“#55”后,该进程将再次被挂起,等待“5+55=60ns”时刻,该进程将被再次唤醒;
  • 这时,由于rst_n_i被置位于0,这一rst_n_i的下降沿将触发INV_DFF_inst中的always语句。执行“if(1'b0 == rst_n) DataOut <= 1'b0;”语句,这样,DataOut变量降重初始的“X”状态被复位为0。always语句的进程将被挂起。
  • 至此,5ns仿真时刻的语句执行完成,仿真时间轴向前推进。由于在仿真时间5ns以前,已经没有需要执行的任务了,因此仿真器会将时间轴推进到10ns时刻;
(4)10ns仿真时刻:
  • 在10ns仿真时刻,0时刻被挂起的时钟产生进程将被重新唤醒,执行“sclk_i = ~sclk_i;”语句。由于原来sclk_i是0,因此这里sclk_i出现了一个上升沿。这时,该进程的执行过程还未完成。由于forever是一个永远循环的语句,因此仿真器继续返回到“forever #10”语句,知道遇到#10以后,该进程才被挂起,执行其他10ns时刻的进程语句
  • sclk_i的上升沿必然触发INV_DFF_inst中的always语句,但这时rst_n_i还为0,因此依然执行“if(0 == rst_n_i) DataOut <= 1'b0;”语句,但DataOut的值并未发送变化。这实际上模拟了D触发器的特性当触发器处于复位状态时,无论是否有时钟沿出现,输出都不会改变
  • 在这以后的时刻,如30ns、50ns等,虽有时钟沿出现,但DataOut仍为0,仿真时刻推进到60ns后,将唤醒产生复位进程,rst_n_i被重新置1,撤销对D触发器的复位,同时该initial进程执行完成,并将永远被挂起。
(5)70ns仿真时刻:
  • 这时,时钟产生进程的initial语句的执行将再产生一个sclk_i的上升沿。这一时间将触发INV_DFF_inst中的always语句。由于此时rst_n_i为1,因此执行语句“DataOut <= DataInv;”。这样DataOut的值在70ns更新为1。
(6)80ns仿真时刻:
  • 在80ns仿真时刻,产生输入的initial进程将被唤醒,执行“Din_i=1;”语句。这一时间将触发INV_DFF_inst 中的“assign #3 DataInv = ~DataIn;”语句的执行。首先计算DataInv的新值0,然后将DataInv的更新事件调度到3ns以后的83ns时刻执行。即在83ns仿真时刻,DataInv更新为0
(7)90ns仿真时刻:
  • 在90ns仿真时刻将再次出现sclk_i的上升沿。这一事件将触发INV_DFF_inst中的always进程语句,执行语句“DataOut <= DataInv;”。因此DataOut的值在90ns时刻将更新为0。
(8)120ns仿真时刻:
  • 在120ns仿真时刻,产生输入进程将被唤醒,执行“Din_i = 0;”语句,同时该initial进程执行完成后将被永远挂起。这一事件继续触发INV_DFF_inst进程中的“assign #3 DataInv = ~DataIn;”语句的执行。首先计算DataInv的新值1,然后将DataInv的更新事件调度到123ns执行。于是在123ns仿真时刻,DataInv的值将被更新为1。
(9)130ns仿真时刻:
  • 在130ns仿真时刻将再次出现sclk_i的上升沿。这一事件将触发INV_DFF_inst中always进程语句,执行语句“DataOut <= DataInv;”。这样DataOut的值在130ns时刻将更新为1。




0

阅读 收藏 喜欢 打印举报/Report
  

新浪BLOG意见反馈留言板 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 产品答疑

新浪公司 版权所有