梳理Linux系统下USB子系统的工作流程,USB总线注册、HCD注册两个切入点,内容根据官方资料、内核源码及个人理解。
一、USB 总线注册
1.猜测
SoC作为host,应该要创建一条USB总线,搭建一个USB系统
SoC作为host,应该能够管理USB设备,包括设备的接入和移除等
2.验证猜测
通过分析内核源码,当内核启动后,会调用subsys_initcall(),执行到usb_init(void),usb_init函数在定义时用了__init修饰,于是连接脚本会将该函数放到_init段中,而subsys_initcall会从该段中执行初始化函数。研究USB到这里即可,具体内核如何调用,应该单独放到内核篇中,这里就不深究了。
那么usb_init(void)做了哪些事情?
1.注册一条总线类型:bus_register(&usb_bus_type)
该总线类型包含有两个链表,一个是device链表,一个是driver链表,还提供了一个match函数,作用是当新设备插入时为该新设备匹配驱动,当有新驱动时为新驱动匹配设备
usb_bus_type定义
struct bus_type usb_bus_type = {
.name = “usb”
.match = usb_device_match,
.uevent = usb_uevent,
.need_parent_lock = true,
};
- 为USB总线注册一个内核通知链
当USB总线上状态发生变化时,就会执行通知链中的函数,具体怎么实现不需要关心,由内核控制;bus_register_notifier(&usb_bus_type, &usb_bus_nb)
3.申请一个USB设备号,向内核注册一个设备: usb_major_init()->register_chrdev(USB_MAJOR, “usb”, &usb_fops)
这里需要注意,前面的操作都是针对USB总线的,而这里的设备号是给host设备自身的注册的是一个字符设备,带的参数usb_fops是字符设备的操作函数集向内核注册一个设备(host)驱动usb_register(&usbfs_driver)
4.创建一个虚拟hub:
usb_hub_init();
向内核注册一个设备(hub)驱动:usb_register(&hub_driver),启动hub监测的工作队列workqueue: alloc_workqueue(“usb_hub_wq”, WQ_FREEZABLE, 0);
5.为总线类型注册一个通用USB设备(device)驱动: usb_register_device_driver(&usb_generic_driver, THIS_MODULE);
向内和注册一个设备(device)驱动:driver_register()
小结一下
注册一条总线类型,并为总线注册内核通知链
注册一个字符host设备,驱动为usbfs_driver,可以忽略
注册一个虚拟hub设备,驱动为hub_driver
为总线注册一个通用usb驱动,usb_generic_driver
二、HCD初始化
1.疑惑和答案
第一点USB的初始化中,应该说大部分的工作,还是给内核的,比如创建总线类型、通知链、工作队列、虚拟hub等。而HCD的大部分工作是给主机控制器的。
有个前提,HCD是以“设备-总线平台-驱动”模型存在的,于是引出三个问题:
1.platform_device设备从哪里来?
2.platform_bus平台总线从哪里来?
3.如何工作?
第二个问题好解答,当内核启动时,会自动创建一个platform_bus。第一个问题,内核启动后,会加载设备树文件,解析其中的设备树节点,将满足平台设备要求的设备树节点,创建成平台设备platform_device。所以,要工作起来,首先需要在设备树中定义满足平台要求的节点,包含compatible属性、子节点包含compatible属性且值为”simple-bus”,”simple-mfd”,”isa”,”arm,amba-bus”,会被转成platform_device,当解析完设备树之后,得到了platform_device并向内核注册,当内核发现了这个平台设备,就会去调用总线链表上的match函数,来查找总线上的driver链表为该设备匹配对应的platform_driver,当platform_driver和platform_device匹配一致,则执行platform_driver里面的probe函数。
再提第一个问题:platform_bus上的driver怎么来?
在编写平台驱动的时候,需要用module_init向内核注册一个模块驱动,这时候内核就将这些模块驱动放到一起。
再提第二个问题:内核如何platform_driver对应到这个platform_device?每一条总线都有一个match函数,在device中有compatible属性信息、设备名称、ID信息,驱动中也有一个ofmatch_table表,也包含了属性、名称等信息,那match函数一比较就知道了
2.HCD注册
以dwc2为例,platform驱动存放在driver/usb/dwc2目录下,入口在platform.c中,module_platform_driver(dwc2_platform_driver),其中dwc2_platform_driver为dwc2模块驱动的结构体,当内核解析设备树生成dwc2的平台设备后,内核根据设备of_match_table信息查找对应的驱动,匹配成功找到驱动后调用驱动的probe接口:dwc2_driver_probe(struct platform_device *dev),做以下几件事:
1.定义设备结构体,并申请内存
获取IORESOURCE_MEM 资源,即硬件模块的物理地址,通过ioremap将物理地址转换成虚拟地址:devm_ioremap_resource;
2.配置模块寄存器
相关的host配置
3.获取设备中断号,并向内核申请中断,和处理函数
4.判断设备角色
角色为host模式则执行dwc2_hcd_init,为peripheral则执行dwc2_gadget_init,仅分析host
- 创建一个HCD
hcd = usb_create_hcd(&dwc2_hc_driver, dev, name),创建一个主机控制器hcd。创建主机控制器,申请内存,主机控制器对应有一条usb总线,初始化该usb总线,将hc_driver挂到hcd上
- 添加HCD
usb_add_hcd(hcd),hcd中的usb总线向内核注册。当hcd注册完之后,需要为hcd创建一个虚拟设备roothub:register_root_hub(hcd)
roothub虽然是虚拟设备但是也是设备,调用usb_new_device(usb_dev) 来创建一个新设备并添加到USB总线上。
一添加roothub平台设备,内核就会去匹配对应的平台驱动,最后匹配到前面usb core中注册的usb_generic_driver,执行它的probe函数。generic_driver的probe函数,向内核添加一个roothub
7.小结一下
1. 注册一个主机控制器的平台设备hcd,一个hcd对应一条usb总线,一个hcd有一个roothub的usb设备。当往hcd上添加一个roothub的usb设备时,会匹配到通用驱动generic_driver,并执行该驱动的probe接口
2. roothub主要功能是监控hcd对应的usb总线是否有设备插入,将roothub的中断回调函数设置为hub_event,后续再有新设备插入时,都会在hub_event中被检测到,检测到之后则添加一个新的usb设备,对应的generic_driver的probe接口再次被调用
三、总结
第一段描述了总线在上电时创建了一条USB总线类型,并在总线上注册了一个usb的通用驱动, usb_generic_driver;第二段描述了向内核注册一个主机控制设备HSD, HCD自己有一条总线self,然后再self上再挂上一个roothub的device,于是内核开始调用usb_generic_driver的probe函数,那么问题来了:
hcd的self和前面注册的usb_bus_type,这两条总线是同一条吗?
如果不是同一条,为什么hcd的roothub,会调用usb总线上的通用驱动?
如果是同一条,这两者是单独创建的,在哪里统一?
带着问题又梳理了下,我是这样理解的,usb_bus_type是一个总线类型,bus_register是向内核注册一个总线类型,而后面的usb_bus_register是注册该总线类型的一条总线,该类型的总线也拥有该类型的驱动和匹配函数,所以创建hcd里面的usb总线,把roothub挂上去时就能调用该类型的驱动了。
理解汇总:
- 系统启动时调用usb_init,向内核注册一个USB的总线类型,并为它添加了通用驱动、匹配函数以及分配一个内核通知链;
- 系统解析设备树之后得到了一个平台设备,会根据match table去查找平台驱动;
- 平台驱动中对USB模块进行初始化并创建一个usb主机控制设备HCD,并为HCD创建一条USB总线;
- HCD需要一个roothub的虚拟USB设备,于是创建第一个USBdevice并添加到USBbus上,当总线被添加了设备,就会调用usb_generic_driver的probe接口,剩下的工作交给roothub,roothub会监控总线上的状态变化,当有设备插入时,hub会启动枚举流程,枚举完成之后,将新设备添加到USB总线,再次调用generic_driver的probe接口。