3. Verilog基础语法(下):always块、时序逻辑、组合逻辑、阻塞与非阻塞赋值

好,咱们接着聊。上一章我们把Verilog的基本门级建模和数据类型捋了一遍。这一章,我要跟你聊聊真正让FPGA“活”起来的东西——always块,以及它背后的时序逻辑组合逻辑。还有那个让无数新手栽跟头的阻塞与非阻塞赋值

说实话,我刚入行那会儿,也在这几个概念上吃过亏。有一次写一个简单的计数器,仿真怎么都对,下载到板子上就是乱跳。查了两天,最后发现是赋值方式用错了。嗯,从那以后,我对这块的理解就深刻多了。

3.1 always块:硬件描述的核心

always块,说白了就是一个无限循环的进程。它不像C语言的while(1)那样顺序执行,而是并行地、持续地监控敏感信号的变化。只要敏感列表里的信号有变化,块内的代码就会被触发执行。

它的基本语法长这样:

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

这里的“敏感信号列表”是关键。它决定了这个always块什么时候被激活。我个人习惯把它分成两类:

  • 电平敏感:用于组合逻辑,比如 always @ (a or b or c)。只要a、b、c中任何一个电平发生变化,块内代码就执行。
  • 边沿敏感:用于时序逻辑,比如 always @ (posedge clk or negedge rst_n)。只在时钟上升沿或复位下降沿触发。

重要提醒: 敏感列表写全,否则综合出来的电路可能跟你想象的不一样。我见过有人写组合逻辑时漏掉一个信号,结果综合出了锁存器,功能直接跑偏。

3.2 组合逻辑 vs 时序逻辑

这两个概念,是FPGA设计的基石。你想想看,一个数字电路,要么是纯组合的,要么是带记忆的时序的。

组合逻辑:输出只取决于当前的输入。没有记忆,没有时钟。比如一个加法器、一个多路选择器。它的always块里,敏感列表通常是电平敏感的。

// 组合逻辑:一个简单的与门
always @ (a or b) begin
    y = a & b;
end

时序逻辑:输出不仅取决于当前输入,还取决于之前的状态。它需要时钟沿来驱动,需要寄存器来存储状态。比如计数器、状态机。

// 时序逻辑:一个简单的D触发器
always @ (posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 1'b0;
    else
        q <= d;
end

我在项目中遇到过最典型的例子,就是边沿检测。你想检测一个按键的上升沿,用纯组合逻辑是做不到的,必须用时序逻辑把信号打一拍,然后比较前后两个时钟周期的值。

我的小技巧: 写代码前,先想清楚这个模块是组合逻辑还是时序逻辑。如果是组合逻辑,就用阻塞赋值;如果是时序逻辑,就用非阻塞赋值。这个习惯能帮你避免90%的时序问题。

3.3 阻塞赋值与非阻塞赋值

这是Verilog里最容易让人混淆的地方,没有之一。我当年也是花了好长时间才彻底搞明白。

阻塞赋值(=)
“阻塞”的意思是,这条语句没执行完,后面的语句就得等着。它是顺序执行的,跟C语言一样。通常用在组合逻辑的always块里。

非阻塞赋值(<=)
“非阻塞”的意思是,所有赋值语句是同时执行的。右边的表达式在时钟沿到来时被计算,但赋值操作要等到所有语句都计算完才统一进行。通常用在时序逻辑的always块里。

来看个对比的例子:

// 阻塞赋值:组合逻辑
always @ (a or b) begin
    c = a;
    d = c + b;  // d = a + b
end

// 非阻塞赋值:时序逻辑
always @ (posedge clk) begin
    c <= a;
    d <= c + b;  // d = 旧c + b,不是a + b
end

为什么会这样?
在非阻塞赋值里,所有右边的表达式(ac + b)都是在时钟沿到来时同时被采样的。所以 d 拿到的是 c 的旧值,而不是 a 的新值。

我曾经踩过的坑: 在同一个always块里混用阻塞和非阻塞赋值。综合工具会报warning,甚至直接报错。更可怕的是,有时候它不报错,但仿真和实际电路行为不一致。所以,我的原则是:一个always块里,只用一种赋值方式

3.4 实战中的选择

好了,理论说完了,咱们来点实际的。在写代码时,到底该怎么选?

场景 推荐赋值方式 敏感列表 说明
纯组合逻辑(如译码器) 阻塞赋值(=) 电平敏感(@(*)) 输出只依赖当前输入,顺序执行没问题
时序逻辑(如计数器) 非阻塞赋值(<=) 边沿敏感(posedge clk) 需要寄存器存储状态,并行赋值
组合+时序混合(如状态机) 分开写两个always块 分别对应电平/边沿 组合部分用阻塞,时序部分用非阻塞

我个人习惯,写组合逻辑时,直接用 always @ (*)。这个星号是Verilog 2001引入的,表示自动推导敏感列表。省事,也不容易漏信号。

写时序逻辑时,我一般会加上异步复位:

always @ (posedge clk or negedge rst_n) begin
    if (!rst_n)
        cnt <= 32'd0;
    else if (en)
        cnt <= cnt + 1'b1;
    else
        cnt <= cnt;
end

嗯,这里要注意,cnt <= cnt 这种写法,综合工具会识别成“保持当前值”,不会生成额外的逻辑。但如果你写 cnt <= cnt + 1'b0,那就会多出一个加法器,浪费资源。

3.5 知识体系图

为了让你更直观地理解这些概念之间的关系,我画了一张图:

Verilog 基础语法(下)知识体系 always 块 敏感信号列表 逻辑类型 赋值方式 电平敏感 (@(*)) 边沿敏感 (posedge) 组合逻辑 时序逻辑 阻塞赋值 (=) 非阻塞赋值 (<=) 核心原则:组合逻辑用阻塞赋值,时序逻辑用非阻塞赋值 一个always块内,只用一种赋值方式

这张图把本章的核心内容串起来了。你写代码时,可以对照着看:先确定逻辑类型,再选敏感列表,最后定赋值方式。按这个流程走,基本不会出错。

3.6 避坑指南

最后,分享几个我实战中总结的教训:

  • 不要在一个always块里同时描述组合和时序逻辑。分开写,可读性高,也不容易出错。
  • 组合逻辑的敏感列表一定要写全。用 @(*) 是最省心的办法。
  • 时序逻辑的复位,最好用异步复位。同步复位虽然也能用,但综合出来的电路面积更大,时序也更紧张。
  • 非阻塞赋值右边的表达式,不要依赖左边的结果。因为它们是同时计算的,依赖关系会导致意想不到的行为。

好了,这一章的内容就到这里。记住,写Verilog不是写C语言,你是在描述硬件。脑子里要有电路图,心里要有时序图。多练,多想,慢慢就找到感觉了。


公众号:蓝海资料掘金营,微信deep3321