关于msOS内核切换学习-汇编
说明
上两个星期都在学习msOS,第一周看完了一本书,第二周看了相关的视频和认真分析代码。原计划是每一周做一次总结,结果每一周总有那么多事。这样我深切的认识到,不能把所有事都安排到周末,也不要太高估自己的执行能力。因此决定一边学习一边做相应笔记吧!
msOS是我朋友公司老板自己编写的一个小型操作系统。具体msOS是什么这里不进行详细说明,学习msOS主要的目的是更好学习RTOS操作系统和学习设计过程中展现的智慧。接下来贴出OS的内核代码的任务切换部分。内核切换可以说是OS的核心,理解了这个之后,其他知识只是一个锦上添花,可以很好的一部分一部分学习。
目录
代码讲解
- 任务栈定义代码
/*******************************************************************************
* 描述 : 初始化任务栈,xPSR、PC、R14、R12、R3、R2、R1、R0入栈顺序是由Cortex\
* : 芯片决定的。中断入栈是芯片硬件自动完成,R11~R4顺序是由OS自己决定的。
* 输入参数 : taskPointer: 任务入口地址, stackRamTopPointer: 栈内存最高地址
* 返回参数 : 栈顶
*******************************************************************************/
static uint * InitStack(void (*taskPointer)(void), uint * stackPointer)
{
*(stackPointer) = (uint)0x01000000L; // xPSR
*(--stackPointer) = (uint)taskPointer; // 任务入口地址
*(--stackPointer) = (uint)0xFFFFFFFEL; // R14 (LR)
*(--stackPointer) = (uint)0x12121212L; // R12
*(--stackPointer) = (uint)0x03030303L; // R3
*(--stackPointer) = (uint)0x02020202L; // R2
*(--stackPointer) = (uint)0x01010101L; // R1
*(--stackPointer) = (uint)0x00000000L; // R0
*(--stackPointer) = (uint)0x11111111L; // R11
*(--stackPointer) = (uint)0x10101010L; // R10
*(--stackPointer) = (uint)0x09090909L; // R9
*(--stackPointer) = (uint)0x08080808L; // R8
*(--stackPointer) = (uint)0x07070707L; // R7
*(--stackPointer) = (uint)0x06060606L; // R6
*(--stackPointer) = (uint)0x05050505L; // R5
*(--stackPointer) = (uint)0x04040404L; // R4
return(stackPointer);
}
栈的讲解
栈的定义
栈的本质就是一种数据结构类型,是一种只能单向操作的线性列表。访问栈只需要一个栈顶指针SP就可以了,栈底是固定不变的数值。在M3中有两个栈指针分别是MSP和PSP,都用同一个R13寄存器表示。MSP是用来指向系统的栈顶(main函数、系统异常、中断),PSP是用来指向用户任务栈顶指针。栈的应用
由上面我们知道栈是一种数据结构,由于M3内核在运行函数时候存储形参和局部变量的时候存储方式和栈操作是一样的因此用来存放局部变量和形参的这块区域叫作栈。本质上栈是SRAM的一块存储区域。
注意:但函数调用完后或者函数切换时候会执行出栈操作,这样会使得sp栈顶会指向栈底。
- 内核切换汇编代码
/*******************************************************************************
- 描述 : 任务切换,在PendSV中断中实现
*******************************************************************************/
__asm void PendSV_Handler(void)
{
IMPORT CurrentTaskPointer
IMPORT NextTaskPointer
CPSID I // 关闭中断
MRS R0, PSP // 读取PSP到R0
LDR R2, =CurrentTaskPointer // 获取指向当前任务指针的地址
LDR R3, =NextTaskPointer // 获取指向新任务指针的地址
CBZ R0, NextTask // 复位后,任务启动时,从中断模式跳转到任务中
// 入栈
SUBS R0, R0, #32 // 栈顶减32,即8Word
STM R0, {R4-R11} // 压栈R4~R11,8个寄存器
LDR R1, [R2] // 获取指向当前任务的指针,也指向第一成员StackPointer
STR R0, [R1] // 把当前栈顶存入表中第一个成员
// 获取新栈顶
NextTask
LDR R0, [R3] // 获取新任务指针变量值
STR R0, [R2] // 赋值给当前任务指针变量
// 出栈
LDR R0, [R0] // 加载新任务栈顶到R0
LDM R0, {R4-R11} // 加载栈内容到R4~R11
ADDS R0, R0, #32 // 新栈顶升32,即8Word
MSR PSP, R0 // 新栈顶写入PSP
ORR LR, LR, #0x04 // LR的bit2写入1,中断退出后进入任务模式,使用PSP栈
CPSIE I // 开中断
BX LR // 返回
NOP
}
STM32寄存器学习
通过代码我们可以看出来,任务的切换实质就是当前任务使用到的寄存器的备份,以及恢复下一次任务寄存器的数值,最后切换PSP(任务栈指针)的地址和PC地址。其中用到寄存器[R0-R15]、xPSR。
xPSR是特殊寄存器,用于存放程序运行状态用的。关于寄存器更多知识请查看Cortex M3权威指南。
汇编语法知识学习
关键字列表
关键字 | 描述 |
---|---|
__asm | 表示在C代码中插入汇编 |
IMPRORT | 声明标号来自外部文件,跟 C 语言中的 EXTERN 关键字类似 |
CPSID | PRIMASK写1 用来关闭中断 |
MRS | MRS指令用于将程序状态寄存器的内容传送到通用寄存器中 |
LDR | 从存储器中加载字到一个寄存器中 |
CBZ | 比较(Compare),如果结果为零(Zero)就转移(只能跳到后面的指令) |
SUBS | SUB减法操作 当没有S时指令不更新CPSR中条件标志位的值 |
STM | STM批量存储指令可以实现一组寄存器和一块连续的内存单元之间传输数据。 |
STR | 把寄存器的数值存储到内存单元 |
LDM | LDM批量加载可以实现一组寄存器和一块连续的内存单元之间传输数据。 |
ADDS | ADD加法操作 当没有S时指令不更新CPSR中条件标志位的值 |
MSR | MSR指令用亍将操作数的内容传送到程序状态寄存器的特定域中 |
ORR | 指令用于在两个操作数上进行逻辑或运算,通常用来置位 |
CPSIE | PRIMASK写0 用来开启中断 |
BX | BX指令跳转到指令中所指定的目标地址 |
NOP | 执行NOP指令只使程序计数器PC加1,所以占用一个机器周期 |
重点讲解
- MRS R0, PSP // 读取PSP到R0
注意:因为在进入PendSV_Handler后我们使用的是MSP,因此需要使用MRS指令来进行访问PSP。由于xPSR、PC、R14、R12、R3、R2、R1、R0是芯片自动入栈,入栈顺序是按照InitStack定义寄存器顺序。所以读取到PSP指向是R0位置,即当前任务栈中第八个成员。
- CBZ R0, NextTask // 复位后,任务启动时,从中断模式跳转到任务中
因为在一开始初始化的时候把PSP设置为零,这句操作是为了判断是否为第一次进入任务切换操作,如果是就跳过R4-R11的备份,反之亦然。
- SUBS R0, R0, #32 // 栈顶减32,即8Word
- STM R0, {R4-R11} // 压栈R4~R11,8个寄存器
因为是入栈操作所以地址需要减小,至于为什么要先减地址后做数据备份,这个和STM指令操作方式有关。M3内核栈操作方式是,栈顶为最高地址,入栈地址要减小,出栈地址要增加。
注意:R0这时刻存放的是当前任务栈顶指针。这里使用到的LDM和STM都是先自增4字节地址后赋值。
- LDR R1, [R2] // 获取指向当前任务的指针,也指向第一成员StackPointer
- STR R0, [R1] // 把当前栈顶存入表中第一个成员
主要是把当前任务的栈顶指针保存到,当前任务栈中的第一个成员。R0中存放的是栈顶指针。
- LDR R0, [R3] // 获取新任务指针变量值
- STR R0, [R2] // 赋值给当前任务指针变量
把下一个任务的指针赋值给当前任务。即两个任务指针变量相等。
- LDR R0, [R0] // 加载新任务栈顶到R0
- LDM R0, {R4-R11} // 加载栈内容到R4~R11
- ADDS R0, R0, #32 // 新栈顶升32,即8Word
由上面入栈代码可以知道,任务栈指针中实际存放的数值是栈顶指针,因此通过上面段代码第一句操作可以得到下一个任务的栈顶,通过LDM操作加载栈中备份R4-R11数值恢复到寄存器中,因为这是出栈操作,所以地址需要增加。