[应用相关]

尝试两个任务的切换

[复制链接]
1133|38
手机看帖
扫描二维码
随时随地手机跟帖
dingbo95|  楼主 | 2019-6-16 19:36 | 显示全部楼层 |阅读模式
上次我们创建了两个任务task1与task2,这次我们来实现tsak1与task2两个任务的切换。先了解下CM3内核的一些常用指令,不一定非要记住,只需要熟能生巧!先来看MSR和MRS指令。
MRS  加载特殊功能寄存器的值到通用寄存器
MSR  存储通用寄存器的值到特殊功能寄存器

使用特权

评论回复
dingbo95|  楼主 | 2019-6-16 19:37 | 显示全部楼层
问题一:什么是特殊功能寄存器?包括哪些?
问题二:什么是通用寄存器?包括哪些?
这里也涉及到特殊功能寄存器和通用寄存器,好了,科普一下,有图有真相。
41475d0629eb46d59.png

使用特权

评论回复
dingbo95|  楼主 | 2019-6-16 19:38 | 显示全部楼层
再来看一个指令:CBZ,这个也比较常用,主要用来做判断,类似于C语言的if。具体格式和使用如下:
CBZ  比较(Compare),如果结果为零(Zero)就转移(只能跳到后面的指令)
本次两个任务间的切换使用了下面一条指令:CBZ  R0, PendSVHander_nosave,主要作用是判断R0的值是否为0如果为0就会进入PendSVHander_nosave段去执行。

使用特权

评论回复
dingbo95|  楼主 | 2019-6-16 19:38 | 显示全部楼层
趁热打铁,再来看STMDB指令与LDMIA指令,这个很重要,用于批量写入和批量加载多个寄存器,IA:表示采用自增方式,DB:表示采用自减方式。
LDMIA   批量加载,并且在加载后自增基址寄存器
STMDB   批量存储,并且在存储后自减基址寄存器
本次两个任务间的切换使用了下面两条指令:STMDB R0,{R4-R11} 与 LDMIA R0!,{R4-R11} 分别表示将R0寄存器的值批量存储到R4-R11,后面一条指令是从R4-R11里面批量加载值到R0中。在实现任务切换的过程中我们需要很多这样的操作,后面我们会慢慢熟悉。

使用特权

评论回复
dingbo95|  楼主 | 2019-6-16 19:39 | 显示全部楼层
好了,关于CM3的指令本次暂且了解这么多,开始进入正题,首先用任务结构体创建两个任务,具体代码如下:
//创建两个任务,OS_Task1与OS_Task2
um_os  OS_Task1;
um_os  OS_Task2;

使用特权

评论回复
dingbo95|  楼主 | 2019-6-16 19:39 | 显示全部楼层
接下来就是任务的具体实现函数,分别是void task1(void *param)与void task1(void *param),在任务函数中是一个while(1)的死循环,有的同学会质疑了哈,这个不就是一直在里面执行了吗?怎么跳出去?答:问的好,本人特意加了一个函数,umTaskSched();该函数出现在两个任务函数的while(1)中,很多人也许已经猜到了,这不是任务调度器吗?哈哈,真聪明,是的,本次先来一个简单的任务调度,简单实现了两个任务之间的调度,不是多任务哦!不要急,后面会升级!任务函数具体代码如下:
void task1(void *param)
{
  while(1)
        {
          task1Flag = 1;
                delay(1000);
                task1Flag = 0;
                delay(1000);
                umTaskSched();
        }
}
void task2(void *param)
{
  while(1)
        {
          task2Flag = 1;
                delay(1000);
                task2Flag = 0;
                delay(1000);
                umTaskSched();
        }
}

使用特权

评论回复
dingbo95|  楼主 | 2019-6-16 19:39 | 显示全部楼层
关于umTaskSched()任务调度器函数,先来聊几句,什么是任务调度器?字面意思很简单就是分配时间段执行任务,上一时刻让这个任务执行,下个时刻又让另外一个任务执行,实现任务之间的切换。哪如何切换呢?切换任务的本质是什么?很简单,切换就是将PC指针指向要执行任务函数的入口处,切换的本质就是PC值的改变,关于切换后前一个的任务状态信息如何保存?以及切换前准备要切换的任务的状态信息如何读取?这些问题先留在后面。在了解任务调度器之前,需要事先定义 *currentTask  *nextTask; *taskTable[2] 三个指针。

使用特权

评论回复
dingbo95|  楼主 | 2019-6-16 19:40 | 显示全部楼层
currentTask指针用来指向当前运行的任务,nextTask指针用来指向下一个即将运行的任务,taskTable[2]是任务数组列表,表示可以存储2个任务。
um_os  *currentTask;   //用来指向当前运行的任务
um_os  *nextTask;     //用来指向下一个即将运行的任务
um_os  *taskTable[2];  //任务数组列表

使用特权

评论回复
dingbo95|  楼主 | 2019-6-16 19:40 | 显示全部楼层
初始化期间,将创建两个任务,OS_Task1与OS_Task2的地址存入任务数组列表中,taskTable[0]中存入OS_Task1的地址,taskTable[1]存入OS_Task2的地址。
//对任务数组列表初始化
taskTable[0] = &OS_Task1;
taskTable[1] = &OS_Task2;

使用特权

评论回复
dingbo95|  楼主 | 2019-6-16 19:40 | 显示全部楼层
我们再次来看任务调度器函数,就很明白了,里面只是做了判断,如果当前任务指针currentTask指向任务列表taskTable[0]时,将会切换到下一个任务,即是nextTask = taskTable[1],需要注意的是,任务调度器函数的最后会执行umTaskSwitch()函数,我猜一下哈,应该是改变PC的值,然后保存当前任务的状态值,加载要执行的任务状态值。

使用特权

评论回复
dingbo95|  楼主 | 2019-6-16 19:41 | 显示全部楼层
任务调度器函数具体代码如下:
//任务调度器
void umTaskSched()
{
   if(currentTask == taskTable[0])
         {
           nextTask = taskTable[1];
         }
         else
         {
           nextTask = taskTable[0];
         }
         umTaskSwitch();
}

使用特权

评论回复
dingbo95|  楼主 | 2019-6-16 20:02 | 显示全部楼层
开始啃骨头,来到PendSV异常处理函数这里,一开始只是把 currentTask 和nextTask 导入进来,然后使用MRS指令将特殊功能寄存器PSP的值加载到通用寄存器R0中,暂且不管PSP寄存器是什么鬼,先接着啃,比较指令CBZ R0,PendSVHander_nosave 判断R0寄存器的值是否为0,间接判断PSP的值是否为0,不为0将接着执行下去,为0时将会跳转,跳转到PendSVHander_nosave代码段处开始执行。啃到这里感觉很香,路子要发叉了,纠结要走哪一条路,人生十字路,匆匆忙忙,来来回回,转眼之间还在原处驻留!

使用特权

评论回复
dingbo95|  楼主 | 2019-6-16 20:02 | 显示全部楼层
插点题外话,撩一下R13寄存器。很明显我们需要明白PSP是个什么东西,它为0时意味着什么?先来了解下R13寄存器,R13寄存器为堆栈指针。在 CM3 处理器内核中共有两个堆栈指针,立即推:也就支持两个堆栈。
01.主堆栈指针(MSP),或写作 SP_main。这是缺省的堆栈指针,它由 OS 内核、异常服务例程以及所有需要特权访问的应用程序代码来使用。
02.进程堆栈指针(PSP),或写作 SP_process。用于常规的应用程序代码(不处于异常服用例程中时)。要注意的是,并不是每个应用都必须用齐两个堆栈指针。简单的应用程序只使用 MSP就够了。

使用特权

评论回复
dingbo95|  楼主 | 2019-6-16 20:03 | 显示全部楼层
这里似乎明白了PSP是进程堆栈指针,主要用于常规应用程序代码。如果PSP的值为0即不使用进程堆栈指针,使用主堆栈指针MSP。至于后面怎么玩,我的大体思路是这样的,先在主函数中,添加一个FirstTask()函数,这个跟OS_Task1与OS_Task2完全不同,只是一个普通的执行函数,该函数具体实现代码如下,首先将PSP设置为0,然后再触发PendSV异常。为什么要引入FirstTask()函数?目的是?简单解释一下,FirstTask()函数是没有进行任务切换前要执行的第一个函数,我们在学习ucos操作系统中通常会看到第一个执行任务,该任务是为了创建更多的任务,然后再将自己销毁,让创建任务处于切换中,这里的FirstTask()函数也有类似的意义,不要奢求我能讲的很明白,很多东西需要自己去悟、去悟、……..
void FirstTask()
{
        __set_PSP(0);
  MEM32(NVIC_INT_CTRL) = NVIC_PENDSV_SET;  //触发PendSV
  MEM8(NVIC_SYSPRI2) =  NVIC_PENDSV_PRI;   //设置PendSV优先级
}

使用特权

评论回复
dingbo95|  楼主 | 2019-6-16 20:03 | 显示全部楼层
再回来PendSV这里,这里的代码是不是很香,越来越有意思了,开始分叉,FirstTask()执行完毕,PSP值为0,尽情跳转吧!来到PendSVHander_nosave 代码段,这段主要是用来干嘛的?好让我们带着目的去看,要不然我不看了。先透漏下哈,这里主要是为了进入下一个进程任务做准备,完成任务切换时下一个任务状态的读取,具体如何操作,请听我一一道来。LDR R0, = currentTask 与 LDR R1, = nextTask 是将currentTask和nextTask的地址值加载到R0与R1中,接着LDR R2,[R1] 是从R1地址下取值存到R2中,即取nextTask的值到R2,后面的STR R2,[R0] 就更加明白了,便是往R0地址处写R2操作,即是往currentTask写值,立即推:完成了nextTask向currentTask赋值操作。继续往下LDR R0,[R2]  这个地方有点难以理解,特别是对于C语言功底不是很好的同学,同时也包括我。这里简单跟大家解释一下,currentTask定义形式是:um_os  *currentTask; 那um_os是个什么鬼?

使用特权

评论回复
dingbo95|  楼主 | 2019-6-16 20:04 | 显示全部楼层
typedef struct
{
  um_uint32_t  *stack_addr; //指向该任务堆栈地址
}um_os;
原来是个任务结构体,该结构体内部定义了一个指针,用于指向任务的堆栈地址。到这里就很清楚了,LDR R0,[R2]的操作了,R2是currentTask的值,即是stack_addr指针的地址,立即推:该操作就是从currentTask取出堆栈的地址。

使用特权

评论回复
dingbo95|  楼主 | 2019-6-16 20:04 | 显示全部楼层
继续啃骨头,LDMIA R0!,{R4-R11} 大家都懂,就是批量加载R4-R11寄存器,那么问题来了,值从哪里来?如何加载?从天上来,到人间去!跑偏了哈!值明显从R0寄存器来,上个步骤已经将currentTask任务对应的堆栈地址存到了R0寄存器中了,所以这些值来自currentTask任务的堆栈,至于如何加载,这个问题的确是个问题,先给谁都不好,为了不打架,就约定一个规矩吧,先从大的值开始,依次递减赋值。(明白LDMIA指令的都懂,IA是递减方式!)。现在大家都有值了,准备好开始干活,为进入第一个任务做好准备。到了至关重要的一步了,指令MSR PSP,R0操作,目的是将R0寄存器的值加载到PSP寄存器中,再次触发时由于PSP的值不再为0,不会再进入PendSVHander_nosave 段执行,至于另外一个岔路想做什么,后面再进行分析。最后就是ORR LR,LR,#0x04 退出异常使用SP所指向的堆栈,
BX  LR退出PendSV,不出意外,这是程序已经切换到currentTask指向的那个任务中。半块骨头已啃完,内心那种激动地心情无法用言语表达!

使用特权

评论回复
dingbo95|  楼主 | 2019-6-16 20:04 | 显示全部楼层
接着走另外一个岔路,任务可以被切换,除了恢复下一个要被执行的任务状态,同时也要保存此时即将执行完毕的任务状态,方便下次切换,该岔路就是实现了任务状态的保存。越是到最后了越香,几行短的代码实现了神奇的效果,汇编也没想象的那么难嘛。STMDB R0!,{R4-R11} 实现了批量从R4-R11中加载数据到R0寄存器,LDR R1, = currentTask 将currentTask指向的当前任务的地址加载到R1中,LDR R1,[R1] 加载当前任务的堆栈地址到R1中,STR R0,[R1] 将当前任务的堆栈地址存入R0中。这样当前任务在被切换到其他任务之前就将自己的状态值给保存下来了,方便下以次任务切换。骨头已经啃完,大家觉得香不香,受益匪浅吧!

使用特权

评论回复
dingbo95|  楼主 | 2019-6-16 20:05 | 显示全部楼层
//PendSV异常处理函数
__asm__ void PendSV_Handler(void)
{
         IMPORT currentTask
         IMPORT nextTask
         MRS R0, PSP                   //将PSP寄存器的值保存到R0中
         CBZ R0,PendSVHander_nosave    //判断PSP寄存器的值是否为0 从而判断是否是
                                       // FirstTask任务触发还是umTaskSwitch()触发         STMDB R0!,{R4-R11}
         LDR R1, = currentTask
         LDR R1,[R1]
         STR R0,[R1]

PendSVHander_nosave
   LDR R0, = currentTask //完成了nextTask向currentTask赋值操作
   LDR R1, = nextTask
   LDR R2,[R1]
   STR R2,[R0]        
   LDR R0,[R2]  //从currentTask取出堆栈的地址
   LDMIA R0!,{R4-R11} //批量加载R4-R11寄存器
   MSR PSP,R0        
   ORR LR,LR,#0x04
   BX  LR         
}

使用特权

评论回复
dingbo95|  楼主 | 2019-6-16 20:05 | 显示全部楼层
下面开始Debug测试一下,具体测试如下,1.        使用F10到FirstTask()函数
880045d06309725722.png

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

52

主题

1197

帖子

5

粉丝