接手一个移动机器人的导航项目时,我最早花了很多时间只调 DWA,认为局部避障做好了,车就能跑起来。后来发现,在几十米范围的仓库场景里,光靠 DWA 根本走不到目标点——它只能看到周围一米左右的环境,遇到长的障碍物带就绕不过去,常常原地左右摇摆。搭上 A* 之后全局路径有了,跑起来顺了很多;再后来又遇到巷道边缘堆着临时托盘、行人乱走的状况,A* 的栅格路径在动态变化的环境里不够稳,于是把 RRT 也拉进来做全局备选。从“只会一个算法”到“三个算法分工配合”,这个过程踩了不少坑,也把每种算法的脾气摸清了。这篇内容算是自己在这个项目里的完整梳理,适合正在做移动底盘导航、或者刚入门路径规划的开发者参考。
我手里的测试平台是一台差速底盘的轮式机器人,配单线激光雷达,建图用的是常规栅格地图,地图分辨率 0.05m,整车尺寸 0.6m×0.7m,最大速度 1.2m/s,最大角速度 1.5rad/s。场景是一个 40m×50m 的模拟仓储库房,有三条 1.8m 宽的巷道,还有两处直角弯和一处狭窄门洞。后面讲到的参数、代码、数据都是从这套环境里来的,你可以按自己的平台适当缩放。
1. 路径规划系统里,三个算法到底各自扛什么活
1.1 项目背景与“单算法”的隐患
先说结论:A*、RRT、DWA 这三个东西不是同一个层级上的同类算法,硬拿来对比谁好谁坏没有意义。它们解决的是不同尺度、不同约束下的问题。DWA 是局部规划器,负责在当前时刻输出车的线速度和角速度;A* 是全局规划器,负责在已知静态地图里算出一条从起点到终点的大致路径;RRT 也是全局规划器,但它更适合高维空间、非结构化环境和快速避让动态变化区域。
最初我接到这个任务时,只用 DWA 做导航。DWA 本质上是一个速度空间采样算法,它从当前速度出发,枚举一小段时间内所有可能的“线速度+角速度”组合,用运动模型推演出未来一段轨迹,再根据评分选择最优的一组速度去执行。听起来很合理,实际一跑就发现问题:DWA 的规划视野取决于速度采样时的模拟时间predict_time,我一开始设成 1.5s,最大速度 1.2m/s,也就是它最多能看到未来 1.8m 左右的轨迹。而仓库里一堵墙可能有 8m 长,车到了墙面前,DWA 检测到前方障碍,横向的候选速度又被减掉,只能反复切换“左转/右转”,在两个方向上摇摆,最后停在原地报“无可行轨迹”。
这不是参数没调好,而是原理决定的:局部规划器只看得到局部,永远做不出“从墙的尽头绕过去”这种全局决策。要让车在真实场景里从 A 点移动到 B 点,必须有一个更高层的规划器先画出一条完整路线,DWA 再沿着这条路线做局部避障。这个认知花了两个礼拜才彻底想明白,也解释了为什么业内大多数导航框架都是“全局规划 + 局部规划”分层架构。
A* 在这个架构里承担全局选路的任务。它工作在栅格地图上,把地图看成网格,用广度优先搜索的思路从起点开始扩展节点,但通过一个启发函数指导扩展方向。它保证在给定栅格地图上找到最短路径(如果启发函数满足一致性要求),这也是它在工程中受欢迎的原因之一——路径质量稳定、可重复、可解释。
RRT 则是我在后期处理复杂场景时才加进来的。它不需要把地图网格化,而是直接在连续空间里随机撒点,然后从起点出发不断把新采样点连接到已有随机树上。它的实现简单且非常适合高维状态空间,在地图分区不规则、包含动态障碍物或需要快速重新规划的场合,RRT 比 A* 更快找到一条可行通道,但路径长度通常不是最优的。它不是用来替代 A*,而是作为全局规划层的另一条候选路径来源,在某些场景下比 A* 更可靠。
1.2 全局规划与局部规划的职责边界
工程实现上,我会把整个导航逻辑拆成三层:
- 任务层:接收目标点,维护当前状态机(如待命、前往目标、任务完成);
- 全局规划层:负责在栅格地图上计算全局路径,每隔一段时间或检测到地图变化时重新规划;
- 局部规划层(DWA):以全局路径作为参考,输出实时控制指令。
全局规划和局部规划之间靠一个关键机制对接:全局路径上的“局部目标点”。DWA 不是机械地追全局路径上的每一个点,而是从当前机器人位置出发,沿全局路径找一个距离机器人一定距离(比如 2~3m)的前瞻点,把这个点作为当前局部目标。这样做的原因是,全局路径通常很密集,A* 在 0.05m 分辨率地图上算出的一小段路径可能包含几十上百个点,如果 DWA 每个周期都试图精确逼近当前最近点,会出现两个问题:一是路径点之间的小角度变化会让 DWA 频繁加减速,车走起来一顿一顿的;二是如果全局路径在某个转角处有轻微锯齿,DWA 可能在转角处反复对点,导致轻微摆动。
用前瞻点之后,DWA 只关心“怎么在避开障碍的前提下到达前方那个点”,全局路径的锯齿被天然过滤掉了。这个设计也符合人的开车间距意识——不是每一步都盯着眼前的车道线,而是看着前方一个合适距离的参考点。
这个分层结构还有一个好处:如果全局规划算法要替换或者新增(比如我要用 RRT 做备选路径),只需要改全局规划层,DWA 完全不需要动。我最初给全局规划层设计了一个统一接口,输入是当前坐标、目标坐标、地图,输出是PathPoint[],后续不管是 A* 还是 RRT 都实现同一个接口,切换成本很低。这个项目经验让我养成了一个习惯:做算法集成时先定义好数据接口,再往上堆算法,否则后期每加一个算法都要重写局部规划器,工程量会成倍增加。
2. 动态窗口法 DWA:本地执行层的安全底线
2.1 DWA 是怎么在速度空间里“找窗口”的
DWA 全称 Dynamic Window Approach,中文常译作“动态窗口法”,核心思想非常直白:每一时刻,机器人能执行的速度并不是一个无限集合,而是受到自身加速度和最大速度限制的“动态窗口”约束。在这个窗口内采一组离散速度,模拟未来一段时间的轨迹,然后按评分挑选最优速度执行。整个过程在实时控制周期内完成,通常一轮计算耗时在 10ms 级,完全满足控制频率要求。
具体来说,速度空间由三个约束裁剪:
- 机器人自身速度极限:线速度不能超过
v_max,角速度不能超过w_max; - 电机加减速能力:当前速度
v_cur在时间间隔dt内最多能变化v_acc * dt,角速度同理,这形成了速度值的动态下界和上界; - 安全制动距离:以当前候选速度模拟刹车,如果在碰到障碍之前能停下来,该速度才被允许;否则速度组被剔除。
经过这三层约束后,剩下的合法速度组就叫“动态窗口”。接下来在这个窗口里按照一定分辨率(通常线速度 0.01m/s、角速度 0.05rad/s 左右)均匀采样。
然后对每一组候选速度推演轨迹。对于一个差速底盘,可以用下面的运动模型近似推演:
x_{k+1} = x_k + v * cos(theta_k) * dt y_{k+1} = y_k + v * sin(theta_k) * dt theta_{k+1} = theta_k + w * dt这里v是线速度,w是角速度,theta是航向角。把这一步重复n次(n = predict_time / dt),就得到一条未来轨迹。然后把轨迹上的每个点与障碍物地图做碰撞检测,对可通行轨迹计算评分。
评分函数是 DWA 最有意思的部分,基本上每个工程落地时都要针对场景调整权重。最经典的评分公式是:
score = alpha * heading + beta * clearance + gamma * velocityheading:轨迹末端方向与目标方向的角度差越小,分越高;clearance:轨迹离最近障碍物的距离,距离越大,分越高;velocity:鼓励保持较大线速度,减少到达时间。
但实际工程里我很少直接套用这三个权重。比如在一个窄巷道环境里,如果beta(障碍距离权重)设置相对过大,机器人会走到巷道正中间,碰到只有一侧有障碍物的区域时,轨迹会偏向另一侧空地上,导致大幅绕行,走出来的路径严重偏离全局路径。所以我更倾向于在评分前用全局路径做“目标点”约束:用前面提到的前瞻点替代heading的单一目标方向。
如果追求更平滑的路径,还可以在评分项里加上“与上一时刻速度变化量”的惩罚项,减少急加速急减速。这个项类似 PID 里的微分项:
score += -lambda * (abs(v_candidate - v_current) + abs(w_candidate - w_current))加了这项之后,机器人实际控制指令更加平滑,底盘电机的电流波动也小很多。代价是到达时间会长一点,但这个代价在仓储环境下完全值得。
2.2 自己实现 DWA 时的参数调参经验
DWA 参数算不上多,但每一项都很敏感。我自己在模拟仓库环境里调出来的参数可以供参考:
| 参数 | 数值 | 作用说明 |
|---|---|---|
| 最大线速度 | 1.2 m/s | 由底盘硬件决定 |
| 最大角速度 | 1.5 rad/s | 受限于转向能力 |
| 线加速度 | 0.8 m/s² | 电机加减速能力 |
| 角加速度 | 1.2 rad/s² | 电机加减速能力 |
| predict_time | 2.0 s | 规划视野时长 |
| 模拟时间步长 | 0.1 s | 轨迹推演步长 |
| 线速度采样间隔 | 0.02 m/s | 候选密度 |
| 角速度采样间隔 | 0.06 rad/s | 候选密度 |
| 前瞻点距离 | 2.5 m | 全局路径截取距离 |
predict_time 的选择很关键。设置太小,机器人视野短,速度刚起来就发现前方障碍,频繁急刹;设置太大,轨迹推演的点数增多,计算量变大,而且远离机器人的预测轨迹可信度低(地图误差和动力学误差都会累积)。我的经验是,predict_time ≈ 2 * 前瞻距离 / 最大速度是一个不错的出发点,比如前瞻 2.5m、最大速度 1.2m/s,算出来约 2.1s,取 2.0s 正好。
v_max不能想当然地设成底盘标称最大速度。在一个 1.8m 宽的巷道里,如果最大速度 1.2m/s,直角转弯处 DWA 为了让轨迹安全通过,会频繁选择低速转向,实际平均速度可能只有 0.4m/s。这种情况下适当降速反而提升通过效率,我后来在直角弯区域把限速降到了 0.6m/s,整体任务时间反而缩短了。
还有一点,DWA 给出的线速度角速度指令需要低通滤波后再发给底盘,否则底盘执行器响应很快,会出现轻微的抖动。我在指令侧加了一个一阶低通滤波,时间常数约 0.2s,实测轨迹跟踪平滑性提升很明显。但要注意滤波时间常数不能太大,否则会导致真实速度反馈滞后,DWA 的碰撞检测防线会失效,这在动态障碍多的场景里很危险。
3. A* 与 RRT 的选型和组合细节
3.1 A* 在栅格地图上的实现与性能优化
A* 在栅格地图上的实现,本质上是在节点图上做带启发式的最短路径搜索。我采用的栅格地图是 0.05m 分辨率,40m×50m 的场景就是 800×1000 个栅格,总共 80 万个节点。暴力搜索全部节点不现实,所以 A* 的性能优化非常重要。
A* 的核心公式是:
f(n) = g(n) + h(n)其中g(n)是起点走到当前节点n的实际代价,h(n)是从当前节点到目标点的启发估计。启发函数的选择直接影响搜索速度和路径质量。我前前后后试过三种启发:
- 欧几里得距离:
sqrt((dx)^2 + (dy)^2),计算代价略高,路径方向性弱一些; - 曼哈顿距离:
|dx| + |dy|,要求只能横竖移动,在允许斜向移动的情况下会低估实际距离,搜索范围偏大; - 八方向距离(切比雪夫距离):
max(|dx|, |dy|),适合允许八邻域移动的栅格图,我最终选了这个,搜索节点数量比曼哈顿少约 30%,路径也更自然。
A* 的性能瓶颈通常不在启发函数,而在开放列表的维护。我用的是二叉堆(priority queue),每次从开放列表取出f值最小的节点,这一步是 O(logN)。还有一个关键优化:提前判断节点能否进入开放列表。
常规做法是把当前节点能到达的邻居都先塞进去,再由堆排序去筛选。但栅格地图里很多邻居会重复进入,浪费大量时间和内存。我的做法是:对每个节点维护一个best_g值,只有当新的g比之前的best_g更小时才允许入堆,否则直接丢弃。这个“懒删除”思路能大幅减少无用节点数量。实测下来,在 80 万栅格的地图里,从头到尾规划一条横穿全场的路径,平均耗时从 280ms 降到了 90ms 左右。
还有工程上很实用的两个小技巧。第一,栅格地图在做碰撞检测时,一定要先把机器人半径考虑进去,对地图做膨胀处理。机器人半径 0.35m,加上 0.1m 的安全余量,就是 0.45m,膨胀半径按 9 个栅格(0.45 / 0.05)处理。不做膨胀的后果是 A* 算出来的路径贴着墙走,DWA 执行时发现障碍物距离不足,频繁给“刹车”指令,根本跑不起来。
第二,A* 的路径点密度极高,在 0.05m 分辨率下,1m 路径就有 20 个点。直接传给 DWA 会让局部规划器对每个点都做一次目标点匹配,开销很大,而且容易发生抖动。我在 A* 输出端做了抽稀处理,保留路径方向变化大于 15° 的点和关键转折点,这样一条 30m 的全局路径从 600 个点压到大概 80 个点,DWA 的匹配压力小了很多。
3.2 RRT 面向复杂巷道和变化环境的用法
RRT 的全称是 Rapidly-exploring Random Tree,中文常叫快速扩展随机树。它的原理比 A* 原始得多:从起点开始建立一棵树,每次在地图的可通行区域内随机采样一个点,然后从树上找距离这个采样点最近的节点,沿着两点连线方向扩展一段固定距离(步长),如果这段路径没有碰到障碍,就把新节点加入树中。不断重复这个过程,直到某个节点靠近目标点一定距离时,回溯出一条从起点到目标的路径。
我最早觉得 RRT 就是个“碰运气”的算法,路径不可预测,而且不够最优。直到遇到这种场景:仓库里有几排货架,货架之间形成窄巷道,如果其中一个巷道被临时停放的托盘堵住了,A* 在静态栅格地图上仍然会规划穿过这个巷道的路径,因为地图上根本没有托盘。而使用 RRT 时,如果我们在每次传感器发现新障碍后重新采样,RRT 的随机性反而变成了优势——它会尝试各种其他通道,哪怕它们不在原始 A* 路径上。
RRT 的步长参数非常关键。步长越大,每次扩展距离越远,收敛越快,但路径越粗糙,甚至在窄通道采样时,新节点会直接跨过走廊、嵌入对侧障碍物里。步长越小,路径越精细,但节点数量爆炸,收敛时间变长。在 1.8m 宽的巷道环境里,我用的步长是 0.3m,这个值大约是巷道宽度的 1/6,既能保证通过窄通道时不会“跨栏”,又能控制扩展次数。
另一个参数是提前停止的判断:当树中某节点与目标点的距离小于goal_tolerance时,就认为路径已找到。我设的goal_tolerance是 0.3m,这防止了算法为了精确到达目标点而无限扩展。如果地图里目标点周围有障碍物,还要检查最后一段路径是否真的无碰撞。
RRT 生成路径后,一般要做平滑。因为这个算法生成的路径由大量短线段组成,转折角可能很大,直接交给 DWA 跟踪会出现频繁减速。我用的平滑方式是:对 RRT 路径做“剪枝”,凡是两点之间的连线不碰障碍,就跳过中间节点,将这两点直接相连。重复这个过程直到无法再缩短,然后再做一次三次样条平滑。这样处理之后,RRT 路径的可跟踪性跟 A* 路径差别不大了。
3.3 两者怎么选,怎么互补
A* 和 RRT 同属全局规划,但在架构里承担的角色其实可以共存。我采用的方式是双全局路径源:
A* 是默认主路径,因为它在静态地图上给出的是确定性的最短路径,便于预测任务时间、避开已知大障碍。RRT 作为备选路径源,在以下场合启用:
- 全局路径的终点附近被新障碍物阻断,A* 重新规划后仍然走不通;
- 地图部分区域因为动态障碍出现明显变化,需要快速生成一条替代路线;
- 目标点在人无法进入的狭窄空间,A* 因网格离散化可能找不到路径,RRT 的连续采样更擅长这种场景。
在实际系统中,我保留两条全局路径,由上层逻辑根据代价选择。代价函数包括路径长度、预估通行耗时、以及路径与最近障碍物的距离。RRT 路径通常在长度上吃亏,但在“绕开变化区域”这一点上更有价值。我把 A* 看成“稳妥的保守派”,把 RRT 看成“灵活的冒险派”,两者并存时,绝大多数场景都能兜住。
用表格简单总结一下它们的对比:
| 对比维度 | A* | RRT |
|---|---|---|
| 工作空间 | 栅格离散图 | 连续或任意空间 |
| 最优性 | 栅格分辨率下最优 | 不保证最优 |
| 计算复杂度 | 与栅格数量相关 | 与迭代次数相关 |
| 窄通道表现 | 稳定,但耗时长 | 可能快速通过 |
| 动态变化适应性 | 需重新规划 | 天然适合快速重规划 |
| 路径平滑度 | 较好,仍有锯齿 | 差,需要后处理 |
从实现角度看,A* 是先算后走,RRT 是边走边算(快速重规划)。两者的本质差异在于:A* 依赖一张准确的全局代价图,RRT 更依赖实时采样和碰撞检测器的速度。这在它们与 DWA 融合时,会体现出截然不同的工程调性。
4. 三算法融合落地:全局规划 + 局部避障的工程方案
4.1 融合框架:从全局路径到局部目标点的传递
现在说最核心的部分——怎么把三个算法安全地组合在一个系统里跑。一个最常见的错误是“主程序里先调 A*,再在每一步循环里调 DWA,完全不管中间状态”,这样两个算法各做各的,A* 的路径只是一根“看得见摸不着”的线,DWA 走到一半发现路径被动态障碍挡住,也不会重新请求 A* 规划,结果就是机器人停在障碍前发呆。
我的做法是引入一个“全局重规划条件”和“局部目标点管理模块”。整体控制循环可以抽象成这样的伪代码:
while task_active: if need_global_replan(): global_path = choose_global_path(astar_path, rrt_path) path_smooth = smooth_global_path(global_path) lookahead_point = get_lookahead_point(robot_pose, path_smooth, lookahead_dist) v_cmd, w_cmd = dwa_planner(robot_pose, lookahead_point, local_costmap) send_velocity(v_cmd, w_cmd)choose_global_path(astar_path, rrt_path)就是融合决策模块。我给它设定的规则很简单:
- 优先采用 A* 路径;
- 如果 A* 路径被动态障碍阻断,再重新规划一次;
- 若重新规划后仍然阻断,调用 RRT 生成新路径,并在下一次控制周期里评估两条路径的代价,选择可行且总代价较低的一条;
- 如果 RRT 找不到可行路径,则上报“全局路径失败”,请求操作员介入。
get_lookahead_point是连接全局规划和 DWA 的纽带。这个函数接收机器人当前位置和完整全局路径,返回一个距离当前位置指定距离的点。这里的距离不是欧几里得距离,而是沿路径的累计弧长。因为如果直接用欧几里得距离选点,在经过 U 形弯时会选到弯道对侧的路径点,导致 DWA 目标点穿越障碍区。我用的是沿路径点逐个累计距离,找到第一个累计弧长大于lookahead_dist的点。
DWA 拿到这个局部目标点后,它的作用就变得非常单纯:在局部代价地图里,车载激光雷达实时扫描数据与全局地图叠加,算出避障轨迹并把车导向这个前瞻点。只要前瞻点选择合理,DWA 的避障就和全局方向天然一致,不会出现在错误的障碍侧绕行。
工程上还有一个细节很值得写:DWA 的代价地图与 A* 的全局地图的坐标系必须完全同步。设备启动时,我们做了一次坐标标定,把激光雷达坐标系、底盘坐标系、里程计坐标系全部转换到地图坐标系。之后每次全局重规划或 DWA 更新局部代价地图,都从同一个坐标基准读取,这省掉了很多在融合阶段极其隐蔽的“坐标系错位”bug。早期项目里,DWA 局部地图漂移了 0.2m,车在窄巷道里老是贴着墙跑,排查了两天才发现是坐标变换多乘了一次车辆的偏航角。
4.2 路径平滑为目的的可操作步骤
全局路径直接给 DWA 用还是不够。A* 的路径有锯齿,RRT 的路径更是瘸腿的折线。直接跟踪这种路径,DWA 为了贴合每次的转向点,会频繁减速,严重时可能把平均速度从 1.0m/s 拉到 0.35m/s。所以我的融合层里有一个独立的“路径平滑”步骤。
当初我尝试过很多平滑算法,简单有效的方案是“梯度下降 + 三次样条”的组合。大致步骤这样:
- 对全局路径点做抽稀,把路径点数量减少到原来的 1/5 左右;
- 对抽稀路径做碰撞检测,如果某段抽稀后的连线穿过障碍物,保留中途的关键节点;
- 对所有路径点做带障碍物约束的平滑迭代;
- 对平滑结果做三次样条插值,得到稠密的、曲率连续的路径。
平滑迭代的核心代码如下,它其实就是一个带约束的梯度下降:
# 参数 iters = 300 alpha = 0.3 obs_cost = 0.15 for it in range(iters): for i in range(1, len(path) - 1): prev_p = path[i - 1] next_p = path[i + 1] cur_p = path[i] # 向两点中点收敛 mid = (prev_p + next_p) / 2 grad = (mid - cur_p) * alpha # 避开障碍物 if is_obstacle(cur_p): grad += (cur_p - nearest_obstacle(cur_p)) * obs_cost path[i] += grad这里有一个重要细节:平滑迭代前必须先把路径抽稀,否则连续相邻的路径点会因为彼此距离太近,导致步长和障碍代价相互干扰,平滑结果反而变得扭曲。我抽稀后,相邻点之间的最小距离保持在 0.2m 左右,迭代收敛很稳定。
迭代次数不是越多越好。400 次以后路径基本定型,再多迭代也不会明显改善,反而可能因为反复拉扯而出现轻微过冲,导致路径压到障碍物边缘。所以在迭代过程中我会持续检查平滑后路径与障碍物的最近距离,一旦小于安全阈值就停止迭代并回退到上一版路径——这个保护机制在实际调试中救过我很多次。
平滑完成后再用三次样条插值,把路径均匀加密到每 0.1m 一个点。DWA 拿到的是这样一条“顺滑的、连续的、可跟踪的”路径。从 A*/RRT 粗糙输出到 DWA 可用输入,质量提升非常明显,实测平均跟踪误差从 0.18m 降到 0.06m。
4.3 融合实测效果与关键对比数据
我在模拟仓储环境里做了一组对照实验,用同一台底盘、同一张地图、同一组任务点,分别跑了以下几种配置:
| 配置 | 平均速度 | 任务完成时间 | 碰到障碍次数 | 全局重规划次数 |
|---|---|---|---|---|
| 仅 DWA | 0.28 m/s | 未完成 | 多次 | 0 |
| A* + DWA | 0.72 m/s | 128s | 0 | 8次 |
| RRT + DWA | 0.65 m/s | 142s | 0 | 17次 |
| A* + RRT + DWA | 0.78 m/s | 115s | 0 | 5次 |
数据说明几个问题。仅 DWA 跑不完全程,因为它在长墙前反复摇摆。A* + DWA 配置下平均速度已经可以接受,但重规划次数偏多——原因是 A* 规划出的最短路径要经过一个很窄的门洞,而门洞附近经常有临时堆放的箱子,DWA 判断无法通过,于是频繁触发全局重规划。这个场景正是 RRT 发挥作用的地方,加进来之后,遇到障碍物时 RRT 快速生成另一条绕行路径,全局重规划次数反而下降到 5 次,而且被选中路径并不总是 A* 的路径——有时候是通过另一个巷道多绕了 6m 但是更稳妥的那条。
融合配置的任务时间从 128s 降到 115s,主要收益来自减少“无效重规划 + 原地等待”。A* 每次重规划耗时大约 90ms,RRT 每次约 40ms,等待本身并不算长,但每次重规划后 DWA 都要重新确定前瞻点、重新加速,这个“重新建立运动状态”的过程才是时间损耗的大头。从这个角度看,全局规划层的冗余反而提升了系统整体效率。
还有一个值得注意的指标:碰撞次数。三种配置下都是 0 次,这说明 DWA 在面对未知障碍物时确实可靠。DWA 的职责边界很清晰——它不负责“绕远路”,它只负责“别撞上”。全局规划层给了它一个绕行的机会,DWA 只需要在局部做精细避让,两者协作后,系统的安全底线和通行效率都有保障。
5. 实战排坑与避坑经验
5.1 常见问题速查表
调这三个算法时,目标行为怪异,但出错点往往不在算法本身,而在输入数据和参数之间。我在自己的项目里总结了一个问题速查表,基本覆盖了遇到的大多数情况。
| 现象 | 最可能原因 | 解决方案 |
|---|---|---|
| DWA 原地摇摆 | 全局路径缺失或前瞻点太近 | 检查 lookahead_dist,上调到 2.5~3.0m |
| A* 规划极慢 | 开放列表重复节点过多 | 加 best_g 防止重复入堆 |
| A* 路径贴墙 | 没有进行地图膨胀 | 按机器人半径+安全余量膨胀地图 |
| RRT 迟迟找不到路径 | 步长过大,窄通道内采样失效 | 缩短步长至巷道宽度 1/6 左右 |
| RRT 路径过弯太大 | 节点回溯后未做剪枝和平滑 | 增加剪枝和三次样条平滑 |
| 融合后车突然急刹 | 前瞻点跨过障碍区 | 用累计弧长选点而非欧几里得距离 |
| 局部地图漂移 | 坐标变换额外乘了偏航角 | 检查 tf 树和坐标系变换 |
| DWA 在转弯处减速明显 | 速度权重太低或最大速度过大 | 适当降低该区域限速,降低速度权重 |
| A* 路径被废弃,RRT 频繁接管 | 动态障碍在 A* 路径附近 | 检查地图刷新频率,加大局部代价地图更新频率 |
| 车绕大圈走远路 | 平滑迭代把路径扯变形 | 抑制过冲,增加碰撞检测保护 |
这个表格里每个问题都是我实际调过、修过的,不是网上随便抄来的。比如“DWA 原地摇摆”这个现象最有迷惑性,第一眼看着像参数问题,实际上往往不是 DWA 的问题——你去看 DWA 日志,会发现它每个周期都在选“当前最好”的速度,只是那个“最好”一直在左右横跳。真正原因是全局路径在墙前面断了,或者前瞻点选到了障碍物上。把这几行排查逻辑记住,能省下大量调参时间。
5.2 我踩过三次以上的坑
第一个大概率会踩的坑:忽略模拟器与真机的动力学差异。我在模拟仓库里把 DWA 的加速度设得很大,因为仿真模型的响应是完美的,顺利跑通。到真机上,底盘电机的响应延迟和摩擦非线性导致实际加速度明显低于设定值,结果 DWA 模拟出来的轨迹与真实轨迹不一致,碰撞检测形同虚设。后来我在每个速度周期里加入了真实底盘速度反馈的闭环补偿:DWA 模拟所用起始速度不再取“上次指令速度”,而是取“当前里程计反馈速度”,这从原理上保证了运动模型和真实状态同步。真机效果瞬间正常了。
第二个反复踩的坑是“地图刷新频率与全局重规划之间的时间竞赛”。初版系统里,激光雷达以 10Hz 更新局部代价地图,全局规划层每 5s 才检查一次是否需要重规划。当动态障碍突然出现在全局路径上时,DWA 第一个发现并避让,但全局规划层不知道路径已经失效,还在用旧路径引导。这导致一个很别扭的状态:DWA 明明绕开了障碍,全局路径却还在障碍上,下次前瞻点又会把车拉回去,车像“画圈”似的来回绕。解决办法是把“DWA 连续 N 次检测到前方无障碍但无法沿全局路径前进”作为全局重规划的触发条件,而不是单纯按时间轮询。这个改动让整个系统在动态场景下的表现稳定了很多。
第三个坑属于工程素养层面的:不要试图在一个功能里同时改三个算法。我在早期融合时,每次遇到问题就顺手调一下 A* 的启发代价、再改一改 RRT 的步长、再动一下 DWA 的权重,结果根本分不清是哪个改动导致的,出了新问题也无法定位。后来我给自己定了一个死规矩:每次只改一个参数,改完跑一遍基准测试,记录行为变化,再改下一个。虽然过程慢,但排查效率非常高,很多之前积累的参数选择经验都是靠这个习惯沉淀下来的。做路径规划算法融合,真正的难点不在算法本身,而在“把三个算法各自的状态、触发条件、参数影响边界梳理清楚”这个系统性工程。
如果让我给新人一个直接的建议:先把 DWA 单独调到能在一个简单的矩形场地里稳定绕开临时障碍,再把 A* 或 RRT 单独调好,最后才考虑融合;融合时也要先从最简单的一个全局路径源 + DWA 的组合入手,跑通了再叠加冗余路径源。这个顺序能避开 80% 的坑。
最后再分享一个小技巧:每个规划周期里,把当前选择的速度、轨迹、全局路径、前方障碍物距离这些关键数据全部可视化出来。你不需要画多精致的图,只要能同时看到“全局路径”和“DWA 当前候选轨迹”这两条线,融合逻辑里大部分诡异行为都会一目了然。我调试过程中有一半的问题都是靠盯着一张实时轨迹图瞬间看明白的,比看任何日志都快。