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
接收端设计
串口中断只负责收数据,不做任何解析:
| 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 收到服务器数据时,会主动推送:
这种**主动上报**不能用 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 是否就绪
| 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 模式
| 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
| 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 连接
| 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 的发送需要两步:先告诉它要发多少字节,等它回复 > 后再发实际数据。
| 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 收到服务器数据时,会主动推送:
这种 主动上报 不能用 esp_at_cmd 处理(因为没有对应的"发送指令"步骤)。需要额外在主循环中轮询检测 RingBuffer 中是否出现 +IPD 头,然后提取后续数据。
我们一直在WHILE中调用我们之前写的接收状态机即可。
| esp_process_data(&esp01s_device);
|