1、创建任务函数
1.1 动态内存的使用
在之前我们如果要创建一个与学生有关的任务,我们会定义:
//打印50个学生的信息
char name[50][100];
int age[50];
int sex[50]; //1表示男,0表示女
int score[50];
如果之后要对其进行修改会非常麻烦,因此我们要引入面对对象的编程思想,这个对象就是student,那么我们就定义一个相关的结构体来表示一个学生:
struct Student{char name[100];int age;int sex;int score;struct Student *next; //这里定义一个指针,可以使用链表把学生管理起来
};
通过这种编程思想,我们在FressRTOS中对任务也要构造出一个结构体,之前我们是通过xTaskCreate函数动态创建了任务,xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1),同时在创建时指定了栈的大小为100*4个字节,这个TaskHandle_t * const则是指向了typedef struct tskTaskControlBlock * TaskHandle_t,也就是TCB_t *这个结构体,所以说我们创建任务时所返回的handle,就只是这个TCB_t 的指针,另外取了一个名字。
在xTaskCreate中有一个TCB_t *结构体,同时可以看出这里用了malloc从动态内存,也就是从堆里面来做分配
1.2 静态创建任务
使用xTaskCreateStatic函数静态创建任务,要事先分配好TCB结构体,栈。
如果要使用这个函数,还要在FreeRTOSconfig.h中定义"configSUPPORT_STATIC_ALLOCATION""为1,并且实现vApplicationGetldleTaskMemory函数。
//关键代码
void Task1Function( void * param)
{while(1){printf("1");}
}void Task2Function( void * param)
{while(1){printf("2");}
}void Task3Function( void * param)
{while(1){printf("3");}
}/*-----------------------------------------------------------*/
StackType_t xTask3Stack[100];StaticTask_t xTask3TCB;StackType_t xIdleTaskStack[100]; //定义空闲任务栈
StaticTask_t xIdleTask3TCB; //定义空闲任务TCB//申请获得空闲任务内存
//要提供空闲任务的TCBBuffer,空闲任务的栈Buffer,空闲任务的栈大小
void vApplicationGetIdleTaskMemory(StaticTask_t ** ppxIdleTaskTCBBuffer, StackType_t ** ppxIdleTaskStackBuffer,uint32_t * pulIdleTaskStackSize)
{*ppxIdleTaskTCBBuffer = &xIdleTask3TCB;*ppxIdleTaskStackBuffer = xIdleTaskStack;*pulIdleTaskStackSize = 100;
}//main.c
xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1); //动态创建
xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL); //动态创建
xTaskCreateStatic(Task3Function, "Task3", 100, NULL, 1, xTask3Stack, &xTask3TCB); //静态创建
1.3 进一步实验
1.3.1 优先级实验
在FreeRTOS中,优先级的取值范围是:0~(configMAX_PRIORITIES – 1),数值越大优先级越高。
在之前的代码中添加修改:
static int task1flagrun = 0;
static int task2flagrun = 0;
static int task3flagrun = 0;void Task1Function( void * param)
{while(1){task1flagrun = 1; task2flagrun = 0;task3flagrun = 0;printf("1");}
}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");}
}
然后再main函数中打断点,打开调试,运行到断点,然后将task1flagrun、task2flagrun、task3flagrun这三个变量添加到逻辑分析仪中去运行一段时间,我们会发现,同优先级的3个任务,不会同时进行。
如果我们将Task1Function的优先级设置为2,其它仍为1,运行时候我们就会发现这时候,只打印1,没有执行另外两个任务。由此可以看出,对于FreeRTOS来说,在默认的调度下面,高优先级的任务先执行,如果高优先级的任务没有主动放弃运行,其它低优先级的任务无法执行。
1.3.2 删除任务
我们在创建任务的时候传入了一个handle,以后想要引用这个任务就必须要通过这个handle执行。如果
void Task2Function( void * param)
{int i = 0;while(1){task1flagrun = 0; task2flagrun = 1;task3flagrun = 0;printf("2");//删除任务一if(i++ == 100){vTaskDelete(xHandleTask1);}//自杀if(i == 200){vTaskDelete(NULL);}}
}
通过观察实验结果可以发现,经过修改的程序,在运行之初会打印1、2、3,运行一会儿后只打印2、3,再之后就只打印3了。
使用vTaskDelete既可以删除动态创建的任务,也可以删掉静态创建的任务,但是要想删除任务,必须要记录创建任务时的返回值handle。
FreeRTOS在xTaskCreate中分配TCB和栈,但是并不是在vTaskDelete中释放TCB和栈,而是在空闲任务中进行这些清理工作,如果连续不断的调用xTaskCreate和vTaskDelete,最终会导致内存耗光。
1.3.3 使用同一个任务函数创建多个任务
void TaskGenericFunction( void * param)
{int val = (int)param;while(1){ printf("%d", val);}
}xTaskCreate(TaskGenericFunction, "Task4", 100, (void *)4, 1, NULL);
xTaskCreate(TaskGenericFunction, "Task5", 100, (void *)5, 1, NULL);
通过结果发现4和5都成功打印出来了,证明可以在同一个任务函数中创建多个任务。
使用同一个任务会产生不同的效果,是因为它们的栈是不一样的,传入的参数都保存在不同的栈里面,运行的时候互不影响。
1.3.4 栈大小实验
void Task1Function( void * param)
{//在创建任务时,申请了100*4的空间,这里故意定义500,耗尽栈的空间,从高地址向下增长,破坏下面的空间//这样程序运行的时候是完全不可控的,程序会崩溃volatile char buf[500]; //使用关键字,不允许没有使用的空间被优化掉int i;while(1){task1flagrun = 1; task2flagrun = 0;task3flagrun = 0;printf("1");for(i = 0; i < 500; i++){buf[i] = 0;}}
}
程序按上面修改之后运行,直接崩溃了,在申请的栈空间以及TCB空间的前面会有一个头来存储有关的信息,便于程序返回等操作,一旦栈的空间使用不当,冲破了空间限制,把前面头部的信息更改了,程序就会发生不可控的问题。
2、任务状态
2.1 任务切换的基础:tick中断
在FreeRTOS系统中有个定时器,这个定时器每隔一段时间会产生一个中断,这个间隔就称为tick,发生中断时,它会将发生中断的次数记录下来,初始值为0,每发生一次中断就累加1,这个中断值就成为tick count,这将是RTOS的时钟基准。
在发生中断时,tick中断处理函数被调用,在这个函数中,它会判断是否要切换任务,如果要切换任务就会去切换任务。而运行任务的基准时间(周期),可以在源码的FreeRTOSConfig.h中去配置,我们也可以指定每个任务每次执行几个tick:
#define configTICK_RATE_HZ ((TickType_t)1000)
在使用keil模拟器中的逻辑分析仪时,如果时间不准确,要确认代码中设置时钟时用的频率和Options中Xtal的频率相同。
在main函数中创建多个任务时,为什么后面创建的任务反而先运行?因为后面的任务插入链表时,pxCurrentTCB先执行它。创建任务时代码如下,后创建的最高优先级任务先执行:
2.2 有哪些任务状态?状态切换图
正在运行的任务:Running状态
可以随时运行但是现在还没轮到:Ready状态
阻塞,等待某些事情发生才能继续执行:Blocked状态
暂停,主动/被动休息:Suspended状态
状态转换图:
2.3 怎么管理不同状态的任务:放在不同的链表里
将不同状态的任务放在不同的链表里,当有需要执行时,就从对应链表中挑出一个来执行。
2.4 阻塞状态(Blocked)举例:vTaskDelay函数
使用vTaskDelay时,如果想要延时若干毫秒,那么可以自己把毫秒换算成Tick数,或者使用宏把毫秒换算成Tick数。有一个宏:pdMS_TO_TICKS(ms),可以把毫秒转换成Tick数。
void Task2Function( void * param)
{int i = 0;while(1){task1flagrun = 0; task2flagrun = 1;task3flagrun = 0;printf("2");//让任务2进入阻塞状态vTaskDelay(10);}
}
2.5 暂停状态(Suspended)举例:vTaskSuspend/vTaskResume
void Task1Function( void * param)
{//获得TickCount的值TickType_t tStart = xTaskGetTickCount();TickType_t t;int flag = 0;while(1){t = xTaskGetTickCount(); //在运行的时候获取当前时间task1flagrun = 1; task2flagrun = 0;task3flagrun = 0;printf("1");//命令任务3进入暂停状态,如果想让自己主动休息,可以传入NULL//对于进入暂停状态的任务必须由别人来唤醒if(!flag && t > tStart + 10){vTaskSuspend(xHandleTask3);flag = 1; //设置一个标志位,不要让重复让任务3进入暂停状态}if(t > tStart + 30){//命令任务3恢复运行状态vTaskResume(xHandleTask3);}}
}
3、实现周期性的任务
3.1 vTaskDelay
至少等待指定个数的Tick Interrupt才能变成就绪状态。
static int rands[] = {2, 18, 64, 121, 9};void Task1Function( void * param)
{//获得TickCount的值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;}vTaskDelay(20); //延时20个tick}
}
通过结果图可以看出,延迟的时间的一致的。
3.2 vTaskDelayUntil
等待到指定的绝对时刻,才能变为就绪状态。
vTaskDelayUntil是老版本,它没有返回值,在新版本中可以用xTaskDelayUntil函数,二者传入的参数是一样的,只是新的有返回值。
static int rands[] = {2, 18, 64, 121, 9};void Task1Function( void * param)
{//获得TickCount的值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 0vTaskDelay(20); //延时20个tick
#elsevTaskDelayUntil(&tStart, 20); //延时到(tStart+20tick)时刻,同时更新tStart=tStart+20tick
#endif}
}
通过结果图可以看出,每两次开始执行之间的时间是一致的。
4、空闲任务及其钩子函数
4.1 空闲任务
在xTaskCreate中分配TCB和栈,但是并不一定是在vTaskDelete中释放TCB和栈,对于自杀的任务,由空闲任务来清理内存,对于他杀的任务,由凶手来清理。
我们在任务1中创建任务2,并且将任务一的优先级设置为1,任务二的优先级设置为2,在任务二中打印语句之后自杀。这样我们在程序中会有3个任务,任务一、任务二和空闲任务,空闲任务的优先级最低,为0。这样执行之后,程序很快就崩溃了,因为可分配的内存不够了,堆不够了。
void Task2Function( void * param);/*-----------------------------------------------------------*/void Task1Function( void * param)
{TaskHandle_t xHandleTask2;BaseType_t xReturn;while(1){printf("1");xReturn = xTaskCreate(Task2Function, "Task2", 1024, NULL, 2, &xHandleTask2);if(xReturn != pdPASS){printf("xTaskCreate err\r\n");}}
}void Task2Function( void * param)
{while(1){printf("2");//vTaskDelay(2);vTaskDelete(NULL);}
}
如果是在任务一中杀死任务二,那么程序现象会一直执行。
void Task2Function( void * param);/*-----------------------------------------------------------*/void Task1Function( void * param)
{TaskHandle_t xHandleTask2;BaseType_t xReturn;while(1){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){printf("2");vTaskDelay(2);}
}
4.2 钩子函数
使用空闲任务不仅可以帮我们清理自杀的任务,还可以执行一些低优先级、后台的、需要连续执行的函数、测量系统的空闲时间、让系统进入省电模式等。如果要做到这些,我们可以通过修改空闲任务的函数来实现,但是如果直接修改会破坏FreeRTOS的核心文件,因此提供了一个钩子函数,可以先配置规定的宏,来告诉程序你要使用这个钩子函数。
对钩子函数,空闲任务对其也会有一些限制:不能导致空闲任务进入阻塞状态、暂停状态;如果你会使用vTaskDelete()来删除任务,那么钩子函数要非常高效地执行。如果空闲任务一直卡在钩子函数里的话,它就无法释放内存。
使用钩子函数的前提:
在FreeRTOSConfig.h中把这个宏定义为1:configUSE_IDLE_HOOK
实现vApplicationIdleHook函数
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");
}//main函数里
xTaskCreate(Task1Function, "Task1", 100, NULL, 0, &xHandleTask1);
在这个程序中,任务一先执行,在任务一中创建任务二,此时任务二的优先级最高,因此任务二先执行,之后杀掉任务二,任务一和空闲任务的优先级相同,二者交替执行,在执行空闲任务时会用到钩子函数。
5、任务调度算法
5.1 状态与事件
正在运行的任务,被称为"正在使用处理器",它处于运行状态。在单处理器系统中,任何时间里只能有一个任务处于运行状态。
非运行状态的任务,它处于这3种状态之一:
阻塞(Blocked)
暂停(Suspended)
就绪(Ready)
就绪态的任务,可以被调度器挑选出来切换为运行状态,调度器永远都是挑选最高优先级的就绪态任务并让它进入运行状态。
阻塞状态的任务,它在等待"事件",当事件发生时任务就会进入就绪状态。
事件分为两类:
时间相关的事件:就是设置超时时间,在指定时间内阻塞,时间到了就进入就绪状态。使用时间相关的事件,可以实现周期性的功能、可以实现超时功能。
同步事件:同步事件就是某个任务在等待某些信息,别的任务或者中断服务程序会给它发送信息。怎么"发送信息"的方法有很多,比如任务通知(task notification)、队列(queue)、事件组(event group)、信号量(semaphoe)、互斥量(mutex)等。这些方法用来发送同步信息,比如表示某个外设得到了数据。
5.2 调度策略
5.2.1 可否抢占?
高优先级的任务能否优先执行(配置项: configUSE_PREEMPTION)
如果可以:被称作"可抢占调度"(Pre-emptive),高优先级的就绪任务马上执行,下面再细化。
如果不可以:不能抢就只能协商了,被称作"合作调度模式"(Co-operative Scheduling)。当前任务执行时,更高优先级的任务就绪了也不能马上运行,只能等待当前任务主动让出CPU资源。其他同优先级的任务也只能等待:更高优先级的任务都不能抢占,平级的更应该老实点
2.2.2 可抢占的前提下,同优先级的任务是否轮流执行(配置项: configUSE_TIME_SLICING)?
轮流执行:被称为"时间片轮转"(Time Slicing),同优先级的任务轮流执行,你执行一个时间片、我再执行一个时间片
不轮流执行:英文为"without Time Slicing",当前任务会一直执行,直到主动放弃、或者被高优先级任务抢占。
5.2.3 允许抢占、允许时间片轮转时,空闲任务是否让步?
在"可抢占"+"时间片轮转"的前提下,进一步细化:空闲任务是否让步于用户任务(配置项:configlDLE_SHOULD_YIELD)。如果让步,则空闲任务低人一等,每执行一次循环,就看看是否主动让位给用户任务。如果不让步,空闲任务跟用户任务一样,大家轮流执行,没有谁更特殊。