四足和人形有什么不同?从代码结构看两条产品线的真实差异
- 从 SDK 的模块命名与目录构成,看出四足与人形在控制模型上的真实分歧
- 理解人形型号为什么需要独立的手臂指令通道和显式的终止条件判断
- 掌握「改配置 vs 写代码」这条分水岭,知道加一个新机器人到底要写多少东西
问一个普通人四足机器人和人形机器人有什么不同,答案基本都是「一个四条腿,一个两条腿」。这个回答不算错,但它停在了外观上。
如果你是要基于它们写代码的人,差别会以另一种方式扑面而来:你为四足写熟的那套调用,搬到人形上会发现名字不对、模块不对、连该继承的基类都不对。等你把两条产品线的仓库结构并排摊开看完,会得到一个结论——这不是同一套软件改改参数,而是两套控制哲学。
这篇我们不谈参数、不谈价格、不谈谁更先进,只做一件事:把宇树两个公开仓库 unitree_sdk2 和 unitree_rl_gym 的目录结构横向对齐,找出四足与人形分道扬镳的地方,再解释每一处分歧背后是什么工程约束在起作用。
先把边界说清楚:本文的全部依据是这两个公开仓库的文档与代码结构,我没有真机可以验证,所以文中不会出现任何运行效果的描述。另外还有一条更要紧的自律——某个型号的目录下没有某个模块,只说明这套 SDK 没有为它暴露这项能力,不能反推「这个型号做不到」。仓库里没写的事,我们就不写。
证据一:运动模块的名字就不一样
include/unitree/robot/ 是 SDK 里放型号能力的地方,每个型号一个目录,目录下再按能力分子目录。把所有型号的运动相关模块列出来,会看到一条干净利落的分界线:
include/unitree/robot/go2/sport/ ← 四足
include/unitree/robot/b2/sport/ ← 四足
include/unitree/robot/a2/sport/ ← 四足
include/unitree/robot/as2/sport/ ← 四足
include/unitree/robot/g1/loco/ ← 人形
include/unitree/robot/h1/loco/ ← 人形
include/unitree/robot/h2/loco/ ← 人形
include/unitree/robot/r1/loco/ ← 人形
没有一个例外。四足叫 sport,人形叫 loco——locomotion 的缩写。
如果只是想统一命名,这件事成本极低:改个目录名、改几个类名,一次提交的事。它没有被统一,说明维护者认为这两个东西本来就不是同一个东西。
这个判断从指令语义上是站得住的。四足的运动指令,你大致能想象它长什么样:以某个速度朝某个方向移动、原地转身、趴下、站起、切换到某种步态。它的抽象层次是「整机作为一个刚体在地面上移动」,四条腿是实现这个移动的机构,对使用者基本是透明的。
人形不一样。两条腿走路本身就要处理步态切换与相位;上半身和下半身要协调,否则迈步时躯干会甩;手臂在很多控制方案里是参与平衡的,不是纯粹的负载。这些东西没办法藏进「朝某个方向以某个速度走」这一句话里。
我不去猜维护者当初具体是怎么权衡的,但从接口设计的角度看,硬把两者塞进同一套接口只会两边都别扭:要么四足这边被迫接受一堆用不上的参数,要么人形这边被迫把关键语义塞进注释里。命名的分歧,通常是底层控制模型真实分歧的外化。 读任何陌生代码库时,看到该统一却没统一的命名,都值得停一秒——那多半不是懒,是不能。
顺带一提,能力子目录内部的组织倒是高度一致,都是 api / client / error 三件套,这一点我们在 unitree_sdk2 架构那篇里拆过。分歧只发生在「有哪些模块」这一层,不发生在「模块怎么写」这一层——这恰恰是分层做得好的表现。
证据二:能力模块的构成完全不同
把 include/unitree/robot/ 下各型号的子目录横向摊开,差异就不止命名了:
go2 sport video vui config robot_state obstacles_avoid utrack
b2 sport front_video back_video config robot_state motion_switcher
a2 sport audio
as2 sport
g1 loco arm audio agv common/terminations
h2 loco arm common/terminations
h1 loco
r1 loco audio
四足这一侧,能力是围绕「一台会走路的移动平台」展开的:视频、语音提示界面、配置、整机状态、避障、跟随。b2 甚至给了前后两路视频 front_video 与 back_video,还多了一个 motion_switcher(运动模式切换)。这些模块的共同点是,它们都不改变「腿只用来走路」这个前提。
人形这一侧多出来的东西,性质完全不同。
arm:手臂是操作机构,需要独立通道
g1 和 h2 的目录下都有 arm,四足型号一个都没有。
这不是「人形零件多所以模块多」这么简单。四条腿在功能上是移动机构,它们服务于同一个目标——让整机位移。所以四条腿可以被一个 sport 模块整体封装起来,使用者不需要单独对某一条腿下指令。
手臂不是。手臂是操作机构,它的目标和移动目标是并列的,甚至经常是冲突的:机器人可能一边走一边举着东西,也可能站着不动只动手臂。这两件事的指令来源不同、更新频率不同、失败模式也不同。把手臂指令塞进运动模块,等于强迫使用者每次想动手臂就得走一遍运动接口——所以它必须是一条独立的指令通道。
同样的信号在示例目录里也能看到。example/g1/high_level/ 下有一批以 arm 命名的示例文件,example/h2/high_level/ 与 example/r1/high_level/ 也各有 arm 相关示例;example/g1/low_level/ 与 example/h2/low_level/ 下则有双臂示例。四足型号的示例目录里没有对应的东西。
再往下看一层,include/unitree/robot/g1/ 还有 agv,example/g1/g1d/ 下有对应的示例与高度控制示例。这些都属于「该型号的 SDK 暴露了这项能力」,我不做任何超出目录名的解读。
terminations:只有人形需要显式的终止条件
第二个只出现在人形侧的模块更有意思:
include/unitree/robot/g1/common/terminations.hpp
include/unitree/robot/h2/common/terminations.hpp
example/g1/low_level/terminations.cpp
example/h2/low_level/terminations.cpp
terminations,终止条件。它同时出现在头文件和示例里,说明这是使用者需要主动处理的东西,不是内部实现细节。四足型号的目录里没有对应文件。
这一条值得单独说一句。一台机器人需要一套显式的、可编程的终止判断,通常意味着它有一类需要被立刻叫停的状态——姿态超限、控制发散、检测到异常。对于两条腿站立的机器来说,这类状态出现得更频繁,也更不容拖延:从「姿态开始不对」到「已经倒在地上」的时间窗很短,等控制回路自己收敛回来往往来不及。
再强调一次红线:四足目录下没有 terminations,只能说明这套 SDK 没有为四足单独暴露这一项,不代表四足不做异常处理,更不代表它不需要。
顺带一个额外发现:消息定义也分了两套
include/unitree/idl/ 下的分组同样耐人寻味:
include/unitree/idl/go2/ ← 一整套
include/unitree/idl/hg/ ← 另一套
include/unitree/idl/hg_doubleimu/
include/unitree/idl/ros2/
go2 那套里有 SportModeCmd、SportModeState、LowCmd、LowState、MotorCmd、HeightMap、UwbState 这类定义;hg 那套里则出现了 HandCmd、HandState、PressSensorState、MainBoardState 这些 go2 那套里没有的类型,旁边还单独放着一个 hg_doubleimu。
也就是说,两条产品线的分歧不只发生在客户端接口层,连最底层的数据结构定义都是分开维护的。这是比接口命名更硬的证据:如果两者只是「腿的数量不同」,共用一套消息定义完全够用。
证据三:强化学习环境的分水岭是「改配置」还是「写代码」
换个仓库看。unitree_rl_gym 是宇树公开的强化学习实现,README 里写明支持的训练任务是 go2、g1、h1、h1_2——一个四足加三个人形。
它的环境目录长这样:
legged_gym/envs/base/
base_task.py
legged_robot.py
base_config.py
legged_robot_config.py
legged_gym/envs/go2/ go2_config.py
legged_gym/envs/g1/ g1_config.py g1_env.py
legged_gym/envs/h1/ h1_config.py h1_env.py
legged_gym/envs/h1_2/ h1_2_config.py h1_2_env.py
看出来了吗:go2 目录下只有一个 config,没有 env;三个人形型号则各带一个 *_env.py。
base/legged_robot.py 是足式机器人的通用环境实现。go2 什么都不用覆写,改配置就能跑起来——因为这个通用环境本来就是照着四足的形态写的,四足对它来说是「默认情况」。
人形则必须写代码。环境类要覆写的东西无非那几样:观测量怎么拼、什么情况判定回合终止、奖励函数由哪些项组成。四足的观测里没有上半身姿态这一说,终止条件也不需要盯着躯干倾角,奖励项里更不会有「别把手臂甩起来」这类约束。这些差异改不动配置文件,只能落到代码里。
这就是那条分水岭:四足是改配置,人形是写代码。 如果你想把一台自制机器人接进这套框架,它给出的路径也很清楚——先只写一个 config 试试,跑不通再补 env。注意这里的 terminations(终止条件)又一次出现了,只不过这回是以「人形要覆写环境逻辑」的形式。两个不同仓库、不同技术路线,指向了同一件事。
资产目录 resources/robots/ 里也有同样的味道。go2 那边是一个 urdf 目录加一批网格文件,结构简单直接;g1 那边则是一组用来区分不同关节配置的 URDF 与 MJCF 文件,文件名里还带着是否锁定腰部、是否带手、是否只取双臂这类修饰;h1_2 那边同样有多个变体,包括一个简化版。
同一台机器提供多份身体定义,说明训练时经常需要锁掉一部分关节来降低问题维度——只训下肢行走时把腰和手臂固定住,问题会小很多。四足没有这个需求,因为它没有可以锁掉的上半身。这一点在 unitree_rl_gym 拆解那篇里从流水线角度也提过,这里换个视角看同一个事实。
还有个细节:仓库的 deploy/deploy_mujoco/configs/ 与 deploy/deploy_real/configs/ 下放的 YAML 是 g1、h1、h1_2,deploy/pre_train/ 下的预训练模型也是这三个。部署侧这条链路,仓库当前只为这三个人形型号提供了现成配置——这是目录事实,至于原因仓库里没写,我不猜。
为什么人形更难:三个绕不过去的物理约束
上面三组证据都是代码层面的现象。往下再挖一层,它们其实指向同样几条力学约束。这部分是足式机器人的通用常识,不是从仓库里读出来的,我把它单独放在这一节,免得和前面的仓库事实混在一起。
第一,支撑多边形的大小。 一台稳定站着的机器,它的重心投影必须落在几个着地点围成的多边形内。四条腿着地时,这个多边形是个不小的四边形;两只脚站立时,它缩水成一块脚掌大小的区域。走动起来还要更糟——单脚支撑相期间,可用的支撑区域只剩一只脚。允许的重心晃动余量小了一个量级,控制回路的容错空间也就小了一个量级。
第二,静态稳定与动态稳定的分野。 四足可以采用始终保持三条腿着地的步态,任何时刻断电都能大致维持姿态,这叫静态稳定。人形行走本质上是可控的持续跌倒——身体先失衡,再靠迈出下一步接住自己。它没有「停下来就安全」这个默认状态,控制一旦中断,结果就是摔。这也从力学上解释了为什么人形侧会需要显式的终止条件判断:出问题时的正确动作不是继续控制,而是尽快进入一个受控的收场流程。
第三,自由度之间的耦合。 四足的四条腿相对独立,一条腿的摆动对整机姿态的影响有限。人形的腰、臂、腿挂在同一条运动链上,手臂一挥,角动量会实实在在地传到躯干上。这既是麻烦,也是资源——手臂可以拿来做平衡补偿。但一旦你打算用它来补偿平衡,手臂就不再是一个可以随便下指令的独立部件了,上下半身的协调必须进到同一个控制器里考虑。
回头看 sport 和 loco 这两个名字,会觉得它们分得挺自然:前者描述的是一台在地面上移动的整机,后者描述的是一套需要持续维持平衡的行走系统。如果你想补一补平衡控制的基本直觉,可以先从自平衡机器人是怎么站住的那篇入手,用两个轮子的例子把倒立摆这件事想明白,再回头看两条腿的情况会顺很多。
读完这三组证据,记住三件事
第一,接口命名的分歧是有信息量的。 sport 与 loco 的区别不是文档写法问题。当一个成熟项目在明明可以统一的地方保持了分歧,去找它不能统一的原因,往往能直接摸到设计的核心。
第二,看模块构成,不要看模块数量。 人形侧多出来的 arm 和 terminations 才是关键——前者说明操作机构需要独立于移动机构的指令通道,后者说明这类机器需要显式的异常收场路径。数量多少无所谓,多出来的是什么才重要。
第三,「改配置 vs 写代码」是判断复用边界的通用尺子。 一个框架里,能靠配置搞定的型号说明它落在基类的假设范围内;必须覆写环境的说明它超出了这个范围。你以后接手任何插件式框架,都可以用这把尺子快速估出「加一个新东西要付多少代价」。
最后再补一句克制的话:这篇全部结论来自公开仓库的目录与文档,没有真机验证,也没有任何厂商内部信息。它能告诉你的是软件层面的设计取舍,不能告诉你两条产品线在实际使用中表现如何——那需要另一类完全不同的证据。想把足式机器人的基础一步步补起来,机器人与具身智能卷里有从 ESP32 起步的完整路径,把电机、姿态、闭环控制这些东西亲手做过一遍,再回来读这些工业级仓库,很多目录一眼就能看懂它在防什么。