1. 建议在阅读之前下载FreeRTos源码配合阅读
2. 源码阅读确实特别枯燥无趣,可以配合硬件来学习,我购买的是乐鑫科技的ESP8266配合学习的,STM32的也是可以的,编译好固件之后烧录通过串口反馈信息可以验证你的程序
3. 推荐使用UBUNTU环境下进行编译,同时能锻炼你的linux使用能力与故障排查能力(莫名其妙很多warning和error,我已经要无了 = = )
本文基于《USING THE FREERTOS REAL TIME KERNEL》进行整理,感谢作者与翻译Zou Changjun
一、任务
1. 任务函数
void ATaskFunction( void *pvParameters );
FreeRTOS 任务不允许以任何方式从实现函数中返回——它们绝不能有一条==”return”==语句,也不能执行到函数末尾。如果一个任务不再需要,可以显式地将其删除。
一个任务函数可以用来创建若干个任务——创建出的任务均是独立的执行实例,拥有属于自己的栈空间,以及属于自己的自动变量(栈变量),即任务函数本身定义的变量。
2. 创建任务
portBASE_TYPE xTaskCreate( pdTASK_CODE pvTaskCode,
const signed portCHAR * const pcName,
unsigned portSHORT usStackDepth,
void *pvParameters,
unsigned portBASE_TYPE uxPriority,
xTaskHandle *pxCreatedTask );
//示例
int main (void) {
xTaskCreate( vTask1, /* 指向任务函数的指针 */
"Task 1", /* 任务的文本名字,只会在调试中用到 */
1000, /* 栈深度 – 大多数小型微控制器会使用的值会比此值小得多 */
NULL, /* 没有任务参数 */
1, /* 此任务运行在优先级1上. */
NULL ); /* 不会用到任务句柄 */
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
vTaskStartScheduler(); //启动调度器,任务开始执行
for (;;);
}
3. 任务优先级
在VxWorks的学习记录中已经提到,对于实时系统,调度算法是非常重要的,基本上RTOS比较常用的就是基于优先级的时间片调度,FreeRTOS也一样。
上一节提到的创建任务函数中就已经设定了任务的优先级,同时该优先级也支持进行更改,与Vx不同的是,Vx的0优先级为最高,FreeRTOS的0优先级最低。同时FreeRTOS没有设定这个常量的最大值,该值越大,内核花销的内存空间越多,因此建议将常量设为能够用到的最小值。
个人理解:内核需要维护多个基于优先级设置队列,同优先级任务置于一个队列中,优先级越多,队列越多,从而导致花销增多
任务调度以心跳(tick)为单位,可设置为毫秒为单位。
用户程序在指定延迟周期时不必考虑心跳计数溢出问题,因为时间连 贯性在内核中进行管理。
基于优先级调度的算法可能导致出现饥饿问题,既低优先级的任务持续得不到CPU的使用权,内核一直在调度高优先级任务。高优先级任务之所以总是可运行,是因为其不会等待任何事情(不会进入阻塞态)。
(运行-挂起-阻塞)(三个基本的状态)
类似于上述提到的不需等待的任务只能被创建在最低优先级上,如果它们运行在其它任何优先级上,那么比它们优先级更低的任务将永远没有执行的机会。
改变任务优先级
void vTaskPrioritySet( xTaskHandle pxTask, unsigned portBASE_TYPE uxNewPriority );
4. 事件驱动
为了使我们的任务切实有用,我们需要通过某种方式来进行事件驱动。一个事件驱动任务只会在事件发生后触发工作(处理),而在事件没有发生时是不能进入运行态的。 调度器总是选择所有能够进入运行态的任务中具有最高优先级的任务。一个高优先级但不能够运行的任务意味着不会被调度器选中,而代之以另一个优先级虽然更低但能够运 行的任务。因此,采用事件驱动任务的意义就在于任务可以被创建在许多不同的优先级上,并且最高优先级任务不会把所有的低优先级任务饿死。
5.任务状态机
对一个想要周期执行的任务来说,可以使用delay()函数进行延时,但该方法缺点是无意义消耗CPU资源,因此可以调用vTaskDelay() API 函数来代替空循环。
void vTaskDelay( portTickType xTicksToDelay );
vTaskDelay() 是利用了任务进入阻塞态来实现延迟的。
void vTaskFunction( void *pvParameters ) {
char *pcTaskName;
/* The string to print out is passed in via the parameter. Cast this to a character pointer. */
pcTaskName = ( char * ) pvParameters;
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; ) {
/* Print out the name of this task. */
vPrintString( pcTaskName );
/* 延迟一个循环周期。调用vTaskDelay()以让任务在延迟期间保持在阻塞态。延迟时间以心跳周期为单位,常量portTICK_RATE_MS可以用来在毫秒和心跳周期之间相换转换。本例设定250毫秒的循环周 期。 */
vTaskDelay( 250 / portTICK_RATE_MS );
}
}
对比上述两图,图 4 展现的是当任务采用空循环进行延迟时的执行流程——结果就是任务总是可运行并占用了大量的机器周期。从图 9 中的执行流程中可以看到,任务在整个延迟周期内都处于阻塞态,==只在进行实际工作的时候才占用处理器时间(==本例中任务的实际工作只是简单地打印输出一条信息)。
注意:在没有任务使用CPU时,CPU会自动运行一个空闲任务。空闲任务可以获得的执行时间量,是系统处理能力裕量的一个度量指标。
vTaskDelayUntil() API 函数
void vTaskDelayUntil( portTickType * pxPreviousWakeTime, portTickType xTimeIncrement );
vTaskDelay() 的参数用来指定任务在调用 vTaskDelay()到切出阻塞态整个过程包含多少个心跳周期。 任务保持在阻塞态的时间量由 vTaskDelay()的入口参数指定,但任务离开阻塞态的时刻 实际上是相对于 vTaskDelay()被调用那一刻的。(相对时间)
vTaskDelayUntil()的参数就是用来指定任务离开阻塞态至进入就绪态那一刻的精确心跳计数值。API 函数 vTaskDelayUntil()可以用于实现一个固定执行周期的需求(当你需要让你的任务以固定频率周期性执行的时候)。
由于调用此函数的任务解除阻塞的时间是绝对时刻,比起相对于调用时刻的相对时间更精确(即比调用 vTaskDelay()可以实现更精确的周期性)。
void vTaskFunction( void *pvParameters ) {
char *pcTaskName;
portTickType xLastWakeTime;
/* The string to print out is passed in via the parameter. Cast this to a character pointer. */
pcTaskName = ( char * ) pvParameters;
/* 变量xLastWakeTime需要被初始化为当前心跳计数值。说明一下,这是该变量唯一一次被显式赋值。之后, xLastWakeTime将在函数
vTaskDelayUntil()中自动更新。 */
xLastWakeTime = xTaskGetTickCount();
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; ) {
/* Print out the name of this task. */ vPrintString( pcTaskName );
/* 本任务将精确的以250毫秒为周期执行。同vTaskDelay()函数一样,时间值是以心跳周期为单位的, 可以使用常量portTICK_RATE_MS将毫秒转换
为心跳周期。变量xLastWakeTime会在vTaskDelayUntil()中自动更新,因此不需要应用程序进行显示更新。 */
vTaskDelayUntil( &xLastWakeTime, ( 250 / portTICK_RATE_MS ) );
}
}
空闲任务与钩子函数
处理器总是需要代码来执行——所以至少要有一个任务处于运行态。
当调用 ==vTaskStartScheduler()==时,调度器会自动创建一个空闲任务。空闲任务是 一个非常短小的循环。空闲任务拥有最低优先级(优先级 0)以保证其不会妨碍具有更高优先级的应用任务 进入运行态。
通过空闲任务钩子函数(或称回调,hook, or call-back),可以直接在空闲任务中添 加应用程序相关的功能。
通常空闲任务钩子函数被用于:
- 执行低优先级,后台或需要不停处理的功能代码;
- 测试处系统处理裕量;
- 将处理器配置到低功耗模式。
空闲任务钩子函数必须遵从以下规则: - 绝不能阻或挂起。
- 如果应用程序用到了vTaskDelete() AP 函数,则空闲钩子函数必须能够尽快返回。
==注意: == FreeRTOSConfig.h 中的配置常量 configUSE_IDLE_HOOK 必须定义为 1,这样空 闲任务钩子函数才会被调用。
(需要功能的开启都需要去FreeRTOSConfig.h中配置,阅读源码的时候你会发现有很多#ifndef)