WIFI接收代码_1

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-08-29 08:09:36 Views: 0 Likes: 0 Comments: 0

060.8.5 WIFI接收代码(1)

/* ==================== 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

一、课程回顾

上一节已经让 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 指令同步回复,如 OKERROR
  • 异步网络事件,如 WIFI DISCONNECTCLOSED
  • 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 通常可行,但必须满足:

  1. 三方 GND 共地,否则接收端没有共同电压参考;
  2. USB 转 TTL 的 TXD 不得接入,否则会和 51 TXD 争用 ESP8266 RXD;
  3. 不要把两个模块各自的 3.3 V/5 V 电源输出直接并联;
  4. USB 转 TTL 的 RXD 应能识别 3.3 V 高电平;
  5. 若要看 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 加了一个逻辑监听点,可以看出程序卡在 CWJAPCIPSTART 还是 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 控制命令:

服务器发送单片机动作电平含义
LED10LED1 点亮LED1=0,开发板 LED 为低电平有效
LED11LED1 熄灭LED1=1
LED20LED2 点亮LED2=0
LED21LED2 熄灭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 识别服务端发过来的数据

识别过程分两层:

  1. AT 层状态机确认 +IPD,解析正文长度;
  2. 应用层状态机只在 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 也会通过。正确门槛应依次匹配完整前缀 +IPD,,再解析至少一位十进制长度和冒号。

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 控制仅适合隔离的本地教学网络。