简介:这份资源面向FPGA与数字图像处理方向的开发者及电子类学生,提供一套基于OV7670摄像头、SDRAM缓存与VGA显示的完整工程实现,并集成中值滤波算法以改善画质。系统涵盖IIC寄存器配置、SDRAM控制器读写、VGA时序驱动以及图像去噪等关键环节,适合作为课程设计、毕业设计或FPGA图像处理入门的实战参考。压缩包共576个文件,约11.74MB,以cdb、hdb、v、vhd等综合与源码文件为主,辅以tdf、qip、qsf等工程配置,以及dat、mif、sof等下载与数据文件,另含少量txt说明与图片,目录结构完整,便于按模块查阅。目前已有399人学习下载。通过研究该工程,读者可掌握摄像头数据采集、跨时钟域存储与显示输出的整体链路,理解中值滤波在FPGA上的实现思路,并借鉴SDRAM多端口控制与VGA时序生成方法,为自建图像处理系统提供可复用的代码框架与排错参考。
1. 从一颗摄像头到一块屏幕:OV7670+SDRAM+VGA 这条链路到底能干什么
如果你手头正好有一块 FPGA 开发板,上面焊着 OV7670 摄像头接口、一片 SDRAM 和 VGA 输出口,那这套「OV7670 采集 + SDRAM 缓存 + VGA 显示」的工程就是最值得先跑通的一条完整链路。它做的事情很朴素:摄像头把实时画面送进来,FPGA 把像素写进 SDRAM,再从 SDRAM 读出来按 VGA 时序打到显示器上。听起来简单,但真正动手才会发现,跨时钟域、SDRAM 刷新、VGA 行场同步这几件事凑在一起,翻车点一个接一个。这套工程适合刚接触 FPGA 图像处理、想搞明白「一帧图像怎么从传感器走到屏幕」的开发者,也适合已经会点灯、会跑 UART,但还没碰过视频通路的熟手拿来练手。它不追求高分辨率,也不追求复杂算法,核心价值在于把整条数据通路打通,让你能在此基础上加中值滤波、做图像缓存、改分辨率。
2. 系统架构拆解:三个时钟域、两条数据流、一个缓存核心
2.1 整体框图与模块划分
这套工程的核心模块可以拆成五块:OV7670 初始化配置模块(通常叫 i2c_config 或 sccb_config)、摄像头数据采集模块(capture)、SDRAM 读写控制模块(sdram_ctrl)、跨时钟域 FIFO(async_fifo)、VGA 时序生成模块(vga_timing)。它们之间的关系不是简单的串联,而是围绕 SDRAM 这个共享缓存做分时复用。
摄像头这边,OV7670 输出 PCLK、VSYNC、HREF 和 8 位数据。PCLK 是像素时钟,典型值在 12MHz 到 24MHz 之间,取决于你配置的输出格式。VSYNC 是帧同步,HREF 是行有效。采集模块要在 PCLK 域下把数据拼成 16 位 RGB565,然后写进异步 FIFO,跨到 SDRAM 控制器的时钟域。
SDRAM 控制器这边,通常跑 100MHz 或 50MHz,负责把 FIFO 里的像素按地址写进 SDRAM,同时还要响应 VGA 那边的读请求。VGA 时序模块跑 25.175MHz(640x480@60Hz 的标准像素时钟),从 SDRAM 读数据,生成 RGB 信号和 HSYNC、VSYNC。
这里有个关键点:SDRAM 只有一个物理接口,读写不能同时进行。常见做法是给读和写各分配一个 FIFO,SDRAM 控制器在读写请求之间做仲裁,优先保证读不被饿死,否则屏幕会出现撕裂或闪烁。
2.2 时钟域划分与跨时钟处理
三个时钟域分别是:摄像头 PCLK 域、SDRAM 控制器 CLK 域、VGA 像素时钟域。跨时钟域处理是这套工程最容易出玄学问题的地方。
摄像头到 SDRAM 的跨时钟,用异步 FIFO 解决。FIFO 的写时钟是 PCLK,读时钟是 SDRAM CLK。写使能在 HREF 有效且 VSYNC 表示帧有效时拉高。FIFO 深度一般选 512 或 1024,太浅会溢出,太深浪费 Block RAM。
SDRAM 到 VGA 的跨时钟,同样用异步 FIFO。VGA 这边按像素时钟往外读,SDRAM 控制器按自己的时钟往里写。这里要注意 FIFO 的空满标志不能直接当读写使能,得做一级寄存器打拍,否则亚稳态会让你调半天。
// 异步 FIFO 例化示例:摄像头写、SDRAM 读 async_fifo #( .DATA_WIDTH (16), .FIFO_DEPTH (512) ) u_cam_to_sdram_fifo ( .wr_clk (cam_pclk), // 摄像头像素时钟 .wr_rst_n (rst_n), .wr_en (cam_data_valid), // HREF 有效且帧有效 .wr_data ({cam_d[7:0], cam_d[7:0]}), // 灰度转 RGB565 或直接拼接 .rd_clk (sdram_clk), // SDRAM 控制器时钟 .rd_rst_n (rst_n), .rd_en (sdram_wr_ready), // SDRAM 写通道就绪 .rd_data (sdram_wr_data), .empty (cam_fifo_empty), .full (cam_fifo_full) );这段代码里,wr_data的拼接方式取决于你用的数据格式。如果 OV7670 配置成 RGB565 输出,两个 PCLK 拼一个像素;如果配置成灰度,可以直接复制成 RGB565 的格式。rd_en不能直接接!empty,因为 SDRAM 写通道有自己的节奏,得等控制器发出写允许信号。
2.3 SDRAM 读写仲裁与地址映射
SDRAM 的地址映射决定了你后面怎么加算法。常见做法是把一帧图像存成 640x480 的像素数组,每个像素 16 位,占用 6404802 = 614400 字节。一片 64Mbit 的 SDRAM 有 8MB 空间,存几帧都够。
地址计算方式:行地址和列地址分别对应图像的 y 和 x。比如addr = y * 640 + x,然后把这个线性地址拆成 SDRAM 的 Bank、Row、Column 地址。读写指针各自维护一个计数器,写指针跟着摄像头数据走,读指针跟着 VGA 时序走。
仲裁策略上,我一般会设一个阈值:当写 FIFO 里的数据超过半满,就优先处理写;当读 FIFO 快空,就优先处理读。这样能保证屏幕不会因为读不到数据而黑屏,也不会因为写不进去而丢帧。
// 简化的读写仲裁逻辑 always @(posedge sdram_clk or negedge rst_n) begin if (!rst_n) begin wr_grant <= 1'b0; rd_grant <= 1'b0; end else begin // 读优先:读 FIFO 快空时必须保证读 if (rd_fifo_used < 8) rd_grant <= 1'b1; // 写 FIFO 超过半满且没有读请求时,处理写 else if (wr_fifo_used > 256 && !rd_req) wr_grant <= 1'b1; else wr_grant <= 1'b0; end end这段仲裁逻辑的核心是「读优先,写让路」。因为 VGA 显示对时序要求严格,读不到数据会直接导致画面异常;而摄像头数据短暂丢失,人眼不一定能察觉。
3. 从零跑通:工程搭建、引脚约束与上板调试
3.1 工程目录结构与文件说明
拿到源码后,先别急着编译。把目录结构理清楚,后面改起来才不迷路。典型的工程目录长这样:
ov7670_sdram_vga/ ├── rtl/ │ ├── top.v // 顶层模块,例化所有子模块 │ ├── i2c_config.v // OV7670 SCCB 初始化配置 │ ├── capture.v // 摄像头数据采集 │ ├── async_fifo.v // 异步 FIFO │ ├── sdram_ctrl.v // SDRAM 读写控制器 │ ├── vga_timing.v // VGA 行场时序生成 │ └── clk_gen.v // PLL 时钟生成 ├── sim/ │ └── tb_top.v // 仿真测试平台 ├── constr/ │ └── top.sdc // 时序约束 └── doc/ └── pinmap.xdc // 引脚约束top.v是入口,所有子模块都在这里例化。clk_gen.v通常用 PLL IP 生成三个时钟:摄像头 XCLK(给 OV7670 提供工作时钟,一般 24MHz)、SDRAM 时钟(100MHz)、VGA 像素时钟(25.175MHz)。i2c_config.v负责通过 SCCB 协议配置 OV7670 的寄存器,设置输出格式、分辨率、帧率等。
3.2 关键参数配置:OV7670 寄存器与 VGA 时序
OV7670 的配置是通过写寄存器完成的。常见的配置项包括:输出格式(RGB565/YUV/灰度)、分辨率(VGA/QVGA)、帧率、白平衡、曝光等。如果你只想要灰度图做中值滤波,可以把输出配置成 YUV,只取 Y 分量。
// OV7670 部分关键寄存器配置(简化) // 寄存器地址 0x12: 输出格式控制 // 0x04 = RGB565, 0x00 = YUV localparam REG_COM7 = 8'h12; localparam REG_COM3 = 8'h0C; localparam REG_COM15 = 8'h40; // 配置序列示例 // 设置 RGB565 输出 i2c_write(REG_COM7, 8'h04); // 使能缩放 i2c_write(REG_COM3, 8'h04); // 输出范围 0x00-0xFF i2c_write(REG_COM15, 8'hC0);VGA 时序这边,640x480@60Hz 的标准参数是:像素时钟 25.175MHz,行同步 HSYNC 96 个像素,行后沿 48 个像素,行有效 640 个像素,行前沿 16 个像素,总共 800 个像素时钟一行。场同步 VSYNC 2 行,场后沿 33 行,场有效 480 行,场前沿 10 行,总共 525 行。
// VGA 640x480@60Hz 时序参数 localparam H_ACTIVE = 640; localparam H_FP = 16; localparam H_SYNC = 96; localparam H_BP = 48; localparam H_TOTAL = 800; localparam V_ACTIVE = 480; localparam V_FP = 10; localparam V_SYNC = 2; localparam V_BP = 33; localparam V_TOTAL = 525;这些参数不能随便改,改了之后显示器可能不认。如果你用的是 800x600 或 1024x768 的屏,得重新查时序表。
3.3 上板调试步骤与现象判断
第一步,先不接摄像头,用测试图案验证 VGA 通路。在 SDRAM 里预存一个彩条图案,看屏幕能不能正常显示。如果屏幕不亮,先查 VGA 的 HSYNC、VSYNC 和 RGB 引脚约束对不对,再查像素时钟有没有输出。
第二步,接上摄像头,用 ILA 或 SignalTap 抓 PCLK、VSYNC、HREF 和数据线。如果 PCLK 没有波形,说明 XCLK 没给到摄像头,或者 SCCB 配置失败。如果 HREF 一直不拉高,说明摄像头没有输出有效行,检查寄存器配置。
第三步,看屏幕上的画面。如果画面是雪花或彩色噪点,说明 SDRAM 读写地址对不上,或者 FIFO 溢出。如果画面稳定但有撕裂,说明读写仲裁有问题,读指针追上了写指针。
提示:调试时先把摄像头分辨率降到 QVGA(320x240),减少数据量,等通路稳定了再升到 VGA。
4. 核心代码逐段拆解:采集、缓存、显示三段链路
4.1 摄像头采集模块:HREF 与 VSYNC 的配合
采集模块的核心是判断什么时候数据有效。OV7670 的输出时序是:VSYNC 下降沿表示一帧开始,HREF 高电平表示一行有效,PCLK 上升沿数据稳定。
// 摄像头数据采集:在 PCLK 上升沿采样 always @(posedge pclk or negedge rst_n) begin if (!rst_n) begin cam_data_valid <= 1'b0; cam_pixel <= 16'd0; byte_cnt <= 1'b0; end else begin // HREF 有效且 VSYNC 表示帧有效时,才采集数据 if (href_sync2 && vsync_frame_valid) begin if (!byte_cnt) begin // 第一个字节:高 8 位 cam_pixel[15:8] <= cam_d; byte_cnt <= 1'b1; cam_data_valid <= 1'b0; end else begin // 第二个字节:低 8 位,拼成 16 位像素 cam_pixel[7:0] <= cam_d; byte_cnt <= 1'b0; cam_data_valid <= 1'b1; // 一个像素采集完成 end end else begin byte_cnt <= 1'b0; cam_data_valid <= 1'b0; end end end这段代码里,href_sync2和vsync_frame_valid都是打过两拍的同步信号,避免亚稳态。byte_cnt用来区分高字节和低字节。cam_data_valid只在第二个字节到来时拉高一个 PCLK 周期,表示一个完整的 16 位像素可以写入 FIFO 了。
4.2 SDRAM 控制器:突发读写与刷新
SDRAM 控制器的状态机通常包括:上电初始化、空闲、刷新、写、读。初始化阶段要发送预充电、刷新、模式寄存器设置等命令。刷新计数器每隔 64ms 要发一次刷新命令,否则数据会丢。
// SDRAM 写突发:一次写 8 个 16 位数据 always @(posedge sdram_clk or negedge rst_n) begin if (!rst_n) begin wr_state <= WR_IDLE; wr_addr <= 24'd0; end else begin case (wr_state) WR_IDLE: begin if (wr_req) begin wr_state <= WR_ACTIVE; wr_addr <= wr_base_addr; end end WR_ACTIVE: begin // 发送行激活命令 sdram_cmd <= CMD_ACTIVE; sdram_addr <= {bank, row}; wr_state <= WR_WRITE; end WR_WRITE: begin // 发送写命令,连续写 8 个数据 sdram_cmd <= CMD_WRITE; sdram_addr <= {bank, col}; if (wr_burst_cnt == 3'd7) begin wr_state <= WR_IDLE; wr_burst_cnt <= 3'd0; end else begin wr_burst_cnt <= wr_burst_cnt + 1'b1; end end endcase end end突发长度选 8 还是 16,取决于你的 SDRAM 型号和控制器设计。突发越长,效率越高,但延迟也越大。对于 640x480 的图像,一行 640 个像素,突发 8 的话要发 80 次写命令,突发 16 只要 40 次。
4.3 VGA 显示模块:行场计数与像素输出
VGA 模块的核心是两个计数器:行计数器和场计数器。行计数器从 0 数到 799,场计数器从 0 数到 524。在有效区域内,从 SDRAM 读数据输出到 RGB 引脚。
// VGA 行场计数与同步信号生成 always @(posedge vga_clk or negedge rst_n) begin if (!rst_n) begin h_cnt <= 12'd0; v_cnt <= 12'd0; end else begin if (h_cnt == H_TOTAL - 1) begin h_cnt <= 12'd0; if (v_cnt == V_TOTAL - 1) v_cnt <= 12'd0; else v_cnt <= v_cnt + 1'b1; end else begin h_cnt <= h_cnt + 1'b1; end end end // 同步信号:低电平有效 assign hsync = ~(h_cnt >= H_ACTIVE + H_FP && h_cnt < H_ACTIVE + H_FP + H_SYNC); assign vsync = ~(v_cnt >= V_ACTIVE + V_FP && v_cnt < V_ACTIVE + V_FP + V_SYNC); // 有效显示区域 assign video_active = (h_cnt < H_ACTIVE) && (v_cnt < V_ACTIVE);hsync和vsync的极性要看显示器要求,大部分 VGA 显示器是低电平有效。video_active拉高时,RGB 输出有效像素;拉低时,RGB 输出 0(黑色)。
5. 避坑与排查:那些让我熬夜的典型问题
5.1 屏幕不亮或显示「无信号」
现象:上电后显示器提示无信号,或者一直黑屏。
原因:VGA 的 HSYNC、VSYNC 引脚约束错误,或者像素时钟没有输出。也有可能是 SDRAM 初始化失败,导致读不出数据。
解决:先用示波器或 ILA 抓 HSYNC 和 VSYNC,看有没有波形。如果没有,检查引脚约束和 PLL 配置。如果有波形但屏幕还是不亮,把 RGB 输出固定为某个颜色(比如全红),排除数据通路问题。
5.2 画面雪花或彩色噪点
现象:屏幕上有雪花状噪点,或者颜色完全不对。
原因:SDRAM 读写地址错位,或者 FIFO 溢出导致数据丢失。也有可能是 OV7670 输出格式和采集模块不匹配。
解决:先检查 OV7670 的寄存器配置,确认输出格式是 RGB565 还是 YUV。如果是 YUV,采集模块要取 Y 分量。然后检查 SDRAM 地址计算,确保写地址和读地址对应同一帧的同一位置。
5.3 画面撕裂或滚动
现象:画面上下滚动,或者有水平撕裂。
原因:读写指针不同步,读指针追上了写指针。或者 VSYNC 同步信号没有正确对齐。
解决:在 SDRAM 控制器里加一个帧同步机制,写指针写完一帧后,读指针才从帧头开始读。或者用双缓冲,写第一帧时读第二帧,写完交换。
5.4 中值滤波后画面延迟大
现象:加了中值滤波后,画面明显延迟,甚至卡顿。
原因:中值滤波需要 3x3 窗口,意味着要缓存两行数据。如果直接在像素时钟域做,Block RAM 消耗大,时序也紧张。
解决:把中值滤波放在 SDRAM 读之后、VGA 输出之前,用行缓存(line buffer)存两行,第三行实时进来。滤波后的数据直接送 VGA,不写回 SDRAM。
5.5 SCCB 配置失败
现象:摄像头没有输出,PCLK 有波形但 HREF 一直不拉高。
原因:SCCB 时序不对,或者寄存器地址写错。OV7670 的 SCCB 和 I2C 不完全一样,没有 ACK 也能写。
解决:用逻辑分析仪抓 SCCB 的 SCL 和 SDA,对照 OV7670 数据手册的时序图。确认从机地址是 0x42(写)和 0x43(读)。配置完成后,读回几个关键寄存器验证。
6. 二次开发:从中值滤波到多分辨率适配的改造思路
6.1 加中值滤波的两种位置选择
中值滤波加在哪里,决定了你的资源消耗和时序余量。第一种是加在摄像头采集之后、写 SDRAM 之前。这样做的好处是 SDRAM 里存的就是滤波后的图,VGA 直接读就行。坏处是滤波模块要跑在 PCLK 域,而 PCLK 的频率取决于摄像头配置,可能是 12MHz 也可能是 24MHz,时序约束不好写。
第二种是加在 SDRAM 读之后、VGA 输出之前。这样滤波模块跑在 VGA 像素时钟域,25.175MHz 固定,时序好收敛。但需要额外的行缓存来存两行数据,Block RAM 消耗会大一些。
我一般选第二种,因为 VGA 时序是固定的,调试起来心里有底。行缓存用两个双口 RAM,每个存 640 个 16 位像素,深度 640,宽度 16。
// 3x3 中值滤波的行缓存结构 // line_buffer_0: 存第 N-2 行 // line_buffer_1: 存第 N-1 行 // 当前行数据实时进来,组成 3x3 窗口 wire [15:0] window [0:8]; assign window[0] = lb0_dout; // 第 N-2 行,列 x-1 assign window[1] = lb0_dout; // 第 N-2 行,列 x assign window[2] = lb0_dout; // 第 N-2 行,列 x+1 assign window[3] = lb1_dout; // 第 N-1 行,列 x-1 assign window[4] = lb1_dout; // 第 N-1 行,列 x assign window[5] = lb1_dout; // 第 N-1 行,列 x+1 assign window[6] = cur_data; // 第 N 行,列 x-1 assign window[7] = cur_data; // 第 N 行,列 x assign window[8] = cur_data; // 第 N 行,列 x+1 // 中值计算:取 9 个数的中值 // 常用做法是排序网络,或者分三次取中值中值计算本身可以用排序网络实现,9 个数的中值需要 19 个比较器。如果资源紧张,可以简化成十字形窗口,只取 5 个数。
6.2 分辨率切换的改造点
从 640x480 切到 800x600 或 1024x768,要改三个地方:VGA 时序参数、SDRAM 地址映射、FIFO 深度。VGA 时序参数查标准表就行,SDRAM 地址映射要把行宽从 640 改成 800 或 1024。FIFO 深度也要相应增加,否则高分辨率下容易溢出。
改完之后,先跑测试图案验证 VGA 时序,再接摄像头。如果屏幕显示不全或者偏移,调 H_FP 和 V_FP 参数。
6.3 验证方法:从仿真到上板的检查清单
仿真阶段,用测试平台生成模拟的 VSYNC、HREF 和像素数据,看 SDRAM 读写地址是否连续,VGA 输出是否在有效区域内。上板阶段,先用 ILA 抓关键信号,确认时序关系正确,再看屏幕现象。
我习惯在每次改完代码后,强制走一遍检查清单:时钟有没有锁?复位有没有释放?SDRAM 初始化完成没有?FIFO 空满标志对不对?VGA 同步信号极性对不对?这几项过了,基本不会有大问题。
从那以后我每次改视频通路,都先把测试图案跑通再接摄像头,省得在雪花屏里找bug。希望帮到你。
本文还有配套的精品资源,点击获取