
简介面向STM32嵌入式开发者这份资源以完整工程形式提供ESP8266 WiFi模块的AT指令通信实现解决单片机与ESP8266之间的串口交互、指令解析及状态判断等常见问题适合正在学习物联网通信或需要快速集成WiFi功能的开发者。压缩包内共248个文件以.h头文件与.c源码为主包含STM32L4系列HAL库的UART、TIM、I2C、ADC等外设驱动实现同时保留编译产生的.o、.d、.elf、.bin等中间文件及工程配置文件整体14.6MB便于直接打开工程查看完整代码结构与编译流程。该资源已有1947人学习下载内容涵盖HAL库初始化、ESP8266 AT指令集封装、串口中断接收与数据缓存处理等核心环节通过源码可以理解AT指令交互时序、超时重试机制以及常见异常排查思路为后续项目开发提供可复用的底层驱动模板和参考实现。1. 从串口到固件升级ESP8266 的 AT 指令不只是“发字符串”这么简单很多人拿到 STM32 和 ESP8266 的第一反应是用串口发ATCWMODE1这类命令能返回OK就算打通。这个印象没有错但它只覆盖了 AT 指令最表层的用法。真正把 ESP8266 的 AT 指令用在项目里你会发现问题全在细节上模块上电后什么时候才算就绪、ATCIPSEND发到一半失败怎么重试、接收缓冲区溢出时怎么恢复同步、固件版本不同导致指令集差异……这些才是从“点灯”走向“产品”的分水岭。这篇文章按我实际调试的顺序来写先帮你把串口链路和 AT 指令的握手机制理清楚然后从最小可用的代码开始一步一步覆盖连接 Wi-Fi、建立 TCP 连接、收发数据、解析返回帧、处理缓冲区溢出最后落到两个工程上最值钱的技巧AT 指令的超时重试状态机以及利用ATCIUPDATE和串口升级完成固件版本统一。适合手里有正点原子、野火或自绘 STM32 开发板想把 ESP8266 真正用起来的读者。2. 先把串口链路和 AT 指令的“握手”机制吃透2.1 ESP8266 启动时序为什么上电后不能立刻发 ATESP8266 模块上电后芯片要先完成内部 ROM 启动、Flash 加载、RF 校准这个过程通常需要 200ms 到 500ms。如果 STM32 在模块完全就绪前就发送AT字符会直接丢弃甚至可能因为模块 UART 尚未初始化而产生乱码。更隐蔽的是模块的 RST 引脚被 STM32 控制时按下复位后的等待时间也要重新计算。常见做法是在 STM32 上电后先延时 1 到 2 秒再发送AT测试指令。这个延时并非拍脑袋而是要给模块留出足够的稳定时间如果模块还接了 EN使能引脚需要先把 EN 拉高并保持至少 10ms再开始计时。// stm32_esp8266_boot.c void ESP8266_PowerOn(void) { HAL_GPIO_WritePin(ESP_EN_GPIO_Port, ESP_EN_Pin, GPIO_PIN_SET); // EN 拉高 HAL_GPIO_WritePin(ESP_RST_GPIO_Port, ESP_RST_Pin, GPIO_PIN_SET); // 复位脚释放 HAL_Delay(500); // 等待 RF 校准和 Flash 加载 ESP8266_SendString(AT\r\n); // 测试同步 }逻辑说明EN 引脚是模块的总开关拉高后模块才开始上电流程RST 释放后模块执行完整的启动序列。先延时 500ms 再发第一条AT基本能覆盖绝大多数 ESP-01、ESP-12F 模块的启动时间。如果你用的是 4800 波特率的老固件延时还需要更长一点因为模块串口打印的引导日志会占用一定时间。参数说明AT\r\n中\r\n是必须的ESP8266 官方 AT 固件只识别 CRLF 结尾只发\n或\r都会导致无响应。这与很多 STM32 教程里习惯用的\n不同是第一个容易踩的坑。2.2 串口参数匹配波特率不一致时会发生什么ESP8266 默认波特率通常是 115200但部分出厂固件可能是 9600。STM32 的 USART 和 ESP8266 之间波特率如果不一致表现是发送AT没有任何返回或者返回一堆乱码。乱码往往不是模块坏了而是速率不匹配。调试时我建议用 USB-TTL 先把模块接到电脑用串口助手确认波特率和固件版本再用 STM32 去对接。这样可以避免两头都黑盒。确认方法是在串口助手里发送ATGMR模块会返回固件版本信息。// stm32_usart_config.c void ESP8266_UART_Init(void) { huart2.Instance USART2; huart2.Init.BaudRate 115200; // 与 ESP8266 默认波特率一致 huart2.Init.WordLength UART_WORDLENGTH_8B; huart2.Init.StopBits UART_STOPBITS_1; huart2.Init.Parity UART_PARITY_NONE; HAL_UART_Init(huart2); }参数说明WordLength为 8 位数据位StopBits为 1 位停止位Parity为无校验。ESP8266 的 AT 固件默认就是这个格式STM32 的 USART 配置不需要做特殊处理。但如果你的模块被改过波特率比如设置成了 9600那么这里也要改否则你会花很多时间在“为什么没响应”上。2.3 AT 指令的三层握手同步、测试、配置AT 指令的正常流程不是一条指令打天下而是有固定的顺序先测同步再查版本/模式最后配置参数。这个顺序的好处是每步都能确认模块状态排错时可以快速定位哪一层出了问题。第一步发 AT等 OK 第二步发 ATCWMODE1等 OK 或 ERROR 第三步发 ATCWJAPSSID,PASSWORD等 WIFI GOT IP每一步的返回都携带状态信息OK表示指令执行成功ERROR表示语法错误或当前状态不允许WIFI GOT IP表示已拿到 IP 地址。如果卡在某一步优先检查上一步的连接状态。3. 用 STM32 的串口中断收完整一帧 AT 返回数据3.1 为什么不能用简单的 HAL_UART_Receive 死等很多初学者用HAL_UART_Receive(huart2, (uint8_t*)buf, len, 1000)去收 AT 返回这个写法在模块响应快速且长度固定时勉强能用但 AT 指令的返回帧长度不确定比如ATCIFSR的返回包含多个 IP 地址长度远超预期。阻塞接收会导致两个问题一是如果模块返回的数据比len长多出来的数据留在缓冲区里下一帧解析会错位二是超时时间难选太短收不全太长影响响应速度。我一般用串口空闲中断加环形缓冲区的方式收数据。STM32 的 HAL 库提供了HAL_UART_Receive_DMA和HAL_UARTEx_ReceiveToIdle_DMA后者可以在一帧数据接收完成后触发空闲中断非常适合 AT 指令这种以空闲为帧边界的场景。// esp8266_uart_dma.c #define RX_BUF_SIZE 512 uint8_t esp_rx_buf[RX_BUF_SIZE]; volatile uint16_t esp_rx_len 0; void ESP8266_UART_DMA_Start(void) { HAL_UARTEx_ReceiveToIdle_DMA(huart2, esp_rx_buf, RX_BUF_SIZE); } void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart-Instance USART2) { esp_rx_len Size; // 记录本次接收到的字节数 // 可以在此处理一帧完整数据 } }逻辑说明HAL_UARTEx_ReceiveToIdle_DMA在每收到一个数据后开始判断线路是否空闲如果总线上有超过一个字节时间的空闲就会触发回调Size参数是实际接收的字节数。这样即使 AT 返回长达几百字节也能一次性收完。回调里拿到esp_rx_buf和esp_rx_len后就可以走解析流程。参数说明RX_BUF_SIZE设成 512 是因为ATCIFSR和ATCWJAP的返回可能较长尤其查询 IP 时可能返回两行地址512 字节足够覆盖。如果你的项目里模块会收到大量数据比如 TCP 服务器推送缓冲区需要按业务大小调整。3.2 环形缓冲区解决连续返回和粘帧问题空闲中断能解决单帧接收但模块有时会连续返回多条数据比如执行ATCIPSTART后立刻又收到 TCP 数据这时用固定缓冲区就会丢数据。更好的方案是建一个环形缓冲区把每次 DMA 接收到的数据追加进去解析时按\r\n分割完整行。// ring_buffer.c typedef struct { uint8_t buf[1024]; uint16_t head; uint16_t tail; } RingBuffer; RingBuffer esp_rb; void RingBuffer_Write(RingBuffer *rb, uint8_t *data, uint16_t len) { for (uint16_t i 0; i len; i) { rb-buf[rb-head] data[i]; rb-head (rb-head 1) % 1024; } }逻辑说明head指向下一个写入位置tail指向下一个读取位置。写入时把 DMA 收到的数据逐个放进缓冲区读取时按需取出。要注意的是当head追上tail时缓冲区已满实际项目中要留出足够的空间或者做丢帧处理不能用覆盖写的方式静默丢弃。3.3 从缓冲区解析OK、ERROR、WIFI GOT IP有了完整的一帧数据下一步就是解析关键字。AT 指令返回的标准格式是\r\n关键字\r\n解析时要把\r\n剥离后再看内容。// esp8266_parser.c bool ESP8266_CheckResponse(const char *response, const char *keyword) { if (strstr(response, keyword) ! NULL) { return true; } return false; }逻辑说明strstr是最直接的子串匹配方式适合OK、ERROR、WIFI GOT IP这类关键字。但要注意ATCIPSTATUS的返回里可能同时包含STATUS和OK如果只判断OK就会误判状态。更严谨的做法是先按行分割再对每一行做精确匹配。此外模块收不到响应时ESP8266_CheckResponse返回false需要配合超时机制做重发这个在后面的状态机里讲。4. 连接 Wi-Fi 和建立 TCP 连接从ATCWJAP到ATCIPSTART4.1 Wi-Fi 连接指令的参数和返回码详解ATCWJAP是连接 Wi-Fi 的核心指令参数格式为ATCWJAPSSID,PASSWORD。SSID 和密码都要求用双引号括起来且不支持中文 SSID。如果连接失败模块返回ERROR此时需要用ATCWJAP?查询当前连接状态或者用ATCWLAP扫描附近网络确认 SSID 是否存在。ATCWJAPMyWiFi,12345678 返回 WIFI DISCONNECT WIFI GOT IP OK注意返回顺序模块先上报WIFI DISCONNECT表示断开旧连接然后上报WIFI GOT IP最后才是OK。所以代码里不能只等OK就认为拿到 IP要同时检查WIFI GOT IP。如果网络是隐藏 SSID需要加第三个参数ATCWJAPSSID,PASSWORD,1其中1表示允许连接隐藏网络。4.2 ATCIPSTART 建立 TCP 连接的正确姿势Wi-Fi 连上后下一步是建立一个 TCP 或 UDP 连接。ATCIPSTART的参数格式为ATCIPSTARTTCP,服务器地址,端口号。服务器地址可以是域名或 IP如果是域名模块会走 DNS 解析需要多等一些时间。ATCIPSTARTTCP,192.168.1.100,8080 成功返回 CONNECT OK如果服务器地址不可达模块可能长时间无响应或者返回DNS Fail、CONNECT FAIL。这里有个容易忽视的点ESP8266 默认工作在单连接模式执行ATCIPSTART前必须确保ATCIPMUX0。如果之前设置过ATCIPMUX1多连接模式ATCIPSTART的格式会变成ATCIPSTART0,TCP,IP,port少参数直接报错。// esp8266_tcp_connect.c bool ESP8266_ConnectTCP(const char *ip, uint16_t port) { char cmd[128]; sprintf(cmd, ATCIPSTART\TCP\,\%s\,%d\r\n, ip, port); ESP8266_SendString(cmd); // 等待 CONNECT 和 OK if (ESP8266_WaitFor(CONNECT, 5000) ESP8266_WaitFor(OK, 1000)) { return true; } return false; }逻辑说明sprintf拼出完整指令后发送然后分两步等待返回。先等CONNECT表示 TCP 握手成功再等OK表示指令执行完成。两个关键字的超时时间分别设置是合理的TCP 握手过程取决于网络状况给 5 秒OK在CONNECT之后基本立即返回给 1 秒即可。4.3 透传模式与非透传模式的选择ATCIPMODE控制传输模式0是普通模式每条数据用ATCIPSEND长度发送1是透传模式进入后所有串口数据直接发往网络。透传模式适合数据流场景比如持续上传传感器数据但退出透传需要发送特殊序列而且前后各需要 1 秒静默时间。ATCIPMODE1 ATCIPSEND 进入透传返回 后面发的所有数据直接进入 TCP 连接 退出发送 等待 1 秒非透传模式更可控每条消息都有明确长度和返回状态适合请求-响应式的指令交互比如 MQTT 控制灯带或查询传感器状态。我一般用普通模式做请求响应用透传模式做日志流。非透传发送数据的指令格式是ATCIPSEND4其中4是数据长度模块返回提示符后再发送实际数据发送完模块返回SEND OK。5. 实战STM32 通过 ESP8266 发送 HTTP 请求并解析响应5.1 完整的发送-等待-接收流程这一节把前面的内容串起来STM32 发送一条 HTTP GET 请求把响应完整收下来并解析状态码。HTTP 协议本身是纯文本AT 指令只负责 TCP 透传。发送时要先计算请求体的长度再按ATCIPSEND长度的方式发送。// esp8266_http_demo.c void ESP8266_HTTP_GET(const char *host, const char *path) { char request[256]; char cmd[32]; int req_len; // 构造 HTTP 请求 sprintf(request, GET %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n, path, host); req_len strlen(request); // 告诉模块要发送的字节数 sprintf(cmd, ATCIPSEND%d\r\n, req_len); ESP8266_SendString(cmd); ESP8266_WaitFor(, 2000); // 等模块提示可以发送数据 ESP8266_SendString(request); // 发送 HTTP 请求体 ESP8266_WaitFor(SEND OK, 1000); // 确认数据已发出 // 等待接收 HTTP 响应这里用空闲中断或阻塞超时 ESP8266_WaitFor(\r\n200, 5000); // 简化的状态码判断 }逻辑说明先拼好 HTTP 请求计算长度发送ATCIPSEND后等待提示符。出现代表模块进入数据传输状态此时才能发送实际数据。发送完等SEND OK确认底层 TCP 已经把数据推出去。整个流程里最容易出错的是长度计算如果strlen的结果里包含了结尾的\0发送时会把不可见字符也带上导致 HTTP 解析失败。所以req_len不要加 1。参数说明ATCIPSEND的长度参数是十进制字节数不包括附件回车。这里 HTTP 请求用的Connection: close是为了让服务器在发完响应后主动关闭连接否则需要根据Content-Length判断响应何时结束处理起来复杂得多。5.2 解析返回数据状态码与耗时评估HTTP 响应的第一行是HTTP/1.1 200 OK状态码就在这一行里。解析时先用strstr定位HTTP/1.1再往后读三位数字。注意返回帧里可能混入模块自身的OK、SEND OK等提示所以解析 HTTP 响应前最好先清理接收缓冲区或者从缓冲区末尾倒着找。// http_parse.c int ESP8266_ParseHTTPCode(const char *buf, int len) { const char *p strstr(buf, HTTP/1.1); if (p NULL) { return -1; } p strlen(HTTP/1.1); while (*p || *p \t) p; // 跳过空格 if (p - buf len) { return atoi(p); // 提取三位状态码 } return -1; }逻辑说明strstr找到HTTP/1.1的位置偏移到状态行末尾的空格后用atoi提取数字。这个写法很轻量能应对大多数场景。如果服务器用的是HTTP/1.0这个解析会失败我一般把查找模式改成HTTP/1.就能兼容两个版本。耗时评估方面从SEND OK之后到收到第一个字节的时间主要花在服务器处理和网络 RTT 上正常局域网内几百毫秒外网可能几秒所以超时时间按网络环境调整不要统一用 5 秒。5.3 调试技巧串口日志、AT 回显与硬件流控调试 AT 指令时最有效的工具是 STM32 的串口日志输出。我把 ESP8266 的 TX/RX 引脚额外接到一个 USB-TTL 上用串口助手同时监听模块侧的原始数据这样能区分是 STM32 发送错误还是模块返回异常。另外AT 固件默认是回显模式发送的指令会被模块原样返回一遍开启ATE0可以关闭回显让代码解析时少一层干扰。发送ATE0\r\n 返回OK硬件流控方面ESP8266 默认是关闭 RTS/CTS 的用三线串口TX、RX、GND就能工作。但如果 STM32 和 ESP8266 之间有长线连接或电磁干扰较大可以开启硬件流控降低丢包率。开启方式是在 Wi-Fi 连接前发送ATRFC1并且 STM32 侧要在串口初始化时使能 RTS/CTS 引脚。注意一旦开启硬件流控两边的接线必须规范否则模块会一直等待 CTS 信号表现为完全无响应。6. AT 指令的进阶技巧超时重试状态机与固件升级6.1 把 AT 指令交互改造成状态机在实际项目中AT 指令的每一步都可能失败失败后不重试或盲目重试都会出问题。我用一个简单的状态机管理 AT 指令生命周期空闲、发送中、等待响应、解析完成、超时重试。核心是每次发送前记录当前指令名和重试次数收到响应后根据关键字决定下一步。// at_state_machine.c typedef enum { AT_IDLE, AT_SEND, AT_WAIT_RESP, AT_RETRY } AT_State; AT_State at_state AT_IDLE; uint8_t at_retry_count 0; #define AT_MAX_RETRY 3 void AT_Task(void) { switch (at_state) { case AT_IDLE: // 开始下一个指令 if (cmd_queue_not_empty()) { at_retry_count 0; at_state AT_SEND; } break; case AT_SEND: send_next_command(); at_state AT_WAIT_RESP; start_timeout(3000); break; case AT_WAIT_RESP: if (response_received()) { at_state AT_IDLE; } else if (is_timeout()) { if (at_retry_count AT_MAX_RETRY) { at_state AT_IDLE; // 放弃或进入错误处理 } else { at_state AT_SEND; // 重发 } } break; default: break; } }逻辑说明状态机的核心思想是让 AT 指令的执行不阻塞主循环。发送后设置一个超时定时器收到响应就回到空闲状态超时则按重试次数决定是否重发。AT_MAX_RETRY设为 3 次是经验值Wi-Fi 连接建立本身有不稳定性1 次失败往往只是瞬态超过 3 次基本是配置问题或模块异常继续重试只会浪费时间。提示超时时间要按指令类型区分ATCWJAP给 8 到 10 秒ATCIPSEND给 2 到 3 秒不能用一个统一值。6.2 固件版本管理与ATCIUPDATE的坑不同版本的 ESP8266 AT 固件指令集和返回格式有细微差异。比如早期固件的ATCIPSEND不需要长度参数新固件则必须指定长度ATCIPMUX1多连接模式下返回格式也不同。所以每次拿到新模块第一步就是发ATGMR查看版本号并记录下来。ATCIUPDATE是远程升级指令模块会从乐鑫服务器下载新固件整个过程需要几分钟且不能断电。这里有几个坑一是模块需要联网且服务器在海外国内网络可能非常慢甚至失败二是升级过程中模块会重启波特率可能被重置升级完成后需要用新的波特率连接确认版本。ATCIUPDATE 返回 ATCWMODE_CUR1 ATCWMODE_CUR1 OK ... WIFI CONNECTED WIFI GOT IP CIUPDATE: ROOM0 CIUPDATE: ROOM1 CIUPDATE: ROOM2 CIUPDATE: DONE如果升级失败模块可能变砖。保险做法是下载官方固件用串口工具本地烧录用乐鑫 ESPFlashDownloadTool 选好 Flash 大小和波特率直接刷。STM32 也可以通过串口把固件发给模块但需要严格按 ESP8266 的 bootloader 协议来步进时序比较麻烦建议只在没有 USB-TTL 的现场才这么做。6.3 用 AT 指令实现 OTA 远程升级的简单姿势如果你的项目已经用 STM32 加 ESP8266OTA 不一定要升级 ESP8266 固件更实际的是升级 STM32 自身的应用程序。做法是STM32 通过 AT 指令从服务器下载固件文件然后写入 STM32 的内部 Flash 或外部 SPI Flash最后跳转运行。这里的核心是分块下载和校验。用ATCIPRECVLEN或透传模式持续接收 TCP 数据每收一块校验一次校验和全部完成后比对整体 CRC32。ATCIPSTARTTCP,192.168.1.200,9000 ATCIPSEND1024 发送 1024 字节固件数据 SEND OK 重复直到文件结束接收端要处理粘包拆包文件的边界不是网络包边界必须按固定块大小切分。我在工程里是每个块加 4 字节校验头接收方解析时先读块长度再读数据校验通过写 Flash这样即使网络丢包也能定位到具体的块做重传。这个方案比直接升级 ESP8266 固件实用得多因为 STM32 的应用升级频率远高于 ESP8266 的 AT 固件。配合ATCIUPDATE做一次性的模块固件版本统一就可以让整个设备在生命周期内不需要拆机。本文还有配套的精品资源点击获取