一、先搞懂两个核心角色: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:蓝牙模块收不到命令,或者发的数据乱码

这个是最常见的问题,原因一般有三个:

  1. 波特率不匹配:HC-05默认波特率是9600,要是你主芯片的串口波特率设成115200,肯定收不到。解决方法:检查主芯片和蓝牙模块的波特率,必须一致。
  2. 命令格式不对:HC-05的AT命令必须以回车换行(\r\n)结尾,要是你发命令的时候没加,蓝牙模块会认为命令没发完,不会处理。解决方法:发命令的时候一定要加\r\n。
  3. 多个任务同时发命令:要是你没开蓝牙专属任务,两个任务同时往串口发命令,数据就会混在一起,导致乱码。解决方法:必须用蓝牙专属任务,所有发命令的操作都通过它来做。

4.2 问题2:队列满了,数据丢了

比如温湿度任务每秒往队列放一次数据,要是队列最多存10条,过了10秒没处理,队列就满了,再放数据就会丢。解决方法:

  1. 加大队列的大小:比如把队列的最大条数改成20,这样能存更久的数据。
  2. 提高蓝牙任务的优先级:让蓝牙任务处理数据的速度更快,队列就不容易满。
  3. 给队列加超时:比如放数据的时候等50ms,要是50ms队列还没位置,就先放旧数据,保证新数据能放进去。

4.3 问题3:蓝牙断开后,设备卡死

要是手机和蓝牙模块断开连接,有些蓝牙模块会停止工作,要是你的代码没处理这个情况,就会卡死。解决方法:

  1. 给蓝牙任务加一个“连接检查”:每隔1秒给蓝牙模块发一个“AT+LINK”命令,要是返回“OK”就是连接正常,返回“ERROR”就是断开了。
  2. 要是断开了,就把队列里的旧数据清空,或者暂停温湿度任务,等重新连接后再继续。

五、技术优缺点分析

5.1 优点

  1. 稳定性高:FreeRTOS的任务调度机制,保证了蓝牙通信不会被其他任务干扰,不会出现“顾此失彼”的情况。
  2. 扩展性好:要是你以后要加新功能(比如加个按键、加个OLED屏幕),只要开新的任务就行,不用改原来的蓝牙逻辑。
  3. 开发效率高:队列、任务这些工具都是FreeRTOS自带的,不用自己写复杂的逻辑,省时间。

5.2 缺点

  1. 资源占用多:FreeRTOS本身要占用一定的内存和CPU资源,要是你的主芯片性能很差(比如8位的51单片机),可能跑不起来。
  2. 学习成本:要是你之前没接触过FreeRTOS,得先花时间学任务、队列这些概念,比直接写裸机代码麻烦。

六、应用场景

这种架构适合所有需要同时处理多个任务的嵌入式蓝牙设备,比如:

  1. 环境监测设备:温湿度、PM2.5传感器,需要定时采集数据,还要通过蓝牙传给手机。
  2. 智能家居设备:智能灯、智能插座,需要通过蓝牙接收手机的控制命令,还要反馈设备状态。
  3. 工业控制设备:比如工业传感器,需要把数据通过蓝牙传给上位机,还要处理上位机的控制命令。

七、注意事项

  1. 任务优先级的设置:紧急的任务(比如蓝牙处理命令)优先级要高,不紧急的任务(比如定时采集数据)优先级要低,避免重要任务被耽误。
  2. 队列的大小:队列不能太小(容易满),也不能太大(浪费内存),要根据实际情况调整。
  3. 串口的中断处理:要是你用串口中断收数据,一定要注意中断里不能做太多事,不然会影响FreeRTOS的调度,最好把收到的数据放进缓冲区,再让蓝牙任务去处理。

八、文章总结

把FreeRTOS和蓝牙模块结合,核心就是“分工明确”:给蓝牙单独开一个任务,专门管和蓝牙模块的交互,其他任务通过队列和它通信。这么做既能保证蓝牙通信的稳定性,又能让整个系统的逻辑更清晰,扩展性更好。只要把任务划分和队列的用法搞懂,再避开常见的坑,就能顺利完成集成。