嵌入式非线性模型预测控制:acados如何用单次QP实现实时迭代

发布时间:2026/10/19 8:48:09
嵌入式非线性模型预测控制:acados如何用单次QP实现实时迭代 简介acados 是一套面向非线性最优控制与嵌入式优化的高效求解器聚焦模型预测控制MPC与最优控制问题OCP的实时求解适合从事机器人、自动驾驶、电力电子等领域的算法工程师与科研人员。资源提供 C、Python、MATLAB、Octave 等接口并包含完整的安装与构建配置可快速集成到不同开发环境。 整个压缩包共 1227 个文件约 21.72MB。其中 371 个 C 源码与 122 个头文件构成求解器核心296 个 .m 文件是 MATLAB/Octave 接口与示例88 个 Python 脚本覆盖 Python 端用法另有 78 个 txt 说明、42 个 shell 脚本、31 个 C 文件及 CMake/Makefile 构建配置便于理解工程结构、二次开发与跨平台编译。 已有 1979 人浏览学习。代码工程还保留了 include、examples、interfaces 等典型目录形态从底层求解算法到上层接口演示均有涉及并附有安装说明、引用信息与示例用例适合需要快速搭建非线性优化控制原型或深入研究 acados 实现细节的开发者可作为非线性最优控制与嵌入式优化的参考实现。1. 非线性最优控制的嵌入式求解为什么「每次只解一个 QP」反而更可靠非线性模型预测控制NMPC在很长一段时间里被默认成「上层优化」的事每次更新控制量都要把非线性规划NLP迭代到收敛桌面机上都要几十毫秒放上嵌入式目标板更是想都不敢想。acados 把这个习惯反过来——它默认不把 NLP 收敛每个采样周期只做一次 SQP 迭代在当前工作点把动力学、目标函数和约束线性化解一个 QP立刻输出控制量。这个看似「偷懒」的做法恰好因为上一拍留下的解、对偶乘子和 KKT 残差就是当前问题的最佳初值在采样控制语境下比「每次迭代到收敛」更稳。值得搞透的不是它快在哪而是它凭什么敢只解一个 QP又是怎样把这条链路在嵌入式 C 环境里变成可预测的计算量和固定内存。2. 从 NLP 到单个 QPRTI 是把「在线最优控制」变「例行公事」的机制非线性最优控制问题的难点在于系统动力学是非线性的约束也是非线性的每一拍的控制更新都要面对一个全新的非线性规划。acados 的核心策略是用「实时迭代」Real-Time IterationRTI把这个问题拆成可预测的几步。这一章先把这条链路的起点讲清楚连续问题怎么变成 NLPNLP 的 Hessian 为什么可以只算一阶导以及为什么只迭代一步就够用。2.1 直接多段打靶连续最优控制怎么变成有限维 NLPacados 处理的对象是标准的连续时间最优控制问题给定状态方程 ẋ f(x, u, p)在时间窗 [0, T] 上最小化min J φ(x(T)) ∫₀ᵀ L(x(t), u(t), p) dt同时满足 x(0) x₀路径约束 g(x,u) ≤ 0 和状态/控制的上下界。直接法的思路是把连续问题离散成有限维 NLP 再求解。acados 用的是直接多段打靶direct multiple shooting——把时间轴切成 N 段每一段用 Runge-Kutta 积分器把动力学「压缩」成一步映射 Φ_k(x_k, u_k)于是优化变量变成每个打靶节点上的状态 x_k 和控制 u_kmin Σₖ₌₀^{N-1} l_k(x_k, u_k) m(x_N) s.t. x_{k1} Φ_k(x_k, u_k), k 0 … N-1 x₀ x(t₀), g_k(x_k, u_k) ≤ 0选多段打靶而不是单段打靶是刻意的N 个积分区间各自有独立的状态初值只在一步映射处耦合。即便开环动力学不稳定、某个 xi 猜错误差也只被限制在对应区间里不会沿着整条轨迹放大。更重要的是每个 Φ_k 的积分和求导彼此独立天然适合并行也为后面 RTI 的「提前准备」留了口子。积分器本身也有讲究。acados 同时提供显式 Runge-KuttaERK和隐式 Runge-KuttaIRK支持 Gauss-Legendre 与 Radau 配置刚性系统用 IRK弱非线性、采样快的问题用 ERK。每个积分区间内部还要解一个非线性方程对 IRK 而言这个 Newton 迭代的次数和容差直接决定 Φ_k 的精度也决定后面雅可比是否与积分轨迹自洽——这个坑在第 5 章会专门讲。2.2 GGN 近似为什么最常见的目标函数形式能绕开二阶导数NLP 用 SQP 求解时QP 子问题需要目标函数的 Hessian。如果目标函数是「任意非线性函数」Hessian 要么用有限差分、要么用自动微分去算二阶导代价都不小在嵌入式上根本不现实。acados 的默认选择是 Gauss-Newton 近似GGN前提是代价写成最小二乘形式l_k(x_k, u_k) ½ (y_k − y_ref)ᵀ W (y_k − y_ref) y_k h_k(x_k, u_k)其中 y_k 是输出可以是状态的任意非线性函数y_ref 是参考轨迹W 是权重矩阵。在当前迭代点把 y_k 线性化Hessian 的近似就是 J_yᵀ W J_y其中 J_y ∂h/∂(x_k, u_k)。这样一来只需要一阶灵敏度——也就是雅可比——就能得到半正定的凸 QP 子问题。这里有个经常被误解的点GGN 不是「偷工减料地省略二阶项」而是在利用最小二乘结构的性质。如果残差 y − y_ref 在解处接近零GGN 的局部收敛速度和精确二阶法一样快即便残差不小只要问题本身是「追踪型」的GGN 在实践中的表现也足够可靠。acados 里代价类型有 LINEAR_LS、NONLINEAR_LS 和 EXTERNAL 三种前两种走 GGNEXTERNAL 才需要你显式选择 BFGS 或精确 Hessian。对嵌入式场景把代价改写成最小二乘形式几乎总是最划算的一步。2.3 RTI 的单次迭代为什么「上一拍的解」是最优初值完整 SQP 的流程是线性化、解 QP、更新变量、检查 KKT 残差不达标就再来一轮直到收敛。对时变系统这意味着在每个采样周期里可能要连续解多个 QP计算量既不可控也无法预算。RTI 只做一件事把这套 SQP 迭代压缩成「每拍恰好一次 QP」。为什么一步就够关键在于采样控制的时序特性。相邻两个采样周期里测量值的变化、参考轨迹的移动、扰动的累积相对于系统带宽通常都是小量。也就是说前后两拍的 NLP 解在解空间中离得很近。上一拍留下的解 x、对偶乘子 π 和控制 u本身就是这一拍问题的一个好初值。SQP 本质是 Newton 型迭代在解附近有超线性收敛性质——初值误差小一次迭代就能把残差压回和「解的移动速度」同一个量级。用控制语言说一步 Newton 迭代的收缩速度追得上解在采样间隔内的漂移速度。RTI 的每次迭代可以拆成固定的几件事在当前状态和上一拍的 (x, u, π) 处求雅可比、组装 QP、解 QP、把第一个控制分量施加给被控对象。这个流程没有「循环直到收敛」的退出条件所以每拍耗时天然确定。它牺牲的那点最优性在反馈回路的噪声和扰动面前根本不可分辨——这就是「每次只解一个 QP 反而更可靠」的机制根源。至于怎么把这几步调度到采样周期的空隙里第 4 章再展开。3. 结构是关键condensing、Riccati 递归与免动态内存RTI 把 NLP 压成了单次 QP但 QP 本身仍可能很大N 段状态 × 每段 n_x 维。如果不利用问题结构一个 n_x·N 维的稠密 QP 在嵌入式上依然跑不动。acados 的下一步是用三层结构手段把 QP 压到可执行condensing 降维度、HPIPM 的 Riccati 递归利用带状结构、免动态内存设计让一切在执行前就定好。3.1 Condensing消元状态把问题压成控制量维度在 SQP 迭代里动力学约束被线性化成仿射形式x_{k1} A_k x_k B_k u_k d_k注意这组等式是顺序耦合的x_k 由 x_{k-1} 决定。所以每个 x_k 都能写成 x₀ 和 u₀ … u_{k-1} 的仿射函数。把这些表达式代入代价和约束消掉全部状态变量得到一个只含控制量 u₀ … u_{N-1} 的稠密 QP——这一步就是 condensing。消元之后问题维度从 n_x·N n_u·N 降到 n_u·N对「状态多、控制少」的典型机械系统比如 n_x10、n_u2收益非常可观。但全量 condensing 有个代价消元过程本身是 O(N²) 量级的递归运算得到的稠密 QP 求解复杂度随 N 近似立方增长而且稠密矩阵占内存。acados 因此提供 partial condensing用qp_solver_cond_N控制折中程度# acados Python 接口condensing 与 QP 求解器设置 ocp.solver_options.qp_solver PARTIAL_CONDENSING_HPIPM ocp.solver_options.qp_solver_cond_N 20 # 把 N 段折成 20 个块 ocp.solver_options.qp_solver_iter_max 15 # 内点法固定迭代次数这里的cond_N的意思是把 N 个打靶区间分组成cond_N个「元区间」每组内部做全量消元组与组之间保留状态耦合的带状结构。取 cond_N N 就是完全稠密取 cond_N 1 就是完全稀疏。实际调参时它是「内存占用、消元开销、QP 求解开销」三者间的旋钮——不是越大越好也不是越小越好要在目标板上实测决定。3.2 HPIPM 的 Riccati 递归带状 KKT 为什么能线性时间求解partial condensing 之后QP 的 KKT 系统保留了一个关键性质状态方程按时间顺序耦合海森矩阵和控制/状态约束组合起来是块三对角的带状矩阵。通用稀疏求解器会把它当成普通稀疏阵做符号分解分解代价随 N 增长很快且需要动态内存。acados 自带的 HPIPM 求解器则走一条完全不同的路Riccati 递归。常见的实现思路是对 KKT 系统做分块高斯消元从最后一拍开始逐步消去 x_k每消一步只对一个 (n_x n_u) 大小的小稠密矩阵做一次分解再把结果回代到前一步。整个求解过程是前向-后向两趟扫描总计算量 O(N · (n_xn_u)³)随 N 线性增长。这个复杂度只有在动力学呈顺序结构时才成立——这就是为什么 acados 愿意把所有宝都押在「最优控制问题」这个结构假设上。HPIPM 的另一个嵌入式友好特性是固定迭代次数。内点法本质是迭代法但 acados 允许你设定qp_solver_iter_max到点就停。对 RTI 来说这不是坏事QP 解只是整个反馈回路的一环这一拍误差大一点下一拍 RTI 的线性化会帮你补回来。实践中常见设定 10~20 次内点迭代配合上一拍的乘子做 warm startKKT 残差通常已经足够低。3.3 免动态内存一次 create、零 malloc 的运行时嵌入式实时系统最怕的不是慢而是「不可预测」不知道这段代码要分配多少内存、会不会触发缺页、最坏执行时间是多少。acados 在架构上把这个问题从根上消掉——问题建立和求解分离。在 Python/MATLAB 接口里配置好 OCP 之后AcadosOcpSolver的创建过程会精确算出每个工作区缓冲区的大小状态、控制、灵敏度、QP 数据、乘子、松弛变量全部在一次create里分配完毕。运行时热路径上的solve()只读写在预先分配好的内存里全程不调用 malloc/free。对裸机环境甚至可以把整个工作区放进静态内存段编译期就锁定 footprint。注意这意味着问题规模N、n_x、n_u、约束个数必须在创建求解器时就定死。想运行时动态改预测时域做不到也不该做——这正是换取 WCET最坏执行时间可分析性的代价。4. 代码生成与求解器栈从符号模型到 C 内核的编译链路前面讲的是算法机制这一章回答「怎么落到能跑在板子上的 C 代码」。acados 的嵌入式属性来自一条完整的编译链路符号建模 → 代码生成 → 静态库 → 实时循环调用。链路里每一步都在为「可预测、无分配、可上板」服务。4.1 CasADi 代码生成雅可比如何变成纯 C 函数模型在 Python 或 MATLAB 里用符号变量定义比如 CasADi 的 SX 符号状态、控制、参数、输出函数、约束函数全部符号化。acados 调用代码生成器把这些符号函数翻译成纯 C 代码自动做公共子表达式消除CSE并保留函数内的稀疏性。生成结果像下面这样/* 示意acados 实际生成的模型函数签名各版本细节略有差异 */ int model_dynamics_expl_vde_forw( const real_t *x, const real_t *u, const real_t *p, real_t *xdot, real_t *Sx, real_t *Su);这里的vde_forw是 variational differential equations 的前向模式在积分名义轨迹的同时把变分方程 d/dt δx ∂f/∂x · δx ∂f/∂u · δu 一起积分。返回值 Sx 和 Su 就是一步映射对状态和控制的灵敏度即 RTI 线性化需要的 A_k、B_k。用同一次积分同时得到 Φ_k 和它的雅可比既省计算又保证了线性化和轨迹预测自洽——这一点对 RTI 至关重要。生成后的 C 函数不分配任何内存输入输出都走调用方提供的缓冲区且数据布局对缓存友好。acados 还允许你用model选项选择前向还是伴随adjoint灵敏度状态多、控制少用前向划算反之用伴随更省。4.2 求解器栈SQP-RTI 为何默认、什么时候退回完整 SQPacados 的求解器是分层可插拔的外层是 NLP 求解器SQP 或 SQP-RTI内层是 QP 求解器默认 HPIPM也支持 qpOASES 等。外层和内层的搭配决定了每一拍算多狠。外层求解器每拍解 QP 次数适用场景SQP-RTI恰好 1 次在线反馈控制默认模式SQP完整迭代多次直到 KKT 收敛启动阶段、轨迹规划、离线仿真为什么默认是 RTI因为它把算法结构和硬件时序对齐了单次 QP 固定迭代次数耗时可以预算。但 RTI 有个前提——初值要足够接近解。冷启动、参考突变、任务切换这些情况会让上一拍的解失效这时应该先跑几轮完整 SQP把解拉回跟踪状态再切回 RTI 模式。实践里常见的手法是启动时用 full SQP 跑一次进入稳态后切 RTI。Hessian 的选择也在这里默认 GGN但如果代价不是最小二乘形式就得选 BFGS只需要梯度用迭代维护的近似 Hessian或 EXTERNAL用户提供精确二阶导。BFGS 在强非线性代价下比 GGN 更稳代价是多一份内存和每拍额外的更新计算。4.3 准备与反馈的调度怎么把「算一半」留到上一个周期RTI 每拍的工作量并不均匀。求雅可比、condensing、QP 分解这些「准备」阶段和最终的回代求解除 QP 相比前者重得多。acados 把 solve 显式拆成两个相位# RTI 相位拆分acados Python 接口示意不同版本写法略有差异 acados_ocp_solver.set(rti_phase, preparation) acados_ocp_solver.solve() # 提前完成求导、condensing 与 QP 分解 # …… 等新测量值到来 …… acados_ocp_solver.set(rti_phase, feedback) acados_ocp_solver.solve() # 只做稀疏回代输出当前控制量逻辑很直接上一拍发完控制量到下一拍测量到来之间有一段硬件空闲。这段时间足够完成准备相位QP 的 KKT 矩阵已经被分解好了。新测量一到反馈相位只是把新的 x₀ 代入已分解的 KKT 系统做回代毫秒级甚至微秒级就能出控制量。这让整个回路的输入输出延迟从「一个完整 SQP」压缩到「一次稀疏回代」对不稳定系统的稳定化是质的差别。提示RTI 调度只在硬件确认「准备相位能在测量间隔内完成」时才成立。上板前务必用示波器或计时器量出两个相位各自的实际耗时。5. 移植避坑嵌入式 NMPC 最容易翻车的 5 个问题把 acados 从桌面仿真搬到板子上翻车点几乎全集中在「时序、数值、结构」三个词上。以下 5 条是实践中最高频的踩坑记录每条都按「现象 → 原因 → 解决」拆开讲。5.1 采样周期内算不完看似 CPU 不够快其实调度没拆开现象控制循环在示波器上频繁超时RTI 模式下solve()依然吃掉整个采样周期。原因绝大多数情况不是 CPU 不够快而是把准备相位和反馈相位合在一起放到测量之后执行或者qp_solver_cond_N设得太大QP 实际规模虚高再或者内点法没有设迭代上限遇到病态问题迭代次数失控。解决先用rti_phase把准备相位挪到上一拍然后把qp_solver_cond_N从 N 降到 10~20再给 HPIPM 设qp_solver_iter_max10~20 次足够。最后用求解器返回的stat字段分别读两个相位的耗时确认瓶颈在求导、condensing 还是 QP 求解逐个击破。5.2 状态量纲悬殊QP 病态让每个控制量都在抖现象模型里某个状态是 1e6 量级、另一个是 1e-4 量级求解结果高频抖动甚至直接报不可行。原因KKT 矩阵里各块的数值尺度差几个数量级条件数爆炸。固定迭代次数下内点法残差根本压不下去返回的解在数值上不可信。解决给状态和控制做归一化等价于对变量做对角缩放后重写模型或者把权重矩阵 W 改成按量纲归一后的对角阵。这是典型的「改模型比改求解器参数有用」的场景。从stat里看残差下降曲线——如果每拍都停在同一个高位上下不去十有八九是尺度问题。5.3 积分容差设得不对Φ 和雅可比对不上线性化就是错的现象解析雅可比单独验证全对但 RTI 控制效果差预测轨迹和实际开环轨迹明显偏离。原因IRK 积分器内部的 Newton 迭代没收敛就提前返回或者容差设得太松导致一步映射 Φ_k 和灵敏度 Sx、Su 不是从同一次内部解得到的。QP 基于这套不一致的线性化预测自然一路偏。解决把 integrator 的newton_tol和最大迭代次数调到一个折中档——容差太松线性化失真太紧又拖慢准备相位。上板前做一次开环预演用生成的 Φ 前向推一条轨迹再用 A_k、B_k 递推同样轨迹两条线在预测时域内必须重合。5.4 软约束太「软」QP 工作集每拍都在换控制量来回抽动现象没有真正触碰硬约束但控制序列高频切换看起来像抖振。原因软约束惩罚权重设得太小约束可以随意进出边界同时上一拍的对偶乘子没有被正确 warm startQP 每拍都从零乘子出发活动集判别完全随机化。解决提高软约束权重或者把关键约束改成硬约束在每拍结束时显式保存乘子和松弛变量下一拍通过set写回去。记得看stat里的 QP 迭代次数——如果每拍迭代次数波动很大多半是 warm start 丢了。5.5 换用通用 QP 求解器把嵌入式最值钱的结构优势扔了现象把 QP 求解器从 HPIPM 换成某个通用稀疏求解器后内存占用上一个量级最坏执行时间无法保证。原因通用求解器面对的是「任意稀疏矩阵」必须做符号分解、动态分配工作区无法利用状态方程的顺序耦合结构——它们把 O(N) 的 Riccati 递归变成了 O(N³) 的普通稀疏分解。解决嵌入式目标上优先保留 HPIPM。若确实要用 qpOASES 这类主动集方法先确认问题规模小、活动集变化不频繁并在目标板上做一轮完整的 WCET 测试。别在仿真环境里换求解器仿真里的「快」和板子上的「可预测」是两回事。6. 设计取舍acados 敢把宝押在结构假设上acados 的一切速度都来自一个赌注你手里的问题真的是「最优控制问题」——状态方程按时间顺序耦合、代价能写成非线性最小二乘、控制维数远小于状态维数、问题规模在创建求解器时固定。这个假设一旦成立condensing 的维度压缩、HPIPM 的 Riccati 递归、免动态内存的架构就全部成立假设不成立它就退化为一个不伦不类的通用求解器既没有 IPOPT 那样处理任意稀疏结构的鲁棒性也没有手写专用代码的极致性能。这个取舍最明显的地方在约束上。Riccati 递归只认「顺序耦合」的动力学遇到跨所有打靶节点的全局耦合约束比如整条轨迹的燃料积分上限带状结构立刻被破坏。常见的处理方法是把积分量补成一个额外状态让耦合重新变成局部的。代价是状态维数 1换来结构不变。这不是 acados 的缺陷而是它「选择性放弃通用性」的必然结果——每个商业级嵌入式求解器都得选一个主攻结构选完就要拒绝不属于这个结构的问题。GGN 和 RTI 的搭配同样是一场交易。GGN 在残差小的追踪问题里表现出二阶收敛的潜力但目标函数如果是强非凸的比如障碍规避里的排斥势场一阶近似可能把 QP 引向错误方向RTI 一步迭代的前提是解的漂移速度小于 Newton 收缩速度参考轨迹阶跃这类场景会让它失效。这些都不是 bug是「为速度预设的容差」。真正的工程能力体现在什么情况下切 full SQP什么情况下把容差放宽什么时候干脆把模型改写成结构友好的形式。验证方法也很朴素上板之前先在桌面环境把 RTI 相位拆分、固定迭代次数、workspace 大小这三个数字写进设计文档再用stat里的 KKT 残差做闭环监控最后在目标板上测两个相位的 WCET。这三件事做完acados 对你就不再是黑匣子。从某次把完整 QP 求解塞进反馈时刻、结果在示波器上看到整整一拍延迟之后我每接一个嵌入式 NMPC 任务都强制自己在第一周内先跑一遍「相位拆分 固定迭代 内存统计」三板斧再谈调参。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询