1. 舵机控制原理与STM32 PWM实现详解
舵机(Servo Motor)是嵌入式系统中最为基础且高频使用的执行机构之一,广泛应用于机器人关节、云台控制、模型设备等场景。其核心价值在于 位置闭环控制能力 ——用户只需提供一个脉宽调制(PWM)信号,舵机内部电路即可自动完成角度定位,无需外部反馈传感器或复杂控制算法。这种“开环输入、闭环执行”的特性,使其成为初学者理解嵌入式外设驱动与机电协同的理想切入点。
但必须明确:舵机并非真正的“开环”系统。其内部集成了电位器(角度反馈)、比较器、H桥驱动和减速齿轮组,构成一个完整的模拟伺服回路。用户提供的PWM信号仅作为 参考输入量 ,舵机芯片持续采样电位器电压,与目标脉宽对应电压做差,生成误差信号驱动电机转动,直至电位器电压与目标值匹配,电机停止。整个过程在毫秒级内完成,对外表现为“给定即到达”。
1.1 标准舵机电气接口与时序规范
以本课程所用的SG90舵机为例,其为最典型的模拟舵机,采用三线制连接:
| 线缆颜色 | 引脚标识 | 电气功能 | 电压范围 | 关键约束 |
|---|---|---|---|---|
| 棕色(或黑色) | GND | 电源地 | — | 必须与MCU共地 |
| 红色 | VCC | 供电正极 | 4.8V–6.0V | 超压易烧毁内部IC |
| 橙色(或黄色) | SIG | PWM信号输入 | 3.3V/5V逻辑电平 | 高电平有效,占空比决定角度 |
其核心时序要求如下:
- 周期(Period) :严格为20ms(50Hz),允许±1%偏差。此周期由定时器自动重装载值(ARR)与预分频系数(PSC)共同决定。
- 高电平宽度(Pulse Width) :
- 0.5ms → 对应0°(极限左偏)
- 1.0ms → 对应45°
- 1.5ms → 对应90°(中位,机械零点)
- 2.0ms → 对应135°
- 2.5ms → 对应180°(极限右偏)
该线性关系源于舵机内部RC积分电路设计,将脉宽直接映射为电位器目标电压。需注意:超出0.5–2.5ms范围的脉宽可能导致舵机堵转、过热甚至损坏,实际工程中必须进行软件限幅。
1.2 PWM信号生成的本质:时间精度即控制精度
PWM信号的本质是 对时间间隔的精确分割与控制 。在STM32中,这一过程由通用定时器(如TIM3)通过以下硬件机制实现:
- 时钟源选择 :APB1总线时钟(通常为36MHz),经预分频器(PSC)分频后作为计数器时钟。
- 计数器运行 :向上计数模式下,计数器从0开始递增,直至等于自动重装载寄存器(ARR)值,然后清零并触发更新事件(UEV)。
- 通道输出 :每个通道(CH1/CH2)配置为PWM模式1(Active High)。当计数器值小于捕获/比较寄存器(CCR)值时,输出高电平;否则输出低电平。
-
周期与占空比计算
:
- 周期 $ T = \frac{(PSC+1) \times (ARR+1)}{f_{CLK}} $
- 占空比 $ D = \frac{CCR}{ARR} $ (忽略PSC影响,因ARR已包含其效应)
因此,要生成20ms周期,核心是求解满足 $ \frac{(PSC+1) \times (ARR+1)}{f_{CLK}} = 20 \times 10^{-3} $ 的整数解。以$f_{CLK}=36MHz$为例:
$$
(PSC+1) \times (ARR+1) = 36 \times 10^6 \times 20 \times 10^{-3} = 720000
$$
取PSC=71(即分频72倍),则ARR=9999(即计数10000次),验证:
$$
T = \frac{72 \times 10000}{36 \times 10^6} = 0.02s = 20ms
$$
此时,1.5ms高电平对应CCR值为:
$$
CCR = \frac{1.5ms}{20ms} \times 10000 = 750
$$
此即代码中
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 750)
的理论依据。
1.3 为什么选择TIM3而非TIM2?
在多外设共存的工程实践中,外设资源分配是首要考量。本项目中,TIM2已被超声波测距模块占用(用于Echo信号高电平计时),若再复用TIM2驱动舵机,将导致:
- 定时器中断优先级冲突,可能丢失超声波回波边沿;
- HAL库底层句柄(
htim2
)被多个模块修改,引发不可预测行为;
- 调试时无法独立使能/禁用各模块,增加排错难度。
TIM3作为APB1总线下另一独立定时器,具备完全隔离的寄存器空间与中断向量,是理想替代方案。其通道1(CH1)映射至PA6引脚,通道2(CH2)映射至PA7,物理引脚布局紧凑,便于PCB布线。
2. STM32 HAL库PWM驱动工程实现
本节基于STM32F103C8T6(Blue Pill)开发板,使用HAL库构建可复用的舵机驱动模块。所有配置均遵循ST官方参考手册(RM0008)与数据手册(DS5319)规范,确保跨平台可移植性。
2.1 工程结构与文件组织
为提升代码可维护性,采用模块化设计:
Core/
├── Inc/
│ ├── servo.h // 舵机驱动头文件,声明API与配置宏
│ └── ...
├── Src/
│ ├── servo.c // 舵机驱动实现,含初始化与控制函数
│ └── ...
servo.h
定义关键配置:
#ifndef __SERVO_H
#define __SERVO_H
#include "main.h"
// 舵机参数宏定义(可全局配置)
#define SERVO_PERIOD_MS 20U // 周期:20ms
#define SERVO_MIN_PULSE_MS 0.5F // 最小脉宽:0.5ms(0°)
#define SERVO_MAX_PULSE_MS 2.5F // 最大脉宽:2.5ms(180°)
#define SERVO_NEUTRAL_PULSE_MS 1.5F // 中位脉宽:1.5ms(90°)
// 定时器硬件资源绑定(强类型约束)
#define SERVO_TIM_INSTANCE TIM3
#define SERVO_TIM_CLK_ENABLE() __HAL_RCC_TIM3_CLK_ENABLE()
#define SERVO_GPIO_PORT GPIOA
#define SERVO_GPIO_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE()
#define SERVO_AFIO_CLK_ENABLE() __HAL_RCC_AFIO_CLK_ENABLE() // 复用功能时钟
// 通道映射(支持双路输出,为后续电机控制预留)
#define SERVO_CH1_PIN GPIO_PIN_6
#define SERVO_CH2_PIN GPIO_PIN_7
#define SERVO_CH1_AF GPIO_AF2_TIM3
#define SERVO_CH2_AF GPIO_AF2_TIM3
// API声明
void Servo_Init(void);
void Servo_SetAngle(uint8_t angle); // 0~180度
void Servo_SetPulseWidth(uint16_t us); // 直接设置微秒级脉宽
#endif /* __SERVO_H */
2.2 硬件初始化:时钟、GPIO与定时器三级配置
舵机PWM输出依赖三个硬件模块的协同工作,初始化顺序必须严格遵循“时钟→GPIO→定时器”原则,否则将导致外设无响应。
2.2.1 时钟使能与AFIO配置
void Servo_Init(void)
{
// 1. 使能相关时钟(按依赖顺序)
SERVO_TIM_CLK_ENABLE(); // TIM3时钟
SERVO_GPIO_CLK_ENABLE(); // GPIOA时钟
SERVO_AFIO_CLK_ENABLE(); // AFIO时钟(必需!用于重映射与复用功能)
// 2. GPIO初始化:PA6/PA7配置为复用推挽输出
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = SERVO_CH1_PIN | SERVO_CH2_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 复用推挽
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; // 低速足够(PWM频率仅50Hz)
HAL_GPIO_Init(SERVO_GPIO_PORT, &GPIO_InitStruct);
// 3. 配置AFIO:将PA6/PA7映射至TIM3_CH1/TIM3_CH2(无重映射,使用默认)
// 注:F1系列默认映射,此步可省略,但显式配置增强可读性
GPIO_PinRemapConfig(GPIO_Remap_TIM3, ENABLE); // 启用TIM3重映射(若需)
// 4. 定时器基础配置
TIM_HandleTypeDef htim3;
htim3.Instance = SERVO_TIM_INSTANCE;
htim3.Init.Prescaler = 71; // PSC=71 → 分频72倍 (36MHz/72=500kHz)
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 9999; // ARR=9999 → 计数10000次 (500kHz/10000=50Hz)
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim3.Init.RepetitionCounter = 0;
if (HAL_TIM_PWM_Init(&htim3) != HAL_OK) {
Error_Handler(); // 用户自定义错误处理
}
// 5. 通道配置:CH1与CH2均启用PWM模式1
TIM_OC_InitTypeDef sConfigOC = {0};
sConfigOC.OCMode = TIM_OCMODE_PWM1; // PWM模式1:计数器<CCR时输出有效电平
sConfigOC.Pulse = 750; // 初始占空比:1.5ms → CCR=750
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
if (HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1) != HAL_OK) {
Error_Handler();
}
if (HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_2) != HAL_OK) {
Error_Handler();
}
// 6. 启用通道输出与定时器
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2);
HAL_TIM_Base_Start(&htim3); // 启动计数器
}
关键参数解析
:
-
Prescaler=71
:APB1时钟36MHz经72分频得500kHz计数器时钟,确保高分辨率(1us计数精度)。
-
Period=9999
:计数10000次耗时20ms(10000/500000=0.02s),满足舵机周期要求。
-
Pulse=750
:对应1.5ms高电平(750/500000=0.0015s),舵机初始定位90°。
-
TIM_OCMODE_PWM1
:选择PWM模式1而非模式2,因舵机要求高电平为有效控制信号。
2.2.2 复用功能(AFIO)的必要性
GPIO引脚具有多重功能,PA6/PA7默认为通用IO,需通过AFIO模块将其切换为“TIM3_CH1/TIM3_CH2”复用功能。此过程涉及:
- 使能AFIO时钟(
__HAL_RCC_AFIO_CLK_ENABLE()
);
- 配置GPIO端口的复用功能选择寄存器(AFIO->PCRL/PCRH);
- 设置GPIO模式为
GPIO_MODE_AF_PP
。
若遗漏AFIO时钟使能,HAL库
HAL_TIM_PWM_Init()
将返回
HAL_ERROR
,且调试器无法捕获此错误(因HAL默认不校验AFIO状态),导致“代码编译通过但无PWM输出”的典型疑难问题。
2.3 角度控制API:从物理量到寄存器值的映射
舵机控制的核心是建立“用户角度”与“CCR寄存器值”的线性映射。
Servo_SetAngle()
函数封装了这一转换逻辑:
void Servo_SetAngle(uint8_t angle)
{
// 角度限幅:0~180度
if (angle > 180) angle = 180;
// 线性映射:angle(0~180) → pulse_width_us(500~2500)
// 公式:pulse = min_pulse + (max_pulse - min_pulse) * angle / 180
uint16_t pulse_us = (uint16_t)(SERVO_MIN_PULSE_MS * 1000U +
(SERVO_MAX_PULSE_MS - SERVO_MIN_PULSE_MS) * 1000U * angle / 180U);
// 转换为CCR值:pulse_us / (1 / f_timer) = pulse_us * f_timer / 1000000
// f_timer = 500kHz = 500000Hz → CCR = pulse_us * 0.5
uint16_t ccr_val = (uint16_t)(pulse_us * 0.5F);
// 写入CCR寄存器(原子操作,避免计数器更新期间写入异常)
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, ccr_val);
}
映射原理深度剖析
:
- 舵机角度与脉宽呈严格线性关系,斜率 $ k = \frac{2500-500}{180-0} = \frac{2000}{180} \approx 11.11 $ us/°。
- CCR值计算本质是时间单位转换:目标脉宽(微秒)× 计数器频率(Hz)÷ 1,000,000。
- 因计数器频率为500kHz(周期2us),故1us对应0.5个计数器单位,
ccr_val = pulse_us * 0.5
是最优简化。
2.4 实时性保障:避免阻塞与机械惯性适配
舵机存在显著的机械响应延迟(典型值100~300ms),若在主循环中连续快速调用
Servo_SetAngle()
,将导致:
- 舵机尚未到达前一目标位置,新指令已覆盖CCR值;
- 电机频繁启停,产生抖动、噪音及额外发热;
- 严重时可能触发内部过流保护而失步。
工程实践中,必须引入 最小动作间隔 (Minimum Move Interval)机制:
static uint32_t last_move_ms = 0;
#define SERVO_MIN_INTERVAL_MS 200U // 两次动作最小间隔200ms
void Servo_SetAngle_Safe(uint8_t angle)
{
uint32_t now_ms = HAL_GetTick(); // 获取系统滴答计时器值
if ((now_ms - last_move_ms) < SERVO_MIN_INTERVAL_MS) {
return; // 未达到最小间隔,忽略本次指令
}
last_move_ms = now_ms;
Servo_SetAngle(angle);
}
// 主循环中调用示例
while (1)
{
if (button_pressed) {
Servo_SetAngle_Safe(0); // 0°
HAL_Delay(1000);
Servo_SetAngle_Safe(90); // 90°
HAL_Delay(1000);
Servo_SetAngle_Safe(180); // 180°
HAL_Delay(1000);
}
}
此设计利用HAL库
HAL_GetTick()
获取毫秒级时间戳,确保每次动作间隔≥200ms,既满足舵机机械响应需求,又避免软件过度等待阻塞系统。
3. 硬件连接与可靠性设计
软件实现的最终效果高度依赖硬件连接的正确性与鲁棒性。SG90舵机虽结构简单,但电源噪声、地线干扰、信号完整性等问题极易导致控制失效。
3.1 电源设计:分离供电与共地原则
SG90标称工作电压4.8–6.0V,峰值电流可达500mA(堵转时)。若直接由STM32的3.3V或5V引脚供电:
- MCU USB供电能力有限(通常500mA),舵机启动电流将拉低MCU电压,导致复位;
- 电机换向产生的反电动势通过电源线耦合至MCU,引发IO误触发或ADC读数跳变。
推荐方案
:
- 使用独立5V/2A开关电源为舵机供电;
- 将电源GND与STM32的GND通过短而粗的导线(或PCB铺铜)
单点连接
,形成共地参考;
- 绝对禁止将舵机VCC连接至STM32的5V引脚(即使板载有LDO)。
3.2 信号线抗干扰措施
PA6引脚输出的PWM信号(3.3V TTL电平)需驱动舵机内部CMOS输入级。长线传输时易受电磁干扰,导致舵机误动作。实践验证有效的防护措施包括:
- 串联限流电阻 :在PA6与舵机SIG线间串接100Ω电阻,抑制高频振铃与反射;
- 并联滤波电容 :在舵机VCC与GND间并联100μF电解电容(低频滤波)+ 100nF陶瓷电容(高频去耦),靠近舵机引脚焊接;
- 屏蔽线缆 :若线长>20cm,使用带屏蔽层的杜邦线,并将屏蔽层单端接地(接MCU GND)。
3.3 舵机测试仪:硬件故障的快速诊断工具
在代码调试前,务必使用舵机测试仪验证舵机本体是否完好。其工作原理为:
- 内置555定时器或MCU,生成标准20ms周期、可调脉宽的PWM信号;
- 通过旋钮调节占空比,实时驱动舵机摆动;
- 若舵机在测试仪上响应正常,而接入MCU后无动作,则问题100%在软件或连接;反之,则为舵机硬件故障。
测试仪接线极其简单:仅需将“+”(5V)、“-”(GND)、“S”(SIG)三线对应接入舵机红、棕、橙线。此步骤可节省80%以上的硬件排错时间。
4. 双路PWM扩展:为直流电机驱动奠基
本课程中同时初始化TIM3_CH1与CH2,表面看是冗余设计,实则为后续H桥电机驱动埋下关键伏笔。直流电机方向控制依赖 两路互补PWM信号 :
| 电机状态 | CH1(IN1) | CH2(IN2) | H桥输出 |
|---|---|---|---|
| 正转 | PWM(50%) | 0(低电平) | A高B低 |
| 反转 | 0(低电平) | PWM(50%) | A低B高 |
| 制动 | PWM(50%) | PWM(50%) | A高B高(短接制动) |
| 惯性滑行 | 0(低电平) | 0(低电平) | A低B低(高阻态) |
此模式下,CH1与CH2的CCR值需同步更新,且占空比相同。
Servo_SetAngle()
函数可无缝复用,仅需将
TIM_CHANNEL_1
替换为
TIM_CHANNEL_2
,体现模块化设计的前瞻性。
5. 常见问题与实战排错指南
在数百次舵机调试中,以下问题出现频率最高,附带根因分析与解决路径:
5.1 现象:舵机完全不动作,万用表测SIG线无PWM波形
-
根因1:AFIO时钟未使能
表现:编译无错,但HAL_TIM_PWM_Start()后PA6无任何电平变化。
解决:检查SERVO_AFIO_CLK_ENABLE()是否调用,确认RCC->APB2ENR寄存器bit0(AFIOEN)为1。 -
根因2:GPIO复用功能配置错误
表现:PA6输出恒定3.3V或0V,非PWM。
解决:用示波器测量PA6,确认GPIO_InitTypeDef.Mode设为GPIO_MODE_AF_PP而非GPIO_MODE_OUTPUT_PP。 -
根因3:定时器未启动计数器
表现:HAL_TIM_PWM_Start()成功,但CCR写入无效。
解决:必须调用HAL_TIM_Base_Start(&htim3)启动计数器,否则CCR值不生效。
5.2 现象:舵机抖动、定位不准、发出“滋滋”声
-
根因:电源噪声过大
表现:仅舵机单独工作时正常,接入MCU后异常。
解决:立即加入100μF+100nF去耦电容,改用独立电源供电。 -
根因:PWM周期偏离20ms
表现:舵机角度范围压缩(如1.5ms只到60°)。
解决:用示波器实测PA6波形,若周期≠20ms,重新核算PSC/ARR值,注意HAL库中Prescaler与Period均为寄存器值减1。
5.3 现象:舵机到达目标角度后缓慢漂移
-
根因:机械零点偏移或电位器磨损
表现:同一脉宽下,不同舵机定位角度差异达±5°。
解决:通过软件校准,在Servo_SetAngle()中添加偏移量:ccr_val += calibration_offset。量产时需逐台标定。
我在实际项目中曾遇到一个隐蔽问题:使用USB-TTL模块为STM32供电时,舵机转动导致USB端口电压跌落,电脑弹出“USB设备供电不足”提示。最终解决方案是彻底切断USB供电,改用外部5V电源,并将USB-TTL仅作串口通信使用。这个教训提醒我们: 嵌入式系统的电源设计永远是第一道防线,而非最后一步优化 。

107

被折叠的 条评论
为什么被折叠?



