C++使用serial串口通信 + ROS2示例IMU串口驱动

这篇具有很好参考价值的文章主要介绍了C++使用serial串口通信 + ROS2示例IMU串口驱动。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。


串行接口 (Serial Interface)简称串口(通常指COM接口),是采用串行通信方式的扩展接口,是指数据一位一位地顺序传送,串口通信就要解析这一位一位数据。这里使用的是亚博智能的10轴IMU模块为例介绍C++使用serial串口通信,此IMU模块为UART通信,它是一异步通讯:不需要时钟信号进行数据同步,它们直接在数据信号中穿插一些同步用的信号位,或者把主体数据进行打包,以数据帧(串口:起始位 数据 校验位(可以没有) 停止位)的格式传输数据,某些通讯中还需要双方约定数据的传输速率(波特率),以便更好地同步。例如:
c++ serial,c++,开发语言,linux,imu,串口通信

  • 波特率是一个衡量通信速度的参数,它表示每秒钟传送的 bit 的个数;
  • 数据位是衡量通信中实际数据位的参数,当计算机发送一个信息包,标准的值是 8 位(也可以是其他,比如5,6,7);
  • 停止位用于表示单个包的最后一位,典型的值为 1,1.5和 2 位,停止位不仅表示传输的结束,并且提供计算机校正时钟同步的机会;
  • 奇偶校验位是串口通信中一种简单的检错方式,有四种检错方式——偶、奇、高和低,也可以没有校验位。

一、通信协议

通讯协议分为物理层和协议层。物理层规定通讯系统中具有机械、电子功能部分的特性,确保原始数据在物理媒体的传输(通俗一点就是硬件部分)。协议层主要规定通讯逻辑,统一收发双方的数据打包、解包标准(软件)。使用的IMU模块(自己修改波特率406800)其通信协议(部分)如下:
c++ serial,c++,开发语言,linux,imu,串口通信
数据是按照16进制方式发送的,不是ASCII码。每个数据帧(不同类型)包含11位(8bit),其中0x55为帧头,第二位TYPE为数据类型,中间8个为数据位,最后一个为检验位。写代码时注意以下三个方面:

  • (1)通过0x55帧头和第二位TYPE数据类型识别数据帧;
  • (2)每一个数据分低字节和高字节(千万注意高低顺序)依次传送,二者组合成一个有符号的short类型的数据。例如数据DATA1,其中DATA1L为低字节,DATA1H为高字节。转换方法如下:假设DATA1为实际的数据,DATA1H为其高字节部分,DATA1L为其低字节部分,那么:DATA1=(short)((short)DATA1H<<8|DATA1L)。这里一定要注意DATA1H需要先强制转换为一个有符号的short类型的数据以后再移位,并且DATA1的数据类型也是有符号的short类型,这样才能表示出负数;
  • (3)通过检验和来检验数据SUMCRC=0x55+TYPE+DATA1L+DATA1H+DATA2L+DATA2H+DATA3L+DATA3H+DATA4L+DATA4H
    SUMCRC为char型,取校验和的低8位。

c++ serial,c++,开发语言,linux,imu,串口通信
c++ serial,c++,开发语言,linux,imu,串口通信
c++ serial,c++,开发语言,linux,imu,串口通信

二、串口调试工具

sudo apt-get install cutecom

选择串口,设置波特率
c++ serial,c++,开发语言,linux,imu,串口通信打开串口,可以查看每一位数据的十六进制和字符格式的数据输出
c++ serial,c++,开发语言,linux,imu,串口通信

三、serial库的使用

3.1 安装serial

对于ROS1来说,可以直接安装ROS版

sudo apt-get install ros-<ros版本>-serial

Cmake配置:

find_package(catkin REQUIRED COMPONENTS
  serial
)

catkin_package(
  CATKIN_DEPENDS serial
)

target_link_libraries(myserial  ${catkin_LIBRARIES})

但是由于ROS2没有再封装串口库serial,因此需要手动安装serial:

git clone https://github.com/ZhaoXiangBox/serial
cd serial && mkdir build
cmake .. && make
sudo make install

Cmake配置:

set(CMAKE_INSTALL_RPATH /usr/local/lib)
find_package(serial REQUIRED)

ament_target_dependencies(myserial "serial")

3.2 serial的使用

给连接的串口打开权限

sudo chmod 777 /dev/ttyUSB*

(1)首先需要添加serial的头文件

#include "serial/serial.h"

(2)实例化一个对象,之后的操作都是通过对此设置而进行的

serial::Serial serialPort;

(3)串口进行初始化

serialPort.setPort("/dev/ttyUSB0");//选择要开启的串口号
serialPort.setBaudrate(9600);//设置波特率
serial::Timeout _time =serial::Timeout::simpleTimeout(2000);//超时等待
serialPort.setTimeout(_time);

(4)开启串口

serialPort.open();//开启串口

(5)判断一下是否打开了

if(serialPort.isOpen())
{
	ROS_INFO("serial port is open");
}
else 
{
	ROS_ERROR("serial port error");
}

(6)发送数据

先设置一个数组,注意数组类型一定是uint8_t类型的

uint8_t  senddata[要发送数据的长度]={1};

定义好数组之后我们就可以给单片机发送数据了

serialPort.write(senddata,sizeof(senddata));//两个参数,第一个参数是要发送的数据地址,第二个数据是要发送数据的长度

(7)接收下位机的数据

先创建一个数组,数组类型也是uint8_t类型的

uint_8 receivedata[要接受数据的长度]={1};

接收数据的函数

serialPort.read(receivedata,sizeof(receivedata));

3.3 绑定端口

一旦出现断电或者重新插拔设备情况,代码就会出现找不到设备或者找错设备的错误,因为端口序号分配是随机的,因此我们需要绑定端口,确保每次插拔端端口一样,不需要修改代码
首先通过插拔两个端口,我们可以lsusb查看端口信息:
c++ serial,c++,开发语言,linux,imu,串口通信
可以看到IMU端口为Bus 001 Device 007: ID 10c4:ea60 Silicon Labs CP210x UART Bridge
然后创建imu_usb.rules(随便命名)文件,填写以下内容:

KERNEL=="ttyUSB*", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", MODE:="0777", SYMLINK+="imu_usb

然后:

sudo cp imu_usb.rules /etc/udev/rules.d
service udev reload
service udev restart

重新插拔设备,输入以下指令检测绑定端口是否成功

ll /dev/imu_usb

c++ serial,c++,开发语言,linux,imu,串口通信

四、编写IMU ROS2串口驱动

ROS2节点为例,首先通过串口调试工具可以看到一帧数据:
c++ serial,c++,开发语言,linux,imu,串口通信

通过上面的通信协议以及串口使用方法,我编写了以下代码来实现IMU的ROS2串口驱动:

#include <iostream>
#include <chrono>
#include <cmath>
#include <serial/serial.h>
#include <sensor_msgs/msg/imu.hpp>
#include <tf2/LinearMath/Quaternion.h>
#include "rclcpp/rclcpp.hpp"

#define BAUDRATE 460800

// std::atomic_bool 是 C++ 标准库中的原子布尔类型。它是一种特殊的数据类型,用于在多线程环境中进行原子操作。
// 原子操作是指不可中断的操作,要么完全执行,要么完全不执行。std::atomic_bool 类型提供了原子性的读取和写入操作,以确保多个线程对该变量的访问不会导致竞争条件或数据不一致的问题。
std::atomic_bool imu_thread_running;
std::atomic_bool imu_data_ready;
std::vector<uint8_t> buff;
std::vector<int16_t> acceleration(4, 0);
std::vector<int16_t> angularVelocity(4, 0);
std::vector<int16_t> magnetometer(4, 0);
std::vector<int16_t> angle_degree(4, 0);

class IMUDriverNode : public rclcpp::Node
{
public:
    IMUDriverNode(const char *nodeName) : Node(nodeName)
    {
        // 获取串口
        _port_name = this->declare_parameter("~port_name", "/dev/ttyUSB0");
        // 发布IMU数据
        publisher_ = this->create_publisher<sensor_msgs::msg::Imu>("/imu_raw", 10);
        // IMU驱动线程
        imu_thread_ = std::thread(&IMUDriverNode::imuThread, this, _port_name);
    }

    void joinIMUThread()
    {
        imu_thread_.join();
    }

private:
    // IMU驱动线程
    void imuThread(const std::string &port_name)
    {
        // 1 创建串口对象
        serial::Serial imu_serial;
        // 串口初始化
        try
        {
            imu_serial.setPort(port_name);
            imu_serial.setBaudrate(BAUDRATE);
            serial::Timeout timeout = serial::Timeout::simpleTimeout(500);
            imu_serial.setTimeout(timeout);
            imu_serial.open();
            RCLCPP_INFO(this->get_logger(), "\033[32mSerial port opened successfully...\033[0m");
        }
        catch (const serial::IOException &e)
        {
            RCLCPP_ERROR(this->get_logger(), "Failed to open the IMU serial port.");
            return;
        }

        // Clear the buffer
        imu_serial.flush();

        // 2 循环读取串口数据
        while (rclcpp::ok() && imu_thread_running.load())
        {
            if (imu_serial.available())
            {
                uint8_t data;
                imu_serial.read(&data, 1);
                // 一位一位地添加到队列
                buff.push_back(data);

                // 当大于11个数据位时解析数据,四种数据的第一位均为0x55
                if (buff.size() >= 11 && buff[0] == 0x55)
                {
                    // 获取11位数据
                    std::vector<uint8_t> data_buff(buff.begin(), buff.begin() + 11);
                    // 获取中间数字位
                    std::vector<uint8_t> data(buff.begin() + 2, buff.begin() + 10);
                    // 获取完一帧的标志位
                    bool angle_flag = false;

                    // 加速度数据
                    if (data_buff[1] == 0x51)
                    {
                        // 检查检验和
                        if (checkSum(data_buff))
                            // 获取16位数据
                            acceleration = hexToShort(data);
                        else
                            RCLCPP_WARN(this->get_logger(), "0x51 Check failure.");
                    }
                    // 下面的都类似
                    else if (data_buff[1] == 0x52)
                    {
                        if (checkSum(data_buff))
                            angularVelocity = hexToShort(data);
                        else
                            RCLCPP_WARN(this->get_logger(), "0x52 Check failure.");
                    }
                    else if (data_buff[1] == 0x53)
                    {
                        if (checkSum(data_buff))
                        {
                            angle_degree = hexToShort(data);
                            angle_flag = true;
                        }
                        else
                            RCLCPP_WARN(this->get_logger(), "0x53 Check failure.");
                    }
                    // 实际上没用到
                    else if (data_buff[1] == 0x54)
                    {
                        if (checkSum(data_buff))
                            magnetometer = hexToShort(data);
                        else
                            RCLCPP_WARN(this->get_logger(), "0x54 Check failure.");
                    }

                    buff.clear();

                    // 已经获取角度数据(顺序第三个),可以发布了
                    if (angle_flag)
                    {
                        // 可以输出数据的标志位
                        imu_data_ready.store(true);
                    }
                }
                else if (buff[0] != 0x55)
                {
                    buff.clear();
                }
            }

            if (imu_data_ready.load())
            {
                sensor_msgs::msg::Imu msg;
                msg.header.stamp = this->now();
                msg.header.frame_id = "base_link";

                // 将16数据转化位double
                // Accelerations should be in m/s^2 (not in g's), and rotational velocity should be in rad/sec
                // 加速度X=((AxH<<8)|AxL)/32768*16g(g为重力加速度,可取9.8m/s2)
                msg.linear_acceleration.x = static_cast<double>(acceleration[0]) / 32768.0 * 16 * 9.8;
                msg.linear_acceleration.y = static_cast<double>(acceleration[1]) / 32768.0 * 16 * 9.8;
                msg.linear_acceleration.z = static_cast<double>(acceleration[2]) / 32768.0 * 16 * 9.8;

                // 角速度X=((WxH<<8)|WxL)/32768*2000 °/s
                // 转化为弧度X=((WxH<<8)|WxL)/32768*2000*PI/180 rad/s
                msg.angular_velocity.x = static_cast<double>(angularVelocity[0]) / 32768.0 * 2000 * M_PI / 180;
                msg.angular_velocity.y = static_cast<double>(angularVelocity[1]) / 32768.0 * 2000 * M_PI / 180;
                msg.angular_velocity.z = static_cast<double>(angularVelocity[2]) / 32768.0 * 2000 * M_PI / 180;

                // 滚转角X=((RollH<<8)|RollL)/32768*180 (°)
                // 转化为弧度X=((RollH<<8)|RollL)/32768*180*PI/180=((RollH<<8)|RollL)/32768*PI (rad)
                double roll = static_cast<double>(angle_degree[0]) / 32768.0 * M_PI;
                double pitch = static_cast<double>(angle_degree[1]) / 32768.0 * M_PI;
                double yaw = static_cast<double>(angle_degree[2]) / 32768.0 * M_PI;
                tf2::Quaternion quat;
                quat.setRPY(roll, pitch, yaw);
                // Assign quaternion to msg.orientation
                msg.orientation.x = quat.getX();
                msg.orientation.y = quat.getY();
                msg.orientation.z = quat.getZ();
                msg.orientation.w = quat.getW();

                publisher_->publish(msg);
                imu_data_ready.store(false);
            }
        }
        imu_serial.close();
    }

    // 检验和:四个数据的检验和计算方式是一样的,所以统一写了
    bool checkSum(const std::vector<uint8_t> &data_buff)
    {
        uint8_t sum = 0;
        for (size_t i = 0; i < data_buff.size() - 1; i++)
        {
            sum += data_buff[i];
        }

        // 计算结果是否与输出一致
        return sum == data_buff[data_buff.size() - 1];
    }

    // 解析数据
    std::vector<int16_t> hexToShort(const std::vector<uint8_t> &hex_data)
    {
        std::vector<int16_t> short_data(4, 0);
        for (size_t i = 0; i < hex_data.size(); i += 2)
        {
            // 将两个连续的字节(低位hex_data[i] 和 高位hex_data[i+1])组合为一个 int16_t 类型的数值:千万注意高低顺序,仔细看通讯协议
            // 高位hex_data[i+1]需要先强制转换为一个有符号的short类型的数据以后再移位
            short high = static_cast<int16_t>(hex_data[i + 1]);
            // 左移运算符 << 将 high 的二进制表示向左移动 8 位。这样做是因为 int16_t 类型占用 2 个字节,而我们希望将 high 的数据放置在最高的 8 位上。
            // |: 按位或运算符 | 将经过左移的 high 数据和 hex_data[i] 数据进行按位或操作,将它们组合为一个 16 位的数值
            short_data[i / 2] = static_cast<int16_t>((high << 8) | hex_data[i]);
        }

        return short_data;
    }

    // 与上面那个函数是等价的,直接采用C++函数解析
    std::vector<int16_t> hexToShort1(const std::vector<uint8_t> &hex_data)
    {
        char rawData[] = {hex_data[0], hex_data[1], hex_data[2], hex_data[3], hex_data[4], hex_data[5], hex_data[6], hex_data[7]};
        // 创建一个存储解析后整数的数组
        short result[4];
        // 使用memcpy将字节数组解析为整数数组
        memcpy(result, rawData, sizeof(result));

        std::vector<int16_t> short_data = {result[0], result[1], result[2], result[3]};
        return short_data;
    }

    std::string _port_name;
    rclcpp::Publisher<sensor_msgs::msg::Imu>::SharedPtr publisher_;
    std::thread imu_thread_;
};

int main(int argc, char **argv)
{
    rclcpp::init(argc, argv);
    auto imu_node = std::make_shared<IMUDriverNode>("imu_driver_node");
    imu_thread_running.store(true);

    rclcpp::spin(imu_node);
    imu_thread_running.store(false);

    imu_node->joinIMUThread();

    rclcpp::shutdown();
    return 0;
}

cmake配置如下:

cmake_minimum_required(VERSION 3.8)
project(imu_driver)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
find_package(tf2 REQUIRED)
find_package(sensor_msgs REQUIRED)
set(CMAKE_INSTALL_RPATH /usr/local/lib)
find_package(serial REQUIRED)

add_executable(imu_driver_exe src/imu.cpp)
target_include_directories(imu_driver_exe PUBLIC
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:include>)
target_compile_features(imu_driver_exe PUBLIC c_std_99 cxx_std_17)  # Require C99 and C++17
ament_target_dependencies(
  imu_driver_exe
  "rclcpp"
  "std_msgs"
  "sensor_msgs"
  "tf2"
  "serial"
)

install(TARGETS imu_driver_exe DESTINATION lib/${PROJECT_NAME})

install(DIRECTORY launch DESTINATION share/${PROJECT_NAME})

if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  ament_lint_auto_find_test_dependencies()
endif()

ament_package()

package.xml配置如下:

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>gnss_imu_sim</name>
  <version>0.0.0</version>
  <description>TODO: Package description</description>
  <maintainer email="zard@todo.todo">zard</maintainer>
  <license>TODO: License declaration</license>

  <buildtool_depend>ament_cmake</buildtool_depend>

  <depend>rclcpp</depend>
  <depend>std_msgs</depend>
  <depend>tf2</depend>
  <depend>sensor_msgs</depend>

  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>

  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

启动文件:

from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package='gnss_imu_sim',
            executable='imu_driver_exe',
            name='imu_driver_exe',
            parameters=[{"~port_name":"/dev/imu_usb"}],
            output='both'
        )
    ])

订阅话题查看:
c++ serial,c++,开发语言,linux,imu,串口通信
安装IMU可视化工具,ROS2有直接对IMU姿态的可视化工具:

sudo apt install ros-galactic-imu-tools

添加话题,并输入正确的坐标系,即可看到IMU坐标系姿态变化文章来源地址https://www.toymoban.com/news/detail-823401.html

到了这里,关于C++使用serial串口通信 + ROS2示例IMU串口驱动的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • ros2 机器人imu传感器 加速度计 陀螺仪精度和数据填充单位换算

    起因,imu解算出了加速度 角速度,但原始数据是没有单位的,只是在一个精度范围的值,要使用这些数据,就需要把这些没有单位的数据换算成带单位的数据,下面解说一下换算原理。 imu读取数据代码参考上期的博客: ros2 c++实现JY_95T IMU解算三轴 加速度 角速度 欧拉角 磁力

    2024年02月13日
    浏览(55)
  • ros2与web通信实例

    最近需要进行ros2与web端进行通信操作,目标是ros2发送的消息web端能够显示在界面,并且前端能够发布数据,最终实例如下: 然而网上查的的资料如古月居的: 利用Websocket实现ROS与Web的交互 https://www.guyuehome.com/5386 包括ros官网上以及目前网上绝大部分资料都是ros1与web交互的

    2024年02月21日
    浏览(39)
  • ROS2从入门到精通1-2:详解ROS2服务通信机制与自定义服务

    本专栏旨在通过对ROS2的系统学习,掌握ROS2底层基本分布式原理,并具有机器人建模和应用ROS2进行实际项目的开发和调试的工程能力。 🚀详情:《ROS2从入门到精通》 服务 是 ROS 图中节点之间的另一种通信方法。服务基于 服务器-客户端 模型,不同于话题的 发布者-订阅者

    2024年04月09日
    浏览(37)
  • 速腾RS16的ROS2驱动

    ROS2-foxy ubuntu20.04 速腾RS16

    2024年02月10日
    浏览(39)
  • 利用Web Serial API实现Vue与单片机串口通信

            Web Serial API 是一项 Web 技术,用于在浏览器中访问串行端口设备(如 Arduino、传感器等)并与之通信。它提供了一组 JavaScript 接口,使得 Web 应用程序可以通过 USB 串行端口连接到硬件设备,并进行数据发送和接收操作。         浏览器版本:Google Chrome 版本

    2024年02月14日
    浏览(42)
  • 微信小程序和ros2进行通信

      ROS2做为一款优秀的机器人操作系统软件,其搭载了丰富的机器人平台,是目前机器人领域应用最多的软件。微信做为一个大型社交软件,应用非常广泛,其中的小程序直接通过扫二维码进行加载,使用起来非常方便快捷。为了让手机端能够对机器人进行操作,于是通过微

    2024年02月10日
    浏览(55)
  • 【C#】【串口通信(Serial Port)】建议串口调试WinForm桌面应用实例——已实现功能<存在未知BUG>

    1、界面组件  2、界面实现代码 界面代码   3、逻辑代码 逻辑代码   4、效果展示   5、错误提醒   参考网址:C# 实现串口通信 - 编程教程 (17bigdata.com)

    2024年02月08日
    浏览(45)
  • ESP32中micro-ROS与ROS2通信(点亮esp32指示灯)

    micro-ROS,是基于ROS2进行优化的一套轻量级ROS系统,它提供了完全部署的ROS 2生态系统的大多数吸引人的工具和功能,并具有入式和低资源设备的卓越能力,可以运行在MCU硬件平台。 传统上,即使机器人包含许多ROS,ROS仍停留在微控制器边界。它们通常通过串行协议与旧版RO

    2024年02月01日
    浏览(41)
  • 【手把手做ROS2机器人系统开发五】使用C++实现编写简单的服务器和客户端

    目录 使用C++实现编写简单的服务器和客户端 一、程序编写 1、创建软件包  2、编译软件包 3、软件配置 4、服务器程序编写 5、客户端程序编写 6、软件包设置 7、设置编译选项 二、程序测试 1、编译程序 2、开启节点测试运行 3、执行效果展示          上一讲我们讲解了如

    2024年02月10日
    浏览(40)
  • Ubuntu20.04安装Nvidia显卡驱动、CUDA11.3、CUDNN、TensorRT、Anaconda、ROS/ROS2

    打开终端,输入指令:  选择 【5】 更换系统源,后面还有一个要输入的选项,选择 【0】 退出,就会自动换源。 这一步最痛心了家人们,网上的教程太多了,我总是想着离线安装,每次安装都无法开机,要不就卡在锁屏界面,要不就黑屏,要不就卡在snaped界面,重装系统装

    2024年01月17日
    浏览(74)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包