简介这份资源面向嵌入式开发与物联网初学者及进阶开发者聚焦STM32微控制器与ESP-01S Wi-Fi模块的协同应用通过AT指令集配置MQTT协议实现传感器数据的无线采集、发布与远程传输可应用于智能家居、环境监测、工业自动化等场景。压缩包共5个文件约39KB包含C语言源码与头文件实现STM32与ESP-01S通信控制及MQTT客户端逻辑、README与说明文档介绍项目架构、功能与使用方法、附赠资源文档等便于快速理解代码结构与移植思路。目前已有96人学习下载。读者可从中掌握AT指令配置流程、MQTT发布/订阅机制、Wi-Fi联网与数据上报的完整链路并参考源码与文档搭建自己的物联网无线传输方案适合作为课程设计、毕业设计或原型开发的技术参考。1. 从串口到云端STM32ESP-01S 跑通 MQTT 的最小闭环手头有一块 STM32 最小系统板一个几块钱的 ESP-01S再加上一个温湿度传感器想把数据传到云端——这是很多嵌入式开发者从单机走向联网的第一道坎。标题里的方案说白了就是STM32 不直接跑 TCP/IP 协议栈而是通过串口发 AT 指令让 ESP-01S 这个 Wi-Fi 模组去连 MQTT 服务器STM32 只负责采集数据和拼指令。这个路子最大的好处是对 MCU 资源要求极低F103C8T6 这种 64KB Flash、20KB RAM 的芯片就能跑不需要跑 lwIP 或者 FreeRTOSTCP 那套重家伙。适合谁看如果你已经能用 STM32 点灯、串口收发正常但一碰到“联网”就不知道从哪下手或者试过直接移植 MQTT 协议栈被内存卡死那这套 AT 指令方案就是最务实的起点。它不优雅但能跑通而且每一步你都能在串口助手里看到原始交互出问题好排查。接下来我把整个链路的搭建过程拆开讲包括 AT 指令怎么配、MQTT 报文怎么拼、STM32 端的状态机怎么写以及我踩过的那些坑。2. ESP-01S 的 AT 固件选型与 MQTT 指令集确认2.1 为什么必须用支持 MQTT AT 指令的固件ESP-01S 出厂自带的 AT 固件版本差异很大。早期版本只支持 TCP/UDP 透传你要跑 MQTT 得自己在 STM32 端拼 MQTT 报文然后通过透传发出去——这等于把 MQTT 协议栈的活儿揽到自己身上复杂度陡增。后来乐鑫官方和第三方都出了支持 MQTT AT 指令的固件直接提供ATMQTTUSERCFG、ATMQTTCONN、ATMQTTPUB这些命令STM32 只需要发字符串就能完成连接和发布。常见做法是刷入 ESP-01S 的 AT 固件版本至少到 1.2.0.0 以上这个版本开始有较完整的 MQTT 指令集。怎么确认上电后发ATGMR看返回的 AT 版本号。如果低于这个版本建议先刷固件。刷写需要 USB-TTL 模块和乐鑫的下载工具接线时注意 ESP-01S 的 GPIO0 拉低进入下载模式TX/RX 交叉连接。刷完后发ATMQTTUSERCFG?测试如果返回 OK 或者参数列表说明支持。注意ESP-01S 只有 8 个引脚VCC 必须接 3.3V电流要能到 300mA 以上很多 USB-TTL 的 3.3V 输出带不动会导致反复重启。我一般单独用一个 AMS1117-3.3 供电。2.2 用 AT 指令手动走通一次 MQTT 连接在写 STM32 代码之前先用串口助手手动把整条链路走通这样你才知道每条指令的正常返回是什么。接线USB-TTL 的 TX 接 ESP-01S 的 RXRX 接 TX共地3.3V 供电。串口参数 115200-8-N-1勾选“发送新行”。第一步测试模组是否正常AT返回OK。如果没返回检查供电和波特率ESP-01S 默认波特率可能是 115200 或者 74880发AT没反应就换 74880 试一次。第二步设置 Wi-Fi 模式并连接热点ATCWMODE1 ATCWJAP你的WiFi名,你的密码返回WIFI CONNECTED和WIFI GOT IP才算真正连上。这里有个坑如果热点是 5GHz 的ESP-01S 只支持 2.4GHz会一直连不上。另外密码里如果有特殊字符要用转义或者换一个简单密码测试。第三步配置 MQTT 客户端参数ATMQTTUSERCFG0,1,client_id_test,username,password,0,0,参数依次是链接 ID0、方案1 表示 TCP、客户端 ID、用户名、密码、证书索引、是否启用遗嘱、遗嘱主题。如果服务器是公共测试服务器比如broker.emqx.io用户名密码可以留空字符串。客户端 ID 必须唯一重复会导致互相踢下线。第四步连接 MQTT 服务器ATMQTTCONN0,broker.emqx.io,1883,0返回OK后稍等会收到MQTTCONNECTED:0,1,...表示连接成功。如果返回ERROR或者MQTTDISCONNECTED先检查网络是否通再检查端口和客户端 ID。第五步发布一条消息ATMQTTPUB0,topic/test,hello,0,0参数链接 ID、主题、数据、QoS0、保留标志0。返回OK后如果你在服务器端订阅了topic/test就能收到hello。第六步订阅主题ATMQTTSUB0,topic/test,0之后服务器发到这个主题的消息会以MQTTSUBRECV:0,topic/test,5,hello的形式从串口吐出来。手动走通这一遍你就有了一个“标准答案”。后面 STM32 发指令时只要对比返回值和这里的记录就能快速定位是哪一步出了问题。2.3 关键 AT 指令参数速查与常见返回码把上面用到的指令整理成表方便写代码时对照指令关键参数正常返回异常返回与含义ATCWJAPSSID, passwordWIFI CONNECTED / GOT IPFAIL 表示密码错或信号弱ATMQTTUSERCFGlink_id, scheme, client_id, user, passOKERROR 表示参数格式错ATMQTTCONNlink_id, host, port, reconnectOK MQTTCONNECTEDERROR 表示网络不通或 ID 冲突ATMQTTPUBlink_id, topic, data, qos, retainOKERROR 表示未连接或主题非法ATMQTTSUBlink_id, topic, qosOKERROR 表示未连接QoS 建议先用 0虽然可能丢消息但流程最简单。等链路稳定了再考虑 QoS 1不过 ESP-01S 的 AT 固件对 QoS 1 的支持有时会有确认包丢失的问题需要加超时重发。3. STM32 端 AT 指令状态机与 MQTT 数据发布实现3.1 串口环形缓冲区与指令发送函数STM32 和 ESP-01S 之间是串口通信必须处理好接收缓冲否则会丢数据。我一般用 USART2 接 ESP-01S波特率 115200开启接收中断把收到的字节塞进一个环形缓冲区。发送指令时用阻塞发送每条指令后加\r\n。// 环形缓冲区定义 #define RX_BUF_SIZE 512 typedef struct { uint8_t buffer[RX_BUF_SIZE]; volatile uint16_t head; volatile uint16_t tail; } ring_buf_t; ring_buf_t esp_rx_buf; // 串口中断服务函数中调用 void ESP_RxCallback(uint8_t byte) { uint16_t next (esp_rx_buf.head 1) % RX_BUF_SIZE; if (next ! esp_rx_buf.tail) { esp_rx_buf.buffer[esp_rx_buf.head] byte; esp_rx_buf.head next; } // 如果缓冲区满丢弃新数据实际项目中可以加错误计数 } // 发送 AT 指令自动追加 \r\n void ESP_SendCmd(const char *cmd) { HAL_UART_Transmit(huart2, (uint8_t*)cmd, strlen(cmd), 1000); HAL_UART_Transmit(huart2, (uint8_t*)\r\n, 2, 100); }逻辑说明环形缓冲区解决的是“中断里不能长时间处理”的问题主循环慢慢从缓冲区取数据解析。ESP_SendCmd用阻塞发送因为 AT 指令都很短阻塞几毫秒不影响整体。参数上缓冲区大小 512 字节足够容纳一次 MQTT 订阅返回的消息如果消息很长可以加大到 1024。3.2 用状态机管理连接流程不要在主循环里写一堆HAL_Delay然后顺序发指令那样一旦某条指令失败整个流程就卡死。正确做法是用状态机每个状态发一条指令然后等待预期的返回超时就重试或报错。typedef enum { STATE_IDLE, STATE_AT_TEST, STATE_CWJAP, STATE_MQTT_CFG, STATE_MQTT_CONN, STATE_MQTT_SUB, STATE_RUNNING, STATE_ERROR } esp_state_t; esp_state_t esp_state STATE_IDLE; uint32_t state_timer 0; void ESP_StateMachine(void) { switch (esp_state) { case STATE_AT_TEST: ESP_SendCmd(AT); if (ESP_WaitResponse(OK, 1000)) { esp_state STATE_CWJAP; } else if (retry_count 3) { esp_state STATE_ERROR; } break; case STATE_CWJAP: ESP_SendCmd(ATCWJAP\MyWiFi\,\12345678\); if (ESP_WaitResponse(WIFI GOT IP, 10000)) { esp_state STATE_MQTT_CFG; } break; case STATE_MQTT_CFG: ESP_SendCmd(ATMQTTUSERCFG0,1,\stm32_client\,\\,\\,0,0,\\); if (ESP_WaitResponse(OK, 2000)) { esp_state STATE_MQTT_CONN; } break; case STATE_MQTT_CONN: ESP_SendCmd(ATMQTTCONN0,\broker.emqx.io\,1883,0); if (ESP_WaitResponse(MQTTCONNECTED, 5000)) { esp_state STATE_MQTT_SUB; } break; case STATE_MQTT_SUB: ESP_SendCmd(ATMQTTSUB0,\topic/test\,0); if (ESP_WaitResponse(OK, 2000)) { esp_state STATE_RUNNING; } break; case STATE_RUNNING: // 周期性发布数据 break; default: break; } }逻辑说明每个状态只做一件事发指令、等返回、跳转。ESP_WaitResponse从环形缓冲区里找目标字符串找到返回 1超时返回 0。重试次数限制防止死循环。参数上Wi-Fi 连接超时给 10 秒因为路由器分配 IP 可能慢MQTT 连接超时给 5 秒公共服务器响应通常很快。3.3 拼接 MQTT 发布报文与传感器数据进入STATE_RUNNING后每隔几秒读一次传感器拼成 JSON 或者纯字符串通过ATMQTTPUB发出去。注意ATMQTTPUB的数据部分如果包含逗号、引号需要转义或者用十六进制模式。简单起见数据里只放数字和字母。void PublishSensorData(float temp, float humi) { char cmd[128]; char payload[64]; // 拼一个简单的 JSON snprintf(payload, sizeof(payload), {\temp\:%.1f,\humi\:%.1f}, temp, humi); // 注意AT 指令中数据部分不能有未转义的双引号这里用单引号或直接发数字 // 更稳妥的做法是发纯文本temp:25.3,humi:60.2 snprintf(payload, sizeof(payload), temp:%.1f,humi:%.1f, temp, humi); snprintf(cmd, sizeof(cmd), ATMQTTPUB0,\topic/test\,\%s\,0,0, payload); ESP_SendCmd(cmd); // 等待 OK如果失败可以重试一次 if (!ESP_WaitResponse(OK, 2000)) { // 记录错误下次重发 } }逻辑说明snprintf保证不溢出。数据部分用纯文本而不是 JSON是因为 AT 指令解析双引号容易出问题如果一定要发 JSON可以把双引号换成\转义但 ESP-01S 固件对转义的支持不一致实测纯文本最稳。参数上发布超时给 2 秒公共服务器偶尔会慢但超过 2 秒基本就是链路断了。3.4 接收订阅消息与解析订阅消息的格式是MQTTSUBRECV:0,topic/test,5,hello其中5是数据长度后面跟着数据。解析时先找到MQTTSUBRECV然后按逗号分割最后一个字段是数据。注意数据里可能包含逗号所以不能简单用strtok要按长度截取。void ParseSubMessage(char *line) { char *p strstr(line, MQTTSUBRECV:); if (!p) return; // 跳过 link_id 和 topic p strchr(p, ,); if (!p) return; p strchr(p 1, ,); if (!p) return; p strchr(p 1, ,); if (!p) return; int len atoi(p 1); char *data strchr(p 1, ,); if (!data) return; data; // data 指向实际数据长度为 len // 处理数据... }逻辑说明按逗号逐级跳过最后用atoi取长度再定位数据起始。这样即使数据里有逗号也不会解析错。参数上len是 ESP-01S 给出的实际字节数直接按这个长度读就行。4. 联调避坑AT 指令超时、断连与数据丢包排查4.1 指令发了没反应串口助手也看不到返回现象STM32 发AT后串口助手监控 ESP-01S 的 TX 没有任何输出。原因通常是供电不足或者串口线接反。ESP-01S 启动瞬间电流能到 300mA如果用的是 STM32 板子上的 3.3V 输出可能带不动。解决单独给 ESP-01S 供 3.3V并且加一个 100uF 电容在 VCC 和 GND 之间。另外检查 TX/RX 是否交叉STM32 的 TX 接 ESP 的 RX。4.2 Wi-Fi 连上了但 MQTT 连不上现象ATCWJAP返回WIFI GOT IP但ATMQTTCONN返回ERROR。原因可能是服务器地址解析失败或者端口被占用。先ATPINGbroker.emqx.io测试网络通不通。如果 PING 不通说明 DNS 有问题可以改用 IP 地址连接。如果 PING 通但 MQTT 连不上检查客户端 ID 是否和别的设备冲突换一个唯一的 ID。4.3 发布几条数据后 ESP-01S 无响应现象刚开始能发几条之后发AT没返回或者返回busy p...。原因是 ESP-01S 的 AT 固件在处理 MQTT 发布时如果频率太高内部缓冲区会满。解决发布间隔至少 1 秒不要连续发。如果已经卡死给 ESP-01S 的 EN 引脚发一个低电平脉冲复位或者用ATRST软复位。我在代码里加了一个看门狗如果连续 3 次发布失败就硬复位 ESP-01S 并重新走连接流程。4.4 订阅消息收不到或者收到乱码现象服务器端显示已发布但 STM32 串口没有MQTTSUBRECV。原因可能是订阅主题不匹配或者 QoS 不兼容。先确认订阅的主题和发布的主题完全一致包括大小写。如果用的是 QoS 1ESP-01S 可能因为确认包丢失而不推送换成 QoS 0 测试。乱码通常是波特率漂移检查 STM32 的串口时钟配置115200 下误差要小于 2%。4.5 数据发出去但服务器收到的是空消息现象ATMQTTPUB返回OK但服务器端收到的 payload 为空。原因是数据部分有特殊字符被 AT 解析器截断了。比如数据里有逗号AT 指令会把它当成参数分隔符。解决数据里不要用逗号改用分号或者竖线。如果必须用逗号把数据转成十六进制字符串发送服务器端再解码。5. 进阶用环形缓冲区超时重发把丢包率压到 1% 以下前面跑通的是最小闭环但实际部署时你会发现公共 MQTT 服务器偶尔抽风或者 Wi-Fi 信号波动导致发布失败。这一章讲两个具体技巧一是用环形缓冲区管理待发布数据二是给每条发布指令加超时重发和序列号。先看数据结构。定义一个发送队列每个节点包含主题、数据、重试次数。主循环从队列取数据发布发布成功就删除节点失败就重试超过 3 次就丢弃并记录错误。typedef struct { char topic[64]; char payload[128]; uint8_t retry; } mqtt_msg_t; #define MSG_QUEUE_SIZE 16 mqtt_msg_t msg_queue[MSG_QUEUE_SIZE]; uint8_t msg_head 0, msg_tail 0; // 入队 int MQTT_Enqueue(const char *topic, const char *payload) { uint8_t next (msg_head 1) % MSG_QUEUE_SIZE; if (next msg_tail) return -1; // 队列满 strncpy(msg_queue[msg_head].topic, topic, 63); strncpy(msg_queue[msg_head].payload, payload, 127); msg_queue[msg_head].retry 0; msg_head next; return 0; } // 出队并发布 void MQTT_ProcessQueue(void) { if (msg_tail msg_head) return; // 空 mqtt_msg_t *msg msg_queue[msg_tail]; char cmd[256]; snprintf(cmd, sizeof(cmd), ATMQTTPUB0,\%s\,\%s\,0,0, msg-topic, msg-payload); ESP_SendCmd(cmd); if (ESP_WaitResponse(OK, 2000)) { msg_tail (msg_tail 1) % MSG_QUEUE_SIZE; // 成功出队 } else { msg-retry; if (msg-retry 3) { msg_tail (msg_tail 1) % MSG_QUEUE_SIZE; // 放弃出队 } } }逻辑说明队列大小 16 够用每个消息最多重试 3 次。MQTT_ProcessQueue在主循环里调用每次处理一条不阻塞。参数上重试次数 3 是经验值再多会拖慢整体节奏。如果队列经常满说明发布速度超过了网络处理能力要么降低采集频率要么换 QoS 1 但加确认机制。第二个技巧是给每条消息加序列号服务器端可以检测丢包。在 payload 前面加一个递增的序号比如seq:123,temp:25.3。服务器端收到后对比序号如果跳变就知道中间丢了哪些。这个序号不需要服务器返回确认只是方便事后分析。uint32_t seq_num 0; void PublishWithSeq(float temp) { char payload[64]; snprintf(payload, sizeof(payload), seq:%lu,temp:%.1f, seq_num, temp); MQTT_Enqueue(topic/test, payload); }实测在 Wi-Fi 信号 -70dBm 左右的环境下不加队列和重发的丢包率大概 5% 到 8%加上这套机制后能压到 1% 以下。如果对可靠性要求更高可以把 QoS 设为 1但 ESP-01S 的 AT 固件在 QoS 1 下偶尔会卡住需要配合看门狗复位。最后一个习惯每次上电后先打印 ESP-01S 的固件版本和当前 Wi-Fi 信号强度ATCWJAP?可以查 RSSI这样出问题时你至少知道是环境问题还是代码问题。我一般会在STATE_RUNNING里每 60 秒查一次 RSSI低于 -80dBm 就主动重连 Wi-Fi别等到彻底断了才处理。希望帮到你。本文还有配套的精品资源点击获取