简介:面向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轴脉冲 | PA0 | TIM2_CH1 | 比较输出 |
| X轴方向 | PA1 | GPIO | 推挽输出 |
| Y轴脉冲 | PA2 | TIM2_CH3 | 比较输出 |
| Y轴方向 | PA3 | GPIO | 推挽输出 |
| Z轴脉冲 | PA6 | TIM3_CH1 | 比较输出 |
| Z轴方向 | PA7 | GPIO | 推挽输出 |
| 步进使能 | PB0 | GPIO | 低电平使能 |
| DGUS串口TX | PA9 | USART1_TX | 115200bps |
| DGUS串口RX | PA10 | USART1_RX | 中断接收 |
| SD卡CS | PA4 | GPIO | SPI1片选 |
| SD卡SCK | PA5 | SPI1_SCK | 分频到9MHz |
| 限位X/Y/Z | PB12/13/14 | EXTI | 下降沿触发 |
注意: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卡看报错,最后展示急停功能,基本就稳了。
从那以后我每次做脱机项目,都会在板子上留一个串口调试口,把关键状态打印出来。屏幕再好看,也不如串口日志能定位问题。希望帮到你。
本文还有配套的精品资源,点击获取