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
为什么会这样?
在非阻塞赋值里,所有右边的表达式(a 和 c + 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 知识体系图
为了让你更直观地理解这些概念之间的关系,我画了一张图:
这张图把本章的核心内容串起来了。你写代码时,可以对照着看:先确定逻辑类型,再选敏感列表,最后定赋值方式。按这个流程走,基本不会出错。
3.6 避坑指南
最后,分享几个我实战中总结的教训:
- 不要在一个always块里同时描述组合和时序逻辑。分开写,可读性高,也不容易出错。
- 组合逻辑的敏感列表一定要写全。用
@(*)是最省心的办法。 - 时序逻辑的复位,最好用异步复位。同步复位虽然也能用,但综合出来的电路面积更大,时序也更紧张。
- 非阻塞赋值右边的表达式,不要依赖左边的结果。因为它们是同时计算的,依赖关系会导致意想不到的行为。
好了,这一章的内容就到这里。记住,写Verilog不是写C语言,你是在描述硬件。脑子里要有电路图,心里要有时序图。多练,多想,慢慢就找到感觉了。
公众号:蓝海资料掘金营,微信deep3321