3. 硬件描述语言基础(Verilog):模块与端口、组合逻辑与时序逻辑、always块与assign语句、仿真初探
好,咱们进入正题。Verilog 这东西,说白了就是硬件工程师的“普通话”。你想想看,我们要设计一个网络加速器,总不能拿烙铁一个个焊门电路吧?Verilog 就是用来描述硬件行为的语言。这一章,我带你把最核心的几个概念捋清楚。
3.1 模块与端口:硬件的“黑盒子”
模块(module)是 Verilog 的基本单元。你可以把它想象成一个黑盒子——有输入、有输出,内部具体怎么实现的,外面不用管。我刚开始学的时候,总觉得模块就是 C 语言的函数,后来发现完全不是一回事。函数是顺序执行的,而模块是并行存在的硬件实体。
看个最简单的例子:
module and_gate (
input wire a, // 输入端口 a
input wire b, // 输入端口 b
output wire y // 输出端口 y
);
assign y = a & b; // 组合逻辑:y = a 与 b
endmodule
这里有几个关键点:
- input 和 output 定义了端口方向。注意,没有 inout 的话,端口就是单向的。
- wire 表示连线型变量。组合逻辑输出一般用 wire。
- 模块名和文件名最好保持一致。这是我的血泪教训——有一次项目里模块名和文件名对不上,仿真跑了一天才发现是编译顺序的问题。
3.2 组合逻辑 vs 时序逻辑:一个“即时响应”,一个“听时钟的”
这两个概念,是数字电路设计的基石。我见过不少新手在这上面栽跟头。
组合逻辑:输出只取决于当前输入。就像你按开关,灯立刻就亮。没有记忆,没有延迟(理想情况下)。
时序逻辑:输出不仅取决于当前输入,还取决于之前的状态。说白了,它有“记忆”。这个记忆是靠时钟沿触发的触发器(Flip-Flop)实现的。
为什么网络加速器里大量使用时序逻辑?因为我们需要流水线、需要状态机、需要缓存数据包。纯组合逻辑搞不定这些。
我画了一张图,帮你理清关系:
3.3 always 块与 assign 语句:两种描述方式
Verilog 里描述硬件,主要有两种“武器”:assign 和 always。
3.3.1 assign 语句——组合逻辑的“直通车”
assign 就是连续赋值。只要右边表达式变了,左边立刻更新。适合描述简单的组合逻辑。
// 用 assign 实现一个 2 选 1 多路选择器
module mux2to1 (
input wire [3:0] a,
input wire [3:0] b,
input wire sel,
output wire [3:0] y
);
assign y = sel ? b : a;
endmodule
嗯,这里要注意:assign 的左边必须是 wire 类型。你不能把 reg 放在 assign 左边,这是语法规定。
3.3.2 always 块——更强大的描述方式
always 块可以描述组合逻辑,也可以描述时序逻辑。关键看敏感列表怎么写。
组合逻辑的 always 块:
always @(*) begin
if (sel)
y = b;
else
y = a;
end
这里的 @(*) 表示“对所有输入信号敏感”。只要 a、b、sel 任何一个变了,这个块就会重新计算。我个人习惯用 @(*) 而不是手动列出所有信号,因为手动列容易漏掉。我曾经因为漏了一个信号,仿真和综合结果不一致,查了两天才发现。
时序逻辑的 always 块:
// 一个带同步复位的 D 触发器
always @(posedge clk) begin
if (rst)
q <= 1'b0;
else
q <= d;
end
注意这里用的是 非阻塞赋值 <=。这是时序逻辑的标配。为什么?因为非阻塞赋值能保证多个触发器在同一个时钟沿同时更新,不会出现竞争。我刚开始用阻塞赋值写时序逻辑,仿真结果总是怪怪的,后来才明白这个道理。
=,时序逻辑用非阻塞赋值 <=。别混着用!
3.4 仿真初探:让代码“跑”起来
写完了代码,怎么知道对不对?仿真。说白了,就是给设计加激励,看输出是否符合预期。
一个最简单的 testbench 长这样:
`timescale 1ns / 1ps
module tb_and_gate;
reg a, b;
wire y;
// 实例化被测试模块
and_gate uut (
.a(a),
.b(b),
.y(y)
);
// 产生激励
initial begin
a = 0; b = 0;
#10;
a = 0; b = 1;
#10;
a = 1; b = 0;
#10;
a = 1; b = 1;
#10;
$finish;
end
// 监视输出
initial begin
$monitor("Time=%0t, a=%b, b=%b, y=%b", $time, a, b, y);
end
endmodule
这里有几个要点:
- `timescale 定义了时间单位和精度。1ns/1ps 表示时间单位是 1ns,精度是 1ps。
- initial 块 只执行一次。适合用来产生激励信号。
- #10 表示延迟 10 个时间单位(这里是 10ns)。
- $monitor 是系统任务,只要信号变化就打印一行。
你想想看,如果没有仿真,直接去烧录 FPGA,出了问题找原因多痛苦?仿真就是你的“安全网”。我每次写代码,都会先写一个简单的 testbench 跑一下,确认基本功能没问题,再去做综合。
3.5 本章小结
这一章,我们聊了 Verilog 的四个核心概念:
- 模块与端口:硬件的封装方式,输入输出要分清。
- 组合逻辑 vs 时序逻辑:一个即时响应,一个靠时钟记忆。
- assign 与 always:两种描述方式,各有各的用法。
- 仿真初探:写 testbench 是验证的第一步,别偷懒。
嗯,这些基础打牢了,后面讲状态机、流水线、AXI 总线的时候,你才不会觉得吃力。记住,硬件设计和软件不一样——你写的每一行代码,最终都会变成实实在在的电路。所以,想清楚再写。
公众号:蓝海资料掘金营,微信deep3321