劉長勇,王宜懷
(1.武夷學院數學與計算機學院,福建 武夷山 354300;2.蘇州大學計算機科學與技術學院,江蘇 蘇州 215006;3.武夷學院 認知計算與智能信息處理福建省高校重點實驗室,福建 武夷山 354300)
在無操作系統的嵌入式系統運行過程中,常常會設計一個綠色指示燈,讓它按某個固定的周期不停地閃爍(實際上就是按一定的時間頻率不斷切換亮暗),以此來表明系統正在運行,這個固定的周期就是延時,一般采用原地跑(空循環)的方式進行或通過定時器進行定時中斷,在這種方式下程序仍然占用CPU的使用權,其他程序只能等待空循環結束或定時器中斷執行完之后才能得以運行。而在帶有實時操作系統(Real-time Operation System,RTOS)的嵌入式系統中,會使用延時函數進行延時,此時RTOS的內核會暫時把不需執行的任務放入延時隊列中,讓出CPU的使用權,并對線程進行調度。使用延時函數的延時操作并非停止其他操作的空跑等待,而是由內核通過延時隊列管理這些延時任務,從而實現對任務的延時。目前,延時操作被廣泛地應用于各個領域。張輝等[1]指出可以根據火面環境、星歷、地形圖以及火星車的工作能力和約束條件,通過延時指令序列控制火星車完成火星表面巡視探測任務。樊智勇等[2]設計了一種大數據量低延時航電中繼系統,實現了將不同類型信號在多種設備之間進行遠程異地低延時數據交換。岑伯維等[3]通過在串聯支路、并聯支路、孤立支路和復合支路上對延時進行建模,有效地降低了電子物聯網邊緣計算終端業務的時序邏輯結構對計算資源配置的影響。王旭等[4]提出了基于ARM設計時鐘同步與觸發單元對分布式測試系統進行同步的方法,通過延時觸發輸出,實現了亞微秒級時鐘同步精度和精確同步觸發功能。張華健等[5]結合Linux系統和開源機器人操作系統ROS,設計了一個低成本、可擴展、高性能的開源移動機器人,實現了在同一局域網內低延時的遠程圖像傳輸和控制。黃雨航等[6]基于可驗證延時函數為基礎的隨機信標方法,結合Boneh-Lynn-Shacham(BLS)聚合簽名算法,在區塊鏈場景中生成公平且安全的隨機數。李川[7]通過向注入器電子槍等相關設備發送可精確延時調節的觸發信號,確保了HLS-II時序系統在Linux操作系統下可將直線加速器ns級長度的電子宏脈沖對準儲存環220 ns長度范圍內的任一相位的穩定。本文將分析RTOS延時函數的基本工作原理和RTOS的調度策略,剖析mbedOS實時操作系統延時函數的調度機制和關鍵代碼,以意法半導體的STM32L431芯片為例進行mbedOS延時函數剖析實踐,將線程響應延時函數的調度過程信息進行直觀輸出,對線程調度過程時序進行梳理與剖析,有助于讀者更加透徹地理解mbedOS延時函數響應機制,也可為不同實時操作系統的延時函數的比較分析提供借鑒。
在RTOS中,為了避免程序空跑占用CPU,采用延時函數來實現對任務的延時管理。在任務中執行延時函數時,RTOS內核會將當前任務狀態由激活態更改為阻塞態,并根據延時參數指示時間插入到延時隊列的相應位置,該隊列中的任務按照延時時長從小到大排序,每一個任務控制塊都記錄了自身需要的等待喚醒時間(等待喚醒時間=任務本身的延時時間-所有前驅結點的等待時間)。在任務延時期間,RTOS內核會調度其他就緒隊列的任務運行,當所有任務都因延時進入延時隊列后,此時RTOS內核會調度空閑任務運行。在空閑任務運行期間,SysTick定時器中斷會每隔1個時間嘀嗒檢測1次延時隊列中的任務是否到期,若有到期的任務則將任務從延時隊列移出,并將任務狀態由阻塞態更改為就緒態,放入就緒隊列中,等待RTOS的再次調度運行。在不同的RTOS中延時函數的基本工作原理是相同的,但延時函數的名稱及參數會略有不同,如在mbedOS實時操作系統中使用sleep_for延時函數,其參數millisec為32位的整型,表示延時的嘀嗒數[8];在MQX實時操作系統中使用time_delay_ticks延時函數,其參數time_in_ticks為32位的整型,表示延時的嘀嗒數[9];在RT-Thread實時操作系統中使用rt_thread_sleep延時函數,其參數tick為32位的整型,表示延時的嘀嗒數[10-11]。
在剖析延時函數響應機制的過程中,會涉及任務的管理和調度機制。任務的管理主要涉及就緒隊列和延時隊列,就緒隊列管理將運行的就緒態任務,延時隊列管理因調用延時函數而被阻塞的任務。任務調度主要是完成對任務狀態的切換、任務進出隊列的管理以及上下文切換等工作。在mbedOS中任務也稱為線程,對線程采用優先級搶占和時間片輪詢的綜合調度策略,調度策略是通過系統服務調用(Supervisor Call,SVC中斷)、可掛起系統調用(Pendable Supervisor,PendSV中斷)和定時器中斷(SysTick中斷)來實現的。
為實現用戶程序對系統硬件的間接訪問,RTOS內核往往會為用戶提供系統服務函數,用戶通過調用這些函數,觸發SVC中斷,實現對系統硬件的訪問。在mbedOS中執行SVC指令時(如在線程中調用延時函數),將觸發SVC中斷服務程序,完成指定的功能。SVC中斷被觸發后一般會被立即執行,有較高的精確性,如圖1所示。

圖1 觸發SVC中斷的簡單示例
ARM Cortex-M系列內核中提供了一個24位的SysTick定時器,采用倒計時的方式計數,當減1計數到0時,可產生SysTick中斷。mbedOS的內核時鐘頻率采用48 MHz,1個時間嘀嗒為1 ms,每個時間片為5個時間嘀嗒,當一個時間嘀嗒到時則產生一次SysTick中斷,在中斷服務程序中完成對線程的調度,如圖2所示。

圖2 觸發SysTick中斷的簡單示例
當用戶線程調用線程延時函數sleep_for后,會觸發SVC中斷,其內部函數調用順序為ThisThread∶∶sleep_for→osDelay→__svcDelay→觸發SVC中斷服務程序SVC_Handler→實際調用svcRtxDelay→最終調用osRtxThreadWaitEnter。線程延時等待函數osRtxThreadWaitEnter的主要功能包括獲取當前正在運行的線程、阻塞當前運行線程、將阻塞的線程根據延時時長插入延時隊列中、獲取當前優先級最高的就緒態線程并切換其狀態為激活態,該函數執行流程如圖3所示。
在mbedOS中,每一個時間嘀嗒(1 ms)執行一次SysTick_Handler中斷服務程序,通過調用osRtxTick_Handler函數完成對線程的調度,在osRtxTick_Handler函數中主要實現對到期的線程按優先級搶占策略和時間片輪詢策略進行調度。
其中涉及優先級搶占調度的關鍵代碼如下:
if ((kernel_state == osRtxKernelRunning)
&& (thread_ready != NULL) &&(thread_ready->priority >thread_running->priority))
{
osRtxThreadListRemove(thread_ready);
osRtxThreadBlock(thread_running);
osRtxThreadSwitch(thread_ready);
}
代碼分析:if語句判斷就緒隊列最高優先級的線程(thread_ready)的優先級,如果大于當前正在運行線程的優先級,則調用osRtxThreadListRemove函數從就緒隊列取出thread_ready,然后調用osRtxThreadBlock函數阻塞當前正在運行的線程,調用osRtxThreadSwitch函數切換thread_ready狀態為激活態準備運行,這就是優先級搶占調度策略。
其中涉及時間片輪詢調度的關鍵代碼如下:
if (osRtxInfo.thread.robin.tick == 0U) {
if (osRtxKernelGetState() == osRtxKernelRunning){
thread = osRtxInfo.thread.ready.thread_list;
if ((thread != NULL) &&(thread->priority == osRtxInfo.thread.robin.thread->priority))
{
osRtxThreadListRemove(thread);
osRtxThreadReadyPut(osRtxInfo.thread.robin.thread);
EvrRtxThreadPreempted(osRtxInfo.thread.robin.thread);
osRtxThreadSwitch(thread);
osRtxInfo.thread.robin.thread = thread;
osRtxInfo.thread.robin.tick= osRtxInfo.thread.robin.timeout;
}}}
代碼分析:第一條if (osRtxInfo.thread.robin.tick == 0U)語句指出當時間片結束(時間片=0)時,取出就緒隊列最高優先級的線程thread,接著繼續執行if ((thread != NULL) &&(thread->priority == osRtxInfo.thread.robin.thread->priority)),該語句指明如果thread的優先級等于當前正在運行線程的優先級,則從就緒隊列取出thread,調用osRtxThreadReadyPut函數將當前運行線程放入就緒隊列,切換thread狀態為激活態準備運行,且重新設置時間片為5個時間嘀嗒,這就是基于時間片的輪詢調度策略。
2014年ARM公司推出了mbedOS,它是一種專為物聯網(IoT)中的“物體”設計的開源嵌入式實時操作系統(Real-Time Operating System,RTOS)[12],具備一般RTOS的基本功能,在物聯網設備平臺[13]、物聯網應用[14]、通信技術與安全訪問服務機制[15]、協議棧與 IP網絡組件[16]等方面得到廣泛應用?,F針對mbedOS實時操作系統,給出具體延時響應的實踐分析。
mbedOS的延時響應測試工程采用SD-mbedOS工程框架[17],在Kinetis Design Studio 3.0.0 IDE集成開發環境和基于Cortex-M4內核的STM32微控制器上進行測試。STM32片內Flash大小為256 KB,用于中斷向量表和程序代碼等的存儲;片內RAM大小為32 KB,用于各類變量的存儲。
測試工程的功能是由主線程創建三個優先級相同的用戶線程,即綠燈線程、紅燈線程和藍燈線程,這三個線程分別實現每隔8 s綠燈切換亮暗、每隔4 s紅燈切換亮暗和每隔2 s藍燈切換亮暗,測試工程的執行流程如圖4所示。

圖4 測試工程執行流程
當mbedOS啟動和主線程的執行函數app_init運行結束后,先后共創建了3個系統線程和3個用戶線程[18],其相關信息如表1所示。此時,在就緒隊列的線程按優先級從高到低排列依次為綠燈線程、紅燈線程、藍燈線程和空閑線程,mbedOS內核開始對線程進行調度。由于就緒隊列的第一個線程是Green_Thread,它優先得到激活運行。Green_Thread線程實現每隔8 s控制一次綠燈的亮暗狀態,當Green_Thread線程的執行函數run_greenlight調用延時函數sleep_for執行延時8 s時,會觸發SVC中斷,暫時剝奪該線程對CPU的使用權,將該線程放入延時隊列中。接著mbedOS內核依次調度運行Red_Thread線程的執行函數run_redlight和Blue_Thread線程的執行函數run_bluelight,當它們執行到延時函數sleep_for時也依次被放入延時隊列中,最后空閑線程得到調度運行。在mbedOS內核調度線程的期間,SysTick中斷會每隔1 ms查看延時隊列中的線程是否到期,對到期的線程會按優先級搶占策略和時間片輪詢策略進行調度運行。

表1 線程信息一覽表
根據以上分析,基于優先級相同和延時機制的用戶線程調度時序情況如圖5所示,圖中給出了表示各對象的有效運行時間,實線箭頭表示線程進入隊列,虛線箭頭表示從隊列取線程。

圖5 基于優先級相同和延時機制的用戶線程調度時序圖
基于測量的確定性時序分析技術指出,可以利用在實際處理器硬件上執行感興趣的程序來獲得所觀察的數據[19]。程序插樁技術也表明可以在源程序中添加一些語句來獲取程序執行時的動態信息[20]。printf調試方法是應用最廣泛的調試手段之一,是一種動態分析方法,具有簡單、直觀等優點。在嵌入式系統開發過程中可將printf函數封裝成printf調試控件用來輸出調試過程信息[21],在CPU仿真中可輸出調試信息[22],在計算機視覺中輸出中間結果[23]。本文采用基于時序圖的printf調試方法對延時函數執行流程進行輸出,通過結果解析和理解延時函數的響應機制以及線程調度過程。延時函數響應過程可以歸納總結為以下11個步驟,由于篇幅有限,只給出與藍燈線程有關的輸出信息。另外,地址20004530表示藍燈線程,地址200045F0表示綠燈線程,地址200046B0表示紅燈線程,地址20004260表示空閑線程,地址8005A41表示缺省處理函數DefaultISR,地址2000303C表示延時隊列,地址2000302C表示就緒隊列。
4.3.1 線程啟動
在主線程的執行函數中創建并啟動三個用戶線程運行,然后主線程被阻塞。接著mbedOS內核對這些線程按照優先級搶占策略進行調度,對優先級相同的線程按時間片輪詢策略進行調度,首先調度綠燈線程運行。printf輸出如下信息:
0-1.MCU啟動
0-2.啟動綠燈線程
0-3.啟動紅燈線程
0-4.啟動藍燈線程
3-1.當前運行的線程=200045F0 (綠燈)開始
3-2.當前運行的線程=200045F0 (綠燈)調用延時等待函數
sleep_for->osDelay->__svcDelay->svcRtxDelay
4.3.2 綠燈線程延時8 s
綠燈線程調用延時函數ThisThread∶∶sleep_for(8000)延時8 s,放棄CPU的控制權進入延時隊列,取出紅燈線程激活運行。
4.3.3 紅燈線程延時4 s
紅燈線程調用延時函數ThisThread∶∶sleep_for(4000)延時4 s,放棄CPU的控制權進入延時隊列,取出藍燈線程激活運行。
4.3.4 藍燈線程延時2 s
藍燈線程調用延時函數ThisThread∶∶sleep_for (2000)延時2 s,放棄CPU的控制權進入延時隊列,取出空閑線程激活運行。printf輸出如下信息:
4.當前運行線程=20004530
4-1.調用osRtxThreadWaitEnter前延時隊列=2000303C中的線程: 0->200046B0->200045F0
4-2.調用osRtxThreadWaitEnter前就緒隊列=2000302C中的線程: 20004260->0->0
5-1.調用osRtxThreadWaitEnter->osRtxThreadDelayInsert將當前運行線程=20004530放到延時隊列
5-2.調用osRtxThreadWaitEnter->osRtxThreadListGet從就緒隊列獲取優先級最高的線程=20004260
5-3.調用osRtxThreadWaitEnter->osRtxThreadSwitch將線程=20004260設置為激活態準備運行
4-3.調用osRtxThreadWaitEnter后延時隊列: 20004260->200046B0->200045F0
4-3-1.線程延時時間: 2000->1995->3990
6.調用wait結束
4.3.5 運行空閑線程
空閑線程不做任何事情,只為了讓CPU不停止運行。
4.3.6 優先級搶占調度激活藍燈線程
在mbedOS調度期間每隔1 ms會產生一次SysTick中斷,當延時達到2 s時,會從延時隊列中將藍燈線程移出,其狀態由阻塞態更改為就緒態,放入就緒隊列中,按優先級搶占策略激活藍燈線程運行。printf輸出如下信息:
7-1.從延時隊列移出到期線程=20004530
7-2.將移出的線程=20004530放入就緒隊列中
8-1.從就緒隊列(2000302C)獲取線程(20004530),該線程的優先級=24>當前運行線程(20004260)的優先級=1,優先級搶占
8-2.將當前運行線程(20004260) 放到就緒隊列中(即阻塞當前線程)
8-3.設置線程(20004530)為激活態
4.3.7 藍燈線程結束
當藍燈延時結束后,藍燈反轉,接著又開始新一輪的延時等待。printf輸出如下信息:
3-3.當前運行的線程=20004530 (藍燈)反轉
3-4.當前運行的線程=20004530 (藍燈)結束
4.3.8 優先級搶占調度激活紅燈線程
在mbedOS調度期間每隔1 ms會產生一次SysTick中斷,當延時達到4 s時,會從延時隊列中將紅燈線程移出,其狀態由阻塞態更改為就緒態,放入就緒隊列中,按優先級搶占策略激活紅燈線程運行。
4.3.9 紅燈線程結束
當紅燈延時結束后,紅燈反轉,接著又開始新一輪的延時等待。
4.3.10 優先級搶占調度激活綠燈線程
在mbedOS調度期間每隔1 ms會產生一次SysTick中斷,當延時達到8 s時,會從延時隊列中將綠燈線程移出,其狀態由阻塞態更改為就緒態,放入就緒隊列中,按優先級搶占策略激活綠燈線程運行。
4.3.11 綠燈線程結束
當綠燈延時結束后,綠燈反轉,接著又開始新一輪的延時等待。
在RTOS中線程通過使用延時函數,線程會讓出CPU的使用權,使CPU能更好地為其他線程服務,提高了嵌入式系統的實時性。本文分析了延時函數的原理和RTOS調度策略,給出優先級搶占策略和時間片輪詢策略的關鍵代碼,解析了mbedOS延時函數的調度機制,構建了一個測試工程對mbedOS延時函數進行詳細剖析。通過基于時序圖的printf調試方法,將mbedOS延時函數的整個執行過程進行顯示輸出,有助于讀者快速理解延時函數的使用和調度機理,為分析比較其他實時操作系統提供借鑒。