1
站长荐语:虽然本文谈的主题是添加USB Host Class驱动,但文中所用到的方法具有普遍意义,所有MCU工程师都可以使用这种方法,参照已有功能做其它功能的扩展。
前 言
由于NXP有专业的MCU USB软件开发团队,NXP SDK USB协议栈支持了众多的常用class,功能异常强大,为用户带来了很多的便利,加速了客户的产品研发。
但是由于USB的应用场景实在是过于广泛,有的class比如CCID host,是SDK USB 协议栈目前暂时没有支持的。遇到这种情况,我们就需要自己动手来开发新的USB host class。本文以USB host CCID class 为例,探讨如何在i.MX RT1020上实现此host class。本文将从以下3个方面阐述如何基于SDK USB协议栈进行新的USB host class的开发:实现一个新的USB host class需要解决的问题。
SDK USB host协议栈的基础知识。
实现新的USB host class的一些要点展示。
需要解决的问题
实现一个新的USB host class,我们需要解决以下问题:
如何进行枚举,拿到设备的各种描述符?
如何建立pipe(Bulk, Inerrupt, … …)通信?
如何进行USB host class的上层开发?
小编整理了一个USB 应用的模型结构如下图所示。
从这个结构图中,我们可以看到,基于USB协议栈,我们要重点实现pipe通信,在pipe通信之上,是具体的USB class的实现。而最终的应用是基于class实现的基础之上的。
这个图同时也说明,class的实现以及应用,可以通过pipe与USB stack进行隔离,相互保持独立。软件模块之间保持独立/低耦合,可以使软件系统更加易于调试、维护和更新。
而USB控制器,在强大的SDK USB协议栈的加持下,对我们来说完全可以不关心。小编整个开发过程中,完全不需要去了解USB控制器的任何知识就可以完成新的host class的开发。
协议栈的基础知识
一、从task的角度来看USB host协议栈
下图以USB HOST CDC为例,展示了USB host协议栈的task结构。了解task结构对于了解USB host协议栈是如何工作的非常有帮助。
几个关键点(图中红色字体所示)就在于:
二、从函数调用栈的角度看USB host协议栈
对于复杂的软件系统,分析调用栈是个不二捷径,屡试不爽。
软件工程学里面有一个概念,叫隔离。隔离是一个非常重要的概念,软件工程学者认为隔离可以给软件系统带来很多的好处,两个隔离的模块,一个模块做了内部改动的同时,不会影响到另一个模块。对于复杂系统来说,这点尤其重要。
而具体到C语言,这种隔离的具体体现就是函数指针。
比如,我们要打开一个门,函数可以写成:
void open_door(void)
{
// action of open door
}
然后调用的时候我们只需要:
open_door();
就可以了。
引入隔离和指针后,我们就看不见函数的实现了,甚至看不到被调用的函数名,而是只需要知道通过指针进行访问。
对于引用方来说代码就变成:void (*p_open_door)(void);这个模块在初始化的时候,初始化这个指针,调用的时候只需要:(*p_open_door)();这样,我们可以在开始把void open_my_door(void)传过去,后面又把void open_your_door(void)传过去。而使用了函数指针的模块不会有任何影响,这个模块本身不会因为外部函数的改动而改动,甚至摆脱了linker的控制,因为这个模块本身甚至不需要重新做link来指向更改后的函数地址。函数指针除了带来了隔离的好处,另一个好处是灵活性,就像上面的例子,我们甚至可以在运行中动态的来改变函数指针,而被隔离模块在不知不觉中就实现了多种不同的open door,而自己执行的代码在binary层面并没有任何改变,只是通过同一个函数指针调用该指针指向的函数。这个方法在软件工程学界是得到了公认的一种做法,得到了极高的赞许和评价,对实际的软件应用产生了十分深远的影响。在小编见过的不少协议栈软件中,这个理念用得特别的广泛。给人的感觉就是函数指针的应用俨然已经成为了专业软件的一个标配,没有函数指针的代码必定不是好代码,没有函数指针的代码,必定是不专业的代码,不懂函数指针的工程师,必定是很low的工程师。这个方法好是好,但是对于使用者来(非协议栈的开发者)说,会有一个比较麻烦的地方,就是代码读着读着,一看到指针就不知道飞到哪儿去了。静态代码阅读,根本无法了解代码前世今生,来龙去脉。甚至极端的情况,一段函数指针满天飞的代码只能让人晕头转向,感到天昏地暗,垂头丧气,昏昏欲睡,挫折不已。但是小编几乎从来没有看到过有专业的书籍、大咖或者文章指出这个问题。不知道是不是只是小编自己会觉得这个会是一个问题,难道是小编自己太菜了?呜呜呜… …依稀记得以前上哲学课的时候学到的一些观点,比如矛盾论竟然可以完美的在这里得到解释,好与坏,白与黑,精华与糟粕就这样完美的统一在一起了。当然,我们的SDK USB协议栈是由专业的软件团队开发的,自然也不可避免的使用了这一理念,在带来各种强大而精彩的功能的同时,也不可避免的引入了其弊端。所以我们是无法用静态代码阅读的方式去快速了解这套软件的。
小编的解决方法是观察调用栈,几个核心的调用栈被列出来后,整个软件的运行体系就自然而然水落石出,山高月小。
调用栈分析的方法除了可以用在有函数指针的场景下,对于没有函数指针的复杂软件分析的场景也同样适用,可以用海量的代码中迅速看到函数之间的多层级联调用关系,这是快速分析复杂软件的很高效的方法。
这里小编列出了6个核心调用栈给大家参考,根据小编的实际使用体验,这6个核心调用栈已经足以帮助小编解决新的USB host classk开发中的所有问题了。如果读者有别的问题,也可以采用类似的方法来了解整个软件体系的结构,这比直接阅读代码要高效太多太多。
核心调用栈1:在何处发起枚举的控制传输?
核心调用栈2:在何处解析配置描述符?
核心调用栈3:Host event是如何回调回来的?
核心调用栈4:什么时候打开系统的控制interface/pipe?
核心调用栈5:什么时候打开class的控制interface/pipe?
核心调用栈6:什么时候打开class的数据interface/pipe?
实现的一些要点展示
在本章节中,将探讨如何基于现有的USB host CDC class来实现USB host CCID class。本章节会展示一些关键点,也基本上是step by step的guide。
为了突出重点,有些不是很重要的细枝末节的地方并没有讲述,读者如果有兴趣可以参考本文对应的代码工程获取更多更详细的信息。一、获取设备描述符内容
获取设备描述符的相关函数在USB_HostProcessCallback() in usb_host_devices.c
这里我们只需要加入内存打印语句,就可以把从device获取到的描述符打印出来。
由于我们重用了USB host CDC的架构,这部分不需要做任何改动就可以直接进行枚举。
相关的核心代码如下:
case kStatus_DEV_GetDes8: /* process get 8 bytes descriptor result */
… …
usb_echo("kStatus_DEV_GetDes8
");
mem_dump_8(deviceInstance->deviceDescriptor, dataLength);
case kStatus_DEV_GetDes: /* process get full device descriptor result */
… …
usb_echo("kStatus_DEV_GetDes
");
mem_dump_8(deviceInstance->deviceDescriptor, dataLength);
break;
case kStatus_DEV_GetCfg9: /* process get 9 bytes configuration result */
… …
usb_echo("kStatus_DEV_GetCfg9
");
mem_dump_8(configureDesc, dataLength);
case kStatus_DEV_GetCfg: /* process get configuration result */
… …
usb_echo("kStatus_DEV_GetCfg
");
mem_dump_8(deviceInstance->configurationDesc, dataLength);
运行后输出结果:
Console output:
kStatus_DEV_GetDes8
0x20003fa8: 12 01 00 02 00 00 00 40
kStatus_DEV_GetCfg9
0x20003fba: 09 02 5d 00 01 01 00 c0
0x000000c7: 32 -- -- -- -- -- -- --
kStatus_DEV_GetCfg
0x20003fd0: 09 02 5d 00 01 01 00 c0
0x20003fd8: 32 09 04 00 00 03 0b 00
0x20003fe0: 00 03 36 21 10 01 01 02
0x20003fe8: 01 00 00 00 fc 0d 00 00
0x20003ff0: fc 0d 00 00 00 80 25 00
0x20003ff8: 00 80 25 00 00 00 00 00
0x20004000: 00 00 00 00 00 00 00 00
0x20004008: 00 00 38 00 02 00 0f 01
0x20004010: 00 00 00 00 00 00 00 01
0x20004018: 07 05 81 02 40 00 00 07
0x20004020: 05 02 02 40 00 00 07 05
0x000000f7: 83 03 08 00 08 -- -- --
device not supported.
从这里我们可以看到从设备获取的设备描述符和配置描述符,但是进一步显示设备不支持。
二、为什么设备不支持?
要得到答案,这个问题还是要研究一下的,这里小编就不绕弯子,直接公布答案了:
在USB_HostCdcEvent(), host_cdc.c,这里面会解析配置描述符的信息,看是不是CDC的class,因为我们接入的是CCID设备,而原始代码是按照CDC class去解析,自然就会失败。
解决的方式就是在这个函数里面做CCID class的解析就好了。
usb_status_t USB_HostCdcEvent(usb_device_handle deviceHandle,
usb_host_configuration_handle configurationHandle,
uint32_t event_code)
{
… …
switch (event_code)
{
case kUSB_HostEventAttach:
… …
for (interface_index = 0; interface_index < configuration->interfaceCount; ++interface_index)
{
hostInterface = &configuration->interfaceList[interface_index];
id = hostInterface->interfaceDesc->bInterfaceClass;
if (id == USB_HOST_CCID_CLASS_CODE)
{
usb_echo("***ccid device detected.
");
cdcDataInterfaceHandle = hostInterface;
cdcDeviceHandle = deviceHandle;
break;
}
}
if ((NULL != cdcDataInterfaceHandle) && (NULL != cdcDeviceHandle))
{
status = kStatus_USB_Success;
}
else
{
status = kStatus_USB_NotSupported;
}
break;
当我们在这里正确的识别到CCID class设备,返回kStatus_USB_Success,就不会出现设备不支持了。
此时的log输出为:
Console output:
kStatus_DEV_GetDes8
0x20003fa8: 12 01 00 02 00 00 00 40
kStatus_DEV_GetCfg9
0x20003fba: 09 02 5d 00 01 01 00 c0
0x000000b6: 32 -- -- -- -- -- -- --
kStatus_DEV_GetCfg
0x20003fd0: 09 02 5d 00 01 01 00 c0
0x20003fd8: 32 09 04 00 00 03 0b 00
0x20003fe0: 00 03 36 21 10 01 01 02
0x20003fe8: 01 00 00 00 fc 0d 00 00
0x20003ff0: fc 0d 00 00 00 80 25 00
0x20003ff8: 00 80 25 00 00 00 00 00
0x20004000: 00 00 00 00 00 00 00 00
0x20004008: 00 00 38 00 02 00 0f 01
0x20004010: 00 00 00 00 00 00 00 01
0x20004018: 07 05 81 02 40 00 00 07
0x20004020: 05 02 02 40 00 00 07 05
0x000000e6: 83 03 08 00 08 -- -- --
***ccid device detected.
可以看到,我们目前拿到了CCID的配置描述符,并且根据spec正确的识别到了CCID设备,这样枚举就过了。
是不是感觉很轻松?
三、CCID配置描述符解析
这里仅列出CCID配置描述符的结构。
重点是我们要知道,CCID class有一个interface,里面有3个EP,一个Bulk In,一个Bulk Out,一个Interrupt In,我们会根据这个信息在下一步调整class状态机。
四、class状态机分析
Class状态机在USB_HostCdcTask()中实现。
先看看CDC的状态机:
与CDC相比,CCID只有一个interface,并且设备相关上层操作小编想独立出来在另外的地方做,于是CCID的状态机如下,灰色部分为跳过的部分。
五、打开interface和pipe
打开interface和pipe和操作在USB_HostCdcOpenDataInterface(), 位于文件usb_host_cdc.c中。
这里需要适配CCID的操作,去openBulk In, Bulk Out, Interrupt In pipe。注意这3个endpoint在同一个interface下面。
for (ep_index = 0; ep_index < interfaceHandle->epCount; ++ep_index)
{
usb_echo("ep_index = %x
", ep_index);
ep_desc = interfaceHandle->epList[ep_index].epDesc;
if (((ep_desc->bEndpointAddress & USB_DESCRIPTOR_ENDPOINT_ADDRESS_DIRECTION_MASK) ==
USB_DESCRIPTOR_ENDPOINT_ADDRESS_DIRECTION_IN) &&
((ep_desc->bmAttributes & USB_DESCRIPTOR_ENDPOINT_ATTRIBUTE_TYPE_MASK) == USB_ENDPOINT_BULK))
{
… …
status = USB_HostOpenPipe(cdcInstance->hostHandle, &cdcInstance->inPipe, &pipeInit);
… …
}
else if (((ep_desc->bEndpointAddress & USB_DESCRIPTOR_ENDPOINT_ADDRESS_DIRECTION_MASK) ==
USB_DESCRIPTOR_ENDPOINT_ADDRESS_DIRECTION_OUT) &&
((ep_desc->bmAttributes & USB_DESCRIPTOR_ENDPOINT_ATTRIBUTE_TYPE_MASK) == USB_ENDPOINT_BULK))
{
… …
status = USB_HostOpenPipe(cdcInstance->hostHandle, &cdcInstance->outPipe, &pipeInit);
… …
}
else if (((ep_desc->bEndpointAddress & USB_DESCRIPTOR_ENDPOINT_ADDRESS_DIRECTION_MASK) ==
USB_DESCRIPTOR_ENDPOINT_ADDRESS_DIRECTION_IN) &&
((ep_desc->bmAttributes & USB_DESCRIPTOR_ENDPOINT_ATTRIBUTE_TYPE_MASK) == USB_ENDPOINT_INTERRUPT))
{
… …
status = USB_HostOpenPipe(cdcInstance->hostHandle, &cdcInstance->interruptPipe, &pipeInit);
……
}
需要注意的是,USB协议栈会自动解析interface和endpoint,这里的数据结构是前面已经解析过的。我们需要在这里去识别Bulk In, Bulk Out, 以及Interrupt In。
相关的log:
Console log:***ccid device detected.
device cdc attached:
pid=0x9cvid=0x1fc9 address=1
cdc device attached
s - kUSB_HostCdcRunSetControlInterfaceDone
--> USB_HostCdcOpenDataInterface
ep_index = 0bulk in ep_index = 1bulk out ep_index = 2interrupt in
s - kUSB_HostCdcRunSetDataInterfaceDone
从log我们可以看到,我们已经成功的检测到interface下面的3个EP了,Bulk In, Bulk Out,Interrupt In。
六、测试pipe的通信
既然pipe已经打开,下面我们就要测试一下pipe的通信了。
这里我们沿用了USB stack的task的做法,在一个无限loop里面去做处理,所以需要变量记录状态。
首先记录状态,代码如下(位于函数USB_HostCdcTask()中):
case kUSB_HostCdcRunSetControlInterfaceDone:
... ...
if (USB_HostCdcSetDataInterface(cdcInstance->classHandle, cdcInstance->dataInterfaceHandle, 0,
USB_HostCdcControlCallback, &g_cdc) != kStatus_USB_Success)
{
usb_echo("set data interface error
");
}
… …
ccid_communication_ready();
break;
然后我们就可以基于USB stack的API进行pipe通信了,相关代码如下(位于函数ccid_app_task()中):
if(flag_test == 0)
{
usb_echo("ccid_ready_for_communicatio
"); USB_HostCdcDataSend(g_cdc.classHandle, "12345", 5, USB_CCID_BULK_OUT_Callback, &g_cdc);
}
else if(flag_test == 2)
{ USB_HostCdcInterruptRecv(g_cdc.classHandle, buf, 8,USB_CCID_HID_Callback, &g_cdc);
}
else if(flag_test == 4)
{ USB_HostCdcDataRecv(g_cdc.classHandle, buf, 8,USB_CCID_BULK_IN_Callback, &g_cdc);
}
注意这里的收发API都是基于回调机制,收发完成后和app通过回调函数进行同步(通信)。
回调机制是一个非常优秀的机制(这同时也是小编前面吐槽的函数指针,又爱又恨),这样避免了低效率的状态轮询。
完成相关的代码后,接下来测试pipe,看看log输出:
Console log:ep_index = 0
bulk in
ep_index = 1
bulk out
ep_index = 2
interrupt in
ccid_ready_for_communicatio
s - kUSB_HostCdcRunSetDataInterfaceDone
USB_CCID_BULK_OUT_Callback
USB_CCID_HID_Callback
USB_CCID_BULK_IN_Callback
这里我们可以看到回调机制已经正确触发了。
这里可以看到,我们已经正确的触发了Bulk In,Bulk Out以及Interrupt transfer。
七、关于新的class的开发和上层应用开发
在pipe的通信已经正确的建立后,class的开发和上层应用的开发,并没有统一的模式。每个工程师很可能都有自己的想法去实现,这部分的实现,自由度可以很大。
对于CCID我们要做的主要工作是集成spec定义的消息,以及spec定义的相关的通信状态机。这部分本文并不做重点讨论,每个class都有自己的特点和定义,需要参考spec和应用场景去具体实现。
小编这里推荐尽量把具体的class处理的这部分相对于USB stack独立出来,这样系统的整体设计脉络更加清晰一些,让我们更能聚焦在新的USBhost class的开发,也便于软件的长期开发和维护。
八、本文的相关代码
本文的相关代码可以从以下链接进行获取,该代码下载后可以直接编译并且运行在i.MXRT1020 EVK上。
https://github.com/jiaguonxpcom/usb_host_ccid
小 结
本文基于i.MX RT1020平台,向读者展示了如何基于NXP SDK USB host来实现一个新的class,重点讲述了相关pipe的建议。建立pipe通信是实现新的USB host的核心步骤。
希望本文能给需要做相关类似开发的读者一些参考,避免少走弯路,而愉快的基于SDK USB协议栈完成相关的新任务的开发。
责任编辑:haq
全部0条评论
快来发表一下你的评论吧 !