SystemVerilog可综合硬件设计
28 Jun 2023 22086字 74分 次 SystemVerilog打赏作者 CC BY 4.0 (除特别声明或转载文章外)
1 前言
SystemVerilog完全兼容Verilog HDL,还加入了类似C++的语法用于验证。
SystemVerilog在硬件设计中有助于编写可综合硬件模型,对Verilog HDL的增强部分如下:
- 设计内部的封装通信和协议检查的接口;
- 类似于C语言的数据类型;
- 用户自定义类型;
- 枚举类型;
- 类型转换;
- 结构体和联合体;
- 可被多个设计快共享的定义包(package);
- 外部编译单元区域(scope)声明;
- ++,–,+=以及其他赋值操作;
- 显式过程块;
- 优先级(priority)和唯一(unique)修饰符;
- 编程语句增强。
2 特定逻辑过程
2.1 always_ff
描述时序逻辑电路模块,使用非阻塞赋值,如果过程内部写成组合逻辑,则会报错。
2.2 always_comb
描述组合逻辑电路模块,使用阻塞赋值,如果过程内部写成锁存器,则会报错。
2.3 always_latch
描述锁存器电路模块,使用非阻塞赋值,如果写成其他电路,则会报错。
3 新添加的数据类型
Verilog HDL的reg、integer和time变量的每一位都有四种逻辑;wire、wor、wand和其他线网(net)类型的每一位有120种值(四态逻辑加上多个强度级)及专用线逻辑决断函数。
Systemverilog有四态,两态和实型三种数据类型,其中两态和实型不适合RTL设计。
数据类型 | 数据类型 | 位宽 | 标准 |
---|---|---|---|
reg | 4态 | 位宽可变 | Verilog-2001 |
integer | 4态有符号 | 32 | Verilog-2001 |
logic | 4态0,1,X,Z | 位宽可变 | SystemVerilog |
bit | 2态0或1 | 位宽可变 | SystemVerilog |
byte | 2态有符号整型 | 8 | SystemVerilog |
shortint | 2态有符号整型 | 16 | SystemVerilog |
int | 2态有符号整型 | 32 | SystemVerilog |
longint | 2态有符号整型 | 64 | SystemVerilog |
3.1 Logic
4态,0、1、X、Z,位宽可变,可以代替所有其他类型,包括reg。
logic
类型类似于VHDL的std_ulogic
类型:
- 对应的具体元件待定;
- 只允许使用一个驱动源,或者来自于一个或者多个过程块的过程赋值(对同一变量既进行连续赋值(
assign
)又进行过程赋值是非法的); - 在SV中,
logic
和reg
是一致的(类似于Verilog中wire
和reg
类型是一致的)。
wire
数据类型仍旧有用,因为:
- 用于多驱动源总线:如多路总线交换器;
- 用于双向总线(两个驱动源)。
输入和输出:
- 当端口方向被声明为
input
或inout
时:- 如果指定了
wire
,但没有指定数据类型,则默认为logic
; - 如果指定了数据类型为没有关键字
wire
的logic
,则数据类型仍为默认的net
; - 如果整个数据类型未定义,则默认数据类型为
wire logic
;
- 如果指定了
- 当端口方向被声明为
output
:- 如果指定了
wire
,但没有指定数据类型,则默认为logic
; - 如果整个数据类型未定义,则默认数据类型为
wire logic
; - 如果指定了数据类型为没有关键字
wire
的logic
,则数据类型仍为默认的var
。
- 如果指定了
例如:
module my_module (
input wire logic clk,
input wire logic en,
input wire logic [7:0] din,
inout wire logic [7:0] bus,
output var logic [7:0] dout,
output var logic [7:0] bus
);
3.2 real
64位有符号数,等价于C中的double
。
3.3 shortreal
32位有符号数,等价于C中的float
。
3.4 用户定义的类型——typedef
允许生成用户定义的或者容易改变的类型定义,通常命名用“_t”做后缀:
`ifdef STATE2
typedef bit bit_t; //2态
`else
typedef logic bit_t;//4态
`endif
用户自定义类型可以在局部定义,也可以在编译单元域进行外部定义。局部定义的类型声明写在module
内,外部定义的类型声明写在package
内。
只要用typedef
就能很容易的在4态和2态逻辑仿真之间切换以加快仿真速度。
可以直接从package
中引用自定义类型:
package chip_types;
`ifdef TWO_STATE
typedef bit dtype_t;
`else
typedef logic dtype_t;
`endif
endpackage
module counter(
output chip_types::dtype_t[15:0] count,
input chip_types::dtype_t clock,resetN
);
always_ff@(posedge clock, negedge resetN)begin
if(!resetN) count <= 0;
else count <= count + 1;
end
endmodule
如果觉得这样每个端口都需要引用包名很繁琐,也可以将包定义导入到$unit
编译单元域中:
package chip_types;
`ifdef TWO_STATE
typedef bit dtype_t;
`else
typedef logic dtype_t;
`endif
endpackage
import chip_types::dtype_t;
module counter(
output dtype_t [15:0] count,
input dtype_t clock,resetN
);
always_ff@(posedge clock, negedge resetN)begin
if(!resetN) count <= 0;
else count <= count + 1;
end
endmodule
3.5 枚举——enum
缺省状态下为2态整型(int
)变量:
enum {red,yellow,green} lignt1,lignt2; //red = 0, yellow = 1, green = 2
enum {bronze=3,silver,gold} medal; //silver = 4, gold = 5
enum {bronze=4'h3,silver,gold} medal; //silver = 4'h4, gold = 4'h5
常用于状态机设计中状态的声明。
整型:
module fsm_1;
......
enum { //类型缺省,为整型
IDLE = 3'b000, //对枚举名赋值
READ, //3'b001
DLY, //3'b010
DONE, //3'b011
XX = 3'b111
} state,next;
....
endmodule
四状态:
module fsm_2;
......
enum reg [1:0]{ //指定四状态数据类型
IDLE = 2'b00, //对枚举名赋值
READ, //2'b01
DLY, //2'b10
DONE, //2'b11
XX = 'x //x赋值在仿真无关项优化综合和调试时非常有用
} state,next;
....
endmodule
另外,枚举类型变量可以使用“.name”显示字符。
当枚举类型从包中导入时,只有类型名被导入,枚举列表中的数值标签不会被导入,因此不会在枚举类型名导入的命名范围中可见。下面的代码将不会正确工作:
package chip_types;
typedef enum {WAITE,LOAD,READY} states_t;
endpackage
module chip(...);
import chip_types::states_t; //只导入typedef名
states_t state,next_state;
always_ff@(posedge clk, negedge resetN)begin
if(!resetN)
state <= WAITE; //错误,“WAITE”还未导入
else
state <= next_state;
end
...
endmodule
为了使枚举类型标签可见,可以显式导入每个标签,或用通配符导入整个包。通配符导入将使枚举类型名和枚举值标签在import
语句作用域内都可见。
3.6 有符号/无符号声明
SystemVerilog在声明变量时能够指定变量是否有符号:
<size> '[s|S] b|d|o|h : [01...xXzZ]
例如:
logic [7:0] a,b,c,d;
assign a = 1'b1; //8'b0000_0001
assign b = 8'b1; //8'b0000_0001
assign c = 1'sb1; //8'b1111_1111
assign d = 8'sb1; //8'b0000_0001
3.7 Net与Var
net
数据类型:- 代表物理连接;
- 通常用于模块接口列表
- 使用
wire
创建:wire logic [15:0] dout;
- 只能由连续赋值语句驱动,不能被
always
块驱动。
var
数据类型:- 代表数据存储单元;
- 通常用于模块内部值的本地存储;
- 使用
var
创建:var logic [15:0] bus;
- 能被
always
块或连续赋值语句驱动。
4 数组与队列
4.1 固定数组
固定数组的定义举例:
integer numbers [5]; //二维数组(5×32),未赋初值
int b[2] = '{ 3,7 }; //一维数组,赋初值3,7
int c[2][3] = '{ { 3,7,1 },{ 5,1,9 } }; //二维数组赋初值
byte d[7][2] = '{default:-1}; //所有数据赋值为“-1”
bit [31:0] a[2][3] = c; //把矩阵c复制给a
以下定义仅在仿真时可用:
bit [7:0] d []; //动态数组
bit [7:0] d [$]; //队列数组
bit [7:0] d [data_type]; //联合数组
超过边界值的写操作将被忽略,超过边界值的读操作:两值逻辑返回“0”,四值逻辑返回“x”。
4.2 动态数组
动态数组的定义举例:
reg [7:0] ID[],array1[] = new[16]; //使用new函数分配数组ID和array1,其中array1的大小为16
reg [7:0] data_array[]; //分配动态数组data_array
ID = new[100]; //ID的大小为100
ID.delete(); //释放ID大小
动态数组只有一维,在仿真运行时,使用构造函数分配数组大小,如果越界对动态数组进行读写操作,仿真出错。
4.3 队列
队列的定义:
int array1[$] = {0,1,3,6};
int array2[$] = {4,5};
int j = 2;
array1.insert[2,j]; //{0,1,2,3,6}
array1.insert[4,array2]; //{0,1,2,3,4,5,6}
array1.delete[1]; //{0,2,3,4,5,6}
array1.push_front[7]; //{7,0,2,3,4,5,6}
j = array1.pop_back[]; //{7,0,2,3,4,5} j=6
其中:
- 在仿真运行时,分配或者释放数组的内存空间:
- 分配:push_back()、push_front()、insert();
- 释放:pop_back()、pop_front()、delete();
- 不能使用“new[]”函数创建内存空间;
- 索引0指向队列最低的索引;
- 索引$指向队列最高的索引;
- 越界对队列进行读写操作会导致仿真错误;
- 队列操作的效果类似与FIFO或堆栈;
- 单一维度。
4.4 联合数组
联合数组的定义:
byte array[string],t[*],a[*]; //byte类型的数组,索引类型为string
int index;
array["byte0"] = -8; //创建“byte0”索引,索引的数组值为-8
for(int i=0;i<10;i++)
t[1<<i] = i; //生成十个t数组元素
a = t; //数组复制
其中:
- 索引的类型可以是数字、字符串或者类;
- 动态分配和动态释放内存空间:
- 分配
- 数组的移动搜索:
- first()、next()、prev()、last();
- num()函数决定了数组元素的个数;
- exists()函数可以判断有效索引是否存在;
- 越界读操作时,两值逻辑返回“0”,四值逻辑返回“x”;
- 只支持一维。
4.5 未打包的数组
未打包的四维数组:
logic xdata [3:0][2:0][1:0][7:0]; //对于这个语句最大的可访问单元为1位
4.6 打包的数组
打包的一维数组和未打包的三维数组:
logic [7:0] xdata [3:0][2:0][1:0]; //对于这个语句最大的可访问单元为8位
打包的二维数组和未打包的二维数组:
logic [1:0][7:0] xdata [3:0][2:0]; //对于这个语句最大的可访问单元为16位
打包的四维数组:
logic [3:0][2:0][1:0][7:0] xdata; //对于这个语句最大的可访问单元为192位
4.7 数组总结
| 数组类型 | 物理内存 | 索引 | | — | — | — | | 固定数组 | 编译时创建,之后不能修改 | 数字 | | 动态数组 | 创建时仿真,仿真期间可以改变 | 数字 | | 队列 | 仿真时可以改变队列大小 | 数字 | | 联合数组 | 仿真时分配内存 | 数字、字符串、类 | —-
5 接口
5.1 隐含的端口连接
Verilog和VHDL都能用按端口名连接或按顺序连接的方式引用实例模块。SystemVerilog用了两个新的隐含端口连接解决了顶层代码编写时表示端口连接代码的冗长:
- .name:端口连接;
- .*:隐含的端口连接。
隐含.name和.*端口连接的规则:
- 在同一个实例引用中禁止混用.*和.name端口;
- 允许在同一个实例引用中使用.name和.name(signal)连接;
- 允许在同一个实例中引用中使用.*和.name(signal)连接;
- 必须用.name(signal)连接的情况:
- 位宽不匹配;
- 名称不匹配;
- 没有连接的端口。
5.2 SystemVerilog中的接口
隐藏的端口连接的缺点之一是不容易发现错误,所以出现了接口。
接口提供了新的层次结构,把内部连接和通信封装起来,把模块的通信功能从其他功能中分离出来,并且消除了接线引起的错误,让RTL级别的抽象成为可能。
有关接口的说明:
- 接口能传递穿越端口的记录数据类型;
- 有两种类型的接口元素:
- 声明的;
- 作为参数可以直接传递进入模块。
- 接口可以是:
- 参数,常数和变量;
- 函数和任务;
- 断言。
接口的引用:
- 接口变量可以用接口实例名加“.”变量名引用;
- 接口函数可以用接口实例名加“.”变量名引用;
- 通过接口的模块连接:
- 能调用接口任务和函数的成员来驱动通信;
- 抽象级和通信协议能容易地加以修改,用一个包含相同成员的新接口来替换原来的接口。
接口的使用:
interface intf; //接口类型声明
logic a,b;
logic c,d;
logic e,f;
endinterface
module top;
intf w(); //接口实例引用
mod_a m1(.i1(w)); //具体化的接口实例w在mod_a中被称为i1
mod_b m2(.i2(w)); //具体化的接口实例w在mod_b中被称为i2
endmodule
module mod_a(intf i1); //括号内:引用定义的接口类型 / 被引用接口的本地访问名
endmodule
module mod_b(intf i2); //括号内:引用定义的接口类型 / 被引用接口的本地访问名
endmodule
如图:
接口不要都用线网类型。一般情况下,接口将把模块的输出连接到另一模块的输入,输出可能是过程性的或者连续性驱动的,输入是连续性驱动的。既然接口是把输出与输入连接起来,而输出常由过程性语句赋值,即使输出是由连续赋值语句指定的,当它们被连接到不同模块时,它也往往被转变成过程性赋值。而双向和多驱动线网在接口定义中必须声明为线网类型。
6 task和function
6.1 task
task概述:
- 含有input、output、inout语句;
- 可以调用function;
- 消耗仿真时间:
- 延迟:
#20;
- 时钟周期:
@(posedge clock)
- 事件:
event
- 延迟:
举例:
task task_name
parameter
input
output
reg
…text body…
endtask
6.2 function
function
概述:
- 执行时不消耗仿真时间;
- 不能有控制仿真时间的语句,例如
task
中的延时等; - 不能调用
task
; void function
没有返回值;
function
语法为:
funtion <返回值的类型或范围> (函数名)
<端口说明语句> //input XXX
<变量类型说明语句> //reg YYY
begin
<语句>
……
函数名 = zzz; //函数名就相当于输出变量
end
endfuntion
计算有符号数绝对值的例子:
function [width-1:0] DWF_absval;
input [width-1:0] A;
begin
DWF_absval = ((^(A^A)!==1'b0))?{width{1'bx}} : (A[width-1] == 1'b0) ? A : -A;
end
endfunction
6.3 return语句
SystemVerilog中增加了return
语句。return
用于退出task
和function
,执行时返回表达式的值,否则最后的返回数值赋值给函数名。
function int divide (input int numerator,denominator);
if(denominator==0)
return 0;
else
return numerator/denominator;
endfunction
always_ff@(posedge clock)
result <= divide(b,a);
//or
always_ff@(posedge clock)
result <= divide( .denominator(b),
.numerator(a) );
6.4 传递参数
SystemVerilog增强了函数形式参数:
- 增加了
input
、output
传输参数 - 每一个形式参数都有一个默认的类型,调用
task
和function
时,不必给就具有默认参数值的参数传递数值,如果不传递参数值,就会使用默认值。如上一节的例子。
使用引用(reference)代替复制的方式传递参数:
- 常见的向
task
和function
传递参数值的方式是复制; - 使用reference的方式显式地向
task
和function
传递参数:- 关键字
ref
取代了input
、output
、inout
; - 只有
automatic
任务和函数才可以使用re
f参数。
- 关键字
function automatic void fill_packet ( ref logic [7:0] data_in [7:0],
ref pack_t data_out);
for(int i=0;i<=7;i++) begin
data_out.data[(8*i)+:8]=data_in[i];
end
endfunction
always_ff@(posedge clock) begin
if(data_ready)
fill_packet(.data_in(raw_data),.data_out(data_packet));
end
8 并发线程
SystemVerilog中除了fork join
之外还增加了fork join_any
和fork join_none
。
8.1 fork join
当fork join
中的子线程都执行完之后才会跳出fork
继续执行父线程语句,例如:
initial begin
Statement1; //父线程,第1个执行
#10 Statement2; //父线程,第2个执行
fork
Statement3; //子线程,第3个执行
#50 Statement4; //子线程,第7个执行
#10 Statement5; //子线程,第4个执行
begin
#20 Statement6; //子线程,第5个执行
#10 Statement7; //子线程,第6个执行
end
join
#30 Statement8; //父线程,第8个执行
Statement9; //父线程,第9个执行
end
8.2 fork join_any
当fork join_any
中的子线程有一条执行完,则会跳出fork继续执行父线程的语句,在父线程中遇到延时等语句后又会继续执行fork
,此时变为fork join
,但执行的时长为父线程中遇到的延时语句的时长,如果父线程中没有延时语句,则不会再进入fork
,例如:
initial begin
Statement1; //父线程,第1个执行
#10 Statement2; //父线程,第2个执行
fork
Statement3; //子线程,第3个执行
#50 Statement4; //子线程,第8个执行
#10 Statement5; //子线程,第5个执行
begin
#20 Statement6; //子线程,第6个执行
#10 Statement7; //子线程,第7个执行
end
join_any
Statement8; //父线程,第4个执行
#80 Statement9; //父线程,第9个执行
end
而如果Statement9的延时小于子线程内部的50个时间单位的延时,则子程序不会都执行完:
initial begin
Statement1; //父线程,第1个执行
#10 Statement2; //父线程,第2个执行
fork
Statement3; //子线程,第3个执行
#50 Statement4; //子线程,不执行
#10 Statement5; //子线程,第5个执行
begin
#20 Statement6; //子线程,第6个执行
#10 Statement7; //子线程,不执行
end
join_any
Statement8; //父线程,第4个执行
#30 Statement9; //父线程,第7个执行;此时遇到延时,该线程挂起30个时间单位,回fork执行语句,30个时间单位后执行该线程
end
8.3 fork join_none
先执行父线程的语句,如果父线程中有延时语句,才会进入fork
执行子线程:
initial begin
Statement1; //父线程,第1个执行
#10 Statement2; //父线程,第2个执行
fork
Statement3; //子线程,第4个执行
#50 Statement4; //子线程,第8个执行
#10 Statement5; //子线程,第5个执行
begin
#20 Statement6; //子线程,第6个执行
#10 Statement7; //子线程,第7个执行
end
join
Statement8; //父线程,第3个执行
#80 Statement9; //父线程,第9个执行
end
如果Statement9的延时小于子线程内部的50个时间单位的延时:
initial begin
Statement1; //父线程,第1个执行
#10 Statement2; //父线程,第2个执行
fork
Statement3; //子线程,第4个执行
#50 Statement4; //子线程,第9个执行
#10 Statement5; //子线程,第5个执行
begin
#20 Statement6; //子线程,第6个执行
#10 Statement7; //子线程,第7个执行
end
join
Statement8; //父线程,第3个执行
#50 Statement9; //父线程,第8个执行
end
8.4 wait fork
如果在fork join_any
或fork join_none
语句的后面没有延迟语句,可以使用wait fork
语句等待所有的fork
并发进程执行完成:
task do_test;
fork
exec1;
exec2;
join_any
fork
exec3;
exec4;
join_none
wait fork; //会等待exec1、exec2、exec3、exec4全部执行完成
endtask
8.5 disable fork
与wait fork
相反,停止fork
所有并发子线程的执行:
task do_test;
fork
exec1;
exec2;
join_any
fork
exec3;
exec4;
join_none
disable fork; //exec1、exec2、exec3、exec4只要有一个执行完则会跳出fork
endtask
9 不定长赋值
- ‘0:所有位赋0;
- ‘1:所有位赋1;
- ‘z:等于Verilog中的’bz;
- ‘x:等于Verilog中的’bx。
10 循环语句性能增强
举例说明吧,在Verilog中:
module for4a(
input s,
input [31:0] a,
output reg [31:0] y
);
integer i; //独立的迭代变量声明
always@(a or s)
for (i=0;i<32;i=i+1) //显式递增
if(!s)
y[i] = a[i];
else
y[i] = a[31-i];
endmodule
而在SystemVerilog中:
module for4a(
input s,
input [31:0] a,
output reg [31:0] y
);
always_comb
for (int i=0;i<32;i++) //本地迭代变量声明,退出循环后不存在
if(!s) //隐式自动递增
y[i] = a[i];
else
y[i] = a[31-i];
endmodule
SystemVerilog还增加了数组循环方式:
logic value [5];
foreach (value[i]) begin
value[i] = ...;
end
11 断言
当断言内的语句为真时,系统工具会执行相应操作:
assert(ture condition)
12 增强case语句
12.1 case
case
表达式的值是按位匹配,可以处理x和z:
case(select[1:0])
2'b00: result = 0;
2'b01: result = 1;
2'b0x,
2'b0z: result = flag;
default:result = 'x;
endcase
case
语句支持通过inside
关键字设置成员关系,支持通配符和范围:
logic [2:0] status;
always_comb begin
case(status) inside
1,3 : task1(); //match 'b001 and 'b011
3'b0?0,[4:7] : task2(); //match 'b000 'b010 'b0x0 'b0z0
// 'b100 'b101 'b110 'b111
endcase
end
12.2 casez
case
且不关心z和通配符?:
casez(select[3:0])
4'b000?:result = 0;
4'b01??:result = 1;
default:result = 'x;
endcase
综合不支持expression里有通配符的情况:
//Supported by synthesis
casez(status)
3'b?0? :task(); //match 'b000 'b010 'b0z0
endcase
//Not supported by synthesis
casez(3'b?0?)
12.3 casex
case
且不关心x、z和通配符?:
casex(select[3:0])
4'b000x:result = 0;
4'b01xx:result = 1;
default:result = 'x;
endcase
综合不支持expression里有通配符的情况:
//Supported by synthesis
casex(status)
3'b?0? :task(); //match 'b000 'b010 'b0z0 'b0x0
endcase
//Not supported by synthesis
casex(3'b?0?)
12.4 修饰符
SystemVerilog针对以上三个语句增加了两个修饰符,分别是unique
和priority
,语法如下:
unique case (<expression>)
...
endcase
priority case (<expression>)
...
endcase
unique
:- expression同时只能匹配一个selection;
- 一个selection必须存在与之对应的expression;
- 修饰case语句是完整且并行的,各case分支之间相互排斥没有交叠;
unique0
:- 在SystemVerilog 2012版本之后增加了
unique0
; - 当所有的selection不能与expression匹配,此时不会产生Warning,输出保持上一次匹配时的输出;
- 在SystemVerilog 2012版本之后增加了
priority
:- 至少有一个selection需要匹配expression;
- 如果存在多个selection匹配expression,则第一个匹配的分支会被执行。
在综合时,需要考虑full_case和parallel_case属性。
13 void函数
void
用于定义没有返回值的函数。
与Verilog中的任务的异同:
- 相同点:
- 不必从Verilog表达式中被调用,可以像Verilog的任务一样,被独立调用。
- 不同点:
- 不能等待;
- 不能包括延迟;
- 不能包括事件触发;
- 被
always_comb
搜寻到的信号自动加入敏感列表。
舉例說明:
module comb1(
input bit_t a,b,c,
output bit_t [2:1] y
);
always_comb //等价于always@(a,b,c)
orf1(a);
function void orf1; //void函数的行为类似于0延迟的任务
input a; //b和c是隐含的输入
y[1] = a|b|c;
endfunction
always_comb 等价于always@(a)
ort1(a);
task ort1; //Verilog任务
input a; //b和c是隐含的输入
y[2] = a|b|c;
endtask
endmodule
14 包(package)
14.1 定义
为了使多个模块共享用户定义类型的定义,SystemVerilog语言增加了包,与VHDL类似,包在package
和endpackage
之间定义。
包中可以包含的可综合的结构有:
parameter
和localparam
常量定义(这俩在package
里是一样的);const
变量定义;typedef
用户定义类型;- 全自动
task
和function
定义; - 从其他包中
import
语句; - 操作符重载定义。
在包中还可以进行全局变量声明、静态任务定义和静态函数定义,但这些是不可综合的。
包是一个独立的声明空间,不需要包含在Verilog模块中。举例:
package definitions;
parameter VERSION = "1.1";
typedef emnum {ADD,SUB,MUL} opcodes_t;
typedef struct{
logic [31:0] a,b;
opcodes_t opcode;
}instruction_t;
function automatic [31:0] multiplier (input [31:0] a,b);
//用户定义的32位乘法代码从这开始
return a*b;
endfunction
endpackage
包中可能包含parameter
、localparam
和const
等常量定义。在Verilog中,module
的每个实例可以对parameter
常量重新定义,但不能对localparam
直接重定义,但包中的parameter
不能被重定义,因为它不是模块实例的一部分,在包中,parameter
和localparam
是相同的。
14.2 引用包的内容
模块和接口可以用四种方式引用包中的定义和声明:
- 用范围解析操作符直接引用;
- 将包中特定子项导入到模块或接口中;
- 用通配符导入包中的子项到模块或接口中;
- 将包中子项导入到
$unit
声明域中。
相对于Verilog,SystemVerilog增加了作用域解析操作符“::”。这一操作符允许通过包的名称直接引用包,然后选择包中特定的定义或声明。包名和包中子项名用双冒号“::”隔开。例如,SystemVerilog的端口可定义为instruction_t类型(之前的例子),举例:
module ALU(
input definitions::instruction_t IW,
input logic clock,
output logic [31:0] result
);
always_ff@(posedge clock)begin
case(IW.opcode)
definitions::ADD : result <= IW.a + IW.b;
definitions::SUB : result <= IW.a - IW.b;
definitions::MUL : result <= definitions::multiplier(IW.a, IW.b);
endcase
end
endmodule
显式地引用包中的内容有助于提高设计的源代码的可读性,但是当包中的一项或多项需要在模块中多次引用时,每次显示地引用包的名称则太过麻烦,所以希望将包中子项导入到设计块中。
SystemVerilog中允许用import
语句将包中特定子项导入到模块中。当包定义或声明导入到模块或接口中时,该子项在模块或接口内是可见的,就好像它是该模块或接口中的一个局部定义名一样,这样就不需要每次引用包中子项时都显式引用包名。将上面的例子修改为以下代码,使用导入语句使枚举类型的元素成为模块内的局部名称,然后case
语句就可以引用这些名称而不用每次都显式的使用包名:
module ALU(
input definitions::instruction_t IW,
input logic clock,
output logic [31:0] result
);
import definitions::ADD;
import definitions::SUB;
import definitions::MUL;
import definitions::multiplier;
always_comb begin
case(IW.opcode)
ADD : result <= IW.a + IW.b;
SUB : result <= IW.a - IW.b;
MUL : result <= multiplier(IW.a, IW.b);
endcase
end
endmodule
导入枚举类型定义并不导入那个定义使用的元素,在上面的例子中,下面的导入语句不会起作用:
import definations::opcode_t;
这个导入语句会使用户定义的类型opcode_t在模块中可见,但它不会使opcode_t中使用的枚举元素可见。为使元素在模块内成为可见的局部名称,每个枚举元素必须显式导入,当有许多子项需要从包中导入时,使用通配符导入更实用。
SystemVerilog允许包中子项使用通配符导入,而不用指定包中子项名称。通配符记号是一个星号(*),例如:
import definations::*;
通配符导入并不能自动导入包中的所有内容,只有在模块或接口中实际使用的子项才会被真正导入,没被引用的包中的定义和声明不会被导入。
模块或接口内的局部定义和声明优先于通配符导入。包中指定子项名称的导入也优选于通配符导入。从设计者的角度来看,通配符导入只是简单地将包添加到标识符(identifier)搜索规则中。EDA软件将先搜索局部声明(遵循Verilog在模块内的搜索规则),然后在通配符导入的包中搜索,最后将在&unit声明域中搜索。
下面的例子中使用通配符导入语句,实际上是把包添加到标识符搜索路径中。当case
语句引用ADD、SUB和MUL及函数multiplier等枚举元素时,就会在dufinitions包中查找这些名称的定义:
module ALU(
input definitions::instruction_t IW,
input logic clock,
output logic [31:0] result
);
import definitions::*; //通配符导入
always_comb begin
case(IW.opcode)
ADD : result <= IW.a + IW.b;
SUB : result <= IW.a - IW.b;
MUL : result <= multiplier(IW.a, IW.b);
endcase
end
endmodule
在以上例子中,对于模块端口IW,包名仍需显式引用,因为不能在关键字module和模块端口定义之间加入一个import
语句。但是使用$unit
声明域可以避免在端口列表中显式引用包名称。
14.3 可综合性
当模块引用一个包中定义的任务或函数时,综合会复制该任务或函数的功能并把它看作是已经在模块中定义了的。
为了能够综合,包中定义的任务和函数必须声明为automatic,并且不能包含静态变量。因为自动任务或函数的存储区在每次调用时才会分配。这就保证了综合前对包中任务或函数引用的仿真行为与综合后的行为相同。综合后,这些任务或函数的功能就在引用的一个或多个模块中实现。
由于类似的原因,综合不支持包中的变量声明。仿真时,包中的变量会被导入该变量的所有模块共享。一个模块向变量写值,另一个模块看到的就将是新值。这类不通过模块端口传递数据的模块间通信是不可综合的。
15 $unit编译单元声明
15.1 定义
相比Verilog,SystemVerilog增加了编译单元的概念。编译单元是同时编译所有源文件。编译单元为软件工具提供了一种对于整个设计的各个子块单独编译的方法。一个子块可能包含一个或多个module
,这些module
可能包含在一个或多个文件中。设计的子块还可能包含接口块和测试程序块。
SystemVerilog允许在包、模块、接口和程序块的外部进行声明,这些外部声明在“编译单元域”中都是可见的,并且对所有同时编译的模块都是可见的。
编译单元域可以包含:
- 时间单位和精度声明;
- 变量声明;
- net声明;
- 常量声明;
- 用户定义数据类型,使用
typedef
、enu
m或class
; - 任务和函数定义。
举例:
/****************************外部声明****************************/
parameter VERSION = "1.2a" //外部常量
reg resetN = 1; //外部变量,低有效
typedef struct packed{
reg [31:0] address;
reg [31:0] data;
reg [31:0] opcode;
}instruction_word_t;
function automatic int log2(input int n); //外部函数
if(n<=1)return(1);
log2 = 0;
while(n>1)begin
n = n/2;
log2++;
end
return(log2)
endfunction
/****************************模块声明****************************/
//用外部声明定义端口类型
module register (
output instruction_word_t q,
input instruction_word_t d,
input wire clock
);
always_ff@(posedge clock, negedge resetN)
if(!resetN) q <= 0;
else q <= d;
endmodule
外部编译单元域声明不是全局的,只作用于同时编译的源文件,每次编译源文件,就创建一个唯一仅针对此次变异的编译单元域。
15.2 编码指导
- 不要在
$unit
空间进行任何声明,所有的声明都要在命名包内进行; - 必要时可以将包导入到
$unit
中。这在模块或接口的多个端口使用用户自定义类型,而这个类型定义又在包中时非常有用。
15.3 SystemVerilog标识符搜索规则
编译单元域中的任何声明可以再组成编译单元的模块的任何层次引用。
SystemVerilog定义了简单直观的搜索规则来引用标识符:
- 搜索那些按IEEE 1364 Verilog标准定义的局部声明;
- 搜索通配符导入到当前作用域的包中的声明;
- 搜索编译单元域中的声明;
- 搜索设计层次中的声明,遵循IEEE 1364 Verilog搜索规则。
SystemVerilog搜索规则保证了SystemVerilog完全向后兼容。
15.4 源代码顺序
数据标识符和类型标识符必须在引用前声明。未声明的标识符假定为net类型(通常为wire
类型)。EDA工具必须在标识符引用之前找到外部声明,否则,这个名称将被看做未声明的标识符并遵守Verilog隐式类型的规则。举例:
module parity_gen (
input wire [63:0] data
);
assign parity = ^data; //parity是一个隐式局部net
endmodule
reg parity; //因为声明在被parity_gen引用之后出现
//因此外部声明没被模块parity_gen使用
module parity_check(
input wire [63:0] data,
output logic err
);
assign err = (^data != parity); //parity是$unit变量
endmodule
15.5 将包导入$unit的编码规则
SystemVerilog允许将模块端口声明为用户定义类型。如之前的例子:
module ALU(
input definitions::instruction_t IW,
input logic clock,
output logic [31:0] result
);
当许多模块端口都是用户自定义类型时,像上面那样显式的引用包就会显得繁琐。一种可选择的风格是在模块声明之前将包导入到$unit
编译单元域中。这样用户定义类型的定义在SystemVerilog搜索序列中可见,例如:
//将包中特定子项导入到$unit中
import definitions::instruction_t;
module ALU(
input instruction_t IW,
input logic clock,
output logic [31:0] result
);
endmodule
包还可以通过通配符导入到$unit
域中,注意通配符导入实际上不能导入包中所有子项。它只能将包加到SystemVerilog源路径中:
//将包中特定子项导入到$unit中
import definitions::*;
module ALU(
input instruction_t IW,
input logic clock,
output logic [31:0] result
);
endmodule
每个需要保重定义的设计或测试平台都应该将package
文件包含在文件的开始:
`include "xxx.pkg"
如果包已经编译并导入到当前$unit
域中,该文件的编译将被忽略。
16 仿真时间单位和精度
16.1 包含时间单位的时间值
Verilog HDL不在时间值中指定时间的单位,SystemVerilog可以给时间值指定时间单位。允许的时间单位如下表:
单位 | 描述 |
---|---|
s | 秒 |
ms | 毫秒 |
us | 微秒 |
ns | 纳秒 |
ps | 皮秒 |
fs | 飞秒 |
step | 软件工具使用的最小时间单位 |
时间值和单位值之间不允许有空格。
16.2 范围级(scope-level)时间单位和精度
SystemVerilog允许指定局部性的时间单位和精度,作为模块、接口或程序块的一部分,而不是作为软件工具的指令。
在SystemVerilog中,通过使用关键字``timeunit和
timeprecision`进一步增强了时间单位的说明,这些关键字作为模块定义的一部分,用来指定模块内的时间单位和精度信息,且必须在其他任何声明或语句之前、紧随模块、接口或程序的声明之后指定。例如:
module adder(
input wire [63:0] a,b,
output reg [63:0] sum,
output reg carry
);
timeunit 1ns;
timeprecision 10ps;
endmodule
16.3 编译单元的时间单位和精度
SystemVerilog中,时间单位和精度值可以在很多地方指定。SystemVerilog定义了一个搜索法则来确定时间值的单位和精度:
- 如果时间值带单位,则使用指定的单位;
- 否则,使用在模块、接口和程序块内部指定的时间单位和精度;
- 否则,如果模块或接口声明嵌入到其他的模块和接口内,使用父模块接口或接口指定的时间单位和精度。
- 否则,使用模块编译时,有效的`timeacale时间单位和精度;
- 否则,使用在编译单元域中定义的时间单位和精度;
- 否则,使用仿真器默认的时间单位和精度。
举例说明:
timeunit 1ns; //外部声明的时间单位和精度
timeprecision 1ns;
module my_chip(...);
timeprecision 1ps; //局部精度(优先于外部精度)
always@(posedge data_request)begin
#2.5 send_packet;//使用外部单位和局部精度
#3.7ns check_crc; //使用指定的单位
end
task send_packet();
...
endtask
task check_crc();
...
endtask
endmodule
`timescale 1ps/1ps //`timescale指令指定的单位和精度,优于外部声明
module FSM(...)
timeunit 1ns; //局部声明优于`timescale的指定
always@(state)begin
#1.2 case(state) //使用局部声明的单位和`timescale指定的精度
WAITE: #20ps; //使用此处指定的单位
...
endcase
end
endmodule
17 `define增强
17.1 字符串内的宏变量替换
Verilog HDL的`define宏中使用的双引号内的文本变成了文本串,不能在字符串中嵌入宏变量的文本替换宏创建字符串。而SystemVerilog可以进行宏文本字符串内的变量替换,这是通过在形成字符串的引号前加重音号“`”来实现。例如:
`define print(v) $display(`"variable v = %h`" ,v);
`print(data);
17.2 通过宏建立标识符名
Verilog HDL的`define宏不能通过连接两个或多个文本宏来建立一个新标识符,SystemVerilog提供了一个不引入空格的方法,使用两个连接的重音符号“`”,使两个或多个文本宏连接成一个新名字。
在无文本替换的源文件中,是这样声明:
bit d00_bit; wand d00_net = d00_bit;
bit d01_bit; wand d01_net = d01_bit;
... //对每一位都重复60多次
bit d62_bit; wand d62_net = d62_bit;
bit d63_bit; wand d63_net = d63_bit;
使用SystemVerilog对`define的增强,代码可简化为:
`define TWO_STATE_NET(name) bit name``_bit; wand name``_net = name``_bit;
`TWO_STATE_NET(dOO);
`TWO_STATE_NET(dO1);
...
`TWO_STATE_NET(d62);
`TWO_STATE_NET(d63);
18 通配符和三态逻辑
18.1 比较操作符
在SystemVerilog中,比较操作符支持通配符:
- 新加入的比较操作符是
==?
和!=?
; - 允许屏蔽掉比较数中不关心的bit;
- 右操作数中的
x
、z
、?
可以作为通配符不与左操作数进行比较; - 不能将左操作数中的
x
、z
视为通配符;
18.2 case语句支持的通配符
这部分在12章说过了。
18.3 三态门的综合
三态信号必须是net(wire)类型,必须在连续赋值语句中被驱动:
wire D_out;
assign D_out = (x) ? A : 'z; //'z的条件复制意味着综合为一个三态门
告辞