WIFI发送代码优化
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+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 数据正文。