4. Verilog组合逻辑设计:always块、if-else、case语句、三目运算符
组合逻辑,说白了就是输出只取决于当前输入,跟历史状态没关系。你给它什么,它立马给你什么,不带任何记忆。今天咱们就把Verilog里实现组合逻辑的几种常用手段掰扯清楚。
4.1 always块:组合逻辑的“工作台”
always块是Verilog里最核心的语句块之一。我刚开始学的时候,总觉得它神秘兮兮的。其实它就是个“敏感列表驱动”的代码块——只要敏感列表里的信号变了,它就执行一次。
对于组合逻辑,我们用的是 always @(*) 这种写法。星号表示“所有输入信号”,省得你一个一个列,还容易漏掉。我个人习惯,写组合逻辑一律用 @(*),干净利落。
核心要点:组合逻辑的always块里,必须用阻塞赋值(=),不能用非阻塞赋值(<=)。这是无数新手踩过的坑。
// 正确的组合逻辑写法
always @(*) begin
y = a & b; // 阻塞赋值
end
// 错误的写法(会导致仿真行为异常)
always @(*) begin
y <= a & b; // 非阻塞赋值,综合结果可能正确,但仿真会出问题
end
我在项目中遇到过一位同事,用非阻塞赋值写组合逻辑,仿真怎么跑都对,一上板子就出问题。查了两天才发现是赋值方式用错了。嗯,从那以后我每次看到always块,第一眼先看赋值符号。
4.2 if-else语句:条件分支的“老大哥”
if-else是写条件逻辑最直观的方式。你想想看,如果条件A成立,就做这件事;否则做那件事。跟日常思维一模一样。
always @(*) begin
if (sel) begin
out = a;
end else begin
out = b;
end
end
这里有个关键点:if-else必须写完整。如果你只写了if,没写else,综合器会认为“条件不满足时保持原值”。但组合逻辑没有保持能力,于是综合器会给你生成一个锁存器(latch)。这玩意儿在组合逻辑里是毒瘤,时序难控,功耗还大。
避坑指南:我曾经因为一个if后面忘了写else,导致综合出来一堆latch。后端的哥们跑过来问我:“你这设计里怎么这么多latch?”我一看,脸都绿了。从那以后,我写if-else必检查else分支是否覆盖完整。
if-else还有个特点:优先级结构。先判断的条件优先级高,后判断的优先级低。这跟硬件上的级联选择器是一一对应的。
// 优先级编码器:a的优先级最高,c最低
always @(*) begin
if (a) out = 2'b00;
else if (b) out = 2'b01;
else if (c) out = 2'b10;
else out = 2'b11;
end
4.3 case语句:多路选择的“瑞士军刀”
当条件分支比较多的时候,if-else嵌套会变得又臭又长。这时候就该case上场了。case语句本质上是一个并行选择器,所有分支是平等的,没有优先级。
always @(*) begin
case (sel)
2'b00: out = a;
2'b01: out = b;
2'b10: out = c;
2'b11: out = d;
default: out = 4'b0; // 务必加上default
endcase
end
注意看,我加了个 default 分支。为什么?因为case语句如果不写default,当sel出现未列出的值时,综合器又会给你生成latch。道理跟if-else不写else是一样的。
小技巧:写case时,如果所有可能的值都列全了,可以不加default。但我建议你永远加上default。为什么?因为仿真时如果出现X态(未知态),default能帮你兜底。我吃过这个亏,仿真跑得好好的,一上硬件就随机出错,最后发现是某个信号在初始化阶段是X,case没匹配到任何分支。
case还有两个变种:casez 和 casex。它们允许在比较时忽略某些位(用z或x表示“不关心”)。
// casez:忽略高阻态位
always @(*) begin
casez (sel)
3'b1??: out = a; // 只要最高位是1,其他位不管
3'b01?: out = b; // 最高位0,次高位1,最低位不管
3'b001: out = c;
default: out = 4'b0;
endcase
end
我个人习惯,能用普通case解决的,尽量不用casez/casex。因为“不关心”位容易引入歧义,代码可读性下降。除非是做地址译码这类明确需要掩码的场景。
4.4 三目运算符:一行代码搞定选择
三目运算符 条件 ? 真值 : 假值 是Verilog里最简洁的条件表达式。它本质上就是一个2选1选择器。
// 三目运算符写法
assign out = sel ? a : b;
// 等价于always块写法
always @(*) begin
if (sel) out = a;
else out = b;
end
三目运算符可以嵌套使用,但我不建议超过两层。你想想看,a ? b ? c : d : e 这种写法,读代码的人得在脑子里画括号,太累了。
// 两层嵌套,勉强能看
assign out = (sel == 2'b00) ? a :
(sel == 2'b01) ? b :
(sel == 2'b10) ? c : d;
// 三层以上?别这么干,老老实实用case
实战建议:三目运算符最适合写简单的条件赋值,比如数据路径上的选择。复杂的多路选择,用case更清晰。我在项目中定了一条规矩:超过3个分支,禁止用三目运算符嵌套,一律改用case。
4.5 四种方式的对比与选择
| 方式 | 适用场景 | 硬件结构 | 注意事项 |
|---|---|---|---|
| always @(*) | 所有组合逻辑 | 取决于内部语句 | 必须用阻塞赋值 |
| if-else | 优先级逻辑、少量分支 | 级联选择器 | else分支必须完整 |
| case | 多路选择、译码器 | 并行选择器 | default分支必须完整 |
| 三目运算符 | 简单条件赋值 | 2选1选择器 | 避免深层嵌套 |
说白了,没有哪种方式绝对好。我个人的选择原则是:代码可读性优先。你写的代码,三个月后自己还能看懂,同事也能看懂,这就是好代码。
4.6 一个完整的例子:4选1多路选择器
咱们用四种方式实现同一个功能,你感受一下差异。
// 方式1:always + case
module mux4_case (
input [1:0] sel,
input [3:0] a, b, c, d,
output reg [3:0] out
);
always @(*) begin
case (sel)
2'b00: out = a;
2'b01: out = b;
2'b10: out = c;
2'b11: out = d;
default: out = 4'b0;
endcase
end
endmodule
// 方式2:always + if-else
module mux4_if (
input [1:0] sel,
input [3:0] a, b, c, d,
output reg [3:0] out
);
always @(*) begin
if (sel == 2'b00) out = a;
else if (sel == 2'b01) out = b;
else if (sel == 2'b10) out = c;
else out = d;
end
endmodule
// 方式3:三目运算符嵌套
module mux4_ternary (
input [1:0] sel,
input [3:0] a, b, c, d,
output [3:0] out
);
assign out = (sel == 2'b00) ? a :
(sel == 2'b01) ? b :
(sel == 2'b10) ? c : d;
endmodule
这三种方式综合出来的硬件是一样的——都是4选1选择器。但代码风格不同。我个人最喜欢case版本,因为结构清晰,每个分支一目了然。if-else版本隐含了优先级,虽然这里sel的四种取值互斥,优先级不影响结果,但读代码的人会多想一层“为什么用if-else而不是case”。
最后说一句:写组合逻辑,心里要时刻想着硬件。你写的每一行代码,最终都会变成门电路。if-else变成级联的MUX,case变成并行的MUX,三目运算符变成2选1MUX。想清楚硬件结构,代码自然就写对了。