当前位置: 首页 > news >正文

ROS2 Humble/Jazzy下,用Serial_Driver搞定串口通信的保姆级教程(附完整代码)

ROS2 Humble/Jazzy串口通信实战:从设备权限到异步收发的完整指南

当你第一次在ROS2中尝试串口通信时,可能会遇到各种问题——设备权限不足、波特率配置错误、数据接收异常...这些问题往往让初学者感到挫败。本文将带你从零开始,在ROS2 Humble或Jazzy环境下,使用Serial_Driver库构建一个稳定可靠的串口通信节点。

1. 环境准备与基础配置

在开始编码之前,我们需要确保开发环境已经正确设置。ROS2的Serial_Driver库依赖于asio-cmake-module,这是一个基于Boost.Asio的跨平台异步I/O库。

首先安装必要的依赖包:

# 对于Humble版本 sudo apt install ros-humble-asio-cmake-module ros-humble-serial-driver # 对于Jazzy版本 sudo apt install ros-jazzy-asio-cmake-module ros-jazzy-serial-driver

串口设备在Linux系统中通常以/dev/ttyUSB*/dev/ttyACM*的形式出现。连接设备后,使用以下命令查看:

ls /dev/ttyUSB*

常见问题:如果找不到设备,可能是驱动未安装。对于常见的CH340芯片,需要安装驱动:

sudo apt install build-essential sudo apt install linux-headers-$(uname -r) git clone https://github.com/juliagoda/CH341SER.git cd CH341SER make sudo make load

2. 设备权限与调试工具

Linux系统默认会限制普通用户对串口设备的访问权限。我们需要将当前用户添加到dialout组:

sudo usermod -aG dialout $USER newgrp dialout

验证权限是否生效:

groups | grep dialout

安装串口调试工具cutecom,用于手动测试设备:

sudo apt-get install cutecom

使用cutecom时,注意以下参数设置:

参数推荐值说明
波特率9600/115200需与设备匹配
数据位8标准配置
停止位1常见设置
校验位None无校验
流控None无硬件流控

3. 创建ROS2功能包与节点结构

创建一个新的ROS2功能包,包含serial_driver依赖:

ros2 pkg create serial_example --build-type ament_cmake --dependencies rclcpp serial_driver

典型的串口节点类结构如下:

#include "rclcpp/rclcpp.hpp" #include "serial_driver/serial_driver.hpp" class SerialNode : public rclcpp::Node { public: SerialNode(); private: void initSerialPort(); void asyncReceive(); void sendData(const std::vector<uint8_t>& data); std::shared_ptr<drivers::serial_driver::SerialDriver> serial_driver_; std::shared_ptr<drivers::common::IoContext> io_context_; };

关键组件说明:

  • IoContext: 提供异步I/O操作的事件循环
  • SerialDriver: 串口通信的核心类
  • SerialPortConfig: 封装串口配置参数

4. 串口配置与初始化

串口配置是通信稳定的关键。SerialPortConfig构造函数接受四个主要参数:

drivers::serial_driver::SerialPortConfig config( 115200, // 波特率 drivers::serial_driver::FlowControl::NONE, // 流控 drivers::serial_driver::Parity::NONE, // 校验位 drivers::serial_driver::StopBits::ONE // 停止位 );

波特率选择指南

  • 9600: 低速设备,长距离传输
  • 115200: 常用速率,适合大多数场景
  • 460800/921600: 高速传输,短距离可靠

初始化串口设备的完整流程:

try { io_context_ = std::make_shared<drivers::common::IoContext>(1); serial_driver_ = std::make_shared<drivers::serial_driver::SerialDriver>(*io_context_); // 设备名通常为/dev/ttyUSB0或/dev/ttyACM0 serial_driver_->init_port("/dev/ttyUSB0", config); serial_driver_->port()->open(); RCLCPP_INFO(this->get_logger(), "串口初始化成功"); } catch (const std::exception &ex) { RCLCPP_ERROR(this->get_logger(), "串口初始化失败: %s", ex.what()); throw; }

5. 异步接收与数据处理

Serial_Driver的核心优势在于其异步接收机制,不会阻塞主线程。实现异步接收的关键是设置回调函数:

void SerialNode::asyncReceive() { auto port = serial_driver_->port(); port->async_receive([this](const std::vector<uint8_t> &data, const size_t &size) { if (size > 0) { processReceivedData(data, size); } // 继续监听新数据 asyncReceive(); }); }

数据处理函数示例:

void SerialNode::processReceivedData(const std::vector<uint8_t>& data, size_t size) { std::string message(data.begin(), data.begin() + size); // 简单的数据解析示例 if (message.find("ERROR") != std::string::npos) { RCLCPP_ERROR(this->get_logger(), "设备报告错误: %s", message.c_str()); } else { RCLCPP_INFO(this->get_logger(), "收到数据: %s", message.c_str()); } // 可以在这里触发响应逻辑 if (message == "REQUEST_DATA") { sendData({0x01, 0x02, 0x03}); } }

6. 数据发送与定时任务

实现定时发送任务的两种方式:

  1. 使用ROS2定时器:
// 在构造函数中 send_timer_ = create_wall_timer( std::chrono::milliseconds(1000), [this]() { std::string message = "PING"; sendData(std::vector<uint8_t>(message.begin(), message.end())); } );
  1. 直接发送数据:
void SerialNode::sendData(const std::vector<uint8_t>& data) { try { auto port = serial_driver_->port(); size_t sent = port->send(data); RCLCPP_DEBUG(this->get_logger(), "发送 %zu 字节数据", sent); } catch (const std::exception &ex) { RCLCPP_ERROR(this->get_logger(), "发送失败: %s", ex.what()); } }

7. 错误处理与调试技巧

串口通信中常见的错误及解决方法:

  1. 权限拒绝

    • 确认用户已加入dialout组
    • 检查设备文件权限:ls -l /dev/ttyUSB0
  2. 设备未找到

    • 确认设备已连接:dmesg | grep tty
    • 尝试不同的设备名:/dev/ttyUSB0, /dev/ttyUSB1等
  3. 数据乱码

    • 确认双方波特率一致
    • 检查数据位、停止位、校验位设置
  4. 数据接收不完整

    • 增加接收缓冲区大小
    • 实现简单的数据帧解析逻辑

调试技巧:

// 在回调函数中添加详细的调试信息 RCLCPP_DEBUG(this->get_logger(), "接收缓冲区大小: %zu", receive_buffer_.size()); // 使用rqt_console查看详细日志 ros2 run rqt_console rqt_console

8. 性能优化与高级功能

缓冲区管理

// 在类定义中 std::vector<uint8_t> receive_buffer_; size_t buffer_position_ = 0; // 在回调函数中 void SerialNode::processReceivedData(const std::vector<uint8_t>& data, size_t size) { // 确保缓冲区足够大 if (buffer_position_ + size > receive_buffer_.capacity()) { receive_buffer_.reserve(receive_buffer_.capacity() * 2); } // 拷贝数据到缓冲区 std::copy(data.begin(), data.end(), receive_buffer_.begin() + buffer_position_); buffer_position_ += size; // 处理完整帧 processCompleteFrames(); }

多串口管理

std::map<std::string, std::shared_ptr<drivers::serial_driver::SerialDriver>> serial_devices_; void SerialNode::addSerialPort(const std::string& name, const std::string& device) { auto config = drivers::serial_driver::SerialPortConfig(115200, /* 其他参数 */); auto driver = std::make_shared<drivers::serial_driver::SerialDriver>(*io_context_); driver->init_port(device, config); driver->port()->open(); serial_devices_[name] = driver; }

自定义消息协议

struct SerialMessage { uint8_t header[2] = {0xAA, 0xBB}; uint8_t command; uint16_t length; std::vector<uint8_t> payload; uint8_t checksum; std::vector<uint8_t> serialize() const { std::vector<uint8_t> data; data.insert(data.end(), header, header + 2); data.push_back(command); // 继续添加其他字段... return data; } };

9. 完整代码示例与集成测试

一个完整的串口节点实现:

#include "rclcpp/rclcpp.hpp" #include "serial_driver/serial_driver.hpp" #include <vector> #include <string> class RobustSerialNode : public rclcpp::Node { public: RobustSerialNode() : Node("robust_serial_node") { // 参数配置 declare_parameter<std::string>("device", "/dev/ttyUSB0"); declare_parameter<int>("baud_rate", 115200); // 初始化串口 initSerialPort(); // 启动异步接收 asyncReceive(); // 定时发送测试数据 test_timer_ = create_wall_timer( std::chrono::seconds(1), [this]() { sendTestData(); } ); } private: void initSerialPort() { try { auto device = get_parameter("device").as_string(); auto baud_rate = get_parameter("baud_rate").as_int(); io_context_ = std::make_shared<drivers::common::IoContext>(2); serial_driver_ = std::make_shared<drivers::serial_driver::SerialDriver>(*io_context_); drivers::serial_driver::SerialPortConfig config( baud_rate, drivers::serial_driver::FlowControl::NONE, drivers::serial_driver::Parity::NONE, drivers::serial_driver::StopBits::ONE); serial_driver_->init_port(device, config); serial_driver_->port()->open(); RCLCPP_INFO(get_logger(), "串口 %s 初始化成功,波特率: %d", device.c_str(), baud_rate); } catch (const std::exception &ex) { RCLCPP_FATAL(get_logger(), "串口初始化失败: %s", ex.what()); rclcpp::shutdown(); } } void asyncReceive() { auto port = serial_driver_->port(); port->async_receive([this](const std::vector<uint8_t> &data, const size_t &size) { if (size > 0) { processData(data, size); } asyncReceive(); // 继续接收 }); } void processData(const std::vector<uint8_t>& data, size_t size) { std::lock_guard<std::mutex> lock(data_mutex_); receive_buffer_.insert(receive_buffer_.end(), data.begin(), data.begin() + size); // 简单协议处理:查找换行符作为消息结束符 auto it = std::find(receive_buffer_.begin(), receive_buffer_.end(), '\n'); while (it != receive_buffer_.end()) { std::string message(receive_buffer_.begin(), it); RCLCPP_INFO(get_logger(), "收到完整消息: %s", message.c_str()); // 从缓冲区移除已处理数据 receive_buffer_.erase(receive_buffer_.begin(), it + 1); it = std::find(receive_buffer_.begin(), receive_buffer_.end(), '\n'); } } void sendTestData() { std::string message = "Hello from ROS2 at " + std::to_string(now().seconds()) + "\n"; std::vector<uint8_t> data(message.begin(), message.end()); try { auto sent = serial_driver_->port()->send(data); RCLCPP_DEBUG(get_logger(), "发送 %zu 字节数据", sent); } catch (const std::exception &ex) { RCLCPP_ERROR(get_logger(), "发送失败: %s", ex.what()); } } std::shared_ptr<drivers::serial_driver::SerialDriver> serial_driver_; std::shared_ptr<drivers::common::IoContext> io_context_; rclcpp::TimerBase::SharedPtr test_timer_; std::vector<uint8_t> receive_buffer_; std::mutex data_mutex_; }; int main(int argc, char** argv) { rclcpp::init(argc, argv); auto node = std::make_shared<RobustSerialNode>(); rclcpp::spin(node); rclcpp::shutdown(); return 0; }

测试流程:

  1. 使用cutecom验证串口设备正常工作
  2. 启动ROS2节点:ros2 run serial_example robust_serial_node
  3. 观察日志输出:ros2 topic echo /rosout
  4. 使用rqt_console查看详细调试信息

10. 实际项目中的经验分享

在工业自动化项目中,我们发现串口通信的稳定性至关重要。一个实用的技巧是在节点启动时发送一个测试命令,并等待设备响应,以此验证连接是否正常:

bool SerialNode::checkConnection() { const std::vector<uint8_t> test_cmd = {0x01, 0x00, 0x00, 0x01}; std::promise<bool> response_received; auto port = serial_driver_->port(); port->async_receive([&](const std::vector<uint8_t> &data, const size_t &size) { if (size >= 4 && data[0] == 0x01 && data[3] == 0x01) { response_received.set_value(true); } }); port->send(test_cmd); auto future = response_received.get_future(); return future.wait_for(std::chrono::seconds(1)) == std::future_status::ready; }

另一个常见需求是处理二进制协议。我们可以使用结构体和联合体来简化协议处理:

#pragma pack(push, 1) struct MotorCommand { uint8_t header; uint8_t address; uint16_t speed; uint8_t direction; uint8_t checksum; }; #pragma pack(pop) void SerialNode::sendMotorCommand(uint8_t address, uint16_t speed, bool forward) { MotorCommand cmd; cmd.header = 0xAA; cmd.address = address; cmd.speed = speed; cmd.direction = forward ? 0x01 : 0x00; cmd.checksum = calculateChecksum(reinterpret_cast<uint8_t*>(&cmd), sizeof(cmd)-1); std::vector<uint8_t> data(sizeof(cmd)); memcpy(data.data(), &cmd, sizeof(cmd)); sendData(data); }

对于需要高可靠性的应用,建议实现以下机制:

  1. 心跳检测:定期发送心跳包,监测连接状态
  2. 超时重传:未收到响应时自动重发
  3. 数据校验:使用CRC或校验和确保数据完整性
  4. 连接恢复:检测到异常时自动重新初始化串口
http://www.cnnetsun.cn/news/1589355.html

相关文章:

  • 【ZABBIX】-1 zabbix的简要介绍
  • 城市更新智慧设计:亲测有效的解决方案分享
  • 如何在30分钟内用OpCore-Simplify完成OpenCore EFI自动化配置?
  • 鱼鱼刘怀旧手游|永恒岛高清重置版:4K 焕新归来,重走彩虹青春路
  • AI大模型之RAG(向量库milvus实现)
  • 3种进阶方案:B站音频无损提取与管理全流程指南
  • 如何永久保存微信聊天记录?WeChatMsg完整备份与数据分析方案
  • 电路耦合技术:原理、应用与实战避坑指南
  • Janus-Pro-7B真实效果:会议白板照片→要点提取→纪要初稿生成
  • 每日 AI 研究简报 · 2026-03-30
  • 5个提升游戏体验的技巧:如何通过League-Toolkit实现高效游戏辅助
  • Redis 完整入门指南 | 零基础小白也能轻松掌握的缓存神器
  • Windows APK安装新方案:告别模拟器,实现Android应用原生运行
  • Masa Mods汉化资源包:让中文玩家无障碍体验Minecraft模组生态
  • 从零理解自然数系统:用Python类模拟皮亚诺公理(含加法乘法实现)
  • RK3588开发基础
  • 基因集(模块)活性量化:R语言+Java原生
  • ZEMAX实例解析:施密特—卡塞格林系统的多项式非球面优化与MTF分析
  • 手把手教你配置PUSCH repetition type A跳频:Intra-slot与Inter-slot参数设置详解(含RB位置计算器)
  • Python实战:用SymPy解常微分方程 vs 偏微分方程的5个关键差异
  • Flutter Isolates:多线程编程的艺术
  • PyTorch 3.0静训架构深度拆解(企业级容错+混合精度+梯度压缩三重加固)
  • 为什么APKMirror是安卓用户最安全的应用下载工具?完整指南解析
  • ROS2数据录制实战:用ros2 bag记录小海龟运动轨迹(附常见问题排查)
  • 嵌入式系统内存碎片优化方案与实践
  • crypto-js 测试验证全攻略:从浏览器到自动化的加密功能验证实践
  • Umi-OCR服务化集成方案:构建企业级OCR自动化工作流的技术实现
  • 终极指南:3个维度解锁Cyber Engine Tweaks,重塑赛博朋克2077游戏体验
  • 告别Matrikon模拟器:用C#和Workstation.UaClient从零搭建一个真正的OPC UA客户端
  • PCB邮票孔设计与应用全解析