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数据包从天线到应用层的完整流程:
- 硬件中断:射频模块收到数据包,触发RADIO_IRQHandler
- 上半部处理:ISR里读取数据到缓冲区,释放信号量通知链路层任务
- 链路层处理:链路层任务被信号量唤醒,解析数据包,进行CRC校验、解密等
- 队列传递:链路层把解析后的数据封装成消息,发送到HCI队列
- HCI处理:HCI任务从队列取出消息,组装成HCI事件
- 应用回调:HCI事件通过另一个队列传递给应用层任务
- 应用处理:应用层任务收到数据,调用用户回调函数
你看,每一层都是通过信号量或队列来同步和通信的。这样做的好处是:每一层都可以独立开发和测试,出了问题也容易定位。
调试建议:在队列的发送和接收处加一些统计信息,比如队列当前使用量、最大使用量、溢出次数。这些数据能帮你快速定位性能瓶颈。我习惯在串口调试工具里开一个页面专门显示这些统计信息。
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协议栈移植中最容易出问题的地方,但也是最值得花时间优化的地方。下一章咱们会讲内存管理——这也是个容易踩坑的点,到时候我会分享一些我踩过的坑和解决方案。