#ifndef RAW_CPU_H
#define RAW_CPU_H
unsigned int OS_CPU_SR_Save(void);
//上述函数是把PRIMASK的值读到R0中,并通过返回值将PRIMASK赋值给cpu_sr
//关于PRIMASK,PRIMASK是只有1个位的寄存器,用于屏蔽中断
void OS_CPU_SR_Restore(unsigned int sr);
//该函数是通过形参传递把cpu_sr的值给R0,然后把R0中的值给PRIMASK
#define RAW_SR_ALLOC() unsigned int cpu_sr = 0
//RAW_SR_ALLOC说明:调用临界区这两个接口时必须要定义一个cpu_sr 变量,
//这个变量是用来存放关中断前的状态寄存器的,看懂代码意思知道这个局部变量只需定义,至于赋值0与否不重要
#define USER_CPU_INT_DISABLE() {cpu_sr = OS_CPU_SR_Save();}
//上述宏实现将PRIMASK的值通过R0赋值给cpu_sr
#define USER_CPU_INT_ENABLE() {OS_CPU_SR_Restore(cpu_sr);}
//上述宏实现将cpu_sr的值通过R0赋值给PRIMASK
#endif
下面是汇编文件中,实现上述头文件中进入临界区和退出临界区的部分:
OS_CPU_SR_Save
MRS R0, PRIMASK ;读取BASEPRI寄存器到R0中。MRS: 状态寄存器到通用寄存器的传送指令
CPSID I ;屏蔽所有中断,只剩下NMI、复位中断、硬件中断无法屏蔽,也屏蔽了PendSV
BX LR ;跳转到调用该函数的函数中去。LR存的是调用此函数的地址
OS_CPU_SR_Restore
MSR PRIMASK, R0 ;MSR: 通用寄存器到状态寄存器的传送指令。形参的值通过R0传递,现在将cpu_sr赋值给PRIMASK
BX LR ;跳转到调用该函数的函数中去。LR存的是调用此函数的地址
如果对于以上几句没看懂,可以看我的另一篇文章中的详细介绍:
https://blog.csdn.net/wcc243588569/article/details/117825965
在raw_os_start()中,调用了该函数启动操作系统:
NVIC_INT_CTRL EQU 0xE000ED04 ; Interrupt control state register,写这个地址的第28位可以悬起PENDSV中断,
;参考M3权威指南第131页表8.5
NVIC_SYSPRI14 EQU 0xE000ED22; System priority register (priority 14)。控制PendSV的优先级
;参考M3权威指南第128页表8.3B
NVIC_PENDSV_PRI EQU 0xFF; PendSV priority value (lowest)。设置PendSV的优先级为最低
;把PendSV配置最低优先级,那么如果同时有多个异常被触发,它会在其他异常执行完毕后再执行,而且任何异常都可以中断它
NVIC_PENDSVSET EQU 0x10000000 ; Value to trigger PendSV exception.将第28位置1,用以悬起PendSV中断
raw_start_first_task;这个函数的作用是设置PendSV的异常中断优先级,并把优先级设置为最低
;设置了PendSV的中断优先级以后,当检测到其他的中断被systick抢占,就会触发PendSV异常,
LDR R0, =NVIC_SYSPRI14
LDR R1, =NVIC_PENDSV_PRI
STRB R1, [R0] ;设置PendSV的优先级
MOVS R0, #0 ;把R0设置为0
MSR PSP, R0 ;将PSP任务堆栈指针指向0。MSR: 通用寄存器到状态寄存器的传送指令。把R0的值复制给PSP
;之所以要把PSP指向0,不是要让程序的堆栈从0地址开始,而是要在PendSV_Handler中CBZ R0,OS_CPU_PendSVHandler_nosave检测是否操作系统第一次执行任务
;align msp to 8 byte,8字节对齐
; MRS R0, MSP ;把MSR的值复制给R0
; LSRS R0, R0, #3
; LSLS R0, R0, #3
; MSR MSP, R0 ;感觉这2句没用,移位后又移回来,没有任何作用,可以去掉,功能不变,没发现有任何影响
LDR R0, =NVIC_INT_CTRL
LDR R1, =NVIC_PENDSVSET
STR R1, [R0];这一句就是往ICSR第28位写1,触发PendSV异常,若是当前没有高优先级中断产生,那么程序将会进入PendSV handler,可以参考M3权威指南第131页的表进行理解
;STR是吧第1个数的值复制到第二个数里面
CPSIE I ;开启中断,开启中断后,如果没有其他外部中断,就会响应PendSV handler。否则就会执行下面的OSStartHang
OSStartHang
B OSStartHang ;should never get here,一般情况下不会执行到这里,但初始化时可能会执行到这里一下
;B是跳转的意思
关于以上配置中,为什么要将PendSV配置为最低优先级,可以看我的这篇文章:
https://blog.csdn.net/wcc243588569/article/details/117792602
本来按顺序应该讲PendSV_Handler中断部分,但是必须要先知道任务创建时干了什么,才能知道任务切换时做了什么。
在raw_task_create函数的形参中,第一个是
RAW_TASK_OBJ *task_obj
而RAW_TASK_OBJ 的第一个成员是RAW_VOID *task_stack;用户堆栈指针。因此task_obj地址也就是用户用户堆栈的二级指针。
在raw_task_create函数中调用了以下函数
raw_memset(task_obj, 0, sizeof(RAW_TASK_OBJ));
上述函数的作用是把任务控制块结构体的所有内容都初始设置为0
在raw_task_create函数中调用了以下函数
task_obj->task_stack = port_stack_init(task_stack_base, stack_size, task_arg, task_entry);
这个函数的作用就很有意思了,因此我们将该函数内容放到下面:其中去掉了FPU的部分,减少篇幅
RAW_VOID *port_stack_init(PORT_STACK *p_stk_base, RAW_U32 stk_size, RAW_VOID *p_arg, RAW_TASK_ENTRY p_task)
{
PORT_STACK *stk;
RAW_U32 temp = (RAW_U32)(p_stk_base + stk_size);
temp &= 0xfffffff8;
stk = (PORT_STACK *)temp;
*(--stk) = (RAW_U32)0x01000000L; /* xPSR */
*(--stk) = (RAW_U32)p_task; /* Entry Point */
*(--stk) = (RAW_U32)0xFFFFFFFEL; /* R14 (LR) (init value will cause fault if ever used)*/
*(--stk) = (RAW_U32)0x12121212L; /* R12 */
*(--stk) = (RAW_U32)0x03030303L; /* R3 */
*(--stk) = (RAW_U32)0x02020202L; /* R2 */
*(--stk) = (RAW_U32)0x01010101L; /* R1 */
*(--stk) = (RAW_U32)p_arg; /* R0 : argument */
*(--stk) = (RAW_U32)0x11111111L; /* R11 */
*(--stk) = (RAW_U32)0x10101010L; /* R10 */
*(--stk) = (RAW_U32)0x09090909L; /* R9 */
*(--stk) = (RAW_U32)0x08080808L; /* R8 */
*(--stk) = (RAW_U32)0x07070707L; /* R7 */
*(--stk) = (RAW_U32)0x06060606L; /* R6 */
*(--stk) = (RAW_U32)0x05050505L; /* R5 */
*(--stk) = (RAW_U32)0x04040404L; /* R4 */
return stk;
}
上述函数实现的功能是在用户的堆栈中预留出32个字节用于存放寄存器R4-R11数据,再预留出32个字节用于存放R0-R3、R12、R14、任务入口函数地址、xPSR数据。
然后返回的stk是用户开辟的用户堆栈区数据的栈顶地址减去64个字节后的地址,赋值给ask_obj->task_stack。
则ask_obj->task_stack地址递增方向是存放CPU寄存器的地址,往减方向才是用户任务中用到的临时变量等内容存放的用户堆栈区。
任务创建函数中用到了raw_list_entry(node, RAW_TASK_OBJ, task_list)函数,不明白这个函数什么意思的同学可以看我的这一篇文章:
https://blog.csdn.net/wcc243588569/article/details/117754931
相信看了我https://blog.csdn.net/wcc243588569/article/details/117792602这篇文章的,都知道为什么要在systick中断里面做系统心跳,在PendSV_Handler中做任务切换了。
PendSV_Handler任务切换部分实现过程如下:
PendSV_Handler
CPSID I ;关中断
MRS R0, PSP ;把PSP指针的值赋给R0,PSP指针是双堆栈指针中的一个,
;可以参考https://blog.csdn.net/hanchaoman/article/details/103727155
;也就是说,PSP指针存放的是进PendSV_Handler中断前,用户任务的堆栈区地址。
;可以通过切换PSP指针来指向不同任务的堆栈区
CBZ R0, OS_CPU_PendSVHandler_nosave ;为0则跳转
;如果程序堆栈指针为0(也就是如果PSP指针是0),说明程序是首次运行,可以直接跳转到OS_CPU_PendSVHandler_nosave,
;因为是第一次运行,因此不需要不保存任务的R4-R11寄存器的值,
执行到上面后,会因为第一次执行,PSP为0而跳转到下面代码中:
OS_CPU_PendSVHandler_nosave
LDR R0, =raw_task_active ;将目前活跃任务的任务控制块地址放到R0里。因为每1个任务在创建时都执行了port_stack_init函数,
;这个函数将每个任务的task_stack指向了每个任务栈的栈顶减去用来存放寄存器R0-R12、R14、任务入口地址、xPSP数据的共16*4=64个字节,
LDR R1, =high_ready_obj ;将目前最高优先级的任务控制块地址放到R1里。
;因为*task_stack在任务控制块结构体的第一个,任务控制块的起始地址存放的数据也就是*task_stack的值
;raw_task_active是一个结构体指针,结构体指针指向的地址就是结构体中第一个成员的地址,也就是内部指针*task_stack的地址
LDR R2, [R1] ;将存储器地址为R1的32位数据读入寄存器R2。也就是high_ready_obj指向的结构体的地址
STR R2, [R0] ;刚好与LDR相反,将R2寄存器中的32位数据放到内存地址为寄存器地址R0值的地址中去。
;通过以上语句,将high_ready_obj指向的结构体所在地址复制给了raw_task_active,
;raw_task_active指向的结构体是high_ready_obj指向的结构体
;以上2句并没有改变R1和R0的值
LDR R0, [R2] ;R2目前存的high_ready_obj指针指向的结构体地址,[R2]表示的是high_ready_obj指向的结构体中
;task_stack的内容
;以上一句,R0目前存的是high_ready_obj指向的结构体中task_stack的内容
;以上5句,raw_task_active指向了原本high_ready_obj指向的结构体
LDM R0, {R4-R11} ;LDM的方向与LDR相反,该指令是把R0指向的地址中的内容复制到R4-R11中,因R4-R11为32位寄存器,共拷贝了4*8=32个字节
;以上语句,把high_ready_obj指向的结构体中task_stack指向的任务堆栈中内容拷贝到了R4-R11寄存器中。其中R0地址+
ADDS R0, R0, #0x20 ;R0+=32,跳过了用户堆栈中用来存放r3-r11数据的32个字节,再往上就是FPU数据或者R0数据,具体看port_stack_init这个函数
IF {FPU} != "SoftVFP"
;只有M4内核有,M3内核没有MPU
;如果有FPU,则r0+=32,空出32个字节
VLDMFD r0!, {d8 - d15} ; pop FPU register s16~s31。LDMFD是事前递增方式,也就是R0每赋一个值给D8,R0都递增一下
ENDIF
;经过以上递增后,task_stack再往上就是R0-R3、R12、R14、函数入口、PSR数据了
MSR PSP, R0 ;Load PSP with new process SP。MSR和MRS是用于在状态寄存器和通用寄存器之间传送数据。
;MRS: 状态寄存器到通用寄存器的传送指令。MSR: 通用寄存器到状态寄存器的传送指令。
;把R0的地址给PSP,PSP也就是用户堆栈
ORR LR, LR, #0x04 ;指令用于在两个操作数上进行逻辑或运算,并把结果放置到目的寄存器中。
;上句是将LR的低3位拉高。LR是连接寄存器,
CPSIE I ;开中断,从PendSV_Handler一进入就关闭中断,到这开中断之间的代码是临界区代码,也就是不可被中断的操作。
BX LR ;异常返回。如果是从异常状态返回到线程状态,则使用新的PSP指针作为栈顶指针
NOP
END
当第二次再执行时,因为PSP指针不为0了,会在执行
CBZ R0, OS_CPU_PendSVHandler_nosave ;为0则跳转
判断时,会转而执行:
IF {FPU} != "SoftVFP"
VSTMFD r0!, {d8 - d15} ; push FPU register s16~s31
ENDIF
SUBS R0, R0, #0x20 ;R0保存的地址(SP)减去0x20(32),地址减去32,是因为PSP是堆栈栈顶指针,而根据port_stack_init函数,
;栈顶往下32个字节保存的是R0-R3、R12、R14、程序入口等信息。
;在响应中断时,ARM架构会自动的保存R0-R3、R12、R14、程序入口等信息,则此时到了R3-R11的R11处。
;减去32个字节后,R0就指向了存放R3所在的地址,进而执行下面指令,地址递增,将R3-R11共32个字节数据保存在用户堆栈中
STM R0, {R4-R11} ;把8个寄存器的值装进R0减去32个字节的地方。
LDR R1, =raw_task_active
LDR R1, [R1]
STR R0, [R1]
PUSH {R14}
bl raw_stack_check
POP {R14}
自此,整个汇编文件我们讲解完毕。
*如果感觉对你有帮助,还希望帮忙点个赞、收藏一下哦^~^!!!
我把带详细注释的程序下载链接放到了下面,是不要积分不要C币免费下载的。有下载不了的可以联系我,留下邮箱
https://download.csdn.net/download/wcc243588569/19576360