任务功能:任务以C函数的形式实现。唯一特别的地方是它们的原型,它必须返回void并接受一个void指针形参。
void ATaskFunction(void *pvParameters);
**每个任务本身都是一个小程序。它有一个入口点,通常在无限循环中永远运行,不会退出。**典型任务的结构如Listing12所示。
FreeRTOS任务不能以任何方式从它们的实现函数中返回——它们不能包含’return’语句,也不能在函数结束后执行。如果不再需要某个任务,应该显示删除它。
单个任务函数定义可用于创建任意数量的任务,每个创建的任务都是一个单独的执行实例,具有自己的堆栈和任务本身中定义的任何自动(堆栈)变量的副本。

void ATaskFunction(void *pvParameters)
{
/*可以像普通函数一样声明变量。
使用这个实例函数创建的每个任务实例都有自己的lVariableExample变量副本。
如果变量被声明为静态,这种情况下,变量只存在一个副本,并且这个副本将由每个创建的任务实例共享。*/
int32_t lVariableExample = 0;
/*一个任务通常被实现为无限循环*/
for(;;)
{
/*实现任务的功能代码*/
}
/*如果任务实现突破了上面的循环,那么必须在达到其实现功能的结束之前删除任务。
传递给vTaskDelete()函数的NULL参数表示要删除的任务是调用(this)任务
*/
vTaskDele(NULL);
}
当任务处于Running状态时,处理器正在执行任务的代码。当任务处于Not Running状态时,该任务处于休眠状态,它的状态已被保存,以便下次调度器决定它应该进入Running状态时恢复执行。当任务恢复执行时,它从上次离开Running状态之前即将执行的指令开始执行。

从“未运行”状态转换到“运行”状态的任务被称为“换入”或“换入”。相反,从运行状态转换到非运行状态的任务被称为“换出”或“换出”。
FreeRTOS调度器是唯一可以切换任务的实体。
xTaskCreate() API函数:任务是使用FreeRTOS的xTaskCreate() API函数创建的。
BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
const char * const pcName,
uint16_t usStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *pxCreatedTask );
参数详细介绍
例1.创建任务
(这个例子演示了创建两个简单任务,然后开始执行这些任务所需的步骤。任务只是定期打印一个字符串,使用一个粗糙的空循环来创建周期延迟。这两个任务以相同的优先级创建,除了输出的字符串外是相同的)
void vTask1(void *pvParameters)
{
const char *pcTaskName = "Task 1 is running\r\n";
volatile uint32_t ul; /*volatile以确保ul没有被优化掉*/
/*与大多数任务一样,该任务是在无限循环中实现的。*/
for(;;)
{
/*打印出此任务的名称。*/
vPrintString(pcTaskName);
/*延迟一段时间*/
for(ul = 0; ul < mainDELAY_LOOP_COUNT; ul++)
{
/**/
}
}
}
void vTask2(void *pvParameters)
{
const char *pcTaskName = "Task 2 is running\r\n";
volatile uint32_t ul; /*volatile以确保ul没有被优化掉*/
/*与大多数任务一样,该任务是在无限循环中实现的。*/
for(;;)
{
/*打印出此任务的名称。*/
vPrintString(pcTaskName);
/*延迟一段时间*/
for(ul = 0; ul < mainDELAY_LOOP_COUNT; ul++)
{
/**/
}
}
}
main()函数在启动调度器之前创建了任务
int main(void)
{
/*创建两个任务中的一个。真正的应用程序应该检查xTaskCreate()调用的返回值,来确保任务被成功创建。*/
xTaskCreate( vTask1, /*指向实现任务的函数指针。*/
"Task 1",/*任务的文本名称 */
1000, /* 堆栈深度,小型微控制器使用的堆栈比这还少 */
NULL, /*未使用task参数。 */
1, /*任务以优先级1运行。*/
NULL ); /*不使用任务句柄 */
/*以完全相同的方式和相同的优先级创建另一个任务。* /
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
/*启动调度程序开始执行*/
vTaskStartScheduler();
/*如果一切正常,main()将永远不会到达这里,因为调度器现在正在运行任务。如果main()到达这里,则很可能没有足够的堆内存用于创建空闲任务。*/
for(;;);
}

两个任务以相同的优先级运行,因此在相同的处理器核心上共享时间。它们的实际执行模式如图11所示。

同一时间只能有一个任务处于Running状态。因此,当一个任务进入Running状态(该任务被切换进来)时,另一个任务进入Not Running状态(该任务被切换出去)。
示例1在启动调度器之前,从main()中创建了这两个任务。也可以从另一个任务中创建一个任务。例如,可以从Task1中创建Task2.
void vTask1(void *pvParameters)
{
const char *pcTaskName = "Task 1 is running\r\n";
volatile uint32_t ul; /*volatile以确保ul没有被优化掉*/
/*在进入无限循环之前创建另一个任务。*/
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
/*与大多数任务一样,该任务是在无限循环中实现的。*/
for(;;)
{
/*打印出此任务的名称。*/
vPrintString(pcTaskName);
/*延迟一段时间*/
for(ul = 0; ul < mainDELAY_LOOP_COUNT; ul++)
{
/**/
}
}
}
例2.使用task参数
在示例1中创建的两个任务几乎相同,唯一的区别是它们打印出的文本字符串。
我们可以通过创建单个任务实现的两个实例来消除这种重复。然后可以使用task参数向每个任务传递它应该打印出来的字符串。
void vTaskFunction(void *pvParameter)
{
char *pcTaskName;
volatile uint32_t ul;
pcTaskName = (char *)pvParameter;
for(;;)
{
vPrintString(pcTaskName);
for(ul = 0; ul < mainDELAY_LOOP_COUNT; ul++)
{
}
}
}
即使现在只有一个任务实现(vTaskFunction),也可以创建已定义任务的多个实例。每个创建的实例将在FreeRTOS调度程序的控制下独立运行。
/*定义将作为任务参数传入的字符串。
它们被定义为const,而不是在堆栈上,以确保它们在任务执行时仍然有效*/
static const char *pcTextForTask1 = "Task 1 is running\r\n";
static const char *pcTextForTask2 = "Task 2 is running\r\n";
int main(void)
{
xTaskCreate( vTaskFunction, /* 指向实现任务的函数指针 */
"Task 1", /* 任务的文本名称。只是为了方便调试。 */
1000, /*堆栈深度,小型微控制器使用的堆栈比这要少得多。 */
(void*)pcTextForTask1, /*使用task参数将要打印的文本传递到任务中。*/
1, /*任务优先级为1 */
NULL ); /*没有使用文件句柄。 */
}
/*以完全相同的方式创建另一个任务。注意,这一次将从SAME任务实现(vTaskFunction)创建多个任务。只有参数中传递的值不同。正在创建同一个任务的两个实例。* /
xTaskCreate(vTaskFunction, "Task 2", 1000, (void *)pcTextForTask2, 1, NULL );
/*启动调度程序*/
vTaskStartScheduler();
/*如果一切正常,main()将永远不会到达这里,因为调度器现在正在运行任务。如果main()到达这里,则很可能没有足够的堆内存用于创建空闲任务。第2章提供了关于堆内存管理的更多信息。* /
for(;;);
xTaskCreate() API函数的uxPriority参数为正在创建的任务分配初始优先级。通过使用vTaskPrioritySet() API函数,可以在启动调度器之后更改优先级。
可用的最大优先级数由FreeRTOSConfig.h中应用程序定义的configMAX_PRIORITIES编译时配置常量设置。
低数值优先级值表示低优先级任务,低优先级0是可能的最低优先级。
优先级范围(0 ~ configMAX_PRIORITIES-1)。任务数量的任务都可以共享相同的优先级,确保最大的设计灵活性。
FreeRTOS调度器使用两种方法决定哪个任务处于Running状态。configMAX_PRIORITIES可以设置的最大值取决于所使用的方法:
FreeRTOS调度器将始终确保能够运行的优先级最高的任务是被选中进入运行状态的任务。当多个具有相同优先级的任务能够运行时,调度器将依次将每个任务转换为Running状态和退出Running状态。
为了能够选择要运行的下一个任务,调度器本身必须在每个时间切片1结束时执行。周期性中断,称为“tick interrupt”。
时间片的长度由tick中断频率设置,该频率由FreeRTOSConfig.h中应用程序定义的configTICK_RATE_HZ编译时间配置常量。
如果configTICK_RATE_HZ被设置为100HZ,那么时间片就是10毫秒。
configTICK_RATE_HZ的最佳值取决于正在开发的应用程序。
两次tick中断之间的时间被称为tick 周期,一个事件切片就等同于一个tick周期。

重要的是要注意,时间片的结束并不是调度器可以选择要运行的新任务的唯一位置;正如本书将演示的那样,当当前执行的任务进入Blocked状态,或者当中断将一个高优先级的任务移到Ready状态时,调度程序也将选择一个新任务来立即运行。
FreeRTOS API调用总是以多个Tick 周期指定时间,这通常称为Ticks。
pdMS_TO_TICKS()宏将以毫秒为单位指定的时间转换为以tick为单位指定的时间。
可用的分辨率取决于定义的tick频率,如果tick频率高于1KHZ(configTICK_RATE_HZ大于1000),pdMS_TO_TICKS()不能使用。
/*pdMS_TO_TICKS()将以毫秒为单位的时间作为唯一参数,并计算为以tick为周期的等效时间。这个示例显示将xTimeInTicks设为相当于200ms的tick周期的数量*/
TickType_t xTimeInTicks = pdMS_TO_TICKS(200);
不建议在程序中直接指定以tick为单位的时间,而是使用pdMS_TO_TICKS()宏指定以毫秒为单位的世界,这样做可以确保在tick频率发生更改时,在应用程序中指定的时间不会更改。
'tick count’值是从调度程序启动以来发生tick的总数,假设tick计数没有溢出。
实例3.带有优先级的试验
调度器将始终确保能够运行的优先级的最高的任务是被选中进入Running状态的任务。到目前为止,在我们的示例中,已经以相同的优先级创建了两个任务,因此它们依次进入和退出Running状态。这个示例将查看在示例2中创建的两个任务之一的优先级发生更改时发生的情况。这一次,第一个任务将在优先级1上创建,第二个任务将在优先级2上创建。


调度器始终选择能够运行的优先级最高的任务。Task2优先级高于Task1,所以Task 2是唯一进入Running状态的任务。因为Task 1从来没有进入Running状态,所以它从来没有打印出它的字符串。Task 1被Task 2“耗尽”了处理时间。

到目前为止,创建的任务总是有处理要执行,而且从不需要等待任何事情——因为它们从不需要等待任何事情,所以它们总是能够进入Running状态。这种“持续处理”任务的用处有限,因为它们只能在最低优先级的情况下创建。如果它们以任何其他优先级运行,它们将完全阻止低优先级的任务运行。
为了使任务有用,必须将它们重写为事件驱动的。事件驱动的任务只有在触发它的事件发生之后才有工作(处理)要执行,并且在该事件发生之前无法进入Running状态。调度器总是选择能够运行的优先级最高的任务。高优先级任务不能运行意味着调度器不能选择它们,而必须选择能够运行的低优先级任务。因此,使用事件驱动的任务意味着可以在不同的优先级上创建任务,而不会让最高优先级的任务耗尽所有低优先级任务的处理时间。
阻塞状态
等待事件的任务被称为处于“Blocked”状态,这也是“Not Running”状态的子状态。
任务可以进入Blocked状态来等待两种不同类型的事件:
FreeRTOS队列、二进制信号量、计数信号量、互斥锁、递归互斥锁、事件组和直接接到的通知都可以用来创建同步事件。
任务可以通过超时阻塞同步事件,有效地同时阻塞两种类型的事件。
例如,一个任务可以选择等待数据到达队列的最长时间为10毫秒,如果有数据在10毫秒内到达,或者10毫秒过后没有数据到达,任务将离开Blocked状态。
挂起状态
处于挂起状态的任务对调度器不可用。进入Suspended状态的唯一方法是调用vTaskSuspend()API函数,唯一离开的方法是调用vTaskResume()或xTaskResumeFromISR() API函数。大多数应用程序不使用Suspended状态。
就绪状态
处于“未运行”状态但没有阻塞或挂起的任务称为“就绪”状态。它们能够运行,因此“准备好”运行,但当前没有处于Running状态。
完成状态转换图

例4.使用Blocked状态来创建延迟
上述例子高优先级任务在执行空循环时保持Running状态,“限制”低优先级任务的任何处理时间。
任何形式的轮询都有几个缺点,尤其是效率低下。
在轮询期间,任务实际上没有任何工作要做,但它仍然使用最大处理时间,因此浪费处理器周期。
使用vTaskDelay() API函数替换轮询空循环,从而纠正这种行为。
只有在FreeRTOSConfig.h中将INCLUDE_vTaskDelay设置为1时,vTaskDelay() API函数才可用。
vTaskDelay()将调用任务置于Blocked状态,进行固定数量的tick中断,该任务在处于Blocked状态时,不使用任何处理时间,因此该任务仅在有实际工作需要完成时使用处理时间。
void vTaskDelay(TickType_t xTicksToDelay);
宏pdMS_TO_TICKS()可用于将毫秒转换为指定的tick数,例如调用vTaskDelay(pdMS_TO_TICKS(100)),导致调用任务保持阻塞状态100毫秒。
void vTaskFunction(void *pvParamaters)
{
char *pcTaskName;
const TickType_t xDelay250ms = pdMS_TO_TICKS(250);
/*要打印出来的字符串是通过参数传入的。将其转换为字符指针。*/
pcTaskName = (char *)pvParamaters;
/* 和大多数任务一样,这个任务是在一个无限循环中实现的。*/
for(;;)
{
vPrintString(pcTaskName);
/*
延迟一段时间,这一次使用对vTaskDelay()的调用,它将任务置于Blocked状态,直到延迟期过期。参数的时间用'ticks'指定
*/
vTaskDelay(xDelay250ms);
}
}
空闲任务是在启动调度器时自动创建的,以确保始终至少有一个任务能够运行(至少有一个任务处于Ready状态)。

任务在整个延迟期间进入Blocked状态,只有真正需要执行工作时才使用处理器。
大多数情况下,没有能够运行的应用程序任务(没有可以选择进入running状态的应用程序任务。)**在这种情况下,空闲任务将运行。分配给空闲任务的处理时间量是系统中空闲处理能力的度量。**通过允许应用程序完全由事件驱动,使用RTOS可以显著增加空闲处理能力。
vTaskDelayUntil()API函数
vTaskDelayUntil()类似于vTaskDelay()。
void vTaskDelayUntil(TickType_t * pxPreviousWakeTime, TickType_t xTimeIncrement);
例5。将示例任务转换为使用vTaskDelayUntil()
例4中创建的两个任务是周期性任务,但是**使用vTaskDelay()不能保证它们运行的频率是固定的,**因为任务离开Blocked状态的时间与它们调用vTaskDelay()时间是相对的。
将任务转换为使用vTaskDelayUntil()而不是vTaskDelay()解决了这个潜在的问题。
void vTaskFunction(void *pvParameters)
{
char *pcTaskName;
TickType_t xLastWakeTime;
pcTaskName = (char *)pvParameters;
/*xLastWakeTime变量需要用当前tick计数初始化。注意,这是唯一一次显式写入变量。之后xLastWakeTime在vTaskDelayUntil()中自动更新。*/
xLastWakeTime = xTaskGetTickCount();
for(;;)
{
vPrintString( pcTaskName );
/*该任务每250毫秒执行一次。*/ vTaskDelayUntil(&xLastWakeTime,pdMS_TO_TICKS(250));
}
}
例6.组合阻塞和非阻塞任务



规定总是至少有一个任务可以进入running状态。为了确保这种情况,调度程序在调用vTaskStartScheduler()时自动创建Idle任务。空闲任务除了在一个循环中运行之外,没有做其他的。
空闲任务具有尽可能低的优先级(优先级为0),确保它不会阻止更高优先级的应用程序任务进入运行状态。
FreeRTOSConfig.h中的configIDLE_SHOULD_YIELD编译时配置常量可用于防止Idle任务占用处理时间。
以最低优先级运行可以确保当高优先级任务进入就绪状态时,Idle任务将从Running状态转换出来。
如果应用程序使用vTaskDelete()API函数,那么Idle任务不缺乏处理时间是很重要的。因为Idle任务负责在删除任务后清理内核资源。
空闲任务钩子函数
可以通过使用空闲钩子函数(空闲回调),将特定于应用程序的功能直接添加到空闲任务中——空闲任务在每次迭代空闲任务循环时自动调用该函数。
Idle任务钩子的常用用法:
实现空闲任务钩子函数的限制
空闲任务钩子函数必须遵守以下规则。
void vApplicationIdleHook(void);
定义一个空闲任务钩子函数
/*声明一个钩子函数递增的变量。*/
volatile uint32_t ulIdleCycleCount = 0UL;
/*空闲钩子函数必须调用vApplicationIdleHook(),不接受参数,并返回void*/
void vApplicationIdleHook(void)
{
ulIdleCycleCount++;
}
configUSE_IDLE_HOOK必须在FreeRTOSConfig.h中设置为1,才能调用空闲钩子函数。


示例7产生的输出如图21所示。它显示了在应用程序任务的每次迭代之间大约调用了400万次空闲任务钩子函数(迭代的次数取决于执行演示的硬件的速度)。
修改任务的优先级
vTaskPrioritySet() API函数可用于在启动调度器后更改任何任务的优先级。
只有在FreeRTOSConfig.h中将INCLUDE_vTaskPrioritySet设置为1时,vTaskPrioritySet() API函数才可用。
void vTaskPrioritySet(TaskHandle_t pxTask,UBaseType_t uxNewPriority);
**uxTaskPriorityGet()**API函数可用于查询任务的优先级。
只有在FreeRTOSConfig.h中将INCLUDE_uxTaskPriorityGet设置为1时,uxTaskPriorityGet()才可以使用。
UBaseType_t uxTaskPriorityGet(TaskHandle_t pxTask);
例8.改变任务优先级
调度器将始终选择最高的Ready状态任务作为进入Running状态的任务。示例8通过使用vTaskPrioritySet() API函数来改变两个任务的优先级。
例8创建两个具有两个不同优先级的任务。这两个任务都没有进行任何可能导致它进入Blocked状态的API函数调用,因此它们总是处于Ready状态或Running状态。因此,相对优先级最高的任务将始终是调度器选择的处于Running状态的任务。
void vTask1(void *pvParamaters)
{
UBaseType_t uxPriority;
/*此任务总是在task 2之前运行,因为它是用更高的优先级创建的。Task 1和Task 2都不会阻塞,所以它们总是处于Running或Ready状态。
查询任务运行时的优先级—传入NULL表示“返回调用任务的优先级”。* /
}
uxPriority = uxTaskPriorityGet(NULL);
for(;;)
{
vPrintString( "Task 1 is running\r\n" );
}
/*将Task 2的优先级设置在Task 1的优先级之上,将导致Task 2立即开始运行(这样Task 2的优先级将高于创建的两个任务)。注意在对vTaskPrioritySet()的调用中使用了task 2的句柄(xTask2Handle)。清单35显示了如何获取句柄。* /
vPrintString( "About to raise the Task 2 priority\r\n" );
vTaskPrioritySet(xTask2Handle,(uxPriority + 1));
void vTask2( void *pvParameters )
{
UBaseType_t uxPriority;
/* Task 1总是在此任务之前运行,因为Task 1的优先级更高。Task 1和Task 2都不会阻塞,所以它们总是处于Running或Ready状态。
查询任务运行时的优先级—传入NULL表示“返回调用任务的优先级”。* /
}
uxPriority = uxTaskPriorityGet( NULL );
for(;;)
{
vPrintString( "Task 2 is running\r\n" );
/*将该任务的优先级降低到初始值。传递NULL作为任务句柄意味着“更改调用任务的优先级”。将优先级设置为低于Task 1的优先级将导致Task 1立即重新开始运行—抢占此任务。* /
Task 1 to immediately start running again – pre-empting this task. */
vPrintString( "About to lower the Task 2 priority\r\n" );
vTaskPrioritySet( NULL, ( uxPriority - 2 ) );
}
每个任务都可以查询和设置自己的优先级,而不需要使用有效的任务句柄,只需使用NULL即可。只有当任务希望引用自身以外的任务时,例如当任务1更改任务2的优先级时,才需要任务句柄。为了允许Task 1这样做,在创建Task 2时获取并保存Task 2句柄,如清单35中的注释所突出显示的那样。
TaskHandle_t xTask2Handle = NULL;
int main(void)
{
xTaskCreate(vTask1, "Task 1", 1000, NULL, 2, NULL);
xTaskCreate(vTask1, "Task 1", 1000, NULL, 2, &xTask2Handle);
vTaskStartScheduler();
for(;;);
}

删除任务
**vTaskDelete()**函数可以删除自己或任何其它任务。只有在FreeRTOSConfig.h中将INCLUDE_vTaskDelete设置为1时,vTaskDelete() API函数才可用。
删除的任务不再存在,无法再次进入运行状态。
空闲任务负责释放分配给已删除任务的内存。
当删除任务时,只有内核本身分配给任务的内存才会自动释放。必须显式释放任务实现所分配的任何内存或其他资源。
void vTaskDelete(TaskHandle_t pxTaskToDelete);
例9.删除任务
int main(void)
{
xTaskCreate(vTask1,"Task 1", 1000, NULL, 1, NULL);
vTaskStartScheduler();
for(;;);
}
TaskHandle_t xTask2Handle = NULL;
void vTask1(void *pvParamater)
{
const TickType_t xDelay100ms = pdMS_TO_TICKS(100UL);
for(;;)
{
vPrintString( "Task 1 is running\r\n" );
xTaskCreate(vTask2,"Task 2",1000,NULL,2,&xTask2Handle);
vTaskDelay(xDelay100ms);
}
}
void vTask2( void *pvParameters )
{
vPrintString( "Task 2 is running and about to delete itself\r\n" );
vTaskDelete( xTask2Handle );
}

任务状态和事件的概述
在单核处理器上,在任何给定时间内只能有一个任务处于Running状态。
任务可以在阻塞状态等待事件发生,并在事件发生时自动移回就绪状态。临时事件发生在特定的时间,例如,当块时间到期时,通常用于实现周期性或超时行为。同步事件发生在任务或中断服务例程使用任务通知、队列、事件组或总多类型的信号量之一发送信息时。
配置调度算法
调度算法是决定哪个Ready状态的任务转换到Running状态的软件例程。
可以使用configUSE_PREEMPTION和configUSE_TIME_SLICING配置常量来更改算法。这两个常量都在FreeRTOSConfig.h中定义。
第三个配置常量configUSE_TICKLESS_IDLE也会影响调度算法,因为使用它会导致tick中断在很长一段时间内完全关闭。configUSE_TICKLESS_IDLE是一个高级选项,专门用于必须最小化能耗的应用程序。
在所有可能的配置中,FreeRTOS调度器将确保共享优先级的任务依次被选中进入运行状态。这种“轮流进行”的策略通常被称为“轮询调度”。轮询调度算法不保证相同优先级的任务之间的时间平等共享,只保证相同优先级的“就绪”状态的任务依次进入“运行”状态。
基于时间切片的优先抢占调度
表14所示的配置将FreeRTOS调度器设置为使用一种名为“固定优先级与时间切片抢占式调度”的调度算法这是大多数小型RTOS应用程序使用的调度算法。
FreeRTOSConfig.h设置,配置内核使用带时间切片的优先抢占调度。


如果configIDLE_SHOULD_YIELD被设置为0,那么Idle任务将在整个时间片内保持Running状态,除非它被更高优先级的任务抢占。
如果configIDLE_SHOULD_YIELD被设置为1,那么Idle任务将在其循环的每次迭代中让步(自愿放弃其分配的时间片的剩余部分),如果有其他Idle优先级任务处于Ready状态。

当configIDLE_SHOULD_YIELD设置为1时,Idle任务之后选择进入Running状态的任务不会执行整个时间片,而是执行Idle任务产生期间剩余的时间片。
优先抢占式调度(无时间切片)
FreeRTOSConfig.h设置将FreeRTOS调度器配置为使用优先级抢占调度而不进行时间切片。

如果使用时间切片,并且有多个具有最高优先级的就绪状态任务可以运行,那么叼赌气将在每个RTOS tick中断期间选择一个新任务进入Running状态。
如果没有使用时间切片,那么调度程序将只选择一个新任务进入运行状态,当:
不使用时间切片的任务上下文切换比使用时间切片时的任务上下文切换少。因此,关闭时间切片可以减少调度器的处理开销。然而,关闭时间切片也会导致具有相同优先级的任务接收到的处理时间相差很大。

假设configIDLE_SHOULD_YIELD设置为0.
合作调度
这本书的重点是抢占式调度,但FreeRTOS也可以是合作调度。


通常情况下,使用合作调度器比使用抢占式调度器更容易避免同时访问引起的问题:
使用抢占式调度器相比,使用协同调度器时系统的响应会更慢: