当前位置: 首页 > news >正文

基于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的信号;而 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 / 1psmodule ov5640_top#(parameter   DEVICE_ADDR     =   7'b0111_100     ,                                                   //i2c从机地址parameter   SYS_CLK_FREQ    =   27'd100_000_000  ,                                                  //系统时钟频率parameter   I2C_FREQ        =   19'd400_000,                                                        //i2c时钟频率,400kparameter   PIC_CNT_MAX     =   4'd10,                                                              //舍弃前10帧数据parameter   REG_NUM         =   8'd0250,                                                            //需配置寄存器个数parameter   CNT_WAIT_MAX    =   15'd20000                                                           //寄存器配置等待时间)(input                                                   clk,                                    //系统时钟,100MHzinput                                                   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 / 1psmodule 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) beginif(!rst_n)vsync_r <= 1'b0;else vsync_r <= vsync;endalways @(posedge pclk or negedge rst_n) beginif(!rst_n)pic_cnt <= 10'd0;else if(pic_cnt < PIC_CNT_MAX && pic_flag == 1'b1)pic_cnt <=pic_cnt + 1'b1;elsepic_cnt <= pic_cnt;endalways @(posedge pclk or negedge rst_n) beginif(!rst_n)pic_valid <= 1'b0;else if(pic_cnt == PIC_CNT_MAX && pic_flag == 1'b1)pic_valid <= 1'b1;elsepic_valid <= pic_valid;endalways @(posedge pclk or negedge rst_n) beginif(!rst_n)begindata_flag <= 1'b0;ov5640_din_r <= 8'd0;ov5640_dout_r <= 16'd0;endelse if(hsync)begindata_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;endelse begindata_flag <= 1'b0;ov5640_din_r <= 8'd0;ov5640_dout_r <= ov5640_dout_r;endendalways @(posedge pclk or negedge rst_n) beginif(!rst_n)data_flag_r <= 1'b0;elsedata_flag_r <= data_flag;endassign 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     ,   			//输入系统时钟,100MHzinput		        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设备的串行时钟信号sclinout   wire		sda         				//输出至i2c设备的串行数据信号sda
);		//状态机定义		
localparam	IDLE 			= 4'd0,					//初始化状态START1			= 4'd1,					//发送开始信号状态1SEND_D_ADDR_W 	= 4'd2,					//设备地址写入状态 + 控制写ACK1			= 4'd3,					//等待从机响应信号1SEND_R_ADDR_H	= 4'd4,					//发送寄存器地址高8位ACK2			= 4'd5,					//等待从机响应信号2SEND_R_ADDR_L	= 4'd6,					//发送寄存器地址低8位ACK3			= 4'd7,					//等待从机响应信号3WR_DATA         = 4'd08,  				//写数据状态ACK4            = 4'd09,  				//应答状态4START2          = 4'd10,  				//发送开始信号状态12SEND_D_ADDR_R   = 4'd11,  				//设备地址写入状态 + 控制读ACK5            = 4'd12,  				//应答状态5RD_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)beginif(~sys_rst_n)begini2c_clk <= 1'b0;clk_cnt <= 10'd0;endelse if(clk_cnt == CLK_DIVIDE - 1'b1)begini2c_clk <= ~i2c_clk;clk_cnt <= 10'd0;		endelse begini2c_clk <= i2c_clk;clk_cnt <= clk_cnt + 1'd1;	end
end//i2c_clk计数器使能
always@(posedge i2c_clk or negedge sys_rst_n)beginif(!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)beginif(!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)beginif(~sys_rst_n)cur_state <= IDLE;elsecur_state <= next_state;
end//三段式状态机第二段
always@(*)beginnext_state = IDLE;case(cur_state)IDLE:if(i2c_start)next_state = START1;					//接收到开始信号,跳转到发送起始信号状态elsenext_state = IDLE;START1:if(i2c_clk_cnt == 2'd3)						//i2c_clk 计数到最大值3,跳转到发送器件地址+写标志位状态next_state = SEND_D_ADDR_W;elsenext_state = START1;SEND_D_ADDR_W:if(i2c_clk_cnt == 2'd3 && bit_cnt == 3'd7)	//发送了8位地址后跳转到从机响应状态next_state = ACK1;	elsenext_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;	elsenext_state = ACK2;SEND_R_ADDR_L:if(i2c_clk_cnt == 2'd3 && bit_cnt == 3'd7)	//发送了寄存器低8位地址后跳转到从机响应状态next_state = ACK3;elsenext_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;				//跳转到写数据状态endelse if(i2c_clk_cnt == 2'd3)				next_state = IDLE;						//响应无效或者响应不及时则跳转回初始状态elsenext_state = ACK3;START2:if(i2c_clk_cnt == 2'd3)						next_state = SEND_D_ADDR_R;				//第二次发送起始信号后跳转到发送器件地址+读标志位状态elsenext_state = START2;SEND_D_ADDR_R:if(i2c_clk_cnt == 2'd3 && bit_cnt == 3'd7)	//发送完了8位地址后跳转到从机响应状态next_state = ACK5;	elsenext_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;elsenext_state = RD_DATA;		NACK:if(i2c_clk_cnt == 2'd3)						next_state = STOP;						//发送完了非响应信号后跳转到发送结束信号状态elsenext_state = NACK;						WR_DATA:if(bit_cnt == 3'd7 && i2c_clk_cnt == 2'd3)	next_state = ACK4;						//写完了8位数据后跳转到从机响应状态elsenext_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;						//响应无效或者响应不及时则跳转回初始状态elsenext_state = ACK4;STOP:if(bit_cnt == 2'd3 && i2c_clk_cnt == 2'd3)	//结束信号发送完毕(这里还预留了2个周期)跳转到初始状态,等待下一次传输开始信号next_state = IDLE;elsenext_state = STOP;		default:next_state = IDLE;endcase
end//三段式状态机第三段
always@(posedge i2c_clk or negedge sys_rst_n)beginif(~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;endelse begini2c_end <= 1'b0;case(cur_state)IDLE:beginsda_en <= 1'b1;						//控制总线sda_out <= 1'b1;					//拉高总线end	START1:beginif(i2c_clk_cnt == 2'd3)begin	//发送完了开始信号if(addr_w[7])begin			//如果器件地址的最高位为1则提前拉高总线sda_en <= 1'b1;sda_out <= 1'b1;													endelse begin					//如果器件地址的最高位为0则提前拉低总线sda_en <= 1'b1;sda_out <= 1'b0;												endendelse begin						//还没发送完开始信号则保持低电平sda_en <= 1'b1;sda_out <= 1'b0;					endend	SEND_D_ADDR_W:beginif(bit_cnt == 3'd7)begin			if(i2c_clk_cnt == 2'd3)begin	//发送了8个数据(器件地址+写标志位)bit_cnt <= 3'd0;			//发送数据计数器清零sda_en <= 1'b0;				//释放总线endendelse if(i2c_clk_cnt == 2'd3)begin	//发送完了一个数据bit_cnt <= bit_cnt + 1'd1;		//发送数据计数器清零sda_en <= 1'b1;					//控制总线sda_out <= addr_w[6-bit_cnt];	//总线依次串行输出地址endendACK1:beginif(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;	endelse begin					//如果器件地址的16位为0则提前拉低总线sda_en <= 1'b1;sda_out <= 1'b0;	endendelse begin						//如果器件地址为8位if(i2c_addr[7])begin		//如果器件地址的8位为1则提前拉高总线sda_en <= 1'b1;sda_out <= 1'b1;	endelse begin					//如果器件地址的8位为0则提前拉低总线sda_en <= 1'b1;sda_out <= 1'b0;	endend			end endSEND_R_ADDR_H:beginif(bit_cnt == 3'd7)begin			//8个数据发送完了if(i2c_clk_cnt == 2'd3)beginbit_cnt <= 3'd0;			//发送数据计数器清零sda_en <= 1'b0;				//释放总线endendelse if(i2c_clk_cnt == 2'd3)beginbit_cnt <= bit_cnt + 1'd1;		//发送数据计数器清零sda_en <= 1'b1;                 //控制总线sda_out <= i2c_addr[14-bit_cnt];//总线依次串行输出地址end			endACK2:beginif(i2c_clk_cnt == 2'd3)beginif(i2c_addr[7])begin			//下一个要发送数据的首个数据为高则提前拉高总线sda_en <= 1'b1;sda_out <= 1'b1;													endelse begin						//下一个要发送数据的首个数据为低则提前拉低总线sda_en <= 1'b1;sda_out <= 1'b0;												end				endend	SEND_R_ADDR_L:begin	if(bit_cnt == 3'd7)begin				//8个数据发送完了if(i2c_clk_cnt == 2'd3)beginbit_cnt <= 3'd0;				//发送数据计数器清零sda_en <= 1'b0;					//释放总线endendelse if(i2c_clk_cnt == 2'd3)beginbit_cnt <= bit_cnt + 1'd1;		//发送数据计数器清零sda_en <= 1'b1;                 //控制总线sda_out <= i2c_addr[6-bit_cnt]; //总线依次串行输出地址end			endACK3:beginif(!i2c_rw)begin					//是写操作if(i2c_clk_cnt == 2'd3)beginif(i2c_data_w[7])begin		//下一个要发送数据的首个数据为高则提前拉高总线sda_en <= 1'b1;sda_out <= 1'b1;													endelse begin					//下一个要发送数据的首个数据为低则提前拉低总线sda_en <= 1'b1;sda_out <= 1'b0;												end				end endelse begin							//是读操作if(i2c_clk_cnt == 2'd3)begin	//提前拉高总线进入再次发送起始信号状态sda_en <= 1'b1;sda_out <= 1'b1;								endelse beginsda_en <= 1'b1;sda_out <= 1'b0;												end				endendSTART2:beginif(i2c_clk_cnt == 2'd1)begin		//拉低总线sda_en <= 1'b1;sda_out <= 1'b0;						endelse if(i2c_clk_cnt == 2'd3)beginif(addr_r[7])begin				//下一个要发送数据的首个数据为高则提前拉高总线sda_en <= 1'b1;sda_out <= 1'b1;													endelse begin						//下一个要发送数据的首个数据为低则提前拉低总线sda_en <= 1'b1;sda_out <= 1'b0;												end						endendSEND_D_ADDR_R:beginif(bit_cnt == 3'd7)begin				//8个数据发送完了if(i2c_clk_cnt == 2'd3)beginbit_cnt <= 3'd0;				//发送数据计数器清零sda_en <= 1'b0;					//释放总线endendelse if(i2c_clk_cnt == 2'd3)beginbit_cnt <= bit_cnt + 1'd1;		//发送数据计数器清零sda_en <= 1'b1;                 //控制总线sda_out <= addr_r[6-bit_cnt];   //总线依次串行输出地址endend		ACK5:sda_en <= 1'b0;							//下一个状态是接收数据,所以释放总线RD_DATA:if(i2c_clk_cnt == 2'd3)beginif(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;	//将读取的数据输出						endelse begin							//数据还未接收完毕		bit_cnt <= bit_cnt + 3'd1;				end				endelse 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)beginbit_cnt <= 3'd0;				//发送数据计数器清零sda_en <= 1'b0;					//释放总线endendelse 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)beginsda_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;																									endelse if( i2c_clk_cnt == 2'd3 )beginif(bit_cnt == 2'd3)begin					bit_cnt <= 2'd0;i2c_end <= 1'b1;				//发送完了终止信号且延时一段时间发送IIC结束信号endelsebit_cnt <= bit_cnt + 1'd1;enddefault:;endcaseend
end//i2c时钟生成
always@(posedge i2c_clk or negedge sys_rst_n)beginif(~sys_rst_n)scl <= 1'b1;else if(cur_state != STOP)beginif(i2c_clk_cnt == 2'd2)scl <= 1'b0;else if(i2c_clk_cnt == 2'd0)scl <= 1'b1;	endelse scl <= 1'b1;
end
//从机响应信号标志
always@(posedge i2c_clk or negedge sys_rst_n)beginif(~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
endendmodule
  1. 6.仿真结果

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

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

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

7.问题总结

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

相关文章:

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

1.OV5640简介 OV5640是OV&#xff08;OmniVision&#xff09;公司推出的一款CMOS图像传感器&#xff0c;实际感光阵列为&#xff1a;2592 x 1944&#xff08;即500w像素&#xff09;&#xff0c;该传感器内部集成了图像出炉的电路&#xff0c;包括自动曝光控制&#xff08;AEC…...

CDN ❀ Http协议标准缓存字段梳理

文章目录 1. 背景介绍2. 测试环境搭建3. 缓存字段3.1 Expires3.2 Cache-Control3.3 协商缓存 1. 背景介绍 Http协议标准有RFC定义好的请求和响应头部字段用于进行缓存设置&#xff0c;本文主要进行介绍缓存功能相关的头部字段及其使用方法。在使用CDN功能是&#xff0c;协议标…...

浅谈NODE的NPM命令和合约测试开发工具HARDHAT

$ npm install yarn -g # 将模块yarn全局安装 $ npm install moduleName # 安装模块到项目目录下 默认跟加参数 --save 一样 会在package文件的dependencies节点写入依赖。 $ npm install -g moduleName # -g 的意思是将模块安装到全局&#xff0c;具体安装到磁盘哪个位置&…...

k8s-pod 实战六 (如何在不同的部署环境中调整startupprobe的参数?)

在不同的部署环境中(如开发、测试、生产环境),你可能希望对 startupProbe 的参数进行调整,以适应不同的需求和条件。以下是几种常见的方法和实践: 方法一:使用 Kustomize 1. 目录结构 假设你的项目目录结构如下: my-app/ ├── base/ │ └── deployment.yaml …...

和服务端系统的通信

首先web网站 前端浏览器 和 后端系统 是通过HTTP协议进行通信的 同步请求&异步请求&#xff1a; 同步请求&#xff1a;可以从浏览器中直接获取的&#xff08;HTML/CSS/JS这样的静态文件资源)&#xff0c;这种获取请求的http称为同步请求 异步请求&#xff1a;js代码需要到服…...

python 实现perfect square完全平方数算法

python 实现perfect square完全平方数算法介绍 完全平方数&#xff08;Perfect Square&#xff09;是一个整数&#xff0c;它可以表示为某个整数的平方。例如&#xff0c;1,4,9,16,25,… 都是完全平方数&#xff0c;因为 1 1 2 , 4 2 2 , 9 3 2 11^2,42^2,93^2 112,422,93…...

【漏洞复现】某客圈子社区小程序审计(0day)

0x00 前言 █ 纸上得来终觉浅,绝知此事要躬行 █ Fofa:"/static/index/js/jweixin-1.2.0.js"该程序使用ThinkPHP 6.0.12作为框架,所以直接审计控制器即可.其Thinkphp版本较高,SQL注入不太可能,所以直接寻找其他洞. 0x01 前台任意文件读取+SSRF 在 /app/api/c…...

信息安全数学基础(1)整除的概念

前言 在信息安全数学基础中&#xff0c;整除是一个基础且重要的概念。它涉及整数之间的特定关系&#xff0c;对于理解数论、密码学等领域至关重要。以下是对整除概念的详细阐述&#xff1a; 一、定义 设a, b是任意两个整数&#xff0c;其中b ≠ 0。如果存在一个整数q&#xff0…...

SearchGPT与谷歌:早期分析及用户反馈

光年AI系统&#xff0c;作为先进AI技术的成果&#xff0c;推出了一个AI驱动搜素引擎的原型&#xff0c;类似于SearchGPT。 该发布引起了广泛的关注&#xff0c;并引发了关于其是否有能力与Google竞争的讨论。 然而&#xff0c;早期的研究和用户反馈表明&#xff0c;虽然Searc…...

VUE饿了么UPload组件自定义上传

代码&#xff1a; 1.视图&#xff1a; <el-dialog :title"dialogTitle" width"30%" :visible.sync"dialogFormVisible" :destroy-on-close"true"><el-form ref"fileForm" class"items-align" ><e…...

2.1概率统计的世界

欢迎来到概率统计的世界&#xff01;在量化交易中&#xff0c;概率统计是至关重要的工具。通过理解概率&#xff0c;我们可以用数学的方法来描述市场行为&#xff0c;预测未来走势&#xff0c;并制定交易策略。让我们一起从基础概念开始&#xff0c;逐步深入&#xff0c;揭开概…...

SpringBoot使用QQ邮箱发送邮件

1.开启POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务 设置 -> 账号 -> POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务 获取授权码 SpringBoot依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter&l…...

使用 OpenCV 和 NumPy 进行图像处理:HSV 范围筛选实现PS抠图效果

使用 OpenCV 和 NumPy 进行图像处理&#xff1a;HSV 范围筛选实现PS抠图效果 在计算机视觉和图像处理领域&#xff0c;OpenCV 是一个非常强大的库&#xff0c;能够帮助我们执行各种图像操作。在这篇博客中&#xff0c;我们将通过一个简单的示例演示如何使用 OpenCV 和 NumPy 来…...

IIS中间件

中间件 中间件是一类软件&#xff0c;为应用程序、服务和组件提供一个通用的服务层。 主要功能 通信&#xff1a;提供通信框架&#xff0c;帮助不同系统与应用之间进行数据交换和通信 事务管理、资源管理 安全服务&#xff1a;提供认证、授权、加密等安全策略 数据访问&a…...

BMP280气压传感器详解(STM32)

目录 一、介绍 二、传感器原理 1.原理图 2.引脚描述 3.传感器数据获取流程 三、程序设计 main.c文件 bmp280.h文件 bmp280.c文件 四、实验效果 五、资料获取 项目分享 一、介绍 BMP280是一款基于博世公司APSM工艺的小封装低功耗数字复合传感器&#xff0c;它可以测…...

DWPD指标:为何不再适用于大容量SSD?

固态硬盘&#xff08;Solid State Drives, SSD&#xff09;作为计算机行业中最具革命性的技术之一&#xff0c;凭借其更快的读写速度、增强的耐用性和能效&#xff0c;已经成为大多数用户的首选存储方案。然而&#xff0c;如同任何其他技术一样&#xff0c;SSD也面临自身的挑战…...

路由器的固定ip地址是啥意思?固定ip地址有什么好处

‌在当今数字化时代&#xff0c;‌路由器作为连接互联网的重要设备&#xff0c;‌扮演着举足轻重的角色。‌其中&#xff0c;‌路由器的固定IP地址是一个常被提及但可能让人困惑的概念。‌下面跟着虎观代理小二一起将深入探讨路由器的固定IP地址的含义&#xff0c;‌揭示其背后…...

Java——踩坑Arrays.asList()

坑1&#xff1a;不能直接使用 Arrsys.asList() 来转换基本类型数据 public static void test1(){// 1、不能直接使用asList来转换基本类型数组int[] arr {1, 2, 3};List list Arrays.asList(arr);System.out.printf("list:%s size:%s class:%s", list, list.size(…...

前缀列表(ip-prefix)配置

一. 实验简介 本来前缀列表是要和访问控制列表放在一起讲的&#xff0c;但是这里单拎出来是为了更详细的讲解两者的区别 1.前缀列表针对IP比访问控制更加灵活。 2.前缀列表在后面被引用时是无法对数据包进行过滤的 实验拓扑 二. 实验目的 R4路由器中只引入子网LoopBack的…...

每日OJ_牛客_电话号码(简单哈希模拟)

目录 牛客_电话号码&#xff08;简单哈希模拟&#xff09; 解析代码 牛客_电话号码&#xff08;简单哈希模拟&#xff09; 电话号码__牛客网 解析代码 #include <iostream> #include <unordered_map> #include <set> #include <string> using name…...

CMake基础:构建流程详解

目录 1.CMake构建过程的基本流程 2.CMake构建的具体步骤 2.1.创建构建目录 2.2.使用 CMake 生成构建文件 2.3.编译和构建 2.4.清理构建文件 2.5.重新配置和构建 3.跨平台构建示例 4.工具链与交叉编译 5.CMake构建后的项目结构解析 5.1.CMake构建后的目录结构 5.2.构…...

Go 语言接口详解

Go 语言接口详解 核心概念 接口定义 在 Go 语言中&#xff0c;接口是一种抽象类型&#xff0c;它定义了一组方法的集合&#xff1a; // 定义接口 type Shape interface {Area() float64Perimeter() float64 } 接口实现 Go 接口的实现是隐式的&#xff1a; // 矩形结构体…...

Ascend NPU上适配Step-Audio模型

1 概述 1.1 简述 Step-Audio 是业界首个集语音理解与生成控制一体化的产品级开源实时语音对话系统&#xff0c;支持多语言对话&#xff08;如 中文&#xff0c;英文&#xff0c;日语&#xff09;&#xff0c;语音情感&#xff08;如 开心&#xff0c;悲伤&#xff09;&#x…...

selenium学习实战【Python爬虫】

selenium学习实战【Python爬虫】 文章目录 selenium学习实战【Python爬虫】一、声明二、学习目标三、安装依赖3.1 安装selenium库3.2 安装浏览器驱动3.2.1 查看Edge版本3.2.2 驱动安装 四、代码讲解4.1 配置浏览器4.2 加载更多4.3 寻找内容4.4 完整代码 五、报告文件爬取5.1 提…...

AI,如何重构理解、匹配与决策?

AI 时代&#xff0c;我们如何理解消费&#xff1f; 作者&#xff5c;王彬 封面&#xff5c;Unplash 人们通过信息理解世界。 曾几何时&#xff0c;PC 与移动互联网重塑了人们的购物路径&#xff1a;信息变得唾手可得&#xff0c;商品决策变得高度依赖内容。 但 AI 时代的来…...

JS手写代码篇----使用Promise封装AJAX请求

15、使用Promise封装AJAX请求 promise就有reject和resolve了&#xff0c;就不必写成功和失败的回调函数了 const BASEURL ./手写ajax/test.jsonfunction promiseAjax() {return new Promise((resolve, reject) > {const xhr new XMLHttpRequest();xhr.open("get&quo…...

Windows安装Miniconda

一、下载 https://www.anaconda.com/download/success 二、安装 三、配置镜像源 Anaconda/Miniconda pip 配置清华镜像源_anaconda配置清华源-CSDN博客 四、常用操作命令 Anaconda/Miniconda 基本操作命令_miniconda创建环境命令-CSDN博客...

【Android】Android 开发 ADB 常用指令

查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...

Unity UGUI Button事件流程

场景结构 测试代码 public class TestBtn : MonoBehaviour {void Start(){var btn GetComponent<Button>();btn.onClick.AddListener(OnClick);}private void OnClick(){Debug.Log("666");}}当添加事件时 // 实例化一个ButtonClickedEvent的事件 [Formerl…...

掌握 HTTP 请求:理解 cURL GET 语法

cURL 是一个强大的命令行工具&#xff0c;用于发送 HTTP 请求和与 Web 服务器交互。在 Web 开发和测试中&#xff0c;cURL 经常用于发送 GET 请求来获取服务器资源。本文将详细介绍 cURL GET 请求的语法和使用方法。 一、cURL 基本概念 cURL 是 "Client URL" 的缩写…...