一:模块
在学习过verilog和sv之后,我们知道RTL是属于硬件世界(除了用Verilog写的tb),而TB是属于软件世界。而硬件世界与软件世界拥有属于自己的特有使用的模块。
大部分RTL(硬件) | module/endmodule | interface/endinterface |
TB(软件) | program/endprogram | class/endclass |
二:语句
赋值语句:
连续赋值语句的主要特点:
1)语法上,有关键词“assign”来标识;
2)连续赋值语句不能出现在过程块中(initial/always);
3)连续赋值语句主要用来对组合逻辑进行建模以及线网数据间进行描述;
4)左侧被赋值的数据类型必须是线网型数据(wire)
5)连续赋值语句总是处于激活状态。只要任意一个操作数发生变化(右侧),表达式就会被立即重新计算,并且将结果赋给等号左边的线网。
过程赋值语句的主要特点:
1)过程赋值语句分为阻塞赋值“=”与非阻塞赋值“<=”,并且在过程语句“always”与“initial”中使用。
2)在过程赋值语句的情况下,只有在过程赋值语句被执行时才执行赋值操作,语句执行完后被赋值变量的取值不再受到赋值表达式的影响, 这些类型的变量在被赋值后,其值将保持不变,直到被其他过程赋值语句赋予新值。过程赋值语句只有在执行到的时候才会起作用。
阻塞赋值与非阻塞赋值语句的区别:
1:阻塞赋值:阻塞赋值操作(“=”)是在赋值时先计算等号右侧部分的值,此时赋值语句不允许任何别的Verilog语句的干扰,直到现行的赋值完成时刻,即把计算好的右侧的值赋值给左侧值的时刻,他才允许别的赋值语句的执行。所谓阻塞的概念是指在同一个always块中,其后面的赋值语句从概念上是在前一句赋值语句结束后再开始赋值的。我们也可以认为阻塞赋值是顺序执行的。
2:非阻塞赋值:相对的我们可以将非阻塞赋值看做为是并行执行的。那么为什么非阻塞赋值语句的前一句不会阻塞后一句呢?
因为寄存器的输出不是瞬时的,需要时间,而所有的触发器采样却是同时进行的。
3:关于阻塞赋值与非阻塞赋值的小结:
非阻塞赋值(触发器) |
时序电路建模 |
锁存器电路建模 |
在同一个always块中同时建立时序和组合逻辑电路时 |
阻塞赋值(wire连线) |
用always块写组合逻辑时 |
注:1)在同一个always块中不能同时使用阻塞与非阻塞赋值
2)不要在多个always块中为同一个变量赋值,会引起竞争与冒险现象
3)在赋值时不能使用#0 延时
过程语句:
always与initial
作用 & 特性 & 区别
always | initial |
描述硬件行为语句 | 激励测试语句 |
只在module/interface中使用 | 可以在module/interface/program中使用 |
always可以凭借敏感列表执行多次 | initial只可以执行一次 |
always语句可综合 | initial语句不可综合 |
注:1)always与initial语句并行执行,且两者都无法被延迟执行,且两者内部都是顺序执行
2)always通过不同的敏感列表去描述不同的电路
对于时序电路采用边沿触发 always@(边沿)
对于组合电路采用电平触发 always@(电平)
子程序:
function与task
在Verilog中,任务与函数之间后明显的区别,其中最重要的是,任务可以消耗时间而函数不行,函数里面不能带#100之类的延时语句,或者@(**)与wait(**)的阻塞语句,更不能调用任务。而且在verilog中函数必须有返回值,并且返回值必须被使用。
在sv中,允许函数调用任务,但只能在由fork_join_none语句生成的线程中调用,并且sv中可以直接声明为void的函数,达到忽略返回值的目的。
function | task |
不耗时,也不能有耗时语句存在 | 内部可以有耗时语句存在 |
只能调用函数 | 能调用函数和任务 |
有返回值可以用void消除 | 无返回值 |
纯粹的数字或者逻辑运算 | 需要耗时的信号采样或者驱动场景 |
子程序参数:
sv中的函数定义同c语言非常类似:可以在参数列表中指定输入参数(input),输出参数(output),输入输出参数(inout),或者引用参数(ref)。而且参数列表允许默认值,默认值为input,数据类型为logic。
问题:我们经常使用的是input,output,inout,那么为什么还要使用ref呢?
答:1)input,output,inout都需要物理存储空间,而ref(既可以做输入也可以做输出)是引用参数,不需要存储,这样的好处就是省内存,特别是当形参特别大的时候使用,能够加快仿真速度。
2)ref参数在任务里可以修改变量,而且修改结果对调用他的函数随时可见。
不需要等到总线处理完成就能接收到数据变量的变化,缩短仿真周期,提高仿真速度。
task bus_read(input logic [31:0] addr,
ref logic [31:0] data);
bus.request = 1'b1;
@(posedge bus.grant) bus.addr = addr;
@(posedge bus.enable) data = bus.data;
bus.request = 1'b0;
@(negedge bus.grant);
endtask
logic [31:0] addr,data;
initial
fork
bus_read(addr,data);
thread2: begin
@data;
$display("read %h from bus",data);
end
join
从如上代码可以看出,一旦bus.enable有效,那么由于ref的作用,thread2块中立刻能获取来自存储器的数据,而不需要等到bus_read任务完成总线上的数据处理后返回,这可能需要若干个时钟周期。由于ref的作用,只要任务中的data一旦发生变化,@data的语句就会被执行。如果把data声明成output,那么@data语句需要等到总线处理完成后才能触发。
关于ref的一些注意:
1)数据变量可以声明为ref,线网类型不行;
2)若想保护数据对象只被读取,不被写入,可以通过const ref来限制
如下代码
function void print_checksum (const ref bit [31:0] a[]);
bit [31:0] checksum = 0;
for (int i=0;i<a.size();i++)
checksum = a[];
$display();
endfucntion
//const--方向--数据类型--数据变量
//此代码对于动态数组a使用了const ref 因此子程序不能对数组进行任何修改。
3)理论上ref只能用于automatic function/task中,但是在vcs中ref也可以用于static的function/task中
子程序的返回:
verilog中的子程序结束方式比较简单;当你执行完子程序的最后一条语句,程序就会返回到调用子程序的代码上,此外,函数还会返回一个值,该值被赋给与函数同名的变量。而且verilog的子程序只能返回一个简单值,例如bit,int,或是向量。
在sv中,增加了return语句,使子程序中的流程控制变得更方便。sv中还可以采用多种方式去返回一个数组。
方式1:定义一个数组类型变量,然后在函数声明中使用该类型
typedef int array[5];
array f5;
function array init(int start); //定义函数init
foreach(init[i])
init[i] = i+start;
endfunction: 函数实现累加功能
initial begin
f5 = init[i];
foreach(f5[i])
$display("f5 [%0d] = %0d",i,f5[i]);
end
方式2:通过ref来进行参数的传递(ref)类型的数组
function void init (ref int f[5],input int start);
foreach(f[i])
f[i] = i+start;
endfunction
int fa[5];
initial begin
init(fa,5);
foreach(f[i])
$display("fa [%0d] = %0d",i,fa[i]);
end
方式3:使用构造函数new,将数组包装到一个类中,然后返回数组对象的句柄
class tr;
logic [31:0] array[5];
logic [31:0] start = 5;
function new();
foreach(array[i])
array[i] = i+start;
endfunction
module top;
initial begin
tr = t;
$display("t",t);
end
endmodule
局部数据存储:
在sv中模块(module)和program中的子程序缺省的情况下仍然使用的是静态存储。若果要使用动态存储,则必须在程序句中加入automatic关键词。
通过下面例子我们可以看出static和automatic的一些差别:
static:
module top;
reg [1:0] result1,result2;
co1,co2;
initial fork
begin
#1;
abc(1'b01,result1,co1);
end
begin
#2;
abc(1'b10,result2,co2);
end
join
task abc (input [1:0] opt,output [1:0] result,output co);
reg [1:0] temp;
begin
temp = opt;
#4;
{co,redult} = temp + 2'b10;
$display("static task : %0t :temp is %0d,result is %0d,co is %0d",$time,result,co);
end
endtask
endmodule
仿真结果:
automatic
module top;
reg [1:0] result1,result2;
co1,co2;
initial fork
begin
#1;
abc(1'b01,result1,co1);
end
begin
#2;
abc(1'b10,result2,co2);
end
join
task automatic abc (input [1:0] opt,output [1:0] result,output co);
reg [1:0] temp;
begin
temp = opt;
#4;
{co,redult} = temp + 2'b10;
$display("automatic task : %0t :temp is %0d,result is %0d,co is %0d",$time,result,co);
end
endtask
endmodule
仿真结果:
关于动态子程序和静态子程序总结:
automatic | static |
显示出现 | 默认static(隐式出现) |
task/automatic 中定义的局部变量是动态分布的,即对于不同的调用分配的存储空间不同,所以task/function被多次调用时之间不会影响 | task/automatic 中定义的局部变量是静态分布的,即对于不同的调用分配的存储空间相同,所以task/function被多次调用时会相互影响 |
task/funtion中定义的变量在task/function执行完后就立刻释放掉 | task/funtion中定义的变量一直保持,直到整个仿真过程结束 |
变量的初始化:
当你试图在声明中初始化局部变量时,类似的问题也会出现,因为局部变量实际上在仿真开始前就被赋予了初值。
解决办法:
1)把程序块声明为automatic;
2)将声明与初始化分隔开;
时间值:
时间单位和时间精度:
timeunit 1ns时间单位;timeprecision 1ps时间精度 = ‘timescale 1ns/ps;
如果使用这些语句代替 ‘timescale,则必须把他们放到每个带有时延的模块里。你还可以通过使用经典verilog时间函数$timeformat,$time,$realtime.来使代码在时间标度上更清楚。
$timeformate | |
units_number | -9表示ns;-12表示ps;-15表示fs |
precision_number | 数据精度,小数点后的多少位; |
suffix_string | 时间值之后的字符串 |
minimum_field_width | 时间字符串字段的最小宽度 |
timenuit 1ns;
timeprecision 1ps;
initial begin
$timeformat(-9,3,"ns",8);
end
时间和变量:
我们可以把时间存储在变量里,但根据当前的时间量程和精度,时间值会被缩放或者舍入,time类型的变量不能保存小数时延,因为time是64位的整数,小数部分会被舍入,real类型会保存精确数值,他们只在用作时延量的时候才被舍入。