STM32六轴机械臂示教操作与轨迹复现教程
2026/9/4 21:36:30 网站建设 项目流程

做六轴机械臂项目做到中期以后,大家问得最多的往往不是“怎么让舵机转起来”,而是“怎样让它按照我想要的轨迹动起来”。如果要靠运动学逆解去编写一段手腕翻转、抓取、回位的组合动作,对数学和代码能力的要求都不低;但如果采用示教+轨迹复现的方式,整个过程就变成“人拖动小机械臂做一遍,单片机记下来,再让执行臂重放”,工程门槛会一下子低很多。

这一期教程围绕六自由度机械臂的示教操作与轨迹复现展开,控制核心使用 STM32F103 单片机。文章会先讲清楚整体架构和硬件接线,再给出可落地的 PWM 舵机驱动、ADC 角度采集、轨迹记录和回放状态机设计思路,最后补充常见故障排查和工程注意事项。无论你是在做课程设计、毕业设计,还是自己组一套六轴机械臂玩,这套思路都可以直接迁移。

1. 为什么六轴机械臂要做“示教操作 + 轨迹复现”

很多刚开始接触六轴机械臂的同学,第一反应是让机械臂完成“从 A 点抓取,放到 B 点”这样的任务。

要做到这一点,常规思路是:

  1. 给机械臂建立 DH 参数模型。
  2. 标定六个关节的零位和运动范围。
  3. 写出正运动学,把关节角度换算成末端位姿。
  4. 再写逆运动学,把目标位姿换算成六个关节角。
  5. 规划轨迹,生成每个时刻的关节位置。
  6. 最后让控制器逐点执行。

这套流程当然是最接近工业机器人开发的,但放在 DIY 六轴机械臂上,问题也很明显:结构件加工误差、舵机安装偏差、减速箱回差都会让理论模型和实际运动对不上。如果每一段动作都想靠代码去“算”出来,调试周期会非常长。

示教操作则绕开了复杂的运动学推导。它的核心思想是:

由人直接拖动一套与执行机械臂结构相似的小机械臂,各关节的角度传感器实时检测关节位置;主控把传感器数据转换为舵机 PWM 信号,驱动另一条机械臂同步跟随。

“拖动小机械臂”是人工示教比较常用的一种做法。好处有三个:

  1. 不需要逆解:操作人员无需关心关节角度,只要用手把教臂摆成目标姿态即可。
  2. 动作直观:教臂转多少,执行臂就按预设比例转多少,效果所见即所得。
  3. 可以快速产生轨迹:把示教过程中每个时刻的六个关节角度按固定时间间隔保存下来,就能形成一条完整轨迹;需要复现时,单片机按同样的时间间隔把角度点回放出来即可。

因此,本文要做的事情并不复杂:读取教臂角度、实时驱动执行臂、定时保存轨迹点、按记录顺序回放。这正好是 STM32F103 这类资源不多、但控制能力足够的 MCU 所擅长的任务。

2. 整体方案与工作流程

2.1 系统由哪几部分组成

整套系统可以划分为四个部分:

模块作用典型器件
示教小机械臂人手拖动,提供六个关节的目标角度六轴串联结构,各关节安装角度传感器
角度采集电路把机械角度变成 MCU 可读的电信号旋转电位器,或 AS5600 等磁编码器
主控板读取角度、处理按键、记录和回放轨迹STM32F103C8T6 最小系统板
执行机械臂按主控给出的 PWM 信号运动六个 PWM 数字舵机 + 结构件

这里要说明一下:很多同学会问,为什么不让执行臂直接被手拖动?

对于普通舵机驱动的机械臂,舵机内部有减速齿轮组,通电后反向拖动很容易导致舵机堵转、发热甚至烧毁。因此更安全的 DIY 示教方案是采用“主从式”结构:人拖动的是无动力或轻负载的教臂,执行臂只在复现阶段承担运动任务。如果想让执行臂本身支持直接拖动示教,需要特殊设计的电机抱闸释放机构和绝对值角度反馈,复杂度会高很多。

2.2 数据流向

用一句话概括系统流程:

人手拖动教臂 -> 六个关节电位器产生模拟电压 -> STM32F103 的 ADC 采样 -> 转换为舵机脉宽 -> 输出 PWM 驱动执行臂 -> 同时按时间间隔保存当前角度 -> 按键切换后按保存顺序回放

在实时跟随模式下,STM32F103 每 20ms 左右读取一次六路 ADC 值,并把这组值映射成 PWM 脉宽输出。这个循环不断重复,看起来就是执行臂跟随教臂同步运动。

在记录模式下,单片机除了实时跟随,还会把每一次的 PWM 脉宽保存到轨迹数组中。停止记录后,数组里保存的就是这段时间的完整动作序列。

在回放模式下,单片机从轨迹数组第一个点开始,每隔固定时间间隔输出一组 PWM 脉宽,直到全部回放结束。

3. 本期硬件组装与接线要点

3.1 材料清单

第二期既然要接入示教与复现功能,硬件上需要增加“教臂”部分。由于材料包的螺丝、铝件尺寸不一样,下面只给出通用清单:

类别说明
执行机械臂结构底座、腰部、大臂、小臂、腕部等六轴结构件
教臂结构件与执行臂关节数量一致的小型串联臂,可以用 3D 打印,也可以用型材搭建
角度传感器每个教臂关节一个旋转电位器,共 6 个
舵机每个执行关节一个 PWM 舵机,共 6 个
主控板STM32F103C8T6 最小系统板,或类似兼容板
电源舵机独立电源,例如 2S 锂电池加稳压模块;5V 主控供电
按键与指示灯用于切换“记录 / 回放 / 停止”
接线材料杜邦线或者排线、铜柱、扎带等

如果你手里的执行臂是“总线舵机”版本,则舵机接口不再是普通 PWM,通信协议不同,本文示例代码需要对应替换;以下内容以最适合入门学习的 PWM 舵机为例。

3.2 示教臂角度传感器装配

对于 DIY 主从示教,旋转电位器是最容易获取的角度传感器。装配时有几点需要注意:

  1. 电位器要和关节旋转轴同心,否则转动时会憋住。
  2. 每个关节最好有机械限位,避免电位器超过有效电气转角。
  3. 电位器两端分别接 3.3V 和 GND,中间抽头接 STM32 的 ADC 引脚。
  4. 如果某个关节方向反了,可以在软件里做反向映射,不一定要重新改结构。

在功能上,教臂并不需要很大的力矩,关节转动越顺滑越好。如果结构件转动不顺畅,首先检查轴承或轴套的安装同轴度。

3.3 执行臂舵机与供电

六自由度机械臂的六个舵机在启动和换向瞬间电流较大,最忌讳的是用 STM32 最小系统板的 USB 口直接给舵机供电。更合理的供电方式是:

电池或电源适配器 -> 稳压/降压模块 -> 舵机电源正负极 -> 舵机信号线接 STM32 PWM 引脚

还需要特别注意:舵机电源地、STM32 系统板地必须共地。很多“舵机不转、舵机乱抖、单片机复位”的问题,本质都是没有共地。

舵机的信号线是高电平脉冲信号,STM32 的 PWM 引脚输出就可以驱动。接线时不需要把所有舵机电源并到主控板上,正确的电源走线是直接从电源模块分出来,再让主控板单独取电。

3.4 引脚规划与避免 IO 冲突

STM32F103C8T6 的引脚资源有限。如果六个舵机 PWM 和六路 ADC 都要占用引脚,直接随便接很容易冲突。

一个比较直观的分配方案如下:

功能引脚对应外设
舵机 0~3 PWMPA0、PA1、PA2、PA3TIM2_CH1~CH4
舵机 4、5 PWMPA8、PA9TIM1_CH1、TIM1_CH2
教臂 ADC 通道 0~5PA4、PA5、PA6、PA7、PB0、PB1ADC1_IN4~IN9

这样安排后,PWM 引脚和 ADC 引脚不重叠,六个舵机和六路电位器都能接入。如果你手里的 PCB 已经定义好其他引脚,以你手上的原理图为准,重点是把引脚配置集中到宏定义或配置文件中,方便后续调整。

4. STM32F103 核心驱动代码解析

4.1 舵机 PWM 控制原理

常见的模拟或数字舵机,控制信号是周期约 20ms 的脉冲。脉冲宽度不同,舵机输出的角度不同:

脉宽典型角度
0.5ms0° 附近
1.5ms中间位置
2.5ms最大角度附近

不同厂家舵机的脉宽范围有差异,有的实际范围是 0.5ms~2.5ms,有的只有 1ms~2ms。建议先查舵机数据手册,然后在代码里把范围定义成宏,方便标定。

在 STM32CubeMX 中,将定时器时钟配置成 72MHz,预分频器设为 71,则计数器频率为:

72MHz / 72 = 1MHz

也就是计数器每计数一次为 1 微秒。如果把自动重载值 ARR 设为 19999,则 PWM 周期为:

(19999 + 1) * 1us = 20000us = 20ms

这正好符合舵机的 20ms 周期要求。此时设置 CCR 比较值:

CCR 值脉宽
500500us
15001500us
25002500us

因此,代码里不需要写很复杂的数学公式,直接“往 CCR 寄存器写入微秒值”即可。

4.2 六路舵机 PWM 输出代码

假设你在 STM32CubeMX 中已经生成了工程,定时器句柄分别是htim1htim2。可以写一个统一的舵机设置接口:

// servo.c #include "main.h" #include "servo.h" #define PULSE_MIN_US 500 #define PULSE_MAX_US 2500 void Servo_SetPulse(uint8_t idx, uint16_t pulse_us) { if (pulse_us < PULSE_MIN_US) pulse_us = PULSE_MIN_US; if (pulse_us > PULSE_MAX_US) pulse_us = PULSE_MAX_US; switch (idx) { case 0: __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, pulse_us); break; case 1: __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, pulse_us); break; case 2: __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_3, pulse_us); break; case 3: __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_4, pulse_us); break; case 4: __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, pulse_us); break; case 5: __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, pulse_us); break; default: break; } } void Servo_InitAll(void) { HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_4); /* TIM1 是高级定时器,需要使能主输出 */ __HAL_TIM_MOE_ENABLE(&htim1); HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2); for (uint8_t i = 0; i < 6; i++) { Servo_SetPulse(i, 1500); } }

代码里的“1500”表示舵机先回到 1.5ms 脉宽中位。实际项目中,如果机械臂结构安装时零位并不在舵机中位,这里需要按你的机械零位修改。

4.3 六路 ADC 角度读取

如果教臂使用电位器,ST 的 ADC1 可以依次扫描多个通道。为了减少抖动,我习惯对每个通道连续读几次然后取平均。下面是一个轻量实现:

// teach_adc.c #include "main.h" #include "teach_adc.h" static uint16_t adc_channel_map[6] = { ADC_CHANNEL_4, // PA4 ADC_CHANNEL_5, // PA5 ADC_CHANNEL_6, // PA6 ADC_CHANNEL_7, // PA7 ADC_CHANNEL_8, // PB0 ADC_CHANNEL_9 // PB1 }; uint16_t ReadOneChannel(uint16_t channel) { ADC_ChannelConfTypeDef sConfig = {0}; uint16_t value = 0; sConfig.Channel = channel; sConfig.Rank = 1; sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; HAL_ADC_ConfigChannel(&hadc1, &sConfig); HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, 10); value = HAL_ADC_GetValue(&hadc1); HAL_ADC_Stop(&hadc1); return value; } void TeachArm_ReadAll(uint16_t *raw) { uint32_t sum = 0; for (uint8_t ch = 0; ch < 6; ch++) { sum = 0; for (uint8_t sample = 0; sample < 5; sample++) { sum += ReadOneChannel(adc_channel_map[ch]); } raw[ch] = sum / 5; } }

这里之所以每读一个通道都执行HAL_ADC_Stop(),是为了让下一次切换通道时不要沿用上一次的配置。在 6 个电位器、采样率要求不高的场景下,这种写法简单可靠。

5. 示教、记录与回放软件设计

5.1 状态机设计

机械臂示教不只是一个“循环读角度”的过程,还需要考虑“什么时候开始记录”“什么时候停止”“什么时候回到轨迹首点”“什么时候开始回放”。使用状态机会清晰很多。

可以定义这样几种模式:

模式含义
MODE_FOLLOW实时跟随教臂运动,但不记录
MODE_RECORD实时跟随,同时按周期保存轨迹点
MODE_BACK_TO_START从当前姿态平滑回到轨迹首点
MODE_PLAYBACK逐点回放轨迹

按钮的功能可以这样设计:

  • 按键 1:进入记录。如果已经在记录,则停止记录,并进入“待机”或“跟随”模式。
  • 按键 2:开始回放。如果采集到的轨迹点数为 0,则按下无效。
  • 按键 3:急停或复位,所有舵机输出中位脉宽。

采用这种划分后,主循环的逻辑会很清楚。

5.2 轨迹数据结构

使用 STM32F103C8T6 时,RAM 只有约 20KB。如果六个关节都保存为uint16_t,每个轨迹点需要:

6 * 2 = 12 字节

如果保存 800 个点,总容量为 9600 字节,约占 RAM 的一半。示例中先按这个规模设计:

// trajectory.h #define SERVO_NUM 6 #define MAX_TRAJ_POINT 800 typedef struct { uint16_t pulse[SERVO_NUM]; } TrajPoint; extern TrajPoint g_traj[MAX_TRAJ_POINT]; extern volatile uint16_t g_traj_len;

当然,800 个点通常不够保存几分钟的长轨迹。如果希望断电保存或保存更长时间,可以外接 SPI Flash、SD 卡,或者用串口把数据发送到上位机保存。这里先把 RAM 方案跑通,再考虑扩展存储。

5.3 实时跟随与轨迹记录

下面这段代码是核心循环逻辑:

// main.c 中的循环片段 void MainLoop_Tick(void) { static uint32_t last_record_tick = 0; static uint32_t last_follow_tick = 0; uint32_t now = HAL_GetTick(); if (g_mode == MODE_FOLLOW || g_mode == MODE_RECORD) { if (now - last_follow_tick >= 20) { uint16_t raw[6]; TeachArm_ReadAll(raw); for (uint8_t i = 0; i < 6; i++) { g_current_pulse[i] = MapRawToPulse(i, raw[i]); Servo_SetPulse(i, g_current_pulse[i]); } last_follow_tick = now; } } if (g_mode == MODE_RECORD) { if (now - last_record_tick >=

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

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

立即咨询