手写RTOS
芯片内核简介
Cortex-M3使用的是“向下生长的满栈的模型”,采用双堆栈机制。
进入异常
- 硬件将xPSR、PC、LR、R12和R0-R3自动压入当前堆栈,其他寄存器根据需要由ISR自行保存。
- 从中断向量表取入口地址。
- SP: 入栈后保存最后的堆栈地址。
- PC: 更新为中断服务入口地址。
- LR: 更新为特殊的EXC_RETURN值
- 执行异常处理程序
退出异常
- 执行返回指令,如BX LR
- 恢复先前入栈的寄存器。出栈顺序与入栈时的相对应,堆栈指针的值也改回去。
- 从原中断发生位置继续往下运行。
PendSV异常
在PendSV异常中执行RTOS上下文切换(即任务切换)。
工作原理: 配置为最低优先级,上下文切换的请求将自动延迟到其他的ISR都完成后才处理,并且可被其他异常抢占。
基本任务切换实现
任务定义与切换原理
任务切换的本质:保存前一任务当前的运行状态,恢复后一任务之前的运行状态。
任务状态数据:
- 代码、数据区:由编译器自动分配,各个任务相互独立,并不冲突。
- 堆:不使用
- 栈: 不同任务的栈要分隔开
- 内核寄存器: 编译器会在某些时间将值保存到栈中,如函数调用, 异常处理。
- 其他的状态数据
保存任务状态数据方法:
为每个任务配置独立的栈,用于保存该任务的所有状态数据。
定义堆栈类型
// Cortex-M3的堆栈单元大小为32位
typedef uint32_t tTaskStack;
定义任务类型
typedef struct _tTask{
//tinyOS在运行该任务前,会从stack指向的位置处,读取栈中的环境参数恢复到cpu寄存器中,然后开始运行。
// 在切换至其他任务时,会将当前cpu寄存器中的值保存到栈中,等待下一次运行该任务时恢复。
// stack保存了最后保存环境参数的地址位置,用于后续恢复。
uint32_t *stack;
}tTask;
声明两个任务
tTask tTask1;
tTask tTask2;
tTaskStack task1Env[1024];
tTaskStack task2Env[1024];
任务切换的实现
切换至初始任务
taskTable[0] = &tTask1;
taskTable[1] = &tTask2;
nextTask = taskTable[0];
tTaskRunFirst();
void tTaskRunFirst()
{
// 这里设置了一个标记,PSP=0,用于与tTaskSwitck()区分,用于在PendSV中判断当前切换是TinyOS启动时切换至第一个任务,还是多任务已经跑起来后执行的切换。
__set_PSP(0);
MEM8(NVIC_SYSPRI2) = NVIC_PENDSV_PRI;
MEM32(NVIC_INT_CTRL) = NVIC_PENDSVSET;
// 这个函数没有返回值,一旦出发PendSV异常,将会在PendSV后进行任务切换,切换至第一个任务运行。
}
void tTaskSched()
{
if(currentTask == taskTable[0])
{
nextTask = taskTable[1];
}
else
{
nextTask = taskTable[0];
}
tTaskSwitch();
}
void tTaskSwitch()
{
MEM32(NVIC_INT_CTRL) = NVIC_PENDSVSET;
}
PendSV异常处理函数汇编实现
__asm void PendSV_Handler(void)
{
IMPORT currentTask
IMPORT nextTask
MRS R0, PSP
CBZ R0, PendSVHander_nosave
STMDB R0!, {R4-R11}
LDR R1, =currentTask
LDR R1, [R1]
STR R0, [R1] //任务状态的保存
PendSVHander_nosave
LDR R0, =currentTask
LDR R1, =nextTask
LDR R2, [R1]
STR R2, [R0]
LDR R0, [R2]
LDMIA R0!, {R4-R11}
MSR PSP, R0
ORR LR, LR, #0X04
BX LR
}
双任务时间片运行原理
上个小节中,任务的切换需要在每个任务函数中主动去调用,本节实现构建一个基于时间片去切换任务的系统。
涉及要点:
- 定时器配置
- 如何实现时间片切换
配置Systick
void tSetSysTickPeriod(uint32_t ms)
{
SysTick->LOAD = ms*SystemCoreClock/1000 - 1;
NVIC_SetPriority(SysTick_IRQn, (1<<__NVIC_PRIO_BITS)-1);
SysTick->VAL = 0;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk;
}
定时中断切换任务
void SysTick_Handler()
{
tTaskSched();
}
双任务延时原理与空闲任务
上一节的实现中,当代码执行到delay()函数时,CPU会停留在那里去等待,非常浪费性能,本节提供一个任务延时接口,使得延时过程中,当前任务放弃CPU使用权,转而运行其他任务,提高CPU利用率。
每个任务采用软计时器的方式
为每个任务添加计时器
typedef struct _tTask{
tTaskStask *stack;
uint32_t delay_Ticks;
}tTask;
当所有任务都进入延时状态时,这时需要添加一个空闲任务
tTask tTaskIdle;
tTaskStask idleTaskEnv[1024];
void idleTaskEntry()
{
for(;;)
{
}
}
在时钟节拍中断里递减计时器,并调用tTaskSched()切换任务
void tTaskSystemTickHandler()
{
for(int i = 0; i < 2; i++)
{
if(tTaskTable[i]->delay_Ticks > 0)
tTaskTable[i]->delay_Ticks--;
}
tTaskSched();
}
添加延时接口函数taskDelay()
void taskDelay(uint32_t delay)
{
currentTask->delayTicks = delay;
tTaskSched();
}
内核核心实现
临界区保护
中断嵌套时
中断控制寄存器 | 功能描述 |
---|---|
PRIMASK | 这是个一位寄存器,将它置1时,就关掉所有可屏蔽的异常,只剩下NMI和硬fault可以相应。他的缺省值是0,表示没有关中断。 |
进入临界区
uint32_t tTaskEnterCritical()
{
uint32_t primask = __get_PRIMASK();
__disable_irq();
return primask;
}
退出临界区
void tTaskExitCritical(uint32_t status)
{
__set_PRIMASK(status);
}
调度锁
- 上锁时,禁止任务进行切换。无论何种情况。例如时间片用完,也不切换任务。
- 解锁时,允许任务切换。
调度锁计数器初始化
void tTaskSchedInit()
{
uint8_t schedLockCount = 0;
}
调度器上锁
void taskSchedDisable()
{
uint32_t status = tTaskEnterCritical();
if(schedLockCount < 255)
{
schedLockCount++;
}
tTaskExitCritical(status);
}
调度器解锁
void taskSchedEnable()
{
uint32_t status = tTaskEnterCritical();
if(schedLockCount > 0)
{
schedLockCount--;
if(schedLockCount == 0)
{
tTaskSched();
}
}
tTaskExitCritical(status);
}
禁止调度
void tTaskSched()
{
uint32_t status = tTaskEnterCritical();
if (schedLockCount > 0)
{
tTaskExitCritical(status);
return ;
}
tTaskExitCritical(status);
}
思考,目前
- 只有两个任务,怎样支持多任务
- 任务占用cpu优先级相同,怎样支持多优先级
位图数据结构
位图是一组连续的标志位,每一位用来标识状态的有无
位图结构定义
typedef struct {
uint32_t bitmap;
}tBitmap;
位图初始化
void tBitmapInit(tBitmap *bitmap)
{
bitmap->bitmap = 0;
}
置1操作
void tBitmapSet(tBitmap *bitmap, uint32_t pos)
{
bitmap->bitmap |= (1 << pos);
}
置0操作
void tBitmapSet(tBitmap *bitmap, uint32_t pos)
{
bitmap->bitmap &= ~(1 << pos);
}
查找第一个置1的位置(从第0位开始)
uint32_t tBitmapGetFirstSet(tBitmap *bitmap)
{
if(bitmap->bitmap & 0xff)
{
return quickFindTable[bitmap->bitmap &0xff];
}
else if(bitmap->bitmap & 0xff00)
{
return quickFindTable[bitmap->bitmap &0xff00] + 8;
}
else if(bitmap->bitmap & 0xff0000)
{
return quickFindTable[bitmap->bitmap &0xff0000] + 16;
}
else if(bitmap->bitmap & 0xff000000)
{
return quickFindTable[bitmap->bitmap &0xff000000] + 24;
}
else
{
return 32;
}
}
多优先级任务
为任务指定优先级,优先级更高的任务,具备对CPU、事件和资源的优先处理权和占有权。
- RTOS维护一个就绪表,每个表项对应一个任务,对应一种优先级。就绪表指明哪些优先级的任务等待占用CPU运行。
- 为便于快速找到优先级更高的任务运行,使用了就绪位置标记就绪,快速查找
添加优先级字段
typedef struct _tTask{
tTaskStack *stack;
uint32_t delayTicks;
uint32_t prio;
}tTask;
添加优先级字段位图表
tBitmap taskPrioBitmap;
tTask *taskTable[TINYOS_PRO_COUNT];
修改调度算法
void tTaskSched()
{
tTask *tempTask;
uint32_t status = tTaskEnterCritical();
if (schedLockCount > 0)
{
tTaskExitCritical(status);
return ;
}
tempTask = tTaskHighestReady();
if(currentTask != tempTask)
{
nextTask = tempTask;
tTaskSwitch();
}
tTaskExitCritical(status);
}
当前的实现方法中,通过一个优先级下,只能有一个任务。使用链表可以实现一个优先级下多个任务。
双向链表数据结构
已知父结构,访问特定节点
已知节点如何得到父结构体的地址?
首先在地址0处定义复制一个父结构体,节点的地址就是偏移量。
代码实现
#define tNodeParent(node, parent, name) (parent*)((uint32_t)node - (uint32_t)&((parent*)0)->name) )
定义节点
typedef struct _tNode{
struct _tNode * pre;
struct _tNode * next;
}tNoed;
定义链表
typedef struct _tList{
tNode headNode;
uint32_t nodeCount;
}tList;
链表操作
void tNodeInit(tNode *node)
{
node->preNode = node;
node->nextNode = node;
}
void tListInit(tList *list)
{
list->headNode.preNode = &(list->headNode);
list->headNode.nextNode = &(list->headNode);
list->nodeCount = 0;
}
uint32_t tListCount(tList *list)
{
return list->nodeCount;
}
tNode* tListFirst(tList *list)
{
tNode* node = (tNode *)0;
if (list->nodeCount != 0)
{
return list->headNode.nextNode;
}
return node;
}
tNode* tListLast(tList *list)
{
tNode* node = (tNode*)0;
if (list->nodeCount != 0)
{
return list->headNode.preNode;
}
return node;
}
tNode* tListPre(tList* list, tNode* node)
{
if (node->preNode == node)
{
return (tNode*)0;
}
else
{
return node->preNode;
}
}
tNode* tListNext(tList* list, tNode* node)
{
if (node->nextNode == node)
{
return (tNode*)0;
}
else
{
return node->nextNode;
}
}
void tListRemoveAll(tList* list)
{
tNode* nextNode;
nextNode = list->headNode.nextNode;
for (uint32_t i = 0; i < list->nodeCount; i++)
{
tNode* currentNode =nextNode;
nextNode = nextNode->nextNode;
currentNode->nextNode = currentNode;
currentNode->preNode = currentNode;
}
list->headNode.nextNode = &(list->headNode);
list->headNode.preNode = &(list->headNode);
list->nodeCount = 0;
}
void tListAddFirst(tList* list, tNode* node)
{
// node->preNode = &(list->headNode);
// node->nextNode = list->headNode.nextNode;
// list->headNode.nextNode->preNode = node;
// list->headNode.nextNode = node;
node->preNode = list->headNode.nextNode->preNode;
node->nextNode = list->headNode.nextNode;
list->headNode.nextNode->preNode = node;
list->headNode.nextNode = node;
list->nodeCount++;
}
void tListAddLast(tList* list, tNode* node)
{
// node->nextNode = list->headNode.nextNode->preNode;
node->preNode = &(list->headNode);
node->preNode = list->headNode.nextNode;
list->headNode.preNode = node;
list->headNode.nextNode->nextNode = node;
list->nodeCount++;
}
tNode* tListRemoveFirst(tList* list)
{
tNode* node;
if (list->nodeCount != 0)
{
node = list->headNode.nextNode;
list->headNode.nextNode = node->nextNode;
node->nextNode->preNode = &(list->headNode);
list->nodeCount--;
}
return (tNode*)0;
}
void tListInsertAfter(tList* list, tNode* nodeAfter, tNode* nodeToInsert)
{
nodeToInsert->preNode = nodeAfter;
nodeToInsert->nextNode = nodeAfter->nextNode;
nodeAfter->nextNode->preNode = nodeToInsert;
nodeAfter->nextNode = nodeToInsert;
list->nodeCount++;
}
void tListRemove(tList* list, tNode* node)
{
node->preNode->nextNode = node->nextNode;
node->nextNode->preNode = node->preNode;
list->nodeCount--;
}
任务延时队列
RTOS维护一个就绪表,每个表项对应一个任务,对应一种优先级。就绪表指明哪些优先级的任务等待占用CPU运行。这种方式存在以下问题:
- 每次时钟节拍中断都需要扫描所有任务,比较耗时
- 不易支持同一优先级下多个任务
void tTaskSystemTickHandler()
{
uint32_t status = tTaskEnterCritical();
for(int i = 0; i < TINYOS_PRO_COUNT; i++)
{
if(tTaskTable[i]->delay_Ticks > 0)
tTaskTable[i]->delay_Ticks--;
else
tBitmapSet(&taskProBitmap,i);
}
tTaskExitCritical(status);
tTaskSched();
}
延时队列设计
将所有需要延时的任务单独放置在一个队列中,每次发生系统节拍,只需扫描该队列。
方式一:独立保存延时时间
-
插入延时任务比较简单
方式二:递增的延时队列 -
插入延时任务比较复杂
本系统采用方案一
延时队列的定义
tList tTaskDelayedList;
延时队列的插入
void tTimeTaskWait(tTask *task, uint32_t ticks)
{
task->delayTicks = ticks;
tListAddLast(&tTaskDelayedList, &(task->delayNode));
task->state |= TINYOS_TASK_STATE_DELAYED;
}
时钟节拍扫描延时队列
void tTaskSystemTickHandler()
{
uint32_t status = tTaskEnterCritical();
for(node = tTaskDelayedList.headNode.next; node != &(tTaskDelayedList.headNode); node = node.next)
{
tTask *task = (tTask *)tNodeParent(node, tTask, delayNode);
if(--task->delayTicks == 0)
tTimeTaskWakeUp(task); //将任务从延时队列移除
tTaskSchedRdy(task); //将任务恢复为就绪态
}
tTaskExitCritical(status);
tTaskSched();
}
同优先级时间片运行
构建一个允许多任务具备相同的优先级,且优先级相同的任务间按时间片占用CPU运行的系统。
配置优先级列表
//
tList taskTable[TINYOS_PRO_COUNT];
任务链接节点
typedef struct _tTask{
tTaskStack *stack;
tNode LinkNode;
uint32_t delayTicks;
tNode delayNode;
uint32_t prio;
uint32_t state;
uint32_t slice;
}tTask;
修改获取最高优先级任务的方式
tTask *tTaskHighestReady()
{
uint32_t highestPrio = tBitmapGetFirstSet(&taskPrioBitmap);
tNode *node = tListFirst(&taskTable[highestPrio]);
return (tTask *)tNodeParent(node, tTask, linkNode);
}
时钟节拍处理:增加时间片流转
void tTaskSystemTickHandler()
{
if(--currentTask->slice == 0)
{
if(tListCount(&taskTable[currentTask->prio]));
{
tListRemoveFirst(&taskTable[currentTask->prio]);
tListAddLast(&taskTable[currentTask->prio], &(currentTask->linkNode));
currentTask->slice = TINYOS_SLICE_MAX;
}
}
tTaskExitCritical(status);
tTaskSched();
}
任务管理模块实现
任务的挂起与唤醒
现有任务状态:
- 未创建:只定义了任务代码,未调用tTaskInit()初始化
- 就绪:任务已经创建完毕,且等待机会占用CPU运行
- 运行:任务正在占用cpu运行代码
- 延时:任务调用了tTaskDelay()
- 挂起:任务暂停运行
添加挂起计数器
tNode delayNode;
uint32_t prio;
uint32_t state;
uint32_t slice;
uint32_t suspendCount;
}tTask
挂起函数
void tTaskSuspend(tTask *task)
{
uint32_t status = tTaskEnterCritical();
if(!(task->state & TINYOS_TASK_STATE_DELAYED))
{
if(++task->suspendCount <= 1)
{
task->state |= TINYOS_TASK_SUSPEND;
tTaskSchedUnRdy(task);
if(task == currentTask)
{
tTaskSched();
}
}
}
tTaskExitCritical(status);
}
唤醒函数
void tTaskWakeUp (tTask * task)
{
// 进入临界区
uint32_t status = tTaskEnterCritical();
// 检查任务是否处于挂起状态
if (task->state & TINYOS_TASK_STATE_SUSPEND)
{
// 递减挂起计数,如果为0了,则清除挂起标志,同时设置进入就绪状态
if (--task->suspendCount == 0)
{
// 清除挂起标志
task->state &= ~TINYOS_TASK_STATE_SUSPEND;
// 同时将任务放回就绪队列中
tTaskSchedRdy(task);
// 唤醒过程中,可能有更高优先级的任务就绪,执行一次任务调度
tTaskSched();
}
}
// 退出临界区
tTaskExitCritical(status);
}
任务的删除
删除工作之一:将任务从所在队列中移除
删除工作之二:释放/关闭占用的资源
安全删除方式之一:设置清理回调函数,在强制删除时调用
安全删除方式之二:设置删除请求标志,由任务自己决定何时删除
添加删除清理和请求删除标志位函数
//任务被删除时调用的清理函数
void (*clean) (void *param);
//传递给清理函数的参数
void *cleanParam;
// 请求删除标志,非0表示请求删除
uint8_t requsetDeletFlag;
}tTask;
请求删除函数
void tTaskRequestDelet(tTask *task)
{
uint32_t status = tTaskEnterCritical();
// 设置清除删除标记
task->requestDeletFlag = 1;
tTaskExitCritical(status);
}
检查是否请求删除函数
uint8_t tTaskIsRequestedDelet(void)
{
uint8_t delet;
uint32_t status = tTaskEnterCritical();
// 获取请求删除标记
delet = currentTask->requsetDeletFlag;
tTaskExitCritical(status);
return delet;
}
强制删除函数
void tTaskForceDelete (tTask * task)
{
// 进入临界区
uint32_t status = tTaskEnterCritical();
// 如果任务处于延时状态,则从延时队列中删除
if (task->state & TINYOS_TASK_STATE_DELAYED)
{
tTimeTaskRemove(task);
}
// 如果任务不处于挂起状态,那么就是就绪态,从就绪表中删除
else if (!(task->state & TINYOS_TASK_STATE_SUSPEND))
{
tTaskSchedRemove(task);
}
// 删除时,如果有设置清理函数,则调用清理函数
if (task->clean)
{
task->clean(task->cleanParam);
}
// 如果删除的是自己,那么需要切换至另一个任务,所以执行一次任务调度
if (currentTask == task)
{
tTaskSched();
}
// 退出临界区
tTaskExitCritical(status);
}
任务的状态查询
状态结构的定义
typedef struct _tTaskInfo{
uint32_t delayTicks;
uint32_t prio;
uint32_t state;
uint32_t slice;
uint32_t suspendCount;
}tTaskInfo;
状态信息获取
void tTaskGetInfo(tTask *task, tTaskInfo *info)
{
uint32_t status = tTaskEnterCritical();
info->delayTicks = task->delayTicks;
info->prio = task->prio;
info->state = task->state;
info->slice = task->slice;
info->suspendCount = task->suspendCount;
tTaskExitCritical(status);
}
事件控制块实现
事件控制块的原理与创建
问题一:如何同步两个任务的运行
问题二:如何处理多个任务共享资源的冲突问题
问题三:如何在多个任务间传递消息通信
问题四:如何在中断ISR与任务之间传递多个事件标志
- 任务在事件控制块上等待,暂停运行
- 事件发生、通知事件控制块
- 事件控制块通知等待任务列表中的任务
事件控制块需实现两大功能:
- 任务进入事件控制块等待
- 发送事件给控制块恢复任务运行
定义事件控制块
typedef enum _tEventType{
tEventTypeUnknow = 0,
}tEventType;
typedef struct_tEvent{
tEventType type;
tList waitList;
}tEvent;
注明任务等待的事件参数
//任务正在等待的事件类型
tEvent *waitEvent;
//等待事件的消息存储位置
void *eventMsg;
//等待事件的结果
uint32_t waitEventResult;
}tTask;
事件控制块的初始化
void tEventInit(tEvent *event, tEventType type)
{
event->type = type;
tListInit(&event->waitList);
}
事件的等待与通知
现有任务状态切换图
事件控制块的等待与通知:任务进入事件控制块等待队列中暂停运行,事件发送时通知任务从队列移除继续运行。
- 等待事件:任务正在等待某个事件发生
事件控制块等待
void tEventWait(tEvent *event, tTask *task, void *msg, uint32_t state, uint32_t timeout)
{
uint32_t status = tTaskEnterCritical();
task->state |= state;
task->waitEvent = event;
task->eventMsg = msg;
task->waitEventResult = tErrorNoError;
tTaskSchedUnRdy(task);
tListAddLast(&event->waitList, &task->linkNode);
if(timeout)
{
tTimeTaskWait(task, timeout);
}
tTaskExitCritical(status);
}
时钟节拍处理中添加等待超时处理
void tTaskSystemTickHandler(void)
{
tNode *node;
uint32_t status = tTaskEnterCritical();
for (node = tTaskDelayedList.headNode.nextNode; node != &(tTaskDelayedList.headNode); node = node->nextNode)
{
tTask * task = tNodeParent(node, tTask, delayNode);
if (--task->delayTicks == 0)
{
if(task->waitEvent)
{
tEventRemoveTask(task, (void *)0, tErrorTimeout);
}
// 将任务从延时队列中移除
tTimeTaskWakeUp(task);
// 将任务恢复到就绪状态
tTaskSchedRdy(task);
}
}
// 检查下当前任务的时间片是否已经到了
if (--currentTask->slice == 0)
{
// 如果当前任务中还有其它任务的话,那么切换到下一个任务
// 方法是将当前任务从队列的头部移除,插入到尾部
// 这样后面执行tTaskSched()时就会从头部取出新的任务取出新的任务作为当前任务运行
if (tListCount(&taskTable[currentTask->prio]) > 0)
{
tListRemoveFirst(&taskTable[currentTask->prio]);
tListAddLast(&taskTable[currentTask->prio], &(currentTask->linkNode));
// 重置计数器
currentTask->slice = TINYOS_SLICE_MAX;
}
}
// 退出临界区
tTaskExitCritical(status);
// 这个过程中可能有任务延时完毕(delayTicks = 0),进行一次调度。
tTaskSched();
}