Verilog 是一种硬件描述语言(HDL),常用于数字电路的设计、模拟和验证,特别是用于 FPGA 和 ASIC 的设计。Verilog 让设计者能够描述和模拟硬件系统的行为和结构,最终将其转化为硬件电路。
一、模块结构
Verilog 中的设计是基于模块的,一个模块可以代表一个电路单元、一个功能模块或者整个系统。模块的基本结构如下:
/* module_name:模块的名称,自定义。 input1, input2:输入端口,表示外部信号进入模块。 output:输出端口,表示模块的输出信号。 */ module module_name (input1, input2, output); // Module code here endmodule
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. 定义了一个D触发器的模块 module D_flip_flop( input CLK, // 时钟信号 input D, // 数据输入 output reg Q // 数据输出 ); always @(posedge CLK) begin Q <= D; // 在时钟的上升沿,将 D 的值传递给 Q end endmodule
module top_module( input clk, // 时钟信号 input d, // 输入信号 d input a, // 输入信号 a output reg q // 输出信号 q ); wire d1; // OR 门的输出信号 wire q1; // 第一个 D 触发器的输出信号 // OR 门逻辑:d1 = a | q assign d1 = a | q; // 第一个 D 触发器实例化 DFF dff1 ( .CLK(clk), // 时钟信号 .D(d1), // 输入信号 d1 .Q(q1) // 输出信号 q1 ); // 第二个 D 触发器实例化 DFF dff2 ( .CLK(clk), // 时钟信号 .D(q1), // 输入信号 q1 .Q(q) // 输出信号 q ); endmodule
3. 原门
原门(Primitive Gates) 是在硬件描述语言(如 Verilog)中用来描述基本逻辑功能的基本模块,它们通常对应于实际硬件中的逻辑门(如与门、或门、非门等)。这些门提供了最基础的逻辑运算,并且是构建更复杂逻辑电路的基础。在 Verilog 中,原门是直接使用 and
、or
、not
等关键字来描述的,不需要额外声明模块。
(1)与门:输出只有在所有输入都为1时才为1。
and (out, in1, in2);
(2)非门:输出为输入的反向。
not (out, in);
(3)或门:输出当至少有一个输入为1时为1。
or (out, in1, in2);
(4)异或门:输出当输入不相同时为1。
xor (out, in1, in2);
(5)同或门:输出当输入相同时为1。
xnor (out, in1, in2);
(6)与非门:输出为与门输出的反向,即输入全为1时输出为0。
nand (out, in1, in2);
(6)或非门:输出为或门输出的反向,即至少一个输入为1时输出为0。
nor (out, in1, in2);
二、数据类型
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 赋值给 X always @(A) // 每当 A 改变时,执行以下语句 Y = X; // 将 X 的值赋给 Y assign 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) begin Q <= 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) begin if (reset) begin q <= 0; end else begin q <= d; end end
(3)
always语句块中只有一条语句时,begin和end可以省略。但是有多个语句时,就不可以省略。assign语句中没有begin和end。
4. 阻塞赋值和非阻塞赋值
阻塞赋值(Blocking Assignment)和非阻塞赋值(Non-blocking Assignment)是两种不同的赋值方式,它们的行为在模拟过程(尤其是时序模拟)中有显著的不同。理解这两种赋值的区别对于正确设计时序逻辑和避免潜在的仿真错误至关重要。
(1)阻塞赋值
阻塞赋值使用
=
操作符,它会在当前语句执行完毕后,立即将值赋给变量,并暂停继续执行后面的代码。换句话说,当前语句阻塞了后续语句的执行,直到赋值操作完成。always @(posedge clk) begin a = b; // 阻塞赋值 c = a; // 等待上一个赋值完成后再执行 end
执行过程:a = b; 会立即执行,并将 b 的值赋给 a。c = a; 会在 a = b; 完成之后才会执行,因此它会取到 a 的新值。
(2)非阻塞赋值
非阻塞赋值使用
<=
操作符,它在赋值时并不会立即改变变量的值。相反,它会在当前时钟周期的末尾(即在所有语句都执行完后)才将值赋给变量。因此,非阻塞赋值允许在同一个时钟周期内并行更新多个变量,而不会互相阻塞。always @(posedge clk) begin a <= 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 wire [3:0] A, // 输入信号 A
input wire [3:0] B, // 输入信号 B
input wire RST, // 复位信号
input wire CLK, // 时钟信号
input wire SEL, // 多路选择器控制信号
output reg [3:0] Q // 输出信号 Q
);
wire [3:0] sum; // 加法器输出
wire [3:0] mux_out; // 多路选择器输出
// 加法器模块:计算 A + B
assign sum = A + B;
// 多路选择器:根据 SEL 选择 sum 或寄存器的反馈值 Q
assign mux_out = (SEL == 1'b0) ? sum : Q;
// D 触发器(寄存器):存储多路选择器的输出
always @(posedge CLK or posedge RST) begin
if (RST) begin
Q <= 4'b0000; // 复位时输出清零
end else begin
Q <= mux_out; // 时钟上升沿更新 Q 的值
end
end
endmodule
六、Verilog常用运算符
这里大部分运算符和C语言一致,但是部分还是有区别(如缩减运算符、拼接复制运算符),由于那些有区别的运算符我们不常用,这里就不做过多解释,详情可以自己查看其他博客。这里我们主要了解下面数据的表达形式即可。
七、高级特性
1. 任务和函数
用于封装重复代码,简化设计。
task multiply; input [3:0] A, B; // 输入 A 和 B,4 位宽 output [7:0] result; // 输出 result,8 位宽 begin result = 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) begin Q <= D; // 在时钟的上升沿,将 D 的值传递给 Q end endmodule // 顶层模块 module top_module( input clk, // 时钟信号 input [7:0] data, // 8 位宽的数据输入 output [7:0] out // 8 位宽的数据输出 ); // 定义循环变量 i genvar 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 位 ); end endgenerate endmodule
八、Verilog三种描述方法
1. 结构化描述(门级描述)
结构化描述通常使用基本的门原语(如与门、或门、非门等)来构建电路。这种描述方式接近硬件实现,能反映出每一个逻辑门的连接关系。
module mux4to1(
output out, // 输出
input i0, i1, i2, i3, // 输入信号
input s1, s0 // 选择线
);
wire s1n, s0n; // 选择线的反向信号
wire y0, y1, y2, y3; // 中间的与门输出
// 反向选择线
not (s1n, s1); // s1n = ~s1
not (s0n, s0); // s0n = ~s0
// 根据选择线选择输入信号
and (y0, i0, s1n, s0n); // y0 = i0 & ~s1 & ~s0
and (y1, i1, s1n, s0); // y1 = i1 & ~s1 & s0
and (y2, i2, s1, s0n); // y2 = i2 & s1 & ~s0
and (y3, i3, s1, s0); // y3 = i3 & s1 & s0
// 最终输出为所有中间信号的或运算
or (out, y0, y1, y2, y3); // out = y0 | y1 | y2 | y3
endmodule
2. 数据流级描述
数据流级描述是通过 assign
语句来描述电路的信号流动和计算逻辑,适合用来描述组合逻辑电路。
module mux4to1(out, i0, i1, i2, i3, s1, s0);
output out;
input i0, i1, i2, i3; // 输入信号
input s1, s0; // 选择线
// 使用条件赋值表达式来实现多路选择器
assign out = (~s1 & ~s0 & i0) | (~s1 & s0 & i1) | (s1 & ~s0 & i2) | (s1 & s0 & i3);
endmodule
3. 行为级描述
行为级描述通过 always
块来描述电路的行为,通常结合 if
或 case
语句,用来定义电路在不同输入条件下的响应。这种方式更接近软件编程,抽象度较高,适合用于描述时序电路。
module mux4to1(out, i0, i1, i2, i3, s1, s0);
output reg out; // 输出为reg类型,因为在always块中赋值
input i0, i1, i2, i3; // 输入信号
input s1, s0; // 选择线
always @(s1 or s0 or i0 or i1 or i2 or i3) begin
case ({s1, s0}) // 根据选择线组合选择对应输入
2'b00: out = i0; // 当s1=0, s0=0时,选择i0
2'b01: out = i1; // 当s1=0, s0=1时,选择i1
2'b10: out = i2; // 当s1=1, s0=0时,选择i2
2'b11: out = i3; // 当s1=1, s0=1时,选择i3
default: out = 1'bx; // 如果没有匹配,输出不确定值(x)
endcase
end
endmodule