WIFI发送代码优化

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-08-29 08:09:36 Views: 0 Likes: 0 Comments: 0

059.8.4 WIFI发送代码优化

/* ==================== main.c ==================== */
#include "reg52.h"
#include "delay.h"
#include "main.h"
#include "uart.h"

#define ESP_EVENT_OK      0x01
#define ESP_EVENT_PROMPT  0x02

volatile uchar esp_events = 0;
volatile uchar ok_match_state = 0;

/* 等待串口中断置位;等待期间仍可正常进入串口中断。 */
bit ESP_Wait_Event(uchar event_mask, uint timeout_ms)
{
    while (timeout_ms != 0)
    {
        if ((esp_events & event_mask) != 0)
        {
            return 1;
        }
        Delay_xms(1);
        timeout_ms--;
    }
    return 0;
}

/* 一次发送只在完整超时后才重试,避免快速重复打断 ESP8266。 */
bit ESP_Send_Command(const char *command,
                     uchar expected_event,
                     uint timeout_ms,
                     uchar max_attempts)
{
    uchar attempt;

    for (attempt = 0; attempt < max_attempts; attempt++)
    {
        ES = 0;
        esp_events = 0;
        ok_match_state = 0;
        ES = 1;

        UART_Send_Str(command);
        if (ESP_Wait_Event(expected_event, timeout_ms))
        {
            return 1;
        }
    }
    return 0;
}

void main(void)
{
    bit init_success = 1;

    Delay_xms(1000);
    UART_Init();

    /* 模式设置通常很快;加入 Wi-Fi 和建立 TCP 连接需要更长超时。 */
    if (!ESP_Send_Command("AT+CWMODE=3\r\n",
                          ESP_EVENT_OK, 2000, 2))
    {
        init_success = 0;
    }

    if (init_success &&
        !ESP_Send_Command("AT+CWJAP=\"YOUR_SSID\",\"YOUR_PASSWORD\"\r\n",
                          ESP_EVENT_OK, 15000, 2))
    {
        init_success = 0;
    }

    if (init_success &&
        !ESP_Send_Command("AT+CIPSTART=\"TCP\",\"192.168.1.100\",5132\r\n",
                          ESP_EVENT_OK, 10000, 2))
    {
        init_success = 0;
    }

    /* CIPSEND 的关键就绪标志是字符 '>',不是仅看到 OK。 */
    if (init_success &&
        !ESP_Send_Command("AT+CIPSEND=4\r\n",
                          ESP_EVENT_PROMPT, 3000, 2))
    {
        init_success = 0;
    }

    if (init_success)
    {
        ES = 0;
        esp_events = 0;
        ok_match_state = 0;
        ES = 1;

        UART_Send_Str("qwer");
        ESP_Wait_Event(ESP_EVENT_OK, 3000); /* 对应 SEND OK 中的 OK */
    }

    while (1)
    {
        /* init_success=0 时可在这里点亮故障灯或进入重连流程。 */
    }
}

void UART_Routine(void) interrupt 4
{
    uchar received_byte;

    if (RI == 1)
    {
        RI = 0;
        received_byte = SBUF;

        if (received_byte == '>')
        {
            esp_events |= ESP_EVENT_PROMPT;
        }

        /* 只识别相邻的、大写的 O 和 K,并允许 OOK 这样的重叠起点。 */
        if (ok_match_state == 0)
        {
            if (received_byte == 'O')
            {
                ok_match_state = 1;
            }
        }
        else
        {
            if (received_byte == 'K')
            {
                esp_events |= ESP_EVENT_OK;
                ok_match_state = 0;
            }
            else if (received_byte == 'O')
            {
                ok_match_state = 1;
            }
            else
            {
                ok_match_state = 0;
            }
        }
    }
}

/* ==================== main.h ==================== */
#ifndef _MAIN_H_
#define _MAIN_H_

#include "reg52.h"

typedef unsigned char uchar;
typedef unsigned int  uint;

sbit LED1 = P1^0;
sbit LED2 = P1^1;
sbit LED3 = P1^2;
sbit LED4 = P1^3;
sbit BEEP = P1^6;
sbit JDQ1 = P2^0;

#endif

/* ==================== Uart/uart.c ==================== */
#include "uart.h"

void UART_Send_Byte(uchar send_byte)
{
    SBUF = send_byte;
    while (TI == 0);
    TI = 0;
}

void UART_Send_Str(const char *send_str)
{
    while (*send_str != '\0')
    {
        UART_Send_Byte((uchar)*send_str);
        send_str++;
    }
}

void UART_Init(void)
{
    SCON = 0x50;
    PCON &= 0x7F;
    TMOD = (TMOD & 0x0F) | 0x20;
    TH1 = 0xFD;
    TL1 = 0xFD;                    /* 11.0592 MHz,9600 bit/s */
    RI = 0;
    TI = 0;
    TR1 = 1;
    ES = 1;
    EA = 1;
}

/* ==================== Uart/uart.h ==================== */
#ifndef _UART_H_
#define _UART_H_

#include "main.h"

void UART_Send_Byte(uchar send_byte);
void UART_Send_Str(const char *send_str);
void UART_Init(void);

#endif

/* ==================== Delay/delay.c ==================== */
#include "delay.h"
#include <intrins.h>

void Delay_xms(uint xms)
{
    uchar data i, j;

    while (xms != 0)
    {
        _nop_();
        i = 2;
        j = 199;
        do
        {
            while (--j);
        } while (--i);
        xms--;
    }
}

/* ==================== Delay/delay.h ==================== */
#ifndef _DELAY_H_
#define _DELAY_H_

#include "main.h"

void Delay_xms(uint xms);

#endif

一、任务信息资源导图

ESP8266 上电后会从 TXD 输出启动信息和 AT 固件状态信息。若单片机沿用此前“收到一个字符就控制 LED、蜂鸣器或继电器”的 switch 逻辑,启动文本中的某个普通字符可能碰巧等于控制命令,进而误触发外设。

ESP8266 上电
    │ TXD 输出启动文本、WIFI 状态等
    ▼
51 串口接收中断
    │ 若仍把单个字符直接当作控制命令
    ▼
字符碰撞 ──> LED / 蜂鸣器被误触发

本实验已经不再需要单字符遥控功能,所以应删除对应 switch 分支,只保留 ESP8266 回复解析。更一般的解决方法不是简单“删掉接收代码”,而是给应用命令设计明确的数据帧,例如帧头、命令字、长度和校验,避免任何普通文本都能触发执行器。

建议的应用帧:AA 55 | 命令 | 长度 | 数据 | 校验
只有帧头、长度、校验全部正确,才允许控制蜂鸣器或继电器。

硬件方面仍要注意:ESP8266 是 3.3 V 器件,裸模块不能直接接 5 V;51 TXD 若为 5 V 逻辑,应经过分压或电平转换再接 ESP8266 RXD。上电误响既可能来自软件误判,也可能来自供电毛刺或复位时引脚默认电平,应分别验证,不能把所有现象都归为单一原因。

二、优化代码

固定延时的缺点不只是“等待十几秒”。延时太长会浪费启动时间,延时太短又会在上一条指令尚未完成时发送下一条。正确思路是发送命令后等待与该命令匹配的响应,并设置超时

stateDiagram-v2
    [*] --> 发送命令
    发送命令 --> 等待响应
    等待响应 --> 成功: 收到期望事件
    等待响应 --> 失败: 收到 ERROR/FAIL
    等待响应 --> 重试: 超时且还有次数
    重试 --> 发送命令
    等待响应 --> 失败: 超时且次数用尽
    成功 --> [*]
    失败 --> [*]

不能把“每条 AT 指令都只要看到 OK 就算完成”当成通用规则:

操作更有意义的成功依据
AT+CWMODEOK
AT+CWJAPWIFI CONNECTEDWIFI GOT IP,最后 OK
AT+CIPSTARTCONNECT,随后 OK
AT+CIPSEND=n发送就绪提示符 >
发送数据正文SEND OK

本节完整代码为了保持规模适中,使用“相邻 OK”和 > 两种事件。工程继续扩展时,应进一步解析 ERRORFAILCLOSEDWIFI DISCONNECT 等事件,并为不同状态设置不同处理策略。

2.1 定义全局数组

视频采用两个元素的数组保存字符 OK

char esp_recv_buf[2] = {0};

数组下标 0 和 1 分别用于保存两个字节:

收到 O:esp_recv_buf[0] = 'O'
收到 K:esp_recv_buf[1] = 'K'
比较:  [0]=='O' && [1]=='K'

这种做法适合讲解“串口中断每次收到一个字节”的概念,但若没有严格维护接收顺序,就容易把不相邻的字符拼成 OK。完整代码改用 ok_match_state 表示匹配进度:状态 0 等 O,状态 1 只接受紧跟着的 K

中断服务程序和主程序都会访问 esp_eventsok_match_state,所以它们必须声明为 volatilevolatile 告诉编译器这些变量可能在当前代码看不见的位置发生改变,循环中必须重新从内存读取;它不等于线程安全,也不自动解决复杂的并发读写问题。

2.2 删除switch语句

删除旧的外设控制 switch 后,串口中断只负责接收和识别网络模块事件。字符与字符串要区分:

写法类型/含义
'O'单个字符,通常为一个字节
"OK"C 字符串,内存中为 OK\0 三个字节
'OK'多字符常量,含义依编译器实现,不应拿来比较串口字符串

视频中“不是 O 就当成 K”的 else 逻辑并不严谨,因为 ESP8266 会返回 CR、LF、空格、字母和数字等很多字符。正确判断必须明确写出 received_byte == 'K',其他字符应使匹配状态回到起点。

2.3 识别单个字符

严格相邻匹配的状态变化为:

状态 0(等待 O)
  ├─ 收到 O → 状态 1
  └─ 其他   → 状态 0

状态 1(已经收到 O,等待 K)
  ├─ 收到 K → 产生 OK 事件,回到状态 0
  ├─ 收到 O → 仍为状态 1,新的 O 可作为起点
  └─ 其他   → 状态 0

这样 O...K 不会被误判为连续的 OK,而 OOK 仍能从第二个 O 开始匹配成功。

若使用数组方案,可以用:

memset(esp_recv_buf, 0, sizeof(esp_recv_buf));

三个参数分别是起始地址、按字节填入的数值、处理的字节数。第三个参数优先写 sizeof(esp_recv_buf),避免数组长度改变后忘记同步修改常数。memset 只是把内存的每个字节写成同一值;清零时适用,但不能用 memset(array, 1, sizeof array) 来得到整数数组 {1,1,...}

2.4 优化识别到OK的处理

视频中的 do...while (esp_flag) 会每隔 500 ms 或 1 s 重发同一条指令,直到任意位置出现 OK。这会产生三个问题:

  1. AT+CWJAP 连接热点本来就可能需要数秒,过早重发会让模块返回 busy 或打乱流程;
  2. 没有总超时和最大次数,模块断电或接线错误时会永久发送;
  3. AT+CIPSEND 真正表示“可发送正文”的事件是 >,只识别 OK 不够精确。

完整代码使用“每次发送后等待完整超时,超时才重试”的方式,并限制 max_attempts。主循环等待时,串口中断仍会运行:

主程序:发命令 ──────── 1 ms 轮询事件 ────────> 成功/超时
                         ▲
中断:       每到一个字节就解析,并设置事件位

采用固定 1 ms 软件延时做超时计时仍只是入门方案,其精度会受中断和编译器影响;正式项目宜使用硬件定时器产生系统节拍,并把整个过程组织成非阻塞状态机。

2.5 删除无用代码

从其他实验复制工程时,要清理已不再使用的遥控变量、switch 分支和测试代码,但不能用“编译没有报错”作为删对的唯一依据。编译器只能发现语法、类型和链接问题,无法证明业务逻辑正确。

遇到错误时可按以下顺序处理:

  1. 先看第一条 error,而不是最后一条连带错误;
  2. 确认报错的 .c 文件和行号;
  3. 双击错误跳转后,检查标识符拼写、声明与定义是否一致;
  4. 若提示函数未声明,检查是否包含正确头文件;
  5. 若提示重复定义,检查变量是否同时写在头文件和多个 .c 文件中;
  6. 编译通过后还要验证返回信息、超时和失败路径。

全局变量通常在一个 .c 文件中定义,在需要访问它的头文件中用 extern 声明;不要在头文件里直接定义普通全局变量,否则头文件被多个 .c 文件包含时可能造成重复定义。

2.6 包含头文件

标准库头文件通常用尖括号,自定义工程头文件通常用双引号:

#include <string.h>   /* 标准库:memset、strlen 等 */
#include "uart.h"    /* 当前工程自定义头文件 */

双引号和尖括号的核心区别是头文件搜索顺序,不是语法上“系统头文件也都用双引号”。视频中的数组方案若调用 memset,应包含 <string.h>;完整代码使用状态变量,不调用 memset,因此不需要这个头文件。

烧录前仍应断电隔离 ESP8266 与下载串口,避免两个发送端争用线路。不要只拔 GND 后带电操作;应关闭电源,断开或用跳帽隔离 TXD/RXD,烧录完成后再断电恢复接线。

三、测试

测试目标不只是服务器出现 qwer,还要证明每个状态的转换与异常处理都符合预期。建议保留一个只接收的串口监听通道观察 ESP8266 TXD;监听器 RXD 可以接到 ESP8266 TXD,但监听器 TXD 不能与单片机 TXD 并联。

3.1 工具与窗口的打开及串口选择

网络调试助手选择 TCP Server 并绑定正确的电脑局域网 IP 和端口。串口监听工具选择开发板对应的 COM 口、9600 8N1,文本采用 ASCII 显示。

如果下载器、调试串口和独立 USB 转 TTL 同时存在,Windows 会显示多个 COM 口。可通过插拔单个设备观察端口变化,或在设备管理器中查看硬件标识,不能只凭固定的 COM4 判断。

3.2 服务器打开与AT指令问题

应先打开 TCP Server,再复位单片机,因为初始化程序只在复位后执行一次。服务器尚未监听时,AT+CIPSTART 会失败或超时。

ESP8266 没有供电、UART 断线或波特率不一致时,程序不应无限快速发送 AT 指令。完整代码每条命令最多尝试两次,失败后令 init_success=0,主程序可进一步点亮故障灯、延时重启或等待人工处理。

3.3 接电与识别及AT指令停止

必须在断电状态完成 VCC、GND、TXD、RXD 的连接,再统一上电。若模块未回应,按顺序检查:

3.3 V 是否稳定 → GND 是否共地 → TX/RX 是否交叉
→ 51 TX 是否降到 3.3 V → 两端是否同为 9600 8N1
→ ESP8266 是否真的烧录 AT 固件

程序停止重试只能说明收到了期望事件,不能单凭停止现象证明整个网络链路成功;还要检查服务器是否显示在线、是否收到正确的 4 字节。

3.4 窗口重新打开与复位操作

关闭串口监听窗口通常只是停止电脑软件占用串口,并不会复位单片机或 ESP8266。按下开发板复位键才会让 51 从 main() 重新执行;给整板断电再上电则会同时重启 51、ESP8266 和电源电路。

三种操作含义不同:

操作51 是否重启ESP8266 是否重启网络连接是否重建
重开电脑串口窗口通常否
按 51 复位键通常否程序会重新发送联网命令
整板断电再上电

3.5 杜邦线连接问题与解决方法

杜邦线频繁插拔后,弹片夹持力会下降,可能出现接触电阻增大或间歇断路。重点检查 ESP8266 的 VCC、GND、EN/CH_PD、TXD、RXD,不存在所谓“VCC 和晶振引脚”这组连接;外接的四线接口通常不包含晶振引脚。

排查接触问题应先断电,再轻拉每根线检查固定情况,必要时直接更换杜邦线。上电后可测模块 VCC 是否稳定在 3.3 V 附近。反复开关电源只能暂时重启设备,不能修复松动接点。

3.6 服务器接收与问题解决验证

正常复位后的可观察证据应为:

  1. CWMODE 返回 OK
  2. CWJAP 出现 WIFI CONNECTEDWIFI GOT IPOK
  3. CIPSTART 出现 CONNECTOK,服务器显示客户端在线;
  4. CIPSEND=4 出现 >
  5. 单片机发送 qwer,模块返回 SEND OK
  6. 服务器接收区只出现 4 字节 qwer

若复位后第一次成功、第二次却提示已连接,说明 ESP8266 的 TCP 连接在 51 复位时没有同步断开。可以在初始化开头查询连接状态、发送 AT+CIPCLOSE,或正确处理 ALREADY CONNECTED,不能只靠重复 CIPSTART

3.7 例题1: 代码优化与工程保存

工程目录可以按课程阶段保存为 8.1ESP8266TCP实验8.2ESP8266TCP发送_代码优化。名称中的编号用于表明学习顺序,真正的版本管理仍建议使用 Git,便于查看每次修改、恢复历史状态并记录修改原因。

这次优化的核心不是“把延时换成循环”,而是建立了最初级的事件驱动结构:

固定等待:发送 → 猜一个时间 → 发送下一条
事件等待:发送 → 解析明确回复 → 成功继续 / 超时失败

后续接收实验将在此基础上扩展串口缓冲区和 +IPD 数据解析;届时要区分 AT 回复、异步 Wi-Fi 状态和真正的 TCP 数据正文。