一、Verilog 模块——数字世界的“黑盒子”
各位同学,咱们今天聊聊 Verilog 里最基础的东西——模块与端口。
你想想看,数字电路设计说白了就是搭积木。每个积木块就是一个 module。我刚开始学的时候,总觉得这东西玄乎,后来做项目多了才发现,module 其实就是个“黑盒子”。
盒子里装的是逻辑,盒子外面露出来的是端口。别人用你的模块,根本不用管里面怎么实现的,只要知道怎么接信号就行。
1.1 module/endmodule——模块的骨架
每个 Verilog 文件,开头一定是 module 模块名,结尾一定是 endmodule。这是规矩,少一个都不行。
module my_first_module (
// 端口声明写在这里
);
// 内部逻辑写在这里
endmodule
嗯,这里要注意:模块名最好见名知意。我见过有人用 module a、module b 这种名字,三个月后自己都看不懂。我个人习惯用 模块名_功能 的格式,比如 counter_8bit、uart_tx。
1.2 端口定义——模块的“手脚”
端口分三种:input、output、inout。
| 端口类型 | 方向 | 典型用途 |
|---|---|---|
| input | 输入 | 时钟、复位、数据输入 |
| output | 输出 | 数据输出、状态指示 |
| inout | 双向 | I2C数据线、三态总线 |
说白了,input 是别人给你的信号,output 是你给别人发的信号。inout 比较特殊,既能发也能收,但同一时刻只能干一件事。
我在项目中遇到过用 inout 端口踩坑的情况。当时做 I2C 控制器,忘了加三态控制,结果数据线冲突,仿真没问题,上板就死。后来加了个 assign sda = (sda_en) ? sda_out : 1'bz; 才搞定。
module and_gate (
input wire a, // 输入端口
input wire b, // 输入端口
output wire y // 输出端口
);
assign y = a & b;
endmodule
二、wire 与 reg——数据类型的“双胞胎”
很多新手搞不清 wire 和 reg 的区别。我当年也迷糊过一阵子。
其实很简单:
- wire:线网类型,相当于物理连线。它不能存储值,只能传递值。
- reg:寄存器类型,可以存储值。但注意,它不一定是真正的寄存器!
为什么会这样?因为 reg 在组合逻辑里只是“变量”的概念,综合出来可能只是一根线。只有在时序逻辑(always @(posedge clk))里,它才会变成真正的触发器。
重要原则:
- assign 语句左边必须是 wire
- always 块里赋值的变量必须是 reg
- 模块端口默认是 wire 类型
我建议你刚开始做设计时,先记住这个规则:组合逻辑用 wire,时序逻辑用 reg。等做多了自然就理解了。
三、assign 连续赋值——组合逻辑的“直通车”
assign 语句是 Verilog 里最直接的组合逻辑描述方式。它的语法很简单:
assign 左边信号 = 右边表达式;
左边必须是 wire 类型,右边可以是任何表达式。只要右边信号一变,左边立刻更新。这就是“连续赋值”的含义——信号一直在“连续”地计算。
来看几个例子:
// 基本逻辑门
assign y = a & b; // 与门
assign y = a | b; // 或门
assign y = ~a; // 非门
assign y = a ^ b; // 异或门
// 多输入组合
assign y = (a & b) | (c & d); // 与或组合
// 条件赋值(三目运算符)
assign y = sel ? a : b; // 二选一多路器
我曾经犯过一个低级错误:在 assign 语句里写了 assign y = y + 1;。你想想看,这相当于把输出又连回输入,综合出来就是个组合逻辑环,仿真直接崩了。所以记住:assign 不能描述带反馈的电路。
四、组合逻辑电路实战——用模块实现一个全加器
光说不练假把式。咱们用刚才学的知识,写一个 1 位全加器。
全加器有三个输入:a、b、cin(进位输入),两个输出:sum(和)、cout(进位输出)。
真值表如下:
| a | b | cin | sum | cout |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 |
| 0 | 0 | 1 | 1 | 0 |
| 0 | 1 | 0 | 1 | 0 |
| 0 | 1 | 1 | 0 | 1 |
| 1 | 0 | 0 | 1 | 0 |
| 1 | 0 | 1 | 0 | 1 |
| 1 | 1 | 0 | 0 | 1 |
| 1 | 1 | 1 | 1 | 1 |
逻辑表达式:
- sum = a ^ b ^ cin
- cout = (a & b) | (a & cin) | (b & cin)
用 Verilog 实现:
module full_adder (
input wire a,
input wire b,
input wire cin,
output wire sum,
output wire cout
);
assign sum = a ^ b ^ cin;
assign cout = (a & b) | (a & cin) | (b & cin);
endmodule
你看,用 assign 语句描述组合逻辑就是这么直接。把逻辑表达式写出来,assign 一赋值,完事。
小技巧:写组合逻辑时,尽量用 assign 而不是 always @(*)。assign 更直观,综合结果也更可控。我一般只在需要 if-else 或 case 语句时才用 always。
五、本章知识体系
下面这张图帮你理清本章的核心脉络:
避坑指南:
- 千万不要在多个 always 块里对同一个 reg 赋值,综合会报多驱动错误
- inout 端口一定要有三态控制,否则上板必死
- assign 左边不能是 reg,always 块里不能给 wire 赋值
好了,这一章的内容就这些。模块、端口、wire 和 reg、assign 语句,这四个东西是 Verilog 的基石。你把这章吃透了,后面学时序逻辑、状态机什么的就轻松多了。
记住我的一句话:写 Verilog 不是写软件,你脑子里要时刻想着电路。每个 assign 都是一根线,每个 reg 都可能是一个触发器。想清楚了再下笔,比写完再改要快十倍。