☰
acados 实时 MPC 求解器:从建模、代码生成到嵌入式部署与调优
2026/10/15 12:01:20 网站建设 项目流程

简介: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_file='acados_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_type:NONLINEAR_LS表示非线性最小二乘代价,适合大多数跟踪问题;如果代价是通用非线性函数,用EXTERNAL。
  • qp_solver:PARTIAL_CONDENSING_HPIPM是 acados 里最常用的 QP 求解器,利用阶段稀疏性做部分凝聚,内存和速度平衡较好。
  • hessian_approx:GAUSS_NEWTON忽略二阶项,速度快但精度略低;如果问题非线性强、收敛差,换EXACT。
  • nlp_solver_type:SQP_RTI是实时迭代,每次只做一次 SQP 迭代,适合 MPC 在线滚动;离线优化用SQP全步。
  • integrator_type:ERK显式龙格库塔,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_file='acados_ocp.json', generate=True, build=True)

生成的代码包含求解器、积分器和 QP 求解器,依赖 BLASFEO 做线性代数加速。嵌入式上通常关掉不必要的打印和检查,用-O3编译。我一般会在 x86 上先跑通,再交叉编译到 ARM,最后在目标板上测单次求解时间。注意:生成代码里的内存是静态分配的,阶段数和状态维度在编译期就固定了,所以改问题规模要重新生成。

3. 参数调优与实时性验证:把求解时间压到周期以内

3.1 影响求解时间的五个关键参数

调参不是玄学,acados 的求解时间对几个参数非常敏感。下面这张表是我在实际项目里反复验证过的优先级:

参数作用调大后果调小后果建议
N_horizon预测步数求解时间线性增长预见性不足从 20 起调,嵌入式可降到 10
sim_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_approx,GAUSS_NEWTON比EXACT更稳定,条件数更好。

4.2 现象:生成代码在目标板上跑,结果和 x86 不一致

原因:浮点精度差异、编译器优化等级不同、BLASFEO 的目标架构配置不对。嵌入式 ARM 上如果没开对 NEON 指令,线性代数会走标量路径,速度和精度都受影响。

解决:确认 BLASFEO 编译时选了正确的目标架构,编译选项里开-march=native或对应的 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 自定义 Hessian:GAUSS_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_file='acados_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 不收敛”,其实是问题本身在离线就不可行。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询