一、先搞懂两个核心角色:FreeRTOS和蓝牙模块
很多做嵌入式开发的朋友,尤其是刚接触的新手,一听到“FreeRTOS”和“蓝牙”就发懵,觉得这俩东西凑一起肯定特别难。其实咱们先把这俩“角色”拆明白,就没那么可怕了。
1.1 FreeRTOS到底是啥?
简单说,FreeRTOS就是嵌入式设备的“小管家”。比如你做一个带蓝牙的温湿度传感器,这个设备要干好几件事:隔1秒读一次温度、隔0.5秒读一次湿度、还要随时等着蓝牙发命令、还要把读好的数据通过蓝牙发出去。要是没有FreeRTOS,你就得自己写一堆逻辑去安排这些事的先后顺序,很容易乱套——比如读温度的时候,蓝牙刚好发命令过来,你就处理不了;或者读湿度的时间被耽误了,数据不准。
FreeRTOS的作用就是把这些事分成一个个“任务”,给每个任务安排优先级,比如蓝牙处理命令的优先级比读温度高,设备就会先处理蓝牙的命令,再回来读温度,完全不用你自己瞎操心顺序。
1.2 蓝牙模块的基本逻辑
蓝牙模块其实就是个“中转盒子”,它自己不会主动干别的,只会听你发的命令,然后做对应的事。比如你给它发“AT+NAME=我的温湿度”,它就把自己的蓝牙名字改成这个;发“AT+SEND=123”,它就把“123”通过蓝牙发出去;要是有别的设备连它,它还会主动给你发个“CONNECTED”的消息。
咱们常用的蓝牙模块(比如HC-05、HC-06),都是通过串口(UART)和主芯片(比如STM32)通信的——主芯片发命令给蓝牙,蓝牙把收到的内容(比如手机发的命令)通过串口传给主芯片。
二、核心技术要点:怎么把俩东西凑一起?
把FreeRTOS和蓝牙模块结合,最核心的就是解决“谁来管蓝牙”的问题——不能让FreeRTOS的多个任务随便往蓝牙发命令,也不能让蓝牙收到的消息随便乱传。这里的关键是两个:任务划分和通信机制。
2.1 任务怎么划分才合理?
一般我们会给蓝牙单独开一个任务,专门管和蓝牙模块的所有交互,别的任务要和蓝牙打交道,只能通过这个“专属任务”来。举个例子,比如你有三个任务:读温度、读湿度、处理蓝牙命令,那么读温度和读湿度的任务要发数据给手机,就只能把数据交给蓝牙专属任务,让它去发;蓝牙收到手机的命令,蓝牙专属任务再把命令传给处理蓝牙命令的任务。
这么划分的好处是:所有和蓝牙相关的逻辑都集中在一个任务里,不会乱。比如你发命令的时候,不会有两个任务同时往串口发,导致蓝牙模块收不到正确的命令。
2.2 任务之间怎么传数据?
FreeRTOS里有个专门的工具叫“队列”,就像一个带顺序的快递柜,一个任务往里面放数据,另一个任务从里面拿数据,完全不用管对方是不是在忙。比如读温度的任务把温度数据放进队列,蓝牙专属任务就从队列里拿出来,发给手机;蓝牙专属任务把收到的手机命令放进另一个队列,处理命令的任务就从这个队列拿命令处理。
三、完整示例:带蓝牙的温湿度传感器
咱们拿STM32F103C8T6(俗称“蓝板”)、HC-05蓝牙模块、DHT11温湿度传感器举个完整的例子,所有代码都是可运行的,咱们一步步拆。
首先明确技术栈:STM32F103C8T6 + FreeRTOS V10.4.6 + HC-05蓝牙模块 + DHT11温湿度传感器
3.1 先搭基础:串口配置
蓝牙模块是通过串口和主芯片通信的,所以第一步要配置串口。这里的串口用的是USART1,波特率9600(HC-05默认的波特率),8位数据位,1位停止位,无校验。
// 串口初始化函数
void USART1_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
// 使能GPIOA和USART1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);
// 配置PA9为TX(输出)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置PA10为RX(输入)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置USART1参数
USART_InitStructure.USART_BaudRate = 9600;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure);
// 使能USART1
USART_Cmd(USART1, ENABLE);
}
3.2 定义队列:任务之间传数据的“快递柜”
咱们需要两个队列:一个用来放要发的蓝牙数据(比如温度、湿度),一个用来放蓝牙收到的命令(比如手机发的“读温度”)。
// 定义队列句柄
QueueHandle_t BLE_SendQueue; // 要发的蓝牙数据队列
QueueHandle_t BLE_RecvQueue; // 收到的蓝牙命令队列
// 队列初始化函数
void Queue_Init(void)
{
// 创建要发的队列:最多存10条数据,每条数据是32字节(比如"Temp:25.0C,Hum:60.0%")
BLE_SendQueue = xQueueCreate(10, 32);
// 创建收到的队列:最多存10条命令,每条命令是16字节(比如"READ_TEMP")
BLE_RecvQueue = xQueueCreate(10, 16);
}
3.3 蓝牙专属任务:专门管蓝牙的“管家”
这个任务只干两件事:一是从“要发的队列”拿数据,发给蓝牙模块;二是从蓝牙模块收数据,放进“收到的队列”。
// 蓝牙专属任务
void BLE_Task(void *pvParameters)
{
char recv_buf[16]; // 存收到的蓝牙命令
char send_buf[32]; // 存要发的蓝牙数据
while(1)
{
// 1. 检查有没有要发的数据:从队列拿,最多等10ms(10ms没拿到就继续循环)
if(xQueueReceive(BLE_SendQueue, send_buf, pdMS_TO_TICKS(10)) == pdPASS)
{
// 把数据发给蓝牙模块:先加AT命令前缀,再加回车换行(HC-05的命令要求)
USART1_SendString("AT+SEND=");
USART1_SendString(send_buf);
USART1_SendString("\r\n");
}
// 2. 检查蓝牙有没有发数据过来:从串口读
if(USART1_ReceiveString(recv_buf, sizeof(recv_buf)) == pdPASS)
{
// 把收到的命令放进队列
xQueueSend(BLE_RecvQueue, recv_buf, pdMS_TO_TICKS(10));
}
// 任务延时10ms,避免占用太多CPU
vTaskDelay(pdMS_TO_TICKS(10));
}
}
3.4 温湿度采集任务:读传感器的数据
这个任务每隔1秒读一次DHT11的温湿度,然后把数据放进“要发的队列”,让蓝牙任务发出去。
// 温湿度采集任务
void DHT11_Task(void *pvParameters)
{
char data_buf[32]; // 存温湿度数据
while(1)
{
// 读DHT11的温湿度(这里的DHT11_Read是自己写的传感器驱动,就不展开了)
float temp = DHT11_ReadTemp();
float hum = DHT11_ReadHum();
// 把数据格式化,比如"Temp:25.0C,Hum:60.0%"
sprintf(data_buf, "Temp:%.1fC,Hum:%.1f%%", temp, hum);
// 把数据放进要发的队列,最多等10ms(10ms没放进去就继续循环)
xQueueSend(BLE_SendQueue, data_buf, pdMS_TO_TICKS(10));
// 任务延时1秒,每隔1秒读一次
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
3.5 命令处理任务:处理手机发的命令
这个任务从“收到的队列”拿命令,比如手机发“READ_TEMP”,就把温度发出去;发“READ_HUM”,就把湿度发出去。
// 命令处理任务
void Command_Task(void *pvParameters)
{
char cmd_buf[16]; // 存收到的命令
char data_buf[32]; // 存要发的响应数据
while(1)
{
// 从收到的队列拿命令,最多等10ms
if(xQueueReceive(BLE_RecvQueue, cmd_buf, pdMS_TO_TICKS(10)) == pdPASS)
{
// 处理命令:如果是"READ_TEMP"
if(strcmp(cmd_buf, "READ_TEMP") == 0)
{
float temp = DHT11_ReadTemp();
sprintf(data_buf, "Temp:%.1fC", temp);
xQueueSend(BLE_SendQueue, data_buf, pdMS_TO_TICKS(10));
}
// 如果是"READ_HUM"
else if(strcmp(cmd_buf, "READ_HUM") == 0)
{
float hum = DHT11_ReadHum();
sprintf(data_buf, "Hum:%.1f%%", hum);
xQueueSend(BLE_SendQueue, data_buf, pdMS_TO_TICKS(10));
}
}
// 任务延时10ms
vTaskDelay(pdMS_TO_TICKS(10));
}
}
3.6 主函数:启动所有任务
主函数里先初始化硬件,再初始化队列,最后启动FreeRTOS的调度器,所有任务就会自动运行了。
int main(void)
{
// 初始化硬件:串口、DHT11
USART1_Init();
DHT11_Init();
// 初始化队列
Queue_Init();
// 创建任务:蓝牙任务优先级最高(3),温湿度任务次之(2),命令处理任务(1)
xTaskCreate(BLE_Task, "BLE_Task", 128, NULL, 3, NULL);
xTaskCreate(DHT11_Task, "DHT11_Task", 128, NULL, 2, NULL);
xTaskCreate(Command_Task, "Command_Task", 128, NULL, 1, NULL);
// 启动FreeRTOS调度器,开始运行任务
vTaskStartScheduler();
// 调度器启动后不会走到这里
while(1);
}
四、常见问题及解决方法
实际开发中,肯定会遇到各种问题,咱们挑几个最常见的讲。
4.1 问题1:蓝牙模块收不到命令,或者发的数据乱码
这个是最常见的问题,原因一般有三个:
- 波特率不匹配:HC-05默认波特率是9600,要是你主芯片的串口波特率设成115200,肯定收不到。解决方法:检查主芯片和蓝牙模块的波特率,必须一致。
- 命令格式不对:HC-05的AT命令必须以回车换行(\r\n)结尾,要是你发命令的时候没加,蓝牙模块会认为命令没发完,不会处理。解决方法:发命令的时候一定要加\r\n。
- 多个任务同时发命令:要是你没开蓝牙专属任务,两个任务同时往串口发命令,数据就会混在一起,导致乱码。解决方法:必须用蓝牙专属任务,所有发命令的操作都通过它来做。
4.2 问题2:队列满了,数据丢了
比如温湿度任务每秒往队列放一次数据,要是队列最多存10条,过了10秒没处理,队列就满了,再放数据就会丢。解决方法:
- 加大队列的大小:比如把队列的最大条数改成20,这样能存更久的数据。
- 提高蓝牙任务的优先级:让蓝牙任务处理数据的速度更快,队列就不容易满。
- 给队列加超时:比如放数据的时候等50ms,要是50ms队列还没位置,就先放旧数据,保证新数据能放进去。
4.3 问题3:蓝牙断开后,设备卡死
要是手机和蓝牙模块断开连接,有些蓝牙模块会停止工作,要是你的代码没处理这个情况,就会卡死。解决方法:
- 给蓝牙任务加一个“连接检查”:每隔1秒给蓝牙模块发一个“AT+LINK”命令,要是返回“OK”就是连接正常,返回“ERROR”就是断开了。
- 要是断开了,就把队列里的旧数据清空,或者暂停温湿度任务,等重新连接后再继续。
五、技术优缺点分析
5.1 优点
- 稳定性高:FreeRTOS的任务调度机制,保证了蓝牙通信不会被其他任务干扰,不会出现“顾此失彼”的情况。
- 扩展性好:要是你以后要加新功能(比如加个按键、加个OLED屏幕),只要开新的任务就行,不用改原来的蓝牙逻辑。
- 开发效率高:队列、任务这些工具都是FreeRTOS自带的,不用自己写复杂的逻辑,省时间。
5.2 缺点
- 资源占用多:FreeRTOS本身要占用一定的内存和CPU资源,要是你的主芯片性能很差(比如8位的51单片机),可能跑不起来。
- 学习成本:要是你之前没接触过FreeRTOS,得先花时间学任务、队列这些概念,比直接写裸机代码麻烦。
六、应用场景
这种架构适合所有需要同时处理多个任务的嵌入式蓝牙设备,比如:
- 环境监测设备:温湿度、PM2.5传感器,需要定时采集数据,还要通过蓝牙传给手机。
- 智能家居设备:智能灯、智能插座,需要通过蓝牙接收手机的控制命令,还要反馈设备状态。
- 工业控制设备:比如工业传感器,需要把数据通过蓝牙传给上位机,还要处理上位机的控制命令。
七、注意事项
- 任务优先级的设置:紧急的任务(比如蓝牙处理命令)优先级要高,不紧急的任务(比如定时采集数据)优先级要低,避免重要任务被耽误。
- 队列的大小:队列不能太小(容易满),也不能太大(浪费内存),要根据实际情况调整。
- 串口的中断处理:要是你用串口中断收数据,一定要注意中断里不能做太多事,不然会影响FreeRTOS的调度,最好把收到的数据放进缓冲区,再让蓝牙任务去处理。
八、文章总结
把FreeRTOS和蓝牙模块结合,核心就是“分工明确”:给蓝牙单独开一个任务,专门管和蓝牙模块的交互,其他任务通过队列和它通信。这么做既能保证蓝牙通信的稳定性,又能让整个系统的逻辑更清晰,扩展性更好。只要把任务划分和队列的用法搞懂,再避开常见的坑,就能顺利完成集成。
评论
围绕“FreeRTOS与蓝牙通信模块集成的技术要点与问题解决”参与讨论