第4章:定时器PWM输出——控制舵机/直流电机的基础
各位同学,今天我们来啃一块硬骨头——定时器的PWM输出。说实话,PWM这东西在嵌入式里太常见了,调光、调速、控制舵机,哪哪都离不开它。我刚开始学的时候,总觉得不就是个方波嘛,有啥难的?结果第一次调舵机,电机死活不转,查了半天发现是频率设错了。嗯,这种坑,咱们今天一次性填平。
4.1 PWM到底是什么?
PWM,全称Pulse Width Modulation,脉冲宽度调制。说白了,就是通过调节一个方波里高电平占的比例,来模拟一个模拟量。你想想看,数字IO只能输出0或1,但电机需要的是连续的速度,怎么办?用PWM。
一个PWM信号有四个关键参数:
| 参数 | 含义 | 典型值(舵机) | 典型值(直流电机) |
|---|---|---|---|
| 频率 | 每秒多少个周期 | 50 Hz | 1k~20k Hz |
| 周期 | 一个完整脉冲的时间 | 20 ms | 1 ms ~ 50 μs |
| 占空比 | 高电平时间占周期的比例 | 5%~10% | 0%~100% |
| 分辨率 | 占空比可调节的精细度 | 8~16 bit | 8~16 bit |
这里有个关键点:舵机对频率非常敏感,必须是50 Hz左右。直流电机则宽泛得多,但频率太低会听到明显的嗡嗡声,太高了MOS管开关损耗又上去了。我个人习惯,直流电机PWM频率取10 kHz左右,既听不到噪音,效率也还行。
4.2 STM32定时器怎么产生PWM?
STM32的定时器,本质上是一个不断递增的计数器。它跟一个预设的"比较值"做对比,当计数值小于比较值时,输出高电平;大于等于时,输出低电平。这样,一个PWM波形就出来了。
我画了一张图,帮你理解这个流程:
你看,整个过程其实就三步:计数器跑、比较器判断、IO口翻转。STM32的硬件帮你做了全部,你只需要设置好ARR和CCR两个寄存器就行。
4.3 实战:用PWM控制舵机
舵机控制是PWM最经典的应用之一。我当年做六足机器人,18个舵机同时控制,PWM频率和相位稍微没调好,机器人直接原地抽搐。那场面,真是又好笑又心疼。
舵机的控制协议很简单:
- 周期:20 ms(频率50 Hz)
- 高电平时间:0.5 ms ~ 2.5 ms
- 对应角度:0° ~ 180°
换算成占空比就是:2.5% ~ 12.5%。中间值1.5 ms对应90°。
下面是一个完整的HAL库配置代码,我加了详细的注释:
// 定时器2,通道1,PA0输出PWM控制舵机
void Servo_Init(void)
{
GPIO_InitTypeDef gpio = {0};
TIM_OC_InitTypeDef oc = {0};
TIM_HandleTypeDef htim2 = {0};
// 1. 使能时钟
__HAL_RCC_TIM2_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
// 2. 配置GPIO为复用推挽输出
gpio.Pin = GPIO_PIN_0;
gpio.Mode = GPIO_MODE_AF_PP;
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &gpio);
// 3. 配置定时器基础参数
htim2.Instance = TIM2;
htim2.Init.Prescaler = 84 - 1; // 84 MHz / 84 = 1 MHz
htim2.Init.Period = 20000 - 1; // 1 MHz / 20000 = 50 Hz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_PWM_Init(&htim2);
// 4. 配置PWM通道
oc.OCMode = TIM_OCMODE_PWM1;
oc.Pulse = 1500; // 初始占空比:1.5 ms → 90°
oc.OCPolarity = TIM_OCPOLARITY_HIGH;
oc.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim2, &oc, TIM_CHANNEL_1);
// 5. 启动PWM输出
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
}
// 设置舵机角度
void Servo_SetAngle(uint8_t angle)
{
// 角度范围:0~180
// 脉宽范围:500~2500 μs
uint16_t pulse = 500 + (uint16_t)angle * 2000 / 180;
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, pulse);
}
关键点:预分频器Prescaler和周期Period的计算。我的STM32主频84 MHz,先分频到1 MHz(精度1 μs),再计数20000次得到50 Hz。这样CCR的值直接就是微秒数,非常直观。
4.4 实战:用PWM控制直流电机
直流电机控制比舵机简单,但有个坑——电机启动瞬间电流很大。我记得有一次调试四驱小车,直接上100%占空比,结果电源指示灯闪了一下就黑了。后来学乖了,软启动。
直流电机PWM控制要点:
- 频率选择:1 kHz ~ 20 kHz。低于1 kHz能听到电机啸叫,高于20 kHz驱动管损耗增大。我一般取10 kHz。
- 死区处理:如果使用H桥驱动,上下管切换时必须插入死区时间,否则会直通烧管子。
- 软启动:占空比从0逐渐增加到目标值,避免电流冲击。
下面是一个带软启动的直流电机控制代码:
// 定时器3,通道2,PB5输出PWM控制直流电机
void Motor_Init(void)
{
// ... GPIO和定时器配置类似,略 ...
// 关键参数:10 kHz
htim3.Init.Prescaler = 84 - 1; // 1 MHz
htim3.Init.Period = 100 - 1; // 1 MHz / 100 = 10 kHz
// ...
}
// 软启动到目标占空比
void Motor_SoftStart(uint8_t target_duty)
{
// target_duty: 0~100
for(uint8_t i = 0; i <= target_duty; i++)
{
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, i);
HAL_Delay(10); // 每步延时10 ms
}
}
// 直接设置占空比(紧急停止用)
void Motor_SetDuty(uint8_t duty)
{
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, duty);
}
我的经验:调试电机时,先用一个LED代替电机,观察PWM波形是否正确。波形对了再上电机,能省很多烧驱动管的时间。
4.5 常见故障与定位
这部分是我最想跟你分享的。PWM出问题,症状千奇百怪,但根因就那么几个。
| 故障现象 | 可能原因 | 排查方法 |
|---|---|---|
| 舵机不动 | 频率不是50 Hz | 用示波器测周期,确认是20 ms |
| 舵机抖动 | 占空比不稳定或电源纹波大 | 检查CCR是否被意外修改;加电容滤波 |
| 电机不转 | 占空比太低或驱动管没导通 | 先设50%占空比,测电机两端电压 |
| 电机嗡嗡响 | PWM频率太低 | 提高到10 kHz以上 |
| 驱动管发烫 | 频率太高或死区不够 | 降低频率;检查死区时间设置 |
⚠️ 曾经有个血的教训:我在调试一个机械臂时,舵机线序接反了,信号线和电源线短路。结果PWM引脚直接烧了,换了两块STM32才找到原因。所以,上电前一定要用万用表量一遍线序!
4.6 本章小结
PWM控制说难不难,说简单也不简单。核心就三点:频率算对、占空比算对、线序接对。掌握了这三条,舵机和直流电机你都能玩得转。
我个人建议,初学者先用逻辑分析仪或者示波器看波形,亲眼看到PWM的变化,比看一百遍代码都管用。等你对波形有感觉了,再回头调代码,会发现很多问题迎刃而解。