前言
本篇基于 STM32F103C8T6 最小系统板,搭配 ESP8266‑01、DHT11 温湿度传感器、OLED 屏幕完成整套物联网 Demo。实现功能:单片机采集环境温湿度,OLED 本地显示;ESP8266 开启 AP 热点搭建 TCP 服务器,手机 TCP 客户端连接热点后接收温湿度数据;手机发送指令123,设备返回I get it!,完成简单的双向交互。
整体工作流程:DHT11 采集环境温湿度 → STM32 读取传感器数据 → OLED 本地显示 → STM32 串口发送 AT 指令控制 ESP8266 → ESP8266 搭建 WiFi 热点 + TCP 服务器 → 手机连接热点接收温湿度 → 手机下发指令,设备应答回复。
整体系统分层
- 传感与显示层:DHT11 测量温湿度,OLED 屏幕做本地状态显示
- 控制层:STM32 负责传感器读取、数据格式化、指令判断逻辑
- 网络层:ESP8266 负责 WiFi 热点创建、TCP 连接维护、网络数据收发
简单理解:ESP8266 在这里充当串口转 WiFi 转换器,STM32 不需要处理复杂 WiFi 协议栈,只需要通过 AT 指令就可以实现网络通信。

一、硬件接线说明
⚠️重点注意:串口 TX、RX 必须交叉相连;不同模块 GND 必须共地;ESP8266 必须 3.3V 供电,严禁接 5V!
| 模块引脚 | STM32F103C8T6 引脚 | 功能说明 |
|---|---|---|
| ESP8266 VCC | 3.3V | ESP8266 模块电源,禁止 5V 输入 |
| ESP8266 TX | PA10(USART1_RX) | ESP8266 向 STM32 输出数据 |
| ESP8266 RX | PA9(USART1_TX) | STM32 向 ESP8266 发送 AT 指令 |
| ESP8266 GND | GND | 共地,通信正常必要条件 |
| DHT11 VCC | 5V | DHT11 传感器供电 |
| DHT11 DATA | PA11 | DHT11 单总线数据引脚 |
| DHT11 GND | GND | 传感器地 |
| OLED VCC | 5V | OLED 屏幕供电 |
| OLED SCL | PB8 | 软件 I2C 时钟 |
| OLED SDA | PB9 | 软件 I2C 数据 |
| OLED GND | GND | 屏幕地 |
1. 串口交叉接线原理
串口通信规则:发送端必须接对方接收端
- STM32 PA9(TX) → ESP8266 RX
- STM32 PA10(RX) → ESP8266 TX
- 双方 GND 连接在一起
如果 TX 接 TX、RX 接 RX,两个设备完全无法收发任何数据。

2.ESP8266 供电坑点
ESP8266 使用 WiFi 发射瞬间电流大,供电不足会出现各种异常现象:
- 模块反复上电重启
- 可以搜索到热点,手机一连接热点直接消失
- 发送网络数据异常、模块自动复位
- 串口偶尔打印 ready,代表模块不断重启
裸 ESP‑01 模块:EN/CH_PD 引脚必须上拉 3.3V;GPIO0、GPIO2 保持正常启动电平;带底板的 ESP8266 模块已经处理好该部分引脚,无需额外操作。 如果使用独立电源给 ESP8266 供电,电源 GND 一定要和 STM32GND 相连。
二、ESP8266 AP 热点 + TCP 服务器工作原理
本项目 ESP8266 使用 AT 固件,内部自带 WiFi 射频电路和完整 TCP/IP 协议栈,STM32 不需要实现复杂无线网络逻辑,只发送 AT 指令控制模块。
1.AP 热点模式
AP 即无线接入点,ESP8266 自己生成一个 WiFi 热点,不需要路由器,不需要外网,手机直接连接这个热点。
项目配置参数:
- WiFi 名称:
ESP8266_WIFI - WiFi 密码:
12345678 - 加密方式:WPA2‑PSK
- 信道:1
- ESP8266 本机 IP:
192.168.4.1
2.TCP 服务器模式
开启热点之后,ESP8266 在 8080 端口启动 TCP 服务器。手机作为 TCP 客户端接入。
手机 TCP 客户端配置参数:
- 工作模式:TCP Client
- 服务器 IP:
192.168.4.1 - 端口号:
8080 - 收发格式:ASCII 文本格式
流程说明:手机连接 ESP 热点之后,建立 TCP 连接;设备周期性把温湿度数据通过 TCP 发给手机;手机发送文本指令,网络数据通过 ESP8266 串口转发给 STM32 单片机。
注意:WiFi 密码用于连接热点,TCP 连接本身没有密码,本项目仅适合局域网教学演示,不能直接用作正式产品。
三、ESP8266 AT 指令初始化流程
单片机上电之后调用 ESP8266 初始化函数,串口使用 USART1,波特率 115200,8N1。按照顺序发送 AT 指令完成模块配置。
初始化步骤:
- 初始化 USART1 外设,PA9、PA10 引脚
- 发送
AT:检测模块是否正常在线 - 发送
ATE0:关闭指令回显,减少串口多余输出 AT+CWMODE=2:设置模块为 AP 热点模式AT+CWSAP=xxx:配置热点名称、密码、信道、加密AT+CIPMODE=0:普通 TCP 传输模式AT+CIPMUX=1:开启多客户端连接AT+CIPDINFO=0:简化接收数据帧格式AT+CIPSERVER=1,8080:开启 8080 端口 TCP 服务器- 等待手机 TCP 客户端接入
常用 AT 指令说明:
| AT 指令 | 功能描述 |
|---|---|
AT | 测试模块是否正常响应 |
ATE0 | 关闭命令回显 |
AT+CWMODE=2 | 设置 AP 热点模式 |
AT+CWSAP="ESP8266_WIFI","12345678",1,3 | 设置热点名称密码加密 |
AT+CIPMUX=1 | 开启多连接 |
AT+CIPSERVER=1,8080 | 启动 TCP 服务器 8080 端口 |
AT+CIPSEND=<ID>,<长度> | 向指定客户端发送数据 |
提示:不同版本 AT 固件存在差异,部分固件需要使用
_CUR后缀指令,程序内部做了兼容处理。
四、串口中断 + 环形缓冲区接收机制
ESP8266 返回串口数据时机不确定,项目采用串口接收中断 + 环形缓冲区方案。
- USART1 收到 1 字节数据立刻触发接收中断;
- 在中断服务函数里面,只把字节存入 1024 字节环形缓冲区,中断中不做字符串解析,不发送 AT 指令,保证中断执行时间极短;
- 主循环再从环形缓冲区读取字节,解析 AT 响应、解析
+IPD网络数据包。
环形缓冲区两个指针:
- head:写入位置,中断使用
- tail:读取位置,主程序使用
优点:就算主程序正在读取 DHT11、刷新 OLED,串口过来的数据也不会直接丢失。
如果主程序长时间不读取缓冲区,会出现缓冲区溢出,程序标记溢出标志丢弃新字节,保护旧数据。
五、业务逻辑工作流程
1. 温湿度数据发送流程
DHT11 读取成功后,把温湿度转为字符串,类似Temp:25.0C,Humi:60.0%,发送给手机。
- STM32 发送 AT 查询连接状态,获取客户端 ID
- 发送
AT+CIPSEND=ID,数据长度告诉 ESP 要发送多少字节 - ESP 返回
>符号,代表准备就绪 - STM32 发送温湿度文本数据
- ESP8266 把数据通过 WiFiTCP 发送手机,返回
SEND OK代表发送成功
需要指定发送长度原因:TCP 数据包可以包含任意字符,ESP 需要区分 “有效负载” 和后续 AT 命令。 项目约 2 秒采集一次温湿度,兼顾观察效果,避免数据刷屏。
2. 手机下发指令解析回复
手机发送文本123,ESP8266 串口输出+IPD,0,3:123
+IPD:网络数据到来标记0:客户端连接 ID3:有效数据字节长度123:手机发送实际内容
程序解析逻辑:
- 在缓冲区寻找
+IPD帧头 - 解析客户端 ID、数据长度
- 根据长度取出网络收到的数据
- 剔除换行、空格、回车字符
- 判断内容等于
123,则调用发送接口回复I get it!
只有纯 123 指令触发回复;
123\r\n也可以识别;0123、1234、abc123不会触发应答。 数据解析、回复发送全部放在主循环,不在中断中执行,保障系统稳定性。
3. 主循环调度逻辑
- 轮询处理 ESP8266 网络数据,判断是否收到手机指令;收到 123 则回复应答,更新 OLED 状态标记
- 判断是否到达 2 秒 DHT11 采样周期
- 周期到读取 DHT11 传感器;读取成功就发送温湿度数据,刷新 OLED 屏幕
- ESP8266 初始化失败,程序每 5 秒自动重试初始化,处理模块上电慢、临时通信故障。
OLED 屏幕显示状态说明:
DHT11 + WiFi
Temp:25.0C
Humi:60.0%
APP:DATA SENT
底部状态标识含义:
| OLED 显示状态 | 含义 |
|---|---|
| APP:DATA SENT | 温湿度成功发送手机 |
| APP:WAIT | WiFi 热点正常,TCP 客户端还未连接 |
| APP RX:123 OK | 成功收到手机 123 指令 |
| WiFi:AT ERR | ESP8266 不响应 AT 指令 |
| WiFi:AP ERR | 热点配置失败 |
| Sensor Error | DHT11 读取失败,检查接线供电 |
六、源代码存放区
📌本节用于存放完整工程源码。 工程文件说明:
ESP8266.h:WiFi 参数、端口、缓冲区、热点账号密码配置ESP8266.c:ESP 初始化、AT 指令发送、串口中断、IPD 解析、数据发送、轮询处理函数main.c:主函数,业务调度逻辑
main.c代码
/* STM32F103C8T6 teaching demo: DHT11 + OLED + ESP8266. */
#include "delay.h"
#include "sys.h"
#include "oled.h"
#include "dht11.h"
#include "ESP8266.h"
#define SENSOR_REFRESH_MS 2000u
#define WIFI_RETRY_MS 5000u
#define MAIN_LOOP_MS 20u
#define WIFI_POLL_MS 10u
static void Show_Startup(const char *message);
static void Show_DHT11_Data(u8 humiH, u8 humiL,
u8 tempH, u8 tempL,
u8 wifiStatus, u8 appStatus,
u8 commandReceived);
static void Show_DHT11_Error(u8 wifiStatus);
int main(void)
{
u8 humiH;
u8 humiL;
u8 tempH;
u8 tempL;
u8 commandReceived;
ESP8266_Status wifiStatus;
ESP8266_Status appStatus;
ESP8266_Status pollStatus;
u16 sensorTimer;
u16 wifiRetryTimer;
humiH = 0u;
humiL = 0u;
tempH = 0u;
tempL = 0u;
commandReceived = 0u;
appStatus = ESP8266_ERROR_NO_CLIENT;
sensorTimer = SENSOR_REFRESH_MS;
wifiRetryTimer = 0u;
delay_init();
NVIC_Configuration();
OLED_Init();
OLED_ColorTurn(0u);
OLED_DisplayTurn(0u);
DHT11_Init();
Show_Startup("WiFi starting");
wifiStatus = ESP8266_Init();
while (1)
{
if (wifiStatus == ESP8266_OK)
{
/* Poll often so a phone command is answered without waiting for
* the next two-second DHT11 sample. */
pollStatus = ESP8266_Poll(WIFI_POLL_MS);
if (pollStatus != ESP8266_OK)
{
appStatus = pollStatus;
}
}
else
{
delay_ms(WIFI_POLL_MS);
}
if (ESP8266_TakeCommandReceived() != 0u)
{
commandReceived = 1u;
}
delay_ms((u16)(MAIN_LOOP_MS - WIFI_POLL_MS));
sensorTimer = (u16)(sensorTimer + MAIN_LOOP_MS);
if (wifiStatus != ESP8266_OK)
{
wifiRetryTimer = (u16)(wifiRetryTimer + MAIN_LOOP_MS);
if (wifiRetryTimer >= WIFI_RETRY_MS)
{
Show_Startup("WiFi retry");
wifiStatus = ESP8266_Init();
wifiRetryTimer = 0u;
}
}
if (sensorTimer >= SENSOR_REFRESH_MS)
{
sensorTimer = 0u;
if (DHT11_Read_Data(&humiH, &humiL, &tempH, &tempL) == 0u)
{
if (wifiStatus == ESP8266_OK)
{
appStatus = ESP8266_SendTemperatureHumidity(tempH, tempL,
humiH, humiL);
}
else
{
appStatus = wifiStatus;
}
Show_DHT11_Data(humiH, humiL, tempH, tempL,
wifiStatus, appStatus, commandReceived);
commandReceived = 0u;
}
else
{
Show_DHT11_Error(wifiStatus);
if (wifiStatus == ESP8266_OK)
{
(void)ESP8266_SendStringToClient("DHT11 read error\r\n");
}
DHT11_Init();
}
}
}
}
static void Show_Startup(const char *message)
{
OLED_Clear();
OLED_ShowString(16u, 4u, (u8 *)"DHT11 + WiFi", 16u);
OLED_ShowString(8u, 28u, (u8 *)message, 16u);
OLED_ShowString(4u, 50u, (u8 *)"AP:ESP8266_WIFI", 12u);
OLED_Refresh();
}
static void Show_DHT11_Data(u8 humiH, u8 humiL,
u8 tempH, u8 tempL,
u8 wifiStatus, u8 appStatus,
u8 commandReceived)
{
OLED_Clear();
OLED_ShowString(16u, 0u, (u8 *)"DHT11 + WiFi", 16u);
OLED_ShowString(0u, 18u, (u8 *)"Temp:", 16u);
OLED_ShowNum(48u, 18u, tempH, 2u, 16u);
OLED_ShowString(64u, 18u, (u8 *)".", 16u);
OLED_ShowNum(72u, 18u, tempL, 1u, 16u);
OLED_ShowString(88u, 18u, (u8 *)"C", 16u);
OLED_ShowString(0u, 34u, (u8 *)"Humi:", 16u);
OLED_ShowNum(48u, 34u, humiH, 2u, 16u);
OLED_ShowString(64u, 34u, (u8 *)".", 16u);
OLED_ShowNum(72u, 34u, humiL, 1u, 16u);
OLED_ShowString(88u, 34u, (u8 *)"%", 16u);
if (commandReceived != 0u)
{
OLED_ShowString(0u, 52u, (u8 *)"APP RX:123 OK", 12u);
}
else if (wifiStatus != ESP8266_OK)
{
OLED_ShowString(0u, 52u, (u8 *)"WiFi:", 12u);
OLED_ShowString(36u, 52u, (u8 *)ESP8266_StatusText(wifiStatus), 12u);
}
else if (appStatus == ESP8266_OK)
{
OLED_ShowString(0u, 52u, (u8 *)"APP:DATA SENT", 12u);
}
else
{
OLED_ShowString(0u, 52u, (u8 *)"APP:WAIT", 12u);
}
OLED_Refresh();
}
static void Show_DHT11_Error(u8 wifiStatus)
{
OLED_Clear();
OLED_ShowString(16u, 0u, (u8 *)"DHT11 + WiFi", 16u);
OLED_ShowString(8u, 22u, (u8 *)"Sensor Error", 16u);
OLED_ShowString(0u, 44u, (u8 *)"Check PA11/VCC", 12u);
OLED_ShowString(0u, 54u, (u8 *)"WiFi:", 12u);
OLED_ShowString(36u, 54u, (u8 *)ESP8266_StatusText(wifiStatus), 12u);
OLED_Refresh();
}
ESP8266.c代码
/*
* 文件名称:ESP8266.c
* 文件编码:UTF-8
* 功能说明:使用 STM32F10x 标准外设库驱动 ESP8266,完成 AP 热点、
* TCP 服务器、温湿度上报以及手机指令接收和自动回复。
*/
#include "ESP8266.h"
#include "delay.h"
#include <stdio.h>
#include <string.h>
#if (ESP8266_RX_BUFFER_SIZE < 64u)
#error "ESP8266_RX_BUFFER_SIZE must be at least 64"
#endif
#if (ESP8266_RX_RING_SIZE < 2u)
#error "ESP8266_RX_RING_SIZE must be at least 2"
#endif
#if (ESP8266_MAX_CLIENT_ID > 9u)
#error "ESP8266_MAX_CLIENT_ID must be 0..9"
#endif
#define ESP8266_IPD_NORMAL 0u
#define ESP8266_IPD_HEADER 1u
#define ESP8266_IPD_PAYLOAD 2u
#define ESP8266_IPD_HEADER_SIZE 32u
#define ESP8266_IPD_PAYLOAD_SIZE 16u
static char esp8266_rx_buffer[ESP8266_RX_BUFFER_SIZE];
static volatile uint8_t esp8266_rx_ring[ESP8266_RX_RING_SIZE];
static volatile uint16_t esp8266_rx_head;
static volatile uint16_t esp8266_rx_tail;
static volatile uint8_t esp8266_rx_overflow;
static uint8_t esp8266_last_client_id = ESP8266_NO_CLIENT_ID;
static uint8_t esp8266_pending_reply_id = ESP8266_NO_CLIENT_ID;
static uint8_t esp8266_command_received;
static uint8_t esp8266_ipd_state;
static uint8_t esp8266_ipd_prefix_index;
static char esp8266_ipd_header[ESP8266_IPD_HEADER_SIZE];
static uint8_t esp8266_ipd_header_length;
static char esp8266_ipd_payload[ESP8266_IPD_PAYLOAD_SIZE];
static uint8_t esp8266_ipd_payload_length;
static uint16_t esp8266_ipd_remaining;
static uint8_t esp8266_ipd_client_id = ESP8266_NO_CLIENT_ID;
/* ======================== 环形接收缓冲区 ======================== */
/* 计算环形缓冲区的下一个下标,到达末尾后自动回到 0。 */
static uint16_t ESP8266_RingNext(uint16_t index)
{
index++;
if (index >= ESP8266_RX_RING_SIZE)
{
index = 0u;
}
return index;
}
/* 丢弃环形缓冲区内尚未处理的数据,并清除溢出标志。 */
void ESP8266_ClearRxBuffer(void)
{
esp8266_rx_tail = esp8266_rx_head;
esp8266_rx_overflow = 0u;
}
/* 查询 USART1 中断环形缓冲区中是否存在可读数据。 */
uint8_t ESP8266_RxAvailable(void)
{
return (esp8266_rx_head != esp8266_rx_tail) ? 1u : 0u;
}
/* 从环形缓冲区取出一个字节;缓冲区为空时返回 0。 */
uint8_t ESP8266_ReadRxByte(void)
{
uint8_t data;
if (esp8266_rx_head == esp8266_rx_tail)
{
return 0u;
}
data = esp8266_rx_ring[esp8266_rx_tail];
esp8266_rx_tail = ESP8266_RingNext(esp8266_rx_tail);
return data;
}
/* 返回环形缓冲区溢出标志,用于诊断主循环处理速度是否过慢。 */
uint8_t ESP8266_RxOverflowed(void)
{
return esp8266_rx_overflow;
}
/*
* 将中断收到的字节写入环形缓冲区。缓冲区已满时保留原有数据,
* 丢弃当前字节并设置溢出标志。
*/
void ESP8266_OnReceivedByte(uint8_t data)
{
uint16_t next_head;
next_head = ESP8266_RingNext(esp8266_rx_head);
if (next_head == esp8266_rx_tail)
{
esp8266_rx_overflow = 1u;
return;
}
esp8266_rx_ring[esp8266_rx_head] = data;
esp8266_rx_head = next_head;
}
/* ======================== +IPD 数据帧解析 ======================== */
/* 复位 TCP 数据帧状态机,准备寻找下一段“+IPD,”帧头。 */
static void ESP8266_ResetIpdParser(void)
{
esp8266_ipd_state = ESP8266_IPD_NORMAL;
esp8266_ipd_prefix_index = 0u;
esp8266_ipd_header_length = 0u;
esp8266_ipd_payload_length = 0u;
esp8266_ipd_remaining = 0u;
esp8266_ipd_client_id = ESP8266_NO_CLIENT_ID;
}
/*
* 从字符串当前位置解析一个无符号十进制数。
* 解析成功时更新字符串指针和数值,并返回 1;失败时返回 0。
*/
static uint8_t ESP8266_ParseNumber(const char **text, uint16_t *value)
{
const char *p;
uint32_t result;
uint8_t has_digit;
p = *text;
result = 0u;
has_digit = 0u;
while ((*p >= '0') && (*p <= '9'))
{
has_digit = 1u;
result = result * 10u + (uint32_t)(*p - '0');
if (result > 65535u)
{
return 0u;
}
p++;
}
if (has_digit == 0u)
{
return 0u;
}
*text = p;
*value = (uint16_t)result;
return 1u;
}
/*
* 解析“+IPD,<连接ID>,<数据长度>:”帧头,保存客户端 ID 和负载长度。
* 同时兼容单连接模式下不带连接 ID 的“+IPD,<数据长度>:”格式。
*/
static uint8_t ESP8266_ParseIpdHeader(void)
{
const char *p;
uint16_t first_number;
uint16_t data_length;
uint8_t client_id;
esp8266_ipd_header[esp8266_ipd_header_length] = '\0';
p = esp8266_ipd_header;
if (ESP8266_ParseNumber(&p, &first_number) == 0u)
{
return 0u;
}
if (*p == ',')
{
if (first_number > ESP8266_MAX_CLIENT_ID)
{
return 0u;
}
client_id = (uint8_t)first_number;
p++;
if (ESP8266_ParseNumber(&p, &data_length) == 0u)
{
return 0u;
}
}
else
{
/* 兼容单连接模式的 +IPD,<数据长度>: 格式。 */
data_length = first_number;
client_id = esp8266_last_client_id;
}
esp8266_ipd_client_id = client_id;
esp8266_ipd_remaining = data_length;
esp8266_ipd_payload_length = 0u;
if (client_id <= ESP8266_MAX_CLIENT_ID)
{
esp8266_last_client_id = client_id;
}
return 1u;
}
/*
* 一帧 TCP 负载接收完成后的处理函数。
* 去除首尾空白字符后,如果负载严格等于“123”,则记录指令标志和
* 发出该指令的客户端 ID,稍后由轮询函数回复“I get it!”。
*/
static void ESP8266_FinishIpdPayload(void)
{
uint8_t first;
uint8_t last;
first = 0u;
last = esp8266_ipd_payload_length;
while ((first < last) &&
((esp8266_ipd_payload[first] == ' ') ||
(esp8266_ipd_payload[first] == '\r') ||
(esp8266_ipd_payload[first] == '\n') ||
(esp8266_ipd_payload[first] == '\t')))
{
first++;
}
while ((last > first) &&
((esp8266_ipd_payload[last - 1u] == ' ') ||
(esp8266_ipd_payload[last - 1u] == '\r') ||
(esp8266_ipd_payload[last - 1u] == '\n') ||
(esp8266_ipd_payload[last - 1u] == '\t')))
{
last--;
}
if (((uint8_t)(last - first) == 3u) &&
(esp8266_ipd_payload[first] == '1') &&
(esp8266_ipd_payload[first + 1u] == '2') &&
(esp8266_ipd_payload[first + 2u] == '3') &&
(esp8266_ipd_client_id <= ESP8266_MAX_CLIENT_ID))
{
esp8266_command_received = 1u;
esp8266_pending_reply_id = esp8266_ipd_client_id;
}
ESP8266_ResetIpdParser();
}
/*
* 将每个串口接收字节送入独立的 +IPD 状态机。
* 即使程序正在等待 AT 指令的 OK 或 SEND OK 响应,夹在响应中的手机
* 数据也会被识别,避免“123”指令丢失。
*/
static void ESP8266_ParseIncomingByte(uint8_t data)
{
static const char prefix[] = "+IPD,";
if (esp8266_ipd_state == ESP8266_IPD_PAYLOAD)
{
if (esp8266_ipd_payload_length < ESP8266_IPD_PAYLOAD_SIZE)
{
esp8266_ipd_payload[esp8266_ipd_payload_length] = (char)data;
esp8266_ipd_payload_length++;
}
if (esp8266_ipd_remaining > 0u)
{
esp8266_ipd_remaining--;
}
if (esp8266_ipd_remaining == 0u)
{
ESP8266_FinishIpdPayload();
}
return;
}
if (esp8266_ipd_state == ESP8266_IPD_HEADER)
{
if (data == ':')
{
if (ESP8266_ParseIpdHeader() == 0u)
{
ESP8266_ResetIpdParser();
}
else if (esp8266_ipd_remaining == 0u)
{
ESP8266_FinishIpdPayload();
}
else
{
esp8266_ipd_state = ESP8266_IPD_PAYLOAD;
}
}
else if (esp8266_ipd_header_length < (ESP8266_IPD_HEADER_SIZE - 1u))
{
esp8266_ipd_header[esp8266_ipd_header_length] = (char)data;
esp8266_ipd_header_length++;
}
else
{
ESP8266_ResetIpdParser();
}
return;
}
if (data == (uint8_t)prefix[esp8266_ipd_prefix_index])
{
esp8266_ipd_prefix_index++;
if (prefix[esp8266_ipd_prefix_index] == '\0')
{
esp8266_ipd_state = ESP8266_IPD_HEADER;
esp8266_ipd_header_length = 0u;
esp8266_ipd_prefix_index = 0u;
}
}
else
{
esp8266_ipd_prefix_index = (data == '+') ? 1u : 0u;
}
}
/* ======================== USART1 底层驱动 ======================== */
/*
* 初始化 PA9、PA10、USART1 和接收中断。
* ESP8266 默认使用 8 位数据位、1 位停止位、无校验、无流控。
*/
static void ESP8266_DefaultPlatformInit(uint32_t baudrate)
{
GPIO_InitTypeDef gpio;
USART_InitTypeDef usart;
NVIC_InitTypeDef nvic;
RCC_APB2PeriphClockCmd(ESP8266_GPIO_CLK | RCC_APB2Periph_AFIO, ENABLE);
RCC_APB2PeriphClockCmd(ESP8266_USART_CLK, ENABLE);
GPIO_StructInit(&gpio);
gpio.GPIO_Pin = ESP8266_TX_PIN;
gpio.GPIO_Mode = GPIO_Mode_AF_PP;
gpio.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(ESP8266_GPIO_PORT, &gpio);
gpio.GPIO_Pin = ESP8266_RX_PIN;
gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(ESP8266_GPIO_PORT, &gpio);
USART_DeInit(ESP8266_USART);
USART_StructInit(&usart);
usart.USART_BaudRate = baudrate;
usart.USART_WordLength = USART_WordLength_8b;
usart.USART_StopBits = USART_StopBits_1;
usart.USART_Parity = USART_Parity_No;
usart.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
usart.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(ESP8266_USART, &usart);
ESP8266_ClearRxBuffer();
nvic.NVIC_IRQChannel = ESP8266_USART_IRQ;
nvic.NVIC_IRQChannelPreemptionPriority = ESP8266_IRQ_PREEMPT_PRIORITY;
nvic.NVIC_IRQChannelSubPriority = ESP8266_IRQ_SUB_PRIORITY;
nvic.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&nvic);
USART_ITConfig(ESP8266_USART, USART_IT_RXNE, ENABLE);
USART_Cmd(ESP8266_USART, ENABLE);
}
/* 处理 USART1 接收和溢出错误,并把收到的字节写入环形缓冲区。 */
void ESP8266_USARTIrqHandler(void)
{
volatile uint16_t data;
if (USART_GetITStatus(ESP8266_USART, USART_IT_RXNE) != RESET)
{
data = USART_ReceiveData(ESP8266_USART);
ESP8266_OnReceivedByte((uint8_t)(data & 0x00FFu));
}
if (USART_GetFlagStatus(ESP8266_USART, USART_FLAG_ORE) == SET)
{
data = USART_ReceiveData(ESP8266_USART);
(void)data;
}
}
/* STM32F103 启动文件所需的 USART1 实际中断入口。 */
void USART1_IRQHandler(void)
{
ESP8266_USARTIrqHandler();
}
/* 等待 USART1 发送数据寄存器变为空闲。 */
static void ESP8266_WaitTxEmpty(void)
{
while (USART_GetFlagStatus(ESP8266_USART, USART_FLAG_TXE) == RESET)
{
}
}
/* 通过 USART1 发送一个字节。 */
static void ESP8266_WriteByte(uint8_t data)
{
ESP8266_WaitTxEmpty();
USART_SendData(ESP8266_USART, (uint16_t)data);
}
/* 发送以“\0”结尾的字符串,并等待最后一个字节发送完成。 */
static void ESP8266_WriteString(const char *text)
{
if (text == 0)
{
return;
}
while (*text != '\0')
{
ESP8266_WriteByte((uint8_t)*text);
text++;
}
while (USART_GetFlagStatus(ESP8266_USART, USART_FLAG_TC) == RESET)
{
}
}
/* 发送指定长度的二进制数据,并等待最后一个字节发送完成。 */
static void ESP8266_WriteBuffer(const uint8_t *buffer, uint16_t length)
{
uint16_t i;
for (i = 0u; i < length; i++)
{
ESP8266_WriteByte(buffer[i]);
}
while (USART_GetFlagStatus(ESP8266_USART, USART_FLAG_TC) == RESET)
{
}
}
/*
* 在发送新 AT 指令前处理已经到达的串口数据。
* 数据只交给 +IPD 解析器处理,不会被误认为新 AT 指令的响应。
*/
static void ESP8266_DrainPendingInput(void)
{
uint8_t data;
while (ESP8266_RxAvailable() != 0u)
{
data = ESP8266_ReadRxByte();
ESP8266_ParseIncomingByte(data);
}
}
/*
* 等待 ESP8266 返回指定关键字,并把完整接收过程同步送给 +IPD 解析器。
* 检测到 ERROR、FAIL 或 busy 时提前返回响应错误。
*/
static ESP8266_Status ESP8266_WaitForResponse(const char *reply1,
const char *reply2,
uint16_t timeout_ms)
{
uint16_t elapsed_ms;
uint16_t index;
uint8_t data;
elapsed_ms = 0u;
index = 0u;
esp8266_rx_buffer[0] = '\0';
while (elapsed_ms < timeout_ms)
{
while (ESP8266_RxAvailable() != 0u)
{
data = ESP8266_ReadRxByte();
ESP8266_ParseIncomingByte(data);
if (index < (ESP8266_RX_BUFFER_SIZE - 1u))
{
esp8266_rx_buffer[index] = (char)data;
index++;
esp8266_rx_buffer[index] = '\0';
}
if ((reply1 != 0) && (strstr(esp8266_rx_buffer, reply1) != 0))
{
return ESP8266_OK;
}
if ((reply2 != 0) && (strstr(esp8266_rx_buffer, reply2) != 0))
{
return ESP8266_OK;
}
if ((strstr(esp8266_rx_buffer, "ERROR") != 0) ||
(strstr(esp8266_rx_buffer, "FAIL") != 0) ||
(strstr(esp8266_rx_buffer, "busy") != 0))
{
return ESP8266_ERROR_RESPONSE;
}
}
delay_ms(1u);
elapsed_ms++;
}
return ESP8266_ERROR_TIMEOUT;
}
/* ======================== AT 指令与初始化流程 ===================== */
/* 清空硬件接收寄存器、软件环形缓冲区、+IPD 状态机和响应字符串。 */
void ESP8266_Clear(void)
{
volatile uint16_t data;
while (USART_GetFlagStatus(ESP8266_USART, USART_FLAG_RXNE) == SET)
{
data = USART_ReceiveData(ESP8266_USART);
(void)data;
}
ESP8266_ClearRxBuffer();
ESP8266_ResetIpdParser();
esp8266_rx_buffer[0] = '\0';
}
/*
* 发送一条自动追加回车换行的 AT 指令,并等待 reply1 或 reply2。
* 两个期望响应都为空时只发送指令,不执行等待。
*/
ESP8266_Status ESP8266_SendCommand(const char *command,
const char *reply1,
const char *reply2,
uint16_t timeout_ms)
{
if (command == 0)
{
return ESP8266_ERROR_ARGUMENT;
}
/* 先解析已经排队的 +IPD 数据,不能在发送 AT 指令前直接丢弃。 */
ESP8266_DrainPendingInput();
esp8266_rx_buffer[0] = '\0';
ESP8266_WriteString(command);
ESP8266_WriteString("\r\n");
if ((reply1 == 0) && (reply2 == 0))
{
return ESP8266_OK;
}
return ESP8266_WaitForResponse(reply1, reply2, timeout_ms);
}
/* 设置 ESP8266 为 AP 热点模式,并兼容新旧两种 AT 指令名称。 */
static ESP8266_Status ESP8266_SetApMode(void)
{
ESP8266_Status status;
status = ESP8266_SendCommand("AT+CWMODE=2", "OK", "no change", 3000u);
if (status == ESP8266_OK)
{
return ESP8266_OK;
}
return ESP8266_SendCommand("AT+CWMODE_CUR=2", "OK", "no change", 3000u);
}
/* 根据配置生成 AT+CWSAP 指令,设置热点名称、密码、信道和加密方式。 */
static ESP8266_Status ESP8266_BuildAp(const ESP8266_ApServerConfig *config)
{
char command[ESP8266_COMMAND_BUFFER_SIZE];
ESP8266_Status status;
sprintf(command, "AT+CWSAP=\"%s\",\"%s\",%u,%u",
config->ssid, config->password,
(unsigned int)config->channel,
(unsigned int)config->encryption);
status = ESP8266_SendCommand(command, "OK", 0, 5000u);
if (status == ESP8266_OK)
{
return ESP8266_OK;
}
sprintf(command, "AT+CWSAP_CUR=\"%s\",\"%s\",%u,%u",
config->ssid, config->password,
(unsigned int)config->channel,
(unsigned int)config->encryption);
return ESP8266_SendCommand(command, "OK", 0, 5000u);
}
/*
* 设置普通传输和多连接模式,关闭扩展 IPD 信息,然后启动 TCP 服务器。
*/
static ESP8266_Status ESP8266_StartServer(const ESP8266_ApServerConfig *config)
{
char command[40];
ESP8266_Status status;
(void)ESP8266_SendCommand("AT+CIPSERVER=0", "OK", "ERROR", 1000u);
status = ESP8266_SendCommand("AT+CIPMODE=0", "OK", 0, 1000u);
if (status != ESP8266_OK)
{
return status;
}
status = ESP8266_SendCommand("AT+CIPMUX=1", "OK", 0, 1000u);
if (status != ESP8266_OK)
{
return status;
}
/* 保持简单的 +IPD,<连接ID>,<数据长度>: 帧头格式。 */
(void)ESP8266_SendCommand("AT+CIPDINFO=0", "OK", "ERROR", 1000u);
sprintf(command, "AT+CIPSERVER=1,%u", (unsigned int)config->port);
status = ESP8266_SendCommand(command, "OK", 0, 2000u);
if (status != ESP8266_OK)
{
return status;
}
sprintf(command, "AT+CIPSTO=%u", (unsigned int)config->timeout_s);
(void)ESP8266_SendCommand(command, "OK", "ERROR", 1000u);
return ESP8266_OK;
}
/* 检查热点名称、密码、信道、加密方式和 TCP 端口是否合法。 */
static ESP8266_Status ESP8266_CheckConfig(const ESP8266_ApServerConfig *config)
{
size_t ssid_length;
size_t password_length;
if ((config == 0) || (config->ssid == 0) || (config->password == 0))
{
return ESP8266_ERROR_ARGUMENT;
}
ssid_length = strlen(config->ssid);
password_length = strlen(config->password);
if ((ssid_length == 0u) || (ssid_length > 32u) ||
(password_length > 64u) ||
((config->encryption != ESP8266_ENCRYPT_OPEN) &&
(password_length < 8u)) ||
(config->channel == 0u) || (config->channel > 13u) ||
(config->port == 0u) ||
(config->encryption > ESP8266_ENCRYPT_WPA_WPA2_PSK))
{
return ESP8266_ERROR_ARGUMENT;
}
return ESP8266_OK;
}
/* 将头文件中的默认参数写入调用者提供的配置结构体。 */
void ESP8266_LoadDefaultConfig(ESP8266_ApServerConfig *config)
{
if (config == 0)
{
return;
}
config->ssid = ESP8266_AP_SSID;
config->password = ESP8266_AP_PASSWORD;
config->channel = ESP8266_AP_CHANNEL;
config->encryption = ESP8266_AP_ENCRYPTION;
config->port = ESP8266_SERVER_PORT;
config->timeout_s = ESP8266_SERVER_TIMEOUT_S;
}
/* 使用默认参数初始化 ESP8266_WIFI 热点和 TCP 服务器。 */
ESP8266_Status ESP8266_Init(void)
{
ESP8266_ApServerConfig config;
ESP8266_LoadDefaultConfig(&config);
return ESP8266_InitApServer(&config);
}
/*
* ESP8266 完整初始化流程:初始化串口、检测 AT、关闭回显、进入 AP 模式、
* 配置热点,最后在指定端口启动 TCP 服务器。
*/
ESP8266_Status ESP8266_InitApServer(const ESP8266_ApServerConfig *config)
{
ESP8266_Status status;
status = ESP8266_CheckConfig(config);
if (status != ESP8266_OK)
{
return status;
}
ESP8266_ResetIpdParser();
esp8266_pending_reply_id = ESP8266_NO_CLIENT_ID;
esp8266_command_received = 0u;
esp8266_last_client_id = ESP8266_NO_CLIENT_ID;
ESP8266_DefaultPlatformInit(ESP8266_UART_BAUDRATE);
delay_ms(ESP8266_BOOT_DELAY_MS);
status = ESP8266_TestAT();
if (status != ESP8266_OK)
{
return ESP8266_ERROR_AT;
}
(void)ESP8266_SendCommand("ATE0", "OK", 0, 1000u);
status = ESP8266_SetApMode();
if (status != ESP8266_OK)
{
return ESP8266_ERROR_MODE;
}
status = ESP8266_BuildAp(config);
if (status != ESP8266_OK)
{
return ESP8266_ERROR_AP;
}
status = ESP8266_StartServer(config);
if (status != ESP8266_OK)
{
return ESP8266_ERROR_SERVER;
}
return ESP8266_OK;
}
/* 连续发送 AT 测试指令,在规定重试次数内确认模块在线。 */
ESP8266_Status ESP8266_TestAT(void)
{
uint8_t retry;
ESP8266_Status status;
for (retry = 0u; retry < ESP8266_AT_RETRY_COUNT; retry++)
{
status = ESP8266_SendCommand("AT", "OK", 0, 500u);
if (status == ESP8266_OK)
{
return ESP8266_OK;
}
delay_ms(ESP8266_AT_RETRY_INTERVAL_MS);
}
return ESP8266_ERROR_AT;
}
/* 发送 AT+RST 重启模块,并等待固件返回 ready 或 OK。 */
ESP8266_Status ESP8266_Restart(void)
{
ESP8266_Status status;
status = ESP8266_SendCommand("AT+RST", "ready", "OK", 6000u);
delay_ms(ESP8266_BOOT_DELAY_MS);
return (status == ESP8266_OK) ? ESP8266_OK : ESP8266_ERROR_AT;
}
/*
* 通过 AT+CIPSTATUS 查询 TCP 连接,并返回找到的第一个客户端 ID。
* 没有手机客户端连接时返回 ESP8266_NO_CLIENT_ID。
*/
uint8_t ESP8266_GetConnectedId(void)
{
const char *p;
ESP8266_Status status;
status = ESP8266_SendCommand("AT+CIPSTATUS", "OK", 0, 1200u);
if (status != ESP8266_OK)
{
return ESP8266_NO_CLIENT_ID;
}
p = esp8266_rx_buffer;
while ((p = strstr(p, "+CIPSTATUS:")) != 0)
{
p += 11;
if ((*p >= '0') && (*p <= (char)('0' + ESP8266_MAX_CLIENT_ID)))
{
esp8266_last_client_id = (uint8_t)(*p - '0');
return esp8266_last_client_id;
}
}
esp8266_last_client_id = ESP8266_NO_CLIENT_ID;
return ESP8266_NO_CLIENT_ID;
}
/* 查询当前是否至少存在一个 TCP 客户端。 */
uint8_t ESP8266_HasClient(void)
{
return (ESP8266_GetConnectedId() != ESP8266_NO_CLIENT_ID) ? 1u : 0u;
}
/*
* 使用 AT+CIPSEND 向指定客户端发送数据。
* 先等待“>”发送提示符,再发送负载并等待 SEND OK。
*/
static ESP8266_Status ESP8266_SendBufferById(uint8_t client_id,
const uint8_t *buffer,
uint16_t length)
{
char command[32];
ESP8266_Status status;
if ((client_id > ESP8266_MAX_CLIENT_ID) ||
(buffer == 0) || (length == 0u))
{
return ESP8266_ERROR_ARGUMENT;
}
sprintf(command, "AT+CIPSEND=%u,%u",
(unsigned int)client_id, (unsigned int)length);
status = ESP8266_SendCommand(command, ">", 0,
ESP8266_SEND_PROMPT_TIMEOUT_MS);
if (status != ESP8266_OK)
{
return ESP8266_ERROR_SEND;
}
ESP8266_WriteBuffer(buffer, length);
status = ESP8266_WaitForResponse("SEND OK", 0,
ESP8266_SEND_COMPLETE_TIMEOUT_MS);
if (status != ESP8266_OK)
{
return ESP8266_ERROR_SEND;
}
esp8266_last_client_id = client_id;
return ESP8266_OK;
}
/* 向指定 TCP 连接 ID 发送二进制缓冲区。 */
ESP8266_Status ESP8266_SendBufferToId(uint8_t client_id,
const uint8_t *buffer,
uint16_t length)
{
return ESP8266_SendBufferById(client_id, buffer, length);
}
/* 向指定 TCP 连接 ID 发送字符串。 */
ESP8266_Status ESP8266_SendStringToId(uint8_t client_id, const char *str)
{
if (str == 0)
{
return ESP8266_ERROR_ARGUMENT;
}
return ESP8266_SendBufferById(client_id, (const uint8_t *)str,
(uint16_t)strlen(str));
}
/* 自动查询当前客户端 ID,并向该客户端发送二进制缓冲区。 */
ESP8266_Status ESP8266_SendBufferToClient(const uint8_t *buffer,
uint16_t length)
{
uint8_t client_id;
if ((buffer == 0) || (length == 0u))
{
return ESP8266_ERROR_ARGUMENT;
}
client_id = ESP8266_GetConnectedId();
if (client_id == ESP8266_NO_CLIENT_ID)
{
return ESP8266_ERROR_NO_CLIENT;
}
return ESP8266_SendBufferById(client_id, buffer, length);
}
/* 自动查询当前客户端 ID,并向该客户端发送字符串。 */
ESP8266_Status ESP8266_SendStringToClient(const char *str)
{
if (str == 0)
{
return ESP8266_ERROR_ARGUMENT;
}
return ESP8266_SendBufferToClient((const uint8_t *)str,
(uint16_t)strlen(str));
}
/*
* 将 DHT11 数据转换为“Temp:xx.xC,Humi:xx.x%”文本并发送给手机 APP。
*/
ESP8266_Status ESP8266_SendTemperatureHumidity(uint8_t temperature_int,
uint8_t temperature_dec,
uint8_t humidity_int,
uint8_t humidity_dec)
{
char data[48];
sprintf(data, "Temp:%u.%uC,Humi:%u.%u%%\r\n",
(unsigned int)temperature_int,
(unsigned int)(temperature_dec % 10u),
(unsigned int)humidity_int,
(unsigned int)(humidity_dec % 10u));
return ESP8266_SendStringToClient(data);
}
/*
* 在指定时间内处理串口接收数据。
* 当 +IPD 状态机识别到“123”时,向原客户端回复“I get it!”。
*/
ESP8266_Status ESP8266_Poll(uint16_t timeout_ms)
{
uint16_t elapsed_ms;
uint8_t data;
uint8_t client_id;
ESP8266_Status status;
elapsed_ms = 0u;
esp8266_rx_buffer[0] = '\0';
do
{
while (ESP8266_RxAvailable() != 0u)
{
data = ESP8266_ReadRxByte();
ESP8266_ParseIncomingByte(data);
}
if (esp8266_pending_reply_id != ESP8266_NO_CLIENT_ID)
{
client_id = esp8266_pending_reply_id;
esp8266_pending_reply_id = ESP8266_NO_CLIENT_ID;
status = ESP8266_SendStringToId(client_id, "I get it!\r\n");
return status;
}
if (elapsed_ms >= timeout_ms)
{
break;
}
delay_ms(1u);
elapsed_ms++;
} while (1);
return ESP8266_OK;
}
/* 取出并清除手机指令到达标志,供主程序更新 OLED 状态。 */
uint8_t ESP8266_TakeCommandReceived(void)
{
uint8_t received;
received = esp8266_command_received;
esp8266_command_received = 0u;
return received;
}
/* 返回最近一次保存的 AT 响应缓冲区首地址。 */
const char *ESP8266_GetLastResponse(void)
{
return esp8266_rx_buffer;
}
/* 把状态码转换成长度较短、适合 OLED 显示的文本。 */
const char *ESP8266_StatusText(uint8_t status)
{
switch (status)
{
case ESP8266_OK:
return "OK";
case ESP8266_ERROR_ARGUMENT:
return "ARG";
case ESP8266_ERROR_TIMEOUT:
return "TIMEOUT";
case ESP8266_ERROR_RESPONSE:
return "RESP";
case ESP8266_ERROR_AT:
return "AT ERR";
case ESP8266_ERROR_MODE:
return "MODE ERR";
case ESP8266_ERROR_AP:
return "AP ERR";
case ESP8266_ERROR_SERVER:
return "SERVER";
case ESP8266_ERROR_NO_CLIENT:
return "NO APP";
case ESP8266_ERROR_SEND:
return "SEND ERR";
default:
return "ERROR";
}
}
ESP8266.h代码
#ifndef __ESP8266_H
#define __ESP8266_H
#include <stdint.h>
/*
* 文件名称:ESP8266.h
* 文件编码:UTF-8
* 功能说明:ESP8266 WiFi 模块驱动的配置、状态码和公共接口声明。
*
* 本工程硬件连接:
* ESP8266 RX <- STM32 PA9 (USART1_TX)
* ESP8266 TX -> STM32 PA10 (USART1_RX)
*
* ESP8266 工作在 AP(无线热点)模式,并建立 TCP 服务器。手机先连接
* ESP8266_WIFI 热点,再通过 TCP 客户端连接 192.168.4.1:8080。
*/
/* ======================== WiFi 与 TCP 参数 ======================== */
#define ESP8266_UART_BAUDRATE 115200u
#define ESP8266_AP_SSID "ESP8266_WIFI"
#define ESP8266_AP_PASSWORD "12345678"
#define ESP8266_AP_CHANNEL 1u
#define ESP8266_SERVER_PORT 8080u
#define ESP8266_SERVER_TIMEOUT_S 7200u
/* ======================== 接收与命令缓冲区 ======================== */
#define ESP8266_RX_BUFFER_SIZE 512u
#define ESP8266_RX_RING_SIZE 1024u
#define ESP8266_COMMAND_BUFFER_SIZE 160u
#define ESP8266_MAX_CLIENT_ID 4u
#define ESP8266_NO_CLIENT_ID 0xFFu
/* ======================== AT 指令超时参数 ========================= */
#define ESP8266_AT_RETRY_COUNT 8u
#define ESP8266_AT_RETRY_INTERVAL_MS 200u
#define ESP8266_BOOT_DELAY_MS 1000u
#define ESP8266_SEND_PROMPT_TIMEOUT_MS 1500u
#define ESP8266_SEND_COMPLETE_TIMEOUT_MS 3000u
/* ESP8266 驱动函数统一使用的状态类型。 */
typedef uint8_t ESP8266_Status;
/* 驱动函数状态码。 */
#define ESP8266_OK 0u
#define ESP8266_ERROR_ARGUMENT 1u
#define ESP8266_ERROR_TIMEOUT 2u
#define ESP8266_ERROR_RESPONSE 3u
#define ESP8266_ERROR_AT 4u
#define ESP8266_ERROR_MODE 5u
#define ESP8266_ERROR_AP 6u
#define ESP8266_ERROR_SERVER 7u
#define ESP8266_ERROR_NO_CLIENT 8u
#define ESP8266_ERROR_SEND 9u
/* ESP8266 AT 固件规定的无线热点加密方式。 */
typedef enum
{
ESP8266_ENCRYPT_OPEN = 0u,
ESP8266_ENCRYPT_WEP = 1u,
ESP8266_ENCRYPT_WPA_PSK = 2u,
ESP8266_ENCRYPT_WPA2_PSK = 3u,
ESP8266_ENCRYPT_WPA_WPA2_PSK = 4u
} ESP8266_Encryption;
#define ESP8266_AP_ENCRYPTION ESP8266_ENCRYPT_WPA2_PSK
/* AP 热点和 TCP 服务器的运行参数。 */
typedef struct
{
const char *ssid; /* WiFi 热点名称。 */
const char *password; /* WiFi 热点密码。 */
uint8_t channel; /* WiFi 信道,取值范围为 1~13。 */
uint8_t encryption; /* 加密方式,参见 ESP8266_Encryption。 */
uint16_t port; /* TCP 服务器监听端口。 */
uint16_t timeout_s; /* TCP 客户端空闲超时时间,单位:秒。 */
} ESP8266_ApServerConfig;
/* ==================== STM32F10x 标准外设库配置 ==================== */
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_usart.h"
#include "misc.h"
#define ESP8266_USART USART1
#define ESP8266_USART_CLK RCC_APB2Periph_USART1
#define ESP8266_GPIO_PORT GPIOA
#define ESP8266_GPIO_CLK RCC_APB2Periph_GPIOA
#define ESP8266_TX_PIN GPIO_Pin_9
#define ESP8266_RX_PIN GPIO_Pin_10
#define ESP8266_USART_IRQ USART1_IRQn
#define ESP8266_IRQ_PREEMPT_PRIORITY 2u
#define ESP8266_IRQ_SUB_PRIORITY 2u
/*
* 函数名称:ESP8266_LoadDefaultConfig
* 函数功能:将本头文件中的默认热点和 TCP 参数写入配置结构体。
* 入口参数:config,目标配置结构体指针。
* 返回参数:无。
*/
void ESP8266_LoadDefaultConfig(ESP8266_ApServerConfig *config);
/*
* 函数名称:ESP8266_Init
* 函数功能:初始化 USART1、建立 ESP8266_WIFI 热点并启动 TCP 服务器。
* 入口参数:无。
* 返回参数:ESP8266_OK 表示成功,其他值表示相应初始化错误。
*/
ESP8266_Status ESP8266_Init(void);
/*
* 函数名称:ESP8266_InitApServer
* 函数功能:按照指定配置初始化 ESP8266 AP 模式和 TCP 服务器。
* 入口参数:config,热点和 TCP 服务器配置。
* 返回参数:ESP8266_OK 或 ESP8266_ERROR_xxx 状态码。
*/
ESP8266_Status ESP8266_InitApServer(const ESP8266_ApServerConfig *config);
/* 检测 ESP8266 是否能够正确响应 AT 指令。 */
ESP8266_Status ESP8266_TestAT(void);
/* 通过 AT+RST 指令重新启动 ESP8266 模块。 */
ESP8266_Status ESP8266_Restart(void);
/*
* 函数名称:ESP8266_SendCommand
* 函数功能:发送 AT 指令并等待两个期望响应中的任意一个。
* 入口参数:command 为不带回车换行的 AT 指令;reply1、reply2 为期望响应;
* timeout_ms 为等待超时时间,单位:毫秒。
* 返回参数:ESP8266_OK 或 ESP8266_ERROR_xxx 状态码。
*/
ESP8266_Status ESP8266_SendCommand(const char *command,
const char *reply1,
const char *reply2,
uint16_t timeout_ms);
/*
* 函数名称:ESP8266_Poll
* 函数功能:轮询解析 TCP 数据;收到手机 APP 的“123”后回复“I get it!”。
* 入口参数:timeout_ms,本次轮询最多等待的毫秒数。
* 返回参数:ESP8266_OK 表示正常,其他值表示自动回复失败。
* 使用说明:应在主循环中频繁调用,以缩短手机指令的响应时间。
*/
ESP8266_Status ESP8266_Poll(uint16_t timeout_ms);
/* 向指定 TCP 连接 ID 发送字符串。 */
ESP8266_Status ESP8266_SendStringToId(uint8_t client_id, const char *str);
/* 向指定 TCP 连接 ID 发送指定长度的二进制数据。 */
ESP8266_Status ESP8266_SendBufferToId(uint8_t client_id,
const uint8_t *buffer,
uint16_t length);
/* 向当前已连接的手机 APP 发送字符串。 */
ESP8266_Status ESP8266_SendStringToClient(const char *str);
/* 向当前已连接的手机 APP 发送指定长度的二进制数据。 */
ESP8266_Status ESP8266_SendBufferToClient(const uint8_t *buffer,
uint16_t length);
/*
* 按“Temp:xx.xC,Humi:xx.x%”格式向手机发送 DHT11 温湿度数据。
*/
ESP8266_Status ESP8266_SendTemperatureHumidity(uint8_t temperature_int,
uint8_t temperature_dec,
uint8_t humidity_int,
uint8_t humidity_dec);
/* 查询当前 TCP 客户端的连接 ID,无客户端时返回 ESP8266_NO_CLIENT_ID。 */
uint8_t ESP8266_GetConnectedId(void);
/* 查询当前是否存在 TCP 客户端连接,存在返回 1,否则返回 0。 */
uint8_t ESP8266_HasClient(void);
/* 读取并清除“已收到 123 指令”标志;每条有效指令只返回一次 1。 */
uint8_t ESP8266_TakeCommandReceived(void);
/* 清空串口接收数据、+IPD 解析状态以及最近一次响应内容。 */
void ESP8266_Clear(void);
/* 清空 USART1 中断使用的环形接收缓冲区。 */
void ESP8266_ClearRxBuffer(void);
/* 判断环形接收缓冲区中是否存在可读字节。 */
uint8_t ESP8266_RxAvailable(void);
/* 从环形接收缓冲区读取一个字节,无数据时返回 0。 */
uint8_t ESP8266_ReadRxByte(void);
/* 查询环形接收缓冲区是否曾经发生溢出。 */
uint8_t ESP8266_RxOverflowed(void);
/* 将一个 USART 接收字节写入环形缓冲区,供中断处理函数调用。 */
void ESP8266_OnReceivedByte(uint8_t data);
/* ESP8266 所用 USART 的通用中断处理入口。 */
void ESP8266_USARTIrqHandler(void);
/* 获取最近一次 AT 指令响应字符串,仅用于调试。 */
const char *ESP8266_GetLastResponse(void);
/* 将 ESP8266 状态码转换成适合 OLED 显示的简短英文文本。 */
const char *ESP8266_StatusText(uint8_t status);
#endif
七、实物演示章节
硬件准备清单
- STM32F103C8T6 最小系统板 ×1
- ESP‑01S (ESP8266) 模块带底板 ×1
- DHT11 温湿度传感器模块 ×1
- 0.96 寸 I2C OLED 屏幕 ×1
演示操作步骤
- 按照接线表格完成全部接线,仔细核对 ESP8266 供电 3.3V,TX RX 交叉,全部模块 GND 共地。
- 将编译好的固件烧录进 STM32 单片机,上电。
- 观察 OLED 屏幕,等待 ESP8266 初始化完成,模块开启 WiFi 热点。
- 手机 WLAN 搜索 WiFi 名称
ESP8266_WIFI,密码12345678连接热点。

⚠️手机连接热点之后会提示无互联网,忽略提示保持连接。
- 打开 TCP 调试助手 APP,选择 TCP Client 模式,填写服务器 IP:
192.168.4.1,端口:8080,建立 TCP 连接。 - 连接成功,OLED 屏幕状态由
APP:WAIT切换为APP:DATA SENT,手机端每 2 秒收到一条温湿度字符串。 - 在 APP 发送框输入文本
123,点击发送,手机收到返回字符串I get it!,OLED 同步显示APP RX:123 OK。 - 断开手机 TCP 连接,OLED 状态变回
APP:WAIT,重新连接 TCP 客户端后继续接收数据。 - 断电重新上电,观察设备自动初始化流程;如果 ESP8266 异常,单片机会自动重试初始化。
演示现象总结
✅OLED 屏幕实时显示本地温度湿度; ✅手机连上热点建立 TCP 连接,自动接收设备上报温湿度; ✅下发指令 123,设备返回应答字符串,实现简单双向交互。
八、常见问题排查
1. 手机搜不到 ESP8266_WIFI 热点
- ESP8266 供电是否稳定 3.3V,电流足够;
- ESP‑01 模块 EN 引脚是否拉高;
- TX RX 是否交叉接反;
- AT 固件波特率是否为 115200;
- OLED 屏幕查看报错:AT ERR / AP ERR,定位故障。
2. 手机连上热点,但是收不到温湿度
连上 WiFi 热点≠建立 TCP 连接!! 手机必须使用 TCP 客户端工具连接
192.168.4.1:8080;OLED 显示APP:WAIT说明 TCP 客户端未接入。
3. 发送 123 没有任何回复
- APP 发送模式选择 ASCII 文本,不要使用 HEX 十六进制发送;
- 发送内容严格是
123,不要附带多余字符; - APP 自动追加
\r\n不影响识别,程序会自动过滤换行; - 确认 TCP 连接没有断开。
4. 串口乱码,一直报 AT ERR
ESP8266 固件实际波特率和代码设置 115200 不匹配,使用串口工具读取模块确认真实波特率,修改代码配置。
5. 发送数据 ESP8266 自动重启
绝大多数是供电电流不足。缩短电源线,增加电源滤波,更换输出能力更强的 3.3V 电源。
九、总结
本项目完整实现 STM32 采集传感器,借助 ESP8266‑AT 固件实现串口转 WiFi,搭建 AP 模式 TCP 服务器,完成本地显示 + 无线网络双向通信。 核心要点:
- ESP8266 只做网络转发,复杂网络协议栈由模块内部完成,STM32 只负责业务逻辑;
- 串口交叉接线、共地、稳定 3.3V 供电是硬件成功的关键;
- 串口中断 + 环形缓冲区,中断中不做复杂处理,保证单片机系统稳定;
- 通过
+IPD帧解析手机下发 TCP 数据,实现设备和手机双向交互。

598

被折叠的 条评论
为什么被折叠?



