【ARM Cortex-M 开发实战指南(基础篇)】第9章 呼吸灯

开发环境:
MDK:Keil 5.30
STM32CubeMX:V6.4.0
MCU:STM32F103ZET6

9.1呼吸灯的工作原理

呼吸灯,就是指灯光设备的亮度随着时间由暗到亮逐渐增强,再由亮到暗逐渐衰减,很有节奏感地一起一伏,就像是在呼吸一样,因而被广泛应用于手机、电脑等电子设备的指示灯中。

要使用数字器件控制灯光的强弱,我们很自然就想到 PWM(脉冲宽度调制)技术。假如以LED 作为灯光设备,且由控制器输出的 PWM 信号可以直接驱动 LED,PWM 信号中的低电平可点亮 LED 灯。当 LED 以较高的频率进行开关(亮灭)切换时,由于视觉暂留效应,人眼是看不到 LED 灯的闪烁现象的,反映到人眼中能感觉到的是亮度的差别。即以一定的时间长度为周期,LED 灯亮的平均时间越长,亮度就越高,反之越暗。因此,我们可以使用高频率的 PWM 信号,通过调制信号的占空比,控制 LED 灯的亮度。

那么具体我们应该控制 LED 灯以怎样的亮度曲线变化能够达到最好的效果呢?亮度随着时间逐渐变强再衰减,可以用两种常见的数学函数表示,分别是半个周期的正弦函数与指数上升曲线及其对称得到的下降曲线。

XGZPmj.png

相对来说,使用下凹函数曲线灯光处于暗的状态更长,所以指数函数的曲线更符合我们呼吸灯的亮度变化要求。

9.2呼吸灯实现-标准库

9.2.1简单方式

笔者先用最简单的方式来实现,也就是定时改变比较寄存器的值。

1.初始化 GPIO
下面分析具体的定时器配置代码。本实验使用 PB0 作为定时器 PWM 输出通道,先对它进行初始化。作 PWM 输出通道的引脚需要被配置为复用推挽输出模式。

/**
* @brief  配置TIM3复用输出PWM时用到的I/O
* @param  None
* @retval None
*/
static void TIM3_GPIO_Config(void) 
{
  GPIO_InitTypeDef GPIO_InitStructure;

  /* GPIOB clock enable */
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); 

  /* 配置呼吸灯用到的PB0引脚 */
  GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_0 ;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;           // 复用推挽输出
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

  GPIO_Init(GPIOB, &GPIO_InitStructure);
}

2.配置定时器模式

在TIM3_Mode_Config()函数中,完成了呼吸灯所需要的定时器 PWM 输出模式配置。

/**
* @brief  配置TIM3输出的PWM信号的模式,如周期、极性
* @param  None
* @retval None
*/
static void TIM3_Mode_Config(void)
{
  TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
  TIM_OCInitTypeDef  TIM_OCInitStructure;

  /* 设置TIM3CLK 时钟为72MHZ */
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //使能TIM3时钟

  /* 基本定时器配置 */
  TIM_TimeBaseStructure.TIM_Period = 255;       //当定时器从0计数到255,即为256次,为一个定时周期
  TIM_TimeBaseStructure.TIM_Prescaler = 71;   //设置预分频:
  TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1 ;//设置时钟分频系数:不分频(这里用不到)
  TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;   //向上计数模式
  TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);

  /* PWM模式配置 */
  TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;     //配置为PWM模式1
  TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //使能输出
  TIM_OCInitStructure.TIM_Pulse = 0;//设置初始PWM脉冲宽度为0 
  TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;   //当定时器计数值小于CCR1_Val时为低电平

  TIM_OC3Init(TIM3, &TIM_OCInitStructure);//使能通道3
  TIM_OC3PreloadConfig(TIM3, TIM_OCPreload_Enable); //使能预装载 
  TIM_ARRPreloadConfig(TIM3, ENABLE);//使能TIM3重载寄存器ARR

  /* TIM3 enable counter */
  TIM_Cmd(TIM3, ENABLE);      //使能定时器3  
  //TIM_ITConfig(TIM3,TIM_IT_Update, ENABLE);//使能update中断   
  //NVIC_Config_PWM();//配置中断优先级
}

这个定时器的模式配置主要分为两个部分,分别为时基初始化,输出模式初始化。

  • 时基初始化

代码中前10行是定时器的时基初始化,这部分主要负责配置定时器的定时周期、时钟频率、计数方式等。它使用到库函数 TIM_TimeBaseInit,利用结构体TIM_TimeBaseInitTypeDef 进行配置,该结构体有以下成员:

1) TIM_Period
定时周期,实质是存储到重载寄存器 TIMx_ARR 的数值,脉冲计数器从 0 累加到这个值上溢或从这个值自减至 0 下溢。这个数值加 1 然后乘以时钟源周期就是实际定时周期。

本实验中向该成员赋值为 255,即定时周期为(255+1) * T ,T 为定时器的时钟周期。

2) TIM_Prescaler
对定时器时钟 TIMxCLK 的预分频值,分频后作为脉冲计数器 TIMx_CNT 的驱动时钟,得到脉冲计数器的时钟频率为:fCK_CNT=fTIMx_CLK/(N+1),其中 N 为即为赋给本成员的时钟分频值。

3) TIM_ClockDivision

时钟分频因子。怎么又出现一个配置时钟分频的呢?要注意这个TIM_ClockDivision 和上面的 TIM_Prescaler 是不一样的。TIM_Prescaler 预分频配置是对TIMxCLK进行分频,分频后的时钟被输出到脉冲计数器TIMx_CNT ,而TIM_ClockDivision 虽然也是对 TIMxCLK 进行分频,但它分频后的时钟频率为 fDTS,是被输出到定时器的 ETRP 数字滤波器部分,会影响滤波器的采样频率。TIM_ClockDivision 可以被配置为 1 分频(fDTS=fTIMxCLK)、2 分频及 4 分频。ETRP 数字滤波器的作用是对外部时钟 TIMxETR 进行滤波。

本实验中是使用内部时钟 TIMxCLK 作为定时器时钟源的,没有进行滤波所以配置TIM_ClockDivision 为任何数值都没有影响。

4) TIM_CounterMode

本成员配置的为脉冲计数器 TIMx_CNT 的计数模式,分别为向上计数,向下计数,及中央对齐模式。向上计数即 TIMxCNT 从 0 向上累加到 TIM_Period 中的值,(重载寄存器 TIMx_ARR 的值),产生上溢事件;向下计数则 TIMxCNT 从 TIM_Period 的值累减至0,产生下溢事件。而中央对齐模式则为向上、向下计数的合体,TIMxCNT 从 0 累加到TIM_Period 的值减 1 时,产生一个上溢事件,然后向下计数到 1 时,产生一个计数器下溢事件,再从 0 开始重新计数。
本实验中 TIM_CounterMode 成员被赋值为 TIM_CounterMode_Up(向上计数模式)。填充完配置参数后,调用库函数 TIM_TimeBaseInit()把这些控制参数写到寄存器中,定时器的时基配置就完成了。

  • 输出模式配置

在本函数代码的后面是关于定时器的输出模式配置的。通用定时器的输出模式由 TIM_OCInitTypeDef 类型结构体的以下几个成员来设置:

1) TIM_OCMode

输出模式配置,主要使用的为 PWM1 和 PWM2 模式。PWM1 模式是:在向上计数时,当 TIMx_CNT < TIMx_CCRn (比较寄存器,其数值等于 TIM_Pulse 成员的内容) 时,通道 n 输出为有效电平,否则为无效电平;在向下计数时,当 TIMx_CNT>TIMx_CCRn 时通道 n 为无效电平,否则为有效电平。PWM2 模式跟 PWM1模式相反。

其中的有效电平和无效电平并不是固定地对应高电平和低电平,也是需要配置的,由下面介绍的 TIM_OCPolarity 成员配置。本实验中使用 PWM1 输出模式。

2) TIM_OutputState
配置输出模式的状态使能或关闭输出。

本实验中向该成员赋值为 TIM_OutputState_Enable(使能输出)。

3) TIM_OCPolarity
有效电平的极性,把 PWM 模式中的有效电平设置为高电平或低电平。
本实验中向该成员赋值为 TIM_OCPolarity_Low(有效电平为低电平),因为在上面把输出模式配置为 PWM1 模式,向上计数,所以在 TIMx_CNT < TIMx_CCRn 时,通道 n 输出为低电平,否则为高电平。

4) TIM_Pulse
直译为跳动,本成员的参数值即为比较寄存器 TIMx_CCR 的数值,当脉冲计数器TIMx_CNT 与 TIMx_CCR 的比较结果发生变化时,输出脉冲将发生跳变。

本实验中就是通过不断改变比较寄存器 TIMx_CCR 的值,赋予它指数曲线数据,达到控制 PWM 信号的占空比呈指数曲线变化的目的。在本函数代码中,我们对该成员赋予初始为 0,而改变比较寄存器 TIMx_CCR 值的操作是在中断服务函数中修改的。填充完输出模式初始化结构体后,调用输出模式初始化函数 TIM_OCxInit()对通道进行初始化。

以上是最基本的PWM输出调制实现呼吸灯,由 TIM3_CH2 输出 PWM 来控制LED的亮度。下面我们介绍通过库函数来配置该功能的步骤。

1)开启 TIM3 时钟以及复用功能时钟,配置 PB0为复用输出。

要使用 TIM3,我们必须先开启 TIM3 的时钟,这点相信大家看了这么多代码,应该明白了。这里我们还要配置 PB0为复用输出,此时,PB0属于复用功能输出。在此只列出库函数设置 AFIO 时钟的方法。

RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); //复用时钟使能

其余的和前面的配置一样,就不再列出了。

2)初始化 TIM3,设置 TIM3 的 ARR 和 PSC。

3)设置 TIM3_CH2 的 PWM 模式,使能 TIM3 的 CH2 输出。
在库函数中, PWM 通道设置是通过函数 TIM_OC1Init()~TIM_OC4Init()来设置的, 不同的通道的设置函数不一样, 这里我们使用的是通道 2,所以使用的函数是 TIM_OC2Init()。

void TIM_OC2Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);

4)使能 TIM3。

在完成以上设置了之后,我们需要使能 TIM3。 使能 TIM3 的方法前面已经讲解过:

TIM_Cmd(TIM3, ENABLE); //使能 TIM3

5)修改 TIM3_CCR2 来控制占空比。

最后,在经过以上设置之后, PWM 其实已经开始输出了,只是其占空比和频率都是固定的,而我们通过修改 TIM3_CCR2 则可以控制 CH2 的输出占空比。继而控制LED的亮度。在库函数中,修改 TIM3_CCR2 占空比的函数是:

void TIM_SetCompare2(TIM_TypeDef* TIMx, uint16_t Compare2);

当然也可直接操作寄存器TIM3->CCR3。

理所当然,对于其他通道,分别有一个函数名字, 函数格式为 TIM_SetComparex(x=1,2,3,4)。通过以上5个步骤,我们就可以控制 TIM3 的 CH2 输出 PWM 波了。

接下来看看主函数的代码:

/* Includes*********************************************************************/
#include "./PWM/stm32f103_pwm.h"
#include "./SysTick/stm32f103_SysTick.h"

/**
  * @brief  main
  * @param  None
  * @retval None
  */
int main(void)
{
    uint16_t i = 0;
    FlagStatus breathe_flag = SET;

    // systick init 
    SysTick_Init();

    /* 初始化呼吸灯 */
    TIM3_Breathing_Init();

    while(1)
    {
        /* delay a time in milliseconds */
        Delay_ms(5);
        if(SET == breathe_flag) 
        {
            i++;
        }
        else
        {
            i--;
        }
        if(255 < i)
        {
            breathe_flag = RESET;
        }
        if(0 >= i)
        {
            breathe_flag = SET;
        }
        /* configure TIMER channel output pulse value */
        //TIM_SetCompare2(TIM3, i);
       TIM3->CCR3 = (uint32_t)i;
    }

}

代码很简单,就是不断改变CCR3的值从而控制 CH2 的输出占空比。

9.2.2中断方式

1.生成指数曲线 PWM 数据
要实现 LED 亮度随着指数曲线变化,我们需要使用占空比呈指数曲线变化的 PWM 信号,而这样的信号由定时器经过查表产生。这个表的数据存储在程序中的数组 indexWave中。

uint8_t indexWave[] = {1,1,2,2,3,4,6,8,10,14,19,25,33,44,59,80,
107,143,191,255,255,191,143,107,80,59,44,33,25,19,14,10,8,6,4,3,2,2,1,1};

这个表有 40 个数字,从图中可以看到这些数字呈指数上升再衰减,正好是呼吸灯的一个控制周期。数字的大小范围是 0~255,即把 LED 的亮度分为了 0~255 个等级。

假如我们把定时器的脉冲计数器 TIMx_CNT 上限设置为 255,把这个表的数据一个一个地赋值到定时器的比较寄存器 TIMx_CCR 中,那么在每个 PWM 周期中,当 TIMx_CNT的计数值小于比较寄存器 TIMx_CCR 的值时, 就会在通道中输出低电平,点亮 LED,而随着 TIMx_CCR 的值由 LED 亮度表得来,所以 LED 点亮的时间就会呈图中的曲线变化,实现呼吸灯的功能。

这个表的数据是使用 matlab 软件生成的。该代码运行后会生成一个“index_wave.c”的文件,用户把该文件中的数据复制到工程中的数组中即可。

%本代码用于产生呼吸灯使用的指数函数数据
clear;

x = [0 : 8/19 : 8];       %设置序列 ,指数上升
up = 2.^x ;               %求上升指数序列  
up = uint8(up);           %化为8位数据

y = [8: -8/19 :0];       %设置序列 ,指数下降
down = 2.^y ;            %求下降指数序列
down = uint8(down);      %化为8位数据

line = [[0:8/19:8],[8:8/19:16]]         %拼接序列
val = [up , down]                       %拼接输出序列

dlmwrite('index_wave.c',val);       %输出到文件index_wave.c
plot(line,val,'.');                 %显示波形图

2.初始化 GPIO

前面已经讲过了,这里就不赘述了。

3.配置定时器模式
在 TIM3_Mode_Config 函数中,完成了呼吸灯所需要的定时器 PWM 输出模式配置。

static void TIM3_Mode_Config(void)
{
    TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
    TIM_OCInitTypeDef  TIM_OCInitStructure;

   /* 设置TIM3CLK 时钟为72MHZ */
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);  //使能TIM3时钟

  /* 基本定时器配置 */ 
  TIM_TimeBaseStructure.TIM_Period = 255; //当定时器从0计数到255,即为256次,为一个定时周期
  TIM_TimeBaseStructure.TIM_Prescaler = 1999;       //设置预分频:
//设置时钟分频系数:不分频(这里用不到)
  TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1 ;
  TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;   //向上计数模式
  TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);

  /* PWM模式配置 */
  TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;  //配置为PWM模式1
  TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //使能输出
  TIM_OCInitStructure.TIM_Pulse = 0;

  //设置初始PWM脉冲宽度为0   
  //当定时器计数值小于CCR1_Val时为低电平
  TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;        

  TIM_OC3Init(TIM3, &TIM_OCInitStructure);          //使能通道3
  TIM_OC3PreloadConfig(TIM3, TIM_OCPreload_Enable);     //使能预装载 
  TIM_ARRPreloadConfig(TIM3, ENABLE);//使能TIM3重载寄存器ARR

  /* TIM3 enable counter */
  TIM_Cmd(TIM3, ENABLE);              //使能定时器3  
  TIM_ITConfig(TIM3,TIM_IT_Update, ENABLE);     //使能update中断    
  NVIC_Config_PWM();    //配置中断优先级       
}

这个定时器的模式配置主要分为三个部分,分别为时基初始化,输出模式初始化和中断配置。
时基初始化,输出模式初始化和不同方式差不多,就不再赘述了。下面谈谈中断部分。

  • 定时器中断及其它配置

本函数剩下的代码用 TIM_OCxPreloadConfig() 配置了各通道的比较寄存器 TIM_CCR预装载使能;使用 TIM_ARRPreloadConfig()把重载寄存器 TIMx_ARR 使能,调用了TIM_ITConfig()配置定时器更新中断,每个定时周期结束后触发一次。该中断的优先级由函数 NVIC_Config_PWM()配置。

static void NVIC_Config_PWM(void)
{
  NVIC_InitTypeDef NVIC_InitStructure;

  /* Configure one bit for preemption priority */
  NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);

  /* 配置TIM3_IRQ中断为中断源 */
  NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);
}

配置好中断,下面就要编写中断服务函数。

void TIM3_IRQHandler(void)
{
    static uint8_t pwm_index = 0;           //用于PWM查表
    static uint8_t period_cnt = 0;      //用于计算周期数

    if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)  //TIM_IT_Update
    {
        period_cnt++;
        if(period_cnt >= 10) //若输出的周期数大于10,输出下一种脉冲宽的PWM波
        {
            //根据PWM表修改定时器的比较寄存器值
            TIM3->CCR3 = indexWave[pwm_index];   
            pwm_index++;        //标志PWM表的下一个元素
            //若PWM脉冲表已经输出完成一遍,重置PWM查表标志
            if( pwm_index >=  40)    
            {
                pwm_index=0;
            }
            period_cnt=0;       //重置周期计数标志
        }
        TIM_ClearITPendingBit (TIM3, TIM_IT_Update);    //必须要清除中断标志位
    }
}

本中断服务函数在每次定时器更新事件发生时执行一次(即 256 个定时器时钟周期)。函数中使用了静态变量 pwm_index 和 period_cnt,它们分别用来查找 PWM 表元素和记录同样占空比的脉冲输出了多少次。

本代码的目的是每 10 次定时器中断更新一次 PWM 表中的数据到比较寄存器TIMx_CCR 中,当遍历完 PWM 表的 40 个元素时,再重头开始遍历 PWM 表,周而复始,重复 LED 的呼吸过程。
整个呼吸过程的时间计算方法如下:

因为定时器的 TIM_Prescaler 设置为 1999;

所以定时器的时钟频率:fTIM = 72000000 /(TIM_Prescaler+1) = 36000 Hz

即定时器的时钟周期为:tTIM = 1/fTIM = 1/36000 s

因为定时器的 TIM_Period 设置为 255;

所以定时器的中断周期为:tint= tTIM * (TIM_Period+1) =0.0071 s

因为 PWM 表有 pwm_index = 40 个亮度占空比数据,同种占空比信号输出 period_cnt =10 次

所以一个呼吸周期 T = tint 40 10 = 2.84s

以上是最基本的PWM输出调制实现呼吸灯,由 TIM3_CH2 输出 PWM 来控制LED的亮度。下面我们介绍通过库函数来配置该功能的步骤。

1)开启 TIM3 时钟以及复用功能时钟,配置 PB0为复用输出。
要使用 TIM3,我们必须先开启 TIM3 的时钟,这点相信大家看了这么多代码,应该明白了。这里我们还要配置 PB0为复用输出,此时,PB0属于复用功能输出。在此只列出库函数设置 AFIO 时钟的方法。

RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); //复用时钟使能

其余的和前面的配置一样,就不再列出了。

2)初始化 TIM3,设置 TIM3 的 ARR 和 PSC。

3)设置 TIM3_CH2 的 PWM 模式,使能 TIM3 的 CH2 输出。
在库函数中, PWM 通道设置是通过函数 TIM_OC1Init()~TIM_OC4Init()来设置的, 不同的通道的设置函数不一样, 这里我们使用的是通道 2,所以使用的函数是 TIM_OC2Init()。

void TIM_OC2Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);

4)使能 TIM3。

在完成以上设置了之后,我们需要使能 TIM3。 使能 TIM3 的方法前面已经讲解过:

TIM_Cmd(TIM3, ENABLE); //使能 TIM3

5)修改 TIM3_CCR2 来控制占空比。

最后,在经过以上设置之后, PWM 其实已经开始输出了,只是其占空比和频率都是固定的,而我们通过修改 TIM3_CCR2 则可以控制 CH2 的输出占空比。继而控制LED的亮度。在库函数中,修改 TIM3_CCR2 占空比的函数是:

void TIM_SetCompare2(TIM_TypeDef* TIMx, uint16_t Compare2);

当然也可直接操作寄存器TIM3->CCR3。

理所当然,对于其他通道,分别有一个函数名字, 函数格式为 TIM_SetComparex(x=1,2,3,4)。通过以上5个步骤,我们就可以控制 TIM3 的 CH2 输出 PWM 波了。

9.3呼吸灯实现-HAL库

9.3.1 STM32Cube生成工程

和上一节内容差不多,稍微有些不同罢了。
1.设置RCC
设置高速外部时钟HSE,选择外部时钟源。

XGZEt0.md.png

2.时钟配置
笔者的板子使用的外部晶振为8MHz,选择外部时钟HSE 8MHz ,PLL锁相环9倍频后为72MHz,系统时钟来源选择为PLL,设置APB1分频器为 /2,这时候定时器的时钟频率为72Mhz。本文笔者使用的定时器是TIM3,TIM3挂在APB1上,不同的定时器挂在不同总线上的。

XGZVhV.md.png

3.Times配置
选择TIM,使能TIM3,指定时钟源。

XGZm1U.md.png

本节要使用TIM3的CH3通道,因此需要将其使能。这里选择PWM输出。当对应的通道打开后,对应的GPIO也会被使能。笔者的板子的LED接到了PB0上,这里要根据自己的板子来配置TIM和CH。

XGZMnJ.md.png

PWM参数配置如下:

  • Counter setting

Prtscaler (定时器分频系数) : 1999
Counter Mode(计数模式) :Up(向上计数模式)
Counter Period(自动重装载值) : 255
CKD(时钟分频因子) : No Division 不分频
选项: 可以选择二分频和四分频
auto-reload-preload(自动重装载) : Enable 使能

  • TRGO Output (TRGO) Parameters

Master/Slave Mode(MSM bit):Disable
TRGO:定时器的触发信号输出 在定时器的定时时间到达的时候输出一个信号(如:定时器更新产生TRGO信号来触发ADC的同步转换,)

  • PWM Generation Channel (四个CH)

Mode(定时模式):PWM mode 1
Pulse(计数比较值):0
CH Polarity(输出极性):High

XGZdnH.md.png

根据前面的参数配置,我们可以算出PWM的输出周期:

PWM=1/(Tclk/(psc+1)) * (arr+1)

这里我们 arr=255 psc=1999 Tclk=72Mhz ,

PWM=1/(72Mhz/(1+1999)) *(255+1)

这个要开启中断。

XGZwBd.md.png

另外将设置GPIO的速度为高速。

XGZ0HA.md.png

好了,到这里,就配置完成了。

9.3.2呼吸灯的具体代码分析

我们还是先看看主函数。

int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_TIM3_Init();
  /* USER CODE BEGIN 2 */
  HAL_TIM_Base_Start_IT(&htim3);
  HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_3);
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

主函数和上一节的差不多,还好了几句,这个只需初始化TIM3的CH3。那么是哪里不同呢,在回答这个问题之前,先看看呼吸灯的编程流程。

1) 硬件初始化,系统时钟初始化;
2) GPIO初始化,TIM3以及PWM初始化;
3) 启动定时器和PWM相应通道。
4) 调用中断回调函数,不断改变TIMx_CCR寄存器的值。

我们就根据这个思路来看看呼吸灯的具体代码。
1.生成指数曲线 PWM 数据

要实现 LED 亮度随着指数曲线变化,我们需要使用占空比呈指数曲线变化的 PWM 信号,而这样的信号由定时器经过查表产生。这个表的数据存储在程序中的数组 indexWave中。在标准库中已经介绍过了。

2.初始化GPIO和定时器

硬件初始化,系统时钟初始化就不说了,我们看看GPIO和定时器初始化。

/**
  * @brief TIM3 Initialization Function
  * @param None
  * @retval None
  */
static void MX_TIM3_Init(void)
{

  /* USER CODE BEGIN TIM3_Init 0 */

  /* USER CODE END TIM3_Init 0 */

  TIM_ClockConfigTypeDef sClockSourceConfig = {0};
  TIM_MasterConfigTypeDef sMasterConfig = {0};
  TIM_OC_InitTypeDef sConfigOC = {0};

  /* USER CODE BEGIN TIM3_Init 1 */

  /* USER CODE END TIM3_Init 1 */
  htim3.Instance = TIM3;
  htim3.Init.Prescaler = 1999;
  htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim3.Init.Period = 255;
  htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  if (HAL_TIM_Base_Init(&htim3) != HAL_OK)
  {
    Error_Handler();
  }
  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  if (HAL_TIM_ConfigClockSource(&htim3, &sClockSourceConfig) != HAL_OK)
  {
    Error_Handler();
  }
  if (HAL_TIM_PWM_Init(&htim3) != HAL_OK)
  {
    Error_Handler();
  }
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  if (HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig) != HAL_OK)
  {
    Error_Handler();
  }
  sConfigOC.OCMode = TIM_OCMODE_PWM1;
  sConfigOC.Pulse = 0;
  sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
  sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
  if (HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_3) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN TIM3_Init 2 */

  /* USER CODE END TIM3_Init 2 */
  HAL_TIM_MspPostInit(&htim3);

}

/**
  * @brief GPIO Initialization Function
  * @param None
  * @retval None
  */
static void MX_GPIO_Init(void)
{
  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOB_CLK_ENABLE();

}

GPIO初始化说的,就配置了一个时钟。这里主要讲讲TIM3初始化,MX_TIM3_Init()函数有两部分内容,一部分TIM的Counter配置,这部分内容和上一章是一样的,我们重点关注TIM_OC_InitTypeDef结构体,这个结构体就是输出比较配置的结构体,这个就是PWM的具体输出的配置结构体。

typedef struct
{
  uint32_t OCMode;        /*!< Specifies the TIM mode.
                               This parameter can be a value of @ref TIM_Output_Compare_and_PWM_modes */

  uint32_t Pulse;         /*!< Specifies the pulse value to be loaded into the Capture Compare Register.
                               This parameter can be a number between Min_Data = 0x0000 and Max_Data = 0xFFFF */

  uint32_t OCPolarity;    /*!< Specifies the output polarity.
                               This parameter can be a value of @ref TIM_Output_Compare_Polarity */

  uint32_t OCNPolarity;   /*!< Specifies the complementary output polarity.
                               This parameter can be a value of @ref TIM_Output_Compare_N_Polarity
                               @note This parameter is valid only for timer instances supporting break feature. */

  uint32_t OCFastMode;    /*!< Specifies the Fast mode state.
                               This parameter can be a value of @ref TIM_Output_Fast_State
                               @note This parameter is valid only in PWM1 and PWM2 mode. */

  uint32_t OCIdleState;   /*!< Specifies the TIM Output Compare pin state during Idle state.
                               This parameter can be a value of @ref TIM_Output_Compare_Idle_State
                               @note This parameter is valid only for timer instances supporting break feature. */

  uint32_t OCNIdleState;  /*!< Specifies the TIM Output Compare pin state during Idle state.
                               This parameter can be a value of @ref TIM_Output_Compare_N_Idle_State
                               @note This parameter is valid only for timer instances supporting break feature. */
} TIM_OC_InitTypeDef;
  • OCMode:输出比较模式的选择,对应的是TIMx_CCMR1寄存器的OC1M位。
  • Pulse:设置电平跳变值,最小值为0x0000 ,最大值为0Xffff。
  • OCPolarity:设置输出比较的极性。
  • OCNPolarity:设置互补输出比较极性。
  • OCFastMode:输出比较快速使能和失能。
  • OCIdleState:选择空闲状态下的非工作状态。
  • OCNIdleState:设置非空闲状态下的非工作状态。

根据上述讲解,也就能明白MX_TIM3_Init()的含义了。接下来重点来了,以上是STM32cudeMX自动生成的代码,下面是我们自己实现的代码,也就是呼吸灯的核心代码。

3.PWM输出中断回调函数
关于如何配置中断,这部分昂看上一章,下面讲解如何编写PWM中断服务函数。

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  static uint8_t pwm_index = 1;         /* 用于PWM查表 */
    static uint8_t period_cnt = 0;      /* 用于计算周期数 */

  period_cnt++;
  /* 若输出的周期数大于10,输出下一种脉冲宽的PWM波 */
  if(period_cnt >= 10)                                       
  {
    /* 根据PWM表修改定时器的比较寄存器值 */
    __HAL_TIM_SET_COMPARE(htim,TIM_CHANNEL_3,indexWave[pwm_index]);

    /* 标志PWM表的下一个元素 */
    pwm_index++;                                                
    /* 若PWM脉冲表已经输出完成一遍,重置PWM查表标志 */
    if( pwm_index >=  40)                                
    {
      pwm_index=0;                              
    }
    /* 重置周期计数标志 */
    period_cnt=0;                                               
  }
}

本中断服务函数在每次定时器更新事件发生时执行一次(即 256 个定时器时钟周期)。函数中使用了静态变量 pwm_index 和 period_cnt,它们分别用来查找 PWM 表元素和记录同样占空比的脉冲输出了多少次。

本代码的目的是每 10 次定时器中断更新一次 PWM 表中的数据到比较寄存器TIMx_CCR 中,当遍历完 PWM 表的 40 个元素时,再重头开始遍历 PWM 表,周而复始,重复 LED 的呼吸过程。

9.4呼吸灯的实验现象

将程序编译好下载到板子中,可一看到LED像呼吸一样渐渐变明或者渐渐变暗。


欢迎访问我的网站

BruceOu的哔哩哔哩
BruceOu的主页
BruceOu的博客
BruceOu的CSDN博客
BruceOu的简书
BruceOu的知乎


资源获取方式

1.关注公众号[嵌入式实验楼]
2.在公众号回复关键词[Cortex-M]获取资料提取码

Related posts

Leave a Comment