资讯详情

FPGA运动目标检测实战:帧差法Verilog实现与DDR3缓存设计

📅 2026/10/15 7:36:53 | 华诺云谱 👁 阅读
FPGA运动目标检测实战:帧差法Verilog实现与DDR3缓存设计
最近把一个FPGA运动目标检测的仿真工程完整跑通了硬件用的是手头某原子达芬奇系列开发板软件在Vivado和Quartus两个环境里都验证过。这套代码实现的是基于帧差法的运动目标检测从摄像头采集到HDMI显示一条链路全部打通仿真测试也一并做完了。今天把整个实现思路、模块拆解、关键代码和踩过的坑一次性写清楚。如果你正在做类似的图像处理项目或者手里正好有达芬奇、新起点系列板子想跑图像算法这篇内容可以直接拿来参考。先说清楚这套东西能干什么摄像头对着一个场景当画面里有物体移动时系统会把运动区域实时框出来在显示器上叠加显示。处理帧率受限于摄像头和DDR3带宽实测在720P分辨率下能跑到60fps左右1080P下也能稳定30fps。整个工程代码量大概两千行左右核心模块包括灰度转换、帧差计算、阈值分割、形态学滤波和目标位置标记。FPGA做运动目标检测和用OpenCV在电脑上跑完全是两种思路。电脑上资源充裕可以直接调库、用浮点、上深度学习模型FPGA上所有逻辑都要用硬件资源去搭需要考虑流水线、位宽、资源占用和时序收敛。但FPGA的优势是低延迟、高吞吐、并行处理适合实时性要求很高的场景。这篇文章会把每个关键决策背后的原因讲明白不是光给代码而是让你知道为什么这么做。1. 项目拆解这套FPGA运动目标检测到底在做什么1.1 为什么选帧差法而不是光流和背景建模运动目标检测的经典方法有三种帧差法、背景减除法、光流法。我最初也纠结过选哪种最后综合FPGA资源限制和实时性要求选了帧差法。帧差法原理很直接连续两帧图像做差分像素值变化超过阈值的地方就认为是运动区域。公式就是|I(n) - I(n-1)| TI(n)表示当前帧灰度值T是阈值。这个操作在FPGA上实现非常友好只需要两个数据源做减法、取绝对值、比较阈值三个周期就能出结果适合流水线处理。背景减除法需要在内存里维护一张背景模型并且要不断更新背景以适应光照变化。这个模型在FPGA里实现会比较占资源尤其是自适应更新逻辑往往需要一个额外的帧缓存和复杂的算术运算。光流法精度最高能算出每个像素的运动矢量但涉及矩阵运算和迭代求解硬件实现难度大通常在GPU或者高端DSP上才能满足实时性。对于入门级FPGA开发板从可行性角度讲帧差法是性价比最高的方案。帧差法的缺点也得说清楚它只能检测运动物体的边缘轮廓如果物体颜色和背景接近或者运动速度太慢检测效果会打折扣。最典型的问题是“空洞效应”就是运动物体内部颜色均匀的区域帧差后像素变化值很小会出现中间空洞、只有边缘被检测出来的情况。后文会讲怎么用形态学滤波尽量缓解这个问题。1.2 开发板与开发工具的匹配思路这套工程考虑了不同开发板的兼容性。达芬奇系列和某原子新起点系列在FPGA资源上有差异前者资源相对充裕后者更入门一些所以在代码设计上做了一个核心原则尽量使用通用原语和标准接口避免依赖某一款板子的专属硬件。具体来说工程分成两个版本一个面向Vivado主要使用AXI4接口和Xilinx的Video In/Out IP另一个面向Quartus改为用Altera的Streaming Video IP和PLL。两份代码的核心算法模块灰度、帧差、形态学完全一致只是接口适配层不同。这样做的好处很明显——算法逻辑是平台无关的换板子只需要改接口适配层核心识别逻辑不用动。开发工具版本方面Vivado用的是2018.3以上的版本Quartus Prime 18.1也测过。有一点值得注意Quartus对Verilog-2001的支持很完整但对SystemVerilog的部分语法支持不够好所以在公共模块里统一用Verilog-2001风格编写不用SV的interface和logic类型这样在两个工具里都能直接综合。2. 系统方案设计从镜头到屏幕的数据流该怎么走2.1 整体架构与数据通路整套系统的数据流向是这样的CMOS摄像头输出RAW Bayer格式数据经过摄像头配置和采集模块送到灰度转换模块转成8bit灰度图。灰度数据兵分两路一路直接送进帧差模块作为当前帧输入另一路经过帧缓存模块存在DDR3里延迟一帧时间后作为上一帧数据也送到帧差模块。帧差结果经过阈值分割变成二值图再做形态学滤波去噪最后通过目标定位模块计算运动区域的包围框叠加到原画面上通过HDMI输出。这个流程里最核心的设计点是“如何获得上一帧数据”。很多人第一反应是用一块SRAM存一整帧但720P一帧灰度数据就是921600字节一片普通的SRAM通常只有512KB甚至更小根本装不下。所以正确做法是外挂DDR3通过AXI4接口做帧缓存。这里用的是“双缓冲”思路DDR3里划分两个帧缓冲区当前帧写入Buffer A的同时从Buffer B读出上一帧下一帧写入Buffer B的同时从Buffer A读出。双缓冲可以避免读写同一块内存时的冲突但前提是DDR3带宽要够。2.2 模块划分、位宽与资源预算整个工程按功能拆分成7个模块每个模块职责单一cmos_capture负责CMOS传感器初始化通过I2C配置寄存器接收PCLK像素时钟和同步信号输出24bit RGB数据。rgb2gray将RGB888转成8bit灰度。frame_bufferDDR3读写控制封装对外提供写入端口和读取端口内部处理AXI4协议和地址管理。frame_diff当前帧和上一帧做差分、取绝对值、阈值比较。morph_filter3x3中值滤波和腐蚀膨胀操作。target_loc计算运动区域的包围框坐标通过简单的行列扫描和连续区域合并实现。timing_gen生成HDMI需要的行场同步时序和像素时钟。位宽设计要提前想清楚不然最后改起来很痛苦。灰度统一用8bit差分结果理论上范围是0到255也占8bit。二值化输出是1bit但在形态学滤波中为了利用FPGA的并行比较能力我把二值图扩展成8bit的0/255表示避免数据位宽频繁转换带来的麻烦。坐标寄存器用16bit1080P的宽度1920绰绰有余。资源预算方面达芬奇系列跑这套工程LUT占用约12000FF约15000BRAM约60块DSP几乎不用灰度转换用移位实现没有用乘法器。新起点系列资源少一些也能跑但需要把显示分辨率降到720P并适当减少形态学滤波的窗口大小。一般来说入门级FPGA跑720P的帧差检测是没问题的1080P就建议用资源更充足的板子。3. 仿真代码实现核心模块的Verilog写法拆解3.1 灰度转换用移位运算替代浮点乘法RGB转灰度的标准公式是Y 0.299R 0.587G 0.114B。这个公式里有浮点系数在FPGA里直接做浮点运算是很浪费的。常用做法是把系数放大成整数再右移比如乘以256后再右移8位系数变成76、150、29取整后。但更高效的做法是只用移位和加法连乘法器都不用因为FPGA里乘法和加法都会消耗逻辑资源而移位操作几乎不占资源。我用的近似公式是Y (R (R 1) (R 3) (G 2) (G 3) (G 4) (B 1) (B 2)) 4拆开看就是灰度近似等于(11R 56G 14B) / 16虽然和标准系数有误差但视觉上几乎没有区别而综合后节省了三个DSP乘法器。这个技巧在FPGA图像处理里非常常用凡是遇到固定系数的浮点运算都可以用“放大成整数移位”的方式硬算。有一点要特别提醒移位写法看着简单但一定要加足够的中间位宽。R、G、B都是8bit左移4位后最大是12bit如果中间寄存器只定义8bit高位直接被截掉灰度图会变得一团糟。这块我当时调试时踩过坑波形仿真中看到的输出一直是乱的后来发现是中间变量位宽不够。所有中间加法结果建议至少定义一个REGBITS BW 4的位宽最后再右移截断到8bit。3.2 帧差计算与阈值分割的关键细节帧差模块是整个工程的核心代码本身不长但有几个关键信号要处理对。先看简化版代码module frame_diff #( parameter DW 8, parameter THRESHOLD 32 )( input wire clk, input wire rst_n, input wire [DW-1:0] gray_cur, input wire [DW-1:0] gray_prev, input wire data_valid, output reg [DW-1:0] diff_out, output reg diff_valid ); reg [DW:0] diff_abs; always (posedge clk or negedge rst_n) begin if (!rst_n) begin diff_out 8d0; diff_valid 1b0; end else if (data_valid) begin if (gray_cur gray_prev) diff_abs {1b0, gray_cur} - {1b0, gray_prev}; else diff_abs {1b0, gray_prev} - {1b0, gray_cur}; diff_out (diff_abs THRESHOLD) ? 8d255 : 8d0; diff_valid 1b1; end else begin diff_valid 1b0; diff_out 8d0; end end endmodule两个细节值得讲。第一个是减法运算必须扩展位宽。两个8bit数相减结果范围是-255到255如果直接定义8bit寄存器做减法会出现溢出和补码问题。这里我把两个操作数都扩展成9bit再做差确保不会溢出。第二个是data_valid信号必须参与控制。摄像头输出的像素是在有效数据窗口内才有效的行消隐和场消隐期间PCLK依然在跳变但像素数据是无效的。如果不对valid做判断帧差模块会把消隐期的垃圾数据处理一遍出来的画面会出现一道道横条纹。阈值THRESHOLD选多少是个经验问题。我最初设成16发现室内灯光稍微有点变化整个画面都在闪。后来提高到40又发现运动的暗色物体检测不到了。经过多轮测试8bit灰度下阈值设在25到35之间比较稳。这个值不能做成固定常量最好通过串口或者按键实时调整方便在不同环境光照下用。我在工程里也加了这样的按键调节逻辑上板调试时按几个键就能改变阈值比重新编译快得多。3.3 形态学滤波与目标定位的处理逻辑帧差二值化后的图像有很多噪声点主要来自传感器暗电流和光照波动。直接用这种带噪点的二值图去框目标会出现大量误检小框。所以必须做形态学滤波。我实现的是3x3窗口的“中值滤波膨胀”组合。在FPGA里做3x3窗口滤波绕不开行缓存Line Buffer。3x3窗口意味着同时要访问三行数据最简单的方式是用两个移位寄存器SHIFT_REG原语分别延迟1行和2行这样在任意时刻可以得到同一列上三行的数据。这是Vivado和Quartus都支持的硬件原语不是可综合的普通数组用起来要留意。// 例化两个行延迟模块 shift_reg #( .DW(8), .DEPTH(H_ACTIVE) ) line_delay_1 ( .clk(clk), .din(gray_in), .dout(line1_data) ); shift_reg #( .DW(8), .DEPTH(H_ACTIVE) ) line_delay_2 ( .clk(clk), .din(line1_data), .dout(line2_data) );中值滤波对3x3窗口的9个像素排序取中间值在FPGA里可以用排序网络实现先对三行分别排序再对排序结果的列排序最后取对角中间值。这种方法用比较器比较多但延迟只有3个周期。其实实战里还有个更省资源的做法直接用“最小值和最大值之间取中间”的近似效果略差但只用少数比较器。我因为FPGA资源够用用了完整排序网络如果你用的是资源紧张的板子可以降级成近似方案。目标定位模块的思路是逐行扫描二值图遇到255的像素就记录当前行列然后维护一个滑动窗口同一目标内的像素坐标范围会不断扩张直到连续N行都没有新像素加入就认为一个目标扫描结束输出包围框坐标。这个算法类似简单的连通域标记但在硬件里实现的是“单目标跟踪”和“框坐标更新”逻辑不算复杂缺点是不能同时跟踪多个分离目标。如果需要多目标跟踪就得引入帧间多目标匹配工作量和复杂度会明显上升属于另一个量级的项目了。4. 工程搭建、DDR3调度与综合时序实战4.1 Vivado与Quartus的工程创建差异同一套算法代码在两个工具里搭建工程流程上大同小异但有几个区别需要提前知道。Vivado这边我建议用Block Design的方式搭视频通路。原因很简单Xilinx提供了完整的视频处理IP链包括Video In to AXI4-Stream、VDMA、AXI4-Stream to Video Out这些IP之间用AXI4-Stream协议连接配合AXI4互联实现DDR3读写比自己手写AXI接口省太多时间。算法模块作为自定义IP插入到AXI4-Stream数据流中用Vivado的IP Integrator可以快速完成连线。关键是把时钟和复位逻辑理顺视频处理链路一般跑像素时钟VDMA和DDR3跑200MHz左右的高频时钟两个时钟域之间用异步FIFO衔接。Quartus这边则不同。虽然Altera也有类似的MegaWizard IP但IP种类和接口风格有差异。我在Quartus工程里没有用整套视频IP链而是自己写了简单的FIFO和DDR3控制器封装。这听起来工作量很大实际上Quartus的DDR3 UniPHY IP会帮你生成完整的DDR3控制器对外提供简单的Avalon-MM接口配合FIFO模块就能实现帧缓存。相比Vivado的VDMA这种方式更底层、更可控适合理解原理但开发周期确实更长。4.2 DDR3缓存架构与读写带宽测算帧缓存是整个系统的“血管”DDR3调度出问题画面就会花屏、撕裂、卡顿。先算一笔带宽账你就知道为什么有的板子跑不动1080P。按720P60fps算每帧有效像素是1280x720921600灰度数据8bit也就是约0.9MB。一秒钟60帧数据量约55MB。写入DDR3需要55MB/s带宽读出一帧给帧差模块也需要55MB/s加上显示输出从DDR3读回又是55MB/s总共约165MB/s。这还不算DDR3刷新开销和AXI协议头开销实际有效带宽需求在250MB/s左右。DDR3的带宽通常有几GB/s看起来绰绰有余对吧但问题是DDR3访问有延迟如果读写请求没有充分批处理每次随机访问的带宽利用率可能只有百分之十几。所以DDR3读写调度的核心原则是把二维图像访问转成一维长突发访问。每一行图像数据在DDR3里连续存放读写的时候尽可能一次发满一串长度的突发请求比如一行的1280字节拆成20次64字节的突发写。避免每次只写几个像素就换行那样会产生大量地址切换和bank切换开销带宽利用率会崩掉。还有一个工程上的取巧点VDMA本身自带内部缓存和地址生成逻辑用它的Frame Write和Frame Read通道可以天然地把整帧图像按行突发搬运。我用VDMA的S2MM通道写DDR3用MM2S通道读DDR3两个通道各自独立由VDMA内部状态机调度省去了大量手动管理地址的麻烦。如果你用Quartus就手动实现一套类似的按行突发逻辑逻辑复杂度会高一些但原理一致。4.3 综合时序违例的处理方法这套工程在Vivado里综合后第一次跑时序是有一堆违例的主要集中在两个地方一是像素时钟域内的组合逻辑链太长二是跨时钟域FIFO的同步逻辑。灰度转换模块的问题最典型。Y (R (R1) (R3) ...) 4这个表达式如果不做任何处理直接综合编译器会把所有加法串成一长条组合逻辑链从输入到输出的延迟可能是10ns以上而1080P60Hz的像素时钟约148.5MHz周期只有6.7ns时序必然违例。解决办法是在加法链中间插入寄存器打拍。把大表达式拆成两级先算R和G的中间和再算B和上一步结果的和每级之后都加寄存器。这样组合逻辑长度直接减半。Vivado综合时建议开启-retiming选项会自动做流水线重定时。Quartus也有类似的优化选项。处理完加法链流水线之后又遇到一个问题在模块握手信号上数据路径没有延迟对齐。数据和valid信号之间差了一个周期导致下游模块可能采到无效数据。这个问题最容易出现在自己写的模块拼接时。方法很土但很有效写一个简单的testbench把模块A的输出直接接到模块B的输入对比每个时钟周期两个模块的data_valid和数据是否对齐。仿真过了时序大概率没问题仿真对不上上板必挂。5. 调试实录仿真过不代表上板就稳5.1 仿真的局限与激励构造FPGA开发的常规流程是先仿真后上板但很多新手在仿真阶段就把错误路径走歪了。必须明确一点仿真验证的是逻辑行为不是时序。仿真里看不出DDR3带宽瓶颈看不出组合逻辑延迟更看不出信号完整性问题。仿真过了只能说明功能正确离“能上板跑”还有不小距离。仿真激励的构造上最省事的办法是用文本文件读取像素数据。testbench里用$readmemh读取一张转成txt的灰度图模拟连续两帧图像输入。或者直接在代码里用task生成简单的图像数据比如一帧全黑、一帧带一个白色方块这样帧差结果一目了然。我习惯两种都做先用合成图验证帧差逻辑对不对再用真实图片序列验证算法效果。这里有个常见误区仿真用的图像尺寸和实际分辨率不一致。我见过有人用4x4的测试图仿真逻辑都对一上板接720P摄像头就乱了。原因是很多模块内部的行计数器、帧同步逻辑是依赖具体分辨率的分辨率变了时序自然乱。建议仿真时用至少64x64的图像并且把实际分辨率H_ACTIVE、V_ACTIVE作为参数传入而不是在模块里写死。这样换分辨率只改参数不用改代码。5.2 上板调试的经典故障上板调试我遇到的第一个问题就是花屏。现象是HDMI输出画面整体错位上半部分是绿色噪点下半部分是正常的图像。排查到最后发现是VDMA的帧地址设置问题。VDMA配置成自动地址模式后Frame Buffer地址需要按当前帧率和分辨率计算对齐地址没有按4096字节边界对齐导致读出的数据行错位。解决办法是在DDR3地址分配时给每个帧缓冲区的起始地址做一个对齐宏保证首地址是4096的整数倍。第二个问题是帧差图像闪烁。现象是画面静止时依然有大量白色噪点持续闪动。一开始怀疑是帧差阈值太低调高后确实改善了但没有完全消除。后来发现真正的元凶是DDR3读回数据和写入数据的时钟不同步导致读回的“上一帧”其实混入了当前帧的部分内容。帧差算出的自然有差异。这个问题的根源是跨时钟域处理不彻底。我重新检查了VDMA的读通道和写通道的异步FIFO确认FIFO复位时序正确、且读写的像素对齐后闪烁基本消失。第三个问题比较隐蔽——目标定位框抖动。目标明明没有移动框的位置却在左右晃动。这是因为静止背景下也有一些细小的帧差像素残留在边缘逐行扫描时这些残差像素会被当成目标的一部分导致包围框宽度不稳定。解决办法是在目标定位模块里加了“最小框尺寸过滤”框宽或者框高小于设定值的直接丢弃。加上这个过滤之后框的稳定性明显好多了。5.3 常见问题速查表把这段时间踩过的坑整理成一张表方便大家直接对照排查问题现象可能原因排查方法 / 解决方案输出画面花屏、行错位DDR3帧地址未对齐确认Frame Buffer地址按4096字节对齐核对VDMA地址寄存器静止画面帧差闪烁读回帧数据不完整/不同步检查异步FIFO复位确认读通道像素与写通道严格对齐目标框抖动不稳定帧差残渣噪声并入目标区域在定位模块中增加最小框宽高过滤或增大形态学膨胀次数帧差结果空洞明显运动物体内部颜色均匀适当增大膨胀操作次数让空洞被填充灰度图整体发绿/发紫RGB顺序或位宽分配错误用纯色测试图验证R/G/B通道输入顺序视频流畅但检测延迟大帧差缓存用了多帧延迟确认帧缓存模块只做一帧延迟检查VDMA控制器的Frame Count设置阈值调低就全屏噪点环境光照波动大改用自适应阈值或增加形态学滤波强度Quartus综合资源不足行缓存用了大量分布式RAM改用Block RAM实现Shift Register减少窗口大小这里面特别要说一下“帧差结果空洞”的解决方式。膨胀操作确实可以把空洞填上但膨胀同时会让运动区域的边缘向外扩张如果膨胀次数多了两个靠得近的目标可能会连成一片。所以膨胀次数建议控制在1到2次不要贪多。更精细的做法是用“先闭运算再开运算”即先膨胀后腐蚀再腐蚀后膨胀能同时保留目标轮廓并去除孤立噪声点。代价是逻辑资源和处理延迟翻倍看你的实时性需求决定要不要上。还有一个细节值得说帧差检测对摄像头固定程度非常敏感。如果你用的不是工业级固定支架而是手持或者带有自动对焦的摄像头即使没有任何运动物体对焦过程本身就会引起画面变化导致系统始终认为有运动目标。调试阶段建议先把摄像头固定好、关闭自动对焦再验证算法逻辑。等基本功能稳定后再逐步加入抗干扰处理。最后分享一个实际工程中的体会。这套代码在仿真中跑通了所有功能测试但上板后依然花了整整两天排查DDR3时序和跨时钟域问题。仿真的价值是帮你把算法逻辑收敛到“大概率对”但真正让系统稳定跑起来靠的还是板级调试的耐心和对时序的敏感度。如果你正在做类似项目建议一定要先看懂VDMA和DDR3 IP的手册不要等到花屏了再翻文档那时你会发现自己丢失的不只是时间还有心态。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑