3. Verilog HDL基础(二):过程块(always)、阻塞与非阻塞赋值、if-else与case语句
好,咱们接着聊。上一章我们把连线、门级建模这些基础过了一遍,这一章要进入真正的“行为级”描述了。说白了,就是让代码像人一样思考——什么时候干活、怎么干活、遇到分支怎么选。
我个人觉得,这部分是Verilog从“画电路图”到“写逻辑”的分水岭。你想想看,如果只会assign,那跟搭积木有什么区别?而掌握了always块和条件语句,你才算真正开始设计数字电路。
3.1 过程块:always
always块,是Verilog里最核心的“干活”单元。它不像assign那样一直执行,而是被“触发”了才执行。
always块的基本语法:
always @(敏感列表) begin
// 过程赋值语句
end
敏感列表,就是触发条件。我刚开始学的时候,总觉得这东西玄乎。后来想明白了——它就像你手机上的闹钟。闹钟响了(敏感事件发生),你才起床(执行块内语句)。
3.1.1 组合逻辑的always块
组合逻辑,说白了就是输出只跟当前输入有关。敏感列表里放所有输入信号。
// 组合逻辑:4选1多路选择器
module mux4to1(
input [3:0] sel,
input a, b, c, d,
output reg y
);
always @(*) begin
case(sel)
4'b0001: y = a;
4'b0010: y = b;
4'b0100: y = c;
4'b1000: y = d;
default: y = 1'b0;
endcase
end
endmodule
注意这里的 @(*),这是Verilog 2001引入的语法,表示“所有输入信号”。我个人习惯用这个,省得漏写信号。我在项目中遇到过有人手写敏感列表,结果漏了一个信号,仿真和综合结果不一致,查了两天才找到原因。
3.1.2 时序逻辑的always块
时序逻辑,输出不仅跟当前输入有关,还跟之前的状态有关。敏感列表里放时钟沿和异步复位。
// 时序逻辑:带异步复位的D触发器
module dff_async(
input clk,
input rst_n,
input d,
output reg q
);
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
q <= 1'b0;
else
q <= d;
end
endmodule
嗯,这里要注意:时序逻辑的赋值用非阻塞赋值(<=),组合逻辑用阻塞赋值(=)。这个规矩千万别搞反了,否则仿真结果会让你怀疑人生。
3.2 阻塞赋值与非阻塞赋值
这是Verilog里最容易踩坑的地方,没有之一。我见过太多新手(甚至老手)在这里翻车。
| 特性 | 阻塞赋值(=) | 非阻塞赋值(<=) |
|---|---|---|
| 执行顺序 | 顺序执行,阻塞后续语句 | 并行执行,不阻塞后续语句 |
| 适用场景 | 组合逻辑 | 时序逻辑 |
| 硬件行为 | 类似组合逻辑连线 | 类似寄存器更新 |
| 仿真行为 | 立即更新 | 块结束时统一更新 |
3.2.1 阻塞赋值:组合逻辑的“即时更新”
// 阻塞赋值示例:交换两个变量
always @(*) begin
temp = a;
a = b;
b = temp;
end
这里用阻塞赋值没问题,因为它是组合逻辑,我们想要的就是“立即生效”的效果。但如果你在时序逻辑里这么写,那就出大事了。
3.2.2 非阻塞赋值:时序逻辑的“统一更新”
// 非阻塞赋值示例:移位寄存器
always @(posedge clk) begin
q1 <= d;
q2 <= q1;
q3 <= q2;
end
为什么这里要用非阻塞赋值?因为所有赋值在块结束时才同时生效。q2拿到的是q1的旧值,而不是更新后的值。这样才符合硬件行为——每个时钟沿,所有寄存器同时采样。
我曾经踩过的坑:有一次写一个流水线设计,在时序逻辑里用了阻塞赋值。仿真结果看起来没问题,但综合后功能全乱。查了三天,最后发现是赋值方式用错了。从那以后,我给自己定了个规矩:时序逻辑只用非阻塞赋值,组合逻辑只用阻塞赋值。绝不混用。
3.3 if-else与case语句
这两个是条件判断的“双胞胎”。功能上可以互相替代,但各有各的脾气。
3.3.1 if-else语句
if-else适合优先级判断的场景。比如,多个条件中,哪个优先级最高?
// if-else示例:优先级编码器
always @(*) begin
if(sel[3])
y = 4'b1000;
else if(sel[2])
y = 4'b0100;
else if(sel[1])
y = 4'b0010;
else if(sel[0])
y = 4'b0001;
else
y = 4'b0000;
end
这里sel[3]的优先级最高。if-else天生就是“链式结构”,综合出来也是优先级逻辑。
小技巧:if-else一定要写全。漏掉else,综合时会生成锁存器(latch)。我见过有人写if没写else,结果综合出来一堆latch,面积和功耗都炸了。
3.3.2 case语句
case语句适合“平行”判断的场景。所有条件地位平等,没有优先级。
// case示例:7段数码管译码器
always @(*) begin
case(bcd)
4'b0000: seg = 7'b0111111; // 显示0
4'b0001: seg = 7'b0000110; // 显示1
4'b0010: seg = 7'b1011011; // 显示2
4'b0011: seg = 7'b1001111; // 显示3
4'b0100: seg = 7'b1100110; // 显示4
4'b0101: seg = 7'b1101101; // 显示5
4'b0110: seg = 7'b1111101; // 显示6
4'b0111: seg = 7'b0000111; // 显示7
4'b1000: seg = 7'b1111111; // 显示8
4'b1001: seg = 7'b1101111; // 显示9
default: seg = 7'b0000000; // 全灭
endcase
end
注意这里的default分支。case语句如果不写default,也会生成latch。这是新手最容易犯的错误之一。
3.3.3 if-else vs case:怎么选?
我个人的经验是:
- 有优先级关系 → 用if-else
- 平行判断 → 用case
- 条件复杂(比如多个信号组合) → 用if-else
- 条件简单且数量多 → 用case(代码更简洁)
你想想看,如果写一个128选1的多路选择器,用if-else得写128层嵌套,代码能看吗?这时候case就香了。
3.4 知识体系总览
为了让你更直观地理解这一章的知识结构,我画了一张图:
3.5 总结与避坑
这一章的内容,说白了就是三个核心:
- always块:组合逻辑用@(*),时序逻辑用时钟沿
- 赋值方式:时序逻辑用非阻塞(<=),组合逻辑用阻塞(=)
- 条件语句:if-else有优先级,case平行判断,都要写全分支
我曾经踩过的坑,再强调一遍:
- 时序逻辑里用了阻塞赋值,仿真和综合结果不一致
- if-else漏写else,综合出latch
- case漏写default,综合出latch
- 敏感列表漏写信号,仿真结果不对
这些坑我都踩过,希望你别再踩了。
嗯,这一章就到这里。记住:写代码的时候,多想想硬件会怎么实现。你写的每一行Verilog,最终都会变成实实在在的电路。