3、Verilog语法精讲(下):过程块(always)、阻塞与非阻塞赋值、条件与循环语句

好,咱们接着聊。上一节我们把连线、寄存器、运算符这些基础过了一遍。这一节,我要带你进入Verilog最核心、也最容易踩坑的地方——过程块。

说白了,过程块就是描述硬件“行为”的地方。你写的组合逻辑、时序逻辑,最终都要靠 always 块来实现。我见过不少新手,代码能仿真通过,一上板就跑飞,十有八九是这里出了问题。

3.1 always块:硬件行为的“舞台”

always 块是Verilog里最常用的过程块。它不像 assign 那样是连续赋值,而是在特定事件触发时才执行内部的语句。

它的基本结构长这样:

always @ (敏感列表) begin
    // 你的逻辑代码
end

这里的“敏感列表”就是触发条件。我习惯把它理解成“闹钟”——列表里的信号一变,闹钟就响,块里的代码就跑一遍。

3.1.1 组合逻辑的always块

如果你想让 always 块描述组合逻辑,敏感列表里就要把所有输入信号都列进去。漏掉一个,综合出来的就是锁存器,而不是你想要的组合电路。

举个例子,一个简单的加法器:

always @ (a or b) begin
    sum = a + b;
end

嗯,这里要注意。老一点的写法喜欢用 or 连接信号。现在更推荐用 always @ (*),星号代表“所有信号”。我个人觉得这写法更省心,不容易漏。

always @ (*) begin
    sum = a + b;
end
我的习惯:写组合逻辑时,一律用 always @ (*)。省事,还不会因为漏信号而生成奇怪的锁存器。

3.1.2 时序逻辑的always块

时序逻辑就不同了。它需要时钟边沿作为触发条件。比如一个D触发器:

always @ (posedge clk) begin
    q <= d;
end

posedge 是上升沿,negedge 是下降沿。我建议你统一用上升沿,除非有特殊需求。这样代码风格一致,调试时也少一些混乱。

如果还要带异步复位,写法是这样的:

always @ (posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 1'b0;
    else
        q <= d;
end

这里 rst_n 是低电平有效的复位信号。注意敏感列表里同时有 posedge clk 和 negedge rst_n。我曾经见过有人把复位信号漏掉,结果仿真时复位功能正常,上板后完全不受控——因为综合工具把它当成同步复位了。

避坑指南:异步复位的敏感列表,必须同时包含时钟和复位信号。漏掉任何一个,综合结果都会和你的预期不符。

3.2 阻塞赋值与非阻塞赋值:一字之差,天壤之别

这是Verilog里最容易让人迷糊的地方。我当年也在这上面栽过跟头。

简单说:

  • 阻塞赋值(=):顺序执行,前面的赋值完成,后面的才开始。
  • 非阻塞赋值(<=):并行执行,所有赋值在同一时刻发生。

你想想看,这区别在硬件里意味着什么?

3.2.1 组合逻辑用阻塞赋值

组合逻辑里,信号是实时传递的。用阻塞赋值正好符合这个特性。

always @ (*) begin
    c = a & b;
    d = c | e;
end

这里 c 先被赋值,然后 d 才用 c 的新值去计算。这符合组合逻辑的传播顺序。

3.2.2 时序逻辑用非阻塞赋值

时序逻辑里,所有寄存器都是在时钟沿同时更新的。非阻塞赋值正好模拟这个行为。

always @ (posedge clk) begin
    q1 <= d;
    q2 <= q1;
end

这段代码描述的是一个两级移位寄存器。在时钟上升沿,q1 和 q2 同时更新。q2 拿到的是 q1 的旧值,而不是新值。如果你用阻塞赋值,结果就完全不一样了——q2 会拿到 q1 的新值,变成两级直通,而不是移位。

黄金法则:
  • 组合逻辑:用阻塞赋值(=)
  • 时序逻辑:用非阻塞赋值(<=)
  • 同一个always块里,不要混用两种赋值方式

这个法则我建议你刻在脑子里。我见过太多因为混用赋值方式导致的bug,仿真时看不出来,上板后逻辑完全乱套。

3.3 条件语句:if-else 与 case

条件语句是描述硬件决策逻辑的基本工具。Verilog里主要有 if-else 和 case 两种。

3.3.1 if-else 语句

if-else 适合描述优先级逻辑。比如一个多路选择器:

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

这里要注意,if-else 是有优先级的——先判断的条件优先级高。综合出来的电路,优先级高的信号离输出近,路径延迟小。

如果你写 if 没有对应的 else,综合工具会认为“条件不满足时保持原值”,从而生成锁存器。这通常不是你想要的。

避坑指南:组合逻辑里,if 一定要配 else。否则综合出锁存器,你的电路行为就会变得诡异。

3.3.2 case 语句

case 语句适合描述并行选择逻辑。它没有优先级,所有分支是平等的。

always @ (*) begin
    case (sel)
        2'b00: out = a;
        2'b01: out = b;
        2'b10: out = c;
        2'b11: out = d;
        default: out = 1'b0;
    endcase
end

default 分支很重要。它相当于 if-else 里的最后一个 else,用来处理所有未列出的情况。漏掉 default,同样可能生成锁存器。

我个人习惯,写 case 时永远带上 default,哪怕你觉得所有情况都覆盖了。为什么?因为仿真时信号可能有不定态(X)或高阻态(Z),没有 default 的话,输出就会变成X,导致后续逻辑全乱。

3.4 循环语句:for 与 generate

循环语句在Verilog里用得不多,但在某些场景下非常有用。

3.4.1 for 循环

for 循环主要用于简化重复的赋值或计算。比如初始化一个寄存器数组:

integer i;
always @ (posedge clk) begin
    for (i = 0; i < 8; i = i + 1) begin
        reg_array[i] <= 8'b0;
    end
end

注意,这里的 for 循环在综合时会被展开成并行电路,而不是像软件那样顺序执行。所以循环次数必须是常数,不能是变量。

3.4.2 generate 循环

generate 是另一种循环方式,常用于模块实例化或参数化设计。

genvar i;
generate
    for (i = 0; i < 4; i = i + 1) begin : adder_gen
        adder u_adder (
            .a(a[i*8 +: 8]),
            .b(b[i*8 +: 8]),
            .sum(sum[i*8 +: 8])
        );
    end
endgenerate

这段代码生成了4个8位加法器。用 generate 的好处是代码简洁,而且每个实例都有独立的标签(adder_gen[0] 到 adder_gen[3]),方便调试。

我的经验:需要重复实例化相同模块时,优先用 generate。它比手写4遍代码更清晰,也更容易维护。

3.5 本章小结

这一节我们聊了:

  • always块:组合逻辑用 @ (*),时序逻辑用 @ (posedge clk)
  • 阻塞与非阻塞赋值:组合逻辑用 =,时序逻辑用 <=,别混用
  • 条件语句:if-else 有优先级,case 无优先级,都要注意补全所有分支
  • 循环语句:for 和 generate 用于简化重复逻辑,循环次数必须固定

这些语法点,每一个我都踩过坑。你如果能把这些规则内化成习惯,写出来的代码就会少很多bug。下一节,我们聊聊状态机的设计——那是数字系统的“大脑”。

Verilog过程块与赋值知识体系 过程块与赋值 always块:硬件行为描述 组合逻辑 @(*) 时序逻辑 @(posedge clk) 赋值方式 阻塞赋值 = (组合逻辑用) 非阻塞赋值 <= (时序逻辑用) 条件与循环语句 核心原则:组合逻辑用阻塞赋值,时序逻辑用非阻塞赋值,不要混用

可可精品资料库