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