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 知识体系总览

为了让你更直观地理解这一章的知识结构,我画了一张图:

Verilog HDL基础(二)知识体系 过程块(always) 阻塞/非阻塞赋值 条件语句 组合逻辑 时序逻辑 阻塞赋值(=) 非阻塞赋值(<=) if-else case @(*) 敏感列表 posedge/negedge 顺序执行 并行执行 优先级结构 平行结构 ⚠️ 避坑指南 时序逻辑用非阻塞赋值 | 组合逻辑用阻塞赋值 | 条件语句必须写全(else/default) 💡 实战建议 有优先级用if-else | 平行判断用case | 敏感列表用@(*) | 避免生成latch

3.5 总结与避坑

这一章的内容,说白了就是三个核心:

  • always块:组合逻辑用@(*),时序逻辑用时钟沿
  • 赋值方式:时序逻辑用非阻塞(<=),组合逻辑用阻塞(=)
  • 条件语句:if-else有优先级,case平行判断,都要写全分支

我曾经踩过的坑,再强调一遍:

  1. 时序逻辑里用了阻塞赋值,仿真和综合结果不一致
  2. if-else漏写else,综合出latch
  3. case漏写default,综合出latch
  4. 敏感列表漏写信号,仿真结果不对

这些坑我都踩过,希望你别再踩了。

嗯,这一章就到这里。记住:写代码的时候,多想想硬件会怎么实现。你写的每一行Verilog,最终都会变成实实在在的电路。

可可精品资料库