☰
FPGA图像采集入门:OV7725摄像头与TFT-LCD实时显示工程详解
2026/10/27 17:42:21 网站建设 项目流程

简介:这是一套OV7725摄像头实时采集图像并在RGB TFT-LCD屏上显示的FPGA实验工程,使用Verilog语言编写,目标器件为Cyclone IV E系列中的EP4CE10F17C8,Quartus 18.0版本可直接编译运行。工程包含OV7725的SCCB寄存器配置模块、CMOS图像采集模块、SDRAM读写缓存模块以及RGB LCD显示驱动模块,完整演示了从摄像头像素输入到屏幕显示的图像处理链路,也展示了跨时钟域数据同步与流水线控制的基本思路。压缩包内共有366个文件,主要涵盖Verilog源码文件、Quartus工程配置与约束文件、IP核描述文件、综合后生成的下载文件等,整体大小约8.66MB,目录结构按功能模块划分,便于检索和复用。目前已有五百六十六人学习下载,适合正在学习FPGA图像采集与显示的学生、参加电子设计竞赛的队员以及相关项目开发者参考,可借此快速理解OV7725驱动方式、SDRAM调度策略和LCD时序控制,并在此基础上进行二次开发。

1. 这事到底在做什么:OV7725抓图、TFT-LCD显示,一套最小的实时图像通路

OV7725摄像头加640x480 RGB TFT-LCD屏显示实验,是FPGA图像采集最常被拿出来复刻的入门工程。它用Verilog在Quartus里搭出“摄像头采集 → 像素拼接 → 数据缓存 → LCD驱动”的完整数据通路,最终把OV7725拍到的画面实时刷到TFT-LCD上。这个工程不点LED、不跑UART,而是同时驾驭SCCB寄存器配置、DVP时序解析、跨时钟域缓存和RGB像素时序,非常适合想从基础语法往完整项目迈进的人,也适合手上有摄像头但总是“黑屏、花屏”的老手用来定位问题。练熟这一套,后续做图像处理或更高分辨率采集,底层逻辑都是相通的。

2. 先看懂图像数据流:OV7725配置、RGB565转RGB888、TFT-LCD时序,三个环节缺一不可

画面上看是“摄像头对着屏幕”,但FPGA里实际是三组时钟在跑:OV7725的PCLK负责把像素吐出来,TFT-LCD自己的PCLK负责把像素吸进去,而摄像头和屏的帧率、行参数、数据格式又完全独立。你只要做错一个环节,屏幕上就是花屏或者黑屏。这一章先把三个环节的同步信号、数据格式和缓存策略讲清楚,再动手写代码。

2.1 摄像头侧:OV7725的SCCB配置和DVP输出格式

OV7725不是一个即插即用的传感器。上电之后,它默认输出的分辨率、格式、帧率和我们想要的640x480 RGB565完全对不上,必须先用SCCB总线写寄存器。SCCB跟I2C几乎兼容,同样是SCL+SDAT两根线,开始条件、停止条件、ACK机制都一样,只是协议细节上不支持多主机。实际工程里可以直接用I2C的主机状态机去写。

从地址要注意:SCCB传输期间地址字节是“从地址左移1位加上读写位”。OV7725的7位从地址是0x21,写操作就是0x42,读操作是0x43。很多新手抄网上代码时把地址写成0x21,结果一直没ACK。你的摄像头模块如果是OV7725,需要确认一下模块上SCCB地址有没有被电阻拉成其他值,一般同一个模块有多个地址可选。

配置序列怎么组织?常见做法是在FPGA里放一个寄存器表,用状态机依次写。寄存器表网上很多,但不同厂商版本有差异,建议拿到模块后先确认芯片丝印,再找对应配置。下面这个状态机是完整的SCCB写操作流程,可以直接用于把寄存器表和地址数据按顺序发出去:

// SCCB 写寄存器状态机 localparam IDLE = 3'd0, START = 3'd1, ADDR = 3'd2, REG = 3'd3, DATA = 3'd4, STOP = 3'd5, WAIT = 3'd6; // scl/sda 为标准三态输出,sda_in 用于读ACK reg [2:0] state; reg [3:0] bit_cnt; reg [7:0] sreg; // 移位寄存器 reg [6:0] table_idx; // 寄存器表下标 // 状态机主流程 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; scl <= 0; sda_out <= 1; end else begin case (state) IDLE: if (start_config) begin state <= START; table_idx <= 0; sda_out <= 1; end START: begin scl <= 1; sda_out <= 0; // 时钟高电平时 sda 从1跳0 state <= ADDR; bit_cnt <= 8; sreg <= {1'b0, SLAVE_ADDR[6:0]}; // 0x21 左移后写入位 end ADDR: begin scl <= bit_cnt[0] ? 1 : 0; // 半周期翻转简化示意 // 实际需按位发送 sreg,此处省略打拍过程 bit_cnt <= bit_cnt - 1'b1; if (bit_cnt == 0) begin // 等待 ACK state <= REG; bit_cnt <= 8; sreg <= reg_table[table_idx][15:8]; // 寄存器地址 end end REG: begin bit_cnt <= bit_cnt - 1'b1; if (bit_cnt == 0) state <= DATA; sreg <= reg_table[table_idx][7:0]; // 寄存器数据 end DATA: begin bit_cnt <= bit_cnt - 1'b1; if (bit_cnt == 0) state <= STOP; end STOP: begin scl <= 1; sda_out <= 1; // 时钟高电平时 sda 从0跳1 state <= WAIT; table_idx <= table_idx + 1'b1; if (table_idx == TABLE_LEN-1) config_done <= 1; end WAIT: state <= IDLE; endcase end end

这段代码是示意结构,不是完整可直接综合的版本。它的重点是:地址字节由从地址左移一位得到;每发完一字节要释放SDA让从设备拉低ACK;发送节奏要有半拍打拍,否则SCCB时序不满足建立保持时间。实际工程里我一般把scl、sda的边沿用计数器精确控制,避免这种简化写法带来的亚稳态。

配置好之后,OV7725会通过DVP接口输出图像。DVP的关键信号是PCLK、VSYNC、HREF、D[7:0]。在640x480 RGB565模式下,每个像素占两个PCLK周期:HREF拉高有效期间,第一个PCLK给高8位,第二个PCLK给低8位。像素拼接时,用HREF作为门控,在PCLK上升沿采样数据,再用bit counter区分高低字节即可。注意:OV7725有三种PCLK极性可选,寄存器里的 INV_PCLK 位会改变数据锁存沿,后文调试章节会专门说这个问题。

2.2 显示侧:640x480 TFT-LCD的行场同步和RGB像素时序

TFT-LCD 的接口看起来和 VGA 像,都有 VSYNC、HSYNC、PCLK、DE 和数据线,但很多工业屏的同步极性、前肩后肩参数并不完全一样。标题里写的是“RGB TFT-LCD”,常见有两类:一类是 RGB888 接口,24 根数据线;一类是 RGB565 接口,16 根数据线。如果你的屏是 640x480,那它的扫描时序基本就是下面这张表(具体以规格书为准):

时序参数水平垂直
有效像素640480
前肩 FP1610
同步脉冲962
后肩 BP4833
总长度800525

水平单位是像素时钟,垂直单位是行。这块 640x480 的屏如果刷新率做到 60Hz,像素时钟就是 800 x 525 x 60 ≈ 25.2MHz,用 25MHz 也没有问题。FPGA 侧要做的就是把计数器从 0 跑到 800,再从 0 跑到 525,在合适的位置拉高 HSYNC、VSYNC,并且只在有效区间拉高 DE,把像素数据送上数据线。下面是一个简化的 LCD 时序生成框架:

// 640x480 RGB LCD 行场时序生成 reg [9:0] h_cnt, v_cnt; // 行/场计数器(0~799, 0~524) wire active = (h_cnt < 640) && (v_cnt < 480); assign de = active; always @(posedge lcd_pclk or negedge rst_n) begin if (!rst_n) begin h_cnt <= 0; v_cnt <= 0; end else begin if (h_cnt == 799) begin h_cnt <= 0; v_cnt <= (v_cnt == 524) ? 0 : v_cnt + 1'b1; end else h_cnt <= h_cnt + 1'b1; end end // 同步信号:按表格参数位置拉低(这里用低有效示意,实际以屏规格为准) assign hsync = (h_cnt >= 16 && h_cnt < 16+96) ? 1'b0 : 1'b1; assign vsync = (v_cnt >= 10 && v_cnt < 10+2) ? 1'b0 : 1'b1;

注意这里的 hsync/vsync 是高有效还是低有效,完全看屏幕数据手册。很多国产屏默认是低有效,但有些跟VGA一样是分离同步且极性可配。如果显示错位或者扫描方向不对,第一件事就是查规格书里的同步极性。

DE 信号是关键。有些屏建议使用“DE only”模式,忽略 HSYNC/VSYNC;有些屏则必须用同步模式。在 DE only 模式下,FPGA 只需要保证 DE 拉高时数据是有效的即可。后面的避坑章节里,显示偏移八成出在 DE 与行场计数不对齐上。

2.3 中间那一跳:帧缓存要不要用、为什么有人用双口RAM有人直接推流

接下来是最容易纠结的点:OV7725 给出来的像素走 PCLK,LCD 要的像素走 lcd_pclk,两边的时钟不同步。如果直接把摄像头的像素按摄像头节奏送到 LCD 数据线上,图像会撕裂、错行,因为 LCD 扫描一行时数据源可能刚好断流。常见做法是加一个缓存。

缓存有两档:行缓存和帧缓存。640x480x16bit 一帧大约是 4.9Mbit,普通 Cyclone IV 的片内 RAM 一般不够放整帧,所以实验工程普遍用“异步FIFO做行缓冲”。具体拓扑是:采集模块把像素写进 FIFO,写时钟用 PCLK,写使能用 HREF 的像素有效脉冲;LCD 驱动以 lcd_pclk 为读时钟,只要 FIFO 不空就持续读数据,并送到数据总线。因为读速率略高于写速率,FIFO 不会溢出。这个方案实现简单,但帧与帧之间没有隔离,如果写速率短时间高于读速率,会产生撕裂。实验显示效果足以满足,刚好也不会太复杂。

另一种做法是用双口RAM做一帧缓存:摄像头写完一帧后,LCD再读出来,读写地址互相独立。这能彻底解决撕裂,但需要足够大的片内RAM或外挂SDRAM。如果用的是 EP4CE30 这种大一点的芯片,640x480x16bit 塞进 M9K 可能接近满,使用率很高,布线容易因为RAM占用过紧出现时序问题。所以我的习惯是:实验阶段先用异步FIFO行缓存把通路跑通,后续做稳了再换SDRAM帧缓存。异步FIFO在Quartus里直接用IP核,配置写时钟域、读时钟域、数据宽度16位、深度设成2048或4096就够。

3. 在Quartus里搭工程:最小文件清单和核心Verilog模块怎么划分

3.1 工程文件组织:源码、IP、约束文件的摆放

一个可综合的工程通常分成几个模块,不要把所有代码写在一个文件里。我这里给一个常见的文件组织方式,你在Quartus里新建工程时,把源码和IP核分开放在不同目录,后面维护会很省事:

文件名模块作用
top.v顶层,例化所有模块,连接输入输出
pll.v由MegaWizard生成的PLL,分频输出摄像头、LCD时钟
sccb_config.vSCCB配置状态机,写OV7725寄存器
camera_capture.vDVP时序解析,拼接RGB565像素
fifo_ip.qip异步FIFO IP(读写时钟分离)
lcd_drive.v生成行场时序、读FIFO数据、输出RGB

如果你用的是Quartus II 13.1或更高版本,新建工程时选择目标芯片后,把各模块Add到工程里,IP核用Tools里的MegaWizard Plug-In Manager生成即可。不要直接在顶层include整个VM文件列表,用Quartus的工程文件列表管理,仿真和综合才不会乱。至于Quartus安装,网上有大量quartus ii安装教程,装好后记得把USB-Blaster驱动也装上,不然板子连不上。

3.2 核心模块代码:摄像头采集、像素拼接、LCD驱动

摄像头采集模块负责把DVP上的字节流拼成RGB565,并产生写FIFO的请求信号。代码如下,它是一个可综合的简化版本:

module camera_capture( input wire pclk, // OV7725 像素时钟 input wire rst_n, // 异步复位 input wire vsync, // 帧同步 input wire href, // 行有效 input wire [7:0] din, // 像素数据 output reg [15:0] dout, // 拼接后的RGB565 output reg wren // 写FIFO使能 ); reg [1:0] byte_cnt; reg active_frame; always @(posedge pclk or negedge rst_n) begin if (!rst_n) begin byte_cnt <= 0; wren <= 0; end else begin if (vsync == 0) begin // 假设VSYNC低有效,复位帧逻辑 active_frame <= 0; byte_cnt <= 0; end if (href) begin active_frame <= 1; byte_cnt <= byte_cnt + 1'b1; if (byte_cnt == 0) dout[15:8] <= din; // 先收高字节 else begin dout[7:0] <= din; // 后收低字节,数据拼齐 wren <= 1; end end else wren <= 0; end end endmodule

逻辑说明:用byte_cnt打拍,把两个字节在第二个节拍拼成一个16位像素,wren拉高一个PCLK周期,FIFO在这个时钟沿上升沿采样。这里的关键是wren必须只在像素完整时拉高一次,否则FIFO会收到错乱数据。参数上,PCLK频率不能超过FIFO写侧最高时钟;如果摄像头分辨率变了,这个模块不用改,但FIFO深度要调整。

LCD驱动模块读FIFO并向屏幕输出,框架如下:

module lcd_drive( input wire lcd_pclk, input wire rst_n, input wire [15:0] fifo_dout, input wire fifo_empty, output reg [15:0] rgb_out, output wire hsync, vsync, de, output reg rd_en ); reg [9:0] h_cnt, v_cnt; wire active = (h_cnt < 640) && (v_cnt < 480); // 在有效区域且FIFO非空时读数据 always @(posedge lcd_pclk) begin if (!rst_n) rd_en <= 0; else rd_en <= active && !fifo_empty; end always @(posedge lcd_pclk) begin if (rd_en) rgb_out <= fifo_dout; end endmodule

说明:rd_en只是拉高一个时钟周期,作为FIFO读请求。这里没有处理FIFO空时候的像素替换,实际工程中会在FIFO空时输出黑色或保持上一像素,具体取决于你看重的是流畅还是防撕裂。注意,如果FIFO的读侧在de有效时拉低,读数据会晚一个拍子出现在数据总线上,所以LCD的数据要配套加一级流水线对齐DE,后面调试会再提。

3.3 约束与时钟:PLL怎么设、引脚怎么分

时钟是这套系统最容易出问题的一环。板上一般只有一个晶振,比如50MHz,OV7725需要24MHz,LCD需要25MHz,所以要用PLL生成两个独立时钟。在Quartus的MegaWizard里选ALTPLL,输入时钟填50MHz,输出c0设置成24MHz,c1设置成25MHz。如果摄像头模块上自带晶振,也可以只用PLL给LCD提供时钟,摄像头的PCLK由摄像头自己产生。要格外注意:OV7725的PCLK在配置完成后才会稳定输出,配置期间不要把它当作系统主时钟来用。

引脚分配方面,把引脚按你的开发板原理图分配。常见IO标准是3.3V LVCMOS。在Quartus中双击Pin Planner,把OV7725的scl、sdat、pclk、vsync、href、din[7:0]以及LCD的rgb、行场同步引脚逐一锁定。注意FPGA的BANK电压必须和摄像头、LCD的IO电平匹配,否则信号翻转不稳定,会出现随机花屏。这是很多新手会忽略的硬件边界。

4. 调试时必踩的坑:图像不出、花屏、卡顿的排查顺序

我总结一下做这个工程最常见的四类坑,每一条都是能在屏幕上直接看见的现象。调试顺序建议从“显示侧”往“采集侧”推:先让LCD显示固定图案,再让它显示FIFO里的数据,最后才开摄像头。

4.1 现象:屏幕全白或全黑

屏幕上完全没有图像,或者整个屏幕一种颜色不变。原因多半在时钟或复位上。PLL未锁定、全局复位一直有效、摄像头VSYNC没有输出来,都会让采集和显示链路停在初始状态。解决方法是先用SignalTap抓PLL的locked、复位、ov7725_vsync三个信号。如果VSYNC一直不跳,说明SCCB没配置成功或摄像头没上电。如果LCD黑屏但VSYNC正常,则多半是LCD同步极性接反,先单独给LCD送一个彩条测试图案,确认时序基本正确再接入采集链路。

4.2 现象:花屏、颜色不对、偏移

花屏通常是数据位序和时序错位。先检查RGB565的高低字节顺序:OV7725可以通过寄存器配置每个像素先出高字节还是低字节,如果配置和采集模块不一致,颜色就不对,比如红色和蓝色交换。再检查PCLK采样沿:HREF有效时,PCLK上升沿和下降沿两种采样模式都可能对,取决于模块设计。解决方法是先用一个小色块或单色背景测试,肉眼观察颜色正确性;再用彩条测试图看行偏移,如果每行都整体往右移,则DE信号延迟了几拍,需要在LCD驱动里给DE加流水线调节。

4.3 现象:帧率上不去、画面闪烁

闪烁多半是FIFO读写速率不匹配。OV7725在640x480@30fps下,PCLK约24MHz,LCD刷新率60Hz,读时钟25MHz,读写速率差约4%。如果FIFO深度太浅,读侧会频繁遇到空状态,画面就一卡一卡的。解决方法是把FIFO深度至少做到2048,同时检查有没有丢帧使能逻辑。另一处坑是:有人直接用摄像头的VSYNC当作LCD的VSYNC,这会强制两个帧同步互相牵制,闪烁更严重。正确做法是LCD用自己的行场计数器,不要依赖摄像头的VSYNC。

4.4 现象:SCCB配置没生效

摄像头配置是重灾区。现象是信号抓不到VSYNC或图像全白。先查SDA、SCL有没有上拉电阻,一般需要4.7k欧姆上拉到3.3V,没有上拉就没ACK。再用逻辑分析仪抓SCCB波形,确认从地址是不是0x21、每个字节后有没有ACK。如果从时序上看没有ACK,可能从地址被模块上跳线改成了0x28之类,需要看原理图。另外,配置序列最后往往要写一个分频相关寄存器,有些抄来的寄存器表会把输出分辨率配成别的值,也会导致没有640x480的信号。我一般会把配置表拆成单步写,逐个寄存器验证,不嫌麻烦。

5. 参数怎么调:从640x480到别的分辨率,从RGB565到RGB888的关键改动

5.1 分辨率改动会影响哪些参数

如果你想把工程从640x480改成其他分辨率,不要只改LCD的行场参数。OV7725侧需要重新配置窗口寄存器:HSTART、HSIZE、VSTRT、VSIZE,以及输出比例寄存器。LCD侧要改的更多:水平/垂直总长度、同步脉冲位置、有效区间。最关键的是像素时钟:分辨率提高,像素时钟必然提高。比如1024x768@60Hz需要约65MHz像素时钟,FPGA要保证PLL能输出并满足时序约束。同时,FIFO深度要重新评估,因为每行的像素数变了,行存长度至少是“一行像素数”的1.5倍,否则读侧一次burst取不到完整一行。

5.2 颜色格式转换的位宽和拼接坑

OV7725输出RGB565,而屏幕接口如果是RGB888,就需要做位扩展。简单的做法是高位补0,但画面会发暗;更好的是复制高位到低位,例如R的5位扩展为8位:assign r8 = {r5, r5[2:0]}; G的6位扩展为8位:assign g8 = {g6, g6[1:0]}; B同理。如果用不了RGB888,可以先在FPGA里把RGB565直接送到RGB565屏上,这是最省RAM的做法。

另一个坑:有些屏的数据线是RGB888,但内部时序按RGB565映射到高18位或高16位,这时如果忽略屏的数据线接法,颜色会整体偏色。所以拿到屏幕先把引脚原理图看清楚,搞清楚哪几位接的是R、G、B的哪几位,再写拼接逻辑。特别是那些带LVDS转换板的模组,数据映射关系更是要逐位核对。

5.3 帧率与时钟频率的匹配关系

摄像头帧率、LCD刷新率、像素时钟三者必须联动。OV7725在640x480下单帧数据量约614,400字节,如果PCLK是24MHz,理论帧率约24MHz除以每帧像素数,但传感器内部有消隐,所以实际输出30帧左右。LCD要求刷新率60帧,像素时钟约25MHz。因此必须保证LCD的读速率高于摄像头的写速率,否则FIFO会被读空。反过来,如果屏幕刷新率高于摄像头帧率,画面会重复显示同一帧,这是正常的。如果你想提高输出流畅度,可以修改OV7725的帧率寄存器把PCLK提高到更高频率,但要注意传感器最大PCLK限制。调试时可以通过SignalTap观察FIFO的空满比例,若写快读慢则FIFO会持续满,需要降低摄像头帧率或提高读时钟。

6. 验证与进阶:用一张测试图把通路跑通,再谈图像处理的下一步

6.1 用FPGA内部的测试图案验证LCD时序

摄像头还没配好时,先用一个计数器生成彩条送到LCD,验证驱动本身。比如h_cnt<320时送红色,否则送蓝色,屏幕上立刻能看到屏幕扫描方向、颜色通道是否接对。这是成本最低的排错手段。我每次上板都会先切到测试图案模式,确认LCD没问题再碰摄像头,这样问题隔离得快。

6.2 把OV7725替换成仿真模型:在ModelSim里看波形

写一个简单的OV7725行为仿真模型,输出VSYNC、HREF、PCLK和RGB数据,把它接到采集模块上,在ModelSim里仿真。这样不用上板就能验证像素拼接和FIFO读写时序。仿真模型里用task产生像素序列,并故意制造PCLK和LCD时钟的相位差,能提前暴露跨时钟域问题。比如让PCLK比lcd_pclk快一点点,看FIFO是否出现溢出,这个仿真比上板看屏幕直观得多。

6.3 进阶方向:帧缓存、灰度化、边缘检测

通路跑通后,下一步是加SDRAM帧缓存,彻底解决撕裂,然后就能做真正的图像处理。比如把RGB565转成灰度,再用3x3滑动窗口滤波verilog做高斯或Sobel边缘检测。要留意的是,滑动窗口需要两行像素缓存,正好复用你之前学的行缓存经验。如果要做串口或以太网传图,UART的控制流和这里的FIFO空满控制也能复用。我自己做过一次高分辨率采集后,回头再看这个640x480工程,最大的体会是:前期把时钟域和缓存宽度想清楚,后边换分辨率和加处理都是改参数而不是改架构。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询