1. rosserial_mbed_lib 项目概述

rosserial_mbed_lib 是一个面向 ARM Cortex-M 系统(特别是基于 mbed OS 的开发平台)的 ROS(Robot Operating System)序列化通信库。它并非官方 ROS 组织维护的 rosserial 主干分支,而是由开发者基于原始 rosserial 协议栈进行针对性裁剪、适配与增强的个人分支。其核心目标是为资源受限的嵌入式微控制器(如 NXP LPC1768、ST STM32F4/F7/H7 系列、Renesas RA6M5 等)提供轻量、可靠、可移植的 ROS 节点接入能力,使 MCU 能够作为 ROS 图(ROS Graph)中的第一类公民,直接发布/订阅话题(Topic)、调用服务(Service),并参与参数服务器(Parameter Server)交互。

该库严格遵循 ROS 1 的 rosserial 协议规范(Protocol Version 0.8+),采用帧同步、校验和(CRC-16-CCITT)、消息序列号、超时重传等机制保障串行链路(UART)上的数据完整性与有序性。与通用 PC 端 rosserial_python rosserial_arduino 相比, rosserial_mbed_lib 的关键差异化在于: 深度耦合 mbed OS 的异步 I/O 框架与 RTOS 抽象层 ,摒弃阻塞式 read() / write() 调用,转而采用事件驱动(Event-based)与回调(Callback)模型,天然支持多任务并发、低功耗休眠唤醒、以及与 mbed OS 中其他组件(如 FATFS、EthernetStack、ThreadX 内核)的无缝集成。

在工程实践中,该库常被用于以下典型场景:

  • 移动机器人底盘控制:MCU 实时采集编码器脉冲、IMU 数据、电机电流,并通过 /odom /imu/data /joint_states 等标准话题上报至 ROS 主机;
  • 多传感器融合节点:将温湿度、气压、激光测距、超声波等异构传感器数据统一打包为 sensor_msgs/Imu sensor_msgs/Range 等 ROS 消息格式,经 UART 汇聚至树莓派或 Jetson 边缘计算单元;
  • 执行器闭环控制:接收 /cmd_vel 速度指令,经 PID 运算后驱动电机驱动器,同时将实际 PWM 占空比、故障状态以自定义消息反馈;
  • 诊断与调试接口:实现 diagnostic_updater 兼容的诊断发布器,将 MCU 温度、供电电压、看门狗计数等关键健康指标注入 ROS Diagnostics 可视化系统。

其设计哲学可概括为:“ 最小协议开销 + 最大运行时可控性 + 零动态内存分配 ”。所有消息缓冲区、序列号管理、连接状态机均在编译期静态分配,避免堆内存碎片与 malloc/free 引发的不可预测延迟,满足工业级实时性要求(典型端到端延迟 < 5ms,UART 波特率 ≥ 115200)。

2. 系统架构与核心组件

2.1 整体分层架构

rosserial_mbed_lib 采用清晰的四层架构,每一层职责分明,便于移植与调试:

层级 名称 核心职责 关键实现文件
L4 应用接口层(Application Interface) 提供 C++ 类封装,屏蔽底层协议细节;定义 Topic 发布/订阅、Service 客户端/服务器的高层 API RosNode.h , Publisher.h , Subscriber.h , ServiceClient.h , ServiceServer.h
L3 协议处理层(Protocol Handler) 实现 rosserial 帧格式解析/序列化、CRC 校验、消息序列号管理、连接状态机(SYNC、ADVERTISE、PARAM、PUBLISHER、SUBSCRIBER、SERVICE_CALL 等) ROSSerial.h , ROSSerial.cpp , MsgPack.h , MsgPack.cpp
L2 传输抽象层(Transport Abstraction) 封装 UART 设备操作,提供非阻塞读写、中断回调注册、波特率配置;解耦具体硬件(如 Serial , RawSerial , BufferedSerial HardwareSerial.h , HardwareSerial.cpp
L1 硬件适配层(Hardware Porting Layer) 提供平台相关宏定义、时钟节拍获取、临界区保护( core_util_critical_section_enter/exit )、弱符号钩子(如 _sys_exit 重定向) mbed_port.h , mbed_port.cpp

该分层设计确保了库的高度可移植性:仅需修改 L1 和 L2 层少量代码,即可将库迁移到任意支持 mbed OS 的 MCU 平台,无需触碰上层协议逻辑。

2.2 核心类关系与生命周期

RosNode 是整个库的入口点与管理中心,其对象实例化即启动 ROS 通信栈:

#include "RosNode.h"
#include "std_msgs/Int32.h"
#include "geometry_msgs/Twist.h"

// 1. 创建 RosNode 实例(绑定 UART 外设)
Serial pc(USBTX, USBRX); // 调试串口
RawSerial device(PA_2, PA_3); // 主通信串口 (TX=PA_2, RX=PA_3)
RosNode node(device);

// 2. 定义消息类型与回调函数
std_msgs::Int32 msg_int;
geometry_msgs::Twist cmd_vel;

void twist_callback(const geometry_msgs::Twist& msg) {
    // 解析接收到的 /cmd_vel 指令
    float linear_x = msg.linear.x;
    float angular_z = msg.angular.z;
    // ... 执行电机控制逻辑
}

// 3. 创建 Publisher 与 Subscriber 对象
Publisher<std_msgs::Int32> pub_int("/mcu_status", &msg_int);
Subscriber<geometry_msgs::Twist> sub_twist("/cmd_vel", &twist_callback);

int main() {
    // 4. 初始化节点(建立串口连接,发送 SYNC 包)
    node.init();

    // 5. 注册 Publisher/Subscriber 到节点
    node.advertise(pub_int);
    node.subscribe(sub_twist);

    // 6. 主循环:持续处理串口数据与定时器事件
    while(1) {
        node.spinOnce(); // 处理一帧数据(接收、解析、分发、发送应答)
        ThisThread::sleep_for(1); // 1ms 调度间隔,避免空转
    }
}

RosNode 的关键成员函数语义如下:

函数 参数 返回值 工程作用
init() bool (true=连接成功) 初始化 UART、清空缓冲区、发送 SYNC 帧请求主机握手;失败返回 false,可触发降级模式(如仅本地日志)
spinOnce() void 核心调度函数 :检查 UART RX FIFO 是否有新数据 → 解析完整帧 → 校验 CRC → 分发至对应 Subscriber/Service 回调 → 检查 Publisher 缓冲区是否需发送 → 处理超时重传;必须在主循环中高频调用(≥1kHz)
advertise(Publisher<T>&) Publisher 引用 void 向 ROS 主机注册本节点发布的 Topic,发送 ADVERTISE 帧,包含 Topic 名、消息类型、MD5 校验和;主机据此建立转发路由
subscribe(Subscriber<T>&) Subscriber 引用 void 向 ROS 主机声明本节点欲订阅的 Topic,发送 SUBSCRIBER 帧;主机开始向该节点推送匹配消息
getHardware() HardwareSerial& 获取底层串口句柄,用于高级定制(如动态切换波特率、配置流控)

Publisher Subscriber 均为模板类,其类型参数 T 必须是符合 ROS Message IDL 规范的结构体(通常由 rosmsg 工具生成)。 Publisher 内部维护一个固定大小的环形缓冲区(默认 32 字节), publish() 调用仅将消息拷贝至缓冲区, spinOnce() 在空闲时将其序列化并发送; Subscriber 则在 spinOnce() 解析到匹配 Topic 的帧后,自动调用用户注册的回调函数。

3. 关键 API 详解与参数配置

3.1 RosNode 构造与初始化参数

RosNode 构造函数接受 HardwareSerial& 对象,并可选传入 uint32_t timeout_ms (默认 5000):

// 自定义超时时间(单位:毫秒)
RosNode node(device, 10000); // 连接超时设为 10 秒

timeout_ms 控制两个关键行为:

  • SYNC 握手超时 :若在 timeout_ms 内未收到主机返回的 SYNC_ACK 帧, init() 返回 false;
  • 心跳检测超时 spinOnce() 内部维护一个心跳计时器,若连续 timeout_ms 未收到来自主机的任何有效帧(包括 PING ),则自动断开连接并尝试重连。

3.2 Publisher 配置与发布流程

Publisher 模板类构造函数签名如下:

template<typename M>
Publisher(const char* topic_name, M* msg_ptr, uint16_t buffer_size = 32);
参数 类型 说明 工程建议
topic_name const char* ROS Topic 全路径名(如 "/sensors/imu" 必须与 ROS 主机中 rostopic list 显示的名称完全一致,区分大小写
msg_ptr M* 指向消息实例的指针(如 &imu_msg 必须为全局或静态变量 ,禁止使用栈变量地址,因 publish() 仅存储指针而非拷贝内容
buffer_size uint16_t 序列化后消息的最大字节数 根据消息复杂度预估: std_msgs/Int32 =12B, sensor_msgs/Imu =256B, nav_msgs/Odometry =512B;过小导致 publish() 失败,过大浪费 RAM

publish() 函数为内联实现,无返回值,其内部逻辑为:

template<typename M>
void Publisher<M>::publish() {
    // 1. 调用 MsgPack::serialize() 将 *msg_ptr 序列化为二进制流
    // 2. 计算 CRC-16-CCITT 校验和
    // 3. 构建完整帧:[0xFF 0xFE] [LEN_H LEN_L] [MSG_ID] [SEQ] [DATA...] [CRC_H CRC_L]
    // 4. 将帧写入 HardwareSerial TX FIFO(非阻塞)
}

3.3 Subscriber 回调机制与线程安全

Subscriber 构造函数签名:

template<typename M>
Subscriber(const char* topic_name, void (*callback)(const M&));

回调函数 callback spinOnce() 的上下文中被 直接调用 ,即运行于主循环线程。这意味着:

  • 无额外线程开销 ,但要求回调函数执行时间极短(< 100μs),避免阻塞 spinOnce()
  • 非线程安全 :若回调需访问共享资源(如全局传感器数据结构),必须使用 core_util_critical_section_enter() 保护;
  • 零拷贝设计 callback 接收的是 const M& 引用,指向 ROSSerial 内部解析缓冲区, 回调返回后该引用立即失效 ,禁止保存指针或在回调外使用。

对于需长时间处理的场景,推荐模式为:

// 全局队列(mbed OS 的 rtos::Queue)
rtos::Queue<geometry_msgs::Twist, 4> cmd_queue;

void twist_callback(const geometry_msgs::Twist& msg) {
    // 快速入队,不执行耗时操作
    if (!cmd_queue.try_put(&msg)) {
        // 队列满,丢弃旧消息(FIFO)
        cmd_queue.try_get();
        cmd_queue.try_put(&msg);
    }
}

// 在独立线程中消费队列
void cmd_processor() {
    geometry_msgs::Twist cmd;
    while (true) {
        if (cmd_queue.try_get_for(1000, &cmd)) { // 等待 1s
            // 执行 PID、PWM 更新等耗时操作
            update_motor_control(cmd);
        }
    }
}

3.4 Service 客户端与服务器 API

rosserial_mbed_lib 支持完整的 ROS Service 交互,API 设计严格对齐 ROS 1 语义:

// Service Client 示例(调用主机上的 /add_two_ints 服务)
#include "roscpp_tutorials/AddTwoInts.h"
roscpp_tutorials::AddTwoInts::Request req;
roscpp_tutorials::AddTwoInts::Response res;
ServiceClient<roscpp_tutorials::AddTwoInts> client("/add_two_ints");

// 在需要时发起调用
req.a = 10;
req.b = 20;
if (client.call(req, res)) {
    printf("Result: %d\n", res.sum);
} else {
    printf("Service call failed!\n");
}

// Service Server 示例(在 MCU 上提供 /get_battery_level 服务)
#include "my_msgs/BatteryLevel.h"
my_msgs::BatteryLevel::Request srv_req;
my_msgs::BatteryLevel::Response srv_res;

bool battery_cb(const my_msgs::BatteryLevel::Request& req,
                my_msgs::BatteryLevel::Response& res) {
    res.voltage = read_battery_voltage();
    res.percentage = calculate_soc();
    return true; // true 表示成功,false 表示失败
}

ServiceServer<my_msgs::BatteryLevel> server("/get_battery_level", &battery_cb);

ServiceClient::call() 同步阻塞调用 ,会等待主机响应或超时(默认 5000ms),期间 spinOnce() 仍被调用以维持通信。 ServiceServer 的回调函数同样在 spinOnce() 中执行,需保证快速返回。

4. 源码关键逻辑解析

4.1 帧解析状态机(ROSSerial.cpp)

ROSSerial::parse() 是协议解析的核心,采用有限状态机(FSM)处理 UART 流:

enum ParseState {
    STATE_IDLE,      // 等待帧头 0xFF 0xFE
    STATE_LEN_HIGH,  // 读取长度高字节
    STATE_LEN_LOW,   // 读取长度低字节
    STATE_MSG_ID,    // 读取消息 ID
    STATE_SEQ,       // 读取序列号
    STATE_DATA,      // 读取数据载荷
    STATE_CRC_HIGH,  // 读取 CRC 高字节
    STATE_CRC_LOW    // 读取 CRC 低字节
};

状态机严格按字节推进,每个状态只处理一个字节,避免长时阻塞。当 STATE_DATA 状态下 data_len 字节全部接收完毕,即进入 STATE_CRC_HIGH ,最终校验通过后,根据 msg_id 分发至 handleAdvertise() handlePublish() handleServiceRequest() 等处理函数。

4.2 CRC-16-CCITT 实现(MsgPack.cpp)

采用查表法(Look-up Table)实现高效 CRC 计算, crc_table[256] 在编译期生成:

static const uint16_t crc_table[256] = {
    0x0000, 0x1021, 0x2042, 0x3063, /* ... 256 项 ... */
};

uint16_t MsgPack::crc16(const uint8_t* data, uint16_t len) {
    uint16_t crc = 0xFFFF;
    for (uint16_t i = 0; i < len; i++) {
        uint8_t idx = (crc >> 8) ^ data[i];
        crc = (crc << 8) ^ crc_table[idx];
    }
    return crc;
}

此实现兼顾速度与 ROM 占用(约 512 字节),比逐位计算快 5-10 倍,满足实时性要求。

4.3 内存管理策略

全库禁用 new / delete malloc / free 。所有缓冲区均为静态分配:

  • ROSSerial 类内嵌 uint8_t rx_buffer[256] tx_buffer[256]
  • Publisher 模板中 uint8_t buffer[buffer_size] 为成员数组;
  • Subscriber 不分配接收缓冲,直接解析 rx_buffer 中的数据。

此设计彻底规避了动态内存分配的不确定性,是工业嵌入式系统的基本要求。

5. 实际工程部署指南

5.1 mbed OS 项目配置(mbed_app.json)

{
    "target_overrides": {
        "*": {
            "platform.stdio-baud-rate": 115200,
            "platform.default-thread-stack-size": 4096,
            "target.printf_lib": "std",
            "target.restrict-to-stdio-stdio": true
        },
        "DISCO_F769NI": {
            "target.extra_labels_add": ["STM32F769"],
            "target.macros_add": ["MBED_CONF_RTOS_PRESENT=1"]
        }
    },
    "config": {
        "rosserial_uart_tx": {"value": "PA_2"},
        "rosserial_uart_rx": {"value": "PA_3"},
        "rosserial_node_name": {"value": "\"mcu_node\""}
    }
}

关键配置项说明:

  • platform.stdio-baud-rate : 设置 Serial pc 的调试波特率;
  • platform.default-thread-stack-size : 为 ThisThread 主循环分配足够栈空间(≥4KB);
  • target.extra_labels_add : 添加芯片系列标签,便于条件编译;
  • rosserial_uart_tx/rx : 通过宏定义指定物理引脚,避免硬编码。

5.2 ROS 主机端配置(Ubuntu 20.04 + ROS Noetic)

# 1. 安装 rosserial
sudo apt-get install ros-noetic-rosserial ros-noetic-rosserial-python

# 2. 启动 rosserial_python 节点(监听 /dev/ttyACM0)
rosrun rosserial_python serial_node.py _port:=/dev/ttyACM0 _baud:=115200

# 3. 验证连接(应看到 "Connected to" 日志)
rostopic list  # 应列出 /mcu_status, /cmd_vel 等话题
rosparam list  # 应显示 /rosserial/mcu_node 参数

serial_node.py 会自动识别 rosserial_mbed_lib SYNC 帧,并建立双向通信通道。若连接失败,请检查:

  • MCU 串口电平是否与主机匹配(TTL 3.3V vs RS232 ±12V);
  • udev 规则是否正确设置设备权限( sudo usermod -a -G dialout $USER );
  • rosserial 版本兼容性(本库基于 Protocol v0.8,需 ROS Noetic 的 rosserial ≥ 0.9.0)。

5.3 调试技巧与常见问题

  • 串口抓包定位协议错误 :使用 Logic Analyzer Saleae 抓取 UART 波形,验证帧头、长度、CRC 是否符合规范;
  • 消息丢失排查 :增大 rx_buffer 尺寸(修改 ROSSerial.h RX_BUFFER_SIZE ),或降低主机发布频率;
  • 连接不稳定 :检查 MCU 电源纹波,添加 100nF 陶瓷电容紧靠 UART 引脚;确认 spinOnce() 调用频率 ≥ 500Hz;
  • 内存溢出 :启用 mbed OS 的 HeapStats 功能,监控 mbed_stats_heap_get() ,确认无动态分配残留。

在某 AGV 底盘项目中,我们曾遇到 Subscriber 回调中调用 printf 导致 spinOnce() 阻塞的问题。解决方案是:移除所有 printf ,改用 rtos::Mailbox 将日志字符串投递至独立日志线程,主线程仅做轻量级数据搬运。此举将平均循环周期从 8ms 降至 1.2ms,满足了 100Hz 闭环控制需求。

rosserial_mbed_lib 的价值,正在于它将 ROS 这一庞大生态的接入门槛,压缩至一片 256KB Flash、64KB RAM 的 Cortex-M4 微控制器之上。当工程师在 Keil 或 Mbed Studio 中敲下 node.publish() 的瞬间,他不仅是在发送一串字节,更是在嵌入式世界与机器人智能之间,亲手铺设了一条确定性的数据通路。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐