公司动态

【v4l2驱动驱动程序分析一】

📅 2026/7/17 4:03:14
【v4l2驱动驱动程序分析一】
V4L2驱动程序分析一1.概述1.1 V4L2 的三大核心模块1.1.1 统一控制流ioctl 接口与控制1.1.2 高能数据流videobuf2缓冲区管理1.1.3 媒体控制器Media Controller框架1.2 V4L2 驱动框架与核心结构体1.2.1 三驾马车核心结构体解析1.2.2 核心纽带三大结构体如何协同1.2.3 驱动初始化与绑定流程1.2.4 Ioctl 控制命令的底层路由机制2.v4l2三大重要结构体2.1 video_device2.1.1 结构体详解2.2.2 关键成员2.2 vb2_queue结构体详解2.2.1 关键核心字段2.2.3初始化代码段2.3 v4l2_device结构体详解2.3.1 代码2.3.2 核心成员深度解析2.4 三者的关系绑定2.4.1 三者绑定机制图2.4.2 三者在 Probe 阶段的绑定与注册流程1.概述V4L2 是 Linux 内核中关于视频、图像采集以及传输设备的统一驱动框架。简单来说无论是普通的 USB 摄像头、嵌入式 MIPI 摄像头还是复杂的硬件编解码器、视频缩放器、电视调谐器在 Linux 底层都是通过 V4L2 架构来进行管理的。它向上为用户空间提供了标准统一的 ioctl 接口向下屏蔽了各种不同硬件控制器的物理差异。如下图所示上层应用程序想要调动底层的硬件必须经过v4l2驱动程序1.1 V4L2 的三大核心模块1.1.1 统一控制流ioctl 接口与控制V4L2 设备的一切参数配置都是通过 ioctl 触发的。除了配置分辨率和像素格式外V4L2 还专门设计了一套 Query / Set Controls 机制。物理相机调节比如曝光度Exposure、白平衡White Balance、增益Gain、亮度、对比度等。1.1.2 高能数据流videobuf2缓冲区管理这是 V4L2 中最复杂的子系统。它位于内核态专门用来打通“硬件 DMA 控制器”到“用户空间内存”之间的高速数据通道。vb2 实现了底层的状态机管理、内存申请等操作函数。它向外提供了 mmap、userptr 以及现代嵌入式开发中至关重要的 dmabuf 接口。1.1.3 媒体控制器Media Controller框架在普通的 USB 摄像头中我们直接操作 /dev/video0 就能完成全部工作。但对于嵌入式 S视频链路要复杂得多链路构成它将每一个物理或逻辑器件Sensor、ISP、DMA 等抽象为一个 Entity (实体)。各个 Entity 之间通过 Pad (引脚) 和 Link (连线) 进行互联。通过 media-ctl 工具或相应的底层 API你可以动态把 Sensor 的输出和 ISP 的输入连起来甚至把同一路视频流分发到不同的硬件处理分支上去。1.2 V4L2 驱动框架与核心结构体懂了应用层的三大概念后我们要想在内核态实现或对接一个相机驱动就必须下潜到内核源码中。V4L2 驱动框架看似庞大但它的核心设计思想非常清晰将“总管”、“子器件”和“用户接口”彻底解耦。在内核中这三大角色分别对应了三个至关重要的核心结构体。在动笔写代码前先让我们通过下面这张内核架构图建立整体的空间感1.2.1 三驾马车核心结构体解析v4l2_device全局大管家struct v4l2_device 代表一个顶层的 V4L2 设备实例。它不直接和具体的硬件交互而是作为一个容器和上下文管理器。职责管理所有注册到它名下的子设备维护它们的内容列表并负责全局的引用计数和资源释放。白话理解它是整个相机系统的“大本营”。一个复杂的 SOC 可能会有多个摄像头输入每个输入通道对应一个 v4l2_device。v4l2_subdev硬件子设备抽象struct v4l2_subdev 代表视频链路上具体的一个物理或逻辑硬件节点。职责封装具体的硬件操作。比如物理 Camera Sensor控制曝光、白平衡、MIPI PHY 接收器、ISP 处理单元等在内核里都会被抽象为一个 v4l2_subdev。控制路由子设备会向上注册到 v4l2_device。当大管家收到控制指令时可以通过它给具体的子设备下达命令。video_device用户态的“代言人”struct video_device 是真正向用户空间暴露接口的结构体。职责它在内核中注册后会在系统的 /dev/ 目录下生成我们熟悉的字符设备节点如 /dev/video0。它负责对接应用层的 open、close、read、write 以及海量的 ioctl 调用。数据流绑定它不仅处理控制指令还要绑定 videobuf2 队列是高速视频数据流进出内核的门户。1.2.2 核心纽带三大结构体如何协同简单来说video_device 负责对接用户v4l2_subdev 负责对接硬件而 v4l2_device 则是将它们捏合在一起的粘合剂。它们之间的引用和包含关系如下v4l2_device 内部维护着一个双向链表 subdevs所有属于该系统的 v4l2_subdev 在初始化时都会通过 v4l2_device_register_subdev() 挂载到这个链表上。video_device 内部持有一个指向 v4l2_device 的指针这样当用户对 /dev/video0 发起操作时video_device 就能通过大管家轻松找到底下的各个子设备并把指令分发下去。1.2.3 驱动初始化与绑定流程在编写具体的平台驱动时我们需要在驱动的 probe 函数中按照严格的先后顺序将这三者初始化并绑定。顺序如果搞错很容易导致内核 OOPS 崩溃。1.初始化并注册 v4l2_device首先分配 v4l2_device 内存并调用 v4l2_device_register() 进行注册。这一步会初始化全局链表和锁作为后续所有器件的容器。2.注册并挂载各子设备v4l2_subdev加载 Sensor、MIPI 接收器等驱动。这些驱动会初始化各自的 v4l2_subdev然后通过v4l2_device_register_subdev() 将自己注册到第一步创建的v4l2_device 中。3.初始化 videobuf2 缓冲区队列在注册视频节点前必须先准备好数据流通道。初始化 struct vb2_queue配置内存分配类型、缓冲区类型以及底层的内存操作函数集。4.注册 video_device 字符设备初始化 struct video_device将其 queue 指针指向第三步的 vb2_queue将 v4l2_dev 指针指向第一步的 v4l2_device。最后调用video_register_device()此时 /dev/video0 节点正式在用户态可见。1.2.4 Ioctl 控制命令的底层路由机制当应用层调用 ioctl(fd, VIDIOC_S_CTRL, ctrl) 企图修改相机的曝光时内核是如何完成这一链路的控制命令路由链用户态 ioctl→\rightarrow→/dev/video0 (video_device)→\rightarrow→v4l2_ioctl_ops 回调→\rightarrow→v4l2_device 广播→\rightarrow→具体的 v4l2_subdev (Sensor 驱动)→\rightarrow→I2C 物理写入 Sensor 寄存器。2.v4l2三大重要结构体2.1 video_device2.1.1 结构体详解structvideo_device{/* 1. 基础信息与设备节点 */conststructv4l2_file_operations*fops;/* 字符设备操作函数集 */structdevicedev;/* 内核统一设备模型中的 device */structv4l2_device*v4l2_dev;/* 指向全局大管家 */charname[32];/* 设备节点名称用于 debug */intnum;/* 设备次设备号如 /dev/videoX 中的 X *//* 2. 核心控制链 */conststructv4l2_ioctl_ops*ioctl_ops;/* V4L2 专属 ioctl 回调 */structv4l2_ctrl_handler*ctrl_handler;/* 控制句柄曝光、白平衡等 *//* 3. 数据流绑定 */structvb2_queue*queue;/* 绑定的 videobuf2 队列 *//* 4. 内存管理与生命周期 */void(*release)(structvideo_device*vdev);/* 必须实现的释放回调防止内存泄漏 */};如上面代码所示ioctl_ops就是上面提到过的操作底层函数的回调queue即为上面存放数据的成员2.2.2 关键成员fops (struct v4l2_file_operations)标准的字符设备文件操作集如 open, release, read, write, mmap, poll。在现代 V4L2 驱动中我们几乎不需要自己去实现繁琐的 ioctl 逻辑。我们会把 fops 中的 .unlocked_ioctl 成员直接指向 V4L2 核心提供的辅助函数 video_ioctl2。ioctl_ops (struct v4l2_ioctl_ops)V4L2 专属的控制命令回调集包含数十个回调如 vidioc_querycap, vidioc_g_fmt, vidioc_reqbufs 等。当用户调用 ioctl() 时调用流会先进入 fops-unlocked_ioctl即 video_ioctl2然后由 V4L2 核心层进行参数解析和安全检查最后精准分流到 ioctl_ops 中对应的具体回调函数上比如分配内存时分流到 vidioc_reqbufs。queue (struct vb2_queue )绑定该视频设备的数据缓冲区队列通常由 videobuf2 框架管理。这个指针是控制流向数据流转化的核心枢纽。当应用层通过 ioctl 下发诸如 REQBUFS请求缓冲、QBUF入队、DQBUF出队等流媒体控制命令时如果驱动在 video_device 中绑定了这个 queueV4L2 核心层会自动接管这些命令并直接调用 vb2 框架的对应函数进行处理。v4l2_dev (struct v4l2_device ) 指向该设备注册到的 V4L2 “全局大管家”。一个系统里可能会有多个 /dev/videoX比如一个双目摄像头会生成两个节点但它们通常都属于同一个物理设备。通过将v4l2_dev 指向同一个大管家可以方便地共享资源、同步状态并进行统一的日志打印。ctrl_handler (struct v4l2_ctrl_handler ) 视频控制属性句柄。 如果该字段不为空V4L2核心层会自动把这些控制项暴露给应用层。应用层调用 VIDIOC_S_CTRL 调节亮度时核心层会自动通过这个 handler找到对应的硬件控制逻辑。2.2 vb2_queue结构体详解structvb2_queue{unsignedinttype;/* 【配置】双向队列类型如 V4L2_BUF_TYPE_VIDEO_CAPTURE */unsignedintio_modes;/* 【配置】支持的 I/O 方式VB2_MMAP, VB2_USERPTR, VB2_DMABUF */structdevice*dev;/* 【设备】分配内存时使用的物理设备指针主要用于 DMA 映射 */structmutex*lock;/* 【同步】互斥锁用于序列化应用层发起的队列操作 ioctl */void*drv_priv;/* 【私有】驱动自定义上下文通常指向驱动的主结构体如 my_camera_dev */unsignedintbuf_struct_size;/* 【配置】驱动自定义 Buffer 结构体的大小必须包含 v4l2_buffer */u32 timestamp_flags;/* 【属性】时间戳类型如 V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC */gfp_tgfp_flags;/* 【属性】分配内存时的内核 GFP 标志如 GFP_DMA32 */unsignedintmin_buffers_needed;/* 【流控】启动 Streaming 前应用层必须要压入队列的最小 Buffer 数量 *//* * 核心回调函数集指针 * */conststructvb2_ops*ops;/* 【回调】驱动必须实现的回调如 queue_setup, start_streaming */conststructvb2_mem_ops*mem_ops;/* 【回调】底层物理内存分配器回调如 vb2_dma_contig_memops */conststructvb2_buf_ops*buf_ops;/* 【回调】具体子系统如 v4l2 / cec专属的 buffer 辅助回调 *//* * 缓冲区池Buffer Pool管理 * */structvb2_buffer*bufs[VB2_MAX_FRAME];/* 【池】核心 Buffer 指针数组保存当前已分配的所有 Buffer 实例 */unsignedintnum_buffers;/* 【池】数组中当前已分配的 Buffer 总数量 *//* * 缓冲区流转状态链表数据流调度的灵魂 * */structlist_headqueued_list;/* 【链表】应用层已经 QBUF 传入、但底层硬件 DMA 还没消费的 Buffer 链表 */unsignedintqueued_count;/* 【计数】queued_list 中目前挂载的 Buffer 数量 */structlist_headdone_list;/* 【链表】底层硬件 DMA 已经灌满数据等待应用层 DQBUF 取走的 Buffer 链表 */spinlock_tdone_lock;/* 【自旋锁】保护 done_list 的自旋锁防止硬件中断与应用层读取冲突 */wait_queue_head_tdone_wq;/* 【等待队列】当用户态阻塞调用 DQBUF 但没有空闲数据帧时在此处休眠等待唤醒 *//* * 状态机控制标志位位域设计节省空间且便于读写 * */unsignedintstreaming:1;/* 是否已经开启了视频流传输 */unsignedintstart_streaming_called:1;/* start_streaming 回调函数是否已被成功调用 */unsignedinterror:1;/* 队列是否发生了致命硬件错误 */unsignedintwaiting_for_buffers:1;/* 当前是否因为没有数据帧而处于等待状态 */unsignedintis_multiplanar:1;/* 是否是多平面视频格式即多通道格式如 Y, U, V 分开存储 */unsignedintis_output:1;/* 该队列是输入端Capture还是输出端Output如编码器输入 */unsignedintcopy_timestamp:1;/* 是否需要强制复制时间戳 */};2.2.1 关键核心字段queued_list当用户调用 QBUFBuffer 会首先挂入 queued_list表示这些内存可以被硬件使用了。done_list硬件中断到来、一帧数据采集完毕驱动在中断处理函数中调用 vb2_buffer_done(vb, VB2_BUF_STATE_DONE)。此时这个 Buffer 会脱离驱动挂载到 done_list。同时内核唤醒在 done_wq 上因 DQBUF 阻塞的用户进程。2.2.3初始化代码段/* 初始化 vb2_queue */qcam-queue;q-typeV4L2_BUF_TYPE_VIDEO_CAPTURE;q-io_modesVB2_MMAP|VB2_DMABUF;// 允许应用层做 mmap 或直接传 dmabufq-drv_privcam;// 绑定上下文q-opsmy_vb2_ops;q-mem_opsvb2_dma_contig_memops;// 【硬核】物理连续内存分配器走 CMA 内存池q-buf_struct_sizesizeof(structmy_buffer);q-timestamp_flagsV4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;q-min_buffers_needed2;// Ping-Pong 双缓冲设计必须至少 2 帧才能开流q-lockcam-queue_lock;// 让 V4L2 核心层自动帮我们做 ioctl 互斥保护q-devcam-dev;2.3 v4l2_device结构体详解2.3.1 代码structv4l2_subdev{#ifdefined(CONFIG_MEDIA_CONTROLLER)/* 用于 Media Controller 框架的实体结构负责管理拓扑结构和 Pad 连接 */structmedia_entityentity;#endif/* 指向该子设备注册到的 V4L2 主设备 (v4l2_device) */structv4l2_device*v4l2_dev;/* 核心操作函数集驱动的核心业务如开关流、配置分辨率都实现于此 */conststructv4l2_subdev_ops*ops;/* 供 V4L2 框架内部调用的操作函数集如 open/close 时的钩子 */conststructv4l2_subdev_internal_ops*internal_ops;/* 控制句柄用于管理该子设备支持的各种 V4L2 Controls如曝光、白平衡、增益 */structv4l2_ctrl_handler*ctrl_handler;/* 子设备的名字要求在整个系统中唯一通常表现为 ov5640 1-003c 形式 */charname[V4L2_SUBDEV_NAME_SIZE];/* 子设备的特征标志例如 V4L2_SUBDEV_FL_HAS_DEVNODE 表示是否在 /dev 下生成独立的设备节点 */u32 flags;/* 指向底层硬件的设备结构体指针如 i2c_client-dev 或 spi_device-dev */structdevice*dev;/* 如果 flags 开启了 HAS_DEVNODE内核会自动创建并指向这个生成的字符设备节点 */structvideo_device*devnode;/* 存储驱动特有的私有数据指针通常用 v4l2_set_subdevdata() 来绑定 */void*dev_priv;void*host_priv;/* 异步注册与通知机制相关结构体 */structv4l2_async_notifier*notifier;structv4l2_async_subdev*asd;/* ... 后面还有若干用于内核内部同步、流管理(stream)、上下文引用的私有字段 */};2.3.2 核心成员深度解析ops (struct v4l2_subdev_ops)这是写驱动时最花精力实现的地方。它内部并不是扁平的而是按功能划分类别的子结构体指针entity (struct media_entity) 当系统启用 MC (Media Controller)架构时这个结构让子设备在拓扑图中变成一个可见的“节点”。你可以使用用户空间的 media-ctl 工具看到它并使用 Link 将它和ISP 或 DMA 关联起来。ctrl_handler 如果你的摄像头 Sensor 支持硬件调节亮度、曝光等你需要初始化这个handler并把对应的 control 注册进去。2.4 三者的关系绑定2.4.1 三者绑定机制图2.4.2 三者在 Probe 阶段的绑定与注册流程注册逻辑1.先注册 v4l2_device 奠定软件框架基石。它是所有组件的宿主。如果没有它子设备subdev和字符设备video_device就找不到“归属”后续的辅助函数如v4l2_err 等打印函数也无法正常工作。2.再绑定 v4l2_subdev (物理器件)打通底层硬件控制通道。在将设备暴露给用户之前必须先控制好底层的 Sensor、Deserializer 等硬件。把 subdev 注册进大管家大管家就能统一管理控制句柄ControlHandler方便后续把 Sensor 的曝光、增益等控制接口合并暴露给用户。3.接着初始化 vb2_queue (数据缓存准备)配置 DMA 内存分配规则。此时需要把内存分配器、互斥锁、队列类型等全部填妥。只有队列初始化成功video_device 才有底气去声明自己支持Streaming数据流传输。4.最后一步注册 video_device (生成设备节点)将一切成果奉献给用户态。将 v4l2_dev 和 queue 的指针塞进 video_device然后调用 video_register_device。记住这一步必须放在最后。因为一旦注册成功用户态如OpenCV、FFmpeg就能立刻通过 /dev/videoX 执行 open/ioctl。如果此时你的队列或子设备还没初始化好就会瞬间发生内核空指针崩溃。代码示例structmy_camera_dev{structdevice*dev;structv4l2_devicev4l2_dev;/* 全局大管家 */structvideo_devicevdev;/* 用户空间接口 */structvb2_queuequeue;/* 数据流队列 */structv4l2_subdev*sensor_sd;/* 从设备如传感器 */structmutexlock;/* 互斥锁 */};staticintmy_camera_probe(structplatform_device*pdev){structmy_camera_dev*cam;intret;/* 1. 分配驱动主上下文结构体 */camdevm_kzalloc(pdev-dev,sizeof(*cam),GFP_KERNEL);if(!cam)return-ENOMEM;cam-devpdev-dev;mutex_init(cam-lock);/* 2. 注册 v4l2_device (大管家) */retv4l2_device_register(cam-dev,cam-v4l2_dev);if(ret)returnret;/* 3. 绑定并注册子设备 v4l2_subdev (如 Sensor) */// 注实际开发中Sensor subdev 往往通过 v4l2-async (异步通知机制) 匹配后绑定。// 这里为了展示关系简化为直接绑定cam-sensor_sdget_sensor_subdev_somehow();retv4l2_device_register_subdev(cam-v4l2_dev,cam-sensor_sd);if(ret)gotoerr_v4l2_unreg;/* 4. 初始化并绑定 vb2_queue (数据通道) */cam-queue.typeV4L2_BUF_TYPE_VIDEO_CAPTURE;cam-queue.io_modesVB2_MMAP|VB2_DMABUF;cam-queue.drv_privcam;/* 关键绑定queue 绑定驱动上下文 */cam-queue.opsmy_vb2_ops;cam-queue.mem_opsvb2_dma_contig_memops;cam-queue.lockcam-lock;/* 关键绑定让 V4L2 核心层自动帮我们上锁 */cam-queue.devcam-dev;retvb2_queue_init(cam-queue);/* 内部会初始化 queued_list 和 done_list */if(ret)gotoerr_subdev_unreg;/* 5. 初始化并注册 video_device (字符设备节点) */strscpy(cam-vdev.name,my-camera,sizeof(cam-vdev.name));cam-vdev.v4l2_devcam-v4l2_dev;/* 关键绑定video_device 绑定大管家 */cam-vdev.queuecam-queue;/* 关键绑定video_device 绑定数据队列 */cam-vdev.fopsmy_fops;cam-vdev.ioctl_opsmy_ioctl_ops;cam-vdev.releasevideo_device_release_empty;/* 避免内存泄露 */// 这一步是终点一旦成功用户空间就会出现 /dev/videoXretvideo_register_device(cam-vdev,VFL_TYPE_VIDEO,-1);if(ret)gotoerr_subdev_unreg;platform_set_drvdata(pdev,cam);return0;err_subdev_unreg:v4l2_device_unregister_subdev(cam-sensor_sd);err_v4l2_unreg:v4l2_device_unregister(cam-v4l2_dev);returnret;}