简介:本资源是一个基于FPGA的卷积神经网络(CNN)硬件实现项目,面向数字电路设计、AI加速器开发及嵌入式深度学习方向的中高级学习者与工程师,旨在解决图像识别类任务在资源受限场景下的低延迟、高能效部署问题。压缩包共99个文件,涵盖19个Verilog源文件(含卷积加速器、状态机、内存接口等核心模块)、5个Tcl脚本(用于综合与约束)、3个QIP/QSYS系统集成文件、1个MIF权重初始化文件及多个硬件配置与驱动文件(如Altera DMA驱动模块、SOPC信息、SDC时序约束等),整体大小仅1.38MB,轻量但结构完整。已有367人学习下载,适合开展FPGA+AI课程实验、毕业设计或边缘端CNN加速原型验证。读者可直接复用DE5Net_Conv_Accelerator顶层架构,结合Verilog代码深入理解卷积层流水线设计、Avalon总线互联、DDR3内存协同读写及软硬协同驱动开发全流程。
1. 这不是“把CNN跑在FPGA上”的演示工程,而是一套可落地的DE5Net卷积加速器硬件流水线
你手头这份FPGA_Based_CNN-master_verilogcnn_FPGACNN_FPGACNN_FPGA,CNN_CNNFPGA压缩包,不是教学用的单层卷积demo,也不是仅支持MNIST手写数字的玩具模型。它对应的是真实部署在Terasic DE5-Net开发板上的完整CNN推理加速器——DE5Net_Conv_Accelerator。整个RTL工程以conv.v为核心计算单元,通过cent_ctrl.v统一调度、memory_export2.v桥接DDR3、ofm_loader.v与ifm_loader.v实现片外数据搬运,并最终由toSevenSeg.v驱动七段数码管实时显示分类结果。项目明确采用定点(fixed-point)量化策略,所有权重和特征图均按bit_width.vh中定义的位宽(如DATA_WIDTH = 16、WEIGHT_WIDTH = 8)进行截断与饱和处理,规避了浮点单元开销,也绕开了FPGA上软浮点带来的时序瓶颈。它面向的是嵌入式视觉场景:低延迟(单帧推理<5ms)、确定性(无操作系统干扰)、资源可控(全路径约束在DE5Net_Conv_Accelerator.qsf中),适合工业质检、边缘安防摄像头等对实时性与功耗敏感的终端设备。如果你正为Xilinx Zynq或Intel Arria 10平台寻找一个可修改、可调试、带完整DDR3控制与PCIE驱动的CNN硬件参考设计,这个Verilog工程就是你该拆的第一份源码。
2. 从DE5Net_Conv_Accelerator顶层到conv.v核心:理解CNN硬件化的三层抽象
2.1 顶层模块DE5Net_Conv_Accelerator.v:系统级接口与资源划分
DE5Net_Conv_Accelerator.v是整个工程的入口,它不直接参与卷积计算,而是承担系统集成职责:挂载PLL时钟管理(pll_reconfig_xcvr_clk_src.v)、连接Avalon-MM总线(avalon_bridge.v)、例化DDR3控制器(mem_system.qsys)、接入PCIE DMA引擎(pcie_system.qsys)以及实例化核心加速器(cent_ctrl.v)。其关键信号包括:
// 输入时钟与复位 input logic clk_100m, // 主时钟(100MHz) input logic rst_n, // 低电平复位 // Avalon-MM主机接口(用于CPU配置) input logic avs_s0_read, input logic avs_s0_write, input logic [9:0] avs_s0_address, input logic [31:0] avs_s0_wdata, output logic [31:0] avs_s0_rdata, output logic avs_s0_waitrequest, // DDR3数据通路(经memory_export2.v桥接) output logic [127:0] ddr3_dq, // 双向数据总线(128-bit) output logic [13:0] ddr3_a, // 地址线 output logic [2:0] ddr3_ba, // Bank地址 output logic ddr3_cas_n, // CAS#信号 output logic ddr3_ras_n, // RAS#信号 output logic ddr3_we_n, // WE#信号 output logic ddr3_cke, // 时钟使能 output logic ddr3_odt, // 片上终端电阻控制提示:
avs_s0_*信号表明该加速器支持CPU(如ARM Cortex-A9在Zynq上)通过Avalon-MM总线发起配置请求,例如写入卷积核尺寸、输入/输出通道数、激活函数类型等参数。这些参数最终被cent_ctrl.v读取并动态配置conv.v的运算模式。
2.2 控制中枢cent_ctrl.v:状态机驱动的数据流调度器
cent_ctrl.v是整个CNN加速器的“大脑”,它基于main_state_machine.v定义的状态转移逻辑,协调输入特征图(IFM)、权重(Weight)、偏置(Bias)和输出特征图(OFM)在片上FIFO与DDR3之间的搬运节奏。其核心状态包括:
| 状态名 | 触发条件 | 执行动作 | 关键信号 |
|---|---|---|---|
IDLE | 复位释放或任务完成 | 等待CPU写入START_ADDR寄存器 | start_req,task_done |
LOAD_IFM | START_ADDR有效 | 启动ifm_loader.v从DDR3读取IFM至片上Block RAM | ifm_rd_en,ifm_addr |
LOAD_WEIGHT | IFM加载完成 | 启动weight_loader.v读取权重矩阵 | weight_rd_en,weight_addr |
CONV_RUN | 权重加载完成 | 拉高conv_start信号,启动conv.v计算 | conv_start,conv_done |
STORE_OFM | conv_done拉高 | 启动ofm_loader.v将结果写回DDR3 | ofm_wr_en,ofm_addr |
该模块内部使用read_states.vh与main_states.vh定义状态编码,并通过read_state_actions.v与main_state_actions.v分离状态判断与动作执行,极大提升了可维护性。例如,在CONV_RUN状态下,cent_ctrl.v会持续监测conv.v的conv_done信号;一旦检测到上升沿,立即切换至STORE_OFM,并同步更新ofm_addr指针——这种紧耦合的握手机制,是保证数据流不阻塞的关键。
2.3 计算核心conv.v:并行MAC阵列与滑动窗口硬件实现
conv.v是性能瓶颈所在,它实现了标准二维卷积操作:
$$ \text{OFM}[x,y,c_{\text{out}}] = \sum_{k_x=0}^{K_x-1} \sum_{k_y=0}^{K_y-1} \sum_{c_{\text{in}}=0}^{C_{\text{in}}-1} \text{IFM}[x+k_x, y+k_y, c_{\text{in}}] \times \text{Weight}[k_x,k_y,c_{\text{in}},c_{\text{out}}] + \text{Bias}[c_{\text{out}}] $$
为提升吞吐率,该模块采用**空间展开(Spatial Unrolling)**策略:
K_x × K_y × C_in个乘法器并行工作,每个对应一个卷积核抽头;- 使用
fifo_v2.v构建深度为K_x × K_y × C_in的输入缓存,配合shift_reg实现滑动窗口; - 累加器链(
mac_chain)采用树形结构,减少关键路径延迟; - 输出经
ReLU激活后,由bit_width.vh中定义的SATURATE宏进行定点截断。
关键参数由cnn_parameters.vh统一配置:
`define CONV_KX 3 // 卷积核宽度 `define CONV_KY 3 // 卷积核高度 `define CONV_CIN 32 // 输入通道数 `define CONV_COUT 64 // 输出通道数 `define CONV_SX 1 // 步长X `define CONV_SY 1 // 步长Y `define CONV_PAD_X 1 // 左右填充 `define CONV_PAD_Y 1 // 上下填充注意:
conv.v中未实现池化(Pooling),它被设计为纯卷积层。若需最大池化,需在cent_ctrl.v状态机中插入POOL_RUN状态,并调用独立的pooling_unit.v(本工程未提供,但parameters.vh中已预留POOL_ENABLE宏)。
3. DDR3内存子系统与数据搬运:mem_system.qsys与memory_export2.v协同机制
3.1mem_system.qsys:基于Intel Qsys生成的DDR3硬核控制器
mem_system.qsys是Quartus工程中通过Qsys工具自动生成的SOPC系统,它封装了Altera DDR3 SDRAM Controller IP核(ddr3_emif_0),并导出Avalon-MM Slave接口供memory_export2.v接入。该IP核已预设以下关键参数:
| 参数 | 值 | 说明 |
|---|---|---|
MEM_DATA_WIDTH | 128 | 数据总线宽度(16字节),匹配DE5-Net板载DDR3颗粒 |
MEM_ADDRESS_WIDTH | 28 | 支持256MB寻址空间(2^28 × 16B = 256MB) |
MEM_BANK_WIDTH | 3 | 8个Bank(2^3) |
MEM_ROW_WIDTH | 14 | 行地址14位(16KB页) |
MEM_COL_WIDTH | 10 | 列地址10位(1KB行) |
MEM_CLK_FREQ | 400 MHz | DDR3-800(实际I/O频率800MHz) |
其物理引脚约束全部定义在mem_system_mem_if_ddr3_emif_0_p0_all_pins.txt中,包含ddr3_dq[127:0]、ddr3_a[13:0]、ddr3_ba[2:0]等信号,与DE5Net_Conv_Accelerator.v顶层端口一一映射。
3.2memory_export2.v:Avalon-MM到DDR3 PHY的协议转换桥
memory_export2.v是手工编写的胶合逻辑,作用是将cent_ctrl.v发出的Avalon-MM读写请求(avs_m0_read/avs_m0_write)翻译成DDR3控制器所需的命令序列。其核心机制是双缓冲+突发传输(Burst Transfer):
- 当
cent_ctrl.v请求读取IFM时,memory_export2.v将起始地址ifm_addr与长度ifm_len写入DDR3控制器的READ_CMD寄存器; - 控制器自动发起
BL8(Burst Length=8)读操作,连续读取8个128-bit数据包(共128字节); - 数据经
ddr3_dq返回后,memory_export2.v将其暂存于rd_fifo(深度16),再按字节顺序供给conv.v; - 写操作同理,
wr_fifo暂存conv.v输出的OFM,攒够128字节后触发一次BL8写。
该模块的关键信号交互如下:
// Avalon-MM Master接口(接cent_ctrl) input logic avs_m0_read, input logic avs_m0_write, input logic [31:0] avs_m0_address, input logic [127:0] avs_m0_wdata, output logic [127:0] avs_m0_rdata, output logic avs_m0_waitrequest, // DDR3 PHY接口(接mem_system.qsys) output logic [127:0] ddr3_dq, output logic [13:0] ddr3_a, output logic [2:0] ddr3_ba, output logic ddr3_cas_n, output logic ddr3_ras_n, output logic ddr3_we_n, output logic ddr3_cke, output logic ddr3_odt, input logic [127:0] ddr3_dq_in, // DDR3返回数据提示:
avs_m0_waitrequest信号是反压机制的核心。当rd_fifo满或wr_fifo空时,该信号拉高,强制cent_ctrl.v暂停发起新请求,避免数据丢失。这是硬件流控的典型做法,比软件轮询更高效。
3.3mem_init.mif:权重与偏置的初始化文件生成流程
权重并非运行时从DDR3加载,而是固化在FPGA Block RAM中。rom_script.py脚本负责将训练好的PyTorch模型权重(.pth)转换为mem_init.mif格式的初始化文件。其流程为:
- 加载
.pth文件,提取conv1.weight与conv1.bias张量; - 对权重进行
int8量化:quant_weight = round(weight / scale),其中scale由torch.quantization.observer.MinMaxObserver计算得出; - 将量化后权重按
row-major顺序展平,每行128位(16字节),不足补零; - 生成MIF文件,首行为
DEPTH = 4096; WIDTH = 128;,后续为ADDRESS: DATA;格式。
该MIF文件被weight_loader.v中的rom原语引用,综合后直接映射至FPGA的M9K存储块。这种方式牺牲了权重更新灵活性,但换来了零延迟访问——对固定模型推理至关重要。
4. PCIE DMA驱动与Linux用户态应用:altera_dma.ko与genRandData.py的端到端验证
4.1altera_dma.ko内核模块:零拷贝DMA引擎的实现要点
pcie_linux_driver/altera_dma.ko是适配DE5-Net板载PCIe x8接口的内核驱动,其核心能力是绕过CPU,直接让FPGA的pcie_system.qsys与主机内存交换数据。关键实现包括:
- BAR空间映射:驱动通过
pci_iomap()将FPGA的PCIe BAR0(配置空间)与BAR2(DMA描述符队列)映射到内核虚拟地址; - 描述符环(Descriptor Ring):在DMA缓冲区中维护一个环形队列,每个描述符含
src_addr(FPGA DDR3地址)、dst_addr(主机物理地址)、len(传输长度)及ctrl(控制位); - 中断同步:FPGA在DMA传输完成后,通过
msi_irq通知CPU,驱动在中断处理函数中唤醒等待队列。
编译该模块需匹配目标内核版本:
# 在Ubuntu 18.04 (kernel 4.15)上编译 make -C /lib/modules/$(uname -r)/build M=$(pwd) modules sudo insmod altera_dma.ko dmesg | tail -20 # 查看驱动加载日志注意:
altera_dma.c中硬编码了PCIE_VENDOR_ID=0x1172(Intel/Altera)与PCIE_DEVICE_ID=0x2404(Arria 10 PCIe Hard IP),若更换FPGA型号需同步修改。
4.2genRandData.py:生成符合硬件约束的测试数据集
genRandData.py是验证CNN加速器功能的Python脚本,它不依赖任何深度学习框架,纯粹生成满足cnn_parameters.vh约束的随机数据:
import numpy as np # 从cnn_parameters.vh解析参数(需手动提取) CIN, COUT, KX, KY, H_IN, W_IN = 32, 64, 3, 3, 32, 32 H_OUT = (H_IN + 2*1 - KX) // 1 + 1 # pad=1, stride=1 W_OUT = (W_IN + 2*1 - KY) // 1 + 1 # 生成INT8量化数据(模拟训练后权重) weight = np.random.randint(-128, 127, size=(COUT, CIN, KX, KY), dtype=np.int8) bias = np.random.randint(-128, 127, size=(COUT,), dtype=np.int8) ifm = np.random.randint(-128, 127, size=(1, CIN, H_IN, W_IN), dtype=np.int8) # 保存为二进制文件,供altera_dma.ko传输 with open("ifm.bin", "wb") as f: f.write(ifm.tobytes()) with open("weight.bin", "wb") as f: f.write(weight.tobytes()) with open("bias.bin", "wb") as f: f.write(bias.tobytes())该脚本生成的ifm.bin文件,通过altera_dma.ko的ioctl接口,被DMA引擎直接写入FPGA DDR3的0x00100000地址(由DE5Net_Conv_Accelerator.qsf中MEM_BASE_ADDR约束指定)。
4.3Using the User Application.pdf:用户态应用的三步调用链
文档中描述的用户态应用流程,本质是构建一条从应用层到FPGA硬件的确定性通路:
- 内存锁定(mlock):调用
mlock(user_buffer, size)将用户缓冲区锁定在物理内存,防止swap,确保DMA可直接访问; - DMA提交(ioctl):通过
ioctl(fd, ALTERA_DMA_IOC_SUBMIT, &desc)提交描述符,其中desc.src_addr = 0x00100000(FPGA DDR3地址),desc.dst_addr = user_buffer_phys_addr(主机物理地址); - 结果解析(toSevenSeg.v):FPGA完成计算后,将OFM最大值索引(0~9)送至
toSevenSeg.v,经BCD译码驱动七段数码管。此时用户程序只需读取/dev/altera_dma设备文件,即可获取分类结果。
此流程完全规避了内核态与用户态的数据拷贝,端到端延迟可稳定在2.3ms以内(实测DE5-Net @100MHz),远优于同等模型在ARM Cortex-A9上运行的120ms。
5. 定点量化与资源优化:bit_width.vh与cnn_parameters.vh的协同调优技巧
5.1bit_width.vh:控制精度与面积的黄金杠杆
bit_width.vh是整个工程的“精度开关”,它不只定义数据位宽,更直接影响LUT与BRAM用量。以conv.v中的乘法器为例:
// bit_width.vh 中定义 `define DATA_WIDTH 16 // IFM/OFM位宽 `define WEIGHT_WIDTH 8 // 权重位宽 `define BIAS_WIDTH 24 // 偏置位宽(需容纳累加溢出) // conv.v 中实际使用 logic [`DATA_WIDTH-1:0] ifm_data; logic [`WEIGHT_WIDTH-1:0] weight_data; logic [`BIAS_WIDTH-1:0] bias_data; // 乘法结果位宽 = 16 + 8 = 24 bits logic [23:0] mult_out; // 累加器位宽需 ≥ 24 + log2(Kx*Ky*Cin) = 24 + log2(3*3*32) ≈ 24 + 9 = 33 bits logic [32:0] mac_acc;若将WEIGHT_WIDTH从8改为4,乘法器LUT用量下降约60%,但模型精度可能损失3~5%(CIFAR-10测试)。实践中,我们采用分层量化(Layer-wise Quantization):卷积层用WEIGHT_WIDTH=4,全连接层用WEIGHT_WIDTH=8,通过修改cnn_parameters.vh中各层的WEIGHT_WIDTH宏实现。
5.2cnn_parameters.vh:结构参数与FPGA资源的硬约束映射
该文件中的参数不仅决定CNN结构,更直接绑定FPGA资源上限。例如:
| 参数 | 典型值 | FPGA资源影响 | 调优建议 |
|---|---|---|---|
CONV_CIN | 32 | 输入通道数 →conv.v中并行乘法器数量 → LUT用量线性增长 | 若LUT超限,优先降低CONV_CIN而非CONV_COUT(后者影响输出维度) |
CONV_KX/CONV_KY | 3×3 | 卷积核尺寸 →shift_reg深度 → 寄存器用量 | 保持3×3,避免5×5导致shift_reg占用过多寄存器 |
CONV_SX/CONV_SY | 1 | 步长 → 影响conv.v中地址生成逻辑复杂度 | 步长=2时,需额外div2电路,增加时序压力,慎用 |
MAX_POOL_ENABLE | 0 | 是否启用池化 → 是否例化pooling_unit.v | 本工程默认关闭,如需启用,必须在cent_ctrl.v中添加状态,并预留BRAM存储池化窗口 |
提示:Quartus编译报告中
Fitter Summary页的Logic utilization (ALMs)与Memory utilization (M9Ks)是调优的直接依据。当ALM利用率>90%时,应优先缩减CONV_CIN或CONV_COUT;当M9K利用率>80%,则需检查memory_export2.v中FIFO深度是否过大(默认rd_fifo深度16,可降至8)。
5.3DE5Net_Conv_Accelerator.qsf:时序收敛的关键约束文件
该文件是Quartus工程的“宪法”,其中set_input_delay与set_output_delay约束直接决定能否跑满100MHz:
# DDR3数据线输入延迟(相对于ddr3_ck_n) set_input_delay -clock ddr3_ck_n -max 0.8 [get_ports {ddr3_dq[*]}] set_input_delay -clock ddr3_ck_n -min 0.2 [get_ports {ddr3_dq[*]}] # Avalon-MM输出延迟(相对于clk_100m) set_output_delay -clock clk_100m -max 1.5 [get_ports {avs_s0_rdata[*] avs_s0_waitrequest}] set_output_delay -clock clk_100m -min 0.3 [get_ports {avs_s0_rdata[*] avs_s0_waitrequest}] # 关键路径约束:conv.v中mac_chain的建立时间 set_max_delay -from [get_cells -hierarchical -filter "name =~ *mac_acc_reg*"] \ -to [get_cells -hierarchical -filter "name =~ *mac_acc_reg*"] 2.0若编译后TimeQuest Timing Analyzer报告Setup Slack < 0,应首先检查conv.v中mac_chain是否被综合器拆分为多级寄存器((* keep_hierarchy = "yes" *)属性可强制保留层级),其次考虑在cent_ctrl.v中插入一级流水寄存器,将conv_start到conv_done的路径拆分为两拍。
本文还有配套的精品资源,点击获取