前言
在嵌入式开发中,我们经常需要用数字信号去控制模拟设备——调节LED的亮度、控制电机的转速、让舵机转到精确的角度。微控制器的GPIO只能输出高或低两种电平,而真实世界中的很多被控对象需要的是“介于0和1之间的连续量”。脉冲宽度调制(PWM,Pulse Width Modulation) 正是解决这一矛盾的核心技术。
STM32的通用定时器几乎都支持硬件PWM生成,无需CPU干预即可输出高精度的方波信号。本文将以STM32F103C8T6、标准库函数为基础,从原理到代码,彻底讲透PWM的应用。

一、PWM基础原理
1.1 什么是PWM
PWM是一种对模拟信号进行数字编码的方法——用一连串固定频率、但占空比可调的方波,来等效地表示一个模拟电压。
关键参数:
| 参数 | 含义 |
|---|---|
| 周期(T) | 一个完整方波的时间,由定时器的自动重载值决定 |
| 频率(f = 1/T) | 每秒产生的方波个数 |
| 占空比(Duty Cycle) | 高电平时间占整个周期的百分比 |
| 分辨率 | 占空比可调节的最小粒度(16位定时器为65536级) |
例如,一个周期为1ms、高电平0.3ms、低电平0.7ms的方波,其占空比就是30%。
1.2 为什么PWM能等效为模拟量?
大多数物理系统(LED、电机线圈等)具有惯性或低通特性,无法跟随瞬间的高频变化,其表现趋近于对电压的平均效果。根据面积等效原理:
平均电压 ≈ 高电平电压 × 占空比
当高电平为3.3V、占空比为50%时,负载感受到的平均电压约为1.65V。调节占空比,就等于调节了输出电压。
1.3 定时器如何产生PWM?
STM32的定时器采用比较输出的方式产生PWM(以向上计数为例):
- 定时器从0开始向上计数;
- 设置自动重载值(ARR),决定计数周期(即PWM周期);
- 设置比较值(CCRx),当计数器值 < CCRx 时输出高电平,当计数器值 ≥ CCRx 时输出低电平(极性可设);
- 计数器达到ARR后自动归零,开始下一个周期。
全过程由硬件自动完成,无需中断介入。
1.4 关键参数计算
以STM32F103C8T6的通用定时器TIM2为例(挂载APB1,系统时钟72MHz,APB1预分频系数为2,则定时器时钟为72MHz):
- PWM频率 = 定时器时钟 / ((PSC + 1) × (ARR + 1))
- 占空比 = CCRx / (ARR + 1) (高电平有效时)
- 分辨率 = ARR + 1
实际设计时,先确定需要的PWM频率,再选取合适的PSC和ARR。
二、硬件接线
PWM信号的硬件连接非常简单,只需将定时器对应的通道引脚连接到被控电路即可。
常用通道引脚(STM32F103C8T6):
| 定时器通道 | 输出引脚 | 典型用途 |
|---|---|---|
| TIM2_CH1 | PA0 | LED调光、舵机控制 |
| TIM2_CH2 | PA1 | 双路电机驱动 |
| TIM3_CH1 | PB4 | 呼吸灯 |
| TIM3_CH2 | PB5 | 舵机 |
| TIM1_CH1 | PA8 | 带死区的H桥电机驱动 |
注意:
- 输出引脚必须配置为复用推挽输出;
- 大功率负载需通过MOS管或电机驱动模块隔离;
- 舵机通常需独立供电。
三、标准库软件实现(可直接使用)
开发环境:Keil MDK5,STM32F10x_StdPeriph_Lib_V3.5.0
示例:TIM2_CH1(PA0)输出1kHz PWM,实现LED呼吸灯;附舵机控制(50Hz)参数。
3.1 简单毫秒延时函数(基于SysTick)
为了方便演示呼吸灯,先提供一个通用的毫秒延时函数:
#include "stm32f10x.h"
void delay_ms(uint32_t ms) {
// 配置SysTick为1ms中断(不开启中断,仅用查询)
SysTick->LOAD = (SystemCoreClock / 1000) - 1; // 对于72MHz, LOAD = 71999
SysTick->VAL = 0;
SysTick->CTRL = SysTick_CTRL_ENABLE_Msk | SysTick_CTRL_CLKSOURCE_Msk; // 使能,使用内部时钟
for (uint32_t i = 0; i < ms; i++) {
while (!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk)); // 等待计数到0
}
SysTick->CTRL = 0; // 关闭SysTick
}
注:此延时使用了Cortex-M3的SysTick定时器,不依赖外设定时器。
3.2 PWM初始化代码(TIM2_CH1, 1kHz)
/* 定义目标参数:频率1kHz,TIM2时钟=72MHz */
#define PWM_ARR 999 // 自动重载值(决定频率)
#define PWM_PSC 71 // 预分频器值(72MHz / (71+1) = 1MHz 计数频率)
void PWM_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
// 1. 开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // GPIOA
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // TIM2
// 2. 配置PA0为复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 3. 定时器基本配置(向上计数)
TIM_TimeBaseStructure.TIM_Prescaler = PWM_PSC; // 预分频
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_Period = PWM_ARR; // 自动重装值
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
// 4. PWM通道配置(模式1,高电平有效)
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; // PWM模式1
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; // 高电平有效
TIM_OCInitStructure.TIM_Pulse = 0; // 初始占空比0%
TIM_OC1Init(TIM2, &TIM_OCInitStructure); // 通道1
// 5. 使能定时器
TIM_Cmd(TIM2, ENABLE);
}
3.3 占空比调节函数
/**
* @brief 设置PWM占空比(0~100%)
* @param duty : 占空比百分数(0~100)
*/
void PWM_SetDuty(uint8_t duty) {
uint16_t pulse;
if (duty > 100) duty = 100;
// 计算比较值:占空比% × (ARR+1) / 100
// 当duty=100时,pulse = ARR+1,可实现100%输出
pulse = (uint16_t)((uint32_t)duty * (PWM_ARR + 1) / 100);
TIM_SetCompare1(TIM2, pulse); // 更新通道1比较值
}
3.4 呼吸灯示例(main函数)
int main(void) {
uint8_t duty = 0;
int8_t dir = 1;
PWM_Init(); // 初始化PWM,PA0
while (1) {
PWM_SetDuty(duty); // 更新占空比
duty += dir;
if (duty >= 100) {
duty = 100;
dir = -1;
} else if (duty == 0) {
dir = 1;
}
delay_ms(10); // 每10ms变化一次,形成渐变效果
}
}
将LED正极接PA0,负极通过220Ω电阻接地,就能看到平滑的呼吸灯。
3.5 舵机控制示例(50Hz,独立参数)
舵机要求周期20ms(50Hz),高电平脉宽0.5ms2.5ms对应0°180°。
只需修改初始化参数:
/* 舵机PWM:50Hz, TIM2时钟=72MHz */
#define SERVO_ARR 19999 // (72MHz / (71+1)) / 50 - 1 = 1MHz / 50 - 1 = 19999
#define SERVO_PSC 71 // 预分频后频率=1MHz
舵机角度设置函数(1MHz计数,1us/计数值):
void Servo_SetAngle(uint8_t angle) {
uint16_t pulse;
if (angle > 180) angle = 180;
// 脉宽 = 500us + (角度/180°) * 2000us
pulse = 500 + (uint32_t)angle * 2000 / 180; // 结果在500~2500之间
TIM_SetCompare1(TIM2, pulse);
}
在主函数中调用 Servo_SetAngle(90) 即可转动到90°位置。
四、调试与常见问题
4.1 用示波器或逻辑分析仪检查
- 频率:测量一个完整周期的宽度,验证是否为目标值。
- 占空比:测量高电平时间,计算是否与设定一致。
- 波形质量:观察方波是否干净,有无过冲(一般GPIO输出良好)。
4.2 常见故障排查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 无波形输出 | GPIO未配置为复用推挽;定时器未使能;通道未配置 | 检查初始化代码,确保调用了TIM_Cmd、TIM_OC1Init |
| 占空比无法调节 | 写入了错误的通道;计算值溢出 | 确认TIM_SetCompare1对应通道1;检查比较值范围 |
| 频率偏差大 | 定时器时钟计算错误(忽略APB1×2因子) | 系统时钟72MHz时TIM2时钟为72MHz,重新计算PSC/ARR |
| LED闪烁而非渐变 | PWM频率太低(<50Hz),人眼可察觉 | 提高频率至1kHz以上 |
| 舵机不动作或抖动 | 供电不足;周期/脉宽不正确;信号反相 | 单独供电,用示波器确认50Hz周期和0.5~2.5ms脉宽 |
4.3 多路PWM输出
同一通用定时器的不同通道(如TIM2_CH1/CH2/CH3/CH4)可以共享频率,但各自独立设置占空比。只需额外调用 TIM_OC2Init 等函数并配置对应的GPIO即可。
五、总结
本文详细讲解了PWM的基本原理,并基于STM32标准库给出了完整可用的硬件PWM初始化、占空比调节、呼吸灯和舵机驱动代码。所有代码都经过逻辑分析仪验证,可直接复制到工程中使用。
掌握了PWM,你就拥有了一座连接数字与模拟世界的桥梁。无论是智能车、四轴飞行器还是智能家居,都离不开这项核心技术。
有任何疑问或需要讨论的地方,欢迎在评论区留言,我们一起交流!
8万+

被折叠的 条评论
为什么被折叠?



