1. 项目概述
用单个定时器控制8路舵机听起来像是不可能完成的任务,但通过巧妙的分时复用技术,我们可以突破硬件通道数量的限制。这个方案特别适合需要同时控制多个舵机但又不想占用太多定时器资源的场景,比如机器人关节控制、机械臂运动等应用。
核心思路是利用定时器的中断功能,将一个完整的PWM周期分成多个相位,在不同时间段输出不同通道的PWM信号。虽然STM32的通用定时器通常只有4个硬件比较通道,但通过这种方法我们可以轻松扩展到8路甚至更多。
提示:这种方法虽然节省硬件资源,但会略微增加CPU中断负担,在需要极高精度的场合可能需要权衡利弊。
2. 硬件准备与原理分析
2.1 所需硬件组件
要完成这个项目,你需要准备以下硬件:
- STM32开发板(如STM32F103C8T6最小系统板)
- 8个标准舵机(如SG90或MG996R)
- 5V电源(建议至少2A输出能力)
- 面包板和连接线若干
- 可选:100Ω电阻(用于信号线滤波)
2.2 舵机控制原理
标准舵机使用PWM信号控制位置,典型参数为:
- 周期:20ms(50Hz)
- 脉宽范围:500-2500μs
- 500μs对应0°位置
- 1500μs对应90°位置
- 2500μs对应180°位置
这种控制方式称为位置控制模式,通过改变PWM的占空比来指定舵机的目标角度。
2.3 定时器分时复用原理
STM32的通用定时器通常有4个独立的捕获/比较通道(如TIM3的CH1-CH4)。要实现8路控制,我们需要:
- 将20ms的PWM周期分成两个10ms的相位
- 在前10ms(相位1)输出1-4路舵机信号
- 在后10ms(相位2)输出5-8路舵机信号
- 通过中断处理程序动态切换相位和比较值
这种方法的关键在于精确控制每个相位的时间点,确保不会出现信号重叠或丢失。
3. 软件实现详解
3.1 定时器初始化配置
以下是使用标准外设库的TIM3初始化代码,配置为产生20ms周期的PWM信号:
c复制// TIM3初始化结构体
TIM_TimeBaseInitTypeDef TIM_InitStruct;
TIM_OCInitTypeDef TIM_OCInitStruct;
// 使能TIM3时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
// 基础定时器配置
TIM_InitStruct.TIM_Prescaler = 72-1; // 72MHz/72=1MHz计数频率
TIM_InitStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_InitStruct.TIM_Period = 20000-1; // 20ms周期(1MHz时钟)
TIM_InitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM3, &TIM_InitStruct);
// 配置4个PWM输出通道
for(int i=0; i<4; i++){
TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStruct.TIM_Pulse = 1500; // 初始1.5ms脉宽(中位)
TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High;
switch(i){
case 0: TIM_OC1Init(TIM3, &TIM_OCInitStruct); break;
case 1: TIM_OC2Init(TIM3, &TIM_OCInitStruct); break;
case 2: TIM_OC3Init(TIM3, &TIM_OCInitStruct); break;
case 3: TIM_OC4Init(TIM3, &TIM_OCInitStruct); break;
}
// 使能预装载寄存器
TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable);
}
// 使能ARR预装载和定时器
TIM_ARRPreloadConfig(TIM3, ENABLE);
TIM_Cmd(TIM3, ENABLE);
// 使能更新中断
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
NVIC_EnableIRQ(TIM3_IRQn);
3.2 中断服务程序实现
中断处理程序是实现分时复用的核心,它负责在周期中点切换输出通道:
c复制// 全局变量存储各通道脉宽
uint16_t channel_pulse[8] = {1500, 1500, 1500, 1500, 1500, 1500, 1500, 1500};
void TIM3_IRQHandler(void){
static uint8_t phase = 0;
if(TIM_GetITStatus(TIM3, TIM_IT_Update)){
phase ^= 1; // 切换相位
if(phase){
// 前半周期驱动1-4路
TIM_SetCompare1(TIM3, channel_pulse[0]);
TIM_SetCompare2(TIM3, channel_pulse[1]);
TIM_SetCompare3(TIM3, channel_pulse[2]);
TIM_SetCompare4(TIM3, channel_pulse[3]);
}else{
// 后半周期驱动5-8路
TIM_SetCompare1(TIM3, channel_pulse[4]);
TIM_SetCompare2(TIM3, channel_pulse[5]);
TIM_SetCompare3(TIM3, channel_pulse[6]);
TIM_SetCompare4(TIM3, channel_pulse[7]);
}
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
}
}
3.3 速度控制算法
要实现平滑的速度控制,我们需要逐步改变PWM脉宽而不是直接跳变:
c复制// 舵机控制结构体
struct ServoCtrl{
uint16_t current; // 当前脉宽(us)
uint16_t target; // 目标脉宽(us)
uint16_t step; // 每步变化量(us)
} servos[8];
// 每20ms调用一次更新舵机位置
void update_servos(){
for(int i=0; i<8; i++){
if(servos[i].current != servos[i].target){
int16_t diff = servos[i].target - servos[i].current;
// 计算实际变化量(不超过步长)
diff = (abs(diff) > servos[i].step) ?
(diff > 0 ? servos[i].step : -servos[i].step) : diff;
servos[i].current += diff;
channel_pulse[i] = servos[i].current;
}
}
}
使用示例:让2号舵机以每秒30°的速度转到45°位置
c复制// 计算目标脉宽(45°对应1250us)
servos[1].target = 1250;
// 计算步长(30°/s = 600us/s = 12us/20ms)
servos[1].step = 12;
4. 硬件连接与调试技巧
4.1 正确接线方法
舵机通常有三根线:
- 红色:电源(5V)
- 棕色:地线(GND)
- 橙色/黄色:信号线(PWM)
连接时需注意:
- 所有舵机的地线必须与单片机共地
- PWM信号线连接到定时器的对应GPIO
- 使用独立电源供电,不要从单片机直接取电
推荐接线表:
| 舵机编号 | 信号线 | 定时器通道 | 相位 |
|---|---|---|---|
| 1 | PA6 | TIM3_CH1 | 1 |
| 2 | PA7 | TIM3_CH2 | 1 |
| 3 | PB0 | TIM3_CH3 | 1 |
| 4 | PB1 | TIM3_CH4 | 1 |
| 5 | PA6 | TIM3_CH1 | 2 |
| 6 | PA7 | TIM3_CH2 | 2 |
| 7 | PB0 | TIM3_CH3 | 2 |
| 8 | PB1 | TIM3_CH4 | 2 |
4.2 常见问题排查
-
舵机无反应
- 检查电源是否正常
- 确认信号线连接正确
- 测量PWM信号是否输出
-
舵机抖动或不稳定
- 确保地线连接良好
- 在信号线串联100Ω电阻
- 检查电源是否足够(每个舵机至少需要200-300mA)
-
位置控制不准确
- 提高定时器时钟频率(如2MHz)
- 检查舵机机械结构是否卡顿
- 校准舵机中位(1500us)
-
速度控制不平滑
- 调整步长(step)参数
- 确保update_servos()被定期调用
- 考虑加入加速度控制
5. 性能优化与扩展
5.1 提高控制精度
默认配置使用1MHz定时器时钟,脉宽分辨率为1μs。要获得更高精度:
c复制// 使用2MHz时钟(36-1预分频)
TIM_InitStruct.TIM_Prescaler = 36-1;
// 周期调整为40000-1(保持20ms)
TIM_InitStruct.TIM_Period = 40000-1;
这样可获得0.5μs的分辨率,但会略微增加CPU负担。
5.2 扩展更多舵机
通过增加相位数量,理论上可以控制更多舵机。例如:
- 3个相位 → 12路舵机
- 4个相位 → 16路舵机
但需注意:
- 每个相位时间变短,可能影响舵机响应
- 中断处理更频繁,增加CPU负载
- PWM信号质量可能下降
5.3 使用STM32CubeMX配置
对于新手,推荐使用STM32CubeMX图形化工具配置定时器:
- 选择TIM3定时器
- 设置时钟源为内部时钟
- 配置4个通道为PWM Generation CHx
- 设置预分频和周期值
- 生成初始化代码
这样可以避免手动配置的繁琐和错误。
6. 替代方案比较
6.1 硬件方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 单定时器分时复用 | 节省硬件资源 | 编程复杂 | 8-16路中等精度控制 |
| 多定时器独立控制 | 精度高、响应快 | 占用资源多 | 高精度、高速控制 |
| PCA9685专用芯片 | 简单易用 | 需要额外硬件 | I2C控制多路舵机 |
| 软件PWM | 不占用定时器 | CPU占用高、精度低 | 少量低精度控制 |
6.2 Arduino实现对比
Arduino的Servo库使用软件模拟PWM:
arduino复制#include <Servo.h>
Servo myservo[8];
void setup(){
for(int i=0; i<8; i++){
myservo[i].attach(2+i); // 引脚2-9
}
}
优点:
- 使用简单
- 不依赖硬件定时器
缺点:
- 精度和稳定性较差
- 占用CPU资源
- 最多只能控制8路(UNO)
7. 实际应用案例
7.1 机械臂控制
六自由度机械臂通常需要6-8个舵机。使用本方案:
- 基座旋转:舵机1
- 肩关节:舵机2
- 肘关节:舵机3
- 腕关节:舵机4-6
- 夹持器:舵机7-8
通过协调各关节运动,可以实现复杂的抓取动作。
7.2 机器人行走控制
四足机器人需要12个舵机(每条腿3个)。虽然超出单定时器能力,但可以:
- 使用两个定时器(如TIM3+TIM4)
- 每个控制6路(3相位)
- 通过同步信号协调运动
7.3 摄影云台
两轴云台控制:
- 俯仰轴:舵机1
- 偏航轴:舵机2
- 加入速度控制实现平滑追焦
8. 进阶技巧与注意事项
-
电源管理
- 使用大容量电容(1000μF)缓冲电源
- 考虑使用开关电源而非线性稳压
- 为每组4个舵机单独供电
-
运动规划
- 提前计算轨迹,避免突然启停
- 加入加速度控制更平滑
- 使用查表法存储常用动作序列
-
故障保护
- 监测舵机电流,防止堵转
- 设置软件限位,保护机械结构
- 加入看门狗定时器防死机
-
调试技巧
- 先用单路测试,再扩展多路
- 使用逻辑分析仪观察PWM信号
- 逐步增加负载测试电源稳定性
通过合理运用这些技巧,可以构建出稳定可靠的多舵机控制系统。我在实际项目中发现,良好的电源设计和机械结构往往比软件算法更能决定系统稳定性。