4. Verilog语法基础(下):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) |
时钟沿或复位沿 | 带复位的时序逻辑 |
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更新后的值。
=又用<=。仿真跑得挺好,综合出来一堆奇怪的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 常见陷阱与避坑指南
- 敏感列表不完整:组合逻辑漏了输入信号,综合出latch。用
@*可以避免。 - 混用赋值方式:同一个always块里既有
=又有<=,仿真和综合结果不一致。 - 多个always块对同一个变量赋值:Verilog不允许,综合会报错。一个变量只能在一个always块里被赋值。
- 组合逻辑里用非阻塞赋值:仿真结果可能正确,但综合出来多出几级不必要的触发器,时序很难收敛。
- 边沿敏感列表里写电平敏感信号:比如
@(posedge clk or en),综合工具会报警告,行为也不确定。
嗯,这一章的内容就这些。说白了就是三句话:always块描述行为,敏感列表决定触发时机,阻塞/非阻塞赋值决定电路类型。把这三点刻在脑子里,后面写状态机、写接口逻辑的时候,你会感谢今天打下的基础。