行业资讯
📅 2026/9/3 3:15:44
基于STM32F103的六自由度机械臂示教操作与轨迹复现实现指南
1. 这篇文章真正要解决的问题很多做机械臂项目的开发者在第一阶段会卡在同一个地方舵机单独转动没问题但把六个舵机组装成机械臂之后想让机械臂“听话”地走一段轨迹代码逻辑就乱成一团。更常见的情况是机械臂只能按预先写死的角度执行动作换一个摆放位置、换一个抓取目标整段程序就要重写。标题里提到的“六自由度机械臂”“示教操作”“轨迹复现”本质上就是在解决这个问题。所谓示教操作就是让开发者或操作者直接用手拖动机械臂末端把机械臂摆到想要的位置系统记录下每个关节的角度变化轨迹复现则是把刚才记录下来的关节数据重新播放出来让机械臂自己重复走一遍。这套流程在工业机器人领域已经非常成熟但在 STM32F103 这种入门级单片机上实现很多人容易陷入“代码烧进去没反应”“记录下来的轨迹复现出来完全变形”这类困境。本文要讲清楚三件事六自由度机械臂的核心结构以及为什么示教比直接编程更适合新手。基于 STM32F103 的示教操作与轨迹复现系统的完整实现思路不依赖昂贵视觉方案只靠舵机反馈和定时器就能跑通。组装、接线、固件调试中最常见的坑以及如何在低成本硬件上把轨迹误差控制在可接受范围。如果你手里正有一套六自由度机械臂散件或者准备用 STM32F103 做一个类似的小项目这篇文章值得收藏备用。2. 六自由度机械臂的基础概念与结构拆解2.1 什么是“六自由度”自由度的概念可以简化理解为“机械臂能独立运动的方向数量”。每个自由度通常对应一个可以旋转的关节。六自由度机械臂意味着它有六个独立控制的旋转关节理论上末端执行器可以在三维空间内到达一定范围内的任意位置和姿态。常见的六自由度机械臂构型是底座旋转、肩部俯仰、肘部俯仰、前臂旋转、腕部俯仰、腕部旋转。这种构型模仿人类手臂前三个关节主要决定末端位置后三个关节主要决定末端姿态。从 STM32F103 的控制角度看六个自由度对应六路 PWM 输出。每一个关节由一个舵机驱动MCU 通过输出不同占空比的 PWM 信号控制舵机旋转到指定角度。对 STM32F103 来说这个任务的关键不在于“能不能输出六路 PWM”而在于“六路 PWM 的更新是否同步、控制周期是否稳定”。如果六个舵机的控制信号更新时刻不一致机械臂运动时就会出现肉眼可见的抖动和卡顿。2.2 示教操作与传统编程控制的区别传统机械臂运动控制是“先算后动”。你需要先通过运动学正解和逆解计算出机械臂从 A 点运动到 B 点时每个关节的目标角度然后把角度数组写进代码里。这种方式的缺点是正向运动学计算相对简单但逆向运动学涉及大量三角函数和矩阵运算对新手不友好。机械臂的实际安装误差、舵机零位偏差会导致理论计算的角度和实际到达的位置不一致。每次更换任务场景都要重新计算和修改代码。示教操作则完全绕开了运动学计算。操作者直接用手把机械臂末端拖动到目标位置单片机通过读取每个关节舵机的反馈信号实时记录关节角度。这个过程相当于“告诉机械臂怎么走”而不是“告诉机械臂每个点在哪里”。从工程实践角度看示教操作把“路径规划”从数学问题变成了“记录与回放”问题特别适合 DIY 级别的机械臂项目。工业机器人厂商之所以在昂贵设备上做示教器是因为示教能大幅降低编程门槛和调试时间。STM32F103 虽然性能有限但完成这类记录与回放任务绰绰有余。2.3 舵机选择的常见误解很多新手在组装机械臂时把注意力全部放在 STM32F103 的代码上忽略了舵机本身的差异。实际上影响示教轨迹复现精度的第一大因素不是单片机而是舵机的类型和质量。常见的舵机类型有两种类型信号特点角度反馈能力适合场景普通 PWM 舵机单向控制只接收 PWM 信号无角度反馈只能反推目标角度简单位置控制带反馈的串行总线舵机通过串口发送指令和读取状态可以读取当前实际角度、温度和电压示教操作、轨迹回放、力反馈场景如果只有普通 PWM 舵机要实现“拖动机械臂记录轨迹”会比较麻烦因为需要额外安装角度传感器比如电位器或编码器来感知关节角度。如果使用带位置反馈的串行舵机单片机可以直接读取每个舵机的当前角度天然适合示教操作。从材料看这里强调的是“拖动小机械臂实现示教操作”这意味着机械臂关节最好选用带有角度反馈能力的舵机或者在每个关节安装独立的角度检测装置。具体方案取决于你的材料包配置后面会给出两种实现方式的对比。3. 基于 STM32F103 的示教系统核心原理3.1 为什么 STM32F103 依然适合这类项目STM32F103 系列是意法半导体推出的经典 ARM Cortex-M3 内核单片机主频最高 72MHz内置多种定时器、ADC、UART、SPI、I2C 等外设资源在入门级 MCU 中属于非常均衡的选择。对于六自由度机械臂项目来说STM32F103 的核心优势有几点定时器资源丰富。STM32F103 有多个高级定时器和通用定时器可以输出多路 PWM。合理分配定时器通道可以同时驱动六个舵机。中断响应快。示教模式下需要高频读取关节角度数据串口中断和定时器中断可以保证数据采集的实时性。生态成熟。HAL 库和标准外设库的代码示例非常多遇到问题很容易查到解决方案对新手友好。成本低。整颗芯片和最小系统板的价格都很便宜适合用来验证机械臂控制算法。当然STM32F103 也有局限。它的 RAM 通常只有 20KB 到 64KBFlash 从 64KB 到 512KB。如果要记录复杂的运动轨迹不能无限采样必须考虑存储策略。这一点在后面轨迹记录模块中会详细说明。3.2 示教操作的工作流程整套示教与复现系统的逻辑可以拆成三个状态待命状态、示教记录状态、轨迹复现状态。在待命状态下机械臂所有舵机处于释放或轻力矩状态操作者可以自由拖动机械臂。单片机持续以固定频率读取每个关节的当前角度但在内存中暂存最新一帧数据并不写入轨迹缓冲区。当操作者按下“开始记录”按钮后系统进入示教记录状态。此时单片机以固定采样周期比如每 20ms把六个关节的角度数据打包成一帧依次存入轨迹缓冲区。操作者拖动机械臂完成一段动作后再次按下按钮或发出停止指令记录结束。在轨迹复现状态下单片机从轨迹缓冲区逐帧读取角度数据通过 PWM 或串口指令驱动六个舵机按相同的时间顺序运动。只要采样周期足够短、舵机响应足够快机械臂就能近似还原示教时的动作。这个流程和工业机器人示教器的逻辑是完全一致的区别只是工业机器人通常通过示教器按键控制机械臂移动而 DIY 项目直接用手拖动。两者的共同点是“记录关节空间下的轨迹并按序回放”完全不涉及笛卡尔空间下的路径规划。3.3 角度数据采集的两种方案对比方案一使用带反馈的串行总线舵机。这种方案的硬件连接非常简单。六个舵机通过串口总线串联只需要两根电源线和一根信号线。读取角度时单片机向舵机发送读取位置指令舵机回传包含角度、电压、温度等信息的帧数据。由于串行总线舵机内部集成了角度传感器示教时不需要额外安装检测装置。这种方案的缺点是响应速度受限于总线波特率和通信协议。如果六个舵机逐一查询单个舵机查询周期较长会限制最高采样率进而影响轨迹记录的细腻程度。方案二使用 PWM 舵机加外部角度传感器。这种方案在每个关节安装一个电位器或编码器作为角度反馈元件。电位器输出模拟电压由 STM32F103 的 ADC 采集并换算成角度编码器则通过定时器或外部中断读取脉冲数。这种方案的优点是角度读取速度快可以做到每毫秒刷新一次所有关节角度缺点是硬件结构复杂每个关节都要额外布线且安装误差容易导致角度换算不准确。从 DIY 项目角度看两种方案都可以做出“拖动机械臂实现示教”的效果。选择哪种取决于材料包里舵机的类型。如果你手里的舵机本身是带反馈的总线舵机建议直接走方案一省去大量机械改装工作如果只有普通 PWM 舵机则可以通过增加角度传感器来补齐反馈链路。4. 六自由度机械臂组装与硬件接线4.1 机械结构组装要点六自由度机械臂的机械结构组装虽然不涉及代码但直接影响后续示教精度这里值得单独说明。组装时最容易出现的问题有三个第一关节紧固件松动。机械臂运动时关节处会承受较大的力矩。如果舵机输出轴与连杆之间的固定螺丝没有拧紧舵机转动时连杆会产生空程导致轨迹复现时末端位置偏移。建议在所有关节紧固件安装完成后统一检查一遍扭力并在关键部位使用螺纹胶。第二舵机零位没有校准。每个舵机的 90° 位置在生产时都存在微小差异。如果直接把舵机臂安装到输出轴上很难保证六个关节在初始状态下处于机械零位。推荐的做法是先给舵机发送中线 PWM 信号让舵机回到理论 90° 位置然后再安装舵机臂和连杆。这样可以避免后续程序中的角度偏移修正。第三电源线过细。六自由度机械臂在运动瞬间的电流峰值可能达到数安培。如果使用过细的杜邦线给所有舵机供电线材压降会导致舵机供电电压不足轻则抖动重则复位。建议使用 AWG20 以上的硅胶线作为舵机主电源线并将 STM32F103 控制板的电源与舵机电源分开只在信号线上共地。4.2 基于 STM32F103 的接线规划无论采用 PWM 舵机还是串行总线舵机STM32F103 这边的电路规划都需要提前做。如果采用 PWM 舵机方案需要规划六路 PWM 输出和若干路 ADC 输入。常见的定时器通道分配如下舵机STM32F103 定时器通道对应引脚底座旋转TIM2_CH1PA0肩部俯仰TIM2_CH2PA1肘部俯仰TIM2_CH3PA2前臂旋转TIM2_CH4PA3腕部俯仰TIM3_CH1PA6腕部旋转TIM3_CH2PA7这里特别提醒一点STM32F103 的多个定时器可以同时工作但如果六个舵机的 PWM 信号来自不同定时器不同定时器之间可能存在微小的相位差。解决方法是让同一个定时器的多个通道分别控制不同舵机保证它们共享同一个计数周期更新时刻步调一致。如果采用串行总线舵机方案接线更简单。STM32F103 通过 USART 发送舵机控制指令再通过 UART 接收舵机反馈数据。需要注意电平转换问题部分串行总线舵机使用 TTL 电平可以直接与 STM32 连接部分舵机使用 RS485 总线则需要外接 RS485 转 TTL 模块。4.3 按键与状态指示电路示教和复现需要至少两个触发信号开始/停止记录、开始复现。最简单的方案是使用两个独立按键连接到 STM32F103 的 GPIO 引脚。按键电路建议按下拉方式连接按键一端接 3.3V另一端通过 10kΩ 电阻接地同时接到 GPIO。检测时使用外部中断或定时扫描的方式识别按下事件。STM32F103 的 GPIO 输入模式下需要注意 5V 耐压问题部分引脚标注为 FTFive-volt tolerant可以承受 5V 输入但为了稳妥按键供电直接使用 3.3V避免电平不匹配的风险。状态指示可以使用 LED 或 OLED 屏幕。LED 的好处是简单便宜OLED 则能显示当前状态、关节角度和记录点数调试时更直观。推荐至少预留一个 LED 显示系统当前处于示教模式还是复现模式否则操作时很容易忘记自己按了几次按钮。5. 基于 STM32F103 的代码实现5.1 开发环境说明本文的示例代码基于 STM32F103 的 HAL 库编写。你在创建工程时可以使用 STM32CubeMX 生成初始化代码也可以直接使用标准外设库核心逻辑不冲突。需要提前确定几个参数系统主频72MHz。PWM 频率50Hz对应周期 20ms这是舵机常用的控制周期。PWM 脉冲宽度0.5ms 到 2.5ms对应舵机 0° 到 180° 的旋转范围。采样周期20ms即每 20ms 记录一帧关节角度数据。5.2 六路舵机 PWM 初始化以 TIM2 输出四路 PWM、TIM3 输出两路 PWM 为例初始化代码如下// pwm.h #ifndef __PWM_H #define __PWM_H #include stm32f1xx_hal.h #define SERVO_PWM_PERIOD 2000 // 20ms #define SERVO_MIN_PULSE 50 // 0.5ms - 0度 #define SERVO_MAX_PULSE 250 // 2.5ms - 180度 void Servo_PWM_Init(void); void Servo_Set_Angle(uint8_t channel, float angle); #endif// pwm.c #include pwm.h TIM_HandleTypeDef htim2; TIM_HandleTypeDef htim3; static void MX_TIM2_Init(void) { TIM_OC_InitTypeDef sConfigOC {0}; htim2.Instance TIM2; htim2.Init.Prescaler 72 - 1; // 1MHz计数频率 htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 2000 - 1; // 20ms周期 htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(htim2); sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 150; // 初始1.5ms90度 sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim2, sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_ConfigChannel(htim2, sConfigOC, TIM_CHANNEL_2); HAL_TIM_PWM_ConfigChannel(htim2, sConfigOC, TIM_CHANNEL_3); HAL_TIM_PWM_ConfigChannel(htim2, sConfigOC, TIM_CHANNEL_4); } static void MX_TIM3_Init(void) { TIM_OC_InitTypeDef sConfigOC {0}; htim3.Instance TIM3; htim3.Init.Prescaler 72 - 1; htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 2000 - 1; htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(htim3); sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 150; sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim3, sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_ConfigChannel(htim3, sConfigOC, TIM_CHANNEL_2); } void Servo_PWM_Init(void) { __HAL_RCC_TIM2_CLK_ENABLE(); __HAL_RCC_TIM3_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); GPIO_InitStruct.Pin GPIO_PIN_6 | GPIO_PIN_7; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); MX_TIM2_Init(); MX_TIM3_Init(); 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); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_2); }这段代码的关键点是把预分频器设为 71定时器计数频率为 72MHz / 72 1MHz即每 1us 计数一次。自动重装载值为 1999周期为 2000us 20ms。这种情况下计数寄存器 CCR 的值直接等于 PWM 高电平的微秒数。设置 Pulse 150 时高电平持续 1.5ms舵机处于 90° 位置。5.3 角度设置函数舵机脉冲宽度与角度的换算并不复杂。0.5ms 对应 0°2.5ms 对应 180°角度与脉冲宽度呈线性关系。换算公式为// pwm.c #include pwm.h void Servo_Set_Angle(uint8_t channel, float angle) { uint16_t pulse; TIM_HandleTypeDef *tim; uint32_t ch; if (angle 0.0f) angle 0.0f; if (angle 180.0f) angle 180.0f; // 将角度映射到 50~250 之间的计数值 pulse (uint16_t)(SERVO_MIN_PULSE (angle / 180.0f) * (SERVO_MAX_PULSE - SERVO_MIN_PULSE)); // 根据通道选择定时器和通道 switch(channel) { case 0: tim htim2; ch TIM_CHANNEL_1; break; case 1: tim htim2; ch TIM_CHANNEL_2; break; case 2: tim htim2; ch TIM_CHANNEL_3; break; case 3: tim htim2; ch TIM_CHANNEL_4; break; case 4: tim htim3; ch TIM_CHANNEL_1; break; case 5: tim htim3; ch TIM_CHANNEL_2; break; default: return; } __HAL_TIM_SET_COMPARE(tim, ch, pulse); }这里真正容易踩坑的地方是 PWM 频率和占空比的对应关系。如果你把自动重装载值设成了 2000周期是 20ms那么 Pulse 寄存器数值的单位是微秒50 到 250 对应 0.5ms 到 2.5ms。如果预分频器或重装载值设置错误舵机要么根本不转要么直接转到机械限位位置发出嗡嗡声长时间通电还可能烧毁舵机。建议先把 Pulse 设置为 150 测试舵机能否回到 90° 位置再继续后续调试。5.4 轨迹记录模块轨迹记录模块的核心数据结构是一个环形缓冲区或固定长度数组。每个采样帧保存六个关节的角度值帧间使用时间戳或固定采样周期控制。// trajectory.h #ifndef __TRAJECTORY_H #define __TRAJECTORY_H #include stdint.h #define JOINT_COUNT 6 #define MAX_FRAME_COUNT 500 // 根据RAM容量调整 typedef struct { uint8_t joint_angles[JOINT_COUNT]; uint16_t timestamp_ms; } TrajectoryFrame; typedef struct { TrajectoryFrame frames[MAX_FRAME_COUNT]; uint16_t head; uint16_t count; } TrajectoryBuffer; void Trajectory_Init(void); uint8_t Trajectory_AddFrame(uint8_t *angles, uint16_t timestamp_ms); uint8_t Trajectory_GetFrame(uint16_t index, uint8_t *angles); uint16_t Trajectory_GetCount(void); #endif// trajectory.c #include trajectory.h static TrajectoryBuffer g_traj; void Trajectory_Init(void) { g_traj.head 0; g_traj.count 0; } uint8_t Trajectory_AddFrame(uint8_t *angles, uint16_t timestamp_ms) { uint16_t write_index; if (g_traj.count MAX_FRAME_COUNT) { return 0; // 缓冲区已满 } write_index (g_traj.head g_traj.count) % MAX_FRAME_COUNT; for (uint8_t i 0; i JOINT_COUNT; i) { g_traj.frames[write_index].joint_angles[i] angles[i]; } g_traj.frames[write_index].timestamp_ms timestamp_ms; g_traj.count; return 1; } uint8_t Trajectory_GetFrame(uint16_t index, uint8_t *angles) { if (index g_traj.count) { return 0; } uint16_t read_index (g_traj.head index) % MAX_FRAME_COUNT; for (uint8_t i 0; i JOINT_COUNT; i) { angles[i] g_traj.frames[read_index].joint_angles[i]; } return 1; } uint16_t Trajectory_GetCount(void) { return g_traj.count; }这段代码使用环形缓冲区的思路在记录满之前可以持续写入。实际工程中500 帧数据在 20ms 采样周期下只能记录 10 秒轨迹。如果你需要更长的记录时间一种方案是把采样周期从 20ms 延长到 40ms但轨迹细腻度会下降另一种方案是使用 STM32F103 的 Flash 或外挂 SPI Flash 保存轨迹数据记录完成后通过串口导出到上位机分析这里不做展开。6. 主逻辑控制与状态机设计6.1 状态机整体框架示教操作与轨迹复现系统的本质是一个有限状态机。系统需要明确区分当前处于哪个状态才能在按键触发时执行正确的操作。整个系统建议设计为以下五个状态IDLE机械臂保持当前姿态所有舵机正常受控。TEACH_START操作者按下记录键系统从 IDLE 切换到示教状态并清空轨迹缓冲区。TEACHING系统正在实时记录关节角度每 20ms 新增一帧。TEACH_END操作者再次按下记录键或发出停止指令系统结束记录。PLAYBACK系统从轨迹缓冲区按顺序读取帧数据驱动舵机复现轨迹。C 语言实现时状态机可以用枚举变量配合简单 switch 结构完成。这里的关键问题是示教过程中如何读取关节角度如果使用串行总线舵机需要读取舵机反馈帧如果使用 PWM 舵机加 ADC则需要读取六路 ADC 值并转换为角度。下面以带反馈钩子函数的方式给出主循环思路。6.2 状态机主循环代码// main.c 主逻辑片段 #include pwm.h #include trajectory.h typedef enum { STATE_IDLE 0, STATE_TEACH_START, STATE_TEACHING, STATE_TEACH_END, STATE_PLAYBACK } SystemState; static volatile SystemState g_state STATE_IDLE; static volatile uint8_t g_record_button_pressed 0; static volatile uint8_t g_play_button_pressed 0; static uint16_t g_frame_index 0; static uint32_t g_last_sample_ms 0; // 读取六个关节当前角度数据来源根据舵机方案不同而不同 void Read_All_Joint_Angles(uint8_t *angles); // 根据角度驱动六个舵机 void Write_All_Joint_Angles(uint8_t *angles) { for (uint8_t i 0; i JOINT_COUNT; i) { Servo_Set_Angle(i, (float)angles[i]); } } void System_Task_Process(void) { uint8_t current_angles[JOINT_COUNT]; uint8_t frame_angles[JOINT_COUNT]; switch (g_state) { case STATE_IDLE: if (g_record_button_pressed) { g_record_button_pressed 0; Trajectory_Init(); g_state STATE_TEACH_START; } break; case STATE_TEACH_START: Read_All_Joint_Angles(current_angles); Trajectory_AddFrame(current_angles, 0); g_last_sample_ms HAL_GetTick(); g_state STATE_TEACHING; break; case STATE_TEACHING: if (g_record_button_pressed) { g_record_button_pressed 0; g_state STATE_TEACH_END; break; } if (HAL_GetTick() - g_last_sample_ms 20) { g_last_sample_ms HAL_GetTick(); Read_All_Joint_Angles(current_angles); Trajectory_AddFrame(current_angles, (uint16_t)(HAL_GetTick() 0xFFFF)); } break; case STATE_TEACH_END: g_frame_index 0; g_state STATE_PLAYBACK; break; case STATE_PLAYBACK: if (g_play_button_pressed) { g_play_button_pressed 0; g_state STATE_IDLE; break; } if (g_frame_index Trajectory_GetCount()) { if (Trajectory_GetFrame(g_frame_index, frame_angles)) { Write_All_Joint_Angles(frame_angles); g_frame_index; } } else { g_state STATE_IDLE; } break; } }这个主循环逻辑并不复杂。需要特别注意的是示教开始状态与示教结束状态的处理。示教开始时先把轨迹缓冲区清空再记录第一帧避免旧轨迹与当前轨迹混在一起。示教结束后直接自动切换到复现状态这样可以快速验证刚才录制的效果。6.3 采样周期与运行时间的关系在 20ms 采样周期下每秒钟记录 50 帧。假设每个关节角度使用 1 个字节存储六自由度机械臂每帧数据量为 6 字节。10 秒轨迹的数据量为50 帧/秒 × 6 字节 × 10 秒 3000 字节这个数据量在 STM32F103 上可以直接存放在 RAM 中。如果需要记录更长时间5000 帧则占用 30000 字节这就超出了小容量型号的 RAM 范围需要考虑外扩存储或实时上传到电脑。实际项目中建议先录制短动作验证逻辑再逐步延长录制时间。如果发现机械臂复现轨迹时出现明显滞后优先检查采样周期是否稳定、舵机响应速度是否足够而不是盲目增加帧率。7. 运行验证与效果调试7.1 最小系统通电测试接通电源后先不要急着拖动机械臂。第一步是验证六路 PWM 信号是否正常输出。用示波器或逻辑分析仪测量任意一路 PWM 输出引脚正常情况下应该看到周期 20ms、脉宽约为 1.5ms 的方波信号。如果没有示波器也可以先通过代码让所有舵机回到 90° 位置。机械臂通电后所有关节应处于中间姿态如果某个关节明显偏向一侧说明该通道的 Pulse 初始值或角度换算有问题。舵机控制中的常见误区是占空比到不了 100% 或超出范围。STM32F103 的定时器在 PWM 模式下当 CCR 值等于 ARR 值时输出保持高电平当 CCR 值为 0 时输出保持低电平。如果你的舵机在 0° 或 180° 附近出现抖动多数情况下是因为将 Pulse 设置到了超出舵机信号有效范围的值。建议对角度值进行限幅避免无意义的超范围输出。7.2 示教记录与复现验证步骤完成最小系统测试后可以按照以下步骤验证示教记录与复现功能让机械臂处于 IDLE 状态用手缓慢拖动机械臂末端观察各关节能否自由运动。如果阻力很大检查舵机是否处于锁定状态必要时在代码中让舵机进入释放模式。按下开始记录按键状态指示灯亮起表示系统进入示教状态。以较慢的速度拖动机械臂走出一个简单的弧形路径。结束位置最好与起始位置不同方便观察复现轨迹时是否准确。再次按下记录按键系统自动进入轨迹复现状态。观察机械臂是否按照刚才拖动的顺序依次经过各点。如果复现轨迹与原轨迹相差较大检查采样周期是否过短导致数据量过大或舵机响应速度不足。从调试经验看第一次做示教验证时最容易出现的问题是拖动机械臂时速度太快导致采样的轨迹点数过少复现时舵机每帧之间角度跳变过大机械臂动作看起来一顿一顿。解决办法是放慢拖动手速或缩短采样周期。示教操作本身追求的是“完整记录运动过程中的关键点”拖得越快关键点丢失得越多轨迹自然不流畅。7.3 轨迹复现偏差的常见原因机械臂轨迹复现出现偏差可以从“采样的数据不准”“回放的动作不准”两个方向排查。采样的数据不准往往是因为角度读取模块本身有误差。比如串行总线舵机的角度反馈精度是 1°记录阶段就引入了 1° 的量化误差外接电位器则因为供电电压波动或 ADC 参考电压不稳定导致角度换算值漂移。回放的动作不准则要检查舵机是否带载能力不足。六自由度机械臂在运行过程中靠近底座的关节需要承担所有上方关节的重量。如果底座舵机扭矩不够复现时会因为力矩不足而丢失位置具体表现是机械臂运动到某些姿态时出现明显下垂或抖动。解决轨迹偏差的思路不是追求完全无误差而是把误差控制在可接受范围内。对 DIY 级别的机械臂来说末端误差在 1cm 到 2cm 范围内都属于正常水平。如果偏差过大优先检查机械结构松动问题再检查控制参数。8. 常见问题与排查思路问题现象可能原因排查方式解决方案舵机不通电不转动PWM 信号未输出或接线错误用示波器量 PWM 引脚检查舵机电源检查 GPIO 复用配置和定时器初始化舵机上电后嗡嗡响并剧烈抖动PWM 脉宽超出舵机有效范围或电源不足测量舵机供电电压检查 Pulse 值范围将角度限幅到 0~180 度更换电源线或加大电源功率部分关节有动作部分关节没反应定时器通道初始化遗漏或通道映射错误核对代码中 HAL_TIM_PWM_Start 的通道参数确保每个通道都启动了 PWM 输出示教时角度记录不下来角度读取函数返回异常或采样逻辑没有进入记录分支在记录代码处加调试串口输出当前状态检查状态机切换条件与按钮消抖逻辑轨迹复现时动作抖动严重采样帧数过多或采样周期不稳定打印复现时的帧计数和当前角度降低采样频率或调整舵机加速度参数拖动机械臂时舵机阻力很大舵机仍处于位置锁定状态查阅舵机手册了解释放模式指令示教前发送舵机释放命令串行总线舵机读取角度超时波特率设置错误或总线接线反了用串口工具查看舵机返回数据核对舵机协议和波特率配置以上排查思路基本可以覆盖大多数 STM32F103 机械臂项目的初级问题。设计调试流程时不必追求一次成功应习惯于把系统拆成“PWM 输出”“角度读取”“状态切换”“轨迹回放”四个独立模块分别验证。每个模块单独跑通后再合并联调定位问题的成本会大大降低。9. 低成本六自由度机械臂的最佳实践与工程建议9.1 从固定动作到示教轨迹的推进路线如果你还是第一次接触六自由度机械臂不建议一上来就直接做示教与轨迹复现。更推荐的推进路线是先让机械臂执行一组固定动作验证六路 PWM 控制逻辑然后引入简单的按键切换让机械臂在几个预设姿态之间切换接着实现“记录并回放”的闭环最后才考虑加入更复杂的平滑插值、速度规划、上位机通信等功能。这套路线背后的逻辑是逐步扩展系统的可靠边界。固定动作阶段暴露的是舵机接线与 PWM 配置问题按键切换阶段暴露的是状态机设计问题记录与回放阶段暴露的是数据采集和时序问题。每一层问题解决后下一层的调试才有稳定基础。9.2 轨迹数据的存储与后续处理思路STM32F103 的 RAM 资源有限不适合长期保存大量轨迹数据。工程实践中可以把轨迹记录模块设计成两类模式一类是单片机实时记录并回放适合验证算法另一类是单片机将轨迹数据通过串口上传到电脑由上位机保存成 CSV 或 JSON 文件后续在 Python 或 MATLAB 中做运动学分析。串口上传的格式建议采用固定帧头加六字节角度数据的二进制格式而不是直接发送 ASCII 字符串。二进制的解析效率更高数据量更小。例如// 串口数据帧格式 // 0xAA 0x55 0x01 cmd angle1 angle2 angle3 angle4 angle5 angle6 checksum上位机收到数据后可以绘制每个关节的角度随时间变化的曲线也可以把角度数据转换成末端执行器的三维坐标轨迹用来判断机械臂是否实际走出了预期路径。9.3 代码规范与版本管理建议即使是入门级项目也建议提前规划好工程目录。STM32 工程的代码量虽然不大但涉及 PWM、ADC、串口、状态机、轨迹存储等多个模块如果全部堆在 main.c 中后期几乎无法维护。推荐按功能拆分文件Inc/ pwm.h trajectory.h servo_bus.h state_machine.h Src/ main.c pwm.c trajectory.c servo_bus.c state_machine.c代码中的角度值建议统一使用 float 或 int16 类型避免在多个模块之间因为数据类型不一致而发生转换错误。另一个容易被忽略的问题是按键检测需要做软件消抖。STM32F103 的按键如果不做消抖处理按下一次可能会被识别成多次触发导致状态机在多个状态之间反复跳转。最简单的消抖方法是延时 20ms 后再读取一次电平确认电平状态一致后才认为有效触发。9.4 供电安全与持续运行注意事项六自由度机械臂的峰值电流比许多人想象的要大。使用普通 USB 供电完全不够带动机器人建议使用 6V 到 7.4V、输出电流 5A 以上的舵机电源。STM32F103 控制板与舵机之间需要共地但电源要分开防止舵机启动瞬间拉低单片机供电电压导致 MCU 复位。如果项目需要长时间运行或反复调试还要关注舵机温度。舵机堵转时电流很大温度上升很快。一旦闻到焦糊味或发现舵机外壳异常发烫应立即断电并检查机械结构是否卡死。在代码中也可以加入温度读取功能如果使用支持温度反馈的串行总线舵机做好高温保护逻辑超过阈值时自动停止运动。10. 总结与后续学习方向这篇文章讲清楚了几件事六自由度机械臂为什么需要示教操作基于 STM32F103 的六路舵机控制系统如何搭建示教记录状态机如何设计轨迹数据如何存储和回放以及低成本项目中常见的坑和排查方法。相比直接编写每段路径的代码这套“拖动-记录-回放”的方案更贴合初学者需求也能让你在后续学习运动学算法之前先把机械臂的底层控制跑通。完成这套基础系统后建议沿着以下方向继续深入把轨迹记录从固定数组改为 Flash 存储或外挂存储支持更长的示教时间。使用串行总线舵机的角度反馈能力把关节角度实时上传到上位机绘制关节运动曲线。研究运动学正解根据六路关节角度计算末端执行器的空间坐标把示教轨迹从关节空间映射到笛卡尔空间。研究平滑插值算法例如梯形速度规划或 S 型速度规划让复现轨迹不再停顿和抖动。引入无线模块用手机或电脑蓝牙发送开始记录、开始复现的控制指令把操作者从线缆和按键中解放出来。说回项目本身。六自由度机械臂是一个典型的“机械结构、硬件电路、嵌入式代码三者交叉”的项目。很多人以为难点在代码其实当你完整组装并在 STM32F103 上跑通示教记录与轨迹复现之后会发现真正的难点是系统化调试能力先判断问题属于机械层、电气层还是软件层再针对性排查。把这种能力和这套示教方案一起沉淀到你的工具箱里后续无论是做桌面级机械臂、云台控制还是其他运动控制项目都会顺畅很多。建议收藏这篇文章动手实践时对照排查少走弯路。