第 4 节

四、Boost库串口通信:单例化+反馈机制

作者
尚凡兴的头像尚凡兴
协作者
董佳辉的头像董佳辉
发布于 2024/07/18更新于 2024/07/25

串口通信完整源码

Serial_comm.zip

串口通信单例化实现

源码部分

  • serialport.h

/**
* Created by moruofan on 2014-4-20.
* directory info: Serial communication class definition
*/

#ifndef SERIALPORT_H
#define SERIALPORT_H

#include "config.h"
#include "config_serial.h"
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <boost/array.hpp>
#include <memory>
#include <thread>
#include <iostream>

using namespace std;
using namespace boost::asio;
class SerialPort
{
public:
//<<<<<<<<<<<<<<<<<<<<<<<<<< 串口通信的发送 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    static SerialPort& getInstance() {
    static SerialPort instance;
    return instance;
    }
    // SerialPort();
    // ~SerialPort();

    //bool init(string port_name, uint16_t baud_rate);
    void runService();
    bool open();
    void close();
    void write(uint8_t& buf);

    void startAsyncRead();
    void handleRead(const boost::system::error_code& ec, size_t byte_read);
//<<<<<<<<<<<<<<<<<<<<<<<<< 串口通信的数据打包 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    void SendByte(uint8_t date);// 发送一个字节数据  自己写的函数
    void Send(const uint8_t* data, uint8_t len); // 发送定长数据   不需要改动
    void Send_Cmd_Data(uint8_t cmd, const uint8_t* datas, uint8_t len);
//                  -------- 串口通信的数据校验 ----------
    uint16_t CRC16_Check(const uint8_t* data, uint8_t len);// 16位循环冗余校验  不需要改动
//                  -------- 串口通信的数据解包 ----------
    void Data_Analysis(uint8_t cmd, const uint8_t* datas, uint8_t len);// 数据帧解析   自己写
    void Receive(uint8_t bytedata);// 接收数据   不需要改动
//<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 发送方式 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
//此处先忽略
private:
    SerialPort();
    ~SerialPort();

    boost::system::error_code errorCode;
    boost::system::error_code ec;
    io_service io;
    static shared_ptr<serial_port> serialPort;
    string portName;
    uint32_t baudRate;

    static uint8_t receiveData[1024];
};

#endif // SERIALPORT_H
  • serialport.cpp更改部分

SerialPort::SerialPort() :
    portName("/dev/ttyTHS0"),
    baudRate(115200)
{
    memset(receiveData, 0, sizeof(receiveData));
    //当串口通信类进行单例化之后,init函数失效,open函数提前到构造函数中
    open();
}

SerialPort::~SerialPort()
{
    if (serialPort != nullptr)
        serialPort->close();
    serialPort = nullptr;
}

// bool SerialPort::init(string port_name, uint16_t baud_rate)
// {
//     portName = port_name;
//     baudRate = baud_rate;

//     return open();
// }

单例化使用

  1. getInstance 方法:

static SerialPort& getInstance() {
    static SerialPort instance; // 静态局部变量
    return instance;
}
  • getInstance 方法返回 SerialPort 类的唯一实例。

  • 静态局部变量 instance 保证了 SerialPort 类在整个程序生命周期内只有一个实例。首次调用时,instance 被创建,后续调用直接返回该实例。

  1. 私有构造函数和析构函数:

SerialPort();
~SerialPort();
  • 将构造函数和析构函数设为私有,确保类的实例只能通过 getInstance 方法访问,外部不能直接创建或销毁该类的实例。

调用方式

在主程序的任何位置都可以采用此方式获取单例化对象,使用串口通信功能:

SerialPort& port = SerialPort::getInstance();
    uint8_t datas[] = { 0x01 };
    port.Send_Cmd_Data(0x08, datas, 1);

image\.png

【注意】

在使用单例化的时候一定要注意,要保证程序在运行的整个时间线上,任意一个时刻或一段时间(一个流程周期)要保证只能有一个地方调用此单例化对象。

如果出现一个时刻程序多线程的多个位置要使用串口通信对象,则可能出现程序的多个位置争用对象的情况。虽然程序使用互斥锁以及线程间提醒机制,不会导致程序崩溃,但争对象的行为可能导致,线程程序堵塞、信息丢失的可能,程序执行效率低下。

一般情况下,我们所写的代码虽然有面向对象的特征,但实际上嵌入机器人的程序是一个面向过程的程序,只要保证程序是一个流程进行的(包换循环流程),一般都不会导致上述情况。

同时,建议在往后的过程中,上下位机的串口通信使用两个或两个以上的串口,在信息交流上有显著的优点,在与其他高校的交流过程中,大部分高校串口都是采用的两个及以上串口,两个串口一般是足够的。

串口通信反馈机制

接收反馈机制

源码部分

  • serialport.h

class SerialPort
{
public:
//<<<<<<<<<<<<<<<<<<<<<<<<<< 串口通信的发送 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    static SerialPort& getInstance() {
    static SerialPort instance;
    return instance;
    }

    void runService();
    bool open();
    void close();
    void write(uint8_t& buf);

    void startAsyncRead();
    void handleRead(const boost::system::error_code& ec, size_t byte_read);
//<<<<<<<<<<<<<<<<<<<<<<<<< 串口通信的数据打包 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    void SendByte(uint8_t date);// 发送一个字节数据  自己写的函数
    void Send(const uint8_t* data, uint8_t len); // 发送定长数据   不需要改动
    void Send_Cmd_Data(uint8_t cmd, const uint8_t* datas, uint8_t len);
//                  -------- 串口通信的数据校验 ----------
    uint16_t CRC16_Check(const uint8_t* data, uint8_t len);// 16位循环冗余校验  不需要改动
//                  -------- 串口通信的数据解包 ----------
    void Data_Analysis(uint8_t cmd, const uint8_t* datas, uint8_t len);// 数据帧解析   自己写
    void Receive(uint8_t bytedata);// 接收数据   不需要改动
//<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 发送方式 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    void ReceiveFeedback_mechanism(mutex& data0xXX_mutex,
                                    condition_variable& data0xXX_cond,
                                    uint8_t* data0xXX,
                                    bool& isReceived,
                                    bool& data_ready,
                                    uint8_t cmd,
                                    bool is_reset_data=false);
private:
    SerialPort();
    ~SerialPort();

    boost::system::error_code errorCode;
    boost::system::error_code ec;
    io_service io;
    static shared_ptr<serial_port> serialPort;
    string portName;
    uint32_t baudRate;

    static uint8_t receiveData[1024];
};
  • serial port.cpp功能部分

void SerialPort::ReceiveFeedback_mechanism(mutex& data0xXX_mutex,
                                            condition_variable& data0xXX_cond,
                                            uint8_t* data0xXX,
                                            bool& isReceived,
                                            bool& data_ready,
                                            uint8_t cmd,
                                            bool is_reset_data)
{
    while (true)
    {
        {
            std::unique_lock<std::mutex> lock(data0xXX_mutex);
            if (!isReceived)
            {

                data0xXX_cond.wait(lock, [&data_ready,&is_reset_data]
                    {return ((!is_reset_data&&MCU_Reset!=-1)||data_ready); });
                if(!is_reset_data&&MCU_Reset!=-1){
                    data0x00_data_ready = false;//---------------------------------------注意这个地方
                    cout<<"程序复位"<<endl;
                    break;
                }

                this->Send_Cmd_Data(cmd, data0xXX, 1);
                isReceived = true;
            }
            else
            {
                if (!data0xXX_cond.wait_for(lock, std::chrono::milliseconds(50), [&data_ready] { return data_ready; })){
                    data_ready = false;
                    isReceived = false;
                    break;
                }
                else
                    this->Send_Cmd_Data(cmd, data0xXX, 1);
            }
            data_ready = false;
            data0xXX_cond.notify_one();
        }
    }
}

运行流程

接收反馈的流程:当函数开始运行之后,便开始等待指定信息(根据cmd帧和len帧来判断指定信息),函数会锁 定第一次接收到对应信息,并给下位机反馈,使用多线程wait_for()函数在指定时间之内如 果接收到下位机再次发来信息,就再次给下位机反馈,并再次判断指定时间内是否接收到指定 信息;如果指定时间内未接收到指定信息,则判断为下位机没有再发送信息,函数退出。

【注意】:wait_for()函数内要填写延时时间ms单位,这个函数的作用是如果在指定时间内如果满足了要求的条件就不阻塞线程(满足条件立刻跳出,不会等完延时时间)如果不满足条件,超过延时时间之后,就不再阻塞程序。

  1. 设置的延时时间一定要大于下位机的发送延时,否则会出现失效⭐

【使用方式】

  1. 配置文件(写通信信息的锁、通知、格式、数据等变量)一个.h和.cpp文件

image\.png

image\.png

image\.png

  1. 数据解析方式这么写

image\.png

  1. 主线程这样在需要这个功能的时候,调用这两行

【功能测试】

1721385862937.mp4

⭐如上视频为测试接收反馈机制:用串口助手+虚拟串口模仿下位机向程序发送信息

下位机发送信息:0x22 -----> 打包后为:A5 5A 01 00 22 18 AB FF

上位机回复信息:0x01 -----> 打包后为:A5 5A 01 00 01 C1 EA FF

可以看到,程序一开始执行时没有什么反应,当我向程序连续发送0x22打包信息后,程序一直在向下位机(串口助手模仿的)发送打包后的0x01,可以看到基本上下位机每发送一条,上位机就会反馈一条,当下位机不再发送时,上位机也不再反馈。

  • 在测试视频中,我有停止过几次用串口助手的发送,第一次停止后再开启,程序依然会给反馈。原因是,我将wait_for()函数的延时设置为3000ms(为了便于检测,正常情况下最好小于30ms),也就是说当程序反馈信息发送完成后,下位机如果在3000ms内再给上位机发送信息,则程序认定,下位机未接收到反馈,会再次发送反馈信息。

  • 在测试视频中,最后两次停止后再开启,程序不给出反馈。原因是,上位机程序发送反馈信息后,再3000ms内未再接收到下位机信息,则程序认定,下位机已经接收到反馈,不会再次发送反馈信息。

发送反馈机制

源码部分

  • serialport.h

class SerialPort
{
public:
    //<<<<<<<<<<<<<<<<<<<<<<<<<< 串口通信的发送 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    static SerialPort& getInstance() {
        static SerialPort instance;
        return instance;
    }
    // SerialPort();
    // ~SerialPort();

    //bool init(string port_name, uint16_t baud_rate);
    void runService();
    bool open();
    void close();
    void write(uint8_t& buf);

    void startAsyncRead();
    void handleRead(const boost::system::error_code& ec, size_t byte_read);
    //<<<<<<<<<<<<<<<<<<<<<<<<< 串口通信的数据打包 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    void SendByte(uint8_t date);// 发送一个字节数据  自己写的函数
    void Send(const uint8_t* data, uint8_t len); // 发送定长数据   不需要改动
    void Send_Cmd_Data(uint8_t cmd, const uint8_t* datas, uint8_t len);
    //                  -------- 串口通信的数据校验 ----------
    uint16_t CRC16_Check(const uint8_t* data, uint8_t len);// 16位循环冗余校验  不需要改动
    //                  -------- 串口通信的数据解包 ----------
    void Data_Analysis(uint8_t cmd, const uint8_t* datas, uint8_t len);// 数据帧解析   自己写
    void Receive(uint8_t bytedata);// 接收数据   不需要改动
    //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 发送方式 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    void ReceiveFeedback_mechanism(mutex& data0xXX_mutex,
        condition_variable& data0xXX_cond,
        uint8_t* data0xXX,
        bool& isReceived,
        bool& data_ready,
        uint8_t cmd,
        bool is_reset_data = false);
    void SendFeedback_mechanism(mutex& data0xXX_mutex,
                                condition_variable& data0xXX_cond,
                                uint8_t* data0xXX,
                                bool& isReceived,
                                bool& data_ready,
                                uint8_t cmd,
                                uint8_t len);
private:
    SerialPort();
    ~SerialPort();

    boost::system::error_code errorCode;
    boost::system::error_code ec;
    io_service io;
    static shared_ptr<serial_port> serialPort;
    string portName;
    uint32_t baudRate;

    static uint8_t receiveData[1024];
};
  • serialport.cpp功能部分

void SerialPort::SendFeedback_mechanism(mutex& data0xXX_mutex,
    condition_variable& data0xXX_cond,
    uint8_t* data0xXX,
    bool& isReceived,
    bool& data_ready,
    uint8_t cmd,
    uint8_t len)
{
    while (true) {
        std::unique_lock<std::mutex> lock(data0xXX_mutex);
        data0xXX_cond.wait(lock, [&isReceived]
            { return !isReceived; });
        if (!data_ready) {
            this->Send_Cmd_Data(cmd, data0xXX, len);
            // 可以考虑加入一定的延时
            std::this_thread::sleep_for(std::chrono::milliseconds(15));
        }
        else {
            data_ready = false;
            isReceived = false;
            break;
        }
        data0xXX_cond.notify_one();
    }
}

运行流程

**发送反馈的流程:**当函数开始运行之后,程序会先对指定信息的相关变量上锁(根据cmd帧和len帧来判断指定信 息),然后对是否接收到下位机信息的标志位作为wait()函数的判断条件,如果没有接收到下位 机信息就继续执行,向下位机发送指定信息,如果下位机给与反馈,第二个标志位就会在另一 个线程被赋值,从而在下一个函数内循环中因为这个标志位的改变得知下位机给与反馈,然后 退出函数,解锁并通知其他线程;否则就会继续执行下一次的程序内循环。

【注意】

  1. **向下位机的发送一定要设置延时,并且最好要大于5ms,上位机cpu主频远远大于单片机,如果不加延时连续给下位机发送信息,会直接把下位机干爆⭐ **

【使用方式】

  1. 配置文件(写通信信息的锁、通知、格式、数据等变量)一个.h和.cpp文件

image\.png

image\.png

image\.png

  1. 数据解析方式这么写

image\.png

  1. 主线程这样在需要这个功能的时候,调用这两行

【功能测试】

1721391328922.mp4

⭐如上视频为测试发送反馈机制:用串口助手+虚拟串口模仿下位机向程序发送信息

上位机发送信息:0x01 -----> 打包后为:A5 5A 01 00 01 C1 EA FF

下位机反馈信息:0x22 -----> 打包后为:A5 5A 01 00 22 18 AB FF

可以看到,程序一开始执行时因为程序进入发送反馈函数中,于是不断向下位机发送打包好的0x01信息(发送延迟是自己设置的,目前设置的是15ms),当下位机给与反馈的瞬间,上位机接收到反馈,便不再向下位机继续发送消息。


序言

串口通信的全部程序见顶部压缩包,一~四部分是对本年上位机工程串口通信部分代码的讲解:

对于串口通信部分的改进和优化意见:

  1. 串口通信的硬件部分,建议上下位机使用两个串口(多线程代码,在并行部分,可能导致需要同时发送两种以上不同信息)这时单个串口就不够用;

    两个串口也可以设置优先级,比如下位机复位后发送的第一条信息,就可以作为上位机得知下位机复位的条件,这种重要的信息,就可以放在优先级高的串口。

  2. 对于书写串口通信的代码库,如果往后使用ROS去构建整个工程,建议**使用ROS自带的串口通信库****Serial**库,该库的API文档网址:https://docs.ros.org/en/jade/api/serial/html/classserial_1_1Serial.html

去重新封装一个串口通信的实现。基于ROS的节点式通信方式,由于ROS的不同节点间都是并行的, 类似于多线程,所以在ROS上构建串口通信要比写多线程考虑的相对少一些,也更易于使用和维护。

当然即使没有使用ROS也可以尝试使用其他串口通信库,去尝试封装自己需要的串口通信功能。

  1. 战队以前的学长提到,使用串口通信的信息交流速度比较慢,结合电子科技大学的交流分享会,

电科大提到他们使用的串口通信使用的虚拟串口,其本质上是can通信,往后队员可以考虑使用其他 连接方式完成上下位机的通信,提高速率和稳定性。

第五部分不涉及代码,简述一下串口通信的硬件连接部分,和遇到的一些问题 》》

image\.png