3. V4L2框架入门:核心数据结构、ioctl命令集、视频设备注册流程

好,咱们今天来啃V4L2这块硬骨头。

说实话,我早年刚接触Linux摄像头驱动时,被V4L2那一堆结构体和ioctl搞得晕头转向。后来做项目多了,慢慢摸清了门道。其实V4L2的设计思路很清晰——它就是把摄像头抽象成一个文件,你用open/read/write/ioctl去操作它。嗯,就是这么回事。

3.1 V4L2核心数据结构

先说说最常用的几个结构体。我个人习惯把它们分成三类:设备描述类、缓冲区类、控制类。

3.1.1 struct video_device

这是V4L2设备的"身份证"。每个摄像头设备对应一个video_device实例。它里面包含了设备号、操作函数集、设备名称等信息。

struct video_device {
    const struct v4l2_file_operations *fops;  // 文件操作函数
    struct device dev;                        // 内核设备结构
    int minor;                                // 次设备号
    // ... 还有一堆,但这两个最关键
};

我在项目中遇到过一个问题:注册video_device时忘了设置fops,结果应用层调用open直接返回-ENODEV。排查了半天才发现是fops指针为空。所以啊,注册前一定要检查这个指针。

3.1.2 struct v4l2_device

这个结构体是V4L2设备的顶层容器。一个物理摄像头可能包含多个子设备(比如传感器、ISP、闪光灯),v4l2_device就是用来管理这些子设备的。

struct v4l2_device {
    struct device *dev;                       // 父设备
    struct list_head subdevs;                 // 子设备链表
    spinlock_t lock;                          // 自旋锁
};

说白了,v4l2_device就是个大管家。它负责协调各个子设备的工作。你想想看,如果传感器和ISP之间没有同步机制,那画面肯定乱套。

3.1.3 struct v4l2_pix_format

这个结构体描述图像的格式。宽高、像素格式、每行字节数等等。应用层通过它告诉驱动:我要拍什么格式的照片。

struct v4l2_pix_format {
    __u32 width;          // 宽度
    __u32 height;         // 高度
    __u32 pixelformat;    // 像素格式,如V4L2_PIX_FMT_YUYV
    __u32 bytesperline;   // 每行字节数
    __u32 sizeimage;      // 一帧图像的总字节数
    // ...
};
我的小技巧:调试时经常用hexdump查看pixelformat的值。比如V4L2_PIX_FMT_YUYV对应的ASCII码是"YUYV",一眼就能看出来对不对。

3.2 ioctl命令集

ioctl是V4L2的灵魂。应用层通过它来配置摄像头、启动采集、获取数据。常用的ioctl命令大概有20多个,但核心的就那么几个。

3.2.1 查询与枚举类

命令 功能 我踩过的坑
VIDIOC_QUERYCAP 查询设备能力 返回的capabilities字段一定要检查,有些设备不支持视频采集
VIDIOC_ENUM_FMT 枚举支持的格式 索引从0开始,别越界
VIDIOC_ENUM_FRAMESIZES 枚举分辨率 有些传感器支持连续分辨率,有些只支持离散的

3.2.2 格式设置类

这部分是重头戏。应用层通过VIDIOC_S_FMT设置采集格式,驱动根据硬件能力做调整。

struct v4l2_format fmt = {0};
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;

if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) {
    perror("VIDIOC_S_FMT");
    return -1;
}

我曾经遇到一个情况:设置格式后,驱动悄悄把分辨率改成了640x480,但应用层没检查返回的fmt结构体。结果缓冲区大小算错了,画面全是花屏。所以啊,设置完格式一定要读回实际值。

3.2.3 缓冲区管理类

V4L2支持三种缓冲区模式:read/write、mmap、userptr。最常用的是mmap模式。

  1. VIDIOC_REQBUFS:申请缓冲区。指定数量和类型。
  2. VIDIOC_QUERYBUF:查询缓冲区信息,获取物理地址和大小。
  3. VIDIOC_QBUF:把缓冲区放入队列,等待硬件填充数据。
  4. VIDIOC_DQBUF:从队列取出已填满数据的缓冲区。
注意:缓冲区操作必须按顺序来。先REQBUFS,再QUERYBUF,然后QBUF,最后DQBUF。顺序乱了,驱动会报错。我见过有人把QBUF和DQBUF搞反了,结果队列永远空着。

3.3 视频设备注册流程

这部分是驱动开发的核心。我把它拆成四个步骤,每一步都有坑。

3.3.1 分配video_device

struct video_device *vdev;
vdev = video_device_alloc();
if (!vdev) {
    pr_err("Failed to allocate video device\n");
    return -ENOMEM;
}

这里有个细节:video_device_alloc()分配的是零初始化内存。但有些驱动开发者喜欢用kzalloc自己分配,然后手动初始化。我个人建议用标准API,省心。

3.3.2 设置操作函数

static const struct v4l2_file_operations camera_fops = {
    .owner      = THIS_MODULE,
    .open       = camera_open,
    .release    = camera_release,
    .unlocked_ioctl = camera_ioctl,
    .mmap       = camera_mmap,
};

vdev->fops = &camera_fops;

嗯,这里要注意:V4L2推荐用unlocked_ioctl而不是ioctl。因为ioctl会持有BKL(大内核锁),影响性能。我早期写驱动时用的ioctl,后来被review打回来改了。

3.3.3 注册设备

int ret;
ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
if (ret < 0) {
    pr_err("Failed to register video device\n");
    video_device_release(vdev);
    return ret;
}

第三个参数是设备号。传-1表示让内核自动分配。我建议用自动分配,省得跟其他设备冲突。有一次我手动指定了设备号,结果跟声卡冲突了,摄像头死活注册不上。

3.3.4 初始化v4l2_device

struct v4l2_device v4l2_dev;
v4l2_dev.dev = &pdev->dev;
ret = v4l2_device_register(&pdev->dev, &v4l2_dev);
if (ret < 0) {
    pr_err("Failed to register v4l2 device\n");
    return ret;
}

vdev->v4l2_dev = &v4l2_dev;

这一步很多人会忘。如果不把video_device关联到v4l2_device,那子设备管理功能就用不了。说白了,就是摄像头只能当个"哑巴"设备,没法跟传感器、ISP通信。

完整注册流程总结:
  1. 分配video_device
  2. 设置fops和ioctl回调
  3. 注册v4l2_device
  4. 关联video_device到v4l2_device
  5. 调用video_register_device
  6. 在remove函数中调用video_unregister_device

最后说一句:调试V4L2驱动时,多用v4l2-ctl这个工具。它能帮你验证ioctl调用是否正确。我每次写完驱动,第一件事就是用v4l2-ctl --list-formats看看格式枚举对不对。省得写应用层代码时才发现问题。

好了,V4L2框架的核心内容就这些。下一章咱们聊聊实际的数据采集流程,包括缓冲区循环和中断处理。到时候我会分享一个我在嵌入式设备上调试花屏问题的案例,挺有意思的。