← 返回文章库

机器人为什么用 DDS 而不是 MQTT?从宇树的消息定义说起

最后更新 2026-08-23
⏱ 约 13 分钟 🟡 涉接线/强电
你将学到
  • 说清 DDS 和 MQTT 的根本差异,以及机器人为什么选了前者
  • 看懂一套机器人的消息体系怎么划分:ROS 标准消息、厂商自定义消息、请求响应消息
  • 从话题命名和消息命名空间里,读出这套系统跟 ROS 生态的关系

如果你做过物联网项目,对 MQTT 一定不陌生。一个 Broker 架在中间,设备连上去,往主题里发消息,别人订阅这个主题就能收到。简单、成熟、随便一台服务器就能跑起来。

那机器人为什么不用它?

宇树的整套 SDK 建在 DDS 上,ROS 2 的默认中间件也是 DDS。这不是巧合。这篇我们借 unitree_dds_wrapper 这个仓库把机器人的消息体系摊开看一遍——它把所有消息类型都生成成了 Python 文件,目录结构一目了然,是理解这套通信设计最省力的入口。

按惯例说明边界:本文基于该仓库公开的 README 与文件树结构,没有真机可验证,涉及运行行为的部分以官方文档的说法为准。

先看一段官方示例,三个细节

unitree_dds_wrapper 的 README 给了一段最小示例,大意是:创建一个 Publisher 和一个 Subscription,指定消息类型和话题名,然后循环发送并读取。

from unitree_dds_wrapper.publisher import Publisher
from unitree_dds_wrapper.subscription import Subscription
from unitree_dds_wrapper.idl import unitree_go

msg_type = unitree_go.msg.dds_.LowState_
pub = Publisher(message=msg_type, topic="rt/test_dds")
sub = Subscription(message=msg_type, topic="rt/test_dds")

短短几行,藏了三个值得追问的细节。

细节一:没有 Broker。 从头到尾没有出现服务器地址、端口、连接这类东西。你只是声明"我要在这个话题上发某种类型的消息",然后就发了。

这是 DDS 和 MQTT 最根本的分野。MQTT 是中心化的:所有消息都要过 Broker 中转,Broker 挂了整个系统哑火。DDS 是去中心化的:节点通过多播自动发现彼此,数据点对点直达,没有中间人。

放到机器人身上,这个差异是致命的。机器人身上跑着十几个进程——运动控制、传感器采集、视觉处理、导航——它们都在一台机器或一个局域网内。如果所有数据都要绕道一个 Broker 再分发出去,那多出来的一跳延迟和单点故障风险,是控制系统不能接受的。控制回路里多一个可以挂掉的中间件,就是多一个让机器人摔倒的理由。

细节二:话题名叫 rt/test_dds

rt/ 这个前缀不是随手起的。它是 ROS 2 把自己的话题映射到底层 DDS 时的命名约定——ROS 2 里的话题 /foo,到了 DDS 层面就叫 rt/foo(rt 是 ROS topic 的缩写)。

看到这个前缀,你就知道这套通信不是关起门来自己设计的,而是刻意对齐了 ROS 2 的线上格式。它带来的直接好处是:一个 ROS 2 节点和一个宇树 SDK 程序,在网络上说的是同一种话,可以直接互通。

细节三:消息类型来自 unitree_go.msg.dds_.LowState_

这个命名路径是自动生成的痕迹。DDS 的消息类型要先用 IDL(接口定义语言)写好,再由工具生成各语言的绑定代码。unitree_go 是命名空间,LowState_ 结尾那个下划线是生成器留下的标记。

顺着这条线索去看仓库的 idl/ 目录,能看到整套消息体系的全貌。

消息目录:一半是 ROS 的,一半是自己的

unitree_dds_wrapper/idl/ 下的命名空间,可以清楚地分成两组。

第一组,直接来自 ROS 的标准消息包:

builtin_interfaces/   —— Time、Duration
std_msgs/             —— Header、String、Int8
geometry_msgs/        —— Point、Pose、Quaternion、Twist、Transform、
                         PoseStamped、TwistWithCovariance …
sensor_msgs/          —— PointCloud2、PointField
nav_msgs/             —— Odometry、OccupancyGrid、MapMetaData
tf2_msgs/             —— 坐标变换
trajectory_msgs/      —— 轨迹

这些名字,任何写过 ROS 的人都会觉得眼熟。geometry_msgs/Twist 是速度指令的通用格式,nav_msgs/Odometry 是里程计,nav_msgs/OccupancyGrid 是占据栅格地图,sensor_msgs/PointCloud2 是点云。

厂商直接复用这些定义,意味着你的数据天然能被整个 ROS 生态消费。你不需要写转换代码,就能把里程计喂给现成的定位算法、把点云喂给现成的建图工具、把地图喂给现成的路径规划器。

这个决定的分量不小。自定义一套消息格式当然更自由,但代价是整个生态的工具链都用不上,什么都得自己造。选择兼容,等于选择站在别人的肩膀上——这和我们在机器人操作系统那篇里讲的 ROS 生态价值是同一件事。

留意 geometry_msgs 下有一批 *WithCovariance 结尾的类型——带协方差的位姿、带协方差的速度。协方差描述的是这个测量值有多不确定。一个只报"我在坐标 (3.2, 1.7)"的系统和一个还会报"这个估计的误差椭圆有多大"的系统,是两个层次。做传感器融合时,没有协方差就没法做加权。这类细节是「工程级」和「玩具级」的分界。

第二组,宇树自定义的命名空间:

unitree_go/     unitree_hg/     unitree_hand/     unitree_hx/     unitree_arm/

四足、人形、手、机械臂各有各的命名空间。这又一次印证了我们在四足与人形差异那篇里讲的:这两类机器人在软件层面是被当成两套东西对待的,连消息定义都不共用。

自定义消息清单:一台机器人在说些什么

unitree_go 下的消息类型列出来,等于拿到了一张"这台机器人对外说什么、听什么"的完整清单:

LowCmd_ / LowState_             —— 低层指令与状态
MotorCmd_ / MotorCmds_          —— 单个/多个电机指令
MotorState_ / MotorStates_      —— 电机状态
IMUState_                       —— 惯性测量单元状态
SportModeCmd_ / SportModeState_ —— 运动模式指令与状态
BmsCmd_ / BmsState_             —— 电池管理系统
LidarState_                     —— 激光雷达状态
HeightMap_                      —— 高度图
PathPoint_                      —— 路径点
WirelessController_             —— 无线手柄
AudioData_                      —— 音频
Go2FrontVideoData_              —— 前置视频
UwbState_ / UwbSwitch_          —— UWB 定位
Error_                          —— 错误
Req_ / Res_                     —— 请求与响应
TimeSpec_ / InterfaceConfig_    —— 时间与接口配置

这张清单里有几处值得停下来看。

LowCmd/LowStateSportModeCmd/SportModeState 是两套并行的东西。

前者是低层:直接跟电机打交道,MotorCmd 里放的是给单个关节的指令。后者是高层:SportMode 说的是"运动模式",你告诉它往哪走多快,具体每条腿怎么迈由机器人内部的控制器决定。

同一台机器人暴露两套完全不同抽象层次的接口,这是足式机器人 SDK 的典型设计,也是新手最容易懵的地方。这个划分足够重要,我们在后面单独一篇里细讲。

Cmd/State 成对出现,是控制系统的基本节奏。 你发指令、它报状态,两条独立的话题构成一个闭环。这和请求-响应模式不一样:这里没有"回复"的概念,两条数据流各跑各的,各自有自己的频率。

BmsState(电池管理)出现在消息清单里,说明电量、电芯状态这类信息是作为一等公民对外广播的,而不是藏在某个查询接口后面。对于一台会自己走动的机器人来说,电量是随时要监控的量——快没电时你需要它立刻回来,而不是等你去问。

Req_/Res_ 这对消息,对应的就是我们在SDK 架构那篇里讲过的那套 RPC 机制。请求-响应模式被架在发布-订阅的底座上,用两条特殊的话题来承载。

QoS:DDS 真正的护城河

讲到这里,还有一件 DDS 独有的事没说:QoS,服务质量策略

MQTT 也有 QoS,但只有三档(最多一次、至少一次、恰好一次),管的是"要不要保证送达"。DDS 的 QoS 是一整套策略集合,每条话题可以单独配置:

  • 可靠性:尽力而为还是可靠传输
  • 历史深度:缓存最近几条
  • 持久性:新订阅者连上来时,能不能拿到之前发的数据
  • 时限:多久没收到新数据算异常
  • 生命周期:一条数据多久之后算过期

unitree_sdk2 的头文件里,QoS 相关的封装占了好几个文件,可见它不是个可有可无的角色。

为什么机器人需要这么细的控制?举两个反向的例子:

关节控制指令,要的是"快,丢了就丢了"。 控制循环每毫秒发一次新指令,如果某一帧因为网络抖动没送到,正确的做法是让下一帧覆盖过去。你绝不希望通信层"贴心地"把那条过期指令重发一遍——一条来自 5 毫秒前的指令,此刻执行出来就是一次抽搐。

机器人的配置参数或地图数据,要的是"新来的也得看到"。 一个进程晚启动了三十秒,它需要能立刻拿到当前的地图,而不是干等下一次全量广播。

这两种需求在 MQTT 那种全局统一 QoS 的模型下很难同时满足,在 DDS 里只是两条话题配不同策略的事。这才是机器人选 DDS 的核心理由:不是它更快,而是它允许同一套通信系统里,不同数据用完全不同的可靠性规则。

仓库的示例里还有一个 multi_domain_id.py,对应 DDS 的另一个概念——Domain ID。同一个网络里,不同 Domain 的节点互相看不见。这是逻辑隔离手段:一个实验室里同时调三台机器人,各自分配不同的 Domain ID,就不会互相串话。

那 MQTT 就没用了吗

当然不是。它们的定位根本不同:

DDS MQTT
架构 去中心化,节点直连 中心化,Broker 中转
典型场景 单机/局域网内的实时系统 跨网络、跨公网的设备接入
QoS 每话题独立、多维度策略 全局三档
上手成本

DDS 擅长的是机器人体内——十几个进程在一台机器或一个局域网里高频交换数据。MQTT 擅长的是机器人和外界——设备穿过公网连到云平台上,网络不稳、带宽有限、要考虑断线重连。

真实系统里两者经常并存:机器人内部走 DDS,需要上云的部分再由一个网关进程转成 MQTT 发出去。如果你在做 ESP32 的物联网项目,MQTT 依然是那个正确答案——DDS 那套发现机制和 QoS 引擎,对 MCU 来说太重了。

三个可以带走的结论

第一,看到 rt/ 前缀,就知道它在跟 ROS 2 对齐。 通信协议的兼容性选择往往藏在这种不起眼的命名约定里,这是判断一套系统开放程度的快速方法。

第二,看一个系统的消息定义清单,比看它的 API 文档更快理解它。 消息类型是系统的"词汇表"——它能说什么、听什么,全在这张表里。拿到一套陌生的机器人系统,先找它的 IDL 或消息定义目录。

第三,Cmd/State 成对、Low/High 分层,这两个模式在机器人系统里反复出现。 认出它们,你就能快速定位一套陌生 SDK 里"我该往哪发指令、从哪读状态"。

关于低层和高层这条分界线到底怎么划、什么时候该用哪个,我们在这一卷后面的文章里接着展开。想动手实践通信这件事的话,机器人与具身智能卷里有从 ESP32 起步的 MQTT 实践,先把发布订阅的思维方式建立起来,再回头看 DDS 会顺很多。

📄 来源 / 自校链接

本文为公开资料整理,非亲测。关键参数与代码请结合实物与下列官方来源验证。

内容有错、看不懂、或想看下一期?告诉我们 →

本文为公开资料的学习整理,非亲测。涉接线/花钱/合规的步骤请结合实物与官方最新资料验证,风险自负。见免责声明