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_REQBUFS、VIDIOC_QUERYBUF、VIDIOC_QBUF、VIDIOC_DQBUF等 IOCTL 命令来管理缓冲区,应用程序可以使用这些缓冲区来存储视频数据。 - 视频流控制:通过
VIDIOC_STREAMON和VIDIOC_STREAMOFF命令来启动或停止视频流的捕捉或播放。 v4l2_buffer结构体:用于描述视频帧的数据结构,包含缓冲区的状态、大小、内存位置等信息。
这部分功能使得视频设备能够高效地传输视频帧,保证了视频流的顺畅性和性能。
4. 视频格式与控制管理(Video Format & Control Management)
视频格式和控制管理部分处理与视频流格式、控制参数(如亮度、对比度、饱和度等)相关的操作。
- 视频格式管理:通过
VIDIOC_S_FMT和VIDIOC_G_FMT命令来设置和获取视频流的格式,支持各种分辨率、像素格式、帧率等。 - 控制接口:V4L2 支持对视频设备进行多种控制,例如调整亮度、对比度、饱和度等。通过
VIDIOC_S_CTRL和VIDIOC_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 用户空间访问摄像头的基本步骤
- 打开视频设备
- 查询设备能力
- 设置视频格式
- 请求缓冲区
- 映射缓冲区到用户空间
- 开始流
- 捕获视频帧
- 停止流
- 释放资源
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;
}
代码解释
-
打开设备: 使用
open()打开/dev/video0设备文件。 -
查询设备能力: 使用
VIDIOC_QUERYCAP获取设备的能力信息,确认设备是否支持视频捕捉和流功能。 -
设置视频格式: 使用
VIDIOC_S_FMT设置视频的分辨率(640x480)和像素格式(YUYV)。 -
请求缓冲区: 使用
VIDIOC_REQBUFS请求视频缓冲区,并通过VIDIOC_QUERYBUF查询每个缓冲区的参数。 -
开始视频流: 使用
VIDIOC_STREAMON启动视频流。 -
捕获视频帧: 使用
VIDIOC_DQBUF获取视频帧,通过缓冲区的映射获取图像数据,然后将缓冲区重新排队。 -
停止视频流和清理资源: 使用
VIDIOC_STREAMOFF停止视频流,并释放映射的缓冲区。
Linux V4L2框架详解与摄像头访问示例
2203

被折叠的 条评论
为什么被折叠?



