grbl 1.1f源码解析:从G-code解析到Arduino烧录实践

发布时间:2026/9/11 19:09:43
grbl 1.1f源码解析:从G-code解析到Arduino烧录实践 简介grbl 1.1f是面向Arduino平台的开源CNC运动控制固件这份20170801版源码压缩包适合嵌入式开发者、CNC DIY爱好者及学习运动控制算法的人群。包内共69个文件以头文件和C源文件为主辅以Arduino工程文件、构建脚本、示例文档及配置文件完整覆盖G-code解析、加减速规划、串口通信、多轴插补等核心模块。压缩包仅297KB结构紧凑便于对照源码逐行学习。已有813人学习下载。通过阅读这套源码读者可以掌握grbl的实时控制架构理解从G-code指令到步进电机脉冲的完整链路并可在Arduino IDE中直接编译烧录为进一步移植或定制运动控制固件打下坚实基础。1. grbl 1.1f 不是库而是一块能跑实时插补的嵌入式内核很多人在 GitHub 上搜到 grbl 源码第一反应是它不过是一段能解析 G-code 的 C 代码。实际上 grbl 1.1f2017 年 8 月 1 日发布是一个在 8 位 AVR 上硬跑出来的实时运动控制内核串口收命令、G-code 解析、前瞻加减速、步进脉冲分配都在一个 main 循环里完成没有操作系统也不需要实时补丁。它适合三类人想把桌面级 CNC 的成本压到百元以内、需要在 Arduino 上亲手验证运动控制算法、或者想在移植到 STM32 之前先弄清规划器与插补器的边界。理解了 1.1f 的调度顺序后面读 grbl-Mega 或 grblHAL 都会轻松很多。2. 从 grbl-1.1f.20170801 的目录结构看固件模块边界grbl 的源码不是一个大文件而是按“协议、解析、规划、执行、硬件、配置”拆开的十几个文件。解压 grbl-1.1f.20170801.zip 之后根目录有 Makefile、README.md、COPYING、doc/ 和 build/真正的固件代码在 grbl/ 子目录里。读懂这个边界才能知道烧录时该把哪个目录交给 Arduino IDE二次开发时改哪个文件。2.1 先看 Makefile 与 build 目录确认构建粒度根目录的 Makefile 是给 AVR-GCC 工具链用的不是 Arduino IDE 必需。README.md 里写的是编译入口而 build/ 目录存放编译中间产物真正需要拿进 IDE 的只有 grbl/ 这个子目录。典型文件对应关系见下表文件职责二次开发时最常改的点grbl/config.h编译期功能开关禁用限位、改默认进给单位grbl/defaults.h默认参数初始值$ 参数表复位后初始值grbl/settings.h参数读写与校验新增自定义 $ 参数grbl/stepper.c步进脉冲生成与插补改脉冲占空比、加细分驱动grbl/planner.c前瞻规划与加减速调加速度计算权重grbl/gcode.cG 代码解析扩展 M 指令在 Linux 下用一条命令可以验证工具链是否完整cd grbl-1.1f.20170801 make clean make 21 | tail -20这条命令先清理 build 目录中的旧对象再调用 Makefile 编译出grbl.hex。第 2 步的tail -20只是过滤输出方便看最下面的错误和avr-objcopy生成的 hex 文件名。如果你的环境没有装 avr-gcc会在这里直接报avr-gcc: command not found那就说明 Makefile 这条路走不通改用 Arduino IDE 更省事。对于 1.1f 来说Makefile 里默认的MCU atmega328p和CPU 16MHz就是 Uno 的配置两块板共用一个板载晶振。2.2 grbl.h、config.h、defaults.h 三层的配置关系grbl.h 是总入口它做两件事包含核心头文件、定义编译期版本号。config.h 决定“这个固件编译出来有哪些功能”defaults.h 决定“第一次上电时 $ 参数表里存什么”。在实际代码里config.h 的宏大多用条件编译包着比如// grbl/config.h (1.1f) #define GRBL_VERSION 1.1f #define GRBL_VERSION_BUILD 20170801 // 限制与探针 #define USE_LIMIT_SWITCHES #define USE_PROBE_PIN这里USE_LIMIT_SWITCHES打开后limits.c 才会被编入固件把它注释掉限位端口相关的代码直接被预处理器移除。USE_PROBE_PIN是探测功能的开关。注意 1.1f 的 config.h 默认开了很多功能但 Arduino Uno 只有 32KB Flash如果后续加了自己的代码导致体积超限第一件要做的就是重新评估这些开关而不是上来就换 Mega。defaults.h 里的值要配合 settings.c 的校验逻辑看。例如默认的加速度如果写大了planner 在规划时会做限速但不会报错而写成一个非法值settings.c 会拒绝写入并返回Error:7EEPROM 写入失败或直接回退到默认值。我一般改默认参数时会把DEFAULT_X_ACCELERATION这类宏改成目标值再执行一次$RST*让参数表重新加载。提示$RST*是全参数复位只复位 EEPROM 里的设置项不会动固件本身。改完 defaults.h 需要重新烧录固件再执行这步。2.3 主循环与串口协议protocol_main_loop 怎么调度grbl 的实时性主要体现在 protocol.c 的protocol_main_loop()。这个循环不是简单的 while(1)而是按优先级处理先执行规划好的插补段再读串口最后处理报警。简化的逻辑如下// grbl/protocol.c (1.1f 对应逻辑) void protocol_main_loop(void) { for (;;) { if (plan_check_full_buffer()) // 规划缓冲快满时先执行 protocol_execute_realtime(); if (serial_get_rx_buffer_available() 0) protocol_process_line(); // 取一行 G-code 解析 else protocol_execute_realtime(); // 执行限位、急停等实时指令 if (system_state STATE_ALARM) system_handle_alarm(0); } }这段代码里plan_check_full_buffer()是背压控制的核心当规划缓冲区还剩不到两段时主循环强制去跑插补避免运动指令堆积serial_get_rx_buffer_available()检查串口接收缓冲区还有没有数据没有数据就继续执行插补。这里最值得注意的设计是protocol_execute_realtime()它不是被 main 循环单独调度的而是被嵌入到执行过程中所以急停和限位信号能在一条 G-code 执行到一半时被快速响应。这解释了为什么 grbl 的主循环看起来“变快”了——它不是靠中断抢占而是靠调度点密集。3. G-code 解析与 planner 联动grbl 1.1f 的插补路径从串口收到G01 X10 Y0 F300到步进电机转动中间经历两次缓冲gcode.c 把文本解析成parser_block_tplanner.c 把运动意图做成plan_block_tstepper.c 再按 1.1f 的 DDA 算法把这段轨迹切分成脉冲段。任何做二次开发的人都绕不过这条路径。3.1 解析器 gc_execute_line 如何把 G 代码变成 motion_blockgc_execute_line()一次只处理一行 G-code。它先把字符抽到parser_block_t结构体里再交给gc_execute_line()内部的状态机执行。1.1f 的解析器有一个特点不支持行内注释以外的预读所以G90 G0 X10这种多指令行是被顺序执行的而不是先解析后执行。以G01 X10 Y20 F300为例关键流程是// 伪代码完整逻辑在 gcode.c 的 gc_execute_line() uint8_t gc_execute_line(char *line, parser_block_t *gc_block) { mortal gc_parser_flags(block, line); // 判断 G 的模态分组 gc_tool gc_get_axis_value(...); // 提取 X、Y 值并检查软限位 if (mortal ! GC_INITIAL) { // 非初始状态才做运动 plan_buffer_line(...); // 交给规划器 } }gc_parser_flags()处理 G 代码的模态组比如 G00/G01/G02/G03 同属模态组执行过其中一个后下一次再写X10仍然沿用该模态。gc_get_axis_value()不只是读取坐标还负责把当前位置转成机床坐标并做软限位判断。只有当这条指令被判定为有效运动指令时才会调用plan_buffer_line()。这一步的取舍很关键M03 这种辅助指令在 1.1f 里不会经过 planner它被直接解析成gc_block-spindle标志然后交给spindle_control.c输出。如果自己扩展 G 代码最容易在这里卡住新指令如果要改变坐标或触发运动必须走plan_buffer_line()如果只是控制 IO直接在gc_execute_line()里处理并返回状态即可。1.1f 对这两类指令的区分非常严格混着写容易导致 planner 缓冲区里的 motion_block 状态不一致。3.2 梯形加减速在 planner 模块里的计算与限速planner.c 不产生脉冲它只负责算“下一段能不能按这个速度跑”。grbl 1.1f 的规划器维护一个环形缓冲每次接收新运动时会对末段重新计算进给速度防止在拐角处由于加速度不足而丢步。核心公式由planner_recalculate()触发关键参数来自settings.c读出的加速度和最大速度。// planner.c 中速率更新的核心逻辑已简化 static uint8_t planner_recalculate(plan_block_t *block) { float entry_speed block-entry_speed_max; float exit_speed block-maximum_speed; // 计算当前 block 的加减速约束 block-entry_speed_max limit_value_by_acceleration( entry_speed, exit_speed, block-acceleration); }entry_speed_max是进入该段时允许的最大速度maximum_speed是该段设定的进给速度limit_value_by_acceleration()根据段长和加速度算出实际能到达的速度。如果段长太短可能整段都达不到 F 设定值这时的最高速度被 block 的acceleration限制住。所以 F 值不是越大越好进给速度最终由加速度和段长共同决定。1.1f 默认的加速度算法是梯形加减速没有 S 曲线。体现在参数表上就是$110X 最大速率、$120X 加速度。这两个值要匹配步进驱动和机械结构参数单位修改影响$110 / $111 / $112mm/min对应轴最大进给超过后运动被截断$120 / $121 / $122mm/sec^2加速度越低启停越柔和越不会丢步$130 / $131 / $132mm软限位行程设成 0 表示关闭$50/1限位开关低有效还是高有效如果你从老版本 grbl 0.9 升级到 1.1f要注意这里的单位变化1.1f 的加速度单位是mm/sec^20.9 里是mm/min^2。直接把旧参数写进$120数值会造成实际加速度偏差两个数量级表现就是启动瞬间啸叫或者原地丢步。3.3 修改速度、加速度参数时先动哪里最稳妥的顺序是先用$$查看当前参数表再动$120和$110。先降加速度后降最大进给避免电机直接闷住。例如 X 轴皮带传动的小机架常见工作参数是$1103000、$120300如果是牙距 2mm 的丝杆$120150就偏迟疑可以提高到 300 再试。改完后用G0 X50 F600测试回零和重复定位如果出现丢步先减小的是$120而不是$110。注意$指令在 grbl 1.1f 里属于实时命令不需要写在 G-code 文件里通过任意串口终端发送即可。发送格式不要带空格$120300直接回车固件返回ok才表示写入成功如果返回error多半是数值超出该参数允许范围。4. 烧录 grbl 1.1f 源码到 Arduino 并验证参数的完整操作grbl 1.1f 的烧录路径很典型一种是把 grbl 文件夹当作 Arduino 库从菜单里加载另一种是直接用 Makefile 交叉编译后用 avrdude 烧。两种方式最终都生成 hex但 1.1f 源码在 Arduino IDE 里必须用grbl/grbl.ino打开而不是把整个压缩包目录拖进 sketch。4.1 Arduino IDE 与 make 两种编译路径Arduino IDE1.8.x 或 2.x和 Makefile 虽然都编译为 AVR hex但处理方式完全不同IDE 会调用自带的标准库头文件路径而 Makefile 需要你自己装好 avr-gcc 和 avrdude。对于只在 grbl 上做小改动IDE 更直接对于要签入 CI 或做版本批量编译Makefile 更可靠。先看 IDE 的常用做法解压 zip确认grbl/grbl.ino存在。打开grbl/grbl.ino。在“工具 - 开发板”里选择 Arduino/Genuino Uno。点击“上传”。对应的 Makefile 路径是sudo apt install gcc-avr avr-libc avrdude cd grbl-1.1f.20170801 make avrdude -c arduino -p m328p -P /dev/ttyUSB0 -b 115200 -U flash:w:build/grbl.hex其中-c arduino指定编程器协议-p m328p对应 ATmega328P-b 115200既是 avrdude 的烧录波特率也是 grbl 固件的默认串口波特率。如果烧录时报stk500_getsync()说明 Arduino 板载 bootloader 没有正常进入烧录模式常见原因是串口号不对或者板子上还接着 CNC 驱动板导致 DTR 信号被拉低。如果板子是 Arduino Mega 2560-p要改成m2560并且把主控类型对应到 ATmega2560。grbl 1.1f 对 Mega 的支持默认没有打开需要在 config.h 里先检查#ifdef分支否则编译出的固件无法启用第四轴。4.2 串口接线与 $ 命令验证固件烧好后用 USB-TTL 连接 Arduino 的 RX/TX 和 GND。grbl 1.1f 默认串口参数是 115200 8N1。启动后给一个\r\n固件会返回Grbl 1.1f [$ for help]或者直接显示帮助菜单。发送$$查看参数表$1103000.000 (x max rate, mm/min) $120300.000 (x accel, mm/sec^2)如果什么都没有返回优先检查串口 TX/RX 是否接反。与 Arduino Uno 通信时USB-TTL 的 TXD 接板子的 RX0 脚RXD 接板子的 TX1 脚。很多板子上这两个引脚同时连了板载 LED观察 D1 脚是否闪烁可以判断数据是否进来。我用 Python 验证串口时pyserial 脚本里需要显式关闭rtscts和dsrdtr否则某些 USB-TTL 会自动拉低 DTR导致 Arduino 复位后 grbl 还没启动完就收到了字符import serial s serial.Serial(/dev/ttyUSB0, 115200, timeout2) s.rtscts False s.dsrdtr False s.write(b\r\n) print(s.read(64)) s.write(b$$\r\n) print(s.read(128))这段代码先发一个空行让 grbl 打印欢迎信息再发$$读取参数表。timeout2用来避免 read 永久阻塞。如果返回内容里出现ALARM:1说明限位或硬限位状态异常先发送$X强制解锁再继续查限位接线。现象常见原因处理返回乱码波特率不匹配确认串口工具设为 115200 8N1无任何输出TX/RX 接反交换 USB-TTL 的 TXD 和 RXD上电立刻 ALARM限位开关接常开把开关常闭端接入或改 $5收到error:5G-code 行写入超过缓冲区检查是否发送了超过 128 字节的长行4.3 常见故障bootloader 与限位信号grbl 1.1f 对限位信号要求物理上是常闭接法NC配置宏上默认只对两端限位做了软件处理。如果接的是常开NO开关一上电就会进入 ALARM 状态。此时不要慌先发送$X强制解锁再检查$5的值。$5是限位开关逻辑状态值为 0 时低电平触发值为 1 时高电平触发。换驱动器之后经常遇到要翻转这个值的情况。另一个高频坑是 EEPROM 里残留了旧版参数烧完 1.1f 后第一次连接发送$RST*可以清除所有设置并恢复为默认值但不影响已经编译进固件的 config.h 开关。如果发$X后电机不动先开$10状态报告并观察反馈检查 stepper.c 里STEP_PULSE_DELAY是否和驱动板的最小脉宽匹配。1.1f 里这个值默认是 0但某些老版 A4988 模块需要加到 5 才能稳定出脉冲。5. 二次开发从 1.1f 源码改出带固定电平主轴控制的最小改动grbl 1.1f 的主轴控制接口在 spindle_control.c 里。默认配置里SPINDLE_PWM_MIN_VALUE和SPINDLE_PWM_MAX_VALUE把 PWM 输出限制在一个安全区间配合M03 S1000这类指令可以直接调速。对于没有调速主轴、只有继电器开关的机器最小改动不是重写模块而是把 PWM 改成开关量。5.1 定位 spindle 控制函数spindle_control.c 中的关键函数是spindle_run()它根据 parser 传过来的spindle_speed计算 PWM 占空比。如果要让M03只输出固定电平最简单的是忽略 S 值把占空比写死// 简化版把主轴 PWM 固定为高电平 void spindle_run(uint8_t state, uint8_t pwm_off) { if (state SPINDLE_ENABLE) { SPINDLE_PWM_DDR | (1 SPINDLE_PWM_BIT); // 不再读取 pwm_value直接拉高 SPINDLE_PWM_PORT | (1 SPINDLE_PWM_BIT); } else { SPINDLE_PWM_PORT ~(1 SPINDLE_PWM_BIT); } }注意这里没有处理 pwm_off意味着M05会直接停主轴但不会等待主轴真正停转。如果沿用这个改法需要自己把pwm_off的延时逻辑加回去否则排屑器或丝杆主轴可能因为惯性继续转动产生碰撞风险。5.2 编译验证与边界改完后重新编译用avrdude烧录再发一条M03 S1000。此时无论 S 值大小PWM 引脚应该输出恒定电平。若要退回 PWM 调速把这段代码恢复原逻辑即可。这个例子说明 1.1f 的硬件抽象层并不厚改一个输出引脚和一处占空比逻辑就能覆盖大多数小型主轴驱动板真正的复杂度在 planner 对主轴状态的时序保证上M03在 grbl 1.1f 里默认不等待主轴启动完成所以大惯量主轴需要在上位机侧增加停顿而不是修改固件。本文还有配套的精品资源点击获取