軟定時器
所謂軟定時器,是由一個線程運行維護的定時器列表。由線程調用定時器回調函數。
相對硬定時器,是由中斷(SysTick)維護的定時器列表,并在中斷中調用定時器回調函數。
另外,還有一種*硬件定時器*,這個和單片機里的定時器是一個概念,由外設定時器實現定時。和 rt-thread 提供的硬定時器是兩個不同概念。
對硬定時器回調函數有嚴格的執行時間要求,而且不能調用任何在中斷中不能調用的函數。總之不能有任何不能在中斷中執行的操作。
那么,軟定時器呢,要求需要這么嚴格嗎?
比如有個處理執行時間 10ms,比如在定時器中斷函數里發送個等待 10ms 的消息...
無論是硬定時器還是軟定時器,它們各自有一個定時器列表。列表中的定時器根據定時時間長短排序,定時時間短的在前。掃描這個列表中所有定時器,直到結尾或者出現第一個定時時間未到的定時器節點。當判斷出定時時間到的時候,調用定時器回調函數。
假如,某次掃描這個列表中多于一個定時器到達定時時間,也就是需要執行兩個以上的定時器回調函數。并且前一個掃描到的定時器的回調函數執行時間比較長,出現上面設想的某一種使用情況。這時候會出現什么效果?
因為硬定時器的種種硬性要求,以下討論只針對軟定時器。
從對 `rt_soft_timer_check` 的幾個疑問講起
先擺出官方的 rt_soft_timer_check 函數,實現。這個函數是來掃描定時器列表中到達定時時間的定時器,并調用定時器回調函數的。
```
void rt_soft_timer_check(void)
{
rt_tick_t current_tick;
struct rt_timer *t;
register rt_base_t level;
rt_list_t list;
rt_list_init(&list);
RT_DEBUG_LOG(RT_DEBUG_TIMER, ("software timer check enter\n"));
/* disable interrupt */
level = rt_hw_interrupt_disable();
while (!rt_list_isempty(&rt_soft_timer_list[RT_TIMER_SKIP_LIST_LEVEL - 1]))
{
t = rt_list_entry(rt_soft_timer_list[RT_TIMER_SKIP_LIST_LEVEL - 1].next,
struct rt_timer, row[RT_TIMER_SKIP_LIST_LEVEL - 1]);
current_tick = rt_tick_get();
/*
* 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_enter_hook, (t));
/* remove timer from timer list firstly */
_rt_timer_remove(t);
if (!(t->parent.flag & RT_TIMER_FLAG_PERIODIC))
{
t->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;
}
/* add timer to temporary list */
rt_list_insert_after(&list, &(t->row[RT_TIMER_SKIP_LIST_LEVEL - 1]));
soft_timer_status = RT_SOFT_TIMER_BUSY;
/* enable interrupt */
rt_hw_interrupt_enable(level);
/* call timeout function */
t->timeout_func(t->parameter);
RT_OBJECT_HOOK_CALL(rt_timer_exit_hook, (t));
RT_DEBUG_LOG(RT_DEBUG_TIMER, ("current tick: %d\n", current_tick));
/* disable interrupt */
level = rt_hw_interrupt_disable();
soft_timer_status = RT_SOFT_TIMER_IDLE;
/* Check whether the timer object is detached or started again */
if (rt_list_isempty(&list))
{
continue;
}
rt_list_remove(&(t->row[RT_TIMER_SKIP_LIST_LEVEL - 1]));
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 break; /* not check anymore */
}
/* enable interrupt */
rt_hw_interrupt_enable(level);
RT_DEBUG_LOG(RT_DEBUG_TIMER, ("software timer check leave\n"));
}
疑問一 `rt_list_t list` 這個臨時中間變量的作用是什么?
- 進入 `rt_soft_timer_check` 函數后,先初始化 list 變量。
- 掃描定時器列表,當有到時的定時器,把這個定時器從軟定時器列表 `rt_soft_timer_list` 移除,插入到這個 list 臨時存放。
- 開中斷,調用定時器回調函數,關中斷
- 把 list 中存放的定時器移除
- 判斷定時器的周期定時器還是一次性定時器
- 繼續掃描定時器列表
list 是個局部變量,僅僅起臨時存放當前這個定時器的作用。看似是一種穩妥的做法。
但是,這樣做的初衷是什么?
為什么這個定時器一定要放到某個列表里?
如果從軟定時器列表 `rt_soft_timer_list` 移除后,不插入任何列表會有什么影響?
因為退出 `rt_soft_timer_check` 函數后,list 列表不復存在了,應該不是退出 `rt_soft_timer_check` 函數后的需求,那么插入 list 和 從 list 取出之間有哪些情況需要我們注意,需要用一個臨時列表將軟定時器暫存?
定時器回調函數里可能發生哪些操作?
- 修改定時器設置
- 停止,重啟定時器
- 刪除定時器
- 發生中斷,在中斷里執行上述三種操作
修改定時器設置,可能只涉及到定時器的定時時間間隔和定時周期特性。這兩個參數設置需要定時器必須在某個列表中嗎?
停止,重啟定時器,必然導致修改定時器所在列表指針,這里就涉及到雙向列表的操作了。
> 簡短介紹一下雙向列表,
- rt-thread 使用的雙向列表,每一個節點有一個 prev 和 一個 next 指針,分別指向雙向列表中的前一個節點和后一個節點。
- 一個空列表 l 僅有一個不含數據的節點,此節點的 prev next 指針均指向它自己。
- 任何一個帶數據的列表節點必須進行初始化,使得它的 prev next 分別指向它自己,這一點和空列表 l 完全雷同!換句話說,***任何一個雙向列表節點均有作為鏈表的潛質***。從操作上講,你可以定義兩個定時器,然后這兩個定時器之間構建一個含有兩個節點的雙向列表,當然,這種做法沒有多少實用意義。
- 從鏈表中移除的節點,**必須**使得它的 prev next 指針指向它自己。
- **無論一個節點是否在某個雙向鏈表中,或者僅僅是一個獨立節點,對它進行刪除操作,效果是完全一樣的!**
- 更多的操作詳見 rtservice.c 文件中相關函數,`rt_list_init, rt_list_insert_after, rt_list_insert_before, rt_list_remove` 等等。
停止定時器會把當前定時器從定時器列表刪除,無論這個定時器有沒有在某個定時器列表中,或者只是一個獨立的定時器節點,刪除操作的結果都是一樣的,使用 list 這個臨時列表可能不能保護它不被刪除。
重啟定時器會把它先從前一個列表中刪除,然后插入軟定時器列表 `rt_soft_timer_list` 。list 這個臨時列表也阻止不了重啟定時器操作。
**至此,可以看出,`rt_list_t list` 這個臨時列表無任何存在意義**
疑問二 `soft_timer_status` 指示的是什么狀態?
這是一個全局靜態變量,它的使用也很簡單,只在四個地方使用了,上面的源碼函數里有兩處,其它兩個地方分別是初始化聲明
/* soft timer status */
static rt_uint8_t soft_timer_status = RT_SOFT_TIMER_IDLE;
和——以下摘自 `rt_timer_start` 函數
#ifdef RT_USING_TIMER_SOFT
if (timer->parent.flag & RT_TIMER_FLAG_SOFT_TIMER)
{
/* check whether timer thread is ready */
if ((soft_timer_status == RT_SOFT_TIMER_IDLE) &&
((timer_thread.stat & RT_THREAD_STAT_MASK) == RT_THREAD_SUSPEND))
{
/* resume timer thread to check soft timer */
rt_thread_resume(&timer_thread);
rt_schedule();
}
}
#endif /* RT_USING_TIMER_SOFT */
將這四個地方聯系起來看,意思好像是調用定時器回調函數前修改軟定時器為 busy 狀態,返回回調函數后恢復為 idle 狀態,而如果是在定時器回調函數里調用 `rt_timer_start` ,可以達到不進行任務調度的目的。好像是起了雙保險作用,真是這樣嗎?
我們分析一下上面這段 `rt_timer_start` 函數片段。
首先判斷定時器是不是軟定時器,只有軟定時器啟動時才有進行任務調度的可能。
其次,判斷 `soft_timer_status` 是否空閑,以及軟定時器線程是否***掛起態***。
以上仨條件均滿足,進行任務調度。
我們重點關注“其次”,一個定時器線程調用的定時器回調函數,這個線程會是掛起態嗎?答案是肯定不是。它在運行著,肯定是 `RT_THREAD_RUNNING` 的。那么這個 `soft_timer_status` “雙保險”了嗎?
疑問三 開篇提到的假想
開篇提到了一種假想,假想軟定時器回調函數占用 cpu 時間有點兒長,會產生什么影響,引起什么后果。
討論這個問題仍然離不開 `rt_soft_timer_check` 函數工作原理,我們再梳理一下 `rt_soft_timer_check` 函數的操作。(以下分析忽略 list 以及 soft_timer_status 相關操作)
- 關中斷
- 掃描列表是否有節點
- 取出第一個節點
- ***獲取當前系統 tick***
- 檢查定時器是否定時時間到,如果到時
- 先從軟定時器列表 `rt_soft_timer_list` 移除定時器。非周期定時器,取消激活態
- 開中斷
- 執行定時器回調函數。(這里可能存在長時間操作)
- 關中斷
- 對于周期性定時器,重啟定時器;非周期定時器,前面已經做了取消激活態操作。
- 繼續掃描列表,取出第一個節點,***獲取當前系統 tick*** ,檢查定時器是否定時時間到。。。
假設 RT_TICK_PER_SECOND = 1000,有兩個周期性定時器 t0 t1 ,定時間隔不同,同時啟動,各自的定時器回調函數執行時間 t0 500us,t1 5 ms。
經過一段時間以后,總是可能會出現定時間隔公倍數時刻 Tn ,它們倆同時達到定時時間。
如果 t1 先被處理,那么 t1 重啟的時候系統 tick 已經是 Tn + 5;t0 的重啟時間有 50% 的可能是 Tn + 5, 50%的可能是 Tn + 6。
如果 t0 先被處理,t0 的重啟時間有 50% 的可能是 Tn, 50%的可能是 Tn + 1;t1 重啟時間是 Tn + 5 或 Tn + 6。
即便不考慮 t0 不考慮它對外的影響,也不考慮它受到的影響,僅僅分析 t1 自己對自己的影響,也可以看出來,隨著時間的推移,它的定時間隔不是初始設定的 Inv ,而是 Inv + 5。
優化后的 `rt_soft_timer_check` 流程
- ***獲取當前系統 tick***
- 關中斷
- 掃描列表是否有節點
- 取出第一個節點
- 檢查定時器是否定時時間到,如果到時
- 先從軟定時器列表 `rt_soft_timer_list` 移除定時器。
- 對于周期性定時器,***重啟定時器***;非周期定時器,取消激活態。
- 開中斷
- 執行定時器回調函數。(這里可能存在長時間操作)
- 關中斷
- 繼續掃描列表,取出第一個節點,檢查定時器是否定時時間到。。。
其中,優化后的重啟定時器不能使用原來的接口。需要使用如下原型函數接口
static rt_err_t _rt_timer_start(rt_timer_t timer, rt_tick_t current_tick)
第二個參數是進入 `rt_soft_timer_check` 函數,第一次關中斷前獲取的當前系統 `tick` 值,無論下面掃描出多少個到達時間的定時器,啟動時間都是同一個 `tick` 值。
而且無論其中某個定時器回調函數執行時間有多長,或者多個回調函數累積執行時間有多長,它們的啟動時間都是相同的!
注:由此,引起的另一個弊端的,期間某個定時器定時時間到了,但是會被判定為未到,下次調用 `rt_soft_timer_check` 時才會被處理。
附測試程序
static void led1_timeout(void *parameter)
{
rt_tick_t current_tick;
int pin = rt_pin_read(LED1_PIN);
rt_pin_write(LED1_PIN, !pin);
current_tick = rt_tick_get();
rt_hw_us_delay(50000);
}
void led_tick_thread(void *parameter)
{
rt_timer_t led1_timer;
led1_timer = rt_timer_create("ledtim1", led1_timeout,
RT_NULL, 1000,
RT_TIMER_FLAG_PERIODIC | RT_TIMER_FLAG_SOFT_TIMER);
if (led1_timer != RT_NULL) {
rt_timer_start(led1_timer);
}
while (1) {
rt_pin_write(LED0_PIN, PIN_HIGH);
rt_thread_mdelay(500);
rt_pin_write(LED0_PIN, PIN_LOW);
rt_thread_mdelay(500);
}
}
作為對比,兩個 led 一個用軟定時器控制亮滅頻率,一個用 mdelay 延時控制亮滅頻率。
如果 timeout 沒有延遲,兩個燈一直是同步的;有延遲后,過一段時間兩個燈亮滅變不同步了。
總結
肯定有很多人反對說,定時器回調函數不要有長時間操作。發消息,信號,郵箱...交給其它線程操作云云。
軟定時器本身就是一個線程,通過某種技術手段,在這個線程中可以完成的工作,一定要使用消息機制交給另外一下線程完成嗎?
如何抉擇,請君深思
本優化系列所有提到的更改已經提交到 gitee ,歡迎大家測試
https://gitee.com/thewon/rt_thread_repo
相關文章:
rt-thread 優化系列(0) SysTick 優化分析
rt-thread 優化系列(一) 之 過多關中斷
rt-thread 優化系列(二) 之 同步和消息關中斷分析
審核編輯:湯梓紅
-
定時器
+關注
關注
23文章
3246瀏覽量
114740 -
函數
+關注
關注
3文章
4329瀏覽量
62576 -
RT-Thread
+關注
關注
31文章
1285瀏覽量
40094
發布評論請先 登錄
相關推薦
評論