匯總1:STM32的USB常式修改步驟,來自http://blog.csdn.net/cy757/archive/2010/01/01/5117610.aspx
以下是筆者將ST的Custom_HID常式修改為“自訂USB裝置”常式時總結出來的,因為筆者也是剛剛學USB開發不久,某些方面理解錯誤在所難免,請各位大蝦指正。
一、usb_desc.c檔案
根據你程式使用的通訊方式修改。usb_desc.h檔案中定義要根據usb_desc.c檔案中的數組的大小;ConfigDescriptor[SIZ_CONFIG_DESC]下添加需要處理的端點;根據需要添加或刪除報告描述符(主要用於HID)和CDC介面描述符(主要用於實現USB轉串口)等。具體方法可以下載個“電腦圈圈”使用D12編寫的例子。
二、Usb_conf.h檔案:
1、修改需要處理那些中斷
CNTR_CTRM 處理資料正確傳輸後控制,比如說響應主機
CNTR_DOVRM /* DMA OVeR/underrun Mask */
CNTR_ERRM /* ERRor Mask */
CNTR_WKUPM 0 /* WaKe UP Mask */
CNTR_SUSPM /* SUSPend Mask */
CNTR_RESETM 主要處理USB複位後進行一些初始化任務
CNTR_SOFM /* Start Of Frame Mask */
CNTR_ESOFM /* Expected Start Of Frame Mask */
如:
usb_conf.h中的#define IMR_MSK (CNTR_CTRM | CNTR_SOFM | CNTR_RESETM )是決定USB_CNTR寄存器中的那個USB相關中斷啟動還是屏蔽。
2、根據需要增加端點緩衝地址,要根據緩衝區的地址修改,防止資料重疊
如下為根據每個緩衝區的大小為64位元組修改:
#define ENDP1_TXADDR (0xC0)
#define ENDP1_RXADDR (0xD0)
#define ENDP2_TXADDR (0x100)
#define ENDP2_RXADDR (0x140)
#define ENDP3_TXADDR (0x180)
#define ENDP3_RXADDR (0x1C0)
3、修改/* CTR service routines */下的EPX_IN_Callback和EPX_OUT_Callback。注釋掉需要處理的函數。NOP_Process表示不處理。
三usb_prop.c檔案
1、修改void XX_Reset(void)(如:void Joystick_Reset(void))
一般/* Initialize Endpoint 0 */的不用修改,如下為舉例說明端點1的初始化,其他連接埠原理一樣。
SetEPType(ENDP1, EP_INTERRUPT);//設定端點1類型
/*EP_BULK 批量端點
EP_CONTROL 控制端點
EP_ISOCHRNOUS 同步端點
EP_INTERRUPT 中斷端點*/
SetEPTxAddr(ENDP1, ENDP1_TXADDR); //設定端點1緩衝區基地址
SetEPTxCount(ENDP1, 64);// 配置Tx 緩衝計數器
SetEPRxStatus(ENDP1, EP_RX_DIS);// //設定端點接收關閉
SetEPTxStatus(ENDP1, EP_TX_NAK);// //設定端點1發送不應答
/*
#define EP_RX_DIS (0x0000) // EndPoint RX DISabled 端點接收關閉
#define EP_RX_STALL (0x1000) // EndPoint RX STALLed 端點接收延遲
#define EP_RX_NAK (0x2000) // EndPoint RX NAKed 端點接收不應答
#define EP_RX_VALID (0x3000) // EndPoint RX VALID端點接收有效
#define EP_TX_DIS (0x0000) //EndPoint TX DISabled
#define EP_TX_STALL (0x0010) // EndPoint TX STALLed
#define EP_TX_NAK (0x0020) // EndPoint TX NAKed
#define EP_TX_VALID (0x0030) // EndPoint TX VALID */
2、刪除不相干的描述符等。
如自訂的USB裝置就不需要以下結構體初始化:
ONE_DESCRIPTOR Joystick_Report_Descriptor
ONE_DESCRIPTOR Mouse_Hid_Descriptor
3、修改RESULT XX_Data_Setup(u8 RequestNo)的資料類請求處理。
如Custom_HID常式修改為“自訂USB裝置”常式時可以將以下代碼刪除
if ((RequestNo == GET_DESCRIPTOR)
&& (Type_Recipient == (STANDARD_REQUEST | INTERFACE_RECIPIENT))
&& (pInformation->USBwIndex0 == 0))
{
if (pInformation->USBwValue1 == REPORT_DESCRIPTOR)
{
CopyRoutine = Joystick_GetReportDescriptor;
}
else if (pInformation->USBwValue1 == HID_DESCRIPTOR_TYPE)
{
CopyRoutine = Joystick_GetHIDDescriptor;
}
}
4、刪除不相干的獲得描述符返回函數
如自訂的USB裝置就不需要以下函數:
Joystick_GetReportDescriptor
Joystick_GetHIDDescriptor
四、usb_endp.c檔案
1、增加之前定義的中斷資料處理函數
如:
void EP1_OUT_Callback(void)
{
這些寫接收代碼
}
五、資料發送和接收,舉例說明
1、資料接收
u8 DataLen;
DataLen = GetEPRxCount(ENDP1);
PMAToUserBufferCopy(TX1_buffer, ENDP1_RXADDR, DataLen);
SetEPRxValid(ENDP1);
USART1_Send(DataLen);
count_out = 1;
2、資料發送
UserToPMABufferCopy(InBuffer, GetEPTxAddr(ENDP1), 64);
SetEPTxCount(ENDP1, 64);
SetEPTxValid(ENDP1);
===========================================================================
匯總2:STM32 USB 程式將BULK EP改成雙緩衝機制後,一直狂飆到了1MB/S!來自:http://www.powermcu.com/bbs/viewthread.php?tid=693
前天測試自己編寫的USB驅動程式時候發現從主機到STM32的OUT傳輸(主機到裝置)速率竟然只有最高33KB/S,實在是暈死了。經過研究後發現是驅動程式中設定的PIPE MaxTransferSize參數的關係,原先設定64隻能33KB/S,後參考其他USB裝置驅動程式的值,設定成了65535,再測試USB OUT的速度,達到了500KB/S,終於解決了驅動程式的瓶頸。不過算下USB 2.0全速的通訊速率是12Mb/S,排除掉CRC、令牌、SOF等等開銷怎麼也應該不止最大500KB/S啊。到網上看了看,基本上應該能達到600KB/S~700KB/S以上,我現在的速度應該還有很大的提升才是。
看看程式,發現
void EP3_OUT_Callback(void)//EP3 OUT的回呼函數,當EP3接收到資料時候中斷調用該函數
{
count_out = GetEPRxCount(ENDP3);//獲得接收到的資料長度
PMAToUserBufferCopy(buffer_out, ENDP3_RXADDR, count_out);//將資料從USB EP3 RX的緩衝區拷貝到使用者指定的數組中
SetEPRxValid(ENDP3); //完成拷貝後置有效狀態,從而EP3發送ACK主機可以進行下一個資料包的發送
}
試著將PMAToUserBufferCopy這句注釋掉(這樣STM32就不處理接收到的資料了)後再測試速度,驚奇地發現速度竟然達到了997KB/S!晚上仔細想了想,資料肯定是要使用的,這個資料拷貝的過程的時間消費總是少不了的;由於通常情況下USB裝置BULK資料接收的步驟就是:接收到資料,置NAK->將緩衝區資料拷貝到使用者區(使用者處理過程)->發ACK通知主機完成了完整的接收可以發送下一個->主機發送下一個,按照以上的步驟USB接收一步步的進行,只要STM32不完成資料處理,狀態就一直是NAK,主機就會不停地發送該資料包,浪費了頻寬,因此就會導致我上面最大速度500KB/S難以再增加的情況!不甘心啊~~
昨天晚上又仔細研究了STM32的技術參考手冊的USB章節內容,裡面提到BULK可以採用雙緩衝機制(PING-PONG)進行處理,正好可以解決上面的情況。雙緩衝機制的原理就是分配2塊接收緩衝,STM32的使用者處理和USB介面可以分別交替佔用2個緩衝區,當USB端點接收資料寫其中一個緩衝區的時候,使用者的應用程式可以同時處理另一個緩衝區,這樣緩衝區依次交換佔有者,只要使用者處理常式在USB端點接收的時間片段內完成處理,就能夠完全不影響USB的通訊速度!
程式部分修改
一、EP3_OUT的設定修改,
//ZYP:修改EP3為BULK雙緩衝方式-------------------------
SetEPType(ENDP3, EP_BULK);
SetEPDoubleBuff(ENDP3);
SetEPDblBuffAddr(ENDP3, ENDP3_BUF0Addr, ENDP3_BUF1Addr);
SetEPDblBuffCount(ENDP3, EP_DBUF_OUT, VIRTUAL_COM_PORT_DATA_SIZE);
ClearDTOG_RX(ENDP3);
ClearDTOG_TX(ENDP3);
ToggleDTOG_TX(ENDP3);
SetEPRxStatus(ENDP3, EP_RX_VALID);
SetEPTxStatus(ENDP3, EP_TX_DIS);
//------------------------------------------------------
二、EP3_OUT回呼函數的修改
void EP3_OUT_Callback(void)
{
//ZYP:以下是修改成EP3雙緩衝OUT後的處理函數
if (GetENDPOINT(ENDP3) & EP_DTOG_TX)//先判斷本次接收到的資料是放在哪塊緩衝區的
{
FreeUserBuffer(ENDP3, EP_DBUF_OUT); //先釋放使用者對緩衝區的佔有,這樣的話USB的下一個接收過程可以立刻進行,同時使用者並行進行下面處理
count_out = GetEPDblBuf0Count(ENDP3);//讀取接收到的位元組數
PMAToUserBufferCopy(buffer_out, ENDP3_BUF0Addr, count_out);
}
else
{
FreeUserBuffer(ENDP3, EP_DBUF_OUT);
count_out = GetEPDblBuf1Count(ENDP3);
PMAToUserBufferCopy(buffer_out, ENDP3_BUF1Addr, count_out);
}
}
經過上面的修改,終於解決了STM32在處理接收資料時導致主機等待的情況,用BUS HOUND軟體測試了下
哈哈,這下終於爽了。
PS:上面的FreeUserBuffer(ENDP3, EP_DBUF_OUT); 這句話的上下位置是關鍵,如果放到函數的後面,則仍舊會有主機等待STM32處理資料的情況,速度仍然是500KB/S!
把這句話放在拷貝函數的前面的話就真正把雙緩衝PING-PONG機制用起來了。大致算了下PMAToUserBufferCopy(buffer_out, ENDP3_BUF1Addr, count_out);這句話當count_out為最大值64的時候STM32執行需要302個周期,72MHZ情況下約4.2微秒執行時間,而USB傳輸按照12Mb/s的線速度傳輸64位元組的資料至少也得40微秒,因此只要PMAToUserBufferCopy的時間不超過40微秒,就不會導致緩衝區競爭的情況。
===============================================================================
匯總3:STM32的USB中斷說明,來自:http://bbs.ednchina.com/BLOG_ARTICLE_238817.HTM
STM32的USB模組可以產生三種中斷:USB喚醒中斷、USB高優先順序中斷和USB低優先順序中斷,在STM32的參考手冊中沒有詳細說明這三種中斷對應哪些事件,現說明如下:
1)USB喚醒中斷:在中斷向量表中的位置是42。這個中斷在USB裝置從暫停模式喚醒時產生,喚醒事件由USB_ISTR寄存器的WKUP位標識。
2)USB高優先順序中斷:在中斷向量表中的位置是19。這個中斷僅由USB同步(Isochronous)模式傳輸或雙緩衝塊(Bulk)傳輸模式下的正確傳輸事件產生,正確傳輸事件由USB_ISTR寄存器的CTR位標識。
3)USB低優先順序中斷:在中斷向量表中的位置是20。這個中斷由所有其它的USB事件產生,例如正確傳輸(不包括同步模式和雙緩衝塊模式)、USB複位等,事件標誌位在USB_ISTR寄存器中。
在STM32的USB開發包的例子中包含了上述中斷的處理,例如在USB擴音器的例子中,CTR_HP函數處理USB高優先順序中斷;在所有例子中都有USB_Istr()函數處理USB低優先順序中斷
===============================================================================
匯總4:如何使用STM32的USB庫支援控制端點0,來自:http://bbs.ednchina.com/BLOG_ARTICLE_242276.HTM
首先我們先回顧一下控制端點的傳輸方式:
控制端點的傳輸有三個階段,SETUP階段、資料階段和狀態階段;資料階段又分為資料入(DATA IN)和資料出(DATA OUT),控制端點傳輸可以沒有資料階段;狀態階段有狀態入(STATUS IN)和狀態出(STATUS OUT)。
總結起來,控制端點有如下三種可能的傳輸過程(以下括弧中的0或1表示DATA0或DATA1傳輸):
一、 SETUP DATA_IN(0) DATA_IN(1) DATA_IN(0) ...... STATUS_OUT(1)
二、 SETUP DATA_OUT(0) DATA_OUT(1) DATA_OUT(0) ...... STATUS_IN(1)
三、 SETUP STATUS_IN(1)
這裡做一個約定,把上述過程一定義為“資料入過程”,過程二定義為“資料出過程”,過程三定義為“無資料過程”。所有的USB控制端點的資料轉送都可以而且只用這三種傳輸過程表示。HID的SET_REPORT是資料出過程,HID的GET_REPORT是資料入過程,USB的GET DEVICE DESCRIPTOR是資料入過程,USB的SET CONFIGURATION是無資料過程,等等。
接下來,我們看看STM32的USB庫是如何處理控制端點0的傳輸。
根據USB協議,每個SETUP包都由8個位元組構成,使用者程式可以通過結構體Device_Info(類型DEVICE_INFO)訪問SETUP包的資料,因為在整個的USB處理中都要用到結構體Device_Info的內容,庫中定義了一個全域的指標pInformation指向這個結構體,使用者可以通過這個指標訪問結構體的內容。
對應SETUP包的8個位元組,使用者可以用下述方式訪問:
pInformation->USBbmRequestType (位元組類型)
pInformation->USBbRequest (位元組類型)
pInformation->USBwValue (雙位元組類型)
pInformation->USBwIndex (雙位元組類型)
pInformation->USBwLength (雙位元組類型)
使用pInformation->USBwValue0訪問wValue的低位元組,pInformation->USBwValue1訪問wValue的高位元組。
使用pInformation->USBwIndex0訪問USBwIndex的低位元組,pInformation->USBwIndex1訪問USBwIndex的高位元組。
使用pInformation->USBwLength0訪問USBwLength的低位元組,pInformation->USBwLength1訪問USBwLength的高位元組。
通過分析SETUP包的8個位元組,可以判斷出一個SETUP的傳輸過程是屬於資料入過程、資料出過程還是無資料過程。STM32的USB庫中處理了所有的USB協議文本中定義的標準SETUP命令,對於USB協議文本中未定義的命令,USB庫按照資料入過程、資料出過程或無資料過程通過回呼函數交給使用者程式處理。
全域變數Device_Property(DEVICE_PROP類型)封裝了所有的回呼函數,DEVICE_PROP定義如下:
typedef struct _DEVICE_PROP
{
void (*Init)(void); // 裝置初始化回呼函數
void (*Reset)(void); // USB複位回呼函數
void (*Process_Status_IN)(void); // STATUS_IN階段處理回呼函數
void (*Process_Status_OUT)(void); // STATUS_OUT階段處理回呼函數
RESULT (*Class_Data_Setup)(u8 RequestNo); // 資料入/出過程處理回呼函數
RESULT (*Class_NoData_Setup)(u8 RequestNo); // 無資料過程處理回呼函數
RESULT (*Class_Get_Interface_Setting)(u8 Interface, u8 AlternateSetting); // GET_INTERFACE 回呼函數
u8* (*GetDeviceDescriptor)(u16 Length); // GET_DEVICE_DESCRIPTION回呼函數
u8* (*GetConfigDescriptor)(u16 Length); // GET_CONFIGURATION_DESCRIPTION回呼函數
u8* (*GetStringDescriptor)(u16 Length); // GET_STRING_DESCRIPTION回呼函數
u8 MaxPacketSize; // 最大包長度
} DEVICE_PROP;
結合SETUP的三種傳輸過程,使用者通過實現不同的回呼函數即可完成對各種USB類命令的處理,下面以HID的SET REPORT為例說明。
在介紹具體實現之前,先介紹一下另一個回呼函數CopyRoutine的概念,這個函數的原型是:
u8 *CopyRoutine(u16 length); // 返回一個緩衝區指標
USB庫通過這個函數獲得使用者的資料緩衝區地址,從而可以在資料出過程中把收到的資料拷貝到使用者緩衝區,或在資料入過程中把使用者緩衝區的資料拷貝到USB發送緩衝區。每個資料出過程可能有若干次DATA_OUT傳輸,USB庫每完成一次這樣的傳輸都會調用一次回呼函數CopyRoutine,參數length是本次傳輸所收到的資料位元組數目,CopyRoutine必須返回一個緩衝區指標,這個緩衝區必須能夠容納length位元組的資料,CopyRoutine返回到USB庫之後,USB庫將把收到的資料拷貝到使用者指定的緩衝區。同樣每個資料入過程也可能有若干次DATA_IN傳輸,每次需要向主機傳輸資料時,USB庫都會調用一次回呼函數CopyRoutine,參數length是本次傳輸所要發送的資料位元組數目,CopyRoutine必須返回一個緩衝區指標,這個緩衝區中必須包含要求的資料位元組,USB庫將把使用者緩衝區的資料拷貝到USB緩衝區並擇機發送出去。
當以length=0調用CopyRoutine時,CopyRoutine需要返回使用者緩衝區的長度,因為CopyRoutine的傳回型別是一個指標,所以需要通過類型的強制轉換返回緩衝區長度。這個功能是為了處理使用者緩衝區的長度與主機SETUP資料請求長度不符的情況,而不至於造成使用者緩衝區的溢出。
介紹完上述若干概念和回呼函數,再看SET_REPORT的實現就很容易了。
SET_REPORT是一個資料出過程,因此需要實現一個Class_Data_Setup回呼函數,樣本如下:
RESULT HID_Data_Setup(u8 RequestNo)
{
u8 *(*CopyRoutine)(u16 length);
CopyRoutine = NULL;
if (pInformation->USBbmRequestType == CLASS_REQUEST|INTERFACE_RECIPIENT
&& RequestNo == SET_REPORT)
CopyRoutine = My_Data_Request;
if (CopyRoutine == NULL)
return USB_UNSUPPORT;
pInformation->Ctrl_Info.CopyData = CopyRoutine;
pInformation->Ctrl_Info.Usb_wOffset = 0;
pInformation->Usb_wLength = (*CopyRoutine)(0);
return USB_SUCCESS;
} // End of HID_Data_Setup()
u8 My_Buffer[10];
u8 *My_Data_Request(u16 length)
{
if (length == 0)
return (u8*)10; // 假定你的REPORT長度和Buffer長度為10
return My_Buffer;
}
上面介紹的CopyRoutine用於把多次傳輸的資料包合并到一個完整的緩衝區中,因此只有到STATUS階段才能夠指導一次SETUP傳輸是否結束,所以使用者程式需要在回呼函數Process_Status_IN中處理從SET_REPORT接收到的資料。因為所有的回呼函數都是USB中斷處理的一部分,所以更好的辦法是在Process_Status_IN中設定一個標記,然後在使用者主程式中判斷這個標記並做處理。
注意,STM32的USB庫設計成以回呼函數處理使用者命令請求,包含類命令請求,是為了能夠清晰地區分庫程式和使用者程式,使這兩者不會混在一起,這樣的好處是非常明顯的,當USB庫需要更新升級時,只需替換掉相應的程式模組,而不必修改使用者已經完成的程式。
以上的介紹都可以在STM32 USB庫的說明手冊中找到。
上述示意代碼是以My_Buffer長度為10位元組為例,而USB庫的預設包長度為16位元組,因此My_Data_Request並沒有多包的處理。
關於多包的緩衝區處理的示意代碼可以是這樣的:
u8 *My_Data_Request(u16 length)
{
if (length == 0)
return (u8*)100; // 假定你的REPORT長度和Buffer長度為100
return &My_Buffer[pInformation->Ctrl_Info.Usb_wOffset];
}
這裡有一個庫中使用的變數pInformation->Ctrl_Info.Usb_wOffset,這個變數回在傳輸每個資料包時候由庫中的程式按資料包長度增加,如最大包長為16位元組時,第一次調用My_Data_Request時Usb_wOffset=0,第二次調用My_Data_Request時Usb_wOffset=16,第三次調用My_Data_Request時Usb_wOffset=32,依此類推。這樣就可以使用Usb_wOffset作為My_Buffer的下標從My_Data_Request返回。
對於提問“如何傳遞length?在上面沒有看到這個參數的傳遞過程”的回答:
參數length是用於檢測緩衝區長度是否足夠,如果你有足夠長的緩衝區,可以不必檢測,上述樣本中使用了一個固定的緩衝區,所以不必使用參數length檢測緩衝區長度。