news 2026/9/17 10:28:23

STM32与ESP-01S手写AT指令和MQTT报文接入云平台

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32与ESP-01S手写AT指令和MQTT报文接入云平台

STM32 和 ESP-01S 这套组合,做物联网课设、毕业设计、设备联网改造都绕不开。很多人拿到 ESP-01S 后第一反应是找现成云平台 SDK,或者直接套 MQTT 库,结果一换芯片、一换平台就抓瞎。我这里记录的是手打代码的方式:STM32 只负责串口收发和状态机,ESP-01S 跑 AT 固件,所有 AT 指令、MQTT 报文、心跳、订阅、发布都自己拼。云平台可以选 OneNET,也可以换成任意支持 MQTT 的物联网平台,参数按平台文档填。适合已经能点亮 LED、会串口收发、想搞懂“设备到底怎么连上云平台”的人。整套流程不依赖冷门库,核心代码都能自己敲出来,调试过程也能看清每一层到底在干什么。

1. 方案选型与整体链路设计

1.1 为什么不用现成 SDK 直接开搞

很多教程一上来就让人移植 ESP8266 的 SDK,或者在 STM32 上跑 MQTT 库。这样做的确能跑通,但对初学者不友好。ESP-01S 本身资源小,默认 AT 固件已经把 WiFi 协议栈、TCP/IP 都做好了,STM32 只需要通过串口发命令,就能建立 TCP 连接。既然目标只是连接云平台,没必要把整个网络协议栈再塞进 STM32。

手打代码的价值在于,你能清楚看到设备联网的每一步:先设置 WiFi 模式,再连路由器,再建立 TCP,再发 MQTT 连接报文,最后等平台返回 CONNACK。中间任何一步失败,你都能通过串口日志定位。用现成库时,报错往往是一句“连接失败”,排查范围太大。自己写一遍之后,换云平台、换模组、换芯片,思路都是通的。

还有一个现实原因:ESP-01S 的 AT 固件版本很多,有的支持 MQTT AT 指令,有的只支持 TCP。如果不确定手上模组固件,最稳妥的办法就是把它当纯 TCP 透传模块用,MQTT 报文自己在 STM32 里拼。这样不管模组是哪个版本,只要支持基础 AT 指令就能用。

1.2 STM32 和 ESP-01S 各自负责什么

这套方案里,STM32 是主控,负责三件事:第一,初始化串口,和 ESP-01S 通信;第二,维护一个接收缓冲区,处理 ESP-01S 返回的响应和云平台下发的数据;第三,拼装 MQTT 报文,按业务流程发布和订阅。ESP-01S 只负责联网和 TCP 传输,它不关心你发的是 MQTT 还是 HTTP,只负责把字节流送到云平台。

这样分工有个好处:STM32 的业务代码和网络代码可以分开写。网络层只提供esp_at_sendesp_tcp_sendesp_tcp_read这类接口,业务层负责调用。以后要换成 4G 模组或者以太网,只需要替换网络层,MQTT 层和业务层不用大改。

硬件连接上,我习惯用两个串口:USART1 接 USB-TTL,作为调试口,用来打印日志;USART2 接 ESP-01S,作为通信口。调试口非常重要,因为 ESP-01S 返回的数据有时是二进制,直接看很难受,把原始字节打印成 HEX 会方便很多。

1.3 云平台协议选择:MQTT 比 HTTP 更合适

云平台常见的接入协议有 HTTP、MQTT、TCP 透传。HTTP 适合低频上报,每次请求都要重新建立连接,开销大;TCP 透传最灵活,但平台侧通常要写解析脚本,通用性差。MQTT 是物联网平台最常用的协议,特点是长连接、发布订阅、心跳保活、支持 QoS,适合设备持续在线。

对 STM32 来说,MQTT 报文虽然需要自己拼,但结构并不复杂。固定头只有两个字节起步,剩余长度用变长编码,可变头和载荷按字段拼。只要把 CONNECT、SUBSCRIBE、PUBLISH、PINGREQ 这几个报文写出来,就能完成大部分业务。相比 HTTP 每次拼 JSON 请求,MQTT 的长连接反而更省资源。

选云平台时,优先看它是否提供 MQTT 接入。OneNET、阿里云 IoT、腾讯云 IoT、EMQX 自建服务都可以。不同平台的连接参数不同,但 MQTT 协议本身一样。这篇记录以通用 MQTT 参数为主,OneNET 的接入信息放在配置章节里说明。

2. 硬件接线与开发环境搭建

2.1 物料清单与供电坑

先列一下我实际用到的东西:STM32F103C8T6 最小系统板一块,ESP-01S 模组一个,ESP-01S 转接板一个,USB-TTL 调试器一个,3.3V 电源模块一个,杜邦线若干。如果手头是 ESP-01S 裸模组,最好配一个转接板,因为它的引脚间距小,直接接线容易短路。

供电是第一个大坑。ESP-01S 在发射 WiFi 信号时峰值电流能到 200mA 以上,有些甚至接近 300mA。STM32 最小系统板上的 3.3V LDO 通常只适合给 MCU 和少量外设供电,再带一个 WiFi 模组很容易掉电重启。我一开始用板载 3.3V,模组每次连 WiFi 就重启,串口打印一堆乱码。后来单独用一个 3.3V 电源模块给 ESP-01S 供电,GND 和 STM32 共地,问题立刻消失。

还有一点,ESP-01S 的 EN 脚和 GPIO0 脚要处理。正常运行时 EN 接 3.3V,GPIO0 悬空。如果 GPIO0 拉低,上电会进入下载模式。转接板一般已经处理好,裸模组要自己注意。电源正负极不要接反,ESP-01S 没有反接保护,接反很容易损坏。

2.2 串口接线和电平匹配

STM32 和 ESP-01S 之间只连四根线:STM32 的 USART2_TX 接 ESP-01S 的 RX,STM32 的 USART2_RX 接 ESP-01S 的 TX,VCC 接 3.3V,GND 接 GND。注意 TX 和 RX 要交叉,很多人接成 TX 对 TX,结果收不到任何响应。

电平方面,STM32F103 的 IO 是 3.3V,ESP-01S 也是 3.3V,可以直接连,不需要电平转换。如果用的是 5V 单片机,比如某些 Arduino,就要加电平转换,否则 ESP-01S 的 RX 可能被 5V 损坏。串口波特率初始化先设 115200,ESP-01S 默认通常是 115200。如果没响应,可以尝试 9600 或者 74880,有些老固件默认波特率不同。

调试串口 USART1 也接 USB-TTL,波特率同样设 115200。PC 端用串口助手打开,能看到 STM32 打印的日志。最好把 ESP-01S 的原始返回也通过调试串口转发出来,格式化成字符串或者 HEX,这样调试时不用来回插拔。

2.3 Keil 工程与串口收发框架

开发环境我用 Keil MDK5,芯片包装 STM32F1 系列。新建工程时选好芯片型号,勾选 CMSIS 的 Core 和 Device Startup,然后手动添加main.cusart.cesp01s.cmqtt.c这些文件。不用勾选复杂中间件,这个方案不需要 RTOS,也不需要网络库。

串口初始化用 HAL 库或者标准库都行。我为了代码直观,HAL 库和标准库都用过。HAL 库的HAL_UART_Receive_IT适合做单字节中断接收,标准库直接写中断服务函数也可以。关键是接收不能丢字节。ESP-01S 返回的数据可能很快,如果主循环来不及读,就会丢包。

我建议用一个环形缓冲区,中断里只做一件事:把收到的字节放进缓冲区,然后重新开启接收中断。主循环或者状态机再去解析缓冲区。下面是一个简化的环形缓冲区结构:

#define ESP_RX_BUF_SIZE 512 typedef struct { uint8_t buf[ESP_RX_BUF_SIZE]; volatile uint16_t head; volatile uint16_t tail; } RingBuf_t; RingBuf_t esp_rx; void ring_push(RingBuf_t *rb, uint8_t data) { uint16_t next = (rb->head + 1) % ESP_RX_BUF_SIZE; if (next != rb->tail) { rb->buf[rb->head] = data; rb->head = next; } } int ring_pop(RingBuf_t *rb, uint8_t *data) { if (rb->head == rb->tail) return 0; *data = rb->buf[rb->tail]; rb->tail = (rb->tail + 1) % ESP_RX_BUF_SIZE; return 1; }

中断回调里这样写:

uint8_t esp_rx_byte; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart == &huart2) { ring_push(&esp_rx, esp_rx_byte); HAL_UART_Receive_IT(&huart2, &esp_rx_byte, 1); } }

初始化时调用一次HAL_UART_Receive_IT(&huart2, &esp_rx_byte, 1);让中断先跑起来。调试串口的打印函数最好也重定向一下,方便用printf输出日志。注意printf可能阻塞,不要在中断里调用。

3. ESP-01S AT 指令驱动层手写

3.1 串口中断接收与超时等待

AT 指令驱动层最核心的两个函数,一个是发送命令,一个是等待响应。发送命令时,先把接收缓冲区清空,避免旧数据干扰;然后通过串口发送命令字符串,末尾加\r\n;接着在超时时间内不断从环形缓冲区取数据,拼成字符串,查找期望的响应关键字。

等待响应不能死等,要有超时。ESP-01S 有时候不回复,如果代码里写while(!found),整个程序就卡死了。我一般设 2 秒到 5 秒的超时,具体看命令。连接 WiFi 可能耗时较长,可以设 10 秒。超时后返回失败,业务层决定是否重试。

下面是一个简化实现:

uint8_t esp_wait_ack(const char *ack, uint32_t timeout_ms) { char resp[256]; uint16_t idx = 0; uint32_t start = HAL_GetTick(); memset(resp, 0, sizeof(resp)); while ((HAL_GetTick() - start) < timeout_ms) { uint8_t ch; while (ring_pop(&esp_rx, &ch)) { if (idx < sizeof(resp) - 1) { resp[idx++] = ch; resp[idx] = '\0'; } if (strstr(resp, ack) != NULL) { return 0; } } } return 1; }

这个实现没有处理二进制数据,只适合 AT 阶段的文本响应。MQTT 阶段返回的 CONNACK 是二进制,不能用strstr判断,要单独解析。

3.2 AT 发送、重试与状态机

发送函数可以这样封装:

uint8_t esp_send_cmd(const char *cmd, const char *ack, uint32_t timeout_ms) { while (ring_pop(&esp_rx, &(uint8_t){0})); // 清空缓冲区,实际写法要循环弹出 uart_send_string(&huart2, cmd); uart_send_string(&huart2, "\r\n"); return esp_wait_ack(ack, timeout_ms); }

实际代码里不要用复合字面量取地址那种写法,老老实实用一个临时变量循环弹出。发送后等待OK或者指定响应。如果失败,可以重试两次。但重试不是万能的,比如 WiFi 密码错了,重试一百次也连不上。所以重试次数不要太多,失败后要打印错误日志。

状态机适合管理联网流程。比如定义状态:ESP_STATE_IDLEESP_STATE_WIFI_CONNECTINGESP_STATE_TCP_CONNECTINGESP_STATE_MQTT_CONNECTINGESP_STATE_ONLINE。主循环里根据状态调用对应函数,成功就切换下一个状态,失败就回到空闲并延时重试。这样不会因为某一步卡死导致整个程序无响应。

3.3 连接 WiFi 和 TCP 的完整时序

上电后先发AT,确认模组有响应。如果返回OK,说明串口和波特率正确。接着发AT+CWMODE=1,设置为 Station 模式。然后发AT+CWJAP="WiFi名称","WiFi密码",等待WIFI CONNECTEDWIFI GOT IP,最后返回OK。这一步耗时较长,超时要设大一点。

连上 WiFi 后,如果只需要一条 TCP 连接,发AT+CIPMUX=0,设置单连接模式。然后发AT+CIPSTART="TCP","mqtt.example.com",1883,把域名和端口换成云平台提供的地址。如果模组不支持域名解析,就先用电脑解析出 IP,再填 IP。连接成功后返回CONNECTOK

发送数据时,先发AT+CIPSEND=长度,等模组返回>提示符,再发送指定长度的原始数据。注意长度是字节数,不是字符数。MQTT 报文里可能有0x00,串口发送函数不能遇到0x00就停止,要按长度发送。

void esp_tcp_send(uint8_t *data, uint16_t len) { char cmd[32]; sprintf(cmd, "AT+CIPSEND=%d", len); esp_send_cmd(cmd, ">", 2000); uart_send_bytes(&huart2, data, len); esp_wait_ack("SEND OK", 5000); }

接收云平台数据时,ESP-01S 会返回+IPD,长度:数据。需要解析这个格式,把数据部分取出来,再交给 MQTT 层处理。如果数据里包含二进制,不能用字符串函数找冒号,要按字节解析。先找到+IPD,,读长度,跳过冒号,再读指定长度的数据。

4. MQTT 报文手写与云平台连接

4.1 MQTT 报文结构快速拆解

MQTT 报文分三部分:固定头、可变头、载荷。固定头第一个字节高 4 位是报文类型,低 4 位是标志位。比如 CONNECT 是0x10,CONNACK 是0x20,PUBLISH 是0x30,SUBSCRIBE 是0x80,PINGREQ 是0xC0。固定头后面是剩余长度,表示可变头加载荷一共多少字节。

剩余长度是变长编码,最多四个字节。每个字节低 7 位是数据,最高位表示是否还有后续字节。比如剩余长度 64,编码就是一个字节0x40;剩余长度 200,编码是两个字节0xC8 0x01。计算方法是:先取长度除以 128 的余数,如果商大于 0,最高位置 1,继续;直到商为 0。

CONNECT 报文的可变头包括协议名MQTT、协议级别0x04、连接标志、Keep Alive 时间。载荷包括 Client ID、Will Topic、Will Message、Username、Password。连接标志决定这些字段是否存在。比如设置 Clean Session 位,表示不保存会话状态。

4.2 CONNECT 报文拼装与剩余长度计算

先写一个剩余长度编码函数:

uint8_t mqtt_encode_len(uint8_t *buf, uint32_t len) { uint8_t i = 0; do { uint8_t byte = len % 128; len /= 128; if (len > 0) byte |= 0x80; buf[i++] = byte; } while (len > 0); return i; }

然后拼 CONNECT 报文。为了代码清晰,我用一个临时缓冲区,先写固定头,预留剩余长度位置,写完可变头和载荷后再回填剩余长度。注意剩余长度不包括固定头第一个字节和剩余长度本身,只包括后面的字节。

uint16_t mqtt_build_connect(uint8_t *buf, const char *client_id, const char *username, const char *password, uint16_t keepalive) { uint16_t pos = 0; buf[pos++] = 0x10; // CONNECT uint8_t rl_pos = pos++; // 协议名 buf[pos++] = 0x00; buf[pos++] = 0x04; buf[pos++] = 'M'; buf[pos++] = 'Q'; buf[pos++] = 'T'; buf[pos++] = 'T'; buf[pos++] = 0x04; uint8_t flags = 0x02; // Clean Session if (username) flags |= 0x80; if (password) flags |= 0x40; buf[pos++] = flags; buf[pos++] = keepalive >> 8; buf[pos++] = keepalive & 0xFF; // Client ID uint16_t len = strlen(client_id); buf[pos++] = len >> 8; buf[pos++] = len & 0xFF; memcpy(&buf[pos], client_id, len); pos += len; // Username if (username) { len = strlen(username); buf[pos++] = len >> 8; buf[pos++] = len & 0xFF; memcpy(&buf[pos], username, len); pos += len; } // Password if (password) { len = strlen(password); buf[pos++] = len >> 8; buf[pos++] = len & 0xFF; memcpy(&buf[pos], password, len); pos += len; } // 回填剩余长度 uint32_t remain = pos - rl_pos - 1; uint8_t rl[4]; uint8_t rl_len = mqtt_encode_len(rl, remain); memmove(&buf[rl_pos + rl_len], &buf[rl_pos + 1], remain); memcpy(&buf[rl_pos], rl, rl_len); return pos + rl_len - 1; }

发送 CONNECT 后,云平台会返回 CONNACK。CONNACK 报文是0x20 0x02 0x00 0x00,最后一位返回码为0x00表示连接成功。如果返回0x010x05,分别表示协议版本不对、Client ID 被拒绝、服务不可用、用户名密码错误、未授权。解析时从+IPD数据里取前几个字节判断即可。

4.3 订阅、发布、心跳的实现

订阅报文类型是0x82,固定头后面是剩余长度、包标识符、主题过滤器、QoS。包标识符每发一次订阅要递增,避免重复。主题可以订阅平台下发的命令主题,QoS 一般用 0 或 1。手写时注意主题长度是两个字节。

发布报文类型是0x30,QoS 0 时没有包标识符,直接跟主题长度、主题、载荷。如果 QoS 1,标志位要改成0x32,并加包标识符。大多数传感器上报用 QoS 0 就够了,掉一两条数据可以接受。如果控制指令要求可靠,可以用 QoS 1。

uint16_t mqtt_build_publish(uint8_t *buf, const char *topic, const uint8_t *payload, uint16_t plen) { uint16_t pos = 0; buf[pos++] = 0x30; // PUBLISH, QoS 0 uint16_t topic_len = strlen(topic); uint32_t remain = 2 + topic_len + plen; pos += mqtt_encode_len(&buf[pos], remain); buf[pos++] = topic_len >> 8; buf[pos++] = topic_len & 0xFF; memcpy(&buf[pos], topic, topic_len); pos += topic_len; memcpy(&buf[pos], payload, plen); pos += plen; return pos; }

心跳报文PINGREQ最简单,两个字节:0xC0 0x00。云平台一般要求 Keep Alive 时间内至少有一次报文交互,每隔半个 Keep Alive 时间发一次心跳。比如 Keep Alive 设 60 秒,就每 30 秒发一次PINGREQ。如果超过 1.5 倍 Keep Alive 没收到任何报文,平台会断开连接。

心跳最好放在定时器中断或者主循环的时间判断里,不要用HAL_Delay阻塞。主循环里记录last_ping_tick,每次循环判断HAL_GetTick() - last_ping_tick > 30000,满足就发心跳并更新。这样不影响其他任务。

5. 云平台侧配置与联调

5.1 创建产品与设备

以 OneNET 为例,登录平台后创建产品,选择 MQTT 接入协议。产品创建后,平台会生成产品 ID、鉴权信息等。然后在产品下创建设备,设备名称可以自定义,比如stm32_esp01s_01。平台会生成设备 ID、设备密钥,或者让你自己填鉴权信息。

不同版本的 OneNET 接入参数略有差异。老版多协议接入里,MQTT 服务器地址可能是mqtt.heclouds.com,端口6002,Client ID 填设备 ID,Username 填产品 ID,Password 填鉴权 token。新版 OneNET Studio 的接入地址、端口、主题格式又不一样,具体以平台文档为准。如果不想折腾平台鉴权,可以先用公共 MQTT 服务器测试,比如broker.emqx.io,端口1883,不需要用户名密码。等 MQTT 报文跑通后,再把参数换成 OneNET 的。

参数项公共 MQTT 测试OneNET 示例说明
服务器地址broker.emqx.iomqtt.heclouds.com以平台文档为准
端口18836002明文 MQTT 端口
Client ID任意唯一字符串设备 ID不能重复
Username可空产品 ID按平台要求
Password可空鉴权 token可能需工具生成
发布主题自定义平台定义注意主题权限

创建完设备和参数后,记下这些信息:服务器地址、端口、Client ID、Username、Password、发布主题、订阅主题。把这些填到 STM32 代码的宏定义里,编译烧录。

5.2 参数映射与在线调试

STM32 代码里不要写死太多字符串,最好用宏定义集中管理:

#define MQTT_BROKER "broker.emqx.io" #define MQTT_PORT 1883 #define MQTT_CLIENT_ID "stm32_esp01s_01" #define MQTT_USERNAME "" #define MQTT_PASSWORD "" #define MQTT_PUB_TOPIC "stm32/esp01s/data" #define MQTT_SUB_TOPIC "stm32/esp01s/cmd" #define MQTT_KEEPALIVE 60

连接流程按顺序执行:先发AT,再AT+CWMODE=1,再AT+CWJAP,再AT+CIPMUX=0,再AT+CIPSTART,然后发 CONNECT 报文。每步成功打印一条日志,失败就重试。调试串口上能看到类似:

AT -> OK CWMODE -> OK CWJAP -> WIFI CONNECTED CWJAP -> WIFI GOT IP CWJAP -> OK CIPSTART -> CONNECT CIPSTART -> OK MQTT CONNECT -> CONNACK 0x00

看到CONNACK 0x00时,说明设备已经连上云平台。如果平台侧显示设备在线,但在线状态一会就掉,通常是心跳没发或者 Keep Alive 设置太短。先检查PINGREQ是否按时间发出。

5.3 数据上报与命令下发

数据上报就是拼 PUBLISH 报文,把传感器数据转成字符串或者 JSON,作为载荷发出去。比如 DHT11 温湿度数据,可以拼成{"temp":25,"humi":60}。注意 JSON 字符串长度要算准,PUBLISH 剩余长度要包含主题长度和载荷长度。

命令下发时,云平台会向订阅主题发 PUBLISH 报文。STM32 收到+IPD数据后,先判断是不是 MQTT PUBLISH。固定头第一个字节高 4 位是0x30,说明是 PUBLISH。然后解析剩余长度、主题长度、主题、载荷。如果主题匹配订阅主题,就把载荷取出来执行。比如收到{"led":1},就点亮 LED。

解析 PUBLISH 时要注意,主题长度和包标识符的位置。QoS 0 没有包标识符,主题长度直接跟在剩余长度后面。QoS 1 会多两个字节的包标识符。如果订阅时用了 QoS 1,解析时就要跳过这两个字节。建议先统一用 QoS 0 跑通,再改 QoS 1。

报文类型固定头首字节用途注意点
CONNECT0x10连接平台剩余长度回填容易错
CONNACK0x20连接确认返回码 0x00 才算成功
PUBLISH0x30上报/下发数据QoS 决定有无包标识符
SUBSCRIBE0x82订阅主题包标识符要递增
PINGREQ0xC0心跳两个字节,别拼错
DISCONNECT0xE0断开连接主动断开时发

6. 常见问题与排查实录

6.1 ESP-01S 不响应或者返回乱码

最常见的原因是波特率不对。ESP-01S 默认 115200,但有些固件是 9600 或 74880。可以先在 115200 下发AT,没反应就换 9600。如果返回乱码,说明波特率不匹配,多试几个常见值。还有可能是供电不足,模组启动时电压跌落,复位后打印启动信息,看起来像乱码。用独立 3.3V 电源供电,并在电源脚附近加一个 100uF 电解电容和一个 0.1uF 陶瓷电容,能缓解瞬间掉压。

接线错误也会导致不响应。TX 和 RX 必须交叉,GND 必须共地。如果只连了 TX、RX 和 VCC,没连 GND,串口电平没有参考,通信肯定失败。另外检查 EN 脚是否拉高,GPIO0 是否悬空。如果 GPIO0 被拉低,模组进入下载模式,AT 固件不运行。

6.2 TCP 连上但 MQTT 被拒绝

TCP 连接成功只说明网络通了,MQTT 连接失败通常是参数问题。CONNACK 返回码能直接告诉你原因:0x01协议版本不对,检查 CONNECT 报文里协议级别是不是0x040x02Client ID 被拒绝,检查 Client ID 是否唯一、是否超过平台限制;0x04用户名密码错误,检查 Username 和 Password;0x05未授权,检查设备是否被禁用或者 token 过期。

OneNET 的鉴权 token 有时效性,过期后需要重新生成。如果设备之前在线,突然连不上,先看 token 是否过期。公共 MQTT 服务器如果不需要用户名密码,CONNECT 标志位不要设置用户名和密码位,否则服务器可能拒绝。

还有一点,MQTT 报文里的字符串长度字段是两个字节,低字节在前,高字节在后。比如 Client ID 长度 13,要写成0x00 0x0D。如果写成0x0D 0x00,平台解析长度会变成一个很大的数,直接拒绝连接。

6.3 数据发不出去或者频繁掉线

数据发不出去,先看AT+CIPSEND是否返回>。如果没有返回,可能是 TCP 连接已经断开,或者模组忙。可以先发AT+CIPSTATUS查看连接状态。如果返回CLOSED,需要重新走AT+CIPSTART和 MQTT CONNECT 流程。

频繁掉线通常是心跳问题。Keep Alive 设 60 秒,但代码里没有按时发PINGREQ,平台在 90 秒左右会断开。检查心跳定时逻辑,确保主循环没有长时间阻塞。如果用了HAL_Delay(1000)这类阻塞延时,心跳可能被推迟。把延时改成非阻塞的时间判断,或者把心跳放在定时器中断里设置标志位。

缓冲区溢出也会导致数据错乱。ESP-01S 返回的+IPD数据可能比较长,如果接收缓冲区只有 128 字节,而云平台下发 200 字节的 JSON,后面的数据就被丢了。把缓冲区设大一点,至少 512 字节。解析时先判断长度,不够就丢弃或者分片处理。

现象可能原因排查方法
AT 无响应波特率、接线、供电换波特率,查 TX/RX,单独供电
返回乱码波特率不匹配、掉电试 9600/74880,加电容
TCP 连接失败WiFi 未连、地址端口错发 AT+CIPSTATUS,检查 IP
CONNACK 0x04用户名密码错核对三元组,重新生成 token
上报无数据主题无权限、长度算错平台查日志,抓包看报文
设备频繁离线心跳未发、Keep Alive 短检查 PINGREQ 周期,加大 Keep Alive
收到数据乱码解析越界、QoS 不符按长度解析,统一 QoS 0

7. 实战心得与后续扩展

7.1 长时间运行的几个处理

设备联网不能只跑几分钟,要连续跑几天才算过关。我一般会加三个处理:第一,看门狗。STM32 的独立看门狗或者窗口看门狗,在主循环里喂狗。如果程序跑飞,看门狗复位后重新联网。第二,断线重连。MQTT 连接断开后,状态机回到 TCP 连接之前,重新走一遍流程。重连不要太频繁,每次失败后延时 5 秒到 10 秒。第三,日志分级。调试阶段打印所有 AT 交互,正式运行时只打印关键状态,减少串口占用。

ESP-01S 长时间运行偶尔会死机,表现为不回复 AT。这种情况可以在代码里加一个“AT 探活”,每隔几分钟发一次AT,连续三次没响应就控制 ESP-01S 的复位脚,或者用继电器断电重启。转接板上如果有 RST 脚,可以接 STM32 的一个 GPIO,低电平复位。注意复位后要重新初始化 WiFi 和 MQTT。

7.2 后续可以扩展的方向

跑通基础连接后,可以往几个方向扩展。第一,接传感器。DHT11、DS18B20、光敏电阻、土壤湿度传感器都可以,把数据拼成 JSON 上报。第二,接执行器。继电器、LED、蜂鸣器,通过订阅命令主题实现远程控制。第三,加 OLED 显示。本地显示网络状态、IP 地址、温湿度,调试时不用一直看电脑。第四,用 FreeRTOS。把网络任务、传感器任务、显示任务分开,心跳和重连放在独立任务里,主循环更清晰。

如果产品要量产,建议把 ESP-01S 换成支持 MQTT AT 固件的模组,或者换成 ESP32,直接跑 MQTT 库。但作为学习过程,手打一遍 MQTT 报文非常有价值。以后看任何物联网平台的接入文档,都能快速理解那些参数到底对应协议里的哪个字段。

7.3 个人调试体会

我在实际调试时,最大的时间不是花在写代码上,而是花在确认 ESP-01S 到底有没有收到命令。后来我养成了一个习惯:所有发给 ESP-01S 的 AT 指令和收到的原始数据,都通过调试串口打印出来,二进制数据用 HEX 格式显示。这样一眼就能看出是没发出去,还是发出去了但响应被解析错。

另一个体会是,不要一上来就接云平台。先用公共 MQTT 服务器把 CONNECT、SUBSCRIBE、PUBLISH 跑通,确认 MQTT 报文拼装没问题。然后再换 OneNET,把 Client ID、Username、Password 换成平台参数。这样出问题时,你能确定是 MQTT 报文的问题,还是平台鉴权的问题。每次只改一个变量,排查效率会高很多。

最后再分享一个小技巧:剩余长度计算和回填是手写 MQTT 最容易出错的地方。可以在拼完报文后,把整个报文按字节打印出来,对照 MQTT 协议文档手工核对一遍。特别是 CONNECT 报文,剩余长度、协议名长度、Client ID 长度、Username 长度、Password 长度,任何一个长度字段写错,平台都会直接拒绝。核对几次之后,后面再写就顺手了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 10:23:31

Word+MathType论文公式居中编号右对齐:制表位与自动编号实战

论文写到最后&#xff0c;格式往往比内容还折磨人。导师一句“公式居中、编号右对齐”&#xff0c;能让不少人卡上一整天。我这些年帮人调过不少论文模板&#xff0c;本科毕设、硕士论文、期刊投稿都有&#xff0c;用 Word 配合 MathType 来做公式居中编号右对齐&#xff0c;是…

作者头像 李华
网站建设 2026/9/17 10:22:32

SQL UNION联合查询详解:纵向合并结果集的核心用法与性能优化

SQL入门系列讲到这里&#xff0c;单表查询和连接查询的基本功就算打完了。这一讲我们聊UNION联合查询——一个我工作中用得非常频繁、但很多人一开始都会搞混的集合操作。它的使用场景特别直白&#xff1a;你手上有好几张结构相同的表&#xff0c;按年份拆的、按部门拆的、按区…

作者头像 李华