天天看點

《嵌入式 – GD32開發實戰指南》第5章 跳動的心髒-Systick

開發環境:

MDK:Keil 5.30

MCU:GD32F207IK

Cortex-M的核心中包含Systick定時器了,隻要是Cortex-M系列的MCU就會有Systick,是以這是通用的,下面詳細分析。

5.1 Systick工作原理分析

SysTick 定時器被捆綁在 NVIC 中,用于産生 SysTick 異常(異常号 :15)。在以前,作業系統和所有使用了時基的系統都必須有一個硬體定時器來産生需要的“滴答”中斷,作為整個系統的時基。滴答中斷對作業系統尤其重要。例如,作業系統可以為多個任務配置設定不同數目的時間片,確定沒有一個任務能霸占系統 ;或者将每個定時器周期的某個時間範圍賜予特定的任務等,作業系統提供的各種定時功能都與這個滴答定時器有關。是以,需要一個定時器來産生周期性的中斷,而且最好還讓使用者程式不能随意通路它的寄存器,以維持作業系統“心跳”的節律。

《嵌入式 – GD32開發實戰指南》第5章 跳動的心髒-Systick

Cortex-M3 在核心部分包含了一個簡單的定時器——SysTick。因為所有的 CM3 晶片都帶有這個定時器,軟體在不同晶片生産廠商的 CM3 器件間的移植工作就得以簡化。該定時器的時鐘源可以是内部時鐘(FCLK,CM3 上的自由運作時鐘),或者是外部時鐘。不過,外部時鐘的具體來源則由晶片設計者決定,是以不同産品之間的時鐘頻率可能大不相同。是以,需要閱讀晶片的使用手冊來确定選擇什麼作為時鐘源。在 GD32 中SysTick 以 HCLK(AHB 時鐘)或 HCLK/8 作為運作時鐘,見上圖。

SysTick 定時器能産生中斷,CM3 為它專門開出一個異常類型,并且在向量表中有它的一席之地。它使作業系統和其他系統軟體在 CM3 器件間的移植變得簡單多了,因為在所有 CM3 産品間,SysTick 的處理方式都是相同的。SysTick 定時器除了能服務于作業系統之外,還能用于其他目的,如作為一個鬧鈴、用于測量時間等。Systick 定時器屬于Cortex 核心部件,可以參考《ARM Cortex-M3 權威指南》((英)JosephYiu 著,宋岩譯,北京航空航天大學出版社出版)來了解。

5.2 Systick寄存器分析

在傳統的嵌入式系統軟體按中通常實作 Delay(N) 函數的方法為:

for(i = 0; i <= x; i ++); 
x --- ;      

對于GD32系列微處理器來說,執行一條指令隻有幾十個 ns,進行 for 循環時,要實作 N 毫秒的 x 值非常大,而且由于系統頻率的寬廣,很難計算出延時 N 毫秒的精确值。針對GD32 微處理器,需要重新設計一個新的方法去實作該功能,以實作在程式中使用 Delay(N)。

Cortex-M3 的核心中包含一個 SysTick 時鐘。SysTick 為一個 24 位遞減計數器,SysTick 設定初值并使能後,每經過 1 個系統時鐘周期,計數值就減 1。計數到 0 時,SysTick 計數器自動重裝初值并繼續計數,同時内部的 COUNTFLAG 标志會置位,觸發中斷 (如果中斷使能情況下)。

在 GD32 的應用中,使用 Cortex-M3 核心的 SysTick 作為定時時鐘,設定每一毫秒産生一次中斷,在中斷處理函數裡對 N 減一,在Delay(N) 函數中循環檢測 N 是否為 0,不為 0 則進行循環等待;若為 0 則關閉 SysTick 時鐘,退出函數。

注: 全局變量 TimingDelay , 必須定義為 volatile 類型 , 延遲時間将不随系統時鐘頻率改變。

Cortex-M3 中的Systick 部分内容屬于NVIC控制部分,一共有4個寄存器,名稱和位址分别是:

 STK_CTRL, 0xE000E010 – 控制寄存器

《嵌入式 – GD32開發實戰指南》第5章 跳動的心髒-Systick

第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 – 重載寄存器

《嵌入式 – GD32開發實戰指南》第5章 跳動的心髒-Systick

Systick是一個遞減的定時器,當定時器遞減至0時,重載寄存器中的值就會被重裝載,繼續開始遞減。STK_LOAD 重載寄存器是個24位的寄存器最大計數0xFFFFFF。

 STK_VAL, 0xE000E018 – 目前值寄存器

《嵌入式 – GD32開發實戰指南》第5章 跳動的心髒-Systick

也是個24位的寄存器,讀取時傳回目前倒計數的值,寫它則使之清零,同時還會清除在SysTick 控制及狀态寄存器中的COUNTFLAG 标志。

 STK_CALRB, 0xE000E01C – 校準值寄存器

《嵌入式 – GD32開發實戰指南》第5章 跳動的心髒-Systick

校準值寄存器提供了這樣一個解決方案:它使系統即使在不同的CM3産品上運作,也能産生恒定的SysTick中斷頻率。最簡單的作法就是:直接把TENMS的值寫入重裝載寄存器,這樣一來,隻要沒突破系統極限,就能做到每10ms來一次 SysTick異常。如果需要其它的SysTick異常周期,則可以根據TENMS的值加以比例計算。隻不過,在少數情況下, CM3晶片可能無法準确地提供TENMS的值(如, CM3的校準輸入信号被拉低),是以為保險起見,最好在使用TENMS前檢查器件的參考手冊。

SysTick定時器除了能服務于作業系統之外,還能用于其它目的:如作為一個鬧鈴,用于測量時間等。要注意的是,當處理器在調試期間被喊停( halt)時,則SysTick定時器亦将暫停運作。

5.3 Systick定時器實作

SysTick屬于Cortex-M核心的部分,是以其相關的定義在core_cm3.h檔案中。

5.3.1 main檔案分析

主函數如下:

/* Includes*********************************************************************/
#include "gd32f2_systick.h"
#include "gd32f2_led.h"
#include "gd32f2_systick.h"

/*
    brief      main function
    param[in]  none
    param[out] none
    retval     none
*/
int main(void)
{
    //systick init
    sysTick_init();
    
    /* configure LED1 GPIO port */
    led_init(LED1);

    /* configure LED2 GPIO port */
    led_init(LED2);

    /* configure LED3 GPIO port */
    led_init(LED3);

    /* configure LED4 GPIO port */
    led_init(LED4);

    while(1) 
    {
        /* turn on LED1, turn off LED4 */
        led_on(LED1);
        led_off(LED4);
        /*delay 500ms*/
        delay_ms(500);


        /* turn on LED2, turn off LED1 */
        led_on(LED2);
        led_off(LED1);
        /*delay 500ms*/
        delay_ms(500);


        /* turn on LED3, turn off LED2 */
        led_on(LED3);
        led_off(LED2);
        /*delay 500ms*/
        delay_ms(500);


        /* turn on LED4, turn off LED3 */
        led_on(LED4);
        led_off(LED3);
        /*delay about 500ms*/
        delay_ms(500);

    }
}      

在 main 函數中,sysTick_init和 delay_us() 這兩個函數比較陌生,它們的功能分别是配置好 SysTick 定時器和進行精确延時。整個 main 函數的流程就是初始化 LED 及SysTick 定時器之後,就進入死循環,點亮LED的時間為精确的 500 ms。

5.3.2 gd32f2_systick.c檔案分析

 配置并啟動 SysTick

我們看一下 systick_config() 這個函數,其功能是啟動系統滴答定時器 SysTick。

/*
    brief      SysTick init
    param[in]  none
    param[out] none
    retval     none
*/
void sysTick_init(void)
{
     /* SystemFrequency / 1000    1ms中斷一次
      * SystemFrequency / 100000  10us中斷一次
      * SystemFrequency / 1000000 1us中斷一次
      */
    /* setup systick timer for 1000Hz interrupts */
    if(SysTick_Config(SystemCoreClock / 100000U)){
        /* capture error */
        while(1){
        }
    }

        // 關閉滴答定時器  
      SysTick->CTRL &= ~ SysTick_CTRL_ENABLE_Msk;
        
    /* configure the systick handler priority */
    NVIC_SetPriority(SysTick_IRQn, 0x00U);
}      

本函數實際上隻是調用了 SysTick_Config() 函數,它是屬于核心層的 Cortex-M3 通用函數,位于 core_cm3.h 檔案中。若調用 SysTick_Config() 配置 SysTick 不成功,則進入死循環,初始化 SysTick 成功後,先關閉定時器,在需要的時候再開啟。SysTick_Config() 函數無法在GD32 外設固件庫檔案中找到其使用方法。是以我們在 Keil 環境下直接跟蹤這個函數到 core_cm3.h 檔案,檢視函數的定義。

/** \brief  System Tick Configuration

    The function initializes the System Timer and its interrupt, and starts the System Tick Timer.
    Counter is in free running mode to generate periodic interrupts.

    \param [in]  ticks  Number of ticks between two interrupts.

    \return          0  Function succeeded.
    \return          1  Function failed.

    \note     When the variable <b>__Vendor_SysTickConfig</b> is set to 1, then the
    function <b>SysTick_Config</b> is not included. In this case, the file <b><i>device</i>.h</b>
    must contain a vendor-specific implementation of this function.

 */
__STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)
{
  if ((ticks - 1) > SysTick_LOAD_RELOAD_Msk)  return (1);      /* Reload value impossible */

  SysTick->LOAD  = ticks - 1;                                  /* set reload register */
  NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);  /* set Priority for Systick Interrupt */
  SysTick->VAL   = 0;                                          /* Load the SysTick Counter Value */
  SysTick->CTRL  = SysTick_CTRL_CLKSOURCE_Msk |
                   SysTick_CTRL_TICKINT_Msk   |
                   SysTick_CTRL_ENABLE_Msk;                    /* Enable SysTick IRQ and SysTick Timer */
  return (0);                                                  /* Function successful */
}      

在這個函數定義的前面有關于它的注釋,如果我們不想去研究它的具體實作,可以根據這段注釋了解函數的功能 :這個函數啟動了 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 << SysTick_CTRL_COUNTFLAG_Pos)            /*!< SysTick CTRL: COUNTFLAG Mask */

#define SysTick_CTRL_CLKSOURCE_Pos          2                                             /*!< SysTick CTRL: CLKSOURCE Position */
#define SysTick_CTRL_CLKSOURCE_Msk         (1ul << SysTick_CTRL_CLKSOURCE_Pos)            /*!< SysTick CTRL: CLKSOURCE Mask */

#define SysTick_CTRL_TICKINT_Pos            1                                             /*!< SysTick CTRL: TICKINT Position */
#define SysTick_CTRL_TICKINT_Msk           (1ul << SysTick_CTRL_TICKINT_Pos)              /*!< SysTick CTRL: TICKINT Mask */

#define SysTick_CTRL_ENABLE_Pos             0                                             /*!< SysTick CTRL: ENABLE Position */
#define SysTick_CTRL_ENABLE_Msk            (1ul << SysTick_CTRL_ENABLE_Pos)               /*!< SysTick CTRL: ENABLE Mask */

/* 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_VAL_CURRENT_Pos)        /*!< SysTick VAL: CURRENT Mask */

/* SysTick Calibration Register Definitions */
#define SysTick_CALIB_NOREF_Pos            31                                             /*!< SysTick CALIB: NOREF Position */
#define SysTick_CALIB_NOREF_Msk            (1ul << SysTick_CALIB_NOREF_Pos)               /*!< SysTick CALIB: NOREF Mask */

#define SysTick_CALIB_SKEW_Pos             30                                             /*!< SysTick CALIB: SKEW Position */
#define SysTick_CALIB_SKEW_Msk             (1ul << SysTick_CALIB_SKEW_Pos)                /*!< SysTick CALIB: SKEW Mask */

#define SysTick_CALIB_TENMS_Pos             0                                             /*!< SysTick CALIB: TENMS Position */
#define SysTick_CALIB_TENMS_Msk            (0xFFFFFFul << SysTick_VAL_CURRENT_Pos)        /*!< SysTick CALIB: TENMS Mask */
/*@}*/ /* 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 << SysTick_CTRL_CLKSOURCE_Pos”, 把無符号長整型數值(ul) 1 左移 2 位, 得 到 了 一 個 隻 有 Bit2 :CLKSOURCE 位被置 1,其他位為 0 的數值,這樣的數值配合位操作 &(按位與)、| (按位或)可以很友善地修改寄存器的某些位。假如控制 CLKSOURCE 需 要 4 個寄存器位,這個宏就應該被改為( 0xf ul <<SysTick_CTRL_CLKSOURCE_Pos),這樣就會得到一個關于 CLKSOU RCE 的 4位被置 1 的值,這些宏的參數就是這樣被确定的。寄存器位訓示宏和位屏蔽宏在操作寄存器的代碼(大部分庫函數)中用得十分廣泛,在前面 GPIO_Init() 函數分析時也遇到很多,為了友善以後再使用,我們就給這兩類宏取了這兩個名字。

 配置中斷向量及重置 STK_VAL 寄存器

回到 SysTick_Config() 函數,接下來調用了 NVIC_SetPriority () 函數并配置了 SysTick中斷,如果想修改SysTick的優先級,也可以在外部使用 NVIC 配置 SysTick 中斷。配置好SysTick 中斷後把 STK_VAL 寄存器重新指派為 0(在使能 SysTick 時,硬體會把存儲在STK_LOAD 寄存器 中的 ticks 值加載給它)。

 配置 SysTick 時鐘為 AHB

在這段代碼最後,向 STK_CTRL 寄存器寫入了 SysTick 的控制參數,配置為使用AHB 時鐘,使能計數至 0 時引起中斷,使能 SysTick。執行了這行代碼,SysTick 就開始運作并進行脈沖計數了。

若想要使用 AHB/8 作為時鐘,可以直接在SysTick_Config()函數中對SysTick->CTRL進行修改,當然最好自定義sysTick_init()函數中修改。

 使能、關閉定時器

由于調用 SysTick_Config() 函數之後,SysTick 定時器就被開啟了,但我們在初始化的時候并不希望這樣,而是根據需要再開啟。是以在 sysTick_init() 函數中,調用完SysTick_Config() 并配置好後,應先把定時器關閉了。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 都是被配置為 120MHz 的,也就是這個 SystemCoreClock 宏展開為數值 12000 0000。

根據前面對 SysTick_Config() 函數的介紹,它的輸入參數為 SysTick 将要計時的脈沖數,經過 ticks 個脈沖(經過 ticks 個時鐘周期)後将觸發中斷,觸發中斷後又重新開始計數。由此我們可以算出定時的時間,下面為計算公式 :

T=ticks×(1/f)

其中,T 為要定時的總時間 ;ticks 為 SysTick_Config() 的輸入參數 ;1/ f 即為SysTick 使用的時鐘源的時鐘周期,f 為該時鐘源的時鐘頻率,當時鐘源确定後為常數。

本例中使用時鐘源為 AHB 時鐘,其頻率被配置為 120 MHz。調用函數時,把 ticks 指派為 ticks=SystemFrequency / 100000 =1200,表示 1200 個時鐘周期中斷一次 ;1/f 是時鐘周期的時間,此時1/f =1/120 us,是以最終定時總時間 T=1200x(1/120),為1200 個時鐘周期,正好是 10us。

SysTick 定時器的定時時間(配置為觸發中斷,即為中斷周期)由 ticks 參數決定,最大定時周期不能超過 224 個。

 編寫中斷服務函數

一旦我們調用了 delay_us() 函數,SysTick 定時器就被開啟,按照設定好的定時周期遞減計數,當 SysTick 的計數寄存器的值減為 0 時,就進入中斷函數,當中斷函數執行完畢之後重新計時,如此循環,除非它被關閉。

/*
    brief      delay a time
    param[in]  count: count
    param[out] none
    retval     none
*/
void delay_us(uint32_t count)
{
    delay = count;

     // 使能滴答定時器  
     SysTick->CTRL |=  SysTick_CTRL_ENABLE_Msk;
    
    while(0U != delay){
    }
}      

使能了 SysTick 之後,就使用while(0U != delay)語句等待 delay 變量變為 0,這個變量是在中斷服務函數中被修改的。是以,我們需要編寫相應的中斷服務程式,在本實驗室中我們配置為 10us 中斷一次,每次中斷把 delay 減 1。中斷程式在 gd32f10x_it.c 中實作。

void SysTick_Handler(void)
{
  delay_decrement (); 
}      

SysTick中斷屬于系統異常向量,在gd32f10x_it.c檔案中已經預設有了它的中斷服務函數SysTick_Handler(),但内容為空。我們找到這個函數,其調用了使用者函數delay_decrement()。後者是由使用者編寫的一個應用程式。

/*
    brief      delay decrement
    param[in]  none
    param[out] none
    retval     none
*/
void delay_decrement(void)
{
    if(0U != delay){
        delay--;
    }
}      

每次進入 SysTick 中斷就調用一次 delay_decrement()函數,使全局變量delay 自減一次。使用者函數 delay_us ()在delay 被減至0時,才退出延時循環,即我們對 delay 賦的值為要中斷的次數。是以總的延時時間 :

T 延時 = T 中斷周期 x delay

至此,SysTick 的精确延時功能講解完畢。

5.4實驗現象