上一篇瞭解如何使用按鍵來觸發外部中斷,將中斷服務程式中的計數累計起來,並顯示在 OLED 螢幕上。這篇要稍微改一下中斷的方式,改以 Timer 定時器來引發中斷,並瞭解一下 STM32 的定時器功能。
在設計嵌入式的應用程式開發時,定時器(Timer)和計數器(Counter)對系統來說非常重要,常用於計算所經過的時間、延遲、計算次數等。談到定時器,要先認識 STM32 提供的時鐘種類,有以下幾種:
這裡所提的內部、外部時鐘其實還有分高速和低速,內容較多,改天整理資料再撰寫另一篇文章討論一下 STM32 的時鐘體系。這裡先瞭解要使用定時器 Timer 和 計數器 Counter,會用到上述 4 種時鐘來源即可。
STM32F103x 一共有 11 個定時器:
上述的計數類型有三種:
再來看一下基本定時器的方塊圖:
① 時鐘源
計時器時鐘 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 設定為預設值。
- 引數 TIMx 是定時器編號。
✈ TIM_InternalClockConfig(TIM_TypeDef *TIMx)
採用內部時鐘給 TIMx 提供時鐘源。
- 引數 TIMx 是定時器編號。
✈ TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct)
對 TIMx 進行初始化,需先定義並填入一個 TIM_TimeBaseInitTypeDef 類型的結構體。
✈ 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_ARRPreloadConfig(TIM_TypeDef *TIMx, FunctionalState NewState)
設定是否使用預裝載緩衝器。
- 引數 TIMx 是定時器編號。
- 引數 NewState 設定為啟用 ENABLE 或停用 DISABLE。
在設計嵌入式的應用程式開發時,定時器(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 定時器
類別 | 名稱 | 匯流排 | 計數器類型 | 捕獲/比較通道 | 互補輸出 | 功能 |
---|---|---|---|---|---|---|
基本定時器 | TIM6、TIM7 | APB1 | 向上 | 0 | 沒有 | ▸定時中斷:16 位元可往上自動裝載計數器 ▸觸發 DAC 的同步電路 ▸更新事件時產生中斷/DMA:計數器溢出 |
通用定時器 | TIM2、TIM3、 TIM4、TIM5 | APB1 | 向上、 向下、 向上/向下 | 4 | 沒有 | 擁有基本定時器全部功能之外,加上: ▸16位元可向上、向下及向上向下自動裝載計數器 ▸內外時鐘源選擇 ▸輸入捕獲(Input capture):測量輸入信號的脈衝寬度 ▸輸出比較(Output compare):產生輸出波形 ▸編碼器介面 ▸主從觸發模式 ▸PWM 輸出 |
高級定時器 | TIM1、TIM8 | APB2 | 向上、 向下、 向上/向下 | 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: 暫停中斷來源
✈ 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 連接的方式如下:STM32F103x | SSD1306 OLED |
---|---|
3.3v | VDD |
GND | GND |
B8 | SCK/SCL |
B9 | SDA |
[程式]
主程式一開始先對 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
張貼留言