ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32+ESP8266:AT指令串口通信与状态机实战解析

STM32+ESP8266:AT指令串口通信与状态机实战解析 简介基于STM32的ESP8266 AT指令控制工程面向物联网入门与嵌入式开发者解决STM32与ESP8266模块间的串口通信、AT命令下发与响应解析等常见问题。工程基于STM32L4系列HAL库编写包含完整源码、头文件及工程配置文件代码模块划分明确便于直接移植或二次开发。压缩包共248个文件以c源文件和h头文件为主同时包含编译生成的o/d中间文件、bin固件、map映射表以及PDF说明文档整体约14.6MB文件类型覆盖源码、编译配置与参考文档。资源附带ioc与mxproject可导入STM32CubeMX查看引脚配置帮助理解硬件连接。目前已有1947人学习下载适合正在调试ESP8266网络通信或AT指令流程的开发者能从中快速定位串口初始化、指令拼接、超时等待等环节的错误思路减少反复试错成本。1. 为什么宁可用裸机 AT 指令也没给 ESP8266 刷 NodeMCU 固件拿到一块 ESP8266多数人第一反应是刷 NodeMCU 固件用 Lua 脚本或者直接在 Arduino IDE 里写程序连 SDK 都不用碰。但实际做产品时STM32 才是主控WiFi 模块只负责透传数据此时 AT 固件反而是最稳的方案AT 固件把 TCP/IP 协议栈、WiFi 驱动全封装好了STM32 只需要通过 UART 发文本指令就能完成配网、连接服务器、收发数据省掉了 ESP8266 内部 SDK 调试的麻烦也方便后期替换成其他 WiFi 模块。我带过的一个环境监测项目就是用 STM32L4 系列做主控跑着 HAL 库用三个 USART 分别接 ESP8266、GPS 和调试串口整个通信链路没在 ESP8266 端写过一行业务代码。这个工程里涉及的 stm32l4xx_hal_uart.c、stm32l4xx_hal_tim.c、stm32l4xx_hal_adc.c 等文件其实是标准 STM32L4 固件包的一部分真正核心的是你如何用 HAL 库把串口收发、超时管理、状态机这些环节组织起来。本文不贴整个工程源码而是把「STM32 通过 AT 指令控制 ESP8266」这条主链路拆开讲从串口初始化到指令发送从应答解析到断线重连每一步都给出可复现的代码和参数说明。适合正在做 STM32ESP8266 入门到进阶开发的工程师尤其是想把模块稳定地跑在产线上而不是只在开发板上亮灯的读者。2. 串口通信基石STM32L4 的 USART 初始化与数据收发模型2.1 为什么选 USART2 接 ESP8266而不是其他外设主控与 ESP8266 之间最直接的物理通道是 UART。STM32L4 的 LPUART、USART 都可以用但考虑到 ESP8266 的波特率通常在 115200且需要处理不定长数据帧我用的是USART2挂载在 APB1 上时钟配置灵活也支持 DMA。之所以不选 LPUART是因为低功耗串口在低速时虽然省电但驱动能力和 FIFO 深度有限调试阶段容易丢数据。接线方面很简单STM32 的 TXPA2接 ESP8266 的 RXDRXPA3接 ESP8266 的 TXD注意共地。GND 必须连否则电平参考不一致会出现乱码。ESP8266 的 CH_PDEN引脚要拉高否则模块不工作。很多新手卡在“AT 没反应”十个里有八个是 CH_PD 悬空。2.2 HAL 库的 UART 初始化参数与 DMA 配置代码这里给出基于 CubeMX 生成的 HAL 库代码但关键参数我会单独解释。串口初始化时要注意WordLength、StopBits、Parity必须和 ESP8266 默认 AT 固件一致8 数据位、1 停止位、无校验。如果用过其他波特率比如 9600要确认 ESP8266 端是否也改过否则开机回显乱码。// uart_dma_config.c 片段 void MX_USART2_UART_Init(void) { huart2.Instance USART2; huart2.Init.BaudRate 115200; huart2.Init.WordLength UART_WORDLENGTH_8B; huart2.Init.StopBits UART_STOPBITS_1; huart2.Init.Parity UART_PARITY_NONE; huart2.Init.Mode UART_MODE_TX_RX; huart2.Init.HwFlowCtl UART_HWCONTROL_NONE; huart2.Init.OverSampling UART_OVERSAMPLING_16; HAL_UART_Init(huart2); // DMA 接收环形缓冲避免频繁进出中断 __HAL_UART_ENABLE_IT(huart2, UART_IT_IDLE); HAL_UART_Receive_DMA(huart2, (uint8_t *)esp8266_rx_buf, ESP8266_RX_BUF_SIZE); }这段代码里最关键的不是波特率而是开启了UART_IT_IDLE空闲中断。DMA 会把串口数据源源不断搬进esp8266_rx_buf但不告诉你“这一帧数据到哪结束”。IDLE 中断在数据流出现空闲一个字节时间没有新数据时触发正好用来标记一帧 AT 应答接收完成。ESP8266_RX_BUF_SIZE我一般设 512AT 应答最长的是查询固件版本或 CIPSTATUS都不会超过这个数。2.3 环形缓冲区的设计与数据帧切片DMA 配合空闲中断会产生一个问题缓冲区是环形的数据可能被覆盖。所以收到 IDLE 中断后要算出本次数据长度然后立刻搬出到另一个线性 buffer。计算方式是两个指针差当前 DMA 计数器与上次保存的计数器。// stm32l4xx_it.c 中的 USART2_IRQHandler void USART2_IRQHandler(void) { if (__HAL_UART_GET_FLAG(huart2, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart2); uint16_t cur __HAL_DMA_GET_COUNTER(huart2.hdmarx); uint16_t len last_rx_pos - cur; if (len 0) len ESP8266_RX_BUF_SIZE - last_rx_pos cur; // 处理环绕 if (len 0) { // 将环形缓冲中的 len 字节拷贝到独立的解析缓冲 memcpy(esp8266_parse_buf, esp8266_rx_buf last_rx_pos, len); esp8266_parse_len len; last_rx_pos cur; } } }注意last_rx_pos初始是ESP8266_RX_BUF_SIZE而不是 0__HAL_DMA_GET_COUNTER返回的是剩余未传输字节数当 DMA 从头开始写时它的值从 BUF_SIZE 递减。这里有个容易写错的点如果 DMA 满了会覆盖整个缓冲但正常情况下 115200 波特率下一帧完整应答最多几十字节512 的缓冲完全够用。如果你发现偶尔丢帧优先检查是不是没处理环形环绕而不是加缓冲大小。3. AT 指令状态机发送、等待应答、超时与重试机制3.1 不要把收发逻辑写在主循环里裸等最简单的做法是HAL_UART_Transmit发 AT\r\n然后HAL_UART_Receive等待回显 OK。这在开发板上能跑但一旦 WiFi 连接不稳定阻塞等待会把整个主流程卡死。我的做法是建立一个分层状态机命令发送层只负责把指令交给 DMA应答解析层通过串口回调接收完整帧后更新状态变量主循环每 10ms 轮询一次状态。每个 AT 指令对应一个命令结构体包含指令字符串、期望应答串、超时时间、重试次数和回调函数。// at_cmd.h typedef struct { const char *cmd; // 要发送的AT指令如 ATCWJAP\ssid\,\pwd\\r\n const char *expect; // 期望收到的应答子串如 OK uint32_t timeout_ms; // 超时时间 uint8_t retry; // 重试次数 uint8_t state; // 0: idle, 1: sent, 2: matched, 3: timeout void (*done_cb)(uint8_t err); } at_cmd_t;这种结构最大的好处是每个指令带独立的超时和重试比如连接 AP 的指令ATCWJAP可能耗时 5~10 秒而查询信号强度的ATCWSIGNAL只需要几百毫秒如果混用固定超时会让系统响应变慢。3.2 应答匹配从简单 strstr 到多关键字回调解析 AT 应答最直接的方法是用strstr找关键字比如OK、ERROR、IPD。但真实场景下一条ATCIFSR的应答可能多行而且OK可能出现在中间也可能出现在最后ATCIPSTATUS的返回在连接状态下还包含CIPSTATUS: 0,3,0,0这样带数字的状态码。更可靠的方式是把关键字表注册进回调匹配时按优先级检查错误关键字再检查成功关键字。// at_parser.c static const char *err_keywords[] { ERROR, FAIL, DNS Fail, Link is End, NULL }; int at_parse_response(const char *buf, uint16_t len, const char *expect) { for (int i 0; err_keywords[i] ! NULL; i) { if (strstr(buf, err_keywords[i]) ! NULL) { return AT_RESULT_ERR; // 一旦命中错误关键字直接结束 } } if (expect ! NULL strstr(buf, expect) ! NULL) { return AT_RESULT_OK; } return AT_RESULT_PENDING; // 没匹配完继续等待后续数据 }注意strstr匹配OK时会遇到一个坑如果 buf 里有OK123这样的内容也会匹配到而 AT 固件一般不会有这种输出但以防万一可以加边界判断比如检查OK后面是\r、\n或字符串结束。另外有些固件会回显你发送的指令比如你发AT\r\n它先回AT\r\n\r\nOK\r\n所以匹配前先决定是处理整包还是逐行处理。我是直接把整帧给解析函数然后先剔除回显部分比如找\r\n之后的位置再开始匹配关键字。3.3 超时和重试的调度实现超时不能靠HAL_Delay否则阻塞期间又有新的串口数据进来状态机全乱了。我习惯用一个 1ms 的 tick 计数器在发送指令时记录start_tick每次主循环轮询时检查now - start_tick timeout_ms是否成立。如果超时重试计数减一再次发同一指令重试耗尽则回调错误。这里给出调度核心void at_state_machine_tick(uint32_t now_ms) { at_cmd_t *c active_cmd; if (c-state ! 1) return; if (c-state 1) { // 已发送等待应答 if (at_parse_response(esp8266_parse_buf, esp8266_parse_len, c-expect) AT_RESULT_OK) { c-state 2; if (c-done_cb) c-done_cb(0); } else if (at_parse_response(...) AT_RESULT_ERR || (now_ms - c-start_tick c-timeout_ms)) { if (c-retry 0) { c-retry--; at_send_raw(c-cmd); // 重新发送 c-start_tick now_ms; } else { c-state 3; if (c-done_cb) c-done_cb(1); } } } }这个函数会每秒被主循环调用 100 次左右开销主要花在strstr上字符串长度最多几百字节完全能接受。这里有一个隐藏坑重新发送指令前必须清空esp8266_parse_buf和esp8266_parse_len否则上一次残留的OK会和新应答混在一起导致误判。4. 实战STM32 控制 ESP8266 连接 WiFi 并和 TCP 服务器通信4.1 上电后的第一步进入透明传输模式前的准备ESP8266 上电后默认是 AT 指令模式要进入透传ATCIPMODE1才能把串口收到的数据直接转发到 TCP 服务器。但透传模式下不再回显OK所以失败排查很麻烦。我的建议是先不要开透传用普通模式把链路跑通再切换到透传。下面是完整指令序列包含每步的预期应答和超时参数。序号指令预期关键应答超时(ms)说明1ATOK500检查模块是否响应2ATE0OK500关闭回显减小解析负担3ATCWMODE1OK500Station 模式连接外部路由4ATCWJAPssid,passwordWIFI GOT IP10000连接 AP注意转义引号5ATCIPSTARTTCP,192.168.1.100,8080CONNECT OK5000建立 TCP 连接6ATCIPMODE1OK500开启透传模式7ATCIPSEND1000进入透传发送状态第 4 步最容易出错ATCWJAP的参数里如果密码包含特殊字符需要原始字符串拼接不能用snprintf多次格式化否则引号位置错一位就连接失败。另外有些路由器的 5GHz 频段 ESP8266 不支持必须连接 2.4GHz 网络。我在代码里会先检测应答是否包含FAIL如果包含立刻重试而不是傻等WIFI GOT IP。4.2 发送指令的正确姿势逐字节发还是整包发很多 STM32 示例用HAL_UART_Transmit把整个字符串一次发出这在低速 9600 波特率下没问题115200 波特率下 ESP8266 的串口 FIFO 也能承受。但如果你用 DMA 发送要小心HAL_UART_Transmit_DMA和普通模式在同一串口上混用会导致句柄状态错乱。所以我统一用阻塞发送因为 AT 指令很短最多几十字节115200 波特率下发送 50 字节耗时不到 5ms完全不影响实时性。void at_send_raw(const char *cmd) { uint16_t len strlen(cmd); // 确保发送期间不被中断被打断到过长 while (__HAL_UART_GET_FLAG(huart2, UART_FLAG_TXE) RESET); HAL_UART_Transmit(huart2, (uint8_t *)cmd, len, 100); }有经验的工程师可能会问为什么不用 DMA 发送因为 DMA 发送时需要保证缓冲区在传输完成前不被修改而字符串常量或静态数组没问题但如果是一条动态拼接的指令比如ATCIPSTART的 IP 和端口拼接后存入局部数组DMA 异步发送后局部数组已经弹出栈数据就被破坏了。所以除非你用static数组否则阻塞发送更安全。4.3 透传模式下的双向数据处理如何同时收串口和收 TCP 数据进入透传后ESP8266 会把远端服务器发来的数据直接通过 UART 输出不会加任何前缀。此时 STM32 收到的数据就是纯业务数据但你的程序仍然需要解析 UART 数据流。这意味着此前写的 AT 应答状态机在透传模式下要暂时关闭改为把串口收到的数据原样转发给业务队列。// 透传模式下的串口处理 void esp8266_passthrough_process(uint8_t *data, uint16_t len) { // 转发到业务队列比如存储到环形 buffer 或发送到下一个外设 for (uint16_t i 0; i len; i) { ringbuf_put(tcp_rx_queue, data[i]); } }这里要注意的是透传模式下 ESP8266 仍然会输出OK等响应吗不会。但如果你在透传中发并按 1 秒间隔发送模块会退出透传回到 AT 模式并回显OK。这个退出序列在代码中要预留接口后续要重连或改配置时必须用到。我建议把透传模式和 AT 模式做成两个独立的状态串口 IRQ 里根据状态选择不同的分发函数而不是混合在一起。4.4 断线检测与重连策略服务器断开连接时ESP8266 在透传模式下会输出\r\n\r\nERROR: CLOSED\r\n在普通 AT 模式下会输出CLOSED或Link is End。如果没处理这个信号你的主控会以为连接还活着继续向服务器发数据结果全部丢弃。合理的策略是定义一个连接状态变量在解析函数里捕获CLOSED或Link is End关键字同时启动一个 TCP 保活定时器。// 连接状态监测 if (strstr(esp8266_parse_buf, CLOSED) ! NULL || strstr(esp8266_parse_buf, Link is End) ! NULL) { tcp_connected 0; at_send_raw(ATCIPSTART\TCP\,\192.168.1.100\,8080\r\n); }重连时有两个细节第一ATCIPSTART之前必须确认模块已经退出透传模式否则指令不会被解析。第二重连失败不能无限重试我一般设最大 5 次间隔 5 秒超过就把整个 WiFi 链路重启从ATCWJAP重新来过。这种“先重连 TCP再重连 AP”的降级策略比一上来就重启整个模块更可靠也更容易在日志里定位问题。5. 进阶用串口回环法验证整个链路定位 AT 指令无响应的坑最后分享一个我常用的验证方法。很多人在调 STM32ESP8266 时第一步就卡在发送AT没反应于是怀疑代码、怀疑接线、怀疑模块坏了。其实最快的排查办法是绕过 ESP8266把 STM32 的 TX 和 RX 直接用杜邦线短接注意不要接错引脚然后用串口调试助手或直接在代码里发一条AT\r\n如果串口能收到自己的回显说明 UART 初始化、中断、DMA 全部正常。# 验证 STM32 串口回环 stm32_tx - 连接到 stm32_rx # 发送字符串 AT\r\n在逻辑分析仪或串口助手中应看到相同内容我遇到的 70% 的“AT 无响应”问题最终都出在接线和供电上。ESP8266 的峰值电流可以达到 300mA如果用 STM32 开发板的 3.3V 引脚直接供电电压会被拉低到 3.0V 以下模块表现为偶尔能启动、偶尔完全无响应。正确做法是给 ESP8266 单独配一个 AMS1117-3.3 稳压芯片输入 5V输出 3.3V且输出端加一个 470uF 电解电容同时尽量让 STM32 的 TX 引脚与 ESP8266 的 RX 之间加一个 1kΩ 串联电阻做电平缓冲。ESP8266 的 RX 是 3.3V 逻辑STM32L4 也是 3.3V不需要电平转换但要注意 STM32 的某些板子用的是 5V 供电IO 输出高电平可能超过 3.3V长期使用会损伤 ESP8266。另一个进阶技巧是不要依赖笼统的OK应答而是用ATCIPSTATUS主动查询链路状态。这个指令返回类似CIPSTATUS: 0,3,0,0的内容第二位数 3 表示 TCP 连接已建立0 表示未连接。我一般在每次发送业务数据前调用一次ATCIPSTATUS如果状态不对就提前走重连逻辑而不是等数据发出去后才收到CLOSED才反应。当然这条指令会占用一定的时间大约 10ms 左右对于大多数非实时场景足够了。最后如果你的工程里用到了stm32l4xx_hal_tim.c可以顺带用定时器产生一个 1ms 时基给状态机做超时管理用stm32l4xx_hal_adc.c采集电压来监测 ESP8266 供电是否稳定用stm32l4xx_hal_i2c.c外接 OLED 显示调试信息。透传模式下把这些外设和串口状态机整合起来就是一个很完整的 STM32 物联网节点了。本文还有配套的精品资源点击获取
返回列表