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; // 一帧图像的总字节数
// ...
};
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模式。
- VIDIOC_REQBUFS:申请缓冲区。指定数量和类型。
- VIDIOC_QUERYBUF:查询缓冲区信息,获取物理地址和大小。
- VIDIOC_QBUF:把缓冲区放入队列,等待硬件填充数据。
- VIDIOC_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通信。
- 分配video_device
- 设置fops和ioctl回调
- 注册v4l2_device
- 关联video_device到v4l2_device
- 调用video_register_device
- 在remove函数中调用video_unregister_device
最后说一句:调试V4L2驱动时,多用v4l2-ctl这个工具。它能帮你验证ioctl调用是否正确。我每次写完驱动,第一件事就是用v4l2-ctl --list-formats看看格式枚举对不对。省得写应用层代码时才发现问题。
好了,V4L2框架的核心内容就这些。下一章咱们聊聊实际的数据采集流程,包括缓冲区循环和中断处理。到时候我会分享一个我在嵌入式设备上调试花屏问题的案例,挺有意思的。