WIFI发送代码优化

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 0 Likes: 0 Comments: 0

059.8.4 WIFI发送代码优化

/* ==================== main.c:基于 ESP8266 回复事件的发送流程 ====================
 * 目的:发送每条 AT 命令后,由串口中断识别 OK 或 >,主程序在限定时间内等待对应事件,超时才重试。
 * 并发约定:中断只置位事件、更新 OK 匹配状态;主程序短暂关闭 ES 后清除共享状态,避免竞争。
 * 局限:仅识别 OK 和 >,未解析 ERROR、FAIL、CONNECT、SEND OK 的完整语义,适合本节入门优化。
 */
#include "reg52.h"  /* 提供 ES、RI、SBUF 及 8051 特殊功能寄存器定义。 */
#include "delay.h"  /* 提供 1 ms 轮询等待所调用的 Delay_xms()。 */
#include "main.h"   /* 提供 uchar、uint 等项目公共类型。 */
#include "uart.h"   /* 提供 UART 发送和初始化接口。 */

#define ESP_EVENT_OK      0x01 /* 事件位 0:已识别相邻字符组成的 OK。 */
#define ESP_EVENT_PROMPT  0x02 /* 事件位 1:已收到 CIPSEND 后的 > 数据发送提示符。 */

volatile uchar esp_events = 0;    /* 事件位集合;中断置位、主程序读取和清零,必须声明 volatile。 */
volatile uchar ok_match_state = 0; /* OK 匹配状态:0 等待 O,1 表示刚收到 O、等待紧邻的 K。 */

/* 等待串口中断置位;等待期间仍可正常进入串口中断。 */
bit ESP_Wait_Event(uchar event_mask, uint timeout_ms) /* 在最多 timeout_ms 次约 1 ms 轮询内,等待指定事件位出现。 */
{                                                     /* 等待函数体开始。 */
    while (timeout_ms != 0)                           /* 尚未用尽超时预算时持续检查。 */
    {                                                 /* 单次轮询开始。 */
        if ((esp_events & event_mask) != 0)           /* 按位与检测所需事件是否已被中断置位。 */
        {                                             /* 期望事件已到达。 */
            return 1;                                 /* 返回真,调用者可进入下一状态。 */
        }                                             /* 本次未找到事件的判断结束。 */
        Delay_xms(1);                                 /* 等待约 1 ms;中断在此期间仍可抢占并设置事件位。 */
        timeout_ms--;                                 /* 消耗一个近似毫秒的超时计数。 */
    }                                                 /* 超时计数耗尽。 */
    return 0;                                         /* 返回假,表示本次等待超时。 */
}                                                     /* 等待函数结束。 */

/* 一次发送只在完整超时后才重试,避免快速重复打断 ESP8266。 */
bit ESP_Send_Command(const char *command, /* 待发送的完整 AT 命令字符串,命令类指令应自带 \r\n。 */
                     uchar expected_event, /* 本命令完成所必须等待的事件位,例如 OK 或 >。 */
                     uint timeout_ms,       /* 每次发送后允许等待的近似毫秒数。 */
                     uchar max_attempts)    /* 最多发送次数;限制故障时无限重发。 */
{                                           /* 发送并等待函数体开始。 */
    uchar attempt;                          /* 循环变量,记录当前已尝试次数。 */

    for (attempt = 0; attempt < max_attempts; attempt++) /* 每次循环是一次“清状态→发送→完整等待”的尝试。 */
    {                                                      /* 单次尝试开始。 */
        ES = 0;                                            /* 暂关串口中断,防止清状态过程中中断写入事件。 */
        esp_events = 0;                                    /* 清除上一次命令遗留的 OK、> 事件。 */
        ok_match_state = 0;                                /* 从“等待 O”的初始状态重新开始匹配。 */
        ES = 1;                                            /* 恢复串口中断,使随后到达的模块回复可被解析。 */

        UART_Send_Str(command);                            /* 将本次 AT 命令逐字节送给 ESP8266。 */
        if (ESP_Wait_Event(expected_event, timeout_ms))     /* 阻塞等待该命令真正关心的回复事件。 */
        {                                                  /* 在时限内等到事件。 */
            return 1;                                      /* 返回真;不再进行剩余重试。 */
        }                                                  /* 本次等待超时,for 将决定是否重试。 */
    }                                                      /* 所有尝试均未成功。 */
    return 0;                                              /* 返回假,让上层停止初始化流程。 */
}                                                          /* 命令发送函数结束。 */

void main(void)                       /* 系统入口,按网络连接步骤串行执行初始化。 */
{                                    /* 主函数体开始。 */
    bit init_success = 1;            /* 1 表示前序步骤都成功;任一步失败后置 0,阻止后续命令继续发送。 */

    Delay_xms(1000);                 /* 给 ESP8266 上电启动文本留出时间;仍不能替代真正的 AT 就绪检测。 */
    UART_Init();                     /* 建立 51 与 ESP8266 的 9600 bit/s 串口连接,并打开接收中断。 */

    /* 模式设置通常很快;加入 Wi-Fi 和建立 TCP 连接需要更长超时。 */
    if (!ESP_Send_Command("AT+CWMODE=3\r\n", /* 设置 AP+STA Wi-Fi 模式。 */
                          ESP_EVENT_OK, 2000, 2)) /* 最多两次,每次最多等待约 2 秒 OK。 */
    {                                             /* 未收到模式设置成功事件。 */
        init_success = 0;                         /* 记录失败,后续依赖网络状态的命令不再执行。 */
    }                                             /* 第一条命令处理结束。 */

    if (init_success &&                         /* 仅在设置模式成功时才尝试加入热点。 */
        !ESP_Send_Command("AT+CWJAP=\"YOUR_SSID\",\"YOUR_PASSWORD\"\r\n", /* 填入实际 SSID、密码后执行关联。 */
                          ESP_EVENT_OK, 15000, 2)) /* 热点关联可能较慢,每次等待最多约 15 秒。 */
    {                                             /* 关联失败或超时。 */
        init_success = 0;                         /* 阻止建立 TCP 连接。 */
    }                                             /* 加入热点步骤结束。 */

    if (init_success &&                           /* 必须先成功入网,才有条件连接局域网服务器。 */
        !ESP_Send_Command("AT+CIPSTART=\"TCP\",\"192.168.1.100\",5132\r\n", /* 向指定 IP:端口发起 TCP 主动连接。 */
                          ESP_EVENT_OK, 10000, 2)) /* 最多两次,每次等待约 10 秒 OK;完整工程还应确认 CONNECT。 */
    {                                               /* TCP 建连没有获得期望结果。 */
        init_success = 0;                           /* 停止后续 CIPSEND 与数据发送。 */
    }                                               /* TCP 建连步骤结束。 */

    /* CIPSEND 的关键就绪标志是字符 '>',不是仅看到 OK。 */
    if (init_success &&                         /* 只有连接成功后才声明发送正文长度。 */
        !ESP_Send_Command("AT+CIPSEND=4\r\n", /* 请求 ESP8266 接收随后的 4 字节数据。 */
                          ESP_EVENT_PROMPT, 3000, 2)) /* 此命令以 > 而不是 OK 表示可以发送正文。 */
    {                                             /* 未等到 > 或重试次数用尽。 */
        init_success = 0;                         /* 不发送正文,避免数据落入错误的 AT 状态。 */
    }                                             /* 发送长度准备步骤结束。 */

    if (init_success)                            /* 四个准备步骤均成功时才发送 TCP 正文。 */
    {                                            /* 正文发送分支开始。 */
        ES = 0;                                  /* 与前面同理:关串口中断后原子地清接收状态。 */
        esp_events = 0;                          /* 清掉 CIPSEND 返回的 >,避免它被误认为正文发送后的事件。 */
        ok_match_state = 0;                      /* 清掉可能处于半匹配状态的 O。 */
        ES = 1;                                  /* 重新允许中断解析 ESP8266 返回的 SEND OK。 */

        UART_Send_Str("qwer");                 /* 发送恰好 4 个字节,与 AT+CIPSEND=4 严格匹配。 */
        ESP_Wait_Event(ESP_EVENT_OK, 3000);     /* 等待 SEND OK 中出现的 OK;本示例未使用返回值处理失败。 */
    }                                            /* 正文发送分支结束。 */

    while (1)                                    /* 初始化只执行一次;后续应用逻辑或重连策略可写在这里。 */
    {                                            /* 无限主循环体开始。 */
        /* init_success=0 时可在这里点亮故障灯或进入重连流程。 */
    }                                            /* 当前没有前台任务,循环立即开始下一轮。 */
}                                                /* 正常运行不会从 main 返回。 */

void UART_Routine(void) interrupt 4 /* 8051 串口中断服务函数;每接收一个字节时进入。 */
{                                   /* 中断函数体开始。 */
    uchar received_byte;            /* 保存本次读出的字节,避免反复访问 SBUF。 */

    if (RI == 1)                    /* 仅处理接收完成标志;TI 由 UART_Send_Byte() 查询并清除。 */
    {                               /* 接收处理分支开始。 */
        RI = 0;                     /* 软件确认并清除接收完成事件。 */
        received_byte = SBUF;       /* 读取 ESP8266 刚发送的一个字节。 */

        if (received_byte == '>')   /* > 是 CIPSEND 告知“现在可发送指定长度正文”的提示符。 */
        {                           /* 收到提示符。 */
            esp_events |= ESP_EVENT_PROMPT; /* 按位置 1;不影响 OK 等其他已出现事件。 */
        }                           /* 提示符处理结束。 */

        /* 只识别相邻的、大写的 O 和 K,并允许 OOK 这样的重叠起点。 */
        if (ok_match_state == 0)    /* 初始状态:尚未收到 OK 的第一个字符 O。 */
        {                           /* 等待 O 的分支开始。 */
            if (received_byte == 'O') /* 当前字节可作为 OK 的起点。 */
            {                       /* 确认收到 O。 */
                ok_match_state = 1; /* 转入“等待紧邻 K”的状态。 */
            }                       /* O 的处理结束;其他字符保持状态 0。 */
        }                           /* 初始状态分支结束。 */
        else                        /* 状态为 1,说明上一字节是 O。 */
        {                           /* 等待 K 的分支开始。 */
            if (received_byte == 'K') /* O 后紧跟 K,严格匹配到 OK。 */
            {                       /* 完成匹配。 */
                esp_events |= ESP_EVENT_OK; /* 设置 OK 事件供主程序轮询到。 */
                ok_match_state = 0;          /* 重置匹配器,准备识别下一组 OK。 */
            }                                  /* 成功匹配分支结束。 */
            else if (received_byte == 'O')    /* O O K 时,第二个 O 仍可作为新的起点。 */
            {                                  /* 处理重叠起点。 */
                ok_match_state = 1;           /* 保持等待 K 状态。 */
            }                                  /* 重叠起点处理结束。 */
            else                               /* O 后若是无关字符,说明相邻 OK 匹配失败。 */
            {                                  /* 失败处理开始。 */
                ok_match_state = 0;            /* 回到初始状态,等待下一个 O。 */
            }                                  /* 失败处理结束。 */
        }                                      /* OK 状态机分支结束。 */
    }                                          /* 接收分支结束。 */
}                                              /* 中断返回。 */

/* ==================== main.h ==================== */
#ifndef _MAIN_H_       /* 若尚未展开 main.h,才处理其公共声明。 */
#define _MAIN_H_       /* 定义保护宏,防止同一翻译单元重复定义类型和 sbit 别名。 */

#include "reg52.h"     /* 使 P1、P2 的特殊功能寄存器名称在本头文件中可见。 */

typedef unsigned char uchar; /* 8 位无符号整数的项目内短名。 */
typedef unsigned int  uint;  /* Keil C51 中通常为 16 位无符号整数,供毫秒参数使用。 */

sbit LED1 = P1^0;      /* C51 位寻址别名:LED1 对应 P1.0,开发板采用低电平点亮。 */
sbit LED2 = P1^1;      /* 将 LED2 映射到 P1.1。 */
sbit LED3 = P1^2;      /* 将 LED3 映射到 P1.2。 */
sbit LED4 = P1^3;      /* 将 LED4 映射到 P1.3。 */
sbit BEEP = P1^6;      /* 将低电平有效的蜂鸣器控制信号映射到 P1.6。 */
sbit JDQ1 = P2^0;      /* 将低电平有效的继电器 1 控制信号映射到 P2.0。 */

#endif                 /* 结束 main.h 的防重复包含区。 */

/* ==================== Uart/uart.c:串口发送与初始化实现 ====================
 * 发送策略:每写入 SBUF 一个字节后,轮询 TI,确认硬件已从 TXD 发完才送下一个字节。
 * 初始化前提:11.0592 MHz、传统 12T 8051、SMOD=0 时,定时器 1 重装值 0xFD 对应 9600 bit/s。
 */
#include "uart.h"      /* 引入接口声明,以及其间接包含的 uchar、寄存器定义。 */

void UART_Send_Byte(uchar send_byte) /* 将一个字节阻塞式发送到 UART TXD。 */
{                                    /* 单字节发送函数体开始。 */
    SBUF = send_byte;               /* 写数据缓冲寄存器,串口硬件随后按起始位、数据位和停止位自动发送。 */
    while (TI == 0);                /* TI 未置位就持续等待;空循环体的分号表示故意忙等。 */
    TI = 0;                         /* 软件清发送完成标志,避免下一次发送读取到旧状态。 */
}                                    /* 单字节发送完成。 */

void UART_Send_Str(const char *send_str) /* 发送以 C 语言 \0 结尾的字符串;const 表示函数不修改字符串内容。 */
{                                       /* 字符串发送函数体开始。 */
    while (*send_str != '\0')          /* 读取当前字符;遇到结束符则停止,结束符本身不会被发送。 */
    {                                   /* 当前字符的处理体开始。 */
        UART_Send_Byte((uchar)*send_str); /* 把 char 转成项目的 8 位 uchar 后,交给单字节发送函数。 */
        send_str++;                     /* 指针向后移动一个字符,准备读取字符串中的下一个字节。 */
    }                                   /* 所有非 \0 字符发送完毕后退出。 */
}                                       /* 字符串发送结束。 */

void UART_Init(void)                /* 配置 UART、波特率发生器和串口中断。 */
{                                   /* 初始化函数体开始。 */
    SCON = 0x50;                    /* 设置方式 1(8 位 UART、可变波特率),并将 REN 置 1 允许接收。 */
    PCON &= 0x7F;                   /* 清 SMOD=0,不将串口波特率加倍;其余 PCON 位保持。 */
    TMOD = (TMOD & 0x0F) | 0x20;   /* 保留定时器 0 的低四位,把定时器 1 设置为方式 2 自动重装。 */
    TH1 = 0xFD;                     /* 写入定时器 1 的重装值,决定稳定的溢出周期。 */
    TL1 = 0xFD;                     /* 同时初始化当前计数值,使第一次溢出也按目标周期产生。 */
    RI = 0;                         /* 清除可能遗留的接收完成标志,避免初始化后误进中断。 */
    TI = 0;                         /* 清除可能遗留的发送完成标志,使第一次发送确实等待硬件完成。 */
    TR1 = 1;                        /* 启动定时器 1,开始向 UART 提供波特率时钟。 */
    ES = 1;                         /* 允许串口中断源提出请求。 */
    EA = 1;                         /* 打开 CPU 总中断开关;ES 与 EA 都为 1 时 RI 才能触发服务程序。 */
}                                   /* 初始化结束。 */

/* ==================== Uart/uart.h ==================== */
#ifndef _UART_H_       /* 若 uart.h 尚未被包含,才保留以下接口声明。 */
#define _UART_H_       /* 定义唯一保护宏,避免重复声明。 */

#include "main.h"      /* 获得 uchar 类型和寄存器相关公共定义。 */

void UART_Send_Byte(uchar send_byte);     /* 对外声明:阻塞式发送一个 8 位数据。 */
void UART_Send_Str(const char *send_str); /* 对外声明:发送 C 字符串,不包含末尾 \0。 */
void UART_Init(void);                     /* 对外声明:初始化本工程 UART。 */

#endif                 /* 结束 uart.h 的防重复包含区。 */

/* ==================== Delay/delay.c:软件忙等延时实现 ====================
 * 用途:在教学示例中粗略等待 ESP8266 处理前一条指令;等待期间 CPU 无法执行其他前台工作。
 */
#include "delay.h"     /* 引入 Delay_xms() 声明及 uint、uchar 类型。 */
#include <intrins.h>    /* 引入 Keil C51 的 _nop_() 内在函数声明。 */

void Delay_xms(uint xms)            /* 反复执行约 1 ms 的校准循环;仅在 11.0592 MHz 等前提成立时近似正确。 */
{                                   /* 延时函数体开始。 */
    uchar data i, j;                /* 两个 8 位计数器;data 指定 C51 片内直接寻址 RAM 存储类别。 */

    while (xms != 0)                /* 仍有未完成的毫秒数时,执行一轮空转。 */
    {                               /* 一轮近似 1 ms 的延时开始。 */
        _nop_();                    /* 运行一条不改变程序状态的空指令,补齐校准所需指令周期。 */
        i = 2;                      /* 设定外层循环次数,是延时公式中的固定参数。 */
        j = 199;                    /* 设定内层初值;后续 8 位递减与回绕共同制造空转时间。 */
        do                          /* 后判断循环确保循环体至少执行一次。 */
        {                           /* 外层空转循环体开始。 */
            while (--j);            /* 先递减 j;只要结果非零就继续空循环,j 到零时退出。 */
        } while (--i);              /* 再递减 i;重入时 j 会在下一次 --j 从 0 回绕到 255。 */
        xms--;                      /* 已消耗约 1 ms,减少剩余轮数。 */
    }                               /* xms 为零时退出。 */
}                                   /* 返回调用者。 */

/* ==================== Delay/delay.h ==================== */
#ifndef _DELAY_H_      /* 检查 delay.h 是否已在当前翻译单元展开。 */
#define _DELAY_H_      /* 定义延时模块保护宏。 */

#include "main.h"      /* 引入 uint 类型,使下面声明中的参数类型有效。 */

void Delay_xms(uint xms); /* 对外声明:请求约 xms 毫秒的软件忙等。 */

#endif                 /* 结束 delay.h 防重复包含区。 */

一、任务信息资源导图

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 CONNECTED、WIFI GOT IP,最后 OK
AT+CIPSTARTCONNECT,随后 OK
AT+CIPSEND=n发送就绪提示符 >
发送数据正文SEND OK

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

2.1 定义全局数组

视频采用两个元素的数组保存字符 O、K:

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_events、ok_match_state,所以它们必须声明为 volatile。volatile 告诉编译器这些变量可能在当前代码看不见的位置发生改变,循环中必须重新从内存读取;它不等于线程安全,也不自动解决复杂的并发读写问题。

2.2 删除switch语句

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

写法类型/含义
'O'单个字符,通常为一个字节
"OK"C 字符串,内存中为 O、K、\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 CONNECTED、WIFI GOT IP 和 OK;
  3. CIPSTART 出现 CONNECT 和 OK,服务器显示客户端在线;
  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 数据正文。