《数字IC入门》白栎杨阅读笔记第三章《仿真方法》
·

3.1 设计者仿真与验证工作的区别
3.2 仿真平台的一般架构
- 待测设计(DUT)
- 参考模型和DUT的响应被放在一起进行比较,以确认两者是一致的。当发现不一致时,首先应该怀疑DUT的问题,因为参考模型被编写出来,本身就默认为正确的,它也被称为金标准(Golden)。但参考模型也只能相对保证正确。
- 设计电路的基本单元称为模块(Module),而仿真的基本单元称为模型(Model)。前者是真实的电路,后者是对真实器件行为的模仿,如模仿Flash、EEPROM、ADC等器件行为的Model。
- 断言是一种判断的言论,例如信号a等于1就是一种判断。若信号a真的等于1,就会报告正确,或者不报告,若a不等于1,则报告错误。设计和验证都可以使用断言。当设计在代码中留下断言后,若验证仿真时发现断言报错,则说明设计本人认为有错,就可以直接去找设计。断言的实现有多种方式,System Verilog中推荐的断言方式称为SVA(System Verilog Assertions)。
3.3 Verilog和System Verilog
- Verilog是System Verilog(SV)的子集
- 在仿真阶段,一般会在仿真工具中加入-sv选项,使其在编译时支持SV语法。
3.4 Testbench文件的基本结构
- 外围设备可分为3类,
- 第一类是设备A,它需要与DUT交互,I2C就属于第一类。
- 第二类是设备B,只单纯地负责激励,ADC和串口发送设备属于第二类。
- 第三类如设备C,只单纯地接收信号,并做出反应,串口接收设备、PWM及由GPIO驱动的LED等设备属于第三类。

图3-3 Testbench文件的基本结构
3.5 时钟和复位的产生
- initial块的内部是有顺序的,可以当作一个独立的小型C语言来看待。initial块是SV最有别于普通Verilog的特征,因为它足够灵活,可以按时间顺序或事件发生的顺序决定内部信号的输出,而不必刻意找规律。
- forever块必须放在initial内部。有规律是无规律的一种特例,forever即是initial中的特例,体现了一种规律性。从英文上理解,forever(永远)和always(总是)都是规律性的体现,因此两者可以互换,即带forever的initial块可以与always互相替代。
- intial中如果没有forever,执行到最后一句就结束了,不会像always一样重复执行。
- always #5 clk <= ~clk;
-
intial begin forever #(10/2.0) clk = ~clk; end
- initial内部产生的信号,类型可声明为reg
- #(10/2.0)是延时,单位默认为ns(纳秒):延迟5ns。
- 不直接写5,而是写为10/2.0,这是因为10ns体现时钟周期(时钟频率为100MHz),一个周期电平变换两次,因此需要除以2。
- 写成2.0是为了转换为浮点数,例如9/2的结果为4,但是9/2.0的结果为4.5。
- 凡是带#延迟的,都不能被综合,只能用于仿真行为描述。
3.6 灵活的等待方式
等待方式一:如下图所示,a初始化为0,之后,它每10ns观测一次b,若发现b为1,a就自加。最后的结果类似于产生了一个以10ns为周期的时钟,在时钟驱动下自加。当条件未达到时,a的数值会自动保持,而不是清零。

等待方式二:@后面跟敏感条件,意为仅当敏感条件发生时,后面的事件才发生。下图表示当信号b从0变到1后,再等3ns,信号a才会变为1。

等待方式三:wait(a),其中a是一个信号,该句意为当a为0时就一直等待,直到a变成1为止。
3.7 信号类型的扩展和强制转换
- 在SV中,除了传统的wire、reg类型,logic类型,其他常见的还有bit类型、int类型、real类型等。
- bit类型在语法上只支持0和1两种状态,非此即彼,不存在x态和z态,
- 在logic和reg类型中,0、1、x、z等4种信号状态都支持,因此TB中较少用到bit类型,而且logic和reg类型经常混用。
- int其实相当于“reg signed[31:0]”声明,即32位有符号整数。若表示32位无符号整数,则可以直接声明为“reg[31:0]”。也可使用integer类型,再或者使用int unsigned类型。
- logic、bit、int等类型本质上是reg类型,即这类信号可以在initial和always块中产生,而不能在assign块中产生。
- wire类型和上述类型不同,它可以在assign块中产生。
- real表示带符号的浮点数,相当于C语言中的float。real类型信号在initial、always和assign中都可以产生。
- 转换
- int'(A/B):将结果强制转换为整数,且自动将小数四舍五入
- signed'()可以将无符号数转换为有符号数,并且可综合
- 浮点数/整数,会自动转为浮点数。
- SV中的内建(Build-In)命令都是以$开头的
3.8 log的打印
$display("state = % d, Isens = % f ", state, Isens);
- 要打印log时,可以使用$display()函数
- %d代表以十进制打印整数,
- %f代表打印浮点数,
- %x代表以十六进制数打印整数,
- $display会自动加入换行符,不用写“\n
- %f、%d、%x、%t等,前面都可以对字符宽度进行约束
- 例如%5t,即以5个字符宽度来打印时间。若宽度本身超过5个,例如17.000宽度是6个字符,则仍然打印完整的6个字符。
- 对于整数如%d,不仅可以设定打印宽度,还可以设置高位补0,例如%08d,表示以8位宽度打印,当宽度不够时在高位补0。
- 对于浮点数%f也可以约束精度,例如%3.5f,将整数位宽约束到3个字符,将小数精度约束到5个字符,当整数超过3个时按照实际数值打印。
- 所有浮点数类型%f、%t都不能像整数%d、%x那样自动在高位补零。
3.9 内建功能函数
- 一些内建函数:
- 结束仿真:$finish,$stop, 但stop,仿真界面不退出,便于Debug,而$finish是彻底退出。
- $pow(a,b),即以a为底,b为指数,计算该幂次方。
- $hypot(a,b),求复数a+bi的模
- $ln(a),求以自然常数e为底,a的对数。
- $ln(a),求以自然常数e为底,a的对数。
- $atan2(a,b),已知某个角度的正弦值a和余弦值b,反推该角度值。
- 实际上所有函数都支持浮点数和定点数,即便输入的是定点数,仿真工具在内部计算时也要转换为浮点数,最终输出的也是浮点数,但如果TB上规定输出的类型为定点数,工具会自动采用四舍五入法将其转换为定点数。
3.10 仿真器也会出错
- 出现仿真器波形错误的原因主要是复杂的时钟分频,这种分频会使仿真器逻辑错乱,混淆事件发生的前后顺序。
- 原因:仿真器也是由面向对象的高级语言编写而成的,因此,虽然硬件实际上是并行的,而且仿真器也会竭力给仿真者一种并行的假象,但在其内部处理上,仍然与其他计算机语言一样是按顺序执行的。
- 仿真器以顺序的步骤做出并行的效果的方法:将时间和事件分开。例如:“#10 a=2;”
- 对于时钟信号,仿真器会特别区分,它辨别时钟的方法就是从寄存器的敏感列表中寻找,但如果该时钟线路经过了很多类似数据的处理,将影响它的分辨。
- 若工具将时钟当作普通信号,则它将不把时钟线放在时间变量中处理,而是放在事件变量中,和其他的事件一起排序。就可能发生错误。
- 解决方法:当判断为仿真器仿错的情况后,可以在设计的分频处加一小段延迟,人为将这几个事件放在不同的时间点上,这样执行顺序就不会混乱了。例如:cnt <= #0.01 2'd0;
- 示例:
// 意图:在时钟clk的上升沿出现后,将信号aaa赋值为3。
// 即希望在仿真时,当前clk上升沿来临,采到aaa的值为0,下一个clk上升沿来临时,采到aaa的值为3
//错误方式如下。这种方式,往往会使时钟上升沿采到3,即EDA软件会认为aaa赋值事件在时钟上升沿之前发生
initial
begin
aaa = 0;
@(posedge clk);
aaa = 3;
end
//正确方式1:插入延迟
initial
begin
aaa = 0;
@(posedge clk);
#1;
aaa = 3;
end
//正确方式2:用非阻塞。EDA软件将自动认为该时钟上升沿采样到的aaa值为0,下一个上升沿处aaa才变为3
initial
begin
aaa = 0;
@(posedge clk);
aaa < = 3;
end
- 非阻塞赋值的使用
- 非阻塞赋值语句必须连续写,阻塞赋值语句应写在非阻塞赋值之后。
- 得分析具体情况
3.11 前仿中的真相与假象
- 前仿就是直接使用RTL仿真
- 问题:
- 组合逻辑产生的信号中间大概率是存在细微毛刺的,只不过同步系统只以时钟沿作为评判标准,只要时钟敏感沿不采样到毛刺,则组合逻辑的毛刺就可以忽略。则仿真会看起来没问题,但实际有问题。
- 信号与时钟做组合逻辑时,结果也容易出错。前仿时不带延迟信息,ps级的延迟不可见,因而会出现有的信号明明没有变化,却产生了只有它变化后才会造成的影响。因此,电路的设计者应避免用时钟信号参与组合逻辑。
- 对于仿真中的一些怪现象,要联系实际电路进行分析,如果是仿真器本身的问题(如3.10节),则可通过修改TB来避免,如果是电路RTL设计问题,则不应用TB来规避,而是应修正设计中的真实缺陷。
3.12 从DUT中直接获取信号
- 引出信号方法:assign tb_a = tb.DUT.AA.BB.a;
- 含义:TB中例化了DUT,DUT中有一个例化名为AA的模块,AA中有一个例化名为BB的模块,需要引出的是BB中的信号a
- 注意,DUT、AA、BB都是例化名,而非模块的名称。
- tb_a是其在TB中的新名字,可以任意命名。
- 这种方法既可以引出单根信号线,又可以引出总线。
3.13 数据预读取
- 数据预读取含义是在仿真的第零时刻,所有先验数据都已经进入存储器或TB的数组序列中。
- $readmemb("user_num.dat ", DUT.memory);
- 扩展名随便都可以
- 含义:将文本文件user_num.dat中的信息加载到DUT.memory路径中
- 为了方便仿真器找到文件,双引号中还可以明确填写文件路径
- DUT.memory只是本例的假设位置,读者可根据DUT存储的实际位置填写,存储器并不一定放在DUT内部。
- 若user_num.dat里面以二进制记录数据,则用$readmemb命令,若以十六进制记录数据,则用$readmemh命令
- 以二进制记录的数据并不是二进制文件。二进制文件是不用ASCII码显示,直接将原始数据保存下来的文件,用文本编辑器打开是乱码。以二进制记录的数据,其字符仍然是转换为ASCII码进行存储,用文本编辑器打开可以正常观看。
- 在数字设计或芯片测试的过程中,坑使用逻辑分析仪、示波器等仪器收集一些测试波形,并将该波形保存为数据,然后将该数据输入TB中进行仿真。保存数据的文件多以CSV为扩展名,在Windows系统中默认以Excel表格形式打开。

- reg timeBai[$]:表示一个无限长度数组,$表示无限长度
- 使用$fopen命令可以打开一个名叫AAA.csv的波形文件,生成一个文件句柄,括号中的r表示目的为读取而非写入
- $feof(file)用于寻找文件的结尾,使用while循环持续寻找
- $fscanf用于在遇到与文本匹配的字符时就将其输入数组中,该文件是两列,因此分别输入保存时间的数组timeBai和保存数据的数组dat_vector中
- timeBai和timeBai2两个数组没用以后,可以用timeBai.delete()语句将其内存空间释放。因为它们是无限数组,在仿真器内部用链表表示,比较占用内存空间,所以需要及时释放。
- 上述处理过程没有出现SV中的延时语句,因此这些动作都发生在第零时刻,在仿真器看来就是初始化过程,没有时间消耗。
- dat_f>1.65是为了将浮点形式的dat_f转换为二进制形式的dat,这样才能作为数字IC的输入。如果CSV本身就保存着二进制数,则无须这样判决。
- 为什么是1.65? 这个值通常与电路的电平标准有关。例如:
- 在一个电压域为 3.3V 的数字系统中,逻辑‘0’和逻辑‘1’的阈值电压通常围绕中间值 1.65V 附近。
- 如果 dat_f > 1.65V,则认为它更接近于代表逻辑‘1’的高电平(如3.3V),因此比较器输出 1。
- 如果 dat_f <= 1.65V,则认为它更接近于代表逻辑‘0’的低电平(如0V),因此比较器输出 0。
3.14 将仿真数据以文本形式输出

- 先用$fopen打开一个文本句柄,语句中w表示以可写方式打开。
- $fdisplay的操作同C语言中的fprintf一致,其后自带换行符,还可使用$fwrite代替,区别是其后不带换行符。
- 最后使用$fclose函数关闭句柄。
- 数组不是信号,只有将数组中的数在不同的时刻发出去,才是信号,反之也成立,将不同时刻的信号用一个数组收集起来,信号就变成了数组。数组可以看作损失了时间信息的信号。
3.15 并行处理的方法
更多推荐
所有评论(0)