四足机器人是怎么走路的?步态、支撑相与摆动相讲透
- 用支撑相与摆动相这一对概念,把「走路」拆成可以被工程化的子问题
- 理解相位差与占空比如何共同决定步态类型,以及静态稳定与动态稳定的分界线
- 分清摆动腿的位置问题和支撑腿的力分配问题,知道为什么足式控制里总有一个二次规划求解器
先做一件很奇怪的事:站起来,慢慢走两步,同时留意自己的重心。
你会发现一件事——从抬起一只脚到那只脚落地的这段时间里,你的身体其实是朝前倒的。不是走过去的,是倒过去,然后用落下的那只脚接住自己。人类走路的本质是一连串被接住的跌倒。
这个观察不是玩梗,它是足式机器人控制里最重要的一条直觉。轮式机器人的问题是"往哪转、转多快",一个连续的、时时刻刻都成立的控制问题;而足式机器人的问题是"抬哪条腿、什么时候抬、抬多高、落在哪",四个离散决策环环相扣,中间还夹着一段身体没有支撑的时间。
这篇把这套最底层的概念讲透。读完你应该能看懂任何一篇足式论文里的相位图,也能看懂强化学习学出来的步态到底"少做"了什么。按惯例交代边界:本文以足式运动的通用原理为主体,代码结构部分参照公开开源仓库 unitree_guide 的目录与文件命名,我没有真机验证,也不会有任何实测描述。
难点到底在哪
把"让四条腿协调地走路"这句话拆开,会掉出四个彼此纠缠的问题。
第一,重心必须被管住。 脚踩在地上的那几个点,连起来会围出一个多边形,叫支撑多边形。如果重心在地面上的投影落在这个多边形里面,机器人就是稳的——你把它按暂停键,它也不会倒。一旦投影跑到多边形外面,重力就会产生一个把身体推翻的力矩。
第二,抬起哪条腿是有代价的。 每抬起一条腿,支撑多边形就缩小一次。四条腿全着地时支撑区域最大,抬起一条变成三角形,抬起两条就退化成一条线段——一条线段没有面积,重心投影几乎不可能"落在里面",这时候静态稳定这个概念直接失效了。
第三,时机决定了一切。 同样是抬右前腿,在身体重心正好偏向左后方时抬,和在重心还没转移过去时抬,结果完全不同。腿的动作必须和身体的运动在时间上对上。
第四,落在哪里不是随便定的。 落脚点既决定了下一段支撑多边形的形状,也决定了这条腿能给身体施加什么方向的力。它同时是"稳定性问题"和"速度控制问题"。
这四件事无法分开求解——抬腿改变支撑区域,支撑区域限制重心能走到哪,重心位置又决定了下一步该落在哪。整个足式控制领域,本质上就是在用不同的方法处理这个耦合。
两个基本状态:支撑相与摆动相
好在,有一个极其简洁的分解方式,让这团乱麻变得可编程。
任何一条腿,在任何时刻,只处于两个状态之一。
支撑相(stance):脚踩在地上,承担身体的重量。这条腿在物理上被地面"钉住"了——你没法命令它移动到别的位置去。你能做的只有一件事:控制它推地的力有多大、朝哪个方向。而且推地的目的不是让这条腿动,是让身体动。牛顿第三定律,脚往后下方蹬地,地面就往前上方推身体。
摆动相(swing):脚离开地面,在空中向前摆到下一个落脚点。这条腿不承重,不受地面约束,是自由的。你要做的是让它的足端沿一条轨迹从起点飞到终点——这是一个纯粹的位置跟踪问题。
每条腿都在这两个状态之间无限循环:支撑、摆动、支撑、摆动。一条腿走完一次"支撑+摆动",叫一个步态周期。
而步态,就是四条腿的相位安排。 就这么简单——所谓走路,就是给四条腿的循环安排一个错开的节奏。
这个二分的威力在于,它把一个混沌的整体问题,切成了两类性质完全不同、可以分别求解的子问题:摆动腿解位置,支撑腿解力。这一刀切下去之后,剩下的一切都好办了。
相位差:一组数字定义所有步态
给每条腿一个从 0 到 1 的相位变量,表示它在当前周期里走到了百分之多少。四条腿之间的相位差,决定了这是什么步态。
对角小跑(trot):左前和右后同相位,右前和左后同相位,两组之间相差二分之一个周期。你看机器狗小跑,永远是对角线上的两条腿同时抬起、同时落地。为什么用对角?因为对角的两只脚连成的线段穿过身体中心附近,绕这条线的翻倒力矩最小——这是四足动物在中速下几乎一致的选择,机器人也照抄了。
慢走(walk):四条腿依次相差四分之一个周期,任何时刻都只有一条腿在空中,另外三条撑着一个三角形。这是速度最慢但最稳的步态。
跳跃类(bound / pronk):前后腿分组。bound 是两条前腿同相、两条后腿同相,两组错开——像兔子那样弓身跳;pronk 是四条腿完全同相,一起蹬地一起落地,四脚同时腾空。
注意一件事:换步态在代码里往往不是换算法,而是换一组参数。周期长度、占空比、四个相位偏移量——把这几个数一改,同一套控制流水线就能产生完全不同的运动模式。这解释了为什么开源实现里那个模块叫「波形发生器」而不是「小跑控制器」。
占空比:静态稳定与动态稳定的分水岭
相位差决定腿之间怎么错开,还有一个量决定每条腿"在地上待多久":占空比(duty factor),即支撑相占整个周期的比例。
占空比是理解步态最关键的一个量,因为它直接决定了同一时刻有几条腿在地上。
占空比大于四分之三时,任何时刻至少有三条腿着地。支撑多边形始终是一个有面积的三角形,只要控制得当,重心投影可以一直待在里面。这类步态是静态稳定的——你在任意一个时刻把整个系统冻结,机器人都不会倒。
占空比等于二分之一左右时,比如典型的对角小跑,同一时刻只有两条腿着地。支撑区域退化成一条线段。这时候静态稳定已经不存在了:重心投影落在一条没有宽度的线上是不现实的,机器人在绕这条线的方向上时时刻刻都在倒。它之所以没倒,是因为下一对腿马上就落地接住了它。
占空比小于二分之一时,会出现四条腿全在空中的腾空期。这时候身体是一个自由飞行的抛体,除了重力没有任何外力可以施加。控制器唯一能做的是在腾空前就把状态准备好,并规划好落地。
所以这条分界线是这样的:
占空比 > 3/4 → 多腿支撑,支撑多边形有面积 → 静态稳定,慢
占空比 ≈ 1/2 → 两腿支撑,支撑区退化成线段 → 动态稳定,中速
占空比 < 1/2 → 存在腾空期,无支撑 → 纯动态,快
静态稳定 vs 动态稳定,是理解足式运动的真正分水岭。
静态稳定的思路是:我保证在每一个瞬间都不倒。它的代价是慢——你必须等重心慢慢转移到新的支撑区域里,才敢抬下一条腿。早期的六足、八足机器人大多走这条路,画面上看起来非常"机械",一步一顿。
动态稳定的思路是:我允许自己在某些瞬间是不稳的,但我保证在下一个落脚发生之前把状态纠正回来。它的代价是你必须一直在控制——一旦控制回路中断(掉电、算法崩溃、状态估计跑飞),机器人立刻摔倒,没有任何缓冲。
用一句可能有点吓人但很准确的话总结:动态稳定的步态,本质上是一场受控的持续跌倒。
这句话也解释了为什么足式机器人对状态估计的依赖如此致命。静态稳定的机器人估计错了几厘米,无非是走歪一点;动态稳定的机器人估计错了速度,落脚点就会算错,下一步接不住自己,就是摔。
摆动腿要解决的两个问题
回到摆动相。这条腿在空中,你要回答两个问题。
落脚点:它其实是速度控制器
直觉上你会觉得"往前走就把脚往前放"。但落脚点真正决定的不是位置,而是下一步的速度。
想想开头那个"被接住的跌倒"。身体正在往前倒,脚落在重心投影前面,相当于伸出一根撑杆去挡住这次跌倒——产生一个减速的效果;落在后面,则挡不住,身体继续加速前倾。
所以经典的落脚点计算思路是:根据当前速度和期望速度的差来调整落脚位置。走得比想要的快,就把脚往前多放一点来"刹车";走得太慢,脚就往后收一点让身体继续往前倒。这个思路在足式领域有一系列成熟的公式,核心结构都是"名义落点 + 一个正比于速度误差的修正项"。
这是经典控制里非常"人设计出来"的一环:有明确的物理含义,可以推导,可以画图,出了问题能定位到具体系数。在开源实现里,这一环对应的是 Gait/FeetEndCal.h(足端落点计算)这类模块。
摆动轨迹:抬多高,怎么落
知道了终点,还要规划从起点到终点的这条空中轨迹。它有三个硬要求:
要抬得起来。 足端必须离地足够高,否则地面上一点起伏就会绊到。抬得越高越安全,但同样越费能量、越占时间。
要平滑。 轨迹如果有折角,逆运动学反解出的关节角就会有速度突变,关节会剧烈抖动甚至报错。
落地速度要接近零。 如果脚带着明显的竖直速度砸下去,冲击会通过腿传到身体上,既伤关节电机也让状态估计的加速度读数产生尖峰。理想的落地是"贴上去"而不是"砸下去"。
常用的做法是摆线或贝塞尔曲线。摆线的好处是天然满足起点和终点处速度为零;贝塞尔曲线的好处是控制点直观——你可以通过挪动控制点来调抬腿高度和落地角度,而曲线的平滑性由构造本身保证。两者都是几行公式的事,工程上很省心。
算出足端的期望位置之后,用逆运动学反解出这条腿三个关节的角度,交给关节层去跟踪。跟踪的实现通常就是 PD 控制,也就是PID 那篇里讲的那套东西在关节空间的直接应用。
支撑腿要解决的问题:力怎么分
摆动腿好办,支撑腿才是四足控制里真正有难度的地方。
假设此刻有三条腿在地上。控制器已经算出身体需要一个什么样的合力(把重心推向期望速度)和合力矩(把身体姿态摆正)。现在的问题是:这个合力合力矩,怎么分配到这三条腿上?
这个问题通常没有唯一解。 三条腿,每条腿出一个三维力,一共九个未知数;而约束只有六个方程(三个力分量 + 三个力矩分量)。九个未知数六个方程,解空间是无穷的。
更麻烦的是,这些解还不能随便挑,因为存在硬性的物理约束:
腿只能推不能拉。 脚和地面之间没有胶水,法向力必须是压力。算出一个"往上拉地面"的解,物理上根本不可能执行。
摩擦锥约束。 切向力不能超过法向力乘以摩擦系数,否则脚会打滑。这个约束在数学上是一个以法向为轴的圆锥——力向量必须待在锥内。工程上常把圆锥近似成一个多面锥,因为多面锥是线性约束,好求解得多。
力矩上限。 关节电机能输出的力矩是有限的,算出来的足端力必须是电机真给得出来的。
于是问题变成了:在一组线性约束下,找一组让某个二次代价函数最小的解。
这正好是二次规划(QP)的标准形式。代价函数通常写成"实际产生的合力合力矩与期望值的偏差"加上一些正则项(比如让各腿受力尽量均匀、让力的变化尽量平滑)。
这就是为什么你打开任何一个正经的足式控制项目,几乎都能在依赖里翻出一个二次规划求解器。unitree_guide 那个只有几十个文件的教学仓库,第三方依赖里就躺着一个 quadProgpp。一个教学项目专门引入 QP 求解器,指向的就是这件事。
顺带说明一个容易混淆的点:这里的 QP 和 MPC 不是一回事。这里解的是"当前这一拍,力怎么分";MPC 解的是"未来若干拍,身体的轨迹和力序列该是什么",规模大得多,但底层往往也归结成一个 QP。两条路线的对照在unitree_guide 的控制实现那篇里有完整展开。
代码结构上的印证
上面讲的三件事——相位、落脚点、支撑摆动的分工——在开源实现里有非常直白的对应。unitree_guide 的 include/Gait/ 目录下只有三个文件:
WaveGenerator.h —— 相位波形发生器
GaitGenerator.h —— 步态生成
FeetEndCal.h —— 足端落点计算
一条流水线:相位 → 支撑/摆动划分 → 落脚点。三个文件对应本文的三节,顺序都一样。这不是巧合,是这套概念本身的结构。
另一处印证在状态机目录里。include/FSM/ 下除了 State_Trotting.h(小跑)这样的实际运动状态,还有 State_SwingTest.h 和 State_StepTest.h 这类单项测试状态。
这两个状态在正常运行的机器人上没有任何用处——它们是调试脚手架。它们的存在说明作者很清楚:小跑状态里同时跑着相位、摆动轨迹、状态估计、力分配四样东西,一旦出问题你无从下手。所以要先单独验证摆动腿的轨迹和逆运动学对不对,再单独验证踏步时的支撑切换对不对,最后才合起来。
把"分步验证"做进架构里,这是足式开发里最实用的一条工程经验,跟具体用哪套算法无关。
从这里看强化学习的步态
现在带着这套概念去看强化学习学出来的步态,会有几个明显的不同。
相位不再显式存在。 RL 策略的输入是观测(姿态、角速度、关节状态、指令速度等),输出直接是关节目标。中间没有一个变量叫"相位",没有一个模块叫"落脚点计算"。这些结构(如果存在的话)以某种形式隐含在网络权重里。有些训练方案会在观测里塞进一个人为设计的周期信号来引导策略产生周期性动作,但那是引导,不是执行。
步态可以是连续变化的。 经典实现里,walk 和 trot 是两组不同的参数,切换是离散的。而学出来的策略往往会随速度指令平滑地改变占空比和相位关系——低速时腿在地上待久一点,高速时自动变得更像小跑。它不需要你去定义这些模式,因为奖励函数只要求"跟上指令速度且别摔"。
代价是你失去了中间量。 经典路线里,速度不对你去查落脚点公式,姿态抖你去查 PD 增益,打滑你去查摩擦锥约束——问题可以被定位到具体环节。RL 路线里,策略输出一个奇怪的动作,你没有中间变量可查,只能改奖励再训一遍。
我不给这两条路排名次,它们付出的代价在不同的地方。但学习顺序是有讲究的:先搞懂支撑相、摆动相、相位、占空比、落脚点、力分配这套概念,你才知道那个策略网络到底替你解决了什么、又把什么变成了黑箱。
三句话带走
走路的最小分解是支撑相与摆动相,步态就是四条腿的相位安排。 相位差决定是 trot 还是 walk 还是 bound,占空比决定同一时刻有几条腿着地。
静态稳定和动态稳定是分水岭。 占空比大就静态稳定,慢但停下来也不倒;占空比小就必须动态平衡,快但本质是受控的持续跌倒——控制回路一断就摔。
摆动腿解位置,支撑腿解力。 摆动腿走轨迹规划加逆运动学,落脚点其实是速度控制器;支撑腿走带约束的力分配,摩擦锥和单向受力让它成了一个二次规划问题。
想看这套概念在真实控制器里怎么串成一条数据流,接着读unitree_guide 的控制实现;想补齐更底层的地基——电机、PID、运动学——可以先回机器人与具身智能卷走一遍。