使用 RTOS 的实时应用程序可以被构建为一组独立的任务。每个任务在自己的上下文中执行,不依赖于系统内的其他任务或 RTOS 调度器本身。在任何时间点,应用程序中只能执行一个任务,实时 RTOS 调度器负责决定所要执行的任务。因此, RTOS 调度器可以在应用程序执行时重复启停每个任务(将任务调入或调出)。由于任务不了解 RTOS 调度器活动,因此实时 RTOS 调度器负责确保任务调入时的处理器上下文(寄存器值、堆栈内容等)与任务调出时的处理器上下文完全相同。为实现这一点,每个任务都分配有自己的堆栈。当任务调出时,执行上下文被保存到该任务的堆栈中,以便以后再调入相同的任务时可以准确地恢复其执行上下文。
任务状态
FreeRTOS中任务共有4中状态:
- 运行态:正在执行的任务,该任务就处于运行态。如果运行 RTOS 的处理器只有一个内核, 那么在任何给定时间内都只能有一个任务处于运行状态。
- 就绪态:准备就绪任务指那些能够执行(它们不处于阻塞或挂起状态), 但目前没有执行的任务, 因为同等或更高优先级的不同任务已经处于运行状态。
- 阻塞态:如果一个任务因延时或者等待外部事件发生,那么这个任务就处于阻塞态。 例如,如果一个任务调用vTaskDelay(),它将被阻塞(被置于阻塞状态), 直到延迟结束------一个时间事件。 任务也可以通过阻塞来等待队列、信号量、事件组、通知或信号量 事件。 处于阻塞状态的任务通常有一个"超时"期, 超时后任务将被超时,并被解除阻塞, 即使该任务所等待的事件没有发生。"阻塞"状态下的任务不使用任何处理时间,不能 被选择进入运行状态。
- 挂起态:类似暂停。与"阻塞"状态下的任务一样, "挂起"状态下的任务不能 被选择进入运行状态,但处于挂起状态的任务 没有超时。 相反,任务只有在分别通过 vTaskSuspend() 和 xTaskResume() API 调用明确命令时 才会进入或退出挂起状态。
任务调度
FreeRTOS一共支持三种任务调度方式:
- 抢占式调度:主要是针对优先级不同的任务,每一个任务都有一个优先级,优先级高的任务可以抢占优先级低的任务。
- 时间片调度:主要针对优先级相同的任务,当多个任务的优先级相同时,任务调度器会在每一次系统时钟节拍到的时候切换任务。
- 协程式调度:官方表示不再更新协程式调度。
"时间片" 是指调度器会在每个 tick 中断上在同等优先级任务之间进行切换, tick 中断之间的时间构成一个时间片。(tick 中断是 RTOS 用来衡量时间的周期性中断。)
总是运行优先级最高且可运行的任务的后果是, 永远不会进入"阻塞"或 "挂起"状态的高优先级任务会让所有任意执行时长的低优先级任务永久饥饿 。这就是为什么通常最好创建事件驱动型任务的原因之一 。例如,如果一个高优先级任务正在等待一个事件, 那么它就不应处于该事件的循环(轮询)中,因为如果处于轮询中,它会一直运行,永远不进入"阻塞"或"挂起"状态。 反之,该任务应进入"阻塞" 状态来等待事件。可以使用众多 FreeRTOS 任务间通信和同步原语之一将事件发送给任务。接收到 事件后, 优先级更高的任务会自动解除"阻塞"状态。高优先级任务处于"阻塞"状态时, 低优先级任务会运行。
动态创建任务和删除任务
c
#define configSUPPORT_DYNAMIC_ALLOCATION 1
上述宏定义配置为1的时候,才支持动态创建。
c
BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
const char * const pcName,
const configSTACK_DEPTH_TYPE uxStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *pxCreatedTask
);
- pvTaskCode:指向任务入口函数的指针(即实现任务的函数名称)。任务通常以无限循环的形式实现,实现任务的函数绝不能阐释返回或者退出。除非返回前自行删除。
- pcName:任务的描述性名称,可以用于获取任务句柄。
- uxStackDepth:要分配的用作任务堆栈的字数(不是字节数,STM32中一个字是4字节)。这个堆栈会自动从FreeRTOS堆中分配。
- pvParameters:作为参数传递给所创建任务的值。如果 pvParameters 设置为某变量的地址, 则在创建的任务执行时,该变量必须仍然存在, 因此,不能传递局部变量的地址。
- uxPriority :创建的任务将以该指定优先级执行,优先级默认上限为 (configMAX_PRIORITIES - 1)。该值越大表示优先级越高。
- pxCreatedTask:用于将句柄传递至由 xTaskCreate() 函数创建的任务。pxCreatedTask 是可选参数, 可设置为 NULL。
创建一项新任务的时候,会将其添加到就绪任务列表中。
c
void vTaskDelete( TaskHandle_t xTask );
- xTask:要删除的任务的句柄。如果传递 NULL,会删除调用任务。
从RTOS内核管理中删除任务时,要删除的任务将从所有就绪、阻塞、挂起和事件列表中移除。空闲任务负责释放由RTOS内核分配给已删除任务的内存。因此,如果应用程序调用了vTaskDelete,请务必确保空闲任务获取足够的微控制器处理时间。任务代码分配的内存不会自动释放,应在任务删除之前手动释放。
任务的挂起和恢复
c
void vTaskSuspend( TaskHandle_t xTaskToSuspend );
- xTaskToSuspend:被挂起的任务句柄。传递空句柄将导致调用任务被挂起。
挂起的任务将无法获取任何微控制器处理时间。对vTaskSuspend的调用不会累计计数,若在同一任务上调用多次,仍然仅需调用一次vTaskResume,即可恢复挂起的任务。
c
void vTaskResume( TaskHandle_t xTaskToResume );
BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume );
- xTaskToResume:待恢复任务的句柄。
- FromISR:带FromISR后缀的函数表示只能在中断中调用。
返回:
- 返回 pdTRUE ,需要进行任务上下文切换。(实际应该比较了任务优先级,如果恢复了优先级较高的任务,则返回pdTRUE)
- 返回 pdFALSE。不需要上下文切换。
c
portYIELD_FROM_ISR(x)
调用此函数进行任务上下文切换,传入参数不为pdFALSE时,就执行上下文切换函数。
注意:任务挂起就是相当于暂停,任务恢复就是继续执行。所以任务恢复的时候不是说在任务函数中从头开始。
其他
- 在FreeRTOS中PendSV和SysTick中断是设置为最低优先级的(优先级值为15),并且不可更改,这样做的目的是保证系统任务切换时不会阻塞系统其他中断的响应。PendSV中断函数中进行任务切换,SysTick中断函数中进行时基计数。
- 中断服务函数的优先级需在FreeRTOS所管理的范围内(默认设置为5~15,如果使用MX配置优先级的话,也只能在这个范围)。
- 在中断服务函数里边调用FreeRTOS的API函数,必须使用带"FromISR"后缀的函数。
临界段代码保护
临界段:也叫做临界区,是指那些必须完整运行,不能被打断的代码段。
适用场合如:
- 外设:需要严格按照时序初始化的外设,如i2c,spi等。
- 系统:系统自身需求。
- 用户:用户需求。
那么,什么东西可以打断当前的程序呢?有中断,有系统任务调度器。其实任务调度器也是在PendSV中断函数中调用的,所以如果关闭中断,就没有东西可以打断程序了。
本质:FreeRTOS在进入临界段代码的时候其实是关闭中断,当处理完临界段代码以后再打开中断。
c
void taskENTER_CRITICAL( void );
void taskEXIT_CRITICAL( void );
UBaseType_t taskENTER_CRITICAL_FROM_ISR( void );
void taskEXIT_CRITICAL_FROM_ISR( UBaseType_t uxSavedInterruptStatus );
- 需要成对使用
- 支持嵌套使用
- 尽量保持临界段代码耗时短
任务调度器的挂起和恢复
挂起任务调度器,调用此函数不需要关闭中断。
c
void vTaskSuspendAll( void );
BaseType_t xTaskResumeAll( void );
- 与临界区不一样的是,挂起任务调度器,未关闭中断。它仅仅是防止任务之间的资源争夺,中断照样可以直接响应。
- xTaskResumeAll仅恢复调度器,不会恢复之前通过调用vTaskSuspend而挂起的任务。