电子说
为了让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"//调用串口相关头文件 #include "ros/ros.h"//在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、整片文章里,我几乎没有提到过函数报错的问题,主要是我暂时没有考虑关于报错的问题,所以代码中很少会有关于报错的内容。这个问题,可以等以后完善此通讯协议时解决。
审核编辑:汤梓红
全部0条评论
快来发表一下你的评论吧 !