FreeRTOS 中任务切换的过程, 提到触发任务切换的两种情况 : 高优先级任务就绪抢占和同优先级任务时间共享(包括提前挂起)。 系统中,时间延时和任务阻塞,时间片都以 Systick 为单位。通过设置文件 FreeRTOSConfig.h
中 configTICK_RATE_HZ
设置任务节拍中断频率, 在启动任务调度器时,系统会根据另一个变量, CPU 的频率configCPU_CLOCK_HZ
计算对应写入节拍计数器的值,启动定时器中断。
系统在每一次节拍计数器中断服务程序xPortSysTickHandler
(平台实现 port.c 中) 中调用处理函数 xTaskIncrementTick, 依据该函数返回值判断是否需要触发 PendSV 异常, 进行任务切换。
涉及任务时间片轮循, 任务阻塞超时, 以及结束以此实现的延时函数。
分析的源码版本是 v9.0.0
xTaskIncrementTick()
系统每次节拍中断服务程序中主要任务由函数 xTaskIncrementTick
完成。
在任务调度器没有挂起的情况下( xTaskIncrementTick != pdFALSE ),该函数主要完成 :
* 判断节拍计数器xTickCount 是否溢出, 溢出轮换延时函数队列
* 判断是否有阻塞任务超时,取出插入就绪链表
* 同优先级任务时间片轮
而当任务调度器被挂起时, 该函数累加挂起时间计数器 uxPendedTicks
, 调用用户钩子函数, 此时,正在运行的任务不会被切换, 一直运行。
当恢复调度时, 系统会先重复调用 xTaskIncrementTick
补偿 (uxPendedTicks
次)。
不管, 系统调度器是否挂起, 每次节拍中断都会调用用户的钩子函数 vApplicationTickHook
。 由于函数是中断中调用,不要在里面处理太复杂的事情!!
节拍计数器溢出
涉及的变量, 定义在 task.c
开头。
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
初始化时, pxDelayedTaskList
指向 xDelayedTaskList1
, pxOverflowDelayedTaskList
指向 pxOverflowDelayedTaskList
,一开始我还在郁闷延时链表为什么要两个,到这里才明白。
当任务由于等待事件(延时,消息队列什么的堵塞)时,会设置一个时间,这时候,响应的任务会被挂到延时链表中,如果超过设置时间没有事件响应,则系统会从延时链表中取出任务恢复就绪。
系统任务延时参考系统节拍计数器 xTickCount
, 加入链表前依据当前计数器的值计算出超时的值 ( xTickCount+ xTicksToDelay ), 顺序插入到延时链表中。
对不同平台xTickCount
表示的位数不同,但是每次节拍中断加一,总会溢出。 上述计算任务延时时间,如果系统发现计算出来的时间已经溢出,则会将该任务加入到 pxOverflowDelayedTaskList
这个链表中。
在系统节拍中断时, 节拍计数器每次加一, 系统判断是否溢出,如果溢出, 调用宏 taskSWITCH_DELAYED_LISTS()
切换上述的链表指针。
宏主要实现如下 :
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
这就是设置两个链表的原因,轮流倒应对计数器的溢出。
唤醒超时任务
全局变量 xNextTaskUnblockTime
记录下一个需要退出延时链表的任务时间, 因此, 接下来判断当前时间,延时链表中是否有任务需要推出阻塞状态。
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
对应的, 把所有阻塞时间达到的任务取出, 推入到就绪链表,更新下一个任务解除时间给变量 xNextTaskUnblockTime
。
任务时间片轮循
处理完延时任务后, 开始判断当前运行任务, 对应优先级链表中是否有其他任务就绪, 如果有,需要保证每个任务都能获得运行时间, 标记需要任务切换, 作为函数返回。
完整函数
完整函数注释如下,
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
系统延时函数
任务执行过程中需要使用到延时函数进行延时, 使用系统提供的延时函数可以将当前任务挂起,让出CPU 使用时间,当时间到达的时候, 有系统恢复任务运行。 FreeRTOS 提供两种类型的延时函数
普通延时函数 vTaskDelay
一般情况下,需要延时一定时间,就调用此函数,将需要的延时时间转换为对应系统节拍数传递(如宏pdMS_TO_TICKS()), 之后,当前任务会从就绪链表移除, 加入到延时链表中,系统会在节拍中断中检查是否到达延时时间, 重新恢复任务就绪。
- 1
- 1
该函数调用到另一个函数是 prvAddCurrentTaskToDelayedList
, 将任务加入到延时链表中, 函数中会判断设定时间是否溢出, 选择加入到对应的延时链表, 同上提到计数器溢出的问题。
循环延时函数 vTaskDelayUntil
相比上面的普通延时函数, 这个函数适用于任务周期性执行的。
举个例子说明下, 有一个任务, 需要周期性 500ms 读取一次传感器数据, 用上例子可以这么写 :
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
看起来是周期性 500 ms 执行, 但是考虑, 如果任务由于优先级比较低之类的问题, 在延时返回就绪状态后没有及时被运行,那么实际时间就开始飘了。
如果使用函数 vTaskDelayUntil
,
- 1
- 1
多了一个参数 pxPreviousWakeTime
, 就不会有这个问题了
先看以下如何使用 :
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
周前性执行前调用一个变量, 获取当前节拍计数器 ,简单认为是第一次调用的时间, 而后开始周期性执行, 传入的变量第一次由我们设置后, 后续会由函数自动更新。
比如, 我们在SystickCount 为 0 开始延时, 在500 返回读取数据, 再延时, 和上一个例子一样, 当 500 延时后返回, 调度原因延迟, 等到 600 才读取数据并开始下一次延时, 这里, 这个函数不同地方在于, 他会考虑这延迟的 100, 而第二次延时的时间, 其实还是从 500 开始算的, 也就是, 1000 的时候, 任务延时第二次就结束了, 而不是等到 1100 。
由于涉及到任务调度, 所以, 理论上来说, 两个函数定时都是”不住确”的。 时间单位是系统节拍 !