十二、任务管理 我们要澄清在任一时刻CPU只能运行一个任务当系统中有多个任务那么由谁来决定运行哪个就是调度器调度器会不停地启动、停止要运行的每一个任务宏观上像是所有的任务都在同时运行。那么调度器是怎么实现这个过程的呢其实就是在任务切换的时候也就是切入切出时保存上下文环境寄存器的值、堆栈的内容。这样的话每个在FreeRTOS中的任务需要有自己独立的栈空间当任务切出的时候它的执行环境会被保存在该任务的栈空间中这样当任务再次运行的时候就能从堆栈中正确的恢复上次的运行环境。任务越多所需的堆栈空间也就越大反过来说系统可用的SRAM多大就决定着任务的多少。FreeRTOS中的任务有两种调度机制一是抢占式调度高优先级抢占低优先级低优先级需要等待高优先级阻塞或者结束方可被调度二是时间片轮转调度。任务是可以被删除的通过调用任务删除API函数实现。FreeRTOS中提供的调度器是基于优先级的全抢占式调度在系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外系统的其它部分都是可以抢占的。当有比当前优先级更高的任务就绪时当前任务将立刻被切出高优先级任务抢占处理器运行。FreeRTOS内核中也允许创建相同优先级的任务相同优先级的任务采用时间片轮转的方式进行调度时间片轮转调度仅在当前系统中无更高优先级就绪任务存在的情况下才有效。任务调度的原则是一旦任务状态发生了改变并且当前运行的任务优先级小于优先级队列组中任务最高优先级时立刻进行任务切换。任务的状态有哪几种其实是3种就绪(Ready)该任务在就绪列表中就绪的任务已经具备执行的能力只等待调度器进行调度新创建的任务会初始化为就绪态。运行(Running):该状态表明任务正在执行此时它占用处理器FreeRTOS调度器选择运行的永远是处于最高优先级的就绪态任务当任务被运行的一刻它的任务状态就变成了运行态。实际上在FreeRTOS是没有这个状态也是就绪态只是它是处于最高优先级的就绪态阻塞(Blocked):如果任务当前正在等待某个时序或者外部中断我们就说这个任务处于阻塞状态该任务不在就绪列表中。包含任务被延时、任务正在等待信号量、读写队列或者等待读写事件等。挂起(Suspended):处于挂起态的任务对调度器而言是不存在的让一个任务处于挂起态的唯一办法就是调用vTaskSuspend()函数而把一个处于挂起态的任务恢复的唯一途径就是调用vTaskResume()或vTaskResumeFromISR()函数。任务状态的迁移(1).从创建的任务的时候他就会被添加到就绪列表中。处于就绪列表中该任务是随时可以被切换的。也就是说他已经具备了任务的执行条件。(2).假如新创建的任务是最高优先级的那么调度器会直接运行它所以他会直接到运行这里。运行态说白了就是具备最高优先级的就绪任务。(3).当从运行态到就绪态说明已经有了一个更高优先级的就绪任务抢占了它所以它会变成就绪态而运行了一个更高优先级的任务。(4).从运行态到阻塞态主动调用延时函数或者等待信号量他就会变为阻塞态这里是就绪态变为阻塞态会从就绪列表中把它移除添加到阻塞列表中。(5).当等待到了信号量或者延时时间到了就会从阻塞态变为就绪态如果此时这个任务处于最高优先级的就绪态的话他又会进入到运行态。(6.7.8).处于就绪态或者阻塞态或者运行态只要它被调用了vTaskSuspend()函数也就是主动调用了一个挂起函数他都会直接变为挂起态(9).挂起态变为就绪态它必须是调用了vTaskResume()或vTaskResumeFromISR()函数才可以变为就绪态。接下来我们来看一下真正的函数是什么样的vTaskSuspend()挂起指定任务被挂起的任务绝不会得到CPU的使用权不管任务具备什么样的优先级。参数任务句柄。比如说想把阻塞任务挂起我们只需要把阻塞任务的对应的任务句柄传递进去他就会被挂起同样的就绪任务把他的任务句柄传递进去也会被挂起。#if ( INCLUDE_vTaskSuspend 1 ) void vTaskSuspend( TaskHandle_t xTaskToSuspend ) { TCB_t *pxTCB; taskENTER_CRITICAL(); { /* If null is passed in here then it is the running task that is being suspended. */ pxTCB prvGetTCBFromHandle( xTaskToSuspend );//通过任务句柄获取任务控制块 traceTASK_SUSPEND( pxTCB );//宏定义没有实现不用管 /* Remove task from the ready/delayed list and place in the suspended list. */ if( uxListRemove( ( pxTCB-xStateListItem ) ) ( UBaseType_t ) 0 ) { taskRESET_READY_PRIORITY( pxTCB-uxPriority ); } else { mtCOVERAGE_TEST_MARKER();//宏定义没有实现不用管 } /* Is the task waiting on an event also? */ if( listLIST_ITEM_CONTAINER( ( pxTCB-xEventListItem ) ) ! NULL ) { ( void ) uxListRemove( ( pxTCB-xEventListItem ) ); } else { mtCOVERAGE_TEST_MARKER(); } vListInsertEnd( xSuspendedTaskList, ( pxTCB-xStateListItem ) ); } taskEXIT_CRITICAL(); if( xSchedulerRunning ! pdFALSE ) { /* Reset the next expected unblock time in case it referred to the task that is now in the Suspended state. */ taskENTER_CRITICAL(); { prvResetNextTaskUnblockTime(); } taskEXIT_CRITICAL(); } else { mtCOVERAGE_TEST_MARKER(); } if( pxTCB pxCurrentTCB ) { if( xSchedulerRunning ! pdFALSE ) { /* The current task has just been suspended. */ configASSERT( uxSchedulerSuspended 0 ); portYIELD_WITHIN_API(); } else { /* The scheduler is not running, but the task that was pointed to by pxCurrentTCB has just been suspended and pxCurrentTCB must be adjusted to point to a different task. */ if( listCURRENT_LIST_LENGTH( xSuspendedTaskList ) uxCurrentNumberOfTasks ) { /* No other tasks are ready, so set pxCurrentTCB back to NULL so when the next task is created pxCurrentTCB will be set to point to it no matter what its relative priority is. */ pxCurrentTCB NULL; } else { vTaskSwitchContext(); } } } else { mtCOVERAGE_TEST_MARKER(); } } #endif /* INCLUDE_vTaskSuspend */ /*-----------------------------------------------------------*/首先进入临界段就说明这一段它是不想被打断的。因为他需要将一个任务从一个列表中移除而移除的时候需要遍历列表这个时间是不确定的所以他不想要被别人打扰。那我们就进入临界段来处理这些操作就好了。首先通过任务句柄获取任务控制块假如没有传入对应的任务句柄他会怎样呢他就会返回当前任务控制块。因此只要传入了任务句柄就会返回对应的任务控制块。假如没有传或者传入了一个NULL他就会返回当前任务控制块的指针可以看prvGetTCBFromHandle()函数。所以无论你传了什么他都会把你挂起。传了NULL他就把自己挂起掉。既然找了一个任务控制块那么就把要挂起的任务从对应的列表之中删除掉他可能在就绪列表中也可能在延时列表中所谓延时也就是阻塞列表如果删除了列表中没有其他任务了那么我么就把对应的任务让它置零。也就是说当前优先级无任务我们就把他清零。如果删除了列表中还有其他任务我们就不能把他的位图清零。然后我们再看看他是不是还有其他等待的事件比如等待的信号量假如我在阻塞态在等待一个信号量但还没等到就把他挂起了挂起之后肯定不能再等待该信号量了那我们就把它从等待的列表中删除掉。然后我们再将这个任务已经移除的再插入到已经挂起的列表中尾部。退出临界段。假如任务在延时的时候被挂起就要把下一个要阻塞的时间给重置一下。假如我挂起的这个任务就是下一个要解除的任务我已经把它挂起了他不能解除。也就是说下一个解除的任务为A此时A又被挂起了那么下一个肯定是是不能解除A就可能要解除他的后面一个B所以我们要更新一下下一个要解除阻塞任务的时间。假如任务是当前任务并且调度器也在运行的时候那我们就要切换一次任务。因为我们已经把当前任务挂起了就要切换一下。如果没有更高优先级任务的话就把当前任务设置为NULL否则的话就选择一次新更高优先级的任务。接下来我们讲解一下恢复函数vTaskResume()它是唯一可以恢复挂起任务的函数。或者vTaskResumeFromISR()也是可以的专门用在中断中恢复一个任务无论通过调用一次或者多次vTaskResume()函数而被挂起的任务也只需调用一次恢复任务函数就可以恢复。如果被恢复任务在所有就绪态任务中处于最高优先级那么系统将进行任务上下文的切换即任务立即被执行。#if ( INCLUDE_vTaskSuspend 1 ) void vTaskResume( TaskHandle_t xTaskToResume ) { TCB_t * const pxTCB ( TCB_t * ) xTaskToResume; /* It does not make sense to resume the calling task. */ configASSERT( xTaskToResume ); /* The parameter cannot be NULL as it is impossible to resume the currently executing task. */ if( ( pxTCB ! NULL ) ( pxTCB ! pxCurrentTCB ) ) { taskENTER_CRITICAL(); { if( prvTaskIsTaskSuspended( pxTCB ) ! pdFALSE ) { traceTASK_RESUME( pxTCB ); /* As we are in a critical section we can access the ready lists even if the scheduler is suspended. */ ( void ) uxListRemove( ( pxTCB-xStateListItem ) ); prvAddTaskToReadyList( pxTCB ); /* We may have just resumed a higher priority task. */ if( pxTCB-uxPriority pxCurrentTCB-uxPriority ) { /* This yield may not cause the task just resumed to run, but will leave the lists in the correct state for the next yield. */ taskYIELD_IF_USING_PREEMPTION(); } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } } taskEXIT_CRITICAL(); } else { mtCOVERAGE_TEST_MARKER(); } } #endif /* INCLUDE_vTaskSuspend */首先它的参数也是任务的控制块表明是哪个任务想要恢复。假如该任务控制块不是NULL或者不是当前任务控制块因为当前任务是正在运行的不可能自己来调用来恢复自己。所以不能是NULL也不能是当前任务。进入临界段。判断任务是否被挂起如果是就恢复刚刚插入的时候将该任务的状态列表插入到挂起列表之中那么恢复的时候就将它从挂起列表中删除掉再把它添加到就绪列表之中再判断一下恢复的任务是否大于当前任务的优先级如果大于则要进行一次任务切换如果不大于只需要插入就绪列表就可以了不执行操作放入就绪列表中就说明它已经具备执行的资格就等待调度器的调度了。因此从挂起列表中恢复任务也非常简单核心就是从挂起列表中删除插入到就绪列表当中如果需要任务的切换就进行一次任务的切换。vTaskResumeFromISR()这个函数也是很简单的他只是在中断中恢复。#if ( ( INCLUDE_xTaskResumeFromISR 1 ) ( INCLUDE_vTaskSuspend 1 ) ) BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume ) { BaseType_t xYieldRequired pdFALSE; TCB_t * const pxTCB ( TCB_t * ) xTaskToResume; UBaseType_t uxSavedInterruptStatus; configASSERT( xTaskToResume ); /* RTOS ports that support interrupt nesting have the concept of a maximum system call (or maximum API call) interrupt priority. Interrupts that are above the maximum system call priority are keep permanently enabled, even when the RTOS kernel is in a critical section, but cannot make any calls to FreeRTOS API functions. If configASSERT() is defined in FreeRTOSConfig.h then portASSERT_IF_INTERRUPT_PRIORITY_INVALID() will result in an assertion failure if a FreeRTOS API function is called from an interrupt that has been assigned a priority above the configured maximum system call priority. Only FreeRTOS functions that end in FromISR can be called from interrupts that have been assigned a priority at or (logically) below the maximum system call interrupt priority. FreeRTOS maintains a separate interrupt safe API to ensure interrupt entry is as fast and as simple as possible. More information (albeit Cortex-M specific) is provided on the following link: http://www.freertos.org/RTOS-Cortex-M3-M4.html */ portASSERT_IF_INTERRUPT_PRIORITY_INVALID(); uxSavedInterruptStatus portSET_INTERRUPT_MASK_FROM_ISR(); { if( prvTaskIsTaskSuspended( pxTCB ) ! pdFALSE ) { traceTASK_RESUME_FROM_ISR( pxTCB ); /* Check the ready lists can be accessed. */ if( uxSchedulerSuspended ( UBaseType_t ) pdFALSE ) { /* Ready lists can be accessed so move the task from the suspended list to the ready list directly. */ if( pxTCB-uxPriority pxCurrentTCB-uxPriority ) { xYieldRequired pdTRUE; } else { mtCOVERAGE_TEST_MARKER(); } ( void ) uxListRemove( ( pxTCB-xStateListItem ) ); prvAddTaskToReadyList( pxTCB ); } else { /* The delayed or ready lists cannot be accessed so the task is held in the pending ready list until the scheduler is unsuspended. */ vListInsertEnd( ( xPendingReadyList ), ( pxTCB-xEventListItem ) ); } } else { mtCOVERAGE_TEST_MARKER(); } } portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus ); return xYieldRequired; } #endif /* ( ( INCLUDE_xTaskResumeFromISR 1 ) ( INCLUDE_vTaskSuspend 1 ) ) */首先他也是要获取一下要恢复任务的任务控制块然后检查下他是不是被挂起了如果没有被挂起就什么也不做退出临界段就可以了已经被挂起了才能够进行恢复再检查下调度器有没有被挂起要是被挂起就不能就行任务的切换当它没有被挂起才能进行一次任务的切换如果被恢复的任务的优先级比当前任务的优先级要高的时候这里是它首先先判断一下如果大于的话就把该变量设为pdTRUE,该变量在函数内声明默认pdFALSE,把它定义为pdTRUE就说明要进行一次任务的切换。在后面就需要把任务从挂起列表里移除并且添加到就绪列表之中这两段可以放在判断之前。假如调度器已经被挂起了就无法直接访问就绪列表因此就需要将将要恢复的任务添加到挂起就绪列表之中该列表是当调度器被挂起的时候就绪列表的内容就放在这里在调度器被恢复的时候该列表的数据又被重新放回到就绪列表之中他只是一个中间的列表。这里要说明一下不只是任务可以被挂起调度器也可以被挂起当调度器挂起的时候我有个任务要恢复了但是此时是不能访问就绪列表的因为你访问了就绪列表可能就会进行一次任务切换但是当调度器挂起的时候是不能进行任务切换的所以我们就把一个任务首先添加到挂起就绪列表之中当恢复调度器的时候就会从挂起就绪列表之中拿到一些任务并且把它添加到就绪列表之中。最后返回一个是否需要任务切换的一个变量。所以我们调用这个函数的时候要判断他的返回值是pdFALSE还是pdTRUE当pdTRUE就要在中断之中进行一次任务的切换。vTaskSuspendAll()和vTaskResumeAll()vTaskSuspendAll()其实挂起所有任务就是挂起任务调度器。超级简单void vTaskSuspendAll( void ) { /* A critical section is not required as the variable is of type BaseType_t. Please read Richard Barrys reply in the following link to a post in the FreeRTOS support forum before reporting this as a bug! - http://goo.gl/wu4acr */ uxSchedulerSuspended; }通过uxSchedulerSuspended变量记录挂起多少次。在调度器挂起期间被中断恢复的任务将被挂载到xPendingReadyList待处理就绪列表中。那么调用对应的vTaskResumeAll()会将变量uxSchedulerSuspended减一。将任何准备好的任务从待处理就绪列表移动到相应的就绪列表中。重置下一个任务的解除阻塞时间。更新时基确保滴答定时器的计数不会滑动。调用了多少次的vTaskSuspendAll()就要调用多少次vTaskResumeAll()进行恢复。​ BaseType_t xTaskResumeAll( void ) { TCB_t *pxTCB NULL; BaseType_t xAlreadyYielded pdFALSE; /* If uxSchedulerSuspended is zero then this function does not match a previous call to vTaskSuspendAll(). */ configASSERT( uxSchedulerSuspended ); /* It is possible that an ISR caused a task to be removed from an event list while the scheduler was suspended. If this was the case then the removed task will have been added to the xPendingReadyList. Once the scheduler has been resumed it is safe to move all the pending ready tasks from this list into their appropriate ready list. */ taskENTER_CRITICAL(); { --uxSchedulerSuspended; if( uxSchedulerSuspended ( UBaseType_t ) pdFALSE ) { if( uxCurrentNumberOfTasks ( UBaseType_t ) 0U ) { /* Move any readied tasks from the pending list into the appropriate ready list. */ 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 the moved task has a priority higher than the current task then a yield must be performed. */ if( pxTCB-uxPriority pxCurrentTCB-uxPriority ) { xYieldPending pdTRUE; } else { mtCOVERAGE_TEST_MARKER(); } } if( pxTCB ! NULL ) { /* A task was unblocked while the scheduler was suspended, which may have prevented the next unblock time from being re-calculated, in which case re-calculate it now. Mainly important for low power tickless implementations, where this can prevent an unnecessary exit from low power state. */ prvResetNextTaskUnblockTime(); } /* If any ticks occurred while the scheduler was suspended then they should be processed now. This ensures the tick count does not slip, and that any delayed tasks are resumed at the correct time. */ { UBaseType_t uxPendedCounts uxPendedTicks; /* Non-volatile copy. */ if( uxPendedCounts ( UBaseType_t ) 0U ) { do { if( xTaskIncrementTick() ! pdFALSE ) { xYieldPending pdTRUE; } else { mtCOVERAGE_TEST_MARKER(); } --uxPendedCounts; } while( uxPendedCounts ( UBaseType_t ) 0U ); 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; } ​首先这里会进行一下任务的切换的变量他会定义一下如果需要为pdTRUE不需要则pdFALSE。进入临界段。把变量uxSchedulerSuspended自减也就是说该变量挂起了一次就减一次当uxSchedulerSuspended等于pdFALSE的时候也就是0也就是说减到0的时候才进行恢复否则的话无论你怎么样都不恢复不进行任何操作退出临界段。也就是“调用了多少次的vTaskSuspendAll()就要调用多少次vTaskResumeAll()进行恢复”的由来。当uxSchedulerSuspended0了也就说明要恢复了当挂起列表不为空就说明我已经有任务被恢复中断挂到了这里那么我就需要把该列表的任务移除掉移除掉添加到哪里呢就添加到就绪列表里。把任务移除掉把事件也移除掉将任务添加到就绪列表中假如该任务的优先级比当前任务的优先级还要高的话就要进行一次任务的切换即将变量xYieldPending 设为pdTRUE。假如任务控制块不是NULL的话我们就更新下下一次要解除阻塞的任务的时间因为在调度器被挂起的时候任务虽然被解除了阻塞但是这可能阻止了下一次重新计算的时间。在这种情况下我们就需要重新重置下下一个任务要解除的阻塞的时间。那么如果在调度器被挂起的这段时间产生的滴答定时器并且在这段时间内已经有任务解除阻塞了由于调度器被挂起导致没法切换任务那么当恢复任务的时候需要进行处理这些任务也就是说当uxPendedCounts 0的时候我就需要将这些任务处理一下比如说我要更新一下时基把一个任务来进行调度把这些任务来处理一下。如果xYieldPending需要任务切换的话那就要进行一次任务的切换。退出临界段。返回变量也是表示要进行一次任务的切换。还是很简单的首先将变量uxSchedulerSuspended减一减到0的时候就说明已经恢复了调度器把准备好的一些解除阻塞的任务从挂起的就绪列表里移动到就绪列表之中然后重置下一个任务的解除阻塞时间并且更新时基保证滴答定时器的计数不会滑动。vTaskDelete()vTaskDelete()用于删除一个任务。当一个任务删除另一个任务的时候形参为要删除的任务在创建的时候返回的任务句柄如果删除自身则形参为NULL。#if ( INCLUDE_vTaskDelete 1 ) void vTaskDelete( TaskHandle_t xTaskToDelete ) { TCB_t *pxTCB; taskENTER_CRITICAL(); { /* If null is passed in here then it is the calling task that is being deleted. */ pxTCB prvGetTCBFromHandle( xTaskToDelete ); /* Remove task from the ready list. */ if( uxListRemove( ( pxTCB-xStateListItem ) ) ( UBaseType_t ) 0 ) { taskRESET_READY_PRIORITY( pxTCB-uxPriority ); } else { mtCOVERAGE_TEST_MARKER(); } /* Is the task waiting on an event also? */ if( listLIST_ITEM_CONTAINER( ( pxTCB-xEventListItem ) ) ! NULL ) { ( void ) uxListRemove( ( pxTCB-xEventListItem ) ); } else { mtCOVERAGE_TEST_MARKER(); } /* Increment the uxTaskNumber also so kernel aware debuggers can detect that the task lists need re-generating. This is done before portPRE_TASK_DELETE_HOOK() as in the Windows port that macro will not return. */ uxTaskNumber; if( pxTCB pxCurrentTCB ) { /* A task is deleting itself. This cannot complete within the task itself, as a context switch to another task is required. Place the task in the termination list. The idle task will check the termination list and free up any memory allocated by the scheduler for the TCB and stack of the deleted task. */ vListInsertEnd( xTasksWaitingTermination, ( pxTCB-xStateListItem ) ); /* Increment the ucTasksDeleted variable so the idle task knows there is a task that has been deleted and that it should therefore check the xTasksWaitingTermination list. */ uxDeletedTasksWaitingCleanUp; /* The pre-delete hook is primarily for the Windows simulator, in which Windows specific clean up operations are performed, after which it is not possible to yield away from this task - hence xYieldPending is used to latch that a context switch is required. */ portPRE_TASK_DELETE_HOOK( pxTCB, xYieldPending ); } else { --uxCurrentNumberOfTasks; prvDeleteTCB( pxTCB ); /* Reset the next expected unblock time in case it referred to the task that has just been deleted. */ prvResetNextTaskUnblockTime(); } traceTASK_DELETE( pxTCB ); } taskEXIT_CRITICAL(); /* Force a reschedule if it is the currently running task that has just been deleted. */ if( xSchedulerRunning ! pdFALSE ) { if( pxTCB pxCurrentTCB ) { configASSERT( uxSchedulerSuspended 0 ); portYIELD_WITHIN_API(); } else { mtCOVERAGE_TEST_MARKER(); } } } #endif /* INCLUDE_vTaskDelete */首先他也是通过任务句柄来获取任务控制块的指针通过任务句柄获取任务控制块将该任务从他对应的列表当中删除掉比如说可以从就绪列表中删除从延时列表删除都是可以的删除之后判断对应的列表当前是否有其它任务有任务就不用管它如果为空就把位图最高优先级的这个变量uxTopReadyPriority的那个位 清零。判断任务是不是再等待事件也就是看看他的等待事件的列表中他有没有等待事件如果有也把它从这个列表当中删除掉。然后记录下系统的任务总个数。如果删除的是当前任务我们就把任务插入到待回收的列表中也就是说是僵尸队列在空闲任务中才会把这个任务清除因为当前任务是有一些信息的比如说 任务栈这些变量还需要继续运行的不可能马上删除掉那么我们就把这个任务插入到待处理的列表之中或者说是待回收列表或者是僵尸列表在运行空闲任务的时候我们把它删除掉就好了。记录下这个变量需要清除的任务个数在空闲任务里会判断该变量在大于0的时候在回收列表里删除这个任务就可以了。如果删除的是其他任务就将当前任务的个数减一就可以了并且直接删除它的任务控制块重置下阻塞的时间为什么要重置假如删除的任务正好是下一次要解除阻塞的任务那么要解除阻塞的时候就找不到这个已经被删除的任务了那就会出错我们重置下时间就可以了。调度器在运行的时候正好删除的是当前任务就运行一次任务的切换。总结就是如果此时删除的任务是任务自身的话那么删除的任务函数不能在任务本身完成删除操作因为需要上下文切换到另一个任务所以需要将任务放到回收列表中(xTasksWaitingTermination)空闲任务会检查结束列表并在空闲任务prvIdleTask()中释放任务的控制块和已删除任务的堆栈内存。删除任务时只会自动释放内核本身分配给任务的内存应用程序而不是内核分配给任务的内存或任何其他资源必须是删除任务时由应用程序显式释放。(比如我们在应用程序分配了一个内存用malloc分配了一个指针指向了一块大内存但是我删除任务的时候没有把分配的这段大内存释放掉那么这块内存就永远不会再被使用了所以在删除任务的时候需要把所有应用程序分配的内存全部给释放掉。因为你删除了任务的主体所以也不会再进入该内存了。)vTaskDelay() 延时函数--很重要vTaskDelay() 是用于阻塞的调用该函数后任务将进入阻塞状态进入阻塞态的任务将让出CPU资源。比如说任务A运行1s阻塞1s再运行1s再阻塞1s运行1s...而在阻塞的时候我可以去运行任务B或者任务C在阻塞之中它是放弃CPU使用权的当前没有任务的话我就去运行一个空闲任务。其实是很简单的但是它有一个弊端假设我在任务执行的时候突然被一个任务D抢占了假如任务D执行了100ms或者是300ms也就是0.3s,那么当任务A执行完了进行一个延时那它的偏移就是到了5300运行正常是1000的倍数来的。那么他被打断进入延时的话是从5300开始的延时结束就到了6300所以它是不能是周期的。而FreeRTOS有一个绝对延时是以周期运行的。延时的话我们之前已经实现了它就是将一个任务从就绪列表中移除并且添加到我们的延时列表中。vTaskDelay()是相对性的延时还有一个绝对延时vTaskDelayUntil()常用于较精确的周期运行任务也就是任务从上一次运行开始到下一次运行开始的时间间隔是绝对的。实际上绝对延时在正常使用中并不多但也可以使用。首先任务A运行1s延时1s运行1s延时1s...假设在(1)处打断0.3s那么当任务A执行完了之后就到了5300这里那么延时的话用绝对延时只会延时0.7s而不会延时1s为什么因为它是绝对的。也就是说无论你执行了多久下次只会在6000这个点来唤醒你。假如在0处开始无论你阻塞了多久下次只会在3000这个时间点唤醒你。总结任务会先调用绝对延时vTaskDelayUntil()使任务进入阻塞态等到时间到了就从阻塞中解除然后执行主体代码。任务主体代码执行完毕会继续调用vTaskDelayUntil()使任务进入阻塞态然后就是循环这样子执行。即使任务在执行中发生中断那么也不会影响这个任务的执行周期仅仅是缩短了阻塞的时间而已。到了要唤醒的时间依旧会将任务唤醒。#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 */这里有几个变量是很重要的这里单独列出来xTimeIncrement:任务周期时间pxPreviousWakeTime:上一次唤醒的时间点xTimeToWake:下一次唤醒的系统时间点xConstTickCount:进入延时的时间点首先将所有任务/调度器都挂起掉因为我不想要别的任务来打搅。获取下开始延时的时间点xTickCount是系统延时的时间点xConstTickCount进入延时的时间点获取一下保存下来xTimeToWake下次唤醒的时间点就是我们传递进来的时间加上任务的周期xTimeIncrement唤醒后它是有一定的时间来执行代码如果上一次唤醒的时间大于当前时间说明节拍计数器已经溢出了。再判断一下上一次唤醒的时间是否大于下一次唤醒的时间点以及下一次唤醒的时间点大于任务开始延时的时间点说明虽然溢出了但是他要唤醒的时候说明时间轴是对的他可以看作是不溢出的就正常跑就好了也就相当于没有溢出因为下一次唤醒的时间还是在这里所以它是相当于没有溢出的不用管他。第二种情况如果上一次的时间大于下一次的时间 或者 唤醒时间大于当前计数的时间见下图也是可以看作没有溢出的因为时间是正轴来跑的。然后更新一下上一次唤醒的时间就是下一次唤醒的时间。如果变量xShouldDelay 是pdFALSE说明需要调整。是pdTRUE时直接将任务添加到延时列表之中。然后恢复任务。总结一下无论是溢出还是没有溢出都要求在下次唤醒任务之前当前任务主体代码必须被执行完。也就是说任务执行的时间必须小于任务周期时间xTimeIncrement。任务会先调用vTaskDelayUntil()使任务进入阻塞态等到时间到了就从阻塞中解除然后执行主体代码任务主体代码执行完毕。会继续调用vTaskDelayUntil()是任务进入阻塞态然后就是循环这样子执行。即使任务在执行过程中发生中断那么也不会影响这个任务的运行周期仅仅是缩短了阻塞的时间而已到了要唤醒的时间依旧会将任务唤醒。实际中要怎么设计任务怎么分配它的优先级什么时候要阻塞对于一个嵌入式的设计人员要对自己设计的嵌入式系统了如指掌。任务的优先级信息任务与中断的处理任务的运行时间状态逻辑等等这些都要知道这样才能设计出好的系统。所以在设计的时候建议根据需求来建立一个框架在设计之初就要考虑以下这些要点任务的上下文环境任务的执行时间还有一些中断的处理比如配合一些IPC的通信机制这些在后续会讲解。其实使用一个操作系统它的核心就是解决一个生产者消费者。那什么是生产者顾名思义就是产生一些东西比如说我有一个事件产生了这就是一个生产者比如说我有一个中断或者某一个事件发生了或者我需要传递一些信息给到别人那么这些就是一个生产者。那什么是消费者呢简单来说就是处理任务那么整个操作系统比如说有一个中断来了那我就要去处理 对应的处理任务来运行它有一个事件来了我要去处理它有一个信息来了我就交给处理任务来处理它核心就是一个生产者和消费者的解决。那怎么解决的呢其实借助我们系统的IPC通信机制。后续会讲解。借助它告诉它并且实时操作系统会占用CPU就是说你有东西来了我要处理肯定要占用CPU的我需要很快的占用CPU那么操作系统就这样子产生了。那什么是上下文呢比如说一个上下文环境一个任务就有一个任务栈那些在任务里面的主体函数他有一些局部的变量那么假如一个任务切出来之后它的局部变量的生存周期随着函数的结束而结束那切出任务的时候局部变量会压入这个任务栈被压入到任务栈中是保存起来的所以上下文环境有一个是中断还有一个任务对于中断的这种中断服务函数也是一种上下文环境它运行在非任务的执行环境下它是特殊的可以理解它是特权模式下的一种运行。它使用的是MSP主栈指针而任务使用的PSP线程指针。在这个上下文环境中不能使用挂起当前任务的操作比如说一个任务当前在运行突然有一个中断来了就会去执行它的中断服务函数这时当前任务还是这个任务那我任务都还没有执行完你肯定是不能把我的当前任务挂起也不允许调用任何会阻塞当前任务运行的API。所以FreeRTOS中有一套以From_ISR为结尾的API函数专门用于在中断函数中使用的。那么我们从裸机里知道在中断里的处理一般要快进快出做一些事件的标记然后通知让对应的任务去执行相关的处理如果中断的处理时间过长就会导致整个系统任务都无法正常运行假如我一个任务才执行1ms中断服务函数就执行了100ms那么这样的一个任务整个系统就乱套了。所以中断要快进快出所以在设计的时候要考虑中断的频率还有中断的处理时间然后去配合中断处理任务。如果中断频率很高的话比如1ms中断一次太高了当然也是可以的任务如果处理的快就在中断里处理了如果处理的慢我就直接让任务来处理而这个任务的处理必须在1ms内响应它不然的话下一个中断又来了这就不行了1ms响应完全可以做到那么这个时候任务的优先级就要设置的高一点然后当中断来的时候我去通知他这个任务能马上得到运行之后呢他就可以马上处理它但是假如你的中断处理很长比如说中断处理需要100ms那么这个任务就不能设置的太高了如果设置的太高假设下面还有一个任务需要10ms来响应的在100ms的时间10ms是没有办法响应的因为你的优先级比我高所以假如你的中断处理很长的话那么这个任务就设置的低一点就好了。就这样子的一个设计一个思想不过这些都要根据实际的应用场景来进行调整的跳到最优为好了。作为一个优先级明确的实时系统如果一个任务中的程序出现了死循环操作(此处的死循环指的是没有阻塞机制的任务循环体)那么比这个任务优先级低的任务都将无法执行。比如说有一个任务优先级是10它是无阻塞的那么9、8、7~0的这些任务都无法得到CPU的使用权。所以在任务设计的时候要保证任务在不活跃的时候任务可以进入阻塞态以交出CPU的使用权那么什么时候进入阻塞态呢这就靠我们自己的设计在什么时候需要阻塞让出CPU就可以了如果你真的想要无阻塞的话建议将它与空闲任务设置为相同的优先级就是说使用时间片来轮转调度空闲任务和这个任务他不需要阻塞或者比空闲任务高一点也是可以的。反正一般都是要建立一个有阻塞的任务。还有一个是空闲任务是FreeRTOS系统中没有其他工作进行时自动进入的系统任务。因为处理器总是需要代码执行的所以至少要有一个任务处于运行态。他可以钩入一些钩子函数什么是钩子函数比如说我空闲任务在跑的时候使能了一个钩子函数就去到这个钩子函数这里处理完了再回来继续跑空闲任务。在钩子函数这里不能将空闲任务阻塞或者挂起所以钩子函数不能调用任何阻塞空闲任务的API函数他也不应该进入一个死循环因为空闲任务需要做一些其他的处理比如说系统删除了一个任务我都是在空闲任务将这些东西释放掉。而且空闲任务还是一个唯一不允许出现阻塞情况的任务。除此以外还要注意任务的执行时间其实任务的执行时间包括两个方面一是任务从开始到结束的时间二是任务的周期。比如说A事件对应Task_AB事件对应Task_BC事件对应Task_C那么假设A事件实时响应的时间是10ms我需要在10ms把它响应了Task_A的运行时间是1ms就是说他处理的时间是1ms那么假设Task_B的运行时间是0.1ms而且运行Task_B的周期是50ms那么这时对事件A的响应只需要10ms我就可以把Task_B的优先级设置的高一些比Task_A高假设Task_A执行的时候要对Task_A事件响应的时候被Task_B抢占了Task_B只执行了0.1ms完全不影响在10ms之内响应A事件。所以Task_B可以设置的高一点Task_A可以设置的低一些。假设Task_C运行的时间是20ms在Task_A运行的时候假设Task_C设置的高一些Task_C会抢占Task_ATask_C要执行20ms那么就错过了Task_A 10ms的响应10ms的期间执行了Task_C就完全错过了所以Task_C不能设置的比Task_A 高要低一些。在我需要响应事件A的时候Task_C你不能抢占我所以简单来说处理时间更短的任务优先级应该设置的更高一些。当然越重要的任务优先级也要设置的更高一些。下面做两个小实验通过一个按键来挂起和恢复一个任务。实验一使用相对延时【见附件程序】实验二我们想要使用以下绝对延时。需要打开一个宏。【#define INCLUDE_vTaskDelayUntil 1】打开。【见附件程序】