3、FreeRTOS基础:任务调度、队列、信号量、互斥锁、软件定时器、内存管理,以及中断安全API

在将BLE协议栈(如Zephyr、Nordic SoftDevice或TI BLE-Stack)移植到FreeRTOS之前,必须深刻理解FreeRTOS的内核机制。BLE协议栈的HCI层、L2CAP层、GATT层以及射频驱动对实时性、资源互斥和内存分配有严苛要求。本章将系统梳理FreeRTOS的六大核心机制——任务调度、队列、信号量、互斥锁、软件定时器、内存管理,并重点剖析中断安全API的使用边界。这些知识点是后续移植工作中配置中断优先级、设计协议栈任务模型、规避死锁与内存碎片问题的理论基石。

FreeRTOS 核心机制与 BLE 移植映射 FreeRTOS 内核 任务调度器 + 内核对象 任务调度 优先级抢占 + 时间片轮转 BLE: HCI_Task / GATT_Task 队列 任务间数据传递 (HCI事件) BLE: RX_Queue / TX_Queue 信号量 二值/计数同步 BLE: 广播事件同步 互斥锁 优先级继承防翻转 BLE: Flash/EEPROM 访问 软件定时器 周期/单次回调 BLE: 连接间隔管理 内存管理 heap_1~heap_5 策略 BLE: GATT 数据库分配 中断安全API: FromISR 系列 xQueueSendFromISR / xSemaphoreGiveFromISR 图:FreeRTOS 六大机制与 BLE 协议栈移植的典型映射关系

3.1 任务调度:抢占式优先级与时间片轮转

核心原理

FreeRTOS 默认采用抢占式优先级调度(Preemptive Scheduling),配合可选的时间片轮转(Round-Robin)机制。每个任务被赋予 0~configMAX_PRIORITIES-1 的优先级,数值越大优先级越高。调度器保证最高优先级的就绪任务始终获得 CPU 使用权。当多个同优先级任务就绪时,若 configUSE_TIME_SLICING 置 1,则每个任务运行一个时间片(由 tick 中断驱动)后轮转。

在 BLE 协议栈移植中,任务优先级的分配直接影响射频时序。例如 Nordic SoftDevice 的 radio 中断优先级最高,而协议栈处理任务(如 SWI 中断)应高于应用任务,但低于射频中断。FreeRTOS 的优先级数值与 ARM Cortex-M 中断优先级(数值越小优先级越高)是相反逻辑,移植时必须做映射转换。

判断方法与操作流程

  1. 确定任务集合:列出 BLE 协议栈所需的全部任务(HCI 处理、GATT 服务、连接管理、应用主逻辑)。
  2. 分配优先级:射频事件相关任务(如连接事件处理)设为最高优先级;协议栈内部处理次之;应用任务最低。
  3. 配置时间片:若两个任务同优先级且无强实时要求,可开启时间片轮转;但 BLE 连接事件处理任务建议独占优先级,避免被同优先级任务抢占。
  4. 验证调度延迟:使用 vTaskDelay 或 vTaskDelayUntil 控制任务周期,通过 trace 工具(如 FreeRTOS+Trace)测量任务切换时间。

案例拆解:BLE 连接事件处理任务优先级设计

某 BLE 外设项目使用 nRF52832,连接间隔为 7.5ms。射频中断(RADIO_IRQHandler)在中断上下文完成收包,随后触发 SWI 中断唤醒协议栈任务。若协议栈任务优先级低于应用任务,应用任务的长循环(如 Flash 擦写)会阻塞协议栈处理,导致连接超时。实际配置中,将协议栈任务优先级设为 3(最高),应用任务设为 1,中间留空优先级 2 给日志或调试任务。通过逻辑分析仪抓取连接事件窗口,确认协议栈任务在射频中断后 50μs 内被调度,满足协议栈时序要求。

常见误区:不要将所有任务都设为最高优先级。FreeRTOS 调度器在最高优先级任务就绪时,低优先级任务完全得不到执行。若 BLE 协议栈任务与应用任务同优先级且未开启时间片,应用任务可能饿死协议栈任务。建议保留至少一个空闲任务(IDLE)用于系统资源回收。

3.2 队列:任务间数据传递的管道

核心原理

队列是 FreeRTOS 任务间通信的主要机制,采用环形缓冲区实现,支持 FIFO(先进先出)或 LIFO(后进先出)模式。队列存储的是数据副本而非指针,因此适合传递小尺寸结构体(如 HCI 事件包)。队列操作包括 xQueueSend(入队)、xQueueReceive(出队)、xQueuePeek(窥视)。队列满时,发送任务可阻塞或立即返回;队列空时,接收任务可阻塞等待。

在 BLE 移植中,队列常用于:HCI 层向主机发送事件包、GATT 层向应用层上报通知/指示、射频驱动向协议栈任务传递原始数据包。

操作流程与参数选择

参数推荐值说明
队列长度4~16根据 BLE 事件频率和消费速度决定,过短导致丢包,过长浪费 RAM
队列项大小sizeof(Event_t)建议使用结构体而非指针,避免悬垂指针
阻塞时间portMAX_DELAY 或 100ms接收任务通常阻塞等待,发送任务在中断中用 FromISR 版本

实战场景:HCI 事件分发

BLE 协议栈收到 ACL 数据后,需要将 HCI 事件(如 LE Connection Complete)发送给主机任务。定义结构体 HCI_Event_t 包含事件码、长度、参数数组。主机任务调用 xQueueReceive(hciQueue, &event, portMAX_DELAY) 阻塞等待。若协议栈在中断上下文中产生事件,则调用 xQueueSendFromISR(hciQueue, &event, &xHigherPriorityTaskWoken),并在中断退出后检查该变量以触发上下文切换。

// 中断中发送 HCI 事件
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendFromISR(hciQueue, &event, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
经验提醒:队列项大小不宜超过 256 字节。若需传递大数据(如 GATT 长属性),建议队列传递指针,但需确保指针指向的内存生命周期有效,且使用互斥锁保护共享缓冲区。

3.3 信号量:二值、计数与互斥的边界

核心原理

FreeRTOS 信号量分为二值信号量(Binary Semaphore)和计数信号量(Counting Semaphore)。二值信号量用于任务同步(如 ISR 通知任务),计数信号量用于资源计数(如可用缓冲区数量)。信号量操作包括 xSemaphoreTake(获取)和 xSemaphoreGive(释放)。二值信号量在任务间同步时,若 ISR 连续多次 Give,任务只会 Take 一次,存在事件丢失风险。

在 BLE 移植中,二值信号量常用于:射频中断唤醒协议栈任务、UART DMA 接收完成通知。计数信号量可用于管理 TX 缓冲区池,每次分配缓冲区时 Take,释放时 Give。

判断方法:何时用信号量 vs 队列

  • 若只需通知“事件发生”而不传递数据,用二值信号量。
  • 若需传递事件的具体内容(如 HCI 包),用队列。
  • 若需统计可用资源数量(如内存块),用计数信号量。

案例拆解:广播事件同步

BLE 广播间隔为 100ms,射频硬件在每次广播发送完成后触发中断。中断中调用 xSemaphoreGiveFromISR(advSemaphore, &xHigherPriorityTaskWoken)。广播任务调用 xSemaphoreTake(advSemaphore, 100ms) 等待。若中断在任务 Take 之前连续触发多次,信号量计数值最多为 1,任务只会处理一次广播完成事件。若需处理每次广播,应使用计数信号量并设置最大计数值为 1,或改用队列传递广播序号。

关键区别:互斥锁(Mutex)与二值信号量在实现上相似,但互斥锁支持优先级继承(Priority Inheritance),可防止高优先级任务被低优先级任务持有的锁阻塞时发生优先级翻转。信号量不具备该特性,因此禁止用二值信号量替代互斥锁保护共享资源。

3.4 互斥锁:保护共享资源与优先级继承

核心原理

互斥锁(Mutex)是特殊的二值信号量,具有所有权概念:只有获取锁的任务才能释放锁。其核心优势是优先级继承:当高优先级任务尝试获取被低优先级任务持有的锁时,低优先级任务会临时提升到高优先级任务的优先级,直到释放锁。这避免了高优先级任务因等待锁而被中优先级任务抢占的“优先级翻转”问题。

在 BLE 移植中,互斥锁用于保护:Flash 读写操作(如 GATT 数据库持久化)、共享 I2C/SPI 总线(如外部传感器)、全局状态变量(如连接参数更新标志)。

操作流程与注意事项

  1. 创建互斥锁:xSemaphoreCreateMutex()。
  2. 在任务上下文中获取:xSemaphoreTake(mutex, timeout)。
  3. 释放:xSemaphoreGive(mutex)。注意:不能在 ISR 中使用互斥锁,因为 ISR 无法阻塞。
  4. 若需在中断中保护资源,应使用二值信号量或直接关闭中断(临界区)。

实战场景:Flash 写入互斥

BLE 设备需要将配对信息写入 Flash。应用任务和 GATT 服务任务都可能触发写入。若不加锁,两个任务同时调用 Flash 写函数会导致数据损坏。使用互斥锁后,低优先级应用任务持有锁时,高优先级 GATT 任务等待,应用任务优先级被临时提升,快速完成写入并释放锁,GATT 任务随即获得锁。实测写入耗时从无锁时的 15ms(含冲突重试)降至 8ms,且无数据损坏。

风险提示:互斥锁使用不当会导致死锁。例如任务 A 持有锁 1 等待锁 2,任务 B 持有锁 2 等待锁 1。务必遵循“按固定顺序获取锁”或使用 xSemaphoreTake 的超时参数(如 100ms)避免永久阻塞。

3.5 软件定时器:周期回调与守护任务

核心原理

FreeRTOS 软件定时器由定时器守护任务(Timer Service Task)统一管理。定时器回调函数在守护任务上下文中执行,而非在调用 xTimerStart 的任务中执行。因此回调函数中不能调用阻塞 API(如 vTaskDelay、xQueueReceive 带阻塞时间)。定时器支持单次(One-shot)和周期(Auto-reload)模式。

在 BLE 移植中,软件定时器用于:连接参数更新请求延迟、广播周期管理、GATT 服务端通知节流、看门狗喂狗超时监控。

配置参数与操作流程

参数典型值说明
定时器周期10ms ~ 1000ms取决于 BLE 事件粒度,最小建议 10ms 以上
守护任务栈大小256 字若回调函数复杂,需增大栈空间
定时器数量5~10由 configTIMER_QUEUE_LENGTH 和 configTIMER_TASK_STACK_DEPTH 决定
// 创建周期定时器,每 100ms 触发一次
TimerHandle_t xTimer = xTimerCreate("adv_timer", pdMS_TO_TICKS(100), pdTRUE, NULL, vAdvTimerCallback);
xTimerStart(xTimer, 0);

案例拆解:连接参数更新延迟

BLE 从机请求更新连接参数(如延长连接间隔)时,主机可能拒绝。为避免频繁请求,从机使用单次软件定时器,在收到拒绝后 30 秒再发起请求。定时器回调中设置标志位,由应用任务检查标志并重新发起请求。注意回调中不能直接调用协议栈 API(可能阻塞),只能置标志或发送队列消息。

经验提醒:若定时器回调需要执行较长时间(如超过 1ms),应将其拆分为“置标志 + 唤醒任务”模式,由任务完成实际工作。否则会阻塞定时器守护任务,影响其他定时器的准时触发。

3.6 内存管理:heap_1 到 heap_5 的选择

核心原理

FreeRTOS 提供 5 种内存管理方案(heap_1 ~ heap_5),各有优劣。heap_1 最简单,只分配不释放,适合永不删除任务的场景;heap_2 支持释放但不合并相邻空闲块,易产生碎片;heap_3 封装标准 malloc/free,线程安全但依赖编译器;heap_4 合并相邻空闲块,最常用;heap_5 支持多段不连续内存,适合外部 RAM。

在 BLE 移植中,GATT 数据库、L2CAP 缓冲区、HCI 命令包都需要动态分配。若使用 heap_2,频繁创建/删除 GATT 服务会导致碎片累积,最终分配失败。推荐使用 heap_4。

选择判断流程

  1. 若系统运行期间不删除任何任务/队列/信号量,选 heap_1。
  2. 若需删除对象且内存为单段连续 RAM,选 heap_4。
  3. 若使用外部 SDRAM 且地址不连续,选 heap_5。
  4. 若需完全控制分配策略(如内存池),可自定义 heap 实现。

实战场景:GATT 数据库动态分配

某设备支持动态添加/删除自定义 GATT 服务。使用 heap_2 时,连续添加/删除 50 次后,xPortGetFreeHeapSize() 显示剩余 1.2KB,但实际最大可分配块仅 200 字节,导致服务添加失败。切换至 heap_4 后,相邻空闲块自动合并,最大可分配块恢复至 1.1KB,问题解决。

风险提示:heap_4 的分配时间不确定(可能触发块合并),在实时性要求极高的射频中断中禁止调用 pvPortMalloc。应在任务上下文中预分配缓冲区,或使用内存池(如 FreeRTOS+MPU 的静态分配)。

3.7 中断安全API:FromISR 系列与临界区

核心原理

FreeRTOS 提供后缀为 FromISR 的 API 变体,用于在中断服务函数中安全操作内核对象。这些 API 不会阻塞,且通过 pxHigherPriorityTaskWoken 参数告知调度器是否需要上下文切换。常用包括:xQueueSendFromISR、xSemaphoreGiveFromISR、xTaskNotifyFromISR、vTaskDelayUntil(不适用)。

在 BLE 移植中,射频中断、UART 接收中断、定时器中断都可能调用这些 API。必须确保中断优先级低于 configMAX_SYSCALL_INTERRUPT_PRIORITY,否则内核无法安全处理。

操作流程与注意事项

  • 在中断中只调用 FromISR 版本 API,禁止调用普通版本。
  • 每次调用后检查 xHigherPriorityTaskWoken,若为 pdTRUE,在中断退出前调用 portYIELD_FROM_ISR()。
  • 临界区保护:使用 taskENTER_CRITICAL() / taskEXIT_CRITICAL() 保护共享变量,但临界区中不能调用任何 FreeRTOS API。
// UART 接收中断示例
void UART_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    uint8_t byte = UART->DATA;
    xQueueSendFromISR(rxQueue, &byte, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

案例拆解:射频中断与任务同步

nRF52 的 RADIO 中断在收到 BLE 包后触发。中断中解析包头,将数据存入预分配缓冲区,然后调用 xSemaphoreGiveFromISR(radioSem, &xHigherPriorityTaskWoken)。协议栈任务被唤醒后处理数据。实测中断到任务开始处理的时间为 12μs,满足 BLE 协议栈 150μs 的时序要求。若在中断中调用普通 xSemaphoreGive,会导致断言失败(configASSERT 触发),系统崩溃。

总结复盘:FreeRTOS 的六大机制在 BLE 移植中各有明确分工:任务调度决定实时性,队列传递 HCI 事件,信号量同步射频中断,互斥锁保护 Flash/总线,软件定时器管理连接参数,内存管理策略影响长期稳定性。中断安全 API 是连接硬件中断与任务世界的桥梁。掌握这些基础后,下一阶段将进入 FreeRTOS 与 BLE 协议栈的集成层设计,包括中断优先级映射、任务栈深度估算和内存池规划。