|
原帖子是在21IC.COM,舍友后来传我看"DIY实时操作系统中",觉得很好理解,循序渐进.
昨天刚认真开始看的,花了一天半的时间看得感觉还可以,不过只是看了只有延时服务的协作式的内核那部分,后面的暂时没看.后来自己就写了个总结,花了我三个钟头,太崩溃了,希望大家有空帮我指正指正.
一般我都会写一些东西,放在我的blog上:http://blog.csdn.net/zhiyu520/
先把原来的人家写的贴出来:
/*
第四篇:只有延时服务的协作式的内核
Cooperative Multitasking
前后台系统,协作式内核系统,与占先式内核系统,有什么不同呢?
记得在21IC上看过这样的比喻,“你(小工)在用厕所,经理在外面排第一,
老板在外面排第二。如果是前后台,不管是谁,都必须按排队的次序使用
厕所;如果是协作式,那么可以等你用完厕所,老板就要比经理先进入;
如果是占先式,只要有更高级的人在外面等,那么厕所里无论是谁,都要
第一时间让出来,让最高级别的人先用。”
*/
#include <avr/io.h>
#include <avr/Interrupt.h>
#include <avr/signal.h>
unsigned char Stack[200]; //人工堆栈
register unsigned char OSRdyTbl asm("r2"); //任务运行就绪表
register unsigned char OSTaskRunningPrio asm("r3"); //正在运行的任务
#define OS_TASKS 3 //设定运行任务的数量
struct TaskCtrBlock //任务控制块
{
unsigned int OSTaskStackTop; //保存任务的堆栈顶
unsigned int OSWaitTick; //任务延时时钟
} TCB[OS_TASKS+1];
//防止被编译器占用
register unsigned char tempR4 asm("r4");
register unsigned char tempR5 asm("r5");
register unsigned char tempR6 asm("r6");
register unsigned char tempR7 asm("r7");
register unsigned char tempR8 asm("r8");
register unsigned char tempR9 asm("r9");
register unsigned char tempR10 asm("r10");
register unsigned char tempR11 asm("r11");
register unsigned char tempR12 asm("r12");
register unsigned char tempR13 asm("r13");
register unsigned char tempR14 asm("r14");
register unsigned char tempR15 asm("r15");
register unsigned char tempR16 asm("r16");
register unsigned char tempR17 asm("r17");
//建立任务(每个任务在其本身的人工堆栈里压了19个字节)
void OSTaskCreate(void (*Task)(void),unsigned char *Stack,unsigned char TaskID)
{
unsigned char i;
*Stack--=(unsigned int)Task; //将任务的地址低位压入堆栈
*Stack--=(unsigned int)Task>>8; //将任务的地址高位压入堆栈
*Stack--=0x00; //R1 __zero_reg__
*Stack--=0x00; //R0 __tmp_reg__
*Stack--=0x80; //SREG 在任务中,开启全局中断
for(i=0;i<14;i++) //在 avr-libc 中的 FAQ中的 What registers are used by the C compiler?
*Stack--=i; //描述了寄存器的作用
TCB[TaskID].OSTaskStackTop=(unsigned int)Stack;
//将人工堆栈的栈顶,保存到TCB结构体数组中
OSRdyTbl|=0x01<<TaskID; //任务就绪表相应位置1,表明处于就绪态
}
//开始任务调度,从最低优先级的任务TaskScheduler开始
void OSStartTask()
{
OSTaskRunningPrio=OS_TASKS; //首先运行TaskScheduler任务
SP=TCB[OS_TASKS].OSTaskStackTop+17; //等号右边得到任务TaskScheduler的入口地址
__asm__ __volatile__("reti" "\t"); //ret和reti,它们都可以将堆栈栈顶SP的两个字节弹出来送入程序计数器PC中
}
//进行任务调度
void OSSched(void)
{
// 根据中断时保存寄存器的次序入栈(压栈不是在人工堆栈里面,而是在avr芯片本来分配的堆栈里),模拟一次中断后,入栈的情况
__asm__ __volatile__("PUSH __zero_reg__\t"); //R1
__asm__ __volatile__("PUSH __tmp_reg__\t"); //R0
__asm__ __volatile__("IN __tmp_reg__,__SREG__ \t"); //保存状态寄存器SREG
__asm__ __volatile__("PUSH __tmp_reg__\t");
__asm__ __volatile__("CLR __zero_reg__\t"); //R0重新清零
__asm__ __volatile__("PUSH R18\t");
__asm__ __volatile__("PUSH R19\t");
__asm__ __volatile__("PUSH R20\t");
__asm__ __volatile__("PUSH R21\t");
__asm__ __volatile__("PUSH R22\t");
__asm__ __volatile__("PUSH R23\t");
__asm__ __volatile__("PUSH R24\t");
__asm__ __volatile__("PUSH R25\t");
__asm__ __volatile__("PUSH R26\t");
__asm__ __volatile__("PUSH R27\t");
__asm__ __volatile__("PUSH R30\t");
__asm__ __volatile__("PUSH R31\t");
__asm__ __volatile__("PUSH R28\t"); //R28与R29用于建立在堆栈上的指针
__asm__ __volatile__("PUSH R29\t"); //入栈完成
TCB[OSTaskRunningPrio].OSTaskStackTop=SP; //将正在运行的任务的堆栈顶保存
unsigned char OSNextTaskID; //在现有堆栈上开设新的空间
for (OSNextTaskID = 0; //进行任务调度
OSNextTaskID < OS_TASKS && !(OSRdyTbl & (0x01<<OSNextTaskID)); //有已就绪任务就退出循环
OSNextTaskID++); //注意分号位置,这里循环体只是那个For语句
OSTaskRunningPrio = OSNextTaskID ; //得到一个新任务,调度到就执行态
cli(); //关中断,保护堆栈转换
SP=TCB[OSTaskRunningPrio].OSTaskStackTop; //OSSched函数一执行完,就可以运行这个新任务
sei();
//根据中断时的出栈次序
__asm__ __volatile__("POP R29\t");
__asm__ __volatile__("POP R28\t");
__asm__ __volatile__("POP R31\t");
__asm__ __volatile__("POP R30\t");
__asm__ __volatile__("POP R27\t");
__asm__ __volatile__("POP R26\t");
__asm__ __volatile__("POP R25\t");
__asm__ __volatile__("POP R24\t");
__asm__ __volatile__("POP R23\t");
__asm__ __volatile__("POP R22 \t");
__asm__ __volatile__("POP R21\t");
__asm__ __volatile__("POP R20\t");
__asm__ __volatile__("POP R19\t");
__asm__ __volatile__("POP R18\t");
__asm__ __volatile__("POP __tmp_reg__\t"); //SERG 出栈并恢复
__asm__ __volatile__("OUT __SREG__,__tmp_reg__\t");
__asm__ __volatile__("POP __tmp_reg__\t"); //R0 出栈
__asm__ __volatile__("POP __zero_reg__\t"); //R1 出栈
//中断时出栈完成
}
//使正在运行的任务自动放弃CPU,但不是进入就绪态
void OSTimeDly(unsigned int ticks)
{
if(ticks) //当延时有效
{
OSRdyTbl &= ~(0x01<<OSTaskRunningPrio); //任务运行就绪表相应位清0
TCB[OSTaskRunningPrio].OSWaitTick=ticks;//设置任务延时时钟
OSSched(); //从新调度
}
}
void TCN0Init(void) // 计时器0
{
TCCR0 = 0;
TCCR0 |= (1<<CS02); // 256预分频
TIMSK |= (1<<TOIE0); // T0溢出中断允许
TCNT0 = 100; // 置计数起始值 (递增,往255计数)
}
SIGNAL(SIG_OVERFLOW0)
{
unsigned char i;
for(i=0;i<OS_TASKS;i++) //任务时钟
{
if(TCB.OSWaitTick)
{
TCB.OSWaitTick--;
if(TCB.OSWaitTick==0) //当任务时钟到时,必须是由定时器减时的才行
{
OSRdyTbl |= (0x01<<i); //使任务在就绪表中置位
}
}
}
TCNT0=100;
}
void Task0()
{
unsigned int j=0;
while(1)
{
PORTB=j++;
OSTimeDly(2);
}
}
void Task1()
{
unsigned int j=0;
while(1)
{
PORTC=j++;
OSTimeDly(4);
}
}
void Task2()
{
unsigned int j=0;
while(1)
{
PORTD=j++;
OSTimeDly(8);
}
}
// 任务调度任务(相当于Task3),一般优先级最低
void TaskScheduler()
{
while(1)
{
OSSched(); //反复进行调度
}
}
int main(void)
{
TCN0Init();
OSRdyTbl=0;
OSTaskRunningPrio=0;
OSTaskCreate(Task0,&Stack[49],0);
OSTaskCreate(Task1,&Stack[99],1);
OSTaskCreate(Task2,&Stack[149],2);
OSTaskCreate(TaskScheduler,&Stack[199],OS_TASKS);
//经过上面四个语句后,建立了四个任务,都处于就绪态
OSStartTask();
}
/*
在上面的例子中,一切变得很简单,三个正在运行的主任务,都通过延时服务,主动放弃对CPU的控制权。
在时间中断中,对各个任务的的延时进行计时,如果某个任务的延时结束,将任务重新在就绪表中置位。
最低级的系统任务TaskScheduler(),在三个主任务在放弃对CPU的控制权后开始不断地进行调度。如果
某个任务在就绪表中置位,通过调度,进入最高级别的任务中继续运行。
*/
下面就我自己的总结,花了很大力气啊,写点以后希望对一些新手有用,声明,本人也是新手,刚学AVR,想做点小项目.
总结如下:
按函数执行顺序的详细流程文字说明:
主函数main()
OSTaskCreate()
函数建立了4个任务,分别是task0,task1,task2和TaskScheduler,在这个函数中,我们做了三件主要的事情
1、我们先把每个任务的函数地址分别压到它相对应的人工堆栈里面
2、压完了任务函数地址,我们就把人工堆栈的栈顶指针赋给TCB结构体数组里面的OSTaskStackTop中,
保存住(其实就是为了后面的寻找相应任务的函数地址)
3、把每个任务都在任务就绪表的相应位置1,表示任务可以被调用得到CPU的能力之一。但其实还要靠任务的优先级,有
点需要注意的是:任务就绪表中相应位越低,优先级越高,比如,bit0就比bit1低,bit0的那个任
务优先级就高。优先级体现在任务调度函数OSSched()里面的任务调度for循环语句中。应该仔细体会。
OSStartTask()
准备开始执行任务,因为函数里面“OSTaskRunningPrio=OS_TASKS;”这里(只是这里)根本就没需要考虑优先级的情况,
所以第一个准备执行(还没开始)的任务是TaskScheduler。
第二句SP=TCB[OS_TASKS].OSTaskStackTop+17;等号右边得到任务TaskScheduler的入口地址,接着赋给SP。
给它干吗,当然是有用了,你还记得“2人工堆栈”里面有一句话“ 对于ret和reti,它们都可以将堆栈栈顶的
两个字节弹出来送入程序计数器PC中,一般用来从子程序或中断中退出。”,显然,那就是等接着的第三句去完
成他的使命了。呵呵,第三句"reti"一完事,你的avrCPU就可以去程序计数器PC里面取地址,而这个地址呢,
就正是任务TaskScheduler的函数地址,哈哈,这才是正式开始执行第一个任务TaskScheduler。
现在,你看,我们已经不用再看主函数里面的函数了,因为以后也不会涉及到。以后的任务就是靠任务调度了。我们接着来看吧
刚才不是任务TaskScheduler是执行态嘛,这个任务又去反复不断调用OSSched函数,OSSched是个关键哦,这个函数在
这个“4只有延时服务的协作式的内核.”里面应该是最主要的,操作系统要靠调度,调度就是你妈,吃饭的时候,给
你吃饭你就可以吃,不给你吃你就那里先撑着,还给"TMD"按孩子从大到小(优先级)来分,不过不会相信你妈有那么偏心。
OK,不说废话,我们接着讲。
调度任务里面的调度函数OSSched()
OSSched()
一大堆PUSH指令
这里的一大堆PUSH汇编指令是为了保存一些通用积存器寄存器,也就是保护现场,不懂的,先得看看操作系统里面的书。
中断就是这样处理的,和这个差不多。刚才我们不是运行了第一个任务TaskScheduler吗,这里保存就是任务TaskScheduler
的现场,保存了大堆的寄存器。需要注意的是,这里PUSH的压可不是象建立任务OSTaskCreate()里面的把寄存器压到
每人任务相应的人工堆栈里面,而是压到avr芯片里面专门定义的一片堆栈里面。
接着TCB[OSTaskRunningPrio].OSTaskStackTop=SP;呵呵,你看了这里的SP是区别于人工堆栈里面的栈顶指针,一旦
这里的SP赋给了TCB[OSTaskRunningPrio].OSTaskStackTop,那么相应的,此任务的OSTaskStackTop原本指向人工堆
栈的栈顶指针就再也找不到,看OSTaskCreate()仔细对照一下吧。现在呢,我们OSTaskStackTop保存是正在运行的任务
的栈顶指针。其实保存SP也应该算是保存现场的一部分工作。
接着
unsigned char OSNextTaskID; //在现有堆栈上开设新的空间
for (OSNextTaskID = 0; //进行任务调度
OSNextTaskID < OS_TASKS && !(OSRdyTbl & (0x01<<OSNextTaskID)); //有已就绪任务就退出循环
OSNextTaskID++); //注意分号位置,这里循环体只是那个For语句
OSTaskRunningPrio = OSNextTaskID ; //得到一个新任务(有可能是原来的任务),调度到就执行态
注释也都很清楚了,关键有一点就是“得到一个新任务(有可能是原来的任务)”这句。这个要等我后面就会提到了,这
里暂时不会。
for里面的循环查找任务就绪表里面优先级最高的任务,都是从bit0向高位开始查找的。很显然,在这之前,我们从来
没有运行过task0,task1,task2,况且,他们在建立任务(见OSTaskCreate())的时候的就绪位都还是置1,具体
OSRdyTbl = 0000_1111。在检查了bit0位后就跳出了循环,所以,我们得到了将可执行的任务task0。相应的,
我们把此时的TCB[O].OSTaskStackTop赋给SP,OSTaskStackTop是等于指向人工堆栈里面的那个栈顶指针。哈哈,你
有点明白了吗,我们为什么在OSTaskCreate里面压那么多次的栈,其实有有用的,只可惜,它就只用一次,一夜情,用
完就走,哈哈。SP=TCB[OSTaskRunningPrio].OSTaskStackTop后接下来我们就开始大规模的弹栈了,这次(也只有这次,
好好再想想,再想想),我们弹出的是人工堆栈里面的那些东西。哦,还是弹17次哦,呵呵,看看OSTaskCreate(),是
不是也是压17次,想明白了吧。你看,弹了栈后,OSSched函数准备退出,那肯定也执行了"ret",呵呵,这不,SP不是
给了程序计数器PC,所以,我们就可以从TaskScheduler跳到task0了
提醒一下,弹栈的时候,我们只会弹一次人工堆栈里面的东西,也只有当这个任务(这个任务可能是task0,task1,task2,
之一,但不会是TaskScheduler,看for循环里OSNextTaskID < OS_TASKS=3)第一次准备被执行的时候(搞清楚点吧,多看下)
也就都弹出去了,以后也不会有了,所以,task0,task1,task2的人工堆栈最后只剩下任务入口地址在那里,它没有被弹出,
但其实它也没什么用了,一旦运行过它本身的任务后,任务函数入口地址就无意义了(为什么无意义,注意一下任务函数
里面的那个while(1)无限循环)。用一次后就无意义,不是个套吗,一次性,哈哈。
说到这里,感觉写得真多,不知道能不能让你明白。呵呵,接着
我们因为上面的调度,task0就开始执行了,我们看
void Task0()
{
unsigned int j=0;
while(1)
{
PORTB=j++;
OSTimeDly(2);
}
}
如果我们把OSTimeDly(2);注释掉,那这个任务就永远在那里运行了,也不会被调度,因为OSTimeDly()函数里面有个OSSched()。
没有它,就不会让其他任务得到运行的机会。
OSTimeDly()
OSTimeDly()函数会让任务自动放弃CPU,并且任务就绪表OSRdyTbl相应为清0,任务就暂时没有执行权利了.OSRdyTbl = 00001110
哦,只是暂时哦,什么时候有呢?那要看定时器/计数器的工作,也就是看定时器的那个中断服务程序。
SIGNAL(SIG_OVERFLOW0)
当每个任务时钟到时,使各自任务在就绪表中置位,这样任务就重新得到了运行的机会条件(还需要考虑优先级)之一了。
刚才不是说到任务Task0通过OSTimeDly()-->OSSched()有进行了一次调度。
这次调度有点象Task0任务被调度到一样。有一点不同的是,OSRdyTbl = 00001110,根据优先级,知道任务Task1会被调度到,
调度的过程,比如说压栈,弹栈什么的同第一次调用Task0一样。
依次类推,会接着执行到Task2,直到执行完........
现在,你仔细想过没有,上面的过程
TaskScheduler-->OSSched()-->Task0--->OSSched()-->Task1--->OSSched()-->Task2
有可能不是这样吗?
答案是有可能,不过是有可能,编写这个操作系统的人肯定考虑到这种状况了,所以没出现那个问题。
问题出在哪?就出现在OSTimeDly(unsigned int ticks)函数的那个任务时钟ticks的设定上,期间,可能会出现很多情况。
就用我们这个操作系统的的例子说一下吧
有三个任务(不包括调度任务)执行,任务就绪表OSRdyTbl = 00001111
Task0任务比较大,4.5秒 才能执行完这个任务,void Task0{ ...OSTimeDly(2秒);}
Task1任务比较大,5.0秒 才能执行完这个任务,void Task1{ ...OSTimeDly(4秒);}
Task2任务比较大,0.5秒 就能执行完这个任务,void Task2{ ...OSTimeDly(8秒);}
现在,我执行了Task0,花掉了4.5秒,期间,我也让它OSTimeDly(2秒),OSRdyTbl = 00001110。当然,调用这个函数肯定不能是
这样的,这里只是打个比方。就让它延时两秒。Task0执行完了,根据优先级,Task1将会被调用,但执行了Task1,要执行5.0秒,
在执行Task1的5秒期间,Task0延时的2秒早就完了,它会让任务就绪表中Task0的相应位置1,重新得到执行可能。也正是它,瞧,
Task1执行完后,又去搜索任务就绪表,而此刻OSRdyTbl = 00001101,我们就又能执行Task0了,任务Task2没有执行的份!!
接着,执行Task0需要4.5秒,但,Task1延时的那个4秒早就过了,它在任务Task0没执行完就已经得到就绪机会了....周而复始。
任务Task2不管大小,永远也轮不到它去执行,死在那里看人家吃饭了。
所以,这个ticks的设置在这个"只有延时服务的协作式的内核"是有点讲究的。
调度任务里面的调度函数OSSched() 我曾经说过:
OSTaskRunningPrio = OSNextTaskID ; //得到一个新任务(有可能是原来的任务),调度到就执行态
注释也都很清楚了,关键有一点就是“得到一个新任务(有可能是原来的任务)”这句。这个要等我后面就会提到了,这
里暂时不会。
你想到了答案没有?
比如说
Task0任务比较大,0.5秒 才能执行完这个任务,void Task0{ ...OSTimeDly(2秒);}
Task1任务比较大,0.5秒 才能执行完这个任务,void Task1{ ...OSTimeDly(10秒);}
Task2任务比较大,0.5秒 就能执行完这个任务,void Task2{ ...OSTimeDly(20秒);}
Task0有一段是将在那里一直执行几次,当然,期间有有点延时的。
其实,还有其他的问题,大家按这种思路,也不会很难,就可能在有一点上不开窍而已,多看几次,如果你会用仿真,那就更好
了,知道具体程序怎么走。我没用过,只是凭思路的。希望对大家有点用处 |
阿莫论坛20周年了!感谢大家的支持与爱护!!
如果天空是黑暗的,那就摸黑生存;
如果发出声音是危险的,那就保持沉默;
如果自觉无力发光,那就蜷伏于牆角。
但是,不要习惯了黑暗就为黑暗辩护;
也不要为自己的苟且而得意;
不要嘲讽那些比自己更勇敢的人。
我们可以卑微如尘土,但不可扭曲如蛆虫。
|