纯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),仅供参考
1万+

被折叠的 条评论
为什么被折叠?



