简介: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 计算与 ERK 积分器实现,便于理解算法结构与测试流程。目前已有 1979 人学习下载,适合需要将非线性 MPC 落地到嵌入式平台、研究实时优化求解的读者参考。
1. acados 到底解决什么问题:从「模型预测控制跑不动」说起
如果你做过非线性模型预测控制(NMPC)的落地,大概率遇到过这个场景:算法在仿真里跑得挺好,一上真实控制器,单步求解时间从 2ms 飙到 40ms,控制周期直接被撑爆。问题往往不在控制律本身,而在求解器——通用非线性规划求解器为了兼容各种问题形式,牺牲了太多结构信息。acados 就是冲着这个痛点来的:它是一个面向非线性最优控制(OCP)的快速、嵌入式求解器,核心思路是把 OCP 的结构(多阶段、稀疏、固定维度)吃透,用实时迭代(RTI)和序列二次规划(SQP)把每步计算压到微秒到毫秒级。
它适合谁?做机器人运动控制、自动驾驶轨迹跟踪、无人机姿态控制、化工过程控制的工程师,尤其是那些需要把 NMPC 部署到嵌入式平台(如 ARM Cortex-A、x86 工控机)的团队。acados 不是 MATLAB 工具箱那种「先仿真再说」的东西,它从设计之初就考虑代码生成和内存确定性,能生成 C 代码并交叉编译到目标板。换句话说,它解决的是「NMPC 从论文到产品」中间那段最脏最累的工程化问题。
2. acados 的求解器选型:SQP-RTI 与 SQP 全步长怎么选
2.1 实时迭代 RTI 的适用边界
acados 默认推荐的是 SQP-RTI(Real-Time Iteration)。它的逻辑是:在每个采样周期只做一次 SQP 迭代,而不是迭代到收敛。这听起来很激进,但控制问题有个特点——上一时刻的解已经离新解很近,一次迭代足够把残差压到可接受范围。RTI 把每个周期的计算量固定下来,最坏情况执行时间(WCET)可预测,这对硬实时系统是刚需。
但 RTI 不是万能的。如果系统动态变化剧烈、参考轨迹跳变、或者初始猜测离解很远,单次迭代可能发散。这时候需要看两个指标:一是 SQP 迭代后的 KKT 残差,二是控制输入的变化率。如果残差在 1e-3 以上且控制量抖动明显,说明 RTI 撑不住,得换全步长 SQP 或者增加迭代次数。
2.2 全步长 SQP 与 RTI 的配置差异
在 acados 的 Python 接口里,切换求解器类型只需要改一个字段。下面是一个最小示例,展示如何为一个倒立摆模型配置 RTI 和全步长 SQP:
from acados_template import AcadosOcp, AcadosOcpSolver import numpy as np # 创建 OCP 对象 ocp = AcadosOcp() ocp.model.name = 'pendulum' # 设置求解器选项 ocp.solver_options.nlp_solver_type = 'SQP_RTI' # 或 'SQP' ocp.solver_options.qp_solver = 'PARTIAL_CONDENSING_HPIPM' ocp.solver_options.hessian_approx = 'GAUSS_NEWTON' ocp.solver_options.integrator_type = 'ERK' ocp.solver_options.sim_method_num_stages = 4 ocp.solver_options.sim_method_num_steps = 1 # RTI 专用参数:每周期迭代次数固定为 1 if ocp.solver_options.nlp_solver_type == 'SQP_RTI': ocp.solver_options.nlp_solver_max_iter = 1 else: ocp.solver_options.nlp_solver_max_iter = 20 ocp.solver_options.nlp_solver_tol_stat = 1e-6 # 创建求解器 solver = AcadosOcpSolver(ocp)这段代码的关键参数有三个:nlp_solver_type决定用 RTI 还是全步长 SQP;qp_solver选 HPIPM 是因为它针对 OCP 的带状稀疏结构做了优化,比通用 QP 求解器快一个量级;hessian_approx用 GAUSS_NEWTON 而不是精确 Hessian,省掉二阶导数计算,代价是收敛速度略慢,但对 RTI 来说够用。
逻辑说明:RTI 模式下nlp_solver_max_iter必须设为 1,否则就退化成普通 SQP 了。全步长 SQP 则需要设置收敛容差tol_stat,通常 1e-6 到 1e-4 之间,太松会导致控制精度不够,太紧会拖长计算时间。sim_method_num_stages和sim_method_num_steps控制积分器精度,ERK 四阶一步在多数机械系统上够用,如果是刚性系统(比如含化学反应的过程控制),得换 IRK 并增加步数。
2.3 代码生成与嵌入式部署的配置要点
acados 的真正杀伤力在代码生成。它能把上面定义的 OCP 编译成一个独立的 C 库,不依赖 Python 运行时。生成命令如下:
# 在 Python 脚本中调用生成接口 ocp.code_export_directory = 'c_generated_code' AcadosOcpSolver(ocp, generate=True, build=True) # 生成的目录结构: # c_generated_code/ # acados_solver_pendulum.c/h # Makefile # CMakeLists.txt生成后的 C 代码可以直接链入嵌入式工程。交叉编译时注意两点:一是关闭 BLASFEO 的运行时 CPU 检测(嵌入式平台通常不需要),在CMakeLists.txt里设BLASFEO_LA=HIGH_PERFORMANCE;二是如果目标板没有硬件浮点单元,得把ACADOS_WITH_OPENMP关掉,否则线程调度会引入不可预测的延迟。
3. 用 acados 搭建一个 NMPC 控制器的完整流程
3.1 定义模型:从符号表达式到 C 代码
acados 支持两种模型定义方式:一是用 CasADi 符号变量写微分方程,二是直接写 C 函数。前者适合快速原型,后者适合最终部署。下面用 CasADi 定义一个简单的二维运动模型:
import casadi as ca # 状态:位置 p_x, p_y;速度 v_x, v_y x = ca.SX.sym('x', 4) # 控制:加速度 a_x, a_y u = ca.SX.sym('u', 2) # 连续时间动态 p = x[0:2] v = x[2:4] xdot = ca.vertcat(v, u) # 创建 CasADi 函数 f_expl = ca.Function('f_expl', [x, u], [xdot])这段代码定义了xdot = f(x,u),acados 会用积分器把它离散化。注意状态和控制的维度必须固定,acados 不支持变维度 OCP。如果模型里有参数(比如质量、摩擦系数),用ca.SX.sym('p', n_p)单独定义,通过ocp.model.p传入,这样可以在线更新而不重新生成代码。
3.2 代价函数与约束的配置模板
代价函数决定了控制器的行为。acados 支持线性最小二乘和非线性最小二乘两种形式。对于轨迹跟踪问题,通常用非线性最小二乘:
# 参考轨迹 x_ref = ca.SX.sym('x_ref', 4) y_ref = ca.SX.sym('y_ref', 4) # 阶段代价:跟踪误差的加权平方 W = ca.diag([10.0, 10.0, 1.0, 1.0]) cost_y = x - y_ref ocp.cost.cost_type = 'NONLINEAR_LS' ocp.cost.W = W ocp.cost.yref = np.zeros(4) ocp.model.cost_y_expr = cost_y # 终端代价 ocp.cost.cost_type_e = 'NONLINEAR_LS' ocp.cost.W_e = W * 10 ocp.cost.yref_e = np.zeros(4) ocp.model.cost_y_expr_e = cost_y # 约束:控制量限幅 ocp.constraints.lbu = np.array([-5.0, -5.0]) ocp.constraints.ubu = np.array([5.0, 5.0]) ocp.constraints.idxbu = np.array([0, 1])参数说明:W是状态跟踪权重,位置权重给 10、速度权重给 1,是因为位置误差直接影响任务完成度,速度误差可以容忍。终端代价W_e放大 10 倍是为了保证终端稳定性,这是 NMPC 的常见做法。lbu和ubu是控制量上下界,idxbu指定哪些控制量受约束。如果要对状态加约束(比如避障),用ocp.constraints.lbx和ubx,但注意状态约束可能让 QP 不可行,需要加松弛变量。
3.3 仿真闭环与求解时间统计
搭好 OCP 后,跑一个闭环仿真验证性能:
import time N_sim = 100 x_current = np.array([0.0, 0.0, 0.0, 0.0]) solver = AcadosOcpSolver(ocp) solve_times = [] for i in range(N_sim): # 更新参考轨迹 yref = np.array([1.0, 1.0, 0.0, 0.0]) solver.set(0, 'yref', yref) # 求解 t0 = time.perf_counter() solver.solve() solve_times.append(time.perf_counter() - t0) # 获取控制量并模拟一步 u_opt = solver.get(0, 'u') x_current = x_current + 0.01 * np.array([x_current[2], x_current[3], u_opt[0], u_opt[1]]) print(f'平均求解时间: {np.mean(solve_times)*1000:.3f} ms') print(f'最大求解时间: {np.max(solve_times)*1000:.3f} ms')这段代码的关键是solver.set(0, 'yref', yref),它在线更新参考轨迹而不重新生成代码。solve_times统计的是 Python 调用开销加求解时间,实际嵌入式部署时只有 C 求解时间,通常比这个数字小 30% 到 50%。如果最大求解时间远大于平均值,说明某些时刻 QP 迭代次数飙升,需要检查约束是否太紧或者初始猜测质量太差。
4. acados 避坑记录:从编译失败到控制抖动的五个典型问题
4.1 编译时报 BLASFEO 链接错误
现象:cmake配置阶段报undefined reference to blasfeo_dgemm_nn。原因通常是 BLASFEO 没有正确编译,或者目标架构不匹配。acados 默认用X64_AUTOMATIC,但在 ARM 上需要手动指定ARMV8A_ARM_CORTEX_A57之类的目标。解决方法是清理build目录,重新运行cmake -DBLASFEO_TARGET=ARMV8A_ARM_CORTEX_A57 ..,然后make install。如果还不行,检查LD_LIBRARY_PATH是否包含 BLASFEO 的lib目录。
4.2 RTI 模式下控制量高频抖动
现象:控制输入在稳态附近以 10Hz 以上频率抖动。原因通常是 RTI 单次迭代没有收敛到足够精度,残差在 QP 边界附近振荡。解决方法是先切到全步长 SQP 确认问题是否消失,如果消失,说明 RTI 的精度不够。可以尝试增加sim_method_num_steps提高积分精度,或者在代价函数里加控制量变化率惩罚项W_u,抑制高频抖动。
4.3 状态约束导致 QP 不可行
现象:求解器返回QP_INFEASIBLE,控制量卡在边界。原因是状态约束和动态方程冲突,比如要求位置在 0.1 秒内从 0 跳到 1,但速度限幅不允许。解决方法是给状态约束加松弛变量,在 acados 里设置ocp.constraints.idxsbx和ocp.cost.Zl、ocp.cost.Zu,允许约束被轻微违反,代价是牺牲一点安全性。松弛变量的权重需要调,太小起不到作用,太大等于没加。
4.4 代码生成后嵌入式平台运行超时
现象:在 x86 上跑 1ms 的求解器,交叉编译到 ARM 后变成 8ms。原因可能是 BLASFEO 没有针对目标 CPU 优化,或者编译器优化等级不够。解决方法是检查CMakeLists.txt里的CMAKE_C_FLAGS是否包含-O3 -march=native(交叉编译时-march要换成目标架构),并确认 BLASFEO 的TARGET设置正确。另外,关闭ACADOS_WITH_OPENMP可以避免线程调度开销,单线程反而更快。
4.5 参考轨迹更新后求解器不响应
现象:调用solver.set('yref', ...)后控制量没变化。原因是 acados 的set接口对字段名敏感,yref和yref_e要分开设置,且索引必须对应阶段。如果 OCP 有 N 个阶段,yref要设 N 次,yref_e设 1 次。另外,如果用了NONLINEAR_LS,参考轨迹是通过cost_y_expr里的符号变量传入的,set的字段名应该是yref而不是cost_y_expr。检查solver.get_stats('residuals')可以看到残差是否真的更新了。
5. 进阶技巧:用 acados 做参数自适应与多速率控制
5.1 在线更新模型参数
acados 支持在运行时更新模型参数,不需要重新生成代码。假设模型里有个质量参数m,定义时用ca.SX.sym('p', 1),然后在闭环里调用solver.set(0, 'p', np.array([m_new]))。这个接口的延迟在微秒级,适合做自适应控制。但注意参数更新后,之前的最优解可能不再是最佳初始猜测,如果参数变化超过 20%,建议重置求解器状态或者多跑几次 SQP 迭代。
5.2 多速率 NMPC 的实现方式
有些系统需要控制周期和估计周期不同,比如视觉伺服里相机 30Hz、控制 100Hz。acados 本身不直接支持多速率,但可以通过两个求解器实例实现:一个高频短时域求解器负责稳定,一个低频长时域求解器负责规划。两个求解器共享状态估计,高频求解器的终端代价用低频求解器的解来构造。这种架构在 acados 的示例里有类似实现,关键是保证两个求解器的采样时间比是整数倍,否则终端代价对不齐。
5.3 验证求解器性能的三个指标
部署前建议跑三组测试:一是阶跃响应测试,看超调量和调节时间;二是正弦跟踪测试,看相位滞后和幅值衰减;三是扰动抑制测试,在稳态时加脉冲扰动,看恢复时间。每组测试记录平均求解时间、最大求解时间和 QP 迭代次数。如果最大求解时间超过控制周期的 80%,说明没有余量,需要简化模型或者降低时域长度。我一般会留 30% 的余量,因为嵌入式平台的负载不是恒定的,中断和 DMA 传输会抢 CPU。
5.4 一个容易忽略的细节:初始猜测的质量
acados 默认用上一时刻的解作为初始猜测,这在大多数情况下够用。但如果系统有模式切换(比如从位置控制切到力控制),上一时刻的解可能离新解很远,导致 RTI 发散。这时候可以手动设置初始猜测:solver.set(0, 'x', x_guess)和solver.set(0, 'u', u_guess)。更好的做法是维护一个解的热启动缓冲区,在模式切换时用插值或者上一轮同模式的解来初始化。这个技巧在机械臂抓取任务里特别有用,能减少 50% 以上的求解时间波动。
希望帮到你。
本文还有配套的精品资源,点击获取