Coos简介
最近开始为51写一个分时任务管理,取名coos,终于也差不多把功能完善了,趁这几天还有点冲动发布上来吧。Coos最大的特色:完全用C语言实现。让我不禁原创一句:敲一敲键盘,不留下一句汇编。呵呵~~~接下来吐槽一下先。
吐槽 在51上实现任务管理?省省吧,有时间不如去搞arm,Linux?那才是高深技术!如果你是这样想的,那这篇文章你没必要再看下去了…….如果你觉得不把51玩个透彻,使尽脑汁就是要把它发挥到极致,就像PC上很多高手,听说有不少人拿CPU来超频,玩得可以闻到烧焦味,偶尔主板冒起烟来,甚至时不时地喷出火花(那种感觉是帅啊~~)。就算不是发烧友,但至少温度稍高一点点吧。如果是这样的话,那么,兄弟………………..知音呐!呜呜呜,海内难道真的存知己?
还是转入正题吧。貌似已经有不少高手为51写系统了,ucos也算是可以移植到51上(我没测试过),嵌入式实时内核,但是毕竟占用资源太多,而且让我感觉学术性大于使用性,好像很少人用?(至少我没用过)。俺的分时任务管理,算不上系统,估计也经不起什么考验,不过俺还是写了出来,毕竟自己写的东西自己用着才舒服嘛。老是用别人提供的玩久了也无聊。慢慢完善俺的coos吧,下次在把它移植到stm32上,嘿嘿~~
为什么写这个分时任务管理?嗯,这个问题问得好!(口气来自9527…)。这学期上了一门课,微型计算机原理与接口技术,加上看了ucos一点点任务管理代码,心里那个是不淡定啊。感觉任务管理也不难,何不尝试自己写一个任务管理?再加上本人实在无聊,就决定写出这个无聊的任务管理了。
Coos的命名 先说一下命名,本人网名coolweedman,操作系统简写为OS,coolweedman+OS让我只能想到coos,所以就命名coos了,译音“裤子”,在51上跑coos,我就想到了给51穿一条裤子呵呵,习惯裸跑,偶尔加条裤子感觉还行。
Coos的任务管理原理 说一下任务管理的原理。学过微机的同学就知道,无非是直接或间接去修改PC指针。没错。我这里是间接修改PC指针的。怎么个间接法?利用出栈来修改!如果入栈的时候PC指针入栈,那么出栈的时候就是对PC指针的修改了。只要控制出栈时PC指针的值,就可以实现任务调度了。具体做法是: 1. 定时器产生中断(分时的依据) 2. 中断引起入栈,(PC指针入栈,其他寄存器也有) 3. 定时器响应函数判断需要执行哪个任务 4. 修改SP指针为相应任务的SP,(注意不是PC指针) 5. 定时器按照相应的SP出栈,实现间接修改PC指针(任务调度) 大概思路就是这样,算是伪代码吧。然后把相应的伪代码用C语言实现,就完成了(当然其中还有很多细节部分,编写代码时才会发现)。
Coos的任务管理机制 再说一下任务管理机制吧。分时任务管理,一个周期20个时间片(多少个可配置,时间片具体时间也可配置),在这20个时间片上,运行任务。coos最多管理8个任务(多少个可配置),每个任务时间片在创建时候指定。比如有5个任务,时间片分别为2,3,10,11,6。那么coos执行任务是这样的。
第一次:
任务1执行1个时间片
任务2执行1个时间片
任务3执行1个时间片
任务4执行1个时间片
任务5执行1个时间片
(已占用5个时间片)
第二次:
任务1执行1个时间片
任务2执行1个时间片
任务3执行1个时间片
任务4执行1个时间片
任务5执行1个时间片
(已占用10个时间片)
第三次:(任务1的2个时间片执行完,不执行任务1)
任务2执行1个时间片
任务3执行1个时间片
任务4执行1个时间片
任务5执行1个时间片
(已占用14个时间片)
第四次:(任务2的3个时间片执行完,不执行任务2)
任务3执行1个时间片
任务4执行1个时间片
任务5执行1个时间片
(已占用17个时间片)
第五次:
任务3执行1个时间片
任务4执行1个时间片
任务5执行1个时间片
(已占用20个时间片)
一次系统周期结束(一次任务循环),coos重新刷新时间片。也就是5个任务,时间片分别为2,3,10,11,6。然后按照任务时间片去执行。
这样就实现了所谓的分时任务管理了。因为是分时,所以如果有多个任务,没有一个任务可以占用全部CPU,各个任务都是平等的,呵呵。
这样的效果是,如果有多个任务,那么,每个任务占用CPU必然减小,就像平时Windows开了太多任务,就会觉得有点卡。哈哈~~~
Coos的使用 最后说一次怎么使用吧。coos包含文件:(6个) Coos_typedef.h 类型定义
Coos_conf.h coos配置,相当于裁剪
Coos.c coos实现的源文件
Coos.h coos实现的头文件
Coos_hook.h coos钩子函数头文件
Coos_hook.c coos钩子函数源文件
包含这些文件之后,主函数只要调用几个coos函数就可以实现分时任务管理了。举个例子:
void main(void)
{
OS_Init(); //coos初始化
OS_TaskCreate(Task_One, Stk_TaskOne, 5,0);
OS_TaskCreate(Task_Two Stk_TaskTwo, 2, 1);
OS_TaskCreate(Task_Thr, Stk_TaskThr, 7, 2);
OS_Start(); //coos开始运行, 任务交给coos管理
}
结束语 简介到此结束,再慢慢为coos写一份说明书吧,呵呵,源码开发下载,上传在我的百度网盘上。 欢迎同学们下载,当然coos会有bug,但是我还没有发现,还请同学们多多支持多多反馈啊!!
目录
第一章前言 第二章coos简介 第三章任务管理详解 第四章任务管理的幕后操作 第五章任务管理的幕后操作 第六章coos条件编译 第七章coos的配置 裁剪 类型定义
前言 (以下所说的51特指STC89C52RC,晶振12M)
经过了七七四十九天的闭关修炼,终于把51的分时任务管理实现了?
有点夸张。从产生为51写任务管理的念头,到开始着手写代码,应该有3,4天的时间。然后接下来就一直写个不停了。本来是从开始写到第二天,功能就实现了,剩下的就是慢慢完善。但是,事情总是没那么顺利,发现了一个又一个的bug,修改完一个又有另一个,再修改一个。这样持续了好几天。有时候一整天没解决一个bug …
修改bug就是每天晚上睡不好,老是想着coos的bug,失眠是必须的。到最后发现了一个致命的bug,发现不得不用汇编来解决,然后就尝试着C语言嵌入汇编,但却偏偏一个C语言函数汇编出来的代码执行有错…让我只能怀疑编译器的问题的,因为本来没嵌入汇编那个函数是正常的,嵌入汇编后,函数的返回值传递出错了(编译器把返回值放到R3还是哪个忘记了,然后却从R4取返回值?问题我至今仍未能理解),我只能崩溃了。最后终于还是上火了,感冒发烧头痛…到校医院看病额。
把coos搁一边一两个星期了,心里那个还真是不爽。把代码全部重写,并用回了最初的设计思想,任务调度完全在中断里面实现,让中断负责任务切换所有工作,参考着自己上次写的代码,老样子,功能是一两天就实现了,幸运的是这次调试没发现什么bug。然后,这样一搁又是好多天。
在电脑上安装win8来玩玩,挺新鲜的,界面也挺好看。期间曾一度因为在win8上运行不了RVMDK而让我再度崩溃多次。最终放弃win8,用回win7。
重新在win7上把coos完善。也就是多写了一些函数。像延时,任务删除,条件编译等等,也是搞了好多天,每个函数都需要调试看看正不正确啊…
当然最终功能也差不多,而且发现暂时已经不想再写下去了,就发布一下新浪博客。虽然我知道没人看,但是因为以前在网上看到一句话,原话我忘记了。意思就差不多是:经历了一段时间的学习,最好最后把学到的东西记录下来,走的弯路也记下来,不然留不下什么东西,最终忘记了也就像一无所获。学到的东西最好是发到网上分享。这些话我是接受了,因为我也是经常在网上下载别人的资料学习,所以这次我就发了出来…虽然没用。就当留作纪念吧呵呵。
第二章 coos简介
coos是一个专门为51写的分时任务管理,完全由C语言实现,传统的任务管理总会涉及到汇编,因为有些操作是C语言实现不了的,例如出栈入栈,现场保护等。设计coos是我就试着尽量减少汇编代码,到最后居然真的实现了零汇编,是最理想的结果。不过我得先说明的是,任务管理的话,我也觉得从汇编的角度去理解效果会好一点。
coos是利用51的定时器0中断作为分时依据,进入中断后判断需要运行哪个任务,中断退出便切换到相应的任务去执行,实现了任务调度。这种管理机制类似于我们熟悉Windows,例如我们平时在Windows上运行多个任务时,有时会觉得卡。coos也一样,如果任务多了,每个任务占用的CPU必然就减少,给我们的感觉也是有点卡。
分时笼统的讲就是把一秒钟(比较容易理解的单位)划分周期,我这里是划分成10个周期,每个周期100ms,然后每个周期划分成20个时间片,每个时间片为5ms,也即是每个任务运行时间的最小单位是5ms(一个时间片)。一个周期有20个时间片,当有多个任务时,每个任务轮流运行一个时间片。还需要说明的是一个周期有20个时间片,每个任务一个周期运行多少个时间片需要设置。例如有2个任务,其中一个任务设置运行时间片为4,另一个设置运行时间片为50,那么在coos管理的一个周期中,任务一运行4个时间片,任务二运行16个时间片。如果没创建新任务或者删除任务的话,下个周期也是这样执行。
当然为了让coos更灵活,我也用了不少的条件编译。实现可配置,可裁剪。例如一个时间片的时间是5ms还是其他,用户可自行配置。一个周期有多少个时间片也可配置。当然还提供了不少函数,例如任务删除,任务主动放弃CPU,系统延时等。因为有些实在不需要用到,所以用条件编译实现可裁剪。尝试了裁剪掉除任务管理以外的功能,coos占用RAM :44Byte + 1bit,code:692
还需要说的是coos的包含的文件。 coos.c 任务管理实现源文件 coos.h 任务管理实现头文件 coos_hook.c 钩子函数源文件 coos_hook.h 钩子函数头文件 coos_conf.h 任务管理配置头文件 coos_typedef.h 类型定义头文件
coos的实现由coos.c完成,是coos最核心的文件。 coos_typedef.h是为了coos的移植性需要加入的,定义coos使用的变量类型,如uint8_t coos_conf.h是coos的配置文件,实现的可裁剪功能都在这个文件设置,也就是条件编译的开关。 coos_hook.c钩子函数源文件是由用户实现的,类似于ucos的钩子函数,例如选择启用任务调度的钩子函数,用户将自己的钩子函数写好后每次任务调度都会调用任务调度的钩子函数。
第三章 任务管理原理
在这里最重要的是要知道PC指针是什么?翻译成中文是程序计数器,它保存的是CPU下一个指令的地址。下一个指令是什么?PC指针不知道,但是它知道下一条指令放在哪里,就是PC指针指向的那个地址的内容。修改了PC指针,就修改了下一条指令的位置,PC指针指向哪里,CPU就运行哪个指令。任务调度就是通过修改PC指针实现的,我这里是间接修改PC指针,当然修改前要将它保存,也就是入栈操作。修改它就是通过出栈操作完成的。还有51的PC指针是16位的。入栈占用了两个字节。下面再详细介绍:先看任务管理函数的截图。
功能看截图就大概可以理解了。我再说明一下大概思路: 606行之前:51产生定时中断,C语言看不到的入栈操作:包含R0~R7,ACC,B,DP0L,DP0H,PSW,还有最重要的PC指针,也就是现场保护。 606行:一个赋值语句,保存的是当前的SP(栈指针),保存在当前任务的任务控制块里面 607行:也是赋值语句,修改当前的SP,将当前栈指针修改为定时器函数专用的栈 609~627行:先别管 629行:赋值语句,修改SP指针为下一个任务的SP指针,中断退出将按下一个任务的任务栈出栈,出栈时修改了PC指针,实现了现场还原和任务调度
至于为什么要这么做我也说明一下。任务调度最重要的就是现场保护和现场还原了。现场保护利用的是51产生中断时的入栈操作,这是C语言不用管也管不了的。现场还原是利用51退出中断的出栈操作,也是C语言所管不了的。虽然说不用管,但是大部分时候现场保护并不完全。我们需要将每个任务用到的寄存器都入栈。经调试发现,51入栈机制是,在中断服务程序有用到的寄存器就一定会入栈,PSW和PC是一定会入栈的。
为了现场保护完全,我在中断服务函数里面,用C语言把每个通用寄存器都访问了一遍(因为有一些通用寄存器没入栈保护),所以每个通用寄存器都入栈。其他像ACC,B寄存器等也入栈了。这些操作在606行之前完成的。要看汇编出来的代码才知道,所以说任务管理还是从汇编的角度去理解好一点。 然后现场还原就好办了。只要在中断服务函数退出前修改SP指针(629行),SP指针在中断服务函数就保存在相应任务的任务控制块了(606行),629行一个赋值语句的作用就是,中断会按照相应的任务栈出栈(在606行保存的SP的值),之后会还原寄存器和PC指针,实现了任务调度。只要修改了PC指针,程序就跳转到相应的位置执行,而PC指针在中断服务函数里面已将入栈保存起来了,出栈后就切换到上次该任务执行的断点位置,实现任务的继续。
能理解这些操作就好办了,简单的说就是定时器产生中断会引起入栈,退出中断会出栈,每个任务的任务栈不同,入栈由51中断的现场保护完成,出栈由51中断的现场还原完成。任务切换需要做的就仅仅是在退出中断前修改出栈的SP指针(栈指针),629行就完成这个功能。剩下的就是51的现场还原了。还原后就回去上次被中断的任务的断点处所,继续执行上次被中断的任务。
详细的任务管理将在下一章继续介绍。
第四章 任务管理详解
有了上一章的任务管理原理的基础,来理解整个中断响应函数的功能就容易多了。继续上一章的截图。(大概浏览一下代码,不到20行)
609~612行,624~627行。这两个实现相同的功能,为定时器赋初值,条件编译的条件为是否使能精确延时,条件编译结果是选择其中的一个。 在coos_conf.h文件里有: #define OS_ACRT_TIM_EN 0 也就是默认没有使能精确延时,如果需要使能精确延时的话只要将0改为1就行了。 说明一下65535 - OS_TICK_CNT + OS_ETR_TICK_TIM OS_TICK_CNT 是定时器计数个数,可配置(coos_conf.h),默认5000,分时时间片 OS_ETR_TICK_TIM 是定时器响应周期,也就是入栈所用时间,默认0x2A
使能精确延时的结果是,coos提供的时钟对外是比较精确的,例如做一个定时器时钟,可以比较精确的运行一段时间。但是每个任务执行的时间却减少了,因为中断响应函数的执行也是需要时间的,这对于分时任务管理来说是不公平的,因为任务调度的时间是不一定相同的,导致任务执行的时间也不一定相同。 没有使能精确延时,那么coos提供的延时默认是5ms + 任务调度的时间。分时任务管理的话,每个任务是公平的,执行的时间相同。但是coos提供的延时对外是不精确的。
接下来看一下619~621行。条件编译,条件是如果使能定时器钩子函数,在配置文件里是否使能(coos_conf.h)。 函数OS_Hook_Tick()在coos_hook.c文件,默认函数体为空,该函数由用户实现。
最后就剩下614~617行,三个函数了,一个一个来,都是比较好理解的。 先看最简单的617行,OS_ResSav();该函数访问通用寄存器,引起定时器中断函数执行前,通用寄存器入栈(中断服务函数访问到哪些通用寄存器,那些通用寄存器就会入栈),具体代码是: 540行:让res_ptr指向0x00,也就是51的工作寄存器组0的R0,这个还是从汇编的角度去看会好理解一点。51的256Byte的RAM,前8Byte * 4是四个工作寄存器组。 542~549行:访问通用寄存器,用下标的形式访问,自己给自己赋值,分别访问R0,R1…R7,实际寄存器值没改变,目的是要在定时器函数执行前将通用寄存器入栈。完善现场保护工作。
接下来看614行,OS_TickTimDeal(); 代码也是相当简单的,看截图 该函数处理coos的时间,由于是分时任务管理,当然要知道当前运行到哪个时间片,并且每个任务一旦运行了一个时间片,也要将时间片减一。 499行:当前任务时间片减一,也就是当前任务运行了一个时间片 501行:系统时间片加一,分时管理任务,当然要有一个变量(OS_Tick)记录当前时间片
502行:OS_TICK_PERIOD系统时间片周期,默认为20,判断是否满一个周期 504行:满一个时间片周期,时间片清零 505行:满一个时间片周期,置位系统周期标志位
508行:判断是否满一个系统周期,标志位是在505行置位的 510行:满一个系统周期,标志位清零 512行~518行:条件编译,如果使能系统提供参考时间,也就是延时时间才编译这几行 513行:系统时间加一 514行:如果时间满一个时间周期 516行:时间清零 520行:调用任务时间片初始化函数,重新刷新任务时间片,具体代码如下截图 477行:一个for循环,循环次数为任务个数,其中OS_TASK_NUM可配置,最大为8 479行:for循环的循环体,OS_TaskTimGrp数组记录任务时间片,这里是按照任务创建时的时间片去刷新 481~483行:条件编译,如果使能系统空闲任务。系统空闲任务是指在一个系统周期内(例如20个时间片内),如果没有可运行任务(只需要执行不到20个时间片的任务),也就是创建的各个任务时间片已经执行完,但是一个周期还没到,就执行空闲任务。coos默认提供的空闲任务是让51进入低功耗模式,省电。当然可配置,用户可以自行创建空闲任务。 482行:刷新空闲任务时间片
任务调度还剩下最后一个函数,继续任务调度那张截图: 看615行最后一个函数:目的是获取下一个需要执行的任务,为切换任务做准备 具体函数代码如下截图:
该函数的思路是:在任务状态表上查找下一个任务。任务状态表是579行的OS_TaskStatGrp,是一个8bit的变量,每一位为0或者为1代表是否创建了任务,1表示创建了任务,当然最多支持8个任务。 查找到有创建的任务,然后判断该任务时间片是否为0,如果不为0,那么下一个任务就是被查找到的任务,如果为0,那么继续查找下一个任务。如果所有被创建的任务时间片都为0,那么返回OS_TASK_NUM,代表着下一个任务执行的是空闲任务。 567行:为id赋初值,为当前执行任务的id 568行:i初值为0,循环控制用 570行:进入循环,循环次数为coos管理任务的个数,由OS_TASK_NUM决定,可配置 572行:进入了循环,循环次数加一 573行:id加一,表示下一个任务 574行:判断id是否溢出,超出coos管理任务的个数 676行:id溢出则清零 579行:重点!!判断该id的任务是否需要执行,判断的条件有两个,分别是: 1、在该id上创建了任务 2、该任务时间片不为0
前半部分:(OS_TaskStatGrp & SET_BIT(id)) OS_TaskStatGrp是coos任务状态表,8bit,每一位代表一个任务,为1表示创建了任务 SET_BIT()是一个宏操作,代码是 #define SET_BIT(i) (1 << i) 表示置位某个位,这里SET_BIT(id)是置位id那一位 操作(OS_TaskStatGrp & SET_BIT(id)表示判断在任务状态表上判断id那一位是否创建了任务 后半部分:OS_TaskTimGrp[id] > 0 判断任务时间片数组上该任务时间片是否为0 如果满足前半部分和后半部分都为真,那么就需要切换到的任务就是临时变量id了。 所以就有: return id;
当然如果不能同时满足两个条件,就 return OS_TASK_NUM; 表示下一个任务是空闲任务。
至此,任务调度讲解是完了,不过还是再总结一下: 1、 进入任务调度函数前(定时器中断服务函数),有C语言看不见的入栈操作,保存了寄存器和PC指针等 2、 入栈后就进入任务调度函数,第一步保存当前任务的SP在相应任务的任务控制块里面 3、 将SP修改为任务调度专用的SP,任务调度函数专用的栈 4、 系统时间处理,分时管理需要处理时间 5、 获取下一个需要执行的任务 6、 访问通用寄存器,为了第一步入栈操作所有通用寄存器都入栈 7、 任务调度的钩子函数(如果使能) 8、 定时器计时赋初值 9、 修改SP为下一个任务的SP,在相应的任务控制块里面读取(第2步保存) 10、退出任务调度函数,按照下一个任务的SP出栈,还原寄存器,PC指针等,实现了任务调度
到这里其实还遗留一个问题:切换到的任务如果还没被执行过的话(定时器中断入栈是入栈被中断的任务,说明该任务已经执行了),那么相应的PC指针,寄存器值是什么?下一章继续介绍。
第五章 任务管理的幕后操作
来到这里,可以看到,要实现任务管理功能,还需要很多幕后操作的。这次我们实际来看任务管理从头到尾怎么做的,要开始来弄清楚来龙去脉了。这一章,需要分几个小节了。 5、1 coos的文件结构和使用
看看coos的C语言工程吧。截图:
先看左边的文件,两个文件夹coos和user。 coos里面两个源文件coos.c和coos_hook.c。这两个文件是任务管理文件,只要包含了这两个文件和相应的头文件,再调用几个函数就可以实现分时任务管理功能了。 user里面两个源文件是用户自己编写的。main.c和task.c分别是主函数文件和被管理任务的文件。任务到底是什么,等一下再看。这里先介绍主函数应该怎么样写,也就是前面啰嗦了那么多,那到底coos怎么用?
要先添加了coos的几个文件到自己的工程,然后大概浏览一下那10来行代码。可以看到主函数的操作分成三部分: 1、初始化coos 2、创建任务 3、开始运行coos
具体再看代码:
12~14行:定义三个数组,用来做任务栈。可以看到该主函数创建了三个任务,因为每个任务要有一个任务栈,所以定义了三个数组来当作任务栈。 16行:主函数开始入口。 18行:coos初始化。调用coos的函数,一些coos运行必要的操作,等一下再说。 20~22行:创建任务。也是调用coos的函数,创建任务后准备交给coos管理。 24行:coos开始运行。同样是coos的函数,coos开始分时管理被创建的三个任务。
可以看到,要使用来管理任务是非常简单的,主要在主函数里面调用几个coos提供的函数就行了。没错,但是这次我们要看coos是怎样运行的,了解到具体的每一步。只要理解coos几个重要的函数,就能很深入地理解coos了。
5、2 coos重要函数说明
1、void OS_Tick_Init(); coos 定时器初始化函数,跟普通的51定时器初始化函数没什么不同,但也看一下具体代码。功能是配置定时器0,让51能够产生中断,相当好理解。 其中457行的TR0 = 1;被我注释掉是因为我们并不是要在这个时候让coos开始运行,等到创建好任务在让它开始运行。
2、void OS_TaskCreate(void Task(void), uint8_t *TaskStk, uint8_t TaskTim, uint8_t TaskID) coos创建任务的函数,非常重要!!
可以说coos得以运行都是这个函数和定时器中断函数在幕后的操作了,这个函数有这跟定时器中断函数同样级别的重要性!!并且coos的很多概念都在这个函数里面使用到。
先介绍一下一些基本概念,再来说明任务创建函数。
①在C语言中,函数名代表函数的地址。什么意思?举个例子说明一下: 比如有一个函数:void Func(); Func是函数名, Func()是这个函数。 要知道它们是有区别的。 首先我们可以把Func当成一个常量,这个常量是一个地址,也就是Func()在ROM存放的地址,ROM在51上我们称为code。这些属于C语言的知识,大家有兴趣在继续深入了解。我们这里要用到的只是要知道Func就是一个常量,这个常量是一个地址,这个地址是函数Func()在ROM上的地址。就行了。
但是它有什么用?如果跟PC指针联系起来的话就知道了解这个是有作用了。 PC指针存放的是下一条指令的地址,如果把Func赋值给PC,那么CPU下一个执行的指令就是Func()函数的指令了,类似于C语言的函数调用,调用的函数是Func()。其实调用函数的时候就是将PC指针修改成对应函数的地址,CPU就跳转去执行相应的函数了。但是调用函数的话还涉及到函数参数的传递和函数的返回值,与只修改PC指针也是有区别的。
②任务控制块 先看代码截图: 要管理任务,当然要有任务的信息。任务控制块就是用来保存任务信息的。因为51的RAM只有256Byte,所以任务控制块我也尽量减小,只有任务栈和任务时间片是必要的。
任务栈地址:每个任务要有自己的栈,任务栈的地址用来记录任务进入中断的SP的值(栈指针),方便中断退出按该任务栈出栈实现现场还原。在第二章讲任务管理原理时,进入中断的第一件事就是保存任务的SP到任务控制块的栈地址,也就是606行的赋值语句。 任务时间片:每个任务还要有时间片。也就是任务在一个周期内最多执行多少个时间片。因为是分时任务管理,所以不能让某个任务占用所有CPU,要按照时间片去执行。
接下来看任务创建函数,截图:
函数带四个参数,每个参数都相当重要。 第一个参数: void Task(void) --- 这个参数是一个函数,可能有点难以理解。但是函数的参数可以是一个函数,这个参数函数不带参数,不带返回值。这个参数函数就是我们用C语言写的任务函数。也就是任务创建函数创建的任务就放在这个参数。 第二个参数: uint8_t *TaskStk --- 这个参数是任务栈首地址。如果我们定义一个数组,那么数组名就是数组的首地址,一般这个参数传递的是数组名(数组的首地址),然后数组就作为该任务的任务栈。(当然动态分配内存也行) 第三个参数: uint8_t TaskTim --- 这个参数是任务的时间片。比较容易理解了,一个任务在一个周期内最多运行多少个时间片就是看创建任务时传递进来的这个参数。 第四个参数: uint8_t TaskID --- 任务的id。coos管理任务是根据任务的id管理的,只有任务挂在coos任务状态表(OS_TaskStatGrp)上coos才管理这个任务,不然任务是不会被运行的,哪怕有再多的时间片。并且coos的任务状态表是一个8bit的变量,每一位代表一个任务id。任务的id分别是从低位的0到高位的7。
开始看具体的函数代码,现在应该很好理解了。 099行:目的是判断该id上是否已经创建了任务。判断条件是任务状态表该为是否为1。其中操作SET_BIT()是一个宏操作。在coos.h里面有: #define SET_BIT(i) (1 << i) 目的是置位某个位,操作(SET_BIT(TaskID))是置位id那一位,然后跟OS_TaskStatGrp(任务状态表)与一下是否为真。如果为真,该id上已经创建了任务;如果不为真,该id上未创建任务。
如果任务id已被占用存在,那么运行101~104行。 101~103行:条件编译,条件是是否使能系统错误统计,如果使能系统错误统计则运行OS_ErrCnt(); ,这个函数是当coos出错时候统计错误次数的,这里不是重点。 104行:return,函数返回。因为不能创建任务(任务id被占用了)
如果任务id未被占用,则运行108~118行: 108行:因为要在该id上创建任务,所以置位任务状态表在该id上的那一位
109行:记录任务的任务栈,也就是任务的SP。记录在对应任务的任务控制块里面。任务控制块是一个结构,任务控制块在上面已经说明了。我这里是定义一个结构数组,结构就是任务控制块结构,每个任务的任务控制块就是在该结构数组上的一个元素(结构数组的元素是一个结构)。
右边(uint8_t)TaskStk + OS_TASK_STK_SIZE_MIN有两部分。 第一部分(uint8_t)TaskStk 是任务创建函数的第二个参数,任务栈首地址,本来是一个指针,强制转换成uint8_t,无符号八位的变量 第二部分 OS_TASK_STK_SIZE_MIN 是任务栈的大小,是一个常量,默认值为13,这个有点难理解。因为与定时器中断服务函数相关,并且是从汇编的角度去看才能知道这个常量的大小。详细说明一下:中断服务函数执行前有入栈操作,但是具体入栈寄存器多少事先难以预知,最好的办法是查看编译后的汇编代码。我就是查看汇编代码的,默认总共入栈的寄存器有13个,占用14个字节(PC指针占用两个字节,其他寄存器占用一个字节)。除了PC指针还有额外的12个字节,分别是R0~R7,ACC,DP0H,DP0L,PSW。还需要了解的是51入栈机制,栈是向高地址增长的,而且是先增长再入栈(貌似专业一点讲是一个是满栈,UB,U是up,B是before,要入栈前先增长栈地址)。 了解了这些之后就可以推断出 OS_TASK_STK_SIZE_MIN 这个常量是13了,当然我也顺便说一下。总共入栈14个字节,数组名是首地址,数组名加上13就是栈顶了,总共是14个字节,这样就模仿得跟中断入栈后的栈顶的值是一样的。
左边OS_TaskTcbGrp[TaskID].TaskStk 是该任务的任务栈,接受右边的栈顶地址。
110行:右边是任务创建函数的第三个参数,任务时间片,在任务控制块上记录任务时间片。 112~113行也是重点和难点!! 赋值语句,模拟入栈。要先知道PC指针是16bit的,入栈占用两个字节。但是究竟是高字节先入栈还是低字节先入栈?有办法!进入调试模式,然后观察内存。我是用这个办法知道51入栈PC指针是低地址先入栈,然后高地址的。大家有兴趣可以自行尝试。 知道了先入栈低地址,下一步是模拟出入栈的是任务刚要运行时的PC指针。上面已经讲到函数名就是函数的地址,与PC指针有联系。 112行: TaskStk[0] = FUNC_ADDR_LOW(Task); 右边是一个宏操作,在coos.h里面有: #define FUNC_ADDR_LOW(func) (uint8_t)(((uint32_t)func) >> 0) 目的是获取函数的低地址,宏操作的参数是func,具体应用时候是一个函数名,也就是一个常量(函数的地址),操作先将函数的地址func强制转换成一个32bit的变量,右移0是为了配合获取函数高地址操作写的,实际没用,然后再强制转换成8bit的变量。 左边是任务栈,用下标形式访问栈底,将函数的低地址赋值给栈底,模拟了PC指针入栈时的低八位地址入栈。 113行:TaskStk[1] = FUNC_ADDR_HIGH(Task) 理解了112行,这一行就好办了。模拟入栈PC指针的高八位。 右边一样是一个宏操作,在coos.h里面有: #define FUNC_ADDR_HIGH(func) (uint8_t)(((uint32_t)func) >> 8) 目的是获取函数func的高八位地址。 左边任务栈,因为刚才入栈函数的低八位地址,所以栈要向上增长,用下标访问的形式就是 TaskStk[1]了,这一行模拟出PC指针入栈是的高八位地址入栈。
115~118行:模拟其它寄存器入栈,默认都是0x00,其他寄存器包含有R0~R7,ACC,DP0H,DP0L,PSW具体要看汇编代码才知道是这些寄存器的。
至此任务创建函数就完了。这里在总结一下,在coos里要创建一个任务就是要将任务挂在任务状态表上,供coos管理,而且要填好任务的信息,包括任务栈顶,任务时间片。最后模拟任务被中断服务函数中断了的入栈操作。具体再分几步说明: 1、判断任务id是否被占用 2、如果任务id被占用就不创建任务,根据条件编译是否运行系统错误统计函数
3、任务id没有被占用,置位coos任务状态表对应id那一位,说明要在该id创建任务 4、为任务栈的栈顶初始值 5、任务时间片初始化 6、模拟入栈PC指针和其他寄存器。
3、void OS_TaskTim_Init(void) 因为coos管理任务是按时间片管理的,任务一旦运行一个时间片,时间片就被减一。所以每个周期要刷新一次任务时间片。这个函数就是每个周期调用一次的任务时间片初始化,重新刷新任务时间片共coos管理。 477行:for循环,循环次数是coos管理的任务个数 479行:for的循环体,功能是根据任务时间片初始化任务时间片数组,就是记录各个任务创建时的时间片,创建一个副本。任务运行后的时间片减一操作在这个副本进行的。 481~483行:条件编译,条件是如果使能系统提供空闲任务。如果使能了系统提供空闲任务,那么就会刷新空闲任务的时间片。其中常量OS_IDLE_TASK_TIM的值与coos一个周期的时间片个数相等。在coos_conf.h里面有: #define OS_IDLE_TASK_TIM OS_TICK_PERIOD 其中后半部分OS_TICK_PERIOD是coos一个周期时间片的个数。
理解了这三个函数后就可以来看看coos具体的运行过程了。现在开始要具体从main函数开始看coos的任务管理了。
5、3 从main函数的执行理解coos的运行
再来main函数的截图吧: 从现在开始一步一步分析main函数。
1、OS_Init();
进入这个函数继续分析。看该函数截图: 062行:OS_Tick_Init();代码上节已经分析过,重温一下,看截图: 064行:变量OS_CurTaskID记录当前是执行那个任务,初始化将它初始化为空闲任务。 065行:变量OS_TaskStatGrp是coos任务状态表,初始化为0表示没有创建任务。 067行:for循环,循环次数是coos管理任务的个数,也就是常量OS_TASK_NUM,可配置 069~070行:for循环的循环体,将coos管理的任务的任务控制块任务栈初始化为0x00,时间片初始化为0 073~075行:条件编译,两个条件。分别是 1、如果使能coos空闲任务,可以不使能 2、如果使能coos提供的空闲任务,当然可以自行创建其他任务作为coos空闲任务 coos默认提供的空闲任务是让51进入低功耗模式,省电。此时51等待中断唤醒。具体可以查找51数据手册继续深入理解,代码看截图: 到了这里,主函数的第一个函数就完了,接下来继续看主函数的其他操作。
2、OS_TaskCreate(Task_One, Stk_TaskOne, 5, 0);
重温一下任务创建函数,截图:
这个函数在上一节也已经说明过,再罗嗦一下。 该函数的操作是创建一个名字为“Task_One”的任务,其中Task_One是一个函数名,也就是有一个Task_One()的函数,具体任务做什么等一下再说。 任务 Task_One的任务栈是Stk_TaskOne,也就是主函数文件中012行定义的那个数组,将数组的首地址传递进来,这个数组的空间给Task_One做任务栈。 任务Task_One的时间片为5,也就是Task_One一个周期最多执行5个时间片 任务Task_One的id为0,也就是Task_One在任务状态表OS_TaskStatGrp占用了第0位
Task_One的具体代码再看一下,相当简单的一个任务。截图: 说明一下任务必须在一个死循环里面,即时任务只需执行一次,也必须让任务在死循环里面执行,不能让任务函数返回。因为返回的话,有发生不可预知的结果,一般就是程序跑飞了。因为返回时谁也不知PC指针会指向哪里。 如果有只需执行一次的任务的话,coos当然考虑了这个情况。那就是在任务运行完后调用coos的任务删除函数void OS_TaskDel(uint8_t TaskID);将该任务删除掉,coos就再也不会执行该任务了。参数TaskID是待删除任务的id。比如要删除刚才创建的任务的话就调用: OS_TaskDel(0); 就将任务删除了。 3、OS_TaskCreate(Task_Two, Stk_TaskTwo, 2, 1);和 OS_TaskCreate(Task_Thr, Stk_TaskThr, 7, 3); 再创建两个任务,类似与创建任务1。 任务代码也很简单,截图: 功能类似与任务一。任务二中被注释的内容是我在写coos时测试函数功能是否正常用的。 创建了任务之后,我们要让coos开始运行了。继续看mian函数中的最后一个函数。
3、OS_Start(); 代码在截图:
135行:coos任务时间片初始化,该函数上面已经上一节也已经说过,重温一下代码: 因为创建任务的时候在任务控制块里面填写了任务的时间片。然后我们想让coos管理这些任务,我就创建了一个数组,作为任务时间片的副本。因为任务运行后我们要记录已经运行了,直接在任务控制块里面操作会造成任务信息丢失,所以创建一个副本。在coos.c里面有: static uint8_t idata OS_TaskTimGrp[OS_TASK_NUM + 1]; 用这个数组来记录任务时间片,也就是运行了任务就来一次任务时间片减一操作,满一个周期后再重新刷新。任务时间片为0任务就不会被执行。
138行:因为coos运行前默认coos是在运行空闲任务,第一次进入中断服务函数(任务调度函数),会将相应任务的时间片减一(第一次进入中断空闲任务时间片会被减一),所以先加一,解决第一次进行任务调度出现的bug 138行:因为第一次进入中断,OS_Tick会加一(coos时间片记录函数),表示中断了一个任务,coos运行了一个时间片,但实际第一次进入中断并没有运行一个时间片的任务,所以OS_Tick减一,解决第一次进行任务调度的bug
140行:表示定时器开始计时。 141行:表示定时器产生溢出,实际没溢出,只是为了立刻进入中断。 此时CPU的不会继续执行141行下面的代码了,它会进入定时器0中断服务函数,但是下面的代码还是有可能被执行的。 143~147行:条件编译,如果使能空闲任务和coos提供的空闲任务 空闲任务放在这里,是因为当没有可运行任务的时候,coos任务调度返回,141行下面的代码。为什么,可以自行思考一下。原因是141行是第一次被中断的断点,然后进行任务调度。任务调度函数会记录断点,入栈保护,并且coos初始化是默认的任务是空闲任务,所以入栈保护入的栈是空闲任务的栈。如果将空闲任务放在141行下面,那么就模仿的非常的像是第一次中断断点就是空闲任务了。
至此,coos任务调度讲解完毕。当然要看代码,要调试,要看一下汇编代码,要看51的说明书,要懂一点微机原理,要有C语言基础,才能更深刻的了解coos。 简单总结: 1、初始化定时器,coos使用的变量。为coos分时管理任务做准备 2、创建任务,准备让coos管理(创建任务是记录任务信息,并模拟任务被中断函数打断) 3、开始运行coos,启用定时器中断根据任务信息调度任务
当然为了让coos更强大,并且作为嵌入式的东西,可裁剪是必不可少的。所以很多条件编译默认都是不编译的,下一章主要说一下条件编译函数。
以下已经不是重点了,可有可无。coos的主要目的是任务管理。并且发现分时任务管理比较简单实现,所以就是分时任务管理的coos了。
第六章 coos条件编译
数了一下是11个条件编译函数,都是相当简单或者说理所当然会有这些函数,没有多少技术含量了这些。下面一一说明:
1、void OS_TaskDel(uint8_t TaskID) 任务删除函数,看截图: 169行:传递进来的参数是任务的id 171行:判断任务是否创建了,因为创建任务函数默认已经置位了任务状态表(OS_TaskStatGrp)对应的id那一位,所以如果对应的为1则说明确实创建了任务,准备删除任务。 173行:目的是将任务状态表(OS_TaskStatGrp)对应任务id那一位清零。其中RESET_BIT()是一个宏操作,在coos.h文件里面有: #define RESET_BIT(i) (~(1 << i)) 目的是清零对应i的那一位,最重要是这一步就把任务删除了,任务就再也不会运行了,下面的步骤只能说是善后吧。 174~175行:任务栈置为0x00,时间片置零 176行:任务时间片数组(OS_TaskTimGrp)对应任务id那个元素的时间片清零
如果任务状态表(OS_TaskStatGrp)对应任务id那一位为零,也就是对应id上没有创建任务,根据条件编译是否运行系统错误统计函数,这里不是重点。
2、void OS_IdleTask(void) coos提供的空闲任务,老样子,截图: 跟普通任务一样是进入一个while死循环,因为一旦函数返回,会有意想不到的结果反生,难以预测发生什么事,程序跑飞了那是不可避免的。 204行:根据51数据手册,置位PCON的第0位让51进入低功耗,具体可参考数据手册
3、uint8_t OS_GetCurTaskID(void) 获取当前运行任务id的函数,截图: 直接将OS_CurTaskID返回,该变量记录当前被运行的任务 来到这里相信同学们也觉得条件编译函数是相当的…继续
4、uint8_t OS_GetCurTickTim(void) 获取coos提供的参考时间,截图: 类似与上一个函数,不说了。
5、void OS_SetCurTickTim(uint8_t TickTim) 设置coos当前参考时间,截图: 对OS_Tim赋值,就是设置coos的参考时间了
6、void OS_TaskExit(void) 例如当前正在运行某个任务,我们已经不想再让任务运行下去了,就可以调用这个函数,进入任务调度函数,让任务调度函数判断下一个任务是什么而去执行下一个任务,当前任务就没有被执行了。 这个时候任务这个函数的价值就体现出来了。因为我们的任务都必须设置成在一个死循环里面执行,如果没有这个函数,那么coos必须也让该任务运行一个时间片,如果在这个时间片内引脚的状态都为0,那么可想而知,该任务是在浪费CPU。如果我们改进一下: 如果状态为0,则一样运行一段代码,如果引脚状态为1,调用该函数,让任务放弃CPU。 那么CPU的利用率就大大提高了,下面看代码。截图: 282行:判断任务是否存在 284行:任务确实存在,置位TF0,定时器0溢出标志,则进入定时器中断函数进行任务调度。 289行:任务并不存在,根据条件编译是否运行coos错误统计函数(这里不是重点)
7、void OS_TaskTimEdit(uint8_t TaskID, uint8_t TaskTim) 任务时间片修改函数:截图 因为创建任务的时候我们指定了任务的时间片,coos管理任务后任务的CPU使用率是一定的,如果要增加或减少任务的CPU使用率可以试着用这个函数。 传递进来的参数: 1、uint8_t TaskID 任务的id 2、uint8_t TaskTim 任务的新时间片 当然311行先判断任务是否存在 任务存在则修改任务控制块数组中该任务的任务控制块的时间片为新设置的时间片 任务不存在根据条件是否编译运行coos错误统计函数
8、void OS_ErrCntClr(void) coos错误次数清零函数。截图: 条件编译有两个条件: 1、使能coos的错误统计 2、使能错误统计清零 340行:简单的将OS_Err_Cnt赋值为0就清零了
9、uint8_t OS_GetTick(void) 获取coos当前时间片,截图: 360行:直接将OS_Tick返回。
10、void OS_TimDly(uint8_t Tim) coos提供延时函数,截图: 稍微有一点点复杂,不过说一下基本思路应该就很清晰了。 延时的话是根据coos提供的参考时间作为延时依据的,所以要根据延时的时间和coos提供的参考时间,计算出延时的终点(延时的结束条件),在延时的条件内就等待。 384行:临时保存coos参考时间变量OS_Tim 385行:Tim是传递进来的参数,加上OS_Tim是延时的终点,但要判断是否溢出 387行:判断延时终点是否溢出,溢出条件是Tim 〉 OS_TIM_PERIOD(coos参考时间的周期,可配置) 389行:延时终点溢出了,用coos的参考时间周期取余Tim,得到新的延时终点。 其实由这里也可以看出,coos提供的延时范围实在一个coos参考时间周期内的,因为越界部分取余后已经没了。 390行:进入while等待延时结束。因为延时终点越界了,可知等待的终点coos提供的OS_Tim是小于当前临时保存的coos参考时间tim_tmp,所以等待条件第一个为: OS_Tim >= tim_tmp 因为延时终点溢出也可知延时终点coos的参考时间OS_Tim大于计算出的延时终点Tim(Tim是计算出的延时终点,第二个条件是只要coos参考时间大于该值则等待结束) ,所以有 OS_Tim < Tim
394行:延时终点为溢出,那么延时就好理解了,只要coos提供的参考时间大于计算出的延时终点,那么延时就结束了,有: while(OS_Tim <= Tim);
如果对延时函数不理解的话拿起笔和纸写一下就懂了,我是用笔和纸计算出来的…(——#!)
11、void OS_ErrCnt(void) 最后一个条件编译函数,coos错误统计。虽然是个很简单的函数,不过因为没有更复杂的函数了,所以也只能拿它来压压轴,看截图: 415行:判断错误次数是否溢出,未溢出进入417~420行 417行:错误次数未溢出,错误次数加一 418~420行:条件编译,条件是是否使能coos错误统计钩子函数。什么是钩子函数,了解过ucos的同学们就知道,为了增强ucos的功能,ucos作者为ucos引入10个钩子函数,是在当ucos发生某些情况时候运行的函数,由用户实现的。我这里也是模仿ucos的,引入钩子函数OS_Hook_Errr(),在coos_hook.c文件里面,默认函数体为空,该函数又用户实现,目的是为了增强coos的功能,多一些自定义功能,其实也没什么大不了的。
422~428行:条件编译,条件是是否使能错误次数溢出钩子函数,进行错误次数溢出处理。 425行:错误次数清零 426行:错误次数溢出钩子函数,同样在coos_hook.c文件里面,默认函数体为空,由用户实现,目的是增强coos功能。
条件编译函数就完了,最后还剩下的就是coos的配置,裁剪了,下一章介绍。
第七章 coos的配置、裁剪、类型定义
当然作为嵌入式的东西很将就可裁剪性的,配置和裁剪在coos_conf.h文件里面。 先看看配置部分,截图: 一个一个说: 19行:coos定时器计数值,也就是coos一个时间片的长度。该值越大,coos任务调度的频率就越小,CPU的使用率就越高。但是太大的话任务被打算时间太长,所以要折衷配置。默认5000,晶振12M的时候一个任务时间片是5ms
20行:coos时间片周期,也就是多少个时间片为一个coos周期。一个周期后coos会重新刷新任务时间片。同样该值越大,CPU利用率越高,但这个值的影响是不明显的。
21行:coos参考时间的周期,也是coos提供延时的最大值。对CPU利用率几乎没影响。
22行:coos管理任务的个数。该值越小,CPU利用率越高,RAM占用越少。因为任务调度的时候遍历了每个任务,需要时间。并且每个任务都要有任务控制块(2Byte)和任务时间片副本(1Byte),占用RAM。综合说名一下就是,我们需要创建多少个任务,就定义该值为多少,不用预留余量之类的。
23行:定时器专用栈的栈大小,默认为8其实已经够用了,如果使能的定时器钩子函数,并且钩子函数过于复杂(运行起来占用栈较大),那么该值需要适当加大,因为栈溢出了程序运行是肯定会有问题的。
24行:coos空闲任务默认的时间片,该值与coos时间片周期大小一样。目的是为了在一个时间片周期内,如果没有一个可以运行的任务,那么所有时间片周期运行空闲任务(不会因为空闲任务时间片不够而发生意想不到的结果)
接下来是裁剪,老样子,截图: 裁剪也就是上一章的条件编译函数的使能,后面记录裁剪掉多少RAM和code 需要使用到上一章的条件编译函数就使能,1是使能 可以看到coos默认使用很少条件编译函数,因为都是比较简单的(就是上一章的内容),所以不说了。
钩子函数的配置,截图: 相信大家一看就懂了的,1使能。 coos钩子函数要用户自己实现(自定义的功能了),在coos_hook.c文件里面,将自己的代码写进去就行了,当然要使能了才有用。看一下coos_hook.c这几个函数吧。 截图:
还有类型定义,看截图: uint8_t 是stm32的风格,因为我已经习惯了这种风格了,不该了,当然也在coos用了这种风格。
附录 coos的说明书也应该结束了吧。顺便提一下就是,实现coos任务管理功能也就那不到200行代码(除去条件编译这些可有可无的东西),但是也写了我一个月左右的时间了。因为确实也走了一下弯路。例如我想让任务调度独立成为一个函数,定时器中断只是负责篡改返回地址,但是却有寄存器没入栈保存完善的bug。这个bug曾让我一度想放弃,因为发现到不得不用汇编,我是大大的不想用汇编的。机器语言已经跟机器密切相关了,移植性大大减少了,同时新鲜性也没了。因为用汇编的话实现任务管理难度是不大的。 期间也上火,头痛,牙痛,在校医院看了两次,差点要放弃。 装上win8,安装了RVMDK,运行不了,用什么win7兼容啊,XP SP3兼容啊什么的,都不行。百度也找不到win8兼容的RVMDK,我崩溃了好几次。因为装一次和卸载一次都是比较花时间的… (RVMDK是什么?接近keil增强版吧,我就是用RVMDK写coos的,keil是RVMDK的前身,RVMDK是stm32,arm之类用的,当然51也行,支持很多微处理器)
参考文献: 挺多的,可以说看了不少书吧。能想到的就写出来吧 1、C和指针(C语言的书) 2、51单片机汇编实例(网上随便下载看看的) 3、c51中文书名keil c51(网上随便下载的) 4、微型计算机原理与接口技术(上课的课本…) 5、《嵌入式实时操作系统uCOS-II》(第二版) 6、其它的对coos贡献不大,有C语言的,数据结构的,算法的,stm32的 |