news 2026/9/25 6:39:00

GRBL速度前瞻算法解析:反向规划与正向规划让雕刻机告别顿挫

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 正向扫描的触发条件与公式

正向规划不是无脑抬速度,它要满足两个条件:

  1. 当前这一段有能力跑得更快,也就是它的实际入口速度比出口速度低,是一个“加速段”;
  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(),它干的事情可以概括为三步:

  1. 从尾部倒推,对每个 block 计算“入口速度上限”,压掉不合理的速度;
  2. 从头部正推,把能加速的段尽量顶上去;
  3. 根据最终的入口/出口速度,算出每段的加速位置、减速位置等细节,供 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 随时可能修改还没执行的 block,stepper 正在用怎么办?GRBL 的做法是,stepper 每次“吃”一个 block 时,直接读取那个 block 的最新字段。因为整段读写发生在单线程的中断逻辑里,只要注意临界区,就不会读到半更新状态。实际运行中,即使 planner 在 stepper 执行前把速度曲线重算了,机器也只是平滑地换了条速度曲线,不会突然跳变。

5. 调参实战:让前瞻真正“看得远”

5.1 加速度与速度上限怎么设

GRBL 里最常动的参数是 $110/$111(X/Y 最大速度,单位 mm/min)和 $120/$121(X/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 在你眼里就不再是黑盒了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 6:38:04

MFC界面库Xtreme ToolkitPro源码包:编译接入与避坑实践

简介:一份包含 Xtreme ToolkitPro v17.2.0 完整源代码的压缩包,主要面向希望深入理解 MFC 界面扩展控件实现、并能进行二次定制的 C 开发者。包内共 12111 个文件,以 h/cpp 源码文件为核心,辅以 rc 资源脚本、xaml 界面描述、png/…

作者头像 李华
网站建设 2026/9/25 6:37:00

OpenART Plus与AprilTag实战:智能车视觉定位与AR引导线实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 6:36:59

Android system.img解包与权限管理深度解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 6:31:50

WebCrack实战:Web后台弱口令批量检测与万能密码判定

简介:WebCrack 是一款基于 Python 的 Web 后台弱口令与万能密码批量检测工具,面向安全测试人员、渗透学习者及 Python 脚本爱好者。它支持批量导入后台地址并自动检测,内置多重判断机制以减少误报,同时提供随机 UA、随机 X-Forwar…

作者头像 李华
网站建设 2026/9/25 6:30:55

嵌入式C语言手搓UTF-8编解码与工具函数实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 6:30:55

从幺蓝破解官网案例拆解软件分发与版本管理技术实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华