自己写一个基于STM32的多任务操作系统

目录

前言

任务的特性

创建任务

 任务切换

调度任务

系统运行

delay()函数处理


前言

先来了解一下什么是多任务操作系统

        就像我们电脑,开机后可以同时运行很多的软件,并且看起来像是在同时的运行这些软件,我们可以把这些软件看成是一个个的任务;
但实际上在单核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);
			}
		}
	}
}

然后我们的操作系统的调度器部分就可以运行起来了

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值