一、前言
在物联网开发中,51单片机凭借其低成本、易上手的优势,依然是许多初学者的首选平台。而ESP8266 WiFi模块则为51单片机插上了"无线的翅膀",让原本只能本地运行的单片机具备了联网通信的能力。
本文将基于STC89C52单片机与ESP8266-01S模块,手把手带你实现:
-
ESP8266 TCP发送:单片机通过WiFi向电脑上的TCP服务器发送数据
-
ESP8266 TCP接收:单片机接收来自TCP服务器的数据,并解析执行LED控制等操作
整个项目的硬件框图如下:

二、硬件准备与接线
2.1 所需硬件
| 硬件 | 说明 |
|---|---|
| 51单片机开发板(STC89C52) | 主控芯片 |
| ESP8266-01S WiFi模块 | 乐鑫/安信可出品 |
| CH340 USB转TTL模块 | 用于调试和烧录 |
| 杜邦线若干 | 连接各模块 |
2.2 ESP8266-01S引脚说明
ESP8266-01S模块共有8个引脚,我们主要使用其中4个:
| 引脚 | 功能 |
|---|---|
| VCC | 3.3V供电(实际可接5V,模块内部有稳压) |
| GND | 接地 |
| TX | 串口发送(连接单片机RX) |
| RX | 串口接收(连接单片机TX) |
2.3 接线方式(最终实验接线)
ESP8266与51单片机接线(交叉连接) :
| ESP8266引脚 | 连接 | 单片机引脚 |
|---|---|---|
| 3V3/VCC | → | 5V(开发板5V输出) |
| GND | → | GND |
| TX | → | P3.0(RX) |
| RX | → | P3.1(TX) |
⚠️ 重要 :TX与RX必须交叉连接------ESP8266的TX接单片机的RX,ESP8266的RX接单片机的TX。

2.4 ESP8266与CH340调试模块接线
在调试阶段,我们需要用CH340模块连接ESP8266,通过串口助手发送AT指令:
| ESP8266引脚 | 连接 | CH340引脚 |
|---|---|---|
| 3V3 | → | 5V |
| GND | → | GND |
| TX | → | RX |
| RX | → | TX |
⚠️ 注意 :连接CH340时,需要去掉CH340模块上的跳帽。如果不拔掉跳帽,可能会影响串口通信。


三、ESP8266核心知识储备
3.1 三种WiFi模式
ESP8266支持三种WiFi工作模式:
| 模式 | 指令 | 说明 |
|---|---|---|
| Station模式 | AT+CWMODE=1 |
模块连接其他WiFi热点(作为客户端) |
| AP模式 | AT+CWMODE=2 |
模块自己发射WiFi热点(作为服务器) |
| AP+Station混合模式 | AT+CWMODE=3 |
两种模式共存 |
类比理解:
-
Station模式 = 手机连接家里WiFi
-
AP模式 = 手机开启热点
-
混合模式 = 手机同时连接WiFi又开热点
我们的代码中使用的是模式3(混合模式),既能让ESP8266连接路由器上网,又能作为热点被其他设备发现。
3.2 透传模式 vs 非透传模式
| 模式 | 指令 | 特点 |
|---|---|---|
| 非透传模式 | AT+CIPMODE=0 |
每次发送数据前必须先发AT+CIPSEND=长度,再发数据 |
| 透传模式 | AT+CIPMODE=1 |
只需发一次AT+CIPSEND,之后所有内容都当作数据发送 |
透传模式的优势:不需要每次计算数据长度,数据可以连续发送,就像一根"无线串口线"。
透传模式的条件:
-
必须是单连接模式 (
AT+CIPMUX=0) -
仅支持TCP单连接或UDP固定通信
我们的代码用的是非透传模式 ,所以每次发送数据前都要先发AT+CIPSEND=4指定长度,再发qwer。
3.3 必须修改波特率!
这是一个新手最容易忽略的问题:
-
ESP8266出厂默认波特率 :115200
-
51单片机串口最高稳定波特率 :9600(11.0592MHz晶振下)
不匹配的后果:单片机发送的指令ESP8266无法识别,通信完全失败。
解决方法 :先用CH340模块连接ESP8266,在串口助手中以115200波特率发送以下指令:
AT+UART_DEF=9600,8,1,0,0

修改成功后,ESP8266的波特率会永久保存 为9600。之后把串口助手波特率也改成9600,发送AT测试,收到OK即表示修改成功。
⚠️ 修改完波特率后,需要给ESP8266重新上电才能生效。
四、代码逐行解析
4.1 main.h ------ 引脚与类型定义
cpp
#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
这里定义了:
-
uchar和uint类型别名,方便代码书写 -
LED1~LED4分别对应P1.0~P1.3
-
蜂鸣器(BEEP)对应P1.6
-
继电器(JDQ1)对应P2.0
4.2 delay.c ------ 毫秒级延时
cpp
void Delay_xms(uint xms) //@11.0592MHz
{
uchar data i, j;
while(xms)
{
_nop_();
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
xms--;
}
}
这是一个软件延时 函数,通过CPU空转实现毫秒级延时。在11.0592MHz晶振下,i=2, j=199的双层循环约等于1ms。
💡 为什么用软件延时而不是定时器? 在AT指令配置阶段,对延时精度要求不高(500ms、1000ms),软件延时足够且代码简单。但在实际项目中,建议使用定时器中断实现更精确的延时。
4.3 uart.c ------ 串口通信底层
串口初始化(最关键!)
cpp
void UART_Init(void)
{
SCON = 0x50; // 串口配成工作方式1
PCON &= 0x7F; // 波特率不加倍
TMOD &= 0x0f;
TMOD |= 0x20; // 定时器1,模式二,自动重装初值
TH1 = 0xFD;
TL1 = 0xFD; // @11.0592MHz,9600波特率
TR1 = 1; // 定时器开始运行
ES = 1; // 串口中断打开
EA=1; // 总中断打开
}
逐个寄存器解析:
| 寄存器 | 设置值 | 含义 |
|---|---|---|
SCON=0x50 |
0101 0000 | 工作方式1(8位UART),允许接收 |
PCON&=0x7F |
清零最高位 | 波特率不加倍(SMOD=0) |
TMOD&=0x0F |
保留低4位 | 不清除定时器0的设置 |
| `TMOD | =0x20` | 高4位=0010 |
TH1=0xFD |
253 | 9600波特率初值(11.0592MHz) |
波特率计算公式(方式1):
cpp
波特率 = (2^SMOD / 32) × (定时器溢出率)
定时器溢出率 = 晶振频率 / (12 × (256 - TH1))
代入:9600 = (1/32) × (11059200 / (12 × (256 - 253))) = 9600 ✓
串口发送
cpp
void UART_Send_Byte(uchar send_byte)
{
SBUF = send_byte;
while(!TI); // 等待发送完成
TI=0; // 清除发送中断标志
}
void UART_Send_Str(uchar *send_str)
{
while(*send_str != '\0')
{
UART_Send_Byte(*send_str++);
}
}
UART_Send_Str通过指针遍历字符串,逐个字节发送,直到遇到字符串结束符\0。
💡 为什么AT指令结尾都有
\r\n? ESP8266的AT指令解析器以\r\n作为指令结束标志。如果没有这两个字符,ESP8266会认为指令不完整而不执行。
4.4 main.c ------ 主控逻辑
全局变量
cpp
char recv; // 当前接收的字符
char esp_recv_buf[15] = {0}; // 接收缓冲区
char esp_flag = 1; // 指令执行状态标志(1=等待OK,0=已收到OK)
esp_flag的工作机制:
-
初始值为1
-
发送一条AT指令后,单片机等待ESP8266返回
OK -
串口中断中收到
OK后,将esp_flag置0 -
do-while循环检测到esp_flag=0,跳出循环,执行下一条指令
main函数流程
cpp
void main()
{
Delay_xms(1000); // 上电延时,滤掉ESP8266上电TX引脚的数据
UART_Init();
// 1. 设置WiFi模式为AP+STA混合模式
do {
UART_Send_Str("AT+CWMODE=3\r\n");
Delay_xms(500);
} while(esp_flag);
esp_flag = 1;
// 2. 连接路由器(WiFi必须是2.4G,不支持5G)[reference:25]
do {
UART_Send_Str("AT+CWJAP=\"i am god\",\"god05430543\"\r\n");
Delay_xms(1000);
} while(esp_flag);
esp_flag = 1;
// 3. 作为TCP客户端连接服务器
do {
UART_Send_Str("AT+CIPSTART=\"TCP\",\"192.168.31.118\",5132\r\n");
Delay_xms(1000);
} while(esp_flag);
esp_flag = 1;
// 4. 发送数据(非透传模式,先指定长度)
do {
UART_Send_Str("AT+CIPSEND=4\r\n");
Delay_xms(500);
} while(esp_flag);
UART_Send_Str("qwer\r\n"); // 实际数据
while(1) {
Delay_xms(1000);
}
}
关键点解析:
① 上电延时 :Delay_xms(1000)是为了等待ESP8266上电初始化完成。ESP8266上电后TX引脚会输出一些启动信息,如果立即初始化串口,这些垃圾数据可能被误读。
② 双引号转义 :在C语言字符串中,双引号需要用反斜杠转义:\"i am god\"。否则编译器会认为字符串提前结束。
③ 网段匹配 :AT+CIPSTART中的服务器IP(192.168.31.118)必须和ESP8266获取的STA IP在同一网段。
④ AT+CIPSEND=4 :告诉ESP8266接下来要发送4个字节的数据。ESP8266收到后会返回>提示符,等待输入数据。
4.5 串口中断 ------ 数据接收与解析(代码升级版)
这是TCP接收功能的核心,也是代码中最复杂的部分:
cpp
void UART_Routine(void) interrupt 4
{
static char recv_count = 0;
if(1 == RI)
{
RI = 0;
recv = SBUF;
// 检测到'O'或'+',认为是新数据的开始
if(recv == 'O' || recv == '+')
{
memset(esp_recv_buf,'\0',sizeof(esp_recv_buf));
recv_count = 0;
esp_recv_buf[recv_count] = recv;
}
else
{
recv_count++;
esp_recv_buf[recv_count] = recv;
}
// 判断是否收到"OK"
if(esp_recv_buf[0] == 'O' && esp_recv_buf[1] == 'K')
{
esp_flag = 0;
memset(esp_recv_buf,'\0',sizeof(esp_recv_buf));
}
// 解析TCP接收数据(格式:+IPD,长度:数据)
if(esp_recv_buf[0] == '+' && esp_recv_buf[3] == 'D')
{
// 控制LED1
if(esp_recv_buf[7] == 'L'&& esp_recv_buf[10] == '1'&& esp_recv_buf[11] == '0')
LED1 = 0;
if(esp_recv_buf[7] == 'L'&& esp_recv_buf[10] == '1'&& esp_recv_buf[11] == '1')
LED1 = 1;
// 控制LED2
if(esp_recv_buf[7] == 'L'&& esp_recv_buf[10] == '2'&& esp_recv_buf[11] == '0')
LED2 = 0;
if(esp_recv_buf[7] == 'L'&& esp_recv_buf[10] == '2'&& esp_recv_buf[11] == '1')
LED2 = 1;
// 控制蜂鸣器
if(esp_recv_buf[7] == 'B'&& esp_recv_buf[10] == 'P'&& esp_recv_buf[11] == '0')
BEEP = 0;
if(esp_recv_buf[7] == 'B'&& esp_recv_buf[10] == 'P'&& esp_recv_buf[11] == '1')
BEEP = 1;
}
recv_count = recv_count % 14; // 防止数组越界
}
}
ESP8266接收数据格式解析
当ESP8266从TCP服务器收到数据时,会通过串口发送如下格式的数据:
cpp
+IPD,<长度>:<数据>
实际例子:
cpp
+IPD,3:556 // 收到3个字节:556[reference:32]
+IPD,5:hello // 收到5个字节:hello[reference:33]
我们的数据协议:
cpp
+IPD,4:L10 // 控制LED1灭
+IPD,4:L11 // 控制LED1亮
+IPD,4:L20 // 控制LED2灭
+IPD,4:L21 // 控制LED2亮
+IPD,4:BP0 // 蜂鸣器关
+IPD,4:BP1 // 蜂鸣器开
解析逻辑:
-
esp_recv_buf[0] == '+':检测到数据包开始 -
esp_recv_buf[3] == 'D':确认是+IPD格式(+是第0位,I是第1位,P是第2位,D是第3位) -
esp_recv_buf[7]:判断控制对象(L=LED,B=蜂鸣器) -
esp_recv_buf[10]和esp_recv_buf[11]:判断控制值(10=关,11=开)
五、TCP通信完整流程
5.1 TCP发送流程(代码已实现)
cpp
单片机 ESP8266 电脑TCP服务器
| | |
|-- AT+CWMODE=3 -------------->| |
|<------------- OK ------------| |
| | |
|-- AT+CWJAP="SSID","PWD" ---->| |
|<------------- OK ------------| |
| |---- 连接WiFi热点 ------------->|
| |<------- 分配IP ----------------|
| | |
|-- AT+CIPSTART="TCP",IP,Port->| |
|<------------- OK ------------| |
| |---- 三次握手建立TCP连接 ------>|
| |<------- 连接成功 --------------|
| | |
|-- AT+CIPSEND=4 ------------>| |
|<------------- ">" -----------| |
| | |
|-- "qwer\r\n" -------------->| |
|<---------- SEND OK ----------| |
| |------- 转发"qwer" ----------->|
| | |
5.2 TCP接收流程(代码已实现)
cpp
电脑TCP服务器 ESP8266 单片机
| | |
|------- 发送"L10" ----------->| |
| |-- +IPD,4:L10 ---------------->|
| | |-- 解析指令
| | |-- LED1 = 0
| | |
|------- 发送"L11" ----------->| |
| |-- +IPD,4:L11 ---------------->|
| | |-- 解析指令
| | |-- LED1 = 1
六、TCP服务器配置(电脑端)
6.1 使用网络调试助手
-
打开网络调试助手,选择TCP Server模式
-
设置端口号:可自定义(如5132、8080等)
-
点击"开始监听" ,按钮变为红色表示成功
6.2 确保同一网段
关键步骤:
-
先用串口助手向ESP8266发送
AT+CIFSR查询IP地址 -
返回示例:
cpp
+CIFSR:APIP,"192.168.4.1"
+CIFSR:STAIP,"192.168.31.151" ← 这是ESP8266的STA IP
-
电脑网络调试助手中选择的服务器IP,前三个数字必须与STA IP相同(即同一网段)
-
如果STA IP是
192.168.31.151,服务器IP应为192.168.31.xxx -
如果STA IP是
192.168.43.xxx,服务器IP应为192.168.43.xxx
-
💡 什么是网段? IP地址
192.168.31.151中,前三个数字192.168.31就是网段。同一网段的设备才能直接通信。
6.3 常见网络问题
| 问题 | 解决方法 |
|---|---|
| WiFi必须是2.4G | ESP8266不支持5G WiFi |
| WiFi名字不能有中文 | 改为英文或数字 |
| 电脑防火墙阻挡 | 关闭电脑网络防火墙 |
| 苹果手机开热点 | 建议用安卓手机开2.4G热点 |
| 校园网不行 | 校园网通常有隔离,建议用手机热点 |
| 服务器点不开 | 检查端口是否被占用,换个端口试试 |
七、实验常见问题与避坑指南
7.1 ESP8266模块发热发烫
正常现象!ESP8266在工作时会有一定发热,只要不是异常高温都属正常。
7.2 代码烧录失败(ESP8266连接单片机时)
现象:连接ESP8266后,单片机代码烧录失败。
原因:ESP8266的串口占用了单片机的TX/RX引脚,影响了烧录通信。
解决方法 :烧录代码时先拔掉ESP8266模块,烧录完成后再插上。
7.3 AT指令返回ERROR
检查清单:
-
波特率是否匹配?(单片机9600 vs ESP8266是否也是9600)
-
AT指令结尾是否有
\r\n? -
指令中是否有多余空格?
-
WiFi密码是否正确?
-
ESP8266是否已经连接上WiFi?(用
AT+CIFSR查询)
7.4 收不到+IPD数据
可能原因:
-
TCP连接未建立成功(检查
AT+CIPSTART返回) -
服务器没有发送数据
-
串口中断未正确配置
-
esp_recv_buf数组越界导致数据丢失
7.5 数组越界问题(代码优化点)
原代码中esp_recv_buf大小为15,但接收数据时如果没有边界检查,可能导致数组越界。优化代码中加入了:
cpp
recv_count = recv_count % 14; // 确保recv_count始终在0~13之间
7.6 大小写识别问题
ESP8266返回的OK是大写,ok不会被识别。代码中判断条件esp_recv_buf[0] == 'O' && esp_recv_buf[1] == 'K'严格匹配大写。
八、代码优化建议
8.1 增加超时机制
当前代码使用do-while无限等待OK,如果ESP8266一直不返回,程序会卡死。建议增加超时计数:
cpp
uint timeout = 0;
do {
UART_Send_Str("AT+CWMODE=3\r\n");
Delay_xms(500);
timeout++;
if(timeout > 20) break; // 10秒超时
} while(esp_flag);
8.2 使用状态机管理AT指令
当前代码顺序执行,可读性一般。可以使用状态机+switch-case管理:
cpp
enum {STATE_INIT, STATE_SET_MODE, STATE_CONNECT_WIFI, STATE_CONNECT_TCP, STATE_SEND_DATA};
uint8_t state = STATE_INIT;
while(1) {
switch(state) {
case STATE_INIT:
UART_Send_Str("AT+CWMODE=3\r\n");
state = STATE_SET_MODE;
break;
// ...
}
}
8.3 使用环形缓冲区
当前代码使用静态数组存储接收数据,对于大量数据可能不够用。可以使用环形缓冲区:
cpp
#define BUFFER_SIZE 64
typedef struct {
uint8_t data[BUFFER_SIZE];
volatile uint8_t head;
volatile uint8_t tail;
} RingBuffer;
8.4 增加AT指令响应完整性判断
当前只判断OK,实际ESP8266可能返回ERROR、FAIL等。建议增加更多判断:
cpp
if(strstr(esp_recv_buf, "OK") != NULL) {
esp_flag = 0;
} else if(strstr(esp_recv_buf, "ERROR") != NULL) {
// 处理错误
}
九、总结
通过本文的讲解,你应该已经掌握了:
-
ESP8266的三种工作模式:Station、AP、混合模式
-
透传与非透传的区别:透传无需每次指定长度,非透传需要
-
波特率匹配的重要性:51单片机用9600,ESP8266需从115200改到9600
-
TCP发送流程:设置模式→连接WiFi→建立TCP连接→发送数据
-
TCP接收与解析 :识别
+IPD格式,提取控制指令 -
常见踩坑点 :WiFi必须2.4G、同一网段、AT指令结尾加
\r\n
整个实验的核心逻辑是:51单片机通过串口发送AT指令控制ESP8266,ESP8266负责WiFi通信。理解了这一点,你就可以在这个基础上扩展更多功能------比如温湿度上传、远程开关控制、智能家居等。
希望这篇教程能帮助你顺利完成ESP8266的TCP通信实验!如有问题,欢迎交流讨论。