☰
基于MPC的无人艇自主航行仿真:避碰代价函数设计与实现
2026/10/22 18:56:41 网站建设 项目流程

简介:基于模型预测控制(MPC)的水面无人艇自主航行仿真源码与文档资源包,面向船舶控制、智能航行方向的研究者与学生,可复现无人艇沿期望航线循迹、安全避开静态与动态障碍的全过程仿真。包内共11个文件,以8个MATLAB脚本(.m)为核心,包含model.m(船舶水动力与干扰模型)、constraint.m(执行机构约束)、collision_avoidance.m(避碰策略)、computeCost.m(由航迹偏移量、航速变化量、碰撞危险度和规则符合度构成的评价函数)以及rk4.m(四阶龙格库塔积分);另有1份PDF研究文档、1个README.md说明文件和1张演示效果图,整体压缩包仅871KB,结构紧凑易读。已有126人学习下载。通过学习可掌握MPC在无人艇自主航行中的建模、约束设计与仿真验证思路,也可直接基于源码修改参数开展航线跟踪与避碰实验,适合作为课程设计、本科毕设或相关研究的起点。

1. 基于 MPC 的 USV 自主航行仿真:为什么避碰要写进代价函数而不是单独规划

做 USV 自主航行仿真,最难的往往不是让船“走起来”,而是循迹、避碰和执行机构限制同时存在时,还能输出一串不违反物理规律的控制量。基于 MPC 的框架天生适合这个场景:每个控制周期向前预测一段航迹,把航迹偏移、航速变化、碰撞危险度和规则符合度都折算成代价,再在线求解带约束的优化问题。这份资源把闭环链路的代码都摆出来了:model.m 做船舶运动建模,rk4.m 负责状态递推,computeCost.m 拼装代价函数,constraint.m 写执行机构约束,collision_avoidance.m 处理动静态障碍物,PDF 文档完整讲设计思路。适合正在做无人艇方向毕业设计、需要一套能跑出完整轨迹的仿真样例的开发者。下面从仿真链路开始拆。

2. 模型与仿真链路:从 model.m 到 rk4.m 的预测闭环怎么搭

2.1 船舶运动模型:为什么选响应型模型而不是全驱动六自由度

拆这个包时我第一件事是看 model.m 的输入输出结构。它是典型的三段式:传入当前状态、控制量和一个参数结构体,返回状态导数。这种写法适合 MPC,因为优化器每帧要反复调用模型,接口越干净越不容易出错,也方便你换成自己的船型参数。

我拆过的多个 USV 仿真里,教学向代码基本都用响应型船舶模型(Nomoto 模型加非线性阻尼),而不是六自由度全驱动模型。原因很实际:MPC 每帧要调用模型几十次,模型自由度越高,数值越敏感,求解时间越长。对自主航行仿真来说,回转响应、速度衰减、漂角这些静态特征用一阶 Nomoto 响应模型就能覆盖,风、浪、流作为外扰项叠加进去,已经足够支撑循迹和避碰验证。直接上六自由度模型,调参难度会成倍上涨且收益有限。

下面是我按常见做法还原的 model.m 骨架,状态顺序是 x、y、艏向角 psi、纵向速度 u、横向速度 v、回转角速度 r:

function dx = model(t, x, u, param) % 状态: x(1)=x_e, x(2)=y_e, x(3)=psi, x(4)=u, x(5)=v, x(6)=r % 控制: u(1)=舵角delta(rad), u(2)=螺旋桨转速n psi = x(3); r = x(6); du = x(4); dv = x(5); % 一阶非线性响应模型:回转响应 + 非线性阻尼 r_dot = (param.K * u(1) - r) / param.T ... + param.nonlinear * r * abs(r); % 非线性阻尼项 % 纵荡: 推力减阻力, 转速映射成推力系数 u_dot = (param.Ct * u(2) * abs(u(2)) - param.Ruu * du) / param.m11; % 横荡: 线性近似 v_dot = (param.Yv * dv + param.Yr * r) / param.m22; % 运动学: 北东坐标系下的位置变化 x_dot = du * cos(psi) - dv * sin(psi); y_dot = du * sin(psi) + dv * cos(psi); dx = [x_dot; y_dot; r; u_dot; v_dot; r_dot]; end

逻辑说明:model.m 的核心是把“控制指令变成加速度”的物理过程压缩成几个可辨识参数。T 是航向时间常数,越小舵效出来越快,相当于方向盘阻尼;K 是稳态回转增益,决定同样舵角下的稳态回转角速度;非线性阻尼项 r|r| 用来描述高速时回转能力下降,少了它预测的回转半径会偏小。

参数说明:状态顺序 x、y、psi、u、v、r 是这套代码的全局约定,computeCost、constraint、collision_avoidance 都按这个索引取数。如果你要改状态顺序,必须同步改所有引用的索引,这是最容易埋雷的地方,后面避坑章还会再提。param.m11、param.m22 是附加质量后的惯性项,通常用经验公式估,不同船型差别很大。

2.2 rk4.m:固定步长积分器在预测时域里的作用

MPC 的滚动预测本质上是一遍遍做数值积分:从当前状态出发,喂入候选控制序列,往前推 N 步,每推一步算一次代价。rk4.m 把这件事固定下来。源码里它是独立函数,我一般习惯在预测循环里直接调用它,而不是用 ode45 变步长积分——固定步长在 MPC 里更合适,因为优化器里每步的时间权重应该一致,变步长会让代价函数累加的语义变模糊。

function x_next = rk4(fun, dt, x, u, param) % 固定步长四阶龙格库塔,推进一个控制周期 k1 = fun(0, x, u, param); k2 = fun(0, x + 0.5*dt*k1, u, param); k3 = fun(0, x + 0.5*dt*k2, u, param); k4 = fun(0, x + dt*k3, u, param); x_next = x + (dt/6) * (k1 + 2*k2 + 2*k3 + k4); end

逻辑说明:rk4 用四个中间点的导数加权平均来逼近 h 区间内的状态变化,误差阶数为 O(dt^4),在 MPC 预测时域里足够用。步长 dt 一般取 0.1~0.2s,预测步数 N 取 8~12,这样预测时域在 1.6~2.4 秒。USV 从全速到停船的时间远大于这个值,所以 MPC 不需要把整个减速过程都预测进来,只需要覆盖对当前控制决策最敏感的时间窗口。

参数说明:dt 取大了预测轨迹容易数值发散(尤其是横荡速度 v 和角速度 r 耦合时),取小了每帧优化器要算更多步,实时性变差。我建议先按源码给的步长跑通,再逐步改小验证结果稳定性。有一点要注意:rk4 里 fun 的第一个参数 t 没有实际作用,但保留这个位置能让函数接口和 ode45 兼容,方便以后替换积分器。

2.3 MPC 主循环:预测、优化、执行三步的数据流

把 model.m 和 rk4.m 串起来的是主循环。这个循环每个控制周期做三件事:测量当前状态、求解最优控制序列、只执行第一步。

% 主循环:每个控制周期更新一次控制量 for k = 1:sim_steps % 1. 取当前状态、参考线和障碍物信息 x0 = x_log(end, :)'; ref = getReferenceLine(k, ref_waypoints); obs = obstacles(k); % 随时间步更新的障碍物状态 % 2. 求解最优控制序列(代价 + 约束交给优化器) U_opt = mpc_solve(x0, ref, obs, param); % 3. 只取第一个控制量,用 rk4 推进真实状态 u_apply = U_opt(1, :); x0 = rk4(@model, dt_ctrl, x0, u_apply, param); x_log(end+1, :) = x0'; end

逻辑说明:注意第 3 步只取了最优序列的第一步,这就是“滚动时域”的由来。预测了未来 N 步,但真正执行的只有眼下这一步,下一周期再重新测量状态。这样即使模型有偏差,每一步都用真实状态重新校正,不容易积累误差。

参数说明:dt_ctrl 是控制周期,也就是优化器被调用的频率,通常和 rk4 步长一致或成整数倍。仿真步长可以更细,但控制周期不要小于 solver 求解耗时,否则实时性就崩了。这里的 mpc_solve 是个接口,源码里用具体求解器实现,代价函数和约束在下一章拆开讲。

3. 代价函数与约束拆解:computeCost.m、constraint.m 与 computeMiu.m 的调参坐标

3.1 computeCost.m 里的四项代价与权重语义

摘要里说的四项设计目标——航迹偏移量、航速变化量、碰撞危险度和规则符合度——在 computeCost.m 里对应四块代价相加。先看骨架:

function J = computeCost(X, U, ref, refPsi, obs, Q, R, W, rule_w) % X: (N+1) x 6 预测状态序列 % U: N x 2 控制序列 % ref: (N+1) x 2 参考航迹点 % refPsi: (N+1) x 1 参考航向 % Q: 循迹权重 [q_pos, q_psi] % R: 控制变化权重 [r_delta, r_n] % W: 碰撞危险度权重 % rule_w: 规则符合度权重 pos_err = sqrt((X(:,1) - ref(:,1)).^2 + (X(:,2) - ref(:,2)).^2); J_path = Q(1) * sum(pos_err.^2); psi_err = wrapAngle(X(:,3) - refPsi(:,1)); J_psi = Q(2) * sum(psi_err.^2); % 控制增量惩罚:让执行机构别被命令得又猛又快 d_delta = diff(U(:,1)); d_n = diff(U(:,2)); J_ctrl = R(1) * sum(d_delta.^2) + R(2) * sum(d_n.^2); % 碰撞危险度:直接累加避碰模块的输出 J_col = W * sum(collision_avoidance(X, obs)); % 规则符合度:例如右侧超船代价 J_rule = rule_w * sum(ruleViolation(X, obs)); J = J_path + J_psi + J_ctrl + J_col + J_rule; end

逻辑说明:J_path 惩罚的是船到参考线的距离,J_psi 惩罚的是船艏和期望航向的夹角;两项合起来让船贴线走。J_ctrl 用的是控制量的差分而不是绝对值,目的是抑制舵角/转速的频繁跳变。J_col 把避碰危险度直接作为一个软约束加入代价,这就是 MPC 避碰和传统路径规划避碰的本质区别——这里不绕开障碍物,而是让碰撞风险变成惩罚项,优化器自己去权衡“偏航多远值得”。

参数说明:权重调节不是玄学,先做无量纲化再调相对比例。比如位置误差单位是米,1 米偏差对应代价 Q(1);控制变化单位是 rad/step,一次 0.1rad 舵跳变对应代价 R(1);以米为单位的目标权重大概在个位到几十之间。如果 W 比 Q 大三个数量级,船会远远绕开障碍物,航迹偏移惨不忍睹;反过来障碍物就像不存在。我一般先让 J_path 主导,再逐步增大 W 观察船在障碍物附近的“绕行半径”是否合适。

3.2 constraint.m:执行机构的幅值与变化率约束为什么必须分开写

MPC 优化问题的约束分两类:一类是控制量幅值约束,比如舵角不能超过 ±35 度、螺旋桨转速有上下限;另一类是变化率约束,比如舵机转舵速度只有每秒 10 度,相邻两个控制周期的舵令不能突变。两者不分开写,优化器会给出“瞬时大舵角”的命令,仿真里看着能躲开障碍物,实际机构根本执行不出来。

function [A, b] = buildConstraints(delta_max, n_max, slew_delta, slew_n, N, dt) % 幅值约束:每个周期的控制量都要在限幅内 A1 = kron(eye(N), [1 0; -1 0; 0 1; 0 -1]); b1 = repmat([delta_max; delta_max; n_max; n_max], N, 1); % 变化率约束示意:|u(k)-u(k-1)| <= slew * dt % 实际实现要先把上一帧控制量 u0 拼进优化变量再构造 A2 A2 = kron(eye(N-1), [1 0; -1 0; 0 1; 0 -1]); b2 = repmat([slew_delta*dt; slew_delta*dt; slew_n*dt; slew_n*dt], N-1, 1); A = blkdiag(A1, A2); b = [b1; b2]; end

逻辑说明:A、b 的规模和优化变量展平方式强相关——优化变量如果是 [delta_1, n_1, delta_2, n_2, ...] 的展平向量,约束矩阵就是用 kron 铺开的块结构。源码里如果直接用 CasADi 或 fmincon,写约束时只要保证维度对得上,不需要手拼矩阵。变化率约束的价值在于把执行机构的物理限制前置到优化问题里,而不是等控制量解出来再限幅。

参数说明:slew_delta=10pi/180dt,dt 是控制周期,算出来是两步之间允许的舵角增量;取值过紧会让船在大转向时“僵住”,过松又失去约束意义。调参时先看舵令日志有没有触顶,触顶再考虑放宽限幅还是降低航速,前者改执行力边界,后者改行为期望。

3.3 computeMiu.m 的物理含义:控制指令到实际执行机构的映射

拆包时我最开始忽略了 computeMiu.m,后面发现它是执行机构动态的关键一环。它的作用是把优化器给出的控制指令换算成 model.m 的输入——最常见的是把转速指令映射为推力系数,同时对舵角做一阶惯性滤波。原因很简单:真实舵机不可能瞬间到达指令位置,有一个时间常数;MPC 如果无视这个延迟,预测轨迹会偏乐观,导致真实系统跟不上。

function miu = computeMiu(u, u_last, dt_act, tau_delta, tau_n, param) % 执行器一阶惯性:指令到实际输出的滤波 delta_cmd = u(1); n_cmd = u(2); delta_act = delta_cmd + (u_last(1) - delta_cmd) * exp(-dt_act/tau_delta); n_act = n_cmd + (u_last(2) - n_cmd) * exp(-dt_act/tau_n); % 实际推力系数:转速映射到推力 thrust_coeff = param.Ct * n_act * abs(n_act); miu = [delta_act; thrust_coeff]; end

逻辑说明:如果直接使用 delta_cmd 进 model.m,MPC 是把舵机当成理想执行器,这在慢速 USV 上误差不大,但动态障碍物场景下会表现为“绕得开但动作晚了一拍”。把一阶惯性加进去后,代价函数会自然学会提前打舵。

参数说明:tau_delta 取舵机时间常数,小型电动舵约 0.2~0.5s;tau_n 取推进器响应时间,直流电机加螺旋桨通常 0.5~1.5s。建议把这两个值调大一个数量级看效果——轨迹会明显变得更“钝”,这就是执行器动态对控制品质的影响。想验证这个结论,保持其他权重不变,只把 tau 从 0.3 改到 3,看绕行弧线是不是明显变大。

4. 避碰与角度计算:collision_avoidance.m 和 computeAngle.m 的配合细节

4.1 静态障碍物危险度函数:重要的是梯度而不是数值

collision_avoidance.m 返回一个与预测状态序列等长的危险度向量,取值范围通常是 0~1。对静态障碍物,常见实现是用“船到障碍物圆心的距离 d 减去安全半径 R”构造指数型函数:距离越近、危险度越大,而且随距离递减的梯度不能太平。梯度太平的代价是优化器感受不到压力,权重再高也等于没有。

function hazard = collisionAvoidanceStatic(X, obs) % obs: 圆心位置 pos=[ox, oy], 半径 R, 安全余量 margin N = size(X, 1); hazard = zeros(N, 1); for i = 1:N d = norm(X(i,1:2) - obs.pos); r_min = obs.R + obs.margin; if d < r_min hazard(i) = 1; % 已进入禁入区,饱和 else hazard(i) = exp(-k_dist * (d - r_min)); end end end

逻辑说明:k_dist 决定危险度下降速率。k_dist 太小,障碍物还没进入预测时域就几乎没有代价;k_dist 太大,危险度只有贴脸时才变,优化器没有提前量。经验上让危险度在“预测时域末端那个距离”处降到 0.05 左右比较合适。

参数说明:一个很常见的坑是 cost 里加的 W*J_col 一直压不过 J_path。这时候不要只加权重,先算一下预测时域覆盖的距离:N 步乘以 dt 再乘以当前航速,就是船能“看到”的最远距离。如果这个距离比障碍物安全半径还小,避碰必然失效,把 N 从 8 加到 14 往往比把 W 加一倍更有效。

4.2 动态障碍物:相对速度与最近会遇距离的进代价方式

动态障碍物比静态难点在于危险度不仅要看距离,还要看相对运动趋势。常规做法是计算 DCPA(最近会遇距离)和 TCPA(到最近会遇点的时间):两条船都在预测时域内推一小段,取相对位置最近的那个点。DCPA 小于安全距离且 TCPA 在预测窗口内,危险度逼近 1;否则即便当前距离近,只要相对运动在拉开,危险度也可以很小。

实现上我一般把 TCPA 阈值和预测时域绑定:TCPA 大于当前预测时域长度的会遇不在本次决策窗口里,可以直接忽略,别让远处的动态障碍物拉扯控制量。动态障碍物模块的返回值会同时影响 J_col 和规则符合度——如果相对方位说明应该从右侧让开,但预测轨迹偏向左侧,规则符合度项会给额外惩罚。

4.3 computeAngle.m 的角度约定:五个最容易出错的归一化点

角度问题是这套代码里最容易出现玄学报错的地方。computeAngle.m 负责把参考航向、目标方位、相对方位算成适合进代价函数的量。我总结五个点:

第一,atan2 返回值在 (-pi, pi] 区间,但船艏向在多次累加后可能超过 2pi,所有状态里的角度先 wrap 再使用。第二,目标方位差必须 wrap 到 (-pi, pi],否则在 pi 附近会出现跳变,船会在航向参考点附近原地转圈。第三,横向偏差的符号要以船前进方向为基准,左右舷定了就不能反,反了就是持续回转。第四,动态避碰里计算相对方位时,统一采用“从船指向障碍物”的方向,尽量避免在方位角上同时出现船艏系和大地系两个参考系,否则绕行方向可能反。第五,角度误差进入代价函数前要做归一化,直接平方一个未 wrap 的角度会把 6.28 和 0.1 的差距放大到不可接受。

function angle = wrapAngle(a) % 归一化到 [-pi, pi] angle = atan2(sin(a), cos(a)); end

逻辑说明:wrapAngle 函数本身三行就能写出来,但要在 computeAngle.m 开头调用一次,在 computeCost.m 里算 psi_err 前再调一次。这样能保证无论状态里角度怎么累计,误差永远在最小等价区间内。

参数说明:如果你发现船在参考线附近来回摆动,但平均航迹偏移并不大,优先检查 psi_err 计算时是否少了一个 wrap——这是我在这类代码里见到频率最高的细小 bug。

5. 复现避坑:我在这套源码上踩过的五个问题与定位方法

5.1 数值与配置层面的三个坑

坑一:预测轨迹发散成 NaN。现象:仿真跑几帧后状态全变成 NaN,日志里 x/y 位置出现 inf。原因:最常见是角度没有 wrap,psi 累加到几十几百弧度,cos/sin 本身没有问题,但差分误差会被 RK4 放大;其次是 rk4 步长太大,船模回转角速度达到每秒十几度时,dt 超过 0.5s 就会数值发散。解决:给所有角度入口套 wrapAngle;把 dt 降到 0.05s 做对照试验,如果恢复正常说明是步长问题;最后查约束矩阵里有没有 NaN 元素混进优化问题。

坑二:仿真慢到让人崩溃。现象:一个 200 秒的仿真跑了十几分钟,调一次参数要等半天。原因:主循环内每帧都调用绘图函数实时刷新;预测时域 N 设到三四十步;优化器每次迭代都重新初始化 Hessian 近似。解决:把绘图改成“每 10 步刷一次”,或者干脆跑完再统一 plot;预测时域回到 10 步左右;如果必须用 fmincon,把最优性容差放宽。实时性达标线是单帧求解耗时小于控制周期,否则闭环就名存实亡。

坑三:约束矩阵维度报错。现象:solver 报“约束维度不一致”,或者解出来的控制序列形状和预期错位。原因:MPC 优化变量可以是展平向量也可以是矩阵,A/b 的行数必须严格对应。改过控制量个数(比如从舵加转速变成三推进器)之后,约束矩阵还沿用两变量的 kron 结构,就会出这种问题。解决:先构造一个 dummy 问题的约束并打印尺寸,核对无误后再接进 solver。我自己的习惯是约束矩阵单独写一个函数,不在主循环里拼接,方便随时打印排查。

5.2 逻辑与一致性层面的两个坑

坑四:USV 在目标点附近原地画圈。现象:距离目标越来越近,但船就是反复绕圈,轨迹像螺旋。原因:多半是参考方位角差没有 wrap 到 (-pi, pi],当目标方位越过 pi 边界时,误差从 0.1 跳成 6.2,MPC 认为需要转一整圈才能对准。解决:检查 computeAngle 里目标方位与当前艏向的差值是否强制归一化;顺手在日志里打印 psi_err 看看有没有跳变。

坑五:避碰模块完全没有触发。现象:船直直穿过障碍物范围,危险度输出始终为 0。原因:一个可能是障碍物坐标写进了大地坐标系,但预测状态 X 里位置是相对某个局部原点的;另一个是预测时域太短,障碍物根本不在预测窗口内,危险度函数一直作用在零输入区。解决:先打印 X 和 obs 坐标,确认两者在同一坐标系;然后把 dt*N 缩短到 1 秒做一次“贴脸过障碍”的实验,如果危险度还是 0,问题一定出在坐标映射;如果危险度变了,就是时域不够,加 N 或加 dt。

再补充一条比对经验:PDF 文档里的权重参数和源码默认值经常不一致。文档描述的是设计思路和一组推荐参数,源码为了跑通可能调得比较温和。遇到和文档数据对不上的情况,不代表代码有问题,按源码可复现行为为准;想复现文档里的图表,再用文档参数覆盖默认值跑一遍。

6. 验证与进阶:用 demo.jpg 对照仿真效果,再把代码改造成自己的场景

6.1 怎么验证结果有效

demo.jpg 是包里的示例输出图。先把仿真跑完,保存 x_log 和 u_log,再画轨迹图。效果达标看四个指标:平均航迹偏移能否压在船宽量级(比如 1~2 米以内);最小会遇距离是否始终大于障碍物半径加安全余量;控制量序列有没有频繁触顶或剧烈跳变;单帧求解耗时是否小于控制周期。这四个指标同时满足,才能说这个 MPC 闭环是“可以落地”的,而不是只在图里看着漂亮。

然后打开 demo.jpg 对比轨迹形态:正常结果应该有一段贴线航行、一到两个平滑的绕行弧线、回到参考线后不再振荡。如果形态差异大,先回到第 5 章逐条排查,不要急着调权重。

6.2 改造成自己场景的三个方向

第一个方向是换船型参数。model.m 里的 T、K、m11、m22 等换成你的 USV 参数,这一步基本是查表替换。第二个方向是给动态障碍物加匀速外推轨迹,如果 obstacles 结构里带速度向量,collision_avoidance 就能把“预测窗口内的会遇距离”算进去,这就是从静态避碰跨到动态避碰的最小改动。第三个方向是给风浪流场加时间变化,在 model.m 里把干扰项从常量改成时间函数,MPC 自然会把这部分当成未来扰动处理,适合做抗扰验证。

我最初拿到这类源码总想先调权重,结果在“绕圈”问题上空耗了两天。后来我习惯拿到任何 MPC 仿真代码,第一时间把状态向量顺序、角度归一化调用点、控制周期和预测时域四个常量画在一张纸上,再去碰代价和约束。从那以后每次调试都先跑通基线,再逐项加权重或障碍物,不再把优化器当黑匣子乱试——希望帮到你。

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

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

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

立即咨询