FreeRTOS官网:www.freertos.org
调度
实时操作系统
特点:如果有一个任务需要执行,实时操作系统会马上(在较短时间内)执行该任务,不会有较长的延时。这种特性保证了各个任务的及时执行。
实现方式:实时操作系统中都要包含一个实时任务调度器,这个任务调度器与其它操作系统的最大不同是强调:严格按照优先级来分配CPU时间,并且时间片轮转不是实时调度器的一个必选项。
freeRTOS的调度模式
支持多任务运行的实时操作系统,具有时间片、抢占式和合作式三种调度方式。
- 合作式调度,主要用在资源有限的设备上面,现在已经很少使用了。出于这个原因,后面的FreeRTOS版本中不会将合作式调度删除掉,但也不会再进行升级了。
- 抢占式调度,每个任务都有不同的优先级,任务会一直运行直到被高优先级任务抢占或者遇到阻塞式的 API 函数,比如
vTaskDelay
。 - 时间片调度,每个任务都有相同的优先级,任务会运行固定的时间片个数或者遇到阻塞式的 API函数,比如
vTaskDelay
,才会执行同优先级任务之间的任务切换。
当 FreeRTOS 多任务启动执行后,基本会按照如下的方式去执行:
-
首先执行的最高优先级的任务 Task1,Task1 会一直运行直到遇到系统阻塞式的 API 函数,比如延迟、事件标志等待、信号量等待,Task1 任务会被挂起,也就是释放 CPU 的执行权,让低优先级的任务得到执行。
-
FreeRTOS 操作系统继续执行任务就绪列表中下一个最高优先级的任务 Task2,Task2 执行过程中有两种情况:
-
Task1由于延迟时间到,接收到信号量消息等方面的原因,使得 Task1从挂起状态恢复到就绪态,在抢占式调度器的作用下,Task2的执行会被 Task1 抢占。
-
Task2 会一直运行直到遇到系统阻塞式的 API函数,比如延迟,事件标志等待,信号量等待,Task2任务会被挂起,继而执行就绪列表中下 一个最高优先级的任务。
-
-
如果用户创建了多个任务并且采用抢占式调度器的话,基本都是按照上面两条来执行。 根据抢占式调度器,当前的任务要么被高优先级任务抢占,要么通过调用阻塞式 API 来释放 CPU 使用权让低优先级任务执行,没有用户任务执行时就执行空闲任务
任务
特点:
- 操作简单,没有使用限制
- 支持完全抢占机制
- 完全按优先顺序排列
- 每个人物保留自己的堆栈,从而提高RAM的使用率
- 如果使用抢占式机制,必须考虑重入问题
任务创建
每个任务都需要 RAM 的堆栈来保存任务状态,如果使用 xTaskCreate 创建任务,则所需的 RAM 将自动 从 FreeRTOS 堆中分配。 如果创建任务 使用了 xTaskCreateStatic(),则 RAM 由应用程序编写者提供,因此可以在编译时进行静态分配。 请参阅 静态分配 Vs 动态分配 页面,了解更多信息。
任务状态
- 运行:当任务实际执行时,它被称为处于运行状态。 任务当前正在使用处理器。 如果运行 RTOS 的处理器只有一个内核, 那么在任何给定时间内都只能有一个任务处于运行状态。
- 准备就绪:准备就绪任务指那些能够执行(它们不处于阻塞或挂起状态), 但目前没有执行的任务, 因为同等或更高优先级的不同任务已经处于运行状态。
- 阻塞:如果任务当前正在等待时间或外部事件,则该任务被认为处于阻塞状态,且不能被选择进入运行状态。 例如,如果一个任务调用
vTaskDelay()
,它将被阻塞(被置于阻塞状态),直到延迟结束或者一个时间事件才被放回就绪态。 任务可通过阻塞来等待队列、信号量、事件组、通知或信号量事件。并且处于阻塞状态的任务通常有一个超时期, 事件等待超时后任务将被解除阻塞。 - 挂起:与“阻塞”状态下的任务一样, “挂起”状态下的任务不能被选择进入运行状态,但处于挂起状态的任务没有超时概念。相反,任务只有在分别通过
vTaskSuspend()
和xTaskResume()
API调用才会进入或退出挂起状态。
任务调度
RTOS的调度方法针对 单核、非对称多核 (AMP)、和对称多核 (SMP) 有不同设置。以此来决定哪个 RTOS 任务应处于运行状态。且每个处理器核心只能有一个任务处于运行状态。在 AMP 中, 每个处理器核心运行自身的 FreeRTOS 实例。在 SMP 中, 存在一个 FreeRTOS 实例,可以跨多核调度 RTOS 任务 。
单核
FreeRTOS 默认使用固定优先级的抢占式调度策略,对同等优先级的任务执行时间片轮询调度:
- 固定优先级:是指调度器不会永久更改任务的优先级, 尽管它可能会因优先级继承而暂时提高任务的优先级,因此要避免任务饥饿。
- 抢占式调度:是指调度器始终运行优先级最高且可运行的 RTOS 任务, 无论任务何时能够运行。例如, 如果中断服务程序 (ISR) 更改了优先级最高且可运行的任务, 调度器会停止当前正在运行的低优先级任务并启动高优先级任务(即使这发生在同一个时间片内 )。此时高优先级任务 “抢占”了低优先级任务。
- 轮询调度:是指具有相同优先级的任务轮流进入运行状态
- **时间片:**是指调度器会在每个 tick 中断上在同等优先级任务之间进行切换, tick 中断之间的时间构成一个时间片。tick 中断是 RTOS 用来衡量时间的周期性中断。
配置:
-
configUSE_PREEMPTION
如果 configUSE_PREEMPTION 设置为 0,则关闭“抢占”, 只有当运行状态的任务进入“阻塞”或“挂起”状态, 或运行状态任务调用
taskYIELD()
, 或中断服务程序 (ISR) 手动请求上下文切换时,才会发生上下文切换。 -
configUSE_TIME_SLICING
如果 configUSE_TIME_SLICING 设置为 0,则表示关闭时间切片, 因此调度器不会在每个 tick 中断上在同等优先级的任务之间切换
非对称多核 (AMP)
多核设备的每个核心都单独运行自己的 FreeRTOS 实例。这些 核心并不都需要具有相同架构, 但如果 FreeRTOS 实例之间需要进行通信,则需要共享一些内存。
对称多核 (SMP)
一个 FreeRTOS 实例可以跨多个处理器核心调度 RTOS 任务,由于 只有一个 FreeRTOS 实例在运行,一次只能使用 FreeRTOS 的一个端口, 因此每个核心必须具有相同的处理器架构并共用相同的内存空间。
- 任何给定时间都会导致多个任务处于运行状态
- 只有缺乏可运行的高优先级任务时,才会运行低优先级任务的假设不再成立
配置:
-
configRUN_MULTIPLE_PRIORITIES
将
configRUN_MULTIPLE_PRIORITIES
设置为 0,则调度器只会同时运行具有相同优先级的多个任务。这可能会修复基于“一次只运行一个任务”这一假设编写的代码, 但这就无法享受到 SMP 配置带来的一些好处。 -
configUSE_CORE_AFFINITY
如果在 中将
FreeRTOSConfig.hconfigUSE_CORE_AFFINITY
设置为 1, 则vTaskCoreAffinitySet()
API 函数可用于定义某个任务可以在哪些核心上运行 以及不可以在哪些核心上运行。使用该方法,应用程序编写者可以防止问题:同时执行假设了自身执行顺序的两个任务 。
任务优先级
在FreeRTOS中,数字优先级越小,逻辑优先级也越小,且优先级通过 就绪列表 实现,利用pxCurrenTCB
全局的TCB指针,用于指向优先级最高的就绪任务的TCB,即可实现任务优先级调度
就绪列表:pxReadyTasksLists[ configMAX_PRIORITIES ]
里面存的是就绪任务的TCB(准确来 说是TCB里面的xStateListItem节点)
- 数组的下标对应任务的优先级
- 每一个元素中存放的是任务链表,如2优先级中有两个任务构成的一个链表
优先级范围
每个任务均被分配了从 0
到 configMAX_PRIORITIES - 1
的优先级,
configMAX_PRIORITIES
在FreeRTOSConfig.h
中定义。configMAX_PRIORITIES
可以采取合理范围内的值,但出于 RAM 使用效率的原因,应保持在实际需要的最小值。
查找最高优先级的就绪任务
就绪态最高优先级的任务存储在 uxTopReadyPriority
中,默认初始化为0
/* 空闲任务优先级宏定义,在task.h中定义 */
#define tskIDLE_PRIORITY ( ( UBaseType_t ) 0U )
/* 定义uxTopReadyPriority,在task.c中定义 */
staticvolatile UBaseType_t uxTopReadyPriority = tskIDLE_PRIORITY;
而获取 uxTopReadyPriority
有两种方法,通过宏 configUSE_PORT_OPTIMISED_TASK_SELECTION
控制
- 定义为0:选择通用方法
- 定义为1:选择根据处理器优化的方法
默认在portmacro.h中定义为 configUSE_PORT_OPTIMISED_TASK_SELECTION = 1
,即使用优化过的方法
1. 通用方法
在通用方法中 uxTopReadyPriority
定义为就绪任务中的最高优先级
- 新的就绪态任务加入: 使用
taskRECORD_READY_PRIORITY()
更新uxTopReadyPriority
的值,只有当 优先级比最高就绪优先级高 时才更新
taskRECORD_READY_PRIORITY( uxPriority ){
if( ( uxPriority ) > uxTopReadyPriority ){
uxTopReadyPriority = ( uxPriority );
}
}
- 选择就绪态任务加入运行态: 使用
taskSELECT_HIGHEST_PRIORITY_TASK()
用于寻找下一个优先级最高的就绪任务,即更新uxTopReadyPriority
和pxCurrentTCB
的值,其中寻找优先级方法通过遍历实现
taskSELECT_HIGHEST_PRIORITY_TASK(){
UBaseType_t uxTopPriority = uxTopReadyPriority;
/* 遍历寻找包含就绪任务的最高优先级的队列 */
while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) ){
--uxTopPriority;
}
/* 获取优先级最高的就绪任务的TCB,然后更新到pxCurrentTCB */
listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, &(pxReadyTasksLists[ uxTopPriority ]));
/* 更新uxTopReadyPriority */
uxTopReadyPriority = uxTopPriority;
}
2. 优化方法
优化方法中uxTopReadyPriority
的每个位号对应的是任务的优先级(优先级位图表),因此计算最高优先级的编号只需要计算 uxTopReadyPriority
的前导零(clz
)即可,由于一个变量大多数使用32位保存,因此此方法中 configMAX_PRIORITIES
不得大于32
- clz: 计算一个变量从高位开始 第一次出现1的位的前面的零的个数
如图所示,一个32位的 变量 uxTopReadyPriority
,其位0、位24和位25均置1,其余位为0,因此前导零的个数为6,就绪任务中的最高优先级为:( 31UL - (uint32_t ) __clz( ( uxReadyPriorities ) ) ) = ( 31UL - ( uint32_t ) 6 ) = 25
。
- 新的就绪态任务加入/就绪态任务加入运行态后: 使用
taskRECORD_READY_PRIORITY
和taskRESET_READY_PRIORITY
来维护优先级位图表
taskRECORD_READY_PRIORITY( uxPriority ){
portRECORD_READY_PRIORITY( uxPriority, uxTopReadyPriority )
}
portRECORD_READY_PRIORITY( uxPriority, uxReadyPriorities ){
( uxReadyPriorities ) |= ( 1UL << ( uxPriority ) ) // uxPriority位置1
}
taskRESET_READY_PRIORITY( uxPriority ){
portRESET_READY_PRIORITY((uxPriority ), (uxTopReadyPriority));
}
portRESET_READY_PRIORITY( uxPriority, uxReadyPriorities ){
( uxReadyPriorities ) &= ~( 1UL << ( uxPriority ) ) // uxPriority清0
}
- 选择就绪态任务加入运行态:使用
taskSELECT_HIGHEST_PRIORITY_TASK()
用于寻找下一个优先级最高的就绪任务,即更新uxTopReadyPriority
和pxCurrentTCB
的值,其中寻找优先级方法通过计算前导零实现,如下所示:
taskSELECT_HIGHEST_PRIORITY_TASK(){
UBaseType_t uxTopPriority;
/* 寻找最高优先级 */
portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );
/* 获取优先级最高的就绪任务的TCB,然后更新到pxCurrentTCB */
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );
}
portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ){
uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )
}
时间片调度
FreeRTOS支持时间片功能,实现在同一个优先级下可以有多个任务, 每个任务轮流地享有相同的CPU时间,其中最小的时间单位为一个tick
实现时间片调度的算法已经在 通用方法 和 优化方法 中函数 listGET_OWNER_OF_NEXT_ENTRY
有所体现
listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList ){
List_t * const pxConstList = ( pxList );
/* 节点索引指向链表第一个节点调整节点索引指针,指向下一个节点,
如果当前链表有N个节点,当第N次调用该函数时,pxIndex则指向第N个节点 */
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;
/* 当遍历完链表后,pxIndex回指到根节点 */
if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) ){
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;
}
/* 获取节点的OWNER,即TCB */
( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;
}
-
并不是获取链表下的第一个节点的pvOWNER,而是用于获取下一个节点的pvOWNER
-
pxReadyTasksLists
每一个元素实际上是一个环形链表的首地址,每次调用listGET_OWNER_OF_NEXT_ENTRY
只需要寻找下一个 pvOWNER 即可
空闲任务
RTOS 调度器启动时,自动创建空闲任务,以确保始终存在一个能够运行的任务。 它以优先级0创建,以确保如果有更高的优先级应用程序任务处于就绪态,则不使用任何 CPU 时间。其他应用程序任务可以共享空闲任务优先级 (tskIDLE_PRIORITY)。共享方式与 configIDLE_SHOULD_YIELD 参数配置有关
功能:负责释放 RTOS 分配给被删除的任务的内存,因此使用 vTaskDelete() 函数时需要确保空闲任务能够被执行
空闲任务Hook函数
空闲任务钩子是在空闲任务中调用的函数。 通常使用空闲钩子函数将微控制器 CPU 置于节能模式。
创建方式:
- 将 configUSE_IDLE_HOOK 设置为 1
- 定义具有以下名称和原型的函数:
void vApplicationIdleHook( void )
配置:
-
configIDLE_SHOULD_YIELD
-
此参数控制空闲优先级任务的行为。 它仅在以下情况下有效:
- 使用抢占式调度器。
- 应用程序存在共享空闲任务优先级的任务。
-
若设置为 1:其他空闲优先级任务准备运行,空闲任务将立即挂起(等价于被抢占)。 这确保 当有应用程序任务可调度时,空闲任务花费的时间最少
-
若设置为 0:会阻止空闲任务让出处理时间,直到其时间片结束。 这可以确保所有空闲优先级任务 分配相同的处理时间,但这是以分配给空闲任务更高比例的处理时间为代价的
-
设置为 1存在问题:相同空闲优先级的任务分配时间资源不同
-
任务 A、B 和 C 是应用程序任务,任务 I 是空闲任务,都为空闲优先级任务。 当空闲任务挂起,任务 A 开始执行,但当前时间片已经被空闲任务消耗了一部分,导致**任务 I 和任务 A 共用同一个时间片**,因此**任务 B 和 C 获得的处理时间多于任务 A**
**避免方法**:
1. 使用[空闲钩子](#空闲任务钩子)代替单独的空闲优先级任务
2. 以高于空闲的优先级创建所有应用程序任务
3. 将 `configIDLE_SHOULD_YIELD` 设置为 0
- configUSE_IDLE_HOOK: 将其设置为 1,使用空闲钩子, 或设置为 0,忽略空闲钩子。
API
TaskHandle_t (type)
任务引用的类型。例如,调用 xTaskCreate
(通过指针参数)返回 TaskHandle_t
变量,然后可以将该变量用作 vTaskDelete
的参数来删除任务。
xTaskCreate()
创建一个新任务并将其添加到准备运行的任务列表中。 configSUPPORT_DYNAMIC_ALLOCATION
必须在 FreeRTOSConfig.h 中被设置为 1,才能使用此 RTOS API 函数。
BaseType_t xTaskCreate(TaskFunction_t pvTaskCode,
const char * const pcName,
configSTACK_DEPTH_TYPE usStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *pxCreatedTask);
参数 | 含义 |
---|---|
pvTaskCode | 指向任务入口函数的指针。任务通常以无限循环的形式实现;实现任务的函数决不能试图返回或退出。 |
pcName | 任务的描述性名称,可用于获取任务句柄。任务名称最大长度由 configMAX_TASK_NAME_LEN 定义。 |
usStackDepth | 分配用于任务堆栈的字数(不是字节)。堆栈深度与堆栈宽度的乘积不得超过 size_t 类型变量所能包含的最大值。堆栈应有多大? |
pvParameters | 传递给任务的参数。如果 pvParameters 设置为变量的地址, 则在执行创建的任务时该变量必须仍然存在——因此 传递堆栈变量的地址是无效的。 |
uxPriority | 创建任务执行的优先级 |
pxCreatedTask | 用于将句柄传递至由 xTaskCreate() 函数创建的任务 。 pxCreatedTask 是可选的,可设置为 NULL。 |
xTaskCreateStatic()
创建一个新任务并将其添加到准备运行的任务列表中。 configSUPPORT_STATIC_ALLOCATION
必须在 FreeRTOSConfig.h 中被设置为 1,才能使用此 RTOS API 函数。
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 );
参数 | 含义 |
---|---|
ulStackDepth | puxStackBuffer 参数用于将 StackType_t 变量数组传递给 xTaskCreateStatic() 。必须将 ulStackDepth 设置为数组中的索引数。 |
puxStackBuffer | 必须指向至少具有 ulStackDepth 索引的 StackType_t 数组(请参阅上面的 ulStackDepth 参数),该数组用作任务的堆栈,因此必须是永久性的(而不是在函数的堆栈上声明)。 |
pxTaskBuffer | 必须指向 StaticTask_t 类型的变量。该变量用于保存新任务的数据结构体 (TCB) ,因此必须是持久的(而不是在函数的堆栈中声明)。 |
vTaskDelete()
此函数的作用为从 RTOS 内核管理中移除任务。被删除的任务将从所有的就绪、阻塞、挂起和事件的列表中移除。INCLUDE_vTaskDelete
必须定义为 1 才能使用此函数。在使用之后应该注意空闲任务能够获得运行时间 以释放堆栈空间
void vTaskDelete( TaskHandle_t xTask );
参数 | 含义 |
---|---|
xTask | 待删除的任务的句柄。传递 NULL 将导致调用任务被删除。 |
协程
协程仅用于 RAM 严重受限的极小处理器, 通常不会用于 32 位微控制器。与任务之间区别如下:
- 堆栈:协程是没有堆栈分配的,是所有创建的协程共同使用一个堆栈空间,这相比于任务来说,减少了RAM的使用空间。
- 调度和优先级: 协程使用协同调度,但是可以包含在使用的抢占优先级之中。
- 宏定义:协程例程实现是通过一组宏提供的。
- 条件限制:RAM使用量的减少是以在如何构建协程方面的一些严格限制为代价的。
协程状态
- 运行:当协程实际执行时,它被称为处于运行状态。 协程当前正在使用处理器。
- 就绪:就绪的协程是那些能够执行(未阻塞)但目前未执行的协程。 协程处于就绪状态的可能情况包括:
- 另一个具有相同或更高优先级的协程已处于运行状态,或
- 任务处于运行状态——只有在应用程序同时使用任务和协程时才会出现这种情况。
- 阻塞:如果协程当前正在等待时间事件或外部事件,则该协程被称为处于阻塞状态。 例如,如果协程调用 crDELAY(),它将阻塞(被置于阻塞状态),直到延迟期结束(即时间事件)。 阻塞的协程不可用于调度。
协程优先级
每个协程分配的优先级从 0 到 configMAX_CO_ROUTINE_PRIORITIES - 1
- 低优先级的数字表示低优先级的协程
- 协程优先级只与其他协程相关。若将任务和协程混合在一起,则任务优先级将始终优先于协程
协程实现
调用 xCoRoutineCreate()
即可创建 CoRoutineHandle_t
类型的协程。
void vACoRoutineFunction( CoRoutineHandle_t xHandle, UBaseType_t uxIndex ){
crSTART( xHandle );
for( ;; ){
-- Co-routine application code here. --
}
crEND();
}
注意事项:
- 所有协程函数都必须以调用
crSTART()
开始。 - 所有协程函数都必须以调用
crEND()
结束。 - 协程函数不应返回任何值,因此通常实现为连续循环。
- 可通过单个协程函数创建多个协程。 提供的 uxIndex 参数可用于区分 协程之间传递数据的信息。
协程调度
协程是通过重复调用 vCoRoutineSchedule()
来调度的。最好是在空闲任务钩子中调用 vCoRoutineSchedule()
。优点是:即使应用程序只使用协程(没有任务)也会创建协程,因为空闲任务仍将在调度程序启动时自动创建。
void vApplicationIdleHook( void ){
vCoRoutineSchedule( void );
}
如果空闲任务没有执行任何其他函数,那按以下方式在循环中调用 vCoRoutineSchedule()
效率会更高(因为没有任务,因此不需要释放已删除任务的内存):
void vApplicationIdleHook( void ){
for( ;; ){
vCoRoutineSchedule( void );
}
}
限制
与任务相比,协程的优势是使用的 RAM 更少,但代价是使用协程时存在一些限制条件,导致限制性更强,使用起来也更复杂。
共享堆栈
当协程阻塞时,协程的堆栈无法维持。 这意味着在堆栈上分配的变量很可能丢失。 要克服这一问题,必须将一个必须在阻塞调用中保持其值的变量声明为静态 。 例如:
void vACoRoutineFunction( CoRoutineHandle_t xHandle, UBaseType_t uxIndex ){
static char c = 'a';
crSTART( xHandle );
for( ;; ){
// If we set c to equal 'b' here ...
c = 'b';
// ... then make a blocking call ...
crDELAY( xHandle, 10 );
// ... c will only be guaranteed to still
// equal 'b' here if it is declared static
}
crEND();
}
堆栈共用的另一种结果是,可能导致协程阻塞API函数 crDELAY()
的调用只能来自于协程函数本身,不能在协程调用的其他函数 vACalledFunction
() 中实现。 例如:
void vACoRoutineFunction( CoRoutineHandle_t xHandle, UBaseType_t uxIndex ){
crSTART( xHandle );
for( ;; ){
// It is fine to make a blocking call here,
crDELAY( xHandle, 10 );
// but a blocking call cannot be made from within vACalledFunction().
vACalledFunction();
}
crEND();
}
void vACalledFunction( void ){
// Cannot make a blocking call here!
}
switch 语句使用
不允许从switch语句中进行阻塞调用,例如:
void vACoRoutineFunction( CoRoutineHandle_t xHandle, UBaseType_t uxIndex ){
crSTART( xHandle );
for( ;; ){
// It is fine to make a blocking call here,
crDELAY( xHandle, 10 );
switch( aVariable ){
case 1 : // Cannot make a blocking call here!
break;
default: // Or here!
}
}
crEND();
}
流缓冲区& 消息缓冲区
流缓冲区
功能:将字节流从中断服务程序传递到任务, 或者从一个任务传递到另一个任务
- 字节流可以是任意长度, 且并不一定具有开头或结尾
- 可以一次写入或读取任意数量的字节
- 数据通过拷贝传递,发送方将数据复制到缓冲区中, 读取方将数据复制出缓冲区
阻塞读取和触发级别
- xStreamBufferReceive() 用于读取 来自 RTOS 任务的流缓冲区的数据。
- xStreamBufferReceiveFromISR() 用于 从中断服务程序 (ISR) 的流缓冲区中读出数据。
如果在任务读取时流缓冲区为空,且指定了非零阻塞时间, 则该任务将进入阻塞态 ,直到流缓冲区中有指定数量的数据可用, 或者阻塞时间到期。 其中流缓冲区中指定数量的数据量称为流缓冲区的触发等级。 例如:如果任务在读取触发级别为 N 的空流缓冲区时被阻塞, 则在流缓冲区至少包含 N 个字节或任务的阻塞时间到期之前, 该任务将不会被解除阻塞。
注意事项:
- 将触发器级不能设置为 0。 若尝试设置触发等级为 0 将导致使用 1 触发等级。
- 指定的触发级不能大于流缓冲区的大小。
阻塞写入
- xStreamBufferSend() 用于 将数据从 RTOS 任务发送到流缓冲区。
- xStreamBufferSendFromISR() 用于 将数据从中断服务程序 (ISR) 发送到流缓冲区。
如果任务写入时流缓冲区已满,且指定了非零阻塞时间,则该任务将进入阻塞态 ,直到流缓冲区中出现可用空间,或者 阻塞时间到期。
消息缓冲区
消息缓冲区构建在流缓冲区之上与 流缓冲区 作用类似,区别在于:长度为 N 个字节的消息只能作为 N 个字节的消息读取,而不能以单独的字节读取
阻塞读取和写入
- xMessageBufferReceive()用于 从 RTOS 任务的消息缓冲区读取数据
- xMessageBufferReceiveFromISR()用于 从中断服务程序 (ISR) 的消息缓冲区读取数据。
- xMessageBufferSend())用于将数据发送到 RTOS 任务的消息缓冲区
- xMessageBufferSendFromISR())用于 将数据从中断服务程序 (ISR) 发送到消息缓冲区
如果任务使用 xMessageBufferReceive() 从消息缓冲区读取数据, 而该消息缓冲区正好是空的,此时如果指定了非零的阻塞时间,则任务将进入阻塞态,直到消息缓冲区中的任一数据变得可用,或超过阻塞时间。
如果任务使用 xMessageBufferSend() 将消息写入消息缓冲区, 而该消息缓冲区正好已满,此时如果指定了非零的阻塞时间,则任务将进入阻塞态,直到消息缓冲区中的任一空间变得可用,或超过阻塞时间。
消息缓冲区中消息的大小
在将消息写入消息缓冲区之前,需将每条消息的长度 configMESSAGE_BUFFER_LENGTH_TYPE
先写入消息缓冲区(通过 FreeRTOS API 函数在内部完成),默认 configMESSAGE_BUFFER_LENGTH_TYPE
为 size_t
类型,在 32 位架构上,size_t
通常为 4 个字节,因此每条消息都会多存储4个字节做为消息的长度。
例如:当 configMESSAGE_BUFFER_LENGTH_TYPE
为 4 个字节时, 将 10 字节长度的消息写入消息缓冲区实际上会消耗 14 字节的缓冲区空间。 同理,将一条 100 字节长度的消息 写入消息缓冲区实际上将占用 104 字节的缓冲区空间。
Hook 函数
流缓冲区和消息缓冲区在每次发送和接收操作完成后执行回调:
- 使用
xStreamBufferCreate()
和xMessageBufferCreate()
API 函数 (及其静态分配的等效函数)创建的流缓冲区和消息缓冲区共享相同的回调函数,可使用sbSEND_COMPLETED()
和sbRECEIVE_COMPLETED()
宏定义这些回调函数。 - 使用
xStreamBufferCreateWithCallback()
和xMessageBufferCreateWithCallback()
API 函数 (及其静态分配的等效函数)创建的流缓冲区和消息缓冲区各自具有各自的回调函数。
同步
在FreeRTOS中所有的通信和同步机制都是基于队列(FIFO)实现。但是也可以使用 LIFO 的存储缓冲,也就是后进先出,FreeRTOS 中的队列也提供了 LIFO 的存储缓冲机制
可以使用 任务通知 来实现队列、事件组或信号量的轻量级替代方案
队列
队列不是属于某个特别指定的任务的,任何任务都可以向队列中发送消息,或者从队列中提取消息。
- 出队阻塞:当一个任务试图从一个空队列中读取时,该队列将 进入阻塞状态(因此它不会消耗任何 CPU 时间,且其他任务可以运行) 直到队列中的数据变得可用,或者阻塞时间过期。
- 入队阻塞:当一个任务试图写入到一个空队列时,该队列将 进入阻塞状态(因此它不会消耗任何 CPU 时间,且其他任务可以运行) 直到队列中出现可用空间,或者阻塞时间过期。
实现方式
- 复制队列(Queue by copy) 表示写入队列的数据每个字节都被完整复制到队列中了
- 引用队列(Queue by reference) 表示写入队列的是要写入数据的引用(或者说是一个指针指向所引用的数据)而并不是数据本身
FreeRTOS采用是复制队列的实现方式,有如下优势:
- 有些储存在栈上的变量在函数运行结束后是将会被销毁的,如果采用引用队列的话引用会失效
- 发送数据的任务可以重复使用变量,采用引用队列的话每发送一个数据需要一个新的变量
- 发送队列数据的任务和接受队列数据的任务之间是没有耦合的,两者互相不影响
- 复制队列可以通过储存指向数据块的指针实现引用队列的功能
API
xQueueCreate()
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize )
xQueueCreate()
是创建队列用到的函数。函数的返回值是 QueueHandle_t
具柄类型,表示的是对所创建队列的一个引用句柄。FreeRTOS从FreeRTOS的堆中指定一些内存空间给队列使用。如果堆中没有足够空间给队列使用的话函数的返回值会是 NULL
。
函数的几个参数介绍如下
- uxQueueLength 队列包含数据的最大长度
- uxItemSize 每个数据占用的字节大小
xQueueSend()
xQueueSend()
函数用于将数据发送到队列(具体一点就是队列的尾部)。如果要在中断程序调用的话需要使用xQueueSendFromISR()
函数。函数的几个参数介绍如下
- xQueue:队列的具柄,来自于**xQueueCreate()**的返回值
- pvItemToQueue:所发送数据的引用,然后这些数据会被复制到队列中
- xTicksToWait:队列如果满时发送任务的阻塞时间(block time),上文已经介绍过,可以通过pdMS_TO_TICKS()把时间转换成节拍数。如果设置为portMAX_DELAY的话任务将永远等待下去(需要FreeRTOSConfig.h头文件中INCLUDE_vTaskSuspend参数设置为1)
- 返回值:发送数据成功时返回pdPASS,失败时返回errQUEUE_FULL
下面两个函数用于明确指定发送数据到队列的头部还是尾部。
BaseType_t xQueueSendToFront( QueueHandle_t xQueue, const void * pvItemToQueue,
TickType_t xTicksToWait )
BaseType_t xQueueSendToBack( QueueHandle_t xQueue, const void * pvItemToQueue,
TickType_t xTicksToWait )
xQueueSend()
函数和 xQueueSendToBack()
函数本质上是一样的,可以在下图 xQueueSend()
的宏定义中queueSEND_TO_BACK
看到
#define xQueueSend(xQueue, pvItemToQueue, xTicksToWait)\
xQueueSendToBack((xQueue), (pvItemToQueue), (xTicksToWait), queueSEND_TO_BACK)
xQueueReceive()
xQueueReceive()
函数用于从队列中读取数据,同时读取到的数据会被从队列中移除。函数的几个参数介绍如下:
BaseType_t xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer,
TickType_t xTicksToWait )
- xQueue:队列的具柄,来自于**xQueueCreate()**的返回值
- pvBuffer:指向内存空间的一个引用,读取的数据会被复制到这片内存
- xTicksToWait:队列如果空时接送任务的阻塞时间(block time),上文已经介绍过,可以通过pdMS_TO_TICKS()把时间转换成节拍数。如果设置为portMAX_DELAY的话任务将永远等待下去(需要FreeRTOSConfig.h头文件中INCLUDE_vTaskSuspend参数设置为1)
- 返回值:接收数据成功时返回pdPASS,失败时返回errQUEUE_EMPTY
uxQueueMessagesWaiting()
uxQueueMessagesWaiting()
函数用于获得队列中数据的数量
UBaseType_t uxQueueMessagesWaiting( QueueHandle_t xQueue )
信号量
FreeRTOS中信号量分为二值信号量、计数型信号量、互斥信号量和递归互斥信号量
二值信号量
二值信号量即任务与中断间或者两个任务间的标志,该标志非“满”即“空”。也可以理解为只有一个队列项的队列,该队列要么是满要么是空,Send操作相当把该标志置“满”,Receive操作相关与把该标志取"空",经过send和receive操作实现任务与中断间或者两任务的操作同步。二值信号量的使用方法如下图所示
二进制信号量和互斥锁区别:
- 互斥锁包括优先继承机制,而二值信号量则不包括
- 互斥锁用于互斥,二值信号量用于同步
计数信号量
正如二进制信号量可以被认为是长度为 1 的队列那样,计数信号量也可以被认为是长度大于 1 的队列。 再次强调一下, 信号量的用户对存储在队列中的数据不感兴趣,只关心队列是否为空。
计数信号量通常用于两种情况:
-
盘点事件
在此使用方案中,每次事件发生时,事件处理程序将“给出”一个信号量(信号量计数值递增) ,并且 处理程序任务每次处理事件(信号量计数值递减)时“获取”一个信号量。因此,计数值是 已发生的事件数与已处理的事件数之间的差值。在这种情况下, 创建信号量时计数值为零。(消费者生产者模型)
-
资源管理
在此使用情景中,计数值表示可用资源的数量。要获得对资源的控制权,任务必须首先获取 一个信号量——同时递减信号量计数值。当计数值达到零时,表示没有空闲资源可用。当任务使用完资源时, “返还”一个信号量——同时递增信号量计数值。在这种情况下, 创建信号量时计数值等于最大计数值
互斥锁
互斥锁是包含优先级继承机制的二值信号量。 二值信号量能更好实现同步(任务间或任务与中断之间), 而互斥锁有助于更好实现简单互斥(即相互排斥)。 互斥锁可理解为保护资源的令牌。 任务希望访问资源时,必须首先 Take 令牌,此时其他任务不能够使用资源;使用资源后,必须 Give 令牌,这样其他任务就有机会访问相同的资源。
优先级反转
**含义:**当低优先级任务持有令牌时,若高优先级的任务也要使用资源,则其必须被低优先级的任务阻塞,直到低优先级任务资源使用完毕。
error:如果把这种行为进一步放大。若有3个任务A、B、C同时使用资源,他们优先级为A>B>C。当C先拿到互斥量,此时A要使用资源,则要等待C使用资源结束,此时若又有任务B执行,则C无法执行,此时讲很久无法使得A获得资源的使用权。
优先级继承
拿到互斥量的任务暂时会拥有等待互斥量任务的优先级。当互斥量归还时,该任务优先级自动还原。
- 确保较高优先级的任务保持阻塞状态的时间尽可能短, 从而最大限度地减少已经发生的** 优先级反转 现象**
- 无法完全解决优先级反转,只是在某些情况下将影响降至最低
不应在中断中使用互斥锁:
- 互斥锁使用的优先级继承机制要求:从任务中(而不是从中断中)拿走和放入互斥锁。
- 中断无法保持阻塞来等待一个被互斥锁保护的资源由互斥锁保护的资源变为可用。
递归互斥锁
可以重复加锁,为每个成功的 xSemaphoreTakeRecursive()
调用 xSemaphoreGiveRecursive()
后,互斥锁才会重新变为可用。例如,如果一个任务成功“加锁”相同的互斥锁 5 次, 那么任何其他任务都无法使用此互斥锁,直到任务也把这个互斥锁“解锁”5 次。
事件组
概念
事件位:用于指示事件是否发生,称为事件标志。在实际应用中可以:
- 定义一个位,设置为 1 时表示“已收到消息并准备好处理”, 设置为 0 时表示“没有消息等待处理”。
- 定义一个位,设置为 1 时表示“应用程序已将准备发送到网络的消息排队”, 设置为 0 时表示 “没有消息需要排队准备发送到网络”。
- 定义一个位,设置为 1 时表示“需要向网络发送心跳消息”, 设置为 0 时表示“不需要向网络发送心跳消息”。
事件组:一组事件位,通过位编号来引用,由 EventGroupHandle_t
类型的变量引用,以上面列出的三个例子为例
- 事件标志组位编号 为 0 表示“已收到消息并准备好处理”。
- 事件标志组位编号 为 1 表示“应用程序已将准备发送到网络的消息排队”。
- 事件标志组位编号 为 2 表示“需要向网络发送心跳消息”。
其中, EventGroupHandle_t
变量由 configUSE_16_BIT_TICKS
决定
- 当configUSE_16_BIT_TICKS==0的时候,整个数据是4个字节,消息占据了三个字节
- 当configUSE_16_BIT_TICKS==1的时候,整个数据是2个字节,消息占据了一个字节
API
xEventGroupCreate()
创建一个新的 RTOS 事件组,并返回可以引用新创建的事件组的句柄
EventGroupHandle_t xEventGroupCreate( void );
参数 | 含义 |
---|---|
返回值 | NULL 表示没有足够的堆空间分配给事件组而导致创建失败。 |
非 NULL 值表示事件组创建成功。此返回值应当保存下来,以作为操作此事件组的句柄。 |
要使此 RTOS API 函数可用:
configSUPPORT_DYNAMIC_ALLOCATION
必须设置为 1,或保留未定义状态(默认为 1)。- RTOS 源文件
FreeRTOS/source/event_groups.c
必须 包含在构建中。
xEventGroupSetBits()
在 RTOS 事件组中置1,将自动解除所有等待位置1的任务的阻塞态
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet );
参数 | 含义 |
---|---|
xEventGroup | 目标事件组的句柄。这个句柄即是调用 xEventGroupCreate() 创建该队列时的返回值。 |
uxBitsToSet | 要设置的事件标志位 |
返回值 | 返回在调用这个函数之后的事件组的值 |
xEventGroupWaitBits()
读取 RTOS 事件组中的位,选择性地进入“已阻塞”状态(已设置 超时值)以等待该位置1。
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
TickType_t xTicksToWait )
参数 | 含义 |
---|---|
xEventGroup | 目标事件组的句柄。这个句柄即是调用 xEventGroupCreate()创建该队列时的返回值。 |
uxBitStowait | 用于指示要在事件组内测试的位的逐位值。例如,要等待位0和位2,请将uxBitsToWaitFor设置为0x05 |
xClearOnExit | true表示读取之后将所在位清空,false表示不清空。 |
xWaitForAllBits | true表示所有事件位都为真的时候才返回;false表示有任意一位被设置就能返回。 |
xTicksToWait | 阻塞超时时间。如果在发送时队列已满,这个时间即是任务处于阻塞态等待队列空间有效的最长等待时间。如 果 xTicksToWait 设 为 0 ,并且队列已满,则xQueueSendToFront()与 xQueueSendToBack()均会立即返回。阻塞时间是以系统心跳周期为单位的,所以绝对时间取决于系统心跳频率。常量 portTICK_RATE_MS 可以用来把心跳时间单位转换为毫秒时间单位。如果把 xTicksToWait 设置为 portMAX_DELAY ,并且在FreeRTOSConig.h 中设定 INCLUDE_vTaskSuspend 为 1,那么阻塞等待将没有超时限制。 |
vEventGroupDelete()
删除先前的事件组。在被删除的事件组上阻塞的任务将被取消阻塞,并且返回事件组值为 0。
void vEventGroupDelete( EventGroupHandle_t xEventGroup )
参数 | 含义 |
---|---|
xEventGroup | 目标事件组的句柄。这个句柄即是调用 xEventGroupCreate()创建该队列时的返回值。 |
任务通知
任务通知是直接发送至任务的事件, 而不是通过中间对象 (如队列、事件组或信号量)间接发送至任务的事件。 向任务发送“直达任务通知” 会将目标任务通知设为“挂起”状态。 正如任务可以阻塞中间对象 (如等待信号量可用的信号量),任务也可以阻塞任务通知, 以等待通知状态变为“挂起”。
任务通知可以通过如下方法更新接收任务的通知值:
- 不覆盖接收任务的通知值 ( 如果上次发送给接收任务的通知还没被处理 ) 。
- 覆盖接收任务的通知值。
- 更新接收任务通知值的一个或多个 bit 。
- 增加接收任务的通知值。
优势:
- 使用时无需创建中间对象。 通过直接通知来解除 RTOS 任务阻塞状态的速度和使用中间对象(如二进制信号量)相比快了 **45% **,
- 使用的 RAM 也更少,其涉及到任务TCB的如下两个字段(5字节),而一个队列数据结构至少占用76字节RAM
typedef struct tskTaskControlBlock{
...
#if ( configUSE_TASK_NOTIFICATIONS == 1 )
volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
#endif
...
} tskTCB;
劣势:
- 只能有一个任务接收通知,即必须指定接受通知的任务; 不过,这个条件在大多数真实世界情况下是满足的。比如,中断解除了一个任务的阻塞状态,该任务将处理由中断接收的数据。
- 只有等待通知的任务可以阻塞,发送通知的任务,在任何情况下都不会因为发送失败而进入阻塞。
应用
- 作为二进制信号量
- 作为计数信号量
- 作为事件组
- 作为邮箱
注意事项
- 不可以将通知发送到中断中
- 通知不可以发送给多个任务
- 发送通知的任务不可以被阻塞
API
任务通知发送函数
函数 | 描述 |
---|---|
xTaskNotify() | 带有通知值并且不保留接收任务原通知值 |
xTaskNotifyFromISR() | 函数 xTaskNotify() 的中断版本。 |
xTaskNotifyGive() | 不带通知值并且不保留接收任务的通知值,此函数会将接收任务的通知值加一 |
vTaskNotifyGiveFromISR() | 函数 xTaskNotifyGive() 的中断版本。 |
xTaskNotifyAndQuery() | 带有通知值并且保留接收任务的原通知值 |
xTaskNotiryAndQueryFromISR() | 函数 xTaskNotifyAndQuery() 的中断版本,用在中断服务函数中。 |
xTaskNotify()
此函数用于发送任务通知,此函数发送任务通知的时候带有通知值,此函数是个宏,真正执行的函数xTaskGenericNotify()
,函数原型如下:
BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify,
uint32_t ulValue
eNotifyAction eAction)
typedef enum{
eNoAction=0,
eSetBits,//更新指定的bit
elncrement,//通知值加一
eSetValueWithOverwrite,//覆写的方式更新通知值
eSetValueWithoutOverwrite//不覆写通知值
}eNotifyAction;
参数 | 描述 |
---|---|
xTaskToNotify | 任务句柄,指定任务通知是发送给哪个任务的。 |
ulValue | 任务通知值。 |
eAction | 任务通知更新的方法,eNotifyAction 是个枚举类型,在文件task.h中有定义 |
返回值 | pdFAIL :当参数eAction 设置为eSetValueWithoutOverwrite 的时候,如果任务通知值没有更新成功就返回pdFAIL。pdPASS :eAction 设置为其他选项的时候统一返回pdPASS 。 |
xTaskNotifyFromISR()
此函数用于发送任务通知,是函数 xTaskNotify()
的中断版本,此函数是个宏,真正执行的是函数xTaskGenericNotifyFromISR()
,此函数原型如下:
BaseType_t xTaskNotifyFromlSR( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
BaseType_t*pxHigherPriorityTaskWoken);
参数 | 描述 |
---|---|
xTaskToNotify | 任务句柄,指定任务通知是发送给哪个任务的。 |
ulValue | 任务通知值。 |
eAction | 任务通知更新的方法。 |
pxHigherPriorityTaskWoken | 记退出此函数以后是否进行任务切换,这个变量的值函数会自动设置的,用户不用进行设置,用户只需要提供一个变量来保存这个值就行了。当此值为pdTRUE的时候在退出中断服务函数之前一定要进行一次任务切换。 |
返回值 | pdFAIL:当参数eAction设置为eSetValueWithoutOverwrite的时候,如果任务通知值没有更新成功就返回pdFAIL。pdPASS:eAction设置为其他选项的时候统一返回pdPASS。 |
xTaskNotifyGive()
相对于函数xTaskNotify0
,此函数发送任务通知的时候不带有通知值。此函数只是将任务通知值简单的加一,此函数是个宏,真正执行的是函数xTaskGenericNotify()
,此函数原型如下:
BaseType_txTaskNotifyGive(TaskHandle_t xTaskToNotify);
参数 | 描述 |
---|---|
xTaskToNotify | 任务句柄,指定任务通知是发送给哪个任务的。 |
返回值 | pdPASS:此函数只会返回pdPASS。 |
vTaskNotifyGiveFromISR()
此函数为xTaskNotifyGive()的中断版本,用在中断服务函数中,函数原型如下:
void vTaskNotifyGiveFromlSR(TaskHandle_t xTaskHandle,
BaseType_t* pxHigherPriorityTaskWoken);
参数 | 描述 |
---|---|
xTaskToNotify | 任务句柄,指定任务通知是发送给哪个任务的。 |
pxHigherPriorityTaskWoken | 记退出此函数以后是否进行任务切换,这个变量的值函数会自动设置的,用户不用进行设置,用户只需要提供一个变量来保存这个值就行了。当此值为pdTRUE的时候在退出中断服务函数之前一定要进行一次任务切换。 |
xTaskNotifyAndQuery()
此函数和xTaskNotify()很类似,此函数比xTaskNotify()多一个参数,此参数用来保存更新前的通知值。此函数是个宏,真正执行的是函数xTaskGenericNotify0,此函数原型如下:
BaseType_txTaskNotifyAndQuery( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
uint32_t*pulPreviousNotificationValue);
参数 | 描述 |
---|---|
xTaskToNotify | 任务句柄,指定任务通知是发送给哪个任务的。 |
ulValue | 任务通知值。 |
eAction | 任务通知更新的方法。 |
pulPreviousNotificationValue | 用来保存更新前的任务通知值。 |
返回值 | pdFAIL:当参数eAction设置为eSetValueWithoutOverwrite的时候,如果任务通知值没有更新成功就返回pdFAIL。pdPASS:eAction设置为其他选项的时候统一返回pdPASS。 |
xTaskNotifyAndQueryFromISR()
此函数为 xTaskNotifyAndQuery()
的中断版本,用在中断服务函数中。此函数同样为宏,真正执行的是函数xTaskGenericNotifyFromISR()
,此函数的原型如下:
BaseType_t xTaskNotifyAndQueryFromISR( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
uint32_t* pulPreviousNotificationValue,
BaseType_t*pxHigherPriorityTaskWoken);
参数 | 描述 |
---|---|
xTaskToNotify | 任务句柄,指定任务通知是发送给哪个任务的。 |
ulValue | 任务通知值。 |
eAction | 任务通知更新的方法。 |
pulPreviousNotificationValue | 用来保存更新前的任务通知值。 |
pxHigherPriorityTaskWoken | 记退出此函数以后是否进行任务切换,这个变量的值函数会自动设置的,用户不用进行设置,用户只需要提供一个变量来保存这个值就行了。当此值为pdTRUE的时候在退出中断服务函数之前一定要进行一次任务切换。 |
返回值 | pdFAIL:当参数eAction 设置为eSetValueWithoutOverwrite 的时候,如果任务通知值没有更新成功就返回pdFAIL。pdPASS:eAction 设置为其他选项的时候统一返回pdPASS。 |
任务获取通知函数
函数 | 描述 |
---|---|
ulTaskNotifyTake() | 获取任务通知,可以设置在退出此函数的时候将任务通知值清零或者减一。当任务通知用作二值信号量或者计数信号量的时候使用此函数来获取信号量。 |
xTaskNotifyWait() | 等待任务通知,比 ulTaskNotifyTak() 更为强大,全功能版任务通知获取函数。 |
ulTaskNotifyTake()
此函数为获取任务通知函数,当任务通知用作二值信号量或者计数型信号量的时候可以使用此函数来获取信号量,函数原型如下
uint32_tulTaskNotifyTake( BaseType_t xClearCountOnExit,
TickType_t xTicksToWait);
参数 | 描述 |
---|---|
XClearCountOnExit | pdFALSE :在退出函数 ulTaskNotifyTake() 的时候任务通知值减一,类似计数型信号量pdTRUE :的话在退出函数的时候任务任务通知值清零,类似二值信号量。 |
xTickToWait | 阻塞时间。 |
返回值 | 任何值:任务通知值减少或者清零之前的值 |
xTaskNotifyWait()
此函数也是用来获取任务通知的,不过此函数比 ulTaskNotifyTake()
更为强大,不管任务通知用作二值信号量、计数型信号量、队列和事件标志组中的哪一种,都可以使用此函数来获取任务通知。但任务通知用作二值信号量和计数型信号量的时候推荐使用函数 ulTaskNotifyTake()
。此函数原型如下:
BaseType_txTaskNotifyWait( uint32_tulBitsToClearOnEntry,
uint32_t ulBitsToClearOnExit,
uint32_t*pulNotificationValue,
TickType_t xTicksToWait);
参数 | 描述 |
---|---|
ulBitsToClearOnEntry | 当没有接收到任务通知的时候将任务通知值与此参数的取反值进行按位与运算,当此参数为0xffffffff或者ULONG_MAX的时候就会将任务通知值清零。 |
ulBitsToClearOnExit | 如果接收到了任务通知,在做完相应的处理退出函数之前将任务通知值与此参数的取反值进行按位与运算,当此参数为0xffffffff或者ULONG_MAX的时候就会将任务通知值清零。 |
pulNotificationValue | 此参数用来保存任务通知值。 |
xTickToWait | 阻塞时间。 |
返回值 | pdTRUE:获取到了任务通知。pdFALSE:任务通知获取失败。 |
内存
静态内存分配 vs 动态内存分配
动态创建 RTOS 对象的好处是最大限度地减少使用应用程序的最大 RAM:
- 创建对象时所需的函数参数较少
- 内存申请在 RTOS API 函数内自动分配
- 如果对象被删除,它所使用的内存可以重新被使用, 从而减少应用程序的最大 RAM 占用。
- 提供 RTOS API函数以返回关于堆使用的信息, 从而允许优化堆大小。
- 可以选择最适合应用程序使用的内存分配方案, 其中包括 heap_1.c,可实现对安全关键应用程序必备的 简单性和决定性;heap_4.c,可实现碎片保护; heap_5.c,可在多个 RAM 区内分割堆;或者由应用程序写入器自身提供的分配方案。
静态分配的 RTOS 创建 RAM 对象可为应用程序写入器提供更好的控制力:
- RTOS 对象可以放置在特定的内存位置。
- 最大 RAM 占用可以在链接时间确定,而不是运行时间。
- 应用程序写入器可以很好地处理内存分配故障。
- 它允许在应用程序中使用 RTOS, 这类应用程序不允许任何动态内存分配(尽管 FreeRTOS 包含 可以克服大多数异议的分配方案)。
堆内存管理
标准 C 库 malloc()
和 free()
函数的缺点:
- 在嵌入式系统上并不总是可用
- 占用了宝贵的代码空间
- 不是线程安全的
- 它们不是确定性的 (执行函数所需时间将因调用而异)
为了避免此问题, 当 RTOS 内核需要 RAM 时,它不调用 malloc()
,而是调用 pvPortMalloc()
。 释放 RAM 时, RTOS 内核调用 vPortFree()
,而不是 free()
。
FreeRTOS提供了 pvPortMalloc()
和 vPortFree()
的五个示例实现,FreeRTOS应用程序可以使用示例实现之一,也可以提供自己的实现。这五个实例分别定义在heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c源文件中,均位于FreeRTOS/Source/portable/MemMang
目录中。
Heap_1
特点:内存一经分配,它不允许内存再被释放
做法:使用 RAM 时将一个单一的数组细分为更小的块 ,因此不考虑也不会出现内存碎片化的现象。 数组的总大小(堆的总大小)通过 configTOTAL_HEAP_SIZE
设置。每个创建的任务都要两个内存块,一个是任务控制块(TCB),另一个堆栈,这两个都从堆分配。下图演示heap_1如何在创建任务时细分堆
- A 显示创建任何任务之前的堆——整个堆都是空闲的。
- B 显示创建一项任务后的堆。
- C 显示创建三个任务后的堆。
使用场景:
- 适用于那些一旦创建好任务、信号量、队列和事件组就再也不会删除的应用
- 禁止使用动态内存分配的商业关键和安全关键系统
Heap_2
与方案 1 不同,它允许释放先前分配的块,但不将相邻的空闲块组合成一个大块。而再次分配时会返回一个指向分配的内存块中最低字节的指针
- A 显示创建三个任务后的数组。 一个大的空闲块保留在阵列的顶部。
- B 显示其中一个任务被删除后的数组。 数组顶部的大空闲块仍然存在。 现在还有两个较小的空闲块,它们以前分配给已删除任务的 TCB 和堆栈。
- C 显示创建另一个任务后的情况。 创建任务导致对 pvPortMalloc() 的两次调用,一次分配新的 TCB,一次分配任务堆栈。 任务是使用 xTaskCreate() API 函数创建的。 对 pvPortMalloc() 的调用发生在 xTaskCreate() 内部。
Heap_2 执行时间不是确定性的,但比 malloc() 和 free() 的大多数标准库实现要快。
Heap_3
Heap_3使用标准库 malloc()
和 free()
函数,因此堆的大小由链接器配置定义,configTOTAL_HEAP_SIZE
设置对其没有影响,Heap_3通过暂时挂起FreeRTOS调度程序使 malloc()
和 free()
线程安全。
Heap_4
与方案 2 不同,将数组细分为更小的块, 并将相邻的空闲碎片组成单个大内存块,从而最大限度的降低内存碎片的风险。在分配内存时候使用 the first fit算法 确保 pvPortMalloc()
使用第一个能够容纳请求字节数的空闲块.
- A 显示创建三个任务后的数组。一个大的空闲块保留在阵列的顶部。
- B 显示其中一个任务被删除后的数组。数组顶部的大空闲块仍然存在。还有一个空闲块,其中先前分配了已删除任务的 TCB 和堆栈。请注意,与演示 heap_2 时不同,删除 TCB 时释放的内存和删除堆栈时释放的内存不会保留为两个单独的空闲块,而是组合在一起以创建更大的单个空闲块。
- C 显示了创建 FreeRTOS 队列后的情况。队列是使用 xQueueCreate() API 函数创建的。 xQueueCreate() 调用 pvPortMalloc() 来分配队列使用的 RAM。由于 heap_4 使用the first fit算法,pvPortMalloc() 将从第一个足够大以容纳队列的空闲 RAM 块分配 RAM,使用的是删除任务时释放的 RAM。然而,队列不会消耗空闲块中的所有 RAM,因此该块被分成两部分,未使用的部分仍然可用于未来对 pvPortMalloc() 的调用。
- D 显示了直接从应用程序代码调用 pvPortMalloc() 后的情况,而不是通过调用 FreeRTOS API 函数间接调用的情况。用户分配的块足够小,可以放入第一个空闲块中,该块是分配给队列的内存和分配给后续 TCB 的内存之间的块。
- 删除任务时释放的内存现在已拆分为三个单独的块;第一个块保存队列,第二个块保存用户分配的内存,第三个块保持空闲。
- E显示队列被删除后的情况,自动释放已分配给删除队列的内存。现在用户分配块的两侧都有空闲内存。
- F 显示用户分配的内存也被释放后的情况。用户分配块已使用的内存已与任一侧的空闲内存组合以创建更大的单个空闲块。
Heap_4 不是确定性的,但比 malloc() 和 free() 的大多数标准库实现要快。
Heap_5
heap_5用于分配和释放内存的算法和heap_4相同,不同的是heap_5不限于从单个静态声明的数组分配内存,heap_5可以从多个独立的内存空间分配内存
Heap_5 必须调用 vPortDefineHeapRegions()
初始化,采用单一参数为 HeapRegion_t
的结构体数组,其定义为:
typedef struct HeapRegion{
/* Start address of a block of memory that will be part of the heap.*/
uint8_t *pucStartAddress;
/* Size of the block of memory. */
size_t xSizeInBytes;
} HeapRegion_t;
以下图为例,其中包含三个独立的RAM块:RAM1、RAM2和RAM3,假设可执行代码放置在只读寄存器中,没有显示。
初始化Heap_5的代码可以利用 HeapRegion_t
写为
/* Define the start address and size of the three RAM regions. */
#define RAM1_START_ADDRESS ( ( uint8_t * ) 0x00010000 )
#define RAM1_SIZE ( 65 * 1024 )
#define RAM2_START_ADDRESS ( ( uint8_t * ) 0x00020000 )
#define RAM2_SIZE ( 32 * 1024 )
#define RAM3_START_ADDRESS ( ( uint8_t * ) 0x00030000 )
#define RAM3_SIZE ( 32 * 1024 )
const HeapRegion_t xHeapRegions[] ={
{ RAM1_START_ADDRESS, RAM1_SIZE },
{ RAM2_START_ADDRESS, RAM2_SIZE },
{ RAM3_START_ADDRESS, RAM3_SIZE },
{ NULL, 0 } /* Marks the end of the array. */
};
int main( void ){
/* Initialize heap_5. */
vPortDefineHeapRegions( xHeapRegions );
}
Malloc调用失败的Hook函数
pvPortMalloc()
可以直接从应用程序代码中调用。每次创建内核对象时,它也会在 FreeRTOS 源文件中调用。内核对象的例子包括任务、队列、信号量和事件组。如果 pvPortMalloc()
因为请求大小的块不存在而无法返回 RAM 块,那么它将返回 NULL,则不会创建内核对象。
如果对 pvPortMalloc()
的调用返回 NULL,则所有示例堆分配方案都可以配置一个调用钩子(或回调)函数。需要将 configUSE_MALLOC_FAILED_HOOK
设置为 1,应用程序还需提供一个 malloc 失败的钩子函数。该函数可以以任何适合应用程序的方式实现,该函数如下:
void vApplicationMallocFailedHook( void );
配置:
常量 | 说明 |
---|---|
configAPPLICATION_ALLOCATED_HEAP | 允许将堆放置在内存中的特定地址。 |
configUSE_MALLOC_FAILED_HOOK | 需要配置 malloc 失败的钩子函数 |
configTOTAL_HEAP_SIZE | HEAP数组的总大小(堆的总大小) |
API
xPortGetFreeHeapSize()
返回堆中的空闲字节数,它可用于优化堆大小。 例如,如果 xPortGetFreeHeapSize() 在所有内核对象创建后返回 2000,那么 configTOTAL_HEAP_SIZE 的值可以减少 2000。
size_t xPortGetFreeHeapSize( void );
xPortGetMinimumEverFreeHeapSize()
返回自FreeRTOS应用程序开始执行以来,存在的最小空闲堆空间量,表明应用程序接近耗尽堆空间的程序,仅在使用 heap_4 或 heap_5 时可用。
size_t xPortGetMinimumEverFreeHeapSize( void );
堆栈溢出检测
每个任务都拥有其独立维护的堆栈,FreeRTOS 提供了两种可选机制来检测和纠正堆栈溢出的情况,使用 configCHECK_FOR_STACK_OVERFLOW
配置
Tips:堆栈溢出检查会增加上下文切换的开销,因此建议只在开发或测试阶段使用此检查
检测方法
configCHECK_FOR_STACK_OVERFLOW
设置为 1 :由于堆栈溢出,退出时的堆栈指针可能达到其最大(最深)值,RTOS 内核可以检查处理器堆栈指针是否仍在有效堆栈空间内。如果堆栈指针超出有效堆栈范围的值, 则调用堆栈溢出钩子函数。
configCHECK_FOR_STACK_OVERFLOW
设置为 2 :任务首次创建时,其堆栈会在最后 16 个字节填充一个已知值。 任务退出运行状态时,RTOS 内核可以检查有效堆栈范围内的最后 16 个字节,以确保这些已知值未被任务或中断活动覆盖。 如果这 16 个字节中的任何一个不再为初始值,则调用堆栈溢出钩子函数。这种方法比方法 1 效率低,但仍然相当快。 它很可能会捕获堆栈溢出, 但仍无法保证能够捕获所有溢出。
堆栈溢出的 Hook 函数
如果 configCHECK_FOR_STACK_OVERFLOW
未设置为 0 ,则应用程序必须提供堆栈溢出Hook函数。 该Hook函数必须命名为 vApplicationStackOverflowHook()
,并具有以下原型:
void vApplicationStackOverflowHook( TaskHandle_t xTask,
signed char *pcTaskName );
xTask
和 pcTaskName
参数分别将违规任务的句柄和名称传递给该钩子函数。 但请注意,根据溢出的严重程度,这些参数本身可能会损坏,在这种情况下可直接检查 pxCurrentTCB
变量。
软件定时器
软件定时器由FreeRTOS内核实现,不需要硬件支持, 是由定时器服务(或守护进程)任务提供。
作用:在指定的时间到来时执行指定的函数,或者以某个频率周期性地执行某个函数。被执行的函数叫做软件定时器回调(钩子)函数。
软件定时器Hook函数
软件定时器回调函数是在软件定时器任务中被执行的,这个任务是在vTaskStartScheduler()函数内部由内核自动创建的。不要在回调函数中使用一些导致任务阻塞的函数或代码,例如vTaskDelay(),否则会导致FreeRTOS后台任务进入到阻塞状态。而且应该尽量让定时器回调函代码简洁高效快速执行。
//参数xTimer :因定时到期而调用这个回调函数的定时器的句柄
void ATimerCallback( TimerHandle_t xTimer );
命令队列
软件定时器的相关API本质上是操作队列的API。当FreeRTOS调度器启动时,内核除了自动创建软件定时器任务外,还会自动创建软件定时器命令队列。在用户任务中使用软件定时器相关的API,例如启动定时器,停止定时器,复位定时器,本质上就是通过这个队列来向软件定时器任务发送相关消息。
软件定时器命令队列的长度使用 configTIMER_QUEUE_LENGTH
来定义。
可以看出软件定时器在队列为空(没有命令)的情况下为阻塞状态
触发方式
- 单次触发定时器:当定时器启动,并到达定时时间后,回调函数只会执行一次。定时器不会自动重新启动,但可以手动启动
- 自动重装定时器:每次达到定时时间间隔后,除了执行回调函数,还会自动重新启动,可以实现周期性执行回调函数
下图中的时间线展示了一次性定时器和自动重载定时器之间活动行为的差异 。定时器 1 是 定时为 100 的单次触发定时器,定时器 2 是自动重载定时器,定时为 200。
重置定时器
重置定时器后,到期时间将与重置定时器的时间挂钩, 而非最初启动定时器的时间。
下图演示了此行为, 其中 Timer 1 是一次性定时器,周期等于 5 秒。在所描绘的示例中,假设应用程序在按下某个键时打开 LCD 背光, 并且保持开启状态, 如果没有按下任何键,可保持 5 秒。
- 1s时按下按键,灯开始亮
- 5s时又按下一次按键重置定时器,因此6s时定时器并不会结束,应该是10s时结束
- 10s时定时器执行回调函数,灯灭
软件定时器的状态
- 休眠状态:休眠状态时,定时器是存在的,可以通过它的句柄来操作它,但是它没有运行,没有计时,所以它的回调函数不会执行
- 运行状态:定时器正在计时,包括正在执行它的回调函数
配置:
常量 | 说明 |
---|---|
configUSE_TIMERS | 设置为 1 以包括定时器功能。 当 configUSE_TIMERS 设置 1 时, 随着 RTOS 调度器的启动, 将自动创建定时器服务任务。 |
configTIMER_TASK_PRIORITY | 设置定时器服务任务的优先级。 和所有任务一样, 定时器服务任务可以在 0 和 (configMAX_priority - 1 ) 之间运行任何优先级。需要仔细选择此数值,以满足 应用程序的要求。 例如,如果定时器服务任务 成为系统中最高优先级任务,那么 那么发送到定时器服务任务的命令(当调用定时器 API 函数时) 和过期定时器都均会立即得到处理。 反之, 如果定时器服务任务被赋予低优先级, 则发送到定时器服务任务和过期的定时器的命令均不会被处理 直到所述定时器服务任务是 能够运行的最高优先级任务为止。 但值得注意的是 定时器到期时间是相对于发送命令的时间而计算, 而非相对于处理命令的时间进行计算。 |
configTIMER_QUEUE_LENGTH | 这设置了定时器命令队列在 在任何时间均可以保存的未处理命令的最大数量。定时器命令队列可能已满的原因包括:在启动 RTOS 调度程序之前 进行多次定时器 API 函数调用。中断服务例程 (ISR) 进行多个(中断安全) 定时器 API 函数调用。从优先级高于定时器服务任务的任务 调用多个定时器 API 函数。 |
configTIMER_TASK_STACK_DEPTH | 设置分配给定时器服务任务的堆栈大小 (以字为单位,而不是以字节为单位)。定时器回调函数在定时器上下文中执行 服务任务。 因此,定时器服务任务的堆栈要求 取决于定时器回调 函数。 |