Verilog篇(三)仿真原理
首先引入一个例子:
`timescale 1ns/100ps
module TB; module INV_DFF(Clock, Reset_n, DataIn, DataOut);
reg Ck, Rst_n, Din; input Clock;
wire Dout; input Reset_n;
//Clock generation input DataIn;
initial begin output reg Data_Out
Ck = 0; wire DataInv;
forever #10 Ck = ~Ck; always @(posedge Clock or negedge Reset_n)
end begin if(~Reset_n)
//Reset generation Data_Out <= 1'b0;
initial begin else Data_Out <= Data_Inv;
Rst_n = 1; end
#5 Rst_n = 0; assign #3 DataInv = ~ DataIn;
#55 Rst = 1; endmodule
end
//Data input generation
initial begin
Din = 0;
#80 Din = 1;
#40 Din = 0;
end
INV_DFF u_INV_DFF( //DUV
.Clock(Ck),
.Reset_n(Rst_n),
.DataIn(Din),
.DataOut(Dout),
);
0仿真时刻:3个initial进程和1个DUV同时执行。同时执行的进程其顺序不是固定的,和所用的仿真器有关,假设此处的同时执行,按代码中的顺序来执行。
执行Clock Generation 中的语句Ck = 0; ~Ck。 执行forever #10 进程挂起。
执行Reset Generation 中的语句Rst_n = 1; #5 进程挂起。
执行Data Input Generation中的语句Din = 0; #80 进程挂起。
执行DUV中的语句~DataIn; #3进程挂起。always @进程挂起。 至此0仿真时刻的语句全部执行完毕,仿真时间轴向前推进。
3仿真时刻:只有一个计算事件 DataInv = #3 ~DataIn;更新DataInv的值。无触发更多计算事件,所以仿真时间轴向前推进。
5仿真时刻:执行Reset Generation 中的语句 Rst_n = 0; #55 进程挂起。
由于Rst_n的更新时间,DUV中的always @进程执行,DataOut值更新。无触发更多计算事件,仿真时间轴向前推进。
10仿真时刻: 执行Clock Gneration 中的计算事件CK。更新事件触发。#10进程挂起。
执行DUV中的always进程,计算事件Data_Out = 0。无触发更多事件,仿真时间轴向前推进。
60仿真时刻: 执行Clock Gneration 中的计算事件CK。更新事件触发。#10进程挂起。
执行Reset Generation 中的语句 Rst_n = 1; 进程结束。
执行DUV中的always进程,计算事件Data_Out = 1(Rst_n的值已经为1)。无触发更多事件,仿真时间轴向前推进。
80仿真时刻: 执行Clock Gneration 中的计算事件CK。更新事件触发。#10进程挂起。
执行Data Input Generation中的语句Din = 1; #40 进程挂起。
执行DUV中的always进程,计算事件Data_Out = Data_Inv。
执行DUV中的语句~DataIn; #3进程挂起。always @进程挂起。 至此0仿真时刻的语句全部执行完毕,仿真时间轴向前推进。
仿真时间:是仿真时间维护的时间值,用来对仿真电路的真实时间进行建模(仿真时间和软件的执行时间没有任何联系),当仿真时间推进到某一个时间点时,该时间点就被称为当前仿真时间,而以后的任何时间都被称为将来仿真时间。
事件:模型中数值的变化,功能仿真是一种事件驱动的仿真,整个仿真过程都是围绕事件来组织的。
更新事件:在被仿真的电路中,线网或寄存器的值在任何进程中的任何改变都被认为是一个更新事件。
计算事件:由于更新事件产生的,进程的计算,计算事件。
计算事件和更新事件之间循环往复的互相触发,推动仿真时间的前进。
进程是Verilog中的独立执行单元,包括:原语(Primitives), 模块(Moules), initial过程块, always过程块, 连续赋值语句(assign), 异步任务(task)。在仿真时,所有的进程都是仿真器按Verilog的语义来顺序执行的,效果是各个进程并行执行的效果,在未执行完当前所有的进程时,仿真时间不会向前推进。
initial begin
Ck = 0; forever Ck = ~Ck;
end
例子会hang在仿真时刻0,因为更新时间一直触发计算事件,计算事件一直触发更新时间。
Verilog中的时序控制:事件语句(@),延时语句(#),等待语句(wait)。
Verilog仿真时的不确定性:在同一个仿真时间内,几个同一调度模块中事件执行顺序的任意性,进程之间语句的任意交织。
verilog或者systemverilog相对于C,能用于硬件建模的最大区别:
1)connectivity 一个大的design可以由很多block,组成,通过例化或者interface来进行连接
2)time time的变化可以使得design的state变化,与执行时间(execution time)完全不同,通过@/wait/#来实现
3)concurrency event之间的同步发生关系, 通过initial/always/fork join来实现
对于验证还缺少:1)randomization; 2)constrainability; 3)functional coverage;
在simulator中的concurrency是基于time_share的来跑的,CPU轮询执行。
always模块,initial模块,连续赋值模块,fork/join,都是完全并行执行的,并没有先后顺序。
在基于time-sharing的处理中,simulator对于各个并行thread,保证访问机会时一样的,但是各个模块的先后顺序不能保证,
和代码块中的位置有关。simulator也不会自己关掉一个thread,除非自己执行结束。
Verilog的事件调度:
在systemverilog中有两种thread,module thread和program thread,module thread主要用来建模design,
program thread主要用来建模testbench。
program中的task的执行,主要看task的定义位置,如果是在module内定义的task,则尽管在program中调用,也算是一个
module thread。
对于program和module内的class的执行顺序,systemverilog标准中,并没有准确给出,根据simulator的实现而不同。
program中在SV2012中,才开始支持非阻塞赋值。
其中的执行顺序:
1)sample阶段,进行clock,property这样的采样。
2)excute阶段,执行阻塞赋值等。
3)reactive阶段,执行非阻塞赋值等。以及阻塞赋值的迭代。
4)evaluate阶段,执行assertion的判断。
5)program thread阶段,执行program中的阻塞赋值,非阻塞赋值
simulation的前进,依靠两条线,一条simulation time,另一个是module thread中的非阻塞赋值和阻塞赋值之间的迭代。
其中第二种也称为delta cycle,一个设计中可能会有很多delta cycle,因为信号的依赖关系可以一直迭代。
几种存在race condition的情况:
1)concurrent中的冲突:
2)delta cycle对clock的影响:
如果两个时钟有信号之间的采样关系,而且一个clock比另一个clock多一个非阻塞赋值。
此时,两个clock之间会差一个delta cycle,在RTL仿真时,两个clock之间的信号采样会出错。
快一点的clock的值,可以再被慢一点的采到,但是在波形中不会体现出来。
这时的解决方法:
1)手工增加delay
2)将另一个clock 也经过一个非阻塞赋值。