初探 Verilog 语言

文章发布时间:

最后更新时间:

文章总字数:
2.9k

预计阅读时间:
10 分钟

初探 Verilog 语言

基于《FPGA至简设计原理与应用》进行学习。

综合与仿真

通过一张流程图看看典型 FPGA 的开发流程:
image.png
可以看到,综合和仿真两个词出现的非常多,了解一下综合和仿真是什么。

综合: Verilog 是一种硬件描述语言,就是用代码的形式描述了硬件的功能,而综合就是将 Verilog 代码进行解释并转化为实际的电路来表示,这种实际的电路也被称为网表。

image.png

如图,实现了加法器功能的 Verilog 代码经过综合器解释成了一个加法器电路,VIVADO 等 FPGA 开发工具就是综合器。

仿真: 仿真就是在综合成电路,烧写到 FPGA 前测试代码:

image.png

为了模拟真实的情况,需要编写测试文件,该文件也是用 Verilog 编写,描述了仿真对象的输入激励情况。需要注意的是,这个测试文件最后并不会被综合,故不需要关心该文件是否可以被转成电路。
产生激励信号,将该信号的波形输入到仿真对象,查看仿真对象的输出是否与预期一致。

可综合设计

Verilog 硬件描述语言有类似高级语言的完整语法结构和系统,但是其终究是用来描述硬件电路的,很多语法结构只是以仿真测试为目的,而不能与实际电路对应起来,注意这些语法只能在测试文件中使用,而不能在需要综合的代码中使用。Verilog 语言语法非常多,但是可综合语法非常少,故称“ if-else,case 走天下”。学习过程中重点需要关心的就是这些少量的可综合语法,而仅用于测试的语法每次现查就行。

下面列出了可综合代码中可以使用的代码:
image.png

模块结构

模块各部分概述

模块(module)是 Verilog 的基本描述单位,用于描述某个设计的功能或结构及与其他模块通信的外部端口,在概念上可等同一个器件,与高级语言进行类比就是一个类。模块有五个主要部分:端口定义,参数定义(可选),I/O 说明,内部信号声明,功能定义。

稍等一下,配个环境(基于 vscode 仿真测试 Verilog,界面比较清爽):
https://blog.csdn.net/weixin_60094035/article/details/126532981

然后给出一个示例代码,再分开解析每一部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
module module_name(           //模块以 module 开始,以 endmodule 结束
clk,
rst_n,
dout //声明模块端口
);
parameter DATA_W = 8; //为方便使用,使用 parameter 定义参数

input clk;
input rst_n;
output[DATA_W-1:0] dout; //定义输入输出端口,信号位宽默认为1,[信号位宽-1:0]

reg [DATA_W-1:0] dout_b; //定义信号类型,always 里使用的信号用 reg 类型 //其他一律使用 wire 类型,默认 wire 且位宽1

always @(*) begin //组合逻辑功能描述,后面会细说

end

always @(posedge clk or negedge rst_n) begin //时序逻辑功能描述
if(rst_n == 1'b0) begin

end
else begin

end
end

assign a = b && c // assign 描述 wire 的逻辑,这里描述了两输入与门

endmodule;

模块例化

一个模块能在另外一个模块中被引用,引用的方式就是模块实例化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module and (C,A,B);
input A,B;
output C;
//省略
endmodule
//在下面的“and_2”块中对上一模块“and”进行例化,可以有两种方式:
module and_2(xxxxx)
...
//方式一:实例化时采用位置关联,T3 对应输出端口 C,A 对应 A,B 对应 B。
and A1 (T3, A, B );
//方式二:实例化时采用名字关联,.C 是 and 器件的端口,其与信号 T3 相连
and A2(
.C(T3),
.A(A),
.B(B));

endmodule;

推荐使用的实例化方式是采用名字关联,左边是需要引用的器件的端口看,括号里的是本模块声明了的信号,如果被引用模块有些管脚用不到,括号里就可以为空,表示管脚悬空。

信号类型

主要使用的信号类型有两种,wire 和 reg ,reg 不一定会对应生成寄存器,只需要记住凡是 always 里面使用的信号就用 reg,不在 always 里的就用 wire。

信号位宽

定义信号类型的同时一定要定义信号的位宽,位宽取决于该信号要表示的最大值,2的位宽次方-1。

wire 和 reg 类型

wire 是线网类型,用于对结构化器件之间的物理连线的建模,如器件的管脚,芯片内部器件如与门的输出等。由于线网类型代表的是物理连接线,因此其不存储逻辑值,必须由器件驱动。通常用 assign 进行赋值,如 assign A = B ^ C。相当于将 A 和 B 异或 C 的输出用线绑定。

reg 是最常用的寄存器类型,寄存器类型通常用于对存储单元的描述,如 D 型触发器、ROM等。寄存器类型信号的特点是在某种触发机制下分配了一个值,在下一触发机制到来之前保留原值。但必须注意的是:reg 类型的变量不一定是存储单元,如在 always 语句中进行描述的必须是用 reg类型的变量。

功能描述:组合逻辑

程序语句

assign 语句是连续赋值语句,一般是将一个变量的值不间断地赋值给另一变量,两个变量之间就类似于被导线连在了一起,习惯上当做连线用。

always 语句是条件循环语句,执行机制是通过对一个称为敏感变量表的事件驱动来实现的。基本格式如下代码所示:

1
2
3
4
5
6
7
8
always @(a or b or d or sel) begin
if(sel == 0)begin
c = a + b;
end
else begin
c = a + d;
end
end

先 always @,后面跟着事件。整个 always 的意思是:当敏感事件的条件满足时,就执行一次“程序语句”。敏感事件每满足一次,就执行“程序语句”一次。

当敏感信号特别多的时候可能遗漏,可以直接使用(*),来代表程序语句中所有的条件信号,这种条件信号变化结果立即变化的 always 语句被称为“组合逻辑”。

再看另一种 always 的敏感列表:

1
2
3
4
5
6
7
always@(posedge sysclk or negedge rst_n) begin
if(!rst_n) io_o <= 1'b0;
else begin
if(count == 32'd0) io_o <= ~io_o;
else io_o <= io_o;
end
end

posedge 表示上升沿,也就是说 sysclk 从 0 变成 1 的时候会执行一次程序代码,而 negedge 表示下降沿,当 rst_n 从 1 变成 0 的时候会执行一次,这种信号边沿触发,即信号上升沿或者下降沿才变化的 always,被称为“时序逻辑”,此时信号 sysclk 是时钟。注意:识别信号是不是时钟不是看名称,而是看这个信号放在哪里,只有放在敏感列表并且是边沿触发的才是时钟。而信号 rst_n 是复位信号,同样也不是看名字来判断,而是放在敏感列表中且同样边沿触发,更关键的是“程序语句”首先判断了 rst_n 的值,这表示 rst_n 优先级最高,一般都是用于复位。

注意,组合逻辑器件的赋值采用阻塞赋值“=,时序逻辑器件的赋值语句采用非阻塞赋值“<=”。关于这两种赋值,后面在时序逻辑部分会细说。

数字进制

在 Verilog 中的数字表示方式,最常用的格式是:<位宽>’<基数><数值>,如 4’b1011

后面的各种运算符直接看书就好了,没有必要在这里一一列出,事实上和高级语言里面的各种运算符并没有多大区别。

功能描述:时序逻辑

时钟信号是每隔固定时间上下变化的信号。本次上升沿和上一次上升沿之间占用的时间就是时钟周期,其倒数为时钟频率。高电平占整个时钟周期的时间,被称为占空比。FPGA 中时钟的占空比一般是 50%,即高电平时间和低电平时间一样。其实占空比在 FPGA 内部没有太大的意义,因为 FPGA 使用的是时钟上升沿来触发,设计师们更加关心的是时钟频率。

always 语句

时序逻辑的代码一般有两种:同步复位的时序逻辑和异步复位的时序逻辑。在同步复位的时序逻辑中复位不是立即有效,而在时钟上升沿时复位才有效。其代码结构如下:

1
2
3
4
5
always@(posedge sysclk) begin
if(!rst_n)
else begin
end
end

可以看到敏感事件只有上升沿的时钟,当在敏感列表里面加上下降沿的复位时就成了异步复位的时序逻辑,复位立即有效,与时钟无关,代码如下:

1
2
3
4
5
always@(posedge sysclk) begin
if(!rst_n)
else begin
end
end

建议统一采用异步时钟逻辑,设计时只需考虑是用时序逻辑还是组合逻辑结构来进行代码编写即可。

D 触发器

在数字电路中,触发器(Flip-Flop)是一种存储器件,用于存储和控制二进制数据。它可以在特定的时钟信号下,根据输入信号的状态,在输出端口上保持或改变其状态。在 FPGA 中使用的是最简单的 D 触发器。

image.png

可以将 D 触发器看成一个芯片,该芯片拥有 4 个管脚,其中 3 个是输入管脚:时钟 clk、复位 rst_n、信号 d;1 个是输出管脚:q。

该芯片的功能如下:当给管脚 rst_n 给低电平(复位有效),即赋值为 0 时,输出管脚 q 处于低电平状态。如果管脚 rst_n 为高电平,则观察管脚 clk 的状态,当 clk 信号由 0 变 1 即处于上升沿的时候,将此时 d 的值赋给 q。用代码表示如下:

1
2
3
4
5
6
7
8
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)begin
q <= 0;
end
else begin
q <= d
end
end

用波形图表示如下:

image.png

注意,复位信号是在系统开始时刻或者出现异常时才使用,一般上电后就不会再次进行复位,也可以认为复位是一种特殊情况。

阻塞赋值和非阻塞赋值

很简单,一句话表明,阻塞赋值用等号,一行赋完值才到下一行赋值,非阻塞赋值用小于等于号,多行赋值语句同时赋值,根据规范要求,组合逻辑中应使用阻塞赋值“=”,时序逻辑中应使用非阻塞赋值“<=”。可以将这个规则牢牢记住,按照这一规则进行设计绝对不会发生错误。

1
2
3
4
5
6
7
8
9
10
begin
c = a;
d = c + a;
end
//阻塞赋值,假设c=0,d=0,a=1,则赋完值c=1,d=2
else begin
c <= a;
d <= c + a;
end
//非阻塞赋值,假设c=0,d=0,a=1,则赋完值c=1,d=1

Verilog 语言的基本语法就简单了解到这里,以后会通过更多项目继续熟悉 Verilog,以及尝试仿真综合过程。