跳转至

STM32 使用 AT 指令驱动 ESP01S 进行 TCP 通信

项目说明

最近将记录一些使用 ESP01S 遇到的问题和解决思路。所有文章中所提及的工具和代码均整理免费分享。

手机上可以点击下方的右上角复制,然后软件就可以自动打开了。在获取的网盘链接中找到编号 B007 的项目资料。

我用夸克网盘给你分享了「32项目集」,点击链接或复制整段内容,打开「夸克APP」即可获取。 /~58423bH72w~:/ 链接:https://pan.quark.cn/s/0d502be9f165
网页上可以点击链接:https://pan.quark.cn/s/0d502be9f165

有问题可以展开联系我

微信号:Qhua_Li7

上篇笔记讲了 AT 指令下的 TCP 通信方式,本节内容说一下怎么使用 STM32 驱动 ESP01S 进行 TCP 通信。

使用 STA 的工作模式,串口连接 STM32,代码在文件夹Stm32Code中,和笔记序号一致。

使用的固件是 (1471)ESP8266-AT-1M.bin。

设计思路

ESP01S 的 AT 指令响应有几个特点:

特点 带来的问题 解决思路
不定长 不知道要接收多少字节才算"收完" 不固定读取长度,而是匹配特定结束标志(如 OK、ERROR)
非即时 发送后可能要等几百毫秒才有响应 设置超时机制,轮询等待
不连续 一个响应可能分多次中断到达 用缓冲区把分散的数据攒起来,主循环统一处理

所以整体架构采用 "中断接收 + 环形缓冲 + 主循环匹配":

USART 中断 ──→ RingBuffer ──→ 主循环轮询提取 ──→ 字符串匹配
    ↑                                              ↓
    └──────────── 解耦收发速度 ──────────────────────┘

中断里只做一件事:收到字节立刻丢进 RingBuffer,越快越好 主循环里做判断:从 RingBuffer 取出数据,拼成完整响应,匹配结果

关于此设备驱动,最重要的是怎么处理它返回值的不定长以及不是及时响应,一个命令可能会隔段时间才会回应,还不连续。所以,我们使用一个环形缓冲区把接收到的内容先暂存,然后再处理。

环形缓冲区这里使用了 RT-Thread 中的环形缓冲区思想,但是只保留了一些常用函数。环形缓冲区可以当作被两个索引管理的数组。我们只谈使用。在串口接收中断中,我们将接收到的数值放到环形缓冲区中,在主任务中进行提取处理。

说明一下,ESP01S接的是串口3,调试打印接的是串口1

接收端设计

串口中断只负责收数据,不做任何解析:

1
2
3
4
5
6
7
8
9
void USART3_IRQHandler(void)
{
    if (USART_GetITStatus(USART3, USART_IT_RXNE) != RESET)
    {
        uint8_t data = USART_ReceiveData(USART3); 
        ringbuffer_putchar(&esp01s_device.rx_rb, data); // 将数据放入环形缓冲区 esp01s_device.rx_rb 中
        USART_ClearITPendingBit(USART3, USART_IT_RXNE);
    }
}

为什么中断里不能多做事情?

因为中断要尽可能短。如果在这里加 delay 或字符串匹配,会阻塞其他中断,甚至丢数据。RingBuffer 的作用就是 让中断快速退出,把数据暂存起来,主循环慢慢处理 。

esp01s_device 是管理 ESP01S 的结构体变量,rx_rb 就是里面定义的环形缓冲结构体。RingBuffer 可以简单理解为"被两个索引管理的数组",这里不展开讲解实现,只关注使用。

发送指令与等待响应

这是驱动的核心。esp_at_cmd 负责发送一条指令并等待期望的响应:

int esp_at_cmd(struct esp01s *esp, const char *cmd, const char *expect, uint16_t timeout_ms)
{
    // 清空缓存(很重要)
    int ret = 0;
    ringbuffer_reset(&esp->rx_rb);
    esp_send(cmd);
    ret = esp_wait_response(esp, expect, timeout_ms);
    esp->resp_len = 0;
    return ret;
}

为什么要 ringbuffer_reset?

假设上条指令超时了,RingBuffer 里可能还残留着未读完的数据。如果不清空,下次发送新指令后,这些旧数据会被误认为是新响应,导致匹配错误。

esp_wait_response 负责轮询提取数据并匹配:

int esp_wait_response(struct esp01s *dev, const char *expect, uint32_t timeout)
{  
    uint16_t delay_ms_counter = 0; 
    dev->resp_len = 0;
    while (delay_ms_counter < timeout)
    {
        uint8_t ch;
        if (ringbuffer_getchar(&dev->rx_rb, &ch) == 1)
        {
            // 保存数据
            if (dev->resp_len < ESP_RESP_BUF_SIZE - 1)
            {
                dev->resp_buf[dev->resp_len++] = ch;
                dev->resp_buf[dev->resp_len] = '\0';
            }
            // 判断成功
            if (strstr((char *)dev->resp_buf, expect))
            {
                return 0;
            }
            // 判断错误
            if (strstr((char *)dev->resp_buf, "ERROR"))
            {
                return -1;
            }
        } else {
            // 没有数据,等待一段时间
            delay_ms_counter++;
            esp_delay_ms(1);
        } 
    }
    return -2; // 超时
}

发送数据完成后,就等待响应,响应就是把串口中断中环形缓冲区得到的值,一个个取出来存到数组中,然后再进行判断,比如判断是否接收到响应字符,是否接收到错误字符。

三个返回值的意义:

返回值 含义 典型场景
0 成功匹配到期望响应 收到 OK、CONNECT 等
-1 指令执行失败 收到 ERROR
-2 超时未收到期望响应 网络不通、ESP01S 未就绪

为什么用 strstr 而不是比较固定长度?

因为响应是不定长的。比如 AT\r\n 的响应可能是 \r\nOK\r\n,也可能是 \r\nbusy p...\r\n\r\nOK\r\n。strstr 只关心"有没有出现目标字符串",不关心前面有多少额外内容,正好适配这种场景。

接收处理状态机

当 ESP01S 收到服务器数据时,会主动推送:

+IPD,5:hello

这种**主动上报**不能用 esp_at_cmd 处理(因为没有对应的"发送指令"步骤)。需要额外在主循环中轮询检测 RingBuffer 中是否出现 +IPD 头,然后提取后续数据。我这里实现了一个接收状态机,在主循环中循环调用,不阻塞。从环形缓冲区读取字节流,解析 ESP01S 发送的协议数据。

完整的代码设计如下,建议下载源码查看。先使用一个解析头的状态机,解析到这次的接收指令实际的头。

void esp_process_data(struct esp01s *esp)
{
    uint8_t ch;
    int16_t parse_ret = 0;
    /* 主循环:从环形缓冲区读取每一个字节 */
    while (ringbuffer_getchar(&esp->rx_rb, &ch) == 1)
    {
        switch (esp->state)
        {
            /* 检测协议头的起始字符 '+' */
            case ESP_STATE_IDLE:
            {
                if (ch == '+')
                {
                    esp->header_len = 0;
                    esp->header_buf[esp->header_len++] = ch;
                    esp->state = ESP_STATE_COLLECT_HEADER;
                }
            }
            break;

            /* 收集完整的协议头,然后与协议逐一比对  */
            case ESP_STATE_COLLECT_HEADER:
            {
                if (ch == ':' || ch == ',')
                {
                    uint8_t found = 0;

                    if (memcmp(esp->header_buf, "+IPD", 4) == 0)
                    { 
                        // 清空 IPD 相关变量值,并进入解析 IPD 的解析状态
                        esp->ipd_data.link_id = 0;
                        esp->ipd_data.expected_len = 0; 
                        esp->state     = ESP_STATE_PARSE_IPD;
                        esp->ipd_state = ESP_IPD_PARSE_LINKID;
                        found = 1;
                        break;
                    }
                    if (!found)
                    {
                        esp->state = ESP_STATE_IDLE;
                    }
                }
                else if (esp->header_len < ESP_MAX_HEADER_LEN - 1)
                {
                    esp->header_buf[esp->header_len++] = ch;
                }
                else
                {
                    esp->state = ESP_STATE_IDLE;
                }
            }
            break;
        // 解析 IPD 接收数据
        case ESP_STATE_PARSE_IPD:
            parse_ret = esp_process_ipd_data(esp,ch);
            if(parse_ret == -1 || parse_ret == 1)
            {
                esp->state = ESP_STATE_IDLE;
            }
            break;
        } 
    }
}

解析到比如 IPD 这个头后,我们再进入解析 IPD 数据函数,里面还是一个状态机。

/* 解析 IPD 接收的函数
 *
 * 返回值:
 *   -1 - 异常
 *   0  - 接收处理中
 *   1  - 成功 
 */
int16_t esp_process_ipd_data(struct esp01s *esp,uint8_t ch)
{
    int16_t ret = 0;
    switch (esp->ipd_state)
    { 
        /* 解析第一个数字(可能是 LinkID 或长度)
         * 实现说明:
         *   - 遇到数字:累加到 link_id
         *   - 遇到逗号:link_id 是 LinkID,后面还有长度 → 进入 PARSE_LENGTH
         *   - 遇到冒号:link_id 就是长度(客户端模式) → 进入 RECEIVE_DATA
         *   - 其他字符:格式错误 → 进入 IDLE
         * -------------------------------------------------- */
        case ESP_IPD_PARSE_LINKID:
            if (ch >= '0' && ch <= '9')
            {
                /* 数字字符,累加到 link_id */
                esp->ipd_data.link_id = esp->ipd_data.link_id * 10 + (ch - '0');
            }
            else if (ch == ',')
            {
                /* 遇到逗号,说明第一个数字是 LinkID */
                esp->ipd_state = ESP_IPD_PARSE_LENGTH;
                esp->ipd_data.expected_len = 0;
            }
            else if (ch == ':')
            {
                /* 遇到冒号,第一个数字就是长度(客户端模式) */
                esp->ipd_data.expected_len = esp->ipd_data.link_id;
                esp->ipd_data.link_id = 0;
                esp->ipd_state = ESP_IPD_RECEIVE_DATA;
                esp->ipd_data.data_len = 0;
            }
            else
            {
                ret = -1;
            }
            break;

        /* --------------------------------------------------
         * 状态:ESP_STATE_PARSE_LENGTH
         * 任务:解析数据长度(在 LinkID 之后的数字)
         *
         * 实现说明:
         *   - 遇到数字:累加到 expected_len
         *   - 遇到冒号:长度解析完成 → 进入 RECEIVE_DATA
         *   - 其他字符:格式错误 → 进入 IDLE
         * -------------------------------------------------- */
        case ESP_IPD_PARSE_LENGTH:
            if (ch >= '0' && ch <= '9')
            {
                /* 数字字符,累加到 expected_len */
                esp->ipd_data.expected_len = esp->ipd_data.expected_len * 10 + (ch - '0');
            }
            else if (ch == ':')
            {
                /* 遇到冒号,长度解析完成 */
                esp->ipd_state = ESP_IPD_RECEIVE_DATA;
                esp->ipd_data.data_len = 0;
            }
            else
            {
                ret = -1;
            }
            break;

        /* --------------------------------------------------
         * 状态:ESP_STATE_RECEIVE_DATA
         * 任务:按照解析出的长度,接收完整的数据
         *
         * 实现说明:
         *   将接收到的字节存入 data_buf
         *   当 data_len >= expected_len 时,数据接收完成
         *   调用对应协议的处理函数,然后回到 IDLE
         * -------------------------------------------------- */
        case ESP_IPD_RECEIVE_DATA:
        {
            if (esp->ipd_data.data_len < ESP_DATA_BUF_SIZE)
            {
                esp->ipd_data.data_buf[esp->ipd_data.data_len++] = ch;
                if (esp->ipd_data.data_len >= esp->ipd_data.expected_len)
                {
                    /* 数据接收完成,调用处理函数 */
                    esp_handle_ipd(esp, esp->ipd_data.link_id, esp->ipd_data.data_len, esp->ipd_data.data_buf);
                    ret = 1;
                }
            }
            else
            { 
                ret = -1;
            }
        }
        break;
    }
    return ret;
}

完整 TCP 通信流程

下面用上面封装的函数,完成一次完整的 TCP 通信。每个步骤都标注了期望响应和超时时间。

1. 测试 AT 是否就绪

1
2
3
ret = esp_at_cmd(&esp01s_device, "AT\r\n", "OK", 2000);
printf("AT resp: %s\n", esp01s_device.resp_buf);
if(ret != 0) while(1);

2. 设置 STA 模式

1
2
3
ret = esp_at_cmd(&esp01s_device, "AT+CWMODE=1\r\n", "OK", 2000);
printf("AT+CWMODE resp: %s\n", esp01s_device.resp_buf);
if(ret != 0) while(1);

3. 连接 WiFi

1
2
3
ret = esp_at_cmd(&esp01s_device, "AT+CWJAP=\"CMCC-XJmL\",\"sR62HiPv\"\r\n", "OK", 5000);
printf("AT+CWJAP resp: %s\n", esp01s_device.resp_buf);
if(ret != 0) while(1);

超时 5s:连接 WiFi 需要协商,时间比较长。

4. 建立 TCP 连接

1
2
3
ret = esp_at_cmd(&esp01s_device, "AT+CIPSTART=\"TCP\",\"192.168.1.4\",40000\r\n", "OK", 5000);
printf("AT+CIPSTART resp: %s\n", esp01s_device.resp_buf);
if(ret != 0) while(1);

5. 发送数据

ESP01S 的发送需要两步:先告诉它要发多少字节,等它回复 > 后再发实际数据。

1
2
3
4
5
6
7
ret = esp_at_cmd(&esp01s_device, "AT+CIPSEND=5\r\n", ">", 5000);
printf("AT+CIPSEND resp: %s\n", esp01s_device.resp_buf);
if(ret == 0)
{
    ret = esp_at_cmd(&esp01s_device, "HELLO", "SEND OK", 5000);
    printf("HELLO resp: %s\n", esp01s_device.resp_buf);
}

6. 接收数据(被动接收)

当 ESP01S 收到服务器数据时,会主动推送:

+IPD,5:hello

这种 主动上报 不能用 esp_at_cmd 处理(因为没有对应的"发送指令"步骤)。需要额外在主循环中轮询检测 RingBuffer 中是否出现 +IPD 头,然后提取后续数据。

我们一直在WHILE中调用我们之前写的接收状态机即可。

esp_process_data(&esp01s_device);