目录
前言
先来了解一下什么是多任务操作系统
就像我们电脑,开机后可以同时运行很多的软件,并且看起来像是在同时的运行这些软件,我们可以把这些软件看成是一个个的任务;
但实际上在单核CPU的情况下是不能真正做到同时运行多个任务的,其本质是CPU的运行速度很快,可以通过快速的切换来执行每一个任务,这样在我们看起来它就是在同时运行多个任务了。
目前市场上也有很多运行在STM32上的操作系统,例如:UCOS,FreeRTOS,OneOS 等等,想要更深入的了解这些操作系统,我们可以尝试着自己来实现一个简单的多任务系统
任务的特性
当我们在使用单片机的时候,经常会碰到delay()这种空循环等待的函数,非常的浪费资源
我们这里的调度式任务就是在遇到这个函数的时候在后台为其计数,然后将程序指针切换到其他的准备好的任务来运行;
这样一来,我们的任务就有两种形态,运行中的任务和等待运行的任务,又因为我们的任务有多个,我们可以创建一个数组来保存任务的状态
//------------------------------RTOS.h------------------------------
/*最大任务数量*/
#define TASK_SIZE 200
void rtos_SetRdyTask(uint8_t TaskID); //设置任务为就绪态
void rtos_DelRdyTask(uint8_t TaskID); //设置任务为挂起态
void rtos_GetRdyTask(void); //获取任务就绪表中最高优先级的就绪任务
//------------------------------RTOS.c------------------------------
uint8_t TaskState[TASK_SIZE+1] = {0}; //任务状态列表0:挂起状态,1:就绪状态
uint8_t TaskRunID; //运行中任务的ID
uint8_t TaskNextID; //下一个就绪任务的ID
/**
* @brief 设置任务为就绪态
* @param[in] TaskID:任务ID
* @retval 无
*/
void rtos_SetRdyTask(uint8_t TaskID){TaskState[TaskID] = 1;}
/**
* @brief 设置任务为挂起态
* @param[in] TaskID:任务ID
* @retval 无
*/
void rtos_DelRdyTask(uint8_t TaskID){TaskState[TaskID] = 0;}
/**
* @brief 获取就绪表中最高优先级的ID
* @param[in] 无
* @retval 无
*/
void rtos_GetRdyTask(void)
{
uint8_t i;
for(i=0;i<TASK_SIZE;i++)
{
if(TaskState[i] == 1)
{
TaskNextID = i;
return;
}
}
TaskNextID = TASK_SIZE;
}
这里定义了一个用于记录任务状态的数组,数组的下标就是这个任务的ID,用1表示任务准备就绪,用0表示任务被挂起,这里因为数组的类型为uint8_t,最大的任务数量为255个,然后两个函数来控制这个函数的挂起状态和就绪状态,用rtos_GetRdyTask来找到这个表里面准备就绪的小的ID,也就是设备就绪的最高优先级的任务。
创建任务
目前我们的系统已经可以判断自己要运行哪一个任务了,所以接下来我们需要将这个函数创建出来,每个任务有一个属性,可以理解为这个任务的当前运行的程序指针(栈顶指针),因为任务内可能会使用到Delay()函数,这个也是做任务切换的核心,所以还需要一个变量来记录当前需要的延时时间,创建一个结构体用来保存这两个变量
//------------------------------RTOS.h------------------------------
/*定义任务控制块*/
typedef struct _TaskCtrBlock
{
uint32_t TCBStkPtr; //保存任务的堆栈顶
uint32_t TCBDlay; //保存任务延时时间
}TaskCtrBlock;
然后就可以编写创建任务的函数了
//------------------------------RTOS.c------------------------------
TaskCtrBlock TCB[TASK_SIZE+1]; //任务控制块列表
TaskCtrBlock *pTaskNow; //指向当前任务的指针
TaskCtrBlock *pTaskNext; //指向下一个任务的指针
/**
* @brief 创建任务
* @param[in] Task:需要创建任务的函数指针
* @param[in] p_Stack:任务栈的首地址
* @param[in] StackSize:任务栈的大小
* @param[in] TaskID:任务ID
* @retval 无
* @note 为函数注册一个私有的栈区
*/
void rtos_TaskCreate(void (*Task)(void),uint32_t *p_Stack,uint32_t StackSize,uint8_t TaskID)
{
p_Stack[StackSize-1] = (uint32_t)0x01000000L; //xPSR寄存器 24位THUMB必须置1 //Cortex-M3拥有两个堆栈指针,然而它们是banked,任一时刻只能使用其中的一个。
//主堆栈指针(MSP):复位后缺省使用的堆栈指针,用于操作系统内核以及异常处理(包括中断服务)。
//进程堆栈指针(PSP):由用户的应用程序代码使用。
p_Stack[StackSize-2] = (uint32_t)Task; //任务入口地址 //程序计数寄存器PC指向当前的程序地址。如果修改它的值,能改变程序的执行流。
//因为Cortex-M3内部使用了指令流水线,读PC时返回的值时当前指令的地址值+4,如:
//0x1000: MOV R0, PC ; R0 = 0x1004
p_Stack[StackSize-3] = (uint32_t)0xFFFFFFFEL; //R14 子函数返回寄存器,不会返回,用不到 //连接寄存器LR用于在调用子程序时存储返回地址。
//例如,在使用BL(分支变连接,Branch and Link)指令时,就自动填充LR的值。
p_Stack[StackSize-4] = (uint32_t)0x12121212L; //R12
p_Stack[StackSize-5] = (uint32_t)0x03030303L; //R3
p_Stack[StackSize-6] = (uint32_t)0x02020202L; //R2
p_Stack[StackSize-7] = (uint32_t)0x01010101L; //R1
p_Stack[StackSize-8] = (uint32_t)0x00000000L; //R0输出参数寄存器,用不到
p_Stack[StackSize-9] = (uint32_t)0x11111111L; //R11
p_Stack[StackSize-10] = (uint32_t)0x10101010L; //R10
p_Stack[StackSize-11] = (uint32_t)0x09090909L; //R9
p_Stack[StackSize-12] = (uint32_t)0x08080808L; //R8
p_Stack[StackSize-13] = (uint32_t)0x07070707L; //R7
p_Stack[StackSize-14] = (uint32_t)0x06060606L; //R6
p_Stack[StackSize-15] = (uint32_t)0x05050505L; //R5
p_Stack[StackSize-16] = (uint32_t)0x04040404L; //R4
TCB[TaskID].TCBStkPtr = (uint32_t)(&p_Stack[StackSize-16]); //保存堆栈地址 -- 该任务控制块中应当指向栈顶的指针 指向了该任务的新栈顶
TCB[TaskID].TCBDlay = 0; //初始化任务延时
rtos_SetRdyTask(TaskID); //将当前任务设置为就绪态
}
寄存器参考下图,详细部分请查看Cortex-M3权威指南的通用寄存器部分
任务切换
;/*引用外部变量*/
IMPORT pTaskNow ;当前任务控制块的指针
IMPORT pTaskNext ;下一个任务控制块的指针
;/*引用外部函数*/
EXPORT rtos_StartRtos ;来自rtos.h
EXPORT rtos_TaskSwitch ;来自rtos.h
EXPORT _PendSV_Handler ;来自startup_stm32f10x_***.s
;EQU 类似宏定义 将 NVIC_INT_CTRL 替换为地址 0xE000ED04
NVIC_INT_CTRL EQU 0xE000ED04 ;中断控制寄存器
NVIC_SYSPRI14 EQU 0xE000ED22 ;系统优先级寄存器(优先级14)
NVIC_PENDSV_PRI EQU 0xFF ;PendSV优先级(最低)
NVIC_PENDSVSET EQU 0x10000000 ;PendSV触发值
PRESERVE8
AREA |.text|, CODE, READONLY
THUMB
;****************************************
;调度第一个任务
;先设置PendSV的中断优先级
;然后开启中断,让程序指针进入PendSV中断
;后续在PendSV_Handler函数实现
;****************************************
rtos_StartRtos
;LDR将NVIC_SYSPRI14的地址赋值给R0
LDR R0,=NVIC_SYSPRI14
LDR R1,=NVIC_PENDSV_PRI
STRB R1,[R0]
MOVS R0,#0 ;任务堆栈设置为0
MSR PSP,R0 ;PSP清零,作为首次切换上下文标志
LDR R0,=NVIC_INT_CTRL
LDR R1,=NVIC_PENDSVSET
STR R1,[R0] ;触发PendSV异常
CPSIE I ;开中断
;****************************************
;以下两个函数是任务切换的实现
;1.
;
;****************************************
_PendSV_Handler ;PendSV异常处理函数
CPSID I ;关中断
MRS R0,PSP ;把PSP指针赋值给R0
CBZ R0,rtos_PendSV_Tackle ;如果SPS为0则跳到PendSV_Handler_Nosave
SUBS R0,R0,#0x20
STM R0,{R4-R11} ;手动入栈R4-R11
LDR R1,=pTaskNow ;这里入栈了很多寄存器,,,当前任务的指针
LDR R1,[R1] ;让TCB->TCBStkPtr=SP 指向新的栈顶
STR R0,[R1] ;TCB->TCBStkPtr
rtos_PendSV_Tackle
LDR R0,=pTaskNow
LDR R1,=pTaskNext
LDR R2,[R1]
STR R2,[R0] ;将R2的值写入到[R0]的地址中,实现TaskNow = TaskNext
LDR R0,[R2] ;将新的栈顶给R0,实现PSP= TaskNext->TCBStkPtr
LDM R0,{R4-R11} ;弹出R4-R11
ADDS R0,R0,#0x20
MSR PSP,R0 ;更新PSP;MSP为复位后缺省使用的堆栈指针,异常永远使用MSP,如果手动开启了PSP,那么线程使用PSP,否则也使用MSP
ORR LR,LR,#0x04; ;设置LR的位2为1,那么异常返回后,用户线程使用PSP
CPSIE I ;开中断
BX LR ;把LR寄存器的内容复制到PC寄存器里
rtos_TaskSwitch
LDR R0,=NVIC_INT_CTRL
LDR R1,=NVIC_PENDSVSET
STR R1,[R0] ;触发PendSV
BX LR
NOP
END
该部分参考UC/OS-II,基本上都有注释,主要流程为:
设置PendSV中断优先级,这个要设为最低。然后触发PendSV异常。在PendSV中断中把当前任务的现场数据保存在自己的任务栈里面,再把待运行的任务的数据从自己的任务栈装载到 CPU 中,改变 CPU 的 PC,SP,寄存器等。任务切换的关键是把任务的私有堆栈指针赋予处理器的堆栈指针 SP
调度任务
上文的汇编部分已经可以实现任务的切换了,切换的任务指针我们就可以直接在C中编写
/**
* @brief 任务调度
* @param 无
* @retval 无
* @note 搜索到下一个需要运行的任务并且切换到下一个可执行的任务
*/
void rtos_TaskSched(void)
{
rtos_GetRdyTask(); //获取下一个运行的任务
if(TaskNextID == TaskRunID)
return ;
TaskRunID = TaskNextID; //更新当前执行的任务ID
pTaskNext = &TCB[TaskRunID]; //准备调度的任务(该变量在汇编中调用)
rtos_TaskSwitch(); //任务切换
}
系统运行
以上准备工作就基本完成了,接下来我们就可以创建一个任务并运行它了,这个空闲任务的目的是使系统运行起来,在没有其他任务的情况下运行它
/**
* @brief 开始第一个任务
* @param 无
* @retval 无
* @note 创建并调用一个空闲任务
*/
uint32_t FreeTaskStack[64];
void rtos_TaskStart(void)
{
rtos_TaskCreate(rtos_FreeTask,FreeTaskStack,64,TASK_SIZE); //创建空闲任务
rtos_GetRdyTask(); //获取下一个运行的任务
TaskRunID = TaskNextID; //准备执行这个任务
pTaskNext = &TCB[TaskRunID]; //准备调度的任务(该变量在汇编中调用)
rtos_StartRtos(); //启动任务调度
}
/**
* @brief 一个空闲任务
* @param 无
* @retval 无
*/
static void rtos_FreeTask(void)
{
uint32_t FreeCount = 0;
while(1)
{
FreeCount++;
rtos_TaskSched();
}
}
delay()函数处理
以上可以对系统进行运行和调度了,但是还有一个比较重要的东西,延时函数,也就是切换任务的关键
/**
* @brief 任务延时
* @param[in] Time:需要延时的时间--该时间单位与系统心跳的时机一致
* @retval 无
* @note eg: 系统中断为1ms中断一次,则改函数的单位为ms
*/
void rtos_TaskDelay(uint32_t Time)
{
if(Time > 0)
{
rtos_DelRdyTask(TaskRunID); //挂起当前任务
TCB[TaskRunID].TCBDlay = Time; //设置当前任务需要延时的时间
rtos_TaskSched(); //进行任务切换
}
}
/**
* @brief 定时器中断对任务延时处理函数
* @param[in] 无
* @retval 无
* @note 如果任务需要使用延时函数,则需要在定时器中断中调用该函数
*/
void rtos_TaskTicks(void)
{
uint8_t i;
for(i=0;i<= TASK_SIZE;i++)
{
if(TCB[i].TCBDlay != 0)
{
TCB[i].TCBDlay--;
if(TCB[i].TCBDlay==0)
{
rtos_SetRdyTask(i);
}
}
}
}
然后我们的操作系统的调度器部分就可以运行起来了