Cortex-M3 系列處理器是基於 ARMv7-M 架構的處理器,應用非常廣泛,為了能夠深入的分析在此平台上跑 RTOS 的各種細節,所以有必要寫一篇關於 CM3 處理器的結構相關的文章(CM4 類似),在 OS 調度初始化、系統調用、進程調度等方面的細節均是和具體處理器息息相關,所以先讓我們來看看 CM3 處理器的一些特征;
1、寄存器組
如下所示,CM3 處理器擁有 R0~R15 一共 16 個內部寄存器,其中:
R0~R12 稱之為通用寄存器。在這 13 個寄存器中,根據指令集訪問的特性,R0~R7 是所有指令都可以訪問,而 R8~R12 只有很少的 16 位的 Thumb 指令可以訪問,32 位的 Thumb-2 不受限制;
R13 默認情況下被用做棧指針;棧指針分為 MSP 和 PSP,后面會詳細描述;
R14 默認情況作為 LR,也就是鏈接寄存器,當程序調用其他函數后,此寄存器保存了返回地址,使得子程序執行完畢后,得以返回;
R15 默認作為 PC 指針;
2、特殊功能寄存器組
CM3 中,除了上述 16 個寄存器以外,還有幾個特殊的寄存器組:
xPSR:狀態寄存器;
PRIMASK:中斷屏蔽寄存器;
FAULTMASK:中斷屏蔽寄存器;
BASEPRI:中斷屏蔽寄存器,按照優先級進行屏蔽;
CONTROL:處理器模式和堆棧選擇;
他們的含義如下:
下面我們一個一個看
2.1、xPSR
xPSR 是 Program Status Register 程序狀態寄存器的意思,前面有個 x 代表他是由 3 個小的寄存器構成:
APSR:應用程序狀態寄存器;
IPSR:中斷程序狀態寄存器;
EPSR:執行程序狀態寄存器;
它們 3 個一起叫做程序狀態寄存器,xPSR 的組成是 32 位的寄存器,在這 32 位中,APSR、IPSR、EPSR 各占一部分:
藍色部分是 APSR,占領了高 27bit ~ 31bit
紫色部分是 EPSR,占領了高 9bit ~ 26bit
綠色部分是 IPSR,占領了低 0bit ~ 8bit
如果寫匯編的話呢,APSR 的 N、Z、C、V、Q 這些標志會被使用到,詳見指令集部分;
IPSR 中存儲了當前服務的中斷號;
2.2、PRIMASK
這個是只有單一 bit 的寄存器。當它被置位 1 后,就關掉了所有可屏蔽的異常(中斷),只剩下 NMI 和 HardFault 可以響應。缺省值是 0,表示沒有屏蔽中斷;
PRIMASK 也可以叫一級中斷開關,這里值得注意的是,即便是通過 PRIMASK 寫 1 屏蔽了中斷,但是之后觸發的中斷依然會被 Pending 住,只不過得不到執行,如果在 PRIMASK 為 1 的情況下,有中斷處於 Pending 狀態,此刻往 PRIMASK 寫 0,那么立馬會進入 ISR;也就是說,PRIMASK 只是屏蔽掉中斷,而並不是不讓中斷源產生中斷!
2.3、BASEPRI
這個寄存器最多有 9 bit(由表達優先級的位數決定)。它定義了被屏蔽優先級的閾值;換言之,當這個寄存器被設置某個數值后,所有優先級號大於等於該值的中斷都被關閉(優先級號越大,優先級越低);默認值是0,也就是不關閉任何中斷;
2.4、FAULTMASK
這也是只有 1 bit 的寄存器,當設置為 1 的時候,只有 NMI 才能夠響應,其他所有的異常,甚至是 HardFault 也不響應,默認值是 0,也就是都響應;
2.5、CONTROL
根據名字就知道,這是個控制寄存器,這個控制寄存器由兩個 bit 構成,我們稱之為 CONTROL[0] 和 CONTROL[1];
CONTROL[0] 用來指明運行的 CPU 的特權級別;
CONTROL[1] 用來指明使用的堆棧類型;
稍后會對堆棧指針和 CPU 特權級別以及線程模式/Handler 模式做說明;
2.6、特殊寄存器組訪問方式
上述的特殊寄存器組 xPSR、PRIMASK、FAULTMASK、BASEPRI 以及 CONTROL 都是 CM3 內核的寄存器,CM3 定義的訪問他們的方式是只能通過 MRS 和 MSR 指令,比如:
MRS R0, BASEPRI ; 讀取 BASEPRI 到 R0
MRS R0, FAULTMASK ; 讀取 FAULTMASK 到 R0
MRS R0, PRIMASK ; 讀取 PRIMASK 到 R0
MRS R0, CONTROL ; 讀取 CONTROL 到 R0
MSR BASEPRI, R0 ; 將 R0 寫入 BASEPRI
MSR FAULTMASK, R0 ; 將 R0 寫入 FAULTMASK
MSR PRIMASK, R0 ; 將 R0 寫入 PRIMASK
MSR CONTROL, R0 ; 將 R0 寫入 CONTROL
其實,為了快速的開關中斷,CM3 還專門定義了一條叫 CPS 的指令,有如下 4 種用法:
CPSID I ;PRIMASK=1, ;關中斷
CPSIE I ;PRIMASK=0, ;開中斷
CPSID F ;FAULTMASK=1 ;關異常
CPSIE F ;FAULTMASK=0 ;開異常
3、處理器工作模式
在 2.5、CONTROL 章節提到了工作模式和特權等級這里就需要說明一下 CM3 處理器的工作模式和特權等級做一下說明;
3.1、運行等級
CM3 有兩種運行等級:
1、特權等級;
2、用戶等級;
處理器在特權等級(Privilege )下,代碼可以訪問任意的寄存器;
處理器在用戶等級(User)下,對系統控制寄存器 SCS(System Control Space)和特殊寄存器(通過 MSR/MRS 訪問的寄存器)的訪問將被阻止(除了 APSR,因為 APSR 是專門用於給應用程序標志位的);如果在用戶級下,訪問了上述寄存器,那么 HardFault 伺候;
SCS 就是那些 NVIC、SCB(系統控制寄存器)這些的玩意;
也就是說,特權級和用戶級的區別在於,訪問 Core 寄存器的限制!
3.2、運行模式
CM3 的運行模式分為兩種:
1、Thread 模式:也就是說的線程模式
2、Handler 模式;
簡言之,線程模式就是跑普通代碼時候處理器所處的模式,Handler 模式就是異常的時候處理器的模式;
3.3、運行等級 VS 運行模式
運行等級和運行模式之間有如下關系:
對這個圖的解釋為:
異常 Handler 一定是 Handler 模式,並且一定是特權級;
正常住應用代碼,即,非 ISR 運行的程序(線程模式)可以是特權級(訪問所有的寄存器),也可以運行在用戶級(寄存器訪問受限)
正常情況下,系統復位后,處理器處於特權級+線程模式(因為系統復位后,一般都需要先配置系統寄存器,狀態等);
在配置完本機需要的系統寄存器后,可以選擇往 CONTROL[0] 寫1,進入用戶線程模式,此刻系統寄存器不再接受改動,一旦進入了用戶線程模式,用戶級下的代碼不能再試圖修改 CONTROL[0] 來返回特權模式;
但是用戶線程模式下,可以通過觸發一個異常 Handler,進入 Handler 模式,所有 Handler 模式一定都是特權模式,可以在這個模式下去更改 CONTROL[0],讓 Handler 返回的時候再次進入特權的線程模式;
狀態轉換如下所示:
典型的時序如下所示:
當在線程特權模式進入中斷后,處理器的特權等級還是處於特權級,僅僅有線程模式變化為 Handler 模式區別,如下:
當在線程用戶模式進入中斷后,處理器在 Handler 下處於特權級,退出 Handler 后,變化為用戶級,如下:
4、棧
CM3 處理器使用的是 “向下生長的滿棧” 模型,什么叫向下生長的滿棧呢?首先我們聊一下處理器棧模型,棧模式分為 4 種:
1、向下生長的滿棧;
2、向下生長的空棧;
3、向上生長的滿棧;
4、向上生長的空棧;
向下生長的含義是:棧由高地址向低地址生長;
向上生長的含義是:棧由低地址向高地址生長;
滿棧的含義是:棧指針pos指向的是一個空的 slot,也就是下一個可用的空閑。便於壓棧,而彈的時候需要彈pos-1或者pos+1
空棧的含義是:棧指針pos指向的是一個有可用數據的 slot,也就是最后一個使用的空間。便於彈棧,而壓的時候需要壓pos+1或者pos-1。
OK,CM3 處理器使用了“向下生長的滿棧”模型,R13 默認作為了 SP 堆棧指針;
既然是這樣,在簡單的應用場景下,那么在初始化堆棧指針的時候呢,最為安全的辦法是,將 SP 指針初始化到 SRAM 的最高位置(也就是末尾);代碼和數據從 SRAM 起始地址開始遞增,這樣便最大程度上避免數據互踩;
CM3 為了能夠更好的支持 OS,支持了雙堆棧機制,雙堆棧不是指的有兩個堆棧,而是說系統中支持兩個堆棧指針(但是當前使用的 SP 只能是其中之一):
1、MSP:主堆棧指針;
2、PSP:用戶堆棧指針;
還記得 CONTROL 寄存器么,它是 2 bit 構成,CONTROL[1] 用來決定使用哪個堆棧!
CONTROL[1] = 0 的時候,使用 MSP
CONTROL[1] = 1 的時候,使用 PSP
在簡單的應用場景下,如果裸機的情況下,不打算對系統進行任何保護,CM3 上電后默認系統跑在特權的線程模式,默認使用 MSP 作為 SP 堆棧指針(即 CONTROL[1] = 0 );中斷/異常 Handler 下也使用 MSP 作為 SP 堆棧指針;
當 CONTROL[1] = 1 的時候,線程模式不在使用 MSP,而是使用 PSP(Handler 模式永遠使用 MSP);
那么什么時候使用 PSP 呢?比如你要跑一個 RTOS,多任務,那么每個任務都需要有自己的堆棧,此刻 PSP 就可以用起來了;PSP 將用戶堆棧和系統堆棧 MSP 分開,防止用戶堆棧破壞系統 OS 堆棧;
在這種情況下的 PSP 與 MSP 切換,是硬件自動完成並壓棧的,無需軟件干預;
在帶 OS 情況下,OS 可以手動壓棧彈棧,修改 PSP 來達到切換任務上下文目的;
訪問 MSP 和 PSP 也需要通過使用 MRS、MSR指令來完成:
MRS R0, MSP ; 讀取 MSP 指針到 R0 MSR MSP, R0 ; 寫 R0 的值到 MSP MRS R0, PSP ; 讀取 PSP 指針到 R0 MSR PSP, R0 ; 寫 R0 的值到 PSP
5、指令集
cortex-M3 使用的是 Thumb-2 指令集
指令集部分內容較多,請參考 Cortex-M3 權威指南指令集章節;以后將會將常用的指令(STR, LDR, LDMIA, STMIA等)拿出來分析;
6、中斷/異常向量表
CM3 的中斷/異常依賴於一個異常向量表,0~15 的編號為系統所用,大於 16 的編號為芯片公司自行定義的中斷,最大支持到中斷標號 255(一般用不到那么多);
這里主要分為幾類:
1、Reset Handler:復位信號;
2、NMI:不可屏蔽信號,通過接 NMI 引腳;
3、系統各種 fault:包括 HardFault,BusFault,MemManageFault,UsageFault;
4、SVC 系統調用;
5、PendSV:給 OS 調度預留;
6、IRQ #xxx:芯片公司定義;
既然稱之為 “中斷向量表”,那么它就是一張軟硬件約定好的一個表,默認地址放在 0x0000_0000 開始(0x0000_0000 為 MSP,Reset Handler 放在 0x0000_0004),當發生對應中斷/異常的時候,CPU 到這張表對應的地址去獲取 ISR 的入口,並跳轉到對應的 ISR 執行;
在 CM3 處理器中,實現了一個叫 NVIC 的東西,全名叫中斷向量嵌套控制器;在軟件層面,它是以一組寄存器的形式體現出來,軟件可以修改 NVIC 寄存器,實現中斷優先級,中斷使能,中斷屏蔽,清除 Pending,手動 Pending 等操作;
NVIC 能夠支持中斷嵌套,即高優先級的中斷搶占低優先級的中斷(注意,都是用的是 MSP),但自己無法搶占自己;
更多的 NVIC 相關的東西不在多說,配合權威指南,通俗易懂;
7、中斷/異常響應序列
當系統發生中斷/異常的時候,CM3 處理器會:
1、入棧:將 8 個寄存器的值壓入棧;
2、取向量:從向量表中獲取對應中斷的 ISR 入口地址;
3、取出MSP,更新到棧指針 SP 中,更新鏈接寄存器 LR為EXC_RETURN,更新 PC為中斷函數;
入棧就是在進入中斷/異常服務程序之前的現場保存,硬件自動將 xPSR、PC、LR、R12、R3、R2、R1、R0 壓入棧,如果當中斷/異常發生時刻,正在使用 PSP,則壓入 PSP;否則壓入 MSP;一旦進入 ISR,那就一直使用 MSP;
7.1、中斷/異常入棧
假設准備入棧的時候,SP 的值為 N,那么在入棧順序如下所示(由於處理器流水線,自動入棧過程中寫入的時間順序和空間順序並不是一致的)
從存儲序列的空間順序來講,是表從上到下的順序,時間順序是 PC,xPSR.... 的順序;
CM3 這樣做,也是有原因,先保存 PC 和 xPSR 可以更早的啟動 ISR 的指令預取(因為需要修改 PC),同時也可以在早起就更新 xPSR 的 IPSR 的值;
R0~R3 和 R12 入棧了,那其他的 R4~R11 呢,在 ARM 的 C 語言標准函數調用約定中(AAPCS)編譯器優先使用入棧的寄存器來保存中間結果,如果真的用到了 R4~R11,編譯器生成代碼來 push 它們;
7.2、取向量
在數據總線正在入棧操作的同時,指令總線從向量表中找出對應的 ISR 的入口,這兩者同時進行;
7.3、更新寄存器
當上述兩步完成之后,還需要更新一些寄存器:
SP:入棧后,把棧指針更新到新的位置,在 ISR 中使用 MSP;
xPSR:更新 IPSR 為對應的異常編號;
PC:取向量完成后,PC 將指向 ISR 的入口;
LR:在出入 ISR 的時候,LR 的值不再是我們之前理解的鏈接寄存器的含義,此刻的 LR 稱為 EXC_RETURN;在異常進入的時候,由系統計算賦值給 LR 寄存器,在異常返回的時候使用它;
7.4、異常返回值 EXC_RETURN
當進入 ISR 的時候,LR 將被賦予新的含義:EXC_RETURN;這個是高 28 位全部為 1,只有 [3:0] 有含義;
當異常服務程序將這個值送給 PC,就意味着啟動處理器的中斷返回序列
如果主程序在線程模式並使用 MSP 的時候進入 ISR,則 EXC_RETURN=0xFFFF_FFF9
如果主程序在線程模式並使用 PSP 的時候進入 ISR,則 EXC_RETURN=0xFFFF_FFFD
如果當前運行在一個 ISR,此刻來了優先級更高的 ISR,則 EXC_RETURN=0xFFFF_FFF1
線程模式 + MSP 進入 ISR1 的時候,LR 被設置成為了 0xFFFF_FFF9,因為返回的時候是線程模式 + MSP
此刻被 ISR2 嵌套了,所以 LR 更新為了 0xFFFF_FFF1
線程模式 + PSP 的時候進入 ISR1,此刻 LR 更新為 0xFFFF_FFFD,因為返回的時候,是線程模式 + PSP
此刻 ISR2 優先級更高,嵌套了 ISR1,所以 LR 更新為 0xFFFF_FFF1
8、SVC 和 PendSV
這兩個 IRQ 與操作系統相關,所以拉出來單獨聊聊;
玩過 ARM7 的都知道,有一個指令叫 SWI,軟件中斷,SVC 和 SWI 是一樣的,主要的目的是用來呼叫系統調用,進入操作系統內核;一般的,操作系統不允許讓用戶態的程序直接訪問硬件(防止破壞),如果用戶態的軟件要訪問硬件,需要通過系統調用(在 Linux 上的 open、write,read,ioclt 這些)進入內核態;那么這個 SVC(SWI)就是呼叫系統調用的方式;
這種方式使得用戶代碼和具體硬件無關,硬件全部交給 OS;
SVC 只是作為一個封皮,通過系統調用,進入 SVC Handler 特權級的 Handler 模式;
SVC 異常通過執行 SVC 指令產生,該指令需要一個 8 位的立即數充當系統調用號,SVC 異常的 ISR 會去拿出此立即數,從而判斷本次的系統調用具體是要呼叫哪種系統調用函數(Open,Write,Read 的調用號不一樣);比如:
SVC 0x03; 調用 3 號系統服務
注意:這個 8 位的立即數,被封裝在指令本身中,就像上面的例子,呼叫 3 號系統服務,這個 3 被封裝在觸發這個 SVC 異常的 SVC 指令中;因此,在 SVC 的 ISR 中,需要讀取本次觸發 SVC 異常的 SVC 指令,並且提取出 8 位立即數的位段,來知道系統調用號,提取的代碼如下:
首先是一段匯編,通過判斷 EXC_RETURN 的值來判斷是 PSP 還是 MSP:
__asm void SVC_Handler(void) { // 匯編操作,用於提出堆棧幀的起始位置,並放到R0中,然后跳轉至實際的SVC服務例程中 IMPORT svc_handler TST LR, #4 ITE EQ MRSEQ R0, MSP MRSNE R0, PSP B svc_handler }
然后將 SP 棧指針放到 R0 中,跳轉到 SVCHandler_main:
// “真正”的服務函數,接受一個指針參數(pwdSF):棧的起始地址。 // pwdSF[0] = R0 , pwdSF[1] = R1 // pwdSF[2] = R2 , pwdSF[3] = R3 // pwdSF[4] = R12, pwdSF[5] = LR // pwdSF[6] = 返回地址(入棧的PC) // pwdSF[7] = xPSR unsigned long svc_handler(unsigned int* pwdSF) { unsigned int svc_number; unsigned int svc_r0; unsigned int svc_r1; unsigned int svc_r2; unsigned int svc_r3; int retVal; //用於存儲返回值 svc_number = ((char *) pwdSF[6])[-2]; // 沒想到吧,C的數組能用得這么絕! svc_r0 = ((unsigned long) pwdSF[0]); svc_r1 = ((unsigned long) pwdSF[1]); svc_r2 = ((unsigned long) pwdSF[2]); svc_r3 = ((unsigned long) pwdSF[3]); printf (“SVC number = %xn”, svc_number); printf (“SVC parameter 0 = %x\n”, svc_r0); printf (“SVC parameter 1 = %x\n”, svc_r1); printf (“SVC parameter 2 = %x\n”, svc_r2); printf (“SVC parameter 3 = %x\n”, svc_r3); //做一些工作,並且把返回值存儲到retVal中 pwdSF[0]=retVal; return 0; } //注意,這個函數返回的其實不是0!進一步地,灰色的文字只是用於哄編譯器開心的,具體參考Cortex-M3權威指南P169
SVCHandler_main 提取 svc_number 這個地方和 CM3 處理器異常入棧順序相關,這里獲取到了引發異常的 PC,也就是呼叫 SVC 的那個地方的指令,並獲取到 8 位立即數(數組[-2]的方式)
8.2、PendSV
PendSV 可以像普通中斷一樣被 Pending(往 NVIC 的 PendSV 的 Pend 寄存器寫 1),常用的場合是 OS 進行上下文切換;它可以手動拉起后,等到比他優先級更高的中斷完成后再執行;
帶 OS 系統的 CM3 中有兩個就緒的任務,假設上下文切換可以發生在 SYSTICK 中斷中:
這里展現的是兩個任務 A 和 B 輪轉調度的過程;但是,如果在產生 SYSTICK 異常時,系統正在響應一個中斷,則 SYSTICK 異常會搶占其他 ISR。在這種情況下 OS 是不能執行上下文切換的,否則將使得中斷請求被延遲;
而且,如果在 SYSTICK 中做任務切換,SYSTICK中斷最后的BX LR指示返回被打斷的中斷(從別的中斷進SYSTICK中斷,PC等寄存器自動壓棧到MSP,退出SYSTICK中斷自動從MSP出棧到PC等寄存器),但是R4~R11被修改為任務的值,不是被打斷中斷的值,將導致用法無法預測的問題,下圖紅色字表述的有問題
為了解決這種問題,早期的 OS 在上下文切換的時候,檢查是否有中斷需要響應,沒有的話,采取切換上下文,然而這種方法的問題在於,可能會將任務切換的動作拖延很久(如果此次的 SYSTICK 無法切換上下文,那么要等到下一次 SYSTICK 再來切換),嚴重的情況下,如果某 IRQ 來的頻率和 SYSTICK 來的頻率比較接近的時候,會導致上下文切換遲遲得不到進行;
引入 PendSV 以后,可以將 PendSV 的異常優先級設置為最低,在 PendSV 中去切換上下文,PendSV 會在其他 ISR 執行完后,立馬執行:
上圖的過程可以描述為:
1、任務 A 呼叫 SVC 請求任務切換;
2、OS 收到請求,准備切換上下文,手動 Pending 一個 PendSV;
3、CPU 退出 SVC 的 ISR 后,發現沒有其他 IRQ 請求,便立即進入 PendSV 執行上下文切換;
4、正確的切換到任務 B;
5、此刻發生了一個中斷,開始執行此中斷的 ISR;
6、ISR 執行一半,SYSTICK 來了,搶占了該 IRQ;
7、OS 執行一些邏輯,並手動 Pending PendSV 准備上下文切換;
8、退出 SYSTICK 的 ISR 后,由於之前的 IRQ 優先級高於 PendSV,所以之前的 ISR 繼續執行;
9、ISR 執行完畢退出,此刻沒有優先級更高的 IRQ,那么執行 PendSV 進行上下文切換;
10、PendSV 執行完畢,順利切到任務 A,同時進入線程模式;
9、其他
NVIC 還提供了一些 fault 狀態寄存器,以便於 fault 服務例程找出導致異常的具體原因。
參考資料
《ARMv7-M Architecture Reference Manual》