☰
STM32+ESP8266 MQTT通信实战:AT指令、联网与断线重连
2026/11/2 4:48:26 网站建设 项目流程

手里有一块 STM32 最小系统板,外加一颗十几块钱的 ESP8266 WiFi 模组,想把采集到的温湿度、开关状态推到自建的消息服务器上,再用手机或者电脑订阅下来看——这套组合几乎是入门物联网最省钱、也最能学到东西的路子。这里要聊的就是STM32 配合 ESP8266 WiFi 模块做 MQTT 通信的完整实现过程:从接线、供电、串口收发的底层细节,到 AT 指令一条条怎么发、参数怎么算、断线了怎么自愈,再到联调时那些让人抓狂的坑怎么排查。适合已经能点亮 LED、能跑通串口打印,但对"我的板子怎么上网"这件事还只有一个模糊概念的朋友;也适合做过 HTTP 请求、想换成更省流量、更适合长期在线的 MQTT 协议的人。

我不打算把这篇写成一份指令手册——手册官方文档里有,而且比我写得全。我想讲的是手册里不会写、但我自己焊过板子、烧过固件、半夜盯着串口日志才搞明白的那些东西。

1. 整体方案设计:为什么让 STM32 去"指挥"一颗 WiFi 模块

1.1 需求拆解:一块板子要联网,到底缺什么

先把问题拆干净。STM32 本身没有网络能力,它的外设里只有 USART、SPI、I2C、ADC、定时器这些东西,物理层和链路层的射频部分它一概没有。所以"让 STM32 上网"这句话,本质上等价于"给 STM32 配一个能和外界交换字节的通道,然后在 STM32 上跑一套协议栈去理解这些字节"。

这个通道可以是有线的,比如加一颗以太网 PHY 芯片走 RJ45;也可以是無线的,比如挂一颗 WiFi 模组走串口。选后者的理由很实际:布线自由、成本低、模组自带射频校准和协议栈,开发者不用碰 RF 那一摊子事。

通道有了之后,协议栈在哪一端跑就成了关键决策。ESP8266 这类模组出厂固件里已经内置了 TCP/IP 协议栈,很多版本还内置了 MQTT 客户端。这意味着 STM32 完全可以把"联网"这件事整个外包出去,自己只负责通过串口下达命令、读取结果。这种分工方式在嵌入式里非常常见,叫"主控 + 通信协处理器",本质上和当年手机基带处理器分管通话、应用处理器分管界面是一个思路。

我选择这条路的核心原因就一条:把复杂度和不确定性隔离到一个可以被"重启"的器件上。WiFi 连接会掉、信号会衰减、路由器会重启、服务器会踢人,这些在应用层看来都是随机事件。如果协议栈跑在 STM32 上,一次 TLS 握手失败可能就把主循环卡住几百毫秒;而如果跑在模组里,最坏情况我复位一下模组,主控该采数据的采数据、该点灯的點灯,一点不受影响。

代价当然也有。串口是个慢速通道,115200 波特率下理论极限也就 11.5KB/s,实际有效吞吐还得打折;AT 指令是纯文本的,每条命令都有固定开销;命令和应答是异步的,你必须自己维护状态机去匹配。但对我们这种"每秒发几十字节传感器数据"的场景,这些代价完全可以接受。

1.2 三条技术路线的取舍:AT 指令、透传固件、模块内置 MQTT

同样是挂 ESP8266,玩法差别很大,我在不同项目里三条路都走过,说说各自的体感。

第一条是透传模式。模组配好之后进透传,串口上敲什么它就直接发到 TCP 连接里,等于把一条 TCP 连接变成了"串口延长线"。优点是 STM32 侧代码极简,不用解析任何 AT 应答;缺点是状态不可见——你根本不知道连接是活着还是死了,只能靠发数据没反应来猜,而且一旦断了,模组往往不会主动告诉你,得靠你自己定个超时去复位它。适合做一次性数据上报的小玩具,不适合要长期在线的设备。

第二条是标准 AT 指令模式,逐条下发AT+CWJAP、AT+CIPSTART、AT+CIPSEND,自己去实现 MQTT 报文的组包和解析。技术上最灵活,能在 STM32 上真正理解每条报文;但工作量最大,MQTT 的固定头、可变头、剩余长度变长编码、QoS 确认这些你都得自己写,调试起来也很痛苦——报文字节错一位,服务器直接不认。

第三条是模组内置 MQTT 客户端,用AT+MQTTUSERCFG、AT+MQTTCONN这套专用指令。这是我最推荐新手上手的路径:协议栈细节全在模组里,STM32 侧只要拼几条指令字符串、再从串口里把+MQTTSUBRECV上报的数据抠出来就行。代价是灵活性受限——你要发的主题、载荷格式必须能在一条 AT 指令里表达完,复杂的 JSON 嵌套或者大文件传输就不太合适。

三条路线放一起对比一下更清楚:

方案STM32 侧工作量连接状态可见性掉线自恢复适合场景
透传模式极低差,基本靠猜需外部干预一次性上报、Demo 验证
标准 AT + 自组 MQTT 报文高,需实现报文解析中,靠AT+CIPSTATUS轮询中等需要精细控制报文、定制协议
模组内置 MQTT 客户端中,主要是字符串拼接与解析好,有心跳和+MQTTDISCONNECTED上报好,可开自动重连传感器周期上报、设备控制

本文走的是第三条。顺带说一句,如果你手上的模组固件太老,不支持 MQTT 指令族,第一件事是去查当前固件版本号,而不是硬着头皮走第二条路——升级固件花的时间,远少于你自己实现 MQTT 报文解析并调通所花的时间。

1.3 数据链路与任务划分

把整条链路画清楚,后面所有细节才有位置安放。从传感器到最终用户手机,数据要经过这么几段:

  • 传感器(I2C 或 ADC)→ STM32 应用层拿到原始值
  • STM32 应用层 → 组包成约定好的报文格式(我一般用极简的 JSON 或纯文本键值对)
  • STM32 串口发送 → ESP8266 串口接收
  • ESP8266 把载荷封装成 MQTT PUBLISH 报文 → TCP → WiFi → 路由 → 服务器
  • 服务器把消息分发给订阅了同一主题的其他客户端

反方向的控制指令走的是同一条路,只不过角色互换:用户发布到控制主题,模组收到后通过+MQTTSUBRECV上报给 STM32,STM32 解析后决定开灯还是关继电器。

在 STM32 这端,我习惯把代码分成三层,这样改起来不牵一发动全身:

  1. 驱动层:串口收发、环形缓冲区、毫秒计时。这一层只关心"有没有字节进来",不理解字节是什么意思。
  2. 模组抽象层:把"连接热点""连接服务器""发布消息"包装成函数,内部处理 AT 指令的下发和应答匹配。调用者看到的是wifi_join_ap("ssid", "pass")这样的接口,看不到AT+开头的字符串。
  3. 应用层:采集数据、组包、定时发布、解析下行指令、处理业务逻辑。

分层的价值在排查问题时体现得最明显。日志里出现乱码,你不用怀疑业务逻辑,直接怀疑第一层;日志里 AT 指令回的是ERROR,那就是第二层的参数或时序问题;一切正常但服务器收不到数据,那才轮到检查组包格式和主题拼写。

注意:分层不是为了好看,是为了让故障域收敛。我曾经把 AT 指令直接写进业务函数里,后来换了个固件版本、指令参数变了,改了几十处,那个下午我记到现在。

2. 硬件连线与串口通信的底层细节

2.1 供电与电平:最容易翻车的地方

先讲供电,因为这是新手翻车率最高的一环,而且现象极具迷惑性——代码没错、指令没错,就是莫名其妙重启、连不上、发几条数据就断。

ESP8266 在发射瞬间的电流是脉冲式的,平均电流可能只有七八十毫安,但峰值能冲到三百毫安以上,持续时间很短。如果你的 3.3V 电源是从 STM32 板载稳压器上"顺"出来的,而板载稳压器本身还要给 STM32 和若干外设供电,那这个瞬态压降足以让模组内部欠压复位。表现就是:串口上突然出现一堆启动乱码(那是模组重启打印的启动信息),然后你的连接没了。

我自己的做法是给模组单独一路 3.3V,输入侧并一个 470μF 的电解电容、输出侧并一个 0.1μF 的瓷片电容。电解电容负责扛住毫秒级的电流冲击,瓷片负责滤掉高频毛刺。如果只能共用一路电源,那至少也要保证这路稳压器的额定电流在两倍以上,并且电容一定要加。这个改动花不了几毛钱,但能省掉你一整个晚上的排查时间。

电平方面,STM32 和 ESP8266 都是 3.3V 逻辑,直连没有问题,不需要电平转换。但有一类模组小板自带 USB 转串口芯片,它的 RX 引脚可能通过分压电阻接到 5V 兼容电路上,这种板子直接接 STM32 的 TX 有时会有轻微串扰,实测多数能用,但如果通信不稳,可以先量一下空闲电平是不是干净的 3.3V。

接线我一般固定这么接,形成肌肉记忆:

STM32 引脚ESP8266 引脚说明
3.3V(独立 LDO 输出)VCC / 3V3必须独立供电或加强滤波
GNDGND共地,务必接牢
USARTx_TXRX交叉连接
USARTx_RXTX交叉连接
任意 GPIO(推挽输出)RST / CH_PD用于软件复位与使能
可选 GPIOGPIO0需要进烧录模式时拉低

这里有两个细节值得说。第一,CH_PD(有些板子标EN)必须拉高,很多模组板上已经有上拉电阻,但如果你买的是裸模块,忘了拉高就会表现为"完全没反应"。第二,把RST接到一个 GPIO 上非常值得,它是你实现"软复位自愈"的物理基础,后面第 6 节讲重连时会用到。

2.2 串口接收设计:定长、空闲中断还是环形缓冲

串口怎么收数据,决定了整个项目稳不稳。新手最常写的代码是"发完指令,HAL_Delay(500),然后读缓冲区",这种做法在实验室能跑,一上现场就废——因为模组什么时候回、回多长,完全不可控。

三种接收方式我都试过,说说体感。

定长 DMA 接收:优点是零 CPU 占用,配置简单;缺点是长度必须固定,而 AT 应答是变长的,短了截断、长了丢包,非常别扭。除非你只做单向透传,否则不推荐。

空闲中断 + DMA:串口检测到一帧结束(总线空闲一个字符时间)触发中断,此时把 DMA 已收到的字节数读出来。这在 STM32 上非常好用,因为 AT 应答天然是"一帧一帧"的——模组发完一段就停,正好触发空闲。缺点是对"一帧里含有多个逻辑行"的情况要再拆一次,比如一次上报里既有+MQTTSUBRECV又有OK。

逐字节中断 + 软件环形缓冲:CPU 开销略高,但完全灵活,不会因为 DMA 长度没配好而丢数据。115200 波特率下每秒最多 11.5K 次中断,对主频几十兆的 STM32 来说毫无压力。我现在基本都用这个方案,配合一个够大的环形缓冲,稳妥。

环形缓冲有个经典 bug:写指针追上读指针时要判断是"满"还是"空",很多实现只比较指针相等,结果缓冲区满和缓冲区空被判成同一种状态。正确的做法是留一个空位不用,或者额外维护一个计数字段。另外,如果缓冲区在中断里写、在主循环里读,读写指针要用volatile修饰,并且保证指针更新的顺序不会让主循环读到半截数据——稳妥做法是中断里只更新写指针,而且更新前后用临界区保护一两个指令周期。

缓冲多大合适?我给的经验值是:至少能装下三倍于最长单条应答的长度。典型 AT 应答几十字节,MQTT 下行载荷如果是几十字节的 JSON,整条上报也就一两百字节,那么 1024 字节的环形缓冲足够。但如果你要收固件升级数据或者图片,那就得按最大传输块来算,别硬套。

2.3 复位与状态引脚的正确用法

RST引脚的用法有讲究。这个引脚是低电平复位的,所以要复位时拉低、保持至少几十毫秒、再拉高。拉高的瞬间模组开始启动,会往串口吐一大段启动信息,里面混杂着各种波特率的乱码(因为启动初期波特率还没切换到你的设定值),然后才稳定下来。所以你的解析逻辑必须能容忍垃圾数据——遇到不认识的行直接丢,不要在解析函数里因为一行乱码就返回错误、把状态机打乱。

我通常的流程是:上电后先硬复位一次,然后清空环形缓冲,再等两秒左右(给模组启动留足时间),然后发一条AT,如果两秒内收到OK就认为模组活了,否则重试,重试三次还不活就报硬件故障。

另外,AT+RST这条软复位指令也能达到类似效果,但它依赖模组当前还"听得懂"指令。如果模组已经卡死(比如内部死循环或者内存耗尽),软复位指令是进不去的,这时候只有拉物理RST引脚管用。所以两套都要留。

提示:不要在主循环里频繁拉RST。模组每次重启都要重新连热点、重新连服务器,一次完整的恢复可能要十几秒。频繁重启会让你的设备看起来一直在"抽搐",反而更难连上。建议加个退避机制,后面会讲。

3. ESP8266 侧配置:从连上热点到连上 Broker

3.1 基础指令链路与版本确认

动手写代码之前,先用手边的串口调试工具,用 115200、8N1 的配置把模组单独打通一遍。这一步不能跳,因为你在电脑上验证过的指令序列,才是后面写进代码的可靠基准。

第一条永远是AT,回的OK说明波特率和接线对了。如果没反应,先试几个常见波特率(9600、74880、115200),再检查CH_PD有没有拉高。如果是乱码而不是没反应,那基本是波特率不匹配,模组出厂常见 115200 和 9600 两种,老固件还有 74880 这个奇怪的调试波特率。

接着关掉回显:ATE0。这一步很关键,因为开着回显的话,你发出去的每条指令都会被模组原样再吐回来一遍,你的应答匹配逻辑就会被自己发的指令误触发,调试时看起来像"指令还没执行就返回 OK 了"。关掉之后串口就干净了。

然后查版本:AT+GMR。这个指令会回三行信息,分别是 AT 固件版本、SDK 版本、编译时间。你要确认的是 AT 固件版本足够新、支持 MQTT 指令族。常见的判断方式是直接发一条AT+MQTTUSERCFG=?,如果回的是一串参数说明,说明支持;如果回ERROR,那就是版本不支持,得先升级固件。

联网一共三步:设置模式、连接热点、查 IP。

AT+CWMODE=1 // 1 = Station 模式,只做客户端 AT+CWJAP="你的热点名","密码" // 连接,成功会回 WIFI CONNECTED / WIFI GOT IP / OK AT+CIPSTA? // 查询拿到的 IP,确认真的拿到地址了

AT+CWMODE有三个取值:1 是 Station(作为客户端连别人),2 是 SoftAP(自己当热点),3 是两者共存。我们只做客户端,用 1 就够,模式越简单越不容易出问题。

AT+CWJAP是整条链路里最容易失败的一条。它需要真实扫描、认证、关联、DHCP 拿地址,整个过程可能持续几秒到十几秒。如果你的串口接收超时设得太短(比如只给 1 秒),就会误判为失败。我一般给它 15 秒的超时窗口。

这条指令的失败分两类,要区分开。一类是返回FAIL,通常意味着密码错了、热点不在范围内、或者热点用了它不支持的加密方式(某些企业级认证或者只允许 5GHz 的路由器就会这样)。另一类是压根没返回,通常意味着模组正在反复尝试重连,或者卡在 DHCP 阶段。前者查配置,后者可以发一次AT+CWQAP断开再重试,或者干脆硬复位。

还有个隐蔽的坑:热点名称或密码里含有逗号、引号、反斜杠。AT 指令用逗号分隔参数,你的密码里如果有个逗号,模组会把它当成参数分隔符,直接返回ERROR。这种情况需要在字符前加转义反斜杠。我见过有人密码里带了个逗号,排查了两小时最后发现是这个问题。

3.2 MQTT AT 指令族参数逐条拆解

连上热点之后,就可以配置 MQTT 客户端了。以常见的支持 MQTT 指令的固件为例,一共四条核心指令,我一条条拆。

第一条是客户端配置:

AT+MQTTUSERCFG=0,1,"stm32_node_01","user","passwd",0,0,""

第一个参数是链路 ID,固定填 0(多数固件只支持一条 MQTT 链路)。第二个是协议方案:1 表示 MQTT over TCP 明文,2 表示 MQTT over TLS 但不校验证书,3 表示 TLS 且带证书校验。第三个是 Client ID——这个必须全局唯一,如果两个设备用了同一个 ID,服务器会把先连的那个踢掉,现象就是"两个设备轮流在线",非常迷惑。我习惯用"项目前缀 + 芯片唯一 ID 后几位"来生成。

第四、五个是用户名和密码,如果你的服务器不需要认证,传空字符串就行,但要注意引号不能省,得写成""。第六、七个是证书相关,明文连接时都填 0。最后一个是路径,一般留空。

第二条是建立连接:

AT+MQTTCONN=0,"192.168.1.100",1883,1

第二个参数是服务器地址,可以填 IP 也可以填域名,填域名的话模组内部会做解析,但需要你先配好 DNS(多数固件会自动用 DHCP 下发的 DNS)。第三个是端口,明文一般是 1883。第四个参数是自动重连标志,填 1 表示连上之后如果链路断了模组会自己尝试重连,这个一定要开——这是你实现"自愈"的第一道保险。

连接成功的标志是收到OK,如果失败会回ERROR或者+MQTTCONN:0,失败原因码。常见原因:服务器地址不通(先 ping 一下)、端口被防火墙挡了、Client ID 重复、用户名密码错、服务器要求 TLS 而你用了明文。我建议刚上手时先在同一局域网里跑一个开源的消息代理服务,把变量降到最少,确认通了再换成远端。

第三条是订阅:

AT+MQTTSUB=0,"cmd/node01",1

第三个参数是 QoS。QoS 0 是"最多一次",发了就不管,丢了也不知道;QoS 1 是"至少一次",需要服务器回确认,可能重复但不会丢;QoS 2 是"恰好一次",握手最复杂。传感器上报用 QoS 0 就够了,反正下一秒还有新数据;控制指令建议用 QoS 1,因为丢一条控制指令用户会觉得很莫名。

第四条是发布:

AT+MQTTPUB=0,"data/node01","{\"t\":25.3,\"h\":61}",0,0

第三个参数是载荷。这里有个非常容易踩的坑:如果载荷里含双引号,必须转义成\",否则模组会把它当字符串结束符。我第一版发的 JSON 就是这么挂的,串口上回ERROR,服务器那边啥也没收到。后来改成用单引号包 JSON 的内部键,或者干脆用逗号分隔的纯文本协议,世界清静了。

第四个参数是 QoS,第五个是 retain 标志。retain 设为 1 时,服务器会保存这条消息,之后任何新订阅者一连上就能立刻收到最后一条状态——对"设备当前状态"这种主题特别合适,新开的客户端不用等下一次上报就知道灯是开是关。

3.3 订阅与发布的实操细节

主题设计这块,我踩过的坑比代码还多。有两条原则值得记住。

一是不要在主题里塞动态变化的值。有些人喜欢把时间戳或者随机数放主题里,结果每发一条就产生一个新主题,服务器上主题数量爆炸,而且订阅方根本没法用通配符匹配。主题应该是层级稳定的,比如项目/设备类型/设备ID/数据类型,具体数值全放载荷里。

二是上下行用不同前缀。我用data/前缀走设备上报,用cmd/前缀走用户下行指令,中间加个保留主题做在线状态。这样在服务器侧配置权限时非常清晰,也方便排查——一看主题就知道方向。

载荷格式上,JSON 可读性好但体积大,一条{"temperature":25.3,"humidity":61.2}就三十多个字节;纯文本25.3,61.2只有九个字节。在串口这个慢速通道上,体积差异是实打实的。我的做法是:调试阶段用 JSON,方便肉眼看;上线前如果发现串口成为瓶颈,再换成紧凑格式。别一上来就为了省那几个字节把可读性牺牲掉,调试阶段的痛苦会加倍还回来。

还有一点常被忽略:上电后要先订阅,再发布。如果你先发布,可能服务器还没来得及处理你的订阅请求,导致你错过了自己刚发的消息(如果你订阅了自己发布的主题做回环测试)。虽然听起来傻,但我真的在这个顺序上纠结过半小时。

4. STM32 侧代码框架:把 AT 指令包成可复用的接口

4.1 指令发送与应答匹配的状态机

裸写 AT 指令的代码通常长这样:发送字符串、延时、检查缓冲区里有没有OK。这种写法的问题是它在假设模组的响应是同步的,而实际上 AT 协议是异步的——你发了指令,模组可能立刻回,可能等三秒回,也可能先回一段主动上报(比如另一个订阅主题的消息到了)再回OK。

正确的做法是状态机加超时。我把每条指令抽象成一个结构:要发的字符串、期望的成功标志、期望的失败标志、超时时间、当前状态。发送时把状态置为"等待应答",然后主循环里轮询,收到匹配的成功标志就置"成功",收到失败标志或超时就置"失败"。

匹配的时候要小心子串误匹配。比如你等的是OK,但缓冲区里先来了一条内容含OK的下行消息(比如某个传感器状态叫OK),你的状态机就会误判成功。解决办法是匹配时带上行首或者+前缀,比如匹配\r\nOK\r\n而不是裸的OK。我一般让解析器按行切分,每行单独比对,这样误判率极低。

超时值要给够但不能太长。AT给 500ms,AT+CWJAP给 15s,AT+MQTTCONN给 10s,AT+MQTTPUB给 5s。给太短会误判失败,给太长会让整个设备在异常时"卡住"太久,影响其他任务的实时性。所以我从不写阻塞式等待,而是让状态机跑在主循环里,超时到了就跳到下一个状态,主循环始终是转的。

4.2 环形缓冲区与解析实现

接收部分我按行解析。核心思路是:串口中断往环形缓冲写,主循环每次取一行出来(遇到\r\n就认为一行结束),把这一行交给分发函数判断类型。

行长度要有上限,我设 256 字节。如果一行超过这个长度还没遇到换行,说明要么是垃圾数据流,要么是超长载荷,直接清空当前行缓存重新开始——比无限拼接然后内存溢出好得多。

分发函数按行首特征判断:以+MQTTSUBRECV:开头的是下行数据,以WIFI开头的是连接状态变化,以+MQTTDISCONNECTED开头的是掉线通知,OK/ERROR/FAIL是通用应答。剩下的当垃圾丢掉。

解析+MQTTSUBRECV这行要特别小心,因为它格式是<前缀>,<链路ID>,<主题>,<长度>,<载荷>,而载荷本身可能包含逗号!如果你简单地按逗号切分,载荷里有逗号就会被切碎。正确做法是:先按逗号切出前四个字段拿到长度,然后从当前位置往后精确取"长度"个字节作为载荷,剩下的不管。这个细节我第一次写的时候没注意,结果一条含逗号的 JSON 被解析成两半,找了好久。

4.3 应用层:报文封装与心跳

应用层其实很简单,主要是组包和定时。我一般用一个软件定时器(或者干脆在主循环里比对毫秒计数)控制上报周期,两秒一次对大多数环境监测项目够用。

组包时要注意不要用sprintf往固定数组里怼而不检查长度。sprintf不知道目标缓冲区多大,超了就越界,表现是随机崩溃或者把相邻变量改掉——这种 bug 极难查。用snprintf并且把缓冲区大小作为参数传进去,返回值大于等于缓冲区大小时说明被截断了,要处理。

心跳这件事,MQTT 协议本身有 keepalive 机制,模组会在空闲时自动发 PING。但应用层心跳更有价值,因为协议层心跳只能证明 TCP 连接还活着,不能证明服务器那边的业务逻辑还正常。我习惯每 30 秒往一个状态主题发一条带时间戳的消息,服务器侧如果超过 90 秒没收到就判定设备离线,推个告警。这个机制帮我抓到过好几次"连接看似正常但服务器侧会话已被清掉"的情况。

还有一点:发布频率别太高。有人为了"实时",100ms 发一次,结果串口拥塞、缓冲区溢出、模组来不及处理,反而更不稳定。我的经验是 1 秒一次已经完全够用,多数场景 5 秒甚至 30 秒一次都行。数据的新鲜度和稳定性之间,稳定性优先。

5. 联调实录:分五步把链路打通

5.1 分阶段验证步骤

我最怕的一种情况是:代码写完了,一上电,什么都不工作,于是从几百行代码里瞎猜哪里错了。所以联调必须分阶段,每一阶段都有明确的"通过标准"。

第一阶段,串口回环。不接模组,把 STM32 的 TX 和 RX 短接,发什么收什么。这一阶段验证串口外设配置、中断、环形缓冲、行解析全都正常。通过标准:发送 "hello\r\n",能在解析器里收到完整一行 "hello"。

第二阶段,模组握手。接上模组,发AT,收OK。这一步验证供电、接线、波特率。通过标准:连续发十次AT,十次都收到OK,一次不漏。

第三阶段,联网。发配置模式和连接热点指令,等待拿到 IP。通过标准:AT+CIPSTA?返回的 IP 与你所在网段一致,且能连续保持五分钟不掉。

第四阶段,连接服务器。先在局域网里跑一个消息代理服务,用电脑上的客户端工具连上去,然后让 STM32 连同一个服务。通过标准:服务器侧日志里能看到你的 Client ID 上线,且订阅成功后,从电脑发一条消息,STM32 串口能打印出收到的内容。

第五阶段,双向业务联调。STM32 周期上报,电脑侧订阅并显示;电脑侧发控制指令,STM32 收到后点灯。通过标准:连续跑一小时,上报不丢、控制不误、连接不断。

这个顺序的价值在于:每一阶段的失败只可能是本阶段引入的问题。第一阶段失败绝不会是模组的问题,第四阶段失败也基本不用怀疑供电。把可能性逐步收窄,是排查效率最高的方式。

5.2 关键参数的计算与选择

几个参数值得算一算,别凭感觉填。

缓冲区大小。假设上报周期 2 秒,单条载荷最大 200 字节,算上 AT 指令头和 MQTT 报文开销,一条发布指令也就 300 字节左右。串口 115200 波特率传 300 字节约 26ms。如果主循环最坏情况下被其他任务阻塞 500ms,那么期间最多堆积一条指令,收发缓冲各留 1024 字节绰绰有余。但如果你的下行载荷可能很大(比如下发一个配置表),就得按最大可能值乘以 3 来算。

心跳与超时。MQTT 的 keepalive 我设 60 秒,服务器侧判定离线的时间设 1.5 倍即 90 秒。应用层心跳 30 秒一条,服务器侧 90 秒没收到告警。这三个数字的关系是:应用层心跳最密,用来快速发现业务异常;协议层心跳居中,用来维持连接;服务器判定最宽松,避免网络抖动导致误告警。

重连退避。第一次失败等 1 秒重试,第二次 2 秒,第三次 4 秒,第四次 8 秒,上限 60 秒。指数退避的意义在于:如果是服务器挂了,你疯狂重试只会加重它的负担;如果是网络问题,快速重试也没用。上限设 60 秒是因为再长用户就会觉得设备"死"了。

发布周期与功耗。如果你的设备是电池供电,"发得越勤越费电"这条铁律就要认真算。ESP8266 在发射时电流三百多毫安,接收时几十毫安,深睡时几十微安。一次发布从唤醒到完成大约 200ms,如果每 5 分钟发一次,平均电流能压到极低;如果每 2 秒发一次,平均电流就上去了,电池撑不了几天。这个权衡没有标准答案,取决于你的电池容量和容忍的延迟。

5.3 完整上电流程与日志核对

把上面的东西串起来,我实际使用的上电流程是这样的:

[0.0s] 系统初始化,串口/定时器/传感器就绪 [0.1s] 拉低 RST 50ms,拉高,清空接收缓冲 [2.5s] 发 AT,预期 500ms 内收到 OK -> 记录日志 "模组响应正常" [3.0s] 发 ATE0 关回显 [3.5s] 发 AT+CWMODE=1 [4.0s] 发 AT+CWJAP="ssid","pass",超时 15s [19.0s] 收到 WIFI GOT IP,发 AT+CIPSTA? 确认地址 [20.0s] 发 AT+MQTTUSERCFG=...,配置客户端 [20.5s] 发 AT+MQTTCONN=...,超时 10s [22.0s] 收到 OK,发 AT+MQTTSUB=... 订阅控制主题 [22.5s] 订阅成功,进入主循环,开始周期上报

这份时间线要写进代码注释里,因为它就是你排查时的对照基准。实际跑的时候,如果第 19 秒那里没看到WIFI GOT IP,你就知道问题出在连热点;如果在第 22 秒那里卡住,问题就在连接或订阅。没有时间线,日志就是一锅粥;有了时间线,日志就是体检报告。

日志本身的输出也讲究。我一般给每行加个前缀区分来源,比如[TX]表示发给模组、[RX]表示模组回上来的、[APP]表示应用层自己的。这样一眼就能看出是"我发错了"还是"模组回错了"还是"我解析错了"。别小看这个习惯,它能把定位时间从半小时压缩到三分钟。

6. 常见问题与排查技巧实录

6.1 问题速查表

下面这张表是我这几年攒下来的,按出现频率排序,遇到问题先照这个顺序过一遍,能解决八成情况。

现象最可能原因排查动作
上电后串口全是乱码,然后就没反应供电不足导致模组反复重启加大滤波电容,改用独立 LDO,量 3.3V 波形
发AT完全没返回波特率不对、CH_PD未拉高、TX/RX 没交叉逐个波特率试,量电平,检查接线
AT+CWJAP一直返回FAIL密码错、热点名带特殊字符、加密方式不兼容换简单 SSID 和纯数字密码测试
AT+MQTTCONN返回ERROR地址端口不通、Client ID 重复、认证失败用电脑先连同一服务器验证,换唯一 ID
能连上但收不到下行消息订阅未成功、主题拼写不一致、QoS 不匹配在AT+MQTTSUB后确认OK,逐字符核对主题
发AT+MQTTPUB返回ERROR载荷含未转义的双引号、载荷过长用纯文本载荷测试,缩短载荷
运行几分钟后自己掉线又重连心跳太长、服务器踢人、WiFi 信号弱缩短 keepalive,查信号强度,检查服务器日志
收到下行数据但解析出来是乱码长度字段解析错误、缓冲区越界打印原始十六进制字节核对
设备在线但服务器说会话已清Client ID 冲突,两台设备互踢检查是否有多台设备使用相同 ID
偶发卡死,看门狗复位某条 AT 指令超时后状态机未复位检查超时分支是否把所有状态清零

6.2 断线重连与异常恢复的实现

自动重连不能只靠一句"打开自动重连标志",得做成分层的。

第一层是模组内部重连。AT+MQTTCONN的自动重连标志打开后,TCP 层面的断开模组会自己处理。但它有两个局限:一是只处理链路层断开,如果服务器因为 Client ID 冲突主动踢你,它不一定能救回来;二是重连成功后,之前的订阅关系不一定还在,取决于服务器是否保留了会话。

第二层是应用层检测。我用心跳加超时:每 30 秒发一条状态消息,如果连续两次发布失败(AT+MQTTPUB返回ERROR或者超时),就认为连接不可信,主动发AT+MQTTCONN=0重连一次。这里要注意,重连前先发AT+MQTTCLEAN=0清理旧会话,避免服务器上残留脏状态。

第三层是硬复位。如果应用层重连连续失败三次,说明模组可能已经处于异常状态,直接拉RST引脚硬复位,然后从"发 AT"那一步重新走一遍完整流程。这一步是最后的保险,虽然粗暴但有效。

这三层的触发条件要严格区分,不能一有失败就硬复位,那样设备会一直在重启。我的阈值是:单次发布失败不处理(可能只是网络抖动),连续两次失败触发应用层重连,应用层重连三次失败触发硬复位。

另外,硬复位之后要清空所有状态:缓冲区清空、状态机重置、订阅标志清零、重连计数清零。我见过有人复位后忘了清状态,导致状态机停在"等待连接应答"上,然后新的应答来了匹配不上,整个系统就僵在那里。

6.3 几条踩坑心得

最后说几个不太容易从文档里查到、但实际会咬人的点。

别在主循环里做长阻塞。有人在AT+CWJAP后面写了个HAL_Delay(10000),等十秒。这十秒里你的传感器采集停了、看门狗可能喂不上、按键响应也没了。正确的做法是把等待拆成状态机,主循环照常转,只是每隔几毫秒检查一下应答到了没。

AT指令之间留间隔。连续不停地怼指令,模组可能来不及处理,表现为随机ERROR或者某条指令被吞掉。指令之间至少留 50ms,如果上一条返回的是OK,就等它返回后再发下一条,别用固定的延时去猜。

串口日志别开太多。调试阶段一行不改地打印所有收发内容很方便,但上线后这些打印会占用大量串口带宽(如果日志和模组共用一个串口就更糟),甚至因为printf阻塞导致丢数据。上线前把日志分级,只保留错误和关键状态。

主题名用 ASCII,别用中文。虽然协议上允许 UTF-8,但不同服务器、不同客户端对编码的处理不一致,很容易出现"我发的和收的对不上"。用拼音或者英文缩写,省心。

设备 ID 要能一眼看出是谁。我最开始用自增数字当 ID,后来设备多了,日志里全是 1、2、3,根本不知道哪台是哪台。改成"项目缩写 + 芯片唯一号后四位"之后,排查效率立刻不一样。

低温或电压波动时表现会变。有些模组在 3.3V 掉到 3.1V 附近时还能工作,但射频性能会下降,表现为"能连上但经常丢包"。如果你发现设备在实验室好好的、装到现场就不稳,先量一下供电电压,别急着怀疑代码。

我在几个实际项目里反复验证下来,最深的体会是:这套方案能不能稳,八成取决于供电和重连机制,代码逻辑本身反而是最简单的那部分。AT 指令就那么几条,拼字符串谁都会;真正拉开差距的是你有没有给每一条指令设超时、有没有在失败时优雅地退回上一个状态、有没有让设备在没人管的情况下自己从坑里爬出来。我现在的习惯是,任何一个功能写完,先拔一次电、再断一次网、再让服务器踢一次,看它能不能自己恢复——这三个测试过了,才敢让它上现场。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询