(4)从1开始写一个操作系统

第四章

任务就绪表

我们有任务之后为了方便切换的时候判断需要读取那个任务的属性进行比较,我们就需要任务就绪表来记录哪个任务是就绪的,我们可以通过查表来找到这个就绪的任务。

由于就绪表记录的状态只有就绪和未就绪两种,所以我们可以使用1位来表示一个任务的就绪状态。

假如说我们有8个任务,那么我们可以使用一个字节表示这8个任务的就绪状态,第0位就是任务0的就绪状态,以此类推。

代码中任务就绪表定义为

u8 os_rdy_tbl = 0;

任务控制块

既然我们有任务了,就需要一个结构来记录每个任务的属性,这样我们才能实现任务切换算法。下面我们参考ucos的任务控制块来实现我们自己的任务控制块。

我们需要先确定我们想要支持多少个任务,好用来分配多少个任务控制块来记录任务。

这个任务的数量不是凭空想象的,一定是有一些限制。上面做堆栈讲解的时候说过,stc单片机能够作为堆栈的大小只有256个字节,加入我们为每个任务分配20个字节堆栈(这样往往是不够的),那么我们就能有最多12个任务。但是还要考虑中断和函数执行过程中使用堆栈所以不可能有12个任务,那么我们选择8个任务吧,因为我们有个任务就绪表的概念,任务就绪表是8位为一个字节,所以任务尽量选择8的倍数为宜。

typedef struct os_tcb {
    void            *OSTCBStkPtr;           /* 指向堆栈栈顶的指针            */
#ifdef STACK_DETECT_MODE
    u8              OSTCBStkSize;           /* 堆栈的大小                 */
#endif
} OS_TCB;

暂时定义控制块中的数据有这些,之后需要在向里添加。

我们还需要一个能够记录当前运行的是哪一个任务的当前任务id记录变量。

u8 os_task_running_num = 0;

这样我们创建任务的函数功能就是将相应任务控制块进行初始化的过程。

//建立任务
u8 os_task_create(void (*task)(void), u8 taskID, u8 *pstack, u8 stack_size)
{
    if (taskID >= TASK_SIZE)
        return (OS_TASK_NUM_INVALID);
    if (os_tcb[taskID].OSTCBStkPtr != NULL)
        return (OS_TASK_NUM_EXIST);

#ifdef STACK_DETECT_MODE
{
    u8 i = 0;
    for (; i<stack_size; i++)
        pstack[i] = STACK_MAGIC;
}
#else
    stack_size = stack_size; //消除编译警告
#endif
    *pstack++ = (unsigned int)task; //将函数的地址高位压入堆栈,
    *pstack = (unsigned int)task>>8; //将函数的地址低位压入堆栈,
    pstack += 13;

    os_tcb[taskID].OSTCBStkPtr = pstack; //将人工堆栈的栈顶,保存到堆栈的数组中
    os_rdy_tbl |= 0x01<<taskID; //任务就绪表已经准备好

    return (OS_NO_ERR);
}

这个时候我们只是把控制块填充和就绪表更新,sp并没有切换到对应任务,所以我们还需要一个start函数。

//开始任务调度,从最低优先级的任务的开始
void os_start_task(void)
{
    os_task_running_num = 0;
    SP = (u8)os_tcb[0].OSTCBStkPtr - 13;
}

这样我们就能在调用函数之后进行任务切换。

这里启动的是任务0,也就是说这个操作系统必须有一个任务0,我们可以在后面把任务0作为空闲任务,在这里可以进行一些省电和统计任务,优先级最低。为了不让用户更改这个任务,我们可以增加任务初始化函数,在这里进行任务0的创建。同时也可以暴露回调函数进行空闲任务的用户接口。

idle_user idle_user_func = NULL;
//空闲任务
void idle_task_0(void)
{
    while (1) {
        if (idle_user_func)
            idle_user_func();
    }
}

#define TASK0_STACK_LEN 20
STACK_TYPE stack[TASK0_STACK_LEN]; //建立一个 20 字节的静态区堆栈
//操作系统初始化
void os_init(void)
{
    os_task_create(idle_task_0, 0, stack, TASK0_STACK_LEN);
}

这样我们只需要在main函数中这样调用。

void main(void)
{
    os_init();
    os_start_task();
}

就可以了。

到这里我们就构建起了最基本的只有idle任务的操作系统。

这里我们会发现最多可以定义8个任务,而且还有1个被空闲任务占用,也就是只留给了用户7个任务,这样的操作系统有什么意义呢?其实只要经过精心的设计,7个任务已经够用,除非有特殊要求的。

但是还是有解决办法,只是比较耗费系统资源,会增加调度开销。

方法我大致说一下思路,这里我们还是以学习为主,从最简单的做起,下面的这段话听不懂也不要求,在全部学完之后会对这段话有更好的理解。

前面我们说过,对于stc单片机来说影响任务个数的因素主要是堆栈的大小,那么如何能够解决呢?片内的ram只有256,但是片外的ram可是有3840字节的,这样比较片内资源要丰富许多。我们只需要在片内ram中开辟一个共享最大堆栈,例如200字节,每个任务的堆栈都是用片外ram保存,每次任务切换时我们都进行一次片内片外的交换,就可以实现我们的想法了。大致过程如下:

任务切换发生

->将片内ram数据拷贝到对应任务的片外ram上

经过任务调度,需要运行task2,我们就把task2的片外堆栈拷贝到片内堆栈中

然后我们设置好sp指针的值,这个在任务控制块中有栈顶指针做记录,之后退出调度函数就可以继续运行task2了。

大体思想就是这样,这里就不进行代码实现了,感兴趣的同学可以自己尝试实现以下。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章