STM32:ESP8266 + MQTT 云端与报文全解析

发布于:2025-06-05 ⋅ 阅读:(30) ⋅ 点赞:(0)

知识点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_revbuf
		data_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);
	}
}

结束

代码重在练习!

代码重在练习!

代码重在练习!

今天的分享就到此结束了,希望对你有所帮助,如果你喜欢我的分享,请点赞收藏加关注,谢谢大家!!!


网站公告

今日签到

点亮在社区的每一天
去签到