简介:基于MQTT协议实现STM32数据上云与微信小程序远程控制,是一套可直接运行的嵌入式物联网项目方案,适合毕业设计、课程设计、工程实训及学科竞赛等场景。压缩包共262个文件,约7.94MB,以STM32F10x标准库C源码(53个.c)和头文件(55个.h)为主,并包含工程文件、编译产物、说明文档及微信小程序交互代码,覆盖USART、ADC、定时器、I2C、CAN等常见外设驱动,便于二次开发。已有477人学习下载。资料提供完整源码与工程配置,经测试可编译烧录,同时对不熟悉硬件画板的小白,可用面包板加杜邦线连接外设模块快速复刻。既可用作单片机与物联网结合的练手项目,也可在MQTT通信框架上扩展更多传感器或远程控制功能,适合作为云端远程控制原型的基础工程。
1. 为什么 STM32 设备上阿里云不直接用 TCP 协议而走 MQTT
先说结论:在毕设和竞赛这类短周期项目里,自定义 TCP 协议是时间开销最大的选择。你要自己处理粘包拆包、心跳保活、断线重连、请求应答对应,这些工作做完至少一周没了。而 MQTT 协议把这些问题全部变成标准行为,阿里云物联网平台已经实现了 broker 端闭环,设备端只需要关注三件事:算对签名、拼好 JSON、发布到正确 topic。这套资源就是沿这条链路搭的——STM32 采集数据,ESP8266 经 MQTT 上抛到阿里云,微信小程序订阅和发布完成远程控制,工程文件用 Keil5 打开就能编译。从课程设计到毕业设计,这条链路都是最稳妥的快速原型方案,拿到源码后换掉密钥和引脚定义就能复刻成自己的项目。
2. 接入链路拆解:MQTT Broker 参数、三要素签名与物模型 Topic
2.1 三层架构中每层关注的协议边界
把整个系统拆开,其实是四段设备围绕一张 MQTT broker 转。STM32 负责采集和控制,ESP8266 负责把串口数据变成 MQTT 报文,阿里云物联网平台负责认证转发,微信小程序负责可视化展示与远程下发。这里有一个经常被忽略的设计点:MQTT 的发布订阅模型天然地解耦了生产者和消费者,STM32 上报数据时并不知道小程序在不在线,小程序下发指令时也不需要知道 STM32 是否醒着,这一切由 broker 的 topic 订阅关系在中间维持。
因此,代码层面真正硬编码的东西只有三样:阿里云实例地址、设备三要素、三条 topic。对应到工程里就是宏定义和初始化串口时的数据结构。毕设答辩被问到的概率最大的问题就是“topic 为什么这么设计”,回答思路是自上而下一条链路:物模型的属性对应 topic 的 property,操作属性就是 set,属性变化就是 post。
2.2 连接参数与 HMAC 签名计算
阿里云物联网平台为每个设备预分配了三个唯一标识:productKey、deviceName、deviceSecret。MQTT 接入时认证用的是“动态签名”而不是静态密码——你必须用 deviceSecret 计算出一个一次性密码,这个密码里隐含了时间戳和设备身份。连接参数列表如下:
| 参数 | 拼接规则 | 示例 |
|---|---|---|
| MQTT broker 地址 | ${productKey}.iot-as-mqtt.cn-shanghai.aliyuncs.com | a1xxxxx.iot-as-mqtt.cn-shanghai.aliyuncs.com |
| 端口 | 1883(TCP)/ 443(WebSocket) | 1883 |
| clientId | `${productKey}.${deviceName} | securemode=2,signmethod=hmacsha1 |
| username | ${deviceName}&${productKey} | dev01&a1xxxxx |
| password | HMACSHA1(deviceSecret, 拼接串) | 32 位 hex 字符串 |
clientId 里的 securemode 可选 2 或 3,2 对应无加密 TCP,3 对应 TLS 加密。在设备端和 ESP8266 做联调时先用 securemode=2,链路通了再切 3,不然加密握手失败会浪费很多排错时间。password 的计算过程是整个接入方案的核心,拼接串格式必须严格按下面的顺序:
import hmac, hashlib, time product_key = "a1xxxxxxxxx" device_name = "stm32_dev01" device_secret = "29f5a1c2cf0b6ff0d8c7d7c6c6a9bc95" timestamp = str(int(time.time() * 1000)) client_id = "{}|securemode=2,signmethod=hmacsha1|".format( product_key + "." + device_name) content = "clientId{}deviceName{}productKey{}timestamp{}".format( client_id, device_name, product_key, timestamp) password = hmac.new(device_secret.encode(), content.encode(), hashlib.sha1).hexdigest() print("clientId:", client_id) print("username:", device_name + "&" + product_key) print("password:", password)这段脚本用于在烧录固件前验证密码是否算得对。如果烧进 STM32 后连不上,把这个脚本算出来的三要素填进 MQTTX 客户端连接一下,能连上就说明是单片机端的问题,连不上就说明签名参数有误。我遇到过比较多的情况是 timestamp 位数不对:阿里云要求毫秒级时间戳,秒级时间戳会导致签名不一致,重启后密码就只能用一分钟。
2.3 属性上报/下发的 Topic 结构与 JSON 物模型
在阿里云控制台的产品定义里,你新建的属性会生成对应的 topic 路径。设备不使用传统的自定义主题格式,而是统一走物模型标准 topic。常用三条:
| 场景 | topic |
|---|---|
| 属性上报 | /sys/{productKey}/{deviceName}/thing/event/property/post |
| 属性设置(云端到设备) | /sys/{productKey}/{deviceName}/thing/service/property/set |
| 属性设置回复 | /sys/{productKey}/{deviceName}/thing/service/property/set_reply |
上报 JSON 需要用固定外层结构包裹:
{ "id": "1001", "version": "1.0", "method": "thing.event.property.post", "params": { "Temperature": 26.5, "Humidity": 60.3, "Power": 1 } }params 里的字段名必须和物模型 identifier 完全一致。很多同学报错不是协议问题,而是大小写或下划线写错,云端那边会直接提示“数据格式错误”并在日志中心丢弃。设置类指令的格式类似,但 method 变为 thing.service.property.set,设备解析完必须在 15 秒内回复 set_reply,否则控制台会标红超时。
2.4 Topic 订阅与断线重连的联动
STM32 端的 MQTT 订阅动作要和连接成功回调绑定,而不是只写在主循环开头。我自己的工程是封装了一个 mqtt_connect() 函数,里面先完成连接、订阅、在线状态置位三步。断线后触发重连,重连成功会再次调用订阅流程,这样小程序端才不至于在设备重启后收不到任何回复。这个细节在资源里的 stm32f10x_tim.c 和 stm32f10x_usart.c 之间的配合上体现得比较明显,定时器中断负责周期上报,串口中断负责接收 AT 事件帧。
3. STM32 端实现:标准外设库、AT 指令上抛与控制指令解析
3.1 工程结构与外设对应关系
资源里的 Keil 工程使用标准外设库,涉及 stm32f10x_adc.c、stm32f10x_usart.c、stm32f10x_tim.c、stm32f10x_i2c.c、stm32f10x_can.c 等文件。打开工程后先在 stm32f10x_conf.h 里检查用到的外设宏是否打开,标准库默认很多是用不到的。实际接线:USART1_TX/PA9 接 ESP8266 的 RXD,USART1_RX/PA10 接 ESP8266 的 TXD,GND 共地,3.3V 供电注意 ESP8266 峰值电流可能到 300mA,建议单独供电而不是直接从板载稳压取电。ADC 采集用 PA0 通道 0,控制继电器用 PB1、PB2。
3.2 ESP8266 AT 指令序列与 MQTT 连接
ESP8266 固件版本建议至少 2.0 以上,太低不支持 MQTT AT 指令。STM32 的启动流程是先发AT\r\n试探,返回 OK 后依次配置模式、连接 WiFi、设置 MQTT 参数、建立连接。完整序列:
static const char *boot_cmd[] = { "AT\r\n", "AT+CWMODE=1\r\n", // 1=Station模式 "AT+CWJAP=\"MyWiFi\",\"12345678\"\r\n", // 连接无线网络 "AT+MQTTUSERCFG=0,1,\"NULL\",\"dev01\",\"password\",0,0,\"\"\r\n", "AT+MQTTCONN=0,\"a1xxxxx.iot-as-mqtt.cn-shanghai.aliyuncs.com\",1883,1\r\n", "AT+MQTTSUB=0,\"/sys/a1xxxxx/dev01/thing/service/property/set\",1\r\n" };AT+MQTTUSERCFG 的第三个参数是 clientID,第七第八个参数是用户名密码的开关位,如果用三要素签名方式,这里直接填上一步计算好的 clientId、username、password。AT+MQTTSUB 在每次重连后都要重新执行,所以这段序列不能只跑一次,建议封装成 mqtt_reconnect(),在 MQTT 连接异常断开时重新调用。
3.3 ADC 采样与属性上抛
属性上抛的代码放在主循环的定时器轮询里,比如每 5 秒采样一次 ADC 并发布:
uint16_t adc_value = adc_read_channel(ADC_Channel_0); float temperature = (float)adc_value * 3.3f / 4096.0f * 100.0f; char json[96]; sprintf(json, "{\"id\":\"%d\",\"version\":\"1.0\"," "\"params\":{\"Temperature\":%.1f,\"Humidity\":%.1f}," "\"method\":\"thing.event.property.post\"}", ++msg_id, temperature, humidity); char cmd[192]; sprintf(cmd, "AT+MQTTPUB=0,\"/sys/a1xxxxx/dev01/thing/event/property/post\",\"%s\",0,0\r\n", json); uart1_send(cmd);代码里最后两个 0 分别是 QoS 和 retain 标志。QoS 0 适用于周期性上报,丢了下一帧补上即可;控制指令涉及状态变化,建议用 QoS 1。retain 保持 false,否则 broker 会把最后一帧数据缓存,新设备上线立即收到过期数据。
3.4 property/set 指令解析与状态回复
ESP8266 接收到消息后,会在串口输出以 +MQTTSUBRECV 开头的事件帧,STM32 中断里把它缓存到环形缓冲区,主循环里解析:
// 例如: // +MQTTSUBRECV:0,"/sys/.../property/set",26,{"method":"thing.service.property.set",...} if (strstr(rx_line, "\"Power\":1")) { GPIO_SetBits(GPIOC, GPIO_Pin_13); // 开继电器 } else if (strstr(rx_line, "\"Power\":0")) { GPIO_ResetBits(GPIOC, GPIO_Pin_13); // 关继电器 } // 收到set后需要主动回一条reply,否则控制台显示超时 uart1_send("AT+MQTTPUB=0,\"/sys/a1xxxxx/dev01/thing/service/property/set_reply\"," "\"{\\\"code\\\":200,\\\"data\\\":{}}\",1,0\r\n");这里用子串匹配解析简单场景是可接受的,但一旦物模型里出现多个开关量,最好引入 cJSON 解析。子串匹配的问题是"Power":10会把"Power":1误判成开。回复报文里 code 固定为 200,data 可以是空对象,重点是必须带 id 字段对应回消息归属。
4. 微信小程序端接入:MQTT over WebSocket 与双向控制
4.1 为什么小程序不能直接连 1883
微信小程序底层网络能力基于 wx.connectSocket,它只允许建立 WebSocket 连接,而原生 MQTT 是 TCP 长连接,在小程序环境里直接使用 mqtt.js 连接 1883 必然失败。阿里云物联网平台对这种情况提供了 MQTT over WebSocket 的桥接方案,broker 地址不变、端口改为 443、路径固定在 /mqtt,并在握手阶段自动识别 WebSocket 子协议为 mqtt。这样开发者不需要自建协议转换网关,只需要把端口和路径换掉,剩下的 MQTT 报文照常编解码。实际校验的时候注意:wxs:// 前缀是平台针对微信小程序的特殊适配,用 wss:// 在部分老版本基础库上会握手失败。
4.2 mqtt.js 连接与 npm 构建
4.2.1 构建 npm 前置步骤
小程序端使用 mqtt.js 需要先在项目根目录执行 npm init 并安装依赖,然后在微信开发者工具里点击“工具 → 构建 npm”,构建完成之后才能 import。不执行构建这一步,直接 require 会报module not found。
4.2.2 连接代码与参数说明
import mqtt from 'mqtt'; const productKey = 'a1xxxxx'; const deviceName = 'stm32_dev01'; const client = mqtt.connect( `wxs://${productKey}.iot-as-mqtt.cn-shanghai.aliyuncs.com/mqtt`, { clientId: `${productKey}.${deviceName}|securemode=3,signmethod=hmacsha1|`, username: `${deviceName}&${productKey}`, password: 'HMAC生成的字符串', connectTimeout: 8000 } ); client.on('connect', () => { // 设备上报的属性,小程序订阅后实时刷新页面 client.subscribe(`/sys/${productKey}/${deviceName}/thing/event/property/post`); });如果 mqtt.js 版本较老,还需要手动设置transport: 'wss'。建议把 mqtt.js 版本锁在 4.3.8,这个版本对微信小程序的适配比较稳定,3.x 版本的 connect 回调和 4.x 存在显著差异,踩过坑之后再也没升过。connectTimeout 设 8 秒是折中值,太短弱网环境频繁掉线,太长用户等待无反馈。
4.3 页面数据刷新与控制指令发布
页面里 onLoad 时保存 client 实例,onMessage 里解析 JSON 后调用 this.setData 更新温度显示。控制开关的代码:
// 按钮点击 sendControl(cmd) { const topic = `/sys/${productKey}/${deviceName}/thing/service/property/set`; const payload = { id: String(Date.now()), version: '1.0', method: 'thing.service.property.set', params: { Power: cmd ? 1 : 0 } }; this.client.publish(topic, JSON.stringify(payload), { qos: 1 }); }publish 的 QoS 为 1 时,如果 30 秒内没有收到 broker 的 puback,mqtt.js 会主动重发。重发场景下,STM32 端可能已经执行过一次控制,就会产生重复动作。因此在设备端做好幂等处理:同一个 id 的指令如果已经执行过直接忽略,只回 set_reply 不重复操作外设。
微信小程序的用户界面还需要处理 APP 切入后台再回前台时 WebSocket 可能断开的情况,onShow 里检测 client.connected 状态决定是否重连。有个容易被忽略的点:微信开发者工具里一切正常,安卓真机上也正常,但 iOS 上连不上,大部分情况是没把域名加进 socket 合法域名列表。
4.4 合法域名注册表与实例状态注意
微信公众平台对 socket 合法域名的校验比 request 更严格,域名必须经过 ICP 备案且不能带端口。开发阶段可以勾选“不校验合法域名”,但真机预览时必须把wss://a1xxxxx.iot-as-mqtt.cn-shanghai.aliyuncs.com加入 socket 合法域名列表。发布审核时小程序涉及 IoT 远程控制需要选择合适类目,最好在页面醒目位置注明“教学演示用途”。
另外有一个今年特别容易踩的外部限制:阿里云物联网平台已经不再支持新购公共实例,只对已开通的账号提供存量服务。如果账号下没有可用实例,可以先在本地用 EMQX 或 MQTTX 把协议逻辑跑通,等有可用实例或改用企业认证时再切换地址和密钥。这种方案不影响整体架构,改的只是 broker 地址和签名参数。
5. 调试三板斧:签名校验、MQTTX 交叉验证与 MQTT 返回码对照
5.1 先用独立脚本卡住签名参数
把第 2 章的 Python 签名脚本保存为 sign.py,每次修改 deviceSecret 后先用它输出三要素。再在桌面端装一个 MQTTX,用脚本生成的结果新建连接,地址填 broker,端口 1883。能连上就说明签名正确,把三要素原样填到 STM32 工程宏定义里,再出问题就从串口和网络两个方向排查。
5.2 MQTTX 的两种模拟角色
一种是把 MQTTX 当云端模拟器:STM32 开发板正常运行,MQTTX 订阅 property/post topic,验证传感器数据是否按预期上报;另一种是把 MQTTX 当设备模拟器:小程序端正常连接,MQTTX 发布 property/set topic,验证小程序收到的状态变化是否符合预期。这种方法能做到协议层全链路交叉验证,比直接烧录整套工程再找问题快得多。
5.3 连接失败常见返回码
| 返回码 | 含义 | 处理建议 |
|---|---|---|
| 4 | 用户名或密码错误(签名过期/拼接错误) | 核对 productKey、deviceName、timestamp |
| 5 | 未授权或连接被拒 | 确认设备状态为“已激活”,且实例可用 |
| 7 | 未知错误(多为网络) | ping broker 域名,确认 DNS 解析正常 |
| 8 | 域名解析失败 | 检查 AT+CIPDOMAIN 是否能解析 |
调试时在 STM32 串口日志里把 +MQTTSUBRECV 前缀和 AT 响应帧全部打印出来,出现 +MQTTDISCONNECTED 时记录是模块主动断开还是服务器断开。服务器断开普遍是心跳间隔超过 120 秒,ESP8266 端把 MQTT keepalive 设成 60 秒内就能稳定保住长连接。
本文还有配套的精品资源,点击获取