第三讲:模块声明、端口定义、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就是水龙头——一开就有水出来。
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——综合时会报多驱动错误。一根线只能有一个驱动源。