一、FreeRTOS 数据结构
二、在main函数中 启动调度器后,调度器 运行第一个任务taskA
OS确定第一个运行的任务taskA:启动调度器前,taskA在创建的任务中优先级最高(假设为max_priority),且在优先级为max_priority的那一批任务中,taskA是最后一个创建的任务。
CPU复位后(进入特权级的线程模式、栈指针为MSP),自动更新 MSP = 中断向量表Item0的值、PC = 中断向量表Item1的值(复位中断函数指针),然后CPU开始运行复位中断函数。可以理解为:CPU复位后,硬件自动运行 第一个任务resetTask。
任务resetTask 最终会调用main函数。在main函数中,创建各个任务,然后启动调度器,调度器启动时,会更新 MSP = 中断向量表Item0的值(即回收任务resetTask的栈空间),然后通过汇编指令 “svc 0” 触发SVC中断,在SVC中断函数中:
找到taskA的栈指针(taskA_TCB.pxTopOfStack),然后手动 从 taskA 的栈中把 R4~R11 加载到 CPU对应寄存器(加载完后taskA的栈指针需更新),然后更新 寄存器PSP = taskA的栈指针,最后设置 寄存器LR 低4bit 为 0b1101(后面执行指令 “bx LR ”时返回线程模式 且 从PSP指向的栈中做出栈操作,返回后栈指针使用PSP), 并调用指令 “bx LR” 返回线程模式(NVIC硬件自动把 R0~R3、R12、LR、PC和xPSR 这8个寄存器 从taskA的栈中 pop到 CPU,PC = taskA入口函数地址,因此taskA开始运行)。
PS:
1、调度器运行第一个任务taskA后,任务resetTask将永远消失,不会再得到CPU运行。
2、CPU硬件运行的第一个任务resetTask,使用MSP作为栈指针;而当调度器运行第一个任务taskA后,所有任务都使用PSP作为栈指针。另外在中断函数中,硬件强制使用MSP作为栈指针。注意:各个任务的栈是相互独立的,中断函数的栈也是独立于各任务的。
3、在main函数中创建任务taskA时(其他任务也是一样),OS会在taskA的栈中手动做一个CPU现场(R0~R12,LR,PC,xPSR;R13为SP,无须压栈),其中包含taskA的入口函数地址及函数参数、函数返回地址(即taskA异常返回时 会返回到哪里)。
4、在SVC中断函数中:CPU执行 “bx LR”指令退出SVC中断时,NVIC硬件自动把 R0~R3、R12、LR、PC和xPSR 这8个寄存器从taskA的栈中 pop到 CPU(PC = taskA入口函数地址,因此中断返回后 CPU运行的是 taskA);另外的8个寄存器(R4~R11),需在退出SVC中断前,手动从taskA的栈中 加载到 CPU。并且,由于进入SVC中断前,任务resetTask 使用MSP作为栈指针,而在退出SVC中断后,任务taskA 需使用PSP作为栈指针,因此,在执行 “bx LR”指令退出SVC中断前,需设置寄存器LR 低4bit 为 0b1101。
5、CPU执行 “svc 0” 指令时,svc中断必须能立刻响应,如果此时svc中断被屏蔽,或有更高优先级的中断挂起导致svc中断无法立刻响应,则会触发Hardfault。
三、调度器 切换任务的流程
OS 在启动调度器时,将PendSV中断优先级配置为最低,后面在需要切换任务时,通过置位PendSV中断挂起标志 来手动触发PendSV中断,然后在PendSV中断函数中切换任务。假设当前正在运行的是taskA 且 taskA没有挂起调度器,CPU此时响应PendSV中断,在PendSV中断函数中:
1、找到切换的目标任务taskB
先确定就绪任务中的最高优先级 max_priority,
然后更新 pxReadyTasksLists[ max_priority ].pxIndex = pxReadyTasksLists[ max_priority ].pxIndex->pxNext,
更新完成后,pxReadyTasksLists[ max_priority ].pxIndex->pvOwner 即为要切换的目标任务taskB
注意:“确定就绪任务中的最高优先级 max_priority” 有两种方法,通过宏 “configUSE_PORT_OPTIMISED_TASK_SELECTION” 来选择其中一种:
①、configUSE_PORT_OPTIMISED_TASK_SELECTION = 0时:
定义一个全局变量 uint32_t uxTopReadyPriority = 0。
当前运行的任务或中断 每次向 pxReadyTasksLists[ configMAX_PRIORITIES ] 中插入优先级为 priority_x 的任务X时,若 priority_x > uxTopReadyPriority,则更新 uxTopReadyPriority = priority_x。(uxTopReadyPriority的值向上更新,记录优先级的历史最高值)
在 PendSV中断函数中确定就绪任务的最高优先级时,遍历 pxReadyTasksLists[ uxTopReadyPriority ] ----> pxReadyTasksLists[ 0 ],当发现链表 pxReadyTasksLists[ max_priority ] 不为空时,则 max_priority 即为任务的最高优先级,同时更新 uxTopReadyPriority = max_priority 。(uxTopReadyPriority的值向下更新,记录当前最高优先级的准确值)
②、configUSE_PORT_OPTIMISED_TASK_SELECTION = 1时(此时configMAX_PRIORITIES 最大值限制为 32):
定义一个全局变量 uint32_t uxTopReadyPriority = 0。
当前运行的任务或中断 每次向 pxReadyTasksLists[ configMAX_PRIORITIES ] 中插入优先级为 priority_x 的任务X时,置位 uxTopReadyPriority 的第 priority_x 位;
当前运行的任务或中断 每次从 pxReadyTasksLists[ configMAX_PRIORITIES ] 中移除优先级为 priority_x 的任务X时,若链表 pxReadyTasksLists[ priority_x ] 为空,则清零 uxTopReadyPriority 的第 priority_x 位。
在 PendSV中断函数中,直接计算就绪任务的最高优先级 max_priority = ( 31 - ( uint32_t ) __clz( uxTopReadyPriority ) )
总结就是:用变量 uxTopReadyPriority 的各个bit,准确记录 pxReadyTasksLists[ configMAX_PRIORITIES ] 中各个链表的状态。举例:uxTopReadyPriority 的bit0 记录 链表pxReadyTasksLists[ 0 ] 的状态,bit值为0表示链表空,bit值为1表示链表非空;uxTopReadyPriority 的bit1 记录 链表pxReadyTasksLists[ 1 ] 的状态;uxTopReadyPriority 的bit2 记录 链表pxReadyTasksLists[ 2 ] 的状态; … … ;uxTopReadyPriority 的bit31 记录 链表pxReadyTasksLists[ 31 ] 的状态。
当前运行的任务或中断每次对 pxReadyTasksLists[ configMAX_PRIORITIES ] 插入或移除优先级为 priority_x 的任务X时,根据链表pxReadyTasksLists[ priority_x ]状态来置位或清零 uxTopReadyPriority的第 priority_x 位。
在 PendSV中断函数中确定任务的最高优先级时,通过芯片支持的“__clz”指令,可直接计算出任务的最高优先级 max_priority
PS:__clz:Count Leading Zeros ,计算前导零指令。该指令返回操作数二进制编码中第一个1前0的个数。如果操作数为0,则指令返回32;如果操作数二进制编码第31位为1,指令返回0
2、把taskA 未被NVIC硬件压栈的CPU寄存器 R4~R11,手动保存到taskA的栈中(保存后taskA的栈指针需更新),然后把taskA的栈指针保存到 taskA_TCB.pxTopOfStack。
PS:进入PendSV中断时,NVIC硬件自动把 R0~R3、R12、LR、PC和xPSR 这8个寄存器 push到 taskA的栈中
3、找到taskB的栈指针(taskB_TCB.pxTopOfStack),然后手动 从 taskB 的栈中把 R4~R11 加载到 CPU对应寄存器(加载完后taskB的栈指针需更新),然后更新 寄存器PSP = taskB的栈指针,最后调用指令 “bx LR” 返回线程模式(NVIC硬件自动把 R0~R3、R12、LR、PC和xPSR 这8个寄存器 从taskB的栈中 pop到 CPU,PC = taskB上一次被打断的地址,因此taskB可从上一次被打断处继续运行)。
四、触发任务切换的时机
假设当前正在运行taskA,则触发任务切换(软件触发PendSV中断)的时机有:
1、taskA 主动触发:
taskA 挂起等待事件(等待超时事件,或 等待信号量/互斥锁/事件组…);
taskA 主动让出CPU(调用taskYIELD());
taskA 自杀(调用vTaskDelete(NULL));
(taskA 为空闲任务idleTask)任务idleTask 检测到有其他同优先级的任务就绪。
2、(时间片轮转调度)Systick中断触发时:taskA 的 1 tick 时间片用完了,调度器 自动切换到同优先级的下一个任务。
3、(抢占式调度)优先级更高的taskB 就绪了,调度器 自动切换到taskB。
五、注意事项
1、在main函数中启动调度器时,OS会自动创建一个空闲任务idleTask,以确保在任意时刻总有一个任务就绪。任务idleTask优先级默认是最低的(优先级为0),且为用户提供了一个钩子函数(用户若使用此钩子函数,注意不要调用会让idleTask阻塞的API)。
2、taskA 调用vTaskDelete(NULL) 自杀时,OS会先把 taskA_TCB.xStateListItem 插入链表 xTasksWaitingTermination,后面由任务idleTask 负责回收 taskA的栈 和 taskA_TCB。
3、Cortex-M3/M4 使用的是“向下生长的满栈”模型,SP 指向最后一个被压栈的 32位数值。使用push指令压栈时, SP 先自减 4, 再存入新的数值;使用pop指令出栈时,先从 SP 指针处读出上一次被压入的值,再把 SP 指针自增 4。
参考资料
[1] https://github.com/FreeRTOS/FreeRTOS-Kernel
[2] Joseph Yiu(著),宋岩(译). Cortex-M3 权威指南.
[3] ARMv7-M_Architecture_Reference_Manual.
[4] CortexM3_Technical_Reference_Manual.
[5] The ARM-THUMB Procedure Call Standard. 24 October, 2000.