1. 为什么需要 EKF?(非线性的痛点)
标准的卡尔曼滤波 (KF) 是完美的,但它有一个致命前提:系统必须是线性 的(即状态转移和观测都能用矩阵乘法 y=Ax+By = Ax + By=Ax+B 表示)。
然而,现实中的物理系统大多是高度非线性的。例如在车辆动力学或混动系统 (HEV) 中:
- 车辆运动学 :车辆的转向角和坐标变化包含大量的正弦和余弦函数(如 cos(θ)\cos(\theta)cos(θ))。
- 电池管理:电池的开路电压 (OCV) 与荷电状态 (SOC) 之间的关系是一条复杂的非线性曲线。
如果直接把非线性模型塞给标准 KF,高斯分布在经过非线性变换后就不再是高斯分布了,算法会直接崩溃。
2. EKF 的核心魔法:局部线性化
EKF 解决非线性问题的方法非常直接:泰勒展开 (Taylor Expansion) 。
它在当前状态的估计值附近,保留一阶导数,舍弃高阶项,强行把一段曲线"切线化",变成局部的直线。
在非线性系统中,状态方程和观测方程通常表示为:
xk=f(xk−1,uk−1)+wk−1x_k = f(x_{k-1}, u_{k-1}) + w_{k-1}xk=f(xk−1,uk−1)+wk−1
zk=h(xk)+vkz_k = h(x_k) + v_kzk=h(xk)+vk
(其中 fff 和 hhh 是非线性函数,www 和 vvv 是噪声)
为了让卡尔曼滤波的协方差矩阵能够继续更新,EKF 引入了雅可比矩阵 (Jacobian Matrix) 。你需要对 fff 和 hhh 求偏导:
- 状态转移雅可比矩阵:Fk−1=∂f∂x∣x^k−1F_{k-1} = \left. \frac{\partial f}{\partial x} \right|{\hat{x}{k-1}}Fk−1=∂x∂f x^k−1
- 观测雅可比矩阵:Hk=∂h∂x∣x^k∣k−1H_k = \left. \frac{\partial h}{\partial x} \right|{\hat{x}{k|k-1}}Hk=∂x∂h x^k∣k−1
3. 算法的两步走策略
和写代码的逻辑非常相似,EKF 也在死循环执行两个函数:
- Step 1: 预测 (Predict) - "根据物理规律盲猜"
- 用非线性模型预测下一时刻状态:x^k∣k−1=f(x^k−1,uk−1)\hat{x}{k|k-1} = f(\hat{x}{k-1}, u_{k-1})x^k∣k−1=f(x^k−1,uk−1)
- 用雅可比矩阵 FFF 预测误差协方差:Pk∣k−1=Fk−1Pk−1Fk−1T+QP_{k|k-1} = F_{k-1} P_{k-1} F_{k-1}^T + QPk∣k−1=Fk−1Pk−1Fk−1T+Q
- Step 2: 更新 (Update) - "根据传感器数据纠偏"
- 计算卡尔曼增益(权衡预测和测量的可信度):Kk=Pk∣k−1HkT(HkPk∣k−1HkT+R)−1K_k = P_{k|k-1} H_k^T (H_k P_{k|k-1} H_k^T + R)^{-1}Kk=Pk∣k−1HkT(HkPk∣k−1HkT+R)−1
- 结合实际测量值 zkz_kzk 更新状态:x^k=x^k∣k−1+Kk(zk−h(x^k∣k−1))\hat{x}k = \hat{x}{k|k-1} + K_k (z_k - h(\hat{x}_{k|k-1}))x^k=x^k∣k−1+Kk(zk−h(x^k∣k−1))
- 更新协方差:Pk=(I−KkHk)Pk∣k−1P_k = (I - K_k H_k) P_{k|k-1}Pk=(I−KkHk)Pk∣k−1
自动驾驶/车辆控制 中最经典、也是最基础的场景:2D 车辆运动学模型(自行车模型)的状态估计。
1. 场景设定与非线性模型定义
假设我们有一辆车,只能通过传感器测量它的 GPS 位置,但我们想知道它的精确位置和航向角。
状态向量 (State Vector, xxx) :
包含三个变量:横坐标、纵坐标、航向角(车头朝向)。
x=[pxpyθ]x = \begin{bmatrix} p_x \\ p_y \\ \theta \end{bmatrix}x= pxpyθ
控制输入 (Control Input, uuu) :
车辆的线速度和角速度(偏航率)。
u=[vω]u = \begin{bmatrix} v \\ \omega \end{bmatrix}u=[vω]
非线性状态转移方程 f(x,u)f(x, u)f(x,u) :
根据高中物理的运动学知识,假设采样时间为 Δt\Delta tΔt,下一时刻的状态可以表示为:
px,k=px,k−1+v⋅cos(θk−1)⋅Δtp_{x, k} = p_{x, k-1} + v \cdot \cos(\theta_{k-1}) \cdot \Delta tpx,k=px,k−1+v⋅cos(θk−1)⋅Δt
py,k=py,k−1+v⋅sin(θk−1)⋅Δtp_{y, k} = p_{y, k-1} + v \cdot \sin(\theta_{k-1}) \cdot \Delta tpy,k=py,k−1+v⋅sin(θk−1)⋅Δt
θk=θk−1+ω⋅Δt\theta_k = \theta_{k-1} + \omega \cdot \Delta tθk=θk−1+ω⋅Δt
这就是那个非线性函数 fff。之所以非线性,就是因为里面有 cos(θ)\cos(\theta)cos(θ) 和 sin(θ)\sin(\theta)sin(θ)。
2. 手推雅可比矩阵 (Jacobian Matrix)
为了让卡尔曼滤波能用,我们需要对这个非线性方程组求偏导数,构建状态转移雅可比矩阵 FFF。
雅可比矩阵的维度是 状态数 x 状态数(这里是 3x3),它的每一个元素是函数对状态变量的偏导:
F=[∂px,k∂px,k−1∂px,k∂py,k−1∂px,k∂θk−1∂py,k∂px,k−1∂py,k∂py,k−1∂py,k∂θk−1∂θk∂px,k−1∂θk∂py,k−1∂θk∂θk−1]F = \begin{bmatrix} \frac{\partial p_{x,k}}{\partial p_{x,k-1}} & \frac{\partial p_{x,k}}{\partial p_{y,k-1}} & \frac{\partial p_{x,k}}{\partial \theta_{k-1}} \\ \frac{\partial p_{y,k}}{\partial p_{x,k-1}} & \frac{\partial p_{y,k}}{\partial p_{y,k-1}} & \frac{\partial p_{y,k}}{\partial \theta_{k-1}} \\ \frac{\partial \theta_k}{\partial p_{x,k-1}} & \frac{\partial \theta_k}{\partial p_{y,k-1}} & \frac{\partial \theta_k}{\partial \theta_{k-1}} \end{bmatrix}F= ∂px,k−1∂px,k∂px,k−1∂py,k∂px,k−1∂θk∂py,k−1∂px,k∂py,k−1∂py,k∂py,k−1∂θk∂θk−1∂px,k∂θk−1∂py,k∂θk−1∂θk
逐行求导解析:
- 第一行 :px,kp_{x, k}px,k 对 pxp_xpx 求导是 1;对 pyp_ypy 求导是 0;对 θ\thetaθ 求导,cos(θ)\cos(\theta)cos(θ) 的导数是 −sin(θ)-\sin(\theta)−sin(θ),所以是 −v⋅sin(θ)⋅Δt-v \cdot \sin(\theta) \cdot \Delta t−v⋅sin(θ)⋅Δt。
- 第二行 :py,kp_{y, k}py,k 对 pxp_xpx 求导是 0;对 pyp_ypy 求导是 1;对 θ\thetaθ 求导,sin(θ)\sin(\theta)sin(θ) 的导数是 cos(θ)\cos(\theta)cos(θ),所以是 v⋅cos(θ)⋅Δtv \cdot \cos(\theta) \cdot \Delta tv⋅cos(θ)⋅Δt。
- 第三行 :θk\theta_kθk 对 pxp_xpx 是 0,对 pyp_ypy 是 0,对 θ\thetaθ 是 1。
最终推导出的雅可比矩阵 FFF :
F=[10−v⋅sin(θk−1)⋅Δt01v⋅cos(θk−1)⋅Δt001]F = \begin{bmatrix} 1 & 0 & -v \cdot \sin(\theta_{k-1}) \cdot \Delta t \\ 0 & 1 & v \cdot \cos(\theta_{k-1}) \cdot \Delta t \\ 0 & 0 & 1 \end{bmatrix}F= 100010−v⋅sin(θk−1)⋅Δtv⋅cos(θk−1)⋅Δt1
(至于观测模型,假设 GPS 只能测 pxp_xpx 和 pyp_ypy,这本身是个线性观测,雅可比矩阵 HHH 就是个简单的 [100010]\begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \end{bmatrix}[100100])。
3. Python 代码实现 (Numpy 矩阵计算)
理解了数学原理后,写代码就非常直观了。你可以把这段代码复制到本地的 Jupyter Notebook 跑一下。
python
import numpy as np
def ekf_predict_update(x_est, P, u, z, dt):
"""
x_est: 当前状态估计 [px, py, theta]
P: 协方差矩阵 (3x3)
u: 控制输入 [v, omega]
z: 观测值 [px_gps, py_gps]
dt: 采样时间
"""
# 噪声协方差矩阵 (现实中需要调参)
Q = np.diag([0.1, 0.1, np.deg2rad(1.0)]) ** 2 # 预测噪声
R = np.diag([1.0, 1.0]) ** 2 # 观测噪声 (GPS误差)
px, py, theta = x_est[0], x_est[1], x_est[2]
v, omega = u[0], u[1]
# ==========================
# Step 1: 预测 (Predict)
# ==========================
# 1. 物理模型非线性预测
x_pred = np.array([
px + v * np.cos(theta) * dt,
py + v * np.sin(theta) * dt,
theta + omega * dt
])
# 2. 计算我们在上面手推的雅可比矩阵 F
F = np.array([
[1.0, 0.0, -v * np.sin(theta) * dt],
[0.0, 1.0, v * np.cos(theta) * dt],
[0.0, 0.0, 1.0]
])
# 3. 预测协方差 P
P_pred = F @ P @ F.T + Q
# ==========================
# Step 2: 更新 (Update)
# ==========================
# 观测雅可比矩阵 H (只测量了位置 px, py)
H = np.array([
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0]
])
# 1. 计算卡尔曼增益 K
S = H @ P_pred @ H.T + R
K = P_pred @ H.T @ np.linalg.inv(S)
# 2. 根据实际观测值 z 修正预测值
# 理论观测值 z_pred = H @ x_pred
z_pred = H @ x_pred
x_est_new = x_pred + K @ (z - z_pred)
# 3. 更新协方差矩阵
I = np.eye(len(x_est))
P_new = (I - K @ H) @ P_pred
return x_est_new, P_new
# --- 测试运行 ---
x_init = np.array([0.0, 0.0, 0.0]) # 初始状态
P_init = np.eye(3) # 初始置信度
u_input = np.array([10.0, 0.1]) # 10m/s 直行并轻微打方向盘
z_measure = np.array([1.1, 0.0]) # GPS 读取到的位置
dt_step = 0.1
x_new, P_new = ekf_predict_update(x_init, P_init, u_input, z_measure, dt_step)
print("更新后的状态 [X, Y, Theta]:", x_new)
这个运动学模型不仅用于状态估计,在模型预测控制 (MPC) 中生成参考轨迹时也会高频使用。
我们分两步走:先通过 Python 绘图把刚刚的 EKF 算法跑起来,让你亲眼看到"滤波"的魔力;然后我们拔高视角,看看这个滤波器在真实的自动驾驶系统中,是如何与 MPC(模型预测控制)打配合的。
第一部分:Python 仿真与可视化(让代码动起来)
为了看出效果,我们让这辆车走一个圆弧轨迹。我们会模拟出真实的轨迹(Ground Truth) ,加上带有强烈噪声的 GPS 测量值 ,最后看看 EKF 估算出的轨迹 是如何在一堆噪点中还原出真实路线的。
你可以把这段代码和之前的 ekf_predict_update 函数放在同一个 Python 环境下运行:
python
import numpy as np
import matplotlib.pyplot as plt
# ===================== EKF 核心函数(必须有!)=====================
def ekf_predict_update(x, P, u, z, dt):
v, w = u
theta = x[2]
# 预测
x_pred = np.array([
x[0] + v * np.cos(theta) * dt,
x[1] + v * np.sin(theta) * dt,
x[2] + w * dt
])
F = np.array([
[1, 0, -v * np.sin(theta) * dt],
[0, 1, v * np.cos(theta) * dt],
[0, 0, 1]
])
Q = np.diag([0.1, 0.1, 0.01])
P_pred = F @ P @ F.T + Q
# 更新
H = np.array([
[1, 0, 0],
[0, 1, 0]
])
R = np.diag([2.0, 2.0]) ** 2
y = z - H @ x_pred
S = H @ P_pred @ H.T + R
K = P_pred @ H.T @ np.linalg.inv(S)
x_new = x_pred + K @ y
P_new = (np.eye(3) - K @ H) @ P_pred
return x_new, P_new
# ===================== 主程序 =====================
time_steps = 100
dt = 0.1
u = np.array([10.0, 0.5])
x_true = np.array([0.0, 0.0, 0.0])
x_est = np.array([0.0, 0.0, 0.0])
P_est = np.eye(3)
history_true = []
history_meas = []
history_est = []
for i in range(time_steps):
# 真实轨迹
x_true[0] += u[0] * np.cos(x_true[2]) * dt
x_true[1] += u[0] * np.sin(x_true[2]) * dt
x_true[2] += u[1] * dt
# 带噪声观测
z_meas = np.array([
x_true[0] + np.random.normal(0, 2.0),
x_true[1] + np.random.normal(0, 2.0)
])
# EKF
x_est, P_est = ekf_predict_update(x_est, P_est, u, z_meas, dt)
history_true.append(x_true.copy())
history_meas.append(z_meas.copy())
history_est.append(x_est.copy())
# 绘图
history_true = np.array(history_true)
history_meas = np.array(history_meas)
history_est = np.array(history_est)
plt.figure(figsize=(10,6))
plt.scatter(history_meas[:,0], history_meas[:,1], c='red', marker='x', alpha=0.5, label='GPS Measurement')
plt.plot(history_true[:,0], history_true[:,1], 'k--', linewidth=2, label='True Trajectory')
plt.plot(history_est[:,0], history_est[:,1], 'b', linewidth=3, label='EKF Estimate')
plt.title("EKF Tracking")
plt.xlabel("X")
plt.ylabel("Y")
plt.legend()
plt.grid(True)
plt.show()
你会看到什么?
运行后,图上会有散落得满地都是的红叉(模拟糟糕的 GPS 信号),一条黑色的虚线(真实的圆弧),以及一条蓝色的实线(EKF 结果)。你会惊喜地发现,蓝线完美地穿过了红叉的"迷雾",紧紧贴合着黑色的真实轨迹。这就是状态估计的价值:在信息不完美的世界里,用数学推导出真相。
第二部分:EKF 与 MPC 的"左膀右臂"协同
在自动驾驶的控制架构中,**状态估计(EKF)和控制算法(MPC)**永远是一对形影不离的好兄弟。理解它们的配合,是跨越到系统级思维的关键。
1. 为什么 MPC 离不开 EKF?
MPC(模型预测控制)的逻辑是:"如果我知道当前的精确状态,我就能推演出未来10步该怎么打方向盘最优。"
但现实很骨感:
- 传感器有噪声(如刚才代码里的红叉)。
- 有些物理量根本测不到(比如车辆在冰面上的侧偏角)。
所以,EKF 是 MPC 的"眼睛"。传感器把满是杂音的数据丢给 EKF,EKF 进行滤波和状态重构,然后把干净、完整的状态向量(位置、速度、航向角等)交给 MPC。MPC 拿到这些"确切"的情报,才敢放心大胆地去解算油门和转向。
2. EKF 与 MPC 的数据闭环架构
你可以把它们的交互想象成一个以毫秒为单位疯狂运转的死循环:
- 读取数据 :传感器读取 GPS、IMU 数据 (zkz_kzk)。
- EKF 发力 (Estimation) :EKF 根据上一时刻的预测,结合当前观测 zkz_kzk,计算出当前最准的状态估计值 x^k\hat{x}_kx^k。
- MPC 发力 (Control) :MPC 接手 x^k\hat{x}_kx^k。它内部也有一个车辆模型,它会计算出未来一段时间的最优控制指令 uku_kuk(比如:方向盘转角 10 度,油门 20%)。
- 执行与反馈 :控制指令 uku_kuk 下发给底层线控系统,车子动了。
- 情报共享 :MPC 计算出的 uku_kuk 会立刻抄送一份给 EKF。因为 EKF 在下一个周期进行"物理模型预测"(Step 1)时,必须知道驾驶员(或系统)刚刚打了多少方向盘、踩了多少油门。
3. 它们共享的核心:车辆运动学模型
这是最巧妙的一点!在现代工程实践中,EKF 用来做预测的非线性方程 f(x,u)f(x, u)f(x,u),和 MPC 用来预测未来轨迹的模型,通常是同一个数学模型(比如我们刚才手推雅可比矩阵的那个自行车模型)。
既然你已经掌握了如何对自行车模型进行泰勒展开局部线性化 (求雅可比矩阵),那么你其实已经掌握了 LTV-MPC (线性时变模型预测控制) 的核心第一步。LTV-MPC 也是每一帧都在当前状态点进行线性化,然后把非线性问题变成二次规划 (QP) 问题来光速求解。
站在程序设计的角度来看,这段代码其实就是一个典型的状态机(State Machine)在时间轴上的迭代。
在车辆控制系统中,无论是跑在工控机里的 C++ 程序,还是在 Simulink 里搭的模块,其底层逻辑都和这段 Python 脚本如出一辙。
我们把这段代码拆解为三个层面来详细看:核心算法层 、仿真环境层 和工程调参(玄学)层。
1. 核心算法层:ekf_predict_update 函数
这个函数是真正的"大脑",它严格执行了我们之前推导的数学公式。它分为两个阶段:
A. 预测阶段 (Predict) ------ "纯靠物理规律盲猜"
python
# 物理模型非线性预测
x_pred = np.array([
x[0] + v * np.cos(theta) * dt,
x[1] + v * np.sin(theta) * dt,
x[2] + w * dt
])
- 这三行代码就是车辆的运动学自行车模型 。它假设系统是完美的:如果没有外界干扰,我以速度 vvv 和角速度 www 跑了 Δt\Delta tΔt 时间,我"应该"在这个位置。
python
# 雅可比矩阵计算与协方差预测
F = np.array([...])
P_pred = F @ P @ F.T + Q
F就是我们手动求导得出的雅可比矩阵,它代表了当前状态下系统的线性化切线。P_pred是预测的误差协方差矩阵 。@是 Numpy 中的矩阵乘法。随着时间推移,只要没有传感器的数据进来,系统的误差是会不断累积发散的,这里的Q就是每次迭代强行加进去的系统噪声。
B. 更新阶段 (Update) ------ "结合传感器纠偏"
python
y = z - H @ x_pred # 计算残差 (Innovation)
H是观测矩阵,因为我们只测了 GPS 的 X 和 Y,所以它是针对前两个状态变量的。y非常关键,在控制理论里叫残差 (Residual / Innovation) 。它等于"传感器实际测到的位置"减去"我们刚刚盲猜的位置"。如果y为 0,说明我们猜得无比准确。
python
S = H @ P_pred @ H.T + R
K = P_pred @ H.T @ np.linalg.inv(S) # 计算卡尔曼增益
- 这是整个算法最耗算力的地方(包含矩阵求逆
np.linalg.inv)。K就是卡尔曼增益 (Kalman Gain)。它的核心作用是"分锅":究竟是预测的模型更准,还是传感器的测量更准?
python
x_new = x_pred + K @ y
P_new = (np.eye(3) - K @ H) @ P_pred
- 最终,把残差
y乘以增益K,补偿回我们盲猜的x_pred上,得到这一帧最终的最优估计x_new。同时收敛更新协方差P_new。
2. 仿真环境层:主程序循环 (for 循环)
主程序其实是在电脑里构建了一个微型的数字孪生 (Digital Twin) 测试台。
python
for i in range(time_steps):
# 1. 真实轨迹 (Ground Truth)
x_true[0] += ...
# 2. 带噪声观测 (Simulated Sensor)
z_meas = np.array([
x_true[0] + np.random.normal(0, 2.0), ...
])
- 在真实的无人车开发中,
x_true是上帝视角,我们是不知道的。在这里我们用它来模拟一台绝对理想的车辆,生成一条完美的圆弧。 z_meas模拟了一个极其劣质的 GPS 模块。np.random.normal(0, 2.0)意味着在真实坐标基础上,加入了均值为 0、标准差为 2 米的高斯白噪声。这也就是为什么画出来的红叉散落得那么厉害。
3. 控制工程的"玄学":Q 和 R 矩阵
代码里有两行不起眼但决定生死的代码:
Q = np.diag([0.1, 0.1, 0.01]):系统过程噪声协方差。它反映了你对车辆物理模型的信任程度。R = np.diag([2.0, 2.0]) ** 2:观测噪声协方差。它反映了你对GPS 传感器的信任程度。
在实际写底层车辆控制算法时,公式推导完毕只是第一步,工程师可能要花一个月的时间在实车上调 Q 和 R 的参数:
- 如果把
R调得非常小,EKF 会认为 GPS 极其准确,滤波出来的轨迹就会跟着满屏的红叉剧烈震荡。 - 如果把
Q调得非常小,EKF 会变得极其"固执",只相信自己的物理模型,此时如果车辆发生打滑(不符合自行车模型了),滤波器就会彻底跑偏。
推开 MPC(模型预测控制)的大门,你其实就跨入了一个全新的维度:从"被动地估算现在"变成了"主动地规划未来"。
从代码和算法设计的视角来看,MPC 的本质根本不是传统意义上的反馈控制(像 PID 那样算误差),而是一个在死循环里疯狂求解的"受限最优化问题" (Constrained Optimization Problem)。
为了让你直观感受到这种降维打击的快感,我们直接拆解它在 Python 和 Simulink 中的实现思路。
1. MPC 的核心灵魂:滚动优化 (Receding Horizon)
想象你正在开夜车,车灯只能照亮前方 50 米(这就是预测区间 Prediction Horizon, NNN)。
- 看前方 :根据当前的起步状态 x0x_0x0(由 EKF 提供)和车辆运动学模型,预测未来 50 米的不同开法。
- 算代价:计算哪种开法能最贴合车道中心线,同时打方向盘最省力。
- 走一步 :虽然你脑海里规划了未来 50 米的最优动作序列 [u0,u1,...,uN−1][u_0, u_1, \dots, u_{N-1}][u0,u1,...,uN−1],但你只执行第一个动作 u0u_0u0。
- 重新算:车子往前走了一步,吸收新的 EKF 状态,把预测区间往前推,再重新计算未来 50 米。
用严谨的数学公式表达这个过程,就是每一帧都在解下面这个代价函数 (Cost Function):
minU∑k=0N−1(∥xk−xref,k∥Q2+∥uk∥R2)\min_{U} \sum_{k=0}^{N-1} \left( \|x_{k} - x_{ref, k}\|{Q}^2 + \|u_k\|{R}^2 \right)Umink=0∑N−1(∥xk−xref,k∥Q2+∥uk∥R2)
约束条件 (Constraints):
- 动力学约束 :xk+1=f(xk,uk)x_{k+1} = f(x_k, u_k)xk+1=f(xk,uk) (即我们之前推导的车辆自行车模型,必须符合物理规律)。
- 执行器约束 :umin≤uk≤umaxu_{min} \le u_k \le u_{max}umin≤uk≤umax (方向盘不能打过 360 度,油门不能踩进油箱里)。
2. 实现路径对决:Python 纯代码 vs Simulink 框图
在控制工程领域,Python 和 Simulink 代表了两种完全不同的开发哲学。
| 维度 | Python 实现 (如 CasADi, OSQP) | Simulink 实现 (MPC Toolbox) |
|---|---|---|
| 思维逻辑 | 代码流。强调用矩阵和符号变量显式构建最优化问题,然后丢给外部求解器。 | 信号流。基于模型设计 (MBD),拉拽模块,关注系统级的输入输出关系。 |
| 模型处理 | 需要手动或通过符号计算库(如 CasADi)对非线性模型进行雅可比求导和离散化。 | 可以在 Simulink 里搭好复杂的非线性 Plant 框图,MPC 模块往往能自动线性化。 |
| 调试体验 | 打印矩阵排错,适合与强化学习 (PPO/RL) 等其他 AI 算法做无缝数据交互和对比。 | 可视化极佳,示波器一目了然,一键生成 C 代码部署到 dSPACE 等硬件在环 (HIL) 设备。 |
| 门槛 | 需深入理解优化理论、二次规划 (QP) 转化的底层逻辑。 | 上手极快,像调参游戏,但容易变成"知其然不知其所以然"的调包侠。 |
3. 具体实现思路拆解
路线 A:Python 中的符号化编程 (推荐使用 CasADi)
在 Python 里手撕 MPC,最忌讳用 scipy.optimize,因为太慢了。工业界和学术界目前的主流是 CasADi 库。它的逻辑非常清晰:
- 定义变量 :用符号变量(Symbolic variables)声明状态 XXX 和控制输入 UUU。
- 构建预测模型 :在一个
for循环中,把车辆运动学模型 xk+1=f(xk,uk)x_{k+1} = f(x_k, u_k)xk+1=f(xk,uk) 变成显式的符号表达式。 - 堆叠代价函数 :把每一步的误差累加到一个大变量 JJJ 里。
- 添加约束:把边界条件(如转角限制)加到一个列表里。
- 召唤求解器:调用内部的非线性求解器(如 IPOPT)。你只需要传入当前状态和参考轨迹,它就会瞬间吐出最优控制序列。
路线 B:Simulink 中的模块化搭建
如果你在系统性地学习控制理论,Simulink 是绕不开的工程利器。
- 搭建被控对象 (Plant) :用普通的积分器、三角函数模块把 px,py,θp_x, p_y, \thetapx,py,θ 的微分方程搭出来,或者直接写一个 MATLAB Function。
- 引入 MPC 模块 :从库里拖出
Model Predictive Controller模块。 - 配置参数 (GUI) :双击打开 MPC Designer 界面,在这个可视化界面里:
- 设定预测步长 (Prediction Horizon, PPP) 和控制步长 (Control Horizon, MMM)。
- 在表格里直接填入权重矩阵 QQQ 和 RRR。
- 填入执行器的上下限 (Constraints)。
- 闭环仿真:把 MPC 的输出连到 Plant,把 Plant 的状态拉回 MPC,点击 Run 看示波器。
第一部分:Python + CasADi (非线性 MPC 轨迹跟踪)
在 Python 里做 MPC,最核心的逻辑就是:定义变量 →\rightarrow→ 写出代价函数 →\rightarrow→ 加入物理约束 →\rightarrow→ 丢给求解器。
我们需要用到强大的符号计算与最优化库 CasADi (你可以通过 pip install casadi 安装)。下面是一段极简但完整的非线性 MPC(NMPC)代码,我们要让车辆追踪一条直线。
python
import casadi as ca
import numpy as np
import matplotlib.pyplot as plt
# ================= 1. MPC 参数设置 =================
N = 10 # 预测区间 (Prediction Horizon):往前看 10 步
dt = 0.1 # 采样时间
sim_time = 50 # 总仿真步数
# ================= 2. 建立 CasADi 优化器 =================
opti = ca.Opti()
# ================= 3. 定义优化变量 (状态和控制) =================
# X 矩阵维度: 3 行 (x, y, theta), N+1 列
X = opti.variable(3, N + 1)
# U 矩阵维度: 2 行 (v, omega), N 列
U = opti.variable(2, N)
# 定义外部传入的参数 (每一帧都在变)
x_current = opti.parameter(3) # 车辆当前真实状态
x_reference = opti.parameter(3) # 车辆希望到达的目标状态 (这里简化为一个固定目标)
# ================= 4. 编写目标函数 (Cost Function) =================
# Q 矩阵惩罚位置误差,R 矩阵惩罚打方向盘和踩油门的剧烈程度
Q = np.diag([10.0, 10.0, 1.0])
R = np.diag([0.1, 0.5])
cost = 0
for k in range(N):
state_error = X[:, k] - x_reference
# 累加状态误差代价和控制努力代价
cost += ca.mtimes([state_error.T, Q, state_error]) + ca.mtimes([U[:, k].T, R, U[:, k]])
opti.minimize(cost)
# ================= 5. 添加约束条件 (Constraints) =================
# 约束 A: 初始状态必须等于当前实际状态
opti.subject_to(X[:, 0] == x_current)
# 约束 B: 运动学模型约束 (车辆必须符合物理规律)
for k in range(N):
# 下一步的状态 = 当前状态 + 变化率 * dt (欧拉积分)
x_next = X[0, k] + U[0, k] * ca.cos(X[2, k]) * dt
y_next = X[1, k] + U[0, k] * ca.sin(X[2, k]) * dt
theta_next = X[2, k] + U[1, k] * dt
opti.subject_to(X[0, k+1] == x_next)
opti.subject_to(X[1, k+1] == y_next)
opti.subject_to(X[2, k+1] == theta_next)
# 约束 C: 执行器物理极限
opti.subject_to(opti.bounded(-5.0, U[0, :], 5.0)) # 速度 v 的范围 [-5, 5]
opti.subject_to(opti.bounded(-np.pi/4, U[1, :], np.pi/4)) # 角速度 omega 的范围 [-45度, 45度]
# 配置底层的非线性求解器 IPOPT
opts = {'ipopt.print_level': 0, 'print_time': 0, 'ipopt.sb': 'yes'}
opti.solver('ipopt', opts)
# ================= 6. 闭环仿真 (滚动优化) =================
# 初始位置 (0,0) 面朝右,目标位置 (10, 10)
current_state = np.array([0.0, 0.0, 0.0])
target_state = np.array([10.0, 10.0, np.pi/4])
history_x = []
history_y = []
for i in range(sim_time):
# 1. 给优化器传入当前获取的最新情报
opti.set_value(x_current, current_state)
opti.set_value(x_reference, target_state)
# 2. 求解最优化问题!
try:
sol = opti.solve()
# 3. 提取算出的未来 N 步控制量,但**只取第一个**执行
u_optimal = sol.value(U)[:, 0]
except:
# 如果问题无解,保持上一帧指令
u_optimal = np.array([0.0, 0.0])
# 4. 把控制量输入给现实中的车辆 (这里用纯数学模拟)
current_state[0] += u_optimal[0] * np.cos(current_state[2]) * dt
current_state[1] += u_optimal[0] * np.sin(current_state[2]) * dt
current_state[2] += u_optimal[1] * dt
history_x.append(current_state[0])
history_y.append(current_state[1])
# 画图
plt.plot(history_x, history_y, '-o', label="MPC Trajectory")
plt.scatter([10], [10], color='red', marker='*', s=200, label="Target")
plt.legend(); plt.grid(True); plt.title("CasADi Nonlinear MPC"); plt.show()
看代码的重点 :注意到 opti.solve() 没?MPC 的本质就是每一帧都在强行解一个带方程约束的多元函数极值问题。
第二部分:Simulink (基于模型设计的系统架构)
在工程上,我们不可能把 Python 代码直接刷进汽车的 ECU 里。主流车企(如博世、比亚迪)大多使用 Simulink,它的逻辑是"连线"与"模块化"。
在 Simulink 中实现我们刚才的非线性 MPC,分为四大步:
步骤 1:搭建"被控对象" (Plant)
你需要在 Simulink 画板上,把真实的车辆物理模型搭出来。
-
拉一个 MATLAB Function 模块,双击进去写代码:
matlabfunction x_dot = bicycle_kinematics(x, u) v = u(1); omega = u(2); theta = x(3); x_dot = [v*cos(theta); v*sin(theta); omega]; end -
把这个模块的输出连到一个 Integrator (积分器) 模块上。这样积分器的输出就是车辆真实的连续状态 [px,py,θ][p_x, p_y, \theta][px,py,θ]。
步骤 2:拖入核心大脑 (Nonlinear MPC 模块)
因为我们的系统是非线性的(带 sin/cos),普通的 MPC 模块无法直接处理。
- 在库浏览器搜索并拖入 Nonlinear MPC Controller 模块。
- 双击打开它,你会看到一个非常硬核的配置界面:
- Number of states (状态数): 填 3
- Number of outputs (输出数): 填 3
- Number of manipulated variables (控制变量数): 填 2
步骤 3:配置预测模型与参数 (GUI 调参)
在 Nonlinear MPC 模块的属性界面里,你需要告诉控制器系统的物理规律是什么:
- Prediction Model (预测模型): 指向你刚才写的那个 MATLAB 运动学函数。系统会利用内置的算法(如多重打靶法)自动在后台帮你完成我们之前手推的雅可比矩阵线性化!
- Horizons :
- Prediction horizon (PPP): 填 10 到 20
- Control horizon (MMM): 填 2 到 5 (为了节省算力,控制步长通常远小于预测步长)
- Weights (权重): 这里就是 Python 里的 Q 和 R 矩阵,直接在表格里填数字。
- Constraints (约束): 同样在表格里设定 Manipulated Variable (MV) 的 Max 和 Min,比如转向角限制。
步骤 4:连线形成闭环 (Closed Loop)
现在要把所有的器官连起来:
- 将外部的目标参考轨迹 (Reference) 连入 NLMPC 模块的
ref端口。 - 将 Plant (积分器) 输出的真实状态,连入 NLMPC 模块的
mo(Measured Output) 或x(State) 端口。(注:在实际工程中,这里中间必须插入我们在上一回写的 EKF 模块!由 EKF 给 MPC 提供干净的 State)。 - 将 NLMPC 模块计算出的控制指令
mv(Manipulated Variable),连回 Plant 的输入端。 - 点击仿真顶部的 Run 按钮,用 Scope (示波器) 观察轨迹收敛过程。
总结一下区别
- Python (CasADi) 强迫你理解 MPC 背后的非线性规划数学本质,对于学术研究、搞懂原理、验证新算法(比如把神经网络加进 MPC)来说是无敌的。
- Simulink 屏蔽了底层数学求解器的复杂性,让你专注于信号流向、工程约束(比如延迟、丢包)和 C 代码生成,是去车企找工作必须点满的技能树。