STM32 进阶封神之路(十六):PWM 波深度实战 —— 定时器输出 + LED 调光 + 电机调速(库函数 + 寄存器)
上一篇我们掌握了 DHT11 单总线温湿度采集,这一篇聚焦 STM32 的核心控制功能 ——PWM 波输出。PWM(脉冲宽度调制)是嵌入式控制中最常用的模拟信号替代方案,通过调节高电平占空比,可实现 LED 调光、电机调速、音频输出等场景,其核心依赖 STM32 的通用定时器(如 TIM2~TIM5)或高级定时器(TIM1/TIM8)。
本文基于实战资料,从 PWM 核心概念、定时器 PWM 原理、硬件配置,到 STM32 代码实现(固定占空比 + 动态调光 + 电机调速),手把手带你吃透 PWM 输出的全流程,让你从 “理论认知” 落地到 “实际控制”!
一、复习回顾:PWM 与定时器核心基础衔接
在深入 PWM 实战前,先衔接关键知识,确保逻辑连贯:
- PWM 核心关联:PWM 波由 STM32 的通用定时器或高级定时器生成,依赖定时器的 “计数模式” 和 “比较模式”;
- 定时器基础:通用定时器(如 TIM3)支持向上 / 向下 / 中心对齐计数,PWM 输出常用向上计数模式(计数器从 0 增至 ARR,循环往复);
- 核心寄存器:PWM 输出关键寄存器为
ARR(自动重装载值,决定 PWM 频率)和CCR(比较值,决定占空比); - 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 输出关键逻辑(向上计数模式)
- 定时器时钟经预分频器(PSC)分频后,驱动计数器(CNT)从 0 开始向上计数;
- 计数器值(CNT)与比较寄存器值(CCR)实时比较;
- 当
CNT < CCR时,PWM 输出高电平;当CNT ≥ CCR时,输出低电平(PWM 模式 1,最常用); - 计数器计数至
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),计算PSC和ARR值:
- 已知:
f_clk=72MHz,f_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=71,ARR=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_CH1 | PA6 | 复用推挽输出(AF_PP) | LED 调光、电机 PWM 输出 |
| TIM3_CH2 | PA7 | 复用推挽输出 | 双路 PWM 控制(如双色 LED) |
| TIM3_CH3 | PB0 | 复用推挽输出 | 辅助 PWM 输出 |
| TIM3_CH4 | PB1 | 复用推挽输出 | 辅助 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:
PSC或ARR值计算错误(遗漏 + 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 周期、1ms
2ms 高电平的 PWM 波,控制舵机转角(如 0°180°)。
掌握 PWM 输出后,你已具备 STM32 “模拟控制” 的核心能力,结合之前的传感器、串口、GPIO 知识,可搭建更复杂的智能控制系统(如智能照明、无人机电机控制、机器人底盘驱动)。下一篇我们将学习 I2C 通信,实现 STM32 与 OLED 屏幕、EEPROM 的交互,让数据可视化!