[应用相关] STM32的PWM

[复制链接]
1108|16
小海师 发表于 2025-9-4 09:01 | 显示全部楼层 |阅读模式
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% → 高低时间相等

46968b8e4d655cd7.png


2.输出比较通道

2336968b8e4cfa842c.png


3035568b8e4c9d05b8.png


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%。

模式比较


8269668b8e4ab7dd17.png



4.PWM基本结构

1451968b8e4a5efa3f.png


代码例子(呼吸灯)
//-----------------------------
// 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;          // 滤波采样分频=1
    TIM_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 = 1kHz
    TIM_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); // 修改 CCR1
            Delay_ms(1);        // 延时 1ms
        }
        // 占空比 100% -> 0%
        for(i=0; i<=100; i++)
        {
            PWM_SetCompare1(100 - i);
            Delay_ms(1);
        }
    }
}

5641768b8e49ad3a27.png


6960468b8e48875fec.png


5.运用场景

5793868b8e4809e987.png


————————————————
版权声明:本文为CSDN博主「JasmineX-1」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/2504_92863595/article/details/150425355

jkl21 发表于 2025-9-4 21:40 | 显示全部楼层
占空比的范围和精度需要根据具体应用进行设置。
claretttt 发表于 2025-9-6 11:03 | 显示全部楼层
STM32 的 PWM 功能强大但复杂度较高,需重点关注 时钟配置、输出极性、死区时间、同步机制 及 硬件滤波。
youtome 发表于 2025-9-6 12:15 | 显示全部楼层
若使用中断控制PWM,需合理设置优先级
deliahouse887 发表于 2025-9-6 13:42 | 显示全部楼层
分别使能定时器和对应GPIO的时钟。
qiufengsd 发表于 2025-9-6 15:41 | 显示全部楼层
将GPIO引脚配置为复用推挽输出模式 ,以支持PWM信号输出
loutin 发表于 2025-9-6 16:40 | 显示全部楼层
STM32 提供了非常强大且灵活的定时器 PWM 输出功能
sesefadou 发表于 2025-9-6 19:09 | 显示全部楼层
通过 ​​定时器的输出比较通道              
deliahouse887 发表于 2025-9-6 22:36 | 显示全部楼层
使用适当的滤波和去耦电容              
febgxu 发表于 2025-9-13 11:22 | 显示全部楼层
选择合适的定时器 来生成PWM信号。
fengm 发表于 2025-9-13 14:43 | 显示全部楼层
对于需要高精度和稳定性的PWM输出,可以考虑使用DMA来传输数据。
primojones 发表于 2025-9-13 16:21 | 显示全部楼层
当ARR等于CCR时,理论上应为持续高电平,但实际可能出现小脉冲。
pmp 发表于 2025-9-13 20:51 | 显示全部楼层
占空比分辨率由ARR决定              
hudi008 发表于 2025-9-13 22:24 | 显示全部楼层
PWM 是通过 ​​定时器 产生​​固定频率、可调占空比​​的方波信号
mattlincoln 发表于 2025-9-14 14:10 | 显示全部楼层
设置引脚为推挽输出或开漏输出              
pixhw 发表于 2025-9-14 15:48 | 显示全部楼层
通用定时器(TIM2-TIM5):适合基础PWM输出,支持16位分辨率。
高级定时器(TIM1/TIM8):支持互补输出、死区时间、刹车功能,适用于电机控制。
tabmone 发表于 2025-9-14 17:37 | 显示全部楼层
避免同一定时器用于PWM和编码器接口、ADC触发等。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

89

主题

272

帖子

1

粉丝
快速回复 在线客服 返回列表 返回顶部