4. 硬件描述语言入门(Verilog):模块与端口、assign 与 always 块、阻塞赋值与非阻塞赋值
好,咱们今天来聊聊 Verilog 最核心的几个概念。说实话,我刚学 Verilog 那会儿,觉得它跟 C 语言长得挺像,心想这有什么难的?结果一上手写 RTL 代码,就被综合器狠狠教育了一顿。嗯,硬件描述语言跟软件语言,骨子里就不是一回事。
这一节,我带你把地基打牢。我们只聊四件事:模块与端口、assign 语句、always 块,以及那个让无数新手翻车的阻塞赋值与非阻塞赋值。
4.1 模块与端口:芯片的“黑盒子”
Verilog 里最基本的单元叫 module。你可以把它想象成一个黑盒子——外面的人只看到它的引脚(端口),里面具体怎么实现的,外部不关心。
一个模块的定义长这样:
module counter (
input wire clk, // 时钟
input wire rst_n, // 复位,低有效
output reg [3:0] count // 4位计数器输出
);
// 内部逻辑写在这里
endmodule
端口有三种方向:input、output、inout。我个人习惯把时钟和复位放在端口列表的最前面,这样一眼就能看到时序逻辑的“心脏”。
wire 和 reg 的类型可以写在端口列表里,也可以分开写。我建议初学者统一写在端口声明里,代码更紧凑,不容易漏掉。
4.2 assign 语句:组合逻辑的“连线”
assign 用来描述组合逻辑。说白了,就是一根导线——左边是输出,右边是输入,信号一变,输出立刻跟着变。
assign led = (switch == 1'b1) ? clk_out : 1'b0;
上面这行代码的意思是:如果开关按下,LED 就输出时钟信号;否则输出低电平。没有时钟沿,没有延迟,纯粹的组合逻辑。
我在项目中遇到过一个问题:有人用 assign 写了很长的嵌套条件表达式,结果综合出来的电路又大又慢。我的建议是——assign 适合简单的逻辑,复杂的条件判断,交给 always 块里的 case 语句会更清晰。
4.3 always 块:时序逻辑的“心脏”
always 块是 Verilog 里最灵活、也最容易出错的构造。它既可以描述组合逻辑,也可以描述时序逻辑。区别在于敏感列表怎么写。
4.3.1 组合逻辑的 always 块
always @(*) begin
if (sel)
out = a;
else
out = b;
end
@(*) 表示对所有输入信号敏感。只要 sel、a、b 中任何一个发生变化,这个块就会重新执行。嗯,这里要注意:组合逻辑的 always 块里,必须把所有输入信号都列全,否则综合会生成锁存器(latch)。
4.3.2 时序逻辑的 always 块
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 4'b0000;
else
count <= count + 1'b1;
end
敏感列表里写 posedge clk,表示只在时钟上升沿触发。加上 negedge rst_n 是为了支持异步复位。这是最标准的写法,我建议你把它当成模板记下来。
4.4 阻塞赋值与非阻塞赋值:一字之差,天壤之别
这是 Verilog 里最经典的“坑”,没有之一。很多面试官就爱拿这个考人。
- 阻塞赋值(=):顺序执行,前面的赋值会阻塞后面的赋值。用在组合逻辑的 always 块里。
- 非阻塞赋值(<=):并行执行,所有赋值同时发生。用在时序逻辑的 always 块里。
看个例子你就明白了:
// 阻塞赋值 —— 组合逻辑
always @(*) begin
a = b;
c = a; // c 得到的是 b 的新值
end
// 非阻塞赋值 —— 时序逻辑
always @(posedge clk) begin
a <= b;
c <= a; // c 得到的是 a 的旧值
end
为什么会这样?因为非阻塞赋值在时钟沿来临时,先把所有右边的值“拍个快照”,然后统一更新到左边。而阻塞赋值是“来一个算一个”,立即生效。
<=,组合逻辑用 =。混着用?我劝你别试。我曾经在一个项目里把两者混在同一个 always 块里,仿真结果和实际芯片行为完全对不上,最后只好重写。
4.5 知识体系总览
下面这张图,帮你把这一节的核心逻辑串起来:
4.6 实战建议
学到这里,你可能觉得知识点有点散。没关系,我帮你总结几条实战中一定会用到的原则:
- 模块化设计:每个模块只做一件事,端口尽量少。一个模块超过 20 个端口,就该考虑拆分了。
- 区分清楚“线”和“寄存器”:
wire对应组合逻辑的输出,reg对应时序逻辑的输出。但注意,reg在 always 块里也可以描述组合逻辑。 - 敏感列表要完整:组合逻辑用
@(*),时序逻辑明确写posedge clk和negedge rst_n。 - 赋值方式别混用:同一个 always 块里,要么全用
=,要么全用<=。混着写,综合结果会让你怀疑人生。
好了,这一节的内容就到这里。记住:模块是骨架,端口是接口,assign 和 always 是血肉,阻塞与非阻塞赋值是灵魂。把这几个概念吃透了,后面的路就好走了。
公众号:蓝海资料掘金营,微信deep3321