资讯动态

acados 实时 MPC 求解器:从建模、代码生成到嵌入式部署与调优

发布时间:2026/10/15 12:01:27 来源:尧图企业网站定制
简介acados 是一套面向非线性最优控制的开源快速嵌入式求解器主要服务于从事嵌入式控制优化、模型预测控制与机器人/车辆动力学研究的工程师和科研人员。它提供 C、Python、MATLAB 与 Octave 多语言接口便于在仿真与嵌入式平台间迁移算法。资源包共 1227 个文件约 21.72MB以 371 个 C 源文件、296 个 m 脚本、122 个头文件、88 个 Python 脚本为主辅以 Makefile、CMake、sh 脚本及 md 文档覆盖求解器核心、代码生成模板与构建配置。内容预览可见 vde_hess_chain、casadi_erk4_chain 等链式系统示例涉及 Hessian 计算与显式 Runge-Kutta 积分器便于理解算法落地方式。已有 1979 人学习下载适合需要快速搭建非线性最优控制原型、研究嵌入式实时求解与代码生成的读者参考。1. 从一次 MPC 求解超时说起acados 到底解决什么问题如果你做过非线性模型预测控制NMPC的工程落地大概率遇到过这个场景算法在仿真里跑得漂漂亮亮一上嵌入式控制器单次求解时间从几毫秒飙到几十毫秒控制周期直接被拖垮。问题往往不在控制律设计而在求解器——通用非线性规划求解器为了兼容各种问题结构牺牲了实时性。acados 就是冲着这个痛点来的它是一个面向非线性最优控制OCP的快速求解器框架核心卖点是在嵌入式硬件上做到毫秒级甚至微秒级的求解速度同时保留对非线性动力学和约束的完整支持。它适合谁做 MPC、轨迹优化、机器人控制、自动驾驶横纵向控制、无人机姿态控制的工程师尤其是那些已经用 CasADi 或类似工具建模、但发现求解速度不够、需要往嵌入式平台迁移的团队。acados 不是又一个通用 NLP 求解器它把最优控制问题的结构多阶段、稀疏、实时迭代吃透了用实时迭代RTI和 SQP 变体把每步计算量压到最低。下面我从建模、代码生成、参数调优到踩坑把这条链路拆开讲。2. acados 的求解器选型与建模链路为什么不是随便挑一个2.1 三类求解器对应三种问题结构acados 内部不是单一算法而是一组求解器选错了等于白干。常见的有三类OCP 求解器SQP-RTI / SQP 全步面向多阶段最优控制问题利用阶段间稀疏性。RTI 版本只做一次 SQP 迭代适合实时 MPC每次求解时间可预测。NLP 求解器如基于 SQP 的通用接口当问题不是标准多阶段结构时用灵活性高但速度不如 OCP 专用路径。积分器ERK / IRK负责动力学离散化显式 RK 适合非刚性系统隐式 RK 适合刚性系统。选型逻辑很直接如果你的问题是标准 MPC 形式——多阶段、每阶段有状态和控制的约束、目标函数可分离——那就走 OCP RTI。如果动力学刚性很强比如含快速化学反应或刚性机械耦合积分器换隐式。我一般会先用显式 RK 试求解时间不稳定或步长被压得很小时再换隐式。2.2 用 CasADi 描述 OCP最小可跑示例acados 的建模入口通常通过 CasADi 或其自带的 Python 接口。下面是一个倒立摆式简化系统的 OCP 描述目标是让状态回到原点控制量有界。import casadi as ca import numpy as np from acados_template import AcadosOcp, AcadosOcpSolver, AcadosModel def create_model(): # 状态: [位置, 速度, 角度, 角速度] x ca.SX.sym(x, 4) # 控制: [力] u ca.SX.sym(u, 1) # 动力学: 简化线性倒立摆 非线性耦合项 xdot ca.SX.sym(xdot, 4) xdot[0] x[1] xdot[1] 0.5 * x[0] 0.1 * ca.sin(x[2]) u[0] xdot[2] x[3] xdot[3] 0.3 * x[2] - 0.2 * x[3] 0.05 * u[0] # 显式表达式形式acados 需要 f_expl f_expl ca.Function(f_expl, [x, u], [xdot]) model AcadosModel() model.f_expl_expr f_expl(x, u) model.x x model.u u model.name sim_pendulum return model def create_ocp(): ocp AcadosOcp() ocp.model create_model() # 预测步长与阶段数 N 20 Tf 1.0 ocp.solver_options.N_horizon N ocp.solver_options.tf Tf # 代价: 状态二次 控制二次 ocp.cost.cost_type NONLINEAR_LS ocp.cost.W np.diag([10.0, 1.0, 10.0, 1.0, 0.1]) ocp.cost.yref np.zeros(5) ocp.model.cost_y_expr ca.vertcat(ocp.model.x, ocp.model.u) # 约束: 控制量限幅 ocp.constraints.lbu np.array([-2.0]) ocp.constraints.ubu np.array([2.0]) ocp.constraints.idxbu np.array([0]) # 初始状态约束 ocp.constraints.x0 np.zeros(4) # 求解器选项 ocp.solver_options.qp_solver PARTIAL_CONDENSING_HPIPM ocp.solver_options.hessian_approx GAUSS_NEWTON ocp.solver_options.integrator_type ERK ocp.solver_options.nlp_solver_type SQP_RTI ocp.solver_options.sim_method_num_stages 4 ocp.solver_options.sim_method_num_steps 1 return ocp ocp create_ocp() solver AcadosOcpSolver(ocp, json_fileacados_ocp.json) # 设置初始状态并求解 x0 np.array([1.0, 0.0, 0.2, 0.0]) solver.set(0, x, x0) status solver.solve() print(status:, status) print(u0:, solver.get(0, u))这段代码的逻辑先定义符号状态和控制写出显式动力学表达式然后配置 OCP 的代价类型、约束和求解器选项。关键参数说明N_horizon预测步数直接决定问题规模和求解时间。嵌入式上通常 10~30太大就得上移动阻塞或降低步数。cost_typeNONLINEAR_LS表示非线性最小二乘代价适合大多数跟踪问题如果代价是通用非线性函数用EXTERNAL。qp_solverPARTIAL_CONDENSING_HPIPM是 acados 里最常用的 QP 求解器利用阶段稀疏性做部分凝聚内存和速度平衡较好。hessian_approxGAUSS_NEWTON忽略二阶项速度快但精度略低如果问题非线性强、收敛差换EXACT。nlp_solver_typeSQP_RTI是实时迭代每次只做一次 SQP 迭代适合 MPC 在线滚动离线优化用SQP全步。integrator_typeERK显式龙格库塔sim_method_num_stages和sim_method_num_steps控制精度和计算量。2.3 代码生成与嵌入式部署路径acados 的杀手锏是 C 代码生成。Python 侧配置好 OCP 后可以生成独立的 C 库交叉编译到目标板。常见做法是# 生成 C 代码在 Python 脚本中调用 ocp.code_export_directory c_generated_code AcadosOcpSolver(ocp, json_fileacados_ocp.json, generateTrue, buildTrue)生成的代码包含求解器、积分器和 QP 求解器依赖 BLASFEO 做线性代数加速。嵌入式上通常关掉不必要的打印和检查用-O3编译。我一般会在 x86 上先跑通再交叉编译到 ARM最后在目标板上测单次求解时间。注意生成代码里的内存是静态分配的阶段数和状态维度在编译期就固定了所以改问题规模要重新生成。3. 参数调优与实时性验证把求解时间压到周期以内3.1 影响求解时间的五个关键参数调参不是玄学acados 的求解时间对几个参数非常敏感。下面这张表是我在实际项目里反复验证过的优先级参数作用调大后果调小后果建议N_horizon预测步数求解时间线性增长预见性不足从 20 起调嵌入式可降到 10sim_method_num_stages积分器级数精度高但计算量大精度差可能不稳定显式 RK 用 4 级起步sim_method_num_steps每阶段积分步数精度高时间增加离散误差大通常 1刚性系统加qp_solver_cond_N部分凝聚的凝聚步数内存换速度速度换内存设为 N 的一半左右nlp_solver_max_iter最大迭代数可能超时可能不收敛RTI 下固定 1全步设 20~50其中qp_solver_cond_N容易被忽略。部分凝聚把多个阶段的 QP 合并成一个大 QP凝聚步数越少QP 越小但稀疏结构越复杂。我一般先设成N//2然后根据实测微调。3.2 用 RTI 做在线 MPC 的时序验证RTI 的核心思想是每次采样只做一次 SQP 迭代用上一次的解做线性化点。这样单次求解时间可预测但代价是收敛精度不如全步。验证时序时我会写一个循环模拟真实控制周期import time N_sim 1000 times [] x_current np.array([1.0, 0.0, 0.2, 0.0]) for k in range(N_sim): solver.set(0, x, x_current) t0 time.perf_counter() status solver.solve() t1 time.perf_counter() times.append((t1 - t0) * 1000) # 毫秒 # 取第一个控制量用真实系统推进一步这里用模型代替 u0 solver.get(0, u) x_current x_current 0.01 * np.array([ x_current[1], 0.5 * x_current[0] 0.1 * np.sin(x_current[2]) u0[0], x_current[3], 0.3 * x_current[2] - 0.2 * x_current[3] 0.05 * u0[0] ]) print(mean: %.3f ms, max: %.3f ms, p99: %.3f ms % ( np.mean(times), np.max(times), np.percentile(times, 99)))这段代码测的是单次求解的墙钟时间。关键看三个指标均值、最大值、99 分位。嵌入式上最怕的是最大值超周期所以我会把max和p99作为硬指标。如果最大值超标优先降N_horizon或换更轻的 QP 求解器。注意Python 侧的计时包含接口开销真实嵌入式上要用 C 侧计时排除 Python 调用层。3.3 收敛失败时看什么RTI 不收敛或返回非零状态时别急着改参数先看求解器日志。acados 会输出每次迭代的残差和 QP 状态。常见原因线性化点太远初始状态跳变太大、约束冲突控制量上下界和状态约束打架、积分器步长太大导致动力学失真。我一般会先把nlp_solver_type临时改成SQP全步看能不能收敛如果能说明 RTI 的单次迭代不够需要增加迭代次数或改善初始猜测。4. 避坑与常见问题那些让我熬夜的坑4.1 现象求解时间忽大忽小最大值超标原因QP 求解器的迭代次数不固定某些阶段问题条件数差导致迭代次数飙升。RTI 虽然只做一次 SQP 迭代但内层 QP 仍可能多轮迭代。解决给 QP 求解器设最大迭代次数上限并开启热启动。acados 支持用上一次的解做 QP 热启动能显著降低迭代次数波动。另外检查hessian_approxGAUSS_NEWTON比EXACT更稳定条件数更好。4.2 现象生成代码在目标板上跑结果和 x86 不一致原因浮点精度差异、编译器优化等级不同、BLASFEO 的目标架构配置不对。嵌入式 ARM 上如果没开对 NEON 指令线性代数会走标量路径速度和精度都受影响。解决确认 BLASFEO 编译时选了正确的目标架构编译选项里开-marchnative或对应的 ARM 架构标志。数值差异如果在小数点后 6 位以内通常是正常的浮点误差如果控制量明显不同检查是否用了未初始化的内存。4.3 现象约束明明设了求解结果却越界原因acados 的约束是软约束还是硬约束取决于配置。默认情况下lbu/ubu是硬约束但如果 QP 求解器设置了松弛变量约束可能被放松。另外状态约束如果和动力学冲突求解器会优先满足动力学导致约束违反。解决检查ocp.constraints里是否意外启用了松弛。对于必须满足的约束确认没有加松弛惩罚。如果约束本身不可行先做可行性检查别指望求解器变魔术。4.4 现象改了模型表达式生成代码报错或结果全零原因CasADi 表达式里用了不支持的函数或者符号变量的维度对不上。acados 对动力学表达式的形式有要求必须是显式f_expl或隐式f_impl不能混用。解决先用 CasADi 的Function单独求值确认表达式在数值上正确再喂给 acados。维度错误通常报错信息很隐晦我一般会在create_model里打印每个符号的维度逐个核对。4.5 现象嵌入式上内存不够生成代码编译失败原因acados 静态分配所有阶段的内存N_horizon大、状态维度高时内存占用线性增长。QP 求解器的凝聚矩阵也会占不少内存。解决降低N_horizon或者用移动阻塞move blocking减少决策变量。另外可以关掉不需要的求解器组件比如不用隐式积分器就不编译。生成代码时检查.json里的内存配置必要时手动调小 QP 的工作空间。5. 进阶技巧用外部代价和自定义 Hessian 换性能5.1 外部代价函数把复杂代价从符号表达式里解放出来当代价函数包含查表、分段函数或复杂非线性时用 CasADi 符号表达式会生成巨大的计算图拖慢求解。acados 支持EXTERNAL代价类型允许你提供 C 函数计算代价和梯度。这样可以把代价逻辑写成高效的手写 C避免符号展开。// 外部代价函数的 C 实现示例 void cost_y_fun(double *y, double *p, double *cost_y) { // y 是代价表达式输入这里假设 y [x; u] // 计算残差供最小二乘使用 cost_y[0] y[0] - 0.0; // 位置误差 cost_y[1] y[1] - 0.0; // 速度误差 cost_y[2] y[2] - 0.0; // 角度误差 cost_y[3] y[3] - 0.0; // 角速度误差 cost_y[4] y[4]; // 控制量 }配置时把cost_type设为EXTERNAL并指定对应的 C 函数名。逻辑说明acados 会在每次迭代调用这个函数计算残差然后乘以权重矩阵W得到代价。参数说明y是输入向量p是外部参数cost_y是输出残差。注意 C 函数的签名必须和 acados 的接口一致生成代码时会做链接检查。5.2 自定义 HessianGAUSS_NEWTON 不够用时的手动方案GAUSS_NEWTON用一阶信息近似 Hessian速度快但收敛慢。如果问题非线性强、需要精确二阶信息可以用EXACT让 acados 自动微分但计算量大。折中方案是提供自定义 Hessian 的 C 函数只计算关键项。我一般会先测GAUSS_NEWTON的收敛情况如果迭代次数在 5 次以内能收敛就不折腾自定义 Hessian。如果超过 10 次还不收敛再考虑。自定义 Hessian 的调试成本很高容易因为符号错误导致求解器发散血泪经验是先用自动微分的EXACT跑通确认问题本身可解再逐步替换成手写 Hessian每步对比数值。5.3 验证方法用离线全步 SQP 做基准在线 RTI 的结果好不好需要一个基准。我的习惯是同一组初始状态和参考轨迹分别用SQP_RTI和SQP全步跑对比控制量和状态轨迹。如果 RTI 的控制量和全步差异在可接受范围内比如 5% 以内说明 RTI 的线性化点足够好。如果差异大要么增加 RTI 迭代次数要么改善初始猜测。# 基准对比全步 SQP vs RTI ocp.solver_options.nlp_solver_type SQP ocp.solver_options.nlp_solver_max_iter 50 solver_full AcadosOcpSolver(ocp, json_fileacados_ocp_full.json) # 同样的初始状态 solver_full.set(0, x, x0) solver_full.solve() u_full solver_full.get(0, u) # 对比 RTI 结果 print(full step u0:, u_full, RTI u0:, solver.get(0, u))这段代码的关键是保持 OCP 配置一致只改求解器类型。对比时看第一个控制量和前几个阶段的轨迹如果差异集中在后期阶段说明 RTI 的短视效应明显可以适当增加N_horizon或降低对远期精度的要求。最后说个习惯我每次改完参数或模型都会先跑一遍离线全步确认问题本身有解且收敛正常再切回 RTI 做在线测试。这个顺序能省掉大量排查时间——很多所谓的“RTI 不收敛”其实是问题本身在离线就不可行。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑