ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32舵机控制机械臂:从PWM到数据手套与VR映射实战

STM32舵机控制机械臂:从PWM到数据手套与VR映射实战

简介:这份资源面向嵌入式开发初学者与机器人爱好者,提供一套以STM32微控制器驱动舵机、并通过数据手套实现手势操控机械臂的完整工程。项目将嵌入式控制、舵机PWM角度调节与传感器人机交互结合,可用于课程设计、毕业设计或机器人入门实践。压缩包共206个文件,约4.88MB,以C源码与头文件为主体,包含STM32外设驱动、定时器PWM输出、串口通信等实现,并附带Keil工程配置、编译中间文件与可执行输出,方便直接打开编译与二次修改。目前已有6568人学习下载,说明其在同类开源项目中具备一定参考价值。读者可从中获取STM32工程搭建思路、舵机控制代码框架、数据手套信号解析与系统集成方法,并借助完整目录结构快速定位关键模块,理解从手势采集到机械臂动作执行的完整链路。

1. 从零搭一套 STM32 舵机控制机械臂:为什么我劝你先搞懂 PWM 而不是急着写逆解

很多人第一次接触机械臂,脑子里想的是六轴逆运动学、ROS、MoveIt,结果板子焊好、舵机一通电,机械臂直接抽搐着撞向限位。问题不在算法,在于连最底层的 PWM 都没配对。STM32 舵机控制机械臂这个方向,本质是用定时器输出多路 50Hz 的 PWM 波,每路占空比对应一个关节角度,再往上叠加人机交互(电位器、数据手套、上位机)和虚拟现实映射。它适合两类人:一类是电子/自动化方向的学生,想拿它当嵌入式外设综合练习;另一类是想做遥操作或 VR 联动 Demo 的开发者,需要一个低成本、可复现的执行端。这套东西不复杂,但坑集中在定时器通道分配、舵机供电和角度标定三处,下面按我实际拆装的顺序讲。

2. 舵机控制原理与 STM32 定时器选型:50Hz 从哪来、通道怎么分

2.1 舵机为什么吃 50Hz 的 PWM

常见舵机(SG90、MG996R 这类)内部是一个小控制板加电位器反馈,它期望外部给它一个周期 20ms 的方波,高电平持续时间在 0.5ms 到 2.5ms 之间对应 0° 到 180°。换算一下:20ms 周期就是 50Hz,0.5ms 高电平对应 2.5% 占空比,2.5ms 对应 12.5%。有些舵机标称 1ms~2ms 对应 0~180°,实际用起来会有偏差,这就是后面要标定的原因。

STM32 输出这种波形靠定时器的 PWM 模式。以常见的 STM32F103C8T6 为例,它有 TIM1~TIM4 四个通用/高级定时器,每个定时器有 4 个独立通道。也就是说一颗 F103 最多能同时输出 16 路 PWM,驱动一台六轴机械臂绰绰有余。选型时优先用 TIM2、TIM3、TIM4,因为 TIM1 是高级定时器,配置刹车和死区时容易出玄学问题,新手先绕开。

2.2 定时器参数怎么算:预分频和自动重装

PWM 频率由定时器时钟、预分频系数(PSC)和自动重装值(ARR)共同决定:

PWM频率 = 定时器时钟 / ((PSC + 1) * (ARR + 1))

F103 主频 72MHz,TIM2/3/4 挂在 APB1 上,但定时器时钟是 72MHz。要得到 50Hz:

72,000,000 / ((PSC+1)*(ARR+1)) = 50 (PSC+1)*(ARR+1) = 1,440,000

取 PSC=71,则 (ARR+1)=20000,ARR=19999。这样计数器每 20ms 溢出一次,正好一个舵机周期。分辨率是 20000 个计数对应 20ms,即 1 个计数 = 1us,0.5ms 就是 500,2.5ms 就是 2500。这个分辨率对舵机够用,因为舵机本身死区就有几微秒。

2.3 用 CubeMX 配一路 PWM 的完整步骤

我一般用 CubeMX 生成初始化代码,省得手写寄存器。步骤如下:

  1. 选好芯片型号,在 Pinout 里把某个 GPIO 设为 TIMx_CHy(比如 PA0 设为 TIM2_CH1)。
  2. 左侧 Timers 里点开 TIM2,Clock Source 选 Internal Clock。
  3. Channel1 选 PWM Generation CH1。
  4. Parameter Settings 里:Prescaler = 71,Counter Period = 19999,Counter Mode = Up,Pulse 先填 1500(对应中位 1.5ms)。
  5. 生成代码,在 main 里调用HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);。

生成的初始化代码里关键几行是这样:

// tim.c 中 MX_TIM2_Init 片段 htim2.Instance = TIM2; htim2.Init.Prescaler = 71; // 72MHz/(71+1)=1MHz 计数频率 htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 19999; // 1MHz/20000 = 50Hz htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(&htim2); sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 1500; // 初始 1.5ms,舵机中位 sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);

逻辑说明:Prescaler 把 72MHz 降到 1MHz,即每个计数 1us;Period 设 19999 让计数器数 20000 次溢出,周期 20ms;Pulse 是捕获比较值,决定高电平持续多少个计数,也就是多少微秒。改角度就是改 Pulse。

参数说明:Pulse 范围理论 500~2500,实际因为舵机差异,我一般限制在 600~2400,避免顶到机械限位烧舵机。如果你用的是 270° 舵机,这个范围要重新标。

2.4 多路 PWM 的通道分配策略

一台六轴机械臂需要 6 路 PWM。用 TIM2 的 CH1~CH4 加 TIM3 的 CH1~CH2 就够。分配时注意两点:一是同一定时器的四个通道共用 PSC 和 ARR,所以频率必须一致,这对舵机正好合适;二是避开 JTAG 占用的引脚(PA13/PA14/PA15/PB3/PB4),否则调试口和 PWM 打架。我吃过这个亏,PA15 配了 PWM 结果下载器连不上,查了半天。

提示:如果通道不够,可以用 TIM 的互补输出或者换引脚重映射,但重映射要开 AFIO 时钟,CubeMX 里勾一下就行。

3. 人机交互与数据手套映射:把手指弯曲变成关节角度

3.1 交互方案选型:电位器、数据手套还是上位机

机械臂的输入方式决定整套系统的复杂度。常见三种:

方案成本精度适用场景
电位器直连 ADC极低中单臂手动示教、教学演示
数据手套(弯曲传感器)中中低遥操作、VR 手势映射
上位机/VR 串口下发中高高虚拟现实联动、轨迹录制

数据手套的核心是柔性弯曲传感器,手指弯曲时电阻变化,通过分压电路进 STM32 的 ADC。它的问题是每根手指的阻值范围不一致,且随温度漂移,所以必须做逐通道标定。虚拟现实那头一般是在 PC 或一体机里跑一个仿真模型,把关节角通过串口或无线模块发给 STM32。

3.2 ADC 采集弯曲传感器并映射到角度

先看采集。假设弯曲传感器和 10k 电阻分压,接到 PA1(ADC1_IN1):

// 读取一次 ADC 并转成 0~180 角度 uint16_t adc_raw = 0; float angle = 0; HAL_ADC_Start(&hadc1); if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK) { adc_raw = HAL_ADC_GetValue(&hadc1); // 12位,0~4095 } HAL_ADC_Stop(&hadc1); // 标定值:伸直时 raw_min,弯曲到底时 raw_max #define RAW_MIN 800 #define RAW_MAX 3200 angle = (float)(adc_raw - RAW_MIN) / (RAW_MAX - RAW_MIN) * 180.0f; if (angle < 0) angle = 0; if (angle > 180) angle = 180;

逻辑说明:ADC 是 12 位,读到 0~4095。RAW_MIN 和 RAW_MAX 是实测标定值,不是理论值,必须自己测。映射后做限幅,防止超范围。

参数说明:RAW_MIN/RAW_MAX 的测法——串口打印原始值,手指完全伸直记一个数,完全弯曲记一个数,多测几次取平均。如果传感器抖动大,加一个滑动平均滤波:

// 8 次滑动平均,抑制传感器抖动 #define FILTER_N 8 uint16_t filter_buf[FILTER_N] = {0}; uint8_t filter_idx = 0; uint16_t filter_adc(uint16_t new_val) { filter_buf[filter_idx] = new_val; filter_idx = (filter_idx + 1) % FILTER_N; uint32_t sum = 0; for (int i = 0; i < FILTER_N; i++) sum += filter_buf[i]; return sum / FILTER_N; }

3.3 角度到 PWM 脉宽的转换与死区处理

拿到 0~180 的角度后,要转成定时器的 Pulse 值。前面算过 1 计数 = 1us,所以:

// 角度转 PWM 比较值,脉宽 500~2500us uint16_t angle_to_pulse(float angle) { if (angle < 0) angle = 0; if (angle > 180) angle = 180; return (uint16_t)(500 + angle / 180.0f * 2000); } // 应用到某个通道 __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, angle_to_pulse(angle));

逻辑说明:__HAL_TIM_SET_COMPARE是宏,直接改 CCR 寄存器,比重新调用 HAL 库函数快,适合在控制循环里高频调用。

参数说明:500 和 2000 这两个数对应 0.5ms 和 2.5ms 的跨度。如果你的舵机是 1ms~2ms 规格,改成 1000 + angle/180*1000。死区处理指的是:机械臂相邻关节不能同时转到极限,否则会自碰撞。我一般软件里给每个关节设一个软限位,比如关节 2 限制在 15°~165°,超出就截断。

3.4 虚拟现实端的串口协议设计

VR 或上位机下发数据,最省事的是定长帧。我常用这种格式:

帧头(0xAA 0x55) + 6个关节角度(各1字节, 0~180) + 校验和(1字节)

STM32 端用串口空闲中断加 DMA 接收,解析时先找帧头,再校验和。这样即使丢字节也能重新同步。数据手套如果是有线的,直接走 ADC;如果是无线的,就在手套端加个 MCU 打包发送,协议同上。虚拟现实场景里,PC 端把仿真模型的关节角实时发下来,STM32 只负责执行,不做逆解,这样分工最清晰,也最容易调试。

4. 供电、共地与舵机抖动:三个最容易翻车的地方

4.1 现象:舵机一上电就抖,或者动一下就复位

原因:舵机堵转电流能到 1A 以上,六轴同时动瞬间电流好几安。如果和 STM32 共用一路 5V(比如 USB 供电),电压被拉低,MCU 直接欠压复位。

解决:舵机电源和 MCU 电源分开。舵机用独立的 5V/5A 开关电源,MCU 用另一路 5V 或 USB。两路电源的地必须共地,否则 PWM 信号没有参考电平,舵机不受控。我一般用一个大电容(1000uF)并在舵机电源输入端,吸收瞬态电流。

4.2 现象:PWM 配好了但舵机不动,或者只往一个方向转

原因:一是 PWM 没真正启动,只调了HAL_TIM_PWM_ConfigChannel但没调HAL_TIM_PWM_Start;二是引脚被复用功能占用,比如默认的 JTAG 引脚;三是 Pulse 值算错,比如把 ARR 当成 20000 而不是 19999,导致周期差一点,有些舵机就不认。

解决:先用示波器或逻辑分析仪看引脚有没有波形。没有波形就查启动函数和引脚配置;有波形但舵机不动,查 Pulse 范围和供电。没有仪器的话,接一个 LED 加电阻到 PWM 脚,看亮度变化也能粗判。

4.3 现象:多个舵机同时动时,某些通道角度乱跳

原因:同一定时器的多个通道共用 ARR,如果你在中断里频繁改 ARR 去调频率,会互相干扰。另外,如果在主循环里逐个HAL_Delay更新通道,刷新率太低,看起来就是乱跳。

解决:频率固定 50Hz 不动,只改 CCR。更新时用一个循环一次性写完六个通道,不要加延时。控制周期保持在 20ms 以内,最好 10ms 更新一次。

4.4 现象:数据手套映射的角度和实际手指弯曲对不上

原因:弯曲传感器的阻值和弯曲角度不是线性关系,且每个传感器个体差异大。用统一 RAW_MIN/RAW_MAX 必然对不上。

解决:逐通道标定,并且做分段线性化。简单做法是测三个点(伸直、半弯、全弯),用两段直线拟合。要求高的话上查表法,把 0~4095 分成 16 段,每段对应一个角度,存成数组。

注意:标定值要存在 Flash 里,否则每次断电重来。用 STM32 的 Flash 模拟 EEPROM 或者外挂一片 24C02 都行。

5. 进阶:用定时器中断做轨迹插值,让机械臂动得不那么愣

前面讲的都是「指哪打哪」,输入角度直接映射到 PWM,机械臂动作很生硬,关节是跳变的。要让动作顺滑,得做插值。我的习惯是在 TIM6 或 TIM7 上开一个 10ms 的定时器中断,在中断里做角度插值,逐步逼近目标角度。

// 每个关节的当前角度和目标角度 float cur_angle[6] = {90, 90, 90, 90, 90, 90}; float tgt_angle[6] = {90, 90, 90, 90, 90, 90}; #define STEP 1.5f // 每 10ms 最多变化 1.5 度 // TIM6 中断回调,10ms 触发一次 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM6) { for (int i = 0; i < 6; i++) { float diff = tgt_angle[i] - cur_angle[i]; if (diff > STEP) cur_angle[i] += STEP; else if (diff < -STEP) cur_angle[i] -= STEP; else cur_angle[i] = tgt_angle[i]; } // 一次性更新六路 PWM __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, angle_to_pulse(cur_angle[0])); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, angle_to_pulse(cur_angle[1])); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_3, angle_to_pulse(cur_angle[2])); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_4, angle_to_pulse(cur_angle[3])); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, angle_to_pulse(cur_angle[4])); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, angle_to_pulse(cur_angle[5])); } }

逻辑说明:主循环只负责更新tgt_angle,中断负责以固定步长逼近。STEP 越大动作越快但越顿,越小越顺滑但响应慢。1.5°/10ms 相当于 150°/s,对大多数舵机是合适的速度。

参数说明:TIM6 的配置和 PWM 定时器不同,它不需要输出引脚,只要开更新中断。CubeMX 里 Activated 勾上,Prescaler 设 7199,Period 设 99,得到 10ms 中断。注意中断里不要放耗时操作,__HAL_TIM_SET_COMPARE是寄存器操作,很快,安全。

验证方法:把tgt_angle从 90 改到 180,用手机慢动作拍机械臂,看是不是匀速转过去而不是瞬间跳。如果还是跳,检查中断有没有真正进,可以在中断里翻转一个 GPIO 用示波器看。

还有一个技巧是给每个关节设不同的 STEP,底座关节负载大,STEP 给小一点,末端关节轻,可以快一点。这样整体动作更协调。我一开始六个关节用同一个 STEP,结果底座还没转到位,末端已经甩过去了,看着像抽筋。从那以后我每次调机械臂,都强制先把每个关节单独跑一遍限位和速度,再联调。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进