STM32 HAL库控制ESP8266:串口AT指令解析与掉线重连实战
发布时间:2026/9/16 2:43:30 作者:尧图编辑部 阅读量:1,286

简介STM32 HAL库驱动ESP8266的完整工程源码包面向嵌入式物联网开发者解决STM32通过UART与ESP8266进行AT指令交互、实现Wi-Fi透传的关键问题。资源共77个文件以C语言源文件21个和头文件48个为主包含HAL库驱动、外设初始化及主程序逻辑同时提供IAR/Keil工程文件.ioc、.uvprojx与烧录hex文件压缩包约549KB可直接导入工程对照学习。已有2520人学习下载。代码从串口配置、AT指令封装到透传数据收发均有清晰分层辅以工程裁剪脚本便于二次开发和移植。对于入门STM32ESP8266透传开发、希望了解HAL库工程结构并快速实现联网通信的读者具备实用参考价值。1. STM32 HAL 库操作 ESP8266先想清楚你写的其实是串口程序很多人在 ESP8266 上栽跟头不是不懂 TCP/IP而是没转过弯来STM32 和 ESP8266 之间没有 SPI 接口连接、没有“网络通信芯片”一说它们之间就是一根 UART 线。你写的 HAL 库代码本质上是在做“通过串口向一个带 WiFi 功能的协处理器发字符串、收字符串”的活而 ESP8266 自己完成 TCP、UDP、DHCP、DNS 这些栈。想通这一点整个项目的复杂度和排查范围就立刻缩小了。依赖 HAL 库的 USART 驱动接上 ESP8266 的 TX/RX通过 AT 指令集完成联网、建立 TCP 连接、收发数据这一套组合在智能台灯、鱼缸控制器、WS2812 灯带这类低成本物联网场景里依然是最常见的选型。对 STM32 开发者来说难点往往不在 HAL 库 API 本身而在三件事串口数据怎么可靠地收下来、AT 指令的应答怎么解析、掉线之后怎么自动恢复。本文按这个路线把从 CubeMX 配置到 AT 报文解析的完整思路过一遍。2. ESP8266 的 AT 指令模型和 HAL 库串口中断的设计选择2.1 AT 固件的三件事指令、应答、数据的格式约定ESP8266 出厂固件运行的是一套 Hayes 风格的 AT 指令系统。所有指令以 ASCII 文本行构成以\r\n作为一行结束。一条指令发出后模块返回的应答分成三类立即应答行如OK、ERROR、异步上报行如IPD,4:test、WIFI DISCONNECT、以及进入透传模式后的裸数据流。这三种返回形态混杂在同一根 RX 线上所以 STM32 侧接收逻辑不能只是“收到一帧就丢给用户”得有明确的协议解析。以最常见的联网时序为例AT # 返回 OK ATCWMODE1 # 返回 OK设置 Station 模式 ATCWJAPssid,password # 返回 OK 或 CWJAP:ERROR ATCIFSR # 返回本机 IP ATCIPSTARTTCP,192.168.1.10,8080 # 返回 CONNECT / OK ATCIPSENDlen # 返回 随后发送指定字节的数据这个过程中ATCWJAP是一条慢指令运气不好时 DHCP 得跑十几秒才返回ATCIPSEND则要求你在模块打出提示符后再发数据。如果你不管时序、一口气全塞进去模块会直接把后半段当成指令解析然后回你一个ERROR。2.2 HAL 库接收方案中断、DMA、空闲中断怎么选HAL 库给了HAL_UART_Receive、HAL_UART_Receive_IT、HAL_UART_Receive_DMA三条路径。对 ESP8266 这种“应答不固定、长度不可预知”的设备轮询绝对不要用会卡死主循环中断方式配合单字节缓存可行但 CPU 开销高常规工程我都用 DMA 循环接收再用串口的空闲中断IDLE判断一帧结束。方案接收长度CPU 占用帧边界判断适合场景轮询任意高手动调试临时用单字节中断任意高手动拼帧数据量小、实时性要求低DMA 循环 IDLE 中断任意低硬件自动判定推荐AT 应答和 IPD 数据都适用双缓冲 DMA任意低硬件判定 均匀拷贝高频收发比如连续 TCP 上行HAL 库最新版本在stm32f1xx_hal_uart.h里处理空闲中断的方式比较繁琐先使能UART_IT_IDLE然后在中断回调里读__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE)先置位再读取数据寄存器才能清掉标志。下面是 CubeMX 生成工程里最常见的一段扩展uint8_t esp_rx_buf[512]; // DMA 环形缓冲区 uint16_t esp_rx_last_pos; // 上一次处理到的位置 void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart-Instance USART2) { uint16_t pos 0; if (__HAL_UART_GET_FLAG(huart2, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart2); pos sizeof(esp_rx_buf) - __HAL_DMA_GET_COUNTER(hdma_usart2_rx); } // pos 到 esp_rx_last_pos 之间的数据就是完整一帧 esp_frame_process(esp_rx_buf[esp_rx_last_pos], pos - esp_rx_last_pos); esp_rx_last_pos pos; } }esp_rx_last_pos用来记录上次处理到哪因为 DMA 循环缓冲区的写指针会绕回开头。当空闲中断触发时__HAL_DMA_GET_COUNTER返回的是还剩多少字节没写用缓冲区长度减掉它就是当前写位置。这里有一个隐藏约束一帧数据不能超过缓冲区长度否则 DMA 覆盖后就没有完整帧了。所以对 AT 应答这种小帧512 字节足够但如果做透传建议把缓冲提到 2048并在应用层做分包。2.3 复位和 GPIO 控制是很多人忽略的一环ESP8266 的RST引脚低电平复位CH_PD也叫 EN引脚必须拉高才能工作。CubeMX 里把两个引脚配置为推挽输出驱动代码里加一个软复位函数void esp_hard_reset(void) { HAL_GPIO_WritePin(ESP_RST_GPIO_Port, ESP_RST_Pin, GPIO_PIN_RESET); HAL_Delay(50); // 持续拉低 50ms确保是完整复位 HAL_GPIO_WritePin(ESP_RST_GPIO_Port, ESP_RST_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(ESP_EN_GPIO_Port, ESP_EN_Pin, GPIO_PIN_SET); HAL_Delay(100); // 等待模块启动此时串口会输出 boot 日志 }模块上电初期会输出一段 115200 波特率的乱码启动日志有些固件还会自动执行一次重启所以正确做法是先等 1 秒发ATRST再等 2 秒之后才开始跑 AT 握手流程。否则第一条AT撞在启动日志里会被淹没。3. 用 CubeMX 生成 HAL 库工程实现 ESP8266 最小联网命令3.1 引脚分配和串口参数的实际组态STM32 和 ESP8266 之间只需要 TX、RX、GND再加上 RST 和 EN。注意 RX 和 TX 必须交叉连接STM32 的 TX 接 ESP8266 的 RXSTM32 的 RX 接 ESP8266 的 TX。CubeMX 里我用两个串口USART1 作为调试日志输出115200PA9/PA10USART2 接 ESP8266115200PA2/PA3并开启 USART2 的 DMA 接收。配置项USART2 参数原因Baud Rate115200ESP8266 默认 AT 固件波特率兼容性最好Word Length8 Bits标准 AT 固件帧格式ParityNone无校验减少解析复杂度Stop Bits1默认DMA RX ModeCircular循环接收才能配合空闲中断取整帧DMA RX Data WidthByte单字节宽度和缓冲区类型一致串口参数里最容易踩坑的是波特率。ESP8266 出厂固件一般是 115200但有些商家烧录了 9600 的固件。上电后用 USB 转 TTL 直接看模块主动输出的 boot 日志就能判断波特率日志能看懂就是对的乱码就是错的。代码里把波特率做成宏换模块时只改一处。另外 GPIO 的两个细节不要让 EN 引脚悬空模块可能会随机复位STM32 的 3.3V 如果由板载稳压器提供注意 ESP8266 射频发射瞬间电流可以冲到 300mA很多开发板的 LDO 撑不住现象是模块连上 WiFi 后不定时重启。我一般单独给 ESP8266 配一个 AMS1117-3.3输入 5V。3.2 最小指令执行函数和应答等待逻辑有了初始化代码第一个要跑通的函数是“发送一条 AT 指令并等待 OK”。最直接的做法是发送后轮询接收环形缓冲区uint8_t esp_send_cmd_wait_ok(char *cmd, uint32_t timeout_ms) { uint8_t resp[128]; uint16_t resp_len 0; uint32_t tick HAL_GetTick(); esp_uart_send(cmd, strlen(cmd)); // 统一在外部拼好 \r\n memset(resp, 0, sizeof(resp)); while (HAL_GetTick() - tick timeout_ms) { uint16_t len esp_frame_read(resp resp_len, sizeof(resp) - resp_len - 1); if (len 0) { resp_len len; if (strstr((char*)resp, OK) ! NULL) return RESP_OK; if (strstr((char*)resp, ERROR) ! NULL) return RESP_ERROR; } } return RESP_TIMEOUT; }esp_frame_read从第 2 章那个环形缓冲区里取走数据取完后更新esp_rx_last_pos。代码里的超时非常重要ATCIPSTART在 DNS 解析失败时会拖几秒你设 500ms 超时就白白误判了。我一般根据指令类型给不同超时普通配置指令 1000msTCP 连接 5000msJAP 联网 15000ms。3.3 跑通联网流程的最小代码下面这段代码可以在复位后直接跑依次完成测试模块在线、设为 Station 模式、连接 WiFi、查询 IP、建 TCP 连接、发数据。为了阅读方便省略了错误处理实际工程每一步失败都要做重试或错误上报。void esp_demo_link_and_send(void) { char buf[128]; // 1. 先测模块是否在线 esp_send_cmd_wait_ok(AT\r\n, 1000); // 2. 软件复位清掉之前残留的连接状态 esp_send_cmd_wait_ok(ATRST\r\n, 3000); HAL_Delay(2000); // 等待模块重启完成 // 3. 模式1Station2AP3双模 esp_send_cmd_wait_ok(ATCWMODE1\r\n, 1000); // 4. 连接 WiFi这个指令耗时最长要等 DHCP sprintf(buf, ATCWJAP\%s\,\%s\\r\n, WIFI_SSID, WIFI_PASS); esp_send_cmd_wait_ok(buf, 15000); // 5. 建立 TCP 连接到服务器 sprintf(buf, ATCIPSTART\TCP\,\%s\,%d\r\n, SERVER_IP, SERVER_PORT); esp_send_cmd_wait_ok(buf, 5000); // 6. 发送数据先发 CIPSEND等到 后发正文 esp_send_cmd_wait_ok(ATCIPSEND5\r\n, 1000); esp_uart_send((uint8_t*)hello, 5); esp_send_cmd_wait_ok(ATCIPCLOSE\r\n, 1000); }这段代码里最值得说的细节是第 6 步的通知模式。ATCIPSEND5返回的不是OK而是esp_send_cmd_wait_ok内部会因找不到OK而超时——所以这里不能用等待 OK 的函数要新写一个esp_wait_prompt(, 1000)。很多初学者在这个指令上卡住就是没分清应答形态。ATCWJAP也一样它慢在 DHCP 阶段返回结果是WIFI CONNECTED、WIFI GOT IP和OK三行连在一起。strstr扫OK的时候没问题但如果网络环境里有其他OK出现比如模块状态上报就会误判。严谨的做法是等WIFI GOT IP或检测完整的行尾OK\r\n。4. 接收不定长数据HAL 库空闲中断与 ATIPD 报文解析4.1 把 HAL 库的接收事件变成一帧一回调CubeMX 生成的 HAL 库函数里DMA 循环接收需要配合空闲中断才能拿到“一帧结束”的边界。上一章给了 CubeMX 开启 DMA 后的接口这里补一个能直接用的完整初始化片段。要注意清空闲标志的顺序必须先读 SR 再读 DR否则标志清不掉会导致频繁进入中断。void esp_uart_init_dma_idle(UART_HandleTypeDef *huart) { // 使能空闲中断 __HAL_UART_ENABLE_IT(huart, UART_IT_IDLE); // 启动 DMA 循环接收固定收满整个缓冲区后自动从头开始 HAL_UART_Receive_DMA(huart, esp_rx_buf, sizeof(esp_rx_buf)); }空闲中断触发后从 DMA 计数寄存器拿到当前写位置就能把数据从环形缓冲区里完整拷贝出来。把这段逻辑封装好HAL 库的复杂度就被隔离了上层代码永远面对“完整的、已拼好的帧”。需要特别注意 DMA 半满和全满中断。如果开了HAL_UART_Receive_DMA但没处理 DMA 完成回调数据在绕回时没有影响但如果你在中断里调用HAL_UART_Receive_DMA重新启动反而会破坏环形缓冲区的连续性。所以这里刻意不调用任何 HAL 接收函数只在启动时调用一次。4.2 解析 IPD 报文按长度字段准确切片ESP8266 在非透传模式下收到 TCP 数据上报格式是IPD,len:payloadlen是 payload 的字节数payload 里可能含任意二进制内容包括\r\n和OK这些字符串。所以接收解析绝不能按行处理必须先用strstr定位IPD,再提取长度字段按长度切出 payloadvoid esp_frame_process(uint8_t *frame, uint16_t len) { uint8_t *p frame; uint8_t *end frame len; char len_str[8]; uint16_t payload_len; while (p end) { uint8_t *ipd my_strstr(p, end - p, IPD,); if (ipd NULL) break; // 跳过 IPD, ipd 5; // 解析长度数字直到冒号 uint8_t *colon my_strchr(ipd, end - (ipd), :); if (colon NULL) break; uint16_t digits colon - ipd; if (digits sizeof(len_str) - 1) break; // 长度字段异常保护缓冲 memcpy(len_str, ipd, digits); len_str[digits] \0; payload_len (uint16_t)atoi((char*)len_str); // 数据区是冒号后的 payload_len 个字节 uint8_t *payload colon 1; if (payload payload_len end) break; // 数据还没收完整等下一帧 user_tcp_data_callback(payload, payload_len); p payload payload_len; } }代码里的my_strstr和my_strchr是我在嵌入式里常用的两个函数作用是在指定内存范围里做字符串搜索而不越界避免调用标准库strstr时读到非字符串区域。两个都要求用户手工维护长度。特别注意payload payload_len end这个判断。TCP 分包是网络常态一个IPD报文可能被拆成两次串口接收到达。这时不要丢弃要保留半包数据等方法里的流程走到p继续时把已处理的数据拷贝到帧头等待剩余部分拼上来。最简单的做法是维护一个长度计数typedef struct { uint8_t buf[2048]; uint16_t count; uint16_t expected_len; uint8_t in_ipd; } esp_ipd_parser_t;解析器进入等待状态后每一帧到达都先填充buf直到count expected_len再触发用户回调。这个设计能同时处理粘包和半包是 TCP over UART 场景最实用的方案。4.3 透传模式的收发和退出方法TCP 长连接场景下逐帧ATCIPSEND的开销太大每次都要发指令、等提示符、再发数据实际吞吐率低而且 AT 指令和业务数据混在同一根串口线上解析更容易出错。常见做法是进入透传模式// 进入透传模式完成后所有串口数据直接走 TCP esp_send_cmd_wait_ok(ATCIPMODE1\r\n, 1000); esp_send_cmd_wait_ok(ATCIPSEND\r\n, 2000); // 返回 后进入透传退出透传固定用注意它没有回车换行也不能带其他字符干扰发送后等待 1 秒再继续发 AT 指令。这个时序如果不满足模块不会退出透传导致后续指令被当成 TCP 数据发给服务器。透传模式下接收侧不再有IPD头所有串口数据都是裸业务流解析逻辑更简单代价是失去了报文的长度边界如果 TCP 对端发送的数据是变长的你还需要在应用层自己定义协议边界比如固定头 长度字段。5. 掉线重连和 AT 指令的易错点排查5.1 ESP8266 模块常见不稳定现象根因连上 WiFi 后频繁复位这是供电问题不是代码问题。检查 ESP8266 的 3.3V 电源能否持续供给 300mA 以上验证手段是看重启瞬间串口日志如果在发射状态下复位日志会突然截断。模块和 STM32 的 GND 必须共地而且两根线尽量短粗避免地弹导致逻辑电平漂移。串口收不到回复一个常见原因是模块没跑在 115200。用 USB 转 TTL 单独接模块手动输入AT回车能返回 OK 就说明波特率正确乱码则说明固件被烧过其他波特率。另一种情况是 RX/TX 接反了这在调试时最容易忽视。AT 指令和固件版本密切相关。老版本固件用ATCWJAP设置 WiFi 参数新版本分成ATCWJAP_DEF参数保存到 flash和ATCWJAP_CUR临时生效。拿到模块先执行ATGMR查看固件版本再决定用哪条指令。如果你发现ATCWJAP总是返回ERROR而模块单独用串口工具发同样指令正常多半是 STM32 这边发送的数据不干净最常见的是少了行尾\r\n或混入了\n。5.2 掉线检测与指数退避重连策略ESP8266 掉线时串口会异步上报WIFI DISCONNECT或CLOSED。这类异步消息不响应任何请求只能被动接收。主循环里也可以主动查询连接状态两个手段配合才能做到及时感知void esp_keepalive_poll(void) { // 每 30 秒向服务器发一个空数据包保持链路 if (HAL_GetTick() - last_keepalive 30000) { esp_send_cmd_custom(ATCIPSTATUS\r\n, 1000); last_keepalive HAL_GetTick(); } } void esp_link_lost_handler(void) { static uint8_t retry_count 0; uint32_t delay_ms 1 (retry_count 5 ? 5 : retry_count); // 指数退避封顶 32 秒 esp_hard_reset(); // 先复位模块保证状态干净 HAL_Delay(delay_ms * 1000); esp_link_to_server(); // 重新走完整的联网流程 retry_count; }重连成功后把retry_count清零。指数退避是为了避免网络抖动时多台设备同时重连导致 AP 过载而且 WiFi 模块反复快速重启容易让路由器把它踢进黑名单。另外有个细节重连时不要只执行ATCWJAP重连 WiFi我的做法是直接硬件复位整个模块再跑一遍完整流程。因为 WiFi 掉线往往伴随着 TCP 连接状态残留只要模块不复位ATCIPSTART可能返回ALREADY CONNECTED的错误状态。硬复位虽然慢但最干净、最可靠。5.3 调优技巧把接收缓冲和数据解析拆到不同执行层级串口空闲中断里做的时间开销要严格控制。像esp_frame_process这种涉及strstr和atoi的解析我一般不直接放在中断服务函数里而是把原始数据拷到应用层缓冲区置一个标志位由主循环或 RTOS 任务处理。HAL 库回调里只做一次memcpy和变量赋值整体耗时控制在几微秒以内。这样才能保证串口不丢字节后续解析即使慢一点数据还在缓冲区里躺着。缓冲区大小按照最坏情况算波特率 115200 时串口速率约 11.5KB/s如果业务方一次上报 1KB 数据主循环响应周期 5ms那么缓冲至少要有 11.5 × 5 ≈ 58 字节余量。考虑到 TCP 可能一次拼多个包到达我惯用 2048 字节的接收环配合半包解析器基本能覆盖常见物联网关的负载。发数据前先检查环内的空间不足时等主循环消费完再继续避免覆盖未处理的数据。本文还有配套的精品资源点击获取