FreeRTOS与DHT11实战:STM32物联网温湿度数据采集与云端同步
1. 项目缘起:为什么需要FreeRTOS来管理温湿度采集?
大家好,我是老张,一个在嵌入式领域摸爬滚打了十多年的“老码农”。今天想和大家聊聊一个非常经典,但又常做常新的项目:用STM32结合DHT11传感器采集温湿度,并且把数据同步到云端。听起来是不是很简单?很多新手朋友可能觉得,不就是读个传感器,再通过Wi-Fi模块发个数据嘛,裸机程序(也就是不用操作系统,直接在main函数里写while(1)循环)也能搞定。
这话没错,裸机确实能实现。但如果你想让你的设备更“聪明”、更稳定,比如同时处理传感器数据、刷新OLED屏幕、响应网络事件、还能处理按键输入,裸机程序很快就会变得像一团乱麻,各种延时和中断处理交织在一起,代码又长又难维护。这时候,FreeRTOS 就该登场了。
你可以把FreeRTOS想象成一个“超级管家”。在没有管家(裸机)的时候,你一个人(CPU)要同时烧水、扫地、接电话,手忙脚乱,一件事卡住了(比如等水烧开),其他事都得干等着。而有了FreeRTOS这个管家,它可以把烧水、扫地、接电话这些事安排给不同的“仆人”(任务)去专门负责,并且管家会合理地分配你的时间,让你感觉所有事情都在同时、流畅地进行。这就是多任务并发的魅力。
在我们的项目中,至少有三个核心“仆人”需要同时工作:
- 温湿度采集任务:定期(比如每2秒)去读取DHT11的数据。
- 数据显示任务:把采集到的温湿度实时显示在OLED屏幕上。
- 数据上传任务:检查数据是否有变化,如果有,就通过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引脚 | 备注 |
|---|---|---|---|
| DHT11 | VCC | 3.3V | 模块供电,注意有些模块兼容3.3V/5V,我们统一用3.3V更安全 |
| GND | GND | 共地 | |
| DATA | PA11 | 单总线数据线,需要接一个4.7K-10K的上拉电阻到3.3V | |
| ESP8266-01S | VCC | 3.3V | 务必使用独立且稳定的3.3V电源,电流需求可能较大 |
| GND | GND | 共地 | |
| TX | PA3 (USART2_RX) | 模块的TX接STM32的RX | |
| RX | PA2 (USART2_TX) | 模块的RX接STM32的TX | |
| CH_PD/EN | 3.3V | 使能引脚,接高电平 | |
| GPIO0 | 3.3V | 悬空或接高电平,进入正常工作模式 | |
| OLED (I2C) | VCC | 3.3V | 供电 |
| GND | GND | 共地 | |
| SCL | PB6 (I2C1_SCL) | 时钟线 | |
| SDA | PB7 (I2C1_SDA) | 数据线 |
几个关键的硬件坑点,我踩过,希望你别再踩:
- DHT11的上拉电阻:单总线协议要求DATA线在空闲时保持高电平。如果不加上拉电阻,STM32的IO口内部弱上拉可能力度不够,导致通信失败。务必在DATA线和3.3V之间接一个4.7K或10K的电阻。
- ESP8266的供电:这是最容易出问题的地方!ESP8266在发射Wi-Fi信号时瞬时电流可能超过200mA,很多STM32开发板上的3.3V LDO输出能力不足,会导致模块不断重启或连接失败。强烈建议给ESP8266单独供电,比如使用一个AMS1117-3.3V模块从5V取电,或者使用外部稳定的3.3V电源。
- 电平匹配:确保所有模块的逻辑电平都是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中,每个任务都是一个独立的无限循环函数。我们需要为它们分配合理的优先级和堆栈大小。
-
温湿度采集任务 (
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,取决于你使用的显示库复杂度。
- 功能:以较高的频率(比如每秒10次)刷新OLED屏幕,显示最新的温湿度数据。这里会用到OLED的驱动库(如
-
数据上传任务 (
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连接流程如下:
- 测试通信:
AT-> 回复OK - 设置Wi-Fi模式:
AT+CWMODE=1(Station模式) ->OK - 连接路由器:
AT+CWJAP="你的Wi-Fi名","密码"-> 等待连接成功 - 连接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任务中,长时间的阻塞会严重影响其他任务。有两种思路:- 状态机:将整个连接、发布过程拆分成多个小步骤,用状态机推进。每次任务执行只推进一小步,不满足条件就
vTaskDelay一小会儿再检查。这是更优雅的方式。 - 分离任务:单独创建一个
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发送一条告警通知。
- 数据可视化:可以创建仪表盘,实时显示温湿度曲线图,历史数据查询等。
项目扩展思路: 这个项目是一个完美的起点,你可以基于它做很多有趣的扩展:
- 增加更多传感器:如光照传感器(GY-30)、空气质量传感器(MQ135)。在FreeRTOS中为每个传感器创建一个独立的采集任务即可。
- 本地控制:增加一个按键任务,按下按键可以切换OLED显示内容,或者控制一个继电器(连接加湿器、风扇)。
- OTA远程升级:通过MQTT或者HTTP,从云端下载新的固件,实现设备程序的无线更新。这是产品化必不可少的功能。
- 低功耗优化:如果设备是电池供电,可以让STM32和ESP8266在大部分时间进入休眠模式,定时唤醒采集和上传数据,极大延长续航。
在我实际做过的智能农业大棚项目中,就是在这个基础框架上,增加了土壤湿度、光照、二氧化碳浓度传感器,并通过继电器控制水泵和补光灯。FreeRTOS的任务管理让整个系统代码结构非常清晰,新增功能就像搭积木一样方便。
最后,我想说,嵌入式物联网开发就是这样,从一个个小模块(传感器驱动、RTOS任务、网络通信)开始,逐步搭建起一个完整的系统。过程中肯定会遇到各种问题,时序不对、内存溢出、任务卡死……但每次解决问题的过程,都是宝贵的经验积累。希望这篇长文能帮你少走些弯路,顺利搭建起你的第一个FreeRTOS物联网数据采集节点。如果遇到问题,多查数据手册,多用调试工具(串口打印、逻辑分析仪),耐心分析,你一定能搞定它。
更多推荐
所有评论(0)