使用STM32編寫一個簡單的RTOS:4.時鐘管理(二)定時器


參考資料:RTT官網文檔
關鍵字:分析RT-Thread源碼、stm32、RTOS、定時器timer。

問題及總結

一、爲什麼定時器定時不支持超過RT_TICK_MAX / 2(RT_ASSERT(timer->init_tick < RT_TICK_MAX / 2);)?

這個問題其實就跟我們現實中的時鐘一樣。這個問題分爲過了12點(歸0)和沒過12點。

1)沒過12點:
在這裏插入圖片描述
已知 a + b + c = max;因爲定時不超過RT_TICK_MAX / 2,即b < RT_TICK_MAX / 2。
所以a + c 就會大於RT_TICK_MAX / 2,即cur_tick - timeout_tick 在cur_tick小於timeout時,cur_tick - timeout_tick = a + c > RT_TICK_MAX / 2。

2)超過12點:
在這裏插入圖片描述
因爲定時init_tick不超過RT_TICK_MAX / 2,所以a + c < RT_TICK_MAX / 2,
所以b > RT_TICK_MAX / 2,即cur_tick - timeout_tick = b > RT_TICK_MAX / 2。

所以我們纔可以用(t->timeout_tick - timer->timeout_tick) < RT_TICK_MAX / 2來判斷是否到達超時時間。

二、定時器跳錶總結
在這裏插入圖片描述
加入插入一個400tick的定時器時start的流程。這裏就沒有話引索的建立了(額,靈魂畫手。。)
1:start的第一個for循環,用來遍歷各級引索
2:第二個for循環,遍歷該級引索鏈表
345:這行代碼的意思:row_head[row_lvl + 1] = row_head[row_lvl] + 1;
總結:這種方法實現跳錶比較簡潔易懂,不過多少級引索每個定時器就要包含多少個節點,比較浪費空間。目前還不清楚這個建立引索的算法是否高效。

跳錶

背景介紹(可以忽略)
有了時鐘節拍之後,我們就可以利用它來完成一些和時間相關功能,如定時器。我們只要記錄下調用定時時的時鐘節拍tick_start,和需要定時多久的時鐘節拍tick_count,然後我們在在時鐘節拍的處理裏面比較當前tick_current, 只要檢測到tick_cuurent >= tick_start +tick_count,就可以調用超時處理函數了。操作系統中會廣泛的使用定時器,所以我們不可能只維護一個,這個時候就需要用鏈表把它們串起來,只要我們按超時時間順序鏈起來,從頭遍歷比較就可以實現管理多個定時任務了。
好了,既然鏈表得順序排序,那我們插入鏈表的時候就得順序插入。即便對於已經按順序排序的鏈表,我們也得從頭開始一個個遍歷,如果鏈表比較長,而我們要插入的節點在比較靠後的位置上,那麼我們就要花費很大時間才能插入這個節點了,有什麼辦法可以解決這個問題嗎?
二分法?可惜鏈表不能跟數組一樣可以隨機訪問。哈希/HASH?消耗較大。看過RTT文檔你應該知道是跳錶了。跳錶。也許你可能沒聽說過,大部分數據結構和算法的書籍好像也都不怎麼講關於跳錶。但它是個好東西。

跳錶
跳錶在原有的有序鏈表上面增加了多級索引,通過索引來實現快速查找。首先在最高級索引上查找最後一個小於當前查找元素的位置,然後再跳到次高級索引繼續查找,直到跳到最底層爲止,這時候以及十分接近要查找的元素的位置了(如果查找元素存在的話)。由於根據索引可以一次跳過多個元素,所以跳查找的查找速度也就變快了。

在這裏插入圖片描述
例如在原有鏈表上,我們要找39,需要從3一個個遍歷到39,需要3->18->39就可以找到39了,
跳錶就是利用引索來快速遍歷的,時間複雜度爲O(log n),相當於二分查找。可以看出跳錶是用空間來換時間的。
跳錶的難點在於其是動態的,需要動態的建立多級引索,每次插入一個節點都要考慮是否需要重新建立引索。如何建立引索又是一個難題,建立不好,不僅浪費空間,也會降低搜索的時間,極端情況還可能退化到單鏈表,所以需要考慮如何才能適當的建立引索,纔有高效的搜索。

這裏推薦一下王爭老師的數據結構與算法之美,講的不錯,也不太枯燥。
跳錶一節可以試讀https://time.geekbang.org/column/article/42896

源碼分析

先來看一下定時器的結構體成員組成。

/**
 * timer structure
 */
struct rt_timer
{
    struct rt_object parent;                            /**< inherit from rt_object */

    rt_list_t        row[RT_TIMER_SKIP_LIST_LEVEL];

    void (*timeout_func)(void *parameter);              /**< timeout function */
    void            *parameter;                         /**< timeout function's parameter */

    rt_tick_t        init_tick;                         /**< timer timeout tick */
    rt_tick_t        timeout_tick;                      /**< timeout tick */
};

成員: struct rt_object parent
parent是用來“繼承”基本的內核對象的,加入對象容器隊列中,形成一條定時器鏈表。
parent.flag這裏是用來記錄這個定時器的狀態。flag的狀態有以下幾種:
RT_TIMER_FLAG_DEACTIVATED(無效的),RT_TIMER_FLAG_ACTIVATED(激活的),
RT_TIMER_FLAG_ONE_SHOT(單次的),RT_TIMER_FLAG_PERIODIC(週期性的),
RT_TIMER_FLAG_HARD_TIMER(硬定時), RT_TIMER_FLAG_SOFT_TIMER(軟定時)

成員: rt_list_t row[RT_TIMER_SKIP_LIST_LEVEL];
這是一個鏈表數組,用來記錄跳錶的引索,下標是引索的級數。默認是1。

成員: void (*timeout_func)(void *parameter);
超時回調函數,當定時時間到了的時候,將會調用這個超時函數。

成員:void *parameter;
超時函數的參數,類型時void *。

成員:init_tick
是用來記錄超時間隔的,在啓動定時器後的init_tick時間後,將觸發超時。

成員:timeou_tick
用來記錄超時時的tick。

瞭解了定時器的結構體後,現在我們來看一下定時器相關的API。以介紹硬定時器爲例。

void rt_system_timer_init(void)
{
    int i;

    for (i = 0; i < sizeof(rt_timer_list) / sizeof(rt_timer_list[0]); i++)
    {
        rt_list_init(rt_timer_list + i);
    }
}

Alt
在rt_system_timer_init中初始化了rt_timer_list,默認只有一個。

接着看一下初始化定時器:

void rt_timer_init(rt_timer_t  timer,
                   const char *name,
                   void (*timeout)(void *parameter),
                   void       *parameter,
                   rt_tick_t   time,
                   rt_uint8_t  flag)
{
    /* timer check */
    RT_ASSERT(timer != RT_NULL);

    /* timer object initialization */
    rt_object_init((rt_object_t)timer, RT_Object_Class_Timer, name);

    _rt_timer_init(timer, timeout, parameter, time, flag);
}

在這裏插入圖片描述
第2行代碼rt_object_init將timer初始化爲內核的定時器對象,並插入定時器對象鏈表中。
第3行,真正初始化定時器的地方。

static void _rt_timer_init(rt_timer_t timer,
                           void (*timeout)(void *parameter),
                           void      *parameter,
                           rt_tick_t  time,
                           rt_uint8_t flag)
{
    int i;

    /* set flag */
    timer->parent.flag  = flag;	//設置標識位,單次或週期性,硬定時或軟定時

    /* set deactivated */
    timer->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;	//初始化時先設置爲無效定時器。

    timer->timeout_func = timeout;	//設置超時函數
    timer->parameter    = parameter;	//設置超時函數參數

    timer->timeout_tick = 0;	//超時tick初始值爲0
    timer->init_tick    = time;	//超時間隔

    /* initialize timer list */
    for (i = 0; i < RT_TIMER_SKIP_LIST_LEVEL; i++)
    {
        rt_list_init(&(timer->row[i]));	//初始化引索鏈表,i爲引索級別
    }
}

初始化定時器後,就可以啓動啓動器了。
下面就是重點了,在啓動中插入鏈表和建立引索。
這個start函數有點長,這裏以硬定時爲例。

rt_err_t rt_timer_start(rt_timer_t timer)
{
    unsigned int row_lvl;
    rt_list_t *timer_list;
    register rt_base_t level;
    rt_list_t *row_head[RT_TIMER_SKIP_LIST_LEVEL];
    unsigned int tst_nr;
    static unsigned int random_nr;

 
    /* remove timer from list */
    _rt_timer_remove(timer);	//防止重複插入鏈表
    timer->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;	//將定時器標識爲無效

    /*
     * get timeout tick,
     * the max timeout tick shall not great than RT_TICK_MAX/2
     */
    RT_ASSERT(timer->init_tick < RT_TICK_MAX / 2);	//init_tick必須小於RT_TICK_MAX / 2。
    timer->timeout_tick = rt_tick_get() + timer->init_tick;	//計算超時時間
    {
        /* insert timer to system timer list */
        timer_list = rt_timer_list;	//將timer_list指向rt_timer_list
    }

    row_head[0]  = &timer_list[0];	//將row_head[0]指向rt_timer_list

   //遍歷各級跳錶,這裏的level默認爲1,這個for就相當於沒有。
    for (row_lvl = 0; row_lvl < RT_TIMER_SKIP_LIST_LEVEL; row_lvl++)
    {
//遍歷當前級別(row_lvl)跳錶,第一次沒有定時器,不會進入。
//注意這裏for的語句1爲空,row_head是從上一級跳下來的,而不從第一個節點開始遍歷
//如果不是空或者最後一個的則遍歷,即當找到不到比他大的時候已指向最後一個
        for (; row_head[row_lvl] != timer_list[row_lvl].prev;
             row_head[row_lvl]  = row_head[row_lvl]->next)
        {
            struct rt_timer *t;
            rt_list_t *p = row_head[row_lvl]->next;	//這裏指向的是下一個定時器的row而不是當前的。

            /* fix up the entry pointer */
//通過成員地址獲取結構體地址,rt_list_entry比較簡單,就不介紹了
            t = rt_list_entry(p, struct rt_timer, row[row_lvl]);	//t指向了p的定時器地址

            /* If we have two timers that timeout at the same time, it's
             * preferred that the timer inserted early get called early.
             * So insert the new timer to the end the the some-timeout timer
             * list.
             */
            if ((t->timeout_tick - timer->timeout_tick) == 0)	//如果超時時間一樣。下一輪則插入其後面
            {
                continue;
            }
//如果要插入的超時時間小於遍歷的這個定時器的超時時間,即找到位置,則break。
            else if ((t->timeout_tick - timer->timeout_tick) < RT_TICK_MAX / 2)	
            {
                break;
            }
        }
//把row_head[row_lvl + 1]指向了上一級找到的位置的下一個row
//如果不是後面一級,則進入下一級引索繼續遍歷,這裏的+1等於sizeof(row_head[0]),即將row_head[row_lvl +1]指向了該定時器的row[row_lvl+1]。
        if (row_lvl != RT_TIMER_SKIP_LIST_LEVEL - 1)
            row_head[row_lvl + 1] = row_head[row_lvl] + 1;
    }

    /* Interestingly, this super simple timer insert counter works very very
     * well on distributing the list height uniformly. By means of "very very
     * well", I mean it beats the randomness of timer->timeout_tick very easily
     * (actually, the timeout_tick is not random and easy to be attacked). */
    random_nr++;	//這裏將插入的定時器次數作爲隨機值,爲了讓跳錶引索分佈均勻
    tst_nr = random_nr;

//將定時器插入到最後一級的跳錶鏈表中,即RT_TIMER_SKIP_LIST_LEVEL - 1級,所有的都會插入最後一級鏈表中
    rt_list_insert_after(row_head[RT_TIMER_SKIP_LIST_LEVEL - 1],
                         &(timer->row[RT_TIMER_SKIP_LIST_LEVEL - 1]));

//建立跳錶引索,RT_TIMER_SKIP_LIST_LEVEL應該要大於2的
    for (row_lvl = 2; row_lvl <= RT_TIMER_SKIP_LIST_LEVEL; row_lvl++)
    {
//判斷低兩位是否爲0,是則插入當前引索鏈表中,越上層的引索應該插入的越少。
        if (!(tst_nr & RT_TIMER_SKIP_LIST_MASK))
            rt_list_insert_after(row_head[RT_TIMER_SKIP_LIST_LEVEL - row_lvl],
                                 &(timer->row[RT_TIMER_SKIP_LIST_LEVEL - row_lvl]));
        else
            break;
        /* Shift over the bits we have tested. Works well with 1 bit and 2
         * bits. */
//將tst_nr左移2位
        tst_nr >>= (RT_TIMER_SKIP_LIST_MASK + 1) >> 1;
    }
//置爲活躍狀態
    timer->parent.flag |= RT_TIMER_FLAG_ACTIVATED;

    return -RT_EOK;
}

接着看一下stop函數。

rt_err_t rt_timer_stop(rt_timer_t timer)
{
    register rt_base_t level;

    /* timer check */
    RT_ASSERT(timer != RT_NULL);
//如果不是活躍狀態
    if (!(timer->parent.flag & RT_TIMER_FLAG_ACTIVATED))
        return -RT_ERROR;

    RT_OBJECT_HOOK_CALL(rt_object_put_hook, (&(timer->parent)));

    /* disable interrupt */
    level = rt_hw_interrupt_disable();
//從鏈表中移除
    _rt_timer_remove(timer);

    /* enable interrupt */
    rt_hw_interrupt_enable(level);
//修改狀態爲無效
    /* change stat */
    timer->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;

    return RT_EOK;
}

stop中將定時器從鏈表中移除,並將狀態設置爲無效狀態。

現在看一下是怎麼檢測定時器是否到達定時時間的。rt_timer_check放在系統節拍的中斷上,也就是每個系統節拍都會檢查一下是否有定時器到達超時時間。

void rt_timer_check(void)
{
    struct rt_timer *t;
    rt_tick_t current_tick;
    register rt_base_t level;

    RT_DEBUG_LOG(RT_DEBUG_TIMER, ("timer check enter\n"));

//獲取當前tick
    current_tick = rt_tick_get();

    /* disable interrupt */
    level = rt_hw_interrupt_disable();

//判斷定時器最低層(已排序好)的第一個定時器是否超時。
    while (!rt_list_isempty(&rt_timer_list[RT_TIMER_SKIP_LIST_LEVEL - 1]))
    {
        t = rt_list_entry(rt_timer_list[RT_TIMER_SKIP_LIST_LEVEL - 1].next,
                          struct rt_timer, row[RT_TIMER_SKIP_LIST_LEVEL - 1]);

        /*
         * It supposes that the new tick shall less than the half duration of
         * tick max.
         */
//如果到達超時時間
        if ((current_tick - t->timeout_tick) < RT_TICK_MAX / 2)
        {
            RT_OBJECT_HOOK_CALL(rt_timer_timeout_hook, (t));
//從鏈表中移除該定時器
            /* remove timer from timer list firstly */
            _rt_timer_remove(t);
//調用超時函數,並把parameter當做參數傳入
            /* call timeout function */
            t->timeout_func(t->parameter);

            /* re-get tick */
            current_tick = rt_tick_get();

            RT_DEBUG_LOG(RT_DEBUG_TIMER, ("current tick: %d\n", current_tick));
//判斷是否是週期性的以及是否還是活躍狀態
            if ((t->parent.flag & RT_TIMER_FLAG_PERIODIC) &&
                (t->parent.flag & RT_TIMER_FLAG_ACTIVATED))
            {
                /* start it */
                t->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;
                rt_timer_start(t);	//週期性的話就重新插入跳錶中
            }
            else
            {
                /* stop timer */
                t->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;
            }
        }
        else
            break;
    }

    /* enable interrupt */
    rt_hw_interrupt_enable(level);

    RT_DEBUG_LOG(RT_DEBUG_TIMER, ("timer check leave\n"));
}

rt_timer_check中找到超時的定時器,並調用超時函數。

有了定時器,現在實現線程的延時/睡眠就輕鬆多了。線程管理那一篇已經介紹過rt_thread_delay了,
現在不用看代碼我們也能知道是怎麼實現delay的了。首先delay的時候需要將線程掛起suspend,然後根據要delay的時間,start啓動定時器,然後timeout超時函數裏則是將線程喚醒resume就緒態。

關於rtt的定時器就分析到這裏,總結及問題就放在最前面了。

測試

下面是RTT-MINI的定時器測試,這裏沒有用跳錶,代碼見鏈接。
Alt
在這裏插入圖片描述
發現了兩個bug,thread_exit裏面沒有調用schedule,導致推出時跑死。schedule_ready_del_thread裏面沒有判斷同優先級下是否還有其它線程。
在這裏插入圖片描述
在這裏插入圖片描述
測試代碼鏈接:https://download.csdn.net/download/u012220052/11236208

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章