GRBL速度前瞻算法解析:反向规划与正向规划让雕刻机告别顿挫
第一次用 GRBL 固件跑雕刻机时我遇到一个特别折磨的现象G 代码里明明是一整串连续走线机器却每跑一小段就“咔哒”一下像在跳机械舞。后来翻到 GRBL 源码的 planner.c才搞清楚问题不在电机也不在驱动而在固件内部的“速度前瞻”不够聪明。GRBL 的速度前瞻算法里决定最终速度曲线的是两个方向相反的扫描反向规划和正向规划。看懂这两个 pass基本就摸透了 GRBL 运动控制的内核。这篇文章不打算贴一整段源码然后逐行念注释而是把 GRBL 的规划器planner当成一个“怎么开车才又快又不翻车”的问题来讲。适合这几类人看想在 Arduino 上跑 GRBL、被雕刻机/激光机一顿一顿折磨过的玩家以及想搞懂嵌入式运动控制固件、准备往 Marlin/Klipper 源码里钻的开发者。读完你至少能明白为什么同样的进给速度有的固件跑得丝滑有的固件跑得像拖拉机以及当你想调快机器时该动加速度还是动前瞻窗口。1. 先把 GRBL 的速度规划模型讲清楚1.1 单段运动曲线是梯形的GRBL 里的每个运动段block对应 G 代码里的一次直线或圆弧插补运动。对单独一段来说速度曲线长这样从入口速度加速到目标速度跑一段匀速再减速到出口速度。三段拼起来就是一个梯形如果段太短来不及上到匀速就退化成只有加速和减速的三角形。很多刚接触的人以为“每段开头结尾速度都应该是 0”这是最大的误解。如果真让每段都从 0 加速到目标速度再刹回 0机器每走一条线就停一次加工时间会翻好几倍表面还会留下明显的停顿痕迹。GRBL 要做的就是让相邻段之间的速度尽量“接上”上一段出去的速度能成为下一段进来的速度。这就引出了 block 里的几个核心字段entry_speed入口速度、exit_speed出口速度、nominal_speed名义目标速度、acceleration加速度、millimeters段长。用开车打比方你从 A 开到 B再拐到 C最傻的司机是每个路口都停稳再起步老司机会提前观察路口前收油、过弯后给油让车速尽量平滑。GRBL 的 planner 就是那个老司机反向规划和正向规划就是它的两套观察动作。1.2 Planner 缓冲区与“已入队未执行”的窗口GRBL 里有两个模块在同时跑planner 负责把 G 代码解析成运动段、规划速度然后写进一个环形缓冲区stepper 在定时器中断里从缓冲区头部取段按照规划好的速度曲线一步不差地发脉冲。这俩是异步的planner 写尾部stepper 读头部。关键点来了头部已经被 stepper 拿走的段无法修改但头部和尾部之间那些“已入队、还没执行”的段是可以重新规划的。GRBL 的前瞻窗口就是这个缓冲区的长度常见的是 16 到 32 个 block。Arduino UNO 内存小GRBL 1.1 默认好像是 16 个左右具体要看编译选项。有了这个窗口新段入队时planner 就可以把窗口里所有段的速度重新“铺一遍”让速度曲线在整个窗口内尽量连续。RG 里经常说的“前瞻太浅”指的就是这个窗口太小只能提前规划十几段遇到密集的小线段就力不从心。2. 反向规划先保证每段都刹得住2.1 核心公式入口速度上限怎么算反向规划要解决一个问题某一端的出口速度确定后这一端在段内最多只能以多快的速度冲进来不然距离不够刹不住。假设一段长度为 d入口速度是 v_entry出口速度是 v_exit减速度大小是 a那么运动学关系是v_exit² v_entry² - 2 * a * d反过来已知出口速度允许的最大入口速度就是v_entry_max sqrt(v_exit² 2 * a * d)GRBL 源码里有一个函数专门干这个事原型近似是这样的// 计算在给定距离内能从 target_speed 降低到的最大入口速度 // accel 传入负号表示减速过程 static float max_allowable_speed(float accel, float target_speed, float distance) { return sqrtf(target_speed * target_speed - 2.0f * accel * distance); }举个例子一段直线长 5mm加速度设置成 1000mm/s²算完这段的速度要刹到 0那它入口速度最多就是 sqrt(0 2 * 1000 * 5) 100mm/s。如果哪个上游段想以 150mm/s 的速度冲进这一段那完了5mm 距离内不可能刹停必须要往回调。注意GRBL 内部用的是 mm/min 和 mm/min²我这里为了直觉统一用 mm/s 说明。真要改源码的话留个心眼看单位。2.2 从队尾往队头逐段传播反向规划的方向很有意思从缓冲区尾部往头部扫。为什么必须从尾部开始因为整条路径的最终状态是确定的——要么是程序末尾停住要么是到达某个明确目标速度这个“最终出口速度”是唯一已知的硬约束。算法的骨架长这样exit_speed 0 // 最后一段的出口速度假设最终要停住 从缓冲区尾部向前遍历每个 block: 该 block 允许的最大入口速度 sqrt(exit_speed² 2 * 加速度 * 段长) 如果 block 原来规划的入口速度 这个上限: 把它压到这个上限 exit_speed 该 block 现在的入口速度 // 它同时是前一个段的出口速度每一段算出来的入口速度会成为前一段的出口速度继续往前传一层一层“刹车能力”就往前覆盖了。这就是反向规划最核心的传播机制从最后一段开始根据每一段的刹车距离把前面所有段的速度全部压住。看懂这段代码就明白一个道理GRBL 不是先规划一段再规划下一段它是先从尾部倒推出一堵“不能超过这速度”的墙再让前面的段在墙内活动。2.3 一个能说明问题的数值例子假设只有两段段 1 长 40mm段 2 长 5mm加速度都是 1000mm/s²最终机器停在段 2 末尾。先看段 2出口是 0长度 5mm允许的入口速度上限是 sqrt(0 2 * 1000 * 5) 100mm/s所以段 1 的出口速度最多只能到 100mm/s。再看段 1出口被压到 100mm/s 后长度 40mm允许的入口速度上限是 sqrt(100² 2 * 1000 * 40) ≈ sqrt(10000 80000) ≈ 300mm/s。如果段 1 起点确实是 0那它完全有能力在 40mm 内加速到 100mm/s 并稳稳交棒给段 2因为从 0 加速到 100mm/s 只需要 5mm 距离。这个例子告诉我们真正卡住整条路径的往往是尾部那些又短又要求刹停的段。段 2 只有 5mm导致前面所有段都只能以 100mm/s 收尾而不是名义上设置的 300mm/s。反向规划干的就是这件事它不让任何一段“飙车飙到刹车线前才急刹”。3. 正向规划在刹得住的前提下把速度重新顶上去3.1 反向规划会带来什么副作用反向规划保证安全但它有个毛病它只考虑“能不能刹住”完全不管“能不能加速到”。实际加工时路径头部的速度往往很低甚至从 0 开始。如果只做反向规划就会出现一种尴尬情况后面段很长、刹车余量很大反向算出来的速度上限很高但前一段实际从 0 起步、段长又短根本跑不到那个速度。换个说法反向规划给了每段一个“最高限速”但不是每个段都能真正碰顶。前面段跑不到会导致后面段入口速度偏低又白白浪费了后面段的加速能力。这时候就需要正向规划出马。正向规划的方向和反向正好相反从缓冲区头部往尾部扫目标是把每一段实际能达到的出口速度尽量往上抬让速度曲线在“不超限”的前提下尽量贴近理想梯形曲线。3.2 正向扫描的触发条件与公式正向规划不是无脑抬速度它要满足两个条件当前这一段有能力跑得更快也就是它的实际入口速度比出口速度低是一个“加速段”下一段的入口速度还能接受更大的值抬上去不会撞翻反向规划立下的那堵墙。如果上一段本来就在减速入口 出口那就没有“继续加速”的余力正向扫描会跳过它。核心公式是把运动学关系反过来用v_exit_max sqrt(v_entry² 2 * a * d)意思是在段长 d 内如果入口速度是 v_entry最多能加速到多少出口速度。GRBL 源码里正向规划的伪代码大致长这样previous 缓冲区头部第一个 block current 下一个 block while current 还没到尾部: if previous 是加速段 且 current 的入口速度比 previous 出口高: 最大可提升出口 sqrt(previous.入口² 2 * previous.加速度 * previous.段长) previous.出口 min(current.入口, 最大可提升出口) previous current current 下一个 block注意它每次都只调整“前一段的出口速度”而这个出口速度恰好就是“后一段的入口速度”所以信息是一格一格往上传的。3.3 正向和反向不是一次性就能算完有人会问只正扫一遍够吗反向压了一遍正向抬了一遍中间某些段会不会出现“正向抬过头、反向又要压回来”的情况理论上可能但在 GRBL 这种嵌入式固件里缓冲区只有十几段而且扫描本身很快实际效果可以接受。源码里通常的做法是先反向、再正向各扫一遍然后直接进入每个 block 的细部速度曲线计算。先反向再正向的顺序很重要。反向先建立起“允许速度墙”正向在墙内做提升这样永远不会把某段速度抬到刹不住的程度。如果颠倒顺序正向可能先盲目抬速反向再往回压虽然迭代几轮也能收敛但没有必要反而增加计算量。4. 完整的重算流程与 stepper 协同4.1 新段入队时发生了什么GRBL 解析完一条 G 代码运动指令后会走一条固定的链路先算段长、速度、加速度把新 block 写进缓冲区尾部然后触发一次重算。重算不是把缓冲区从头到尾全部推倒重来而是在窗口内做一轮反向和正向扫描再把每个 block 的详细加减速曲线刷新一遍。grbl 源码里对应的是planner_recalculate()它干的事情可以概括为三步从尾部倒推对每个 block 计算“入口速度上限”压掉不合理的速度从头部正推把能加速的段尽量顶上去根据最终的入口/出口速度算出每段的加速位置、减速位置等细节供 stepper 使用。4.2 拐角速度与名义速度都参与约束除了段长和加速度GRBL 里还有一个重要约束拐角速度。两段路径方向变化越大过弯时的离心误差越大所以拐角处必须限制速度。GRBL 用相邻两段的单位向量来计算一个 cos 值再结合允许的“拐角偏差”junction deviation算出最大拐弯速度。公式大致是junction_speed sqrt(junction_acceleration * junction_deviation * (1 - cosθ) / (1 cosθ))这里 θ 是两段的夹角。方向几乎不变时cosθ 接近 1拐角速度非常大基本不限速直角转弯时 cosθ 0拐角速度会显著变小掉头时 cosθ 接近 -1速度会被压到很低。反向规划和正向规划算出来的入口/出口速度最后还要和这个拐角速度取最小值相当于在“刹车约束”之外又加了一道“转弯约束”。这就是为什么有些用户觉得“我加速度设得很大了为什么直角转弯还是明显减速”——因为真正限制你的不一定是段内加减速而是转弯处的几何约束。4.3 Stepper 是怎么消费这些数据的planner 算完之后每个 block 里会留下几个关键值accelerate_until加速到哪、decelerate_after从哪开始减速、exit_speed。stepper 在定时器中断里就是靠这几个值实时决定下一步的脉冲频率。这里有个容易担心的点planner 随时可能修改还没执行的 blockstepper 正在用怎么办GRBL 的做法是stepper 每次“吃”一个 block 时直接读取那个 block 的最新字段。因为整段读写发生在单线程的中断逻辑里只要注意临界区就不会读到半更新状态。实际运行中即使 planner 在 stepper 执行前把速度曲线重算了机器也只是平滑地换了条速度曲线不会突然跳变。5. 调参实战让前瞻真正“看得远”5.1 加速度与速度上限怎么设GRBL 里最常动的参数是 $110/$111X/Y 最大速度单位 mm/min和 $120/$121X/Y 加速度单位 mm/min²。很多人只知道往大了调结果不是丢步就是机器“突突”响。一个经验公式如果你希望机器在长度 L 的段内能从 0 加速到速度 v那么加速度至少要满足a ≥ v² / (2L)举个例子想在 10mm 内加速到 100mm/s加速度至少需要 10000 / 20 500mm/s²。如果你的路径里大量是 2mm 短线段又想跑到 100mm/s那需要的加速度就是 10000 / 4 2500mm/s²很多业余机器根本扛不住电机一转就丢步。这就是为什么“速度调上去反而更慢”的根本原因盲目拉高速度短线段跑不到提前规划全在减速机器反而在原地纠结。5.2 小线段多的路径怎么处理GRBL 的前瞻窗口是有限的如果 G 代码里全是 0.1mm 级别的微线段一个窗口只能覆盖几毫米路径前瞻基本等于没有。我在实际使用中见过很多 CAM 软件默认把圆弧离散成大量短直线这是导致机器一顿一顿的头号原因。解决办法有三个尽量用 G2/G3 圆弧指令GRBL 1.1 支持圆弧实时插补一个圆弧段只占一个 block能把一大段曲线塞进前瞻窗口在后处理阶段合并共线线段把角度变化极小的相邻直线合并成一条长直线如果必须用短线段把进给速度降下来让每段至少有几毫秒的加减速余量否则神仙算法也救不了。5.3 常见问题速查表现象可能原因排查方向每段都明显停顿前瞻窗口太小 / 加速度过低 / 段间夹角大看 $120/$121 加速度是否合理检查 G 代码是否被离散成过多短线段直角转弯急速减速拐角速度被 junction deviation 限制正常现象想改善需要提高加速度或降低进给小线段路径像“爬行”每段都来不及加速就减速改用圆弧指令、合并共线段、降低进给加工中突然掉速或停住串口缓冲区欠载上位机发送跟不上提高波特率、检查 USB 线、减少上位机卡顿电机丢步、噪音大加速度设置过高降低 $120/$121或增大驱动电流5.4 改源码后的调试习惯如果你真的动了 planner.c 里的算法我建议准备一个能实时打印的调试手段。GRBL 1.1 源码里可以用串口把每个 block 的入口速度、出口速度、名义速度打出来跑一段简单路径就能看到反向、正向这两个 pass 的“脚印”。比如打一段五段连续的折线你会看到反向扫描后尾部几段的入口速度被明显压低正向扫描后头部能加速的段又被抬回来。把这个过程可视化之后再回头读planner_recalculate()的代码你会觉得整个逻辑一目了然。别只靠眼睛看电机转得快不快打印数据才是判断算法行为最靠谱的方式。我个人在实际操作中的体会是GRBL 这几十 KB 代码里最值钱的就是 planner.c。你不需要改多少代码只要理解了反向和正向这两个 pass很多调参和丢步问题都能从原理上想明白。先刹得住再跑得快这两件事正好对应反向规划和正向规划。每次改完加速度参数我的固定动作是先跑一段“连续直线 直角转弯 圆弧”的测试代码观察有没有异常顿挫再用串口确认 block 状态。等你能在数据里看出反向压低、正向抬升的痕迹GRBL 在你眼里就不再是黑盒了。