目录
任务创建
有静态创建和动态创建两种方式:
静态创建:
xTaskCreateStatic( prvIdleTask, /*任务入口*/
configIDLE_TASK_NAME, /*任务名称*/
ulIdleTaskStackSize, /*任务栈大小*/
( void * ) NULL, /*任务形参*/
portPRIVILEGE_BIT,
pxIdleTaskStackBuffer, /*任务栈起始地址*/
pxIdleTaskTCBBuffer ); /*任务控制块*/
动态创建:
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, /*任务函数*/
const char * const pcName, /*任务名称 */
const configSTACK_DEPTH_TYPE usStackDepth, /*任务堆栈大小 */
void * const pvParameters, /*传递给任务函数的参数 */
UBaseType_t uxPriority, /*任务优先级 */
TaskHandle_t * const pxCreatedTask ) /*任务句柄 */
参数:
pxTaskCode:指向具体的任务函数的函数指针,需要注意以下几点:
- 任务函数的函数名是任意的
- 任务函数的形参名必须是pvParameters
- 任务函数不允许以任何方式返回,即在函数中不能存在returm,也不能执行到函数末尾
- 允许在任务函数中创建其他任务
- 每个任务函数都有属于自己的栈空间,其大小由usStackDepth参数指定
- 同一个任务函数可以被多次创建,但它们都有各自的栈空间,且栈空间变量不会相互影响,但用static修饰的变量,将被各任务所共享。
pcName:指任务名称,可以通过修改 configMAX_TASK_NAME_LEN 的值来定义pcName的最大长度,默认是16
usStackDepth:指任务堆栈大小。freertos内核在每个任务创建时,都会为其分配固定的栈空间,所以usStackDepth的作用时通知内核该任务需要分配多大的栈空间。
注意:usStackDepth是指字数,而不是字节数。且不能超过configSTACK_DEPTH_TYPE 定义类型变量的最大值。
pvParameters:即在任务创建时,需要传递给任务函数的参数。如果不需要传入参数,则可以设置为NULL。
uxPriority:指任务初始的优先级,值越小,优先级越低。
注:FreeRTOS允许创建相同优先级的任务,同时也允许在程序执行过程中修改任务的优先级。
pxCreatedTask:即任务句柄,用来操作已经创建的任务,例如改变任务优先级、删除等操作。
返回值:
如果任务创建成功,则返回pdPASS;
如果任务创建失败,则返回相应的错误码。
注:创建成功后,任务会被加入就绪链表中,进入就绪态。
两者区别:
静态创建任务的堆栈是由用户自己传递。动态创建任务的堆栈是由系统分配。
静态创建任务时需要传递预先定义好的任务控制块指针; 动态创建任务时自动返回一个指针指向系统动态分配的一块内存。
任务控制块
任务控制块是任务三要素之一,每个任务都有对应的任务控制块,里面存储了任务的所有信息:例如任务的栈顶指针、任务名称……
调度机制
任务状态:
- 就绪态
- 运行态
- 阻塞态:等待某件事
- 挂起态:休息去了
状态之间的切换:
(1)创建任务→就绪态:任务创建完成后直接进入就绪态,表明任务已准备就绪,只等待调度器进行调度就可以运行。
(2)就绪态→运行态:发生任务切换时,就绪列表中最高优先级的任务被执行,从而进入运行态。
(3)运行态→就绪态:有更高优先级任务创建或者恢复后,会发生任务调度,此刻就绪列表中最高优先级任务变为运行态,原先运行的任务由运行态变为就绪态,在就绪列表中,等待最高优先级的任务运行完毕继续运行原来的任务(此处可以看做是 CPU 使用权被更高优先级的任务抢占了)。
(4)运行态→阻塞态:正在运行的任务发生阻塞(任务被挂起、任务被延时、等待信号量、读写队列或等待读写事件)时,该任务会从就绪列表中删除,任务状态由运行态变成阻塞态,然后发生任务切换,运行就绪列表中当前最高优先级任务。
(5)阻塞态→就绪态:阻塞的任务被恢复后(任务恢复、延时时间超时、读信号量超时或读到信号量等),此时被恢复的任务会被加入就绪列表,从而由阻塞态变成就绪态;如果此时被恢复任务的优先级高于正在运行任务的优先级,则会发生任务切换,将该任务将再次转换任务状态,由就绪态变成运行态。
(6)就绪态、阻塞态、运行态→挂起态:通过调用 vTaskSuspend() API 函数可以将处于任何状态的任务挂起,被挂起的任务得不到CPU 的使用权,也不会参与调度,除非它从挂起态中解除。
(7)挂起态→就绪态: 把 一 个 挂 起 状态 的 任 务 恢复的 唯 一 途 径 就 是调 用 vTaskResume() 或 vTaskResumeFromISR() API 函数,若此时被恢复任务的优先级高于正在运行任务的优先级,则发生任务切换,该任务将再次转换任务状态,由就绪态变成运行态。否则,仍处于就绪态。
怎么管理任务?
FreeRTOS 的任务调度器是基于优先级的抢占式调度:在系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的。任何一个时间点只有一个任务运行,具体运行哪个任务由RTOS调度器决定。RTOS调度器的职责是确保当一个任务开始执行的时候的上下文环境与任务上一次退出时相同。
有最高优先级的任务:寻找最高优先级
- 通用方法,在就绪链表中从高优先级往低优先级查找 uxTopPriority,因为在创建任务的时候已经将优先级进行了排序,查找到的第一个 uxTopPriority 就是我们需要的任务,然后通过 uxTopPriority 获取对应的任务控制块;
- 特殊方法,利用计算前导零指令 CLZ,直接在uxTopReadyPriority 这个 32 位的变量中直接得出 uxTopPriority,这样子就知道哪一个优先级任务能够运行,这种调度算法比普通方法更快捷,但受限于平台(在 STM32 中我们就使用这种方法)。
有相同优先级,且无最高优先级:采用时间片轮转方式进行调度,时间片轮转调度仅在当前系统中无更高优先级就绪任务存在的情况下才有效。
任务切换任务调度的原则是一旦任务状态发生了改变,并且当前运行的任务优先级小于优先级队列组中任务最高优先级时,立刻进行(除非当前系统处于中断处理程序中或禁止任务切换的状态)。
注:这一切都是通过链表实现:
可抢占:高优先级任务先运行
时间片轮转:同级任务轮流执行
空闲任务礼让:如果有同为优先级为0的任务或其他就绪任务,空闲任务主动放弃一次运行机会。
怎么切换?
每个任务创建时都为其分配一个堆栈,当需要进行任务切换时会将上下文环境保存在堆栈中(保护现场),这样当任务再次执行时就可以从堆栈中取出上下文环境(恢复现场),任务恢复运行。
例如:在 A 函数中调用 B 函数,当 B 函数执行完以后再回到 A 函数继续执行。如果再跳回 A 函数以后代码能够接着正常运行,那就必须在跳到 B 函数之前将当前处理器状态保存起来(就是保存 R0~R15 这些寄存器值),当 B 函数执行完成以后再用前面保存的寄存器值恢复R0~R15 即可。保存 R0~R15 寄存器的操作就叫做现场保护,恢复 R0~R15 寄存器的操作就叫做恢复现场。
保护现场的过程:
1)首先将R0-R7或R0-R12PUSH进堆栈中保存,并将堆栈指针保存在对应中断模式的R13(SP)中。
2)然后把即将执行的下一条指令(PC-4)的地址保存到对应中断模式的R14(LR)中;把CPSR的值保存到对应中断模式的SPSR中,以实现对处理器当前状态、中断屏蔽及各标志位的保护。
3)设置CPSR的相应位:设置CPSR[4:0]的5位以进入相应的工作模式;CPSR[5] = 0切换到ARM状态;设置CPSR[7] = 1禁止IRQ中断;如果进入复位模式或FIQ模式,还要设置CPSR[6] = 1以禁止FIQ中断。
4)给程序计数器PC强制赋值,转入中断向量地址,执行相应的中断服务程序
恢复现场的过程:
1)将原来保存在堆栈中的R0-R12或R0-R7 POP出栈,赋值给相应的寄存器。
2)将LR的值赋值给PC,将SPSR的值赋值给CPSR中,恢复被中断的程序状态。
谁进行调度?
Tick中断
vTaskDelay()函数将LED_Task任务从pxReadyTaskLists队列中移动到pxDelayTaskList队列中。这样才让低优先级任务有机会运行。
然后会每隔一段时间轮询一次LED_Task任务的延迟时间是否已经到了,若到了延时时间,则将LED_Task任务从pxDelayTaskList队列中移动到pxReadyTaskLists队列,进入就绪态,然后判断优先级,若优先级最高,则直接进入运行态,否则仍处于就绪态。