简介:这份源码资源面向嵌入式开发初学者与物联网项目实践者,聚焦STM32F103C8T6通过ESP8266以AT指令方式接入阿里云物联网平台的完整实现。代码中已给出MQTT服务器地址、ClientID、用户名与密码等登录配置,并定义了属性上报与订阅发布话题,可直接对照阿里云物联网平台的三元组信息进行替换调试,帮助读者理解从串口AT交互到MQTT报文收发的完整链路。资源包共134个文件,以h头文件与c源文件为核心,辅以o、d、crf等编译中间文件,以及uvprojx、uvoptx工程文件、hex固件、map映射与bat批处理脚本,压缩包约3.17MB,结构贴近Keil MDK实际工程。目前已有8405人学习下载,适合需要快速跑通STM32+ESP8266上云流程、排查连接与鉴权问题的开发者参考借鉴。
1. 从一块“哑巴”开发板到云端在线设备:这套 STM32+ESP8266 源码到底解决了什么
很多人第一次做物联网项目,卡住的地方不是写业务逻辑,而是设备根本连不上云。板子上的传感器数据读出来了,串口助手也打印得漂漂亮亮,但要让这块 STM32 把数据送到阿里云物联网平台,再让平台把控制指令下发回来,中间隔着 ESP8266 的 AT 指令、MQTT 协议握手、三元组鉴权、Topic 订阅发布这一整套链路。这套源码干的事情,就是把这条链路完整跑通:STM32 作为主控,通过串口发 AT 指令驱动 ESP8266 联网,再在 ESP8266 上跑 MQTT 协议接入阿里云物联网平台,实现数据上报和指令下发。它适合正在做单片机课程设计、毕业设计,或者公司里要快速验证一个物联网原型的开发者。你拿到手不是一堆零散函数,而是一条从硬件初始化到云端通信的完整可运行路径,改改三元组和传感器读取就能变成自己的项目。
2. 技术选型与整体架构:为什么是 STM32 + ESP8266 + AT 指令 + MQTT
2.1 四层结构拆开看,每一层各管什么
这套方案的本质是一个典型的“主控 + 通信模组”架构,STM32 负责业务逻辑和传感器采集,ESP8266 负责网络传输,两者之间用串口 AT 指令对话,云端用阿里云物联网平台做设备管理和消息路由。把它拆成四层会更清楚。
第一层是硬件层。STM32 通过 USART 与 ESP8266 连接,通常用 USART2 或 USART3,波特率默认 115200。ESP8266 模组常见的是 ESP-01S 或 NodeMCU 板载版本,供电必须稳定在 3.3V,峰值电流能到 300mA 以上,这是后面避坑章节要重点说的。
第二层是 AT 指令驱动层。STM32 不直接操作 ESP8266 的寄存器,而是发字符串命令,比如AT+CWMODE=1设置 Station 模式、AT+CWJAP="ssid","password"连 Wi-Fi、AT+MQTTUSERCFG配置 MQTT 参数。ESP8266 收到后回OK或ERROR,STM32 解析这些回显来判断状态。这一层最考验的是超时处理和回显匹配,因为 AT 指令的返回不是固定长度,得靠状态机去啃。
第三层是 MQTT 协议层。ESP8266 的 AT 固件里已经内置了 MQTT 指令集,包括AT+MQTTUSERCFG、AT+MQTTCONN、AT+MQTTPUB、AT+MQTTSUB。STM32 只需要按顺序把这些指令发出去,就能完成 MQTT 连接、发布和订阅。阿里云物联网平台要求 MQTT 连接时携带三元组信息,也就是 ProductKey、DeviceName、DeviceSecret,通过AT+MQTTUSERCFG里的 clientId、username、password 参数传进去。
第四层是云平台层。阿里云物联网平台为每个设备分配一个专属的 MQTT 接入点,设备连接后可以往物模型 Topic 上报属性,也可以订阅 Topic 接收云端下发的指令。平台侧还能做规则引擎,把数据转发到数据库或其他服务。
这四层里,STM32 和 ESP8266 之间的 AT 指令交互是最容易翻车的地方,因为它是异步的、字符串匹配的、对时序敏感的。源码里通常会用环形缓冲区加状态机来处理串口数据,而不是简单用HAL_UART_Receive阻塞等待。
2.2 目录结构:拿到源码先看哪几个文件
一套典型的 STM32+ESP8266+MQTT 工程,目录结构大致如下。不同开发者用的 IDE 不同,可能是 Keil MDK、STM32CubeIDE 或者 VSCode + PlatformIO,但文件组织逻辑是相通的。
Project/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── usart.h │ │ ├── gpio.h │ │ └── stm32f1xx_hal_conf.h │ └── Src/ │ ├── main.c // 主循环,初始化 + 业务调度 │ ├── usart.c // 串口初始化,含中断配置 │ ├── gpio.c │ └── stm32f1xx_it.c // 中断服务函数,串口接收中断在这里 ├── Drivers/ │ └── STM32F1xx_HAL_Driver/ // HAL 库 ├── Hardware/ │ ├── esp8266.c // AT 指令封装,核心文件 │ ├── esp8266.h │ ├── mqtt.c // MQTT 连接、发布、订阅封装 │ ├── mqtt.h │ └── sensor.c // 传感器读取,按实际硬件替换 └── User/ ├── aliyun.c // 阿里云三元组配置和 Topic 拼接 └── aliyun.h拿到源码后,优先看三个文件:esp8266.c里的 AT 指令发送与回显解析函数、mqtt.c里的连接和发布流程、aliyun.c里的三元组和 Topic 定义。这三个文件决定了项目能不能连上云。main.c反而是最后看的,因为它只是把这些模块串起来调用。
2.3 阿里云三元组与 Topic:连接前必须搞清楚的参数
阿里云物联网平台为每个设备生成唯一的三元组:ProductKey、DeviceName、DeviceSecret。这三个值在源码里通常定义在aliyun.h或aliyun.c中,形如:
// aliyun.h 设备身份信息,需替换为自己的三元组 #define PRODUCT_KEY "a1B2c3D4e5F" #define DEVICE_NAME "my_stm32_device" #define DEVICE_SECRET "0123456789abcdef0123456789abcdef" // MQTT 接入点,阿里云华东2节点示例 #define MQTT_HOST "a1B2c3D4e5F.iot-as-mqtt.cn-shanghai.aliyuncs.com" #define MQTT_PORT 1883 // 物模型通信 Topic #define TOPIC_PROPERTY_POST "/sys/a1B2c3D4e5F/my_stm32_device/thing/event/property/post" #define TOPIC_PROPERTY_SET "/sys/a1B2c3D4e5F/my_stm32_device/thing/service/property/set"这里的MQTT_HOST拼接规则是ProductKey.iot-as-mqtt.区域ID.aliyuncs.com,区域 ID 取决于你在阿里云控制台创建产品时选的节点。TOPIC_PROPERTY_POST用于设备上报属性,TOPIC_PROPERTY_SET用于接收云端下发的属性设置指令。这两个 Topic 是物模型通信的核心,源码里所有上报和下发都围绕它们展开。
MQTT 连接时的 clientId、username、password 不是随便填的,阿里云有固定拼接格式。clientId 一般是DeviceName|securemode=3,signmethod=hmacsha1|,username 是DeviceName&ProductKey,password 是用 DeviceSecret 对特定字符串做 HMAC-SHA1 签名后的结果。源码里通常会提供一个mqtt_password_generate函数来做这件事,或者直接让你在阿里云控制台生成好填进去。如果源码里没有签名函数,说明它用的是预生成密码的方式,改三元组后需要重新生成密码。
3. 核心模块拆解:AT 指令状态机与 MQTT 收发链路
3.1 串口接收:为什么不能用阻塞式 HAL_UART_Receive
STM32 和 ESP8266 通信,最忌讳的就是在主循环里用HAL_UART_Receive阻塞等数据。ESP8266 的 AT 回显是不定长的,可能回OK\r\n,也可能回+MQTTSUBRECV:0,"topic",10,data\r\n,阻塞等待要么等不够丢数据,要么等太久卡死主循环。合格的做法是串口中断 + 环形缓冲区,中断里把收到的字节塞进缓冲区,主循环或状态机去消费。
// esp8266.c 串口环形缓冲区定义 #define RX_BUFFER_SIZE 512 static uint8_t rx_buffer[RX_BUFFER_SIZE]; static volatile uint16_t rx_head = 0; // 写入位置,中断里更新 static volatile uint16_t rx_tail = 0; // 读取位置,主循环更新 // 串口接收中断回调,每收到一个字节触发一次 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART2) { uint16_t next = (rx_head + 1) % RX_BUFFER_SIZE; if (next != rx_tail) { // 缓冲区未满才写入,防止覆盖 rx_buffer[rx_head] = rx_byte; rx_head = next; } HAL_UART_Receive_IT(&huart2, &rx_byte, 1); // 重新开启接收中断 } } // 从缓冲区读一个字节,无数据返回 -1 int esp8266_read_byte(void) { if (rx_head == rx_tail) return -1; uint8_t data = rx_buffer[rx_tail]; rx_tail = (rx_tail + 1) % RX_BUFFER_SIZE; return data; }这段代码的关键点有三个。第一,rx_head和rx_tail用volatile修饰,因为一个在中断里改、一个在主循环里改,编译器不能优化掉。第二,缓冲区满时丢弃新数据而不是覆盖旧数据,保证已收到的指令回显不被破坏。第三,每次中断回调末尾必须重新调用HAL_UART_Receive_IT,否则只会接收一个字节就停了,这是 HAL 库的常见坑。
3.2 AT 指令发送与回显匹配:状态机怎么啃字符串
有了环形缓冲区,下一步是发指令和等回显。源码里通常有一个esp8266_send_cmd函数,发完指令后在一个超时窗口内不断从缓冲区读数据,匹配预期关键字。
// esp8266.c AT 指令发送与回显等待 // cmd: 要发送的 AT 指令字符串 // expect: 期望的回显关键字,如 "OK" 或 ">" // timeout_ms: 超时时间,单位毫秒 // 返回 0 表示匹配成功,-1 表示超时 int esp8266_send_cmd(const char *cmd, const char *expect, uint32_t timeout_ms) { esp8266_clear_buffer(); // 清空残留数据 HAL_UART_Transmit(&huart2, (uint8_t *)cmd, strlen(cmd), 1000); HAL_UART_Transmit(&huart2, (uint8_t *)"\r\n", 2, 1000); uint32_t start = HAL_GetTick(); uint16_t match_idx = 0; uint16_t expect_len = strlen(expect); while ((HAL_GetTick() - start) < timeout_ms) { int ch = esp8266_read_byte(); if (ch < 0) continue; // 暂无数据,继续等 if (ch == expect[match_idx]) { match_idx++; if (match_idx == expect_len) return 0; // 完整匹配 } else { match_idx = (ch == expect[0]) ? 1 : 0; // 失配后判断是否重新开始 } } return -1; // 超时 }这个匹配逻辑用的是最朴素的逐字符比对,但加了一个失配回退:如果当前字符不等于期望字符,但等于期望的第一个字符,就把match_idx置 1 而不是 0。这样能处理OK\r\nOK\r\n这种连续回显里第二个 OK 被漏掉的情况。实际项目中,回显匹配还要处理ERROR、FAIL等异常关键字,通常会在调用处根据返回值做分支,而不是在send_cmd里硬编码。
参数说明:cmd不带\r\n,函数内部自动补;expect一般传"OK",连接 Wi-Fi 时传"WIFI GOT IP",MQTT 连接时传"CONNACK"或"+MQTTCONNECTED",具体取决于 AT 固件版本;timeout_ms连 Wi-Fi 给 10000 以上,普通 AT 指令给 2000 足够。
3.3 MQTT 连接与发布:从三元组到一条属性上报
MQTT 连接流程在源码里通常封装成mqtt_connect函数,内部依次发几条 AT 指令。以 ESP8266 AT 固件为例,顺序是:配置 MQTT 用户参数、配置 MQTT 连接参数、发起连接、等待 CONNACK。
// mqtt.c MQTT 连接流程 int mqtt_connect(void) { char cmd[256]; // 1. 配置 MQTT clientId、username、password // clientId 格式: DeviceName|securemode=3,signmethod=hmacsha1| snprintf(cmd, sizeof(cmd), "AT+MQTTUSERCFG=0,1,\"%s|securemode=3,signmethod=hmacsha1|\",\"%s&%s\",\"%s\",0,0,\"\"", DEVICE_NAME, DEVICE_NAME, PRODUCT_KEY, mqtt_password); if (esp8266_send_cmd(cmd, "OK", 2000) != 0) return -1; // 2. 配置 MQTT 连接参数:接入点、端口、保持连接 snprintf(cmd, sizeof(cmd), "AT+MQTTCONNCFG=0,60,0,\"\",\"\",0,0"); if (esp8266_send_cmd(cmd, "OK", 2000) != 0) return -1; // 3. 发起 MQTT 连接 snprintf(cmd, sizeof(cmd), "AT+MQTTCONN=0,\"%s\",%d,1", MQTT_HOST, MQTT_PORT); if (esp8266_send_cmd(cmd, "+MQTTCONNECTED", 10000) != 0) return -1; return 0; } // 上报一条属性,payload 为 JSON 字符串 int mqtt_publish_property(const char *payload) { char cmd[512]; snprintf(cmd, sizeof(cmd), "AT+MQTTPUB=0,\"%s\",\"%s\",0,0", TOPIC_PROPERTY_POST, payload); return esp8266_send_cmd(cmd, "OK", 3000); }mqtt_connect里三步的顺序不能乱:必须先MQTTUSERCFG再MQTTCONNCFG最后MQTTCONN,否则 ESP8266 会返回ERROR。mqtt_password是预先用 DeviceSecret 算好的 HMAC-SHA1 签名,源码里可能是一个硬编码字符串,也可能有一个hmacsha1函数在启动时计算。如果是硬编码,改三元组后必须重新生成,否则连接会被阿里云拒绝,返回CONNACK里的错误码。
mqtt_publish_property的 payload 是 JSON 格式,阿里云物模型要求形如{"params":{"temperature":25.6,"humidity":60}}。Topic 用TOPIC_PROPERTY_POST,QoS 传 0 表示最多一次,传 1 表示至少一次。源码里通常用 0,因为传感器数据丢一两条不影响,用 1 会增加重传逻辑的复杂度。
3.4 云端指令下发:订阅 Topic 与解析 payload
设备要接收云端下发的指令,需要在 MQTT 连接成功后订阅TOPIC_PROPERTY_SET。订阅之后,ESP8266 收到消息会通过串口主动上报,格式类似+MQTTSUBRECV:0,"/sys/.../thing/service/property/set",42,{"params":{"led":1}}。STM32 需要在主循环里不断检查缓冲区,识别这个前缀并解析出 payload。
// mqtt.c 检查并解析订阅消息 // 返回 1 表示收到消息,payload 拷贝到 out 缓冲区 int mqtt_check_subrecv(char *out, uint16_t out_size) { char line[256]; if (esp8266_read_line(line, sizeof(line), 100) != 0) return 0; // 匹配 +MQTTSUBRECV 前缀 if (strstr(line, "+MQTTSUBRECV") == NULL) return 0; // 定位最后一个逗号后的 payload 部分 char *last_comma = strrchr(line, ','); if (last_comma == NULL) return 0; strncpy(out, last_comma + 1, out_size - 1); out[out_size - 1] = '\0'; return 1; }esp8266_read_line是一个按行读取的辅助函数,从环形缓冲区里读到\n为止。strstr匹配前缀,strrchr找最后一个逗号,是因为 payload 本身可能包含逗号,但 Topic 和长度字段里不会。解析出来的 JSON 字符串再交给一个轻量 JSON 解析函数,比如cJSON或者手写的键值提取,去拿params里的具体字段。源码里如果没带 JSON 库,通常就是手写strstr找"led":然后取后面的数字,够用但不健壮,二次开发时建议换成 cJSON。
4. 编译、烧录与联调:从零把设备连上阿里云
4.1 工程导入与编译环境准备
这套源码大概率是 Keil MDK 工程,也可能是 STM32CubeIDE 工程。先看目录里有没有.uvprojx或.project文件。如果是 Keil,需要装好 STM32F1 或 F4 的 Device Pack,具体型号看main.h里的#include "stm32f1xx_hal.h"还是stm32f4xx_hal.h。打开工程后先别急着编译,检查三个地方:芯片型号是否和你手上的板子一致、晶振频率是否匹配、下载器配置是 ST-Link 还是 J-Link。
编译前还要确认串口宏定义。源码里通常用#define ESP8266_UART huart2这种方式指定串口,如果你的 ESP8266 接在 USART3 上,就得改这里,同时改usart.c里的初始化。改完编译,如果报undefined reference多半是某个.c文件没加进工程组,在 Keil 里右键 Add Existing Files 补上。
烧录时注意 BOOT0 跳线。STM32 从 Flash 启动要求 BOOT0 接地,如果之前用来做串口下载改了跳线,记得改回来。烧录成功后复位,板子应该开始跑主循环。
4.2 阿里云侧配置:产品、设备与物模型
在阿里云物联网平台控制台,先创建一个产品,节点类型选“直连设备”,联网方式选“Wi-Fi”,数据格式选“Alink JSON”。创建产品后会生成 ProductKey。然后在这个产品下创建设备,填一个 DeviceName,平台会生成 DeviceSecret。这三个值填到源码的aliyun.h里。
接着配置物模型。物模型是阿里云对设备能力的抽象,你需要定义属性,比如temperature(float,读写)、humidity(float,只读)、led(bool,读写)。定义完发布,平台会自动生成对应的 Topic。TOPIC_PROPERTY_POST用于上报,TOPIC_PROPERTY_SET用于下发。如果你在源码里看到的 Topic 和平台生成的不一样,以平台生成的为准,因为不同区域、不同产品类型的 Topic 前缀可能不同。
注意:三元组里的 DeviceSecret 只在创建时显示一次,关掉页面就看不到了。如果丢了,只能在控制台重置,重置后源码里的密码也要重新生成。
4.3 联调步骤:先串口,再 Wi-Fi,最后 MQTT
联调不要一上来就烧录完整程序然后盯着云端看设备在不在线,那样出了问题不知道卡在哪一层。按顺序来:先单独测 ESP8266 的 AT 指令,再测 Wi-Fi 连接,最后测 MQTT。
第一步,把 ESP8266 通过 USB 转 TTL 接到电脑,用串口助手发AT,回OK说明模组和波特率正常。发AT+CWMODE=1设为 Station,发AT+CWJAP="ssid","password"连 Wi-Fi,回WIFI GOT IP说明联网成功。这一步能过,说明硬件和网络没问题。
第二步,把 ESP8266 接回 STM32,烧录源码,在esp8266_send_cmd里加串口打印,看每条 AT 指令的返回值。如果卡在连 Wi-Fi,检查ssid和password有没有写错,注意大小写和特殊字符。如果卡在 MQTT 连接,看返回的是CONNACK还是ERROR,ERROR多半是三元组或密码不对。
第三步,连接成功后,在阿里云控制台的设备详情页看“物模型数据”,应该能看到上报的属性值。在“在线调试”里给设备下发一条led=1的指令,看 STM32 串口有没有打印出收到的 payload。如果云端显示设备在线但收不到数据,检查 Topic 是否拼错,尤其是 ProductKey 和 DeviceName 的大小写。
4.4 常见问题排查:连不上、掉线、收不到
现象一:ESP8266 一直回ERROR,连AT都不回OK。原因通常是波特率不对或供电不足。ESP8266 默认波特率可能是 115200 也可能是 9600,用串口助手试两个值。供电方面,ESP8266 峰值电流能到 300mA,如果从 STM32 的 3.3V 引脚直接取电,很可能带不动,表现为反复重启或回显乱码。解决方法是单独给 ESP8266 供 3.3V,或者加一个大电容缓冲。
现象二:Wi-Fi 能连上,但 MQTT 连接返回ERROR。原因多半是三元组或密码错误。检查PRODUCT_KEY、DEVICE_NAME、DEVICE_SECRET是否和控制台一致,注意不要有多余空格。如果密码是预生成的,确认它是对当前 DeviceSecret 做的签名,换了设备就要重新生成。另外检查MQTT_HOST的区域 ID 是否和控制台节点一致,华东2是cn-shanghai,华北2是cn-beijing。
现象三:设备上线后频繁掉线。原因可能是 MQTT 心跳间隔设置过长,或者 Wi-Fi 信号弱。AT+MQTTCONNCFG里的 keepalive 参数默认 60 秒,如果网络不稳定可以调到 30 秒。另外检查 STM32 主循环里有没有长时间阻塞的操作,比如延时超过 keepalive 时间,导致 ESP8266 等不到心跳包而断开。
现象四:云端下发指令,STM32 收不到。先确认订阅是否成功,AT+MQTTSUB返回OK只代表指令被接受,还要等+MQTTSUBRECV或SUBACK才算订阅生效。然后检查mqtt_check_subrecv里的前缀匹配是否和实际回显一致,不同 AT 固件版本回显格式可能不同,用串口打印原始数据确认。最后检查 payload 解析,如果 JSON 里字段名和物模型定义的不一致,解析会失败但不会报错。
5. 二次开发改造:换传感器、加指令、做低功耗
5.1 替换传感器与上报格式
源码里的sensor.c通常是一个示例,读的是某个具体传感器,比如 DHT11 温湿度。换成你自己的传感器,只需要改sensor_read函数的实现,保持返回结构体不变。上报格式在mqtt_publish_property的 payload 拼接处改,阿里云物模型要求 JSON 的键名和物模型属性标识符一致。
// sensor.c 替换为读取光照强度的示例 typedef struct { float temperature; float humidity; float illuminance; // 新增光照字段 } sensor_data_t; int sensor_read(sensor_data_t *data) { >// 处理云端下发的属性设置指令 void handle_property_set(const char *payload) { // payload 形如 {"params":{"led":1,"motor":0}} if (strstr(payload, "\"led\":1") != NULL) { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); } else if (strstr(payload, "\"led\":0") != NULL) { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET); } if (strstr(payload, "\"motor\":1") != NULL) { motor_start(); } else if (strstr(payload, "\"motor\":0") != NULL) { motor_stop(); } }这种strstr匹配的方式简单直接,但不够健壮,如果 JSON 里字段顺序变化或者有嵌套,容易误判。更稳妥的做法是引入 cJSON,把 payload 解析成对象再按 key 取值。cJSON 只有两个文件,加到工程里不费事,二次开发时建议直接换掉手写匹配。
5.3 低功耗与断线重连的改造思路
如果项目是电池供电,低功耗是绕不开的。STM32 可以在上报间隔进入 Stop 模式,ESP8266 也要用AT+SLEEP指令进入睡眠。但要注意,MQTT 连接在睡眠期间会断开,唤醒后需要重连。改造思路是把mqtt_connect封装成一个可重入的函数,在主循环里检测连接状态,断了就重连。
// 主循环里的连接状态检测与重连 void mqtt_keepalive_task(void) { static uint32_t last_check = 0; if (HAL_GetTick() - last_check < 5000) return; // 每 5 秒检查一次 last_check = HAL_GetTick(); if (esp8266_send_cmd("AT+MQTTCONN?", "OK", 1000) != 0) { // 查询失败,说明连接可能已断,重新连接 mqtt_connect(); mqtt_subscribe_property_set(); } }断线重连的关键是重连后要重新订阅 Topic,因为 MQTT 会话状态在断开后不一定保留。如果用的是 clean session,每次重连都要重新订阅。另外重连不要过于频繁,加一个退避策略,比如第一次等 1 秒,第二次等 2 秒,最多等 30 秒,避免网络故障时疯狂重连耗电。
5.4 一个容易忽略的细节:AT 指令回显的时序竞争
最后说一个我踩过的坑。AT 指令发送和回显接收之间有时序竞争:如果esp8266_send_cmd在发送前清空了缓冲区,但上一条指令的回显还在路上,清空后刚好把新回显的前几个字节也清掉了,导致匹配失败。解决方法是清空缓冲区后加一个短延时,或者用“发送前记录当前 head 位置,只消费之后的数据”的方式。
// 改进的发送前清空:只丢弃已到达的数据,不丢后续 void esp8266_clear_buffer(void) { __disable_irq(); rx_tail = rx_head; // 直接把读指针追到写指针,丢弃所有已收数据 __enable_irq(); }这个函数在临界区里把rx_tail直接设为rx_head,比逐个读字节快,也不会在清空过程中被中断打断。但即便如此,发送指令后仍要留足超时时间,因为 ESP8266 处理 AT 指令本身有延迟,尤其是连 Wi-Fi 和 MQTT 连接这种耗时操作,超时给短了会误判失败。
从那以后我每次改 AT 指令相关的代码,都会先在串口助手上把完整指令序列跑一遍,确认每条的回显和时序,再写进 STM32 里。这个习惯帮我省了很多对着代码干瞪眼的时间。希望帮到你。
本文还有配套的精品资源,点击获取