创建51轻量级操作系统.pdf
《创建51轻量级操作系统.pdf》由会员分享,可在线阅读,更多相关《创建51轻量级操作系统.pdf(16页珍藏版)》请在淘文阁 - 分享文档赚钱的网站上搜索。
1、前言想了很久,要不要写这篇文章?最后觉得对操作系统感兴趣的人还是很多,写吧.我不一定能造出玉,但我可以抛出砖.包括我在内的很多人都对 51 使用操作系统呈悲观态度,因为 51 的片上资源太少.但对于很多要求不高的系统来说,使用操作系统可以使代码变得更直观,易于维护,所以在 51 上仍有操作系统的生存机会.流行的 uCos,Tiny51 等,其实都不适合在 2051 这样的片子上用,占资源较多,唯有自已动手,以不变应万变,才能让51也有操作系统可用.这篇贴子的目的,是教会大家如何现场写一个OS,而不是给大家提供一个OS版本.提供的所有代码,也都是示例代码,所以不要因为它没什么功能就说 LAJI
2、之类的话.如果把功能写全了,一来估计你也不想看了,二来也失去灵活性没有价值了.下面的贴一个示例出来,可以清楚的看到,OS 本身只有不到 10 行源代码,编译后的目标代码 60 字节,任务切换消耗为 20 个机器周期.相比之下,KEIL 内嵌的 TINY51 目标代码为 800 字节,切换消耗 100700 周期.唯一不足之处是,每个任务要占用掉十几字节的堆栈,所以任务数不能太多,用在 128B 内存的 51 里有点难度,但对于 52 来说问题不大.这套代码在 36M 主频的 STC12C4052 上实测,切换任务仅需 2uS.#include#define MAX_TASKS 2/任务槽个数.
3、必须和实际任务数一至#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;/任务装入函数.将指定的函数(参数 1)装入指定(参数 2)的任务槽中.如果该槽中原来就有任务,则原任务丢失
4、,但系统本身不会发生错误.void task_load(unsigned int fn,unsigned char tid)task_sptid=task_stacktid+1;task_stacktid0=(unsigned int)fn&0 xff;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
5、+;task_switch();/编译后在这里打上断点void task2()static unsigned char j;while(1)j+=2;task_switch();/编译后在这里打上断点void main()/这里装载了两个任务,因此在定义 MAX_TASKS 时也必须定义为 2task_load(task1,0);/将 task1 函数装入 0 号槽task_load(task2,1);/将 task2 函数装入 1 号槽os_start(0);这样一个简单的多任务系统虽然不能称得上真正的操作系统,但只要你了解了它的原理,就能轻易地将它扩展得非常强大,想知道要如何做吗?一.什么
6、是操作系统?人脑比较容易接受类比这种表达方式,我就用公交系统来类比操作系统吧.当我们要解决一个问题的时候,是用某种处理手段去完成它,这就是我们常说的方法,计算机里叫程序(有时候也可以叫它算法).以出行为例,当我们要从 A 地走到 B 地的时候,可以走着去,也可以飞着去,可以走直线,也可以绕弯路,只要能从 A 地到 B 地,都叫作方法.这种从 A 地到 B 的需求,相当于计算机里的任务,而实现从 A 地到 B 地的方法,叫作任务处理流程很显然,这些走法中,并不是每种都合理,有些傻子都会采用的,有些是傻子都不采会用的.用计算机的话来说就是,有的任务处理流程好,有的任务处理流程好,有的处理流程差.可
7、以归纳出这么几种真正算得上方法的方法:有些走法比较快速,适合于赶时间的人;有些走法比较省事,适合于懒人;有些走法比较便宜,适合于穷人.用计算机的话说就是,有些省 CPU,有些流程简单,有些对系统资源要求低.现在我们可以看到一个问题:如果全世界所有的资源给你一个人用(单任务独占全部资源),那最适合你需求的方法就是好方法.但事实上要外出的人很多,例如 10 个人(10 个任务),却只有 1 辆车(1 套资源),这叫作资源争用.如果每个人都要使用最适合他需求的方法,那司机就只好给他们一人跑一趟了,而在任一时刻里,车上只有一个乘客.这叫作顺序执行,我们可以看到这种方法对系统资源的浪费是严重的.如果我们
8、没有法力将 1 台车变成 10 台车来送这 10 个人,就只好制定一些机制和约定,让 1台车看起来像 10 台车,来解决这个问题的办法想必大家都知道,那就是制定公交线路.最简单的办法是将所有旅客需要走的起点与终点串成一条线,车在这条线上开,乘客则自已决定上下车.这就是最简单的公交线路.它很差劲,但起码解决客人们对车争用.对应到计算机里,就是把所有任务的代码混在一起执行.这样做既不优异雅,也没效率,于是司机想了个办法,把这些客户叫到一起商量,将所有客人出行的起点与终点罗列出来,统计这些线路的使用频度,然后制定出公交线路:有些路线可以合并起来成为一条线路,而那些不能合并的路线,则另行开辟行车车次,
9、这叫作任务定义.另外,对于人多路线,车次排多点,时间上也优先安排,这叫作任务优先级.经过这样的安排后,虽然仍只有一辆车,但运载能力却大多了.这套车次/路线的按排,就是一套公交系统.哈,知道什么叫操作系统了吧?它也就是这么样的一种约定.操作系统:我们先回过头归纳一下:汽车系统资源.主要指的是 CPU,当然还有其它,比如内存,定时器,中断源等.客户出行任务正在走的路线进程一个一个的运送旅客顺序执行同时运送所有旅客多任务并行按不同的使用频度制定路线并优先跑较繁忙的路线任务优先级计算机内有各种资源,单从硬件上说,就有 CPU,内存,定时器,中断源,I/O 端口等.而且还会派生出来很多软件资源,例如消息
10、池.操作系统的存在,就是为了让这些资源能被合理地分配.最后我们来总结一下,所谓操作系统,以我们目前权宜的理解就是:为解决计算机资源争用而制定出的一种约定.二.二.51 上的操作系统对于一个操作系统来说,最重要的莫过于并行多任务.在这里要澄清一下,不要拿当年的 DOS来说事,时代不同了.况且当年 IBM 和小比尔着急将 PC 搬上市,所以才抄袭 PLM(好象是叫这个名吧?记不太清)搞了个今天看来很粗制滥造的 DOS 出来.看看当时真正的操作系统-UNIX,它还在纸上时就已经是多任务的了.对于我们 PC 来说,要实现多任务并不是什么问题,但换到 MCU 却很头痛:1.系统资源少在 PC 上,CPU
11、 主频以 G 为单位,内存以 GB 为单位,而 MCU 的主频通常只有十几 M,内存则是 Byts.在这么少的资源上同时运行多个任务,就意味着操作系统必须尽可能的少占用硬件资源.2.任务实时性要求高PC 并不需要太关心实时性,因为 PC 上几乎所有的实时任务都被专门的硬件所接管,例如所有的声卡网卡显示上都内置有DSP以及大量的缓存.CPU只需坐在那里指手划脚告诉这些板卡如何应付实时信息就行了.而 MCU 不同,实时信息是靠 CPU 来处理的,缓存也非常有限,甚至没有缓存.一旦信息到达,CPU 必须在极短的时间内响应,否则信息就会丢失.就拿串口通信来举例,在标准的 PC 架构里,巨大的内存允许将
12、信息保存足够长的时间.而对于 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_load(unsigned int fn,
17、unsigned char tid)task_sptid=task_stacktid+1;task_stacktid0=(unsigned int)fn&0 xff;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()stati
18、c unsigned char j;while(1)j+=2;task_switch();/编译后在这里打上断点void main()/这里装载了两个任务,因此在定义 MAX_TASKS 时也必须定义为 2task_load(task1,0);/将 task1 函数装入 0 号槽task_load(task2,1);/将 task2 函数装入 1 号槽os_start(0);限于篇幅我已经将代码作了简化,并删掉了大部分注释,大家可以直接下载源码包,里面完整的注解,并带 KEIL 工程文件,断点也打好了,直接按 ctrl+f5 就行了.现在来看看这个多任务系统的原理:这个多任务系统准确来说,叫作
19、协同式多任务.所谓协同式,指的是当一个任务持续运行而不释放资源时,其它任务是没有任何机会和方式获得运行机会,除非该任务主动释放 CPU.在本例里,释放 CPU是靠 task_switch()来完成的.task_switch()函数是一个很特殊的函数,我们可以称它为任务切换器.要清楚任务是如何切换的,首先要回顾一下堆栈的相关知识.有个很简单的问题,因为它太简单了,所以相信大家都没留意过:我们知道,不论是 CALL 还是 JMP,都是将当前的程序流打断,请问 CALL 和 JMP 的区别是什么?你会说:CALL 可以 RET,JMP 不行.没错,但原因是啥呢?为啥 CALL 过去的就可以用 RET
20、 跳回来,JMP 过去的就不能用 RET 来跳回呢?很显然,CALL 通过某种方法保存了打断前的某些信息,而在返回断点前执行的 RET 指令,就是用于取回这些信息.不用多说,大家都知道,某些信息就是 PC 指针,而某种方法就是压栈.很幸运,在 51 里,堆栈及堆栈指针都是可被任意修改的,只要你不怕死.那么假如在执行 RET前将堆栈修改一下会如何?往下看:当程序执行 CALL 后,在子程序里将堆栈刚才压入的断点地址清除掉,并将一个函数的地址压入,那么执行完 RET 后,程序就跳到这个函数去了.事实上,只要我们在 RET 前将堆栈改掉,就能将程序跳到任务地方去,而不限于 CALL 里压入的地址.重
21、点来了.首先我们得为每个任务单独开一块内存,这块内存专用于作为对应的任务的堆栈,想将 CPU交给哪个任务,只需将栈指针指向谁内存块就行了.接下来我们构造一个这样的函数:当任务调用该函数时,将当前的堆栈指针保存一个变量里,并换上另一个任务的堆栈指针.这就是任务调度器了.OK 了,现在我们只要正确的填充好这几个堆栈的原始内容,再调用这个函数,这个任务调度就能运行起来了.那么这几个堆栈里的原始内容是哪里来的呢?这就是任务装载函数要干的事了.在启动任务调度前将各个任务函数的入口地址放在上面所说的任务专用的内存块里就行了!对了,顺便说一下,这个任务专用的内存块叫作私栈,私栈的意思就是说,每个任务的堆栈都
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 创建 51 轻量级 操作系统
限制150内