目录
1 系统延时API详解
2 相对延时与绝对延时的区别
3 相对延时与绝对延时的应用
4 系统延时函数实现原理
4.1 vTaskDelay业务流程
4.2 vTaskDelayUntil业务流程
5 任务挂起/任务恢复详解
1 系统延时API详解
TickType_t 实际上是uint32_t类型
2 相对延时与绝对延时的区别
3 相对延时与绝对延时的应用
- 1、创建一个任务使用vTaskDelayUntil()
- 2、分别在两个任务里定时5s打印一次任务运行状态
- 3、vTaskDelayUntil()任务 vTaskDelay()任务。添加HAL_Delay()观察两种延时接口的区别
HAl库会用到systick,在操作系统也会用到systick,所以我们会把HAL库换成tim1,这样不冲突。
cubemx配置
使能vTaskDelayUntil
串口通信、FreeRTOS任务新建2个,优先级相同、时钟配置、SW配置
4 系统延时函数实现原理
搞清楚4个函数
vTaskDelay
vTaskDelayUntil
vTaskSuspendAll/xTaskResumeAll(调度锁,用来触发整个调度器挂起和恢复)
4.1 vTaskDelay业务流程
1.挂起调度器
挂起调度器的原因:
挂起3个步骤
1切换任务状态
2计算系统节拍值
3上下文的切换。处理这些要占用cpu的时间
如果这个时候有个优先级高的任务产生调度cpu的抢占,等任务恢复就会遥遥无期
调度锁的作用:挂起和恢复,是一种资源的保护
2.添加任务到延时列表
延时列表会去遍历所有延时列表任务,
然后再去控制块里读取延时时间,
如果延时时间到达就会恢复任务到就绪态
3.恢复调度器进行上下文切换
vTaskDelay源码分析
void vTaskDelay( const TickType_t xTicksToDelay )
{
//xAlreadyYielded :已经调度的状态,初始赋值为0
BaseType_t xAlreadyYielded = pdFALSE;
/* 延时周期是否大于0,不大于0,就不应该调度 */
if( xTicksToDelay > ( TickType_t ) 0U )
{
configASSERT( uxSchedulerSuspended == 0 );
//挂起调度器
vTaskSuspendAll();
{
traceTASK_DELAY();
/*
1、添加到延时列表中
2、需要传入两个参数
2.1、xTicksToDelay延时:周期
2.2、pdFALSE 延时:状态值为0
*/
prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
}
//恢复调度器,这个调度器是有返回值的,这个返回值,表示在恢复调度器的时候,是否已经进行了任务切换
xAlreadyYielded = xTaskResumeAll();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* xAlreadyYielded 等于代表在恢复调度器的时候,没有进行任务切换 */
if( xAlreadyYielded == pdFALSE )
{
//调用了任务切换:内部就是触发PendSV
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
prvAddCurrentTaskToDelayedList源码分析
//添加任务到延时列表中
//传入两个参数
//xTicksToWait:延迟周期
//xCanBlockIndefinitely :延时的确定状态,阻塞或不阻塞
static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait, const BaseType_t xCanBlockIndefinitely )
{
//延时周期----下次唤醒的时间
TickType_t xTimeToWake;
//系统节拍值,是个全局变量,会在Systick中++
const TickType_t xConstTickCount = xTickCount;
/*把当前任务从就绪列表中移除 */
if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
{
/* The current task must be in a ready list, so there is no need to
check, and the port reset macro can be called directly. */
portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
//是否使用任务挂起功能
#if ( INCLUDE_vTaskSuspend == 1 )
{
//portMAX_DELAY = 0xFFFFFFFF,表示一直阻塞
//xCanBlockIndefinitely =True,表示可以无限阻塞
if( ( xTicksToWait == portMAX_DELAY ) && ( xCanBlockIndefinitely != pdFALSE ) )
{
/* 把任务添加到,挂起列表中去 */
vListInsertEnd( &xSuspendedTaskList, &( pxCurrentTCB->xStateListItem ) );
}
else
{
/* 先去计算,下次唤醒的tick值 */
xTimeToWake = xConstTickCount + xTicksToWait;
/* 每个任务控制块里,状态列表都有一个延时值:value
这个value就是任务延时周期,在systick里面进行比较,是否达到
*/
listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );
//判断下次延时周期,是否小于系统节拍值,那就证明定时已经溢出
if( xTimeToWake < xConstTickCount )
{
/* 溢出就把任务添加到延时溢出列表 */
vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
}
else
{
/* 没有溢出把任务添加到延时列表中,让内核进行调度 */
vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
/* 还要去更新系统的时间片,因为系统时间片永远保存最小的延时周期 */
if( xTimeToWake < xNextTaskUnblockTime )
{
xNextTaskUnblockTime = xTimeToWake;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
}
#else /* INCLUDE_vTaskSuspend */ //调度器没有开启
{
/* 计算下次唤醒的系统节拍值 */
xTimeToWake = xConstTickCount + xTicksToWait;
//赋值到任务控制块里
listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );
if( xTimeToWake < xConstTickCount )
{
/* 溢出添加到延时溢出列表中*/
vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
}
else
{
/* 没有溢出,添加到延时列表中 */
vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
/* 更新时间片 */
if( xTimeToWake < xNextTaskUnblockTime )
{
xNextTaskUnblockTime = xTimeToWake;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
/* Avoid compiler warning when INCLUDE_vTaskSuspend is not 1. */
( void ) xCanBlockIndefinitely;
}
#endif /* INCLUDE_vTaskSuspend */
}
总结:添加任务到延时列表的处理,主要是去计算tick值,通过tick值判断是添加到延时溢出列表、还是延时列表、还是挂起,三种状态进行处理。
4.2 vTaskDelayUntil业务流程
- 挂起调度器
- 判断记录的系统节拍值是否溢出,如果溢出,并且大于当前滴答值,把当前任务添加到延时列表(uint32_t 0xFFFFFFFF 溢出就是0了,溢出并且大于当前滴答值,说明任务还没到达)
- 判断记录的系统节拍,值是否溢出,没有溢出,当前定时间隔小于记录值,或者大于系统节拍值,把当前任务添加到延时列表(认为任务可以触发,添加到延时列表)
- 更新记录值,恢复调度器,进行上下文切换
源码分析
void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement )
{
//下次任务要唤醒的系统节拍值
TickType_t xTimeToWake;
//xAlreadyYielded:表示是否已经进行了任务切换
//xShouldDelay :表示是否需要进行延时处理
BaseType_t xAlreadyYielded, xShouldDelay = pdFALSE;
//挂起调度器
vTaskSuspendAll();
{
/* 获取全局变量系统节拍值,在systick中会++ */
const TickType_t xConstTickCount = xTickCount;
/* 获取任务下次唤醒的系统节拍值 */
xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;
//pxPreviousWakeTime指向上一次保存的任务的唤醒节拍值
//这个时候如果大于当前系统节拍值,无非两种可能
//1、延时周期达到了
//2、整个tick计数值,已经溢出了
if( xConstTickCount < *pxPreviousWakeTime )
{
/*1、下次要唤醒的系统节拍值要小于上次要唤醒的节拍值---表示系统节拍值计数溢出
2、下次要唤醒的系统节拍值大于当前的系统节拍值---表示需要延时
*/
if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) )
{
//标记需要延时
xShouldDelay = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else //系统节拍值没有溢出
{
/*1、下次要唤醒的系统节拍值,小于上次唤醒的系统节拍值---证明系统节拍值溢出,延时进行
2、下次要唤醒的系统节拍值大于当前系统节拍值---证明延时周期,在当前的时间之后
*/
if( ( xTimeToWake < *pxPreviousWakeTime ) || ( xTimeToWake > xConstTickCount ) )
{
//标记延时状态
xShouldDelay = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
/* 保存下次唤醒的节拍值 */
*pxPreviousWakeTime = xTimeToWake;
//判断是否需要延时
if( xShouldDelay != pdFALSE )
{
traceTASK_DELAY_UNTIL( xTimeToWake );
/*添加任务到延时列表中 */
prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
//调度器恢复,如果调度器内部已经进行了任务切换,那么返回一个True
xAlreadyYielded = xTaskResumeAll();
/* 如果调度器没有进行任务切换,那么要进行任务切换 */
if( xAlreadyYielded == pdFALSE )
{
//进行PendSV异常
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
5 任务挂起/任务恢复详解
vTaskSuspendAll业务流程
- ++uxSchedulerSuspended(在分析systick调度的时候也用到过)
- 上下文切换中断判断,uxSchedulerSuspended>0不进行任务切换
void vTaskSuspendAll( void )
{
/* 调取记录值++
这个值是去让Systick中断产生的时候,不去遍历阻塞列表,进行任务恢复
*/
++uxSchedulerSuspended;
}
vTaskResumeAll业务流程
- 进入临界区,挂起记录减1
- 判断是挂起就绪列表(全称是挂起就绪列表)是否为空,不为空,添加任务到就绪列表中,如果优先级高于当前任务,则进行上下文切换。(任务可以在中断中ISR恢复,我们是没法进行任务切换的,所以要放入挂起就绪列表中)
- 判断调度器挂起后的SysTick值,重新遍历阻塞列表,进行上下文切换(每次在systick中,都会判断是否挂起,挂起的话不做处理,但是systick会累加,如果不重新遍历阻塞列表,找出之前错过的任务列表,进行调度)
BaseType_t xTaskResumeAll( void )
{
TCB_t *pxTCB = NULL;
//是否已经任务切换
BaseType_t xAlreadyYielded = pdFALSE;
/* 进入临界段,不想中断打扰 */
configASSERT( uxSchedulerSuspended );
taskENTER_CRITICAL();
{
//调度器记录值减一
--uxSchedulerSuspended;
//如果调度器恢复了
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
{
//判断当前任务数量是否大于0
if( uxCurrentNumberOfTasks > ( UBaseType_t ) 0U )
{
/* 从挂起的就绪列表中遍历*/
while( listLIST_IS_EMPTY( &xPendingReadyList ) == pdFALSE )
{
//获取任务控制块
pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &xPendingReadyList ) );
//移除 挂起就绪列表
//移除 事件列表
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
//添加到就绪列表
prvAddTaskToReadyList( pxTCB );
/* 如果优先级大于当前任务优先级,则进行任务切换 */
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
xYieldPending = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
//获取到的任务控制块不为空
if( pxTCB != NULL )
{
//需要更新系统的时间片
prvResetNextTaskUnblockTime();
}
/* 获取 在调度器挂起时,systick挂起记录值 */
{
UBaseType_t uxPendedCounts = uxPendedTicks; /* Non-volatile copy. */
//如果记录值大于0
if( uxPendedCounts > ( UBaseType_t ) 0U )
{
do
{
//进行systick调度处理,其实就是遍历阻塞列表,如果需要任务切换,返回Ture
if( xTaskIncrementTick() != pdFALSE )
{
//标记任务需要切换
xYieldPending = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
--uxPendedCounts;
//一直遍历,直到 uxPendedCounts= 0
} while( uxPendedCounts > ( UBaseType_t ) 0U );
//赋值为0
uxPendedTicks = 0;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
//如果需要任务切换
if( xYieldPending != pdFALSE )
{
//判断是否内核是抢占式
#if( configUSE_PREEMPTION != 0 )
{
//标记已经进行调度的状态
xAlreadyYielded = pdTRUE;
}
#endif
//进行调度
taskYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
//退出临界段
taskEXIT_CRITICAL();
//返回调度值
return xAlreadyYielded;
}