【编写自己的RTOS】搞定任务调度

回顾:

Q3:关于RTOS编写,要解决哪些核心问题
A3:
 a. 系统心跳:SysTick初始化
 b. SysTick和PendSV的优先级设置
 c. 任务控制块TCB结构与堆栈初始化
 d. 上下文切换:PendSV_Handler函数
 e. 系统延时函数(阻塞,调度)
 f. 任务调度:SysTick_Handler函数

理清问题关键所在后,开始动手。

 首先,最基本的功能要先保证,如SysTick初始化,任务堆栈初始化,上下文切换;
 然后,开始时步子不能迈太大,我当时就差点裂开了。开始时只想着时间片轮转调度,选择了循环队列来管理列表,结果发现延时列表不能适用,它需要将延时短的任务先取出;
  然后又想到用min_heap来实现优先级队列,它好像很适合动态管理数据,结果因为要判断”叶“的位置,需要额外的空间,多次malloc后失败,因为Heap_Size只有0x00000200 = 512字节(搞来搞去,结构体指针数组初始化,非法访问,数据被破坏,,,)。最后直接用一个结构体指针数组+任务状态来管理了。先搞出来,然后才能谈其它数据结构,优化调度算法。

上面说的有几个问题:(当时刚接触操作系统和数据结构不久,望体谅)
1、512字节应该是可以实现小顶堆的,在数组式堆中辅助长度可以判断左右孩子是否存在(leftchild = 2*i + 1) < len;没有孩子节点或父节点优先权最小时,下沉结束;(只有动态分配的TCB才占用heap,一个任务控制块32字节,理论上可创建管理16个任务)
2、Heap_size可以在startup_stm32f10x_md.s中设置,注意几个地方:芯片具体Flash容量,Target中的IROM1的size,和代码实际使用的内存大小(Project.map)。


1、SysTick

SysTick的作用
 通过SysTick异常周期性地切入系统,进行任务调度。 (SysTick 的最大使命,就是定期地产生异常请求,作为系统的时基。 OS 需要这种“滴答” 来推动任务和时间的管理。—— CM3权威指南)

关于SysTick,我们需要知道什么?
 所有Cortex-M3芯片内部都包含了SysTick定时器(简化了在CM3器件间的软件移植工作),该定时器的时钟源可以是内部时钟,或者是外部时钟。
SysTick定时器是一个24位的倒计数定时器,当计数到0时,将从RELOAD寄存器中自动重载定时器初值,开始新的一轮计数。STM32的延时一般就是通过内部的SysTick来实现的。

STM32基础例程中有关SysTick定时器的初始化设置,可以在delay.c文件(源自正点原子FreeRTOS例程)中查看。delay_init()中也有关于SysTick时钟源的说明:SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK); 由此可知,SysTick的时钟源是HCLK。

HCLK :AHB总线时钟,由系统时钟SYSCLK分频得到,一般不分频,等于系统时钟(72MHZ),HCLK是高速外设时钟,是给外部设备的,比如内存,flash,DMA。

SysTick控制及状态寄存器:

初始化代码:

void SysTick_Init(void)
{
	char *Systick_priority = (char *)0xe000ed23;	//Systick中断优先级寄存器
	*Systick_priority = 0x00;           			//设置SysTick中断优先级最高
	SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK);//选择外部时钟  HCLK
	SysTick->LOAD  = ( SystemCoreClock / configTICK_RATE_HZ) - 1UL;	//定时周期1ms
	SysTick->VAL   = 0;                            	//Systick定时器计数器清0
	//SysTick->CTRL: bit0-定时器使能 bit1-中断使能 bit2-时钟源选择(=1系统主频,=0系统主频/8)
	SysTick->CTRL = 0x7;	//选择外部时钟,允许中断,开启定时器
}

敲重点:
①SysTick->CTRL: bit0-定时器使能 bit1-中断使能 bit2-时钟源选择(=1系统主频,=0系统主频/8);
重装载值计算原理:系统时钟频率为72MHz,1秒钟计数72M次;现SysyTick时钟为72M,但只计数72M/1000次(计数范围:72M/1000-1 ~ 0),可得1ms(1/1000秒)重装载一次。


2、PendSV

PendSV作用:任务切换时保存上下文(将一些寄存器和变量的值保存到任务堆栈,或将任务堆栈出栈,恢复过去任务的运行环境)

将任务堆栈出栈,就是将要运行的任务的堆栈的栈顶赋给sp,只有知道了sp我们才能找到对应的任务堆栈,才能找到堆栈保存任务信息;
恢复任务过去的运行环境(如通过恢复PC的值,我们可以知道上一次这个任务运行到哪了)

悬起PendSV的方法是:手工往NVIC的PendSV悬起寄存器中写1。

int* NVIC_INT_CTRL= (int *)0xE000ED04;  //中断控制寄存器地址
void SetPendSV(void)//挂起PendSV
{
	*NVIC_INT_CTRL=0x10000000;//1<<28
}

③PendSV的中断优先级

void PendSVPriority_Init(void)
{
	char* NVIC_SYSPRI14= (char *)0xE000ED22;  		//PendSV优先级寄存器地址
	*NVIC_SYSPRI14=0xFF;							//设置PendSV中断优先级最低 
}	

3、TCB与任务堆栈
######task.h文件######

typedef enum
{
	eTask_Running = 0,
	eTask_Ready,
	eTask_Suspended,
	eTask_Blocked,
	eTask_Deleted,
}eTaskSta;

#define OS_MAX_TASK 8	//最大任务数量
typedef struct _TaskControlBlock
{
	stk32 *StkPtr;
	char name[16];
	int state;	//任务状态
	int prio;
	int DlyTim; //任务阻塞时间
}TCB,*pTCB;


######task.c文件######

pTCB TASK_LIST[OS_MAX_TASK];
int created_task_num = 0;//全局变量

//任务堆栈初始化
//入栈顺序:栈顶->栈尾 xPSR,PC,LR,R12,R3-R0,R4-R11共16个(SP(R13)保存在TCB首个成员变量中)。
stk32* task_stk_init(void* func, stk32 *TopOfStack)
{
    stk32 *stk;
    stk = TopOfStack;
    *(stk)    = (u32)0x01000000L;//xPSR 程序状态寄存器,xPSR T位(第24位)必须置1,否则第一次执行任务时进入Fault中断                                                     
    *(--stk)  = (u32)func;   //PC   初使化时指向任务函数首地址(任务切换时,可能指向任务函数中间某地址)            
	*(--stk)  = (u32)0xFFFFFFFEL;//LR   保存着各种跳转的返回连接地址,初使化为最低4位为E,是一个非法值,主要目的是不让使用R14,即任务是不能返回的              
	stk-=13;	
    return stk;
}

void create_new_task( void *func, char name[], int prio, stk32 *TopOfStack, pTCB *tcb)
{
	int name_len = strlen(name);
	irq_disable();
	
	if(created_task_num == OS_MAX_TASK)
	{
		printf("Create Task Fail\r\n");
	}
	
	if(tcb)
	{
		*tcb = (pTCB)malloc(sizeof(TCB));
		if(*tcb)
		{
			(*tcb)->StkPtr = task_stk_init(func,TopOfStack);
			memcpy((*tcb)->name,name,sizeof(char)*name_len);
			(*tcb)->state = eTask_Ready;
			(*tcb)->prio = prio;
			(*tcb)->DlyTim = 0;	
			TASK_LIST[created_task_num] = *tcb;
			created_task_num++;
		}
	}
	irq_enable();
}

//数字越大,任务优先权越高
extern pTCB pTCB_IDLE;
pTCB GetHighRdyTask(void)
{
	int i = 0;
	pTCB pTtmp ,pTrdy = pTCB_IDLE;
	for(i = 0; i < created_task_num; i++)
	{
		if(TASK_LIST[i]->state == eTask_Ready)
		{
			pTtmp = TASK_LIST[i];
			if(pTtmp->prio > pTrdy->prio)
				pTrdy = pTtmp;
		}
	}
	return pTrdy;		
}

敲重点:
①“stk32 *StkPtr;”必须放在任务控制块的首位;
②入栈顺序与PendSV保存和恢复现场过程有关
③没有另外维护就绪、延时列表,只用了一个结构体指针数组,根据任务的状态来管理任务调度


4、上下文切换:PendSV_Handler函数
	IMPORT pTCB_Cur
	IMPORT pTCB_Rdy	
	EXPORT PendSV_Handler
	EXPORT SP_INIT
		
	PRESERVE8                  ;//字节对齐关键词,指定当前文件八字节对齐。
	AREA |.text|, CODE, READONLY ;//定义一个代码段或数据段。
	THUMB                      ;//指定以下指令都是THUMB指令集(ARM汇编有多种指令集)

SP_INIT  					   ;初始化PSP指针                      
    CPSID    I                 ;//关闭全局中断 
	
    LDR R4,=0x0                ;//R4装载立即数0(不直接给PSP赋值0而是经进R寄存器作为媒介是因为PSP只能和R寄存器打交道)            
    MSR     PSP, R4            ;//PSP(process stack pointer)程序堆栈指针赋值0。PSP属用户级(特级权下为MSP),双堆栈结构。 
	
    CPSIE    I                 ;//打开全局中断(此时若没有其他中断在响应,则立即进入PendSV中断函数)  
    BX    LR  
    
;/******************PendSV_Handler************/
PendSV_Handler
    CPSID    I                            ; OS_ENTER_CRITICAL();
	
    MRS     R0, PSP                            ; R0 = PSP;
    CBZ     R0, PendSV_Handler_NoSave          ; if(R0 == 0) goto PendSV_Handler_NoSave;
    
    SUB     R0, R0, #0x20            ; R0 = R0 - 0x20;
    
    ; easy method
    STM     R0, {R4-R11}
    
    LDR     R1, =pTCB_Cur            ; R1 = OSTCBCur;
    LDR     R1, [R1]                 ; R1 = *R1;(R1 = OSTCBCur->OSTCBStkPtr)
    STR     R0, [R1]                 ; *R1 = R0;(*(OSTCBCur->OSTCBStkPrt) = R0)
 
PendSV_Handler_NoSave				;每次都会进去(因为PendSV_Handler_NoSave不是函数,而是中间的一个标签,用于跳转)

	;实质就是pTCB_Cur = pTCB_Rdy
	;每次运行PendSV_Handler,都会使pTCB_Cur指向pTCB_Rdy,所以调度时只需从任务数组中获取pTCB_Rdy
    LDR     R0, =pTCB_Cur           ; R0 = OSTCBCur;
    LDR     R1, =pTCB_Rdy           ; R1 = OSTCBNext;
    LDR     R2, [R1]                ; R2 = OSTCBNext->OSTCBStkPtr;
    STR     R2, [R0]                ; *R0 = R2;(OSTCBCur->OSTCBStkPtr = OSTCBNext->OSTCBStkPtr)
    
    LDR     R0, [R2]                 ; R0 = *R2;(R0 = OSTCBNext->OSTCBStkPtr)
    LDM     R0, {R4-R11}
    ADD    R0, R0, #0x20
 
    MSR     PSP, R0                 ; PSP = R0;(PSP = OSTCBNext->OSTCBStkPtr)
    ORR     LR, LR, #0x04           ; LR = LR | 0x04;
	
    CPSIE     I                     ; OS_EXIT_CRITICAL();
    BX    LR                        ; return;                                       ; Enable interrupts at processor level
	
	align 4                    ;//内存对齐指令(编译器提供的),以4个字节(32位)对齐
    end                        ;//伪指令,放在程序行的最后,告诉编译器编译程序到此结束

kernel.asm汇编代码可以不用自己实现,但要理解它的流程与作用;这里有几个要注意的点:
①PendSV_Handler在kernel.asm中定义了,需要注释掉原来的函数(在stm32f10x_it.h)
②PendSV_Handler_NoSave不是函数,而是中间的一个标签,用于跳转
③每次运行PendSV_Handler,都会使pTCB_Cur指向pTCB_Rdy,所以调度中只需操作pTCB_Rdy
④SP_INIT是PSP指针初始化函数,用汇编写的,在其它地方被调用


5、OS_Start与系统延时函数(阻塞,调度)

void OS_Start(void)
{
	PendSVPriority_Init();	
	SysTick_Init();

	pTCB_Rdy = GetHighRdyTask();
	pTCB_Rdy->state = eTask_Running;
	SP_INIT();
	SetPendSV();
	while(1);//等待调度
	
}

//任务创建举例:
create_new_task(Print_Task,"Print_Task",1,&STK_PRINT_TASK[STK_SIZE-1],&pT2);
void Print_Task(void)
{
	while(1)
	{
		printf("print task\r\n");
		OSDelayTicks(1000);
	}
}


void OSDelayTicks(int ticks)
{
	pTCB_Cur->state = eTask_Blocked;
	pTCB_Cur->DlyTim = ticks-1;
	OS_Schedule();
	while(pTCB_Cur->DlyTim != 0);//不能是while(1),否则下次任务解阻塞后,继续运行while(1);
}

6、任务调度:SysTick_Handler函数

void OS_Schedule(void)
{	
	pTCB pT = GetHighRdyTask();
	//检测是否需要任务切换,如果需要则挂起PendSV中断
	if(pT != pTCB_Cur)
	{
		if(pTCB_Cur->state == eTask_Running)
			pTCB_Cur->state = eTask_Ready;
		
		pTCB_Rdy = pT;
		pTCB_Rdy->state = eTask_Running;
	
		SetPendSV();
	}
}

void SysTick_Handler(void)
{	
		int i = 0;	
		os_cpu_interrupt_disable();
		if(GetTaskNum(eTask_Blocked) != 0)//延时任务列表中是否有阻塞任务
		{
			for(i = 0; i < created_task_num; i++)
			{
				if(TASK_LIST[i]->state == eTask_Blocked)
				{
					if(TASK_LIST[i]->DlyTim == 0)//延时时间到了,解除阻塞
					{
						TASK_LIST[i]->state = eTask_Ready;
					}
					else
						TASK_LIST[i]->DlyTim--;
				}
			}
		}

		if(GetTaskNum(eTask_Ready) != 0)
			OS_Schedule();
		
		os_cpu_interrupt_enable();
}

踩了不少坑,终于成功迈出了第一步:编写了一个具有多任务功能的RTOS;后续还有很多可以改动的地方(可参考FreeRTOS的机制)

①基础:
 如用双向链表(或其它数据结构)优化调度和管理,不阻塞调度的延时函数,阻塞调度的绝对延时函数,补充挂起和删除的操作。
②进阶:
 内存管理、共享资源(消息队列、信号量、任务通知)的访问、中断管理等。

结束:

  这个是去年5月份做的一个小项目,后面去学Linux系统编程了,也就没有再去花时间和精力将其继续扩展完善。这里将当时的项目过程和笔记分享出来,算是抛转引玉吧。希望能帮助大家对M3内核和RTOS调度有更好的理解。

附上:实现多任务调度的demo

  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值