转载自 https://blog.csdn.net/zhoutaopower/article/details/107101723
在《FreeRTOS --(7)任务管理之入门篇》中讲过,如果有几个任务同时跑,但是又都不阻塞的话,那么最高优先级的任务将会占领整个 CPU,因为每次都会调度到它,一直处于 Ready 状态,所以呢,调度器每次都要选择优先级最高的任务来让它执行;所以,不管怎么样,任务做完自己该做的事情,就应该进入阻塞状态,等待下次该自己做任务的时候,在占领 CPU,这样既可以让 Idle 线程,在系统空闲的时候跑,也可以让让任务在合理的时间占领 CPU;
之前也说过,让任务进入阻塞状态的方式有两种:
1、让任务延时:因为任务是一个 While 1 的无限循环,所以执行完自己的事情后,可以调用接口进行延时,进入阻塞,让出 CPU;
2、让任务等待某个事件:当任务需要的资源满足不了的时候,可以让任务阻塞的等待所需的资源,这样也是合理的;
这章就是要讲让任务进入延时进入阻塞的方法以及相关的原理;
1、接口介绍
任务执行完自己的事情后,可以调用如下接口,让任务进入阻塞态,delay 一段时间:
1、vTaskDelay()
2、vTaskDelayUntil()
下面分别来介绍这两个函数的用法和官方的解释;
1.1、vTaskDelay
1.1.1、Usage
这个函数是相对延时函数,它的函数原型为:
/** * task. h * <pre>void vTaskDelay( const TickType_t xTicksToDelay );</pre> * * Delay a task for a given number of ticks. The actual time that the * task remains blocked depends on the tick rate. The constant * portTICK_PERIOD_MS can be used to calculate real time from the tick * rate - with the resolution of one tick period. * * INCLUDE_vTaskDelay must be defined as 1 for this function to be available. * See the configuration section for more information. * * * vTaskDelay() specifies a time at which the task wishes to unblock relative to * the time at which vTaskDelay() is called. For example, specifying a block * period of 100 ticks will cause the task to unblock 100 ticks after * vTaskDelay() is called. vTaskDelay() does not therefore provide a good method * of controlling the frequency of a periodic task as the path taken through the * code, as well as other task and interrupt activity, will effect the frequency * at which vTaskDelay() gets called and therefore the time at which the task * next executes. See vTaskDelayUntil() for an alternative API function designed * to facilitate fixed frequency execution. It does this by specifying an * absolute time (rather than a relative time) at which the calling task should * unblock. * * @param xTicksToDelay The amount of time, in tick periods, that * the calling task should block. * * Example usage: void vTaskFunction( void * pvParameters ) { // Block for 500ms. const TickType_t xDelay = 500 / portTICK_PERIOD_MS; for( ;; ) { // Simply toggle the LED every 500ms, blocking between each toggle. vToggleLED(); vTaskDelay( xDelay ); } } * \defgroup vTaskDelay vTaskDelay * \ingroup TaskCtrl */ void vTaskDelay( const TickType_t xTicksToDelay ) PRIVILEGED_FUNCTION;
官方的注释写得非常非常仔细,甚至于用法都写进了注释;
任务调用这个函数,传入一个 xTicksToDelay ,代表相对现在,对任务进行延时;这个的这个 xTicksToDelay 的单位是 Tick,比如,你 1ms 一个 Tick 的话,那么设置 50,就是延时 50ms;如果 10ms 一个 Tick 的话,设置 50,就是延时 500ms;
实际情况是,将当前任务阻塞,到时间的时候,再把它加入到就绪队列,参与调度;
简单的 Demo 如下所示:
void vTaskFunction( void * pvParameters ) { // Block for 250ms. const TickType_t xDelay250ms = pdMS_TO_TICKS( 250 ); for( ;; ) { // Simply toggle the LED every 250ms, blocking between each toggle. vToggleLED(); vTaskDelay( xDelay250ms ); } }
时序上比如:
1.1.2、Implement
知道了用法后,我们来看下 vTaskDelay 的实现,在 task.c 文件中:
#if ( INCLUDE_vTaskDelay == 1 ) void vTaskDelay( const TickType_t xTicksToDelay ) { BaseType_t xAlreadyYielded = pdFALSE; /* 如果延时时间为0,则不会将当前任务加入延时列表 */ if( xTicksToDelay > ( TickType_t ) 0U ) { vTaskSuspendAll(); { /* 将当前任务从就绪列表中移除,并根据当前系统节拍计数器值计算唤醒时间,然后将任务加入延时列表 */ prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE ); } xAlreadyYielded = xTaskResumeAll(); } /* 强制执行一次上下文切换*/ if( xAlreadyYielded == pdFALSE ) { portYIELD_WITHIN_API(); } } #endif /* INCLUDE_vTaskDelay */
vTaskDelay 的实现依赖于宏 INCLUDE_vTaskDelay ,必须定义这个宏,才能够使用这个函数;
先判空,如果入参,也就是需要延时的时间大于 0 才有效;
先调用 vTaskSuspendAll(); 来挂起调度器,暂时暂停调度;
然后调用 prvAddCurrentTaskToDelayedList 将当前的这个任务添加到 Delayed 链表;
这个函数稍后深入分析,这里先扩展分析一下几个链表:
/* Lists for ready and blocked tasks. -------------------- xDelayedTaskList1 and xDelayedTaskList2 could be move to function scople but doing so breaks some kernel aware debuggers and debuggers that rely on removing the static qualifier. */ PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];/*< Prioritised ready tasks. */ PRIVILEGED_DATA static List_t xDelayedTaskList1; /*< Delayed tasks. */ PRIVILEGED_DATA static List_t xDelayedTaskList2; /*< Delayed tasks (two lists are used - one for delays that have overflowed the current tick count. */ PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList; /*< Points to the delayed task list currently being used. */ PRIVILEGED_DATA static List_t * volatile pxOverflowDelayedTaskList; /*< Points to the delayed task list currently being used to hold tasks that have overflowed the current tick count. */ PRIVILEGED_DATA static List_t xPendingReadyList; /*< Tasks that have been readied while the scheduler was suspended. They will be moved to the ready list when the scheduler is resumed. */
对于一个任务,有很多种状态,可能是 Running、Ready、Blocked、Suspend,那对于不一样的的状态,FreeRTOS 中将其挂接到不同的链表,进行管理;
单核情况下,同一时间,只有一个任务处于 Running 状态,所以用 pxCurrentTCB 便可以代表了 Running 状态;
Blocked 阻塞态的任务,阻塞在时间上的任务,被挂接到名为 xxxDelayedTaskListx 上:
PRIVILEGED_DATA static List_t xDelayedTaskList1; /*< Delayed tasks. */ PRIVILEGED_DATA static List_t xDelayedTaskList2; /*< Delayed tasks (two lists are used - one for delays that have overflowed the current tick count. */ PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList; /*< Points to the delayed task list currently being used. */ PRIVILEGED_DATA static List_t * volatile pxOverflowDelayedTaskList; /*< Points to the delayed task list currently being used to hold tasks that have overflowed the current tick count. */
用了两个链表,主要是为了处理时间回绕的场景(时间回绕,的含义为,用 U32 来记录运行时间,每次 SysTick 的时候增加1个计数,由于 SysTick 的周期我们是知道的,比如 1ms,所以我们就知道当前的时间,但是如果系统长时间运行,记录时间的 U32 势必会溢出,就导致了时间回绕);具体的处理方式我们细细来品;
最后调用 xTaskResumeAll 查看是否有需要调度的任务,有的话,强制触发一次调度;
下面来看看 prvAddCurrentTaskToDelayedList 的实现:
static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait, const BaseType_t xCanBlockIndefinitely ) { TickType_t xTimeToWake; const TickType_t xConstTickCount = xTickCount; #if( INCLUDE_xTaskAbortDelay == 1 ) { /* About to enter a delayed list, so ensure the ucDelayAborted flag is reset to pdFALSE so it can be detected as having been set to pdTRUE when the task leaves the Blocked state. */ pxCurrentTCB->ucDelayAborted = pdFALSE; } #endif /* Remove the task from the ready list before adding it to the blocked list as the same list item is used for both lists. */ 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 ); /*lint !e931 pxCurrentTCB cannot change as it is the calling task. pxCurrentTCB->uxPriority and uxTopReadyPriority cannot change as called with scheduler suspended or in a critical section. */ } else { mtCOVERAGE_TEST_MARKER(); } #if ( INCLUDE_vTaskSuspend == 1 ) { if( ( xTicksToWait == portMAX_DELAY ) && ( xCanBlockIndefinitely != pdFALSE ) ) { /* Add the task to the suspended task list instead of a delayed task list to ensure it is not woken by a timing event. It will block indefinitely. */ vListInsertEnd( &xSuspendedTaskList, &( pxCurrentTCB->xStateListItem ) ); } else { /* Calculate the time at which the task should be woken if the event does not occur. This may overflow but this doesn't matter, the kernel will manage it correctly. */ xTimeToWake = xConstTickCount + xTicksToWait; /* The list item will be inserted in wake time order. */ listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake ); if( xTimeToWake < xConstTickCount ) { /* Wake time has overflowed. Place this item in the overflow list. */ vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) ); } else { /* The wake time has not overflowed, so the current block list is used. */ vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) ); /* If the task entering the blocked state was placed at the head of the list of blocked tasks then xNextTaskUnblockTime needs to be updated too. */ if( xTimeToWake < xNextTaskUnblockTime ) { xNextTaskUnblockTime = xTimeToWake; } else { mtCOVERAGE_TEST_MARKER(); } } } } #else /* INCLUDE_vTaskSuspend */ { /* Calculate the time at which the task should be woken if the event does not occur. This may overflow but this doesn't matter, the kernel will manage it correctly. */ xTimeToWake = xConstTickCount + xTicksToWait; /* The list item will be inserted in wake time order. */ listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake ); if( xTimeToWake < xConstTickCount ) { /* Wake time has overflowed. Place this item in the overflow list. */ vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) ); } else { /* The wake time has not overflowed, so the current block list is used. */ vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) ); /* If the task entering the blocked state was placed at the head of the list of blocked tasks then xNextTaskUnblockTime needs to be updated too. */ if( xTimeToWake < xNextTaskUnblockTime ) { xNextTaskUnblockTime = xTimeToWake; } else { mtCOVERAGE_TEST_MARKER(); } } /* Avoid compiler warning when INCLUDE_vTaskSuspend is not 1. */ ( void ) xCanBlockIndefinitely; } #endif /* INCLUDE_vTaskSuspend */ }
代码的逻辑是:
1、首先,当前的这个任务,肯定是处于 Ready 链表,所以将它从 Ready 链表移除;
2、获取当前的绝对时间 xTickCount,也就是 SysTick 会累积增加的那个,然后将延时的时间加上这个基准时间,配置成为唤醒该任务的时间,并赋值给这个 Item 的 Value 字段,并将其挂接到 Delay 链表;
虽然代码逻辑如上所示,不过在链表挂接的时候,需要处理一些临界状态,比如,将当前任务从 Ready 链表中拿去的时候,需要判断当前 Ready 链表中,拿去这个任务后,是否已为空,如果是这样的话,就要清除记录优先级对应的 uxTopReadyPriority (Bitmap);
配置唤醒时间的时候,就要通过比对时间基准来判断 U32 的回绕,如果时间回绕,那么将其挂接到 pxOverflowDelayedTaskList 这个链表,否则挂接到 pxDelayedTaskList 链表;
唤醒的时间如果比最近的唤醒时间还早,那么需要更新唤醒时间到全局变量 xNextTaskUnblockTime 中,在 SysTick 来的时候进行判断比对唤醒时间;
总的来说,vTaskDelay 接口实现基于当前时间的一个增量延时,并 Block 了当前任务;
1.2、vTaskDelayUntil
我们再来看看这个 vTaskDelayUntil ;
1.2.1、Usage
这个函数是绝对延时函数,可以用来做周期性任务(必须最高优先级才可以),它的函数原型为:
/** * task. h * <pre>void vTaskDelayUntil( TickType_t *pxPreviousWakeTime, const TickType_t xTimeIncrement );</pre> * * INCLUDE_vTaskDelayUntil must be defined as 1 for this function to be available. * See the configuration section for more information. * * Delay a task until a specified time. This function can be used by periodic * tasks to ensure a constant execution frequency. * * This function differs from vTaskDelay () in one important aspect: vTaskDelay () will * cause a task to block for the specified number of ticks from the time vTaskDelay () is * called. It is therefore difficult to use vTaskDelay () by itself to generate a fixed * execution frequency as the time between a task starting to execute and that task * calling vTaskDelay () may not be fixed [the task may take a different path though the * code between calls, or may get interrupted or preempted a different number of times * each time it executes]. * * Whereas vTaskDelay () specifies a wake time relative to the time at which the function * is called, vTaskDelayUntil () specifies the absolute (exact) time at which it wishes to * unblock. * * The constant portTICK_PERIOD_MS can be used to calculate real time from the tick * rate - with the resolution of one tick period. * * @param pxPreviousWakeTime Pointer to a variable that holds the time at which the * task was last unblocked. The variable must be initialised with the current time * prior to its first use (see the example below). Following this the variable is * automatically updated within vTaskDelayUntil (). * * @param xTimeIncrement The cycle time period. The task will be unblocked at * time *pxPreviousWakeTime + xTimeIncrement. Calling vTaskDelayUntil with the * same xTimeIncrement parameter value will cause the task to execute with * a fixed interface period. * * Example usage: <pre> // Perform an action every 10 ticks. void vTaskFunction( void * pvParameters ) { TickType_t xLastWakeTime; const TickType_t xFrequency = 10; // Initialise the xLastWakeTime variable with the current time. xLastWakeTime = xTaskGetTickCount (); for( ;; ) { // Wait for the next cycle. vTaskDelayUntil( &xLastWakeTime, xFrequency ); // Perform action here. } } </pre> * \defgroup vTaskDelayUntil vTaskDelayUntil * \ingroup TaskCtrl */ void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement ) PRIVILEGED_FUNCTION
两个入参:
pxPreviousWakeTime:初始化为当前的时间,后面便不用管了;
xTimeIncrement:任务的周期;
简单的 Demo 如下所示:
void vTaskB( void * pvParameters ) { static portTickType xLastWakeTime; const portTickType xFrequency = pdMS_TO_TICKS(500); // 使用当前时间初始化变量xLastWakeTime ,注意这和vTaskDelay()函数不同 xLastWakeTime = xTaskGetTickCount(); for( ;; ) { /* 调用系统延时函数,周期性阻塞500ms */ vTaskDelayUntil( &xLastWakeTime,xFrequency ); // ... // 这里为任务主体代码,周期性执行.注意这和vTaskDelay()函数也不同 // ... } }
1.2.2、Implement
它的实现如下所示:
#if ( INCLUDE_vTaskDelayUntil == 1 ) 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(); { /* Minor optimisation. The tick count cannot change in this block. */ const TickType_t xConstTickCount = xTickCount; /* Generate the tick time at which the task wants to wake. */ xTimeToWake = *pxPreviousWakeTime + xTimeIncrement; if( xConstTickCount < *pxPreviousWakeTime ) { /* The tick count has overflowed since this function was lasted called. In this case the only time we should ever actually delay is if the wake time has also overflowed, and the wake time is greater than the tick time. When this is the case it is as if neither time had overflowed. */ if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) ) { xShouldDelay = pdTRUE; } else { mtCOVERAGE_TEST_MARKER(); } } else { /* The tick time has not overflowed. In this case we will delay if either the wake time has overflowed, and/or the tick time is less than the wake time. */ if( ( xTimeToWake < *pxPreviousWakeTime ) || ( xTimeToWake > xConstTickCount ) ) { xShouldDelay = pdTRUE; } else { mtCOVERAGE_TEST_MARKER(); } } /* Update the wake time ready for the next call. */ *pxPreviousWakeTime = xTimeToWake; if( xShouldDelay != pdFALSE ) { traceTASK_DELAY_UNTIL( xTimeToWake ); /* prvAddCurrentTaskToDelayedList() needs the block time, not the time to wake, so subtract the current tick count. */ prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE ); } else { mtCOVERAGE_TEST_MARKER(); } } xAlreadyYielded = xTaskResumeAll(); /* Force a reschedule if xTaskResumeAll has not already done so, we may have put ourselves to sleep. */ if( xAlreadyYielded == pdFALSE ) { portYIELD_WITHIN_API(); } else { mtCOVERAGE_TEST_MARKER(); } } #endif /* INCLUDE_vTaskDelayUntil */
也是先挂起任务,最后恢复;
与 xTaskDelay 不同,它定义了几个变量:
*pxPreviousWakeTime 代表上一次解除阻塞执行任务的时间;
xConstTickCount 是当前的时间;
xTimeToWake 是下一次唤醒的时间;
首先还是判断 Tick 溢出的场景;
可以看到,在最后调用:
prvAddCurrentTaskToDelayedList
的时候,它每次都是动态的去调整 Delay 的时间:xTimeToWake - xConstTickCount,尽量做到任务执行开始的时间保持一致;