人形机器人怎么保持平衡?从支撑多边形到全身控制
- 说清人形机器人在平衡上比四足难的四个结构性原因
- 分清支撑多边形、ZMP、捕获点各自的适用边界与失效条件
- 理解全身控制为什么是人形绕不开的一层,以及角动量在其中的作用
如果你把一台四足机器人和一台人形机器人并排放在地上,然后关掉它们的电机,会看到两种完全不同的结果:四足会趴下去,姿态不太优雅但基本停在原地;人形会直接倒。
这个差别不是工艺水平的差别,是构型带来的物理差别。人形机器人在所有常见机器人形态里,平衡是最难的那一个——它的难不是难在算法不够聪明,而是难在留给算法的余量本来就少。
这篇讲清楚为什么难,以及这个领域这些年发展出来的几条主要思路各自在解决什么问题。ZMP、捕获点、全身控制这些都是公开的通用方法,不属于任何一家的实现,我讲原理;涉及代码的地方,我会拿开源仓库的目录结构做参照,说明这些概念在工程上大致落在哪个位置。按惯例说明一句:本文基于公开资料与开源仓库的代码结构写成,没有真机验证,不会出现任何实测描述。
一、人形到底难在哪
把四足和人形放在一起对比,差别集中在四条上。想理解后面所有方法为什么长成那个样子,得先把这四条捋清楚。
支撑面积小得多
四足站立时,四只脚在地面上围出一个近似矩形的区域。人形只有两只脚,它俩的接触面加起来围出的支撑区域,无论怎么算都比四足那个矩形小一大截。而且形状很不友好——两只脚前后方向长、左右方向窄,横向的余量尤其小。
支撑区域小意味着什么?意味着允许身体偏离的空间小。同样一个扰动,四足偏出去还在支撑区里,人形已经出界了。
重心高,反应时间短
人形的重心位置远高于四足。这一点用倒立摆模型看最直观:把机器人简化成一根底部铰接、顶端有质量的杆,杆越长,一旦偏离竖直位置,倒下去的过程反而越"慢",但它对应的时间常数、也就是系统自然发散的节奏,和摆长直接相关。
这里的关键结论不是某个具体秒数(我不会给数字,那取决于具体构型),而是:倒立摆的失稳是指数发散的。偏离越大,恢复力矩需求增长越快。控制器必须在偏离还很小的时候就介入,一旦错过窗口,后面无论出多大力都拉不回来。四足因为重心低、支撑广,这个窗口宽得多;人形的窗口窄,对控制频率、对状态估计的延迟都更敏感。
顺带说一句,如果你想用最低成本亲手感受一次倒立摆控制的手感,两轮自平衡车是最短的路径——它就是一个单自由度的倒立摆,你在上面踩过的坑,在人形上会以放大若干倍的形式重现。
单支撑相:一半时间只有一只脚
站着还只是静态问题,走起来才是真正的麻烦。
行走过程天然分成两个阶段:双脚都着地的双支撑相,和只有一只脚着地的单支撑相。在单支撑相,支撑区域从"两只脚围出来的区域"退化成"一只脚的鞋底"。这个区域小到什么程度?小到你几乎不可能把重心的地面投影稳稳压在里面还留有余量。
也就是说,人形走路的大部分时间里,它在静态意义上是不稳定的。它没有倒下去,靠的不是"站得稳",而是"下一步及时接住了自己"。这一点是理解现代人形平衡方法的钥匙,后面还会回来说。
四足则不同。四足的常见步态里,对角小跑(trot)之类的节律虽然也只有两条腿着地,但那两条腿是对角分布的,支撑线跨过身体中心;更保守的爬行步态甚至能保证任何时刻都有三只脚着地,支撑区域始终是个三角形。四足有"退回静稳定"这条后路,人形没有。
自由度多,而且互相耦合
人形的自由度数量远超四足,多出来的部分主要在上半身:腰、肩、肘、腕。这些自由度不是白给的,它们会实实在在地影响平衡。
你甩一下手臂,身体会朝反方向有个微小的扭转反应——角动量守恒。四足没有需要大幅摆动的上肢,这类耦合弱得多;人形只要一动手,平衡控制器就得把这笔账算进去。这既是负担,也是资源,后面讲全身控制时会看到,手臂恰恰是人形手里一件很好用的平衡工具。
二、第一层:支撑多边形与静态稳定
最古老也最直观的判据是这一条:
把所有着地点在地面上连成一个凸多边形(支撑多边形),只要重心在地面的垂直投影落在这个多边形内部,机器人静止时就不会倒。
道理很简单。重力作用线落在支撑区内,地面反作用力总能配出一个平衡的分布;一旦投影跑到多边形外,重力对边缘的力矩就没有东西能抵消,机器人会绕着那条边翻过去。
这条判据的价值在于它给了一个几何化的、可以直接看图判断的安全条件。早期的静态步行机器人就是照这个做的:抬腿之前先把重心挪到剩下那些脚围成的区域内部,确认安全了再抬,慢慢挪,一步一停。
它的问题也同样明显:这条判据只对静止(或极慢运动)成立。
因为它完全忽略了惯性。机器人一旦有加速度,除了重力还有惯性力参与,"重心投影在支撑区内"既不充分也不必要。你可以很容易举出反例:一个人快速前冲时,重心投影早就跑到前脚之外了,但他没倒;反过来,一个人站着不动却猛地转动上半身,重心投影还在脚下,脚却可能被扭得打滑抬起。
所以支撑多边形是地基,不是答案。
三、第二层:ZMP,把惯性算进去
零力矩点(Zero Moment Point,ZMP)是为了把惯性纳进来而提出的概念,它是双足行走理论里流传最广的一个。
它的物理含义
机器人踩在地上,地面对脚底的反作用力不是集中在一个点,而是分布在整个接触面上的一片压力。这片分布的力,可以等效成作用在某一个点上的一个合力加一个力矩。
ZMP 就是那个特殊的点:在这一点上,等效力矩的水平分量为零。 换句话说,把地面反力全部压缩到这一点上表示时,它不会额外产生让脚"绕水平轴翻转"的力矩。
判据随之而来:
ZMP 落在支撑多边形内部,脚就不会绕着边缘翻转。
注意这和静态判据的区别。静态判据看的是重心投影,只考虑重力;ZMP 是由重力、惯性力、角动量变化率共同决定的,它把运动状态算进去了。机器人静止时,ZMP 恰好退化成重心投影——静态判据是它的特例。
经典的 ZMP 行走怎么做
有了判据,规划路线就很自然:
1. 先定落脚点 —— 决定每一步踩在哪,支撑多边形序列就确定了
2. 规划 ZMP 轨迹 —— 让 ZMP 在每个支撑多边形内平滑移动(通常沿脚心走)
3. 反推重心轨迹 —— 用简化模型(线性倒立摆之类)由 ZMP 反解质心该怎么动
4. 解逆运动学 —— 由质心轨迹和落脚点,算出每个关节该转到什么角度
5. 关节位置跟踪 —— 交给关节层的位置环去执行
这套流程的工程属性非常好:每一步都是确定性的,可以离线算,可以调试,可以画图看,出问题也容易定位是哪一环。很长一段时间里,能稳定行走的人形机器人基本都走这条路。
它的局限在哪
ZMP 判据成立有前提,而这些前提相当苛刻:
- 脚必须是平面接触,且整个脚掌贴地。 脚只有边缘着地、或者站在碎石上时,"支撑多边形"本身就不好定义了。
- 不能打滑。 ZMP 只约束翻转,不约束滑动。地面摩擦不够,机器人会以另一种方式失稳,判据完全看不到。
- 地面得是平的。 ZMP 的经典表述建立在水平地面上,斜坡、台阶、软地面都需要额外处理。
- 它是个"不翻倒"判据,不是"走得好"判据。 满足 ZMP 只保证脚不掀起来,不保证姿态自然。
最后一条是很多人对经典 ZMP 步态的直观印象来源:为了让 ZMP 稳稳待在支撑区里,规划出来的步态往往屈膝、小步、重心走直线、上身端着不动。看起来像一个人小心翼翼地端着一碗满的汤在走路。这不是审美问题——屈膝是为了避免膝关节伸直时的奇异位形,小步是为了让每个支撑多边形的切换更平缓,都是被判据逼出来的。
四、第三层:捕获点,允许自己往前倒
更现代的思路承认了一件事:人走路本来就不满足 ZMP 那种"始终稳稳压住"的条件。
回到前面说的单支撑相。人在走路的大半时间里,重心其实是在往前"倒"的,只不过每一次倒到一定程度,另一条腿及时迈出去接住了。行走不是一连串静态平衡姿态的串联,而是一连串受控的跌倒。
这条思路的形式化表达就是捕获点(capture point)——大致可以理解为:如果机器人现在的运动状态保持不变,那么它应该把脚落在地面的哪个位置,才能刚好把动能耗完、把自己停下来。
由此得到的策略完全不同:
- 不再要求 ZMP 时刻在支撑区内;
- 而是不断计算"要接住当前状态,脚该落在哪";
- 然后调整落脚点去接住它。
它带来两个直接好处。第一,步态自然多了——身体允许前倾,步幅可以放开,不用一直屈膝端着。第二,抗扰动能力质变。ZMP 路线遇到一个大的推力时,可用的手段只有在支撑区内挪 ZMP,余量很快就用完了;捕获点路线的手段是"多迈一步",而迈步这件事的余量大得多。
这也解释了一个每个人都有的日常体验:你被人从背后推一把,第一反应不是绷紧脚踝硬扛,而是下意识地跨出一步。 那一步落在哪里,你的小脑算的就是捕获点。你不是靠"稳住"活下来的,你是靠"迈对了那一步"活下来的。
从工程结构上也能看到这类思想的痕迹。以四足教学项目 unitree_guide 为例,它的 Gait 目录里,落脚点计算是被单独拆出来的一个模块:
unitree_guide/include/Gait/
WaveGenerator.h —— 步态相位波形,决定每条腿何时支撑、何时摆动
GaitGenerator.h —— 步态生成
FeetEndCal.h —— 足端落点计算
FeetEndCal 单独成模块这件事本身就说明问题:"脚该落在哪"是一个和"腿该怎么摆"平级的独立决策,而不是步态轨迹的附属产物。我不去替作者解释他的具体考虑,但从工程角度看,把落脚点抽出来,正是为了让它能被平衡状态实时调整——这和捕获点思路要的接口是一致的。
五、第四层:全身控制,给多个目标排优先级
到这里还有一个问题没解决:人形自由度太多,而任务不止一个。
设想机器人正在走路,同时要用手臂端着一个托盘保持水平,同时躯干要朝向前方,同时脚不能打滑。这些任务会互相冲突——手臂为了保持托盘水平做的动作,会改变身体角动量,进而影响平衡。你不能让四个模块各写各的、各自往关节上叠指令,那必然打架。
全身控制(Whole-Body Control,WBC)就是这个仲裁机制。它的基本形态是把所有事情写成一个统一的优化问题:
决策变量: 全部关节加速度 / 关节力矩 / 各接触点的地面反力
目标函数: 各项任务误差的加权和(质心轨迹、姿态、末端位置……)
约束条件: 刚体动力学方程(必须物理上成立)
关节角度 / 速度 / 力矩上限(硬件打不过去)
接触力在摩擦锥内(不能要求地面提供做不到的摩擦)
接触点速度为零(支撑脚不许滑)
每个控制周期解一次,把结果发给关节。任务之间的冲突由权重或者严格的优先级层次来裁决:平衡永远排在最高优先级,手臂的任务在不影响平衡的零空间里做——也就是说,只要有多余的自由度,就拿去做次要任务;没有多余的,次要任务就先放着。
这里最值得停一秒的是摩擦锥约束。它把"地面能提供多大的横向力"这个物理限制直接写进了优化问题。ZMP 判据看不到打滑,WBC 看得到——因为打滑在这里是一条显式约束。这是它比纯 ZMP 路线扎实的地方之一。
优化求解在代码里长什么样
WBC 是人形上的说法,但"把平衡写成一个带约束的优化问题"这个做法,在四足上同样成立,而且规模小一些,更适合拿来看结构。还是看 unitree_guide:
unitree_guide/include/control/
Estimator.h —— 状态估计:我现在是什么状态
BalanceCtrl.h —— 平衡控制:该给每只脚分配多大的力
ControlFrame.h —— 控制框架
CtrlComponents.h —— 控制组件集合
unitree_guide/include/thirdParty/quadProgpp/
QuadProg++.hh —— 二次规划求解器(随仓库一起提供)
Array.hh
两件事一目了然。第一,状态估计和平衡控制是并列的两个模块——一个回答"我在哪",一个回答"该出什么力",前者的输出是后者的输入。关于前一半,足式状态估计那篇讲得更细,尤其是"哪些量注定会漂"这个结论,直接决定了平衡控制器能信任输入到什么程度。
第二,仓库里随源码带了一个二次规划求解器。平衡控制需要在线解优化,这不是巧合——把接触力分配写成带约束的 QP,是这一类控制器的通行做法。人形的 WBC 是同一个思路的放大版:变量更多、任务更多、优先级层次更复杂,但内核都是"每个周期解一个带物理约束的优化"。
角动量:手臂是平衡工具
WBC 框架里有一项常常被忽略的能力:调节角动量。
身体的总角动量变化率,等于外力对质心的力矩。关键在于,上肢的摆动可以在不改变地面接触力的前提下改变身体的角动量分布——你甩手臂时,地面并不需要额外多推你一把。
这就给了控制器一个额外的执行器。当脚下的余量已经用尽(ZMP 顶到支撑区边缘了)、又还来不及迈步时,摆手臂、转躯干是最后一张牌。
人走钢丝时张开双臂就是这个道理。张开双臂增大了转动惯量,也让手臂的小幅摆动能产生更大的角动量修正量。杂技演员手里那根长杆是同一个原理的极端版本。人形机器人多出来的那些上肢自由度,既是平衡的麻烦制造者,也是平衡的最后一道防线,而能把这两面统一处理的框架,就是全身控制。
六、强化学习路线怎么处理平衡
上面几层有一个共同点:都要显式建模。你得写出动力学方程、定义 ZMP、设计优先级、调权重。
强化学习路线换了个思路——不显式建模平衡,让策略在海量仿真交互中自己学会。 训练时对地面摩擦、质量分布、执行器延迟、外部推力做大范围随机化,策略被逼着在各种条件下都能站住,学出来的行为里自然包含了调整落脚点、摆动手臂这些人类会做的事,只是没有人告诉它这些叫捕获点、叫角动量调节。
这条路线上有一个可观察的事实,正好佐证了人形建模确实需要额外处理。看 unitree_rl_gym 的环境目录:
legged_gym/envs/
base/legged_robot.py —— 通用足式环境基类
base/legged_robot_config.py
go2/go2_config.py —— 四足:只有配置
g1/g1_config.py
g1/g1_env.py —— 人形:配置之外还有独立的环境实现
h1/h1_config.py
h1/h1_env.py
h1_2/h1_2_config.py
h1_2/h1_2_env.py
差别很清楚:四足型号目录下只有一个 *_config.py,说明它靠调参数就能复用基类环境;而三个人形型号,每一个都额外带了 *_env.py。
这意味着人形不只是"参数不同的足式机器人",它需要覆写基类的行为——观测量怎么组织、终止条件怎么判、奖励项怎么加,都得单独写一份。至于每份 *_env.py 里具体改了什么,README 没写,我也不做推断;但"必须单独存在"这个结构性事实本身,已经说明了人形在建模上确实要多做一层功课。
顺带提一句仓库里的另一个细节:resources/robots/g1_description/ 下有一组区分不同关节配置的 URDF/MJCF 文件,包括锁腰版本和带手版本。同一台机器人,用不用腰、带不带手,动力学模型是不一样的,训练和部署都得对上号。这个对上号的动作,恰恰是平衡问题对模型敏感的一个侧面证据。
RL 路线的代价是可解释性。ZMP 步态出问题,你能画出 ZMP 轨迹看它从哪一步跑出支撑区;RL 策略出问题,你只能回去改奖励、改随机化范围,再训一遍。关于这两条路线更完整的对比,可以看模型预测控制与强化学习的路线之争,以及四足与人形在开发上的差异。
七、摔倒是必然的,工程要做的是接受它
最后说一件容易被忽略的事。
上面所有方法都有失效边界。ZMP 假设不打滑,WBC 依赖模型精度和状态估计精度,RL 策略遇到训练分布之外的情况会失效。足式机器人一定会摔——这不是水平问题,是概率问题。跑得够久,总会遇到超出设计包络的那一下。
所以工程上真正该做的,不是追求"永不摔倒",而是:
- 及时检测。 姿态角超限、失去所有接触、状态估计输出发散,都是判据。检测得越早,后面的动作越有意义。
- 主动进入保护姿态。 一旦判定必摔,与其继续硬撑到最后一刻再失控倒下,不如主动收腿、护住脆弱部位,用可控的方式落地。
- 切到安全状态。 摔倒之后立刻停止输出高力矩指令,进入一个不会自伤也不会伤人的被动状态。
这就是为什么这类控制框架里几乎一定会有一个"什么都不做"的状态。unitree_guide 的有限状态机里,State_Passive 是初始状态,按 README 的说法,控制器启动后机器人是趴在地上的,要按键才切到站立、再切到小跑:
unitree_guide/include/FSM/
FSM.h
FSMState.h
State_Passive.h —— 初始状态
State_FixedStand.h
State_FreeStand.h
State_Trotting.h
State_BalanceTest.h
State_SwingTest.h
State_StepTest.h
把"不动"设成默认,把"运动"设成需要显式切入的状态,这是一个默认安全的设计。同样地,在 RL 那条路线上,终止条件也不只是训练时用来重置环境的技术手段——它同时是在教策略"什么状态算已经失败了"。这条边界画在哪,直接决定了策略学出来的行为有多保守。
收束
回过头看这几层,它们不是相互替代,而是层层放宽假设:
支撑多边形只管静止,重心投影在里面就不倒;ZMP 把惯性算进来,让"运动中不翻脚"有了判据,代价是要求平地、不打滑,步态偏拘谨;捕获点放弃"时刻压得住",改成"倒了就迈一步接住",换来自然的步态和真正的抗扰能力;全身控制把这一切和其他任务塞进一个带优先级的优化问题,让平衡永远排第一,还顺手把手臂变成了平衡工具;强化学习则绕开显式建模,用随机化把这些能力压进一个策略里。
如果只带走三句话,我希望是这三句:
人形难在余量小。 支撑区小、重心高、一半时间单脚着地,这三条叠加,控制器可用的窗口比四足窄得多。
现代平衡的核心不是"站稳",是"接住"。 从 ZMP 到捕获点的转变,本质是承认行走就是一连串受控的跌倒。人被推一把会下意识跨一步,机器人也一样。
摔倒要当成设计输入,不是失败。 检测、保护、切到安全状态,这三件事的价值不比平衡算法本身低。
想接着往下读:平衡控制吃的是状态估计的输出,足式状态估计讲清楚了这份输入有多可靠;平衡控制吐的是每条腿的力和落点,步态基础讲清楚了这些指令是按什么节律组织起来的。