一、FreeRTOS 与裸机对比
为什么FreeRTOS要比裸机实时性好?
- 裸机:一般都是 main 函数里放个 while() ,无限循环,不管任务有多紧急,也要等轮到才执行。
- FreeRTOS:抢占式内核,优先执行高优先级任务,执行完毕后将 CPU 使用权交还给低优先级任务。
二、FreeRTOS 任务基础知识
1. 任务状态
- 运行态:当一个任务正在运行时,那么就说这个任务处于运行态,处于运行态的任务就是当前正在使用处理器的任务。如果使用的是单核处理器的话那么不管在任何时刻永远都只有一个任务处于运行态。
- 就绪态:处于就绪态的任务是那些已经准备就绪(这些任务没有被阻塞或者挂起),可以运行的任务,但是处于就绪态的任务还没有运行,因为有一个同优先级或者更高优先级的任务正在运行!
- 阻塞态:如果一个任务当前正在等待某个外部事件的话就说它处于阻塞态,比如说如果某个任务调用了函数 vTaskDelay() 的话就会进入阻塞态,直到延时周期完成。任务在等待队列、信号量、事件组、通知或互斥信号量的时候也会进入阻塞态。任务进入阻塞态会有一个超时时间,当超过这个超时时间任务就会退出阻塞态,即使所等待的事件还没有来临!
- 挂起态:像阻塞态一样,任务进入挂起态以后也不能被调度器调用进入运行态,但是进入挂起态的任务没有超时时间。任务进入和退出挂起态通过调用函数 vTaskSuspend() 和 xTaskResume() 。
2. 任务优先级
每个任务都可以分配一个从 0 - (configMAX_PRIORITIES-1) 的优先级,优先级数字越低表示任务的优先级越低,0 的优先级最低,configMAX_PRIORITIES-1 的优先级最高。空闲任务的优先级最低,为 0。
任务优先级和 STM32 中的中断优先级不同。中断的优先级永远高于任何任务的优先级,即任务在执行的过程中,中断来了就开始执行中断服务程序。区别:中断优先级的数值越小,优先级越高;任务优先级数值越小,优先级越低。
configUSE_PORT_OPTIMISED_TASK_SELECTION 设置为 1,configMAX_PRIORITIES 不能超过 32,其他情况可以为任意值。
configUSE_TIME_SLICING 定义为 1,允许多个任务共用 1 个优先级,此时处于就绪态的优先级相同的任务就会使用时间片轮转调度器获取运行时间。
3. 任务控制块
FreeRTOS 把每个任务都有的一些属性集合到一起用一个结构体表示,这个结构体就是任务控制块:TCB_t,当不使用某些功能的时候与其相关的变量就不参与编译,任务控制块大小就会进一 步的减小。裁剪以后再考虑。
4. 任务堆栈
因为是抢占式操作,所以肯定要恢复抢占之前的现场。FreeRTOS 之所以能正确的恢复一个任务的运行就是因为有任务堆栈在保驾护航,任务调度器在进行任务切换的时候会将当前任务的现场(CPU 寄存器值等)保存在此任务的任务堆栈中,等到此任务下次运行的时候就会先用堆栈中保存的值来恢复现场,恢复现场以后任务就会接着从上次中断的地方开始运行。
三、FreeRTOS 任务相关 API 函数
1. 任务创建和删除 API 函数
- xTaskCreate() 使用动态的方法创建一个任务。
- xTaskCreateStatic() 使用静态的方法创建一个任务。
- xTaskCreateRestricted() 创建一个使用 MPU 进行限制的任务,相关内存使用动态内存分配。
- vTaskDelete() 删除一个任务。
(1)函数 xTaskCreate() 使用动态的方法创建一个任务
创建任务时需要给任务指定堆栈,任务需要 RAM 来保存与任务有关的状态信息(任务控制块),使用这个函数创建任务的话这些所需要的 RAM 就会自动从 FreeRTOS 的堆中分配,因此必须提供内存管理文件,默认使用的是 Heap_.c 这个内存管理文件,而且 configSUPPORT_DYNAMIC_ALLOCATION 必须为 1。新创建的任务默认为就绪态,如果当前没有比它优先级高的任务会立即进入运行态,不管是任务调度器前还是后都可以创建任务。
函数原型如下:
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
const char* const pcName,
const uint16_t usStackDepth,
void* const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t* const pxCreatedTask )
参数:
pxTaskCode:任务函数。
pcName:任务名字,一般用于追踪和调试,任务名字长度不能超过 configMAX_TASK_NAME_LEN。
usStackDepth:任务堆栈大小,注意实际申请到的堆栈是 usStackDepth 的 4 倍。其中空闲任务的任务堆栈大小为 configMINIMAL_STACK_SIZE。
pvParameters:传递给任务函数的参数。
uxPriority:任务优先级,范围 0 - (configMAX_PRIORITIES-1)。
pxCreatedTask:任务句柄,任务创建成功以后会返回此任务的任务句柄,这个句柄其实就是任务的任务堆栈。此参数就用来保存这个任务句柄。其他 API 函数可能会使用到这个句柄。
返回值:
pdPASS:任务创建成功。
errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY:任务创建失败,因为堆内存不足!
(2)函数 xTaskCreateStatic() 使用静态的方法创建一个任务
函数原型如下:
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,
const char* const pcName,
const uint32_t ulStackDepth,
void* const pvParameters,
UBaseType_t uxPriority,
StackType_t* const puxStackBuffer,
StaticTask_t* const pxTaskBuffer )
参数:
pxTaskCode:任务函数。
pcName:任务名字,一般用于追踪和调试,任务名字长度不能超过 configMAX_TASK_NAME_LEN。
usStackDepth:任务堆栈大小,由于本函数是静态方法创建任务,所以任务堆栈由用户给出,一般是个数组,此参数就是这个数组的大小。
pvParameters:传递给任务函数的参数。
uxPriority:任务优先级,范围 0 - (configMAX_PRIORITIES-1)。
puxStackBuffer:任务堆栈,一般为数组,数组类型要为 StackType_t 类型。
pxTaskBuffer:任务控制块。
返回值:
NULL:任务创建失败,puxStackBuffer 或 pxTaskBuffer 为 NULL 的时候会导致这个错误的发生。
其他值:任务创建成功,返回任务的任务句柄。
(3)函数 xTaskCreateRestricted() 创建一个使用 MPU 进行限制的任务,相关内存使用动态内存分配
此函数也是用来创建任务的,只不过此函数要求所使用的 MCU 有 MPU(内存保护单元),用此函数创建的任务会受到 MPU 的保护。其他的功能和函数 xTaxkCreate() 一样。
函数原型如下:
BaseType_t xTaskCreateRestricted( const TaskParameters_t* const pxTaskDefinition,
TaskHandle_t* pxCreatedTask )
参数:
pxTaskDefinition:指向一个结构体 TaskParameters_t,这个结构体描述了任务的任务函数、堆栈大小、优先级等。此结构体在文件 task.h 中有定义。
pxCreatedTask:任务句柄。
返回值:
pdPASS:任务创建成功。
其他值:任务未创建成功,很有可能是因为 FreeRTOS 的堆太小了。
(4)函数 vTaskDelete() 删除一个任务
如果此任务是使用动态方法创建的,也就是使用函数 xTaskCreate() 创建的,那么在此任务被删除以后此任务之前申请的堆栈和控制块内存会在空闲任务中被释放掉,因此当调用函数 vTaskDelete() 删除任务以后必须给空闲任务一定的运行时间。只有那些由内核分配给任务的内存才会在任务被删除以后自动的释放掉,用户分配给任务的内存需要用户自行释放掉,比如某个任务中用户调用函数 pvPortMalloc() 分配了 500 字节的内存,那么在此任务被删除以后用户也必须调用函数 vPortFree() 将这 500 字节的内存释放掉,否则会导致内存泄露。
内存泄露:使用完的内存空间没有归还系统,使这段内存一直处于被占用的状态,一次问题不大,一次次累计最终会导致内存空间占完。
函数原型如下:
vTaskDelete( TaskHandle_t xTaskToDelete )
参数:
xTaskToDelete:要删除的任务的任务句柄。
返回值:无。
2. 任务挂起和恢复 API 函数
- vTaskSuspend() 挂起一个任务。
- vTaskResume() 恢复一个任务的运行。
- xTaskResumeFromISR() 中断服务函数中恢复一个任务的运行。
(1)函数 vTaskSuspend() 挂起一个任务
将任务暂停,可以重新恢复且任务中保存的变量值不会丢失。此函数用于将某个任务设置为挂起态,进入挂起态的任务永远都不会进入运行态。退出挂起态的唯一方法就是调用任务恢复函数 vTaskResume() 或 xTaskResumeFromISR() 。
函数原型如下:
void vTaskSuspend( TaskHandle_t xTaskToSuspend )
参数:
xTaskToSuspend:要挂起的任务的任务句柄,创建任务的时候会为每个任务分配一个任务句柄。如果使用函数 xTaskCreate() 创建任务的话那么函数的参数 pxCreatedTask 就是此任务的任务句柄,如果使用函数 xTaskCreateStatic() 创建任务的话那么函数的返回值就是此任务的任务句柄。也可以通过函数 xTaskGetHandle() 来根据任务名字来获取某个任务的任务句柄。注意!如果参数为 NULL 的话表示挂起任务自己,所以调用之前最好检查一下句柄。
返回值:无。
(2)函数 vTaskResume() 恢复一个任务的运行
将一个任务从挂起态恢复到就绪态,只有通过函数 vTaskSuspend() 设置为挂起态的任务才可以使用 vTaskRexume() 恢复!
函数原型如下:
void vTaskResume( TaskHandle_t xTaskToResume )
参数:
xTaskToResume:要恢复的任务的任务句柄。
返回值:无。
(3)函数 xTaskResumeFromISR() 中断服务函数中恢复一个任务的运行
此函数是 vTaskResume() 的中断版本,用于在中断服务函数中恢复一个任务。
函数原型如下:
BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume )
参数:
xTaskToResume:要恢复的任务的任务句柄。
返回值:
pdTRUE:恢复运行的任务的任务优先级等于或者高于正在运行的任务(被中断打断的任务),这意味着在退出中断服务函数以后必须进行一次上下文切换。
pdFALSE:恢复运行的任务的任务优先级低于当前正在运行的任务(被中断打断的任务),这意味着在退出中断服务函数的以后不需要进行上下文切换。
四、任务调度后的执行顺序
第一次执行:在每个任务第一次执行时,会从函数开头开始执行。
第二次及以后执行:从 while 执行,执行 while 中的代码。