【v4l2驱动程序分析(三)从源码分析uvc驱动,mediacontrolller和v4l2驱动程序】
1全局架构与拓扑图景在内核中一个 UVC 摄像头的驱动并不是孤立存在的。它处于以下三层架构之中如上图所示分为用户层内核层和硬件层应用层主要是做一些软件用到的各种框架有很多不做研究内核层就是管接入设备后该怎么做的事情这部分应该仔细研究下2.UVC驱动初始化源码分析2.1 驱动注册与探针入口 (uvc_driver.c)当 UVC 摄像头插入 USB 接口时USB 核心总线会匹配并调用 uvc_probe。// drivers/media/usb/uvc/uvc_driver.cstaticintuvc_probe(structusb_interface*intf,conststructusb_device_id*id){structuvc_device*dev;// 1. 分配 uvc_device 结构体它是 UVC 驱动的核心控制块devkzalloc(sizeof(*dev),GFP_KERNEL);// 2. 初始化 UVC 设备的 Media Controller 媒体设备media_device_init(dev-mdev);// 3. 解析 UVC 描述符这是 UVC 协议的核心// 该函数会遍历 USB 配置描述符找出所有的 Input Terminal, Output Terminal, Processing Unitif(uvc_parse_control(dev)0){gotoerror;}// 4. 初始化视频流分配并设置并行的/等时传输的 URB 队列if(uvc_video_init(dev)0){gotoerror;}// 5. 将解析出来的 Entities 注册到 Media Controller 框架if(uvc_mc_register_entities(dev)0){gotoerror;}// 6. 注册 V4L2 设备向上提供标准 video 节点if(uvc_register_chains(dev)0){gotoerror;}// 7. 正式向内核注册 Media Device 节点 (/dev/mediaX)media_device_register(dev-mdev);return0;}3. Media Controller 在 UVC 中的实现为了让应用层知道 UVC 设备内部有哪些 Processing Unit (PU) 或 Extension Unit (EU)驱动会在初始化时将这些单元映射为 media_entity。3.1 实体与管脚初始化为了让应用层知道 UVC 设备内部有哪些 Processing Unit (PU) 或 Extension Unit (EU)驱动会在初始化时将这些单元映射为 media_entity。3.1 实体与管脚初始化在 uvc_mc_register_entities 中UVC 驱动会为每个 UVC 实体初始化 Pads// drivers/media/usb/uvc/uvc_entity.cint uvc_mc_register_entities(struct uvc_device *dev){structuvc_entity*entity;list_for_each_entry(entity,dev-entities,list){// 确定该实体的 Pad 数量通常输入/输出端点至少有 1 个 Padunsignedintnum_padsentity-num_pads;// 初始化 Pads 并将 Entity 放入 media_device 中media_entity_pads_init(entity-vdev-entity,num_pads,entity-pads);// 注册子设备或实体media_device_register_entity(dev-mdev,entity-vdev-entity);}return0;}4. 对接 V4L2 核心层创建 /dev/videoX在 uvc_register_chains 链路中驱动会真正创建用户空间能够访问的设备节点。4.1 视频设备注册 (uvc_queue.c uvc_v4l2.c)// drivers/media/usb/uvc/uvc_driver.cstatic int uvc_register_video(struct uvc_device *dev, struct uvc_streaming *stream){structvideo_device*vdevstream-vdev;// 1. 绑定 V4L2 设备与驱动私有数据vdev-v4l2_devdev-vdev;vdev-fopsuvc_fops;// 用户空间基础操作open, release, read, mmapvdev-ioctl_opsuvc_ioctl_ops;// 核心控制操作Querycap, Format, Buffersvdev-releasevideo_device_release_empty;// 2. 告诉内核这个设备支持视频捕获vdev-device_capsV4L2_CAP_VIDEO_CAPTURE|V4L2_CAP_STREAMING;// 3. 注册字符设备生成 /dev/videoXreturnvideo_register_device(vdev,VFL_TYPE_VIDEO,-1);}4.2 核心 ioctl 控制映射关系当我们在应用层调用 ioctl 时uvc_ioctl_ops 会将其映射到具体的 UVC 操作conststructv4l2_ioctl_opsuvc_ioctl_ops{.vidioc_querycapuvc_ioctl_querycap,.vidioc_g_fmt_vid_capuvc_ioctl_g_fmt_vid_cap,// 获取分辨率和格式.vidioc_s_fmt_vid_capuvc_ioctl_s_fmt_vid_cap,// 设置分辨率和格式.vidioc_reqbufsuvc_ioctl_reqbufs,// 向 videobuf2 申请缓冲区.vidioc_querybufuvc_ioctl_querybuf,// 查询缓冲状态.vidioc_qbufuvc_ioctl_qbuf,// 将空缓冲区入队.vidioc_dqbufuvc_ioctl_dqbuf,// 填充好数据的缓冲区出队.vidioc_streamonuvc_ioctl_streamon,// 开启视频流.vidioc_streamoffuvc_ioctl_streamoff,// 关闭视频流};5. 数据传输链路分析从 USB 中断到应用层我们把这 5 个关键步骤放回 Linux 内核源码的“聚光灯”下详细看看每一步的运行机制和底层代码实现。5.1. 启动流传输Stream ON一句话本质应用层下达总动员令驱动组建“URB车队”硬件拉满弓弦准备发送。当应用层调用ioctl(fd,VIDIOC_STREAMON)时内核通过 V4L2 框架层层下发最终调用 UVC 驱动的uvc_start_streaming()提交协商Commit驱动首先将用户选定的分辨率、帧率、像素格式等打包成一个 UVC_VS_COMMIT_CONTROL 结构体通过 USB 控制端点以 uvc_set_video_ctrl() 发送给摄像头硬件。摄像头收到后会调整内部的 ISP、时钟腾出硬件 FIFO。组建并初始化 URB 车队在 uvc_init_video() 中驱动会预先分配一批 URBUSB Request Block通常为 5-32 个。每个 URB 都会绑定一块物理内存用于硬件 DMA 落地和一个核心回调函数指针// 伪代码绑定回调urb-completeuvc_video_complete;把车推进轨道最后驱动调用 usb_submit_urb() 将这些空 URB 全部提交给 Linux 的 USB 核心层USB Core。此时底层 USB 主机控制器如 xHCI被激活开始在总线上定时发送 IN 令牌包向摄像头索要视频数据。5.2 USB 中断触发与 uvc_video_complete一句话本质物理总线传输完成硬件中断通知 CPU“车装满了快来处理”当摄像头把数据吐到 USB 总线上USB 控制器把这批数据自动搬运到对应 URB 的物理内存中。当一个 URB 的数据传输结束或者报错硬件会触发一个 软中断Tasklet/Top Half。进入回调USB 核心层响应中断后根据该 URB 之前绑定的指针直接跳转调用 UVC 驱动的 uvc_video_complete() 函数。状态检查驱动首先检查 urb-status。如果状态是 -EPROTO协议错误或者 -ESHUTDOWN设备拔出则丢弃该包或停止传输如果状态是 0成功则说明这是一车完好无损的硬数据。分流处理UVC 支持等时传输Isochronous和批量传输Bulk。如果是等时传输数据包是由多个连续的小 packet 组成的驱动会遍历这些小包调用关键的解析函数uvc_video_decode_isoc()。5.3 UVC 驱动解析拼包与内存拷贝一句话本质剥离 UVC 协议外壳Header将散乱的数据包碎片拼接成一幅完整的画卷。这是最消耗 CPU 算力的一步也是 MMAP 模式下唯一的内存拷贝点。因为 USB 传输是切碎的比如每个 Packet 只有 1024 字节而 V4L2 缓冲区需要一个完整的图像。在 uvc_video_decode_start() 和 uvc_video_decode_data() 中解析 UVC 头部Header每个 UVC 数据包前几个字节是 Header。驱动会去读取它的控制位FID (Frame ID)如果 FID 翻转了说明上一帧已经结束当前开始的是全新的一帧。EOF (End of Frame)如果这一位为 1代表这是当前视频帧的最后一个包。剥离并拷贝数据确认头部无误后驱动越过 Header 指针将真正的视频流有效载荷Payload利用 memcpy() 拷贝到当前激活的 videobuf2 (VB2) 缓冲区中// 伪代码逻辑memcpy(vb2_plane_vaddr,urb_packet_dataheader_len,payload_len);错误检测如果读取 Header 时发现 ERR 标志位为 1硬件告知由于带宽或噪声丢包驱动会把整个 VB2 缓冲区标记为错误防止把花屏数据喂给应用层。5.4. 通知 VB2 数据就绪一句话本质一幅画拼好了盖上“就绪”印章唤醒在门口苦苦等待的买家。当上一步检测到 EOF帧结束 标志位时意味着这块 VB2 缓冲区已经集齐了这幅图像的所有碎片。移交控制权UVC 驱动会立刻调用 V4L2 框架的通用核心 APIvb2_buffer_done(buf-vb.vb2_buf, VB2_BUF_STATE_DONE)。状态流转VB2 核心框架接收到通知将这个 Buffer 的内部状态从 VB2_BUF_STATE_ACTIVE硬件正在写入正式修改为 VB2_BUF_STATE_DONE数据已就绪。唤醒等待队列VB2 内部维护着一个等待队列Wait Queue。此时它会触发唤醒信号通知所有因为调用 DQBUF 而处于休眠阻塞状态的用户空间进程“货到了快起来提货”循环车队在移交完满载的 Buffer 后UVC 驱动会立刻从自己的空闲队列里再捞出一个空的 VB2 Buffer 顶上继续迎接下一个 URB 的数据。5.5. 应用层出队数据DQBUF一句话本质应用层进程被唤醒兴高采烈地拿走数据指针送去显示或编码。此时原本卡在 ioctl(fd, VIDIOC_DQBUF) 的应用层进程被唤醒。出队操作内核执行 vb2_core_dqbuf()从 vb2_queue 的 done_list已完成链表中弹出那个刚刚被填满的 Buffer。信息同步内核把这个 Buffer 的最新元数据例如真正的写入大小 bytesused、时间戳 timestamp、帧序号 sequence填入用户空间的 struct v4l2_buffer 结构体中。安全返回ioctl 成功返回 0。应用程序拿到返回的 buf.index缓冲区索引。因为在前期映射阶段应用层已经通过 mmap 拿到了 buffer_start[index] 的虚拟地址指针此时应用层直接访问该指针就能直接读取到热乎乎的摄像头画面复用闭环应用层消费完这帧图像后比如刷到了屏幕上必须立刻再次调用 ioctl(fd, VIDIOC_QBUF) 把这个 index 归还给内核整个物流传送带才能源源不断地滚滚向前。