从零构建二自由度机械臂:STM32F103与SG90舵机的实战指南
1. 项目缘起与核心器件选型
去年夏天,我在清理工作室时偶然发现了几只闲置的SG90舵机。这些橙色外壳的小家伙让我萌生了一个想法:能否用它们搭配手头的STM32F103开发板,打造一个简易的二自由度机械臂?这个看似简单的想法,最终演变成了一场充满挑战与惊喜的嵌入式系统实战之旅。
SG90舵机作为创客圈内的"国民级"执行器件,其核心优势在于:
- 性价比突出:单价不足20元,却具备位置反馈功能
- 控制简单:标准50Hz PWM信号即可驱动
- 扭矩适中:1.5kg·cm扭矩足以支撑小型机械结构
与之配合的STM32F103RCT6开发板,则是嵌入式入门者的经典选择:
主要参数: - ARM Cortex-M3内核 - 72MHz主频 - 16个PWM输出通道 - 丰富的GPIO资源2. 硬件架构设计与避坑实践
2.1 供电系统的关键细节
初次连接时,我犯了个典型错误——直接使用开发板的USB供电驱动两个舵机。当机械臂试图举起一支马克笔时,舵机突然出现"抽搐"现象。这个教训让我明白:
多舵机系统的供电要点:
- 独立电源供电(推荐5V/2A以上)
- 共地处理(连接开发板与电源地线)
- 加装大容量电容(470μF以上)滤除电压波动
实测发现:当两个舵机同时动作时,瞬时电流可能突破1A。使用示波器观察,电源电压会出现0.8V左右的跌落。
2.2 信号线布局的艺术
PWM信号线看似简单,却暗藏玄机。我的第二次失败是使用30cm长的杜邦线连接舵机,结果出现:
- 角度控制不精确
- 偶尔出现异常抖动
优化方案:
// 硬件PWM配置示例(TIM3通道1) void PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // GPIOA6配置为复用推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 定时器基础配置(50Hz PWM) TIM_TimeBaseStructure.TIM_Period = 19999; // 20ms周期 TIM_TimeBaseStructure.TIM_Prescaler = 71; // 72MHz/(71+1)=1MHz TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); // PWM模式配置 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 1500; // 初始1.5ms脉宽 TIM_OC1Init(TIM3, &TIM_OCInitStructure); TIM_Cmd(TIM3, ENABLE); }3. 软件控制的核心算法
3.1 角度到脉宽的精确映射
SG90的理论控制范围是0-180°,但实测发现不同舵机存在个体差异。我开发了校准程序:
// 舵机校准函数 void Servo_Calibrate(uint8_t channel) { uint16_t pulse_min = 500; // 0.5ms uint16_t pulse_max = 2500; // 2.5ms for(int angle=0; angle<=180; angle+=10){ uint16_t pulse = pulse_min + (angle * (pulse_max-pulse_min)/180); PWM_SetPulse(channel, pulse); HAL_Delay(500); } }3.2 运动轨迹规划
直接设置目标角度会导致机械臂动作生硬。通过引入缓动算法,运动变得流畅自然:
// 二次缓动函数 float easeOutQuad(float t) { return t*(2-t); } void SmoothMove(uint8_t channel, float start_angle, float end_angle, uint16_t duration) { uint32_t start_time = HAL_GetTick(); while(HAL_GetTick()-start_time < duration){ float progress = (float)(HAL_GetTick()-start_time)/duration; float current_angle = start_angle + (end_angle-start_angle)*easeOutQuad(progress); PWM_SetAngle(channel, current_angle); HAL_Delay(10); } }4. 机械结构设计与优化
4.1 3D打印件设计要点
经过三次迭代,我的机械臂结构优化路径如下:
| 版本 | 特点 | 问题 | 改进 |
|---|---|---|---|
| V1 | 单层结构 | 刚性不足 | 增加加强筋 |
| V2 | 全封闭设计 | 散热不良 | 增加通风孔 |
| V3 | 模块化连接 | 拆装不便 | 改用磁吸接口 |
4.2 配重与力矩平衡
在末端执行器加装摄像头时,发现第二个关节出现"点头"现象。解决方案:
- 在前臂添加配重块
- 改用金属齿轮舵机(MG90S)
- 降低运动速度
5. 人机交互实现
5.1 摇杆控制方案
采用PS2摇杆模块作为输入设备,其ADC值转换为角度:
#define JOYSTICK_DEADZONE 50 uint8_t Joystick_GetAngle(uint16_t adc_val) { if(adc_val < 1024/2 - JOYSTICK_DEADZONE){ return 90 + (adc_val * 90)/(1024/2 - JOYSTICK_DEADZONE); } else if(adc_val > 1024/2 + JOYSTICK_DEADZONE){ return 90 + ((adc_val - 1024/2 - JOYSTICK_DEADZONE) * 90)/(1024/2 - JOYSTICK_DEADZONE); } return 90; // 中位值 }5.2 手机蓝牙控制
通过HC-05模块实现手机APP控制,协议设计如下:
| 字节 | 含义 |
|---|---|
| 0 | 帧头(0xFF) |
| 1 | 通道号 |
| 2 | 角度高字节 |
| 3 | 角度低字节 |
| 4 | 校验和 |
6. 进阶技巧与性能提升
6.1 多舵机同步控制
传统顺序控制会导致机械臂动作不连贯。采用定时器中断实现同步:
// 在定时器中断中更新所有通道 void TIM2_IRQHandler(void) { if(TIM_GetITStatus(TIM2, TIM_IT_Update)){ static uint8_t count = 0; if(++count >= 20){ // 50Hz更新 count = 0; for(int i=0; i<SERVO_NUM; i++){ PWM_SetPulse(i, target_pulse[i]); } } TIM_ClearITPendingBit(TIM2, TIM_IT_Update); } }6.2 负载自适应算法
通过检测电流变化判断是否发生堵转:
#define CURRENT_THRESHOLD 800 // mA void Safety_Check(void) { uint16_t current = ADC_Read(CURRENT_SENSOR_CH); if(current > CURRENT_THRESHOLD){ PWM_DisableAll(); Buzzer_Alert(); } }在完成这个项目后,最让我惊喜的不是最终成品的机械臂能完成多少动作,而是在解决各种意外问题时积累的实战经验。比如发现用热熔胶固定舵机齿轮能有效消除回程间隙,这个技巧现在已经成为我的秘密武器。嵌入式开发的魅力,往往就藏在这些看似微不足道的细节之中。