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 分别接在
PB10和PB11上。 -
配置 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()
-
检测 ESP8266 是否正常(发送
AT) -
设置工作模式为 STA(普通客户端)
-
扫描周围热点(
AT+CWLAP,可选) -
连接指定 SSID 和密码(
AT+CWJAP="ssid","password") -
查询 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
核心操作:
