基于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,后面我再对其进行赋值就会出现未知态。还有就是寄存器的配置,在网上找了很多资料都没有一个确定的答案,可能文中的说法也会有错,欢迎大家批评指正,代码参考正点原子。