STM32 单片机 ESP8266 联网 和 MQTT协议

1、原理图分析:

ESP8266 模块通过 USART3 与 MCU 进行通信。

2、AT 指令:

ESP8266 的串口固件(如 AT Firmware)内置了一套命令解释器。

用户只要通过串口发送如 AT+xxxx 的指令,就能远程操作 Wi-Fi 模块。

用法类似于你在电脑里输入命令行------只不过是通过串口输入给模块的。

常见的 AT 指令

功能 AT 指令 说明
检查模块是否正常 AT 返回 OK 说明正常
重启模块 AT+RST 重启 Wi-Fi 模块
设置工作模式 AT+CWMODE=1 1=Station,2=AP,3=AP+Station
连接路由器 AT+CWJAP="SSID","password" 连接 Wi-Fi
列出可用 Wi-Fi AT+CWLAP 扫描 Wi-Fi 热点
建立 TCP 连接 AT+CIPSTART="TCP","192.168.1.10",8080 建立 TCP
发送数据 AT+CIPSEND=5 发送 5 字节数据
退出连接 AT+CIPCLOSE 关闭连接

3、ESP8266代码实现:

1. 硬件初始化
  • 使能 GPIOB 和 USART3 外设时钟

    用于 ESP8266 与 STM32 通信的串口是 USART3。它的 TX/RX 分别接在 PB10PB11 上。

  • 配置 GPIOB 的 USART 引脚

    • PB10:复用推挽输出(TX)

    • PB11:浮空输入(RX)

2. 配置 USART3
  • 采用 8N1 格式

    • 8 位数据位

    • 无校验

    • 1 个停止位

  • 设置波特率(如 115200)

  • 使能 USART3 接收和发送功能

3. 开启 USART3 中断
  • 启动两个中断事件:

    • RXNE:接收寄存器非空(读取每个字节)

    • IDLE:总线空闲,表示一帧数据已经完全接收

  • 配置 NVIC,将 USART3 配置到合适的优先级并使能

4. 中断处理逻辑:USART3_IRQHandler
  • RXNE 事件 :一个字节接收完成,存入缓冲数组 esp8266_value.data

  • IDLE 事件:接收一帧数据完成,触发处理逻辑

    • 使用 strstr() 检测 JSON 数据中的命令内容

    • 控制 LED、舵机等外设动作

    • 转发接收数据到 PC 调试串口(USART1

5. AT 指令封装发送

封装了 ESP8266_Send_AT() 函数:

  • 通过 USART3 发送 AT 命令

  • 等待 ESP8266 响应预期的字符串(如 "OK")

  • 超时则返回失败

6. 连接 WiFi:ESP8266_Connect()
  1. 检测 ESP8266 是否正常(发送 AT

  2. 设置工作模式为 STA(普通客户端)

  3. 扫描周围热点(AT+CWLAP,可选)

  4. 连接指定 SSID 和密码(AT+CWJAP="ssid","password"

  5. 查询 IP 地址等网络信息(AT+CIFSR

cpp 复制代码
#include "esp8266.h"

ESP8266_Data esp8266_value = {0};

void ESP8266_Init(void)
{

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;            // 配置引脚编号为 10 
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 输出速度为 50MHz 
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;   // 工作模式为复用推挽输出模式
	
	GPIO_Init(GPIOB, &GPIO_InitStructure); 
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;        // 配置引脚编号为 11
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 配置当前 10 号引脚对应浮空输入模式
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	USART_InitTypeDef USART_InitStructure;
	
	// USART 对应的波特率 115200
	USART_InitStructure.USART_BaudRate = 115200;
	// 数据帧有效数据位个数为 8
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;
	// 不使用 USART 数据校验位
	USART_InitStructure.USART_Parity = USART_Parity_No;
	// 数据停止位为 1 位
	USART_InitStructure.USART_StopBits = USART_StopBits_1;
	// 串口配置工作模式,选择打开 USART TX 和 RX
	USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
	// 串口硬件控制配置
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	// USART3 初始化
	USART_Init(USART3, &USART_InitStructure);
	NVIC_InitTypeDef NVIC_InitStructure;
	
	// 当前注册中断对应的 IRQn ,当前注册终端为 USART3_IRQn
	NVIC_InitStructure.NVIC_IRQChannel = USART3_IRQn;
	// 当前 USART3 中断优先级中的【占先优先级】为 0
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
	// 当前 USART3 中断优先级中的【次级优先级】为 1
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	// 告知内核,当前中断开始状态。
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	
	USART_ITConfig(USART3, USART_IT_IDLE, ENABLE);
	USART_ITConfig(USART3, USART_IT_RXNE, ENABLE);
	
	// NVIC 初始化
	NVIC_Init(&NVIC_InitStructure);
	
	USART_Cmd(USART3, ENABLE);
}


void USART3_IRQHandler(void)
{
	u8 temp = 0;
	
	if (USART_GetITStatus(USART3, USART_IT_IDLE) == SET)
	{
		// flag 标志位为 1 表示数据已接收完毕
		esp8266_value.flag = 1;	
		// 【中断清除】清除当前 USART3 USART_IT_IDLE 中断
		temp = USART3->SR;
		temp = USART3->DR;
		
		if (strstr((const char *)(esp8266_value.data + 5), "\"Light0\":false"))
		{
			Led0_Ctrl(0);
		} 
		else if (strstr((const char *)(esp8266_value.data + 5), "\"Light0\":true"))
		{
			Led0_Ctrl(1);
		}
		else if (strstr((const char *)(esp8266_value.data + 5), "\"Light1\":false"))
		{
			Led1_Ctrl(0);
		}
		else if (strstr((const char *)(esp8266_value.data + 5), "\"Light1\":true"))
		{
			Led1_Ctrl(1);
		}
		
		
		// 利用 USART1 将数据发送到 PC 端串口调试工具
		USART1_SendBuffer(esp8266_value.data, esp8266_value.count);
	}
	
	// 当前 ESP8266 USART3 数据接收标志位。
	if (esp8266_value.flag)
	{
		memset(&esp8266_value, 0, sizeof(ESP8266_Data));
	}
	
	if (USART_GetITStatus(USART3, USART_IT_RXNE) == SET)
	{
		esp8266_value.data[esp8266_value.count++] = USART_ReceiveData(USART3);
		
		// 清除中断
		temp = USART3->DR;
		
		// 如果数据接收已满,当前 flag 标志位为 1
		if (esp8266_value.count == ESP8266_DATA_SIZE)
		{
			esp8266_value.flag = 1;	
			// TODO 利用 USART1 将数据发送到 PC 端串口调试工具
			USART1_SendBuffer(esp8266_value.data, esp8266_value.count);
		}
	}
}

void ESP8266_SendByte(u8 byte) 
{
	while (USART_GetFlagStatus(USART3, USART_FLAG_TC) == RESET);
	
	USART_SendData(USART3, byte);
}

void ESP8266_SendBuffer(u8 *buffer, u16 count)
{
	while (count--)
	{
		ESP8266_SendByte(*buffer);
		buffer += 1;
	}
}

void ESP8266_SendString(const char *str)
{
	while (*str)
	{
		ESP8266_SendByte(*str);
		str += 1;
	}
}

u8 ESP8266_Send_AT(const char *cmd, const char * ack, u16 timeout)
{
	// 利用 USART1 和 PC 端串口调试工具数据传递,展示当前发送的 AT 指令
	USART1_SendString("AT : ");
	USART1_SendString(cmd);
	USART1_SendString("\r\n");
	ESP8266_SendString(cmd);
	
	while (timeout--)
	{
		if (esp8266_value.flag)
		{
			if (strstr((const char *)(esp8266_value.data), ack))
			{
				return 0;
			}
			else
			{
				return 1;
			}
		}
	}
	
	return 1;
}


void ESP8266_Connect(const char *ssid, const char *psk)
{
	USART1_SendString("ESP8266 WiFi Connect!\r\n");
	
	// 1. USART3 发送 "AT" 指令到 ESP8266 查看当前设备状态
	ESP8266_Send_AT("AT\r\n", "OK", 2000);
	// 延时预留 ESP8266 响应时间
	Delay_ms(1000);
	
	// 2. 设置当前 ESP8266 工作模式,选择 STA(Station) 模式
	ESP8266_Send_AT("AT+CWMODE=1\r\n", "OK", 2000);
	Delay_ms(1000);
	
	ESP8266_Send_AT("AT+CWLAP\r\n", NULL, 2000);
	Delay_ms(5000);
	
	char at_connect_ap[128] = "";
	sprintf(at_connect_ap, "AT+CWJAP=\"%s\",\"%s\"\r\n", ssid, psk);
	ESP8266_Send_AT(at_connect_ap, "OK", 2000);
	
	Delay_ms(5000);
	
	ESP8266_Send_AT("AT+CIFSR\r\n", "OK", 2000);

	Delay_ms(2000);
	
}

4、MQTT协议

前面单片机通过 ESP8266 连接上网络,但还没有定义通信格式或内容。也就是说,此时你只是接入了"网络",但没有规定:

  • 如何传输数据?

  • 数据长什么样?

  • 谁发给谁?怎么识别?

  • 有无加密或订阅机制?

这就像你连上了高速公路,但不知道去哪条道,也没有导航或红绿灯。因此我们需要同意一种协议来规范数据的格式。

MQTT 是一种应用层协议,你需要它来"说话"

**MQTT(Message Queuing Telemetry Transport)**是一种轻量级、基于发布/订阅的通信协议,非常适合物联网设备。

它的特点:

  • 低带宽占用,适合嵌入式系统

  • 设备之间通过"主题 topic"通讯,不直接互相知道对方 IP

  • 由服务器(Broker)中转消息 → 解耦发送者和接收者

  • 支持 QoS(消息质量等级)、心跳保活、离线遗嘱等重要特性

所以,如果你想让单片机通过网络联网,并与云平台或 APP 通信,就必须依靠 MQTT 或其他协议(如 HTTP、WebSocket 等)来构建框架。

👉 ESP8266 自带的 AT 指令并不会自动处理 MQTT 协议,你需要在单片机程序中:

  • 用 AT 指令调用 MQTT 连接、订阅、发布等接口

  • 或者自己实现协议(通常不推荐)

MQTT协议数据包组成:

常用的报文类型有 CONNECT,PUBLISH,SUBSCRIBE

报文对应标志位信息:

在固定头第二个字节开始对应的是数据包剩余长度

Fixed Header 剩余字节长度 = 可变头 variable header 字节数 + 有效载荷 payload 字节数。

CONNECT 报文(连接服务端):

可变头 Variable Header(固定10字节)

字节位置 长度 含义
0-1 2 字节 协议名长度(0x00 0x04,表示 4 字节)
2-5 4 字节 协议名"MQTT"
6 1 字节 协议级别(0x04 表示 MQTT v3.1.1)
7 1 字节 连接标志位(例如是否有用户名、密码、遗嘱等)
8-9 2 字节 保持连接时间(Keep Alive,单位:秒)
cpp 复制代码
MQTT Connect 报文组成
	u8 MQTT_Connect_Data[256] = "";
Fixed_Header
	MQTT_Connect_Data[0] 是报文类型 + 报文标志位
		0001 0000 ==> 0x10
	MQTT_Connect_Data[1] 是报文剩余长度,对应 可变头 + 有效载荷
		可变头(10) + 有效载荷(19) ==> 0001 1101 ==> 29 ==> 0x1D
		
Variable_Header 
	【协议内容】
		可变头第一个组成部分是 协议名称数据提交方式为 【2 + N】 模式
		需要提供的数据是协议长度(2 Byte) + 协议内容,当前是使用 MQTT 协议,对应内容为 MQTT 字符串
		协议长度 => 4 个字节,协议内容 => MQTT
			MQTT_Connect_Data[2] 数据长度高位 MSB   4 / 256 ==> 0000 0000 ==>0x00
			MQTT_Connect_Data[3] 数据长度低位 LSB    4 % 256 ==> 0000 0100 ==> 0x04
			MQTT_Connect_Data[4] 'M' ==> 0100 1101 ==> 0x4D
			MQTT_Connect_Data[5] 'Q' ==> 0101 0001 ==> 0x51
			MQTT_Connect_Data[6] 'T' ==>  0101 0100 ==> 0x54
			MQTT_Connect_Data[7] 'T' ==>  0101 0100 ==> 0x54
	【协议版本】
		当前使用的协议版本是 MQTT 3.1.1 采用一个字节数据来告知 MQTT 服务器对应的版本信息
			MQTT_Connect_Data[8] ==> 0000 0100 ==> 0x04
	【连接标志 Connect Flags】
		组成部分是告知当前 MQTT 协议后续的载荷内容,每一个标志位告示后续的数据中,
		是否包含对应的数据内容
		Username(占1位) Password(占1位) Will Retain(占1位) Will QoS(占2位) Will Flag(占1位)  Clean Session(占1位) 保留(占1位)
		根据 ThingsCloud 分析,需要提供用户名和密码,同时按照常规内容,不需要返回,QoS=0 flag 不需要,Clean Session 需要
			MQTT_Connect_Data[9]  ==> 1100 0010 ==> 0xC2
	【Keep Alive 时间】
		MQTT 客户端和服务器之间发送数据的最大间隔,超出范围登录退出。
		需要两个字节 16 位来描述对应的时间
		分别对应 
			假设 Keep Alive ==> 60 
			Keep Alive 时间 MSB 高位 MQTT_Connect_Data[10] ==> 60 / 256 ==> 0000 0000 ==> 0x00
			Keep Alive 时间 LSB 低位  MQTT_Connect_Data[11] ==> 60 % 256 ==> 0011 1100 ==> 0x3CD

Payload
	Payload 字节数 = 2 + 客户端名称字节数 + 2 + 用户名字节数 + 2 + 密码字节数
				= 2 + 9 + 2 + 2 + 2 + 2 ==> 19
	根据当前报文分析和标志位分析,当前有效载荷的组成部分是,全部采用 2 + N 模式
	【客户端标识符】假设客户端标识符/客户端名称是 Client_GL
		2 + N 模式分析
		2 表示当前数据的字节个数 ==> 9 byte
			MQTT_Connect_Data[12] 数据长度高位 MSB ==> 9 / 256 ==> 0000 0000 ==> 0x00
			MQTT_Connect_Data[13] 数据长度低位 LSB ==> 9 % 256 ==> 0000 1001 ==> 0x09
		N 对应的数据,按照字节方式处理
			MQTT_Connect_Data[14] ==> 'C'
			MQTT_Connect_Data[15] ==> 'l'
			MQTT_Connect_Data[16] ==> 'i'
			MQTT_Connect_Data[17] ==> 'e'
			MQTT_Connect_Data[18] ==> 'n'
			MQTT_Connect_Data[19] ==> 't'
			MQTT_Connect_Data[20] ==> '_'
			MQTT_Connect_Data[21] ==> 'G'
			MQTT_Connect_Data[22] ==> 'L'
	【用户名】假设用户名 CG
		2 + N 模式分析
		2 表示当前数据的字节个数 ==> 2 byte
			MQTT_Connect_Data[23] 数据长度高位 MSB ==> 2 / 256 ==> 0000 0000 ==> 0x00
			MQTT_Connect_Data[24] 数据长度低位 LSB ==> 2 % 256 ==> 0000 0010 ==> 0x02
		N 对应的数据,按照字节方式处理
			MQTT_Connect_Data[25] ==> 'C'
			MQTT_Connect_Data[26] ==> 'G'

	【密码】假设密码 HL
		2 + N 模式分析
		2 表示当前数据的字节个数 ==> 2 byte
			MQTT_Connect_Data[27] 数据长度高位 MSB ==> 2 / 256 ==> 0000 0000 ==> 0x00
			MQTT_Connect_Data[28] 数据长度低位 LSB ==> 2 % 256 ==> 0000 0010 ==> 0x02
		N 对应的数据,按照字节方式处理
			MQTT_Connect_Data[29] ==> 'H'
			MQTT_Connect_Data[30] ==> 'L'

PUBLISH 报文(发布消息报文):

可变头:

  • 2 + Topic Name 长度(如果 QoS 为 0)

  • 4 + Topic Name 长度(如果 QoS 为 1 或 2,需要 Packet Identifier)

cpp 复制代码
MQTT PUBLISH  报文组成
	u8 MQTT_Publish_Data[256] = "";
Fixed_Header
	MQTT_Publish_Data[0] 是报文类型 + 报文标志位
		0011 0000 ==> 0x30
	MQTT_Publish_Data[1] 是报文剩余长度,对应 可变头 + 有效载荷
		XXXX XXXX ==> 可变头(2 +  strlen("attributes")) +
		
Variable_Header 
	【发布主题】
	ThingsCloud 要求的发布主题为 attributes,首先可变头中的是 2 + N
	    发布主题名称字节长度高位和低位
	MQTT_Publish_Data[2] = strlen("attributes") / 256;
	MQTT_Publish_Data[3] = strlen("attributes") % 256;
	  memcpy(&MQTT_Publish_Data[4], "attributes", strlen("attributes"));
	
Payload
	有效载荷是当前提交的 JSON 格式字符串,直接在 Variable_Header 拼接即可
	"{\"Temp\":25.3,\"Hum\":55}"
	memcpy(&MQTT_Publish_Data[4 + strlen("attributes")], "{\"Temp\":25.3,\"Hum\":55}"
, 	strlen("{\"Temp\":25.3,\"Hum\":55}"))

SUBSCRIBE 报文(订阅报文,用于接收下发消息):

cpp 复制代码
MQTT SUBSCRIBE  报文组成
	u8 MQTT_Subscirbe_Data[256] = "";
Fixed_Header
	MQTT_Subscirbe_Data[0] 是报文类型 + 报文标志位
		1000 0000 ==> 0x80
	MQTT_Subscirbe_Data[1] 是报文剩余长度,对应 可变头 + 有效载荷
		2 + 2 +  strlen("attributes/push") + 1
Variable_Header 
	2 个字节,订阅主题数据包 ID,因为当前使用的是 QoS=0 服务等级
	完全可以自定义
	MQTT_Subscirbe_Data[2] ==> 0x00
	MQTT_Subscirbe_Data[3] ==> 0x08
	
Payload
	2 + N + 1
	2 ==> MSB + LSB
		MQTT_Subscirbe_Data[4] ==> strlen("attributes/push") / 256
		MQTT_Subscirbe_Data[5] ==> strlen("attributes/push") % 256

        N ==> 订阅主题字符串数据
		memcpy(&MQTT_Subscirbe_Data[6],"attributes/push", strlen("attributes/push"));
        1 ==> QoS = 0
		MQTT_Subscirbe_Data[6 + strlen("attributes/push")] = 0x00

核心操作:

相关推荐
古译汉书11 小时前
Stm32江科大入门教程--各章节详细笔记---查阅传送门
数据结构·stm32·单片机·嵌入式硬件·算法
塔能物联运维11 小时前
物联网运维中基于数字孪生的实时设备状态同步与仿真验证技术
运维·物联网
brave and determined12 小时前
硬件-内存学习DAY20——GDDR6革命:显存技术如何重塑游戏与AI未来
嵌入式硬件·ddr·ddr4·ddr5·ddr6·内存原理·内存技术
一个学Java小白12 小时前
LV.5 文件IO
stm32·单片机·嵌入式硬件
电鱼智能的电小鱼13 小时前
基于电鱼 ARM 工控机的煤矿主控系统高可靠运行方案——让井下控制系统告别“死机与重启”
arm开发·人工智能·嵌入式硬件·深度学习·机器学习
sheepwjl13 小时前
《嵌入式硬件(十八):基于IMX6ULL的ADC操作》
单片机·嵌入式硬件·imx6ull·adc
2301_8059629315 小时前
AXF文件变量地址查找完全指南
stm32
BMS苦研者15 小时前
芯片选型避坑指南:如何根据需求快速筛选MCU
单片机·嵌入式硬件
国科安芯17 小时前
基于AS32A601型MCU芯片的屏幕驱动IC方案的技术研究
服务器·人工智能·单片机·嵌入式硬件·fpga开发