WiFi无线温湿度监控:STM32+ESP8266+DHT11+OLED教程

前言

本篇基于 STM32F103C8T6 最小系统板,搭配 ESP8266‑01、DHT11 温湿度传感器、OLED 屏幕完成整套物联网 Demo。实现功能:单片机采集环境温湿度,OLED 本地显示;ESP8266 开启 AP 热点搭建 TCP 服务器,手机 TCP 客户端连接热点后接收温湿度数据;手机发送指令123,设备返回I get it!,完成简单的双向交互。

整体工作流程:DHT11 采集环境温湿度 → STM32 读取传感器数据 → OLED 本地显示 → STM32 串口发送 AT 指令控制 ESP8266 → ESP8266 搭建 WiFi 热点 + TCP 服务器 → 手机连接热点接收温湿度 → 手机下发指令,设备应答回复。

整体系统分层

  1. 传感与显示层:DHT11 测量温湿度,OLED 屏幕做本地状态显示
  2. 控制层:STM32 负责传感器读取、数据格式化、指令判断逻辑
  3. 网络层:ESP8266 负责 WiFi 热点创建、TCP 连接维护、网络数据收发

简单理解:ESP8266 在这里充当串口转 WiFi 转换器,STM32 不需要处理复杂 WiFi 协议栈,只需要通过 AT 指令就可以实现网络通信。

视频讲解:WiFi无线温湿度监控:STM32+ESP8266+DHT11+OLED教程https://www.bilibili.com/video/BV1k4tb6sE1x/?share_source=copy_web&vd_source=91b967b41a17a5686913521545a8c3fb

一、硬件接线说明

⚠️重点注意:串口 TX、RX 必须交叉相连;不同模块 GND 必须共地;ESP8266 必须 3.3V 供电,严禁接 5V!

模块引脚STM32F103C8T6 引脚功能说明
ESP8266 VCC3.3VESP8266 模块电源,禁止 5V 输入
ESP8266 TXPA10(USART1_RX)ESP8266 向 STM32 输出数据
ESP8266 RXPA9(USART1_TX)STM32 向 ESP8266 发送 AT 指令
ESP8266 GNDGND共地,通信正常必要条件
DHT11 VCC5VDHT11 传感器供电
DHT11 DATAPA11DHT11 单总线数据引脚
DHT11 GNDGND传感器地
OLED VCC5VOLED 屏幕供电
OLED SCLPB8软件 I2C 时钟
OLED SDAPB9软件 I2C 数据
OLED GNDGND屏幕地

1. 串口交叉接线原理

串口通信规则:发送端必须接对方接收端

  • STM32 PA9(TX) → ESP8266 RX
  • STM32 PA10(RX) → ESP8266 TX
  • 双方 GND 连接在一起

如果 TX 接 TX、RX 接 RX,两个设备完全无法收发任何数据。

2.ESP8266 供电坑点

ESP8266 使用 WiFi 发射瞬间电流大,供电不足会出现各种异常现象:

  1. 模块反复上电重启
  2. 可以搜索到热点,手机一连接热点直接消失
  3. 发送网络数据异常、模块自动复位
  4. 串口偶尔打印 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 指令完成模块配置。

初始化步骤:

  1. 初始化 USART1 外设,PA9、PA10 引脚
  2. 发送AT:检测模块是否正常在线
  3. 发送ATE0:关闭指令回显,减少串口多余输出
  4. AT+CWMODE=2:设置模块为 AP 热点模式
  5. AT+CWSAP=xxx:配置热点名称、密码、信道、加密
  6. AT+CIPMODE=0:普通 TCP 传输模式
  7. AT+CIPMUX=1:开启多客户端连接
  8. AT+CIPDINFO=0:简化接收数据帧格式
  9. AT+CIPSERVER=1,8080:开启 8080 端口 TCP 服务器
  10. 等待手机 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 返回串口数据时机不确定,项目采用串口接收中断 + 环形缓冲区方案。

  1. USART1 收到 1 字节数据立刻触发接收中断;
  2. 在中断服务函数里面,只把字节存入 1024 字节环形缓冲区,中断中不做字符串解析,不发送 AT 指令,保证中断执行时间极短;
  3. 主循环再从环形缓冲区读取字节,解析 AT 响应、解析+IPD网络数据包。

环形缓冲区两个指针:

  • head:写入位置,中断使用
  • tail:读取位置,主程序使用

优点:就算主程序正在读取 DHT11、刷新 OLED,串口过来的数据也不会直接丢失。

如果主程序长时间不读取缓冲区,会出现缓冲区溢出,程序标记溢出标志丢弃新字节,保护旧数据。

五、业务逻辑工作流程

1. 温湿度数据发送流程

DHT11 读取成功后,把温湿度转为字符串,类似Temp:25.0C,Humi:60.0%,发送给手机。

  1. STM32 发送 AT 查询连接状态,获取客户端 ID
  2. 发送AT+CIPSEND=ID,数据长度告诉 ESP 要发送多少字节
  3. ESP 返回>符号,代表准备就绪
  4. STM32 发送温湿度文本数据
  5. ESP8266 把数据通过 WiFiTCP 发送手机,返回SEND OK代表发送成功

需要指定发送长度原因:TCP 数据包可以包含任意字符,ESP 需要区分 “有效负载” 和后续 AT 命令。 项目约 2 秒采集一次温湿度,兼顾观察效果,避免数据刷屏。

2. 手机下发指令解析回复

手机发送文本123,ESP8266 串口输出+IPD,0,3:123

  • +IPD:网络数据到来标记
  • 0:客户端连接 ID
  • 3:有效数据字节长度
  • 123:手机发送实际内容

程序解析逻辑:

  1. 在缓冲区寻找+IPD帧头
  2. 解析客户端 ID、数据长度
  3. 根据长度取出网络收到的数据
  4. 剔除换行、空格、回车字符
  5. 判断内容等于123,则调用发送接口回复I get it!

只有纯 123 指令触发回复;123\r\n也可以识别;01231234abc123不会触发应答。 数据解析、回复发送全部放在主循环,不在中断中执行,保障系统稳定性。

3. 主循环调度逻辑

  1. 轮询处理 ESP8266 网络数据,判断是否收到手机指令;收到 123 则回复应答,更新 OLED 状态标记
  2. 判断是否到达 2 秒 DHT11 采样周期
  3. 周期到读取 DHT11 传感器;读取成功就发送温湿度数据,刷新 OLED 屏幕
  4. ESP8266 初始化失败,程序每 5 秒自动重试初始化,处理模块上电慢、临时通信故障。

OLED 屏幕显示状态说明:

  DHT11 + WiFi
Temp:25.0C
Humi:60.0%
APP:DATA SENT

底部状态标识含义:

OLED 显示状态含义
APP:DATA SENT温湿度成功发送手机
APP:WAITWiFi 热点正常,TCP 客户端还未连接
APP RX:123 OK成功收到手机 123 指令
WiFi:AT ERRESP8266 不响应 AT 指令
WiFi:AP ERR热点配置失败
Sensor ErrorDHT11 读取失败,检查接线供电

六、源代码存放区

📌本节用于存放完整工程源码。 工程文件说明:

  1. ESP8266.h:WiFi 参数、端口、缓冲区、热点账号密码配置
  2. ESP8266.c:ESP 初始化、AT 指令发送、串口中断、IPD 解析、数据发送、轮询处理函数
  3. 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

七、实物演示章节

硬件准备清单

  1. STM32F103C8T6 最小系统板 ×1
  2. ESP‑01S (ESP8266) 模块带底板 ×1
  3. DHT11 温湿度传感器模块 ×1
  4. 0.96 寸 I2C OLED 屏幕 ×1

演示操作步骤

  1. 按照接线表格完成全部接线,仔细核对 ESP8266 供电 3.3V,TX RX 交叉,全部模块 GND 共地。
  2. 将编译好的固件烧录进 STM32 单片机,上电。
  3. 观察 OLED 屏幕,等待 ESP8266 初始化完成,模块开启 WiFi 热点。
  4. 手机 WLAN 搜索 WiFi 名称ESP8266_WIFI,密码12345678连接热点。

⚠️手机连接热点之后会提示无互联网,忽略提示保持连接。

  1. 打开 TCP 调试助手 APP,选择 TCP Client 模式,填写服务器 IP:192.168.4.1,端口:8080,建立 TCP 连接。
  2. 连接成功,OLED 屏幕状态由APP:WAIT切换为APP:DATA SENT,手机端每 2 秒收到一条温湿度字符串。
  3. 在 APP 发送框输入文本123,点击发送,手机收到返回字符串I get it!,OLED 同步显示APP RX:123 OK
  4. 断开手机 TCP 连接,OLED 状态变回APP:WAIT,重新连接 TCP 客户端后继续接收数据。
  5. 断电重新上电,观察设备自动初始化流程;如果 ESP8266 异常,单片机会自动重试初始化。

演示现象总结

✅OLED 屏幕实时显示本地温度湿度; ✅手机连上热点建立 TCP 连接,自动接收设备上报温湿度; ✅下发指令 123,设备返回应答字符串,实现简单双向交互。

八、常见问题排查

1. 手机搜不到 ESP8266_WIFI 热点

  1. ESP8266 供电是否稳定 3.3V,电流足够;
  2. ESP‑01 模块 EN 引脚是否拉高;
  3. TX RX 是否交叉接反;
  4. AT 固件波特率是否为 115200;
  5. OLED 屏幕查看报错:AT ERR / AP ERR,定位故障。

2. 手机连上热点,但是收不到温湿度

连上 WiFi 热点≠建立 TCP 连接!! 手机必须使用 TCP 客户端工具连接192.168.4.1:8080;OLED 显示APP:WAIT说明 TCP 客户端未接入。

3. 发送 123 没有任何回复

  1. APP 发送模式选择 ASCII 文本,不要使用 HEX 十六进制发送;
  2. 发送内容严格是123,不要附带多余字符;
  3. APP 自动追加\r\n不影响识别,程序会自动过滤换行;
  4. 确认 TCP 连接没有断开。

4. 串口乱码,一直报 AT ERR

ESP8266 固件实际波特率和代码设置 115200 不匹配,使用串口工具读取模块确认真实波特率,修改代码配置。

5. 发送数据 ESP8266 自动重启

绝大多数是供电电流不足。缩短电源线,增加电源滤波,更换输出能力更强的 3.3V 电源。

九、总结

本项目完整实现 STM32 采集传感器,借助 ESP8266‑AT 固件实现串口转 WiFi,搭建 AP 模式 TCP 服务器,完成本地显示 + 无线网络双向通信。 核心要点:

  1. ESP8266 只做网络转发,复杂网络协议栈由模块内部完成,STM32 只负责业务逻辑;
  2. 串口交叉接线、共地、稳定 3.3V 供电是硬件成功的关键;
  3. 串口中断 + 环形缓冲区,中断中不做复杂处理,保证单片机系统稳定;
  4. 通过+IPD帧解析手机下发 TCP 数据,实现设备和手机双向交互。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值