YNQ + OV5640 视频系统开发(二):OV5640_Data 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 并不复杂,但它解决了视频系统中的几个关键问题:

  1. 数据位宽适配

把摄像头两拍 8bit 数据整理为单像素 16bit 数据,再进一步扩展为 24bit。

  1. 时序适配

通过 Frame_Clk、Frame_Ce、DataValid、Frame_Vsync 生成适合后级视频 IP 使用的控制信号。

  1. 启动稳定性优化

通过丢弃前 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
相关推荐
Flamingˢ2 小时前
ZYNQ + OV5640 视频系统开发(三):AXI VDMA 帧缓存原理
arm开发·嵌入式硬件·fpga开发·vim·音视频
xiangw@GZ2 小时前
功耗测量:基于INA226的功耗测量原理深度解析
嵌入式硬件
芯智工坊2 小时前
第7章 Mosquitto增加SSL/TLS加密通信
网络协议·https·ssl
EmbeddedCore2 小时前
低成本物联网产品放弃SSL加密的隐形成本与市场逻辑
物联网·网络协议·ssl
Strange_Head2 小时前
快速入门 MQTT:从 Broker、发布订阅到双机通信
嵌入式硬件
Benszen2 小时前
一些存储类型
网络·网络协议·rpc
fLDiSQV1W2 小时前
springMVC-HTTP消息转换器与文件上传、下载、异常处理
网络协议·http·okhttp
Hello World . .3 小时前
Linux:Linux命令行音视频播放器
linux·音视频
YYYing.3 小时前
【Linux/C++网络篇(二) 】TCP并发服务器演进史:从多进程到Epoll的进化指南
linux·服务器·网络·c++·tcp/ip