目录
1. 任务状态理论
2. 任务状态实验
3. vTaskDelay和vTaskDelayUntil
4. 空闲任务及其钩子函数
5. 任务调度算法
5.1 状态与事件
5.2 调度策略
(thread)。
FreeRTOSConfig.h,每个任务之间是以1ms的tick中断来切换的。
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )
任务切换的4种状态。
任务状态转换图,每个任务都是以链表的形式加入到里面,如:
xTaskCreate() -> prvAddNewTaskToReadyList( pxNewTCB );
vTaskSuspend -> vListInsertEnd;
对应程序:08_freertos_example_task_status
任务切换的基础:tick中断
有哪些任务状态?状态切换图
怎么管理不同状态的任务:放在不同链表里
阻塞状态(Blocked)举例:vTaskDelay函数
暂停状态(Suspended)举例:vTaskSuspend/vTaskResume
任务1调用vTaskSuspend(),让任务3进入暂停状态,过了一会,在调用vTaskResume(),让任务3进入到就绪状态;
任务2调用vTaskDelay(),直接进入到阻塞状态;
main.c
TaskHandle_t xHandleTask1;
TaskHandle_t xHandleTask3;
static int task1flagrun = 0;
static int task2flagrun = 0;
static int task3flagrun = 0;
void Task1Function(void * param)
{
TickType_t tStart = xTaskGetTickCount();
TickType_t t;
int flag = 0;
while (1)
{
t = xTaskGetTickCount();
task1flagrun = 1;
task2flagrun = 0;
task3flagrun = 0;
printf("1");
if (!flag && (t > tStart + 10))
{
vTaskSuspend(xHandleTask3);
flag = 1;
}
if (t > tStart + 20)
{
vTaskResume(xHandleTask3);
}
}
}
void Task2Function(void * param)
{
while (1)
{
task1flagrun = 0;
task2flagrun = 1;
task3flagrun = 0;
printf("2");
vTaskDelay(10);
}
}
void Task3Function(void * param)
{
while (1)
{
task1flagrun = 0;
task2flagrun = 0;
task3flagrun = 1;
printf("3");
}
}
/*-----------------------------------------------------------*/
StackType_t xTask3Stack[100];
StaticTask_t xTask3TCB;
StackType_t xIdleTaskStack[100];
StaticTask_t xIdleTaskTCB;
/*
* The buffers used here have been successfully allocated before (global variables)
*/
void vApplicationGetIdleTaskMemory( StaticTask_t ** ppxIdleTaskTCBBuffer,
StackType_t ** ppxIdleTaskStackBuffer,
uint32_t * pulIdleTaskStackSize )
{
*ppxIdleTaskTCBBuffer = &xIdleTaskTCB;
*ppxIdleTaskStackBuffer = xIdleTaskStack;
*pulIdleTaskStackSize = 100;
}
int main( void )
{
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
printf("Hello, world!\r\n");
xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);
xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);
xHandleTask3 = xTaskCreateStatic(Task3Function, "Task3", 100, NULL, 1, xTask3Stack, &xTask3TCB);
/* Start the scheduler. */
vTaskStartScheduler();
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
运行结果和代码逻辑一致:
任务1调用vTaskSuspend(),让任务3进入暂停状态,过了一会,在调用vTaskResume(),让任务3进入到就绪状态;
任务2调用vTaskDelay(),直接进入到阻塞状态;
URAT1打印出的信息,基本和上图一致,但串口信息直观性不好,没有上图清晰。
对应程序:09_freertos_example_delay
有两个Delay函数:
vTaskDelay:至少等待指定个数的Tick Interrupt才能变为就绪状态
vTaskDelayUntil
老版本,没有返回值
等待到指定的绝对时刻,才能变为就绪态。
下面这个do_something()函数,执行时间不定,但是vTaskDelay(N)延迟函数的tick是固定的。
如下图,每个N Tick是固定的时间。
使用vTaskDelayUntil(t2, ∆t)函数, 传入参数(t2, ∆t),假如在t2 - t3的时刻,不管t2从A B C哪个位置开始运行,必须保证t1 - t2和t2 - t3的∆t是相等的。
t2为 指针结构体 之前唤起的时间,∆t为增量。
①运行间隔时间:*pxPre + ∆t
②更新每次传入的起始时间:*pxPre = *pxPre + ∆t
代码:
让任务1的优先级为2,任务2 3的优先级为1,先让任务1运行,任务1使用xTaskGetTickCount()函数获得当前开始的Tick Count,
TickType_t tStart = xTaskGetTickCount();
给vTaskDelayUntil(&tStart, 20);函数使用。
任务2、任务3 不做复杂的逻辑。
TaskHandle_t xHandleTask1;
TaskHandle_t xHandleTask3;
static int task1flagrun = 0;
static int task2flagrun = 0;
static int task3flagrun = 0;
static int rands[] = {3, 56, 23, 5, 99};
void Task1Function(void * param)
{
TickType_t tStart = xTaskGetTickCount();
int i = 0;
int j = 0;
while (1)
{
task1flagrun = 1;
task2flagrun = 0;
task3flagrun = 0;
for (i = 0; i < rands[j]; i++)
printf("1");
j++;
if (j == 5)
j = 0;
#if 0
vTaskDelay(20);
#else
vTaskDelayUntil(&tStart, 20);
#endif
}
}
void Task2Function(void * param)
{
while (1)
{
task1flagrun = 0;
task2flagrun = 1;
task3flagrun = 0;
printf("2");
}
}
void Task3Function(void * param)
{
while (1)
{
task1flagrun = 0;
task2flagrun = 0;
task3flagrun = 1;
printf("3");
}
}
/*-----------------------------------------------------------*/
StackType_t xTask3Stack[100];
StaticTask_t xTask3TCB;
StackType_t xIdleTaskStack[100];
StaticTask_t xIdleTaskTCB;
/*
* The buffers used here have been successfully allocated before (global variables)
*/
void vApplicationGetIdleTaskMemory( StaticTask_t ** ppxIdleTaskTCBBuffer,
StackType_t ** ppxIdleTaskStackBuffer,
uint32_t * pulIdleTaskStackSize )
{
*ppxIdleTaskTCBBuffer = &xIdleTaskTCB;
*ppxIdleTaskStackBuffer = xIdleTaskStack;
*pulIdleTaskStackSize = 100;
}
int main( void )
{
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
printf("Hello, world!\r\n");
xTaskCreate(Task1Function, "Task1", 100, NULL, 2, &xHandleTask1);
xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);
xHandleTask3 = xTaskCreateStatic(Task3Function, "Task3", 100, NULL, 1, xTask3Stack, &xTask3TCB);
/* Start the scheduler. */
vTaskStartScheduler();
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
运行结果:
图1:是 vTaskDelay(20)函数运行的结果,任务1的空闲时间都是相等的,为20ms。
图2:是vTaskDelayUntil(&tStart, 20)函数的运行结果,任务1的每个∆t都是相等的,为20ms。
对应程序:10_freertos_example_idletask
,在05_freertos_example_createtask
基础上修改
任务后的清理工作在哪执行?分两类:
自杀的任务:在空闲任务中完成清理工作,比如释放内存(都自杀了,怎么清理自己的尸体? 由别人来做)
非自杀的任务:在vTaskDelete内部完成清理工作(凶手执行清理工作)
空闲任务何时才能执行?
空闲任务只能处于这2个状态之一:Running、Ready
空闲任务钩子函数
执行一些低优先级的、后台的、需要连续执行的函数
测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都停止了,所以测量空闲任 务占据的时间,就可以算出处理器占用率。
让系统进入省电模式:空闲任务能被执行就意味着没有重要的事情要做,当然可以进入省电模式 了。
绝对不能导致任务进入Blocked、Suspended状态
如果你会使用 vTaskDelete() 来删除任务,那么钩子函数要非常高效地执行。如果空闲任务移植 卡在钩子函数里的话,它就无法释放内存。
任务1的优先级是1,运行一段时间后,创建任务2,然后任务2马上运行,之后任务2进入2ms的休眠中,任务1删除任务2,在之后的循环中,任务1不断的创建2,然后在不断的删除任务2
void Task2Function(void * param);
/*-----------------------------------------------------------*/
static int task1flagrun = 0;
static int task2flagrun = 0;
static int taskidleflagrun = 0;
void Task1Function(void * param)
{
TaskHandle_t xHandleTask2;
BaseType_t xReturn;
while (1)
{
task1flagrun = 1;
task2flagrun = 0;
taskidleflagrun = 0;
printf("1");
xReturn = xTaskCreate(Task2Function, "Task2", 1024, NULL, 2, &xHandleTask2);
if (xReturn != pdPASS)
printf("xTaskCreate err\r\n");
vTaskDelete(xHandleTask2);
}
}
void Task2Function(void * param)
{
while (1)
{
task1flagrun = 0;
task2flagrun = 1;
taskidleflagrun = 0;
printf("2");
vTaskDelay(2);
}
}
void vApplicationIdleHook( void )
{
task1flagrun = 0;
task2flagrun = 0;
taskidleflagrun = 1;
printf("0");
}
/*-----------------------------------------------------------*/
int main( void )
{
TaskHandle_t xHandleTask1;
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
printf("Hello, world!\r\n");
xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);
/* Start the scheduler. */
vTaskStartScheduler();
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
空闲任务的优先级是 0,任务1的优先级是 1,任务2的优先级是 2
在任务1中不断的去创建,不断的去消耗内存,但是删除任务2的清理工作没办法进行,于是任务1的堆会逐渐消耗光,最终导致创建任务失败。
xReturn = xTaskCreate(Task2Function, "Task2", 1024, NULL, 2, &xHandleTask2);
if (xReturn != pdPASS)
printf("xTaskCreate err\r\n");
我们的内存也只有几十k,在执行上面代码的时候,并没有打印 printf("xTaskCreate err\r\n");这句话,我们期望的事情并没有发生,我们之前说在空闲任务中执行清理工作,这个结论我们要修正一下,应该是自杀的任务:在空闲任务中完成清理工作,比如释放内存。
如果把代码修改成这样,运行几次后,代码立马就运行出错了,出现错误的唯一原因就是内存不够了,堆不够了,原因是自杀任务,使用vTaskDelete(NULL)函数后,不能自己释放内存,需要由别人来做。
void Task1Function(void * param)
{
TaskHandle_t xHandleTask2;
BaseType_t xReturn;
while (1)
{
task1flagrun = 1;
task2flagrun = 0;
taskidleflagrun = 0;
printf("1");
xReturn = xTaskCreate(Task2Function, "Task2", 1024, NULL, 2, &xHandleTask2);
if (xReturn != pdPASS)
printf("xTaskCreate err\r\n");
//vTaskDelete(xHandleTask2);
}
}
void Task2Function(void * param)
{
while (1)
{
task1flagrun = 0;
task2flagrun = 1;
taskidleflagrun = 0;
printf("2");
//vTaskDelay(2);
vTaskDelete(NULL);
}
}
结论:
任务后的清理工作在哪执行?分两类:
自杀的任务:在空闲任务中完成清理工作,比如释放内存(都自杀了,怎么清理自己的尸体? 由别人来做)
非自杀的任务:在vTaskDelete内部完成清理工作(凶手执行清理工作)
所以自杀任务就要使用空闲任务来完成清理工作了,具体做法如下:
在main函数的任务调度函数,vTaskStartScheduler()中,第一个参数prvIdleTask,
xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,
configIDLE_TASK_NAME,
ulIdleTaskStackSize,
( void * ) NULL,
portPRIVILEGE_BIT,
pxIdleTaskStackBuffer,
pxIdleTaskTCBBuffer );
在tasks.c中搜索到static portTASK_FUNCTION( prvIdleTask, pvParameters )函数,一般我们不在tasks.c的核心文件中修改代码,会破坏核心代码,但是该函数末提供了钩子函数,
#if ( configUSE_IDLE_HOOK == 1 )
{
extern void vApplicationIdleHook( void );
/* Call the user defined function from within the idle task. This
* allows the application designer to add background functionality
* without the overhead of a separate task.
* NOTE: vApplicationIdleHook() MUST NOT, UNDER ANY CIRCUMSTANCES,
* CALL A FUNCTION THAT MIGHT BLOCK. */
vApplicationIdleHook();
}
我们要在 FreeRTOSConfig.h 中 添加宏定义configUSE_IDLE_HOOK == 1 ,之后再main.c中实现 void vApplicationIdleHook( void )函数,如下:
void vApplicationIdleHook( void )
{
task1flagrun = 0;
task2flagrun = 0;
taskidleflagrun = 1;
printf("0");
}
整体代码如下:
因为空闲任务(vApplicationIdleHook())的优先级为0,所以我们要修改任务1的优先级也为0,这要在任务2自杀后,空闲任务才可以执行起来,去调用到我们提供的钩子函数,完成任务2的清理工作了。
注:钩子函数不要来死循环,要不然空闲任务就做不了其他事情了。
void Task2Function(void * param);
/*-----------------------------------------------------------*/
static int task1flagrun = 0;
static int task2flagrun = 0;
static int taskidleflagrun = 0;
void Task1Function(void * param)
{
TaskHandle_t xHandleTask2;
BaseType_t xReturn;
while (1)
{
task1flagrun = 1;
task2flagrun = 0;
taskidleflagrun = 0;
printf("1");
xReturn = xTaskCreate(Task2Function, "Task2", 1024, NULL, 2, &xHandleTask2);
if (xReturn != pdPASS)
printf("xTaskCreate err\r\n");
//vTaskDelete(xHandleTask2);
}
}
void Task2Function(void * param)
{
while (1)
{
task1flagrun = 0;
task2flagrun = 1;
taskidleflagrun = 0;
printf("2");
//vTaskDelay(2);
vTaskDelete(NULL);
}
}
void vApplicationIdleHook( void )
{
task1flagrun = 0;
task2flagrun = 0;
taskidleflagrun = 1;
printf("0");
}
/*-----------------------------------------------------------*/
int main( void )
{
TaskHandle_t xHandleTask1;
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
printf("Hello, world!\r\n");
xTaskCreate(Task1Function, "Task1", 100, NULL, 0, &xHandleTask1);
/* Start the scheduler. */
vTaskStartScheduler();
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
运行结果:
在任务2为低电平时,任务1和空闲任务交替执行,空闲任务会调用到我们提供的钩子函数,完成了清理工作。
对应程序:11_freertos_example_scheduler
正在运行的任务,被称为"正在使用处理器",它处于运行状态。在单处理器系统中,任何时间里只能有一个任务处于运行状态。
非运行状态的任务,它处于这3种状态之一:
阻塞(Blocked)
暂停(Suspended)
就绪(Ready)
就绪态的任务,可以被调度器挑选出来切换为运行状态,调度器永远都是挑选最高优先级的就绪态任务并让它进入运行状态。
阻塞状态的任务,它在等待"事件",当事件发生时任务就会进入就绪状态。
事件分为两类:
时间相关的事件
所谓时间相关的事件,就是设置超时时间:在指定时间内阻塞,时间到了就进入就绪状态。
使用时间相关的事件,可以实现周期性的功能、可以实现超时功能。
同步事件
同步事件就是:某个任务在等待某些信息,别的任务或者中断服务程序会给它发送信息。
怎么"发送信息"?方法很多
任务通知(task notification)
队列(queue)
事件组(event group)
信号量(semaphoe)
互斥量(mutex)等
这些方法用来发送同步信息,比如表示某个外设得到了数据。
是否抢占?
允许抢占时,是否允许时间片轮转?
允许抢占、允许时间片轮转时,空闲任务是否让步?
以下调度规则使用的代码如下,修改不同的宏定义,既有不同的效果。
static void prvSetupHardware( void );
static volatile int flagIdleTaskrun = 0; // 绌洪棽浠诲姟杩愯鏃秄lagIdleTaskrun=1
static volatile int flagTask1run = 0; // 浠诲姟1杩愯鏃秄lagTask1run=1
static volatile int flagTask2run = 0; // 浠诲姟2杩愯鏃秄lagTask2run=1
static volatile int flagTask3run = 0; // 浠诲姟3杩愯鏃秄lagTask3run=1
/*-----------------------------------------------------------*/
void vTask1( void *pvParameters )
{
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
flagIdleTaskrun = 0;
flagTask1run = 1;
flagTask2run = 0;
flagTask3run = 0;
/* 打印任务的信息 */
printf("T1\r\n");
}
}
void vTask2( void *pvParameters )
{
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
flagIdleTaskrun = 0;
flagTask1run = 0;
flagTask2run = 1;
flagTask3run = 0;
/* 打印任务的信息 */
printf("T2\r\n");
}
}
void vTask3( void *pvParameters )
{
const TickType_t xDelay5ms = pdMS_TO_TICKS( 5UL );
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
flagIdleTaskrun = 0;
flagTask1run = 0;
flagTask2run = 0;
flagTask3run = 1;
/* 打印任务的信息 */
printf("T3\r\n");
// 如果不休眠的话,其他任务无法得到执行
vTaskDelay( xDelay5ms );
}
}
void vApplicationIdleHook(void)
{
flagIdleTaskrun = 1;
flagTask1run = 0;
flagTask2run = 0;
flagTask3run = 0;
/* 故意加入打印让flagIdleTaskrun变为1的时间维持长一点 */
//printf("Id\r\n");
}
int main( void )
{
prvSetupHardware();
xTaskCreate(vTask1, "Task 1", 1000, NULL, 0, NULL);
xTaskCreate(vTask2, "Task 2", 1000, NULL, 0, NULL);
xTaskCreate(vTask3, "Task 3", 1000, NULL, 2, NULL);
vTaskStartScheduler();
/* Will only get here if there was not enough heap space to create theidle task. */
return 0;
}
以下配置项都是在 FreeRTOSConfig.h 中。
#define configUSE_PREEMPTION 1
可抢占调度"(Pre-emptive),宏定义为1的时候,支持该调度规则。
#define configUSE_PREEMPTION 0
可抢占调度"(Pre-emptive),宏定义为0的时候,不支持该调度规则,只要任务3让出cpu资源后,任务1就一直执行下去了,高优先级的就绪态也不能马上运行,只能等待当前任务主动放弃当前cpu的资源,平级任务更应该等待。
破解这种方法,需要使用如下的代码,让其主动休眠一会:
// 如果不休眠的话,其他任务无法得到执行
vTaskDelay( xDelay5ms );
#define configUSE_TIME_SLICING 1
时间片轮转"(Time Slicing),如果使用时间片轮转,宏定义应该是1,如果不使用时间片轮转,是0,运行结果为下图 ,高优先级任务3先执行,直到自己让出cpu资源,其他优先级为0的任务才可以执行,这时候谁抢占上,就一直执行,直到任务2再次抢占过去cpu资源。
空闲任务是否让步于用户任务(配置项:configIDLE_SHOULD_YIELD)
#define configIDLE_SHOULD_YIELD 1
空闲任务低人一等,每执行一次循环,就看看是否主动让位给用户任务
#define configIDLE_SHOULD_YIELD 0
空闲任务跟用户任务一样,大家轮流执行,没有谁更特殊