简介:这份资源面向嵌入式开发与物联网初学者及工程师,聚焦STM32微控制器与ESP-01S Wi-Fi模块的协同开发,通过AT指令集配置MQTT协议,实现传感器数据的无线采集与远程发布。内容覆盖嵌入式编程、网络通信、MQTT发布/订阅机制及服务器端数据处理,可应用于智能家居、环境监测、工业自动化等场景。压缩包共5个文件,约39KB,包含C语言源码与头文件(实现STM32与ESP-01S的通信控制及MQTT客户端逻辑)、README与说明文档(介绍项目架构、功能与使用方法),以及附赠资源文档,便于快速理解与二次开发。目前已有96人学习,适合希望掌握AT指令配置MQTT、打通设备到云端数据链路的开发者参考,可从中获取完整的无线传输实现思路与排错参考。
1. 从串口到云端:STM32+ESP-01S 跑通 MQTT 的完整资源拆解
手头有一块 STM32 最小系统板,一个几块钱的 ESP-01S,想把传感器数据丢到服务器上,结果卡在 AT 指令回显乱码、MQTT 连接被拒、发出去的数据对不上——这套流程我前后翻车过三次。这份资源包给的就是一条已经踩平的路:STM32 负责采集和组包,ESP-01S 跑 AT 指令连 Wi-Fi,再用 MQTT 协议把数据发布到 Broker。它适合正在做物联网课设、智能家居原型、环境监测节点的嵌入式开发者,尤其是那些不想在 ESP8266 上重写固件、只想用 AT 指令快速打通无线链路的人。包里mqtt.c/mqtt.h是核心,README 和说明文件交代了接线与配置,附赠文档补了协议背景。下面按“资源是什么 → 怎么接怎么配 → 坑在哪 → 怎么验证”的顺序拆开讲。
2. 资源结构与运行链路:mqtt.c 里到底封装了什么
2.1 文件清单与各自职责
拿到压缩包先别急着编译,把目录结构看清楚能省掉后面很多返工。这个项目的文件不多,但每个都有明确分工,漏掉一个就可能出现“编译过了但连不上”的玄学问题。
| 文件/目录 | 类型 | 作用 |
|---|---|---|
mqtt-main | 主程序目录 | 存放main入口与任务调度逻辑 |
mqtt.c | 源文件 | AT 指令收发、MQTT 报文组包与解析的核心实现 |
mqtt.h | 头文件 | 对外暴露的连接、订阅、发布接口与状态枚举 |
README.md | 说明 | 接线表、AT 指令序列、编译环境说明 |
说明文件.txt | 说明 | 补充的配置参数与注意事项 |
附赠资源.docx | 文档 | 协议背景、Broker 参数、调试参考 |
mqtt.c是这个包的心脏。它没有直接操作 ESP-01S 的寄存器,而是通过 UART 发 AT 指令,等模块回OK或>之后再发下一段。常见做法是把“发指令—等响应—超时重试”封装成一个状态机,而不是用HAL_Delay死等,否则一旦模块没回,整个主循环就卡死了。
2.2 STM32 与 ESP-01S 的接线与串口配置
接线错了,后面所有 AT 指令都是对牛弹琴。ESP-01S 是 3.3V 供电,峰值电流能到 300mA 左右,直接用 STM32 板上的 LDO 小电流输出带它,很容易出现“连上 Wi-Fi 几秒后掉线”的现象。
| STM32 引脚 | ESP-01S 引脚 | 说明 |
|---|---|---|
| 3.3V | VCC | 建议独立 3.3V 供电,电流余量 ≥500mA |
| GND | GND | 共地,必须接 |
| PA2 (USART2_TX) | RX | STM32 发,模块收 |
| PA3 (USART2_RX) | TX | 模块发,STM32 收 |
| PA4 | EN/CH_PD | 拉高使能,部分模块标注为 CH_PD |
串口参数按 ESP-01S 出厂默认来:波特率 115200,8 位数据位,1 位停止位,无校验,无硬件流控。如果你手里的模块被改过波特率,先用AT+UART_DEF?查一下当前值,再决定 STM32 侧怎么配。
/* USART2 初始化,对接 ESP-01S,参数与模块出厂默认一致 */ void ESP_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; /* 不接 RTS/CTS,必须关流控 */ HAL_UART_Init(&huart2); }这段初始化的关键参数有三个:波特率必须和模块一致,流控必须关掉(ESP-01S 一般只引出 TX/RX/GND/VCC 四根线),模式要开收发。初始化完之后,建议先发一条AT\r\n,看模块回不回OK,这一步能过滤掉一大半接线和供电问题。
2.3 AT 指令状态机:为什么不能死等
很多人写 AT 指令的第一版是这样的:发AT+CWJAP,然后HAL_Delay(5000),再读串口。这种写法在实验室能跑,一到现场就翻车——路由器响应慢一点、信号弱一点,5 秒不够,后面全乱。
mqtt.c里更稳的做法是状态机加超时重试。每发一条指令,进入一个等待状态,收到预期响应就推进到下一步,超时就重发,重发超过次数就复位模块重新来。
/* AT 指令发送与响应等待的简化状态机 */ typedef enum { AT_IDLE = 0, AT_SEND_CMD, /* 发送指令 */ AT_WAIT_RESP, /* 等待 OK / ERROR / > */ AT_TIMEOUT, /* 超时,准备重试 */ AT_DONE /* 当前指令完成 */ } at_state_t; /* 发送一条 AT 指令,等待指定关键字,超时重试 max_retry 次 */ int AT_SendCmdWait(const char *cmd, const char *expect, uint32_t timeout_ms, uint8_t max_retry) { uint8_t retry = 0; while (retry <= max_retry) { HAL_UART_Transmit(&huart2, (uint8_t *)cmd, strlen(cmd), 100); if (AT_WaitKeyword(expect, timeout_ms) == 0) { return 0; /* 命中预期响应 */ } retry++; HAL_Delay(200); /* 重试前给模块一点缓冲 */ } return -1; /* 多次重试仍失败,交由上层复位模块 */ }expect参数是这条指令期望看到的关键字,比如AT+CWJAP等OK,AT+CIPSEND等>。timeout_ms建议按指令类型区分:纯 AT 测试给 500ms,连 Wi-Fi 给 10000ms,连 MQTT Broker 给 5000ms。重试次数一般 2 到 3 次,再多说明链路本身有问题,该查供电和信号了。
3. MQTT 报文组包与发布:从 CONNECT 到 PUBLISH 的落地细节
3.1 MQTT 控制报文的最小结构
MQTT 跑在 TCP 之上,ESP-01S 负责把 TCP 链路建起来,STM32 负责往这条链路里塞符合 MQTT 格式的字节流。一个 MQTT 报文由固定头、可变头、有效载荷三部分组成。固定头第一个字节高 4 位是报文类型,低 4 位是标志位;第二个字节开始是剩余长度,用变长编码表示。
| 报文类型 | 值 | 用途 |
|---|---|---|
| CONNECT | 1 | 客户端请求连接 Broker |
| CONNACK | 2 | Broker 确认连接 |
| PUBLISH | 3 | 发布消息 |
| SUBSCRIBE | 8 | 订阅主题 |
| PINGREQ | 12 | 心跳保活 |
mqtt.c里对 CONNECT 和 PUBLISH 的组包是重点。CONNECT 的可变头包含协议名MQTT、协议级别、连接标志、保活时间;有效载荷是 Client ID、用户名、密码。PUBLISH 的可变头是主题名,有效载荷就是你要发的数据。
3.2 用 AT 指令建立 TCP 并发送 MQTT 报文
ESP-01S 本身不解析 MQTT,它只负责透传 TCP 数据。所以流程是:先用 AT 指令连 Wi-Fi,再建 TCP 连接到 Broker 的 IP 和端口,然后进入透传模式,把 STM32 组好的 MQTT 字节流原样发出去。
/* 连接 Wi-Fi 并建立到 Broker 的 TCP 连接 */ int ESP_ConnectBroker(const char *ssid, const char *pwd, const char *broker_ip, uint16_t port) { char cmd[128]; /* 1. 复位并检查模块 */ if (AT_SendCmdWait("AT+RST\r\n", "ready", 3000, 2) != 0) return -1; /* 2. 设置为 Station 模式 */ if (AT_SendCmdWait("AT+CWMODE=1\r\n", "OK", 1000, 2) != 0) return -1; /* 3. 连接路由器,SSID 和密码按实际填写 */ snprintf(cmd, sizeof(cmd), "AT+CWJAP=\"%s\",\"%s\"\r\n", ssid, pwd); if (AT_SendCmdWait(cmd, "OK", 15000, 3) != 0) return -1; /* 4. 建立单路 TCP 连接,0 表示单连接模式 */ snprintf(cmd, sizeof(cmd), "AT+CIPSTART=\"TCP\",\"%s\",%d\r\n", broker_ip, port); if (AT_SendCmdWait(cmd, "CONNECT", 8000, 3) != 0) return -1; /* 5. 开启透传,之后 UART 数据直接进 TCP */ if (AT_SendCmdWait("AT+CIPMODE=1\r\n", "OK", 1000, 2) != 0) return -1; if (AT_SendCmdWait("AT+CIPSEND\r\n", ">", 2000, 2) != 0) return -1; return 0; }AT+CWJAP的超时给到 15 秒是有原因的:路由器 DHCP 分配、加密握手、信号弱时重传,都可能拖到 10 秒以上。AT+CIPSTART等的是CONNECT关键字而不是OK,这是 ESP-01S 的固定回显格式,等错了就会一直超时。进入透传后,退出透传要发+++,前后各留至少 1 秒静默,这个细节后面避坑章节还会提。
3.3 发布传感器数据的组包与发送
透传模式下,STM32 把 MQTT PUBLISH 报文拼好,直接HAL_UART_Transmit发出去就行。下面是一个发布温湿度字符串的最小实现。
/* 组一个 PUBLISH 报文并发送,topic 为发布主题,payload 为数据 */ int MQTT_Publish(const char *topic, const char *payload) { uint8_t buf[256]; uint16_t idx = 0; uint16_t topic_len = strlen(topic); uint16_t payload_len = strlen(payload); uint16_t remain_len = 2 + topic_len + payload_len; /* 主题长度字段2字节 */ /* 固定头:PUBLISH 类型,QoS0,剩余长度用单字节表示(<128) */ buf[idx++] = 0x30; buf[idx++] = (uint8_t)remain_len; /* 可变头:主题长度 + 主题名 */ buf[idx++] = (topic_len >> 8) & 0xFF; buf[idx++] = topic_len & 0xFF; memcpy(&buf[idx], topic, topic_len); idx += topic_len; /* 有效载荷 */ memcpy(&buf[idx], payload, payload_len); idx += payload_len; /* 透传模式下直接发,模块自动封进 TCP */ HAL_UART_Transmit(&huart2, buf, idx, 1000); return 0; }0x30表示 PUBLISH 且 QoS 为 0,如果你的 Broker 要求 QoS1,这里要改成0x32,并且报文里要带 Packet Identifier。remain_len这里假设小于 128,用单字节编码;如果主题加数据超过 127 字节,就得用变长编码,这是新手最容易忽略的边界。发布频率建议不要低于 1 秒一次,太密会触发 Broker 限流,也会让 ESP-01S 的发送缓冲溢出。
4. 避坑与排查:AT 指令和 MQTT 连接最容易翻车的五个点
4.1 上电回显乱码,AT 无响应
现象:串口助手打开,模块上电打印一堆乱码,发AT没有任何回显。
原因:九成是波特率不匹配,或者供电不足导致模块反复重启。ESP-01S 出厂 115200,但有些批次或二手模块被改过。供电方面,用 STM32 板载 3.3V 带 ESP-01S,电流不够时模块会不断重启,串口自然乱码。
解决:先单独给 ESP-01S 供 3.3V/500mA 以上的电,串口助手用 115200 试,不行再依次试 9600、74880、57600。确认模块正常回OK之后,再接到 STM32 上。如果必须共用电源,在 VCC 和 GND 之间并一个 100uF 电解加 0.1uF 陶瓷电容。
4.2 AT+CWJAP 返回 ERROR 或一直超时
现象:发AT+CWJAP="ssid","pwd"后,要么立刻ERROR,要么等十几秒超时。
原因:SSID 或密码里有特殊字符没转义;路由器开了 5GHz 而 ESP-01S 只支持 2.4GHz;信号强度低于 -80dBm 时握手失败。
解决:先用手机热点做对照测试,排除路由器兼容问题。SSID 和密码里的引号、逗号要按 AT 指令规范处理。查信号用AT+CWJAP?看返回的 RSSI,低于 -80 就挪近路由器或加天线。另外,AT+CWJAP在已连接状态下重复发会返回ERROR,先AT+CWQAP断开再连。
4.3 透传模式发不出数据,或退出不了
现象:进了透传,发 MQTT 报文没反应;想退出透传发+++也没反应。
原因:+++退出透传要求前后各有至少 1 秒的静默,中间不能有其他数据。如果 STM32 在持续发数据,+++会被当成普通数据发出去。另外,透传模式下如果 TCP 连接已断,数据发出去也是石沉大海。
解决:退出透传前先停掉所有发送任务,延时 1 秒,发+++,再延时 1 秒,等模块回OK。如果 TCP 已断,直接发AT+CIPCLOSE关闭连接,再重新AT+CIPSTART。建议在代码里加一个“连接状态检测”,定期发AT+CIPSTATUS确认链路还在。
4.4 MQTT CONNECT 被 Broker 拒绝
现象:TCP 连上了,CONNECT 报文发出去,Broker 返回 CONNACK 但连接标志是 0x05(未授权)或直接断开。
原因:Client ID 重复、用户名密码错误、Broker 要求 clean session 但报文里没设、或者协议级别填错(MQTT 3.1.1 是 4,3.1 是 3)。
解决:Client ID 用设备唯一标识,比如 STM32 的 UID 拼上随机数,避免多个设备撞 ID。协议级别确认 Broker 支持哪个版本,大多数公共 Broker 用 3.1.1。用户名密码按 Broker 要求填,注意长度字段是 2 字节大端。如果 Broker 开了 ACL,确认发布主题在授权列表里。
4.5 数据发出去但服务器收不到或对不上
现象:PUBLISH 报文发出去了,Broker 那边没收到,或者收到的数据缺字节、乱码。
原因:剩余长度编码错误,超过 127 字节没用变长编码;串口发送和接收冲突,HAL_UART_Transmit阻塞时间太长导致丢数据;主题名和 Broker 订阅的主题大小写不一致。
解决:组包后用串口助手把原始字节打出来,逐字节核对。剩余长度超过 127 时按 MQTT 规范用多字节编码。发送用中断或 DMA 方式,别用阻塞式长延时。主题名严格区分大小写,建议在 README 里把主题格式固定下来,比如device/{id}/data。
5. 验证与进阶:用心跳保活和抓包确认链路真的稳
链路搭起来只是第一步,能不能长时间稳定跑,得靠验证。我一般会做三件事:看心跳、抓报文、压测重连。
心跳保活是 MQTT 连接不断的关键。CONNECT 报文里的 Keep Alive 字段告诉 Broker 多久没收到消息就判定掉线。代码里要按这个周期发 PINGREQ,Broker 回 PINGRESP。如果 Keep Alive 设 60 秒,那 40 秒左右就该发一次心跳,留出余量。
/* 心跳任务,放在主循环里按周期调用 */ void MQTT_KeepAlive_Task(void) { static uint32_t last_ping = 0; uint32_t now = HAL_GetTick(); /* Keep Alive 设 60s,这里 40s 发一次,留 20s 余量 */ if (now - last_ping > 40000) { uint8_t pingreq[2] = {0xC0, 0x00}; /* PINGREQ 固定头 */ HAL_UART_Transmit(&huart2, pingreq, 2, 100); last_ping = now; } }抓包验证用串口助手把 UART 上的原始字节存下来,对照 MQTT 报文格式逐字段看。重点看 CONNECT 的协议名是不是MQTT(4 字节),协议级别是不是 4,CONNACK 的返回码是不是 0。PUBLISH 报文看主题长度字段和实际主题是否一致,有效载荷是不是你发的那串数据。
压测重连是很多人忽略的一步。把路由器断电 30 秒再上电,看设备能不能自动重连 Broker 并恢复发布。常见做法是在主循环里加一个连接状态检查,发现AT+CIPSTATUS返回非CONNECT就重新走一遍ESP_ConnectBroker和 MQTT CONNECT。重连要有退避,第一次等 2 秒,第二次等 4 秒,最多退到 30 秒,避免疯狂重试把 Broker 惹毛。
最后说一个我自己的习惯:每次改完 AT 指令序列或 MQTT 组包代码,先不接传感器,用固定字符串发到 Broker,用网页版 MQTT 客户端订阅同一个主题看能不能收到。确认链路通了,再接传感器调数据格式。从那以后我每次调无线链路都强制走一遍“固定字符串—网页订阅—再接传感器”的流程,省掉了很多在传感器数据和协议之间来回猜的时间。希望帮到你。
本文还有配套的精品资源,点击获取