3、FreeRTOS基础:任务调度、队列、信号量、互斥锁、软件定时器、内存管理,以及中断安全API
在将BLE协议栈(如Zephyr、Nordic SoftDevice或TI BLE-Stack)移植到FreeRTOS之前,必须深刻理解FreeRTOS的内核机制。BLE协议栈的HCI层、L2CAP层、GATT层以及射频驱动对实时性、资源互斥和内存分配有严苛要求。本章将系统梳理FreeRTOS的六大核心机制——任务调度、队列、信号量、互斥锁、软件定时器、内存管理,并重点剖析中断安全API的使用边界。这些知识点是后续移植工作中配置中断优先级、设计协议栈任务模型、规避死锁与内存碎片问题的理论基石。
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 中断优先级(数值越小优先级越高)是相反逻辑,移植时必须做映射转换。
判断方法与操作流程
- 确定任务集合:列出 BLE 协议栈所需的全部任务(HCI 处理、GATT 服务、连接管理、应用主逻辑)。
- 分配优先级:射频事件相关任务(如连接事件处理)设为最高优先级;协议栈内部处理次之;应用任务最低。
- 配置时间片:若两个任务同优先级且无强实时要求,可开启时间片轮转;但 BLE 连接事件处理任务建议独占优先级,避免被同优先级任务抢占。
- 验证调度延迟:使用 vTaskDelay 或 vTaskDelayUntil 控制任务周期,通过 trace 工具(如 FreeRTOS+Trace)测量任务切换时间。
案例拆解:BLE 连接事件处理任务优先级设计
某 BLE 外设项目使用 nRF52832,连接间隔为 7.5ms。射频中断(RADIO_IRQHandler)在中断上下文完成收包,随后触发 SWI 中断唤醒协议栈任务。若协议栈任务优先级低于应用任务,应用任务的长循环(如 Flash 擦写)会阻塞协议栈处理,导致连接超时。实际配置中,将协议栈任务优先级设为 3(最高),应用任务设为 1,中间留空优先级 2 给日志或调试任务。通过逻辑分析仪抓取连接事件窗口,确认协议栈任务在射频中断后 50μs 内被调度,满足协议栈时序要求。
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);
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,或改用队列传递广播序号。
3.4 互斥锁:保护共享资源与优先级继承
核心原理
互斥锁(Mutex)是特殊的二值信号量,具有所有权概念:只有获取锁的任务才能释放锁。其核心优势是优先级继承:当高优先级任务尝试获取被低优先级任务持有的锁时,低优先级任务会临时提升到高优先级任务的优先级,直到释放锁。这避免了高优先级任务因等待锁而被中优先级任务抢占的“优先级翻转”问题。
在 BLE 移植中,互斥锁用于保护:Flash 读写操作(如 GATT 数据库持久化)、共享 I2C/SPI 总线(如外部传感器)、全局状态变量(如连接参数更新标志)。
操作流程与注意事项
- 创建互斥锁:
xSemaphoreCreateMutex()。 - 在任务上下文中获取:
xSemaphoreTake(mutex, timeout)。 - 释放:
xSemaphoreGive(mutex)。注意:不能在 ISR 中使用互斥锁,因为 ISR 无法阻塞。 - 若需在中断中保护资源,应使用二值信号量或直接关闭中断(临界区)。
实战场景:Flash 写入互斥
BLE 设备需要将配对信息写入 Flash。应用任务和 GATT 服务任务都可能触发写入。若不加锁,两个任务同时调用 Flash 写函数会导致数据损坏。使用互斥锁后,低优先级应用任务持有锁时,高优先级 GATT 任务等待,应用任务优先级被临时提升,快速完成写入并释放锁,GATT 任务随即获得锁。实测写入耗时从无锁时的 15ms(含冲突重试)降至 8ms,且无数据损坏。
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(可能阻塞),只能置标志或发送队列消息。
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。
选择判断流程
- 若系统运行期间不删除任何任务/队列/信号量,选 heap_1。
- 若需删除对象且内存为单段连续 RAM,选 heap_4。
- 若使用外部 SDRAM 且地址不连续,选 heap_5。
- 若需完全控制分配策略(如内存池),可自定义 heap 实现。
实战场景:GATT 数据库动态分配
某设备支持动态添加/删除自定义 GATT 服务。使用 heap_2 时,连续添加/删除 50 次后,xPortGetFreeHeapSize() 显示剩余 1.2KB,但实际最大可分配块仅 200 字节,导致服务添加失败。切换至 heap_4 后,相邻空闲块自动合并,最大可分配块恢复至 1.1KB,问题解决。
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 触发),系统崩溃。