Upload
others
View
3
Download
0
Embed Size (px)
Citation preview
μC/OS-II 卧槽宝典(上)
目录
开山废话 ..................................................................... 2
第一章 经典暗器 ....................................................... 2
· 栈 .............................................................................................. 3
· CPU 寄存器 ............................................................................... 3
第二章 两类武器 ....................................................... 4
· 局部变量 .................................................................................. 4
· 全局变量 ................................................................................ 10
第三章 风水轮流转 ................................................. 10
· 怎样实现两个任务的切换 ................................................... 10
· μC/OS-II 的任务切换 .............................................................. 12
· μC/OS-II 怎样实现多任务 ...................................................... 13
· 前后台系统与实时系统对比 ............................................... 14
· 临界区 .................................................................................... 14
第四章 作战指挥部 ................................................. 16
· main.c ...................................................................................... 16
· AppCfg.h .................................................................................. 16
· AppTask.C ................................................................................ 17
第五章 核武器一 ..................................................... 18
· 非结构体全局变量及参数 ................................................... 18
· 结构体全局变量 OS_TCB ..................................................... 19
第六章 OS 初始化战队一 ........................................ 20
· OSInit() ..................................................................................... 20
· OS_InitRdyList() ....................................................................... 21
· OS_InitTCBList() ....................................................................... 22
第七章 OS 任务管理战队一 .................................... 23
· OSTaskCreate() ....................................................................... 23
· OSTaskStkInit() ....................................................................... 24
· OS_TCBInit()............................................................................ 25
· OSTaskDel()............................................................................. 27
第八章 OS 内核调度战队一 ................................... 29
· OS_Sched() ............................................................................. 29
· OS_SchedNew() ...................................................................... 30
· OS_TASK_SW() / OSCtxSw() ................................................... 30
第九章 OS 启动战队一 ........................................... 34
· OSStart() ................................................................................. 34
· OSStartHighRdy() .................................................................... 34
第十章 时间管理战队一......................................... 35
· OSTimeDlyHMSM() ................................................................ 35
· OSTimeDly() ............................................................................ 36
第十一章 关于 Cortex-M3 ...................................... 37
· CPU 寄存器 ............................................................................ 37
· 操作模式与操作级别........................................................... 37
· 中断 ....................................................................................... 39
第十二章 μC/OS-II 代码特色.................................. 40
· 巧妙的全局变量定义与声明 .............................................. 40
· 适当的空间换时间 ............................................................... 41
第十三章 FAQ .......................................................... 43
笔记 ........................................................................... 43
编后语 ....................................................................... 45
Wav
eshar
e 微雪
电子
开山废话
‚开山废话‛并不像其它多数秘籍的‚前言‛ —— 废话居多。
它就算不是很重要,也十分有必要存在①。跳过它,可能导致你误解本宝典,甚至不能很好的‚卧槽‛。
【适用群体】
本书不像其它秘籍,或从操作系统原理开始讲起,或将μC/OS-II讲得透彻。
本书只适合熟悉单片机的研发/学习人员阅读②,且该类人员最好是:
·具备‚C老鸟‛与‚汇编菜鸟‛的双重身份:本文介绍了少量汇编知识,可能只有汇编菜鸟才不会反感。
·具备‚μC/OS迷‛与‚μC/OS初学者‛的双重身份:μC/OS看的迷迷糊糊(基础不是 0),来看卧槽宝典刚好。
【重要说明】
若无特殊说明,本书所研究的μC/OS-II,其运行环境为:
·版本号:V2.91(但,上册会将 V2.91裁剪为一个更小的核心,只删不改③)
·编译器:μVisions V4.23(话说是 Keil出的),编译优化参数设置为‚Level 0‛(不优化)
·硬件:Port103V(话说是微雪电子出的,基于 Cortex-M3 STM32F103VE的简易开发板)
【‚勘误表‛④】
这里集中给出一些‚懒人简写‛(这些简写甚至可能会引起读者误会):
·μC/OS:μC/OS-II⑤
·CM3:Cortex-M3
·反汇编:C语言对应的汇编代码⑥
【自私说明】
·本书的版权归‚微雪电子‛怪浪侠所有。怪浪侠允许自由传播。
·由于作者水平二般,导致本书难免有错漏,请读者用红字标出,并通知⑦笔者修正‚原著‛,谢谢。
·本书中的灰色小号字体,多是一些补充。(尽管,那些补充对个别读者来说很有用。)
-------------------------------------------------------------------------------------------------------------------------------------------
① 伟大提示:如果可能,还是请阅读‚开山废话‛。‚欲练此功,必先自宫;若不自宫,也能成功;若想成功,不能自宫。‛如果没看完整,显然郁闷。
② 之所以说‚只适合嵌入式研发/学习人员阅读‛,是因为,本文研究的μC/OS-II在 Cortex-M3 STM32上跑,所以,建议非专业人士,另寻武功秘籍。
另外,如果读者用过 STM32,阅读本书将更容易明白所以然。毕竟,笔者假定读者具备那些基础知识,所以,跳过了一些相关的基础知识。
③ 为让读者轻装上阵,了解μC/OS-II的核心,笔者决定裁剪μC/OS-II(只删不改)。
④ 之所以,勘误表加了双引号,是因为,如果读者抽读本书,可能会以为本书写错了,其实不然,懒人简写而已。
⑤ 虽然‚II‛实际上应为罗马数字‚II‛,但笔者为方便录入,写为‚II‛。
⑥ 早些时候,我认为,反汇编指的应该是,机器语言转为汇编语言,而高级语言转为汇编语言,最多只能说是‚返汇编‛,但后来,我觉得可能是自己鸡毛。
我们很多命名并不严谨,至少可以说,出卖了我们的‚语感‛。就像我们的化学,能调慢反应速度也叫‚催化剂‛,这个‚催‛字实在无语,催它慢点。。。
考试的时候,还来个判断题:‚催化剂的作用是加快反应速率吗?‛,读起来好顺,勾!之后才知道,催化剂的作用是改变反应速率。
我叉,何不叫‚调化剂‛,或者通俗点:‚调速剂‛。
⑦ 笔者的联系方式是:QQ:2355742825(这是我同事的 Q,本人不直接对外)。
本 Q除了接受错误批判外,也接受技术交流。但请注意,若是要进行技术交流:
·读者毋须以 WORD格式(.doc,.docx)提交; ·读者只能提出 5个疑问,超过 5个疑问,笔者可能拒绝回答。
-------------------------------------------------------------------------------------------------------------------------------------------
第一章 经典暗器
【开门见山的废话】
菜鸟学μC/OS-II 时,一般,会遇到以下问题:
Wav
eshar
e 微雪
电子
·栈是啥玩意?感觉μC/OS-II 到处是‚栈栈栈‛。
·CPU 寄存器又是啥?
栈、CPU 寄存器非常‚经典‛,对于 C 程序员来说‚并不可见‛,所以,本书称它们为‚经典暗器‛。
本章要要做的,就是分析这些暗器。
· 栈
【概念简要说明】
·栈:一种数据结构,是一个虚无①的东西,其实体是 RAM。STM32 的栈就存在 RAM 中。
特点:先进后出,只能在它的一端进行插入和删除操作(这么戳的东西,还挺有用)。
·栈底:栈存储变量的起始地址。
·栈顶:栈中最后压入数据的地址。
·栈顶指针:SP(Stack Point),指向栈顶。SP 的值存放在 CPU 寄存器②中。
当 PUSH、POP 时,栈顶就变了,所以,SP 的值也会跟着自动改变。
【相关重要说明】
有些葵花宝典或神来之笔说,栈顶指针指向当前存储数据的下个存放地址,这是有误的③。
栈顶指针‚名副其实‛就是指向当前存储数据‚最上面‛的地址。
【碉堡指引】
什么时候需要入栈?什么时候需要出栈?什么时候需要改变栈顶指针?后面章节会有少量相关说明,但不多。
读者看的不明白,笔者也没办法,这个问题只能交给他们自己去解决。
解决方法是:学习点汇编语言,推荐学 8051,简单,容易抓住核心,容易明白所以然。
-------------------------------------------------------------------------------------------------------------------------------------------
① 有人问过我:‚什么是栈,跟 CPU寄存器、RAM、ROM有什么关系?‛
我说:‚它是个虚无的东西,跟 RAM有关,跟 CPU寄存器没球关系,除非 CPU寄存器希望它帮忙存下东西(任务切换),才会偶尔的发生下性关系。‛
② SP 存在 CPU 寄存器中,这样,CPU 才能快速访问。对于一般的 MCU 来说,没缓存,没其它地方可供存放,SP 存在 CPU 寄存器中更是毫无疑问。
③ 另外,有些雾里看花的秘籍写道‚SP 总是先加 1 或减 1 再存数据(看堆栈是递减还是递增)‛,之所以说它们‚雾里看花‛,是因为:
·SP 不是加 1 或减 1,即不是偏移 1,而是偏移 n 个堆栈单位长度。在μC/OS-II 里,系统需定义堆栈长度,如:typedef unsigned int OS_STK。
n>=1,由变量类型决定。如用 INT8U、INT16U、INT32U定义变量,则 n=1,如用 FP64定义变量,则 n=2。
(为方便懒人输入,下文将‚偏移 n 个堆栈单位长度‛简写为‚偏移‛。)
·当使用 C 语言存变量时,SP 会自动偏移再存。
(但,这其实没有绝对,请参阅【第二章】;另外,汇编语言存变量,SP 也不会自动偏移再存。)
·当使用汇编语言 PUSH/POP 时,SP 会自动偏移再存。
(如果不是 PUSH/POP,则是直接存入 SP 指向的地址,SP 并不会自动偏移,这将导致 SP 指向的地址中的内容会被覆盖。)
以上说明,笔者已通过实验检验。
自私声明:其实,笔者也可能在本宝典中犯些类似的错误,诚恳的希望读者能提出,这样,笔者才能修正错误。
-------------------------------------------------------------------------------------------------------------------------------------------
· CPU 寄存器
CM3 STM32 的 CPU寄存器如下(以下截图来自μVisions编译器):
Wav
eshar
e 微雪
电子
·R0-R12:平民百姓
·R13(SP):存放堆栈指针
·R14(LR):存放最近一次被转向前①的 PC值
·R15(PC):存放下一条将要执行的指令地址②
·xPSR:特殊功能寄存器
介绍到这,就够后续几章用了,关于 Cortex-M3的 CPU寄存器更多说明,参见【第十一章】。
-------------------------------------------------------------------------------------------------------------------------------------------
① 函数执行到一半,调用了其它函数,或者被中断,则 pc会突然改变,否则,pc将自动指向下一条指令的地址。
② 很多书籍或文档,写的是‚下一条指令地址‛,笔者认为,还是加上‚将要执行‛才不会有歧义。
-------------------------------------------------------------------------------------------------------------------------------------------
第二章 两类武器
【保持队形的废话】
初学者可能会困惑:
·局部变量分配在哪?
·全局变量又是分配在哪?
·它们与栈、CPU 寄存器有什么关系?
有以上困惑的,便是‚μC/OS 迷‛ —— μC/OS 看的迷迷糊糊。恭喜,本章适合你‚观赏‛。
· 局部变量
软件的一切神秘的运行机制全在反汇编代码里。(网上的一句话,写的中肯,挪来用下)
不错,C 代码华丽的外表下,其‚内在‛是什么,如果不清楚,那么真相就不甚明了。
所以,本节,我们要做的就是揭开 C 语言的面纱,看下它的反汇编。下面我们做两个实验,并以此进行研究。
【实验一】
『实验内容』
这里给出一个简单的函数 testX(): Wav
eshar
e 微雪
电子
接下来,我们对程序进行编译,得到以下反汇编代码:
再接下来,我们按代码的执行顺序,观察反汇编代码,并分析局部变量如何分配,赋值。
-----------------------------------------------------
入栈 r1-r3及 lr,因为本程序将改变 r1-r3及 lr的值。
说明:r0的值在本程序中也被改变,但外部没有使用,所以,不需要入栈
-----------------------------------------------------
Wav
eshar
e 微雪
电子
将 r2赋值为 3,并将它的值传给栈顶指向的地址(a[0]的存储地址分配为‚栈顶地址‛)
将 r2赋值为 5,并将它的值传给栈顶+0x08指向的地址(a[2]的存储地址分配为‚栈顶地址+0x08‛)
将 r2赋值为 4,并将它的值传给栈顶+0x04指向的地址(a[1]的存储地址分配为‚栈顶地址+0x04‛)
说明:
·本文使用的编译器,每个 int占 4个字节,两个 int则为 0x08字节
·根据汇编指令 STR 的功能,STR r2,[sp, #0x00]是将 sp+0x00 地址中的值传给 r2,但不改变 sp 的值
-----------------------------------------------------
将栈顶地址中的值(即 a[0]的值)传给 r2,并判断它是否为 3
·如果不是:跳到 0x0800325A处(if(a[1]==4)语句的地址)
·如果是:将 r2赋值为 6,并将它的值传给栈顶地址(即传给 a[0])
将 r1赋值为 4(即传给 b);将 r0赋值为 5(即传给 c)
-----------------------------------------------------
将地址‚栈顶+0x04‛中的值(即 a[1]的值)传给 r2,并判断它是否为 4
·如果不是:跳到 0x08003264处(if(a[2]==4)语句的地址)
·如果是:将 r2赋值为 7,并将它的值传给地址‚栈顶+0x04‛(即传给 a[1])
-----------------------------------------------------
将地址‚栈顶+0x08‛中的值(即 a[2]的值)传给 r2,并判断它是否为 4
·如果不是:跳到 0x0800326E处(‚}‛语句的地址)
·如果是:将 r2赋值为 11,并将它的值传给地址‚栈顶+0x08‛(即传给 a[2]);
将 r0赋值为 5(即传给 c);
判断结束后,出栈 r1-r3及 lr(lr需要放到 pc里,故,代码为 POP {r1-r3,pc})
『实验现象』
我们在μVisions中查看 memory、core,观察到的现象是:
·执行‚函数开始符‘{’‛到‚定义变量‛这部分代码:
入栈:r1-r3、lr,SP值减小。(STM32是递减堆栈,当 PUSH时,SP值减小)
RAM、SP的情况如下:
0x20002224(r1的值) ← SP(PUSH后)
0x20002228(r2的值)
0x2000222C(r3的值)
0x20002230(lr的值)
0x20002234(???)← SP(PUSH前)
Wav
eshar
e 微雪
电子
·执行‚定义变量‛后到‚函数退出符‘}’‛前这部分代码:
SP值不变。数组 a存到了 SP及 SP之后的地址。
RAM、SP的情况如下:
0x20002224(a[0]的值)← SP
0x20002228(a[1]的值)
0x2000222C(a[2]的值)
0x20002230(lr的值)
0x20002234(???)
·执行函数退出符‘}’代码:
出栈,并存放到:r1-r3、pc,SP值增大。(STM32是递减堆栈,当 POP时,SP值增大)
RAM、SP的情况如下:
0x20002224(a[0]的值)→(存到 r1)
0x20002228(a[1]的值)→(存到 r2)
0x2000222C(a[2]的值)→(存到 r3)
0x20002230(lr的值) →(存到 pc)
0x20002234(???) ← SP
『潜在问题』
读者将发现,原 r1-r3并不能被成功恢复,因为它们的值被 a[0]、a[1]、a[2]覆盖了。
确实是这样,笔者遇到这样的问题,也汗了一把。
但,笔者很快就为编译器的圆场,猜想:‚是否被覆盖了也不会导致程序出现问题?‛
带着这个疑问查看了程序,发现,主程序调用 testX()函数后,并没有再使用 r1-r3,因而,不会出现问题。
所以,编译器还是蛮聪明的,知道没有再使用 r1-r3,则肆无忌惮的覆盖它们。
当然,这种聪明有点水分,既然这样,当初何必对 r1-r3压栈,直接不压它们不就得了。
前面,我们提到了‚覆盖‛。‚覆盖‛这种东西,难免让人不安:
如果,加大数组 a的长度,定义为 a[5],会是怎样?该不会连 lr(pc)也覆盖了吧,那就完了!
带着这个疑问,我们将进行‚实验二‛。
【实验二】
『实验内容』
为了一探究竟,笔者将数组 a,加大数组长度,定义长度为 5,并进行其它少量改动,改动后的程序如下:
接下来,我们对程序进行编译,得到以下反汇编代码:
Wav
eshar
e 微雪
电子
由于实验一已进行了详细的分析,所以,这里,我们只给出相应的反汇编代码,而不再进行分析。
『实验现象』
我们在μVisions中查看 memory、core,观察到的现象是:
·执行‚函数开始符‘{’‛到‚定义变量‛这部分代码:
入栈:r4-r5、lr,为数组 a[5]腾出空间:SUB sp,sp,#0x14,SP值减小。
RAM、SP的情况如下:
0x20002210(???)← SP(执行 SUB sp,sp,#0x14后)
0x20002214(???)
0x20002218(???)
0x2000221C(???)
0x20002220(???)
0x20002224(r4的值)← SP(执行 PUSH {r4-r5,lr}后)
0x20002228(r5的值)
0x2000222C(lr的值)
0x20002230(???)← SP(执行 PUSH {r4-r5,lr}前)
·执行‚定义变量‛后到‚函数退出符‘}’‛前这部分代码:
SP值不变。数组 a存到了 SP及 SP之后的地址。
RAM、SP的情况如下:
0x20002210(a[0]的值)← SP
0x20002214(a[1]的值)
0x20002218(a[2]的值)
0x2000221C(a[3]的值)
0x20002220(a[4]的值)
0x20002224(r4的值)
0x20002228(r5的值)
0x2000222C(lr的值)
Wav
eshar
e 微雪
电子
0x20002230(???)
·执行函数退出符‘}’代码:
出栈,并存放到:r4-r5、pc,SP值增大。
RAM、SP的情况如下:
0x20002210(a[0]的值)← SP(执行 ADD sp,sp,#0x14前)
0x20002214(a[1]的值)
0x20002218(a[2]的值)
0x2000221C(a[3]的值)
0x20002220(a[4]的值)
0x20002224(r4的值)← SP(执行 ADD sp,sp,#0x14后) →(存到 r4)
0x20002228(r5的值) →(存到 r5)
0x2000222C(lr的值) →(存到 pc)
0x20002230(???)← SP(执行 POP {r4-r5,lr}后)
『实验一之潜在问题续』
实验一,我们提到了‚覆盖‛问题,之后,基于此问题,展开了实验二。
现在,我们来回顾下,‚覆盖‛问题最终如何解决。
由于,代码 SUB sp,sp,#0x14的作用,局部变量分配的地址不与使用过的地址重叠,所以,避免了‚覆盖‛。
同时,由于,代码 ADD sp,sp,#0x14的作用,SP的值也恢复到调用函数前的值。
调用一个函数如此,调用 N个函数当然也如此。(如果按此方法,调用函数退出后,总能得到正确的 SP。)
【总结】
『局部变量与‚CPU寄存器、栈‛之间的关系』
·当局部变量不多的时候,将它们分配到 CPU寄存器中,这样,读写速度是最快的。
这是μVisions编译器编译 STM32程序的规则,并不表示所有的编译器都这么做。
·当局部变量较多的时候(笔者有另外经过测试)或局部变量为数组,‚习惯‛的将它们分配到 RAM中。
这种情况,编译器并不会跳过任何 RAM,而是‚紧挨着‛原 SP值的地址分配局部变量。
并不是只有μVisions编译器这样处理,作为一个‚正常‛的 C编译器就必须这样。
其实,变量怎么分配,C程序员不必 Care,这活由编译器去干①。但由于我们要研究操作系统,才需要清楚。
由前面的分析,我们知道:局部变量可能存放在 CPU寄存器 Rn中,也可能存放在与 SP相关的 RAM中(栈)。
这是个非常重要的特点,后续,我们分析 MCU需要做什么操作,就要根据这一特点。
『编译器需要怎么做,才能让程序正常运行』
·执行‚函数开始符‘{’‛到‚定义变量‛这部分代码:
·入栈相应 CPU寄存器(如果变量分配在 CPU寄存器中,且退出函数后,相应 CPU寄存器需要被使用)
·入栈 LR(这个不是必须的,笔者经过测试,某些情况,不入栈 LR,而之后通过 BX LR跳转)
·减小 SP的值(如果变量分配到栈中,且栈中数据不可被覆盖)
·执行‚定义变量‛后到‚函数退出符‘}’‛前这部分代码:
·堆栈不变,SP值不变。分配局部变量可能会借助 SP,但并不会改变 SP。
·执行执行函数退出符‘}’代码:
·增大 SP的值(如果之前有改变过),使得 SP值恢复到进入函数后的值。
·出栈 LR(这个不是必须的,见前面说明)
·出栈相应 CPU寄存器(如果之前有入栈),使得 SP值恢复到进入函数前的值。
总结就到这,后续章节,我们将研究任务切换原理,就需要‚学习‛这的‚MCU需要做什么‛。
-------------------------------------------------------------------------------------------------------------------------------------------
① 聪明的编译器分配的好点,愚蠢的编译器分配的傻叉点,有 BUG的编译器乱分配,以至于程序出错罢了。
Wav
eshar
e 微雪
电子
-------------------------------------------------------------------------------------------------------------------------------------------
· 全局变量
编译时,编译器将会为全局变量分配固定的 RAM 地址,而不会像局部变量那样,临时分配在 CPU 寄存器或堆栈中。
因为,在程序运行的过程,全局变量不能因为退出了某个函数就‚失效‛,不能被‚消灭‛。
对于 STM32,全局变量分配在哪,可以查看编译后生成的后缀为 map的文件(打开文件后,Ctrl+F,可搜索变量)
第三章 风水轮流转
【承前启后的废话】
本章探讨怎样实现多任务,并重点研究切换任务需要做什么。
关于:在嵌入式系统中,为何要基于操作系统开发程序?它相对于典型的前后台程序有什么优点?
什么是任务?什么是多任务?等等。这类问题,本章不作额外说明。
笔者假定读者已经理解这些基本问题。如不理解,请自行百度、谷歌。
大多数书籍讲述多任务的方法是直接告诉读者系统怎么做,如μC/OS-II的任务由什么组成,怎样调度等等。
笔者不打算这么做,而是打算像做一道证明题那样,一步步告诉读者需要怎么做,为什么要这么做。
为让初学者清楚‚怎样实现多任务‛,笔者决定让自己做回‚初学者‛,一步步研究到底如何实现多任务。
‚怎样实现多任务‛的‚子问题‛是‚怎样实现多任务的切换‛,这也是一个关键问题。
‚怎样实现多任务的切换‛的‚降阶问题①‛是‚怎样实现两个任务的切换‛。
所以,本章先从‚怎样实现两个任务的切换‛这个特殊化、简单化的问题着手研究,之后层层深入。
最后,再分析‚μC/OS-II的任务切换‛及‚怎样实现多任务‛。
-------------------------------------------------------------------------------------------------------------------------------------------
① 你不能理解某个问题,通常意味着,你还无法理解更简单的问题;你不能解决某个问题,一般可以说,你还没能解决更简单的问题。
笔者认为,将问题‚降阶‛、‚特殊化‛应该是一个数理工作者(或工程师)惯用的‚武器‛。
将问题降阶、特殊化的目的是为了更快的解决问题,同时,在研究降阶问题往往能给我们一些启示,这些启示并非直接研究一般问题就能够轻易得到。
-------------------------------------------------------------------------------------------------------------------------------------------
· 怎样实现两个任务的切换
【例化问题】
为方便研究‚怎样实现两个任务的切换‛,我们对它进行‚例化‛,给出具体问题。
如果,在一个系统中①,它运行着 A、B两个任务,执行顺序如下:
(1)运行 A任务,延时 50ms。
(2)运行 B任务,延时 50ms。
(3)运行 A任务,延时 50ms。
(4)运行 B任务,延时 50ms。
问:系统应如何处理,才能成功切换任务?(只须给出与‚切换任务‛相关的信息)
-------------------------------------------------------------------------------------------------------------------------------------------
① 这里所指的系统,只含有一个单核单线程 CPU,单核单线程 CPU在任意时刻只能执行一条指令。
-------------------------------------------------------------------------------------------------------------------------------------------
【理论分析】
Wav
eshar
e 微雪
电子
下面,我们对以上第(1)至(4)步进行分析:
(1):不需作其它处理。(直接运行 A任务①,因为,到此看不出需要处理什么。)
(2):运行 B 任务前,将 CPU的 PC值更新为 B任务地址②,不需作其它处理。(理由同上)
(3):运行 A 任务前,将 CPU寄存器③更新为上一次运行中断时的 CPU寄存器。(为使 A任务‚接着原来‛运行)
然而,‚上一次运行中断时的 CPU寄存器‛并没有被保存,这样:
(2)就需修改为:运行 B 任务前,将 A 任务运行中断时的 CPU寄存器值存到全局变量④中。
(4):运行 B 任务前,将 CPU寄存器更新为上一次运行中断时的 CPU寄存器。(为使 A任务‚接着原来‛运行)
然而,‚上一次运行中断时的 CPU寄存器‛并没有被保存,这样:
(3)就需修改为:运行 A 任务前,将 B 任务运行中断时的 CPU寄存器值存到全局变量中,
并将 CPU寄存器更新为上一次运行中断时的 CPU寄存器。
以上内容实际上是分析过程的‚草稿‛,读者如果能耐心看完,相信,能够明白所以然。
为更清楚的说明问题,将‚草稿‛进一步整理如下:
(1):不需作其它处理。
(2):运行 B 任务前,将 A 任务运行中断时的 CPU寄存器值存到全局变量中。
(3):运行 A 任务前,将 B 任务运行中断时的 CPU寄存器值存到全局变量中,
并将 CPU寄存器更新为 A 任务上一次运行中断时的数据。
(4):运行 B 任务前,将 CPU寄存器更新为上一次运行中断时的 CPU寄存器。
说明:如果第(4)步后,系统任务还没结束,需要再切换任务,那么,第(4)步需改为:
运行 B 任务前,将 A 任务运行中断时的 CPU寄存器值存到全局变量中,
并将 CPU寄存器更新为 B 任务上一次运行中断时的数据。
再次将‚草稿‛进一步整理如下:
(1):不需作其它处理。
(2):运行 B 任务前,将 A 任务运行中断时的 CPU寄存器值存到全局变量中。
(3):运行 A 任务前,将 B 任务运行中断时的 CPU寄存器值存到全局变量中,
并将 CPU寄存器更新为 A 任务上一次运行中断时的数据。
(4):运行 B 任务前,将 A 任务运行中断时的 CPU寄存器值存到全局变量中,
并将 CPU寄存器更新为 B 任务上一次运行中断时的数据。
通过上面的分析,我们知道切换任务时,需要做的是:(本文称此为 Solution 1)
·将当前任务的 CPU寄存器值存到 RAM中。
·将当前任务更新为即将运行任务⑤。
·将 CPU寄存器更新为即将运行任务上一次运行中断时的数据。
-------------------------------------------------------------------------------------------------------------------------------------------
① 即调用 A任务里的首个需要运行的函数。
② 任务的地址即相应的首个运行函数的函数名地址。如,void taskA() {},将 taskA赋值给 PC。
③ CPU寄存器包括了 PC、SP、寄存器组等。
④ 用过μC/OS-II的读者,知道μC/OS-II将 CPU寄存器值存到栈中,即存放在局部变量里。但,在这里,由于没有特殊处理,则只能存到全局变量里。
⑤ 这个在分析过程中并没有提到,但,它是隐含需要的,因为:
·本次切换的第三步要更新 CPU寄存器,就需要知道‚即将运行任务‛是哪个。
·下次切换的第一步、第三步,系统也需要知道到‚当前任务‛。
就本简单问题而言,可以这么说,由于 A、B轮流切换,所以,系统需要记录当前任务,以便知道切换到哪个任务。
-------------------------------------------------------------------------------------------------------------------------------------------
【实践测试】
实际上,将以上理论付诸于实践,并不能实现多任务。
原因是:A 任务被挂起时,SP 指向某个 RAM 地址(如 0x2000)。切换到 B 任务,B 任务依然要使用 SP,然而这
个 SP 值并不是 B 任务在被挂起时的 SP 值,而是 A 任务被挂起时的 SP 值(如,前面提到的 0x2000)。
根据前面章节的研究,我们知道局部变量被分配在与 SP相关的 RAM中,即栈中。
Wav
eshar
e 微雪
电子
若代码并不‚刻意‛改变 SP的值,则整个栈是连续的。也就是,有且只有一个堆栈。
但,如果要实现多任务,则需要有多个堆栈支持,使得各个任务的 RAM数据不会相互影响。
系统可以这么处理:(本文称此为 Solution 2)
·建立任务①时
·为任务分配相应的堆栈,即确定栈顶地址及堆栈大小。
·切换任务时
·将 SP指向即将运行任务的栈顶地址(‚刻意‛改变 SP值,使得各个任务使用相应的堆栈)。
·将当前任务的 SP保存到全局变量中②。
我们综合 Solution 1 及 Solution 2,可以得到:
·建立任务时
·为任务分配相应的堆栈,即确定栈顶地址及堆栈大小。(来自 Solution 2)
·切换任务时
·将当前任务的 CPU寄存器值存到 RAM中。(来自 Solution 1)
·将当前任务的 SP③保存到全局变量中。(来自 Solution 2)
·将当前任务更新为即将运行任务。(来自 Solution 2)
·将 SP指向即将运行任务的栈顶地址。(来自 Solution 2)
·将 CPU寄存器更新为即将运行任务上一次运行中断时的数据。(来自 Solution 1)
将 Solution 3 付诸于实践,前文提到的简单问题得以解决④。
-------------------------------------------------------------------------------------------------------------------------------------------
① OSTaskCreate、OSTaskCreateExt。
② 这个在分析过程中并没有提到,但,它是隐含需要的,因为:下次切换,系统要‚将 SP指向即将运行任务的栈顶地址‛,所以,需要保存 SP。
为何要保存为全局变量,是因为,只有这样才能找回它。否则,如果分配在局部变量中,难以或者说无法找回它。(加标识有可能找回)
③ 细心的读者将发现:这一步的‚SP‛已包含在上一步的‚CPU寄存器‛中,所以,这一步可以省去。确实是这样。
但因为,在μC/OS-II中,分为两步:将 CPU寄存器保存在栈中,将 SP保存在 TCB控制块中。所以,在这,笔者并没有省去这步。以方便后续分析。
④ 仅以上处理是不够的,但,笔者在此只讨论了最基本的、需要处理的东西,以让读者容易明白。
-------------------------------------------------------------------------------------------------------------------------------------------
· μC/OS-II 的任务切换
『利用中断』
前文提到 Solution 3 的‚将当前任务的 CPU寄存器值存到 RAM中‛,可以由程序员自己编写代码实现。
但由于多数 MCU①有软中断指令或者陷阱指令 TRAP指令等,所以,我们可以通过模拟一次中断
②,让 CPU自动保存部
分 CPU寄存器。这样就减少了时间复杂度③,通俗的说,就提高了切换速度。μC/OS-II就是这么做的。
需要说明的是,采用模拟中断的方式,CPU自动压栈,所以,第(1)步‚存到 RAM中‛更具体的说是‚存到栈中‛。
『任务切换实现方法步骤』
将上文提到的信息进行梳理,我们进一步知道,μC/OS-II切换任务的方法步骤是:(本文称此为 Solution 4)
·触发中断(硬件自动‚将当前任务的部分 CPU④寄存器值存到栈中‛)。
·将当前任务的部分④CPU寄存器值存到栈中。
·将当前任务的 SP保存到全局变量中。
·将当前任务更新为即将运行任务。
·将 SP指向即将运行任务的栈顶地址。
·将部分④CPU寄存器更新为即将运行任务上一次运行中断时的数据。
·执行中断返回指令(硬件自动‚将 CPU寄存器更新为即将运行任务上一次运行中断时的数据‛)。
厨神提醒:读者可以配合【第八章】【经典风味】『任务切换实现方法步骤』一起吃,味道更佳。
Wav
eshar
e 微雪
电子
-------------------------------------------------------------------------------------------------------------------------------------------
① 少数 MCU,如 MCS-51没有软中断指令或者陷阱指令 TRAP指令等,采用μC/OS-II,也只能由程序一一保存、恢复各 CPU寄存器。请参阅【第十一章】。
② μC/OS-II的任务切换函数 OS_TASK_SW()实际上是一个宏,它的‚真身‛是 OSCtxSw()。OSCtxSw()见后续章节说明。
当然,通过模拟中断实现任务切换的条件是,就绪任务的栈结构总是看起来跟刚刚发生过中断一样。
μC/OS-II未雨绸缪的在建立任务时建立了以上条件,这样就支持了之后的通过模拟中断实现任务切换。
③ 因为,减少了代码复杂度 —— 不必由程序一一保存、恢复各 CPU寄存器。
④ 产生中断后,硬件本身将自动压栈、出栈 CPU寄存器,但并非一定是所有 CPU寄存器。(但也不排除个别‚小 CPU‛会压栈、出栈所有 CPU寄存器)
关于 CM3的中断,将压栈什么寄存器,请参阅【第十一章】
-------------------------------------------------------------------------------------------------------------------------------------------
· μC/OS-II 怎样实现多任务
前面我们大概讨论了任务切换,还没涉及调度等基础且核心的内容。
本节接着就针对 μC/OS-II 简单说明下这些内容。
『调度思想』
每个操作系统内核有自身的任务调度思想。多数实时内核基于优先级调度。
基于优先级调度的意思是:内核总是让处于就绪态的、优先级最高的任务先运行。
基于优先级调度的内核还分为两种:不可剥夺型、可剥夺型。μC/OS-II 的内核为可剥夺型。
多数实时内核都是可剥夺型:优先级最高的任务一旦就绪,总能得到 CPU 使用权,而原来运行的任务就挂了。
顺便提下,某些 OS支持时间片轮转(如μC/OS-III),某些则不支持(如μC/OS-II)。
『调度器』
查找最高优先级就绪任务,并确定是否让它运行的工作由调度器(Scheduler)完成。
任务级的调度由函数 OSSched()完成。中断级的调度由另一个函数 OSIntExt()完成,这些函数将在以后说明。
『任务的状态』
μC/OS-II的任务有五种状态:
·休眠态:任务驻留在内存中,但并不被操作系统调度。
·就绪态:任务已经准备好,可以运行,但是由于比它优先级更高的任务在运行,所以,它暂时还不能运行。
·运行态:任务掌握了 CPU的使用权,正在运行。
·挂起态:任务在等待某一事件(共享资源、信号)的发生。
·被中断态:任务被中断,暂时得不到运行。
Wav
eshar
e 微雪
电子
『任务的组成』
μC/OS-II的任务由以下三部分组成:
·任务控制块
前文提到:
·将当前任务的 SP保存到全局变量中,μC/OS做的是将 SP保存到‚当前运行任务控制块‛中。
·将当前任务更新为即将运行任务,μC/OS做的是将‚当前任务控制块指针‛指向‚即将运行任务控制块‛。
实际上,任务控制块还有很多用途,更多相关说明请参阅后面章节。
·任务堆栈
这个前面章节已有相关说明,简单来说,就是各个任务有自己的堆栈。
·任务程序代码
这个显而易见,每个任务当然有自己的程序代码。
· 前后台系统与实时系统对比
项目 前后台系统 可剥夺型实时系统
任务响应时间 由具体情况决定(程序员编程时需把握) 寻找最高优先级任务时间 + 任务切换时间
占用 ROM 应用程序代码 应用程序代码 + 内核
占用 RAM 应用程序全局变量(固定) + 局部变量(动态) 应用程序全局变量 + 内核 RAM + 任务栈 + MAX(ISR 栈)
· 临界区
由于实现多任务,需要保护临界区,所以,‚临界区‛也就暂寄放在这(不排除以后调到其它章)。
【临界资源与临界区】
临界资源是一次仅允许一个进程使用的共享资源。全局变量是一种临界资源。
不论是硬件临界资源,还是软件临界资源,多个进程必须互斥地对它进行访问。
每个进程中访问临界资源的那段代码称为临界区(Critical Section)。
每次只准许一个进程进入临界区,进入后不允许其他进程进入。
不论是硬件临界资源,还是软件临界资源,多个进程必须互斥地对它进行访问。
【保护临界区的实现】
Wav
eshar
e 微雪
电子
关中断可使得系统能够避免同时有其它任务或中断服务进入临界区。
在μC/OS-II中,定义了两个宏,以避免不同 C编译器厂商选择不同的方法来处理开关中断。
这两个宏是:
·OS_ENTER_CRITICAL():进入临界区。
·OS_EXIT_CRITICAL():离开临界区。
它们总是成双成对,不离不弃,将临界区‚夹住‛。
【进出临界区的实现方式】
在μC/OS-II中,有三种方式可以实现进出临界区,选择哪种方法由 OS_CRITICAL_METHOD决定。
『OS_CRITICAL_METHOD = 1』
·OS_ENTER_CRITICAL():用 CPU指令关中断。
·OS_EXIT_CRITICAL():用 CPU指令开中断。
这种方式虽然最简单,但有如下缺点:离开临界区后,中断总是被打开。而如果,进入临界区前,中断是被关着的,
显然不应被打开。在 Jean.J.Labrosse著的μC/OS-II(第 2版)中,提到:‚对一些 CPU或编译器,使用这种方式
却是唯一选择①。‛
『OS_CRITICAL_METHOD = 2』
·OS_ENTER_CRITICAL():在堆栈中保存中断的开关状态,然后再关中断,示例代码如下:
#define OS_ENTER_CRITICAL() asm("PUSH PSW") asm("DI")
·OS_EXIT_CRITICAL():从堆栈中弹出中断的开关状态,示例代码如下:
#define OS_EXIT_CRITICAL() asm("POP PSW")
这种方式能够得到原来中断的开关状态(中断的开关状态记录在 PSW 中),但却很可能由于编译器对插入的行汇编
支持不好,导致以上方法不可行,或者说导致严重错误。因为,PUSH的出现导致了堆栈指针发生了改变,这样,在
临界区内存取若存放在栈中的局部变量,就不再是局部变量原来的地址了。
『OS_CRITICAL_METHOD = 3』
·OS_ENTER_CRITICAL():将 CPU的状态寄存器值保存到局部变量 cpu_sr,在笔者的工程中,代码如下:
#define OS_ENTER_CRITICAL() {cpu_sr = OS_CPU_SR_Save();}
OS_CPU_SR_Save
MRS R0②, PRIMASK ;保存全局中断标志
CPSID I ;关中断
BX LR
·OS_EXIT_CRITICAL():将 cpu_sr装回 CPU的状态寄存器,在笔者的工程中,代码如下:
#define OS_EXIT_CRITICAL() {OS_CPU_SR_Restore(cpu_sr);}
OS_CPU_SR_Restore
MSR PRIMASK, R0 ;恢复全局中断标志
BX LR
这种方式能够得到原来中断的开关状态,条件是:CPU 拥有状态寄存器,且状态寄存器的某一位可以被用来标识某
处理器设置中断的办法。大部分 ARM都采用本方式保护临界区。
-------------------------------------------------------------------------------------------------------------------------------------------
① 也就是说,无法使用方式 2及方式 3,也就是无法读写 PSW(可能根本就没有 PSW)。笔者认为,如果使用方式 1,要解决前文提到的缺点,则只能是:
开关中断时(进出临界区除外),将中断记录到某个全局变量中,相应的,恢复中断,则是读取相应的全局变量。
② 由于函数返回值存在 R0中,所以将 PRIMASK放到 R0,之后,即可通过代码‚cpu_sr = OS_CPU_SR_Save();‛将 cpu状态寄存器存到 cpu_sr中。
-------------------------------------------------------------------------------------------------------------------------------------------
【保护临界区的注意事项】
Wav
eshar
e 微雪
电子
使用 OS_ENTER_CRITICAL()、OS_EXIT_CRITICAL()的方式来保护临界区要注意:如果在调用一些如 OSTimeDel()之
类的功能函数之前关中断,应用程序将会崩溃。这个问题是这样引起的:任务被挂起一段时间,直到挂起时间到,
但由于中断关掉了,时钟节拍中断一直得不到服务。显然,所有的挂起(PEND)类调用都存在这类问题。
读者可以记得一条普适规则:调用功能函数时,中断应当总是开着的。
第四章 作战指挥部
【前仆后继的废话】
作战指挥部建立两个应用任务,一个任务负责点灯,一个任务负责灭灯,你来我往,让人觉得好闪。
当然,当然阅读完本章,读者还是不知道怎样做到想闪就闪,想不闪就不闪,以及怎样才能闪到眼都花掉。
具体的处理还要查看后面章节才能知道,基于μC/OS-II之‚点灯术‛怎样实现。
· main.c
#include <includes.h>
extern OSTaskCreate_(void);
extern DeviceInit(void);
INT32S main (void) ①
{
DeviceInit(); //初始化器件,包括 MCU、外设的相关初始化,由于与 OS无关,本书省去它的说明
OSInit(); //初始化 OS,这个函数将在【第六章】集中说明
OSTaskCreate _(); //建立 OS任务,这个函数是 OSTaskCreate(…) 或 OSTaskCreateExt(…)的替身②
//‚原函数‛OSTaskCreate()这个函数将在【第七章】集中说明
OSStart(); //启动 OS,这个函数将在【第九章】集中说明
return (0);
}
-------------------------------------------------------------------------------------------------------------------------------------------
① ‚够力‛提示:μC/OS-II 的 main()函数内,经典最小代码为:
OSInit();
OSTaskCreate(…); 或 OSTaskCreateExt(…)
OSStart();
② 采用替身的目的是,为了让 main.c文件‚永恒不变‛。
因为,如果直接调用 OSTaskCreate、OSTaskCreateExt,还需要相应的多个函数参数,而这些参数是可能变化的。
如果将 OSTaskCreate_()函数代码放到 main.c外,再到那些文件里调用 OSTaskCreate、OSTaskCreateExt,
那样,main.c就可以不作任何修改,一直拿来用则可以。可以看到,DeviceInit()函数代码也均在 main.c外。
这样,main.c的代码就非常简洁,且移到其它工程,不需作任何修改,可直接使用。
-------------------------------------------------------------------------------------------------------------------------------------------
· AppCfg.h
#define APP_TASK_STK_SIZE 64u
#define TASK_A_STK_SIZE 64u
#define TASK_B_STK_SIZE 64u
#define APP_TASK_PRIO 4
#define TASK_A_PRIO 5
#define TASK_B_PRIO 6
Wav
eshar
e 微雪
电子
static OS_STK AppTaskStk[APP_TASK_STK_SIZE];
static OS_STK TaskAStk[TASK_A_STK_SIZE];
static OS_STK TaskBStk[TASK_B_STK_SIZE];
· AppTask.C
void OSTaskCreate_ (void)
{
OSTaskCreate( AppTask, 0, &AppTaskStk[APP_TASK_STK_SIZE - 1], APP_TASK_PRIO );
}
static void AppTask (void *p_arg)
{
AppTaskCreate();① // 通过 AppTask建立实现需要运行的任务
OSTaskDel(OS_PRIO_SELF); ② // 删除自身
}
static void AppTaskCreate (void)
{
OSTaskCreate(taskA, (void *)0, &Task_AStk[TASK_A_STK_SIZE-1], TASK_A_PRIO);
OSTaskCreate(taskB, (void *)0, &Task_BStk[TASK_B_STK_SIZE-1], TASK_B_PRIO);
}
static void TaskA(void *parg)
{
while(1) ③
{
LED_On(1);
LED_On(2);
LED_On(3);
LED_On(4);
OSTimeDlyHMSM(0, 0, 0, 100); ④
}
}
static void TaskB(void *parg)
{
while(1)
{
LED_Off(1);
LED_Off(2);
LED_Off(3);
LED_Off(4);
OSTimeDlyHMSM(0, 0, 1, 0);
}
}
-------------------------------------------------------------------------------------------------------------------------------------------
① AppTaskCreate()函数见后面章节说明。
Wav
eshar
e 微雪
电子
② OSTaskDel()函数见后面章节说明。
这种方法最为常见,因为本任务仅仅需要运行一次,所以,最合理的方式就是将它删除。
当然,它也可以改为以下两种方法,从而进行任务切换:
·OSTimeDlyHMSM(0, 0, 2, 0);
·y = OSTCBCur->OSTCBY;
OSRdyTbl[y] &= (OS_PRIO)~OSTCBCur->OSTCBBitX;
if (OSRdyTbl[y] == 0u)
OSRdyGrp &= (OS_PRIO)~OSTCBCur->OSTCBBitY;
OS_Sched();
③ 应用任务必须是一个超循环体,这是显然的。
④ OSTimeDlyHMSM()函数见后面章节说明。
-------------------------------------------------------------------------------------------------------------------------------------------
第五章 核武器一
【传宗接代的废话】
μC/OS-II内核有很多重要全局变量,OS的实现离不开它们。鉴于它们在 OS核中,我们就称它们为‚核武器‛。
本章只介绍本册涉及的那部分核武器,并将这部分核武器称为‚核武器一‛,其余将在中册‚核武器二‛中介绍。
(为简单起见,本章所介绍的变量部分基于预编译后)
· 非结构体全局变量及参数
本节介绍的是‚轻型核武器‛ —— 非结构体类全局变量。
#define OS_TICKS_PER_SEC 1000u //定义一秒钟为多少 TICKS
#define OS_MAX_TASKS 20u //定义用户任务最大数
#define OS_N_SYS_TASKS 2u //定义系统任务总数
#define OS_LOWEST_PRIO 63u //定义最低优先级
#define OS_TASK_IDLE_STK_SIZE 32u //定义统计任务堆栈大小
#define OS_TASK_STAT_PRIO (OS_LOWEST_PRIO - 1u) //定义统计任务优先级为次低
#define OS_TASK_IDLE_PRIO (OS_LOWEST_PRIO) //定义空闲任务优先级为最低
#define OS_RDY_TBL_SIZE ((OS_LOWEST_PRIO) / 8u + 1u) //定义就绪表(数组)大小
#define OS_TCB_RESERVED ((OS_TCB *)1) //定义保留的 TCB标志为 1
#define OS_EXT extern //定义 OS_EXT表示在外部使用
typedef INT8U OS_PRIO; //定义 OS_PRIO类型为 INT8U
typedef INT32U OS_STK; //定义 OS_STK类型为 INT32U
OS_EXT INT32U OSCtxSwCtr; //定义任务切换计数器。μC/OS-II用‚Ctr‛后缀表示计数器
OS_EXT INT8U OSIntNesting; //定义中断嵌套数。可作为调度器可否进行调度的标志
//以保证调度器不会在中断服务程序中进行任务调度
OS_EXT INT8U OSLockNesting; //定义锁定嵌套数。可作为调度器可否进行调度的标志
OS_EXT INT8U OSPrioCur; //定义当前任务的优先级
OS_EXT INT8U OSPrioHighRdy; //定义最高就绪任务的优先级
OS_EXT OS_PRIO OSRdyGrp; //定义就绪任务组
//用途:每一位对应的表示每一组 OSRdyTbl是否有进入就绪态的任务
OS_EXT OS_PRIO OSRdyTbl[OS_RDY_TBL_SIZE]; //定义任务就绪表
//若系统支持 64个任务,则把 64个任务分为 8组,每组 8个任务
//每位数据的 0、1状态代表 64个任务是否处于就绪态
OS_EXT BOOLEAN OSRunning; //定义 OS是否运行标志
OS_EXT INT8U OSTaskCtr①; //定义任务计数器
Wav
eshar
e 微雪
电子
OS_EXT volatile INT32U OSIdleCtr①; //定义空闲任务计数器
OS_EXT OS_STK OSTaskIdleStk①[OS_TASK_IDLE_STK_SIZE]; //定义空闲任务堆栈
OS_EXT OS_STK OSTaskStatStk[OS_TASK_STAT_STK_SIZE]; //定义统计任务堆栈
-------------------------------------------------------------------------------------------------------------------------------------------
① 从以上代码(‚原版‛μC/OS代码也同)可以看出,μC/OS开山鼻祖及‚引渡者‛对变量名的斟酌并没有下太多功夫。
笔者认为μC/OS变量的命名有时不能尽更大程度,让人一目了然,甚至一些变量命名充满矛盾,缺乏规范。
如,本节的 OSTaskCtr、OSIdleCtr、OSTaskIdleStk,如果统一,则可以命名为:
OSTaskCtr、OSTaskIdleCtr、OSTaskIdleStk。(对于 Task,有时带上,有时省去,对用户来说并不友好。)
-------------------------------------------------------------------------------------------------------------------------------------------
· 结构体全局变量 OS_TCB
本节介绍的是‚重型核武器‛ —— 结构体类全局变量。
typedef struct os_tcb
{
OS_STK *OSTCBStkPtr; //任务堆栈指针(指向任务堆栈栈顶),翻译为英文是 TaskSP①
struct os_tcb *OSTCBNext; //指向下一个任务控制块(在 TCBList中)②
struct os_tcb *OSTCBPrev; //指向上一个任务控制块(在 TCBList中)②
INT32U OSTCBDly; //任务等待的节拍数
INT8U OSTCBStat; //任务控制块的状态标志
INT8U OSTCBStatPend; //任务控制块的等待状态标志
INT8U OSTCBPrio; //任务优先级
INT8U OSTCBX; //OSTCBX = priority & 0x07;
INT8U OSTCBY; //OSTCBY = priority >> 3;
OS_PRIO OSTCBBitX; //OSTCBBitX = OSMapTbl[priority & 0x07];
OS_PRIO OSTCBBitY; //OSTCBBitY = OSMapTbl[priority >> 3];
//提前做了运算并存储,以上四个变量存在的目的是节约任务调度时间
//相关说明参见【第十二章】
} OS_TCB;
OS_EXT OS_TCB *OSTCBCur; //定义指向运行任务控制块的指针
OS_EXT OS_TCB *OSTCBFreeList; //定义指向空闲任务控制块列表的指针
OS_EXT OS_TCB *OSTCBHighRdy; //定义指向最高就绪任务控制块的指针
OS_EXT OS_TCB *OSTCBList; //定义指向任务控制块列表的指针
OS_EXT OS_TCB *OSTCBPrioTbl[OS_LOWEST_PRIO + 1u]; //定义指向优先级任务控制块表③
OS_EXT OS_TCB OSTCBTbl[OS_MAX_TASKS + OS_N_SYS_TASKS]; //定义任务控制块表
//这货只在 OS_InitTCBList()中用到
-------------------------------------------------------------------------------------------------------------------------------------------
① 用‚TaskSP‛这个命名显然更为合理,那为何‚TaskSP‛会变成‚OSTCBStkPtr‛?笔者认为估计是经过了如下演变:
·从‚TaskSP‛到‚OSTCBTaskSP‛(因为,μC/OS开山鼻祖及‚引渡者‛将 OS_TCB内所有变量都加了前缀‚OSTCB‛,这个很合理)
·从‚OSTCBTaskSP‛到‚OSTCBSP‛(因为,μC/OS开山鼻祖及‚引渡者‛编写的代码有时会省去‚Task‛)
·从‚OSTCBSP‛到‚OSTCBStkPtr‛(可能早期,大家喜欢将‚StackPointer‛简写为‚StkPtr‛吧,不过,笔者认为,时至今日,‚SP‛比‚StkPtr‛优越)
当然,为了整个 OS的代码风格统一,‚TaskSP‛前面加‚OSTCB‛是必要的,那样,‚OSTCBStkPtr‛更合理的命名是‚OSTCBTaskSP‛
② 这个数组里的元素,以任务优先级为 0开始,依次存放各个任务控制块的指针,这个数值存在的目的是:在需要时,快速得到相应 TCB。
‚快速‛是因为,只需要通过以下步骤:
·通过函数:OS_SchedNew(),获取就绪任务最高优先级,并将它存给 OSPrioHighRdy。
·通过代码:OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy],将 OSTCBHighRdy指向最高就绪任务的 TCB。
Wav
eshar
e 微雪
电子
③ OSTCBList是一个双向链表,双向链表相比单链表的优点是能够通过 Prev访问前一个节点,这也是μC/OS-II采用双链表的原因。
虽然增加任务时,只会将任务 TCB从 OSTCBList一端插入,那样,将 OSTCBList构建为单链表就可以了。
但,删除任务时,删除的 TCB节点并不一定在 OSTCBList的一端,那样就需要 Prev,也就是需用将 OSTCBList构建为双链表。
-------------------------------------------------------------------------------------------------------------------------------------------
第六章 OS 初始化战队一
【代代相传的废话】
使用μC/OS-II,最先须调用的函数是 OSInit()。
OSInit()引用了很多子函数,本章只介绍(可能一笔带过)本册涉及的那部分函数,其余函数将在中册里剖析。
· OSInit()
void OSInit(void)
{
OS_InitMisc();
OS_InitRdyList();
OS_InitTCBList();
OS_InitTaskIdle();
#if OS_TASK_STAT_EN > 0u
OS_InitTaskStat();
#endif
#if OS_DEBUG_EN > 0u
OSDebugInit();
#endif
}
函数功能:初始化μC/OS-II所有变量及数据结构,此外,通过 OS_InitTaskIdle()建立空闲任务 OS_TaskIdle()。
函数所在文件:os_core.c。
可以说 OSInit()没有直接的代码处理,而是由一堆子函数组成。
需要了解的子函数,除了以下外,其余的,均在本章其它节进行说明。
·OS_InitTaskIdle()函数功能:建立空闲任务,将在中册分析这个函数。
·OS_InitTaskStat()函数功能:建立统计任务,将在中册分析这个函数。
·OS_InitMisc()函数功能:初始化常量、变量,由于该函数比较简单、显然,所以,说明从略。
·OSDebugInit()函数功能:用于保证应用程序未使用的调试变量没有被优化掉。
若编译器可以不使用本函数(或通过设置优化参数后不使用本函数)则可以删除该函数。
-------------------------------------------------------------------------------------------------------------------------------------------
闲话 从 OS_Init()里的代码(‚原版‛μC/OS代码也同)可以看出,μC/OS开山鼻祖并没有罹患‚整齐强迫症‛。
否则,里面的子函数不会有以下几种不同的风格:OSInit***(); OS***Init; OS_Init***(); OS_***Init();(最后面的函数本文没有给出)
天才的开山鼻祖与‚引渡者‛如果能不幸患上‚整齐强迫症‛,相信,代码会更有观赏性。
-------------------------------------------------------------------------------------------------------------------------------------------
Wav
eshar
e 微雪
电子
· OS_InitRdyList()
函数功能:初始化就绪列表①,就绪列表是一个数组。
函数所在文件:os_core.c。
函数使用的全局变量:INT8U OSRdyGrp; INT8U OSRdyTbl[OS_RDY_TBL_SIZE];
INT8U OSPrioCur; INT8U OSPrioHighRdy; OS_TCB *OSTCBCur; OS_TCB *OSTCBHighRdy;
行 1369:如果 OSRdyTbl[i]均为 0,则 OSRdyGrp = 0。后面代码将设置 OSRdyTbl[i]为 0,这里‚先知‛了下②。
行 1370-1373:根据 OS_RDY_TBL_SIZE的值,将 OSRdyTbl[i]设置为 0。
行 1375:将正在运行任务的优先级设置为 0。
行 1376:将就绪任务中的最高优先级设置为 0。
行 1378:将指向就绪最高优先级任务的 OS_TCB指针清 0。
(OS_TCB *)0虽然是指向了一个起始地址为 0的任务块,但在μC/OS-II中,实际上是标志为未使用。
将 OSPrioTable[i]置为(OS_TCB *)1,则表明该任务优先级已被使用。
因为,OS通过检查 OSPrioTable[i]是否等于(OS_TCB *)0来判断对应的优先级是否已被使用。
行 1379:将指向正在运行任务的 OS_TCB指针清 0。
-------------------------------------------------------------------------------------------------------------------------------------------
① OS_InitRdyList()的命名不太恰当,这个说明也不太正确。因为,该函数也初始化 OSPrioCur、OSPrioHighRdy、OSTCBHighRdy、OSTCBCur等变量。
② 将先知‚行 1369‛放到‚行 1370-1373‛成为马后炮,则,代码的可读性会更好。
-------------------------------------------------------------------------------------------------------------------------------------------
Wav
eshar
e 微雪
电子
· OS_InitTCBList()
函数功能:初始化 OSTCBTbl 、OSTCBPrioTbl 、OSTCBList、OSTCBFreeList。
函数所在文件:os_core.c。
函数使用的全局变量:OS_TCB OSTCBTbl[OS_MAX_TASKS + OS_N_SYS_TASKS];
OS_TCB *OSTCBPrioTbl[OS_LOWEST_PRIO + 1u];
行 1525-1526:只定义 1个变量 OS_TCB *ptcb也可以完成初始化,方法参考中册的 OS_MemInit()函数。
行 1529:清除 OSTCBTbl。
行 1530:清除 OSTCBPrioTbl。
行 1534-1535:可以改用其它写法①。
行 1531-1540:执行结果:[OSTCBTbl[0]->OSTCBNext] → [OSTCBTbl[1]],
[OSTCBTbl[1]->OSTCBNext] → [OSTCBTbl[2]] …
行 1541-1542:执行结果:[OSTCBTbl[0]->OSTCBNext] → [OSTCBTbl[1]],
[OSTCBTbl[1]->OSTCBNext] → [OSTCBTbl[2]] … →0
行 1546-1547:执行结果:OSTCBList → 0,OSTCBFreeList② → OSTCBTbl[0]
-------------------------------------------------------------------------------------------------------------------------------------------
① 因为,&OSTCBTbl[0]等价于 OSTCBTbl,&OSTCBPrioTbl[0]等价于 OSTCBPrioTbl,所以:
·‚OS_MemClr((INT8U *)&OSTCBTbl[0], sizeof(OSTCBTbl));‛可改写为‚OS_MemClr((INT8U *)OSTCBTbl, sizeof(OSTCBTbl));‛
·‚OS_MemClr((INT8U *)&OSTCBPrioTbl[0], sizeof(OSTCBPrioTbl));‛ 可改写为‚OS_MemClr((INT8U *)OSTCBPrioTbl, sizeof(OSTCBPrioTbl));‛
② 初始化完后 OSTCBFreeList如下:
-------------------------------------------------------------------------------------------------------------------------------------------
Wav
eshar
e 微雪
电子
第七章 OS 任务管理战队一
【一如既往的废话】
本章只介绍(可能一笔带过)本册涉及的那部分代码,完整代码将在中册里剖析。
· OSTaskCreate()
INT8U OSTaskCreate (void (*task)(void *p_arg), void *p_arg, OS_STK *ptos, INT8U prio) ①
{
OS_STK *psp;
INT8U err;
#if OS_CRITICAL_METHOD == 3u
OS_CPU_SR cpu_sr = 0u; ②
#endif
#if OS_ARG_CHK_EN > 0u
if (prio > OS_LOWEST_PRIO) //如果优先级低于最低任务
return (OS_ERR_PRIO_INVALID); //返回优先级错误
#endif
OS_ENTER_CRITICAL();③
if (OSIntNesting > 0u) //如果中断嵌套层数大于 0
{
OS_EXIT_CRITICAL();③
return (OS_ERR_TASK_CREATE_ISR); //返回错误,以免在中断里建立任务
}
if (OSTCBPrioTbl[prio] == (OS_TCB *)0) //如果 OSTCBPrioTbl表不存在该 TCB
{
OSTCBPrioTbl[prio] = OS_TCB_RESERVED; //将 OSTCBPrioTbl相应元素记为使用中(直到任务建立完)
OS_EXIT_CRITICAL();
psp = OSTaskStkInit(task, p_arg, ptos, 0u);④
err = OS_TCBInit(prio, psp, (OS_STK *)0, 0u, 0u, (void *)0, 0u);⑤
if (err == OS_ERR_NONE)
if (OSRunning == OS_TRUE) //如果 OS已经启动
OS_Sched();⑥
}
else
{
OS_ENTER_CRITICAL();
OSTCBPrioTbl[prio] = (OS_TCB *)0; //将 OSTCBPrioTbl相应元素标记为未使用
OS_EXIT_CRITICAL();
}
return (err);
}
OS_EXIT_CRITICAL();
return (OS_ERR_PRIO_EXIST);
}
Wav
eshar
e 微雪
电子
函数功能:建立任务。
函数所在文件:os_task.c。
-------------------------------------------------------------------------------------------------------------------------------------------
① void (*task)(void *p_arg):指向任务名称的指针。void *p_arg:传递的参数。OS_STK *ptos:指向栈顶的指针。INT8U prio:任务优先级。
这里顺便提下,OSTaskCreate调用的写法可以有多钟,就前文 main()函数里的 OSTaskCreate而言,它可以采用以下几种写法的任何一种。
·最严格写法 —— 带数据类型转换,参数完整。(经另外测试,void(*)与 void(*task)对应,(void *)与(void *p_arg)对应)
OSTaskCreate( (void (*)(void *))AppTask, (void *)0, (OS_STK *)&AppTaskStk[APP_TASK_STK_SIZE-1], (INT8U)APP_TASK_PRIO );
·不严格写法 —— 带数据类型转换,参数不完整,当采用‚不严格写法‛的时候,编译器会提示 void(*)(void *)。
OSTaskCreate( (void *)AppTask, (void *)0, (OS_STK *)&AppTaskStk[APP_TASK_STK_SIZE - 1], (INT8U)APP_TASK_PRIO );
·最简略写法 —— 不带数据类型转换,比较常用的写法。
OSTaskCreate( AppTask, 0, &AppTaskStk[APP_TASK_STK_SIZE - 1], APP_TASK_PRIO );
·任务名采用函数指针的写法,如果前面有先定义指针*pAppTask,可以通过。但,这种写法并不会在实际中投入使用,它只是笔者用来测试而已。
OSTaskCreate( *pAppTask, (void *)0, (OS_STK *)&AppTaskStk[APP_TASK_STK_SIZE - 1], (INT8U)APP_TASK_PRIO );
② 如果设置为方式 3,则定义 cpu_sr,因为之后,要使用 cpu_sr。
③ OS_ENTER_CRITICAL()、OS_EXIT_CRITICAL()函数见后面章节说明。
④ OSTaskStkInit()函数见后面章节说明。psp:process stack pointer,线程堆栈指针,它指向任务堆栈初始化后的栈顶地址。
⑤ OS_TCBInit()函数见后面章节说明。
⑥ OS_Sched()函数见后面章节说明。
-------------------------------------------------------------------------------------------------------------------------------------------
· OSTaskStkInit()
函数功能:初始化堆栈。
函数所在文件:os_cpu_c.c。
相关说明:
·初始化任务的栈的结构,要求任务栈看起来就像是刚发生了一个中断①。
·创建任务的时候,R12至 R1的值不需要初始化,所以,可以随意赋值。R12=0x12121212L,并非只能这样赋值。
·初始化结束后,返回栈顶地址给上层程序。
·使用该堆栈的任务,定义的变量并非从这个返回的栈顶地址开始分配②。
Wav
eshar
e 微雪
电子
-------------------------------------------------------------------------------------------------------------------------------------------
① 为何要这么做?请参阅【第三章】。入栈顺序为何这样?请参阅【第十二章】
② 如果该任务的第一个局部变量分配在栈中,则该地址为‚返回的栈顶地址 + 0x3C - n个 0x04‛。
因为,切换到该任务后,xPSR–> PC–> R14(LR)–> R12–> R3-R0 -> R11-R4,共 15个寄存器都会被退栈。
笔者使用的系统,1个堆栈单位长度为 4个字节,于是,退栈后,地址将加上 0x3C(15x4=60,即 0x3C)。
然而,正常情况,编译器遇到 C语言定义变量时,SP会自动(不是绝对,参阅【第二章】)偏移 n个堆栈单位长度(参阅【第一章】),再分配地址给变量。
若 n=1,则第一个变量的地址 = 返回的栈顶地址 + 0x3C - 0x04 = 0x38。本说明,笔者已通过实验检验。
-------------------------------------------------------------------------------------------------------------------------------------------
· OS_TCBInit()
INT8U OS_TCBInit (INT8U prio, OS_STK *ptos, OS_STK *pbos, INT16U id,
INT32U stk_size, void *pext,INT16U opt)
{
OS_TCB *ptcb;
#if OS_CRITICAL_METHOD == 3u
OS_CPU_SR cpu_sr = 0u;
#endif
#if OS_TASK_REG_TBL_SIZE > 0u
INT8U i;
#endif
OS_ENTER_CRITICAL();
ptcb = OSTCBFreeList; //将 OSTCBFreeList赋给 ptcb①
//即第一次建立任务,ptcb将取到 OSTCBTbl[0] ①
if (ptcb != (OS_TCB *)0)
{
OSTCBFreeList = ptcb->OSTCBNext; //将 OSTCBFreeList指向下个 OSTCB①
//即第一次建立任务,OSTCBFreeList将指向 OSTCBTbl[1] ①
OS_EXIT_CRITICAL();
ptcb->OSTCBStkPtr = ptos; //将 OSTCBStkPtr指向 ptos(ptos为形参,由 psp的值传入)
ptcb->OSTCBPrio = prio; //将 OSTCBPrio赋值为 prio(ptos为形参)
ptcb->OSTCBStat = OS_STAT_RDY; //将 OSTCBStat设置为就绪
ptcb->OSTCBStatPend = OS_STAT_PEND_OK; //将 OSTCBStatPend设置为未等待,清除等待状态
ptcb->OSTCBDly = 0u; //将 OSTCBStatPend设置为不延时
#if OS_LOWEST_PRIO <= 63u
ptcb->OSTCBY = (INT8U)(prio >> 3u); ②
ptcb->OSTCBX = (INT8U)(prio & 0x07u); ②
#else
ptcb->OSTCBY = (INT8U)((INT8U)(prio >> 4u) & 0xFFu); ②
ptcb->OSTCBX = (INT8U) (prio & 0x0Fu); ②
#endif
ptcb->OSTCBBitY = (OS_PRIO)(1uL << ptcb->OSTCBY); ②
ptcb->OSTCBBitX = (OS_PRIO)(1uL << ptcb->OSTCBX); ②
Wav
eshar
e 微雪
电子
OS_ENTER_CRITICAL();
OSTCBPrioTbl[prio] = ptcb; //将 OSTCBPrioTbl中相应元素指向 ptcb
ptcb->OSTCBNext = OSTCBList; ③
ptcb->OSTCBPrev = (OS_TCB *)0; ③
if (OSTCBList != (OS_TCB *)0) ③
OSTCBList->OSTCBPrev = ptcb; ③
OSTCBList = ptcb; ③
OSRdyGrp |= ptcb->OSTCBBitY; ④
OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX; ④
OSTaskCtr++; //任务切换计数器加 1
OS_EXIT_CRITICAL();
return (OS_ERR_NONE);
}
OS_EXIT_CRITICAL();
return (OS_ERR_TASK_NO_MORE_TCB);
}
函数功能:初始化任务控制块。
函数所在文件:os_core.c。
-------------------------------------------------------------------------------------------------------------------------------------------
① 也就是每次建立任务,空闲 TCB将减少 1个,OSTCBFreeList将指向 OSTCBNext(这样的操作从‚删除‛OSTCBTbl[0]开始)。
建立第一个任务前,OSTCBFreeList如下:
建立第一个任务后,OSTCBFreeList如下:
建立第二个任务后,OSTCBFreeList如下:
② 在之前 OSTaskCreate中,代码 OSTCBPrioTbl[prio] = OS_TCB_RESERVED; 临时将任务标价为使用中。
到此,任务真正建立完,代码 OSTCBPrioTbl[prio] = ptcb; 将任务在 TCB优先级表中相应的地方设置为实际 TCB。
③ 建立第一个任务后,OSTCBList、OSTCBFreeList如下:
建立第二个任务后,OSTCBList、OSTCBFreeList如下:
Wav
eshar
e 微雪
电子
④ OSTCBY、OSTCBX、OSTCBBitY、OSTCBBitX存在的目的是什么,请参阅【第十二章】
-------------------------------------------------------------------------------------------------------------------------------------------
· OSTaskDel()
INT8U OSTaskDel (INT8U prio)
{
OS_TCB *ptcb;
#if OS_CRITICAL_METHOD == 3u
OS_CPU_SR cpu_sr = 0u;
#endif
if (OSIntNesting > 0u) //如果中断嵌套层数大于 0
return (OS_ERR_TASK_DEL_ISR); //返回错误 OS_ERR_TASK_DEL_ISR
if (prio == OS_TASK_IDLE_PRIO) //如果任务为 TASK_IDLE
return (OS_ERR_TASK_DEL_IDLE); //返回错误 OS_ERR_TASK_DEL_IDLE
#if OS_ARG_CHK_EN > 0u
if (prio >= OS_LOWEST_PRIO) //如果优先级在允许范围内
if (prio != OS_PRIO_SELF) //如果删除的不是自身
return (OS_ERR_PRIO_INVALID); //返回错误 OS_ERR_PRIO_INVALID
#endif
OS_ENTER_CRITICAL();
if (prio == OS_PRIO_SELF) ① //如果删除的是自身
prio = OSTCBCur->OSTCBPrio; //将当前任务优先级存到 PRIO
ptcb = OSTCBPrioTbl[prio]; //将 ptcb指向相应 TCB
if (ptcb == (OS_TCB *)0) //如果 ptcb不存在
{
OS_EXIT_CRITICAL();
return (OS_ERR_TASK_NOT_EXIST); //返回错误 OS_ERR_TASK_NOT_EXIST
}
if (ptcb == OS_TCB_RESERVED) //如果 ptcb正在使用
{
OS_EXIT_CRITICAL();
return (OS_ERR_TASK_DEL); //返回 OS_ERR_TASK_DEL
}
OSRdyTbl[ptcb->OSTCBY] &= (OS_PRIO)~ptcb->OSTCBBitX; ②
if (OSRdyTbl[ptcb->OSTCBY] == 0u) ②
OSRdyGrp &= (OS_PRIO)~ptcb->OSTCBBitY; ②
ptcb->OSTCBDly = 0u;
Wav
eshar
e 微雪
电子
ptcb->OSTCBStat = OS_STAT_RDY;
if (OSLockNesting < 255u) //避免 OSLockNesting变回 0
OSLockNesting++;
OS_EXIT_CRITICAL();
OS_Dummy();③
OS_ENTER_CRITICAL();
if (OSLockNesting > 0u) //如果 OSLockNesting>0
OSLockNesting--; //解一层锁
OSTaskCtr--;
OSTCBPrioTbl[prio] = (OS_TCB *)0; //将 OSTCBPrioTbl相应元素标记为未使用
if (ptcb->OSTCBPrev == (OS_TCB *)0) ④
{
ptcb->OSTCBNext->OSTCBPrev = (OS_TCB *)0; ④
OSTCBList = ptcb->OSTCBNext; ④
}
else ④
{
ptcb->OSTCBPrev->OSTCBNext = ptcb->OSTCBNext; ④ ⑤-1
ptcb->OSTCBNext->OSTCBPrev = ptcb->OSTCBPrev; ④ ⑤-2
}
ptcb->OSTCBNext = OSTCBFreeList; ④ ⑤-3
OSTCBFreeList = ptcb; ④
OS_EXIT_CRITICAL();
if (OSRunning == OS_TRUE)
OS_Sched();
return (OS_ERR_NONE);
}
-------------------------------------------------------------------------------------------------------------------------------------------
① μC/OS-II惯用‚if (prio == OS_PRIO_SELF) prio = OSTCBCur->OSTCBPrio; ‛来获取当前任务优先级。
这样做的用意是,上层函数可以通过‚OSTaskDel(OS_PRIO_SELF)‛删除任务自身,而不需知道、传递自身的任务优先级到删除函数。
也就是说,如果没有这么处理,那么上层函数的样子大概是 OSTaskDel(PRIO),PRIO是当前任务优先级的值。
② 请参阅【第十二章】
③ OS_Dummy()没有什么操作。目的是保证处理器在中断开着的情况下至少执行一条指令。
许多处理器开中断后,CPU会强制执行下一条指令,然后才开中断。也就是说,直到执行完下一条指令后,才会真正开中断。
④ 假设操作前,OSTCBList、OSTCBFreeList如下,ptcb指向 OSTCBTbl[1]:
那么,删除 ptcb后,OSTCBList、OSTCBFreeList如下:
Wav
eshar
e 微雪
电子
⑤ 一个例子,初始状态及 ‚⑤-1‛、‚⑤-2‛、‚⑤-3‛相应的操作结果如下:
-------------------------------------------------------------------------------------------------------------------------------------------
第八章 OS 内核调度战队一
【继往开来的废话】
本章分析任务调度,重点分析任务切换。
· OS_Sched()
void OS_Sched (void)
{
#if OS_CRITICAL_METHOD == 3u
OS_CPU_SR cpu_sr = 0u;
#endif
OS_ENTER_CRITICAL();
if (OSIntNesting == 0u && OSLockNesting == 0u) //如果中断嵌套层数等于 0,且调度器没有上锁
{
OS_SchedNew();①
Wav
eshar
e 微雪
电子
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy]; //将最高就绪任务的 TCB存给 OSTCBHighRdy
if (OSPrioHighRdy != OSPrioCur) //如果当前任务是最高就绪任务
{
OSCtxSwCtr++;
OS_TASK_SW()②; //进行任务切换
}
}
OS_EXIT_CRITICAL();
}
函数功能:查找最高优先级就绪任务,满足条件就让它运行。
函数所在文件:os_core.c。
相关说明:为增加可读性、可移植性,将汇编语言代码最少化,OSSched()用 C 写,虽然它可以用汇编实现。
-------------------------------------------------------------------------------------------------------------------------------------------
① OS_SchedNew()函数见后面章节说明。
② OS_TASK_SW()函数见后面章节说明。
-------------------------------------------------------------------------------------------------------------------------------------------
· OS_SchedNew()
static void OS_SchedNew①(void)
{
INT8U y;
y = OSUnMapTbl[OSRdyGrp]; ②
OSPrioHighRdy = (INT8U)((y << 3u) + OSUnMapTbl[OSRdyTbl[y]]); ②
}
函数功能:从就绪任务中查找最高优先级,将结果存到 OSPrioHighRdy。
函数所在文件:os_core.c。
-------------------------------------------------------------------------------------------------------------------------------------------
① 笔者认为,μC/OS-II后期流通版本的函数‚OS_SchedNew‛命名的不太对劲,如果将它改名为‚OS_SchedFindHighRdy‛之类,程序会更具可读性。
② 请参阅【第十二章】。
-------------------------------------------------------------------------------------------------------------------------------------------
· OS_TASK_SW() / OSCtxSw()
【节首语】
『代码字义』
为方便初学者,这里给出‚代码字义‛说明:
·SW:switch,切换。
·Ctx:context,上下文。
所以,CtxSW也就是‚context switch‛,即上下文切换,即任务切换。
『相关概念与说明』
这里集中给出本节需要读者事先了解的一些相关概念与说明:
·中断向量:中断服务程序的入口地址。本节的中断向量实际指异常处理程序或指令陷阱处理程序的入口地址。
·OS_TASK_SW()与 OSCtxSw()的关系:被替身与替身。OS_TASK_SW()只是一个宏,为的是可供 OS_Sched调用。
·建议初学者阅读【第三章】后,再来阅读本章,因为,本章要求读者明白多任务、临界区等基础理论。
Wav
eshar
e 微雪
电子
【经典风味】
所谓‚经典‛,即 X86等机器,‚经典风味‛也就是μC/OS-II在 X86等上经典的任务切换方法。
由于笔者觉得经典非常值得初学者学习,所以,整理了本节。
『任务切换实现方法步骤』
·将 OS_TASK_SW()定义为相应的中断向量
示例代码:#define OS_TASK_SW() asm INT 080H
·让相应的中断向量指向 OSCtxSw()
示例代码:1)ORG 080H 2)AJMP OSCtxSw() //这两行代码为伪代码①,取自 8051汇编代码
·在 OSCtxSw()里执行上下文切换
示例代码:(尽管 OSCtxSw()需要采用汇编语言编写,但,笔者在此给出类 C的伪代码)
void OSCtxSw(void)
{
SaveCPUReg; //大部分 CPU,需要用代码保存部分 CPU寄存器
OSTCBCur -> OSTCBStkPtr = SP; //保存当前任务的 SP
OSPrioCur = OSPrioHighRdy; //将即将运行任务的优先级给 OSPrioCur
OSTCBCur = OSTCBHighRdy; //将即将运行任务的 TCB给 OSTCBCur
SP = OSTCBHighRdy -> OSTCBStkPtr; //取得即将运行任务的 SP
RestoreCPUReg; //大部分 CPU,需要用代码恢复部分 CPU寄存器
RETI; //执行中断返回指令②
}
厨神提醒:读者可以配合【第三章】Solution 4 一起吃,味道更佳。
-------------------------------------------------------------------------------------------------------------------------------------------
① 笔者暂没查到相应代码,但大概的处理是:在 080H处放置跳转指令。
以下是其它也可供借鉴的资料;
.global _start
_start: (1)mov r8, #0 (2)adr r9, vector_init_block (3)ldmia {r0-r7} (4)stmia {r0-r7} …
vector_init_block: (1)ldr pc, reset_addr (2)ldr pc, undefined_addr (3)ldr pc, swi_addr …
reset_addr: .word reset_handler
undefined_addr: .word undefined_handler
swi_addr: .word swi_handler
说明:程序从_start开始执行,首先将 vector_init_block 开始的 8条指令复制到 0x00000000开始的地方,每条指令占 4字节,共 8*4=32字节。
假定程序从 0x30000000开始执行,之后调用 swi发生软件中断,pc跳到 0x00000008执行,该位置的指令是 ldr pc, swi_addr, 则再跳到 swi_addr执行。
为方便读者对比,以下提供μVisions,STM32的代码:
__Vectors DCD … DCD PendSV_Handler
② 细心的读者将发现:‚调用 OSCtxSw前,先调用了 OS_ENTER_CRITICAL(),关了中断‛,所以,可能会问以下问题:
·调用 OSCtxSw能触发中断?
答:可以触发中断,采用这种任务切换方式的 CPU(不是所有 CPU),其软件中断(及 NMI中断)的触发并不受中断允许标志、中断开关的控制。
·那执行中断返回指令能成功返回吗?
答:可以成功返回,关中断,并不影响中断返回指令的正确执行。
-------------------------------------------------------------------------------------------------------------------------------------------
【8051风味】
8051 MCU是早期 MCU的杰出代表,相信有不少读者用过,笔者亦在 10年前使用过它。
『任务切换实现方法步骤』
Wav
eshar
e 微雪
电子
·将 OS_TASK_SW()替代为 OSCtxSw()
示例代码:#define OS_TASK_SW() OSCtxSw()
·在 OSCtxSw()里执行上下文切换
示例代码:
_OSCtxSw:
PUSHALL①
OSIntCtxSw_in:
r1=[_OSTCBCur] ;获得当前 TCB的指针
[r1]=sp① ;OSTCBCur->OSTCBStkPtr = SP
r2=[_OSPrioHighRdy] ;将即将运行任务的优先级给 OSPrioCur
[_OSPrioCur]=r2
r1=[_OSTCBHighRdy]
[_OSTCBCur]=r1 ;将即将运行任务的 TCB给 OSTCBCur
sp=[r1] ;取得即将运行任务的 SP
POPALL
RETI
-------------------------------------------------------------------------------------------------------------------------------------------
① 读者看到,8051 MCU并没有触发软中断,而是由程序一一保存、恢复各 CPU寄存器,这是因为,8051 MCU并没有软中断可供触发。
PUSHALL将压栈所有 CPU寄存器,如,SP这类不需要被压栈的,也将被压如栈中。
-------------------------------------------------------------------------------------------------------------------------------------------
【CM3风味】
本书所研究的μC/OS-II基于 CM3 STM32,当然,研究 CM3是必不可少的。
『任务切换实现方法步骤』
·将 OS_TASK_SW()替代为 OSCtxSw()
示例代码:#define OS_TASK_SW() OSCtxSw()
·在 OSCtxSw()中设置①PendSV中断
示例代码:
NVIC_INT_CTRL EQU 0xE000ED04 ; 中断控制及状态寄存器 ICSR的地址
NVIC_PENDSVSET EQU 0x10000000 ; 触发 PendSV异常(将第 28位设为 1)
OSCtxSw
LDR R0, =NVIC_INT_CTRL ; NVIC_INT_CTRL:中断控制及状态寄存器 ICSR的地址
LDR R1, =NVIC_PENDSVSET ; NVIC_PENDSVSET:异常中断设置值
STR R1, [R0] ; NVIC_INT_CTRL = NVIC_PENDSVSET
; 之后只要开中断,就能引起 PendSV_Handler①
BX LR
·在 PendSV()里执行上下文切换
示例代码:
PendSV_Handler
CPSID I ; 关中断②,这是首先必须做的
MRS R0, PSP ; R0=PSP
CBZ R0, PendSV_Handler_Nosave ; 如果 R0=0,即 PSP=0则跳过下面的几行代码③
SUBS R0, R0, #0x20 ; R0=PSP-0x20④
STM R0, {R4-R11} ; 压栈 R4-R11④
LDR R1, =OSTCBCur ; R1=OSTCBCur的地址⑤
LDR R1, [R1] ; R1=OSTCBCur⑤
Wav
eshar
e 微雪
电子
STR R0, [R1] ; R1=*OSTCBCur⑤
; 以上三行代码的执行结果:OSTCBCur->OSTCBStkPtr = PSP⑥
PendSV_Handler_Nosave
LDR R0, =OSPrioCur
LDR R1, =OSPrioHighRdy
LDRB R2, [R1]
STRB R2, [R0] ; 以上四行代码的执行结果:OSPrioCur = OSPrioHighRdy
LDR R0, =OSTCBCur
LDR R1, =OSTCBHighRdy
LDR R2, [R1]
STR R2, [R0] ; 以上四行代码的执行结果:OSTCBCur = OSTCBHighRdy
LDR R0, [R2] ; R0 = OSTCBHighRdy->OSTCBStkPtr
LDM R0, {R4-R11} ; 出栈 R4-R11⑦
ADDS R0, R0, #0x20 ; R0 = OSTCBHighRdy->OSTCBStkPtr - 0x20⑦
MSR PSP, R0 ; PSP = R0
ORR LR, LR, #0x04 ; 构造 LR,以设置回线程模式
CPSIE I ; 开中断,因为进入本程序后关过
BX LR ; 返回,PendSV返回将跳到相应的任务函数地址⑧
END
-------------------------------------------------------------------------------------------------------------------------------------------
① 大部分书籍用了‚触发‛二字,极为不妥,则也误导了笔者当初的学习。笔者后来测试到的结果是,这里,仅仅是设置,而不能直接触发软中断。
因为,调用了 OS_ENTER_CRITICAL(),关了中断,而 CM3的 PendSV要在打开中断的环境下才能响应,所以只能在之后 OS_EXIT_CRITICAL()才可能执行。
之所以说可能是因为,OS_EXIT_CRITICAL()也未必打开中断,请参阅【第三章】。以上说明,笔者经过实际测试。
② 进入中断后,自动压栈了部分 CPU寄存器,也就是说上下文切换已经进行,这时,就应避免中断被打开,指导切换完成。
对比‚经典风味‛,我们将看到两者的不同是:经典不需要关中断,而 CM3需要关中断。导致原因见①。
③ 如果 PSP是 0,表示任务没有运行过,那么不需要压栈,首次运行的是由 OSStartHighRdy引起的软中断,而在 OSStartHighRdy中,PSP被初始化为 0。
④ 这两行代码可以改为:‚PUSH R11 PUSH R10 … PUSH R5 PUSH R4‛。但为简化代码(能提高运行速度?), 改为使用两行代码进行压栈。
其实,还可以用一行代码取代:‚STMFD R0!, {R4 - R11}‛。STMFD Rn{!},{Rn ' - Rn ''}的功能是:预先减少再存储数据,‚! ‛表示,最后的值写回 Rn。
为方便初学者,这里进一步对原文的代码进行说明:
·STM32发生中断时,最后入栈的是 R0,这时 SP指向入栈 R0的地址。由于 STM32是递减堆栈,而接着要入栈 R11-R4,所以,需要 SP先减少再存。
·减去的数值为分配的入栈数据的空间大小,R4-R11为 8个寄存器,共 0x20字节,所以,应减去数值为 0x20。
⑤ LDR Rn,= 'C Variable' 表示:将 C变量的地址放到 Rn;[Rn]表示:将 Rn中的数据作为地址,该地址中的数据。
所以,执行‚LDR Rn,= 'C Variable' ‛、‚LDR Rn,[Rn]‛的结果是,取得 C变量的值。这是 CM3读取 C变量值的方式,可以作为一个结论记得。
根据这个结论,执行‚LDR R1,= OSTCBCur‛、‚LDR R1,[R1]‛的结果是,取得 OSTCBCur的值,伪代码为 R1 = OSTCBCur。
但如果,我们要取得*OSTCBCur的值,则需要再取一次[Rn]。
⑥ 由于:·执行‚STR R0,[R1]‛的结果是,将 PSP存到*OSTCBCur中。(R0=PSP,[R1]=*OSTCBCur)
·OSTCBCur指向当前 TCB,更具体的说,OSTCBCur指向当前 TCB第一个字段 OSTCBStkPtr。
所以:将 PSP存到*OSTCBCur中,也就是将 PSP存到 OSTCBCur指向的 OSTCBStkPtr中,也就是 OSTCBCur->OSTCBStkPtr = PSP。
⑦ 这两条指令可以改为 1条指令,方法参考④。
⑧ 例:首次切换任务,切换到任务 AppTask。 static void AppTask(void *p_arg){ } ,将跳到‚{‛代码的位置。
-------------------------------------------------------------------------------------------------------------------------------------------
【经典与 CM3的任务切换对比】
经典和 CM3在任务级调度方面的任务切换有较大不同,表现在:
Wav
eshar
e 微雪
电子
·触发中断的方式不同
·经典:通过调用 OS_TASK_SW()实现触发中断。这个触发是必然的。
·CM3:通过调用 OS_TASK_SW()设置 PendSV中断,而后,通过 OS_EXIT_CRITICAL()开中断,而触发中断。
这个触发并非必然,触发的条件是,运行 OS_EXIT_CRITICAL()后,中断被打开。
·触发时机不同
·经典:调用 OS_TASK_SW()即引起。
·CM3:要等到 OS_EXIT_CRITICAL()后才引起。
·代码风格不同
·经典:上下文切换由 OSCtxSw()实现。
·CM3:上下文切换由 PendSV()实现。OSCtxSw()只是设置 PendSV中断。
·代码内容不同
·经典:在 OSCtxSw()中不需要开关中断。
·CM3:在 PendSV ()中需要开关中断。
第九章 OS 启动战队一
【源远流长的废话】
本章讲述 OSStart、OSStartHighRdy这两个函数。
· OSStart()
void OSStart(void)
{
if (OSRunning == OS_FALSE)
{
OS_SchedNew();
OSPrioCur = OSPrioHighRdy;
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
OSTCBCur = OSTCBHighRdy;
OSStartHighRdy();①
}
}
函数功能:启动 OS。
函数所在文件:os_core.c。
说明:在调用 OS_START()前必须建立至少一个任务②。
-------------------------------------------------------------------------------------------------------------------------------------------
① OSStartHighRdy()函数见后面章节说明。
② 曾有个朋友问我:‚看到某个文档写,调用 OS_START()前必须建一个任务,那能否建多个任务?‛
当时,我刚看了μC/OS-II几天,不知道答案,所以,用实验测试了下,可以建立多个任务。其实,这个问题不必用实践检验,如果熟悉μC/OS-II的话。
因为,在调用 OS_START()前,须先调用 OSInit(),而,在 OSInit()里已经先建立了两个任务:TaskIdle、TaskStat。
-------------------------------------------------------------------------------------------------------------------------------------------
· OSStartHighRdy()
NVIC_SYSPRI14 EQU 0xE000ED22 ; PendSV中断优先级的地址是 0xE000ED22
NVIC_PENDSV_PRI EQU 0x00
Wav
eshar
e 微雪
电子
OSStartHighRdy
LDR R0, =NVIC_SYSPRI14
LDR R1, =NVIC_PENDSV_PRI
STRB R1, [R0] ; 设置 PendSV 异常优先级
MOVS R0, #0
MSR PSP, R0 ; 将 PSP设置为 0,告诉上下文切换这是第一次运行
LDR R0, =OSRunning ; R0 = OSRunning的地址
MOVS R1, #1
STRB R1, [R0] ; OSRunning = 1①
LDR R0, =NVIC_INT_CTRL ; NVIC_INT_CTRL:中断控制及状态寄存器 ICSR的地址
LDR R1, =NVIC_PENDSVSET ; NVIC_PENDSVSET:异常中断设置值
STR R1, [R0] ;NVIC_INT_CTRL = NVIC_PENDSVSET
CPSIE I ; 开中断,才能触发 PendSV中断
OSStartHang
B OSStartHang
函数功能:启动最高优先级任务(首次启动)。
函数所在文件:os_cpu_a.asm。
-------------------------------------------------------------------------------------------------------------------------------------------
① 更合理的做法是将设置 OSRunning为 1的这部分代码放到 OSStart()中,但由于一开始没注意到这个问题,而后为了已移植过的系统,只能将错就错。
Jean.J.Labrosse提过这个问题。
-------------------------------------------------------------------------------------------------------------------------------------------
第十章 时间管理战队一
【长长久久的废话】
本章讲述 OSTimeDlyHMSM、OSTimeDly这两个函数。
· OSTimeDlyHMSM()
INT8U OSTimeDlyHMSM (INT8U hours, INT8U minutes, INT8U seconds, INT16U ms)
{
INT32U ticks;
if (OSIntNesting > 0u)
return (OS_ERR_TIME_DLY_ISR);
if (OSLockNesting > 0u)
return (OS_ERR_SCHED_LOCKED);
#if OS_ARG_CHK_EN > 0u
检测 hours、minutes、seconds、ms四个参数的合法性,如不合法,返回相应错误。
#endif
ticks = ((INT32U)hours * 3600uL + (INT32U)minutes * 60uL + (INT32U)seconds) * OS_TICKS_PER_SEC
+ OS_TICKS_PER_SEC * ((INT32U)ms + 500uL / OS_TICKS_PER_SEC) / 1000uL;①
Wav
eshar
e 微雪
电子
OSTimeDly(ticks);
return (OS_ERR_NONE);
}
函数功能:挂起当前任务,进行任务切换,当指定时间到后,将当前任务恢复为就绪态。
函数所在文件:os_time.c
-------------------------------------------------------------------------------------------------------------------------------------------
① 将 hours、minutes、seconds、ms转换为 ticks,并将 ticks进行四舍五入。
ticks = ((INT32U)hours * 3600uL + (INT32U)minutes * 60uL + (INT32U)seconds) * OS_TICKS_PER_SEC
+ OS_TICKS_PER_SEC * ((INT32U)ms + 500uL / OS_TICKS_PER_SEC) / 1000uL
由于((INT32U)hours * 3600uL + (INT32U)minutes * 60uL + (INT32U)seconds) * OS_TICKS_PER_SEC这部分代码显然正确。
所以,我们可以不管它,只研究 OS_TICKS_PER_SEC * ((INT32U)ms + 500uL / OS_TICKS_PER_SEC) / 1000uL这一部分。
OS_TICKS_PER_SEC * (INT32U)ms / 1000 + 0.5(为了四舍五入)
= OS_TICKS_PER_SEC * (INT32U)ms / 1000 + OS_TICKS_PER_SEC * 500 / OS_TICKS_PER_SEC / 1000
= OS_TICKS_PER_SEC * ((INT32U)ms + 500uL / OS_TICKS_PER_SEC) / 1000uL
-------------------------------------------------------------------------------------------------------------------------------------------
· OSTimeDly()
void OSTimeDly (INT32U ticks)
{
INT8U y;
#if OS_CRITICAL_METHOD == 3u
OS_CPU_SR cpu_sr = 0u;
#endif
if (OSIntNesting > 0u)
return;
if (OSLockNesting > 0u)
return;
if (ticks > 0u) //非 0值,则 OSTimeDly()会将当前任务从就绪表中移除
{
OS_ENTER_CRITICAL();
y = OSTCBCur->OSTCBY; ①
OSRdyTbl[y] &= (OS_PRIO)~OSTCBCur->OSTCBBitX; ①
if (OSRdyTbl[y] == 0u)
OSRdyGrp &= (OS_PRIO)~OSTCBCur->OSTCBBitY; ①
OSTCBCur->OSTCBDly = ticks;
OS_EXIT_CRITICAL();
OS_Sched(); //当前任务不再处于就绪表中,调度结果是:执行优先级最高的就绪任务
}
}
函数功能:挂起当前任务,进行任务切换,当指定时间到后,将当前任务恢复为就绪态。
函数所在文件:os_time.c
-------------------------------------------------------------------------------------------------------------------------------------------
Wav
eshar
e 微雪
电子
① OSTCBY、OSTCBX、OSTCBBitY、OSTCBBitX存在的目的是什么,请参阅【第十二章】
-------------------------------------------------------------------------------------------------------------------------------------------
第十一章 关于 Cortex-M3
【不曾被遗忘的废话】
本章将说明 CM3的相关知识。
· CPU 寄存器
我们在【第一章】已经简单说明了 CPU寄存器,本章继续进行详细说明。
CM3的 CPU寄存器包括:
·通用寄存器
·R0-R12:平民百姓
·R13(SP):存放堆栈指针
·R14(LR):存放最近一次被转向前的 PC值
·R15(PC):存放下一条将要执行的指令地址
·特殊功能寄存器
·程序状态寄存器组(PSRs),共 32位
·应用状态寄存器(APSR):[27-31]
·中断状态寄存器(IPSR):[0-8]
·执行状态寄存器(EPSR):[10-15],[24-6]
·中断屏蔽寄存器组
·PRIMASK :中断总开关。当 PRIMASK=1时,屏蔽所有中断(除 NMI和 fault外)
·FAULTMASK: 屏蔽错误中断
·BASEPRI:优先级屏蔽寄存器中断
·控制寄存器(CONTROL )
·CONTROL[0]:决定操作级别,0为特权级,1为用户级
·CONTROL[1]:决定当前堆栈指针,0为主堆栈(复位缺省),1为备用堆栈
· 操作模式与操作级别
【两种操作模式】
CM3支持两种操作模式:处理者模式(handler mode)和线程模式(thread mode):
·线程模式:运行在应用程序时所处的模式。(系统复位后自动进入线程模式)
·处理者模式:运行在异常服务程序时所处的模式。
总结及说明:
所处的模式并非由代码控制(或者说并非代码能控制),而是由所处的代码类型决定。
这也是 CM3引入模式的用意,硬件支持了区分普通应用程序代码和异常服务程序代码(包括中断服务例程代码)。
CM3在执行中断返回指令时必须处于处理模式下,否则将引起内存访问异常。
这样,即使程序跑飞后,进到异常处理代码,也无法返回。(查到的资料,读者未跳转进行验证)
『两者的转换』
·线程模式
·中断返回应用程序时,由处理者模式切换到线程模式。
·处理者模式
·由应用程序转入中断时,由线程模式切换到处理者模式。
Wav
eshar
e 微雪
电子
【两级操作级别】
CM3支持两级操作级别:特权级和用户级:
·特权级
·既可执行主应用程序(这时处于线程模式),也可执行异常服务程序(这时处于处理者模式)。
·程序可以访问所有范围的存储器,并且可以执行所有指令。
·用户级
·只能执行主应用程序(这时处于线程模式)。
总结及说明:
这是一个基本的安全模型,可以提供一种存储器访问的保护机制。
若配 MPU,它还可以作为特权机制的补充 —— 保护关键的存储区域不被破坏,这些区域通常是操作系统的区域。
能够在硬件水平上限制某些不受信任的或者还没有调试好的程序,这使得普通的用户程序代码不能意外地,甚至是
恶意地执行涉及要害的操作,因而系统的可靠性得到了提高。
『两者的转换』
·特权级
·可以通过改写 CONTROL寄存器,切换到用户级。
·用户级
·无法通过改写 CONTROL寄存器,切换到特权级。
·只能通过①以下步骤进入特权级:
·执行一条系统调用指令(SVC)(将触发 SVC异常)
·进入异常服务程序,处于‚特权级处理者模式‛(由硬件自动完成)
·修改 CONTROL[0]寄存器,切换到特权级
-------------------------------------------------------------------------------------------------------------------------------------------
① 这是唯一途径。触发异常后,处理器总是先切换成特权级,并且在异常服务例程执行完毕退出时,根据 CONTROL寄存器决定切换成特权级还是用户级。
-------------------------------------------------------------------------------------------------------------------------------------------
【两种模式与级别的关系】
以下为合法的操作模式转换图:
整理成表格,如下:
代码类型 特权级下 用户级下
异常处理代码
·处理者模式可访问
·线程模式不可访问(程序跑飞)
·处理者模式(不会出现这种情况)
·线程模式不可访问(程序跑飞)
主应用程序代码 ·处理者模式可访问(程序跑飞)
·线程模式可访问
·处于主应用程序,不应设为特权级
·处理者模式可访问(程序跑飞)
·线程模式可访问
·处于主应用程序,应设为用户级
Wav
eshar
e 微雪
电子
将上述表格简化,可以得到:
代码类型 特权级下 用户级下
异常处理代码 处理者模式 //////
主应用程序代码 线程模式 线程模式
· 中断
『NVIC 简介』
CM3在内核水平上搭载了一颗中断控制器——嵌套向量中断控制器 NVIC(Nested Vectored Interrupt Controller)。
它与内核紧密相连,能提高相应性能。
中断控制器 NVIC的功能、性能如下:
·可嵌套中断支持
可嵌套中断支持的作用范围很广,覆盖了所有的外部中断和绝大多数系统异常。
外在表现是,这些异常都可以被赋予不同的优先级。当前优先级被存储在 xPSR 的专用字段中。
当一个异常发生时,硬件会自动比较该异常的优先级是否比当前的异常优先级更高。
如果发现来了更高优先级的异常,处理器就会中断当前的中断服务例程(或者是普通程序),服务新来的异常。
·向量中断支持
当开始响应一个中断后,CM3会自动定位一张向量表,并且根据中断号从表中找出 ISR的入口地址,
然后跳转过去执行。不需要像以前的 ARM那样,由软件来分辨到底是哪个中断发生了,
也无需半导体厂商提供私有的中断控制器来完成这种工作。这么一来,中断延迟时间大为缩短。
·动态优先级调整支持
软件可以在运行时期更改中断的优先级。如果在某 ISR 中修改了自己所对应中断的优先级,
而且这个中断又有新的实例处于悬起中(pending),也不会自己打断自己,从而没有重入(reentry)风险。
·中断延迟大大缩短
CM3为了缩短中断延迟,引入了好几个新特性。部分特性在前文已提到。
除此外,还包括自动的现场保护和恢复,以及其它的措施,用于缩短中断嵌套时的 ISR间延迟。
·中断可屏蔽
·可以屏蔽优先级低于某个阈值的中断/异常(设置 BASEPRI寄存器)。
·可以全体封杀(设置 PRIMASK 和 FAULTMASK 寄存器)。
『中断的入栈出栈』
产生中断后:
·入栈
·硬件本身将自动入栈①CPU寄存器:xPSR–> PC–> R14(LR)–> R12–> R3-R0。(入栈顺序由前到后)
·硬件本身没有自动入栈②CPU寄存器:r4-r11、sp
③。
·出栈
·硬件本身将自动出栈①CPU寄存器:R3-R0–> R12–> R14(LR)–> PC–> xPSR。(入栈顺序由前到后)
·硬件本身没有自动出栈②CPU寄存器:r4-r11、sp
③。
-------------------------------------------------------------------------------------------------------------------------------------------
① 因为,它们在发生中断时要被使用。
② 读者可能有疑问:‚为何 STM32的中断不压栈所有 CPU寄存器呢?那这样在前后台系统中,如果用到了其它寄存器,那程序岂不不能正常运行?‛
答:‚能正常工作,因为,编译器在编译时,将知道哪些寄存器被使用,并确定是否需要生成相应代码进行压栈。‛(相关内容请查阅【第二章】)。
③ 其中,sp不需被保存,因为它本来就要被保存到全局变量中,所以,不需要再存到栈中。
-------------------------------------------------------------------------------------------------------------------------------------------
Wav
eshar
e 微雪
电子
第十二章 μC/OS-II 代码特色
【被遗传的废话】
本章介绍一些μC/OS-II的代码特色,所谓特色,就是一些可供初学者学习的亮点。
· 巧妙的全局变量定义与声明
熟悉 C语言的程序员,如果想在多个文件中使用一个全局变量 var,一般的处理如下:
//include.h
extern int var ;
some code …
//include.c
#include"var.h"
int var = 10;
some code …
//otherA.c
#include"var.h"
some code …
//otherB.c
#include"var.h"
some code …
大多数 C程序员一般认为:
·.h文件里可以有的是:避免重复加载的条件编译、常量、结构、定义类型、声明变量、声明函数①。
·.h文件里不可以有的是:定义变量, 定义函数。
然而,μC/OS-II却离经背道,颠覆了‚不可以在.h里定义变量‛这一‚规矩‛。
它反过来,在.H文件中‚声明+定义‛全局变量。
下面以 UCOS_II.H、UCOS_II.C为例说明问题。
UCOS_II.H的代码如下:
#ifndef OS_uCOS_II_H
#define OS_uCOS_II_H
#ifdef OS_GLOBALS
#define OS_EXT
#else
#define OS_EXT extern
#endif
some code …
OS_EXT INT8U OSPrioCur; //‚声明+定义‛二合一
OS_EXT INT8U OSPrioHighRdy; //‚声明+定义‛二合一
some code …
#endif
UCOS_II.C的代码如下:
#define OS_GLOBALS
Wav
eshar
e 微雪
电子
#include <ucos_ii.h>
some code …
这样,编译器编译工程 C文件时便会这么处理 UCOS_II.H:
·如果编译的是 UCOS_II.C:
由于代码‚#define OS_GLOBALS‛的作用,就编译了‚#define OS_EXT‛语句,即 OS_EXT相当于空。
相应的定义变量代码就变为:
INT8U OSPrioCur;
INT8U OSPrioHighRdy;
·如果编译的是其它 C文件:
由于没有代码‚#define OS_GLOBALS‛的作用,就编译了‚#define OS_EXT extern‛语句。
相应的定义变量代码就变为:
extern INT8U OSPrioCur;
extern INT8U OSPrioHighRdy;
这样做,有以下优点:
·全局变量‚声明+定义‛二合一,有且只有一次。
·编译不会报错,不会重复分配多次变量空间。
·全局变量可以被任何 C文件使用。
-------------------------------------------------------------------------------------------------------------------------------------------
① C编译器:声明变量必须有 extern关键字,且不可以赋初值。
函数声明可缺省 extern关键字,笔者尚未调查过是否这个是硬性规定,如果不是,那么,严谨写代码的话,还是加上 extern。
-------------------------------------------------------------------------------------------------------------------------------------------
· 适当的空间换时间
μC/OS-II的空间换时间思想也值得初学者学习,本节整理相关信息。
【任务就绪表】
图 1 任务就绪表
【使任务进入就绪态】
『方法』
将任务所对应的 OSRdyTbl[]和 OSRdyGrp位置‚1‛。
这样,后续,在执行任务调度时,通过 OSRdyGrp 的值即可判断出第 x 组任务中有任务处于就绪态,然后再通过
OSRdyTbl[x]数组的第 y个字节即可获知,哪个任务处于就绪态,以便,后续做任务切换。
Wav
eshar
e 微雪
电子
『一个特例』
假设要让优先级为 12的任务进入就绪状态(12 = 1100b),则:
OSRdyTbl[1]的第 4位①置 1,且 OSRdyGrp的第 1位置 1(以表第 1组有任务处于就绪态)
相应的代码为:OSRdyGrp|=0x02;OSRdyTbl[1]|=0x10;
『进入就绪态代码』
我们观察前面例子的特例,以便给出一般方法。
OSRdyGrp的第 1位置 1:与 0x02相或;OSRdyTbl[1]的第 4位置 1:与 0x10相或。
即:若 OSRdyGrp及 OSRdyTbl[x]的第 n位置 1,则应该把 OSRdyGrp及 OSRdyTbl[x]的值与 2^n相或。
为减少计算时间,我们希望以空间换时间,所以,我把 2^n的 8个值存在数组 OSMapTbl[]中:
OSMapTbl[]={0x01, 0x02,…, 0x80}
要让优先级 Prio的任务进入就绪状态,需要对相应的 Grp及 Tbl进行处理。
相应的 Grp及 Tbl当然由 Prio唯一确定。
μC/OS-II中,优先级数分解为高 3位和低 3位,高 3位代表任务组号,低 3位代表任务在所在组中的位置。
所以,任意优先级为 prio的任务进入就绪态只需执行以下程序:
OSRdyGrp |= OSMapTbl[prio>>3];
OSRdyTbl[prio>>3] |= OSMapTbl[prio&0x07];
-------------------------------------------------------------------------------------------------------------------------------------------
① 起始值为第 0位,本节说明内容均基于此。
-------------------------------------------------------------------------------------------------------------------------------------------
【使任务进入空闲态】
『方法』
将任务所对应的 OSRdyTbl[]位置‚0‛。若置‚0‛后,所在组全为 0,还需将 OSRdyGrp的相应位置‚0‛。
『OSUnMapTbl』(就绪态最高优先级任务计算表)
【查找就绪态最高优先级任务】
『一个特例』
假设 OSRdyGrp的值为 01101000b,OSRdyTbl[3]的值是 11100100b,则:
OSRdyGrp最低位为 1的是第 3位,OSRdyTbl[3] 最低位为 1的是第 2位,所以:
处于就绪态的最高任务的优先级 prio=3x8+2=26。
『从特例看 OSUnMapTbl』
OSRdyGrp的值为 01101000b,查得 OSUnMapTbl[OSRdyGrp]的值是 3
OSRdyTbl[3]的值是 11100100b,查得 OSUnMapTbl[OSRdyTbl[3]]的值是 2
Wav
eshar
e 微雪
电子
High3 = OSUnMapTbl[OSRdyGrp]; //优先级高 3位,即当前处于就绪态的最高优先级的任务的组号
Low3 = OSUnMapTbl[OSRdyTbl[High3]]; //优先级低 3位
Prio = (Hign3<<3)+Low3;//获得当前处于就绪态的最高优先级的任务
『OSUnMapTbl存在的意义』
从上文的可以知道,μC/OS-II查找当前最高优先级任务所需时间很短且为常数,
与应用程序中建立的任务数无关,这个特性是本文实现新型嵌入式数据管理的关键。
第十三章 FAQ
笔者及部分读者在学习的过程可能会存在一些疑问,本章整理若干常见问题,并不断更新。
【问:将 SP指向即将运行任务的栈顶地址,SP不是一直指向栈顶地址?】
如果有这个疑问,说明读者还没搞清楚比较基础的问题。
首先,请查看前面关于栈的章节再回来看下文。
SP 确实是指向栈顶,但存在多个任务,就有多个栈顶,切换任务,就需要将 SP 指向即将运行任务的栈顶。
【问:主程序堆栈、任务堆栈、任务控制块存储空间是否会有冲突?】
·‚主程序堆栈‛与‚任务堆栈‛:
可能会有冲突,更具体的说,‚任务堆栈‛可能会覆盖‚主程序堆栈‛。
但,系统一旦交给 OS 处理后,就不会再返回主程序,所以,就算‚主程序堆栈‛被覆盖,也跟系统运行没
一分钱关系,不必理会。
·‚任务堆栈‛与‚任务控制块‛:
‚任务堆栈‛存放局部变量,‚任务控制块‛属于全局变量,这两者在编译时就决定了不会冲突。
除非编译器有问题,或者堆栈溢出。
【问:让新任务运行时,装回原来被中断之前的 CPU 寄存器即可?】
是的。CPU寄存器,已包括了 Rn、SP、LR、PC、xPSR,这些重要数据。
当然,这还要有其它‚隐含条件‛,如,每个任务有自己专用的 RAM空间①等。
其实,这是宿命,让任务具备了上一次被挂起前:一样的 ROM(这个本来就不变),一样的 RAM,一样的 CPU。
再让任务继续运行,必然能正确无误的接着运行。
有些读者问:‚如果原任务访问的资源(I/O资源等)在执行其它任务时也没有被改变过,那不能说系统能‘正确无
误的接着运行’。‛当然,但这是用户没能正确避免资源竞争的设计问题,而不是任务切换问题。
-------------------------------------------------------------------------------------------------------------------------------------------
① 这个条件只是相对于μC/OS-II而言,实际上,可以不必这么做。
-------------------------------------------------------------------------------------------------------------------------------------------
【问:在μC/OS-II 中,系统内核,包括调度器等运行在哪个线程?是否有自己独立的线程?】
如果有这个疑问,说明读者还没搞清楚比较基础的问题。
μC/OS-II 内核,包括调度器不会有自己的线程,它‚寄生‛在各线程中当,不会有自己的线程。
若系统有任务切换,则,有如以下情况发生:xxx
·当前线程将自身挂起,并通过调用 OS_Sched 进行任务调度。
·中断服务程序让某个任务进入了就绪态,并通过调用 OSIntExit 进行任务调度。
笔记
如果说本书是笔记,那么本节就是笔记中的笔记 —— 笔者专用,读者可以不必阅读。
Wav
eshar
e 微雪
电子
【μVisions里看到加载到 PC的值总是奇数】
查到资料:
PC的 LSB 读回内容始终为 0,不论是直接写入 PC的值,还是使用分支跳转命令,
都要求加载到 PC的值是奇数(LSB=1),用以表明处理器是在 Thumb状态下执行。
若写入 0,则视为企图跳转到 ARM模式,Cortex-M3将产生一个 fault异常。
【无法通过‚单步调试‛的方式进入 PendSV(),只能设断点】
可能是由于:
·单步调试,打乱了 MCU原中断方式。(这个是笔者的猜想)
·单步调试,在关闭 MCU中断的状态下执行。(这个在 rt-thread论坛里查到)
对这个问题,以上原因暂未进行进一步确认。
【确认 OSCtxSW()是否直接触发 PendSV()】
由于单步调试无法进入 PendSV(),只能通过其它方法确认触发 PendSV()。
·改动代码:
·在 OSCtxSW()、OSIntCtxSw()中,让全局变量 testGlobal减 1。
·在 PendSV()中,让全局变量 testGlobal加 1。
·在 1秒定时中断里,printf testGlobal的值。
·执行现象:
·第一次,显示数值比初值加 1。
·之后,testGlobal一直保持不变。
·分析:
·第一次由 OSStartHighRdy触发,所以,数值加 1。
·之后,OSCtxSW()或 OSIntCtxSw()总是与 PendSV()成双成对出现,所以,数值没有变动。
·结论:
·OSCtxSW()或 OSIntCtxSw()开中断后,总是会触发 PendSV()。
【确认调用 OSCtxSW()后,什么时候触发 PendSV()】
·改动代码:
·OS_Sched()函数:
·在 OS_TASK_SW()前后,对 testGlobal进行赋值,代码如下:
testGlobal = 0;
OS_TASK_SW();
testGlobal = 1;
·在 OS_EXIT_CRITICAL ()前后,对 testGlobal进行赋值,代码如下:
testGlobal = 2;
OS_EXIT_CRITICAL();
testGlobal = 3;
OS_Sched()函数:
·PendSV()函数:
·进入函数后,加入如下伪代码:
testGlobal = testGlobal + 4;
·执行现象
·testGlobal = 6
·结论:
·调用 OSCtxSW()后,再执行 OS_EXIT_CRITICAL()后,开了中断才触发 PendSV()。
Wav
eshar
e 微雪
电子
编后语
自私提示 如果你是个正经人或是个皇冠级专家,建议你飘过本段。(这个温馨的自私提示,只为了不让你骂我)
【本书的编写意图】
笔者编写本书,关键意图是:
·学习、总结。
笔者学习μC/OS-II的目的是,了解实时操作系统。
然而,μC/OS-II的资料虽然很多,却较为凌乱,至少并不很适合笔者。所以,笔者决定做笔记。
这是笔者首次为嵌入式技术做笔记,笔者希望能总结一些学习方法,形成套路,以提高自身的学习能力。
·留记录。
如果在学习时能理解,但却没留下任何自己的笔记,那么,之后,需要用到这些知识,这可能又是从头学一次。
这是比较郁闷的,笔者是个业余爱好者,很可能学过后,就长年不用μC/OS-II。
但,万一以后需要使用,或者需要相关知识,那,当然不希望什么都没留下。
于是,笔者决定做笔记。
【本书的编写及编排方式】
‚传统书籍‛的惯用做法是:先罗列相关基础知识,再研究μC/OS-II。本书则没有采用这种做法。
本书各章的编写及整体的编排,按照‚遇到问题->分析问题(给出一定资料)->给出答案‛这个原则进行。
有时,一个问题,会转变成好几个小问题,而小问题,下面还有小问题,直到解决各个小问题。
各个小问题及它们的答案可能分别在各个章节。
当然,笔者也不可能完全按照自己学习顺序去编排,毕竟,那样也是不可取的。
所以,读者可以看到,前面几章还是稍微的介绍了下相关知识。
之所以,本书会有这样的特点,是因为:
·笔者学习μC/OS-II,采用了‚步步探究‛的方式①,而本书的雏形在学习过程形成。
·笔者认为将这样编写有一定的优点(见下节)。
-------------------------------------------------------------------------------------------------------------------------------------------
① 所谓‚步步探究‛,就是在没有事先系统的学习的条件下,直接接触问题,分析问题,并试图解决问题。(虽然,笔者也稍微了解下实时系统的基础知识)
遇到问题,不懂就问百度,google,一直问到懂为止。(当然,问的前后,同时进行猜想、思考、动手实验,力图解决每个遇到的问题)
其实,如果你不能理解一个问题,通常,意味着,你无法理解一个更为简单的问题。要做的就是一直问下去,直到把那个不明白的点给挖出来。
举个例子,不知道为何‚32+23=55‛,通常是不知道最基本的加法运算 —— 10以内的加法运算。所以,要做的就是搞清楚它。
‚步步探究‛就像是程序员按‚程序的执行顺序‛阅读代码 —— 调用了函数就看下函数做了什么,函数下面可能还有子函数,子函数下面又有子函数。
至顶向下看,其实,也能搞清楚问题。否则,代码及资料是一片汪洋,先全面阅读子函数及大量相关知识,再搞清楚系统,即使可行也不容易。
听过不少人μC/OS-II难,其实,是基础不好或学习方法有问题。
可以说,学μC/OS-II,需要很多基本知识,但也可以说,什么都不需要。
笔者学习μC/OS-II之前已有将近 5年几乎没摸过 C语言,STM32更是可以说没用过(有的只是少量 pc端的编程经验),但也成功自学了μC/OS-II。
-------------------------------------------------------------------------------------------------------------------------------------------
【本书的优缺点】
‚传统书籍‛的优点是,能让读者‚按部就班‛的进行学习。缺点则是,较为枯燥。本书与它们不同。
本书有以下优点:
·边学边写,初学者遇到的问题,很可能被提出并解决。
·分上、中、下册,由浅入深的介绍μC/OS-II。如,上册介绍的μC/OS-II为‚裁剪版‛,便于抓住重点。
当然,由于有‚自学笔记‛的身影,所以,本书难免存在以下缺点:
·对问题的认识不一定够深刻,对问题的讲解不一定够透彻。
·不适合那些本来就基本理解μC/OS-II的用户。
Wav
eshar
e 微雪
电子
【本书等待改进的地方】
·本书多处提到‚请参阅【第 X章】‛,这些地方并没有写明在【第 X章】中的哪一节,不便读者阅读。
这个暂未‚知错能改‛,原因是:
本书尚处于‚低级版本‛,被修改的可能性很大,笔者没法保证修改章节名称时,会同时将引述它的也改掉。
当笔者认为本书内容较为完善后,最后才会修复以上问题。
·本书代码中的注释较少,有些不容易明白的代码没有给出注释。
这个原因可能是:
·有些代码尽管用意不易了然,但这些代码在其它地方已经注释或说明过了,所以,不再重复注释、说明。
所以,当你对代码有疑问,而代码却没有被注解时,建议你 CTRL+F下,可能有意外惊喜。
·有些代码读者自身觉得很难理解,其实是由于读者的基础不扎实导致。
解决这个问题的办法是,先百度、GOOGLE,如果还是搞不懂,问笔者。
【鸣谢】
·μC/OS-II(第 2版)(Jean J.Labrosse著,邵贝贝译)
·CM3权威指南
·屌丝网页
=================================================
我是草稿
加入对空闲任务的说明
=================================================
Wav
eshar
e 微雪
电子