ROS下的串口通讯
有参考价值的博客:
http://stevenshi.me/2017/07/18/control-mobile-base-by-serial/
http://stevenshi.me/2017/05/17/linux-serial/
http://stevenshi.me/2017/05/17/ros-serial/
https://blog.csdn.net/m0_37598942/article/details/80713512
https://blog.csdn.net/Tansir94/article/details/81357612
https://github.com/wjwwood/serial
https://blog.csdn.net/weixin_43795921/article/details/85219249
https://blog.csdn.net/m0_37598942/article/details/80713512
https://blog.csdn.net/u012247418/article/details/82960889 libusb
详解stm32串口到底如何与ROS实现信息交互
设计思路:
有的朋友使用全向移动底座,有的使用两轮差动或四轮驱动实现移动底座;为了保持代码通用性,运动学解析这一部分没有特别的说明,移动底座只接收 x 与 y 两个方向的线速度以及一个绕 z 轴的角速度;针对不同的移动底座,还需要设计不同的运动学解析函数,以便于将线速度与角速度转变成电机运动指令,从而控制电机运动。ROS 部分实现一个节点,该节点订阅 cmd_vel 话题,并将该话题转变成 x y 两个方向的线速度以及一个绕 z 轴的角速度,通过串口发送到移动底座,即给STM32;另外该节点还需要发布导航需要的 odom 消息,这个消息需要移动底座提供,通过STM32的串口发送机器人的位置、速度、偏航角等信息,经过特殊的变换之后再发布。
STM32
STM32使用最多的便是STM32F103,为了便于调试,选择STM32的串口1作为调试串口,打印一些调试信息;使用STM32的串口3作为与 ROS 通信的端口;调试串口波特率设置为9600,8位数据1个停止位,偶校验;串口3设置为115200波特率,8位数据1个停止位无校验模式;当然这个波特率可以自行设置。数据的发送和接收可以是固定长度或不固定长度,为了适应不同的长度字节,建议使用DMA模式,并开启串口的空闲中断,否则很难判断一帧数据包的完全接收。当然也可以开启一个定时器,在一定时间内判断是否超时来间接的判断一帧数据是否完成。最好的方式就是开启串口空闲中断,将数据的发送和接收都由DMA负责,这样不占用CPU资源。
调试端口:
PA9—>USART1_TX—->DMA1_Channel4
PA10—>USART1_RX—>DMA1_Channel5
与ROS通信端口:
PB10—>USART3_TX—->DMA1_Channel2
PB11—->USART3_RX—>DMA1_Channel3
数据接收
使用DMA接收数据,配置如下:
DMA_Cmd(USARTz_Rx_DMA_Channe,DISABLE);//配置之前先停止
DMA_DeInit(USARTz_Rx_DMA_Channe);//恢复缺省配置
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)USARTz_DR_Base;// 设置串口接收数据寄存器
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)USARTzRxBuffer;//接收缓存地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;//设置外设为数据源
DMA_InitStructure.DMA_BufferSize = USARTzTxBufferSize;//需要接收的字节数
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址不做增加调整
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存缓冲地址增加调整
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // 外设数据宽度一个字节
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // 内存数据宽度一个字节
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //单次传输模式
DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh; // 高优先级
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //关闭内存到内存的DMA模式
DMA_Init(USARTz_Rx_DMA_Channe, &DMA_InitStructure);// 写入配置
DMA_ClearFlag(USARTz_Rx_DMA_FLAG); //清除DMA所有标志
DMA_Cmd(USARTz_Rx_DMA_Channe, ENABLE); // 开启DMA接收通道
开启串口的空闲中断,并在空闲中断中读取串口数据:
/* Enable USART3 Receive interrupts */
USART_ITConfig(USART3, USART_IT_IDLE, ENABLE);
void USART3_IRQHandler(void)//串口中断
{
if(USART_GetITStatus(USART3, USART_IT_IDLE) != RESET)
{
com_x_usart_dma_read();
USART_ReceiveData( USART3 );
}
}
这样才能接收变长的数据帧。
数据发送
数据发送时只要将数据发送的首地址以及数据宽度给 DMA 即可,因为一开始固定了发送缓存地址,所以只需要指定发送缓存长度即可:
void com_x_usart_dma_start_tx(uint8_t size)
{
USARTz_Tx_DMA_Channe->CNDTR = (uint16_t)size; //重新赋值,指定发送缓存长度
DMA_Cmd(USARTz_Tx_DMA_Channe, ENABLE); // 开启DMA发送
}
调试功能
关于 stm32 的 printf 功能实现可以参考站内文章 STM32如何在DMA模式下实现printf
这里建议使用 DMA 方式实现 printf,因为底座还有很多任务要处理包括运动学解析以及电机控制等。
//将偏航角转换成四元数才能发布
odom_quat = tf::createQuaternionMsgFromYaw(yaw.fvalue);
//发布坐标变换父子坐标系
odom_trans.header.frame_id = “odom”;
odom_trans.child_frame_id = “base_link”;
//发布tf坐标变换
odom_broadcaster.sendTransform(odom_trans);
//获取当前时间
current_time = ros::Time::now();
//载入里程计时间戳
odom.header.stamp = current_time;
//里程计位置数据
odom.pose.pose.position.x = posx.fvalue;
//载入线速度和角速度
odom.twist.twist.linear.x = vx.fvalue;
odom.twist.twist.angular.z = va.fvalue;
//发布里程计消息
read_pub.publish(odom);
ROS_INFO(“publish odometry”);
××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××
××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××
问题解决:
浮点数与16进制的相互转换
强制转换法
浮点转换成4字节16进制:
void float2bytes(float p,unsigned char *bytes)
{
unsigned char *pchar = (unsigned char*)&p;
for(int i=0;i < sizeof(float);i++)
{
*bytes = *pchar;
pchar++;
bytes++;
}
}
16进制4字节转换成浮点:
float bytes2float(unsigned char *bytes)
{
return *((float*)bytes);//强制转换
}
联合体法
typedef union{
unsigned char cvalue[4];
float fvalue;
}float_union;
定义联合体变量:
float_union trans_data;
此时如果给变量trans_data赋值一个浮点数:
trans_data.fvalue = 10.05;
那么直接调用cvalue即可获取浮点数10.05的4字节16进制数据:
for(int i=0;i<4;i++)
printf(" 0x%02x",trans_data.cvalue[i]);
#include <stdio.h>
/*
要点提示:
1. float和unsigned long具有相同的数据结构长度
2. union据类型里的数据存放在相同的物理空间
*/
typedef union
{
float fdata;
unsigned long ldata;
}FloatLongType;
/*
将浮点数f转化为4个字节数据存放在byte[4]中
*/
void Float_to_Byte(float f,unsigned char byte[])
{
FloatLongType fl;
fl.fdata=f;
byte[0]=(unsigned char)fl.ldata;
byte[1]=(unsigned char)(fl.ldata>>8);
byte[2]=(unsigned char)(fl.ldata>>16);
byte[3]=(unsigned char)(fl.ldata>>24);
}
/*
将4个字节数据byte[4]转化为浮点数存放在*f中
*/
void Byte_to_Float(float *f,unsigned char byte[])
{
FloatLongType fl;
fl.ldata=0;
fl.ldata=byte[3];
fl.ldata=(fl.ldata<<8)|byte[2];
fl.ldata=(fl.ldata<<8)|byte[1];
fl.ldata=(fl.ldata<<8)|byte[0];
*f=fl.fdata;
}
/*
测试函数
*/
int main()
{
float f=123456.789f;
unsigned char byte[4]={0};
printf("float data=%f\n",f);
Float_to_Byte(f,byte);
f=789.123456f;
printf("changed float data=%f\n",f);
Byte_to_Float(&f,byte);
printf("float data=%f\n",f);
return 0;
}
仅做参考:
首先,这里要引入一个名称为serial的包,这个包的安装命令为:
$ sudo apt-get install ros-<版本号>-serial
serial包的介绍:http://wiki.ros.org/serial
接下来,创建一个自己的包,借助serial这个包来编写串口通信的代码。
1、创建一个包,依赖roscpp和serial两个包
$ catkin_create_pkg serial_port roscpp serial
进入下载的软件包的位置
roscd serial
1
若是安装成功会看到:
$:/opt/ros/kinetic/share/serial
1
新建工作空间级程序包:
cd
mkdir -p serialPort/src
cd serialPort
catkin_make
source devel/setup.bash
cd src
catkin_create_pkg serialPort std_msgs roscpp serial
cd serialPort/src
#include <ros/ros.h>
#include <serial/serial.h>
#include <iostream>
int main(int argc, char** argv)
{
ros::init(argc, argv, "serial_port");
//创建句柄(虽然后面没用到这个句柄,但如果不创建,运行时进程会出错)
ros::NodeHandle n;
//创建一个serial类
serial::Serial sp;
//创建timeout
serial::Timeout to = serial::Timeout::simpleTimeout(100);
//设置要打开的串口名称
sp.setPort("/dev/ttyUSB0");
//设置串口通信的波特率
sp.setBaudrate(115200);
//串口设置timeout
sp.setTimeout(to);
try
{
//打开串口
sp.open();
}
catch(serial::IOException& e)
{
ROS_ERROR_STREAM("Unable to open port.");
return -1;
}
//判断串口是否打开成功
if(sp.isOpen())
{
ROS_INFO_STREAM("/dev/ttyUSB0 is opened.");
}
else
{
return -1;
}
ros::Rate loop_rate(500);
while(ros::ok())
{
//获取缓冲区内的字节数
size_t n = sp.available();
if(n!=0)
{
uint8_t buffer[1024];
//读出数据
n = sp.read(buffer, n);
for(int i=0; i<n; i++)
{
//16进制的方式打印到屏幕
std::cout << std::hex << (buffer[i] & 0xff) << " ";
}
std::cout << std::endl;
//把数据发送回去
sp.write(buffer, n);
}
loop_rate.sleep();
}
//关闭串口
sp.close();
return 0;
代码中&0xFF的目的:
byte类型的数字要&0xff再赋值给int类型,其本质原因就是想保持二进制补码的一致性。
当byte要转化为int的时候,高的24位必然会补1,这样,其二进制补码其实已经不一致了,&0xff可以将高的24位置为0,低8位保持原样。这样做的目的就是为了保证二进制数据的一致性。
实现过程中的问题:
如果出现如下错误,则是因为权限不够引起的
terminate called after throwing an instance of 'boost::exception_detail::clone_impl<boost::exception_detail::error_info_injector<boost::system::system_error> >'
what(): open: Permission denied
首先查看电脑链接的串口信息(名称),据此更改程序:
dmesg | grep ttyS*
打开/dev/ttyUSB0 权限不够解决办法:
非永久性:
sudo chmod 666 /dev/ttyUSB0
永久性解决
1、将你的用户名加入dialout用户组,因为默认情况下只有root用户才有权操作tty设备
sudo adduser USER_NAME dialout
2、登出系统,再重新登入,就可以了
3、可以通过下面的指令查看你的用户名是否已经加入dialout用户组
cat /etc/group
错误:
terminate called after throwing an instance of ‘boost::exception_detail::clone_impl<boost::exception_detail::error_info_injectorboost::lock_error >’
what(): boost: mutex lock failed in pthread_mutex_lock: Invalid argument
解决办法:
sudo apt-get dist-upgrade
ubuntu串口助手:
下载:sudo apt-get install cutecom
打开:sudo cutecom
程序写完后CmakeLists文件的修改:
find_package(catkin REQUIRED COMPONENTS
roscpp
cv_bridge
std_msgs
serial
)
find_package(Boost REQUIRED COMPONENTS system)
add_executable(serialtest
src/serialtest.cpp)
下面的可不添加
add_dependencies(${PROJECT_NAME}_node ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
target_link_libraries(${PROJECT_NAME}_node
${catkin_LIBRARIES}
示例代码:
#include <stdio.h>
#include <string.h>
#include <ros/ros.h>
#include <cv_bridge/cv_bridge.h>
#include <ros/package.h>
#include <geometry_msgs/PoseStamped.h>
#include <nav_msgs/Odometry.h> //数据格式
#include <serial/serial.h> //ros串口
#include <std_msgs/String.h>
#include <std_msgs/Empty.h>
#include <fstream>
#include <iostream>
using namespace std;
serial::Serial ser; //定义一个串口对象
ros::Subscriber position_sub; //订阅位姿节点
typedef union{ //定义一个联合体,用于double数据与16进制的转换
unsigned char cvalue[4];
double fvalue;
}double_union;
static uint8_t s_buffer[15]; 分配静态存储空间
void position_world_callback(const nav_msgs::Odometry& odom_msg)
{
geometry_msgs::PoseStamped pose_msg;
pose_msg.pose = odom_msg.pose.pose;
cout<<"positon-> x: "<<pose_msg.pose.position.x<<" y : "<<pose_msg.pose.position.y<<" z : "<<pose_msg.pose.position.z<<endl;;
ROS_INFO_STREAM("----------------------------------------------");
double_union position_x,position_y,position_z;
memset(s_buffer,0,sizeof(s_buffer)); //内存空间初始化为0
position_x.fvalue=pose_msg.pose.position.x;
position_y.fvalue=pose_msg.pose.position.y;
position_z.fvalue=pose_msg.pose.position.z;
//数据打包
s_buffer[0] = 0xff; //数据的帧头
s_buffer[1] = 0xfe;
s_buffer[2] = position_x.cvalue[0];
s_buffer[3] = position_x.cvalue[1];
s_buffer[4] = position_x.cvalue[2];
s_buffer[5] = position_x.cvalue[3];
s_buffer[6] = position_y.cvalue[0];
s_buffer[7] = position_y.cvalue[1];
s_buffer[8] = position_y.cvalue[2];
s_buffer[9] = position_y.cvalue[3];
s_buffer[10] = position_z.cvalue[0];
s_buffer[11] = position_z.cvalue[1];
s_buffer[12] = position_z.cvalue[2];
s_buffer[13] = position_z.cvalue[3];
//CRC 校验和从有效数据开始取异或即可
s_buffer[14] = s_buffer[2]^s_buffer[3]^s_buffer[4]^s_buffer[5]^s_buffer[6]^s_buffer[7]^\
s_buffer[8]^s_buffer[9]^s_buffer[10]^s_buffer[11]^s_buffer[12]^s_buffer[13];
ser.write(s_buffer,15);
ROS_INFO("data send successful");
}
int main(int argc, char** argv)
{
ros::init(argc, argv, "serial_node"); //ROS串口节点名称
ros::NodeHandle my_node;
position_sub = my_node.subscribe("/camera/odom/sample", 1, position_world_callback);
//读取串口名称
string filename = "/home/lmf/WORK_SPACE/catkin_ws/src/realsense-ros/realsense2_camera/parameter.txt";
ifstream fin( filename.c_str() );
if (!fin)
{
cout<<"parameter file does not exist."<<endl;
return 0;
}
string serialPort;
getline( fin, serialPort );
fin.close();
cout<<serialPort<<endl;
try
{
//设置串口属性,并打开串口
ser.setPort(serialPort); //设置对应串口的名称
ser.setBaudrate(115200); //波特率115200
serial::Timeout to = serial::Timeout::simpleTimeout(1000);
ser.setTimeout(to);
ser.open();
}
catch (serial::IOException& e)
{
ROS_ERROR_STREAM("Unable to open port ");
return -1;
}
if(ser.isOpen())
{
ROS_INFO_STREAM("Serial Port opened");
}
else
{
return -1;
}
//指定循环的速率
ros::Rate loop_rate(50);
while (ros::ok())
{
ros::spinOnce();
loop_rate.sleep();
}
ser.close();
return 0;
}
更多推荐
所有评论(0)