☰
STM32F103脱机GRBL雕刻机控制器:从移植到屏幕面板的完整方案
2026/10/14 17:40:52 网站建设 项目流程

简介:一套基于STM32F103RC系列打造的GRBL脱机控制代码,配套迪文二代4.3英寸DGUS屏显方案,面向DIY雕刻机、写字机、激光机、CNC等运动控制设备的开发与调试人群。方案支持上位机通过串口控制电机运动,也可利用SD卡完成全脱机运行,相关轴参数、速度、密码等硬件配置均集成在屏幕上,默认密码2356,并可通过按键一键切换设备型号,减少重复烧录带来的调试成本。压缩包共601个文件,大小约18.75MB,核心代码包括C/H源码及编译生成的O/D中间文件、AXF/HEX固件输出;界面侧则有90个BMP位图、52个TFT屏显资源,以及HMI、CFG、BIN等配置与变量文件,整体覆盖了从电机驱动、串口协议到屏幕交互的完整工程链,适合对照学习GRBL在STM32上的移植思路与脱机控制流程。目前已有3221人学习/浏览;借助工程可直接修改引脚定义以适应自制控制板,也能基于迪文屏界面二次开发,调整人机交互与设备参数;对具备一定嵌入式基础、想快速搭建脱机运动控制平台的玩家来说,是兼具参考价值与实用性的开源式DIY资料。

1. 脱机控制:GRBL 跑在 STM32F103 上,才是雕刻机的完全体

很多人以为 GRBL 就是 Arduino UNO 加一个扩展板的事,但真正上过手做产品的人会告诉你,GRBL 加屏幕、加编码器、加 SD 卡,做成一台不依赖电脑的脱机控制器,才是这个固件最实用的形态。STM32F103 跑 GRBL 带屏幕控制,解决的是车间或桌面场景里最硬的一个诉求:机器旁边没有电脑,或者你不想每次对刀、换文件都回电脑敲命令。主控选 STM32F103,是因为它的定时器资源、Flash 容量和价格都适合承接 GRBL 这个实时性要求不低的固件,而且现成的移植方案很多,不用自己从头实现梯形加减速算法。这篇文章想讲的,不是把 GRBL 编译一遍烧进去就完事,而是从脉冲时序、菜单交互、SD 卡喂码、异常排查到进阶验证,把一套能稳定跑的脱机屏幕控制方案给你拆开。适合谁看?DIY 雕刻机、激光雕刻、微型 CNC 的开发者,或者想把手上的 GRBL 机器从「插着 USB 线才能动」升级成「独立面板操作」的人。文章里不会有某个别人项目的源码包,但会给出我习惯用的移植步骤和参数,照着做,你也能拉出一台能脱机跑的机器。

2. 移植 GRBL 到 STM32F103:先搞清脉冲、速度与定时器这三个骨架

2.1 GRBL 的“柔加速度”算法是什么:梯形规划为什么够用

GRBL 的速度规划,说穿了是梯形加减速,不是 S 曲线。你也许在一些高级固件里见过 S 曲线规划,但 GRBL 那种查表式的梯形规划,在 STM32F103 这种主频 72MHz 的芯片上反而是优势:计算量小,中断抖动可控,脉冲输出稳定。它的核心思想是,根据当前设定的加速度和最大速度,算出加速段距离、匀速段距离和减速段距离,然后把每个脉冲的时间间隔以查表或迭代的方式拉出来。有了这个概念,你就明白为什么「调速度只调 $110 最大速度」是不够的——加减速时间没变,速度拉高只是让梯形变得更陡,电机更容易丢步。

我把 GRBL 源码里的规划逻辑简化成一句话:每一步执行时,系统都在计算「这一拍的脉冲周期该是多少」,然后把这个数值写进定时器的自动重载寄存器。这里的关键点是,STM32F103 的定时器是 16 位的,意味着自动重载值的范围是 1~65535。如果你的脉冲周期超过了这个范围,就会溢出。这在低速场景下特别容易翻车,因为低速意味着脉冲周期长,周期值大,可能超过 65535。我见过有人把脉冲周期算错了,低速时步进电机直接不动,高速时又乱冲,其实就是定时器溢出。解决方式有两种:一种是把定时器预分频加大,降低计数频率;另一种是用 32 位的定时器,比如 TIM2 或 TIM5。F103 的 TIM2 和 TIM5 是 32 位的,所以做步进脉冲定时器时,优先选它们。GRBL 原版在 AVR 上用的是 16 位定时器,因为 AVR 的时钟频率和 GRBL 的设计约束刚好匹配,但移植到 F103 时,我会直接把脉冲定时器映射到 TIM2 上。

还有一个必须注意的点:GRBL 的方向引脚跳变和 STEP 脉冲之间是有时间要求的。绝大多数步进驱动器的数据手册里都写着最小 DIR 建立时间,常见的是 5 微秒左右。GRBL 固件本身会在方向引脚改变后插入一个设定的延迟,移植时如果你改了引脚映射,这个延迟参数也要跟着调,否则会偶发「某个方向跑几步就卡住」的玄学问题。说白了,这不是 GRBL 的问题,是你的硬件时序不满足驱动器要求。

2.2 用定时器生成步进脉冲:预分频、自动重载与方向电平时序

在 STM32F103 上做步进脉冲,我一般不用 PWM 模式,而是用定时器的更新中断里手动翻转 STEP 引脚。原因很简单:GRBL 的速度规划需要精确控制每个脉冲的时间间隔,PWM 模式虽然能产生连续脉冲,但频率调节需要不停改预分频和重载值,而且方向变化时还要关闭再重启 PWM,时序上更容易出问题。用更新中断,每个脉冲触发一次中断,在中断服务函数里判断当前这一步的周期,然后写入定时器的自动重载寄存器,直观而且可控性高。

// 定时器更新中断服务函数:每进一次,产生一步脉冲 void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 翻转 STEP 引脚,产生一个上升沿或下降沿 GPIO_WriteBit(STEP_PORT, STEP_PIN, (BitAction)(1 - GPIO_ReadOutputDataBit(STEP_PORT, STEP_PIN))); // 读取 GRBL 规划器算出的下一步脉冲周期(单位:定时器计数个数) uint32_t next_period = st_get_step_period(); // 把周期写入自动重载寄存器,决定下一个脉冲什么时候来 TIM_SetAutoreload(TIM2, next_period - 1); // 方向引脚在脉冲之前已经由 GRBL 的 stepper 逻辑设好, // 这里只需要确保 DIR 信号稳定时间足够 } }

这个中断服务函数的逻辑是整个脉冲输出的心脏。每次进中断,STEP 引脚翻转一次,产生一个脉冲沿,然后从 GRBL 的步进状态机里取下一个脉冲周期,写进自动重载寄存器。注意我在自动重载值里减了 1,因为定时器的计数是从 0 到 ARR 值,总共产生 ARR + 1 个计数周期。如果没有这个减 1,实际脉冲频率会比设定值偏低一点点,短时间无所谓,但跑一个长文件,位移误差会累积到肉眼可见的程度。

// 初始化 TIM2 为 1MHz 计数频率,用于步进脉冲输出 void stepper_timer_init(void) { TIM_TimeBaseInitTypeDef TIM_InitStruct; // 系统时钟 72MHz,预分频 71 => 计数频率 1MHz => 每个计数周期 1us TIM_InitStruct.TIM_Prescaler = 71; // 自动重载初始值,后续每个脉冲周期都会重设 TIM_InitStruct.TIM_Period = 1000; TIM_InitStruct.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_InitStruct); // 开启更新中断,使能定时器 TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); TIM_Cmd(TIM2, ENABLE); }

这里的预分频参数决定了定时器的计数分辨率。我选 1MHz 计数频率,也就是每个计数单位 1 微秒。GRBL 的脉冲频率范围通常在 500Hz 到 50kHz 之间,对应的周期是 2 微秒到 2000 微秒,在 32 位定时器下完全放得下。如果你用的是激光雕刻,脉冲频率要求更高,可以把计数频率提高到 2MHz,但要注意中断频率也会翻倍。还有一个容易忽略的点:预分频调到 72MHz 分频之后,务必确认定时器时钟源是否正确。F103 的 APB1 定时器时钟在系统时钟 72MHz 时是 72MHz,你直接按这个算就行,但如果改了时钟树配置,这条就不成立了。

方向电平时序方面,我习惯在 GRBL 源码的 stepper 模块里找到方向设置函数,在它执行后加一个短延时:

// 方向引脚切换后,等待 DIR 建立时间 void stepper_set_direction(uint8_t dir_mask) { // 根据 dir_mask 设置方向引脚电平 // ... 省略具体引脚操作 ... // 插入 5us 延时,满足大多数步进驱动器的 DIR 建立时间要求 volatile uint32_t delay = 5; while (delay > 0) { delay--; } }

这段代码的设计思路是「在 GRBL 逻辑和硬件电平之间加一道保险」。不同驱动器的 DIR 建立时间差异很大,A4988 和 DRV8825 的要求相对宽松,但一些高性能驱动器,比如 TB6600 在高速模式下,对时序的要求更严格。我一般会把延时参数留成一个宏,方便在不同驱动板之间切换时调整。

2.3 移植前三件套:CPU 时钟、EEPROM 模拟与串口重定向

移植 GRBL 到 F103,有三件事不做,后面全是坑:时钟配置、EEPROM 模拟和串口底层。GRBL 原版是给 AVR 写的,它内部会直接调用一些 AVR 的库函数,比如 eeprom_read_byte 和 eeprom_write_byte,这些在 STM32 上根本不存在,必须用 F103 内置的 Flash 来模拟 EEPROM。另外,GRBL 的串口接收中断逻辑也是为 AVR 的 UART 寄存器设计的,你需要把它重映射到 STM32 的 USART 上。

时钟配置的要点是把系统时钟稳定在 72MHz。GRBL 的定时器分频系数、串口波特率都和系统时钟直接相关。如果你用 STM32CubeMX 初始化时钟,记得把 USB 预分频器也检查一遍,虽然 GRBL 不用 USB,但时钟树配置不当会导致 USART 波特率偏差,进而让上位机连接不稳定。

// 模拟 EEPROM 读写:使用 STM32F103 内部 Flash 的一个扇区 uint8_t eeprom_read_byte(uint16_t addr) { // 读取内部 Flash 指定地址的数据 return *(volatile uint8_t *)(FLASH_EEPROM_START_ADDR + addr); } void eeprom_write_byte(uint16_t addr, uint8_t value) { // 先擦除整个扇区,再写入数据 // 注意:Flash 写入前必须擦除,且擦除以扇区为单位 FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); FLASH_Unlock(); FLASH_ErasePage(FLASH_EEPROM_START_ADDR); FLASH_ProgramByte(FLASH_EEPROM_START_ADDR + addr, value); FLASH_Lock(); }

这段代码展示了移植时最核心的适配层:把 AVR 的 EEPROM 接口替换成 F103 的 Flash 读写。写操作是整页擦除再写,这是因为 STM32F103 的 Flash 没有单字节擦除功能,只能按页擦除。这里有个重要的代价:GRBL 的 $ 参数设置每次修改都会触发 EEPROM 写入,而 Flash 的擦写寿命是 1 万次左右,如果频繁改参数,Flash 会提前报废。解决方式是先在 RAM 里维护一份参数缓存,只在参数真正变化时才写 Flash,同时可以做一个简单的磨损均衡,轮流使用两个扇区。这个细节不算复杂,但不做的话,你的脱机控制器可能会在用了一个月后出现参数丢失的问题。

串口重定向这件事,本质上就是把 GRBL 源码里的 serial_getc、serial_putc 这些函数替换成 STM32 的 USART 收发。如果你只是做纯脱机控制,串口只用于调试信息输出,那重定向的工作量很小,只要保证 STM32 的 HAL 库初始化代码能和 GRBL 源码编译共存就行。我的经验是,不要把 HAL 库的中断处理和 GRBL 的中断处理混在一起,GPIO 初始化和定时器初始化可以用 HAL 库,但步进脉冲中断和 GRBL 状态机执行必须用 GRBL 自己的路径。

3. 屏幕 + 编码器做脱机人机界面:让 GRBL 从“上位机附属”变成“独立设备”

3.1 用编码器和菜单替代 G 代码输入

做过脱机面板的都知道,菜单交互是最容易翻车的地方。给 GRBL 加屏幕控制,常见做法是配一个 EC11 旋转编码器,旋转调节参数,按下确认,长按返回或急停。为什么不用按键阵列?因为按键数量有限,做参数输入要切换数字位,效率太低。编码器自带 A/B 相脉冲输出,配合 STM32 的外部中断,可以实现精确定位,手感也比按键好得多。

我习惯把屏幕显示做成三层结构:主菜单、子菜单、编辑页。主菜单只放四个入口:「运行文件」「手动控制」「坐标设置」「系统参数」。手动控制里是 X/Y/Z 轴的步进移动,步长可选 0.01、0.1、1、10 毫米。系统参数里则直接透传 GRBL 的 $ 参数,比如 $110 是 X 轴最大速度,$120 是 X 轴加速度。这样设计的好处是:用户不接触 G 代码,也能完成机器校准、对刀和跑文件这三个日常操作。

// 编码器旋转中断处理:根据旋转方向切换菜单项或调整参数值 void EXTI_Encoder_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line_EncoderA) != RESET) { EXTI_ClearITPendingBit(EXTI_Line_EncoderA); // 读取 B 相信号,判断旋转方向 if (GPIO_ReadInputDataBit(ENCODER_PORT, ENCODER_B_PIN)) { menu_cursor_down(); // 顺时针:下移菜单 } else { menu_cursor_up(); // 逆时针:上移菜单 } } }

这个中断处理函数是编码器交互的核心模板。它的逻辑是:每当 A 相产生上升沿中断,就读取 B 相的电平,如果 B 相是高电平,说明编码器顺时针旋转,否则是逆时针。这种判断方式叫正交解码的简化版,可以工作,但不稳定。正式做法是使用 STM32 的定时器编码器模式,让 TIM 硬件来处理 A/B 相脉冲,你只需要读取计数器的值即可。编码器模式的好处是自动滤波、不会误判,缺点是占一个定时器外设。在 F103 上有 5 个定时器,如果功能和资源允许,我建议用 TIM3 做编码器接口,TIM2 做步进脉冲,两者互不干扰。

菜单按键的事别忘了去抖动。EC11 编码器的内部机械触点,在旋转时会产生大量毛刺,如果没有硬件 RC 滤波,就一定要在软件里做去抖。最简单的方式是利用定时器产生一个 5ms 的软件扫描周期,在扫描里检测编码器状态变化,而不是直接用外部中断。我后来的一版就是这么做的:用一个 1ms 的 Systick 中断做时基,在主循环里每 5ms 去读一次编码器引脚逻辑。这样做的代价是响应速度略慢,但稳定性高很多,尤其是在电机运行时,干扰源多,外部中断很容易被误触发。

3.2 屏幕刷新的中断模型:别让 UI 挤占脉冲时序

屏幕控制最怕的一件事是:刷屏的时候,步进脉冲中断被阻塞,导致丢步。STM32F103 的 SPI 刷屏,尤其是 12864 OLED,刷新一帧全屏大概要传输 8KB 数据,在 18MHz SPI 下大约需要几毫秒。这几毫秒里如果你用的是阻塞式 SPI 传输,CPU 会被占死,脉冲中断响应延迟就会不稳定。所以脱机控制器的屏幕刷新,必须和脉冲生成解耦。

我的做法是把屏幕刷新放在主循环里,不放在中断里。具体来说:GRBL 的步进中断保持最高优先级,它负责脉冲生成,永远不被阻塞;屏幕刷新在主循环中执行,每次刷新一页或一个局部区域,不要一次性刷全屏。OLED 这种屏幕,其实每次只更新变化的区域就够了,比如参数值变了一个数字,那你就只刷那个数字所在的 8x16 像素区域。

// 主循环中的屏幕刷新:非阻塞,只有需要更新时才写入显示缓冲区 void ui_task(void) { static uint8_t last_cursor = 0xFF; static uint16_t last_value = 0xFFFF; // 只有当菜单项或参数值变化时才触发刷新 if (menu_cursor != last_cursor || current_value != last_value) { ui_draw_menu(menu_cursor, current_value); last_cursor = menu_cursor; last_value = current_value; } // 如果使用 OLED,可以在这里调用局部刷写函数 oled_update_dirty_region(); }

这里把「变化检测」放在了刷新的前提上,只在数据变化时重绘,其余时间屏幕刷新完全静默。你可能会问,如果屏幕需要显示实时坐标怎么办?坐标值每秒变化几十次,每次变化都刷屏会不会影响脉冲?这里有一个取舍:坐标刷新可以只在运动停止时进行,或者在运动过程中以 10Hz 的低频率刷新,而不是每个脉冲都刷新。GRBL 本身不提供实时坐标回传的接口,脱机控制时你可以在定时中断里读取 GRBL 的步进位置变量,但不要直接用它驱动屏幕刷新。更好的做法是维护一个 UI 侧的坐标缓存,在主循环中每隔 100ms 更新一次,这个延迟在视觉上不会让人察觉,但给脉冲中断留足了空间。

3.3 面板接线与干扰处理

屏幕控制的稳定性,一半在软件,一半在硬件接线。这个问题我吃过不少亏。脱机控制器装在雕刻机机箱里,电机驱动器的 PWM 干扰会通过地线传导到屏幕和编码器上,轻则显示花屏,重则编码器误触发。我的接线习惯是:控制板的数字地(GND)和驱动器的功率地分开走线,在靠近主控板的地方用磁珠或 0 欧电阻单点相连。如果是分体电源供电,还要注意编码器的屏蔽线必须单端接地,不能两端都接。

还有一个很多人忽略的细节:编码器 A/B 相建议各串一个 100 欧姆电阻,再接主控的 GPIO。这个电阻可以抑制边沿振铃,让信号更干净。如果你发现屏幕刷新时编码器会误跳,优先检查的就是有没有加 RC 滤波或串联电阻。对于 OLED 屏幕,我建议 I2C 接口的屏幕优先选 1.3 寸以下的小屏,因为大屏需要 I2C 超过标准 100kHz 的速率才能有可接受的刷新率,而 F103 的 I2C 外设本身有些坑,很多人翻过车,用软件模拟 I2C 反而更稳。SPI 接口的 OLED 则没有这个问题,速度更快,我一般选 SPI。

编码器和屏幕共享 3.3V 电源时,注意总电流。F103 的 LDO 稳压器一般只适合小负载,如果屏幕加上编码器后系统电压被拉到 3.0V 以下,表现出来就是「屏幕偶尔白屏,机器偶尔不启动」。我做过的一台机器,开始用的是 AMS1117-3.3,OLED 一刷全屏,电源就掉电复位,最后换了 DC-DC 降压模块才解决。这种问题,用万用表测一下 3.3V 引脚在刷屏瞬间的电压降,基本就能定位。

4. 脱机跑文件:SD 卡里的 G 代码如何喂给 GRBL 而不掉步

4.1 文件读取与 GRBL 缓冲区的配合

脱机控制的核心工作流是:把 G 代码文件放进 SD 卡,机器上电后从屏幕选择文件,然后自动运行。这一部分最需要小心的,是 G 代码文件的读取速度和 GRBL 串口接收缓冲区之间的配合。GRBL 原版设计是依赖上位机通过串口逐行发送 G 代码,每发送一行,GRBL 执行完再请求下一行。脱机模式下,你是在主控内部直接读取 SD 卡,替代了串口上位机的角色。如果你把文件内容一股脑喂给 GRBL 的缓冲区,它会因为缓冲区满而丢弃数据,但你并不知道它丢了哪一行。

我的做法是:自己实现一个「文件读取器」,它模拟上位机的行为——只有 GRBL 发出请求新数据的信号时,才从 SD 卡读取下一行。GRBL 源码里有一个 serial_rx_buffer 的循环取字符函数,你可以把它的数据源替换成 SD 卡文件读取。常见做法是改造 serial_getc,让它在缓冲区空时调用 SD 卡读取函数,从文件里读出下一个字符。要注意的是每次读取不能太久,SD 卡在读一个新扇区时可能需要几十微秒,这个时间如果发生在中断里,还是会影响脉冲,所以建议用 DMA 或使用 FATFS 的 f_read 配合预读缓冲。

// 从 SD 卡文件读取一个字符,替代串口接收 char sd_serial_getc(void) { // 如果当前缓冲为空,从 SD 卡按行读取数据填充 if (sd_buf_index >= sd_buf_length) { // 读取下一行 G 代码,最大长度 255 字节 sd_buf_length = read_line_to_buffer(sd_buf, sizeof(sd_buf)); sd_buf_index = 0; // 文件读到末尾,返回 GRBL 的结束标志 if (sd_buf_length == 0) { return GCODE_EOF; } } return sd_buf[sd_buf_index++]; }

这段代码解决的是 G 代码行读取与 GRBL 消费速率之间的适配。read_line_to_buffer 函数负责从 FATFS 中读出一行,并去掉换行符。这里有一个关键参数:读取的行缓冲长度。G 代码单行最长的情况出现在包含注释、参数和坐标组合时,实际经验值是 80 字符以内,但我见过有人写 G 代码,单行带了很长的注释,超过 200 个字节,如果你的缓冲区只有 128,这一行会被截断,然后 GRBL 解析出一半的命令,机器就会乱动。为了安全起见,缓冲区长度我设 256 字节,留足余量。

还有一个细节:G 代码文件的编码。GRBL 只认 ASCII 字符,如果你在手机上编辑 G 代码,存成了带 BOM 的 UTF-8,文件开头的三个字节(EF BB BF)会被 GRBL 当成非法字符,导致第一行被忽略或报错。卡在读文件不执行的第一反应,先检查文件头有没有 BOM,这是个很隐蔽的坑。

4.2 状态机与暂停/继续/急停逻辑

脱机控制器必须有明确的运行状态机:空闲、运行中、暂停、急停、回零中。GRBL 本身有运行状态,但屏幕控制需要额外的逻辑来处理用户操作。我一般把状态机放在主循环里,用枚举类型定义状态,每个状态对应一个处理函数,然后通过编码器按键触发状态切换。

暂停和急停是两回事,必须分清。暂停是指 GRBL 在安全的位置停止进给,保持当前坐标和夹具状态,等用户继续。急停则是立刻停止所有运动,最好还触发驱动器使能断开。GRBL 原版的暂停是通过发送「~」字符控制流实现的,急停则是 alarm 状态。你在屏幕程序中,暂停键对应发送「~」,继续也是「~」,急停则设置一个全局标志,让定时器停止输出脉冲,同时给驱动器 disable 引脚拉低。

// 脱机状态机的核心处理逻辑 void machine_state_task(void) { switch (machine_state) { case STATE_IDLE: // 空闲时显示主菜单,等待用户操作 break; case STATE_RUNNING: // 通过 SD 卡文件持续喂 GRBL 代码 if (grbl_is_buffer_available()) { sd_serial_feed(); } break; case STATE_PAUSED: // 暂停状态下,固定输出 ~ 保持停止 // 同时屏幕显示当前坐标和已运行行号 display_paused_info(); break; case STATE_ESTOP: // 急停:立刻关闭脉冲输出,禁用驱动器 disable_stepper_driver(); break; } }

这段逻辑展示了状态机的骨架。STATE_RUNNING 状态里有一个关键函数 grbl_is_buffer_available,它检查 GRBL 的串口缓冲区和规划器缓冲区是否还有空间,只有空间够时才读取 SD 卡下一行。这个判断是防止「缓冲区塞满导致 GRBL 内部丢数据」的保险。注意在暂停状态,不要停止 SD 卡的读取,而是停止向 GRBL 喂数据,GRBL 在暂停时不会消费新的 G 代码行,如果你还继续喂,数据就会堆积在缓冲区里,恢复运行时出现爆冲。

急停的处理,我特别强调一点:急停按键必须用硬件中断实现,不能只靠主循环轮询。因为主循环里如果正在执行 SD 卡读取,有可能会阻塞几百微秒,这个时间对于急停来说太长了。把急停接到 F103 的外部中断上,在中断里直接关闭定时器输出、禁用驱动器使能、拉低 GRBL 的 alarm 标志。注意在中断里不要执行复杂的文件操作,只做最简单的安全动作。

4.3 坐标系与对刀:屏幕上的直观设置

脱机控制器要让人拿到就能用,坐标系和对刀操作必须做得足够直观。GRBL 有工作坐标系(G54 等)和机器坐标系(机床原点),对刀的本质是把工作坐标系的原点设置在工件上的某个位置。屏幕控制界面上,我需要提供三个核心操作:回机器原点、设置工作原点、手动移动轴。

在屏幕程序里,设置工作原点最简单的方式是:用编码器手动移动各轴,移动到你认为的零点,然后按下确认,发送 G92 命令把当前位置设为零点。需要注意的是,GRBL 的原点是存储在掉电保持存储区的,如果你希望断电后还能记住原点,需要在发送 G92 后调用 GRBL 的 EEPROM 写入逻辑,否则下次上电原点就丢了。

// 在手动对刀模式下,编码器控制轴移动 void manual_jog_task(void) { // 每转一格编码器,移动一个步长 if (encoder_delta != 0) { char cmd[32]; // 根据当前选中的轴和方向生成手动进给命令 // 例如:$J=X+10 F300 snprintf(cmd, sizeof(cmd), "$J=%c%c%d F%d", axis_char, // 轴名 X/Y/Z encoder_delta > 0 ? '+' : '-', // 方向 step_size, // 步长,0.01/0.1/1/10 可选 jog_feed_rate); // 进给速度,mm/min grbl_send_command(cmd); encoder_delta = 0; } }

这里的 $J 命令是 GRBL 的实时手动进给命令。它不同于普通的 G 代码,系统执行 $J 命令时会暂停当前文件执行,执行完手动进给后继续。这个设计非常适合面板操作。注意 $J 命令不支持负数步长,所以方向是通过正负号控制的,代码里通过判断 encoder_delta 的正负来决定字符是 '+' 还是 '-'。还有一个参数需要调:jog_feed_rate。手动移动速度太慢,对刀时效率低;太快又危险,容易撞刀。我通常把它设置在 200~500mm/min 之间,这样既能快速定位,又能保证安全。

对刀还有一个业界玄学:如果你使用 Z 轴对刀仪,那在屏幕程序里要学会「先慢后快」。快速下降接近对刀仪,然后减速,接触到对刀仪的瞬间停止并记录高度。这个逻辑在 GRBL 里需要写一小段 G 代码脚本,屏幕控制只负责触发脚本执行。把对刀脚本放在 SD 卡的一个固定文件里,屏幕程序只需要在用户按下「自动对刀」时让 GRBL 执行那个文件即可,不必把脚本嵌入固件。

5. STM32F103 GRBL 脱机控制常见问题排查:5 个高频翻车点

现象 1:开机后步进电机嗡嗡响,但轴不动

这是我第一次做移植时遇到的血泪教训。现象是上电后,电机锁死但会发出持续的高频嗡嗡声,手动转轴有明显阻力,但屏幕和菜单都正常,就是 $J 手动移动时轴纹丝不动。

原因:步进驱动的 STEP 脉冲正常,但方向信号和使能信号在初始化时处于未定义状态。具体来说,F103 的 GPIO 在复位后默认是浮空输入,如果你在配置 GPIO 之前,GRBL 的初始化代码就执行了方向引脚电平设置,那么方向引脚可能是高电平也可能是低电平,同时电机已经使能,就会产生锁死但不运动的情况。

解决:把所有步进相关引脚的 GPIO 初始化全部放在 GRBL 的硬件初始化函数之前,并且明确设置为推挽输出,初始电平设为指定值。另外,驱动器使能引脚要主动拉低(按 GRBL 的默认逻辑,低电平使能),确保在 GRBL 状态机起来之前电机处于失能状态,等初始化完成后再使能。

现象 2:G 代码跑到一半,屏幕正常但机器停住不动

这个现象有几种表现,一种是真的停了,一种是很缓慢地继续走,有点像「堵车」。

原因之一是 SD 卡读取速度跟不上了。FATFS 在读取一个文件时,如果 SD 卡比较慢(低速卡或扩容卡),每次读取一个扇区需要的时间会波动。我遇到过一张 Class 4 的旧卡,随机读取速度只有几百 KB/s,而 GRBL 在高速运行时每秒钟要消费几十行 G 代码,平均下来刚好正常,但遇到带圆弧插补的密集代码段时就会瞬间吃满缓冲区。

解决:换一张高速卡,同时把 SD 卡读取改成连续块读取,而不是按行读一个字符就操作一次 SD 卡。预读缓冲是关键,每次从 SD 卡一次性读 512 字节或更多,然后从这个缓冲里逐行解析,缓冲快用完时再读下一块,这样可以把 SD 卡访问次数降低一个数量级。

现象 3:屏幕用 SPI OLED 时,偶尔花屏

花屏的表现是:屏幕会出一些随机像素噪点,或者整屏显示错位,但机器运动正常。

原因有两个。一个是 SPI 时钟频率太高。F103 的硬件 SPI 最高支持 18MHz,我一开始直接拉到最高速率,屏幕初始化没问题,但刷屏速率高的时候,干扰会出现在 SPI 数据线上。另一个原因是电源噪声,电机驱动器在大电流动作时,地线上会有尖峰,影响 3.3V 电源的稳定性。

解决:把 SPI 时钟降到 9MHz 或更低,同时在 SPI 的 MOSI 和 SCLK 引脚上各加 33 欧姆串联电阻,抑制边沿振铃。如果是电源问题,用示波器看 3.3V 在电机动作时的电压波形,如果尖峰超过 300mV,就换 DC-DC 供电或加滤波电容。

现象 4:编码器旋转时,菜单跳到不对的选项或数值乱跳

现象是编码器转一格,菜单会跳两格或三格,或者旋转方向不稳定,时好时坏。

原因:没有做硬件去抖,或者软件里去抖时间太短。EC11 编码器的机械抖动通常在 1~3ms 之间,如果用外部中断直接触发,抖动会同时触发多个中断,导致多读取几次 A 相。

解决:软件采样周期改为 5ms 轮询模式,或者用定时器的编码器接口模式。在硬件上,A/B 相各加一个 10nF 电容到地,配合 10k 欧姆上拉电阻,形成低通滤波。我后来把所有编码器都放在定时器编码器模式上,再也没有出现跳变问题。

现象 5:上电后 GRBL 参数丢失,所有 $ 设置回归默认

现象是:在屏幕控制里设置好了 $110、$120 等参数,断电后重新开机,参数全部变回默认值,需要重新设置。

原因:固件里的 EEPROM 模拟写入失败,或者写入的地址与读取的地址不一致。有一个很隐蔽的情况:STM32 的 Flash 写入操作需要解锁,如果你在写 Flash 时被其他中断打断,解锁状态可能丢失,写入就会静默失败。

解决:在写入函数里检查 FLASH 操作状态标志,确认写入成功后再返回。同时,把 EEPROM 的起始地址定义为 Flash 的一个独立页,避免与代码区冲突。另外,建议在参数区开头写一个版本号字节,每次固件升级时更新版本号,如果读取到的版本号不匹配,就重置为默认参数,这样可以避免固件改动导致参数偏移的问题。

现象 6(额外记录):激光雕刻机用这套方案时,激光会发出「一抖一抖」的光斑

这个现象不属于步进电机丢步,而是激光 PWM 输出不稳定。原因是你把屏幕刷新的任务放在了与激光 PWM 相同的定时器中断里,或者 SPI 刷屏时导致 PWM 的占空比更新被延迟。

解决:激光 PWM 使用独立的定时器通道,并确保它的中断优先级高于 SPI 刷屏的中断优先级,或者干脆把 PWM 更新放到 DMA 循环模式中。如果 GRBL 源码里激光输出是使用比较匹配中断更新的,那么把屏幕刷新频率降低,或者把 SPI 刷屏改成小块局部刷新,通常能解决光斑抖动的问题。

6. 进阶验证:用示波器和激光测速仪检验你的脱机 GRBL 方案

完成了脱机控制器的组装和基本调试之后,检验它是否真的能稳定工作,不能只依靠「跑一个文件不丢步」这种模糊判断。你需要三个具体的验证工具:示波器、逻辑分析仪和激光测速仪。没有激光测速仪也可以用千分表或光栅尺替代。

第一个验证是脉冲时序检查。用示波器测量 STEP 引脚波形,在 $J 手动移动时,观察脉冲频率是否平滑变化。GRBL 的梯形加减速表现是:启动时脉冲慢慢变密,高速时达到稳定频率,减速时慢慢变稀。如果你看到脉冲频率有跳变或不连续,说明定时器自动重载值的计算精度不够,或者中断响应受到了其他中断的干扰。脉冲频率的稳定性可以用示波器的频率测量功能来评估,误差应该小于 1%。

第二个验证是真机位移测量。在机器上安装千分表,让 X 轴以 100mm/min 的速度移动 50mm,观察千分表读数。这个测试要在不同速度档位下各做一次。如果低速时位移准确,高速时反而有误差,说明是加速度设置过高导致丢步;如果各速度下位置都偏小但偏差比例一致,则是步进电机每毫米步数($100)的设置错误。这个测试建议周期性做,尤其在调整了驱动器细分之后。

第三个验证是重复性测试。同一个 G 代码文件连续运行十次,每次运行后回到原点,记录机器坐标值。理论上十次应该完全一致,实际允许偏差在 1 个步进脉冲对应的距离以内。如果你的机器每次偏差都在同一个方向逐渐增大,说明存在累积误差,大概率是脉冲丢失,优先检查驱动器电流设置和加速度参数。

我自己的习惯是:把验证数据记录在一张表里,每次调整参数后重新做一遍,看趋势。最后检查一下固件里对 GRBL 默认参数 $110 等的覆盖值是否正确,这一步经常被漏掉。祝你能把这套脱机方案用得顺手,希望帮到你。

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

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

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

立即咨询