從0到1寫RT_Thread內核 ——— 線程切換中的彙編代碼分析與常用的ARM彙編指令

結合野火的<<RT_Thread內核實現與應用開發實戰>> 進行學習從0開始寫RT_Thread的內核,從而達到對實時系統內核的瞭解和對C語言和彙編的提高。

常用的ARM彙編指令如下表格所示

指令名稱 作用
EQU 用於給數字常量設置一個符號名,相當於C語言中的define
AREA 彙編一個新的代碼段或者數據段
SPACE 分配內存空間
PRESERVE8 當前文件棧需要按照8字節進行對齊
EXPORT 聲明一個標號爲全局屬性,這個標號可以被外部文件使用
DCD 以字爲單位分配內存,要求爲4字節對齊,並且要求初始化這些內存
PROC 定義子程序,與ENDP進行成對使用。ENDP表明該子程序結束
WEAK 弱定義,如果外部文件聲明瞭一個標號,則優先使用外部文件定義好的標號。(WEAK不爲ARM指令,而是編譯器指令)
IMPORT 聲明標號來自於外部文件,與C語言中的 extern關鍵字比較相類似
B 轉跳到一個標號
ALIGN (不爲ARM指令,爲編譯器指令) 用於對指令或者數據的存放地址進行對齊,一般需要跟上一個立即數,默認對齊是4字節對齊。
END 到達文件的尾部,文件結束
IF, ELSE, ENDIF 彙編條件分支語句,與C語言中的if else類似
MRS 加載特殊功能寄存器的值到通用寄存器
MSR 存儲在通用寄存器的值加載到特殊功能寄存器
CBZ 比較,結果爲0就進行轉移
CBNZ 比較,結果爲非0則轉移
LDR 從存儲器中加載字到一個寄存器中
LDR[僞指令] 加載一個立即數或者一個地址值到一個寄存器。例如,LDR Rd,=lable,如果lable是立即數,那麼Rd等於立即數;如果lable是一個標識符(比如指針),那麼存入Rd的就是lable標識符的地址。
LDRH 從存儲器中加載半字到一個寄存器中
LDRB 從存儲器中加載字節到一個寄存器中
STR 把一個存儲器按字節存儲到存儲器中
STRB 把一個寄存器的低字節存儲到存儲器中
LDMIA 加載多個字,並且在加載後自增基址寄存器
STMIA 存儲多個字,並且在存儲後自增基址寄存器
ORR 按位或
BX 直接跳轉到由寄存器給定的地址
BL 跳轉到標號對應的地址,並且把跳轉前的下一條指令地址保存到LR
BLX 跳轉到由寄存器 REG 給出的的地址,並根據 REG 的 LSB 切換處理器狀態 , 還 要 把 轉 移 前 的 下 條 指 令 地 址 保 存 到 LR 。 ARM(LSB=0) ,Thumb(LSB=1)。 CM3 只在 Thumb 中運行,就必須保證 reg 的 LSB=1,否則會報錯。
CPSID    I 關中斷
CPSID    F 關異常
CPSIE     I 開中斷
CPSIE     F 開異常
LDMFD

主要用於中斷對現場環境的恢復。

指令格式
STMFD/LDMFD  Rn! ,{寄存器列表(由小到大)}^

Rn 爲 SP(堆棧指針)  後綴 "!"表明將最後的地址回寫到SP中
"^"在用戶模式及系統模式下不可使用,一般用於在LDM指令的寄存器列表中包含PC寄存器時,它可以將SPSR中的值還原到CPSR中,可用於異常處理的返回。
LSMFD寫在前面的寄存器先出棧

STMFD 主要用於中斷對現場環境的保護。STMFD寫在後面的寄存器先入棧

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

;*************************************************************************
;                                 全局變量
;*************************************************************************
; IMPORT聲明標號來自外部文件 跟C語言中的EXTERN關鍵字類似 
; 以下3個變量在C語言文件中定義,此處進行聲明用於彙編的調用 
	IMPORT rt_thread_switch_interrupt_flag    ;PendSV中斷服務函數執行標誌;
	IMPORT rt_interrupt_from_thread           ;上一個線程的棧的SP的指針
	IMPORT rt_interrupt_to_thread             ;下一個運行線程的棧的sp的指針
		
;*************************************************************************
;                                 常量
;*************************************************************************
;-------------------------------------------------------------------------
;有關內核外設寄存器定義可參考官方文檔:STM32F10xxx Cortex-M3 programming manual
;系統控制塊外設SCB地址範圍:0xE000ED00-0xE000ED3F
;-------------------------------------------------------------------------
;EQU 是給常量取一個名字 相當於C語言中的define;

SCB_VTOR        EQU     0xE000ED08     ; 向量表偏移寄存器
NVIC_INT_CTRL   EQU     0xE000ED04     ; 中斷控制狀態寄存器
NVIC_SYSPRI2    EQU     0xE000ED20     ; 系統優先級寄存器(2)
NVIC_PENDSV_PRI EQU     0x00FF0000     ; PendSV 優先級值 (lowest)
NVIC_PENDSVSET  EQU     0x10000000     ; 觸發PendSV exception的值
	
;*************************************************************************
;                              彙編代碼產生指令
;*************************************************************************
;當新建一個彙編文件寫代碼時,必須包括類似的指令。
;AREA表示彙編一個新的數據段或者代碼段
;.text表明段名,如果段名不是以字母開頭,而是以其他符號開頭,則需在段名兩段加上"|"
;CODE 表示僞代碼; READONLY表示只讀; ALIGN=2表示當前文件指令要按2^2字節對齊
;THUMB 表示THUMB指令代碼;REQUIRE8和PRESERVE8表明當前文件棧按照8字節進行對齊
    AREA |.text|, CODE, READONLY, ALIGN=2
    THUMB
    REQUIRE8
    PRESERVE8
		

;/*
; *-----------------------------------------------------------------------
; * 函數原型:void rt_hw_context_switch_to(rt_uint32 to);
; * r0 --> to
; * 該函數用於開啓第一次線程切換
; *-----------------------------------------------------------------------
; */
; PROC用於定義子程序 與EDNP成對使用
rt_hw_context_switch_to    PROC
    
	; 導出rt_hw_context_switch_to,讓其具有全局屬性,可以在C文件調用此函數
	EXPORT rt_hw_context_switch_to
		
    ; 設置rt_interrupt_to_thread的值
    LDR     r1, =rt_interrupt_to_thread             ;將rt_interrupt_to_thread的地址加載到r1
    STR     r0, [r1]                                ;將r0的值存儲到rt_interrupt_to_thread 即sp

    ; 設置rt_interrupt_from_thread的值爲0,表示啓動第一次線程切換 並沒有上一個運行線程
    LDR     r1, =rt_interrupt_from_thread           ;將rt_interrupt_from_thread的地址加載到r1
    MOV     r0, #0x0                                ;配置r0等於0
    STR     r0, [r1]                                ;將r0的值存儲到rt_interrupt_from_thread

    ; 設置中斷標誌位rt_thread_switch_interrupt_flag的值爲1 當執行PendSVC_Handlder時 會被清 0
    LDR     r1, =rt_thread_switch_interrupt_flag    ;將rt_thread_switch_interrupt_flag的地址加載到r1
    MOV     r0, #1                                  ;配置r0等於1
    STR     r0, [r1]                                ;將r0的值存儲到rt_thread_switch_interrupt_flag

    ; 設置 PendSV 異常的優先級 設置爲最低  實際線程的切換就是在PendSV中斷中進行
    LDR     r0, =NVIC_SYSPRI2
    LDR     r1, =NVIC_PENDSV_PRI
    LDR.W   r2, [r0,#0x00]       ; 讀
    ORR     r1,r1,r2             ; 改
    STR     r1, [r0]             ; 寫

    ; 觸發 PendSV 異常 (產生上下文切換)
    LDR     r0, =NVIC_INT_CTRL
    LDR     r1, =NVIC_PENDSVSET
    STR     r1, [r0]

    ; 開中斷   CPSIE F 開異常  CPSIE I 開中斷
    CPSIE   F
    CPSIE   I

    ENDP    ;表明一個子程序的結束



;/*
; *-----------------------------------------------------------------------
; * void rt_hw_context_switch(rt_uint32 from, rt_uint32 to);
; * r0 --> from
; * r1 --> to
; *-----------------------------------------------------------------------
; */
;rt_hw_context_switch_interrupt
    ;EXPORT rt_hw_context_switch_interrupt
	; 用於產生上下文 當一個彙編函數被C文件調用時,如果有兩個形參,則會將形參傳入CPU寄存器 r0 r1中。		
rt_hw_context_switch    PROC
    EXPORT rt_hw_context_switch

    ; 設置中斷標誌位rt_thread_switch_interrupt_flag爲1     
    LDR     r2, =rt_thread_switch_interrupt_flag          ; 加載rt_thread_switch_interrupt_flag的地址到r2
    LDR     r3, [r2]                                      ; 加載rt_thread_switch_interrupt_flag的值到r3
    CMP     r3, #1                                        ; r3與1比較,相等則執行BEQ指令,否則不執行
    BEQ     _reswitch
    MOV     r3, #1                                        ; 設置r3的值爲1
    STR     r3, [r2]                                      ; 將r3的值存儲到rt_thread_switch_interrupt_flag,即置1
    
	; 設置rt_interrupt_from_thread的值
    LDR     r2, =rt_interrupt_from_thread                 ; 加載rt_interrupt_from_thread的地址到r2
    STR     r0, [r2]                                      ; 存儲r0的值到rt_interrupt_from_thread,即上一個線程棧指針sp的指針

_reswitch
    ; 設置rt_interrupt_to_thread的值
	LDR     r2, =rt_interrupt_to_thread                   ; 加載rt_interrupt_from_thread的地址到r2
    STR     r1, [r2]                                      ; 存儲r1的值到rt_interrupt_from_thread,即下一個線程棧指針sp的指針

    ; 觸發PendSV異常,實現上下文切換
	LDR     r0, =NVIC_INT_CTRL              
    LDR     r1, =NVIC_PENDSVSET
    STR     r1, [r0]
	
    ; 子程序返回
	BX      LR
	
	; 子程序結束
    ENDP


;/*
; *-----------------------------------------------------------------------
; * void PendSV_Handler(void);
; * r0 --> switch from thread stack
; * r1 --> switch to thread stack
; * psr, pc, lr, r12, r3, r2, r1, r0 are pushed into [from] stack
; *-----------------------------------------------------------------------
; */
; 這纔是真正的實現線程上下文切換的函數
PendSV_Handler   PROC
    EXPORT PendSV_Handler

    ; 失能中斷,爲了保護上下文切換不被中斷
    MRS     r2, PRIMASK     ;加載特殊功能寄存器的值到通用寄存器 PRIMASK是中斷屏蔽寄存器
    CPSID   I               ;關中斷

    ; 獲取中斷標誌位,判斷是否爲0  爲0則退出PendSV_Handler函數
    LDR     r0, =rt_thread_switch_interrupt_flag     ; 加載rt_thread_switch_interrupt_flag的地址到r0
    LDR     r1, [r0]                                 ; 加載rt_thread_switch_interrupt_flag的值到r1
    CBZ     r1, pendsv_exit                          ; 判斷r1是否爲0,爲0則跳轉到pendsv_exit

    ; r1不爲0則清0
    MOV     r1, #0x00
    STR     r1, [r0]                                 ; 將r1的值存儲到rt_thread_switch_interrupt_flag,即清0

    ; 判斷rt_interrupt_from_thread的值是否爲0  用於判斷是否爲第一次線程切換
    LDR     r0, =rt_interrupt_from_thread            ; 加載rt_interrupt_from_thread的地址到r0
    LDR     r1, [r0]                                 ; 加載rt_interrupt_from_thread的值到r1
    CBZ     r1, switch_to_thread                     ; 判斷r1是否爲0,爲0則跳轉到switch_to_thread
                                                     ; 第一次線程切換時rt_interrupt_from_thread肯定爲0,則跳轉到switch_to_thread

; ========================== 上文保存 ==============================
    ; 當進入PendSVC Handler時,上一個線程運行的環境即:
 	; xPSR,PC(線程入口地址),R14,R12,R3,R2,R1,R0(線程的形參)
 	; 這些CPU寄存器的值會自動保存到線程的棧中,剩下的r4~r11需要手動保存
	
 	
    MRS     r1, psp                                  ; 獲取線程棧指針到r1 特殊寄存器的值保存在通用寄存器中
    STMFD   r1!, {r4 - r11}                          ;將CPU寄存器r4~r11的值存儲到r1指向的地址(每操作一次地址將遞減一次)
    LDR     r0, [r0]                                 ; 加載r0指向值到r0,即r0=rt_interrupt_from_thread
    STR     r1, [r0]                                 ; 將r1的值存儲到r0,即更新線程棧sp
	
; ========================== 下文切換 ==============================
; 下文切換的實質 是將接下來要運行的線程棧中的內容加載到CPU寄存器 更改PC指針和PSP指針,從而實現程序的轉跳
switch_to_thread
    LDR     r1, =rt_interrupt_to_thread               ; 加載rt_interrupt_to_thread的地址到r1
	                                                  ; rt_interrupt_to_thread是一個全局變量,裏面存的是線程棧指針SP的指針
    LDR     r1, [r1]                                  ; 加載rt_interrupt_to_thread的值到r1,即sp指針的指針
    LDR     r1, [r1]                                  ; 加載rt_interrupt_to_thread的值到r1,即sp

    LDMFD   r1!, {r4 - r11}                           ;將線程棧指針r1(操作之前先遞減)指向的內容加載到CPU寄存器r4~r11
    MSR     psp, r1                                   ;將線程棧指針更新到PSP  MSR通用寄存器的值保存到特殊寄存器

pendsv_exit
    ; 恢復中斷
    MSR     PRIMASK, r2

    ORR     lr, lr, #0x04                             ; 確保異常返回使用的堆棧指針是PSP,即LR寄存器的位2要爲1
    BX      lr                                        ; 異常返回,這個時候任務堆棧中的剩下內容將會自動加載到xPSR,PC(任務入口地址),R14,R12,R3,R2,R1,R0(任務的形參)
	                                                  ; 同時PSP的值也將更新,即指向任務堆棧的棧頂。在ARMC3中,堆是由高地址向低地址生長的。
    ; PendSV_Handler 子程序結束
	ENDP	
	
	
	ALIGN   4

    END                                               ;彙編文件結束,每一個彙編文件都需要一個END 
		

 

我服了csdn了。好好的代碼格式貼上去,又亂格式了。

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