WIFI接收代码_2
061.8.6 WIFI接收代码(2)
/* ==================== main.c ==================== */
#include "reg52.h"
#include "delay.h"
#include "main.h"
#include "uart.h"
#define ESP_EVENT_OK 0x01
#define ESP_EVENT_PROMPT 0x02
#define IPD_WAIT_PLUS 0
#define IPD_WAIT_I 1
#define IPD_WAIT_P 2
#define IPD_WAIT_D 3
#define IPD_WAIT_COMMA 4
#define IPD_READ_LENGTH 5
#define IPD_READ_PAYLOAD 6
volatile uchar esp_events = 0;
volatile uchar ok_match_state = 0;
/* 以下状态只在串口中断中使用。 */
uchar ipd_state = IPD_WAIT_PLUS;
uint ipd_length = 0;
uint ipd_received = 0;
uchar app_state = 0;
uchar led_number = 0;
void APP_Restart_Matcher(uchar received_byte)
{
app_state = 0;
if (received_byte == 'L')
{
app_state = 1;
}
else if (received_byte == 'B')
{
app_state = 10;
}
}
/*
* 从 TCP 连续字节流中识别:LED10、LED11、LED20、LED21、BIP0、BIP1。
* 该状态机可跨越多个 +IPD 分段,也能连续处理多条紧邻的命令。
*/
void APP_Parse_Byte(uchar received_byte)
{
switch (app_state)
{
case 0:
APP_Restart_Matcher(received_byte);
break;
case 1: /* 已收到 L */
if (received_byte == 'E') app_state = 2;
else APP_Restart_Matcher(received_byte);
break;
case 2: /* 已收到 LE */
if (received_byte == 'D') app_state = 3;
else APP_Restart_Matcher(received_byte);
break;
case 3: /* 已收到 LED */
if (received_byte == '1' || received_byte == '2')
{
led_number = (uchar)(received_byte - '0');
app_state = 4;
}
else
{
APP_Restart_Matcher(received_byte);
}
break;
case 4: /* 等待 LED 的电平参数 */
if (received_byte == '0' || received_byte == '1')
{
if (led_number == 1) LED1 = (received_byte == '1');
if (led_number == 2) LED2 = (received_byte == '1');
}
APP_Restart_Matcher(received_byte);
break;
case 10: /* 已收到 B */
if (received_byte == 'I') app_state = 11;
else APP_Restart_Matcher(received_byte);
break;
case 11: /* 已收到 BI */
if (received_byte == 'P') app_state = 12;
else APP_Restart_Matcher(received_byte);
break;
case 12: /* 等待 BIP 的电平参数 */
if (received_byte == '0') BEEP = 0;
if (received_byte == '1') BEEP = 1;
APP_Restart_Matcher(received_byte);
break;
default:
app_state = 0;
break;
}
}
/* 返回 1 表示当前字节属于 TCP 正文,不能再拿去匹配 AT 回复。 */
bit IPD_Parse_Byte(uchar received_byte)
{
switch (ipd_state)
{
case IPD_WAIT_PLUS:
if (received_byte == '+') ipd_state = IPD_WAIT_I;
break;
case IPD_WAIT_I:
if (received_byte == 'I') ipd_state = IPD_WAIT_P;
else ipd_state = (received_byte == '+') ? IPD_WAIT_I : IPD_WAIT_PLUS;
break;
case IPD_WAIT_P:
if (received_byte == 'P') ipd_state = IPD_WAIT_D;
else ipd_state = (received_byte == '+') ? IPD_WAIT_I : IPD_WAIT_PLUS;
break;
case IPD_WAIT_D:
if (received_byte == 'D') ipd_state = IPD_WAIT_COMMA;
else ipd_state = (received_byte == '+') ? IPD_WAIT_I : IPD_WAIT_PLUS;
break;
case IPD_WAIT_COMMA:
if (received_byte == ',')
{
ipd_length = 0;
ipd_state = IPD_READ_LENGTH;
}
else
{
ipd_state = IPD_WAIT_PLUS;
}
break;
case IPD_READ_LENGTH:
if (received_byte >= '0' && received_byte <= '9')
{
/* 限制长度,避免十进制累加发生 16 位溢出。 */
if (ipd_length <= 6000)
{
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;
}
else
{
ipd_state = IPD_WAIT_PLUS;
}
break;
case IPD_READ_PAYLOAD:
APP_Parse_Byte(received_byte);
ipd_received++;
if (ipd_received >= ipd_length)
{
ipd_state = IPD_WAIT_PLUS;
}
return 1;
default:
ipd_state = IPD_WAIT_PLUS;
break;
}
return 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;
}
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();
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)
{
/* TCP 数据由串口中断中的 +IPD 状态机持续解析。 */
}
}
void UART_Routine(void) interrupt 4
{
uchar received_byte;
if (RI == 1)
{
RI = 0;
received_byte = SBUF;
if (!IPD_Parse_Byte(received_byte))
{
if (received_byte == '>')
{
esp_events |= ESP_EVENT_PROMPT;
}
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 = 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
一、WIFI接收代码
本节完成接收程序的烧录、排错和复测,并对 TCP 客户端/服务端、IP、端口及 OSI 分层进行复盘。页首代码使用长度驱动的 +IPD 状态机,因此不需要依靠 %14 把数组下标强行绕回,也不会把一位数长度写死在固定下标中。
1.1 编译并试运行代码
编译通过只能说明语法、类型和链接阶段没有发现错误,不能证明数组不会越界,也不能证明网络状态正确。第一次试运行应观察完整证据链:
编译无 error
↓
下载到正确的单片机和正确的 HEX
↓
ESP8266 上电并输出启动/AT 日志
↓
CWMODE 成功 → CWJAP 成功 → CIPSTART 成功
↓
服务器显示客户端在线
↓
服务器发 LED10,P1.0 变为低电平,LED1 点亮
当前完整源码在 UART_Init() 前保留 1 秒启动等待,同时用 AT 回复和超时判断后续步骤。固定延时只是给硬件上电留出基本时间,真正决定是否继续的是收到期望事件。
1.2 动手实践
烧录时按以下步骤操作,能够减少串口争用和带电插拔风险:
- 关闭占用下载串口的串口助手;
- 关闭开发板和 ESP8266 电源;
- 用跳帽隔离或断开 ESP8266 的 TXD、RXD,避免它与下载器共同驱动串口;
- 选择单片机开发板对应的 COM 口;
- 核对待下载 HEX 文件的目录和生成时间;
- 完成下载后再次断电,恢复 TX/RX 交叉连接;
- 确认裸 ESP8266 使用稳定 3.3 V,并让三方 GND 共地;
- 先打开电脑 TCP Server,再给系统上电。
只拔 ESP8266 的 GND、却保留 VCC 和 UART 信号线并不安全,因为模块可能经信号线的保护结构反向取电。需要改线时应整体断电。
1.3 重新配网
ESP8266 未上电、TX/RX 接反或波特率不一致时,单片机发送命令后收不到事件。页首程序每条命令有限次尝试,失败后停止初始化;不会无限高速刷屏。
判断模块是否上电不能只看串口有没有乱码:ESP8266 ROM 启动输出常见于约 74880 bit/s,而 AT 端口在本实验中已改为 9600 bit/s,同一串口窗口不一定能用一个波特率清晰看到两个阶段。可分别观察:
| 观察目标 | 波特率/方法 | 可说明什么 |
|---|---|---|
| ROM 启动信息 | 常见约 74880,依芯片启动输出而定 | 芯片发生了复位并开始启动 |
| AT 固件回复 | 本实验为 9600 8N1 | AT 端口和单片机参数一致 |
WIFI GOT IP | AT 配置波特率 | 已加入热点并取得地址 |
CONNECT | AT 配置波特率 | 已建立 TCP 连接 |
“数组长度为 15”本身不会导致配网不稳定;真正危险的是写数组前没有边界检查、把启动日志误当 OK,或反复重发联网指令打乱模块状态。
1.4 问题处理
视频发现计数器持续自增可能导致数组越界,于是加入:
esp_count = esp_count % 14;
这不是通用的数组边界解决方案:
%14的结果范围是0~13,不是0~14;- 长度 15 的数组合法下标确实是
0~14; - 如果先执行
esp_count++、再写数组、最后才取模,边界是否安全完全依赖前一时刻的值和语句顺序; - 环回会覆盖尚未处理的数据,并不能恢复
+IPD的帧边界; - 固定 15 字节也容纳不了较长正文。
真正使用数组时,应在写入之前检查:
if (write_index < sizeof(receive_buffer))
{
receive_buffer[write_index++] = received_byte;
}
else
{
/* 丢弃当前帧并复位解析状态,同时记录溢出错误。 */
write_index = 0;
}
页首代码更进一步:不缓存整段 +IPD 文本,只累积十进制长度,并按该长度逐字节处理正文,因而没有 15 字节接收数组的越界点。
1.5 编译运行新代码
烧录后先让服务器处于监听状态,再统一上电。用只接收的 USB 转 TTL 或逻辑分析仪观察 ESP8266 TXD,可以逐步核对:
AT+CWMODE=3 → OK
AT+CWJAP=... → WIFI CONNECTED / WIFI GOT IP / OK
AT+CIPSTART=... → CONNECT / OK
AT+CIPSEND=4 → >
qwer → SEND OK
服务器发 LED10 → +IPD,5:LED10
若某一步缺失,就停在该层检查,不要同时修改 Wi-Fi 密码、串口波特率、端口和解析代码。
1.6 动手实践
接线方向应在上电前逐根确认:
51 TXD ──电平转换──> ESP8266 RXD
51 RXD <──────────── ESP8266 TXD
51 GND ───────────── ESP8266 GND
稳定 3.3 V ────────── ESP8266 VCC、EN/CH_PD
若 ESP8266 通过带稳压器的转接板供电,5 V 只能接转接板明确标注的 5V/VIN,不能接裸模块 VCC。51 TXD 为 5 V 时,需要分压或电平转换保护 ESP8266 RXD。
代码仍无响应时,可用万用表先测静态 3.3 V,再用示波器观察上电瞬间是否下跌;Wi-Fi 发射造成的瞬时电流可能让容量不足的稳压器掉压并反复复位模块。
1.7 问题处理
带 static 的局部计数变量具有静态存储期,即使没有显式写 =0,C 语言也会在程序启动时把它初始化为 0。视频中需要补的“清零”不是为了修复随机初值,而是为了在识别到新帧起点后丢弃上一帧的计数状态:
上一段数据结束时 count=12
↓
收到新的 '+',若不重置 count
↓
新数据从 buffer[12] 之后开始,固定下标判断全部错位
因此,数组解析器至少要在确认帧起点时同时清缓冲区和计数器。但仅以字符 + 作为帧起点仍不充分,因为正文中也可能包含 +。页首状态机只在非正文状态匹配完整 +IPD,,一旦进入正文,就严格按长度消费字节,正文中的任何符号都不会重启 AT 帧解析。
1.8 动手实践
每次重新烧录前都要核对三项:
- 编译输出中的 HEX 路径与当前工程一致;
- 文件时间戳对应刚才的编译;
- 下载软件选中的是单片机下载口,不是只读监听用的 USB 转 TTL。
调试最好一次只改变一个变量,并记录“修改内容—预期结果—实际结果”。反复拔线、复位、改代码但不记录,会使偶然成功难以复现。
1.9 动手实践
服务器应发送完整的大写 ASCII 命令:LED10、LED11、LED20、LED21、BIP0、BIP1。这些命令区分大小写,不能把口语中的“一零”“二一”直接当成实际发送内容。
预期结果:
| 命令 | GPIO 结果 | 外设结果 |
|---|---|---|
LED10 | P1.0=0 | LED1 亮 |
LED11 | P1.0=1 | LED1 灭 |
LED20 | P1.1=0 | LED2 亮 |
LED21 | P1.1=1 | LED2 灭 |
BIP0 | P1.6=0 | 蜂鸣器响 |
BIP1 | P1.6=1 | 蜂鸣器停 |
TCP 是双向连接,但本实验中“控制命令”应从电脑 TCP Server 的发送区发出。串口助手只用于观察 ESP8266 的 UART 文本,不是控制命令的网络来源。
1.10 问题总结
接收程序最容易出错的地方包括:
- ESP8266 与 51 串口波特率不一致;
- 裸模块错误接入 5 V,或 3.3 V 电源电流能力不足;
- 51 TXD 的 5 V 电平未经转换进入 ESP8266 RXD;
- 未严格匹配
+IPD,和十进制长度; - 写数组之前没有边界检查;
- 把不相邻字符拼成
OK; - 将
CIPSEND的OK误当成正文就绪,而没有等待>; - 把
CIPSEND=4后的qwer\r\n当作 4 字节,实际多送了 2 字节; - 假定 TCP 一次发送永远对应一次完整接收;
- 字符大小写、灯号或高低电平含义写反。
“只判断几个关键字符”并不等于可以省略协议校验。恰当的优化是用状态机避免重复比较,同时保证顺序、长度和边界全部有效。
1.11 实验内容总结
TCP 发送实验解决的是:51 如何通过 AT 指令把 UART 数据送到网络。TCP 接收实验解决的是:网络数据如何由 ESP8266 以 +IPD 形式送回 UART,并由 51 安全地恢复应用命令。
发送:51 字节 → UART → ESP8266 → TCP → 电脑
接收:电脑字节 → TCP → ESP8266 → +IPD/UART → 51 → GPIO
二者共同构成双向控制闭环。掌握的重点是串口事件解析和网络字节流,而不是记住某个固定 IP、端口或数组下标。
1.12 复盘总结
蓝牙串口模块、ESP8266、4G 模块、语音播放模块都可能采用 UART 命令接口,但它们并不共享同一套 AT 指令。使用新模块时应逐项核对:
| 项目 | 为什么必须查手册 |
|---|---|
| 供电电压和峰值电流 | 防止损坏或掉压复位 |
| UART I/O 电平 | 判断是否需要电平转换 |
| 默认波特率与帧格式 | 保证 8N1、校验和流控一致 |
| 上电/复位引脚 | 保证进入正常工作模式 |
| 指令终止符 | 有的使用 CR-LF,有的使用固定二进制帧 |
| 返回值与异步事件 | 编写可靠状态机和超时处理 |
“都是串口模块”只说明物理接口相似,不代表供电、协议和命令可以互换。
1.13 任务信息资源导图组件库搜索
在组件库或电商平台搜索模块只能用于了解类别,不能替代数据手册。选型时应优先找到芯片/模块厂商提供的原理图、引脚定义、最大额定值、UART/AT 指令手册和固件版本说明。
可以把常见组件按链路位置分类:
flowchart TB
M[51 单片机] --> U[UART 接口模块]
U --> B[蓝牙模块]
U --> W[Wi-Fi / ESP8266]
U --> C[蜂窝 4G 模块]
U --> V[语音播放模块]
W --> IP[IP 网络]
C --> IP
其中 ESP8266 和 4G 模块通常把复杂网络协议封装在模块内部,MCU 通过串口命令驱动;语音模块可能完全不涉及 IP,只是复用了 UART 作为控制入口。
1.14 TCP知识
TCP 连接有两个端点,每个端点由“IP 地址+端口”标识:
客户端端点 服务端端点
192.168.31.150:临时端口 ───────> 192.168.31.118:5132
ESP8266 主动连接 电脑程序监听
“客户端”和“服务端”描述的是连接建立与服务角色,不等于设备强弱:客户端主动发起连接,服务端在已知端口等待连接。连接建立后,双方都能调用发送和接收,实现全双工通信。
ESP8266 的局域网 IP 常由 DHCP 动态分配,重新连接后可能从 .151 变成 .150。电脑服务器 IP 也可能变化,所以每次实验前要核对当前网卡地址。是否同一子网由 IP 和子网掩码共同决定;只有典型 /24 网络才能简化成“前三段相同”。
1.15 回到思维导图
IP 和端口解决不同层次的寻址问题:
| 标识 | 所在层次 | 回答的问题 |
|---|---|---|
| MAC 地址 | 数据链路层 | 同一链路上把帧交给哪个网卡 |
| IP 地址 | 网络层 | 跨网络把数据包送到哪台主机/接口 |
| TCP 端口 | 传输层 | 到达主机后交给哪个应用进程 |
端口不是固定四位数。TCP/UDP 端口字段为 16 位,数值范围是 0~65535;端口 0 通常不作为普通监听端口。教学实验可选择未被占用的 1024~65535 端口,例如 5132。客户端连接时还会由系统分配一个临时源端口。
1.16 OSI七层模型
OSI 模型用于分层理解网络功能;实际互联网工程更常使用 TCP/IP 分层,但两者可以大致对应:
| OSI 层 | 本实验中的例子 | 主要作用 |
|---|---|---|
| 7 应用层 | ESP-AT 文本、LED10 自定义协议,HTTP/SSH/FTP | 定义应用数据含义 |
| 6 表示层 | 文本编码、加密、序列化 | 表示格式转换 |
| 5 会话层 | 会话建立与管理概念 | 管理通信会话 |
| 4 传输层 | TCP | 端口、可靠有序字节流、拥塞与流量控制 |
| 3 网络层 | IPv4 | IP 寻址和路由 |
| 2 数据链路层 | IEEE 802.11 MAC、以太网 | 局域链路成帧、MAC 寻址、差错检测 |
| 1 物理层 | 2.4 GHz 无线电波、电缆电信号 | 传输原始比特 |
ESP8266 芯片本身同时实现了物理层、数据链路层以及部分更高层协议功能,不能简单地把“一块网卡/一颗 ESP8266”只等同于链路层。IEEE 802.11 是本实验相关的 Wi-Fi 标准系列,不是 802.6。
TCP 建立连接常用“三次握手”表示:
sequenceDiagram
participant C as ESP8266 Client
participant S as PC TCP Server
C->>S: SYN,seq=x
S->>C: SYN+ACK,seq=y,ack=x+1
C->>S: ACK,ack=y+1
Note over C,S: 连接建立,可双向传输字节流
三次的目的不仅是“打招呼”,还要确认双方收发路径可用并同步初始序列号。正常主动关闭通常用“四次挥手”描述,是因为 TCP 两个方向可以分别关闭:
sequenceDiagram
participant A as 主动关闭方
participant B as 被动关闭方
A->>B: FIN
B->>A: ACK
B->>A: FIN(B 也发送完毕时)
A->>B: ACK
实际报文可能因 ACK 与 FIN 合并、同时关闭、RST 异常终止等情况呈现不同组合;“三次/四次”是典型正常流程,不应误解为每次 TCP 数据发送也重复握手。
1.17 课程总结
完成这组实验后,应能独立回答:
- ESP8266 为什么必须安全使用 3.3 V 供电和 I/O 电平;
- UART 的 TX/RX 为什么交叉、监听器为什么只能并联 RX;
- AT 指令为什么以 CR-LF 结尾,
CIPSEND正文为什么不能随意追加换行; +IPD,<长度>:中每一部分表示什么;- 为什么固定下标和取模不能代替协议状态机与边界检查;
- TCP 客户端、服务端、IP、端口和字节流分别是什么;
- 本实验各部分在 OSI/TCP-IP 分层中的位置。
后续若更换 Wi-Fi、蓝牙、4G 或其他串口模块,通用方法仍然是:先读电气与协议手册,验证最小硬件链路,再建立带有状态、超时、错误分支和边界检查的串口驱动。