第三讲:模块声明、端口定义、assign语句、wire与reg类型

各位同学,欢迎来到Verilog语法基础的上半部分。说实话,很多初学者一上来就被各种语法细节搞晕了。我当年也一样,看着那些wire、reg、assign,完全搞不清它们到底在干嘛。别急,这一讲我们就把这些最基础的东西掰开揉碎了讲清楚。

3.1 模块——Verilog的基本单元

Verilog里,模块(module)就是最小的设计单元。你可以把它想象成一个黑盒子:有输入、有输出,里面装着具体的逻辑电路。

一个模块的基本骨架长这样:

module 模块名 (
    端口列表
);
    // 内部逻辑
endmodule

嗯,就这么简单。每个模块都以module开头,以endmodule结束。我习惯把endmodule写在最后一行,对齐缩进,这样代码结构一目了然。

3.2 端口定义——模块的“手脚”

端口就是模块和外界通信的通道。说白了,就是输入、输出和双向口。

端口方向 关键字 说明
输入 input 数据从外部流入模块
输出 output 数据从模块流出到外部
双向 inout 既可输入也可输出(初学者先别碰)

来看一个完整的例子:

module and_gate (
    input  wire a,      // 输入端口a
    input  wire b,      // 输入端口b
    output wire y       // 输出端口y
);
    assign y = a & b;   // 与门逻辑
endmodule

这里我顺便提一句:端口定义时,方向必须写在最前面。我曾经见过有人把input和wire的顺序写反了,结果编译报错,查了半天才发现是语法顺序问题。

3.3 assign语句——连续赋值

assign是Verilog里最常用的语句之一。它的作用是把一个表达式的结果“连续地”赋值给一个信号。

什么叫“连续地”?就是说,只要等号右边的信号发生变化,左边的信号就会立刻更新。这跟硬件电路的行为完全一致——输入变了,输出马上跟着变。

核心理解:assign描述的是组合逻辑,没有时钟,没有时序,纯粹是输入到输出的映射。

举个例子:

assign sum = a + b;
assign flag = (a > b) ? 1'b1 : 1'b0;

第一行:sum等于a加b,只要a或b变了,sum立刻更新。
第二行:如果a大于b,flag就是1,否则是0。这也是组合逻辑。

我个人习惯把assign语句写在模块的前半部分,这样读代码的人一眼就能看到关键的数据通路。

3.4 wire与reg——两种核心数据类型

这是初学者最容易混淆的地方。我当年也花了很长时间才搞清楚它们的区别。

3.4.1 wire——连线

wire代表物理连线。它只能被assign语句驱动,或者被模块的输入端口驱动。你不能在always块里给wire赋值。

说白了,wire就是一根导线,信号从一端传到另一端,没有存储能力。

wire w1;
assign w1 = a & b;   // 正确:assign驱动wire

wire w2 = a | b;     // 也正确:声明时直接赋值(其实是隐式assign)

3.4.2 reg——寄存器(其实不一定是寄存器)

reg这个名字有点误导人。它不代表一定是寄存器,而是代表一个变量,可以被always块赋值。

关键点:

  • reg只能在always或initial块中被赋值
  • 如果always块描述的是组合逻辑(比如用@(*)),那reg综合出来就是连线
  • 如果always块描述的是时序逻辑(比如用@(posedge clk)),那reg综合出来就是触发器

避坑指南:我曾经犯过一个错误——在always块里给wire赋值,结果编译报错。记住:wire归assign管,reg归always管,别搞混了。

3.4.3 对比总结

特性 wire reg
本质 物理连线 变量(可存储)
赋值方式 assign或声明时赋值 always或initial块内
默认值 高阻态z 不定态x
综合结果 始终是连线 可能是连线或触发器
端口类型 默认类型(input/output默认是wire) 必须显式声明(output reg)

3.5 知识体系总览

下面这张图把本章的核心知识点串起来了。你想想看,模块就像一座房子,端口是门窗,wire是水管电线,reg是储物柜,assign就是水龙头——一开就有水出来。

Verilog模块知识体系(第3章) module 模块 端口定义 数据类型 assign语句 input output wire reg 核心规则 • wire 只能被 assign 驱动,reg 只能在 always 块中赋值 • 端口默认类型是 wire,输出端口若需在 always 中赋值必须声明为 output reg • assign 描述组合逻辑,always 可描述组合或时序逻辑

3.6 实战小例子——用assign实现一个全加器

光说不练假把式。我们写一个1位全加器,把今天学的知识都用上。

module full_adder (
    input  wire a,      // 加数1
    input  wire b,      // 加数2
    input  wire cin,    // 进位输入
    output wire sum,    // 和
    output wire cout    // 进位输出
);

    // 用assign实现组合逻辑
    assign sum  = a ^ b ^ cin;          // 异或:三个数相加的个位
    assign cout = (a & b) | (a & cin) | (b & cin);  // 三个数中至少两个为1

endmodule

这个例子很简单,但包含了模块声明、端口定义、wire类型(端口默认就是wire)、assign语句。你看,全加器的逻辑就是几根线连起来,没有任何时序概念。

小技巧:写assign时,等号右边的表达式尽量用括号明确优先级。我见过有人写assign y = a + b << 1;,结果优先级搞错,仿真和综合结果不一样。加个括号,大家都安心。

3.7 常见错误与避坑

  • 在always块里给wire赋值——编译直接报错。记住:wire只认assign。
  • 忘记声明端口类型——默认是wire,但如果想在always里用输出端口,必须写成output reg。
  • assign左边用了reg——语法错误。assign的左边必须是wire类型。
  • 多个assign驱动同一个wire——综合时会报多驱动错误。一根线只能有一个驱动源。