简介:这份资源面向嵌入式开发与物联网初学者及进阶开发者,聚焦STM32微控制器与ESP-01S Wi-Fi模块的协同应用,通过AT指令集配置MQTT协议,实现传感器数据的无线采集、发布与远程传输,可应用于智能家居、环境监测、工业自动化等场景。压缩包共5个文件,约39KB,包含C语言源码与头文件(实现STM32与ESP-01S通信控制及MQTT客户端逻辑)、README与说明文档(介绍项目架构、功能与使用方法)、附赠资源文档等,便于快速理解代码结构与移植思路。目前已有96人学习下载。读者可从中掌握AT指令配置流程、MQTT发布/订阅机制、Wi-Fi联网与数据上报的完整链路,并参考源码与文档搭建自己的物联网无线传输方案,适合作为课程设计、毕业设计或原型开发的技术参考。
1. 从串口到云端:STM32+ESP-01S 跑通 MQTT 的最小闭环
手头有一块 STM32 最小系统板,一个几块钱的 ESP-01S,再加上一个温湿度传感器,想把数据传到云端——这是很多嵌入式开发者从单机走向联网的第一道坎。标题里的方案说白了就是:STM32 不直接跑 TCP/IP 协议栈,而是通过串口发 AT 指令,让 ESP-01S 这个 Wi-Fi 模组去连 MQTT 服务器,STM32 只负责采集数据和拼指令。这个路子最大的好处是对 MCU 资源要求极低,F103C8T6 这种 64KB Flash、20KB RAM 的芯片就能跑,不需要跑 lwIP 或者 FreeRTOS+TCP 那套重家伙。
适合谁看?如果你已经能用 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 指令的固件,直接提供AT+MQTTUSERCFG、AT+MQTTCONN、AT+MQTTPUB这些命令,STM32 只需要发字符串就能完成连接和发布。
常见做法是刷入 ESP-01S 的 AT 固件版本至少到 1.2.0.0 以上,这个版本开始有较完整的 MQTT 指令集。怎么确认?上电后发AT+GMR,看返回的 AT 版本号。如果低于这个版本,建议先刷固件。刷写需要 USB-TTL 模块和乐鑫的下载工具,接线时注意 ESP-01S 的 GPIO0 拉低进入下载模式,TX/RX 交叉连接。刷完后发AT+MQTTUSERCFG=?测试,如果返回 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 的 RX,RX 接 TX,共地,3.3V 供电。串口参数 115200-8-N-1,勾选“发送新行”。
第一步,测试模组是否正常:
AT返回OK。如果没返回,检查供电和波特率,ESP-01S 默认波特率可能是 115200 或者 74880,发AT没反应就换 74880 试一次。
第二步,设置 Wi-Fi 模式并连接热点:
AT+CWMODE=1 AT+CWJAP="你的WiFi名","你的密码"返回WIFI CONNECTED和WIFI GOT IP才算真正连上。这里有个坑:如果热点是 5GHz 的,ESP-01S 只支持 2.4GHz,会一直连不上。另外密码里如果有特殊字符,要用转义或者换一个简单密码测试。
第三步,配置 MQTT 客户端参数:
AT+MQTTUSERCFG=0,1,"client_id_test","username","password",0,0,""参数依次是:链接 ID(0)、方案(1 表示 TCP)、客户端 ID、用户名、密码、证书索引、是否启用遗嘱、遗嘱主题。如果服务器是公共测试服务器,比如broker.emqx.io,用户名密码可以留空字符串。客户端 ID 必须唯一,重复会导致互相踢下线。
第四步,连接 MQTT 服务器:
AT+MQTTCONN=0,"broker.emqx.io",1883,0返回OK后稍等,会收到+MQTTCONNECTED:0,1,...表示连接成功。如果返回ERROR或者+MQTTDISCONNECTED,先检查网络是否通,再检查端口和客户端 ID。
第五步,发布一条消息:
AT+MQTTPUB=0,"topic/test","hello",0,0参数:链接 ID、主题、数据、QoS(0)、保留标志(0)。返回OK后,如果你在服务器端订阅了topic/test,就能收到hello。
第六步,订阅主题:
AT+MQTTSUB=0,"topic/test",0之后服务器发到这个主题的消息会以+MQTTSUBRECV:0,"topic/test",5,hello的形式从串口吐出来。
手动走通这一遍,你就有了一个“标准答案”。后面 STM32 发指令时,只要对比返回值和这里的记录,就能快速定位是哪一步出了问题。
2.3 关键 AT 指令参数速查与常见返回码
把上面用到的指令整理成表,方便写代码时对照:
| 指令 | 关键参数 | 正常返回 | 异常返回与含义 |
|---|---|---|---|
| AT+CWJAP | SSID, password | WIFI CONNECTED / GOT IP | FAIL 表示密码错或信号弱 |
| AT+MQTTUSERCFG | link_id, scheme, client_id, user, pass | OK | ERROR 表示参数格式错 |
| AT+MQTTCONN | link_id, host, port, reconnect | OK + MQTTCONNECTED | ERROR 表示网络不通或 ID 冲突 |
| AT+MQTTPUB | link_id, topic, data, qos, retain | OK | ERROR 表示未连接或主题非法 |
| AT+MQTTSUB | link_id, topic, qos | OK | ERROR 表示未连接 |
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("AT+CWJAP=\"MyWiFi\",\"12345678\""); if (ESP_WaitResponse("WIFI GOT IP", 10000)) { esp_state = STATE_MQTT_CFG; } break; case STATE_MQTT_CFG: ESP_SendCmd("AT+MQTTUSERCFG=0,1,\"stm32_client\",\"\",\"\",0,0,\"\""); if (ESP_WaitResponse("OK", 2000)) { esp_state = STATE_MQTT_CONN; } break; case STATE_MQTT_CONN: ESP_SendCmd("AT+MQTTCONN=0,\"broker.emqx.io\",1883,0"); if (ESP_WaitResponse("+MQTTCONNECTED", 5000)) { esp_state = STATE_MQTT_SUB; } break; case STATE_MQTT_SUB: ESP_SendCmd("AT+MQTTSUB=0,\"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 或者纯字符串,通过AT+MQTTPUB发出去。注意AT+MQTTPUB的数据部分如果包含逗号、引号,需要转义或者用十六进制模式。简单起见,数据里只放数字和字母。
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), "AT+MQTTPUB=0,\"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 连不上
现象:AT+CWJAP返回WIFI GOT IP,但AT+MQTTCONN返回ERROR。原因可能是服务器地址解析失败,或者端口被占用。先AT+PING="broker.emqx.io"测试网络通不通。如果 PING 不通,说明 DNS 有问题,可以改用 IP 地址连接。如果 PING 通但 MQTT 连不上,检查客户端 ID 是否和别的设备冲突,换一个唯一的 ID。
4.3 发布几条数据后 ESP-01S 无响应
现象:刚开始能发几条,之后发AT没返回,或者返回busy p...。原因是 ESP-01S 的 AT 固件在处理 MQTT 发布时如果频率太高,内部缓冲区会满。解决:发布间隔至少 1 秒,不要连续发。如果已经卡死,给 ESP-01S 的 EN 引脚发一个低电平脉冲复位,或者用AT+RST软复位。我在代码里加了一个看门狗:如果连续 3 次发布失败,就硬复位 ESP-01S 并重新走连接流程。
4.4 订阅消息收不到或者收到乱码
现象:服务器端显示已发布,但 STM32 串口没有+MQTTSUBRECV。原因可能是订阅主题不匹配,或者 QoS 不兼容。先确认订阅的主题和发布的主题完全一致,包括大小写。如果用的是 QoS 1,ESP-01S 可能因为确认包丢失而不推送,换成 QoS 0 测试。乱码通常是波特率漂移,检查 STM32 的串口时钟配置,115200 下误差要小于 2%。
4.5 数据发出去但服务器收到的是空消息
现象:AT+MQTTPUB返回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), "AT+MQTTPUB=0,\"%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 信号强度(AT+CWJAP?可以查 RSSI),这样出问题时你至少知道是环境问题还是代码问题。我一般会在STATE_RUNNING里每 60 秒查一次 RSSI,低于 -80dBm 就主动重连 Wi-Fi,别等到彻底断了才处理。希望帮到你。
本文还有配套的精品资源,点击获取