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

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的信号;而 PCLK是OV5640输出数据时的同步信号,它是由OV5640输出的信号。XCLK可以外接晶振或由外部控制器提供。

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

3.OV5640寄存器功能介绍

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

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

输出像素设置则通过0x3808~0x380b进行控制,方法也非常简单,只需要将期望得到的分辨率转换为16进制数据,再分别写入四个寄存器即可。举个例子,我想要的分辨率为960x540,960转换为16进制数据为3c0,540转换为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分频,否则不分频;在demo中3037[4]为1,故二分频;840MHz/2=420MHz;

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

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

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

Scale divider分频,分频系数为0x3824[4:0],在demo中0x3824[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

6.仿真结果

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

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

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

7.问题总结

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

相关推荐
redcocal1 小时前
地平线内推码 kbrfck
c++·嵌入式硬件·mcu·算法·fpga开发·求职招聘
邹莉斯1 天前
FPGA基本结构和简单原理
fpga开发·硬件工程
悲喜自渡7211 天前
易灵思FPGA开发(一)——软件安装
fpga开发
ZxsLoves1 天前
【【通信协议ARP的verilog实现】】
fpga开发
爱奔跑的虎子1 天前
FPGA与Matlab图像处理之伽马校正
图像处理·matlab·fpga开发·fpga·vivado·xilinx
机器未来2 天前
基于FPGA的SD卡的数据读写实现(SD NAND FLASH)
arm开发·嵌入式硬件·fpga开发
贾saisai2 天前
Xilinx系FPGA学习笔记(八)FPGA与红外遥控
笔记·学习·fpga开发
吉孟雷3 天前
ZYNQ FPGA自学笔记
fpga开发·verilog·led·仿真·vivado·zynq
行者..................3 天前
1. ZYNQ 2. MPSOC 3. FPGA 4. IO分配 5. 硬件设计
fpga开发
tsumikistep3 天前
【无标题】Efinity 0基础进行流水灯项目撰写(FPGA)
fpga开发