STM32 上的任務調度策略 (一)

最近難得有點空閑時間, 就來講講 STM32 上任務調度的策略吧。
在我的認知中, 最基本的任務調度無非有兩種方式:

  • 利用 system tick 的中斷計數
  • 保存上下文, 利用中斷進行上下文切換

這麽簡單概括肯定不方便理解, 一次講不完肯定要分兩次 (挖坑)。那這次就先來説説 system tick 的方式吧。

什麽是 system tick

這是一個 24 位的系統節拍定時器, 具有自動重載和溢出中斷功能,
所有基於 Cortex Profile M 的微控制器都可以由這個定时器獲得一定的時間間隔。

它每隔一段時間會產生一次中斷, 一般使用 1ms 為佳,
STM32 的 HAL 庫會使用 HAL_IncTick 來更新系統時間計數, 它就相當於整個單片機系統的心臟。

1
2
3
4
5
6
/**
* @brief This function handles System tick timer.
*/
void SysTick_Handler(void) {
HAL_IncTick();
}

HAL_Delay 它的内部就使用 system tick 來實現等待,
就連 HAL 庫中阻塞調用外設的接口也是使用它來進行超時檢測的。

基於 system tick 任務調度的實現

在知道它之後, 我們來開始講怎麽去用它實現任務調度。
我們需要擬定一下它整個運作的生命周期:

img

如上圖所示, 我們需要在中斷中遞增 tick_inc,
並在主循環中每次遍歷任務之前保存它並重置計數。 因爲中斷隨時會發生, 所以這一步要越快越好。
在遍歷中逐個減少任務的剩餘時間, 當有任務的剩餘時間為 0 或者小於 0 時, 執行該任務並重置它的剩餘時間。

這個模型的好處有

  • 穩定可靠
  • 編寫和使用簡單
  • 平台相關程式碼少, 移植方便快捷
  • 非搶占模型, 不會發生資源衝突

缺點如下

  • 無法保證調度實時性
  • 任務過多容易造成性能瓶頸

基於此模型的異步 Sleep 的實現

在理解整個模型如何運作之後, 那麽實現異步等待就非常簡單了。

因爲現在所有的任務都是在 Main Loop 中調度的(參見圖 1.), 所以如果我們在某個任務中
使用了 delay (比如 HAL_Delay), 那麽勢必會造成系統假死。 因爲它會阻塞主循環, 導致調度器無法正常調度任務。

爲了解決這個問題, 我們只需要把任務調取權交給需要 delay 的函數, 並同時根據 systick 計算超時。
就像這樣。 當然它並沒有考慮到 systick 的溢出情況。

1
2
3
4
5
6
7
void scheduler_sleep(uint32_t milliseconds) {
// loop until time exceeds
uint32_t start = tick_get_time();
while(tick_get_time() - start < milliseconds)
scheduler_handler();
}

需要説明的是, 如果這個 delay 時間太長且超過了任務本身的時間, 則會發生嵌套調用。
即: 這個任務因爲剩餘時間小於 0, 在 scheduler_sleep 中被執行, 並再次進入 scheduler_sleep, 最終導致炸棧。
這個問題就需要額外處理了, 本文假設不會遇到這種情況。

一些儸嗦

關於不能保證任務實時性

因爲執行任務的時候必定會消耗一定的時間, 如果此時單片機處理速度較慢且任務間隔時間小,
則會造成任務堆積導致卡頓或者響應速度變慢。

所以在使用時應提高間隔時間, 盡可能降低調度壓力。

與 RTOS 的不同

實際上它跟 RTOS 的任務調度方式不是一種東西, 它的任務並非「綫程」。
因爲不需要進行上下文切換, 且任務之間不是並行的關係, 因此也不存在資源競爭。

C 語言中的實現

我已經根據這個思路實現了一個跨平台的任務調度器,支持 async/await,支持 tls存儲。請參考 TheSnowfield/frost

寫到最後

如果有任何意見或者問題或是錯誤或遺漏, 請在下方評論區留言。謝謝 (๑•̀ㅂ•́)و✧

引用

Author

TheSnowfield

Posted on

2022-09-21

Updated on

2024-04-01

Licensed under