
1. 项目概述与设计思路拆解1.1 为什么选FPGA做SAD模板匹配之前在嵌入式平台上尝试过用软件做SAD模板匹配目标跟踪用的是ARM Cortex-A系列处理器640x480分辨率的灰度图搜索窗口稍微大一点帧率就掉到个位数。后来实在扛不住性能压力把目光转向了FPGA方案。这块板子到手之后实测下来同样的分辨率模板匹配跟踪的帧率直接从个位数拉到了100fps以上功耗反而更低。这个项目选择FPGA来做SAD模板匹配本质原因就一个——SAD算法天然具有数据并行性硬件实现可以做到每个时钟周期同时计算多个像素的绝对差累加。SADSum of Absolute Differences绝对差值和算法的数学表达很简单模板T在搜索图I上滑动每个位置计算Σ|I(i,j) - T(i,j)|SAD值最小的位置就是最佳匹配点。说人话就是拿一张小图模板去大图里找最像的地方逐像素比亮度差异差异越小越像。这个算法放到FPGA上做有几个天然优势像素差绝对值的计算可以全并行搜索窗口内的每个像素与模板对应的每个像素做差取绝对值这个操作彼此无依赖一个时钟周期可以启动N个像素的计算累加树可以流水线化加法器摆成树形结构log2(N)级流水线就能完成N个像素的累加吞吐率不随窗口尺寸线性下降多个搜索位置可以分时复用同一套计算单元搜索位置之间没有数据写入冲突读操作天然并行一个SAD计算核可以轮询扫描整个搜索区域1.2 系统整体架构与技术选型整套系统的硬件架构分五块牺牲一点面子工程换来了非常清晰的数据流。第一块是图像采集模块接的是OV5640摄像头分辨率先固定到640x480输出RGB565格式灰度化之后送入算法模块。第二块是DDR3缓存用于存储原始图像帧数据同时给结果叠加显示留带宽。第三块是核心的SAD匹配引擎支持16x16模板搜索范围为64x64像素这部分用纯逻辑实现不跑软核。第四块是目标位置输出模块把匹配结果反馈给显示端同时通过串口发给上位机。最后是VGA显示驱动直接在屏幕上画框标记目标位置。选型上最关键的一点是不要引入软核处理器来做算法主控。最开始我也纠结过用MicroBlaze还是Zynq的PS端后来想通了模板匹配的控制逻辑很简单就是控制搜索范围起始坐标、模板数据加载、结果比较这三个环节用状态机实现完全够用把软核省掉之后时序收敛难度大大降低关键路径长度也更好控制。开发板用的是Xilinx Artix-7系列具体型号XC7A35T资源利用率刚好卡在70%左右。如果实际上板遇到瓶颈可以考虑换用更大容量的芯片但A35T做这个规模的SAD匹配已经够用。2. 核心细节解析与实操要点2.1 SAD匹配算法的硬件化改造软件上写SAD匹配最自然的写法就是三重循环——遍历搜索位置每个位置再遍历模板像素。这种写法在FPGA上直接照搬性能会非常难看。关键问题在于软件是顺序执行硬件是空间换时间。需要把算法重新组织成数据流形态。第一步改造把模板数据缓存到片上RAM。16x16的模板每个像素8bit总共只需要256个存储单元。上电后用串口或者SD卡加载模板图像写入BRAM。匹配过程中模板数据是只读的不存在刷新问题硬件上做成同步RAM就行。第二步改造搜索区域的数据缓存。这是最容易踩坑的地方。搜索区域是64x64的灰度图同样全部缓存到片上显然不现实64x64x8bit 32Kbit虽然不算大但如果后面要扩展到更大搜索范围片上资源会吃紧。更合理的方案是行缓存Line Buffer加滑动窗口外部存储DDR3逐行把搜索区域的图像数据送入FPGA内部用移位寄存器阵列维护一个16x16的滑动窗口每个时钟周期窗口向右滑动一个像素。这样每次只需要从DDR读一个像素的数据SAD计算单元的输入始终保持完整。第三步改造SAD计算单元阵列。16x16的模板窗口硬件上摆256个abs-diff单元和一级加法树。加法树分4级第一级128个加法器第二级64个第三级32个第四级16个最后再一个累加器输出最终SAD值。每一级之间插寄存器打拍形成流水线。这样虽然单个SAD值计算有5个时钟周期的延时但吞吐率是每个时钟周期出1个SAD结果这就是流水线和顺序执行的最大区别。2.2 数据流与时序控制的坑控制逻辑是整个工程里最容易写崩的部分。SAD匹配引擎内部有三层流水窗口推进、SAD计算、结果比较。三层流水之间如果控制信号对不齐结果就会出现偏移而且这类Bug在仿真里很难抓因为单个时钟周期的错位在波形上看很不明显。我的做法是把控制状态机单独拆出来用格雷码编码状态。状态机只干三件事启动信号到来后从DDR的搜索区域起始地址开始逐像素推进行缓存每个时钟周期产生一个窗口有效信号window_valid驱动SAD计算单元工作搜索区域全部扫完之后从结果寄存器中读出最小SAD值对应的坐标。这里有一个细节值得注意SAD计算单元输出的最小值对应的坐标是在窗口推进的第K个时钟周期产生的但这个周期经过加法树的流水延时5拍之后坐标信号早就过去了。所以必须在流水线入口处用移位寄存器把当前坐标信息跟数据一起打拍延后对齐到SAD输出那一拍再锁存。具体做的时候我在窗口有效信号产生的同时把当前窗口的中心坐标写入一个深度6的移位寄存器SAD结果出来的时候从移位寄存器尾部取坐标。这个细节如果没处理好目标跟踪框会一直飘在目标旁边的一个固定偏移位置非常诡异。2.3 模板加载与匹配阈值设定模板加载是在初始化阶段完成的。我把摄像头对准待跟踪目标拍一帧图像截取16x16的区域作为模板通过串口发送到FPGA内部的模板RAM。这样处理的好处是可以在上位机里预览模板内容确认选的目标区域没有大面积遮挡。匹配阈值的问题需要注意。SAD值本身对光照和噪声比较敏感不同场景的绝对数值差异很大。我的做法是用归一化SAD归一化互相关系数的简化版本来替代原始SAD作为判定条件实际实现时先计算模板像素能量和搜索窗口像素能量再做归一化。加了这个步骤之后光照变化的适应性明显改善跟踪目标从室内走到窗边时不会因为亮度差太大而丢失。逻辑实现上归一化可以用查表法近似把模板能量预先算好存下来搜索窗口能量用硬件实时计算然后比大小而不是算除法。SAD值乘上模板能量再跟窗口能量乘上某个阈值做比较等效于归一化但不用除法器节省不少资源。3. 实操过程与核心环节实现3.1 开发环境与工程搭建工程基于Vivado 2019.1开发硬件平台是自己搭的Artix-7核心板加OV5640摄像头模块。整个工程结构分四个IP核图像采集与灰度化模块、行缓存与滑动窗口模块、SAD计算引擎、坐标解析与叠加显示模块。这四个模块用AXI-Stream接口互联数据通路非常清晰。建工程的时候推荐把SAD计算引擎做成独立的模块输入输出接口固定好像素数据、窗口有效信号、坐标信息、SAD结果、匹配完成信号这样后期如果要改成其他匹配算法比如NCC只需要替换这一个模块即可。我在这块板子上后续又实现了归一化互相关算法整个替换过程只花了半天时间。3.2 RTL代码核心片段解析SAD计算引擎的核心代码思路如下以Verilog为例// 16x16 SAD计算引擎 module sad_engine #(parameter WIN_SIZE 16) ( input wire clk, input wire rst_n, input wire [7:0] search_pixel, // 搜索图当前像素 input wire [7:0] template_data, // 模板对应位置像素 input wire window_valid, // 窗口有效 input wire [15:0] current_x, // 当前窗口左上角x坐标 input wire [15:0] current_y, // 当前窗口左上角y坐标 output reg [19:0] sad_result, // SAD结果 output reg result_valid, // 结果有效 output reg [15:0] best_x, // 最佳匹配x坐标 output reg [15:0] best_y // 最佳匹配y坐标 );实现时需要注意窗口内像素的并行读取需要从行缓冲和模板RAM同时取数。我用了256个abs_diff子模块每个子模块计算两个8bit数的绝对差// 绝对差子模块 module abs_diff #(parameter DW 8) ( input wire [DW-1:0] a, input wire [DW-1:0] b, output wire [DW-1:0] d ); assign d (a b) ? (a - b) : (b - a); endmodule加法树我建议用括号分组而不是单层大拼接因为综合工具对深层次括号结构的加法链优化效果更好// 第一级加法32个4输入的加法器替换256个2输入加法器 wire [11:0] sum_stage1 [0:63]; genvar g; generate for (g 0; g 64; g g 1) begin: stage1 assign sum_stage1[g] {2b0, abs_out[g*40]} {2b0, abs_out[g*41]} {2b0, abs_out[g*42]} {2b0, abs_out[g*43]}; end endgenerate实际实现中我推荐先写一个全组合逻辑的版本仿真确认SAD值正确之后再逐步加流水线寄存器这样可以缩小问题排查范围。3.3 搜索窗口遍历的时序控制搜索范围64x64模板16x16实际的搜索位置是 (64-161)^2 2401个。每个位置算一个SAD值需要5个时钟周期流水线深度但因为是流水线操作总时间约等于2401 5个周期。在100MHz时钟下单帧搜索时间约24us加上图像传输时间640x48060fps下每帧约16.6ms完全可以在帧间空闲时间完成搜索。具体做法是VGA/HDMI输出一帧图像的消隐期间vsync或hsync之后产生一个帧开始信号同时启动DDR读取下一帧的搜索区域数据。搜索区域的大小可以根据目标运动速度调节——目标运动快的场景用大搜索窗但帧率会下降目标运动慢的场景用小搜索窗帧率可以拉满。此处有个工程技巧把搜索区域从整帧图像中截取而不是每次都访问DDR中整个帧缓冲。虽然DDR带宽足够但频繁的随机访问时序不好控制固定地址顺序读数据在片上做窗口滑动是更工程化的选择。我实际测试搜索区域64x64实测帧率可以做到130fps以上相比整帧搜索的方案大约40fps提升非常明显。3.4 最小SAD值的在线比较设计2401个SAD值依次从流水线输出需要一个模块实时记录目前看到的最小值和对应的坐标。这个模块逻辑很简单收到新的SAD结果时如果比当前寄存器里的值小就更新寄存器和坐标。扫描结束后寄存器里存的就是全局最优解。但要注意一个边界情况如果图像中存在周期性纹理SAD值可能出现多个相近的局部最小值。我的处理方法是加入一个次小值检查记录最小值和次小值如果两者差距小于10%就判定为匹配歧义跟踪结果保留上一帧的坐标并降低置信度。这个逻辑用硬件实现代价很小但能明显减少跟踪时的抖动。4. 常见问题与排查技巧实录4.1 时序不收敛怎么处理第一次综合后时序报告显示WNS为负关键路径出现在SAD加法树的最后一级到结果比较模块。排查发现是累加器用了阻塞赋值导致综合出的加法链过长。解决办法有两个按优先级排列第一在加法树的每一级之间插入寄存器形成真正的流水线结构这样虽然SAD结果延后几个周期但时钟频率可以轻松拉高第二用DSP48E1的预加器特性实现绝对值差Artix-7里有DSP48E1硬核单个DSP48E1可以完成 |a-b| 操作256个DSP48E1刚好对应16x16模板A35T上有90个DSP48E1确实不够做全并行所以我自己用LUT逻辑搭了256个abs_diff在A35T上逻辑资源反而更充裕。折中方案是拆成两个16x8的半窗口分时复用DSP资源帧率会损失一半但资源占用大幅下降。4.2 匹配结果总偏一个固定偏移这个Bug花了我一整个晚上才定位。现象是跟踪框中心和实际目标中心总有一个常量偏移尤其是目标运动方向改变时偏移方向跟着变。排查思路先确认窗口有效信号与坐标移位寄存器深度是否对齐。我的加法树有5级流水但坐标移位寄存器深度只写了3结果就是坐标比SAD结果早了2拍到达比较模块每次锁存的都是2拍之前的位置。修正方法把坐标移位寄存器深度改成5对齐后一切正常。排查这类问题的方法在Modelsim里拉出SAD结果、窗口有效、坐标寄存器三组信号检查结果有效脉冲对应坐标是否和手动计算一致Vivado里用ILA IP核抓片上信号也是必备手段。4.3 帧率上不去的原因是什么之前一直以为帧率瓶颈在SAD计算单元后来用Vivado的功耗分析发现DDR3读取数据占用了大量时钟周期。搜索区域64x64按顺序读只要4096个像素但实际帧率只能跑到60fps明显是DDR初始化时序和Bank切换占用了太多周期。优化方案在DDR3控制器配置里开了Burst Length8每次突发读8个像素。一次DDR读操作可以喂满8个时钟周期的滑动窗口推进。这样读数据的时间占比下降到可接受范围。另外建议把DDR3控制器频率从200MHz提高到333MHz数据率667Mbps读取带宽翻倍实测帧率从60fps跑到120fps。4.4 低对比度场景下匹配失效跟踪目标穿着深色衣服在暗背景前SAD值整体偏小阈值判定容易出问题。解决方案是引入帧差辅助判定连续三帧的匹配位置如果都不飘就认为目标可信一旦SAD最小值超过正常值1.5倍或者连续两帧匹配位置跳变超过40像素就触发重初始化重新采集模板。5. 工程优化与多目标扩展思路5.1 资源占用与功耗实测整板实测XC7A35T上LUT用了42%FF用了31%BRAM用了7块36Kb换算DSP48E1用了0因为用的是LUT实现SAD功耗约1.7W不包括DDR3功耗。如果换用更小的芯片如XC7S15TLUT占用率会到78%也能跑通但余量不大后期扩展困难。5.2 多目标跟踪扩展方案单个SAD引擎只能跟踪一个目标扩展为多目标有两种思路一种是在片内复制多份SAD引擎每个引擎配独立的模板RAM和坐标寄存器搜索区域可以各自独立代价是资源翻倍另一种是时间片复用单个引擎多个模板轮询计算速率会除以目标数量。我实际测试了双目标跟踪的方案资源激增到LUT 79%、BRAM 13块由于A35T资源紧张最终的组合方案是两个目标共享一个行缓存和DDR通道SAD引擎复制两份。帧率下降到80fps但跟踪稳定性没问题可以满足双目标同时跟踪的需求。5.3 后续可以深入的方向模板更新策略是模板匹配类算法的难点。当前设计里模板是固定不变的目标旋转、尺度变化时容易跟丢。可以考虑在匹配结果周围提取新的候选模板用帧差结果动态更新模板内容相当于一个简易的自适应跟踪器。如果片上资源足够还可以把SAD匹配结果作为候选框输入到卡尔曼滤波模块对目标运动轨迹做平滑预测进一步提升稳定性。6. 经验总结与硬件开发建议FPGA做图像处理算法最大的优势在于确定性。这个项目的所有模块里SAD计算引擎是最简单的部分难度都在数据流控制和时间对齐上。写代码的时候始终记住一条原则用打拍的方式把数据流对齐而不是用标志位来判断什么时候数据有效。后者虽然看起来灵活但在时序验证上非常痛苦。最后分享几个实用的调试习惯。第一每个模块都做独立的仿真环境验证完数据通路再接进顶层否则排错会变成大海捞针。第二Vivado的XSim仿真比ModelSim慢但是和Vivado的集成度高波形直接关联RTL代码定位问题效率高很多两者可以混用。第三上板调试前先把所有IP核的Status引脚引到LED上不用ila也能快速判断哪个模块没起来。这套流程跑顺了开发效率会大幅提升。