为什么 reg 不是寄存器?Verilog 模块与信号系统入门
💡 先说一个反直觉的事实:Verilog 里的 reg 关键字,和寄存器没有必然关系。
很多人学 Verilog 的第一天就被告知”wire 是线,reg 是寄存器”。这句话害了无数人。实际上,一个声明为 reg 的信号,综合后可能只是一根纯粹的连线——它到底变成什么,取决于你把它放在哪种 always 块里。
如果你正在学 Verilog,搞不清 wire 和 reg 的区别,或者不确定 module 的各个部分都是干什么的——这篇文章就是为你写的。我们会从 Verilog 最基本的积木 module(模块) 讲起,一路讲到 wire/reg 的本质区别和 parameter 的优雅用法。
📌 系列衔接:本文承接上一篇 HDL 入门指南,开始深入 Verilog 的具体语法。如果你还不清楚 Verilog 和 C 语言的本质区别,建议先读上一篇。
目录
- 1. module:Verilog 的基本设计单元
- 2. 端口定义与 I/O 声明
- 3. 参数化设计:parameter 与 localparam
- 4. wire 与 reg:两个最容易被误解的概念
- 5. 常量与逻辑值
- 6. 总结
- 常见问题
- 参考资料
1. module:Verilog 的基本设计单元
Verilog 的一切都围绕 module(模块) 展开。一个 module 就是一个独立的电路单元——它有输入引脚、输出引脚和内部逻辑。你可以把它想象成一颗芯片,或者 PCB 上的一个功能模块。
module led_ctrl (
input wire clk,
input wire rst,
output reg led
);
// 内部逻辑
endmodule
一个 module 由 五个部分 组成:
| 部分 | 说明 | 是否必须 |
|---|---|---|
| 端口定义 | 模块的输入输出”引脚”名称 | 是 |
| I/O 声明 | 每个端口的方向和位宽 | 是 |
| 参数声明 | 用 parameter 定义可配置常量 | 可选 |
| 内部信号声明 | wire 和 reg 类型的中间信号 | 视需要 |
| 功能定义 | assign、always、模块实例化 | 是 |

模块名是它的唯一标识符。好的命名习惯是用功能来命名,比如 uart_tx、fifo_ctrl、led_blink,而不是 module1、test。
💡 工程师手记:刚开始写 Verilog 的时候,我总是把所有逻辑堆在一个大 module 里。后来维护代码时发现,一个 500 行的 module 根本看不懂自己三个月前写的是什么。后来我养成了一个习惯:一个 module 只做一件事,复杂功能通过实例化多个小 module 来组合。代码量可能多了一点,但可读性和可复用性完全不一样。
2. 端口定义与 I/O 声明
模块的端口就是它和外部世界的接口。Verilog 支持三种端口方向:
- input:输入端口,信号从外部流入
- output:输出端口,信号从内部流出
- inout:双向端口,用于双向总线(较少用)
端口声明时需要指定位宽:
module adder (
input wire [7:0] a, // 8位输入
input wire [7:0] b, // 8位输入
output wire [8:0] sum // 9位输出(考虑进位)
);
assign sum = a + b;
endmodule
模块实例化:连接你的积木
当你写好一个 module 后,在其他 module 中使用它叫做实例化(Instantiation)。实例化有两种方式:
方式一:按名称连接(推荐)
adder u_adder (
.a (data_a),
.b (data_b),
.sum (result)
);
方式二:按顺序连接(不推荐)
adder u_adder (data_a, data_b, result);
按名称连接更清晰、不容易出错,尤其当端口很多时。按顺序连接一旦端口顺序变了,所有实例化的地方都要跟着改——这是维护噩梦。
💬 你可能会问:实例化和函数调用有什么区别?
完全不同。C 语言的函数调用是”执行一段代码然后返回”,而 Verilog 的实例化是在电路板上焊接了一颗芯片。它不会被”调用”,而是始终存在、始终工作。一旦实例化,这个电路模块就永远在那里运行。
3. 参数化设计:parameter 与 localparam
硬编码(Hard-code)是需要警惕的习惯。parameter 让你的 module 变得可配置——同一份代码,通过不同的参数值,可以生成不同规格的电路。
基本用法
module counter #(
parameter WIDTH = 8, // 计数器位宽,默认8位
parameter MAX_VAL = 255 // 最大计数值
)(
input wire clk,
input wire rst,
output reg [WIDTH-1:0] count
);
always @(posedge clk) begin
if (rst)
count <= {WIDTH{1'b0}};
else if (count == MAX_VAL)
count <= {WIDTH{1'b0}};
else
count <= count + 1'b1;
end
endmodule
实例化时覆盖参数
// 实例化一个16位计数器
counter #(
.WIDTH(16),
.MAX_VAL(1023)
) u_cnt16 (
.clk (sys_clk),
.rst (sys_rst),
.count (cnt_out)
);
parameter vs localparam
| 特性 | parameter | localparam |
|---|---|---|
| 实例化时可修改 | ✅ 是 | ❌ 否 |
| 作用域 | 模块级 | 模块/生成块内 |
| 典型用途 | 对外暴露的可配置项 | 内部计算的派生常量 |
module uart_tx #(
parameter CLK_FREQ = 100_000_000, // 时钟频率(可配置)
parameter BAUD_RATE = 115200 // 波特率(可配置)
)(
// ...
);
// 分频系数由参数计算得出,外部不应直接修改
localparam CLK_DIV = CLK_FREQ / BAUD_RATE;
endmodule
参数命名规范
- 参数名使用全大写,单词间用下划线分隔(行业惯例)
- 相关参数放在一起声明(如时钟相关、总线相关)
- 避免在参数中使用浮点数(部分综合工具不支持)
💬 你可能会问:
defparam是什么?还需要学吗?
defparam是一种在模块实例化之后、在其他地方修改参数的旧语法。它会让参数声明分散在代码各处,可读性极差,部分综合工具已经弃用。不要用它,统一使用#()语法传递参数。
4. wire 与 reg:两个最容易被误解的概念
这是 Verilog 新手最大的困惑来源。wire 和 reg 的名字极具误导性——reg 不一定对应寄存器,wire 也不仅仅是”线”。
wire(线网类型)
wire 表示模块中的物理连线。它的核心特性是:
- 不能存储值——必须被持续驱动(由
assign或模块输出驱动) - 默认初始值为
z(高阻态,即没有任何驱动) - Verilog 中
input和output端口默认就是 wire 类型
wire [7:0] data_bus; // 8位数据总线
assign data_bus = enable ? data_in : 8'bz; // 必须被驱动
reg(寄存器类型)
reg 表示 always 块内被赋值的信号。它的核心特性是:
- 可以保持值——在下一次赋值之前,保持当前值不变
- 默认初始值为
x(不确定值) - 在
always块中被赋值的信号必须声明为 reg 类型
reg [7:0] counter;
always @(posedge clk) begin
if (rst)
counter <= 8'd0;
else
counter <= counter + 1'b1;
end
关键:reg ≠ 寄存器
这是本文最重要的一节,也是整个 Verilog 学习过程中最容易被误导的地方。
reg 只是一个语法标记,意思是”这个信号会在 always 块中被赋值”——仅此而已。 它和硬件中的寄存器(触发器)没有必然对应关系。reg 综合后到底变成什么,完全取决于它所在的 always 块类型:
- 在
always @(posedge clk)中 → 综合为触发器(Flip-Flop),真正的寄存器 - 在
always @(*)中 → 综合为纯组合逻辑,就是一根连线加逻辑门
// 这个 reg 综合后是触发器
reg q_ff;
always @(posedge clk) begin
q_ff <= d;
end
// 这个 reg 综合后是组合逻辑(MUX),没有触发器
reg y_comb;
always @(*) begin
if (sel) y_comb = a;
else y_comb = b;
end
助记规则:reg 的真正含义是”这个信号会在 always 块中被赋值”,仅此而已。
wire vs reg 速查表
| 特性 | wire | reg |
|---|---|---|
| 赋值场景 | assign 语句、模块输出 | always 块内 |
| 能否存储值 | ❌ 不能 | ✅ 能 |
| 默认初始值 | z(高阻) | x(不确定) |
| 综合结果 | 连线 | 触发器或组合逻辑 |
| input 默认类型 | ✅ 是 | — |
💡 工程师手记:我在学 Verilog 的时候,被 wire 和 reg 折磨了很久。最后帮我理清思路的是一句话:看赋值在哪里发生。如果在
assign里赋值,用 wire;如果在always里赋值,用 reg。就这么简单,不要被名字骗了。
5. 常量与逻辑值
四值逻辑
数字电路是二值的(0 和 1),但 Verilog 为了精确建模,定义了四种逻辑值:
| 值 | 含义 | 说明 |
|---|---|---|
0 | 逻辑低 | 确定的低电平 |
1 | 逻辑高 | 确定的高电平 |
x | 未知值 | 作信号状态时表示未知;作条件判断时表示”不关心” |
z | 高阻态 | 没有任何驱动,信号处于悬空状态 |
注意:实际电路中不存在 x 值,它只是仿真中的概念。实际电路的亚稳态也不等同于 x。
常量表示
Verilog 中常量的格式为:<位宽>'<进制><数值>
8'b1010_0011 // 8位二进制:10100011
8'hA3 // 8位十六进制:等同于上面
8'd163 // 8位十进制:等同于上面
4'b1x0z // 4位,包含不确定和高阻值
几个容易忽略的规则:
- 未声明位宽的常量默认 32 位(与平台相关)
- 未声明进制的常量默认十进制
- 负数的减号必须写在最前面:
-8'd5(而不是8'd-5) - 可以用下划线
_分隔数字提高可读性:32'hDEAD_BEEF
memory 类型
Verilog 通过 reg 数组来建模存储器(Memory):
reg [7:0] mem [0:255]; // 256个8位存储单元
// 读写必须指定地址
mem[0] = 8'hFF; // 写入地址0
data = mem[addr]; // 从指定地址读取
注意:一个 reg 变量可以在一条语句中整体赋值,但 memory 数组不能整体赋值——必须逐个地址操作。
6. 总结
| 要点 | 核心内容 |
|---|---|
| module | Verilog 基本设计单元,一个 module = 一颗芯片 |
| 端口 | input / output / inout,推荐按名称实例化 |
| parameter | 让模块可配置,localparam 用于内部派生常量 |
| wire | 连线,必须被驱动,用于 assign 赋值 |
| reg | always 块中赋值的信号,不一定是寄存器 |
| 四值逻辑 | 0、1、x(未知)、z(高阻) |
给初学者的建议:先记住一条黄金规则——assign 里用 wire,always 里用 reg。这条规则能帮你解决 90% 的信号类型困惑。剩下的 10%,随着你写更多代码自然就明白了。
常见问题
Q1:为什么 Verilog 不直接用一个类型?wire 和 reg 分开太麻烦了。
这确实是 Verilog 的历史包袱。SystemVerilog 引入了
logic类型,可以在大多数场景下替代 wire 和 reg,不用再纠结用哪个。如果你的工具支持 SystemVerilog,推荐直接用logic。
Q2:input 端口一定是 wire 吗?output 端口呢?
input端口只能是 wire 类型(因为外部驱动)。output端口可以是 wire 也可以是 reg,取决于内部是用assign还是always来驱动它。
Q3:parameter 和 `define 有什么区别?
parameter是模块级的,每个实例可以有不同的参数值。`define是全局宏定义,所有文件共享同一个值。parameter 用于模块的可配置设计,define用于全局常量(如总线宽度、编译开关)。
Q4:memory 类型在 FPGA 中会综合成什么?
取决于大小和使用方式。小的 memory 综合为分布式 RAM(用 LUT 实现),大的 memory 综合为 Block RAM(BRAM)。综合工具会自动选择,你也可以用属性(如
(* ram_style = "block" *))来指定。
参考资料
- IEEE Std 1364-2005: IEEE Standard for Verilog Hardware Description Language
- Stuart Sutherland,Verilog HDL Quick Reference Guide
- Xilinx/AMD,UG901: Vivado Design Suite User Guide - Synthesis
- 夏宇闻,《Verilog 数字系统设计教程》
系列导航:本文是「FPGA 入门系列」第 7 篇。
如果这篇文章对你有帮助,欢迎点赞、收藏,也欢迎在评论区交流你对 wire 和 reg 的理解。