Linux V4l2子系统分析5

Linux V4L2框架详解与摄像头访问示例

Linux V4l2子系统分析5(基于Linux6.6)---总结&实例

一、详解V4L2框架

V4L2 核心代码大致可以分为以下四类:

1. V4L2 核心 API 与接口(Core API & Interface)

这一部分主要包括与 V4L2 相关的基本接口和数据结构,提供了用户空间和内核空间之间的通信接口。这些接口是应用程序访问视频设备的基础。

  • v4l2-device.h:定义了 V4L2 设备的相关数据结构和常用宏。
  • v4l2-ioctl.c:实现了 V4L2 的核心 IO 控制命令(IOCTL)处理。V4L2 设备通过 IOCTL 接口与用户空间交互,执行设备查询、控制等操作。
  • v4l2-core.c:包含 V4L2 核心的设备注册、注销等功能,提供了对 V4L2 设备的统一管理。

V4L2 核心 API 通常涉及设备能力查询(VIDIOC_QUERYCAP)、设置视频格式(VIDIOC_S_FMT)、获取视频帧(VIDIOC_STREAMON)等操作。

2. 视频设备管理(Video Device Management)

这部分主要负责视频设备的管理与注册,是整个 V4L2 系统的基础。

  • 设备注册:通过 video_device_alloc()video_register_device() 等函数将视频设备注册到 V4L2 框架中。
  • video_device 结构体:该结构体用于描述视频设备,它包含了设备的基本信息(如名称、设备类型)、视频操作函数、以及设备状态等。
  • 设备释放:通过 video_device_release() 进行设备的注销和内存释放。

这部分代码保证了 V4L2 设备的注册、注销和操作能够正确地处理不同的设备类型,确保设备与内核之间的协作。

3. 视频流与缓冲区管理(Video Streaming & Buffer Management)

视频流和缓冲区管理部分处理的是视频数据的获取、处理与传输。

  • 缓冲区管理:V4L2 通过 VIDIOC_REQBUFSVIDIOC_QUERYBUFVIDIOC_QBUFVIDIOC_DQBUF 等 IOCTL 命令来管理缓冲区,应用程序可以使用这些缓冲区来存储视频数据。
  • 视频流控制:通过 VIDIOC_STREAMONVIDIOC_STREAMOFF 命令来启动或停止视频流的捕捉或播放。
  • v4l2_buffer 结构体:用于描述视频帧的数据结构,包含缓冲区的状态、大小、内存位置等信息。

这部分功能使得视频设备能够高效地传输视频帧,保证了视频流的顺畅性和性能。

4. 视频格式与控制管理(Video Format & Control Management)

视频格式和控制管理部分处理与视频流格式、控制参数(如亮度、对比度、饱和度等)相关的操作。

  • 视频格式管理:通过 VIDIOC_S_FMTVIDIOC_G_FMT 命令来设置和获取视频流的格式,支持各种分辨率、像素格式、帧率等。
  • 控制接口:V4L2 支持对视频设备进行多种控制,例如调整亮度、对比度、饱和度等。通过 VIDIOC_S_CTRLVIDIOC_G_CTRL 命令进行设备控制。
  • v4l2_format 结构体:定义了视频流的格式,包括像素格式、分辨率、图像大小等信息。

1.1、V4L2基础框架

上图V4L2框架是一个标准的树形结构,v4l2_device充当了父设备,通过链表把所有注册到其下的子设备管理起来,这些设备可以是GRABBER、VBI或RADIO。V4l2_subdev是子设备,v4l2_subdev结构体包含了对设备操作的ops和ctrls,这部分代码和硬件相关,需要驱动工程师根据硬件实现控制上下电、读取ID、饱和度、对比度和视频数据流打开关闭等接口函数。Video_device用于创建子设备节点,把操作设备的接口暴露给用户空间。V4l2_fh是每个子设备的文件句柄,在打开设备节点文件时设置,方便上层索引到v4l2_ctrl_handler,v4l2_ctrl_handler管理设备的ctrls,这些ctrls(摄像头设备)包括调节饱和度、对比度和白平衡等。

结构体v4l2_device、video_device、v4l2_subdev和v4l2_ctrl_handler是构成框架的主要元素,现分别介绍:

 1. struct v4l2_device :
    v4l2_device在v4l2框架中充当所有v4l2_subdev的父设备,管理着注册在其下的子设备
    
struct v4l2_device {
    structlist_head subdevs;  //用链表管理注册的subdev
    charname[V4L2_DEVICE_NAME_SIZE];  //device 名字
    structkref ref;  //引用计数
    .........
};

    可以看出v4l2_device的主要作用是管理注册在其下的子设备,方便系统查找引用到。
v4l2_device的注册和注销:
    int v4l2_device_register(struct device*dev, struct v4l2_device *v4l2_dev)
    static void v4l2_device_release(struct kref *ref)

2. struct v4l2_subdev :
    v4l2_subdev代表子设备,包含了子设备的相关属性和操作。结构体原型:

struct v4l2_subdev {
    struct v4l2_device *v4l2_dev;  //指向父设备
    conststruct v4l2_subdev_ops *ops; //提供一些控制v4l2设备的接口
    conststruct v4l2_subdev_internal_ops *internal_ops; //向V4L2框架提供的接口函数
    structv4l2_ctrl_handler *ctrl_handler; //subdev控制接口
    charname[V4L2_SUBDEV_NAME_SIZE]; 
    struct video_device *devnode;  
    ..........
};

    每个子设备驱动都需要实现一个v4l2_subdev结构体,v4l2_subdev可以内嵌到其它结构体中,也可以独立使用。
    结构体中包含了对子设备操作的成员v4l2_subdev_ops和v4l2_subdev_internal_ops
            struct v4l2_subdev_ops {
                const struct v4l2_subdev_core_ops *core; //视频设备通用的操作:初始化、加载FW、上电和RESET等
                const struct v4l2_subdev_tuner_ops *tuner; //tuner特有的操作
                const struct v4l2_subdev_audio_ops *audio; //audio特有的操作
                const struct v4l2_subdev_video_ops *video; //视频设备的特有操作:裁剪图像、开关视频流等
                const struct v4l2_subdev_pad_ops *pad;
                ..........
            };
            struct v4l2_subdev_internal_ops {
                /* 当subdev注册时被调用,读取IC的ID来进行识别 */
                int(*registered)(struct v4l2_subdev *sd);
                void(*unregistered)(struct v4l2_subdev *sd);
                /* 当设备节点被打开时调用,通常会给设备上电和设置视频捕捉FMT */
                int(*open)(struct v4l2_subdev *sd, struct v4l2_subdev_fh *fh);
                int(*close)(struct v4l2_subdev *sd, struct v4l2_subdev_fh *fh);
            };

    视频设备通常需要实现core和video成员,这两个OPS中的操作都是可选的,但是对于视频流设备video->s_stream(开启或关闭流IO)必须要实现。v4l2_subdev_internal_ops是向V4L2框架提供的接口,只能被V4L2框架层调用。在注册或打开子设备时,进行一些辅助性操作。
    Subdev的注册和注销:
                        int v4l2_device_register_subdev(struct v4l2_device *v4l2_dev, struct v4l2_subdev *sd)
                        void v4l2_device_unregister_subdev(struct v4l2_subdev *sd)


3. struct video_device
    video_device结构体用于在/dev目录下生成设备节点文件,把操作设备的接口暴露给用户空间

struct video_device
{
    const struct v4l2_file_operations *fops;  //V4L2设备操作集合
    struct cdev *cdev; //字符设备

    struct v4l2_device *v4l2_dev;
    struct v4l2_ctrl_handler *ctrl_handler;

    struct vb2_queue *queue; //指向video buffer队列
    int vfl_type;      /* device type */

    intminor;  //次设备号

    /*ioctl回调函数集,提供file_operations中的ioctl调用 */
    const struct v4l2_ioctl_ops *ioctl_ops;
    ..........
};
    
    Video_device分配和释放, 用于分配和释放video_device结构体:
        struct video_device *video_device_alloc(void)
        void video_device_release(struct video_device *vdev)
    video_device注册和注销,实现video_device结构体的相关成员后,就可以调用下面的接口进行注册:
        static inline int __must_check video_register_device(struct video_device *vdev, inttype, int nr)
        void video_unregister_device(struct video_device*vdev);
            vdev:需要注册和注销的video_device;
            type:设备类型,包括VFL_TYPE_GRABBER、VFL_TYPE_VBI、VFL_TYPE_RADIO和VFL_TYPE_SUBDEV。
            nr:设备节点名编号,如/dev/video[nr]。

4. struct v4l2_ctrl_handler
    v4l2_ctrl_handler是用于保存子设备控制方法集的结构体,结构体如下: 
struct v4l2_ctrl_handler {
    struct list_head ctrls;
    struct list_head ctrl_refs;
    struct v4l2_ctrl_ref *cached;
    struct v4l2_ctrl_ref **buckets;
    v4l2_ctrl_notify_fnc notify;
    u16 nr_of_buckets;
    int error;
};

    其中成员ctrls作为链表存储包括设置亮度、饱和度、对比度和清晰度等方法,可以通过v4l2_ctrl_new_xxx()函数创建具体方法并添加到链表ctrls。

1.2、videobuf管理

在讲解v4l2的buffer管理前,先介绍v4l2的IO访问, V4L2支持三种不同IO访问方式:

1.read和write:是基本帧IO访问方式,通过read读取每一帧数据,数据需要在内核和用户之间拷贝,这种方式访问速度可能会非常慢;

2.内存映射缓冲区(V4L2_MEMORY_MMAP):是在内核空间开辟缓冲区,应用通过mmap()系统调用映射到用户地址空间。这些缓冲区可以是大而连续DMA缓冲区、通过vmalloc()创建的虚拟缓冲区,或者直接在设备的IO内存中开辟的缓冲区(如果硬件支持);

3.用户空间缓冲区(V4L2_MEMORY_USERPTR):是用户空间的应用中开辟缓冲区,用户与内核空间之间交换缓冲区指针。很明显,在这种情况下是不需要mmap()调用的,但驱动为有效的支持用户空间缓冲区,其工作将也会更困难。

read和write方式属于帧IO访问方式,每一帧都要通过IO操作,需要用户和内核之间数据拷贝,而后两种是流IO访问方式,不需要内存拷贝,访问速度比较快。内存映射缓冲区访问方式是比较常用的方式。

现以V4L2_MEMORY_MMAP简单介绍数据流通过程:

Camera sensor捕捉到图像数据通过并口或MIPI传输到CAMIF(camera interface),CAMIF可以对图像数据进行调整(翻转、裁剪和格式转换等)。然后DMA控制器设置DMA通道请求AHB将图像数据传到分配好的DMA缓冲区。待图像数据传输到DMA缓冲区之后,mmap操作把缓冲区映射到用户空间,应用就可以直接访问缓冲区的数据。而为了使设备支持流IO这种方式,v4l2需要实现对video buffer的管理,即实现:

 /* vb2_queue代表一个videobuffer队列,vb2_buffer是这个队列中的成员,vb2_mem_ops是缓冲内存的操作函数集,vb2_ops用来管理队列 */
struct vb2_queue {
    enum v4l2_buf_type type;  //buffer类型
    unsigned int io_modes;  //访问IO的方式:mmap、userptr etc
    const struct vb2_ops *ops;  //buffer队列操作函数集合
    const struct vb2_mem_ops *mem_ops;  //buffer memory操作集合
    struct vb2_buffer *bufs[VIDEO_MAX_FRAME];  //代表每个frame buffer
    unsignedint num_buffers;  //分配的buffer个数
    ..........
};


/* vb2_mem_ops包含了内存映射缓冲区、用户空间缓冲区的内存操作方法 */
struct vb2_mem_ops {
    void *(*alloc)(void *alloc_ctx, unsignedlong size);  //分配视频缓存
    void (*put)(void *buf_priv);  //释放视频缓存

    /* 获取用户空间视频缓冲区指针 */
    void *(*get_userptr)(void *alloc_ctx, unsigned long vaddr, unsignedlong size, int write);
    void (*put_userptr)(void *buf_priv);  //释放用户空间视频缓冲区指针
    /* 用于缓存同步 */
    void (*prepare)(void *buf_priv);
    void (*finish)(void *buf_priv);
    /* 缓存虚拟地址 & 物理地址 */
    void *(*vaddr)(void *buf_priv);
    void *(*cookie)(void *buf_priv);

    unsignedint (*num_users)(void *buf_priv);  //返回当期在用户空间的buffer数
    int (*mmap)(void *buf_priv, structvm_area_struct *vma);  //把缓冲区映射到用户空间
    ..............
};

/* mem_ops由kernel自身实现并提供了三种类型的视频缓存区操作方法:连续的DMA缓冲区、集散的DMA缓冲区以及vmalloc创建的缓冲区,分别由videobuf2-dma-contig.c、videobuf2-dma-sg.c和videobuf-vmalloc.c文件实现,可以根据实际情况来使用。*/


/* vb2_ops是用来管理buffer队列的函数集合,包括队列和缓冲区初始化等 */
struct vb2_ops {
    //队列初始化
    int(*queue_setup)(struct vb2_queue *q, const struct v4l2_format *fmt,
                       unsigned int *num_buffers, unsigned int*num_planes,
                       unsigned int sizes[], void *alloc_ctxs[]);

    //释放和获取设备操作锁
    void(*wait_prepare)(struct vb2_queue *q);
    void(*wait_finish)(struct vb2_queue *q);

    //对buffer的操作
    int(*buf_init)(struct vb2_buffer *vb);
    int(*buf_prepare)(struct vb2_buffer *vb);
    int(*buf_finish)(struct vb2_buffer *vb);
    void(*buf_cleanup)(struct vb2_buffer *vb);

    //开始/停止视频流
    int(*start_streaming)(struct vb2_queue *q, unsigned int count);
    int(*stop_streaming)(struct vb2_queue *q);

    //把VB传递给驱动,以填充frame数据
    void(*buf_queue)(struct vb2_buffer *vb);
};

一个frame buffer(vb2_buffer/v4l2_buffer)可以有三种状态:

1. 在驱动的输入队列中,驱动程序将会对此队列中的缓冲区进行处理,用户空间通过IOCTL:VIDIOC_QBUF 把缓冲区放入到队列。对于一个视频捕获设备,传入队列中的缓冲区是空的,驱动会往其中填充数据;

2. 在驱动的输出队列中,这些缓冲区已由驱动处理过,对于一个视频捕获设备,缓存区已经填充了视频数据,正等用户空间来认领;

3. 用户空间状态的队列,已经通过IOCTL:VIDIOC_DQBUF传出到用户空间的缓冲区,此时缓冲区由用户空 间拥有,驱动无法访问。

这三种状态的切换如下图所示:

                 

最终落脚点的struct v4l2_buffer结构如下:

 struct v4l2_buffer {
    __u32 index;  //buffer 序号
    __u32 type;  //buffer类型
    __u32 bytesused;  //缓冲区已使用byte数
    structtimeval timestamp;  //时间戳,代表帧捕获的时间

    __u32 memory;  //表示缓冲区是内存映射缓冲区还是用户空间缓冲区
    union {
        __u32 offset;  //内核缓冲区的位置
        unsignedlong userptr;   //缓冲区的用户空间地址
        structv4l2_plane *planes;
        __s32 fd;
    } m;
    __u32 length;   //缓冲区大小,单位byte
};

当用户空间拿到v4l2_buffer,可以获取到缓冲区的相关信息。Byteused是图像数据所占的字节数,如果是V4L2_MEMORY_MMAP方式,m.offset是内核空间图像数据存放的开始地址,会传递给mmap函数作为一个偏移,通过mmap映射返回一个缓冲区指针p,p+byteused是图像数据在进程的虚拟地址空间所占区域;如果是用户指针缓冲区的方式,可以获取的图像数据开始地址的指针m.userptr,userptr是一个用户空间的指针,userptr+byteused便是所占的虚拟地址空间,应用可以直接访问。

1.3、Ioctl框架

用户空间通过打开/dev/目录下的设备节点,获取到文件的file结构体,通过系统调用ioctl把cmd和arg传入到内核。通过一系列的调用后最终会调用到__video_do_ioctl函数,然后通过cmd检索v4l2_ioctls[],判断是INFO_FL_STD还是INFO_FL_FUNC。如果是INFO_FL_STD会直接调用到视频设备驱动中video_device->v4l2_ioctl_ops函数集。如果是INFO_FL_FUNC会先调用到v4l2自己实现的标准回调函数,然后根据arg再调用到video_device->v4l2_ioctl_ops或v4l2_fh->v4l2_ctrl_handler函数集。

二、用户空间访问 camera & 示例程序

2.1、V4L2 用户空间访问摄像头的基本步骤

  1. 打开视频设备
  2. 查询设备能力
  3. 设置视频格式
  4. 请求缓冲区
  5. 映射缓冲区到用户空间
  6. 开始流
  7. 捕获视频帧
  8. 停止流
  9. 释放资源

2.2、示例程序

以下是一个简化的 C 语言示例,展示了如何使用 V4L2 API 来打开摄像头设备、设置格式、捕获视频帧并显示图像。

依赖库

首先,确保你的系统上已经安装了必要的开发库。你需要安装 libv4l-dev(V4L2 的开发库):

sudo apt-get install libv4l-dev

示例代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <errno.h>
#include <string.h>
#include <assert.h>
#include <linux/videodev2.h>

#define DEVICE "/dev/video0"  // 摄像头设备

// 缓冲区结构体
struct buffer {
    void *start;
    size_t length;
};

// 全局变量
static int fd = -1;  // 摄像头文件描述符
static struct buffer *buffers = NULL;  // 缓冲区数组
static unsigned int n_buffers = 0;  // 缓冲区数量

// 错误处理函数
void xioctl(int fh, int request, void *arg) {
    if (-1 == ioctl(fh, request, arg)) {
        fprintf(stderr, "ioctl失败: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }
}

// 打开视频设备
int open_device(const char *dev_name) {
    fd = open(dev_name, O_RDWR | O_NONBLOCK, 0);
    if (fd == -1) {
        perror("无法打开设备");
        return -1;
    }
    return 0;
}

// 查询设备的能力
void query_capabilities() {
    struct v4l2_capability cap;
    xioctl(fd, VIDIOC_QUERYCAP, &cap);

    if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
        fprintf(stderr, "设备不支持视频捕捉功能\n");
        exit(EXIT_FAILURE);
    }
    if (!(cap.capabilities & V4L2_CAP_STREAMING)) {
        fprintf(stderr, "设备不支持流功能\n");
        exit(EXIT_FAILURE);
    }
}

// 设置视频格式
void set_format() {
    struct v4l2_format fmt;
    memset(&fmt, 0, sizeof(fmt));
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width = 640;
    fmt.fmt.pix.height = 480;
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;  // 使用YUYV像素格式
    fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;

    xioctl(fd, VIDIOC_S_FMT, &fmt);
}

// 请求缓冲区
void request_buffers() {
    struct v4l2_requestbuffers req;
    memset(&req, 0, sizeof(req));
    req.count = 4;  // 请求4个缓冲区
    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory = V4L2_MEMORY_MMAP;

    xioctl(fd, VIDIOC_REQBUFS, &req);

    buffers = calloc(req.count, sizeof(*buffers));
    if (!buffers) {
        perror("分配缓冲区失败");
        exit(EXIT_FAILURE);
    }

    for (n_buffers = 0; n_buffers < req.count; ++n_buffers) {
        struct v4l2_buffer buf;
        memset(&buf, 0, sizeof(buf));
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index = n_buffers;

        xioctl(fd, VIDIOC_QUERYBUF, &buf);

        buffers[n_buffers].length = buf.length;
        buffers[n_buffers].start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset);
        if (MAP_FAILED == buffers[n_buffers].start) {
            perror("映射缓冲区失败");
            exit(EXIT_FAILURE);
        }
    }
}

// 启动视频流
void start_capturing() {
    for (unsigned int i = 0; i < n_buffers; ++i) {
        struct v4l2_buffer buf;
        memset(&buf, 0, sizeof(buf));
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index = i;

        xioctl(fd, VIDIOC_QBUF, &buf);
    }

    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    xioctl(fd, VIDIOC_STREAMON, &type);
}

// 捕获一帧
void capture_frame() {
    struct v4l2_buffer buf;
    memset(&buf, 0, sizeof(buf));
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;

    xioctl(fd, VIDIOC_DQBUF, &buf);

    printf("捕获到一帧,大小:%zu\n", buf.bytesused);

    // 处理图像(例如保存为文件,显示等)

    xioctl(fd, VIDIOC_QBUF, &buf);
}

// 停止流
void stop_capturing() {
    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    xioctl(fd, VIDIOC_STREAMOFF, &type);
}

// 释放资源
void uninit_device() {
    for (unsigned int i = 0; i < n_buffers; ++i) {
        munmap(buffers[i].start, buffers[i].length);
    }
    free(buffers);
}

// 关闭设备
void close_device() {
    close(fd);
}

int main() {
    if (open_device(DEVICE) == -1) {
        return EXIT_FAILURE;
    }

    query_capabilities();
    set_format();
    request_buffers();
    start_capturing();

    for (int i = 0; i < 10; ++i) {
        capture_frame();
        usleep(100000);  // 每0.1秒捕获一帧
    }

    stop_capturing();
    uninit_device();
    close_device();

    return 0;
}

代码解释

  1. 打开设备: 使用 open() 打开 /dev/video0 设备文件。

  2. 查询设备能力: 使用 VIDIOC_QUERYCAP 获取设备的能力信息,确认设备是否支持视频捕捉和流功能。

  3. 设置视频格式: 使用 VIDIOC_S_FMT 设置视频的分辨率(640x480)和像素格式(YUYV)。

  4. 请求缓冲区: 使用 VIDIOC_REQBUFS 请求视频缓冲区,并通过 VIDIOC_QUERYBUF 查询每个缓冲区的参数。

  5. 开始视频流: 使用 VIDIOC_STREAMON 启动视频流。

  6. 捕获视频帧: 使用 VIDIOC_DQBUF 获取视频帧,通过缓冲区的映射获取图像数据,然后将缓冲区重新排队。

  7. 停止视频流和清理资源: 使用 VIDIOC_STREAMOFF 停止视频流,并释放映射的缓冲区。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值