搜索
bottom↓
楼主: rainyss

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

  [复制链接]

出0入0汤圆

发表于 2008-8-14 01:43:44 | 显示全部楼层 |阅读模式
前言

想了很久,要不要写这篇文章?最后觉得对操作系统感兴趣的人还是很多,写吧.我不一定能造出玉,但我可以抛出砖.

包括我在内的很多人都对51使用操作系统呈悲观态度,因为51的片上资源太少.但对于很多要求不高的系统来说,使用操作系统可以使代码变得更直观,易于维护,所以在51上仍有操作系统的生存机会.

流行的uCos,Tiny51等,其实都不适合在2051这样的片子上用,占资源较多,唯有自已动手,以不变应万变,才能让51也有操作系统可用.这篇贴子的目的,是教会大家如何现场写一个OS,而不是给大家提供一个OS版本.提供的所有代码,也都是示例代码,所以不要因为它没什么功能就说LAJI之类的话.如果把功能写全了,一来估计你也不想看了,二来也失去灵活性没有价值了.


下面的贴一个示例出来,可以清楚的看到,OS本身只有不到10行源代码,编译后的目标代码60字节,任务切换消耗为20个机器周期.相比之下,KEIL内嵌的TINY51目标代码为800字节,切换消耗100~700周期.唯一不足之处是,每个任务要占用掉十几字节的堆栈,所以任务数不能太多,用在128B内存的51里有点难度,但对于52来说问题不大.这套代码在36M主频的STC12C4052上实测,切换任务仅需2uS.


#include <reg51.h>

#define MAX_TASKS 2       //任务槽个数.必须和实际任务数一至
#define MAX_TASK_DEP 12   //最大栈深.最低不得少于2个,保守值为12.
unsigned char idata task_stack[MAX_TASKS][MAX_TASK_DEP];//任务堆栈.
unsigned char task_id;    //当前活动任务号


//任务切换函数(任务调度器)
void task_switch(){
        task_sp[task_id] = SP;

        if(++task_id == MAX_TASKS)
                task_id = 0;

        SP = task_sp[task_id];
}

//任务装入函数.将指定的函数(参数1)装入指定(参数2)的任务槽中.如果该槽中原来就有任务,则原任务丢失,但系统本身不会发生错误.
void task_load(unsigned int fn, unsigned char tid){
        task_sp[tid] = task_stack[tid] + 1;
        task_stack[tid][0] = (unsigned int)fn & 0xff;
        task_stack[tid][1] = (unsigned int)fn >> 8;
}

//从指定的任务开始运行任务调度.调用该宏后,将永不返回.
#define os_start(tid) {task_id = tid,SP = task_sp[tid];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_start(0);
}




这样一个简单的多任务系统虽然不能称得上真正的操作系统,但只要你了解了它的原理,就能轻易地将它扩展得非常强大,想知道要如何做吗?



所附文件下载:

从单任务到多任务并行系统的演变ourdev_378093.rar(文件大小:115K) (原文件名:演变.rar)
一个最简单的多任务并行系统ourdev_378094.rar(文件大小:19K) (原文件名:mtask.rar)

出0入0汤圆

 楼主| 发表于 2008-8-14 01:44:50 | 显示全部楼层
一.什么是操作系统?



人脑比较容易接受"类比"这种表达方式,我就用"公交系统"来类比"操作系统"吧.

当我们要解决一个问题的时候,是用某种处理手段去完成它,这就是我们常说的"方法",计算机里叫"程序"(有时候也可以叫它"算法").
以出行为例,当我们要从A地走到B地的时候,可以走着去,也可以飞着去,可以走直线,也可以绕弯路,只要能从A地到B地,都叫作方法.这种从A地到B的需求,相当于计算机里的"任务",而实现从A地到B地的方法,叫作"任务处理流程"

很显然,这些走法中,并不是每种都合理,有些傻子都会采用的,有些是傻子都不采会用的.用计算机的话来说就是,有的任务处理流程好,有的任务处理流程好,有的处理流程差.
可以归纳出这么几种真正算得上方法的方法:
有些走法比较快速,适合于赶时间的人;有些走法比较省事,适合于懒人;有些走法比较便宜,适合于穷人.
用计算机的话说就是,有些省CPU,有些流程简单,有些对系统资源要求低.

现在我们可以看到一个问题:
如果全世界所有的资源给你一个人用(单任务独占全部资源),那最适合你需求的方法就是好方法.但事实上要外出的人很多,例如10个人(10个任务),却只有1辆车(1套资源),这叫作"资源争用".
如果每个人都要使用最适合他需求的方法,那司机就只好给他们一人跑一趟了,而在任一时刻里,车上只有一个乘客.这叫作"顺序执行",我们可以看到这种方法对系统资源的浪费是严重的.
如果我们没有法力将1台车变成10台车来送这10个人,就只好制定一些机制和约定,让1台车看起来像10台车,来解决这个问题的办法想必大家都知道,那就是制定公交线路.
最简单的办法是将所有旅客需要走的起点与终点串成一条线,车在这条线上开,乘客则自已决定上下车.这就是最简单的公交线路.它很差劲,但起码解决客人们对车争用.对应到计算机里,就是把所有任务的代码混在一起执行.
这样做既不优异雅,也没效率,于是司机想了个办法,把这些客户叫到一起商量,将所有客人出行的起点与终点罗列出来,统计这些线路的使用频度,然后制定出公交线路:有些路线可以合并起来成为一条线路,而那些不能合并的路线,则另行开辟行车车次,这叫作"任务定义".另外,对于人多路线,车次排多点,时间上也优先安排,这叫作"任务优先级".
经过这样的安排后,虽然仍只有一辆车,但运载能力却大多了.这套车次/路线的按排,就是一套"公交系统".哈,知道什么叫操作系统了吧?它也就是这么样的一种约定.




操作系统:


我们先回过头归纳一下:
汽车                                            系统资源.主要指的是CPU,当然还有其它,比如内存,定时器,中断源等.
客户出行                                        任务
正在走的路线                                    进程
一个一个的运送旅客                              顺序执行
同时运送所有旅客                                多任务并行
按不同的使用频度制定路线并优先跑较繁忙的路线    任务优先级


计算机内有各种资源,单从硬件上说,就有CPU,内存,定时器,中断源,I/O端口等.而且还会派生出来很多软件资源,例如消息池.
操作系统的存在,就是为了让这些资源能被合理地分配.
最后我们来总结一下,所谓操作系统,以我们目前权宜的理解就是:为"解决计算机资源争用而制定出的一种约定".

出0入0汤圆

 楼主| 发表于 2008-8-14 01:45:31 | 显示全部楼层
二.51上的操作系统

对于一个操作系统来说,最重要的莫过于并行多任务.在这里要澄清一下,不要拿当年的DOS来说事,时代不同了.况且当年IBM和小比尔着急将PC搬上市,所以才抄袭PLM(好象是叫这个名吧?记不太清)搞了个今天看来很"粗制滥造"的DOS出来.看看当时真正的操作系统---UNIX,它还在纸上时就已经是多任务的了.

对于我们PC来说,要实现多任务并不是什么问题,但换到MCU却很头痛:

1.系统资源少
在PC上,CPU主频以G为单位,内存以GB为单位,而MCU的主频通常只有十几M,内存则是Byts.在这么少的资源上同时运行多个任务,就意味着操作系统必须尽可能的少占用硬件资源.
2.任务实时性要求高
PC并不需要太关心实时性,因为PC上几乎所有的实时任务都被专门的硬件所接管,例如所有的声卡网卡显示上都内置有DSP以及大量的缓存.CPU只需坐在那里指手划脚告诉这些板卡如何应付实时信息就行了.
而MCU不同,实时信息是靠CPU来处理的,缓存也非常有限,甚至没有缓存.一旦信息到达,CPU必须在极短的时间内响应,否则信息就会丢失.
就拿串口通信来举例,在标准的PC架构里,巨大的内存允许将信息保存足够长的时间.而对于MCU来说内存有限,例如51仅有128字节内存,还要扣除掉寄存器组占用掉的8~32个字节,所以通常都仅用几个字节来缓冲.当然,你可以将数据的接收与处理的过程合并,但对于一个操作系统来说,不推荐这么做.
假定以115200bps通信速率向MCU传数据,则每个字节的传送时间约为9uS,假定缓存为8字节,则串口处理任务必须在70uS内响应.


这两个问题都指向了同一种解决思路:操作系统必须轻量轻量再轻量,最好是不占资源(那当然是做梦啦).

可用于MCU的操作系统很多,但适合51(这里的51专指无扩展内存的51)几乎没有.前阵子见过一个"圈圈操作系统",那是我所见过的操作系统里最轻量的,但仍有改进的余地.

很多人认为,51根本不适合使用操作系统.其实我对这种说法并不完全接受,否则也没有这篇文章了.
我的看法是,51不适合采用"通用操作系统".所谓通用操作系统就是,不论你是什么样的应用需求,也不管你用什么芯片,只要你是51,通通用同一个操作系统.

这种想法对于PC来说没问题,对于嵌入式来说也不错,对AVR来说还凑合,而对于51这种"贫穷型"的MCU来说,不行.
怎样行?量体裁衣,现场根据需求构建一个操作系统出来!

看到这里,估计很多人要翻白眼了,大体上两种:
1.操作系统那么复杂,说造就造,当自已是神了?
2.操作系统那么复杂,现场造一个会不会出BUG?
哈哈,看清楚了?问题出在"复杂"上面,如果操作系统不复杂,问题不就解决了?

事实上,很多人对操作系统的理解是片面的,操作系统不一定要做得很复杂很全面,就算仅个多任务并行管理能力,你也可以称它操作系统.
只要你对多任务并行的原理有所了解,就不难现场写一个出来,而一旦你做到了这一点,为各任务间安排通信约定,使之发展成一个为你的应用系统量身定做的操作系统也就不难了.

为了加深对操作系统的理解,可以看一看<<演变>>这份PPT,让你充分了解一个并行多任务是如何一步步从顺序流程演变过来的.里面还提到了很多人都在用的"状态机",你会发现操作系统跟状态机从原理上其实是多么相似.会用状态机写程序,都能写出操作系统.

出0入0汤圆

 楼主| 发表于 2008-8-14 01:46:26 | 显示全部楼层
三.我的第一个操作系统


直接进入主题,先贴一个操作系统的示范出来.大家可以看到,原来操作系统可以做得么简单.
当然,这里要申明一下,这玩意儿其实算不上真正的操作系统,它除了并行多任务并行外根本没有别的功能.但凡事都从简单开始,搞懂了它,就能根据应用需求,将它扩展成一个真正的操作系统.

好了,代码来了.
将下面的代码直接放到KEIL里编译,在每个task?()函数的"task_switch();"那里打上断点,就可以看到它们的确是"同时"在执行的.


#include <reg51.h>

#define MAX_TASKS 2       //任务槽个数.必须和实际任务数一至
#define MAX_TASK_DEP 12   //最大栈深.最低不得少于2个,保守值为12.
unsigned char idata task_stack[MAX_TASKS][MAX_TASK_DEP];//任务堆栈.
unsigned char task_id;    //当前活动任务号


//任务切换函数(任务调度器)
void task_switch(){
        task_sp[task_id] = SP;

        if(++task_id == MAX_TASKS)
                task_id = 0;

        SP = task_sp[task_id];
}

//任务装入函数.将指定的函数(参数1)装入指定(参数2)的任务槽中.如果该槽中原来就有任务,则原任务丢失,但系统本身不会发生错误.
void task_load(unsigned int fn, unsigned char tid){
        task_sp[tid] = task_stack[tid] + 1;
        task_stack[tid][0] = (unsigned int)fn & 0xff;
        task_stack[tid][1] = (unsigned int)fn >> 8;
}

//从指定的任务开始运行任务调度.调用该宏后,将永不返回.
#define os_start(tid) {task_id = tid,SP = task_sp[tid];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_start(0);
}



限于篇幅我已经将代码作了简化,并删掉了大部分注释,大家可以直接下载源码包,里面完整的注解,并带KEIL工程文件,断点也打好了,直接按ctrl+f5就行了.




现在来看看这个多任务系统的原理:

这个多任务系统准确来说,叫作"协同式多任务".
所谓"协同式",指的是当一个任务持续运行而不释放资源时,其它任务是没有任何机会和方式获得运行机会,除非该任务主动释放CPU.
在本例里,释放CPU是靠task_switch()来完成的.task_switch()函数是一个很特殊的函数,我们可以称它为"任务切换器".
要清楚任务是如何切换的,首先要回顾一下堆栈的相关知识.

有个很简单的问题,因为它太简单了,所以相信大家都没留意过:
我们知道,不论是CALL还是JMP,都是将当前的程序流打断,请问CALL和JMP的区别是什么?
你会说:CALL可以RET,JMP不行.没错,但原因是啥呢?为啥CALL过去的就可以用RET跳回来,JMP过去的就不能用RET来跳回呢?

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

重点来了......
首先我们得为每个任务单独开一块内存,这块内存专用于作为对应的任务的堆栈,想将CPU交给哪个任务,只需将栈指针指向谁内存块就行了.
接下来我们构造一个这样的函数:

当任务调用该函数时,将当前的堆栈指针保存一个变量里,并换上另一个任务的堆栈指针.这就是任务调度器了.

OK了,现在我们只要正确的填充好这几个堆栈的原始内容,再调用这个函数,这个任务调度就能运行起来了.
那么这几个堆栈里的原始内容是哪里来的呢?这就是"任务装载"函数要干的事了.

在启动任务调度前将各个任务函数的入口地址放在上面所说的"任务专用的内存块"里就行了!对了,顺便说一下,这个"任务专用的内存块"叫作"私栈",私栈的意思就是说,每个任务的堆栈都是私有的,每个任务都有一个自已的堆栈.

话都说到这份上了,相信大家也明白要怎么做了:

1.分配若干个内存块,每个内存块为若干字节:
这里所说的"若干个内存块"就是私栈,要想同时运行几少个任务就得分配多少块.而"每个子内存块若干字节"就是栈深.记住,每调一层子程序需要2字节.如果不考虑中断,4层调用深度,也就是8字节栈深应该差不多了.

unsigned char idata task_stack[MAX_TASKS][MAX_TASK_DEP]

当然,还有件事不能忘,就是堆指针的保存处.不然光有堆栈怎么知道应该从哪个地址取数据啊
unsigned char idata task_sp[MAX_TASKS]

上面两项用于装任务信息的区域,我们给它个概念叫"任务槽".有些人叫它"任务堆",我觉得还是"槽"比较直观

对了,还有任务号.不然怎么知道当前运行的是哪个任务呢?
unsigned char task_id
当前运行存放在1号槽的任务时,这个值就是1,运行2号槽的任务时,这个值就是2....

2.构造任务调度函函数:
void task_switch(){
        task_sp[task_id] = SP;//保存当前任务的栈指针

        if(++task_id == MAX_TASKS)//任务号切换到下一个任务
                task_id = 0;

        SP = task_sp[task_id];//将系统的栈指针指向下个任务的私栈.
}


3.装载任务:
将各任务的函数地址的低字节和高字节分别入在
task_stack[任务号][0]和task_stack[任务号][1]中:

为了便于使用,写一个函数:  task_load(函数名, 任务号)

void task_load(unsigned int fn, unsigned char tid){
        task_sp[tid] = task_stack[tid] + 1;
        task_stack[tid][0] = (unsigned int)fn & 0xff;
        task_stack[tid][1] = (unsigned int)fn >> 8;
}

4.启动任务调度器:
将栈指针指向任意一个任务的私栈,执行RET指令.注意,这可很有学问的哦,没玩过堆栈的人脑子有点转不弯:这一RET,RET到哪去了?嘿嘿,别忘了在RET前已经将堆栈指针指向一个函数的入口了.你别把RET看成RET,你把它看成是另一种类型的JMP就好理解了.

SP = task_sp[任务号];
return;

做完这4件事后,任务"并行"执行就开始了.你可以象写普通函数一个写任务函数,只需(目前可以这么说)注意在适当的时候(例如以前调延时的地方)调用一下task_switch(),以让出CPU控制权给别的任务就行了.


最后说下效率问题.
这个多任务系统的开销是每次切换消耗20个机器周期(CALL和RET都算在内了),贵吗?不算贵,对于很多用状态机方式实现的多任务系统来说,其实效率还没这么高--- case switch和if()可不像你想像中那么便宜.

关于内存的消耗我要说的是,当然不能否认这种多任务机制的确很占内存.但建议大家不要老盯着编译器下面的那行字"DATA = XXXbyte".那个值没意义,堆栈没算进去.关于比较省内存多任务机制,我将来会说到.
概括来说,这个多任务系统适用于实时性要求较高而内存需求不大的应用场合,我在运行于36M主频的STC12C4052上实测了一把,切换一个任务不到3微秒.


下回我们讲讲用KEIL写多任务函数时要注意的事项.
下下回我们讲讲如何增强这个多任务系统,跑步进入操作系统时代.

出0入0汤圆

 楼主| 发表于 2008-8-14 01:50:06 | 显示全部楼层
四.用KEIL写多任务系统的技巧与注意事项

C51编译器很多,KEIL是其中比较流行的一种.我列出的所有例子都必须在KEIL中使用.为何?不是因为KEIL好所以用它(当然它的确很棒),而是因为这里面用到了KEIL的一些特性,如果换到其它编译器下,通过编译的倒不是问题,但运行起来可能是堆栈错位,上下文丢失等各种要命的错误,因为每种编译器的特性并不相同.所以在这里先说清楚这一点.
但是,我开头已经说了,这套帖子的主要目的是阐述原理,只要你能把这几个例子消化掉,那么也能够自已动手写出适合其它编译器的OS.

好了,说说KEIL的特性吧,先看下面的函数:

sbit sigl = P1^7;
void func1(){
        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:0x00C7    DFFC     DJNZ     R7,C:00C5
   199: }
C:0x00C9    22       RET      

看清楚了没?这个函数里用到了R7,却没有对R7进行保护!
有人会跳起来了:这有什么值得奇怪的,因为上层函数里没用到R7啊.呵呵,你说的没错,但只说对了一半:事实上,KEIL编译器里作了约定,在调子函数前会尽可能释放掉所有寄存器.通常性况下,除了中断函数外,其它函数里都可以任意修改所有寄存器而无需先压栈保护(其实并不是这样,但现在暂时这样认为,饭要一口一口吃嘛,我很快会说到的).
这个特性有什么用呢?有!当我们调用任务切换函数时,要保护的对象里可以把所有的寄存器排除掉了,就是说,只需要保护堆栈即可!

现在我们回过头来看看之前例子里的任务切换函数:

void task_switch(){
        task_sp[task_id] = SP;//保存当前任务的栈指针

        if(++task_id == MAX_TASKS)//任务号切换到下一个任务
                task_id = 0;

        SP = task_sp[task_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;
   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:00C3)
   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(){
        register 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:0x00C7    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      


看到了吧?哈哈,这回神仙也算不出来了.因为两个函数没有了直接调用的关系,所以编译器认为它们之间不会产生冲突,结果分配了一对互相冲突的寄存器,当任务从func1()切换到func2()时,func1()中的寄存器内容就给破坏掉了.大家可以试着去编译一下下面的程序:

sbit sigl = P1^7;
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);
}

我们这里只是示例,所以仍可以通过手工分配不同的寄存器避免寄存器冲突,但在真实的应用中,由于任务间的切换是非常随机的,我们无法预知某个时刻哪个寄存器不会冲突,所以分配不同寄存器的方法不可取.那么,要怎么办呢?
这样就行了:

sbit sigl = P1^7;
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);
        }
}

将两个函数中的变量通通改成静态就行了.还可以这么做:

sbit sigl = P1^7;
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();
        }
}

即,在变量的作用域内不切换任务,等变量用完了,再切换任务.此时虽然两个任务仍然会互相破坏对方的寄存器内容,但对方已经不关心寄存器里的内容了.

以上所说的,就是"变量覆盖"的问题.现在我们系统地说说关于"变量覆盖".

变量分两种,一种是全局变量,一种是局部变量(在这里,寄存器变量算到局部变量里).
对于全局变量,每个变量都会分配到单独的地址.
而对于局部变量,KEIL会做一个"覆盖优化",即没有直接调用关系的函数的变量共用空间.由于不是同时使用,所以不会冲突,这对内存小的51来说,是好事.
但现在我们进入多任务的世界了,这就意味着两个没有直接调用关系的函数其实是并列执行的,空间不能共用了.怎么办呢?一种笨办法是关掉覆盖优化功能.呵呵,的确很笨.

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

说完了"覆盖"我们再说说"重入".
所谓重入,就是一个函数在同一时刻有两个不同的进程复本.对初学者来说可能不好理解,我举个例子吧:
有一个函数在主程序会被调用,在中断里也会被调用,假如正当在主程序里调用时,中断发生了,会发生什么情况?

void func1(){
        static char data i;
        i = 5;
        do{
                sigl = !sigl;
        }while(--i);
}

假定func1()正执行到i=3时,中断发生,一旦中断调用到func1()时,i的值就被破坏了,当中断结束后,i == 0.

以上说的是在传统的单任务系统中,所以重入的机率不是很大.但在多任务系统中,很容易发生重入,看下面的例子:
void func1(){
....
delay();
....
}
void func2(){
....
delay();
....
}
void delay(){
        static unsigned char i;//注意这里是申明为static,不申明static的话会发生覆盖问题.而申明为static会发生重入问题.麻烦啊
        for(i=0;i<10;i++)
                task_switch();
}

两个并行执行的任务都调用了delay(),这就叫重入.问题在于重入后的两个复本都依赖变量i来控制循环,而该变量跨越了任务,这样,两个任务都会修改i值了.
重入只能以防为主,就是说尽量不要让重入发生,比如将代码改成下面的样子:
#define delay() {static unsigned char i; for(i=0;i<10;i++) task_switch();}//i仍定义为static,但实际上已经不是同一个函数了,所以分配的地址不同.
void func1(){
....
delay();
....
}
void func2(){
....
delay();
....
}

用宏来代替函数,就意味着每个调用处都是一个独立的代码复本,那么两个delay实际使用的内存地址也就不同了,重入问题消失.
但这种方法带来的问题是,每调用一次delay(),都会产生一个delay的目标代码,如果delay的代码很多,那就会造成大量的rom空间占用.有其它办法没?

本人所知有限,只有最后一招了:
void delay() reentrant{
        unsigned char i;
        for(i=0;i<10;i++)
                task_switch();
}
加入reentrant申明后,该函数就可以支持重入.但小心使用,申明为重入后,函数效率极低!



最后附带说下中断.因为没太多可说的,就不单独开章了.
中断跟普通的写法没什么区别,只不过在目前所示例的多任务系统里因为有堆栈的压力,所以要使用using来减少对堆栈的使用(顺便提下,也不要调用子函数,同样是为了减轻堆栈压力)
用using,必须用#pragma NOAREGS关闭掉绝对寄存器访问,如果中断里非要调用函数,连同函数也要放在#pragma NOAREGS的作用域内.如例所示:

#pragma SAVE
#pragma NOAREGS  //使用using时必须将绝对寄存器访问关闭
void clock_timer(void) interrupt 1 using 1 //使用using是为了减轻堆栈的压力
}
#pragma RESTORE

改成上面的写法后,中断固定占用4个字节堆栈.就是说,如果你在不用中断时任务栈深定为8的话,现在就要定为8+4 = 12了.
另外说句废话,中断里处理的事一定要少,做个标记就行了,剩下的事交给对应的任务去处理.



现在小结一下:

切换任务时要保证没有寄存器跨越任务,否则产生任务间寄存器覆盖.        使用静态变量解决
切换任务时要保证没有变量跨越任务,否则产生任务间地址空间(变量)覆盖.  使用静态变量解决
两个不同的任务不要调用同时调用同一个函数,否则产生重入覆盖.          使用重入申明解决

出0入0汤圆

 楼主| 发表于 2008-8-14 03:34:17 | 显示全部楼层
占位

出0入0汤圆

 楼主| 发表于 2008-8-17 18:30:57 | 显示全部楼层
TO49楼:
1.OS放到延时处代替延时和在延时处CALL子程序有什么区别么

没看明白.

2.要是再任务1切换到任务2的时候~任务2执行时间长于任务1中的延时时间那不是不能跳回任务1~非要到任务2完成后释放CPU了才能去继续做任务1么~那任务1不就不连贯了么~要是任务1是液晶显示这种的话显示不就被拖慢了么??

在操作系统里设计任务思路与单任务时是不一样的,对于有时序上要求时应使用中断和缓冲区机制.其实对于很多单任务的程序来说,这种方法一样用得很多.
而且操作系统并不是万能的,对于时间粒度非常小的延时(比如几微秒到几十微秒),几乎所有的操作系统是无能为力的.
只有通过合理的使用中断/缓冲区及划分任务片(跟在状态机上的要求一样,只是写代码直观点罢了)

出0入0汤圆

 楼主| 发表于 2008-8-17 20:20:20 | 显示全部楼层
延时的时候调子程序是一个"土"办法,你调的子程序是固的,当任务较多或周期性不一样的时候,处理起来很麻烦.
而调任务切换函数,任务不需要关心切换函数调用后所发生的事(也就是到底调用到哪去了),更加有系统性.

出0入0汤圆

 楼主| 发表于 2008-8-17 23:00:17 | 显示全部楼层
呵呵

出0入0汤圆

 楼主| 发表于 2008-8-18 10:52:30 | 显示全部楼层
TO62楼
"任务栈切换确实比较简单,但是有个问题:就是环境的保存和恢复,在这篇文章里只是讨论如何回避这个问题。"


先来一句开场白:天下没有免费的午餐,牺牲少量的自由可以换取巨大的速度提升!!!!

所谓的"环境的保存和恢复",就是上下文.我的核心理念就是尽可能的避过上下文,而不是去保存它们,原因是这样的:

1.对于51这样的CPU来说,对寄存器进行保护会占用大量的存储(每任务+8字节),以及大量的时间(保存/恢复8个寄存器需要额外支出最少16个机器周期).
2.即使保存了寄存器,还得面对内存变量的覆盖问题.而解决内存变量的覆盖与解决寄存器变量覆盖的方法在KEIL中完全相同.

很多人会跳起来说保存寄存器是所有操作系统都是要做的,我只说一句:别太定向思维

可以换个思维方式:

所谓的上下文,不仅包括了堆栈和各寄存器,其实也包括了所有的内存的单元以及各种片上外设,仅将寄存器和堆栈划分到上下文中而其它部分取名为"公共资源"显然只是一厢情愿的定势思维.反过来说,既然可以选择避过那么多公共资源,又何不将寄存器也一起避过呢(除非是无法避过)?

事实上KEIL的编器给我们提供了这种机制,只要我们遵守一些简单的约定,就可以完全地避过寄存器保护的问题.

我手头最快的版本,切换一个任务只需10个机器周期,这是靠避过寄存器保护而换取的,否则,时间马上飚升至100周期.且对内存的巨大占用将使得在128B的片子上由原先的"免强可用"变成"完全不可用".

保护寄存器的版本我早就做过,意义不大,与现有的大量流行版本相比,没有什么优势和特点.
顺便提一句,省内存的版本我也做过,每字节堆栈开销为6机器周期!!!

讲的就是速度

出0入0汤圆

 楼主| 发表于 2008-8-18 11:49:29 | 显示全部楼层
我说的速度是在保证可靠性的前提下.

出0入0汤圆

 楼主| 发表于 2008-8-18 19:24:15 | 显示全部楼层
to 69楼

protothread是一种很优秀的多任务机制,事实上我的文中就有一部分是从protothread里借鉴总结过来的,从技术上来讲,你甚至可以说是由它扩展而来的.
对于128字节内存的51上来说protothread其实更适合,但本文的主旨是代码要简单而机制上又要尽量接近操作系统,以方便大家在理解文中的例子后能自行构造OS,所以没以protothread为例,有兴趣者可以自行上网找资料参考下,网上的protothread资料非常多,应用也很成熟,我就不在这里多说了.

出0入0汤圆

 楼主| 发表于 2008-8-20 11:30:40 | 显示全部楼层
请帮忙搬到5楼去,提前预留了发帖位置,但超时不能编辑了.


                          五.向操作系统迈进

源代码打包 ourdev_385493.rar(文件大小:39K) (原文件名:aos.rar)
先下载示例代码.用KEIL打开它,但先别急着看,回这里来.

前面所说的例子中,除了多任务并行执行能力外,没有其它功能,这对于一个极简单的系统来说是够用的,但如果系统稍复杂一点,例如:

1.某任务中需要延时
2.某任务中需要等待,直至某事务处理完.
3.任务并非一开始就全部装入,随着处理流程的展开,在不同的时刻装入不同的任务.任务具有生命周期,事务处理完毕后,希望将任务结束并清除.

这里就是操作系统的几个典型功能:
1.休眠机制
2.消息机制
3.进程机制

事实上这些功能非常容易实现,如果对前面几篇的内容全部了解的话,很容易想象这些机制是如何实现的.
这一回我们就来讲讲这些机制是怎样实现的.

1.休眠及延时(延时又叫睡眠,这里刻意改称"延时",以防止与休眠混淆)机制:

为每个任务定义一字节计数器:
unsigned char idata task_sleep[MAX_TASKS];//任务睡眠定时器

该计数器会在每次定时器中断时减1(除非它的值为0,或为0xff)
void clock_timer(void) interrupt 1 using 1
{

...
        //任务延迟处理
        i = MAX_TASKS;
        p = task_sleep;
        do{
                if(*p != 0 && *p != -1)//不为0,也不为0xff,则将任务延时值减1.为0xff表示任务已挂起,不由定时器唤醒
                        (*p)--;
                p++;
        }while(--i);
}

在任务切换时,检查task_sleep的值是否为0.不为零则跳过该任务不执行,检查下一个任务是否符合执行条件.
void task_switch(){
...
        while(1){
...
                task_id++;//task_id切到下一个.实际上不只是增1这么简单,还要取模.这里只是示范,所以就不写全了.

                if( task_sleep[task_id] == 0)//不为0表示该任务在休眠/延时中,所以跳过.
                        break;
        }

...
}
相关宏:
task_sleep(timer) 延时timer个定时器中断周期.取值0~254
task_suspend() 休眠.如果无其它进程唤醒,则永远不会再执行
task_wakeup(tid) 唤醒任务号为tid的进程


2.任务动态载入与结束:

在task_switch()里,当发现该进程的task_sp值为0则不再保存该任务的栈指针,这个任务也就消失了.
在搜索下一个可执行任务时,检测task_sp值是否非0.为零则表示该位置无任务.
void task_switch(){

        if(task_sp[task_id] != 0)//如果该任务没被删除,则保存当前栈指针.
                task_sp[task_id] = SP;

        while(1){
                task_id++;//task_id切到下一个.实际上不只是增1这么简单,还要取模.这里只是示范,所以就不写全了.

                if( task_sp[task_id] != 0)//实际上这里还要检查task_sleep的值.但那跟现在所说无关,所以暂时去除掉那部分代码
                        break;
        }
...
}

调用task_switch()前清除自已的task_sp值.
#define task_exit() task_sp[task_id] = 0, task_switch()
附带说下,调用task_delete(tid) 可删除tid指定的进程.


3.消息机制:
消息机制是借助sleep/suspend来实现的,不能算真正的消息机制.但在很多场合下已经足够了,对51这样的芯片来说,资源占用率和执行效率更重要.

定义一个消息向量表,每个表示一个消息,每个项能保存一个task_id
event_vector[MAX_EVENT_VECTOR]
#define EVENT0 0
#define EVENT1 1
#define EVENT2 2
....
#define EVENTn MAX_EVENT_VECTOR - 1

当进程要监听该消息时,将自已的task_id号装入对应的向量中即可.例如要监听EVENT_RF_PULS_SENT,只需:

event_vector[EVENT_RF_PULS_SENT] = task_id;

这个过程称为"消息注册",已写为一个宏 event_replace(eid)

所以上例只需写成
event_replace(EVENT_RF_PULS_SENT);
...
event_clear(EVENT_RF_PULS_SENT);//使用完后消除该消息

如果不确定该消息目前是否有其它过程已在监听,可使用event_reg(eid, reg),将原先的向量保存在参数reg指定的变量中,并在用完消息后用event_restor(eid, reg)还原回来.

static unsigned char old_event_vector;
event_reg(EVENT_RF_PULS_SENT, old_event_vector);
....
event_unreg(EVENT_RF_PULS_SENT, old_event_vector);//使用完后还原该消息

如果监听了消息,在退出任务前必须解除监听,否则会引发错误地唤醒.
当进程处于运行态时是无法收到消息的,因此要等待消息必须进入休眠/延时状态.完整过程:

注册消息
...
进入休眠
...

要唤醒一个等待消息的进程,调用event_push(eid)即可.
如果eid指定的消息无进程监听,则消息被丢弃.
另外要注意的是,由于消息机制是借用休眠机制来完成的,所以如果监听消息的进程未处于休眠/延时中时,进程是无法收到消息的,该消息会被直接丢弃.在这种情况下,应使用task_wait_interrupt()来完成.
这种情况发生于进程监听的消息产生于中断服务程序中.其机制如下:

假定任务A与中断服务A_ISV
A中完成对缓冲区的填写,填完后进入休眠,等待消息MESSAGE_A
A_ISV负责在定时中断发生时将缓冲区中的字节写到P1口,写完后发送MESSAGE_A

通常情况下,这个过程并无问题,但当以下情况发生时,任务A将永远处于等待中:
A填写完缓冲区后,进入休眠前,定时器中断发生了
此时中断服务程序按步就班地将缓冲区处理完,并发送MESSAGE_A消息.如前所说,发送消息的实质是将task_sleep的值置为0
中断服务返回后,也按步就班将task_sleep置值,此刻它一点也不知道,也无从知道中断已经发生过了,于是信息实质上丢失.于是该任务再也不会醒来.

解决的方法是,在写缓冲区前先将任务的task_sleep置值,然后才写缓冲区,然后才进入休眠状态.这样,中断发生时task_sleep必已完成赋值,因而消息不会丢失.
该过程已写为一个宏task_wait_interrupt(缓冲区操作的语句)
写法有点别扭,但工作得很好.如果不习惯这样的风格,可以直接展开该宏书写代码:
task_setsuspend(task_id);
操作缓冲区的语句
task_switch();










示例代码:

1.调用子任务,并等待子任务完成后发送消息.类似于调用函数.与调用函数相比好处在于,可以启动多个子任务同时执行,而调用函数只能一个一个执行.
void task2(){
        static unsigned char i;

        i = sizeof(stra);
       
        do{
                stra[i-1] = strb[i-1];
                task_switch();
        }while(--i);
       
        event_push(EVENT_RF_PULS_SENT);//发送消息(其实质是唤醒监听该消息的进程)

        task_exit();//结束任务.
}

void task3(){
        static unsigned char event_backup;//用于保存信号EVENT_RF_PULS_SENT原来的值.在这个例子里实际上是不需要保存的,因为EVENT_RF_PULS_SENT未被其它进程监听.但在真实应用中则不一定能预知.

        event_reg(EVENT_RF_PULS_SENT, event_backup);//注册消息,原值保存在event_backup中(该变量必须申明为静态)

        //如果等待的消息产生于另一任务进程中,则使用task_suspend()就可以了.
        strb[0] = 3, strb[1] = 2, strb[2] = 1;//填写缓冲区
        task_load(task2);//装载子任务
        task_suspend();

        event_unreg(EVENT_RF_PULS_SENT, event_backup);//退出前必须还原消息中原来的值

        task_exit();//结束任务.
}



2.等待中断处理并发送消息:
void clock_timer(void) interrupt 1 using 1
{
        if(strb[0] != 0 && strb[1] != 0 && strb[2] != 0){
                P0 =  strb[0];
                P1 =  strb[1];
                P2 =  strb[2];
                push_event(EVENT_RF_PULS_SENT);
        }
}

void task3(){
        static unsigned char event_backup;//用于保存信号EVENT_RF_PULS_SENT原来的值.在这个例子里实际上是不需要保存的,因为EVENT_RF_PULS_SENT未被其它进程监听.但在真实应用中则不一定能预知.

        event_reg(EVENT_RF_PULS_SENT, event_backup);//注册消息,原值保存在event_backup中(该变量必须申明为静态)

        //如果等待的消息产生于另一任务进程中,则使用task_suspend()就可以了.

        task_wait_interrupt(
                        strb[0] = 3, strb[1] = 2, strb[2] = 1;
                )//填写缓冲区
        //如果写成以下形式:
        //strb[0] = 3, strb[0] = 2, strb[0] = 1;
        //task_suspend();
        //如果在执行完第一行语句后正好发生中断,则从中断返回后,任务调用task_suspend()后将永远不会醒.

        event_unreg(EVENT_RF_PULS_SENT, event_backup);//退出前必须还原消息中原来的值

        task_exit();//结束任务.
}

出0入0汤圆

 楼主| 发表于 2008-8-20 19:29:58 | 显示全部楼层
呵呵,很高兴你能提问.

你提的问题的确是问题,你也没理解错,所以event_push并没有写成函数,而是写成了宏,这样做正是为了避免重入.所以你完全不用担心例子里提供的所有系统"函数",因为它们要么不可能产生重入,要么根本就不是函数.

另外,实际上即使写成函数也不会发生重入,因为没有变量覆盖问题,中断也不会引起任务切换.任务切换必须由当前控制CPU的进程自发产生.所以采用避免重入的方式只是为了便于维护

"函数"列举如下:
task_switch()  不可能发生重入
task_load() 不可能发生重入(除非你在中断里调用该函数.但在本系统中,这种方式是禁止的)
其它所有函数均为宏,主要有如下
task_sleep()
task_suspend()
task_wakeup()
task_exit()
task_delete()
task_setsleep()
task_setsuspend()
event_reg()
event_unreg()
event_set()  //这个宏好象错写成event_replace了.不过不影响使用.
event_clear()
event_push()

出0入0汤圆

 楼主| 发表于 2008-8-27 17:59:58 | 显示全部楼层
1.下载完整程序
2.虽然很简单,但系统地看一下书理解更透.C里的数组/结构对地址的操作几句话说不清楚.
3.是的.

出0入0汤圆

 楼主| 发表于 2008-8-28 20:25:07 | 显示全部楼层
假定task_stack定义为    task_stack[m][n]

&task_stack == task_stack + i
&task_stack[j] = task_stack + j

所以,写成task_stack[tid] + i只是为了少打个&符号,一种风格罢了.
task_stp[tid] = task_stack[tid] + 1的意思就是,将第tid-1个栈的第2个字节的地址放入task_sp的第tid-1个单元去.

总之解释起来很麻烦,这些东西都是比较抽象的,虽简单,但不好描述.还是希望你看书解决,以免因表述和理解的偏差而被误导.

出0入0汤圆

 楼主| 发表于 2008-9-9 13:45:00 | 显示全部楼层
如果你的程序的确没有逻辑问题,应检查你的上下文变量是否产生了覆盖问题,以及有没有发生重入.还不行的话,把代码打包传上来(不要直接帖在这里)

出0入0汤圆

 楼主| 发表于 2008-9-18 06:41:32 | 显示全部楼层
如果有问题还能工作得起来?这可是堆栈,错一丁点都不行的.





本贴被 rainyss 编辑过,最后修改时间:2008-09-18,06:43:18.

出0入0汤圆

 楼主| 发表于 2008-9-18 06:49:27 | 显示全部楼层
//任务装入函数&nbsp;

void&nbsp;task_load(unsigned&nbsp;int&nbsp;fn,&nbsp;unsigned&nbsp;char&nbsp;tid){&nbsp;

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;task_sp[tid]&nbsp;=&nbsp;task_stack[tid]&nbsp;+&nbsp;1;&nbsp;//装入栈指针,该值在切换任务时会加载到SP中



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;task_stack[tid][0]&nbsp;=&nbsp;(unsigned&nbsp;int)fn&nbsp;&&nbsp;0xff;&nbsp;//向私栈中装入任务地址低8位

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;task_stack[tid][1]&nbsp;=&nbsp;(unsigned&nbsp;int)fn>>&nbsp;8;&nbsp;//任务地址高8位

}&nbsp;



任务切换过程:



call&nbsp;task_switch函数

将SP值保存到task_sp[tid]中

将tid切换到下一个任务

将task_sp[tid]中的值装入SP中

ret&nbsp;from&nbsp;task_switch

出0入0汤圆

 楼主| 发表于 2008-9-18 09:03:12 | 显示全部楼层
这几句里头没有用到非标准C的句型.

出0入0汤圆

 楼主| 发表于 2008-9-26 23:49:04 | 显示全部楼层
初值不同.减到零不再减,同时任务恢复正常调度

出0入0汤圆

 楼主| 发表于 2008-10-20 12:16:47 | 显示全部楼层
建了个群,4759163,专门用来讨论超轻量OS.

刚建好,目前里面没啥讨论,想加入的话,确认你是否有耐心等着人多起来吧.

出0入0汤圆

 楼主| 发表于 2008-10-30 21:56:53 | 显示全部楼层
就是分时复用啊.多任务OS,就是制定一种分时复用的规则

本贴被 rainyss 编辑过,最后修改时间:2008-10-30,22:25:56.

出0入0汤圆

 楼主| 发表于 2008-11-4 11:56:10 | 显示全部楼层
【146楼】&nbsp;xt0320&nbsp;

积分:6

派别:

等级:------

来自:

&nbsp;楼主好像笔误少了这句:&nbsp;

unsigned&nbsp;char&nbsp;idata&nbsp;task_sp[MAX_TASKS];//任务的栈指针&nbsp;&nbsp;







你看得可真够仔细的.厉害.估计大部分人都没有注意到.

出0入0汤圆

 楼主| 发表于 2008-11-9 00:31:28 | 显示全部楼层
【152楼】&nbsp;gamethink&nbsp;

积分:1023

派别:

等级:------

来自:

&nbsp;void&nbsp;task_switch(){&nbsp;



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;task_sp[task_id]&nbsp;=&nbsp;SP;&nbsp;



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if(++task_id&nbsp;==&nbsp;MAX_TASKS)&nbsp;

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;task_id&nbsp;=&nbsp;0;&nbsp;



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SP&nbsp;=&nbsp;task_sp[task_id];&nbsp;

}&nbsp;



怎么觉得task_sp[task_id]&nbsp;=&nbsp;SP;是完全没有作用的...调试了很多次也没发现具体作用&nbsp;&nbsp;









你把它删了试试看.

出0入0汤圆

 楼主| 发表于 2008-11-9 22:55:08 | 显示全部楼层
【156楼】&nbsp;robinyuan&nbsp;



积分:130

派别:

等级:------

来自:

&nbsp;这个RTOS的主要问题就是要限制子程序里的代码不要太长,以防止实时性不够&nbsp;

但一般LCD的显示程序比较长,有无好方法处理&nbsp;&nbsp;







找几个点插入若干task_switch()就行了.

出0入0汤圆

 楼主| 发表于 2008-12-2 00:38:27 | 显示全部楼层
【171楼】&nbsp;gamethink&nbsp;



积分:1057

派别:

等级:------

来自:广州

&nbsp;LZ,其实一直不太明白"监听进程"的具体意思和作用.&nbsp;

按我理解,是不是看看另外一个任务是否有被挂起???&nbsp;&nbsp;

 &nbsp;&nbsp;





得看上下文.有两种可能:

一种是用于监控其它进程的进程,但这种可能性小,"监听"二字用得不规范,应该叫"监控".

"监听进程"通常指的是等待外部触发条件发生的进程.比如等待网络包到达,等待串口数据到达,等待引脚电平翻转.



"监听"二字必定"服务"及"事件"挂勾.每种监听都对应着相应的事务处理.当事件发生时,监听被激活,转入或开启新进程进入事件处理的服务流程,处理完成后回到等待状态.等待过程中监听进程就像一个老式电话局里等待客户请求接线的接线生,所以叫"监听".

本贴被 rainyss 编辑过,最后修改时间:2008-12-02,00:43:47.

出0入0汤圆

 楼主| 发表于 2008-12-4 11:31:42 | 显示全部楼层
比较接近了.监听就意味着挂起.

操作系统里有"事件"和"锁"的概念.

当事件未发生时,监听进程读取共享锁被挂起.当底层程序(例如中断)发现指定的动作发生时,比如串口数据到达时,底层程序会将数据以事件(有时也叫消息或邮件)的形式唤醒监听进程.



在我写的例子中没有使用锁,而是用了挂起功能.消息是用唤醒功能完全的.当然,了解了原理则区别不大.

出0入0汤圆

 楼主| 发表于 2008-12-19 05:40:53 | 显示全部楼层
【186楼】&nbsp;gamethink&nbsp;



积分:1093

派别:

等级:------

来自:广州

&nbsp;to&nbsp;kejian2000&nbsp;&nbsp;

谢谢你的回答,我试过确实不能够删除&nbsp;



to&nbsp;LZ&nbsp;

我从"3.消息机制"开始就看得很迷惑了...其实是不是还有其它文章没放的?而且和源代码的差别也很大&nbsp;&nbsp;

 &nbsp;&nbsp;









消息机制是一种软件机制,用于进程间通信的.就是说,你要理解消息,就要先真正理解什么叫进程,为什么要进行进程间通信,进程间通信通的是什么东西.接下来要理解进程挂起和唤醒.这些背景知识不先理解,则无法理解消息的含义.

限于篇幅我并没有解释这些东西,因为帖子的主要目的是"如果搭一个精简的操作系统"而非"操作系统原理教学",写太啰嗦反而搞得谁都看不下去.

本贴被 rainyss 编辑过,最后修改时间:2008-12-19,05:45:48.

出0入0汤圆

 楼主| 发表于 2008-12-27 02:43:39 | 显示全部楼层
to192楼



那些背景知识要系统全面的讲会很复杂,不全面讲又会引出更多的疑问,反而搞得晕头转向.个人认为这些知识还是先找本操作系统原理的书看看比较好.



操作系统里绝大部分概念都是鸡与蛋的关系,必须使用反复通读慢慢了解全盘渐近的方法,才能真正吸收,靠背,靠教,作用都不大

出0入0汤圆

 楼主| 发表于 2009-1-2 17:59:35 | 显示全部楼层
&lt;&lt;操作系统原理>>是一定要看的,不用精读,以大至看懂概念为标准通读一遍,不懂的先记下来,在读完第一遍后重读,如此迭低若干次.



接下来去google里搜"实时多任务",将两方面的知识结合起来.



做完这两部分后去找陈明计和电脑圈圈,这两人的OS模型一个倾向于"正规化作战"一个喜欢搞"游击战".吸收他们的个性到你的知识体系中去.记住,知识本身位居其二,个性才是知识的本源.



说起来简单做起来并不易,要持之以恒长期努力,且永远不要认为自已已经懂了.

出0入0汤圆

 楼主| 发表于 2009-1-24 15:19:41 | 显示全部楼层
不存在你担心的覆盖问题,跑飞跟函数没有关系.跑飞极有可能是你内存使用过量引起的



void&nbsp;main(void)&nbsp;

{&nbsp;

idata&nbsp;unsigned&nbsp;int&nbsp;i;&nbsp;&nbsp;//题外话,这个变量没必要定义为idata,idata太占CPU资源,好象也没有必要用uint吧?uchar够用了

unsigned&nbsp;char&nbsp;array[100]&nbsp;=&nbsp;{"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~!@#$%^&*()[]{}|\\&lt;>?/|\0"};&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;&nbsp;这个数组应定义为unsigned&nbsp;char&nbsp;code&nbsp;array[]&nbsp;

unsigned&nbsp;short&nbsp;c;&nbsp;



最后,你的LcdDelay(DELAY)函数是不应该使用的,多任务操作中不应使用空循环,除非延时时间足够短,因为延时期间CPU是被白白消耗的.我另一个例子里有task_sleep()的实现方法,不占CPU资源,延时最小单位为1&nbsp;tick,你可以参考一下.

本贴被 rainyss 编辑过,最后修改时间:2009-01-24,15:24:30.

出0入0汤圆

 楼主| 发表于 2009-3-27 02:54:12 | 显示全部楼层
【215楼】 lrxgr
积分:34
派别:
等级:------
来自:
有个很低级的问题,如果执行第一任务里有一个延时,那么延时还没到达是,切换到第二个任务是,这时第一个任务的延时到达,cpu是不是就错过了一次捕捉延时的机会??

   



这个问题并不低级.这里头有个很重要的概念,就是总任务周期时间:

当任一任务暂时放弃CPU后,在最坏的情况下CPU要多久才能回到该任务.

最简单的计算方法是将每一个任务的最大执行时间加起来,设法让该时间不超过任意一个任务所允许的最大延迟.当然,这种方法太浪费了,但可以保证绝对有效.经验多了以后再去推算动态情况下如何保证每个任务都不会错过执行时机.

出0入0汤圆

 楼主| 发表于 2009-4-8 03:11:40 | 显示全部楼层
楼上精辟.不要用画圈把自已困住才是正道.

出0入0汤圆

 楼主| 发表于 2009-4-22 01:00:10 | 显示全部楼层
AVR上已经有很多现成的轻量系统,没必要自已做.
你把我专为51写的系统拿到AVR上用是不行的,不是改一点代码就可以的,而是连设计理念都不能在AVR上成立.
当然,你的尝试是对的,不过写一个操作系统需要对该CPU和编译器都十分了解,你需要先练练功底,可以找一两个小OS来研究一下.坛里有一篇阿莫转载的AVR OS DIY的文章,但我找不着了.

出0入0汤圆

 楼主| 发表于 2009-4-24 23:41:49 | 显示全部楼层
http://www.ouravr.com/bbs/bbs_content.jsp?bbs_sn=574348&bbs_page_no=1&search_mode=4&search_text=rainyss&bbs_id=9999

找着了.234楼想做AVR的OS,可以去那里看.

出0入0汤圆

 楼主| 发表于 2009-4-28 00:20:26 | 显示全部楼层
那样的话所有的任务都慢下来了.

出0入0汤圆

 楼主| 发表于 2009-6-7 23:38:33 | 显示全部楼层
to 【263楼】 wolf11_1234
积分:59
派别:
等级:------
来自:



以空间换时间.

出0入0汤圆

 楼主| 发表于 2009-7-28 00:15:29 | 显示全部楼层
【281楼】 he306
积分:1
派别:
等级:------
来自:
寄存器为什么不入堆栈呀?
不会丢失数据吗?  


这就是技巧了。放弃寄存器入栈可以极大地提高任何切换的速度以及节省大量的堆栈空间,代价是使用局部变量时不作用域内不得跨越任务。

出0入0汤圆

 楼主| 发表于 2009-10-2 10:30:30 | 显示全部楼层
【311楼】 szxy06dq 怪兽

积分:52
派别:
等级:------
来自:安徽合肥
感觉分时复用好  
__________________________


“时分复用”。

出0入0汤圆

 楼主| 发表于 2009-12-18 05:12:55 | 显示全部楼层
gshuang1.....无语

"连脑子都不动下,连试都不试一下就回复...."
"不想多解释,你下载到单片机去执行下就知道,除非你根本没了解过...."
   
这些全都是自已说的话,也恰恰是你自已犯下的错误.真想叫你把话吃回去,哈哈.

出0入0汤圆

 楼主| 发表于 2009-12-23 23:42:06 | 显示全部楼层
【375楼】 first_vip
积分:5
派别:
等级:------
来自:
对应该做成时间轮片或者实时性的,否则和多弄几个函数没多大区别,另外为每个任务单独开辟一个内存空间太太太浪费了,不符和超小操作系统的环保风格,建议像uC一样做成一个列队,节约内存。  
   
2009-12-21,13:24:30   资料  邮件           编辑  删除   

【376楼】 first_vip
积分:5
派别:
等级:------
来自:
另外一个操作系统必须有内存回收和垃圾清理机制否则没有可靠性可言  
   


两组发言矛盾

不管怎么样,还是回应你矛盾的提问吧

没内存回收?要求整上,否则没实用价值
OK,有内存回收了,但没标准API?要求整上,否则没实用价值
OK,有标准API了,没显示器键盘接口?要求整上,否则没实用价值
OK,有显示器了,不支持标准显示器?要求整上,否则没实用价值
OK,支持标准显示器了,不支持图形模式?要求整上,否则没实用价值
.....
哇,运行在51上的windows 7

出0入0汤圆

 楼主| 发表于 2009-12-28 16:15:05 | 显示全部楼层
【393楼】 yvhksovo
积分:85
派别:
等级:------
来自:
《演变》ppt在哪里找哦?  
   
在1楼.<<从单任务到多任务并行系统的演变>>

出0入0汤圆

 楼主| 发表于 2010-5-9 01:47:24 | 显示全部楼层
【480楼】 whj19860123
积分:103
派别:
等级:------
来自:
菜鸟提问下,#define os_start(tid) {task_id = tid,SP = task_sp[tid];return;} 这句话RETURN的用处是这样吗???
return的汇编是RET,将函数入口地址弹出来,使得PC指向要调用函数的入口地址。



是的.背景知识掌握得不错.

出0入0汤圆

 楼主| 发表于 2010-8-12 02:29:55 | 显示全部楼层
【521楼】 pallana
积分:14
派别:
等级:------
来自:
今天花了几个小时的时间,终于搞懂了。巧妙的利用了RET指令来切换任务。在Keil中调试通过,确实是多个任务同时。
于是,我不得不来顶作者的帖子。
下一步,我打算自己敲一遍程序,背下来,以后写的时候就是就不需要来翻帖子了。我的经验,学习编程“背”非常管用。只是不知道这算不算剽窃。  
   


算呀,不过我也是从别人那里抄来的,有protothread,还有电脑圈圈.剥完皮再穿上我做的衣服,人家就认不出来啦...

出0入0汤圆

 楼主| 发表于 2010-8-19 00:10:52 | 显示全部楼层
【553楼】 bluehacker
积分:30
派别:
等级:------
来自:
欣赏楼主的精神,但我坚持认为这类在51上跑系统的想法没有意义  
   

种树不一定为了吃桃,也可以是乘凉.

出0入0汤圆

 楼主| 发表于 2010-10-8 22:29:34 | 显示全部楼层
回复【595楼】sdpz
rainyss,你说接了36mhz晶振,stc12类型标称最多35mhz,你试过最快可以跑多快?
-----------------------------------------------------------------------

最高36M,多一点都不行.试过36.1XXX,结果是变成几M.

出0入0汤圆

 楼主| 发表于 2010-10-16 11:05:39 | 显示全部楼层
回复【610楼】zhanghzhan
如果每个任务都执行完,才开始 task_switch()。那么跟时间片论调没有区别。
但是这种想法的确让人很受启发!
起码我是看得真真的!不错 非常不错。超级不错。
---吗日寇!
-----------------------------------------------------------------------

本来就是启发目的.我自用的正式版本复杂多了,带能源控制的,任务休眠时单片机耗电由40mA降到7mA.

出0入0汤圆

 楼主| 发表于 2010-11-10 20:41:55 | 显示全部楼层
回复【622楼】youngdog
回复【楼主位】rainyss  
-----------------------------------------------------------------------
请教一下rainyss大神,在主函数进入到os_start()时,这个时候的sp里面的值是主函数里的断点,然后在sp被修改了之后,原来的这个断点的值就被抛弃了,那这个值是不是就永远留着ram里面,还是会被keil重新利用起来呢?
-----------------------------------------------------------------------

主函数是没有自已的堆栈空间的,这从main()头一行就可以看出来.主函数的堆栈是借用了任务的堆栈,在系统还没开始调度前,每个任务的堆栈都只占用2字节,后面全是空的,可以拿来用.
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-4-19 12:34

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表