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秒,客户端需在超时前发送心跳或消息,避免被服务端断开连接。
- 缓冲区大小需根据消息长度调整,避免发布长消息时溢出。