GD32实战21__编写一个OS

OS编写目的

1. 让大家明白OS原理
2. 编译原理及程序运行原理入门
3. Cortex M3指令集等基础知识入门

OS功能列表

1. 任务切换

  1. 任务创建函数 OS_TASK_CreateTask
  2. 默认任务每10ms切换一次,可通过宏OS_TASK_SWITCH_INTERVAL配置
  3. 最多支持OS_TASK_ID_MAX个任务,可修改宏达到配置最多任务数,实际任务数是OS_TASK_MAX+1个,因为系统默认启动idle任务
  4. 任务优先级最多支持OS_TASK_PRIORITY_MAX,数值越小优先级最高,且不会出现相同优先级的两个任务
  5. 轮询式

2. 延时等待

  1. 延时函数 OS_TASK_TaskDelay
  2. 延时单位毫秒(ms)
  3. 考虑到优先级和任务切换,该函数表示任务至少需要延时多少毫秒
  4. 延时期间,任务阻塞

3. 任务通信

  1. 通过事件位图实现通信目的
  2. 每个任务最多支持32个事件
  3. 任务与任务之间,任务与中断之间都可以相互通信
  4. 事件的创建和清除必须手工处理
  5. 事件未到达之前,任务阻塞

OS使用说明

​ 下面是操作系统任务相关的函数接口

/*==================================================================
* Function	: OS_TASK_CreateTask
* Description	: 创建新任务
* Input Para	:     
    IN U8 id,  任务号,同任务优先级,每个任务唯一
    IN TaskFunction_t taskHandle,  任务函数,任务入口
    IN U16 taskStackDeep,  任务的最大堆栈深度,sizeof(StackSize_t)*taskStackDeep=实际占内存字节数
    IN U32 *eventBitMap  任务事件位图,用于任务通信,不需要可以填NULL
* Output Para	: 无
* Return Value: 
    OS_OK    创建成功
    OS_ERROR 创建失败
==================================================================*/
extern S32 OS_TASK_CreateTask
(
    IN U8 id, 
    IN TaskFunction_t taskHandle, 
    IN U16 taskStackDeep, 
    IN U32 *eventBitMap
);

/*==================================================================
* Function	: OS_TASK_SchedulerTask
* Description	: 启动任务调度
* Input Para	: 无
* Output Para	: 无 
* Return Value: 无
==================================================================*/
extern VOID OS_TASK_SchedulerTask(VOID);

/*==================================================================
* Function	: OS_TASK_TaskDelay
* Description	: 用于阻塞任务等待超时
* Input Para	: IN U16 ms  阻塞毫秒数
* Output Para	: 无
* Return Value: 无
==================================================================*/
extern VOID OS_TASK_TaskDelay(IN U16 ms);

/*==================================================================
* Function	: OS_TASK_WaitForEvent
* Description	: 用于阻塞任务等待事件
* Input Para	: 无
* Output Para	: 无
* Return Value: 无
==================================================================*/
extern VOID OS_TASK_WaitForEvent(VOID);

OS原理分析

​ 首先,需要理解CPU是如何运行程序的,然后,理解操作系统是如何完成任务切换的。下面我们从这3个问题出发去理解:

  1. 代码编译后发生了什么呢?

    答:当我们打开map文件时,我们就会理解,代码编译连接的过程其实是把我们写每一行代码都映射到代码空间上地址上的一个过程,最终生产的bin文件就是代码段的完全映射。如下图

    GD32实战21__编写一个OS_第1张图片

  2. 把编译好的bin文件烧录到CPU上,CPU发生了什么?

    答:烧录过程,只是把bin文件完整的写入到Flash上而已

  3. 上电后,CPU又产生了什么变化?

    答:

    1. 从CPU的起始地址开始,如图,M3内核的CPU会从0x00000004开始运行代码GD32实战21__编写一个OS_第2张图片
    2. 图中示范了一个最简单的例子,每步的执行,实际只是PC指针的调整和相应寄存器赋值取值的过程GD32实战21__编写一个OS_第3张图片GD32实战21__编写一个OS_第4张图片
  4. 上面的例子可以理解,单个任务时CPU是如何运作的,那么当多个任务时,我们只要把上面用到的寄存器和堆栈,每个任务复制一份,独立存储访问,然后切换PC指针和堆栈指针就可以完成任务的调度切换。如下图GD32实战21__编写一个OS_第5张图片

OS设计说明

1. 任务状态切换

如下图,任务必须严格按这三个状态切换

GD32实战21__编写一个OS_第6张图片

2. 任务创建流程

  1. 该代码运行在系统特权级线程模式,堆栈使用MSP
  2. 申请任务控制块
  3. 申请堆栈内存,并赋值栈顶指针,注意:malloc的内存返回地址是内存的起始地址,而堆栈是向下生产的,所以栈顶指针赋值时必须加上堆栈深度
  4. 初始化堆栈空间,必须严格按Cortex M3的入栈要求执行
  5. 任务状态置为ready
static StackSize_t* TASK_TaskStackFirstInit(IN StackSize_t *topStack, IN TaskFunction_t func)
{
    /* 按堆栈地址顺序入栈,而非寄存器入栈顺序
     * PSR,PC,LR,R12,R3,R2,R1,R0 以上是芯片自动入栈的
     * R4,R5,R6,R7,R8,R9,R10,R11 以上手工入栈,入出栈顺序注意保持一致
     * 此处也可以增加计数,用于堆栈溢出检查
     */
    topStack--;
    *topStack = OS_TASK_INITIAL_XPSR;
    topStack--;
    *topStack = (((StackSize_t)func) & OS_TASK_START_ADDRESS_MASK);
    topStack--; /* 任务栈初次初始化,已是最上层了,返回即错,因此可以增加返回函数用户调试 */
    topStack -= 5; /* 可用于函数入参 */
    topStack -= 8;
    return topStack;
}

S32 OS_TASK_CreateTask
(
    IN U8 id, 
    IN TaskFunction_t taskHandle, 
    IN U16 taskStackDeep, 
    IN U32 *eventBitMap
)
{
    TCB_S *newTcb = NULL;
    StackSize_t *topStack = NULL;
    
    if (id >= OS_TASK_MAX)
    {
        return OS_ERROR;
    }
    
    newTcb = (TCB_S*)malloc(sizeof(TCB_S));
    if (NULL == newTcb)
    {
        return OS_ERROR;
    }
    
    newTcb->state = TASK_INIT;
    topStack = (StackSize_t *)malloc(sizeof(StackSize_t)*taskStackDeep);
    if (NULL == topStack)
    {
        return OS_ERROR;
    }
    topStack += sizeof(StackSize_t)*taskStackDeep;

    newTcb->topStack = TASK_TaskStackFirstInit(topStack, taskHandle);
    
    newTcb->state = TASK_READY;
    newTcb->delay = 0;
    newTcb->delayMax = 0;
    newTcb->eventBitMap = eventBitMap;
    newTcb->id = id;

    gTaskTcbList[id] = newTcb;
    
    return OS_OK;
}

3. 任务首次调度流程

​ 该流程的目的是把CPU的控制权,有特权级线程模式切成用户级线程模式,根据OS原理分析得知,需要做如下处理

  1. 找到当前优先级最高且处于Ready状态的任务,即gCurrentTCB指向的任务

  2. 触发svc 0,进入SVC中断,此时处于handler模式

  3. PSP指向当前任务的堆栈指针

  4. 利用LR寄存器异常返回特性,返回到线程模式使用线程堆栈,完成CPU控制权交接给当前任务,如图GD32实战21__编写一个OS_第7张图片

    __asm static VOID TASK_SvcHandler(VOID)
    {
        extern gCurrentTCB;
    
        /* 任务相关内容映射到入线程栈 */
    	ldr	r3, =gCurrentTCB
    	ldr r1, [r3]
    	ldr r0, [r1]
    	ldmia r0!, {r4-r11}
        msr psp, r0
        isb
    
        /* 利用LR寄存器异常返回进入线程模式特性 */
    	mov r14, #0xfffffffd
    	bx r14
        nop
    }
    
    void SVC_Handler(void)
    {
        TASK_GetCurrentTask();
        TASK_SvcHandler();
    }
    
    __asm static VOID TASK_StartFirstTask(VOID)
    {
        /* 触发svc,在svc中断中通过修改LD寄存器值的方式进入线程模式 */
        svc 0
        nop
        nop
    }
    
    VOID OS_TASK_SchedulerTask(VOID)
    {   
        TASK_StartFirstTask();
        return;
    }
    
    

4. 任务调度切换流程

​ 当存在多个任务时,每隔任务都需要轮流取得CPU控制权,从而达到并行运作的效果,目前设计的是每隔10ms切换一次,实现流程如下:

  1. 配置systick为1ms触发一次中断
  2. 每10ms触发一次PendSV中断,使用PendSV进行任务上下文切换可以完美避开中断中发生任务切换的问题,必须注意:PendSV中断优先级必须最低
  3. PendSV中断中,做如下事情
    1. 把当前任务入栈,主要是R4-R11,因为其它已自动入栈
    2. 切换任务上下文,注意堆栈保存,R3, r14需要重新恢复
    3. 使用新任务栈返回
#define TASK_NVIC_INT_CTRL_REG		( * ( ( volatile uint32_t * ) 0xe000ed04 ) )
#define TASK_NVIC_PENDSVSET_BIT		( 1UL << 28UL )
/* ICSR寄存器bit28置1,触发PendSV中断 */
#define OS_TASK_SWITCH  TASK_NVIC_INT_CTRL_REG = TASK_NVIC_PENDSVSET_BIT

VOID SysTick_Handler(VOID)
{
    TASK_DelayList(); /* 本例中忽略 */
    TASK_WaitForEventList(); /* 本例中忽略 */
    gTaskSysTickCount++;
    if ((gTaskSysTickCount%OS_TASK_SWITCH_INTERVAL) != 0)
    {
        return;
    }

    OS_TASK_SWITCH;
}

__asm VOID PendSV_Handler(VOID)
{
    extern gCurrentTCB;
    extern TASK_GetCurrentTask;
            
    /* 把当前任务入栈,主要是R4-R11,因为其它已自动入栈 */
    mrs r0, psp
    isb
    stmdb r0!, {r4-r11}
    dsb
    isb

    /* 把堆栈地址映射到TCB */
    ldr r3, =gCurrentTCB
    ldr r2, [r3]  /* r2 = gCurrentTCB*/
    str r0, [r2]  /* 把r0赋值给gCurrentTCB->topStack */

    /* 切换任务上下文,注意堆栈保存,R3, r14需要重新恢复*/
    stmdb sp!, {r3,r14}
    dsb
    isb
    bl TASK_GetCurrentTask
	ldmia sp!, {r3,r14}
    dsb
    isb

    /* 获取新任务栈 */
	ldr r1, [r3]
	ldr r0, [r1]
	ldmia r0!, {r4-r11}
    dsb
    isb
    msr psp, r0
    isb

	bx r14
    nop
}

5. 任务超时阻塞等待流程

​ 经常需要某任务等待一段时间后再继续运行,即延时,其中等待的这段时间内,其它任务可以运行,从而充分利用CPU资源,流程如下:

  1. 保存需要等待的时间到任务控制块中,任务状态置成SUSPENDED,并释放CPU控制权

  2. 在systick 中断中,轮询每个任务,检测是否超时,

    1. 如果超时,则将任务状态置成READY,触发调度
    2. 如果没有到时间,则状态保持SUSPENDED
    VOID OS_TASK_TaskDelay(IN U16 ms)
    {
        if ((0 == gCurrentTCB->delay) && (0 == gCurrentTCB->delayMax))
        {
            gCurrentTCB->delayMax = ms;
            gCurrentTCB->delay = gTaskSysTickCount;
            gCurrentTCB->state = TASK_SUSPENDED;
            OS_TASK_SWITCH;
        }
    }
    
    static VOID TASK_DelayList(VOID)
    {
        volatile TCB_S *tmpTcb = NULL;
        U8 id = 0;
    
        for (id = 0; id < OS_TASK_MAX; id++)
        {
            tmpTcb = gTaskTcbList[id];
            if (NULL == tmpTcb)
            {
                continue;
            }
            
            if (tmpTcb->delayMax != 0)
            {
                if ((gTaskSysTickCount - tmpTcb->delay) >= tmpTcb->delayMax)
                {
                    tmpTcb->delay = 0;
                    tmpTcb->delayMax = 0;
                    tmpTcb->state = TASK_READY;
                    OS_TASK_SWITCH;
                    return;
                }
                else
                {
                    tmpTcb->state = TASK_SUSPENDED;
                }
            }
        }
        
        return;
    }
    

6. 任务事件阻塞等待流程

​ 经常会遇到这样的需求,一个任务期待在另一个任务触发了某事件后,才执行后续操作,在等待期间,CPU控制权由其他任务占用,提供CPU利用率。流程如下:

  1. 事件需要用户申请一个U32类型的全局变量,注意是位图,在任务创建时,填入到任务创建接口的eventBitMap参数,注意,必须传地址。extern S32 OS_TASK_CreateTask(IN U8 id, IN TaskFunction_t taskHandle, IN U16 taskStackDeep, IN U32 *eventBitMap);

  2. 在当前任务中需要等待的地方,调用OS_TASK_WaitForEvent函数等待,当事件满足时,该函数后面的代码才会被执行

  3. OS会在在systick 中断中,轮询每个任务,检测是否收到事件,

    1. 如果收到,则任务状态置成READY,并触发任务切换
    2. 如果没收到,则继续保持SUSPENDED状态
    VOID OS_TASK_WaitForEvent(VOID)
    {
        if (NULL == gCurrentTCB->eventBitMap)
        {
            return;
        }
        
        if (0 == *gCurrentTCB->eventBitMap)
        {
            gCurrentTCB->state = TASK_SUSPENDED;
            OS_TASK_SWITCH;
        }
    }
    
    static VOID TASK_WaitForEventList(VOID)
    {
        volatile TCB_S *tmpTcb = NULL;
        U8 id = 0;
    
        for (id = 0; id < OS_TASK_MAX; id++)
        {
            tmpTcb = gTaskTcbList[id];
            if (NULL == tmpTcb)
            {
                continue;
            }
            
            if (NULL == tmpTcb->eventBitMap)
            {
                continue;
            }
            
            if (*tmpTcb->eventBitMap != 0)
            {
                tmpTcb->state = TASK_READY;
                OS_TASK_SWITCH;
            }
            else
            {
                tmpTcb->state = TASK_SUSPENDED;
            }
        }
    }
    

7. 任务优先级设计

​ 任务总有先后,优先级必不可少,期望当多个任务都进入READY时,可以优先执行优先级最高的任务,当最高优先级的任务转为SUSPENDED状态后,在执行次优先级的任务,用如此简单的设定实现优先调度的目的,如下:

  1. 在任务切换,获取任务上下文时,会调用下面的函数,该函数会从头开始变量任务,找到第一个READY状态的任务,使它进入RUNNING状态,获取CPU控制权。

  2. 可见,优先级同任务ID,ID越小优先级越高

    VOID TASK_GetCurrentTask(VOID)
    {
        volatile TCB_S *tmpTcb = NULL;
        U8 id = 0;
    
        for (id = 0; id < OS_TASK_MAX; id++)
        {
            tmpTcb = gTaskTcbList[id];
            if ((TASK_READY == tmpTcb->state) || (TASK_RUNNING == tmpTcb->state))
            {
                tmpTcb->state = TASK_RUNNING;
                gCurrentTCB = tmpTcb;
                break;
            }
        }
        
        return;
    }
    

需要储备以下知识

  1. 熟悉CPU架构
  2. 熟悉指令集
  3. 熟悉操作系统原理

参考资料

《Cortex-M3权威指南Cn.pdf》

《GD32F10xCH_V1.1.pdf》

代码路径

https://github.com/YaFood/GD32F103/tree/master/TestOS

你可能感兴趣的:(ARM)