2021年7月16日 星期五

STM32_USB_DIY(零) --- 簡單的解讀原廠的USB 標準函數庫(4.1.0 版)

相對於IC 設計或硬體,搞USB 絕對是純系統開發,不管你用誰的單晶片 MCU。

因為不管是 8 bits 或 32 bits MCU,他們的USB 硬體上的支持都是大同小異的。

晶片中硬體可以幫你做的事真的很有限,幾乎所有的USB 系統應用絕對是

要靠寫韌體及 Host (PC) 應用軟體來完成。所以你想搞好USB 系統應用,

絕對是擺脫不了這兩件事的。

現在關於 32 bits ARM, 尤其是市場主力大宗的 stm32 來說:在網路上大家都

很容易取得許多開發支援,所以我認為可以寫這方面的技術參考文件。

我在去年十月有陸續說明了一下這方面的一些資訊,但我想可能對許多想入門

的人來說:可能還是太簡單快速地說明了,可能不是很多人可以搞清楚的。




所以我就補了這一篇第零章的簡單原廠提供的 USB 標準函數庫的文章,

嘗試用另一個角度來提供一些USB 的相關系統開發參考資料。

雖然在大陸或其他網站上、書籍等有很多stm32 的資料或原始碼,但真的很少

有人會針對USB 這方面的資料提供比較詳細的解說資料。

但我還是要先聲明一下:我雖然藉由原廠提供的 USB 標準函數庫,但有很多USB

基本的通訊協議或相關規格,大家還是要一些基本的認識或了解,但也可以參考

我其他USB DIY 文章內的一些簡單教學與說明吧。


這個 Hinet 網頁已經時代久遠了,但有些USB 的基本知識與概念還是非常有用的。

另還有一些關於 HID 簡單應用說明:


這篇文章尾端還有其他內容的連結,大家無聊有空也可以多少參考一下吧。

有些幾乎都是十幾年前的技術內容了,但USB 在科技技術應用領域裡,

真的走得很遠了,它仍歷久不衰,的確是一個值得學習典藏的技術啊。
///--
USB 傳輸通訊,基本上跟UART 或其他系統通訊界面都是一樣的:只有TX/RX 。

USB 是主從架構:一般我們的裝置都屬於從(Slave),而主(Master)就是PC (Host)。

我這邊所謂的USB 系統開發指的就是一般 USB 裝置,屬於傳輸的被動裝置。

所以我們以下內容指的  

RX 就從PC (host) 接收命令/資料。(在USB 裡稱為Out Token)

TX 就是傳命令/資料给PC (Host)。(在USB 裡稱為In Token)

這張圖就很清楚明瞭。


這是基本的 Endpoint 0,最基本的一層通道。(Endpoint 的概念,你可以這麼想吧)

一個USB 裝置可以定義很多通道(Endpoint 0~ Endpoint X),但一定有Endpoint0。

所以對USB 裝置來說:

Rx 介面所對應就是 Setup Token 及 Out Token。

Tx 介面所對應就是 In Token。

但是上面那張圖無法說明這些 Setup/Out/In Token 跟我們要講的USB 函數庫

有甚麼關係?一般來說:USB 所有的通訊命令/資料內容,都是透過單晶片裡的

中斷機制來通知你的,再由你在中斷服務程式裡,來處理這些通訊命令/資料。

我們就用上圖例子補充說明:

這是 Host 要下一組命令/資料給USB 裝置時,會發生中斷的時間點。

一般來說:最後一組沒有帶任何資料,所以我習慣都稱為 No data in (Null in)。

後面兩個ACK 可以用 NAK 來擋通訊持續進行,爭取我們在韌體程式上的

處理時間。但第一個 Setup 的ACK 不能用NAK擋。

不過你也不用擔心,這部分單晶片的硬體會幫你處理的。

這裡先補充一點:USB 裝置韌體開發有兩個基本非常重要的觀念:

1. 我們的韌體程式永遠是後知後覺的。

    都是等硬體發中斷通知我們才知道剛剛在USB 通訊上發生甚麼事,哪怕我們

自己發出命令/資料有沒有成功?也是要等硬體發中斷通知我們才知道的。

2. 對於USB 的通訊方式,韌體程式基本上都是用擋的。

    因為USB Bus 的通訊線上都是一直有內容在跑動,所以我們的裝置韌體只能靠

單晶片裡的USB 硬體幫我們擋住(回覆處理)這些一直跑動的內容。免得我們程式

受到干擾,等我們把命令/資料處理完之後,再通知硬體幫我們發送或接收的。

而這張圖指的是Host 要求我們韌體回覆或傳輸一些資料給他。我們韌體也

是會收到Host 發出 Ack 的中斷。

(這邊補充一下,不管上面兩種傳輸方向,中間那些 "..." 代表多少資料量?

我自己的經驗:在 Windows 作業系統裡,它可以一次傳到 4096 Bytes,

以 USB 的最大Packet Size (USB 2.0 ) 64 Bytes 來說:可以一次傳 64 次才會

發出一次中斷通知你的韌體程式。)

這是沒有帶任何通訊資料的一種傳輸內容。這部分後面會補充說明。
----
以下這張圖就是示意USB 通訊發生中斷點的時機點:NAK 沒有。ACK 才有。


----
所以有以上的圖示說明,我們就很容易解讀原廠的程式內容了。

以下的程式範例內容我是引用 stm32 USB 4.1.0 版的函數庫。

stm32 的USB 中斷有兩組 : 

void CTR_LP(void)
///Low priority Endpoint Correct Transfer interrupt's service

void CTR_HP(void)
///High Priority Endpoint Correct Transfer interrupt's service

我們常用的就只有上面那一組Low priority Endpoint,因為所謂的 High priority 

Endpoint 是用來處理 ISO/Double buffer Bulk 傳輸的,用白話文來說:

就你要拿來傳影音資料/乒乓buffer(交互式資料緩衝區)傳輸的。是拿來搶

USB 傳輸頻寬的。但我跟你說:這些在市面產品上,幾乎都會被USB 專用IC

給搶了,用單晶片寫韌體程式?很難啊。

你可以參考這篇文章內容:

Hinet 網頁系列 --- USB DIY 系列(四)---USB DIY 講座 (二)


-----
所以:原廠的 USB 中斷服務程式就分成(USB_int.c):

Rx 方向的(Endpoint0):Setup0_Process();/Out0_Process();

Tx 方向的(Endpoint0):In0_Process();

(只有 Endpoint 0 才會有 Setup/Out/In 這些東西)

而其他Endpoint 就統稱為:

Rx 方向: (*pEpInt_OUT[EPindex-1])();

Tx 方向: (*pEpInt_IN[EPindex-1])();

至於你的系統應用,是用了多少或哪幾個 EndpointX ,那就得看你的USB 宣告。

再依照宣告打開對應的服務副程式(usb_conf.h):

/* associated to defined endpoints */
//#define  EP1_IN_Callback   NOP_Process
#define  EP2_IN_Callback   NOP_Process
#define  EP3_IN_Callback   NOP_Process
#define  EP4_IN_Callback   NOP_Process
#define  EP5_IN_Callback   NOP_Process
#define  EP6_IN_Callback   NOP_Process
#define  EP7_IN_Callback   NOP_Process


#define  EP1_OUT_Callback   NOP_Process
//#define  EP2_OUT_Callback   NOP_Process
#define  EP3_OUT_Callback  NOP_Process
#define  EP4_OUT_Callback   NOP_Process
#define  EP5_OUT_Callback   NOP_Process
#define  EP6_OUT_Callback   NOP_Process
#define  EP7_OUT_Callback   NOP_Process

所以接下來就是對應的:

Setup0_Process(); 依照我們上述原則:後知後覺... 這個副程式就是解讀

Setup Token 所傳給我們裝置的命令或資料內容了。

往下就會分成兩種有帶資料傳輸或沒有帶資料傳輸(usb_core.c):

  if (pInformation->USBwLength == 0)
  {
    /* Setup with no data stage */
    NoData_Setup0();
  }
  else
  {
    /* Setup with data stage */
    Data_Setup0();
  }

簡單來說:就是以下三種情形發生順序。(參考上述圖示說明)

1.Setup_Out_In(Null In)

2.Setup_In_Out(Null Out)

3.Setup_In(Null In)

---
uint8_t Out0_Process(void) 這個就是處理 Out Token 產生的ACK 中斷,

不過很重要的是:這個 Out 可能會連續出現好幾次,也有可能是 Null Out 的

那一次。所以這些副程式就比較複雜一點了。

同理:

uint8_t In0_Process(void) 這個就是處理 In Token 產生的ACK 中斷,

不過很重要的是:這個 In 可能會連續出現好幾次,也有可能是 Null In 的

那一次。所以這些副程式就比較複雜一點了。

但實際上:這兩個副程式的後續應用程式是大不同的。

Setup_Out_In(Null In)  這個東西是指Host 給你命令或資料後,你裝置是有後續

工作要做的;而Setup_In_Out(Null Out)就有可能是回覆Host 之後,

USB 裝置就沒事幹了。這些實際應用,後續有機會再舉例說明吧。

至於在USB 規範中,哪些是屬於:Setup_In(Null In)的呢?

譬如像:

Device 類的:
        Set Configuration
        Set Address
        Set Feature
        Clear Feature

Interface 類的:
        Set Interface
    
Endpoint 類的:
        Clear Feature
        Set Endpoint Feature。        

這一部分你們自己可以參考程式裡的寫法。
---
以上這些都是USB 基本韌體程式的控制流程。

但 USB 令大家比較討厭的地方就是:不同的裝置宣告,就有不同的應用介面

傳輸方式,我以下就舉兩個不同的裝置 Class :

1. HID Class 裝置。(這個要支持 Interrupt Token)

2. MSDC Class 裝置。(這個就是一般 Bulk Only Transfer 的類似隨身碟裝置)

USB 不管你是宣告哪一種 Class 裝置,基本上所有的回覆宣告內容都是一樣

流程的,只是內容不同而已。

所以 stm32 原廠就提供一個簡單的程式對應方式(usb_core.h):

typedef struct _DEVICE_PROP
{
  void (*Init)(void);        /* Initialize the device */
  void (*Reset)(void);       /* Reset routine of this device */

  /* Device dependent process after the status stage */
  void (*Process_Status_IN)(void);
  void (*Process_Status_OUT)(void);

  /* Procedure of process on setup stage of a class specified request with data stage */
  /* All class specified requests with data stage are processed in Class_Data_Setup
   Class_Data_Setup()
    responses to check all special requests and fills ENDPOINT_INFO
    according to the request
    If IN tokens are expected, then wLength & wOffset will be filled
    with the total transferring bytes and the starting position
    If OUT tokens are expected, then rLength & rOffset will be filled
    with the total expected bytes and the starting position in the buffer

    If the request is valid, Class_Data_Setup returns SUCCESS, else UNSUPPORT

   CAUTION:
    Since GET_CONFIGURATION & GET_INTERFACE are highly related to
    the individual classes, they will be checked and processed here.
  */
  RESULT (*Class_Data_Setup)(uint8_t RequestNo);

  /* Procedure of process on setup stage of a class specified request without data stage */
  /* All class specified requests without data stage are processed in Class_NoData_Setup
   Class_NoData_Setup
    responses to check all special requests and perform the request

   CAUTION:
    Since SET_CONFIGURATION & SET_INTERFACE are highly related to
    the individual classes, they will be checked and processed here.
  */
  RESULT (*Class_NoData_Setup)(uint8_t RequestNo);

  /*Class_Get_Interface_Setting
   This function is used by the file usb_core.c to test if the selected Interface
   and Alternate Setting (uint8_t Interface, uint8_t AlternateSetting) are supported by
   the application.
   This function is writing by user. It should return "SUCCESS" if the Interface
   and Alternate Setting are supported by the application or "UNSUPPORT" if they
   are not supported. */

  RESULT  (*Class_Get_Interface_Setting)(uint8_t Interface, uint8_t AlternateSetting);

  uint8_t* (*GetDeviceDescriptor)(uint16_t Length);
#ifdef LPM_ENABLED
  uint8_t* (*GetBosDescriptor)(uint16_t Length);
#endif
  uint8_t* (*GetConfigDescriptor)(uint16_t Length);
  uint8_t* (*GetStringDescriptor)(uint16_t Length);

  /* This field is not used in current library version. It is kept only for 
   compatibility with previous versions */
  void* RxEP_buffer;
   
  uint8_t MaxPacketSize;

}DEVICE_PROP;

這些基本上就是對應到我們剛剛講的東西及一些USB 基本宣告架構。

所以針對兩種 USB class 裝置來說就是:

對於 HID class 的對應副程式為(usb_prop.c):

DEVICE_PROP Device_Property =
  {
    CustomHID_init,
    CustomHID_Reset,
    CustomHID_Status_In,
    CustomHID_Status_Out,
    CustomHID_Data_Setup,
    CustomHID_NoData_Setup,
    CustomHID_Get_Interface_Setting,
    CustomHID_GetDeviceDescriptor,
    CustomHID_GetConfigDescriptor,
    CustomHID_GetStringDescriptor,
    0,
    0x40 /*MAX PACKET SIZE*/
  };
---
對於 MSDC class 的對應副程式為(usb_prop.c):

DEVICE_PROP Device_Property =
  {
    MASS_init,
    MASS_Reset,
    MASS_Status_In,
    MASS_Status_Out,
    MASS_Data_Setup,
    MASS_NoData_Setup,
    MASS_Get_Interface_Setting,
    MASS_GetDeviceDescriptor,
    MASS_GetConfigDescriptor,
    MASS_GetStringDescriptor,
    0,
    0x40 /*MAX PACKET SIZE*/
  };
---
這樣子你就比較容易維護相關的USB 韌體程式。不同的 USB Class 裝置就可以

有自己對應的宣告副程式(包括回覆宣告不同,也有不同對應的副程式):

HID Class :


MSDC Class:


因為不同的 Class 所要支援的介面特性與內容不同,所以就必須分開定義與處理。

但基本上這些一部分屬於USB 規格 Chapter 9 定義外,也要參考各個 Class 裝置的

規格說明書。

然後也依據不同 USB Class  裝置不同,所要對應的裝置特殊需求也會有不同的

韌體需求:譬如:

HID Class 是用不到 Class Clear Feature 的。


但對於 MSDC Class 這種裝置來說: Class Clear Feature 是還蠻常遇到的。


所以這兩種所要處理的副程式也會有所不同。

接下來就是非常簡單的韌體程式拓展開發與維護了。

以上,就是很簡單的以一般常見的 stm32 平台中,原廠所提供的USB 函數庫

說明一下USB 韌體系統的開發架構。

一下子講太多,怕大家又亂了。
---
最後我藉由舉一個簡單例子,來說明這些韌體函數庫的重要性。這也是我在之前

一開始的三篇 stm32 USB DIY 文章中提到的東西:

stm32 原廠 USB 標準函數庫(4.1.0 版) 裡所碰到的怪現象:

原廠函數庫(usb_core.c):

/**
  * Function Name  : DataStageOut.
  * Description    : Data stage of a Control Write Transfer.
  * Input          : None.
  * Output         : None.
  * Return         : None.
  */
void DataStageOut(void)
{
...
...
...
  /* Set the next State*/
  if (pEPinfo->Usb_rLength >= pEPinfo->PacketSize)
  {
    pInformation->ControlState = OUT_DATA;
  }
  else
  {
    if (pEPinfo->Usb_rLength > 0)
    {
      pInformation->ControlState = LAST_OUT_DATA;
    }
    else if (pEPinfo->Usb_rLength == 0)
    {
      /* USB spec: section 8.5.3.1 Reporting Status Results */
      pInformation->ControlState = WAIT_STATUS_IN;
      (*pProperty->Process_Status_IN)();      
      if (pInformation->ControlState == STALLED)
      {
        /* command failed to complete: 
        in this case we should return a STALL */
        vSetEPRxStatus(EP_RX_STALL);
        vSetEPTxStatus(EP_TX_STALL);
      }
      /* command completed successfully: 
      send a zero-length packet during status stage */
      else USB_StatusIn();
    }
  }
}

這段程式就是處理我們上述的那一個程序問題;

即所謂的"Setup_Out_In(Null In)"程序。就是發生在第二個中斷點的程式,

這段程式是在已經完成接收完Host資料,並等下一個 Null -In 的ACK 中斷發生。

但USB 2.0 規格書中的 8.5.3.1 有定義說:人家主機(Host) 也可以不理你。

不回你 ACK;反而用 Stall Token 來解問題。那你的韌體程式就掛了。

因為一般這種"Setup_Out_In(Null In)" 往往就是USB 裝置在接收Host  下命令

來執行某一個工作(Task)。結果你的USB 裝置沒收到最後的ACK   中斷,

可能就會錯失(Miss) 這一個Host 下達的指令了。

(譬如:這有可能發生在透過 USB 執行系統程式更新的 Bootloader 裡面:

等你收完新版韌體程式碼之後,等著Host 的Ack 來進行下一步的韌體更新,

也有可能發生在下載更新程式碼的階段、時候。就有可能掉資料 或重複

收資料了。反正,會造成很難除錯的系統問題。)

所以原廠的工程師就乾脆自作聰明的:反正我不管有沒有最後一個ACK 中斷。

我就直接執行相關的應用程序:"(*pProperty->Process_Status_IN)();  "。

所以就造成我在以下這篇實驗文章中看到原廠USB HID 範例的怪現象:

STM32_USB_DIY(三) --- Custom HID (三) :PC 端應用軟體及韌體開發



因為 Custom HID Class 裝置應用中,Set Feature 就是這一種"Setup_Out_In(Null In)"

命令型態方式處理的。結果第一個出現的 EP0 LED1 On 是這一個程式:

"(*pProperty->Process_Status_IN)();  " 執行的,但最後在真正有Null-In 的

ACK  中斷中: In0_Process(); 又會執行一次。雖然在這一個原廠示範程式裡,

都是去執行"EP0 LED1 On" 點LED 亮,大家也就沒發現這個問題了。

所以我們真正的修正這個問題應該改寫如下才對:

    else if (pEPinfo->Usb_rLength == 0)
    {
      /* USB spec: section 8.5.3.1 Reporting Status Results */
      //pInformation->ControlState = WAIT_STATUS_IN;
      //(*pProperty->Process_Status_IN)();      
      if (pInformation->ControlState == STALLED)
      {
        /* command failed to complete: 
        in this case we should return a STALL */
        vSetEPRxStatus(EP_RX_STALL);
        vSetEPTxStatus(EP_TX_STALL);
        (*pProperty->Process_Status_IN)();      
      }
      /* command completed successfully: 
      send a zero-length packet during status stage */
      else{ 
pInformation->ControlState = WAIT_STATUS_IN;
USB_StatusIn();
        }
    }
---
那甚麼大家都沒發現呢?因為如果你是採用一般標準的 USB Class 裝置規格的

話,應該也沒有這一種:"Setup_Out_In(Null In)" ,要USB 裝置去執行某一個特定

命令的情事發生吧。

 --- 所以很明顯的,原廠工程師還是犯了一個小錯誤啊。

-------
結語:

原本想簡簡單單的描述一下關於 stm32 的 USB 原廠標準函數庫的內容。

結果,還是寫得落落長...這也只能說:搞USB 系統韌體真的有點挑戰性。

這還光只是基本韌體而已,我們還沒有真正進入與應用軟體間的交互

資料傳輸問題。

但不管如何,有總比完全沒有開始還是有點進展吧。個人學會打通這些

USB 系統開發,也是完全陰錯陽差的時機 Chance 緣故。但我相信:

只要肯努力,這些照著死板規格做的東西,還是可以上手的啦。

所以藉此分享這些技術內容給有緣的各位,希望大家都有機會搞定令人

卻步的 USB 系統開發。

(待續)


2 則留言:

  1. 太棒的解說了,你這個很像在拍熱賣電影,會再來個xxx前傳。太精彩了,很有內容。

    回覆刪除
    回覆
    1. 謝謝你的留言與讚美。

      其實搞技術DIY 發展也是可以很有趣的。

      就看你用甚麼心態面對它而已。

      刪除