第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波形就出来了。

我画了一张图,帮你理解这个流程:

STM32定时器PWM生成原理 定时器计数器 CNT:0 → 1 → 2 → ... → ARR(自动重装载值)→ 0 → 1 → ... 比较器:CNT 与 CCR(捕获比较寄存器)比较 CNT < CCR:输出高电平 CNT >= CCR:输出低电平 输出PWM波形:高电平宽度 = CCR × 定时器时钟周期 占空比 = CCR / (ARR + 1) × 100% 💡 改变CCR的值,就能改变占空比;改变ARR的值,就能改变频率

你看,整个过程其实就三步:计数器跑、比较器判断、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的变化,比看一百遍代码都管用。等你对波形有感觉了,再回头调代码,会发现很多问题迎刃而解。