WIFI接收代码_1
060.8.5 WIFI接收代码(1)
/* ==================== main.c:ESP8266 回复与 TCP 正文双层解析 ====================
* 处理顺序:UART 中断先用 +IPD 状态机隔离 TCP 正文;仅非正文数据再用于匹配 AT 回复 OK、>。
* 应用协议:TCP 正文中的 LED10/LED11/LED20/LED21/BIP0/BIP1 分别控制两路低电平有效 LED 或蜂鸣器。
*/
#include "reg52.h" /* 引入 ES、RI、SBUF 等 8051 特殊功能寄存器定义。 */
#include "delay.h" /* 引入 Delay_xms(),供 AT 指令等待超时使用。 */
#include "main.h" /* 引入 uchar、uint 和 LED/BEEP 引脚别名。 */
#include "uart.h" /* 引入 UART 初始化与字符串发送接口。 */
#define ESP_EVENT_OK 0x01 /* AT 同步回复中已识别到 OK 的事件位。 */
#define ESP_EVENT_PROMPT 0x02 /* CIPSEND 已给出 > 提示符的事件位。 */
#define IPD_WAIT_PLUS 0 /* 尚未开始,等待 +IPD 的 +。 */
#define IPD_WAIT_I 1 /* 已收到 +,等待 I。 */
#define IPD_WAIT_P 2 /* 已收到 +I,等待 P。 */
#define IPD_WAIT_D 3 /* 已收到 +IP,等待 D。 */
#define IPD_WAIT_COMMA 4 /* 已收到 +IPD,等待分隔逗号。 */
#define IPD_READ_LENGTH 5 /* 累积冒号前的十进制正文长度。 */
#define IPD_READ_PAYLOAD 6 /* 正在读取长度指定的 TCP 正文字节。 */
volatile uchar esp_events = 0; /* 中断置位、前台读取的事件集合;volatile 防止读取被优化为旧值。 */
volatile uchar ok_match_state = 0; /* OK 匹配进度:0 等 O,1 表示上一个字节是 O、等紧邻 K。 */
/* 以下状态只在串口中断中使用。 */
uchar ipd_state = IPD_WAIT_PLUS; /* +IPD 协议解析状态;仅由 UART 中断访问。 */
uint ipd_length = 0; /* 当前 +IPD 声明的正文总长度。 */
uint ipd_received = 0; /* 本帧已经交给应用解析器的正文数。 */
uchar app_state = 0; /* LED/BIP 命令匹配状态。 */
uchar led_number = 0; /* 已识别的灯号 1 或 2,等待后续电平字符。 */
void APP_Restart_Matcher(uchar received_byte) /* 复位命令匹配器,并把当前字节尝试作为下一条命令的开头。 */
{ /* 函数体开始。 */
app_state = 0; /* 默认回到“等待 L 或 B”的初始状态。 */
if (received_byte == 'L') /* L 可以是 LED 命令的起始字节。 */
{ /* 识别 LED 起始符。 */
app_state = 1; /* 下一字节应为 E。 */
} /* L 分支结束。 */
else if (received_byte == 'B') /* B 可以是 BIP 命令的起始字节。 */
{ /* 识别 BIP 起始符。 */
app_state = 10; /* 下一字节应为 I。 */
} /* B 分支结束;其他字符保持状态 0。 */
}
/*
* 从 TCP 连续字节流中识别:LED10、LED11、LED20、LED21、BIP0、BIP1。
* 该状态机可跨越多个 +IPD 分段,也能连续处理多条紧邻的命令。
*/
void APP_Parse_Byte(uchar received_byte) /* 每收到一个 TCP 正文字节调用一次,允许命令在多帧间拆分或连续。 */
{ /* 应用层解析函数体开始。 */
switch (app_state) /* 依据此前已匹配的前缀,决定本字节的合法含义。 */
{ /* 各状态分支开始。 */
case 0: /* 尚未匹配到命令前缀。 */
APP_Restart_Matcher(received_byte); /* 当前字节若为 L/B,就成为新命令起点。 */
break; /* 结束初始状态处理。 */
case 1: /* 已收到 L */
if (received_byte == 'E') app_state = 2; /* LE 匹配成功,下一字节期待 D。 */
else APP_Restart_Matcher(received_byte); /* 前缀失败,但当前字节仍可能是新起点。 */
break;
case 2: /* 已收到 LE */
if (received_byte == 'D') app_state = 3; /* LED 前缀完整,下一字节应为灯号。 */
else APP_Restart_Matcher(received_byte); /* LE 后非 D,重新寻找起点。 */
break;
case 3: /* 已收到 LED */
if (received_byte == '1' || received_byte == '2') /* 本教学协议只定义 LED1、LED2。 */
{ /* 灯号有效。 */
led_number = (uchar)(received_byte - '0'); /* ASCII 字符转为数值 1 或 2。 */
app_state = 4; /* 下一字节将给出输出电平。 */
} /* 灯号处理结束。 */
else /* 灯号无效。 */
{ /* 重新同步。 */
APP_Restart_Matcher(received_byte); /* 当前字符仍尝试作为新命令起点。 */
} /* 无效灯号处理结束。 */
break;
case 4: /* 等待 LED 的电平参数 */
if (received_byte == '0' || received_byte == '1') /* 电平字符仅允许 0、1。 */
{ /* 收到有效电平。 */
if (led_number == 1) LED1 = (received_byte == '1'); /* LED1 低有效;命令 0 写 0 点亮,1 写 1 熄灭。 */
if (led_number == 2) LED2 = (received_byte == '1'); /* 对 LED2 执行相同的低有效电平控制。 */
} /* 有效电平处理结束。 */
APP_Restart_Matcher(received_byte); /* 一条 LED 命令已结束,当前字节可兼作下一命令起点。 */
break;
case 10: /* 已收到 B,等待 I。 */
if (received_byte == 'I') app_state = 11; /* BI 匹配成功,继续等待 P。 */
else APP_Restart_Matcher(received_byte); /* 前缀失败后重新同步。 */
break; /* 结束 BIP 的第一字符状态。 */
case 11: /* 已收到 BI,等待 P。 */
if (received_byte == 'P') app_state = 12; /* BIP 前缀完整,下一字节应是电平。 */
else APP_Restart_Matcher(received_byte); /* 非 P 时重新同步。 */
break; /* 结束 BI 状态。 */
case 12: /* 已收到 BIP,等待蜂鸣器电平。 */
if (received_byte == '0') BEEP = 0; /* 板载蜂鸣器低有效,写 0 使其鸣响。 */
if (received_byte == '1') BEEP = 1; /* 写 1 撤销有效控制电平,蜂鸣器停止。 */
APP_Restart_Matcher(received_byte); /* 本条命令结束,尝试从当前字节重启匹配。 */
break; /* 结束 BIP 命令。 */
default: /* 防御性分支:状态值异常时不执行任何硬件动作。 */
app_state = 0; /* 恢复到安全的初始匹配状态。 */
break; /* 结束异常状态处理。 */
} /* 应用命令状态机结束。 */
} /* 单字节应用解析结束。 */
/* 返回 1 表示当前字节属于 TCP 正文,不能再拿去匹配 AT 回复。 */
bit IPD_Parse_Byte(uchar received_byte) /* 解析 +IPD,<长度>:;返回 1 时该字节属于正文,不能再参与 AT 回复匹配。 */
{ /* +IPD 解析函数体开始。 */
switch (ipd_state) /* 根据当前前缀进度解释串口字节。 */
{ /* +IPD 各状态开始。 */
case IPD_WAIT_PLUS: /* 空闲状态,寻找一帧上报的 +。 */
if (received_byte == '+') ipd_state = IPD_WAIT_I; /* +IPD 前缀开始,下一字节期待 I。 */
break; /* 非 + 数据仍留在空闲状态。 */
case IPD_WAIT_I: /* 已收到 +,等待 I。 */
if (received_byte == 'I') ipd_state = IPD_WAIT_P; /* +I 已匹配。 */
else ipd_state = (received_byte == '+') ? IPD_WAIT_I : IPD_WAIT_PLUS; /* 连续 + 可作为新起点,其他字符丢弃前缀。 */
break; /* I 状态处理结束。 */
case IPD_WAIT_P: /* 已收到 +I,等待 P。 */
if (received_byte == 'P') ipd_state = IPD_WAIT_D; /* +IP 已匹配。 */
else ipd_state = (received_byte == '+') ? IPD_WAIT_I : IPD_WAIT_PLUS; /* 失败时按当前字节决定能否重启。 */
break; /* P 状态处理结束。 */
case IPD_WAIT_D: /* 已收到 +IP,等待 D。 */
if (received_byte == 'D') ipd_state = IPD_WAIT_COMMA; /* +IPD 已匹配,下一字节应为逗号。 */
else ipd_state = (received_byte == '+') ? IPD_WAIT_I : IPD_WAIT_PLUS; /* 前缀失败则重新同步。 */
break; /* D 状态处理结束。 */
case IPD_WAIT_COMMA: /* 已收到 +IPD,等待长度字段前的逗号。 */
if (received_byte == ',') /* 逗号确认单连接格式的长度字段即将开始。 */
{ /* 分隔符正确。 */
ipd_length = 0; /* 新帧开始,清除上一帧的正文长度。 */
ipd_state = IPD_READ_LENGTH; /* 转入十进制长度累积状态。 */
} /* 正确逗号处理结束。 */
else /* 不是逗号,前缀不是合法 +IPD 格式。 */
{ /* 格式错误处理开始。 */
ipd_state = IPD_WAIT_PLUS; /* 丢弃无效前缀,重新寻找下一帧。 */
} /* 格式错误处理结束。 */
break; /* 结束逗号状态。 */
case IPD_READ_LENGTH: /* 冒号前必须读取至少一位十进制长度。 */
if (received_byte >= '0' && received_byte <= '9') /* 当前字节是有效十进制数字。 */
{
/* 限制长度,避免十进制累加发生 16 位溢出。 */
if (ipd_length <= 6000) /* 先限制累积值,避免 16 位 uint 乘 10 溢出。 */
{ /* 长度仍在允许范围。 */
ipd_length = ipd_length * 10 + (received_byte - '0'); /* 左移十进制位后加入当前字符数值。 */
} /* 数字累积结束。 */
else /* 过长或异常长度。 */
{ /* 放弃这帧。 */
ipd_state = IPD_WAIT_PLUS; /* 回到同步起点,不按不可信长度读取正文。 */
} /* 异常长度处理结束。 */
}
else if (received_byte == ':' && ipd_length != 0) /* 冒号表示长度字段结束,且零长度帧不作为本应用有效正文。 */
{ /* 格式完整,可开始读取正文。 */
ipd_received = 0; /* 本帧已消费字节数从零开始。 */
ipd_state = IPD_READ_PAYLOAD; /* 后续恰好读取 ipd_length 个字节。 */
} /* 正文开始准备结束。 */
else /* 非数字且不是合法冒号,说明帧格式损坏。 */
{ /* 无效长度处理。 */
ipd_state = IPD_WAIT_PLUS; /* 丢弃该帧并重新同步。 */
} /* 无效长度处理结束。 */
break; /* 结束长度解析状态。 */
case IPD_READ_PAYLOAD: /* 当前字节已被协议声明为 TCP 正文。 */
APP_Parse_Byte(received_byte); /* 只将正文交给 LED/BIP 应用命令状态机。 */
ipd_received++; /* 记录已消费一个正文字节。 */
if (ipd_received >= ipd_length) /* 已按声明长度收完本帧。 */
{ /* 本帧结束。 */
ipd_state = IPD_WAIT_PLUS; /* 返回空闲状态,等待后续 +IPD 帧。 */
} /* 帧结束判断完成。 */
return 1; /* 通知调用者:此字节不可再当作 AT 回复中的 OK 或 >。 */
default: /* 状态值意外损坏时采用安全恢复。 */
ipd_state = IPD_WAIT_PLUS; /* 清除未知状态,重新寻找 + 前缀。 */
break; /* 结束异常状态。 */
} /* +IPD 状态机结束。 */
return 0; /* 当前字节不是正文,因此仍可用于 AT 事件解析。 */
} /* +IPD 单字节解析结束。 */
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); /* 空等约 1 ms;UART 中断仍能运行。 */
timeout_ms--; /* 消耗一次轮询时间。 */
} /* 时间已用尽。 */
return 0; /* 未收到期望事件,报告超时。 */
} /* 等待函数结束。 */
bit ESP_Send_Command(const char *command, /* 本次 AT 命令文本。 */
uchar expected_event, /* 完成依据:OK 或 > 等事件位。 */
uint timeout_ms, /* 单次尝试等待上限。 */
uchar max_attempts) /* 最大尝试次数,防止永久重发。 */
{ /* 发送并等待函数体开始。 */
uchar attempt; /* 记录当前尝试序号。 */
for (attempt = 0; attempt < max_attempts; attempt++) /* 每一轮完整地清状态、发送、等待。 */
{ /* 单次尝试开始。 */
ES = 0; /* 临时关闭串口中断,避免共享状态清零与 ISR 竞争。 */
esp_events = 0; /* 去掉上一条指令遗留事件。 */
ok_match_state = 0; /* 重新从等待 O 状态匹配 OK。 */
ES = 1; /* 恢复接收中断。 */
UART_Send_Str(command); /* 将 AT 命令送入 ESP8266。 */
if (ESP_Wait_Event(expected_event, timeout_ms)) return 1; /* 收到期望回复则立即成功返回。 */
} /* 本次已超时,可能进入下一次重试。 */
return 0; /* 所有尝试失败。 */
} /* 命令函数结束。 */
void main(void) /* 依序联网、建 TCP 连接,然后由 ISR 持续接收控制命令。 */
{ /* 主函数体开始。 */
bit init_success = 1; /* 前序流程是否仍成功;失败时阻断后续网络动作。 */
Delay_xms(1000); /* 规避 ESP8266 上电文本;可靠工程应再检测 ready/AT。 */
UART_Init(); /* 初始化 9600 bit/s UART 并打开接收中断。 */
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;
if (init_success &&
!ESP_Send_Command("AT+CIPSEND=4\r\n", ESP_EVENT_PROMPT, 3000, 2))
init_success = 0;
if (init_success)
{
UART_Send_Str("qwer");
}
while (1) /* 网络数据到达时由 UART ISR 异步解析,前台无需轮询正文。 */
{ /* 无限循环体开始。 */
/* TCP 数据由串口中断中的 +IPD 状态机持续解析。 */
} /* 当前无其他前台任务。 */
} /* 正常运行不返回。 */
void UART_Routine(void) interrupt 4 /* 8051 UART 中断服务程序,每个接收字节进入一次。 */
{ /* 中断函数体开始。 */
uchar received_byte; /* 保存本次从 SBUF 读出的字节。 */
if (RI == 1) /* 只在接收完成时处理;TI 由发送函数轮询。 */
{ /* 接收分支开始。 */
RI = 0; /* 清接收完成标志,准备下一次 UART 接收。 */
received_byte = SBUF; /* 读取 ESP8266 当前发来的一个字节。 */
if (!IPD_Parse_Byte(received_byte)) /* 非 TCP 正文才可能是 AT 回复或提示符。 */
{ /* AT 事件解析分支开始。 */
if (received_byte == '>') /* CIPSEND 的发送就绪提示符。 */
{ /* 收到提示符。 */
esp_events |= ESP_EVENT_PROMPT; /* 置位提示事件,供前台等待函数发现。 */
} /* 提示符处理结束。 */
if (ok_match_state == 0) /* 初始状态:等待 OK 的 O。 */
{ /* 初始匹配分支。 */
if (received_byte == 'O') ok_match_state = 1; /* O 到达后等待紧邻的 K。 */
} /* 初始匹配结束。 */
else if (received_byte == 'K') /* 前字节是 O 且当前字节为 K,匹配成功。 */
{ /* OK 处理开始。 */
esp_events |= ESP_EVENT_OK; /* 发布 OK 成功事件。 */
ok_match_state = 0; /* 重置匹配器。 */
} /* OK 处理结束。 */
else if (received_byte != 'O') /* O 后的无关字节使相邻匹配失败。 */
{ /* 失败处理开始。 */
ok_match_state = 0; /* 回到等待 O 的初始状态。 */
} /* 失败处理结束;若又收到 O 则保持状态 1,支持 OOK 重叠。 */
} /* AT 事件解析结束。 */
} /* 接收分支结束。 */
} /* 中断返回前台。 */
/* ==================== 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_
#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:软件忙等延时实现 ====================
* 用途:在教学示例中粗略等待 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 防重复包含区。 */
一、课程回顾
上一节已经让 51 单片机自动配置 ESP8266、连接电脑 TCP Server 并发送 qwer。本节把数据方向反过来:电脑服务器发送控制命令,ESP8266 通过 UART 上报 +IPD,单片机解析其中的 TCP 正文并控制 LED 或蜂鸣器。
1.1 例题1:ESP8266模块不稳定问题及解决过程
视频在 UART_Init() 前加入约 1 秒延时,目的是让 51 暂时不接收 ESP8266 的上电输出,再开始解析 AT 回复。这确实可以降低启动文本干扰简单解析器的概率,但“一秒后一定稳定”不是协议保证。
ESP8266 上电包含多个阶段:
电源上升
↓
ROM 启动输出(常见波特率约 74880,9600 下可能显示乱码)
↓
AT 固件初始化
↓
输出 ready 或进入可响应 AT 的状态
↓
单片机开始发送 AT 指令
更可靠的启动策略是等待 ready,或延时后周期性发送 AT,只有收到完整 OK 才进入下一状态,并为整个过程设置超时。若模块启动需要超过一秒、供电反复跌落或 51 与 ESP8266 并非同时上电,单纯固定延时仍可能失败。
1.2 例题2: 查找ESP8266模块不稳定原因
上电输出本身不是“电气干扰”,而是 ESP8266 TXD 上的合法串口数据。真正的问题是接收程序没有区分:
- ESP8266 启动/状态文本;
- AT 指令同步回复,如
OK、ERROR; - 异步网络事件,如
WIFI DISCONNECT、CLOSED; - TCP 数据上报,如
+IPD,5:LED10。
把这些数据都塞进同一个固定下标数组,再只看少数字符,很容易误判。完整源码采用两个状态机:一个按 +IPD,<长度>: 提取 TCP 正文,另一个只在非 TCP 正文区域识别 AT 事件。
1.3 例题3: 通过监控log分析ESP8266模块问题
可以用 USB 转 TTL 以“只监听”的方式观察 ESP8266 TXD,同时仍让 51 接收同一信号:
┌──> 51 P3.0 / RXD
ESP8266 TXD(3.3 V)─────┤
└──> USB 转 TTL RXD ──> 电脑串口助手
ESP8266 GND ──────────────── 51 GND ─────────── USB 转 TTL GND
USB 转 TTL TXD:悬空,不接入这根监听线
两个 RXD 都是高阻输入,并联监听同一个 TXD 通常可行,但必须满足:
- 三方 GND 共地,否则接收端没有共同电压参考;
- USB 转 TTL 的 TXD 不得接入,否则会和 51 TXD 争用 ESP8266 RXD;
- 不要把两个模块各自的 3.3 V/5 V 电源输出直接并联;
- USB 转 TTL 的 RXD 应能识别 3.3 V 高电平;
- 若要看 ROM 启动文本,可尝试其启动波特率;看 AT 固件通信则使用已配置的 9600。
flowchart LR
T[ESP8266 TXD] --> R1[51 RXD<br/>实际控制程序]
T --> R2[USB-TTL RXD<br/>只读监听]
R2 --> P[电脑串口日志]
G[公共 GND] --- T
G --- R1
G --- R2
这种方法相当于给单向 UART 加了一个逻辑监听点,可以看出程序卡在 CWJAP、CIPSTART 还是 CIPSEND。若需要同时观察 51→ESP8266 的 TX 方向,则应使用双通道逻辑分析仪;不要简单并联另一个 TXD。
1.4 例题4: 单片机接收数据
单片机接收的不是孤立的 LED10,而是 ESP8266 AT 固件通过 UART 上报的格式:
+IPD,5:LED10
│ │ │
│ │ └─ 5 字节 TCP 正文
│ └─── 正文长度为 5
└──────── 数据到达标志
串口每接收 1 字节置位一次 RI 并进入一次中断,状态机必须跨多次中断保存解析进度。只有进入正文状态后,才把字节交给 LED/蜂鸣器命令解析器。
二、TCP接收数据实验
2.1 实验思维导图
本实验使用六条 ASCII 控制命令:
| 服务器发送 | 单片机动作 | 电平含义 |
|---|---|---|
LED10 | LED1 点亮 | LED1=0,开发板 LED 为低电平有效 |
LED11 | LED1 熄灭 | LED1=1 |
LED20 | LED2 点亮 | LED2=0 |
LED21 | LED2 熄灭 | LED2=1 |
BIP0 | 蜂鸣器鸣响 | BEEP=0,按当前板卡低电平有效 |
BIP1 | 蜂鸣器停止 | BEEP=1 |
这里的 0/1 直接表示输出引脚电平,不是抽象的“开/关”。如果换成高电平有效的 LED 或蜂鸣器电路,动作会反过来,所以必须先看开发板原理图中的驱动方式。
2.2 服务器发指令
命令从电脑上的 TCP Server 发送,而不是从串口助手发送。完整链路为:
flowchart LR
S[网络调试助手<br/>TCP Server] -->|TCP 字节流| N[电脑网络协议栈]
N --> W[路由器/手机热点]
W -->|2.4 GHz Wi-Fi| E[ESP8266<br/>TCP Client]
E -->|UART: +IPD| M[51 单片机]
M --> G[GPIO]
G --> L[LED / 蜂鸣器]
服务器软件与电脑不是两个串联的硬件节点;软件通过电脑操作系统的 TCP/IP 协议栈发送数据。ESP8266 收到网络字节后,把它封装成 +IPD 文本再从 TXD 发给 51。
2.3 发送BIP零BIP1
发送 BIP0 后,单片机将 P1.6 置 0;若开发板蜂鸣器为低电平有效,就会鸣响。发送 BIP1 后 P1.6 置 1,蜂鸣器停止。
蜂鸣器通常不是直接由 MCU 引脚供给大电流,而是经三极管或 MOSFET 驱动:
MCU P1.6 ──限流/偏置──> 三极管或 MOSFET ──> 蜂鸣器 ──> 电源
│ │
控制信号 承担负载电流
若板上采用 PNP/P 沟道高边驱动、NPN/N 沟道低边驱动或增加反相器,控制极性可能不同,应以实际原理图为准。代码中的 BEEP=0 之所以表示响,是由当前板卡电路决定的。
2.4 任务分析
视频用 15 字节数组保存从 + 开始的整段文本,并按固定下标取出 L、灯号和电平。它只适用于单连接、长度恰为一位数、正文布局固定的示例。例如:
索引: 0 1 2 3 4 5 6 7 8 9 10 11
字节: + I P D , 5 : L E D 1 0
当正文长度变为两位数时,冒号和正文下标都会后移;多连接模式下还可能出现连接 ID;固定位置判断会立即失效。数组计数若先写入再取模,也容易在边界处写出数组。
完整源码不保存整个 +IPD 文本,而是按语法解析:
匹配“+IPD,” → 累积十进制长度 → 遇到“:” → 严格读取 length 个正文字节
ipd_length 决定正文何时结束,因此长度是一位、两位还是三位都不影响正文起点。当前工程使用单连接模式,解析格式为 +IPD,<len>:;若启用 AT+CIPMUX=1,还要增加 +IPD,<link_id>,<len>: 的分支。
static 局部变量不会“变成全局变量”。它的作用域仍局限在函数内部,但存储期贯穿程序整个运行过程,所以函数下次进入时能保留上次的值。状态机变量也可定义为文件作用域变量;应根据访问范围选择,而不是把二者混为一谈。
三、代码讲解
3.1 识别服务端发过来的数据
识别过程分两层:
- AT 层状态机确认
+IPD,解析正文长度; - 应用层状态机只在 TCP 正文内识别
LED10等命令。
这种分层可以防止启动日志或 AT 回复中偶然出现 LED10 时误控硬件。
3.1.1 用if判断
逐个 if 比较固定数组下标,只有在输入格式永远不变时才可靠。性能上的主要问题也不是“多比较几个字符会拖慢 51”,因为 9600 bit/s 下一个 UART 字节约需 1.04 ms,几个字符比较通常远小于这个时间。真正应该优先解决的是:边界检查、协议完整性、TCP 分段和误触发。
状态机每次只依据当前状态和当前字节做少量判断,既节省内存,又明确了合法顺序。无效字符会让它回到等待起点,而不会留下陈旧下标数据。
3.1.2 判断ipd
只检查 esp_recv_buf[0]=='+' 和 [3]=='D' 不能证明收到的是 +IPD,例如 +XXD 也会通过。正确门槛应依次匹配完整前缀 +、I、P、D、,,再解析至少一位十进制长度和冒号。
stateDiagram-v2
[*] --> Plus
Plus --> I: 收到 +
I --> P: 收到 I
P --> D: 收到 P
D --> Comma: 收到 D
Comma --> Length: 收到逗号
Length --> Length: 收到数字
Length --> Payload: 收到冒号且长度大于 0
Payload --> Plus: 已接收指定长度
源码还限制了长度累加范围,防止恶意或损坏文本让 16 位 uint 在乘 10 时溢出。
3.1.3 识别l
在固定示例 +IPD,5:LED10 中,L 的确位于下标 7,灯号和电平位于下标 10、11;但这些只是该示例的结果,不是协议规则。
应用层状态机直接在正文流中匹配:
L → E → D → 灯号(1/2) → 电平(0/1) → 执行
B → I → P → 电平(0/1) → 执行
这样既能处理两个命令被 ESP8266 合并为 +IPD,10:LED10LED11,也能处理一条命令跨两个 +IPD 上报的情况。原因是 TCP 只保证字节顺序,不保证服务器每次点击发送对应一个完整的接收块。
四、代码总结
main.h 中通过 sbit 把符号名映射到具体引脚,因此 LED1=0 最终就是让 P1.0 输出低电平,BEEP=1 则让 P1.6 输出高电平。sbit 只是寄存器位别名,不会自动判断外围电路是高电平有效还是低电平有效。
本节应掌握四个核心点:
- 用只读串口监听定位 ESP8266 启动与 AT 回复问题;
- 用响应和超时判断模块是否就绪,不把固定 1 秒当作绝对保证;
- 严格按
+IPD,<长度>:取出 TCP 正文,不依赖固定下标; - TCP 是字节流,应用命令解析必须允许合并与拆分,并且要有明确的合法格式。
若以后把这个控制链路暴露到不可信网络,还必须增加身份认证、命令白名单、重放防护和连接加密;本实验的明文 TCP 控制仅适合隔离的本地教学网络。