简介:面向嵌入式与自动化专业学生,这份基于STM32的Marlin三轴机械臂控制程序源码,可用于毕业设计、期末大作业或课程设计。项目内含详细注释,代码思路清晰,新手也可以快速理解,其中还涉及STM32定时器、ADC、I2C等外设驱动,适合深入学习机械臂运动控制。压缩包共有168个文件,大小约3.38MB,文件类型以C语言源文件(.c)和头文件(.h)为主,同时还包含Keil工程配置文件(.uvprojx/.uvoptx)、编译输出文件(.hex/.axf)、构建依赖文件(.dep/.map)及少量备份文件,便于直接打开工程进行编译和部署。这套项目由作者独立编写,获得98分的高分,经过严格调试,系统功能完善、界面直观,运行稳定,可直接作为课题原型或功能基础。目前已有152人学习,适合需要快速积累机械臂控制经验,或想在毕设、大作业中快速落地项目的同学参考。
1. 为什么“基于STM32的Marlin三轴机械臂”不是简单编译烧录
期末大作业答辩时,老师问过最多的一个问题:“Marlin是给3D打印机写的固件,你怎么让它去控制机械臂?”如果你只回答“改配置”,八成会被追问到卡壳。Marlin内核里的G代码解析、运动规划、步进电机控制都是现成的,但它不知道自己拖动的轴是转动关节还是平移直线。这篇文章从三轴机械臂的关节控制这个需求出发,把硬件选型、运动学逆解、Marlin移植、速度规划边界和调参方法逐层讲透,最后落到一条可执行的路径上:让上位机负责逆解,让STM32上的Marlin负责出力。这篇博文的读者定位是正在做STM32相关期末大作业或毕业设计的同学,假设你手里有一块普通开发板、三台步进电机或舵机,以及愿意熬夜调参的耐心。
2. 三轴机械臂的硬件架构与STM32最小系统
2.1 舵机臂还是步进臂:先按机械结构选驱动方案
三轴机械臂从驱动方式上分两种主流做法。一种是舵机直接驱动关节,常见于教学用小型机械臂,舵机转角由PWM占空比决定,控制代码简单,但精度和重复定位能力有限,重载下容易抖动。另一种是步进电机加减速器驱动关节,位置由脉冲数决定,没有累计误差,适合做轨迹演示类的大作业。Marlin这两类都能管:步进轴走的是内置planner的梯形加减速,舵机走的是M280指令或SERVO_ANGLES的PWM扩展。
从期末大作业的容错率讲,我更倾向于让你先用舵机臂打通流程,再用步进臂做精化。因为舵机臂的逆解即使有微小误差,舵机自己咬合住不会累计;而步进臂一旦丢步,整条轨迹都会往后偏移。下面是两者在三个维度的直观对比:
| 对比项 | 舵机直接驱动 | 步进电机加减速器 |
|---|---|---|
| 位置反馈 | 无,开环PWM | 开环脉冲计数,可加编码器闭环 |
| 控制系统 | 定时器PWM | 定时器脉冲+方向引脚 |
| 精度 | 约1到2度 | 取决于细分数和减速比 |
| Marlin适配成本 | 需要额外写舵机扩展 | 原生步进轴支持 |
| 典型成本 | 几十元SG90/MG996R | 42步进电机加驱动板 |
如果你的机械臂是打印件或者铝型材小臂,臂长在20厘米以内,用MG996R级别的舵机就够了。但要注意,普通舵机的死区电压和抖动会让末端定位出现肉眼可见的震颤,答辩演示时很掉分。所以大家都默认步进方案更“像工程”,但代价是机械结构要留出电机安装位和同步带轮。
2.2 STM32主控选型与晶振电容参数
STM32的型号选择直接影响Marlin能不能跑起来。Marlin 2.x对STM32F103和STM32F407都有现成支持,F103的RAM只有48到64KB,跑完整版Marlin加LCD容易吃力,建议关闭不需要的模块;F407性能强很多,但如果你的大作业只是三轴联动,F103性价比已经足够。市面上还有一种做法是直接选APM32F103或GD32F103,它们和STM32F103的引脚和寄存器基本兼容,PlatformIO里仍然按stm32f103目标编译,烧录器选ST-Link或者J-Link都能识别。
晶振部分反而是很多同学忽略的坑。STM32F103最小系统常用8MHz无源晶振加两个负载电容,电容值不是随便挑的。负载电容C_L和两个匹配电容C1、C2的关系是:
$$C_L = \frac{C1 \times C2}{C1 + C2} + C_{\rm stray}$$
$C_{\rm stray}$是PCB走线和芯片引脚引入的杂散电容,经验值在3到5pF。如果晶振规格书要求C_L=18pF,取C_stray=4pF,则C1=C2要接近25到30pF,市面上常用27pF。如果把负载电容选成22pF再配18pF的晶振,起振时间会变长,极端情况下Marlin启动时串口打印卡死在时钟初始化之前,你拿到手的源码再怎么改配置都没用。
2.3 Marlin在STM32上的启动流程概貌
Marlin在STM32上的启动流程和裸机程序不一样,它是跑在HAL层之上的。先从SystemInit配置时钟树,然后进入Marlin的setup(),这个setup里依次完成串口初始化、EEPROM参数加载、步进电机引脚初始化、运动缓冲区planner_init、以及热端和风扇等外设初始化。任何一步出错,串口上可能只输出一行字符就挂起。调试时先在main.cpp里加打印,确认启动到哪一步,再沿这个方向查配置,这比瞎猜有用得多。
3. 运动学逆解:从末端坐标到三个关节角度
3.1 DH参数与正运动学建模
三轴机械臂最常见的构型是底座旋转关节加肩关节加肘关节,末端还有一个固定姿态的抓手。DH参数是把每个关节的转动和平移通过四个量描述出来的标准方法。对这样一个三轴臂,可以用标准DH表建模:
| 关节 i | $\theta_i$ | $d_i$ | $a_i$ | $\alpha_i$ |
|---|---|---|---|---|
| 1 | $\theta_1$ | $d_1$ | $a_1$ | $\pi/2$ |
| 2 | $\theta_2$ | 0 | $a_2$ | 0 |
| 3 | $\theta_3$ | 0 | $a_3$ | 0 |
$a_1$是底座到肩关节的水平偏置,$a_2$是大臂长度,$a_3$是小臂长度。正运动学就是已知三个$\theta$求末端位置,这个用三角函数手推也行:先算机体坐标下的水平距离$r = a_2\cos\theta_2 + a_3\cos(\theta_2+\theta_3)$,然后$x = r\cos\theta_1$,$y = r\sin\theta_1$,$z = d_1 + a_2\sin\theta_2 + a_3\sin(\theta_2+\theta_3)$。
3.2 三轴逆运动学的C语言实现
期末大作业真正要用的是逆解:给定机械臂末端要到达的$x,y,z$,反推出三个关节角。这个逆解有解析解,不涉及数值迭代,适合在STM32另一个核、上位机或者Python脚本里算好再喂给Marlin。下面是C语言版本的核心函数:
// 三段式三轴机械臂逆解,输入末端位置,输出三个关节角 // 返回值:0表示在可达空间内,-1表示超限 int inverse_kinematics(float x, float y, float z, float *theta1, float *theta2, float *theta3) { const float d1 = 100.0f; // 底座高度 mm const float a2 = 120.0f; // 大臂长度 mm const float a3 = 100.0f; // 小臂长度 mm *theta1 = atan2f(y, x); // 底座转角 float r = sqrtf(x*x + y*y); // 水平投影距离 float s = z - d1; // 竖直方向目标高度 float d = sqrtf(r*r + s*s); // 末端到肩关节距离 // 距离合法性检查:三角形两边之和大于第三边 if (d > (a2 + a3) || d < fabsf(a2 - a3)) { return -1; } float alpha = atan2f(s, r); // 目标方向与水平面的夹角 float cos_beta = (a2*a2 + d*d - a3*a3) / (2.0f * a2 * d); float beta = acosf(cos_beta); // 大臂与射线的夹角 *theta2 = alpha + beta; // 肩关节角 float cos_gamma = (a2*a2 + a3*a3 - d*d) / (2.0f * a2 * a3); float gamma = acosf(cos_gamma); *theta3 = -(PI - gamma); // 肘关节角,负号表示屈肘方向 return 0; }这段代码里最关键的是第二条余弦定理的用法:先求末端到肩关节的距离$d$,利用余弦定理求出大臂与连线的夹角$\beta$,然后让$\theta_2 = \alpha+\beta$。$\alpha$是末端矢量的仰角,$\beta$补偿大臂自身方向,两者相加就是肩关节相对垂直轴的实际转角。很多同学会直接把$\alpha$当$\theta_2$,结果末端高度整体偏低一截。肘关节角$\theta_3$用第三条边关系求,注意角度符号要跟实际机械装配方向一致。
3.3 末端笛卡尔坐标与Marlin虚拟轴的映射
Marlin自己没有机械臂关节模型,它只知道X、Y、Z三个轴各自前进多少毫米。所以这里有两种做法。第一种是直接给关节臂做“轴映射”:把底座轴当X、肩关节当Y、肘关节当Z,G1指令里的X值就是底座角度的度数。这种方法实现最快,但缺陷很明显,Marlin会对三个虚拟轴做线性插值,导致关节空间到末端空间的轨迹严重变形,画圆时末端走成椭圆形。
我更推荐的第二种做法是:在PC或另一颗MCU上先对目标轨迹做小步长离散,每个点都调用一次逆解,把得到的关节角写进G代码文件,再烧到Marlin里“盲跑”。也就是说逆解发生在上位机,Marlin只忠实地执行运动规划。把这段话写进答辩文档里,老师基本就不会再问机械臂轨迹失真这类基础问题了。
4. 将Marlin 2.x移植到STM32的最小可运行配置
4.1 源码骨架与板级描述文件
Marlin 2.x的源码结构里,和硬件强相关的部分集中在Marlin/src/pins/目录下。每个板子对应一个pins_xxx.h,里面定义了步进电机引脚、使能引脚、限位开关引脚和串口的映射。你要做的第一件事不是改Configuration.h,而是找到接近你手里PCB的板级文件。
以最常见的RAMPS 1.4结构为例,MDA、MDB和MDC三组排针映射到STM32的不同GPIO。如果你自己画板或者用转接板,需要新写一个pins_custom.h,里面按现有模板把X_STEP_PIN、X_DIR_PIN、X_ENABLE_PIN这些宏改成自己的引脚号。这个过程建议用逻辑分析仪实测,因为不少国产板的丝印标号和PCB走线对不上,只看原理图会烧驱动板。
4.2 用PlatformIO编译并烧录STM32目标
Marlin从2.0开始推荐用PlatformIO做构建系统,它比Keil省心的地方在于会自动拉取正确的HAL和工具链。下面是一个最简的platformio.ini示例,针对STM32F103ZET6:
[env:stm32f103zet6] platform = ststm32 board = genericSTM32F103ZE framework = arduino upload_protocol = stlink build_flags = -DHAL_CAN_MODULE_ENABLED ; 关闭热床等无关外设以减小Flash占用编译前先确认Configuration.h里SERIAL_PORT设为1或2,对应你实际使用的串口,否则G代码发送过去Marlin没有任何反应。烧录用ST-Link最常见,upload_protocol = stlink表示通过SWD接口写入,接线只要SWDIO、SWCLK、GND和3.3V四根线。第一次烧录如果提示Target not found,优先检查BOOT0引脚电平而不是怀疑源码。
4.3 Configuration.h里必改的关键参数
Configuration.h是Marlin里最敏感的文件,参数改错轻则不动,重则在上电瞬间烧驱动。以下这几个宏是三轴机械臂跑通前必须校准的:
// 控制板型号,直接影响pins_xxx.h的加载 #define MOTHERBOARD BOARD_RAMPS_14_EFB // 每个轴单位脉冲对应的移动量或角度 #define DEFAULT_AXIS_STEPS_PER_UNIT { 80, 80, 80, 445 } // 最大进给率,步进臂这里先保守一点 #define DEFAULT_MAX_FEEDRATE { 200, 200, 200, 30 } // 加速度,加速度过大容易导致机械结构振动 #define DEFAULT_MAX_ACCELERATION { 300, 300, 300, 100 }DEFAULT_AXIS_STEPS_PER_UNIT是步进电机细分和后级减速比的综合结果。计算公式是:驱动细分数乘以电机步距角对应的脉冲数,再除以减速比和传动比。比如42步进电机1.8度,16细分,则一圈需要3200个脉冲,如果输出轴有1:5减速器,那么输出轴转一圈是16000个脉冲,对应360度,那么每度约44.44个脉冲。这个值如果不准,逆解算得再精确,机械臂末端也会越走越偏。DEFAULT_MAX_FEEDRATE设成200意味着每秒最多200个“单位”,在你确认步进电机的实际能力前,宁小勿大。
提示:修改完Configuration.h后重新编译,第一次上电最好用手捏住机械臂关节,防止参数错误导致关节猛甩撞到限位结构。
5. 速度是怎么被规划的:加减速边界与PID的适用位置
5.1 开环步进轴不需要PID,但需要加速度规划
很多同学一听到运动控制就想到PID,但在裸步进驱动的三轴机械臂里,Marlin的planner并不对位置环做PID计算。它做的是梯形加减速:一个运动段被分成加速、匀速、减速三段,每步脉冲的间隔时间按下一帧的目标速度计算出来。PID如果要介入,只能出现在闭环步进驱动器的电流环或者舵机的位置反馈环,而不是Marlin的用户态。
所以你看Marlin源码会发现它有三组关键缓存:block_buffer里保存了每个运动段的起始速度和结束速度、加速度、脉冲总数。planner每次收到G1指令就计算能不能在当前速度和下一段速度之间完成加减速,不能就主动降速。这种规划方式的好处是CPU开销极小,适合F103这种主频不高的MCU。
5.2 用M503、M203、M201和M566读取并调整运动参数
Marlin的EEPROM里保存着运动参数,不需要重新编译就能在线调整。下列指令在串口终端里可以随时执行:
M503 ; 打印当前所有参数 M203 X200 Y200 Z200 ; 设置最大进给率 M201 X300 Y300 Z300 ; 设置最大加速度 M204 P500 T1000 ; 设置打印和移动加速度 M566 X10 Y10 Z10 ; 设置瞬时速度变化上限,即jerk M500 ; 保存到EEPROMM566的jerk参数对机械臂影响很大。jerk是相邻两个运动段之间的速度突变上限,设太大会在关节上形成冲击,设太小会让轨迹变得绵软无力。对于三轴机械臂,我一般从5到20开始试,观察哪个值会让机架产生明显共振,然后回退到共振点以下的70%。这些参数改完不需要重启,直接重新发送G1指令就生效,非常适合现场调。
5.3 机械臂用闭环舵机时自己补一个位置PID
如果换用带角度反馈的舵机或直流减速电机方案,Marlin原生就不够用了,你需要在STM32的定时器中断里自己补位置环。最简的位置PID代码如下:
// 位置环PID,控制周期1ms,输入目标角度和目标当前角度 #define KP 2.5f #define KI 0.05f #define KD 0.1f static float integral = 0.0f, prev_error = 0.0f; float pid_update(float target, float current) { float error = target - current; integral += error * 0.001f; if (integral > 1.0f) integral = 1.0f; // 积分限幅 if (integral < -1.0f) integral = -1.0f; float derivative = (error - prev_error) / 0.001f; prev_error = error; return KP * error + KI * integral + KD * derivative; }这里控制周期固定为1毫秒,用大臂角度作为反馈量输出PWM占空比。调试顺序是先使KP让舵机能回正,再逐步加KD抑制过冲,最后加KI消除稳态误差。注意反馈源最好用磁编码器或电位器,不要用舵机内部的模拟电压直接进ADC,噪声会让PID输出抖成一团。
6. 实战增量:用Python生成画圆G代码并让机械臂动起来
6.1 上位机逆解后按小线段下发
画圆是机械臂轨迹演示的常见项目。因为Marlin不懂关节角,正确的姿势是在PC端用Python逐点计算逆解后生成G代码文件:
import math a2 = 120.0 # 大臂长度 a3 = 100.0 # 小臂长度 d1 = 100.0 # 底座高度 def ik(x, y, z): theta1 = math.atan2(y, x) r = math.hypot(x, y) s = z - d1 d = math.hypot(r, s) if d > a2 + a3 or d < abs(a2 - a3): return None alpha = math.atan2(s, r) beta = math.acos((a2*a2 + d*d - a3*a3) / (2*a2*d)) theta2 = alpha + beta gamma = math.acos((a2*a2 + a3*a3 - d*d) / (2*a2*a3)) theta3 = -(math.pi - gamma) return theta1, theta2, theta3 with open("circle.gcode", "w") as f: f.write("G21\n") # 让每个关节先回到基准角度 j0, j1, j2 = ik(100.0, 0.0, 180.0) f.write(f"G1 X{j0:.3f} Y{j1:.3f} Z{j2:.3f} F300\n") # 画半径20mm的圆,每3度一个点 for deg in range(0, 361, 3): x = 120.0 + 20.0 * math.cos(math.radians(deg)) y = 20.0 * math.sin(math.radians(deg)) z = 180.0 j0, j1, j2 = ik(x, y, z) f.write(f"G1 X{j0:.3f} Y{j1:.3f} Z{j2:.3f} F500\n")这段脚本把末端坐标换算成关节角后以X、Y、Z名义坐标的形式记录。发送到Marlin后,planner会对这三个“角度轴”做直线插补,由于每段只有3度,机械臂末端在真实空间里近似走圆。如果要追求高精度轨迹,需要把步长缩小到1度甚至更细,代价是G代码体积变大,但F103处理几百个G1指令完全不成问题。
6.2 上电归零与软限位
机械臂第一次上电不归零,关节角可能停在任意位置,直接执行G代码会甩飞末端。Marlin里有两种归零方式:传统限位开关和软件限位。多数三轴大作业买不到合适的光电限位,也来不及设计支架,这时我建议直接改Configuration.h里的SOFT_ENDSTOPS:
#define SOFT_ENDSTOPS #define MIN_SOFT_ENDSTOP_X -90 #define MAX_SOFT_ENDSTOP_X 90 #define MIN_SOFT_ENDSTOP_Y 0 #define MAX_SOFT_ENDSTOP_Y 170 #define MIN_SOFT_ENDSTOP_Z -45 #define MAX_SOFT_ENDSTOP_Z 120软限位的作用是在planner里拦截超出范围的G1指令,避免电机撞机。但软限位不解决关节初始位置未知问题,所以更稳妥的做法是在机械臂每个关节上留一个手动对准刻度:上电后用G92命令把当前角度设成零点。这个操作在答辩里也说得通,毕竟工业机械臂也有手动示教零点这一工序。如果一定要自动归零,就在底座加一个微动开关,让Marlin的HOME流程驱动步进电机往负方向找开关,这个改起来要动pins文件里的限位引脚定义。
6.3 让源码包看起来像“高分”作业的三个细节
大作业源码里除了工程文件和任性改过的Configuration.h之外,最好补一份调参记录表,记录每组加速度和jerk下的轨迹变形现象,这是工程能力最强的证据。其次,逆解代码和Marlin移植代码要分开放在不同目录,README里写清楚从上位机到固件的完整数据流,这样老师能一眼看懂你的系统设计。最后,把可执行文件的编译固件版本和PlatformIO环境锁进platformio.ini,避免答辩时在对方电脑上重新编译出现依赖不一致。真正做到这一步,你就可以对齐老师最关心的可复现性:给他同一套源码、同一份编译环境,就能复现你的整套机械臂运动过程。
本文还有配套的精品资源,点击获取