1. 程式人生 > >STM32之定時器中斷控制LED閃爍

STM32之定時器中斷控制LED閃爍

上篇部落格我們是用延時函式實現了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;

因為我們用到了定時器3,所以我們就要使能定時器3的時鐘

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;//分頻係數

這裡面每一個成員變數所賦的值都是在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

這條語句是用來設定中斷優先順序分組,在不同的分組裡面會有不同的搶佔優先順序和響應優先順序可供選擇,這裡我也不多講。
到此為止我們的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

不過我還是建議大家用標準的中斷服務函式來寫,不要直接在主函式中進行判斷中斷標誌位,因為寫成標準中斷服務函式的形式,如果中斷到來了,程式會立馬進入到中斷服務函式,而如果你沒有寫中斷服務函式,而是直接在主函式或者其他函式中進行判斷中斷標誌位的話,如果中斷髮生了,程式不會立即執行你想要的中斷服務,你必須等到程式執行到判斷中斷標誌位的那個地方,他才會執行相應的所謂的中斷服務函式,如果你的程式比較多的話,可能就會對中斷的響應有所影響,所以還是建議大家寫成標準的中斷服務函式,然後不要忘了要對中斷有優先順序進行配置。