前言
本篇围绕 v4l2_buffer2 继续展开:
media/
├── test-drivers/vimc/
│ ├── vimc-capture.c
│ └── vimc-streamer.c
│
├── common/videobuf2/
│ ├── videobuf2-v4l2.c
│ ├── videobuf2-core.c
│ ├── videobuf2-vmalloc.c
│ └── videobuf2-memops.c


摄像头不断送出图像,应用不断取走图像。连接这两端的,首先是内存:这一帧写在哪里,下一帧还能写哪里,已经写完的那块何时可以重新使用。
节点能够打开,只说明访问入口已经建立。真正开始连续采集,还要准备存储,并让采集端和处理端按顺序交接。V4L2 中常用的 videobuf2 框架,简称 VB2,正是为这部分工作提供公共实现。
先从一帧图像需要的存储讲起,再打开 vimc-capture.c,看一份采集驱动怎样把这些问题交给 VB2。本篇走到队列初始化和节点发布为止;缓冲区池的实际申请、映射与运行过程,随后逐段展开。
1. 从一个像素,到一块图像缓冲区
1.1 内存里保存的是字节,不是二维画面
RGB24 是一个容易观察的例子。一个像素有 R、G、B 三个颜色分量,每个分量占 1 字节,合计 3 字节。若图像宽 640、高 480,按行排列,行尾没有填充:
一行的像素数据 = 640 × 3 = 1920 字节
一帧的像素数据 = 1920 × 480 = 921600 字节
应用拿到一段内存以后,必须知道像素格式、宽度、高度和每行的排列方式,才能还原画面。相同的一批字节,按错误的行长度或者颜色次序解释,也会显示成错行、斜纹或者偏色。
为这样的图像准备一块可反复使用的存储,就得到了本篇所说的图像缓冲区。这里先讨论普通逐帧采集:一块缓冲区容纳一帧结果所需的数据。VB2 还可以管理码流、元数据等其他载荷,不能把这个教学场景推广成所有设备的固定规则。
1.2 行跨度为什么要单独记录
图像宽度决定一行有多少个像素,却不总能决定下一行从哪里开始。有些设备要求每行起点对齐,行尾需要留出填充字节。
例如,638 像素宽的 RGB24 一行有 638 × 3 = 1914 字节。假设设备要求行跨度按 64 字节对齐,就可以采用 1920 字节,末尾补 6 字节。这是解释对齐的假设,并不是 VIMC 在本文场景中的实际限制。
相邻两行起始位置之间的字节距离,叫作行跨度;V4L2 用 bytesperline 记录它。 对这种线性 RGB24 布局,第 y 行、第 x 个像素的起点为:
偏移 = y × bytesperline + x × 3
如果实际行跨度是 1920,却按 1914 跳到下一行,读到的位置会逐行偏移。程序应使用格式接口返回的值,而不是只凭宽度重新猜一个值。

图 1:上半部分是本文的 640 × 480 示例;下半部分另用 638 像素宽说明行尾填充。两者不是同一幅图像的两套尺寸。
1.3 容量和有效数据量不是同一个数字
sizeimage 表示格式要求的图像存储容量。前面的简单 RGB24 可按 bytesperline × height 计算;有额外对齐、分块布局或压缩载荷时,不能直接套用这个算式,应读取协商后的格式要求。
bytesused 则描述一次结果实际使用的字节数。容量在准备存储时确定,实际数据量在处理结果中报告。压缩图像的大小可能逐帧变化;即使缓冲区足够大,也不能把整块容量都当成有效图像读取。
后面还会遇到平面容量 length。在这里先建立顺序:格式给出存储需求,缓冲区记录可用容量,一次完成结果再报告实际数据量。 它们在简单例子里可能数值相同,职责却不同。
1.4 平面是缓冲区内部的存储组织
RGB24 将 R、G、B 交错放在一起,通常用一个起点和一段容量描述,因此只有一个接口内存平面。三个颜色分量不等于三个内存平面。
某些格式把一帧分成几个需要分别描述地址和容量的区域,这些区域在缓冲区接口里称为平面,英文是 plane。一个双平面缓冲区仍然是一组图像存储,而不是两帧图像。申请三块双平面缓冲区,是三组对象,每组包含两个平面。
V4L2 的多平面接口也能承载只有一个内存平面的格式。本文先使用 VIMC 的单平面采集接口;多平面的字段和映射规则留到查询、映射篇再详细分析。媒体拓扑中的 media_pad 是模块连接端口,也不是这里的内存平面。
本节的布局约定可对照文末的“格式与缓冲区接口”。VIMC 的具体默认尺寸和计算方式,接下来直接看驱动实现。
2. 为什么连续采集需要一个缓冲区池
2.1 同一块内存会被反复使用
缓冲区并不与某一帧永久绑定。buffer 0 这次保存第 10 帧,处理完成并归还后,下一次可能保存第 13 帧。
所以,index 标识池中的缓冲区,帧序号 sequence 标识驱动报告的结果次序。不能把 buffer 0 理解成“永远存放第 0 帧”,也不能把每次申请存储等同于拍摄一帧。
假设后续成功建立三块前述 RGB24 缓冲区,仅像素区域的逻辑容量就是:
3 × 921600 = 2764800 字节
管理结构、分配器元数据、页表以及可能的对齐开销,不包含在这个计算中。“三块”只是后续观察模型,不是说驱动必然按任意请求原样分配。
2.2 采集和处理的速度可能不一致
用一个独立的时序例子说明:假设采集源每秒产生 30 帧,平均间隔约 33.3 毫秒。应用处理一帧的时间却会受调度和算法负载影响。这里的帧率只是说明生产与消费的时间差,不是对 VIMC 线程实际帧率的测量。
若只有一块存储,下一帧准备写入时,应用可能还没有读完上一帧。让两端同时任意读写,会得到不稳定的数据;要求双方总是互相等待,又不利于连续处理。
准备多块缓冲区后,可以让一块由采集端处理,一块保存已完成结果,另一块由应用读取。处理结束的缓冲区再提交回去,继续下一轮。

图 2:这是启动采集以后的一种可能快照,用来解释多缓冲区的必要性。各索引没有永久职责,也不要求始终按 0、1、2 的固定顺序完成。本篇初始化结束时尚未出现这些运行状态。
增加缓冲区可以吸收短时波动,却不能解决长期处理速度不足。排队等待的已完成图像越多,应用拿到的结果就可能越旧;仅仅多分配几块尚未使用的空缓冲区,并不必然增加同样数量的帧延迟。
2.3 “池”提供存储,“队列管理”记录使用过程
只有三块内存还不够。驱动还需要知道每块多大、有没有提交、是否正在处理、结果是否完成,以及结束时应该收回哪些对象。
VB2 把这些共同的管理工作组织起来。采集驱动不必从零实现每一种排队和等待接口,而是在指定位置提供自己的规则:需要什么布局、怎样接收缓冲区、如何开始和停止处理、何时报告完成。
从应用角度,常见过程是先申请和映射,再提交缓冲区、启动流、等待并取回结果。映射提供访问地址,交接规则决定什么时候可以安全使用数据。 地址一直存在,并不表示采集端正在写入时,应用仍能把它当成一帧稳定图像读取。
这些原则解释了 VB2 为什么存在。接下来需要看的是:驱动怎样把格式、存储规则和处理入口交给它。
3. 打开 vimc-capture.c:先认识实际工作的那份驱动
3.1 VIMC 让框架关系可以直接观察
VIMC 是 Virtual Media Controller 的缩写,是一份模拟媒体设备及处理管线的驱动。本文选取它的捕获节点,分析普通 CAPTURE 队列,并沿 vmalloc 内存后端建立认识。
3.2 先看真正写入图像的位置
vimc_capture_process_frame() 在后续运行阶段取到一个待处理缓冲区后,会执行下面这段操作:
vbuf = vb2_plane_vaddr(&vimc_buf->vb2.vb2_buf, 0);
memcpy(vbuf, frame, vcapture->format.sizeimage);
vb2_set_plane_payload(&vimc_buf->vb2.vb2_buf, 0,
vcapture->format.sizeimage);
vb2_buffer_done(&vimc_buf->vb2.vb2_buf, VB2_BUF_STATE_DONE);
这里有三个动作:取得该平面的 CPU 访问地址,将上游送来的图像复制进去,然后填写载荷长度并报告完成。这个实例真正写入像素的是 memcpy();VB2 负责让缓冲区按规则进入、退出处理过程。
这段代码现在不会执行。vimc_capture_add() 只是把函数地址放进 ved.process_frame,运行阶段由 vimc-streamer.c 的软件线程沿管线调用它。线程启动和完成通知将在后续文章展开,本篇不把这个软件例子画成 DMA 中断采集。
先看到实际写入点,再阅读框架,目的就明确了:VB2 要把合适的存储交到这个处理函数能够找到的位置,并在处理结束后把结果送回应用。
3.3 一份捕获设备对象里保存了什么
vimc-capture.c 定义的设备对象如下。这里只去掉原文的说明性注释,成员及顺序保持不变:
structvimc_capture_device {
structvimc_ent_deviceved;
structvideo_devicevdev;
structv4l2_pix_formatformat;
structvb2_queuequeue;
structlist_headbuf_list;
spinlock_t qlock;
structmutexlock;
u32 sequence;
structvimc_streamstream;
structmedia_padpad;
};
vdev 对应对外的视频节点,format 保存当前图像布局,queue 是这份设备自己的 VB2 队列。三者内嵌在同一个 vcapture 对象中,不是每次打开节点都重新建立一套。
buf_list 是 VIMC 私有的待处理缓冲区列表。VB2 将来把对象交给驱动以后,驱动需要一个地方暂存这些对象,等待软件处理线程取走。它与 VB2 内部记录提交、完成的列表不是同一条链表。
lock 是用于相关操作串行化的互斥锁,qlock 保护驱动私有列表的短临界区。stream 保存软件处理管线的运行上下文,sequence 用于后续结果的序号。ved 和 pad 则把捕获端接进 VIMC 的媒体实体组织。
本篇先掌握 vdev、format、queue 和 buf_list 的职责。媒体拓扑的创建与线程内部算法不在这里提前展开。
4. 从设备对象走到一块具体缓冲区
4.1 队列先存在,池中的对象以后才建立
初始化时,驱动执行 q = &vcapture->queue。这只是取得已经随设备对象分配的内嵌成员地址,既不是分配一帧,也没有建立一个新队列副本。
队列需要记录支持的访问方式、回调、缓冲区集合及后续运行状态。等申请成功以后,核心才能用 q->bufs[index] 找到相应的内部缓冲区对象。首次初始化阶段,这些槽位还没有对应对象。
因此,理解 struct vb2_queue 时,不必先背完全部字段。先把它看成这组存储的管理入口:上面连接节点和驱动,下面连接将来建立的缓冲区。
4.2 驱动可以在公共缓冲区外增加自己的成员
VIMC 对每块缓冲区使用下面的扩展结构:
structvimc_capture_buffer {
structvb2_v4l2_buffervb2;
structlist_headlist;
};
vb2 是框架要求的 V4L2 缓冲区部分,list 是驱动追加的列表成员。后续 vimc_capture_buf_queue() 会用这个成员把对象挂到 vcapture->buf_list。
struct vb2_v4l2_buffer 的首成员又是通用的 struct vb2_buffer。于是,完整对象由内到外是:通用缓冲区、V4L2 扩展、VIMC 扩展。不是给同一块图像分配三份相互复制的管理对象。
驱动告诉框架完整对象的大小:
q->buf_struct_size = sizeof(struct vimc_capture_buffer);
后续核心才会据此执行 kzalloc(q->buf_struct_size, GFP_KERNEL)。初始化时设置大小,不会立即执行这次分配。
这套接口要求公共部分位于规定的起始位置。框架以通用指针访问开头的成员,驱动再通过 container_of() 找回完整对象。把 vb2 挪到外层结构中间,或者只给出较小的 sizeof(struct vb2_buffer),都会破坏约定;初始化函数也不会证明任意非零结构大小都足够。
4.3 内部对象保存描述,平面另行关联像素存储
vb2_buffer 记录所属队列、索引、平面数、状态等公共信息。它的 planes[] 保存每个平面的容量和存储关联。V4L2 扩展再补充 field、sequence、flags 等接口信息。
实际像素不塞进这个小管理结构中。对本文选定的 vmalloc 后端,平面的 mem_priv 指向一个 vb2_vmalloc_buf 私有对象,该对象再记录实际存储的内核虚拟地址 vaddr、长度和引用情况。

图 3:上半部分在驱动初始化时已经存在;下半部分预览后续成功申请缓冲区后的关系。虚线表示将来才建立的索引关联,不表示本篇已经完成分配。框内嵌套表示成员内嵌,标有字段名的箭头表示指针关联。
mem_priv 不是统一的像素首地址。换一个内存后端,它可以指向另一种私有结构。因此,驱动通过 vb2_plane_vaddr() 等约定接口取得访问表示,而不是任意强转 mem_priv。
图中出现多个管理结构,也不意味着复制了多份像素。管理对象和后端对象分别服务于队列规则与内存实现,它们最终可以关联同一份图像存储。
4.4 应用的 v4l2_buffer 是接口描述
应用查询、提交或取回某块缓冲区时,传递的是 struct v4l2_buffer。比如查询索引 0,会填写类似下面的描述;这段仅示意输入字段,并未执行申请或采集:
structv4l2_bufferb = {0};
b.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
b.memory = V4L2_MEMORY_MMAP;
b.index = 0;
框架读取其中的类型和索引,再找到内部对象,或把内部结果填回接口描述。b 不是 q->bufs[0] 指向的内核对象,更不是那块 921600 字节的像素内存。
到这里,四个名字已经有了具体位置:
| 对象 |
在本例中负责什么 |
vb2_queue |
管理整组缓冲区及其使用规则。 |
vb2_buffer |
记录池中一块缓冲区的公共信息。 |
vb2_v4l2_buffer |
在通用对象上增加 V4L2 所需信息。 |
v4l2_buffer |
表达一次应用接口操作的输入和输出。 |
其中只有第三个结构是对第二个结构的内嵌扩展。第四个通过接口转换与内部对象交换信息,不应把它画成内部对象的另一个成员。
本节可对照 __vb2_queue_alloc()、vb2_vmalloc_alloc()、__fill_v4l2_buffer() 及文末配套头文件。这里只借后续分配关系理解对象,分配算法本身留给第五篇。
5. 回调怎样把公共框架和实际驱动接起来
5.1 设置函数指针,并不会当场调用函数
看一个最小连接:
.buf_queue = vimc_capture_buf_queue,
它把驱动函数的地址登记在操作表中。之后,框架执行到需要交付缓冲区的阶段,才通过这个地址调用函数。初始化操作表与执行采集,是两个不同时间发生的动作。
这种回调让公共框架不必知道 VIMC 的私有对象布局,也不必写死某个设备的启动方式。框架推进共同的流程,驱动在约定位置提供设备相关操作。
5.2 应用入口先连接 V4L2 和 VB2 的辅助函数
VIMC 的文件操作表如下:
staticconststructv4l2_file_operationsvimc_capture_fops = {
.owner = THIS_MODULE,
.open = v4l2_fh_open,
.release = vb2_fop_release,
.read = vb2_fop_read,
.poll = vb2_fop_poll,
.unlocked_ioctl = video_ioctl2,
.mmap = vb2_fop_mmap,
};
这里延续了前三篇的入口:open 使用标准文件句柄辅助函数,unlocked_ioctl 接到 video_ioctl2();映射和就绪查询则分别连接 VB2 的文件辅助函数。
标准 ioctl 进一步由另一张表接到具体命令。缓冲区相关条目是:
.vidioc_reqbufs = vb2_ioctl_reqbufs,
.vidioc_create_bufs = vb2_ioctl_create_bufs,
.vidioc_prepare_buf = vb2_ioctl_prepare_buf,
.vidioc_querybuf = vb2_ioctl_querybuf,
.vidioc_qbuf = vb2_ioctl_qbuf,
.vidioc_dqbuf = vb2_ioctl_dqbuf,
.vidioc_expbuf = vb2_ioctl_expbuf,
.vidioc_streamon = vb2_ioctl_streamon,
.vidioc_streamoff = vb2_ioctl_streamoff,
不能把这两张表都叫作“驱动操作回调”就忽略了层次。文件操作表决定一次 ioctl 先进入谁;ioctl 操作表决定该标准命令再由哪个处理器承接。驱动显式填写这些成员,连接才成立。
.read = vb2_fop_read 也不能单独证明这个队列支持 read 采集。本文这份 VIMC 的 q->io_modes 没有声明 VB2_READ,节点能力也未声明 V4L2_CAP_READWRITE。框架辅助函数存在与具体队列支持这种方式,必须分开检查。
5.3 q->ops:设备相关的布局、接收和启动规则
VIMC 提供给 VB2 的操作表是:
staticconststructvb2_opsvimc_capture_qops = {
.start_streaming = vimc_capture_start_streaming,
.stop_streaming = vimc_capture_stop_streaming,
.buf_queue = vimc_capture_buf_queue,
.queue_setup = vimc_capture_queue_setup,
.buf_prepare = vimc_capture_buffer_prepare,
.wait_prepare = vb2_ops_wait_prepare,
.wait_finish = vb2_ops_wait_finish,
};
先看几个以后会实际用到的成员。
queue_setup 从当前格式取得布局要求,告诉框架每块缓冲区需要几个平面、各平面多大。它给的是要求,不是“已经分配成功”的内存。
buf_prepare 检查提交的缓冲区是否足够大。VIMC 对应函数的准确名称是 vimc_capture_buffer_prepare(),不是根据成员名字推测出来的另一个函数。
buf_queue 接收框架交来的对象,并把它放进驱动私有列表。start_streaming 和 stop_streaming 则组织软件处理管线的运行与停止。接收一个对象、真正开始填帧,不必发生在同一个时刻。
wait_prepare、wait_finish 用于后续阻塞等待时的锁交接。VIMC 配置了标准辅助函数,但它们必须与调用路径实际持有的操作锁匹配;现在只是登记回调,不会开始睡眠等待。
这里没有配置 buf_init 或 buf_cleanup。框架支持可选回调,不表示每份驱动都必须提供全部成员,更不能在调用图中画出本例没有配置的函数。
5.4 q->mem_ops:具体的存储实现
同样需要一张表告诉核心:如何分配图像存储、如何取得访问地址、如何映射和释放。它就是 q->mem_ops。
在 vmalloc 分支中,它指向 vb2_vmalloc_memops。后续 MMAP 分配才调用其中的 alloc,最终进入 vb2_vmalloc_alloc();取得 CPU 地址使用 vaddr;建立应用映射使用 mmap;解除相应持有关系使用 put。
“内存后端”指这套具体存储实现,不是一种新的硬件模块。选择 vmalloc 后端没有取消队列状态管理,换成 DMA 后端也不会自动替驱动启动采集。
mem_ops 的 prepare/finish 与驱动 ops 的 buf_prepare/buf_finish 也不是同一组函数。前者服务于存储访问交接,后者服务于设备相关检查或收尾。当前选定的 vmalloc 后端没有提供那两个内存同步回调,不能在本例中虚构一次 DMA 缓存操作。
5.5 q->buf_ops:接口描述的适配
通用 VB2 不应该把所有管理工作都写死成 struct v4l2_buffer 的字段格式。V4L2 适配层提供 v4l2_buf_ops,负责检查和转换接口描述:
staticconststructvb2_buf_opsv4l2_buf_ops = {
.verify_planes_array = __verify_planes_array_core,
.init_buffer = __init_vb2_v4l2_buffer,
.fill_user_buffer = __fill_v4l2_buffer,
.fill_vb2_buffer = __fill_vb2_buffer,
.copy_timestamp = __copy_timestamp,
};
例如,fill_user_buffer 把内部缓冲区信息整理成应用接口描述,fill_vb2_buffer 把适配层保存的输入信息整理给通用对象使用。它们搬运的是描述字段,不是整帧像素。
init_buffer 初始化的是 V4L2 缓冲区扩展。在这份实现中,对应函数会把 request_fd 设为 -1。它不是初始化整个队列的函数,也不是驱动可选的 ops->buf_init。
这张表由稍后分析的 vb2_queue_init_name() 设置。VIMC 自己主要提供 ops 并选择 mem_ops,不必重新实现一套 V4L2 描述转换。

图 4:箭头表示函数表和对象的连接,不是初始化阶段已经执行了一次 REQBUFS、mmap 或采集。框架在后续对应操作中,才按这些连接调用函数。
到这里再归纳:ops 提供设备规则,mem_ops 提供存储实现,buf_ops 提供接口适配。三者都围绕缓冲区工作,但不是三种可互相替换的驱动方式。
6. 队列配置:每个赋值都对应一个后续约定
vimc_capture_add() 先清零分配设备对象并初始化 vcapture->lock,然后填写内嵌队列:
q = &vcapture->queue;
q->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
q->io_modes = VB2_MMAP | VB2_DMABUF;
if (vimc_allocator == VIMC_ALLOCATOR_VMALLOC)
q->io_modes |= VB2_USERPTR;
q->drv_priv = vcapture;
q->buf_struct_size = sizeof(struct vimc_capture_buffer);
q->ops = &vimc_capture_qops;
q->mem_ops = vimc_allocator == VIMC_ALLOCATOR_DMA_CONTIG
? &vb2_dma_contig_memops : &vb2_vmalloc_memops;
q->timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;
q->min_buffers_needed = 2;
q->lock = &vcapture->lock;
q->dev = v4l2_dev->dev;
这一段没有申请缓冲区,而是在给未来的申请和流转建立规则。按它的实际用途逐段读,比一次记住所有字段更容易。
6.1 type 选接口种类,不选像素格式
V4L2_BUF_TYPE_VIDEO_CAPTURE 表示这是一条单平面视频采集接口。它不表达图像是 RGB24 还是其他格式,也不携带宽高。图像布局保存在 vcapture->format 中。
这里的 CAPTURE 表示数据通过接口交给应用。节点注册使用的 VFL_TYPE_VIDEO 属于节点类别,V4L2_PIX_FMT_RGB24 属于像素排列,不能拿这三个枚举互相替代。
io_modes 是支持方式的集合。代码用按位或声明 MMAP 和 DMA-BUF,并仅在 vmalloc 分支增加 USERPTR。当前实际采用哪种方式,随后由申请接口选择;不能因为 MMAP 写在前面,就认为初始化已经进入 MMAP 模式。
6.2 buf_struct_size 是管理结构大小
sizeof(struct vimc_capture_buffer) 告诉 VB2,分配一个缓冲区管理对象时要为驱动扩展预留多少字节。它不等于 sizeimage,也不会随 RGB24 图像宽高按比例增长。
一块管理对象很小,并不能保证后续像素存储分配成功;反过来,填一个足够大的 sizeimage 也不能弥补管理结构长度不足。两种大小来自不同的对象。
6.3 min_buffers_needed = 2 不是“这里分配两块”
该字段给后续启动设置最低缓冲区要求。在这份核心实现中,它也参与 REQBUFS 的数量下限计算。
赋值为 2 并没有执行分配。首次初始化完毕、尚未收到外部申请时,q->num_buffers 仍为 0。以后即使已经分配四块,也还要看实际提交了几块,才能分析启动条件。
这两个数量现在只需区分:池中拥有多少对象,与已经交给处理流程多少对象,不是同一个数字。 具体门槛判断留在申请与启动篇分析。
6.4 drv_priv 为回调保留回到设备的入口
VB2 回调常只拿到 struct vb2_queue * 或 struct vb2_buffer *,但 VIMC 的布局和私有列表都在 vcapture 中。
因此驱动设置 q->drv_priv = vcapture。后续通过 vb2_get_drv_priv(q) 取回这个指针,就能访问当前格式。它没有创建新的设备对象,也不与每次打开的 file->private_data 等价。
这里还有一处值得按原文看清。节点私有数据登记的是:
video_set_drvdata(vdev, &vcapture->ved);
一些格式回调却把 video_drvdata(file) 的返回值赋给 struct vimc_capture_device *。这依赖 ved 恰好是外层结构的首成员,成员地址与完整对象起点相同。辅助函数只是取回存进去的指针,并不会自动查找任意内嵌成员的外层对象。
q->drv_priv 与视频节点的驱动私有数据,是两处独立存储;在本例中能到达同一份设备上下文,不意味着修改其中一处会自动同步另一处。每次打开的 fh 则仍按第三篇介绍的规则管理,不在这里重复构造。
6.5 dev 与 timestamp_flags 声明的是什么
q->dev = v4l2_dev->dev 为后续内存分配和设备相关映射提供默认设备关联。它不是图像存储地址,也不表示把像素交给 Sensor 去分配。
V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC 声明结果采用的时间戳类型。它不在初始化时拍摄图像,也不会自动读取 Sensor 曝光时刻。VIMC 后续处理函数会用 ktime_get_ns() 填写时间戳,声明与实际填值需要配合。
6.6 同一把互斥锁被交给两个入口
队列的 q->lock 和稍后节点的 vdev->lock,都指向 &vcapture->lock。这是两个指针指向同一把互斥锁,不是自动创建了两把锁。
采用标准 ioctl 分发时,框架会按命令类别选择操作锁;VB2 核心 API 并不因为 q->lock 非空,就替所有直接调用者自动取得它。驱动自己调用底层接口时,仍需遵守串行化约定。
qlock 则保护 VIMC 私有列表。它与 VB2 内部稍后初始化的 done_lock 也不是同一个锁。这里先记住锁与所保护对象的对应关系,等待阶段的释放和重获过程留到完成、出队篇。
7. vb2_queue_init:V4L2 适配层先补齐接口规则
7.1 入口很短,但下一层的工作不能省略
intvb2_queue_init(struct vb2_queue *q)
{
return vb2_queue_init_name(q, NULL);
}
调用链是 vb2_queue_init() 进入 vb2_queue_init_name(q, NULL),再由后者调用 vb2_core_queue_init()。前一层知道 V4L2 接口约定,后一层建立通用队列设施。
vb2_queue_init_name() 首先检查指针和时间戳标志。标志包含不允许的位会返回 -EINVAL;时间戳类型为 UNKNOWN 的单独检查则只触发告警,不在那一处直接返回错误。不能把所有 WARN_ON 都解释成同一种失败路径。
它还核对 VB2 内存模式与 V4L2 对应枚举的数值是否一致,保证两层接口协作时采用相容定义。这是内部约定检查,不是替应用选择内存模式。
7.2 V4L2 特有的设置在这里完成
以下是其中连续的一段配置,去掉说明性注释:
if (q->buf_struct_size == 0)
q->buf_struct_size = sizeof(struct vb2_v4l2_buffer);
q->buf_ops = &v4l2_buf_ops;
q->is_multiplanar = V4L2_TYPE_IS_MULTIPLANAR(q->type);
q->is_output = V4L2_TYPE_IS_OUTPUT(q->type);
q->copy_timestamp = (q->timestamp_flags & V4L2_BUF_FLAG_TIMESTAMP_MASK)
== V4L2_BUF_FLAG_TIMESTAMP_COPY;
q->quirk_poll_must_check_waiting_for_buffers = true;
驱动没有指定扩展大小时,默认使用 sizeof(struct vb2_v4l2_buffer)。VIMC 已经给出自己的扩展大小,这个非零值会保留。
buf_ops 在这里明确设置为 v4l2_buf_ops。is_multiplanar 与 is_output 则由队列类型推导。VIMC 的单平面 CAPTURE 对应二者均为 false。
copy_timestamp 用于标记时间戳复制策略。VIMC 选择 MONOTONIC,并非 TIMESTAMP_COPY,因此这不是要求从应用输入中照抄时间戳的配置。
最后的 poll 兼容开关只是给后续就绪判断保留规则。初始化不会因为设置这个位,就进入 poll、睡眠或唤醒流程。
若没有传入名称,适配层把 q->name[0] 清零,随后通用核心再生成调试名称。这个队列名称不是 /dev/videoX 的节点编号。
8. vb2_core_queue_init:建立一条空队列
8.1 先检查队列是否有基本工作条件
核心函数的第一组检查是:
if (WARN_ON(!q) ||
WARN_ON(!q->ops) ||
WARN_ON(!q->mem_ops) ||
WARN_ON(!q->type) ||
WARN_ON(!q->io_modes) ||
WARN_ON(!q->ops->queue_setup) ||
WARN_ON(!q->ops->buf_queue))
return -EINVAL;
队列必须有操作表、内存后端、非零类型和支持方式,并且提供 queue_setup 和 buf_queue。后面虽然还没有申请和提交缓冲区,框架仍要先确认必要的协作入口存在。
这一组条件没有逐项检查所有可选回调,也没有验证所有内存模式所需的具体后端函数。某项检查没有在初始化出现,不等于后续使用时可以忽略对应契约。
本实现还拒绝两种 Request 配置组合:要求 Request 却未声明支持,以及支持 Request 同时设置非零 min_buffers_needed。后一条与该版本的请求提交和启动错误传播约束有关。VIMC 清零分配对象后没有启用 Request,因此本篇的 min_buffers_needed = 2 不与它冲突。这里不把条件推广到其他版本。
8.2 链表、锁和等待位置逐项建立
检查通过以后执行:
INIT_LIST_HEAD(&q->queued_list);
INIT_LIST_HEAD(&q->done_list);
spin_lock_init(&q->done_lock);
mutex_init(&q->mmap_lock);
init_waitqueue_head(&q->done_wq);
q->memory = VB2_MEMORY_UNKNOWN;
queued_list 用于记录后续已经提交、尚未出队的对象,done_list 用于记录已经完成、等待取回的对象。现在只建立链表头,没有向其中加入任何缓冲区。
done_lock 保护完成关系的短临界区;mmap_lock 用于后续映射和缓冲区管理之间的互斥;done_wq 是等待结果或队列状态变化时使用的等待队列。等待队列记录需要被唤醒的任务关系,本身不保存整帧像素。
这一步设置 q->memory = VB2_MEMORY_UNKNOWN,进一步说明:支持 MMAP 与已经选定 MMAP 不是同一件事。
8.3 为什么还看到结构大小和 DMA 方向
通用核心在 buf_struct_size == 0 时,才补入 sizeof(struct vb2_buffer)。从 V4L2 的初始化入口进入时,前一层已经提供 V4L2 大小,或者驱动已经提供自己的扩展大小,因此不会退回这个通用默认值。
由此可以看到适配层的必要性:V4L2 驱动应通过相应的 vb2_queue_init() 接入,而不是随意绕过接口层,再期待通用核心自动补上所有视频接口设置。
方向配置为:
if (q->bidirectional)
q->dma_dir = DMA_BIDIRECTIONAL;
else
q->dma_dir = q->is_output ? DMA_TO_DEVICE : DMA_FROM_DEVICE;
没有开启 bidirectional 时,CAPTURE 对应 DMA_FROM_DEVICE,OUTPUT 对应 DMA_TO_DEVICE。这里设置的是内存后端可使用的方向属性,没有当场调用 DMA 映射,更没有启动搬运。本文的 vmalloc 软件实例也会保留这个方向字段。
随后核心在需要时生成队列调试名称并返回 0。到此为止,仍没有调用 queue_setup()、内存后端 alloc() 或驱动 start_streaming()。
8.4 清零内存与初始化设施分别负责什么
vb2_core_queue_init() 没有对整个 vb2_queue 执行 memset()。VIMC 之前使用 kzalloc(sizeof(*vcapture), GFP_KERNEL),使内嵌队列的计数、指针和未启用的标志具有零初值。
但零字节并不等于一个已经可用的链表头、互斥锁或等待队列。因此核心还要执行 INIT_LIST_HEAD()、mutex_init() 和 init_waitqueue_head()。例如循环链表的空表头需要指向自身,不能靠全零指针代替。
反过来,只初始化几个锁和链表,也不能保证其余计数和指针已经正确。驱动负责清零并填写初值,框架负责建立指定的管理设施,两步缺一不可。
q->lock 指向的外部互斥锁仍由 VIMC 事先初始化。核心在这里初始化的是自己内嵌的 mmap_lock,不是替任意外部锁指针完成构造。
9. 回到 vimc_capture_add:准备完整以后才发布节点
9.1 默认格式在队列初始化之后设置,并不矛盾
vb2_queue_init() 成功以后,VIMC 接着执行:
INIT_LIST_HEAD(&vcapture->buf_list);
spin_lock_init(&vcapture->qlock);
vcapture->format = fmt_default;
vpix = vimc_pix_map_by_pixelformat(vcapture->format.pixelformat);
vcapture->format.bytesperline = vcapture->format.width * vpix->bpp;
vcapture->format.sizeimage = vcapture->format.bytesperline *
vcapture->format.height;
vcapture->ved.ent = &vcapture->vdev.entity;
vcapture->ved.process_frame = vimc_capture_process_frame;
vcapture->ved.vdev_get_format = vimc_capture_get_format;
vcapture->ved.dev = vimc->mdev.dev;
顺序很重要:先建立驱动私有列表和锁,再赋默认格式、算出 bytesperline/sizeimage,然后登记 process_frame 等 VIMC 实体回调。
默认格式来自前面定义的 fmt_default:宽 640、高 480、RGB24、逐行方式。vimc-common.c 的像素映射表对 RGB24 给出 bpp = 3;此处 bpp 的单位是字节/像素,不是按字母习惯猜测的位/像素。 因此得到行跨度 1920、容量 921600 字节。
先初始化队列、后补默认格式是安全的,因为队列初始化不执行需要当前格式的 queue_setup()。真正的要求是:在允许外部请求进入之前,格式、回调及其依赖对象必须都准备好。
9.2 节点如何找到刚才的那条队列
接下来填写 video_device:
vdev = &vcapture->vdev;
vdev->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING
| V4L2_CAP_IO_MC;
vdev->entity.ops = &vimc_capture_mops;
vdev->release = video_device_release_empty;
vdev->fops = &vimc_capture_fops;
vdev->ioctl_ops = &vimc_capture_ioctl_ops;
vdev->lock = &vcapture->lock;
vdev->queue = q;
vdev->v4l2_dev = v4l2_dev;
vdev->vfl_dir = VFL_DIR_RX;
strscpy(vdev->name, vcfg_name, sizeof(vdev->name));
video_set_drvdata(vdev, &vcapture->ved);
vdev->queue = q 把节点和刚刚初始化的内嵌队列连接起来。fops、ioctl_ops 连接前面讲过的两张应用入口表,v4l2_dev 给出框架归属,vfl_dir = VFL_DIR_RX 表达节点接收方向。
device_caps 声明采集、流式缓冲区接口和媒体控制器相关能力。能力声明不是执行命令:设置 STREAMING 能力不会让队列的 streaming 状态自动变成 1。
这里的 release = video_device_release_empty 也不表示整个外层对象永远不释放。VIMC 另外通过实体类型的 release 路径清理媒体实体并释放 vcapture。它反映的是内嵌节点与外层对象之间的释放分工,不能照搬成任意驱动的“空回调即可”模板。
最后才调用 video_register_device(vdev, VFL_TYPE_VIDEO, -1)。节点注册细节沿用第一篇;本篇关心的是注册之前已经准备好的那些依赖。

图 5:顺序来自 vimc_capture_add() 的实际控制流。这里只展开调用方已经写出的错误处理;被调用函数内部的回滚仍由相应实现负责。设置回调不会立即运行回调。
9.3 失败清理应该按已经完成的阶段理解
设备对象分配失败,直接返回内存不足。媒体 pad 初始化失败,则进入外层对象释放。VB2 队列初始化或节点注册失败时,调用方先清理媒体实体,再释放设备对象:
err_clean_m_ent:
media_entity_cleanup(&vcapture->vdev.entity);
err_free_vcapture:
kfree(vcapture);
return ERR_PTR(ret);
本例在这些初始化步骤中还没有申请像素缓冲区,所以不能在调用图里补出一次并不存在的“释放已采集帧”。同样,也不能把这段简短清理机械用到已经申请了额外资源、启动了异步任务的驱动里。
节点一旦发布,外部访问可能很快发生。不能把回调依赖的字段留到 video_register_device() 返回后才初始化,也不能仅凭注册函数返回的位置断言现实系统里一定没有别的执行路径已经开始操作。
10. 本篇结束时,队列究竟处于什么状态
在“首次初始化成功,尚无任何外部缓冲区请求进入”的观察条件下,可以得到下面这份状态记录。它是根据清零分配和初始化语句推导的结果,不是硬件运行日志。
| 观察对象 |
此时的状态 |
依据 |
vcapture->format |
RGB24,640 × 480,921600 字节容量 |
默认格式与驱动计算。 |
q->ops、q->mem_ops |
已连接驱动规则和所选后端 |
驱动配置。 |
q->buf_ops |
已连接 V4L2 描述适配 |
V4L2 初始化。 |
q->memory |
VB2_MEMORY_UNKNOWN |
核心显式设置。 |
q->num_buffers、q->bufs[] |
数量为 0,槽位为空 |
清零分配,尚未申请。 |
q->queued_list、q->done_list |
空链表 |
核心初始化。 |
vcapture->buf_list |
空链表 |
驱动初始化。 |
q->owner |
NULL |
还没有相应入口取得队列归属。 |
q->streaming、q->start_streaming_called |
均为 0 |
清零初值,未启动。 |
准备好了管理规则,不等于已经拥有图像存储;拥有图像存储,又不等于已经采集完成。 本篇只完成前一步。
可以沿源码做三个小检查。
首先,找到 vb2_core_queue_init(),确认它有没有调用 queue_setup 或内存后端 alloc。再回到 vimc_capture_add(),观察默认格式为什么可以安排在该调用之后。
其次,将 q->buf_struct_size 与 vcapture->format.sizeimage 放在一起看:一个来自结构体定义,一个来自图像布局计算。把前者误填为 sizeof(struct vb2_buffer),初始化并不一定替驱动拒绝这个过小值,但后续 V4L2 扩展访问就可能越界。
最后,检查 vimc_capture_fops.read、q->io_modes 和 device_caps。三者一起看,才不会把“登记了一个通用入口”误判为“队列已支持这种完整工作方式”。
配套的 examples/vimc_format_probe.c 只打开节点、查询能力和读取当前单平面格式,不执行 REQBUFS、QBUF 或 STREAMON。它能观察应用接口结果,却不能直接证明内核队列内部每个字段的值。运行前应选择空闲测试节点;其他驱动的 open 可能有额外行为。
11. 下一篇内容讲解
现在已经能够从节点找到队列,从队列找到驱动规则和内存后端,也知道一块缓冲区以后会由哪些管理对象描述。VIMC 的处理函数负责实际写入图像,但它要等后续拿到可用缓冲区并启动软件处理,才会执行。
接下来的入口就是 VIDIOC_REQBUFS。应用提出数量,V4L2 适配层把请求送入 VB2,核心再调用已经登记的 vimc_capture_queue_setup(),从当前格式得出平面大小,并建立真正的缓冲区池。
第五篇将从这个调用开始,逐段分析数量协商、管理对象分配、像素存储分配和失败回滚。本篇中还空着的 q->bufs[],届时才会逐项指向真正建立起来的对象。
如果你对 V4L2 或内核驱动有更多问题,欢迎到 云栈社区 继续交流。