1. 为什么要用MPC做弹簧阻尼系统的轨迹跟踪?先聊聊背景和思路
先说清楚这篇文章讲的是什么。标题里的关键词是“模型预测控制”“线性MPC”“二阶弹簧阻尼系统”“轨迹跟踪控制”——说白了,就是拿MPC(Model Predictive Control,模型预测控制)这套控制策略,去让一个典型的二阶弹簧阻尼系统跟上一条期望轨迹。系统的状态变量(位移、速度)和输出(力的输入)都受限,MPC能把这些约束直接塞进优化问题里处理,这是它相比PID最大的优势之一。
谁适合读这篇文章?如果你是做运动控制、机器人、汽车底盘控制或者自动化相关方向的学生或工程师,正在纠结“MPC到底怎么做仿真”“为什么别人的MPC能跟踪得很好而我的抖得厉害”“约束怎么加才不踩坑”,这篇文章应该能给你一个比较完整的参考。我会按“理论推导—代码实现—调参经验—踩坑记录—扩展应用”这条线来讲,整个过程都基于一个可以随手复现的二阶弹簧阻尼系统模型。
先花点时间说说为什么选这个系统。二阶弹簧阻尼系统在物理世界里到处都是:汽车的悬架、伺服电机的负载轴、工业机械臂的关节、甚至某些液位系统,动特性都可以简化成质量—弹簧—阻尼模型。它的数学结构足够简单,适合把MPC的核心机制讲透;同时它又具备振荡、超调、约束耦合这些真实系统的特征,不会让你觉得“这只是个玩具”。
我最早接触MPC是在做自主移动机器人底盘控制的项目里,当时的感觉是:传统PID只能逐环去调参数,想处理前后轮耦合和速度约束非常别扭。后来换成MPC,把问题写成优化问题,约束顺手就加了进去。当然代价也随之而来——计算量上来了、离散化搞不好会发散、预测模型的偏差会导致跟踪精度下降。这些部分恰恰是网上教程讲得最少、实际项目里坑最多的。我在这篇文章里会事无巨细地把它们都摊开讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从动力学方程到MPC预测模型:公式推导与离散化
MPC不是凭空跑起来的,它首先要有一个预测模型。这个模型描述了:给定当前状态和未来一段时间的控制输入,系统状态会怎么演化。模型越准,控制效果越好;模型太复杂,实时性又跟不上。所以第一步,我们要把二阶弹簧阻尼系统的动力学方程写出来,然后再离散化成MPC能用的标准形式。
2.1 连续时间状态方程:质量和弹簧和阻尼怎么进状态空间
一个标准的$m$(质量)、$k$(弹簧刚度)、$c$(阻尼系数)二阶系统,受力分析很简单:
$$
m\ddot{x} + c\dot{x} + kx = F
$$
其中$x$是位移,$F$是外部控制力。把它改写成状态空间形式——引入状态向量$\mathbf{x} = [x, \dot{x}]^T$,于是:
$$
\dot{\mathbf{x}} =
\begin{bmatrix}
0 & 1 \
-\frac{k}{m} & -\frac{c}{m}
\end{bmatrix}
\mathbf{x}
+
\begin{bmatrix}
0 \
\frac{1}{m}
\end{bmatrix}
F
$$
这个模型里,弹簧项$kx$负责把系统往平衡位置拉,阻尼项$c\dot{x}$负责消耗能量、决定振荡衰减快慢,质量$m$决定了惯性大小。你把$k$调大,系统变“硬”,响应变快但容易振荡;把$c$调大,振荡变弱但响应变慢。这种直观的物理解释,对于后面理解MPC预测视野长度和权重矩阵的设置非常有帮助。
如果你把$F$设置为基于状态的反馈,比如$F = -K\mathbf{x}$,那你实际上就在做一个线性二次型调节器(LQR)或极点配置。MPC的思路则完全不同:它不直接给出一个固定增益,而是在每个采样时刻求解一个有限时域优化问题,得到最优的输入序列,然后只应用第一个输入。
2.2 零阶保持离散化:采样时间怎么选才合理
嵌入式控制器和仿真算法处理的都是离散信号,所以得把连续模型离散化。最常用的方法是零阶保持器(ZOH)离散化:假设每个采样周期内控制输入$F$保持不变,然后对矩阵指数积分。
离散化结果如下:
$$
\mathbf{x}(k+1) = A_d\mathbf{x}(k) + B_d F(k)
$$
其中:
$$
A_d = e^{A_c T_s}, \quad B_d = \int_0^{T_s} e^{A_c \tau} d\tau B_c
$$
$T_s$是采样周期。很多刚入门的朋友直接用欧拉法近似,这在小采样周期下问题不大,但一旦$T_s$稍大,欧拉法的精度就会下降,甚至导致预测模型和真实系统严重偏离,MPC的“预测”就变成了“瞎猜”。
采样时间的选择我一般遵循三条原则:
- $T_s$不能大于系统主导时间常数的$1/10$到$1/20$,否则离散模型丢失动态特性;
- $T_s$要能覆盖控制器计算时间——控制周期必须大于单步MPC求解时间,否则实时性崩了;
- $T_s$要和执行机构响应速度匹配。弹簧阻尼系统的执行器如果是电机,那$T_s$取5ms~20ms通常是比较舒服的一个区间。
我在仿真演示中取了$m=1,\text{kg}$,$k=2,\text{N/m}$,$c=1,\text{N·s/m}$,$T_s=0.1,\text{s}$。这个系统自然频率大约在$\omega_n=\sqrt{k/m}\approx1.41,\text{rad/s}$,时间常数大约0.7秒,所以0.1秒的采样周期能捕捉到系统动态的主要变化,同时计算负担又小,适合做教学演示和调参验证。
2.3 增量形式与控制量约束:避免静态偏差的常见做法
直接以$F(k)$作为优化变量,当模型有误差或存在未知扰动时,MPC会留下静态偏差。为了消除这个问题,工程上非常常见的方法是把控制量改成增量形式$\Delta u(k) = u(k) - u(k-1)$,把$u(k-1)$作为额外的状态。
扩展后的状态向量为:
$$
\tilde{\mathbf{x}}(k) = [x(k), \dot{x}(k), F(k-1)]^T
$$
预测方程变为:
$$
\tilde{\mathbf{x}}(k+1) =
\begin{bmatrix}
A_d & B_d \
0 & 1
\end{bmatrix}
\tilde{\mathbf{x}}(k)
+
\begin{bmatrix}
B_d \
1
\end{bmatrix}
\Delta u(k)
$$
这样做的直接好处是:可以在代价函数里对对$\Delta u$施加惩罚,限制控制量的变化率;同时只要优化问题可行,模型有偏差时系统也能通过积分作用消除稳态误差。我强烈建议你在做任何MPC仿真时,一开始就采用增量形式,不然后面加扰动或者模型失配时,追踪效果会很尴尬。
约束条件也顺便定义好:
$$
F_{\min} \le F(k+i) \le F_{\max}, \quad \Delta F_{\min} \le \Delta F(k+i) \le \Delta F_{\max}
$$
以及状态的约束:
$$
x_{\min} \le x(k+i) \le x_{\max}
$$
比如物理上执行器最大输出力为$5,\text{N}$,位移不能超过$[-0.5, 0.5],\text{m}$。让MPC在高性能条件下主动“触碰”这些边界,这正是模型预测控制比传统线性控制器有压倒性优势的地方。
3. 轨迹跟踪问题的MPC数学建模:代价函数、约束、预测时域
模型有了,接下来就是把“轨迹跟踪”这个目标翻译成一个标准的二次规划(QP)问题。整个过程可以分解成:定义代价函数、定义约束、形成QP矩阵。这一节我尽量把每个矩阵的维度都写清楚,因为代码实现时的很多out-of-index错误,根源都在这里。
3.1 代价函数设计:输出误差、控制增量、终端代价
轨迹跟踪的目标是让系统输出(这里就是位移$x$)跟踪参考轨迹$r(k)$。MPC在每个时刻$k$要做的事情是:求解从$k+1$到$k+N_p$的预测输出与参考轨迹的误差平方和,再加上控制增量惩罚。
代价函数写作:
$$
J = \sum_{i=1}^{N_p} \left( \mathbf{y}(k+i) - \mathbf{r}(k+i) \right)^T Q \left( \mathbf{y}(k+i) - \mathbf{r}(k+i) \right) + \sum_{i=0}^{N_c-1} \Delta u(k+i)^T R \Delta u(k+i)
$$
其中:
- $N_p$是预测时域,表示“向前看多少步”;
- $N_c$是控制时域,表示“未来多少个控制增量可调”;
- $Q$是输出权重矩阵,$R$是控制增量权重矩阵。
对于我们的系统,输出$\mathbf{y} = x$,所以$Q$是个$1\times1$矩阵。如果你想让速度也被跟踪(比如让速度平滑),可以把输出扩展到$[x, \dot{x}]^T$,那$Q$就是$2\times2$。
$Q$和$R$的比值决定了系统的“性格”:
| 参数调节 | 效果 | 副作用 |
|---|---|---|
| $Q$增大 | 跟踪误差权重变大,系统更“激进” | 输入可能过大,易振荡 |
| $R$增大 | 控制增量变化被压制,动作更平滑 | 跟踪变慢,误差变大 |
| $N_p$增大 | 看得更远,轨迹更平滑 | 计算量上升,对模型误差更敏感 |
| $N_c$增大 | 自由度更多,优化解更灵活 | 计算量上升,可能产生抖动 |
初次上手我建议$Q=1$,$R=0.1$,$N_p=20$,$N_c=5$,在这个基准上慢慢调。不要一上来就抄论文参数,不同系统参数下最优设置会差很多。
终端代价的设计是MPC领域一个比较深的话题。朴素的MPC如果$N_p$取得不够大,末段的控制会变得很短视,导致整体性能下降。成熟的做法是加一个终端代价项:
$$
J_{terminal} = \mathbf{x}(k+N_p)^T P \mathbf{x}(k+N_p)
$$
其中$P$可以通过解离散Riccati方程或者取终端约束的方式来设计。在工程简化实现中,我经常直接靠足够大的$N_p$和较小的$R$来近似稳定,但如果你要写论文或者仿真系统对稳定性要求很高,建议补上终端代价。
3.2 约束的统一表示:状态约束和控制量约束怎么进优化器
对于线性MPC,把预测方程代入代价函数后,问题就变成标准的QP形式:
$$
\min_{\Delta U} \frac{1}{2} \Delta U^T H \Delta U + g^T \Delta U
$$
满足线性约束:
$$
A_{con} \Delta U \le b_{con}
$$
这里的 $\Delta U \in \mathbb{R}^{N_c \times 1}$ 是未来控制增量序列的列向量。$H$矩阵由$Q$、$R$和预测矩阵共同组成,通常是对称正定的;$g$向量包含当前状态跟踪误差和参考轨迹信息。
约束按类型分有:
- 控制量幅值约束:$F_{\min} \le F(k+i) \le F_{\max}$;
- 控制增量约束:$\Delta F_{\min} \le \Delta F(k+i) \le \Delta F_{\max}$;
- 状态约束:$x_{\min} \le x(k+i) \le x_{\max}$,速度约束同理。
这三类约束都要转换成关于$\Delta U$的不等式。比如控制量$F(k+i)$可以表示成$F(k-1) + \sum_{j=0}^{i}\Delta F(k+j)$,代入后得到一组线性不等式。实现时我建议直接把约束矩阵拼起来,一次性生成$A_{con}$和$b_{con}$,别一条条去拼,代码更清晰,也更容易debug。
一个很容易被忽略的问题是约束的相容性。比如你既要求位移严格不超过0.5m,又要求控制力不能超过1N,在某些初始条件下,可能根本没有可行解。MPC求解器此时会返回错误或者不可行解,控制输出就会异常。工程上常见做法是加软约束——把硬状态约束改成惩罚项放回代价函数,约束本身变成允许轻微越界的软边界。这个技巧我强烈建议做实物控制的朋友一定要掌握,因为真实系统永远有噪声扰动,硬约束太容易崩了。
3.3 参考轨迹的预处理:阶跃、斜坡、正弦,各有讲究
轨迹跟踪的“轨迹”可以是阶跃信号、斜坡信号、正弦信号,或者任意生成的光滑曲线。不同参考信号对MPC的挑战不一样:
- 阶跃信号:在最开始时刻有大跳变,要求MPC快速响应,代价函数里跟踪误差权重$Q$要足够大,否则上升过程会拖沓。但由于系统带宽有限,过大的$Q$容易引发超调甚至振荡。
- 斜坡信号:跟踪的难点在于系统要始终“追随”,稳态时速度和输入都在变化,控制增量约束$\Delta u$如果太紧,会表现出明显的滞后。
- 正弦信号:考验MPC对动态信号的跟踪能力。如果正弦频率高于系统闭环带宽,跟踪误差必然增大;如果参考频率低,MPC能做到几乎零相位延迟跟踪——这是PID很难做到的。
这篇演示里我选用了多段式参考轨迹:前5秒是幅值0.3m的阶跃,5秒后切换成频率为0.2Hz、幅值0.3m的正弦波。这样既能展示MPC对阶跃的快速响应,又能展示对持续动态轨迹的跟踪能力。
如果你希望参考轨迹本身更平滑,可以在生成参考时对它做一阶低通滤波,比如用:
$$
r_{f}(k+1) = r_f(k) + \alpha \left( r(k) - r_f(k) \right)
$$
这样MPC在起始时刻不用面对太陡峭的参考变化,控制量曲线会平滑很多。当然这只是预处理技巧,不是MPC本身的机制。
4. 完整代码实现:从零搭建线性MPC轨迹跟踪仿真
到了动手环节。我会给出一个完整的MATLAB脚本,这个脚本是我自己实际调试过的版本,可以直接跑通。我选择MATLAB是因为它的矩阵计算和quadprog接口非常适合做MPC原型验证,不过思路完全可以移植到Python的cvxpy或OSQP上。
4.1 系统参数定义与计算离散模型
matlab复制%% 弹簧阻尼系统参数
m = 1.0; % 质量 kg
k = 2.0; % 弹簧刚度 N/m
c = 1.0; % 阻尼系数 N·s/m
Ts = 0.1; % 采样周期 s
%% 连续时间状态空间模型
A_c = [0, 1;
-k/m, -c/m];
B_c = [0;
1/m];
C = [1, 0]; % 输出位移
D = 0;
sys_c = ss(A_c, B_c, C, D);
%% 零阶保持离散化
sys_d = c2d(sys_c, Ts, 'zoh');
A_d = sys_d.A;
B_d = sys_d.B;
这是最基础的部分,但也是踩坑高发区。比如$c2d$的第三个参数我特意写了'zoh',如果你直接用默认的'zoh'也行,但如果状态空间矩阵里有常数项或延迟,务必检查离散化方式是否匹配。另外,这里$C$矩阵是[1,0],表示只测量位移,不测量速度。如果你想要全状态反馈,得加上速度传感器或者用观测器去估计。MPC本身通常假设状态已知或者可估计,所以仿真里直接把真实状态拿过来用;实物上你得配合状态观测器(如卡尔曼滤波)把$\dot{x}$估计出来。
4.2 预测矩阵与QP矩阵构建
这里的核心是把预测模型矩阵化。令$N_p$为预测时域,$N_c$为控制时域,扩展状态为$\tilde{\mathbf{x}} = [x; \dot{x}; F_{prev}]$,扩展后的预测模型为$\tilde{A}$和$\tilde{B}$。然后把预测输出序列表达成$\Delta U$的线性函数。
matlab复制%% MPC参数
Np = 20; % 预测时域
Nc = 5; % 控制时域
Q = 1.0; % 输出权重
R = 0.1; % 控制增量权重
%% 扩展状态空间模型(增量形式)
A_tilde = [A_d, B_d;
zeros(1,2), 1];
B_tilde = [B_d;
1];
C_tilde = [1, 0, 0]; % 只跟踪位移x
[nx_tilde, ~] = size(A_tilde);
ny = size(C_tilde, 1);
%% 构建预测矩阵 Phi 和 Theta
Phi = zeros(Np*ny, nx_tilde);
Theta = zeros(Np*ny, Nc);
for i = 1:Np
Phi((i-1)*ny+1:i*ny, :) = C_tilde * (A_tilde^i);
for j = 1:min(i, Nc)
Theta((i-1)*ny+1:i*ny, j) = C_tilde * (A_tilde^(i-j)) * B_tilde;
end
end
%% 构建QP矩阵 H 和 g 的系数矩阵
H = Theta' * kron(eye(Np), Q) * Theta + kron(eye(Nc), R);
H = (H + H') / 2; % 确保对称正定
这段代码里最容易出错的是$\Phi$矩阵中A_tilde^i的幂次计算。注意$A_tilde$是扩展后的状态矩阵,维度是3×3,所以$i$次幂运算复杂度不高,但也别因为写成了标量幂导致维度错误。我习惯在每个关键矩阵运算后加disp(size(...))检查维度,MATLAB不会报错时自己心里也好有底。
矩阵$H$理论上必须是正定的,否则quadprog可能报错。求完$H$后我总习惯加一行(H+H')/2强制对称化,这是数值上的保险,真遇到数值漂移时能省掉很多莫名其妙的报错。
4.3 主循环:预测、优化求解、执行第一步
主循环是MPC的核心:每个采样时刻计算当前状态,拼装参考轨迹向量,求解QP,取最优控制序列的第一个增量,更新系统状态。
matlab复制%% 仿真时长设置
T_total = 10; % 10秒
N_steps = T_total / Ts; % 100步
x_history = zeros(2, N_steps+1);
u_history = zeros(1, N_steps);
u_prev = 0;
x_history(:,1) = [0; 0]; % 初始状态
%% 参考轨迹生成
r = zeros(1, N_steps+1);
for i = 1:N_steps+1
t = (i-1)*Ts;
if t < 5
r(i) = 0.3; % 阶跃
else
r(i) = 0.3 * sin(2*pi*0.2*t); % 正弦
end
end
%% 主循环
for k = 1:N_steps
xk = x_history(:, k);
x_tilde = [xk; u_prev];
% 构建参考轨迹向量 R_ref
R_ref = zeros(Np*ny, 1);
for i = 1:Np
if k+i <= N_steps+1
R_ref((i-1)*ny+1:i*ny) = r(k+i);
else
R_ref((i-1)*ny+1:i*ny) = r(N_steps+1); % 超出部分取末值
end
end
% 代价函数线性项 g
g = Theta' * kron(eye(Np), Q) * (Phi * x_tilde - R_ref);
% 约束: F_min <= F <= F_max, dF_min <= dF <= dF_max
F_max = 5; F_min = -5;
dF_max = 1; dF_min = -1;
% 约束矩阵构建(控制量幅值约束)
A_con = [];
b_con = [];
% 不等式:F = F_prev + C_sum * dU,约束F<=F_max 和 F>=F_min
for i = 1:Np
% 构造 C_sum:第i个控制量对应前min(i,Nc)个增量的累积和
C_sum_row = zeros(1, Nc);
for j = 1:min(i, Nc)
C_sum_row(j) = 1;
end
A_con = [A_con; C_sum_row];
b_con = [b_con; F_max - u_prev];
A_con = [A_con; -C_sum_row];
b_con = [b_con; u_prev - F_min];
end
% 控制增量约束
if Nc > 0
for j = 1:Nc
row = zeros(1, Nc);
row(j) = 1;
A_con = [A_con; row];
b_con = [b_con; dF_max];
A_con = [A_con; -row];
b_con = [b_con; -dF_min];
end
end
% 求解QP
options = optimoptions('quadprog', 'Display', 'off');
[dU_opt, ~] = quadprog(H, g, A_con, b_con, [], [], [], [], [], options);
% 取第一个控制增量并更新控制量
du = dU_opt(1);
u = u_prev + du;
% 将控制量限制在幅值范围内(安全冗余)
u = max(F_min, min(F_max, u));
% 用真实系统模型更新状态(仿真中我们用离散模型)
xk1 = A_d * xk + B_d * u;
% 保存数据
x_history(:, k+1) = xk1;
u_history(k) = u;
u_prev = u;
end
看到这段代码你可能觉得约束拼装很繁琐,但这一步是MPC工程化的关键。如果你略过约束,直接用无约束解析解$dU = -H^{-1}g$,那实现起来会简单很多,但就失去了MPC处理约束边界的核心能力。尤其是当参考轨迹要求位移紧贴约束边界时,无约束解完全没法用。
别忘了每次取完$dU_opt$后还加了一行把$u$限幅在$[F_min,F_max]$的操作。这其实是个双保险——理论上QP已经保证$u$不超限,但数值求解器偶尔会给出微小越界的解,再加一个饱和环节能避免真实系统端点异常。
4.4 画图与结果解读
跑完循环后,把状态和控制量画出来:
matlab复制%% 绘图
t_axis = (0:N_steps) * Ts;
figure;
subplot(2,1,1);
plot(t_axis, x_history(1,:), 'b-', 'LineWidth', 1.5); hold on;
plot(t_axis, r, 'r--', 'LineWidth', 1.5);
xlabel('时间 (s)'); ylabel('位移 (m)');
legend('MPC跟踪输出', '参考轨迹', 'Location', 'northeast');
title('二阶弹簧阻尼系统 - MPC轨迹跟踪');
grid on;
subplot(2,1,2);
stairs(t_axis(2:end), u_history, 'k-', 'LineWidth', 1.5);
xlabel('时间 (s)'); ylabel('控制力 (N)');
title('控制输入序列');
grid on;
跑出来的图你会看到:阶跃段MPC迅速把位移推到位并稳定;正弦段输出曲线紧贴参考轨迹,相位滞后非常小。这个结果是线性MPC在模型完全已知时的正常表现。实际应用里模型总会有点偏差,效果会打折扣,但整体机制仍然是这个逻辑。
5. MPC调参的实战心得:Q、R、Np、Nc到底怎么配
很多朋友拿别人代码跑通了,但换一组参数就崩溃,于是觉得MPC“玄学”。实际上MPC调参是有内在逻辑的,我这里把仿真和项目中积累的经验完整分享出来。
5.1 Q和R的“性能—鲁棒性”天平
$Q$和$R$的比值本质上是在“跟踪精度”和“控制能量”之间做权衡。
- 如果$Q/R$很大,意味着你不在乎控制增量有多大,只在乎误差最小化。此时系统的闭环响应会非常激进,上升时间短,但控制信号可能大幅度跳变,甚至出现小幅振荡。
- 如果$Q/R$很小,系统变得“懒惰”,控制增量被压制得很小,输出跟踪会很慢,稳态误差也可能增大。
一个实用的调参顺序是:先把$R$固定为0.1,$Q$从0.1开始,每次×10地往上试(0.1、1、10、100),观察跟踪误差和控制量的变化;找到一个跟踪误差可接受且控制量小幅值合理的$Q$后,再微调$R$。千万不要同时调两个参数,你会分不清变化到底是谁引起的。
从最优控制的角度理解,$Q/R$的比值也对应闭环极点的位置。比值越大,闭环极点越靠近左半平面远侧,响应越快。但这个“快”受限于系统的物理带宽和采样率的约束,调得太快很可能导致采样率不够,反而让离散系统出现抖动。
5.2 预测时域Np:看得越远,越要模型准
$N_p$决定了MPC在未来多少步以内做优化。$N_p$太短,MPC“目光短浅”,后面可能发生什么都不考虑,控制策略就容易出现末端效应——最后几步控制量乱跳。$N_p$太长,预测误差累积变大,而且QP矩阵维数升高,计算量增大,实时性变差。
经验法则:$N_p$的设置要覆盖系统的主要动态过渡过程。对二阶弹簧阻尼系统,最优值大约在系统自然周期的2~5倍对应步数。以我们选的参数为例,自然周期约$2\pi/\omega_n \approx 4.45,\text{s}$,采样时间0.1s,所以$N_p$至少要45步才能覆盖一个完整周期。但实际演示中,$N_p=20$已经足够好——因为阻尼让系统在2秒内基本稳定下来,而这些振荡信息在20步内基本都被捕捉到了。
如果你是做实时控制,经验是:$N_p$尽量取一个“能覆盖系统的稳定时间,同时QP求解时间不超过采样周期的70%”的值。可以先仿真测瓶颈,再上实物。
5.3 控制时域Nc:自由度不是越多越好
$N_c$代表未来有多少个控制增量是“自由变量”。直觉上,$N_c$越大,控制器越灵活,性能应该越好。但实际中$N_c$增大会让优化解的抖动概率增加,尤其是$R$偏小时。
对于弹簧阻尼这类比较“温和”的线性系统,$N_c = 3\sim10$就足够。我这里取$N_c=5$,性价比很高。你甚至可以取$N_c=2$,结果也不会差太多。真正的收益来自于把$N_c$先从1逐步增大,观察跟踪误差的变化,当误差不再显著下降时就停下来——这个$N_c$就是性能和计算量的黄金平衡点。
5.4 约束的松紧:硬约束和软约束选择
硬约束在状态受限的物理系统上很常见,但不能无脑设硬。我强烈建议把控制量做成硬约束(执行器物理上限必须遵守),把状态量做成软约束(状态越界加惩罚但不硬性禁止)。
软约束的实现方法是在代价函数里加松弛变量$\varepsilon$:
$$
J = J_{nominal} + \rho \varepsilon^2
$$
并添加约束:
$$
x_{\min} - \varepsilon \le x \le x_{\max} + \varepsilon, \quad \varepsilon \ge 0
$$
$\rho$是松弛惩罚权重,越大越接近硬约束。这样即使出现不可行的情况,优化器也会给出一个“尽可能不越界”的软解,而不会直接崩溃。
这一招在做实车控制时救过我很多次。有一次我们给底层执行器留了硬约束,但模型失配导致预测状态进入不可行域,QP直接返回空解,整个系统停摆。改成软约束后,虽然极端情况下会小幅越界,但控制器一直有输出,系统不会无响应。对于安全边界,再在上层做越界告警处理,这样工程上既有约束力,又有鲁棒性。
5.5 仿真中常见的“参数地图”
我把这组系统参数下的调参趋势整理成表,方便你快速定位问题:
| 现象 | 优先调整方向 | 具体操作 |
|---|---|---|
| 跟踪太慢、上升时间过长 | 增大Q或减小R | Q×10或R÷10 |
| 控制量剧烈振荡 | 增大R或减小Nc | R×10或Nc减半 |
| 末端几步控制量乱跳 | 增大Np | Np×2 |
| 跟踪正弦滞后明显 | 增大Q | Q×10 |
| 求解时间超标 | 减小Np或Nc | Np减半或Nc减半 |
| 约束不可行报错 | 改软约束或增大约束范围 | 加松弛变量 |
记住这张表,你在调参时就有一个清晰的“故障排除地图”,不用瞎试。
6. 踩坑实录:我从仿真到实物中遇到的几个典型问题
这一节是整篇文章里我觉得最值钱的部分,全部来自实际调试中真踩过的坑。每一条都是花了几个小时甚至几天才定位到的,写出来给你排雷。
6.1 坑一:矩阵维度不匹配——根源往往是参考轨迹维数
MPC实现中最常见的报错就是矩阵维度不匹配。我见过最多的场景是:$N_p$和$N_c$修改后,Phi、Theta、R_ref矩阵没跟着重新生成,结果quadprog直接报维度错。
排查思路:先在每个矩阵构建位置加上size打印,确认Phi尺寸为[Np*ny, nx_tilde],Theta为[Np*ny, Nc],R_ref为[Np*ny, 1]。一般问题都在$R_{ref}$的行数上——因为参考轨迹只取了一个时刻的值,而不是整个预测时域的向量。
一个小技巧:写一个build_mpc_matrices(Np, Nc, Q, R, A_tilde, B_tilde, C_tilde)函数,把矩阵构建封装起来。每次改参数只需要调用函数,能大幅减少手误和排查时间。
6.2 坑二:quadprog返回空解——约束不可行是最隐蔽的坑
quadprog返回空解时,MATLAB通常会提示“No feasible solution found”或“Solver stopped prematurely”。很多时候你以为模型没问题、参数没问题,但问题出在约束互相矛盾。
举个我遇到的例子:初始状态$x(0)=0$,但参考轨迹第一秒的目标是0.5m,同时控制量上限只有1N,$N_p$又取得很长。MPC发现“想在10步内到达0.5m但力不够”,于是整个QP无可行解,返回空。
应对办法有三个层次:
- 把状态约束改成软约束(推荐);
- 给控制量上限留更大余量(物理允许的话);
- 对参考轨迹加斜率限制(rate limiter),避免初始阶段要求过高的跟踪速度。
你在仿真里先用软约束把系统跑起来,再逐步收紧约束,这样可以快速判断到底哪条约束在作怪。
6.3 坑三:离散化方式的“隐性误差”——欧拉法陷阱
我看到很多新手用欧拉法离散MPC模型:
matlab复制A_d = eye(2) + Ts * A_c;
B_d = Ts * B_c;
看起来没错,但当$T_s=0.1$、系统动态较快时,这种前向欧拉近似带来的预测误差会让MPC出现奇怪的振荡。为什么?因为MPC每次都在用“不太准”的模型预测,反馈机制虽然有纠错能力,但误差累积到一定程度就会导致跟踪性能下降甚至发散。
正确做法是始终用c2d(sys_c, Ts, 'zoh')或者矩阵指数求法。如果你担心c2d的数值问题,可以直接用MATLAB的expm手动算$A_d$和$B_d$:
matlab复制temp = expm([A_c, B_c; zeros(size(B_c,2), size(A_c,2)+1)] * Ts);
A_d = temp(1:size(A_c,1), 1:size(A_c,1));
B_d = temp(1:size(A_c,1), end);
这个本质上就是ZOH离散化的标准算法,算出来和c2d一致。
6.4 坑四:预测时域里的索引溢出——参考轨迹越界
仿真里,参考轨迹是预先生成的,长度为N_steps+1。当k靠近末尾时,k+Np可能超过数组长度。我在主循环里用了一个if k+i <= N_steps+1判断,超出部分直接取末值。这种做法在实际项目中我也常用,处理很简单,但如果你忘了加这个判断,程序会直接越界报错。
如果参考轨迹是实时生成的(比如来自上层的运动规划器),那么超出部分可以保持当前值或者用外推预测,这在跟踪问题中都是可以接受的。
6.5 坑五:控制周期与求解时间的矛盾
这是做实物控制时最“物理”的坑:MATLAB仿真里quadprog求解20步预测域可能只花几毫秒,但在低成本嵌入式平台上可能要到几十毫秒甚至更久。如果你的采样周期是10ms,而求解需要50ms,那这个MPC在实物上完全没法用。
对策:
- 减小$N_p$或$N_c$,直接减小QP规模;
- 用OSQP等嵌入式友好的求解器(OSQP在ARM上运行很快);
- 将MPC问题离线转化为显式MPC(explicit MPC),把在线优化变成查表;
- 用更简单的无约束线性解作为“热启动”,SP-QP求解速度能提升不少。
我建议在做任何实时MPC开发之前,先写一个简单的求解时间基准测试,把最坏情况下的QP求解时间测出来,再决定控制周期怎么设。这一步做晚了很容易在系统集成时被回火。
7. 扩展与进阶:从线性MPC到更实际的控制场景
如果你已经能跑通上面这个二阶系统的线性MPC,那恭喜,你已经掌握了线性MPC的核心框架。顺着这个框架往外走,有四个方向值得探索。
7.1 模型失配与扰动下的鲁棒性
仿真里模型是精确的,但现实中你拿到的$m$、$k$、$c$总有误差,还会有外部干扰。最简单的改进是加一个扰动观测器或者模型误差补偿。
一种常见思路:在每个采样时刻,用当前实际测量值$\mathbf{x}_m(k)$和预测值$\mathbf{x}_p(k)$做差,得到一个“扰动估计”$\mathbf{d}(k)=\mathbf{x}_m(k)-\mathbf{x}_p(k)$。然后把$\mathbf{d}(k)$作为外部干扰加入到预测模型里:
$$
\tilde{\mathbf{x}}(k+1) = \tilde{A}\tilde{\mathbf{x}}(k) + \tilde{B}\Delta u(k) + \tilde{d}(k)
$$
这样MPC在预测阶段已经考虑了当前模型误差,跟踪精度会提升不少。这就是所谓的扰动补偿MPC,实现成本低,效果显著。
7.2 状态约束和输出约束的软约束处理
工程上我不会轻易用硬状态约束,更推荐软约束方案。在quadprog里,松弛变量$\varepsilon$会被加入到优化变量中:
$$
\min_{\Delta U, \varepsilon} \frac{1}{2}\Delta U^T H \Delta U + g^T \Delta U + \rho \varepsilon^2
$$
约束变为:
$$
A_{con}\Delta U - \mathbf{1}\varepsilon \le b_{con}, \quad \varepsilon \ge 0
$$
这里的$\rho$取值一般在$10^3\sim10^6$,取决于你对约束越界的容忍度。$\rho$太小,约束形同虚设;$\rho$太大,数值容易病态。实践经验是先取$10^4$左右,观察越界幅度,再调整。
7.3 非线性化的方向
真正的弹簧阻尼系统如果弹簧是硬非线性(比如$kx + k_{nl}x^3$),线性MPC在偏离平衡点较大时效果就不好了。进阶方向是:
- 线性时变MPC(LTV-MPC):每个采样点重新线性化,预测模型随时间更新;
- 非线性MPC(NMPC):直接用非线性模型做优化,代价是求解难度暴涨,通常用SQP或IPA求解器;
- 反馈线性化+线性MPC:通过坐标变换将非线性系统转化为线性系统,再用线性MPC,适合结构明确的系统。
对做机器人和车辆控制的朋友,LTV-MPC应该是最实用的进阶方向——它保持了线性QP的良好计算特性,又能处理非线性程度中等的系统。
7.4 和无模型方法的对比
最后提一句和PID/LQR的对比。PID和LQR在模型简单、工作点固定、无约束场景下表现很好且计算极快;MPC的优势在于约束处理和前馈能力。如果你只需要在平衡点附近做小幅调节,且约束离工作点很远,那PID就够了,没必要上MPC。
反过来,如果需要大范围轨迹跟踪、状态或控制量有硬性约束、系统是多变量耦合的,那MPC就是“杀鸡用牛刀”——而且这牛刀还得配一个好模型。模型的质量直接决定MPC的上限,这是我做MPC这几年最深的体会。
8. 写在最后:给初学者的一个实验建议
如果你第一次跑通这套代码,我给一个建议:手动改一改参考轨迹的形状。先跑阶跃,看超调量和调节时间;再切换成正弦,看相位滞后;最后把约束上限降到2N,看MPC怎么“妥协”——它会在跟踪精度和物理限制之间找一个最优折中,这个过程你会非常直观地理解什么叫“约束优化”。
我在实际项目里做MPC调试时,最常用的招数是“约束压力测试”:把约束一点点收紧,观察系统行为从正常退化到不可行边界的全过程。这能帮你摸清系统的能力边界,也能让你在真机调试前就知道哪些约束不能太严。
模型预测控制不是银弹,它需要你理解模型、理解优化、理解你的执行器。但一旦你把这几方面打通,它能带来的性能红利和设计上的便利,是传统控制方法很难比拟的。希望这篇文章能帮你少走点弯路。
