開發環境:
MDK:凱爾 5.30
STM32立方體MX:V6.4.0
單片機:STM32F103ZET6
Cortex-M的內核中包含Systick定時器了,只要是Cortex-M系列的MCU就會有Systick,因此這是通用的,下面詳細分析。
4.1 Systick工作原理分析
SysTick 定時器被捆綁在 NVIC 中,用于產生 SysTick 異常(異常號:15)。 在以前,操作系統和所有使用了時基的系統都必須有一個硬件定時器來產生需要的“滴答”中斷,作為整個系統的時基。 滴答中斷對操作系統尤其重要。 例如,操作系統可以為多個任務分配不同數目的時間片,確保沒有一個任務能霸占系統; 或者將每個定時器周期的某個時間范圍賜予特定的任務等,操作系統提供的各種定時功能都與這個滴答定時器有關。 因此,**需要一個定時器來產生周期性的中斷,而且最好還讓用戶程序不能隨意訪問它的寄存器,以維持操作系統“心跳”的節律。 **
Cortex-M3 在內核部分包含了一個簡單的定時器——SysTick。 因為所有的 CM3 芯片都帶有這個定時器,軟件在不同芯片生產廠商的 CM3 器件間的移植工作就得以簡化。 該定時器的時鐘源可以是內部時鐘(FCLK,CM3 上的自由運行時鐘),或者是外部時鐘( CM3 處理器上的 STCLK 信號)。 不過,STCLK 的具體來源則由芯片設計者決定,因此不同產品之間的時鐘頻率可能大不相同。 因此,需要閱讀芯片的使用手冊來確定選擇什么作為時鐘源。 在 STM32 中 SysTick 以 HCLK(AHB 時鐘)或 HCLK/8 作為運行時鐘,見上圖。
SysTick 定時器能產生中斷,CM3 為它專門開出一個異常類型,并且在向量表中有它的一席之地。 它使操作系統和其他系統軟件在 CM3 器件間的移植變得簡單多了,因為在所有 CM3 產品間,SysTick 的處理方式都是相同的。 SysTick 定時器除了能服務于操作系統之外,還能用于其他目的,如作為一個鬧鈴、用于測量時間等。 **Systick 定時器屬于Cortex ** 內核部件 ,可以參考《ARM Cortex-M3 權威指南》((英)JosephYiu 著,宋巖譯,北京航空航天大學出版社出版)或“STM32xxx-Cortex-M3programmingmanual” (這是 ST 官方提供的電子版編程手冊,可以在 ST 官網下載)來了解。
4.2 Systick寄存器分析
在傳統的嵌入式系統軟件按中通常實現 Delay(N) 函數的方法為:
for(i = 0; i <= x; i ++);
x --- ;
對于STM32系列微處理器來說,執行一條指令只有幾十個 ns,進行 for 循環時,要實現 N 毫秒的 x 值非常大,而且由于系統頻率的寬廣,很難計算出延時 N 毫秒的精確值。 針對 STM32 微處理器,需要重新設計一個新的方法去實現該功能,以實現在程序中使用 Delay(N)。
Cortex-M3 的內核中包含一個 SysTick 時鐘。 SysTick 為一個 24 位遞減計數器,SysTick 設定初值并使能后,每經過 1 個系統時鐘周期,計數值就減 1。 計數到 0 時,SysTick 計數器自動重裝初值并繼續計數,同時內部的 COUNTFLAG 標志會置位,觸發中斷 (如果中斷使能情況下)。
在 STM32 的應用中,使用 Cortex-M3 內核的 SysTick 作為定時時鐘,設定每一毫秒產生一次中斷,在中斷處理函數里對 N 減一,在Delay(N) 函數中循環檢測 N 是否為 0,不為 0 則進行循環等待; 若為 0 則關閉 SysTick 時鐘,退出函數。
注:全局變量 TimingDelay , 必須定義為 volatile 類型 , 延遲時間將不隨系統時鐘頻率改變。
STM32中的Systick 部分內容屬于NVIC控制部分,一共有4個寄存器,名稱和地址分別是:
- STK_CTRL, 0xE000E010 -- 控制寄存器
第0位:ENABLE,Systick 使能位
(0:關閉Systick功能; 1:開啟Systick功能)
第1位:TICKINT,Systick 中斷使能位
(0:關閉Systick中斷; 1:開啟Systick中斷)
第2位:CLKSOURCE,Systick時鐘源選擇
(0:使用HCLK/8 作為Systick時鐘; 1:使用HCLK作為Systick時鐘)
第16位:COUNTFLAG,Systick計數比較標志,如果在上次讀取本寄存器后,SysTick 已經數到了0,則該位為1。 如果讀取該位,該位將自動清零
- STK_LOAD, 0xE000E014 -- 重載寄存器
Systick是一個遞減的定時器,當定時器遞減至0時,重載寄存器中的值就會被重裝載,繼續開始遞減。 STK_LOAD 重載寄存器是個24位的寄存器最大計數0xFFFFFF。
- STK_VAL, 0xE000E018 -- 當前值寄存器
也是個24位的寄存器,讀取時返回當前倒計數的值,寫它則使之清零,同時還會清除在SysTick 控制及狀態寄存器中的COUNTFLAG 標志。
- STK_CALRB, 0xE000E01C -- 校準值寄存器
校準值寄存器提供了這樣一個解決方案:它使系統即使在不同的CM3產品上運行,也能產生恒定的SysTick中斷頻率。 最簡單的作法就是:直接把TENMS的值寫入重裝載寄存器,這樣一來,只要沒突破系統極限,就能做到每10ms來一次 SysTick異常。 如果需要其它的SysTick異常周期,則可以根據TENMS的值加以比例計算。 只不過,在少數情況下, CM3芯片可能無法準確地提供TENMS的值(如, CM3的校準輸入信號被拉低),所以為保險起見,最好在使用TENMS前檢查器件的參考手冊。
SysTick定時器除了能服務于操作系統之外,還能用于其它目的:如作為一個鬧鈴,用于測量時間等。 要注意的是,當處理器在調試期間被喊停( halt)時,則SysTick定時器亦將暫停運作。
4.3 Systick定時器實現-標準庫
4.3.1main文件分析
主函數如下:
int main(void)
{
/* LED 端口初始化 */
LED_GPIO_Config();
/* 配置SysTick 為10us中斷一次 */
SysTick_Init();
for(;;)
{
LED1( ON );
Delay_us(10000); //10000 * 10us = 100ms
//Delay_ms(100);
LED1( OFF );
LED2( ON );
Delay_us(10000); // 10000 * 10us = 100ms
//Delay_ms(100);
LED2( OFF );
LED3( ON );
Delay_us(10000); // 10000 * 10us = 100ms
//Delay_ms(100);
LED3( OFF );
}
}
在 main 函數中,SysTick_Init() 和 Delay_us() 這兩個函數比較陌生,它們的功能分別是配置好 SysTick 定時器和進行精確延時。 整個 main 函數的流程就是初始化 LED 及SysTick 定時器之后,就進入死循環,輪流點亮 LED1、LED2、LED3,點亮的時間為精確的 100 ms。
4.3.2 stm32f103_SysTick.c文件分析
- 配置并啟動SysTick
我們看一下 SysTick_Init() 這個函數,其功能是啟動系統滴答定時器 SysTick ,并將 SysTick 配置為 10μs中斷一次。
void SysTick_Init(void)
{
/*SystemFrequency / 100000 10us中斷一次
* SystemFrequency / 1000000 1us中斷一次*/
// if (SysTick_Config(SystemFrequency / 100000)) // ST3.0.0庫版本
if(SysTick_Config(SystemCoreClock / 100000)) // ST3.5.0庫版本
{
/*Capture error */
while(1);
}
//關閉滴答定時器
SysTick->CTRL&= ~ SysTick_CTRL_ENABLE_Msk;
}
本函數實際上只是調用了 SysTick_Config()函數,它是屬于內核層的 Cortex-M3 通用函數,位于 core_cm3.h 文件中。 若調用 SysTick_Config()配置 SysTick 不成功,則進入死循環,初始化 SysTick 成功后,先關閉定時器,在需要的時候再開啟。 SysTick_Config() 函數無法在STM32 外設固件庫文件中找到其使用方法。 所以我們在 Keil 環境下直接跟蹤這個函數到 core_cm3.h 文件,查看函數的定義。
static __INLINE uint32_tSysTick_Config(uint32_t ticks)
{
if (ticks > SysTick_LOAD_RELOAD_Msk) return (1);
/* Reload value impossible */
SysTick->LOAD = (ticks &SysTick_LOAD_RELOAD_Msk) - 1; /* setreload register */
NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);
/* set Priority for Cortex-M0 System Interrupts */
SysTick->VAL = 0;
/* Load the SysTick Counter Value */
SysTick->CTRL =SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk;
/* Enable SysTickIRQ and SysTick Timer */
return (0); /* Functionsuccessful */
}
在這個函數定義的前面有關于它的注釋,如果我們不想去研究它的具體實現,可以根據這段注釋了解函數的功能:這個函數啟動了 SysTick ; 并把它配置為計數至 0 時引起中斷; 輸入的參數 ticks 為兩個中斷之間的脈沖數,即相隔 ticks 個時鐘周期會引起一次中斷; 配置 SysTick 成功時返回 0,出錯時返回 1。 但是,這段注釋并沒有告訴我們它把 SysTick 的時鐘設置為 AHB 時鐘還是 AHB/8,這是一個十分關鍵的問題,于是,我們將對這個函數的具體實現進行分析,與大家再分享一下如何分析底層庫函數。 分析底層庫函數,要有 SysTick 定時器工作分析的知識準備。
- 檢查輸入參數
SysTick_Config() 第 3 行代碼是檢查輸入參數 ticks,因為 ticks 是脈沖計數值,要被保存到重載寄存器 STK_LOAD 寄存器中,再由硬件把 STK_LOAD 值加載到當前計數值寄存器 STK_VAL 中使用,STK_LOAD 和 STK_VAL 都是 24 位的,所以當輸入參數 ticks 大于其可存儲的最大值時, 將由這行代碼檢查出錯誤并返回。
- 位指示宏及位屏蔽宏
檢查 ticks 參數沒有錯誤后,就稍稍處理一下把 ticks-1 賦值給 STK_LOAD 寄存器,要注意的是減 1,若 STK_VAL 從 ticks?1 向下計數至 0,實際上就經過了 ticks 個脈沖。 這句賦值代碼使用了宏SysTick_LOAD_RELOAD_Msk,與其他庫函數類似,這個宏是用來指示寄存器的特定位置或進行位屏蔽的。
/* SysTick Control / Status Register Definitions */
#define SysTick_CTRL_COUNTFLAG_Pos 16 /*!< SysTick CTRL: COUNTFLAG Position */
#define SysTick_CTRL_COUNTFLAG_Msk (1ul <
#define SysTick_CTRL_CLKSOURCE_Pos 2 /*!< SysTick CTRL: CLKSOURCE Position */
#define SysTick_CTRL_CLKSOURCE_Msk (1ul <
#define SysTick_CTRL_TICKINT_Pos 1 /*!< SysTick CTRL: TICKINT Position */
#define SysTick_CTRL_TICKINT_Msk (1ul <
#define SysTick_CTRL_ENABLE_Pos 0 /*!< SysTick CTRL: ENABLE Position */
#define SysTick_CTRL_ENABLE_Msk (1ul <
/* SysTick Reload Register Definitions */
#define SysTick_LOAD_RELOAD_Pos 0 /*!< SysTick LOAD: RELOAD Position */
#define SysTick_LOAD_RELOAD_Msk (0xFFFFFFul << SysTick_LOAD_RELOAD_Pos) /*!< SysTick LOAD: RELOAD Mask */
/* SysTick Current Register Definitions */
#define SysTick_VAL_CURRENT_Pos 0 /*!< SysTick VAL: CURRENT Position */
#define SysTick_VAL_CURRENT_Msk (0xFFFFFFul <
/* SysTick Calibration Register Definitions */
#define SysTick_CALIB_NOREF_Pos 31 /*!< SysTick CALIB: NOREF Position */
#define SysTick_CALIB_NOREF_Msk (1ul <
#define SysTick_CALIB_SKEW_Pos 30 /*!< SysTick CALIB: SKEW Position */
#define SysTick_CALIB_SKEW_Msk (1ul <
#define SysTick_CALIB_TENMS_Pos 0 /*!< SysTick CALIB: TENMS Position */
#define SysTick_CALIB_TENMS_Msk (0xFFFFFFul <
/*@}*/ /* end of group CMSIS_CM3_SysTick */
其中寄存器位指示宏:SysTick_xxx_Pos ,宏展開后即為 xxx 在相應寄存器中的位置,如控制 SysTick 時鐘源的 SysTick_CTRL_CLKSOURCE_Pos,宏展開為 2,這個寄存器位正是寄存器 STK_CTRL 中的 Bit2。
而寄存器位屏蔽宏:SysTick_xxx_Msk,宏展開是 xxx 的位全部置 1 后,左移SysTick_xxx_Pos 位。 如控制 SysTick 時鐘源的SysTick_CTRL_CLKSOURCE_Msk,宏展開為“1ul <
配置中斷向量及重置 STK_VAL 寄存器回到 SysTick_Config()函數,接下來調用了 NVIC_SetPriority () 函數并配置了 SysTick中斷,這就是為什么我們在外部沒有再使用 NVIC 配置 SysTick 中斷的原因。 配置好SysTick 中斷后把 STK_VAL 寄存器重新賦值為 0(在使能 SysTick 時,硬件會把存儲在STK_LOAD 寄存器中的 ticks 值加載給它)。配置 SysTick 時鐘為 AHB在這段代碼最后,向 STK_CTRL 寄存器寫入了 SysTick 的控制參數,配置為使用AHB 時鐘,使能計數至 0 時引起中斷,使能 SysTick。 執行了這行代碼,SysTick 就開始運行并進行脈沖計數了。若讀者想要使用 AHB/8 作為時鐘,可以調用庫函數SysTick_CLKSourceConfig() 進行修改,也可以直接對 SysTick_Config() 函數的代碼進行修改。使能、關閉定時器由于調用 SysTick_Config()函數之后,SysTick 定時器就被開啟了,但我們在初始化的時候并不希望這樣,而是根據需要再開啟。 所以在 SysTick_Init() 函數中,調用完SysTick_Confi g()并配置好后,應先把定時器關閉了。 SysTick 的開啟和關閉由寄存器STK_CTRL 的 Bit0 :ENABLE 位來控制,使用位屏蔽宏以操作寄存器的方式實現。SysTick->CTRL |=SysTick_CTRL_ENABLE_Msk; // 使能滴答定時器SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; // 關閉滴答定時器定時時間的計算在調用SysTick_Config()函數時,向它輸入的參數為SystemCoreClock /100000,SystemCoreClock為定義了系統時鐘(SYSCLK)頻率的宏,即等于 AHB的時鐘頻率。 在本書的所有例程中AHB 都是被配置為 72 MHz 的,也就是這個 SystemCoreClock 宏展開為數值 7200 0000。根據前面對 SysTick_Config()函數的介紹,它的輸入參數為 SysTick 將要計時的脈沖數,經過 ticks 個脈沖(經過 ticks 個時鐘周期)后將觸發中斷,觸發中斷后又重新開始計數。 由此我們可以算出定時的時間,下面為計算公式:T=即時報價×(1/f)其中,T 為要定時的總時間; ticks 為 SysTick_Config() 的輸入參數; 1/ f 即為SysTick 使用的時鐘源的時鐘周期,f 為該時鐘源的時鐘頻率,當時鐘源確定后為常數。例如:本實驗例子中,使用時鐘源為 AHB 時鐘,其頻率被配置為 72 MHz。 調用函數時,把 ticks 賦值為 ticks=SystemFrequency/ 100000 =720,表示 720 個時鐘周期中斷一次; 1/f 是時鐘周期的時間,此時(1/f =1/72 μs),所以最終定時總時間 T=720×(1/72),為720 個時鐘周期,正好是 10 μs。SysTick 定時器的定時時間(配置為觸發中斷,即為中斷周期)由 ticks 參數決定,最大定時周期不能超過 2^24^ 個。編寫中斷服務函數一旦我們調用了 Delay_us() 函數,SysTick 定時器就被開啟,按照設定好的定時周期遞減計數,當 SysTick 的計數寄存器的值減為 0 時,就進入中斷函數,當中斷函數執行完畢之后重新計時,如此循環,除非它被關閉。void Delay_us(__IO u32 nTime){ TimingDelay = nTime; // 使能滴答定時器 SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; while(TimingDelay != 0);}使能了 SysTick 之后,就使用 while(TimingDelay != 0)語句等待 TimingDelay 變量變為 0,這個變量是在中斷服務函數中被修改的。 因此,我們需要編寫相應的中斷服務程序,在本實驗室中我們配置為 10μs 中斷一次,每次中斷把 TimingDelay 減 1。 中斷程序在 stm32f10x_it.c 中實現。void SysTick_Handler(void){ TimingDelay_Decrement(); }SysTick中斷屬于系統異常向量,在stm32f10x_it.c文件中已經默認有了它的中斷服務函數SysTick_Handler(),但內容為空。 我們找到這個函數,其調用了用戶函數TimingDelay_Decrement()。 后者是由用戶編寫的一個應用程序。void TimingDelay_Decrement(void){ if (TimingDelay != 0x00) { TimingDelay--; }}每次進入 SysTick 中斷就調用一次TimingDelay_Decrement()函數,使全局變量TimingDelay 自減一次。 用戶函數 Delay_us ()在TimingDelay 被減至0時,才退出延時循環,即我們對 TimingDelay 賦的值為要中斷的次數。 所以總的延時時間:T 延時 = T 中斷周期 x TimingDelay至此,SysTick 的精確延時功能講解完畢。4.4 Systick定時器實現-HAL庫4.4.1 STM32Cube配置工程關于如何使用STM32Cube新建工程在前文已經講解過了,這里直說配置GPIO部分內容。 本文要實現流水燈,其實輸出為初始化設置為高電平還是低電平都可以,因為流水燈需要不斷反轉。 在上一節筆者已經講過了。1.GPIO配置我們將PB0、PG6、PG7配置輸出模式(高電平、低電平均可)、輸出速率、上/下拉等,默認即可。2.時鐘源配置3.時鐘配置4.sys配置(滴答定時器配置)以上配置和GPIO流水燈是一樣的,本文只具體講解Systick的內容。4.4.2 Systick定時器具體代碼分析Systick屬于內核部分,相關的寄存器定義與庫函數都在內核相關的文件core_cm3.h中,在上標準庫函數版本中已經分析過了。 那么HAL庫函數是如何初始化Systick的呢? 在HAL_Init()函數中調用了HAL_InitTick()函數,這才是Systick初始化入口。__weak HAL_StatusTypeDefHAL_InitTick(uint32_t TickPriority){ /* Configure the SysTick to have interrupt in 1ms time basis*/ if (HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq)) > 0U) { return HAL_ERROR; } /* Configure the SysTick IRQ priority */ if (TickPriority < (1UL << __NVIC_PRIO_BITS)) { HAL_NVIC_SetPriority(SysTick_IRQn,TickPriority, 0U); uwTickPrio = TickPriority; } else { return HAL_ERROR; } /* Return function status */ return HAL_OK;}HAL_SYSTICK_Config()函數和標準庫函數差不多,默認中斷周期是1ms,HAL_TICK_FREQ_DEFAULT是一個宏定義表示計數的頻率,默認是1,也就是1KHz,也就是1/1000,那么中斷一次的時間為72000000/1000/1*(1/72000000)=1ms。 那么我們要延時1s怎么做呢。 我們在上一節流水燈使用了HAL_Delay()函數,函數原型如下。__weak void HAL_Delay(uint32_tDelay){ uint32_t tickstart = HAL_GetTick(); uint32_t wait = Delay; /* Add a freq to guarantee minimum wait */ if (wait < HAL_MAX_DELAY) { wait += (uint32_t)(uwTickFreq); } while ((HAL_GetTick() - tickstart) < wait) { }}在函數中HAL_Delay(),(HAL_GetTick() -tickstart) < wait用于延時的中斷周期數,在Systick初始化函數中,中斷間隔為1ms,HAL_Delay ()函數的傳入參數Delay表示多少個中斷周期,也就是我們最終的延時,我們傳入Delay = 500,那么最終的延時就是500ms。我們再來看看HAL_GetTick()函數。__weak uint32_tHAL_GetTick(void){ return uwTick;}HAL_GetTick()函數很簡單,不斷獲取uwTick得值,這是一個全局變量,可以發現在HAL_IncTick()函數中使用過。 那么HAL_IncTick()函數被那個函數調用了呢?__weak void HAL_IncTick(void){ uwTick += uwTickFreq;}不難發現,在stm32f1xx_it.c中間中的SysTick_Handler()函數中調用了HAL_IncTick()函數,SysTick_Handler()也就是滴答定時器的中斷服務函數,也就是中斷一次會調用一次,也就會uwTick變量累加一次,最終uwTick累加到Delay次,表示此次延時結束。void SysTick_Handler(void){ /* USER CODE BEGIN SysTick_IRQn 0 */ /* USER CODE END SysTick_IRQn 0 */ HAL_IncTick(); /* USER CODE BEGIN SysTick_IRQn 1 */ /* USER CODE END SysTick_IRQn 1 */}好了,使用STM32Cube配置SysTick定時器的延時就講解完成了,在主函數是使用延時函數控制LED就是流水燈了。int main(void){ /* USER CODE BEGIN 1 */ /* USER CODE END 1 */ /* MCUConfiguration--------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and theSystick. */ 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(); /* USER CODE BEGIN 2 */ /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_0); HAL_Delay(500); HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_0); HAL_GPIO_TogglePin(GPIOG,GPIO_PIN_6); HAL_Delay(500); HAL_GPIO_TogglePin(GPIOG,GPIO_PIN_6); HAL_GPIO_TogglePin(GPIOG,GPIO_PIN_7); HAL_Delay(500); HAL_GPIO_TogglePin(GPIOG,GPIO_PIN_7); } /* USER CODE END 3 */}4.5實驗現象將編譯好的程序下載到板子中,可以看到三個LED燈不同地閃爍。
-
mcu
+關注
關注
146文章
16992瀏覽量
350310 -
ARM
+關注
關注
134文章
9046瀏覽量
366817 -
定時器
+關注
關注
23文章
3237瀏覽量
114467 -
Cortex-M
+關注
關注
2文章
227瀏覽量
29727 -
Systick
+關注
關注
0文章
62瀏覽量
13043
發布評論請先 登錄
相關推薦
評論