ImageVerifierCode 换一换
格式:DOC , 页数:22 ,大小:58KB ,
资源ID:2957468      下载积分:20 金币
快捷下载
登录下载
邮箱/手机:
温馨提示:
快捷下载时,用户名和密码都是您填写的邮箱或者手机号,方便查询和重复下载(系统自动生成)。 如填写123,账号就是123,密码也是123。
特别说明:
请自助下载,系统不会自动发送文件的哦; 如果您已付费,想二次下载,请登录后访问:我的下载记录
支付方式: 支付宝    微信支付   
验证码:   换一换

加入VIP,免费下载
 

温馨提示:由于个人手机设置不同,如果发现不能下载,请复制以下地址【https://www.docduoduo.com/d-2957468.html】到电脑端继续下载(重复下载不扣费)。

已注册用户请登录:
账号:
密码:
验证码:   换一换
  忘记密码?
三方登录: 微信登录   QQ登录   微博登录 

下载须知

1: 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。
2: 试题试卷类文档,如果标题没有明确说明有答案则都视为没有答案,请知晓。
3: 文件的所有权益归上传用户所有。
4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
5. 本站仅提供交流平台,并不能对任何下载内容负责。
6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

版权提示 | 免责声明

本文(给DIY超轻量级多任务操作系统.doc)为本站会员(dzzj200808)主动上传,道客多多仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。 若此文所含内容侵犯了您的版权或隐私,请立即通知道客多多(发送邮件至docduoduo@163.com或直接QQ联系客服),我们立即给予删除!

给DIY超轻量级多任务操作系统.doc

1、http:/ unsigned char task_id; /当前活动任务号 /任务切换函数(任务调度器) void task_switch() task_sptask_id = SP; if(+task_id = MAX_TASKS) task_id = 0; SP = task_sptask_id; /任务装入函数.将指定的函数(参数 1)装入指定(参数 2)的任务槽中 .如果该槽中原来就有任务,则原任务丢失,但系统本身不会发生错误. void task_load(unsigned int fn, unsigned char tid) task_sptid = task_stacktid

2、+ 1; task_stacktid0 = (unsigned int)fn task_stacktid1 = (unsigned int)fn 8; /从指定的任务开始运行任务调度.调用该宏后,将永不返回 . #define os_start(tid) task_id = tid,SP = task_sptid;return; /*=以下为测试代码=*/ void task1() static unsigned char i; while(1) i+; task_switch();/编译后在这里打上断点 void task2() static unsigned char j; while(1

3、) j+=2; task_switch();/编译后在这里打上断点 void main() /这里装载了两个任务,因此在定义MAX_TASKS 时也必须定义为 2 task_load(task1, 0);/将 task1 函数装入0 号槽 task_load(task2, 1);/将 task2 函数装入1 号槽 os_start(0); 这样一个简单的多任务系统虽然不能称得上真正的操作系统,但只要你了解了它的原理,就能轻易地将它扩展得非常强大,想知道要如何做吗? 一.什么是操作系统? 人脑比较容易接受“类比“ 这种表达方 式,我就用“公交系统“来类比“ 操作系统“吧. 当我们要解决一个问题的

4、时候,是用某种处理手段去完成它,这就是我们常说的“方法“, 计算机里叫“程序“(有时候也可以叫它“算法“). 以出行为例,当我们要从A 地走到 B地的时候,可以走着去,也可以飞着去,可以走直线,也可以绕弯路,只要能从 A地到 B 地,都叫作方法.这种从 A 地到 B 的需求,相当于计算机里的“任务“, 而实现从 A地到 B 地的方法,叫作“任务处理流程“ 很显然,这些走法中,并不是每种都合理,有些傻子都会采用的,有些是傻子都不采会用的.用计算机的话来说就是,有的任务处理流程好,有的任务处理流程好,有的处理流程差. 可以归纳出这么几种真正算得上方法的方法: 有些走法比较快速,适合于赶时间的人;有

5、些走法比较省事,适合于懒人; 有些走法比较便宜,适合于穷人. 用计算机的话说就是,有些省 CPU,有些流程简单,有些对系统资源要求低. 现在我们可以看到一个问题: 如果全世界所有的资源给你一个人用(单任务独占全部资源), 那最适合你需求的方法就是好方法.但事实上要外出的人很多,例如 10 个人(10 个任务), 却只有 1 辆车(1 套资源),这叫作“ 资源争用“. 如果每个人都要使用最适合他需求的方法,那司机就只好给他们一人跑一趟了,而在任一时刻里,车上只有一个乘客.这叫作“顺序执行“, 我们可以看到这种方法 对系统资源的浪费是严重的. 如果我们没有法力将 1台车变成 10 台车来送这 10

6、 个人,就只好制定一些机制和约定,让 1 台车看起来像 10台车,来解决这个问题的办法想必大家都知道,那就是制定公交线路. 最简单的办法是将所有旅客需要走的起点与终点串成一条线,车在这条线上开,乘客则自已决定上下车.这就是最简单的公交线路.它很差劲,但起码解决客人们对车争用.对应到计算机里,就是把所有任务的代码混在一起执行. 这样做既不优异雅,也没效率,于是司机想了个办法,把这些客户叫到一起商量,将所有客人出行的起点与终点罗列出来,统计这些线路的使用频度,然后制定出公交线路:有些路线可以合并起来成为一条线路,而那些不能合并的路线,则另行开辟行车车次,这叫作“任务定义“. 另外,对于人多路线,车

7、次排多点,时间上也优先安排,这叫作“任务优先级“. 经过这样的安排后,虽然仍只有一辆车,但运载能力却大多了.这套车次/路线的按排,就是一套“公交系统“.哈,知道什么叫操作系统了 吧? 它也就是这么样的一种约定. 操作系统: 我们先回过头归纳一下: 汽车 系统资源.主要指的是CPU,当然还有其它,比如内存,定时器,中断源等. 客户出行 任务 正在走的路线 进程 一个一个的运送旅客 顺序执行 同时运送所有旅客 多任务并行 按不同的使用频度制定路线并优先跑较繁忙的路线 任务优先级 计算机内有各种资源,单从硬件上说,就有 CPU,内存,定时器,中断源,I/O 端口等.而且还会派生出来很多软件资源,例如

8、消息池. 操作系统的存在,就是为了让这些资源能被合理地分配. 最后我们来总结一下,所谓操作系统,以我们目前权宜的理解就是:为“解决计算机资源争用而制定出的一种约定“.二.51 上的操作系统 对于一个操作系统来说,最重要的莫过于并行多任务.在这里要澄清一下,不要拿当年的 DOS来说事,时代不同了.况且当年 IBM 和小比尔着急将 PC 搬上市,所以才抄袭PLM(好象是叫这个名吧?记不太清) 搞了个今天看来很“粗制滥造“ 的 DOS 出来.看看当时真正的操作系统-UNIX,它还在纸上时就已经是多任务的了. 对于我们 PC 来说,要实现多任务并不是什么问题,但换到 MCU 却很头痛: 1.系统资源少

9、 在 PC 上 ,CPU 主频以 G 为单位 ,内存以 GB为单位,而 MCU 的主频通常只有十几 M,内存则是 Byts.在这么少的资源上同时运行多个任务,就意味着操 作系统必须尽可能的少占用硬件资源. 2.任务实时性要求高 PC 并不需 要太关心实时性,因为 PC 上几乎所有的实时任务都被专门的硬件所接管,例如所有的声卡网卡显示上都内置有 DSP 以及大量的缓存.CPU 只需坐在那里指手划脚告诉这些板卡如何应付实时信息就行了. 而 MCU 不同,实时信息是靠 CPU 来处理的,缓存也非常有限 ,甚至没有缓存.一旦信息到达,CPU 必须 在极短的时 间内响应,否则信息就会丢失. 就拿串口通信

10、来举例,在标准的 PC 架构里,巨大的内存允许将信息 保存足够长的时间.而对于MCU 来说内存 有限,例如 51 仅有 128 字节内存,还要扣除掉寄存器组占用掉的 832 个字节,所以通常都仅用几个字节来缓冲.当然,你可以将数据的接收与处理的过程合并,但对于一个操作系统来说,不推荐这么做. 假定以 115200bps 通信速率向 MCU传数据,则每个字节的传送时间约为 9uS,假定缓存为8 字节,则串口处理任务必须在70uS 内响应. 这两个问题都指向了同一种解决思路:操作系统必须轻量轻量再轻量,最好是不占资源(那当然是做梦啦). 可用于 MCU 的操作系 统很多,但适合 51(这里的 51

11、专指无扩展 内存的 51)几乎没有.前阵子见过一个“圈圈操作系 统“, 那是我所见 过的操作系统里最轻量 的,但仍有改进的余地. 很多人认为,51 根本不适合使用操作系统.其实我对这种说法并不完全接受,否则也没有这篇文章了. 我的看法是,51 不适合采用“通用操作系 统“. 所谓通用操作系统就是 ,不论你是什么样的应用需求,也不管你用什么芯片,只要你是 51,通通用同一个操作系统. 这种想法对于 PC 来说没问题,对于嵌入式来说也不错,对 AVR 来说还凑合,而对于 51这种“贫穷型“的 MCU 来说,不行. 怎样行?量体裁衣,现场根据需求构建一个操作系统出来! 看到这里,估计很多人要翻白眼了

12、,大体上两种: 1.操作系统那么复杂,说造就造,当自已是神了? 2.操作系统那么复杂,现场造一个会不会出 BUG? 哈哈,看清楚了?问题出在“ 复杂“上面,如果操作系统不复杂,问题不就解决了? 事实上,很多人对操作系统的理解是片面的,操作系统不一定要做得很复杂很全面,就算仅个多任务并行管理能力,你也可以称它操作系统. 只要你对多任务并行的原理有所了解,就不难现场写一个出来,而一旦你做到了这一点,为各任务间安排通信约定,使之发展成一个为你的应用系统量身定做的操作系统也就不难了. 为了加深对操作系统的理解,可以看一看这份 PPT,让你充分了解一个并行多任务是如何一步步从顺序流程演变过来的.里面还提

13、到了很多人都在用的“状态机“, 你会发现操作系统跟状态机从原理上其实是多么相似.会用状态机写程序,都能写出操作系统.三.我的第一个操作系统 直接进入主题,先贴一个操作系统的示范出来.大家可以看到,原来操作系统可以做得么简单. 当然,这里要申明一下,这玩意儿其实算不上真正的操作系统,它除了并行多任务并行外根本没有别的功能.但凡事都从简单开始,搞懂了它,就能根据应用需求,将它扩展成一个真正的操作系统. 好了,代码来了. 将下面的代码直接放到KEIL 里编译,在每个 task?()函数的“task_ switch();“那里打上断点,就可以看到它们的确是“同时“ 在执行的 . #include #d

14、efine MAX_TASKS 2 /任务槽个数.必须和实际任务数一至 #define MAX_TASK_DEP 12 /最大栈深.最低不得少于 2 个,保守值为 12. unsigned char idata task_stackMAX_TASKSMAX_TASK_DEP;/任务堆栈. unsigned char task_id; /当前活动任务号 /任务切换函数(任务调度器) void task_switch() task_sptask_id = SP; if(+task_id = MAX_TASKS) task_id = 0; SP = task_sptask_id; /任务装入函数.将

15、指定的函数(参数 1)装入指定(参数 2)的任务槽中 .如果该槽中原来就有任务,则原任务丢失,但系统本身不会发生错误. void task_load(unsigned int fn, unsigned char tid) task_sptid = task_stacktid + 1; task_stacktid0 = (unsigned int)fn task_stacktid1 = (unsigned int)fn 8; /从指定的任务开始运行任务调度.调用该宏后,将永不返回 . #define os_start(tid) task_id = tid,SP = task_sptid;retu

16、rn; /*=以下为测试代码=*/ void task1() static unsigned char i; while(1) i+; task_switch();/编译后在这里打上断点 void task2() static unsigned char j; while(1) j+=2; task_switch();/编译后在这里打上断点 void main() /这里装载了两个任务,因此在定义MAX_TASKS 时也必须定义为 2 task_load(task1, 0);/将 task1 函数装入0 号槽 task_load(task2, 1);/将 task2 函数装入1 号槽 os_s

17、tart(0); 限于篇幅我已经将代码作了简化,并删掉了大部分注释,大家可以直接下载源码包,里面完整的注解,并带 KEIL 工程文件,断点也打好了,直接按 ctrl+f5 就行了. 现在来看看这个多任务系统的原理: 这个多任务系统准确来说,叫作“协同式多任务“. 所谓“协同式“,指的是当一个任务持续 运行而不释放资源时 ,其它任务是没有任何机会和方式获得运行机会,除非该任务主动释放 CPU. 在本例里,释放 CPU是靠 task_switch()来完成的.task_switc h()函数是一个很特殊的函数,我们可以称它为“任务切换器 “. 要清楚任务是如何切换的,首先要回顾一下堆栈的相关知识.

18、 有个很简单的问题,因为它太简单了,所以相信大家都没留意过: 我们知道,不论是 CALL 还是 JMP,都是将当前的程序流打断,请问 CALL 和 JMP的区别是什么? 你会说:CALL 可以 RET,JMP 不行.没错,但原因是啥呢?为啥 CALL 过去的就可以用 RET 跳回来,JMP 过去的就不能用RET 来跳回呢? 很显然,CALL 通过某种方法保存了打断前的某些信息,而在返回断点前执行的RET 指令,就是用于取回这些信息. 不用多说,大家都知道,“某些信息 “就是 PC 指针,而“ 某种方法“就是压栈. 很幸运,在 51 里,堆栈及堆栈指针都是可被任意修改的,只要你不怕死.那么假如在

19、执行 RET前将堆栈修改一下会如何?往下看: 当程序执行CALL 后,在子程序里将堆栈刚才压入的断点地址清除掉,并将一个函数的地址压入,那么执行完RET 后,程序就跳到这个函数去了. 事实上,只要我们在RET 前将堆栈改掉,就能将程序跳到任务地方去,而不限于 CALL 里压入的地址. 重点来了 首先我们得为每个任务单独开一块内存,这块内存专用于作为对应的任务的堆栈,想将 CPU交给哪个任务,只需将栈指针指向谁内存块就行了. 接下来我们构造一个这样的函数: 当任务调用该函数时,将当前的堆栈指针保存一个变量里,并换上另一个任务的堆栈指针.这就是任务调度器了. OK 了,现在我们只要正确的填充好这几

20、个堆栈的原始内容 ,再调用这个函数,这个任务调度就能运行起来了. 那么这几个堆栈里的原始内容是哪里来的呢?这就是“任务装载 “函数要干的事了. 在启动任务调度前将各个任务函数的入口地址放在上面所说的“任务专用的内存块“ 里就行了!对了,顺便说一下,这个“ 任务专用的 内存块“叫作“私栈“,私栈的意思就是说,每个任务的堆栈都是私有的,每个任务都有一个自已的堆栈. 话都说到这份上了,相信大家也明白要怎么做了: 1.分配若干个内存块,每个内存块为若干字节: 这里所说的“若干个内存 块“ 就是私栈 ,要想同时运行几少个任 务就得分配多少块.而“每个子内存块若干字节“就是栈深.记住 ,每调一层子程序需要

21、 2字节.如果不考虑中断,4 层调用深度,也就是 8 字节栈深应该差不多了. unsigned char idata task_stackMAX_TASKSMAX_TASK_DEP 当然,还有件事不能忘,就是堆指针的保存处.不然光有堆栈怎么知道应该从哪个地址取数据啊 unsigned char idata task_spMAX_TASKS 上面两项用于装任务信息的区域,我们给它个概念叫“任务槽“. 有些人叫它“任务堆“,我觉得还是“ 槽“ 比较直观 对了,还有任务号.不然怎么知道当前运行的是哪个任务呢? unsigned char task_id 当前运行存放在 1 号槽的任务时,这个值就是1

22、,运行 2 号槽的任务时,这个值就是2 2.构造任务调度函函数: void task_switch() task_sptask_id = SP;/保存当前任 务的栈指针 if(+task_id = MAX_TASKS)/任务号切换到下一个任务 task_id = 0; SP = task_sptask_id;/将系统的栈 指针指向下个任务的私 栈. 3.装载任务: 将各任务的函数地址的低字节和高字节分别入在 task_stack任务号0和 task_stack任务号1 中: 为了便于使用,写一个函数: task_load(函数名, 任务号) void task_load(unsigned in

23、t fn, unsigned char tid) task_sptid = task_stacktid + 1; task_stacktid0 = (unsigned int)fn task_stacktid1 = (unsigned int)fn 8; 4.启动任务调度器: 将栈指针指向任意一个任务的私栈,执行 RET指令.注意,这可很有学问的哦,没玩过堆栈的人脑子有点转不弯:这一 RET,RET 到哪去了?嘿嘿,别忘了在 RET 前已经将堆栈指针指向一个函数的入口了.你别把 RET 看成 RET,你把它看成是另一种类型的 JMP就好理解了. SP = task_sp任务号; return;

24、 做完这 4 件事后,任务“并行“ 执行就开始 了.你可以象写普通函数一 个写任务函数,只需(目前可以这么说)注意在适当的时候 (例如以前调延时的地方)调用一下 task_switch(),以让出 CPU控制权给别的任务就行了. 最后说下效率问题. 这个多任务系统的开销是每次切换消耗 20 个机器周期(CALL 和RET 都算在内了),贵吗?不算贵,对于很多用状态机方式实现的多任务系统来说,其实效率还没这么高- case switch 和if()可不像你想像中那么便宜. 关于内存的消耗我要说的是,当然不能否认这种多任务机制的确很占内存.但建议大家不要老盯着编译器下面的那行字“DATA = XX

25、Xbyte“.那个值没意义,堆栈没算进去.关于比较省内存多任务机制,我将来会说到. 概括来说,这个多任务系统适用于实时性要求较高而内存需求不大的应用场合,我在运行于36M 主频的 STC12C4052 上实测 了一把,切换一个任务不到 3 微秒. 下回我们讲讲用 KEIL 写多任务函数时要注意的事项. 下下回我们讲讲如何增强这个多任务系统,跑步进入操作系统时代.四.用 KEIL写多任务系统的技巧与注意事项 C51 编译器很多,KEIL 是其中比较流行的一种.我列出的所有例子都必须在 KEIL 中使用.为何?不是因为 KEIL 好所以用它(当然它的确很棒), 而是因为这里面用到了KEIL 的一些

26、特性,如果换到其它编译器下,通过编译的倒不是问题,但运行起来可能是堆栈错位,上下文丢失等各种要命的错误,因为每种编译器的特性并不相同.所以在这里先说清楚这一点. 但是,我开头已经说了,这套帖子的主要目的是阐述原理,只要你能把这几个例子消化掉,那么也能够自已动手写出适合其它编译器的 OS. 好了,说说 KEIL 的特性吧,先看下面的函数: sbit sigl = P17; void func1() register char data i; i = 5; do sigl = !sigl; while(-i); 你会说,这个函数没什么特别的嘛!呵呵,别着急,你将它编译了,然后展开汇编代码再看看:

27、193: void func1() 194: register char data i; 195: i = 5; C:0x00C3 7F05 MOV R7,#0x05 196: do 197: sigl = !sigl; C:0x00C5 B297 CPL sigl(0x90.7) 198: while(-i); C:0x00C7 DFFC DJNZ R7,C:00C5 199: C:0x00C9 22 RET 看清楚了没?这个函数里用到了 R7,却没有对 R7 进行保护 ! 有人会跳起来了:这有什么值 得奇怪的,因为上层函数里没用到R7 啊.呵呵,你说的没错,但只说对了一半:事实上,KEIL

28、 编译器里作了约定,在调子函数前会尽可能释放掉所有寄存器.通常性况下,除了中断函数外,其它函数里都可以任意修改所有寄存器而无需先压栈保护(其实并不是这样,但现在暂时这样认为,饭要一口一口吃嘛,我很快会说到的). 这个特性有什么用呢?有! 当我们调用任务切换函数时,要保护的对 象里可以把所有的寄存器排除掉了,就是说,只需要保护堆栈即可! 现在我们回过头来看看之前例子里的任务切换函数: void task_switch() task_sptask_id = SP;/保存当前任 务的栈指针 if(+task_id = MAX_TASKS)/任务号切换到下一个任务 task_id = 0; SP =

29、task_sptask_id;/将系统的栈 指针指向下个任务的私 栈. 看到没,一个寄存器也没保护,展开汇编看看,的确没保护寄存器. 好了,现在要给大家泼冷水了,看下面两个函数: void func1() register char data i; i = 5; do sigl = !sigl; while(-i); void func2() register char data i; i = 5; do func1(); while(-i); 父函数 fun2()里调用 func1(),展开汇编代码看看: 193: void func1() 194: register char data i

30、; 195: i = 5; C:0x00C3 7F05 MOV R7,#0x05 196: do 197: sigl = !sigl; C:0x00C5 B297 CPL sigl(0x90.7) 198: while(-i); C:0x00C7 DFFC DJNZ R7,C:00C5 199: C:0x00C9 22 RET 200: void func2() 201: register char data i; 202: i = 5; C:0x00CA 7E05 MOV R6,#0x05 203: do 204: func1(); C:0x00CC 11C3 ACALL func1(C:0

31、0C3) 205: while(-i); C:0x00CE DEFC DJNZ R6,C:00CC 206: C:0x00D0 22 RET 看清楚没?函数 func2()里的变量使用了寄存器R6, 而在 func1 和 func2 里都没保护. 听到这里,你可能又要跳一跳了:func1()里并没有用到 R6,干嘛要保护?没错,但编译器是怎么知道 func1()没用到 R6的呢? 是从调用关系里推测出来的. 一点都没错,KEIL 会根据函数间的直接调用关系为各函数分配寄存 器,既不用保护,又不会冲突,KEIL 好棒哦! 等一下,先别高兴,换到多任务的环境里再试试: void func1() r

32、egister char data i; i = 5; do sigl = !sigl; while(-i); void func2() register char data i; i = 5; do sigl = !sigl; while(-i); 展开汇编代码看看: 193: void func1() 194: register char data i; 195: i = 5; C:0x00C3 7F05 MOV R7,#0x05 196: do 197: sigl = !sigl; C:0x00C5 B297 CPL sigl(0x90.7) 198: while(-i); C:0x00

33、C7 DFFC DJNZ R7,C:00C5 199: C:0x00C9 22 RET 200: void func2() 201: register char data i; 202: i = 5; C:0x00CA 7F05 MOV R7,#0x05 203: do 204: sigl = !sigl; C:0x00CC B297 CPL sigl(0x90.7) 205: while(-i); C:0x00CE DFFC DJNZ R7,C:00CC 206: C:0x00D0 22 RET 看到了吧?哈哈,这回神仙也算不出来了.因为两个函数没有了直接调用的关系,所以编译器认为它们之间不

34、会产生冲突,结果分配了一对互相冲突的寄存器,当任务从 func1()切换到func2()时,func1() 中的寄存器内容就给破坏掉了.大家可以试着去编译一下下面的程序: sbit sigl = P17; void func1() register char data i; i = 5; do sigl = !sigl; task_switch(); while(-i); void func2() register char data i; i = 5; do sigl = !sigl; task_switch(); while(-i); 我们这里只是示例,所以仍可以通过手工分配不同的寄存器避

35、免寄存器冲突,但在真实的应用中,由于任务间的切换是非常随机的,我们无法预知某个时刻哪个寄存器不会冲突,所以分配不同寄存器的方法不可取.那么,要怎么办呢? 这样就行了: sbit sigl = P17; void func1() static char data i; while(1) i = 5; do sigl = !sigl; task_switch(); while(-i); void func2() static char data i; while(1) i = 5; do sigl = !sigl; task_switch(); while(-i); 将两个函数中的变量通通改成静态

36、就行了.还可以这么做: sbit sigl = P17; void func1() register char data i; while(1) i = 5; do sigl = !sigl; while(-i); task_switch(); void func2() register char data i; while(1) i = 5; do sigl = !sigl; while(-i); task_switch(); 即,在变量的作用域内不切换任务,等变量用完了,再切换任务.此时虽然两个任务仍然会互相破坏对方的寄存器内容,但对方已经不关心寄存器里的内容了. 以上所说的,就是“变量覆

37、盖“ 的问题 .现在我们系统地说说关 于“变量覆盖“. 变量分两种,一种是全局变量,一种是局部变量(在这里,寄存器变量算到局部变量里). 对于全局变量,每个变量都会分配到单独的地址. 而对于局部变量,KEIL 会做一个“覆盖优化“, 即没有直接调用关系的函数的变量共用空间.由于不是同时使用,所以不会冲突,这对内存小的 51 来说,是好事. 但现在我们进入多任务的世界了,这就意味着两个没有直接调用关系的函数其实是并列执行的,空间不能共用了.怎么办呢?一种笨办法是关掉覆盖优化功能.呵呵,的确很笨. 比较简单易行一个解决办法是,不关闭覆盖优化,但将那些在作用域内需要跨越任务(换句话说就是在变量用完前

38、会调用task_switch()函数的)变量通通改成静态(static)即可.这里要对初学者提一下,“静态 “你可以理解 为“全局“,因为它的地址空间一直 保留,但它又不是全局,它只能在定义它的那个花括号对里访问. 静态变量有个副作用,就是即使函数退出了,仍会占着内存.所以写任务函数的时候,尽量在变量作用域结束后才切换任务,除非这个变量的作用域很长(时间上长), 会影响到其它任务的实时性.只有在这种情况下才考虑在变量作用域内跨越任务,并将变量申明为静态. 事实上,只要编程思路比较清析,很少有变量需要跨越任务的.就是说,静态变量并不多. 说完了“覆盖“我们再说说“重入“. 所谓重入,就是一个函数

39、在同一时刻有两个不同的进程复本.对初学者来说可能不好理解,我举个例子吧: 有一个函数在主程序会被调用,在中断里也会被调用,假如正当在主程序里调用时,中断发生了,会发生什么情况? void func1() static char data i; i = 5; do sigl = !sigl; while(-i); 假定 func1()正执行到 i=3 时, 中断发生,一旦中断调用到 func1()时,i 的值就被破坏了,当中断结束后,i = 0. 以上说的是在传统的单任务系统中,所以重入的机率不是很大.但在多任务系统中,很容易发生重入,看下面的例子: void func1() delay();

40、void func2() delay(); void delay() static unsigned char i;/注意这里是申明为 static,不申明 static 的话会发生覆盖问题.而申明为static会发生重入问题.麻烦啊 for(i=0;i10;i+) task_switch(); 两个并行执行的任务都调用了 delay(),这就叫重入.问题在于重 入后的两个复本都依赖变量 i来控制循环,而该变量跨越了任务,这样,两个任务都会修改 i 值了. 重入只能以防为主,就是说尽量不要让重入发生,比如将代码改成下面的样子: #define delay() static unsigned c

41、har i; for(i=0;i10;i+) task_switch();/i 仍定义为stati c,但实际上已经不是同一个函数了,所以分配的地址不同. void func1() delay(); void func2() delay(); 用宏来代替函数,就意味着每个调用处都是一个独立的代码复本,那么两个 delay 实际使用的内存地址也就不同了,重入问题消失. 但这种方法带来的问题是,每调用一次delay(),都会产生一个 delay 的目标代码,如果 delay 的代码很多,那就会造成大量的 rom 空间占用.有其它办法没? 本人所知有限,只有最后一招了: void delay() r

42、eentrant unsigned char i; for(i=0;i10;i+) task_switch(); 加入 reentrant 申明后,该函数就可以支持重入.但小心使用 ,申明为重入后,函数效率极低! 最后附带说下中断.因为没太多可说的,就不单独开章了. 中断跟普通的写法没什么区别,只不过在目前所示例的多任务系统里因为有堆栈的压力,所以要使用using来减少对堆栈的使用 (顺便提下,也不要调用子函数,同样是为了减轻堆栈压力) 用 using,必须用#pragm a NOAREGS 关闭掉绝对寄存器访问,如果中断里非要调用函数,连同函数也要放在#pragm a NOAREGS 的作用

43、域内.如例所示: #pragma SAVE #pragma NOAREGS /使用 using 时必须将绝对寄存器访问关闭 void clock_timer(void) interrupt 1 using 1 /使用 using 是为了减轻堆栈的压力 #pragma RESTORE 改成上面的写法后,中断固定占用 4 个字节堆栈.就是说,如果你在不用中断时任务栈深定为8的话,现在就要定为 8+4 = 12 了. 另外说句废话,中断里处理的事一定要少,做个标记就行了,剩下的事交给对应的任务去处理. 现在小结一下: 切换任务时要保证没有寄存器跨越任务,否则产生任务间寄存器覆盖. 使用静态变量解决

44、切换任务时要保证没有变量跨越任务,否则产生任务间地址空间(变量) 覆盖. 使用静态变量解决 两个不同的任务不要调用同时调用同一个函数,否则产生重入覆盖. 使用重入申明解决 五.向操作系统迈进 源代码打包 ourdev_385 493.rar(文件大小:39K) (原文件名:aos.rar) 先下载示例代码.用 KEIL打开它,但先别急着看,回这里来. 前面所说的例子中,除了多任务并行执行能力外,没有其它功能,这对于一个极简单的系统来说是够用的,但如果系统稍复杂一点,例如: 1.某任务中需要延时 2.某任务中需要等待,直至某事务处理完. 3.任务并非一开始就全部装入,随着处理流程的展开,在不同的

45、时刻装入不同的任务.任务具有生命周期,事务处理完毕后,希望将任务结束并清除. 这里就是操作系统的几个典型功能: 1.休眠机制 2.消息机制 3.进程机制 事实上这些功能非常容易实现,如果对前面几篇的内容全部了解的话,很容易想象这些机制是如何实现的. 这一回我们就来讲讲这些机制是怎样实现的. 1.休眠及延时(延时又叫睡眠, 这里刻意改称“ 延时“,以防止与休眠混淆)机制: 为每个任务定义一字节计数器: unsigned char idata task_sleepMAX_TASKS;/任务睡眠定时器 该计数器会在每次定时器中断时减1(除非它的值为 0,或为 0xff) void clock_tim

46、er(void) interrupt 1 using 1 . /任务延迟处理 i = MAX_TASKS; p = task_sleep; do if(*p != 0 p+; while(-i); 在任务切换时,检查 task_sleep 的值是否为 0.不为零则跳过该任务不执行,检查下一个任务是否符合执行条件. void task_switch() . while(1) . task_id+;/task_id 切到下一个.实际上不只是增 1 这么简单 ,还要取模.这里只是示范,所以就不写全了. if( task_sleeptask_id = 0)/不为 0 表示该任务在休眠/延时中,所以跳过

47、. break; . 相关宏: task_sleep(timer) 延时 timer 个定时 器中断周期.取值 0254 task_suspend() 休眠.如果无其它 进程唤醒,则永远不会再执行 task_wakeup(tid) 唤醒任务号为 tid 的进程 2.任务动态载入与结束: 在 task_switch()里,当发现该进程的 task_sp 值为 0 则不再 保存该任务的栈指针,这个任务也就消失了. 在搜索下一个可执行任务时,检测 task_sp 值是否非 0.为零则表示该位置无任务. void task_switch() if(task_sptask_id != 0)/如果该任务 没被删除,则保存当前栈指针. task_sptask_id = SP; while(1) task_

本站链接:文库   一言   我酷   合作


客服QQ:2549714901微博号:道客多多官方知乎号:道客多多

经营许可证编号: 粤ICP备2021046453号世界地图

道客多多©版权所有2020-2025营业执照举报