文章目录
-
- 概要
- 一、前言
- [二、OV5640_Data 模块的作用](#二、OV5640_Data 模块的作用)
- [三、OV5640 输出数据的特点](#三、OV5640 输出数据的特点)
- 四、输入输出端口解析
- 六、数据有效与帧同步信号
- 七、摄像头控制信号
- [八、Frame_Clk 与 Frame_Ce 的生成](#八、Frame_Clk 与 Frame_Ce 的生成)
- [九、8bit 转 16bit:两拍拼一个 RGB565 像素](#九、8bit 转 16bit:两拍拼一个 RGB565 像素)
- 十、数据有效信号的产生
- 十一、Vcount:帧内行计数
- [十二、为什么要丢弃前 10 帧](#十二、为什么要丢弃前 10 帧)
- [十三、这个 IP 核的工程价值](#十三、这个 IP 核的工程价值)
- 十四、源码设计上的一个关键认识
- 十五、总结
- 十六、后记
概要
提示:这里可以添加技术概要
一、前言
在基于 ZYNQ + OV5640 的视频采集系统中,摄像头并不能直接与 Video In to AXI4-Stream 这类视频 IP 核对接,通常需要一个中间桥接模块对数据格式和时序进行整理。
在我的工程中,这个桥接模块就是自定义 IP:

它的作用并不复杂,但非常关键,主要完成以下几件事:
bash
接收 OV5640 输出的 8bit 并行数据
将两拍 8bit 数据拼接为 1 个 16bit 像素
将 RGB565 扩展为 RGB888
生成数据有效信号
生成输出视频接口所需的时钟、时钟使能和帧同步信号
丢弃前 10 帧不稳定图像数据
这篇文章就结合源码,详细分析 OV5640_Data 的设计思路。
二、OV5640_Data 模块的作用
先看模块接口定义:
bash
module OV5640_Data(
Rst_n, //复位
PCLK, //像素时钟
Vsync, //场同步信号
Href, //行同步信号
Data, //数据
DataValid, //数据有效信号
DataPixel, //像素数据
Cam_Rst_n, //cmos 复位信号,低电平有效
Cam_Pwdn, //电源休眠模式选择
//RGB888接口
Frame_Clk, //时钟信号
Frame_Ce, //时钟使能信号
Frame_Vsync //帧有效信号
);
从接口命名可以看出,这个模块本质上就是一个:OV5640 DVP 接口到 Video In 接口之间的数据桥接模块。

也就是说,OV5640_Data 并不负责复杂图像处理,它最核心的任务就是:
css
把摄像头吐出来的原始像素流,变成后级视频 IP 更容易接收的标准化数据流。
三、OV5640 输出数据的特点
在分析代码前,先要明白 OV5640 的输出方式。
在本工程中,OV5640 输出的是:RGB565
但是由于摄像头数据口只有 8 位,所以一个完整像素不会在一个时钟周期内给出,而是:
bash
第 1 拍输出高 8 位
第 2 拍输出低 8 位
也就是说:
bash
2 个 PCLK 周期 = 1 个 RGB565 像素
因此,FPGA 侧必须先完成一件事:
bash
将两次 8bit 采样拼成一个 16bit 像素数据。
这就是这个 IP 最基础的工作。
四、输入输出端口解析
源码中的端口定义如下:
bash
input Rst_n;
input PCLK;
input Vsync;
input Href;
input [7:0]Data;
output DataValid;
output [23:0]DataPixel;
output Cam_Rst_n;
output Cam_Pwdn;
output Frame_Clk;
output Frame_Ce;
output Frame_Vsync;
寄存器定义如下:
bash
reg r_Vsync;
reg r_Href;
reg [7:0]r_Data;
reg [15:0]r_DataPixel;
reg r_DataValid;
reg [12:0]Hcount;
reg [11:0]Vcount;
reg [3:0]FrameCnt;
reg Dump_Frame;

五、RGB565 扩展为 RGB888
这部分是整个模块最直观的一段逻辑:
bash
assign DataPixel = Dump_Frame ? {r_DataPixel[15:11], 3'd0,
r_DataPixel[10:5], 2'd0,
r_DataPixel[4:0], 3'd0} : 24'd0;

bash
即:
R8 = {R5, 3'b000}
G8 = {G6, 2'b00}
B8 = {B5, 3'b000}
这种扩展方式虽然不是最精确的颜色映射,但硬件实现最简单,非常适合工程入门阶段和一般显示场景。
如果 Dump_Frame = 0,说明当前帧不允许输出,那么 DataPixel 直接输出全 0。
六、数据有效与帧同步信号
对应代码:
bash
assign DataValid = r_Href & Dump_Frame;
assign Frame_Vsync = r_Vsync & Dump_Frame;
这里的思路很简单:
bash
只有在行同步有效时,当前像素数据才有效
只有在允许输出帧数据时,才向后级送出有效信号
也就是说:DataValid 表示:
bash
当前输出的 DataPixel 是否有效
当 r_Href=1 且 Dump_Frame=1 时,说明当前像素处于有效图像区域,而且不是前面被丢弃的无效帧,此时 DataValid=1。
Frame_Vsync 表示:
bash
当前帧同步信号是否有效
同样,只有当系统已经跳过前若干不稳定帧后,才允许真正向后级传递帧同步信号。
七、摄像头控制信号
这部分代码如下:
bash
assign Cam_Rst_n = 1'b1;
assign Cam_Pwdn = 1'b0;
含义很直接:
bash
Cam_Rst_n = 1'b1:摄像头不复位,处于正常工作状态
Cam_Pwdn = 1'b0:摄像头不进入省电休眠模式
也就是说,这个设计中摄像头始终保持正常工作,不做动态控制。
这也是很多基础实验工程里的常见写法,简单直接。
八、Frame_Clk 与 Frame_Ce 的生成
先看时钟:
bash
assign Frame_Clk = PCLK;
说明输出视频接口的时钟直接使用摄像头像素时钟 PCLK。
也就是说,这里没有做新的时钟变换,而是直接将摄像头时钟继续传给后级模块。
再看时钟使能:
bash
assign Frame_Ce = ((Hcount[0]) || (!r_Href)) & Dump_Frame;
这一句刚看会有点绕,但本质上是为了适配:
bash
摄像头 2 拍输出 1 个像素,而后级需要按"像素"为单位取数据
由于 Hcount[0] 可以区分当前是奇数拍还是偶数拍,所以这里利用最低位来控制什么时候真正送出像素使能。
简单理解就是:
bash
一个完整像素在两拍之后才算准备好
因此不是每拍都对后级开放时钟使能
而是在合适的时刻拉高 Frame_Ce
这样后级模块就不会把"半个像素"当成完整像素去处理。
九、8bit 转 16bit:两拍拼一个 RGB565 像素
对应代码:
bash
always@(posedge PCLK or negedge Rst_n)
if(!Rst_n)
r_DataPixel <= 0;
else if(!Hcount[0])
r_DataPixel <= {r_Data,Data};
这是本模块最核心的逻辑之一。
由于摄像头每拍只输出 8 位,所以在两拍之后,使用:
bash
{r_Data, Data}
将前一拍和当前拍拼接成 16 位数据。
也就是说:
bash
r_Data:前一拍数据
Data:当前拍数据
拼起来就是:16bit RGB565
这个过程实际上就是把串行两拍的像素输入,重新组合成并行单像素格式。
十、数据有效信号的产生
代码如下:
bash
always@(posedge PCLK or negedge Rst_n)
if(!Rst_n)
r_DataValid <= 0;
else if(Hcount[0] && r_Href)
r_DataValid <= 1;
else
r_DataValid <= 0;
它的意思是:
bash
只有当行同步有效时才可能有数据
只有在两拍拼接完成的那个时刻,数据才真正有效
因此 r_DataValid 不是每拍都拉高,而是按"每个完整像素"去产生。
这和前面的 Frame_Ce 一起,共同保证了后级拿到的是:
bash
按像素对齐的有效数据流
十一、Vcount:帧内行计数
代码如下:
bash
always@(posedge PCLK or negedge Rst_n)
if(!Rst_n)
Vcount <= 0;
else if(r_Vsync)
Vcount <= 0;
else if({r_Href,Href} == 2'b01)
Vcount <= Vcount + 1'd1;
else
Vcount <= Vcount;
这里 Vcount 的本质作用是统计帧内有效行数。
其中:
bash
{r_Href, Href} == 2'b01
表示 Href 从 0 跳变到 1,也就是:
bash
一行有效数据开始了
因此每检测到一次这样的边沿,就认为进入了新的一行,Vcount 加 1。
虽然这段代码在当前工程里可能没有进一步参与复杂控制,但它为后续做图像区域统计、分辨率判断、算法处理都留出了接口。
十二、为什么要丢弃前 10 帧
这是这个模块很有工程味道的一点。
先看帧计数代码:
bash
always@(posedge PCLK or negedge Rst_n)
if(!Rst_n)
FrameCnt <= 0;
else if({r_Vsync,Vsync}== 2'b01)begin
if(FrameCnt >= 10)
FrameCnt <= 4'd10;
else
FrameCnt <= FrameCnt + 1'd1;
end
else
FrameCnt <= FrameCnt;
再看帧丢弃控制:
bash
always@(posedge PCLK or negedge Rst_n)
if(!Rst_n)
Dump_Frame <= 0;
else if(FrameCnt >= 10)
Dump_Frame <= 1'd1;
else
Dump_Frame <= 0;
这部分的设计思想非常实用。
原因
摄像头刚上电、刚初始化完成时,前几帧数据往往不稳定,可能出现:
bash
颜色异常
行列错位
亮度波动
画面抖动
如果一开始就把这些数据送到后级,很容易导致显示异常,甚至影响后面的视频链路稳定性。
因此这里采用了一个很常见的办法:
bash
前 10 帧直接丢弃,等摄像头稳定后再开始输出有效图像。
这也是为什么前面 DataPixel、DataValid、Frame_Vsync 都和 Dump_Frame 相关。
十三、这个 IP 核的工程价值
从源码来看,OV5640_Data 并不复杂,但它解决了视频系统中的几个关键问题:
- 数据位宽适配
把摄像头两拍 8bit 数据整理为单像素 16bit 数据,再进一步扩展为 24bit。
- 时序适配
通过 Frame_Clk、Frame_Ce、DataValid、Frame_Vsync 生成适合后级视频 IP 使用的控制信号。
- 启动稳定性优化
通过丢弃前 10 帧,避免了摄像头刚启动时的不稳定数据影响整个系统。
所以这个 IP 虽然小,但实际上是:
OV5640 原始数据流与后级视频处理链之间的重要桥梁。
十四、源码设计上的一个关键认识
很多人在第一次看这个模块时,会以为它输出的是"真正原生 RGB888"。
其实不是。
它本质上做的是:
bash
RGB565 → 补零扩展 → RGB888
也就是说:
bash
原始图像信息量还是 RGB565
只是在接口形式上变成了 24bit RGB888
这一点对后面系统设计非常重要。因为这也解释了为什么在很多工程中,还会看到:
bash
RGB888 → RGB565 → VDMA → RGB888
看起来好像来回折腾,实际上是为了兼顾:
bash
前级接口适配
VDMA 16bit 存储稳定性
后级视频显示需求
十五、总结
OV5640_Data 这个自定义 IP 的作用可以概括为一句话:
bash
将 OV5640 摄像头输出的 8bit 并行 RGB565 数据流,整理为适合后级视频处理模块使用的 RGB888 视频数据流。
它主要完成了以下工作:
bash
输入信号同步打拍
两拍 8bit 拼接为 16bit RGB565
RGB565 扩展为 RGB888
生成数据有效和帧同步信号
丢弃前 10 帧不稳定图像
虽然从代码量上看不算复杂,但在整个 ZYNQ 视频系统中,它是一个非常关键的接口适配模块。
这也是我后面调试中非常重要的一个认识。
十六、后记
在后续调试中,我曾尝试去掉中间的格式转换模块,让 VDMA 直接处理 24bit RGB888,结果系统黑屏。后来回头再看 OV5640_Data 的设计,就会更容易理解:
bash
摄像头原始本质仍然是 RGB565
输出成 RGB888 只是为了接口适配
而系统内部的 FrameBuffer 设计,很多时候仍然更适合使用 16bit RGB565
这也说明,在 FPGA 视频系统中,"能显示" 和 "工程上最合理" 往往不是同一个问题。
看似多余的格式转换模块,很多时候恰恰是工程稳定性的关键。
OV5640_Data 代码如下:
bash
module OV5640_Data(
Rst_n, //复位
PCLK, //像素时钟
Vsync, //场同步信号
Href, //行同步信号
Data, //数据
DataValid, //数据有效信号
DataPixel, //像素数据
Cam_Rst_n, //cmos 复位信号,低电平有效
Cam_Pwdn, //电源休眠模式选择
//RGB888接口
Frame_Clk, //时钟信号
Frame_Ce, //时钟使能信号
Frame_Vsync //帧有效信号
);
input Rst_n; //复位
input PCLK; //像素时钟
input Vsync; //场同步信号
input Href; //行同步信号
input [7:0]Data; //数据
output DataValid; //数据有效信号
output [23:0]DataPixel; //像素数据
output Cam_Rst_n;//cmos 复位信号,低电平有效
output Cam_Pwdn;//电源休眠模式选择
output Frame_Clk;//时钟信号
output Frame_Ce;//时钟使能信号
output Frame_Vsync;//帧有效信号
reg r_Vsync;
reg r_Href;
reg [7:0]r_Data;
reg [15:0]r_DataPixel;
reg r_DataValid;
reg [12:0]Hcount;
reg [11:0]Vcount;
reg [3:0]FrameCnt;
reg Dump_Frame;
assign DataPixel = Dump_Frame ? {r_DataPixel[15:11], 3'd0, r_DataPixel[10:5], 2'd0, r_DataPixel[4:0], 3'd0} : 24'd0;
assign DataValid = r_Href & Dump_Frame;
assign Frame_Vsync = r_Vsync & Dump_Frame;
//摄像头硬件复位,固定高电平
assign Cam_Rst_n = 1'b1;
//电源休眠模式选择 0:正常模式 1:电源休眠模式
assign Cam_Pwdn = 1'b0;
//摄像头时钟使能
assign Frame_Ce = ((Hcount[0]) || (!r_Href)) & Dump_Frame;//1'b1;//(r_DataValid & Dump_Frame)||(!r_DataValid);
//assign Frame_Ce = ((!Hcount[0]) || (!r_Href)) & Dump_Frame;//1'b1;//(r_DataValid & Dump_Frame)||(!r_DataValid);
//时钟为像素时钟
assign Frame_Clk = PCLK;
//打拍
always@(posedge PCLK)
begin
r_Vsync <= Vsync;
r_Href <= Href;
r_Data <= Data;
end
//行同步信号为1时,行计数器加一(行同步信号为0时归零)
always@(posedge PCLK or negedge Rst_n)
if(!Rst_n)
Hcount <= 0;
else if(r_Href)
Hcount <= Hcount + 1'd1;
else
Hcount <= 0;
//8位转16位,赋予像素数据
always@(posedge PCLK or negedge Rst_n)
if(!Rst_n)
r_DataPixel <= 0;
else if(!Hcount[0])
r_DataPixel <= {r_Data,Data};
// else
// r_DataPixel[7:0] <= r_Data;
//产生数据有效信号
always@(posedge PCLK or negedge Rst_n)
if(!Rst_n)
r_DataValid <= 0;
else if(Hcount[0] && r_Href)
r_DataValid <= 1;
else
r_DataValid <= 0;
//行同步信号由0变为1时,列计数器加一(场同步信号为1时归零)
always@(posedge PCLK or negedge Rst_n)
if(!Rst_n)
Vcount <= 0;
else if(r_Vsync)
Vcount <= 0;
else if({r_Href,Href} == 2'b01)
Vcount <= Vcount + 1'd1;
else
Vcount <= Vcount;
//场同步信号由0变为1时,帧计数加一,最大为10
always@(posedge PCLK or negedge Rst_n)
if(!Rst_n)
FrameCnt <= 0;
else if({r_Vsync,Vsync}== 2'b01)begin
if(FrameCnt >= 10)
FrameCnt <= 4'd10;
else
FrameCnt <= FrameCnt + 1'd1;
end
else
FrameCnt <= FrameCnt;
//当计数大于等于10帧时,Dump_Frame变为1,否则为0
always@(posedge PCLK or negedge Rst_n)
if(!Rst_n)
Dump_Frame <= 0;
else if(FrameCnt >= 10)
Dump_Frame <= 1'd1;
else
Dump_Frame <= 0;
endmodule