前言

上段时间学弟总是问我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<std::string>("usart_port_name",  usart_port_name,  "/dev/ttyUSB0"); //串口名
private_nh.param<int>        ("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 <iostream>
#include <serial/serial.h>
#include "ros/ros.h"
#include "../include/uart.h"


Uart::Uart()
{
    ros::NodeHandle private_nh("~");
    
    private_nh.param<std::string>("usart_port_name",  usart_port_name,  "/dev/ttyUSB0"); //固定串口号
    private_nh.param<int>        ("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;
}