【TencentOS tiny】深度源碼分析(8)——軟件定時器

軟件定時器的基本概念

TencentOS tiny 的軟件定時器是由操做系統提供的一類系統接口,它構建在硬件定時器基礎之上,使系統可以提供不受硬件定時器資源限制的定時器服務,本質上軟件定時器的使用至關於擴展了定時器的數量,容許建立更多的定時業務,它實現的功能與硬件定時器也是相似的。數據結構

硬件定時器是芯片自己提供的定時功能。通常是由外部晶振提供給芯片輸入時鐘,芯片向軟件模塊提供一組配置寄存器,接受控制輸入,到達設定時間值後芯片中斷控制器產生時鐘中斷。硬件定時器的精度通常很高,能夠達到納秒級別,而且是中斷觸發方式。app

軟件定時器的超時處理是指:在定時時間到達以後就會自動觸發一個超時,而後系統跳轉到對應的函數去處理這個超時,此時,調用的函數也被稱回調函數函數

回調函數的執行環境能夠是中斷,也能夠是任務,這就須要你本身在tos_config.h經過TOS_CFG_TIMER_AS_PROC宏定義選擇回調函數的執行環境了。測試

  • TOS_CFG_TIMER_AS_PROC 爲 1 :回調函數的執行環境是中斷
  • TOS_CFG_TIMER_AS_PROC 爲 0 :回調函數的執行環境是任務

這與硬件定時器的中斷服務函數很相似,不管是在中斷中仍是在任務中,回調函數的處理儘量簡短,快進快出this

軟件定時器在被建立以後,當通過設定的超時時間後會觸發回調函數,定時精度與系統時鐘的週期有關,通常能夠採用SysTick做爲軟件定時器的時基(在m核單片機中幾乎都是採用SysTick做爲系統時基,而軟件定時器又是基於系統時基之上)。spa

TencentOS tiny提供的軟件定時器支持單次模式和週期模式,單次模式和週期模式的定時時間到以後都會調用軟件定時器的回調函數。操作系統

  • 單次模式:當用戶建立了定時器並啓動了定時器後,指定超時時間到達,只執行一次回調函數以後就將該定時器中止,再也不從新執行。
  • 週期模式:這個定時器會按照指定的定時時間循環執行回調函數,直到將定時器刪除。

在不少應用中,可能須要一些定時器任務,硬件定時器受硬件的限制,數量上不足以知足用戶的實際需求,沒法提供更多的定時器,能夠採用軟件定時器,由軟件定時器代替硬件定時器任務。但須要注意的是軟件定時器的精度是沒法和硬件定時器相比的,由於在軟件定時器的定時過程當中是極有可能被其餘中斷打斷,所以軟件定時器更適用於對時間精度要求不高的任務。指針

軟件定時器以tick爲基本計時單位,當用戶建立並啓動一個軟件定時器時, TencentOS tiny會根據當前系統tick與用戶指定的超時時間計算出該定時器超時的時間expires,並將該定時器插入軟件定時器列表。code

軟件定時器的數據結構

如下軟件定時器的相關數據結構都在tos_global.c中定義cdn

軟件定時器列表

軟件定時器列表用於記錄系統中全部的軟件定時器,這些軟件定時器將按照喚醒時間升序插入軟件定時器列表k_timer_ctl.list 中,它的數據類型是timer_ctl_t

timer_ctl_t         k_timer_ctl = { TOS_TIME_FOREVER, TOS_LIST_NODE(k_timer_ctl.list) };

typedef struct timer_control_st {
    k_tick_t    next_expires;
    k_list_t    list;
} timer_ctl_t;複製代碼
  • next_expires:記錄下一個到期的軟件定時器時間。
  • list:軟件定時器列表,全部的軟件定時器都會被掛載到這個列表中。

軟件定時器任務相關的數據結構

若是 TOS_CFG_TIMER_AS_PROC 宏定義爲0,則表示使用軟件定時器任務處理軟件定時器的回調函數,那麼此時軟件定時器的回調函數執行環境爲任務;反之軟件定時器回調函數的處理將在中斷上下文環境中。

k_task_t            k_timer_task;
k_stack_t           k_timer_task_stk[TOS_CFG_TIMER_TASK_STK_SIZE];
k_prio_t            const k_timer_task_prio         = TOS_CFG_TIMER_TASK_PRIO;
k_stack_t          *const k_timer_task_stk_addr     = &k_timer_task_stk[0];
size_t              const k_timer_task_stk_size     = TOS_CFG_TIMER_TASK_STK_SIZE;複製代碼
  • k_timer_task:軟件定時器任務控制塊
  • k_timer_task_stk:軟件定時器任務棧,其大小爲TOS_CFG_TIMER_TASK_STK_SIZE
  • k_timer_task_prio:軟件定時器任務優先級,值爲TOS_CFG_TIMER_TASK_PRIO,默認值是 (k_prio_t)(K_TASK_PRIO_IDLE - (k_prio_t)1u),比空閒任務高1個數值優先級,傑傑認爲這也是很低的優先級了,這樣一來軟件定時器的精度將更低,不過好在這個值是能夠被用戶自定義的,想讓精度高一點就將這個軟件定時器任務優先級設置得高一點就好。
  • ktimertaskstkaddr:軟件定時器任務棧起始地址。
  • ktimertaskstksize:軟件定時器任務棧大小。

    如下軟件定時器的相關數據結構都在tos_timer.h中定義

軟件定時器的回調函數

// 軟件定時器的回調函數類型
typedef void (*k_timer_callback_t)(void *arg);複製代碼

軟件定時器的回調函數是一個函數指針的形式,它支持傳入一個void指針類型的數據。

軟件定時器控制塊

每一個軟件定時器都有對應的軟件定時器控制塊,每一個軟件定時器控制塊都包含了軟件定時器的基本信息,如軟件定時器的狀態、軟件定時器工做模式、軟件定時器的週期,剩餘時間,以及軟件定時器回調函數等信息。

typedef struct k_timer_st {
#if TOS_CFG_OBJECT_VERIFY_EN > 0u
    knl_obj_t               knl_obj;    /**< 僅爲了驗證,測試當前內核對象是否真的是一個軟件定時器 */
#endif

    k_timer_callback_t      cb;         /**< 時間到時回調函數 */
    void                   *cb_arg;     /**< 回調函數中傳入的參數 */
    k_list_t                list;       /**< 掛載到軟件定時器列表的節點 */
    k_tick_t                expires;    /**< 距離軟件定時器的到期時間到期還剩多少時間(單位爲tick) */
    k_tick_t                delay;      /**< 還剩多少時間運行第一個到期的軟件定時器(的回調函數) */
    k_tick_t                period;     /**< 軟件定時器的週期 */
    k_opt_t                 opt;        /**< 軟件定時器的工做模式選項,能夠是單次模式TOS_OPT_TIMER_ONESHOT,也能夠是週期模式TOS_OPT_TIMER_PERIODIC */
    timer_state_t           state;      /**< 軟件定時器的狀態 */
} k_timer_t;複製代碼

軟件定時器的工做模式

// if we just want the timer to run only once, this option should be passed to tos_timer_create.
#define TOS_OPT_TIMER_ONESHOT                   (k_opt_t)(0x0001u)

// if we want the timer run periodically, this option should be passed to tos_timer_create.
#define TOS_OPT_TIMER_PERIODIC                  (k_opt_t)(0x0002u)複製代碼
  • TOS_OPT_TIMER_ONESHOT單次模式,軟件定時器在超時後,只會執行一次回調函數,它的狀態將被設置爲TIMER_STATE_COMPLETED,再也不從新執行它的回調函數,固然,用戶仍是能夠從新啓動這個單次模式的軟件定時器,它並未被刪除。

若是隻但願計時器運行一次,則應將此選項傳遞給tos_timer_create()

  • TOSOPTTIMER_PERIODIC :**週期**模式 ,軟件定時器在超時後,會執行對應的回調函數,同時根據軟件定時器控制塊中的`period`成員變量的值再從新插入軟件定時器列表中,這個定時器會按照指定的定時時間循環執行(週期性執行)回調函數,直到用戶將定時器刪除。

若是咱們但願計時器週期運行,則應將此選項傳遞給tos_timer_create()

軟件定時器的狀態

定時器狀態有如下幾種:

typedef enum timer_state_en {
    TIMER_STATE_UNUSED,     /**< the timer has been destroyed */
    TIMER_STATE_STOPPED,    /**< the timer has been created but not been started, or just be stopped(tos_timer_stop) */
    TIMER_STATE_RUNNING,    /**< the timer has been created and been started */
    TIMER_STATE_COMPLETED   /**< the timer has finished its expires, it can only happen when the timer's opt is TOS_OPT_TIMER_ONESHOT */
} timer_state_t;複製代碼
  • TIMER_STATE_UNUSED:未使用狀態。
  • TIMER_STATE_STOPPED建立了軟件定時器,但此時軟件定時器未啓動或者處於中止狀態,調用tos_timer_create()函數接口或者在軟件定時器啓動後調用tos_timer_stop()函數接口後,定時器將變成該狀態。
  • TIMER_STATE_RUNNING:軟件定時器處於運行狀態,在定時器被建立後調用tos_timer_start()函數接口,定時器將變成該狀態,表示定時器運行時的狀態。
  • TIMER_STATE_COMPLETED:軟件定時器已到期,只有在軟件定時器的模式選擇爲TOS_OPT_TIMER_ONESHOT時纔可能發生,表示軟件定時器已經完成了。

建立軟件定時器

函數

__API__ k_err_t tos_timer_create(k_timer_t *tmr,
                                 k_tick_t delay,
                                 k_tick_t period,
                                 k_timer_callback_t callback,
                                 void *cb_arg,
                                 k_opt_t opt);複製代碼

參數

參數 說明(傑傑)
tmr
軟件定時器控制塊指針
delay
軟件定時器第一次運行的延遲時間間隔
period 軟件定時器的週期
callback 軟件定時器的回調函數,在超時時調用(由用戶本身定義)
cb_arg 用於回調函數傳入的形參(void指針類型)
opt 軟件定時器的工做模式(單次 / 週期)

傑傑以爲 delayperiod 比較有意思,就簡單提一下 delay 參數與 period 參數的意義與區別:

- delay參數實際上是第一次運行的延遲時間間隔(即第一次調用回調函數的時間),若是軟件定時器是單次模式,那麼只用 delay 參數做爲軟件定時器的回調時間,由於軟件定時器是單次工做模式的話,只會運行一次回調函數,那麼就沒有周期一說(period 參數將不起做用),只能是以第一次運行的延遲時間間隔做爲它的回調時間。

- period 參數則是做爲軟件定時器的週期性回調的時間間隔,就比如你的鬧鐘,天天 7 點叫你起牀,可是delay參數在週期工做模式下的軟件定時器也是有做用的,它是對第一次回調函數的延遲時間,舉個例子:今天晚上9點的時候,你設置了一個鬧鐘,鬧鐘時間是天天早上7點的,那麼在10個小時後,鬧鐘將叫你起牀,那麼這10個小時就至關於delay參數的值,由於鬧鐘第一次叫你起牀並非在24小時後,而在明天7點後,鬧鐘響了,此時鬧鐘將在一天後纔會再響,這24小時則至關於 period 參數的值。

系統中每一個軟件定時器都有對應的軟件定時器控制塊,軟件定時器控制塊中包含了軟件定時器的全部信息,那麼能夠想象一下,建立軟件定時器的本質是否是就是對軟件定時器控制塊進行初始化呢?很顯然就是這樣子的。由於在後續對軟件定時器的操做都是經過軟件定時器控制塊來操做的,若是控制塊沒有信息,那怎麼能操做嘛~

步驟以下:

  1. 判斷傳入的參數是否正確:軟件定時器控制塊不爲null,回調函數不爲null,若是是建立週期模式的軟件定時器,那麼 period 參數則不能夠爲0,而若是是單次模式的軟件定時器,參數delay則不能夠爲0,不管是何種模式的軟件定時器,delay 參數與 period 參數都不能夠爲K_ERR_TIMER_PERIOD_FOREVER,由於這表明着軟件定時器不須要運行,那還建立個錘子啊。
  2. 根據傳入的參數將軟件定時器控制塊的成員變量賦初值,軟件定時器狀態state被設置爲TIMER_STATE_STOPPEDexpires 則被設置爲0,由於還還沒有啓動軟件定時器。
  3. 調用tos_list_init()函數將軟件定時器控制塊中可掛載到k_tick_list列表的節點初始化。
__API__ k_err_t tos_timer_create(k_timer_t *tmr,
                                 k_tick_t delay,
                                 k_tick_t period,
                                 k_timer_callback_t callback,
                                 void *cb_arg,
                                 k_opt_t opt)
{
    TOS_PTR_SANITY_CHECK(tmr);
    TOS_PTR_SANITY_CHECK(callback);

    if (opt == TOS_OPT_TIMER_PERIODIC && period == (k_tick_t)0u) {
        return K_ERR_TIMER_INVALID_PERIOD;
    }

    if (opt == TOS_OPT_TIMER_ONESHOT && delay == (k_tick_t)0u) {
        return K_ERR_TIMER_INVALID_DELAY;
    }

    if (opt != TOS_OPT_TIMER_ONESHOT && opt != TOS_OPT_TIMER_PERIODIC) {
        return K_ERR_TIMER_INVALID_OPT;
    }

    if (delay == TOS_TIME_FOREVER) {
        return K_ERR_TIMER_DELAY_FOREVER;
    }

    if (period == TOS_TIME_FOREVER) {
        return K_ERR_TIMER_PERIOD_FOREVER;
    }

#if TOS_CFG_OBJECT_VERIFY_EN > 0u
    knl_object_init(&tmr->knl_obj, KNL_OBJ_TYPE_TIMER);
#endif

    tmr->state          = TIMER_STATE_STOPPED;
    tmr->delay          = delay;
    tmr->expires        = (k_tick_t)0u;
    tmr->period         = period;
    tmr->opt            = opt;
    tmr->cb             = callback;
    tmr->cb_arg         = cb_arg;
    tos_list_init(&tmr->list);
    return K_ERR_NONE;
}複製代碼

銷燬軟件定時器

軟件定時器銷燬函數是根據軟件定時器控制塊直接銷燬的,銷燬以後軟件定時器的全部信息都會被清除,並且不能再次使用這個軟件定時器,若是軟件定時器處於運行狀態,那麼就須要將被銷燬的軟件定時器中止,而後再進行銷燬操做。其過程以下:

  1. 判斷軟件定時器是否有效,而後根據軟件定時器狀態判斷軟件定時器是否建立,若是是未使用狀態TIMER_STATE_UNUSED,則直接返回錯誤代碼K_ERR_TIMER_INACTIVE
  2. 若是軟件定時器狀態是 運行狀態 TIMER_STATE_RUNNING,那麼調用timer_takeoff()函數將軟件定時器中止。
  3. 最後調用timer_reset()函數將軟件定時器控制塊的內容重置,主要是將軟件定時器的狀態設置爲未使用狀態TIMER_STATE_UNUSED,將對應的回調函數設置爲null
__API__ k_err_t tos_timer_destroy(k_timer_t *tmr)
{
    TOS_PTR_SANITY_CHECK(tmr);

#if TOS_CFG_OBJECT_VERIFY_EN > 0u
    if (!knl_object_verify(&tmr->knl_obj, KNL_OBJ_TYPE_TIMER)) {
        return K_ERR_OBJ_INVALID;
    }
#endif

    if (tmr->state == TIMER_STATE_UNUSED) {
        return K_ERR_TIMER_INACTIVE;
    }

    if (tmr->state == TIMER_STATE_RUNNING) {
        timer_takeoff(tmr);
    }

    timer_reset(tmr);
    return K_ERR_NONE;
}複製代碼

中止軟件定時器(內部函數)

在銷燬軟件定時器的時候提到了timer_takeoff()函數,那麼就來看看這個函數具體是怎麼樣中止軟件定時器的,其實本質上就是將軟件定時器從軟件定時器列表中移除。

注意,這個函數是內部靜態函數,不是給用戶使用的,它與tos_timer_stop()不一樣。

  1. 首先經過TOS_LIST_FIRST_ENTRY宏定義將軟件定時器列表k_timer_ctl.list中的第一個軟件定時器取出,由於防止軟件定時器列表中的第一個軟件定時器被移除了,而沒有重置軟件定時器列表中的相關的信息,所以此時要記錄一下第一個軟件定時器。
  2. 調用tos_list_del()將軟件定時器從軟件定時器列表中移除,表示中國軟件定時器就被中止了,由於不知軟件定時器列表中,中國軟件定時器也就不會被處理。
  3. 判斷一下移除的軟件定時器是否是第一個軟件定時器,若是是,則重置相關信息。若是軟件定時器列表中不存在其餘軟件定時器,則將軟件定時器列表的下一個到期時間設置爲TOS_TIME_FOREVER,反正則讓軟件定時器列表的下一個到期時間爲第二個軟件定時器。
__STATIC__ void timer_takeoff(k_timer_t *tmr)
{
    TOS_CPU_CPSR_ALLOC();
    k_timer_t *first, *next;

    TOS_CPU_INT_DISABLE();

    first = TOS_LIST_FIRST_ENTRY(&k_timer_ctl.list, k_timer_t, list);

    tos_list_del(&tmr->list);

    if (first == tmr) {
        // if the first guy removed, we need to refresh k_timer_ctl.next_expires
        next = TOS_LIST_FIRST_ENTRY_OR_NULL(&tmr->list, k_timer_t, list);
        if (!next) {
            // the only guy removed
            k_timer_ctl.next_expires = TOS_TIME_FOREVER;
        } else {
            k_timer_ctl.next_expires = next->expires;
        }
    }

    TOS_CPU_INT_ENABLE();
}複製代碼

啓動軟件定時器

在建立成功軟件定時器的時候,軟件定時器的狀態從TIMER_STATE_UNUSED(未使用狀態)變成TIMER_STATE_STOPPED(建立但未啓動 / 中止狀態),建立完成的軟件定時器是未運行的,用戶在須要的時候能夠啓動它,TencentOS tiny提供了軟件定時器啓動函數tos_timer_start()。啓動軟件定時器的本質就是將軟件定時器插入軟件定時器列表k_timer_ctl.list 中,既然是這樣子,那麼很顯然須要根據軟件定時器的不一樣狀態進行不一樣的處理。

其實現過程以下:判斷軟件定時器控制塊是否爲null,而後判斷軟件定時器狀態,若是爲未使用狀態TIMER_STATE_UNUSED則直接返回錯誤代碼K_ERR_TIMER_INACTIVE;若是爲已經運行狀態TIMER_STATE_RUNNING,那麼將軟件定時器中止,然從新插入軟件定時器列表k_timer_ctl.list中;若是是TIMER_STATE_STOPPED 或者TIMER_STATE_COMPLETED狀態,則將軟件定時器的狀態從新設置爲運行狀態TIMER_STATE_RUNNING,而且插入軟件定時器列表k_timer_ctl.list中。

注意:插入軟件定時器列表的函數是timer_place()

tos_timer_start()函數將軟件定時器控制塊的period或者delay成員變量的值賦值給expires,但這個值是相對的到期時間,而不是絕對值,所以在timer_place()函數中將從新計算得出絕對的到期時間。

__API__ k_err_t tos_timer_start(k_timer_t *tmr)
{
    TOS_PTR_SANITY_CHECK(tmr);

#if TOS_CFG_OBJECT_VERIFY_EN > 0u
    if (!knl_object_verify(&tmr->knl_obj, KNL_OBJ_TYPE_TIMER)) {
        return K_ERR_OBJ_INVALID;
    }
#endif

    if (tmr->state == TIMER_STATE_UNUSED) {
        return K_ERR_TIMER_INACTIVE;
    }

    if (tmr->state == TIMER_STATE_RUNNING) {
        timer_takeoff(tmr);
        tmr->expires = tmr->delay;
        timer_place(tmr);
        return K_ERR_NONE;
    }

    if (tmr->state == TIMER_STATE_STOPPED ||
        tmr->state == TIMER_STATE_COMPLETED) {
        tmr->state = TIMER_STATE_RUNNING;
        if (tmr->delay == (k_tick_t)0u) {
            tmr->expires = tmr->period;
        } else {
            tmr->expires = tmr->delay;
        }
        timer_place(tmr);
        return K_ERR_NONE;
    }
    return K_ERR_TIMER_INVALID_STATE;
}複製代碼

插入軟件定時器列表

插入軟件定時器列表的函數是timer_place(),這個函數會根據軟件定時器的到期時間升序排序,而後再插入。

該函數是一個內部實現的靜態函數

實現過程以下:

  1. 根據軟件定時器的到期時間expires(相對值) 與系統當前時間k_tick_count計算得出到期時間expires(絕對值)。

    舉個例子,鬧鐘將在10分鐘後叫我起牀(這是一個相對值)。鬧鐘將在當前時間(7:00)的10分鐘後叫我起牀,那麼鬧鐘響的時間是7:10分,此時的時間就是絕對值

  1. 經過for循環TOS_LIST_FOR_EACH找到合適的位置插入軟件定時器列表,此時插入軟件定時器列表安裝到期時間升序插入。
  2. 找到合適的位置後,調用tos_list_add_tail()函數將軟件定時器插入軟件定時器列表。
  3. 若是插入的軟件定時器是惟必定時器列表中的第一個,那麼相應的,下一個到期時間就是這個軟件定時器的到期時間,將到期時間更新: k_timer_ctl.next_expires = tmr->expires。若是TOS_CFG_TIMER_AS_PROC 宏定義爲0,則判斷一下軟件定時器任務是否處於睡眠狀態,若是是則調用tos_task_delay_abort()函數恢復軟件定時器任務運行,以便於更新它休眠的時間,由於此時是須要更新軟件定時器任務睡眠的時間的,畢竟第一個軟件定時器到期時間已經改變了。
  4. 若是軟件定時器任務處於掛起狀態,表示並無軟件定時器在工做,如今插入了軟件定時器,須要調用tos_task_resume()函數將軟件定時器任務喚醒。

關於喚醒軟件定時器任務是爲了什麼,咱們在後續講解

__STATIC__ void timer_place(k_timer_t *tmr)
{
    TOS_CPU_CPSR_ALLOC();
    k_list_t *curr;
    k_timer_t *iter = K_NULL;

    TOS_CPU_INT_DISABLE();

    tmr->expires += k_tick_count;

    TOS_LIST_FOR_EACH(curr, &k_timer_ctl.list) {
        iter = TOS_LIST_ENTRY(curr, k_timer_t, list);
        if (tmr->expires < iter->expires) {
            break;
        }
    }
    tos_list_add_tail(&tmr->list, curr);

    if (k_timer_ctl.list.next == &tmr->list) {
        // we are the first guy now
        k_timer_ctl.next_expires = tmr->expires;

#if TOS_CFG_TIMER_AS_PROC == 0u
        if (task_state_is_sleeping(&k_timer_task)) {
            tos_task_delay_abort(&k_timer_task);
        }
#endif
    }

#if TOS_CFG_TIMER_AS_PROC == 0u
    if (task_state_is_suspended(&k_timer_task)) {
        tos_task_resume(&k_timer_task);
    }
#endif

    TOS_CPU_INT_ENABLE();
}複製代碼

中止軟件定時器(外部函數)

在前文也說起中止軟件定時器,可是那個timer_takeoff()函數是內部函數,而tos_timer_stop()函數是外部函數,能夠被用戶使用。

中止軟件定時器的本質也是調用timer_takeoff()函數將軟件定時器從軟件定時器列表中移除,可是在調用這個函數以前還好作一些相關的判斷,這樣能保證系統的穩定性。

  1. 對軟件定時器控制塊檢測,若是軟件定時器控制塊爲null,則直接返回錯誤代碼。
  2. 若是軟件定時器狀態爲未使用狀態TIMER_STATE_UNUSED,則直接返回錯誤代碼K_ERR_TIMER_INACTIVE
  3. 若是軟件定時器狀態爲TIMER_STATE_COMPLETED 或者是TIMER_STATE_STOPPED,則不須要中止軟件定時器,由於這個軟件定時器是未啓動的。則直接返回錯誤代碼K_ERR_TIMER_STOPPED
  4. 若是軟件定時器狀態爲TIMER_STATE_RUNNING ,就將軟件定時器狀態設置爲中止狀態TIMER_STATE_STOPPED,而且調用timer_takeoff()函數將軟件定時器從軟件定時器列表中移除。
__API__ k_err_t tos_timer_stop(k_timer_t *tmr)
{
    TOS_PTR_SANITY_CHECK(tmr);

#if TOS_CFG_OBJECT_VERIFY_EN > 0u
    if (!knl_object_verify(&tmr->knl_obj, KNL_OBJ_TYPE_TIMER)) {
        return K_ERR_OBJ_INVALID;
    }
#endif

    if (tmr->state == TIMER_STATE_UNUSED) {
        return K_ERR_TIMER_INACTIVE;
    }

    if (tmr->state == TIMER_STATE_COMPLETED ||
        tmr->state == TIMER_STATE_STOPPED) {
        return K_ERR_TIMER_STOPPED;
    }

    if (tmr->state == TIMER_STATE_RUNNING) {
        tmr->state = TIMER_STATE_STOPPED;
        timer_takeoff(tmr);
    }

    return K_ERR_NONE;
}複製代碼

軟件定時器的處理(在中斷上下文環境)

咱們知道,TencentOS tiny的軟件定時器是能夠在中斷上下文環境來處理回調函數的,所以當軟件定時器到期後,會在tos_tick_handler()函數中調用timer_update()來處理軟件定時器。這個函數在每次tick中斷到來的時候都會判斷一下是否有軟件定時器到期,若是有則去處理它。過程以下:

  1. 判斷軟件定時器的下一個到期時間k_timer_ctl.next_expires是否小於k_tick_count,若是是小於則表示還未到期,直接退出。
  2. 反之則表示到期,此時要遍歷軟件定時器列表,找到全部到期的軟件定時器,並處理他們。

    由於有可能不僅是一個軟件定時器到期,極可能有多個定時器到期。固然啦,當軟件定時器沒到期的時候就會退出遍歷。

  1. 到期後的處理就是:調用timer_takeoff()函數將到期的軟件定時器中止,若是是週期工做的定時器就調用timer_place()函數將它從新插入軟件定時器列表中(它到期的相對時間就是軟件定時器的週期值:tmr->expires = tmr->period);若是是單次工做模式的軟件定時器,就僅將軟件定時器狀態設置爲TIMER_STATE_COMPLETED
  2. 調用軟件定時器的回調函數處理相關的工做:(*tmr->cb)(tmr->cb_arg)
__KERNEL__ void timer_update(void)
{
    k_timer_t *tmr;
    k_list_t *curr, *next;

    if (k_timer_ctl.next_expires < k_tick_count) {
        return;
    }

    tos_knl_sched_lock();

    TOS_LIST_FOR_EACH_SAFE(curr, next, &k_timer_ctl.list) {
        tmr = TOS_LIST_ENTRY(curr, k_timer_t, list);
        if (tmr->expires > k_tick_count) {
            break;
        }

        // time's up
        timer_takeoff(tmr);

        if (tmr->opt == TOS_OPT_TIMER_PERIODIC) {
            tmr->expires = tmr->period;
            timer_place(tmr);
        } else {
            tmr->state = TIMER_STATE_COMPLETED;
        }

        (*tmr->cb)(tmr->cb_arg);
    }

    tos_knl_sched_unlock();
}複製代碼

軟件定時器的處理(在任務上下文環境)

關於使用軟件定時器任務處理回調函數(即回調函數執行的上下文環境是任務),則必須打開TOS_CFG_TIMER_AS_PROC 宏定義。

建立軟件定時器任務

既然是軟件定時器任務,那麼就必須建立軟件定時器任務,那麼這個任務將在timer_init()函數中被建立,它是一個內核調用的函數,在內核初始化時就被調用(在tos_knl_init()函數中調用)。

建立軟件定時器任務也是跟建立其餘任務沒啥差異,都是經過tos_task_create()函數建立,軟件定時器任務控制塊、任務主體、優先級、任務棧起始地址與大小等都在前面的數據結構中指定了,任務的名字爲"timer"。

__KERNEL__ k_err_t timer_init(void)
{
#if TOS_CFG_TIMER_AS_PROC > 0u
    return K_ERR_NONE;
#else
    return tos_task_create(&k_timer_task,
                            "timer",
                            timer_task_entry,
                            K_NULL,
                            k_timer_task_prio,
                            k_timer_task_stk_addr,
                            k_timer_task_stk_size,
                            0);
#endif
}複製代碼

軟件定時器任務主體

軟件定時器任務的主體也是一個while (K_TRUE)循環,在循環中處理對應的事情。

  1. 調用timer_next_expires_get()函數獲取軟件定時器列表中的下一個到期時間,而且更新next_expires 的值。

    注意:這裏的時間已經在函數內部轉換爲相對到期時間,好比10分鐘後鬧鐘叫我起牀,而不是7:10分鬧鐘叫我起牀)

  2. 根據next_expires的值,判斷一下軟件定時器任務應該休眠多久,在多久後到期時才喚醒軟件定時器任務而且處理回調函數。也就是說,軟件定時器任務在軟件定時器沒有到期的時候是不會被喚醒的,都是處於休眠狀態,調用tos_task_delay()函數將任務進入休眠狀態,此時任務會被掛載到系統的延時(時基)列表中。

    注意:若是next_expires的值爲TOS_TIME_FOREVER,則不是休眠而是直接掛起,由於掛起狀態的任務對調度器而言是不可見的,這樣子的處理效率更高~掛起任務的函數是tos_task_suspend()

  1. 任務若是被喚醒了,或者被恢復運行了,則代表軟件定時器到期了或者有新的軟件定時器插入列表了,那麼在喚醒以後就要判斷一下是哪一種狀況,若是是到期了則處理對應的回調函數:首先調用timer_takeoff()函數將到期的軟件定時器中止,若是是週期工做的定時器就調用timer_place()函數將它從新插入軟件定時器列表中(它到期的相對時間就是軟件定時器的週期值:tmr->expires = tmr->period);若是是單次工做模式的軟件定時器,就僅將軟件定時器狀態設置爲TIMER_STATE_COMPLETED。(這裏也是會遍歷軟件定時器列表以處理全部到期的軟件定時器)
  2. 最後將調用軟件定時器的回調函數處理相關的工做:(*tmr->cb)(tmr->cb_arg)
  3. 若是定時器還未到期,而且軟件定時器任務被喚醒了,那麼就表示有新的軟件定時器插入列表了,此時要更新一下任務的睡眠時間,由於軟件定時器任務主體是一個while循環,仍是會回到 timer_next_expires_get()函數中從新獲取下一個喚醒任務的時間的。

注意:軟件定時器的處理都是在鎖調度器中處理的,就是爲了不其餘任務打擾回調函數的執行。

__STATIC__ void timer_task_entry(void *arg)
{
    k_timer_t *tmr;
    k_list_t *curr, *next;
    k_tick_t next_expires;

    arg = arg; // make compiler happy
    while (K_TRUE) {
        next_expires = timer_next_expires_get();
        if (next_expires == TOS_TIME_FOREVER) {
            tos_task_suspend(K_NULL);
        } else if (next_expires > (k_tick_t)0u) {
            tos_task_delay(next_expires);
        }

        tos_knl_sched_lock();

        TOS_LIST_FOR_EACH_SAFE(curr, next, &k_timer_ctl.list) {
            tmr = TOS_LIST_ENTRY(curr, k_timer_t, list);
            if (tmr->expires > k_tick_count) { // not yet
                break;
            }

            // time's up
            timer_takeoff(tmr);

            if (tmr->opt == TOS_OPT_TIMER_PERIODIC) {
                tmr->expires = tmr->period;
                timer_place(tmr);
            } else {
                tmr->state = TIMER_STATE_COMPLETED;
            }

            (*tmr->cb)(tmr->cb_arg);
        }

        tos_knl_sched_unlock();
    }
}複製代碼

獲取軟件定時器下一個到期時間

timer_next_expires_get()就是用於獲取軟件定時器下一個到期時間,若是軟件定時器到期時間是TOS_TIME_FOREVER,就返回TOS_TIME_FOREVER,若是下一個到期時間小於k_tick_count則直接返回0,表示已經到期了,能夠直接處理它,而若是是其餘值,則須要減去k_tick_count,將其轉變爲相對值,由於調用這個函數就是爲了知道任務能休眠多少時間。

打個比方,我7點醒來了,而7:10分的鬧鐘纔會響,那麼我就能再睡10分鐘,就是這個道理。

__KERNEL__ k_tick_t timer_next_expires_get(void)
{
    TOS_CPU_CPSR_ALLOC();
    k_tick_t next_expires;

    TOS_CPU_INT_DISABLE();

    if (k_timer_ctl.next_expires == TOS_TIME_FOREVER) {
        next_expires = TOS_TIME_FOREVER;
    } else if (k_timer_ctl.next_expires <= k_tick_count) {
        next_expires = (k_tick_t)0u;
    } else {
        next_expires = k_timer_ctl.next_expires - k_tick_count;
    }

    TOS_CPU_INT_ENABLE();
    return next_expires;
}複製代碼

喜歡就關注我吧!

歡迎關注我公衆號

相關代碼能夠在公衆號後臺回覆 「 19 」 獲取。歡迎關注「物聯網IoT開發」公衆號

相關文章
相關標籤/搜索