回顾:
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