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

加入VIP,免费下载
 

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

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

下载须知

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

版权提示 | 免责声明

本文(51单片机多任务的原理和实现.docx)为本站会员(b****5)主动上传,冰豆网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。 若此文所含内容侵犯了您的版权或隐私,请立即通知冰豆网(发送邮件至service@bdocx.com或直接QQ联系客服),我们立即给予删除!

51单片机多任务的原理和实现.docx

1、51单片机多任务的原理和实现51单片机多任务操作系统的原理与实现 51单片机多任务操作系统的原理与实现- 一个超轻量级的操作系统前言想了很久,要不要写这篇文章?最后觉得对操作系统感兴趣的人还是很多,写吧.我不一定能造出玉,但我可以抛出砖.包括我在的很多人都对51使用操作系统呈悲观态度,因为51的片上资源太少.但对于很多要求不高的系统来说,使用操作系统可以使代码变得更直观,易于维护,所以在51上仍有操作系统的生存机会. 流行的uCos,Tiny51等,其实都不适合在2051这样的片子上用,占资源较多,唯有自已动手,以不变应万变,才能让51也有操作系统可用.这篇贴子的目的,是教会大家如何现场写一个

2、OS,而不是给大家提供一个OS版本.提供的所有代码,也都是示例代码,所以不要因为它没什么功能就说LAJI之类的话.如果把功能写全了,一来估计你也不想看了,二来也失去灵活性没有价值了. 下面的贴一个示例出来,可以清楚的看到,OS本身只有不到10行源代码,编译后的目标代码60字节,任务切换消耗为20个机器周期.相比之下,KEIL嵌的TINY51目标代码为800字节,切换消耗100700周期.唯一不足之处是,每个任务要占用掉十几字节的堆栈,所以任务数不能太多,用在128B存的51里有点难度,但对于52来说问题不大.这套代码在36M主频的STC12C4052上实测,切换任务仅需2uS. #includ

3、e #define 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; /任务装入函数.

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

5、;return; /*=以下为测试代码=*/ 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_star

6、t(0); 这样一个简单的多任务系统虽然不能称得上真正的操作系统,但只要你了解了它的原理,就能轻易地将它扩展得非常强大,想知道要如何做吗?一.什么是操作系统?人脑比较容易接受类比这种表达方式,我就用公交系统来类比操作系统吧. 当我们要解决一个问题的时候,是用某种处理手段去完成它,这就是我们常说的方法,计算机里叫程序(有时候也可以叫它算法). 以出行为例,当我们要从A地走到B地的时候,可以走着去,也可以飞着去,可以走直线,也可以绕弯路,只要能从A地到B地,都叫作方法.这种从A地到B的需求,相当于计算机里的任务,而实现从A地到B地的方法,叫作任务处理流程 很显然,这些走法中,并不是每种都合理,有些

7、傻子都会采用的,有些是傻子都不采会用的.用计算机的话来说就是,有的任务处理流程好,有的任务处理流程好,有的处理流程差. 可以归纳出这么几种真正算得上方法的方法: 有些走法比较快速,适合于赶时间的人;有些走法比较省事,适合于懒人;有些走法比较便宜,适合于穷人. 用计算机的话说就是,有些省CPU,有些流程简单,有些对系统资源要求低. 现在我们可以看到一个问题: 如果全世界所有的资源给你一个人用(单任务独占全部资源),那最适合你需求的方法就是好方法.但事实上要外出的人很多,例如10个人(10个任务),却只有1辆车(1套资源),这叫作资源争用. 如果每个人都要使用最适合他需求的方法,那司机就只好给他们

8、一人跑一趟了,而在任一时刻里,车上只有一个乘客.这叫作顺序执行,我们可以看到这种方法对系统资源的浪费是严重的. 如果我们没有法力将1台车变成10台车来送这10个人,就只好制定一些机制和约定,让1台车看起来像10台车,来解决这个问题的办法想必大家都知道,那就是制定公交线路. 最简单的办法是将所有旅客需要走的起点与终点串成一条线,车在这条线上开,乘客则自已决定上下车.这就是最简单的公交线路.它很差劲,但起码解决客人们对车争用.对应到计算机里,就是把所有任务的代码混在一起执行. 这样做既不优异雅,也没效率,于是司机想了个办法,把这些客户叫到一起商量,将所有客人出行的起点与终点罗列出来,统计这些线路的

9、使用频度,然后制定出公交线路:有些路线可以合并起来成为一条线路,而那些不能合并的路线,则另行开辟行车车次,这叫作任务定义.另外,对于人多路线,车次排多点,时间上也优先安排,这叫作任务优先级. 经过这样的安排后,虽然仍只有一辆车,但运载能力却大多了.这套车次/路线的按排,就是一套公交系统.哈,知道什么叫操作系统了吧?它也就是这么样的一种约定. 操作系统: 我们先回过头归纳一下: 汽车 系统资源.主要指的是CPU,当然还有其它,比如存,定时器,中断源等.客户出行 任务正在走的路线 进程 一个一个的运送旅客 顺序执行 同时运送所有旅客 多任务并行按不同的使用频度制定路线并优先跑较繁忙的路线 任务优先

10、级 计算机有各种资源,单从硬件上说,就有CPU,存,定时器,中断源,I/O端口等.而且还会派生出来很多软件资源,例如消息池. 操作系统的存在,就是为了让这些资源能被合理地分配. 最后我们来总结一下,所谓操作系统,以我们目前权宜的理解就是:为解决计算机资源争用而制定出的一种约定.二.51上的操作系统对于一个操作系统来说,最重要的莫过于并行多任务.在这里要澄清一下,不要拿当年的DOS来说事,时代不同了.况且当年IBM和小比尔着急将PC搬上市,所以才抄袭PLM(好象是叫这个名吧?记不太清)搞了个今天看来很粗制滥造的DOS出来.看看当时真正的操作系统-UNIX,它还在纸上时就已经是多任务的了. 对于我

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

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

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

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

15、操作系统.三.我的第一个操作系统直接进入主题,先贴一个操作系统的示出来.大家可以看到,原来操作系统可以做得么简单. 当然,这里要申明一下,这玩意儿其实算不上真正的操作系统,它除了并行多任务并行外根本没有别的功能.但凡事都从简单开始,搞懂了它,就能根据应用需求,将它扩展成一个真正的操作系统. 好了,代码来了. 将下面的代码直接放到KEIL里编译,在每个task?()函数的task_switch();那里打上断点,就可以看到它们的确是同时在执行的.#include #define MAX_TASKS 2 /任务槽个数.必须和实际任务数一至 #define MAX_TASK_DEP 12 /最大栈深

16、.最低不得少于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; /任务装入函数.将指定的函数(参数1)装入指定(参数2)的任务槽中.如果该槽中原来就有任务,则原任务丢失,但系统本身不会发生错误. void task_lo

17、ad(unsigned int fn, unsigned char tid) task_sptid = task_stacktid + 1; task_stacktid0 = (unsigned int)fn & 0xff; 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

18、+; 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_start(0); 限于篇幅我已经将代码作了简化,并删掉了大部分注释,大家可以直接下载源码包,里面完整的注解,并带KEIL工程文件,断点也打好了,直

19、接按ctrl+f5就行了. 现在来看看这个多任务系统的原理: 这个多任务系统准确来说,叫作协同式多任务. 所谓协同式,指的是当一个任务持续运行而不释放资源时,其它任务是没有任何机会和方式获得运行机会,除非该任务主动释放CPU. 在本例里,释放CPU是靠task_switch()来完成的.task_switch()函数是一个很特殊的函数,我们可以称它为任务切换器. 要清楚任务是如何切换的,首先要回顾一下堆栈的相关知识. 有个很简单的问题,因为它太简单了,所以相信大家都没留意过: 我们知道,不论是CALL还是JMP,都是将当前的程序流打断,请问CALL和JMP的区别是什么? 你会说:CALL可以R

20、ET,JMP不行.没错,但原因是啥呢?为啥CALL过去的就可以用RET跳回来,JMP过去的就不能用RET来跳回呢? 很显然,CALL通过某种方法保存了打断前的某些信息,而在返回断点前执行的RET指令,就是用于取回这些信息. 不用多说,大家都知道,某些信息就是PC指针,而某种方法就是压栈. 很幸运,在51里,堆栈及堆栈指针都是可被任意修改的,只要你不怕死.那么假如在执行RET前将堆栈修改一下会如何?往下看: 当程序执行CALL后,在子程序里将堆栈刚才压入的断点地址清除掉,并将一个函数的地址压入,那么执行完RET后,程序就跳到这个函数去了. 事实上,只要我们在RET前将堆栈改掉,就能将程序跳到任务

21、地方去,而不限于CALL里压入的地址. 重点来了. 首先我们得为每个任务单独开一块存,这块存专用于作为对应的任务的堆栈,想将CPU交给哪个任务,只需将栈指针指向谁存块就行了. 接下来我们构造一个这样的函数: 当任务调用该函数时,将当前的堆栈指针保存一个变量里,并换上另一个任务的堆栈指针.这就是任务调度器了.OK了,现在我们只要正确的填充好这几个堆栈的原始容,再调用这个函数,这个任务调度就能运行起来了. 那么这几个堆栈里的原始容是哪里来的呢?这就是任务装载函数要干的事了. 在启动任务调度前将各个任务函数的入口地址放在上面所说的任务专用的存块里就行了!对了,顺便说一下,这个任务专用的存块叫作私栈,

22、私栈的意思就是说,每个任务的堆栈都是私有的,每个任务都有一个自已的堆栈. 话都说到这份上了,相信大家也明白要怎么做了: 1.分配若干个存块,每个存块为若干字节: 这里所说的若干个存块就是私栈,要想同时运行几少个任务就得分配多少块.而每个子存块若干字节就是栈深.记住,每调一层子程序需要2字节.如果不考虑中断,4层调用深度,也就是8字节栈深应该差不多了. unsigned char idata task_stackMAX_TASKSMAX_TASK_DEP 当然,还有件事不能忘,就是堆指针的保存处.不然光有堆栈怎么知道应该从哪个地址取数据啊 unsigned char idata task_spM

23、AX_TASKS 上面两项用于装任务信息的区域,我们给它个概念叫任务槽.有些人叫它任务堆,我觉得还是槽比较直观 对了,还有任务号.不然怎么知道当前运行的是哪个任务呢? unsigned char task_id 当前运行存放在1号槽的任务时,这个值就是1,运行2号槽的任务时,这个值就是2. 2.构造任务调度函函数: void task_switch() task_sptask_id = SP;/保存当前任务的栈指针 if(+task_id = MAX_TASKS)/任务号切换到下一个任务 task_id = 0; SP = task_sptask_id;/将系统的栈指针指向下个任务的私栈. 3

24、.装载任务: 将各任务的函数地址的低字节和高字节分别入在 task_stack任务号0和task_stack任务号1中: 为了便于使用,写一个函数: task_load(函数名, 任务号) void task_load(unsigned int fn, unsigned char tid) task_sptid = task_stacktid + 1; task_stacktid0 = (unsigned int)fn & 0xff; task_stacktid1 = (unsigned int)fn 8; 4.启动任务调度器: 将栈指针指向任意一个任务的私栈,执行RET指令.注意,这可很有学

25、问的哦,没玩过堆栈的人脑子有点转不弯:这一RET,RET到哪去了?嘿嘿,别忘了在RET前已经将堆栈指针指向一个函数的入口了.你别把RET看成RET,你把它看成是另一种类型的JMP就好理解了. SP = task_sp任务号; return; 做完这4件事后,任务并行执行就开始了.你可以象写普通函数一个写任务函数,只需(目前可以这么说)注意在适当的时候(例如以前调延时的地方)调用一下task_switch(),以让出CPU控制权给别的任务就行了. 最后说下效率问题. 这个多任务系统的开销是每次切换消耗20个机器周期(CALL和RET都算在了),贵吗?不算贵,对于很多用状态机方式实现的多任务系统来

26、说,其实效率还没这么高- case switch和if()可不像你想像中那么便宜. 关于存的消耗我要说的是,当然不能否认这种多任务机制的确很占存.但建议大家不要老盯着编译器下面的那行字DATA = XXXbyte.那个值没意义,堆栈没算进去.关于比较省存多任务机制,我将来会说到. 概括来说,这个多任务系统适用于实时性要求较高而存需求不大的应用场合,我在运行于36M主频的STC12C4052上实测了一把,切换一个任务不到3微秒. 下回我们讲讲用KEIL写多任务函数时要注意的事项. 下下回我们讲讲如何增强这个多任务系统,跑步进入操作系统时代.四.用KEIL写多任务系统的技巧与注意事项C51编译器很

27、多,KEIL是其中比较流行的一种.我列出的所有例子都必须在KEIL中使用.为何?不是因为KEIL好所以用它(当然它的确很棒),而是因为这里面用到了KEIL的一些特性,如果换到其它编译器下,通过编译的倒不是问题,但运行起来可能是堆栈错位,上下文丢失等各种要命的错误,因为每种编译器的特性并不相同.所以在这里先说清楚这一点. 但是,我开头已经说了,这套帖子的主要目的是阐述原理,只要你能把这几个例子消化掉,那么也能够自已动手写出适合其它编译器的OS. 好了,说说KEIL的特性吧,先看下面的函数: sbit sigl = P17; void func1() register char data i; i = 5; dosigl = !sigl; while(-i); 你会说,这个函数没什么特别的嘛!呵呵,别着急,你将它编译了,然后展开汇编代码再看看: 193: void func1() 194: register char data i; 195: i = 5; C:0x00C3 7F05 MOV R7,#0x05 196:

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1