为什么双足行走如此困难

在所有人形机器人的演示里,最抓人的往往不是抓取也不是对话,而是稳稳地走起来。让两条腿交替支撑一个一米多高的身体,看上去是人类三岁就会的事,工程上却是控制领域最难的问题之一。难点不在机械加工,而在动力学:双足机器人在任何瞬间只有一只脚或两只脚着地,支撑面小得可怜,身体重心一旦越出支撑区域,重力会迅速把它拉倒。

和轮式、四足平台相比,双足系统的困难集中在三点。第一是欠驱动:腿部的关节数远多于受控自由度,机器人无法像机械臂那样直接指定末端位置。第二是接触的不连续:脚每落地一次,动力学方程就切换一次,冲击与摩擦都发生在这个瞬间。第三是强耦合:摆动手臂会改变躯干姿态,躯干姿态又反过来影响落脚点,任何一处调整都会牵动全身。

因此,双足行走从来不是「管好一条腿」这么简单,而是一套分层的系统工程:先用简化模型弄清楚身体该往哪走,再规划脚该落在哪里,最后用全身控制把几十个关节的力矩算出来并按时下发。

把双足机器人想成一根被手指顶着的长杆:手指必须不停微调位置,长杆才能保持直立。行走,就是这根长杆主动把「手指」挪到前方再落地。
本文核心知识点
  • 欠驱动双足机器人靠脚底接触获得支撑,本质上是不稳定、欠驱动的系统。
  • 线性倒立摆把全身质量简化为一个质心,得到可解析、可控的水平动力学。
  • ZMP零矩点必须落在支撑多边形内部,机器人才不会翻倒。
  • 全身控制用带约束的二次规划同时求解平衡、姿态与手臂任务。
蓝色扁平科技插画:一台白色人形机器人迈步行走,脚下有由发光圆点连成的步态轨迹与半透明支撑多边形轮廓
双足行走的本质是不断把质心「送」到下一个落脚点上方,步态轨迹、支撑面与平衡参考线共同决定了它不会摔倒。

倒立摆模型:把身体简化为质心

要控制一个几十自由度的系统,第一步是降维。工程上最常用的简化是线性倒立摆模型,也就是把全身质量集中到一个质心点上,把双腿看成一根没有质量、长度可伸缩的杆:杆的一端是质心,另一端踩在地面上。这个模型丢掉了关节细节,却抓住了行走最关键的部分——质心的水平运动。

在该模型中,质心高度固定为 z,水平方向的动力学可以写成一个二阶微分方程,系数由自然频率 ω 决定,而 ω 等于重力加速度除以质心高度后的平方根。ω 越大,系统越「急」,身体被拉回支撑点的速度越快。以身高约 1.7 米、质心高度约 0.85 米的机器人为例,ω 大致落在 3.4 弧度每秒附近。

下面这段代码用最朴素的前向欧拉法推进倒立摆模型,观察不加控制时质心位移会怎样发散:

import numpy as np

# 线性倒立摆:质心高度 zc,重力加速度 g
g, zc, dt = 9.81, 0.85, 0.01
w = np.sqrt(g / zc)          # 自然频率,约 3.4 rad/s

def step_lipm(state, ux):
    # state = [x, vx];ux 为质心水平加速度指令
    x, vx = state
    ax = w * w * (x - ux)
    return np.array([x + vx * dt, vx + ax * dt])

state = np.array([0.0, 0.30])          # 初值:前向 0.30 m/s
traj = []
for k in range(200):
    state = step_lipm(state, 0.0)      # 不给控制,观察发散
    traj.append(state[0])
print("第 2 秒质心位移:", round(traj[-1], 3))

跑一遍就会看到,只要质心稍微偏离支撑点,位移会在两秒内迅速放大——这正是「倒立摆不稳定」的数值证据,也是每一步都必须主动规划落脚点的根本原因。反过来说,只要能把落脚点选在合适的位置,让质心在两次落脚之间自然摆动,就能用很小的控制力维持行走。

ZMP 与步态规划

落脚点定在哪里,需要一个稳定的判据。零矩点给出的答案最经典:它是地面上使机器人整体力矩水平分量为零的那个点。只要零矩点始终落在支撑多边形内部,机器人在数学上就不会绕支撑边界翻倒。把零矩点当作「必须被守在脚下的点」,是理解整个步态规划流程的钥匙。

规划时,工程师先由期望行走速度反解出质心轨迹,再由质心轨迹推出对应的零矩点参考,然后用预观控制让实际零矩点跟踪参考值:控制器提前若干个控制周期读取未来的零矩点参考,据此算出当前的质心修正量。这也是经典的平板小车模型解法,长期被工业界直接用于双足平台的行走控制。

步态参数含义典型取值
质心高度决定自然频率与行走节奏0.8–1.0 米
步长单步前进的距离0.4–0.8 米
步频每秒迈步的次数1.2–1.8 次
双支撑期占比双脚同时着地的时间比例15%–30%
落脚点容差允许的落点偏差正负 2–5 厘米

步态规划的另一半是时间规划:每一步的抬脚高度、摆动时长、落地时刻都要与实际关节的能力匹配。迈得太快,关节跟不上摆动轨迹,直接变成绊脚;迈得太慢,单腿支撑期过长,平衡控制的余量被一点点吃掉。经验做法是先把步频定在每秒 1.5 步左右,再逐步放宽到机器人能稳定承受的极限。

平衡控制:从状态估计到全身控制

规划给出的是「理想身体」,真实机器人还要面对摩擦、延迟和外部扰动。平衡控制的第一环是状态估计:把惯性测量单元的角速度与加速度、关节编码器的位置,以及足底力传感器的读数融合起来,估计出躯干姿态与质心速度。互补滤波与卡尔曼滤波是两种最常见的做法,前者实现简单,后者在噪声建模准确时更优。

第二环是分层控制。上层任务层把「以 0.5 米每秒前进」翻译成质心参考与落脚点序列;中层用模型预测控制滚动优化未来一小段时间的输入;底层则是全身控制:把所有任务写成带优先级或带权重的约束,用一个二次规划同时求解,既满足关节力矩上限与摩擦锥约束,又尽量完成平衡、姿态与手部任务。

def control_loop(robot, planner, wbc, dt=0.001):
    while robot.running:
        t = robot.time()
        # 1. 状态估计:融合 IMU 与关节编码器
        est = robot.estimate_state()
        # 2. 任务层:由速度指令生成质心与落脚点参考
        ref = planner.update(est, robot.command)
        # 3. 全身控制:求解带约束的二次规划
        tau = wbc.solve(est, ref)
        # 4. 下发关节力矩,等待下一个控制周期
        robot.send_torque(tau)
        robot.sleep_until(t + dt)

这套分层结构的价值在于可解释:平衡变差时,可以依次检查状态估计是否漂移、模型预测的时域是否过短、二次规划的约束是否过紧,而不是面对一个端到端网络无从下手。工程上常用的调参顺序也是从下往上:先把状态估计调准,再放开控制层的约束。

仿真环境与训练流程

真实的双足机器人摔一次代价很高,所以大部分策略先在仿真里练。MuJoCo、Isaac Sim、PyBullet 是三种主流选择:第一种以接触求解精度见长,第二种胜在渲染质量与大规模并行,第三种则以轻量和易上手著称。无论选哪一个,域随机化都是缩小「仿真到真机」差距的关键手段——随机化摩擦系数、连杆质量、传感器噪声与执行延迟,让策略学会适应不确定的环境。

scene:
  robot: unitree_g1
  terrain: flat
  gravity: 9.81
sim:
  timestep: 0.001
  solver_iterations: 100
  render: false
train:
  algorithm: ppo
  num_envs: 4096
  episode_len: 1000
  randomize:
    friction: [0.6, 1.2]
    mass_scale: [0.9, 1.1]
    latency_ms: [0, 20]
reward:
  alive: 1.0
  track_lin_vel: 1.5
  feet_air_time: 0.4
  torque_penalty: -0.0001

强化学习的训练目标由一个奖励函数决定,常用的几项包括:

  • 存活奖励:每个控制周期都给一个小正数,鼓励机器人尽量不摔倒。
  • 速度跟踪:奖励实际线速度与指令速度的接近程度,这是「会走」的核心。
  • 步态周期:奖励两脚交替摆动的时间均衡,防止机器人拖着一只脚前进。
  • 能耗与力矩惩罚:抑制大幅抖动,让动作更平滑也更省电。

训练得到的策略通常输出关节目标位置或力矩,再经过一层滤波后下发。值得注意的是,仿真里表现完美的策略,在真机上仍可能因为延迟与摩擦建模误差而失效,因此域随机化的区间往往要覆盖到比预期更极端的参数范围。