汪 恒 王宜懷 劉 肖 劉長勇,2
1(蘇州大學計算機科學與技術學院 江蘇 蘇州 215006) 2(武夷學院認知計算與智能信息處理福建省高校重點實驗室 福建 武夷山 354300)
mbedOS是由ARM公司針對ARM CortexM系列處理器專門打造用于物聯網(IoT)的一種開源嵌入式實時操作系統(RTOS)[1-2]。對于實時操作系統來說,其實時性是其最重要的特征,它必須對所接收到的某些信號做出“及時”或“實時”的反應,在規定的時間內完成任務的處理[3-5]。而mbedOS提供了相當多的系統服務來實現任務的實時控制,如事件機制、線程信號機制、消息隊列機制、互斥鎖機制和信號量機制等[6-7],有著優越的實時性,故通常被用于對響應時間有較高實時性要求的嵌入式系統,如智能終端和物聯網節點等。
目前有關RTOS的實時性分析大多是從整體的實時能力來考慮,通過上下文切換時間、中斷響應速度等指標進行總體的實時性考量[8]。但實際應用RTOS來進行任務的控制時,其實時性的高低通常會受到使用的控制機制影響,針對具體機制進行實時性分析,能為應用該機制提供有效的參考。對此,提出一種基于printf函數的時序分析方法,對mbedOS中事件機制的實時性進行剖析。printf函數是嵌入式開發中被廣泛應用于調試的重要手段,有專家甚至認為printf是嵌入式發展過程中的長期基石,通過重要數據、程序運行狀態等調試信息的輸出,能有效加強初學者在嵌入式端的學習和理解、提高開發人員排除BUG的效率[9-10]。本文利用STM32L431RC芯片結合mbedOS程序框架,通過中斷與線程的同步實驗,在簡要分析事件機制響應調度的整個流程及其理論執行時間的基礎上,結合printf方法輸出測試了實際響應時間并進行分段解析,最后對影響事件機制響應時間的主要因素進行探究。通過printf對mbedOS事件機制實時性的深入剖析,能有效了解事件機制的實際響應時間,提供對任務的更精確控制[11],減少因超出任務截止時間導致某些難以預測結果的可能性,同時也為其他RTOS的實時性分析及有關應用提供技術基礎。
同步是一種協調任務執行順序避免發生時間相關差錯的機制,RTOS通常利用同步機制實現任務的有序調度來進行實時控制[12-13]。而事件是RTOS中同步機制中的一種重要手段,相比控制條件單一的信號量、互斥量等機制而言,事件機制可以通過事件的“與”和“或”多種邏輯組合來擁有更加豐富的控制功能。但事件本身只是用作一種控制信號,故無法類似消息隊列機制一樣傳輸數據,因此事件一般用于不需要傳送數據的場合。事件機制在早期出現的RTOS中就一直存在,無論是在1989年出現的MQX,還是之后陸續出現的諸如μC/OS、FreeRTOS及2014年Arm公司出品的mbedOS等RTOS中,事件機制始終被保留并不斷完善。不同RTOS對事件的稱呼不太一樣,例如MQX中事件被稱為事件組,μC/OS和FreeRTOS中稱為事件標志組,mbdeOS中稱為事件字或事件標志字等,但其原理和作用基本一致。
事件通常用一個32位的字來表示,字中的每一位都可表示一個事件,相互獨立且互不干擾??梢哉J為事件字是一個公共資源,與全局變量形式上很類似,但更方便RTOS對任務的控制和管理。使用事件機制進行同步時,一定會有一個任務在等待事件,然后由其他任務或中斷來設置事件位通知事件完成[14-17]。
對事件進行操作的常用函數一般有三種:等待函數、設置函數和清除函數。等待函數一般由被控制的任務使用,任務通過該函數進入等待事件狀態并進入阻塞;設置函數通常由其他任務或中斷使用,通過設置事件字的指定事件位(即事件字對應位進行置1)讓等待事件的線程退出阻塞狀態接受調度;清除函數則是清除指定的事件位(事件位清0)。在mbedOS中,常用wait_any()等待事件、set()設置事件、clear()清除事件,與其他RTOS中的事件函數相比,mbedOS中事件函數在功能上并未發生太大的變化,但基于ARM的OS體系,使得事件函數每次被調用時都會觸發相應的系統服務調用SVC(Supervisor Call)中斷或可掛起系統調用PendSV(Pendable Supervisor)中斷,從而進行對應的調度過程。這種保護機制能有效提高系統的穩定性,但在某種程度上也加大了進行時序分析的難度,如何在這些干擾因素下進行有效而精確的時序分析是很重要的。
本文對mbedOS中事件機制響應調度的實時性分析主要通過printf函數來實現。在嵌入式領域中,通常利用printf進行打樁調試來定位BUG,或是輸出有效的提示信息來輔助理解程序執行的動態過程。但是printf的問題在于輸出速度較慢,直接用于時序分析時會對系統的實時性能有較大的影響。經測試,在48 MHz的內核時鐘頻率下,輸出一個字符平均大約需要80 μs,可見輸出字符的操作本身會占用較多時間。這點在周期較長、粗糙的時序分析時或許不明顯,但對于實時性較高的RTOS,幾秒甚至是毫秒、微秒級的誤差就有可能導致較為嚴重的后果。故利用printf來進行精確的時序分析時,有必要通過主觀的編程手段來排除這部分誤差。
測試時通常根據一段代碼開始和結束這兩端的時間差來有效算出該段代碼的實際執行時間,為了避免在測試代碼兩端直接加printf帶來的較大時間誤差,可以利用全局變量保存代碼兩端時間的方式,將printf輸出放到測試代碼外執行,而全局變量賦值花費的時間是在納秒級,基本可以忽略其影響,這樣能保證得到較為精確的結果。
測試時間一般會基于一個統一的定時器,在mbedOS中,存在一個利用時間嘀嗒中斷來進行整體任務調度的定時器SysTick。該定時器是一個24位的遞減計數器,其中斷周期默認為1 ms,對應SysTick中的計數器需要計數48 000次。由于每次嘀嗒中斷會讓內核中嘀嗒計數結構體變量osRtxInfo.kernel.tick加1來保存嘀嗒次數,故根據嘀嗒計數和SysTick自身計數器的計數值便能夠有效計算出具體當前時間[18]?;赟ysTick定時器的時間具體計算方式為[osRtxInfo.kernel.tick×1 000+(48 000-SysTick->VAL)/48 000×1 000],其中SysTick->VAL為計數器對應值,時間單位為微秒。但要注意的是,在執行某些諸如SVC和PendSV等過程時,Systick中斷會被推遲執行,此時嘀嗒計數值osRtxInfo.kernel.tick不會發生變化,故只能根據SysTick計數器的值來算出1 ms之內的有效執行時間。
本文實驗選用的開發板型號為STM32L431RC,對應的集成開發環境為意法半導體(ST)公司推出的STM32CubeIDE 1.3.0開發工具,移植的工程框架基于5.15.1版本的mbedOS。該開發板是Cortex-M4內核,其RAM大小為64 KB,一般用來存放堆棧、靜態變量與全局變量等;另外,主要用于存放程序代碼、中斷向量表、常數等內容的Flash區大小為256 KB[19]。
測試工程的功能主要依靠串口中斷服務程序以及mbedOS啟動后依次創建的優先級為1的空閑線程、優先級為24的藍燈線程來實現。藍燈線程調用wait_any()等待藍燈事件即等待藍燈事件位被設置,從而反轉藍燈亮暗,同時用于printf輸出測試過程中保存在全局變量中的時間信息;低優先級的空閑線程保證在藍燈線程因等待藍燈事件進入阻塞后能占據CPU,確保MCU處于運行狀態;串口中斷服務程序始終開放,等待接收字符并進行判斷,如果接收到的字符為“b”,就調用set()設置藍燈事件位。測試工程的執行流程如圖1所示。

圖1 測試工程執行流程
事件響應調度的方式一般有兩種:一種是線程等待中斷設置事件位來響應PendSV調度;另一種是線程等待其他線程設置事件位來響應SVC調度。本文主要通過中斷與線程的同步實驗來具體剖析線程響應PendSV調度的實時性。
藍燈線程優先級最高,首先被SysTick中斷調度開始運行,調用wait_any()等待事件位被設置。在wait_any()過程中,藍燈線程被設置為等待事件位狀態且進入等待隊列(osRtxInfo.thread.wait_list)和事件阻塞隊列(osRtxEventFlags_t.thread_list),并從就緒隊列(osRtxInfo.thread.ready)中取出空閑線程開始運行。此時藍燈線程阻塞,等待系統發出信號(中斷設置事件位)來響應調度。
3.2.1理論響應時間分析
響應PendSV調度的整段過程是從串口中斷調用set()設置事件位開始,到等待事件位的藍燈線程被正式切換運行結束。根據編譯后的.lst文件,找到這段過程對應的代碼,其機器指令為1 680條。測試芯片的系統時鐘頻率為48 MHz,即一條機器指令執行時間為1/48 μs,故該過程對應的理論執行時間即響應時間約為35 μs。
3.2.2實際響應時間分析
實際響應時間會根據硬件因素有所不同,但不會偏離理論響應時間太多,可以基本確定實際響應時間是在1 ms之內,故可以利用系統內的SysTick時鐘有效測出響應PendSV調度的實際響應時間。
1) 總響應時間分析。分別在測試工程中串口中斷的set()前一句和藍燈線程的wait_any()后一句插入時間賦值語句,即將[osRtxInfo.kernel.tick×1 000+(48 000-SysTick->VAL)/48 000×1 000]賦值給對應的全局變量,并在藍燈線程中利用printf輸出兩次時間值及對應差值。進行百次輸出實驗,去除對應osRtxInfo.kernel.tick值不一樣的測試結果,這是因為在進行printf輸出時,藍燈線程已經被調度完成,osRtxInfo.kernel.tick可以正常增長,雖然printf輸出這段過程極為短暫,但依舊會產生極少數的osRtxInfo.kernel.tick值不同的結果,為使結果更加精確,排除這部分異常數據,得到時間差值的均值為41 μs,即該過程總執行時間約為41 μs。
2) 分段過程執行時間分析。
(1) 事件響應調度的過程說明。事件響應調度的整個過程為:調用set()設置事件位,藍燈線程接收到事件位被設置的信號,解除阻塞開始運行。調用set()是藍燈線程響應調度的開始,串口中斷調用set()設置事件位并掛起了PendSV中斷。set()調用結束后,繼續執行中斷的后續代碼,串口中斷結束后立即觸發PendSV中斷,跳轉執行PendSV中斷實際服務程序osRtxPendSV_Handler()開始調度處理。主要處理過程是調用事件處理函數osRtxEventFlagsPostProcess()和線程調度函數osRtxThreadDispatch()來改變線程狀態和隊列狀況,osRtxEventFlagsPostProcess()將已設置事件位的藍燈線程移出阻塞隊列和等待隊列并設置為就緒態放入就緒隊列,此時就緒隊列藍燈線程優先級最高,于是又通過osRtxThreadDispatch()從就緒隊列取出藍燈線程設置為激活態。處理結束后,等待到事件位被設置的藍燈線程已經解除阻塞準備運行,從osRtxPendSV_Handler()返回PendSV中斷,執行后續的上下文處理程序SVC_Context進行上下文保護和線程切換,PendSV中斷結束后,藍燈線程被正式切換運行。至此,藍燈線程響應調度結束。
(2) 時序分析。對于響應調度的藍燈線程來說,最關鍵的三個要素為:系統何時發出了使其調度的信號(設置事件位);系統是怎樣調度的,需要花費多長時間來進行調度處理;處理結束到正式被切換運行又需要耗費多長時間。除此之外,調度過程中通常還有一些參數驗證、類型轉換和條件判斷等操作,但這些一般不作考慮,不是關心的重點。
因此,設置事件位的set(),調度處理時調用的osRtxThreadListRemove()、osRtxThreadWaitExit()和osRtxThreadDispatch(),切換上下文的SVC_Context,這幾個過程無疑是測試的重點。對此,根據調度過程中系統的主要操作,將整段過程的運行時間分成7個時間段。過程分段結果如圖2所示。

圖2 事件響應調度過程分段
測試時依舊通過全局變量保存每段過程對應位置的時間點,最后通過藍燈線程輸出時間值及時間差大小。例如求取T0-T1段即set()執行的時間,可以定義全局變量T0、T1,在調用set()這句代碼的前后分別插入T0、T1的時間賦值語句,在藍燈線程中利用printf輸出T0、T1及兩者的差值。另外為了減小誤差,每次測試時只測時間段對應的兩個時間點,即只使用兩個全局變量保存Ti-T(i+1)兩點所在時間值,不同時進行多個時間段的測試處理。
對事件響應調度這段過程中不同時間段分別進行百次printf輸出實驗,排除異常數據后計算每段時間對應的平均值,結果如表1所示。7個時間段加起來的時間為11+8+1+7+3+8+3=41 μs,與測出的總執行時間一致,基本可以認為測出的時間數據是可信和正確的。T0-T1是觸發調度的開始,設置事件位,將PendSV掛起推遲執行;T2-T3、T3-T4、T5-T6三段為調度時主要的處理過程,主要為隊列的變化和線程狀態的改變;T6-T7即為調度處理結束,返回SVC_Context切換藍燈線程正式運行的時間。

表1 事件響應調度分段過程執行時間
以T0為時間基點,藍燈線程響應調度的整個時序過程如圖3所示,粗線條長度代表對應過程執行時間。可以看到,在PendSV調度未開始時,調用set()已耗費了較多時間,約有11 μs,這是由于se()的執行經歷了好幾層的嵌套調用,其調用順序為set()→osEventFlagsSet()→isrRtxEventFlagsSet(),在isrRtxEventFlagsSet()中才會調用子函數EventFlagsSet()設置事件位和osRtxPostProcess()掛起PendSV中斷。PendSV開始后,進行一些調度相關的處理,主要調整了隊列和線程狀態,并在最后進行上下文切換工作,這段過程共耗費了30 μs。其中,在T3-T4和T5-T6階段隊列的進出操作花費了較多時間,這里藍燈線程在T3-T4階段先進入了就緒隊列,由于優先級最高在T5-T6階段又被調出了就緒隊列并激活運行,是否可以考慮直接通過優先級的比較判斷其優先級最大時直接激活運行,避免無意義的進出隊列操作,從而減少整個調度過程的執行時間,這一點值得思考并有待進一步研究。

圖3 事件響應PendSV調度時序圖
事件機制的響應時間主要受兩方面影響,客觀角度上可以通過編譯優化來大幅度提高代碼執行效率以減少響應時間來增強實時性,主觀角度上根據程序功能的不同需求會在某些方面影響響應速度。
3.3.1編譯優化的影響
編譯優化可以通過不同優化等級對編譯器的影響,在編譯時對代碼進行不同程度的優化。使用編譯優化選項會影響編譯耗費的時間、程序占用的Flash和RAM大小、代碼的運行效率等,甚至有可能會導致代碼運行異常,故通常需要根據實際情況來設置。本文只分析不同優化等級對代碼執行效率的影響,分別測試不同優化等級下事件機制的響應時間,即調用set()設置事件到等待事件的線程結束等待被調度運行這段過程的總時間。在STM32CubeIDE中,提供了7種優化等級(optimization level),從低到高分別為:-O0、-Og、-O1、-O2、-O3、-Os、-Ofast。-O0為默認不優化,不優化情況下已經求出響應時間約為41 μs,按照同等方式分別求出其他優化等級下對應響應時間,同樣進行百次輸出實驗并取平均值,結果如表2所示。

表2 不同優化等級下的事件機制響應時間
不同等級的優化選項效果不同:-O1主要對代碼的分支、常量、表達式等進行優化;-Og可以看作是O0.5,是在O1的基礎上,去掉了影響調試的優化;-O2會嘗試更多的寄存器級的優化以及指令級的優化;-Os啟用了所有-O2優化選項但排除了增加目標文件大小的選項,主要用于縮減代碼尺寸;-O3在-O2的基礎上進行更多的優化,例如使用偽寄存器網絡、普通函數的內聯等;-Ofast是在-O3的基礎上,添加了一些非常規優化,無視嚴格的標準合規性,一般不推薦使用。
可以看到隨著優化等級越高,代碼的執行效率越高,即事件機制的響應時間越小。通過編譯優化,響應時間最高減少到15 μs,相比不優化時的41 μs,響應速度提高了一倍多,基于事件機制的mbedOS實時性得到了有效提高。但要注意的是,代碼的優化通常是以程序的可調試性為代價的,優化度越高,可調試性越低,故通常要根據需求來進行合理設置。
3.3.2程序功能的影響
前面已經說明,中斷中調用set()結束后會執行中斷后續未執行的語句,PendSV被掛起直到中斷結束才會觸發。從set()到PendSV開始這段過程,執行時間主要受set()之后的代碼數量影響,本文測試工程中set()為中斷處理函數中的最后一個操作,基本可以看作set()結束后中斷就已經結束,PendSV立即被觸發,但實際應用時中斷里可能會根據需要進行一些其他操作比如添加printf輸出提示,因此這部分執行時間受主觀需求影響是不確定的。
另一方面,由圖3可以看到,在執行PendSV調度處理時,執行時間主要耗費在進隊列或出隊列等操作上,T2-T3、T3-T4和T5-T6的時間和為16 μs,超過了PendSV總執行時間30 μs的一半。本文測試工程只進行了藍燈線程與空閑線程間的調度示例,不同隊列中存在的線程數不會超過兩個,這是為了排除其他線程的干擾,這樣在最簡單的條件下才能測出最準確的響應時間。但實際應用時往往會有多個任務線程,隊列中線程的位置會根據不同場合發生變化,例如同時有多個線程等待事件,事件阻塞隊列會有多個線程,而出隊列的只會是已經設置事件位的對應線程,不同位置進出隊列所耗費的時間自然會有所變化,從而影響整體的響應時間。特別要注意的是T5-T6階段就緒隊列的改變,線程調度函數osRtxThreadDispatch()是根據就緒隊列中線程優先級的比較來激活確定下一運行線程的。只有就緒隊列最高優先級線程的優先級高于正在運行的線程優先級,才會進行線程的切換,將優先級更高的線程調度運行。因此,基于事件機制等待響應調度的線程,優先級通常要設置為比其他線程高一點,這樣才能在等待到事件位被設置后,立即接受調度運行;反之優先級比其他線程低的話,在接收到事件位已設置的信號后,可能被其他等待運行的高優先級線程搶占,響應時間無法估測,不再有實時的特點。
本文對mbedOS中事件機制響應調度的實時性進行了剖析。首先提出基于printf函數的時序測試方法,然后分析得到事件響應調度的理論時間為35 μs,滿足提出的時序測試條件,在簡要說明了響應調度過程的基礎上,對實際響應時間進行了測試,研究表明線程等待中斷設置事件位來響應PendSV調度所需的時間約為41 μs,其中隊列操作花費時間占據比例最大,最后進一步說明了影響事件機制響應時間的主要因素有編譯優化和程序功能兩方面,其中編譯優化最高可將實際響應時間減少到15 μs。通過printf對事件響應調度實時性的深入剖析,不僅為mbedOS中事件機制的精確使用提供參考,也為剖析其他RTOS的實時性提供一定的借鑒。