实现 RTOS 操作系统 【零】内核编程实践
一、概述
Cortx-M3 内核是 ARM 公司开发的 CPU 内核,完整的 MCU 芯片集成了 Cortex-M3 内核以及其他组件。
其内核部分和调试系统由 ARM 设计,通过内部总线和芯片厂设计的外设部分通讯。

1.1 工作模式以及权限级别分类
1.1.1 两种工作模式 (左边绿色框)
线程模式 (Thread mode):非异常状态下 (正常运行程序) 工作,一般主程序、RTOS 任务就是运行在这个模式下。
处理器模式 (Handler mode):进入异常 (比如中断、Fault、SVC 调用) 时进入,只能是特权级,绝对不会是用户级。中断服务函数、异常处理函数就是运行在这个模式里。
1.1.2 两种特权级 (右边紫色/蓝色框)
特权级 (Privileged level):可以访问所有寄存器、所有内存区域。可以访问所有存储区域 (包括外设寄存器、系统控制块、调试寄存器等)。可以使用 MSP (主堆栈指针) 和 PSP (进程堆栈指针),并且可以自由切换。
中断应用程序必须是特权级的,主程序可以是特权级的也可以是用户级的。处理器复位后在特权级模式下运行。在特权级模式下可以通过修改 CONTROL 寄存器进入用户级代码。用户级代码只能通过SVC中断,出发SVC异常才能重新进入特权级。

例如:MRS、MSR 指令只可以在特权级模式下使用。需要通过 MRS、MSR 访问的特殊功能寄存器,除了APSR 可以在用户级访问:

下面给出一个指定 PSP 进行更新的例子:
LDR R0, =0x20008000
MSR PSP, R0
BX LR ;如果是从异常回到线程状态,则使用新的PSP的值作为栈顶指针
用户级 (Unprivileged level):权限受限:不能访问受保护的寄存器或地址空间。常用于提高系统安全性,避免应用代码乱改硬件寄存器。
二、寄存器组
1.1 寄存器组概述
任务在执行他的代码时,必须要用到这些内核寄存器做一些算术、逻辑等运算处理,这部分寄存器相当于任务运行状态的一部分。在进行任务切换时,我们需要在切换代码中将这部分寄存器的值保存/恢复。
寄存器组:

特殊功能寄存器:

其中比较重要的有:
R15 程序计数器 (PC) | 保存了当前代码执行的指令位置地址。 |
R14 连接寄存器 (LR) | 则保存了当前函数执行完成后返回的指令位置地址。 |
R13 寄存器 (MSP) | 指明当前堆栈位置地址。 |
R13 主堆栈指针 (MSP) | 是我们正常程序所使用的,进程堆栈指针(PSP)是任务所使用的,我们可以通过对相关寄存器置位进行切换。 |
其他都是临时变量寄存器,编译器把 C 语言代码会转化成汇编会自动使用这些寄存器。
1.2 MSP 和 PSP 寄存器
对于 ARM Cortx-M3 是如何实现中断后保存现场和恢复,其使用了 R13 双堆栈机制:
除了简单了解其有 R0-R15 外,还要特别注意双堆栈寄存器 R13,用于实现中断/异常所用的栈与任务所用的栈相分离,互不干扰。硬件自动切换到 MSP 指向的堆栈来配合执行相应的处理程序,而退出后,自动切换到 PSP 指向的堆栈空间再执行任务代码。
无论是哪种堆栈,其均使用下面这幅图的增长模式:每次压栈,堆栈地址递减。且堆栈指针 SP(MSP/PSP)。特别注意:SP 总是指向最后压栈的单元。
1.3 程序状态寄存器


1.4 异常屏蔽寄存器

1.5 储存映射

三、堆栈
Cortex-M3使用的是"向下生长的满栈"模型。堆栈指针 SP 指向最后一个被压入堆栈的 32 位数值。在下一次压栈时,SP 先自减 4,再存入新的数值。
3.1 压栈操作
压栈后,首先将 SP 寄存器地址自减 4,然后压入。

3.2 出栈操作
出栈后,首先将 SP 寄存器地址自增 4,然后弹出。

四、异常/中断响应序列
4.1 系统异常编号

外部中断列表

4.2 进入异常中断
步骤一:硬件将 xPSR、PC、LR、R12 和 R0~R3 自动压入当前堆栈,其它寄存器根据需要由 ISR 自行保存。

步骤二:从中断向量表取入口地址。

- SP:入栈后保存
- PC:更新为中断服务入口地址
- LR:更新为特殊的 EXC_RETURN 值
4.2 退出异常中断
步骤一:执行返回指令,如 BX、LR
步骤二:恢复先前入栈的寄存器。出栈顺序与入栈时的相对应,堆栈指针的值也改回去。
步骤三:从原中断发生位置继续往下运行。
注:在返回时,会根据 EXC_RETURN 值来决定返回动作。


4.3 复位异常响应序列
MCU 复位响应如下,将 0x000000000 和 0x000000004 的内容分别赋值给 PC 和 MSP 寄存器,这样程序就开始运行了。
4.4 PendSV 异常
在 PendSV 中执行 RTOS 上下文切换(即不同任务间切换)。工作原理:配置为最低优先级,上下文切的请求将自动延迟到到其它的 ISR 都完成后才处理,并且可被其它异常/中断抢占。
也就是说 PendSV 是一个中断异常,那 PendSV 和其他的中断异常有什么区别呢?

个中事件的流水账记录如下:
- 任务A呼叫 SVC 来请求任务切换 (例如,等待某些工作完成)。
- OS 接收到请求,做好上下文切换的准备,并且悬起一个 PendSV 异常。
- 当 CPU 退出 SVC后,它立即进入 PendSV,从而执行上下文切换。
- 当 PendSV 执行完毕后,将返回到任务 B,同时进入线程模式。
- 发生了一个中断,并且中断服务程序开始执行。
- 在 ISR 执行过程中,发生 SysTick 异常,并且抢占了该 ISR。
- OS 执行必要的操作,然后悬起 PendsV 异常以作好上下文切换的准备。
- 当 SysTick 退出后,回到先前被抢占的 ISR 中,ISR继续执行。
- ISR 执行完毕并退出后,PendsV 服务例程开始执行,并且在里面执行上下文切换9。
- 当 PendSV 执行完毕后,回到任务 A,同时系统再次进入线程模式。
如果我们仔细看上图会发现步骤 8 的时候,SysTick 会先回到之前抢占的 ISR 而不是,而不是立刻进入 PendSV 中 (在 RTOS 中 SysTick 中都会调用 PendSV 中断)。
这是因为 PendSV 可以被悬起,触发 PendSV 后他会等到目前所有 ISR 中断结束再去中断。避免打断其他的中断,破坏 RTOS 的实时性。因为其他中断可能很紧急,不容被滞后。
所以PendSV的最大特点就是,它是系统级别的异常,但它又天生支持缓期执行。
五、指令详解
Cortex-M3 使用的是 Thumb-2 指令集。长度可为 16 位或者 32 位。指令可以携带后缀,如有条件的执行。示例:
CBZ R0,label
如果 R0 为 0,则跳转;否则什么都不做。
5.1 典型写法
操作码 操作数1、操作数2... ;注释
示例:
MOV R0,#0x12 ;R0 0x12
MOV R1,#'A' ;R1字的ASCII码
5.2 指令分类
5.3 储存器访问
LDR/LDRB Rd, = LABEL ;加载符号LABEL对应的地址,储存到Rd中
LDR/LDRB Rd, [Rs] ;从RS寄存器中取出地址,读取相应的32位/8位数据,存储到Rd寄存器
STR/STRB Rd, [RS] ;从RS寄存器中取出地址,将Rd中的32位/8位数据存储到相应的地址处
5.4 批量储存器访问
LDMIA Rd!, {Rn, .... Rm} ;从Rd处连续多次递增地址读取32位数据,存储到Rn.…Rm寄存器列表
STMDB Rd!, {Rn,. Rm} ;从Rd处连续多次递减地址存储32位数据,数据来自Rn..,Rm寄存器列表
IA(Increase After):在操作完成后递增地址;
! 操作结束后,将最终的地址保存到 Rd 寄存器中;
DB(Decrease Before):在操作开始前递减地址;
5.5 MRS 和 MSR
用于访问 xPSR、PSP、MSP 等
MRS Rn, <Sreg> ;加载能寄存器的值到 Rn
MSR <Sreg>, Rn ;存储 Rn 的值到能寄存器
5.6 中断开关
CPSID I ;关中断
CPISE I ;开中断
5.7 无条件跳转
BX Rn ;移到 寄存器 reg 给出的地址,例BX LR可用于子程序的返回
5.8 比较并件跳条转
CBZ Rn,<label> ;如果Rn寄存器值为0,则跳转到label对应的指令,否则执行下一条指令
CBNZ Rn,<label> ;如果Rn寄存器值不为0,则跳转到label对应的指令,否则执行下一条指令
六、内核编程实例
任务目标:使用 PendSVC 触发异常,在异常处理函数中,保存 R4~R11 寄存器到缓冲区,再恢复R4~R11 寄存器,以模拟任务切换时的寄存器保存与恢复。
6.1 系统异常优先级寄存器
我们根据手册将 PendSV 的优先级降至最低:

#define NVIC_SYSPRI2 0xE000ED22 // 系统优先级寄存器
#define NVIC_PENDSV_PRI 0x000000FF // 配置优先级
MEM8(NVIC_SYSPRI2) = NVIC_PENDSV_PRI; // 向NVIC_SYSPRI2写NVIC_PENDSV_PRI,设置其为最低优先级
6.2 中断控制及状态寄存器 ICSR
我们需要悬起 PendSVC 中断,我们根据手册地址定义出中断控制寄存器的宏定义:

#define NVIC_INT_CTRL 0xE000ED04 // 中断控制及状态寄存器
#define NVIC_PENDSVSET 0x10000000 // 触发软件中断的值
MEM32(NVIC_INT_CTRL) = NVIC_PENDSVSET; // 向NVIC_INT_CTRL写NVIC_PENDSVSET,用于PendSV
6.3 阶段代码总结
此段代码包括将 PendSVC 中断优先级降至最低,并且悬起 PendSVC 中断,也就是让内核进入中断,并且进入 PendSVC 的异常中断函数。
#define NVIC_INT_CTRL 0xE000ED04 // 中断控制及状态寄存器
#define NVIC_PENDSVSET 0x10000000 // 触发软件中断的值
#define NVIC_SYSPRI2 0xE000ED22 // 系统优先级寄存器
#define NVIC_PENDSV_PRI 0x000000FF // 配置优先级#define MEM32(addr) *(volatile unsigned long *)(addr)
#define MEM8(addr) *(volatile unsigned char *)(addr)void triggerPendSVC (void)
{MEM8(NVIC_SYSPRI2) = NVIC_PENDSV_PRI; // 向NVIC_SYSPRI2写NVIC_PENDSV_PRI,设置其为最低优先级MEM32(NVIC_INT_CTRL) = NVIC_PENDSVSET; // 向NVIC_INT_CTRL写NVIC_PENDSVSET,用于PendSV
}int main ()
{triggerPendSVC();for (;;) {__nop();}return 0;
}__asm void PendSV_Handler ()
{BX LR
}
6.3 PendSVC 自动执行的步骤
如果我们保存现场,并不是所有的寄存器都需要我们手动保存再写入,PendSV 中断会像普通中断一样会帮我们自动保存当退出时,会帮我们自动恢复这些寄存器。
响应异常的第一个行动,就是自动保存现场的必要部分:依次把 xPSR、PC、LR、R12 以及 R3‐R0 由硬件自动压入适当的堆栈中。如果当响应异常时,当前的代码正在使用 PSP,则压入 PSP,即使用线程堆栈˗否则压入 MSP,使用主堆栈。一进入了服务例程,就将一直使用主堆栈。
为什么不压栈 R4‐R11 寄存器呢,因为 ARM 上,有一套的C语言编译调用标准约定 (C/C++ Procedure Call Standard for the ARM ArchitectureNJ, AAPCS, Ref5) 它使得中断服务例程能用 C语言编写。使汇编后的文件符合标准。
下图为进入中断异常后 ARM 内核自动保存的寄存器:

现在我们知道了 PendSV 会帮我们自动压栈 xPSR、PC、LR、R12 以及 R3‐R0,然后等我们执行完毕 PendSV 中的代码后,退出 PendSV 时中断时则会自动回弹。当然,我们我们需要实现保存完整的现场,则需要手动压栈 R4‐R11 并且恢复。
6.5 修改后的 PendSVC 函数
在阅读下面这段汇编的时候,我们先有一个顺序捋清:
blockPtr 的值 = block 的地址
block 的值 = stackBuffer[1024] 的地址
__asm void PendSV_Handler ()
{//相当于c语言extren 导入blockPtr这个变量IMPORT blockPtr// 加载寄存器存储地址LDR R0, =blockPtr //R0等于blockPtr变量地址LDR R0, [R0] //blockPtr解地址 此时R0等于BlockPtr的值,也就是block的地址LDR R0, [R0] //这还没完 此时R0的值只是block的地址,还需再解一次才能得到stackBuffer[1024]的地址// 保存寄存器STMDB R0!, {R4-R11} //递减读取进数组中,所以我们用stackBuffer[1024]的地址// 将最后的地址写入到blockPtr中LDR R1, =blockPtr //R1等于blockPtr变量地址LDR R1, [R1] //blockPtr解地址 此时R1等于blockPtr的值,也就是block的地址STR R0, [R1] //此时R0是栈顶,也就是stackBuffer[1024-7]的地址 此时将stackBuffer[1024-7]的地址赋给block的值// 修改部分寄存器,用于测试ADD R4, R4, #1ADD R5, R5, #1// 恢复寄存器LDMIA R0!, {R4-R11} //弹出寄存器 恢复到R4-R11// 异常返回BX LR //LR保存了子程序返回的代码地址 BX返回
}
6.6 效果演示
在压栈前 R4-R11 寄存器的值
测试修改 R4 R5 的值
在出栈后 R4-R11 寄存器的值