ov9650摄像头驱动之——linux内核v4l2架构分析

2023年 1月 28日 87点热度 0人点赞

原文地址: ov9650 摄像头驱动之——linux 内核 v4l2 架构分析 3

本系列准备分为 3-4 篇来讲, 因为说的太多会比较乱

v4l2 视频驱动主要涉及几个知识点:

  • 摄像头方面的知识 (摄像头厂家提供的芯片手册可以查看), 要了解选用的摄像头的特性, 包括访问控制方法, 各种参数的配置方法, 信号输出类型等.
  • Camera 解码器, 控制器 (主控芯片的芯片手册里面有摄像头相关的寄存器设置, 比如 2410 里, 里面主要是设置相关控制功能使能, 芯片内部自己的架构), 如果摄像头是模拟量输出的, 要熟悉解码器的配置. 最后数字视频信号进入 camera 控制器后, 还要熟悉 camera 控制器的操作.
  • V4L2 的 API 和数据结构控制 (主要是用户空间需要的一些 v4l2 的操作, 然后针对这些操作必须在底层实现相应的驱动), 编写驱动前要熟悉应用程序访问 V4L2 的方法及设计到的数据结构.
  • V4L2 的驱动架构 (这个是在底层写驱动, 为用户空间提供相应的访问接口, 可以参照内核里面的 /drivers/media/video/zc301/zc301_core.c 中的 ZC301 视频驱动代码, 它是内核提供的非常完善的 v4l2 架构的例子, 基本上都可以在它的基础上进行修改!)
  • 最后编写出符合 V4L2 规范的视频驱动.

摄像头方面的知识

ov9650 摄像头, 暂时先不说, 先了解一下 camera 解码器, 控制器, 不同的主控芯片的 camera 控制器都差不多.

static struct ov9650_reg {
    unsigned char subaddr;
    unsigned char value;
} regs[] = {

    /* OV9650 intialization parameter table for VGA application */
    {0x12, 0x40},// Camera Soft reset. Self cleared after reset.
    {CHIP_DELAY, 10},
    {0x11, 0x81},{0x6a, 0x3e},{0x3b, 0x09},{0x13, 0xe0},{0x01, 0x80},{0x02, 0x80},{0x00, 0x00},{0x10, 0x00},
    {0x13, 0xe5},{0x39, 0x43},{0x38, 0x12},{0x37, 0x91},{0x35, 0x91},{0x0e, 0xa0},{0x1e, 0x04},{0xA8, 0x80},
    {0x14, 0x40},{0x04, 0x00},{0x0c, 0x04},{0x0d, 0x80},{0x18, 0xc6},{0x17, 0x26},{0x32, 0xad},{0x03, 0x00},
    {0x1a, 0x3d},{0x19, 0x01},{0x3f, 0xa6},{0x14, 0x2e},{0x15, 0x10},{0x41, 0x02},{0x42, 0x08},{0x1b, 0x00},
    {0x16, 0x06},{0x33, 0xe2},{0x34, 0xbf},{0x96, 0x04},{0x3a, 0x00},{0x8e, 0x00},{0x3c, 0x77},{0x8B, 0x06},
    {0x94, 0x88},{0x95, 0x88},{0x40, 0xc1},{0x29, 0x3f},{0x0f, 0x42},{0x3d, 0x92},{0x69, 0x40},{0x5C, 0xb9},
    {0x5D, 0x96},{0x5E, 0x10},{0x59, 0xc0},{0x5A, 0xaf},{0x5B, 0x55},{0x43, 0xf0},{0x44, 0x10},{0x45, 0x68},
    {0x46, 0x96},{0x47, 0x60},{0x48, 0x80},{0x5F, 0xe0},{0x60, 0x8c},{0x61, 0x20},{0xa5, 0xd9},{0xa4, 0x74},
    {0x8d, 0x02},{0x13, 0xe7},{0x4f, 0x3a},{0x50, 0x3d},{0x51, 0x03},{0x52, 0x12},{0x53, 0x26},{0x54, 0x38},
    {0x55, 0x40},{0x56, 0x40},{0x57, 0x40},{0x58, 0x0d},{0x8C, 0x23},{0x3E, 0x02},{0xa9, 0xb8},{0xaa, 0x92},
    {0xab, 0x0a},{0x8f, 0xdf},{0x90, 0x00},{0x91, 0x00},{0x9f, 0x00},{0xa0, 0x00},{0x3A, 0x01},{0x24, 0x70},
    {0x25, 0x64},{0x26, 0xc3},{0x2a, 0x00},{0x2b, 0x00},{0x6c, 0x40},{0x6d, 0x30},{0x6e, 0x4b},{0x6f, 0x60},
    {0x70, 0x70},{0x71, 0x70},{0x72, 0x70},{0x73, 0x70},{0x74, 0x60},{0x75, 0x60},{0x76, 0x50},{0x77, 0x48},
    {0x78, 0x3a},{0x79, 0x2e},{0x7a, 0x28},{0x7b, 0x22},{0x7c, 0x04},{0x7d, 0x07},{0x7e, 0x10},{0x7f, 0x28},
    {0x80, 0x36},{0x81, 0x44},{0x82, 0x52},{0x83, 0x60},{0x84, 0x6c},{0x85, 0x78},{0x86, 0x8c},{0x87, 0x9e},
    {0x88, 0xbb},{0x89, 0xd2},{0x8a, 0xe6},
};

上面是需要顺序写 ov9650 的寄存器的地址和写入的数值 (采用 I2C 子系统传输).

I2C 子系统传输已经分析过, 平台设备的资源可以在板文件中初始化:

修改 vi drivers/i2c/busses/Kconfig

修改

config I2C_S3C2410

tristate "S3C2410 I2C Driver"

depends on ARCH_S3C2410 || ARCH_S3C64XX

help

  Say Y here to include support for I2C controller in the

  Samsung S3C2410 based System-on-Chip devices.

为:

config I2C_S3C2410

tristate "S3C2410 I2C Driver"

depends on ARCH_S3C2410 || ARCH_S3C64XX || ARCH_S5PC100

help

  Say Y here to include support for I2C controller in the

  Samsung S3C2410 based System-on-Chip devices.

内核配置并重新编译内核

$ make menuconfig

Device Drivers  --->

<*> I2C support  --->

<*>   I2C device interface

I2C Hardware Bus support  --->

<*> S3C2410 I2C Driver

修改vi arch/arm/mach-s5pc100/mach-smdkc100.c

查看原理图可以知道摄像头是接在 I2C-0 或 1 上, 假设在 1 上, 根据原理图修改 i2c_devs1 添加 ov9650 的内容, 主要是 ov9650 的地址, 这个在芯片手册上可以查到是 0x60

而下面为什么是 0x30 呢? 在我的另外一篇 I2C 子系统分析里面讲过.给个链接解释

修改:

static struct i2c_board_info i2c_devs1[] __initdata = {

};

为:

static struct i2c_board_info i2c_devs1[] __initdata = {
    {
        I2C_BOARD_INFO("ov9650", 0x30),
    },
};

添加 s5pc100 摄像头控制器平台设备相关内容, 这些内容我们可以通过查看 S5PC100 的芯片手册查到

static struct resource s3c_camif_resource[] = {

    [0] = {
              .start = 0xEE200000,
              .end   = 0xEE200000 + SZ_1M - 1,
              .flags = IORESOURCE_MEM,
          },

    [1] = {
              .start = IRQ_FIMC0,
              .end   = IRQ_FIMC0,
              .flags = IORESOURCE_IRQ,
          }
};

static u64 s3c_device_camif_dmamask = 0xffffffffUL;

struct platform_device s3c_device_camif = {
    .name              = "s5pc100-camif",
    .id                = 0,
    .num_resources     = ARRAY_SIZE(s3c_camif_resource),
    .resource          = s3c_camif_resource,
    .dev               = {
                             .dma_mask          = &s3c_device_camif_dmamask,
                             .coherent_dma_mask = 0xffffffffUL
                         }

};

EXPORT_SYMBOL(s3c_device_camif);

注册摄像头控制平台设备:

在 smdkc100_devices 中添加 s3c_device_camif

static struct platform_device *smdkc100_devices[] __initdata = {
    &s3c_device_camif,  // 添加内容
};

添加驱动 (video)

Make menuconfig
Device Drivers  --->  
 <*> Multimedia support  --->
 <*>   Video For Linux 
[*]     Enable Video For Linux API 1 (DEPRECATED) (NEW)
[*]   Video capture adapters (NEW)  --->
[*]   V4L USB devices (NEW)  ---> 
<*>   USB Video Class (UVC) 
[*]     UVC input events device support (NEW)
 <*>   USB ZC0301[P] webcam support (DEPRECATED)

这样 device 已经注册好了!

/* write a register */

static int ov9650_reg_write(struct i2c_client *client, u8 reg, u8 val) {
    int ret;
    u8 _val;
    unsigned char data[2] = { reg, val };
    struct i2c_msg msg = {
                             .addr= client->addr,
                             .flags= 0,
                             .len= 2,
                             .buf= data,
                         };

    //构建 i2c_msg
    ret = i2c_transfer(client->adapter, &msg, 1);  //I2C 适配器和 I2C 设备之间的一组消息的交换
    return 0;
}

static void ov9650_init_regs(void) {
    int i;
    for (i=0; i<ARRAY_SIZE(regs); i++) {
        if (regs[i].subaddr == 0xff) {
            mdelay(regs[i].value);
            continue;
        }
        ov9650_reg_write(ov9650_client, regs[i].subaddr, regs[i].value);
    }
}

至此, 通过 I2C 总线已经将摄像头的寄存器初始化好了.

下一部分将讲解 Camera 解码器, 控制器.

Camera 解码器, 控制器

DMA 内存初始化

根据 camera 控制器的描述, 图像传输有两个 DMA 通道, 我们用的是 C 通道, 所以先将 DMA 内存初始化, 因为在 V4L2 操作中有把 VIDIOC_REQBUFS 中分配的数据缓存转换成物理地址的操作, 所以 DMA 在用之前要初始化, 包括实际物理地址的计算

init_image_buffer(camera_dev);// 初始化
static int __inline__ init_image_buffer(struct s5pc100_camera_device *cam) {
    unsigned long size;
    unsigned int order;
    cam->frame = img_buff;
    size = MAX_WIDTH * MAX_HEIGHT * formats[3].depth / 8; //sizeof image buffer is 600KBytes 
    printk("each image buffer is %dKBytes.\n", (int)(size/1024));
    order = get_order(size); //系统函数, size 应该是 2 的 n 次幂, 内存按页分配
    img_buff[0].order = order;
    // 申请 DMA 空间, 该函数可分配多个页并返回分配内存的首地址, 分配的页数为 2 的 order 次幂, 分配的页也不清零.
    // order 允许的最大值是 10(即 1024 页) 或者 11(即 2048 页), 具体依赖于硬件平台.
    img_buff[0].virt_base = __get_free_pages(GFP_KERNEL|GFP_DMA, img_buff[0].order);
    img_buff[0].img_size = size;
    // the DMA address. 申请的 DMA 的物理地址, 怎么计算的呢?
    // 首先要减去 PAGE_OFFSET why? 因为在 linux 系统中, 进程的 4G 空间被分为用户空间和内核空间两部分,
    // 用户空间的地址一般分布为 0-3G(即 RAGE_OFFSET),
    // 这样剩下的 3-4G 为内核空间, 然后再加上 +PHYS_OFFSET(这个是由具体的 cpu 决定的, RAM 的物理起始地址), 这样的话 phy_base 就对应上了真正的物理地址
    img_buff[0].phy_base = img_buff[0].virt_base - PAGE_OFFSET + PHYS_OFFSET;
    printk("get pages for img_buff[0..3] done.\n");
    return 0;
error0:
    return -ENOMEM;
}

camera 控制器的初始化

  • 图像源的格式设置
  • window cut 的设置
  • 目标图像格式的设置
  • 图像的缩放, 旋转设置
  • (可选, 如果是用本地 LCD 显示的话) 将输出 buffer 地址定位在 Framebuffer 显存地址中 (即内存重叠, 这样的话 LCD 就能直接显示了), 因为这里没用到 LCD, 所以这个就省略

具体代码:

init_camif_config(camera_dev);

static void init_camif_config(struct s5pc100_camera_device* c) {

    struct s5pc100_camera_device*cam = c;
    cam->format = 3;// FIXME, C-path default format, see formats[] for detail. 选择 C 通道
    cam->srcHsize = 640;//  FIXME, the OV9650's horizontal output pixels. 设置图像源的大小
    cam->srcVsize = 480;// FIXME, the OV9650's verical output pixels.

    // 设置图像源的大小
    cam->wndHsize = 640;
    cam->wndVsize = 480; //window cut 的设置
    cam->targetHsize = cam->wndHsize; //目标图像格式的设置, 与 window 图像重叠, 全覆盖
    cam->targetVsize = cam->wndVsize;

    // 旋转没有设置
    // 到目前为止, 只是填充了 cam 的数据, 但是 camera 控制器的源地址寄存器, 目的地址寄存器都还没有配置
    // 这两个寄存器的配置依赖于上面初始化的参数
    update_camera_config(cam, (u32)-1);//这个函数中集成了一个函数, 这个函数就是配置两个寄存器的操作
}

static void update_camera_config (struct s5pc100_camera_device *c, u32 cmdcode) {
    struct s5pc100_camera_device *cam = c;
    update_camera_regs(cam);// config the regs directly. 封装了下面的两个函数, 其实没必要
}

static void __inline__ update_camera_regs(struct s5pc100_camera_device * cam) {
    update_source_fmt_regs(cam);
    update_target_fmt_regs(cam);
}

初始化 source 寄存器

static void __inline__ update_source_fmt_regs(struct s5pc100_camera_device *c) {
    struct s5pc100_camera_device *cam = c;
    u32 cfg;
    cfg = (1<<31)    // ITU-R BT.601 YCbCr 8-bit mode
         |(0<<30)    // CB, Cr value offset cntrol for YCbCr
         |(640<<16)  // target image width
         |(0<<14)    // input order is YCbYCr
         |(640<<0);  // source image height
    writel(cfg, cam->reg_base + S5PC100_CISRCFMT);    //0xEE20_0000 + 0000_0000 图像源地址
    printk("S5PC100_CIGCFMT = %x\n", readl(cam->reg_base + S5PC100_CISRCFMT));
    cfg = (1<<15) | (1<<14) | (1<<30) | (1<<29);
    writel(cfg, cam->reg_base + S5PC100_CIWDOFST);///0xEE20_0000 + 0000_0004 清缓存 fifo
    cfg = (1<<26) | (1<<29) | (1<<16) | (1<<7) | (0<<0);
    writel(cfg, cam->reg_base + S5PC100_CIGCTRL);///0xEE20_0000 + 0000_0008 全局变量控制寄存器, 包含了使能 IRQ 中断等操作
    printk("S5PC100_CIGCTRL = %x\n", readl(cam->reg_base + S5PC100_CIGCTRL));
    writel(0, cam->reg_base + S5PC100_CIWDOFST2);//0xEE20_0000 + 0000_0014 窗口偏移寄存器
    printk("OV9650_VGA mode\n");
}

初始化目的寄存器

static void __inline__ update_target_fmt_regs(struct s5pc100_camera_device * cam)
{
u32 cfg;
u32 h_shift;
u32 v_shift;
u32 prescaler_v_ratio;
u32 prescaler_h_ratio;
u32 main_v_ratio;
u32 main_h_ratio;
switch (formats[cam->format].pixelformat)
{
case V4L2_PIX_FMT_RGB565:
case V4L2_PIX_FMT_RGB24:
case V4L2_PIX_FMT_YUV420:
case V4L2_PIX_FMT_YUYV:
/* YCbCr 1 plane*/
printk("format V4L2_PIX_FMT_YUYV");
writel(img_buff[0].phy_base, cam->reg_base + S5PC100_CIOYSA1);//
 0xEE20_0000 + 0000_0018 
DMAY1 输出开始地址寄存器
            将配置好的 DMA 物理开始地址赋给上述寄存器                                                                       
/* CIPRTRGFMT. */
cfg = (2 << 29) | (cam->targetHsize << 16)| (cam->targetVsize << 0)|(1<<13)|(1<<14)|(1<<15);
将 cam 里已经初始化好的大小信息移位, 写入对应的位置
writel(cfg, cam->reg_base + S5PC100_CITRGFMT);    //
 0xEE20_0000 + 0000_0048 目标格式寄存器
/* CISCPRERATIO. */
calculate_prescaler_ratio_shift(cam->srcHsize, cam->targetHsize, &prescaler_h_ratio, &h_shift);//将源的横坐标进行压缩, 返回 压缩率和移位数
calculate_prescaler_ratio_shift(cam->srcVsize, cam->targetVsize, &prescaler_v_ratio, &v_shift);//将源的纵坐标进行压缩

main_h_ratio = (cam->srcHsize << 8) / (cam->targetHsize << h_shift);
main_v_ratio = (cam->srcVsize << 8) / (cam->targetVsize << v_shift);

cfg = ((10 - (h_shift + v_shift)) << 28) | (prescaler_h_ratio << 16) | (prescaler_v_ratio << 0);       //移位因子, 即共移位多少次
writel(cfg, cam->reg_base + S5PC100_CISCPRERATIO);//
 0xEE20_0000 + 0000_0050 缩放比例寄存器, 实现了图像的缩放处理

cfg = (cam->targetHsize << 16) | (cam->targetVsize << 0);
writel(cfg, cam->reg_base + S5PC100_CISCPREDST); //
 0xEE20_0000 + 0000_0054
最初的目的定位寄存器

cfg = (main_h_ratio << 16) | (main_v_ratio << 0);
writel(cfg, cam->reg_base + S5PC100_CISCCTRL); //main-scaler control Reg 的配置

cfg = cam->targetVsize * cam->targetHsize;        //长*宽, 0-27 位, 满足了
writel(cfg, cam->reg_base + S5PC100_CITAREA);//输出目标区域大小寄存器

cfg = (cam->targetVsize << 0) | (cam->targetHsize << 16);
writel(cfg, cam->reg_base + S5PC100_ORGOSIZE); //
0xEE20_0000 + 0000_0184 
DMA 图像开始坐标寄存器
break;

}
}
下面的函数的意思是: 传进来两个参数, 一个是源的大小, 另个是目的的大小, 如果源是目标的 64 倍以上就错了, 否则进行缩放, 即源的大小是目标的 32-64 倍之间, 就返回 ratio(缩放比例) 和 shift(2 的多少次幂), 缩放比例是 2 的多少次幂, 这样做的目的是方便移位, 因为移位都是 2 的倍数
int calculate_prescaler_ratio_shift(unsigned int SrcSize, unsigned int DstSize, unsigned int*ratio, unsigned int  *shift)
{
if(SrcSize>=64*DstSize) {
return -EINVAL;
}
else if(SrcSize>=32*DstSize) {
*ratio=32;
*shift=5;
}
else if(SrcSize>=16*DstSize) {
*ratio=16;
*shift=4;
}
else if(SrcSize>=8*DstSize) {
*ratio=8;
*shift=3;
}
else if(SrcSize>=4*DstSize) {
*ratio=4;
*shift=2;
}
else if(SrcSize>=2*DstSize) {
*ratio=2;
*shift=1;
}
else {
*ratio=1;
*shift=0;
} 
return 0;
}

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);

NO.4 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;iminor=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);

       }

……

}

rainbow

这个人很懒,什么都没留下

文章评论