4. Verilog语法基础:模块结构、数据类型、运算符、赋值语句(阻塞与非阻塞)
Verilog HDL 作为硬件描述语言,其核心思维与软件编程语言(如 C 语言)有着本质区别:软件代码描述的是“指令序列”,而 Verilog 代码描述的是“硬件电路结构”与“信号随时间变化的逻辑关系”。本章将围绕 Verilog 语法的四大基石——模块结构、数据类型、运算符、赋值语句展开,重点剖析阻塞赋值与非阻塞赋值的本质差异及其对电路综合结果的决定性影响。理解本章内容,是后续进行时序逻辑设计、状态机开发与接口时序控制的前提。
4.1 模块结构:硬件设计的基本单元
在 Verilog 中,模块(Module)是描述硬件功能的基本单位,一个完整的数字系统通常由多个模块通过实例化连接而成。模块结构定义了设计的输入输出接口、内部信号以及逻辑行为,其语法框架如下:
module 模块名 (端口列表);
// 端口方向声明
input wire clk, rst_n;
input wire [7:0] data_in;
output reg [7:0] data_out;
// 内部信号声明
wire [7:0] internal_bus;
reg [3:0] counter;
parameter WIDTH = 8;
// 逻辑功能描述
assign internal_bus = data_in ^ 8'hFF;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
counter <= 4'd0;
else
counter <= counter + 1'b1;
end
always @(*) begin
data_out = internal_bus + counter;
end
endmodule
模块结构的关键要素包括:端口定义(input/output/inout)、数据类型声明(wire/reg/parameter)、逻辑描述(assign 连续赋值与 always 过程块)。端口是模块与外部环境交互的通道,必须明确方向;内部信号用于模块内部的数据传递;参数(parameter)则提供了可配置的常量,增强模块的复用性。在层次化设计中,上层模块通过实例化下层模块来构建复杂系统,每个实例拥有独立的信号空间,这种结构化的设计方法使得大规模 FPGA 工程能够被分解为可独立开发与验证的小模块。
4.2 数据类型:线网与寄存器的本质区别
Verilog 的数据类型主要分为两大类:线网型(wire)与寄存器型(reg)。理解二者的区别是避免语法错误与逻辑错误的基础。
| 特性 | wire(线网) | reg(寄存器) |
|---|---|---|
| 硬件含义 | 物理连线,由门电路输出驱动 | 数据存储单元,在 always 块中赋值 |
| 赋值方式 | assign 连续赋值、模块实例输出 | always 过程块内赋值 |
| 默认值 | 高阻态 z | 未知态 x |
| 综合结果 | 组合逻辑网络 | 触发器或组合逻辑(取决于赋值方式) |
| 使用场景 | 模块间信号连接、组合逻辑输出 | 时序逻辑中的状态变量、计数器 |
此外,向量(Vector)用于表示多位宽数据,声明格式为 [MSB:LSB],例如 wire [7:0] data_bus 表示 8 位宽总线,MSB 为最高位。参数(parameter)用于定义常量,如 parameter IDLE = 2'b00,在模块实例化时可通过 #(...) 进行参数传递,实现不同位宽或不同配置的模块复用。需要特别注意的是,integer 类型通常用于仿真中的循环变量,综合时会被优化为寄存器或线网,不建议在可综合代码中直接使用 integer 进行算术运算。
4.3 运算符:从算术到位操作的完整体系
Verilog 提供了丰富的运算符,覆盖算术、逻辑、位运算、移位、拼接与关系比较等类别。正确使用运算符需要关注位宽扩展与符号性问题。
- 算术运算符:
+、-、*、/、%。加法与减法在综合时会生成加法器/减法器;乘法器会消耗大量逻辑资源,建议使用 DSP 单元或流水线结构;除法与取模运算在 FPGA 中实现代价极高,通常避免直接使用。 - 位运算符:
&(与)、|(或)、^(异或)、~(非)。位运算符对向量的每一位独立操作,是组合逻辑设计的基础。 - 逻辑运算符:
&&、||、!。逻辑运算的结果为 1 位布尔值(0 或 1),常用于条件判断。 - 移位运算符:
<<(左移)、>>(右移)。左移相当于乘以 2 的幂次,右移相当于除以 2 的幂次(无符号数)。 - 拼接运算符:
{a, b}将多个信号按顺序拼接成更宽的向量,常用于构建数据帧或扩展位宽。 - 关系运算符:
>、<、>=、<=、==、!=。比较结果为 1 位布尔值。
signed 声明)扩展时高位补符号位。若未正确声明符号性,可能导致乘法或比较结果与预期不符。建议在运算前统一位宽,或使用 $signed() 系统函数显式转换。
4.4 赋值语句:阻塞与非阻塞的硬件语义
赋值语句是 Verilog 中最容易引发仿真与综合不一致的语法点。阻塞赋值(=)与非阻塞赋值(<=)在仿真执行顺序和综合电路结构上存在本质差异。
阻塞赋值(Blocking Assignment):在 always 块中按书写顺序立即执行,前一条赋值语句完成后才会执行下一条。它描述的是组合逻辑行为,综合后生成纯组合逻辑电路。例如:
always @(*) begin
temp = a & b;
y = temp | c; // 使用 temp 的最新值
end
非阻塞赋值(Non-blocking Assignment):在 always 块中所有语句并行执行,右侧表达式在时间步开始时计算,但变量更新发生在时间步结束时。它描述的是时序逻辑行为,综合后生成触发器电路。例如:
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
q1 <= 1'b0;
q2 <= 1'b0;
end else begin
q1 <= d;
q2 <= q1; // q2 获取的是 q1 的旧值
end
end
在非阻塞赋值中,q2 获取的是时钟沿到来之前 q1 的值,这正好对应了硬件中两个级联触发器的行为:数据在时钟沿同时采样并传递。若将上述代码中的非阻塞赋值改为阻塞赋值,则 q2 会立即获取 q1 更新后的新值,综合出的电路将只有一个触发器,逻辑功能完全改变。
4.5 实战案例:移位寄存器的两种实现对比
为了直观展示阻塞与非阻塞赋值的差异,我们设计一个 4 位右移移位寄存器。该模块在时钟上升沿将数据从 din 移入,并输出移位后的结果。初始现象是:使用阻塞赋值时,仿真波形显示所有位在同一时刻同时更新,无法形成逐位移位的效果。
案例背景与目标:设计一个 4 位右移寄存器,要求每个时钟周期数据右移一位,最高位补 0。输入 din 为 1 位串行数据,输出 dout 为 4 位并行数据。
错误实现(阻塞赋值):
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
dout = 4'b0000;
else begin
dout[3] = din;
dout[2] = dout[3];
dout[1] = dout[2];
dout[0] = dout[1];
end
end
分析过程:在阻塞赋值中,dout[2] = dout[3] 会立即使用 dout[3] 刚刚被赋的新值(即 din),导致所有位在同一个时钟沿被同时更新为 din 的值,移位功能完全失效。仿真波形显示 dout 从 0000 直接跳变为 1111(若 din=1),而非预期的逐位移动。
正确实现(非阻塞赋值):
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
dout <= 4'b0000;
else begin
dout[3] <= din;
dout[2] <= dout[3];
dout[1] <= dout[2];
dout[0] <= dout[1];
end
end
结果对比:采用非阻塞赋值后,所有右侧表达式在时钟沿到来时同时采样旧值,然后在时间步结束时统一更新。因此 dout[2] 获取的是时钟沿之前 dout[3] 的旧值,实现了真正的逐位移位。仿真波形显示 dout 在每个时钟周期依次变为 0001、0010、0100、1000,符合预期。
风险与代价:若在同一个 always 块中混用阻塞与非阻塞赋值,综合工具通常会报错或产生不可预测的电路结构。此外,在组合逻辑 always 块中使用非阻塞赋值,虽然仿真可能正确,但综合时会生成不必要的触发器,浪费资源并增加时序延迟。
4.6 常见误区与设计规范总结
在实际工程中,赋值语句的误用是导致仿真与综合不一致的首要原因。以下是必须遵守的编码规范:
1. 时序逻辑(always @(posedge clk))中,一律使用非阻塞赋值(<=)。
2. 组合逻辑(always @(*) 或 assign)中,一律使用阻塞赋值(=)。
3. 同一个 always 块中,禁止混用阻塞与非阻塞赋值。
4. 不要在多个 always 块中对同一个变量赋值,否则会产生多驱动冲突。
此外,还需注意以下常见误区:
- 误区一:认为阻塞赋值“更快”或“更节省资源”。实际上,综合工具会根据上下文自动推断电路类型,赋值方式只影响仿真语义,不影响综合后的性能。
- 误区二:在组合逻辑中使用非阻塞赋值来“避免竞争”。这会导致综合出锁存器或触发器,而非预期的组合逻辑。
- 误区三:忽略初始状态。时序逻辑必须包含异步复位或同步复位逻辑,否则上电后寄存器处于未知态(x),导致仿真结果不确定。
- 误区四:位宽不匹配。例如将 8 位信号赋值给 4 位寄存器,高位会被截断,造成数据丢失。
总结而言,Verilog 语法的核心在于用代码描述硬件结构,而非编写软件程序。模块结构提供了设计的封装与层次化手段,数据类型决定了信号的物理含义,运算符构成了逻辑表达的基础,而赋值语句则直接决定了电路是组合逻辑还是时序逻辑。掌握这些基础语法,并严格遵循编码规范,是确保 FPGA 设计正确性、可综合性与可维护性的关键。