本文共 10536 字,大约阅读时间需要 35 分钟。
目录
FreeRTOS中的延时列表由如下组件构成,
static List_t xDelayedTaskList1;static List_t xDelayedTaskList2;static List_t * volatile pxDelayedTaskList; // 任务延时列表指针static List_t * volatile pxOverflowDelayedTaskList; // 任务延时溢出列表指针static volatile TickType_t xNextTaskUnblockTime // 下一任务唤醒时间
① 由于记录系统时间的全局变量xTickCount为uint32_t类型,所以一定会发生计数溢出,因此FreeRTOS中定义了2个延时列表,分别用于处理溢出 & 非溢出时的任务延时
当xTickCount计数溢出时,会交换pxDelayedTaskList & pxOverflowDelayedTaskList指针的指向
② 全局变量xNextTaskUnblockTime记录的是pxDelayTaskList指向的延时列表中下一个要被唤醒任务的绝对系统时间
// xTicksToDelay为要延时的tick数void vTaskDelay( const TickType_t xTicksToDelay ){ BaseType_t xAlreadyYielded = pdFALSE; // 如果延时tick数为0,将会触发一次任务切换 if( xTicksToDelay > ( TickType_t ) 0U ) { configASSERT( uxSchedulerSuspended == 0 ); // 此处通过关闭调度器保护全局资源 vTaskSuspendAll(); { prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE ); } xAlreadyYielded = xTaskResumeAll(); } // 如果xTaskResumeAll中没有触发任务调度,此处触发一次 // 因为要将调用vTaskDelay函数的任务阻塞,肯定要进行一次任务调度 if( xAlreadyYielded == pdFALSE ) { portYIELD_WITHIN_API(); }}
说明1:相对延时
① vTaskDelay实现的是相对性延时,指定的延时时间从vTaskDelay调用结束之后开始计算
② 正是由于相对延时的特性,vTaskDelay并不适用于周期性执行任务的场合
注意区分如下2种表述,
a. 每10ms运行一次
b. 每次运行后延时10ms
说明2:prvAddCurrentTaskToDelayedList函数
该函数是实现延时的核心函数
/** xTicksToWait:要延时的tick数* xCanBlockIndefinitely:是否支持挂起式睡眠*/static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait,const BaseType_t xCanBlockIndefinitely ){TickType_t xTimeToWake;const TickType_t xConstTickCount = xTickCount; // 取出当前系统时间 // 将任务从就绪列表删除,如果该优先级已无任务,则注销该优先级 // 调用vTaskDelay函数的任务一定处于就绪列表 if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0 ) { portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority ); } #if ( INCLUDE_vTaskSuspend == 1 ) { if( ( xTicksToWait == portMAX_DELAY ) && ( xCanBlockIndefinitely != pdFALSE ) ) { // 如果延时tick数为portMAX_DELAY且支持挂起式睡眠 // 则直接将任务加入挂起列表 vListInsertEnd( &xSuspendedTaskList, &( pxCurrentTCB->xStateListItem ) ); } else { // xTimeToWake为唤醒当前任务的绝对系统时间 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 /* INCLUDE_vTaskSuspend */ // 不支持vTaskSuspend函数的情况,除了不使用挂起列表,其余流程相同 #endif /* INCLUDE_vTaskSuspend */}
说明3:延时tick数计算宏
FreeRTOS中的pdMS_TO_TICKS宏,提供了将毫秒数转换为ticks数的功能,可用于计算延时tick数
#define pdMS_TO_TICKS( xTimeInMs ) ( ( TickType_t ) \( ( ( TickType_t ) ( xTimeInMs ) * ( TickType_t ) configTICK_RATE_HZ )\/ ( TickType_t ) 1000 ) )
注意:此处一定要先计算乘法后计算除法,因为如果configTICK_RATE_HZ小于1000,先计算除法的结果就是始终为0
/** pxPreviousWakeTime:指向保存上次任务唤醒时间的变量* xTimeIncrement:在上次任务唤醒时间的基础上要延时的tick数*/void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime,const TickType_t xTimeIncrement ){ TickType_t xTimeToWake; BaseType_t xAlreadyYielded, xShouldDelay = pdFALSE; configASSERT( pxPreviousWakeTime ); // 指针有效性 configASSERT( ( xTimeIncrement > 0U ) ); // 延时时间有效性 configASSERT( uxSchedulerSuspended == 0 ); // 不能关闭调度器 // 关闭调度器,实现任务间的资源保护,中断仍可响应 vTaskSuspendAll(); { // 获取当前系统时间 const TickType_t xConstTickCount = xTickCount; // 基于上次任务唤醒时间,计算本次任务唤醒时间 xTimeToWake = *pxPreviousWakeTime + xTimeIncrement; // 判断任务是否需要进入延时 if( xConstTickCount < *pxPreviousWakeTime ) { // xConstTickCount发生溢出 // 那么只要xTimeToWake也溢出,并且超过任务主体时间,则仍需要延时 if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) ) { xShouldDelay = pdTRUE; } } else { // xConstTickCount没有发生溢出 // 那么只要xTimeToWake超过任务主体时间,则仍需要延时 if( ( xTimeToWake < *pxPreviousWakeTime ) || ( xTimeToWake > xConstTickCount ) ) { xShouldDelay = pdTRUE; } } // 更新上次任务唤醒时间,用于下次调用vTaskDelayUntil *pxPreviousWakeTime = xTimeToWake; // 如果任务仍需要延时,则将其加入延时列表 if( xShouldDelay != pdFALSE ) { prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE ); } } xAlreadyYielded = xTaskResumeAll(); // 如果恢复调度器时没有触发任务调度,此处强制触发一次 // 因为如果任务需要延时,需要调度其他任务运行 if( xAlreadyYielded == pdFALSE ) { portYIELD_WITHIN_API(); }}
说明1:绝对延时
① 从vTaskDelayUntil函数的实现可知,任务的唤醒时间是基于上次唤醒时间计算的,因此是一种绝对延时,也就是在固定周期内会唤醒任务
② 调用vTaskDelayUntil函数时传递的参数xTimeIncrement可以理解为任务运行的周期
注意:这里的周期性运行并不是绝对的,会受到任务优先级 & 中断的影响
说明2:判断是否要延时是vTaskDelayUntil函数的关键
结合下面3张示意图,很容易理解何种情况下任务仍然需要延时
① xConstTickCount & xTimeToWake均没有发生溢出
② xConstTickCount没有发生溢出,但xTimeToWake发生溢出
③ xConstTickCount & xTimeToWake均发生溢出
注意:从上述示意图中可见,任务执行的时间必须小于任务周期时间xTimeIncrement,否则周期性的延时是没有意义的
说明3:延时tick数的计算
在调用prvAddCurrentTaskToDelayedList时需要传递的是任务需要延时的tick数,使用的计算方式如下,
xTimeToWake - xConstTickCount
这里需要特别注意,当处于上文中第2种示意图的情况时,xTimeToWake - xConstTickCount得到的是一个负数,但是因为参与计算的变量均为4B,截断后得到的正好是正确的值
作为说明,我们以1B的变量为例,
0x00(xTimeToWake) - 0xFF(xConstTickCount) = -255D = 0b1 0000 0001,
1B变量截断后的值为0x1,这正是要延时的正确tick数
说明4:vTaskDelayUntil函数使用示例
void TestTask(void *pvParameters){ TickType_t PreviousWakeTime; const TickType_t TimeIncrement = pdMS_TO_TICKS(50); PreviousWakeTime = xTaskGetTickCount(); for (;;) { /* 任务主体 */ vTaskDelayUntil(&PreviousWakeTime, TimeIncrement); }}
任务中第1次调用vTaskDelayUntil函数时,需要将PreviousWakeTime初始化为进入任务的for循环的时间点,也就是第1次进入任务主体的时间点。在以后的运行中,vTaskDelayUntil函数中会自动更新PreviousWakeTime
① SysTick属于Cortex-M内核,而不是属于SoC
② SysTick本质上是一个24位的向下计数器
③ 设置好定时周期,使能SysTick中断之后,SysTick会周期性触发中断
④ FreeRTOS使用SysTick作为系统时钟
SysTick共有4个寄存器,
① CTRL寄存器
② LOAD寄存器
vPortSetupTimerInterrupt函数中,对SysTick进行了设置,该函数调用关系如下,
vTaskStartScheduler--> xPortStartScheduler --> vPortSetupTimerInterrupt
① SysTick重装载值根据硬件系统时钟频率和操作系统时钟频率计算得到
② 在控制寄存器中,设置使用系统时钟,并使能中断与SysTick
至此,SysTick即可按照configTICK_RATE_HZ的设置产生中断
注意:由于SysTick的计数值是有24位上限的,所以定时的周期也有上限,也就是2^24 / configSYSTICK_CLOCK_HZ
configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ = 2^24--> 1 / configTICK_RATE_HZ = 2^24 / configSYSTICK_CLOCK_HZ--> configTICK_RATE_HZ = configSYSTICK_CLOCK_HZ / 2^24这里被限制的其实是configTICK_RATE_HZ可设置的值的下限
SysTick中断处理函数的核心是调用xTaskIncrementTick函数,并根据该函数的返回值,确定是否需要触发任务调度
说明:此处开关中断为何没有使用portSET_INTERRUPT_MASK_FROM_ISR
正如代码中注释,SysTick中断优先级最低,所以当该中断运行时,不会抢占其他中断,所以在确知不会发生中断嵌套的情况下,没有保存 & 恢复当前BASEPRI寄存器的值
BaseType_t xTaskIncrementTick( void ){TCB_t * pxTCB;TickType_t xItemValue;BaseType_t xSwitchRequired = pdFALSE; if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ) { // 任务调度器没有关闭 // 增加系统tick计数 const TickType_t xConstTickCount = xTickCount + 1; xTickCount = xConstTickCount; // 如果系统tick计数溢出,则交换延时列表 if( xConstTickCount == ( TickType_t ) 0U ) { taskSWITCH_DELAYED_LISTS(); } // 如果系统当前时间 >= 下一任务唤醒时间,说明有任务延时到期,需要唤醒 if( xConstTickCount >= xNextTaskUnblockTime ) { for( ;; ) { if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE ) { // 延时列表为空,设置最大下一任务解锁时间,并退出循环 xNextTaskUnblockTime = portMAX_DELAY; break; } else { // 取出延时列表第1个任务的唤醒时间 // 延时列表中的任务按唤醒时间升序排列 pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList ); xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) ); // 如果延时列表队首任务已经无需唤醒 // 则更新下一任务唤醒时间并退出循环 if( xConstTickCount < xItemValue ) { xNextTaskUnblockTime = xItemValue; break; } // 将任务从延时队列删除 ( void ) uxListRemove( &( pxTCB->xStateListItem ) ); // 如果延时的任务还在等待事件,则将其从相应的事件列表中删除 // 此时对应任务带延时等待事件已超时 if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL ) { ( void ) uxListRemove( &( pxTCB->xEventListItem )); } // 将唤醒的任务加入就绪列表 prvAddTaskToReadyList( pxTCB ); // 如果使能抢占,则当唤醒任务优先级高于当前任务时, // 标识需要触发任务调度 #if ( configUSE_PREEMPTION == 1 ) { if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority ) { xSwitchRequired = pdTRUE; } } #endif /* configUSE_PREEMPTION */ } } } // 如果使能抢占与同优先级共享时间片,且当前任务优先级下有其他任务, // 标识需要触发任务调度 #if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) { if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 ) { xSwitchRequired = pdTRUE; } } #endif } else { // 如果任务调度器被关闭,记录pending的时钟tick // 被pending的时钟tick将在xTaskResumeAll函数中被处理 ++uxPendedTicks; } // 如果使能抢占,检查是否有被pending的调度请求 #if ( configUSE_PREEMPTION == 1 ) { if( xYieldPending != pdFALSE ) { xSwitchRequired = pdTRUE; } } #endif /* configUSE_PREEMPTION */ return xSwitchRequired;}
说明:交换延时列表
当系统tick计数溢出时,会调用taskSWITCH_DELAYED_LISTS宏交换延时列表
注意:在交换延时列表后,需要更新下一任务唤醒时间,即用交换后的延时列表队首任务的唤醒时间更新下一任务唤醒时间(如果没有任务,则设置为portMAX_DELAY)
转载地址:http://kqwx.baihongyu.com/