4. Verilog语法基础(下):always块、阻塞与非阻塞赋值、敏感列表

好,咱们接着往下聊。上一章我们把连线、寄存器、运算符这些基本元件认了个脸熟。但光有这些,你只能搭一个「静态电路」——上电啥样,永远啥样。真正的数字芯片,核心是「时序逻辑」,说白了就是:时钟沿来了,我才干活。

这一章要讲的三个东西——always块、阻塞/非阻塞赋值、敏感列表——就是描述这种「时钟驱动行为」的语法骨架。我当年刚学的时候,觉得它们就是一堆关键字。直到第一次写状态机,综合出来功能不对,才明白这里面的门道有多深。

核心一句话: always块是Verilog描述硬件行为的「舞台」,敏感列表是「开场信号」,赋值方式决定了「演员怎么动」。
always块、敏感列表与赋值方式知识结构 always块 · 敏感列表 · 赋值方式 知识结构 always 块 敏感列表 组合逻辑 时序逻辑 电平敏感 边沿敏感 阻塞赋值 = 组合电路 非阻塞赋值 <= 时序电路 ⚠ 黄金法则:组合逻辑用阻塞赋值,时序逻辑用非阻塞赋值 违反此法则 → 仿真与综合结果不一致 → 芯片功能错误

4.1 always块:硬件行为的「容器」

always块是Verilog里最核心的行为描述语句。它不像assign那样只描述连线,而是描述「什么时候做什么事」。基本语法长这样:

always @ (敏感列表) begin
    // 要执行的语句
end

嗯,这里要注意:always块里的语句,是顺序执行的。这和C语言很像,但背后的硬件含义完全不同——C语言是CPU一条条取指令执行,而always块综合出来是一堆并行的逻辑门和触发器。

我见过不少新手把always块当成C语言的while(1)来理解,其实不对。它更像是一个「事件驱动的循环」:敏感列表里的信号一变,块里的代码就重新算一遍。

4.2 敏感列表:告诉always什么时候干活

敏感列表就是always @ (...)括号里的内容。它有两种形式:

类型 写法 触发条件 典型用途
电平敏感 @ (a or b or c) 任意信号电平变化 组合逻辑
边沿敏感 @ (posedge clk) 时钟上升沿 时序逻辑
混合敏感 @ (posedge clk or negedge rst_n) 时钟沿或复位沿 带复位的时序逻辑
我的习惯: 写组合逻辑时,敏感列表里把所有输入信号都列全。漏掉一个,综合出来就是锁存器(latch),这玩意儿在大多数设计里都是个坑。我曾经因为少写了一个信号,debug了整整两天。

Verilog 2001之后支持用逗号分隔敏感列表,也支持@(*)或@*表示「自动推导敏感列表」。我个人强烈建议:组合逻辑用@*,时序逻辑显式写posedge clk。这样既省事又清晰。

4.3 阻塞赋值 vs 非阻塞赋值:一字之差,天壤之别

这是Verilog里最容易踩的坑,没有之一。你想想看,一个等号和一个小于等于号,决定了你的电路是组合逻辑还是时序逻辑,决定了仿真结果和综合结果是否一致。

4.3.1 阻塞赋值 =

阻塞赋值是「立即生效」的。执行完当前赋值语句,右边的值立刻赋给左边。后面的语句再用到这个变量时,已经是新值了。

// 阻塞赋值示例:组合逻辑
always @* begin
    c = a & b;   // 先算c
    d = c | e;   // 再用c的新值算d
end

综合出来就是两个逻辑门串联:d = (a & b) | e。说白了,阻塞赋值描述的是数据流。

4.3.2 非阻塞赋值 <=

非阻塞赋值是「延迟生效」的。所有非阻塞赋值语句在同一个always块里是并行计算的:先算好所有右边的值,然后在时钟沿统一更新到左边。

// 非阻塞赋值示例:时序逻辑
always @(posedge clk) begin
    q1 <= d;      // 记录d的旧值
    q2 <= q1;     // 记录q1的旧值
end

这个例子综合出来就是两个触发器级联,构成一个2级移位寄存器。注意:q2拿到的是q1在时钟沿之前的值,而不是q1更新后的值。

我曾经犯过的错: 在同一个always块里混用阻塞和非阻塞赋值,给同一个变量又用=又用<=。仿真跑得挺好,综合出来一堆奇怪的latch和竞争。后来我给自己定了个死规矩:
组合逻辑always块只用阻塞赋值,时序逻辑always块只用非阻塞赋值。 绝不混用。

4.4 黄金法则:组合用阻塞,时序用非阻塞

这个法则不是我编的,是业界几十年的血泪教训。咱们用表格对比一下:

场景 always块写法 赋值方式 综合结果
组合逻辑(加法器、多路选择器) always @* 阻塞 = 逻辑门网络
时序逻辑(寄存器、计数器、状态机) always @(posedge clk) 非阻塞 <= D触发器
带异步复位的时序逻辑 always @(posedge clk or negedge rst_n) 非阻塞 <= 带复位端的D触发器

你可能会问:为什么时序逻辑不能用阻塞赋值?我举个例子你就明白了:

// 错误写法:时序逻辑用阻塞赋值
always @(posedge clk) begin
    a = b;
    c = a;   // 这里a已经是新值了,c拿到的是b,而不是a的旧值
end

综合工具会把a和c都综合成触发器,但c的输入直接连到了b,而不是a的输出。仿真时因为阻塞赋值顺序执行,看起来好像是对的,但综合出来的硬件完全不一样。这就是所谓的「仿真-综合不匹配」——流片回来功能不对,哭都来不及。

记住: 非阻塞赋值模拟的是触发器的「采样-保持」行为。所有触发器在时钟沿同时采样输入,然后同时更新输出。这才是硬件真正的并行本质。

4.5 实战:写一个带复位的8位计数器

光说不练假把式。咱们写一个完整的计数器模块,把今天学的都用上:

module counter (
    input  wire       clk,      // 时钟
    input  wire       rst_n,    // 异步复位,低有效
    input  wire       en,       // 使能
    output reg  [7:0] count     // 8位计数值
);

    // 时序逻辑:非阻塞赋值
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            count <= 8'd0;      // 复位清零
        else if (en)
            count <= count + 1; // 使能时加1
        else
            count <= count;     // 保持(其实可以省略)
    end

endmodule

这个例子包含了:

  • 边沿敏感列表:时钟上升沿 + 复位下降沿
  • 非阻塞赋值:所有<=都在时钟沿统一更新
  • 异步复位:rst_n一旦变低,不管时钟在哪儿,立即清零
一个小技巧: 写复位逻辑时,if (!rst_n)要放在最前面。这样综合工具能识别出这是异步复位,而不是同步复位。我见过有人把复位条件写在else if里,结果综合出来没有复位端,仿真时能复位,芯片上电后却不行。

4.6 常见陷阱与避坑指南

  1. 敏感列表不完整:组合逻辑漏了输入信号,综合出latch。用@*可以避免。
  2. 混用赋值方式:同一个always块里既有=又有<=,仿真和综合结果不一致。
  3. 多个always块对同一个变量赋值:Verilog不允许,综合会报错。一个变量只能在一个always块里被赋值。
  4. 组合逻辑里用非阻塞赋值:仿真结果可能正确,但综合出来多出几级不必要的触发器,时序很难收敛。
  5. 边沿敏感列表里写电平敏感信号:比如@(posedge clk or en),综合工具会报警告,行为也不确定。

嗯,这一章的内容就这些。说白了就是三句话:always块描述行为,敏感列表决定触发时机,阻塞/非阻塞赋值决定电路类型。把这三点刻在脑子里,后面写状态机、写接口逻辑的时候,你会感谢今天打下的基础。