STM32 进阶封神之路(十六):PWM 波深度实战 —— 定时器输出 + LED 调光 + 电机调速(库函数 + 寄存器)

0 阅读15分钟

STM32 进阶封神之路(十六):PWM 波深度实战 —— 定时器输出 + LED 调光 + 电机调速(库函数 + 寄存器)

上一篇我们掌握了 DHT11 单总线温湿度采集,这一篇聚焦 STM32 的核心控制功能 ——PWM 波输出。PWM(脉冲宽度调制)是嵌入式控制中最常用的模拟信号替代方案,通过调节高电平占空比,可实现 LED 调光、电机调速、音频输出等场景,其核心依赖 STM32 的通用定时器(如 TIM2~TIM5)或高级定时器(TIM1/TIM8)。

本文基于实战资料,从 PWM 核心概念、定时器 PWM 原理、硬件配置,到 STM32 代码实现(固定占空比 + 动态调光 + 电机调速),手把手带你吃透 PWM 输出的全流程,让你从 “理论认知” 落地到 “实际控制”!

一、复习回顾:PWM 与定时器核心基础衔接

在深入 PWM 实战前,先衔接关键知识,确保逻辑连贯:

  1. PWM 核心关联:PWM 波由 STM32 的通用定时器高级定时器生成,依赖定时器的 “计数模式” 和 “比较模式”;
  2. 定时器基础:通用定时器(如 TIM3)支持向上 / 向下 / 中心对齐计数,PWM 输出常用向上计数模式(计数器从 0 增至 ARR,循环往复);
  3. 核心寄存器:PWM 输出关键寄存器为ARR(自动重装载值,决定 PWM 频率)和CCR(比较值,决定占空比);
  4. GPIO 配置:PWM 输出引脚需配置为复用推挽输出(GPIO_Mode_AF_PP),才能输出定时器生成的 PWM 信号。

二、PWM 核心认知:从概念到应用场景

1. PWM 波的本质与核心参数

(1)什么是 PWM 波?

PWM 波是一种周期性的方波信号,通过改变一个周期内高电平的持续时间(占空比),模拟 “模拟电压” 输出。例如:

  • 50% 占空比:高电平占周期的一半,等效模拟电压为电源电压的 50%(3.3V 电源→1.65V 等效电压);
  • 10% 占空比:高电平占周期的 10%,等效模拟电压为 0.33V。
(2)核心参数(实战必知)
  • 频率(f) :PWM 波的周期倒数(f=1/T),决定信号变化速度。例如 1kHz 频率→周期 1ms;

    • 应用影响:LED 调光频率需≥100Hz(避免肉眼闪烁),电机调速频率常用 1kHz~20kHz;
  • 占空比(D) :一个周期内高电平持续时间与总周期的比值(0%~100%);

    • 公式:D = 高电平时间 / 周期 × 100%;
  • 分辨率:占空比调节的最小步长,由ARR值决定(分辨率 = 1/(ARR+1))。例如 ARR=999→分辨率 0.1%。

2. PWM 波的典型应用场景

  • LED 调光:通过改变占空比,调节 LED 亮度(占空比越高,亮度越亮);
  • 电机调速:控制直流电机转速(占空比越高,转速越快);
  • 音频输出:PWM 波经过低通滤波后,可还原模拟音频信号;
  • 电源稳压:如 Buck/Boost 变换器,通过 PWM 调节输出电压;
  • 舵机控制:通过 20ms 周期、1ms~2ms 高电平的 PWM 波,控制舵机转角。

3. STM32 定时器 PWM 输出原理

STM32 的通用定时器(TIM2~TIM5)和高级定时器(TIM1/TIM8)支持 PWM 输出,核心依赖 “定时器计数” 与 “比较匹配” 的联动逻辑:

(1)核心原理框图

plaintext

定时器时钟 → 预分频器(PSC)→ 计数器(CNT)→ 向上计数至ARR → 自动重置为0
                                  ↓
比较寄存器(CCR)→ 比较器 → 匹配时触发电平翻转 → 生成PWM波
(2)PWM 输出关键逻辑(向上计数模式)
  1. 定时器时钟经预分频器(PSC)分频后,驱动计数器(CNT)从 0 开始向上计数;
  2. 计数器值(CNT)与比较寄存器值(CCR)实时比较;
  3. CNT < CCR时,PWM 输出高电平;当CNT ≥ CCR时,输出低电平(PWM 模式 1,最常用);
  4. 计数器计数至ARR(自动重装载值)后,重置为 0,开始下一轮计数,形成周期性 PWM 波。
(3)PWM 模式区分(模式 1 vs 模式 2)

STM32 定时器支持两种 PWM 模式,核心区别在于 “比较匹配时的电平状态”:

表格

PWM 模式核心逻辑(向上计数)典型应用
模式 1(TIM_OCMode_PWM1)CNT < CCR → 高电平;CNT ≥ CCR → 低电平LED 调光、电机调速(高电平有效)
模式 2(TIM_OCMode_PWM2)CNT < CCR → 低电平;CNT ≥ CCR → 高电平反向控制场景(低电平有效)

三、PWM 关键参数计算:频率与占空比(必掌握)

PWM 的频率和占空比由PSC(预分频器)、ARR(自动重装载值)和CCR(比较值)共同决定,需精准计算才能满足应用需求。

1. 频率计算(定时器时钟→PWM 频率)

(1)核心公式

plaintext

定时器计数频率 f_cnt = 定时器时钟频率 f_clk / (PSC + 1)
PWM频率 f_pwm = f_cnt / (ARR + 1) = f_clk / [(PSC + 1) × (ARR + 1)]
  • 说明:

    • f_clk:定时器时钟频率(通用定时器挂载 APB1 总线,STM32F103 默认 APB1 预分频系数 = 2,故f_clk=72MHz);
    • PSC:预分频器值(0~65535),分频系数 = PSC+1;
    • ARR:自动重装载值(0~65535),计数器计数周期 = ARR+1。
(2)实战计算示例(生成 1kHz PWM 波)

需求:通过 TIM3 生成 1kHz PWM 波(周期 1ms),计算PSCARR值:

  • 已知:f_clk=72MHzf_pwm=1kHz
  • 推导:(PSC+1) × (ARR+1) = f_clk / f_pwm = 72MHz / 1kHz = 72000
  • 选型:取PSC=71(分频系数 = 72,f_cnt=72MHz/72=1MHz),则ARR+1=72000/72=1000 → ARR=999
  • 结论:PSC=71ARR=999,PWM 频率 = 1kHz,分辨率 = 1/(999+1)=0.1%。

2. 占空比计算(CCR→占空比)

(1)核心公式

plaintext

占空比 D = (CCR + 1) / (ARR + 1) × 100%
  • 说明:

    • CCR:比较值(0~ARR),需满足0 ≤ CCR ≤ ARR
    • CCR=0时,占空比 = 0%(始终低电平);当CCR=ARR时,占空比 = 100%(始终高电平)。
(2)实战计算示例(1kHz PWM,50% 占空比)
  • 已知:ARR=999,目标占空比 = 50%;
  • 推导:CCR + 1 = (ARR + 1) × D = 1000 × 50% = 500 → CCR=499
  • 结论:CCR=499,输出 1kHz、50% 占空比的 PWM 波。

四、硬件配置:PWM 输出引脚与连接

1. 定时器与 GPIO 引脚映射(STM32F103)

PWM 输出需使用定时器的 “通道引脚”,通用定时器的通道与 GPIO 引脚映射如下(以常用 TIM3 为例):

表格

定时器通道映射 GPIO 引脚引脚模式典型应用
TIM3_CH1PA6复用推挽输出(AF_PP)LED 调光、电机 PWM 输出
TIM3_CH2PA7复用推挽输出双路 PWM 控制(如双色 LED)
TIM3_CH3PB0复用推挽输出辅助 PWM 输出
TIM3_CH4PB1复用推挽输出辅助 PWM 输出

实战选型:优先选择 TIM3_CH1(PA6),引脚资源常用,资料丰富。

2. 硬件连接示例

(1)LED 调光硬件连接
  • TIM3_CH1(PA6)→ 1KΩ 限流电阻 → LED 正极 → GND;
  • 核心逻辑:PWM 波通过 PA6 输出,调节占空比改变 LED 两端的平均电压,实现亮度调节。
(2)直流电机调速硬件连接
  • TIM3_CH1(PA6)→ 电机驱动模块(如 L298N)PWM 输入端;
  • 电机驱动模块 VCC → 外部 12V 电源;
  • 电机驱动模块 OUT → 直流电机;
  • 核心逻辑:STM32 输出 PWM 波控制驱动模块的输出电压,实现电机调速。

3. 硬件注意事项

  • GPIO 模式:PWM 输出引脚必须配置为复用推挽输出(GPIO_Mode_AF_PP),不可用普通输出模式;
  • 限流电阻:LED 调光需串联 1KΩ 限流电阻,避免电流过大烧毁 LED 或 GPIO 引脚;
  • 电机驱动:直流电机需通过驱动模块(如 L298N)连接,STM32 引脚无法直接驱动电机(输出电流不足)。

五、STM32 实战代码:PWM 输出全场景实现

核心流程:定时器时钟使能→GPIO 配置(复用推挽)→定时器基础配置(计数模式 + PSC+ARR)→PWM 模式配置→使能输出→动态调节 CCR,以下是库函数与寄存器双版本实现。

1. 基础实战:固定占空比 PWM 输出(TIM3_CH1,PA6)

(1)库函数版实现(1kHz,50% 占空比)

c

运行

#include "stm32f10x.h"

// TIM3 PWM初始化:频率1kHz,占空比可配置
void TIM3_PWM_Init(uint16_t arr, uint16_t psc) {
    GPIO_InitTypeDef GPIO_InitStruct;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct;
    TIM_OCInitTypeDef TIM_OCInitStruct;

    // 1. 使能TIM3和GPIOA时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // TIM3挂载APB1
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    // 2. 配置PA6为复用推挽输出(TIM3_CH1)
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStruct);

    // 3. 配置TIM3基础参数(向上计数模式)
    TIM_TimeBaseStruct.TIM_Prescaler = psc; // 预分频器
    TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数
    TIM_TimeBaseStruct.TIM_Period = arr; // 自动重装载值
    TIM_TimeBaseStruct.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟分频系数=1
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStruct);

    // 4. 配置PWM模式(模式1)
    TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; // PWM模式1
    TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; // 使能输出
    TIM_OCInitStruct.TIM_Pulse = arr / 2; // 初始占空比50%(CCR=ARR/2)
    TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; // 输出极性高
    TIM_OC1Init(TIM3, &TIM_OCInitStruct); // 配置通道1

    // 5. 使能PWM预装载(确保CCR修改在下一周期生效)
    TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable);
    TIM_ARRPreloadConfig(TIM3, ENABLE); // 使能ARR预装载

    // 6. 启动TIM3
    TIM_Cmd(TIM3, ENABLE);
}

int main(void) {
    // 初始化TIM3 PWM:arr=999,psc=71 → 频率1kHz,占空比50%
    TIM3_PWM_Init(999, 71);

    while(1) {
        // 固定占空比输出,LED保持中等亮度
    }
}
(2)寄存器版实现(底层逻辑拆解)

c

运行

#include "stm32f10x.h"

void TIM3_PWM_Init(uint16_t arr, uint16_t psc) {
    // 1. 使能时钟
    RCC->APB1ENR |= (1<<1); // TIM3时钟使能位(bit1)
    RCC->APB2ENR |= (1<<2); // GPIOA时钟使能位(bit2)

    // 2. 配置PA6为复用推挽输出
    GPIOA->CRL &= ~(0x0F<<24); // 清除PA6配置(CRL对应PA0~PA7,PA6对应bit27~bit24)
    GPIOA->CRL |= (0x0B<<24);  // MODE6=11(50MHz),CNF6=10(复用推挽)

    // 3. 配置TIM3基础参数
    TIM3->PSC = psc; // 预分频器
    TIM3->ARR = arr; // 自动重装载值
    TIM3->CR1 |= (1<<7); // 使能ARR预装载
    TIM3->CR1 &= ~(3<<5); // 向上计数模式(CMS=00)

    // 4. 配置PWM模式1(通道1)
    TIM3->CCMR1 &= ~(7<<4); // 清除OC1M位
    TIM3->CCMR1 |= (6<<4);  // OC1M=110(PWM模式1)
    TIM3->CCMR1 |= (1<<3);  // 使能OC1预装载
    TIM3->CCER |= (1<<0);   // 使能通道1输出
    TIM3->CCER &= ~(1<<1);  // 输出极性高(CC1P=0)

    // 5. 设置初始CCR值(50%占空比)
    TIM3->CCR1 = arr / 2;

    // 6. 启动TIM3
    TIM3->CR1 |= (1<<0); // 使能计数器
}

int main(void) {
    TIM3_PWM_Init(999, 71); // 1kHz,50%占空比
    while(1);
}

2. 进阶实战:动态调节占空比(LED 呼吸灯)

通过循环修改CCR1的值,实现 LED 亮度从暗到亮、再从亮到暗的呼吸效果:

c

运行

#include "stm32f10x.h"

// 延时函数(控制呼吸速度)
void delay_ms(uint32_t ms) {
    uint32_t i, j;
    for(i = 0; i < ms; i++) {
        for(j = 0; j < 1000; j++);
    }
}

// TIM3 PWM初始化函数(同前序代码)
void TIM3_PWM_Init(uint16_t arr, uint16_t psc);

int main(void) {
    uint16_t duty = 0;
    uint8_t step = 5; // 占空比调节步长(步长越小,呼吸越平缓)

    // 初始化TIM3 PWM:1kHz,初始占空比0%
    TIM3_PWM_Init(999, 71);
    TIM_SetCompare1(TIM3, 0); // 初始CCR1=0,LED熄灭

    while(1) {
        // 占空比递增(LED从暗到亮)
        while(duty < 999) {
            duty += step;
            TIM_SetCompare1(TIM3, duty); // 修改CCR1,调节占空比
            delay_ms(10); // 控制亮度变化速度
        }

        // 占空比递减(LED从亮到暗)
        while(duty > 0) {
            duty -= step;
            TIM_SetCompare1(TIM3, duty);
            delay_ms(10);
        }
    }
}
核心函数解析
  • TIM_SetCompare1(TIM3, duty):库函数用于动态修改通道 1 的CCR值,无需重新初始化定时器;
  • 寄存器版修改:直接操作TIM3->CCR1 = duty,效果与库函数一致。

3. 实战拓展:直流电机调速(L298N 驱动)

(1)硬件连接
  • TIM3_CH1(PA6)→ L298N 的 PWM 输入端(ENA);
  • STM32 PB0 → L298N 的 IN1;
  • STM32 PB1 → L298N 的 IN2;
  • L298N OUT1/OUT2 → 直流电机;
  • L298N VCC → 12V 电源;
  • L298N GND → STM32 GND(共地)。
(2)代码实现(电机正转 + 调速)

c

运行

#include "stm32f10x.h"

// 延时函数
void delay_ms(uint32_t ms);

// TIM3 PWM初始化(同前序代码)
void TIM3_PWM_Init(uint16_t arr, uint16_t psc);

// 电机GPIO初始化(IN1/IN2)
void Motor_GPIO_Init(void) {
    GPIO_InitTypeDef GPIO_InitStruct;

    // 使能GPIOB时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

    // 配置PB0、PB1为推挽输出(控制电机转向)
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStruct);

    // 初始状态:电机停止(IN1=0,IN2=0)
    GPIO_ResetBits(GPIOB, GPIO_Pin_0 | GPIO_Pin_1);
}

// 电机正转(IN1=1,IN2=0)
void Motor_Forward(void) {
    GPIO_SetBits(GPIOB, GPIO_Pin_0);
    GPIO_ResetBits(GPIOB, GPIO_Pin_1);
}

// 电机调速(0~100%占空比)
void Motor_SetSpeed(uint8_t speed) {
    if(speed > 100) speed = 100;
    uint16_t ccr = (999 * speed) / 100; // 占空比→CCR值转换
    TIM_SetCompare1(TIM3, ccr);
}

int main(void) {
    // 初始化
    TIM3_PWM_Init(999, 71); // 1kHz PWM
    Motor_GPIO_Init();

    // 电机正转,速度从0%递增至100%
    Motor_Forward();
    for(uint8_t speed = 0; speed <= 100; speed += 10) {
        Motor_SetSpeed(speed);
        delay_ms(500);
    }

    // 保持100%速度运行
    while(1);
}
核心逻辑
  • 电机转向:通过 IN1 和 IN2 的电平组合控制(正转 = IN1=1、IN2=0;反转 = IN1=0、IN2=1;停止 = IN1=0、IN2=0);
  • 电机调速:通过Motor_SetSpeed函数调节 PWM 占空比,实现速度控制(0%= 停止,100%= 最大速度)。

六、PWM 输出常见问题与避坑指南

1. PWM 无输出或输出异常

高频原因与解决方案
  • 原因 1:GPIO 模式配置错误(未设为复用推挽输出);解决:PWM 输出引脚必须配置为GPIO_Mode_AF_PP,而非普通推挽输出;
  • 原因 2:定时器时钟未使能(TIM3 挂载 APB1,误使能 APB2 时钟);解决:通用定时器(TIM2~TIM5)需调用RCC_APB1PeriphClockCmd使能时钟;
  • 原因 3:PWM 模式配置错误(如需要模式 1 却配置为模式 2);解决:根据电平需求选择 PWM 模式,LED 调光 / 电机调速优先用模式 1;
  • 原因 4:CCR值超出ARR范围(如 ARR=999,CCR=1000);解决:确保0 ≤ CCR ≤ ARR,占空比 100% 时 CCR=ARR。

2. PWM 频率不符合预期

  • 原因 1:定时器时钟频率计算错误(如未考虑 APB1 预分频系数);解决:STM32F103 默认 APB1 预分频系数 = 2,通用定时器时钟 = 72MHz;
  • 原因 2:PSCARR值计算错误(遗漏 + 1);解决:严格按公式计算,分频系数 = PSC+1,计数周期 = ARR+1;
  • 原因 3:定时器计数模式错误(如中心对齐模式导致频率减半);解决:PWM 输出优先用向上计数模式(TIM_CounterMode_Up)。

3. 动态调节占空比无效果

  • 原因 1:未使能 PWM 预装载(TIM_OC1PreloadConfig未调用);解决:使能预装载后,CCR修改在下一周期生效,避免波形畸变;
  • 原因 2:CCR值修改过快(超出人眼或电机响应速度);解决:添加合理延时(如 LED 呼吸灯延时 10ms),控制变化速度。

4. 电机调速抖动

  • 原因 1:PWM 频率过低(<1kHz),导致电机振动;解决:将 PWM 频率提升至 1kHz~20kHz,减少振动;
  • 原因 2:电源纹波过大,导致 PWM 信号不稳定;解决:在电机驱动模块电源端并联 1000μF 电解电容,滤除低频纹波;
  • 原因 3:占空比调节步长过大(如每次增减 20%);解决:减小步长(如 5%),使速度变化更平缓。

七、总结:PWM 实战核心要点与进阶方向

1. 核心要点回顾

  • PWM 输出核心:定时器时钟 + PSC+ARR(频率)+ CCR(占空比)+ 复用 GPIO
  • 关键公式:频率f_pwm = 72MHz / [(PSC+1)×(ARR+1)],占空比D=(CCR+1)/(ARR+1)×100%
  • 实战场景:固定占空比输出→动态调光→电机调速,核心是CCR值的灵活修改;
  • 避坑核心:GPIO 复用模式、时钟使能、PWM 模式、参数范围(CCR≤ARR)。

2. 进阶学习方向

  • 高级定时器 PWM:使用 TIM1/TIM8 实现互补 PWM 输出(工业电机控制,需死区控制);
  • 多通道 PWM:通过 TIM3 的 4 个通道,实现多路 LED 调光或多电机同步控制;
  • 定时器同步:多个定时器联动(如 TIM1 触发 TIM3),实现复杂时序 PWM;
  • 音频输出:生成 20Hz~20kHz 的 PWM 音频信号,经低通滤波后还原声音;
  • 舵机控制:生成 20ms 周期、1ms2ms 高电平的 PWM 波,控制舵机转角(如 0°180°)。

掌握 PWM 输出后,你已具备 STM32 “模拟控制” 的核心能力,结合之前的传感器、串口、GPIO 知识,可搭建更复杂的智能控制系统(如智能照明、无人机电机控制、机器人底盘驱动)。下一篇我们将学习 I2C 通信,实现 STM32 与 OLED 屏幕、EEPROM 的交互,让数据可视化!