s5k4ba摄像头驱动分析
注释:
本驱动是基于S5PV310的,但是全天下的摄像头驱动都是采用V4L2,因此驱动框架流程基本差不多。其中fimc_init_camera()函数会回调.init函数,该函数主要就是通过IIC总线来初始化摄像头模块寄存器,初始化该寄存器一般是通过写数组(由iic中的地址和数据构成)完成,该数组一般会被芯片原厂提供,如需弄清楚数组做了什么?只需要对照芯片(如OV9650)后面的寄存器表一看即可知道。
参考文章:
http://www.eefocus.com/ayayayaya/blog/11-12/236557_09645.html
S5PV310摄像头通信功能模块图:
Camera控制器,叫FIMC(Fully Interactive Mobile Camera)。
FIMC这个模块不仅仅是一个摄像头的控制接口,它还承担着V4L2的output功能和overlay的功能。
组织关系如下:
可以看到,FIMC的驱动实现了V4L2所有的接口,可以分为v4l2-input设备接口,v4l2-output设备接口以及v4l2-overlay设备接口。这里我们主要关注v4l2-input设备接口,因为摄像头属于视频输入设备。fim
c_v4l2.c里面注册了很多的回调函数,都是用于实现V4L2的标准接口的,但是这些回调函数基本上都不是在fimc_v4l2.c里面实现的,而是在相应的.c中分别去实现。比如:
v4l2-input设备的操作实现: fimc_capture.c
v4l2-output设备的操作实现: fimc_output.c
v4l2-overlay设备的操作实现: fimc_overlay.c
这些代码其实都是和具体硬件操作无关的,这个驱动把所有操作硬件寄存器的代码都写到一个文件里面了,就是fimc40_regs.c。这样把硬件相关的代码和硬件无关的代码分开来实现是非常好的方式,可以最大限度的实现代码复用。
总结:
摄像头驱动我们可以假设分为两种驱动:
1).Camera控制器,即FIMC驱动;
2).摄像头的驱动(如s5k4ba.c、ov9650.c)
它们通过v4l2_subdev结构体联系起来。
驱动分析:
一,Camera控制器,即FIMC驱动:
drivers\media\video\samsung\fimc\fimc_dev.c // 入口驱动
1,设备层分析:
该层没有什么可以分析的了,不外乎就是些注册平台资源、设置平台数据什么的。
static struct resource s3c_fimc0_resource[] = {
[0] = {
.start = S5P_PA_FIMC0, // ctrl->regs = 0x11800000
.end = S5P_PA_FIMC0 + S5P_SZ_FIMC0 - 1,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = IRQ_FIMC0,
.end = IRQ_FIMC0,
.flags = IORESOURCE_IRQ,
},
};
2,驱动层分析:
(1).创建标准的V4L2接口:
static struct platform_driver fimc_driver = {
.probe = fimc_probe,
.remove = fimc_remove,
.driver = {
.name = FIMC_NAME,
.owner = THIS_MODULE,
},
};
static int fimc_register(void)
{
platform_driver_register(&fimc_driver);
}
static int __devinit fimc_probe(struct platform_device *pdev)
{
struct fimc_control *ctrl; // FIMC的主要数据结构
/*******************************************************************************************
获得一个ctrl并初始化,映射寄存器,注册中断,设置和使能时钟,重点关注该过程:
ctrl->vd = &fimc_video_device[id]; // 即设置V4L2的video_device结构体
*******************************************************************************************/
ctrl = fimc_register_controller(pdev); // 这里也分配内存(显存)了,共后面的fimc_reqbufs函数使用
if (pdata->cfg_gpio) // 设置相应的GPIO用于FIMC
pdata->cfg_gpio(pdev); // s3c_fimc0_cfg_gpio
ret = v4l2_device_register(&pdev->dev, &ctrl->v4l2_dev); // 干什么用的?!!
/* things to initialize once */
if (!fimc_dev->initialized) {
ret = fimc_init_global(pdev);
}
// 采用标准的A4L2注册了一个video_device结构体
ret = video_register_device(ctrl->vd, VFL_TYPE_GRABBER, ctrl->id);
}
经过上述过程的分析,我们便为摄像头驱动创建了一个标准的V4L2的接口函数集,如下:
各函数的作用是什么?参考V4L2官网:
http://linuxtv.org/downloads/v4l-dvb-apis/
const struct v4l2_ioctl_ops fimc_v4l2_ops = {
.vidioc_querycap = fimc_querycap, // 查询视频设备的功能
.vidioc_reqbufs = fimc_reqbufs, // 分配内存
.vidioc_querybuf = fimc_querybuf,
.vidioc_g_ctrl = fimc_g_ctrl,
.vidioc_s_ctrl = fimc_s_ctrl,
.vidioc_s_ext_ctrls = fimc_s_ext_ctrls,
.vidioc_cropcap = fimc_cropcap,
.vidioc_g_crop = fimc_g_crop,
.vidioc_s_crop = fimc_s_crop,
.vidioc_streamon = fimc_streamon,
.vidioc_streamoff = fimc_streamoff,
.vidioc_qbuf = fimc_qbuf,
.vidioc_dqbuf = fimc_dqbuf,
.vidioc_enum_fmt_vid_cap = fimc_enum_fmt_vid_capture,
.vidioc_g_fmt_vid_cap = fimc_g_fmt_vid_capture,
.vidioc_s_fmt_vid_cap = fimc_s_fmt_vid_capture,
.vidioc_try_fmt_vid_cap = fimc_try_fmt_vid_capture,
.vidioc_enum_input = fimc_enum_input,
.vidioc_g_input = fimc_g_input,
.vidioc_s_input = fimc_s_input, // 关键函数,深入理解
.vidioc_g_parm = fimc_g_parm,
.vidioc_s_parm = fimc_s_parm,
.vidioc_queryctrl = fimc_queryctrl,
.vidioc_querymenu = fimc_querymenu,
.vidioc_g_fmt_vid_out = fimc_g_fmt_vid_out,
.vidioc_s_fmt_vid_out = fimc_s_fmt_vid_out,
.vidioc_try_fmt_vid_out = fimc_try_fmt_vid_out,
.vidioc_g_fbuf = fimc_g_fbuf,
.vidioc_s_fbuf = fimc_s_fbuf,
.vidioc_try_fmt_vid_overlay = fimc_try_fmt_overlay,
.vidioc_g_fmt_vid_overlay = fimc_g_fmt_vid_overlay,
.vidioc_s_fmt_vid_overlay = fimc_s_fmt_vid_overlay,
.vidioc_enum_framesizes = fimc_enum_framesizes,
.vidioc_enum_frameintervals = fimc_enum_frameintervals,
};
static const struct v4l2_file_operations fimc_fops = {
.owner = THIS_MODULE,
.open = fimc_open,
.release = fimc_release,
.ioctl = video_ioctl2, // 最终会回调到ioctl_ops = &fimc_v4l2_ops。
.read = fimc_read,
.write = fimc_write,
.mmap = fimc_mmap,
.poll = fimc_poll,
};
struct video_device fimc_video_device[FIMC_DEVICES] = {
[0] = {
.fops = &fimc_fops,
.ioctl_ops = &fimc_v4l2_ops,
.release = fimc_vdev_release,
},
......
};
(2).应用程序调用的标准的V4L2接口进一步初始化和操作摄像头:
当点击android系统中的camera功能时,应用程序最先调用的就是open函数,具体实现过程如下:
(a.) app:open
----------------------------------------------------------------------------------------------------------------------------------------------------
对应的驱动定义:.open = fimc_open,
static int fimc_open(struct file *filp)
{
in_use = atomic_read(&ctrl->in_use); // 原子读操作,它返回原子类型的变量v的值
atomic_inc(&ctrl->in_use); // 原子类型的变量v做加1操作
if (in_use == 1) { // 初始化fimc_fbinfo结构体
ctrl->fb.open_fifo = s3cfb_open_fifo;
ctrl->fb.close_fifo = s3cfb_close_fifo;
ret = s3cfb_direct_ioctl(ctrl->id, S3CFB_GET_LCD_WIDTH, (unsigned long)&ctrl->fb.lcd_hres);
ret = s3cfb_direct_ioctl(ctrl->id, S3CFB_GET_LCD_HEIGHT, (unsigned long)&ctrl->fb.lcd_vres);
ctrl->mem.curr = ctrl->mem.base;
ctrl->status = FIMC_STREAMOFF;
}
}
(b.) app:ioctl VIDIOC_G_INPUT, VIDIOC_S_INPUT
-----------------------------------------------------------------------------------------------------------------------------------------------------
VIDIOC_G_INPUT, VIDIOC_S_INPUT用来查询和选则当前的视频输入。
对应的驱动定义:.vidioc_s_input = fimc_s_input
int fimc_s_input(struct file *file, void *fh, unsigned int i)
{
fimc_release_subdev(ctrl);
/* If ctrl->cam is not NULL, there is one subdev already registered.
* We need to unregister that subdev first. */
if (i != fimc->active_camera) {
ret = fimc_configure_subdev(ctrl);
}
}
static int fimc_configure_subdev(struct fimc_control *ctrl)
{
/*
* NOTE: first time subdev being registered,
* s_config is called and try to initialize subdev device
* but in this point, we are not giving MCLK and power to subdev
* so nothing happens but pass platform data through
*/
sd = v4l2_i2c_new_subdev_board(&ctrl->v4l2_dev, i2c_adap,
name, i2c_info, &addr);
}
struct v4l2_subdev *v4l2_i2c_new_subdev_board(struct v4l2_device *v4l2_dev,
struct i2c_adapter *adapter, const char *module_name,
struct i2c_board_info *info, const unsigned short *probe_addrs)
{
/*******************************************************************************************
1. 注册一个iic device层
当符合一定条件(参考iic驱动相关知识),调用iic driver层的probe函数(将在iic driver驱动,即纯摄像头相关驱动s5k4ba.c中分析)。
*******************************************************************************************/
if (info->addr == 0 && probe_addrs)
client = i2c_new_probed_device(adapter, info, probe_addrs);
else
client = i2c_new_device(adapter, info);
/*******************************************************************************************
2. Register with the v4l2_device which increases the module's use count as well.
*******************************************************************************************/
if (v4l2_device_register_subdev(v4l2_dev, sd))
/*******************************************************************************************
3. 调用回调函数.s_config (在纯摄像头相关驱动s5k4ba.c中设置,后面分析)
*******************************************************************************************/
if (sd) {
/* We return errors from v4l2_subdev_call only if we have the
callback as the .s_config is not mandatory */
int err = v4l2_subdev_call(sd, core, s_config, info->irq, info->platform_data);
}
}
问:上面分析的第1步,说会调用probe函数,那probe函数是怎么样的呢?
答:关键就是初始化一个v4l2_subdev_ops,供v4l2_subdev_call()回调,详情如下:
static int s5k4ba_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
/* Registering subdev */
v4l2_i2c_subdev_init(sd, client, &s5k4ba_ops); // 只有其中的v4l2_subdev_core_ops中的.init和.s_conf
// ig有用
}
问:上面分析的第3步,说通过v4l2_subdev_call()回调了函数.s_config,那函数.s_config是怎么样的呢?
答:详情如下:
static int s5k4ba_s_config(struct v4l2_subdev *sd, int irq, void *platform_data)
{
struct i2c_client *client = v4l2_get_subdevdata(sd); // 获得i2c_client,这便是引入v4l2_subdev的作用
// 摄像头一般既是一个I2C设备,也是一个V4L的 // 子设备,这样就把i2c和v4l2联系起来了。
struct s5k4ba_state *state = to_state(sd);
...... // 设置s5k4ba_state
}
(c.) app:ioctl VIDIOC_REQBUFS
-----------------------------------------------------------------------------------------------------------------------------------------------------
分配内存
.vidioc_reqbufs = fimc_reqbufs
static int fimc_reqbufs(struct file *filp, void *fh, struct v4l2_requestbuffers *b)
{
if (b->type == V4L2_BUF_TYPE_VIDEO_CAPTURE) {
ret = fimc_reqbufs_capture(ctrl, b); // 用于视频输入的分配内存 ?先执行
} else if (b->type == V4L2_BUF_TYPE_VIDEO_OUTPUT) {
ret = fimc_reqbufs_output(fh, b);
}
}
int fimc_reqbufs_capture(void *fh, struct v4l2_requestbuffers *b)
{
/* free previous buffers */
......
/* 重新设置buf_sequence,CIFCNTSEQn */
fimc_hw_reset_output_buf_sequence(ctrl); // Value 0 means ― skip frame buffer
for (i = 0; i < cap->nr_bufs; i++) {
fimc_hwset_output_buf_sequence(ctrl, i, 1);
}
bpp = fimc_fmt_depth(ctrl, &cap->fmt); // 计算颜色深度
/*******************************************************************************************
分配显存:
上文提到的fimc_register_controller()函数才是真正分配内存(显存)的地方!这里只是使用上面分配好了的内存,从而在这里形成了一种假的现象“分配显存”。
*******************************************************************************************/
ret = fimc_alloc_buffers(ctrl, 1, cap->fmt.width * cap->fmt.height, 0, bpp, 0);
}
注意:
1. c是分配内存相关的代码。目的是后面把该内存告诉CIOYSA1n寄存器,形成显存。
2. 使用VIDIOC_REQBUFS,我们可以获取v4l2_requestbuffers.count个缓存,下一步我们需要通过调用VIDIOC_QUERYBUF命令(把VIDIOC_REQBUFS中分配的数据缓存转换成物理地址)来获取这些缓存的物理地址,然后使用mmap函数把这些物理地址转换成应用程序中的绝对地址,最后把这段缓存放入缓存队列。
转换关系如图:
应用程序关键代码如下:
struct v4l2_buffer buf;
if (ioctl(fd, VIDIOC_QUERYBUF, &buf) == -1) // 读取缓存
buffers[numBufs].start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED,
fd, buf.m.offset); // 转换成用户空间的绝对地址,那么以后视频数据就保存在这段空间了
(d.) app:ioctl VIDIOC_STREAMON, VIDIOC_STREAMOFF
-----------------------------------------------------------------------------------------------------------------------------------------------------
开始视频显示函数、结束视频显示函数
.vidioc_streamon = fimc_streamon,
.vidioc_streamoff = fimc_streamoff,
static int fimc_streamon(struct file *filp, void *fh, enum v4l2_buf_type i)
{
if (i == V4L2_BUF_TYPE_VIDEO_CAPTURE) {
ret = fimc_streamon_capture(ctrl); // 用于视频输入 ?先执行
} else if (i == V4L2_BUF_TYPE_VIDEO_OUTPUT) {
ret = fimc_streamon_output(fh); // 用于视频输出 ?后执行
}
}
int fimc_streamon_capture(void *fh) // 用于视频输入 ? 重点!!!!
{
fimc_hwset_enable_irq(ctrl, 0, 1);
ret = fimc_init_camera(ctrl); // 设置了lcd显示的大小,并回调函数.init(即用iic初始化摄像头模块)
ret = subdev_call(ctrl, video, enum_framesizes, &cam_frmsize);
subdev_call(ctrl, video, s_stream, 0);
subdev_call(ctrl, video, s_stream, 1);
fimc_hwset_camera_type(ctrl); // CIGCTRLn,Selects ITU Camera A
fimc_hwset_camera_polarity(ctrl); // 设置极性:时钟、VSYNC、HREF、HSYNC
fimc_hwset_enable_lastend(ctrl); // CIOCTRLn,Specifies capture off timing control during the last
// camera capture
fimc_hwset_camera_source(ctrl); // CISRCFMTn,输入模式BT.601、摄像头输入水平大小、摄像头输入 // 垂直大小、像素输入顺序(即下面说的原始图片)
fimc_hwset_camera_offset(ctrl); // CIWDOFSTn(设置左、上偏移)、CIWDOFST2n(设置下、右偏移),设 // 置水平、垂直方向的偏移值。好比要将一张原始图片(摄像头采集 // 进来的图片)剪小,那么剪多少?就是这里设置。如下图:
fimc_capture_scaler_info(ctrl); // 纯软件相关的算法,即计算出缩放比例
fimc_hwset_prescaler(ctrl, &ctrl->sc); // CISCPRERATIOn、CISCPREDSTn,在预览模式下,设置目标图片 // 的大小、缩放比例
fimc_hwset_scaler(ctrl, &ctrl->sc); // CISCCTRLn,BYPASS、水平和垂直方向放大还是缩小、设置垂直缩 // 放比例、设置水平缩放比例、转换方法
fimc_hwset_output_colorspace(ctrl, cap->fmt.pixelformat); // CITRGFMTn,设置颜色输出格式
fimc_hwset_output_addr_style(ctrl, cap->fmt.pixelformat); // CIDMAPARAMn,Specifies the OUTPUT // DMA address access style
fimc_hwset_output_yuv(ctrl, cap->fmt.pixelformat); // CIOCTRLn,设置YUV的排版格式
fimc_hwset_output_area(ctrl, cap->fmt.width, cap->fmt.height); // CITAREAn,Specifies target area for // output DMA
fimc_hwset_output_scan(ctrl, &cap->fmt); // CISCCTRLn、CIOCTRLn
fimc_hwset_output_rot_flip(ctrl, cap->rotate, cap->flip); // CITRGFMTn,设置旋转
rot = fimc_mapping_rot_flip(cap->rotate, cap->flip);
fimc_hwset_org_output_size(ctrl, cap->fmt.width, cap->fmt.height); // Specifies the output DMA source // image horizontal and vertical pixel size,
// Selects ColorSpaceConversion equation
fimc_hwset_output_size(ctrl, cap->fmt.width, cap->fmt.height); // Specifies horizontal and vertical pixel // number of target image,CITRGFMTn
fimc_hwset_jpeg_mode(ctrl, false); // 是否是JPEG format,CIGCTRLn
fimc_hwset_output_address(ctrl, &cap->bufs[i], i); // 设置输出数据到哪里(显存)? CIOYSA1n寄存器
fimc_start_capture(ctrl); // CIIMGCPTn启动摄像头的捕获功能
}
int fimc_streamon_output(void *fh)
{
}
二,摄像头的驱动(如s5k4ba.c、ov9650.c):
驱动文件位置:drivers\media\video\S5k4ba.c
问:打开该驱动,发现没有入口函数,只有一个结构体的定义,如下:
static struct v4l2_i2c_driver_data v4l2_i2c_data = {
.name = S5K4BA_DRIVER_NAME,
.probe = s5k4ba_probe,
.remove = s5k4ba_remove,
.id_table = s5k4ba_id,
};
摄像头驱动,真的就不需要入口函数了吗?
答:答案是否定的,玄机在该驱动包含的头文件<media/v4l2-i2c-drv.h>中,内容如下:
struct v4l2_i2c_driver_data {
const char * const name;
int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);
int (*probe)(struct i2c_client *client, const struct i2c_device_id *id);
int (*remove)(struct i2c_client *client);
int (*suspend)(struct i2c_client *client, pm_message_t state);
int (*resume)(struct i2c_client *client);
const struct i2c_device_id *id_table;
};
static struct v4l2_i2c_driver_data v4l2_i2c_data;
static struct i2c_driver v4l2_i2c_driver;
/* Bus-based I2C implementation for kernels >= 2.6.26 */
static int __init v4l2_i2c_drv_init(void) // 入口函数
{
v4l2_i2c_driver.driver.name = v4l2_i2c_data.name;
v4l2_i2c_driver.command = v4l2_i2c_data.command;
v4l2_i2c_driver.probe = v4l2_i2c_data.probe;
v4l2_i2c_driver.remove = v4l2_i2c_data.remove;
v4l2_i2c_driver.suspend = v4l2_i2c_data.suspend;
v4l2_i2c_driver.resume = v4l2_i2c_data.resume;
v4l2_i2c_driver.id_table = v4l2_i2c_data.id_table;
return i2c_add_driver(&v4l2_i2c_driver);
}
static void __exit v4l2_i2c_drv_cleanup(void)
{
i2c_del_driver(&v4l2_i2c_driver);
}
module_init(v4l2_i2c_drv_init); // 入口修饰函数
module_exit(v4l2_i2c_drv_cleanup);
通过对该头文件的分析,发现该驱动完全符合iic驱动模型,即定义一个i2c_driver结构体,然后通过i2c_add_driver()函数把该结构体加入内核,当确实存在需要的iic设备的时候,则执行i2c_driver结构体中的probe函数。那么该结构体中的probe函数由谁传入呢?分析发现,i2c_driver结构体中的probe函数就是v4l2_i2c_driver_data结构体中的probe函数,因此函数的入口开始转入到probe函数,具体如下:
static int s5k4ba_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
/*该probe函数干了什么?何时被调用?在上面已经分析过了!!*/
......
/* Registering subdev */
v4l2_i2c_subdev_init(sd, client, &s5k4ba_ops);
}
s5k4ba_ops结构体中的成员又什么时候被调用呢?前面也已经分析过了(如通过v4l2_subdev_call()函数回调.init和.s_config等函数)。
打印信息(当初调试时加的,只是切取了其中的一次):
[ 150.395880] 1.==============================fimc_open
[ 150.395941] 2.==============================fimc_open
[ 150.400353] s3c-fimc0: FIMC0 1 opened.
[ 150.416095] 3.==============================fimc_open
[ 150.416209]
[ 150.416212]
[ 150.416215] fimc_s_input activating subdev
[ 150.460878] S5K4BA 0-002d: s5k4ba has been probed
[ 150.460945] S5K4BA 0-002d: fetching platform data
[ 150.464688] S5K4BA 0-002d: parallel mode
[ 150.470176] 1.==============================fimc_open
[ 150.475550] 2.==============================fimc_open
[ 150.479046] s3c-fimc2: FIMC2 1 opened.
[ 150.482573] 3.==============================fimc_open
[ 150.488533] 7.==============================fimc_open
[ 150.539469] S5K4BA 0-002d: s5k4ba_init: camera initialization start
[ 150.896049] s3c-fimc0: fimc_capture_scaler_info: CamOut (800, 600), TargetOut (640, 480)
[ 151.119261] 1.==============================fimc_open
[ 151.119323] 2.==============================fimc_open
[ 151.123983] s3c-fimc1: FIMC1 1 opened.
[ 151.127669] 3.==============================fimc_open
[ 151.140741] 1.==============================fimc_open
[ 151.140800] 2.==============================fimc_open
[ 151.145486] s3c-fimc1: FIMC1 2 opened.
[ 151.150853] 6.==============================fimc_open
[ 151.154181] 8.==============================fimc_open
注意:
写这些文档,纯属个人习惯。如果有什么问题,请大家及时指出。一起交流学习。
交流方法:
1.加入韦东三视频讨论群;
2.加我qq:317312379
3.关注我博客:http://blog.csdn.net/wanyong89