OV260简介
摄像头采用正点原子的OV2640模块,该模块采用OmniVision公司生产的OV2640图像传感器,分辨率最高为1600*1200,向下兼容多种分辨率(SXGA、SVGA 以及按比例缩小到从 SXGA 到 40*30 的任何尺寸),图像输出格式有JPEG 、RGB565、RGB555、YUV(422/420)等多种格式。
模块管脚
SDA和SCL管脚组成SCCB总线,用于控制OV2640的寄存器。
D0-D7为OV2640的图像数据输出管脚,通过PCLK、VSYN核HERF控制时序,PCLK下降沿时更新图像数据,PCLK为上升沿时读取数据,VSYNC输出的是帧时序,HERF输出的是行时序。
RST管脚控制模块的硬件复位,当RST为低电平时复位模块。
PWDN管脚控制模块的电源,当PWDN为低电平时,模块正常工作,当PWDN为高电平时,模块进入低功耗模式。
第三方IP核
在这个系统中需要使用第三方核自定义的IP核,所以要提前准备。
OV2640 IP核
与OV7725和OV5640相比,OV2640的资料相当少,也没有相关的例程和IP核可以直接使用,所以需要自己手动移植。
- 不难发现OV7725和OV5640采用的都是DVP并口传输,并且OV2640的管脚在一定程度上还兼容OV5640,所以可以直接将其代码移植过来。
module ov2640_capture_data(
input rst_n ,
input cam_pclk , //pclk输入
input cam_vsync , //vsync输入
input cam_href , //herf输入
input [7:0] cam_data , //像素数据输入
output cam_pwdn , //pwdn输出
//RGB888接口
output cmos_frame_clk,
output cmos_frame_ce,
output cmos_frame_vsync,
output cmos_frame_href ,
output cmos_frame_de ,
output [23:0] cmos_frame_data
);
//等待10帧之后才输出
localparam WAIT_FRAME = 4'd10 ;
reg rst_n_d0 =1;
reg rst_n_syn =1;
reg cam_vsync_d0 ;
reg cam_vsync_d1 ;
reg cam_href_d0 ;
reg cam_href_d1 ;
reg [3:0] cmos_ps_cnt ;
reg wait_done ;
reg byte_flag ;
reg [7:0] cam_data_d0 ;
reg [15:0] cmos_data_16b ;
reg byte_flag_d0 ;
//wire define
wire pos_vsync ;
assign pos_vsync = (~cam_vsync_d1) & cam_vsync_d0 ;
assign cam_pwdn = 1'b0;
assign cmos_frame_clk = cam_pclk;
assign cmos_frame_ce = wait_done ? (byte_flag_d0 & cmos_frame_href) || (!cmos_frame_href) : 1'b0;
assign cmos_frame_vsync = wait_done ? cam_vsync_d1 : 1'b0;
assign cmos_frame_href = wait_done ? cam_href_d1 : 1'b0;
assign cmos_frame_de = cmos_frame_href ;
assign cmos_frame_data = wait_done ?
{ cmos_data_16b[15:11],3'd0 , cmos_data_16b[10:5],2'd0 , cmos_data_16b[4:0],3'd0 }
: 24'd0;
always @(posedge cam_pclk or negedge rst_n) begin
if(!rst_n) begin
rst_n_d0 <= 1'b0;
rst_n_d0 <= 1'b0;
end
else begin
rst_n_d0 <= 1'b1;
rst_n_syn <= rst_n_d0;
end
end
always @(posedge cam_pclk or negedge rst_n_syn) begin
if(!rst_n_syn) begin
cam_vsync_d0 <= 1'b0;
cam_vsync_d1 <= 1'b0;
cam_href_d0 <= 1'b0;
cam_href_d1 <= 1'b0;
end
else begin
cam_vsync_d0 <= cam_vsync;
cam_vsync_d1 <= cam_vsync_d0;
cam_href_d0 <= cam_href;
cam_href_d1 <= cam_href_d0;
end
end
always @(posedge cam_pclk or negedge rst_n_syn) begin
if(!rst_n_syn)
cmos_ps_cnt <= 4'd0;
else if(pos_vsync && (cmos_ps_cnt < WAIT_FRAME))
cmos_ps_cnt <= cmos_ps_cnt + 4'd1;
end
always @(posedge cam_pclk or negedge rst_n_syn) begin
if(!rst_n_syn)
wait_done <= 1'b0;
else if((cmos_ps_cnt == WAIT_FRAME) && pos_vsync)
wait_done <= 1'b1;
end
always @(posedge cam_pclk or negedge rst_n_syn) begin
if(!rst_n_syn) begin
cmos_data_16b <= 16'd0;
cam_data_d0 <= 8'd0;
byte_flag <= 1'b0;
end
else if( cam_href ) begin
byte_flag <= ~byte_flag;
cam_data_d0 <= cam_data;
if(byte_flag)
cmos_data_16b <= {cam_data_d0,cam_data};
end
else begin
byte_flag <= 1'b0;
cam_data_d0 <= 8'b0;
end
end
always @(posedge cam_pclk or negedge rst_n_syn) begin
if(!rst_n_syn)
byte_flag_d0 <= 1'b0;
else
byte_flag_d0 <= byte_flag;
end
endmodule
可以参考正点原子的领航者的OV5640_HDMI实验,但是需要注意的是,这里将cam_rst_n接口移除,让PS端直接控制,因为不进行硬件复位的话,发现SCCB很难读取摄像头的ID,所以通过PS端控制,可以简单地控制复位时间。
这个模块是为了采集摄像头数据,并且将RGB565数据转换为RGB888数据,对于RGB565转RGB888,其实有两种方法:
- 末位补0,就和当前代码一致:
{R[5:0],{0,0,0},G[6:0],{0,0},B[5:0],{0,0,0}} - 用各自的末位补末位,即:
{R[5:0],R[2:0],G[6:0],R[1:0],B[5:0],B[2:0]}
- 管脚配置
这个配置可有可无,这里进行管脚配置的意思是将多个管脚组合成某一种类型的接口,这里组合成vid_io_rtl接口,将该接口所需的管脚与实际管脚相映射,当有其它有相同类型接口的IP核时,可以直接接口相连,接口下的实际管脚会自动相连,所以也可以不做这一步,只需记得这些管脚对应的映射关系,当遇到那些IP核时,自己手动将一个个管脚连接起来即可。
需要修改的就上述两个地方了,其余可以参考IP核封装的教程。
生成IP核即可。
HDMI驱动IP核
这个IP核可以直接去找别人写好的,正点原子的领航者源码中有提供,或者从这里下载。
这个IP核实现将RGB888数据转换为HDMI所需要的DVI编码数据,然后通过HDMI接口输出视频信号,需要注意的是,它无需实现时序控制。
因为我所使用的板子是使用PL端资源实现DVI编码的,所以需要这个IP核,有一些开发板使用ADV7511等芯片实现HDMI编码,所以那些就需要另外移植相应的驱动了。
block design设计
此时需要搞清楚自己需要什么IP核
ARM核
-
设置DDR内存,这个根据对应的硬件进行设置即可
-
找到Clock Configuration , 在PL Fabric Clocks 设置自己需要的时钟频率,设置50M时钟,然后增加FCLK_CLK1 150M(用于AXI4 HP高速接口用)
-
在PS-PL接口部分,增加 AXI HP 高速接口
-
(可选)增加对应的SD卡,只是为了后面的固化程序,可以不选,这个也建议参考对应的硬件设计
-
(可选)增加对应的UART,只是方便调试,建议选上。(我的板子的UART是连接到PL端的,所以只能使用EMIO)
-
引出三个EMIO,分别用于SCCB的SCL和SDA,以及被我从OV2640 IP核移除的cam_rst_n。
HDMI IP核
将上述HDMI驱动IP核(DVI编码器)添加进来,添加方法参考网上教程。
时钟 IP核
DVI编码需要两个时钟,正点原子为了兼容性,使用动态时钟的方法兼容更多的分辨率,需要引入另外一个第三方IP核,如果有需要建议下载其领航者源码查看,而此处已经明确是800*600分辨率,所以直接使用一个MMCM(PLL)模块即可,输出时钟为40M和200M,后者必须是前者的5倍,前者需要根据输出分辨率决定。
由于此时输入改为了FCLK0,所以时钟输入应该与此频率一致,但一般此模块都是接到板载晶振上。
video out IP核
由于图像数据在DDR内存中是以AXI数据流的形式存储,而DVI编码需要的是RGB888的数据,因此需要一个模块实现数据格式的转换。
唯一需要更改的是将将其时钟源独立出来,Clock Mode代表aclk与vid_io_out_clk两个时钟是否同源。因为HDMI 的显示和aclk的输入这里的时钟域并不同(HDMI显示的像素时钟和ACLK的数据流时钟不同),所以这里选择independent。让两个时钟相互独立。
Timing Mode:因为后面要用video timing controller 模块来提供视频时序,所以这里选择slave模式。
video timing controller IP核
由于DVI编码器没有实现时序控制,因此此处需要一个时序控制模块。
如果比较过正点原子的教程,这里会有差别,首先是这里没有勾选include AXI-Lite Interface以及这里需要将video mode选定为800x600p,这是因为正点原子采用了动态时钟的方法,在软件上检测和修改分辨率,然后通过AXI-Lite Interface将结果传输给这个IP核,然后这个IP核再根据传输的结果更改分辨率,由于我的时钟已经固定,不能在软件上进行更改,所以直接取消该接口,并且固定了时序。
VDMA模块
这个模块是负责搬运图像数据的,既可以将摄像头的图像数据搬运到DDR内存,也可以将DDR内存中的图像数据搬运出去,从而减轻ARM处理器的负担。
Frame Buffer是图像缓存帧数,大一点可以减轻图像撕裂。
Write channel和Read channel一个负责写入,一个负责读取,因此假如只是为了显示PS端的数据(从DDR读取数据进行显示),则只需要一个Read channel即可。因为我们显示的内容是RGB888,所以将系统默认的stream Data Width 从32位改成24位。 Line Buffer Depth默认512(只要AXI总线的速率大于显示的数据流的速率,缓存可以改更小),Read Burst Size 修改成64。
反相器
因为video out IP核的复位是高电平,与其它模块的低电平复位不一样,所以需要增加一个反相器。
video in IP核
与video out IP核相反,因为摄像头IP核产生的是RGB888的数据,需要一个模块将这些数据转换为AXI Stream数据,让VDMA可以进行搬运。
OV2640 IP核
直接增加进来即可,不需要修改,增加方式可以查看vivado添加自定义IP核的教程。
连线
这一步建议搞清楚为什么要这样连。
-
数据流连线
引出摄像头IP核的相应的接口,点击对应的管脚,选择Make External即可,如果OV2640的IP核封装处做了RGB接口的管脚映射,则此处可以直接将RGB接口与video in IP核的对应接口连接即可,否则需要展开管脚一一对应连接。
RGB888数据被Video in模块转换为AXI Stream数据后,交由VDMA模块将其搬运至内存之中,
图中的两根红线代表的就是VDMA模块读写DDR内存的数据通道,但是这两根线无需手动连接,当其它模块连接好之后,点击Run connection automation 就可以自动生成,现在只需要关注图中的绿线。
至此就大致演示了数据的连线了。
-
时钟连线
首先连接好摄像头IP核输出的帧时钟,让Video in 模块将每一帧都转换为AXI数据流。
这些aclk信号是用来同步的,并且都同属于高速的FCLK1(150M)
而这一部分则是低速的。
这个40M输出是给视频时序控制的。
这个200M是给DVI编码的。
-
复位连线
由于时钟模块的locked信号表示时钟模块是否稳定输出,所以以这个信号作为其它模块的复位信号,只有当时钟稳定输出之后,其它模块结束复位状态。
上述说了video out的复位信号是高电平复位,所以需要再与locked信号相连之前要经过一个反相器。 -
余下需要连线的信号
此时剩下的都是一些数据使能、时钟使能、时序控制信号了。
-
自动连线
点 Run connection automation 来自动连接剩下的走线,并且将一些必要的端口引出。 -
总结
-
生成代码
首先验证一下,点击validate design
验证通过后生成hdl代码,create hdl wrapper
管脚约束
这里有两个需要注意的事项,第一点是SCCB管脚的约束,需要对SDA管脚进行上拉配置,否则无法读取到摄像头的ID,也无法进行寄存器配置。
对于上图,记住你的管脚分配,因为[0]对应的是第54,[1]对应的是第55,[2]对应的是第56,在SDK编写C语言的时候需要用到,所以此处注意分配的管脚。
然后就是一个cam_pclk的时序约束,在对应的xdc文件里面填入下面的语句。将使用Make External将cam_pclk引出时的网络名字填入,我这里是cam_pclk_0。
create_clock -period 13.888 -name cam_pclk_0 [get_ports cam_pclk_0]
set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets cam_pclk_0_IBUF]
其它管脚的约束可以参考HDMI、UART的约束方法。
最后生成bit流,并生成硬件平台给SDK使用。
SDK工程
工程就是简单的空项目,然后新建一个
#include"emio_sccb_cfg.h"
#define GPIOPS_ID XPAR_XGPIOPS_0_DEVICE_ID //PS 端 GPIO 器件 ID
#define sccb_delay_utime 50
static XGpioPs gpiops_inst; //PS 端 GPIO 驱动实例
//EMIO初始化
void emio_init(void)
{
XGpioPs_Config *gpiops_cfg_ptr; //PS 端 GPIO 配置信息
//根据器件 ID 查找配置信息
gpiops_cfg_ptr = XGpioPs_LookupConfig(GPIOPS_ID);
//初始化器件驱动
XGpioPs_CfgInitialize(&gpiops_inst, gpiops_cfg_ptr, gpiops_cfg_ptr->BaseAddr);
//设置 sccb端口 为输出
XGpioPs_SetDirectionPin(&gpiops_inst, EMIO_SCL_NUM, 1);
XGpioPs_SetDirectionPin(&gpiops_inst, EMIO_SDA_NUM, 1);
//RST
XGpioPs_SetDirectionPin(&gpiops_inst, 56, 1);
XGpioPs_SetOutputEnablePin(&gpiops_inst, 56, 1);
XGpioPs_WritePin(&gpiops_inst, 56, 0);
usleep(200000);
//使能sccb端口 输出
XGpioPs_SetOutputEnablePin(&gpiops_inst, EMIO_SCL_NUM, 1);
XGpioPs_SetOutputEnablePin(&gpiops_inst, EMIO_SDA_NUM, 1);
//将sccb的SCLK和SDA都拉高
XGpioPs_WritePin(&gpiops_inst, EMIO_SCL_NUM, 1);
XGpioPs_WritePin(&gpiops_inst, EMIO_SDA_NUM, 1);
XGpioPs_WritePin(&gpiops_inst, 56, 1);
usleep(1000000);
}
//产生sccb起始信号
void sccb_start(void)
{
XGpioPs_WritePin(&gpiops_inst, EMIO_SCL_NUM, 1);
XGpioPs_WritePin(&gpiops_inst, EMIO_SDA_NUM, 1);
usleep(sccb_delay_utime);
XGpioPs_WritePin(&gpiops_inst, EMIO_SDA_NUM, 0); //START:when CLK is high,DATA change form high to low
usleep(sccb_delay_utime);
XGpioPs_WritePin(&gpiops_inst, EMIO_SCL_NUM, 0); //钳住I2C总线,准备发送或接收数据
}
//产生sccb停止信号
void sccb_stop(void)
{
XGpioPs_WritePin(&gpiops_inst, EMIO_SCL_NUM, 0);
XGpioPs_WritePin(&gpiops_inst,EMIO_SDA_NUM, 0); //STOP:when CLK is high DATA change form low to high
usleep(sccb_delay_utime);
XGpioPs_WritePin(&gpiops_inst, EMIO_SCL_NUM, 1);
usleep(sccb_delay_utime);
XGpioPs_WritePin(&gpiops_inst, EMIO_SDA_NUM, 1); //发送I2C总线结束信号
}
//sccb发送一个字节
void sccb_send_byte(u8 txd)
{
u8 t;
XGpioPs_WritePin(&gpiops_inst, EMIO_SCL_NUM, 0); //拉低时钟开始数据传输
for(t=0; t<8; t++)
{
XGpioPs_WritePin(&gpiops_inst, EMIO_SDA_NUM, (txd&0x80)>>7);
txd<<=1;
usleep(sccb_delay_utime);
XGpioPs_WritePin(&gpiops_inst, EMIO_SCL_NUM, 1);
usleep(sccb_delay_utime);
XGpioPs_WritePin(&gpiops_inst, EMIO_SCL_NUM, 0);
usleep(sccb_delay_utime);
}
}
//SCCB接收一个字节
u8 sccb_rece_byte(void)
{
unsigned char i=0 , rxd=0;
XGpioPs_SetOutputEnablePin(&gpiops_inst, EMIO_SDA_NUM, 0);
XGpioPs_SetDirectionPin(&gpiops_inst, EMIO_SDA_NUM, 0);
XGpioPs_WritePin(&gpiops_inst, EMIO_SCL_NUM, 0);
usleep(sccb_delay_utime);
for(i=0;i<8;i++ )
{
XGpioPs_WritePin(&gpiops_inst, EMIO_SCL_NUM, 1);
usleep(sccb_delay_utime);
rxd <<= 1;
if( XGpioPs_ReadPin(&gpiops_inst, EMIO_SDA_NUM) ) {
rxd = rxd | 0x01;
}
usleep(2);
XGpioPs_WritePin(&gpiops_inst, EMIO_SCL_NUM, 0);
usleep(sccb_delay_utime);
}
XGpioPs_SetDirectionPin(&gpiops_inst, EMIO_SDA_NUM, 1); //SDA设置为输出
XGpioPs_SetOutputEnablePin(&gpiops_inst, EMIO_SDA_NUM, 1);
return rxd;
}
//产生ACK应答
void sccb_ack(void)
{
XGpioPs_WritePin(&gpiops_inst, EMIO_SCL_NUM, 0);
XGpioPs_WritePin(&gpiops_inst, EMIO_SDA_NUM, 0);
usleep(sccb_delay_utime);
XGpioPs_WritePin(&gpiops_inst, EMIO_SCL_NUM, 1);
usleep(sccb_delay_utime);
XGpioPs_WritePin(&gpiops_inst, EMIO_SCL_NUM, 0);
usleep(sccb_delay_utime);
}
//SCCB写寄存器
void sccb_write_reg8(u8 addr , u8 data)
{
sccb_start();
sccb_send_byte(OV2640_DEV_ID);
sccb_ack();
sccb_send_byte(addr);
sccb_ack();
sccb_send_byte(data);
sccb_ack();
sccb_stop();
usleep(1000);
}
//SCCB读寄存器
u8 sccb_read_reg8(u8 addr )
{
u8 rxd;
sccb_start();
sccb_send_byte(OV2640_DEV_ID);
sccb_ack();
sccb_send_byte(addr);
sccb_ack();
sccb_stop();
sccb_start();
sccb_send_byte(OV2640_DEV_ID | 0x01);
sccb_ack();
rxd = sccb_rece_byte();
sccb_ack();
sccb_stop();
return rxd ;
}
对应的.h头文件
#include"xgpiops.h"
#include"sleep.h"
#include "xil_printf.h"
#ifndef sccb_EMIO_CFG_
#define sccb_EMIO_CFG_
#define EMIO_SCL_NUM 54//这里的数字与管脚约束有关
#define EMIO_SDA_NUM 55
#define OV2640_DEV_ID 0X60 //OV2640 SCCB器件地址
void emio_init(void);
void sccb_start(void);
void sccb_stop(void);
void sccb_ack(void);
void sccb_send_byte(u8 txd);
u8 sccb_rece_byte(void);
u8 sccb_read_reg8(u8 addr );
void sccb_write_reg8(u8 addr , u8 data);
#endif /* sccb_EMIO_CFG_ */
以上两个头文件实现了SCCB通信,以及对应的管脚初始化,其中也包含了RST管脚的初始化。
下面就是摄像头的初始化(ov2640_init.c):
#include "../emio_sccb_cfg/emio_sccb_cfg.h"
#include "../ov2640/ov2640_init.h"
#include "../ov2640/reg_init_cfg.h"
//OV7725初始化
u8 ov2640_init(void)
{
u16 cam_id = 0;
//读OV2640摄像头ID
cam_id = sccb_read_reg8(0X1c);
cam_id<<=8;
cam_id |= sccb_read_reg8(0x1d);
xil_printf("cam_id:%x\r\n",cam_id);
cam_id = sccb_read_reg8(0x0A);
cam_id<<=8;
cam_id |= sccb_read_reg8(0x0B);
xil_printf("cam_id:%x\r\n",cam_id);
if(cam_id != 0X2642) //获取到正确的OV2640 ID
return 1;
else{
reg_init();
return 0;
}
}
reg_init_cfg.c文件的内容,为什么会出现这个文件呢?因为这些寄存器配置文件是在OV2640软件手册里面直接复制出来的,所以为了方便,直接粘贴过来,然后使用define将对应的写寄存器函数替换为已有的。
#include "../ov2640/reg_init_cfg.h"
#define write_SCCB sccb_write_reg8
void reg_init(void)
{
write_SCCB(0xff, 0x01);
write_SCCB(0x12, 0x80);
usleep(5000);
write_SCCB(0xff, 0x00);
write_SCCB(0x2c, 0xff);
write_SCCB(0x2e, 0xdf);
write_SCCB(0xff, 0x01);
write_SCCB(0x3c, 0x32);
//
write_SCCB(0x11, 0x00);
write_SCCB(0x09, 0x02);
write_SCCB(0x04, 0xd8);
write_SCCB(0x13, 0xe5);
write_SCCB(0x14, 0x48);
write_SCCB(0x2c, 0x0c);
write_SCCB(0x33, 0x78);
write_SCCB(0x3a, 0x33);
write_SCCB(0x3b, 0xfB);
//
write_SCCB(0x3e, 0x00);
write_SCCB(0x43, 0x11);
write_SCCB(0x16, 0x10);
//
write_SCCB(0x39, 0x92);
//
write_SCCB(0x35, 0xda);
write_SCCB(0x22, 0x1a);
write_SCCB(0x37, 0xc3);
write_SCCB(0x23, 0x00);
write_SCCB(0x34, 0xc0);
write_SCCB(0x36, 0x1a);
write_SCCB(0x06, 0x88);
write_SCCB(0x07, 0xc0);
write_SCCB(0x0d, 0x87);
write_SCCB(0x0e, 0x41);
write_SCCB(0x4c, 0x00);
write_SCCB(0x48, 0x00);
write_SCCB(0x5B, 0x00);
write_SCCB(0x42, 0x03);
//
write_SCCB(0x4a, 0x81);
write_SCCB(0x21, 0x99);
//
write_SCCB(0x24, 0x40);
write_SCCB(0x25, 0x38);
write_SCCB(0x26, 0x82);
write_SCCB(0x5c, 0x00);
write_SCCB(0x63, 0x00);
write_SCCB(0x46, 0x22);
write_SCCB(0x0c, 0x3c);
//
write_SCCB(0x61, 0x70);
write_SCCB(0x62, 0x80);
write_SCCB(0x7c, 0x05);
//
write_SCCB(0x20, 0x80);
write_SCCB(0x28, 0x30);
write_SCCB(0x6c, 0x00);
write_SCCB(0x6d, 0x80);
write_SCCB(0x6e, 0x00);
write_SCCB(0x70, 0x02);
write_SCCB(0x71, 0x94);
write_SCCB(0x73, 0xc1);
//
write_SCCB(0x12, 0x40);
write_SCCB(0x17, 0x11);
write_SCCB(0x18, 0x43);
write_SCCB(0x19, 0x00);
write_SCCB(0x1a, 0x4b);
write_SCCB(0x32, 0x09);
write_SCCB(0x37, 0xc0);
write_SCCB(0x4f, 0xca);
write_SCCB(0x50, 0xa8);
write_SCCB(0x5a, 0x23);
write_SCCB(0x6d, 0x00);
write_SCCB(0x3d, 0x38);
//
write_SCCB(0xff, 0x00);
write_SCCB(0xe5, 0x7f);
write_SCCB(0xf9, 0xc0);
write_SCCB(0x41, 0x24);
write_SCCB(0xe0, 0x14);
write_SCCB(0x76, 0xff);
write_SCCB(0x33, 0xa0);
write_SCCB(0x42, 0x20);
write_SCCB(0x43, 0x18);
write_SCCB(0x4c, 0x00);
write_SCCB(0x87, 0xd5);
write_SCCB(0x88, 0x3f);
write_SCCB(0xd7, 0x03);
write_SCCB(0xd9, 0x10);
write_SCCB(0xd3, 0x82);
//
write_SCCB(0xc8, 0x08);
write_SCCB(0xc9, 0x80);
//色彩饱和度
write_SCCB(0x7c, 0x00);
write_SCCB(0x7d, 0x00);
write_SCCB(0x7c, 0x03);
write_SCCB(0x7d, 0x48);//此处原本是48
write_SCCB(0x7d, 0x28);
write_SCCB(0x7c, 0x08);
write_SCCB(0x7d, 0x20);
write_SCCB(0x7d, 0x10);
write_SCCB(0x7d, 0x0e);
//
write_SCCB(0x90, 0x00);
write_SCCB(0x91, 0x0e);
write_SCCB(0x91, 0x1a);
write_SCCB(0x91, 0x31);
write_SCCB(0x91, 0x5a);
write_SCCB(0x91, 0x69);
write_SCCB(0x91, 0x75);
write_SCCB(0x91, 0x7e);
write_SCCB(0x91, 0x88);
write_SCCB(0x91, 0x8f);
write_SCCB(0x91, 0x96);
write_SCCB(0x91, 0xa3);
write_SCCB(0x91, 0xaf);
write_SCCB(0x91, 0xc4);
write_SCCB(0x91, 0xd7);
write_SCCB(0x91, 0xe8);
write_SCCB(0x91, 0x20);
//
write_SCCB(0x92, 0x00);
write_SCCB(0x93, 0x06);
write_SCCB(0x93, 0xe3);
write_SCCB(0x93, 0x05);
write_SCCB(0x93, 0x05);
write_SCCB(0x93, 0x00);
write_SCCB(0x93, 0x04);
write_SCCB(0x93, 0x00);
write_SCCB(0x93, 0x00);
write_SCCB(0x93, 0x00);
write_SCCB(0x93, 0x00);
write_SCCB(0x93, 0x00);
write_SCCB(0x93, 0x00);
write_SCCB(0x93, 0x00);
//
write_SCCB(0x96, 0x00);
write_SCCB(0x97, 0x08);
write_SCCB(0x97, 0x19);
write_SCCB(0x97, 0x02);
write_SCCB(0x97, 0x0c);
write_SCCB(0x97, 0x24);
write_SCCB(0x97, 0x30);
write_SCCB(0x97, 0x28);
write_SCCB(0x97, 0x26);
write_SCCB(0x97, 0x02);
write_SCCB(0x97, 0x98);
write_SCCB(0x97, 0x80);
write_SCCB(0x97, 0x00);
write_SCCB(0x97, 0x00);
//
write_SCCB(0xc3, 0xed);
write_SCCB(0xa4, 0x00);
write_SCCB(0xa8, 0x00);
write_SCCB(0xc5, 0x11);
write_SCCB(0xc6, 0x51);
write_SCCB(0xbf, 0x80);
write_SCCB(0xc7, 0x10);
write_SCCB(0xb6, 0x66);
write_SCCB(0xb8, 0xA5);
write_SCCB(0xb7, 0x64);
write_SCCB(0xb9, 0x7C);
write_SCCB(0xb3, 0xaf);
write_SCCB(0xb4, 0x97);
write_SCCB(0xb5, 0xFF);
write_SCCB(0xb0, 0xC5);
write_SCCB(0xb1, 0x94);
write_SCCB(0xb2, 0x0f);
write_SCCB(0xc4, 0x5c);
//
write_SCCB(0xc0, 0x64);
write_SCCB(0xc1, 0x4B);
write_SCCB(0x8c, 0x00);
write_SCCB(0x86, 0x3D);
write_SCCB(0x50, 0x00);
write_SCCB(0x51, 0xC8);
write_SCCB(0x52, 0x96);
write_SCCB(0x53, 0x00);
write_SCCB(0x54, 0x00);
write_SCCB(0x55, 0x00);
write_SCCB(0x5a, 0xC8);
write_SCCB(0x5b, 0x96);
write_SCCB(0x5c, 0x00);
write_SCCB(0xd3, 0x82);
//
write_SCCB(0xc3, 0xed);
write_SCCB(0x7f, 0x00);
//
write_SCCB(0xda, 0x08);
//
write_SCCB(0xe5, 0x1f);
write_SCCB(0xe1, 0x67);
write_SCCB(0xe0, 0x00);
write_SCCB(0xdd, 0x7f);
write_SCCB(0x05, 0x00);
}
然后就是对应的两个.h文件了:
ov2640_init.h
#include "xil_types.h"
#include"sleep.h"
#ifndef _OV2640_INIT_H_
#define _OV2640_INIT_H_
u8 ov2640_init(void);
#endif /* OV5640_INIT_H_ */
reg_init_cfg.h
#ifndef SRC_OV2640_REG_INIT_CFG_H_
#define SRC_OV2640_REG_INIT_CFG_H_
#include "../emio_sccb_cfg/emio_sccb_cfg.h"
void reg_init(void);
#endif /* SRC_OV2640_REG_INIT_CFG_H_ */
然后就剩下main函数了
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "xil_types.h"
#include "xil_cache.h"
#include "xparameters.h"
#include "xaxivdma.h"
#include "xaxivdma_i.h"
#include "emio_sccb_cfg/emio_sccb_cfg.h"
#include "ov2640/ov2640_init.h"
#define VDMA_ID XPAR_AXIVDMA_0_DEVICE_ID //VDMA器件ID
unsigned int const frame_buffer_addr = (XPAR_PS7_DDR_0_S_AXI_BASEADDR + 0x1000000);
XAxiVdma vdma;
#define GPIOPS_ID XPAR_XGPIOPS_0_DEVICE_ID //PS 端 GPIO 器件 ID
#define WIDTH 800
#define DEPTH 600
int run_triple_frame_buffer(XAxiVdma* InstancePtr, int DeviceId, int hsize,
int vsize, int buf_base_addr, int number_frame_count,
int enable_frm_cnt_intr);
int main(void)
{
u32 status;
emio_init(); //初始化EMIO
status = ov2640_init(); //初始化ov7725
if(status == 0)
xil_printf("OV2640 detected successful!\r\n");
else
xil_printf("OV2640 detected failed!\r\n");
//配置VDMA
run_triple_frame_buffer(&vdma, VDMA_ID,WIDTH,DEPTH,frame_buffer_addr,0,0);
return 0;
}
需要注意的是run_triple_frame_buffer这个函数需要导入一个例程才能使用,导入方法:
找到对应的system.mss文件,双击。
选择对应的工程。
然后在工程管理里面找到vdma_api.c,将其复制到原来工程的src文件夹下面。
最后将刚刚导入的例程删除即可,因为只是需要其中一个vdma_api.c而已。
工程的文件结构如下:
由于main函数核ov2640初始化函数里面调用了xil_printf函数,建议开启一个串口给该函数进行输出,否则建议删除相关语句。
工程下载
记得设置RUN AS的配置,在烧录的时候初始化FPGA部分。
总结
- 如果遇到SCCB无法读取摄像头ID的情况,记得检查对应的管脚约束,是否上拉了SDA管脚、是否设置对了管脚。
- 如果综合时对xdc报错,可以检查管脚名字是否正确,以及记得及时将没有的、多的管脚移除,还有加上cam_pclk的时序约束,需要注意的是,只要填入给出的两条语句即可,有时候vivado会自己生成多一条相似的语句,如果造成报错,请认真仔细检查。
- 正点原子对于OV7725和OV5640的硬件初始化都不进行,直接给高电平,但是在我的测试中,OV2640如果不进行初始化就无法读取摄像头ID,所以才将复位管脚通过EMIO的方式输出。
- 需要注意摄像头的SCCB器件地址,如果摄像头ID读取失败,可以检查一下器件地址是否正确。
- 如果遇到画面偏某一种颜色,可以考虑使用ILA抓取对应的信号,看看是后级模块出错还是它就是这样的输入,从而定位问题。
- 没事少用OV2640,一个豪威科技自己都放弃的摄像头模组,咱们就少掺和了。