目录
关键词
Host 主机
Device 设备
1.背景
在解决产品的一个老bug的过程中,意外发现产品的usb模块连接有问题,该产品使用STM32F105系列芯片,支持双角色连接,即可以同时作为Host和Device来连接到控制器或上位机。但在使用过程中发现该产品作为Device时的连接经常出现错误,刚插上数据线时连接正常,一段时间后便会无故断开,且断开的时机不定,频繁操作上位机进行读取和写入会提高发生错误的几率,这在实际使用时体验非常不好,并且该问题在其它产品中没有发现。
本文涉及到的迁移可能仅适合部分项目,可以挑着看,不对的地方欢迎指正。
2.查找问题
对比其它产品,该产品的特点在于使用了双角色进行连接(其它产品中也存在双角色连接,但分别使用一块独立芯片),在同一块芯片上使用Host-Device双角色是以往没有的。因此对比了该产品上的芯片和以往的芯片,果然不同。以往产品大多使用STM32F103系列芯片,查阅资料,发现该芯片使用的USB模块是基于USB-IP内核设计的(只能作为Device进行连接),而本产品使用STM32F105系列芯片,其USB模块是基于OTG-IP内核设计的(支持双向连接),具体如下图,其中FS表示全速,HS表示高速,另有LS表示低速没有标识出来。
既然硬件部分就有区分,那就首先检查相关驱动是否正确,果然,该产品中Device部分的驱动仍延续了其它产品的方式,没有根据OTG内核进行相应更改,这肯定是有问题的,接下来的工作就是去更换适配的驱动,并保证不影响原来的产品功能。
3.驱动移植过程
3.1 驱动文件下载
去ST官网上寻找标准固件库中与USB相关的外设驱动,在这里可以找到OTG内核的驱动:STSW-STM32046 - STM32F105/7、STM32F2和STM32F4 USB on-the-go主机和设备库(UM1021) - 意法半导体STMicroelectronicsSTSW-STM32046 - STM32F105/7、STM32F2和STM32F4 USB on-the-go主机和设备库(UM1021), STSW-STM32046, STMicroelectronicshttps://www.st.com/zh/embedded-software/stsw-stm32046.html 也有USB内核的驱动:
STSW-STM32121 - STM32F10x、STM32L1xx和STM32F3xx USB全速器件库(UM0424) - 意法半导体STMicroelectronicsSTSW-STM32121 - STM32F10x、STM32L1xx和STM32F3xx USB全速器件库(UM0424), STSW-STM32121, STMicroelectronicshttps://www.st.com/zh/embedded-software/stsw-stm32121.html 下载下来后,文件内容如下,其中Libraries里包含了标准库函数和USB驱动,而Project则是st官方给出的USB例程:
打开Libraries,主要关注以下三个文件夹:
- STM32_USB_Device_Library,包含设备类的驱动,即不同类型的设备运行所需要的驱动;
- STM32_USB_HOST_Library,包含主机类的驱动,即不同类型的主机运行所需要的驱动;
- STM32_USB_OTG_Driver,即最核心的USB驱动,包括底层的通信和初始化以及中断处理。
3.2 项目结构
从st官方给出的USB驱动组织结构图入手,如下图:最底层(左边框)为主机和设备的底层驱动,官方称为“low-lever driver”;中间层为主机和设备的驱动库,告诉芯片如何调用这些底层驱动进行USB通信;最顶层为主机和设备类的应用层,对不同的使用场景进行了分化。
3.2.1 底层驱动Low-lever driver
我们从主机和设备的底层驱动出发,如下图:最下面的CIL就是核心接口层,对硬件部分进行了抽象(就是数据结构),还包含了USB通信的各种操作。中间层定义的是主机和设备如何调度CIL的资源以及底层的回调函数(HCD和DCD,我们不会直接使用),还有中断操作(HCD INT和DCD INT),这里的H和D表示的是主机和设备。最上面则是主机和设备的应用,为下一部分内容。
这些底层驱动对应的是STM32_USB_OTG_Driver文件夹下的src源文件,是不是一目了然?
3.2.2 设备端驱动
接着看设备的驱动结构,最底层的仍然是CIL、设备回调和设备中断,如下图:
重点在中间层,左边是USB library module。告诉USB设备如何进行初始化、在通信过程中的不同状态,以及面对主机的不同请求,如何接收和传输数据。此外,USB设备进行数据传输是基于端点的概念,端点成对存在,每对端点都分为IN和OUT两个方向,注意,这里的方向都是相对于主机的,即IN表示设备发送给主机,即主机接收;而OUT表示设备接收来自主机的数据,即主机发送。
右边是USB class module,针对不同应用场景对USB设备进行区分,如HID人机交互设备、audio音视频传输设备、MSC大容量存储设备,平时在设备管理器里就能找到这些不同类型的设备(当然,有些设备不是通过USB进行连接的),如下图。区分device class的目的就是为了更好的操作不同用途的USB设备。
最顶层的是application module,即设备的应用层,这里就比较个性化了,可以直接使用设备库提供给用户的回调函数进行编写;也可以作者自己调用中间层的设备核心函数来编写应用。
3.2.3 主机端驱动
最后看主机的驱动结构,如下图:
同样的,其中间层、应用层和设备的驱动结构基本一致,不同的是,由于主机需要主动寻找设备,验证设备,请求设备,整个过程会更加复杂些,这个过程被称为“枚举(Enumeration)”,此外,主机传输数据是建立在通道(Channel)的基础之上,因此需要管理好通信管道。
3.3 驱动的迁移
3.3.1 底层驱动
结合上一节的项目结构分析,先来迁移底层核心驱动(low-level driver),即下图中的STM32_USB_OTG_Driver文件夹:
先看源代码src:
- usb_bsp_template.c, 为USB的外设驱动文件,注意这个文件只是个模板,通常需要拷贝至具体的应用文件夹下并改名为usb_bsp.c,然后再修改,主要使用到几个函数:
void USB_OTG_BSP_Init(USB_OTG_CORE_HANDLE * pdev) //外设的初始化,如USB外设连接的IO口
void USB_OTG_BSP_EnableInterrupt(USB_OTG_CORE_HANDLE * pdev) //使能USB相关中断
void USB_OTG_BSP_ConfigVBUS(USB_OTG_CORE_HANDLE *pdev) //配置VBUS电源对应的IO口
- usb_core.c,USB核心文件,包含通信过程,状态机等;
- usb_dcd.c,USB设备的底层核心,包括设备的初始化,端点通信和端点状态的相关操作;
- usb_dcd_int.c,USB设备的中断函数集,主入口(全速下)如下函数,在该函数里会通过寄存器进入到具体的中断函数中,如端点接收数据中断(DCD_HandleOutEP_ISR)。
uint32_t USBD_OTG_ISR_Handler (USB_OTG_CORE_HANDLE *pdev) //设备中断函数主入口
- usb_hcd.c,USB主机的底层核心,需要用到主机模式时再加入;
- usb_hcd_int.c,USB主机的中断函数集,需要用到主机模式时再加入;
- usb_otg.c,包含otg模式下的一些驱动配置,在这里不需要用到;
再看头文件inc,主要看几个特殊的(其它的基本和源文件相对应):
- usb_conf_template.h,OTG的配置文件,相当于USB的功能开关,根据实际需求选择全速还是高速,并修改FIFO大小,以及是否启用VBUS检测。该这个文件也只是个模板,通常需要拷贝至具体的应用文件夹下并改名为usb_conf.h,然后进行修改。
- usb_core.h,定义了一个重中之重的对象类型,即链接USB所有功能和底层寄存器的USB_OTG_CORE_HANDLE类型,不用修改;
- usb_defines.h,包括跟速度、端点、通道等相关的宏定义,通常不用修改;
- usb_regs_h,寄存器的抽象,不用修改;
3.3.2 设备层驱动
设备层驱动包括通用的设备核心驱动Core,和针对不同设备开发的类驱动Class(主机端也是一样的),先看核心驱动中的源函数Core/src:
- usbd_core.c,设备的核心,包括设备的初始化USBD_Init()函数,设备的三种状态,数据传输的三个阶段,以及其它一些与通信和连接相关的控制函数,通常不需要修改;
- usbd_ioreq.c,用于处理USB的Transactions结果,不需要修改;
- usbd_req.c,包含设备端答复主机端不同请求的返回实现,如返回Descriptor、Config等,不需要修改;
头文件inc中主要看几个特殊的,其它的头文件与源文件相对应:
- usbd_conf_template.h,设备库的配置文件,定义了不同设备在控制传输中使用的端点,该文件也是一个模板,通常需要复制到应用文件夹下并更名为usbd_conf.h,通常也不需要修改;
- usbd_usr.h,包含用户回调函数的头文件,这些回调函数在源文件usbd_usr.c中定义(官方通常不会编写,需要自己编写一些应用代码),下面会提到;
再看设备类驱动Class,标准库中给出了五种不同的设备类型(实际上是老版的),目前还加入customHID和hid_cdc_wrapper等复合类型,本文主要使用的是customHID类,仅包含一个源文件和一个头文件:
- usbd_customhid_core.c,包含customHID设备类的回调函数集USBD_CUSTOMHID_cb,是一个结构体,用在设备驱动初始化时传入(每个设备类都会有一个这样的回调函数集,用于区分不同的设备类),在特定阶段时会调用这些函数,通常在有需要的地方进行修改,如数据传入(DataOut)和接收(DataIn)。
/* usbd_customhid_core.c */
USBD_Class_cb_TypeDef USBD_CUSTOMHID_cb =
{
USBD_CUSTOM_HID_Init,
USBD_CUSTOM_HID_DeInit,
USBD_CUSTOM_HID_Setup,
NULL, /*EP0_TxSent*/
USBD_CUSTOM_HID_EP0_RxReady, /*EP0_RxReady*/ /* STATUS STAGE IN */
USBD_CUSTOM_HID_DataIn, //设备发送数据到主机
USBD_CUSTOM_HID_DataOut, //设备接收到来自主机的数据
NULL, /*SOF */
NULL,
NULL,
USBD_CUSTOM_HID_GetCfgDesc,
#ifdef USB_OTG_HS_CORE
USBD_CUSTOM_HID_GetCfgDesc, /* use same config as per FS */
#endif
};
- usbd_customhid_core.h,与上面的源文件相对应。
此外,有一些文件不在官方给的驱动里,而在官方给的例程中,可以直接拷贝相应设备类例程中的文件到自己的应用层文件夹下,有需要再进行修改:
- usbd_usr.c,包含USB事件回调函数,USB设备进入不同的事件后,会调用这些函数,如设备初始化、连接、挂起、恢复等,根据需要进行修改;
/* usbd_usr.c */
USBD_Usr_cb_TypeDef USR_cb = {
USBD_USR_Init, //初始化
USBD_USR_DeviceReset, //复位
USBD_USR_DeviceConfigured, //设备配置完毕
USBD_USR_DeviceSuspended, //挂起
USBD_USR_DeviceResumed, //恢复
USBD_USR_DeviceConnected, //设备连接上
USBD_USR_DeviceDisconnected, //设备断开后
};
- usbd_desc.c,设备描述符相关,包括设备的各种描述符以及获取这些描述符的函数;
3.4 回调函数的修改
需要修改回调函数的地方通常就两处:分别是usbd_customhid_core.c
和usbd_usr.c,在上面已经有提到。
3.5 中断函数
USB的主中断入口通常放在stm32xxx_it.c源文件下,由于在这里使用的是USB全速传输(USE_USB_OTG_FS),中断入口函数便被定义为OTG_FS_IRQHandler():
/* stm32xxx_it.c */
#ifdef USE_USB_OTG_FS
void OTG_FS_IRQHandler(void)
#else
void OTG_HS_IRQHandler(void)
#endif
{
OS_CPU_SR cpu_sr;
OS_ENTER_CRITICAL(); /* Tell uC/OS-II that we are starting an ISR */
OSIntEnter();
OS_EXIT_CRITICAL();
if(USB_HOST_DEVICE >= CONNECT_HOST) //自己定义的变量,用于区分当前是主机还是设备模式
USBH_OTG_ISR_Handler(&USB_OTG_Core_dev); //主机端USB中断
else if(USB_HOST_DEVICE == CONNECT_DEVICE)
USBD_OTG_ISR_Handler(&USB_OTG_Core_dev); //设备端USB中断
OSIntExit(); /* Tell uC/OS-II that we are leaving the ISR */
}
为了使用USBH_OTG_ISR_Handler()和USBD_OTG_ISR_Handler()两个中断入口,需要包含usb_hcd_int.h和usb_dcd_int.h两个头文件,具体的,我们进入到USBD_OTG_ISR_Handler()函数中:该函数也是设备中断的总入口,在该函数里,通过对不同寄存器值(不是直接读取,而是通过对象间接获取)进行判断,进入到相应的中断中:
/* usb_dcd_int.c */
uint32_t USBD_OTG_ISR_Handler (USB_OTG_CORE_HANDLE *pdev)
{
USB_OTG_GINTSTS_TypeDef gintr_status;
uint32_t retval = 0;
if (USB_OTG_IsDeviceMode(pdev)) /* ensure that we are in device mode */
{
gintr_status.d32 = USB_OTG_ReadCoreItr(pdev);
if (!gintr_status.d32) /* avoid spurious interrupt */
{
return 0;
}
if (gintr_status.b.outepintr)
{
retval |= DCD_HandleOutEP_ISR(pdev); //数据接收中断,即接收FIFO非空
}
if (gintr_status.b.inepint)
{
retval |= DCD_HandleInEP_ISR(pdev); //数据发送中断,即发送FIFO为空
}
if (gintr_status.b.modemismatch)
{
USB_OTG_GINTSTS_TypeDef gintsts;
/* Clear interrupt */
gintsts.d32 = 0;
gintsts.b.modemismatch = 1;
USB_OTG_WRITE_REG32(&pdev->regs.GREGS->GINTSTS, gintsts.d32);
}
if (gintr_status.b.wkupintr)
{
retval |= DCD_HandleResume_ISR(pdev);
}
if (gintr_status.b.usbsuspend)
{
retval |= DCD_HandleUSBSuspend_ISR(pdev);
}
if (gintr_status.b.sofintr)
{
retval |= DCD_HandleSof_ISR(pdev);
}
if (gintr_status.b.rxstsqlvl)
{
retval |= DCD_HandleRxStatusQueueLevel_ISR(pdev);
}
if (gintr_status.b.usbreset)
{
retval |= DCD_HandleUsbReset_ISR(pdev);
}
if (gintr_status.b.enumdone)
{
retval |= DCD_HandleEnumDone_ISR(pdev);
}
if (gintr_status.b.incomplisoin)
{
retval |= DCD_IsoINIncomplete_ISR(pdev);
}
if (gintr_status.b.incomplisoout)
{
retval |= DCD_IsoOUTIncomplete_ISR(pdev);
}
#ifdef VBUS_SENSING_ENABLED
if (gintr_status.b.sessreqintr)
{
retval |= DCD_SessionRequest_ISR(pdev);
}
if (gintr_status.b.otgintr)
{
retval |= DCD_OTG_ISR(pdev);
}
#endif
}
return retval;
}
这里面也没有什么需要改的,不同的中断函数内会调用不同的回调函数,如果应用层需要使用某个中断,直接在上一节的回调函数中进行编写即可。
3.6 描述符
描述符的修改是个细节,105系列USB驱动的描述符文件位置有两处:
- usbd_desc.c,包含设备描述符和各种字符串描述符(语言、厂家、接口等);
- usbd_customhid_core.c(其它设备类型找相应的core就行了),包含Report报告和配置描述符,我在迁移最后阶段弄了很久,最后发现是Report不一致;
4.总结
在发现问题所在时便就有个疑惑:既然驱动都用错了,应该不可能能连接上,而不是先连接上,但会偶发错误!后面在调试的过程中,我反复地对比USB驱动和OTG驱动在运行过程中的数据,发现二者所用的数据结构基本类似,相关寄存器也能对应的上,可以说这两套驱动本是同源(哭笑),但内核不一样终究会出问题。
本文虽然写了不少,但内容只是个大概,没有针对每个文件进行细说,这里边还是得自己边调试边照着文档一步步来。