4. V4L2 核心 ioctl 操作:从查询到帧缓冲
好,咱们进入第四章。前面几章我们把 V4L2 的框架、设备节点、内存映射这些概念捋了一遍。这一章,我要带你亲手操作几个最核心的 ioctl 命令。说白了,这些就是驱动和应用程序之间的“握手协议”。你写任何一个 Camera 应用,都绕不开它们。
我个人习惯把这几个 ioctl 分成三组:查询能力、协商格式、流转起来。咱们一个一个来。
4.1 VIDIOC_QUERYCAP:先问问设备是什么来头
拿到一个 video 设备节点,别急着干活。先发一个 VIDIOC_QUERYCAP,看看它到底是不是摄像头,支持什么能力。这就像你面试一个人,先看简历,再问技术栈。
struct v4l2_capability cap;
int ret = ioctl(fd, VIDIOC_QUERYCAP, &cap);
if (ret < 0) {
perror("VIDIOC_QUERYCAP failed");
close(fd);
return -1;
}
// 检查是不是视频捕获设备
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
fprintf(stderr, "不是视频捕获设备\n");
close(fd);
return -1;
}
printf("驱动: %s\n", cap.driver);
printf("设备: %s\n", cap.card);
printf("总线: %s\n", cap.bus_info);
cap.driver 是内核驱动的名字,比如 uvcvideo 或者 ov5640。cap.card 是设备名,cap.bus_info 是总线位置。这三个字段,调试时特别有用。我记得有一次,客户说摄像头没图像,我一看 bus_info 发现插错了 USB 口——嗯,低级错误,但排查起来就是靠这个。
4.2 VIDIOC_ENUM_FMT:看看支持什么格式
知道设备是摄像头了,接下来要问:你支持哪些像素格式?是 YUYV、MJPEG 还是 NV12?这个用 VIDIOC_ENUM_FMT 来枚举。
struct v4l2_fmtdesc fmt;
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.index = 0; // 从0开始枚举
while (ioctl(fd, VIDIOC_ENUM_FMT, &fmt) == 0) {
printf("格式索引 %d: %s (fourcc: %c%c%c%c)\n",
fmt.index, fmt.description,
(fmt.pixelformat >> 0) & 0xFF,
(fmt.pixelformat >> 8) & 0xFF,
(fmt.pixelformat >> 16) & 0xFF,
(fmt.pixelformat >> 24) & 0xFF);
fmt.index++;
}
这里有个坑,我跟你讲一下。有些驱动会返回 V4L2_FMT_FLAG_COMPRESSED 标志,表示这是压缩格式(比如 MJPEG)。如果你要处理原始数据,记得避开压缩格式。我曾经在一个项目里,没检查这个标志,直接拿了 MJPEG 流当 YUV 处理,结果画面花得一塌糊涂。
| FourCC 码 | 描述 | 常见场景 |
|---|---|---|
| YUYV | YUV 4:2:2 打包格式 | USB 摄像头、嵌入式 sensor |
| MJPEG | Motion JPEG 压缩流 | 高分辨率 USB 摄像头 |
| NV12 | YUV 4:2:0 平面格式 | ISP 输出、硬件编码器输入 |
| RGB3 | RGB 24-bit 顺序 | 显示直通、截图 |
4.3 VIDIOC_S_FMT:谈好格式,定下规矩
枚举完了,选一个你想要的格式,用 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_YUYV;
fmt.fmt.pix.field = V4L2_FIELD_NONE;
if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) {
perror("VIDIOC_S_FMT failed");
return -1;
}
// 一定要读回来确认
printf("实际分辨率: %dx%d\n", fmt.fmt.pix.width, fmt.fmt.pix.height);
printf("实际格式: %c%c%c%c\n",
(fmt.fmt.pix.pixelformat >> 0) & 0xFF,
(fmt.fmt.pix.pixelformat >> 8) & 0xFF,
(fmt.fmt.pix.pixelformat >> 16) & 0xFF,
(fmt.fmt.pix.pixelformat >> 24) & 0xFF);
4.4 VIDIOC_REQBUFS:申请缓冲区,准备干活
格式谈好了,接下来要申请内存缓冲区。V4L2 支持三种内存类型:V4L2_MEMORY_MMAP(内核分配,用户映射)、V4L2_MEMORY_USERPTR(用户分配,内核使用)、V4L2_MEMORY_DMABUF(跨设备共享)。工程上最常用的是 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;
if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) {
perror("VIDIOC_REQBUFS failed");
return -1;
}
printf("实际分配的缓冲区数量: %d\n", req.count);
为什么是 4 个?太少容易丢帧,太多浪费内存。我个人习惯:嵌入式设备用 3~4 个,PC 端用 4~6 个。你想想看,如果只申请 2 个,应用处理慢一点,驱动就没地方放新数据了,只能丢帧。
申请完缓冲区,还要用 VIDIOC_QUERYBUF 查询每个缓冲区的物理信息,然后 mmap 映射到用户空间。这部分代码比较固定,我直接给个模板:
struct v4l2_buffer buf;
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_QUERYBUF, &buf);
// 映射到用户空间
buffers[i].start = mmap(NULL, buf.length,
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, buf.m.offset);
buffers[i].length = buf.length;
}
4.5 VIDIOC_QBUF / VIDIOC_DQBUF:让数据流转起来
缓冲区准备好了,但驱动还没开始往里面填数据。你需要先把缓冲区“入队”(QBUF),告诉驱动:这个缓冲区空着,你可以往里写数据了。然后调用 VIDIOC_STREAMON 启动流。
之后,在一个循环里:
- DQBUF:从驱动取回一个已经填满数据的缓冲区。
- 处理数据:比如显示、编码、保存。
- QBUF:处理完再还回去,让驱动继续用。
// 启动流之前,先把所有缓冲区入队
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);
}
// 启动视频流
int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd, VIDIOC_STREAMON, &type);
// 主循环:DQBUF → 处理 → QBUF
while (1) {
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0) {
perror("DQBUF failed");
break;
}
// 处理数据:buffers[buf.index].start 指向图像数据
process_image(buffers[buf.index].start, buf.bytesused);
// 处理完,重新入队
if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) {
perror("QBUF failed");
break;
}
}
open() 时加 O_NONBLOCK 标志,或者用 select() / poll() 监听文件描述符。我一般用 poll,超时 2 秒,这样既能及时响应,又不会卡死。
4.6 常见错误与调试思路
这几个 ioctl 调用,出错时 errno 会告诉你原因。我列几个常见的:
| errno | 含义 | 排查方向 |
|---|---|---|
| EBUSY | 设备忙,流已经在运行 | 检查是否重复调用了 STREAMON |
| EINVAL | 参数无效 | 检查 type、memory、pixelformat 是否匹配 |
| ENOMEM | 内存不足 | 减少缓冲区数量,或者检查系统内存 |
| EIO | I/O 错误 | 硬件问题,检查 sensor 供电、时钟、I2C |
嗯,说到 EIO,我想起一个案例。有一次调试一款 sensor,DQBUF 总是返回 EIO。查了半天,发现是 sensor 的复位引脚没拉高。硬件问题,软件怎么调都没用。所以,遇到 EIO,先检查硬件。