打印

用一个春节写完一个RTOS,稳定+可靠【连载】

[复制链接]
10517|42
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
hjz007|  楼主 | 2014-2-5 23:06 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 hjz007 于 2014-2-6 13:33 编辑

一、 除夕那天

看了今年的春晚,啊,看得直起**皮疙瘩,想吐。
你看看,什么《老阿姨》,什么《我的中国梦》,歌词真的恶心。“粪岛”拍马屁的动机也太露吧...

郁闷至于,无聊至极,心血来潮,大年春节晚上就顺便写了个RTOS,原本是很简单的,就是个任务调度,当时是想实现的功能是:把24MHZ的CPU分成16个虚拟CPU,即16个互不相干的任务。

没想到的是居然春晚没演完,就写完了,一测,还真是稳定,没啥问题。
鞭**叽歪叽歪的响,我的卧室对着的就是公园广场,冲天**一个一个的爆,老百姓那个欢乐劲,比登月还开心。
再想想,这样的分时功能也太土了,还是改成和ucos类似的RTOS吧。
但要:比ucosII要功能复杂些,任务调度要更灵活。
再要:资源占用要适合CORTEX M0,M3, M4。51就别管了,用了这么多年51,我觉得有CORTEX M0就可以了。
更要:简单,ucos的用户接口太“过时”了。我想啊,还是借鉴Microsoft的吧,比如一个Wait()打遍天下,对所有的同步机制接口都归一化。
特色:增加Wait3(),类似WaitMulti()即同时等待多个信号,这是很有用的。
无聊:TCPIP,USB,CAN 485这些都是很有用的,应该是不能放入OS中的。以后当功能包来扩吧。
困惑:要不要加GUI呢?现在都没想好。先暂时搁置吧,等用200MHZCPU的时候再说。

反正也睡不了,继续写好了。加点什么呢?
1.先来点Sleep吧。就是Delay咯,不过我觉得Sleep的叫法更加地道。
2.再来点临界区的管理吧。
3.再来点事件管理吧,标志寄存器A,SEMPHORE,MUTEX都给加了。
4.再测吧。死掉了...

功能:16个任务,内存占用小于100字节(特别针对CORTEX M0而设计)。
4个任务优先级,每个优先级里采用轮训调度。
程序代码:小于8K(现在是6K,还没优化)。

(先写这些吧,大家有兴趣的话,我继续写 大年初一,大年初二...,也想把源代码贴出来)
评分
参与人数 2威望 +6 收起 理由
zhangyang86 + 2
niuyaliang + 4

相关帖子

沙发
dong_abc| | 2014-2-5 23:29 | 只看该作者
定制一套自己风格的系统还是很爽的,不过我现在首选keil rtx.

使用特权

评论回复
板凳
dirtwillfly| | 2014-2-6 09:11 | 只看该作者
mark一下,关注

使用特权

评论回复
地板
hjz007|  楼主 | 2014-2-6 10:53 | 只看该作者
先从最简单的开始吧
OS的本质就是任务调度。即把一个CPU虚拟成多个CPU,各个CPU之间尽量独立,最好是谁都不碍谁。当然,如果真的谁都不碍谁,那OS就变得没有意义了。为了减少或者简化各个虚拟CPU之间的关联,就必须要设计一套同步机制,用于用简单不易出错的方式让各个虚拟CPU之间交换信息。最简单的就是FLAG通信了。
先不说同步机制,就说如何虚拟各个CPU吧。
这个也简单,就是不断保存寄存器,恢复寄存器,如此而已。
现在假设已经有了两个任务,一个为gpxTcbRunning,表示正在运行的任务,另外一个gpxTcbComming,表示将要运行的任务。最简单的OS就是将这两个任务不断的切换。
gpxTcbRunning和gpxTcbComming是两个指针,也就是指向两个任务控制块,控制块是什么呢?暂时先理解为任务管理的聚宝盆吧,即和任务相关的信息全部可以通过控制块获取得到,啥也不少,当然一般也没必要多余。
任务切换在CORTEX M系列的处理器里,可以利用PendSV中断来调度。中断服务程序代码如下:
.global HOSPsvHandler
.func HOSPsvHandler, HOSPsvHandler
.thumb_func
HOSPsvHandler:
    LDR     R3,=gpxTcbRunning
    LDR     R1,[R3]
    LDR     R2,=gpxTcbComming
    LDR     R2,[R2]

    CMP     R1,R2
    BEQ     exitPendSV
    MRS     R0, PSP
  
    SUBS    R0,R0,#32
    STR     R0,[R1]
        STMIA   R0!,{R4-R7}
    MOV     R4,R8
    MOV     R5,R9
    MOV     R6,R10
    MOV     R7,R11
    STMIA   R0!,{R4-R7}

    popStk:
    STR     R2, [R3]
    LDR     R0, [R2]
       
    ADDS    R0,R0,#16
    LDMIA   R0!,{R4-R7}
    MOV     R8,R4
    MOV     R9,R5
    MOV     R10,R6
    MOV     R11,R7
    SUBS     R0,R0,#32
    LDMIA   R0!,{R4-R7}
    ADDS     R0,R0,#16
    MSR     PSP, R0

    exitPendSV:
    BX      LR
.endfunc

先写到这里吧,尿急...

使用特权

评论回复
5
mmuuss586| | 2014-2-6 11:23 | 只看该作者
哈哈

使用特权

评论回复
6
icecut| | 2014-2-6 12:37 | 只看该作者
;P

使用特权

评论回复
7
dirtwillfly| | 2014-2-6 13:08 | 只看该作者
hjz007 发表于 2014-2-6 10:53
先从最简单的开始吧
OS的本质就是任务调度。即把一个CPU虚拟成多个CPU,各个CPU之间尽量独立,最好是谁都不 ...

;P尿急不用给大家汇报的

使用特权

评论回复
8
motodefy| | 2014-2-6 13:22 | 只看该作者
膜拜下·····

使用特权

评论回复
9
hjz007|  楼主 | 2014-2-6 13:29 | 只看该作者
HOSPsvHandler 所做的工作就是切换上下文,我们知道,OS有个调度的时钟,叫SysTick,我把我自己写的这个操作系统叫HOS,因为确实不知道给它取什么名字比较好,就把名字的第一个字母H加上,叫HOS吧。能表达意思就可以了。
中间插一句,所有的函数都前面加有HOS,比如SysTick的ISR程序,就叫HOSSysTickHandler。
.global HOSSysTickHandler
.func HOSSysTickHandler, HOSSysTickHandler
.thumb_func
HOSSysTickHandler:
        PUSH        {LR}
        BL        HOSTaskScheduleBySysTick
        LDR     R3,=NVIC_INT_CTRL
        LDR     R3,[R3]
        LDR     R2,=NVIC_PENDSVSET
        LDR     R1,[R2]
        STR     R1, [R3]
        POP                {PC}
.endfunc

这个HOSSysTickHandler是个永不停息的中断,周期一班取1ms到10ms,它做的工作很简单,就是调用一下HOSTaskScheduleBySysTick函数。这个函数负责任务调度,即什么优先级先调度,有那些任务可以调度。确定好需要调度的程序以后,然后做一些准备工作,那些工作实际上都可以在HOSSysTickHandler里处理的,不过HOSSysTickHandler是汇编写的,写起来比较郁闷,就在HOSTaskScheduleBySysTick函里用C语言准备好切换的工作。其中准备gpxTcbRunning和gpxTcbComming是当务之急了。
HOSSysTickHandler调用完HOSTaskScheduleBySysTick之后,触发PENDSVC调用中断。也就会调用HOSPsvHandler这个ISR了。
记得HOSPsvHandler代码里的使用了两个gpxTcbRunning和gpxTcbComming变量吧,这两个变量就是在HOSTaskScheduleBySysTick准备好的。

简单总结一下:
1、周期定时器启动HOSSysTickHandler
2、HOSSysTickHandler启动HOSTaskScheduleBySysTick之后再启动HOSPsvHandler
3、HOSPsvHandler启动两个任务的上下文切换。

1和2都说过了,比较简单吧, 下面要重点解释HOSPsvHandler的工作了,实际上也没多少东西,就是保存寄存器,恢复寄存器而已。
(待续)

使用特权

评论回复
10
a20084666| | 2014-2-6 20:35 | 只看该作者
厉害,赞一个

使用特权

评论回复
11
zd420325| | 2014-2-7 08:52 | 只看该作者
果然是电工

使用特权

评论回复
12
yangyiping| | 2014-2-7 09:23 | 只看该作者
赞一个先。

使用特权

评论回复
13
lvyunhua| | 2014-2-7 09:48 | 只看该作者
顶一个先

使用特权

评论回复
14
hjz007|  楼主 | 2014-2-7 10:55 | 只看该作者
本帖最后由 hjz007 于 2014-2-7 11:02 编辑

继续开工哦。

其实,写一个RTOS没花多少时间,比如就1天时间吧,但调试测试就花了不下5天。任务调度的各种问题,需要测试才能发现,估计爱因斯坦这样的人,如果他是程序员的话,也不可能一开始就把所有的问题想周全。

要玩那个任务调度, 自然第一个事情是要弄清楚ARM的异常处理过程,分别为进入过程和退出过程。当然还有退出是的异常过程...
这方面的资料在ARM EXCEPTION MODEL会详细的讲述。现在来看看简单异常入栈的过程:
if CONTROL.SPSEL == '1' && CurrentMode == Mode_Thread then
        frameptralign = SP_process<2>;
        SP_process = (SP_process - 0x20) AND NOT(ZeroExtend('100',32));
        frameptr = SP_process;
else
        frameptralign = SP_main<2>;
        SP_main = (SP_main - 0x20) AND NOT(ZeroExtend('100',32));
        frameptr = SP_main;
        /* only the stack locations, not the store order, are architected */
        MemA[frameptr,4] = R[0];
        MemA[frameptr+0x4,4] = R[1];
        MemA[frameptr+0x8,4] = R[2];
        MemA[frameptr+0xC,4] = R[3];
        MemA[frameptr+0x10,4] = R[12];
        MemA[frameptr+0x14,4] = LR;
        MemA[frameptr+0x18,4] = ReturnAddress();
        MemA[frameptr+0x1C,4] = (xPSR<31:10>:frameptralign:xPSR<8:0>);
        if CurrentMode==Mode_Handler then
                LR = 0xFFFFFFF1;
        else
                if CONTROL.SPSEL == '0' then
                        LR = 0xFFFFFFF9;
                else
                        LR = 0xFFFFFFFD; 从上面的描述可以看出,异常进入的时候,CPU自动保存了R0~R3, R12, LR, PC, xPSR八个寄存器。同时特别注意,LR的值被赋予一个用于中断返回的特殊值:0xFFFFFFF1,0xFFFFFFF9,0xFFFFFFFD,分别标识CPU进入异常以前的运行模式和堆栈使用情况。如果在中断服务例程里调用了BXL, BL,则LR这个值是需要保存下来的,等需要退出中断服务程序的时候再取出来用。





使用特权

评论回复
15
hjz007|  楼主 | 2014-2-7 11:14 | 只看该作者
现在再来看任务切换的过程:
HOSPsvHandler:
    LDR     R3,=gpxTcbRunning
    LDR     R1,[R3]
    LDR     R2,=gpxTcbComming
    LDR     R2,[R2]

    CMP     R1,R2   //如果gpxTcbComming、gpxTcbRunning相同,就不必要再切换上下文了。
    BEQ     ExitPendSV
    MRS     R0, PSP   //取出正在运行任务的堆栈PSP,后面的工作就是往PSP里灌数据了,保存16个寄存器。上面提到CPU自动保存了8个,还需要手动保存R4~R11
  
    SUBS    R0,R0,#32
    STR     R0,[R1]
        STMIA   R0!,{R4-R7} //保存4个寄存器, R4~R7
    MOV     R4,R8
    MOV     R5,R9
    MOV     R6,R10
    MOV     R7,R11
    STMIA   R0!,{R4-R7}  //保存4个寄存器,R8~R11

    STR     R2, [R3] //用gpxTcbComming更新替换gpxTcbRunning
    LDR     R0, [R2] //R0加载的就是gpxTcbRunning这个指针指向的内容了,不过值已经变成了gpxTcbComming的东西
       
    ADDS    R0,R0,#16 //从gpxTcbRunning实际上已经是(gpxTcbComming)取输出,恢复各个寄存器,同上。
    LDMIA   R0!,{R4-R7}
    MOV     R8,R4
    MOV     R9,R5
    MOV     R10,R6
    MOV     R11,R7
    SUBS     R0,R0,#32
    LDMIA   R0!,{R4-R7}
    ADDS     R0,R0,#16
    MSR     PSP, R0 //这里取出的是即将运行任务的堆栈数据,恢复PSP

ExitPendSV:
    LDR     R3,=NVIC_EXIT_TH_PSP  //返回。返回是CPU自动退栈,把对应位置的数据压入PC,LR等...
        LDR     R0, [R3]
    BX      R0

使用特权

评论回复
16
acebinger| | 2014-2-7 15:55 | 只看该作者
楼主威武,技术强大,文风也彪悍啊

使用特权

评论回复
17
Small_Road| | 2014-2-8 11:55 | 只看该作者
强大.....

使用特权

评论回复
18
guangbiao| | 2014-2-8 16:17 | 只看该作者

使用特权

评论回复
19
hjz007|  楼主 | 2014-2-8 19:41 | 只看该作者
本帖最后由 hjz007 于 2014-2-8 20:06 编辑

任务切换就差不多说明白了, 具体的需要大家看代码,以后再贴代码出来。
剩下的就是任务调度调度了。
为了简化学习的复杂性,不妨假设只有两个任务。那么调度程序可能就是:
void HOSTaskScheduleBySysTick()
{
    if (gpxTcbRunning == &gxTcb[0])
    {
           gpxTcbComming == &gxTcb[1];
    }
    else
    {
           gpxTcbComming == &gxTcb[0];
    }
}
上面的调度,就是两个任务的切换,一个为gxTcb[1],另一个为gxTcb[0],这个gxTcb是个任务块数组,最大也就16个。
哦,白痴也知道这样的调度程序自然毫无意义,如果不是为了学习。
实际上的调度代码长达1000多行,放在这里自然不合适,我估计得写个PDF文件才能说清楚。

那么gxTcb[0],gxTcb[1]里面的东西从哪儿来呢? 那当然是在OS运行起来之前都已经准备好了的,比如在main的第一行语句就可以调用HOSInitTask()。
main()
{
   HOSInitTask(); //当然,也可能包括了CreateTask()...之类的。总之,就是准备工作,你得准备好一切,才能GO...
   .....
}
当然,实际情况负杂的多,而且 HOSInitTask()也不是在main函数里调用的,二是在main函数之前就调用了。大家都知道,main函数作为C语言的默认入口函数是不太严谨的说法,实际上编译连接程序在调用main之前,还调用了给C语言运行环境进行初始化的代码。说得更直接点,就是连接器可以通过一个开关,自己设定真正的入口函数,在51里就是.org 0。在ARM里是通过用高级的连接器了,就是把复位函数的地址写入复位向量表的特定位置中。一帮的用ENTRY()指定你的复位响应函数。
不知道会不会有人问,既然把复位函数的地址写入了向量表的复位位置,干嘛还要ENTRY()呢?
因为连接器会把符号表进行管理,比如F1调用F2了,如果你用ENTRY(F1)指定了,则F1和F2都可以连接进来,如果你指定ENTRY(F2),则F1不会被连接,被“和谐”掉了。所以要用ENTRY()把入口函数指定,这样就可以擒贼先擒王,提纲挈领,把所有该要的函数都连接进来,不好的函数统统走开。

使用特权

评论回复
20
hjz007|  楼主 | 2014-2-8 19:53 | 只看该作者
现在再来看看任务调度块里面有什么吧。

typedef  struct HOST_TCB
{
    HOST_STK        *pxStk;       //堆栈指针,
        U32                        u32Delay; //Sleep需要用的数据
        U8                        u8Head;  //TCB双向链表指针
        U8                        u8Tail;
        unsigned int u4IsrCnt                :4;  //ISR的管理计数
        unsigned int u3Flag                 :3; //标记SLEEP,EVENT是否有阻塞,多了一比特,还没想好是用来喂狗还是干点其它的
        unsigned int u2State                 :2; //任务状态,只有4种,READY, WAITING, RUNNING, NULL(仙逝)
        unsigned int u2Prio                  :2; //优先级,只有4中
        unsigned int u1Block                :1; //这个可算是我的独创了,这个比特用来通过外部停止任务,便于调试
        unsigned int u4TaskId                 :4; //任务ID,从0到15(最大)
} HOST_TCB,*PHOST_TCB;

gpxTcbRunning实际上就是一个TCB块的指针,由于pxStk就是块的第一个元素,因此,gpxTcbRunning也是pxStk的指针。
看看上面的代码,HOSPsvHandler任务切换函数里LDR     R3,=gpxTcbRunning语句实际上取的就是pxStk,在汇编里写代码去全部分析TCB结构体块?在现代社会里,估计没有人会像比尔·盖茨那样蠢。比尔·盖茨可是用汇编写出来个QBASIC语言哦,按现在的标准,简直就是愚笨+臭犟。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

37

主题

372

帖子

5

粉丝