1.事件组
事件组结构体:
事件组 “不关中断” 的核心逻辑
事件组操作时,优先选择 “关调度器” 而非 “关中断” ,原因和实现如下:
关调度器(而非关中断)
FreeRTOS 提供 taskENTER_CRITICAL()(关调度器 + 按需关中断,取决于 configKERNEL_INTERRUPT_PRIORITY 配置 )或 vTaskSuspendAll()(仅关调度器 ),来保护事件组的任务级操作(如任务调用 xEventGroupSetBits )。
关调度器后,中断仍可正常触发(硬件中断不受影响),但任务无法切换,保证事件组操作的原子性。
中断里的事件组操作
中断函数中不能直接修改事件组(因为中断设置事件组的时候有可能唤醒多个满足条件的任务导致中断时间不确定),而是通过 xEventGroupSetBitsFromISR 触发 “守护任务”(daemon task) 来异步处理:
中断里向守护任务的队列发消息,请求设置事件位;
守护任务(任务上下文)真正执行 xEventGroupSetBits,此时用关调度器保证安全。
事件组创建流程:
开始↓
调用 xEventGroupCreate() 函数↓
动态分配事件组内存(若支持静态分配,则使用 xEventGroupCreateStatic())↓
成功? ——→ 是 ——→ 初始化事件组状态(默认所有位为0)↓ ↓否 返回事件组句柄↓ ↓
返回 NULL 结束
设置事件位流程:
开始↓
[上下文判断] → 任务中调用 xEventGroupSetBits() / 中断中调用 xEventGroupSetBitsFromISR()↓
设置指定的事件位(按位或操作更新组值)(操作EventGroup_t 结构体)↓
检查是否有任务因等待这些位被阻塞?↓
是 ——→ 解除阻塞符合条件的任务↓ ↓否 触发上下文切换(若在任务中)或标记延迟调度(若在中断中)↓ ↓
结束 ←←←←←←
等待事件位流程:
开始↓
调用 xEventGroupWaitBits(),传入等待的位掩码、清除标志、阻塞时间等参数↓
检查当前事件位是否满足条件:是否所有指定位被置位?(逻辑AND)或任意指定位被置位?(逻辑OR)↓
满足? ——→ 是 ——→ 根据参数清除事件位↓ ↓否 返回当前事件位值↓ ↓
进入阻塞状态(若阻塞时间 > 0),释放CPU↓ ↓
等待事件被设置或超时 →→→ 超时? ——→ 是 ——→ 返回超时状态↓ ↓否(事件触发) ↓↓ ↓
返回触发后的事件位值 结束
2.任务通知
FreeRTOS 任务通知是一种轻量级任务间通信机制,用于任务或中断向指定任务发送事件或数据:
一、基本概念
核心原理:每个任务的控制块(TCB)中内置通知相关成员(如 ulNotifiedValue 存储通知值 ),无需额外创建通信结构体,可直接向目标任务发送 “通知”。
启用条件:需在 FreeRTOSConfig.h 中定义 configUSE_TASK_NOTIFICATIONS = 1 开启功能。
二、优势与局限
优势
高效性:无需额外结构体,操作直接,比队列、信号量、事件组更快,节省内存(每个任务仅额外占 8 字节存储通知状态和值 )。
灵活更新:支持多种通知值更新方式(如覆盖、保留原值、置位、递增等 ),适配不同场景。
局限
单播特性:仅能指定一个任务接收通知,无法广播给多任务。
数据缓存限制:任务控制块只有一个通知值,无法缓存多个数据,发送方也不能因发送受阻进入阻塞。
中断交互限制:可从中断发通知给任务,但无法给中断发通知(中断无任务结构体 )。
发通知流程:
开始↓
关闭中断 (taskENTER_CRITICAL)↓
目标任务存在?——否——→ 开启中断 ——→ 返回失败 ——→ 结束↓是
目标任务在等待通知?↓
是 ——→ 设置通知值↓从延迟列表移除目标任务↓将目标任务设为就绪状态↓目标任务优先级更高?——是——→ 开启中断 ——→ 设置需要上下文切换标志↓否 ↓开启中断 ←————————————————————————— 触发任务调度 (portYIELD)↓ ↓返回成功 ←————————————————————————— 返回成功↓ ↓结束 结束↓否
设置通知值(累加或覆盖)↓
开启中断 (taskEXIT_CRITICAL)↓
返回成功↓
结束
等待通知流程:
开始↓
关闭中断 (taskENTER_CRITICAL)↓
检查是否有待处理的通知↓
有通知? ——是——→ 清除/递减通知值 ——→ 开启中断 ——→ 返回通知值 ——→ 结束↓否
等待时间为0? ——是——→ 开启中断 ——→ 返回0 ——→ 结束↓否
将任务添加到延迟列表↓
设置任务状态为阻塞↓
开启中断 (taskEXIT_CRITICAL)↓
触发任务调度 (portYIELD)↓
其他任务运行...↓
收到通知或超时?↓
关闭中断 (taskENTER_CRITICAL)↓
从延迟列表移除任务↓
设置任务为就绪状态↓
开启中断 (taskEXIT_CRITICAL)↓
返回通知值(成功)或0(超时)↓
结束
3.软件定时器
软件定时器的核心机制就是时间到达预设值后自动触发回调函数执行。
FreeRTOS 软件定时器的核心是基于系统 tick 的有序链表管理和低优先级检测任务,通过回调函数实现定时事件处理。
内部实现:
一、核心数据结构
1. 定时器控制块(Timer_t
)
typedef struct tmrTimerControl {const char *pcTimerName; // 定时器名称(调试用)ListItem_t xTimerListItem; // 用于链表管理的节点TickType_t xTimerPeriodInTicks; // 定时周期(tick数)UBaseType_t uxAutoReload; // 是否自动重载(周期/单次模式)void (*pxCallbackFunction)(TimerHandle_t xTimer); // 回调函数指针void *pvTimerID; // 定时器ID(用户数据)TickType_t xExpireTime; // 下次超时的绝对tick时间// ...其他内部字段
} Timer_t;
2. 定时器列表(按超时时间排序)
static List_t xActiveTimerList1; // 活跃定时器列表1
static List_t xActiveTimerList2; // 活跃定时器列表2(用于溢出处理)
二、实现机制
1.定时器任务(prvTimerTask
)
- 核心职责:
- 优先级通常设为较低值(如
configTIMER_TASK_PRIORITY
),避免影响关键任务。 - 周期性检查定时器列表,处理超时定时器。
- 优先级通常设为较低值(如
执行流程:
void prvTimerTask(void *pvParameters) {for (;;) {// 1. 进入临界区,防止任务调度干扰taskENTER_CRITICAL();// 2. 获取当前系统tickTickType_t xTimeNow = xTaskGetTickCount();// 3. 检查活跃列表头部定时器是否超时while (listLIST_IS_EMPTY(&xActiveTimerList1) == pdFALSE) {Timer_t *pxTimer = (Timer_t *)listGET_OWNER_OF_HEAD_ENTRY(&xActiveTimerList1);if (pxTimer->xExpireTime <= xTimeNow) {// 4. 移除超时定时器(void)uxListRemove(&pxTimer->xTimerListItem);// 5. 退出临界区,执行回调(避免长时间持有锁)taskEXIT_CRITICAL();pxTimer->pxCallbackFunction((TimerHandle_t)pxTimer);taskENTER_CRITICAL();// 6. 若为周期模式,重新计算超时时间并加入列表if (pxTimer->uxAutoReload == pdTRUE) {pxTimer->xExpireTime += pxTimer->xTimerPeriodInTicks;vListInsertInOrder(&xActiveTimerList1, &pxTimer->xTimerListItem);}} else {break; // 后续定时器未超时,退出循环}}// 7. 退出临界区,进入阻塞状态直到下一个定时器超时或被唤醒taskEXIT_CRITICAL();vTaskDelayUntil(&xTimeNow, pdMS_TO_TICKS(10)); // 10ms检查一次}
}
2. 定时器添加 / 删除机制
添加定时器(xTimerStart()):
计算超时时间(当前tick + 定时周期)。
将定时器按超时时间插入有序链表(保证头部为最近超时的定时器)。
若新定时器成为链表头部,唤醒定时器任务重新计算阻塞时间。
删除定时器(xTimerDelete()):
从活跃链表中移除定时器节点。
标记定时器为无效状态,防止重复操作。
4.两套API
FreeRTOS 设计两套 API 的核心目标是保证中断处理的实时性和系统稳定性:
任务上下文 API:面向普通任务场景,允许阻塞和直接调度,简化编程。
中断安全 API:面向中断场景,避免长时间关中断,通过参数间接控制调度,确保中断快速响应
对比维度 | 任务上下文 API(常规版本) | 中断安全 API(FromISR 版本) |
---|---|---|
命名规则 | 无特殊后缀(如 xQueueSend 、vTaskDelay ) | 后缀为 FromISR (如 xQueueSendFromISR 、vTaskDelayFromISR ) |
调用场景 | 仅能在任务函数中调用,禁止在中断服务程序(ISR)中使用 | 专门用于 ISR 或异常处理程序,可在中断环境中安全调用 |
中断状态处理 | 无需显式处理中断状态(任务上下文默认开中断) | 需保存并恢复中断状态(如 portSET_INTERRUPT_MASK_FROM_ISR ),避免破坏 ISR 上下文 |
上下文切换触发 | 直接调用内核调度器(如 taskYIELD ),主动触发切换 | 通过 pxHigherPriorityTaskWoken 参数标记是否需切换,由 ISR 决定是否调用 portYIELD_FROM_ISR |
关键参数差异 | 常规参数(如队列句柄、数据指针、超时时间) | 部分函数增加 BaseType_t* pxHigherPriorityTaskWoken 参数,用于标记高优先级任务是否被唤醒 |
临界区保护机制 | 使用 taskENTER_CRITICAL() 和 taskEXIT_CRITICAL() | 使用 portSET_INTERRUPT_MASK_FROM_ISR() 和 portCLEAR_INTERRUPT_MASK_FROM_ISR() ,适配中断环境 |
返回值含义 | 直接返回操作结果(如 pdPASS 、pdFAIL 、pdTRUE ) | 除返回操作结果外,需通过 pxHigherPriorityTaskWoken 间接传递调度需求 |
阻塞特性 | 支持阻塞(如等待队列或信号量时可指定超时时间) | 不支持阻塞(中断场景需快速返回,超时参数无效或被忽略) |
典型函数示例 | xQueueSend 、xSemaphoreTake 、vTaskDelay 、vTaskSuspend | xQueueSendFromISR 、xSemaphoreTakeFromISR 、vTaskDelayFromISR 、vTaskSuspendFromISR |
内核调度介入方式 | 函数内部主动触发调度(如任务切换) | 需由 ISR 根据函数返回结果决定是否触发调度 |
对系统实时性的影响 | 可能因阻塞操作降低实时性(任务上下文允许) | 无阻塞操作,中断处理更高效,保证系统实时响应 |
FreeRTOS 在中断中检测到高优先级任务就绪时,会立即请求切换,但实际切换发生在中断返回后的安全时机(不是立即切换)。这一设计在保证中断处理完整性的同时,确保高优先级任务以最小延迟获得 CPU 资源
中断API切换高优先级任务流程:
5.FreeRTOS里的两类中断
特性 | 系统中断 | 用户中断 |
---|---|---|
优先级范围 | 高优先级(通常 0~4) | 低优先级(通常 5~15,取决于配置) |
是否可调度 | 否(不可抢占其他系统中断) | 是(可被系统中断抢占) |
能否调用 FreeRTOS API | 仅能调用带FromISR 后缀的 API | 可调用所有 API(需注意上下文) |
对任务调度的影响 | 可能直接触发任务切换(如 PendSV、SysTick) | 通过pxHigherPriorityTaskWoken 标记间接触发切换 |
典型示例 | PendSV、SysTick、HardFault、NMI | 外设中断(如 UART、GPIO、定时器) |
显然在 FreeRTOS 中,任务调度器依赖的定时器中断(通常是 SysTick 中断 )和 PendSV 中断,优先级配置是刻意设计为低优先级,当我们关闭中断时,任务也不再调度