×
单片机 > 单片机程序设计 > 详情

STM32之定时器中断控制LED闪烁

发布时间:2020-09-02 发布时间:
|

上篇博客我们是用延时函数实现了LED的闪烁,今天我们使用STM32的定时器来使LED闪烁。 


关于32的定时器的种类,今天我在这先不做过多的说明,有时间我会再另写一篇博客来专门介绍32的定时器。今天我们使用32的定时器3来产生中断,以实现LED的闪烁。 


今天我们需要配置的有LED和定时器,首先来配置LED,我们还是使用正点原子精英版开发板上的DS0来进行实验 


配置LED的过程还是和上篇博客中点亮LED的方法一样,我就不再过多的说明,只贴下代码 


led.c文件如下


#include"led.h"


void led_init(void)

{

    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);

    GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;

    GPIO_InitStructure.GPIO_Pin=GPIO_Pin_5;

    GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;

    GPIO_Init(GPIOB,&GPIO_InitStructure);

}


led.h文件如下


#ifndef __LED_H

#define __LED_H

#include "sys.h"


#define LED0 PBout(5)


void led_init(void);


#endif


配置完LED之后,下面就要开始配置今天的主角—定时器,首先我们还是像之前那样,先在工程文件所在的文件夹里面的HARDWARE文件夹里面重新建立一个新文件夹,并命名为Timer,,如下图 



然后我们进入到Keil MDK中,建立两个空白的文件,并分别命名为time.c和time.h,然后都保存在我们刚才在HARDWARE文件夹里面所建立的Timer文件夹里面,如下图 



然后我们再在Keil MDK中点击HARDWARE文件,然后把刚才保存的time.c和time.h文件都给添加到此工程中去。(这些步骤以后我就不再一一说明了),添加完成如下图 

 

 


添加完之后,我们不要忘了要再把这个文件的路径也添加到工程中去,具体做法我在上一篇博客中有介绍。 


上面这些都做好之后,我们就可以开始写配置定时器的程序了,首先我们来写time.c文件。 


我们这次用到了定时器3,所以我们先来写一个定时器3的初始化函数,如下


void time3_init(u16 per,u16 pre)

{

    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);//使能TIM3的时钟

    TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1;//TIM_CKD_DIV1是.h文件中已经定义好的,TIM_CKD_DIV1=0,也就是时钟分频因子为0

    TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up;//计数方式为向上计数

    TIM_TimeBaseStructure.TIM_Period=per;//周期

    TIM_TimeBaseStructure.TIM_Prescaler=pre;//分频系数

    TIM_TimeBaseInit(TIM3,&TIM_TimeBaseStructure);


    TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE);//使能TIM3的更新中断

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

}


初始化函数还是上篇博客中我们配置GPIO口时的老套路,首先就是先定义一个结构体(注意定义结构体必须在函数的开头)


TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;

1

因为我们用到了定时器3,所以我们就要使能定时器3的时钟


RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);//使能TIM3的时钟

1

然后就是对刚才定义过的结构体进行成员赋值


TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1;//TIM_CKD_DIV1是.h文件中已经定义好的,TIM_CKD_DIV1=0,也就是时钟分频因子为0

TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up;//计数方式为向上计数

TIM_TimeBaseStructure.TIM_Period=per;//周期

TIM_TimeBaseStructure.TIM_Prescaler=pre;//分频系数


这里面每一个成员变量所赋的值都是在32的.c文件中已经定义好的,我们只需要去找到相应的.c和.h文件进行复制到这里就行。 


这个结构体的第一个变量是时钟分频因子,这个变量其实我现在也不是很理解它的含义,暂时先不做过多介绍,我们在这里只是给他复制为TIM_CKD_DIV1,也就是让他等于0,具体变量内容如下


#define TIM_CKD_DIV1                       ((uint16_t)0x0000)

#define TIM_CKD_DIV2                       ((uint16_t)0x0100)

#define TIM_CKD_DIV4                       ((uint16_t)0x0200)

#define IS_TIM_CKD_DIV(DIV) (((DIV) == TIM_CKD_DIV1) || \

                             ((DIV) == TIM_CKD_DIV2) || \

                             ((DIV) == TIM_CKD_DIV4))


然后这个结构体的第二个成员是选择计数方式,因为我们知道,定时器的本质也就是计数,所以这里我们选择它的计数方式,一般我们选择向上计数(也就是计数器从0开始计数), 即选择TIM_CounterMode_Up,具体变量内容如下


#define TIM_CounterMode_Up                 ((uint16_t)0x0000)

#define TIM_CounterMode_Down               ((uint16_t)0x0010)

#define TIM_CounterMode_CenterAligned1     ((uint16_t)0x0020)

#define TIM_CounterMode_CenterAligned2     ((uint16_t)0x0040)

#define TIM_CounterMode_CenterAligned3     ((uint16_t)0x0060)

#define IS_TIM_COUNTER_MODE(MODE) (((MODE) == TIM_CounterMode_Up) ||  \

                                   ((MODE) == TIM_CounterMode_Down) || \

                                   ((MODE) == TIM_CounterMode_CenterAligned1) || \

                                   ((MODE) == TIM_CounterMode_CenterAligned2) || \

                                   ((MODE) == TIM_CounterMode_CenterAligned3))


然后结构体的第三个成员是计数周期,也就是说这个成员变量的内容就代表着我们让计数器计数到哪一个数要重现返回0,重新开始计数,这个参数也是我们这个初始化函数的第一个入口参数,待会我们会在主函数中进行调用,并传入参数。(假如说我们让计数周期等于100,那么计数器计数过程就是,0,1,2,3,…,98,99,100,0,1,2,3…98,99,100,0,1,2,3,…) 


然后结构体的第四个成员是分频系数,如果我们让分频系数等于1,那么就是选择不分频,不分频的意思也就是说,定时器计数的频率就等于定时器的输入频率,这里我们使用的是定时器3,它现在的输入时钟频率是72MHz(具体为啥是72MHz,有时间我会再讲),那么他就是以72M的频率进行计数,假如说我们让分频系数等于2,那么他就是以72M / 2的频率进行计数,这就是分频系数的概念。这个定时器的初始化函数,我们通过传入不同的参数,也就是设置不同的计数周期和频率,我们就可以设置定时器多长时间产生一次中断。 


我们把各个成员都赋值之后,然后就是把这些成员利用下面这个函数给导入到相应的寄存器中,函数如下


TIM_TimeBaseInit(TIM3,&TIM_TimeBaseStructure);


通过以上的步骤,我们已经把定时器3的工作方式都给配置好了,接下来我们要做的就是再使能一些玩意。因为我们今天用到了中断,所以我们要使能定时器3的中断,函数如下


TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE);//使能TIM3的更新中断


其中这个函数的第一个入口参数就是我们要使能的是哪一个定时器,然后第二个入口参数就是我们要使能的是哪一种中断,这里我们选择使能更新(溢出)中断,也就是每当计数器溢出的时候,会产生一次中断,第三个入口参数就是选择使能或者不使能,我们选择使能ENABLE。


然后我们用到了定时器3,所以说我们要使能定时器3(要区分开使能定时器3和使能定时器3的时钟),函数如下


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


定时器初始化完成之后,因为我们用到了定时器中断,所以说我们要写定时器中断服务函数,本程序中断服务函数如下


void TIM3_IRQHandler(void) //TIME3中断服务函数  需要设定中断优先级  即NVIC配置

{

    if(TIM_GetITStatus(TIM3,TIM_IT_Update)!=RESET)//判断是否发生了更新(溢出)中断

    {

        TIM_ClearITPendingBit(TIM3,TIM_IT_Update);//清除溢出中断标志位

    }


    LED0=!LED0;

}


注意在32里面,各个中断服务函数的名字不能乱写,只能是固定的名字,写错一个字母都不行,其中定时器3的中断服务函数名字为TIM3_IRQHandler(void),大家一定不要写错。 

然后进入中断服务函数里面,首先我们进行中断的判断,我们要判断的是刚才发生的中断是不是我们想要的中断。在32里面,判断中断有固定的函数,我们通过这个函数来获得相应的中断标志位的值,以此来判断定时器是否真正发生了相应的中断,函数如下


TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t TIM_IT)


这个函数共有两个入口参数,其中第一个参数就是填入你要判断的是哪一个定时器的中断,然后第二个参数是填入你要判断的是哪一种类型的中断,(因为一个定时器中断类型有好几个,所以说我们必须指明判断的类型),这里我们使用的是定时器3的更新(溢出)中断,所以说此函数应写成如下形式


if(TIM_GetITStatus(TIM3,TIM_IT_Update)!=RESET)//判断是否发生了更新(溢出)中断


这个函数的意思其实就是先得到中断标志位的值,然后判断中断标志位的值是0(RESET)还是1(SET),当发生中断的时候,对应的中断标志位会被置为1,也就是SET,如果没有发生中断,那么标志位的值就是0,也就是RESET,所我们在这里判断:如果标志位不是RESET,那么就是发生了中断。如果我们发现标志位被置1了,那么就进入if里面,然后我们第一步要做的就是清零中断标志位,以防程序会多次进入中断服务函数,清零中断标志位函数也是由专门的函数,如下


TIM_ClearITPendingBit(TIM3,TIM_IT_Update);//清除溢出中断标志位


这个函数也是有两个入口参数,他和刚才的获取中断标志位值的函数是一样的入口参数。 

这些都写好之后,我们就可以开始写具体的中断服务函数了,这里我们让LED0的状态进行翻转,程序如下


LED0=!LED0;


这些都配置完之后,还没有完成任务,对于32来说,我们还需要再配置一下中断优先级,程序如下


void NVIC_INIT(void)

{

    NVIC_InitTypeDef NVIC_InitStructure;

    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);//设定中断优先级分组0

    NVIC_InitStructure.NVIC_IRQChannel=TIM3_IRQn;//设定中断向量   本程序为TIM3的中断

    NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;//使能响应中断向量的中断响应

    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0;//配置中断向量的抢占优先级

    NVIC_InitStructure.NVIC_IRQChannelSubPriority=3;//配置中断向量的响应优先级

    NVIC_Init(&NVIC_InitStructure);//根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器

}


配置中断优先级也是一样的套路,定义结构体,然后对结构体的变量进行复制,然后就是把这些成员变量给导入到相应的寄存器中,。 


其中这个结构体的第一个参数是设置中断向量,我们这次使用的是定时器3的中断,所以我们要设置成TIM3_IRQn。 


结构体的第二个参数是使能控制,我们要使能中断响应,也就是说,我们要开启定时器的中断响应(当发生中断的时候,允许程序去响应这个中断)所以要把它设置为ENABLE。 


结构体的第三个成员变量是设置抢占优先级,第四个成员变量是设置响应优先级,关于这两个参数,我在这里不做过多的解释,等有时间我再深入了解一下这两个变量的意义。其实他就是来配置我们当前使用的中断的优先级。 


然后还有一句程序就是


NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);//设定中断优先级分组0

1


这条语句是用来设定中断优先级分组,在不同的分组里面会有不同的抢占优先级和响应优先级可供选择,这里我也不多讲。 


到此为止我们的time.c文件就写完了,time.c文件代码如下


#include "time.h"

#include "led.h"

void time3_init(u16 per,u16 pre)

{

    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);//使能TIM3的时钟

    TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1;//TIM_CKD_DIV1是.h文件中已经定义好的,TIM_CKD_DIV1=0,也就是时钟分频因子为0

    TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up;//计数方式为向上计数

    TIM_TimeBaseStructure.TIM_Period=per;//周期

    TIM_TimeBaseStructure.TIM_Prescaler=pre;//分频系数

    TIM_TimeBaseInit(TIM3,&TIM_TimeBaseStructure);


    TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE);//使能TIM3的更新中断

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

}

void NVIC_INIT(void)

{

    NVIC_InitTypeDef NVIC_InitStructure;

    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);//设定中断优先级分组0

    NVIC_InitStructure.NVIC_IRQChannel=TIM3_IRQn;//设定中断向量   本程序为TIM3的中断

    NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;//使能响应中断向量的中断响应

    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0;//配置中断向量的抢占优先级

    NVIC_InitStructure.NVIC_IRQChannelSubPriority=3;//配置中断向量的响应优先级

    NVIC_Init(&NVIC_InitStructure);//根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器

}


void TIM3_IRQHandler(void) //TIME3中断服务函数  需要设定中断优先级  即NVIC配置

{

    if(TIM_GetITStatus(TIM3,TIM_IT_Update)!=RESET)//判断是否发生了更新(溢出)中断

    {

        TIM_ClearITPendingBit(TIM3,TIM_IT_Update);//清除溢出中断标志位

    }


    LED0=!LED0;

}


接下来我们开始写time.h文件,time.h文件主要就是定义一个头文件”time.h”,然后就是声明一下我们在time.c文件中所定义的所有函数(中断服务函数可以不用再time.h文件声明),time.h文件程序如下


#ifndef __TIME_H_

#define __TIME_H_

#include "sys.h"


void time3_init(u16 per,u16 pre);

void NVIC_INIT(void);


#endif


接下来我们开始写main.c文件,程序如下


#include "sys.h"

#include "led.h"

#include "time.h"

int main(void)

{

    led_init();

    time3_init(7199,9999);//1s产生一次中断,用于控制LED进行闪烁

    NVIC_INIT();

    while(1)

    {

    }

}


main.c文件中程序就稍微简单了一些,它主要就是调用一些我们写好的函数,我们主要就是来看一下time3_init(7199,9999)中的两个参数。回想一下我们上面提到的这个定时器初始化函数的入口参数的意义,第一个入口参数是设定计数周期,我们这里设定为7199,那么程序的计数过程就是:0,1,2,3,…,7199,0,1,2,3,…,7199,…也就是说程序从0开始计数,然后一直计到7199,一个周期总共计了7200个数,计到7199之后,再返回到0重新开始下一轮计数,当它每次计到7199时,定时器就会产生一次中断;然后我们来看这个初始化函数的第二个参数,这个参数控制的是计数的频率,也就是控制计数器多长时间计一个数,这里我们选择参数为9999,也就是定时器计数的频率为72MHz / (9999+1)=7200Hz。(至于为什么要在原来的基础上加1,也就是为什么是9999+1,可能在32里面他都是从0开始算的吧)总体来说,现在计数器是以7200Hz的频率来计数,每计到7199就中断一次,也就是说现在中断是1s产生一次,(1s是这样算出来的:计数频率是7200Hz,也就是说每计一个数需要花费1/7200s的时间,而总共一个周期需要计7199+1个数,那么也就是需要花费t=(1/7200)*(7199+1)=1s)相应的LED也就是1s闪烁一次。如果我们想改变LED闪烁的频率,我们只需要更改 


time3_init(7199,9999); 


这个初始化函数的两个入口参数即可。 


至此,我们本次使用定时器中断来控制LED进行闪烁的实验到此就结束了,把写好的程序烧录到开发板中就会发现LED灯在进行闪烁,闪烁频率是受定时器初始化函数的两个入口参数进行控制。


其实我发现用定时器中断的时候,我们可以不用写中断服务函数,我们可以这样直接在主函数中(或者其他的函数)进行中断标志位的判断,一旦判断出相应的中断标志位被置1的话,我们可以直接在下面写中断服务函数,并且这样我们也就不用再去写那个中断优先级配置的函数了。主函数代码如下


#include "sys.h"

#include "led.h"

#include "time.h"


int main(void)

{

    led_init();

    time3_init(7199,9999);

    while(1)

    {


//一秒产生以一次中断,使得LED的状态发生反转   没用用到中断函数,直接进行中断判断


        if(TIM_GetITStatus(TIM3,TIM_IT_Update)!=RESET)//判断是否发生了更新(溢出)中断

        {

            TIM_ClearITPendingBit(TIM3,TIM_IT_Update);//清除溢出中断标志位

            LED0=!LED0;

        }

    }

}


这个时候time.c文件代码如下(省去了中断优先级配置函数和定时器3的中断观服务函数)


#include "time.h"

#include "led.h"

void time3_init(u16 per,u16 pre)

{

    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);//使能TIM3的时钟

    TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1;//TIM_CKD_DIV1是.h文件中已经定义好的,TIM_CKD_DIV1=0,也就是时钟分频因子为0

    TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up;//计数方式为向上计数

    TIM_TimeBaseStructure.TIM_Period=per;//周期

    TIM_TimeBaseStructure.TIM_Prescaler=pre;//分频系数

    TIM_TimeBaseInit(TIM3,&TIM_TimeBaseStructure);


    TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE);//使能TIM3的更新中断

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

}


time.h文件代码如下


#ifndef __TIME_H_

#define __TIME_H_

#include "sys.h"


void time3_init(u16 per,u16 pre);


#endif


不过我还是建议大家用标准的中断服务函数来写,不要直接在主函数中进行判断中断标志位,因为写成标准中断服务函数的形式,如果中断到来了,程序会立马进入到中断服务函数,而如果你没有写中断服务函数,而是直接在主函数或者其他函数中进行判断中断标志位的话,如果中断发生了,程序不会立即执行你想要的中断服务,你必须等到程序执行到判断中断标志位的那个地方,他才会执行相应的所谓的中断服务函数,如果你的程序比较多的话,可能就会对中断的响应有所影响,所以还是建议大家写成标准的中断服务函数,然后不要忘了要对中断有优先级进行配置。





『本文转载自网络,版权归原作者所有,如有侵权请联系删除』

热门文章 更多
STM32中断向量表的位置.重定向