1. 项目缘起:为什么需要FreeRTOS来管理温湿度采集?

大家好,我是老张,一个在嵌入式领域摸爬滚打了十多年的“老码农”。今天想和大家聊聊一个非常经典,但又常做常新的项目:用STM32结合DHT11传感器采集温湿度,并且把数据同步到云端。听起来是不是很简单?很多新手朋友可能觉得,不就是读个传感器,再通过Wi-Fi模块发个数据嘛,裸机程序(也就是不用操作系统,直接在main函数里写while(1)循环)也能搞定。

这话没错,裸机确实能实现。但如果你想让你的设备更“聪明”、更稳定,比如同时处理传感器数据、刷新OLED屏幕、响应网络事件、还能处理按键输入,裸机程序很快就会变得像一团乱麻,各种延时和中断处理交织在一起,代码又长又难维护。这时候,FreeRTOS 就该登场了。

你可以把FreeRTOS想象成一个“超级管家”。在没有管家(裸机)的时候,你一个人(CPU)要同时烧水、扫地、接电话,手忙脚乱,一件事卡住了(比如等水烧开),其他事都得干等着。而有了FreeRTOS这个管家,它可以把烧水、扫地、接电话这些事安排给不同的“仆人”(任务)去专门负责,并且管家会合理地分配你的时间,让你感觉所有事情都在同时、流畅地进行。这就是多任务并发的魅力。

在我们的项目中,至少有三个核心“仆人”需要同时工作:

  1. 温湿度采集任务:定期(比如每2秒)去读取DHT11的数据。
  2. 数据显示任务:把采集到的温湿度实时显示在OLED屏幕上。
  3. 数据上传任务:检查数据是否有变化,如果有,就通过ESP8266 Wi-Fi模块,使用MQTT协议将数据发送到云端服务器。

如果没有FreeRTOS,你可能需要用定时器中断来触发采集,在主循环里轮询网络状态和刷新屏幕,逻辑会非常复杂且容易出错。用了FreeRTOS,每个任务独立编写,逻辑清晰,互不干扰,系统的稳定性和可扩展性会大大提升。这就是我们这次实战要解决的核心问题。

2. 硬件选型与电路连接:搭建你的物联网“感官”

工欲善其事,必先利其器。我们先来看看需要准备哪些硬件,以及如何把它们正确地连接起来。这套方案成本很低,但功能完整,非常适合学习和原型开发。

核心硬件清单:

  • 主控芯片:STM32F103C8T6(俗称“蓝莓派”或最小系统板)。这款芯片性价比极高,资源丰富,社区支持好,是入门STM32和FreeRTOS的绝佳选择。
  • 温湿度传感器:DHT11。一款经典的数字温湿度复合传感器,采用单总线通信,价格便宜,精度对于大多数室内环境监测(如智能家居、温室大棚)来说完全够用。它的温度测量范围是0-50°C,误差±2°C;湿度测量范围是20%-90%RH,误差±5%RH。
  • Wi-Fi模块:ESP8266-01S。这是一个自带TCP/IP协议栈的SOC,我们可以让它工作在AT指令模式,通过串口与STM32通信,轻松连接网络和MQTT服务器。
  • 显示模块(可选但推荐):0.96寸OLED屏幕(I2C接口)。用于本地实时显示数据,调试时非常方便。
  • 其他:USB转TTL串口模块(用于下载程序和调试)、杜邦线若干、3.3V/5V电源。

电路连接示意图(关键部分):

这里我画一个简单的连接表,大家对照着自己的开发板引脚连接:

器件引脚连接至STM32引脚备注
DHT11VCC3.3V模块供电,注意有些模块兼容3.3V/5V,我们统一用3.3V更安全
GNDGND共地
DATAPA11单总线数据线,需要接一个4.7K-10K的上拉电阻到3.3V
ESP8266-01SVCC3.3V务必使用独立且稳定的3.3V电源,电流需求可能较大
GNDGND共地
TXPA3 (USART2_RX)模块的TX接STM32的RX
RXPA2 (USART2_TX)模块的RX接STM32的TX
CH_PD/EN3.3V使能引脚,接高电平
GPIO03.3V悬空或接高电平,进入正常工作模式
OLED (I2C)VCC3.3V供电
GNDGND共地
SCLPB6 (I2C1_SCL)时钟线
SDAPB7 (I2C1_SDA)数据线

几个关键的硬件坑点,我踩过,希望你别再踩:

  1. DHT11的上拉电阻:单总线协议要求DATA线在空闲时保持高电平。如果不加上拉电阻,STM32的IO口内部弱上拉可能力度不够,导致通信失败。务必在DATA线和3.3V之间接一个4.7K或10K的电阻。
  2. ESP8266的供电:这是最容易出问题的地方!ESP8266在发射Wi-Fi信号时瞬时电流可能超过200mA,很多STM32开发板上的3.3V LDO输出能力不足,会导致模块不断重启或连接失败。强烈建议给ESP8266单独供电,比如使用一个AMS1117-3.3V模块从5V取电,或者使用外部稳定的3.3V电源。
  3. 电平匹配:确保所有模块的逻辑电平都是3.3V。DHT11和OLED一般没问题。ESP8266的IO口虽然是3.3V电平,但通常也能兼容。

连接好硬件,我们就可以开始软件部分的冒险了。

3. DHT11单总线驱动开发:读懂传感器的“语言”

DHT11使用单总线协议通信,这意味着它只用一根数据线,既要接收主机的命令,又要向主机发送数据。这种协议对时序的要求非常严格,差个几微秒可能数据就错了。所以,写好它的底层驱动是项目成功的第一步。

3.1 单总线协议深度解析

DHT11的通信流程可以概括为:主机发起起始信号 -> DHT11响应 -> DHT11发送40位数据 -> 主机接收并校验。

数据格式:一次传输40位(5字节)数据。

  • 字节1:湿度整数部分(Humidity High Byte)
  • 字节2:湿度小数部分(Humidity Low Byte),对于DHT11,通常为0。
  • 字节3:温度整数部分(Temperature High Byte)
  • 字节4:温度小数部分(Temperature Low Byte),对于DHT11,通常为0。
  • 字节5:校验和(Checksum),等于前四个字节相加和的低8位。

核心在于识别“0”和“1”:DHT11不是用固定的高/低电平表示0/1,而是用高电平的持续时间来区分的。

  • 位“0”:50us低电平后,维持26-28us的高电平。
  • 位“1”:50us低电平后,维持70us的高电平。

我们的驱动代码,就是要用STM32的GPIO和精准的延时,来模拟和解析这个时序。下面是我在实际项目中打磨过的驱动代码,关键部分都有注释。

3.2 手把手编写驱动代码

首先,我们创建dht11.h和dht11.c两个文件。

dht11.h 头文件定义:

#ifndef __DHT11_H
#define __DHT11_H

#include "stm32f10x.h"

// 根据你的硬件连接修改这个IO口定义
#define DHT11_GPIO_PORT    GPIOA
#define DHT11_GPIO_PIN     GPIO_Pin_11
#define DHT11_RCC_CLK      RCC_APB2Periph_GPIOA

// IO方向设置宏函数
#define DHT11_IO_IN()  {GPIOA->CRH &= 0XFFFF0FFF; GPIOA->CRH |= 8<<12;}
#define DHT11_IO_OUT() {GPIOA->CRH &= 0XFFFF0FFF; GPIOA->CRH |= 3<<12;}

// IO操作宏函数
#define DHT11_DQ_OUT_HIGH() GPIO_SetBits(DHT11_GPIO_PORT, DHT11_GPIO_PIN)
#define DHT11_DQ_OUT_LOW()  GPIO_ResetBits(DHT11_GPIO_PORT, DHT11_GPIO_PIN)
#define DHT11_DQ_IN()       GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN)

// 函数声明
void DHT11_Init(void);
uint8_t DHT11_Read_Data(uint8_t *temp, uint8_t *humi);
uint8_t DHT11_Read_Byte(void);
uint8_t DHT11_Read_Bit(void);
uint8_t DHT11_Check(void);
void DHT11_Rst(void);

#endif

dht11.c 源文件实现:

这里包含了最核心的时序控制函数。我强烈建议你使用示波器或者逻辑分析仪来观察波形,这是调试单总线设备最直接有效的方法。

#include "dht11.h"
#include "delay.h" // 你需要一个精准的微秒延时函数,可以使用SysTick定时器实现

/**
  * @brief  初始化DHT11的IO口,并检测传感器是否存在
  * @param  无
  * @retval 0: 存在; 1: 不存在
  */
uint8_t DHT11_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB2PeriphClockCmd(DHT11_RCC_CLK, ENABLE);

    GPIO_InitStructure.GPIO_Pin = DHT11_GPIO_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(DHT11_GPIO_PORT, &GPIO_InitStructure);

    DHT11_DQ_OUT_HIGH(); // 拉高总线
    DHT11_Rst(); // 发送复位信号
    return DHT11_Check(); // 等待DHT11响应
}

/**
  * @brief  复位DHT11(主机发起开始信号)
  * @param  无
  * @retval 无
  */
void DHT11_Rst(void)
{
    DHT11_IO_OUT(); // 设置为输出模式
    DHT11_DQ_OUT_LOW(); // 拉低总线
    delay_ms(20); // 至少拉低18ms
    DHT11_DQ_OUT_HIGH(); // 释放总线,由上拉电阻拉高
    delay_us(30); // 主机拉高20-40us
}

/**
  * @brief  检测DHT11是否响应
  * @param  无
  * @retval 0: 存在; 1: 不存在
  */
uint8_t DHT11_Check(void)
{
    uint8_t retry = 0;
    DHT11_IO_IN(); // 设置为输入模式,准备读取

    // 等待DHT11拉低总线(响应信号)
    while (DHT11_DQ_IN() && retry < 100) {
        retry++;
        delay_us(1);
    }
    if (retry >= 100) return 1; // 超时,未检测到响应

    retry = 0;
    // 等待DHT11拉高总线(准备发送数据)
    while (!DHT11_DQ_IN() && retry < 100) {
        retry++;
        delay_us(1);
    }
    if (retry >= 100) return 1; // 超时

    return 0; // 检测成功
}

/**
  * @brief  从DHT11读取一个位
  * @param  无
  * @retval 读到的位值 (0 或 1)
  */
uint8_t DHT11_Read_Bit(void)
{
    uint8_t retry = 0;

    // 等待低电平开始(每个位都以50us低电平开始)
    while (DHT11_DQ_IN() && retry < 100) {
        retry++;
        delay_us(1);
    }
    retry = 0;
    // 等待低电平结束(50us)
    while (!DHT11_DQ_IN() && retry < 100) {
        retry++;
        delay_us(1);
    }

    delay_us(40); // 延时40us后采样,此时若为高电平则是‘1’,低电平则是‘0’

    if (DHT11_DQ_IN()) {
        return 1;
    } else {
        return 0;
    }
}

/**
  * @brief  从DHT11读取一个字节
  * @param  无
  * @retval 读到的字节数据
  */
uint8_t DHT11_Read_Byte(void)
{
    uint8_t i, dat = 0;
    for (i = 0; i < 8; i++) {
        dat <<= 1; // 左移,先接收的是高位
        dat |= DHT11_Read_Bit();
    }
    return dat;
}

/**
  * @brief  从DHT11读取一次完整的温湿度数据
  * @param  temp: 指向温度变量的指针
  * @param  humi: 指向湿度变量的指针
  * @retval 0: 读取成功; 1: 读取失败(校验错误或超时)
  */
uint8_t DHT11_Read_Data(uint8_t *temp, uint8_t *humi)
{
    uint8_t buf[5];
    uint8_t i;

    DHT11_Rst(); // 发起通信
    if (DHT11_Check() == 0) { // 等待DHT11响应
        for (i = 0; i < 5; i++) { // 读取40位数据
            buf[i] = DHT11_Read_Byte();
        }
        // 校验数据
        if ((buf[0] + buf[1] + buf[2] + buf[3]) == buf[4]) {
            *humi = buf[0]; // 湿度整数
            *temp = buf[2]; // 温度整数
            return 0; // 读取成功
        }
    }
    return 1; // 读取失败
}

驱动调试心得:

  • delay_us()函数的准确性至关重要。如果使用for循环实现的微秒延时,需要根据你的主频精确校准。最好使用定时器(如SysTick)来实现。
  • 如果读取数据总是失败或校验错误,首先用逻辑分析仪抓取DATA引脚波形,对照DHT11数据手册的时序图,看高低电平的持续时间是否符合要求。最常见的问题是上拉电阻没接或阻值不对,以及延时函数不准确。
  • DHT11两次读取之间需要至少1秒的间隔,给它足够的“休息”时间。在我们的FreeRTOS任务中,我们会用vTaskDelay来保证这个间隔。

4. FreeRTOS任务设计与创建:让多任务并行不悖

驱动搞定后,我们就可以请出“大管家”FreeRTOS了。我们的目标是创建三个独立的任务,让它们各司其职,协同工作。

4.1 任务规划与优先级分配

在FreeRTOS中,每个任务都是一个独立的无限循环函数。我们需要为它们分配合理的优先级和堆栈大小。

  1. 温湿度采集任务 (DHT11_Task):

    • 功能:每隔2秒调用DHT11_Read_Data函数读取数据,并将结果存入全局变量(如gTemperature, gHumidity)。
    • 优先级:设为中等(例如 osPriorityNormal)。它不需要实时性特别高,但需要稳定执行。
    • 堆栈:512字(对于STM32,1字=4字节)通常足够。
  2. OLED显示任务 (OLED_Task):

    • 功能:以较高的频率(比如每秒10次)刷新OLED屏幕,显示最新的温湿度数据。这里会用到OLED的驱动库(如OLED_ShowString, OLED_ShowNum)。
    • 优先级:可以比采集任务稍低(例如 osPriorityBelowNormal),因为显示刷新慢一点用户也感知不到。
    • 堆栈:512字或1KB,取决于你使用的显示库复杂度。
  3. 数据上传任务 (MQTT_Send_Task):

    • 功能:检查温湿度全局变量是否有变化。如果有变化,则通过ESP8266模块,使用MQTT协议将新数据发布(Publish)到云端指定的主题(Topic)。同时,这个任务还需要处理网络的心跳、重连等逻辑。
    • 优先级:与采集任务同级或略低。网络通信可能阻塞,不能让它阻塞更关键的数据采集。
    • 堆栈:需要较大,建议1KB或更多,因为MQTT和网络处理函数调用层次可能较深。

为什么用全局变量? 因为任务之间需要通信。采集任务产生数据,显示和上传任务消费数据。这是一种简单的数据共享方式。更高级的通信方式可以使用FreeRTOS的队列(Queue)或信号量(Semaphore),但对于我们这个简单场景,全局变量加一些简单的保护(如关中断)已经足够。更复杂的场景,比如多个传感器,我会推荐使用队列。

4.2 代码实现:创建与运行任务

下面是在main.c或专门的任务管理文件中创建这些任务的核心代码。假设你已经正确初始化了FreeRTOS。

#include "FreeRTOS.h"
#include "task.h"
#include "queue.h" // 未来扩展可用
#include "dht11.h"
#include "oled.h"
#include "esp8266_mqtt.h" // 假设这是你封装的ESP8266 MQTT驱动

// 全局变量,用于任务间共享数据
volatile uint8_t gTemperature = 0;
volatile uint8_t gHumidity = 0;
// 可以加一个简单的标志位或互斥量来保护,这里为了简化先不用

// 任务句柄
TaskHandle_t DHT11_Task_Handle = NULL;
TaskHandle_t OLED_Task_Handle = NULL;
TaskHandle_t MQTT_Send_Task_Handle = NULL;

/**
  * @brief  硬件外设初始化
  */
static void BSP_Init(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); // FreeRTOS推荐的分组方式
    Delay_Init(); // 初始化延时函数(SysTick)
    USART1_Init(115200); // 初始化调试串口
    OLED_Init(); // 初始化OLED
    DHT11_Init(); // 初始化DHT11
    ESP8266_UART_Init(115200); // 初始化与ESP8266通信的串口
}

/**
  * @brief  温湿度采集任务函数
  */
static void DHT11_Task_Function(void *pvParameters)
{
    uint8_t temp = 0, humi = 0;
    const TickType_t xDelay = pdMS_TO_TICKS(2000); // 2000ms = 2秒

    while (1) {
        if (DHT11_Read_Data(&temp, &humi) == 0) { // 读取成功
            // 简单的数据保护,可以在这里关中断
            // taskENTER_CRITICAL();
            gTemperature = temp;
            gHumidity = humi;
            // taskEXIT_CRITICAL();
            printf("[DHT11] Temp: %d C, Humi: %d %%\r\n", temp, humi); // 调试输出
        } else {
            printf("[DHT11] Read Error!\r\n");
        }
        vTaskDelay(xDelay); // 阻塞延时,让出CPU给其他任务
    }
}

/**
  * @brief  OLED显示任务函数
  */
static void OLED_Task_Function(void *pvParameters)
{
    uint8_t last_temp = 0, last_humi = 0;
    const TickType_t xDelay = pdMS_TO_TICKS(100); // 100ms刷新一次

    OLED_Clear();
    OLED_ShowString(0, 0, "Temp:   C", 16);
    OLED_ShowString(0, 2, "Humi:   %", 16);

    while (1) {
        // 只有数据变化时才刷新,减少OLED操作
        if (last_temp != gTemperature || last_humi != gHumidity) {
            last_temp = gTemperature;
            last_humi = gHumidity;

            OLED_ShowNum(40, 0, gTemperature, 2, 16); // 在(40,0)位置显示温度
            OLED_ShowNum(40, 2, gHumidity, 2, 16);    // 在(40,2)位置显示湿度
        }
        vTaskDelay(xDelay);
    }
}

/**
  * @brief  MQTT数据上传任务函数
  */
static void MQTT_Send_Task_Function(void *pvParameters)
{
    uint8_t last_temp = 0, last_humi = 0;
    const TickType_t xShortDelay = pdMS_TO_TICKS(500); // 检查间隔
    TickType_t xLastWakeTime;
    BaseType_t xWasDelayed;

    // 初始化ESP8266并连接MQTT服务器
    if (ESP8266_MQTT_Init() != 0) {
        printf("[MQTT] Init Failed!\r\n");
        vTaskDelete(NULL); // 初始化失败,删除自身任务
    }
    printf("[MQTT] Connected!\r\n");

    xLastWakeTime = xTaskGetTickCount();

    while (1) {
        // 使用绝对延时,保证精确的周期
        xWasDelayed = xTaskDelayUntil(&xLastWakeTime, xShortDelay);

        // 检查数据是否有变化
        if (last_temp != gTemperature) {
            if (ESP8266_MQTT_Publish("device/room/temp", gTemperature) == 0) {
                last_temp = gTemperature;
                printf("[MQTT] Temp published: %d\r\n", gTemperature);
            } else {
                printf("[MQTT] Publish Temp failed!\r\n");
                // 这里可以添加重连逻辑
            }
        }

        if (last_humi != gHumidity) {
            if (ESP8266_MQTT_Publish("device/room/humi", gHumidity) == 0) {
                last_humi = gHumidity;
                printf("[MQTT] Humi published: %d\r\n", gHumidity);
            } else {
                printf("[MQTT] Publish Humi failed!\r\n");
            }
        }
        // 这里还可以添加发送心跳包、处理订阅消息等逻辑
    }
}

/**
  * @brief  应用任务创建函数
  */
void AppTaskCreate(void)
{
    BaseType_t xReturn = pdPASS;

    // 创建温湿度采集任务
    xReturn = xTaskCreate(DHT11_Task_Function,
                          "DHT11_Task",
                          128, // 堆栈深度,单位是字(word)
                          NULL,
                          2, // 优先级,数字越大优先级越高
                          &DHT11_Task_Handle);
    if (xReturn != pdPASS) {
        // 创建失败处理,比如点亮错误LED
    }

    // 创建OLED显示任务
    xReturn = xTaskCreate(OLED_Task_Function,
                          "OLED_Task",
                          128,
                          NULL,
                          1,
                          &OLED_Task_Handle);

    // 创建MQTT上传任务
    xReturn = xTaskCreate(MQTT_Send_Task_Function,
                          "MQTT_Send_Task",
                          256, // 网络任务需要更多堆栈
                          NULL,
                          2, // 与采集任务同优先级
                          &MQTT_Send_Task_Handle);
}

int main(void)
{
    BSP_Init(); // 初始化硬件
    printf("System Start...\r\n");

    AppTaskCreate(); // 创建所有应用任务

    vTaskStartScheduler(); // 启动FreeRTOS调度器,从此CPU交给FreeRTOS管理

    // 正常情况下,不会执行到这里
    while (1) {
        // 如果调度器启动失败,会进入这里
    }
}

代码要点解析:

  • vTaskDelay():这是相对延时,意思是“从现在开始,延时X个时钟节拍后再执行”。它会让当前任务进入阻塞状态,CPU可以去执行其他就绪的任务。
  • xTaskDelayUntil(&xLastWakeTime, xDelay):这是绝对延时,常用于需要固定周期执行的任务(如本上传任务)。它能补偿任务本身执行时间带来的误差,让周期更精确。
  • 优先级:我给了采集和上传任务优先级2,显示任务优先级1。这意味着当采集和上传任务就绪时,会先于显示任务运行。你可以根据实际情况调整。
  • 堆栈大小:这是一个需要经验的值。太小会导致栈溢出,系统崩溃;太大浪费内存。开始时可以设大一点,运行稳定后通过FreeRTOS提供的工具(如uxTaskGetStackHighWaterMark)查看剩余堆栈,再调整到合适值。

5. ESP8266与MQTT协议:连接云端的桥梁

数据采集和本地显示都完成了,最后一步就是“上网”,把数据送到云端。我们选择MQTT协议,它是物联网领域最主流的轻量级消息协议,特别适合设备与云端的双向通信。

5.1 ESP8266 AT指令配置

我们让ESP8266工作在AT指令模式,STM32通过串口发送AT指令来控制它。基本的联网和MQTT连接流程如下:

  1. 测试通信:AT -> 回复 OK
  2. 设置Wi-Fi模式:AT+CWMODE=1 (Station模式) -> OK
  3. 连接路由器:AT+CWJAP="你的Wi-Fi名","密码" -> 等待连接成功
  4. 连接MQTT服务器:这里需要一系列指令,例如连接阿里云物联网平台:
    • AT+MQTTUSERCFG=0,1,"NULL","设备名|securemode=3,signmethod=hmacsha1,timestamp=xxx|","设备密钥" (配置用户信息)
    • AT+MQTTCONN=0,"你的产品域名",1883,1 (连接Broker)

这些指令看起来复杂,但我们可以把它们封装成函数,比如ESP8266_ConnectWiFi()和ESP8266_MQTT_Connect()。关键在于处理ESP8266的回复,它可能返回OK、ERROR,或者是带数据的回复,我们需要在串口中断服务程序里耐心地解析。

5.2 在FreeRTOS任务中集成MQTT上传

上面的MQTT_Send_Task_Function已经展示了核心逻辑:周期性地检查数据变化,然后调用封装的发布函数。这里再强调几个实战细节:

  • 断线重连:网络是不稳定的。必须在任务里加入重连机制。如果ESP8266_MQTT_Publish连续失败几次,就应该触发重连流程,从步骤1(AT测试)开始。
  • 非阻塞设计:AT指令的等待回复过程是阻塞的(比如等待OK)。在FreeRTOS任务中,长时间的阻塞会严重影响其他任务。有两种思路:
    1. 状态机:将整个连接、发布过程拆分成多个小步骤,用状态机推进。每次任务执行只推进一小步,不满足条件就vTaskDelay一小会儿再检查。这是更优雅的方式。
    2. 分离任务:单独创建一个ESP8266_AT_Task,专门处理与ESP8266的串口通信。其他任务通过队列(Queue)向它发送指令(如“连接Wi-Fi”、“发布数据”),它异步地执行并返回结果。这样网络操作的阻塞就不会影响传感器采集和显示。
  • 数据格式:MQTT消息负载(Payload)可以是文本(如"25"),也可以是JSON(如{"temp":25, "humi":60})。JSON更通用,方便云端解析。你可以使用轻量级的JSON库(如cJSON)来构建消息。

一个简单的非阻塞状态机示例片段:

typedef enum {
    MQTT_STATE_IDLE,
    MQTT_STATE_WIFI_CONNECTING,
    MQTT_STATE_MQTT_CONNECTING,
    MQTT_STATE_CONNECTED,
    MQTT_STATE_PUBLISHING,
    MQTT_STATE_ERROR
} MQTT_State_t;

static MQTT_State_t mqtt_state = MQTT_STATE_IDLE;
static uint32_t retry_count = 0;

void MQTT_Task_Function(void *pvParameters) {
    while(1) {
        switch(mqtt_state) {
            case MQTT_STATE_IDLE:
                if(need_to_send_data) {
                    mqtt_state = MQTT_STATE_WIFI_CONNECTING;
                    Send_AT_Cmd("AT+CWJAP=\"SSID\",\"PASS\"\r\n");
                }
                break;
            case MQTT_STATE_WIFI_CONNECTING:
                if(Received_Reply("WIFI CONNECTED")) {
                    mqtt_state = MQTT_STATE_MQTT_CONNECTING;
                    // 发送MQTT连接指令...
                } else if(Received_Reply("ERROR") || Timeout()) {
                    retry_count++;
                    if(retry_count > 3) { mqtt_state = MQTT_STATE_ERROR; }
                    // 重试或进入错误状态
                }
                break;
            // ... 其他状态处理
            case MQTT_STATE_CONNECTED:
                // 正常发布数据
                break;
            case MQTT_STATE_ERROR:
                // 错误处理,如复位模块
                break;
        }
        vTaskDelay(100); // 每个循环给其他任务一点时间
    }
}

6. 云端数据可视化与项目扩展

数据成功上传到云端(比如阿里云IoT、腾讯云IoT、或者自己搭建的EMQX Broker)后,你就可以在云端进行存储、分析和展示了。大多数云平台都提供规则引擎和数据可视化工具。

  • 规则引擎:可以设置规则,当温度超过30°C时,自动向你的手机APP发送一条告警通知。
  • 数据可视化:可以创建仪表盘,实时显示温湿度曲线图,历史数据查询等。

项目扩展思路: 这个项目是一个完美的起点,你可以基于它做很多有趣的扩展:

  1. 增加更多传感器:如光照传感器(GY-30)、空气质量传感器(MQ135)。在FreeRTOS中为每个传感器创建一个独立的采集任务即可。
  2. 本地控制:增加一个按键任务,按下按键可以切换OLED显示内容,或者控制一个继电器(连接加湿器、风扇)。
  3. OTA远程升级:通过MQTT或者HTTP,从云端下载新的固件,实现设备程序的无线更新。这是产品化必不可少的功能。
  4. 低功耗优化:如果设备是电池供电,可以让STM32和ESP8266在大部分时间进入休眠模式,定时唤醒采集和上传数据,极大延长续航。

在我实际做过的智能农业大棚项目中,就是在这个基础框架上,增加了土壤湿度、光照、二氧化碳浓度传感器,并通过继电器控制水泵和补光灯。FreeRTOS的任务管理让整个系统代码结构非常清晰,新增功能就像搭积木一样方便。

最后,我想说,嵌入式物联网开发就是这样,从一个个小模块(传感器驱动、RTOS任务、网络通信)开始,逐步搭建起一个完整的系统。过程中肯定会遇到各种问题,时序不对、内存溢出、任务卡死……但每次解决问题的过程,都是宝贵的经验积累。希望这篇长文能帮你少走些弯路,顺利搭建起你的第一个FreeRTOS物联网数据采集节点。如果遇到问题,多查数据手册,多用调试工具(串口打印、逻辑分析仪),耐心分析,你一定能搞定它。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐