从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了。好好的代码格式贴上去,又乱格式了。

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值