WIFI发送代码优化
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+CWMODE | OK |
AT+CWJAP | WIFI CONNECTED、WIFI GOT IP,最后 OK |
AT+CIPSTART | CONNECT,随后 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。这会产生三个问题:
AT+CWJAP连接热点本来就可能需要数秒,过早重发会让模块返回busy或打乱流程;- 没有总超时和最大次数,模块断电或接线错误时会永久发送;
AT+CIPSEND真正表示“可发送正文”的事件是>,只识别OK不够精确。
完整代码使用“每次发送后等待完整超时,超时才重试”的方式,并限制 max_attempts。主循环等待时,串口中断仍会运行:
主程序:发命令 ──────── 1 ms 轮询事件 ────────> 成功/超时
▲
中断: 每到一个字节就解析,并设置事件位
采用固定 1 ms 软件延时做超时计时仍只是入门方案,其精度会受中断和编译器影响;正式项目宜使用硬件定时器产生系统节拍,并把整个过程组织成非阻塞状态机。
2.5 删除无用代码
从其他实验复制工程时,要清理已不再使用的遥控变量、switch 分支和测试代码,但不能用“编译没有报错”作为删对的唯一依据。编译器只能发现语法、类型和链接问题,无法证明业务逻辑正确。
遇到错误时可按以下顺序处理:
- 先看第一条 error,而不是最后一条连带错误;
- 确认报错的
.c文件和行号; - 双击错误跳转后,检查标识符拼写、声明与定义是否一致;
- 若提示函数未声明,检查是否包含正确头文件;
- 若提示重复定义,检查变量是否同时写在头文件和多个
.c文件中; - 编译通过后还要验证返回信息、超时和失败路径。
全局变量通常在一个 .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 服务器接收与问题解决验证
正常复位后的可观察证据应为:
CWMODE返回OK;CWJAP出现WIFI CONNECTED、WIFI GOT IP和OK;CIPSTART出现CONNECT和OK,服务器显示客户端在线;CIPSEND=4出现>;- 单片机发送
qwer,模块返回SEND OK; - 服务器接收区只出现 4 字节
qwer。
若复位后第一次成功、第二次却提示已连接,说明 ESP8266 的 TCP 连接在 51 复位时没有同步断开。可以在初始化开头查询连接状态、发送 AT+CIPCLOSE,或正确处理 ALREADY CONNECTED,不能只靠重复 CIPSTART。
3.7 例题1: 代码优化与工程保存
工程目录可以按课程阶段保存为 8.1ESP8266TCP实验 和 8.2ESP8266TCP发送_代码优化。名称中的编号用于表明学习顺序,真正的版本管理仍建议使用 Git,便于查看每次修改、恢复历史状态并记录修改原因。
这次优化的核心不是“把延时换成循环”,而是建立了最初级的事件驱动结构:
固定等待:发送 → 猜一个时间 → 发送下一条
事件等待:发送 → 解析明确回复 → 成功继续 / 超时失败
后续接收实验将在此基础上扩展串口缓冲区和 +IPD 数据解析;届时要区分 AT 回复、异步 Wi-Fi 状态和真正的 TCP 数据正文。