STM32+ESP8266 AT指令上云:MQTT协议与阿里云接入全解析
2026/9/12 18:01:31 网站建设 项目流程

简介:这是一份基于STM32与ESP8266通过AT指令连接阿里云物联网平台的完整工程源代码,主要面向正在学习嵌入式设备上云、STM32开发或物联网通信的开发者,也适合课程设计及毕业设计参考。工程基于STM32F1系列,使用ESP8266以AT方式接入Wi-Fi与阿里云,覆盖串口指令交互、MQTT连接、数据上报与接收等关键流程,代码结构清晰,可直接对照移植。压缩包共291个文件,以c和h源码、编译生成的o/axf/hex等文件为主,另有uvprojx工程配置、keilkilll清理脚本、map/lst辅助文件,整体体积约6.38MB,减轻下载负担。目前已有1241人学习下载。资料内包含完整可编译的MDK工程、ESP8266通信代码及GPIO、定时器、ADC、OLED显示等外设驱动,既可用于快速搭建云端通信链路,也能学习STM32外设与AT指令的配合方法;通过阅读源码可掌握阿里云物联网平台的设备认证、主题发布订阅等实现思路,显著降低从零上云的门槛。

1. 用 AT 把 STM32 送进阿里云,最容易卡壳的不是接线

STM32、ESP8266、阿里云三个词放在一起,直觉上这是一条“WiFi 模组建立连接”的硬件命题,真到修改源代码跑起来时才发现,卡住的几乎都在握手时序与协议栈分工。这里的“AT 方式”意味着 ESP8266 运行 AT 固件,只负责把 TCP 字节流送到云端,MQTT 报文内容是 STM32 主控一侧逐个字节拼出来再通过串口发出去的。WiFi 已连接不代表 TCP 已建立,TCP 已建立也不代表 MQTT 校验通过,任何一个环节的应答超时、粘包或者重传,都会让设备在云平台上反复“在线”“离线”,最终表现为数据死活上不去。这篇把 STM32 通过 ESP8266 走 AT 指令连接阿里云这个常见工程场景,拆成协议分段、串口通路、签名接入、稳定性四部分,穿插可复用的命令序列和 C 代码,新手能照着跑通,老手可以对照检查自己的状态机设计。

2. 链路先分清楚:ESP8266 只做 TCP,MQTT 和业务逻辑留在 STM32

2.1 为什么选 AT 而不是把 SDK 烧进 ESP8266

很多人拿到类似题目第一反应是“直接把 MQTT 库移植到 ESP8266 上跑不是更方便吗”。这个思路本身没有错,NodeMCU 固件、ESP8266 RTOS SDK 都能以较短代码把数据推到云端。但“AT 方式”这个标题已经替你做了选型,而且这个选型在工程上有着明确理由:把协议栈放在 STM32 侧,ESP8266 退化为一个可替换的通信外设,后续换 Wi-Fi 模组、换 4G 模组,甚至换云平台,都不需要重写一套云端协议。

两者的界限可以理解为“固件职责不同”:

对比维度AT 方式(本文主线)SDK 方式(烧进 ESP8266)
MQTT 协议栈位置STM32 主控ESP8266 内部
STM32 代码量需要处理 AT 状态机、MQTT 组包、重连只需要发 JSON 给串口/SPI
更换模组成本只改 AT 初始化层,协议层不动几乎全部重来
排障手段串口能看到完整字节流,可直接抓包云端日志为主,本地难观测
适合场景产品化、多规格硬件、协议需要深度定制快速打样、固件版本迭代极快的小单品

实际项目里还有一个容易忽略的理由:AT 方式把网络状态暴露在串口字节里,调试时只需要一条 USB 转 TTL 线就能在 PC 上回放全部交互,而 SDK 方式一旦跑进死循环、打印又被 WiFi 中断抢占,只能靠 LED 和 JTAG 猜。何况很多 STM32 项目里主控的资源是富裕的,把 MQTT 包解析放在主控能明显降低对 ESP8266 固件版本的依赖,同一套 STM32 工程可以随意切换 ESP8266 和 ESP32-C3。

2.2 一条数据从 STM32 到阿里云,经过哪些 AT 环节

以最常见的“非透传模式”为例,STM32 要先通过 AT 指令让 ESP8266 完成三件事:连接路由器、建立 TCP 长连接、把 MQTT 数据载荷送出去。ESP8266 对 TCP 之上的内容完全无感知,它不解析 MQTT 头,也不认识 0x10 这个 CONNECT 报文标记,只把它当成普通 TCP 数据段搬运。

发送一个 MQTT 报文的全过程是这样的:

# 1. 建 TCP 长连接,返回值里 CONNECT 表示远端接受 AT+CIPSTART="TCP","产品密钥.iot-as-mqtt.cn-shanghai.aliyuncs.com",1883 # 期望回应: CONNECT\r\nOK\r\n # 2. 声明接下来要发多少字节(此处 74 只是示意长度) AT+CIPSEND=74 # 期望回应: > # 3. 紧接着把 MQTT CONNECT 报文的 74 个原始字节发出去 # 期望回应: Recv 74 bytes\r\nSEND OK\r\n

接收方向,TCP 数据回来时 ESP8266 会自动加一行前缀,形如+IPD,126:...。冒号后面的内容就是阿里云返回的 MQTT CONNACK 或属性上报应答。这里真正考验代码的不是 AT 指令本身,而是对+IPD前缀和SEND OK两个“夹心层”的时序把握。发送前必须确保收到>才能填数据,发送后必须清空缓冲区等待SEND OKERROR,否则下一帧会和残留字节黏在一起。

提示:调试初期不要急着用 AT+CIPMODE=1 透明传输模式。透传省去了每次的AT+CIPSEND头,但也去掉了+IPD长度标记,包边界完全靠 MQTT 自己的剩余长度字节来切,异常掉线时很难判断是网络抖动还是主控解析错误。先走带长度的非透传方式,跑顺后再优化。

2.3 阿里云的接入参数:1883 端口、securemode 与三元组

阿里云物联网平台的 MQTT 接入地址并不是一串固定 IP,而是带产品密钥的域名,一般格式是{productKey}.iot-as-mqtt.cn-shanghai.aliyuncs.com这种结构,华南、新加坡等地域会换成对应 region 字段。端口有三个常见选择:1883 是明文 MQTT,8883 是 TLS 加密 MQTT,443 用于 MQTT over WebSocket。基于 ESP8266 AT 固件的项目大多走 1883,原因是模块侧 TLS 握手在低功耗场景下既慢又容易因堆栈不足重启,而阿里云允许在明文链路上通过 HMAC-SHA1 签名验证设备身份,配套的securemode=3参数就是这个用途。

接入时的 MQTT CONNECT 报文里,需要塞进一组和三元组绑定的身份信息:

MQTT 字段取值规则来源
clientId`{deviceName}securemode=3,signmethod=hmacsha1,timestamp=毫秒时间戳
username{deviceName}&{productKey}自行拼接
password用 deviceSecret 对签名内容做 HMAC-SHA1 后 Base64计算得到
keepalive60 到 120 秒常见自行设定

把这层关系看懂后,就能理解为什么很多源代码包里的“连接”部分代码量和 WiFi 配网差不多大:AT 命令解决的是管道问题,MQTT 报文解决的是“你是谁”的问题,两块缺一不可。

3. 在 STM32 工程里点亮 AT 通路:配网、建 TCP、透传给 MQTT 让路

3.1 硬件和串口三个检查项,先把连接稳定性底座打牢

进入代码前先确认三件与“能否稳定连接”直接相关的硬件事项。

第一是电源。ESP8266 在发射瞬间电流会冲到 300mA 甚至更高,如果用 STM32 开发板上的 3.3V LDO 直接供电,电压跌落会导致模块反复复位。常见做法是单独用一颗 AMS1117-3.3 或者 MP1584 降压模块给 ESP8266 供电,STM32 与 ESP8266 之间只共享地线。

第二是电平匹配。ESP8266 的 GPIO 不是 5V tolerant,STM32 的 TX 如果直连过去,长时间运行有隐患。大多数情况两者都是 3.3V 逻辑,但如果你的 STM32 板子是 5V 供电且串口引脚没有电平转换,就必须在中间加电阻分压或者用 TXS0108 这类电平转换芯片。

第三是复位引脚时序。ESP8266 上电时 CH_PD 必须拉高,RST 引脚不要悬空。如果用了外部单片机去控制 ESP8266 复位,复位低电平时间至少要维持 10ms,否则模块可能进入奇怪的下载模式或启动失败。

串口参数上我一般这样安排:STM32 与 ESP8266 之间用独立的 USART,波特率 115200,8 数据位、无校验、1 停止位,并且这个串口只承担 AT 通信,不允许和调试日志共用。调试日志走另一个串口或者 SWO,否则 printf 和 AT 响应混在一个 DMA 缓冲区里,定位问题会很痛苦。

3.2 最小可复现的 AT 连接序列:配网到 TCP

在 STM32 代码里手写 AT 之前,建议先用 USB-TTL 工具在 PC 上把 ESP8266 的全部行为验证一遍,排除模块本身的坑。最小序列是这四条:

AT # OK AT+CWMODE=1 # OK,1 表示 station 模式 AT+CWJAP="wifi名称","wifi密码" # WIFI CONNECTED # OK AT+CIPSTART="TCP","a1xxxxxxxx.iot-as-mqtt.cn-shanghai.aliyuncs.com",1883 # CONNECT # OK

如果上面任何一步返回 ERROR,优先检查 AT 固件版本。较老的 ESP8266 AT 固件可能不支持AT+CIPSTART的域名解析,只接受 IP,这时需要先把域名解析成 IP 手动填进去。部分出厂固件默认工作在 AP 模式,直接执行AT+CWMODE=1报错,可以先用AT+CWMODE=3过渡,连接成功后再切回 station。

STM32 侧发送 AT 命令时不建议用阻塞式的delay(500);printf(...),代码要写成按状态推进的形式:

int at_send_wait(const char *cmd, const char *expect, uint32_t timeout_ms) { uart_send_blocking((uint8_t *)cmd, strlen(cmd)); // cmd 自带 \r\n uint32_t start = HAL_GetTick(); while (HAL_GetTick() - start < timeout_ms) { if (at_parser_hit(expect)) { // 查已解析的响应队列 return 0; /* 0 表示成功 */ } HAL_Delay(5); } return -1; /* 超时未命中 */ }

这个函数的要点是“不直接扫描 UART 接收寄存器”,而是读取解析器已经按行整理好的响应列表。at_parser_hit内部会遍历一个环形响应队列并做前缀匹配,匹配成功后把条目弹出,避免下一次误命中同一行。超时时间需要按阶段区分:AT给 1 秒,AT+CWJAP给 15 秒,AT+CIPSTART给 5 秒,因为 DHCP 和 TCP 握手耗时不同,统一超时会让慢网络下明明能连却判断失败。

3.3 接收路径:环形缓冲加行解析,别在中断里做字符串比较

ESP8266 的返回数据混杂着 OK、ERROR、+IPD:>提示符和原始负载,如果在串口中断里逐字符比对字符串,一方面占用中断时间过长,另一方面多字节状态无法跨中断保存。更稳妥的结构是一级环形缓冲区加一级行解析器:

#define AT_RX_BUF_SIZE 1024 static volatile uint8_t at_ring[AT_RX_BUF_SIZE]; static volatile uint16_t at_ring_head, at_ring_tail; static uint8_t at_line_buf[128]; static uint16_t at_line_len; void USART_IRQHandler(void) { uint8_t byte = (uint8_t)READ_REG(huart.Instance->DR); at_ring[at_ring_head] = byte; at_ring_head = (at_ring_head + 1) % AT_RX_BUF_SIZE; } uint16_t at_ring_read_line(uint8_t *out, uint16_t max_len) { uint16_t len = 0; while (at_ring_tail != at_ring_head) { uint8_t c = at_ring[at_ring_tail]; at_ring_tail = (at_ring_tail + 1) % AT_RX_BUF_SIZE; if (c == '\n') { out[len] = 0; // 一行以 \n 收尾 return len; } if (c != '\r' && len < max_len) out[len++] = c; } return 0; /* 还没凑齐一行 */ }

主循环里调用at_ring_read_line得到完整行,然后按三个前缀分派:OK表示命令成功;+IPD表示有 TCP 数据到达,需要从后续字节流里读取 MQTT 报文;其他内容统一交给状态机。这里有一个常见的错误做法:把对+IPD的解析和行的收集混在一起,遇到负载本身含有\r\n时就把 MQTT 报文切碎了。应对办法是单独维护一个ipd_remaining_len计数器,读到+IPD,len:后先把长度值解析出来,后续字节直接送入 MQTT 帧缓冲,等计数归零再重新回到行模式。这个设计是整个串口层的核心,后面 MQTT 报文能不能拼完整,全看这里是否严谨。

4. STM32 侧写阿里云接入:三元组签名、CONNECT 报文和属性上报

4.1 第一步先在 PC 上把签名验证逻辑跑通

阿里云 MQTT 建连时,password 的生成规则是许多项目里最容易算错的地方。设备三元组是 ProductKey、DeviceName、DeviceSecret,其中前两个是明文身份,DeviceSecret 是签名密钥。完整的签名内容按这个规则拼接:

clientId + {完整的 clientId 字符串} deviceName + {deviceName} productKey + {productKey} timestamp + {毫秒时间戳}

这里的“完整的 clientId 字符串”指的就是 MQTT 报文里那个带竖线分隔符的版本,例如sensor01|securemode=3,signmethod=hmacsha1,timestamp=1710000000000|。注意签名内容和 MQTT 报文里填的 clientId 必须完全一致,一个字符都不能差,否则云端会返回连接拒绝。

在 STM32 上有两条路线,一是移植 mbedTLS 直接调用 HMAC-SHA1,二是用开源的轻量级 HMAC 实现。只要工程里已经有 mbedTLS,推荐直接复用,代码量最小:

#include "mbedtls/md.h" static int calc_mqtt_password(const char *device_secret, const char *sign_content, uint8_t *out_md, size_t *out_len) { const mbedtls_md_info_t *md_info; md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA1); if (md_info == NULL) return -1; uint8_t digest[20]; mbedtls_md_context_t ctx; mbedtls_md_init(&ctx); mbedtls_md_setup(&ctx, md_info, 1); // 第二个参数 1 表示使用 HMAC mbedtls_md_hmac_starts(&ctx, (const uint8_t *)device_secret, strlen(device_secret)); mbedtls_md_hmac_update(&ctx, (const uint8_t *)sign_content, strlen(sign_content)); mbedtls_md_hmac_finish(&ctx, digest); mbedtls_md_free(&ctx); // 对 digest 做 Base64,写入 out_md base64_encode(digest, sizeof(digest), out_md, out_len); return 0; }

签名内容的字符串建议放到一个足够大的静态缓冲区里,用 snprintf 拼接。timestamp 必须和设备当前时间戳一致,很多 STM32 项目没有 RTC 电池,上电时间从 1970 年开始,最后会被云端以“签名过期”拒绝。常规处理是上电后先连一次 NTP 服务器,或者从上次保存的 RTC 里恢复时间后校时。没有条件连 NTP 的话,至少保证模块每次重连时间戳是递增的。

4.2 把 MQTT CONNECT 报文逐字节拼出来

阿里云物联网平台兼容 MQTT 3.1.1,CONNECT 报文的固定头是 0x10,之后是剩余长度,再依次是协议名、协议级别、连接标志、保活时间和 payload。下面这段代码展示了如何把前面算好的三元组信息装进一个字节数组:

static uint16_t build_mqtt_connect(uint8_t *buf, const mqtt_conn_info_t *info) { uint16_t pos = 2; // 0x10 和剩余长度先占位 const char *proto = "MQTT"; buf[pos++] = 0x00; buf[pos++] = 0x04; memcpy(&buf[pos], proto, 4); pos += 4; buf[pos++] = 0x04; // MQTT 3.1.1 协议级别 buf[pos++] = 0xC0; // 0x80 用户名 + 0x40 密码 buf[pos++] = 0x00; buf[pos++] = 90; // 保活 90 秒 // clientId 字段 pos += mqtt_encode_utf8(&buf[pos], info->client_id, strlen(info->client_id)); // username 字段 pos += mqtt_encode_utf8(&buf[pos], info->username, strlen(info->username)); // password 字段 pos += mqtt_encode_utf8(&buf[pos], (char *)info->password, info->password_len); uint8_t remain_len = pos - 2; // 剩余长度不含固定头 buf[0] = 0x10; buf[1] = remain_len; // 当剩余长度小于 128 时直接用 1 字节 return pos; }

mqtt_encode_utf8做的是“两字节长度前缀 + 内容”,这是 MQTT 字符串的标准编码方式,不能省略。密码字段虽然是二进制 Base64 文本,但同样要前置长度。剩余长度如果超过 127 字节,需要用 MQTT 的变长编码规则拆成多字节,属性上报场景通常不会超,但 CONNECT 报文可能接近,这个边界值得注意。

拼好报文后,走第 3 章的AT+CIPSEND=<len>流程发出去,然后静默等待+IPD数据。平台返回的 CONNACK 报文固定是0x20 0x02 0x00 0x00这种形态,其中最后一个字节 0 表示连接成功,非 0 则要看是我们签名错了还是 clientId 撞了。

4.3 上报属性数据并识别平台应答

属性上报是物联网设备最频繁的操作。阿里云的属性上报 Topic 格式为/sys/{productKey}/{deviceName}/thing/event/property/post,载荷是一段固定结构的 JSON。设备上行的报文格式通常是这样:

{ "method": "thing.event.property.post", "id": "1700000001", "params": { "Temperature": 36.5, "Humidity": 52.0 }, "version": "1.0" }

往这个 Topic 发数据,本质上仍是一次普通 MQTT PUBLISH,报文头是 0x30 或 0x32。按照 MQTT 3.1.1 的规则,PUBLISH 包里要依次放 Topic 字符串、可选的报文标识符、然后是载荷。QoS 0 时不需要包 ID,QoS 1 时才需要,而且 QoS 1 的 PUBLISH 对端会回 PUBACK。阿里云在 IoT 场景默认推荐 QoS 0,因为链路本身是长连接 TCP,QoS 0 足够,还能省去重传状态管理。

属性上报需要等待平台的确认。正常流程是云端在同一 Topic 上以/post_reply结尾的响应 Topic 回一条类似这样的消息:

{"code":0,"data":{},"id":"1700000001","message":"success","version":"1.0"}

设备侧解析时要重点对 id 字段做匹配,因为多条上报在途时会收到乱序回复。直接把最近收到的 JSON 当本次上报结果,会造成业务逻辑误判。建议把 id 设计成自增计数器,收到 reply 后去查对应序号,超过 5 秒没有 reply 就重新上报。

4.4 用状态机管理建连全过程,避免到处是 goto

把第 3 章的 AT 状态和第 4 章 MQTT 建连串起来,一个合理的主控逻辑状态机是下面这样:

typedef enum { AP_STAGE_INIT, // 上电,等待模块启动 AP_STAGE_WIFI, // AT+CWJAP 配网 AP_STAGE_TCP, // AT+CIPSTART 建 TCP AP_STAGE_MQTT_SEND, // AT+CIPSEND 发 MQTT CONNECT AP_STAGE_MQTT_WAIT, // 等 CONNACK AP_STAGE_RUNNING // 正常收发属性/事件 } app_stage_t;

每个 stage 内部只做一件事,完成后推进到下一 stage;失败时则根据错误类型决定是回到 WIFI 阶段还是直接硬件复位。我见过很多源码把 AT 的 OK 判断散落在各个 while 循环里,用 flag 变量互相穿插,最后连接逻辑完全不可读,所以强烈建议用这种显式状态机替代连锁式 if。在 RUNNING 阶段,每收到一帧+IPD就交给 MQTT 解析器,同时维护一个上次收到任意数据的时间戳,用于后续的保活判断。

5. 链路通了以后再看这四处:认证偏差、心跳、断线重连和离线验证

5.1 用 Python 回放 AT 序列,把问题限定在 MCU 之外

在 STM32 上反复烧录调试太慢,更高效的做法是先把 ESP8266 摘出来,用 PC 串口回放整套交互,确认模块和阿里云侧的配置没有问题时再回到代码现场。下面这段是基于 pyserial 的回放骨架:

import serial, time ser = serial.Serial("COM10", 115200, timeout=2) def at(cmd: bytes, wait: float = 2.0) -> bytes: ser.reset_input_buffer() ser.write(cmd + b"\r\n") time.sleep(wait) return ser.read(ser.in_waiting or 1) print(at(b"AT")) print(at(b"AT+CWMODE=1")) print(at(b'AT+CWJAP="wifi","pass"', wait=15)) print(at(b'AT+CIPSTART="TCP","a1xxxx.iot-as-mqtt.cn-shanghai.aliyuncs.com",1883', wait=8))

回放时一旦发现AT+CIPSTART返回 ERROR,基本可以断定是域名解析或者端口不通,和后续 MQTT 签名无关;如果 CONNECT 阶段收到+IPD里是0x20 0x02 0x00 0x05,则基本可以锁定签名或 clientId 构造问题,这时直接在 PC 上修正时间戳和三元组,比在 MCU 上猜更快。

5.2 四个最容易留下隐患的细节

先说保活周期。MQTT 保活时间由 CONNECT 报文里的两字节决定,阿里云要求设备在保活时间内必须有任意报文活动。如果属性上报频率本身就低于 90 秒,必须额外发送 PINGREQ 报文,也就是0xC0 0x00两个字节。这是 MCU 侧自己组包发送,不依赖 AT 指令额外动作,把它做到运行状态的周期任务里即可。

再说时间戳。前面提到 HMAC 签名里的 timestamp,很多源码在设备上电后不校时就能跑通,原因是开发环境的云端签名过期策略比较宽松。生产环境务必让 STM32 在 WiFi 连接成功后先取一次 NTP 时间,否则设备长时间断电再上电,签名必定失败。

第三是断线重连策略。不要做成“断线后立刻重连”的忙循环,这会同时把 ESP8266 和云端网关搞到限流。推荐指数退避:第一次失败等 3 秒,第二次 10 秒,第三次 30 秒,超过五次后复位 ESP8266 重新走完整 AT 流程。复位后等 2 秒再发第一条 AT,给模块留出自检时间。

第四是离线验证。不要只看 MQTT 建连成功就认为链路健康,应该周期性主动上报并在规定时间内等待对应 reply,连续三次等不到回复就主动断开重连。这个方法虽然简单,却能在路由器断网但 TCP 未感知时,让设备在几十秒内自动恢复,而不是挂在半死连接上直到云端超时踢掉。把这四个检查点写进工程,STM32 通过 ESP8266 以 AT 方式跑阿里云的链路基本就具备上线条件了。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询