WIFI接收代码_2

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-10-08 01:26:37 Views: 1 Likes: 0 Comments: 0

061.8.6 WIFI接收代码(2)

/* ==================== main.c:ESP8266 回复与 TCP 正文双层解析 ====================
 * 处理顺序:UART 中断先用 +IPD 状态机隔离 TCP 正文;仅非正文数据再用于匹配 AT 回复 OK、>。
 * 应用协议:TCP 正文中的 LED10/LED11/LED20/LED21/BIP0/BIP1 分别控制两路低电平有效 LED 或蜂鸣器。
 */
#include "reg52.h"  /* 引入 ES、RI、SBUF 等 8051 特殊功能寄存器定义。 */
#include "delay.h"  /* 引入 Delay_xms(),供 AT 指令等待超时使用。 */
#include "main.h"   /* 引入 uchar、uint 和 LED/BEEP 引脚别名。 */
#include "uart.h"   /* 引入 UART 初始化与字符串发送接口。 */

#define ESP_EVENT_OK      0x01 /* AT 同步回复中已识别到 OK 的事件位。 */
#define ESP_EVENT_PROMPT  0x02 /* CIPSEND 已给出 > 提示符的事件位。 */

#define IPD_WAIT_PLUS     0 /* 尚未开始,等待 +IPD 的 +。 */
#define IPD_WAIT_I        1 /* 已收到 +,等待 I。 */
#define IPD_WAIT_P        2 /* 已收到 +I,等待 P。 */
#define IPD_WAIT_D        3 /* 已收到 +IP,等待 D。 */
#define IPD_WAIT_COMMA    4 /* 已收到 +IPD,等待分隔逗号。 */
#define IPD_READ_LENGTH   5 /* 累积冒号前的十进制正文长度。 */
#define IPD_READ_PAYLOAD  6 /* 正在读取长度指定的 TCP 正文字节。 */

volatile uchar esp_events = 0;     /* 中断置位、前台读取的事件集合;volatile 防止读取被优化为旧值。 */
volatile uchar ok_match_state = 0; /* OK 匹配进度:0 等 O,1 表示上一个字节是 O、等紧邻 K。 */

/* 以下状态只在串口中断中使用。 */
uchar ipd_state = IPD_WAIT_PLUS; /* +IPD 协议解析状态;仅由 UART 中断访问。 */
uint ipd_length = 0;             /* 当前 +IPD 声明的正文总长度。 */
uint ipd_received = 0;           /* 本帧已经交给应用解析器的正文数。 */
uchar app_state = 0;             /* LED/BIP 命令匹配状态。 */
uchar led_number = 0;            /* 已识别的灯号 1 或 2,等待后续电平字符。 */

void APP_Restart_Matcher(uchar received_byte) /* 复位命令匹配器,并把当前字节尝试作为下一条命令的开头。 */
{                                             /* 函数体开始。 */
    app_state = 0;                            /* 默认回到“等待 L 或 B”的初始状态。 */
    if (received_byte == 'L')                 /* L 可以是 LED 命令的起始字节。 */
    {                                         /* 识别 LED 起始符。 */
        app_state = 1;                        /* 下一字节应为 E。 */
    }                                         /* L 分支结束。 */
    else if (received_byte == 'B')            /* B 可以是 BIP 命令的起始字节。 */
    {                                         /* 识别 BIP 起始符。 */
        app_state = 10;                       /* 下一字节应为 I。 */
    }                                         /* B 分支结束;其他字符保持状态 0。 */
}

/*
 * 从 TCP 连续字节流中识别:LED10、LED11、LED20、LED21、BIP0、BIP1。
 * 该状态机可跨越多个 +IPD 分段,也能连续处理多条紧邻的命令。
 */
void APP_Parse_Byte(uchar received_byte) /* 每收到一个 TCP 正文字节调用一次,允许命令在多帧间拆分或连续。 */
{                                       /* 应用层解析函数体开始。 */
    switch (app_state)                 /* 依据此前已匹配的前缀,决定本字节的合法含义。 */
    {                                  /* 各状态分支开始。 */
        case 0:                        /* 尚未匹配到命令前缀。 */
            APP_Restart_Matcher(received_byte); /* 当前字节若为 L/B,就成为新命令起点。 */
            break;                     /* 结束初始状态处理。 */

        case 1:                         /* 已收到 L */
            if (received_byte == 'E') app_state = 2; /* LE 匹配成功,下一字节期待 D。 */
            else APP_Restart_Matcher(received_byte); /* 前缀失败,但当前字节仍可能是新起点。 */
            break;

        case 2:                         /* 已收到 LE */
            if (received_byte == 'D') app_state = 3; /* LED 前缀完整,下一字节应为灯号。 */
            else APP_Restart_Matcher(received_byte); /* LE 后非 D,重新寻找起点。 */
            break;

        case 3:                         /* 已收到 LED */
            if (received_byte == '1' || received_byte == '2') /* 本教学协议只定义 LED1、LED2。 */
            {                                                   /* 灯号有效。 */
                led_number = (uchar)(received_byte - '0');     /* ASCII 字符转为数值 1 或 2。 */
                app_state = 4;                                  /* 下一字节将给出输出电平。 */
            }                                                   /* 灯号处理结束。 */
            else                                                /* 灯号无效。 */
            {                                                   /* 重新同步。 */
                APP_Restart_Matcher(received_byte);             /* 当前字符仍尝试作为新命令起点。 */
            }                                                   /* 无效灯号处理结束。 */
            break;

        case 4:                         /* 等待 LED 的电平参数 */
            if (received_byte == '0' || received_byte == '1') /* 电平字符仅允许 0、1。 */
            {                                                   /* 收到有效电平。 */
                if (led_number == 1) LED1 = (received_byte == '1'); /* LED1 低有效;命令 0 写 0 点亮,1 写 1 熄灭。 */
                if (led_number == 2) LED2 = (received_byte == '1'); /* 对 LED2 执行相同的低有效电平控制。 */
            }                                                   /* 有效电平处理结束。 */
            APP_Restart_Matcher(received_byte);                 /* 一条 LED 命令已结束,当前字节可兼作下一命令起点。 */
            break;

        case 10:                        /* 已收到 B,等待 I。 */
            if (received_byte == 'I') app_state = 11; /* BI 匹配成功,继续等待 P。 */
            else APP_Restart_Matcher(received_byte);  /* 前缀失败后重新同步。 */
            break;                      /* 结束 BIP 的第一字符状态。 */

        case 11:                        /* 已收到 BI,等待 P。 */
            if (received_byte == 'P') app_state = 12; /* BIP 前缀完整,下一字节应是电平。 */
            else APP_Restart_Matcher(received_byte);  /* 非 P 时重新同步。 */
            break;                      /* 结束 BI 状态。 */

        case 12:                        /* 已收到 BIP,等待蜂鸣器电平。 */
            if (received_byte == '0') BEEP = 0; /* 板载蜂鸣器低有效,写 0 使其鸣响。 */
            if (received_byte == '1') BEEP = 1; /* 写 1 撤销有效控制电平,蜂鸣器停止。 */
            APP_Restart_Matcher(received_byte); /* 本条命令结束,尝试从当前字节重启匹配。 */
            break;                      /* 结束 BIP 命令。 */

        default:                        /* 防御性分支:状态值异常时不执行任何硬件动作。 */
            app_state = 0;              /* 恢复到安全的初始匹配状态。 */
            break;                      /* 结束异常状态处理。 */
    }                                  /* 应用命令状态机结束。 */
}                                      /* 单字节应用解析结束。 */

/* 返回 1 表示当前字节属于 TCP 正文,不能再拿去匹配 AT 回复。 */
bit IPD_Parse_Byte(uchar received_byte) /* 解析 +IPD,<长度>:;返回 1 时该字节属于正文,不能再参与 AT 回复匹配。 */
{                                      /* +IPD 解析函数体开始。 */
    switch (ipd_state)                /* 根据当前前缀进度解释串口字节。 */
    {                                 /* +IPD 各状态开始。 */
        case IPD_WAIT_PLUS:           /* 空闲状态,寻找一帧上报的 +。 */
            if (received_byte == '+') ipd_state = IPD_WAIT_I; /* +IPD 前缀开始,下一字节期待 I。 */
            break;                    /* 非 + 数据仍留在空闲状态。 */

        case IPD_WAIT_I:              /* 已收到 +,等待 I。 */
            if (received_byte == 'I') ipd_state = IPD_WAIT_P; /* +I 已匹配。 */
            else ipd_state = (received_byte == '+') ? IPD_WAIT_I : IPD_WAIT_PLUS; /* 连续 + 可作为新起点,其他字符丢弃前缀。 */
            break;                    /* I 状态处理结束。 */

        case IPD_WAIT_P:              /* 已收到 +I,等待 P。 */
            if (received_byte == 'P') ipd_state = IPD_WAIT_D; /* +IP 已匹配。 */
            else ipd_state = (received_byte == '+') ? IPD_WAIT_I : IPD_WAIT_PLUS; /* 失败时按当前字节决定能否重启。 */
            break;                    /* P 状态处理结束。 */

        case IPD_WAIT_D:              /* 已收到 +IP,等待 D。 */
            if (received_byte == 'D') ipd_state = IPD_WAIT_COMMA; /* +IPD 已匹配,下一字节应为逗号。 */
            else ipd_state = (received_byte == '+') ? IPD_WAIT_I : IPD_WAIT_PLUS; /* 前缀失败则重新同步。 */
            break;                    /* D 状态处理结束。 */

        case IPD_WAIT_COMMA:          /* 已收到 +IPD,等待长度字段前的逗号。 */
            if (received_byte == ',') /* 逗号确认单连接格式的长度字段即将开始。 */
            {                         /* 分隔符正确。 */
                ipd_length = 0;       /* 新帧开始,清除上一帧的正文长度。 */
                ipd_state = IPD_READ_LENGTH; /* 转入十进制长度累积状态。 */
            }                         /* 正确逗号处理结束。 */
            else                      /* 不是逗号,前缀不是合法 +IPD 格式。 */
            {                         /* 格式错误处理开始。 */
                ipd_state = IPD_WAIT_PLUS; /* 丢弃无效前缀,重新寻找下一帧。 */
            }                         /* 格式错误处理结束。 */
            break;                    /* 结束逗号状态。 */

        case IPD_READ_LENGTH:         /* 冒号前必须读取至少一位十进制长度。 */
            if (received_byte >= '0' && received_byte <= '9') /* 当前字节是有效十进制数字。 */
            {
                /* 限制长度,避免十进制累加发生 16 位溢出。 */
                if (ipd_length <= 6000) /* 先限制累积值,避免 16 位 uint 乘 10 溢出。 */
                {                       /* 长度仍在允许范围。 */
                    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;                   /* 后续恰好读取 ipd_length 个字节。 */
            }                                                   /* 正文开始准备结束。 */
            else                                                /* 非数字且不是合法冒号,说明帧格式损坏。 */
            {                                                   /* 无效长度处理。 */
                ipd_state = IPD_WAIT_PLUS;                      /* 丢弃该帧并重新同步。 */
            }                                                   /* 无效长度处理结束。 */
            break;                                              /* 结束长度解析状态。 */

        case IPD_READ_PAYLOAD:      /* 当前字节已被协议声明为 TCP 正文。 */
            APP_Parse_Byte(received_byte); /* 只将正文交给 LED/BIP 应用命令状态机。 */
            ipd_received++;         /* 记录已消费一个正文字节。 */
            if (ipd_received >= ipd_length) /* 已按声明长度收完本帧。 */
            {                       /* 本帧结束。 */
                ipd_state = IPD_WAIT_PLUS; /* 返回空闲状态,等待后续 +IPD 帧。 */
            }                       /* 帧结束判断完成。 */
            return 1;               /* 通知调用者:此字节不可再当作 AT 回复中的 OK 或 >。 */

        default:                    /* 状态值意外损坏时采用安全恢复。 */
            ipd_state = IPD_WAIT_PLUS; /* 清除未知状态,重新寻找 + 前缀。 */
            break;                  /* 结束异常状态。 */
    }                               /* +IPD 状态机结束。 */
    return 0;                       /* 当前字节不是正文,因此仍可用于 AT 事件解析。 */
}                                   /* +IPD 单字节解析结束。 */

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);                               /* 空等约 1 ms;UART 中断仍能运行。 */
        timeout_ms--;                               /* 消耗一次轮询时间。 */
    }                                               /* 时间已用尽。 */
    return 0;                                       /* 未收到期望事件,报告超时。 */
}                                                   /* 等待函数结束。 */

bit ESP_Send_Command(const char *command, /* 本次 AT 命令文本。 */
                     uchar expected_event, /* 完成依据:OK 或 > 等事件位。 */
                     uint timeout_ms,       /* 单次尝试等待上限。 */
                     uchar max_attempts)    /* 最大尝试次数,防止永久重发。 */
{                                           /* 发送并等待函数体开始。 */
    uchar attempt;                          /* 记录当前尝试序号。 */
    for (attempt = 0; attempt < max_attempts; attempt++) /* 每一轮完整地清状态、发送、等待。 */
    {                                                      /* 单次尝试开始。 */
        ES = 0;                                            /* 临时关闭串口中断,避免共享状态清零与 ISR 竞争。 */
        esp_events = 0;                                    /* 去掉上一条指令遗留事件。 */
        ok_match_state = 0;                                /* 重新从等待 O 状态匹配 OK。 */
        ES = 1;                                            /* 恢复接收中断。 */
        UART_Send_Str(command);                            /* 将 AT 命令送入 ESP8266。 */
        if (ESP_Wait_Event(expected_event, timeout_ms)) return 1; /* 收到期望回复则立即成功返回。 */
    }                                                      /* 本次已超时,可能进入下一次重试。 */
    return 0;                                              /* 所有尝试失败。 */
}                                                          /* 命令函数结束。 */

void main(void)                         /* 依序联网、建 TCP 连接,然后由 ISR 持续接收控制命令。 */
{                                      /* 主函数体开始。 */
    bit init_success = 1;              /* 前序流程是否仍成功;失败时阻断后续网络动作。 */

    Delay_xms(1000);                   /* 规避 ESP8266 上电文本;可靠工程应再检测 ready/AT。 */
    UART_Init();                       /* 初始化 9600 bit/s UART 并打开接收中断。 */

    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)                          /* 网络数据到达时由 UART ISR 异步解析,前台无需轮询正文。 */
    {                                  /* 无限循环体开始。 */
        /* TCP 数据由串口中断中的 +IPD 状态机持续解析。 */
    }                                  /* 当前无其他前台任务。 */
}                                      /* 正常运行不返回。 */

void UART_Routine(void) interrupt 4 /* 8051 UART 中断服务程序,每个接收字节进入一次。 */
{                                   /* 中断函数体开始。 */
    uchar received_byte;            /* 保存本次从 SBUF 读出的字节。 */

    if (RI == 1)                    /* 只在接收完成时处理;TI 由发送函数轮询。 */
    {                               /* 接收分支开始。 */
        RI = 0;                     /* 清接收完成标志,准备下一次 UART 接收。 */
        received_byte = SBUF;       /* 读取 ESP8266 当前发来的一个字节。 */

        if (!IPD_Parse_Byte(received_byte)) /* 非 TCP 正文才可能是 AT 回复或提示符。 */
        {                                    /* AT 事件解析分支开始。 */
            if (received_byte == '>')        /* CIPSEND 的发送就绪提示符。 */
            {                                /* 收到提示符。 */
                esp_events |= ESP_EVENT_PROMPT; /* 置位提示事件,供前台等待函数发现。 */
            }                                /* 提示符处理结束。 */

            if (ok_match_state == 0)          /* 初始状态:等待 OK 的 O。 */
            {                                  /* 初始匹配分支。 */
                if (received_byte == 'O') ok_match_state = 1; /* O 到达后等待紧邻的 K。 */
            }                                  /* 初始匹配结束。 */
            else if (received_byte == 'K')     /* 前字节是 O 且当前字节为 K,匹配成功。 */
            {                                  /* OK 处理开始。 */
                esp_events |= ESP_EVENT_OK;    /* 发布 OK 成功事件。 */
                ok_match_state = 0;            /* 重置匹配器。 */
            }                                  /* OK 处理结束。 */
            else if (received_byte != 'O')     /* O 后的无关字节使相邻匹配失败。 */
            {                                  /* 失败处理开始。 */
                ok_match_state = 0;            /* 回到等待 O 的初始状态。 */
            }                                  /* 失败处理结束;若又收到 O 则保持状态 1,支持 OOK 重叠。 */
        }                                      /* AT 事件解析结束。 */
    }                                          /* 接收分支结束。 */
}                                              /* 中断返回前台。 */

/* ==================== 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_
#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:软件忙等延时实现 ====================
 * 用途:在教学示例中粗略等待 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 防重复包含区。 */

一、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 动手实践

烧录时按以下步骤操作,能够减少串口争用和带电插拔风险:

  1. 关闭占用下载串口的串口助手;
  2. 关闭开发板和 ESP8266 电源;
  3. 用跳帽隔离或断开 ESP8266 的 TXD、RXD,避免它与下载器共同驱动串口;
  4. 选择单片机开发板对应的 COM 口;
  5. 核对待下载 HEX 文件的目录和生成时间;
  6. 完成下载后再次断电,恢复 TX/RX 交叉连接;
  7. 确认裸 ESP8266 使用稳定 3.3 V,并让三方 GND 共地;
  8. 先打开电脑 TCP Server,再给系统上电。

只拔 ESP8266 的 GND、却保留 VCC 和 UART 信号线并不安全,因为模块可能经信号线的保护结构反向取电。需要改线时应整体断电。

1.3 重新配网

ESP8266 未上电、TX/RX 接反或波特率不一致时,单片机发送命令后收不到事件。页首程序每条命令有限次尝试,失败后停止初始化;不会无限高速刷屏。

判断模块是否上电不能只看串口有没有乱码:ESP8266 ROM 启动输出常见于约 74880 bit/s,而 AT 端口在本实验中已改为 9600 bit/s,同一串口窗口不一定能用一个波特率清晰看到两个阶段。可分别观察:

观察目标波特率/方法可说明什么
ROM 启动信息常见约 74880,依芯片启动输出而定芯片发生了复位并开始启动
AT 固件回复本实验为 9600 8N1AT 端口和单片机参数一致
WIFI GOT IPAT 配置波特率已加入热点并取得地址
CONNECTAT 配置波特率已建立 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 结果外设结果
LED10P1.0=0LED1 亮
LED11P1.0=1LED1 灭
LED20P1.1=0LED2 亮
LED21P1.1=1LED2 灭
BIP0P1.6=0蜂鸣器响
BIP1P1.6=1蜂鸣器停

TCP 是双向连接,但本实验中“控制命令”应从电脑 TCP Server 的发送区发出。串口助手只用于观察 ESP8266 的 UART 文本,不是控制命令的网络来源。

1.10 问题总结

接收程序最容易出错的地方包括:

  1. ESP8266 与 51 串口波特率不一致;
  2. 裸模块错误接入 5 V,或 3.3 V 电源电流能力不足;
  3. 51 TXD 的 5 V 电平未经转换进入 ESP8266 RXD;
  4. 未严格匹配 +IPD, 和十进制长度;
  5. 写数组之前没有边界检查;
  6. 把不相邻字符拼成 OK;
  7. 将 CIPSEND 的 OK 误当成正文就绪,而没有等待 >;
  8. 把 CIPSEND=4 后的 qwer\r\n 当作 4 字节,实际多送了 2 字节;
  9. 假定 TCP 一次发送永远对应一次完整接收;
  10. 字符大小写、灯号或高低电平含义写反。

“只判断几个关键字符”并不等于可以省略协议校验。恰当的优化是用状态机避免重复比较,同时保证顺序、长度和边界全部有效。

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 网络层IPv4IP 寻址和路由
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 或其他串口模块,通用方法仍然是:先读电气与协议手册,验证最小硬件链路,再建立带有状态、超时、错误分支和边界检查的串口驱动。