拓冰建站拓冰建站
首页 / 资讯中心 / 正文

FPGA图像处理实战:XC6SLX16驱动OV5640实现实时中值滤波

简介一份基于Xilinx Spartan-6 XC6SLX16 FPGA驱动OV5640摄像头并实现中值滤波的完整Verilog HDL工程适合学习FPGA图像采集与处理、摄像头驱动及硬件描述语言综合仿真的开发者。zip压缩包共755个文件约13.51MB除核心v、vhd、sv源文件外还包含ISE/Vivado工程配置xise、gise、ucf、xdc仿真脚本do、tcl、sh、batIP核与约束文件xco、ngc、asy以及生成报告html、log、xrpt等便于直接打开工程并复现图像采集、滤波链路。已有134人学习资源从FPGA与OV5640通信、图像数据缓存到中值滤波窗口计算均有模块化实现并附带综合、仿真与下载所需脚本可帮助读者完整体验从摄像头驱动到算法落地的流程是图像处理与嵌入式设计领域难得的实操案例。1. 一块LUT不够用为什么要用XC6SLX16做图像管道图像处理入门绕不开一个选择题用树莓派、用ESP32还是用FPGA。实际拆过OV5640之后会发现ESP32采集1080p的帧率被DMA和CPU速度卡得难受而FPGA处理图像强在像素时钟域内完成流水线操作不需要把整帧图像搬进内存再搬出来。XC6SLX16是Spartan-6家族里逻辑单元相对充裕的型号拥有16K逻辑单元和32个DSP48A1片配合软核MicroBlaze或纯逻辑状态机都能驱动OV5640。这个项目的核心路径是OV5640输出并行DVP信号FPGA在行场同步信号驱动下逐像素抓取数据写入三行缓存做3x3中值滤波后再输出。整个过程没有CPU参与逐像素计算这在视频流实时处理场景下尤其有价值适合正在学习FPGA图像处理、想用自己的开发板跑通摄像头采集链路的开发者。下面从驱动协议、采集时序、滤波算法到调试手段逐个拆开讲。2. OV5640寄存器配置与SCCB时序控制2.1 SCCB协议与I2C的兼容性边界OV5640的上电配置依赖两线串行总线官方手册叫SCCBSerial Camera Control Bus。SCCB和I2C协议大部分兼容但有两个容易被忽略的差异SCCB的停止条件不一定在所有传输中都出现而I2C的读操作在ACK后需要额外发一个NACK再产生停止条件SCCB支持连续读和写地址递增模式在寄存器空间内是自动的。实际工程中我一般直接用I2C控制器驱动OV5640但把读写时序改成16位寄存器地址格式。OV5640的SCCB从设备地址是0x788位格式即7位地址0x3C左移一位。上电后摄像头默认输出为QVGA分辨率、YUV422格式频率约15fps这个默认状态不足以支撑后面的中值滤波演示必须通过寄存器配置切换到RGB565输出。// sccb_master.v - 简化版SCCB写寄存器控制器 module sccb_master #( parameter CLK_FREQ 50_000_000 )( input wire clk, input wire rst_n, input wire start, input wire [15:0] reg_addr, input wire [7:0] reg_data, output reg done, inout wire scl, inout wire sda ); localparam TICK_CNT CLK_FREQ / 100_000; // 100kHz SCL // scl分频计数器与状态机省略部分代码 // 状态机包含IDLE、START、WRITE_ADDR_H、WRITE_ADDR_L、 // WRITE_DATA、STOP、WAIT_ACK共7个状态 // 关键scl为高电平时sda跳变表示START/STOP // 数据在scl低电平期间变化高电平期间稳定 endmodule代码逻辑说明sccb_master模块的核心是状态机分频产生100kHz的SCL时钟每个SCL周期传输1bit地址或数据。START条件是SCL高时SDA拉低STOP相反。OV5640要求16位寄存器地址因此一次写操作需要传输器件地址含写标志、寄存器高8位、寄存器低8位、写数据共四个字节每个字节后紧跟一个ACK位。时序中需要特别注意的是停止条件后的总线空闲时间至少为50ns否则传感器状态机可能卡死。2.2 关键寄存器组的选择与配置清单OV5640寄存器数量庞大几百个寄存器不可能全部手动配置。常见做法是先用厂商提供的初始化脚本通常是个数组再在脚本基础上修改输出格式和分辨率。以下是需要重点关注的寄存器区域寄存器地址配置值(示例)功能说明0x38080x07 0x80输出水平分辨率19200x38090x07 0x80高字节低字节组合0x380A0x04 0x38输出垂直分辨率10800x380B0x04 0x38高字节低字节组合0x30340x1A关闭MIPI使用DVP并行输出0x30350x21PLL分频配置0x47400x21HSYNC模式设置0x43000x30输出格式为RGB565这里有一个常见误区很多人只改0x3808-0x380B而不改时钟树相关的0x3034-0x3035导致输出帧率异常或分辨率切换失败。因为OV5640的像素时钟PCLK由MIPI PLL分频而来切换分辨率时如果不重新做PLL参数计算PCLK不满足目标分辨率需要的带宽采集到的图像会出现撕裂或花屏。时序上还有个坑DVP接口的PCLK默认是上升沿采样数据但有的开发板走线较长导致数据建立时间不足可以在0x4740设置为下降沿采样这是后期调试中最容易忽略的调节手段。3. DVP接口采集时序与数据跨时钟域缓存设计3.1 HSYNC与VSYNC的双缓冲策略OV5640的DVP并行接口包含8位或10位数据线D[9:0]、像素时钟PCLK、行同步HSYNC、帧同步VSYNC和场有效信号HREF。在RGB565模式下每个像素需要两个PCLK周期分别传输高字节和低字节VGA分辨率下PCLK频率约为24MHz1080p下则达到72MHz左右。采集模块的核心问题不是抓数据而是正确区分有效像素和消隐区。// cmos_capture.v - 图像采集模块 module cmos_capture #( parameter H_ACTIVE 1920, parameter V_ACTIVE 1080 )( input wire rst_n, input wire pclk, input wire vsync, input wire href, input wire [7:0] din, output reg [15:0] dout, output reg dout_valid, output reg frame_valid ); reg [11:0] h_cnt; reg [11:0] v_cnt; reg vsync_d0, vsync_d1; reg href_d0, href_d1; reg byte_sel; // 边沿检测 : 防止亚稳态 always (posedge pclk or negedge rst_n) begin if(!rst_n) begin vsync_d0 1b0; vsync_d1 1b0; href_d0 1b0; href_d1 1b0; end else begin vsync_d0 vsync; vsync_d1 vsync_d0; href_d0 href; href_d1 href_d0; end end wire vsync_start vsync_d1 ~vsync_d0; // 上升沿检测 wire href_valid href_d1; // 只有在href有效期间才计数 always (posedge pclk or negedge rst_n) begin if(!rst_n) begin h_cnt 0; v_cnt 0; end else if(vsync_start) begin h_cnt 0; v_cnt 0; end else if(href_valid) begin if(h_cnt H_ACTIVE - 1) begin h_cnt 0; v_cnt v_cnt 1; end else begin h_cnt h_cnt 1; end end end // 数据拼接部分省略 endmodule该模块的核心思想是VSYNC上升沿代表新帧开始需要清零行列计数器HREF有效期间每个像素时钟采集一次数据。RGB565需要两个PCLK周期拼成一个16位像素byte_sel用来交替选择高字节和低字节。注意代码中的双寄存器打拍处理这是跨时钟域的最基本防亚稳态手段尤其当摄像头PCLK与FPGA内部时钟不同源时第一级寄存器的输出处于亚稳态的概率不能完全消除但可以大幅降低。采集模块输出的dout_valid信号直接作为写FIFO的写使能不经过额外握手因为图像数据流本身就是连续且背压无关的。3.2 FIFO缓存与BRAM资源的计算方法从PCLK域到系统时钟域的转换用异步FIFO完成。项目里用到了两个FIFO一个用于采集数据缓存位宽16位深度512即可满足VGA分辨率的跨时钟需求另一个用于中值滤波模块的行缓存这个必须用Block RAM实现。XC6SLX16内部只有32个18Kb容量的Block RAMBRAM总存储容量约576Kb。在做1080p图像的3x3滤波时需要缓存两整行图像数据每行1920像素乘以16位等于30720bit两行需要61440bit也就是至少需要4个18Kb BRAM占用12.5%的BRAM资源。但实际设计时还需要考虑输入FIFO和输出FIFO的占用整体BRAM用量会到20%到30%这个比例在XC6SLX16上还算安全。// fifo_512x128b示例参数对应关系 // 实际工程中采用Xilinx FIFO IP核或OpenCores的异步FIFO // 配置参数 // Read Clock Frequency : 100MHz (系统时钟) // Write Clock Frequency : 24MHz (PCLK) // Data Width : 16bit // Depth : 512 // Almost Full Threshold : 480 (预留余量防止溢出)参数说明FIFO深度选512而不是1024因为VGA一帧614400像素但FPGA处理流水线是边采边读的不需要存储整帧图像。真正需要缓存整帧的场景是后续接DDR3或做帧差运算本项目只需缓存几行做滤波所以512深度的FIFO足够缓冲PCLK域与系统时钟域的速率差。Almost Full阈值设置为480是为了给FIFO的读写指针同步留出裕量异步FIFO的读指针同步到写时钟域需要两个时钟周期如果设置成实际深度高频写入时可能丢写请求。这里用到的跨时钟域设计原则是图像数据流优先保证不丢数据处理延迟可以适当放宽。4. 3x3中值滤波窗口生成与排序网络优化4.1 行缓存Shift-RAM做滑动窗口的数据对齐问题中值滤波在FPGA里的实现从数据结构上分两块行缓存生成3x3窗口以及9个数据的排序网络。行缓存最直接的做法是用两个真实的RAM加上读写地址控制但标准做法是用Spartan-6的Shift RegisterSRL16原语或直接用BRAM配置成移位寄存器。Shift-RAM方式在行尾需要清空重建流水线BRAM方式则通过地址连续读写自然实现行延迟。// line_buffer.v - 基于BRAM的行延迟缓存 module line_buffer #( parameter DATA_WIDTH 16, parameter LINE_WIDTH 1920 )( input wire clk, input wire rst_n, input wire wr_en, input wire [DATA_WIDTH-1:0] din, output wire [DATA_WIDTH-1:0] dout ); // 采用Xilinx原生双端口RAM // Port A : 写地址与写数据由像素计数器控制 // Port B : 读地址比写地址延迟一拍输出当前行的前一行像素 // BRAM配置为No-Change模式读操作不影响写数据稳定性 endmodule窗口生成逻辑的难点在于像素对齐窗口的第1行来自当前输入像素第2行来自第一级行缓存输出第3行来自第二级行缓存输出。三个数据流同时进入9个寄存器组成的3x3阵列时必须保证它们对应同一列位置。常见做法是用两级行缓存串联第一级缓存延迟一行第二级缓存在第一级基础上再延迟一行然后当前行数据和两个缓存输出同时打三拍形成完整3x3窗口。这里有个关键参数行缓存深度必须是实际行有效像素数加消隐区宽度如果只填有效像素数行尾的消隐会破坏窗口对齐。4.2 冒泡排序换成三个级排序器能省多少LUT9个数据找中值如果直接用全排序网络做组合逻辑需要比较器约36个每个比较器在Spartan-6上大约消耗3到4个LUT和1个MUX总计约140个LUT。而采用三输入中值器级联的架构可以把比较次数降到约24次节省30%到40%的逻辑资源。工程中采用硬件友好的方法先把3x3矩阵的每行分别用3输入排序器排序得到每行的最小值、中值、最大值然后取三个最小值的最大值、三个中值的中间值、三个最大值的最小值最后对这三个中间结果再取一次中值。// median_filter.v - 3x3中值滤波核心 module median_filter ( input wire clk, input wire rst_n, input wire [15:0] p11, p12, p13, // 窗口第1行像素 input wire [15:0] p21, p22, p23, // 窗口第2行像素 input wire [15:0] p31, p32, p33, // 窗口第3行像素 output wire [15:0] median_out ); // 单行3元素排序器 wire [15:0] row1_min, row1_med, row1_max; sort3 u_sort_row1 (.a(p11), .b(p12), .c(p13), .min_out(row1_min), .med_out(row1_med), .max_out(row1_max)); wire [15:0] row2_min, row2_med, row2_max; sort3 u_sort_row2 (.a(p21), .b(p22), .c(p23), .min_out(row2_min), .med_out(row2_med), .max_out(row2_max)); wire [15:0] row3_min, row3_med, row3_max; sort3 u_sort_row3 (.a(p31), .b(p32), .c(p33), .min_out(row3_min), .med_out(row3_med), .max_out(row3_max)); // 列方向取中值 // max(min列), med(med列), min(max列) // 代入定理max(row_min)与min(row_max)的中间值必为中值 wire [15:0] t1 (row1_min row2_min) ? ((row1_min row3_min) ? row1_min : row3_min) : ((row2_min row3_min) ? row2_min : row3_min); wire [15:0] t2 (row1_med row2_med) ? ((row1_med row3_med) ? row1_med : row3_med) : ((row2_med row3_med) ? row2_med : row3_med); wire [15:0] t3 (row1_max row2_max) ? ((row2_max row3_max) ? row2_max : row3_max) : ((row1_max row3_max) ? row1_max : row3_max); // 最终中值 t1, t2, t3 的中间值 wire [15:0] median_out (t1 t2) ? ((t1 t3) ? ((t2 t3) ? t2 : t3) : t1) : ((t2 t3) ? ((t1 t3) ? t1 : t3) : t2); endmodule这段代码用sort3子模块对每行三个像素做全排序然后列方向通过比较器网络选出最终中值。逻辑耗时约3个时钟周期需要插入寄存器级形成流水线避免组合逻辑路径过长导致时序收敛失败。整个滤波模块在VGA分辨率下的理论处理吞吐量可以达到每像素一个时钟周期即24MHz足够实时处理。中值滤波的窗口大小是可配置参数3x3适合去除椒盐噪声5x5窗口的噪声抑制更强但边缘保留能力减弱且逻辑资源翻倍工程中可根据ISP链路需求调整。提示Spartan-6的LUT是6输入结构sort3子模块用case语句会让综合器映射到LUTRAM务必检查综合报告中的LUT利用率避免意外生成分布式RAM延迟。5. 像素数据流的显示输出配置技巧5.1 用LUT法实现YUV到RGB的快速转换如果直接采集YUV422格式输出到VGA或HDMI需要做色彩空间转换。OV5640的YUV输出按Y0 U0 Y1 V0交错排列而LCD屏通常需要RGB888或RGB565。YUV转RGB的标准公式是R 1.164 * (Y - 16) 1.596 * (V - 128) G 1.164 * (Y - 16) - 0.813 * (V - 128) - 0.391 * (U - 128) B 1.164 * (Y - 16) 2.018 * (U - 128)FPGA里实现浮点乘法资源开销很大工程中一般把系数转成定点数Y系数116/100R的V系数204/128G的V系数102/128G的U系数100/256。或者直接查表把16位YUV数据映射成16位RGB565查找表占用BRAM 64Kb在XC6SLX16上完全可接受且处理延迟固定为1个时钟周期比乘加器DSP流水线更简洁。// yuv_rgb_lut.v - 使用ROM查找表转换颜色空间 module yuv_rgb_lut ( input wire clk, input wire [7:0] y_data, input wire [7:0] uv_data, output wire [15:0] rgb565 ); // 用Xilinx IP生成双端口ROM初始化coe文件 // 地址 {uv_data[7:1], y_data[7:3]}降低查找表规模 // 16Kb ROM方案 // 输入Y取高5位、UV取高7位输出RGB565各分量查表求和 endmodule查表法的代价是颜色精度略降R、G、B各减少1到2个bit精度但对椒盐噪声滤波后的图像显示人眼几乎察觉不到差异。另一种做法是直接在OV5640寄存器配置0x4300设为RGB565输出跳过软件转换流畅性和资源占比都更优前提是你的后续算法链路比如边缘检测、灰度直方图只关心亮度分量。如果后续要接DDR3做帧缓存建议配置为RGB565而不是YUV422因为DDR3的带宽在1080p60fps下已经非常紧张RGB565的16位宽度正好是DDR3最小突发长度的两倍。5.2 时序约束与Xilinx ISE工程管理的落地细节项目工程文件里出现了dvi_pll、DCM_CLKGEN_300M、DCM_CLKGEN_100M等时钟IP整个时钟树设计需要统一规划。建议使用MMCMMixed-Mode Clock Manager而不是DCM虽然Spartan-6的DCM是经典资源但MMCM的抖动性能优于DCM。时钟域划分如下时钟域频率典型值驱动模块clk_pclk24MHz / 72MHz摄像头数据采样clk_sys100MHzFIFO读端、滤波模块、SCCB控制器clk_disp25MHz / 148.5MHzVGA60Hz / HDMI 1080p结合热门搜索里常提到的fpga图像处理路径这个项目完整的流程是SCCB初始化 - 像素同步采集 - FIFO跨时钟域 - Shift-RAM行缓存 - 中值滤波 - 缓存输出 - 显示时序生成。工程里rem_files.bat是ISE环境的清理脚本ise_flow.bat是自动综合脚本使用批处理可以让工程在多环境重复编译中保持一致性避免图形界面手动切换设置导致不同步。写作时的仿真验证不能省略。用Xilinx ISim或Vivado Simulator搭建simulation平台需要产生OV5640的模拟时序参考数据手册的时序参数把HSYNC周期设为1920有效像素280消隐像素VSYNC周期设为1080行40行消隐每像素时钟输出8位数据。仿真中验证中值滤波模块时在bmp测试图里随机注入椒盐噪声点生成bin格式仿真激励文件用$readmemh读入实验数据输出后和Matlab的medfilt23x3窗口结果对比信噪比误差在2%以内说明算法实现正确。这是最直观且可量化的验证手段比单纯看波形更接近工程师的验收标准。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门