第三讲:Verilog基础(一)——模块与端口、组合逻辑、常用数据类型
各位同学,欢迎来到第三讲。今天咱们聊聊Verilog最基础的东西——模块、端口、组合逻辑,还有那两个绕不开的数据类型:wire和reg。
说实话,我刚开始学Verilog的时候,觉得这些东西太简单了,不就是画个框、连根线嘛。结果后来在项目中吃过亏,才明白基础不牢,地动山摇。今天我就把当年踩过的坑,还有后来总结的经验,一并分享给你们。
3.1 模块与端口——芯片设计的“积木块”
Verilog里,模块(module)就是最基本的设计单元。你可以把它想象成一块乐高积木——每个模块有自己的功能,有自己的输入输出,然后多个模块拼在一起,就成了一个完整的系统。
我个人习惯,写模块之前先在纸上画个框图。把输入信号、输出信号、双向信号都标清楚。别小看这一步,我见过太多人上来就写代码,写到一半发现端口少了,或者方向搞反了,回头改起来特别痛苦。
看一个最简单的例子——一个与门模块:
module and_gate (
input wire a, // 输入端口a
input wire b, // 输入端口b
output wire y // 输出端口y
);
assign y = a & b; // 组合逻辑:y等于a与b
endmodule
这里有几个要点:
- module 和 endmodule 成对出现,就像C语言里的花括号
- 端口声明有三种方向:
input、output、inout(双向,用的少,但很重要) - 每个端口都要指定数据类型,默认是wire,但我建议显式写出来,代码更清晰
3.2 组合逻辑——两种写法,一个目的
组合逻辑,说白了就是输出只取决于当前输入,没有记忆功能。你给它什么输入,它立马给你什么输出,不拖泥带水。
Verilog里实现组合逻辑,主要有两种方式:assign 和 always@(*)。这两种我都用,但场景不同。
3.2.1 assign——连续赋值,简单粗暴
assign 适合描述简单的逻辑关系,比如与、或、非、加法器、多路选择器。它的特点是:等号左边必须是wire类型。
// 一个2选1多路选择器
module mux2to1 (
input wire [3:0] data0,
input wire [3:0] data1,
input wire sel,
output wire [3:0] out
);
assign out = sel ? data1 : data0;
endmodule
你看,一行assign就搞定了。简洁、直观、不容易出错。我个人习惯,能用assign的地方绝不用always,因为assign的仿真行为更确定,不容易出现“仿真与综合不一致”的坑。
3.2.2 always@(*)——敏感列表,自动推导
当逻辑变得复杂,比如需要if-else、case语句时,就得用 always@(*) 了。这里的 * 表示“所有输入信号”,综合工具会自动推导敏感列表。
嗯,这里要注意:always@(*) 里赋值的变量,必须是reg类型。但别误会,这里的reg并不代表寄存器,它只是一个语法要求。在组合逻辑中,reg综合出来还是组合电路。
// 用always实现同样的2选1多路选择器
module mux2to1_always (
input wire [3:0] data0,
input wire [3:0] data1,
input wire sel,
output reg [3:0] out
);
always@(*) begin
if (sel)
out = data1;
else
out = data0;
end
endmodule
3.3 常用数据类型——wire和reg,别再傻傻分不清
很多初学者被wire和reg搞晕了。我当年也一样,觉得“reg就是寄存器,wire就是连线”。其实这个理解不完全对。
| 特性 | wire | reg |
|---|---|---|
| 默认值 | 高阻态(z) | 不定态(x) |
| 赋值方式 | assign 连续赋值 | always 或 initial 过程赋值 |
| 物理含义 | 连线、三态门 | 组合逻辑或时序逻辑 |
| 能否存储值 | 不能 | 能(在always块内保持) |
说白了:
- wire 就是一根导线,它本身不存值,值由驱动源决定。如果没驱动,就是高阻z。
- reg 是一个变量,可以在always块里被反复赋值。在组合逻辑中,它综合出来还是连线;在时序逻辑中,它综合出来就是触发器。
你想想看,为什么会有这种设计?其实是为了仿真。在仿真时,reg可以保持上一次赋的值,而wire不行。所以testbench里经常用reg来驱动输入,用wire来观察输出。
🔑 记住这个黄金法则:
- assign 左边 → wire
- always 左边 → reg
- 模块端口 → 默认wire,但output可以声明为reg
3.4 实战小例子——一个4位加法器
光说不练假把式。咱们写一个完整的4位加法器,把今天学的都用上。
module adder_4bit (
input wire [3:0] a, // 加数a
input wire [3:0] b, // 加数b
input wire cin, // 进位输入
output wire [3:0] sum, // 和
output wire cout // 进位输出
);
// 用assign实现,简单直接
assign {cout, sum} = a + b + cin;
endmodule
这里用了一个拼接操作符 {},把cout和sum拼在一起,正好是5位结果。你看,一行代码就搞定了加法器。这就是Verilog的魅力——硬件描述语言,描述的是行为,不是结构。
如果换成always写法:
module adder_4bit_always (
input wire [3:0] a,
input wire [3:0] b,
input wire cin,
output reg [3:0] sum,
output reg cout
);
always@(*) begin
{cout, sum} = a + b + cin;
end
endmodule
两种写法综合出来的电路是一样的。我个人更倾向于第一种,因为代码更少,阅读起来更轻松。
3.5 避坑指南——我踩过的那些雷
最后,分享几个我实际项目中遇到的坑,你们以后遇到了,至少知道往哪个方向查。
- 端口位宽不匹配:比如模块定义的是[7:0],你实例化时连了个[3:0]的信号。综合工具会截断或补零,但行为可能不是你想要的。我的习惯是,所有端口位宽都用parameter定义,这样改起来方便,也不容易出错。
- always@(*) 里漏了敏感信号:虽然用
*可以自动推导,但如果你在always块里用了函数调用或者多维数组,有些老版本工具可能推导不全。保险起见,检查一下综合报告里的“inferred latch”,如果有,赶紧改。 - reg类型在仿真中初始为x:如果你在testbench里没给reg赋初值,仿真一开始它就是x。然后所有跟它相关的信号都会变成x,整个仿真就废了。所以,写testbench时,记得在initial块里给reg赋初值。
好了,今天的内容就到这里。模块与端口是Verilog的骨架,组合逻辑是血肉,wire和reg是细胞。把这些基础打牢,后面学时序逻辑、状态机、FIFO什么的,就会轻松很多。
下一讲,咱们聊聊时序逻辑——always@(posedge clk) 和 非阻塞赋值。到时候我会分享一个关于“仿真与综合不一致”的经典案例,保证让你们印象深刻。
下课!