纯Verilog实现CNN硬件设计

AI助手已提取文章相关产品:

纯Verilog实现CNN卷积网络:卷积层、池化层与全连接层设计(Vivado 2019.2)

在智能摄像头、微型无人机和嵌入式视觉设备中,我们常常面临一个核心挑战:如何在资源极其有限的FPGA上运行神经网络?GPU太耗电,HLS生成的代码不够紧凑,而商用AI加速器又过于昂贵。这时候,一条“硬核”但高效的路径浮现出来—— 用纯Verilog手写整个CNN推理流程

这不是为了炫技,而是出于真实工程需求。当你的目标平台是Artix-7甚至Spartan系列FPGA时,每一组LUT、每一个BRAM块都必须精打细算。本文展示的就是这样一套完全不依赖IP核或高层次综合(HLS)工具的轻量级CNN硬件架构,在Xilinx Vivado 2019.2环境下完成综合与仿真验证。它包含完整的三层结构:卷积层、池化层和全连接层,全部采用定点运算,适合部署于低成本边缘设备。

这套设计的价值在于它的“透明性”和“可控性”。你可以清楚地看到每个乘法累加发生在哪个周期,每一笔数据是如何通过流水线传递的。没有黑盒IP,没有编译器自动插入的延迟,所有的性能瓶颈都可以被精准定位和优化。对于教学、原型开发以及需要极致资源利用率的小型AIoT项目来说,这是一份极具参考价值的实现范例。


卷积层:从数学公式到并行电路

卷积操作的本质是在输入特征图上滑动一个小窗口(即卷积核),对每个位置执行加权求和。以3×3卷积为例,每输出一个像素就需要进行9次乘法和8次加法。如果按串行方式处理,效率极低;但在FPGA中,我们可以将这9个乘法器全部展开为并行结构, 单周期完成一个输出点的计算

关键在于如何高效组织输入数据流。由于图像数据是逐行输入的,当前时刻只能拿到一行像素,而3×3窗口需要三行数据。为此,我们引入 行缓冲(Line Buffer)结构 :用两组寄存器阵列缓存前两行数据,配合当前行构成完整的3×3区域。

// 行缓冲存储
reg [7:0] line_buf0 [0:5];
reg [7:0] line_buf1 [0:5];
reg [7:0] cur_line [0:5];

每当新一行到来时, line_buf0 <= line_buf1 line_buf1 <= cur_line ,实现滑动更新。这种结构虽然占用一定数量的寄存器,但避免了使用Block RAM,节省了更宝贵的片上存储资源。

权重方面,采用 Q4.4 定点格式(4位整数+4位小数),动态范围±7.9375,足以覆盖常见8-bit量化模型中的权重值。例如Sobel边缘检测核可以直接固化为常量:

reg signed [7:0] kernel [0:8] = '{
    -1,  0,  1,
    -2,  0,  2,
    -1,  0,  1
};

MAC单元使用生成语句(generate-for)自动例化9个并行乘法器,并通过树状加法器汇总结果:

wire signed [15:0] mac_res [0:8];
for (i = 0; i < 9; i = i + 1) begin : gen_mac
    assign mac_res[i] = $signed(window[i]) * $signed(kernel[i]);
end
wire signed [15:0] sum_all = ^mac_res; // 简化表示,实际需逐级相加

最终输出截断为16位有符号数,并由valid信号标记有效周期。整个模块工作在流水线模式下,只要输入持续有效,就能保持每个周期输出一个卷积结果。

需要注意的是,上述代码仅为单核单通道示例。实际应用中还需补充:
- 多通道输入的累加逻辑(如RGB三通道分别卷积后叠加);
- 可配置权重接口(可通过外部加载不同卷积核);
- 地址计数器与行列同步信号,确保边界处理正确。


池化层:用状态机做决策

池化层不涉及训练参数,但它在降低计算复杂度方面起着至关重要的作用。最大池化通过对局部区域取最大值来保留最显著特征,同时压缩空间尺寸。2×2 Max Pooling配合同步步长2,可将特征图分辨率减半。

硬件实现的关键问题是如何识别出每一个2×2块的四个角点。由于输入数据是连续流式的,我们必须依靠内部状态机来跟踪当前位置。

reg [1:0] count_xy;  // 00:start, 01:x1, 10:y1, 11:done

状态转移如下:
- 00 :接收第一个点,暂存;
- 01 :第二个水平点到达,与前者比较取大;
- 10 :跳过中间行,等待下一列对齐;
- 11 :右下角点到来,再次比较,输出最终最大值。

case(count_xy)
    2'd0: begin
        buf_reg <= data_in;
        count_xy <= valid_in ? 2'd1 : 2'd0;
    end
    2'd1: begin
        buf_reg <= (data_in > buf_reg) ? data_in : buf_reg;
        count_xy <= 2'd2;
    end
    2'd2:
        count_xy <= 2'd3;
    2'd3: begin
        pooled_out <= (data_in > buf_reg) ? data_in : buf_reg;
        valid_out <= 1;
        count_xy <= 2'd0;
    end
endcase

这个设计假设输入已经按照光栅扫描顺序排列,并且stride匹配。若要增强鲁棒性,建议加入 h_sync v_sync 信号作为帧/行起始标志,以便精确划分块边界。此外,平均池化也可以类似实现,只需将比较器替换为四选一加法器即可。

资源消耗方面,最大池化几乎只用了几个寄存器和比较逻辑,远低于卷积层,非常适合放在紧随其后的流水级中,进一步提升整体吞吐率。


全连接层:复用乘法器,换取面积优势

如果说卷积层追求的是并行性能,那么全连接层的设计哲学则是“以时间换面积”。在一个典型的分类任务中,FC层需要对展平后的特征向量与权重矩阵做矩阵乘法。例如,输入9维向量,输出10类得分,则需执行10组内积运算,每组包含9次乘加。

若全部并行化,将消耗90个乘法器——这对于小型FPGA几乎是不可接受的。因此,我们采用 串行MAC结构 ,复用同一个乘法器依次处理所有项。

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        acc <= 0;
        feat_cnt <= 0;
        out_idx <= 0;
        done <= 0;
    end else if (start && feat_valid && feat_cnt < FEAT_LEN) begin
        acc <= acc + ($signed(feat_in) * $signed(weight_ram[w_idx]));
        feat_cnt <= feat_cnt + 1;
        w_idx <= w_idx + 1;
    end else if (feat_cnt == FEAT_LEN) begin
        result[out_idx] <= (acc[31]) ? 16'd0 : acc[31:16]; // ReLU
        ...
    end
end

权重预先烧录进BRAM或分布式RAM中,地址由 w_idx out_idx 联合索引。每完成一次内积,累加器清零,开始下一轮计算。整个过程持续 OUTPUT_NUM × FEAT_LEN 个周期,虽不如并行快,但仅需一个乘法器和少量控制逻辑,极大降低了资源开销。

激活函数ReLU也在此阶段完成,直接在输出端判断符号位:若结果为负则置零。考虑到后续Softmax通常运行在主机端,此处无需完整实现指数归一化。

值得一提的是, feat_in 是以流式方式输入的,因此顶层模块必须保证特征向量按序送达,且每次启动前重置内部计数器。可以通过添加 ready 信号形成背压机制,防止数据溢出。


系统集成:构建端到端推理流水线

将三个模块串联起来,就构成了一个完整的CNN推理引擎:

[Image Sensor]
     ↓
[Conv Layer] → Valid handshake → [Pooling Layer] → [Flatten] → [FC Layer] → Class ID
     ↑                                ↑                             ↑
Weight ROM                        No Param                   Weight BRAM

各模块之间通过 valid / ready 握手协议通信,形成标准的 握手机制流水线 。上游模块在 valid=1 时表示数据就绪,下游模块拉高 ready 表示可以接收。只有当两者同时为高时,才发生数据传输。

这样的设计带来了两个好处:
1. 弹性缓冲 :当下游忙碌时,上游可暂停发送,避免数据丢失;
2. 频率解耦 :各模块可独立优化时序,不必强求统一工作频率。

在顶层模块中,还需要协调以下控制信号:
- start :触发FC层开始计算;
- reset_count :在每帧图像开始时重置所有计数器;
- enable :根据valid信号链式使能各级模块。

为了便于调试,建议在关键节点添加 (* keep *) 属性,防止综合工具因未连接而误删信号:

reg [15:0] debug_conv_out;
(* keep *) reg debug_valid;

assign debug_conv_out = conv_out;
assign debug_valid = valid_out;

这样可以在ILA中实时观察中间结果,验证功能正确性。


实际部署中的权衡与调优

在真实FPGA平台上运行这套设计时,会遇到一系列典型问题,都需要针对性解决:

资源不足?

→ 使用串行MAC替代并行结构,共享乘法器资源。
→ 将部分权重存在外部SPI Flash,按需加载至BRAM,减少常驻内存占用。

精度下降明显?

→ 采用训练后量化(PTQ)策略,在PyTorch/TensorFlow中导出8-bit整数量化模型,再转换为Q-format定点数。
→ 对激活值进行动态范围校准,避免饱和失真。

时序违例严重?

→ 在关键路径上插入流水级寄存器,尤其是MAC树的中间节点。
→ 合理分配时钟域,必要时使用异步FIFO跨时钟桥接。

数据错位或漏帧?

→ 强制要求输入带有 h_sync / v_sync 同步信号,确保图像帧边界清晰。
→ 在Testbench中模拟真实传感器时序,加入随机延迟测试鲁棒性。

根据估算,在XC7A35T芯片上该系统大致占用:
- LUTs: ~2500
- FFs: ~1800
- BRAM: 2块(用于FC权重和临时缓存)
工作频率可达100MHz以上,满足多数低速视觉应用需求。


教学意义与未来扩展

这套设计最初源于一次本科生毕业课题的尝试,但它展现出的潜力远不止于此。它不仅是理解CNN硬件映射机制的理想入口,也为后续研究提供了可扩展的基础框架。

比如,可以轻松拓展为LeNet-5风格的多层网络:
- 第一层卷积(5×5核,6通道输出);
- 接Max Pooling(2×2);
- 第二层卷积(5×5,16通道);
- 再接Pooling;
- 最后送入FC层分类。

还可以引入近似的批归一化(BN)电路,利用查找表实现均值方差补偿;或者在片上实现Softmax,直接输出概率分布,减少对外部处理器的依赖。

更有前景的方向是与Zynq SoC结合,通过AXI-Stream接口将PS端的数据流喂给PL侧的CNN引擎,形成软硬协同的完整AI系统。此时,Verilog模块可封装为IP,纳入Vivado IP Integrator统一管理。


这种从底层出发、逐级构建的纯RTL实现方式,或许不像现代AI框架那样“一键部署”,但它教会我们的,是如何真正掌控硬件的能力。当我们在深夜调试ILA波形,看着第一个正确分类的手写数字出现在输出端时,那种成就感,正是源于对每一个晶体管行为的理解与驾驭。

而这,也正是FPGA的魅力所在。

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

您可能感兴趣的与本文相关内容

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值