FS_S5PC100平台上Linux Camera驱动开发详解(一)

作者:刘洪涛,华清远见嵌入式学院讲师。

说明:
        理解摄像头驱动需要四个前提:
        1)摄像头基本的工作原理和S5PC100集成的Camera控制器的工作原理
        2)platform_device和platform_driver工作原理
        3)Linux内核V4L2驱动架构
        4)Linux内核I2C驱动架构

1. 摄像头工作原理

FS_S5PC100平台上Linux Camera驱动开发详解(一)_第1张图片

OV9650/9655是CMOS接口的图像传感器芯片,可以感知外部的视觉信号并将其转换为数字信号并输出。通过下面的框图可以清晰的看到它的工作原理:

我们需要通过XVCLK1给摄像头提供时钟,RESET是复位线,PWDN在摄像头工作时应该始终为低。HREF是行参考信号,PCLK是像素时钟,VSYNC是场同步信号。一旦给摄像头提供了时钟,并且复位摄像头,摄像头就开始工作了,通过HREF,PCLK和VSYNC同步传输数字图像信号。数据是通过D0~D7这八根数据线并行送出的。

OV9650向外传输的图像格式是YUV的格式,YUV是一种压缩后的图像数据格式,它里面还包含很多具体的格式类型,我们的摄像头对应的是YCbCr(8 bits, 4:2:2, Interpolated color).一定要搞清楚格式,后面的驱动里面设置的格式一定要和这个格式一致。

OV9650里面有很多寄存器需要配置,配置这些寄存器就需要通过芯片里面的SCCB总线去配置。SCCB其实是一种弱化的I2C总线。我们可以直接把摄像头接在S5PC100的I2C控制器上,利用I2C总线去读写寄存器,当然直接使用GPIO模拟I2C也可以实现读写。我们的驱动代码里两种操作模式都实现了。

从OV9650采集过来的数据没法直接交给CPU处理。S5PC100芯片里面集成了Camera控制器,叫FIMC(Fully Interactive Mobile Camera)。摄像头需要先把图像数据传给控制器,经过控制器处理(裁剪拉升后直接预览或者编码)之后交给CPU处理。

实际上摄像头工作需要的时钟也是FIMC给它提供的。

2. 驱动开发思路

因为驱动程序是承接硬件和软件的桥梁,因此开发摄像头驱动我们要搞清楚两方面的内容:第一是摄像头的硬件接口,也就是它是怎么和芯片连接的,如何控制它,如何给摄像头复位以及传送数据的格式等等;第二是摄像头的软件接口,Linux内核里面摄像头属于标准的V4L2设备,但是这个摄像头只是一个传感器,具体的操作都需要通过FIMC来控制,这看起来关系比较复杂。

相比较而言,硬件接口容易搞懂,通过读芯片手册和原理图基本上就没有问题了,软件接口比较复杂,主要中间有一个Camera控制器。下面主要集中分析软件接口。

3. 硬件接口

摄像头的硬件原理图如下:

FS_S5PC100平台上Linux Camera驱动开发详解(一)_第2张图片

拿到原理图,我们需要关注的是1、2两个管脚分别连接到I2C_SDA1和I2C_SCL1,这说明可以通过I2C控制器1来配置摄像头。另外调试摄像头的时候,可以根据这个原理图使用示波器来测量波形以验证代码是否正确。

这里还需要注意的是开发驱动之前最好用万用表测量摄像头的各个管脚是否和芯片连接正确,否则即使代码没有问题也看不到图像。

另外,还需要仔细阅读芯片手册里Camera控制器一章的描述。主要是明确以下信息:

FS_S5PC100平台上Linux Camera驱动开发详解(一)_第3张图片

FIMC支持以上三种视频工业标准,OV9650支持ITU-R 601 YcbCr 8-bit mode,这对后面的驱动编写非常重要。

FS_S5PC100平台上Linux Camera驱动开发详解(一)_第4张图片

FS_S5PC100平台上Linux Camera驱动开发详解(一)_第5张图片

FS_S5PC100平台上Linux Camera驱动开发详解(一)_第6张图片

FS_S5PC100平台上Linux Camera驱动开发详解(一)_第7张图片

MPLL和APLL都可以作为摄像头的时钟源,不过推荐使用MPLL。这对后面的驱动开发也有帮助。

4. 软件接口(如何和FIMC驱动对接)

硬件的问题搞清楚之后就可以集中精力关注软件的接口了。驱动可以有两种实现方法:第一种是把摄像头驱动做成普通的V4L2设备,直接调用FIMC里的寄存器实现视频数据的捕捉和处理;第二种利用内核已经实现好的FIMC的驱动,通过某种接口形式,把我们的摄像头驱动挂接在FIMC驱动之下。

这两种方法第一种实现起来代码量比较大,因为需要直接操作FIMC的寄存器,难度也大一些;第二种方法是利用内核已经做好的FIMC驱动,难点在于如何把摄像头驱动和FIMC驱动整合起来。

在Android下面,第一种方法并不可行,因为FIMC这个模块不仅仅是一个摄像头的控制接口,它还承担着V4L2的output功能和overlay(显示叠层)的功能,这两个功能对Android的显示系统非常重要。因此最好的方案还是第二种,找到摄像头驱动和FIMC驱动对接的接口,只要明确了这个接口,后面的事情就好办了,工作量也不大。

4-1: FIMC驱动的总体结构分析

FIMC的驱动在内核中的位置:

drivers/media/video/samsung/fimc
        fimc40_regs.c
        fimc43_regs.c
        fimc_capture.c
        fimc_dev.c
        fimc_output.c
        fimc_overlay.c
        fimc_v4l2.c

这些源码里面最基础的是fimc_dev.c,这里面注册了一个platform_driver,在相应的平台代码里面有对应的platform_device的描述。这种SOC上的控制器一般都会挂接在platform_bus上以实现在系统初始化时的device和driver的匹配。

在driver的probe函数里面,主要完成了资源获取以及v4l2设备的注册。因为FIMC一共有三套一样的控制器(fimc0, fimc1, fimc2),所以驱动里使用了一个数组来描述:

struct video_device fimc_video_device[FIMC_DEVICES] = {
                [0] = {
                        .fops = &fimc_fops,
                        .ioctl_ops = &fimc_v4l2_ops,
                        .release = fimc_vdev_release,
                 },
                [1] = {
                        .fops = &fimc_fops,
                        .ioctl_ops = &fimc_v4l2_ops,
                        .release = fimc_vdev_release,
                },
                [2] = {
                        .fops = &fimc_fops,
                        .ioctl_ops = &fimc_v4l2_ops,
                        .release = fimc_vdev_release,
                },
        };

在probe函数里,调用video_register_device()来注册这三个video_device,在用户空间里就会在/dev下看到三个video设备节点,video0,video1,video2. 每个video_device的成员fops对应的是针对v4l2设备的基本操作,定义如下:

static const struct v4l2_file_operations fimc_fops = {
                .owner = THIS_MODULE,
                .open = fimc_open,
                .release = fimc_release,
                .ioctl = video_ioctl2,
                 .read = fimc_read,
                .write = fimc_write,
                .mmap = fimc_mmap,
                .poll = fimc_poll,
        };

另一个成员ioctl_ops非常重要,因为它是对v4l2的所有ioctl操作集合的描述。fimc_v4l2_ops定义在fimc_v4l2.c里面:

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_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_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,
        };

可以看到,FIMC的驱动实现了v4l2所有的接口,可以分为v4l2-input设备接口,v4l2-output设备接口以及v4l2-overlay设备接口。这里我们主要关注v4l2-input设备接口,因为摄像头属于视频输入设备。

fimc_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。这样把硬件相关的代码和硬件无关的代码分开来实现是非常好的方式,可以最大限度的实现代码复用。

这些驱动源码的组织关系如下:

FS_S5PC100平台上Linux Camera驱动开发详解(一)_第8张图片

4-2: FIMC驱动的Camera接口分析

接口的关键还是在于fimc_dev.c里的probe函数。probe里面会调用一个函数叫fimc_init_global(),这里面会完成摄像头的分配以及时钟的获取。这个函数的原型如下:

static int fimc_init_global( struct platform_device *pdev )

这个platform_device是内核从平台代码那里传递过来的,里面包含的就是和具体平台相关的信息,其中就应该包含摄像头信息。

函数的实现:

static int fimc_init_global(struct platform_device *pdev)
        {
                struct fimc_control *ctrl;
                struct s3c_platform_fimc *pdata;
                //这个结构体就是用来描述一个摄像头的,先不管它里面的内容
                //等会儿在分析平台代码的时候可以看到它是如何被填充的
                struct s3c_platform_camera *cam;
                struct clk *srclk;
                int id, i;

        //获得平台信息
                pdata = to_fimc_plat(&pdev->dev);
                id = pdev->id; //id号可能是0,1,2
                ctrl = get_fimc_ctrl(id); //获得id号对应的fimc_control结构体指针

        /* Registering external camera modules. re-arrange order to be sure */
                for (i = 0; i < FIMC_MAXCAMS; i++) {
                        cam = pdata->camera[i]; //从平台数据取得camera的信息
                        if (!cam)
                                continue; // change break to continue by ys

                /* WriteBack doesn't need clock setting */
                        if(cam->id == CAMERA_WB) {
                                fimc_dev->camera[cam->id] = cam;
                                break;
                        }

                        // 获得时钟源信息
                        srclk = clk_get(&pdev->dev, cam->srclk_name);
                        if (IS_ERR(srclk)) {
                                fimc_err("%s: failed to get mclk source\n", __func__);
                                return -EINVAL;
                         }

                // 获得camera的时钟信息
                        /* mclk */
                        cam->clk = clk_get(&pdev->dev, cam->clk_name);
                        if (IS_ERR(cam->clk)) {
                                fimc_err("%s: failed to get mclk source\n", __func__);
                                return -EINVAL;
                        }

                if (cam->clk->set_parent) {
                                cam->clk->parent = srclk;
                                cam->clk->set_parent(cam->clk, srclk);
                        }

                /* Assign camera device to fimc */
                        fimc_dev->camera[cam->id] = cam; // 将从平台获得的camera分配给全局数据结构
                                                                                        // fimc_dev
                }

        fimc_dev->initialized = 1;

        return 0;
        }

可以看到这个函数实际上就是把camera的信息从平台数据那里取过来,然后分配给fimc_dev. fimc_dev定义在fimc.h里面。类型为struct fimc_global,原型如下:

/* global */
        struct fimc_global {
                struct fimc_control        ctrl[FIMC_DEVICES];
                struct s3c_platform_camera *camera[FIMC_MAXCAMS];
                int        initialized;
        };

现在我们需要看一下平台代码那里如何描述一个摄像头以及如何把抽象数据结构传递到平台数据里面。

S5PC100 SOC对应的平台代码位于:

arch/arm/mach-s5pc100/mach-smdkc100.c

我们是这样来描述一个camera的:

#ifdef CONFIG_VIDEO_OV9650
        /* add by ys for ov9650 */
        static struct s3c_platform_camera camera_c = {
                .id = CAMERA_PAR_A, /* FIXME */
                .type = CAM_TYPE_ITU, /* 2.0M ITU */
                .fmt = ITU_601_YCBCR422_8BIT,
                .order422 = CAM_ORDER422_8BIT_YCBYCR,
                .i2c_busnum = 1,
                .info = &camera_info[2],
                .pixelformat = V4L2_PIX_FMT_YUYV,
                .srclk_name = "dout_mpll",
                .clk_name = "sclk_cam",
                .clk_rate = 16000000, /* 16MHz */
                .line_length = 640, /* 640*480 */
                /* default resol for preview kind of thing */
                .width = 640,
                .height = 480,
                .window = {
                        .left = 0,
                        .top = 0,
                        .width = 640,
                        .height = 480,
                },

        /* Polarity */
                .inv_pclk = 1,
                .inv_vsync = 0,
                .inv_href = 0,
                .inv_hsync = 0,

        .initialized = 0,
        };
        #endif

这里面的信息描述了OV9650相关的所有信息。type代表摄像头是ITU的接口,fmt代表摄像头输出的格式是ITU_601_YCBCR422_8BIT,order422代表YUV三个分量的顺序是YcbCr。这些都和前面的描述相符。另外里面还有时钟源的信息,时钟的大小以及捕捉图像的解析度,这里设置的是640x480(VGA模式),因为经过调试发现OV9650工作在VGA的模式下比较流畅清晰。Polarity代表信号的极性,具体的设置要和摄像头本身的设置一致。

i2c_busnum是I2C总线的总线编号,因为S5PC100一共有两条I2C总线(0和1),我们连在SDA1上,所以i2c_busnum是1。

camera_c是fimc_plat结构体的一个成员:

/* Interface setting */
        static struct s3c_platform_fimc fimc_plat = {
                .default_cam = CAMERA_PAR_A,
                .camera[ 2 ] = &camera_c,
                .hw_ver = 0x40,
        };

这里会把camera_c赋值给fimc_plat里的camera数组的第三个元素,之所以是第三个是因为Android的原因。这在分析Android的摄像头硬件抽象层时会有解释。

struct s3c_platform_fimc这个结构体其实就是fimc对应的平台数据结构。在平台代码里,会由以下三个函数负责注册:

s3c_fimc0_set_platdata(&fimc_plat);
        s3c_fimc1_set_platdata(&fimc_plat);
        s3c_fimc2_set_platdata(&fimc_plat);

至于这几个函数如何实现,这里就不分析了,有兴趣可以自己看代码。

也就是说只要平台代码这边我们填充了一个struct s3c_platform_camera类型的结构体,然后把它添加到fimc_plat里面,fimc的驱动就能获得对应的Camera的信息。

你可能感兴趣的:(linux,struct,video,平台,CAM,linux内核)