STM32筆記(19):定時器 Timer 與定時器中斷

上一篇瞭解如何使用按鍵來觸發外部中斷,將中斷服務程式中的計數累計起來,並顯示在 OLED 螢幕上。這篇要稍微改一下中斷的方式,改以 Timer 定時器來引發中斷,並瞭解一下 STM32 的定時器功能。
在設計嵌入式的應用程式開發時,定時器(Timer)和計數器(Counter)對系統來說非常重要,常用於計算所經過的時間、延遲、計算次數等。談到定時器,要先認識 STM32 提供的時鐘種類,有以下幾種:
時鐘來源說明
內部時鐘(CK_INT)-
外部時鐘模式1外部輸入腳(TIx)
外部時鐘模式2外部觸發輸入(ETR)
內部觸發輸入(ITRx,x=0,1,2,3)使用一個定時器作爲另一個定時器的預分頻器。

這裡所提的內部、外部時鐘其實還有分高速和低速,內容較多,改天整理資料再撰寫另一篇文章討論一下 STM32 的時鐘體系。這裡先瞭解要使用定時器 Timer 和 計數器 Counter,會用到上述 4 種時鐘來源即可。

STM32F103x 一共有 11 個定時器:
  • 2 個高階控制定時器
  • 4 個普通定時器
  • 2 個基本定時器
  • 2 個看門狗定時器
  • 1 個 Systick 定時器
扣除看門狗和 Systick 定時器,其他還有 8 個,將之命名為 TIM1~TIM8,STM32 將這 8 個定時器分成以下三類:
類別名稱匯流排計數器類型捕獲/比較通道互補輸出功能
基本定時器TIM6、TIM7APB1向上0沒有▸定時中斷:16 位元可往上自動裝載計數器
▸觸發 DAC 的同步電路
▸更新事件時產生中斷/DMA:計數器溢出
通用定時器TIM2、TIM3、
TIM4、TIM5
APB1向上、
向下、
向上/向下
4沒有擁有基本定時器全部功能之外,加上:
▸16位元可向上、向下及向上向下自動裝載計數器
▸內外時鐘源選擇
▸輸入捕獲(Input capture):測量輸入信號的脈衝寬度
▸輸出比較(Output compare):產生輸出波形
▸編碼器介面
▸主從觸發模式
▸PWM 輸出
高級定時器TIM1、TIM8APB2向上、
向下、
向上/向下
4擁有通用定時器全部功能之外,加上:
▸重複計數器
▸死區生成
▸互補輸出
▸刹車輸入

上述的計數類型有三種:
  • 向上計數:從 0 開始,計算到暫存器 ARR 預設值時產生溢出事件,然後再返回 0 後繼續計數。
  • 向下計數:從暫存器 ARR 的預設值開始,倒數到 0 後產生溢出事件,然後再從 ARR 預設值開始倒數計數。
  • 向上/向下(或稱中央對齊)計數:從 0 開始向上計數到 ARR 預設值後產生溢出事件,然後向下計數到 1 以後再次產生溢出,然後再從 0 開始向上計數。

再來看一下基本定時器的方塊圖:
① 時鐘源
計時器時鐘 TIMxCLK,由內部時鐘 CK_INT,經 APB1 預分頻器後分頻提供,如果 APB1 預分頻係數等於 1,則頻率不變,若是 APB1 預分頻的係數是 2,即 PCLK1 = 72M / 2 = 36M,所以計時器時鐘 TIM x CLK = 36*2 = 72M。

②計數器時鐘
計時器時鐘經過預分頻暫存器(TIMx_PSC)之後,即 CK_CNT,用來驅動計數器計數。PSC 是一個 16 位的預分頻器,可以對計時器時鐘 TIMxCLK 進行 1~65536 之間的任何一個數進行分頻。具體計算方式為:CK_CNT = TIMxCLK / (PSC + 1)。

③計數器
計數器 CNT 是一個 16 位的計數器,只能往上計數,最大計數值為 65535。當計數達到自動重裝載寄存器的時候產生更新事件,並清零從頭開始計數。

④自動重裝載寄存器(TIMx_ARR)
自動重裝載寄存器 ARR 是一個 16 位的暫存器,這裡面裝著計數器能計數的最大數值。當計數到這個值的時候,如果啟用中斷的話,計時器就產生溢出中斷。

上圖最重要的有三個暫存器:計數器暫存器 (TIMx_CNT)、預分頻器暫存器 (TIMx_PSC) 及自動重載暫存器 (TIMx_ARR)。如果要計算「溢出時間 Tout」,可使用以下這個計算公式:

Tout = ((ARR + 1)*(PSC + 1))/ Tclk;

Tout 的單位是秒(s),其中 Tclk 是 TIMx 輸入的時鐘頻率(單位爲 Mhz),通常為 72 Mhz。
舉個例子,如要得到定時器為 0.5 秒的值,各暫存器要如何設定?可以設定 ARR 暫存器的值為 4999,PSC 暫存器的值為 7199,這樣計算出來的結果就是 0.5 秒。計算公式如下:
Tout = (4999 + 1)*(7199 + 1)/72000000 = 0.5s = 500 ms,也就是中斷時間為 0.5 秒。

[常用的 Timer 函式]

TIM_DeInit(TIM_TypeDef *TIMx);
將 Timer 設定為預設值。
- 引數 TIMx 是定時器編號。

TIM_InternalClockConfig(TIM_TypeDef *TIMx)
採用內部時鐘給 TIMx 提供時鐘源。
- 引數 TIMx 是定時器編號。

TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct)
對 TIMx 進行初始化,需先定義並填入一個 TIM_TimeBaseInitTypeDef 類型的結構體。
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStruct 這個結構體有以下 5 個引數:
引數名稱說明
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIVx;用來設置時鐘分頻因子,TIM_CKD_DIVx; x 可以是1,2,4三種。
TIM_TimeBaseInitStructure.TIM_CounterMode = xxx;設置計數方式,xxx 可以設置為:
▸TIM_CounterMode_Up:向上計數
▸TIM_CounterMode_Down:向下計數
▸TIM_CounterMode_CenterAligned:中央對齊計數
TIM_TimeBaseInitStructure.TIM_Period = n;設置自動重載計數週期值,這個值就是上方公式的 PSC。
TIM_TimeBaseInitStructure.TIM_Prescaler = n;預分頻係數。這個係數就是上方所列公式的 ARR。其值範圍是從 0 ~ 65535。
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = n;重複計數,通常設為 0 ,就是經過 n 次溢出時,才產生一個
中斷。是高級定時器才有的功能。

TIM_ClearFlag(TIM_TypeDef *TIMx, uint16_t TIM_FLAG)
清除溢出中斷標誌。
- 引數 TIMx 是定時器編號。

TIM_ITConfig(TIM_TypeDef *TIMx, uint16_t TIM_IT, FunctionalState NewState);
啟用或停用 TIMx 的中斷類型。
- 引數 TIMx 是定時器編號。
- 引數 TIM_IT 用來指明我們使能的定時器中斷的類型,定時器中斷的類型可以是以下幾種:
  • TIM_IT_Update: 更新中斷來源
  • TIM_IT_CC1: 捕捉比對編號1 的中斷來源
  • TIM_IT_CC2: 捕捉比對編號2 的中斷來源
  • TIM_IT_CC3: 捕捉比對編號3 的中斷來源
  • TIM_IT_CC4: 捕捉比對編號4 的中斷來源
  • TIM_IT_COM: 溝通中斷來源
  • TIM_IT_Trigger: 觸發中斷來源
  • TIM_IT_Break: 暫停中斷來源
- 引數 NewState 設定為啟用或停用:ENABLE 或 DISABLE。

TIM_ARRPreloadConfig(TIM_TypeDef *TIMx, FunctionalState NewState)
設定是否使用預裝載緩衝器。
- 引數 TIMx 是定時器編號。
- 引數 NewState 設定為啟用 ENABLE 或停用 DISABLE。


[程式撰寫步驟]

(1) 設定系統時鐘
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
(2) 設定 NVIC 中斷優先順序分組
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
(3) 設定 GPIO,若不需要中斷時控制輸出入的引腳,這段程式可以省略。
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;       //選擇引腳5
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //輸出頻率最大50MHz
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //帶上拉電阻輸出
GPIO_Init(GPIOB,&GPIO_InitStructure);
(4) 設定 TIMER
TIM_InternalClockConfig(TIM2);  //設定 TIM2 內部時鐘源

TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; // 初始化 TIMx,定義並填入 TIM_TimeBaseInitTypeDef 類型的結構體
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;  //用來設置時鐘分頻因子
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;  //設置計數方式
TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1;		//設置自動重載計數週期值
TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;		//預分頻係數
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
	
TIM_ClearFlag(TIM2, TIM_FLAG_Update);  		// 清除溢出中斷標誌。
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);  // 啟用 TIMx 的中斷
(5) 啟用 Timer
TIM_Cmd(TIM2, ENABLE);
(6) 編寫外部中斷服務函式的
void TIM2_IRQHandler(void)  //TIM2 中斷
{
	if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)  ////檢查指定的TIM中斷發生與否
	{
		... 程式邏輯
		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);  //清除TIMx的中斷待處理位
	}
}

依照上述幾個步驟,就可完成定時器中斷的控制,以下就用一個定時器觸發中斷,然後將計數器加 1,根據內部時鐘計算,這個計數器每跳一次剛好是 1 秒鐘,並將秒數顯示在 OLED 上。

[材料]

  • STM32F103C8T6 主板 x 1
  • OLED SSD1306 顯示器 x1
  • STLINK V2 模擬下載器 x 1
  • 麵包板 x1
  • 連接線 x N 條

[接線與電路圖]

按鍵的一個接腳接在 GND,另一腳接在 STM32F103x PB14,與 SSD1306 連接的方式如下:
STM32F103xSSD1306 OLED
3.3vVDD
GNDGND
B8SCK/SCL
B9SDA

[程式]

主程式一開始先對 OLED 進行初始化,進入迴圈循環後就等中斷產生,主程式 main.c 如下:
#include "stm32f10x.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_gpio.h"
#include "delay.h"
#include "sys.h"
#include "oled.h"
#include "bmp.h"
#include "Timer.h"

uint16_t Second;

int main (void)
{
	delay_init();	    	 //延時函數初始化	  
	NVIC_Configuration(); 	 //設置 NVIC 中斷分組2:2位搶佔優先順序,2位回應優先順序 	
	OLED_Init();		 //初始化OLED  
	Timer_Init();
	
	OLED_Clear(0);         //清除螢幕
	OLED_ShowString(2, 2, "Second:",16);	
	while (1)
	{

		OLED_ShowNum(60, 2, Second,5, 16);
	}
}

新增一個 Timer.c 程式及一個 Timer.h 標題檔案,放置於 System 的目錄中。以下是 Timer.c 的內容:
#include "stm32f10x.h"           

extern uint16_t Second;

void Timer_Init(void)
{
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
	
	TIM_InternalClockConfig(TIM2);
	
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
	TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1;
	TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
	
	TIM_ClearFlag(TIM2, TIM_FLAG_Update);
	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_Init(&NVIC_InitStructure);
	
	TIM_Cmd(TIM2, ENABLE);
}

void TIM2_IRQHandler(void)
{
	if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
	{
		Second ++;
		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
	}
}

以下是 Timer.h 標題檔的內容:
#ifndef __TIMER_H
#define __TIMER_H

void Timer_Init(void);

#endif

完整的程式請參考 Github:5. Timers and Timer Interrupts

[結果]

因用手機拍攝的頻率與 OLED 螢幕接近,看起來螢幕是閃爍的,實際用眼睛看時是不會。

[參考資料]


Post a Comment

較新的 較舊