GRBL0.9魔改实战:三轴机械臂运动控制从源码修改到写字机实现
GRBL 0.9 深度魔改:从开源固件到三轴机械臂运动控制器的蜕变之路
如果你玩过CNC雕刻机或者3D打印机,那么对GRBL这个名字一定不会陌生。这个开源、高效、运行在8位AVR单片机上的G代码解析与运动控制固件,几乎是所有桌面级数控设备的“心脏”。但你是否想过,这颗为笛卡尔坐标系(XYZ三轴直线运动)而生的“心脏”,能否移植到完全不同的三轴机械臂上,让它也能精准地执行G代码指令,完成写字、绘图甚至更复杂的任务?这听起来像是一场硬核的“器官移植”手术,不仅需要理解GRBL的运行机理,更要深入运动学的核心,进行一场从底层算法到上层应用的全面改造。今天,我们就来彻底拆解这个过程,分享如何将GRBL 0.9成功魔改为一个专为三轴机械臂设计的运动控制器,并最终实现一台稳定的写字机。
这不仅仅是代码的复制粘贴,而是一次思维模式的转换。我们将从运动学原理的差异讲起,一步步深入到GRBL源码的关键模块,讲解如何植入逆运动学算法、扩展$指令系统、适配G代码逻辑,最后完成从硬件校准到软件调试的完整闭环。无论你是嵌入式开发者,还是热衷于DIY的硬件极客,这篇文章都将为你提供一套清晰、可落地的实战方案。
1. 理解核心差异:从直线运动到旋转关节的思维转换
在开始动刀修改代码之前,我们必须先搞清楚要解决的根本问题是什么。GRBL原生是为笛卡尔坐标系下的设备设计的,比如最常见的三轴雕刻机:X轴电机驱动刀头左右移动,Y轴电机驱动刀头前后移动,Z轴电机驱动刀头上下移动。这三个轴的运动是解耦的,彼此独立,控制逻辑相对直观:给定一个目标点坐标 (X, Y, Z),GRBL只需要分别计算三个轴需要移动的距离,然后通过插补算法协调三个步进电机同步运动即可。
然而,三轴机械臂(通常指SCARA或类似结构)的运动方式截然不同。它通常由两个旋转关节(大臂、小臂)和一个末端旋转或升降关节构成。控制器接收的同样是目标点的空间坐标 (X, Y, Z),但需要驱动的是几个关节的角度。这里就引入了两个核心概念:
- 正向运动学:已知各个关节的角度,计算末端执行器(笔尖、夹爪等)在空间中的位置和姿态。
- 逆运动学:已知末端执行器期望到达的空间位置和姿态,反解出各个关节需要转动的角度。
GRBL原生只处理正向的、线性的位置到步数的转换。而要让机械臂工作,我们必须为其植入逆运动学求解器。这是本次魔改最核心、最基础的一步。
运动学模型定义 为了后续代码编写清晰,我们首先需要明确机械臂的几何模型。这里我们以一个典型的二连杆平面机械臂加上一个末端旋转轴为例进行说明,这也是许多桌面级写字机采用的构型。
| 参数符号 | 物理意义 | 说明 |
|---|---|---|
L1 | 大臂长度 | 从基座旋转中心到大臂与小臂连接点的距离 |
L2 | 小臂长度 | 从小臂与大臂连接点到末端执行器(笔尖)的距离 |
θ1 | 大臂关节角 | 相对于基座零位的旋转角度 |
θ2 | 小臂关节角 | 相对于大臂杆件方向的旋转角度 |
θ3 | 末端旋转角 | 控制笔尖绕自身轴线旋转的角度,对于写字可能用于笔锋调整 |
在这个模型下,末端位置 (X, Y, Z) 与关节角 (θ1, θ2, θ3) 之间的关系,就是我们需要用代码实现的数学公式。
注意:实际的机械臂可能存在多种构型(如Delta、六轴等),其运动学方程也不同。本文所述方法侧重于原理和GRBL的集成思路,你需要根据自己机械臂的具体结构推导或查找对应的正/逆运动学公式。
2. 潜入GRBL源码:关键模块分析与修改策略
GRBL 0.9的代码结构清晰,模块化程度高,这为我们进行定向修改提供了便利。我们不需要重写整个固件,而是像做外科手术一样,在几个关键位置进行“嫁接”。主要涉及以下几个文件:
config.h:全局配置和宏定义开关。settings.c:系统参数($指令)的存储与处理。planner.c:运动规划器,负责将G代码路径分解为可执行的线段块(Block)。system.c&limits.c:系统状态管理和限位处理。gcode.c:G代码解析与执行。
我们的修改策略是:增加一个机械臂运动学模块,并通过条件编译宏(如 ROBOT_ARM)来控制其启用。这样可以在同一份代码库中兼容传统CNC和机械臂两种模式。
2.1 创建运动学核心模块:robot_arm.c/h
这是新增的核心文件,我们将正/逆运动学算法实现在这里。首先创建头文件 robot_arm.h,定义必要的结构、变量和函数原型。
// robot_arm.h
#ifndef ROBOT_ARM_H
#define ROBOT_ARM_H
#include "grbl.h"
// 机械臂参数全局变量
extern volatile float SCARA_LINKAGE_1; // 大臂长度 L1 (mm)
extern volatile float SCARA_LINKAGE_2; // 小臂长度 L2 (mm)
extern volatile float MANUAL_HOME_POS[3]; // 手动设置的各轴零点角度
extern bool angle_mode; // 模式标志:true为直接角度模式,false为坐标模式
// 函数声明
void forward_kinematics_ROBOT(const float *angles, float *cartesian);
void inverse_kinematics_ROBOT(const float *cartesian, float *angles);
void scara_report_positions(void); // 用于报告当前关节角
#endif
接下来,在 robot_arm.c 中实现具体算法。这里以平面二连杆(忽略Z轴或Z轴独立)的逆运动学为例,展示核心计算逻辑。
// robot_arm.c
#include <math.h>
#include "robot_arm.h"
// 初始化默认参数
volatile float SCARA_LINKAGE_1 = 200.0f; // 默认大臂长200mm
volatile float SCARA_LINKAGE_2 = 200.0f; // 默认小臂长200mm
float MANUAL_HOME_POS[3] = {-45.0f, 135.0f, 0.0f}; // 示例零点角度
bool angle_mode = false;
// 正向运动学:已知关节角,求末端坐标
void forward_kinematics_ROBOT(const float *angles, float *cartesian) {
float theta1 = angles[0] * M_PI / 180.0f; // 转为弧度
float theta2 = angles[1] * M_PI / 180.0f;
float theta3 = angles[2]; // 旋转轴,可能独立处理
cartesian[0] = SCARA_LINKAGE_1 * cosf(theta1) + SCARA_LINKAGE_2 * cosf(theta1 + theta2);
cartesian[1] = SCARA_LINKAGE_1 * sinf(theta1) + SCARA_LINKAGE_2 * sinf(theta1 + theta2);
cartesian[2] = 0; // 假设Z轴独立或为固定高度,此处简化
}
// 逆运动学:已知末端坐标,求关节角 (两点式,返回两个可能解之一,通常取“肘部向上”解)
void inverse_kinematics_ROBOT(const float *cartesian, float *angles) {
float x = cartesian[0];
float y = cartesian[1];
float L1 = SCARA_LINKAGE_1;
float L2 = SCARA_LINKAGE_2;
float D = (x*x + y*y - L1*L1 - L2*L2) / (2 * L1 * L2);
// 检查目标点是否在可达工作空间内
if (D > 1.0f || D < -1.0f) {
// 目标不可达,此处应进行错误处理,例如设置标志或使用上一组角度
// 为简单演示,我们返回一个安全值(如零点)
angles[0] = MANUAL_HOME_POS[0];
angles[1] = MANUAL_HOME_POS[1];
angles[2] = 0;
return;
}
float theta2 = acosf(D); // 小臂关节角 (弧度)
// 通常取 theta2 为负值,对应另一种臂形(肘部向下),根据需求选择
// float theta2 = -acosf(D);
float theta1 = atan2f(y, x) - atan2f(L2 * sinf(theta2), L1 + L2 * cosf(theta2));
angles[0] = theta1 * 180.0f / M_PI; // 转回角度制
angles[1] = theta2 * 180.0f / M_PI;
angles[2] = cartesian[2]; // 假设第三轴为独立旋转,直接传递或进行其他计算
}
这个逆运动学函数是算法的核心。它接收一个目标坐标 (x, y, z),计算出机械臂两个主要关节需要转动的角度 θ1 和 θ2。acosf(D) 中的 D 值必须在 [-1, 1] 区间内,否则数学上无解,意味着目标点超出了机械臂的物理工作范围,代码中需要加入健壮的错误处理。
2.2 修改运动规划器 (planner.c):坐标到角度的转换
GRBL的 plan_buffer_line() 函数是运动规划的入口。它接收一个目标坐标,并计算出一系列步进电机脉冲指令。对于机械臂,我们需要在这里插入一个“转换层”:先将目标坐标通过逆运动学转换成目标关节角,再将关节角换算成步进电机所需的步数。
找到 plan_buffer_line() 函数中计算目标步数 (target_steps) 的部分。我们需要在原有的笛卡尔坐标计算路径旁,通过条件编译为机械臂增加一条处理路径。
// 在 planner.c 的 plan_buffer_line 函数内部
#ifdef ROBOT_ARM
float target_angles[N_AXIS]; // 存储计算出的目标角度
// 调用逆运动学,将笛卡尔坐标 target 转换为关节角度 target_angles
inverse_kinematics_ROBOT(target, target_angles);
for (idx=0; idx<N_AXIS; idx++) {
// 关键修改:将“毫米”转换为“步数”的逻辑,改为将“角度”转换为“步数”
// 假设 settings.steps_per_mm[idx] 现在被重新解释为 steps_per_degree(每度步数)
target_steps[idx] = lroundf(target_angles[idx] * settings.steps_per_mm[idx]);
block->steps[idx] = labs(target_steps[idx] - pl.position[idx]);
block->step_event_count = max(block->step_event_count, block->steps[idx]);
// delta_mm 在机械臂语境下可理解为 delta_angle(角度变化量)
float delta_angle = (target_steps[idx] - pl.position[idx]) / settings.steps_per_mm[idx];
unit_vec[idx] = delta_angle;
}
#else
// 原有的笛卡尔坐标系计算代码
for (idx=0; idx<N_AXIS; idx++) {
target_steps[idx] = lroundf(target[idx] * settings.steps_per_mm[idx]);
block->steps[idx] = labs(target_steps[idx] - pl.position[idx]);
block->step_event_count = max(block->step_event_count, block->steps[idx]);
float delta_mm = (target_steps[idx] - pl.position[idx]) / settings.steps_per_mm[idx];
unit_vec[idx] = delta_mm;
}
#endif
这里有一个重要的概念转换:对于机械臂,settings.steps_per_mm 这个参数不再表示“每毫米步数”,而应该被理解为 steps_per_degree(每度步数)。这意味着你需要根据你的步进电机驱动细分、减速比和机械臂关节的直接传动关系,重新校准这个值。例如,如果电机转一圈(360度)需要200步*16细分=3200步,并且通过1:1直接驱动关节,那么 steps_per_degree 就是 3200 / 360 ≈ 8.889。
2.3 扩展 $ 指令系统 (settings.c):机械臂参数配置
GRBL通过 $ 指令进行系统参数配置。我们需要添加新的指令来设置机械臂的专属参数,如臂长、零点角度等。在 settings.c 文件的 settings_store_global_setting() 函数中,找到 switch (index) 语句,在合适的 case 后面添加新的指令处理。
// 在 settings.c 中
case 27: // 假设27是原有最后一个参数
settings.homing_pulloff = value;
break;
// 新增机械臂参数指令
#ifdef ROBOT_ARM
case 30: // $30:设置大臂当前角度(用于初始化或校准)
// 这里不存储到EEPROM,仅临时设置系统位置
sys.position[X_AXIS] = lroundf(value * settings.steps_per_mm[X_AXIS]);
plan_sync_position(); // 同步规划器位置
return(STATUS_OK);
case 31: // $31:设置小臂当前角度
sys.position[Y_AXIS] = lroundf(value * settings.steps_per_mm[Y_AXIS]);
plan_sync_position();
return(STATUS_OK);
case 32: // $32:设置旋转轴当前角度
sys.position[Z_AXIS] = lroundf(value * settings.steps_per_mm[Z_AXIS]);
plan_sync_position();
return(STATUS_OK);
case 33: // $33:设置大臂长度 L1 (mm)
SCARA_LINKAGE_1 = value;
return(STATUS_OK);
case 34: // $34:设置小臂长度 L2 (mm)
SCARA_LINKAGE_2 = value;
return(STATUS_OK);
#endif
通过 $30=85、$31=130、$33=200、$34=200 这样的指令,我们就可以在GRBL控制器软件中动态配置机械臂的几何参数和初始姿态,无需重新编译固件。
2.4 模式切换与G代码扩展 (gcode.c)
有时,我们可能希望直接控制关节角度,而不是空间坐标。我们可以通过扩展G代码来实现一个模式切换功能。例如,自定义一个 G95 代码,用于切换“坐标模式”和“直接角度模式”。
在 gcode.c 的 gc_execute_line() 函数中,找到G代码解析的 switch (int_value) 部分,添加一个新的 case。
// 在 gcode.c 中
case 61:
// ... 原有G61代码处理
break;
#ifdef ROBOT_ARM
case 95: // G95: 切换角度/坐标模式
angle_mode = !angle_mode;
// 可以在这里通过串口反馈当前模式
report_feedback_message(angle_mode ? MESSAGE_ANGLE_MODE_ON : MESSAGE_ANGLE_MODE_OFF);
break;
#endif
在“角度模式”下,发送给GRBL的 X, Y, Z 值将被直接解释为关节角度,绕过逆运动学计算。这在手动调试和校准阶段非常有用。
3. 硬件校准与运动失真解决方案
代码修改完成后,烧录到控制器(如Arduino Uno + CNC Shield),真正的挑战才刚刚开始。硬件世界的非理想特性会带来各种误差和失真。
1. 机械臂几何参数校准
理论上输入的 L1 和 L2 是设计值,但实际加工和装配必然存在误差。一个简单的校准方法是“两点法”:
- 手动控制机械臂末端到达一个精确的已知点A(比如用尺子量),记录下此时GRBL报告的各轴位置(角度)。
- 移动到另一个点B,再次记录。
- 将这两组
(实际坐标, 报告角度)代入运动学公式,可以反推出更精确的L1和L2,甚至基座偏移量。这个过程可以迭代进行。
2. 步距角 (steps_per_degree) 校准
这是影响精度的关键参数。校准步骤:
- 发送指令让某个关节(如大臂)转动一个理论角度,例如
G91 G1 X90 F100(假设X轴对应大臂,在角度模式下)。 - 用量角器或高精度指针,测量实际转动的角度。
- 计算误差比例:
理论角度 / 实际角度。 - 调整
$100(X轴steps/mm,此处为steps/degree)参数:新值 = 旧值 * 误差比例。 - 重复几次,直到误差可接受。
3. 解决回程间隙与结构松动 这是导致写字“重影”或线条不直的主要原因。机械臂连杆之间的连接处(舵机摆臂、联轴器)可能存在微小间隙。
- 机械方案:使用弹簧或橡皮筋,始终给关节一个单向的预紧力,消除间隙。这是最有效的方法。
- 软件方案:在运动规划中实现“单向逼近”。对于需要高精度的轨迹(如写字),规划所有运动路径,使得机械臂始终从同一个方向接近目标点,避免经过间隙区域。但这在复杂路径下实现难度较大。
4. 运动平滑性与抖动 GRBL的加速度和加加速度(Jerk)参数对机械臂尤为重要。过高的加速度会导致步进电机失步或机械臂剧烈抖动。
- 调整
$120、$121、$122:分别对应X, Y, Z轴的加速度(mm/s²)。对于机械臂,需要设置得比同等质量的直线电机更低,因为旋转运动的惯性矩更复杂。可以从100-500 mm/s²开始尝试。 - 调整
$110、$111、$112:最大速率(mm/min)。同样需要保守设置。 - 启用和调整
$112对应的加加速度参数:平滑的速度变化能显著减少抖动。
4. 从理论到实践:构建你的桌面写字机
现在,让我们把所有这些环节串联起来,完成一个写字机应用案例。
第一步:生成笔迹G代码 我们不需要从零开始写G代码。使用矢量图形软件(如Inkscape)和插件可以轻松完成。
- 在Inkscape中创建文字或图形。
- 使用
Extensions>Laserengraver>Laser这类插件(或专门用于笔式绘图机的插件),将路径转换为G代码。 - 关键点在于处理“抬笔”和“落笔”。通常插件生成的激光G代码用
M3(激光开)和M5(激光关)控制。我们需要将其替换为控制Z轴(或一个额外的舵机)的指令。例如:- 将
M3替换为G1 Z0(落笔)。 - 将
M5替换为G1 Z5(抬笔5mm)。 - 如果你的笔架由独立舵机控制,则需要将其映射到GRBL的某个辅助引脚(如冷却泵输出),并用
M8/M9控制。
- 将
第二步:系统初始化与对位
- 物理摆位:将机械臂笔尖移动到你想要建立的“工作原点”上方。确保所有关节运动顺畅,无干涉。
- 连接与配置:使用GRBL控制器(如Universal Gcode Sender, Candle)连接你的主板。
- 设置机械参数:
$33=200 ; 设置大臂长度200mm $34=200 ; 设置小臂长度200mm - 设置初始角度(对位):
- 用量角器测量当前大臂、小臂相对于各自零位的角度。
- 发送指令设置系统内部位置:
$30=85 ; 设置当前大臂角度为85度 $31=130 ; 设置当前小臂角度为130度 $32=0 ; 设置旋转轴角度为0度- 点击软件的“Zero”或“Set Zero”按钮,将当前点设为用户坐标系原点
(0,0,0)。
第三步:试运行与精细调整
- 先发送简单的G代码(如画一个正方形
G0 X10 Y10; G1 X50 Y10; G1 X50 Y50; G1 X10 Y50; G1 X10 Y10),观察机械臂运动轨迹是否与预期相符。 - 如果形状扭曲:检查并重新校准
steps_per_degree($100,$101,$102)。 - 如果笔画错位或重复:检查机械回程间隙,尝试增加弹簧预紧力。
- 如果运动抖动或噪音大:逐步降低加速度
$120-$122和最大速度$110-$112的值。
第四步:执行写字任务
- 加载修改好的G代码文件。
- 确保笔已安装好,处于抬笔状态。
- 点击“开始”或“Stream”按钮。
- 仔细观察第一笔的落点,如果位置偏差较大,可以暂停调整,重新对位。
整个过程就像在调试一台精密的仪器,需要耐心地在机械、电气和软件之间反复迭代。当机械臂终于稳稳地写下第一个清晰的笔画时,那种成就感是无与伦比的。这套基于GRBL魔改的方案,其优势在于你获得了一个功能极其丰富、社区支持强大的G代码控制核心。你可以利用GRBL原有的圆弧插补、加速度控制、坐标系变换、宏命令等功能,为你的机械臂开发更复杂的应用,比如简单的拾取放置、轨迹跟踪等。
魔改GRBL来控制机械臂,是一次对经典开源项目的深度致敬和创造性应用。它打破了“CNC固件只能用于直线运动”的思维定式,展示了嵌入式软件通过核心算法修改所能实现的强大适应性。这个过程中,你对运动控制、实时系统、C语言编程以及机械设计的理解都会上升一个层次。当然,这条路也布满了陷阱:运动学奇点、工作空间限制、实时性保障、误差累积……每一个问题都需要你仔细思考并解决。
我自己的第一台写字机,在写出像样的字之前,画了足足两天的“鬼画符”。问题最终锁定在一个看似不起眼的机米螺丝上,它没有完全锁紧同步带轮,导致了微小的周期性滑移。所以,当你遇到问题时,不妨从最基础的机械连接和电气接线查起。希望这篇详尽的指南能为你点亮一盏灯,让你在DIY智能机械臂的道路上少走些弯路。剩下的,就交给你的双手和创造力去实现了。
更多推荐
所有评论(0)