STM32 上的任務調度策略 (一)
最近難得有點空閑時間, 就來講講 STM32 上任務調度的策略吧。
在我的認知中, 最基本的任務調度無非有兩種方式:
- 利用 system tick 的中斷計數
- 保存上下文, 利用中斷進行上下文切換
這麽簡單概括肯定不方便理解, 一次講不完肯定要分兩次 (挖坑)。那這次就先來説説 system tick 的方式吧。
什麽是 system tick
這是一個 24 位的系統節拍定時器, 具有自動重載和溢出中斷功能,
所有基於 Cortex Profile M 的微控制器都可以由這個定时器獲得一定的時間間隔。
它每隔一段時間會產生一次中斷, 一般使用 1ms 為佳,
STM32 的 HAL 庫會使用 HAL_IncTick 來更新系統時間計數, 它就相當於整個單片機系統的心臟。
1 | /** |
HAL_Delay 它的内部就使用 system tick 來實現等待,
就連 HAL 庫中阻塞調用外設的接口也是使用它來進行超時檢測的。
基於 system tick 任務調度的實現
在知道它之後, 我們來開始講怎麽去用它實現任務調度。
我們需要擬定一下它整個運作的生命周期:
如上圖所示, 我們需要在中斷中遞增 tick_inc,
並在主循環中每次遍歷任務之前保存它並重置計數。 因爲中斷隨時會發生, 所以這一步要越快越好。
在遍歷中逐個減少任務的剩餘時間, 當有任務的剩餘時間為 0 或者小於 0 時, 執行該任務並重置它的剩餘時間。
這個模型的好處有
- 穩定可靠
- 編寫和使用簡單
- 平台相關程式碼少, 移植方便快捷
- 非搶占模型, 不會發生資源衝突
缺點如下
- 無法保證調度實時性
- 任務過多容易造成性能瓶頸
基於此模型的異步 Sleep 的實現
在理解整個模型如何運作之後, 那麽實現異步等待就非常簡單了。
因爲現在所有的任務都是在 Main Loop
中調度的(參見圖 1.), 所以如果我們在某個任務中
使用了 delay (比如 HAL_Delay), 那麽勢必會造成系統假死。 因爲它會阻塞主循環, 導致調度器無法正常調度任務。
爲了解決這個問題, 我們只需要把任務調取權交給需要 delay 的函數, 並同時根據 systick 計算超時。
就像這樣。 當然它並沒有考慮到 systick 的溢出情況。
1 | void scheduler_sleep(uint32_t milliseconds) { |
需要説明的是, 如果這個 delay 時間太長且超過了任務本身的時間, 則會發生嵌套調用。
即: 這個任務因爲剩餘時間小於 0, 在 scheduler_sleep
中被執行, 並再次進入 scheduler_sleep
, 最終導致炸棧。
這個問題就需要額外處理了, 本文假設不會遇到這種情況。
一些儸嗦
關於不能保證任務實時性
因爲執行任務的時候必定會消耗一定的時間, 如果此時單片機處理速度較慢且任務間隔時間小,
則會造成任務堆積導致卡頓或者響應速度變慢。
所以在使用時應提高間隔時間, 盡可能降低調度壓力。
與 RTOS 的不同
實際上它跟 RTOS 的任務調度方式不是一種東西, 它的任務並非「綫程」。
因爲不需要進行上下文切換, 且任務之間不是並行的關係, 因此也不存在資源競爭。
C 語言中的實現
我已經根據這個思路實現了一個跨平台的任務調度器,支持 async/await,支持 tls存儲。請參考 TheSnowfield/frost。
寫到最後
如果有任何意見或者問題或是錯誤或遺漏, 請在下方評論區留言。謝謝 (๑•̀ㅂ•́)و✧
引用
- [1] STM32知识:什么是SYSTICK 作用是什么
http://news.eeworld.com.cn/mcu/ic487932.html
STM32 上的任務調度策略 (一)
https://blog.awa.moe/2022/09/21/stm32/stm32-task-schedule-one/