实验平台:stm32f10x(cortex-m3)开发板,RTT3.0
资料来源:RTT官网文档及cortex-M3权威指南
关键字:分析RT-Thread源码、stm32、RTOS。
因为我要边学习RTT,边模仿RTT写一个简单的RTOS,所以需要先写调度、上下位切换部分。
关于调度的定义和作用这里就不说。调度的实现基于上下文的切换,所以先要弄清除上下文是怎么切的。
上下文的切换和cpu有关,所以不同的cpu具体操作是不同的,但要实现的功能都是一样的,就是保存上文环境,然后切换到下文环境。我用的开发板是cortex m3内核的,所以需要去看cortex m3的权威指南,即使不想自己写调度器,也非常推荐大家去看,虽然嘴上这么说,其实我自己并没怎么去研究。
我们知道任务(线程)是通过rt_schedule函数来实现的,而rt_schedule的功能就是找到那个最高优先级的线程,然后进行上下文切换rt_hw_context_switch*,之后那个线程就能跑起来了。
所以我们能知道上下文切换做的事情就是把被切换的线程当前的运行状态(跑到哪一行代码,各个变量的参数是多少)给保存起来,好让下一次cpu控制权又切换回来时不会迷路。
这个时候就需要介绍一下cortex-M3的寄存器了。正是这些寄存器记录了cpu当前的运行状态,所以我们在上下文上要保存和恢复的就是这些相关的寄存器了。
Cortex‐M3 处理器拥有R0‐R15 的寄存器组。其中R13 作为堆栈指针SP。SP 有两个,但在同一
时刻只能有一个可以看到,这也就是所谓的“banked”寄存器。
R0-R12:通用寄存器
R0‐R12 都是32 位通用寄存器,用于数据操作。但是注意:绝大多数16 位Thumb 指令只能访问R0‐R7,而32 位Thumb‐2 指令可以访问所有寄存器。
Banked R13: 两个堆栈指针
Cortex‐M3 拥有两个堆栈指针,然而它们是banked,因此任一时刻只能使用其中的一个。
主堆栈指针(MSP):复位后缺省使用的堆栈指针,用于操作系统内核以及异常处理例程(包括中断服务例程)
进程堆栈指针(PSP):由用户的应用程序代码使用。堆栈指针的最低两位永远是0,这意味着堆栈总是4 字节对齐的。
R14:连接寄存器LR
当呼叫一个子程序时,由R14 存储返回地址
R15:程序计数寄存器PC
指向当前的程序地址。如果修改它的值,就能改变程序的执行流
特殊功能寄存器
Cortex‐M3 还在内核水平上搭载了若干特殊功能寄存器,包括
程序状态字寄存器组(PSRs)
中断屏蔽寄存器组(PRIMASK, FAULTMASK, BASEPRI)
控制寄存器(CONTROL)
(一波cp操作)寄存器不多,是不是感觉程序运行起来应该是很复杂的,可能会涉及一大堆东西,但是,就是用这么少的寄存器就可以记录程序的运行状态了。
接下来要介绍两个新的概念了(又要一顿cp了,所以真的必须去看指南)
操作模式和特权极别。
Cortex‐M3 处理器支持两种处理器的操作模式,还支持两级特权操作。
两种操作模式分别为:处理者模式(handler mode,以后不再把handler 中译——译注)和线程模
式(thread mode)。引入两个模式的本意,是用于区别普通应用程序的代码和异常服务例程的代码——包括中断服务例程的代码。
Cortex‐M3 的另一个侧面则是特权的分级——特权级和用户级。这可以提供一种存储器访问的
保护机制,使得普通的用户程序代码不能意外地,甚至是恶意地执行涉及到要害的操作。处理器支持两种特权级,这也是一个基本的安全模型。
也就是说,我们要保存的寄存器,并不能让我们随便的去访问。需要特权级权限,通过触发异常,就可以进入特权级模式,事实上,从用户级到特权级的唯一途径就是异常。
没错,接下来要介绍(cp)异常了。
在ARM 编程领域中,凡是打断程序顺序执行的事件,都被称为异常(exception)。除了外部中断外,当有指令执行了“非法操作”,或者访问被禁的内存区间,因各种错误产生的fault,以及不可屏蔽中断发生时,都会打断程序的执行,这些情况统称为异常。在不严格的上下文中,异常与中断也可以混用。
Cortex‐M3 在内核水平上搭载了一个异常响应系统,支持为数众多的系统异常和外部中
断。其中,编号为1-15 的对应系统异常,大于等于16 的则全是外部中断。除了个别异常
的优先级被定死外,其它异常的优先级都是可编程的。
其中有一个专门用来做上下文切换的,就是PendSV。所以重点就是看PendSV了。
个中事件的流水账记录如下:
现在介绍一下cortex-M3堆栈的实现
Cortex‐M3 使用的是“向下生长的满栈”模型。堆栈指针SP 指向最后一个被压入堆栈的32
位数值。在下一次压栈时,SP 先自减4,再存入新的数值。
Cortex‐M3 在进入异常服务例程时,自动压栈了R0‐R3, R12, LR, PSR 和PC,并且在返回时自
动弹出它们。这就为我们减轻了一些工作,我们只需要那R4-R11给保存起来就好了。
我们可以看到,入栈的顺序并没有按入栈的地址大小规则入栈的。
CM3在看不见的内部打乱了入栈的顺序,这是有深层次的原因的。先把PC与xPSR的值保
存,就可以更早地启动服务例程指令的预取——因为这需要修改PC;同时,也做到了在早期
就可以更新xPSR中IPSR位段的值。
细心的读者一定在猜测:为啥袒护R0‐R3以及R12呢,R4‐R11就是下等公民?是的,就是下等公民。因为程序一般只需要用到Rr0-R3,R12这几个,而且ISR应该短小精悍,不要让系统如此操心——译者注(CM3)
但是我们并不会抛弃R4-R11,因为程序过大也可能要用到R4‐R11,所以需要我们手动保存。
如果读者再仔细看,会发现R0‐R3, R12是最后被压进去的。这里也有一番良苦用心:为
的是可以更容易地使用SP基址来索引寻址,(以及为了LDM等多重加载指令,因为LDM必
须加载地址连续的一串数据)。参数的传递也是受益者:使之可以方便地通过压入栈的R0‐R3
取出(主要为系统软件所利用,多见于SVC与PendSV中的参数传递)。
所以我们才可以直接
MRS r1, psp
STMFD r1!, {r4 - r11}
而不用进行地址偏移操作。
因为Cortex‐M3 使用的是“向下生长的满栈”模型,栈顶SP-0x40用来保存寄存器,不过我们的结构体里面的成员的顺序要相反。因为我们结构体的地址的递增的。即:
struct stack {
R4; //N
...
R11;
R0;
...
LR;
PC;
PSR; //N+0x40
};
更新寄存器
在入栈和取向量的工作都完毕之后,执行服务例程之前,还要更新一系列的寄存器:
? SP:在入栈中会把堆栈指针(PSP或MSP)更新到新的位置。在执行服务例程后,将由MSP负责对堆栈的访问。
? PSR:IPSR位段(地处PSR的最低部分)会被更新为新响应的异常编号。
? PC:在向量取出完毕后,PC将指向服务例程的入口地址。
? LR:LR的用法将被重新解释,其值也被更新成一种特殊的值,称为“EXC_RETURN”,并且在异常返回时使用。
注意位2,这就是我们为什么将LR的位2置1了,因为我们要切到PSP。(ORR lr, lr, #0x04)
好了,现在基础知识就补完了,总结一下上下文切换的流程如下:
1、CPU自动帮我们把R0-R3,R12,PC,LR, PSR自动压栈,压倒PSP/MSP指的位置上。
2、手动把当前被切换的线程寄存器R4-R11保存起来。PSP已经自动指到R11的上一个位置了。
3、将PSP指向新的线程to,并更新寄存器。
4、异常返回以并PSP进程堆栈。修改LR(EXC_RETURN)可以切换PSP,MSP。
现在我们可以直接对着RTT写好的context_rvds.S写一个简单版的上下文切换程序。
关于context_rvds.S上下文切换解释,RTT官网已经写的非常清楚了,这里就不重复简述了。我们只需要写必要的步骤就行了。https://www.rt-thread.org/document/site/programming-manual/porting/porting/
好了,上下文切换就到这里了。主要就是要看cortex-M3权威指南。
这里就简单贴一下代码,源码见链接。
void thread_1(void)
{
int i;
while (1) {
printf("hello.\r\n");
for (i = 0; i < 0xffff; ++i)
;
context_switch((unsigned long)&thread_1_sp, (unsigned long)&thread_2_sp);
printf("hello 1.\r\n");
printf("hello 2.\r\n");
printf("hello 3.\r\n");
}
}
void thread_2(void)
{
int i;
while (1) {
printf("world.\r\n");
for (i = 0; i < 0xffff; ++i)
;
context_switch((unsigned long)&thread_2_sp, (unsigned long)&thread_1_sp);
printf("world 1.\r\n");
printf("world 2.\r\n");
printf("world 3.\r\n");
}
}