4. Verilog基础与组合逻辑:模块化设计,assign语句,always块,阻塞与非阻塞赋值,组合逻辑电路设计
各位同学,欢迎来到第四章。这一章我们开始真正动手写代码了。说实话,Verilog 这门语言,你把它当成硬件描述语言,而不是编程语言,很多困惑就迎刃而解了。我见过不少新手,上来就把它当 C 语言写,结果综合出来的电路完全不是那么回事。
这一章我们聚焦组合逻辑。组合逻辑说白了就是:输入一变,输出立马跟着变,没有记忆功能。你想想看,加法器、多路选择器,都是典型的组合逻辑。好,我们开始。
4.1 模块化设计:把大电路拆成小积木
做芯片设计,没人会写一个几万行的文件。都是拆成模块,一个模块干一件事。模块化设计的好处很明显:好调试、好复用、好维护。
一个 Verilog 模块长这样:
module adder (
input [3:0] a,
input [3:0] b,
output [4:0] sum
);
assign sum = a + b;
endmodule
嗯,这里要注意:module 和 endmodule 是成对出现的。端口列表里,input、output、inout 三种方向。我个人习惯把输入放前面,输出放后面,这样读起来顺。
模块化设计还有个关键点:一个文件只放一个模块。我在项目中遇到过有人把十几个模块塞一个文件里,结果后来改一个模块,整个文件都得重新编译,浪费时间不说,还容易改出 bug。
adder,文件就叫 adder.v。这是行业惯例,别问为什么,照做就行。
4.2 assign 语句:连续赋值,硬件连线
assign 语句,说白了就是一根导线。左边是输出,右边是输入。只要右边变了,左边立刻更新。这就是组合逻辑的本质。
assign y = (sel) ? a : b;
这行代码综合出来就是一个二选一多路选择器。你想想看,如果 sel 是 1,y 就连到 a;如果是 0,就连到 b。硬件上就是一堆晶体管开关。
使用 assign 有几个要点:
- 左边必须是
wire类型,不能是reg - 右边可以是任何表达式,包括运算符、函数调用
- 多个
assign语句是并行执行的,没有先后顺序
assign 语句里给同一个信号赋值两次。综合工具直接报错,说有多驱动源。记住:一个 wire 只能被一个 assign 驱动。
4.3 always 块:过程赋值,更灵活的描述方式
always 块比 assign 灵活得多。它可以描述组合逻辑,也可以描述时序逻辑。这里我们先讲组合逻辑的用法。
always @(*) begin
if (sel)
y = a;
else
y = b;
end
注意看,@(*) 表示敏感列表里包含所有输入信号。只要 sel、a、b 中任何一个变了,这个块就会重新执行。我个人习惯写组合逻辑时都用 @(*),省得漏掉信号。
组合逻辑的 always 块里,必须把所有输入都列在敏感列表里。漏掉一个,综合出来的电路可能就是个锁存器。嗯,这里要特别小心。
4.4 阻塞赋值与非阻塞赋值:一字之差,天壤之别
这是新手最容易踩的坑。阻塞赋值用 =,非阻塞赋值用 <=。区别在哪?
| 特性 | 阻塞赋值 (=) | 非阻塞赋值 (<=) |
|---|---|---|
| 执行顺序 | 顺序执行,阻塞后续语句 | 并行执行,不阻塞 |
| 适用场景 | 组合逻辑 always 块 | 时序逻辑 always 块 |
| 综合结果 | 组合逻辑 | 寄存器/触发器 |
为什么会这样?你想想看,阻塞赋值就像写软件代码,一行一行执行。而非阻塞赋值,所有赋值操作在同一个时间点同时完成。硬件上,寄存器就是这样的:时钟沿到来时,所有 D 端数据同时打到 Q 端。
我曾经在项目中看到有人把非阻塞赋值用在组合逻辑里,结果仿真和实际电路行为不一致,查了两天才找到原因。记住这个铁律:
4.5 组合逻辑电路设计实战
理论讲完了,我们动手写几个经典电路。
4.5.1 加法器
最简单的加法器,一行代码搞定:
module adder_4bit (
input [3:0] a,
input [3:0] b,
input cin,
output [3:0] sum,
output cout
);
assign {cout, sum} = a + b + cin;
endmodule
这里用到了拼接运算符 {},把进位和结果拼在一起。综合工具会自动优化成进位链,你不用担心性能问题。
4.5.2 多路选择器
多路选择器,简称 MUX。用 assign 实现最简单:
module mux_4to1 (
input [1:0] sel,
input [3:0] d0, d1, d2, d3,
output [3:0] y
);
assign y = (sel == 2'b00) ? d0 :
(sel == 2'b01) ? d1 :
(sel == 2'b10) ? d2 : d3;
endmodule
用 always 块实现也可以:
module mux_4to1_always (
input [1:0] sel,
input [3:0] d0, d1, d2, d3,
output reg [3:0] y
);
always @(*) begin
case (sel)
2'b00: y = d0;
2'b01: y = d1;
2'b10: y = d2;
2'b11: y = d3;
default: y = 4'b0;
endcase
end
endmodule
注意 case 语句要写全所有分支,否则综合出来可能多出锁存器。我习惯加一个 default 分支,哪怕理论上不会执行到,也能让综合工具放心。
assign 加三元运算符。代码简洁,而且不容易漏掉分支。但如果是复杂逻辑,比如状态机,那就用 always 块加 case,可读性更好。
4.6 本章知识体系
下面这张图总结了本章的核心内容,你可以把它当作一个快速参考。
这张图把本章的核心知识点串起来了。模块化设计是骨架,assign 和 always 是两种描述方式,阻塞与非阻塞赋值是语法关键,加法器和多路选择器是实战例子。你把这几个点吃透了,组合逻辑这块就算入门了。
好,这一章就到这里。下一章我们讲时序逻辑,到时候会看到非阻塞赋值的真正威力。记住今天说的铁律,写代码时多留个心眼。