本项目是一个基于 FPGA RTL(Verilog)实现的 LeNet 风格手写数字识别前向推理工程。
当前开发重点是行为级仿真验证,不包含完整上板部署流程。
当前工程核心关注点:
- 各层 RTL 模块实现与串联(
conv1/pool_max/conv2/fc1/fc2) - 使用
.mem文件加载输入、权重和偏置进行推理 - 通过 testbench 导出各层输出,支持与 Python 侧逐层对比
配套工具仓库:
- lenet-fpga-toolkit
用于辅助模型训练、量化与参数处理,并将 AI 模型参数转换为本工程可加载的.mem文件。
说明:仓库内未包含 Python 对比脚本;“逐层对比”流程当前从 testbench 导出机制与项目结构推断为在仓库外执行。
| 层级 | 计算 | 输入尺寸 | 输出尺寸 |
|---|---|---|---|
| Conv1 | 5x5x1x6 |
28x28x1 |
24x24x6 |
| MaxPool | 2x2, stride=2 |
24x24x6 |
12x12x6 |
| Conv2 | 3x3x6x10 |
12x12x6 |
10x10x10 |
| FC1 | 全连接 | 1000 |
200 |
| FC2 | 全连接 | 200 |
10 |
对应顶层在 CNN.v 的实例化顺序为:conv1 -> pool_max -> conv2 -> fc1 -> fc2。
层级结构示意图:
conv1RTL:conv1.vpool_maxRTL(文件名pool1_max.v,模块名pool_max):pool1_max.vconv2RTL:conv2.vfc1RTL:fc1.vfc2RTL:fc2.v- 顶层串联:
CNN.v - 顶层 testbench:
tb_cnn.v - 单层 testbench:
tb_conv1.v、tb_pool1_max.v、tb_conv2.v、tb_fc1.v、tb_fc2.v - 各层输出导出到
txt(由tb_cnn.v的$fopen/$fdisplay实现)
- Python 逐层自动比对脚本:仓库中未找到
.py文件,未内置自动对比流程 - 上板部署(bitstream / 约束 / I/O / 驱动链路):仓库未体现完整实现
下面是与复现相关的关键目录(省略 Vivado 自动生成中间文件):
LeNet/
├─ LeNet.xpr
├─ README.md
├─ LeNet.mem/
│ ├─ input_image.mem
│ ├─ conv1_w0.mem ... conv1_w5.mem
│ ├─ conv1_bias.mem
│ ├─ conv2_w0.mem ... conv2_w9.mem
│ ├─ conv2_bias.mem
│ ├─ fc1_weight.mem
│ ├─ fc1_bias.mem
│ ├─ fc2_weight.mem
│ └─ fc2_bias.mem
├─ LeNet.srcs/
│ ├─ sources_1/new/
│ │ ├─ CNN.v
│ │ ├─ conv1.v
│ │ ├─ pool1_max.v (module: pool_max)
│ │ ├─ conv2.v
│ │ ├─ fc1.v
│ │ └─ fc2.v
│ └─ sim_1/new/
│ ├─ tb_cnn.v
│ ├─ tb_conv1.v
│ ├─ tb_pool1_max.v (module: tb_pool_max)
│ ├─ tb_conv2.v
│ ├─ tb_fc1.v
│ ├─ tb_fc2.v
│ └─ pool1_max.v (仿真源目录中的同名文件)
└─ LeNet.sim/sim_1/behav/xsim/
├─ conv1_out_fpga.txt
├─ pool_out_fpga.txt
├─ conv2_out_fpga.txt
├─ fc1_out_fpga.txt
└─ fc2_out_fpga.txt
关键文件作用:
CNN.v:顶层级联和调试信号导出conv1.v:首层卷积,读取conv1_w*.mem与conv1_bias.memconv2.v:第二层卷积,读取conv2_w*.mem与conv2_bias.memfc1.v:全连接层1,读取fc1_weight.mem/fc1_bias.memfc2.v:全连接层2,读取fc2_weight.mem/fc2_bias.mempool1_max.v:2x2 最大池化tb_cnn.v:端到端仿真、逐层打印、输出导出、分类统计
推荐/已验证环境(基于工程文件与目录):
- Vivado 2018.3(
LeNet.xpr标注Vivado v2018.3 (64-bit)) - 仿真器:XSim(Vivado 默认行为仿真)
- 语言:Verilog(
timescale 1ns/1ps) - Python 3.x(用于离线逐层对比,脚本需自行提供)
- PyTorch(如你的对比脚本依赖训练侧权重与量化流程)
- 各 RTL 使用
$readmemh("xxx.mem", ...),文件名均为相对路径 - 不依赖绝对路径;复现时请保证
.mem文件在仿真工作目录可见 - 在 Vivado 中应把
.mem文件加入工程(通常放在 Memory Initialization Files)
- 输入图像:
input_image.mem(784 行,对应28*28)
- Conv1:
conv1_w0.mem~conv1_w5.mem(每个 25 行,对应5*5)conv1_bias.mem(6 行)
- Conv2:
conv2_w0.mem~conv2_w9.mem(每个 54 行,对应6*3*3)conv2_bias.mem(10 行)
- FC1:
fc1_weight.mem(200000 行,对应200*1000)fc1_bias.mem(200 行)
- FC2:
fc2_weight.mem(2000 行,对应10*200)fc2_bias.mem(10 行)
按模块参数与样例数据可确定:
- 输入像素:16bit(
pix_in[15:0],例如0000) - 卷积/FC 权重:16bit(例如
FFEB) - Bias:32bit(例如
FFFFFD6F) - 中间特征与输出:32bit 有符号(
conv_out/pool_out/conv2_out/fc1_out/fc2_out)
负数表示方式:
.mem中负数使用补码十六进制,如FFFFFD6F(32bit 负数)- 波形里出现
F...开头十六进制通常表示有符号负值
以下流程面向首次打开工程用户。
-
打开工程
在 Vivado 中打开LeNet.xpr。 -
检查 Design Sources
确认以下 RTL 在Design Sources:conv1.v、pool1_max.v、conv2.v、fc1.v、fc2.v、CNN.v。 -
检查 Simulation Sources
确认以下 testbench 在Simulation Sources:tb_cnn.v(顶层联调)、tb_conv1.v、tb_pool1_max.v、tb_conv2.v、tb_fc1.v、tb_fc2.v。 -
添加
.mem文件
将LeNet.mem/下全部.mem加入工程。
关键是input_image.mem、conv1_w*.mem、conv2_w*.mem、fc1_weight.mem、fc2_weight.mem及对应 bias。 -
设置仿真顶层
在Simulation Settings中将 Top 设置为tb_cnn(工程当前也是该配置)。 -
运行行为仿真
执行Run Simulation -> Run Behavioral Simulation。 -
查看 Tcl Console 输出
tb_cnn.v会输出每层out[...]日志,并在fc2_done时打印: 各层输出数量、PASS/FAIL、PREDICTED DIGIT与MAX SCORE。 -
查看波形
把关键信号加入 Wave(见“波形观察建议”章节)。
默认导出到仿真运行目录(例如 LeNet.sim/sim_1/behav/xsim/):
conv1_out_fpga.txtpool_out_fpga.txtconv2_out_fpga.txtfc1_out_fpga.txtfc2_out_fpga.txt
conv1_out_fpga.txt/pool_out_fpga.txt/conv2_out_fpga.txt
每行:index ch y x valuefc1_out_fpga.txt/fc2_out_fpga.txt
每行:index value
仓库当前未内置 Python 脚本,推荐外部脚本按以下方式比对:
- 读取同一组
.mem权重与输入 - 按 RTL 同样的数据流顺序生成每层输出
- 与对应
*_out_fpga.txt按index对齐比较
如需从训练模型生成 .mem,可使用:
fc2 的 10 个输出解释:
fc2_out_index表示当前输出神经元索引(0~9)- 单个
index的输出值不是最终分类结果 - 最终类别应由 10 个输出做
argmax得到(tb_cnn.v中以max_score/pred_digit实现)
建议在 Wave 中重点观察以下信号:
- 输入侧:
pix_in、pix_valid、frame_start - Conv1:
conv_out_dbg、conv_out_valid_dbg、conv_busy、conv_done - Pool:
pool_out、pool_out_valid、pool_busy、pool_done - Conv2:
conv2_out、conv2_out_valid、conv2_busy、conv2_done - FC1:
fc1_out、fc1_out_valid、fc1_out_index、fc1_busy、fc1_done - FC2:
fc2_out、fc2_out_valid、fc2_out_index、fc2_busy、fc2_done
观察要点:
frame_start仅在帧起始打一个脉冲- 各层
*_valid的输出数量应匹配理论尺寸 *_done为完成脉冲,不是常高保持
- 检查
.mem是否已加入 Vivado 工程 - 检查仿真运行目录是否可见这些文件
- 检查文件名是否与
$readmemh("xxx.mem", ...)完全一致
- 本工程模块按相对路径读
.mem - 不要把路径写死为本机盘符路径
- 当前实现中
done是状态机完成脉冲,用于事件触发 - 不是“运算结束后一直为高”的电平信号
- 不是
fc2_out_index仅表示当前输出神经元编号- 需要收齐 10 个输出后做
argmax
- 通常不是异常
- 这是补码负数在十六进制下的正常显示
- 优先检查量化规则是否一致:缩放、移位(
SHIFT)、ReLU 开关、饱和截断 - 检查输入顺序和通道展开顺序是否与 RTL 一致
- 检查
.mem是否使用同一版本参数
基于当前代码结构,建议路线如下:
- 完成逐层数值严格对齐(建立可重复的一键对比脚本)
- 固化量化规则(训练侧/导出侧/RTL 侧统一)
- 增加自动化回归(单层 + 顶层仿真 + 数值校验)
- 上板验证(时钟/复位/I/O 接口与约束完善)
- 接入主机通信链路(如 PCIe/XDMA)并联动上位机程序
如需进一步工程化,建议下一步先把“外部 Python 比对脚本”纳入仓库,并与 tb_cnn.v 导出的 txt 建立固定接口格式和回归命令。
