一条 ROS2 控制指令的执行路径:从消息到回调,再到控制时效
机器人系统里常有这样的现象:Publisher 按计划周期发送消息,Subscriber 也能够收到,但控制器仍然感到“慢半拍”。把原因归结为网络延时很容易,问题却可能出在消息队列、Executor 回调、线程调度,或者指令本身已经过期。
这里从一个简单场景展开:规划节点发送某个关节的目标位置,执行节点检查命令后交给控制接口。 我们不预设运行平台、不声称这是任何实机的实现;用公开的 ROS2 机制和一个虚拟案例,建立可以逐步验证的时序模型。
1. 先看清消息经过的几段路
Planner Node Control Node
│
│ ① 生成 joint target、seq、stamp
│
├── ② rclcpp Publisher::publish()
│ │
│ ▼
│ RMW / DDS(匹配QoS、缓存与传输)
│ │
│ └──────────────────► ③ 消息变为可获取
│ │
│ ④ Executor 发现就绪
│ │
│ ⑤ 调用 Subscription 回调
│ │
│ ⑥ 检查时效、状态与限幅
│ │
│ ⑦ 交给控制接口
│ │
│ ⑧ 控制循环 / 驱动写入
│ │
◄────────────── ⑨ 关节状态或异常反馈 ────┘从代码看,调用 publish() 只说明应用向通信层提交了消息。它不能单独证明对端已经收到,更不能证明执行器接受了命令。
为此,讨论性能和错误时最好把路径分开:
| 环节 | 主要问题 | 典型可观察信息 |
|---|---|---|
| ①—② 应用发布 | 目标生成是否及时;Publisher 周期是否稳定 | 源端序号、发布调用时间、生成时间 |
| ②—③ 通信中间件 | QoS 是否相容;数据是否在期望条件下送达 | QoS 配置、匹配事件、收发序号 |
| ③—⑤ 执行器与回调 | 消息有了,回调什么时候开始处理 | 回调时间、线程、Callback Group、负载 |
| ⑤—⑦ 业务判断 | 收到的是不是仍可执行的有效命令 | 时间戳、状态、有效期、命令拒绝原因 |
| ⑦—⑨ 控制与反馈 | 控制器何时更新;设备是否响应 | 控制周期、命令/状态接口、关节反馈 |
这不是一个“通信耗时”数字能概括的完整过程。不同实现之间的队列、发现与调度行为也未必相同。
2. ROS2、RMW 与 DDS 的边界
ROS2 应用程序面向 rclcpp 等客户端库开发;底层通过 RMW(ROS Middleware Interface)与选用的中间件实现配合。在常用的 DDS 方案中,DDS 负责数据分发、发现与 QoS 等功能。讨论问题时要给出使用的 ROS 发行版、RMW 实现、通信拓扑和相应配置,不宜仅记录“ROS2”。
以 Topic 为例:Publisher 和 Subscription 的主题名、消息类型匹配仍不够。QoS 端点必须相容,否则可能根本无法建立有效数据通信。
Reliability 的一个容易误解的组合
| 发布端提供 | 订阅端要求 | 相容性 |
|---|---|---|
| Best Effort | Best Effort | 相容 |
| Best Effort | Reliable | 不相容 |
| Reliable | Best Effort | 相容 |
| Reliable | Reliable | 相容 |
这是 ROS2 官方文档中 Reliability 的请求/提供模型。它只展示这一项策略的相容性;即使该列相容,其余 QoS 策略也可能让端点不匹配。
可靠传输对必须完整交付的数据很有价值。不过对于不断刷新的状态流,过期数据也可能在应用层变得没有意义。Reliable 并非所有控制消息的默认最优选择。 应同时考虑 History、Depth、Durability、Deadline/Lifespan、网络条件及业务自身的过期处理。
尤其要分清:某个 Deadline 事件提示预期数据周期可能未被满足,不等于应用会自动让执行器进入安全状态。业务层仍需规定缺失、延迟或过期输入的应对策略。
3. 消息就绪,不等于回调立即开始
Executor 持续等待订阅消息、Timer 到期等就绪事件,再选择相应回调执行。官方文档指出,rclcpp 的 Executor 使用 wait set 获取来自底层的可用性信息;并不存在一个可以简单理解成“每个 ROS2 回调都排进 rclcpp FIFO”的通用模型。
最直观的情况是单线程 Executor:
时间 ───────────────────────────────────────────►
Callback A [--------- 正在执行 ---------]
Topic B 就绪 ↑
Callback B [--- 执行 ---]
└─── 等待其他回调结束 ──┘这幅图只表示调度关系,方框长度不是实测结果。如果回调 A 阻塞、进行大量计算或等待外部接口,单线程执行器不能同时运行回调 B。即使消息已到达,也可能晚些时候才被应用处理。
MultiThreadedExecutor 就一定能改善吗?
不一定。ROS2 的 Callback Group 决定哪些回调可以并发执行:
- Mutually Exclusive:同组回调不会并行执行。
- Reentrant:允许重入,可能出现同一回调的多个并发实例。
- 不同 Callback Group:在多线程 Executor 中,有机会由不同执行线程并发处理,仍受实际线程数、负载和系统调度影响。
默认 Callback Group 属于 Mutually Exclusive。所有实体都留在同一个默认组时,即使使用 MultiThreadedExecutor,也不能由此期待该节点的这些回调并行运行。
反过来,把相关回调随意放进 Reentrant Group 也不安全:它可能使共享状态竞争、生命周期错误及锁的问题显露出来。应先判断访问的是不是同一资源、是否要求消息按状态顺序处理,再安排回调组和同步策略。
这与实时嵌入式系统里分析 Task/ISR 抢占有相似的思考方式,但执行模型不是一一对应的。ROS2 Executor、操作系统线程和具体 RMW 需要分别观察。
4. “收到了”为什么仍不能直接执行
设想 Planner 连续发布三个目标:
seq = 101 target = 0.2 rad
seq = 102 target = 0.4 rad
seq = 103 target = 0.6 rad这组三条消息是演示输入,不是实验抓包。假设执行节点因回调负载而滞后,等它开始处理 seq=101 时,规划端可能早已生成了 seq=103。此时,消息能被读取,不代表命令还适合执行。
业务层应回答:
- 命令是否带有来源、序号、目标单位和生成时间?
- 指令有效期由谁规定、使用哪个时钟?
- 控制状态是否允许接受新的目标?例如当前是否处于错误或停止状态。
- 同一来源的旧序号能否覆盖新目标?不同来源如何仲裁?
- 命令超时后应保持目标、受控停止还是进入其他状态?这需要系统级的具体故障模型。
以下是概念约束,不是某个机器人实际运行的驱动代码:
当收到一条关节目标命令:
检查命令来源、格式和单位
在共同定义的时钟域中检查生成时间与有效期
检查当前状态、控制权、序号/顺序和允许的范围
满足条件 → 接受目标并由控制接口后续执行
不满足条件 → 拒绝或进入该故障模型规定的处理流程其中“超时后怎么做”不能仅靠通用 ROS2 QoS 配置决定。硬件平台、控制目标、失效风险和独立安全机制都可能改变设计。软件层的限时检查不能代替物理急停或安全认证。
5. 100 Hz 到底该测什么
先定义目标:100 Hz 即名义周期 10 ms。可以用两个独立 C++ ROS2 节点测量通信与回调行为,但一定要注明测试层次。
| 指标 | 计算所需原始数据 | 能说明什么 |
|---|---|---|
| 发布周期分布 | 相邻发布调用的单调时钟时间戳 | Publisher 的用户态循环是否接近目标周期 |
| 回调间隔分布 | 相邻订阅回调入口时间戳 | 对端实际处理事件的节奏 |
| 收到的序号缺口 | 发布端与接收端序号、起止范围 | 哪些序号未被观察到;不直接等于网络丢包 |
| 回调执行时间 | 入口/退出时间戳 | 回调自身花了多长时间 |
| 控制周期/反馈偏差 | 控制器更新、命令提交、状态反馈时间 | 控制环节而非仅ROS2通信的行为 |
如果用 steady_clock,要记录其使用方式与时钟域;不能在未经验证的不同机器上直接相减时间戳得出单程延时。模拟时间也不能与系统单调时间随意混用。
一个基本实验可以设计为:
- 基线:固定 ROS2/RMW、Executor、QoS、拓扑和负载,运行两次,每次采集 60 s 的目标 100 Hz 序列。
- 单变量对照:只增加订阅端回调的工作负载,比较间隔分布变化。
- 再做策略对照:在相同条件下变动 QoS 或 Callback Group,而不是一次更改四五项设置。
- 分析:记录 P50、P95、P99、最大间隔、序号缺口及异常样本,并核对记录工具是否本身造成额外负载。
这里没有提供任何“运行结果图”或性能结论,因为相应 ROS2 实测数据尚未完成。待实验后,才能进一步回答当前平台、RMW和配置下具体出现了什么。
6. 让通信、控制与故障恢复有清楚的责任边界
ROS2 通信层可以告诉我们数据有没有匹配、什么时候可用;Executor 决定回调在哪个执行上下文中得到处理;运行时组件检查数据是否仍有效;控制器通过状态与命令接口执行工作;诊断模块保存发生了什么。
把这些工作混在一个订阅回调里可能让最小样例很快跑起来,却会让后续故障难以归因。随着项目扩展,至少需要明确:
| 组件 | 该负责的事情 | 不应默认承担的事情 |
|---|---|---|
| Message / Transport | 按接口约定传递数据,明确QoS | 保证执行器一定执行或满足硬实时 |
| Executor / Callback | 把就绪事件交给应用代码 | 自动保证业务状态和数据一致 |
| Command Gate | 检查有效期、状态、范围和授权 | 替代独立的物理安全回路 |
| Controller / Hardware Adapter | 状态读取、控制更新、输出指令 | 理解上层任务目标的全部语义 |
| Diagnostics | 记录故障、指标与可观测事件 | 单凭日志证明不存在风险 |
这套边界适合用于设计 Mock Hardware 和仿真平台的最小模块。真实执行器还需要依据具体驱动、控制周期、关节限制、设备保护和目标系统的安全要求继续设计。
7. 延伸阅读
公开技术依据
- ROS2 Jazzy Executors 文档:wait set、Single/MultiThreadedExecutor与 Callback Group 的执行规则。
- ROS2 Jazzy QoS 文档:请求/提供模型、History、Depth、Reliability与Deadline。
- ROS2 Using Callback Groups:互斥/可重入组、并发与常见阻塞情况。
- ros2_control Getting Started:Controller Manager 的读状态、更新控制器、写命令职责。
本文描述通用机制与虚拟示例,不对应现有机器人硬件或尚未发生的 ROS2 测试。关于实验结果的结论将以可复现的实际数据为准。