详解基于 Cortex-M3 的任务调度(上)
文章目錄
- 什么是任務
- 任務及其內存結構
- 上下文切換
- CM3 的寄存器組
- CM3 的 CONTROL 寄存器
- 雙棧
- CM3 的中斷
- 入棧
- 取向量
- 更新寄存器
- 異常返回
- 切換的時機
- 切換
- 非基級線程模式(補充材料)
- 代碼
什么是任務
對于嵌入式 RTOS,我覺得任務(task) 其實是線程。為什么這樣說呢?首先,有幾個知識點要明確:
其次,每個進程都有其自己的存儲空間,它們是互相隔離的。比如你在瀏覽網頁,瀏覽器崩潰了,但并不會影響到音樂播放器。但是對于線程來說,資源是共享的,所以對于共享資源的訪問就會存在競爭問題,于是就產生臨界區,互斥、信號量等概念。如果一個線程崩潰了,極大可能會影響到該進程中的其他線程。
對于 MCU 上的資源,每個任務都是共享的,可以認為是單進程多線程模型。MCU一般沒有內存管理模塊,這樣無法很好地保證進程的安全,這也是當某個任務跑飛會導致整個程序崩潰的原因。
通常認為,嵌入式系統在運行時只有一個進程,而把這個進程進行分解之后的那些程序模塊,由于沒有獨立的內存空間,實質上就是線程。在 μC/OS-II 中,把這樣的線程叫做任務。
直觀上來講,
任務及其內存結構
既然是任務調度,那就需要系統把任務管理起來,系統管理任務的數據結構叫做任務控制塊(TCB)
以 μC/OS-II 為例,任務控制塊記錄一個任務的各個屬性,相當于是任務的身份證。TCB 中有兩個指針特別重要:
- 指向任務的指針:在任務初始化的時候,這個指針指向任務的代碼入口
- 指向任務堆棧的指針:指向任務的棧。每個任務都有自己的棧,用來保存局部變量,還有寄存器的快照。在這些寄存器中,最重要的就是 PC,指向任務當前運行的代碼
系統中一般會有多個任務,這些任務的 TCB 用鏈表串起來,也可以用數組。
上下文切換
要進行任務調度,就要進行上下文切換。
先看看線程是如何對付中斷的。當線程在執行時,所做的事情就是從存儲器中取指令、譯碼、執行。在整個過程中,CPU 里寄存器的值會不斷更新。此時如果一個中斷來了,那么 CPU 就要把核心寄存器的值先保存到內存的某個地方(比如這個線程的棧),然后響應中斷。等中斷執行完了,再把剛才保存的值加載到對應的寄存器,從剛才中斷的地方繼續執行(由程序計數器 PC 記錄)。
任務切換也是這個道理,如果要把當前任務 A 換出,就要先找到 A 任務的棧,把當前的寄存器信息保存到棧上,然后找出要換入的任務 B,再找到 B 任務的棧,把棧上保存的寄存器值恢復到寄存器里,最后讓 B 開始運行。
前面做了很多鋪墊,接下來我們就結合具體的一款 CPU 來講任務調度。
CM3 的寄存器組
注意,在 CM3 處理器內核中共有兩個堆棧指針,于是也就支持兩個堆棧。當引用 R13(或寫作 SP)時,引用到的是當前正在使用的那一個,另一個必須用特殊的指令來訪問(MRS,MSR指令)。這兩個堆棧指針分別是:
- 主堆棧指針(MSP), 或寫作 SP_main。這是缺省的堆棧指針,它由 OS 內核、異常服務例程以及所有需要特權訪問的應用程序代碼來使用。
- 進程堆棧指針(PSP), 或寫作 SP_process。用于常規的應用程序代碼(不處于異常服用例程中時)。
要注意的是,并不是每個程序都要用齊兩個堆棧指針才算圓滿。簡單的應用程序只使用 MSP 就夠了。
在本文的示例代碼中,采用了雙棧。
CM3 的 CONTROL 寄存器
復位后,CONTROL[0]=0 ,也就是說線程模式處于特權級。
Cortex-M3 處理器支持兩種處理器的操作模式,還支持兩級特權操作。
兩種操作模式分別為:handler 模式和線程模式(thread mode)。引入兩個模式的本意,是用于區別普通應用程序的代碼和異常服務例程的代碼。
兩級特權分別是:特權級和用戶級。這可以提供一種存儲器訪問的保護機制,使得普通的用戶程序代碼不能意外地、甚至是惡意地執行涉及到要害的操作。
示例代碼中有一句:
__set_CONTROL(0x3); // Switch to use Process Stack, unprivileged state
意思是強行切換到用戶級,且用 PSP(后面馬上就說)
雙棧
我們已經知道了 CM3 的堆棧有兩個:主棧和進程棧,CONTROL[1] 決定如何選擇。
當 CONTROL[1]=0 時,只使用 MSP,此時用戶程序和異常 handler 共享同一個棧,這也是復位后的缺省使用方式。
我們的示例代碼采用了雙棧。
當 CONTROL[1]=1 時,線程模式將不再使用 MSP,而改用 PSP(注意:handler 模式永遠使用 MSP)。這樣做的好處在哪里?原來,在使用 OS 的環境下,我們想讓 OS 內核僅在 handler 模式下執行,用戶程序僅在用戶模式下執行,這種雙棧機制的好處是:萬一用戶棧崩潰了,并不會累及 OS 的棧。
在雙棧模式下,進入異常時的自動壓棧使用的是進程棧,進入異常后會自動改為 MSP,退出異常時切換回 PSP,并且從進程棧上彈出數據。 如下圖所示:
CM3 的中斷
任務切換一般是在中斷中進行的,所以了解 CPU 的中斷過程非常必要。
當 CM3 開始響應一個中斷時,會在它小小的體內奔涌起三股暗流:
- 入棧: 把 8 個寄存器的值壓入棧
- 取向量:從向量表中找出對應服務程序的入口地址
- 更新寄存器:選擇堆棧指針 MSP/PSP,更新堆棧指針 SP,更新連接寄存器 LR,更新程序計數器 PC
好,我們一個一個來說。
入棧
自動入棧的寄存器有 8 個,見表 9.1:
取向量
當數據總線(系統總線)正在為入棧操作而忙得風風火火時,指令總線(I-Code)可不是袖手旁觀——它正在為響應中斷緊張有序地執行另一項重要的任務:從向量表中找出正確的異常向量,然后在服務程序的入口處預取指。由此可以看到各自都有專用總線的好處:入棧與取指這兩個工作能同時進行。
更新寄存器
在入棧和取向量操作完成之后,執行服務例程之前,還要更新一系列的寄存器:
- SP:在入棧后會把堆棧指針(PSP 或 MSP)更新到新的位置。在執行服務例程時,將由 MSP 負責對堆棧的訪問。
- PSR:更新 IPSR 位段的值為新響應的異常編號。
- PC:在取向量完成后,PC 將指向服務例程的入口地址。
- LR:在出入 ISR 的時候,LR 的值將有新意義,這種特殊的值稱為“EXC_RETURN”,在異常進入時由系統計算并賦給 LR,并在異常返回時使用它。EXC_RETURN 的值除了最低 4 位外全為 1,而其最低4位則有另外的含義(見表9.3和表9.4)。
以上是在響應異常時通用寄存器的變化。另一方面,在 NVIC 中,也會更新若干個相關寄存器。例如,新響應異常的懸起位將被清除,同時其活動位將被置位。
異常返回
當異常服務例程執行完畢后,需要很正式地做一個“異常返回”的動作序列,從而恢復先前的系統狀態,才能使被中斷的程序得以繼續執行。從形式上看,有 3 種途徑可以觸發異常返回序列,如表 9.2 所示。而不管使用哪一種,都需要用到先前儲到 LR 的 EXC_RETURN。
在示例代碼中,使用的是第一個方法:
BX LR // Return
在啟動了中斷返回序列后,下述的處理就將進行:
切換的時機
已經說了,任務切換在中斷中進行,但是在哪個中斷呢?
例如,一個系統中有兩個任務,上下文切換被觸發的場合可以是:
- 執行一個系統調用(SVC 異常)
- 系統滴答定時器(SYSTICK)中斷,(輪轉調度中需要)
讓我們舉個簡單的例子。假設有這么一個系統,里面有兩個就緒的任務,并且通過 SysTick 異常啟動上下文切換。如圖 7.15 所示。
上圖是兩個任務輪轉調度的示意圖。但若在產生 SysTick 異常時正在響應一個中斷,則 SysTick 異常會搶占其 ISR。在這種情況下,OS 是不能執行上下文切換的,否則將使中斷請求被延遲,而且在真實系統中延遲時間還往往不可預知——任何有一丁點實時要求的系統都決不能容忍這種事。因此,在 CM3 中也是嚴禁沒商量——如果 OS 在某中斷活躍時嘗試切入線程模式,將觸發用法 fault 異常(但是有例外情況,感興趣的讀者可以看本文末尾的“非基級線程模式”)。
為解決此問題,早期的 OS 大多會檢測當前是否有中斷在活躍中,只有在無任何中斷需要響應時,才執行上下文切換(切換期間無法響應中斷)。然而,這種方法的弊端在于,它可以把任務切換動作拖延很久(因為如果搶占了 IRQ,則本次 SysTick 在執行后不得作上下文切換,只能等待下一次 SysTick 異常),尤其是當某中斷源的頻率和 SysTick 異常的頻率比較接近時,會發生“共振”,使上下文切換遲遲不能進行。
現在好了,有 PendSV 來完美解決這個問題。PendSV 異常會自動延遲上下文切換的請求。為實現這個機制,需要把 PendSV 編程為最低優先級的異常。若 OS 需要執行上下文切換,它將懸起一個 PendSV 異常,并在 PendSV 異常內執行上下文切換。如圖 7.17 所示
解釋:
遺留問題:在調試的時候,我認為 SysTick 異常返回后會立刻進入 PendSV 服務例程,應該看到 Tail chaining 現象,但測試結果是 SysTick 中斷處理后返回到了任務 B,執行了一點點,馬上進入 PendSV
切換
具體的切換如下圖所示。
我給出的解釋:
非基級線程模式(補充材料)
在 CM3 中,原則上異常服務程序要在 handler 模式下執行,但是也允許在服務例程中切換到線程模式。通過設置 NVIC 配置與控制寄存器的“非基級線程模式允許”位(NONBASETHRDENA,位偏移:0),可以在服務例程中把處理器切換入線程模式。為什么要這么做?如果中斷服務例程是用戶程序的一部分,可能需要讓它在線程模式下執行,以限制它訪問特權級下的資源,此時可以讓此功能派上用場。
如果使用此功能,則需要手工調整堆棧指針,還要重建堆棧中的數據。這種乾坤大挪移可是高度危險的作業,一不小心就很容易把整個系統弄垮。所以必須格外嚴肅地對待。另外,在使用時,系統設計者還必須保證服務例程能正確地返回。因為在線程模式下是不允許作中斷返回的,所以必須用一點手腕才行。如果放任不管,則中斷無法退出,這會永遠阻塞其它同級和更低優先級中斷。通常,由系統軟件負責完成這種工作。
此節內容和本文主旨無關,所以僅放一個圖片在這里,提示讀者“居然可以如此操作”!
代碼
囿于篇幅,代碼下一篇博文再講。
歡迎讀者批評指正。
參考資料
【0】RTOS中的任務是線程、進程、還是協程?-面包板社區
【1】任務、進程和線程的區別(轉自博客園) - 雷明 - 博客園
【2】《Cortex-M3 權威指南 》
【3】《The De?nitive Guide to ARM Cortex-M3 and Cortex-M4 Processors(Third Edition)》
總結
以上是生活随笔為你收集整理的详解基于 Cortex-M3 的任务调度(上)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python人脸识别截图_Python
- 下一篇: 详解基于 Cortex-M3 的任务调度