前言
看了很久的操作系统原理,ucos源码也看了大半,但是感觉总是懵懵懂懂,用句流行的网络用语就是始终上不了车,后来在网上被人推荐了一篇文章《建立一个属于自己的操作系统》,这篇文章真的非常好,也附有源码,但不知道是不是我找的文章有差错还是啥,我根据文章提供的源码贴代码,根本无法编译,然后开始读代码修改代码最后成功编译但是在硬件平台运行根本不行。后来又断断续续看ucos源码,反正各种什么数据结构啊的通信什么的让人头痛,后来大学的单片机原理完课,学校安排课设,我选了时钟定时器(有点像闹钟),这种开环的裸机开发没什么难度,闲着也是闲着于是从新捡起几个月前没有完成的os,这次重新开坑,代码完全自己敲,从基本功能开始一步步实现os,像《建立一个属于自己的操作系统》介绍的一样。忙活了两天也终于成功了,并且成功地将时钟定时器移植到自己的os上跑了,说实话在os跑比裸机的前后台的效果好很多(os上跑按按键与现实感觉是同步进行的,前后台的效果在按按键的时候数码显示是会黑屏的),但是51的硬件资源太少,只有128个字节的ram,所以此次设计没有统一的任务通信接口,只能实现基本的优先级,延时服务或者轮询服务。在自己成功地建立一个属于自己os后在看ucos的源码更加顺畅,以前一直搞不懂的任务通信也能明白一二(不过也得谢谢任哲写的那本《嵌入式操作系统基础ucOS-Ⅱ和Linux(第2版)》),废话不多说,正文如下:
ps:第一次写这种博客,写的不好望谅解,由于《建立一个属于自己的操作系统》本身就写的很详细,所以我只写出os建立的核心部分。
正文
1,任务人口地址:在os中,是在不直接用程序名(参数)这种方式调用任务。那怎样呢?这部分《建立一个属于自己的操作系统》讲的非常详细,大家自行搜阅。
2,任务调度:学过单片机原理的都知道,cpu中有sp与pc两个特殊的寄存器,sp是堆栈指针,在51中它可以指向数据区的任意单元,PC是程序计数器,它始终保存下一条程序指令的地址。51C语言是可以直接操控sp的,但是pc不行,所以要想办法间接操控pc,对的,就是通过压栈和弹栈实现,在程序执行发生断点时(调动子程序或中断),cpu会自动将pc的值进行压栈,返回断点时会自动将栈顶的值弹回pc,这就是关键,如果在弹回前,我们修改sp,不就可以间接操控pc了吗!这样就可以将cpu执行其他任务了;
3,人工堆栈:操作系统原理中有一点非常重要,就是上下文切换,所以每个任务必须有属于自己的堆栈,称为人工堆栈。人工堆栈的建立非常讲究,不能短也不能太长,短了会是溢出会可能修改其他任务的人工堆栈,产生调度紊乱。太长会浪费空间,尤其是像51这种硬件资源本就少的单片机。堆栈的空间的预留是通过数组来划分的。在建立任务时,要对堆栈初始化(这也很关键),将任务入口地址压到最底部(不同的单片机情况不同,这里以51为例,后面的也是),然后sp指向正确的堆栈位置(不同的单片机情况不同,要保存的寄存器个数不同),个人在设计中发现,为了不让sp越界,最好将堆栈最底部单元预留出来,避免浪费可以用来保存任务信息,比如堆栈使用情况。
void Task_Creak(void (*pfun)(void),INT8U *pStack,INT8U Task_ID) { INT8U *pSt; OS_ENTER_CRITICAL(); pSt=pStack; *(++pSt)=(INT16U)pfun; *(++pSt)=(INT16U)pfun>>8; os_tcb[Task_ID].OSTCBSP=(INT8U)pSt+13; OS_Task_List|=OSMapTbl[Task_ID]; os_tcb[Task_ID].OSTCBDly=0; OS_EXIT_CRITICAL(); }
4,任务控制块:和人工堆栈一样每个任务也有属于自己的任务控制块,根据系统需求成员定义不同,对于自由延时服务的os,只需要一个保存任务SP的成员变量和保存延时时间的成员变量。
typedef struct{ INT8U OSTCBSP; INT8U OSTCBDly; }OS_TCB;
5,系统时间:也叫时钟节拍,是系统的心脏,有硬件产生,51可以用定时器产生毫秒级中断。
void StartTicker(void) { TMOD=0x01; TH0=0x0d8; TL0=0x0f0; ET0=1; TR0=1; }
6,系统延时函数接口:用于任务延时,在延时的时候让cpu去执行其他任务,提高cpu的效率(通过实践,我个人觉得这也是软实时实现的缘由),在这个函数中要完成sp保存,将任务踢出就绪表,然后调度。
void OSTimeDly(INT8U ticks) { OS_ENTER_CRITICAL(); os_tcb[CurID].OSTCBDly=ticks; OS_Task_List&=~OSMapTbl[CurID]; OS_EXIT_CRITICAL(); OS_TASK_SW(); }
7,调度函数:有两种,一种是普通的调度,用于延时调度,所以要插入汇编语言前后分别将现场保护和现场恢复,还有就是完成获取最高任务和sp获取。一种是中断级别的调度,
用于中断服务程序,由于C语言编译成汇编时编译器会自动现场保护,所以只要在调度函数中只要现场恢复,还有就是在完成获取最高任务和sp获取前,要完成sp保存,将任务踢出就绪表。
void OS_TASK_SW(void) { INT8U i; EA=0; #pragma asm PUSH ACC PUSH B PUSH DPH PUSH DPL PUSH PSW MOV PSW,#00H PUSH AR0 PUSH AR1 PUSH AR2 PUSH AR3 PUSH AR4 PUSH AR5 PUSH AR6 PUSH AR7 #pragma endasm os_tcb[CurID].OSTCBSP=SP; for(i=0;i<MAX_TASK;i++) { if(OS_Task_List&OSMapTbl[i]) { break; } } CurID=i; SP=os_tcb[CurID].OSTCBSP; #pragma asm POP AR7 POP AR6 POP AR5 POP AR4 POP AR3 POP AR2 POP AR1 POP AR0 POP PSW POP DPL POP DPH POP B POP ACC #pragma endasm EA=1; #pragma asm RETI; #pragma endasm }
void TickInterrupt(void) { INT8U i; // SP-=2; for(i=0;i<MAX_TASK;i++) { if(os_tcb[i].OSTCBDly>0) { os_tcb[i].OSTCBDly--; if(os_tcb[i].OSTCBDly==0) OS_Task_List|=OSMapTbl[i]; } } SP-=2; os_tcb[CurID].OSTCBSP=SP; //OS_Task_List&=~OSMap[CurID]; for(i=0;i<MAX_TASK;i++) { if(OS_Task_List&OSMapTbl[i]) { break; } } CurID=i; SP=os_tcb[CurID].OSTCBSP; #pragma asm POP AR7 POP AR6 POP AR5 POP AR4 POP AR3 POP AR2 POP AR1 POP AR0 POP PSW POP DPL POP DPH POP B POP ACC #pragma endasm EA=1; #pragma asm RETI #pragma endasm }
8,sp控制:在调度的过程中,必须保证将任务断点的入口地址保存在堆栈最底部(预留单元上面),再次在任务调度过程中难免会调用其他函数再次压栈,而且可能不在会这个断点,
所以在被调用的这个程序中将sp下调两位。所以在os设计中sp的控制必须十分小心,不能任务调度一定会紊乱。
9.任务函数:os中必须存在一个不能主动申请调度的任务,称为系统任务,为了cpu在没有任何任务的时候有事可做,个人在实践中发现没有这种任务os会出错。
还有就是要把握任务集的可调度性(其中受任务优先级设计影响)。
10:就绪表:就绪表的设计要是至关重要,《操作系统原理》自阅。
最后:本人菜鸟一枚,有错见谅,谢谢观阅!
ps:以后有机会会贴上工程代码及效果图。