我见过不少团队把ROS2环境搭好、节点跑起来之后,就宣称自己已经有了“机器人实时系统”。结果真机一上电,关节抖动、底盘抽搐、安全急停偶尔误触发,整个现场一团糟。原因不是算法不行,而是时间这件事没管好。
再说句实在话,这里讨论的“机器人”,是地上跑的AGV、天上飞的无人机、产线边上干活的机械臂这些实体机器人。QQ群里的聊天机器人、飞书推送通知那种软件机器人,虽然名字里也有“机器人”两个字,但它们对实时性的要求基本为零,不在这次讨论范围内。
真正想聊的是:一个能上真机的机器人实时系统到底是怎么搭出来的?哪些环节是硬骨头?哪些配置不做就是给自己埋雷?这篇文章主要面向做机械臂、四足、AGV底盘、工业控制器对接的工程师,如果你刚开始学机器人,想系统性入门,这篇文章也能帮你把学习路径里最容易被忽略的一块补上。
1. 先搞清“实时”到底在说什么
1.1 实时不是快,而是“时间边界确定”
很多初学者把“实时”理解成“响应快”,这是最大的误区。快递次日达叫及时,不叫实时。实时系统的定义是:无论系统负载怎么变化,任务的完成时间必须在一个有界的、可预先计算的时间范围内。
举个例子。你让机器人做一个阻抗控制,控制周期定在1ms。普通Linux桌面环境下,平均响应可能只有0.3ms,看起来很优秀,但偶尔一次调度延迟达到50ms。对整个系统来说,这50ms的抖动就是灾难——关节可能因为这个延迟产生振荡,甚至触发安全保护。
所以实时系统考核的指标不是平均延迟,而是最坏情况延迟(Worst Case Execution Time)和抖动(Jitter)。你的控制周期是1ms,那么每个周期必须在1ms内完成,周期之间的时间误差也要控制在很小的范围内。延迟低但抖动大,在机器人控制里等于不合格。
1.2 机器人系统里,哪些任务必须实时,哪些不需要
机器人系统是典型的混合关键性系统,不同任务对实时性的要求天差地别。我通常会把任务分成三类:
| 任务类型 | 典型例子 | 实时等级 | 周期/延迟要求 |
|---|---|---|---|
| 硬实时 | 关节伺服控制、力控、电流环、安全逻辑、急停信号 | 必须严格保证 | 周期1kHz以上,最坏延迟可证明 |
| 软实时 | 里程计融合、局部避障、运动学解算、轨迹插补 | 统计性保证 | 周期100Hz~1kHz,允许偶尔超时 |
| 非实时 | SLAM建图、全局路径规划、语音识别、视觉重任务 | 尽力而为 | 延迟几十毫秒到几百毫秒都可接受 |
硬实时任务如果错过截止时间,就会造成物理损害或者安全事故。软实时任务偶尔超时,系统性能会下降,但不会出大事。非实时任务就算卡一下,顶多就是导航慢了一点。
关键点在于:很多人在整机架构上犯的错误,就是让所有任务都跑在同一个操作系统、同一个调度域里,然后又指望它们都能满足各自的时间要求。这基本不可能。音圈电机这种执行器,电流环和位置环必须在控制器的FPGA或DSP里闭环,CPU只负责下发轨迹指令。如果你试图用上位机的Linux线程去直接闭环,那你大概率会看到电流环啸叫或者电机发烫。
1.3 “跑得快”和“控得住”是两套架构
我在项目里反复给团队提一个概念:机器人系统的“大脑”和“小脑”要分开。大脑负责感知、规划、理解环境,算力要求高,用高性能主控来跑,Linux或者更大的系统都行。小脑负责运动控制、状态机、安全保护,对确定性要求极高,必须用实时内核、MCU、DSP或者FPGA来处理。
举一个很常见的例子:一台自制送货机器人,用Jetson做上位机跑导航算法,用STM32做底盘控制板。Jetson上Linux死机了,底盘最多是失去指令来源,但STM32还能执行最后的刹车逻辑,让机器人停下来。如果反过来,把运动控制放在Jetson上,那Linux一卡,整台机器人就失控了。所以设计实时系统,第一步不是选内核、调参数,而是先划分控制边界,确定哪些东西必须由实时单元兜底,哪些可以在通用系统上慢慢算。
2. 自下而上的架构:先定操作系统层
2.1 通用Linux为什么不够实时
先看一组常识:普通Ubuntu内核默认采用CFS(完全公平调度器),它追求的是“让所有进程公平地分享CPU”。这句话本身就是实时性的敌人。公平调度会让你周期性的控制线程和后台的日志进程、网络服务抢CPU时间片,谁都不能保证自己一定能按时跑到。
再加上通用内核里的中断线程化、锁竞争、内存分配缺页、SMP负载均衡,任何一个环节都可能让线程卡上几毫秒甚至几十毫秒。我见过一个项目,机器人在Linux上跑导航,一切正常;但只要后台一跑打包压缩任务,底盘控制周期立刻乱掉,机器人走出来的轨迹像画符一样。这就是通用内核“尽力而为”的调度方式带来的后果。
2.2 实时化方案的对比与选型
要把Linux变成实时系统,目前主流有两个方向:PREEMPT_RT补丁和双内核方案(典型实现是Xenomai)。此外还有更极端的用RTOS直接跑裸机,或者用FPGA做硬实时逻辑。
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| PREEMPT_RT | 给Linux内核打补丁,让内核几乎处处可抢占 | 社区活跃,生态好,主线内核逐步合入;开发调试方便 | 最坏延迟仍在几十微秒到百微秒级,不如硬实时系统稳定 | ROS2机器人、需要跑Linux生态的中高性能主控 |
| Xenomai | 在Linux旁挂一个实时微内核,实时任务跑在微内核,Linux作为非实时域 | 实时性更强,抖动更小,可达微秒级 | 配置复杂,驱动适配麻烦,开发和调试门槛高 | 对控制周期和确定性要求更高的运动控制器 |
| RTOS裸机 | 直接在MCU上跑FreeRTOS、Zephyr等 | 硬实时,中断响应极快,确定性最好 | 生态弱,复杂算法开发效率低,没有Linux那一套软件栈 | 底盘电机控制、执行器闭环、传感器采样层 |
| FPGA/SoC硬逻辑 | 用硬件逻辑实现控制环路 | 延迟是纳秒级的,可靠性最高 | 开发周期长,难度大,改一次逻辑就得重新综合 | 伺服驱动器、高级安全保护、高频电流环 |
选型的时候别盲目追求“越硬越好”。对一个跑ROS2的移动机器人,PREEMPT_RT已经足够,真正决定实时性的往往是你怎么划分任务和配置系统。对一台工业机械臂的控制柜,控制周期2ms都嫌长,这时候PREEMPT_RT也未必稳,要么上Xenomai,要么直接用控制卡的DSP做闭环,Linux只做交互和通信。
我的建议是:入门阶段,优先把PREEMPT_RT玩明白,成本最低、见效最快。当你发现PREEMPT_RT的抖动已经无法满足控制需求时,再去考虑双内核方案,或者把关键控制逻辑下沉到MCU/DSP。
2.3 实时内核装完只是开始:关键系统配置
很多人以为装了实时内核就万事大吉,这是第二个大误区。装完内核只是拿到了“可以实时”的许可证,真的要让你的控制线程稳定运行,还需要做三层配置。
第一层是CPU隔离。如果你的主控是四核或八核,通常会把一两个核心完全隔离出来,专门给实时任务用。做法是在内核启动参数里加isolcpus、nohz_full和rcu_nocbs。以x86平台的Ubuntu为例,编辑/etc/default/grub,找到GRUB_CMDLINE_LINUX,加上这么一段:
GRUB_CMDLINE_LINUX="isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3"然后update-grub并重启。这样CPU2和CPU3上就不会有普通的调度任务和时钟中断跑来跑去,实时线程独占这两个核,确定性会好很多。我自己习惯把CPU0留给系统,CPU1放DDS通信和驱动,CPU2和CPU3放硬实时控制线程。
第二层是线程优先级和调度策略。在Linux里,实时线程要用SCHED_FIFO或SCHED_RR调度策略,并且设置较高的优先级。比如在ROS2节点里,可以通过pthread_setschedparam把控制线程设置为SCHED_FIFO,优先级80。注意别把实时优先级设置成99,那是内核关键线程的领域,直接碰容易把系统搞死。
第三层是锁内存。实时线程最怕缺页中断,一旦代码或数据不在物理内存里,访问时就会触发一次慢到离谱的缺页异常,延迟可能飙到几十毫秒。解决方法是创建线程后立刻mlockall()把地址空间锁进物理内存,同时避免在实时路径里用malloc动态分配内存。这也意味着你在写控制代码时要养成习惯:任务启动时把缓冲区一次性分配好,运行期间只用栈上变量和预先分配的内存池。
3. 中间件与数据通路:ROS2怎么跑出低抖动
3.1 ROS2的架构进步在哪里
很多老工程师还在用ROS1,我可以理解感情因素,但论实时性,ROS2的架构确实比ROS1好太多了。ROS1依赖roscore作为中心节点,一旦roscore卡顿,所有话题通信都跟着遭殃,根本谈不上实时。ROS2采用了去中心化的DDS(数据分发服务)通信架构,每个节点之间直接通信,没有单点瓶颈;DDS本身又支持QoS策略,允许你针对不同的数据流设置不同的可靠性、时效性要求。
但要注意,ROS2只是“提供了支持实时的可能性”,它本身并不是实时系统。你仍然需要把节点里的关键线程设置为实时优先级,需要选择合适的DDS实现并做配置,才能真正让数据通路稳定下来。
3.2 DDS选型:从FastDDS切到CycloneDDS
ROS2默认的DDS实现通常是FastDDS,功能很全,但在一些硬实时场景下,它的延迟抖动表现并不理想。我自己在项目里做过多组对比测试,同样的硬件、同样的节点,把RMW实现切到CycloneDDS之后,话题延迟的抖动明显下降,CPU占用也更低。
切换方式很简单,安装之后设置环境变量即可:
export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp如果你想在系统层面固定,可以写到~/.bashrc或/etc/profile里。CycloneDDS的配置文件里还可以进一步设置线程优先级、启用共享内存传输(Iceoryx)等,这几个选项对实时性提升非常明显。
3.3 QoS配置直接影响端到端时延
QoS配置是ROS2实时化最容易被忽略的环节。同一个话题,reliable_qos和best_effort_qos的延迟表现差别很大。reliable保证不丢包,但会用ACK和重传机制,延迟天然更高;best_effort不保证送达,但时延更低、更稳定。
我的经验是:传感器原始数据、控制指令这类对实时性要求极高、丢一两帧也无所谓的流,果断用best_effort;地图、日志、配置这类不允许丢失的数据,才用reliable。还有一个实用技巧:用Deadline QoS给话题设置一个最大允许间隔,如果DDS检测到数据超时未到达,可以触发回调,用来做链路健康监测,这个机制在排查实时性问题时特别有用。
另外,DDS的发现协议(Discovery)会在节点刚启动时产生大量广播流量。在大型系统中,如果几百个节点同时启动,发现风暴可能让所有通信卡几秒钟。解决办法是用DDS的Discovery Server模式,把节点发现流量引导到中心服务器,而不是全网广播。这种模式虽然多了一个依赖点,但在节点规模大了之后,实时性和稳定性都会好很多。
3.4 从“话题到驱动”的端到端管线怎么设计
拿到传感器数据,经过算法处理,最后变成电机指令,这条链路上只要有一个环节是阻塞的,整个实时性就崩了。我对团队的要求是:硬实时线程里只做确定性操作,绝不做动态内存分配、不加锁、不调用系统调用、不打日志。算法计算堆到非实时线程里去,硬实时线程只负责等数据、解析、解算运动学、下发指令。
举例来说,导航路径规划节点算出目标速度,通过/cmd_vel话题发出来;底盘控制节点收到/cmd_vel后,做速度平滑和运动学解算,然后通过串口或EtherCAT下发到底层电机驱动板。这个流程里,导航规划是非实时的,但底盘控制节点必须跑在实时线程上,并且它要做的事情越少越好。我曾经接手一个项目,底盘控制节点在回调里写文件记录日志,结果每写几百条日志就卡一次,机器人走起来一瘸一拐。去掉这个日志调用之后,抖动立刻下来了。
4. 实操:一个移动机器人底盘实时控制案例
4.1 场景设定与目标指标
说了一堆理论,落地看一个案例。假设你现在要自己做一台送货机器人或者AGV底盘,上位机是Jetson Orin,跑Ubuntu + ROS2;底层是STM32运动控制板,通过串口和上位机通信,串口波特率921600。你的目标是把上位机到电机的端到端控制周期稳定在10ms以内,抖动不超过2ms。
这个指标对底层STM32来说毫无压力,真正的瓶颈在上位机。Jetson跑Linux,还要跑导航算法、摄像头处理、DDS通信,怎么保证10ms周期稳定?这就是实时系统设计要解决的问题。
4.2 从零搭建实时底盘系统的步骤
第一步,安装并验证实时内核。Ubuntu上可以直接安装linux-image-rt-amd64,也可以从kernel.org自己编PREEMPT_RT内核。装完重启后执行uname -a,看到PREEMPT_RT字样就说明实时内核已经生效。
第二步,做CPU隔离。编辑/etc/default/grub,把CPU核心划分出来。我习惯把Jetson的Denver核心留给系统,剩余的A78核心部分隔离出来跑实时任务。具体隔离哪些核心要根据你的硬件结构和业务负载来定,别照抄网上的配置。
第三步,写实时控制节点。这个节点接收/cmd_vel,做轨迹插补,通过串口下发。关键代码里要做三件事:用pthread_setschedparam设置SCHED_FIFO和优先级,用mlockall锁内存,循环里严格让出时间片。
第四步,切到CycloneDDS,启用共享内存传输。ROS2的话题收发就不容易因为网络包处理产生抖动。
第五步,做端到端测试,看延迟分布。不要只测平均延迟,要看P99甚至最大值。我在测试时用的方法是:在上位机发送指令的瞬间打一个时间戳,同时在下位机收到指令后立刻回一个ACK包,上位机用收到ACK的时间减去发送时间,就得到端到端延迟。
4.3 实测数据怎么判断合格
假设你测试得到的结果是:平均延迟1.2ms,P99延迟4.8ms,最大延迟18ms。从平均看似乎很好,但最大延迟18ms已经超过了你10ms周期的要求,这意味着每几百个周期里就会有一次超时,机器人偶尔会抽搐一下。
怎么优化?先查最大延迟出现时的现场。最常见的原因有:网络服务定时扫描导致CPU争抢、某个节点突然启动触发DDS发现流量、串口缓冲区DMA竞争。我的经验是,把DDS通信的接收线程绑定到非实时核,控制线程独占隔离核,再把系统的网络服务能关就关,最大延迟一般能压到5ms以内。
还有一个小细节:Jetson这类嵌入式板卡的CPU调频策略默认是powersave,为了省电,频率会上下浮动,这也会引入延迟抖动。把CPU调频策略改为performance,强制跑高频,实时性会稳定很多。
5. 常见实时性问题排查与调优清单
5.1 问题现象与对策速查表
| 现象 | 常见原因 | 对策 |
|---|---|---|
| 控制周期偶发长时间停顿 | 动态内存分配触发缺页、日志IO阻塞、锁竞争 | 实时路径禁止malloc和printf;锁内存;用无锁队列或RT mutex |
| 话题延迟忽大忽小 | DDS发现流量干扰、QoS配置不合理、CPU争抢 | 切Discovery Server;用best_effort;隔离CPU并绑定线程 |
| 机器人一启动NS就卡顿 | 多节点启动时DDS发现风暴 | 部署Discovery Server,错峰启动节点 |
| 上位机死机或卡死后机器人失控 | 安全逻辑跑在了非实时系统上 | 把安全逻辑下沉到MCU/IPC;实时系统与主控分离 |
| 底盘抖动但平均延迟很低 | 隐藏的最坏延迟没有被观测到 | 做端到端延迟测试,优化P99和最大值,而非只看平均值 |
| 串口或总线偶发丢包 | 缓冲区不足、中断被其他任务抢占 | 增大DMA缓冲;中断绑定到实时核或专用核 |
5.2 排查工具与调试方法
排查实时性问题,靠日志打点是打不出来的,日志本身会干扰时序。正确做法是用trace工具。最简单的工具是ftrace,内核自带,通过/sys/kernel/tracing接口可以抓取调度事件、中断事件和唤醒延迟。另一个更直观的工具是perf sched,可以分析每个线程的调度延迟和运行时间。
对ROS2系统,ros2 trace可以结合LTTng记录用户态节点和内核态的事件,然后把数据导入Trace Compass分析。我排查实时问题时最常用的一条路径是:先看端到端延迟的分布,确认哪些时间点异常;再用perf sched记录调度情况,看异常点是否和某个低优先级进程抢占有关;最后用ftrace看中断和锁事件。
这里要特别提一个经典问题:优先级反转。假设实时控制线程优先级是80,它等一个锁;而持有这个锁的线程优先级是50,又恰好被一个优先级为60的非实时线程抢占。实时线程迟迟拿不到锁,控制周期被拖垮。解决方法很直接:在实时路径里尽量避免锁;如果无法避免,用实时互斥锁(RT mutex),它自带优先级继承机制,能避免这种情况。
5.3 调优经验:几个容易忽略的“元凶”
我调过很多实时系统,发现最后影响稳定性的往往不是那些大框架,而是小细节。
第一个元凶是系统日志服务。journald或者syslog定时刷盘,会产生IO中断,干扰CPU隔离区。解法是把隔离核的中断亲和性全部设置到其他核,并且减少非必要系统服务。
第二个元凶是网络协议栈。如果你用了有线网或者WiFi,网络驱动的软中断会在所有核上飘。用irqbalance可能帮倒忙,建议手动把网卡中断绑定到固定核,让实时核彻底不处理网络中断。
第三个元凶是超线程。对实时系统来说,超线程带来的“伪核心”会引发严重的Cache争用,建议在BIOS或启动参数里直接关闭超线程,让每个物理核独享L1/L2缓存。
我把这些整理成一句话给你:实时系统的调优,本质上就是“消除一切非确定性”,让每个步骤都尽可能变得可预测。
6. 工业机器人与外部系统的实时对接
6.1 工业控制柜的黑盒现实:学会配置而不是改内核
聊完自研系统,再聊工业机器人。库卡、ABB、发那科、安川、埃夫特这些工业机器人,控制柜内部本身就是一套专用的实时系统。对使用者来说,它是个黑盒——你接触不到调度器,也不需要接触,你需要做的是了解它对外提供的接口和参数配置方式。
工业机器人领域的很多“问题”,本质上是参数配置问题,不是系统研发问题。比如FANUC报SYS-212需要应用DCS参数,实际上是因为安全配置没有正确应用或者版本不匹配;ABB机器人基本操作里常见的DSQC板卡参数设置,决定的是IO信号映射和通信行为;安川机器人标定结果的验证,直接关系到绝对精度能否保证。这些场景下,你要做的是理解控制柜的配置逻辑,而不是重写它的内核。
6.2 把自有实时系统和工业机器人对接的常见方式
工业机器人和外部系统对接,通常走现场总线。常见的选择包括EtherCAT、Profinet IRT、EtherNet/IP、CANopen,以及一些厂商自定义协议。这些总线的实时等级不同,选型要考虑你对接的上位机周期是多少毫秒级的。
EtherCAT是我最常用的方案,尤其在需要多轴同步的场景。它工作在第二层,不依赖IP协议栈,一个周期内主站发一帧数据,从站顺次读取并写入,同步抖动可以控制在纳秒级。如果只是简单的IO信号交互,Profinet RT或者EtherNet/IP也能满足需求,但要注意它们的扫描周期一般在毫秒级,嵌入式实时性不如EtherCAT。
设计这类系统时我有一条原则:凡是和机器人安全相关的信号,绝对不允许只走软件通路。急停、安全门信号必须通过硬接线或者安全总线(如Profinet ProIsafe、EtherCAT FSoE)走独立链路。软件系统再实时,也只是“尽力实时”,安全逻辑必须由物理上更可靠的机制兜底。
VDA5050这类面向AGV的上层调度协议,走的是以太网和MQTT,属于软实时甚至非实时的范畴。它负责的是“下一段任务是什么”,而不是“电机下一ms怎么转”。真正控制AGV底盘运动的,还是车端底层实时控制器。所以你在做AGV系统时,要理解这种分层关系:云端调度允许几百毫秒延迟,车端运动控制系统必须毫秒级响应。把这两个层次混在一起设计,将来调试一定痛苦。
6.3 仿真平台与真机之间的实时性鸿沟
很多团队喜欢先上仿真,确认算法没BUG再上真机。这个习惯很好,但必须清醒认识到,仿真平台本身并不等于实时环境。Gazebo、Isaac Sim这些仿真器,物理引擎的计算时间是不确定的,模型越复杂,一帧算多久越说不准。你在仿真里看到“机器人平滑运行”,只能说明算法逻辑没问题,不能说明实时性达标。
更可靠的做法是硬件在环(HIL)测试:把真实的控制板接到仿真器上,仿真器模拟电机和传感器,控制板跑真实的控制代码。这种做法能验证控制板在真实时序下的行为,比纯软件仿真靠谱得多。另一种更粗暴但也有效的办法是录制传感器数据然后回放,验证算法在固定时间戳下的处理速度是否满足要求。
我个人建议,在仿真里可以大胆快速迭代算法,但一旦涉及运动控制和力控制,直接上HIL或者小规模真实硬件测试,别拖到整机阶段才发现时序问题。
7. 最后再分享一段真实经历
我踩过最大的坑,不是内核配置不会写,而是好不容易把内核、DDS、线程优先级都配好了,结果把控制线程和几个高负载算法线程都塞进了同一个隔离核。我以为“隔离出来的核肯定没问题”,结果几个线程在里面互相抢时间片,抖动比不隔离的时候还大。后来才意识到,CPU隔离只是“画了个安静的房间”,房间里住几个人才是真正需要规划的事情。
所以我对做机器人实时系统的朋友,只有一个建议:先定系统级时延指标,再自下而上逐层验证。别指望靠某一个参数“法力无边”地解决所有问题,也别在没测最坏延迟之前就宣称自己的系统是实时的。机器人的实时系统,核心从来不是“跑得有多快”,而是“每个周期都稳得住”。这行做久了你会发现,稳定,是一种比快更高级的能力。