PWM作为硬件中几乎不可或缺的存在,学会 PWM,等于打通了 STM32 的“定时器体系”。学一次,STM32 全系列(甚至 AVR、PIC、ESP32)都能通用。硬件只要一个 I/O 就能驱动功率模块,非常省成本。不会 PWM,几乎没法独立做电机控制、灯光调节、舵机、开关电源类项目。
1.PWM 的本质是什么(从电子角度说)
PWM(Pulse Width Modulation)——脉冲宽度调制
它并不是直接输出“某个电压值”,而是输出一个 固定周期、可变占空比 的方波。
当你用这个方波去驱动:
LED → 人眼视觉暂留会“平均”亮度。
电机 → 电机电感+惯性会把高频方波“平均”成类似直流的电压。
舵机 → 内部单片机通过高电平脉宽判断位置。
开关电源 → 控制 MOSFET 的导通时间来调节能量传输。
占空比定义
假设周期 = T, 高电平时间 = Th,
占空比 D = Th / T
0% → 恒低
100% → 恒高
50% → 高低时间相等
2.输出比较通道
1.最左边:比较与“输出模式控制器”
输入:
CNT > CCR1、CNT = CCR1 两个比较结果
ETR(外部触发脚)
TIM1_CCMR1 里的配置位:OC1M[2:0](选择 PWM1/2、切换、强制高/低等),OC1CE(OCref 清除使能)
做的事:
根据选定的 输出比较模式(OC1M)和比较结果,产生一个“理想参考波形” OC1REF。
PWM1:CNT < CCR1 → OC1REF=有效;否则无效
PWM2:相反
Toggle/强制高/低:按模式固定/翻转
若 OC1CE=1 且 ETR 上升沿到来 → 强制把 OC1REF 拉为无效(快速关断/保护用)
记忆:OC1REF 只是“内部参考信号”,还没考虑死区、极性、关闭时状态。
2中间:死区发生器(高级定时器专属)
输入: OC1REF
控制位: TIM1_BDTR.DTG[7:0](死区时间编码)
输出: 两路互补且带空窗的信号:
OC1_DT:给主输出(OC1)
OC1N_DT:给互补输出(OC1N)
它们之间永不同时为有效,并在换向时插入 死区时间,避免上下管直通。
F1 的 DTG 为 8 位分段编码(三段倍率),实际死区 = 编码值 × t_DTS(或乘 2/8/16 的倍率,具体按手册分段公式换算)。核心记住:数越大,死区越长。
3右侧两组多路选择器:运行/空闲/故障时的“强制电平”
你看到每路(OC1、OC1N)在进入极性反相器之前都有一个小 MUX,它的若干个输入是:
来自死区单元的 OC1_DT / OC1N_DT(“正常工作波形”)
常量 ‘0’ 或 ‘1’(“强制电平”)
它们在什么情况下选谁?
取决于高级定时器的 关断/空闲策略:
运行关断(OSSR):当通道被软件关断(CC1E/CC1NE=0)但定时器还在运行时,输出要进入哪个“安全电平”。
空闲关断(OSSI):当主输出被关闭(MOE=0,比如 Break 触发或还没使能)时,引脚保持哪个“空闲电平”。
OISx/OISxN(在 TIM1_CR2):定义“空闲电平”到底是 0 还是 1(注意是在极性反相之前的电平)。
一句话:MUX 负责在 正常波形 和 强制 0/1 之间选择,OSSR/OSSI/OISx 决定“关掉时该保持什么电平”。
4极性(反相器)
主通道用 TIM1_CCER.CC1P
互补通道用 TIM1_CCER.CC1NP
当这些位为 1 时,对应通道在 MUX 之后做一次逻辑反相。
注意:OISx 的 0/1 是在反相之前定义的,所以最终引脚电平 =(OISx 设定)→(再看 CC1P/CC1NP 是否反相)。
5最右:输出使能电路 & 全局门控
通道局部使能:TIM1_CCER.CC1E(主)、CC1NE(互补)
全局主输出使能(高级定时器特有):TIM1_BDTR.MOE 必须为 1,否则各通道即便 CCxE=1 也出不来波形
还会受 刹车/锁定(BDTR.BKE、LOCK)等保护逻辑控制
结合 OSSR/OSSI/OISx,在被禁止时输出保持“安全/空闲”电平
最终,经过这些门控后,才真正到达芯片引脚 OC1 与 OC1N。
把整条链路串一下(典型的 PWM1 互补输出场景)
比较:CNT 与 CCR1 比 → 输出模式控制器按 OC1M 生成 OC1REF
死区:OC1REF → 生成互补且不重叠的 OC1_DT / OC1N_DT(间隔 = DTG)
关断策略:
正常运行且 CC1E/CC1NE=1 且 MOE=1 → MUX 选 带死区的波形
若你清了 CC1E(通道禁用)且 OSSR=1 → MUX 选 强制 0/1(保持安全电平)
若发生 Break 或 MOE=0 且 OSSI=1 → MUX 选 空闲电平(由 OIS1/OIS1N 定义)
极性:按 CC1P/CC1NP 反相或不反相
输出门:再经过 CC1E/CC1NE 与 MOE 的最终门控 → 到 OC1/OC1N 引脚
各寄存器在这张图里各司其职(速查)
CCMR1:OC1M(决定 OC1REF 的生成方式),OC1CE(ETR 清除 OC1REF)
BDTR:DTG(死区)、MOE(主输出总开关)、OSSR/OSSI(运行/空闲的关断选择)
CR2:OIS1/OIS1N(空闲电平设定)
CCER:CC1E/CC1NE(局部开关)、CC1P/CC1NP(极性)
两个常见“为什么”
为什么有 OC1 和 OC1N 两路?
做半桥/全桥时分别去驱动上管与下管,需要互补且不重叠的门控信号,死区发生器确保不直通。
为什么关掉时还要管 0/1?
关断瞬间的引脚电平关系到功率管安全与外部电路的默认状态,OSSR/OSSI + OISx 就是为“安全、可预测”而设计的。
3.PWM 生成过程(边沿对齐模式)
假设:
PSC = 71(分频 72)
ARR = 99
CCR1 = 30
执行过程:
时钟源:
CK_TIMER = 72 MHz(假设来自 APB1 倍频)
CK_CNT = CK_TIMER / (PSC + 1) = 72 MHz / 72 = 1 MHz
→ CNT 每 1 μs 加 1。
计数周期:
CNT 从 0 → 99(ARR),共 100次计数 → 周期 = 100×1 μs = 1 ms → f = 100Hz。
比较过程:
CNT < CCR1(0–29) → 输出为高(PWM1 模式,极性高)。
CNT = 30时 → 比较事件触发,输出变低。
CNT 继续到 99 → 更新事件(CNT 归零),输出回到高。
形成波形:
周期 1 ms,高电平 500 μs → 占空比 = 30/ 100 = 30%。
模式比较
4.PWM基本结构
代码例子(呼吸灯)
//-----------------------------
// STM32 TIM2_CH1 PWM 输出示例(1kHz,0~100%呼吸)
// 对应结构图流程:PSC -> CNT/ARR -> CCR -> 输出模式控制器 -> 极性 -> 输出使能 -> GPIO
//-----------------------------#include "stm32f10x.h"void PWM_Init(void)
{GPIO_InitTypeDef GPIO_InitStructure;TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;TIM_OCInitTypeDef TIM_OCInitStruct;// 1) 开启 GPIOA、AFIO、TIM2 时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // GPIO 模块时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // 复用功能时钟RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // TIM2 时钟// 2) 部分重映射 TIM2_CH1 到 PA15,并关闭 JTAG(保留 SWD)// 图中相当于把“输出使能电路”的信号线连到 PA15 引脚GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENABLE); GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);// 3) 配置 PA15 为复用推挽输出,交给定时器驱动GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);// 4) 定时器内部时钟作为计数源TIM_InternalClockConfig(TIM2);// 5) 配置时基单元(PSC 和 ARR)—— 图中“计数器单元”TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1; // 滤波采样分频=1TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式TIM_TimeBaseInitStruct.TIM_Period = 100 - 1; // ARR=99,100个计数TIM_TimeBaseInitStruct.TIM_Prescaler = 720 - 1; // PSC=719,把72MHz分频到100kHz// PWM频率 = 100kHz / 100 = 1kHzTIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0; // 重复计数器不用TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStruct);// 6) 配置输出比较单元(CCR、模式、极性)—— 图中“输出模式控制器”TIM_OCStructInit(&TIM_OCInitStruct); TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; // PWM1模式:CNT<CCR为有效TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; // 使能输出TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; // 有效电平=高TIM_OCInitStruct.TIM_Pulse = 0; // CCR1初值=0,占空比0%TIM_OC1Init(TIM2, &TIM_OCInitStruct);// 7) 开启 ARR 与 CCR1 预装载(影子寄存器)—— 避免更新毛刺TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable);TIM_ARRPreloadConfig(TIM2, ENABLE);// 8) 启动定时器TIM_Cmd(TIM2, ENABLE);
}// 设置占空比(0~100)
void PWM_SetCompare1(uint16_t compare)
{TIM_SetCompare1(TIM2, compare); // 图中就是改变“红线阈值”
}int main(void)
{uint8_t i;PWM_Init();while(1){// 占空比 0% -> 100%for(i=0; i<=100; i++){PWM_SetCompare1(i); // 修改 CCR1Delay_ms(1); // 延时 1ms}// 占空比 100% -> 0%for(i=0; i<=100; i++){PWM_SetCompare1(100 - i);Delay_ms(1);}}
}
结构图模块 | 代码位置 | 作用说明 |
---|---|---|
PSC 预分频器(粉色块左下) | TIM_Prescaler = 720 - 1; | 把 72 MHz 系统时钟分频到 100 kHz,减慢计数速度 |
CNT/ARR 计数器 + 自动重装器(粉色块中,锯齿波) | TIM_Period = 100 - 1; + TIM_CounterMode = TIM_CounterMode_Up; | CNT 从 0 数到 99 后自动回零形成锯齿波;计数模式为向上计数 |
CCR 捕获/比较器(绿色“CCR”块) | TIM_Pulse = 0; + TIM_SetCompare1() | 设置红线阈值,当 CNT < CCR 输出高电平,当 CNT ≥ CCR 输出低电平,占空比由 CCR 决定 |
输出模式控制器(绿色框“CNT<CCR时REF有效”) | TIM_OCMode = TIM_OCMode_PWM1; | 选择 PWM1 模式:CNT < CCR 时 REF = 有效电平 |
极性选择(绿色“极性选择”块) | TIM_OCPolarity = TIM_OCPolarity_High; | REF 有效电平为高电平 |
输出使能(极性选择后到 GPIO 前) | TIM_OutputState = TIM_OutputState_Enable; + TIM_Cmd(TIM2, ENABLE); | 打开通道输出,启动定时器,让波形真正到达引脚 |
GPIO 复用输出(GPIO 块) | GPIO_Mode_AF_PP; + GPIO_PinRemapConfig() + GPIO_Remap_SWJ_JTAGDisable; | 配置 PA15 为复用推挽输出,将 TIM2_CH1 信号映射到 PA15 并关闭 JTAG 占用 |
5.运用场景
应用场景 | PWM作用 |
---|---|
LED调光 | 通过占空比控制亮度,肉眼平滑过渡 |
直流电机调速 | 占空比决定电机转速,反应快 |
舵机控制 | 用 PWM 脉宽来表示角度(常见 1–2ms 脉宽) |
开关电源 | 控制 MOSFET 导通时间来稳定输出电压 |
音频信号生成 | PWM 高频调制后滤波得到模拟波形 |
加热器温控 | 占空比决定加热功率 |
通信协议 | 红外遥控等通过 PWM 编码数据 |