Verilog快速入门
文章目录
Verilog 的基础语法.
Verilog中的module是一种黑盒复用,其输入和输出接口(信号),通过把输入在盒子中执行某些操作来实现某项功能。(类似于OOP语言中类的概念)
下面先介绍几个简单的VerilogHDL程序,从中了解Verilog模块的特性。
module muxtwo (out, a, b, sl);
input a,b,sl;
output out;
reg out;
always@(sloraorb)
if (!sl) out = a;
else out = b;
endmodule
单看代码可能半知半解,但结合原理图就很清楚了。
代码描述的是二选一多路选择器,有输入a、b、sl和输出out
如果sl为0,输出a,否则输出b
这里只是简单引入,对于模块有一个简单的了解
模块描述
Verilog中模块(module)用Verilog语言描述如下:
module top_module//模块名称
(
input a ,
input b ,
output out
);//端口声明
......//功能定义
endmodule//结束一定写endmodule
- 模块以module开始,endmodule结束,建议养成习惯打完module接着下一行来个endmodule
- module后面是模块名,模块名后面的括号里面写用到的信号名称
- input为输入信号
- output为输出信号
- 功能描述在module和endmodule之间
1、I/O说明的格式
输入口: input[信号位宽 : 0] 端口名1;
input[信号位宽 : 0] 端口名2;
…
input[信号位宽 : 0] 端口名i; //共有i个输入口
输出口: output[信号位宽 : 0] 端口名1;
output[信号位宽 : 0] 端口名2;
…
output[信号位宽 : 0] 端口名i; //共有i个输出口
I/O说明也可以放在端口声明语句内
module module_name(input port1 , input port2 , … output port1 , output port2…);
2、内部信号说明
在模块内用到的和与端口有关的wire和reg类型变量的声明。
(数据类型后面会介绍)
如:reg [width : 0] R 变量1,R 变量2…;
wire [width : 0] W 变量1,W 变量2…;
3、功能定义
模块中最重要的部分是逻辑功能定义部分。有以下3种方法可在模块中产生逻辑。
(1) 用“assign"声明语句 如:assign a = b &. c;
(2) 用实例元件 如 : and #2 ul( q , a , b);
这代表用到了一个和and门一样的名为ul的与门,#2代表输出延迟为2个单位时间,q为输出端口,a,b为输入端口,参数的填写必须对应端口参数的定义顺序,每个实例元件名称不得相同
(3) 用“always”块 如 :
always @ (posedge clk or posedge clr);
begin
if(clr) q <= 0;
else if(en) q<= d;
end
采用**“assign"语句是描述组合逻辑最常用的方法之一。而“always" 块既可用于描述组合逻辑**,也可描述时序逻辑。用“always"块 的例子生成了一个带有异步清除端的D触发器。“always”块可用很多种描述手段来表达逻辑,例如上例就用了f.else语句来表达逻辑关系。如按一定的风格来编写“always"块,可以通过综合工具把源代码自动综合成用门级结构表示的组合或时序逻辑电路。
4、并行与串行
上述例子中的assign语句、实例原件和always块是并行执行,但是always块中的语句是串行执行(对于非阻塞赋值是并行),但是对于多个always块来讲,它们之间的关系是并行的。
注意
(1)在Verilog模块中所有过程块(如:initial块、always块)、连续赋值语句.实例引用都是并行的;
(2)它们表示的是一种通过变量名互相连接的关系;
(3) 在同一模块中这三者出现的先后秩序没有关系;
(4)只有连续赋值语句assign和实例引用语句可以独立于过程块而存在于模块的功能定义部分。
逻辑块
always逻辑块
前面提到过, always块可构建组合逻辑块和时序逻辑块,复杂的逻辑操作都需要处于该逻辑块中,如if、case、for等
1、组合逻辑块
module top_module();
always @(*) begin
....
end
endmodule
- always逻辑块中任意信号变化时立即触发,执行begin - end之间的语句,begin和end相当于C/C++中的花括号
- begin - end用于将多条语句组成一个代码块,只有一条语句时可省略
- 例子中的always @ (* )括号中的*是无条件执行,可以换成其他条件
2、时序逻辑块
module top_module();
always @(posedge clk) begin
....
end
endmodule
- posedge: 上升沿触发
- negedge: 下降沿触发
- 示例代表clk信号上升沿触发
generate逻辑块
generate主要结合for循环使用,主要用途有:
- 对向量中的多个位进行重复操作
- 对同一个模块进行多次重复实例化(主要用途)
(1)操作向量(vector)
module top_module(input [7:0] in, output [7:0] out);
genvar i; // genvar i; 也可以定义在generate内部
generate
for(i=0; i<8; i++) begin: bit
assign out[i]=^in[8-1:i];
end
endgenerate
endmodule
(2) 模块重复多次实例化
module top_module(
input a,
input b,
output out
);
genvar i;
generate
for(i=0; i<8; i++) begin: gen_mod_a // gen_mod_a 为每个begin_end的结构的名称
mod_a instance2 (.in1(a), .in2(b), .out(out));
end
endgenerate
endmodule
- 注意:模块多次实例化时必须写每个begin_end结构的名称(gen_mod_a)
- 仿真器会通过gen_mod_a来标识生成结构: gen_mod_a[0],gen_mod_a[1]…
initial块
initial块可以理解为一个初始化块,在initial的起始位置的语句在0时刻即开始执行,之后如果遇到延时,则延时之后执行接下来的语句。
初始块是不可综合的,因此不能将其转化为带有数字元素的硬件原理图。因此初始块除了在仿真中使用外,并没有太大的作用。
如:在仿真文件中初始化各种参数:
initial
begin
sys_clk = 1'b1;
sys_rst_n = 1'b0;
#50
sys_rst_n = 1'b1;
end
注意:
- initial 块在电路中不可综合,故一般不出现在RTL代码中
- initial 一般只在仿真文件中使用
若需要在RTL代码中初始化参数,需要用always块,用initial块会导致错误!
如下所示,在RTL代码中初始化存储器的方式为:
reg [255:0]char_data[4:0];
always@(posedge clk)
begin
char_data[0] <= 256'h0000000000000000000000000000000000000000000000000000000000000000;
char_data[1] <= 256'h0000000000000000000000000000000000000000000000000000000000000000;
char_data[2] <= 256'h0000000000000000000000200000000000000000002000000008000010000000;
char_data[3] <= 256'h000000000000000000000038000000000000020000380000000600001C000000;
char_data[4] <= 256'h02000000000100000000003E0000000000000700003E0000000780001F000000;
end
赋值方式
Verilog 中赋值方式有三种: 连续赋值、阻塞赋值、非阻塞赋值
连续赋值
assign x = y;
- 该语句表示把x和y两个信号进行连接,真实的物理连接!
- 不能在always块中使用
阻塞赋值
// 组合块
always @(*) begin
out1 = a ;
a = b ;
out2 = a ;
end
- 组合always块中用阻塞式赋值
- 执行顺序:按照begin_end语句块中的顺序依次执行,上述输出结果为:out1 = a ,out2 = b
非阻塞赋值
// 时序块
always @(posedge clk) begin
out1 <= a ;
a <= b ;
out2 <= a ;
end
-
时序always块中用非阻塞赋值
-
执行顺序:begin_end中所有语句并行执行,上述输出结果为:out1 = a ,out2 = a
一、基础语法
1.1 标识符
verilog标识符的命名规则和C语言类似
(1)用途: 标识符用于定义常数、变量、信号、端口、参数名、模块名等。
(2)组成: 字母、数字、$、下划线任意组合而成
(3)注意事项:
- 区分大小写(Verilog 和 verilog是不同的)
- 第一个字符只能是字母或下划线(123demo 是非法标识符)
1.2 逻辑值与逻辑运算
1.2.1 逻辑值
Verilog中有4中逻辑值:0、1、x、z
- 0: 低电平
- 1:高电平
- x: 表示状态未知
- z:表示高阻状态
注意:这里的z、x是不区分大小写的(X、Z也可)
1.2.2 逻辑运算
(1) 逻辑运算符:&&(与)、==(相等)、||(或)、!=(不等)
-
如 m&&n : 判断m和n是否全为真**(非0即为真**),真则输出1’b1(1位二进制1),否则输出1’b0
例如:
4’b1010 && 4’b0101 = 1’b1
-
最后输出结果只有1bit
(2) 按位运算符: &、|、、^、&、^、|
- 如 m&n : 是把m的每一位与n的每一位按位做与运算 (4’b1010 & 4’b0101 = 4’b0000)
- 输出结果与m/n的bit数相同
(3) 缩减运算符: &、|、、^、&、^、~|
- 只有一个参量参与运算时( &为一元运算符),表示缩减与,即向量内部进行与运算
&a [3:0] // AND:a[3]&a[2]&a[1]&a [0]相当于(a[3:0]== 4'hf)
|b [3:0] // OR: b[3]|b[2]|b[1]|b [0]相当于(b[3:0]!= 4'h0)
^c [2:0] // XOR:c[2]^c[1]^c[0]
- 即(&4’b0101 = 0 & 1 & 0 & 1 = 1’b0 )
- 最后输出结果只有1bit
1.3 常量的表示方法
与C语言类似,常量主要有:整数型、实数型和字符串型三种
1.3.1 用十进制整数表示整型常量
(1) 正数: 直接写 10 表示位宽为32bit的十进制整数(系统默认)
(2) 负数: -10需要用二进制补码表示,多了一位符号位(1 1010)
(3) 用科学计数法表示:12.345e3 表示 12345
1.3.2 用基数法表示整数型常量
[换算成二进制数后的位宽]’ [数制符号] [与数制对应的值]
(1) 二进制(b): 8’b1000_1100
(2) 十六进制(h): 8’h8c
(3) 八进制(o): 8’o214
(4) 十进制(d): 8’140
注意事项:
- 当表示二进制时,最好每4位写一个下划线以增强可读性:如8’b1000_1100 与8’b10001100 是一样的
- 基数表示法中遇到x时:十六进制表示4个x,八进制中表示3个x
- 当位宽大于二进制位数时左边自动补0,小于二进制数时2从左边截断!
1.3.3 字符串(用双引号)
(1) 每个字符由1个8位的ASCII码值表示,即需要1byte存储空间
(2) 如:“Hello world” 字符串由11个ASCII符号构成,需要11byte存储空间
1.3 .4注释方式
Verilog中注释主要有行注释(//)和块注释(/* … */)两种,表示方法与C语言一致!
// 行注释
/*
块注释
*/
1.4 变量(wire、reg)
Verilog中的变量主要有两种: wire和reg
1.4.1 wire
(1) 线网型(wire): 表示电路间的物理连接,wire定义的变量也可看成信号端口
(2) 当两个wire信号被连续赋值时,在逻辑块中会被映射成真实的物理连线,此时这两个信号端口的变化是同步的!
wire a;
wire b;
assign b = a; // 表示a与b之间生成实际的物理连线
1.4.2 reg
(1) 寄存器型(reg): 表示一个抽象的数据存储单元
(2) reg 具有对某一时间点状态进行保持的功能
1.4.3 用法与注意事项
(1) 在always、initial语句中被赋值的变量(赋值号左边的变量)都是reg型变量
(2) 在assign语句中被赋值的变量,为wire型变量
(3) 未被声明类型的变量默认是wire型
1.5 向量(vector)与 参数(常量)
1.5.1 parameter 参数(常量)
(1) 参数是一种常量,通常出现在module内部,常被用于定义状态、数据位宽等
parameter STATE = 1'b0;
(2) 只作用于声明的那个文件,且可以被灵活改变!
(3) 局部参数localparam,只在本模块中使用
localparam STATE= 1'b1’;
(4) 参数的名称一般为大写,以区分其他变量
1.5.2 向量(vector)
vector(向量),是一组信号的集合,可视为位宽超过1bit 的 wire 信号。
(1) 定义方式:
格式: input/output wire/reg [upper:lower] vector_name
//输入输出型
input [7:0] a , b;
output reg [7:0] out;
//模块中间向量
wire [7:0] c , e;
reg [7:0] d;
- [upper : lower]定义位宽,如[7 : 0]表示位宽为8bit,即upper = 7 ,lower = 0
- vector_name可以一次写多个向量
1.5.3 向量片选
-
a[3 : 0] 取向量a的0~3位
-
b[n] 取向量b的第n位数据
-
c[-1 : -2] 取向量c的最低2位数据
-
c[0 : 3] 取向量c的最高4位数据
多路选择器的应用:实现一个256选1的选择器,sel信号作为选择信号,当sel = 0时选择in[3 : 0],sel = 1时选择in[7 : 4],以此类推
module top_module(
input [1023 : 0] in,
input [7 : 0] sel,
output [3 : 0] out
);
assign out = {in[sel*4+3], in[sel*4+2], in[sel*4+1], in[sel*4+0]};
//这里的{}是拼接运算符
//也可以写成如下写法
//assign out = in[sel * 4 +: 4];
//assign out = in[sel * 4 + 3 -: 4];
endmodule
- 片选信号sel输入为n位二进制数,当参与运算、充当索引时会自动转换成十进制数
- 该题所选取的信号片段为: in[sel * 4 + 3: sel * 4] ,但这不符合Verilog的片选语法规则故应写成:
in[sel * 4 +: 4] 表示索引从sel4开始的高4bit信号
in[sel * 4 + 3 -: 4] 表示索引从sel4+3开始的低4bit信号 - 或是直接选出需要的每一位,再用{ }拼接成新向量:
{in[sel * 4 + 3], in[sel * 4 + 2], in[sel * 4 + 1], in[sel * 4 + 0]}
1.6 三元表达式
(1) 和C语言相同,Verilog也有三元表达式:
condition ? if_true : if_false
当条件为真,表达式值为if_true ,否则表达式值为if_false。
(2) 应用
(sel ? b : a) // 一个二选一MUX,通过sel的值选择a或者b
always @(posedge clk) // 一个T触发器
q <= toggle ? ~q : q;
assign out = ena ? q : 1'bz; // 三态缓冲器
1.7 分支语句(if-else、case)
1.7.1 if-else语句
(1) 最常用的形式:(优势:输出的所有可能都写到,不存在未知电平输出!)
if(<条件表达式 1>)
语句或语句块 1;
else if(<条件表达式 2>)
语句或语句块 2;
………
else
语句或语句块 n;
(2) 不建议使用if-else嵌套,会存在优先级问题,导致逻辑混乱,
(3) 所有if-else语句都应写成(1)的形式!
(4) 根据条件表达式依次比较,存在优先级!
1.7.2 case 语句
(1) 书写形式:
case(<控制表达式>)
<分支语句 1> : 语句块 1;
<分支语句 2> : 语句块 2;
<分支语句 3> : 语句块 3;
………
<分支语句 n> : 语句块 n;
default : 语句块 n+1;
endcase
比较<控制表达式>与<分支语句n>的取值相等则执行对应语句,否则执行default后语句!
(2) 执行完某一分支语句后立即跳出case语句结构,终止case语句执行。
(3) <分支语句n>的取值必须互不相同!
(4) 以encase结束case语句块
(5) 各分支语句间不存在优先级!
(6) 具体应用: 用case语句搭建多路选择器,(以9选1多路选择器为例)
module top_module(
input [15 : 0] a , b , c , d , e , f , g , h , i,
input [3 : 0] sel,
output [15 : 0] out);
always @ (*)begin
case(sel)
4'h0:begin out = a; end
4'h1:begin out = b; end
4'h2:begin out = c; end
4'h3:begin out = d; end
4'h4:begin out = e; end
4'h5:begin out = f; end
4'h6:begin out = g; end
4'h7:begin out = h; end
4'h8:begin out = i; end
default: out = 16'hffff;
endcase
end
endmodule
1.8 for循环语句
(1) 书写形式
integer i;
always @(*) begin
for(i = 0 ; i < n ; i++) begin: for_name
<循环语句>
end
end
- 执行<循环语句>n次
- for_name为每一次循环的名称
1.9 关系运算符(>、<、>=、<=)
- 运算结果为真返回 1
- 运算结果为假返回 0
- 若某个操作数值不定(x),则返回值为 x
2.0 拼接运算符({ , })
2.0.1 拼接
用一对花括号加逗号组成**“{ , }”拼接运算符,逗号隔开的数据按顺序拼接成新数据**!
wire [1 : 0] a;
wire [3 : 0] b;
wire [5 : 0] c;
wire [11 : 0] d = {a , b , c};
2.0.2 通过拼接实现移位
在左边拼接实现右移,右边拼接实现左移!
always @(posedge clk) begin
if(rst_n == 1'b0)
out <= 4'b0;
else
out <= {in , out[3 : 1]}; //右移
end
2.03 连接符中重复多次的操作
语法:{重复次数{vector}}
{3{a}} = {a , a , a}
{3'd5 {2{3'd6}}} //9'b101110110
2.1 移位运算符
移位运算符用于将左边操作数左移或右移指定的位数!移位后空闲为用0填充。
- 左移运算符:<<
如: 4’b1101 << 3 结果为: 4’b1000
- 右移运算符:>>
如:4’b1101 >> 3 结果为:4’b0001
- 移位运算符其它用途:左移一位可以看成是乘以2,右移一位可以看成是除以2
- 移位运算符代替乘除法可以节省资源
3 二进制全加器
- a,b为两个1bit的数据
- cin为上一个加法器的进位输入
- cout为当前加法器的进位输出
- sum = a + b
代码实现:
module add1(
input a,
input b,
input cin,
output sum,
output cout
);
assign sum = a ^ b ^ cin;
assign cout = (a & b) | (a & cin) | (b & cin);
endmodule
4 16进制全加器
16进制全加器如上图所示,它可由上节中16个二进制全加器组合而成。
用Verilog实现16进制全加器代码为:
module add16(
input [15:0] a,
input [15:0] b,
input cin,
output [15:0] sum,
output cout
);
wire [16 : 0] Add_cin;
assign Add_cin = cin;
//用generate多次实例化二进制全加器模块(前面介绍过generate的应用场景)
genvar i;
generate
for(i = 0 ; i < 16 ; i++) begin: gen_add16//gen_add16 为每个begin_end的结构,仿真器会通过他来标识生成结构,gen_add16[0],gen_add16[1]...
add1 Add16(.a(a[i]) , .b(b[i]) , .cin(Add_cin[i]) , .sum(sum[i]) , .cout(Add_cin[i + 1]));
//我们这里把cout传给Add_cin的下一位
end
endgenerate
assign cout = Add_cin[16];//进位输出就是我们的Add_cin[16]
endmodule
5 模块中的参数传递
5.1 定义可传递参数的模块
module counter
//参数定义
#(
parameter COUNT_MAX = 25'd24_999_999,
parameter STATE = 1'b0
)
(
input wire sys_clk,
output reg led_out
);
//代码主体
endmodule
5.2 带参数模块的实例化
counter//调用模块名称
//参数传递
#(
.COUNT_MAX(25'd24_999_999),
.STATE(1'b0)
)
counter1_init//实例化模块名称
(
.sys_clk(sys_clk),
.led_out(led_out)
);
Verilog Testbench的编写
Testbench是把我们的RTL代码在ModelSim中进行仿真验证,通过查看仿真波形和打印信息验证代码逻辑是否正确。下面以3-8译码器说明Testbench代码结构。
Testbench代码的本质是通过模拟输入信号的变化来观察输出信号是否符合设计要求!因此Testbench的核心在于如何模拟输入信号,并把模拟的输入信号输入到功能模块中产生输出信号,如上图所示。解决方案为:
-
通过随机数产生输入信号
-
通过实例化模块把输入信号传入功能模块中
Testbench代码可以自定义,也可以自动生成。
1、自定义3-8译码器Testbench代码
1.1 3-8译码器的RTL代码
module decoder3_8(
input wire [2 : 0] in,
output reg [7 : 0] out
);
// always/initial 模块中只能用 reg型变量
always @ (*)begin
case(in)
3'h0 : out = 8'h01;
3'h1 : out = 8'h02;
3'h2 : out = 8'h04;
3'h3 : out = 8'h08;
3'h4 : out = 8'h10;
3'h5 : out = 8'h20;
3'h6 : out = 8'h40;
3'h7 : out = 8'h80;
default: out = 8'h00;
endcase
end
endmodule
1.2 Testbench代码
`timescale 1ns/1ns // 时间单位及=精度设置
module tb_decoder3_8();
reg [2 : 0] in;
wire [7 : 0] out;
initial begin
in <= 3'h0;
end
// 实现输入信号电平自动变化
always #10 in <= {$random} % 8;
initial begin
$timeformat(-9, 0, "ns", 6);
$monitor("time:%t in:%b out:%b",$time,in,out);
end
// 通过实例化模块把模拟输入信号传入功能模块中
decoder3_8 decoder3_8_inist(
.in(in),
.out(out)
);
endmodule
1.3 代码说明
(1) 时间单位:时间尺度预编译指令 时间单位 / 时间精度
- 定义时间单位: `timescale 1ns/1ns 表示时间单位为1ns,时间精度为1ns
- 时间单位和时间精度由值 1、10、和 100 以及单位 s、ms、us、ns、ps 和 fs 组成
- 时间单位不能比时间精度小
- 仿真过程所有与时间相关量的单位(即1单位的时间)
- 时间精度:决定时间相关量的精度及仿真显示的最小刻度 1ns/1ps 精度为0.001ns
(2) 延时:#数字
- #10 表示延时10个单位的时间,时间单位为1ns时,该语句表示延时10ns
(3) 测试模块的命名:tb_<功能模块名>
- 功能模块名为:decoder3_8, 则对应测试模块名为:tb_decoder3_8
(4) 需要定义模拟的输入/输出信号:
- 输入/输出信号与功能模块中定义的输入/输出信号保持一致
- 输入信号一般定义为 reg 型信号,因为后面需要在always/initial语句块中被赋值
- 输出信号一般为 wire型即可
(5) 输入信号初始化
- 用initial 语句进行初始化,该语句中的代码块只执行一次
- 根据需要初始化为0/1都可
(6) 用always 语句实现信号在仿真过程中的电平变化
- always在仿真过程中将被多次执行
- always #10 in <= {KaTeX parse error: Expected 'EOF', got '}' at position 7: random}̲ % 8; 表示**每隔10个…random}%8 表示随机选取[0,7]之间的数
- in <= {$random} % 8; 在赋值时会自动进行数据类型转换
- always后面最好只有一条语句
(7) 通过实例化模块把模拟输入信号传入功能模块中
- 即把模拟的输入信号传入到功能模块中即可
(8) 通过 $monitor实现变量实时监测
- 实时打印输入输出信号的数值,以便于监测
- $monitor等系统函数要在initial语句块中
2.常用系统函数
testbench文件中编写的系统函数要在initial语句块中!
2.1 $timeformat 设置显示时间的格式
使用格式:
$timeformat(time_unit , decimal_number , suffix_string , minimum_field_width);
- time_unit:时间单位,为一个数字。0(s)、-3(ms)、-6(us)、-9(ns)、-12(ps)、-15(ps),也可使用中间值:-10表示100ps为单位
- decimal_number:打印时间值的小数位
- suffix_string:跟在时间值后的字符串(后缀字符串),一般写对应的时间单位ns、ps等
- minimum_field_wdith:是时间值字符串与后缀字符串合起来的这部分字符串的最小长度
- 主要用途:更改 w r i t e 、 write、 write、display、 s t r o b e 、 strobe、 strobe、monitor、 f w r i t e 、 fwrite、 fwrite、fdisplay、 f s t r o b e 、 fstrobe、 fstrobe、fmonitor等任务在**%t格式**下显示时间的方式
2.2 其他常用系统函数
$display //打印信息,自动换行
$write //打印信息
$strobe //打印信息,自动换行,最后执行
$monitor //监测变量
$stop //暂停仿真
$finish //结束仿真
$time //时间函数
$random //随机函数
$readmemb //读文件函数
用法:
$<系统函数名>("格式控制语句", 变量1, 变量2, 变量3....);
- 格式控制语句中的数据类型顺序与变量的顺序一一对应
格式控制语句中的数据类型顺序与变量的顺序一一对应
相应的格式控制符有:
(2) 常用数据格式
转义字符 | 含义 |
---|---|
\n | 换行符 |
\t | 横向制表符 |
\v | 纵向制表符 |
\\ | 反斜杠\ |
\" | 引号" |
\a | 响铃 |
(2) 常用数据格式
格式 | 说明 |
---|---|
%b / %B | 二进制 |
%d / %D | 十进制 |
%o / %O | 八进制 |
%h / %H | 十六进制 |
%e / %E | 科学计数法显示十进制数 |
%c / %C | ASCII码 |
%t / %T | 时间 |
%s / %S | 字符串 |
%v / %V | 线网型信号强度 |
%m / %M | 层次名 |
2.2.1 以 $display 用于输出、打印信息为例
用法:
$display("Add:%b+%b=%d",a, b, c); //格式“%b+%b=%d” 格式控制,未指定时默认十进制
- %b:表示对应位置显示二进制数
- %d:表示对应位置显示十进制数
- a, b, c 为与格式控制语句中格式控制符顺序对应的需要打印的变量
3 Testbench中调用RTL代码寄存器变量的方法
基本语法:实例化的模块名.变量名
如:在RTL代码中定义了变量 state
module rtl_module(
port
);
// 定义状态寄存器
reg [2:0]state;
endmodule
在testbench中访问的方式为:
`timescale 1ns/ 1ps
module tb_rtl_module(
port
);
// 获取rtl代码中的变量
wire [2:0] state = rtl_module_int1.state;
// 实例化的模块
rtl_module rtl_module_int1 (
.port(port)
)
endmodule
注意:testbench中接收的变量要定义为wire型!
4 在testbench中对输入信号进行模拟要用always块进行封装,使其与系统时钟相关联
如:在RTL代码中定义的输入信号in
module rtl_module(
input wire in
);
//
endmodule
在testbench中对输入信号进行模拟的方式为:
`timescale 1 ns/ 1 ps
module tb_rtl_module();
reg in;
always @(posedge sys_clk or negedge sys_rst_n) begin
if(sys_rst_n == 1'b0)
in<= 1'b0;
else
in<= {$random} % 2;
end
rtl_module rtl_module_int1 (
.in(in)
)
endmodule
注意事项:有严格时序要求时不能采用直接复制的方式,否则仿真图中会出现逻辑混乱的问题!
错误演示:
initial
begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
push_money <= 1'b0;
#30
sys_rst_n <= 1'b1;
#28
push_money <= 1'b1;
#43
push_money <= 1'b0;
#59
push_money <= 1'b1;
#68
push_money <= 1'b0;
end
5 仿真参数重定义
在实际仿真中,我们没有必要按照实际的计数器值进行仿真,这将给仿真调试带来不便,此时我们只需改仿真参数为较小的数,能方便的看出输入输出的关系即可:
方式1参数传递的方式
即在仿真文件的顶层模块中给每个参数传入新的值!如下所示:
rom_ip
#(
.CNT_200MS_MAX (199) ,
.CNT_256_MAX (9) ,
.CNT_KEYFILTER_MAX (9) ,
.CUNT_SCAN_MAX (99) ,
.CNT_SHIFT_MAX (21)
)
rom_ip_inst (
port
);
这种写法要求参数要从最顶层模块传到最底层模块,每一级都需写参数列表,当参数过多时会造成不便!
使用defparam命名
用defparam命令重定义每个子模块中的仿真参数,这样比较直观,且可以对任意子模块的参数进行设置,较为方便。
// 重定义仿真参数的方法
defparam rom_ip_inst.rom_rader_inst.CNT_200MS_MAX = 199;
defparam rom_ip_inst.rom_rader_inst.CNT_256_MAX = 9;
defparam rom_ip_inst.key1_filter_inst1.COUNTER_MAX = 9;
defparam rom_ip_inst.key1_filter_inst2.COUNTER_MAX = 9;
defparam rom_ip_inst.dynamic_seg_main_inst1.CUNT_SCAN_MAX = 99;
defparam rom_ip_inst.dynamic_seg_main_inst1.CNT_SHIFT_MAX = 99;
语法:
defparam 顶层模块实例化名.子模块1实例化名.子模块1的子模块实例化名 = 值;