STM32:ESP8266 + MQTT 云端与报文全解析
知识点1【MQTT的概述】
1、概述
MQTT是一种基于发布/订阅模式的轻量级应用层协议,运行在TCP/IP协议之上,专用物联网(IoT)和机器对机器(M2M)设计,其核心目标是低带宽,高延迟或不稳定网络环境下实现可靠的消息传输,尤其适用于资源受限的设备。
-
关键点
1、基于 发布/订阅:对实时性要求不高
2、应用层协议
3、低带宽:轻量级传输
4、高延迟 可靠
5、专用 物联网 和 嵌入式 设备间通信
6、基于TCP/IP协议基础之上
2、透传模式
透传模式:一种数据通信方式
特点:不对传输的数据进行任何解析,封装或修改。仅是将数据从一段传输到另一端。
3、回显模式
回显模式:串口通信 和 AT指令交互中 的一种基础功能。将 设备 接收到的指令原样返回给发送端。
4、心跳包
心跳包:用于 维持长连接,检测连接状态 的一种机制。功能:定期发送小型数据包,确保通信双方能够感知到对方的存活状态
知识点2【WIFI和MQTT的关系】
我接下来要介绍的是 ESP8266 与 MQTT 一起实现上云(Thingscloud)操作
1、层次不同
WIFI:物理层和数据链路层——负责设备间的无线连接,提供数据传输的通道
MQTT:应用层——定义设备间传输消息的格式
2、功能分工
WIFI:为设备提供互联网接入,确保数据能够在互联网中传输
MQTT:在已经建立的网络连接上,通过 订阅/发布 模式管理消息,实现低宽带,高延迟环境下的可靠通信。
知识点3【QoS介绍】
QoS级别 | 传递保证 | 重复风险 | 传输流程 | 适用场景 |
---|---|---|---|---|
QoS 0 | 最多一次(At most once) | 可能丢失 | 消息发送后不等待确认,无重试机制。 | 非关键数据(如周期性传感器读数) |
QoS 1 | 至少一次(At least once) | 可能重复 | 发送方存储消息直到收到确认(PUBACK),否则重发。 | 需要可靠传输但允许重复(如状态更新) |
QoS 2 | 恰好一次(Exactly once) | 无重复 | 四次握手(PUBREC/PUBREL/PUBCOMP),确保消息唯一性。 | 关键指令(如支付、设备控制) |
知识点4【MQTT报文分析】
MQTT的报文类型有很多,这里我仅介绍一下比较重要的 连接报文,订阅报文,发布报文
以上报文均是由三部分组成:固定报头,可变报头,有效载荷
一、三者定义与作用
操作 | 定义 | 核心作用 |
---|---|---|
连接(Connection) | 客户端(如设备)与MQTT代理(Broker)建立通信链路的过程。 | 建立通信通道,为订阅和发布提供基础。 |
订阅(Subscribe) | 客户端向代理注册对某个**主题(Topic)**的兴趣,声明希望接收该主题的消息。 | 接收特定主题的消息,实现“监听”功能。 |
发布(Publish) | 客户端或代理向某个**主题(Topic)**发送消息,消息会被路由给所有订阅者。 | 传递数据,驱动系统行为(如控制指令)。 |
1、连接报文:CONNECT
(1)固定报头
可变报头 | 功能介绍 | 数值 |
---|---|---|
byte1 | 高4bits是报文类型,后4bits是保留位 | 0x10 |
byte2 | 剩余长度:可变报头 + 有效载荷 的字节数 | … |
(2)可变报头
我们这一用的协议都是 “MQTT”
byte1 | 协议长度的高4位 | 0x00 |
---|---|---|
byte2 | 协议长度的第四位 | 0x04 |
byte3 | ‘M’ | |
byte4 | ‘Q’ | |
byte5 | ‘T’ | |
byte6 | ‘T’ | |
byte7 | 协议的版本号,我们是3.1.1版本对应的是4 | 0x04 |
byte8配置的是连接标志,我们单独介绍
连接标志
bit7:用户名标志
**作用:**声明 CONNECT 报文 中 是否包含用户名(在有效载荷中)
取值 | 功能 |
---|---|
1 | 有效载荷包含用户名 |
0 | 无用户名 |
bit6:密码标志
**作用:**声明 CONNECT 报文 中 是否包含密码(在有效载荷中)
取值 | 功能 |
---|---|
1 | 有效载荷中包含密码 |
0 | 无密码 |
bit5:遗嘱保留
**作用:**控制服务器是否将 遗嘱消息 作为保留消息存储
取值 | 功能 |
---|---|
1 | 遗嘱消息保留在服务器,新订阅者会立即受到该消息 |
0 | 不保留 |
注意:仅当 bit2 = 1
(启动遗嘱)时有效
bit4 - 3:遗嘱服务质量
**作用:**定义遗嘱消息的 服务质量等级(QoS)
取值 | 功能 |
---|---|
00 | QoS 0(最多一次) |
01 | QoS 1(至少一次) |
10 | QoS 2(恰好一次) |
11 | 保留值(禁止使用) |
注意:仅当 bit2 = 1
(启动遗嘱)时有效
bit2:遗嘱标志
作用:声明客户端是否设置了遗嘱消息(设备 异常断线 时 触发的消息)
取值 | 功能 |
---|---|
1 | 遗嘱消息有效 |
0 | 遗嘱消息无效 |
bit1:清理会话
**作用:**声明
取值 | 功能 |
---|---|
1 | 清理会话: |
连接断开后,服务器丢弃所有订阅和未确认消息 |
| 0 | 持久会话: 服务器保留订阅和未确认消息(QOS1/2),重连恢复 |
bit0:保留位
**作用:**协议保留位,必须是0
(3)有效载荷
CONNECT的有效载荷(PAYLOAD) 包含一个或多个长度为前缀的字段,可变报头中的标志位决定是否包含这些字段
字段需要按照顺序一下出现:客户端标识符,遗嘱主题,遗嘱消息,用户名,密码
2、订阅报文:SUBSCRIBE
(1)固定报文
可变报头 | 功能介绍 | 数值 |
---|---|---|
byte1 | 高4bits是报文类型,SUBSCRIBE的类型值是 8 | |
后4bits是标志位,必须是 2 | 0x82 | |
byte2 | 剩余长度:可变报头 + 有效载荷 的字节数 | 可变字节编程 |
(2)可变报文
byte1 | 报文标识符的高4位 | |
---|---|---|
byte2 | 报文标识符的第四位 |
报文标识符(Packet Identifier):用来区别客户端对多个订阅请求的应答(SUBACK)
Packet Identifier 从1开始递增(客户端自行管理),0不可用
一般第一个订阅包使用1,切不能重复使用正在等待的ID,但是对应的 Packet Identifier 收到SUBACK后就可以再次使用该值了。
(3)有效载荷
有效载荷 | 功能介绍 |
---|---|
byte1 | 主题长度的高8位 |
byte2 | 主题长度的低8位 |
byte3~N | 主题名 |
byteN+1 | 服务质量等级(QoS),仅 低两bits 有效 |
3、发布报文:PUBLISH
(1)固定报文
可变报头 | 功能介绍 | 数值 |
---|---|---|
byte1 7-4 | 高4bits是报文类型 | 0011→3 |
bit 3 | DUP标志:0表示首次发送,1表示重发 | |
bits 2–1 | QoS等级 | |
bit 0 | 保留 |
DUP介绍:
目的:告诉接收端:“这条消息可能是重发的副本,请不要当作全新消息去处理多次。”
一般用于QoS1模式下
在QoS2模式下,PUBLISH→PUBREC→PUBREL→PUBCOMP
任何一个阶段超时,也需要重发,也需要DUP = 1
(2)可变报文
以上是以主题名位“a/b”举例的
可变报头 | 功能介绍 |
---|---|
byte1 | 主题名长度的高8位 |
byte2 | 主题名长度的8位 |
byte3-N | 主题名 |
补充:
这里补充一下PUBACK/PUBREC/PUBCOMP
这三个都是针对于 PUBLISH 的,在不同的QoS在会有不同的流程
- QoS 0:直接 PUBLISH,不要任何 ACK
- QoS 1:PUBLISH → PUBACK
- QoS 2:PUBLISH → PUBREC → PUBREL → PUBCOMP
(3)有效载荷
要发送的应用消息内容存储在有效载荷当中
知识点4【代码演示】
代码实现是利用AT指令将 WIFI 与 服务器(安信可透传云)建立连接,MQTT层面的还没有写,每天将补充AT指令
安信可透传云的连接:
安信可透传云 V1.0
main.c
#include "stm32f10x.h"
#include "stm32f10x_conf.h"
#include "rs485.h"
#include "esp8266.h"
#include "delay.h"int main(void)
{Systick_Init(72000);//优先级组的配置NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);USART1_Config(115200);USART3_Config(115200);printf("你好\\n");ESP8266_CMD_Init();while(1){ if(data_esp8266.over_flag){data_esp8266.over_flag = 0;data_esp8266.recv_size = 0;memset(data_esp8266.recv_data, 0, sizeof(data_esp8266.recv_data));}}
}
esp.h
#ifndef _ESP8266_H_
#define _ESP8266_H_
#include "stm32f10x.h"
#include "stm32f10x_conf.h"
#include "string.h"
#include "delay.h"
//GPIO 与 PIN口的宏定义
//使用的是USART3 TX:PB10 RX:PB11
#define GPIO_USART3_TXRX GPIOB
#define PIN_USART3_TX GPIO_Pin_10
#define PIN_USART3_RX GPIO_Pin_11
#define WIFI_ID "11223344"
#define WIFI_PASSWORD "12345678"
#define SER_ADDR "36.137.226.30"typedef struct
{u8 recv_data[256];u16 recv_size;u8 over_flag;
}DATA_ESP8266;extern DATA_ESP8266 data_esp8266;
void ESP8266_CMD_Init(void);
void USART3_Config(u32 baud);
void USART3_SendByte(u8 data);
void USART3_SendStr(u8* data);
void USART1_IRQHandler(void);
void USART3_IRQHandler(void);
uint8_t ESP8266_SetMode(uint8_t *cmd,uint8_t *ack1,uint8_t *ack2,uint8_t count);#endif
esp.c
#include "esp8266.h"//存储 接收数据的数组DATA_ESP8266 data_esp8266 = {0};
void ESP8266_CMD_Init(void)
{char send_cmd[256]={0};printf("AT+RST......\\r\\n");
// while(!ESP8266_SetMode("AT+RST\\r\\n","OK",NULL,10));ESP8266_SetMode("AT+RST\\r\\n","OK",NULL,10);printf("\\r\\n");Delay_ms(2000);printf("ATE0......\\r\\n");ESP8266_SetMode("AT+CWMODE=1\\r\\n","OK",NULL,10);memset(send_cmd,0,sizeof(send_cmd));printf("正在设置热点连接......\\r\\n");sprintf(send_cmd,"AT+CWJAP=\\"%s\\",\\"%s\\"\\r\\n",WIFI_ID,WIFI_PASSWORD);ESP8266_SetMode(send_cmd,"OK",NULL,500);Delay_ms(2000);memset(send_cmd,0,sizeof(send_cmd));printf("正在设置单链接......\\r\\n");ESP8266_SetMode("AT+CIPMUX=0\\r\\n","OK",NULL,10);Delay_ms(2000);memset(send_cmd,0,sizeof(send_cmd));printf("正在设置服务端连接信息......\\r\\n");sprintf(send_cmd,"AT+CIPSTART=\\"TCP\\",\\"%s\\",%d\\r\\n",SER_ADDR,35270);ESP8266_SetMode(send_cmd,"OK",NULL,50);Delay_ms(2000);memset(send_cmd,0,sizeof(send_cmd));printf("正在设置透传......\\r\\n");ESP8266_SetMode("AT+CIPMODE=1\\r\\n","OK",NULL,10);Delay_ms(2000);memset(send_cmd,0,sizeof(send_cmd));printf("连接服务器准备发送数据......\\r\\n");ESP8266_SetMode("AT+CIPSEND\\r\\n",">",NULL,10);Delay_ms(2000);
}uint8_t ESP8266_SetMode(uint8_t *cmd,uint8_t *ack1,uint8_t *ack2,uint8_t count)
{//1.发送字符串--AT指令集USART3_SendStr(cmd);//接收返回值,判断返回值是否正确if(data_esp8266.over_flag==1){//esp_revbufdata_esp8266.over_flag=0;while(count--) //count--10{if((strstr((char *)data_esp8266.recv_data,(char *)ack1)!=NULL)||(strstr((char *)data_esp8266.recv_data,(char *)ack2)!=NULL)){Delay_ms(100);//本次AT指令发送成功printf("CMD SEND OK!!\\r\\n");return 1;}}}memset(data_esp8266.recv_data,0,sizeof(data_esp8266.recv_data));return 0;}void USART3_Config(u32 baud)
{GPIO_InitTypeDef GPIO_InitStruct;USART_InitTypeDef USART_InitStruct;NVIC_InitTypeDef NVIC_InitStruct;//时钟配置RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3,ENABLE);//模式配置GPIO_StructInit(&GPIO_InitStruct);GPIO_InitStruct.GPIO_Pin = PIN_USART3_TX;GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;GPIO_Init(GPIO_USART3_TXRX,&GPIO_InitStruct);GPIO_InitStruct.GPIO_Pin = PIN_USART3_RX;GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;GPIO_Init(GPIO_USART3_TXRX,&GPIO_InitStruct);//串口初始化USART_StructInit(&USART_InitStruct);USART_InitStruct.USART_BaudRate = baud;USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;USART_InitStruct.USART_Parity = USART_Parity_No;USART_InitStruct.USART_StopBits = USART_StopBits_1;USART_InitStruct.USART_WordLength = USART_WordLength_8b;USART_Init(USART3,&USART_InitStruct);//中断使能USART_ITConfig(USART3,USART_IT_RXNE,ENABLE);USART_ITConfig(USART3,USART_IT_IDLE,ENABLE);//中断配置NVIC_InitStruct.NVIC_IRQChannel = USART3_IRQn;NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0x01;NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0x01;NVIC_Init(&NVIC_InitStruct);//串口使能USART_Cmd(USART3,ENABLE);}void USART3_SendByte(u8 data)
{USART3->DR = data;while(!USART_GetFlagStatus(USART3,USART_FLAG_TXE));
}void USART3_SendStr(u8* data)
{while(*data){USART3_SendByte(*data);data++;}while(!USART_GetFlagStatus(USART3,USART_FLAG_TC));
}void USART1_IRQHandler(void)
{u8 data;//接收中断:存储后,通过USART1发送(调试助手)if(USART_GetITStatus(USART1,USART_IT_RXNE)){data = USART1->DR;USART3_SendByte(data);USART_ClearITPendingBit(USART1,USART_IT_RXNE);}
}void USART3_IRQHandler(void)
{//接收中断:存储后,通过USART1发送(调试助手)if(USART_GetITStatus(USART3,USART_IT_RXNE)){data_esp8266.recv_data[data_esp8266.recv_size] = USART3->DR;USART1->DR = data_esp8266.recv_data[data_esp8266.recv_size++];while(!USART_GetFlagStatus(USART1,USART_FLAG_TXE));USART_ClearITPendingBit(USART3,USART_IT_RXNE);}//空闲中断if(USART_GetITStatus(USART3,USART_IT_IDLE)){USART3->SR;USART3->DR;data_esp8266.over_flag = 1;//memset(data_esp8266.recv_data,0,sizeof(data_esp8266.recv_data));data_esp8266.recv_size = 0;//防止影响下一次接收USART_ClearITPendingBit(USART3, USART_IT_IDLE);}
}
结束
代码重在练习!
代码重在练习!
代码重在练习!
今天的分享就到此结束了,希望对你有所帮助,如果你喜欢我的分享,请点赞收藏加关注,谢谢大家!!!