基于V4L2 的视频驱动开发1-2

作者: 华清远见 刘洪涛

 

编写基于 V4L2 视频驱动主要涉及到以下几个知识点:

l         摄像头方面的知识

要了解选用的摄像头的特性,包括访问控制方法、各种参数的配置方法、信号输出类型等。

l         Camera 解码器、控制器

如果摄像头是模拟量输出的,要熟悉解码器的配置。最后数字视频信号进入 camera 控制器后,还要熟悉camera 控制器的操作。

l         V4L2 的 API 和数据结构

编写驱动前要熟悉应用程序访问 V4L2 的方法及设计到的数据结构。

l         V4L2 的驱动架构

最后编写出符合 V4L2 规范的视频驱动。

 

 本文介绍基于 S3C2440 硬件平台的 V4L2 视频驱动开发。摄像头采用 OmniVision 公司的 OV9650 和 OV9655 。主要包含以下几个方面的内容:

 

l         视频驱动的整体驱动框架

l         S3C2440 camera 控制器 +ov9650 ( ov9655 )

l         V4L2 API 及数据结构

l         V4L2 驱动框架

l         ov9650 ( ov9655 ) +s3c2440+V4L2 实例

 

 

一、            视频驱动的整体框架

视频驱动的整体框架见下图:

 

二、S3C2440 camera 控制器+ov9650 (ov9655 )

 

( 1 ) S3C2440 camera 控制器介绍

S3C2440 支持 ITU-R BT601/656 格式的数字图像输入 ,支持的 2 个通道的 DMA , Preview 通道和 Codec 通道,参见下图。

 

 

 

 

 

        Preview 通道可以将 YCbCr4:2:2 格式的图像转换为 RGB ( 16bit 或 24bit )格式的数据,并存放于为Preview DMA 分配的内存中,最大分辨率为 640*480 。主要用于本地液晶屏显示。如果将 Preview DMA 的内存和Framebuffer 内存重叠的话,就可以实现采集直接输出到液晶屏上了。

         Codec 通道可以输出 YCbCr4:2:0 或 YCbCr4:2:2 格式到为 Codec DMA 分配的内存中。最大分辨率为4096*4096 。主要用于图像的编解码处理。

 

   上图中的 window cut 功能是指在图像可以先做一个裁剪。通过设置 CIWDOFST 完成此功能,见下图。图像进入P 、 C 通道后,各自的 scaler 单元还可以对其进行缩放、旋转等处理。

 

 

 

       S3C2440 camera 控制器支持乒乓存储。为了防止采集和输出之间的冲突,采用了乒乓存储方式。每次采集一帧后,自动转到下一个存储区。如果你因为内存空间不足,不想使用此功能的话,可以将四个区域设置到同一块空间。

    在做图像处理时,需要关注到最后存储区中的图像格式,如 codec 通道硬件自动把 Y 、 Cb 、 Cr 分离存储。

 

 

 

S3C2440 camera 控制器 Last IRQ 功能的使用,也是需要掌握的。如果处理不好,输出的图像效果会受影响。

 

 

 

 

 

     控制器会在每个 VSYNC 下降沿判断 ImgCptEn 信号等命令。如果在下降沿发现 ImgCptEn 信号有效,则产生 IRQ 中断。然后才开始一帧图像的真正采集。而如果在 VSYNC 下降沿判断到 ImgCptEn 为低电平且之前LastIRQEn 没有使能,则不会产生任何中断,且不会再进行下一帧的采集。如果你想在 ImgCptEn 关闭后,一帧采集完后产生一个中断通知你,那么就需要在最后一次中断产生前( stop capturing 后的 vysnc 下将沿)使能 lastirq就可以了。

     我在移植 linux 驱动时就遇到了一个 Last IRQ 的问题。现象是输出图像上面总是有一条比其它部分反应慢。采集运动图像,就能看出现象。查看代码是因为没有设立 lastirq ,因为每次如果不在 lastirq 产生的情况下读取,图像缓冲中的数据是不稳定的,可能照成图像不完整。修改代码支持 lastirq 后,问题解决。

Camera 控制器时钟设置也是需要注意的, ov9650 需要 Camera 控制器为其提供时钟。

 

 

 

   提供给外部摄像头的时钟是由 UPLL 输出时钟分频得到的。而 CAMIF 的时钟是由 HCLK 提供的。本例中,提供给 ov9650 的时钟为 24M 。

( 2 ) ov9650 ( ov9655 )设置方法

          OV9650 是 OmniVision 公司的 COMS 摄像头, 130 万像素,支持 SXVGA 、 VGA 、 QVGA 、 CIF 等图像输出格式。 最大速率在 SXVGA 时为 15fps ,在 VGA 时为 30fps 。

OV9650 摄像头时序如下图:

 

 

    

    上图中 D[9:2] 用于 8-bitYUV 或者 RGB565/RGB555(D[9]MSB 、 D[2]LSB) 。 D[9:0] 用于 10-bit RGB 。本例中使用 8-bit YUV 模式。

    我手边开发板的 Camera 和 S3C2440 的接线原理图如下(对应 camera 中具体的信号名称参见前文的驱动整体架构图)。

    注: GPG12 用于 PWEN 信号

 

 

 

( 3 )编写 ARM 测试代码测试 camera 功能

      在 Keil 环境下编写一个测试代码完成从摄像头采集图像输出到液晶屏。下面列出程序的流程。

 

( 4 )编写测试代码过程中常见的问题

l         摄像头寄存器的配置

     因为摄像头有很多寄存器,可能一下无法理解里面所有的配置含义,所以开始时希望得到一份可用的配置。但往往从别人的测试代码中拿到配置后,仍然无法使用。我这里列出几个可能的原因:( 1 )摄像头中的图像输出格式和你在 camera 控制器中设置的不一致,同一个摄像头可以设置多种输入格式,如: YCbYCr 或CbYCrY 。( 2 )图像输出的一些时序和你的 camera 控制器设置不一致,摄像头可以设置一些时序,如:图像数据在 CAMPCLK 的上升沿有效还是下降沿有效。( 3 )注意输出图像的格式和 Framebuffer 控制器的匹配,如字节顺序等问题。

l         Ov9650 和 ov9655 的使用区别

    这里主要列出两者之间在复位信号上有差别, ov9650 是高电平复位,而 ov9655 是低电平复位。

 


三、            V4L2 API 及数据结构

V4L2 是 V4L 的升级版本,为 linux 下视频设备程序提供了一套接口规范。包括一套数据结构和底层 V4L2 驱动接口。

1 、常用的结构体在内核目录 include/linux/videodev2.h 中定义

   struct v4l2_requestbuffers // 申请帧缓冲,对应命令 VIDIOC_REQBUFS 
   struct v4l2_capability // 视频设备的功能,对应命令 VIDIOC_QUERYCAP 
   struct v4l2_input   // 视频输入信息,对应命令 VIDIOC_ENUMINPUT
   struct v4l2_standard // 视频的制式,比如PAL ,NTSC ,对应命令 VIDIOC_ENUMSTD 
   struct v4l2_format     // 帧的格式,对应命令VIDIOC_G_FMT 、VIDIOC_S_FMT 等 
   struct v4l2_buffer   // 驱动 中的一帧图像缓存,对应命令VIDIOC_QUERYBUF

   struct v4l2_crop   // 视频信号矩形边框

      v4l2_std_id    // 视频制式

2 、常用的 IOCTL 接口命令也在 include/linux/videodev2.h 中定义

 

VIDIOC_REQBUFS // 分配内存  

VIDIOC_QUERYBUF // 把 VIDIOC_REQBUFS 中分配的数据缓存转换成物理地址

VIDIOC_QUERYCAP // 查询驱动功能

VIDIOC_ENUM_FMT // 获取当前驱动支持的视频格式

VIDIOC_S_FMT // 设置当前驱动的频捕获格式

VIDIOC_G_FMT // 读取当前驱动的频捕获格式

VIDIOC_TRY_FMT // 验证当前驱动的显示格式

VIDIOC_CROPCAP // 查询驱动的修剪能力

VIDIOC_S_CROP // 设置视频信号的矩形边框

VIDIOC_G_CROP // 读取视频信号的矩形边框

VIDIOC_QBUF // 把数据从缓存中读取出来

VIDIOC_DQBUF // 把数据放回缓存队列

VIDIOC_STREAMON // 开始视频显示函数

VIDIOC_STREAMOFF // 结束视频显示函数

VIDIOC_QUERYSTD // 检查当前视频设备支持的标准,例如 PAL 或 NTSC 。

 

 

3 、操作流程

V4L2 提供了很多访问接口,你可以根据具体需要选择操作方法。需要注意的是,很少有驱动完全实现了所有的接口功能。所以在使用时需要参考驱动源码,或仔细阅读驱动提供者的使用说明。

下面列举出一种操作的流程,供参考。

( 1 )打开设备文件

int fd = open(Devicename,mode);

    Devicename : /dev/video0 、 /dev/video1 ……

     Mode : O_RDWR [| O_NONBLOCK]

       如果使用非阻塞模式调用视频设备,则当没有可用的视频数据时,不会阻塞,而立刻返回。

( 2 )取得设备的 capability

struct v4l2_capability capability ;

              int ret = ioctl(fd, VIDIOC_QUERYCAP, &capability);     

看看设备具有什么功能,比如是否具有视频输入特性。

( 3 )选择视频输入

struct v4l2_input input ;

……初始化 input

int ret = ioctl(fd, VIDIOC_QUERYCAP, &input);     

一个视频设备可以有多个视频输入。如果只有一路输入,这个功能可以没有。

( 4 )检测视频支持的制式

            v4l2_std_id std;

            do {

                   ret = ioctl(fd, VIDIOC_QUERYSTD, &std);

            } while (ret == -1 && errno == EAGAIN);

            switch (std) {

                 case V4L2_STD_NTSC: 

                       //……

         case V4L2_STD_PAL:

             //……

}

( 5 )设置视频捕获格式

struct v4l2_format fmt;

fmt.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;

fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_UYVY;

fmt.fmt.pix.height = height;

fmt.fmt.pix.width = width;

fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;

ret = ioctl(fd, VIDIOC_S_FMT, &fmt);

if(ret) {

perror("VIDIOC_S_FMT/n");

close(fd);

return -1;

}

( 6 )向驱动申请帧缓存

     struct v4l2_requestbuffers  req;

if (ioctl(fd, VIDIOC_REQBUFS, &req) == -1) {

           return -1;

}

       v4l2_requestbuffers 结构中定义了缓存的数量,驱动会据此申请对应数量的视频缓存。多个缓存可以用于建立 FIFO ,来提高视频采集的效率。

( 7 )获取每个缓存的信息,并 mmap 到用户空间

typedef struct VideoBuffer {

    void   *start;

    size_t  length;

} VideoBuffer;

                                                              

VideoBuffer*       buffers = calloc( req.count, sizeof(*buffers) );

struct v4l2_buffer    buf;

 

for (numBufs = 0; numBufs < req.count; numBufs++) {// 映射所有的缓存

    memset( &buf, 0, sizeof(buf) );

    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    buf.memory = V4L2_MEMORY_MMAP;

    buf.index = numBufs;

      if (ioctl( fd, VIDIOC_QUERYBUF, &buf) == -1) {// 获取到对应 index 的缓存信息,此处主要利用 length 信息及offset 信息来完成后面的 mmap 操作。

        return -1;

    }

 

    buffers[numBufs].length = buf.length;

    // 转换成相对地址

    buffers[numBufs].start = mmap(NULL, buf.length,

        PROT_READ | PROT_WRITE,

        MAP_SHARED,

        fd, buf.m.offset);

 

    if (buffers[numBufs].start == MAP_FAILED) {

        return -1;

    }

 

( 8 )开始采集视频

int buf_type= V4L2_BUF_TYPE_VIDEO_CAPTURE ;

int ret = ioctl(fd, VIDIOC_STREAMON, &buf_type);

 

( 9 )取出 FIFO 缓存中已经采样的帧缓存

struct v4l2_buffer buf;

memset(&buf,0,sizeof(buf));

buf.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;

buf.memory=V4L2_MEMORY_MMAP;

buf.index=0;// 此值由下面的 ioctl 返回

if (ioctl(fd, VIDIOC_DQBUF, &buf) == -1)

{

    return -1;

}

根据返回的 buf.index 找到对应的 mmap 映射好的缓存,取出视频数据。

( 10 )将刚刚处理完的缓冲重新入队列尾,这样可以循环采集

if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) {

    return -1;

}

( 11 )停止视频的采集

int ret = ioctl(fd, VIDIOC_STREAMOFF, &buf_type);

( 12 )关闭视频设备

close(fd);

 

 

四、            V4L2 驱动框架

上述流程的各个操作都需要有底层 V4L2 驱动的支持。内核中有一些非常完善的例子。

比如: linux-2.6.26 内核目录 /drivers/media/video//zc301/zc301_core.c 中的 ZC301 视频驱动代码。上面的 V4L2 操作流程涉及的功能在其中都有实现。

1 、 V4L2 驱动注册、注销函数

       Video 核心层( drivers/media/video/videodev.c )提供了注册函数

int video_register_device(struct video_device *vfd, int type, int nr)

video_device:  要构建的核心数据结构

Type:  表示设备类型,此设备号的基地址受此变量的影响

Nr:    如果 end-base>nr>0 :次设备号 =base (基准值,受 type 影响) +nr ;

否则:系统自动分配合适的次设备号

       具体驱动只需要构建 video_device 结构,然后调用注册函数既可。

如: zc301_core.c 中的

       err = video_register_device(cam->v4ldev, VFL_TYPE_GRABBER,

                          video_nr[dev_nr]);

       Video 核心层( drivers/media/video/videodev.c )提供了注销函数

void video_unregister_device(struct video_device *vfd)

 

2 、 struct video_device 的构建

              video_device 结构包含了视频设备的属性和操作方法。参见 zc301_core.c

strcpy(cam->v4ldev->name, "ZC0301[P] PC Camera");

       cam->v4ldev->owner = THIS_MODULE;

       cam->v4ldev->type = VID_TYPE_CAPTURE | VID_TYPE_SCALES;

       cam->v4ldev->fops = &zc0301_fops;

       cam->v4ldev->minor = video_nr[dev_nr];

       cam->v4ldev->release = video_device_release;

       video_set_drvdata(cam->v4ldev, cam);

       大家发现在这个 zc301 的驱动中并没有实现 struct video_device 中的很多操作函数 , 如 : vidioc_querycap 、vidioc_g_fmt_cap 等。主要原因是 struct file_operations zc0301_fops 中的 zc0301_ioctl 实现了前面的所有 ioctl 操作。所以就不需要在 struct video_device 再实现 struct video_device 中的那些操作了。

       另一种实现方法如下:

static struct video_device camif_dev =

{

       .name             = "s3c2440 camif",

       .type              = VID_TYPE_CAPTURE|VID_TYPE_SCALES|VID_TYPE_SUBCAPTURE,

       .fops              = &camif_fops,

       .minor            = -1,

       .release    = camif_dev_release,

       .vidioc_querycap      = vidioc_querycap,

       .vidioc_enum_fmt_cap  = vidioc_enum_fmt_cap,

       .vidioc_g_fmt_cap     = vidioc_g_fmt_cap,

       .vidioc_s_fmt_cap     = vidioc_s_fmt_cap,

       .vidioc_queryctrl = vidioc_queryctrl,

       .vidioc_g_ctrl = vidioc_g_ctrl,

       .vidioc_s_ctrl = vidioc_s_ctrl,

};

static struct file_operations camif_fops =

{

       .owner           = THIS_MODULE,

       .open             = camif_open,

       .release    = camif_release,

       .read              = camif_read,

       .poll        = camif_poll,

       .ioctl              = video_ioctl2, /* V4L2 ioctl handler */

       .mmap           = camif_mmap,

       .llseek            = no_llseek,

};

注意 : video_ioctl2 是 videodev.c 中是实现的。 video_ioctl2 中会根据 ioctl 不同的 cmd 来

调用 video_device 中的操作方法。

3 、 Video 核心层的实现

       参见内核 /drivers/media/videodev.c

( 1 )注册 256 个视频设备

       static int __init videodev_init(void)

{

int ret;

           if (register_chrdev (VIDEO_MAJOR, VIDEO_NAME, &video_fops)) {

                  return -EIO;

           }

           ret = class_register(&video_class);

……

}

上面的代码注册了 256 个视频设备,并注册了 video_class 类。 video_fops 为这 256 个设备共同的操作方法。

( 2 ) V4L2 驱动注册函数的实现

 

int video_register_device(struct video_device *vfd, int type, int nr)

{

int i=0;

int base;

int end;

int ret;

       char *name_base;

 

       switch(type) // 根据不同的 type 确定设备名称、次设备号

       {

              case VFL_TYPE_GRABBER:

                     base=MINOR_VFL_TYPE_GRABBER_MIN;

                     end=MINOR_VFL_TYPE_GRABBER_MAX+1;

                     name_base = "video";

                     break;

              case VFL_TYPE_VTX:

                     base=MINOR_VFL_TYPE_VTX_MIN;

                     end=MINOR_VFL_TYPE_VTX_MAX+1;

                     name_base = "vtx";

                     break;

              case VFL_TYPE_VBI:

                     base=MINOR_VFL_TYPE_VBI_MIN;

                     end=MINOR_VFL_TYPE_VBI_MAX+1;

                     name_base = "vbi";

                     break;

              case VFL_TYPE_RADIO:

                     base=MINOR_VFL_TYPE_RADIO_MIN;

                     end=MINOR_VFL_TYPE_RADIO_MAX+1;

                     name_base = "radio";

                     break;

              default:

                     printk(KERN_ERR "%s called with unknown type: %d/n",

                            __func__, type);

                     return -1;

       }

 

       /* 计算出次设备号 */

       mutex_lock(&videodev_lock);

       if (nr >= 0  &&  nr < end-base) {

              /* use the one the driver asked for */

              i = base+nr;

              if (NULL != video_device[i]) {

                     mutex_unlock(&videodev_lock);

                     return -ENFILE;

              }

       } else {

              /* use first free */

              for(i=base;i<end;i++)

                     if (NULL == video_device[i])

                            break;

              if (i == end) {

                     mutex_unlock(&videodev_lock);

                     return -ENFILE;

              }

       }

       video_device[i]=vfd; // 保存 video_device 结构指针到系统的结构数组中,最终的次设备号和 i 相关。

       vfd->minor=i;

       mutex_unlock(&videodev_lock);

       mutex_init(&vfd->lock);

 

       /* sysfs class */

       memset(&vfd->class_dev, 0x00, sizeof(vfd->class_dev));

       if (vfd->dev)

              vfd->class_dev.parent = vfd->dev;

       vfd->class_dev.class       = &video_class;

       vfd->class_dev.devt       = MKDEV(VIDEO_MAJOR, vfd->minor);

       sprintf(vfd->class_dev.bus_id, "%s%d", name_base, i - base);// 最后在 /dev 目录下的名称

       ret = device_register(&vfd->class_dev);// 结合 udev  mdev 可以实现自动在 /dev 下创建设备节点

       ……

}

从上面的注册函数中可以看出 V4L2 驱动的注册事实上只是完成了设备节点的创建,如: /dev/video0 。和video_device 结构指针的保存。

( 3 )视频驱动的打开过程

当用户空间调用 open 打开对应的视频文件时,如:

int fd = open(/dev/video0, O_RDWR );

对应 /dev/video0 的文件操作结构是 /drivers/media/videodev.c 中定义的 video_fops 。

static const struct file_operations video_fops=

{

       .owner           = THIS_MODULE,

       .llseek            = no_llseek,

       .open             = video_open,

};

奇怪吧,这里只实现了 open 操作。那么后面的其它操作呢?还是先看看 video_open 吧。

static int video_open(struct inode *inode, struct file *file)

{

       unsigned int minor = iminor(inode);

       int err = 0;

       struct video_device *vfl;

       const struct file_operations *old_fops;

 

       if(minor>=VIDEO_NUM_DEVICES)

              return -ENODEV;

       mutex_lock(&videodev_lock);

       vfl=video_device[minor];

       if(vfl==NULL) {

              mutex_unlock(&videodev_lock);

              request_module("char-major-%d-%d", VIDEO_MAJOR, minor);

              mutex_lock(&videodev_lock);

              vfl=video_device[minor]; // 根据次设备号取出 video_device 结构

              if (vfl==NULL) {

                     mutex_unlock(&videodev_lock);

                     return -ENODEV;

              }

       }

       old_fops = file->f_op;

       file->f_op = fops_get(vfl->fops);// 替换此打开文件的 file_operation 结构。后面的其它针对此文件的操作都由新的结构来负责了。也就是由每个具体的 video_device  fops 负责。

       if(file->f_op->open)

              err = file->f_op->open(inode,file);

       if (err) {

              fops_put(file->f_op);

              file->f_op = fops_get(old_fops);

       }

……

}

以上是我对 V4L2 的一些理解,希望能对大家了解 V4L2 有一些帮助!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值