freertos之因此可以准确的按照配置的时间片进行任务调度彻底依靠硬件支持。硬件上的某个计数器会提供周期性中断,在中断处理中解决任务调度 如:task1切换到task2,task2再切换到task1,如此循环往复,在外部就表现的如同多个任务在一块儿执行。编程
在CM3/CM4上有提供内部异常定时器:systick。使用systick做为实时系统的运行心脏再好不过,由于人家的名字就叫作系统滴答定时器,可谓门当户对。其它定时器也能够完成相似功能,由于freertos只关注可否提供周期中断,并不会在乎中断产生着是白猫仍是黑猫。使用systick的一个好处是在开发低功耗功能时它将不会受到影响。api
在调度器启动以前已经使能了systick,systick中断发生后进入xPortSysTickHandler异常。在xPortSysTickHandler中没有发生任务切换,它只是利用xTaskIncrementTick函数处理了readylist与delaylist上的任务结点,须要进行任务切换时会交给pendSV来完成。函数
(调度器流程图)(拖动放大)源码分析
在进入xTaskIncrementTick以前首先屏蔽了中断,防止发生中断嵌套破坏内核链表。spa
xTaskIncrementTick中率先自增了xTickCount。xTickCount可以准确表示systick发生了多少次中断,它也理所固然成为freertos中的时间基石。不少api调用与时间有关,例如 某个任务须要等待一个信号量,能够给它设置等待超时,超过必定时间放弃等待;或者某个任务须要被delay一段时间再被从新调度。线程
调度器使用xTickCount记录内核启动时间,一个TCB使用节点上的xItemValue值表示下一次事件触发的时间。假设当前xTickCount值为5,systick 1ms触发一次中断,此时运行的某个任务使用了vTaskDelay来挂起10ms,那么TCB上的xItemValue会被赋值为15,等到xTickCount累加到15时将任务拉回就绪态。指针
时间值一直累加总会有溢出的状况发生。为了更快的故意达到此效果,假设TCB上的xItemValue值与xTickCount都使用1字节存储。当xTickCount值为230时,某个任务调用了vTaskDelay挂起30ms,xItemValue值累加后发生溢出变成了4,调度器再次比较xTickCount与xItemValue值时就会认为此任务到达唤醒时间(小于xTickCount值都会被唤醒),vTaskDelay成为无效的调用。事件
xDelayedTaskList1和xDelayedTaskList2解决了tick值溢出的问题,当累加后xTickCount与xItemValue值比累加以前还要小时能够判断为溢出发生,插入的链表也要发生变化,如图:内存
(拖动放大)资源
pxDelayedTaskList与pxOverflowDelayedTaskList两个指针分别指向xDelayedTaskList1和xDelayedTaskList2两条链表,如有xItemValue值发生溢出则插入pxOverflowDelayedTaskList中。
pxDelayedTaskList链表上的任务将等待xTickCount自增到某一值后从新被唤醒,然而
pxOverflowDelayedTaskList链表上的任务永远不会被唤醒,直到xTickCount值发生溢出将两个指针互换:
(拖动放大)
发生交换后,原来pxOverflowDelayedTaskList上的结点就能以准确的tick值被唤醒,如此重复,整个链表就能得以正确运行调度。
每当有节点插入pxDelayedTaskList时会更新一次xNextTaskUnblockTime变量值,在pxDelayedTaskList上的任务都是须要“睡眠”一段时间,等到唤醒后从新插入pxReadyTasksLists参与调度。pxDelayedTaskList上的结点是按照升序插入,xNextTaskUnblockTime值会一直等于链表上第一个节点的值,也就是须要遍历pxDelayedTaskList链表的最小时间,这样能够减小在systick中断里遍历pxDelayedTaskList的次数。
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
检查当前优先级的任务数是否大于1,大于的话则使能pendSV。有了这行代码,在freertos中多个优先级相同的高优先级任务会按照时间片分配cpu资源。若是当前优先级任务数为1的话则放弃调度,cpu资源将会一直被当前任务占据,直到它被某个操做挂起才能轮到下一个较低优先级的任务执行。
PendSV是CM3/CM4中可以被“缓期执行”一个异常,悬起 PendSV方法是手工向 NVIC 的 PendSV 悬起寄存器中写 1。
CM3权威指南:
PendSV 的典型使用场合是在上下文切换时(在不一样任务之间切换)。
须要把 PendSV 编程为最低优先级的异常(PendSV中断返回时须要置cpu状态为线程模式(LR位段控制),若是 OS 在某中断活跃时尝试切入线程模式,将触犯用法 fault 异常)。
xPortPendSVHandler是pendSV异常处理函数,由汇编实现,主要更新了重要寄存器和调用vTaskSwitchContext进行任务切换。
前面准备了一大堆中断屏蔽以及链表遍历等工做,都为的是这一刻的辉煌!在vTaskSwitchContext中pxCurrentTCB值终于被替换掉,新的任务被分配到cpu资源,调度由此产生。
在更换pxCurrentTCB以前还要检查当前任务栈是否溢出,本着认真负责的态度它还检查了两次。开发者在开发过程当中可能只计算了本身的变量在任务中消耗的栈空间,而且“恰到好处的”分配了任务栈空间,若异常发生时由硬件自动压入的几个寄存器值颇有可能会超出栈的设定范围(异常响应时若当前使用PSP则压入PSP,若使用MSP则压入MSP,进入异常后一直使用MSP),一个TCB中会记录当前任务栈的栈顶和栈底,若是栈顶地址超过栈底则发生溢出。因此只要内存够仍是把栈搞得尽可能大比较好。
获取当前最高任务优先级后,从pxReadyTasksLists中取出新TCB赋给pxCurrentTCB指针就完成了他的使命。
xPortPendSVHandler:
mrs r0, psp //保存当前任务PSP地址到R0中
ldr r3, pxCurrentTCBConst //获取pxCurrentTCBConst指针地址
ldr r2, [r3] //R2被赋予当前TCB地址
stmdb r0!, {r4-r11} //R4-R11压入PSP
str r0, [r2] //新的PSP地址存入到TCB中
stmdb sp!, {r3, r14} //压R3,R14值至MSP
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY //或缺中断屏蔽阈值
msr basepri, r0 //中断屏蔽
dsb
isb
bl vTaskSwitchContext //调用vTaskSwitchContext
mov r0, #0
msr basepri, r0 //打开中断
dsb
isb
ldmia sp!, {r3, r14} //出栈R3,R14
ldr r1, [r3] //R3依然是pxCurrentTCBConst指针地址,R1变为新TCB地址
ldr r0, [r1] //R0值成为新TCB的栈地址(该TCB发生上一次调度的PSP值)
ldmia r0!, {r4-r11} //出PSP
msr psp, r0 //更新PSP
bx r14 //LR位段控制返回线程模式并使用PSP做为SP
.align 2
pxCurrentTCBConst: .word pxCurrentTCB
调度时不仅是更换pxCurrentTCB值,R0-R15等寄存器都要进行大换血才能恢复到新任务的上一次运行环境:
(拖动放大)