1. V4L2驱动为什么绕不开videobuf2
做摄像头驱动,最开始那几个月大多数人都会有一种错觉:只要把寄存器配好、把 MIPI 报文收上来、把帧数据往用户态一丢,活儿就干完了。真上手才知道,帧数据的"进出场"才是麻烦的开始——缓冲区由谁分配、什么时候分配、要不要支持 mmap、DMA 地址怎么同步、流启动失败怎么回收、用户进程中途被杀怎么清理、多个 plane 的 payload 怎么填、时间戳用哪个时钟源。这一堆琐事如果每个驱动都自己写一遍,内核里会多出几百份高度重复又各自带 bug 的代码。
V4L2 子系统的标准答案就是videobuf2,内核里习惯缩写为 vb2。它位于drivers/media/common/videobuf2/下,向上给用户空间提供一套统一的 buffer 管理语义,向下把内存分配、DMA 映射、cache 同步、队列调度这些脏活抽成回调,驱动只需要回答"我这颗芯片一次要几个 buffer、每个 buffer 多大、数据什么时候搬完"这三个问题。
我见过不少刚接触这块的朋友,写驱动时把 vb2 当成黑盒,能跑通就行,一旦出现VIDIOC_QBUF returned -22、dqbuf卡死不返回、或者帧率上不去就开始怀疑硬件。实际上绝大部分问题都出在对 vb2 的队列状态机和回调时序理解不到位。这篇就沿着"是什么、为什么这么设计、驱动侧怎么落地、踩坑怎么查"这条线,把 vb2 的骨架拆开讲清楚,代码片段都来自我在若干款主控上实际移植并跑通的流程,可以直接对着抄。
1.1 从最原始的字符设备写法说起
如果完全不用 vb2,一个采集驱动大概要自己维护这些东西:一块连续内存池、一个空闲链表、一个已完成链表、一把锁、一套 mmap 的vm_operations_struct、一套poll的等待队列、一套poll唤醒逻辑、DMA 中断里的 buffer 切换、以及release时的资源回收。听上去不多,但每一项都有坑。
举个最典型的:用户态调mmap把内核 buffer 映射到自己地址空间。如果你自己实现,就得处理remap_pfn_range的页对齐、vma->vm_flags的VM_DONTEXPAND | VM_DONTDUMP、引用计数在 fork 后的泄漏,以及最要命的——用户态 unmap 之前驱动不能释放这块内存,而驱动又不知道用户态什么时候 unmap。这些问题在 vb2 里已经被封装成vb2_mmap,你只要在vb2_queue里填好mem_ops,剩下的框架帮你扛。
再比如poll/select。采集场景里用户态基本都是poll等帧,如果有 4 个 buffer 在轮流填充,你必须在每帧完成时把"已完成队列非空"这个事件唤醒等待者,同时还得避免惊群和丢失唤醒。vb2 内部用wait机制加__enqueue_in_driver的状态标记统一处理,自己写很容易在"最后一帧完成时恰好没有人等在 poll 上"这种边界上翻车。
1.2 vb2 到底替驱动接下了哪些活
把 vb2 的职责划一下,大致是四块。
第一块是用户空间的 ioctl 语义。VIDIOC_REQBUFS、VIDIOC_QUERYBUF、VIDIOC_QBUF、VIDIOC_DQBUF、VIDIOC_EXPBUF、VIDIOC_CREATE_BUFS、VIDIOC_PREPARE_BUF这一整套,vb2 在videobuf2-v4l2.c里统一实现,驱动不用碰。这一层还负责把struct v4l2_buffer和内核内部的struct vb2_buffer做双向转换,包括bytesused、timestamp、sequence、field、flags这些字段的填充规则。
第二块是内存分配与 DMA 映射。通过vb2_mem_ops,vb2 提供了几种现成的实现:vb2_vmalloc_memops用 vmalloc 拿非连续内存,适合小分辨率和调试;vb2_dma_contig_memops拿物理连续内存,适合没有 IOMMU 的老平台;vb2_dma_sg_memops走 scatter-gather,适合有 IOMMU 或者内存碎片化严重的系统。选哪个直接决定了后续 DMA 的写法和性能上限。
第三块是队列状态机。一个 buffer 在 vb2 里的生命周期是DEQUEUED → PREPARING → QUEUED → ACTIVE → DONE → DEQUEUED,每一步的迁移都由框架控制,驱动只在特定回调里插手。状态机保证了两件事:用户态不会把同一个 buffer 重复入队,驱动也不会在 buffer 还没准备好时被要求提交给硬件。
第四块是文件句柄的生命周期绑定。用户在open之后reqbufs,然后进程被kill -9,release里 vb2 会自动把该句柄名下的所有 buffer 归还、停止流、释放内存。如果你的驱动自己管 buffer,这一步漏了就是内存泄漏加 DMA 野指针,下次访问直接 oops。
1.3 vb1 与 vb2 的取舍,别在旧代码上耗时间
内核里还残留着videobuf(老开发者叫它 vb1),在drivers/media/v4l2-core/videobuf-core.c里,早就处于维护模式。vb1 的设计是围绕videobuf_queue展开的,回调粒度粗,read和mmap的路径耦合,最要命的是它对多平面(multi-planar)格式支持很差,需要额外维护一套videobuf2-dma-contig之外的胶水代码。
如果你接手的是十年前的老驱动,里面还有videobuf_queue_ops,我的建议是直接重写成 vb2,别修补。原因很实际:新内核里 vb1 的很多路径已经没人测试,编解码器、ISP 这些新子系统全部按 vb2 的接口设计,你用它做级联会遇到大量接口不匹配;而且 vb2 的vb2_v4l2_buffer已经把v4l2_buffer的公共字段内嵌进去,时间戳、sequence、field 的处理都有现成逻辑,重写的工作量其实比想象中小。
至于 vb2 内部的版本演进,真正需要留意的是vb2_v4l2_buffer的引入。早期 vb2 的 buffer 结构体里没有sequence和field这些 V4L2 专有字段,驱动得自己在私有结构里再包一层。现在标准做法是让私有结构体内嵌struct vb2_v4l2_buffer,然后用to_vb2_v4l2_buffer()取出来,框架会自动帮你处理timestamp的类型标记。新写的驱动一律按这个来,别再用老式的struct vb2_buffer直接当私有结构。
2. 五个核心结构体撑起vb2的骨架
vb2 的代码量不小,videobuf2-core.c有四千多行,但真正的骨架只有五个结构体:vb2_queue、vb2_buffer、vb2_plane、vb2_ops、vb2_mem_ops。把这五个吃透,剩下的都是流程编排。
2.1 vb2_queue:整个采集通道的总开关
vb2_queue代表一路视频流。一个video_device通常对应一个 queue,多路 sensor 共用一颗 ISP 时也可能各自一个 queue。它的关键字段我按重要性排一下:
| 字段 | 作用 | 填错会怎样 |
|---|---|---|
type | 缓冲类型,CAPTURE / OUTPUT / MPLANE 变体 | 用户态reqbufs直接EINVAL |
io_modes | 允许的访问方式位掩码 | 用户态用了没开放的方式会失败 |
ops | 驱动回调集合 | vb2_queue_init返回-EINVAL |
mem_ops | 内存分配器实现 | MMAP 模式下无法工作 |
buf_struct_size | 私有 buffer 结构体大小 | 框架分配的内存不够,越界写 |
timestamp_flags | 时间戳来源标记 | 初始化被拒 |
lock | 串行化 ioctl 与流的互斥锁 | 并发场景下队列状态错乱 |
min_buffers_needed | 启动流所需的最少 buffer 数 | streamon 时机不对,丢帧 |
buf_struct_size这个字段特别容易出错。框架在reqbufs时会按这个大小kzalloc出 buffer 对象,如果你的私有结构体是struct my_buf,里面内嵌了vb2_v4l2_buffer再加一堆 DMA 描述符,就必须把sizeof(struct my_buf)填进去。填小了框架只分配sizeof(struct vb2_buffer)的大小,你在回调里访问自己的字段就是越界;填大了浪费内存但不致命。实际经验是:永远用sizeof()算,不要手写数字,结构体改一个字段忘了同步就是灾难。
lock字段也有讲究。它指向一把struct mutex,vb2 在进入 ioctl 处理路径时会自己加锁,但驱动在buf_queue回调里如果不加自己的自旋锁,中断上下文和进程上下文仍然会竞争。正确做法是:vb2_queue->lock用进程级的 mutex 保证 ioctl 串行,硬件队列的链表操作用一把spinlock_t单独保护,两把锁各管一段,别混用。
2.2 vb2_buffer与vb2_plane:一块内存的两种视角
vb2_buffer是 buffer 对象,vb2_plane是它内部的数据平面数组,最多VB2_MAX_PLANES(通常是 8)个。单平面格式(比如 YUYV 打包格式)只用planes[0],多平面格式(比如 NV12M、YUV420M)用planes[0]存 Y、planes[1]存 UV。
每个 plane 里有两个容易混淆的字段:length和bytesused。length是这块 plane 的容量,由queue_setup决定,用户态querybuf看到的就是它;bytesused是实际有效数据长度,捕获方向下由驱动在buf_prepare或帧完成时通过vb2_set_plane_payload()设置。用户态拿到的v4l2_buffer.bytesused就是这个值。
我调试时遇到过一个典型现象:用户态收到的帧总是 0 字节,但图像内容其实是有的。查了半天发现驱动没调vb2_set_plane_payload(),框架默认把bytesused初始化成 0,用户态自然认为没数据。捕获方向一定要在buf_prepare里设 payload,或者至少在帧完成前设,否则上层应用会直接丢弃这一帧。
与之配套的是vb2_plane_vaddr()和vb2_plane_size()两个辅助函数,前者拿内核虚拟地址(仅对 vmalloc / dma-contig 有效,dma-sg 在某些配置下返回 NULL),后者拿 plane 容量。写 memcpy 的时候记得用vb2_plane_vaddr(vb, 0)而不是自己算偏移。
2.3 vb2_ops与vb2_mem_ops:两套回调,各管一摊
vb2_ops是驱动必须实现的业务回调,和硬件强相关。核心的几个:
queue_setup:决定"要几个 buffer、每个多大"。在reqbufs和create_bufs时被调用,可以修改入参。buf_init:buffer 第一次被分配时调用一次,适合做一次性初始化,比如初始化 DMA 描述符。buf_prepare:每次qbuf都会调用,检查 plane 大小、设置bytesused、设置field。buf_queue:buffer 进入硬件队列,驱动把 buffer 挂到自己链表里,等硬件轮到它。start_streaming:streamon时调用,此时已有count个 buffer 就绪,可以启动硬件。stop_streaming:streamoff时调用,驱动必须停止硬件、把所有已入队的 buffer 通过vb2_buffer_done()归还。buf_cleanup:buffer 释放前调用一次,回收buf_init里申请的资源。
vb2_mem_ops是内存回调,一般不需要自己实现,直接用内核现成的全局符号就行:
extern const struct vb2_mem_ops vb2_vmalloc_memops; extern const struct vb2_mem_ops vb2_dma_contig_memops; extern const struct vb2_mem_ops vb2_dma_sg_memops;这三个的选择逻辑我在下一章展开,这里先记住一句:有没有 IOMMU 决定了你选 dma-contig 还是 dma-sg,I/O 大小决定了你会不会后悔选 vmalloc。vmalloc 出来的内存在物理上不连续,如果 DMA 控制器不支持分散聚合,只能做单次小块传输,大分辨率下 CPU 占用会飙到离谱。
2.4 队列状态机:看清每个buffer现在在哪
vb2 给每个 buffer 维护一个状态,驱动最常打交道的四个:
VB2_BUF_STATE_QUEUED:已经在驱动队列里,等硬件。VB2_BUF_STATE_ACTIVE:硬件正在往里写(捕获)或从里读(输出)。VB2_BUF_STATE_DONE:硬件完成,等用户态dqbuf。VB2_BUF_STATE_ERROR:出错完成,用户态dqbuf会看到V4L2_BUF_FLAG_ERROR。
驱动唯一被允许改变状态的方式是调vb2_buffer_done(vb, state)。绝不要自己直接改vb->state,框架内部靠这个字段维护queued_count、start_streaming_called等计数,手动改会把状态机搞崩,症状是streamoff之后队列永远清不干净。
顺带说一个高频疑问:vb2_buffer_done()能不能在中断里调?可以,它内部是spin_lock_irqsave保护的,但要注意它可能触发wake_up,如果调用上下文持有其他锁,要检查是否存在锁序问题。我的惯例是中断里只做list_del加vb2_buffer_done,把耗时的统计和日志丢到 workqueue。
3. 四种内存访问模式,选错就是性能灾难
io_modes决定用户态能用哪种方式拿数据,四种模式的实现路径完全不同,性能差一个数量级都很正常。
3.1 read/write模式:适合调试,不适合量产
VB2_READ | VB2_WRITE对应read()系统调用,框架内部维护一个vb2_fileio_data,每次read都临时申请一个 buffer、启动流、等一帧、拷到用户空间、停流。这种模式的好处是用户态代码极简,read(fd, buf, size)就完事,写个测试脚本验证硬件能不能出图很方便。
代价也很明显:每次read都要streamon/streamoff,硬件重启开销全算进去,实测帧率能掉到 mmap 模式的十分之一甚至更低。而且它独占整条流,同一个video_device上别的进程没法同时 mmap。只在 bring-up 阶段用,正式应用一律 mmap。
3.2 mmap模式:绝大多数场景的主力
VB2_MMAP是最常用的。内核侧分配 buffer,用户态通过mmap把内核物理页映射进自己的地址空间,dqbuf时零拷贝直接拿到数据。这也是 Android Camera、GStreamer、各类流媒体服务默认走的路径。
有一点新手容易困惑:mmap 模式下用户态拿到的虚拟地址在reqbufs之后才能 mmap,而且每个 buffer 单独 mmap 一次,偏移量是v4l2_buffer.m.offset。这个 offset 是框架按 buffer 索引和 plane 索引算出来的,不是物理地址,所以不要试图去解析它。
内核侧要做的只是选对mem_ops。有 IOMMU 的平台用vb2_dma_sg_memops,它在map_dmabuf时建立 SG 表并做 IOMMU 映射,对内存碎片免疫;没有 IOMMU 且 DMA 控制器只认物理连续地址的,用vb2_dma_contig_memops,它会从 CMA 区域拿连续内存,分辨率大了容易分配失败。
3.3 userptr模式:零拷贝但有对齐代价
VB2_USERPTR让用户态自己 malloc 一块内存,把虚拟地址传给驱动做 DMA。听上去很美——省掉一次内核分配,内存池可以复用。但坑在于:用户态 malloc 出来的页可能跨页、可能被 swap、可能物理不连续,驱动在做 DMA 映射时需要把整段虚拟地址钉住(pin)并翻译成物理/SG 表。
vb2_dma_sg_memops支持 userptr,但要求用户传的地址和长度按页对齐。如果应用传了个非页对齐的指针,qbuf直接返回EINVAL。我见过应用侧为此专门写了个对齐分配器,把 malloc 换成posix_memalign(&p, 4096, size),一行代码解决问题。
userptr 还有一个隐性风险:应用提前free或者被munmap,而驱动还在用这块内存做 DMA,结果就是内核往一个已经回收的页里写数据,触发内存损坏。正常流程下close(fd)会走vb2_queue_release把 buffer 全部归还,但如果是多进程共享设备句柄,得应用自己保证生命周期。
3.4 dmabuf模式:跨设备共享的正确姿势
VB2_DMABUF是现在最值得投入的路径。典型场景:摄像头采集的帧要送给 GPU 做渲染、送给硬件编码器做压缩、或者从视频解码器出来后直接送去显示控制器,中间不希望经过 CPU 拷贝。做法是让上游设备VIDIOC_EXPBUF导出一个 dma-buf fd,下游设备用qbuf把这个 fd 传进来。
vb2 在处理 dmabuf 导入时会调用mem_ops->attach_dmabuf和map_dmabuf,建立自己的映射。这里有两个必须注意的点:一是导入的 dma-buf 的像素格式、stride、尺寸必须与当前VIDIOC_S_FMT协商的一致,不一致就是花屏甚至越界写;二是 dma-buf 的读写方向要匹配,CAPTURE 方向要求 dma-buf 是可写的,如果上游导出的是只读描述符,qbuf会失败。
3.5 四种模式横向对比
| 模式 | 拷贝次数 | 帧率表现 | 适用场景 | 主要风险 |
|---|---|---|---|---|
| read/write | 2 次(内核→临时 buf→用户) | 最差 | bring-up 验证 | 每次重启流,开销大 |
| mmap | 0 次 | 好 | 通用采集应用 | buffer 数量与尺寸配置 |
| userptr | 0 次 | 好 | 应用自管内存池 | 页对齐、生命周期 |
| dmabuf | 0 次 | 好 | 跨设备零拷贝管线 | 格式/stride 必须一致 |
选型的判断顺序我一般是这样:先看下游是谁。下游是 GPU 或编码器,优先 dmabuf;下游是纯 CPU 算法,优先 mmap,因为 mmap 拿到的地址可以直接读,cache 一致性由框架的mem_ops处理;只有做寄存器 bring-up 或者写最简单的测试脚本时才用 read。userptr 现在用得少了,主要留给那些必须自己管理大页内存的应用。
4. 驱动侧实现:从零把一条vb2队列跑起来
这一章按实际编码顺序走一遍,代码只保留骨架和关键注释,填进你的驱动就能编译。
4.1 私有buffer结构与队列初始化
先定义私有 buffer,把框架的vb2_v4l2_buffer放在第一个成员:
struct my_dev; struct my_buffer { struct vb2_v4l2_buffer vbuf; struct list_head list; dma_addr_t paddr; /* 后面可以放硬件描述符、帧计数等 */ }; #define to_my_buffer(vb) container_of(vb, struct my_buffer, vbuf)队列初始化的函数建议单独抽出来,方便排查:
static int my_queue_init(struct my_dev *dev, struct vb2_queue *q) { int ret; memset(q, 0, sizeof(*q)); q->type = V4L2_BUF_TYPE_VIDEO_CAPTURE; q->io_modes = VB2_MMAP | VB2_DMABUF; q->dev = dev->dev; q->drv_priv = dev; q->buf_struct_size = sizeof(struct my_buffer); q->ops = &my_vb2_ops; q->mem_ops = &vb2_dma_sg_memops; /* 有 IOMMU 时选这个 */ q->timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC; q->min_buffers_needed = 2; q->lock = &dev->mutex; ret = vb2_queue_init(q); if (ret) dev_err(dev->dev, "vb2_queue_init failed: %d\n", ret); return ret; }vb2_queue_init内部会校验几个必填项,缺一个就返回-EINVAL:ops、mem_ops(仅 mmap 类模式需要)、buf_struct_size、timestamp_flags。我见过有人忘了填timestamp_flags,初始化失败但错误日志没打,白白查了一下午。所以初始化之后一定要打印返回值,这是血泪经验。
min_buffers_needed = 2的含义是:至少有两个 buffer 入队后框架才允许streamon成功。这个值按硬件 FIFO 深度定,硬件如果要求至少 3 帧才能连续输出,就填 3。填太小的后果是流刚启动就 underrun,填太大的后果是延迟变高、内存占用变大。
4.2 queue_setup:算清楚要几个buffer、每个多大
这个回调是整个驱动里最需要动脑子的地方,负责回答三件事:需要几个 buffer、每个 plane 多大、格式对不对。
static int my_queue_setup(struct vb2_queue *vq, unsigned int *nbuffers, unsigned int *nplanes, unsigned int sizes[], struct device *alloc_devs[]) { struct my_dev *dev = vb2_get_drv_priv(vq); unsigned int size; unsigned int count = *nbuffers; /* 1. 计算单帧所需字节数 */ size = dev->bytesperline * dev->height; if (!dev->packed) size = size * 3 / 2; /* NV12 这类半平面格式 */ /* 2. 用户态可能指定了 plane 大小,做范围检查 */ if (*nplanes) { if (sizes[0] < size) return -EINVAL; size = sizes[0]; } *nplanes = 1; sizes[0] = size; /* 3. 保证至少 3 个 buffer,低于这个数直接拒绝 */ if (vq->num_buffers + count < 3) count = 3 - vq->num_buffers; *nbuffers = count; return 0; }关于sizes[0]的计算,我把常见的对齐规则写清楚,因为这是花屏和绿边的高发区。假设 sensor 输出 1920×1080 的 NV12,硬件要求 stride 按 256 字节对齐:
stride = ALIGN(1920, 256) = 2048 frame = stride * height * 3 / 2 = 2048 * 1080 * 1.5 = 3,317,760 字节 ≈ 3.16 MB注意3 / 2是作用在height上还是size上,结果完全不同。NV12 的正确算法是stride * height作为 Y 平面,UV 平面为stride * height / 2,合并就是stride * height * 3 / 2。写成stride * (height * 3 / 2)时如果 height 是奇数,整数除法会截断,少算 1024 字节左右,硬件一写就踩到下一个 buffer 的头部,表现为相邻帧串扰。
缓冲区数量也不是越多越好。4 个 buffer 在 1080p30 下内存占用约 12.6 MB,8 个就是 25 MB,对嵌入式设备不算小。我的经验值:采集端 4 个、编码端 6~8 个,采集端多给没意义,因为消费速度跟不上再多也是排队。
4.3 buf_prepare与buf_queue:把buffer交给硬件的两个动作
buf_prepare每次qbuf都会被调用一次,是驱动最后能拒绝这个 buffer 的机会:
static int my_buf_prepare(struct vb2_buffer *vb) { struct my_dev *dev = vb2_get_drv_priv(vb->vb2_queue); struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb); unsigned long size = dev->bytesperline * dev->height; unsigned long ret; if (dev->packed) { ret = vb2_plane_size(vb, 0); if (ret < size) { dev_err(dev->dev, "plane too small: %lu < %lu\n", ret, size); return -EINVAL; } vb2_set_plane_payload(vb, 0, size); } else { /* 半平面格式,两个 plane 各自检查 */ if (vb2_plane_size(vb, 0) < size || vb2_plane_size(vb, 1) < size / 2) return -EINVAL; vb2_set_plane_payload(vb, 0, size); vb2_set_plane_payload(vb, 1, size / 2); } vbuf->field = V4L2_FIELD_NONE; return 0; }buf_queue负责把 buffer 挂进硬件队列。这里的锁要格外小心:
static void my_buf_queue(struct vb2_buffer *vb) { struct my_dev *dev = vb2_get_drv_priv(vb->vb2_queue); struct my_buffer *buf = to_my_buffer(vb); unsigned long flags; spin_lock_irqsave(&dev->qlock, flags); list_add_tail(&buf->list, &dev->queued_list); spin_unlock_irqrestore(&dev->qlock, flags); }有人在buf_queue里直接写硬件寄存器启动 DMA,这是错的。因为buf_queue可能在start_streaming之前和之后都被调用,硬件状态不一定是"已启动"。正确做法是:buf_queue只入链表,真正的硬件提交放在start_streaming里的初次提交,以及中断处理函数里从链表取下一个 buffer。
4.4 start_streaming与stop_streaming:最考验细节的两个回调
start_streaming被调用时,框架保证至少min_buffers_needed个 buffer 已经通过buf_queue进了驱动链表,硬件可以放心启动:
static int my_start_streaming(struct vb2_queue *vq, unsigned int count) { struct my_dev *dev = vb2_get_drv_priv(vq); struct my_buffer *buf, *tmp; unsigned long flags; int ret; /* 1. 配置硬件:格式、分辨率、DMA 目标 */ ret = my_hw_configure(dev); if (ret) { dev_err(dev->dev, "hw configure failed: %d\n", ret); goto err_return_buffers; } /* 2. 把第一个 buffer 交给硬件 */ spin_lock_irqsave(&dev->qlock, flags); buf = list_first_entry_or_null(&dev->queued_list, struct my_buffer, list); if (buf) list_del(&buf->list); spin_unlock_irqrestore(&dev->qlock, flags); if (!buf) { ret = -ENOBUFS; goto err_return_buffers; } my_hw_start_dma(dev, buf); /* 3. 开中断、置状态 */ dev->streaming = true; my_hw_enable_irq(dev); return 0; err_return_buffers: /* 关键:启动失败必须把所有已入队 buffer 归还 */ spin_lock_irqsave(&dev->qlock, flags); list_for_each_entry_safe(buf, tmp, &dev->queued_list, list) { list_del(&buf->list); vb2_buffer_done(&buf->vbuf.vb2_buf, VB2_BUF_STATE_QUEUED); } spin_unlock_irqrestore(&dev->qlock, flags); return ret; }err_return_buffers这段是很多人漏写的。启动失败后如果不归还 buffer,用户态streamon拿到错误码,但框架内部的queued_count还挂着几个 buffer,下一次streamon时这些 buffer 的状态就是脏的,表现是第二次开流必挂。这条规则在stop_streaming里同样成立:
static void my_stop_streaming(struct vb2_queue *vq) { struct my_dev *dev = vb2_get_drv_priv(vq); struct my_buffer *buf, *tmp; unsigned long flags; /* 1. 先停硬件,再动队列,顺序不能反 */ my_hw_disable_irq(dev); my_hw_stop_dma(dev); /* 2. 把链表里没处理完的 buffer 全部归还 */ spin_lock_irqsave(&dev->qlock, flags); list_for_each_entry_safe(buf, tmp, &dev->queued_list, list) { list_del(&buf->list); vb2_buffer_done(&buf->vbuf.vb2_buf, VB2_BUF_STATE_ERROR); } spin_unlock_irqrestore(&dev->qlock, flags); dev->streaming = false; }顺序很重要:先停硬件再归还 buffer。反过来的话,硬件可能正在写一个已经被归还的 buffer,用户态一 mmap 读到半帧数据还能忍,读到 DMA 越界就是内存损坏。
4.5 与video_device对接:ioctl与file_operations
最后一步是把队列挂到video_device上。现在推荐用video_device+v4l2_file_operations的组合,ioctl 走video_ioctl2加v4l2_ioctl_ops:
static const struct v4l2_ioctl_ops my_ioctl_ops = { .vidioc_querycap = my_querycap, .vidioc_enum_fmt_vid_cap = my_enum_fmt, .vidioc_try_fmt_vid_cap = my_try_fmt, .vidioc_s_fmt_vid_cap = my_s_fmt, .vidioc_g_fmt_vid_cap = my_g_fmt, .vidioc_reqbufs = vb2_ioctl_reqbufs, .vidioc_querybuf = vb2_ioctl_querybuf, .vidioc_qbuf = vb2_ioctl_qbuf, .vidioc_dqbuf = vb2_ioctl_dqbuf, .vidioc_expbuf = vb2_ioctl_expbuf, .vidioc_prepare_buf = vb2_ioctl_prepare_buf, .vidioc_create_bufs = vb2_ioctl_create_bufs, .vidioc_streamon = vb2_ioctl_streamon, .vidioc_streamoff = vb2_ioctl_streamoff, }; static const struct v4l2_file_operations my_fops = { .owner = THIS_MODULE, .open = v4l2_fh_open, .release = vb2_fop_release, .read = vb2_fop_read, .poll = vb2_fop_poll, .mmap = vb2_fop_mmap, .unlocked_ioctl = video_ioctl2, };这里能省则省:能交给vb2_ioctl_*和vb2_fop_*的一律不要自己写。这几个通用函数已经把锁的获取、buffer 状态检查、错误返回都做完了。曾经的教科书式写法是自己实现my_ioctl里加一大堆switch,现在没必要。
v4l2_fh_open和vb2_fop_release的配对也要注意:release必须用vb2_fop_release,它会先调vb2_queue_release清空队列再释放v4l2_fh。自己写release只调v4l2_fh_release的话,队列资源会泄漏,反复开关应用后内存一路涨。
5. 用户空间视角:一次完整采集的时序
驱动写完要先在用户态验证,别急着写应用框架。命令行工具和一段几十行的 C 就能定位绝大多数问题。
5.1 格式协商与reqbufs
先用工具把格式摸清楚:
v4l2-ctl -d /dev/video0 --list-formats-ext v4l2-ctl -d /dev/video0 --set-fmt-video=width=1920,height=1080,pixelformat=NV12 v4l2-ctl -d /dev/video0 --get-fmt-video--get-fmt-video的输出里重点看Bytes per Line,这个值就是驱动在s_fmt里填的bytesperline。如果它和你以为的不一样(比如你算的 1920,驱动填了 2048),说明对齐规则没对上,后面 plane 大小的计算也会错。
C 代码里请求 buffer:
struct v4l2_requestbuffers req = {0}; req.count = 4; req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_MMAP; if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) { perror("VIDIOC_REQBUFS"); return -1; } if (req.count < 2) { fprintf(stderr, "insufficient buffers: %u\n", req.count); return -1; }返回的req.count可能小于你请求的值,因为驱动在queue_setup里可能改动它。以返回值为准重新分配指针数组,不要写死 4。
5.2 mmap、入队、出队
struct buffer { void *start; size_t length; } buffers[4]; for (unsigned int i = 0; i < req.count; i++) { struct v4l2_buffer buf = {0}; buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = i; if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) { /* ... */ } buffers[i].length = buf.length; buffers[i].start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset); if (buffers[i].start == MAP_FAILED) { /* ... */ } }入队和出队的循环里,最容易忽略的是bytesused和sequence的读取:
struct v4l2_buffer buf = {0}; buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = i; ioctl(fd, VIDIOC_QBUF, &buf); /* 出队 */ ioctl(fd, VIDIOC_DQBUF, &buf); printf("index=%u bytesused=%u seq=%u ts=%ld.%06ld\n", buf.index, buf.bytesused, buf.sequence, buf.timestamp.tv_sec, buf.timestamp.tv_usec);sequence是驱动在帧完成时自增的计数值,正常应该是连续的。如果发现跳号,说明中间丢帧了,先查 buffer 数量够不够、中断处理有没有延迟太久。
5.3 时间戳与sequence的正确填法
时间戳的坑特别多。timestamp_flags设成V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC表示用CLOCK_MONOTONIC,这是现在推荐的;老代码用V4L2_BUF_FLAG_TIMESTAMP_UNKNOWN让框架不填,用户态自己打时间,会导致音视频同步完全没法做。
驱动侧填时间戳的标准姿势是在帧完成中断里:
static void my_frame_done(struct my_buffer *buf) { struct vb2_v4l2_buffer *vbuf = &buf->vbuf; vbuf->vb2_buf.timestamp = ktime_get_ns(); vbuf->sequence = dev->frame_seq++; vbuf->field = V4L2_FIELD_NONE; vb2_buffer_done(&vbuf->vb2_buf, VB2_BUF_STATE_DONE); }ktime_get_ns()在中断上下文里调用是安全的。注意顺序:先填 timestamp 和 sequence,再调vb2_buffer_done,因为后者一返回用户态就可能立刻dqbuf读到数据,晚填就是读到旧值。
6. 典型故障与排查实录
vb2 报错信息不算友好,很多错误码要结合上下文猜。这一章把我这些年攒下来的对照表整理出来。
6.1 错误码对照与定位思路
| 现象 | 常见原因 | 定位方法 |
|---|---|---|
reqbufs返回 -22 | type不匹配,或io_modes没开对应位 | 打印vq->type和vq->io_modes |
qbuf返回 -22 | plane 太小、buf_prepare返回了错误 | 在buf_prepare里加 dev_dbg |
streamon返回 -16 | 队列已启动,重复调用 | 检查vb2_is_streaming() |
streamon返回 -12 | 内存不足,CMA 区域碎片化 | 换 dma-sg 或减小分辨率 |
dqbuf永久阻塞 | 硬件没产生中断,或没调vb2_buffer_done | 查中断计数、查 DMA 使能位 |
dqbuf返回 -32 | 用户态没qbuf就dqbuf | 检查用户态循环逻辑 |
| 出图有绿边/错位 | bytesperline与硬件不一致 | 对比get-fmt-video与寄存器 |
| 相邻帧串扰 | plane 大小算少了 | 打印sizes[0]与硬件地址差 |
| 反复开关流后 oops | stop_streaming没归还 buffer | 检查链表遍历归还逻辑 |
| 内存持续增长 | release没走vb2_fop_release | 检查file_operations |
qbuf返回-22是最常见的,八成出在buf_prepare。我一般会临时把buf_prepare里的错误分支全加上dev_info,把vb2_plane_size(vb, 0)和期望的 size 都打出来,对比一次就知道是算错了还是用户态传了奇怪的 size。
6.2 cache一致性与DMA方向的那些坑
ARM 平台跑摄像头,cache 一致性绕不过去。vb2_dma_sg_memops在buf_prepare时会做dma_sync_sg_for_device,在dqbuf之前会做dma_sync_sg_for_cpu,方向是对的,前提是框架知道这是个 CAPTURE 队列。
如果你自己在中断里用dma_map_single搬数据,就得手动dma_sync_single_for_cpu,否则 CPU 读到的是 cache 里的旧数据。症状很典型:图像上半部分是上一帧、下半部分是这一帧,或者画面有一半是噪点。凡是驱动自己做了 DMA 而不走 vb2 的 mem_ops,就必须自己管同步。
另一个细节是gfp_flags。在vb2_queue里设q->gfp_flags = GFP_DMA32,可以让分配器把 buffer 放在 32 位 DMA 可寻址区域。有些主控的外设 DMA 只能访问低 4GB,不设这一位就会在某些大内存机器上随机失败,而且失败点飘忽不定。
6.3 缓冲区数量与丢帧的关系
丢帧的排查思路要分层。先看用户态:dqbuf之后有没有立刻qbuf回去?中间如果去做了耗时处理(比如写文件、做算法),缓冲区就会耗尽,硬件没有可写的 buffer,只能丢帧。
再看驱动:中断里vb2_buffer_done之后,有没有从链表取下一个 buffer 交给硬件?如果取下一块的逻辑写在 workqueue 里,中断到 workqueue 之间的延迟就是硬件空转的时间。1080p30 每帧 33ms,workqueue 延迟通常几毫秒,能扛住;但 1080p120 每帧只有 8ms,就必须在中断里完成提交。
最后看数量。一个经验公式:buffer 数 ≥ 帧间隔 / 用户态单帧处理耗时 + 2。用户态处理一帧要 10ms,帧间隔 33ms,那么 4 个够用;如果处理要 30ms,就需要 4 个以上,还得留余量给调度抖动。
7. 移植到新平台时的几条实操心得
先说ops的最小实现集合。有些平台只是把 vb2 当个壳子,硬件其实不支持真正的队列,那也至少要实现queue_setup、buf_prepare、buf_queue、start_streaming、stop_streaming这五个,其它可选回调可以不写。少写queue_setup的话,框架会用默认值分配一个什么都不对的 buffer 大小,问题在很久之后才暴露。
再说调试手段。vb2内部有 tracepoint,打开CONFIG_VIDEO_ADV_DEBUG之后用echo 1 > /sys/module/videobuf2_core/parameters/debug能看到详细的队列操作日志,包括每次qbuf的状态迁移和vb2_buffer_done的调用点。这条比在代码里到处加 printk 高效得多,尤其是在追"某个 buffer 永远回不来"这类问题时,日志里能看到它卡在哪个状态。
还有一点是关于CREATE_BUFS的。有些应用会先reqbufs要 1 个 buffer,跑起来之后再CREATE_BUFS动态追加。如果你的queue_setup里对vq->num_buffers + count做了硬性下限检查,动态追加时可能返回错误。我的做法是:reqbufs路径下做下限检查,create_bufs路径下放松到"至少 1 个",两者的判断条件用vq->num_buffers区分开。
最后提一句多平面格式的s_fmt处理。V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE下,v4l2_format里的fmt.pix_mp.num_planes必须由驱动在try_fmt和s_fmt里填对,用户态依据这个值来决定传几个 plane 的length。填成 0 或者 1 而实际是 2,用户态qbuf时第二个 plane 的 size 就是垃圾值,直接EINVAL。这个错我发现过三次,每次都是漏填num_planes。
写到这里,vb2 的队列骨架、内存模式、驱动回调、用户态时序和排查手段基本覆盖了一遍。下一篇我会接着往下拆videobuf2的 buffer 完成通知机制,也就是vb2_buffer_done到用户态dqbuf之间那段被很多人忽略的等待与唤醒逻辑,以及poll在多 buffer 场景下的行为,这块直接关系到高帧率下会不会出现"帧已就绪但 poll 不醒"的诡异现象。