4、RTOS集成:FreeRTOS任务创建与调度、中断管理、信号量与队列在BLE协议栈中的应用

好,咱们进入第四章。这一章我估计是很多人移植BLE协议栈时最头疼的部分——RTOS集成。说白了,BLE协议栈本身是个复杂的状态机,它需要被“塞进”一个实时操作系统里跑。FreeRTOS是嵌入式领域最常见的选择,咱们今天就把它讲透。

4.1 为什么BLE协议栈需要RTOS?

你想想看,BLE协议栈要处理的事情太多了:广播、扫描、连接、数据收发、加密、重传……如果全放在一个超级循环里,稍微一个阻塞操作,整个协议栈就卡死了。我早期做项目时,就吃过这个亏——一个中断处理时间稍长,连接直接断掉。

所以,我们需要RTOS来帮忙做三件事:

  • 任务调度:把协议栈的不同功能拆成独立任务,让CPU合理分配时间
  • 中断管理:硬件事件来了,快速响应,然后把耗时处理交给任务
  • 同步与通信:任务之间、中断与任务之间,需要安全地传递数据

核心原则:中断里只做最紧急的事(比如读寄存器、清标志),耗时操作全部推到任务里做。这是嵌入式实时系统的黄金法则。

4.2 FreeRTOS任务创建与调度

BLE协议栈通常需要至少三个任务:

任务名称 优先级 栈大小(建议) 职责
BLE_LL_Task 最高 512-1024字 链路层处理,时序敏感
BLE_HCI_Task 512字 HCI命令与事件处理
BLE_APP_Task 1024字 应用层回调与业务逻辑

我个人习惯把链路层任务优先级设得最高,因为BLE的时序窗口非常窄——连接间隔可能只有7.5ms,错过一个窗口就可能丢包。我曾经在一个项目中,因为优先级设反了,导致连接频繁断开,查了两天才发现是任务调度的问题。

创建任务的代码示例:

// 创建BLE链路层任务
xTaskCreate(
    BLE_LL_Task_Entry,    // 任务函数
    "BLE_LL",             // 任务名
    1024,                 // 栈深度(字)
    NULL,                 // 参数
    configMAX_PRIORITIES - 1,  // 最高优先级
    &xBLE_LL_Handle       // 任务句柄
);

// 创建HCI任务
xTaskCreate(
    BLE_HCI_Task_Entry,
    "BLE_HCI",
    512,
    NULL,
    configMAX_PRIORITIES - 2,
    &xBLE_HCI_Handle
);

// 创建应用任务
xTaskCreate(
    BLE_APP_Task_Entry,
    "BLE_APP",
    1024,
    NULL,
    configMAX_PRIORITIES - 3,
    &xBLE_APP_Handle
);

小技巧:栈大小别抠门。我见过太多人为了省RAM把栈设得很小,结果跑着跑着就栈溢出。建议先设大一点,跑稳定后用uxTaskGetStackHighWaterMark()查看实际使用量,再慢慢优化。

4.3 中断管理:上半部与下半部

BLE协议栈里,中断无处不在。射频收发完成、定时器触发、DMA传输结束……都会产生中断。但中断服务程序(ISR)里不能做复杂操作,这是铁律。

我的做法是:中断里只做“上半部”——读硬件状态、清中断标志、然后通过信号量或任务通知“叫醒”对应的任务。任务里做“下半部”——真正的数据处理。

举个例子,BLE链路层收到一个数据包:

// 中断服务程序(上半部)
void RADIO_IRQHandler(void)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    
    // 1. 读取接收数据到缓冲区
    BLE_LL_ReadPacket(rx_buffer);
    
    // 2. 清中断标志
    RADIO->EVENTS_END = 0;
    
    // 3. 通知链路层任务处理
    xSemaphoreGiveFromISR(
        xBLE_LL_Semaphore,
        &xHigherPriorityTaskWoken
    );
    
    // 4. 如果需要,触发任务切换
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// 任务函数(下半部)
void BLE_LL_Task_Entry(void *pvParameters)
{
    while(1)
    {
        // 等待信号量
        xSemaphoreTake(xBLE_LL_Semaphore, portMAX_DELAY);
        
        // 处理接收到的数据包
        BLE_LL_ProcessPacket(rx_buffer);
    }
}

注意:在ISR里调用FreeRTOS的API时,一定要用带FromISR后缀的版本。我刚开始移植时,直接在ISR里调了xSemaphoreGive(),结果系统直接崩溃——因为FreeRTOS在ISR和非ISR上下文中的API实现是不同的。

4.4 信号量:同步的利器

信号量在BLE协议栈里用得特别多。主要有两种场景:

  • 二值信号量:中断通知任务,比如“数据包到了,快来处理”
  • 计数信号量:管理资源池,比如“还有几个可用的缓冲区”

我记得有一次做多连接场景,同时管理6个BLE连接。每个连接都有自己的数据流,如果都用全局变量做标志,代码会乱成一锅粥。用信号量就清爽多了——每个连接对应一个信号量,谁的数据到了就释放谁的信号量。

// 创建信号量数组,每个连接一个
SemaphoreHandle_t xConnSemaphore[MAX_CONNECTIONS];

for(int i = 0; i < MAX_CONNECTIONS; i++)
{
    xConnSemaphore[i] = xSemaphoreCreateBinary();
}

// 中断中根据连接索引释放对应信号量
void RADIO_IRQHandler(uint8_t conn_idx)
{
    xSemaphoreGiveFromISR(xConnSemaphore[conn_idx], NULL);
}

// 任务中等待对应连接的信号量
void BLE_LL_Task_Entry(void *pvParameters)
{
    uint8_t conn_idx = (uint8_t)pvParameters;
    
    while(1)
    {
        xSemaphoreTake(xConnSemaphore[conn_idx], portMAX_DELAY);
        BLE_LL_ProcessConnection(conn_idx);
    }
}

4.5 队列:任务间通信的桥梁

信号量解决了“什么时候做”的问题,队列解决的是“做什么”的问题。在BLE协议栈里,队列用来传递命令、事件和数据包。

举个例子,应用层想发送一个数据包,不能直接操作射频硬件(那是链路层的事)。应用层把数据包封装成一个消息,扔进队列,链路层任务从队列里取出来处理。

// 定义消息结构
typedef struct {
    uint8_t type;      // 消息类型:连接请求、数据发送、断开连接等
    uint8_t conn_idx;  // 连接索引
    uint8_t *data;     // 数据指针
    uint16_t len;      // 数据长度
} BLE_Message_t;

// 创建队列
QueueHandle_t xBLE_Queue = xQueueCreate(10, sizeof(BLE_Message_t));

// 应用层发送消息
void BLE_SendData(uint8_t conn_idx, uint8_t *data, uint16_t len)
{
    BLE_Message_t msg;
    msg.type = MSG_DATA_SEND;
    msg.conn_idx = conn_idx;
    msg.data = data;
    msg.len = len;
    
    // 发送到队列
    xQueueSend(xBLE_Queue, &msg, 0);
}

// 链路层任务接收消息
void BLE_LL_Task_Entry(void *pvParameters)
{
    BLE_Message_t msg;
    
    while(1)
    {
        // 等待队列消息
        xQueueReceive(xBLE_Queue, &msg, portMAX_DELAY);
        
        switch(msg.type)
        {
            case MSG_DATA_SEND:
                BLE_LL_TransmitPacket(msg.conn_idx, msg.data, msg.len);
                break;
            // 其他消息类型...
        }
    }
}

经验之谈:队列深度要合理设置。太浅了容易丢消息,太深了浪费RAM。我一般先设成10,然后跑压力测试,观察队列满的次数。如果频繁出现队列满,就加大深度。

4.6 综合实战:一个完整的BLE接收流程

咱们把上面讲的东西串起来,看看一个BLE数据包从天线到应用层的完整流程:

  1. 硬件中断:射频模块收到数据包,触发RADIO_IRQHandler
  2. 上半部处理:ISR里读取数据到缓冲区,释放信号量通知链路层任务
  3. 链路层处理:链路层任务被信号量唤醒,解析数据包,进行CRC校验、解密等
  4. 队列传递:链路层把解析后的数据封装成消息,发送到HCI队列
  5. HCI处理:HCI任务从队列取出消息,组装成HCI事件
  6. 应用回调:HCI事件通过另一个队列传递给应用层任务
  7. 应用处理:应用层任务收到数据,调用用户回调函数

你看,每一层都是通过信号量或队列来同步和通信的。这样做的好处是:每一层都可以独立开发和测试,出了问题也容易定位。

调试建议:在队列的发送和接收处加一些统计信息,比如队列当前使用量、最大使用量、溢出次数。这些数据能帮你快速定位性能瓶颈。我习惯在串口调试工具里开一个页面专门显示这些统计信息。

4.7 常见坑与避坑指南

  • 优先级反转:低优先级任务持有信号量,高优先级任务等这个信号量,结果中优先级任务抢占了CPU。解决方案:使用互斥量(带优先级继承)代替二值信号量。
  • 中断嵌套:FreeRTOS默认中断嵌套是关闭的,如果BLE需要高优先级中断,记得在FreeRTOSConfig.h里配置configMAX_SYSCALL_INTERRUPT_PRIORITY。
  • 任务栈溢出:我曾经在一个项目中,应用层任务栈设了256字,结果跑了一周后突然死机。查了三天,发现是某个极端情况下递归调用导致栈溢出。从那以后,我每个任务都开了栈溢出检测钩子。
// 开启栈溢出检测
void vApplicationStackOverflowHook(
    TaskHandle_t xTask,
    char *pcTaskName
)
{
    // 记录错误信息,然后复位或进入安全模式
    log_error("Stack overflow in task: %s", pcTaskName);
    NVIC_SystemReset();
}

嗯,这一章的内容就到这里。RTOS集成是BLE协议栈移植中最容易出问题的地方,但也是最值得花时间优化的地方。下一章咱们会讲内存管理——这也是个容易踩坑的点,到时候我会分享一些我踩过的坑和解决方案。