4、硬件描述语言基础(Verilog):模块化设计、组合逻辑与时序逻辑、仿真与测试平台搭建
好,咱们进入第四章。这一章是硬骨头,也是基本功。Verilog 这玩意儿,说白了就是硬件工程师的「普通话」。你设计再牛的神经网络加速器,最终都得靠它落实到门级电路上。我个人习惯把 Verilog 看作「用代码画电路图」,而不是写软件。这个思维转换不过来,后面会踩很多坑。
4.1 模块化设计:把大问题拆成小积木
做芯片设计,最忌讳的就是「一把梭」。一个几百万门的加速器,你不可能在一个文件里写完。模块化设计,就是要把系统拆成功能独立、接口清晰的子模块。
我见过不少新手,喜欢把所有逻辑写在一个 always 块里。结果呢?仿真跑不动,综合报错看不懂,调试起来想砸电脑。你想想看,一个卷积计算单元,你把它拆成「输入缓冲模块」、「乘加树模块」、「累加模块」、「输出缓冲模块」,每个模块单独验证,最后再拼起来,是不是清爽多了?
一个标准的 Verilog 模块长这样:
module conv_mac #(
parameter DATA_WIDTH = 8,
parameter ACC_WIDTH = 16
)(
input wire clk,
input wire rst_n,
input wire valid_in,
input wire [DATA_WIDTH-1:0] weight,
input wire [DATA_WIDTH-1:0] pixel,
output reg [ACC_WIDTH-1:0] result,
output reg valid_out
);
// 内部逻辑写在这里
endmodule
注意看,我用 #(parameter ...) 定义了参数。这样设计出来的模块才是可复用的。我在项目中遇到过,同一个乘加模块,在卷积层用 8bit,在全连接层用 16bit。如果没有参数化,你得维护两套代码,改一个 bug 要改两遍,累死人。
_in 和 _out 后缀,一看就知道方向。时钟和复位永远放在端口列表的最前面,这是行业惯例。
4.2 组合逻辑 vs 时序逻辑:一个管当下,一个管未来
这是 Verilog 里最容易搞混的概念。我简单粗暴地给你总结一下:
- 组合逻辑: 输入一变,输出立刻变。没有记忆功能,不依赖时钟。用
assign或者always @(*)实现。 - 时序逻辑: 在时钟边沿采样输入,下一个时钟周期才输出。有记忆功能,用
always @(posedge clk)实现。
举个例子,一个简单的加法器:
// 组合逻辑加法器
assign sum = a + b;
// 时序逻辑加法器(带寄存器输出)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
sum_reg <= 0;
else
sum_reg <= a + b;
end
看到区别了吗?组合逻辑的 sum 是 wire 类型,时序逻辑的 sum_reg 是 reg 类型。但注意,reg 不一定代表寄存器!在 always @(*) 里赋值的 reg,综合出来还是组合逻辑。这个坑我曾经踩过,调试了整整一天才发现是综合工具把组合逻辑推断成了锁存器。
if-else 和 case 语句覆盖所有情况。
在神经网络加速器里,组合逻辑和时序逻辑各有各的用处。比如:
| 应用场景 | 推荐使用 | 原因 |
|---|---|---|
| 数据通路中的加法、乘法 | 组合逻辑 | 延迟小,适合流水线中的纯计算 |
| 状态机、计数器、累加器 | 时序逻辑 | 需要记忆当前状态 |
| 地址译码、数据选择 | 组合逻辑 | 输入决定输出,无需时钟 |
| 流水线寄存器、FIFO | 时序逻辑 | 数据需要跨时钟域或打拍同步 |
4.3 仿真与测试平台:不仿真就流片?你疯了吗
写代码只占设计工作量的 30%,剩下 70% 都在仿真和调试。测试平台(Testbench)就是你的「虚拟实验台」。没有它,你根本不知道你的设计能不能跑。
一个基本的 Testbench 结构:
`timescale 1ns / 1ps
module tb_conv_mac();
// 参数定义
localparam DATA_WIDTH = 8;
localparam ACC_WIDTH = 16;
localparam CLK_PERIOD = 10; // 10ns = 100MHz
// 信号声明
reg clk;
reg rst_n;
reg valid_in;
reg [DATA_WIDTH-1:0] weight;
reg [DATA_WIDTH-1:0] pixel;
wire [ACC_WIDTH-1:0] result;
wire valid_out;
// 实例化待测模块
conv_mac #(
.DATA_WIDTH(DATA_WIDTH),
.ACC_WIDTH(ACC_WIDTH)
) u_dut (
.clk (clk),
.rst_n (rst_n),
.valid_in (valid_in),
.weight (weight),
.pixel (pixel),
.result (result),
.valid_out(valid_out)
);
// 时钟生成
initial begin
clk = 0;
forever #(CLK_PERIOD/2) clk = ~clk;
end
// 测试激励
initial begin
// 初始化
rst_n = 0;
valid_in = 0;
weight = 0;
pixel = 0;
// 复位释放
#20 rst_n = 1;
// 发送第一个数据
@(posedge clk);
weight = 8'h05;
pixel = 8'h03;
valid_in = 1;
@(posedge clk);
valid_in = 0;
// 等待结果
#100;
// 检查结果
if (result == 16'h0F)
$display("Test Passed: 5 * 3 = 15");
else
$display("Test Failed: expected 15, got %d", result);
#50 $finish;
end
// 波形 dump(用于 GTKWave 查看)
initial begin
$dumpfile("tb_conv_mac.vcd");
$dumpvars(0, tb_conv_mac);
end
endmodule
这里有几个要点:
- 时钟生成: 用
forever循环产生时钟,别手动写几百个#5 clk = ~clk,那是自虐。 - 复位时序: 先拉低复位,等几个时钟周期再释放。我习惯至少等 2 个时钟周期,确保所有寄存器都初始化完毕。
- 自检机制: 用
$display打印结果,用if判断对错。别傻盯着波形图看,眼睛会瞎的。 - 波形 dump: 生成 VCD 文件,用 GTKWave 或 Verdi 查看。这是调试的救命稻草。
4.4 仿真工具的选择与使用
工欲善其事,必先利其器。市面上主流的仿真工具有:
| 工具名称 | 厂商 | 特点 | 适用场景 |
|---|---|---|---|
| Vivado Simulator (xsim) | AMD Xilinx | 与 Vivado 深度集成,免费 | 中小规模设计,FPGA 原型验证 |
| ModelSim / QuestaSim | Siemens EDA | 业界标准,支持 VHDL/Verilog/SystemVerilog | 中大规模设计,团队协作 |
| VCS | Synopsys | 速度快,容量大,价格贵 | 大规模 ASIC 设计,SoC 验证 |
| Icarus Verilog (iverilog) | 开源 | 免费,轻量级 | 学习入门,小模块验证 |
我个人建议,学习阶段用 Icarus Verilog + GTKWave 就够了。完全免费,功能也不差。等你要做真正的项目了,再上 ModelSim 或 VCS。
用 iverilog 跑仿真的命令很简单:
# 编译
iverilog -o tb_conv_mac.vvp tb_conv_mac.v conv_mac.v
# 运行仿真
vvp tb_conv_mac.vvp
# 查看波形
gtkwave tb_conv_mac.vcd
嗯,就这么三行命令。别被那些花里胡哨的 GUI 吓到,命令行才是硬核工程师的浪漫。
4.5 避坑指南:我流过的血,你就不用再流了
最后,分享几个我亲身踩过的坑:
- 阻塞赋值与非阻塞赋值混用: 在同一个 always 块里,组合逻辑用
=,时序逻辑用<=。混用的话,仿真结果和综合结果可能不一致。我曾经因为这个,仿真跑得好好的,综合出来功能全错。 - 敏感列表不完整: 组合逻辑的 always 块,敏感列表要写
@(*),别手写信号列表。漏掉一个信号,仿真结果就是错的。 - 忘记初始化: 仿真时 reg 变量默认是 X(未知态)。如果不初始化,所有依赖它的信号都会变成 X,然后像病毒一样扩散。复位信号一定要在仿真开始就拉低。
- 仿真时间设置太短: 我见过有人设
#100 $finish,结果模块还没跑完就结束了。先设一个足够长的时间,比如#10000,等调试稳定了再优化。
好了,这一章的内容就到这里。模块化设计是骨架,组合逻辑和时序逻辑是血肉,仿真测试是灵魂。这三样东西掌握好了,你就能用 Verilog 搭建出任何数字电路——包括我们后面要讲的神经网络加速器核心。
下一章,咱们开始真正动手,设计一个简单的乘加单元。到时候我会带着你一步步写代码、搭 Testbench、看波形。准备好了吗?