上次我们创建了两个任务task1与task2,这次我们来实现tsak1与task2两个任务的切换。先了解下CM3内核的一些常用指令,不一定非要记住,只需要熟能生巧!先来看MSR和MRS指令。

MRS  加载特殊功能寄存器的值到通用寄存器
MSR  存储通用寄存器的值到特殊功能寄存器

问题一:什么是特殊功能寄存器?包括哪些?

问题二:什么是通用寄存器?包括哪些?

这里也涉及到特殊功能寄存器和通用寄存器,好了,科普一下,有图有真相。

再来看一个指令:CBZ,这个也比较常用,主要用来做判断,类似于C语言的if。具体格式和使用如下:

CBZ  比较(Compare),如果结果为零(Zero)就转移(只能跳到后面的指令)

本次两个任务间的切换使用了下面一条指令:CBZ  R0, PendSVHander_nosave,主要作用是判断R0的值是否为0如果为0就会进入PendSVHander_nosave段去执行。

趁热打铁,再来看STMDB指令与LDMIA指令,这个很重要,用于批量写入和批量加载多个寄存器,IA:表示采用自增方式,DB:表示采用自减方式。

LDMIA   批量加载,并且在加载后自增基址寄存器
STMDB   批量存储,并且在存储后自减基址寄存器

本次两个任务间的切换使用了下面两条指令:STMDB R0,{R4-R11} 与 LDMIA R0!,{R4-R11} 分别表示将R0寄存器的值批量存储到R4-R11,后面一条指令是从R4-R11里面批量加载值到R0中。在实现任务切换的过程中我们需要很多这样的操作,后面我们会慢慢熟悉。

好了,关于CM3的指令本次暂且了解这么多,开始进入正题,首先用任务结构体创建两个任务,具体代码如下:

//创建两个任务,OS_Task1与OS_Task2
um_os  OS_Task1;
um_os  OS_Task2;

接下来就是任务的具体实现函数,分别是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();}
}

关于umTaskSched()任务调度器函数,先来聊几句,什么是任务调度器?字面意思很简单就是分配时间段执行任务,上一时刻让这个任务执行,下个时刻又让另外一个任务执行,实现任务之间的切换。哪如何切换呢?切换任务的本质是什么?很简单,切换就是将PC指针指向要执行任务函数的入口处,切换的本质就是PC值的改变,关于切换后前一个的任务状态信息如何保存?以及切换前准备要切换的任务的状态信息如何读取?这些问题先留在后面。在了解任务调度器之前,需要事先定义 *currentTask  *nextTask; *taskTable[2] 三个指针。currentTask指针用来指向当前运行的任务,nextTask指针用来指向下一个即将运行的任务,taskTable[2]是任务数组列表,表示可以存储2个任务。

um_os  *currentTask;   //用来指向当前运行的任务
um_os  *nextTask;     //用来指向下一个即将运行的任务
um_os  *taskTable[2];  //任务数组列表

初始化期间,将创建两个任务,OS_Task1与OS_Task2的地址存入任务数组列表中,taskTable[0]中存入OS_Task1的地址,taskTable[1]存入OS_Task2的地址。

//对任务数组列表初始化
taskTable[0] = &OS_Task1;
taskTable[1] = &OS_Task2;

我们再次来看任务调度器函数,就很明白了,里面只是做了判断,如果当前任务指针currentTask指向任务列表taskTable[0]时,将会切换到下一个任务,即是nextTask = taskTable[1],需要注意的是,任务调度器函数的最后会执行umTaskSwitch()函数,我猜一下哈,应该是改变PC的值,然后保存当前任务的状态值,加载要执行的任务状态值。任务调度器函数具体代码如下:

//任务调度器
void umTaskSched()
{if(currentTask == taskTable[0]){nextTask = taskTable[1];}else{nextTask = taskTable[0];}umTaskSwitch();
}

到了关键核心部分了,我们来啃这块难啃的骨头,这里如果能深入理解,你就会两个任务的切换了,那多个任务切换也就不远了。我们来看一下umTaskSwitch()函数到底做了什么,具体代码如下,很明显,只是触发了PendSV异常中断,那我们又要跟CM3汇编指令打交道了,不要畏惧,一一攻破,杀他个片甲不留!

void umTaskSwitch()
{MEM32(NVIC_INT_CTRL) = NVIC_PENDSV_SET;
}

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

插点题外话,撩一下R13寄存器。很明显我们需要明白PSP是个什么东西,它为0时意味着什么?先来了解下R13寄存器,R13寄存器为堆栈指针。在 CM3 处理器内核中共有两个堆栈指针,立即推:也就支持两个堆栈。

01.主堆栈指针(MSP),或写作 SP_main。这是缺省的堆栈指针,它由 OS 内核、异常服务例程以及所有需要特权访问的应用程序代码来使用。
02.进程堆栈指针(PSP),或写作 SP_process。用于常规的应用程序代码(不处于异常服用例程中时)。要注意的是,并不是每个应用都必须用齐两个堆栈指针。简单的应用程序只使用 MSP就够了。

这里似乎明白了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;  //触发PendSVMEM8(NVIC_SYSPRI2) =  NVIC_PENDSV_PRI;   //设置PendSV优先级
}

再回来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是个什么鬼?

typedef struct
{um_uint32_t  *stack_addr; //指向该任务堆栈地址
}um_os

原来是个任务结构体,该结构体内部定义了一个指针,用于指向任务的堆栈地址。到这里就很清楚了,LDR R0,[R2]的操作了,R2是currentTask的值,即是stack_addr指针的地址,立即推:该操作就是从currentTask取出堆栈的地址。

继续啃骨头,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指向的那个任务中。半块骨头已啃完,内心那种激动地心情无法用言语表达!

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

//PendSV异常处理函数
__asm__ void PendSV_Handler(void)
{IMPORT currentTaskIMPORT nextTaskMRS R0, PSP                   //将PSP寄存器的值保存到R0中CBZ R0,PendSVHander_nosave    //判断PSP寄存器的值是否为0 从而判断是否是// FirstTask任务触发还是umTaskSwitch()触发    STMDB R0!,{R4-R11}LDR R1, = currentTaskLDR R1,[R1]STR R0,[R1]PendSVHander_nosaveLDR R0, = currentTask //完成了nextTask向currentTask赋值操作LDR R1, = nextTaskLDR R2,[R1]STR R2,[R0]        LDR R0,[R2]  //从currentTask取出堆栈的地址LDMIA R0!,{R4-R11} //批量加载R4-R11寄存器MSR PSP,R0        ORR LR,LR,#0x04BX  LR
}

下面开始Debug测试一下,具体测试如下:

1.使用F10到FirstTask()函数

2.使用F11进入到FirstTask()函数内部,接着按F11将PSP的值设置为0

3.使用F11 触发PendSV异常,执行完毕后会进入PendSV_Hander函数中

4.接着F11,MRS R0, PSP指令后,R0的值将会刷新为0

5.接着F11,由于CBZ R0,PendSVHander_nosave指令,此时R0的值为0,会被跳转到PendSVHander_nosave代码段

6.接着F11,执行LDR R0, = currentTask  LDR R1, = nextTask  LDR R2,[R1]  STR R2,[R0] 四条指令后,会完成nextTask向currentTask赋值操作

7.接着F11,执行LDR R0,[R2],从currentTask取出堆栈的地址给R0

8.接着F11,执行LDMIA R0!,{R4-R11} 从堆栈取值批量加载R4-R11寄存器中

9.接着F11,执行MSR PSP,R0 更改PSP值,为进程间切换做准备

10.最后程序会跳转到第一个要执行中,实现了从FirstTask()任务到任务1的跳转

11.下面就是任务1与任务2的切换,FirstTask()任务已经完成了它的使命。接着F11到umTaskSched()

12.F10进入umTaskSched()任务调度函数中,这里完成了任务的切换,最终会进入umTaskSwitch()函数

13.再次F10 进入umTaskSwitch()函数,在umTaskSwitch()函数中只是完成了PendSV异常的触发,再一次回到了汇编执行区域,这个难缠的家伙

14.这次PSP的值已经不再是0的,我们将PSP的值传给R0寄存器,需要注意的是PSP的值是当前任务的堆栈地址。这样我们就可以轻松保存当前任务的状态了

15.接着F11 ,保存当前任务状态,将R4-R11批量存入R0中

16.接着F11,接着将R0的值写入该任务堆栈的地址中

17.后面就是重复了,一路F11过去,看是否进入任务2中

18.按照我们的计划已经进入任务2中,在逻辑分析仪中已经看到任务2中变量产生的波形

19.最后就是验证,能否从任务2切换回任务1,所以一路F11过去。很幸运,已经可以实现了两个任务之间的切换了

20.最后来展示一个全速运行的逻辑分析仪的波形,以此纪念我打字的辛苦

.

从0到1写嵌入式操作系统---------------------------4尝试两个任务的切换相关推荐

  1. RTOS ---嵌入式操作系统之时钟节拍下的任务切换

    嵌入式操作系统之时钟节拍下的任务切换 嵌入式操作系统如FreeRTOS.FreeRTOS 中任务切换的过程, 提到触发任务切换的两种情况 : 高优先级任务就绪抢占和同优先级任务时间共享(包括提前挂起) ...

  2. 《自己动手写嵌入式操作系统》阅读笔记之操作系统小知识

       微内核与大内核 微内核与大内核在操作系统中应用广泛,是两种截然相反的设计思想."这于CPU设计中的RISC和CISC架构类似."所谓RISC是指精简指令集,RISC指令比较少 ...

  3. 一步步写嵌入式操作系统 arm相关知识

    arm模式分类 1.系统模式 2.用户模式 3.中断模式 4.快速中断模式 5.管理模式 6.中止模式 7.未定义模式 其中除系统模式和用户模式之外的模式为异常模式或特权模式,对应如下的异常状态 ar ...

  4. (1)从1开始写一个操作系统

    第一章 前言 偶然间使用到了RTX51-tiny做一些东西,它是keil自带的51操作系统,以小巧占用资源少著称,这里不细谈它是如何实现的,反正是一个真正的基于时间片的多任务系统. 往往我们在使用单片 ...

  5. 自制嵌入式操作系统 DAY1

    遥想当年刚学习操作系统的时候,很难理解教科书中关于线程/进程的描述.原因还是在于操作系统书上的内容太过抽象,对于一个没有看过内核代码的初学者来说,很难理解各种数据结构的调度.后来自己也买了一些造轮子的 ...

  6. 如何区分嵌入式系统和嵌入式操作系统

    一 什么是嵌入式系统  嵌入式系统一般指非pc系统,有计算机功能但又不称之为计算机的设备或器材.它是以应用为中 心,软硬件可裁减的,适应应用系统对功能.可靠性.成本.体积.功耗等综合性严格要求的专用计 ...

  7. 篇3:嵌入式系统和嵌入式操作系统

    一 什么是嵌入式系统 嵌入式系统一般指非PC系统,有计算机功能但又不称之为计算机的设备或器材.它是以应用为中心,软硬件可裁减的,适应应用系统对功能.可靠性.成本.体积.功耗等综合性严格要求的专用计算机 ...

  8. 嵌入式系统和嵌入式操作系统

    嵌入式系统和嵌入式操作系统 西南交通大学电气学院 张湘 肖建 2004-10-2 文章从概念.特点.种类等不同方面就嵌入式系统和嵌入式操作系统做了介绍. 一 什么是嵌入式系统 嵌入式系统一般指非PC系 ...

  9. [从 0 开始写一个操作系统] 一、准备知识

    从 0 开始写一个操作系统 作者:解琛 时间:2020 年 8 月 29 日 从 0 开始写一个操作系统 一.准备知识 1.1 实现方案 1.2 gcc 1.2.1 AT&T 汇编基本语法 1 ...

最新文章

  1. 为jQuery的$.ajax设置超时时间
  2. 查linux还是unix,C、C++判断操作系统是Linux、windows还是Unix
  3. codeforces CF438D The Child and Sequence 线段树
  4. Spring中使用XML方式导入Spring配置文件,Boot中使用全注解导入Spring配置
  5. 再读阿朱的《走出软件作坊》摘抄整理——20140617
  6. 【编译工具系列】之GCC文件关联
  7. 光电整纬机(日本世联电子株式会社)
  8. Linux安全配置规范
  9. Erlang编程语言的一些痛点
  10. 拓端tecdat|R语言中广义线性模型(GLM)中的分布和连接函数分析
  11. 2018java面试(1)- 自我介绍和项目介绍
  12. iTextSharp笔记
  13. python 存根_如何用Python编写类方法的存根
  14. 【AC.HASH】OpenHarmony啃论文俱乐部——哈希技术:综述和分类(译)
  15. 腾讯互娱技术总监张正:《天涯明月刀》后台技术创新
  16. 关于南通大学教务学生管理公众微信的用户体验。
  17. 统计学的那些冷门思考(各种检验+中心极限)
  18. linux 内存清理/释放命令
  19. JSONObject.parseObject
  20. 如何取消Office 正版增值验证

热门文章

  1. 谷歌搜索广告可以加产品图片吗?
  2. oracle取两个表的差,Oracle查询两表相差的数据
  3. OpenCV实现图像转换为素描效果
  4. 电子鲍伊-迪克测试如何实现蒸汽灭菌的批次控制?
  5. JQuery插件秀:生成PDF文件(文本+上传图片+电子签名)
  6. 数据库系统 - 家庭教育平台设计开发
  7. 微信小程序报错 Now you can provide attr `wx:key` for a `wx:for` to improve performance.
  8. 美图T9 怎么打开heic文件,怎么查看hei
  9. 高薪请你来,就是让你来跑脚本的吗?(性能测试大分享)
  10. linux 下三款监控网卡流量的软件iptraf iftop nload