首先Ubuntu上面建立一个功能包pkg ,包里面写一个cpp文件,然后在这个cpp文件里面建立一个node,然后使用这个node去获取单片机从串口传过来的数据。
平时都是使用串口调试助手来收发数据的,好像ros2里面有一个专门搞这个事情的东西叫做
serial库。学一下这个库就可以让ros2跟单片机通信了。我也不知道这个东西叫不叫驱动,反正能通讯就可以了
为了开发过程的方便,最好是实现确定一个Ubuntu和stm32通讯的通讯协议----其实就是通讯规则,比如我给单片机给Ubuntu发个1表示什么意思,发个2表示什么意思,然后Ubuntu给stm32发个1表示什么意思,2表示什么意思。之所以要提前写出来这个协议,最大的好处是,如果这个过程是2个人一起开发的,一个人开发stm32,一个人开个Ubuntu,就可以同时进行-----依据这个事先写好的协议。对于一个人开发也是有好处的,因为写出来这个协议,接口确定了,就不用总是去推理这里该怎么样,那样可能会比较混乱。
如果ros2没有serial库,那么需要自己学习《Ubuntu系统的串口通信编程》
后面是一个人写的文章,我觉得很写得好
古月
机器人社区“古月居”创始人,《ROS机器人开发实践》作者
上段时间学弟总是问我ROS如何与单片机进行通信,这块也是做车的一大难点之一,但是网上的教程却很少,这次正好趁着这个机会写一篇博客记录一下,不过这段时间是真的忙,各种比赛和各种事情。
ROS与单片机通信通常是使用UART通信,即串口通信。
一、UART通信简介
在UART通信中,每个数据位被传输为一个帧(Frame),每个帧由一个起始位(Start Bit)、一个或多个数据位(Data Bits)、一个可选的校验位(Parity Bit)和一个或多个停止位(Stop Bit)组成。
起始位通常是逻辑0,停止位通常是逻辑1。数据位的数量取决于通信双方事先约定的协议,校验位通常用于检查数据传输的正确性。如果使用校验位,通信双方必须在发送和接收端都采用相同的校验方式。
UART通信的速度是由波特率(Baud Rate)来定义的,波特率表示每秒钟传输的比特数。波特率越高,数据传输速度越快,但同时也需要更高的传输带宽和更可靠的信号质量。
在实际应用中,波特率通常是由发送和接收方事先约定好的,并在通信开始前进行设置。
具体的通信过程就不再这里具体描述了。
由于这里我用的就是STM32单片机,所以下面我就以STM32系列单片机为例了,其他单片机也是同理。
二、硬件连接
STM32芯片先连接到一个TTL电平转换芯片,再由这个电平转换芯片通过usb线连接ROS主控。
我这里的连接情况如下:
这里因为该单片机含有电平转换芯片,所以直接使用USB线连接就可以了。
连接成功之后在终端输入ll /dev/ttyUSB*来查看设备,如果显示下面信息说明设备成功被识别了。
这里我们使用ros中serial库方便我们的通信,首先安装一下serial库
sudo apt install ros-noetic-serial
然后新建一个名为uart_ws的工作空间,在工作空间下新建一个uart功能包,在该功能包下新建一个uart.c和uart.h,目录结构如下:
先在uart.h文件中引用serial库
#include <serial/serial.h>
然后新建一个uart类,在类的定义中,声明一个 serial 类的实例
serial::Serial Stm32_Serial; //声明串口对象
再在类中声明两个私有结构体变量,用来存储接收和要发送的数据
RECEIVE_DATA Receive_Data; //串口接收数据结构体
SEND_DATA Send_Data; //串口发送数据结构体
再在类中声明两个定时函数,用来定时发送和接受
void controlLoopCB_send(const ros::TimerEvent&);//定时发送
void controlLoopCB_receive(const ros::TimerEvent&);//定时接收
在构造函数中设置串口名、波特率以及收发频率。这里由于是测试,就直接拿/dev/ttyUSB0了,实际使用中是串口名是很容易变的,如何把串口重命名再固定下来我将会在下篇博客介绍一下。
private_nh.param("usart_port_name", usart_port_name, "/dev/ttyUSB0"); //串口名
private_nh.param ("serial_baud_rate", serial_baud_rate, 115200); //和下位机通信波特率115200,与单片机一致
private_nh.param("controller_freq", controller_freq, 10); //设置收发频率
然后初始化串口配置,再打开串口
try
{
//Attempts to initialize and open the serial port //尝试初始化与开启串口
Stm32_Serial.setPort(usart_port_name); //选择要开启的串口号
Stm32_Serial.setBaudrate(serial_baud_rate); //设置波特率
serial::Timeout _time = serial::Timeout::simpleTimeout(1000); //超时等待
Stm32_Serial.setTimeout(_time);
Stm32_Serial.open(); //开启串口
}
catch (serial::IOException& e)
{
ROS_ERROR_STREAM("car_robot can not open serial port!"); //如果开启串口失败,打印错误信息
}
判断串口是否被打开
if(Stm32_Serial.isOpen())
{
ROS_INFO_STREAM("car_robot serial port opened"); //Serial port opened successfully //串口开启成功提示
}
串口接受函数,第一个变量是要接受的数据存放的数组,第二个变量是要接受的长度
Stm32_Serial.read(Receive_Data,sizeof(Receive_Data));
执行下面串口发送函数进行数据发送,第一个变量是要发送的数组,第二个变量是每次要发送的长度
try
{
Stm32_Serial.write(Send_Data.tx,sizeof (Send_Data.tx)); //通过串口向下位机发送数据
}
catch (serial::IOException& e)
{
ROS_ERROR_STREAM("Unable to send data through serial port"); //如果发送数据失败,打印错误信息
}
四、通信报文格式
在ROS与STM32通信时,要事先约定一个通信的数据包格式,这样通信双方才可以把收到的信息提取出来。
我这里测试用的格式如下,最终实现单片机直接把收到的消息返回给上位机端进行输出:
串口发送
串口接收
以串口接受为例,首先定义一个标志位判断接收到的数据是否与上述定义相同,读七个字节的数据到数组中,然后判断前两位是否与事先定义的包头相同.
如果相同则视为接受正确,然后将标志位置1,数据位赋值给相应变量,如果不则再读取一字节数据扔掉,将标志位置0,然后等待下次接收再读取7字节数据进行判断,重复上述步骤直到判断相同。具体实现如下:
void Uart::controlLoopCB_receive(const ros::TimerEvent&)
{
int Serial_RxFlag = 0; //标志位
uint8_t buffer[1];
int len=Stm32_Serial.read(Receive_Data.receive,7); //获取长度
if(Receive_Data.receive[0]==0xFF && Receive_Data.receive[1]==0xFE)
{
Serial_RxFlag=1;
}
else
{
len=Stm32_Serial.read(buffer,1);
Serial_RxFlag=0;
}
if(Serial_RxFlag==1 && len==7)
{
for(int i = 0; i<2 ;i++)
{
rd1.receive[i] = Receive_Data.receive[i+2];
rd2.receive[i] = Receive_Data.receive[i+4];
}
ROS_INFO("%d,%d,%d",rd1.d,rd2.d,Receive_Data.receive[6]);
Serial_RxFlag = 0;
}
}
这里接受和发送都是7字节的数组,从图中可以看到电机的高电平数是一个两字节的数,而它存放在每个元素都是一字节的数组中时会被拆开存放,所以要将其取出时,需要把两个字节赋给一个变量。
常见的方法有移位、联合体和c语言中的memcpy函数,这里测试用的是联合体,所以我先简要介绍一下原理,下篇博客中将会说明memcpy函数用法,因为联合体在变量多时会变得十分不易读。
联合体
联合体与结构体类似都是不同类型元素的集合,只不过结构体的每一个成员都拥有自己独立的存储空间,而联合体的成员是共用同一块内存空间的。
也就是说修改一个成员,另一个成员相对应的值也会被覆盖。联合体占用的字节数是成员中最大的那个
union receive_data
{
short d;
unsigned char receive[2];
}rd1,rd2;
对于上面用于接受的联合体,占用两个字节,拿其中左电机rd1变量举例,将单片机发送的左电机数据对应的赋值给rd1,这样就相当于rd1.d的值作了相应的修改。
rd1.receive[0] = Receive_Data.receive[2];
rd1.receive[1] = Receive_Data.receive[3];
对于发送的联合体也是同理,定义的数组长度为要发送的结构体所占字节数。
union Send_Cmd{
struct cmd cmd0;
unsigned char data[8];
};
和校验
校验位采用的比较简单的和校验,实现过程是把要发送的数据拆开,然后做和,然后单片机将收到的数据做对比是否相同。
unsigned char Uart::check_uint(uint16_t data)
{
union num_trans_uint16 num;
num.num_int16 = data;
return num.num_char[0] + num.num_char[1];
}
最后配置一下CMakeLists.txt,我这里配置情况如下。
编译过后执行rosrun uart uart进行测试。
最终打印结果如下:
这里我成功的输出了我发送的数据,说明通信是成功的。
如果报错串口不能打开,就输入下面给予一次串口读写权限
sudo chmod 777 /dev/ttyUSB0
最后附本次测试全部代码:
uart.h文件
#ifndef _UART_H_
#define _UART_H_
#define SEND_DATA_Num 18
const unsigned char header[2] = { 0xFF, 0xFE };
struct RECEIVE_DATA
{
unsigned char receive[7];
};
struct SEND_DATA
{
uint8_t tx[SEND_DATA_Num];
};
union receive_data
{
short d;
unsigned char receive[2];
}rd1,rd2;
struct cmd {
unsigned char null;
unsigned char H;
uint16_t Servo_PWM1;
uint16_t Servo_PWM2;
unsigned char CS;
unsigned char T;
};
union Send_Cmd{
struct cmd cmd0;
unsigned char data[8];
};
union num_trans_uint16{
unsigned char num_char[2];
uint16_t num_int16;
};
class Uart
{
private:
ros::NodeHandle private_nh; //节点句柄
std::string usart_port_name;
int serial_baud_rate;
int controller_freq;
serial::Serial Stm32_Serial;
RECEIVE_DATA Receive_Data;
SEND_DATA Send_Data;
ros::Timer timer1,timer2; //定时器
union Send_Cmd send_cmd;
public:
Uart();
unsigned char uart_send_cmd(uint16_t Servo_PWM1,uint16_t Servo_PWM2);
unsigned char check_uint(uint16_t data);
void controlLoopCB_send(const ros::TimerEvent&); //定时器1回调函数,向单片机发送数据
void controlLoopCB_receive(const ros::TimerEvent&); //定时器2回调函数,接收单片机发送过来的数据
};
#endif
uart.c文件
#include
#include
#include "ros/ros.h"
#include "../include/uart.h"
Uart::Uart()
{
ros::NodeHandle private_nh("~");
private_nh.param("usart_port_name", usart_port_name, "/dev/ttyUSB0"); //固定串口号
private_nh.param ("serial_baud_rate", serial_baud_rate, 115200); //和下位机通信波特率115200
private_nh.param("controller_freq", controller_freq, 10);
timer1 = private_nh.createTimer(ros::Duration((1.0)/controller_freq), &Uart::controlLoopCB_send, this); // Duration(0.05) -> 20Hz
timer2 = private_nh.createTimer(ros::Duration((1.0)/controller_freq), &Uart::controlLoopCB_receive, this); // Duration(0.05) -> 20Hz
try
{
//尝试初始化与开启串口
Stm32_Serial.setPort(usart_port_name); //选择要开启的串口号
Stm32_Serial.setBaudrate(serial_baud_rate); //设置波特率
serial::Timeout _time = serial::Timeout::simpleTimeout(1000); //超时等待
Stm32_Serial.setTimeout(_time);
Stm32_Serial.open(); //开启串口
if(Stm32_Serial.isOpen())
{
ROS_INFO_STREAM("car_robot serial port opened"); //串口开启成功提示
}
}
catch (serial::IOException& e)
{
ROS_ERROR_STREAM("car_robot can not open serial port!"); //如果开启串口失败,打印错误信息
}
}
void Uart::controlLoopCB_send(const ros::TimerEvent&)
{
// Send_Data.tx[0] = header[0];
// Send_Data.tx[1] = header[1];
// double t = 2;
// leftVelSet.d = t;
// rightVelSet.d = t+1.1;
// for(int i=0;i<8;i++)
// {
// Send_Data.tx[i+2] = leftVelSet.data[i];
// Send_Data.tx[i+10] = rightVelSet.data[i];
// }
uart_send_cmd(1500,1500);
}
unsigned char Uart::check_uint(uint16_t data)
{
union num_trans_uint16 num;
num.num_int16 = data;
return num.num_char[0] + num.num_char[1];
}
unsigned char Uart::uart_send_cmd(uint16_t Servo_PWM1,uint16_t Servo_PWM2)
{
send_cmd.cmd0.H = 0xFF;
send_cmd.cmd0.Servo_PWM1 = Servo_PWM1;
send_cmd.cmd0.Servo_PWM2 = Servo_PWM2;
send_cmd.cmd0.CS = check_uint(Servo_PWM1) + check_uint(Servo_PWM2);
send_cmd.cmd0.T = 0xFE;
try
{
Stm32_Serial.write(send_cmd.data,sizeof(send_cmd.data)); //通过串口向下位机发送数据
}
catch (serial::IOException& e)
{
ROS_ERROR_STREAM("Unable to send data through serial port"); //如果发送数据失败,打印错误信息
}
return send_cmd.cmd0.CS;
}
void Uart::controlLoopCB_receive(const ros::TimerEvent&)
{
uint8_t Serial_RxPacket[18];
int Serial_RxFlag = 0;
uint8_t reading[5],buffer[1];
int len=Stm32_Serial.read(Receive_Data.receive,7); //获取长度
if(Receive_Data.receive[0]==0xFF && Receive_Data.receive[1]==0xFE)
{
Serial_RxFlag=1;
}
else
{
len=Stm32_Serial.read(buffer,1);
Serial_RxFlag=0;
}
if(Serial_RxFlag==1 && len==7)
{
for(int i = 0; i<2 ;i++)
{
rd1.receive[i] = Receive_Data.receive[i+2];
rd2.receive[i] = Receive_Data.receive[i+4];
}
ROS_INFO("%d,%d,%d",rd1.d,rd2.d,Receive_Data.receive[6]);
Serial_RxFlag = 0;
}
}
int main(int argc, char *argv[])
{
ros::init(argc,argv,"send");
Uart Uart1;
ros::AsyncSpinner spinner(0);
spinner.start();
ros::waitForShutdown();
return 0;
}
发布于 2023-05-19 15:13・IP 属地湖北