STM32 PWM脉冲宽度调制详解——标准库函数实现,附呼吸灯与舵机驱动(硬件总结三)

前言

在嵌入式开发中,我们经常需要用数字信号去控制模拟设备——调节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(以向上计数为例):

  1. 定时器从0开始向上计数;
  2. 设置自动重载值(ARR),决定计数周期(即PWM周期);
  3. 设置比较值(CCRx),当计数器值 < CCRx 时输出高电平,当计数器值 ≥ CCRx 时输出低电平(极性可设);
  4. 计数器达到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_CH1PA0LED调光、舵机控制
TIM2_CH2PA1双路电机驱动
TIM3_CH1PB4呼吸灯
TIM3_CH2PB5舵机
TIM1_CH1PA8带死区的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_CmdTIM_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,你就拥有了一座连接数字与模拟世界的桥梁。无论是智能车、四轴飞行器还是智能家居,都离不开这项核心技术。

有任何疑问或需要讨论的地方,欢迎在评论区留言,我们一起交流!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值