Linux驱动之USB、MIPI摄像头驱动
一、USB摄像头内部逻辑结构
一个USB摄像头必定有一个VideoControl接口,用于控制。有0个或多个Videostreaming接口,用于传输视频。在VideoControl内部,有多个Unit或Terminal,上一个Unit或Terminal的数据,流向下一个Unit或Terminal,多个Unit或Terminal组成一个完整的UVC功能设备。
之后我们的驱动程序主要是来解析这些描述符的信息,来完善各种功能。
二、UVC驱动框架
2.1 流程分析
一般有几个OT就有几个/dev/video*设备节点。在USB摄像头驱动中,一个USB摄像头 对应一个struct uvc_device结构体,uvc_device中含有usb_device 并且含有一个struct list_head entities链表,这个链表中含有每一个uvc_entity结构体(一般一个termal/unit对应一个entity结构体)。而一个videoStreaming interface 中包含摄像头所支持的格式,每种格式下支持多少的frame。一个videoStreaming interface 在Linux 中会构建出一个uvc_streaming结构体。
那么大致的流程就是 1、处理entity:解析entity,构造entity结构体,放入entity链表。2、处理videostreaming:解析videostreaming(获取format / frame),构造uvc_streaming结构体,放入streams链表。3、对于每个OT,找到完整的chain,去注册video_device。
2.2 源码分析
Linux源码路径:drivers/media/usb/uvc/uvc_driver.c
2.2.1 设备枚举过程
首先先进入到init函数-->在init函数中注册了usb_driver: usb_register(&uvc_driver.driver)-->一旦接入的摄像头设备含有videocontrol interface就会调用该driver中的probe函数。
probe函数主要做了下面几件事,注意这边是按照probe函数的流程:
a. 分配设置注册uvc_device。
b. 解析videocontrol interface,uvc_parse_control-->uvc_parse_standard_control,在这个执行期间,会为每一个entity创建一个uvc_entity结构体,并且将其插入uvc_device的entities链表中,同时一旦解析到UVC_VC_HEADER这一类的entity,会使用uvc_parse_streaming解析videostream interface中的数据,构建,设置uvc_streaming,并将其放到uvc_device的streams链表中。
c. 注册v4l2 device起辅助作用。
d. 使用uvc_ctrl_init_device解析每一个entity,根据entity中的bmcontrols来确定通过该entity可以对设备进行哪些控制。
e. 使用uvc_scan_device扫描这个设备,扫描出一条完整的视频链路chains。如果这个链路可以到达videostreaming,就表示是一条完整的链路,将其放入uvc_device的chains链表。
f. 使用uvc_register_chains函数,对于一个完整的链路,将会注册一个/dev/video*设备节点,过程如下:uvc_register_video-->uvc_register_video_device-->video_register_device
┌──────────────────────────────┐
│ struct uvc_device │
│──────────────────────────────│
│ entities → [uvc_entity list] │───┐ (控制单元/终端)
│ streams → [uvc_stream list] │───┤ (视频流接口)
│ chains → [uvc_chain list] │───┘ (完整视频链路)
└──────────────────────────────┘│├─ v4l2_device_register()├─ uvc_ctrl_init_device()├─ uvc_scan_device()└─ uvc_register_chains() → /dev/videoX
2.2.2 设备控制过程
我们先来看一下控制的调用过程,首先会对每一个创建的entity进行比较,取出每一个的bmControls(应该是一个16位的),bmControls中的每一位会生成一个struct uvc_control controls。
随后如果想设置硬件,需要指定四项内容,1. entity(哪一个entity的能力) 2.selector(什么能力) 3.size 4.offsize(寄存器相关设置)。
在内核中,uvc_control
结构体包含一个 uvc_control_info
成员,用于描述该控制项的详细信息,例如其所属的 entity(功能单元)以及对应的 selector。
在 uvc_control_info
内部,还包含一个 uvc_control_mapping
结构体,该结构体记录了与寄存器操作相关的参数,如 size、offset 等,同时还定义了一个 id 字段。
这个 id 值正是用户态程序在 ioctl
调用时传入的控制标识符,用于在内核中定位对应的控制项。因此,用户态只需提供控制的 id 与 value,即可通过内核匹配到相应的 entity
并修改其功能参数。
uvc_probeuvc_parse_control // 得到PU的entityuvc_ctr1_init_device //初始化Pu的control//从描述符里得到关键数据bmControls =entity->processing.bmControls;//比如0x7F,0x15bControlsize =entity->processing.bcontrolsize;//根据bmControls分配多个uvc_contro1结构体entity->controls=kcalloc(ncontrols,sizeof(*ctr1), GFP_KERNEL);// 对于每个controlfor(i=0;i<bcontrolsize*8;++i){ ctrl->entity=entity;ctrl->index=i;uvc_ctrl_init_ctrl(dev, ctr1);}
关键函数:
2.2.3 传输过程
这个过程涉及到两个重要的结构体。该部分需要借鉴到上一章的内容,才好理解。
在初始化vb2_queue的时候传入了对应的vb2_ops和mem_ops,mem_ops主要是做一些分配内存相关的操作,vb2_ops主要是一些与硬件相关的操作。现在让我们看下uvc_start_streaming开启传输时候进行的操作。
uvc_start_streaminguvc_video_enable(stream, 1);uvc_init_videouvc_init_video_isoc //申请URB buffer等usb_alloc_urburb->complete = uvc_video_complete; //当硬件传输完urb之后会调用这个 uvc_video_complete函数usb_submit_urb //启动usb传输
当驱动程序接受完一整帧的数据之后,uvc_video_complete被调用
uvc_video_completestream->decode(uvc_urb, buf, buf_meta); 解析urb,将其内容放入uvc_buffer中的bufferuvc_video_decode_isoc uvc_video_next_buffersuvc_queue_next_bufferusb_submit_urb 重新提交urb
从硬件相关的irqqueue取出buffer,将urb中的数据放入buffer,把当前buffer从硬件相关的irqqueue移除放入done_list,重新提交urb,应用程序从done_list中取出buffer,并且将buffer从queued_list、done_list中移除。处理完数据之后会重新将buffer放入queued_list和irqqueue。
三、MIPI摄像头硬件
对于摄像头接口,常用的是串行协议CSI,分为两类CSI-2,CSI-3,CSI-2对应的物理接口有D-PHY、C-PHY。
下图为显示接口DBI和DSI。
D-PHY接口图如下(一路信号由一对差分引脚来表示):
对于摄像头,D-PHY接口仅仅是用来传递数据:摄像头发送数据,它被称为:CSI Transmitter
主控接收数据,它被称为:CSI Receiver。
主控通过I2C接口发送控制命令,它被称为:CCIMaster(CCl名为Camera Control Interface)
摄像头接收控制命令,它被称为:CCISlave。
四、subdev和media子系统
4.1 subdev子系统
在 UVC 摄像头驱动中,确实会涉及到 subdev
(sub-device)机制。每一个 uvc_entity
结构体内部都包含一个 struct v4l2_subdev subdev
成员,用于与 V4L2 框架中的子设备接口保持一致。然而,在实际的 UVC 实现中,这些 subdev
的 ops
(操作函数集)通常为空,即并未注册任何具体的回调函数。这意味着虽然 UVC 驱动在结构上支持 subdev
概念,但其功能实现是空的或占位的。
需要注意的是,只有当内核配置项 CONFIG_MEDIA_CONTROLLER
被启用时,UVC 驱动才会真正创建并注册这些 subdev
实例,以便与媒体控制器框架(Media Controller Framework)交互。如果该选项未启用,则不会执行 subdev
的初始化过程。
此外,在 UVC 摄像头中,各个 uvc_entity
(如处理单元、选择器单元、终端等)的功能区分性较弱,且大多通过标准化的 UVC 协议接口进行统一控制。因此,没有必要为每一个 entity 都单独生成对应的 subdev
设备并提供独立的驱动程序支持。
下图展示了一个典型的 MIPI 摄像头系统结构。该系统由多个关键模块共同组成,包括 MIPI 接口、摄像头传感器硬件(Sensor)、数据解析模块(Parser)、图像信号处理器(ISP) 等部分。这些组件协同工作,从图像采集、数据传输到图像处理的整个流程,共同构成一个完整的摄像头系统。
MIPI 摄像头不像 UVC 那样把所有功能都集成在一个 USB 设备里。它通常由多个独立硬件模块组成,例如:
Sensor(图像传感器):负责图像采集;
Lens/AF 驱动器:控制镜头或对焦;
Parser(CSI-2 接口解析模块):解析 MIPI CSI-2 数据流;
ISP(Image Signal Processor):负责去噪、白平衡、色彩校正等;
Scaler / Output 节点:输出最终图像流。
这些模块往往由不同的驱动程序控制,有的属于外部 I2C 器件(比如 Sensor),有的集成在 SoC 内部(比如 ISP)。所以说MIPI摄像头需要使用 v4l2_subdev
来分别表示各个模块(Sensor、Parser、ISP 等),并通过 Media Controller 建立完整的数据流拓扑。
因此,对于每一个 subdev
驱动而言,只需专注于实现该功能单元自身的控制与数据处理逻辑,而不需要直接关心与其他模块的连接关系。
模块之间的连接关系(即数据流向、控制路径)由 Media Controller 框架 统一管理,并通过 media graph 描述。这样可以实现模块化、可复用和灵活的系统设计。
这些模块被抽象为subdev,原因有2:
1. 屏蔽硬件操作的细节,有些模块是I2C接口的,有些模块是SPI接口的,它们都被封装为subdev
2. 方便内核使用、APP使用:可以在内核里调用subdev的函数,也可以在用户空间调用subdev的函数:很多厂家不愿意公开ISP的源码,只能在驱动层面提供最简单的subdev,然后通过APP调用subdev的基本读写函数进行复杂的设置。
使用media来描述不同subdev之间的联系关系。 在media子系统中,每一个实体被称为一个media_entity,entity跟外界的联系被称为pad(可以认为是端口),端口和端口之间的连接称为media_link。 完成的信号通道称为pipeline。
4.1.1 subdev结构体
编写subdev驱动程序的时候,核心就是实现各类ops结构体的函数。其中重点是v4l2_subdev_core_ops、v4l2_subdev_video_ops、v4l2_subdev_pad_ops。
一个摄像头驱动程序中只有一个v4l2_device,其结构体中含有一个subdevs的链表,用来管理多个注册的subdev之间的关系,v4l2_subdev中的media_entity 就是为了和media子系统挂上关系,进行管理。
4.1.2 media子系统的数据结构
每一个subdev中都含有一个media_entity结构体,这个结构体是用来描述各个subdev之间的关系,现在我们来看一下这个结构体中的信息有哪些?首先num_pads表示有几个端口(输出输入端口),num_links表示有多少个连接,然后有多少个端口其media_pads结构体数组就会有几项media_pad结构体,这个media_pad结构体中含有其的flag(是输出还是吸入pad),pad数组的索引,以及属于哪一个entity。
为了给两个subdev或者说是media_entity建立连接(由另外一个驱动程序决定),需要创建一个media_link结构体,media_link中的struct media_pad * source 指向输出的source pad , 而struct media_pad * sink指向输出的pad,建立好连接之后会将这个结构体放入media_entity 的links链表。调用关系如下图所示。这些关系均会被放入media_device结构体中。
4.1.3 subdev的注册和使用
4.1.3.1 注册
subdev管理可以使用v4l2_device,将其注册进内核,并可以选择是否将接口暴露给应用程序,如果将接口程序暴露给应用程序,需要将其注册成字符设备/设备节点。
在内核中,subdev的注册过程分为两步:
1、v4l2_device_register_subdev:把subdev放入v4l2_device的链表
int v4l2_device_register_subdev(struct v4l2_deivce *v4l2_dev , struct v4l2_subdev *sd);
2、v4l2_device_register_subdev_nodes:遍历v4l2_device链表中的各个subdev,如果它想暴露给应用程序,就把它注册成一个普通的字符设备。
int v4l2_device_register_subdev_nodes(struct v4l2_device *v4l2_dev)
4.1.3.2 使用
使用可以分为两种,一种是直接在内核态使用,第二种是应用程序调用
内核态使用
在内核态可以直接调用subdev里的操作函数,也可以使用下面的宏:其主要的方法是去判断subdev中的ops.pad的function函数。
*/
#define v4l2_subdev_call(sd, o, f, args...) \({ \int __result; \if (!(sd)) \__result = -ENODEV; \else if (!((sd)->ops->o && (sd)->ops->o->f)) \__result = -ENOIOCTLCMD; \else \__result = (sd)->ops->o->f((sd), ##args); \__result; \})
应用程序调用过程(看暴露给用户程序的逻辑)
调用过程这一部分需要和前面注册的流程联系起来理解,否则容易弄不清为什么会这样调用。
在调用 v4l2_device_register_subdev_nodes()
时,系统会为每个 subdev 创建对应的字符设备节点 /dev/v4l-subdevX
,并将其 cdev
的 file_operations
设置为 v4l2_fops
。
当用户空间程序打开 /dev/v4l-subdevX
节点时,内核会根据该节点的 file_operations
调用相应的操作函数。执行 ioctl
时,就会进入到 video_device
结构体中定义的 ops
操作集,该结构体类型为 v4l2_file_operations
。
ioctl
函数经过一系列调用后,最终会进入 subdev_do_ioctl()
。在这个函数中,系统会先判断应用层传入的命令 cmd
是否被当前驱动支持,如果匹配,就会进一步调用 subdev 内部注册的 ops
,其类型为 v4l2_subdev_ops
。
这样一来,用户空间发出的 ioctl
调用最终就能落到对应子设备的具体实现函数上,实现从应用层到 subdev 的完整调用链。
┌───────────────────────────────┐
│ 用户空间 │
│ ┌───────────────────────────┐ │
│ │ open("/dev/v4l-subdev0") │ │
│ │ ioctl(fd, CMD, &arg) │ │
│ └───────────┬───────────────┘ │
└─────────────┼─────────────────┘│▼
┌────────────────────────────────────┐
│ 内核空间(V4L2 层) │
│ file_operations.v4l2_fops │
│ └─ video_ioctl2() │
│ └─ subdev_do_ioctl() │
│ └─ v4l2_subdev_call() │
│ └─ sd->ops->core->ioctl() │
└────────────────────────────────────┘│▼
┌──────────────────────────────┐
│ 驱动层(subdev 实现) │
│ struct v4l2_subdev_ops │
│ └─ .core/.video/.pad 等子集│
│ → 具体函数(sensor_ioctl等) │
└──────────────────────────────┘
4.2、媒体media子系统的注册过程和使用
4.2.1 注册过程
4.2.1.1 两个层次
media子系统的注册分为2个层次(底层描述自己,上层描述不同media_entity之间的关系):
- 各个subdev里含有media_entity,但是多个media_entity之间的关系由更上层的驱动决定。
- 更上层的、统筹的驱动:它知道各个subdev即各个media_entity之间的联系:link。
4.2.1.2 四个步骤
media子系统的注册分为4个步骤:
- 描述自己:各个底层驱动构造subdev时,顺便初始化里面的media_entity,比如这个entity有哪些pad
media_entity_pads_init(&sd->entity, SENSOR_PAD_NUM, si->sensor_pads);
- 注册自己:底层或者上层注册subdev时,顺便注册media_entity记录在media_device里
v4l2_device_register_subdev() media_device_register_entity(v4l2_dev->mdev, entity);
- 和别人建立联系:subdev之上的驱动程序决定各个media_entity如何连接,比如调用media_create_pad_link
media_create_pad_link(source, SCALER_PAD_SOURCE, sink, VIN_SD_PAD_SINK , MEDIA_LNK_FL_ENABLED);
- 暴露给应用程序使用:subdev之上的驱动程序注册media_device:media_device里汇聚了所有的media_entity。注册的时候会去分配设置一个media_devnode,设置其中的->fops。然后设置cdev,再设置fops,cdev的fops会是一个中转作用,必定会调用到media_devnode的fops。这里会创建/dev/media*节点。
media_device_register(&vind->media_dev)
4.2.2、media子系统的使用
应用程序打开/dev/media*设备节点,按照上述的注册流程来说,相当于打开了media_devnode节点,如果调用ioctl,就会使用到这个devnode的cdev提供的file_operations结构体中的ioctl函数,这个函数是一个中转函数,会调用到devnode中的media_file_operation结构体。
列举所有的link
设置link状态
获取整体的拓扑图
最近实验室出了些状况,我的源码SDK服务器停了,看不了相关的源码了,G