STM32HAL----USB模擬串列埠(VCP)
想要實現的功能是,USB模擬串列埠收發資料。串列埠助手傳送資料至MCU,MCU接收後返回給串列埠助手。
當初是想用標準庫做這個功能的。但是因為後來瞭解到STM32CubeMX這個軟體,在嘗試之後實在是感覺,太方便了。所以,並沒有使用標準庫,而是直接用STM32CubeMX生成HAL庫的程式碼用了。
(1)先點New Project,然後輸入自己的MCU型號
(2)配置引腳與外設
這裡我用的是ST-LINK進行DeBug,Tim5提供系統延時節拍,PE5與PB5點亮LED。而SysTick,用在FreeRtos提供系統節拍。
(3)時鐘樹配置
(4)配置外設
這個頁面可以對外設進行功能的設定,比如GPIO的輸出型別或者引腳初始電平。在這裡主要設定FreeRTOS,建立2個初始任務。其他的比如USB,預設就可以使用了。
(5)Poject Settings
這裡注意兩個地方,我使用的是MDK。所以IDE選項選擇的是MDK-ARM V5。CodeGenerator選項卡下,將Generated files下的第一個選項打上勾,這樣就會啟用模組化程式設計,不同的外設封裝不同的.c .h檔案。至於Project Name跟Project Location,
自行設定便可。
然後,點選STM32CubeMX主介面的Project,Generate Code。就能在我們指定的資料夾內直接生成工程檔案。生成之後軟體提示你開啟專案,點選開啟後,工程內分組如圖:
因為使用了RTOS,所以程式設計主要圍繞兩個檔案,“usbd_cdc_if.c”以及“freertos.c”
“usbd_cdc_if.h”新增一個USB管理結構體的定義,並將“usbd_cdc_if.c”中兩個定義移到“usbd_cdc_if.h”
/* USER CODE BEGIN PRIVATE_DEFINES */ /* Define size for the receive and transmit buffer over CDC */ /* It's up to user to redefine and/or remove those define */ #define APP_RX_DATA_SIZE 1000 #define APP_TX_DATA_SIZE 1000 typedef struct { uint8_t OutFlag; uint8_t EFlag[2]; uint8_t SFlag; uint16_t ReLen; }USB_Dev; /* USER CODE END PRIVATE_DEFINES */ /* USER CODE BEGIN INCLUDE */
“usbd_cdc_if.c”宣告USB管理結構體變數並賦值,且修改“CDC_Receive_FS”函式。
/* Private typedef -----------------------------------------------------------*/
USB_Dev USB_S =
{
0,
{0x0D,0x0A},
0,
0,
};
static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{
HAL_GPIO_TogglePin(GPIOE,GPIO_PIN_5); //開啟接收指示燈
//將已接收資料長度賦值給USB_S.ReLen
USB_S.ReLen += *Len;
//判斷是否有結束標誌以及接收資料長度是否達到UserRxBufferFS長度上限
if( USB_S.ReLen<APP_RX_DATA_SIZE && \
UserRxBufferFS[USB_S.ReLen-2] != USB_S.EFlag[0] && \
UserRxBufferFS[USB_S.ReLen-1] != USB_S.EFlag[1]
)
{
//設定下一次接收資料的位置
USBD_CDC_SetRxBuffer(&hUsbDeviceFS, UserRxBufferFS + USB_S.ReLen);
USBD_CDC_ReceivePacket(&hUsbDeviceFS); //準備接收資料
}
else //長度達到,或者檢測到標誌位,觸發資料輸出
{
USB_S.OutFlag = 1;
}
return (USBD_OK);
}
“freertos.c”中,新增標頭檔案“usbd_cdc_if.h”並在對應的任務中新增對應功能:
/* LED_Toggle function */
void LED_Toggle(void const * argument)
{
for(;;)
{
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_5);
osDelay(500);
}
}
extern USB_Dev USB_S;
extern USBD_HandleTypeDef hUsbDeviceFS;
extern uint8_t UserRxBufferFS[APP_RX_DATA_SIZE];
/* USB_SendMess function */
void USB_SendMess(void const * argument)
{
uint16_t timeout = 0xffff;
uint8_t temp;
for(;;)
{
timeout = 0xffff;
if(USB_S.OutFlag)
{
temp = !(USB_S.ReLen%64); //判斷長度是否為64整數倍
while( CDC_Transmit_FS(UserRxBufferFS, USB_S.ReLen - temp) != USBD_OK && timeout--);
if(temp) //當傳送資料為64整數倍時,無法傳送成功,故分成2次傳送
{
while( CDC_Transmit_FS(UserRxBufferFS + USB_S.ReLen -1, temp) != USBD_OK && timeout--);
}
USB_S.ReLen = 0;
USBD_CDC_SetRxBuffer(&hUsbDeviceFS, UserRxBufferFS);
USBD_CDC_ReceivePacket(&hUsbDeviceFS);
USB_S.OutFlag = 0;
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_5, GPIO_PIN_SET); //接收指示燈關閉
}
osDelay(1);
}
}
啟動檔案:需將堆的容量調大,因為USB快取使用的是堆空間。我將它調為4K:
Heap_Size EQU 0x1000
至此,程式碼建立並修改完畢。
下載程式碼到板子,下載完畢後關閉重上電,然後開啟串列埠助手對應的串列埠。波特率之類全都不需設定。
傳送一篇長文章,MCU接收後返回給串列埠助手
現象如圖:
寫在最後
(1)MCU端接收資料問題。
USB傳輸一次最多64位元組。所以,如果想一次傳輸大量資料給MCU。需制定協議。如圖的串列埠助手,在傳送完畢資料之後,會補上2位元組結束標誌:0x0D,0x0A。MCU端可根據結束標誌判斷資料是否接收完畢。需注意的是,不同的串列埠助手協議不一定相同。有的是根據資料包長度判斷是否接收完畢。
(2)連續輸出的問題。
1、需要輪詢傳送函式返回值是否是“USBD_OK”
2、輸出字串,如果不去掉字串最後的結束符。某些上位機軟體,只能顯示1次傳送的資料,第二次
傳送的資料不能正常顯示。(本部落格使用的串列埠助手就是如此。。。。) ,但使用過另外一款串列埠
助手,並未發現此情況。
3、輪詢返回值有可能導致程式卡死,因為主機若是沒有接收MCU傳送的資料,MCU會一直輪詢
直到主機接收完資料為止,解決辦法是加個timeout--到while裡面,到時間跳出迴圈。
(3)WINDOWS下不能識別的串列埠有黃色感嘆號。
這個有可能是堆設定不夠大。我在F4下遇到了這問題,將堆空間“Heap_Size EQU 0x200”
設定為“Heap_Size EQU 0x1000”,黃色感嘆號消失
(4)通訊速度問題
參考“http://bbs.21ic.com/icview-811704-1-1.html”,附件中有測試速度的軟體。
串列埠助手當通訊速度40KB/S左右,接收不到資料。當然,如果是連續傳送'0',串列埠助手上視窗不
顯示字元,是可以接收到資料的,但當速度超過500KB/S,依舊不能接收資料。所以,網上很多反映
VCP速度只有幾十KB/S的,估計是上位機軟體問題。
使用附件中的軟體,可以成功測出速度。STM32CubeMX生成的程式,經測試傳送到主機的速度可以
達到1000KB/S以上。
(5)“CDC_Receive_FS”函式的解析
這個函式的作用是兩個,一是設定下一次接收資料的Buff,二是處理接收端點。
這個函式是在MCU接收完資料之後才呼叫的,而不是進入這個函式才開始接收資料。比方MCU接收到64byte的資料,
接收完成後進入這個函式,設定下一次接收資料的Buff。然後呼叫“USBD_CDC_ReceivePacket”處理接收端點。
如果不呼叫“USBD_CDC_ReceivePacket”,是無法進行下一次的接收的,但傳送是可以的。
(6)CDC_Transmit_FS 傳送64整數倍位元組數的資料出錯
當我呼叫“CDC_Transmit_FS”傳送64位元組的資料時,串列埠助手並不能接收到資料。
總的來說,USB的bulk協議以傳送小於64位元組或者是字長為0的資料包作為結束動作。
傳送64位元組的包的時候,只需要末尾再發送個字長為0的包即可。