基于FPGA的OV5640摄像头图像采集

news2024/11/18 3:48:04

1.OV5640简介

OV5640是OV(OmniVision)公司推出的一款CMOS图像传感器,实际感光阵列为:2592 x 1944(即500w像素),该传感器内部集成了图像出炉的电路,包括自动曝光控制(AEC)、自动白平衡( AWB) 等。同时该传感器支持LED补光、 MIPI(移动产业处理器接口,多用于手机等)输出接口和DVP(数字视频并行,在设计HDMI显示时,就用的这个)输出接口选择、 ISP(图像信号处理)以及自动聚焦控制(AFC)等功能。

2.OV5640工作原理

OV5640的功能框图如上,可以看到,时序发生器和系统控制逻辑(timing generator and system control logic)控制着感光阵列(image array)、放大器(AMP)、AD转换(10bit)以及输出外部时序信号(PCLK和行场同步信号等)。

感光阵列输出模拟信号,经过AMP增强信号强度,进入到AD转换器,转换成数字信号并经过ISP,进行相关图像处理,最终输出10位DVP数据流或者MIPI数据流。

AMP和ISP等都是由控制寄存器进行控制,而配置寄存器的接口时序就是使用的SCCB,由于OV5640寄存器较多,OV5640寄存器的地址为16位,所以SCCB协议中的寄存器地址为16位。

OV5640摄像头引脚功能描述如下表所示:

注意XCLK引脚,它跟 PCLK是完全不同的,XCLK是用于驱动个传感器芯片的时钟信号,是外部输入到OV5640的信号;而 PCLKOV5640输出数据时的同步信号,它是由OV5640输出的信号。XCLK可以外接晶振或由外部控制器提供。

OV5640的输出模式如下图所示,我们可以通过对其寄存器的配置来控制不同的工作模式。

  • 3.OV5640寄存器功能介绍

OV5640的寄存器较多,对于其它寄存器的描述可以参OV5640的数据手册。但是,OV5640的数据手册并没有提供全部的寄存器描述, 而大多数必要的寄存器配置在OV5640的软件应用手册(《OV5640 Camera Module Software Application Notes》)中可以找到,其中还有相关初始化例程。这里我们只介绍几个关键的寄存器配置。

输出模式设置如下图所示,可以通过配置0x4300这个寄存器控制输出的像素模式包括REG565YUV422等常用模式。设置输出模式为RGB565时还可以控制输出。

输出像素设置则通过0x3808~0x380b进行控制,方法也非常简单,只需要将期望得到的分辨率转换为16进制数据,再分别写入四个寄存器即可。举个例子,我想要的分辨率为960x540960转换为16进制数据为3c0540转换为16进制数据为21c,因此我需要向0x3808中写入03,向0x3809中写入c0,向0x380a中写入02,向0x380b中写入1c

OV5640的像素时钟计算如图所示

通过图可以看出PCLK是经过图中8个步骤之后得到的频率,以下逐步计算得到PCLK。

OV5640要求输入的时钟频率为6-27MHz,一般情况下输入24MHz,在本次计算中也以24MHz为输入频率;

输入时钟首先经过pre-divider进行分频,分频系数由3037[3:0]确定,在本次计算中3037[3:0]3,故经过分频之后的输出为24/3=8MHz

经过pre-divider分频后需要给分频后的时钟做一次倍频,乘法因子为3036[6:0]=0x69=105,经过倍频后的时钟频率为8MHz*105=840MHz;

Sys divider0分频,分频系数为0x3035[7:4],在demo中的值为1,故没有进行分频;840MHz/1=840MHz;

PLL R divider分频,如果0x3037[4]为高电平,则进行2分频,否则不分频;在demo3037[4]1,故二分频;840MHz/2=420MHz;

BIT divider分频,分频系数为0x3034[3:0],如果是8,则是2分频,如果是A则是2.5分频,如果是其他则为1分频;在demo0x3034[3:0]a,故需要进行2.5分频;420MHz/2.5=168MHz;

PCLK divider分频, 分频系数为0x3108[5:4],00:1分频;01:2分频;10:4分频;11:8分频;在demo0x3108[5:4]=2’b00,故需要进行1分频;168MHz/1=168MHz

P divider分频,如果是mipi2 lane,则分频系数是0x3035[3:0],如果是DVP 接口则分频系数为2*0x3035[3:0]=2,在demo0x3035[3:0]=1,故在此是2分频;168MHz/2=84MHz

Scale divider分频,分频系数为0x3824[4:0],demo0x3824[4:0]=2故需要进行2分频,84MHz/4=21MHz

通过以上分析可以看出在demo中输入时钟为24MHz时,输出时钟为21MHz

OV5640 的图像输出帧率可以通过修改地址为 0x3035、0x3036、0x3037 的寄存器的值来修改,该寄存器实际上是设置了 OV5640 片上 PLL 的各种分频和倍频系数,例如在典型配置模式下,当输入时钟 XCLK 的信号频率为 24MHz 时, 设置 0x3035 寄存器的值为 0x21 可设置输出帧率为30fps,设为0x41可设置输出帧率为15fps、设为0x81可设置输出帧率为7.5fps。

4.SCCB协议

外部控制器对 OV5640 寄存器的配置参数是通过 SCCB 总线传输过去的,而 SCCB 总线跟 I2C 十分类似。

SCCB 的起始、停止信号及数据有效性

  • 起始信号: SCL(图中为 SIO_C 为高电平时, SDA(图中为 SIO_D)出现一个下降沿,则 SCCB 开始传输。
  • 停止信号:在 SCL 为高电平时, SDA 出现一个上升沿,则 SCCB 停止传输。
  • 数据有效性:除了开始和停止状态, 在数据传输过程中,当 SCL 为高电平时,必须保证 SDA 上的数据稳定,也就是说, SDA 上的电平变换只能发生在 SCL 为低电平的时候,SDA 的信号在 SCL 为高电平时被采集。

在 SCCB 协议中定义的读写操作与 I2C 也是一样的,只是换了一种说法。它定义了两种写操作,即三步写操作和两步写操作。三步写操作可向从设备的一个目的寄存器中写入数据,见下图。在三步写操作中,第一阶段发送从设备的ID地址+W标志(等于 I2C 的设备地址:7位设备地址+读写方向标志),第二阶段发送从设备目标寄存器的 8 位地址,第三阶段发送要写入寄存器的 8 位数据。图中的“X”数据位可写入 1 0,对通讯无影响。而在i2c协议中“X”为从机给主机的响应,若主机未收到从机的响应信号则无法发送后面的数据。

而两步写操作没有第三阶段,即只向从器件传输了设备 ID+W 标志和目的寄存器的地址,见下图 。两步写操作是用来配合后面的读寄存器数据操作的,它与读操作一起使用,实现i2c的复合过程。

两步读操作,它用于读取从设备目的寄存器中的数据,见下图。在第一阶段中发送从设备的设备 ID+R 标志(设备地址+读方向标志)和自由位,在第二阶段中读取寄存器中的8 位数据和写 NA 位(非应答信号)。 由于两步读操作没有确定目的寄存器的地址,所以在读操作前,必需有一个两步写操作,以提供读操作中的寄存器地址。

总的来说,i2c协议与SCCB协议的主要区别如下:

.SCCB的应答位称为X,表示“don't care”,而i2c应答位称为ACK

    .SCCB只能单次读,而i2c除了单次读还支持连续读。

  .SCCB读操作中间有stop,而i2c读操作中间可以有stop也可以不需要stop

5.程序设计

OV5640的整体设计框图如图,总共包含三个模块:i2c驱动模块、寄存器配置模块和图像采集模块。

`timescale 1ns / 1ps

module ov5640_top#(
    parameter   DEVICE_ADDR     =   7'b0111_100     ,                                                   //i2c从机地址
    parameter   SYS_CLK_FREQ    =   27'd100_000_000  ,                                                  //系统时钟频率
    parameter   I2C_FREQ        =   19'd400_000,                                                        //i2c时钟频率,400k
    parameter   PIC_CNT_MAX     =   4'd10,                                                              //舍弃前10帧数据
    parameter   REG_NUM         =   8'd0250,                                                            //需配置寄存器个数
    parameter   CNT_WAIT_MAX    =   15'd20000                                                           //寄存器配置等待时间
    )
    (
        input                                                   clk,                                    //系统时钟,100MHz
        input                                                   rst_n,                                  //系统复位

        input                                                   pclk,                                   //ov5640工作时钟
        input                                                   hsync,                                  //行同步信号
        input                                                   vsync,                                  //场同步信号
        input                       [7:0]                       ov5640_din,                             //ov5640输入数据
        input                                                   init_done,                              //初始化完成信号

        output                  wire                            ov5640_dout_en,                         //输出图像数据使能信号
        output                  wire[15:0]                      ov5640_dout,                            //输出16位图像数据
        output                  wire                            cfg_done,                               //寄存器配置完成信号       
        inout                   wire                            sda,                                    //i2c数据总线
        output                  wire                            scl                                     //i2c时钟总线
    );

    wire                                                        cfg_1_done;                             //单个寄存器配置完成信号
    wire                                                        cfg_start;                              //开始配置信号
    wire                       [23:0]                           cfg_data;                               //寄存器地址+写入数据
    wire                                                        i2c_clk;                                //i2c驱动时钟  


ov5640_data #(
    .PIC_CNT_MAX ( 4'd10 ))
 u_ov5640_data (
    .rst_n                                        ( rst_n && init_done                            ),
    .pclk                                         ( pclk                                          ),
    .hsync                                        ( hsync                                         ),
    .vsync                                        ( vsync                                         ),
    .ov5640_din                                   ( ov5640_din                                    ),

    .ov5640_dout_en                               ( ov5640_dout_en                                ),
    .ov5640_dout                                  ( ov5640_dout                                   )
);

ov5640_cfg #(
    .REG_NUM      ( 8'd0250   ),
    .CNT_WAIT_MAX ( 15'd20000 ))
 u_ov5640_cfg (
    .clk                               ( i2c_clk                            ),
    .rst_n                             ( rst_n                              ),
    .cfg_1_done                        ( cfg_1_done                         ),

    .cfg_start                         ( cfg_start                          ),
    .cfg_data                          ( cfg_data                           ),
    .cfg_done                          ( cfg_done                           )
);

i2c_drive #(
    .DEVICE_ADDR  ( 7'b0111_100    ),
    .SYS_CLK_FREQ ( 27'd100_000_000 ),
    .I2C_FREQ     ( 19'd400_000    ))
 u_i2c_drive (
    .sys_clk                 ( clk                      ),
    .sys_rst_n               ( rst_n                    ),
    .i2c_rw                  ( 1'b0                     ),
    .i2c_start               ( cfg_start                ),
    .i2c_num                 ( 1'b1                     ),
    .i2c_addr                ( cfg_data[23:8]           ),
    .i2c_data_w              ( cfg_data[7:0]            ),

    .i2c_clk                 ( i2c_clk                  ),
    .i2c_end                 ( cfg_1_done               ),
    .i2c_data_r              ( i2c_data_r               ),
    .scl                     ( scl                      ),

    .sda                     ( sda                      )
);

endmodule

5.1图像采集模块

模块输入信号有5路,输入时钟信号为OV5640_pclk,由OV5640摄像头自带晶振产生并传入,频率24MHz,作为模块工作时钟;复位信号rst_n,低电平有效;OV5640_vsync为摄像头采集图像的场同步信号,可类比与VGA场同步信号,只在同步阶段为高电平,其他时刻保持低电平;OV5640_hsync为行有效图像使能信号,信号只有采集图像行有效显示区域为高电平,其他时刻为低电平;最后的OV5640_data为摄像头采集到的图像数据,要注意的是,OV5640_data位宽为8bit,采集的图像数据分两次传入模块,先传入图像数据高字节,下个时钟周期传入低字节。

系统上电后,摄像头刚采集的前几帧图像数据不太稳定,要先舍弃前10帧图像,之后的图像才能用于显示。为了舍弃前10帧图像,我们需要声明几个变量。首先要舍弃前10帧图像,需要一个计数器来计数,声明计数器pic_cnt对输入图像帧数进行计数;接下来就要考虑以什么为标志进行计数,这时我们想到每帧图像的传入,帧同步信号必不可少,那么声明帧同步信号寄存信号vsync_r,此信号延后帧同步信号一个时钟周期,利用两信号产生帧同步信号下降沿pic_flag,作为帧计数器pic_cnt的计数标志信号,该信号每拉高一次计数器自加1;声明帧有效信号pic_valid,当计数器计数到第10帧,pic_flag为高电平,将帧有效信号拉高并始终保持高电平。

前面说到,像素点图像信息并不是在一个时钟周期传入,而是在第一个时钟周期传入高8位,下一个时钟周期传入低8位,所以要正确显示图像就需要对传入图像数据进行拼接。实现数据拼接就需要声明若干变量。需要先声明一个寄存器对图像数据的高字节进行数据缓存,待低字节数据传入时,将图像数据进行拼接。首先声明寄存器OV5640_din_r对高字节数据进行缓存;声明标志信号data_flag控制数据缓存与拼接,在hsync信号有效时,标志着输入图像数据有效,data_flag不断取反,当其为低电平时对高字节数据进行缓存,当其为高电平时对数据进行拼接。将拼接后的数据赋值给OV_5640_dout_r

`timescale 1ns / 1ps

module ov5640_data#(
        parameter PIC_CNT_MAX                 =                 4'd10                               //舍弃前10帧数据
    )
    (
        input                                                   rst_n,                              //系统复位
        input                                                   pclk,                               //ov5640工作时钟
        input                                                   hsync,                              //行同步信号
        input                                                   vsync,                              //场同步信号
        input                       [7:0]                       ov5640_din,                         //ov5640输入数据

        output                  wire                            ov5640_dout_en,                     //输出图像数据使能信号
        output                  wire[15:0]                      ov5640_dout                         //输出16位图像数据
    );

    wire                                                        pic_flag;                           //帧图像标志信号,拉高一次标志一帧图像传输完成

    reg                                                         vsync_r;                            //场同步信号打拍
    reg                             [7:0]                       ov5640_din_r;                       //暂存输入8位数据
    reg                             [15:0]                      ov5640_dout_r;                      //暂存输出16位数据
    reg                                                         pic_valid;                          //帧图像有效信号   
    reg                             [9:0]                       pic_cnt;                            //帧图像计数器
    reg                                                         data_flag;                          //图像拼接标志
    reg                                                         data_flag_r;                        //图像拼接标志打拍

    always @(posedge pclk or negedge rst_n) begin
        if(!rst_n)
            vsync_r <= 1'b0;
        else 
            vsync_r <= vsync;
    end

    always @(posedge pclk or negedge rst_n) begin
        if(!rst_n)
            pic_cnt <= 10'd0;
        else if(pic_cnt < PIC_CNT_MAX && pic_flag == 1'b1)
            pic_cnt <=pic_cnt + 1'b1;
        else
            pic_cnt <= pic_cnt;
    end

    always @(posedge pclk or negedge rst_n) begin
        if(!rst_n)
            pic_valid <= 1'b0;
        else if(pic_cnt == PIC_CNT_MAX && pic_flag == 1'b1)
            pic_valid <= 1'b1;
        else
            pic_valid <= pic_valid;
    end

    always @(posedge pclk or negedge rst_n) begin
        if(!rst_n)begin
            data_flag <= 1'b0;
            ov5640_din_r <= 8'd0;
            ov5640_dout_r <= 16'd0;
        end
        else if(hsync)begin
            data_flag <= ~data_flag;
            ov5640_din_r <= ov5640_din;
            ov5640_dout_r <= ov5640_dout_r;
            if(data_flag)
                ov5640_dout_r <= {ov5640_din_r,ov5640_din};                                         //像素数据拼接
            else 
                ov5640_dout_r <= ov5640_dout_r;
        end
        else begin
            data_flag <= 1'b0;
            ov5640_din_r <= 8'd0;
            ov5640_dout_r <= ov5640_dout_r;
        end
    end

    always @(posedge pclk or negedge rst_n) begin
        if(!rst_n)
            data_flag_r <= 1'b0;
        else
            data_flag_r <= data_flag;
    end

    assign pic_flag = (vsync == 1'b1 && vsync_r == 1'b0) ? 1'b1 : 1'b0;

    assign ov5640_dout = (pic_valid == 1'b1) ? ov5640_dout_r : 16'd0;

    assign ov5640_dout_en = (pic_valid == 1'b1) ? data_flag : 1'b0;

endmodule

5.2SCCB协议

由于SCCB协议与i2c协议非常相似,因此我们可以i2c协议稍加改动便可进行寄存器配置,利用之前的i2c驱动模块,将应答信号直接拉高即可。然后还要将器件地址改成OV5640的器件地址,即0111_100

module  i2c_drive
#(
    parameter   DEVICE_ADDR     =   7'b0111_100     ,   //i2c从机地址
    parameter   SYS_CLK_FREQ    =   27'd100_000_000  ,   //系统时钟频率
    parameter   I2C_FREQ        =   19'd400_000         //i2c时钟频率,400k
)
(
//系统接口
    input		        sys_clk     ,   			//输入系统时钟,100MHz
    input		        sys_rst_n   ,   			//输入复位信号,低电平有效
//I2C时序控制接口				
	input				i2c_rw		,				//读写使能信号----1:读;0:写
    input		        i2c_start   ,   			//i2c开始信号
    input		        i2c_num    	,   			//i2c字节地址字节数----1:16位;0:8位
    input		[15:0]  i2c_addr   	,   			//i2c字节地址
    input		[7:0]   i2c_data_w	,   			//写入i2c数据
    output  reg			i2c_clk     ,   			//i2c驱动时钟
    output  reg			i2c_end     ,   			//i2c一次读/写操作完成
    output  reg	[7:0]   i2c_data_r  ,   			//i2c读取数据
//I2C物理接口				
    output  reg			scl     	,   			//输出至i2c设备的串行时钟信号scl
    inout   wire		sda         				//输出至i2c设备的串行数据信号sda
);		
		
//状态机定义		
localparam	IDLE 			= 4'd0,					//初始化状态
			START1			= 4'd1,					//发送开始信号状态1
			SEND_D_ADDR_W 	= 4'd2,					//设备地址写入状态 + 控制写
			ACK1			= 4'd3,					//等待从机响应信号1
			SEND_R_ADDR_H	= 4'd4,					//发送寄存器地址高8位
			ACK2			= 4'd5,					//等待从机响应信号2
			SEND_R_ADDR_L	= 4'd6,					//发送寄存器地址低8位
			ACK3			= 4'd7,					//等待从机响应信号3
			WR_DATA         = 4'd08,  				//写数据状态
            ACK4            = 4'd09,  				//应答状态4
            START2          = 4'd10,  				//发送开始信号状态12
            SEND_D_ADDR_R   = 4'd11,  				//设备地址写入状态 + 控制读
            ACK5            = 4'd12,  				//应答状态5
            RD_DATA         = 4'd13,  				//读数据状态
            NACK            = 4'd14,  				//非应答状态
            STOP            = 4'd15;  				//结束状态

//根据系统频率及IIC驱动频率计算分频系数			
localparam	CLK_DIVIDE = SYS_CLK_FREQ / I2C_FREQ >> 2'd3;	

//reg定义			
reg	[9:0]	clk_cnt			;						//分频时钟计数器,最大计数1023			
reg	[3:0]	cur_state		;						//状态机现态	 
reg	[3:0]	next_state		;						//状态机次态	
reg			i2c_clk_cnt_en	;			 			//驱动时钟计数使能
reg	[1:0]	i2c_clk_cnt		;			 			//驱动计数时钟,方便在SCL的高电平中间采集数据;和在SCL的低电平中间变化数据
reg			sda_out			;						//IIC总线三态输出
reg			sda_en			;						//IIC总线三态门使能
reg [2:0]	bit_cnt			;						//接收数据个数计数器
reg			ack_flag		;						//应答信号标志
reg	[7:0]	i2c_data_r_temp	;						//读取数据寄存器,暂存读到的数据
			
//wire定义		
wire		sda_in			;						//IIC总线三态输入
wire [7:0]	addr_r			;						//器件地址+读控制位
wire [7:0]	addr_w			;						//器件地址+写控制位
		
assign addr_r = {DEVICE_ADDR,1'b1};					//器件地址+读控制位
assign addr_w = {DEVICE_ADDR,1'b0};					//器件地址+写控制位

//双向口处理
assign sda_in = sda;				
assign sda = sda_en ? sda_out : 1'bz;

//scl4分频时钟=IIC驱动时钟i2c_clk,方便操作对采集数据及变化数据操作
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(~sys_rst_n)begin
		i2c_clk <= 1'b0;
		clk_cnt <= 10'd0;
	end
	else if(clk_cnt == CLK_DIVIDE - 1'b1)begin
		i2c_clk <= ~i2c_clk;
		clk_cnt <= 10'd0;		
	end
	else begin
		i2c_clk <= i2c_clk;
		clk_cnt <= clk_cnt + 1'd1;	
	end
end

//i2c_clk计数器使能
always@(posedge i2c_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)
		i2c_clk_cnt_en <= 1'b0;
	//只有在发送完了结束信号或者没有接收到IIC开始传输信号的初始状态下才不停对i2c_clk计数器复位(使能为0)
 	else if ((cur_state == STOP && i2c_clk_cnt == 2'd3 && bit_cnt == 2'd3)||(cur_state == IDLE && !i2c_start ))
		i2c_clk_cnt_en <= 1'b0; 
	else if(i2c_start)							
		i2c_clk_cnt_en <= 1'b1;							//接收到开始信号,代表一次传输开始,计数器开始计数
	else											
		i2c_clk_cnt_en <= i2c_clk_cnt_en;				//其他时候保持不变
end

//i2c_clk_cnt计数器
always@(posedge i2c_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)
		i2c_clk_cnt <= 2'd0;
	else if(i2c_clk_cnt_en)						
		i2c_clk_cnt <= i2c_clk_cnt + 1'd1;				//使能信号有效,计数器开始计数
	else		
		i2c_clk_cnt <= 2'd0;							//使能信号无效,计数器清零
end
	
//三段式状态机第一段
always@(posedge i2c_clk or negedge sys_rst_n)begin
	if(~sys_rst_n)
		cur_state <= IDLE;
	else
		cur_state <= next_state;
end

//三段式状态机第二段
always@(*)begin
	next_state = IDLE;
	case(cur_state)
		IDLE:
			if(i2c_start)
				next_state = START1;					//接收到开始信号,跳转到发送起始信号状态
			else
				next_state = IDLE;
		START1:
			if(i2c_clk_cnt == 2'd3)						//i2c_clk 计数到最大值3,跳转到发送器件地址+写标志位状态
				next_state = SEND_D_ADDR_W;
			else
				next_state = START1;
		SEND_D_ADDR_W:
			if(i2c_clk_cnt == 2'd3 && bit_cnt == 3'd7)	//发送了8位地址后跳转到从机响应状态
				next_state = ACK1;	
			else
				next_state = SEND_D_ADDR_W;			
		ACK1:
			if(ack_flag && i2c_clk_cnt == 2'd3)begin	//响应标志有效
				//根据地址状态位判断是16位地址还是8位地址,从而跳转到不同状态
				if(i2c_num)								//16位地址
					next_state = SEND_R_ADDR_H;			//跳转到寄存器高8位地址发送状态
				else									//8位地址
					next_state = SEND_R_ADDR_L;			//跳转到寄存器低8位地址发送状态
			end	
			else if(i2c_clk_cnt == 2'd3)				//响应无效或者响应不及时则跳转回初始状态
				next_state = IDLE;
			else 
				next_state = ACK1;
 		SEND_R_ADDR_H:									
			if(i2c_clk_cnt == 2'd3 && bit_cnt == 3'd7)	//发送了寄存器高8位地址后跳转到从机响应状态
				next_state = ACK2;
			else										
				next_state = SEND_R_ADDR_H;
		ACK2:
			if(ack_flag && i2c_clk_cnt == 2'd3)			
				next_state = SEND_R_ADDR_L;				//响应标志有效则跳转到寄存器低8位地址发送状态
			else if(i2c_clk_cnt == 2'd3)				//响应无效或者响应不及时则跳转回初始状态
				next_state = IDLE;	
			else
				next_state = ACK2;
		SEND_R_ADDR_L:
			if(i2c_clk_cnt == 2'd3 && bit_cnt == 3'd7)	//发送了寄存器低8位地址后跳转到从机响应状态
				next_state = ACK3;
			else
				next_state = SEND_R_ADDR_L;
		ACK3:
			if(ack_flag && i2c_clk_cnt == 2'd3)begin	//响应标志有效	
				if(i2c_rw)								//读状态
					next_state = START2;				//跳转到第二次发送起始信号
				else									//写状态
					next_state = WR_DATA;				//跳转到写数据状态
			end
			else if(i2c_clk_cnt == 2'd3)				
				next_state = IDLE;						//响应无效或者响应不及时则跳转回初始状态
			else
				next_state = ACK3;
		START2:
			if(i2c_clk_cnt == 2'd3)						
				next_state = SEND_D_ADDR_R;				//第二次发送起始信号后跳转到发送器件地址+读标志位状态
			else
				next_state = START2;
		SEND_D_ADDR_R:
			if(i2c_clk_cnt == 2'd3 && bit_cnt == 3'd7)	//发送完了8位地址后跳转到从机响应状态
				next_state = ACK5;	
			else
				next_state = SEND_D_ADDR_R;			
		ACK5:
			if(ack_flag && i2c_clk_cnt == 2'd3)			
				next_state = RD_DATA;                   //响应标志有效则跳转到读数据状态
			else if(i2c_clk_cnt == 2'd3)                
				next_state = IDLE;				        //响应无效或者响应不及时则跳转回初始状态
			else                                        
				next_state = ACK5;		
		RD_DATA:
			if(i2c_clk_cnt == 2'd3 && bit_cnt == 3'd7)	//接收完了8位数据后跳转到主机发送非响应状态
				next_state = NACK;
			else
				next_state = RD_DATA;		
		NACK:
			if(i2c_clk_cnt == 2'd3)						
				next_state = STOP;						//发送完了非响应信号后跳转到发送结束信号状态
			else
				next_state = NACK;						
		WR_DATA:
			if(bit_cnt == 3'd7 && i2c_clk_cnt == 2'd3)	
				next_state = ACK4;						//写完了8位数据后跳转到从机响应状态
			else
				next_state = WR_DATA;
		ACK4:
			if(ack_flag && i2c_clk_cnt == 2'd3)
				next_state = STOP;						//响应标志有效则跳转到发送结束信号状态
			else if(i2c_clk_cnt == 2'd3)
				next_state = IDLE;						//响应无效或者响应不及时则跳转回初始状态
			else
				next_state = ACK4;
		STOP:
			if(bit_cnt == 2'd3 && i2c_clk_cnt == 2'd3)	//结束信号发送完毕(这里还预留了2个周期)跳转到初始状态,等待下一次传输开始信号
				next_state = IDLE;
			else
				next_state = STOP;		
		default:next_state = IDLE;
	endcase
end

//三段式状态机第三段
always@(posedge i2c_clk or negedge sys_rst_n)begin
	if(~sys_rst_n)begin								//初始状态
		sda_en <= 1'b1;
		sda_out <= 1'b1;
		bit_cnt	<= 3'd0;
		i2c_end <= 1'b0;
		i2c_data_r <= 8'd0;
		i2c_data_r_temp <= 8'd0;
	end
	else begin
		i2c_end <= 1'b0;
		case(cur_state)
			IDLE:begin
				sda_en <= 1'b1;						//控制总线
				sda_out <= 1'b1;					//拉高总线
			end	
			START1:begin
					if(i2c_clk_cnt == 2'd3)begin	//发送完了开始信号
						if(addr_w[7])begin			//如果器件地址的最高位为1则提前拉高总线
							sda_en <= 1'b1;
							sda_out <= 1'b1;													
						end
						else begin					//如果器件地址的最高位为0则提前拉低总线
							sda_en <= 1'b1;
							sda_out <= 1'b0;												
						end
					end
					else begin						//还没发送完开始信号则保持低电平
						sda_en <= 1'b1;
						sda_out <= 1'b0;					
					end
			end	
			SEND_D_ADDR_W:begin
				if(bit_cnt == 3'd7)begin			
					if(i2c_clk_cnt == 2'd3)begin	//发送了8个数据(器件地址+写标志位)
						bit_cnt <= 3'd0;			//发送数据计数器清零
						sda_en <= 1'b0;				//释放总线
					end
				end
				else if(i2c_clk_cnt == 2'd3)begin	//发送完了一个数据
					bit_cnt <= bit_cnt + 1'd1;		//发送数据计数器清零
					sda_en <= 1'b1;					//控制总线
					sda_out <= addr_w[6-bit_cnt];	//总线依次串行输出地址
				end
			end
			ACK1:begin
  				if(i2c_clk_cnt == 2'd3)begin				
					if(i2c_num)begin				//如果器件地址为16位
						if(i2c_addr[15])begin		//如果器件地址的16位为1则提前拉高总线
							sda_en <= 1'b1;
							sda_out <= 1'b1;	
						end
						else begin					//如果器件地址的16位为0则提前拉低总线
							sda_en <= 1'b1;
							sda_out <= 1'b0;	
						end
					end
					else begin						//如果器件地址为8位
						if(i2c_addr[7])begin		//如果器件地址的8位为1则提前拉高总线
							sda_en <= 1'b1;
							sda_out <= 1'b1;	
						end
						else begin					//如果器件地址的8位为0则提前拉低总线
							sda_en <= 1'b1;
							sda_out <= 1'b0;	
						end
					end			
				end 
			end
			SEND_R_ADDR_H:begin
				if(bit_cnt == 3'd7)begin			//8个数据发送完了
					if(i2c_clk_cnt == 2'd3)begin
						bit_cnt <= 3'd0;			//发送数据计数器清零
						sda_en <= 1'b0;				//释放总线
					end
				end
				else if(i2c_clk_cnt == 2'd3)begin
					bit_cnt <= bit_cnt + 1'd1;		//发送数据计数器清零
					sda_en <= 1'b1;                 //控制总线
					sda_out <= i2c_addr[14-bit_cnt];//总线依次串行输出地址
				end			
			end
			ACK2:begin
				if(i2c_clk_cnt == 2'd3)begin
					if(i2c_addr[7])begin			//下一个要发送数据的首个数据为高则提前拉高总线
						sda_en <= 1'b1;
						sda_out <= 1'b1;													
					end
					else begin						//下一个要发送数据的首个数据为低则提前拉低总线
						sda_en <= 1'b1;
						sda_out <= 1'b0;												
					end				
				end
			end	
			SEND_R_ADDR_L:begin	
				if(bit_cnt == 3'd7)begin				//8个数据发送完了
					if(i2c_clk_cnt == 2'd3)begin
						bit_cnt <= 3'd0;				//发送数据计数器清零
						sda_en <= 1'b0;					//释放总线
					end
				end
				else if(i2c_clk_cnt == 2'd3)begin
						bit_cnt <= bit_cnt + 1'd1;		//发送数据计数器清零
						sda_en <= 1'b1;                 //控制总线
						sda_out <= i2c_addr[6-bit_cnt]; //总线依次串行输出地址
				end			
			end
			ACK3:begin
				if(!i2c_rw)begin					//是写操作
					if(i2c_clk_cnt == 2'd3)begin
						if(i2c_data_w[7])begin		//下一个要发送数据的首个数据为高则提前拉高总线
							sda_en <= 1'b1;
							sda_out <= 1'b1;													
						end
						else begin					//下一个要发送数据的首个数据为低则提前拉低总线
							sda_en <= 1'b1;
							sda_out <= 1'b0;												
						end				
					end 
				end
				else begin							//是读操作
					if(i2c_clk_cnt == 2'd3)begin	//提前拉高总线进入再次发送起始信号状态
						sda_en <= 1'b1;
						sda_out <= 1'b1;								
					end
					else begin
						sda_en <= 1'b1;
						sda_out <= 1'b0;												
					end				
				end
			end
			START2:begin
				if(i2c_clk_cnt == 2'd1)begin		//拉低总线
					sda_en <= 1'b1;
					sda_out <= 1'b0;						
				end
				else if(i2c_clk_cnt == 2'd3)begin
					if(addr_r[7])begin				//下一个要发送数据的首个数据为高则提前拉高总线
						sda_en <= 1'b1;
						sda_out <= 1'b1;													
					end
					else begin						//下一个要发送数据的首个数据为低则提前拉低总线
						sda_en <= 1'b1;
						sda_out <= 1'b0;												
					end						
				end
			end
			SEND_D_ADDR_R:begin
				if(bit_cnt == 3'd7)begin				//8个数据发送完了
					if(i2c_clk_cnt == 2'd3)begin
						bit_cnt <= 3'd0;				//发送数据计数器清零
						sda_en <= 1'b0;					//释放总线
					end
				end
				else if(i2c_clk_cnt == 2'd3)begin
						bit_cnt <= bit_cnt + 1'd1;		//发送数据计数器清零
						sda_en <= 1'b1;                 //控制总线
						sda_out <= addr_r[6-bit_cnt];   //总线依次串行输出地址
				end
			end		
			ACK5:
				sda_en <= 1'b0;							//下一个状态是接收数据,所以释放总线
			RD_DATA:
				if(i2c_clk_cnt == 2'd3)begin
					if(bit_cnt == 3'd7)begin			//接收了8个数据
						bit_cnt <= 3'd0;				//发送数据计数器清零
						sda_en <= 1'b1;					//控制总线
						sda_out <= 1'b1;                //拉高总线(为了下一步发送非响应信号)
						i2c_data_r <= i2c_data_r_temp;	//将读取的数据输出						
					end
					else begin							//数据还未接收完毕		
						bit_cnt <= bit_cnt + 3'd1;				
					end				
				end
				else if(i2c_clk_cnt == 2'd1)begin		//在SCL的中间采集数据
					i2c_data_r_temp[7-bit_cnt] <=sda_in;//将总线上的数据依次串行采集				
				end							
			NACK:
 				if(i2c_clk_cnt == 2'd3)begin			
					sda_en <= 1'b1;						//控制总线
					sda_out <= 1'b0;					//拉高总线																					
				end				
			WR_DATA:
				if(bit_cnt == 3'd7)begin				//写完了8个数据
					if(i2c_clk_cnt == 2'd3)begin
						bit_cnt <= 3'd0;				//发送数据计数器清零
						sda_en <= 1'b0;					//释放总线
					end
				end
				else if(i2c_clk_cnt == 2'd3)begin		//没有写完8个数据
					bit_cnt <= bit_cnt + 1'd1;			//发送数据计数器累加
					sda_en <= 1'b1;
					sda_out <= i2c_data_w[6-bit_cnt];	//依次输出数据
				end	
			ACK4:
 				if(i2c_clk_cnt == 2'd3)begin
					sda_en <= 1'b1;						//控制总线
					sda_out <= 1'b0;					//拉低总线(为了下一步发送终止信号)																					
				end				 
			STOP:				
 				if(i2c_clk_cnt == 2'd2 && bit_cnt == 2'd0)begin	//拉高信号作为终止信号	
					sda_en <= 1'b1;
					sda_out <= 1'b1;																									
				end
				else if( i2c_clk_cnt == 2'd3 )begin
					if(bit_cnt == 2'd3)begin					
						bit_cnt <= 2'd0;
						i2c_end <= 1'b1;				//发送完了终止信号且延时一段时间发送IIC结束信号
					end
					else
						bit_cnt <= bit_cnt + 1'd1;
				end
			default:;
		endcase
	end
end

//i2c时钟生成
always@(posedge i2c_clk or negedge sys_rst_n)begin
	if(~sys_rst_n)
		scl <= 1'b1;
	else if(cur_state != STOP)begin
		if(i2c_clk_cnt == 2'd2)
			scl <= 1'b0;
		else if(i2c_clk_cnt == 2'd0)
			scl <= 1'b1;	
	end
	else 
		scl <= 1'b1;
end
//从机响应信号标志
always@(posedge i2c_clk or negedge sys_rst_n)begin
	if(~sys_rst_n)
		ack_flag <= 1'b0;
	else 
		case(cur_state)
			ACK1,ACK2,ACK3,ACK4,ACK5:
				//if(i2c_clk_cnt == 2'd1 && !sda_in)		//在从机响应状态正确接收到了从机发送的响应信号则拉高响应标志
					ack_flag <= 1'b1;
				//else if(i2c_clk_cnt == 2'd3)			
				//	ack_flag <= 1'b0;
			default:ack_flag <= 1'b0;
		endcase
end

endmodule
  1. 6.仿真结果

总体仿真如上图所示,可以看到每一帧图像传输完成,场同步信号拉高一次,并且前10帧图像会被舍弃。

摄像头数据传输仿真如上图所示,使能信号每翻转一次进行一次数据拼接。

寄存器配置如上图所示,不需要从机发送应答信号。

7.问题总结

本次代码还未进行板级验证,先说一说仿真遇到的问题,其他问题后续再进行补充,首先就是方针过程中出现了如下图所示的情况。行同步信号和场同步信号出现了未知态,原因是有两个驱动,我的testbench是自动生成的因此开始会将两个信号的值赋0,后面我再对其进行赋值就会出现未知态。还有就是寄存器的配置,在网上找了很多资料都没有一个确定的答案,可能文中的说法也会有错,欢迎大家批评指正,代码参考正点原子。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2087753.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于深度学习的单目标跟踪系统

基于深度学习的单目标跟踪&#xff0c;效果吊打传统算法&#xff0c;3060显卡上达到实时&#xff0c;代码python和c两个版本都有。 基于深度学习的单目标跟踪系统是一种先进的计算机视觉技术&#xff0c;它可以实现实时的、高精度的目标跟踪。与传统的基于特征匹配或模板匹配的…

实现A-Z滑动检索菜单

写个这小玩意真麻烦 <template><div id"letterPeo"><!-- <button click"getasd">获取</button>--><div class"letter"><div v-for"(item, index) in letter" :key"index" clas…

高速接口IO片上SSN分析方法

Gbps信号在当今的高速IO设计中非常常见。由于封装和板上的寄生电感&#xff0c;高速信号在高频下汲取电流&#xff0c;导致大的电源尖峰或骤降。像DDR5这样的并行总线接口有20多个高速IO一起切换&#xff0c;导致同时切换噪声&#xff08;SSN&#xff09;。SSN的不期望的产物是…

苹果手机怎么恢复微信聊天记录?原来这4个方法这么好用

苹果手机的微信聊天记录不见了怎么办&#xff1f;想要解决这个问题&#xff0c;可以先了解一下导致聊天记录消失的原因有哪些。 误触手机&#xff1a;我们有时误触到某个按钮&#xff0c;也可能恢复导致聊天记录消失。卸载微信&#xff1a;卸载微信后&#xff0c;重新安装再打…

国密起步5:GmSSL3交叉编译arm64

初级代码游戏的专栏介绍与文章目录-CSDN博客 我的github&#xff1a;codetoys&#xff0c;所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。 这些代码大部分以Linux为目标但部分代码是纯C的&#xff0c;可以在任何平台上使用。 之前已经建立好了交叉编译环境…

让 Jenkins 到极狐GitLab 的迁移变得更加简单

极狐GitLab 是 GitLab 在中国的发行版&#xff0c;可以私有化部署&#xff0c;对中文的支持非常友好&#xff0c;是专为中国程序员和企业推出的企业级一体化 DevOps 平台&#xff0c;一键就能安装成功。安装详情可以查看官网指南。 文章原文可以点击极狐GitLab 官方资源中心查…

利用短信群发平台时提升短信营销打开率的关键因素

尽管众多企业依赖短信群发平台作为营销手段&#xff0c;但短信的实际打开率往往不尽如人意。以下是几个显著影响短信营销效果的关键因素及其优化策略&#xff1a; 1. 谨慎选择用词&#xff0c;规避敏感词汇 现代智能手机普遍配备了智能拦截功能&#xff0c;对包含特定敏感词汇…

C++宏展开

感觉自己一直对C的宏展开没有细致地研究过&#xff0c;这两天深入地学习了一下&#xff0c;做个笔记。 文章目录 宏展开基本规则宏嵌套展开补充说明参考资料 首先明确宏展开&#xff0c;是在预处理阶段进行的&#xff0c;进入编译期就是宏展开之后的代码了&#xff0c;所以不会…

基于Flask的新冠疫情信息可视化查询系统【案例模板】

文章目录 有需要本项目的代码或文档以及全部资源&#xff0c;或者部署调试可以私信博主每文一语 有需要本项目的代码或文档以及全部资源&#xff0c;或者部署调试可以私信博主 本项目是一个案例学习项目&#xff0c;可以作为新手进行学习系统的框架&#xff0c;本项目有数据库…

Git 学习

一、基本使用 1. 基本理论 Git 是一个免费的、开源的分布式版本控制系统&#xff0c;可以快速高效地处理从小型到大型的项目&#xff1b;版本控制是一种记录一个或者若干个文件内容变化&#xff0c;以便来查阅特定版本修订情况的系统 集中化版本控制系统&#xff1a;SVN, CV…

单片机使用cJSON的坑

文章目录 问题解决办法方法一方法二 问题 单片机USAR串口通信我想用json&#xff0c; 我不想用 分隔符的方式。感觉性能够&#xff0c;还有就是方便理解。 mcu型号 : AT32F415系列 雅特力的。 cJSON库: https://github.com/DaveGamble/cJSON/tree/master 只要把 cJSON.h 和 c…

云计算实训39——Harbor仓库的使用、Docker-compose的编排、YAML文件

一、Harbor部署 1.验证python版本 [rootdocker2 ~]#python --version 2.安装pip [rootdocker2 ~]# yum -y install python2-pip #由于版本过低&#xff0c;需要对其进行一个升级 #更新pip [rootdocker2 ~]#pip install --upgrade pip 3.指定版本号 [rootdocker2 ~]# p…

geodatatool(地图资源工具)下载高德数据及数据共享

利用geodatatool&#xff08;地图资源工具&#xff09;3.8&#xff08;新&#xff09;下载高德POI数据&#xff1a; 选择类型如下&#xff1a; 数据效果如下&#xff0c;由于用的免费的key&#xff0c;所以可能数据下载还不完全&#xff0c;但已经很多了&#xff1a; 下载数据…

小模型大智慧!港大重磅开源EasyRec,推荐系统进入语言模型时代

在当今的信息时代&#xff0c;我们每天都被海量信息所包围&#xff0c;不断面临各种选择。从网上购物、音乐播放到视频推荐&#xff0c;推荐系统已经成为我们生活中不可或缺的一部分。那么&#xff0c;这些系统是如何运作的&#xff1f;它们又是如何在信息的洪流中帮助我们找到…

《使用 LangChain 进行大模型应用开发》学习笔记(一)

前言 本文是 Harrison Chase &#xff08;LangChain 创建者&#xff09;和吴恩达&#xff08;Andrew Ng&#xff09;的视频课程《LangChain for LLM Application Development》&#xff08;使用 LangChain 进行大模型应用开发&#xff09;的学习笔记。由于原课程为全英文视频课…

智能优化特征选择|基于鲸鱼WOA优化算法实现的特征选择研究Matlab程序(KNN分类器)

智能优化特征选择|基于鲸鱼WOA优化算法实现的特征选择研究Matlab程序&#xff08;KNN分类器&#xff09; 文章目录 一、基本原理原理流程举个例子总结 二、实验结果三、核心代码四、代码获取五、总结 智能优化特征选择|基于鲸鱼WOA优化算法实现的特征选择研究Matlab程序&#x…

Android 优化之 查找so 文件的来源

序言 有时候我们需要优化apk的包体积大小。比如下面这样的。一个so文件大小有10M。但是我们并不知道so文件是那个库引入的。所以需要研究一下。 方法 在参考网上现有方法&#xff0c;加上自己测试以后。有了下面的成功。而且在gradle 8.4.2都可以成功。相信大家都可以成功。…

MathType常见问题汇总

文章目录 MathType常见问题汇总一、如何将MathType内嵌到WPS工具栏中&#xff1f;二、在word中&#xff0c;如何批量修改所有MathType公式的字体以及大小格式&#xff1f;三、如何解决插入MathType公式后的行间距发生改变&#xff1f;参考 MathType常见问题汇总 一、如何将Mat…

CEASC:基于全局上下文增强的自适应稀疏卷积网络在无人机图像上的快速目标检测

Adaptive Sparse Convolutional Networks with Global Context Enhancement for Faster Object Detection on Drone Images 摘要 提出了一种基于稀疏卷积的探测头优化方法&#xff0c;该方法在精度和效率之间取得了较好的平衡。然而&#xff0c;该算法对微小物体的上下文信息融…

C/C++ JSON ORM

structs #include "json_struct.h" #include <vector>JS_ENUM(Error, None, InvalidRange, IllegalParam, Nullptr, OverLimitLen) JS_ENUM_DECLARE_STRING_PARSER(Error)// 搜索匹配区域 struct RangeContent {size_t start;size_t end;std::strin…