4. V4L2框架与多路Camera:Video Device节点注册、Media Controller拓扑、subdev与sensor绑定、Buffer队列管理

好,咱们今天聊点硬核的。多路Camera同时输出,底层绕不开的就是V4L2框架。说实话,我刚入行那会儿,觉得V4L2就是个简单的视频采集接口,直到第一次做双路摄像头同步,才发现这里面的水有多深。

你想想看,一路摄像头还好说,注册一个video节点,配一个sensor subdev,完事。但多路呢?每个sensor都有自己的寄存器、时钟、电源域,还要保证它们的数据流不打架。这时候,V4L2的框架设计就体现出价值了。

4.1 Video Device节点注册:给每路Camera一个“身份证”

在V4L2框架里,每个摄像头对应一个video_device结构体。我习惯把它理解成“门牌号”——用户空间通过/dev/videoX来访问它。

注册一个video device,核心步骤就这几步:

// 分配video_device结构体
struct video_device *vdev = video_device_alloc();

// 设置fops和ioctl回调
vdev->fops = &my_camera_fops;
vdev->ioctl_ops = &my_camera_ioctl_ops;

// 设置v4l2_dev(指向你的camera设备)
vdev->v4l2_dev = &camera_dev->v4l2_dev;

// 设置设备号(通常用-1让内核自动分配)
vdev->minor = -1;

// 注册到内核
video_register_device(vdev, VFL_TYPE_VIDEO, -1);

这里有个坑,我踩过。多路Camera时,每个video device的v4l2_dev必须指向不同的父设备。否则,你在用户空间打开两个节点时,内核会以为你在操作同一个设备,导致资源冲突。

注意: 多路Camera场景下,每个video device的v4l2_dev必须独立分配。我曾经图省事,让两个video device共享同一个v4l2_dev,结果第二路摄像头打开时,第一路直接崩了。

另外,设备节点的命名也有讲究。默认是/dev/video0、/dev/video1这样。但如果你有多个摄像头,建议在驱动里通过video_device->name字段设置一个有意义的名称,比如CAM_MAIN、CAM_WIDE。这样调试时一眼就能看出来。

4.2 Media Controller拓扑:把“谁连谁”说清楚

多路Camera系统里,光有video device还不够。你得告诉内核:sensor的数据流经过哪里?ISP处理单元在哪?最终输出到哪个video节点?

这就是Media Controller干的事。它用图(Graph)来描述硬件拓扑。每个硬件模块是一个media_entity,模块之间的连接是media_link。

我画了一张图,帮你理解这个拓扑结构:

多路Camera Media Controller拓扑示例 Sensor 0 (subdev) Sensor 1 (subdev) ISP 0 (subdev) ISP 1 (subdev) Video Node 0 Video Node 1 图例: Sensor subdev ISP subdev Video Node 箭头方向:数据流从Sensor → ISP → Video Node

在代码里,创建这个拓扑的流程大概是:

// 1. 创建media device
struct media_device *mdev = media_device_alloc();
media_device_init(mdev);

// 2. 为每个硬件模块创建entity
struct media_entity *sensor_entity = &sensor->sd.entity;
struct media_entity *isp_entity = &isp->sd.entity;
struct media_entity *vdev_entity = &vdev->entity;

// 3. 创建连接(link)
media_create_pad_link(sensor_entity, 0, isp_entity, 0, 0);
media_create_pad_link(isp_entity, 1, vdev_entity, 0, MEDIA_LNK_FL_ENABLED);

// 4. 注册media device
media_device_register(mdev);

多路时,每个sensor-isp-video形成一条独立的pipeline。但要注意,如果ISP是硬件共享的(比如一个ISP处理两路sensor),那拓扑就复杂了,需要用到MEDIA_LNK_FL_IMMUTABLE和MEDIA_LNK_FL_DYNAMIC来标记连接是否可动态切换。

4.3 subdev与sensor绑定:让驱动认识你的硬件

subdev是V4L2里对“子设备”的抽象。说白了,sensor、ISP、MIPI桥接芯片,这些都是subdev。每个subdev有自己的ops(操作函数集),比如s_power、s_stream、enum_mbus_code等。

绑定sensor到subdev,我习惯在probe函数里做:

// 分配并初始化subdev
struct v4l2_subdev *sd = v4l2_subdev_alloc(&client->dev);
v4l2_subdev_init(sd, &sensor_subdev_ops);

// 设置subdev的名称(方便调试)
snprintf(sd->name, sizeof(sd->name), "sensor_%s", client->name);

// 注册subdev
v4l2_subdev_register(sd, &client->dev);

// 关键一步:将subdev添加到media entity
sd->entity.function = MEDIA_ENT_F_CAM_SENSOR;
media_entity_pads_init(&sd->entity, 1, &sensor_pad);

这里有个细节:MEDIA_ENT_F_CAM_SENSOR这个function type很重要。用户空间的libcamera或者gstreamer会通过这个type来识别sensor节点。如果你写错了,上层工具可能找不到你的摄像头。

小技巧: 调试时,可以用media-ctl -p命令查看当前系统的media拓扑。如果某个subdev没有显示出来,大概率是media_entity_pads_init没调,或者pads数量不对。

多路Camera时,每个sensor对应一个独立的i2c客户端。所以probe函数会被调用多次。我建议在probe里通过devm_kzalloc分配一个私有结构体,把sensor的寄存器配置、电源控制、时钟频率都存进去。这样每个sensor实例互不干扰。

4.4 Buffer队列管理:数据流的“水管”

Buffer队列是V4L2最核心的机制。用户空间通过VIDIOC_REQBUFS申请buffer,然后VIDIOC_QBUF把buffer放入队列,驱动填充数据后,通过VIDIOC_DQBUF还给用户空间。

多路Camera时,每路都有自己的buffer队列。但有个问题:如果两路Camera共用同一个DMA引擎,buffer的分配和回收就需要同步。

我遇到过这样一个场景:两路1080p@30fps的摄像头,同时采集。DMA带宽只有400MB/s,每路需要约180MB/s。如果buffer队列管理不好,DMA会溢出,导致画面撕裂。

解决方案是使用vb2_queue框架:

// 初始化vb2队列
struct vb2_queue *q = &camera_dev->vb2_q;
q->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
q->io_modes = VB2_MMAP | VB2_USERPTR | VB2_DMABUF;
q->drv_priv = camera_dev;
q->buf_struct_size = sizeof(struct camera_buffer);
q->ops = &camera_vb2_ops;
q->mem_ops = &vb2_dma_contig_memops;
q->timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;

vb2_queue_init(q);

关键在camera_vb2_ops里,你需要实现:

  • queue_setup:告诉内核你支持多少buffer,每个buffer多大。多路时,每路的buffer大小可能不同(比如主摄1080p,副摄720p)。
  • buf_prepare:准备buffer,比如分配DMA地址。我习惯在这里做cache一致性操作。
  • buf_queue:把buffer放入硬件队列。多路时,这里要加锁,防止两路同时操作DMA寄存器。
  • start_streaming:启动数据流。我建议在这里做sensor的stream on操作。
  • stop_streaming:停止数据流。记得把硬件队列里未处理的buffer全部归还。
核心要点: 多路Camera的buffer队列管理,本质上是“资源隔离”+“DMA调度”。每路有自己的队列,但DMA引擎是共享的。你需要一个全局的调度器,决定哪一路的buffer先被DMA处理。

嗯,说到DMA调度,我分享一个经验。我曾经用dmaengine框架做多路传输,每路一个dma channel。但硬件只支持4个channel,我有6路摄像头。最后只能用dma_request_chan动态申请,并在buf_queue里做优先级排队——主摄优先,副摄排队。

另外,buffer的timestamp也很重要。多路同步时,用户空间需要知道每帧的精确时间。我建议在buf_finish回调里,用ktime_get_real打上硬件时间戳。这样上层做音视频同步时,数据才靠谱。

好了,这一章的内容就这些。V4L2框架虽然复杂,但核心就是“注册节点、描述拓扑、绑定设备、管理队列”这四步。多路Camera无非是把这四步重复做,同时处理好资源竞争。

可可精品资料库