2. 嵌入式Linux摄像头驱动框架:V4L2架构解析
好,咱们直接进入正题。V4L2,全称 Video for Linux 2,是 Linux 内核里处理视频设备的统一框架。说白了,它就是摄像头和应用程序之间的“翻译官”。你想想看,市面上那么多摄像头芯片,OV、索尼、安森美,每家寄存器都不一样。如果没有 V4L2,每个应用都得自己写驱动,那不乱套了?
我个人习惯把 V4L2 理解成三层结构:应用层、内核框架层、硬件驱动层。应用层调用 open/ioctl/mmap,内核层负责调度,驱动层直接操作寄存器。嗯,这个分层设计,其实和网络协议栈的思路很像。
2.1 Video设备节点注册
摄像头在用户空间长什么样?就是一个文件:/dev/video0。你打开它,就像打开一个普通文件一样。但背后的事情,远没那么简单。
驱动注册设备节点时,核心调用是 video_register_device()。我截取一段典型的初始化代码,你感受一下:
static struct video_device vdev = {
.name = "MyCamera",
.fops = &my_fops,
.ioctl_ops = &my_ioctl_ops,
.release = my_release,
};
static int my_probe(struct platform_device *pdev)
{
struct video_device *vdev;
int ret;
vdev = video_device_alloc();
if (!vdev)
return -ENOMEM;
strscpy(vdev->name, "MyCamera", sizeof(vdev->name));
vdev->fops = &my_fops;
vdev->ioctl_ops = &my_ioctl_ops;
vdev->release = video_device_release;
ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
if (ret < 0) {
video_device_release(vdev);
return ret;
}
platform_set_drvdata(pdev, vdev);
return 0;
}
这里有个坑,我当年踩过。注册时第三个参数 VFL_TYPE_VIDEO 决定了设备节点类型。如果你注册成 VFL_TYPE_VBI,应用层用标准 V4L2 接口是打不开的。我曾经在调试一个老款 sensor 时,发现 /dev/video0 死活打不开,查了半天才发现是类型写错了。
2.2 ioctl控制流程
ioctl 是 V4L2 的灵魂。应用层通过它来查询能力、设置格式、请求缓冲区。整个流程,我习惯把它分成三个阶段:查询、配置、流控。
先看查询阶段。应用调用 VIDIOC_QUERYCAP,驱动返回设备能力。比如是否支持视频捕获、是否支持流 I/O。代码长这样:
static int my_vidioc_querycap(struct file *file, void *priv,
struct v4l2_capability *cap)
{
strscpy(cap->driver, "my_camera", sizeof(cap->driver));
strscpy(cap->card, "My USB Camera", sizeof(cap->card));
strscpy(cap->bus_info, "usb-0000:00:14.0-1", sizeof(cap->bus_info));
cap->capabilities = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
return 0;
}
然后是配置阶段。应用设置图像格式、帧率、裁剪区域。这里最常用的是 VIDIOC_S_FMT。我建议你在驱动里做一次格式校验,别完全相信应用传进来的参数。比如像素格式,有些应用会传 V4L2_PIX_FMT_YUYV,但你的 sensor 只支持 V4L2_PIX_FMT_MJPEG。这时候驱动要主动拒绝,或者做格式转换。
最后是流控阶段。应用调用 VIDIOC_STREAMON 开始采集,VIDIOC_STREAMOFF 停止。这里有个细节:STREAMON 之后,驱动要立刻启动 DMA 传输。如果硬件有延迟,记得用 workqueue 异步启动,别在 ioctl 上下文里等太久。
strace -e ioctl -f ./camera_app。我曾经靠这招抓到一个应用反复调用 VIDIOC_QBUF 但不调用 VIDIOC_DQBUF 的 bug。
2.3 内存映射(mmap)机制
摄像头数据量有多大?以 1080p@30fps 的 YUYV 格式为例,每秒数据量是 1920×1080×2×30 ≈ 124MB。如果用 read/write 逐帧拷贝,CPU 占用率直接拉满。所以 V4L2 提供了 mmap 机制,让应用和驱动共享内存。
mmap 的核心是 VIDIOC_REQBUFS 和 VIDIOC_QUERYBUF。应用先请求缓冲区,然后查询每个缓冲区的物理地址,最后通过 mmap 映射到用户空间。驱动端要实现的回调是 vidioc_reqbufs 和 vidioc_querybuf。
我画个流程图,帮你理清关系:
| 步骤 | 应用调用 | 驱动行为 |
|---|---|---|
| 1 | VIDIOC_REQBUFS | 分配 DMA 缓冲区,记录物理地址 |
| 2 | VIDIOC_QUERYBUF | 返回缓冲区信息(offset、length) |
| 3 | mmap | 将 DMA 缓冲区映射到用户空间 |
| 4 | VIDIOC_QBUF | 将缓冲区加入硬件采集队列 |
| 5 | VIDIOC_DQBUF | 从完成队列取出已采集的缓冲区 |
驱动里实现 mmap 时,核心是 vb2_mmap 或者自己实现 mmap 回调。我建议用内核的 videobuf2 框架,它帮你处理了大部分脏活累活。比如缓冲区队列管理、DMA 一致性、缓存同步。你只需要实现 queue_setup、buf_prepare、buf_queue 这几个回调。
static int my_queue_setup(struct vb2_queue *q,
unsigned int *nbuffers,
unsigned int *nplanes,
unsigned int sizes[],
struct device *alloc_devs[])
{
sizes[0] = MY_CAMERA_WIDTH * MY_CAMERA_HEIGHT * 2;
*nplanes = 1;
return 0;
}
static int my_buf_prepare(struct vb2_buffer *vb)
{
// 检查缓冲区大小是否足够
if (vb2_plane_size(vb, 0) < MY_CAMERA_WIDTH * MY_CAMERA_HEIGHT * 2)
return -EINVAL;
return 0;
}
static void my_buf_queue(struct vb2_buffer *vb)
{
struct my_device *dev = vb2_get_drv_priv(vb->vb2_queue);
// 将缓冲区加入硬件 DMA 链表
list_add_tail(&vb->queued_entry, &dev->dma_list);
}
dma_alloc_coherent 分配的内存会自动处理。但如果你用 kmalloc 加手动映射,就得自己调用 dma_map_single 和 dma_sync_single_for_device。
嗯,这里还要提一个我踩过的坑。有一次我在 i.MX6 平台上调试摄像头,发现 mmap 后应用读到的图像是花屏的。查了两天,最后发现是 DMA 地址没有对齐到 cache line。ARM 的 cache line 是 64 字节,如果 DMA 缓冲区起始地址不是 64 的倍数,CPU 读到的数据就是错的。解决方案很简单:分配缓冲区时用 dma_alloc_coherent,它保证地址对齐。
最后总结一下。V4L2 框架的核心就是这三件事:设备节点让应用能找到硬件,ioctl 让应用能控制硬件,mmap 让数据能高效传输。你把这三点吃透了,嵌入式摄像头驱动这块就算入门了。下一章,我会讲怎么用 videobuf2 框架做缓冲区管理,那是性能优化的重头戏。