事件组

        事件主要用于实现多任务间的同步,但事件通信只能是事件类型的通信,无数据传输。与信号量不同的是,它可以实现一对多,多对多的同步。事件组是由一组事件位组成,事件组中的事件位通过位编号来引用(每个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 和 任务之间共享数据的临界保护。

参考

事件

FreeRTOS事件组
hook functions

低功耗支持

FReeRTOS 的 SMP

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐