尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

STM32与ESP-01S手打AT指令直连MQTT云平台实践

发布时间:2026/9/17 20:19:33

资讯中心
01
ARTICLE

STM32与ESP-01S手打AT指令直连MQTT云平台实践

STM32与ESP-01S手打AT指令直连MQTT云平台实践
去年帮一个做养殖棚环境监测的客户改板子需求朴素得不能再朴素STM32 采几路温湿度通过 WiFi 把数据打到云平台上看曲线。板子上焊好的就是 ESP-01S客户拿来的参考工程里塞了一堆现成的 WiFi 封装库编译零报错烧进去连不上串口日志里刷的全是乱码和一行行看不懂的超时提示。我把那套库整个拆掉从 AT 指令一层一层手打两个小时通了链路当晚数据就上去了。从那以后只要涉及 STM32 加 ESP-01S 连云平台这种组合我基本都是裸着写不用第三方网络库。这篇就把这套手打代码的完整过程摊开讲一遍从供电这种最基础但最容易翻车的地方到串口收发层怎么写、AT 状态机怎么排、MQTT 报文怎么一个字节一个字节拼出来最后到云平台那头的配置和联调顺序。适合手上有 STM32 最小系统板和一块 ESP-01S、想把数据真正送上去的嵌入式初学者也适合被各种 WiFi 库坑过、想搞明白底层到底发生了什么的老手。1. 为什么我宁愿手打AT指令也不直接套现成的WiFi库1.1 从一次库能编译但连不上的现场说起那次翻车的原因后来查清楚了其实特别典型。客户用的那套库默认 ESP-01S 已经在透传模式而板子上这块模块的固件是通用 AT 固件两条路线根本不交集。库作者写的超时是 500 毫秒ESP-01S 上电之后要打印启动日志、搜热点、拿 IP这个流程冷启动第一次经常要七八秒500 毫秒直接判失败然后库又不停地重试把模块的串口指令队列冲得一团乱最后就变成日志里一直刷乱码。问题不在库本身写得差而在于库把一大坨假设藏在了你看不见的地方。你不知道它发了什么、等了多久、拿什么当成功条件出问题的时候你手里没有任何可观测的东西只能靠猜。手打 AT 指令最大的价值就在这儿每一步都是你自己发出去、自己判断的。连不上的时候串口助手上把原始日志一抓是卡在ATCWJAP还是卡在ATCIPSTART一眼就能看出来。这种可观测性在调试期比什么都值钱。1.2 手写链路和现成库取舍到底在哪不是说库不能用是在这个具体的场景组合下手写更划算。我把两者的差别列出来你可以对着自己的项目判断。对比项手打 AT 指令第三方 WiFi/MQTT 库代码体积通常 8~15 KB Flash可控视库而定重的能到 40 KBRAM 占用一个几百字节的环形缓冲就够内部经常自带大缓冲区可观测性每条指令和回显都能打日志出问题只能进库内部打断点适配成本ESP 固件换版本要自己改指令库一般只支持特定固件版本上手门槛需要懂 AT 指令集和 MQTT 报文调几个 API 就能跑出问题时的排查看串口日志定位到具体指令需要通读库源码我的判断标准很简单如果项目后期要长期无人值守跑或者硬件资源紧手写如果只是做个 Demo、三天内要交差、后续不维护用库。养殖棚那种装在野外、一年到头没人碰的设备属于前者。1.3 这套方案的能力边界先说清楚不吹得把话说在前面手打代码不是万能的。它能做的事很明确让 STM32 通过 ESP-01S 连上 WiFi跟云平台建立一个 TCP 长连接按 MQTT 协议周期上报数据能收平台下发的命令断线能自己重连。这套东西的数据量很小一次上报几十到几百字节上报周期一般几秒到几分钟。它不适合做的事也很明确传图片、传音频、做 OTA 大文件升级这类高带宽场景ESP-01S 本身就不适合AT 指令的ATCIPSEND单次发送还有长度限制虽然可以用ATCIPSENDEX或者分段发送绕过去但效率和稳定性都会打折。另外 ESP-01S 只有一路 UARTSTM32 这边如果串口资源紧张得提前规划好哪路给调试、哪路给模块。把这些边界认清楚后面写代码的时候心里才有底不会走到半路发现方向不对再推倒重来。2. ESP-01S的外围电路三个几乎人人都会踩的坑2.1 供电3.3V不是接上就能用我见过太多程序没问题但模块一直重启的案例九成出在供电上。ESP-01S 标称工作电流八十毫安左右但这是平均值。它在射频发射的瞬间峰值电流能冲到三百毫安甚至更高而且这个尖峰来得非常突然。如果你用 USB 转 TTL 小板上的 3.3V 引脚直接给模块供电那个引脚的输出能力通常只有一百毫安出头带了 STM32 之后基本就废了。表现就是模块不停打印启动信息或者发 AT 指令没反应。正经的做法是给 ESP-01S 单独一路 LDOAMS1117-3.3 这类器件输入 5V 输出 3.3V电流能力有 800 毫安以上足够了。关键是滤波电容不能省输入端放一个 10uF 的钽电容或者电解电容输出端并一个 470uF 的电解电容加一个 0.1uF 的陶瓷电容。那个大电解是专门用来顶射频发射瞬间的电流尖峰的很多人只放 0.1uF看着规范实际顶不住。我自己的板子上还会在模块的 VCC 和 GND 之间再贴一个 100uF 的贴片钽电容离引脚越近越好这样连续上报数据的时候几乎不会出现复位。还有个细节ESP-01S 的天线区域下面千万不要铺铜也不要走线。这属于射频常识但画小板的时候特别容易忽略铺了地之后信号强度能掉一大截表现为搜得到热点但连不上或者连上就断。2.2 引脚连接和电平匹配的那些细节ESP-01S 一共八个引脚真正要用的是 VCC、GND、TX、RX、CH_PD有的批次还带 RST。先说 CH_PD这个脚是芯片使能必须接 3.3V悬空的话模块根本不启动很多人第一次用的时候就是忘了接它然后怀疑模块坏了。RST 如果引出来不接也行内部有上拉但我习惯接一个 10K 上拉到 3.3V同时引一根线到 STM32 的 GPIO这样程序里能主动复位模块比断电重启方便得多。然后是串口交叉。ESP-01S 的 TX 接 STM32 的 RXESP-01S 的 RX 接 STM32 的 TX这个基础但每次接线都要确认一遍。电平方面ESP-01S 的 IO 是 3.3V 电平STM32 如果是 3.3V 供电的型号比如 F103C8T6直接连没问题。如果你用的是 5V 供电的 STM32 老型号那 ESP 的 TX 接 STM32 的 RX 一般可以3.3V 对 5V 输入算高电平但 STM32 的 TX 接 ESP 的 RX 必须做电平转换最简单的办法是串一个 1K 电阻再并一个 3.3V 稳压二极管做钳位别嫌麻烦烧了模块更麻烦。还有一点GPIO0 平时悬空或者上拉就行只有烧写固件的时候才需要拉低进下载模式。板子上最好留一个跳线帽或者测试点不然以后想升级固件得飞线。2.3 固件版本决定了后面要不要手拼MQTT这一步很多人跳过但它直接决定了你后面是写三十行还是三百行代码。ESP-01S 出厂带的固件版本差别很大先发一条ATGMR看一下。如果是安信可后期的一些定制固件会支持ATMQTTUSERCFG、ATMQTTCONN、ATMQTTPUB这一整套 MQTT AT 指令那你只需要串联几条指令就能连上平台MQTT 报文完全不用管。但市面上大量流通的 ESP-01S 是通用 AT 固件AT 版本 1.7 左右的居多这套固件只提供 TCP/UDP 透传没有 MQTT 指令。这时候你有两个选择一是自己用烧写工具刷一个带 MQTT 指令的固件缺点是要接线进下载模式、找对固件包、刷坏了还得救二是老老实实自己拼 MQTT 报文用ATCIPSTART建 TCP用ATCIPSEND发自己组装的字节流。我一般选第二种因为不依赖特定固件换任何一块 ESP-01S 都能跑兼容性最好。代价就是要理解 MQTT 报文的格式这部分我在第 5 节会拆开讲。3. 串口收发层把AT交互变成可复用的函数3.1 中断加环形缓冲先把数据收稳手打 AT 指令的第一个工程问题就是怎么收数据。新手最容易写的版本是发一条指令然后 while 循环等\n超时就报错跑起来也能用但特别脆。原因有两个一是 ESP-01S 上电和复位的时候会吐一大堆启动信息几百个字节你不及时读走就溢出丢了等你真正发指令的时候缓冲里全是历史垃圾二是 AT 指令的回显是分几段到达的中间可能有停顿纯阻塞等很容易把一次完整的响应拆成两次。正确做法是用串口接收中断加环形缓冲。中断里只做一件事把 DR 寄存器里的字节塞进缓冲区然后移动头指针其他什么都不干。#define ESP_RX_BUF_SIZE 1024 static volatile uint8_t s_rx_buf[ESP_RX_BUF_SIZE]; static volatile uint16_t s_rx_head 0; static volatile uint16_t s_rx_tail 0; void USART2_IRQHandler(void) { if (USART2-SR USART_SR_RXNE) { uint8_t b (uint8_t)(USART2-DR 0xFF); uint16_t next (uint16_t)((s_rx_head 1) % ESP_RX_BUF_SIZE); if (next ! s_rx_tail) { /* 满则丢弃不阻塞中断 */ s_rx_buf[s_rx_head] b; s_rx_head next; } } } uint8_t esp_rx_get(uint8_t *out) { if (s_rx_tail s_rx_head) return 0; *out s_rx_buf[s_rx_tail]; s_rx_tail (s_rx_tail 1) % ESP_RX_BUF_SIZE; return 1; }缓冲开 1024 字节在 F103 上完全可以接受反正只占 1KB RAM。指针用uint16_t而不是int是防着编译器把它当有符号数处理导致取模出问题这种坑我踩过一次查了半个下午。中断优先级设成比 SysTick 低一点避免影响系统计时。3.2 esp_at_cmd的参数设计与超时策略有了环形缓冲就可以封装一个统一的指令函数。我在设计它的时候定了几个原则输入是单条指令不带换行、期望的成功回显、超时毫秒数函数内部负责补\r\n、清空历史缓冲、逐行匹配。逐行匹配很关键因为OK这个字符串在 ESP 的回显里到处都是如果直接在整个缓冲里找子串很容易被上一条指令的残留误判成成功。typedef enum { ESP_OK 0, ESP_TIMEOUT, ESP_ERROR } esp_ret_t; esp_ret_t esp_at_cmd(const char *cmd, const char *ack, uint32_t timeout_ms) { static char line[256]; uint16_t idx 0; uint8_t b; while (esp_rx_get(b)) { } /* 先清干净残留 */ if (cmd *cmd) { esp_uart_send_str(cmd); esp_uart_send_str(\r\n); } uint32_t t0 millis(); while ((millis() - t0) timeout_ms) { if (!esp_rx_get(b)) continue; if (b \n) { line[idx] \0; idx 0; if (ack *ack strstr(line, ack)) return ESP_OK; if (strstr(line, ERROR)) return ESP_ERROR; } else if (b ! \r idx sizeof(line) - 1) { line[idx] b; } } return ESP_TIMEOUT; }超时值怎么定这个得靠实测。我一开始全用 2000 毫秒后来发现ATCWJAP连热点有时候要六秒以上尤其是信号弱的时候所以单独给它 15000。而AT这种查活指令500 毫秒足够给太长只会让故障时的等待变得难熬。把每个指令的超时值做成宏后面调起来方便。3.3 半包、粘包和乱码的处理AT 指令的一大特点是回显不规律。发ATRST之后模块会先回显ATRST\r\n\r\nOK\r\n然后过一会儿再吐启动日志最后打印ready。如果你在收到OK就以为复位完成直接发下一条指令那条指令很可能被启动日志淹没。所以复位之后我一般直接等ready这个关键字超时给 5 秒。粘包的情况也常见。发ATCWJAP之后回显可能是WIFI CONNECTED、WIFI GOT IP、OK三行连在一起到也可能分三次到中间隔几百毫秒。用上面那个逐行匹配的方式就没有这个问题因为它是一个字节一个字节地喂进状态机什么时候到都行。乱码的处理要简单粗暴ESP-01S 上电初期输出的启动日志里有一部分是固件内部的调试信息波特率对不上或者芯片刚上电时钟不稳就会打出乱码字节。我的做法是只处理可打印 ASCII 字符遇到 0x00 到 0x1F 之外的非打印字符直接丢弃不放进line缓冲。这样即使有一两个乱码字节混进来也不会污染匹配。4. 从冷启动到TCP建链的完整AT时序4.1 初始化序列与每条指令的预期回显初始化这段代码我改过很多版最后留下来的是一条一条对应写死的流程每一步都打日志。顺序是这样的AT期望OK超时 500ms。作用是探活确认串口通了、波特率对了、模块在正常工作状态。ATE0期望OK。关掉指令回显。这个一定要做否则你发什么它回什么回显和真正的响应混在一起逐行匹配的时候特别容易误判。ATCWMODE1期望OK。设成 Station 模式也就是作为终端去连路由器。别设成 AP 模式那是让模块自己当热点跟连云平台没关系。ATCIPMUX0期望OK。设成单连接模式。这个必须在建 TCP 之前设如果设成多连接后面的ATCIPSEND需要带连接号参数指令格式都不一样。ATCIPMODE0期望OK。普通传输模式不是透传。透传模式下你没法用 AT 指令切回来只能靠特定的时序退出调试期特别难搞。ATCIPSNTPCFG1,8加上ATCIPSNTPTIME?这一步是可选的用来从网络拿时间。后面讲 Token 的时候你会知道为什么它有用。波特率这块提一句ESP-01S 的 AT 固件出厂默认基本都是 115200如果你的串口助手看到的全是乱码先把波特率在 9600 和 115200 之间换着试一遍八成就是这个问题。4.2 联网阶段最容易卡住的两步第 7 步是连热点ATCWJAP你的SSID,你的密码。这条指令的超时要给足我给 15 秒。成功的标志是收到OK中间会先出现WIFI CONNECTED再出现WIFI GOT IP。这里有两个坑。第一个坑2.4G 和 5G。ESP-01S 只支持 2.4GHz 频段如果路由器开了双频合一同一个 SSID 下面既有 2.4G 又有 5G模块有时候会尝试连 5G 然后失败。解决办法是在路由器后台把 2.4G 和 5G 拆成两个不同的 SSID让模块连 2.4G 那个这是最省事的。第二个坑SSID 或密码里有特殊字符。比如引号、逗号、反斜杠这些都是 AT 指令的分隔符会直接把指令解析搞乱。遇到了就要做转义ESP 的 AT 固件里用\,\\\这样的形式。我一般建议直接改 WiFi 密码别在代码里折腾转义不值得。第 8 步是建 TCP 连接ATCIPSTARTTCP,mqtts.heclouds.com,1883。这里要注意域名解析也要时间超时给 10 秒比较稳。成功会看到CONNECT然后OK。如果一直返回ERROR或者CLOSED先确认热点能不能上网拿手机连同一个 WiFi访问一下网页看看。很多现场调试的环境里路由器是内网根本没外网出口模块当然连不出去。4.3 建链与CIPSEND的节奏控制TCP 连上之后发送数据的流程是固定的两步先发ATCIPSEND长度等模块回一个提示符然后紧接着把指定长度的原始字节发出去最后等SEND OK。这里最容易出问题的是长度。长度必须和你后面实际发送的字节数严格一致多一个少一个都会导致模块一直等剩下的字节超时之后返回SEND FAIL然后整个连接状态就乱了。我的做法是把要发的数据先在内存里组装成一个完整的 buffer算出长度之后再发指令绝不一边算一边发。还有一个细节收到之后发送数据时中间不能有任何多余的换行或者空格必须是纯字节流因为模块是严格按字节数计数的。发完之后不要额外补\r\n那会被当成下一条数据的一部分在单连接模式下会直接跟在数据后面发出去。如果你要连续发多条数据中间最好留一点间隔或者等SEND OK之后再发下一条。M5311 那种模块无所谓但 ESP-01S 的指令处理能力比较弱连续喂太快它会丢。我实测的做法是每条之间至少隔 50 毫秒或者干脆用状态机串起来收到SEND OK才进入下一步。5. MQTT报文手工拼装CONNECT和PUBLISH到底长什么样5.1 固定报头与剩余长度的编码规则MQTT 报文的结构就三块固定报头、可变报头、有效载荷。固定报头永远是两个部分第一个字节是报文类型加标志位后面跟着剩余长度。剩余长度用的是变长编码最多四个字节每个字节低七位存数据、最高位表示后面还有没有。小于 128 的长度一个字节就够了128 到 16383 用两个字节。这个编码规则一定要实现对写错了服务器会直接断开连接而且不给你任何有用提示特别难查。uint8_t mqtt_encode_remain_len(uint8_t *buf, uint32_t len) { uint8_t n 0; do { uint8_t byte (uint8_t)(len % 128); len / 128; if (len 0) byte | 0x80; /* 还有后续字节 */ buf[n] byte; } while (len 0 n 4); return n; }拿 CONNECT 报文举例报头第一字节是 0x10。剩余长度等于可变报头长度 有效载荷长度第 5.2 节会给出具体计算。如果你的 ClientID、用户名、密码加起来总长不到 128剩余长度就是一个字节写起来很简单。但如果你后面发了很长的 JSON 数据超过 127 字节就变成两个字节了这部分用上面这个函数自动处理别手工写死。5.2 CONNECT报文逐字节拆解CONNECT 是连上服务器之后发的第一个报文也是鉴权发生的地方。它的结构我按字节列一下理解了这个后面 PUBLISH 就很好懂。固定报头0x10 剩余长度协议名字符串0x00 0x04加M Q T T协议级别0x04表示 MQTT 3.1.1连接标志0xC2。这一位一位看bit7 是用户名标志bit6 是密码标志bit5 是遗嘱保留bit1 是清理会话。0xC2就是1100 0010表示带用户名、带密码、不带遗嘱、清理会话。保持连接时间两个字节0x00 0x3C就是 60 秒。这个值告诉服务器如果 60 秒没收到任何报文就认为你掉线了。所以你的心跳周期必须小于这个数。有效载荷ClientID、用户名、密码每个都是两字节长度 内容的格式。uint16_t mqtt_build_connect(uint8_t *buf, const char *client_id, const char *username, const char *password) { uint8_t *p buf; uint16_t cid_len (uint16_t)strlen(client_id); uint16_t usr_len (uint16_t)strlen(username); uint16_t pwd_len (uint16_t)strlen(password); /* 可变报头固定 10 字节后面是三个带长度前缀的字符串 */ uint32_t remain 10u (2u cid_len) (2u usr_len) (2u pwd_len); *p 0x10; p mqtt_encode_remain_len(p, remain); *p 0x00; *p 0x04; *p M; *p Q; *p T; *p T; *p 0x04; *p 0xC2; *p 0x00; *p 0x3C; *p (uint8_t)(cid_len 8); *p (uint8_t)(cid_len 0xFF); memcpy(p, client_id, cid_len); p cid_len; *p (uint8_t)(usr_len 8); *p (uint8_t)(usr_len 0xFF); memcpy(p, username, usr_len); p usr_len; *p (uint8_t)(pwd_len 8); *p (uint8_t)(pwd_len 0xFF); memcpy(p, password, pwd_len); p pwd_len; return (uint16_t)(p - buf); }发送的时候把返回的长度喂给ATCIPSEND然后把 buffer 原样发出去。如果一切正常服务器会回一个0x20 0x02 0x00 0x00这是 CONNACK第二个字节是 0x00 表示连接成功如果是别的值比如 0x04、0x05分别是用户名密码错、未授权这时候就要回去查鉴权参数。5.3 Token鉴权串的生成与时间戳陷阱云平台的 MQTT 鉴权一般不用明文密码而是让你算一个有时效的 Token。以常见的做法为例用户名填设备名密码填 TokenClientID 也填设备名。Token 的组成大概是这么几段版本、资源路径、过期时间、签名方法、签名值。签名值的算法是把资源路径、过期时间、签名方法拼成一个字符串用平台给的设备密钥做 HMAC-SHA1得到 20 字节再做 Base64。最后把这些参数拼成keyvaluekeyvalue的形式还要对整串做 URL 编码因为 Base64 结果里可能有、/、这些字符不编码的话服务器解析会出错。/* 伪代码实际用 mbedTLS 或自己实现 HMAC-SHA1 */ void build_token(char *out, const char *pid, const char *dev, const char *key, uint32_t et) { char res[96], src[160]; snprintf(res, sizeof(res), products/%s/devices/%s, pid, dev); snprintf(src, sizeof(src), %s%d%s, res, et, sha1); uint8_t mac[20]; hmac_sha1((const uint8_t *)key, strlen(key), (const uint8_t *)src, strlen(src), mac); char sign[40]; base64_encode(mac, 20, sign); /* res 和 sign 都需要 URL 编码 */ snprintf(out, 256, version2018-10-31res%set%umethodsha1sign%s, url_encode(res), et, url_encode(sign)); }这里有个很多人栽过的坑et是过期时间戳服务器会拿它跟当前时间比过期的 Token 直接拒绝连接。而 STM32 上如果没有 RTC 电池、也没做过 NTP 校时上电的时候时间是 1970 年算出来的 et 就被服务器判成过期了。我见过有人把 et 写成当前时间加一小时结果设备放了一周之后重启时间又不对了又连不上。我的做法有两种。第一种是用ATCIPSNTPCFG和ATCIPSNTPTIME?从网络拿一次时间然后用这个时间加一个比较长的有效期比如一年。模块拿时间不需要额外的库一条 AT 指令就行非常省事。第二种更粗暴直接把 et 写成一个很远的未来值比如 4102416000大致对应 2100 年大多数平台的鉴权逻辑只检查有没有过期不检查过期时间是不是长得离谱。这招我用了很久没出过问题但属于偏方正式产品还是建议走校时这条路。5.4 PUBLISH上报与心跳维持数据上报用的是 PUBLISH 报文。第一字节是0x30如果 QoS 是 1 就是0x32。我对传感器的周期上报一般用 QoS 0不要求确认简单、开销小如果有重要的告警数据用 QoS 1服务器会回 PUBACK你能确认送到。uint16_t mqtt_build_publish(uint8_t *buf, const char *topic, const char *payload, uint8_t qos) { uint8_t *p buf; uint16_t tlen (uint16_t)strlen(topic); uint16_t plen (uint16_t)strlen(payload); uint32_t remain 2u tlen plen; *p (uint8_t)(0x30 | ((qos 0x03) 1)); p mqtt_encode_remain_len(p, remain); *p (uint8_t)(tlen 8); *p (uint8_t)(tlen 0xFF); memcpy(p, topic, tlen); p tlen; memcpy(p, payload, plen); p plen; return (uint16_t)(p - buf); }Topic 的格式各平台不一样一般是$sys/产品ID/设备名/xxx/yyy这种系统主题具体要在平台文档里对一下。负载一般是 JSON比如{temperature:25.6,humidity:68}拼 JSON 的时候用snprintf就行但要注意缓冲区大小浮点数格式化会占不少字节我一般给 256 字节的栈上缓冲够用了。别用sprintf那玩意儿没有边界检查栈溢出之后现象的诡异程度能让你怀疑人生。心跳是 PINGREQ就两个字节0xC0 0x00。服务器收到会回0xD0 0x00也就是 PINGRESP。前面 CONNECT 里设的 keepalive 是 60 秒那么心跳周期要小于 60我一般设 30 秒给网络抖动留点余量。心跳千万别漏漏了服务器会主动断开而 ESP-01S 这边不会立刻给你任何提示你以为连接还在实际数据早就发不出去了。用一个 10 毫秒的定时器做时基在定时器中断里累加计数主循环里判断计数值到了就标记一个标志位主循环看到标志位才真正发心跳。这样中断里不做事避免打断串口接收。这个模式在嵌入式里很常见用起来很稳。6. 云平台侧的配置顺序与整套链路联调6.1 产品、设备与物模型的建立顺序代码写完之前云平台那边要先把东西建好不然你没有参数可填。顺序是这样的先建产品产品决定了协议类型一定要选 MQTT选错了后面改不了只能重建。产品建好之后拿到产品 ID这个就是 CONNECT 里的用户名。然后在产品下面建设备设备名就是 ClientID 和用户名设备密钥用来算 Token。接着配物模型数据流。这一步说的是这个设备会上报哪些数据、什么类型、什么单位。比如温度是浮点数单位摄氏度湿度是整数单位百分比。物模型的作用是让平台知道怎么解析你发上来的 JSON。如果你不建物模型直接发数据有的平台会拒绝有的平台会自动创建但类型可能不对。我建议这里先只建一两个数据点跑通之后再扩展。新手最容易犯的错是一上来建十几个点然后数据上不去搞不清是代码问题还是配置问题来回改。6.2 联调应该从哪一步开始往回查链路一长出问题的时候人就容易乱。我的习惯是分层验证从下往上每一层确认了再往上走。第一层串口通不通。用串口助手直接连 ESP-01S手动敲AT能看到OK就说明模块和 PC 之间没问题。如果这步就不通检查波特率、接线、供电。第二层STM32 到模块通不通。把 PC 那一端从模块上摘下来接到 STM32 的调试串口让 STM32 跑初始化代码看打印出来的日志里有没有收到OK。这一步能确认 STM32 的串口收发层是好的。第三层联网通不通。继续看日志ATCWJAP有没有返回OKATCIPSTART有没有返回CONNECT。如果卡在这里大概率是热点问题或者外网问题。第四层MQTT 通不通。看 CONNACK 有没有回来返回的码是不是 0x00。如果返回 0x04 或者 0x05就是鉴权参数的问题回去查产品 ID、设备名、Token 和 et。第五层数据通不通。看平台界面上有没有出现数据点。如果连接成功但数据不显示一般是 Topic 写错了或者 JSON 格式不对。这个顺序的好处是每一层都有明确的成功标志出问题的时候能立刻定位到是哪一层不会在一堆日志里瞎找。6.3 常见错误现象与对应原因现象大概率原因处理方式模块反复打印启动信息供电电流不够换独立 LDO加大电容发 AT 没有任何回显CH_PD 没接 3.3V / 波特率不对检查使能脚试 9600 和 115200回显全是乱码波特率不匹配 / 电平不对确认波特率检查 TX/RX 是否交叉ATCWJAP一直超时连的是 5G 热点 / 密码错拆开双频 SSID确认密码ATCIPSTART返回 ERROR域名解析失败 / 无外网直接用 IP 试检查路由出口CONNACK 返回 0x04 0x05用户名密码错 / Token 过期重算 Token检查 et连上几分钟就掉线心跳没发 / keepalive 太短加 PINGREQ周期设为 keepalive 的一半SEND FAIL长度和实际字节数不一致先组包算长度再发指令这张表我基本是照着这几年踩过的坑填的遇到问题的时候从上往下扫一遍能省不少时间。7. 长时间挂机之后暴露出来的问题和加固做法7.1 断线重连的状态机调试期一切正常装到现场跑两天就掉线这几乎是必经之路。原因五花八门路由器重启、运营商侧连接回收、模块自己死机。所以断线重连不是可选项是必需品。我的做法是维护一个连接状态变量在主循环里做状态机。每次发数据之前先判断状态只有CONNECTED才发。发数据如果连续失败两次就主动断开重连先ATCIPCLOSE再重新ATCIPSTART如果这一步也失败就ATRST复位模块从初始化流程重来一遍。复位之后要重新走完整的初始化包括ATE0、ATCWMODE、ATCWJAP、ATCIPSTART和 MQTT CONNECT。我见过有人只重连 TCP 不重连 WiFi结果路由器重启之后模块还傻等着状态永远恢复不了。重连要有退避策略不能一秒钟重试一次那会把模块彻底搞死。我的做法是失败后等 5 秒、10 秒、30 秒、60 秒这样递增最多到 5 分钟重试一次。反正传感器数据晚几分钟上报不影响大局。7.2 内存和看门狗手写代码的一个隐性成本是内存管理要自己盯着。字符串拼接、JSON 组装这些操作特别容易把栈吃掉。我的经验是所有涉及字符串的缓冲区都定义为static或者全局数组不用大块的栈上数组拼接统一用snprintf并传sizeof如果一次拼接需要超过 256 字节说明设计有问题把数据拆开分次上报。看门狗是必开的。独立看门狗 IWDG 的时钟来自内部的 40kHz RC 振荡器跟主时钟无关主时钟跑飞了它照样能复位芯片。初始化的时候先喂一次然后在主循环里定期喂。喂狗的位置要放在主循环的末尾这样如果前面的任何一步卡死了狗就会咬。有个坑要注意喂狗动作别放在定时器中断里。放在中断里的话即使主循环卡死了中断还在跑狗永远咬不到就失去意义了。这个错误我第一次用看门狗的时候犯过设备死机了但看门狗没动作查了半天才发现喂狗位置错了。7.3 数据上报的节流和异常处理传感器数据不是每次都要上报。ADC 采回来的值抖动很正常如果每个微小变化都上报流量浪费不说平台侧的数据曲线也是一团毛刺。我的做法是设一个变化阈值和最小上报间隔两个条件都满足才上报。比如温度变化超过 0.5 度并且距离上次上报超过 30 秒。异常值也要处理。传感器偶尔会读出一个明显不合理的值比如温度 200 度这通常是 I2C 或 ADC 读时序出问题导致的。这种情况我会丢弃这一次的读数连续三次都异常才认为传感器坏了然后在数据包里加一个状态标志位告诉平台。别直接把 200 度发上去运维的人看到曲线尖刺会打电话找你。上报的数据最好带一个递增的序号和一个设备时间戳。序号能让平台侧看出有没有丢包时间戳能让你在事后分析的时候知道数据是几点产生的而不是靠平台接收时间来倒推。这两个字段加起来也就十几个字节但排查问题的时候价值极大。这套东西我从最早的一版改到现在代码量大概六百行左右用的是标准库加自己写的几个模块没有引入任何第三方网络库。跑得最久的一块板子连续在线了七个多月中途重启过两次都是因为现场断电。我现在做新的类似项目基本就是把这几个文件拷过去改改产品和设备参数半天时间就能出原型。如果你手上正好有板子建议别急着上库先拿串口助手手动敲几条 AT 指令感受一下模块的脾气你会发现后面写代码的时候心里踏实很多。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。