前言
想了很久,要不要写这篇文章?最后觉得对操作系统感兴趣的人还是很多,写吧.我不一定能造出玉,但我可以抛出砖.
包括我在内的很多人都对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)
五.向操作系统迈进
源代码打包 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();//结束任务.
}
一周热门 更多>