4、约束处理:MPC如何处理执行器饱和、状态约束与安全边界

各位同学,咱们今天聊个硬核话题——约束处理。

说实话,我最早做PID的时候,最头疼的就是限幅。你调好一组参数,飞得好好的,突然来个急转弯,电机直接饱和,飞机就开始抖。那时候我只能靠经验,把限幅值设得保守一点,但性能就下去了。

MPC不一样。它天生就带着约束处理的能力。说白了,MPC在规划未来轨迹的时候,就已经把「电机能转多快」「角度不能超过多少」这些限制考虑进去了。这是PID做不到的。

4.1 执行器饱和:别让电机硬扛

执行器饱和,就是电机转速到了极限,你给再大的控制量也没用。

PID遇到饱和,通常的做法是积分限幅、抗饱和。但MPC的做法更优雅——它直接在优化问题里加约束。

比如,你的电机PWM范围是1000到2000us。在MPC里,你只需要写:

u_min ≤ u(k) ≤ u_max

其中 u_min = 1000, u_max = 2000。

我在项目中遇到过一个问题:有个四旋翼,电机响应有延迟。单纯加约束还不够,因为MPC算出来的控制量,电机实际执行时可能因为动态响应而超调。嗯,这里要注意——

避坑指南: 我曾经在高速飞行器上吃过亏。MPC算出的控制量在约束边界上,但电机实际响应有惯性,瞬间超过了限幅值。后来我加了一个「软约束」,把边界往里缩了5%,留出安全余量。

所以,执行器饱和的处理,不只是加个上下限那么简单。你还要考虑:

  • 速率约束:电机转速变化率也有限制
  • 动态余量:给执行器响应留出缓冲
  • 优先级:多个执行器饱和时,哪个优先?

4.2 状态约束:别让飞机飞出安全区

状态约束,就是限制飞机的姿态、位置、速度等状态变量。

比如,你不想让飞机倾斜超过45度。在PID里,你只能靠限幅指令来实现。但MPC可以直接把角度约束写进优化问题:

|φ(k)| ≤ 45°  (滚转角约束)
|θ(k)| ≤ 45°  (俯仰角约束)

为什么这很重要?

你想想看,PID是「事后补救」——角度超了,我才往回拉。MPC是「事前规划」——它知道未来可能会超,提前就调整了。

我个人习惯把状态约束分成两类:

约束类型 示例 处理方式
硬约束 位置边界、障碍物 必须满足,违反即危险
软约束 姿态角、速度上限 允许轻微违反,但需惩罚

硬约束和软约束的区别,我举个例子你就明白了。

有一次做室内编队飞行,墙壁就是硬约束——撞上就炸机。但姿态角是软约束——偶尔超过45度,只要快速恢复,问题不大。

软约束的实现方式,是在代价函数里加一个惩罚项:

J = J_original + ρ * max(0, |φ| - 45°)^2

这个ρ是惩罚系数。ρ越大,约束越硬。我一般从100开始调,看效果再增减。

4.3 安全边界:给MPC加个「保险丝」

安全边界,说白了就是「最后一道防线」。

MPC再厉害,也是基于模型的。模型有误差,传感器有噪声,执行器有故障。万一MPC算出来的轨迹不安全怎么办?

我见过一个案例:某无人机在强风下,MPC预测的轨迹和实际偏差越来越大,最后撞树了。原因就是模型失配,MPC以为自己能飞过去,实际飞不过去。

所以,安全边界的设计,我建议分三层:

  1. 预测层:MPC内部约束,保证规划轨迹安全
  2. 监控层:实时检查状态是否接近边界
  3. 应急层:一旦越界,立即切换保护模式

应急层怎么做?我常用的方法是「安全备份轨迹」。比如,如果MPC失效,就执行一个预设的减速悬停轨迹。这个轨迹要保证:

  • 在任何状态下都能安全执行
  • 不依赖模型预测
  • 响应时间在50ms以内
小技巧: 我习惯在MPC的代价函数里加一个「终端约束」——要求预测终点必须在一个安全区域内。这样即使中途出问题,最后也能回到安全状态。

4.4 约束处理的工程实现

理论说完了,咱们看看代码怎么写。

以OSQP求解器为例,约束矩阵的构建是这样的:

# 状态约束:角度不超过45度
A_ub = np.array([[0, 0, 1, 0, 0, 0],   # 滚转角
                 [0, 0, 0, 1, 0, 0]])  # 俯仰角
l_ub = np.array([-45, -45])  # 下界
u_ub = np.array([45, 45])    # 上界

# 控制约束:PWM 1000-2000
A_ctl = np.array([[1, 0, 0, 0],
                  [0, 1, 0, 0],
                  [0, 0, 1, 0],
                  [0, 0, 0, 1]])
l_ctl = np.array([1000, 1000, 1000, 1000])
u_ctl = np.array([2000, 2000, 2000, 2000])

这里要注意一个细节:约束矩阵的维度要和预测时域匹配。比如预测时域是N=10,那约束矩阵就要重复10次。

我刚开始做的时候,经常在这里出错。矩阵维度不对,求解器直接报错。后来我写了个辅助函数,自动扩展约束矩阵:

def expand_constraints(A, l, u, N):
    """将单步约束扩展为预测时域约束"""
    A_exp = np.kron(np.eye(N), A)
    l_exp = np.tile(l, N)
    u_exp = np.tile(u, N)
    return A_exp, l_exp, u_exp

嗯,这个小工具帮我省了不少调试时间。

4.5 约束冲突怎么办?

这是MPC里最头疼的问题——约束之间互相矛盾。

比如,你想让飞机快速减速,但电机转速有下限,不能反转。这时候「快速减速」和「电机下限」就冲突了。

怎么办?

我的做法是:给约束排优先级。

  • 一级约束:安全相关,必须满足(如位置边界)
  • 二级约束:性能相关,尽量满足(如姿态角)
  • 三级约束:舒适性相关,可以放松(如加速度变化率)

实现方式是用「松弛变量」。给低优先级的约束加一个松弛变量,允许它在一定范围内被违反:

# 软约束:允许姿态角最多超过5度
|φ| ≤ 45° + ε
ε ≥ 0
代价函数中加惩罚:ρ * ε^2

这个ε就是松弛变量。ρ越大,约束越硬。我一般把ρ设成1000,这样ε基本不会超过0.1度。

核心要点: 约束处理不是「加得越多越好」。约束太多,求解器可能找不到可行解。我的经验是:只加真正必要的约束,其他的用代价函数来引导。

4.6 本章小结

好了,咱们把约束处理的核心内容过了一遍。

执行器饱和、状态约束、安全边界,这三者是MPC相比PID的最大优势。但优势背后也有代价——约束越多,计算越慢,调试越复杂。

我个人的建议是:

  • 先从硬约束开始,保证安全
  • 再逐步加软约束,提升性能
  • 最后加安全边界,兜底保护

别想着一步到位。我见过太多人,一上来就加十几个约束,结果求解器跑不动,最后又全部删掉。慢慢来,比较快。

MPC约束处理三层架构 第一层:执行器饱和 控制量约束 | 速率约束 | 动态余量 第二层:状态约束 硬约束(位置/障碍物) | 软约束(姿态/速度) 第三层:安全边界 预测层约束 | 监控层检查 | 应急层保护 优先级 递增

专注资料整理