FreeRTOS 事件组 & 钩子函数 & 低功耗支持 & SMP
事件主要用于实现多任务间的同步,但事件通信只能是事件类型的通信,无数据传输。与信号量不同的是,它可以实现一对多,多对多的同步。事件组是由一组事件位组成,事件组中的事件位通过位编号来引用(每个bit都是用于指示特定事件是否发生的事件位)。注意。
事件组
事件主要用于实现多任务间的同步,但事件通信只能是事件类型的通信,无数据传输。与信号量不同的是,它可以实现一对多,多对多的同步。事件组是由一组事件位组成,事件组中的事件位通过位编号来引用(每个bit都是用于指示特定事件是否发生的事件位)。
- 若configUSE_16_BIT_TICKS 设为 1,则事件组存储的位数为8,否则为24.
- 事件组用于同步任务时,创建通常称为“集合”的任务,在某个同步点已到达的任务设置对应的事件位,并在阻塞状态下等待,直到参与同步的所有其他任务也到达其同步点后解除阻塞。
- 事件组实现同步的本质是用共享状态(bit)+ 条件阻塞实现多任务协同;
- 任务通过“逻辑与” 或 “逻辑或” 与一个或多个事件建立关联,形成一个事件组,以实现多个任务的同步;
- 注意:事件的发送操作是不可多次累计的,且事件接收成功后需要使用xClearOnExit选项来主动清除已接收到的事件类型(事件不会自动清除)
/* 如下所示,task A 到达同步点时,阻塞等待其他所有任务到达同步点,直到事件为0b111 解除阻塞*/
task A {
/* 到达同步点设置bit0 状态 & 阻塞等待其他任务完成 */
xEventGroupSetBits(group, BIT0); /*此时 group状态设置为0b001 */
xEventGroupWaitBits(
group,
0b111, // 等待所有bit
pdTRUE, // 满足后清除
pdTRUE, // 等待“全部bit”
portMAX_DELAY
);
}
在实现事件组时需要克服以下两项主要挑战:
- 避免在用户的应用程序中创建争用条件:
- 可能会出现争用条件的情况:不清楚各个位由谁清除,何时清除,以及不清楚在任务退出时是否设置或清除了位;
- 事件组的实现确保了位的设置,测试和清除具有原子性,从而消除了争用条件的可能性;
- 避免不确定性
- 事件组概念意味着非确定性行为,因为不知道在一个事件组上有多少任务被阻塞,因此不清楚在设置事件位时,需要测试多少条件或解除多少阻塞任务。
- 因此,注意在ISR中需要使用xEventGroupSetBitsFromISR()接口,不能用非确定性的xEventGroupSetBits();
- 事件组概念意味着非确定性行为,因为不知道在一个事件组上有多少任务被阻塞,因此不清楚在设置事件位时,需要测试多少条件或解除多少阻塞任务。
code相关接口
/* 创建事件组,并返回可以引用新创建的事件组的句柄 */
EventGroupHandle_t xEventGroupCreate( void );
/* 删除事件组 */
void vEventGroupDelete( EventGroupHandle_t xEventGroup );
/* 对xEventGroup 设置位,uxBitsToSet 可以为1个多个位的按位值,比如:0x9 */
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet );
BaseType_t xEventGroupSetBitsFromISR(
EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
BaseType_t *pxHigherPriorityTaskWoken );
/* uxBitsToWaitFor: 指定要等待的 1个或多个位的 按位值*/
/* xClearOnExit: 设置为pdTRUE时,因超时以外的任何原因返回,都会将已设置的任何位都清除掉,
pdFALSE:事件组已设置的位不会改变 */
/* xWaitForAllBits: 设置为true时,所有事件位被置位或超时 才会解除阻塞,
pdFALSE: 只要有其中任何一位被置位或超时,就会解除阻塞 */
/* 返回当前的事件组的值,设置了退出清除时,返回自动清除前的事件组值 */
EventBits_t xEventGroupWaitBits(
const EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
TickType_t xTicksToWait );
/* 主动清除事件组的值 */
EventBits_t xEventGroupClearBits(
EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToClear );
BaseType_t xEventGroupClearBitsFromISR(
EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToClear );
/* 返回当前事件组的值 */
EventBits_t xEventGroupGetBits( EventGroupHandle_t xEventGroup );
EventBits_t xEventGroupGetBitsFromISR(
EventGroupHandle_t xEventGroup );
/* 设置事件位 & 等待其他任务设置事件位(uxBitsToWaitFor)*/
EventBits_t xEventGroupSync( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
const EventBits_t uxBitsToWaitFor,
TickType_t xTicksToWait );
钩子函数
钩子函数是一种API拦截手段,通过拦截系统或进程事件消息实现对消息流的监控与修改,其本质是预定义的一种用户可插入的回调函数,内核在某些关键时刻主动调用的这些用户自定义的函数(钩子函数),从而让用户在不修改内核代码的前提下扩展行为。
- 使用钩子函数的主要目的,是为了让用户介入内核关键流程,但又不破坏内核稳定性,内核会预留一些插槽,用户通过自定义实现钩子函数替换掉预留的函数接口,从而让钩子函数的用户参与到原有的调用流程。
空闲钩子函数
空闲任务可以选择性地调用的应用程序定义的钩子函数,即为空闲钩子,只要空闲任务正在运行,就会重复调用空闲钩子(由于空闲任务不能阻塞,故空闲钩子实现时也不能调用任何可能阻塞的api)。若空闲任务选择调用空闲钩子,则用户必须实现具有以下原型的钩子函数。
/* 选择调用空闲钩子 */
#define configUSE_IDLE_HOOK 1
/* 空闲钩子原型 */
void vApplicationIdleHook( void );
Tick 钩子函数
Tick 钩子用于自定义实现定时器功能,但该钩子函数是在ISR内执行,因此必须非常短,不使用很多堆栈,并不能调用任何阻塞接口。Tick中断可以选择性地调用tick 钩子,若选择调用tick 钩子函数,则用户必须实现具有以下原型的钩子函数。
/* 选择调用Tick 钩子函数 */
#define configUSE_TICK_HOOK 1
/* Tick 钩子函数原型 */
void vApplicationTickHook( void );
Malloc失败钩子函数
堆栈内存分配方案可以选择性地调用malloc 失败钩子函数,该函数可以配置为在pvPortMalloc() 返回NULL时调用(堆内存不足)。
/* 选择调用 malloc 失败钩子 */
#define configUSE_MALLOC_FAILED_HOOK 1
/* Malloc失败钩子函数原型 */
void vApplicationMallocFailedHook( void );
堆栈溢出钩子函数
堆栈溢出时调用的钩子函数,具体细节参考:堆栈溢出检查
/* 定义为非0值时,会根据设置的值选择对应的堆栈溢出检测方法,
并在溢出时调用堆栈溢出钩子函数(具体见堆栈溢出检查)*/
configCHECK_FOR_STACK_OVERFLOW
/* 堆栈溢出钩子函数原型 */
void vApplicationStackOverflowHook( TaskHandle_t xTask,
char *pcTaskName );
守护进程任务启动钩子
RTOS 守护进程任务与定时器服务任务相同,有时它被称为守护进程任务,是因为该任务现在不仅仅用于服务定时器。守护进程任务首次开始执行时,会调用守护进程任务启动钩子。
/* 选择调用 守护进程任务启动钩子 */
#define configUSE_DAEMON_TASK_STARTUP_HOOK 1
/* 守护进程任务启动钩子函数原型 */
void vApplicationDaemonTaskStartupHook( void );
低功耗支持
通常要降低运行FreeRTOS 的微控制器的功耗,可使用空闲任务钩子将微控制器置于低功耗状态。FreeRTOS 采用无tick闲置模式(停止tick中断),以使微控制器可以维持在深度节能(深度睡眠)状态,直到中断发生,或到了RTOS内核将任务转换为就绪状态的时间。
- 该方式会停止周期性tick中断,在退出低功耗模式时,会对tick计数值进行校正调整。
- 若不停止tick中断,系统会定时退出低功耗模式以处理tick中断,若tick中断频率过高,则tick 中断进入与退出低功耗模式所消耗的能量和时间将极高,而无法实现节能的效果。
- 深度睡眠时由于关闭了tick,因而需要RTC或其他低功耗计时器来确定何时从深度睡眠唤醒。
将 configUSE_TICKLESS_IDLE 定义为 1时,可以启动内置的无tick空闲功能,而将 configUSE_TICKLESS_IDLE 定义为 2时,FreeRTOS不再使用默认的tickless实现,而是交给用户自己实现低功耗逻辑(portSUPPRESS_TICKS_AND_SLEEP 中实现)。
- portSUPPRESS_TICKS_AND_SLEEP原型:
/* xExpectedIdleTime: 深度睡眠时间 */
portSUPPRESS_TICKS_AND_SLEEP( xExpectedIdleTime )
/* 返回eNoTasksWaitingTimeout时,说明当前系统可以无限期地保持在深度睡眠状态 */
eTaskConfirmSleepModeStatus()
- configUSE_TICKLESS_IDLE为2 时的调用流程:
Idle Task
↓
判断可以睡多久 (xExpectedIdleTime)
↓
调用 portSUPPRESS_TICKS_AND_SLEEP( xExpectedIdleTime )
↓
portSUPPRESS_TICKS_AND_SLEEP中用户自实现:
- 停 tick
- 关PLL & DRR
- 停外设
- 切电源
- 设唤醒源
- 进入低功耗
- configUSE_TICKLESS_IDLE为2的优势:
- 不同MCU的低功耗机制完全不同,用户可以根据实际情况确定具体操作(关哪些外设等如何实现低功耗)
- FreeRTOS 内置实现通常只会停 SysTick,但并不会关PLL,外设,或切电源域等操作,
FreeRTOS的对称多处理(SMP)
FreeRTOS 内核中的 SMP 支持 使得FreeRTOS内核实例可以在多个相同的处理器核心中调度任务,即多个不同优先级任务或ISR可以在多个core上同时运行。这些内核架构必须相同,并共享相同的内存。与单核FreeRTOS相比,需要注意以下:
- 由于是多核,多个任务可以同时运行,因而,应用程序不能再依赖于相对任务优先级来同步,必须使用同步基元(信号量,任务直达通知,事件等)
- 若某些任务需要依赖相对优先级来同步,可以将使用vTaskCoreAffinitySet 将其绑定到同一个核上,或设置configRUN_MULTIPLE_PRIORITIES 为0,使只有具有相同优先级的多个任务才能同时运行,但这种方式可能会导致利用率不足(部分core处于空闲状态,低优先级任务却处于就绪状态)
- 由于是多核,ISR可能与其他ISR & 任务同时运行, 应用程序需要确保ISR 和 任务之间共享数据的临界保护。
参考
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐
所有评论(0)