站長薦語:雖然本文談的主題是添加USB Host Class驅(qū)動,但文中所用到的方法具有普遍意義,所有MCU工程師都可以使用這種方法,參照已有功能做其它功能的擴(kuò)展。
前 言
由于NXP有專業(yè)的MCU USB軟件開發(fā)團(tuán)隊(duì),NXP SDK USB協(xié)議棧支持了眾多的常用class,功能異常強(qiáng)大,為用戶帶來了很多的便利,加速了客戶的產(chǎn)品研發(fā)。
但是由于USB的應(yīng)用場景實(shí)在是過于廣泛,有的class比如CCID host,是SDK USB 協(xié)議棧目前暫時沒有支持的。遇到這種情況,我們就需要自己動手來開發(fā)新的USB host class。本文以USB host CCID class 為例,探討如何在i.MX RT1020上實(shí)現(xiàn)此host class。本文將從以下3個方面闡述如何基于SDK USB協(xié)議棧進(jìn)行新的USB host class的開發(fā):-
實(shí)現(xiàn)一個新的USB host class需要解決的問題。
-
SDK USB host協(xié)議棧的基礎(chǔ)知識。
-
實(shí)現(xiàn)新的USB host class的一些要點(diǎn)展示。
需要解決的問題
實(shí)現(xiàn)一個新的USB host class,我們需要解決以下問題:
小編整理了一個USB 應(yīng)用的模型結(jié)構(gòu)如下圖所示。
從這個結(jié)構(gòu)圖中,我們可以看到,基于USB協(xié)議棧,我們要重點(diǎn)實(shí)現(xiàn)pipe通信,在pipe通信之上,是具體的USB class的實(shí)現(xiàn)。而最終的應(yīng)用是基于class實(shí)現(xiàn)的基礎(chǔ)之上的。
這個圖同時也說明,class的實(shí)現(xiàn)以及應(yīng)用,可以通過pipe與USB stack進(jìn)行隔離,相互保持獨(dú)立。軟件模塊之間保持獨(dú)立/低耦合,可以使軟件系統(tǒng)更加易于調(diào)試、維護(hù)和更新。
而USB控制器,在強(qiáng)大的SDK USB協(xié)議棧的加持下,對我們來說完全可以不關(guān)心。小編整個開發(fā)過程中,完全不需要去了解USB控制器的任何知識就可以完成新的host class的開發(fā)。
協(xié)議棧的基礎(chǔ)知識
一、從task的角度來看USB host協(xié)議棧
下圖以USB HOST CDC為例,展示了USB host協(xié)議棧的task結(jié)構(gòu)。了解task結(jié)構(gòu)對于了解USB host協(xié)議棧是如何工作的非常有幫助。
幾個關(guān)鍵點(diǎn)(圖中紅色字體所示)就在于:
- 應(yīng)用層事件的回調(diào)機(jī)制
- 設(shè)備狀態(tài)管理,主要是設(shè)備的接入、拔除、枚舉等。
- Class狀態(tài)管理,這里是設(shè)備運(yùn)行的狀態(tài)機(jī)管理,包含了關(guān)鍵的class的interface和pipe的建立,class的初始化等等。
二、從函數(shù)調(diào)用棧的角度看USB host協(xié)議棧
對于復(fù)雜的軟件系統(tǒng),分析調(diào)用棧是個不二捷徑,屢試不爽。
軟件工程學(xué)里面有一個概念,叫隔離。隔離是一個非常重要的概念,軟件工程學(xué)者認(rèn)為隔離可以給軟件系統(tǒng)帶來很多的好處,兩個隔離的模塊,一個模塊做了內(nèi)部改動的同時,不會影響到另一個模塊。對于復(fù)雜系統(tǒng)來說,這點(diǎn)尤其重要。
而具體到C語言,這種隔離的具體體現(xiàn)就是函數(shù)指針。
比如,我們要打開一個門,函數(shù)可以寫成:
void open_door(void)
{
// action of open door
}
然后調(diào)用的時候我們只需要:
open_door();
就可以了。
引入隔離和指針后,我們就看不見函數(shù)的實(shí)現(xiàn)了,甚至看不到被調(diào)用的函數(shù)名,而是只需要知道通過指針進(jìn)行訪問。
對于引用方來說代碼就變成:void (*p_open_door)(void);這個模塊在初始化的時候,初始化這個指針,調(diào)用的時候只需要:(*p_open_door)();這樣,我們可以在開始把void open_my_door(void)傳過去,后面又把void open_your_door(void)傳過去。而使用了函數(shù)指針的模塊不會有任何影響,這個模塊本身不會因?yàn)橥獠亢瘮?shù)的改動而改動,甚至擺脫了linker的控制,因?yàn)檫@個模塊本身甚至不需要重新做link來指向更改后的函數(shù)地址。函數(shù)指針除了帶來了隔離的好處,另一個好處是靈活性,就像上面的例子,我們甚至可以在運(yùn)行中動態(tài)的來改變函數(shù)指針,而被隔離模塊在不知不覺中就實(shí)現(xiàn)了多種不同的open door,而自己執(zhí)行的代碼在binary層面并沒有任何改變,只是通過同一個函數(shù)指針調(diào)用該指針指向的函數(shù)。這個方法在軟件工程學(xué)界是得到了公認(rèn)的一種做法,得到了極高的贊許和評價,對實(shí)際的軟件應(yīng)用產(chǎn)生了十分深遠(yuǎn)的影響。在小編見過的不少協(xié)議棧軟件中,這個理念用得特別的廣泛。給人的感覺就是函數(shù)指針的應(yīng)用儼然已經(jīng)成為了專業(yè)軟件的一個標(biāo)配,沒有函數(shù)指針的代碼必定不是好代碼,沒有函數(shù)指針的代碼,必定是不專業(yè)的代碼,不懂函數(shù)指針的工程師,必定是很low的工程師。這個方法好是好,但是對于使用者來(非協(xié)議棧的開發(fā)者)說,會有一個比較麻煩的地方,就是代碼讀著讀著,一看到指針就不知道飛到哪兒去了。靜態(tài)代碼閱讀,根本無法了解代碼前世今生,來龍去脈。甚至極端的情況,一段函數(shù)指針滿天飛的代碼只能讓人暈頭轉(zhuǎn)向,感到天昏地暗,垂頭喪氣,昏昏欲睡,挫折不已。但是小編幾乎從來沒有看到過有專業(yè)的書籍、大咖或者文章指出這個問題。不知道是不是只是小編自己會覺得這個會是一個問題,難道是小編自己太菜了?嗚嗚嗚… …依稀記得以前上哲學(xué)課的時候?qū)W到的一些觀點(diǎn),比如矛盾論竟然可以完美的在這里得到解釋,好與壞,白與黑,精華與糟粕就這樣完美的統(tǒng)一在一起了。當(dāng)然,我們的SDK USB協(xié)議棧是由專業(yè)的軟件團(tuán)隊(duì)開發(fā)的,自然也不可避免的使用了這一理念,在帶來各種強(qiáng)大而精彩的功能的同時,也不可避免的引入了其弊端。所以我們是無法用靜態(tài)代碼閱讀的方式去快速了解這套軟件的。
小編的解決方法是觀察調(diào)用棧,幾個核心的調(diào)用棧被列出來后,整個軟件的運(yùn)行體系就自然而然水落石出,山高月小。
調(diào)用棧分析的方法除了可以用在有函數(shù)指針的場景下,對于沒有函數(shù)指針的復(fù)雜軟件分析的場景也同樣適用,可以用海量的代碼中迅速看到函數(shù)之間的多層級聯(lián)調(diào)用關(guān)系,這是快速分析復(fù)雜軟件的很高效的方法。
這里小編列出了6個核心調(diào)用棧給大家參考,根據(jù)小編的實(shí)際使用體驗(yàn),這6個核心調(diào)用棧已經(jīng)足以幫助小編解決新的USB host classk開發(fā)中的所有問題了。如果讀者有別的問題,也可以采用類似的方法來了解整個軟件體系的結(jié)構(gòu),這比直接閱讀代碼要高效太多太多。
核心調(diào)用棧1:在何處發(fā)起枚舉的控制傳輸?
核心調(diào)用棧2:在何處解析配置描述符?
核心調(diào)用棧3:Host event是如何回調(diào)回來的?
核心調(diào)用棧4:什么時候打開系統(tǒng)的控制interface/pipe?
核心調(diào)用棧5:什么時候打開class的控制interface/pipe?
核心調(diào)用棧6:什么時候打開class的數(shù)據(jù)interface/pipe?
實(shí)現(xiàn)的一些要點(diǎn)展示
在本章節(jié)中,將探討如何基于現(xiàn)有的USB host CDC class來實(shí)現(xiàn)USB host CCID class。本章節(jié)會展示一些關(guān)鍵點(diǎn),也基本上是step by step的guide。
為了突出重點(diǎn),有些不是很重要的細(xì)枝末節(jié)的地方并沒有講述,讀者如果有興趣可以參考本文對應(yīng)的代碼工程獲取更多更詳細(xì)的信息。一、獲取設(shè)備描述符內(nèi)容
獲取設(shè)備描述符的相關(guān)函數(shù)在USB_HostProcessCallback() in usb_host_devices.c
這里我們只需要加入內(nèi)存打印語句,就可以把從device獲取到的描述符打印出來。
由于我們重用了USB host CDC的架構(gòu),這部分不需要做任何改動就可以直接進(jìn)行枚舉。
相關(guān)的核心代碼如下:
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);
運(yùn)行后輸出結(jié)果:
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.
從這里我們可以看到從設(shè)備獲取的設(shè)備描述符和配置描述符,但是進(jìn)一步顯示設(shè)備不支持。
二、為什么設(shè)備不支持?
要得到答案,這個問題還是要研究一下的,這里小編就不繞彎子,直接公布答案了:
在USB_HostCdcEvent(), host_cdc.c,這里面會解析配置描述符的信息,看是不是CDC的class,因?yàn)槲覀兘尤氲氖荂CID設(shè)備,而原始代碼是按照CDC class去解析,自然就會失敗。
解決的方式就是在這個函數(shù)里面做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;
當(dāng)我們在這里正確的識別到CCID class設(shè)備,返回kStatus_USB_Success,就不會出現(xiàn)設(shè)備不支持了。
此時的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的配置描述符,并且根據(jù)spec正確的識別到了CCID設(shè)備,這樣枚舉就過了。
是不是感覺很輕松?
三、CCID配置描述符解析
這里僅列出CCID配置描述符的結(jié)構(gòu)。
重點(diǎn)是我們要知道,CCID class有一個interface,里面有3個EP,一個Bulk In,一個Bulk Out,一個Interrupt In,我們會根據(jù)這個信息在下一步調(diào)整class狀態(tài)機(jī)。
四、class狀態(tài)機(jī)分析
Class狀態(tài)機(jī)在USB_HostCdcTask()中實(shí)現(xiàn)。
先看看CDC的狀態(tài)機(jī):
與CDC相比,CCID只有一個interface,并且設(shè)備相關(guān)上層操作小編想獨(dú)立出來在另外的地方做,于是CCID的狀態(tài)機(jī)如下,灰色部分為跳過的部分。
五、打開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協(xié)議棧會自動解析interface和endpoint,這里的數(shù)據(jù)結(jié)構(gòu)是前面已經(jīng)解析過的。我們需要在這里去識別Bulk In, Bulk Out, 以及Interrupt In。
相關(guān)的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我們可以看到,我們已經(jīng)成功的檢測到interface下面的3個EP了,Bulk In, Bulk Out,Interrupt In。
六、測試pipe的通信
既然pipe已經(jīng)打開,下面我們就要測試一下pipe的通信了。
這里我們沿用了USB stack的task的做法,在一個無限loop里面去做處理,所以需要變量記錄狀態(tài)。
首先記錄狀態(tài),代碼如下(位于函數(shù)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進(jìn)行pipe通信了,相關(guān)代碼如下(位于函數(shù)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);
}
注意這里的收發(fā)API都是基于回調(diào)機(jī)制,收發(fā)完成后和app通過回調(diào)函數(shù)進(jìn)行同步(通信)。
回調(diào)機(jī)制是一個非常優(yōu)秀的機(jī)制(這同時也是小編前面吐槽的函數(shù)指針,又愛又恨),這樣避免了低效率的狀態(tài)輪詢。
完成相關(guān)的代碼后,接下來測試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
這里我們可以看到回調(diào)機(jī)制已經(jīng)正確觸發(fā)了。
這里可以看到,我們已經(jīng)正確的觸發(fā)了Bulk In,Bulk Out以及Interrupt transfer。
七、關(guān)于新的class的開發(fā)和上層應(yīng)用開發(fā)
在pipe的通信已經(jīng)正確的建立后,class的開發(fā)和上層應(yīng)用的開發(fā),并沒有統(tǒng)一的模式。每個工程師很可能都有自己的想法去實(shí)現(xiàn),這部分的實(shí)現(xiàn),自由度可以很大。
對于CCID我們要做的主要工作是集成spec定義的消息,以及spec定義的相關(guān)的通信狀態(tài)機(jī)。這部分本文并不做重點(diǎn)討論,每個class都有自己的特點(diǎn)和定義,需要參考spec和應(yīng)用場景去具體實(shí)現(xiàn)。
小編這里推薦盡量把具體的class處理的這部分相對于USB stack獨(dú)立出來,這樣系統(tǒng)的整體設(shè)計(jì)脈絡(luò)更加清晰一些,讓我們更能聚焦在新的USBhost class的開發(fā),也便于軟件的長期開發(fā)和維護(hù)。
八、本文的相關(guān)代碼
本文的相關(guān)代碼可以從以下鏈接進(jìn)行獲取,該代碼下載后可以直接編譯并且運(yùn)行在i.MXRT1020 EVK上。
https://github.com/jiaguonxpcom/usb_host_ccid
小 結(jié)
本文基于i.MX RT1020平臺,向讀者展示了如何基于NXP SDK USB host來實(shí)現(xiàn)一個新的class,重點(diǎn)講述了相關(guān)pipe的建議。建立pipe通信是實(shí)現(xiàn)新的USB host的核心步驟。
希望本文能給需要做相關(guān)類似開發(fā)的讀者一些參考,避免少走彎路,而愉快的基于SDK USB協(xié)議棧完成相關(guān)的新任務(wù)的開發(fā)。
責(zé)任編輯:haq
-
NXP
+關(guān)注
關(guān)注
60文章
1278瀏覽量
184039 -
usb
+關(guān)注
關(guān)注
60文章
7936瀏覽量
264474 -
驅(qū)動
+關(guān)注
關(guān)注
12文章
1838瀏覽量
85262
原文標(biāo)題:新添USB host class驅(qū)動開發(fā)
文章出處:【微信號:NXP_SMART_HARDWARE,微信公眾號:恩智浦MCU加油站】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論