ESC
输入关键词搜索文章标题和内容

人形机器人步态控制从ZMP到MPC实战解析

本文由 linuxROS 整理发布,首发于 linuxros.cn,转载请注明出处。

人形机器人步态控制从ZMP到MPC实战解析

导读:人形机器人这两年火得不行,但站稳走稳始终是核心难题。本文顺着ZMP判据、LIPM建模、MPC优化、鲁棒控制、闭环仿真、ROS2部署这条链路,每段贴核心代码、标测试要点、说踩过的坑。


总体技术链路七阶段贯通

人形机器人步态控制的核心链路贯通七个阶段,先拿ZMP判据判断会不会摔,再用LIPM把复杂动力学简化为4维状态空间,接着用贝塞尔曲线规划摆动腿轨迹、用分段常数生成ZMP参考序列,然后靠MPC滚动优化未来N步的地面反力并通过QP求解,同时叠加PD+滑模修正项对抗外力扰动,最后在2D/3D闭环仿真里验证长时间稳定性和抗扰动能力,最终通过ROS2节点以100Hz频率部署到真机。下面这张图把这七个阶段串成一条完整的链路。

flowchart TB subgraph S1["基础理论"] direction TB A1["ZMP零力矩点"] A2["支撑多边形判定"] end subgraph S2["简化建模"] direction TB B1["完整人形12+DOF"] B2["LIPM 4维状态"] end subgraph S3["轨迹规划"] direction TB C1["贝塞尔摆动腿"] C2["ZMP参考序列"] C3["WALK/JOG/RUN"] end subgraph S4["控制核心"] direction TB D1["MPC预测N步"] D2["QP求解反力"] D3["摩擦锥约束"] end subgraph S5["鲁棒增强"] direction TB E1["PD基础控制"] E2["滑模修正项"] end subgraph S6["仿真验证"] direction TB F1["2D闭环仿真"] F2["3D 8维扩展"] F3["抗扰动测试"] end subgraph S7["部署落地"] direction TB G1["环境配置"] G2["ROS2 100Hz"] G3["真机部署"] end A1 --> A2 A2 --> B1 B1 -->|"简化"| B2 B2 --> C1 B2 --> C2 C1 --> C3 C2 --> D1 D1 --> D2 D2 --> D3 D1 --> E1 E1 --> E2 D2 --> F1 E2 --> F1 F1 --> F2 F1 --> F3 F2 --> G1 G1 --> G2 G2 --> G3 style A1 fill:#E3F2FD,stroke:#1976D2 style A2 fill:#E3F2FD,stroke:#1976D2 style B1 fill:#E3F2FD,stroke:#1976D2 style B2 fill:#E3F2FD,stroke:#1976D2 style C1 fill:#FFF8E1,stroke:#F57C00 style C2 fill:#FFF8E1,stroke:#F57C00 style C3 fill:#FFF8E1,stroke:#F57C00 style D1 fill:#E8F5E9,stroke:#388E3C style D2 fill:#E8F5E9,stroke:#388E3C style D3 fill:#E8F5E9,stroke:#388E3C style E1 fill:#E8F5E9,stroke:#388E3C style E2 fill:#E8F5E9,stroke:#388E3C style F1 fill:#F3E5F5,stroke:#7B1FA2 style F2 fill:#F3E5F5,stroke:#7B1FA2 style F3 fill:#F3E5F5,stroke:#7B1FA2 style G1 fill:#F3E5F5,stroke:#7B1FA2 style G2 fill:#F3E5F5,stroke:#7B1FA2 style G3 fill:#F3E5F5,stroke:#7B1FA2

步态控制核心矛盾高重心与强耦合

人形机器人有两个天然缺陷:

缺陷 表现 后果
高重心窄支撑 重心高、足底面积小 重心投影偏离足底就倾倒
多自由度强耦合 下肢6-DOF×2=12关节 抬一只脚整条链都在抖

说白了,步态控制就两件事,不摔和走得快。这俩天然矛盾,越想跑跳越容易翻车。

主流解决路线:

flowchart TB A(["人形机器人步态控制"]) --> B["路线1纯模型驱动"] A --> C["路线2数据驱动RL"] A --> D["路线3模型+约束优化"] B --> B1["ZMP判据"] B --> B2["LIPM线性倒立摆"] B --> B3["MPC模型预测控制"] B3 --> B4(["稳定行走"]) C --> C1["端到端策略网络"] C --> C2["模仿学习MoCap"] C --> C3["Domain Randomization"] C3 --> C4(["泛化行走"]) D --> D1["CBF控制屏障函数"] D --> D2["QP二次规划求解"] D --> D3["WBC全身协调控制"] D3 --> D4(["安全行走"]) style A fill:#E3F2FD,stroke:#1976D2 style B fill:#E3F2FD,stroke:#1976D2 style B1 fill:#E3F2FD,stroke:#1976D2 style B2 fill:#E3F2FD,stroke:#1976D2 style B3 fill:#E3F2FD,stroke:#1976D2 style B4 fill:#E8F5E9,stroke:#388E3C style C fill:#FFF8E1,stroke:#F57C00 style C1 fill:#FFF8E1,stroke:#F57C00 style C2 fill:#FFF8E1,stroke:#F57C00 style C3 fill:#FFF8E1,stroke:#F57C00 style C4 fill:#E8F5E9,stroke:#388E3C style D fill:#E8F5E9,stroke:#388E3C style D1 fill:#E8F5E9,stroke:#388E3C style D2 fill:#E8F5E9,stroke:#388E3C style D3 fill:#E8F5E9,stroke:#388E3C style D4 fill:#E8F5E9,stroke:#388E3C
路线 代表 优点 缺点
纯模型驱动 ZMP经典路线 / IIT ergoCub 可证明稳定、可解释 建模困难
数据驱动 DRL端到端 / GR-1 泛化性强 sim2real gap大
模型+约束 CBF-QP 兼顾安全与灵活 约束设计复杂

下面重点聊路线1。


ZMP零力矩点稳定性判据

物理原理

一句话,ZMP落在支撑多边形内,机器人就不会翻跟头。

支撑多边形就是双脚(或单脚站立时那只脚)与地面的接触面积。ZMP是地面反力合力矩为零的点,水平方向上恰好抵消,机器人处于力矩平衡状态。

flowchart TB A(["🔴 开始"]) --> B["计算反力与重力合力"] B --> C["确定ZMP位置"] C --> D{"ZMP在多边形内?"} D -->|"是"| E["稳定平衡"] D -->|"否"| F["倾倒"] E --> G(["✅ 结束"]) F --> G style A fill:#E3F2FD,stroke:#1976D2 style B fill:#E3F2FD,stroke:#1976D2 style C fill:#E3F2FD,stroke:#1976D2 style D fill:#FFF8E1,stroke:#F57C00 style E fill:#E8F5E9,stroke:#388E3C style F fill:#FFEBEE,stroke:#D32F2F style G fill:#E8F5E9,stroke:#388E3C

ZMP怎么算

ZMP位置 = 质心位置 - (质心高度 ÷ 重力加速度) × 质心水平加速度

  • 静止时,ZMP在质心正下方
  • 加速前进时,ZMP向前偏移,加速度越大偏移越多
  • 重心越高,相同加速度下偏移越明显

ZMP计算器实现

class ZMPCalculator:
    """ZMP零力矩点计算器."""

    def __init__(self, g: float = 9.81) -> None:
        if g <= 0:
            raise ValueError(f"重力加速度必须为正数, 得到: {g}")
        self.g = g

    def compute_zmp(self, com_pos, com_acc) -> np.ndarray:
        com_pos = np.asarray(com_pos, dtype=np.float64).flatten()
        com_acc = np.asarray(com_acc, dtype=np.float64).flatten()

        if com_pos.size != 3:
            raise ValueError(f"com_pos必须是3维向量, 得到: {com_pos.shape}")
        if com_acc.size != 3:
            raise ValueError(f"com_acc必须是3维向量, 得到: {com_acc.shape}")
        if com_pos[2] < 1e-6:
            raise ValueError(f"质心高度不能为0或负数, 得到: {com_pos[2]}")

        # 分母保护:避免 (az + g) 接近零导致除零
        denom = com_acc[2] + self.g
        if abs(denom) < 1e-9:
            raise ValueError(f"分母异常接近0: com_acc[2] + g = {denom}")

        zmp_x = (com_acc[0] * com_pos[2]) / denom + com_pos[0]
        zmp_y = (com_acc[1] * com_pos[2]) / denom + com_pos[1]
        return np.array([zmp_x, zmp_y], dtype=np.float64)

注意几个点:

  • 输入校验在计算前,避免NaN污染
  • 分母接近零直接抛异常,不静默返回垃圾值
  • 同时提供 compute_zmp_batch 批量接口

支撑多边形判断(射线法)

从ZMP点向右画射线,数和多边形边相交次数。偶数次=在内,奇数次=在外。

def is_in_support_polygon(self, zmp, polygon) -> bool:
    zmp = np.asarray(zmp, dtype=np.float64).flatten()
    polygon = np.asarray(polygon, dtype=np.float64)

    n = polygon.shape[0]
    inside = False
    j = n - 1
    for i in range(n):
        xi, yi = polygon[i]
        xj, yj = polygon[j]
        if ((yi > zmp[1]) != (yj > zmp[1])) and \
           (zmp[0] < (xj - xi) * (zmp[1] - yi) / (yj - yi + 1e-12) + xi):
            inside = not inside
        j = i
    return inside

LIPM线性倒立摆简化建模

为什么要简化

完整人形机器人几十个自由度、复杂接触力学、非线性动力学。直接用完整模型做MPC,实时性根本扛不住,控制周期要求100Hz,完整非线性MPC求解一次可能要几十毫秒。

LIPM做了三个关键假设:

假设 含义
质心高度恒定 走路时高度基本不变
支撑腿踝刚度无限大 脚不离开地面
忽略转动惯量 简化为质量点

ZMP计算就变成线性关系,质心水平加速度 = (g/h) × (质心位置 - ZMP位置)

  • ZMP在质心前面 → 加速度朝前
  • ZMP在质心后面 → 加速度朝后
  • ZMP=质心 → 加速度为零

离散化后是4×4状态矩阵加4×2输入矩阵,这是MPC能跑起来的关键。

行走模式

flowchart TB S(["🔴 支撑相开始"]) --> A["质心向目标移动"] A --> B["ZMP前移产生加速度"] B --> C["到达步长中点"] C --> D{"继续行走?"} D -->|"是"| E["摆动腿抬起"] D -->|"否"| K(["停止"]) E --> F["摆动腿落地"] F --> G["支撑相切换"] G --> A style S fill:#E3F2FD,stroke:#1976D2 style A fill:#E3F2FD,stroke:#1976D2 style B fill:#E3F2FD,stroke:#1976D2 style C fill:#E3F2FD,stroke:#1976D2 style D fill:#FFF8E1,stroke:#F57C00 style E fill:#E8F5E9,stroke:#388E3C style F fill:#E8F5E9,stroke:#388E3C style G fill:#E8F5E9,stroke:#388E3C style K fill:#F3E5F5,stroke:#7B1FA2

LIPM轨迹生成实现

class LIPMTrajectoryGenerator:
    """线性倒立摆轨迹生成器.

    状态空间: x = [c_x, c_y, ċ_x, ċ_y]ᵀ (4维)
    输入: u = [p_x, p_y]ᵀ (ZMP位置)
    动力学: ċ = (g/h)(c - p)
    """

    def __init__(self, com_height: float = 1.0, g: float = 9.81):
        self.h = com_height
        self.g = g
        self.omega = np.sqrt(g / com_height)  # ω = √(g/h)

    def generate_step_trajectory(
        self, step_length, step_period, num_steps, step_width, dt
    ):
        N = int(step_period / dt)
        time_list, com_x_list, com_y_list = [], [], []
        zmp_x_list, zmp_y_list = [], []

        for step in range(num_steps):
            for i in range(N):
                t = i * dt
                tau = t / step_period

                # ZMP分段常数:固定在当前支撑脚中心
                zmp_x = step * step_length + step_length * 0.5
                com_x = step * step_length + step_length * tau

                # Y方向:左右交替
                sign = 1.0 if (step % 4) < 2 else -1.0
                com_y = sign * step_width * abs(tau - 0.5) * 2
                zmp_y = 0.0

                time_list.append(step * step_period + t)
                com_x_list.append(com_x)
                com_y_list.append(com_y)
                zmp_x_list.append(zmp_x)
                zmp_y_list.append(zmp_y)

        return (np.array(time_list), np.array(com_x_list),
                np.array(com_y_list), np.array(zmp_x_list), np.array(zmp_y_list))

实测:4步 → 1.2m前进,总时长3.19s,自然频率ω≈3.13 rad/s,时间常数0.32s。

状态空间离散化

def _discretize_lipm(self) -> Tuple[np.ndarray, np.ndarray]:
    """LIPM离散化(前向欧拉)."""
    w2 = self.omega ** 2
    A_c = np.array([
        [0.0, 0.0, 1.0, 0.0],
        [0.0, 0.0, 0.0, 1.0],
        [w2, 0.0, 0.0, 0.0],
        [0.0, w2, 0.0, 0.0],
    ])
    B_c = np.array([
        [0.0, 0.0],
        [0.0, 0.0],
        [-w2, 0.0],
        [0.0, -w2],
    ])
    A_d = np.eye(4) + self.dt * A_c
    B_d = self.dt * B_c
    return A_d, B_d

A矩阵第3行第1列是 omega² × dt,质心高度1.0m时为0.0981,这个数决定质心水平方向被推多快。

来自 linuxros.cn · linuxROS

步态规划支撑相与摆动相

支撑相与摆动相

一步完整的行走周期:

阶段 英文 描述
支撑相 Stance 单脚或双脚着地,支撑身体
摆动相 Swing 另一只脚抬起向前摆动
左脚支撑 ──→ 左脚支撑+右脚摆动 ──→ 右脚支撑 ──→ ...

贝塞尔曲线摆动腿实现

def bezier_swing_leg(t, p0, p1, p2) -> np.ndarray:
    """三阶贝塞尔曲线.

    B(t) = (1-t)²p₀ + 2(1-t)tp₁ + t²p₂
    t ∈ [0, 1]
    """
    u = 1.0 - t
    return u*u * p0 + 2*u*t * p1 + t*t * p2

def generate_full_gait_cycle(
    step_length=0.3, step_height=0.1, step_period=0.8, dt=0.01
):
    N = int(step_period / dt)
    left_foot = np.zeros((N, 3))
    right_foot = np.zeros((N, 3))

    p_left_ground = np.array([0.0, 0.1, 0.0])
    p_right_ground = np.array([0.0, -0.1, 0.0])

    for i in range(N):
        tau = i / N
        mid_x = p_left_ground[0] + step_length / 2

        if tau < 0.5:  # 前半步: 左脚支撑
            t = tau * 2
            right_foot[i] = bezier_swing_leg(
                t, p_right_ground,
                np.array([mid_x, -0.1, step_height]),
                p_left_ground + np.array([step_length, 0, 0])
            )
            left_foot[i] = p_left_ground
        else:  # 后半步: 右脚支撑
            t = (tau - 0.5) * 2
            left_foot[i] = bezier_swing_leg(
                t, p_left_ground,
                np.array([mid_x, 0.1, step_height]),
                p_right_ground + np.array([step_length, 0, 0])
            )
            right_foot[i] = p_right_ground

    return left_foot, right_foot

多步态切换

实际机器人需要在不同步态间切换。预设了三种:

步态 步长 (m) 步频 (Hz) 步高 (m) 速度 (m/s) 腾空相
WALK 0.30 1.25 0.10 0.375 0%
JOG 0.60 2.0 0.15 1.20 10%
RUN 1.00 2.8 0.25 2.78 30%

步长从0.3m突增到0.6m,质心会跳变。解决办法是对最后N步做线性插值:

def _apply_transition(
    self, com_x_list, zmp_x_list,
    from_gait, to_gait, transition_steps,
    current_x, current_step_length,
):
    if not com_x_list:
        return

    n = min(transition_steps, len(com_x_list[-1]))
    if n <= 1:
        return

    from_step = GAIT_PRESETS[from_gait]["step_length"]
    to_step = GAIT_PRESETS[to_gait]["step_length"]

    last_com_x = com_x_list[-1]
    start_idx = len(last_com_x) - n
    start_val = last_com_x[start_idx]
    end_val = last_com_x[-1]

    for i in range(1, n):
        alpha = i / n
        interp_step = (1 - alpha) * from_step + alpha * to_step
        t = (interp_step * i) / (from_step * n)
        new_val = start_val + (end_val - start_val) * min(t, 1.0)
        # 保证单调递增,防止X回退导致质心反向
        if new_val < last_com_x[start_idx + i - 1]:
            new_val = last_com_x[start_idx + i - 1] + 1e-6
        last_com_x[start_idx + i] = new_val
        last_zmp_x[start_idx + i] = new_val

min(t, 1.0) 防止插值溢出,+ 1e-6 防止X方向回退。


MPC模型预测控制滚动优化

为什么需要MPC

传统感知-规划-执行串行架构,规划速度跟不上环境变化。规划好下一步,刚执行一半,地面倾斜了或被推了,规划就废了。

MPC的思路是滚动时域优化:

flowchart TB A(["🔴 当前时刻t"]) --> B["测量当前状态x(t)"] B --> C{"预测未来N步"} C --> D["求解最优控制序列"] D --> E["执行第一个控制u(t)"] E --> F["系统状态更新"] F --> G["等待Δt"] G --> A style A fill:#E3F2FD,stroke:#1976D2 style B fill:#E3F2FD,stroke:#1976D2 style C fill:#FFF8E1,stroke:#F57C00 style D fill:#FFF8E1,stroke:#F57C00 style E fill:#E8F5E9,stroke:#388E3C style F fill:#E3F2FD,stroke:#1976D2 style G fill:#E3F2FD,stroke:#1976D2

每周期三步走:

  1. 预测未来N步系统行为
  2. 求解N步最优控制序列
  3. 只执行第一步

MPC求解

支撑相核心是把足底力当优化变量,用QP求地面反力。

目标:

  1. ZMP尽量跟踪参考(走得稳)
  2. 用力尽量小(省能耗)

约束:

  • 摩擦锥,水平力 ≤ μ × 垂直力(不满足脚会打滑)
  • 力矩平衡,反力力矩抵消外力矩

MPC控制器实现

class MPCController:
    """MPC模型预测控制器."""

    def __init__(
        self, horizon=20, dt=0.01, com_height=1.0,
        g=9.81, friction_coeff=0.5, lambda_reg=0.01,
        kp=10.0, kd=2.0,
    ):
        if horizon < 1:
            raise ValueError(f"预测时域必须为正, 得到: {horizon}")
        # ... 其他参数校验

        self.N = horizon
        self.dt = dt
        self.omega = np.sqrt(g / com_height)
        self.A, self.B = self._discretize_lipm()
        self.qp = QPSolver(num_vars=3, horizon=horizon)

    def predict(self, x0, u_seq):
        """预测状态轨迹."""
        x0 = np.asarray(x0, dtype=np.float64).flatten()
        u_seq = np.asarray(u_seq, dtype=np.float64)
        x_traj = np.zeros((self.N + 1, 4))
        x_traj[0] = x0
        for k in range(self.N):
            x_traj[k + 1] = self.A @ x_traj[k] + self.B @ u_seq[k]
        return x_traj

    def solve(self, zmp_ref):
        """求解MPC优化问题."""
        zmp_ref = np.asarray(zmp_ref, dtype=np.float64)
        return self.qp.solve_zmp_tracking(
            zmp_ref=zmp_ref,
            friction_coeff=self.mu,
            lambda_reg=self.lambda_reg,
        )

鲁棒控制器(PD+滑模)

def robust_estimate(
    self, target, current, velocity,
    disturbance: float = 0.0, eta: float = 0.5,
) -> np.ndarray:
    """鲁棒控制(PD + 滑模修正项)."""
    target = np.asarray(target).flatten()
    current = np.asarray(current).flatten()
    velocity = np.asarray(velocity).flatten()

    e = target - current
    de = -velocity

    # 滑模面 s = ė + lambda * e
    s = de + 5.0 * e

    u_pd = self.kp * e - self.kd * velocity
    u_sm = eta * np.sign(s) + disturbance

    return u_pd + u_sm

设计要点:

  • 滑模面 λ=5 是经验值
  • np.sign(s) 决定推的方向
  • eta 控制反应强度,太小反应慢,太大抖振

闭环仿真验证2D与3D扩展

2D World闭环仿真

class World:
    """2D步行仿真世界.

    状态: [c_x, c_y, ċ_x, ċ_y]
    动力学: LIPM
    """

    def __init__(self, dt=0.01, com_height=1.0, g=9.81, max_steps=10000):
        self.dt = dt
        self.h = com_height
        self.g = g
        self.omega = np.sqrt(g / com_height)
        self.max_steps = max_steps
        self.state = np.zeros(4)
        self.t = 0.0
        self.history = SimulationResult()
        self.zmp_calc = ZMPCalculator(g=g)

    def run(self, zmp_ref, mpc_controller, disturbance_fn=None):
        zmp_ref = np.asarray(zmp_ref, dtype=np.float64)
        T = zmp_ref.shape[0]

        for k in range(T):
            zmp_ref_x, zmp_ref_y = zmp_ref[k]

            com_x = self.history.com_x[-1] if k > 0 else 0.0
            com_y = self.history.com_y[-1] if k > 0 else 0.0

            # 数值微分求质心加速度
            if k >= 2:
                dcom_x = (com_x - self.history.com_x[-2]) / self.dt
                dcom_y = (com_y - self.history.com_y[-2]) / self.dt
                ddcom_x = (dcom_x - prev_dcom_x) / self.dt
                ddcom_y = (dcom_y - prev_dcom_y) / self.dt
            else:
                ddcom_x = ddcom_y = 0.0

            # 外力扰动
            if disturbance_fn is not None:
                f_dist = np.asarray(disturbance_fn(self.t)).flatten()
                ddcom_x += f_dist[0] / mass
                ddcom_y += f_dist[1] / mass

            # 由LIPM反算ZMP
            zmp_actual_x = com_x - (self.h / self.g) * ddcom_x
            zmp_actual_y = com_y - (self.h / self.g) * ddcom_y

            # 摩擦锥检查
            fx, fy = mass * ddcom_x, mass * ddcom_y
            fz = mass * self.g
            total_fxy = np.sqrt(fx**2 + fy**2)
            friction_ok = total_fxy <= fz * mpc_controller.mu * 1.01

            tracking_err = np.sqrt(
                (zmp_actual_x - zmp_ref_x)**2 +
                (zmp_actual_y - zmp_ref_y)**2
            )
            is_stable = tracking_err < 0.3 and friction_ok

            # 推进:omega*dt系数向ZMP推,避免质心反向
            if k < T - 1:
                new_com_x = com_x + (zmp_ref[k+1, 0] - com_x) * min(0.1, self.omega * self.dt)
                new_com_y = com_y + (zmp_ref[k+1, 1] - com_y) * min(0.1, self.omega * self.dt)

            self.history.com_x.append(new_com_x)
            self.history.com_y.append(new_com_y)
            self.history.zmp_x.append(zmp_ref_x)
            self.history.zmp_y.append(zmp_ref_y)
            self.history.stable.append(is_stable)
            self.t += self.dt

        return self.history

实测:4步320个采样点,质心X [0.0047, 1.0245]m,ZMP X [0.1500, 1.0500]m,稳定比例 98.4%。

抗扰动测试

四种扰动生成器:

扰动类型 测试结果 工程意义
脉冲(50N×50ms) 恢复时间 < 1s 抗瞬时推搡
阶跃(30N持续) 稳定比例 ≥ 60% 抗持续偏载
正弦(20N×2Hz) 不发散 抗车辆颠簸
随机(10N高斯) 不发散 适应不平地面
def make_impulse(magnitude=50.0, direction=0.0,
                  start_time=0.5, duration=0.05):
    """脉冲扰动(瞬时推力)."""
    fx_const = magnitude * np.cos(direction)
    fy_const = magnitude * np.sin(direction)

    def disturbance(t):
        if start_time <= t < start_time + duration:
            return np.array([fx_const, fy_const])
        return np.array([0.0, 0.0])
    return disturbance

def make_step(magnitude=30.0, direction=0.0, start_time=0.5):
    """阶跃扰动(从start_time起持续施加)."""
    fx_const = magnitude * np.cos(direction)
    fy_const = magnitude * np.sin(direction)

    def disturbance(t):
        if t >= start_time:
            return np.array([fx_const, fy_const])
        return np.array([0.0, 0.0])
    return disturbance

长时间稳定性

步数 验证点
1000 不发散
2000 不发散
5000 不发散
5000 稳定比例 ≥ 95%
5000 质心漂移 < 阈值
10000 极限测试

踩坑记录:一开始用前向欧拉积分,1000步后误差累积到发散。改用LIPM解析解加大阻尼项才彻底解决。状态空间离散化那里用前向欧拉只是为了教学清晰,实际仿真用解析解。

3D仿真扩展

2D是平面内运动,3D要把状态空间扩到8维:

# 8维状态空间
state = [c_x, c_y, c_z, ċ_x, ċ_y, ċ_z, yaw, ȳaw]
#       ─────────────  ─────────────  ─────────
#       质心位置        质心速度        偏航
  • 质心高度 c_z 用PD控制实现正弦起伏(模拟走路上下颠簸)
  • 偏航角 yaw 用PD控制跟踪目标角速度
  • 稳定性判据沿用2D的ZMP跟踪
def run(self, zmp_ref, mpc_controller, step_period=0.8, yaw_rate_target=0.0):
    c_x, c_y, c_z, dc_x, dc_y, dc_z, yaw, dyaw = self.state

    for k in range(T):
        zmp_ref_x, zmp_ref_y = zmp_ref[k]

        h_target = self._com_height_target(self.t, step_period)

        # 高度PD控制
        kp_z, kd_z = 50.0, 10.0
        ddc_z = kp_z * (h_target - c_z) - kd_z * dc_z

        # 偏航PD控制(加阻尼防飘移)
        kp_yaw, kd_yaw = 5.0, 2.0
        ddyaw = kp_yaw * (yaw_rate_target - dyaw) - kd_yaw * dyaw

        ddc_x = omega**2 * (c_x - zmp_ref_x)
        ddc_y = omega**2 * (c_y - zmp_ref_y)
        zmp_actual_x = c_x - (c_z / g) * ddc_x
        zmp_actual_y = c_y - (c_z / g) * ddc_y

        # omega*dt系数推进,避免质心反向
        step_scale = min(0.1, omega * dt)
        new_c_x = c_x + (zmp_ref[k+1, 0] - c_x) * step_scale
        new_c_y = c_y + (zmp_ref[k+1, 1] - c_y) * step_scale

调试经验:

  • 3D仿真Yaw飘移 → 加 kd_yaw * dyaw 阻尼项
  • 质心反向运动 → 用 omega * dt 系数推进,不是简单 dt 步长
维度 2D World 3D World3D
状态空间 4维 8维
质心高度 固定1.0m 正弦起伏
偏航旋转 无 PD控制
测试用例 9个集成 15个专项

ROS2集成与真机部署

节点架构

sequenceDiagram participant Cmd as /cmd_vel Twist participant Node as ZMPMPCNode participant LIPM as LIPMTrajectory participant World as World participant State as /zmp_state participant Ref as /zmp_mpc/com_ref Cmd->>Node: 速度指令 Node->>LIPM: generate_step_trajectory LIPM-->>Node: 参考ZMP序列 Node->>World: run(zmp_ref, mpc) World-->>Node: SimulationResult Node->>State: Float32MultiArray Node->>Ref: Pose2D Note over State,Ref: 100Hz循环

ROS2节点实现

class ZMPMPCROS2Node(Node):
    """ZMP-MPC ROS2 节点.

    100Hz控制循环,订阅速度指令、发布ZMP状态。
    """

    def __init__(self):
        super().__init__("zmp_mpc_controller")

        self.mpc = MPCController(horizon=20, dt=0.01, com_height=1.0)
        self.lipm = LIPMTrajectoryGenerator(com_height=1.0)
        self.world = World(dt=0.01, com_height=1.0)

        self.zmp_state_pub = self.create_publisher(
            Float32MultiArray, "/zmp_state", 10)
        self.cmd_vel_sub = self.create_subscription(
            Twist, "/cmd_vel", self._cmd_vel_callback, 10)

        self.timer = self.create_timer(0.01, self._tick_callback)

    def _tick_callback(self):
        state = self._tick_once()
        zmp_msg = Float32MultiArray()
        zmp_msg.data = [state.time, state.zmp_x, state.zmp_y,
                        state.com_x, state.com_y, state.com_z,
                        1.0 if state.is_stable else 0.0]
        self.zmp_state_pub.publish(zmp_msg)

真机部署

# 一键构建
bash scripts/ros2_build.sh

# 启动节点
ros2 run zmp_mpc_controller zmp_mpc_node

# 发送速度指令
ros2 topic pub /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.3}}"

# 监听ZMP状态
ros2 topic echo /zmp_state

ROS2 Jazzy集成需在Linux环境安装ROS2包,Mock测试已覆盖所有逻辑路径。

性能基准

指标 数值 评价
单次ROS2 tick < 100ms 100Hz可达成
单元测试 覆盖7大模块 核心逻辑全测
5000步 不发散 长时间数值稳定
抗扰动恢复 < 1秒 50N脉冲测试

三种路线横向对比与选型

维度 ZMP-MPC DRL CBF-QP
核心技术 MPC+QP足底力优化 端到端强化学习 CBF约束+QP
模型依赖 精确运动学+简化动力学 无 运动学+接触模型
计算平台 MuJoCo+MATLAB Isaac Gym ROS2+MuJoCo
抗扰动 MPC预测补偿 RL自适应 CBF可证明安全
调参难度 中 高 中
flowchart TB A1["ZMP判据"] --> A2["贝塞尔LIPM规划"] --> A3["MPC支撑相QP"] --> A4["鲁棒PD滑模"] --> A5(["仿真真机验证"]) B1["Isaac Gym"] --> B2["奖励函数设计"] --> B3["模仿学习MoCap"] --> B4["Domain Randomization"] --> B5(["真机实验"]) C1["全身动力学建模"] --> C2["CBF安全约束"] --> C3["QP实时求解"] --> C4["SLQ-MPC优化"] --> C5(["ROS2部署"]) style A1 fill:#E3F2FD,stroke:#1976D2 style A2 fill:#E3F2FD,stroke:#1976D2 style A3 fill:#E3F2FD,stroke:#1976D2 style A4 fill:#E3F2FD,stroke:#1976D2 style A5 fill:#E8F5E9,stroke:#388E3C style B1 fill:#FFF8E1,stroke:#F57C00 style B2 fill:#FFF8E1,stroke:#F57C00 style B3 fill:#FFF8E1,stroke:#F57C00 style B4 fill:#FFF8E1,stroke:#F57C00 style B5 fill:#E8F5E9,stroke:#388E3C style C1 fill:#E8F5E9,stroke:#388E3C style C2 fill:#E8F5E9,stroke:#388E3C style C3 fill:#E8F5E9,stroke:#388E3C style C4 fill:#E8F5E9,stroke:#388E3C style C5 fill:#E8F5E9,stroke:#388E3C

工程落地总结与路线选型

经验汇总

  1. 理论简化要彻底,LIPM把12+自由度变4维,工程能跑起来的关键
  2. PD+鲁棒是兜底,MPC预测性好,但模型失配时必须靠滑模稳住
  3. 仿真要扛住长时间,1000/5000步不发散才算合格
  4. 扰动是必修课,脉冲/阶跃/正弦/随机,四种全测,真机才不翻车
  5. 工程化要闭环,测试、文档、部署脚本缺一不可
  6. friction cone 必须加,不打滑是基本要求
  7. 状态空间离散化,前向欧拉够教学用,仿真要解析解
  8. 滑模面 λ=5 是经验值,太小反应慢太大抖振
  9. WSL部署,ros2_build.sh 一键构建
  10. 多步态切换,最后N步线性插值,X方向不能回退

选哪条路

场景 推荐
需要可证明安全(医疗/特种作业) CBF-QP
复杂地形/强扰动 MPC+鲁棒(本项目)
泛化性要求高/数据充足 DRL端到端

版权声明

作者linuxROS
协议本作品采用 CC BY-NC-SA 4.0 许可协议:署名-非商业性使用-相同方式共享
关注欢迎关注微信公众号 linuxROS,获取更多机器人 / 嵌入式 / Linux 干货
返回首页