
简介基于STM32与ESP8266的AT指令实现工程面向嵌入式开发与物联网学习者演示如何通过串口向ESP8266发送AT指令完成无线网络连接、TCP数据传输等常用操作适合正在入门STM32与Wi-Fi模块联合开发的读者。压缩包内含248个文件其中69个头文件与38个C源文件构成HAL库底层驱动和业务逻辑另有编译过程生成的依赖文件、目标文件以及最终的二进制镜像、链接映射等便于查看从源码到固件的完整流程。工程基于STM32L4系列芯片包含STM32CubeMX工程配置文件、链接脚本、Makefile与编译辅助文件以及一份PDF说明文档可帮助理解外设初始化与AT指令交互的代码组织方式。目前已有1947人学习对于希望快速跑通ESP8266模块指令控制、减少环境配置与串口调试踩坑的开发者是一份可以直接参考甚至移植复用的实用代码包。1. 一块 MCU 和一个 Wi-Fi 模块之间的对话很多人以为 STM32 连上 ESP8266 就能上网其实真正干活的是串口上的那几十条文本命令。ESP8266 内部跑着完整的 TCP/IP 协议栈STM32 只需要通过 UART 发送形如ATCWJAPSSID,PASSWORD这样的指令就能让它去连接路由器、建立 TCP 连接、把收到的数据往串口丢。这个方案之所以经久不衰是因为它把复杂的网络栈隔离在模块侧MCU 侧只做状态机和字符串解析对资源紧张的 STM32F103 这类芯片来说是最省心、最容易调通的联网路径。本篇文章要讲的就是这条路径上从引脚接线到指令时序、再到断线重传的完整做法。不管你是用 CubeMX 生成初始化代码还是手写寄存器版最终会发现核心难点不在 AT 指令本身而在串口数据的可靠收发和响应超时判定上。2. 为什么 ESP8266 的 AT 指令像一个有限状态机2.1 AT 指令的报文格式和常见回包结构ESP8266 的 AT 指令集沿用了老式 Modem 的语法每条指令以AT开头以\r\n结尾模块返回的响应也是以\r\n分隔的多行文本。一个最简单的例子是AT\r\n模块会返回OK\r\n。这看着不起眼但它是所有后续交互的基础STM32 的串口接收中断里必须能识别出\r\n边界才能把收到的内容拼成一条完整消息。指令按功能大致分三类基础测试类如AT、ATRST、参数配置类如ATCWMODE1设置模式、操作执行类如ATCIPSTARTTCP,192.168.1.100,8080建立连接。每一类指令的响应格式基本固定要么直接回OK要么回ERROR要么在执行过程中产生带前缀的异步消息比如IPD,len:data表示收到网络数据。这就意味着 STM32 端不能只是简单地把串口收到的字节存进数组而是要按行解析并且能区分“当前指令的同步响应”和“网络主动上报的异步事件”。2.2 从 STM32 视角看 UART 如何对接 AT 指令STM32 和 ESP8266 之间只有一条 UART 通信链路。常见的接线方式是 STM32 的 USART_TX 接 ESP8266 的 RXDUSART_RX 接 ESP8266 的 TXD双方共地。有的模块还需要 CH_PD使能引脚拉高才能工作这个往往容易被忽略导致串口发指令没反应还以为是波特率错了。在 AT 固件的默认配置下ESP8266 出厂波特率通常是 1152008 位数据位、无校验、1 位停止位。STM32 侧打开 UART 时必须把波特率配成一致。这里有一个容易踩的坑很多 ESP8266 模块尤其是 NodeMCU 板载的那种的串口直接连在 USB 转串口芯片上而不是单独的 UART 引脚如果你把 STM32 连到了模块的调试串口而不是 UART0那指令永远不会被模块的 AT 固件处理。所以接线之前先确认模块原理图上标注的 UART_TX / UART_RX 位置。// 配置 STM32 UART 发送 AT 指令的最小示例假设用 HAL 库 char cmd[] AT\r\n; HAL_UART_Transmit(huart1, (uint8_t*)cmd, strlen(cmd), 100);这段代码把AT\r\n发出去100是超时毫秒数。发送本身没什么可说的真正的难点在接收因为ATCIPSTART这类指令的响应是分段的不能靠单个HAL_UART_Receive期望一次读完。我一般会在中断里逐字节接收把到达的数据放入环形缓冲区后台任务再去按行提取。ESP8266 返回的数据在时间上可能有间断比如先回CONNECT\r\nOK\r\n然后又来一段IPD,4:test如果只做一次固定长度读取很容易把两段数据拼错。2.3 指令超时与重发机制的设计依据AT 指令是典型的请求-响应模型但响应时间并不确定。比如ATCWJAP连接路由器时快则几百毫秒慢则十几秒所以不能像普通串口外设那样发完就等固定时间必须设计一个超时向量表。每发一条指令记录当前系统 tick然后在轮询或状态机里持续判断如果收到OK或者预期响应就进入下一步如果收到ERROR就重试如果超时未收到任何响应就认为模块挂死需要拉低 RST 引脚复位。这个超时机制是所有 AT 指令控制程序的地基。没有它程序会卡在等待响应里看起来像死机。我习惯把模块的 RST 引脚接到一个普通 GPIO方便软件复位这样能在异常时自动恢复而不是只能手动重启。3. 搭建 STM32 与 ESP8266 的 UART 通信最小闭环3.1 硬件接线与板级配置在写代码之前先把硬件接好。以最常见的 STM32F103C8T6 蓝色药丸板加 ESP8266-01 模块为例接线如下表STM32 引脚ESP8266 引脚说明PA9 (USART1_TX)RXDSTM32 发送给模块PA10 (USART1_RX)TXD模块发送给 STM323.3VVCC注意模块的 3.3V 电流要求供电ESP8266-PCB 上电瞬间电流可能达 300mAGNDGND共地PB0 (可选)RST用于软件复位模块注意STM32 和 ESP8266 间不要用 5V 电平的 TTL 串口直接互连否则可能损坏模块。很多 ESP8266 模块是 3.3V 供电但串口引脚兼容 3.3V不兼容 5V。如果你的后续电路中用了电平转换或板载稳压就不需要额外处理。3.2 CubeMX 初始化与 UART 参数用 STM32CubeMX 生成项目时USART1 异步模式开了就行波特率填 115200数据位 8、无校验、1 停止位。NVIC 里打开全局中断否则你没法在中断里收数据。生成代码后把串口接收中断打开// main.c 中相当于初始化后启动接收 HAL_UART_Receive_IT(huart1, (uint8_t*)rx_byte, 1);这一步是逐字节接收的起点。每收完一个字节中断回调里会把该字节放入环形缓冲再重新调用HAL_UART_Receive_IT继续收下一个。这里有个细节HAL_UART_Receive_IT设置一次接收一个字节回调里必须再次调用否则中断只触发一次后续数据就收不到了。3.3 AT 指令发送与响应解析的代码骨架#include string.h // 简单的环形缓冲结构 uint8_t rx_buffer[256]; volatile uint16_t rx_head 0, rx_tail 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { rx_buffer[rx_head] rx_byte; rx_head (rx_head 1) % sizeof(rx_buffer); HAL_UART_Receive_IT(huart1, (uint8_t*)rx_byte, 1); } } // 阻塞发送单条 AT 指令 void at_send(const char *cmd) { HAL_UART_Transmit(huart1, (uint8_t*)cmd, strlen(cmd), 1000); }要判断模块是否在线就调用at_send(AT\r\n)然后在接收缓冲里找是否存在字符串OK\r\n。查找操作需要按行扫描// 从环形缓冲区提取一行返回 0 表示还没凑齐完整行 uint8_t at_readline(char *line, uint16_t max_len) { static uint16_t line_len 0; while (rx_tail ! rx_head) { char c rx_buffer[rx_tail]; rx_tail (rx_tail 1) % sizeof(rx_buffer); if (c \n) { line[line_len] \0; line_len 0; return 1; } if (line_len max_len - 1) { line[line_len] c; } } return 0; }环形缓冲区可以应对串口数据断续到达、但连续读走的情况。at_readline一次只取一行适合逐行匹配OK或ERROR。比起直接睡 200ms 然后读全部数据这种方案能区分指令应答和异步消息对后续处理IPD数据至关重要。3.4 波特率不对时的典型现象怎么排查如果发送AT后完全没有响应优先检查三件事模块是否供电正常看模块上 LED 是否亮、串口 TX/RX 是否接反、波特率是否匹配。还有一个常见问题是模块处于透传模式或已经被上一段程序占用导致不再响应 AT 指令。这时可以拉低 RST 引脚复位模块等 1 秒再发AT试试。如果模块回了一堆乱码说明波特率不匹配或者 ESP8266 被更新过非标准固件。有些模块出厂波特率是 9600 而不是 115200检查模块标签或文档或者用串口助手扫一遍常见波特率。4. 用实际代码让 ESP8266 连上 Wi-Fi 并建立 TCP 连接4.1 配置 Wi-Fi 模式的指令序列AT 固件默认是 Station 模式ATCWMODE1如果之前配置过 SoftAP 模式需要先改回来。完整的连接流程是重启模块、设置模式、连接路由器、查询 IP。我一般按顺序执行ATRST\r\n ATCWMODE1\r\n ATCWJAPMyWiFi,mypassword\r\n ATCIFSR\r\n在 STM32 端这些指令不能一股脑发出去。ATCWJAP的响应时间很长而且中途可能收到WIFI CONNECTED之类的异步提示。所以每发一条指令都要用一个如下的等待函数// 发送指令并等待预期的关键字超时 10 秒 uint8_t at_cmd_wait(const char *cmd, const char *expect, uint32_t timeout_ms) { uint32_t start HAL_GetTick(); char line[128]; at_send(cmd); memset(line, 0, sizeof(line)); while (HAL_GetTick() - start timeout_ms) { if (at_readline(line, sizeof(line))) { if (strstr(line, expect) ! NULL) { return 1; } if (strstr(line, ERROR) ! NULL) { return 0; } } } return 0; }at_cmd_wait做了两件事发送指令、在超时时间内逐行找关键字。参数expect一般是OK但在ATCWJAP时模块会先回WIFI CONNECTED然后回OK所以也可以把expect设为WIFI CONNECTED提前返回。注意这里用strstr做子串匹配不要用strcmp因为响应行可能带额外前缀或空格。4.2 TCP 客户端建立与数据透传网络连接最常用的是ATCIPSTART它属于无返回提示、只有最后OK或CONNECT FAIL的指令。比如连接本机 8080 端口ATCIPSTARTTCP,192.168.1.100,8080\r\n等待模块返回CONNECT\r\nOK\r\n有的固件版本顺序不同然后就可以用ATCIPSEND发数据。老版本固件需要先发送数据长度再发送内容新固件支持直接透传发送ATCIPSENDlength\r\n模块会回提示然后 STM32 把对应长度的数据发过去。发完不需要额外结束符模块在收到指定长度后就自动发到网络。整个过程的 STM32 状态机可以简化为三步// 伪代码描述 TCP 客户端发送的完整流程 at_cmd_wait(ATCIPSTART\TCP\,\192.168.1.100\,8080\r\n, OK, 5000); char len_str[8]; snprintf(len_str, sizeof(len_str), %d, (int)strlen(payload)); at_cmd_wait(ATCIPSEND, , 2000); HAL_UART_Transmit(huart1, (uint8_t*)payload, strlen(payload), 1000);这里有个关键问题at_cmd_wait内部会一行一行读缓冲但ATCIPSEND等待的是提示符而不是换行符。at_readline只按\n切分后面没有换行所以读取会永远等不到。针对这种情况我单独写一个超时等待函数直接检查环形缓冲末尾是否出现而不是按行读取。这就体现出设计缓冲区时底层要能同时支持两种查询方式。4.3 接收来自服务器的数据解析IPD报文ESP8266 收到网络数据后会输出形如IPD,len:data的行。这行格式固定但数据部分可能包含换行符不能简单按行读。我的做法是在按行读取的基础上遇到IPD前缀时再从行尾提取长度和内容if (strncmp(line, IPD,, 5) 0) { int len atoi(line 5); // 假定格式是 IPD,len:data这里 data 已经在 line 里但因为可能含逗号或换行 // 更稳妥的做法是解析冒号位置然后直接从 line 后面的字符里取 len 个字节。 char *data_ptr strchr(line, :) 1; uint16_t data_len strlen(data_ptr); // 存入业务缓冲 }这种解析方式只对短数据单行内放下有效。如果服务器下发的数据超过一行长度at_readline会把数据本身也当成若干行读出来导致strlen(data_ptr)长度不对。所以对于真实应用我更推荐在串口中断里直接检测IPD的消息头然后把后续len个字节原样搬到 DMA 缓冲或大数组绕开按行解析。这里不展开完整代码思路是设计一个小的“帧接收状态机”状态转移条件就是、I、P、D、,、数字、:这些字符。4.4 与服务器通信时 STM32 端的状态机设计把 AT 指令流程写成一个完整状态机比散着调用at_cmd_wait更健壮。状态大致分为STATE_INIT复位模块、STATE_CHECK确认 AT 返回 OK、STATE_CONNECT_WIFI连路由器、STATE_GET_IP、STATE_TCP_CONNECT、STATE_STANDBY空闲等待、STATE_SEND发送数据。状态机里的每个状态对应一个超时定时器超时后进入错误处理并回到STATE_INIT重新来一遍。typedef enum { ST_INIT, ST_CHECK, ST_CONNECT_WIFI, ST_GET_IP, ST_TCP_CONNECT, ST_STANDBY, ST_SEND } at_state; void at_state_machine(void) { switch (at_state) { case ST_INIT: at_send(ATRST\r\n); at_state ST_CHECK; break; case ST_CHECK: if (at_cmd_wait(AT\r\n, OK, 1000)) at_state ST_CONNECT_WIFI; break; // 其余状态类似 } }这个状态机的优势是模块的任何一步出错都能通过超时兜底回到初始状态不需要人为干预。ESP8266 本身不复杂但和 MCU 交互时最怕卡死状态机加超时重发能把这种概率降到最低。4.5 一个完整的发数据到服务器的串口命令串把上面的步骤串起来发一条温度数据到 TCP 服务器的最小流程如下ATRST\r\n ATCWMODE1\r\n ATCWJAPMyWiFi,12345678\r\n ATCIFSR\r\n ATCIPSTARTTCP,192.168.1.100,8080\r\n ATCIPSEND8\r\n temp25.5前面每一步的响应都必须检查但ATCIFSR返回的 IP 行偶尔延迟所以超时时间要给足。发送ATCIPSEND8时长度指后面temp25.5的字符数包含空格和换行务必与strlen算出来的一致否则 ESP8266 要么等不到长度而超时要么把多余字节当作下条指令的一部分。5. 用 DMA 接收和断线重连来省掉你的 CPU在低功耗或高通信频率场景下用中断逐字节接收容易大量打断 MCU 主循环我一般会改成 DMA 空闲中断IDLE Interrupt配合环形缓冲。STM32 上启用 UART DMA 接收后每收到一帧数据串口空闲中断会触发一次这时读取 DMA 剩余计数就能知道本次数据长度。这种方法不丢字节、CPU 占用低解析逻辑不变只是数据来源从逐字节中断变成了 DMA 一块一块填好的缓冲。// 开启 DMA 接收前先配置好 DMA 通道然后调用 HAL_UART_Receive_DMA(huart1, dma_rx_buffer, sizeof(dma_rx_buffer)); // 在 UART 空闲中断回调中计算本次收到的数据长度另外一个让系统稳定运行的关键是断线重连。Wi-Fi 环境里路由器重启、信号波动都会导致 ESP8266 掉线掉线后模块会输出WIFI DISCONNECT或CLOSED。STM32 端不能只在发送失败时才去重连而是要在主循环里定期检查模块状态一旦发现收到CLOSED就把状态机拨回ST_TCP_CONNECT。如果长时间发现 TCP 层重连失败就要考虑整机重启 ESP8266拉低RST引脚 100ms 再拉高然后重新走一遍ATRST之后的完整流程。最后说一个很多人忽略的参数ATCIPMUX0表示单连接模式ATCIPMUX1是多连接模式。单连接模式下无法同时开多个 socket但指令简单多连接模式时IPD报文会携带连接编号例如IPD,0,5:hello。如果用了多连接解析代码要把 id 提取出来否则后续收发的数据会串。选择哪种模式取决于你的应用是否需要同时维持多个连接不需要时不要开多连接否则每次ATCIPSTART都要带 id麻烦还容易出错。尝试着去跑通上面的几个最小例子你会发现 AT 指令控制 ESP8266 的核心思路其实就两件事可靠的串口数据搬运和清晰的状态判断。其余等待超时、重发、断线重连都是在这个基础上做的防御性设计。本文还有配套的精品资源点击获取