一、初识任务
引入FreeRTOS操作系统是因为需要其并发行,如何实现并发性就是通过多个任务实行抢占式调度来达到多个任务同时运行的假象,与单纯的裸机程序不同,操作系统中为每个任务都分配了一个时间片,但是FreeRTOS支持抢占式调度,由调度器来决定哪个任务先运行,哪个任务后运行,高优先级任务可以打断低优先级任务的运行,等待高优先级任务完成后,再将CPU使用权交出去,给低优先级任务,中断可以打断任务的运行。
二、任务优先级
由于Cortex-M系列支持前导指令,所以在FreeRTOS打开了宏定义configUSE_PORT_OPTIMISED_TASK_SELECTION,但是同时因为硬件原因也限制了它最多只能创建32个任务。
优先级数字越低代表其任务的优先级越低,最高优先级为configMAX_PRIORITIES-1的优先级数最高,空闲任务的优先级最低,为0.
当打开宏configUSE_TIME_SLICING时,系统支持多个任务共享一个优先级,数量不限,此时,系统就会使用时间片轮转调度器来为每个任务获取运行时间。
三、任务堆栈申请大小
当开发者采用静态创建任务时,需要自己分配堆栈大小,第三个选项为开辟的堆栈大小,这里需要注意的是ulStackDepth的数据类型是uint32_t,即四个字节,但是FreeRTOS申请空间的时候是以字节为单位的,所以申请的实际堆栈大小为数据的宽度*深度,即为原来的4倍。
四、任务创建
一个任务的创建过程主要为:
1.申请栈、任务控制块内存空间
2.初始化任务:
- 由于FreeRTOS的API是放在flash中,所以需要设置特权模式,调用API时会短暂切换到特权模式
- 初始化任务堆栈为0xa5,方便检测堆栈溢出,保存任务名字、初始化xStateListItem、xEventListItem,并设置归属者,优先级在这里是由configMAX_PRIORITIES-当前任务的优先级,得出来的数字越靠近configMAX_PRIORITIES,则优先级越小。
- 初始化内存单元、任务相关一些功能,返回任务句柄,其实是任务的我控制块
3.将新创建的任务加入到就绪列表中
- 任务加入到就绪列表中会进入一个临界区
- 判断当前系统中的运行任务数量,以及新创建的任务优先级与当前运行的任务优先级高低,如果当前任务的优先级比当前正在运行的任务优先级高,那么需要进行一次上下文切换。否则则将其加入到就绪列表中。
创建任务主要做得事情缩略图:
五、任务的切换
FreeRTOS设计初衷是在支持多任务运行的同时也能处理紧急事件。任务的切换是由滴答定时器来决定,所以这会导致在某个中断进行时,被滴答定时器破坏,而导致无法将紧急处理事件彻底处理完成就被强制切换至运行任务,这会导致不可预估的效果。
为解决这个问题,引入了PendSVC中断服务,RTOS在运行中断服务时,不进行上下文切换,上下文切换将在PendSVC中完成。在进入中断服务时不进行上下文切换,而是检查栈桢中的压栈xPSR或NVIC中的中断活跃状态寄存器。因为需要等待所有的ISR完成时再进行上下文切换,所以PendSVC的优先级必须设置为最低。FreeRTOS中涉及上下文切换最终都是使能PendSVC
上下文切换开关函数:
中断服务函数
PendSVC是由滴答定时器触发,滴答定时器触发触发时,会开启PendSVC中断,当进行任务上下文切换时,就会进入PendSVC服务函数
__asm void xPortPendSVHandler( void ) //PendSVC中断服务函数
{
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
PRESERVE8
mrs r0, psp //读取任务栈空间,并存放在R0中
isb
ldr r3, =pxCurrentTCB //获取当前任务的任务控制
ldr r2, [r3] //将当前任务控制块的地址保存在寄存器R2中
stmdb r0!, {r4-r11} /* Save the remaining registers. */
str r0, [r2] //将最新的任务栈顶指针保存在TCB控制块中
stmdb sp!, {r3, r14} //接下来要调用函数,防止r3(保存了当前任务控制块)的值被改变,以及保存了返回地址的R14
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0 //关闭中断
dsb
isb
bl vTaskSwitchContext //查找下一个要运行的任务
mov r0, #0 //开启中断
msr basepri, r0
ldmia sp!, {r3, r14} //此时的r3中的内容已改变,更新位最高优先级任务的控制块
ldr r1, [r3]
ldr r0, [r1] //获取新任务的运行堆栈栈顶
ldmia r0!, {r4-r11} //恢复现场
msr psp, r0 //更新任务栈指针
isb
bx r14
nop
}
在中断服务函数中主要完成的事情是获取到下一个要执行的任务,如何进行获取是通过bl跳转命令vTaskSwitchContext()函数
寻找下一个任务vTaskSwitchContext()
#define taskSELECT_HIGHEST_PRIORITY_TASK() \
{ \
UBaseType_t uxTopPriority = uxTopReadyPriority; //uxTopReadyPriority记录的是就绪态最高优先级 \
//pxReadyTasksLists是一个就绪任务列表
while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) ) \
{ \
configASSERT( uxTopPriority ); \
--uxTopPriority; \
} \
//从列表中找出下一个要运行的列表项 \
listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \
uxTopReadyPriority = uxTopPriority; \
} \
硬件计算: \
#define taskSELECT_HIGHEST_PRIORITY_TASK() \
{ \
UBaseType_t uxTopPriority; \
portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority ); \
configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 ); \
listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB,&( pxReadyTasksLists[ uxTopPriority ] ) );
\
}
两种方法都是步骤是一致的,先找出就绪任务列表中最高优先级,然后根据pcb控制块找出列表项。但是利用硬件计算前导0比通用方法快,原因在于利用硬件寻找下一个任务时,每个任务不再是每个不同的数值,而是在uxReadyPriorities中的每个位,而uxReadyPriorities是一个32位的变量,所以它只能存储32个优先级,这也是它的缺点。
举个例子:某个任务的优先级为31(实际是低优先级32,因为0也属于优先级范围),那么uxReadyPriorities就为1000 0000 0000 0000(第32位被标记为1),它前面的0的个数为0,所以32-0=32,他的优先级为32.
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) \
uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )
__clz指令需要硬件支持才能,同时也是因为它,计算效率才得以提高