Cortex-M3 處理器窺探

目錄

1、寄存器組

2、特殊功能寄存器組

2.1、xPSR

2.2、PRIMASK

2.3、BASEPRI

2.4、FAULTMASK

2.5、CONTROL

2.6、特殊寄存器組訪問方式

3、處理器工作模式

3.1、運行等級

3.2、運行模式

3.3、運行等級 VS 運行模式

4、堆棧

5、指令集

6、中斷/異常向量表

7、中斷/異常響應序列

7.1、中斷/異常入棧

7.2、取向量

7.3、更新寄存器

7.4、異常返回值 EXC_RETURN

8、SVC 和 PendSV

8.1、SVC

8.2、PendSV


 

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 定義的訪問他們的方式是隻能通過 MRSMSR 指令,比如:

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)下,對系統控制寄存器 SCSSystem 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 權威指南指令集章節;以後將會將常用的指令(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/PSP,更新到堆棧指針 SP 中,更新鏈接寄存器 LR,更新 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 與操作系統相關,所以拉出來單獨聊聊;

8.1、SVC

玩過 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 中做任務切換,那麼就會嘗試切入線程模式,將導致用法 fault 異常;

爲了解決這種問題,早期的 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,同時進入線程模式;

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章