1. 程式人生 > >STM32系統滴答_及不可不知的延時技巧

STM32系統滴答_及不可不知的延時技巧

然在最初入門時,如何讓這些小燈們按照我們的想法歡快地跑起來呢,絕大多數小朋友的做法是:在一個while迴圈里加上延時程式,讓小燈在每個狀態下停留一段時間,再進入下一個狀態,這樣小燈們就會在不同的狀態中切換,就可以根據我們設計的程式閃爍了。

這樣這裡就會涉及到一個延時程式的編寫的問題,而一般的做法是一個for迴圈裡去減一個很大的數,直到為0,則延時完成,那個數的值則是根據時鐘頻率和指令執行週期,估算出來的,還記得較久以前看過一篇帖子介紹51微控制器精確延時的幾種方法,有一種方法是在keil中設定好時鐘頻率,然後通過軟體模擬試來算延時時間,以達到較精確定時。

但這些方法一般都不夠方便,延時也不夠精確,更高階一點的方法便是開一個定時器,在定時中斷裡面計數達到精確延時的目的。

STM32的應用中,可考慮利用SysTick系統嘀嗒定時器來實現。但在STM32開發手冊中對它的介紹卻很少,幾乎到沒有的程度。因為它是Cortex核心的部分,CM3為它專門開出一個異常型別,並且在中斷向量表中佔有一席之地(異常號15),這樣它可以很方便的移植到不同廠商出CM3核心的晶片上,並且對於有實時作業系統的軟體,它一般會作為整個系統的時基,這個對作業系統非常重要。有關SysTick的詳細介紹可參考《Cortex-M3 權威指南》第133 頁第八章及第179頁第十三章。

SysTick總共有四個暫存器:

1、

對應於軟體中SysTick->CTRL;

2、

對應於軟體中

 SysTick-> LOAD

3、

對應於軟體中 SysTick-> VAL

4、


對應於軟體中 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 是一個24 位的定時器,即一次最多可以計數 224個時鐘脈衝,這個脈衝計數值被儲存到SysTick->VAL 當前計數值暫存器中,它只能向下計數,每接收到一個時鐘脈衝SysTick->VAL 的值就向下減 1,直至0,然後由硬體自動把過載暫存器SysTick->LOAD 中的值到SysTick->VAL重新計數,並且當SysTick->VAL值計數到0時,觸發異常,呼叫void SysTick_Handler(void)函式,可以在此中斷服務函式中處理定時中斷事件了,般是對設定值進行遞減計數操作。只要不把它在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的時鐘源的選擇,要注意它的時鐘源可選擇內部時鐘(FCLKCM3上的自由執行時鐘,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清除了,而下個事件中又需要查詢它,這樣就可能導致處理時序的錯亂,相互干擾。

能否在事件處理中只提供查詢功能,而定時的事情就交給定時自己去做?

下節高手將登場了,為大家介紹個我曾在一專案中學到的,非阻塞延時的精妙設計。