我想每一個單片機愛好者及工程開發設計人員都有過點燈的經歷。流水燈是個好東西,尤爲是在調試資源有限的環境中,有時會幫上大忙。app
然在最初入門時,如何讓這些小燈們按照咱們的想法歡快地跑起來呢,絕大多數小朋友的作法是:在一個while循環里加上延時程序,讓小燈在每一個狀態下停留一段時間,再進入下一個狀態,這樣小燈們就會在不一樣的狀態中切換,就能夠根據咱們設計的程序閃爍了。函數
這樣這裏就會涉及到一個延時程序的編寫的問題,而通常的作法是一個for循環裏去減一個很大的數,直到爲0,則延時完成,那個數的值則是根據時鐘頻率和指令運行週期,估算出來的,還記得較久之前看過一篇帖子介紹51單片機精確延時的幾種方法,有一種方法是在keil中設定好時鐘頻率,而後經過軟件仿真試來算延時時間,以達到較精肯定時。ui
但這些方法通常都不夠方便,延時也不夠精確,更高階一點的方法即是開一個定時器,在定時中斷裏面計數達到精確延時的目的。spa
在STM32的應用中,可考慮利用SysTick系統嘀嗒定時器來實現。但在STM32開發手冊中對它的介紹卻不多,幾乎到沒有的程度。由於它是Cortex內核的部分,CM3爲它專門開出一個異常類型,而且在中斷向量表中佔有一席之地(異常號15),這樣它能夠很方便的移植到不一樣廠商出CM3內核的芯片上,而且對於有實時操做系統的軟件,它通常會做爲整個系統的時基,這個對操做系統很是重要。有關SysTick的詳細介紹可參考《Cortex-M3 權威指南》第133 頁第八章及第179頁第十三章。操作系統
SysTick總共有四個寄存器:設計
一、
調試
對應於軟件中 SysTick->CTRL;code
二、
事件
對應於軟件中 SysTick-> LOAD;資源
三、
對應於軟件中 SysTick-> VAL;
四、
對應於軟件中 SysTick-> CALIB (如上圖),沒有用過,也不經常使用,暫不做介紹。
這幾個寄存器的偏移量以下圖所示:
寄存器結構體的定義在 \CMSIS\CM3\CoreSupport core_cm3.h中,以下
/** @addtogroup CMSIS_CM3_SysTick CMSIS CM3 SysTick memory mapped structure for SysTick @{ */ typedef struct { __IO uint32_t CTRL; /*!< Offset: 0x00 SysTick Control and Status Register */ __IO uint32_t LOAD; /*!< Offset: 0x04 SysTick Reload Value Register */ __IO uint32_t VAL; /*!< Offset: 0x08 SysTick Current Value Register */ __I uint32_t CALIB; /*!< Offset: 0x0C SysTick Calibration Register */ } SysTick_Type;
SysTick這個脈衝計數值被保存到SysTick->VAL 當前計數值寄存器中,它只能向下計數,每接收到一個時鐘脈衝SysTick->VAL 的值就向下減 1,直至0,而後由硬件自動把重載寄存器SysTick->LOAD 中的值到SysTick->VAL從新計數,而且當SysTick->VAL值計數到0時,觸發異常,調用void SysTick_Handler(void)函數,能夠在此中斷服務函數中處理定時中斷事件了, 是一個24 位的定時器,即一次最多能夠計數 224個時鐘脈衝,一般是對設定值進行遞減計數操做。只要不把它在SysTick控制及狀態寄存器SysTick->CTRL中的第0位使能位清除,就永不停息。
SysTick 中斷優先級問題這裏須要強調下。
它屬於系統異常,是內核級中斷,而且優先級是能夠設置的,具體設置也是在 core_cm3.h中
/** * @brief Initialize and start the SysTick counter and its interrupt. * * @param ticks number of ticks between two interrupts * @return 1 = failed, 0 = successful * * Initialise the system tick timer and its interrupt and start the * system tick timer / counter in free running mode to generate * periodical interrupts. */ static __INLINE uint32_t SysTick_Config(uint32_t ticks) { if (ticks > SysTick_LOAD_RELOAD_Msk) return (1); /* Reload value impossible */ SysTick->LOAD = (ticks & SysTick_LOAD_RELOAD_Msk) - 1; /* set reload register */ NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1); SysTick->VAL = 0; SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk; return (0); /* Function successful */ }
其中以下這句就是設置優先級的函數,此函數對內核中斷優先級和外部中斷優先級設置通吃,比較強大,但須要手動算出來搶佔和從優先級,不太方便,當跳進此函數,咱們能夠算出Systick默認優先是最低的(效果至關於SCB->SHP[11] = 0xF0;)
NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);
此時若其它外部中斷優先級設置比它高時,能夠剝奪它進而轉向外部中斷。
能夠作以下實驗驗證:
先設置一事件中斷,把優先級設置高一些,
void Exti_Config(void) { EXTI_InitTypeDef EXTI_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; EXTI_InitStructure.EXTI_Line = EXTI_Line1; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Event; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); }
注:中斷分組我在實驗中,最初初始化設置爲以下:
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
設爲第二組。
在
void SysTick_Handler(void) { EXTI_GenerateSWInterrupt(EXTI_SWIER_SWIER1); LED_1 = ON; Delay(); }
系統滴答中斷裏觸發外部中斷事件,並點亮LED1 。
外部中斷處理函數以下
void EXTI1_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line1) != RESET) { EXTI_ClearITPendingBit(EXTI_Line1); LED_0 = ON; Delay(); } }
此延時函數爲阻塞延時以下:
void Delay(void) { u32 i; for(i=0 ; i < 0xFFFFF; i++){} }
加入延時是爲了看出來哪一個燈先亮。
當外部中斷優先級比較高時,它能夠搶佔Systick中斷先執行,以上代碼實驗結果爲,LED0先點亮後,再回到LED1再點亮。
當把外部中斷設置爲與systick相同的優先級時,則systick優先級就會相對較高,例如把上面的優先級改成
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
則會LED1先亮,執行完SysTick_Handle函數後才輪到EXTI1_IRQHandler執行。
我的認爲,若要實現systick精確延時,最好把systick優先級設置高一些,例如
NVIC_SetPriority (SysTick_IRQn, 0);
即把SCB->SHP[11] = 0x00;則可達到systick優先級高於任合外部中斷的效果,此時延時會比較精準。
另外對於SysTick的時鐘源的選擇,要注意它的時鐘源可選擇內部時鐘(FCLK,CM3上的自由運行時鐘,STM32中對應是AHB),或者是外部時鐘( CM3處理器上的STCLK信號,STM32中對應是AHB/8)
可參考以下圖
它是在SysTick->CTRL第二位CLKSOURCE時鐘源選擇中設置。
有關systick延時函數的編寫可參考野火《零死角玩轉stm32-初級篇》。
至此咱們能夠簡單的實現一流水燈程序
while(1) { LED_0 =OFF; LED_1 = ON; Delay_ms(500); LED_0 =OFF; LED_1 = ON; Delay_ms(500); }
然而這樣作真的好嗎 ?這裏用的是 阻塞延時哦,CPU的效率很大一部分就耗在了空轉上了,太浪費資源。
假設系統時鐘頻率爲72MHZ或者幾十上百MHZ時,當完成一個循環只須要幾十或十幾納秒級或者更短,而在這個循環之中阻塞延時個幾十至幾百毫秒的話,就像是在高速公路上忽然橫出一條坑坑窪窪的泥濘路,那可想整條路都會所以而慢下來,甚至會出現災難性的後果,我的認爲,一般在系統初始化過程當中,各芯片的時序對時間有要求,能夠用下阻塞延時,只須要系統啓動時運行一下,當系統跑起來以後,最好就別再傻呼呼的這麼作了。
這時主要採用的是在定時器裏計數,在外部循環中對變量查詢,達到某個值時再執行某個動做,達到延時的效果,而在時間未到時,系統還能夠不停的跑圈圈,作別的事情去。
在定時中斷裏每毫秒計數一次 gticks
while(1) { if(500 == gticks) { LED_0 =OFF; LED_1 = ON; } if(1000 == gticks) { LED_0 =OFF; LED_1 = ON; gticks = 0 } Do_others(); }
以上須要在事件處理過程當中對gticks進行處理,增長了代碼的耦合度,更容易出錯,若是在一個事件處理中對gticks清除了,而下個事件中又須要查詢它,這樣就可能致使處理時序的錯亂,相互干擾。
可否在事件處理中只提供查詢功能,而定時的事情就交給定時本身去作?
下節高手將登場了,爲你們介紹個我曾在一項目中學到的,非阻塞延時的精妙設計。