简介:acados 是一套面向嵌入式实时控制与非线性最优控制(NMPC)的快速求解器,为控制工程师与科研人员提供 C、Python、MATLAB、Octave 多语言接口,覆盖从算法原型验证到嵌入式部署的完整链路,尤其适合机器人、自动驾驶、工业过程控制等对算力与实时性要求较高的场景,也适合已有优化控制基础的学习者深入研究与二次开发,无论学术研究中的算法验证还是工业现场的控制方案落地,都能从中找到可复用的工程实现。压缩包共1227个文件,整体约21.72MB,类型以 C 源文件、MATLAB m 文件、Python 脚本、C++ 源文件与头文件为主:C/C++ 承担底层求解和接口封装,m/py 文件提供高层建模、仿真与调用示例,h/hpp 文件用于定义数据结构和 API;同时附带 CMake/Makefile 构建脚本,以及 Markdown、PDF 说明文档,方便本地安装、查看、修改,压缩包体积不大,便于快速获取并开展实验验证。目前已有1979人浏览学习。工程内含完整源码树,可先执行 git submodule update --recursive --init 初始化子模块,再按需选择 CMake 或 Makefile 构建,并通过调整 BLASFEO 目标适配不同嵌入式平台;大量 CasADi 集成示例(如 ERK4 链式例子)与代码生成接口,能够帮助读者借助符号运算生成导数与雅可比矩阵,再交由 acados 完成实时数值优化,从而快速理解 NMPC 的完整实现流程,并基于此搭建面向实际应用的优化控制模块。
1. acados:为什么嵌入式设备上的非线性最优控制,终于不再靠“玄学”调参
做控制的人大多有过这样的经历:在 PC 上辛辛苦苦搭好的轨迹优化,一部署到嵌入式控制器上就翻车。要么是计算时间跑不过控制周期,要么是内存占用直接击穿芯片的 RAM 预算,要么是优化器动不动就告诉你不收敛,最后只能靠经验拍几个惩罚系数硬顶。非线性最优控制(NMPC)在理论上是漂亮的,但在工程上长期是个“黑匣子”——模型预测控制的核心机制人人能讲,可真正把它压缩进一个几块钱的 MCU 里还能稳定跑,靠的是求解器底层那些看不见的设计取舍。acados 这个快速嵌入式求解器的价值,就是把这些取舍摊开给你看:它为什么敢说“快速”,为什么能把实时最优控制做成确定性行为。这篇文章从底层机制拆开讲,读完你会明白 acados 的架构设计到底解决了什么问题,以及为了快,它在算法和工程上各自牺牲了什么、换来了什么。
2. 求解器为什么慢:从“迭代一次”到“实时闭环”的鸿沟
2.1 NMPC 的计算瓶颈本质上是“内点法在数值优化层面的连锁反应”
大多数非线性模型预测控制问题,最终都落到一个有限时域的最优控制离散化问题上:给定当前状态,求一串控制输入,使得代价函数最小,同时满足系统动力学约束、状态约束和控制约束。这个问题的数学形式可以写成
min_{x_0..x_N, u_0..u_{N-1}} sum_{k=0}^{N-1} l(x_k, u_k) + m(x_N) s.t. x_{k+1} = f(x_k, u_k), g(x_k, u_k) <= 0
如果直接用通用的非线性优化求解器(比如某些开源内点法实现)去解这个问题,你会发现它往往在“迭代一次”这个环节就卡住了。原因不是迭代算法的收敛性差,而是每一步迭代里要反复计算大规模稀疏雅可比矩阵和海森矩阵,还要对 KKT 条件做因式分解。内点法每次迭代都要重新求一次搜索方向,这在高维稀疏系统上虽然比稠密法快一些,但依然要付出高昂的线性代数代价。
2.2 同一个问题,acados 看到的却是“结构”
acados 的切入点很朴素:既然我面对的是一个固定的、时间步进的最优控制问题,那我为什么不利用它的块三对角结构,把求解过程剪枝掉一大半?这就像我们拿到一个带重复模式的方程组,不会老老实实去做通用矩阵求逆,而是把重复块拎出来复用。
常见的实现思路是:把整个时域的优化变量看成一个大向量,但约束矩阵是分块稀疏的。通用求解器会把这个矩阵当成抽象的黑匣子,内部做稀疏 LU 分解时虽然能利用一些稀疏性,但不会针对“每步状态变量只与前后两步相关”这种特定结构做定制。acados 在底层直接把这个结构暴露给求解器:它采用的是直接配点法加实时迭代(real-time iteration,简称 RTI)方案,允许在每个采样周期内只做一次完整的 SQP 迭代,然后用上一次的解作为热启动初值。
2.3 从“迭代到收敛”到“每步只做一次迭代”
这其实是一个反直觉的机制切换:传统 NMPC 要求当前时刻的优化问题必须求解到收敛才能下发控制量,一旦计算时间超出控制周期,整个闭环就失效了。RTI 的思路是——既然控制周期通常只有几毫秒甚至亚毫秒,那么我每次都从上一时刻的解出发,做一次 SQP 迭代,得到的解虽然不是当前问题的最优解,但它已经处于最优解的邻域内。配合反馈系统的渐进稳定性,这一“迭代解”完全足够驱动控制器。
acados 把这个机制做成了一等公民。它在每个采样周期内做的事情是:先对非线性动力学做一次灵敏度传播(也就是在当前状态和当前控制下算出雅可比),然后求解一个二次规划(QP)子问题,最后把 QP 的最优解作为新的控制量输出。整个过程没有多层嵌套迭代,而是一次线性化、一次 QP 求解,完事。这个机制听起来简单,但实现上的难点全部集中在 QP 求解器如何充分利用热启动信息以及如何处理不等式约束的激活集变化。
3. 核心机制:实时迭代 SQP 与外部库深度绑定
3.1 为什么选择 SQP 而不是内点法作为上层框架
NMPC 的问题天然是带强约束的非线性规划,SQP(序列二次规划)的哲学是把非线性问题在每次迭代时近似成一个带线性约束的二次规划,然后不断更新近似点。这个做法的最大优点是:一旦你有一个又快又稳的 QP 求解器,非线性问题的求解速度就基本由 QP 求解器的单次求解速度决定。而 acados 的架构正是围绕“把 QP 子问题求解做到极致”来设计的。
常见的实现思路是,在每次 RTI 迭代中,用当前状态 x0 和上一步的控制初值 u_prev 来构造一个新的 QP:
min_{delta_x, delta_u} sum_k cost_k + gradient_k^T delta_k + 0.5 delta_k^T Hessian_k delta_k s.t. dynamics_linearized at (x_k, u_k), constraints_linearized
这个 QP 的求解结果 delta 会被用来更新当前的控制量。acados 在这里做了一个很关键的设计:它允许用户自由选择底层 QP 求解器后端,包括内点法类型的求解器和活跃集法类型的求解器。
3.2 QP 求解器内部的“热启动”机制
活跃集法的 QP 求解器有一个天然优势:它记住上一次迭代中哪些不等式约束是紧的(激活的),哪些是松的。下一次求解时,它从这个活跃集出发去迭代,而不是从零开始重新判定。这一步在实时控制中价值巨大,因为相邻两个采样时刻的问题变化很小,活跃集大概率只变化几个约束,求解器几乎不会触发冷启动路径。
acados 封装了这些 QP 求解器,把它们的 C 接口统一成一套内部 API。这样无论是用哪种求解器,上层 SQP 框架完全不需要改动。调用 QP 求解器的内部流程大致是:先把线性化的动力学约束组装成稀疏矩阵,再把它传给求解器。为了减少内存拷贝,acados 还维护了一个工作空间(workspace),所有中间矩阵都预先分配好,不在运行时 malloc。这一点对嵌入式系统异常重要——频繁的动态内存分配是实时系统的大忌,因为它带来的延迟不可预测。
3.3 关键源码逻辑与数据流
acados 的核心数据流可以用如下伪代码来表示。这里我省略了具体函数名,只保留机制骨架:
/* RTI 主循环:每个控制周期调用一次 */ void rti_step(acados_solver *solver, double *x0, double *u_prev, double *u_out) { /* 1. 建立当前状态下的动力学线性化 */ eval_dynamics(solver->model, solver->x_curr, solver->u_curr, &solver->jac_x, &solver->jac_u, solver->workspace); /* 2. 组装 QP 子问题 —— 在既有 workspace 上直接填稀疏矩阵 */ assemble_qp(solver->qp, solver->jac_x, solver->jac_u, solver->cost_grad, solver->cost_hess); /* 3. 调用底层的 QP 求解器,传入上一次的活跃集 */ solve_qp(solver->qp, solver->qp_solver); /* 4. 取出解 delta_u,更新控制量 */ *u_out = u_prev + solver->qp->solution.delta_u; }这里的逻辑说明:第一步的线性化并不是把整个模型重新推导一遍,而是用 acados 内部生成的磁带(tape)来计算雅可比。磁带机制可以理解为——把非线性函数 f(x,u) 的计算过程录下来,然后反着走一遍得到导数,就是自动微分。acados 支持正向和反向自动微分,对超过一个的状态变量,反向模式效率更高。第二步组装 QP 时,所有矩阵都已经存在于预分配的 workspace 中,没有动态内存申请。第三步里的活跃集由上一次求解结果继承下来,因此大部分情况下 QP 只做几次迭代就收敛。
4. 底层细节:算法库与嵌入式的博弈
4.1 数值优化层的分工
acados 并没有把全部事都自己做。它在算法层将决策分成两层:一层是外层 NMPC 策略,即 RTI 或 SQP;另一层是底层 QP 求解。这样做有一个工程上的好处:可以针对不同的芯片架构或约束侧重选择不同的 QP 求解后端。
常见的 QP 求解器实现风格有两种。第一种是活跃集法,它擅长处理小到中规模问题,并且得益于热启动机制,在 NMPC 这种序列化问题中表现非常出色。第二种是内点法,它的迭代次数一般与问题维度关系不那么敏感,适合大规模的、约束分布比较复杂的问题,但热启动相对困难。acados 的做法是让用户通过配置项自由切换,并在构建阶段把对应的后端静态链接进去。这意味着最终固件里只包含用到的求解器,不会携带冗余代码。
4.2 内存设计的嵌入式针对性
嵌入式求解器与桌面求解器最大的不同在于内存访问模式。桌面环境下我们可以依赖缓存和操作系统内存管理,但嵌入式控制芯片往往只有几百 KB 的 RAM,而且在硬实时要求下,最坏情况执行时间(WCET)是一个必须保证的参数。acados 为此做到了两点:一是在初始化阶段一次性分配完所有工作空间,求解过程完全栈上作业;二是把大矩阵布局成列主序的连续存储,方便底层线性代数库做向量化优化。
我一般会这么理解:acados 更像一个精简内核的实时控制系统,而不是一个通用数学工具。它把很多在设计阶段就能确定的信息(时域长度 N、状态维数、控制维数、约束个数)提前固定下来,生成专门针对该问题的代码。这有点类似用元编程方式展开一个固定大小的矩阵运算,而不是在运行时去判断矩阵维度。
4.3 conda 环境下的使用陷阱
在实际使用中,acados 通常被包装成 Python 接口供快速原型验证,但在嵌入式部署时要使用 C 接口。很多开发者在 PC 上跑通 Python 版本后,直接尝试把它交叉编译到 ARM 板子上,往往会栽跟孔。原因是 Python 接口默认链接了系统高性能的 BLAS 库,而交叉编译环境下没有对应库,于是 acados 会退化到使用内置的参考实现,矩阵运算慢好几倍。做好这一点的关键在于构建系统里明确指定 BLAS 后端和禁用 Python 模块的链接。
4.4 一个具体的构建配置方案
在 CMake 配置里,常见的做法是:
cmake -DACADOS_WITH_QPOASES=ON -DACADOS_WITH_HPIPM=OFF -DBLAS_FOUND=ON如果你是想评估不同 QP 求解后端对实时性能的影响,可以分别用 HK 距离测一次计算时间。经验数据通常表明,在小规模问题上,活跃集法比内点法少用约 30% 到 50% 的时间。但内点法对初始点不敏感,在某些病态问题上更鲁棒。这个参数选择要根据你的实际控制周期去试,不要盲从。
5. 从 Python 原型到 C 部署:acados 的避坑指南
5.1 现象:Python 仿真一切正常,部署到板子上之后,每个周期都超时
原因:默认配置下 Python 接口使用了完整的高精度求解和高阶集成器,而板子上交叉编译时,线性代数后端回退到了性能较差的参考实现。两者计算耗时差距能达到一个数量级。
解决:明确设置 BLAS 库路径,并在目标板上做性能基准测试。不要用 PC 的仿真耗时来估算目标板上的耗时,应该在硬件上跑一个最小化的 NMPC 例子,测量最坏情况执行时间。
5.2 现象:热启动明明开了,但求解器每次迭代都是从零开始
原因:QP 求解器的活跃集没有被保持。常见原因是你在每次控制周期重置了求解器内部状态,或者分配了一个新的求解器实例。活跃集信息存在于求解器对象内部,如果每次重新初始化,等于丢失热启动信息。
解决:确保求解器实例在整个控制循环中只创建一次,每次求解之后调用对应的重启函数(reset)而不是重新构造实例。检查调用链里是否有不小心调用了“cold_start”接口。
5.3 现象:同样的优化问题,换成更长的预测时域后,内存直接超了
原因:acados 的内存开销与状态维数、控制维数以及预测时域的乘积近似成正比。你在 Python 原型里用的时域是 20,板子上想跑到 40,RAM 占用翻了不止一倍。因为导数矩阵、QP 矩阵都要按这个维度预先分配。
解决:用 acados 提供的内存计算接口先估算需要多少字节,再决定时域长度。或者改用非均匀时间步长,让前段步长更细,后段步长更粗,这样可以显著降低内存压力而不损失控制品质。
5.4 现象:求解器显示“迭代次数达到上限”,但控制效果还算稳定
原因:RTI 本身允许单次迭代不收敛,只要它处于吸引域内。如果你强行要求严格收敛,不仅增加计算负担,还可能因为过度迭代把解带到远离当前工作点的位置,反而破坏闭环。
解决:把求解器的最大迭代次数设为 1 或 2,尝试是否能维持闭环稳定。这是 RTI 的标准做法,不是求解器出错。你可以在仿真里对比迭代 1 次和迭代 5 次的效果,通常性能差距很小。
5.5 现象:动力学模型里包含查表、或者不光滑的函数,acados 的自动微分经常报错
原因:自动微分要求函数是光滑可微的,至少局部 Lipschitz。查表法本质上是分段常数或分段线性插值,其导数要么无法定义,要么是零或跳跃的。这种情况会让 QP 子问题产生错误的梯度信息。
解决:对查表项做光滑化处理,比如用多层感知器或多项式逼近替代;或者把查表输出当作外部参数,求导时视为常数。后者会损失一部分模型精度,但能保证求解器稳定。
6. 设计取舍:把“一次迭代”当作第一公民,acados 为什么敢这么赌
acados 最核心的设计取舍,是牺牲“单步最优性”来换取“闭环实时性”。传统 NMPC 的每个控制周期都在追求当前问题的精确最优解,认为只有收敛到最优才能保证稳定性。而 acados 的 RTI 方案直接把它设计成:每个周期只做一次 SQP 迭代。这背后的判断是,对于采样间隔足够短的控制系统,连续两个采样时刻之间系统状态变化不大,因而最优解的变化也不大。你不需要每一步都追求最优,只需要保证状态变量能一步步逼近最优轨线即可。
这个思路在理论上对应了“扰动鞍点稳定性”的证明框架。我一般会这么理解:实时迭代等于把优化算法本身当成动态系统的一部分,在闭环中同时演化。这样做的好处太大了——代码路径极其固定,没有复杂的收敛判据,没有反复的矩阵分解,一切都是可预测的。这正是一个嵌入式实时系统最需要的性质。
另一个值得深思的取舍是外部 QP 求解器与内部 SQP 层的耦合程度。acados 没有自己发明一个“终极求解器”,而是把 QP 求解问题抽象成接口,允许不同后端接入。这意味着你可以在开发阶段用活跃集法获得最佳性能,在遇到病态问题时切换到内点法,甚至把你的公司自研 QP 求解器封装进来。这种开放式的设计,实际上是承认了数值优化领域没有“万能药”,但把选择权留给工程现场,是很聪明的做法。
从我自己的使用习惯来看,从那以后我每接触一个新的 NMPC 应用场景,都强制先画一张“采样周期 vs. 单次 QP求解耗时”的图,确认 RTI 一次迭代的耗时占控制周期的比例小于 20%,再考虑部署。如果大于这个数,我会先考虑缩短预测时域或者简化动力学模型,而不是继续调惩罚权重。这比一开始就追求复杂的求解配置要省心得多。希望帮到你。
本文还有配套的精品资源,点击获取