二话不说,先贴代码,如下所示:
;********************************************************************************************************
; 声明和导出
;********************************************************************************************************
EXTERN OSRunning ; 在其余文件中定义,在本文件中引用
EXTERN OSPrioCur ; 汇编语言中:extern 声明变量 import 导入函数
EXTERN OSPrioHighRdy
EXTERN OSTCBCur
EXTERN OSTCBHighRdy
EXTERN OSIntNesting
EXTERN OSIntExit
EXTERN OSTaskSwHook
EXPORT OS_CPU_SR_Save ; 在本文件中定义,在其余文件中可直接调用
EXPORT OS_CPU_SR_Restore
EXPORT OSStartHighRdy
EXPORT OSCtxSw
EXPORT OSIntCtxSw
EXPORT PendSV_Handler
;********************************************************************************************************
; 赋值
;********************************************************************************************************
NVIC_INT_CTRL EQU 0xE000ED04 ; 中断控制状态寄存器的地址
NVIC_SYSPRI14 EQU 0xE000ED22 ; 系统优先级寄存器的地址 (priority 14).
NVIC_PENDSV_PRI EQU 0xFF ; PendSV 的优先级为255 (优先级最低).
NVIC_PENDSVSET EQU 0x10000000 ; 该值可触发PendSV异常
;********************************************************************************************************
; 代码生成指令
;********************************************************************************************************
AREA |.text|, CODE, READONLY, ALIGN=2 ; 定义一个段名为.text,该段为代码段,只可读,并且以2^2=4字节的形式对齐
THUMB ; 切换为THUMB指令集
REQUIRE8 ; 指定当前文件 要求堆栈 八字节对齐
PRESERVE8 ; 指定当前文件 保持堆栈 八字节对齐
;********************************************************************************************************
; 中断源 开关函数的实现
; 通过保存的中断源的状态 来使能或者除能中断源;
; 换句话说:这次 人为的 使能或者除能中断源,不能影响中断源本身的状态,
; 所以需要将中断源状态先保存起来。
; 备注:1.这并不是将中断使能或者除能,而是关闭 可屏蔽中断源 除了 硬件中断源以及NMI中断源;
; 2.若PRIMASK为1,则关闭可屏蔽中断源除了硬件中断源以及NMI中断源,默认为0,表示没有关闭
; 3.OS_CPU_SR_Restore函数里面竟然没有配套的打开中断源,是不是很奇怪?
; 原因是:假设在关中断前PRIMASK为0,那么在恢复时,我已经将0还给了PRIMASK,我干嘛还要重复打开呢;
; 假设在关中断前PRIMASK为1,那么在恢复时,我将0还给了PRIMASK,和调用本对函数之前的状态不符合,那就是错误了;
; 所以不需要 配套的打开中断源。
;********************************************************************************************************
OS_CPU_SR_Save
MRS R0, PRIMASK ; 将PRIMASK的值存进通用寄存器R0中
CPSID I ; 关闭中断源
BX LR ; 跳转至调用该函数的位置
OS_CPU_SR_Restore
MSR PRIMASK, R0 ; 将通用寄存器R0的值还给PRIMASK
BX LR ; 跳转至调用该函数的位置
;********************************************************************************************************
; 开始运行多任务
; 该函数触发一个PendSV异常服务来运行第一个任务。
; 该函数需要完成以下事情:
; a) 设定PendSV异常的优先级为最低优先级
; b) 设定进程堆栈的指针为0,让PendSV异常处理知道这是第一次运行任务
; c) 设定OSRunning为TRUE;
; d) 触发PendSV异常处理;
; e) 允许中断。
;********************************************************************************************************
OSStartHighRdy
LDR R0, =NVIC_SYSPRI14 ; 将系统优先级寄存器的地址给R0
LDR R1, =NVIC_PENDSV_PRI ; 将PendSV优先级这个变量值的地址给R1
STRB R1, [R0]
MOVS R0, #0 ; 设定进程堆栈为0
MSR PSP, R0
LDR R0, =OSRunning ; OSRunning这个变量的地址赋值给R0
MOVS R1, #1
STRB R1, [R0] ; 将1赋值给以R0值为地址的变量中,完成了OSRunning=1
LDR R0, =NVIC_INT_CTRL ; 将这个寄存器的地址A赋值给R0
LDR R1, =NVIC_PENDSVSET ; 将这个变量的地址B赋值给R1
STR R1, [R0] ; 将地址B赋值给地址为A的内存单元
; 把NVIC_PENDSVSET赋值给NVIC_INT_CTRL这个寄存器,就会触发了PendSV异常
CPSIE I ; 允许中断
OSStartHang
B OSStartHang ; 正常情况下不应该来这,如果来这,那就出不去了
;********************************************************************************************************
; 任务级别 执行一次上下文切换
;********************************************************************************************************
OSCtxSw
LDR R0, =NVIC_INT_CTRL
LDR R1, =NVIC_PENDSVSET
STR R1, [R0] ; 触发了PendSV异常,进行一次上下文切换
BX LR
;********************************************************************************************************
; 中断级别 执行一次上下文切换
; 备注:没有其他的中断源处于活动状态,并且中断处于使能状态 (个人认为和任务级别没有区别!!!)
;********************************************************************************************************
OSIntCtxSw
LDR R0, =NVIC_INT_CTRL
LDR R1, =NVIC_PENDSVSET
STR R1, [R0] ; 触发了PendSV异常,进行一次上下文切换
BX LR
;********************************************************************************************************
; PendSV 异常处理
; 1) This is because the Cortex-M3 auto-saves half of the processor context on any exception,
; and restores same on return from exception.
; Cortex-M3在任何异常情况下都能够自动保存并且恢复一半的处理器上下文
; So only saving of R4-R11 is required and fixing up the stack pointers.
; 只需要保存R4-R11的寄存器内容,以及修改堆栈指针(该指针应该指向堆栈栈顶)
; Using the PendSV exception this way means that context saving and restoring is identical
; whether it is initiated from a thread or occurs due to an interrupt or exception.
; 使用PendSV 异常处理这种方式意味着无论是从线程启动还是因为中断或异常而导致出现的,
; 上下文的保存和恢复是相同的
;
; 2) Pseudo-code is:
; a) 获得进程的堆栈指针SP,如果该指针为0,则跳过保存内容这部分,直接进入PendSV_Handler_Nosave
; b) 如果不为0,则将寄存器r4-r11保存到对应的进程堆栈中;
; c) 将堆栈指针SP保存到对应的任务控制块中,即OSTCBCur->OSTCBStkPtr = SP
; (任务控制块的首地址也就是任务控制块中堆栈指针变量的地址,详情可看任务控制块的定义);
; d) 调用 OSTaskSwHook();
; e) 将当前的高优先级赋值给当前运行优先级, OSPrioCur = OSPrioHighRdy(这两个是变量);
; f) 将指向当前的高优先级任务控制块的指针赋值给指向当前运行任务控制块的指针,
; OSTCBCur = OSTCBHighRdy(这两个时指针);
; g) 从任务控制块中获得对应的堆栈指针, SP = OSTCBHighRdy->OSTCBStkPtr;
; h) 将寄存器 R4-R11从对应的堆栈中恢复出来;
; i) 执行异常返回,这将恢复剩余的上下文( xPSR, PC, LR, R12, R0-R3).
;
; 3) 进入PendSV异常处理时:
; a) xPSR, PC, LR, R12, R0-R3这些寄存器会被保存到对应的进程堆栈中(先一步保存)
; b) 处理器模式由线程模式切换到Handler模式;
; c) 堆栈由进程堆栈切换为主堆栈;
; d) OSTCBCur指向的任务控制块的任务被挂起;
; OSTCBHighRdy指向的任务控制块的任务被恢复;
;
; 4) 因为PendSV被设置成系统中优先级最低的异常,所以只能运行在没有其他中断或者异常处于运行状态;
; therefore safe to assume that context being switched out was using the process stack (PSP).
; 因此可以认为上下文通过进程堆栈进行切换是安全的,这样翻译不知对不对?
;********************************************************************************************************
PendSV_Handler
CPSID I ; 除能中断
MRS R0, PSP ; 获得进程堆栈指针
CBZ R0, PendSV_Handler_Nosave ; 判断该堆栈指针是否为0,若为0,则跳转到PendSV_Handler_Nosave继续运行,若不为0,则运行下一行
SUBS R0, R0, #0x20 ; 将堆栈指针SP=SP-0x20(每个内存单元占用4个字节,待会需要保存8个字,即32个字节,即0x20,又因为栈的方向是由上往下,所以-0x20)
STM R0, {R4-R11} ; 依次将R4-R11存储到地址为SP,SP+4,SP+8.....中
; 好了,SP指向当前堆栈的栈顶,即存储R4的堆栈内存
LDR R1, =OSTCBCur ; 将指向当前运行任务控制块的指针A的所在的地址B赋值给R1;
LDR R1, [R1] ; 将地址为B的内存中的数据(即A)赋值给R1
STR R0, [R1] ; 将该进程堆栈的指针SP存储到以A为地址的内存中,即SP = OSTCBHighRdy->OSTCBStkPtr
; 至此,该保存的都已经保存了,下面就要开始进行恢复了
PendSV_Handler_Nosave
PUSH {R14} ; 将函数返回地址压栈,返回到哪?
LDR R0, =OSTaskSwHook ; 将该函数的首地址加载到R0
BLX R0 ; 跳转到该地址,运行完再跳转回到这
POP {R14} ; 将函数返回地址出栈
; 完成了 OSPrioCur = OSPrioHighRdy的操作
LDR R0, =OSPrioCur ; 将这个变量的地址A加载到R0
LDR R1, =OSPrioHighRdy ; 将这个变量的地址B加载到R1
LDRB R2, [R1] ; 将地址为B的内存中的数据C赋值给R2(字节形式)
STRB R2, [R0] ; 将数据C存储到地址为A的内存单元中
; 完成了 OSTCBCur = OSTCBHighRdy的操作
LDR R0, =OSTCBCur ; 将这个指针D的地址A加载到R0
LDR R1, =OSTCBHighRdy ; 将这个指针E的地址B加载到R1
LDR R2, [R1] ; 将地址为B的内存中的数据(指针E)赋值给R2
STR R2, [R0] ; 将指针E存储到地址为A的内存单元中
; 恢复R4-R11
LDR R0, [R2] ; 指针E是个地址,该地址为OSTCBHighRdy这个任务控制块的首地址,同时也是变量OSTCBStkPtr的地址,
; 将地址为指针E的内存单元中的数据赋值给R0,即SP = OSTCBHighRdy->OSTCBStkPtr;
LDM R0, {R4-R11} ; 从该进程堆栈中依次将地址为SP,SP+4,SP+8.....中的数据恢复到R4,R5...等寄存器中
ADDS R0, R0, #0x20 ; 将堆栈指针SP=SP+0x20
MSR PSP, R0 ; 将新的堆栈指针赋值给PSP
ORR LR, LR, #0x04 ; 确保异常返回时使用的是进程堆栈,用于给其他函数判断的依据
CPSIE I ; 使能中断
BX LR ; 异常返回时将会恢复剩余的寄存器内容,xPSR, PC, LR, R12, R0-R3
END
备注:
1.在理解这部分的汇编代码时,突然想到有的人智商格外高,一遍就过,有的人智商像我这样,这么艰难,好歹也是受过高等教育的人,只能用水磨工夫来增加自己对这部分内容的领悟;
2.在汇编任务中,如果一个函数中没有跳转指令,他会一直往下运行;
当运行到下一个函数前,若还没有跳转指令,他 竟然 会继续运行下一个函数,比如PendSV_Handler和PendSV_Handler_Nosave这两函数,超出认知了;
可是OSStartHighRdy这个函数又不按照常理出牌了,没有跳转指令,难道要去OSStartHang打转转?待会在线仿真下
3.这篇文档再次加深了对 LDR 这个汇编指令的认识
a)
LDR R0, [R2]
将地址为R2的内存单元中的数据赋值给R0;
b)
LDR R0, =OSTCBCur
将这个指针D的地址A加载到R0
c)
LDR R0, =OSPrioCur
将这个变量的地址A加载到R0
d)
LDR R0, =NVIC_INT_CTRL
将这个寄存器的地址A赋值给R0
LDR R1, =NVIC_PENDSVSET
将这个变量的地址B赋值给R1
STR R1, [R0]
将地址B赋值给地址为A的内存单元
最后这种情况,逻辑正确,但是情理不通,谁能帮我解释一波?
通过在线仿真,发现一个现象:
1.如果单步执行,可以进入OSStartHang函数里面去,如下图所示:
然后,如果继续采取单步执行,会一直停留在该处;
假设因为上面触发了PendSV异常,那么就会跳到PendSV_Handler处理函数中,
又因为这是第一次执行任务切换,所以他不会执行保存寄存器动作,会直接进入PendSV_Handler_Nosave;
好的,我们现在开始做实验,在光标执行到上图所示位置,然后在PendSV_Handler处理函数相关位置打上断点,如下图所示,然后运行:
实验现象,程序先运行到断点处,然后从204行直接跳到PendSV_Handler_Nosave函数;
实验证明:程序由OSStartHang函数中跳出来了,并按照正常的步骤(第一次执行任务切换)运行下去了
2.若是在下图处打上断点,程序复位后,直接RUN,根本就不会运行到断点处,这是为什么?
3.在运行完STR R1, [R0],程序是立刻进入PendSV异常处理函数中,还是有个延迟?
实验现象:程序复位后,可以直接运行到断点1处,如果不打断点1,只打断点2,程序是不能运行到这里的,直接进入了PendSV异常处理函数中
实验证明:在运行完STR R1, [R0],程序是立刻进入PendSV异常处理函数中
实验疑惑:为什么单步运行却可以运行到断点2处?