1. PWM 原理:为什么能控制这么多设备?

PWM 信号本质上是固定频率的方波,通过改变高电平在一个周期内的占比(即占空比)来传递信息。对于不同负载,PWM 的作用机制略有不同:

  • 舵机:舵机内部电路将 PWM 的脉宽(高电平时间)映射为转角。典型周期为 20ms,脉宽 0.5ms~2.5ms 对应 0°~180°。
  • 直流电机:PWM 的平均电压决定了电机两端有效电压,从而控制转速。配合 H 桥电路,还可实现正反转。
  • LED:人眼感知亮度与平均功率近似线性,高频 PWM 调光无闪烁,且比线性调压效率更高。

STM32 定时器可以自动产生 PWM 波形,无需 CPU 干预,非常适合实时控制。

2. STM32 定时器 PWM 工作机制

STM32 的通用定时器(TIM2~TIM5)和高级定时器(TIM1、TIM8)均具备 PWM 生成功能。其核心原理如下:

  • 计数寄存器 CNT:从 0 计数到自动重载值 ARR,然后清零重新计数。
  • 比较寄存器 CCRx:当 CNT < CCRx 时,输出高电平;当 CNT >= CCRx 时,输出低电平(PWM 模式 1)。
  • 占空比 = CCRx / (ARR+1),频率 = 定时器时钟 / ((ARR+1) * (PSC+1))。

通过配置 ARR 和 PSC 可设定频率,通过修改 CCRx 即可调节占空比。

3. 硬件连接与配置步骤

3.1 硬件准备

  • STM32F103 开发板(或其他系列)
  • 舵机(SG90)连接 PA8(TIM1_CH1)
  • 直流电机驱动模块(L298N)连接 PA6(TIM3_CH1)
  • LED 串联电阻连接 PA7(TIM3_CH2)

3.2 配置步骤(以 HAL 库为例)

  1. 启用定时器时钟,并配置 GPIO 为复用推挽输出。
  2. 设置定时器时基:PSC 和 ARR 决定频率。
  3. 配置 PWM 通道:模式、极性、初始占空比。
  4. 使能 PWM 输出,并启动定时器。
  5. 修改 CCRx 寄存器或使用 __HAL_TIM_SET_COMPARE 来更新占空比。

4. 完整代码示例(HAL 库)

以下代码同时演示舵机、电机和 LED 的控制。使用 TIM1_CH1(PA8)驱动舵机,TIM3_CH1(PA6)驱动电机,TIM3_CH2(PA7)驱动 LED。

/* 定时器句柄 */
TIM_HandleTypeDef htim1, htim3;  

void MX_TIM1_Init(void) {
    TIM_OC_InitTypeDef sConfigOC = {0};
    
    __HAL_RCC_TIM1_CLK_ENABLE();
    htim1.Instance = TIM1;
    htim1.Init.Prescaler = 7199;          // 72MHz/7200 = 10kHz
    htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim1.Init.Period = 199;              // 10kHz/200 = 50Hz(20ms周期)
    htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    HAL_TIM_PWM_Init(&htim1);
    
    sConfigOC.OCMode = TIM_OCMODE_PWM1;
    sConfigOC.Pulse = 15;                 // 初始占空比 15/200=7.5%(对应90°)
    sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
    HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_1);
    HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
}

void MX_TIM3_Init(void) {
    TIM_OC_InitTypeDef sConfigOC = {0};
    
    __HAL_RCC_TIM3_CLK_ENABLE();
    htim3.Instance = TIM3;
    htim3.Init.Prescaler = 71;            // 72MHz/72 = 1MHz
    htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim3.Init.Period = 999;              // 1MHz/1000 = 1kHz
    htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    HAL_TIM_PWM_Init(&htim3);
    
    /* 通道1:电机 */
    sConfigOC.OCMode = TIM_OCMODE_PWM1;
    sConfigOC.Pulse = 0;                 // 初始停止
    sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
    HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1);
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
    
    /* 通道2:LED */
    sConfigOC.Pulse = 500;               // 初始50%亮度
    HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_2);
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2);
}

/* GPIO 复用配置 */
void MX_GPIO_Init(void) {
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_GPIOB_CLK_ENABLE();
    
    /* PA8: TIM1_CH1, PA6: TIM3_CH1, PA7: TIM3_CH2 */
    GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_6 | GPIO_PIN_7;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}

/* 应用:设置舵机角度(0~180) */
void set_servo_angle(uint8_t angle) {
    uint32_t pulse = 10 + (angle * 10) / 180;  // 脉宽:0.5ms~2.5ms,按10kHz计数,即5~25
    __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, pulse);
}

/* 应用:设置电机转速(-100~100,负值需配合H桥) */
void set_motor_speed(int16_t speed) {
    if (speed >= 0) {
        __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, speed * 10); // 0~1000
    }
    // 如需反转,通过GPIO控制H桥方向引脚
}

/* 应用:设置LED亮度(0~255) */
void set_led_brightness(uint8_t brightness) {
    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, brightness * 4); // 0~1020
}

int main(void) {
    HAL_Init();
    MX_GPIO_Init();
    MX_TIM1_Init();
    MX_TIM3_Init();
    
    set_servo_angle(90);
    set_motor_speed(50);
    set_led_brightness(128);
    
    while (1) {
        // 动态调节示例:呼吸灯
        for (int i = 0; i <= 255; i++) {
            set_led_brightness(i);
            HAL_Delay(5);
        }
        for (int i = 255; i >= 0; i--) {
            set_led_brightness(i);
            HAL_Delay(5);
        }
    }
}

5. 关键细节与注意事项

  • 频率选择:舵机必须 50Hz(周期20ms),否则会抖动或烧毁。电机一般 1kHz~20kHz(高频率可降低噪音),LED 调光建议 >1kHz 以避免闪烁。
  • 占空比计算:先确定 ARR,再根据需要的脉宽或百分比计算 CCR。例如舵机:ARR=199,PSC=7199,计数时钟10kHz,则 0.5ms = 5,2.5ms = 25。
  • 高级定时器注意:TIM1/TIM8 默认输出极性可能相反,且需要使能主输出 __HAL_TIM_MOE_ENABLE(&htim1)。如果使用 HAL_PWM_Start 则已包含。
  • 死区与互补输出:若控制 H 桥或电机驱动,需注意上下桥臂的互补 PWM 和死区插入,防止直通短路。
  • 动态调参:修改 ARR 会改变频率,建议只修改 CCR 调整占空比。若需可变频率,使用 DMA 或更新事件中断。
  • 标准库移植:将 HAL 的 __HAL_TIM_SET_COMPARE 替换为 TIM_SetCompare1(TIMx, value) 即可,初始化部分需手动配置寄存器或参考标准库例程。

6. 总结

STM32 的 PWM 功能是连接数字世界与模拟世界的桥梁。理解定时器时基与比较寄存器之间的关系,是配置任何 PWM 应用的基础。通过本文的示例,你可以轻松拓展到多通道舵机控制板、无刷电机 ESC、RGB LED 调光等场景。建议动手在开发板上验证,并将逻辑分析和示波器测量作为调试利器,才能深刻掌握 PWM 的精髓。