4. V4L2设备节点与IOCTL:/dev/videoX节点创建、核心ioctl命令详解
V4L2(Video for Linux 2)是Linux内核中为视频设备提供的标准驱动框架。在Camera HAL3架构中,HAL层通过操作V4L2设备节点与内核驱动交互,完成视频流的捕获与控制。本章将深入剖析V4L2设备节点的创建过程,并详细讲解最核心的ioctl命令,包括设备能力查询、格式枚举与设置、缓冲区管理以及流控制。
4.1 /dev/videoX设备节点创建机制
在Linux系统中,V4L2设备节点通常位于/dev/videoX(X为数字编号,如/dev/video0、/dev/video1)。这些节点由内核中的V4L2框架在驱动注册时动态创建,是用户空间程序与内核驱动通信的桥梁。
4.1.1 节点创建流程
- 驱动注册:摄像头驱动调用
video_register_device()函数向V4L2核心注册一个video_device结构体实例。 - 主设备号分配:V4L2核心使用主设备号81(
VIDEO_MAJOR),次设备号从0开始递增。 - udev规则触发:内核创建设备后,udev根据规则在
/dev/目录下生成videoX节点,并设置适当的权限(通常为crw-rw----,属主为root,属组为video)。 - 用户空间访问:应用程序通过
open("/dev/video0", O_RDWR)打开节点,获得文件描述符,后续通过ioctl进行控制。
mknod /dev/video0 c 81 0创建设备节点。主设备号81固定,次设备号与驱动注册顺序相关。
4.1.2 节点类型与功能
| 节点类型 | 典型路径 | 功能描述 |
|---|---|---|
| 视频捕获 | /dev/video0 |
从摄像头传感器获取视频帧数据 |
| 视频输出 | /dev/video1 |
向显示设备输出视频帧 |
| 视频元数据 | /dev/video2 |
传输3A统计信息、ISP参数等元数据 |
| 视频覆盖 | /dev/video3 |
硬件叠加层,用于预览合成 |
/dev/videoX节点。例如,高通平台通常使用/dev/video0作为主图像节点,/dev/video1作为统计信息节点。HAL层需要根据平台配置正确绑定这些节点。
4.2 核心ioctl命令详解
ioctl(Input/Output Control)是V4L2框架中用户空间与内核驱动通信的主要方式。每个ioctl命令对应一个特定的控制操作,通过ioctl(fd, VIDIOC_XXX, &arg)调用。下面按照典型的V4L2初始化流程,逐一讲解最核心的ioctl命令。
4.2.1 查询设备能力:VIDIOC_QUERYCAP
这是打开设备节点后第一个必须调用的ioctl,用于获取设备的基本能力信息,包括驱动名称、设备类型、总线信息以及支持的I/O方法。
struct v4l2_capability cap;
int ret = ioctl(fd, VIDIOC_QUERYCAP, &cap);
if (ret < 0) {
// 处理错误
}
// 检查设备类型:必须支持视频捕获
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
// 不是捕获设备
}
// 检查是否支持流式I/O(mmap模式)
if (!(cap.capabilities & V4L2_CAP_STREAMING)) {
// 不支持流式I/O,可能需要使用read/write
}
关键字段说明:
driver:驱动名称,如"uvcvideo"、"msm_camera"。card:设备名称,如"UVC Camera (046d:0825)"。bus_info:总线位置,如"usb-0000:00:14.0-1"。capabilities:位掩码,标识设备支持的功能(捕获、输出、调谐器、流式I/O等)。
4.2.2 枚举与设置格式:VIDIOC_ENUM_FMT / VIDIOC_S_FMT
在确认设备能力后,下一步是枚举设备支持的像素格式,并设置当前使用的格式。
枚举格式:VIDIOC_ENUM_FMT
struct v4l2_fmtdesc fmt_desc;
fmt_desc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt_desc.index = 0; // 从0开始递增
while (ioctl(fd, VIDIOC_ENUM_FMT, &fmt_desc) == 0) {
printf("Format index %d: %s (fourcc: %c%c%c%c)\n",
fmt_desc.index, fmt_desc.description,
(fmt_desc.pixelformat >> 0) & 0xFF,
(fmt_desc.pixelformat >> 8) & 0xFF,
(fmt_desc.pixelformat >> 16) & 0xFF,
(fmt_desc.pixelformat >> 24) & 0xFF);
fmt_desc.index++;
}
设置格式:VIDIOC_S_FMT
struct v4l2_format fmt;
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 1920;
fmt.fmt.pix.height = 1080;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_NV12; // YUV 4:2:0 半平面
fmt.fmt.pix.field = V4L2_FIELD_NONE;
int ret = ioctl(fd, VIDIOC_S_FMT, &fmt);
if (ret < 0) {
// 格式不支持,驱动会调整到最接近的格式
}
// 实际设置的格式可能不同于请求值,需要读取fmt结构体确认
printf("Actual resolution: %dx%d\n", fmt.fmt.pix.width, fmt.fmt.pix.height);
VIDIOC_S_FMT后,驱动可能会修改width、height和pixelformat字段。应用程序必须读取返回的结构体,使用实际值进行后续操作。常见调整包括:将宽度对齐到16像素、将不支持的格式转换为默认格式等。
4.2.3 请求缓冲区:VIDIOC_REQBUFS
设置格式后,需要向驱动请求用于视频帧传输的缓冲区。V4L2支持多种I/O模式,最常用的是mmap模式。
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; // 使用mmap模式
int ret = ioctl(fd, VIDIOC_REQBUFS, &req);
if (ret < 0) {
// 错误处理
}
// 实际分配的缓冲区数量可能少于请求值
printf("Allocated %d buffers\n", req.count);
4.2.4 查询与映射缓冲区:VIDIOC_QUERYBUF
缓冲区分配完成后,需要查询每个缓冲区的物理地址和大小,并通过mmap映射到用户空间。
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = 0; // 查询第0个缓冲区
ioctl(fd, VIDIOC_QUERYBUF, &buf);
// 映射缓冲区到用户空间
void *buffer = mmap(NULL, buf.length, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, buf.m.offset);
// 保存buffer地址和长度,用于后续帧处理
4.2.5 入队与出队缓冲区:VIDIOC_QBUF / VIDIOC_DQBUF
这是视频流传输的核心操作,构成一个生产者-消费者循环。
- VIDIOC_QBUF:将缓冲区放入驱动队列,等待硬件填充数据。
- VIDIOC_DQBUF:从驱动队列取出已填充数据的缓冲区。
// 入队所有缓冲区
for (int i = 0; i < req.count; 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;
ioctl(fd, VIDIOC_QBUF, &buf);
}
// 开始流传输后,循环出队处理
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
ioctl(fd, VIDIOC_DQBUF, &buf); // 阻塞等待帧数据
// 处理buf.index对应的缓冲区数据
process_frame(buffers[buf.index], buf.bytesused);
// 处理完成后重新入队
ioctl(fd, VIDIOC_QBUF, &buf);
4.2.6 流控制:VIDIOC_STREAMON / VIDIOC_STREAMOFF
这两个命令控制视频流的启动和停止。
int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
// 启动流
ioctl(fd, VIDIOC_STREAMON, &type);
// 停止流
ioctl(fd, VIDIOC_STREAMOFF, &type);
VIDIOC_STREAMOFF后,驱动会丢弃所有已入队但未处理的缓冲区。应用程序需要重新调用VIDIOC_QBUF入队所有缓冲区,才能再次启动流。
4.3 V4L2初始化完整流程
下面的SVG图展示了从打开设备节点到开始视频流捕获的完整初始化流程,以及每个步骤对应的核心ioctl命令。
4.4 常用ioctl命令速查表
| ioctl命令 | 功能 | 调用时机 | 关键结构体 |
|---|---|---|---|
VIDIOC_QUERYCAP |
查询设备能力 | 打开设备后立即调用 | v4l2_capability |
VIDIOC_ENUM_FMT |
枚举支持的像素格式 | 查询能力之后 | v4l2_fmtdesc |
VIDIOC_S_FMT |
设置当前格式 | 枚举格式之后 | v4l2_format |
VIDIOC_G_FMT |
获取当前格式 | 任意时刻 | v4l2_format |
VIDIOC_REQBUFS |
请求分配缓冲区 | 设置格式之后 | v4l2_requestbuffers |
VIDIOC_QUERYBUF |
查询缓冲区信息 | 请求缓冲区之后 | v4l2_buffer |
VIDIOC_QBUF |
入队缓冲区 | 映射缓冲区后、流启动前 | v4l2_buffer |
VIDIOC_DQBUF |
出队缓冲区 | 流启动后循环调用 | v4l2_buffer |
VIDIOC_STREAMON |
启动视频流 | 所有缓冲区入队后 | int(缓冲区类型) |
VIDIOC_STREAMOFF |
停止视频流 | 任意时刻 | int(缓冲区类型) |
4.5 实战:V4L2初始化代码框架
下面是一个完整的V4L2初始化代码框架,整合了上述所有核心ioctl命令,展示了从打开设备到开始流捕获的标准流程。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/videodev2.h>
#define DEVICE "/dev/video0"
#define WIDTH 1920
#define HEIGHT 1080
#define BUFFER_COUNT 4
struct buffer {
void *start;
size_t length;
};
int main() {
int fd;
struct v4l2_capability cap;
struct v4l2_format fmt;
struct v4l2_requestbuffers req;
struct v4l2_buffer buf;
struct buffer buffers[BUFFER_COUNT];
int ret;
// 1. 打开设备
fd = open(DEVICE, O_RDWR);
if (fd < 0) {
perror("open");
return -1;
}
// 2. 查询设备能力
ret = ioctl(fd, VIDIOC_QUERYCAP, &cap);
if (ret < 0 || !(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
fprintf(stderr, "Not a capture device\n");
close(fd);
return -1;
}
// 3. 设置格式
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = WIDTH;
fmt.fmt.pix.height = HEIGHT;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_NV12;
fmt.fmt.pix.field = V4L2_FIELD_NONE;
ret = ioctl(fd, VIDIOC_S_FMT, &fmt);
if (ret < 0) {
perror("VIDIOC_S_FMT");
close(fd);
return -1;
}
// 4. 请求缓冲区
memset(&req, 0, sizeof(req));
req.count = BUFFER_COUNT;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
ret = ioctl(fd, VIDIOC_REQBUFS, &req);
if (ret < 0) {
perror("VIDIOC_REQBUFS");
close(fd);
return -1;
}
// 5. 查询并映射缓冲区
for (int i = 0; i < req.count; i++) {
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
ret = ioctl(fd, VIDIOC_QUERYBUF, &buf);
if (ret < 0) {
perror("VIDIOC_QUERYBUF");
close(fd);
return -1;
}
buffers[i].length = buf.length;
buffers[i].start = mmap(NULL, buf.length,
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, buf.m.offset);
if (buffers[i].start == MAP_FAILED) {
perror("mmap");
close(fd);
return -1;
}
}
// 6. 入队所有缓冲区
for (int i = 0; i < req.count; i++) {
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
ioctl(fd, VIDIOC_QBUF, &buf);
}
// 7. 启动流
int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(fd, VIDIOC_STREAMON, &type);
if (ret < 0) {
perror("VIDIOC_STREAMON");
close(fd);
return -1;
}
printf("V4L2 initialized successfully. Capturing %dx%d NV12...\n",
fmt.fmt.pix.width, fmt.fmt.pix.height);
// 8. 帧捕获循环(示例:捕获10帧)
for (int frame = 0; frame < 10; frame++) {
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
ret = ioctl(fd, VIDIOC_DQBUF, &buf);
if (ret < 0) {
perror("VIDIOC_DQBUF");
break;
}
// 处理帧数据:buffers[buf.index].start, buf.bytesused
printf("Frame %d captured, size: %d bytes\n",
frame, buf.bytesused);
// 重新入队
ioctl(fd, VIDIOC_QBUF, &buf);
}
// 9. 停止流
ioctl(fd, VIDIOC_STREAMOFF, &type);
// 10. 清理
for (int i = 0; i < req.count; i++) {
munmap(buffers[i].start, buffers[i].length);
}
close(fd);
return 0;
}
- 每个ioctl调用后必须检查返回值,驱动可能调整参数。
VIDIOC_S_FMT后读取实际格式,不要假设请求值被完全接受。- 缓冲区映射使用
MAP_SHARED标志,确保内核与用户空间共享同一物理内存。 - 流停止后,所有缓冲区状态被重置,需要重新入队才能再次启动。
- 在Camera HAL3中,上述流程通常由HAL层的
configureStreams()和processCaptureRequest()方法触发。
4.6 常见问题与调试技巧
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
VIDIOC_QUERYCAP返回ENOTTY |
设备节点不是V4L2设备 | 检查/dev/videoX主设备号是否为81 |
VIDIOC_S_FMT返回EINVAL |
格式或分辨率不支持 | 先调用VIDIOC_ENUM_FMT和VIDIOC_ENUM_FRAMESIZES确认支持列表 |
VIDIOC_REQBUFS返回EBUSY |
流正在运行或缓冲区已分配 | 先调用VIDIOC_STREAMOFF,或检查是否重复调用 |
VIDIOC_DQBUF阻塞超时 |
驱动未产生帧数据 | 检查传感器时钟、MIPI链路、ISP配置;使用O_NONBLOCK打开设备测试 |
mmap返回ENODEV |
设备不支持mmap模式 | 检查cap.capabilities是否包含V4L2_CAP_STREAMING |
v4l2-ctl工具可以快速验证设备节点是否正常工作。例如:v4l2-ctl -d /dev/video0 --list-formats枚举格式,v4l2-ctl -d /dev/video0 --set-fmt-video=width=1920,height=1080,pixelformat=NV12设置格式。这些工具底层使用的正是本章讲解的ioctl命令。