第三章 硬件描述语言基础(Verilog):模块化设计、组合逻辑与时序逻辑、阻塞与非阻塞赋值、Testbench编写
各位同学,欢迎来到第三章。说实话,Verilog 这门语言,你把它当成硬件描述,而不是软件编程,很多问题就迎刃而解了。我见过太多新手把 Verilog 当 C 语言写,结果综合出来的电路完全不是那么回事。今天我们就来把这些核心概念掰开揉碎了讲清楚。
3.1 模块化设计:搭积木的艺术
模块化设计,说白了就是把一个复杂系统拆成一个个小功能块。每个模块各司其职,最后通过连线拼起来。我在做金融风控的 FPGA 加速卡时,就把整个交易校验逻辑拆成了十几个小模块,每个模块只做一件事,比如检查订单金额、验证用户身份、计算风险评分。这样调试起来特别方便,哪个环节出问题,直接定位到那个模块就行。
一个标准的 Verilog 模块长这样:
module risk_checker (
input wire clk,
input wire rst_n,
input wire [31:0] trade_amount,
input wire [15:0] user_risk_score,
output reg alarm
);
// 内部逻辑写在这里
endmodule
注意看,模块的端口就是它的「接口」。我习惯把输入输出分开写,每个信号加注释说明用途。你想想看,如果三个月后你自己回来看这段代码,没有注释,你还能记得每个信号是干嘛的吗?
3.2 组合逻辑与时序逻辑:两种思维方式
这是新手最容易混淆的地方。组合逻辑的输出只取决于当前输入,没有记忆功能。时序逻辑则依赖时钟边沿,能记住过去的状态。
组合逻辑——用 assign 或者 always @(*) 实现。举个例子,判断一笔交易是否超过风控阈值:
// 组合逻辑:输出立即反映输入变化
assign is_high_risk = (trade_amount > 100000) && (user_risk_score > 80);
时序逻辑——用 always @(posedge clk) 实现。比如记录过去 10 笔交易的总金额:
// 时序逻辑:每个时钟上升沿更新一次
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
total_amount <= 32'd0;
else
total_amount <= total_amount + trade_amount;
end
嗯,这里要注意:组合逻辑里用阻塞赋值(=),时序逻辑里用非阻塞赋值(<=)。为什么?我们接着往下看。
3.3 阻塞与非阻塞赋值:一个让我吃过亏的话题
我曾经在一个项目里,因为把非阻塞赋值用在了组合逻辑中,导致仿真结果和实际电路行为不一致。排查了整整两天,最后发现是赋值方式用错了。从那以后,我对这个规则记得特别牢。
简单来说:
- 阻塞赋值(=):顺序执行,后面的语句等前面的执行完才执行。用于组合逻辑。
- 非阻塞赋值(<=):并行执行,所有赋值同时发生。用于时序逻辑。
看个对比例子:
// 错误示范:时序逻辑里用阻塞赋值
always @(posedge clk) begin
a = b; // 先执行
c = a; // 此时 a 已经更新,c 拿到的是新值
end
// 综合出来可能是一个寄存器链,但行为和你预期的不一样
// 正确做法:时序逻辑用非阻塞赋值
always @(posedge clk) begin
a <= b; // 同时执行
c <= a; // 同时执行,c 拿到的是 a 的旧值
end
// 这才是两个独立的寄存器
3.4 Testbench 编写:验证你的设计
写 Testbench 其实就是在模拟外部环境,给你的设计喂数据,看它输出对不对。我一般会先写一个简单的测试用例,验证基本功能,然后再加边界条件和异常情况。
一个基本的 Testbench 结构:
module tb_risk_checker;
// 1. 声明信号
reg clk;
reg rst_n;
reg [31:0] trade_amount;
reg [15:0] user_risk_score;
wire alarm;
// 2. 实例化被测试模块
risk_checker u_risk_checker (
.clk (clk),
.rst_n (rst_n),
.trade_amount (trade_amount),
.user_risk_score (user_risk_score),
.alarm (alarm)
);
// 3. 生成时钟
initial begin
clk = 0;
forever #5 clk = ~clk; // 10ns 周期
end
// 4. 生成激励
initial begin
// 复位
rst_n = 0;
trade_amount = 32'd0;
user_risk_score = 16'd0;
#20;
rst_n = 1;
// 测试用例1:正常交易
#10;
trade_amount = 32'd50000;
user_risk_score = 16'd50;
#10;
// 检查 alarm 是否为 0
// 测试用例2:高风险交易
#10;
trade_amount = 32'd200000;
user_risk_score = 16'd90;
#10;
// 检查 alarm 是否为 1
#50;
$finish;
end
// 5. 监控输出(可选)
initial begin
$monitor("time=%0t, alarm=%b", $time, alarm);
end
endmodule
我个人习惯在 Testbench 里加一些自动检查的逻辑,比如用 assert 语句,这样仿真跑完就能知道结果对不对,不用肉眼去看波形。
知识体系总览
下面这张图把本章的核心知识点串起来了,你可以对照着复习:
本章小结:
- 模块化设计是基础,把大问题拆成小问题
- 组合逻辑没有记忆,时序逻辑有记忆
- 阻塞赋值用于组合逻辑,非阻塞赋值用于时序逻辑——这条规则千万别搞反
- Testbench 是验证的利器,花时间写好 Testbench 能省下大量调试时间
好了,这一章的内容就到这里。记住,Verilog 不是写代码,是在画电路。你写的每一行代码,都要能在脑子里想象出对应的硬件结构。多写、多仿真、多思考,慢慢就有感觉了。