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 load2. 设备权限与调试工具
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. 数据发送与定时任务
实现定时发送任务的两种方式:
- 使用ROS2定时器:
// 在构造函数中 send_timer_ = create_wall_timer( std::chrono::milliseconds(1000), [this]() { std::string message = "PING"; sendData(std::vector<uint8_t>(message.begin(), message.end())); } );- 直接发送数据:
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. 错误处理与调试技巧
串口通信中常见的错误及解决方法:
权限拒绝:
- 确认用户已加入dialout组
- 检查设备文件权限:
ls -l /dev/ttyUSB0
设备未找到:
- 确认设备已连接:
dmesg | grep tty - 尝试不同的设备名:/dev/ttyUSB0, /dev/ttyUSB1等
- 确认设备已连接:
数据乱码:
- 确认双方波特率一致
- 检查数据位、停止位、校验位设置
数据接收不完整:
- 增加接收缓冲区大小
- 实现简单的数据帧解析逻辑
调试技巧:
// 在回调函数中添加详细的调试信息 RCLCPP_DEBUG(this->get_logger(), "接收缓冲区大小: %zu", receive_buffer_.size()); // 使用rqt_console查看详细日志 ros2 run rqt_console rqt_console8. 性能优化与高级功能
缓冲区管理:
// 在类定义中 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; }测试流程:
- 使用cutecom验证串口设备正常工作
- 启动ROS2节点:
ros2 run serial_example robust_serial_node - 观察日志输出:
ros2 topic echo /rosout - 使用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); }对于需要高可靠性的应用,建议实现以下机制:
- 心跳检测:定期发送心跳包,监测连接状态
- 超时重传:未收到响应时自动重发
- 数据校验:使用CRC或校验和确保数据完整性
- 连接恢复:检测到异常时自动重新初始化串口
