Verilog 是一种硬件描述语言(HDL),常用于数字电路的设计、模拟和验证,特别是用于 FPGA 和 ASIC 的设计。Verilog 让设计者能够描述和模拟硬件系统的行为和结构,最终将其转化为硬件电路。
一、模块结构
Verilog 中的设计是基于模块的,一个模块可以代表一个电路单元、一个功能模块或者整个系统。模块的基本结构如下:
/*module_name:模块的名称,自定义。input1, input2:输入端口,表示外部信号进入模块。output:输出端口,表示模块的输出信号。 */ module module_name (input1, input2, output);// Module code hereendmodule
Verilog 中的信号通常分为 输入(input)、输出(output)和 双向(inout)。输入是从外部传入模块的信号,输出是从模块传出的信号。 双向可作为输入或输出,通常用于双向信号,如总线。
1. 模块的结构
举例1:二选一数据选择器的Verilog描述。
module MUX21a(a,b,s,y);input a,b,s;output y;assign y=(s?a:b); endmodule
举例2:边沿D触发器的Verilog描述。
module D_flip_flop (input clk, // 时钟信号input D, // 数据输入output reg Q // 数据输出 );always @(posedge clk) begin // 每当时钟上升沿触发Q <= D; // 非阻塞赋值end endmodule
2. 模块的实例化
是 Verilog 语言中的一个重要概念,它允许你在一个顶层模块中创建和使用其他子模块。通过模块实例化,你可以将设计中的一个功能(即模块)“复制”并在另一个模块中使用,从而实现模块化设计。
//1. 定义了一个与门的模块 module and_gate(input A, // 输入 Ainput B, // 输入 Boutput Y // 输出 Y );assign Y = A & B; // 执行与运算 endmodule//2. 示例化与门模块 module top_module(input a, // 输入 ainput b, // 输入 boutput y // 输出 y );// 实例化 and_gate 模块and_gate gate_test (.A(a), // 将顶层模块的 a 连接到 and_gate 的 A 输入.B(b), // 将顶层模块的 b 连接到 and_gate 的 B 输入.Y(y) // 将 and_gate 的 Y 输出连接到顶层模块的 y 输出);endmodule
二、数据类型
Verilog 支持几种基本数据类型:reg、wire。数据位宽如果不做说明的话,默认是1位。数据类型不做说明的话,默认是wire型的。
1. reg:用于存储数据的类型,通常用于描述存储元素。在 Verilog 中,
reg
类型信号是用来存储数据的,通常用于描述时序逻辑(例如触发器、寄存器)。reg
可以是 单一值 或 多位数组,并且在 Verilog 中支持对数组进行定义和操作。reg [7:0] my_array [0:3]; // 定义一个大小为 4 的数组,每个元素是 8 位 //my_array 是一个包含 4 个元素的数组,每个元素有 8 位宽度。你可以通过下标(0 到 3)访问数组中的各个元素。reg [3:0] matrix [0:2][0:2]; // 定义一个 3x3 的二维数组,每个元素是 4 位
2. wire:表示连线或信号的类型,常用于描述模块之间的连接。
wire
类型是用于连接不同模块之间的信号,表示的是物理连接,它的值通常是由其他信号驱动的,并且不能存储数据。因此,wire
类型不适合用来描述时序逻辑或数组。它的主要用途是将信号从一个地方传递到另一个地方。但是
wire
数组本身是可以定义的,但其用途通常仅限于表示并行的物理连接,而不是存储数据。但需要注意,不能通过assign
或always
语句来修改wire
数组的元素,wire
数组只是用来传递信号。wire [7:0] wire_array [0:3]; // 定义一个 4 元素的 wire 数组,每个元素 8 位宽
●综合示例:
module SimpleExample(input A, output B);wire X; // 定义一个 wire 类型的信号reg Y; // 定义一个 reg 类型的信号,寄存器数据类型assign X = A; // 将输入 A 赋值给 Xalways @(A) // 每当 A 改变时,执行以下语句Y = X; // 将 X 的值赋给 Yassign B = Y; // 将 Y 的值赋给输出 B endmodule
三、时序与组合电路
Verilog 中有两种主要的电路模型:组合电路和时序电路。
1. 组合电路:其输出仅依赖于当前输入,不考虑历史状态。通常使用
assign
语句(连续赋值语句)来描述,连续赋值语句总是处于激活状态,只要操作数有变化马上进行计算和赋值。assign C = A & B; // C 是 A 和 B 的与运算
2. 时序电路:其输出不仅依赖于当前输入,还依赖于过去的状态。通常使用
always
语句和触发器(如flip-flop
)来描述时序逻辑。always @(posedge clk) // 每当时钟上升沿时Q <= D; // 将 D 的值赋给 Q(非阻塞赋值)
四、always语句块
1. 特点
(1)always语句本身不是单一的有意义的一条语句,而是和下面的语句一起构成一个语句块,称之为过程块。
(2)过程块中的赋值语句称过程赋值语句。该语句块不是总处于激活状态,当满足激活条件时才能被执行,否则被挂起。
(3)赋值目标必须是reg型。
2. 激活条件
其由敏感信号条件表决定,当敏感条件满足时过程块被激活。
敏感条件有两种,一种是边沿敏感,一种是电平敏感。
always @ (posedge clk) beginQ <= D; // 时钟上升沿触发 end
(1)边沿敏感
always @(posedge clk) // 时钟的上升沿触发 always @(negedge clk) // 时钟的下降沿触发
(2)电平敏感
电平敏感意味着列表中的某个信号的电平变化(高或低)都会触发过程块的执行。
always @(a or b) // 当 a 或 b 电平变化时触发 begin// 组合逻辑 end
3.
assign
语句的区别(1)
使用
assign
语句赋值时,赋值目标必须是wire类型。使用always语句赋值时,赋值目标必须是reg类型。(2)
always语句块中除了可以使用表达式赋值以外,还可以使用if,case等行为描述语句,还能够描述边沿变化,因此其功能比assign语句更强大(assign语句不能使用if等语句,也不能描述边沿变化)
always @(posedge clk) beginif (reset) beginq <= 0;end else beginq <= d;end end
(3)
always语句块中只有一条语句时,begin和end可以省略。但是有多个语句时,就不可以省略。assign语句中没有begin和end。
4. 阻塞赋值和非阻塞赋值
阻塞赋值(Blocking Assignment)和非阻塞赋值(Non-blocking Assignment)是两种不同的赋值方式,它们的行为在模拟过程(尤其是时序模拟)中有显著的不同。理解这两种赋值的区别对于正确设计时序逻辑和避免潜在的仿真错误至关重要。
(1)阻塞赋值
阻塞赋值使用
=
操作符,它会在当前语句执行完毕后,立即将值赋给变量,并暂停继续执行后面的代码。换句话说,当前语句阻塞了后续语句的执行,直到赋值操作完成。always @(posedge clk) begina = b; // 阻塞赋值c = a; // 等待上一个赋值完成后再执行 end
执行过程:a = b; 会立即执行,并将 b 的值赋给 a。c = a; 会在 a = b; 完成之后才会执行,因此它会取到 a 的新值。
(2)非阻塞赋值
非阻塞赋值使用
<=
操作符,它在赋值时并不会立即改变变量的值。相反,它会在当前时钟周期的末尾(即在所有语句都执行完后)才将值赋给变量。因此,非阻塞赋值允许在同一个时钟周期内并行更新多个变量,而不会互相阻塞。always @(posedge clk) begina <= b; // 非阻塞赋值c <= a; // 同时执行,c 会在下一个时钟周期看到 a 的旧值 end
执行过程:a <= b; 并不会立即改变 a 的值,而是将赋值操作推迟到时钟周期结束时。 c <= a; 也会推迟执行,且由于 a <= b; 是非阻塞的,c 会在下一个时钟周期看到 a 的旧值(即赋值前的值)。
★应用
设计组合电路时常用阻塞赋值;设计时序电路时常用非阻塞赋值。但不是绝对的。 不能在一个always块中混合使用阻塞赋值和非阻塞赋值!!可能会导致一些难以察觉的错误和不一致的行为。
五、常见Verilog结构示例
1. 简单的与门(AND Gate)
module AND_gate (input A, B, // 输入信号output C // 输出信号
);assign C = A & B; // 组合逻辑,A 和 B 的与运算
endmodule
2. D触发器(D Flip-Flop)
module D_flip_flop (input clk, // 时钟信号input D, // 数据输入output reg Q // 数据输出
);always @(posedge clk) begin // 每当时钟上升沿触发Q <= D; // 非阻塞赋值end
endmodule
3. 计数器(Counter)
module Counter (input clk, // 时钟信号input reset, // 重置信号output reg [3:0] count // 4位计数器
);always @(posedge clk or posedge reset) beginif (reset) count <= 4'b0000; // 重置计数器elsecount <= count + 1; // 否则计数end
endmodule
六、Verilog常用运算符
这里大部分运算符和C语言一致,但是部分还是有区别(如缩减运算符、拼接复制运算符),由于那些有区别的运算符我们不常用,这里就不做过多解释,详情可以自己查看其他博客。这里我们主要了解下面数据的表达形式即可。
七、高级特性
1. 任务和函数
用于封装重复代码,简化设计。
task multiply;input [3:0] A, B; // 输入 A 和 B,4 位宽output [7:0] result; // 输出 result,8 位宽beginresult = A * B; // 执行乘法操作end endtask
multiply
是一个task
,它有两个输入参数A
和B
,以及一个输出参数result
。任务内部进行的是 乘法操作,并将结果存储到result
中。你可以在其他地方调用这个task
来执行乘法操作。multiply(A, B, result); // 调用任务 multiply,将 A 和 B 作为输入,result 用来接收输出
2. 生成(Generate)
用于在设计中创建重复结构(例如多个寄存器、加法器等)。是 Verilog 中的一种结构,用来在设计中生成重复的硬件结构或模块。它类似于 for 循环,可以用来实例化多个相同或类似的模块。这对于处理多位数据或多个模块的情况非常有用。
// 定义一个模块 D_flip_flop module D_flip_flop(input clk, // 时钟信号input D, // 数据输入output reg Q // 数据输出 );always @(posedge clk) beginQ <= D; // 在时钟的上升沿,将 D 的值传递给 Qend endmodule// 顶层模块 module top_module(input clk, // 时钟信号input [7:0] data, // 8 位宽的数据输入output [7:0] out // 8 位宽的数据输出 );// 定义循环变量 igenvar i;generate// 循环生成 8 个 D_flip_flop 实例for (i = 0; i < 8; i = i + 1) begin: gen_block// 每个 D_flip_flop 的输入 data[i],输出 out[i]D_flip_flop DFF (.clk(clk), // 时钟信号连接.D(data[i]), // 数据输入连接到 data 的第 i 位.Q(out[i]) // 数据输出连接到 out 的第 i 位);endendgenerateendmodule