初识 Verilog
文章目录
1.1 语言简介
Verilog HDL 是一种硬件描述语言,以文本形式来描述数字系统硬件的结构和行为的语言,用它可以表示逻辑电路图、逻辑表达式,还可以表示数字逻辑系统所完成的逻辑功能。1
怎么理解?
假如说,如果想要实现计数器的一个逻辑功能,那么可以使用我们的 Verilog HDL 语言,通过代码的编写对计数器的逻辑功能进行一个描述。代码编写完成之后使用我们前面安装的 Quartus II 开发软件对 Verilog HDL 语言编写的代码进行一个分析综合、布局布线等一系列操作,然后将生成的网表文件下载到 FPGA 开发板中,那么在 FPGA 当中就会实际生成一个硬件电路,这个硬件电路它实现的功能就是计数器的功能。这就是硬件描述语言。
那么我们常用的硬件描述语言有两个:一个就是我们现在学习使用的 Verilog HDL;另一个就是 VHDL。
那么这两种硬件描述语言都是电子电器工程师协会认定的、国际标准的硬件描述语言。可是为什么我们选择了 Verilog 而没有选择 VHDL 呢?
那么接下来我们对两种语言进行一个简单的对比
Verilog HDL 与 VHDL 对比2
Verilog HDL | VHDL |
---|---|
语法自由、易学易用 | 语法严谨、较难上手 |
适合算法级、门级设计 | 适合系统级设计 |
代码简洁 | 代码冗长 |
发展较快 | 发展缓慢 |
Verilog HDL 和 VHDL 两种硬件描述语言它们都在被广泛地使用。
我们的 Verilog HDL 它是由 C 语言发展而来,它的语法比较自由、易学易用(就是易上手),通过实际操作,大概几周的时间就可以掌握它。而我们的 VHDL 语法比较严谨(比较难上手)。
Verilog HDL 语言它适合算法级、门级的设计;VHDL 适合系统级的设计。还有就是,如果实现相同的逻辑功能,我们的 Verilog HDL 语言它的代码更加的简洁,VHDL 显得冗长。而且 Verilog HDL 语言发展速度比较快,VHDL 发展缓慢;电子电器工程师协会对 Verilog HDL 它的标准,更新频率比较快,VHDL 的更新频率是比较慢的
考虑到易于上手这个问题,所以说我们选择了 Verilog HDL 语言。
Verilog HDL 与 C 语言的对比3
那么上文我们也提到了:Verilog 语言是由 C 语言发展而来的。所以说,它在很多语法上都和 C 语言极其的相似,甚至有些语法是可以通用的。这也是 Verilog 语言易于上手的一个很重要的原因。
但是 Verilog 语言主要用于描述硬件,它和 C 语言这种软件语言,思想是完全不同的;C 语言代码它是顺序执行的,而 Verilog 语言是并行执行。还有就是 C 语言所描述的代码它并不会真正地映射成为最后的硬件电路,而是对内存的操作以及数据的搬移,而我们 Verilog 语言它所描述的代码功能则会真正的生成对应的硬件电路。所以说,这也是 Verilog 语言被称为硬件描述语言的原因。C 语言它和 Verilog 语言之间的关系就像软件与硬件的关系,所以说大家不要混为一谈。
当然了,如果之前有 C 语言基础,对于学习 Verilog 会有所帮助,如果没有 C 语言基础也不需要担心,只需要按照教程走也能很快的掌握这门语言。
1.2 Verilog HDL 基础语法
1.2.1 逻辑值4
首先学习的部分是逻辑值
我们都知道:数字电路之中只有两种逻辑就是 0 和 1;0 表示逻辑低电平,条件为假;1 表示逻辑高电平,条件为真。
那么 Verilog 语言之中除了这两种逻辑值之外还有两种逻辑值就是 Z 高阻态,它表示无驱动;还有就是 X 表示未知逻辑电平。其实在实际电路之中并没有什么 X 值,只存在的是 0、1、Z 三种状态;而且在实际电路中还可能存在亚稳态,它既不是 0 也不是 1,而是一种不稳定的状态。
1.2.2 关键字5
了解了逻辑值之后,我们使用一个 .v
文件,来学习一下 Verilog 语言当中的一些关键字(保留字),关键字一般是小写。
example.v
module example
(
input wire sys_clk , //输入信号
input wire sys_rst_n , //输入信号
inout wire sda , //输入输出信号
output wire po_flag //输出信号
);
//线网型变量
wire [0:0] flag;
//寄存器型变量
reg [7:0] cnt;
//参数
parameter CNT_MAX = 100;
localparam CNT_MAX = 100;
//模块实例化
example
#(
.CNT_MAX (8'd100 ) //实例化时参数可修改
)
example_inst
(
.sys_clk (sys_clk ), //输入信号
.sys_rst_n (sys_rst_n ), //输入信号
.sda (sda ), //输入输出信号
.po_flag (po_flag ) //输出信号
)
example example_inst
(
.sys_clk (sys_clk ), //输入信号
.sys_rst_n (sys_rst_n ), //输入信号
.sda (sda ), //输入输出信号
.po_flag (po_flag ) //输出信号
)
/*
常量
基数表示法
格式:[换算为二进制后位宽的总长度]['][数值进制符号][与数值进制符号对应的数值]
8'd171:位宽是8bit,十进制的171。
[数值进制符号]中如果是[h]则表示十六进制,如果是[o]则表示八进制,如果是[b]则表示二进制,如果是[d]则表示十进制。
8'hab表示8bit的十六进制数ab,
8'o253表示8bit的八进制数253;
8'b1010_1011表示8bit的二进制数1010_1011,下划线增强可读性。
[换算为二进制后位宽的总长度]:可有可无,Veri1og会为常量自动匹配合适的位宽。
当总位宽大于实际位宽,则自动在左边补0,总位宽小于实际位宽,则自动截断左边超出的位数。
'd7与8'd7:表示相同数值,8'd7换算为二进制就是8'b0000_0111,前面5位补0;
2'd7换算为二进制就是2'b11,超过2位宽的部分被截断。
如果直接写参数,例如100,表示位宽为32it的十进制数100。
*/
//阻塞赋值“=”
a = 1;
b = 2;
c = 3;
begin
a = b;
c = a;
end
// a == 2;
// b == 2;
// c == 2;
//非阻塞赋值“<=”
a = 1;
b = 2;
c = 3;
begin
a <= b;
c <= a;
end
// a == 2;
// b == 2;
// c == 1;
//always
always(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
cnt <= 8'd0;
else if (cnt == CNT_MAX)
cnt <= CNT_MAX;
else
cnt <= cnt + 8'd1;
//assign
assign po_flag = (cnt==CNT_MAX)? 1'b1: 1'b0;
endmodule
Verilog 语言之中我们常用的关键字有这么几个
- module——表示模块的开始
- endmodule——表示模块的结束
模块开始之后我们要写入模块名,模块名一般与我们的 .v
文件的名称是一致的
那么模块想要实现它的子功能就需要输入信号和输出信号。输入信号的关键字就是 input
,输出信号的关键字是 output
。当然了,还有一种类型是它可以既做输入信号也可以做输出信号叫做 inout
,比如说 I²C 协议当中的 SDA 这个引脚可以作为指令的输入,也可以作为数据的输出。
1.2.3 变量或参数6
那么只有输入信号是不可能直接生成输出信号的,我们需要通过一些变量或参数对输入信号进行一些处理,从而得到我们的输出信号。那么变量又分为两种类型:一种是线网型变量、一种是寄存器型变量
我们常用的线网型变量就是 wire
,常用的寄存器型变量就是 reg
那么 wire
就可以看作成直接的连接,它在可综合的逻辑中会被映射成为一条真实存在的物理连线。而 reg
具有对某一时间点状态进行保持的功能,如果在可综合的时序逻辑中,它会被映射成为一个真实的物理寄存器。
除了变量之外还有参数。参数的定义可以使用两个关键字:一个是 parameter
、另一个是 localparam
。这两个关键字都可以进行参数的定义,但是它俩是有区别的。
如果我们使用的参数关键字是 parameter
,我们可以在顶层文件通过实例化来对子功能模块中的参数进行修改。例如:我们定义了一个参数 parameter CNT_MAX = 100;
,在顶层文件实例化子功能模块时该参数可修改
//模块实例化
example
#(
.CNT_MAX (8'd233 ) //实例化时参数可修改
)
example_inst
(
.sys_clk (sys_clk ), //输入信号
.sys_rst_n (sys_rst_n ), //输入信号
.sda (sda ), //输入输出信号
.po_flag (po_flag ) //输出信号
)
但是 localparam 就只能在模块内部使用,不能进行实例化
1.2.4 常量7
那么说完了变量和参数之外,我们来说一下常量。
那么常量它的表示方法是基数表示法,格式是 [换算为二进制后位宽的总长度]['][数值进制符号][与数值进制符号对应的数值]
我们举个例子:8'd171
表示的是位宽 8bit,十进制的数字 171。
进制符号有 4 种:如果是 h
就表示十六进制,如果是 o
就表示八进制,b
就表示二进制,还有就是 d
表示十进制。
下面我们使用了十六进制、八进制和二进制都对十进制的 171
进行了表示
8'hAB //表示8bit的十六进制数ab
8'o253 //表示8bit的八进制数253
8'b1010_1011//表示8bit的二进制数1010_1011,下划线增强可读性。
我们的位宽总长度是可有可无的,Verilog 会为常量自动匹配合适的位宽。
如果我们的位宽总长度大于我们的实际位宽,就会自动在左边补上 0;如果位宽总长度小于实际位宽,则自动截断左边超出的位数。什么意思呢?
比如说:我们前面没有位宽定义的 'd7
与有位宽定义的 8'd7
,它们俩表示的数值是一样的,因为它们的进制和数值都是一样的。但是我们的 8'd7
换算为二进制就是 8'b0000_0111
,那么前面五位就自动补 0,实际位宽它是三位宽,超出实际位宽的部分是自动补 0 了。但是如果我们用两位宽来表示十进制的 7:2'd7
,超过两位宽的部分就被自动截断了:2'b11
。
如果说我们不进行总位宽和进制的编写,直接进行参数的编写,比如说:100
,表示位宽是 32bit 的十进制数 100,这是 Verilog 中默认的。
1.2.5 赋值方式8
那么说完变量、参数和常量之后,我们来讲一下赋值方式。
赋值方式有两种:一种是阻塞赋值,一种是非阻塞赋值。阻塞赋值和非阻塞赋值分别是什么意思呢?我们通过一个例子来说明。
比方说我们声明三个变量并赋值
//阻塞赋值“=”
a = 1;
b = 2;
c = 3;
begin
a = b;
c = a;
end
在上面的 begin-end 语句块中我们使用的是阻塞赋值,阻塞赋值可以理解为顺序执行。也就是说,第一句语句执行完毕之后才会执行第二句语句。我们将 b
的值 2
赋值给 a
,那么执行完 a = b;
这条语句之后 a
的值就变成了 2
;如果再将 a
的值赋值给 c
就相当于 a
目前的值是 2
赋值给 c
,c
也变成了 2
。最后得到的结果是 a
等于 2
,b
等于 2
,c
也等于 2
。这就是阻塞赋值,每条语句执行完毕之后,才能进行下一条语句的执行。
非阻塞赋值它里面的语句都是并行执行的,什么意思呢?
//非阻塞赋值“<=”
a = 1;
b = 2;
c = 3;
begin
a <= b;
c <= a;
end
在同一时刻,a <= b;
和 c <= a;
这两条语句是同时执行的。那么 b
的值目前是 2
,现在 a
的值是 1
,在同一时刻将 b
的值赋值给 a
,将 a
的值赋值给 c
。因为是同时执行的,此时 a
的值还是为 1
,所以说执行完毕之后 b
的值给了 a
,a
的值同时给了 c
,但是因为是并行执行,所以说 a
等于 2
,b
等于 2
,c
等于原来 a
的值 1
。
这就是阻塞赋值与非阻塞赋值。
1.2.6 always
语句9
接下来讲一下 always
语句。我们使用前面定义的计数器
//always
always(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
cnt <= 8'd0;
else if (cnt == CNT_MAX)
cnt <= CNT_MAX;
else
cnt <= cnt + 8'd1;
它的意思是:当复位信号有效时,我们给变量 cnt
初值 0
;如果它计数到最大值(我们设定的参数) CNT_MAX
时, 它就一直保持最大值;如果没有计数到最大值,每个时钟周期自加 1。
现阶段如果不能理解也没有关系,在后面会为大家详细讲解。
1.2.7 assign
语句10
接下来是 assign
语句
//assign
assign po_flag = (cnt==CNT_MAX)? 1'b1: 1'b0;
这条语句的意思就是:如果括号内 cnt==CNT_MAX
这个条件满足的话,就将第一个值 1'b1
赋值给 po_flag
这个变量;如果不满足的话,就将第二个值 1'b0
赋值给 po_flag
这个变量。
那么以上就是 Verilog 之中常用的一些关键字,这里只讲解了一小部分,后面会结合实例工程为大家进行讲解。
1.2.8 运算符11
1.2.8.1 算术运算符
接下来是算术运算符,算术运算符常用的就是这五种
+ | 加法,如 assign c=a+b; 即把 a 与 b 的和赋值给 c |
---|---|
- | 减法,如 assign c=a-b; 即把 a 减 b 的差赋值给 c |
***** | 乘法,如 assign c=a*3; 即让 a 和 3 相乘,结果赋值给 c ,但是一般不用乘号 |
/ | 除法,如 assign c=a/2; 即让 a 和 2 相除,结果赋值给 c ,一般也不用除号 |
% | 求模或者称为求余,要求 % 两侧均为整型数据,5%3 的值为 2 ,用在测试文件 |
1.2.8.2 归约运算符和按位运算符
接下来是归约运算符和按位运算符,我们以“&”操作符为例。
“&”操作符有两种用途:既可以作为一元运算符(就是仅有一个参与运算的量),也可以作为二元运算符(表示有两个参与运算的量)。
当“&”作为一元运算符时表示归约与,比如说 &m
就是表示将 m
中所有比特的数相与,最后的结果位宽是 1bit。我们这里举了两个例子
&4'b1111=1&1&1&1=1'b1
&4'b1101=1&1&0&1=1'b0
当“&”操作符作为二元运算符时表示按位与,m&n
表示将 m
的每个比特与 n
的相应比特位进行相与运算,在运算的时候要保证 m
和 n
的比特数要相等,最后的结果要和 m
或者 n
的比特数是相同的。同样我们也举了例子
4'b1010&4'b0101=4'b0000
4'b1101&4'b1111=4'b1101
其他的一些操作符(&**、**^**、**^、|、~|)与“&”操作符是同理的。
1.2.8.3 逻辑运算符
接下来是逻辑运算符
以“&&”操作符为例,它的运算规则是:逻辑与运算符号两边只有真或者假(非零表示真、零表示假),逻辑运算符两边都不为零则结果为 1,否则为 0。我们同样举了一个例子
a=4'ha;
b=4'd0;
c=a&&b;//则c的值为0
||(逻辑或)、==(逻辑相等)、!=(逻辑不等)与“&&”同理。
1.2.8.4 关系运算符
接下来是关系运算符
关系运算符有 4 种:<、>、<=、>=
a<b
(a
小于b
)、a>b
(a
大于b
)、a<=b
(a
小于或等于b
)、a>=
(a
大于或等于b
)
关系运算符一般用在条件判断时使用,比如说我们的 if 的判断语句。如果 if 后面接的判断语句是真的,则返回 1,否则,则返回 0。
1.2.8.5 移位运算符
下面是移位运算符
那么移位运算符就是二元运算符。它有两种符号:一个是左移符号,就是两个小于号 <<;一个是右移符号是两个大于号 >>。它们是将运算符左边的操作数左移或者右移指定的位数,然后用 0 来补充空闲位。我们也举了两个例子
b <= a<<1;//表示将 a 左移一位赋值给 b
b <= a>>2;//表示将 a 右移两位赋值给 b
在应用移位运算符的时候一定要注意它的特性:就是空闲位要用 0 来补充。也就是说,一个二进制数不管原数值是多少,只要一直移位最终都会变为 0
我们这儿举了一个例子:4'b1000>>3
后的结果为 4'b0001
,而 4'b1000>>4
的结果为 4'b0000
。
移位运算符在使用时它可以代替乘除法,左移一位表示乘以 2,右移一位可以表示除以 2,但是我们要注意位宽的拓展。
1.2.8.6 位拼接运算符
接下来是位拼接运算符
位拼接运算符是由一对花括号加逗号组成的——{ , },拼接不同的数据之间用逗号隔开。我们这儿举了一个例子:比如说我想把一个 8bit 的 a,3bit 的 b 和一个 5bit 的 c 按顺序拼接成一个 16bit 的 d,那么它的表示方法就是这种 d = {a,b,c};
。
1.2.8.7 条件运算符
下面是条件运算符
它的符号是这个 ? :,它是一个三元运算符,表示有三个参与运算的量。
条件表达式的一般形式是 表达式1? 表达式2: 表达式3
。执行过程是:当表达式 1 为真,则表达式 2 作为条件表达式的值;否则表达式 3 作为条件表达式的值。例如
a=6;
b=7;
c=(a>b)? a: b;//c的结果为7。
如果 a
大于 b
这个条件满足就将 a
的值赋值给 c
;如果条件不满足就将 b
赋值给 c
。a
大于 b
这个条件是不满足的,所以说将 b
的值赋值给 c
,也就是 c
等于 7
。
条件运算符在前面 assign
语句那里我们讲到过
1.2.8.8 运算符之间的优先级
接下来是优先级
因为各种运算符之间他们要配合使用,所以说要搞清楚他们的优先级
他们总的优先级关系就是:归约运算符 > 算术运算符 > 移位运算符 > 关系运算符 > ==和!= > 按位运算符 > &&和|| > 条件运算符;总的来讲就是:一元运算符 > 二元运算符 > 三元运算符。
如果说你在编写代码的时候不容易理清这些关系,最好的方式就是使用括号增加优先级。
那么以上就是运算符的相关内容。
1.2.9 分支语句12
1.2.9.1 if-else 条件分支语句
下面开始 if-else 条件分支语句的学习
if-else 条件语句是根据判断条件是否满足来确定下一步要执行的操作。它主要有三种形式,第一种是
if (<条件表达式>)
语句或语句块;
在这种形式之中没有 else
选项,这种情况下条件分支语句的执行过程是这样的:如果说 条件表达式
成立,就执行 语句或语句块;
,执行完毕之后退出;如果 条件表达式
不成立,直接退出不进行执行。
这种写法在 always
语句块中表达组合逻辑时会产生 latch,所以说我们不推荐。至于 latch 是什么呢?我们后面会详细讲解。
if-else 的第二种形式是这种(这种是我们常用的)
if (<条件表达式1>)
语句或语句块1;
else if (<条件表达式2>)
语句或语句块2;
……
else
在执行这种形式的 if-else 条件分支语句时,将按照各个分支的排列顺序对各个 条件表达式
进行判断,遇到某一项的 条件表达式
成立时,执行该 条件表达式
下面的语句或语句块;如果所有的 条件表达式
都不成立,执行最后的 else
语句。这种写法是我们常用的写法。
第三种是带有嵌套形式的 if-else 条件分支语句,Verilog HDL 允许 if-else 条件分支语句的嵌套,但是我们不推荐这种写法
if (<条件表达式1>) //外层if语句
if (<条件表达式2>) //内层if语句
语句或语句块1;
else //内层else语句
语句或语句块2;
else //外层else语句
语句或语句块3;
因为嵌套会导致优先级的问题,有可能导致逻辑混乱。所以说我们遇到这种情况时,一般是将其改写成为第二种形式。
那么以上就是 if-else 条件分支语句的内容。
1.2.9.2 case 分支语句
下面开始 case 分支语句的学习
case 分支语句是另一种用来实现多路分支控制的分支语句,它与使用 if-else 条件分支语句相比更加的方便和直观。case 分支语句通常用于有限状态机的描述,我们常用的是这种形式
case (<控制表达式>)
<分支语句1>: 语句块1;
<分支语句2>: 语句块2;
<分支语句3>: 语句块3;
<分支语句n>: 语句块n;
default: 语句块n+1;
endcase
控制表达式
代表着对程序流向进行控制的控制信号,各个 分支语句
则是 控制表达式
某些具体状态的取值。
那么 case
语句的执行过程是:如果 控制表达式
的取值等于 分支语句1
时,执行第一个分支语句所包含的 语句块
,如果 控制表达式
的取值等于 分支语句2
时,执行第二个分支项所包含的 语句块
;那么以此类推。
当执行了某一项分支内的语句之后,它就会跳出 case
语句结构,终止 case
语句的执行。
case
语句中的各个分支项语句它们的取值必须是互不相同的,否则就会出现矛盾。
那么以上就是 case
语句的全部内容。
1.2.10 inout双向端口13
在定义端口列表的时候我们知道输入用
input
,输出用output
,其实还有一种双向端口,我们定义时使用inout
,在后面的实例中会用到,例如 IIC 和 SDRAM 的数据线都是双向端口。定义为inout
的端口表示该端口是双向口,既可以作为数据的输入端口也可以作为数据的输出端口,在 Verilog 中的使用方式如下:module test ( input wire sel , /*输入输出控制信号,sel为1时双向数据总线向外输出数据, sel为0时双向数据总线为高阻态可以向内输入数据*/ input wire data_out , /*由内部模块传来要发送给双向数据总线向外输出的数据*/ inout wire data_bus , //双向数据总线 output wire data_in /*接收双向数据总线从外部输入的数据后输出到其他内部模块*/ ); //data_in:接收双向数据总线从外部输入的数据 assign data_in = data_bus; /*data_bus:sel为1时双向数据总线向外输出数据 sel为0时双向数据总线为高阻态可以向内输入数据*/ assign data_bus = (sel == 1’b1) ? data_out : 1’bz; endmodule
1.2.11 系统函数14
下面我们开始系统函数
Verilog 语言之中预先定义了一些任务和函数,用于完成一些特殊的功能,它们被称为系统函数和系统任务。这些函数大多只能在仿真中使用,方便我们的验证。
时间参数
那么首先是我们的时间参数,它是由时间尺度预编译指令、时间单位/时间精度组成
`timescale 1ns/1ns //时间尺度预编译指令 时间单位/时间精度
时间单位和时间精度是由数值(1、10和100)以及时间单位(s、ms、us、ns、ps和fs)组成的。
时间单位是定义仿真过程中所有与时间相关量的单位。
我们在仿真过程中经常使用“#”加一些数字来表示延时相应时间单位的时间,比如说 #10
,如果在 1ns
这个时间单位之下就表示延时十个单位的时间也就是 10ns
。
时间精度是决定时间相关量的精度及仿真显示的最小刻度,我们来举个例子
`timescale 1ns/10ps //精度0.01,#10.11表示延时10110ps。
如果时间精度是 10ps,它的精度就是 0.01ns,那么就表示我们可以延时更加精确。
当然了,时间单位不能比时间精度小,像下面这种写法就是错误的
`timascale 100ps/1ns
除了时间参数之外,我们还要讲一下仿真中常用的一些系统函数,有下面这些
$display //打印信息,自动换行
$write //打印信息
$strobe //打印信息,自动换行,最后执行
$monitor //监测变量
$stop //暂停仿真
$finish //结束仿真
$time //时间函数
$random //随机函数
$readmemb//读文件函数
下面我们分别讲解一下
1.2.10.1 $display
首先第一个是 $display
,它用于输出打印信息,它的格式是
$display("%b+%b=%d",a,b, c);//格式"%b+%b=%d”格式控制,未指定时默认十进制
//%h或%H:以十六进制的形式输出
//%d或%D:以十进制的形式输出
//%o或%O:以八进制的形式输出
//%b或%B:以二进制的形式输出
它将会打印出引号里边的信息,这句话与 C 语言十分类似。其中引号中如果是 %h
或 %H
表示十六进制形式输出,%d
或 %D
表示十进制形式输出,%o
或 %O
表示八进制形式输出,%b
或 %B
表示二进制形式输出。
$display("%b+%b=%d",a,b, c);
这句话就是将 a
按照二进制形式输出加上 b
按照二进制形式输出,然后等于 c
按照十进制形式输出。
下面我们编写了仿真代码,来看一下实际效果
//a,b,c输出列表,需要输出信息的变量
//每次打印信息后自动换行
`timescale 1ns/1ns
module tb_test();
reg [7:0] a;
reg [7:0] b;
reg [7:0] c;
initial begin
$display("Hello ^v^");
$display("cedtek");
a = 8'd105;//8'b0110_1001
b = 8'd128;//8'b1000_0000
c = a + b;
#100;
$display("%b+%b=%d", a, b, c);
end
endmodule
首先是时间参数
`timescale 1ns/1ns
然后是模块开始 module
,模块名 tb_test
,定义了三个 reg
型的变量 a
b
c
。
这儿我们遇到了一个新的关键字 initial
,他只在仿真文件中使用,是不可综合的,仿真开始只执行一次,执行的是 begin
和 end
之间的语句。
那么这条语句 $display("Hello ^v^");
就是将 Hello ^v^
打印出来,这条 $display("cedtek");
是打印 cedtek
。
然后给 a
赋值,给 b
赋值,将 a
和 b
的和赋值给 c
。延时了 100 个时间单位,然后打印出这个信息 01101001+10000000=233
。仿真执行的效果就是这个
打印出了 Hello ^v^
打印出了 cedtek
,然后是二进制的 a
加二进制的 b
得到了十进制的 c
。
1.2.10.2 $write
第二个是 $write
,他也用于输出和打印信息。$write
和 $display
都是输出打印信息,但是它俩有一个区别就是 $display
可以自动换行,而 $write
必须用换行符也就是我们的 \n
。我们看一下 $write
的使用格式
$write("%b+%b=%d\n", a, b, c);//格式"%b+%b=%d”格式控制,未指定时默认十进制
//%h或%H:以十六进制的形式输出
//%d或%D:以十进制的形式输出
//%o或%O:以八进制的形式输出
//%b或%B:以二进制的形式输出
//\n:换行
编写测试文件
//a,b,c输出列表,需要输出信息的变量
//每次打印信息后自动换行
`timescale 1ns/1ns
module tb_test();
reg [7:0] a;
reg [7:0] b;
reg [7:0] c;
initial begin
$write("Hello ^v^ ");
$write("cedtek\n");
a = 8'd105;//8'b0110_1001
b = 8'd128;//8'b1000_0000
c = a + b;
#100;
$write("%b+%b=%d\n", a, b, c);
end
endmodule
$write
在这儿 $write("Hello ^v^ ");
并没有进行换行,只是添加了一个空格;这儿 $write("cedtek\n");
进行了换行,所以说 Hello ^v^
和 cedtek
应该在一行,中间进行了一个空格。
随后在第二行则打印出了 %b+%b=%d\n
这条信息,我们来看一下效果
确实是这样
上面就是 $write
这个系统函数。
1.2.10.3 $strobe
第三个系统函数 $strobe
也是输出打印信息,但是他只在最后执行,他的使用格式是
$strobe("%b+%b=%d", a, b, c);//格式"%b+%b=%d”格式控制,未指定时默认十进制
//%h或%H:以十六进制的形式输出
//%d或%D:以十进制的形式输出
//%o或%O:以八进制的形式输出
//%b或%B:以二进制的形式输出
我们来看一下代码和实际效果
//a,b,c输出列表,需要输出信息的变量
//每次打印信息后自动换行,触发操作完成后执行
`timescale 1ns/1ns
module tb_test();
reg [7:0] a;
reg [7:0] b;
reg [7:0] c;
initial begin
$strobe("strobe:%b+%b=%d", a, b, c);
a = 8'd105;//8'b0110_1001
$display("display:%b+%b=%d", a, b, c);
b = 8'd128;//8'b1000_0000
c = a + b;
end
endmodule
虽然 $strobe("strobe:%b+%b=%d", a, b, c);
这句话是放在了最前面,但是他还是最后执行的,它的执行是在 c
等于 a
加 b
之后。我们这里还添加了 display 与它对比,$display("display:%b+%b=%d", a, b, c);
这条语句执行的时候,b
和 c
并没有进行赋值,所以说是未知状态。
c
的值在 c = a + b;
这条语句才完成赋值,所以说 $strobe("strobe:%b+%b=%d", a, b, c);
这条语句应该是在 c
赋值语句之后才进行执行的。
1.2.10.4 $monitor
第四个系统函数 $monitor
使用格式是
$monitor("%b+%b=%d", a, b, c);//格式"%b+%b=%d”格式控制,未指定时默认十进制
//%h或%H:以十六进制的形式输出
//%d或%D:以十进制的形式输出
//%o或%O:以八进制的形式输出
//%b或%B:以二进制的形式输出
$monitor
是用于持续监测变量,什么意思呢?
就是这条指令他打印的信息之中任何一个变量发生了变化,他就会执行这条语句一次,我们来看一下效果
//a,b,c输出列表,需要输出信息的变量
//被测变量变化触发打印操作,自动换行
`timescale 1ns/1ns
module tb_test();
reg [7:0] a;
reg [7:0] b;
reg [7:0] c;
initial begin
a = 8'd105;//8'b0110_1001
#100;
b = 8'd128;//8'b1000_0000
#100;
c = a + b;
end
initial $monitor("%b+%b=%d", a, b, c);
endmodule
首先是 a
被赋值了,进行了一次打印;b
被赋值了,进行了一次打印;然后 c
被赋值了之后进行了一次打印。
1.2.10.5 $stop
和 $finish
接下来的系统函数是 $stop
和 $finish
,$stop
是用于暂停仿真,$finish
是用于结束仿真
`timescale 1ns/1ns
module tb_test();
initial begin
$display("Hello ^v^");
$display("cedtek");
#100
$display("Stop simulation!");
$stop; //暂停仿真
$display("Continue simulation!");
#100;
$display("Finish simulation!");
$finish; //结束仿真
end
endmodule
那么在语句中我们是首先打印了 Hello ^v^
,然后换行打印了 cedtek
,延迟了 100 个时间单位,然后打印出了 Stop simulation!
,然后执行暂停仿真系统函数 $stop;
,又打印出了 Continue simulation!
,然后等待了 100 个时间单位,打印出了 Continue simulation!
,然后结束仿真系统函数 $finish;
。
我们来看一下实际仿真情况
打印了 Hello ^v^
、cedtek
、Stop simulation!
,然后暂停了仿真,然后继续仿真,然后结束了仿真。
也就是说第二次仿真的时候,并不是从头开始执行的,而是从我们上次暂停的地方开始执行的
1.2.10.6 $time
和 $random
接下来是时间函数 $time
以及随机数的产生 $random
,我们使用监测函数 $monitor
进行打印
`timescale 1ns/1ns
module tb_test();
reg [3:0] a;
always #10 a = $random;
initial $monitor("a=%d @time %d", a, $time);
endmodule
每当数值 a
与时间函数 time 进行变化时就会打印信息,那么下面就是打印效果
1.2.10.7 $readmemb
和 $readmemh
接下来是文件读取的函数:$readmemb
、$readmemh
以二进制结尾表示读取的是二进制文件函数:$readmemb
以十六进制结尾表示读取的是十六进制文件函数:$readmemh
它的使用格式是这种
$readmemb("<数据文件名>", <存储器名>);
$readmemh("<数据文件名>", <存储器名>);
前面双引号引入的是数据文件的名称,然后逗号后面的是存储器的名称
`timescale 1ns/1ns
module tb_test();
integer i;
reg [7:0] a [16:0];
initial begin
$readmemb("cedtek.txt", a);
for (i=0; i<=16; i=i+1) begin
#10;
$write("%s", a[i]);
end
end
endmodule
我们编写的仿真文件当中是读取的 txt 文件,然后将它赋值给变量 a
,变量 a
是位宽为 8bit、深度为 17 的一个存储器。然后使用 for
语句和 $write
函数将 a
的值不断的打印出来。
我们读取的 txt 文件就是这个
01001000 // H
01100101 // e
01101100 // l
01101100 // l
01101111 // o
00100000 // 空格
01100011 // c
01100101 // e
01100100 // d
01110100 // t
01100101 // e
01101011 // k
00100001 // !
00100000 // 空格
01011110 // ^
01110110 // v
01011110 // ^
Hello 然后空格 cedtek! 然后空格,加上 ^v^
最后打印的结果是这个
那么以上就是基础语法的全部内容
参考资料: