為了讓RobomasterC板(這塊板用的是STM32F407IGHX的芯片)能與上位機進行通訊。我最近翻了不少博客和CSDN文章,看到了很多文章存在一些問題,經過了一下午試錯,我成功實現了STM32F407IGHX利用STM32CubeIDE進行配置并然后用HAL庫進行編程,與安裝有ROS的Ubuntu進行虛擬串口通信。
在翻看博客的時候我發現,RM以及上下位機通信資料并不多,而且很多已有資料都只講述了實現原理,卻沒有講如何具體一步步實現某個功能,這就導致初學者可能在翻看過程中,越看越懵,反而寫不出一份能用的代碼。
所以這篇文章會盡可能詳細的講怎么實現串口通信,而盡量少講其原理,由于很多文章都已經詳盡的寫出了串口通信的原理了,所以我就不在贅述原理而著重于實現過程。
此外,我也會把一些小問題和建議寫出來,以便一篇文章就解決所有可能存在的問題。
一、概述
1、STM32端(所謂的下位機):這邊采用的是通過有圖形化的STM32CubeIDE配置工程,配置好USB-CDC創建一個虛擬串口,與上位機通信。
2、Ubuntu端(所謂的上位機):上位機是版本20.04的ubuntu,安裝有版本為noetic的ROS,通過建立一個ROS節點來打開串口并建立通信。
二、STM32端具體實現過程
思路:利用STM32CubeIDE配置好USB-CDC,接著修改對應的頭文件,自定義所需的函數。
1、配置過程
1)先配置時鐘RCC,設置高速時鐘High Speed Clock為內部時鐘(Crystal/Ceramic Resonator),另一個暫時用不到所以不設置。
2)配置下載與調試(必須設置,否則會鎖芯片,到時候還需要通過BOOT重啟,比較麻煩)
設置為Serial Wire,時鐘為SysTick(當然看你到底有什么,如果你擁有的是ST-LINK,那么可以這樣設置)
3)設置USB模式,打開Connectivity,選擇USB-OYG-FS(快速),選擇Mode的Device_only(從機模式)。然后點開左下方的NVIC Settings,勾選Enabled,從而能夠開啟中斷。
備注:還要返回到NVIC中,設置USB中斷的優先級,這里設置個4就行(畢竟沒有啟動其他外設,所以中斷就不需要太嚴謹)、
4)打開MiddleWare,設置USB的具體工作方式,選擇Class For FS IP的Communication Device Class,即VCP(虛擬串口),其余設置保持默認即可,不需要額外修改。
5)時鐘樹設置(時鐘樹的設置,需要查閱所使用開發板的具體原理圖)
例如,RobomasterC板原理圖里是如此說明的,所以Input frequency要設置成12MHz。此外,下方畫紅線部分是USB的時鐘,USB的時鐘需要設置成48MHz才能工作,其余部分看自己的需求。
6)堆棧設置,堆棧的大小需要足夠大,才能滿足USB初始化的需求,此處設置Heap Size為0X600即可解決初始化失敗的問題,另一個不用改。
7)到此,所有的初始化已經結束了,只需要Ctrl+s,保存并生成代碼即可,下方兩個選項均選擇Yes,即可生成STM32CubeIDE工程
2、代碼的修改
這里要先打開工程里的USB_DEVICE中的App的usbd_cdc_if.c,重構官方給出的代碼,具體內容如下
/* USER CODE BEGIN Header */ /** ****************************************************************************** * @file : usbd_cdc_if.c * @version : v1.0_Cube * @brief : Usb device for Virtual Com Port. ****************************************************************************** * @attention * * Copyright (c) 2023 STMicroelectronics. * All rights reserved. * * This software is licensed under terms that can be found in the LICENSE file * in the root directory of this software component. * If no LICENSE file comes with this software, it is provided AS-IS. * ****************************************************************************** */ /* USER CODE END Header */ /* Includes ------------------------------------------------------------------*/ #include "usbd_cdc_if.h" /* USER CODE BEGIN INCLUDE */ /* USER CODE END INCLUDE */ /* Private typedef -----------------------------------------------------------*/ /* Private define ------------------------------------------------------------*/ /* Private macro -------------------------------------------------------------*/ /* USER CODE BEGIN PV */ /* Private variables ---------------------------------------------------------*/ /* USER CODE END PV */ /** @addtogroup STM32_USB_OTG_DEVICE_LIBRARY * @brief Usb device library. * @{ */ /** @addtogroup USBD_CDC_IF * @{ */ /** @defgroup USBD_CDC_IF_Private_TypesDefinitions USBD_CDC_IF_Private_TypesDefinitions * @brief Private types. * @{ */ /* USER CODE BEGIN PRIVATE_TYPES */ /* USER CODE END PRIVATE_TYPES */ /** * @} */ /** @defgroup USBD_CDC_IF_Private_Defines USBD_CDC_IF_Private_Defines * @brief Private defines. * @{ */ /* USER CODE BEGIN PRIVATE_DEFINES */ /* USER CODE END PRIVATE_DEFINES */ /** * @} */ /** @defgroup USBD_CDC_IF_Private_Macros USBD_CDC_IF_Private_Macros * @brief Private macros. * @{ */ /* USER CODE BEGIN PRIVATE_MACRO */ /* USER CODE END PRIVATE_MACRO */ /** * @} */ /** @defgroup USBD_CDC_IF_Private_Variables USBD_CDC_IF_Private_Variables * @brief Private variables. * @{ */ /* Create buffer for reception and transmission */ /* It's up to user to redefine and/or remove those define */ /** Received data over USB are stored in this buffer */ uint8_t UserRxBufferFS[APP_RX_DATA_SIZE]; /** Data to send over USB CDC are stored in this buffer */ uint8_t UserTxBufferFS[APP_TX_DATA_SIZE]; /* USER CODE BEGIN PRIVATE_VARIABLES */ /* USER CODE END PRIVATE_VARIABLES */ /** * @} */ /** @defgroup USBD_CDC_IF_Exported_Variables USBD_CDC_IF_Exported_Variables * @brief Public variables. * @{ */ extern USBD_HandleTypeDef hUsbDeviceFS; /* USER CODE BEGIN EXPORTED_VARIABLES */ /* USER CODE END EXPORTED_VARIABLES */ /** * @} */ /** @defgroup USBD_CDC_IF_Private_FunctionPrototypes USBD_CDC_IF_Private_FunctionPrototypes * @brief Private functions declaration. * @{ */ static int8_t CDC_Init_FS(void); static int8_t CDC_DeInit_FS(void); static int8_t CDC_Control_FS(uint8_t cmd, uint8_t* pbuf, uint16_t length); static int8_t CDC_Receive_FS(uint8_t* pbuf, uint32_t *Len); static int8_t CDC_TransmitCplt_FS(uint8_t *pbuf, uint32_t *Len, uint8_t epnum); /* USER CODE BEGIN PRIVATE_FUNCTIONS_DECLARATION */ /* USER CODE END PRIVATE_FUNCTIONS_DECLARATION */ /** * @} */ USBD_CDC_ItfTypeDef USBD_Interface_fops_FS = { CDC_Init_FS, CDC_DeInit_FS, CDC_Control_FS, CDC_Receive_FS, CDC_TransmitCplt_FS }; /* Private functions ---------------------------------------------------------*/ /** * @brief Initializes the CDC media low layer over the FS USB IP * @retval USBD_OK if all operations are OK else USBD_FAIL */ static int8_t CDC_Init_FS(void) { /* USER CODE BEGIN 3 */ /* Set Application Buffers */ USBD_CDC_SetTxBuffer(&hUsbDeviceFS, UserTxBufferFS, 0); USBD_CDC_SetRxBuffer(&hUsbDeviceFS, UserRxBufferFS); return (USBD_OK); /* USER CODE END 3 */ } /** * @brief DeInitializes the CDC media low layer * @retval USBD_OK if all operations are OK else USBD_FAIL */ static int8_t CDC_DeInit_FS(void) { /* USER CODE BEGIN 4 */ return (USBD_OK); /* USER CODE END 4 */ } /** * @brief Manage the CDC class requests * @param cmd: Command code * @param pbuf: Buffer containing command data (request parameters) * @param length: Number of data to be sent (in bytes) * @retval Result of the operation: USBD_OK if all operations are OK else USBD_FAIL */ static int8_t CDC_Control_FS(uint8_t cmd, uint8_t* pbuf, uint16_t length) { /* USER CODE BEGIN 5 */ switch(cmd) { case CDC_SEND_ENCAPSULATED_COMMAND: break; case CDC_GET_ENCAPSULATED_RESPONSE: break; case CDC_SET_COMM_FEATURE: break; case CDC_GET_COMM_FEATURE: break; case CDC_CLEAR_COMM_FEATURE: break; /*******************************************************************************/ /* Line Coding Structure */ /*-----------------------------------------------------------------------------*/ /* Offset | Field | Size | Value | Description */ /* 0 | dwDTERate | 4 | Number |Data terminal rate, in bits per second*/ /* 4 | bCharFormat | 1 | Number | Stop bits */ /* 0 - 1 Stop bit */ /* 1 - 1.5 Stop bits */ /* 2 - 2 Stop bits */ /* 5 | bParityType | 1 | Number | Parity */ /* 0 - None */ /* 1 - Odd */ /* 2 - Even */ /* 3 - Mark */ /* 4 - Space */ /* 6 | bDataBits | 1 | Number Data bits (5, 6, 7, 8 or 16). */ /*******************************************************************************/ case CDC_SET_LINE_CODING: break; case CDC_GET_LINE_CODING: break; case CDC_SET_CONTROL_LINE_STATE: break; case CDC_SEND_BREAK: break; default: break; } return (USBD_OK); /* USER CODE END 5 */ } /** * @brief Data received over USB OUT endpoint are sent over CDC interface * through this function. * * @note * This function will issue a NAK packet on any OUT packet received on * USB endpoint until exiting this function. If you exit this function * before transfer is complete on CDC interface (ie. using DMA controller) * it will result in receiving more data while previous ones are still * not sent. * * @param Buf: Buffer of data to be received * @param Len: Number of data received (in bytes) * @retval Result of the operation: USBD_OK if all operations are OK else USBD_FAIL */ static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len) { /* USER CODE BEGIN 6 */ USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]); USBD_CDC_ReceivePacket(&hUsbDeviceFS); return (USBD_OK); /* USER CODE END 6 */ } /** * @brief CDC_Transmit_FS * Data to send over USB IN endpoint are sent over CDC interface * through this function. * @note * * * @param Buf: Buffer of data to be sent * @param Len: Number of data to be sent (in bytes) * @retval USBD_OK if all operations are OK else USBD_FAIL or USBD_BUSY */ uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len) { uint8_t result = USBD_OK; /* USER CODE BEGIN 7 */ USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassData; if (hcdc->TxState != 0){ return USBD_BUSY; } USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len); result = USBD_CDC_TransmitPacket(&hUsbDeviceFS); /* USER CODE END 7 */ return result; } /** * @brief CDC_TransmitCplt_FS * Data transmitted callback * * @note * This function is IN transfer complete callback used to inform user that * the submitted Data is successfully sent over USB. * * @param Buf: Buffer of data to be received * @param Len: Number of data received (in bytes) * @retval Result of the operation: USBD_OK if all operations are OK else USBD_FAIL */ static int8_t CDC_TransmitCplt_FS(uint8_t *Buf, uint32_t *Len, uint8_t epnum) { uint8_t result = USBD_OK; /* USER CODE BEGIN 13 */ if(flag) { CDC_Transmit_FS(UserTxBufferFS, APP_TX_DATA_SIZE); } UNUSED(Buf); UNUSED(Len); UNUSED(epnum); /* USER CODE END 13 */ return result; } /* USER CODE BEGIN PRIVATE_FUNCTIONS_IMPLEMENTATION */ /* USER CODE END PRIVATE_FUNCTIONS_IMPLEMENTATION */ /** * @} */ /** * @} */
注意,不要輕易重新初始化代碼,否則這些對官方代碼的修改會被重新覆蓋,導致又要再改一遍,最好一次就初始化好。
3、自定義結構體
在這里我不會給出具體的代碼,但我會舉個例子來說明如何定義所需結構體。
typedef struct ControlData_Chassis _Controldata_Chassis;//這里是定義該結構體的別名 typedef struct ControlData_Chassis{ uint8_t Y_Speed; //縱軸方向速度 uint8_t X_Speed; //橫軸方向速度 uint8_t Rotational_Speed; //小車旋轉速度 uint8_t Chassis_State; //底盤狀態 }*_Controldata_ChassisInfo;//這里定義了該結構體的結構體指針。C語言允許這樣的操作!
在實際操作的時候,可以把這種結構體變量的數值放入到指定的數組中(這也就是所謂的打包。而把接收到的數組中的數據按結構體成員形式放入到指定結構體的過程,就稱之為解包。),從而實現打包。
此外,可以把結構體定義在頭文件中,便于在.c文件里函數的具體實現。
4、自定義解包/打包函數
這里我也只會給出一個例子。
void Pack_Data(_FeedBack* feedback,uint8_t* feedArray) { //把數組中信息封入數據包中 feedArray[0] = 0XFF;//這是幀頭 feedArray[1] = feedback->Shoot_Mode; feedArray[2] = feedback->Shoot_Speed; feedArray[3] = feedback->Armor_Id; feedArray[4] = (uint8_t)(feedback->HP_Remain); feedArray[5] = (uint8_t)(feedback->HP_Remain >> 8); feedArray[6] = 0XAA;//暫時無意義 feedArray[7] = 0XFE;//芝士幀尾 }
實際上,解包函數也是類似上文的操作,只不過是反了過來。
注:1.可以利用與 “ | ” 來將兩個數據拼成一個,將拆分的數據合成一個。
2.幀頭和幀尾起到了驗證的作用,可以用來驗證數據完整性。
5、自定義發送/接收函數
int CDC_SendFeed(uint8_t* Fed, uint16_t Len) { CDC_Transmit_FS(Fed, Len); return 0; }
上文調用了之前修改過的官方代碼,這樣模塊化的代碼更容易理解與閱讀。
6、備注
1)如果你要定義一個結構體指針并想給它賦值,那么你需要在賦值前給它分配空間,否則這個指針無法進行賦值。
例子:
_FeedBack* ft,fd;
ft=(_FeedBack*)malloc(sizeof(_FeedBack));//這里是結構體的空間分配以及具體賦值
三、Ubuntu端具體實現過程
思路:利用ROS的serial包來實現串口通信。
1、創建工程
此處創建工程,要記得包含roscpp rospy std_msgs 以及serial包(serial包是串口通信的關鍵)
2、創建主程序
我這里使用的是Visual Studio Code來編寫代碼。
可以先在終端切換到你所需要編寫代碼的文件夾,然后輸入 code . (注意后面那個點也是要輸入的,然后VS就會啟動并打開這個文件夾)。
接著就可以在VS里創建新.cpp文件。
注意:1、如果#include "ros/ros.h"時發現找不到所需的頭文件,那么需要修改該工程的配置。按住Shift+Ctrl+P ,即可打開配置欄,然后選中第一個即可。
2、創建好.cpp文件,記得要到CMakeList.txt里添加上該頭文件(其實只要去掉這三個語句前的#號,并修改部分內容即可,其他部分不用動)。
add_executable(robo-serial src/robo-serial.cpp)
add_dependencies(robo-serial ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
target_link_libraries(robo-serial
${catkin_LIBRARIES}
)
可以參考下方貼出的代碼來修改你的配置。
{ "configurations": [ { "browse": { "databaseFilename": "${workspaceFolder}/.vscode/browse.vc.db", "limitSymbolsToIncludedHeaders": false }, "includePath": [ "/home/jinshuai/ros-test/tf_test/devel/include/**", "/opt/ros/noetic/include/**", "/usr/include/**" ], "name": "ROS", "intelliSenseMode": "gcc-x64", "compilerPath": "/usr/bin/gcc", "cStandard": "gnu11", "cppStandard": "c++14" } ], "version": 4 }
我在這里會給出初始化的大概配置,而具體代碼不會提供,各位可以參考這個代碼進行修改。
#include "serial/serial.h"http://調用串口相關頭文件 #include "ros/ros.h"http://在ros下使用serial包進行通訊 #include "iostream" //全局變量定義區 serial::Serial sp;//創建一個Serial類 serial::Timeout to = serial::Timeout::simpleTimeout(5000);//創建timeout//全局變量定義區 int main(int argc,char** argv){ setlocale(LC_CTYPE,"zh_CN.utf8");//設置中文輸出 ros::init(argc,argv,"serial_port"); ros::NodeHandle n;//創建句柄 // serial::Serial sp;//創建一個Serial類 // serial::Timeout to = serial::Timeout::simpleTimeout(5000);//創建timeout sp.setPort("/dev/ttyACM0");//設置要打開的串口名稱 sp.setBaudrate(115200);//設置串口通信的波特率 sp.setTimeout(to);//串口設置timeout try { sp.open();//嘗試啟動串口 } catch(serial::IOException& e) { ROS_ERROR_STREAM("Unable to open port!Please check your setting!"); return -1; } if(sp.isOpen()) { ROS_INFO_STREAM("/dev/ttyACM0 is opened!");//判斷是否成功開啟串口 } else { return -1; } ros::Rate loop_rate(500); while(ros::ok()) { Data_Receive();//此處為自定義函數,不要復制,我沒給出具體實現過程 Data_Transmit();//此處為自定義函數,不要復制,我沒給出具體實現過程 loop_rate.sleep(); } sp.close(); return 0; }
3、備注
1)創建結構體,枚舉,打包/解包函數,發送/接收函數和STM32端幾乎一樣,所以可以按照STM32端的思路來操作。但是要注意,上位機的代碼應該是和下位機相對應的,下位機接收到的數據是來自上位機的,所以幀頭幀尾以及結構體成員應該保持一致。避免發送出錯。
2)如果你想要在ROS工程里自定義一個頭文件和C文件,那么記得去修改CMakeList.txt里的
add_executable(robo-serial src/robo-serial.cpp)
add_dependencies(robo-serial Test ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
target_link_libraries(robo-serial Test
${catkin_LIBRARIES}
)
add_library(Serial
src/Test.h
src/Test.cpp
)
否則會報錯,找不到該頭文件。修改方式參考上圖粗體部分。
四、可能存在的報錯
1、如果PC無法連接到虛擬串口,并顯示“無法獲取設備描述符”
我的解決辦法:
1)線路連接不良或者線路有問題,建議重新連接或者換一根線(有一定可能)
2)工程配置錯誤,時鐘樹有誤(需要根據你的開發板,重新觀察時鐘樹的配置。是否引入了正確的時鐘,以及是否配置好了USB時鐘(48MHz))
2、Ubuntu無法打開串口
1)連接有問題或者根本沒有連接
2)沒有權限打開串口(進入管理員模式(終端輸入sudo -i),接著編輯/etc/udev/rules.d/70-ttyusb.rules,加上一行KERNEL=="ttyUSB[0-9]*",MODE="0666" 保存退出即可。注意,要看具體需要給什么串口權限,虛擬串口一般叫做/dev/ttyACM0,所以可以寫入KERNEL=="ttyACM[0-9]*",MODE="0666" ,而真實串口一般叫/dev/ttyUSB0,可以用KERNEL=="ttyUSB[0-9]*",MODE="0666" 。)
3)STM32CubeIDE報錯GDB服務端無法打開。
我在博客里已經給出了詳盡的解釋
關于STM32CubeIDE無法正常啟動GDB服務端的解決辦法 - 墨髯 - 博客園 (cnblogs.com)
五、備注
1、實際上,很多的配置都需要看自己的需求來搞,我之前就盲目抄了其他人的時鐘樹配置,導致設備無法被電腦識別。所以如果出現問題,最好先去翻翻官方文檔。很多問題都可以通過官方文檔來解決。
2、整片文章里,我幾乎沒有提到過函數報錯的問題,主要是我暫時沒有考慮關于報錯的問題,所以代碼中很少會有關于報錯的內容。這個問題,可以等以后完善此通訊協議時解決。
審核編輯:湯梓紅
-
通信協議
+關注
關注
28文章
892瀏覽量
40332 -
Linux
+關注
關注
87文章
11314瀏覽量
209799 -
STM32
+關注
關注
2270文章
10906瀏覽量
356503 -
串口通信
+關注
關注
34文章
1626瀏覽量
55566 -
Ubuntu
+關注
關注
5文章
563瀏覽量
29857
發布評論請先 登錄
相關推薦
評論