dreameng

通用计时器PWM输出实现呼吸灯(基于寄存器实现)

2026/05/17
13
0

实现LED的呼吸效果,需要有节奏的变化LED的输入电压,由于单片机的输出是数字信号(要么高电平、要么低电平),常规情况下通过引脚控制LED时,LED灯只能处于亮/灭状态。为了实现呼吸灯效果,需要调节引脚输出方波的占空比(PWM),进而调节LED的输入电压,达到呼吸的效果。

硬件

我使用的开发板是野火STM32F103ZET6,LED灯的原理图如下所示:

LED原理图
LED原理图
  • LED3的负极接在PB5引脚,LED4的负极接在PB0引脚,LED5接在PB1引脚。

  • PB5引脚使用是TIM3的第二通道,PB0使用的是TIM3的第三通道,PB1使用的是TIM3的第四通道。

功能分析

  • 由于使用引脚产生PWM方波控制LED的亮暗,所以引脚需要设置为复用推挽输出

  • 在我使用的开发板中,LED3与PB5(TIM3_CH2)、LED4与PB0(TIM3_CH3)、LED5与PB1(TIM3_CH4)连接。但stm32引脚的默认复用功能设置中,PB5引脚默认没有复用TIM3_CH2,需要手动重定义后,才能复用TIM3_CH2。而LED4则默认复用了TIM3_CH3。基于以上原因,如果不手动映射,会发现近LED4有呼吸效果,LED3没有。

    STM32F10X数据手册的引脚定义-PB5
    STM32F10X数据手册的引脚定义-PB0

功能实现

LED的GPIO初始化

  • 头文件(bsp_led.h)

    #ifndef __BSP_LED_H
    #define __BSP_LED_H
    #include "stm32f10x.h"
    
    /* 定义LED连接的GPIO端口, 用户只需要修改下面的代码即可改变控制的LED引脚 */
    #define LED3_GPIO_PORT      GPIOB                   /* LED3的GPIO端口 */
    #define LED3_GPIO_PIN       GPIO_Pin_5
    
    #define LED4_GPIO_PORT      GPIOB                 /* LED4的GPIO端口 */
    #define LED4_GPIO_PIN       GPIO_Pin_0
    
    #define LED5_GPIO_PORT      GPIOB                   /*LED5的GPIO端口 */
    #define LED5_GPIO_PIN       GPIO_Pin_1
    
    /** the macro definition to trigger the led on or off
    * 1 - off
    * 0 - on
    */
    #define ON  0
    #define OFF 1
    
    /* 使用标准的固件库控制IO*/
    #define LED3(a) if (a)  \
            GPIO_SetBits(LED3_GPIO_PORT,LED3_GPIO_PIN);\
            else    \
            GPIO_ResetBits(LED3_GPIO_PORT,LED3_GPIO_PIN)
    
    #define LED4(a) if (a)  \
            GPIO_SetBits(LED4_GPIO_PORT,LED4_GPIO_PIN);\
            else    \
            GPIO_ResetBits(LED4_GPIO_PORT,LED4_GPIO_PIN)
    
    #define LED5(a) if (a)  \
            GPIO_SetBits(LED5_GPIO_PORT,LED5_GPIO_PIN);\
            else    \
            GPIO_ResetBits(LED5_GPIO_PORT,LED5_GPIO_PIN)
    
    
    /* 直接操作寄存器的方法控制IO */
    #define digitalHi(p,i)     {p->BSRR=i;}  //输出为高电平
    #define digitalLo(p,i)     {p->BRR=i;}   //输出低电平
    #define digitalToggle(p,i) {p->ODR ^=i;} //输出反转状态
    
    
    /* 定义控制IO的宏 */
    #define LED3_TOGGLE    digitalToggle(LED3_GPIO_PORT,LED3_GPIO_PIN)
    #define LED4_TOGGLE    digitalToggle(LED4_GPIO_PORT,LED4_GPIO_PIN)
    #define LED5_TOGGLE    digitalToggle(LED5_GPIO_PORT,LED5_GPIO_PIN)
    
    /**
     * @brief  初始化控制LED的IO,使用标准外设库函数
     */
    void LED_GPIO_Config(void);
    
    /**
     * @brief  初始化控制LED的IO,使用直接操作寄存器的方法
     */
    void LED_GPIO_Config_REG(void);
    #endif
  • 源文件(bsp_led.c)

    #include "bsp_led.h"
    
    /**
    * @brief  初始化控制LED的IO
    * @param  无
    * @retval 无
    */
    void LED_GPIO_Config(void)
    {
        /*定义一个GPIO_InitTypeDef类型的结构体*/
        GPIO_InitTypeDef GPIO_InitStructure;
    
        /*开启LED相关的GPIO外设时钟*/
        RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB , ENABLE);
        
        /*选择要控制的GPIO引脚*/
        GPIO_InitStructure.GPIO_Pin = LED3_GPIO_PIN;
    
        /*设置引脚模式为通用推挽输出*/
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出
    
        /*设置引脚速率为50MHz */
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    
        /*调用库函数,初始化GPIO*/
        GPIO_Init(LED3_GPIO_PORT, &GPIO_InitStructure);
    
        /*选择要控制的GPIO引脚*/
        GPIO_InitStructure.GPIO_Pin = LED4_GPIO_PIN;
    
        /*调用库函数,初始化GPIO*/
        GPIO_Init(LED4_GPIO_PORT, &GPIO_InitStructure);
    
        /*选择要控制的GPIO引脚*/
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 通用推挽输出
        GPIO_InitStructure.GPIO_Pin = LED5_GPIO_PIN;
    
        /*调用库函数,初始化GPIO*/
        GPIO_Init(LED5_GPIO_PORT, &GPIO_InitStructure);
    
        /* 关闭所有led灯 */
        GPIO_SetBits(LED3_GPIO_PORT, LED3_GPIO_PIN);
        GPIO_SetBits(LED4_GPIO_PORT, LED4_GPIO_PIN);
        GPIO_SetBits(LED5_GPIO_PORT, LED5_GPIO_PIN);
    }
    
    void LED_GPIO_Config_REG(void)
    {
        /*开启LED相关的GPIO外设时钟*/
        RCC->APB2ENR |= RCC_APB2Periph_GPIOB;
    
        /*设置引脚模式为通用推挽输出,速率为50MHz */
        LED3_GPIO_PORT->CRL |= GPIO_CRL_MODE5; // 输出模式11,最大速率50MHz
        LED3_GPIO_PORT->CRL |= GPIO_CRL_CNF5_1; // 复用推挽输出10
        LED3_GPIO_PORT->CRL &= ~ GPIO_CRL_CNF5_0; // 复用推挽输出10
    
        LED4_GPIO_PORT->CRL |= GPIO_CRL_MODE0; // 输出模式11,最大速率50MHz
        LED4_GPIO_PORT->CRL |= GPIO_CRL_CNF0_1; // 复用推挽输出10
        LED4_GPIO_PORT->CRL &= ~ GPIO_CRL_CNF0_0; // 复用推挽输出10
    
        LED5_GPIO_PORT->CRL |= GPIO_CRL_MODE1; // 输出模式11,最大速率50MHz
        LED5_GPIO_PORT->CRL &= ~ (GPIO_CRL_CNF1_1 |GPIO_CRL_CNF1_0); // 推挽输出00
    
        /* 关闭所有led灯 */
        digitalHi(LED3_GPIO_PORT, LED3_GPIO_PIN);
        digitalHi(LED4_GPIO_PORT, LED4_GPIO_PIN);
        digitalHi(LED5_GPIO_PORT, LED5_GPIO_PIN);
    }

通用时钟初始化

初始化TIM3

  • 头文件(bsp_timer.h)

    #ifndef __BSP_TIMER_H
    #define __BSP_TIMER_H
    #include "stm32f10x.h"
    
    /**
     * @brief 定时器配置函数,使用寄存器的方式
     */
    void TIM3_Config(void); // 基础定时器3配置函数
    
    void TIM3_Start(void); // 启动定时器
    
    void TIM3_Stop(void); // 停止定时器
    
    void TIM3_SetCH2DutyCycle(uint8_t duty); // 设置通道2占空比,参数范围0-100,对应0%-100%
    
    void TIM3_SetCH3DutyCycle(uint8_t duty); // 设置通道3占空比,参数范围0-100,对应0%-100%
    
    #endif
  • 源文件(bsp_timer.c)

    #include "bsp_timer3.h"
    
    void TIM3_Config(void)
    {
        // 1. 开启时钟
        RCC->APB1ENR |= RCC_APB1ENR_TIM3EN; // 使能TIM3时钟
    
        // 使能AFIO时钟,并进行TIM3部分重映射,使TIM3_CH2映射到PB5(如果不重定向,会发现LED3没有呼吸效果)
        RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;       // 使能 AFIO 时钟,在手册的2.1节的架构图中,AFIO在APB2总线上。
        AFIO->MAPR &= ~AFIO_MAPR_TIM3_REMAP;      // 保险起见,先将TIM3_REMAP清零,以防第0位被设置为1,导致完全重映射。
        AFIO->MAPR |= AFIO_MAPR_TIM3_REMAP_PARTIALREMAP; // TIM3 部分重映射,在手册的8.4.2节中有说明,部分重映射后TIM3_CH2映射到PB5
    
        // 2. 配置GPIO的工作模式、速度(如果在GPIO初始化时,已经设置,这里不用重复设置)
    
        // 3. 配置定时器的工作模式、计数频率、自动重装载值等
        // 3.1 定时器预分频器,72MHz时钟分频为10000Hz,每计数一次需要100us。
        TIM3->PSC = 7200 - 1; 
    
        // 3.2 自动重装载值,99。每隔10ms(100us * 100 = 10000us = 10ms)溢出一次。
        // 这时PWM的周期,频率为 72000000 HZ/ (7200 * 100) = 100Hz。缩放预分频*自动重装载值,得到PWM频率。
        TIM3->ARR = 99; 
    
        // 3.3 配置定时器的工作模式,向上计数模式,边沿对齐模式。
        TIM3->CR1 &= ~TIM_CR1_DIR; // 设置为0,向上计数
    
        // 3.4 设置通道2的占空比,初始值为0,即TIM3->CCR2=0;(可选)
        TIM3->CCR2 = 0;
    
        // 3.5 捕获/输出选择,CC2S=00,输出模式
        // 通道2
        TIM3->CCMR1 &= ~TIM_CCMR1_CC2S;
        // 通道3
        TIM3->CCMR2 &= ~TIM_CCMR2_CC3S;
    
        // 3.6 配置定时器的输出比较模式,PWM模式1,OC2M=110
        // 通道2
        TIM3->CCMR1 |= TIM_CCMR1_OC2M_2 | TIM_CCMR1_OC2M_1; // 设置最高位和次高位为1
        TIM3->CCMR1 &= ~TIM_CCMR1_OC2M_0; // 设置最低位为0
    
        // 通道3
        TIM3->CCMR2 |= TIM_CCMR2_OC3M_2 | TIM_CCMR2_OC3M_1; // 设置最高位和次高位为1
        TIM3->CCMR2 &= ~TIM_CCMR2_OC3M_0; // 设置最低位为0
    
        // 3.7 使能定时器的输出比较通道,CC2E=1,低电平有效 CC2P=1
        // 通道2
        TIM3->CCER |= TIM_CCER_CC2E; // 使能通道2的CC2输出
        TIM3->CCER |= TIM_CCER_CC2P; // 低电平有效
    
        // 通道3
        TIM3->CCER |= TIM_CCER_CC3E; // 使能通道3的CC3输出
        TIM3->CCER |= TIM_CCER_CC3P; // 低电平有效
    }
    
    void TIM3_Start(void)
    {       
        TIM3->CR1 |= TIM_CR1_CEN; // 使能定时器
    }
    
    void TIM3_Stop(void)
    {
        TIM3->CR1 &= ~TIM_CR1_CEN; // 关闭定时器
    }
    
    void TIM3_SetCH2DutyCycle(uint8_t duty)
    {
        TIM3->CCR2 = duty;
    }
    
    void TIM3_SetCH3DutyCycle(uint8_t duty)
    {
        TIM3->CCR3 = duty;
    }
  • 主函数(main.c)

    #include "bsp_led.h"
    #include "stm32f10x.h"
    #include <stdio.h>
    #include "bsp_timer3.h"
    #include "delay.h"
    
    int main(){
    	/* LED 端口初始化 */
        LED_GPIO_Config_REG();
    
        TIM3_Config(); // 配置定时器3
    
        // 开启TIM3
        TIM3_Start();
    
        // 定义占空比、变化方向和步长
        uint8_t duty = 0; // 初始占空比为0%
        uint8_t direction = 1; // 变化方向,1表示增加,0表示减少
        uint8_t step = 2; // 占空比变化的步长
        
        while (1){
            // 根据当前方向调整占空比
            if (direction) {
                duty += step; // 增加占空比
                if (duty >= 100) { // 达到或超过100%时,切换方向
                    duty = 100; // 确保占空比不超过100%
                    direction = 0; // 切换为减少方向
                }
            } else {
                duty -= step; // 减少占空比
                if (duty <= 0) { // 达到或低于0%时,切换方向
                    duty = 0; // 确保占空比不低于0%
                    direction = 1; // 切换为增加方向
                }
            }
            TIM3_SetCH2DutyCycle(duty); // 设置通道2占空比
            TIM3_SetCH3DutyCycle(100-duty); // 设置通道3占空比
    
            Delay_ms(20); // 延时20ms,控制占空比变化的速度
        }
    }
    
    • 调节步长step、Delay_ms的毫秒数可以控制呼吸灯的缓慢效果。

呼吸效果

LED3、LED4实现交替呼吸效果