基于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 / 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
-
6.仿真结果
总体仿真如上图所示,可以看到每一帧图像传输完成,场同步信号拉高一次,并且前10帧图像会被舍弃。
摄像头数据传输仿真如上图所示,使能信号每翻转一次进行一次数据拼接。
寄存器配置如上图所示,不需要从机发送应答信号。
7.问题总结
本次代码还未进行板级验证,先说一说仿真遇到的问题,其他问题后续再进行补充,首先就是方针过程中出现了如下图所示的情况。行同步信号和场同步信号出现了未知态,原因是有两个驱动,我的testbench是自动生成的因此开始会将两个信号的值赋0,后面我再对其进行赋值就会出现未知态。还有就是寄存器的配置,在网上找了很多资料都没有一个确定的答案,可能文中的说法也会有错,欢迎大家批评指正,代码参考正点原子。
相关文章:

基于FPGA的OV5640摄像头图像采集
1.OV5640简介 OV5640是OV(OmniVision)公司推出的一款CMOS图像传感器,实际感光阵列为:2592 x 1944(即500w像素),该传感器内部集成了图像出炉的电路,包括自动曝光控制(AEC…...
CDN ❀ Http协议标准缓存字段梳理
文章目录 1. 背景介绍2. 测试环境搭建3. 缓存字段3.1 Expires3.2 Cache-Control3.3 协商缓存 1. 背景介绍 Http协议标准有RFC定义好的请求和响应头部字段用于进行缓存设置,本文主要进行介绍缓存功能相关的头部字段及其使用方法。在使用CDN功能是,协议标…...
浅谈NODE的NPM命令和合约测试开发工具HARDHAT
$ npm install yarn -g # 将模块yarn全局安装 $ npm install moduleName # 安装模块到项目目录下 默认跟加参数 --save 一样 会在package文件的dependencies节点写入依赖。 $ npm install -g moduleName # -g 的意思是将模块安装到全局,具体安装到磁盘哪个位置&…...
k8s-pod 实战六 (如何在不同的部署环境中调整startupprobe的参数?)
在不同的部署环境中(如开发、测试、生产环境),你可能希望对 startupProbe 的参数进行调整,以适应不同的需求和条件。以下是几种常见的方法和实践: 方法一:使用 Kustomize 1. 目录结构 假设你的项目目录结构如下: my-app/ ├── base/ │ └── deployment.yaml …...
和服务端系统的通信
首先web网站 前端浏览器 和 后端系统 是通过HTTP协议进行通信的 同步请求&异步请求: 同步请求:可以从浏览器中直接获取的(HTML/CSS/JS这样的静态文件资源),这种获取请求的http称为同步请求 异步请求:js代码需要到服…...
python 实现perfect square完全平方数算法
python 实现perfect square完全平方数算法介绍 完全平方数(Perfect Square)是一个整数,它可以表示为某个整数的平方。例如,1,4,9,16,25,… 都是完全平方数,因为 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)整除的概念
前言 在信息安全数学基础中,整除是一个基础且重要的概念。它涉及整数之间的特定关系,对于理解数论、密码学等领域至关重要。以下是对整除概念的详细阐述: 一、定义 设a, b是任意两个整数,其中b ≠ 0。如果存在一个整数q࿰…...
SearchGPT与谷歌:早期分析及用户反馈
光年AI系统,作为先进AI技术的成果,推出了一个AI驱动搜素引擎的原型,类似于SearchGPT。 该发布引起了广泛的关注,并引发了关于其是否有能力与Google竞争的讨论。 然而,早期的研究和用户反馈表明,虽然Searc…...
VUE饿了么UPload组件自定义上传
代码: 1.视图: <el-dialog :title"dialogTitle" width"30%" :visible.sync"dialogFormVisible" :destroy-on-close"true"><el-form ref"fileForm" class"items-align" ><e…...

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

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 进行图像处理:HSV 范围筛选实现PS抠图效果 在计算机视觉和图像处理领域,OpenCV 是一个非常强大的库,能够帮助我们执行各种图像操作。在这篇博客中,我们将通过一个简单的示例演示如何使用 OpenCV 和 NumPy 来…...
IIS中间件
中间件 中间件是一类软件,为应用程序、服务和组件提供一个通用的服务层。 主要功能 通信:提供通信框架,帮助不同系统与应用之间进行数据交换和通信 事务管理、资源管理 安全服务:提供认证、授权、加密等安全策略 数据访问&a…...

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

DWPD指标:为何不再适用于大容量SSD?
固态硬盘(Solid State Drives, SSD)作为计算机行业中最具革命性的技术之一,凭借其更快的读写速度、增强的耐用性和能效,已经成为大多数用户的首选存储方案。然而,如同任何其他技术一样,SSD也面临自身的挑战…...

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

Java——踩坑Arrays.asList()
坑1:不能直接使用 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)配置
一. 实验简介 本来前缀列表是要和访问控制列表放在一起讲的,但是这里单拎出来是为了更详细的讲解两者的区别 1.前缀列表针对IP比访问控制更加灵活。 2.前缀列表在后面被引用时是无法对数据包进行过滤的 实验拓扑 二. 实验目的 R4路由器中只引入子网LoopBack的…...

每日OJ_牛客_电话号码(简单哈希模拟)
目录 牛客_电话号码(简单哈希模拟) 解析代码 牛客_电话号码(简单哈希模拟) 电话号码__牛客网 解析代码 #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.构…...
基于服务器使用 apt 安装、配置 Nginx
🧾 一、查看可安装的 Nginx 版本 首先,你可以运行以下命令查看可用版本: apt-cache madison nginx-core输出示例: nginx-core | 1.18.0-6ubuntu14.6 | http://archive.ubuntu.com/ubuntu focal-updates/main amd64 Packages ng…...

Opencv中的addweighted函数
一.addweighted函数作用 addweighted()是OpenCV库中用于图像处理的函数,主要功能是将两个输入图像(尺寸和类型相同)按照指定的权重进行加权叠加(图像融合),并添加一个标量值&#x…...
条件运算符
C中的三目运算符(也称条件运算符,英文:ternary operator)是一种简洁的条件选择语句,语法如下: 条件表达式 ? 表达式1 : 表达式2• 如果“条件表达式”为true,则整个表达式的结果为“表达式1”…...

ElasticSearch搜索引擎之倒排索引及其底层算法
文章目录 一、搜索引擎1、什么是搜索引擎?2、搜索引擎的分类3、常用的搜索引擎4、搜索引擎的特点二、倒排索引1、简介2、为什么倒排索引不用B+树1.创建时间长,文件大。2.其次,树深,IO次数可怕。3.索引可能会失效。4.精准度差。三. 倒排索引四、算法1、Term Index的算法2、 …...

Ascend NPU上适配Step-Audio模型
1 概述 1.1 简述 Step-Audio 是业界首个集语音理解与生成控制一体化的产品级开源实时语音对话系统,支持多语言对话(如 中文,英文,日语),语音情感(如 开心,悲伤)&#x…...
拉力测试cuda pytorch 把 4070显卡拉满
import torch import timedef stress_test_gpu(matrix_size16384, duration300):"""对GPU进行压力测试,通过持续的矩阵乘法来最大化GPU利用率参数:matrix_size: 矩阵维度大小,增大可提高计算复杂度duration: 测试持续时间(秒&…...

ArcGIS Pro制作水平横向图例+多级标注
今天介绍下载ArcGIS Pro中如何设置水平横向图例。 之前我们介绍了ArcGIS的横向图例制作:ArcGIS横向、多列图例、顺序重排、符号居中、批量更改图例符号等等(ArcGIS出图图例8大技巧),那这次我们看看ArcGIS Pro如何更加快捷的操作。…...

分布式增量爬虫实现方案
之前我们在讨论的是分布式爬虫如何实现增量爬取。增量爬虫的目标是只爬取新产生或发生变化的页面,避免重复抓取,以节省资源和时间。 在分布式环境下,增量爬虫的实现需要考虑多个爬虫节点之间的协调和去重。 另一种思路:将增量判…...

如何更改默认 Crontab 编辑器 ?
在 Linux 领域中,crontab 是您可能经常遇到的一个术语。这个实用程序在类 unix 操作系统上可用,用于调度在预定义时间和间隔自动执行的任务。这对管理员和高级用户非常有益,允许他们自动执行各种系统任务。 编辑 Crontab 文件通常使用文本编…...