FreeRTOS
测试源码地址
github.com/CherryXiuHuaWoo/WIN32-MSVC-FreeRTOS-
任务管理
任务:每个执行线程都被成为任务。
每个任务都是在自己权限范围内的一个小程序。
1、有程序入口
2、通常会运行再一个死循环,不会退出。任务函数原型
void ATaskFunction( void *pvParameters );
1、必须返回void
2、带有一个void指针参数
3、FreeRTOS任务不允许以任何方式从实现函数中返回(绝不能有return,也不能执行到函数尾部)。
4、一个任务不再需要,可以显式地将其删除。
5、一个任务函数可以用来创建若干个任务(创建出的任务均是独立的执行实例,有属于自己的栈空间,以及属于自己的自动变量(栈变量))
- 典型的任务函数结构
void ATaskFunction( void *pvParameters )
{
/*可定义属于自己的iVaribleExample变量(非static)*/
int iVariableExample = 0;
/*任务通常实现在一个死循环中*/
for( ;; )
{
/*此处存放完成任务功能的代码*/
}
/*若任务的具体实现会调出上面的死循环,则次任务必须在函数运行完之前删除。
传入NULL参数表示删除的是当前任务*/
vTaskDelete( NULL );
}
在应用程序中,FreeRTOS如何为各任务分配处理时间?
在任意给定时刻,FreeRTOS如何选择任务投入运行?
任务优先级如何影响系统行为?
任务存在哪些状态?
应用程序可以包含多个任务。
如果:
运行程序的CPU只有一个核(core) →→
在任意给定时间,只会有一个任务被执行→→
一个任务可以有1个或2个状态:
1、运行状态(Running State)
2、非运行状态(Not Running State)
- 阻塞态(blocked):一个任务正在等待某个事件。
- 挂起态(supended):让一个任务进入挂起状态的唯一办法——调用vTaskSuspend() API函数;把一个任务唤醒的唯一途径——调用vTaskResume()或vTaskResumeFromISR() API函数。
挂起状态的任务对于调度器而言是不可见的,大多数应用程序中都不会用到挂起状态。 - 就绪态(ready):若任务处于非运行态,但既没有阻塞也没有挂起,则这个任务处于就绪。处于就绪态(ready)的任务能够被运行,只是“准备(ready)”运行,而当前尚未运行。
3、完整的状态转移图
切换入/切入(switched in) 、交换入(swapped in):任务从非运行态转移到运行态。
切换出/切出(switched out)、交换出(swapped out):任务从运行态转移到非运行态。
FreeRTOS的调度器是能让任务切入切出的唯一实体。
为任务切实有用,我们需要通过某种方式来进行事件驱动。
一个事件驱动任务只会在事件发生后触发工作(处理),而在事件没有发生时是不能进入运行态的。
采用事件驱动任务的意义:任务可以被创建在许多不同的优先级上,并且最高优先级不会吧所有的低优先级任务饿死。
实例4:使用阻塞态实现延时(vTaskDelay)。
通过调用vTaskDelay( API函数来代替空循环)
函数原型:
void vTaskDelay( portTickType xTicksToDelay );
xTicksToDelayL:延时多少个心跳周期。调用该延时函数的任务将进入阻塞态,经延时指定心跳周期数后,在转移到就绪态。
例:当某个任务调用vTaskDelay(100)时,心跳计数值为10000,则该任务将保持在阻塞态,知道心跳计算到10100。
常数portTICK_RATE_MS:用来将以毫秒为单位的时间值转换为以心跳周期为单位的时间值。
void vTaskFuction( void *pvParametes)
{
char *pcTaskName;
pcTaskName = (char *)pvParameters;
for( ;; )
{
vPrintSrting( pcTaskName);
/*延时一个循环周期,让任务在延时器件保持在阻塞态。延时时间以心跳周期为单位,常量portTICK_RATE_MS可以用来在猫喵和心跳周期之间相互转换。*/
vTaskDelay(250 / portTICK_RATE_MS);
}
}
实例5:精确阻塞时间延时(vTaskDelayUnit)
vTaskDelayUnit函数原型:
void vTaskDelayUntil( portTickType *pxPreviousWakeTime, portTickType xTimeIncrement)
作用:实现任务以固定频率周期性执行。
参数:
1、pxPreviousWakeTime: 保存任务上一次离开阻塞态(被唤醒)的时刻。这个时刻用作一个参考点来计算改任务下次离开阻塞态的时刻。pxPreviousWakeTime 指向的变量值会在API函数 vTaskDelayUnit()调用过程中自动更新,应用程序处理改变量第一次初始化外,通常都不要修改它的值。
2、xTimeIncrement:周期频率。单位:心跳周期(可使用常量portTICK_RATE_MS将毫秒转换为心跳周期)。
void vTaskFunction(void *pvParameters)
{
char *pcTaskName;
portTickType xLastWakeTime;
volatile unsigned long u1;
pcTaskName = (char *)pvParameters;
/*Save the current TickTime Value.
*Here is the only time for xLastWakeTime to assign.
*xLaskWakeTime will be updated in vTaskDelayUntil.
*/
xLastWakeTime = xTaskGetTickCount;
while (1)
{
printf("Time0=%d,%s", (int)xLastWakeTime, pcTaskName);
//vTaskDelay(250 / portTICK_RATE_MS);
vTaskDelayUntil(&xLastWakeTime, (250 / portTICK_RATE_MS));
printf("Time1=%d\r\n", (int)xLastWakeTime);
}
while (1)
{}
}
实例6:合并阻塞与非阻塞任务
持续处理(continuous processing)任务:任务没有调用任何可能导致其进入阻塞态的API函数,要么在就绪态,要么在运行态,具有这种性质的任务称为持续处理任务。
/*代码测试有问题,未想明白~~~*/
如何实现一个任务?
- 创建任务xTaskCreate() API函数原型
portBASE_TYPE xTaskCreate(
pdTASK_CODE pvTaskCode, /*指向任务的实现函数的指针*/
const signed portCHAR * const_pcName, /*具有描述性的任务名*/
unsigned portSHORT usStackDepth, /*为任务分配多大的栈空间,单位:字(word)*/
void *pvParameters, /*传递到任务中的值*/
unsigned portBASE_TYPE uxPriority, /*指定任务执行的优先级*/
xTaskHandle *pxCreatedTask /*传出任务的句柄*/
);
返回值:
1. pdTRUE——任务创建成功。
2. errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY——内存空间不足,FreeRTOS无法分配足够的空间来保存任务结构数据和任务栈,因此无法创建任务。
备注:
1.usStackDepth——告诉内核需要为任务分配多大的栈空间,单位:字(word)。栈深度乘以栈宽度的结果千万不能超过一个size_t类型变量所能表达的最大值
如:32位宽的栈空间,usStackDepth=100
则:将会分配4*100=400(byte)的栈空间
应用程序通过定义常数configMINIMAL_STACK_SIZE来决定空闲任务所用的栈空间大小
2.uxPriority——指定任务执行的优先级。取值范围:0(最低优先级) to configMAX_PRIOPITIES - 1 (最高优先级)。
configMAX_PRIOPITIES:由用户定义的常量。优先级号无上限(除了受限于采用的数据类型和系统的有效内存空间),但最好使用实际需要的最小数值以避免内存浪费。如果uxPriority的值超过了(configMAX_PRIORTIES -1),将会导致实际付给任务的优先级会自动封顶到最大合法值。
3.pxCreatedTask——用于传出任务的句柄。这个句柄将在API调用中对该创建出来的任务进行引用,如改变优先级,删除任务。
如果应用程序中不会用到这个任务的句柄,则可以设为NULL。
如何创建一个或多个任务的实例?
实例1:创建单层任务。
void vTask1( void *pvParameters)
{
const char *pcTaskName = "Task 1 is running\r\n";
volatile unsigned long u1;
for( ;; ) /*死循环*/
{
vPrintString( pcTaskName ); /*Print out the name of this task.*/
_sleep(10); /*延时*/
}
}
void vTask2( void *pvParameters)
{
const char *pcTaskName = "Task 2 is running\r\n";
volatile unsigned long u1;
for( ;; ) /*死循环*/
{
vPrintString( pcTaskName ); /*Print out the name of this task.*/
_sleep(10); /*延时*/
}
}
int main( void)
{
xTaskCreate( vTask1, /*指向任务函数的指针*/
"Task 1", /*任务的文本名字,只会在调试中用到*/
1000, /*栈深度-大多数小微MCU会使用的值会比此值小得多*/
NULL, /*无任务参数*/
1, /*此任务运行在优先级1上*/
NULL); /*不会用到任务句柄*/
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL);
vTaskStartScheduler(); /*启动调度器,任务开始执行*/
/*若一切运行正常, main()函数不应该会执行到此处,
但如果执行到了此处,则很可能是内存堆不足导致空闲任务无法创建*/
for(;;);
}
实例2:实现嵌套任务。
void vTask1( void *pvParameters)
{
const char *pcTaskName = "Task 1 is running\r\n";
volatile unsigned long u1;
/*如果运行到本任务代码,表明调度器已经启动。在进入死循环之前创建另一个任务。*/
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL);
for( ;; ) /*死循环*/
{
vPrintString( pcTaskName ); /*Print out the name of this task.*/
_sleep(10); /*延时*/
}
}
如何使用任务参数?
实例1和2中的Task1和2实现的功能均是一样的,区别在于打印的字符串不一样。这种重复性可以通过创建同一任务代码的两个实例来去除,这时,任务参数就可以用来传递各自打印输出字符串。
实例3:任务函数(vTaskFunction)代替了vTask1和vTask2, 任务参数被强制转换为char*以得到任务需要打印输出的字符串。
void vTaskFuction( void *pvParameters )
{
char * pcTaskName;
volatile unsigned long u1;
/*需要打印输出的字符串从入口参数闯入。强行转换为字符指针*/
pcTaskName = ( char *) pvParameters;
for( ;; )
{
vPrintString( pcTaskName);
_sleep(10); /*延时*/
}
}
/*定义将要通过任务参数传递的字符串。定义为const,且不是在栈空间上,以保证任务执行师也有效*/
static const char *pcTextForTask1 = "Task 1 is running\r\n";
static const char *pcTaxtForTask2 = "Task 2 is running\r\n";
int main( void)
{
xTaskCreate(vTaskFunction, "Task 1", 1000, (void *)pcTextForTask1, 1, NULL);
xTaskCreate(vTaskFunction, "Task 2", 1000, (void *)pcTextForTask2, 1, NULL);
vTaskStartScheduler();
for( ;; );
}
如何改变一个已创建任务的优先级?
xTaskCreate() API函数的参数uxPriority为创建的任务赋予一个初始优先级。
这个优先级可以在调度器启动后调用vTaskPrioritySet() API函数进行修改。
应用程序在文件 FreeRTOSConfig.h中 configMAX_PRIORITIES的值,即是最多可以具有的优先级。
值越大,内核花销的内存空间就越多。建议将该值设置为能够用到的最小值。
任意数量的任务可以共享同一个优先级——以保证最大设计弹性。
优先级号0表示最低优先级。
有效的优先级号范围从0到(cinfigMAX_PRIORITES -1)
调度器保证总是在所有可运行的任务中选择具有最高优先级的任务进入运行态。
如果被选中的优先级上具有不止1个任务,调度器会轮流执行这些任务。
任务在时间片起始时刻进入运行态,在结束时刻退出运行态。
t1与t2之间的时段就等于1个时间片。
心跳(tick,时钟滴答)中断的周期性中断:调度器需要在每个时间片的结束时刻运行自己本身,才能够选择下一个运行任务。
时间片长度:通过心跳中断的频率设定,
心跳中断频率:由FreeRTOSConfig.h中时间配置常量configTICK_RATE_HZ进行配置。
例:cinfigTICK_RATE_HZ设为100(HZ)
则时间片长度为10ms
FreeRTOS API函数调用中指定的时间总是以心跳为单位的时间值转化而来(单位:ms)。有效精度依赖于系统心跳频率。
改变任务优先级
vTaskPriofitySet函数原型
作用: vTaskPriofitySet()可在调度器启动后改变任务的优先级
void vTaskPrioritySet( xTaskHandle pxTask, unsigned portBASE_TYPE uxNewPriority);
参数:
- pxTask——被修改优先级的任务句柄。任务可通过传入NULL值来修改自己的优先级。
- uxNewPriority——目标任务呗设置到哪个优先级上。当超过最高优先级,会自动设为最大值。
uxTaskPriortyGet() 函数原型
作用:查询一个任务的优先级。
unsigned portBASE_TYPE uxTaskPriorityGet( xTaskHandle pxTask);
参数:
- pxTask——被查询任务的句柄。任务可通过传入NULL值来查询自己的优先级。
- 返回值:被查询任务的当前优先级。
实例8:改变任务优先级
extern TaskHandle_t xTask1Handle;
extern TaskHandle_t xTask2Handle;
void vTask1(void *pvParameters)
{
unsigned portBASE_TYPE uxPriority;
uxPriority = uxTaskPriorityGet(NULL);
while (1)
{
printf("Task 1 is running\r\n");
printf("Raise the Task2 priority\r\n");
vTaskPrioritySet(xTask2Handle, (uxPriority + 1));
}
}
void vTask2(void *pvParameters)
{
unsigned portBASE_TYPE uxPriority;
uxPriority = uxTaskPriorityGet(NULL);
while (1)
{
printf("Task 2 is running\r\n");
printf("Lower the Task2 priority\r\n");
vTaskPrioritySet(NULL, (uxPriority - 2));
}
}
/*main部分*/
xTaskCreate(vTask1, "Task 1", 100, (void *)pcTextForTask1, 2, &xTask1Handle);
xTaskCreate(vTask2, "Task 2", 100, (void *)pcTextForTask2, 1, &xTask2Handle);
/* Start the tasks and timer running. */
vTaskStartScheduler();
如何删除任务?
vTaskDelete() API函数
作用:任务可以使用API函数vTaskDelete()删除自己或其他任务。任务被删除后就不复存在,也就不会进入运行态。
空闲任务的责任:将分配给已删除的内存释放掉。
注意:
- 只有内核为任务分配的内存空间才会在任务被删除后自动回收。
- 任务自己占用的内存或资源需要由应用程序自己显式地释放掉。
void vTaskDelete( xTaskHandle pxTaskToDelete);
参数:
pxTaskToDelete——被删除任务的句柄(目标任务)。任务可以通过传入NULL值来删除自己。
实例9:测试vTaskDelete
extern TaskHandle_t xTask1Handle;
extern TaskHandle_t xTask2Handle;
void vTask1(void *pvParameters)
{
unsigned portBASE_TYPE uxPriority;
uxPriority = uxTaskPriorityGet(NULL);
while (1)
{
printf("Task 1 is running\r\n");
xTaskCreate(vTask2, "Task 2", 100, NULL, 2, &xTask2Handle);
vTaskDelay(100 / portTICK_RATE_MS);
}
}
void vTask2(void *pvParameters)
{
printf("Task 2 is running. Task 2 will delete itself at soon\r\n");
vTaskDelete(xTask2Handle);
}
/*main*/
xTaskCreate(vTask1, "Task 1", 100, (void *)pcTextForTask1, 1, &xTask1Handle);
如何实现周期性处理?
优先级抢占式调度
- 固定优先级抢占式调度——每个任务都被赋予一个优先级,这个优先级不能被内核本身修改(只能被任务修改)。
选择任务优先级
协作式调度
空闲任务何时运行,可以用来干什么。
什么是空闲任务?
空闲任务:在调度器启动时在自动创建,以保证至少有1个任务可运行(至少有1个任务在就绪状态)
当调用vTaskStartSchedule()时,调度器会自动创建一个空闲任务。
空闲任务拥有最低优先级(优先级0),以保证不会妨碍具有更高优先级的任务。
空闲任务回调(hook or call-back)
通过空闲任务回调函数,可以直接在空闲任务中添加应用程序相关的功能。
空闲任务回调函数会被空闲任务每循环一次就自动调用一次。
空闲任务回调函数的作用
- 执行低优先级,后台或需要不停处理的功能代码。
- 测试出系统处理裕量(空闲任务只会在所有其他任务都不运行时才会有机会执行,因此测量出空闲任务占用的处理时间就可以知道系统有多少的处理时间)。
- 将处理器配置到低功耗模式——提供一种自动省电方法,使得在没有任何应用功能需要处理时,系统自动进入省电模式。
空闲任务回调函数的实现限制
- 绝对不能挂起
- 若应用程序用到了vTaskDelete() API函数,则空闲任务回调函数必须能够尽快返回。因为在任务呗删除后,空闲任务负责回收内核资源。
空闲任务回调函数原型
void vApplicationIdleHook( void );
FreeRTOSConfig.h中的配置常量configUSE_IDLE_HOOK必须定义为1,这样空闲任务回调函数才会被调用。
实例7:定义一个空闲任务回调函数
unsigned long u1IdleCycleCount = 0UL;
void vApplicationIdleHook(void)
{
u1IdleCycleCount++;
}
/*
* Function Name: vPeriodicTask
* Description: vPeriodicTask
* Input:
* pvParameters --- Parameters Pointer
* Output:
* Return: None
*/
void vPeriodicTask(void * pvParameters)
{
portTickType xLastWakeTime;
xLastWakeTime = xTaskGetTickCount();
while (1)
{
printf("%d\r\n", u1IdleCycleCount);
vTaskDelayUntil(&xLastWakeTime, (250 / portTICK_RATE_MS));
}
}