第4章:V4L2框架入门:Video for Linux 2,设备节点与ioctl

好,咱们今天聊聊V4L2。说实话,这个框架在Linux多媒体子系统里,地位就跟文件系统里的VFS差不多。你写Camera驱动,绕不开它。

V4L2的全称是Video for Linux 2,是第二代视频处理框架。第一代V4L太老了,基本没人用了。V4L2从Linux 2.5内核开始引入,一直用到现在,生命力极强。

我个人习惯把V4L2理解成「摄像头领域的统一接口规范」。不管你是USB摄像头、MIPI CSI摄像头,还是PCIe采集卡,到了应用层,操作方式都一样——打开设备节点,发ioctl命令,拿数据。

4.1 设备节点:用户态看到的「摄像头」

你在Linux系统里插上一个摄像头,会看到什么?

ls -l /dev/video*
crw-rw---- 1 root video 81, 0 Jan 1 00:00 /dev/video0
crw-rw---- 1 root video 81, 1 Jan 1 00:00 /dev/video1

对,就是/dev/videoX。主设备号81,次设备号从0开始。

这里有个坑,我刚开始做驱动时踩过——不是每个video节点都能采集图像。有些节点是metadata节点,有些是输出节点。怎么区分?看功能。

设备节点类型 主功能 典型用途
/dev/videoX 视频采集/输出 摄像头数据流、显示输出
/dev/vbiX VBI数据 电视卡图文信息
/dev/radioX 收音机 FM调频
/dev/mediaX Media Controller 拓扑管理、路由配置

嗯,这里要注意:一个物理摄像头可能对应多个video节点。比如有些SoC的ISP会分出raw节点和processed节点。你选错了,拿到的数据就不对。

避坑指南:我曾经在一个项目中,应用层工程师一直抱怨图像颜色不对。查了两天,发现他打开的是/dev/video2,而真正的图像数据在/dev/video0。所以,写应用层代码前,先用v4l2-ctl --list-devices确认一下。

4.2 ioctl:驱动与应用的「对话协议」

设备节点只是个入口。真正干活的是ioctl。你可以把ioctl理解成「驱动暴露给应用层的API集合」。

V4L2定义了大量的ioctl命令,我挑几个最常用的说说:

  • VIDIOC_QUERYCAP:查询设备能力。你总得知道这摄像头能干啥吧?
  • VIDIOC_ENUM_FMT:枚举支持的格式。RGB?YUV?MJPEG?
  • VIDIOC_S_FMT:设置格式。分辨率、像素格式、帧率。
  • VIDIOC_REQBUFS:申请缓冲区。这是数据流的起点。
  • VIDIOC_QBUF / VIDIOC_DQBUF:入队/出队缓冲区。数据流转起来了。
  • VIDIOC_STREAMON / STREAMOFF:启动/停止数据流。

你想想看,整个Camera数据通路,说白了就是围绕这几个ioctl在转。

4.3 一个典型的ioctl调用流程

我写个最简单的示例,帮你理解这个流程:

#include <sys/ioctl.h>
#include <linux/videodev2.h>

int fd = open("/dev/video0", O_RDWR);
if (fd < 0) {
    perror("open");
    return -1;
}

// 1. 查询能力
struct v4l2_capability cap;
ioctl(fd, VIDIOC_QUERYCAP, &cap);
printf("Driver: %s\n", cap.driver);

// 2. 设置格式
struct v4l2_format 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;
ioctl(fd, VIDIOC_S_FMT, &fmt);

// 3. 申请缓冲区
struct v4l2_requestbuffers req;
req.count = 4;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
ioctl(fd, VIDIOC_REQBUFS, &req);

// 4. 启动流
int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd, VIDIOC_STREAMON, &type);

// 5. 循环采集
for (int i = 0; i < 100; i++) {
    struct v4l2_buffer buf;
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;
    ioctl(fd, VIDIOC_DQBUF, &buf);  // 取出填满数据的buffer
    // 处理数据...
    ioctl(fd, VIDIOC_QBUF, &buf);   // 放回去继续用
}

// 6. 停止
ioctl(fd, VIDIOC_STREAMOFF, &type);
close(fd);

这个流程,我在项目里写过不下几十遍。说白了就是三板斧:查能力→设格式→循环取数据

核心要点:V4L2的ioctl设计是「同步阻塞」的。你调用DQBUF时,如果没有数据,线程会挂起。这在简单场景下没问题,但如果你要做高性能处理,就得考虑多线程或poll机制。

4.4 缓冲区管理:MMAP vs USERPTR vs DMABUF

V4L2支持三种缓冲区管理方式。我分别说说:

  • MMAP(内存映射):驱动分配内存,应用通过mmap映射到用户空间。最简单,最常用。适合大多数场景。
  • USERPTR(用户指针):应用自己分配内存,把指针传给驱动。适合需要自己管理内存的场景,比如用GPU内存。
  • DMABUF(DMA缓冲区):通过dma-buf机制共享缓冲区。零拷贝,性能最好。适合ISP、GPU、Display之间的数据流转。

我个人建议:新手先用MMAP。等你对整个数据流熟悉了,再考虑DMABUF。为什么?因为DMABUF涉及到的同步、生命周期管理,坑比较多。

小技巧:用v4l2-ctl --list-formats-ext可以查看摄像头支持的所有格式和分辨率。调试时特别好用。

4.5 驱动侧:ioctl的实现

从驱动角度看,V4L2框架帮你做了大部分脏活。你只需要实现几个回调函数:

static const struct v4l2_ioctl_ops my_ioctl_ops = {
    .vidioc_querycap      = my_querycap,
    .vidioc_enum_fmt_vid_cap = my_enum_fmt,
    .vidioc_s_fmt_vid_cap    = my_s_fmt,
    .vidioc_reqbufs       = my_reqbufs,
    .vidioc_qbuf          = my_qbuf,
    .vidioc_dqbuf         = my_dqbuf,
    .vidioc_streamon      = my_streamon,
    .vidioc_streamoff     = my_streamoff,
};

然后注册到video_device结构体里。框架会自动帮你处理ioctl的分发、参数校验、权限检查。

嗯,这里有个细节:V4L2框架会帮你做很多默认处理。比如你不实现ENUM_FMT,框架会返回错误。但如果你不实现S_FMT,框架会用默认值。这有时候会掩盖问题——我见过一个驱动,S_FMT实现有bug,但应用层一直用默认格式,所以没发现。直到换了分辨率,才暴露出来。

4.6 调试利器:v4l2-ctl和media-ctl

最后,我强烈建议你学会用这两个工具:

  • v4l2-ctl:操作V4L2设备。可以查询能力、设置格式、抓图。
  • media-ctl:操作Media Controller拓扑。可以查看pipeline、设置路由。

举个例子,调试时我经常这么干:

# 查看设备信息
v4l2-ctl -d /dev/video0 --all

# 抓一帧数据
v4l2-ctl -d /dev/video0 --set-fmt-video=width=640,height=480,pixelformat=YUYV --stream-mmap --stream-to=frame.raw --stream-count=1

# 查看media拓扑
media-ctl -d /dev/media0 --print-topology

这些命令,能帮你快速定位问题。是驱动没配置对?还是应用层参数错了?一测便知。

总结一下:V4L2框架的核心就三样东西——设备节点、ioctl命令、缓冲区管理。理解了这三样,你就掌握了Camera驱动开发的「通用语言」。不管底层硬件怎么变,应用层代码基本不用动。这就是框架的价值。

下一章,咱们深入聊聊V4L2的缓冲区管理机制。这个搞明白了,数据通路就通了一半。

专注资料整理