我们先讲OS部分,大家知道,系统启动之后,是从复位开始的
那么复位对应的栈,就是MSP栈
Cortex系列,存在两个栈 MSP和PSP
之所以分为两个栈,是为了给OS用的
注意,是专门为OS用的,那么具体如何用,就是本次OS的重点
普通裸奔下,系统只有一个SP栈,当有中断的时候,中断就在这个SP栈压栈即可
在有OS的时候,它不是这么设计了,而是分为了三块
应该说两块,我画一个图
这个是程序员自行设定的还是ARM公司规定的?
ARM公司的芯片就这么设计的
你可以只用MSP,那就是裸奔
也可以只用MSP,带OS
这个就是以前的方式
也可以支持MSP和PSP,带OS
也就是说,芯片开始支持OS的一些特性
如图,复位之后,栈顶在MSP位置
复位之后,就运行系统初始化,之后进入main等
这些行为,都在MSP中,用的都是MSP
那么这个MSP的栈多大呢
我以前认为,栈的大小,是编译器根据软件来决定的
实际上我同事发现不是,而是Keil指定了一个大小
Stack_Size EQU 0x00000400
这句话0x400,也就是1K附近的大小,1024个字节,keil默认的
这就是说,若你在软件定义中,动态变量的数组,超过1K,必然会导致系统死机
因为内存越界
这个我们做了实验,证明是如此的了
问:我测试的结果是在 zi rw bss 分配空间的后面
OK,一般想着去修改这个大小吧
但是在OS中,一般不需要修改,因为,实际的运行,是在OS中
这就是PSP的好处
问:即使可以修改 大小应该有限制的吧?
若不用PSP,那你就要根据实际项目来修改了
其实msOS原来的代码,就有一个地方,动态内存分配是比较大的,有512字节
所以这种做法,若不修改0x400,是比较危险的
OS给业务逻辑和菜单界面定义了两个数组
其实这两个数组,是定义在静态变量中的
这两部分都是静态RAM的一部分,只是我们人为的把它用来当作两个任务的栈用
在os.c中定义了两个任务的栈大小
这个里面定义了两个0x400 字的数组
0x400也就是1K的word,而不是byte
我们可以发现,很多系统,一启动初始化后,马上就开启任务
我以前不是很明白,现在明白了
就是因为默认的栈不大
而OS分配的栈比较大,可以做很多初始化工作
任何比较随意的分配动态RAM
当然,这个只是一个理由
系统默认分配的栈比较有限
一些初始化,比如存储类的,就需要很大的ram
而这种情况下,可以少用内存分配这方面的指令
比如malloc,直接在动态RAM中处理了,是一个比较不错的方式
少用堆而多用栈,因为任何的OS,都想着预防万一,OS的栈分配的往往比较大
都是往大里分配的
而堆,你分配了就分配了,会把本底抬高
OK,这是我的一个认识,早点进入OS,在任务中处理
接下来讲,如何用PSP
大家看os.c
ARM公司,作为芯片公司,它是需要研究os的,之后硬件来迎合os
传统栈只有一个,会导致一个问题
那就是中断来的时候,这个中断既可以在业务逻辑中也可以在菜单界面中
也就是说,中断的ram分配,在这两个任务中分配
不确定性比较大
相当于,一个中断在两个任务中晃悠
这个,Cortex权威里面的一个解释,是怕中断出现异常,打乱了任务
所以,arm公司想着,给中断提供独立的一个栈,那就是msp
它跟复位是统一共用的
也就是说,复位的时候,用的就是msp,中断的时候,也要用msp
而任务呢,工作在psp中
那么,在建立任务,进入调度的时候,需要一个进入PSP的工作
退出任务的时候,需要退出PSP,进入MSP的工作
需要这么一个切换
这部分代码,接下来讲解
以上,大家明白吗,对应这张图来看
本质上讲,中断今后占用0x400 byte那部分去了
业务逻辑和菜单界面,都占用0x400 word哪儿去了
所以,切换任务,就只要切换PSP即可
N个任务,就要有N个任务表,这个表里面,放PSP的内容
这个是任务的数据结构
最精简版本,只有三个成员,非常简单
第一个就是PSP内容,栈顶的指针
第二个Message,就是消息,因为msOS基于消息处理的,只需要一个消息即可,32bit的
第三个是Delay,就是节拍延时用的,用于一个任务的延时
那么Message和Delay,都不需要去关心
任务的切换,其实就是切换StackTopPointer
我们用Task定义了两个变量,一个是业务逻辑的任务表,一个是菜单界面的任务表
大家需要搞明白的是,在msOS中,有且只有两个任务
那就是说,不是我运行,就是你运行了,两个中取一个
所以,这个特点决定了,不需要优先级
只需要我想要谁运行
比如,有消息过来的时候,就是业务逻辑运行
业务逻辑运行完了,就切换到菜单界面
所以,这个任务切换的逻辑关系非常简单
所以我们看
Start函数,在两个任务注册之后,执行的
第一步注册节拍,实现超时等待功能
第二步就是把当前的任务和最高级的任务,都指定为业务逻辑
很重要的一步是 InitializePsp
就是要初始化PSP
这个初始化,给了他一个0值
这是让PSP为0值,这个的目的,是为了后面用于判断用的
这个稍等一下讲
向高地址增长?
向下递减的
因为第一次进入任务,PSP是没有值的,需要把业务逻辑任务表的栈指针加入进来
栈是向下递减的,一般都是如此,一般
所以需要先把PSP默认为0,用于第一次装载业务逻辑的栈指针
之后就是任务切换了,用了一句触发语句,中断触发PendSV
#define SwitchContext() NVIC_INT_CTRL_REG = NVIC_PENDSVSET
就是说这些都准备好,启动一个中断,在中断中完成任务的切换
那么这个中断是一段汇编代码
我把汇编嵌入到os.c中
import,是包含外部变量
任务切换主要是4个变量的切换
分为两组
当前任务及栈指针
接下来的任务和栈指针
原ucos,则为当前任务及栈指针
最高优先级的任务与栈指针
因为msOS,没有了最高优先级概念,所以变成下一个任务了
后面我写了注释
第一句话,关闭中断
第二句话,就是基于PSP来判断,因为我们刚开始让PSP为0,所以第一次,不能保存栈,因为这个是在复位main中的栈,也就是msp,不能保存
而是要直接把业务逻辑的栈指针调过来
这就是psp要初始化为0的根本原因
因为第一次,不能保存栈顶
若都用msp的话,就不存在这个问题,直接保存即可,不需要判断了
之前上传的那个版本,就是这样子的,你看代码,是没有PSP判断的
而是直接用一个MSP来存储的
嗯,是的
后面的大家比较容易看懂了,就是存储栈内容,交换栈内容
MSR PSP, R0 // 栈顶写入PSP
ORR LR, LR, #0x04
这两句话,第一句是把新栈顶写入到PSP,因为这个是在中断中执行的,中断是工作在MSP中的
所以,第二句话,把LR的倒数第三位,也就是0100,1这一位置一,之后BX之后,会恢复到PSP中
也就是任务还是用PSP了
以上,就是整个任务的切换过程
那么,任务初始化,是需要做两件事情
InitTaskStack
InitTaskTable
初始化栈,和初始化表
这一段代码,看上去怪怪的
其实,很简单,就是栈的压栈顺序,如此而已
任务的切换,本质上讲,内容都在RAM栈中,只要把CPU内部的通用寄存器和几个特殊寄存器内容保存到栈中,再把新的任务的栈中的寄存器的内容加载回来即可
而中断自动保存的,是R12,R0~R3
需要手工保存的,是R4~R11
刚才任务切换的时候,也就是保存了R4~R11,因为其它的,自动保存了
这个里面很多值,看起来很奇怪
其实这些是可以任意值的
比如0404040404,表示这个是R4
写的人故意如此的,你改成0000004,也是一样的
甚至0000000,也是可以的
但xPSR,特殊寄存器,不要乱动
第二个栈顶,是计算出来的
也就是分配的两个栈的栈顶地址
这个OS,基本上讲完了,大家看一下代码,结合我刚才说的,理一下
目前msOS这个任务切换,实在太简单了
一眼就看透了
所以,自己脑子里非常清晰
这个其实,就是三个杯子,两碗有水,如何交换的问题
两个任务的表和栈,算两个有水的杯子
CPU,就是那个空杯子
用户1602177 2014-9-2 13:44