[FPGA 学习记录] 初识 Verilog

初识 Verilog

封面来源

1.1 语言简介

Verilog HDL 是一种硬件描述语言,以文本形式来描述数字系统硬件的结构和行为的语言,用它可以表示逻辑电路图、逻辑表达式,还可以表示数字逻辑系统所完成的逻辑功能。1

怎么理解?

假如说,如果想要实现计数器的一个逻辑功能,那么可以使用我们的 Verilog HDL 语言,通过代码的编写对计数器逻辑功能进行一个描述。代码编写完成之后使用我们前面安装的 Quartus II 开发软件对 Verilog HDL 语言编写的代码进行一个分析综合、布局布线等一系列操作,然后将生成的网表文件下载到 FPGA 开发板中,那么在 FPGA 当中就会实际生成一个硬件电路,这个硬件电路它实现的功能就是计数器的功能。这就是硬件描述语言。

那么我们常用的硬件描述语言有两个:一个就是我们现在学习使用的 Verilog HDL;另一个就是 VHDL。

那么这两种硬件描述语言都是电子电器工程师协会认定的、国际标准的硬件描述语言。可是为什么我们选择了 Verilog 而没有选择 VHDL 呢?
那么接下来我们对两种语言进行一个简单的对比

Verilog HDL 与 VHDL 对比2

Verilog HDLVHDL
语法自由、易学易用语法严谨、较难上手
适合算法级、门级设计适合系统级设计
代码简洁代码冗长
发展较快发展缓慢

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 文件的名称是一致的

image-20231008092955561

那么模块想要实现它的子功能就需要输入信号和输出信号。输入信号的关键字就是 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 赋值给 cc 也变成了 2。最后得到的结果是 a 等于 2b 等于 2c 也等于 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 的值给了 aa 的值同时给了 c,但是因为是并行执行,所以说 a 等于 2b 等于 2c 等于原来 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; 即把 ab 的和赋值给 c
-减法,如 assign c=a-b; 即把 ab 的差赋值给 c
*****乘法,如 assign c=a*3; 即让 a3 相乘,结果赋值给 c,但是一般不用乘号
/除法,如 assign c=a/2; 即让 a2 相除,结果赋值给 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 的相应比特位进行相与运算,在运算的时候要保证 mn 的比特数要相等,最后的结果要和 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 赋值给 ca 大于 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,他只在仿真文件中使用,是不可综合的,仿真开始只执行一次,执行的是 beginend 之间的语句。
那么这条语句 $display("Hello ^v^"); 就是将 Hello ^v^ 打印出来,这条 $display("cedtek"); 是打印 cedtek
然后给 a 赋值,给 b 赋值,将 ab 的和赋值给 c。延时了 100 个时间单位,然后打印出这个信息 01101001+10000000=233。仿真执行的效果就是这个

image-20231008155402424

打印出了 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 这条信息,我们来看一下效果

image-20231008162725227

确实是这样
上面就是 $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

image-20231008164113357

虽然 $strobe("strobe:%b+%b=%d", a, b, c); 这句话是放在了最前面,但是他还是最后执行的,它的执行是在 c 等于 ab 之后。我们这里还添加了 display 与它对比,$display("display:%b+%b=%d", a, b, c); 这条语句执行的时候,bc 并没有进行赋值,所以说是未知状态。
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

image-20231008171844967

首先是 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;
我们来看一下实际仿真情况

image-20231008173523888

打印了 Hello ^v^cedtekStop 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 进行变化时就会打印信息,那么下面就是打印效果

image-20231008175833259

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^
最后打印的结果是这个

image-20231008182914362

那么以上就是基础语法的全部内容


参考资料:


  1. 04-第四讲-初识Verilog ↩︎

  2. verilog-hdl和vhdl的比较 ↩︎

  3. Verilog HDL和C语言的比较 ↩︎

  4. 逻辑值 ↩︎

  5. 标识符 ↩︎

  6. 变量 ↩︎

  7. 常量 ↩︎

  8. 赋值语句 ↩︎

  9. always ↩︎

  10. assign ↩︎

  11. 关系运算符 ↩︎

  12. if-else与case ↩︎

  13. inout双向端口 ↩︎

  14. Verilog语言中的系统任务和系统函数 ↩︎

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值