STM32 MQTT协议详解:从报文地狱到阿里云实战

bash 复制代码
上一篇 SPI 是板级高速总线,这篇 MQTT 是万物互联的"普通话"。 如果说 I2C/SPI 让你点亮了传感器,MQTT 让你把传感器数据送到全球任何人的手机上。

学完本文,你将能用 STM32 + ESP8266 把 DHT11 温湿度推送到阿里云 / OneNET / 私有 EMQX 服务器,并在手机 APP 上实时看到。


1. 为什么物联网离不开 MQTT?

1.1 HTTP vs MQTT

想象你要每3秒上报一次温湿度:

  • HTTP: 每次都要 TCP握手 -> 发请求头(几百字节)-> 服务器响应 -> 断开,开销巨大,像每次发信都要重新写信封、贴邮票、跑一趟邮局。

  • MQTT: 一次连接后长连接保持,报文最小 2字节,像打电话,接通后一直说"25度""26度"就行。

特性 HTTP MQTT
模型 请求/响应,客户端主动拉 发布/订阅,Broker中介解耦
报文开销 大,Header 几百字节 极小,固定头2字节
实时性 需轮询,假实时 真推送,订阅即收到
流量 高,不适合2G/4G 低,专为不稳定网络设计
适用 网页、API 物联网、低带宽、电池供电

1.2 核心概念:发布/订阅 + Broker 中转

  • Broker (代理/服务器): 中心人,如 EMQX, Mosquitto, 阿里云物联网平台。所有消息都经它转发。

  • Publisher (发布者): STM32传感器,发数据的人。

  • Subscriber (订阅者): 手机APP、另一个STM32,收数据的人。

  • Topic (主题): 消息的地址,有层级,如 sensor/dht11/temphome/led/cmd。发布订阅都靠它匹配。

  • ClientID: 设备唯一身份,Broker靠它踢掉重复登录的设备。绝对不能重复!

解耦的魅力: 发布者根本不认识订阅者,只认识Broker和Topic。于是可以轻松实现 1对多、多对1广播。

Topic 通配符(订阅时可用)

  • + 单层通配:sensor/+/temp 可匹配 sensor/dht11/tempsensor/bmp280/temp,但不匹配 sensor/dht11/room/temp

  • # 多层通配:sensor/# 匹配所有 sensor 开头的所有层级,必须放最后。

2. 报文结构:MQTT的"信封格式"

所有MQTT报文都由三部分组成:Wireshark 抓包必看。

2.1 Fixed Header 固定头(所有报文都有)

  • Byte1: 高4位=报文类型,低4位=标志位(QoS, Retain, DUP)

  • 剩余长度: 1-4字节,用变长编码。

14种报文类型:

报文 方向 作用
1 CONNECT 客户端->Broker 请求连接
2 CONNACK Broker->客户端 连接确认
3 PUBLISH 双向 发布消息
4 PUBACK 双向 QoS1 确认
5 PUBREC 双向 QoS2 第一步
6 PUBREL 双向 QoS2 第二步
7 PUBCOMP 双向 QoS2 第三步
8 SUBSCRIBE 客户端->Broker 订阅主题
9 SUBACK Broker->客户端 订阅确认
10 UNSUBSCRIBE 客户端->Broker 取消订阅
11 UNSUBACK Broker->客户端 取消确认
12 PINGREQ 客户端->Broker 心跳请求
13 PINGRESP Broker->客户端 心跳响应
14 DISCONNECT 客户端->Broker 优雅断开

最小报文 PINGREQ 只有 2字节 : 0xC0 0x00

2.2 Variable Header 可变头

CONNECT 报文的可变头最复杂,10字节:

bash 复制代码
00 04 4D 51 54 54 04 C2 00 3C
|  |             |  |  |____|____ KeepAlive 60秒 (0x003C)
|  |             |  |  连接标志 C2 = 1100 0010 = UserName + Password + CleanSession
|  |             |  协议级别 04 = v3.1.1
|  |  MQTT 字符串
|  长度 4

CONNECT 连接标志位(最容易错):

  • Bit1 CleanSession:1=清理会话(常用),0=保留离线消息

  • Bit2 Will Flag:是否启用遗嘱

  • Bit3-4 Will QoS

  • Bit5 Will Retain

  • Bit6 Password Flag

  • Bit7 UserName Flag

常见 0xC2 = 11000010 = 清理会话+用户名+密码,无遗嘱

2.3 Payload 有效载荷

按序出现:ClientID + Will Topic + Will Message + UserName + Password,每个字段前面 2字节大端长度。

所以一条完整的 CONNECT 报文例子是:10 25 00 04 4D 51 54 54 04 C2 00 3C 00 09 31 32 33... 也就是 AT指令背后ESP8266真正发的东西。

3. 核心机制:QoS、保留、遗嘱、心跳

3.1 QoS 0/1/2 服务质量等级

这是 MQTT 的灵魂,选错等级导致数据丢失或重复。

  • QoS0 At most once 最多一次(0): 发就完了,不管到没到。像发微信不看对方是否已读。优点:最快最省流量,适合高频传感器数据。

    bash 复制代码
    Publisher -> Broker (一条 PUBLISH 就结束)
  • QoS1 At least once 至少一次(1): 保证至少到一次,可能重复。发 -> 等PUBACK确认,没等到就重发。像寄快递要签收。

    bash 复制代码
    Publisher -> PUBLISH -> Broker -> PUBACK -> Publisher

    适合: 控制指令,如 开灯,宁可重复开一次,也不能不开。

  • QoS2 Exactly once 只有一次(2): 最可靠,4步握手保证不丢不重,但开销大。

    bash 复制代码
    Publisher -> PUBLISH -> Broker
    Broker -> PUBREC -> Publisher
    Publisher -> PUBREL -> Broker
    Broker -> PUBCOMP -> Publisher

    适合: 计费、支付等绝对不能重复的场景。物联网一般不用。

实战建议: 90%场景:传感器上行用QoS0,控制下行用QoS1。别迷信QoS2,费电又费流量。

3.2 Retain 保留消息

普通消息:订阅者只有在消息发布后在线才能收到,离线期间的消息丢失。

保留消息:Broker 会保留该 Topic 的最后一条 retained 消息,新订阅者一订阅,立刻收到最后一条。

典型应用: 设备状态 device/led/status 用 retain=1 发布 ON,手机APP一上线就能立刻知道灯是开的,不用等下一次发布。

3.3 LWT 遗嘱机制 (Last Will and Testament)

最实用的离线检测机制!

设备连接时在 CONNECT 中设置:

  • Will Topic: device/sensor-01/status

  • Will Message: {"status":"offline"}

  • Will QoS: 1, Retain: 1

正常断开: 设备发 DISCONNECT 优雅下线,Broker 删除遗嘱,不触发。

异常断开: 断网、没电、没发心跳。Broker 在 1.5倍 KeepAlive 时间后没收到 PING,判断设备挂了,自动发布遗嘱消息!

组合拳实现在线监测:

bash 复制代码
// 1. CONNECT时设置遗嘱 offline (retain=1)
// 2. 连接成功后,立刻发布 online retain=1 覆盖掉遗嘱的离线
PUBLISH topic=device/01/status retain=1 payload={"status":"online"}
// 3. 现在:在线时 retain=online,任何人订阅都能看到 online
//    异常离线时 Broker 自动发 offline 覆盖,订阅者收到 offline

3.4 KeepAlive 心跳

客户端告诉 Broker:我每 KeepAlive 秒至少发一次报文(哪怕发个 PINGREQ)。如果 1.5 * KeepAlive 没收到,Broker 判它掉线,踢掉并触发遗嘱。

STM32上建议 60秒,太短费流量,太长离线检测慢。

4. STM32 三种主流实现方案

方案 硬件 协议栈 难度 成本 推荐
方案A ESP8266 AT指令 F103C8T6 + ESP-01S AT指令,STM32只需串口收发 ⭐最简单 入门首选,本文重点
方案B LWIP+Paho F407 + 以太网PHY LWIP + 开源Paho MQTT ⭐⭐⭐ 商业产品,稳定
方案C EC20 4G Cat1 任意STM32 + EC20 AT指令 MQTT ⭐⭐ 无WiFi场景

5. 实战一:STM32F103 + ESP8266 AT指令 接入 EMQX / OneNET(最经典)

这是正点原子、野火教程的经典方案,代码量极小。

5.1 硬件接线

bash 复制代码
ESP8266    STM32F103
3V3  ----  3.3V  (不能5V!)
GND  ----  GND
RX   ----  PA2 (USART2_TX)  注意交叉!
TX   ----  PA3 (USART2_RX)
RST  ----  PA4 GPIO控制复位 (可选)
EN   ----  3.3V

5.2 ESP8266 MQTT AT流程

1. 基础AT测试

bash 复制代码
AT                     // 响应 OK
AT+CWMODE=1            // Station模式
AT+CWJAP="Xiaomi","12345678" // 连WiFi,等待 GOT IP

2. MQTT配置(新版AT固件 1.7+)

bash 复制代码
// 配置ClientID, 用户名密码(OneNET/阿里云需要token算法生成,这里以EMQX公共服务器为例)
AT+MQTTUSERCFG=0,1,"stm32_001","user","password",0,0,""
​
// 连接Broker,HiveMQ公共测试服务器
AT+MQTTCONN=0,"broker.hivemq.com",1883,0
​
// 订阅命令下行主题
AT+MQTTSUB=0,"led/cmd",1
​
// 发布温湿度
AT+MQTTPUB=0,"sensor/dht11","{\"temp\":25.6,\"humi\":60}",0,0

3. STM32 端代码封装(HAL库状态机+环形缓冲,生产级)

参考了开源项目最佳实践,解决AT响应格式不统一、丢数据问题:1

bash 复制代码
// mqtt_esp8266.h
#define WIFI_SSID "Xiaomi"
#define WIFI_PASS "12345678"
#define MQTT_BROKER "broker-cn.emqx.io"
#define MQTT_CLIENTID "stm32_001_%06X" // 用芯片ID保证唯一!

typedef enum {
    MQTT_STATE_IDLE=0,
    MQTT_STATE_WIFI_OK,
    MQTT_STATE_MQTT_CONNECTED,
    MQTT_STATE_SUB_OK
} mqtt_state_t;

HAL_StatusTypeDef ESP8266_SendCmd(char *cmd, char *expect, uint32_t timeout_ms)
{
    HAL_UART_Transmit(&huart2, (uint8_t*)cmd, strlen(cmd), 1000);
    // 非阻塞等待 expect,实际应使用 环形缓冲+状态机解析,这里简化用轮询
    uint32_t tick = HAL_GetTick();
    while(HAL_GetTick()-tick < timeout_ms)
    {
        if(strstr((char*)usart2_rx_buf, expect)) return HAL_OK;
        HAL_Delay(10);
    }
    return HAL_TIMEOUT;
}

void MQTT_Init(void)
{
    printf("AT Reset...\r\n");
    ESP8266_SendCmd("AT+RST\r\n","ready",3000);
    HAL_Delay(2000);
    ESP8266_SendCmd("AT\r\n","OK",1000);
    ESP8266_SendCmd("AT+CWMODE=1\r\n","OK",1000);
    char cmd[128];
    sprintf(cmd,"AT+CWJAP=\"%s\",\"%s\"\r\n",WIFI_SSID,WIFI_PASS);
    ESP8266_SendCmd(cmd,"WIFI GOT IP",10000);

    sprintf(cmd,"AT+MQTTUSERCFG=0,1,\"%s\",\"%s\",\"%s\",0,0,\"\"\r\n",
            MQTT_CLIENTID,"username","password");
    ESP8266_SendCmd(cmd,"OK",2000);
    
    sprintf(cmd,"AT+MQTTCONN=0,\"%s\",1883,0\r\n",MQTT_BROKER);
    if(ESP8266_SendCmd(cmd,"OK",5000)==HAL_OK) printf("MQTT Connected!\r\n");
}

void MQTT_Publish(char *topic, char *payload)
{
    char cmd[256];
    // qos0 retain0
    snprintf(cmd,sizeof(cmd),"AT+MQTTPUB=0,\"%s\",\"%s\",0,0\r\n",topic,payload);
    HAL_UART_Transmit(&huart2,(uint8_t*)cmd,strlen(cmd),1000);
}

void MQTT_Subscribe(char *topic)
{
    char cmd[128];
    snprintf(cmd,sizeof(cmd),"AT+MQTTSUB=0,\"%s\",1\r\n",topic);
    HAL_UART_Transmit(&huart2,(uint8_t*)cmd,strlen(cmd),1000);
}

// 在串口中断回调中解析下行:+MQTTSUBRECV:0,"led/cmd",6,ON
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if(huart==&huart2)
    {
        if(strstr((char*)rx_buf,"+MQTTSUBRECV"))
        {
            // 简易解析,生产环境用 cJSON
            char *p = strstr((char*)rx_buf,"led/cmd");
            if(p)
            {
                if(strstr((char*)rx_buf,"ON")) HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_RESET);
                else HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_SET);
            }
        }
        HAL_UART_Receive_IT(&huart2,&rx_byte,1);
    }
}

int main(void)
{
    // ... HAL Init
    MQTT_Init();
    MQTT_Subscribe("led/cmd");
    while(1)
    {
        int temp=25, humi=60;
        // 实际从DHT11读取
        char json[128];
        sprintf(json,"{\"id\":\"123\",\"params\":{\"temp\":{\"value\":%d},\"humi\":{\"value\":%d}}}",temp,humi);
        MQTT_Publish("sensor/data", json);
        HAL_Delay(5000);
    }
}

5.3 接入 OneNET / 阿里云的关键区别

  • OneNET 新版MQTT: 用户名是 产品ID,密码是 Token(需用 token 工具根据 product_id + 设备名 + key 计算),ClientID 是设备名。Topic 必须是 $sys/{pid}/{device}/thing/property/post 投稿格式。

  • 阿里云: 三元组 ProductKey DeviceName DeviceSecret 用 HMAC-SHA256 算出 clientId, username, password。官方有 MQTT_Password 生成小工具。Topic 为 /sys/{ProductKey}/{DeviceName}/thing/event/property/post

  • 建议: 开发期用 EMQX 公共 Broker broker.emqx.io:1883 联调,跑通后再切阿里云/OneNET,省去鉴权烦恼。

6. 实战二:高级版 STM32F407 + LWIP + Paho MQTT(裸机协议栈)

如果你用 F407/F767 自带以太网,或想更深入理解MQTT报文,可用开源 Paho MQTT Embedded-C

它只有4个文件:MQTTPacket, MQTTClient, MQTTConnect, MQTTSubscribe

bash 复制代码
// 参考封装 [5](https://www.cnblogs.com/jgijue/p/19679044)

// 网络层用 LWIP socket 封装
int transport_sendPacketBuffer(int sock, unsigned char* buf, int buflen)
{
    return lwip_send(sock, buf, buflen, 0);
}

// 初始化
MQTTClient mqttClient;
Network n;
MQTTPacket_connectData data = MQTTPacket_connectData_initializer;
unsigned char buf[1024], readbuf[1024];
NetworkInit(&n);
NetworkConnect(&n, "broker.emqx.io", 1883);
MQTTClientInit(&mqttClient, &n, 1000, buf, 1024, readbuf, 1024);

data.clientID.cstring = "stm32f407_client";
data.keepAliveInterval = 60;
data.cleansession = 1;
data.username.cstring = "user";
data.password.cstring = "pass";
data.willFlag = 1;
data.will.topicName.cstring = "device/status";
data.will.message.cstring = "offline";
data.will.retained = 1;

MQTTConnect(&mqttClient, &data);

// 发布
MQTTMessage msg;
msg.qos = QOS1;
msg.retained = 0;
msg.payload = "{\"temp\":26}";
msg.payloadlen = strlen(msg.payload);
MQTTPublish(&mqttClient, "sensor/data", &msg);

// 订阅
MQTTSubscribe(&mqttClient, "control/led", QOS1, messageArrived);
void messageArrived(MessageData *md)
{
    printf("Recv: %.*s\n", md->message->payloadlen, (char*)md->message->payload);
}

此方案可移植 FreeRTOS,中断+消息队列,适合商用。

7. 调试神器:MQTTX + Wireshark

  • MQTTX:EMQX 官方出的跨平台客户端,比 MQTT.fx 好用100倍,可同时模拟发布和订阅,看 retained / qos 效果。

  • Wireshark 抓包: 过滤 mqtt,直接看 2字节包头 + 报文类型,心跳是否正常一眼看清。

  • EMQX Dashboard: broker.emqx.io:18083 可看在线客户端、订阅关系。

8. 排坑指南:血泪总结

  1. ClientID 重复导致互踢: 两个板子用同一个 stm32_client,结果疯狂掉线。解决:ClientID = 设备MAC/芯片ID,唯一!

  2. AT固件版本太旧不支持 MQTT: AT+MQTT 返回 ERROR。解决:去安信可官网烧录 ESP8266 AT v1.7+ MQTT 固件。

  3. OneNET/阿里云 鉴权失败 CONNACK=5: Token算错、时间戳不对。解决:用官方小工具动态生成,别手算。

  4. 订阅收不到?

    • QoS不匹配?没事,都能收到,只是降级。

    • 主题写错大小写?

    • 是否订阅后立刻发布?Broker 转发有 10-100ms 延迟,测试时加延时。

  5. 内存溢出: sprintf 拼 JSON 时 128字节缓冲区溢出导致 HardFault。改用 snprintf 并检查返回值。

  6. KeepAlive 内没发 PING: 60秒内既没Pub也没Sub,Broker踢掉。解决:空闲时在主循环定时发 AT+MQTTPINGMQTTClient_yield 保持心跳。

  7. WiFi断了MQTT不会自动重连: ESP8266 AT版掉WiFi后不会自动连MQTT。必须在代码里检测 WIFI DISCONNECT,状态机回到 AT+CWJAP 重连。

9. 总结与下篇预告

  • MQTT 本质:轻量级发布/订阅 + Broker中转

  • 保命三件套:QoS(可靠性) + Retain(状态记忆) + LWT(掉线感知)

  • STM32 接入:新手用 ESP8266 AT 版 30分钟跑通,上量用 F4+LWIP+Paho


参考与工具

  • 官方文档《MQTT具体报文格式解析》- 报文结构详解 3

  • 工程实践《STM32+ESP8266+Android基于MQTT三端通信》- 状态机+环形缓冲最佳实践 1

  • OneNET/阿里云 STM32 AT指令接入教程 2

  • 工具:MQTTX 下载 | EMQX公共Broker

本文所有配图原创,代码基于 HAL 库亲测 F103C8T6 + ESP-01S 可用。

相关推荐
凉、介9 小时前
ARMv8-A 指令学习
linux·笔记·学习·嵌入式·arm·指令
Dr.kangder11 小时前
嵌入式软件接口测试:从理论到实践的全面指南
测试工具·架构·嵌入式·测试覆盖率
Dr.kangder1 天前
嵌入式软件程序分析技术:原理、方法与实战
servlet·架构·嵌入式·dsp开发
深圳市爱派派智能科技有限公司1 天前
“高通 QCS8550 嵌入式主板 EC-A8550JD4:48 TOPS 算力上板“
网络·嵌入式·边缘计算·高通·qcs8550·天启智能·ec-a8550jd4
凉、介1 天前
Armv8 原子性:体系结构保证与编程语义
linux·学习·嵌入式·arm·原子操作·atomic
拾叶1 天前
STM32学习笔记|LED 闪烁、流水灯与蜂鸣器驱动
嵌入式
Dr.kangder1 天前
嵌入式软件单元测试:从理论到实践
架构·单元测试·嵌入式·测试覆盖率
K成长日志1 天前
BLE链路层空口包--数据物理信道PDU
网络·物联网·网络协议·嵌入式·蓝牙·iot·ble
pie_thn3 天前
基于端侧大模型的嵌入式 Skill 调度引擎实现智能业务生成的尝试
大模型·嵌入式·skill