☰
STM32F103上运行GRBL:CNC脱机控制器设计与实现
2026/10/30 3:19:49 网站建设 项目流程

简介:面向DIY雕刻机、CNC、激光机与写字机控制爱好者的STM32F103 GRBL脱机控制代码包,主要解决脱离电脑、通过SD卡执行加工文件的需求。代码基于STM32F103RC搭建,硬件引脚可按自身板卡替换,配合迪文二代4.3英寸屏完成人机交互,并支持在屏幕上一键切换设备型号;硬件相关参数集中在屏幕端,输入默认密码2356后即可调整,便于不同机架适配。压缩包共601个文件,约18.75MB,包含C/H源文件与工程配置、HEX/BIN固件、SCT链接脚本、CFG参数文件,以及BMP/TFT屏幕界面与字库素材,另有AXF/MAP编译输出,便于对照移植和二次开发。已有3230人学习下载。对于想研究GRBL在STM32上的脱机运行、串口电机控制、迪文屏DGUS界面与参数管理的读者,这套工程提供了较完整的代码结构、屏幕资源和可烧录产物,可作为控制板DIY与功能扩展的参考底包。

1. 从“拖电脑尾巴”到脱机运行:为什么要在STM32F103上跑GRBL

很多CNC雕刻机玩家一开始都是拿台电脑连着机器跑GRBL,车间里粉尘一大,电脑风扇就成了吸尘器;稍不留神踢掉USB线,正在走的刀路直接报废。后来圈子里开始流行脱机控制器,核心思路就是把GRBL塞进单片机,SD卡里存G代码文件,屏幕上点两下就能开始加工。STM32F103这款芯片资源刚好够用——72MHz主频、定时器管够、串口和SPI齐全,配上迪文DGUS串口屏,成本能压到百元以内。这套方案适合正在找毕设题目的学生,也适合想给旧雕刻机升级的创客。GRBL脱机运行的关键难点不在算法,而在文件系统和屏幕交互的稳定调度,我见过太多人卡在SD卡读不出、屏幕按键没反应这类细节上。

2. 总体架构设计:四层结构与关键模块职责

拿到这个需求,我一般先把软件拆成四层:硬件抽象层、驱动层、协议层、应用层。硬件抽象层负责STM32的时钟、GPIO、定时器初始化;驱动层包含步进电机脉冲生成、SD卡FatFS文件系统、DGUS屏串口收发;协议层解析G代码指令和DGUS通信帧;应用层是脱机运行状态机和界面逻辑。整个系统跑在裸机上,不用RTOS,靠主循环加中断协作,这样代码体积小、响应也够快。下面从硬件资源分配和软件分层两个角度展开。

2.1 硬件选型与资源分配:STM32F103的引脚与定时器规划

我选的是STM32F103C8T6或者RCT6,C8T6便宜但RAM只有20KB,跑GRBL加上FatFS和屏幕缓存会比较紧张,建议用RCT6(64KB RAM)。定时器方面,TIM2用来产生X/Y/Z三轴的步进脉冲(通过比较输出),TIM3做脉冲计数或者延时,TIM4留给DGUS屏的串口超时检测。USART1接DGUS屏,SPI1接SD卡,另外留几个GPIO给限位开关和急停按钮,用外部中断。下面这张表是我实际用的引脚分配:

功能引脚外设备注
X轴脉冲PA0TIM2_CH1比较输出
X轴方向PA1GPIO推挽输出
Y轴脉冲PA2TIM2_CH3比较输出
Y轴方向PA3GPIO推挽输出
Z轴脉冲PA6TIM3_CH1比较输出
Z轴方向PA7GPIO推挽输出
步进使能PB0GPIO低电平使能
DGUS串口TXPA9USART1_TX115200bps
DGUS串口RXPA10USART1_RX中断接收
SD卡CSPA4GPIOSPI1片选
SD卡SCKPA5SPI1_SCK分频到9MHz
限位X/Y/ZPB12/13/14EXTI下降沿触发

注意:STM32F103的TIM2是32位定时器,适合做长周期脉冲;TIM3是16位,跑Z轴这种短行程够用。如果三轴联动频率高,建议把Z轴也挪到TIM2的另一个通道。

2.2 软件分层:GRBL内核、文件系统、屏幕交互、运动控制

分层的目的就是让每个模块能单独测试。GRBL内核我直接移植了官方0.9j版本的gcode.c和planner.c,把Arduino的millis()和delay_us()换成STM32的SysTick和DWT。文件系统用FatFS R0.14,挂载在SPI1上。屏幕交互单独写一个dgus.c,只负责收发和解析。运动控制层把GRBL规划出来的速度曲线转换成定时器比较值。主循环里做三件事:轮询屏幕事件、喂G代码缓冲区、检查限位状态。下面是一个简化版的主循环调度框架:

// main.c 主循环节选 while (1) { dgus_process(); // 处理屏幕触摸和变量更新 if (grbl_get_state() == STATE_IDLE) { file_prefetch(); // 从SD卡预读G代码到环形缓冲区 } grbl_check_limit(); // 检查限位和急停 if (grbl_get_state() == STATE_RUN) { planner_recalculate(); // 根据缓冲区情况调整规划 } }

这段代码里dgus_process()是非阻塞的,靠串口中断接收完整帧后置标志位;file_prefetch()每次只读512字节,避免长时间占用SPI总线影响脉冲输出;grbl_check_limit()里急停用最高优先级中断,直接切断定时器输出。主循环的节奏决定了系统响应,我一般把dgus_process()放在最前面,保证屏幕按键不卡顿。

3. 核心模块实现:从G代码解析到脉冲输出

核心模块有三个硬骨头:SD卡文件系统要稳、脉冲输出要准、屏幕协议要通。很多开源方案在这三处翻车,要么读文件时丢步,要么屏幕花屏,要么脉冲频率上不去。下面按文件系统、脉冲生成、屏幕通信三个方向拆开讲。

3.1 文件系统挂载与G代码预读缓存设计

脱机运行的第一步是让STM32认出SD卡里的FAT32文件系统。我用FatFS的f_mount挂载,然后f_open打开.nc或.gcode文件。直接逐行读取会导致SPI总线被长时间占用,影响脉冲中断的实时性。解决办法是建一个环形缓冲区,主循环里每次读512字节(一个扇区),GRBL从缓冲区取指令。下面是我用的预读函数:

// sdcard.c 预读缓存实现 #define BUF_SIZE 1024 static uint8_t ring_buf[BUF_SIZE]; static uint16_t rd_ptr = 0, wr_ptr = 0; FRESULT file_prefetch(void) { UINT br; uint16_t free_space = (rd_ptr > wr_ptr) ? (rd_ptr - wr_ptr - 1) : (BUF_SIZE - wr_ptr + rd_ptr - 1); if (free_space < 512) return FR_OK; // 空间不足,先不读 FRESULT res = f_read(&file, &ring_buf[wr_ptr], 512, &br); if (res == FR_OK && br > 0) { wr_ptr = (wr_ptr + br) % BUF_SIZE; } return res; }

这里rd_ptr和wr_ptr分别是读指针和写指针,free_space计算剩余空间时要留一个字节防止指针重合。每次只读512字节,读完立即返回,SPI总线占用时间控制在几毫秒内。如果SD卡读写速度慢,可以降到256字节,但缓冲区不能小于512,否则GRBL解析时容易断流。注意:f_read返回后要检查br,有些劣质卡会返回成功但实际读不到数据,加一句if (br != 512) { /* 重试或报错 */ }能避开不少玄学问题。

3.2 步进电机脉冲生成:定时器中断与梯形加减速

GRBL原本用AVR的定时器比较匹配产生脉冲,移植到STM32后我改用TIM2的PWM模式加中断更新比较值。每个轴对应一个通道,脉冲频率由自动重装载值决定,占空比固定50%。为了做梯形加减速,需要在每个脉冲周期结束后更新下一次的比较值。GRBL的planner已经算好了速度曲线,我们只需要把当前速度转换成定时器周期。下面这段代码是定时器初始化和脉冲中断服务函数:

// stepper.c 脉冲生成 void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); static uint32_t step_count = 0; if (step_count < current_block->steps) { // 根据planner给出的速度更新下一次周期 uint16_t period = planner_get_period(step_count); TIM2->ARR = period; step_count++; if (step_count >= current_block->steps) { stepper_block_done(); // 通知planner切换到下一段 } } } }

planner_get_period()返回的是定时器周期值,单位是微秒对应的计数值。比如系统时钟72MHz,预分频设为71,则计数值等于微秒数。current_block->steps是当前速度段的总步数。这里有个血泪经验:不要在中断里调用浮点运算,planner_get_period里把浮点提前算好存成整数表。另外,如果频率超过50kHz,定时器中断会吃掉大量CPU,建议改用DMA或者定时器的比较输出模式,让硬件自动翻转引脚。

3.3 屏幕通信协议解析:DGUS变量读写与界面响应

DGUS屏的串口帧格式是固定的:帧头5A A5+ 长度 + 命令 + 数据。触摸事件会上报变量地址和键值,我们要解析后触发状态机。发送数据给屏幕时,用5A A5 长度 0x82 变量地址 数据。下面是一个解析函数和发送函数:

// dgus.c 帧解析 void dgus_parse(uint8_t *buf, uint16_t len) { if (buf[0] != 0x5A || buf[1] != 0xA5) return; uint8_t cmd = buf[4]; if (cmd == 0x83) { // 触摸事件 uint16_t addr = (buf[5] << 8) | buf[6]; uint8_t key = buf[7]; dgus_event_handler(addr, key); } } void dgus_write_var(uint16_t addr, uint8_t *data, uint8_t len) { uint8_t frame[16] = {0x5A, 0xA5, 0x00, 0x82}; frame[2] = len + 3; frame[5] = addr >> 8; frame[6] = addr & 0xFF; memcpy(&frame[7], data, len); uart_send(frame, len + 7); }

dgus_event_handler里根据变量地址区分是哪个按钮:比如地址0x1000是“开始”,0x1001是“暂停”。发送函数里的长度字节要算对,0x82表示写变量,后面跟地址和数据。注意DGUS屏的波特率要跟STM32一致,我一般设115200,串口中断接收时用空闲中断判断一帧结束,比固定长度更可靠。

4. 屏幕交互与脱机控制流程的实现

屏幕是用户唯一能摸到的部分,交互设计直接决定方案好不好用。我见过不少脱机控制器功能都跑通了,但屏幕上按钮布局反人类,换个文件要按七八下。这一章讲页面布局和状态机设计。

4.1 DGUS页面布局与控件地址映射

我设计了四个页面:主界面、文件选择、参数设置、手动控制。主界面显示当前坐标、运行状态、文件进度;文件选择页列出SD卡里的G代码文件;参数设置页能改进给倍率、主轴转速;手动控制页有X/Y/Z点动和回零按钮。每个页面的控件对应一组变量地址,我在代码里用宏定义映射。下面这张表是核心地址分配:

页面变量地址控件类型功能
主界面0x1000按键返回开始/暂停
主界面0x1001按键返回停止
主界面0x2000文本显示当前坐标X
主界面0x2010文本显示运行状态
文件页0x3000列表选择文件名列表
参数页0x4000数据输入进给倍率
手动页0x5000按键返回X+点动

文件列表用DGUS的“文件选择”控件,需要提前把文件名写入屏幕的NorFlash。我一般在上电初始化时通过串口把SD卡根目录下的.nc文件列表发送给屏幕,更新列表变量。这样用户换SD卡后屏幕能自动刷新。

4.2 脱机运行状态机与按键事件处理

状态机是整个脱机控制的核心。我定义了五个状态:IDLE(空闲)、RUN(运行)、PAUSE(暂停)、ALARM(报警)、HOMING(回零)。按键事件驱动状态切换,比如IDLE下按“开始”进入RUN,RUN下按“暂停”进入PAUSE,任何状态下按“急停”进入ALARM。下面是一个简化版的状态处理代码:

// state_machine.c void dgus_event_handler(uint16_t addr, uint8_t key) { switch (current_state) { case IDLE: if (addr == 0x1000 && key == 0x01) { // 开始 current_state = RUN; grbl_start_job(); } break; case RUN: if (addr == 0x1000 && key == 0x02) { // 暂停 current_state = PAUSE; grbl_pause(); } break; case PAUSE: if (addr == 0x1000 && key == 0x01) { // 继续 current_state = RUN; grbl_resume(); } break; case ALARM: if (addr == 0x1001 && key == 0x03) { // 复位 current_state = IDLE; grbl_reset(); } break; } }

按键值key是DGUS屏在控件属性里设置的返回值,我习惯用0x01表示确认、0x02表示暂停、0x03表示复位。注意状态切换时要同步更新屏幕上的状态显示,比如进入ALARM后把状态文本变量写成“报警”,并且禁止“开始”按钮响应,直到复位。急停按钮我接在外部中断上,不经过状态机,直接切断定时器输出并置ALARM,这样响应最快。

5. 避坑与常见问题排查:从“电机不转”到“屏幕花屏”

这个项目我前后打了三版板子,踩过的坑能写满一页纸。下面挑五个最典型的问题,按现象、原因、解决三步走,希望能帮你少烧几片芯片。

5.1 电机不转,但用示波器能看到脉冲

现象:屏幕上点了开始,坐标在变,但电机纹丝不动。用示波器量脉冲引脚有波形,方向信号也有。原因:步进驱动器的使能信号极性搞反了,或者驱动器没共地。STM32输出低电平使能,但有些驱动器默认高电平使能,结果芯片一直处于释放状态。解决:先看驱动器手册,确认ENA引脚电平;用万用表量STM32和驱动器之间是否共地,不共地会导致信号电平漂移。我一般会在初始化时把使能引脚拉低,然后屏幕上加一个“电机使能”开关,方便排查。

5.2 屏幕通信时好时坏,偶尔花屏

现象:界面偶尔卡死,或者变量显示乱码。原因:DGUS屏的串口线太长(超过30cm)或者没有屏蔽,车间里变频器一启动就干扰;另外STM32的USART1中断优先级太低,被定时器中断打断后丢帧。解决:串口线换成屏蔽双绞线,屏的地线和STM32的地线单独走一根粗线;把USART1中断优先级设成比TIM2高(NVIC_PriorityGroup_2下设抢占优先级0)。如果还不行,在串口接收里加超时复位,收到半个帧就清空缓冲区。

5.3 SD卡识别失败,f_mount返回FR_NO_FILESYSTEM

现象:上电后屏幕显示“无SD卡”,串口打印挂载失败。原因:卡格式不是FAT32(有些新卡默认exFAT),或者SPI时钟太快导致初始化失败。解决:先用电脑把卡格式化成FAT32,分配单元大小选4096字节;STM32这边把SPI1的分频调到18MHz以下(72MHz/4=18MHz),初始化阶段降到低速(72MHz/128),挂载成功后再切回高速。注意:有些杂牌卡在SPI模式下兼容性差,换张闪迪或三星的卡能省半天调试时间。

5.4 运行中突然停止,屏幕回到主界面

现象:加工到一半机器停了,屏幕像刚上电一样。原因:看门狗复位。GRBL解析复杂G代码时主循环超时,没来得及喂狗。解决:把看门狗的溢出时间从2秒改到5秒,或者在文件预读函数里加喂狗。另外检查堆栈大小,FatFS和DGUS的缓冲区如果开太大,栈溢出也会导致复位。我一般把栈设成0x800,堆设成0x400,从.map文件里确认剩余RAM。

5.5 脉冲频率上不去,电机高速时堵转

现象:低速正常,进给速度一调高电机就嗡嗡响不转。原因:定时器中断频率太高,CPU来不及处理。比如步进细分16,转速3000mm/min,脉冲频率能到20kHz以上,中断里还要算速度规划,直接爆表。解决:把脉冲输出改成定时器比较输出模式,硬件自动翻转引脚,中断里只更新比较值;或者降低细分(8细分够用)。如果还不行,换STM32F103RCT6,RAM大一些能开更大的缓冲区,减少文件读取对中断的影响。

6. 验收与答辩:怎么证明你的脱机控制器真的能用

验收这个项目,我一般按三步走:先验静态功能,再跑动态刀路,最后做老化测试。静态功能包括屏幕按键响应、SD卡文件列表显示、电机点动方向正确、限位开关触发后停止。动态刀路用一个简单的正方形轮廓G代码,观察走刀是否平滑、屏幕坐标是否实时更新。老化测试让机器连续跑半小时以上,看会不会死机或丢步。下面这张表是我答辩时用的验收清单:

测试项通过标准测试方法
SD卡挂载上电5秒内显示文件列表插卡重启,观察屏幕
电机点动X/Y/Z点动方向与屏幕一致手动页分别按+/-按钮
限位保护触发限位后电机立即停止手动压下限位开关
急停响应按下急停后所有轴停转运行中按急停
连续运行30分钟无死机、无丢步跑循环刀路并记录

答辩时评委最爱问三个问题:为什么不用Arduino而选STM32?GRBL移植改了哪些地方?屏幕通信怎么保证不丢帧?我的回答套路是:STM32定时器资源多,能同时做三轴脉冲和串口屏;移植主要改时钟和IO,planner没动;串口用空闲中断加超时重传,实际测试连续按200次按键无丢帧。现场演示时先跑一遍正方形,再故意拔掉SD卡看报错,最后展示急停功能,基本就稳了。

从那以后我每次做脱机项目,都会在板子上留一个串口调试口,把关键状态打印出来。屏幕再好看,也不如串口日志能定位问题。希望帮到你。

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

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

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

立即咨询