TCPWIFI发送代码

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 0 Likes: 0 Comments: 0

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

五个参数依次表示:

参数值含义
baudrate9600每秒 9600 bit
databits88 个数据位
stopbits11 个停止位
parity0无校验
flow control0不使用 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 实际发出的字符
\""
\rCR,即 0x0D
\nLF,即 0x0A
\\一个反斜杠

字符串最外侧的一对双引号负责界定 C 字符串,不会被发出;内部的 \" 才会产生 AT 指令需要的英文双引号。

1.4.3.3 接收解析数据

本节代码虽然开启了串口接收中断,但中断服务函数只是把每个新字节覆盖到单个变量 recv 中,主程序并没有解析它。因此当前程序严格说只是“接收了最后一个字节”,还没有实现完整的 AT 回复解析。

固定延时版本采用:

动作示例等待时间实际应等待的关键回复
设置模式500 msOK
加入 Wi-Fi6000 msWIFI GOT IP 和 OK
连接服务器6000 msCONNECT 和 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 共用串口:

  1. 关闭单片机板和 ESP8266 电源;
  2. 断开 ESP8266 的 TXD、RXD,或使用板载跳帽隔离;
  3. 给开发板上电并下载程序;
  4. 下载完成后再次断电;
  5. 按 TX→RX、RX→TX、GND 共地、3.3 V 安全供电重新连接;
  6. 先打开电脑 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 峰值电流拉低、复位电路、蜂鸣器默认电平和是否存在信号线反向供电。最重要的是先断电再改线,并用万用表或示波器确认电源变化。