Veloris.
返回索引
概念基础 2026-02-23

为什么 reg 不是寄存器?Verilog 模块与信号系统入门

8 分钟
2.4k words

为什么 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 的基本设计单元

Verilog 的一切都围绕 module(模块) 展开。一个 module 就是一个独立的电路单元——它有输入引脚、输出引脚和内部逻辑。你可以把它想象成一颗芯片,或者 PCB 上的一个功能模块。

module led_ctrl (
    input  wire clk,
    input  wire rst,
    output reg  led
);
    // 内部逻辑
endmodule

一个 module 由 五个部分 组成:

部分说明是否必须
端口定义模块的输入输出”引脚”名称
I/O 声明每个端口的方向和位宽
参数声明parameter 定义可配置常量可选
内部信号声明wirereg 类型的中间信号视需要
功能定义assignalways、模块实例化

模块结构

模块名是它的唯一标识符。好的命名习惯是用功能来命名,比如 uart_txfifo_ctrlled_blink,而不是 module1test

💡 工程师手记:刚开始写 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

特性parameterlocalparam
实例化时可修改✅ 是❌ 否
作用域模块级模块/生成块内
典型用途对外暴露的可配置项内部计算的派生常量
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 新手最大的困惑来源。wirereg 的名字极具误导性——reg 不一定对应寄存器,wire 也不仅仅是”线”

wire(线网类型)

wire 表示模块中的物理连线。它的核心特性是:

  • 不能存储值——必须被持续驱动(由 assign 或模块输出驱动)
  • 默认初始值为 z(高阻态,即没有任何驱动)
  • Verilog 中 inputoutput 端口默认就是 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 速查表

特性wirereg
赋值场景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. 总结

要点核心内容
moduleVerilog 基本设计单元,一个 module = 一颗芯片
端口input / output / inout,推荐按名称实例化
parameter让模块可配置,localparam 用于内部派生常量
wire连线,必须被驱动,用于 assign 赋值
regalways 块中赋值的信号,不一定是寄存器
四值逻辑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" *))来指定。


参考资料


系列导航:本文是「FPGA 入门系列」第 7 篇。

如果这篇文章对你有帮助,欢迎点赞、收藏,也欢迎在评论区交流你对 wire 和 reg 的理解。

End of file.