Verilog 语法速览:模块、线网、寄存器、always 块到底在声明什么
更新时间:2026-09-05。本文是「集成电路大类 · 芯片设计」**入门层(beginner)**第九篇新正文。前面几篇用了不少 Verilog 片段,这一篇把语法骨架系统过一遍,让你看到任何一段 RTL 都能逐行读懂。核心心法还是那句:你不是在写程序,是在声明电路(见 从代码到门电路)。本文聚焦可综合的 RTL 子集;SystemVerilog、testbench 激励写法分别在 芯片功能验证、仿真是什么 展开。
本文要回答的问题
- 一个
.v文件的基本骨架长什么样? wire和reg到底区别是什么?声明了reg就一定是寄存器吗?- 位宽
[7:0]、常量8'd255、4'b1010这些写法怎么读? assign和always各自描述什么硬件?
一、最小骨架:一个 module 就是一块电路
Verilog 的基本单元是模块(module),对应一块有输入输出的电路(一颗芯片里有什么 里的「模块/IP」层)。骨架固定:
module 模块名 #(
parameter WIDTH = 8 // 可配置参数(编译时可改)
)(
input wire clk, // 时钟输入
input wire rst_n, // 复位输入(低有效)
input wire [WIDTH-1:0] a, // WIDTH 位输入
output reg [WIDTH-1:0] sum // WIDTH 位输出(过程块里赋值 → reg)
);
// 内部连线
wire [WIDTH-1:0] b;
// 电路描述写这里……
endmodulemodule 名字 (...) ;到endmodule之间是模块体;- 端口方向:
input(输入)、output(输出)、inout(双向,多用于 IO pads); - 模块之间通过**实例化(instance)**连接:一个模块里放另一个模块,就像把芯片焊在板子上连线。
二、wire 与 reg:最容易误解的一对
新手常以为 wire = 物理导线、reg = 触发器寄存器。不准确。它们区分的是「用什么方式赋值」:
| wire(线网) | reg(变量) | |
|---|---|---|
| 赋值方式 | 只能被 assign 连续赋值,或被实例端口驱动 | 在 always/initial 过程块里赋值 |
| 语义 | 一个被持续驱动的连接点(谁驱动它就跟着变) | 一个在过程块中被赋值、块间保持值的变量 |
| 一定是寄存器吗 | 不是 | 也不一定! always @(*) 里的 reg 综合成组合逻辑 |
关键澄清:reg 只是「允许在 always 块里赋值」的语法标记,不代表一定有触发器。 是不是寄存器,取决于 always 的敏感列表——always @(posedge clk) 里被赋值的 reg 才综合成触发器;always @(*) 里的 reg 综合成纯组合逻辑(见 组合与时序逻辑)。
三、assign 与 always:两种描述方式
// 1) 连续赋值 assign:描述组合连线,左边必须是 wire
assign b = a + 1'b1; // 加法器,b 随 a 实时变化
assign and_out = x & y; // 与门
// 2) 过程块 always:时序或组合逻辑
always @(posedge clk or negedge rst_n) begin // 时序 → 触发器
if (!rst_n) sum <= {WIDTH{1'b0}};
else sum <= sum + a; // 累加寄存器
end
always @(*) begin // 组合 → 用阻塞 =
if (sel) y = a;
else y = b;
end@(posedge clk)= 敏感于时钟上升沿 → 时序逻辑,用<=;@(*)= 敏感于所有右边输入 → 组合逻辑,用=;- 还有
initial块(上电跑一次,仅用于 testbench 仿真,不可综合)。
四、位宽、常量与向量索引
wire [7:0] data; // 8 位向量,位 7 是最高位(MSB),位 0 最低
reg [3:0] nib;
assign nib = data[3:0]; // 取低 4 位(位选择/部分选择)
assign bit0 = data[0]; // 取单个 bit
// 常量写法:<位宽>'<进制><值>
8'd255 // 8 位十进制 255(= 8'b11111111)
4'b1010 // 4 位二进制 1010
8'hFF // 8 位十六进制 FF
{WIDTH{1'b0}} // 拼接/复制:WIDTH 个 0,常用于复位
{data, nib} // 拼接:把两个向量接成更宽的向量parameter 让模块可参数化(如数据位宽),实例化时可覆盖,是写可复用 IP 的基础(见 IP 核是什么)。
五、一个完整小模块:带使能的累加器(逐行注释)
module acc #(
parameter W = 8 // 位宽参数,默认 8
)(
input wire clk, // 时钟
input wire rst_n, // 异步复位,低有效
input wire en, // 累加使能
input wire [W-1:0] din, // 输入数据
output reg [W-1:0] acc // 累加结果(always 里赋值 → reg)
);
wire [W-1:0] next; // 内部连线:下一值
assign next = acc + din; // 组合:加法器算出下一值
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
acc <= {W{1'b0}}; // 复位:清 0
else if (en)
acc <= next; // 使能:沿上存入 next
// en=0 时 acc 不赋值 → 触发器自然保持(时序逻辑保持不需要 latch)
end
endmodule对照硬件:next 是加法器(组合),acc 是 W 个触发器(时序),en 控制触发器的使能。注意——在时序逻辑里「某条件不赋值」代表触发器保持,是安全的;会出 latch 的是组合逻辑漏分支(见 组合与时序逻辑)。
六、运算符、if/case 与模块实例化
常用运算符(注意位运算和逻辑运算的区别,这是硬件特有的坑):
| 类别 | 写法 | 硬件对应 |
|---|---|---|
| 算术 | + - * / % | 加法器/减法器/乘法器(/、% 综合代价大,尽量避免或用常数) |
| 位运算 | & | ~ ^ | 按位与/或/非/异或门 |
| 逻辑运算 | && || ! | 结果是 1 位真/假(用于条件,别和位运算混用) |
| 比较 | == != < > <= >= | 比较器 |
| 移位 | << >> | 连线重排(常数移位几乎免费,变量移位是桶形移位器) |
| 条件 | ? : 三目 | 直接对应一个 MUX |
特别提醒:== 比较时若含 x/z(不定态/高阻)结果也是 x,仿真里常见「比较一直为假」,可综合代码用 ==、验证里才用 ===(case equality,连 x/z 也比)。
if 与 case 都综合成 MUX(见 从代码到门电路)。case 适合多分支译码(如状态机、地址解码),if-else if 有优先级(先写的条件优先,综合成优先编码器/级联 MUX)。组合块里用它们务必补全分支或先给默认值,否则长出 latch(见 组合与时序逻辑)。
模块实例化——把一个模块放进另一个模块,靠端口连线(类似「按引脚接线」):
// 实例化上面的 acc 模块,取名 u_acc
acc #(.W(16)) u_acc (
.clk (clk),
.rst_n (rst_n),
.en (acc_en),
.din (data_in),
.acc (result)
);.端口名(连线名) 这种「命名关联」写法最安全(顺序无关、不易接错),是 SoC 里把成百上千个模块/IP 拼起来的基本动作(见 IP 核是什么、片上总线 AMBA 入门)。
七、= 与 <=、位宽与有符号:最容易出隐蔽 bug 的三处
阻塞 = 与非阻塞 <= 不是编码风格,而是两种更新语义,用错会让仿真行为和综合出的电路不一致:
- 时序逻辑(
@(posedge clk))一律用非阻塞<=。非阻塞的含义是「先把所有右边用旧值算好,这一沿结束时再统一提交」,因此块内多个寄存器无论书写顺序如何,都像真实触发器那样同时更新,不会出现「上一条刚写、下一条就读到新值」的假顺序。 - 组合逻辑(
@(*))用阻塞=。组合逻辑没有「沿」,语句按求值依赖即时传导,用阻塞才符合门的连续驱动。 - 一条经验口诀:时钟沿里
<=,组合块里=,一个 always 只写一种逻辑(不要在同一个时钟块里混组合和时序)。下面左、右两块交换两个寄存器的值,只有非阻塞写法是真正的「同时交换」:
// 正确:一个沿后 a、b 互换(两个 DFF 交叉连接)
always @(posedge clk) begin a <= b; b <= a; end
// 错误(阻塞):仿真里顺序执行,变成两个都是旧 b,且与硬件直觉相悖
always @(posedge clk) begin a = b; b = a; end位宽不匹配会静默截断或补零,是功能 bug 的高发区。Verilog 表达式按「上下文中最大的位宽」求值后,再按左值宽度截取:左值窄了,高位被无声丢掉(乘法结果是 2n 位、直接截到 n 位就是典型,为什么积要保留 2n 位见 乘法器与桶形移位器);左值宽了,无符号数高位补 0。计数、累加、比较前先在纸上标出每根线该是几位,宁可多留 1~2 位。常量也有位宽:if (state == 1) 里的裸 1 是 32 位整型,规范写法是带上位宽 2'd1 或用 localparam 命名,避免比较和综合告警。
有符号/无符号是两种完全不同的硬件解释。Verilog-2001 之前所有向量默认无符号;要用补码语义须声明 signed,且运算中一旦混入无符号操作数,整个表达式按无符号处理。$signed()/$unsigned() 可显式转换。最常见的坑是有符号比较与算术右移:同一串 bit 按无符号/补码解释大小关系相反,右移时无符号补 0、有符号复制符号位(有符号/无符号比较器在硬件上的判据差异见 数值比较器与奇偶校验)。跨模块传有符号数还要保证两边位宽、符号属性一致,必要时先做符号扩展。
最后补两个工程常用语法:localparam 是不可被外部覆盖的本地常量(状态编码、位宽计算用它,区别于可在实例化时改的 parameter);位宽可以用函数式算,如 parameter W=8; localparam AW=$clog2(W);($clog2 求地址位宽),这是写参数化 FIFO/MUX 的标配。这些细节在 RTL 设计风格 里有更完整的清单。
八、这一篇在知识体系里的位置
- 想把语法对应到门电路 → 从代码到门电路;
- 想掌握组合/时序写法与 latch、
=/<=规则 → 组合与时序逻辑、时钟与复位; - 想系统写可综合 RTL、模块层次与状态机 → RTL 设计入门、FPGA 线的 Verilog 设计基础;
- 想知道怎么写 testbench 激励、看仿真 → 仿真是什么、看波形基本功。
总结
一句话总结:Verilog 的骨架是 module…endmodule,用 input/output/inout 声明端口、用 parameter 做可配置参数;wire 与 reg 的区别不在「导线还是寄存器」,而在赋值方式——wire 只能被 assign 连续赋值或实例端口驱动,reg 则在 always/initial 过程块里赋值,而 reg 是否综合成触发器取决于敏感列表(@(posedge clk) 才是寄存器、@(*) 仍是组合逻辑);assign 描述实时组合连线,always @(posedge clk) 配 <= 描述时序、always @(*) 配 = 描述组合,位宽用 [7:0]、常量用 8'd255/4'b1010/8'hFF、拼接用 {};看懂任何 RTL 的关键,始终是把每一行语法映射回「它声明了哪块电路」。