4、PWM输出实验:使用STM32输出PWM波形、使用示波器测量PWM、PWM频率与占空比计算

说实话,PWM(脉冲宽度调制)是我在嵌入式开发中用得最多的功能之一。调个LED亮度、控制个舵机角度、驱动个直流电机,几乎都离不开它。这一章,我们就拿STM32来实际输出PWM波形,再用示波器看看它长什么样,最后把频率和占空比的计算彻底搞明白。

本章核心目标:通过STM32定时器的PWM模式,输出一个频率可调、占空比可调的方波信号,并用示波器验证其参数。

4.1 PWM的基本概念回顾

PWM说白了就是一种“假装模拟”的数字信号。它通过快速切换高低电平,让负载感受到的平均电压发生变化。你想想看,一个5V的方波,如果高电平占了50%的时间,低电平占50%,那平均下来就是2.5V的效果。

这里有两个关键参数:

  • 频率(Frequency):一秒内PWM波形重复的次数,单位Hz。频率决定了信号的“刷新速度”。
  • 占空比(Duty Cycle):高电平时间占整个周期的百分比。占空比决定了平均电压的大小。

我在项目中遇到过一个问题:用PWM控制电机时,频率选得太低,电机就会“咔咔”地抖动;选得太高,驱动管的开关损耗又上来了。所以频率的选择是个权衡,后面我们会细说。

4.2 STM32定时器PWM输出原理

STM32的PWM输出,本质上就是利用定时器的“自动重装载”和“比较输出”功能。我画了一张图,帮你理清这个逻辑:

STM32定时器PWM输出原理 时钟源 预分频器(PSC) 自动重装载(ARR) 计数器(CNT) 比较寄存器(CCR) PWM输出引脚 核心逻辑:CNT从0递增到ARR,当CNT < CCR时输出高电平,CNT ≥ CCR时输出低电平

工作流程其实很简单:

  1. 时钟源经过预分频器(PSC)分频后,驱动计数器(CNT)递增。
  2. 计数器从0一直数到自动重装载值(ARR),然后归零重新开始。
  3. 在计数过程中,比较寄存器(CCR)会跟CNT的值做比较。
  4. 当CNT小于CCR时,输出高电平;当CNT大于等于CCR时,输出低电平。

嗯,这里要注意:ARR决定了PWM的周期,CCR决定了占空比。两者配合,就能输出你想要的波形。

4.3 频率与占空比的计算公式

这是硬核部分,我建议你直接记住这两个公式:

PWM频率计算公式:

PWM频率 = 定时器时钟频率 / ((PSC + 1) × (ARR + 1))

占空比计算公式:

占空比 = CCR / (ARR + 1) × 100%

举个例子,假设STM32的定时器时钟是72MHz:

  • 如果PSC = 71,ARR = 999,那么PWM频率 = 72,000,000 / (72 × 1000) = 1000Hz = 1kHz
  • 如果CCR = 500,那么占空比 = 500 / 1000 × 100% = 50%

我曾经在调试一个呼吸灯项目时,把ARR设成了65535,结果LED闪烁频率低得肉眼可见,像在“喘气”一样。后来一算,频率只有1Hz左右。所以,算清楚再动手,能省不少调试时间。

4.4 实战:STM32输出PWM波形

我们以STM32F103系列为例,用定时器TIM2的通道1输出PWM。硬件连接很简单:

  • PA0(TIM2_CH1)作为PWM输出引脚
  • 示波器探头接PA0,GND接板子GND

下面是完整的初始化代码:

// 定时器PWM初始化函数
void PWM_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    TIM_OCInitTypeDef TIM_OCInitStructure;

    // 1. 使能时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    // 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_Period = 999;          // ARR = 999
    TIM_TimeBaseStructure.TIM_Prescaler = 71;        // PSC = 71
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

    // 4. 配置PWM模式1
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStructure.TIM_Pulse = 500;             // CCR = 500,占空比50%
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
    TIM_OC1Init(TIM2, &TIM_OCInitStructure);

    // 5. 使能定时器
    TIM_Cmd(TIM2, ENABLE);
}

小技巧:如果你想在运行时动态调整占空比,可以直接修改CCR的值:

TIM_SetCompare1(TIM2, new_ccr_value);  // 新值范围0~ARR

这样就能实现呼吸灯、电机调速等效果。

4.5 使用示波器测量PWM波形

代码烧进去之后,别急着高兴。你得用示波器看看实际波形对不对。我一般按这个步骤来:

  1. 连接探头:探头钩子勾住PA0,地线夹子夹住GND。
  2. 设置垂直档位:先设成2V/div,因为STM32输出是3.3V逻辑电平。
  3. 设置水平档位:根据目标频率来。1kHz的话,周期是1ms,水平档位设成200μs/div或500μs/div比较合适。
  4. 触发设置:用上升沿触发,触发电平设在1.5V左右。
  5. 读取参数:用示波器的“测量”功能,直接读出频率和占空比。

我记得有一次,我调了一个PWM程序,示波器上死活看不到波形。折腾了半天,发现是GPIO模式配成了推挽输出,而不是复用推挽输出。嗯,这种低级错误,犯过一次就不会再犯了。

4.6 常见参数配置速查表

为了方便你快速上手,我整理了一个常用配置表。假设定时器时钟为72MHz:

目标频率 PSC ARR 分辨率 适用场景
1kHz 71 999 0.1% LED调光、舵机控制
10kHz 71 99 1% 直流电机调速
50kHz 7 179 0.56% 开关电源控制
100kHz 3 179 0.56% 高频逆变器

注意:分辨率 = 1 / (ARR + 1) × 100%。ARR越小,分辨率越低,占空比调节的精细度就越差。比如ARR=99时,你只能以1%的步进调整占空比。所以,在频率和分辨率之间要做好权衡。

4.7 避坑指南

做PWM实验时,有几个坑我踩过,分享给你:

  • GPIO模式别配错:一定要用复用推挽输出(GPIO_Mode_AF_PP),不是普通推挽输出。
  • 定时器时钟源别搞混:TIM1和TIM8挂在APB2上,其他定时器挂在APB1上。APB1的时钟默认是36MHz,不是72MHz。
  • 占空比100%或0%:当CCR = ARR + 1时,输出一直为高;当CCR = 0时,输出一直为低。这在某些场景下有用,但要注意边界。
  • 示波器带宽:如果PWM频率很高(比如几百kHz),普通示波器可能测不准。确保你的示波器带宽足够。

我曾经在一个电机驱动项目里,因为没注意APB1时钟是36MHz,算出来的频率整整差了一倍。电机转得忽快忽慢,排查了好久才发现是时钟树的问题。所以,拿到一个STM32型号,第一件事就是看时钟树图。

4.8 本章小结

这一章我们从原理到实战,把PWM输出这件事彻底讲透了。你掌握了:

  • STM32定时器产生PWM的完整流程
  • 频率和占空比的计算方法
  • 如何用示波器验证波形参数
  • 常见配置参数和避坑经验

PWM这个功能,说难不难,但想用好,还是得动手多试。找个开发板,接上示波器,把上面那段代码跑一遍,看看波形是不是跟你算的一样。实践出真知,这话在嵌入式领域尤其适用。