解码MQTT协议与DHT11传感器

MQTT协议

基本概念

MQTT(消息队列遥测传输协议)是基于C/S架构的发布/订阅模式消息传输协议,设计核心是轻巧、开放、简单、规范,易于嵌入式设备实现。其适配受限环境(如M2M机器通信、IoT物联网),这类场景对代码体积要求极小或网络带宽昂贵。

运行依赖:必须基于TCP/IP或其他能提供有序、可靠、双向连接的网络,确保消息传输的基础稳定性。

核心优势:采用发布/订阅模式实现一对多消息分发,解除应用间耦合;消息传输无需解析负载内容,灵活性高;支持分级服务质量,适配不同业务需求。

服务质量(QoS)

MQTT定义三种服务质量等级,对应不同可靠性需求:

  • QoS 0(最多一次):尽最大努力分发,消息可能丢失,无确认机制。适用于非关键数据,如环境传感器周期性数据(单次丢失可通过后续数据补充)。
  • QoS 1(至少一次):保证消息必达,但可能重复。接收方收到消息后发送PUBACK确认,发送方未收到确认则重发。
  • QoS 2(仅一次):保证消息仅送达一次,无重复、无丢失。通过"PUBLISH→PUBREC→PUBREL→PUBCOMP"四次交互实现,适用于计费、指令下发等关键场景。

控制报文种类

MQTT通过14种控制报文实现通信,核心报文及功能如下(部分保留报文禁止使用):

报文格式

所有MQTT控制报文均由三部分组成,按顺序排列:

固定报头(必含)

占1-多个字节,核心包含两部分:

  • 字节1:高4位为报文类型(如0x10对应CONNECT),低4位为标志位(含DUP重发标志、QoS等级、RETAIN保留标志)。
  • 剩余长度字节:表示后续可变报头+有效载荷的总长度,采用变长编码(1-4字节),每个字节最高位为"续位标志",低7位为长度值。

可变报头(可选)

部分报文含此部分,内容与报文类型相关。例如:

  • CONNECT报文:含协议名(UTF-8编码"MQTT")、协议级别(MQTT 3.1.1对应4)、连接标志、保持连接时间。
  • PUBLISH报文:含主题名(UTF-8编码,无通配符)、报文标识符(仅QoS1/2需携带)。

有效载荷(可选)

携带实际业务数据,仅部分报文含此部分。例如:

  • CONNECT报文:含客户端标识符(必选)、Will主题、Will消息、用户名、密码(均可选,由连接标志控制)。
  • PUBLISH报文:含发布的应用消息内容,格式由用户自定义。
  • SUBSCRIBE报文:含订阅的主题过滤器及对应的QoS等级。

核心报文交互流程

连接流程(CONNECT/CONNACK)

  • 客户端发送CONNECT报文,携带客户端ID、保持连接时间等参数(保持连接时间单位为秒,超时未通信则服务端断开连接)。
  • 服务端处理后返回CONNACK报文,可变报头含连接返回码:0x00表示连接成功,0x01-0x05对应不同失败原因(如协议版本不支持、客户端ID非法、未授权等)。

订阅流程(SUBSCRIBE/SUBACK)

  • 客户端发送SUBSCRIBE报文,指定主题过滤器(支持通配符"+""#")和目标QoS等级。
  • 服务端返回SUBACK报文,携带每个订阅主题的结果码(0x00-0x02表示订阅成功且对应QoS,0x80表示订阅失败)。

发布流程(PUBLISH+确认报文)

  • QoS0:客户端/服务端发送PUBLISH后无确认,消息可能丢失。
  • QoS1:发送方发PUBLISH→接收方发PUBACK确认,未收到确认则重发。
  • QoS2:四步交互(PUBLISH→PUBREC→PUBREL→PUBCOMP),确保消息仅送达一次。

心跳机制(PINGREQ/PINGRESP)

客户端需在保持连接时间内周期性发送PINGREQ报文,服务端收到后回复PINGRESP。若客户端超时未收PINGRESP,或服务端超时未收PINGREQ,均会断开连接。建议心跳发送周期设为保持连接时间的1/2(如保持连接60秒,每30秒发一次心跳)。

DHT11温湿度传感器

基本参数与特性

DHT11是集成校准数字输出的温湿度复合传感器,含电容式感湿元件、NTC测温元件及8位单片机,可靠性高、稳定性强,采用单总线通信,接线简单。

参数类型 湿度参数 温度参数
量程范围 5%~95% RH(无凝结) -20℃~60℃
精度(25℃) ±5% RH ±2℃
响应时间 <6秒(1/e,63%) <10秒(1/e,63%)
供电电压 3.3V~5.5V DC

其他特性:支持完全互换,年漂移量小(湿度<±0.5% RH/年,温度<±0.5℃/年),适合长期监测场景。

硬件接线

DHT11共4个引脚,核心接线规则:

  • VDD:接3.3V/5V电源,需注意电源纹波(纹波过大会导致温度跳动)。
  • DATA:串行数据引脚,单总线通信,需接10K上拉电阻(连接线<5m用4.7K,>5m需降低电阻值)。
  • NC:空脚,无需连接。
  • GND:接电源负极,确保共地稳定。

注意:3.3V供电时连接线尽量短,避免供电不足导致测量偏差;建议搭配LDO稳压器供电,减少电压波动影响。

工作原理(单总线时序)

MCU与DHT11通过单总线交互,流程分为"起始信号→响应信号→数据传输"三步,时序要求严格。

起始信号(MCU发送)

  • MCU将DATA引脚设为输出,拉低电平≥18ms(建议25ms,留足余量)。
  • MCU将DATA引脚切换为输入,释放总线(由上拉电阻拉至高电平),等待DHT11响应。

响应信号(DHT11发送)

  • DHT11检测到起始信号后,拉低DATA引脚83μs作为应答。
  • 随后拉高DATA引脚87μs,告知MCU准备发送数据,MCU检测到该高电平后开始接收数据。

若MCU超时未检测到低电平应答(建议超时阈值100μs),则判定通信失败。

数据传输(DHT11发送)

DHT11共发送40位数据,格式为"8位湿度整数→8位湿度小数→8位温度整数→8位温度小数→8位校验和",高位先出(MSB)。

单bit数据时序区分:

  • bit 0:54μs低电平 + 23~27μs高电平。
  • bit 1:54μs低电平 + 68~74μs高电平。

校验规则:前4字节数据之和等于第5字节校验和,若不相等则数据无效,需重新读取。

注意:每次读取的是上一次测量结果,需连续读2次获取实时数据;两次读取间隔≥2秒,避免频繁读取影响传感器稳定性。

实战:MQTT连接巴法云+DHT11数据采集

基于STM32F4系列,实现WIFI连接巴法云MQTT服务器、DHT11温湿度采集、数据发布功能,核心代码及注释如下。

宏定义与全局变量

c 复制代码
#include "stm32f4xx.h"
#include <string.h>
#include <stdio.h>
#include <stdbool.h>

// 重定向fputc,实现printf串口输出(USART1)
int fputc(int ch, FILE *f)
{
    while( USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET ); // 等待发送缓冲区空
    USART_SendData(USART1, ch);
    return (ch);
}

// WIFI配置参数(需根据实际路由修改)
#define  WIFI_SSID    "wby"         // 路由名称
#define  WIFI_PASSWD  "12345678"    // 路由密码
#define  BEMFA_SERVER "bemfa.com"   // 巴法云域名
#define  BEMFA_PORT   9501          // 巴法云MQTT端口(固定)
#define  BEMFA_CLIENT_UID  "BEMFA_UID"  // 客户端ID(巴法云密钥)
#define  TOPIC_TEMP    "temp004"      // 订阅/发布主题(设备标识)

// 通用配置
#define  BUFFERSIZE  512            // 数据缓冲区大小
#define  DHT11_PIN    GPIO_Pin_9    // DHT11 DATA引脚(PG9)
#define  DHT11_PORT   GPIOG         // DHT11引脚端口

// 全局变量
volatile uint8_t  wifi_recvbuf[BUFFERSIZE] = {0}; // WIFI接收缓冲区
volatile uint32_t wifi_counter = 0;                // WIFI接收计数
volatile uint8_t  wifi_init_flag = 0;              // WIFI初始化完成标志(0:未完成,1:完成)
volatile uint8_t  recvflag = 0;                    // 完整消息接收标志(0:未接收,1:接收完成)

// UTF-8字符串长度编码(拆分16位长度为高低字节)
#define BYTE0(cnt)    ( * (((unsigned char *)(&cnt)) + 0) ) // 低字节
#define BYTE1(cnt)    ( * (((unsigned char *)(&cnt)) + 1) ) // 高字节

基础工具函数(延时、GPIO初始化)

c 复制代码
/**
 * @brief  微秒级延时
 * @param  nus: 延时时间(单位:μs)
 * @retval None
 * @note   Systick时钟源为21MHz,公式:LOAD值 = 延时时间(μs) × 时钟频率(MHz) - 1
 */
void delay_us(u32 nus)
{
    SysTick->CTRL = 0;              // 关闭SysTick定时器
    SysTick->LOAD = nus * 21 - 1;   // 设置重载值
    SysTick->VAL  = 0;              // 清除当前计数值
    SysTick->CTRL = 1;              // 启动定时器(使用内核时钟)
    while ((SysTick->CTRL & 0x00010000) == 0); // 等待计数完成(COUNTFLAG位置1)
    SysTick->CTRL = 0;              // 关闭定时器
}

/**
 * @brief  毫秒级延时
 * @param  nms: 延时时间(单位:ms)
 * @retval None
 * @note   基于delay_us实现,存在轻微误差,适合非精密延时场景
 */
void delay_ms(u32 nms)
{
    while(nms--)
    {
        delay_us(1000); // 1ms = 1000μs
    }
}

/**
 * @brief  DHT11引脚初始化(默认输出高电平,空闲状态)
 * @param  None
 * @retval None
 * @note   引脚为推挽输出,无上下拉(依赖外部上拉电阻)
 */
void DHT11_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG, ENABLE); // 使能GPIOG时钟

    GPIO_InitStructure.GPIO_Pin     = DHT11_PIN;
    GPIO_InitStructure.GPIO_Mode    = GPIO_Mode_OUT;      // 输出模式
    GPIO_InitStructure.GPIO_OType   = GPIO_OType_PP;      // 推挽输出
    GPIO_InitStructure.GPIO_Speed   = GPIO_Speed_100MHz;  // 高速
    GPIO_InitStructure.GPIO_PuPd    = GPIO_PuPd_NOPULL;   // 无上下拉
    GPIO_Init(DHT11_PORT, &GPIO_InitStructure);

    GPIO_SetBits(DHT11_PORT, DHT11_PIN); // 输出高电平,进入空闲状态
    delay_ms(1000); // 上电后等待1秒,跳过传感器不稳定阶段
}

DHT11数据采集函数

c 复制代码
/**
 * @brief  发送DHT11起始信号
 * @param  None
 * @retval None
 * @note   严格遵循时序:拉低≥18ms → 释放总线(输入模式)
 */
void DHT11_SendStart(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    // 切换为输出模式,拉低电平
    GPIO_InitStructure.GPIO_Pin     = DHT11_PIN;
    GPIO_InitStructure.GPIO_Mode    = GPIO_Mode_OUT;
    GPIO_InitStructure.GPIO_OType   = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_Speed   = GPIO_Speed_100MHz;
    GPIO_InitStructure.GPIO_PuPd    = GPIO_PuPd_NOPULL;
    GPIO_Init(DHT11_PORT, &GPIO_InitStructure);
    GPIO_ResetBits(DHT11_PORT, DHT11_PIN); // 拉低

    delay_ms(25); // 拉低25ms,满足≥18ms要求

    // 切换为输入模式,释放总线
    GPIO_InitStructure.GPIO_Mode    = GPIO_Mode_IN;
    GPIO_InitStructure.GPIO_PuPd    = GPIO_PuPd_NOPULL;
    GPIO_Init(DHT11_PORT, &GPIO_InitStructure);
}

/**
 * @brief  接收DHT11响应信号
 * @param  None
 * @retval bool:true(响应成功),false(响应失败/超时)
 * @note   响应时序:83μs低电平 → 87μs高电平,超时阈值设为200μs(低电平)、100μs(高电平)
 */
bool DHT11_ReceiveAck(void)
{
    uint8_t cnt = 0;
    // 等待低电平应答(超时100μs)
    while(GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN) == 1 && cnt <= 100)
    {
        delay_us(1);
        cnt++;
    }
    if(cnt > 100) return false; // 低电平超时,无响应

    // 等待高电平(超时100μs)
    cnt = 0;
    while(GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN) == 0 && cnt <= 100)
    {
        delay_us(1);
        cnt++;
    }
    if(cnt > 100) return false; // 高电平超时,响应异常

    return true; // 响应成功
}

/**
 * @brief  读取1位DHT11数据
 * @param  None
 * @retval uint8_t:0(bit 0),1(bit 1)
 * @note   通过高电平持续时间区分bit值,延时50μs后采样电平
 */
uint8_t DHT11_ReadBit(void)
{
    uint8_t cnt = 0;
    // 等待低电平结束(超时100μs)
    while(GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN) == 1 && cnt <= 100)
    {
        delay_us(1);
        cnt++;
    }

    // 等待高电平开始(超时100μs)
    cnt = 0;
    while(GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN) == 0 && cnt <= 100)
    {
        delay_us(1);
        cnt++;
    }

    delay_us(50); // 延时50μs(处于bit0高电平区间外、bit1高电平区间内)
    // 采样电平:高为1,低为0
    return GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN) ? 1 : 0;
}

/**
 * @brief  读取1字节DHT11数据(高位先出)
 * @param  None
 * @retval uint8_t:读取到的1字节数据
 */
uint8_t DHT11_ReadByte(void)
{
    uint8_t byte = 0;
    for(uint8_t i = 0; i < 8; i++)
    {
		    byte <<= 1;
        byte |= DHT11_ReadBit(); // 读取1位,存入对应位
    }
    return byte;
}

/**
 * @brief  读取DHT11温湿度数据
 * @param  temp:温度整数指针(输出参数,单位:℃)
 * @param  hum:湿度整数指针(输出参数,单位:%RH)
 * @retval bool:true(读取成功,校验通过),false(读取失败/校验失败)
 */
bool DHT11_ReadTH(uint8_t *temp, uint8_t *hum)
{
    uint8_t data[5] = {0}; // 存储5字节数据:湿整+湿小+温整+温小+校验和
    DHT11_SendStart();     // 发送起始信号

    if(!DHT11_ReceiveAck()) return false; // 无响应,返回失败

    // 读取5字节数据
    for(uint8_t i = 0; i < 5; i++)
    {
        data[i] = DHT11_ReadByte();
    }

    // 校验:前4字节和等于第5字节
    if(data[0] + data[1] + data[2] + data[3] == data[4])
    {
        *hum = data[0];  // 湿度整数
        *temp = data[2]; // 温度整数(小数部分忽略,需精确可使用data[1]、data[3])
        return true;
    }
    return false; // 校验失败
}

WIFI与MQTT通信函数

c 复制代码
/**
 * @brief  UART3初始化(连接WIFI模块,波特率115200)
 * @param  baud:波特率(此处固定115200)
 * @retval None
 * @note   UART3引脚:PB10(TX)、PB11(RX),开启接收中断
 */
static void UART3_Config(u32 baud)
{
    USART_InitTypeDef USART_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);  // 使能GPIOB时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE); // 使能USART3时钟

    // 引脚复用配置(PB10→USART3_TX,PB11→USART3_RX)
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource10, GPIO_AF_USART3);
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource11, GPIO_AF_USART3);

    // GPIO参数配置(复用推挽输出,上拉)
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_UP;
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_10 | GPIO_Pin_11;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    // UART参数配置(8位数据,1位停止,无校验,全双工)
    USART_InitStructure.USART_BaudRate = baud;
    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(USART3, &USART_InitStructure);

    // 配置UART3中断(抢占优先级0,使能中断)
    NVIC_InitStructure.NVIC_IRQChannel = USART3_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    USART_ITConfig(USART3, USART_IT_RXNE, ENABLE); // 使能接收中断(接收到数据触发)
    USART_Cmd(USART3, ENABLE); // 使能USART3
}

/**
 * @brief  UART3发送字符串
 * @param  str:待发送字符串(以'\0'结尾)
 * @retval None
 */
void UART3_SendStr(char *str)
{
    while(*str != '\0')
    {
        USART_SendData(USART3, *str++);
        while(USART_GetFlagStatus(USART3, USART_FLAG_TXE) == RESET); // 等待发送完成
    }
}

void UART3_SendData(char * str, int len)
{
    while(len--)
    {
        //等待发送数据寄存器为空        
        USART_SendData(USART3,*str++);
        while( USART_GetFlagStatus(USART3,USART_FLAG_TXE) == RESET );    
    }
}

/**
 * @brief  WIFI模块发送AT指令并等待响应
 * @param  str:AT指令字符串(需带\r\n换行)
 * @param  time:超时时间(单位:ms)
 * @retval bool:true(指令发送成功,收到OK/>响应),false(超时/响应失败)
 */
bool WIFI_SendAT(char *str, uint16_t time)
{
    memset((char *)wifi_recvbuf, 0, BUFFERSIZE); // 清空接收缓冲区
    wifi_counter = 0;
    UART3_SendStr(str); // 发送AT指令

    // 超时等待响应
    while(--time)
    {
        delay_ms(1);
        // 检测到OK或>(透传提示符),表示响应成功
        if(strstr((char *)wifi_recvbuf, "OK") || strstr((char *)wifi_recvbuf, ">"))
        {
            break;
        }
    }
    return time > 0; // 时间>0表示未超时,响应成功
}

/**
 * @brief  WIFI模块初始化(连接路由,进入透传模式,连接巴法云)
 * @param  None
 * @retval None
 * @note   步骤:退出透传→测试模块→设置Station模式→连接路由→透传模式→连接服务器→进入透传
 */
void WIFI_Config(void)
{
    uint8_t buf[512] = {0};
    UART3_Config(115200); // 初始化UART3(连接WIFI模块)

    UART3_SendStr("+++"); // 退出透传模式(部分模块需延时)
    delay_ms(1000);

    // 测试AT指令,确认模块在线
    if(WIFI_SendAT("AT\r\n", 5000))
        printf("WIFI模块在线\r\n");
    else
        printf("WIFI模块离线\r\n");

    // 设置WIFI为Station模式(连接路由),保存配置(重启生效)
    if(WIFI_SendAT("AT+CWMODE_DEF=1\r\n", 5000))
        printf("WIFI模式设置成功\r\n");
    else
        printf("WIFI模式设置失败\r\n");

    // 连接路由(SSID和密码由宏定义配置)
    sprintf((char *)buf, "AT+CWJAP_DEF=\"%s\",\"%s\"\r\n", WIFI_SSID, WIFI_PASSWD);
    if(WIFI_SendAT((char *)buf, 10000))
        printf("WIFI连接路由成功\r\n");
    else
        printf("WIFI连接路由失败\r\n");

    // 设置TCP透传模式
    if(WIFI_SendAT("AT+CIPMODE=1\r\n", 10000))
        printf("透传模式设置成功\r\n");
    else
        printf("透传模式设置失败\r\n");

    // 连接巴法云MQTT服务器(TCP连接)
    memset((char *)buf, 0, 512);
    sprintf((char *)buf, "AT+CIPSTART=\"TCP\",\"%s\",%d\r\n", BEMFA_SERVER, BEMFA_PORT);
    if(WIFI_SendAT((char *)buf, 10000))
        printf("连接巴法云成功\r\n");
    else
        printf("连接巴法云失败\r\n");

    // 进入透传模式(开始MQTT报文交互)
    if(WIFI_SendAT("AT+CIPSEND\r\n", 10000))
    {
        printf("进入透传成功\r\n");
        wifi_init_flag = 1; // 标记WIFI初始化完成
    }
    else
        printf("进入透传失败\r\n");
}

/**
 * @brief  发送MQTT CONNECT报文,连接巴法云服务器
 * @param  client_id:客户端ID(巴法云密钥)
 * @retval bool:true(连接成功),false(连接失败/超时)
 * @note   报文结构:固定报头(0x10)+ 剩余长度 + 可变报头 + 有效载荷(客户端ID)
 */
bool MQTT_Connect(char *client_id)
{
    int num = 3000; // 超时计数(约6秒)
    bool status = false;
    char sendbuf[BUFFERSIZE] = {0};
    int cnt = 0;

    // 1. 固定报头:报文类型0x10(CONNECT),无标志位
    sendbuf[cnt++] = 0x10;

    // 2. 计算剩余长度:可变报头(10字节)+ 有效载荷(2+客户端ID长度)
    int remainlen = 10 + (2 + strlen(client_id));
    // 变长编码剩余长度
    do
    {
        uint8_t encodedByte = remainlen % 128;
        remainlen /= 128;
        if(remainlen > 0) encodedByte |= 0x80; // 续位标志置1
        sendbuf[cnt++] = encodedByte;
    } while(remainlen > 0);

    // 3. 可变报头(10字节)
    sendbuf[cnt++] = 0x00; sendbuf[cnt++] = 0x04; // 协议名长度(4字节)
    sendbuf[cnt++] = 'M'; sendbuf[cnt++] = 'Q'; sendbuf[cnt++] = 'T'; sendbuf[cnt++] = 'T'; // 协议名
    sendbuf[cnt++] = 0x04; // 协议级别(MQTT 3.1.1)
    sendbuf[cnt++] = 0x02; // 连接标志(清理会话,无遗嘱、无用户名密码)
    sendbuf[cnt++] = 0x00; sendbuf[cnt++] = 0x3C; // 保持连接时间(60秒)

    // 4. 有效载荷(客户端ID,UTF-8编码)
    uint16_t id_len = strlen(client_id);
    sendbuf[cnt++] = BYTE1(id_len); // 客户端ID长度高字节
    sendbuf[cnt++] = BYTE0(id_len); // 客户端ID长度低字节
    strcpy(&sendbuf[cnt], client_id);
    cnt += id_len;

    // 5. 发送报文并等待响应
    memset((char *)wifi_recvbuf, 0, BUFFERSIZE);
    wifi_counter = 0;
    UART3_SendData(sendbuf , cnt); // 发送CONNECT报文

    // 等待CONNACK响应(固定报头0x20,剩余长度0x02)
    while(num--)
    {
        delay_ms(2);
        if(wifi_recvbuf[0] == 0x20 && wifi_recvbuf[1] == 0x02)
        {
            // 解析连接返回码(第4字节)
            switch(wifi_recvbuf[3])
            {
                case 0x00: printf("MQTT连接成功\r\n"); status = true; break;
                case 0x01: printf("连接失败:协议版本不支持\r\n"); break;
                case 0x02: printf("连接失败:客户端ID非法\r\n"); break;
                case 0x03: printf("连接失败:服务端不可用\r\n"); break;
                case 0x04: printf("连接失败:用户名/密码无效\r\n"); break;
                case 0x05: printf("连接失败:未授权\r\n"); break;
            }
            break;
        }
    }
    return status;
}

/**
 * @brief  MQTT发布消息(QoS0,最多一次)
 * @param  topic:发布主题
 * @param  msg:消息内容
 * @retval None
 * @note   报文结构:固定报头(0x30)+ 剩余长度 + 可变报头(主题)+ 有效载荷(消息)
 */
void MQTT_Publish_Qos0(char *topic, char *msg)
{
    char sendbuf[BUFFERSIZE] = {0};
    int cnt = 0;

    // 1. 固定报头:报文类型0x30(PUBLISH),QoS0,无DUP、RETAIN
    sendbuf[cnt++] = 0x30;

    // 2. 剩余长度:可变报头(2+主题长度)+ 有效载荷(消息长度)
    int remainlen = 2 + strlen(topic) + strlen(msg);
    // 变长编码剩余长度
    do
    {
        uint8_t encodedByte = remainlen % 128;
        remainlen /= 128;
        if(remainlen > 0) encodedByte |= 0x80;
        sendbuf[cnt++] = encodedByte;
    } while(remainlen > 0);

    // 3. 可变报头(主题,UTF-8编码)
    uint16_t topic_len = strlen(topic);
    sendbuf[cnt++] = BYTE1(topic_len);
    sendbuf[cnt++] = BYTE0(topic_len);
    strcpy(&sendbuf[cnt], topic);
    cnt += topic_len;

    // 4. 有效载荷(消息内容)
    strcpy(&sendbuf[cnt], msg);
    cnt += strlen(msg);

    // 5. 发送报文
    UART3_SendData(sendbuf , cnt);
}

/**
 * @brief  MQTT订阅主题(QoS0)
 * @param  topic:订阅主题
 * @param  id:报文标识符(1-65535,唯一)
 * @retval bool:true(订阅成功),false(订阅失败/超时)
 */
bool MQTT_Subscribe_Qos0(char *topic, uint16_t id)
{
    int num = 3000;
    bool status = false;
    char sendbuf[BUFFERSIZE] = {0};
    int cnt = 0;

    // 1. 固定报头:报文类型0x82(SUBSCRIBE),QoS1(订阅报文需QoS1)
    sendbuf[cnt++] = 0x82;

    // 2. 剩余长度:可变报头(2字节ID)+ 有效载荷(2+主题长度+1字节QoS)
    int remainlen = 2 + 2 + strlen(topic) + 1;
    // 变长编码剩余长度
    do
    {
        uint8_t encodedByte = remainlen % 128;
        remainlen /= 128;
        if(remainlen > 0) encodedByte |= 0x80;
        sendbuf[cnt++] = encodedByte;
    } while(remainlen > 0);

    // 3. 可变报头(报文标识符)
    sendbuf[cnt++] = BYTE1(id);
    sendbuf[cnt++] = BYTE0(id);

    // 4. 有效载荷(主题+QoS)
    uint16_t topic_len = strlen(topic);
    sendbuf[cnt++] = BYTE1(topic_len);
    sendbuf[cnt++] = BYTE0(topic_len);
    strcpy(&sendbuf[cnt], topic);
    cnt += topic_len;
    sendbuf[cnt++] = 0x00; // QoS0

    // 5. 发送报文并等待SUBACK响应
    memset((char *)wifi_recvbuf, 0, BUFFERSIZE);
    wifi_counter = 0;
    UART3_SendData(sendbuf , cnt);

    while(num--)
    {
        delay_ms(2);
        // SUBACK报文:固定报头0x90,剩余长度0x03
        if(wifi_recvbuf[0] == 0x90 && wifi_recvbuf[1] == 0x03)
        {
            // 解析订阅结果(第4字节)
            switch(wifi_recvbuf[4])
            {
                case 0x00: printf("订阅成功(QoS0)\r\n"); status = true; break;
                case 0x01: printf("订阅成功(QoS1)\r\n"); status = true; break;
                case 0x02: printf("订阅成功(QoS2)\r\n"); status = true; break;
                case 0x80: printf("订阅失败\r\n"); break;
            }
            break;
        }
    }
    return status;
}

中断服务函数与主函数

c 复制代码
/**
 * @brief  USART3中断服务函数(WIFI模块接收数据)
 * @param  None
 * @retval None
 * @note   接收到的数据存入缓冲区,溢出时重置计数
 */
void USART3_IRQHandler(void)
{
    if(USART_GetITStatus(USART3, USART_IT_RXNE) == SET)
    {
        if(wifi_counter < BUFFERSIZE)
        {
            wifi_recvbuf[wifi_counter++] = USART_ReceiveData(USART3);
        }
        else
        {
            wifi_counter = 0; // 缓冲区溢出,重置
        }
        USART_ClearITPendingBit(USART3, USART_IT_RXNE); // 清除中断标志
    }
}

/**
 * @brief  主函数(业务逻辑入口)
 * @param  None
 * @retval int:无实际返回(嵌入式无限循环)
 * @note   流程:初始化→MQTT连接→订阅主题→循环采集温湿度并发布
 */
int main(void)
{
    uint8_t temp = 0, hum = 0;
    char msg[32] = {0};

    // NVIC优先级分组(分组4:抢占优先级4位,响应优先级0位)
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);

    // 硬件初始化
    UART_PC_Init(115200);   // 蓝牙**控灯示例有该函数的具体实现**
    WIFI_Config();      // WIFI模块初始化
    DHT11_Init();       // DHT11传感器初始化

    // MQTT连接与订阅
    if(wifi_init_flag)
    {
        MQTT_Connect(BEMFA_CLIENT_UID);       // 连接巴法云
        MQTT_Subscribe_Qos0(TOPIC_TEMP, 100);  // 订阅主题(标识符100)
    }

    // 无限循环:采集温湿度并发布
    while(1)
    {
        if(DHT11_ReadTH(&temp, &hum))
        {
            // 格式化消息(符合巴法云数据格式:#温度#湿度#)
            sprintf(msg, "#%d#%d#", temp, hum);
            MQTT_Publish_Qos0(TOPIC_TEMP, msg); // 发布温湿度数据
            printf("发布数据:%s\r\n", msg);
        }
        else
        {
            printf("DHT11数据读取失败\r\n");
            MQTT_Publish_Qos0(TOPIC_TEMP, "#ERR#ERR#"); // 发布错误信息
        }
        delay_ms(5000); // 每5秒采集发布一次
    }
}

效果

关键注意事项

  • 巴法云客户端ID为设备唯一密钥,需从巴法云平台获取,不可泄露。
  • WIFI模块需支持AT指令(如ESP8266),上电后需等待模块稳定(建议延时1秒)。
  • DHT11时序对延时精度要求高,若读取失败,需检查延时函数准确性、接线是否牢固(上拉电阻不可少)。
  • MQTT保持连接时间需合理设置,建议60秒,客户端需在超时前发送心跳或消息,避免被服务端断开连接。
  • 缓冲区大小需根据消息长度调整,避免发布长消息时溢出。
相关推荐
lingzhilab2 小时前
零知IDE——零知标准板+INA219电流传感器的锂电池智能充放电监测系统
ide·stm32·单片机
F1331689295714 小时前
5G矿山车载监控终端山河矿卡定位监控终端
stm32·单片机·嵌入式硬件·5g·51单片机·硬件工程
vsropy14 小时前
keil5无法注释中文
stm32·单片机
csdn_te_download_00415 小时前
Keil5安装教程 基于C51 安装教程与配置完全指南
stm32·单片机·嵌入式硬件
送外卖的工程师15 小时前
STM32F103 驱动 BMP280 气压温湿度传感器 + OLED 显示教程
stm32·单片机·嵌入式硬件·mcu·物联网·proteus·rtdbs
2501_9277730715 小时前
嵌入式51单片机——中断
stm32·单片机·嵌入式硬件
tianyazhichiC16 小时前
stm32f103 标准外设库下载
stm32·单片机·嵌入式硬件
乡野码圣17 小时前
【RK3588 Android12】固件烧录与启动
stm32·单片机·嵌入式硬件
金色光环19 小时前
【SCPI学习】STM32与LWIP实现SCPI命令解析
stm32·嵌入式硬件·算法·scpi学习·scpi