站長薦語:雖然本文談的主題是添加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,我們需要解決以下問題:
小編整理了一個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協議棧是如何工作的非常有幫助。
幾個關鍵點(圖中紅色字體所示)就在于:
- 應用層事件的回調機制
- 設備狀態管理,主要是設備的接入、拔除、枚舉等。
- Class狀態管理,這里是設備運行的狀態機管理,包含了關鍵的class的interface和pipe的建立,class的初始化等等。
二、從函數調用棧的角度看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
-
NXP
+關注
關注
60文章
1287瀏覽量
185009 -
usb
+關注
關注
60文章
7976瀏覽量
265517 -
驅動
+關注
關注
12文章
1848瀏覽量
85468
原文標題:新添USB host class驅動開發
文章出處:【微信號:NXP_SMART_HARDWARE,微信公眾號:恩智浦MCU加油站】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論