简介:这份资源面向机器人控制、自动化与运动控制方向的学习者和研究者,聚焦基于模型预测控制(MPC)的机械臂轨迹跟踪算法实现,帮助解决机械臂在动态特性、外界干扰与模型不确定性下难以高精度跟踪期望轨迹的问题。压缩包共5个文件,以4个MATLAB脚本(.m)和1个APDL参数化建模文件为主,整体约9KB,其中APDL文件可用于机械臂结构设计与参数化建模,MATLAB脚本则承担预测模型构建、在线优化求解与仿真验证等任务。资源已有174人学习下载,适合具备一定控制理论基础、希望动手复现MPC轨迹跟踪流程的读者。通过包内代码与仿真模型,读者可梳理从机械臂运动学与动力学建模、预测模型设计、性能指标与约束构建,到滚动优化求解及实时反馈校正的完整链路,并借助MATLAB与ANSYS联合仿真加深对控制策略的理解,为后续算法改进与工程落地提供可参考的实现框架。
1. 机械臂轨迹跟踪的模型预测控制:从理论到代码的完整落地
很多做机械臂控制的朋友都遇到过这样的场景:用 PID 调了半天,轨迹跟踪效果始终差强人意,尤其是在高速段或者负载变化时,末端执行器的实际轨迹和期望轨迹之间的偏差能大到让人怀疑人生。这时候如果还死磕 PID,基本就是玄学调参了。模型预测控制(MPC)之所以在机械臂轨迹跟踪这个场景里越来越受关注,核心原因在于它能把系统的动力学约束、关节限位、力矩饱和这些硬约束直接写进优化问题里,而不是像传统方法那样先设计控制器再事后打补丁。这份资源围绕基于 MPC 的机械臂轨迹跟踪算法展开,包含完整的推导思路和可运行的代码实现,适合已经掌握机器人学基础、想从传统控制方法切换到 MPC 框架的从业者,也适合做毕设或课题需要快速搭建验证平台的同学。下面我会按“理论怎么立住 → 代码怎么跑通 → 坑在哪里”的顺序,把这份资源拆开讲透。
2. MPC 轨迹跟踪的数学骨架:从连续动力学到离散 QP 问题
2.1 机械臂动力学模型与状态空间表达
机械臂的动力学方程标准形式是:
[ M(q)\ddot{q} + C(q,\dot{q})\dot{q} + G(q) = \tau ]
其中 ( q ) 是关节角度向量,( M(q) ) 是惯性矩阵,( C(q,\dot{q}) ) 包含科氏力和离心力项,( G(q) ) 是重力项,( \tau ) 是关节力矩。做 MPC 的第一步是把这套二阶方程转成一阶状态空间形式。定义状态 ( x = [q, \dot{q}]^T ),控制输入 ( u = \tau ),则:
[ \dot{x} = f(x, u) = \begin{bmatrix} \dot{q} \ M^{-1}(q)(\tau - C(q,\dot{q})\dot{q} - G(q)) \end{bmatrix} ]
这个非线性形式直接拿去做 MPC 会得到一个非线性优化问题,求解成本高。常见做法是在参考轨迹附近做线性化,得到线性时变模型:
[ \dot{x} = A(t)x + B(t)u + d(t) ]
其中 ( A(t) = \partial f/\partial x |{(x_r, u_r)} ),( B(t) = \partial f/\partial u |{(x_r, u_r)} )。这一步的线性化质量直接决定了后续跟踪精度,参考轨迹变化剧烈时,线性化误差会明显放大。
2.2 离散化与预测方程的推导
连续模型不能直接塞进数字控制器,需要离散化。我一般用零阶保持器(ZOH)或者四阶龙格库塔法。ZOH 简单但要求控制周期足够小,龙格库塔精度更高但计算量稍大。以 ZOH 为例:
[ x_{k+1} = A_d x_k + B_d u_k ]
其中 ( A_d = e^{A T_s} ),( B_d = \int_0^{T_s} e^{A \tau} d\tau \cdot B ),( T_s ) 是采样周期。接下来定义预测时域 ( N_p ) 和控制时域 ( N_c ),把未来 ( N_p ) 步的状态展开成关于当前状态和未来控制序列的表达式:
[ \mathbf{X} = \Phi x_k + \Gamma \mathbf{U} ]
这里 ( \mathbf{X} = [x_{k+1|k}, \ldots, x_{k+N_p|k}]^T ),( \mathbf{U} = [u_{k|k}, \ldots, u_{k+N_c-1|k}]^T )。( \Phi ) 和 ( \Gamma ) 是由 ( A_d, B_d ) 递推得到的矩阵。这个展开过程是 MPC 代码实现里最容易写错的地方之一,后面避坑章节会细说。
2.3 目标函数与约束的 QP 标准化
MPC 的目标函数通常写成跟踪误差和控制增量的加权平方和:
[ J = \sum_{i=1}^{N_p} |x_{k+i|k} - x_{ref,k+i}|Q^2 + \sum{i=0}^{N_c-1} |\Delta u_{k+i|k}|_R^2 ]
把预测方程代入后,目标函数变成关于 ( \mathbf{U} ) 的二次型:
[ J = \frac{1}{2} \mathbf{U}^T H \mathbf{U} + f^T \mathbf{U} + \text{const} ]
约束包括关节角度上下限、关节速度上下限、力矩上下限,以及控制增量约束。这些统一写成 ( A_{ineq} \mathbf{U} \leq b_{ineq} ) 的形式。最终问题就是一个标准的二次规划(QP),可以用 OSQP、qpOASES 或者 MATLAB 的 quadprog 求解。
提示:QP 求解器的选择对实时性影响很大。OSQP 对稀疏问题友好,qpOASES 在中小规模问题上更稳定,建议两个都试一下再定。
3. 代码实现:从动力学建模到 QP 求解的完整链路
3.1 环境准备与依赖安装
这份资源的代码基于 Python 实现,核心依赖包括 NumPy、SciPy、OSQP 和 Matplotlib。如果你习惯用 MATLAB,逻辑完全一致,只是矩阵操作换成 MATLAB 语法。先建一个干净的虚拟环境:
python -m venv mpc_arm_env source mpc_arm_env/bin/activate # Windows 用 mpc_arm_env\Scripts\activate pip install numpy scipy osqp matplotlibNumPy 负责矩阵运算,SciPy 用来做连续系统的离散化(scipy.linalg.expm),OSQP 是 QP 求解器,Matplotlib 用来画轨迹对比图。版本上没有特别苛刻的要求,但 OSQP 建议用 0.6.x 以上,老版本在稀疏矩阵处理上有一些已知问题。
3.2 机械臂动力学模型的代码实现
以一个二连杆平面机械臂为例,动力学项可以解析写出。实际项目中如果关节数更多,建议用 Pinocchio 或 RBDL 这类库自动生成 ( M, C, G )。下面是二连杆的解析实现:
import numpy as np def dynamics(q, dq, tau, m1, m2, l1, l2, g=9.81): """ 二连杆机械臂动力学 q: 关节角度 [q1, q2] dq: 关节角速度 [dq1, dq2] tau: 关节力矩 [tau1, tau2] 返回 ddq: 关节角加速度 """ q1, q2 = q dq1, dq2 = dq # 惯性矩阵 M(q) M11 = (m1 + m2) * l1**2 + m2 * l2**2 + 2 * m2 * l1 * l2 * np.cos(q2) M12 = m2 * l2**2 + m2 * l1 * l2 * np.cos(q2) M21 = M12 M22 = m2 * l2**2 M = np.array([[M11, M12], [M21, M22]]) # 科氏力与离心力项 C(q, dq) h = -m2 * l1 * l2 * np.sin(q2) C = np.array([[h * dq2, h * (dq1 + dq2)], [-h * dq1, 0]]) # 重力项 G(q) G = np.array([(m1 + m2) * g * l1 * np.cos(q1) + m2 * g * l2 * np.cos(q1 + q2), m2 * g * l2 * np.cos(q1 + q2)]) # 求解加速度 ddq = np.linalg.solve(M, tau - C @ dq - G) return ddq这段代码里 ( M ) 矩阵的推导基于标准二连杆模型,( h = -m_2 l_1 l_2 \sin(q_2) ) 是科氏力系数的核心项。参数 ( m_1, m_2 ) 是连杆质量,( l_1, l_2 ) 是连杆长度,( g ) 是重力加速度。实际使用时这些参数需要根据你的机械臂 DH 参数表来填。注意 ( C ) 矩阵的写法不唯一,只要满足 ( \dot{M} - 2C ) 是反对称矩阵即可,我这里用的是常见的 Christoffel 符号展开形式。
3.3 线性化与离散化的实现细节
在参考轨迹上做线性化,数值上最稳妥的方法是用中心差分:
def linearize_dynamics(x_ref, u_ref, dt, eps=1e-6): """ 在参考点附近数值线性化,返回离散后的 Ad, Bd x_ref: [q1, q2, dq1, dq2] u_ref: [tau1, tau2] dt: 采样周期 """ n = len(x_ref) m = len(u_ref) A = np.zeros((n, n)) B = np.zeros((n, m)) def f(x, u): q = x[:2] dq = x[2:] ddq = dynamics(q, dq, u, m1=1.0, m2=1.0, l1=0.5, l2=0.5) return np.concatenate([dq, ddq]) # 中心差分求雅可比 for i in range(n): dx = np.zeros(n) dx[i] = eps A[:, i] = (f(x_ref + dx, u_ref) - f(x_ref - dx, u_ref)) / (2 * eps) for j in range(m): du = np.zeros(m) du[j] = eps B[:, j] = (f(x_ref, u_ref + du) - f(x_ref, u_ref - du)) / (2 * eps) # 前向欧拉离散化(采样周期小时够用) Ad = np.eye(n) + A * dt Bd = B * dt return Ad, Bd中心差分的步长 ( \epsilon ) 取 ( 10^{-6} ) 左右比较合适,太大截断误差明显,太小会引入浮点噪声。离散化这里用了前向欧拉,因为 MPC 的采样周期通常很小(1ms 到 10ms),前向欧拉的精度足够。如果你对精度要求更高,可以把 ( A ) 和 ( B ) 拼成增广矩阵后用scipy.linalg.expm做精确离散化。
3.4 预测矩阵的构建与 QP 求解
预测矩阵 ( \Phi ) 和 ( \Gamma ) 的构建是 MPC 代码的核心。假设预测时域 ( N_p = 20 ),控制时域 ( N_c = 5 ):
def build_prediction_matrices(Ad, Bd, Np, Nc): """ 构建预测方程 X = Phi @ x0 + Gamma @ U """ n = Ad.shape[0] m = Bd.shape[1] Phi = np.zeros((n * Np, n)) Gamma = np.zeros((n * Np, m * Nc)) # 计算 Ad 的幂次 Apow = [np.eye(n)] for i in range(1, Np + 1): Apow.append(Apow[-1] @ Ad) for i in range(Np): Phi[i*n:(i+1)*n, :] = Apow[i+1] for j in range(min(i + 1, Nc)): Gamma[i*n:(i+1)*n, j*m:(j+1)*m] = Apow[i-j] @ Bd return Phi, Gamma这里 ( \Phi ) 的第 ( i ) 块行对应 ( A_d^{i+1} ),( \Gamma ) 的第 ( i ) 块行第 ( j ) 块列对应 ( A_d^{i-j} B_d )。注意当 ( j > i ) 时,控制量对状态没有影响,所以内层循环用min(i+1, Nc)截断。这个细节如果写错,QP 求解出来的控制序列会完全不对,但代码不会报错,属于典型的“静默翻车”。
构建好预测矩阵后,QP 的标准形式是:
import osqp import scipy.sparse as sparse def solve_mpc(Ad, Bd, x0, x_ref_seq, Q, R, Np, Nc, u_min, u_max, du_max): """ 求解 MPC 的 QP 问题 x_ref_seq: 参考轨迹序列,形状 (Np, n) """ n = Ad.shape[0] m = Bd.shape[1] Phi, Gamma = build_prediction_matrices(Ad, Bd, Np, Nc) # 构建 Hessian 和梯度 Q_bar = sparse.kron(sparse.eye(Np), sparse.csc_matrix(Q)) R_bar = sparse.kron(sparse.eye(Nc), sparse.csc_matrix(R)) H = 2 * (Gamma.T @ Q_bar @ Gamma + R_bar) # 参考轨迹展开 x_ref_flat = x_ref_seq.flatten() f = 2 * Gamma.T @ Q_bar @ (Phi @ x0 - x_ref_flat) # 约束:控制量上下限 A_ineq = sparse.vstack([ sparse.eye(m * Nc), -sparse.eye(m * Nc) ]) # 控制增量约束需要额外处理,这里简化为控制量约束 u_min_seq = np.tile(u_min, Nc) u_max_seq = np.tile(u_max, Nc) l = np.concatenate([u_min_seq, -u_max_seq]) u = np.concatenate([u_max_seq, -u_min_seq]) # 求解 prob = osqp.OSQP() prob.setup(sparse.csc_matrix(H), f, sparse.csc_matrix(A_ineq), l, u, verbose=False, max_iter=2000, eps_abs=1e-4, eps_rel=1e-4) res = prob.solve() if res.info.status != 'solved': print(f"QP 求解失败,状态: {res.info.status}") U_opt = res.x.reshape(Nc, m) return U_opt[0] # 只取第一个控制量,滚动优化这段代码里 ( H ) 是 Hessian 矩阵,( f ) 是梯度向量。约束部分只写了控制量上下限,控制增量约束需要额外构造差分矩阵,实际项目中建议加上,否则力矩突变会很明显。OSQP 的eps_abs和eps_rel设到 ( 10^{-4} ) 在 1ms 控制周期下基本能保证实时性,如果求解时间超标,优先放宽这两个参数或者减小 ( N_p )。
4. 避坑与排查:MPC 轨迹跟踪里最容易翻车的五个地方
4.1 现象:轨迹跟踪在低速段还行,高速段直接发散
原因:线性化误差在高速段被放大。参考轨迹速度大时,( A(t) ) 和 ( B(t) ) 变化剧烈,用当前时刻线性化模型预测未来 20 步,模型失配严重。
解决:两个方向。一是缩短预测时域 ( N_p ),从 20 降到 10 甚至 8,牺牲一点前瞻性换稳定性。二是改用线性时变 MPC(LTV-MPC),在每个预测步都用对应的参考点线性化,而不是只用当前时刻的模型。代码上就是把linearize_dynamics放进预测循环里,每个 ( i ) 步单独算 ( A_d^{(i)}, B_d^{(i)} ),代价是计算量翻倍。
4.2 现象:QP 求解器返回solved inaccurate或者直接unbounded
原因:约束之间互相矛盾。比如关节角度上限设成了 ( [\pi, \pi] ),但参考轨迹要求关节转到 ( 1.2\pi ),QP 找不到可行解。或者控制增量约束和力矩约束冲突。
解决:先检查约束的物理合理性。关节角度范围要留余量,别贴着机械限位设。如果确实需要跟踪超出限位的轨迹,要么放宽约束,要么在参考轨迹生成阶段就做限幅。另外 OSQP 的max_iter默认 4000,如果经常跑到上限,说明问题规模太大或者约束太紧,适当减小 ( N_c ) 能显著降低求解时间。
4.3 现象:控制量抖动严重,力矩输出像锯齿
原因:目标函数里没有惩罚控制增量,只惩罚了控制量本身。MPC 每步都在重新优化,没有增量惩罚时,相邻两步的控制量可能跳变很大。
解决:在目标函数里加 ( \Delta u ) 的惩罚项。实现上把状态向量扩展为 ( [x, u_{k-1}] ),控制量变成 ( \Delta u ),这样 QP 的决策变量就是控制增量而不是控制量。代码改动量不大,但效果立竿见影。( R ) 矩阵的权重从 ( 0.01 ) 起步调,太大响应变慢,太小抖动压不住。
4.4 现象:仿真里效果很好,上真机后完全不能用
原因:仿真用的动力学参数和真机不一致,尤其是摩擦项没建模。仿真里 ( \tau ) 直接等于计算值,真机上还有关节摩擦、减速器间隙、电机动力学延迟。
解决:在动力学模型里加粘性摩擦项 ( F_v \dot{q} ) 和库仑摩擦项 ( F_c \text{sign}(\dot{q}) )。参数辨识可以用简单的匀速跟踪实验,记录力矩和速度,做线性回归。另外控制周期要留余量,仿真跑 1ms 的,真机建议先跑 5ms 验证,稳定后再往下压。
4.5 现象:参考轨迹跟踪在换向点出现明显滞后
原因:MPC 的预测模型里没有考虑执行器的动态响应,力矩指令发出后电机需要时间响应,换向时这个延迟被放大。
解决:两个办法。一是把执行器动态建模成一阶惯性环节,扩展进状态空间,让 MPC 提前补偿。二是在参考轨迹生成时做换向点的平滑处理,用 S 型速度规划代替梯形规划,减少加速度突变。我一般两个都做,先平滑轨迹再扩展模型,效果最稳。
5. 进阶技巧:用热启动和时域自适应把 MPC 跑到 1kHz
5.1 热启动:把上一次的解喂给下一次 QP
MPC 每个控制周期都在解结构相似的 QP,上一次的最优解是这一次很好的初始猜测。OSQP 支持热启动:
# 第一次求解后保存状态 prob.solve() # 下一次求解前,用上一次的解初始化 prob.warm_start(x=res.x, y=res.y)热启动在轨迹平滑变化时能减少 30% 到 50% 的迭代次数。注意如果参考轨迹突变,热启动反而可能拖慢收敛,这时候可以检测解的变化率,超过阈值就冷启动。
5.2 时域自适应:根据跟踪误差动态调整预测时域
固定 ( N_p ) 很难同时兼顾跟踪精度和计算负载。一个实用技巧是:跟踪误差大时增大 ( N_p ) 提高前瞻性,误差小时减小 ( N_p ) 降低计算量。实现上维护一个误差滑动窗口,根据窗口内的均方误差查表调整 ( N_p )。我一般设三档:( N_p = 8, 15, 25 ),切换时对 QP 矩阵做增量更新而不是完全重建,能省不少时间。
5.3 验证方法:用频域指标量化跟踪性能
时域图看多了容易麻木,建议补一个频域验证。对跟踪误差做 FFT,看主要能量集中在哪个频段。如果低频段误差大,说明模型精度不够或者 ( Q ) 权重太小;如果高频段误差大,说明控制量抖动没压住,回去调 ( R ) 或者加低通滤波。下面这段代码可以直接用:
from scipy.fft import fft, fftfreq def analyze_tracking_error(error_seq, dt): """ error_seq: 跟踪误差时间序列 dt: 采样周期 返回频率和对应的幅值 """ n = len(error_seq) yf = fft(error_seq) xf = fftfreq(n, dt)[:n//2] amplitude = 2.0/n * np.abs(yf[:n//2]) return xf, amplitude把期望轨迹和实际轨迹的差值喂进去,看幅值谱。如果 10Hz 以上还有明显峰值,说明有高频抖动,优先查控制增量惩罚和传感器噪声。如果 1Hz 以下误差大,那就是模型或者权重的问题。
5.4 一个我踩过的坑
早期做 MPC 的时候,我习惯把 ( Q ) 矩阵设成对角阵,位置误差权重给大,速度误差权重给小。结果跟踪效果一直不温不火,后来才发现速度误差不惩罚的话,MPC 会为了减小位置误差而让速度剧烈波动,反过来又影响位置精度。把速度权重加到位置权重的 0.5 倍左右之后,整体跟踪误差降了将近一半。从那以后我每次调 MPC 权重,都强制先把 ( Q ) 矩阵的速度项和位置项比例调一遍,再动其他参数。希望这个习惯能帮到你。
本文还有配套的精品资源,点击获取