TCPWIFI发送代码
058.8.3 TCP WIFI发送代码
/* ==================== main.c ====================
* 目的:51 单片机以 UART 向 ESP8266 依次发送 AT 指令,完成入网、建 TCP 连接和发送 qwer。
* 前提:ESP8266 已配置为 9600 bit/s,51 晶振为 11.0592 MHz,且 TX/RX 已交叉连接、电源共地。
* 限制:本节用固定延时等待模块回复;recv 只保存最后一个接收字节,尚未实现回复解析或重试。
*/
#include "reg52.h" /* 引入 SBUF、RI、TI、SCON 等 8051 特殊功能寄存器声明。 */
#include "delay.h" /* 引入 Delay_xms() 声明,用于在两条 AT 指令之间留出处理时间。 */
#include "main.h" /* 引入 uchar、uint 等公共类型及板载引脚别名。 */
#include "uart.h" /* 引入 UART 初始化和字符串发送接口。 */
/* 中断服务程序与主程序都会访问该变量,因此声明为 volatile。 */
volatile uchar recv; /* 中断写、前台可能读的单字节接收变量;volatile 防止读取被不当缓存。 */
void main(void) /* C51 用户程序入口;void 表示不接收参数也不返回数值。 */
{ /* 主函数体开始。 */
/* ESP8266 上电时会输出启动信息,先等待电源和串口电平稳定。 */
Delay_xms(1000); /* 等待 ESP8266 上电输出结束;这不是可靠的“模块已就绪”判断。 */
UART_Init(); /* 配置 51 UART 为 9600 bit/s,同时允许串口接收中断。 */
/* 请把 SSID、密码、服务器 IP 和端口替换为自己的实验参数。 */
UART_Send_Str("AT+CWMODE=3\r\n"); /* 发送设置 Wi-Fi 为 AP+STA 模式的 AT 指令;\r\n 是命令结束符。 */
Delay_xms(500); /* 粗略等待模块返回 OK;网络模块忙时该时间可能不足。 */
UART_Send_Str("AT+CWJAP=\"YOUR_SSID\",\"YOUR_PASSWORD\"\r\n"); /* 用自己的热点名和密码替换占位符后,命令会请求加入该热点。 */
Delay_xms(6000); /* 给扫描、认证、DHCP 获取地址预留时间;实际应等 WIFI GOT IP/OK。 */
UART_Send_Str("AT+CIPSTART=\"TCP\",\"192.168.1.100\",5132\r\n"); /* 请求连接指定局域网服务器;IP 和端口必须与电脑监听端一致。 */
Delay_xms(6000); /* 粗略等待 TCP CONNECT/OK;服务器未启动或被防火墙拦截会失败。 */
UART_Send_Str("AT+CIPSEND=4\r\n"); /* 告诉 ESP8266 下一段原始数据恰好是 4 个字节。 */
Delay_xms(500); /* 等待模块返回 > 提示符;正确做法是检测该提示符后再发送正文。 */
/* CIPSEND 已声明 4 字节,这里只能发送 qwer,不能再附加 CR-LF。 */
UART_Send_Str("qwer"); /* 正好发出 q、w、e、r 四字节;不能附加 \r\n,否则长度会与 CIPSEND=4 不符。 */
while (1) /* 发送一次后保持程序运行;本节未安排新的前台业务。 */
{ /* 无限循环体开始。 */
Delay_xms(1000); /* 空等约 1 秒,避免主函数返回;UART 接收仍可通过中断发生。 */
} /* 当前一轮空等结束。 */
} /* 正常运行不会到达此处。 */
/* 串口中断服务函数:接收 ESP8266 返回的每一个字节。 */
void UART_Routine(void) interrupt 4 /* 指定 8051 串口中断向量;UART 收到字节后由硬件转入此函数。 */
{ /* 中断服务程序体开始。 */
if (RI == 1) /* 只处理接收完成事件;TI 发送完成事件由发送函数轮询处理。 */
{ /* 接收分支开始。 */
RI = 0; /* 软件清除接收完成标志,为下一个字节腾出事件标志。 */
recv = SBUF; /* 读出 UART 数据缓冲寄存器;新数据会覆盖上一个 recv 值。 */
} /* 接收分支结束。 */
} /* 中断返回被打断的位置。 */
/* ==================== 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 防重复包含区。 */
一、TCP实验第二阶段
第一阶段由电脑串口助手手动发送 AT 指令;第二阶段把同一串 AT 指令写进 51 单片机程序,由单片机通过 UART 自动控制 ESP8266。两阶段的网络原理没有改变,改变的是 AT 指令的发送者。
flowchart LR
A[第一阶段<br/>电脑串口助手] -->|UART/AT 指令| B[ESP8266]
C[第二阶段<br/>51 单片机程序] -->|UART/AT 指令| B
B -->|2.4 GHz Wi-Fi| D[路由器]
D -->|TCP| E[电脑 TCP Server]
1.1 任务信息资源导图
第二阶段开始前,应确保第一阶段的最小链路已经成功:ESP8266 能回应 AT,能加入 2.4 GHz 热点,能连接电脑的 TCP Server,并能双向传输数据。否则,加入单片机后会同时面对供电、串口、AT 指令和网络四类问题,很难判断故障位置。
推荐按以下顺序操作:
第一阶段通信成功
↓
把 ESP8266 的 AT 串口改为 9600 bit/s
↓
断电后改接 51 单片机,检查供电和电平
↓
编写并下载自动发送 AT 指令的程序
↓
先打开电脑 TCP Server,再复位单片机
↓
观察串口回复和服务器收到的 qwer
1.2 实验常⻅问题总结
定位问题时,不要一次改动多处。可以把整个实验分成四层:
| 层级 | 先检查什么 | 典型现象 |
|---|---|---|
| 电源/电平 | ESP8266 是否使用稳定 3.3 V,GND 是否共地,51 TX 是否降压 | 反复重启、乱码、模块异常发热 |
| UART | 两端是否同为 9600 8N1,TX/RX 是否交叉 | AT 无 OK、返回乱码 |
| AT 状态机 | 是否等到上一条指令成功后再发下一条 | busy、ERROR、连接过程被打断 |
| 网络 | SSID、密码、服务器 IP、端口、防火墙是否正确 | 能入网但 CIPSTART 失败 |
第一阶段与第二阶段可以使用同一份问题清单,但第二阶段还要额外检查单片机晶振、定时器初值、共享串口和代码发送长度。
1.3 课程相关所有链接汇总
遇到实验失败,可先查课程资料中 ESP8266/CH340 的常见问题,再回到模块实际返回的信息。最有价值的诊断证据不是“没有现象”,而是 ESP8266 返回的最后一行,例如 ERROR、FAIL、busy p...、CLOSED 或 WIFI DISCONNECT。
调试时建议保存以下信息:
- ESP8266 AT 固件版本,可用
AT+GMR查询; - 当前 UART 配置,可用支持该命令的固件执行
AT+UART_DEF?; AT+CWJAP的错误码或状态文本;- 电脑的实际局域网 IP、子网掩码和监听端口;
- 单片机晶振频率及串口初始化代码。
1.4 TCP实验第二阶段
这一阶段的核心是让单片机依次完成:设置 Wi-Fi 模式、加入热点、连接服务器、声明发送长度、发送数据。当前程序使用固定延时,便于理解流程,但它没有判断 OK、>、CONNECT 和 SEND OK,网络稍慢时就可能失败。更可靠的状态机将在后续优化中完善。
1.4.1 波特率修改原因
课程工程使用传统 8051 串口、11.0592 MHz 晶振、定时器 1 方式 2,TH1=TL1=0xFD 时得到 9600 bit/s。并不是“所有 51 单片机都只支持 9600”,而是当前硬件与初始化方案选择了 9600;其他 8051 芯片若具有独立波特率发生器或采用不同晶振,也可能支持更高波特率。
在 ESP8266 仍通过 USB 转 TTL 连接电脑时,先以它当前的波特率打开串口并发送 AT。收到 OK 后发送:
AT+UART_DEF=9600,8,1,0,0
五个参数依次表示:
| 参数 | 值 | 含义 |
|---|---|---|
| baudrate | 9600 | 每秒 9600 bit |
| databits | 8 | 8 个数据位 |
| stopbits | 1 | 1 个停止位 |
| parity | 0 | 无校验 |
| flow control | 0 | 不使用 RTS/CTS 硬件流控 |
UART_DEF 会把默认串口配置保存到非易失存储区,重新上电后仍然有效。指令返回 OK 后,模块已经切换到 9600;此时继续用 115200 发送 AT 通常不会得到可读回复,需要把电脑串口改成 9600 8N1 再验证。
不同 ESP8266 AT 固件可能只支持 AT+UART_CUR、AT+UART_DEF 中的一部分,或采用不同命令名称。发送前可用 AT+GMR 记录固件版本,并查对应版本手册。如果改错波特率,可扫描常用波特率或重新烧录已知配置的 AT 固件,而不是盲目反复断电。
1.4.2 接线方式
单片机控制 ESP8266 时,UART 必须交叉连接:
数据方向
51 P3.1 / TXD ───────────> ESP8266 RXD
51 P3.0 / RXD <─────────── ESP8266 TXD
51 GND ──────────── ESP8266 GND
稳定 3.3 V ──────────── ESP8266 VCC、EN/CH_PD
电路构成与作用如下:
flowchart LR
P[5 V 输入] --> R[3.3 V 稳压器<br/>足够的瞬态电流能力]
R --> C[去耦电容<br/>靠近模块]
C --> V[ESP8266 VCC/EN]
M[51 单片机 TXD<br/>通常接近 5 V] --> L[分压器或电平转换]
L --> X[ESP8266 RXD<br/>3.3 V 逻辑]
Y[ESP8266 TXD<br/>3.3 V 逻辑] --> N[51 单片机 RXD]
G[公共 GND] --- R
G --- M
G --- V
需要特别区分“板上的 5 V 电源口”和“ESP8266 的 3.3 V VCC”:
- 裸 ESP-01/ESP-01S 的 VCC、EN/CH_PD 和 I/O 均不能直接接 5 V;
- 只有当转接板明确带有 3.3 V 稳压器,并把某个接口明确标为
5V/VIN时,5 V 才能接到这个转接板输入; - 看到导线标签写着
VCC 3.3V,却准备插到开发板5V针脚时,必须先查原理图或用万用表确认中间确实有稳压器,不能凭接口外观判断; - 传统 5 V 51 单片机的 TXD 高电平可能接近 5 V,应经分压或专用电平转换后再进入 ESP8266 RXD;ESP8266 的 3.3 V TXD 通常可被 5 V 51 的 RXD 识别,但仍应核对具体芯片输入高电平门限。
若开发板下载器与 ESP8266 共用 P3.0/P3.1,烧录单片机程序时两路发送端可能同时驱动同一根线,造成冲突。安全做法是关闭相关电源后完整断开 ESP8266 的 TXD、RXD 或使用跳帽/开关隔离,再下载程序;不要只拔 GND 而保留 VCC 和信号线,因为电流可能经 I/O 保护二极管反向供电。改线必须断电,避免热插拔造成瞬态干扰。
1.4.3 代码编写
程序先初始化 UART,然后通过 UART_Send_Str() 把每一条 AT 指令逐字节送入 SBUF。AT 指令阶段采用 CR-LF 结尾,网络数据阶段则严格按 CIPSEND 声明的字节数发送。
main()
├─ 延时,避开 ESP8266 上电启动信息
├─ UART_Init()
├─ CWMODE + CR-LF
├─ 等待
├─ CWJAP + CR-LF
├─ 等待
├─ CIPSTART + CR-LF
├─ 等待
├─ CIPSEND=4 + CR-LF
├─ 等待“>”的时间
└─ 发送 4 字节 qwer,不带 CR-LF
1.4.3.1 回顾之前实验
第一阶段中,串口助手帮忙完成了两件事:把键盘输入转换为字节流,以及在 AT 指令末尾自动追加回车换行。单片机程序必须自己完成这两件事。
UART_Send_Str() 的工作过程是:读取当前指针指向的字符,调用 UART_Send_Byte() 发送,再让指针加 1,直到遇到 C 字符串结束标志 \0。� 只存在于单片机内存中,用于标记字符串结尾,不会被发送给 ESP8266。
串口发送一个字节时:
CPU 写 SBUF → 串口硬件按起始位、8 数据位、停止位移出 → TI 置 1
→ 程序发现 TI=1 → 软件清 TI → 可以发送下一字节
1.4.3.2 发送数据
AT 指令必须以 CR-LF 结束,在 C 字符串中写作 \r\n:
UART_Send_Str("AT+CWMODE=3\r\n");
这里 \r 是回车,十六进制为 0x0D;\n 是换行,十六进制为 0x0A。它们是两个独立字节。
当 AT 指令本身需要双引号时,要用反斜杠转义:
UART_Send_Str("AT+CWJAP=\"YOUR_SSID\",\"YOUR_PASSWORD\"\r\n");
从 C 语言和实际发送结果两个角度看:
| C 源码写法 | UART 实际发出的字符 |
|---|---|
\" | " |
\r | CR,即 0x0D |
\n | LF,即 0x0A |
\\ | 一个反斜杠 |
字符串最外侧的一对双引号负责界定 C 字符串,不会被发出;内部的 \" 才会产生 AT 指令需要的英文双引号。
1.4.3.3 接收解析数据
本节代码虽然开启了串口接收中断,但中断服务函数只是把每个新字节覆盖到单个变量 recv 中,主程序并没有解析它。因此当前程序严格说只是“接收了最后一个字节”,还没有实现完整的 AT 回复解析。
固定延时版本采用:
| 动作 | 示例等待时间 | 实际应等待的关键回复 |
|---|---|---|
| 设置模式 | 500 ms | OK |
| 加入 Wi-Fi | 6000 ms | WIFI GOT IP 和 OK |
| 连接服务器 | 6000 ms | CONNECT 和 OK |
| 声明发送长度 | 500 ms | 提示符 > |
| 发送正文 | 无固定延时要求 | SEND OK |
课程为了演示把 Wi-Fi 和服务器等待时间增加到 6 秒,使实验能够运行,但固定时间并不能保证所有网络环境都成功。正确的优化方向是把接收字节存入缓冲区,识别上述关键回复,并加入超时与重试。
还有一个关键长度问题:
UART_Send_Str("AT+CIPSEND=4\r\n");
UART_Send_Str("qwer"); /* 正确:恰好 4 字节 */
如果写成 UART_Send_Str("qwer\r\n"),程序实际会送出 6 字节。ESP8266 收到前 4 字节后已完成本次网络发送,剩余 CR-LF 可能滞留在 AT 命令输入流中,干扰下一条指令。
1.4.3.4 完善代码
烧录程序前先关闭占用下载串口的串口助手,确认下载软件选择的是单片机开发板对应的 COM 口,而不是 USB 转 TTL 对应的 COM 口。若开发板下载器和 ESP8266 共用串口:
- 关闭单片机板和 ESP8266 电源;
- 断开 ESP8266 的 TXD、RXD,或使用板载跳帽隔离;
- 给开发板上电并下载程序;
- 下载完成后再次断电;
- 按 TX→RX、RX→TX、GND 共地、3.3 V 安全供电重新连接;
- 先打开电脑 TCP Server,再给系统上电或复位单片机。
只断开 ESP8266 的 VCC/GND、却仍保留 UART 信号线,可能发生信号线反向供电;在带电状态下反复插拔 VCC/GND 还会制造电源毛刺,应避免这种操作。
1.4.3.5 实验展示
电脑端先建立与代码一致的 TCP Server,例如 192.168.1.100:5132。单片机复位后,预期顺序是:
ESP8266 加入热点
↓
ESP8266 主动连接 TCP Server,服务器显示客户端在线
↓
ESP8266 收到 CIPSEND=4 和 qwer
↓
服务器接收区显示 qwer
调试时可在 ESP8266 TXD 上并联一个只接收的 USB 转 TTL RXD,并与系统共地,用串口助手观察模块回复;不要把两个 TXD 直接并联。若只能看到单片机发出的 AT 指令,却看不到模块返回内容,应检查监听位置和接线方向。
视频中插拔 ESP8266 电源时蜂鸣器偶尔鸣响,更可能与供电跌落、地弹、电源回灌或 MCU 复位瞬态有关,不能直接归因于“静电”。蜂鸣器连接在 P1.6,UART 使用 P3.0/P3.1,两者并不直接共用引脚。应检查 3.3 V 稳压器容量、5 V 电源是否被 Wi-Fi 峰值电流拉低、复位电路、蜂鸣器默认电平和是否存在信号线反向供电。最重要的是先断电再改线,并用万用表或示波器确认电源变化。