系列文章目录
FreeRTOS源码分析一:task创建(RISCV架构)
FreeRTOS源码分析二:task启动(RISCV架构)
FreeRTOS源码分析三:列表数据结构
FreeRTOS源码分析四:时钟中断处理响应流程
FreeRTOS源码分析五:资源访问控制(一)
文章目录
- 系列文章目录
- 前言
- 无符号溢出
- tick 溢出的几种情况
- vTaskDelayUntil
- vTaskDelay
- prvAddCurrentTaskToDelayedList
- 附:一个数学证明
- 1) “溢出 ⇒ wake < startTick ”
- 2) “不溢出 ⇒ wake > startTick ”
- 总结
前言
// vTaskDelay - 简单的相对延迟
void vTaskDelay( const TickType_t xTicksToDelay );// xTaskDelayUntil - 精确的绝对延迟
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime,const TickType_t xTimeIncrement );
这两个 API 都可以使当前任务进入延迟,并延迟一定的时间。本文从实现层面介绍他们的区别。
无符号溢出
在 C 语言中,无符号整数(unsigned int
、uint32_t
、TickType_t
等)是按模运算定义的。这一规则由 C 标准规定:
无符号整数运算的结果是对 2N2^N2N 取模的值,其中 NNN 是该类型的比特宽度。
例如:
uint8_t
范围是 0 ~ 255- 任何运算结果超出这个范围,就会自动对 256 取模
- 这不是溢出错误,而是自然回绕(wrap-around)
当结果超过最大值时,从 0 重新开始计数:
uint8_t a = 250;
uint8_t b = a + 10; // 250 + 10 = 260
// 按 256 取模:260 - 256 = 4
// b == 4
对于 32 位 tick 计数器:
uint32_t tick = 0xFFFFFFFE; // 最大值前两步
tick++; // 0xFFFFFFFF
tick++; // 溢出后回到 0
- FreeRTOS 的
TickType_t
通常是 无符号 32 位(范围 0 ~ 4,294,967,295) - 每次
tickCount++
,溢出时自动从 0 重新开始 - 不需要手动处理溢出运算,无符号加减法天生支持回绕
- 但比较逻辑需要自己设计,FreeRTOS 通过“双链表 + 溢出判断”来规避直接比较的问题
tick 溢出的几种情况
#define mainQUEUE_SEND_FREQUENCY_MS pdMS_TO_TICKS( 1000 )/* Initialise xNextWakeTime - this only needs to be done once. */xNextWakeTime = xTaskGetTickCount();for( ; ; ){....../* Place this task in the blocked state until it is time to run again. */vTaskDelayUntil( &xNextWakeTime, mainQUEUE_SEND_FREQUENCY_MS );
}
这是 vTaskDelayUntil
的一般用法。我们一般要在某个位置获取当前 tickCount 记为 startTick
,随后调用 vTaskDelayUntil
延迟当前任务从 startTick
的一段时间。
我们以三个变量代替三个时间:startTick
为 xTaskGetTickCount 调用时返回的 tickCount。nowTick
为用户调用 vTaskDelayUntil
时的 tickCount。而 tickDelay
为用户指定的延迟 tick 数。
这里会存在两个溢出的情况:
- 从
startTick
到nowTick
之间,系统 tick 已经溢出。这个表现为:nowTick < startTick
- 从
startTick
到startTick
+tickDelay
之间,系统 tick 会发生溢出。这个表现为:startTick + tickDelay < startTick
- 那么,为什么溢出之后会表现为
startTick + tickDelay < startTick
?附中有明确的数学证明。
这里我们另外定义一个变量:wake = startTick + tickDelay
,wake
可能溢出或没有
那么,我们结合上面这两种情况,在先判断 从 startTick
到 nowTick
之间,系统 tick 溢出情况之后再判断后面定时器的溢出情况,则有以下情况:
nowTick >= startTick
表明系统计数器未溢出,wake < startTick
(wake 本身溢出了) 或wake > nowTick >= startTick
(wake 未溢出) 这两种情况都表明任务的待唤醒时间尚未抵达,任务需阻塞。若以上都不满足(等价于wake >= startTick
且wake <= nowTick
),说明“计划唤醒点已经过去”,不阻塞,立刻返回。nowTick < startTick
表明系统计数器溢出,只有当wake
也溢出且wake > nowTick
时才会阻塞:条件是(wake < startTick ) && (wake > nowTick)
。直观解释:大家都已经跨到新一圈了,而且wake
还在nowTick
之后,才需要等;否则就是“错过了”或“在旧圈里”,不阻塞。
vTaskDelayUntil
这个时候来看 vTaskDelayUntil 的源码就非常清晰了。
xTaskDelayUntil 用于延迟任务从 *pxPreviousWakeTime 到 *pxPreviousWakeTime + xTimeIncrement 这段时间
@returnpdTRUE
:本次调用确实延迟了任务(进入延迟列表)。pdFALSE
:任务未延迟(唤醒点已过,立即返回)。
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime,const TickType_t xTimeIncrement )
{TickType_t xTimeToWake;BaseType_t xAlreadyYielded, xShouldDelay = pdFALSE;/* 参数有效性检查 */configASSERT( pxPreviousWakeTime );configASSERT( ( xTimeIncrement > 0U ) );/* 挂起调度器,防止 tickCount 在计算期间发生变化 */vTaskSuspendAll();{/* 缓存当前系统 tick 计数(在本代码块中不会变化) */const TickType_t xConstTickCount = xTickCount;configASSERT( uxSchedulerSuspended == 1U );/* 计算下一次唤醒的时间点(无符号加法,可能会溢出) */xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;/* ---- 溢出情况分析 ----* 如果当前 tickCount < 上一次的唤醒时间,说明 tickCount 已经溢出过。* 在这种情况下,我们只有在 “下一次唤醒时间也发生溢出且它仍大于当前 tickCount” 时才需要延迟。* 这样处理是因为这种情况等价于没有溢出。*/if( xConstTickCount < *pxPreviousWakeTime ){if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) ){xShouldDelay = pdTRUE;}}else{/* ---- 未溢出情况 ----* 如果下一次唤醒时间溢出(xTimeToWake < prevWake),或* 下一次唤醒时间仍在未来(xTimeToWake > 当前 tickCount),则需要延迟。*/if( ( xTimeToWake < *pxPreviousWakeTime ) || ( xTimeToWake > xConstTickCount ) ){xShouldDelay = pdTRUE;}}/* 更新 pxPreviousWakeTime,为下一次调用做准备 */*pxPreviousWakeTime = xTimeToWake;if( xShouldDelay != pdFALSE ){/* 将当前任务加入延迟列表。* 这里需要的是“等待的时间”而不是“目标唤醒时间”,* 所以要减去当前 tickCount 得到阻塞时长。*/prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE );}}/* 恢复调度器,如果期间有更高优先级任务就绪,这里可能会发生任务切换 */xAlreadyYielded = xTaskResumeAll();/* 若 ResumeAll 未触发切换(返回 pdFALSE),仍主动 yield 一次:* 1) 让同优先级任务获得公平的时间片/协作式让出;* 2) 在禁抢占或端口差异下,统一通过显式 yield 兑现切换。* 若此时系统中没有更高/同优先级可运行任务,此调用几乎为空操作。 */if( xAlreadyYielded == pdFALSE ){taskYIELD_WITHIN_API();}/* 返回本次调用是否真的延迟了任务 */return xShouldDelay;
}
- 函数用于周期性任务的延时,保证任务唤醒的时间间隔固定,不受执行时间波动影响。
- 与
vTaskDelay()
不同,它基于绝对唤醒时间(pxPreviousWakeTime
)而非相对延迟。 - 每次调用都会将
pxPreviousWakeTime
累加xTimeIncrement
,而不是更新为当前时间。这避免了周期漂移。 xTaskResumeAll()
会恢复调度器、处理挂起期间的就绪任务,并可能立即触发切换。- 若未触发切换(返回
pdFALSE
),仍调用taskYIELD_WITHIN_API()
:
vTaskDelay
vTaskDelay → 基于相对时间延迟任务,延迟是“从执行
vTaskDelay
开始算”,可能因执行时间累积产生周期漂移。
void vTaskDelay( const TickType_t xTicksToDelay )
{BaseType_t xAlreadyYielded = pdFALSE;/* 延时时间为 0 时,不阻塞任务,只是强制进行一次任务切换。 */if( xTicksToDelay > ( TickType_t ) 0U ){/* 挂起调度器,防止任务状态修改过程被调度打断。 */vTaskSuspendAll();{configASSERT( uxSchedulerSuspended == 1U );/* 当前任务不可能在事件列表中(它正运行着),因此直接将它* 从就绪列表移除,并加入延迟列表。 */prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );}/* 恢复调度器:* - 将挂起期间的 pending ready 任务并入就绪列表。* - 处理挂起期间累积的 tick(xPendedTicks)。* - 检查是否需要立即任务切换,必要时发起切换。*/xAlreadyYielded = xTaskResumeAll();}/* 如果恢复调度器时没有触发切换(返回 pdFALSE),* 仍然调用一次 yield:* - 兑现同优先级任务的时间片轮转。* - 处理 xYieldPending 标志(中断中可能置位的“应切换”标志)。*/if( xAlreadyYielded == pdFALSE ){taskYIELD_WITHIN_API();}
}
prvAddCurrentTaskToDelayedList
prvAddCurrentTaskToDelayedList() 的主要功能是 把当前正在运行的任务移出就绪队列,并加入到合适的延时/挂起队列中,以实现延时阻塞功能(包括 Tick 溢出情况处理和无限期阻塞)。
static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait,const BaseType_t xCanBlockIndefinitely )
{TickType_t xTimeToWake;const TickType_t xConstTickCount = xTickCount; // 当前系统 Tick 值的快照List_t * const pxDelayedList = pxDelayedTaskList; // 当前延时任务列表List_t * const pxOverflowDelayedList = pxOverflowDelayedTaskList; // Tick 溢出延时列表#if ( INCLUDE_xTaskAbortDelay == 1 ){/* 进入延时队列前,先清除任务的 ucDelayAborted 标志位* 用于检测任务离开阻塞态时,是否是被中止延时(Abort Delay)唤醒的 */pxCurrentTCB->ucDelayAborted = ( uint8_t ) pdFALSE;}#endif/* 1. 从就绪队列移除当前任务* 因为任务状态链表项在就绪队列和阻塞队列中是同一个 list item,* 所以必须先从就绪队列删除才能放到阻塞队列。 */if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0 ){/* 如果该优先级的就绪任务已经被清空,就更新优先级位图,* 表示该优先级上已无就绪任务 */portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority );}#if ( INCLUDE_vTaskSuspend == 1 ){/* 2. 判断是否是无限期阻塞(portMAX_DELAY 且允许无限阻塞) */if( ( xTicksToWait == portMAX_DELAY ) && ( xCanBlockIndefinitely != pdFALSE ) ){/* 直接加入挂起任务列表(xSuspendedTaskList),* 保证它不会因为时间到而被唤醒,必须手动唤醒。 */listINSERT_END( &xSuspendedTaskList, &( pxCurrentTCB->xStateListItem ) );}else{/* 3. 计算唤醒时间(可能会发生无符号溢出,但内核会处理) */xTimeToWake = xConstTickCount + xTicksToWait;/* 设置链表项的值为唤醒时间(用于延时队列的排序) */listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );if( xTimeToWake < xConstTickCount ){/* 4. Tick 计数溢出* 如果唤醒时间比当前 Tick 值小,说明加法结果溢出,* 则把任务放入溢出延时队列。 */vListInsert( pxOverflowDelayedList, &( pxCurrentTCB->xStateListItem ) );}else{/* 5. Tick 未溢出* 直接加入当前延时队列(pxDelayedList),* 按唤醒时间升序插入。 */vListInsert( pxDelayedList, &( pxCurrentTCB->xStateListItem ) );/* 如果该任务的唤醒时间比当前系统记录的最早唤醒时间还早,* 更新 xNextTaskUnblockTime(优化 Tick 处理,减少无意义扫描)。 */if( xTimeToWake < xNextTaskUnblockTime ){xNextTaskUnblockTime = xTimeToWake;}}}}
}
这里我们回顾:FreeRTOS源码分析四:时钟中断处理响应流程 中提到,时钟中断会调用函数 xTaskIncrementTick
,它会对系统 tick 加1,当检测到 tick 溢出时,会交换两个延时队列。如下所示:
BaseType_t xTaskIncrementTick( void )
{TCB_t * pxTCB; // 任务控制块指针TickType_t xItemValue; // 延迟列表项的值(唤醒时间)BaseType_t xSwitchRequired = pdFALSE; // 是否需要任务切换标志/* 时钟递增应该在每个内核定时器事件上发生。* 如果调度器被挂起,则递增待处理的时钟计数。 */if( uxSchedulerSuspended == ( UBaseType_t ) 0U ){// === 调度器未被挂起的情况 ===/* 小优化:在此代码块中时钟计数不会改变 */const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1;/* 递增RTOS时钟,如果溢出到0则切换延迟和溢出延迟列表 */xTickCount = xConstTickCount;// 处理时钟计数器溢出的情况(从最大值回绕到0)if( xConstTickCount == ( TickType_t ) 0U ){taskSWITCH_DELAYED_LISTS(); // 切换延迟任务列表}....................................
}
而宏 taskSWITCH_DELAYED_LISTS
则会切换两个任务列表的角色:
#define taskSWITCH_DELAYED_LISTS() \do { \List_t * pxTemp; \\/* The delayed tasks list should be empty when the lists are switched. */ \configASSERT( ( listLIST_IS_EMPTY( pxDelayedTaskList ) ) ); \\pxTemp = pxDelayedTaskList; \pxDelayedTaskList = pxOverflowDelayedTaskList; \pxOverflowDelayedTaskList = pxTemp; \xNumOfOverflows = ( BaseType_t ) ( xNumOfOverflows + 1 ); \prvResetNextTaskUnblockTime(); \} while( 0 )
恰与这里两个延时队列想对应,当用户延时会发生在 tick 溢出之后时,则加入溢出队列。而当溢出发生,则溢出队列就变为当前的延时队列。
这样上一轮“溢出延时链表”里的任务会自然地在新的 tick 空间里按照时间顺序等待唤醒。xNextTaskUnblockTime
被重置,用于后续快速判断“下一个到期点”。
附:一个数学证明
因为是无符号模 2N2^N2N 加法,而且 tickDelay > 0
(源码里有 configASSERT( xTimeIncrement > 0U )
)。设:
startTick ∈ [0, 2^N-1]
tickDelay ∈ [1, 2^N-1]
- 真实和
S = startTick + tickDelay
- 存回寄存器/变量的结果
wake = S mod 2^N
结论: 溢出当且仅当 wake < startTick
。证明分两步:
1) “溢出 ⇒ wake < startTick ”
若溢出,则 S ≥ 2^N
,所以
wake=S−2N=startTick+tickDelay−2N=startTick−(2N−tickDelay).wake = S - 2^N = startTick + tickDelay - 2^N = startTick - (2^N - tickDelay). wake=S−2N=startTick+tickDelay−2N=startTick−(2N−tickDelay).
因为 tickDelay ≥ 1
,故 (2^N - tickDelay) ≤ 2^N - 1
且至少为 1,于是
wake=startTick−(2N−tickDelay)⏟≥1≤startTick−1<startTick.wake = startTick - \underbrace{(2^N - tickDelay)}_{\ge 1} \le startTick - 1 \;<\; startTick . wake=startTick−≥1(2N−tickDelay)≤startTick−1<startTick.
所以一旦溢出,wake
一定小于 startTick
。
2) “不溢出 ⇒ wake > startTick ”
若不溢出,则 S < 2^N
,wake = S = startTick + tickDelay
。又因 tickDelay ≥ 1
,
wake=startTick+tickDelay≥startTick+1>startTick.wake = startTick + tickDelay \ge startTick + 1 > startTick . wake=startTick+tickDelay≥startTick+1>startTick.
(只有当 tickDelay = 0
才可能 wake = startTick
,但这被断言禁止了。)
因此,判断溢出最简单的办法就是:做完 wake = startTick + tickDelay
的无符号加法后,看 wake < startTick
是否成立。成立就说明发生了进位丢弃。
总结
完结撒花!!!