1. 项目概述:基于FPGA的运动目标检测系统
去年用正点原子达芬奇开发板做运动检测时,发现市面上大多数教程都停留在理论层面。这次我们玩点实在的——从OV5640摄像头采集到HDMI输出显示,完整走通运动目标检测全链路。整个系统在Xilinx Artix-7 FPGA上跑出了720P@30fps的实时性能,资源占用控制在70%以内。
硬件选型上,正点原子新起点开发板是个性价比不错的选择。它搭载的XC7A35T芯片虽然算不上高端,但用来处理图像算法绰绰有余。软件环境搭配Vivado 2020.1和Quartus Prime 18.1(用于对比验证),这两个工具链的差异后面会具体说到。
关键提示:新手建议直接使用正点原子提供的约束文件,能省去80%的引脚分配烦恼。我最初自己定义引脚时序,结果图像输出总是闪烁,折腾两天才发现是时钟约束没设对。
2. 核心模块设计与实现
2.1 摄像头配置与数据采集
OV5640的配置堪称第一个拦路虎。这个500万像素的摄像头支持多种输出格式,但实测发现YUV模式在FPGA端处理起来特别吃资源。更明智的做法是强制设为RGB565格式:
verilog复制// I2C配置关键寄存器
i2c_write(0x3017, 0xFF); // 释放复位
i2c_write(0x3034, 0x1A); // MIPI时钟分频
i2c_write(0x4300, 0x06); // 强制RGB输出
时钟配置有个坑:正点原子例程默认用25MHz,但实际接HDMI显示时会出现撕裂。后来用锁相环生成精确的24MHz时钟才解决。数据采集模块要注意:
- 行场同步信号必须用双寄存器打拍消除亚稳态
- 像素数据需要对齐到AXI-Stream总线
- 消隐区数据要主动丢弃
2.2 颜色空间转换优化
RGB转YCbCr是算法链上的第一个关键运算。直接用组合逻辑实现虽然省资源,但要注意定点数精度问题。经过多次试验,这个矩阵运算效果最稳定:
verilog复制wire signed [15:0] Y_temp = (77 * R + 150 * G + 29 * B);
wire signed [15:0] Cb_temp = (128 * B - 43 * R - 85 * G);
wire signed [15:0] Cr_temp = (128 * R - 107 * G - 21 * B);
assign Y = Y_temp[15:8]; // 相当于右移8位
assign Cb = Cb_temp[15:8] + 8'd128;
assign Cr = Cr_temp[15:8] + 8'd128;
特别注意所有中间变量必须声明为signed类型,否则负值运算会出错。实测发现用移位代替除法能节省20%的LUT资源。
2.3 帧差法运动检测
双帧存结构是真双口RAM的典型应用场景。这里采用乒乓操作实现无缝切换:
verilog复制// 帧存控制逻辑
always @(posedge vsync) begin
wr_sel <= ~wr_sel;
rd_sel <= wr_sel;
end
// 差分计算
always @(posedge clk) begin
diff <= (cur_y > prev_y) ? (cur_y - prev_y) : (prev_y - cur_y);
binary <= (diff > threshold) ? 1'b1 : 1'b0;
end
阈值选择有讲究:
- 室内静态场景:15~30
- 室外动态光照:30~50
- 夜间模式:需配合自动增益控制
3. 形态学处理与目标框定
3.1 实时腐蚀膨胀实现
3x3结构元素的形态学运算用移位寄存器最经济:
verilog复制reg [7:0] line_buf [2:0][IMG_WIDTH-1:0];
always @(posedge clk) begin
// 行缓存移位
for(int i=0; i<2; i++)
line_buf[i] <= line_buf[i+1];
line_buf[2] <= {line_buf[2][IMG_WIDTH-2:0], pixel_in};
// 3x3窗口生成
window[0] <= {line_buf[0][col-1], line_buf[0][col], line_buf[0][col+1]};
window[1] <= {line_buf[1][col-1], line_buf[1][col], line_buf[1][col+1]};
window[2] <= {line_buf[2][col-1], line_buf[2][col], line_buf[2][col+1]};
end
腐蚀算法核心是"与"运算:
verilog复制assign erosion = &window; // 所有像素都为1时输出1
而膨胀则是"或"运算:
verilog复制assign dilation = |window; // 任意像素为1时输出1
3.2 目标边界检测
行列投影法虽然简单但效果出奇地好:
verilog复制// X方向边界检测
always @(posedge clk) begin
if(vsync) begin
x_min <= IMG_WIDTH;
x_max <= 0;
end else if(binary_pixel) begin
x_min <= (col < x_min) ? col : x_min;
x_max <= (col > x_max) ? col : x_max;
end
end
画框模块要注意时序对齐:
verilog复制assign draw_box = ((row == y_min || row == y_max) && (col >= x_min && col <= x_max)) ||
((col == x_min || col == x_max) && (row >= y_min && row <= y_max));
4. HDMI输出与系统集成
4.1 视频时序生成
720P时序参数必须严格遵循标准:
verilog复制parameter H_ACTIVE = 1280;
parameter H_FP = 110;
parameter H_SYNC = 40;
parameter H_BP = 220;
parameter V_ACTIVE = 720;
parameter V_FP = 5;
parameter V_SYNC = 5;
parameter V_BP = 20;
时钟生成要用到MMCM:
verilog复制clk_wiz_0 clk_gen (
.clk_out1(clk_pixel), // 74.25MHz
.clk_out2(clk_5x), // 371.25MHz
.reset(reset),
.locked(locked),
.clk_in1(sys_clk)
);
4.2 TMDS编码实现
Xilinx原语使用示例:
verilog复制OBUFDS #(
.IOSTANDARD("TMDS_33")
) obufds_clk (
.I(clk_pixel),
.O(tmds_clk_p),
.OB(tmds_clk_n)
);
5. 调试经验与性能优化
5.1 常见问题排查
-
图像撕裂:
- 检查像素时钟与数据时钟相位关系
- 确认帧存读写指针没有冲突
- 用ILA抓取行场同步信号时序
-
检测框漂移:
- 增加形态学处理迭代次数
- 对边界坐标进行滑动平均滤波
- 调整帧差阈值
-
资源超限:
- 将部分算法改为时分复用
- 使用DSP48E1代替LUT实现乘法
- 优化状态机编码方式
5.2 性能实测数据
| 模块 | 时钟周期数 | LUT占用 | 功耗(mW) |
|---|---|---|---|
| 摄像头接口 | 1 | 423 | 58 |
| RGB2YCbCr | 3 | 687 | 92 |
| 帧差法 | 2 | 312 | 45 |
| 形态学处理 | 5 | 1024 | 136 |
| HDMI输出 | 1 | 298 | 67 |
6. 进阶扩展方向
这套基础框架还可以进一步扩展:
- 多目标跟踪:在边界检测后增加连通域分析
- 动态阈值:根据图像亮度自动调整帧差阈值
- 目标分类:集成轻量级CNN加速器
最近在尝试用HLS实现形态学运算,发现相比RTL版本资源占用多15%,但开发效率提升明显。对于快速原型开发,这或许是个不错的折中方案。
