简介这是一套面向FPGA与数字图像处理学习者的完整工程源码围绕OV7670摄像头采集、SDRAM缓存与VGA显示展开并集成中值滤波算法适合电子、集成电路及嵌入式方向的学生与工程师做课程设计、毕业设计或项目练手。资源包共576个文件约11.74MB以Verilog源码.v、VHDL文件.vhd、Quartus工程配置.qpf、.qsf、.qip、综合与仿真报告.rpt、.summary、引脚与时序约束.pin、.sdc以及大量编译缓存与数据库文件为主完整保留了从RTL设计到综合实现的工程痕迹。项目覆盖IIC寄存器配置、SDRAM双端口控制器、VGA时序驱动与中值滤波去噪等关键模块读者可据此理解图像数据流的缓存调度与显示同步机制并参考工程约束与报告排查时序问题。目前已有399人学习下载适合希望打通摄像头采集到屏幕显示全链路的进阶实践者。1. 从一块摄像头到显示器OV7670 SDRAM VGA 这条链路到底能干什么如果你手头正好有一块带 OV7670 摄像头接口和 VGA 输出口的 FPGA 开发板又不想只停留在点灯和跑马灯阶段那这个项目大概率就是你要找的东西。它做的事情很纯粹OV7670 采集图像数据先写进 SDRAM 缓存再由 FPGA 读出并通过 VGA 接口送到显示器上中间还留了中值滤波的处理位置。整条链路跑通之后你就能在屏幕上看到摄像头拍到的实时画面而不是一堆雪花点或者花屏。这套东西适合两类人一类是刚学完 Verilog 基础语法、想找一个完整项目把时序、跨时钟域、存储器控制串起来的新手另一类是做图像采集前端、想拿一个最小系统验证自己算法模块的熟手。它不依赖软核不需要操作系统纯硬件逻辑实现资源占用可控CPLD 也能勉强塞进去。下面我就按实际拆项目的顺序把这条链路从头到尾讲一遍。2. 系统架构拆解四个模块怎么串成一条图像流水线2.1 整体数据流与时钟域划分先看数据怎么走。OV7670 输出的是 8 位并行数据配合 PCLK、VSYNC、HREF 三个同步信号。SDRAM 是单端口或者伪双端口读写共用一套地址和数据总线。VGA 输出需要 25.175MHz 的像素时钟640x48060Hz而 OV7670 的 PCLK 通常在 12MHz 到 24MHz 之间取决于你配置的输出格式和分辨率。这就带来一个核心问题摄像头写入和 VGA 读取是两个不同的时钟域而且 SDRAM 同一时刻只能服务一个方向。常见做法是用一个异步 FIFO 或者乒乓缓冲来隔离但更省资源的方案是让 SDRAM 控制器做时分复用——写优先读在写间隙插入。我一般会把 SDRAM 时钟设成 100MHz然后给写和读各分配时间片保证 VGA 不会断流。整个系统我习惯拆成四个模块ov7670_capture负责把摄像头原始数据打拍对齐输出有效的像素数据和写使能。sdram_ctrlSDRAM 初始化、刷新、读写仲裁对外提供简单的写端口和读端口。vga_timing产生行同步、场同步和像素坐标从 SDRAM 读数据。top例化以上模块连接引脚处理复位和锁相环。中值滤波如果要做通常插在ov7670_capture和sdram_ctrl之间或者放在 VGA 读取之后。放在写入前的好处是 SDRAM 里存的就是处理后的图读取逻辑不用改坏处是滤波模块要跟着 PCLK 跑时序压力大。放在读取后则可以用 VGA 时钟处理但需要额外的行缓冲。这个项目里我建议先跑通直通再考虑加滤波。2.2 OV7670 采集模块的关键时序OV7670 的时序不算复杂但有几个坑。VSYNC 下降沿表示一帧开始HREF 高电平期间数据有效PCLK 上升沿采样。注意 HREF 有效期间并不是每个 PCLK 都有有效数据前几个像素是 dummy。我一般会在 HREF 拉高后延迟几个周期再开始写。// ov7670_capture.v 片段 always (posedge pclk or negedge rst_n) begin if (!rst_n) begin wr_en 1b0; data_out 8d0; end else begin // HREF 有效且延迟计数到位才写 if (href_d1 href_d2 href_d3) begin wr_en 1b1; data_out cam_data; // cam_data 是摄像头原始 8 位 end else begin wr_en 1b0; end end end这里href_d1到href_d3是三级打拍目的是滤掉 HREF 刚拉高时的抖动同时跳过前几个无效像素。参数上延迟级数根据你实际抓到的波形调一般 2 到 4 级都行。cam_data直接接摄像头数据引脚不需要额外缓存因为 SDRAM 写端口会锁存。2.3 SDRAM 控制器的读写仲裁策略SDRAM 部分是这个项目的重头戏。很多人卡在初始化不成功或者读写冲突上。初始化流程是固定的上电后等待 200us然后发预充电、刷新、加载模式寄存器。这些命令的时序参数要严格按芯片手册来比如 tRP、tRFC、tMRD。读写仲裁我一般用状态机实现优先级给写。因为摄像头数据是实时的丢了就丢了VGA 读取晚一点只是画面轻微撕裂人眼不一定看得出来。// sdram_ctrl.v 仲裁片段 localparam IDLE 2d0; localparam WRITE 2d1; localparam READ 2d2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; end else begin case (state) IDLE: begin if (wr_req) state WRITE; else if (rd_req) state READ; end WRITE: begin // 写突发长度 1写完回 IDLE if (wr_done) state IDLE; end READ: begin if (rd_done) state IDLE; end endcase end endwr_req和rd_req分别来自采集模块和 VGA 模块。wr_done和rd_done由 SDRAM 命令序列产生。注意这里没有做读写同时请求的复杂仲裁因为同一时刻只会有一个方向有效——VGA 读取是在消隐期做的而消隐期摄像头可能还在写所以实际项目中我会把读操作安排在 HREF 无效的时间段这样天然避开了冲突。2.4 VGA 时序生成与数据读出VGA 的 640x48060Hz 时序是标准化的行同步 96 个像素行消隐后沿 48 个像素行有效 640 个像素行消隐前沿 16 个像素总共 800 个像素时钟一行。场同步 2 行场消隐后沿 33 行场有效 480 行场消隐前沿 10 行总共 525 行。// vga_timing.v 片段 localparam H_ACTIVE 640; localparam H_FP 16; localparam H_SYNC 96; localparam H_BP 48; localparam V_ACTIVE 480; localparam V_FP 10; localparam V_SYNC 2; localparam V_BP 33; always (posedge vga_clk or negedge rst_n) begin if (!rst_n) begin h_cnt 0; v_cnt 0; end else begin if (h_cnt H_ACTIVE H_FP H_SYNC H_BP - 1) begin h_cnt 0; if (v_cnt V_ACTIVE V_FP V_SYNC V_BP - 1) v_cnt 0; else v_cnt v_cnt 1; end else begin h_cnt h_cnt 1; end end end // 数据有效区域 assign data_valid (h_cnt H_ACTIVE) (v_cnt V_ACTIVE);vga_clk就是 25.175MHz通常用锁相环从 50MHz 晶振分频得到。data_valid拉高时从 SDRAM 读出的像素数据直接送到 VGA 的 RGB 引脚。注意 SDRAM 读数据有 latency一般 2 到 3 个时钟周期所以地址要提前给。我一般会在data_valid前几个周期就开始预取。3. 从零跑通工程搭建、引脚约束与上板验证3.1 工程目录与文件组织拿到源码后先别急着编译。我习惯先把目录理清楚这样后面改起来不迷路。一个典型的工程结构是这样的ov7670_sdram_vga/ ├── rtl/ │ ├── top.v │ ├── ov7670_capture.v │ ├── sdram_ctrl.v │ ├── vga_timing.v │ └── pll.v ├── sim/ │ ├── tb_top.v │ └── sdram_model.v ├── constraint/ │ └── pin.tcl └── doc/ └── timing_note.txtrtl放综合代码sim放仿真测试平台constraint放引脚和时序约束。如果你用的是某公司的开发环境约束文件可能是.qsf或者.ucf但逻辑一样把cam_data[7:0]、pclk、vsync、href、vga_r[4:0]、vga_g[5:0]、vga_b[4:0]、vga_hs、vga_vs这些信号绑到实际引脚上。3.2 锁相环配置与时钟约束锁相环要产生至少两个时钟一个给 SDRAM 控制器通常 100MHz一个给 VGA25.175MHz。如果摄像头 PCLK 直接来自 OV7670 内部那它就是一个异步时钟需要做跨时钟域处理。# pll 配置示例某公司 IP 核 create_clock -name clk_50m -period 20.000 [get_ports clk] create_clock -name clk_vga -period 39.722 [get_ports vga_clk] create_clock -name clk_sdram -period 10.000 [get_ports sdram_clk]clk_vga的周期是 1/25.175MHz ≈ 39.722ns。clk_sdram是 100MHz周期 10ns。注意 SDRAM 时钟输出到芯片时会有相位偏移一般用锁相环的相移功能调 90 度或者 180 度保证数据在时钟中间采样。这个参数需要根据板子实际布线调没有万能值。3.3 上板调试步骤与现象判断编译通过后下载到 FPGA。先别接摄像头用仿真或者内部产生的测试图案验证 VGA 通路。我一般会在top里加一个测试模式用一个计数器产生彩条直接送到 VGA 引脚。如果屏幕上出现稳定的彩条说明 VGA 时序和锁相环没问题。然后接上摄像头观察现象屏幕全黑检查 OV7670 的复位和电源确认 SCCB 配置是否成功。很多模块默认输出 RGB565但如果你没配置它可能输出 YUV颜色会不对。屏幕花屏或者滚动SDRAM 读写冲突或者时序不满足。用逻辑分析仪抓 SDRAM 的 CAS、RAS、WE 信号看命令间隔是否满足 tRC、tRAS。图像偏色或者错位HREF 延迟没调对或者 SDRAM 地址映射搞错了。检查写入地址和读出地址是否一致。提示调试 SDRAM 时先把时钟降到 50MHz 跑通再逐步升到 100MHz。高频下时序余量小容易翻车。4. 中值滤波插入位置与资源权衡4.1 3x3 窗口生成与排序网络中值滤波的核心是取一个 3x3 窗口的 9 个像素排序后取中间值。在 FPGA 里做排序最直接的是排序网络用比较器搭。9 个数的中值可以用 19 个比较器实现比冒泡排序省资源。// median_filter.v 片段三级流水线排序 // 第一级两两比较 wire [7:0] a1, a2, a3, a4, a5, a6, a7, a8, a9; // ... 窗口数据赋值 // 第二级分组排序 // 第三级取中值 assign median /* 排序网络输出 */;实际写的时候我会用三个行缓冲line buffer来缓存前两行加上当前行组成 3x3 窗口。行缓冲用双端口块 RAM 实现深度 640位宽 8 位。每来一个像素窗口右移一列同时更新行缓冲。4.2 滤波前后资源占用对比不加滤波时整个工程在中等规模 FPGA 上大概占用 800 到 1200 个逻辑单元SDRAM 控制器占大头。加上中值滤波后逻辑单元增加约 300 到 500 个块 RAM 增加 2 到 3 个。如果 CPLD 资源紧张可以把滤波关掉用宏定义或者参数控制。模块逻辑单元约块 RAM约采集 SDRAM VGA10001加中值滤波14003这个数字不是绝对的取决于综合工具和优化选项。但趋势是明确的滤波的代价主要在行缓冲和排序网络。4.3 滤波位置对时序的影响前面说过滤波放在写入前还是读取后时序压力不同。放在写入前滤波模块要跟着 PCLK 跑如果 PCLK 是 24MHz那 3x3 窗口的更新和排序必须在一个 PCLK 周期内完成或者用流水线。放在读取后可以用 VGA 时钟25.175MHz 和 PCLK 差不多但读取后的数据是已经缓存的不存在丢帧问题。我一般会放在写入前因为这样 SDRAM 里存的就是干净图VGA 端不用改。但如果你发现时序不收敛就挪到读取后加一个 FIFO 做缓冲。5. 避坑与常见问题排查5.1 现象VGA 显示正常但摄像头图像全黑原因OV7670 没有正确初始化。很多模块上电后默认输出是关闭的需要通过 SCCB 写寄存器配置输出格式、分辨率、时钟分频等。如果你只接了数据线和同步线没接 SCCB那摄像头根本不工作。解决确认 SCCB 的 SCL 和 SDA 是否接到 FPGA并在上电后执行一段配置序列。常见配置包括设置 COM7 为 RGB565、COM3 的时钟分频、CLKRC 的内部分频等。配置序列可以存在 ROM 里上电后依次读出。5.2 现象图像有横向条纹或者抖动原因SDRAM 读写冲突或者刷新不及时。SDRAM 需要定期刷新一般 64ms 内刷新 8192 次平均 7.8us 一次。如果刷新被读写长时间打断数据会丢失。解决在 SDRAM 控制器里加一个刷新计数器优先级高于读写。刷新命令插入时当前读写操作要能暂停。我一般会把刷新周期设成 7.5us留一点余量。5.3 现象图像颜色偏绿或者偏紫原因RGB 数据位序搞反了。OV7670 输出 RGB565 时第一个字节是 R[4:0] 和 G[5:3]第二个字节是 G[2:0] 和 B[4:0]。如果你直接按字节拼接颜色肯定不对。解决在采集模块里做位拼接把两个字节合成 16 位 RGB565再送到 VGA 的 RGB 引脚。VGA 如果是 5-6-5 接口直接对应如果是 4-4-4还要做截断。5.4 现象屏幕上有雪花点或者随机噪点原因跨时钟域没有同步或者 SDRAM 读数据时采样点不对。PCLK 和 SDRAM 时钟是异步的写使能和数据直接跨域会亚稳态。解决在写端口加一个异步 FIFO或者至少用两级触发器同步写使能。SDRAM 读数据时用锁相环把 SDRAM 时钟相移 90 度在数据稳定窗口采样。5.5 现象编译通过但下载后无反应原因引脚约束错了或者锁相环没有锁定。有些开发板的 VGA 接口是电阻网络 DAC引脚电平标准要设成 3.3V LVTTL不能设成 LVDS。解决对照原理图逐个检查引脚号确认锁相环的 locked 信号拉高后再释放复位。可以在 top 里加一个 LED 指示 locked 状态方便判断。6. 二次开发把中值滤波换成其他算法以及验证方法跑通直通之后你可能会想换算法。这个项目的结构对二次开发很友好因为图像数据在 SDRAM 里是线性存放的你只需要改写入前的处理模块或者改读取后的处理模块。我一般会先做一个「算法旁路」开关用一个参数或者拨码开关控制是否启用滤波。这样调试时可以先关掉算法确认通路没问题再打开算法看效果。具体做法是在top里加一个多路选择器// top.v 片段算法旁路 wire [7:0] raw_data; wire [7:0] filtered_data; wire [7:0] final_data; assign final_data bypass ? raw_data : filtered_data;bypass可以接一个拨码开关也可以接一个寄存器通过串口或者按键控制。这样你上板后可以实时切换对比效果。换成其他算法时比如均值滤波或者高斯滤波接口是一样的输入 3x3 窗口输出 8 位像素。你只需要替换median_filter模块的内部实现外部的行缓冲和窗口生成不用动。均值滤波更简单把 9 个数相加除以 9 就行但除法在 FPGA 里占资源可以用移位近似。高斯滤波需要加权权重系数可以存在 ROM 里。验证方法上我习惯分三步走仿真验证用测试平台产生已知图案比如全白、全黑、渐变看输出是否符合预期。在线抓图用逻辑分析仪或者 FPGA 厂商的在线调试工具抓取 SDRAM 写入和读出的数据对比滤波前后的值。上板对比用同一场景切换旁路开关肉眼观察噪声是否减少边缘是否保留。有一个细节容易被忽略中值滤波对椒盐噪声效果好但对高斯噪声一般。如果你发现滤波后图像变模糊可能是窗口太大或者排序网络有 bug。我一般会先用 MATLAB 或者 Python 模拟一遍算法确认系数和窗口没问题再移植到 Verilog。从那以后我每次做图像处理项目都会先写一个行为级模型跑一遍再动手写 RTL。这样能省下大量上板调试的时间。希望帮到你。本文还有配套的精品资源点击获取