基于FPGA的实时图像处理系统(3)——实时视频显示

发布于:2025-09-10 ⋅ 阅读:(19) ⋅ 点赞:(0)

实时视频显示

一、OV5640简介

OV5640(彩色)图像传感器是一款低电压、高性能的1/4英寸500万像素CMOS图像传感器,采用OmniBS技术,以紧凑封装形式实现了单芯片500万像素(2592×1944)相机的全部功能。通过串行摄像机控制总线(SCCCB)接口,可生成全帧、子采样、窗口化或任意缩放的8位/10位图像,并支持多种格式输出。该传感器图像阵列在500万像素分辨率下最高可达每秒15帧(fps),用户可完全自主控制图像质量、格式设置及数据传输方式。所有图像处理功能——包括曝光控制、伽马校正、白平衡调节、色彩饱和度优化、色调调整、缺陷像素消除、噪声抑制等——均可通过SCCCB接口或嵌入式微控制器进行编程配置。OV5640还配备压缩引擎以提升运算能力。此外,奥姆尼视觉图像传感器采用专有技术,通过减少或消除固定图案噪声、拖影等常见光源干扰,生成清晰稳定的彩色图像。该传感器内置微控制器,可与内部自动对焦引擎及可编程通用输入输出模块(GPIO)配合使用实现外部自动对焦控制,并搭载内置防抖引擎提供防抖功能。为便于识别和存储,OV5640还包含一个一次性可编程(OTP)存储器。OV5640同时支持数字视频并行端口和串行MIPI端口。

OV5640传感器核心以恒定帧率生成像素流数据,该帧率由HREF和VSYNC信号指示。如下图展示了OV5640图像传感器的功能框图。定时发生器输出信号控制图像阵列的行扫描,在预充电与采样期间依次对各行进行采样。在预充电与采样之间的时间间隔内,像素电荷会随入射光照射时间的延长而逐渐衰减,这一过程称为曝光时间。通过调节预充电与采样之间的时序间隔,可精确控制曝光时间。完成行内像素数据采样后,经模拟电路处理校正偏移量,并按对应增益系数进行数据乘法运算。随后通过模数转换器(ADC)对每个像素输出10位数据。

在这里插入图片描述
可知配置寄存器采用的是SCCB协议,SCCB介绍在下方。

器件内容不再赘述,,主要介绍用到的引脚以及实现的功能

引脚 功能 引脚 功能 引脚 功能
pclk 数据(输入)同步时钟 xclk FPGA给摄像头的时钟(24mHz) scl SCCB串行时钟
pwdn 电源关断(1=休眠,0=工作) vsync DVP时序帧同步信号 sda SCCB双向开漏数据线
reset 复位(低电平复位) href DVP时序行有效信号 din 摄像头输入的数据线

实现功能: 通过SCCB总线配置ov5640的各个参数,等待配置完成,再通过din数据接口收ov5640传回的图像数据。

二、SCCB与IIC

1、SCCB与IIC的异同

SCCB协议,其全称为(Serial Camera Control Bus)即串行摄像机控制总线协议,与IIC相似,同为同步半双工

1)、总线结构方面:

SCCB:有两线式和三线式两种接口形式。两线式包括 SIO_C(串行时钟输入线)和 SIO_D(串行双向数据线);三线式则在此基础上增加了 SIO_E(片选线),可以实现对多个从器件的控制,但通常在只有一个从机时使用两线式。
IIC:一般为两根信号线,一根是双向的数据线 SDA,另一根是时钟线 SCL。所有接到 IIC 总线设备上的串行数据 SDA 都接到总线的 SDA 上,各设备的时钟线 SCL 接到总线的 SCL 上。

2)、应答机制方面:

SCCB:应答位称为 “x”,表示 “don’t care”(不关心)。从机有可能不发出应答信号,主机可不用判断此处是否有应答,直接默认当前传输完成即可。
IIC:应答位称为 ACK。当发送器发送完一个字节的数据后,接收器必须在第 9 个时钟周期拉低 SDA 线表示应答,以确认数据接收成功。

3)、读操作方面:

SCCB:不支持连续读操作,读操作时在第一次写寄存器地址后必须有结束条件,之后再发起一个新的起始信号进行读操作。
IIC:支持连续读操作。在写完寄存器地址后,可以通过重复开始(Restart)操作,继续写设备地址(此时读 / 写位改变)然后读取数据。

4)、信号驱动方式方面:

SCCB:SCLK(在 SCCB 中类似 IIC 的 SCL)被指定为输出且是有源驱动,而非开漏(open-drain)或三态(tri-state)或集电极开路(open-collector)形式。
IIC:SCL 线通常要求设备连接到总线的输出端时必须是漏极开路或集电极开路输出,以避免总线信号的混乱。

2、传输规则

一个基本传输单元称作一个相
一个相包含总共9比特,前8比特为数据,第9比特为 Don’t-Care bit (不关心比特),该第9比特的数据取决于传输任务是读还是写如果是写操作则don’t care,如果为读操作为NA。
总结如下:
每一个单元组成:8位数据+don’t care/NA
如果是主机发送数据,即进行写操作,第九位就为don’t care(不关心比特)
如果是从机发送数据,即为读操作,第九位就为NA.

相1:主机向从机发送从机的ID号,SCCB协议支持一个主机和多个从机,因此这一个相目的是区分不同的从机,但如果我们只连接了一个从机时,也必须执行这样一个流程。实际上ID Address有8bit,其中bit7-bit1为从机的ID号,大小为0-127,一共能区分128个从机。OV2640的ID号为0x60。而bit0是用来区分对从机是写数据还是读数据,bit0=0代表写数据,bit0=1代表读数据,由于我们要向从机写数据,因此bit0应为0(0x60=01100000写入数据)。而相1中紧跟在ID Address这8位数据后的第九位是一个Don’t care bit(图中打X的位)。对于OV2640来说,从机在接收到主机送来的8bit数据后,将在SCL=1的期间,在SDA引脚输出低电平。在这期间,主机就可以读取SDA上的电平并进行判断,如果读取到低电平,表示从机已经顺利接收到了相1中的前8bit数据。说明数据传输成功,否则说明传输失败。

相2:主机向从机发送将要写入数据的寄存器的编号,寄存器的编号在OV传感器的数据手册上都能找到。寄存器的编号是一个8bit的数据。同样地,相2的第9bit也是一个Don’t care bit,对该位的说明与相1相同
相3:前面两个相指定了数据传输的从机ID以及要写入数据的寄存器的编号,这时候在第三个相就可以向前面指定的寄存器写入数据了。bit7-bit0是我们希望写入寄存器的数据。而第9bit也是一个Don’t care bit,对该位的说明与相1相同。

img

三、功能框图及实现流程

开始,由摄像头配置模块通过SCCB向OV5640发送配置的参数,配置完成,开始接收OV5640发送的数据,在capture模块处理完数据后,将数据发送给SDRAM进行缓存,再将数据发送给VGA进行显示。

在这里插入图片描述

1、接收OV5640数据

在接收ov5640数据之前,要通过SCCB协议对其进行配置,其他参数采用一个通用的设计,主要取更改的部分有两个,为调节分辨率

`ifdef 	PIXEL_1280x720
	223:   	 lut_data	= 	{24'h3808_05}; // DVPHO 1280
	224:   	 lut_data	= 	{24'h3809_00}; // DVPHO
	225:   	 lut_data	= 	{24'h380a_02}; // DVPVO 720
	226:   	 lut_data	= 	{24'h380b_d0}; // DVPVO
`elsif	PIXEL_640x480
	223:   	 lut_data	= 	{24'h3808_02}; // DVPHO 640
	224:   	 lut_data	= 	{24'h3809_80}; // DVPHO
	225:   	 lut_data	= 	{24'h380a_01}; // DVPVO 480
	226:   	 lut_data	= 	{24'h380b_e0}; // DVPVO

和调节显示模式(显示彩条或者正常的图像)

		 252:  lut_data	= 	{24'h503d_80}; // color bar
		 253:  lut_data	= 	{24'h4741_00}; //

正常模式只让计数器计到251,彩条模式计数到253。
当配置模块配置完成,会传出一个配置完成的信号,其他模块接收到这个信号才会开始工作。

ov5640采集的图像数据通过DVP时序发出
在这里插入图片描述

帧开始(VSYNC):
VSYNC 脉冲(低/高电平)表示新一帧开始。
主控检测到 VSYNC 后,准备接收新帧数据。
再代码设计时将第一位数据来临时认定为帧头。
行有效(HREF):
HREF脉冲代表一行数据的有效,一个HREF脉冲中有1280(分辨率为1280*720)*2个pclk周期,一个周期传输一个像素的数据。

ov5640采集到的为RGB565格式的像素数据,但发送为一次发送8bit,故要分两个pclk周期发送一个像素的数据,在接收数据时也要对数据进行特殊的处理。

//---------<pixel_data>------------------------------------------------- 
always @(posedge clk or negedge rst_n)begin 
    if(!rst_n)begin
        pixel_data <= 'd0;
    end 
    else begin 
        pixel_data <= {pixel_data[7:0],din};//高字节先发
    end 
end

//---------<pixel_vld>------------------------------------------------- 
always @(posedge clk or negedge rst_n)begin 
    if(!rst_n)begin
        pixel_vld <= 'd0;
        sop <= 1'b0;
        eop <= 1'b0;
    end 
    else begin 
        pixel_vld <= add_cnt_h && cnt_h[0];
        sop <= add_cnt_h && cnt_v == 0 && cnt_h == 1;
        eop <= end_cnt_v;
    end 
end

这里将传入的数据进行拼接,每来一字节数据就与前一位拼接,而两个数据为一个像素的完整数据,所以当行计数器计到奇数(如计数器为0和1时,这两个数据为一个像素点的完整数据,2和3是一个像素点的数据,而1的二进制最低为为1,3同理,0和2的二级制表达形式的最低为0,所以可以用行计数器的最低位来代表一个完整的像素数据),则代表数据有效,为一个完整的数据;

帧头是当第一个数据有效(行计数器计到1),且列计数器刚开始(为0),代表一帧数据的开始;
帧尾则是列计数器计完,代表一帧数据的结束。

2、SDRAM缓存数据

SDRAM基础框架详见sdram基础内容,在这里主要i介绍所用到的缓存机制——乒乓缓存。

由于sdram的输入输出数据线是双向的,我们可以设定读完之后写,写完后读,其速度很快。外界看来与同时读写无区别。在sdram的4个bank中,这里用到了两个bank,开始时一个进行写,一个进行读,这里摄像头输入的pclk的时钟频率是小于vga的频率的,所以写入sdram的数据速度慢于读出数据的速度,故在进行读写数据时,读数据的速度明显快于写数据的速度,故当读完一帧的数据时,写数据并未写完,所以读方进行复读操作,当读方复读时,写方写完了数据,此时写方等待读方,对下一帧数据进行丢帧操作(不存入SDRAM),待读方复读完成,切换bank,重复上述操作。

重点:防止FIFO溢出或无数据的情况

由于fifo两侧的时钟不同,如若不对读写fifo数据的时机进行约束,必然会出现数据溢出或读空的情况,这里做出以下措施:
写fifo:写fifo的写侧为ov5640采集到的数据,读侧为写入sdram的数据读速度大于写速度,在设计状态机时,设计了一个类突发长度(512,sdram一行刚好有512个存储单元),当fifo中的数据大于等于这一个值时,状态机才会跳转到写状态,sdram才会读取fifo中的数据

assign  IDLE_2_WRITE    = (state_c == IDLE)  && ~pri_flag && (wrfifo_rdusedw >= BURST_LENTH);
assign  WRITE_2_DONE    = (state_c == WRITE) && end_cnt_burst;

//---------<cnt_burst>------------------------------------------------- 

always @(posedge clk or negedge rst_n)begin 
   if(!rst_n)begin
        cnt_burst <= 'd0;
    end 
    else if(add_cnt_burst)begin 
        if(end_cnt_burst)begin 
            cnt_burst <= 'd0;
        end
        else begin 
            cnt_burst <= cnt_burst + 1'b1;
        end 
    end
end 

assign add_cnt_burst = (state_c == READ || state_c == WRITE) && ~avm_waitrequest;
assign end_cnt_burst = add_cnt_burst && cnt_burst == (BURST_LENTH - 1);

读fifo:读fifo写侧为sdram传出的数据,读侧为传给vga的数据,写侧为读出的sdram数据读速度大于vga读出的速度,在此fifo的深度为2048,在此设置一个上下限,当小于所设下限就从sdram读出数据到fifo中,当大于上限,就停止从sdram中读出数据,为了保证数据的准确,下限设为600,一行为512(确保fifo中有一行完整的数据),上限设置为1300(假设已有1299个数据,再进行一次突发读,读取512个数据也不会溢出)

#(parameter BURST_LENTH = 512,   //突发长度
              RD_MAX = 1300,      //读fifo上限
              RD_MIN = 600        //读fifo下限
//rd_flag(标志拉高,读fifo写侧收sdram传的数据)
always @(posedge clk or negedge rst_n)begin 
    if(!rst_n)begin
        rd_flag <= 'd0;
    end 
    else if(rdfifo_rdusedw < RD_MIN)begin 
        rd_flag <= 1'b1;
    end 
    else if(rdfifo_rdusedw > RD_MAX)begin
        rd_flag <= 1'b0;
    end
    else begin 
        rd_flag <= rd_flag;
    end 
end

在状态机跳转时,需要满足读操作后要变为写操作,写操作后要接读操作,这里就要判断一下上一个状态是读状态还是写状态

//last_op_flag
always @(posedge clk or negedge rst_n)begin 
    if(!rst_n)begin
        last_op_flag <= 'd0;
    end 
    else if(WRITE_2_DONE)begin 
        last_op_flag <= 1'b0;
    end 
    else if(READ_2_DONE)begin
        last_op_flag <= 1'b1;
    end
    else begin 
        last_op_flag <= last_op_flag;
    end 
end

在这判断跳转条件,为0代表上一个为写状态,为1代表上一个为读状态,利用这一标志,来确定下一个是什么状态

//pri_flag(1、只写 2、只读 3、读写同时——>a、上一次写,本次读 b、上一次读,本次写)
always  @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        pri_flag <= 1'b0;
    end
    else if((wr_flag && ~rd_flag) /* <---情况1 */|| (wr_flag && rd_flag && last_op_flag)/*<---情况3.b*/)begin
        pri_flag <= 1'b0;
    end
    else if((~wr_flag && rd_flag) /* <---情况2 */|| (wr_flag && rd_flag && ~last_op_flag)/*<---情况3.a*/)begin
        pri_flag <= 1'b1;
    end
end

ps:虽然将各种情况都一一列出,但并不代表所有情况都会出现。
当写完并读完一帧就切换bank。
由于地址线也是读写共用,所以在传输地址时要通过状态机,来判断当前传输的是读地址还是写地址。

3、VGA显示图像

这里与之前内容并无不同,详见VGA简介,这里不在赘述。

四、实现效果

在这里插入图片描述
可以通过采集卡,将输出的图像采集到PC端进行实时显示。


网站公告

今日签到

点亮在社区的每一天
去签到