系列文章目录
·【STM32】STM32与PC端、HC-06、ROS进行USART串口通信
·【ROS】ROS上位机使用Serial库和boost::asio库与STM32进行USART通讯
·【STM32】STM32F103C8T6+L298N通过PWM控制直流电机转速
·【STM32】STM32F103C8T6使用外部中断法和输入捕获法进行编码器测速
·【STM32】STM32F103C8T6实现直流电机速度PID控制
目录
前言
上一篇文章实现了STM32与各个系统进行USART通讯,这一片文章讲解ROS上位机部分与STM32下位机部分进行通讯的测试,这篇文章是测试程序,主要讲解库的使用,在具体功能实现时还需要进行大刀阔斧的更改,再以后会进行讲解。
一、Serial库
Serial库是ros自带的专门用来串口通讯的库,它的基本函数调用比较简单,功能包的函数解释可以参考它的原文档。
1.安装Serial库
先安装Serial库,并查看需要进行串口通讯的串口,这里提供了两种下载方式
方法一:直接安装
$ sudo apt-get install ros-<版本号>-serial
然后在需要用到serial库时,在新建功能包时添加serial的依赖即可。
方法二:通过github源码安装
有时候直接安装可能会出现无法定位项目的情况,比如20.04的ros,可以直接通过源码安装,不过连接可能会失败,可以连手机热点多试几次。
$ cd ~
$ cd catkin_ws/src
$ git clone https://github.com/wjwwood/serial.git
然后编译一下:
$ cd serial
$ make
$ make install
创建功能包时添加依赖:
$ catkin_create_package serial_test rospy roscpp serial geometry_msgs
2.进行串口通讯
我自己总结的ROS的Serial进行串口通讯的步骤如下:
(1)创建串口类
在使用serial库进行串口通讯之前需要建立一个serial对象,基于这个对象进行各种功能的实现。
serial::Serial sp;//创建一个serial对象
(2)串口初始化
串口初始化的过程包括设置超时、波特率、设置要打开的串口等等,其中的一些设置要与通信的另一方保持一致!
在进行初始化后,使用try-catch语句打开串口,serial::IOException& e则是serial库提供的错误提示类。
/********************************************************
函数功能:串口参数初始化
入口参数:无
出口参数:无
作者:K.Fire
日期:2022.02.10
********************************************************/
void Serial_Init()
{
serial::Timeout to = serial::Timeout::simpleTimeout(100);//创建timeout
serial::parity_t pt = serial::parity_t::parity_none;//创建校验位为0位
serial::bytesize_t bt = serial::bytesize_t::eightbits;//创建发送字节数为8位
serial::flowcontrol_t ft = serial::flowcontrol_t::flowcontrol_none;//创建数据流控制,不使用
serial::stopbits_t st = serial::stopbits_t::stopbits_one;//创建终止位为1位
sp.setPort("/dev/ttyUSB0");//设置要打开的串口名称
sp.setBaudrate(9600);//设置串口通信的波特率
sp.setParity(pt);//设置校验位
sp.setBytesize(bt);//设置发送字节数
sp.setFlowcontrol(ft);//设置数据流控制
sp.setStopbits(st);//设置终止位
sp.setTimeout(to);//串口设置timeout
try
{
//打开串口
sp.open();
}
catch (serial::IOException& e)
{
ROS_ERROR_STREAM("Unable to open port.");
return;
}
}
其中用到的函数可以在官方文档中查到,如下图:
当然其实这些可以不设置,如果不进行手动设置的话,serial库也为我们准备了一套默认设置,在这里可以看到:
timeout分为全局超时和间隔超时,简单来说,全局超时用来规定整个传输过程的总时长,如果超过这个时间,就会直接返回;间隔超时是在读或写后触发的超时,在接收到一个数据时超时开始计时,如果在规定时间内接收到下一个数据则说明接收未完,继续接收,如果未收到数据则直接返回。
(3)编写串口接收函数
串口接收功能的实现主要依赖于这个函数:sp.read(buf,n)
我们也可以使用sp.available函数获取接收缓冲区的字节数,或者使用sp.isopen函数判断串口是否打开。
在接收数据时,我们一般建立一个接收缓存数据buf[]进行数据接收,也可以使用ROS_INFO或者std::out将数据打印输出到终端上。
下面是我针对STM32下位机通讯编写的测试程序,在测试时,我是连接到了HC-06上进行的测试,因为买的单片机还没到。
在正式开始函数编写之前先做一些准备工作:
#include <serial/serial.h>
#include "MbotRosSerial.h"
#include <iostream>
/********************************************************
串口发送接收相关常量、变量、共用体对象
********************************************************/
const unsigned char ender[2] = { 0x0d, 0x0a };
const unsigned char header[2] = { 0x55, 0xaa };
serial::Serial sp;//创建一个serial对象
//发送左右轮速控制速度共用体
union sendData
{
short d;
unsigned char data[2];
}leftVelSet, rightVelSet;
//接收数据(左轮速、右轮速、角度)共用体(-32767 - +32768)
union receiveData
{
short d;
unsigned char data[2];
}leftVelNow, rightVelNow, angleNow;
其中在接收过程中使用到了C语言的结构-共用体,以上述程序中的union sendData为例,它的作用简单来说是,其中的两个变量short和unsigned char data[2]公用一段内存,可以根据名字区分两个变量,这个共用体的占用字节数是成员变量中最大的字节数,这里的两个成员变量占用字节数都是2个字节。
在线程序可以看到,我将接收的速度和航向角存到了data[2]数组中,但是在赋值时我们需要的是double类型的变量,这时就可以将d赋值给接收的变量,因为它们公用一块内存。
关于union的知识大家可以查找相关的详细解释。
/********************************************************
函数功能:读取下位机数据,左右轮速度值、航向角、控制信息
入口参数:左右轮速度值、航向角、控制信息
出口参数:bool
作者:K.Fire
日期:2022.02.10
********************************************************/
bool ReceiveSTM32(double& Left_v, double& Right_v, double& Angle, unsigned char& CtrlCmd)
{
char i, length = 0;
unsigned char checkSum;
unsigned char buf[150] = { 0 };
int n;//信息长度
size_t Receive_N = sp.available();//获取缓冲区内的字节数
if (Receive_N != 0)
{
n = sp.read(buf, Receive_N );//读出数据
// 检查信息头
if (buf[0] != header[0] || buf[1] != header[1]) //buf[0] buf[1]
{
ROS_ERROR("Received message header error!");
return false;
}
//读取控制位
CtrlCmd = buf[2]; //buf[2]
//读取数据长度
length = buf[3]; //buf[3]
// 检查信息校验值
checkSum = getCrc8(buf, 4 + length); //buf[10] 计算得出
if (checkSum != buf[4 + length]) //buf[10] 串口接收
{
ROS_ERROR("Received data check sum error!");
return false;
}
// 读取速度值和航向角
for (i = 0; i < 2; i++)
{
leftVelNow.data[i] = buf[i + 4]; //buf[4] buf[5]
rightVelNow.data[i] = buf[i + 6]; //buf[6] buf[7]
angleNow.data[i] = buf[i + 8]; //buf[8] buf[9]
}
Left_v = leftVelNow.d;
Right_v = rightVelNow.d;
Angle = angleNow.d;
return true;
}
}
其中用到的循环冗余校验函数在下面会写出。
(4)发送数据
发送数据主要使用sp.write(buf,n)函数,write函数有多种形式,可以在文档中查看使用,这里我使用的是第一种传入数据存储数组的首地址和发送字节长度的形式,返回值是实际写入串口的字节数,可以通过返回值判断是否发送成功,当然判断的过程在STM32端实现即可。
具体发送数据的代码如下:
/********************************************************
函数功能:将对机器人的左右轮子控制速度,打包发送给下位机
入口参数:机器人线速度、角速度
出口参数:无
作者:K.Fire
日期:2022.02.10
********************************************************/
void SendSTM32(double Left_v, double Right_v, unsigned char CtrlCmd)
{
unsigned char buf[11] = { 0 };//发送数组
int i, length = 4;
leftVelSet.d = Left_v;//mm/s
rightVelSet.d = Right_v;
// 设置消息头
for (i = 0; i < 2; i++)
buf[i] = header[i]; //buf[0] buf[1]
//设置控制位
buf[2] = CtrlCmd; //buf[2]
//设置数据长度
buf[3] = length; //buf[3]
// 设置机器人左右轮速度
for (i = 0; i < 2; i++)
{
buf[i + 4] = leftVelSet.data[i]; //buf[4] buf[5]
buf[i + 6] = rightVelSet.data[i]; //buf[6] buf[7]
}
// 设置校验值、消息尾
buf[4 + length] = getCrc8(buf, 3 + length);//buf[8]
buf[5 + length] = ender[0]; //buf[9]
buf[6 + length] = ender[1]; //buf[10]
// 通过串口下发数据
sp.write(buf, 11);
}
(5)循环冗余校验
这没啥说的,具体内容看计算机组成原理吧。
/********************************************************
函数功能:获得8位循环冗余校验值
入口参数:数组地址、长度
出口参数:校验值
********************************************************/
unsigned char getCrc8(unsigned char* ptr, unsigned short len)
{
unsigned char crc;
unsigned char i;
crc = 0;
while (len--)
{
crc ^= *ptr++;
for (i = 0; i < 8; i++)
{
if (crc & 0x01)
crc = (crc >> 1) ^ 0x8C;
else
crc >>= 1;
}
}
return crc;
}
二、Boost::asio库通讯
boost::asio库也是用来串口通讯的,这个库好像是C++库提供的,不需要自己再进行安装。
因为官方对于这个库的解释比较冗长,我也没仔细看,是通过看别人的实例程序一知半解,分享给大家参考。
1.创建相关对象
using namespace std;
using namespace boost::asio;
//串口相关对象
boost::asio::io_service iosev;
boost::asio::serial_port sp(iosev, "/dev/ttyUSB0");
boost::system::error_code err;
2.串口初始化
void serialInit()
{
sp.set_option(serial_port::baud_rate(115200));
sp.set_option(serial_port::flow_control(serial_port::flow_control::none));
sp.set_option(serial_port::parity(serial_port::parity::none));
sp.set_option(serial_port::stop_bits(serial_port::stop_bits::one));
sp.set_option(serial_port::character_size(8));
}
3.数据发送
void writeSpeed(double Left_v, double Right_v,unsigned char ctrlFlag)
{
unsigned char buf[11] = {0};//
int i, length = 0;
leftVelSet.d = Left_v;//mm/s
rightVelSet.d = Right_v;
// 设置消息头
for(i = 0; i < 2; i++)
buf[i] = header[i]; //buf[0] buf[1]
// 设置机器人左右轮速度
length = 5;
buf[2] = length; //buf[2]
for(i = 0; i < 2; i++)
{
buf[i + 3] = leftVelSet.data[i]; //buf[3] buf[4]
buf[i + 5] = rightVelSet.data[i]; //buf[5] buf[6]
}
// 预留控制指令
buf[3 + length - 1] = ctrlFlag; //buf[7]
// 设置校验值、消息尾
buf[3 + length] = getCrc8(buf, 3 + length);//buf[8]
buf[3 + length + 1] = ender[0]; //buf[9]
buf[3 + length + 2] = ender[1]; //buf[10]
// 通过串口下发数据
boost::asio::write(sp, boost::asio::buffer(buf));
}
其实和serial库差不多,前面的数据处理都是一样的,都是按照我们自己定义的协议进行的,只是发送的函数定义不一样而已,这里write函数时传入了串口对象和buf数组。
4.接收数据
bool readSpeed(double &Left_v,double &Right_v,double &Angle,unsigned char &ctrlFlag)
{
char i, length = 0;
unsigned char checkSum;
unsigned char buf[150]={0};
//=========================================================
//此段代码可以读数据的结尾,进而来进行读取数据的头部
try
{
boost::asio::streambuf response;
boost::asio::read_until(sp, response, "\r\n",err);
copy(istream_iterator<unsigned char>(istream(&response)>>noskipws),
istream_iterator<unsigned char>(),
buf);
}
catch(boost::system::system_error &err)
{
ROS_INFO("read_until error");
}
//=========================================================
// 检查信息头
if (buf[0]!= header[0] || buf[1] != header[1]) //buf[0] buf[1]
{
ROS_ERROR("Received message header error!");
return false;
}
// 数据长度
length = buf[2]; //buf[2]
// 检查信息校验值
checkSum = getCrc8(buf, 3 + length); //buf[10] 计算得出
if (checkSum != buf[3 + length]) //buf[10] 串口接收
{
ROS_ERROR("Received data check sum error!");
return false;
}
// 读取速度值
for(i = 0; i < 2; i++)
{
leftVelNow.data[i] = buf[i + 3]; //buf[3] buf[4]
rightVelNow.data[i] = buf[i + 5]; //buf[5] buf[6]
angleNow.data[i] = buf[i + 7]; //buf[7] buf[8]
}
// 读取控制标志位
ctrlFlag = buf[9];
Left_v =leftVelNow.d;
Right_v =rightVelNow.d;
Angle =angleNow.d;
return true;
}
这里的接收应该是用的read_until函数,但下面一顿操作我懒得查了,估计是数据结构的问题,应该是把数据接收到了迭代器,然后又转换成buf数组了吧。
循环冗余检验和上面是一样的,就不再重复写了。
三、ROS节点
ros节点中我是准备了几个测试数据,进行收发。
#include "ros/ros.h"
#include "std_msgs/String.h"
#include "MbotRosSerial.h"
//test send value
double testSend1=5555.0;
double testSend2=2222.0;
unsigned char testSend3=0x07;
//test receive value
double testRece1=0.0;
double testRece2=0.0;
double testRece3=0.0;
unsigned char testRece4=0x00;
int main(int argc,char **argv)
{
ros::init(argc,argv,"serial_test");//节点初始化
ros::NodeHandle n;//创建节点句柄
ros::Rate loop_rate(10);
//串口初始化
Serial_Init();
while(ros::ok())
{
//发送数据
SendSTM32(testSend1,testSend2,testSend3);
//接收数据
ReceiveSTM32(testRece1,testRece2,testRece3,testRece4);
//打印数据
ROS_INFO("Receive Data is: %f,%f,%f,%d\n",testRece1,testRece2,testRece3,testRece4);
ros::spinOnce();
loop_rate.sleep();
}
return 0;
}
前面就是一些头文件的包含和编写ros节点需要的固定格式,在while循环里就是简单的进行了数据的收发,然后对cmakelist文件进行修改。
主要在这几个地方进行了修改
cmake_minimum_required(VERSION 3.0.2)
project(mbot)
find_package(catkin REQUIRED COMPONENTS
geometry_msgs
roscpp
rospy
serial
)
catkin_package(
INCLUDE_DIRS include
# LIBRARIES mbot
# CATKIN_DEPENDS geometry_msgs roscpp rospy serial
# DEPENDS system_lib
)
###########
## Build ##
###########
## Specify additional locations of header files
## Your package locations should be listed before other locations
include_directories(
include
${catkin_INCLUDE_DIRS}
)
add_executable(serial_test src/serial_test.cpp
src/MbotRosSerial.cpp)
add_dependencies(serial_test ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
target_link_libraries(serial_test ${catkin_LIBRARIES})
总结
使用serial进行串口通讯的测试程序在此,需要boost::asio程序可以私信或者评论获取。