【Hi3861 物联网实战】MQTT 通信从入门到踩坑:连接、收发、心跳、重连、I2C 卡死全记录
摘要:本文完整记录了基于 Hi3861(HarmonyOS LiteOS-M)的智能家居项目开发与调试全过程。设备端通过 WiFi 接入 MQTT Broker,上位机使用 MQTTX 发送 JSON 指令控制 LED、继电器和电机。文章围绕四个典型踩坑案例展开:Topic 前缀不一致导致消息无法送达、缺少心跳被 Broker 静默踢下线、重连时旧 TCP 连接未释放导致 AT+CIPSTART 超时、去掉 OLED 后 I2C 等待 ACK 卡死。每个问题都配有真实串口日志、根因分析和解决方案,并总结了串口打印定位法、常见问题速查表及稳定性建议,适合正在做 WiFi-IoT 项目的开发者参考。
最近在做一个基于 Hi3861(HarmonyOS LiteOS-M)的智能家居项目,设备端通过 WiFi 接入 MQTT Broker,上位机用 MQTTX 发送 JSON 指令来控制 LED、继电器和电机。
听起来很简单的一个流程,实际开发中却踩了一大堆坑:连不上 Broker、Topic 对不上、设备收到一两条消息就掉线、重连失败、去掉 OLED 后 I2C 卡死……
这篇文章把整个调试过程完整记录下来,每一个坑都有真实的串口日志和解决方案,希望能帮到同样在做 WiFi-IoT 项目的同学。
一、项目架构
1.1 硬件清单
| 硬件 | 说明 |
| Hi3861 开发板 | 主控,运行 HarmonyOS LiteOS-M |
| 三色 LED | 红、绿、蓝三路 GPIO 控制 |
| 继电器模块 | GPIO 控制开关 |
| 电机模块 | PWM 调速,正反转控制 |
| OLED 显示屏 | I2C 接口(后期去掉) |
1.2 软件架构
┌─────────────┐ MQTT JSON ┌──────────────┐ MQTT JSON ┌─────────────┐
│ MQTTX │ ───────────────> │ MQTT Broker │ <───────────── │ Hi3861 │
│ (上位机) │ <─────────────── │ broker.emqx │ ──────────────> │ (设备端) │
└─────────────┘ wj/device/to/app └──────────────┘ wj/app/to/device └─────────────┘
Topic 设计:
- wj/app/to/device:上位机 → 设备,下发控制指令
- wj/device/to/app:设备 → 上位机,上报传感器数据
注意:公共 Broker 是全世界共用的,Topic 一定要加自己的前缀(这里用了 wj/),否则会收到陌生人的消息。
二、MQTTX 客户端配置
2.1 选择 Broker
刚开始学习推荐用公共 Broker,不用自己搭服务器:
| Broker 地址 | TCP 端口 | TLS 端口 | 说明 |
| broker.emqx.io | 1883 | 8883 | EMQX 官方,最常用 |
| 1883 | 8883 | HiveMQ 公共服务 | |
| 1883 | 8883 | Eclipse Mosquitto |
如果项目要长期稳定运行,建议自己在云服务器上搭 EMQX,后面会说为什么。
2.2 MQTTX 连接参数
新建连接,只需要关注 General 栏:
| 字段 | 填写内容 | 说明 |
| Name | 随便起,如 WJ | 仅本地显示用 |
| Host | broker.emqx.io | 直接填域名,MQTTX 自动解析 |
| Port | 1883 | 明文 TCP,不用开 SSL/TLS |
| Client ID | 留空自动生成 | 设备和 MQTTX 的 Client ID 不能相同,否则互相踢下线 |
| Username / Password | 留空 | 公共 Broker 不需要认证 |
| SSL/TLS | 关闭 | 1883 端口是明文 |
Advanced 栏全部默认即可(MQTT 3.1.1、Keep Alive 60s、Auto Reconnect 开启)。
2.3 订阅和发布
连接成功后:
- 点 + New Subscription,订阅 wj/device/to/app(接收设备上报)
- 底部发布区 Topic 填 wj/app/to/device(给设备发指令)
- 格式选 JSON,QoS 选 0
小知识:如果你同时订阅了自己发布的 Topic,会收到自己发出去的消息(白色气泡),这是正常的 "回声",不是设备回复的。绿色气泡才是你自己发出去的。
三、设备端基础代码
3.1 WiFi + MQTT 初始化
#include "led.h"
#include "los_task.h"
#include "los_compiler.h"
#include "relay.h"
#include "stdint.h"
#include "data.h"
void os_entry(){
static int length;
static char *p;
ui_init(); // OLED 初始化(后期去掉 OLED 后注释掉)
key_entry(); // 按键初始化
// WiFi 模块初始化(通过 UART 发 AT 指令)
WifiUartInit();
EnableWifi();
// 连接路由器
HOME_DATA da = {0};
ConnectToAP("你的WiFi名", "你的WiFi密码");
// 连接 MQTT Broker
mqtt_connect("broker.emqx.io", 1883);
// 设备上线,发布一条消息
mqtt_publish("wj/device/to/app", da);
// 订阅控制指令 Topic
mqtt_subscribe("wj/app/to/device");
// 主循环:接收消息
while(1){
p = mqtt_getdata(&length);
if(p != NULL){
mqtt_parsedata(p);
delay_ms(1 * 1000);
}
}
}
开机串口输出:
entering kernel init...
---------------liteos_m start------------
init i2c ok
Enable Wi-Fi ... Success ...
Connect to hosspot ... Success ...
Connect to Server broker.emqx.io OK
Sending to hostname broker.emqx.io port 1883
mqtt connect ok
mqtt pushlish ok
sub topic wj/app/to/device ok
3.2 JSON 指令解析
上位机发送 JSON 格式的控制指令,设备端用 cJSON 解析:
void mqtt_parsedata(char *msg)
{
cJSON *root = cJSON_Parse(msg);
if(root == NULL){
printf("json parse fail: %s\n", msg);
return;
}
cJSON *item;
/* ---------- 红灯 ---------- */
item = cJSON_GetObjectItem(root, "led_r");
if(cJSON_IsString(item)){
if(!strcmp(item->valuestring, "on")) red_entry();
else if(!strcmp(item->valuestring, "off")) led_off(LED_RED);
}
/* ---------- 绿灯 ---------- */
item = cJSON_GetObjectItem(root, "led_g");
if(cJSON_IsString(item)){
if(!strcmp(item->valuestring, "on")) green_entry();
else if(!strcmp(item->valuestring, "off")) led_off(LED_GREEN);
}
/* ---------- 蓝灯 ---------- */
item = cJSON_GetObjectItem(root, "led_b");
if(cJSON_IsString(item)){
if(!strcmp(item->valuestring, "on")) blue_entry();
else if(!strcmp(item->valuestring, "off")) led_off(LED_BLUE);
}
/* ---------- 继电器 ---------- */
item = cJSON_GetObjectItem(root, "relay");
if(cJSON_IsString(item)){
if(!strcmp(item->valuestring, "on")) relay_on(RELAY);
else if(!strcmp(item->valuestring, "off")) relay_off(RELAY);
}
/* ---------- 电机 ---------- */
item = cJSON_GetObjectItem(root, "motor_state");
if(cJSON_IsString(item)){
cJSON *jspd = cJSON_GetObjectItem(root, "motor_speed");
int speed = cJSON_IsNumber(jspd) ? jspd->valueint : 0;
if(speed < 0) speed = 0;
if(speed > 100) speed = 100;
if(!strcmp(item->valuestring, "forward")){
motor_ctl(MOTOR_FORWARD, speed);
}else if(!strcmp(item->valuestring, "backward")){
motor_ctl(MOTOR_BACKWARD, speed);
}else{
motor_ctl(MOTOR_STOP, 0); // stop / standby 都停转
}
}
cJSON_Delete(root);
free(msg); // 对应 mqtt_getdata 里的 malloc,防止内存泄漏
}
3.3 上位机指令速查表
在 MQTTX 里发送以下 JSON,Topic 统一填 wj/app/to/device:
| 功能 | JSON 报文 |
| 红灯开 | {"led_r":"on"} |
| 红灯关 | {"led_r":"off"} |
| 绿灯开 | {"led_g":"on"} |
| 蓝灯开 | {"led_b":"on"} |
| 继电器开 | {"relay":"on"} |
| 继电器关 | {"relay":"off"} |
| 电机正转(80% 速度) | {"motor_state":"forward","motor_speed":80} |
| 电机反转(50% 速度) | {"motor_state":"backward","motor_speed":50} |
| 电机停止 | {"motor_state":"stop"} |
| 多条合并 | {"led_r":"on","led_g":"off","relay":"on"} |
JSON 格式注意事项:
- 键名和字符串值必须用英文双引号 ",不能用中文引号
- 数字(如 motor_speed)不加引号
- MQTTX 左下角选 JSON 格式,编辑框里没有红色波浪线才是合法 JSON
四、踩坑一:Topic 对不上,消息发了但设备收不到
4.1 现象
MQTTX 里消息显示发送成功,但设备完全没反应。
4.2 排查过程
一开始设备代码里写的是:
mqtt_publish("device/to/app", da);
mqtt_subscribe("app/to/device");
后来在 MQTTX 里为了防止和别人串消息,加了 wj/ 前缀:
- 订阅:wj/device/to/app
- 发布:wj/app/to/device
结果设备代码没同步改,两边 Topic 差了一个前缀,Broker 当成完全不同的主题,消息根本不会转发。
4.3 解决
设备代码和 MQTTX 里的 Topic 必须一字不差:
mqtt_publish("wj/device/to/app", da);
mqtt_subscribe("wj/app/to/device");
4.4 调试技巧:订阅通配符 #
如果不确定设备到底发到哪个 Topic 了,可以在 MQTTX 里订阅 #(井号),它会接收 Broker 上所有 Topic 的消息,设备一发就能看到实际的 Topic 名字。
五、踩坑二:设备收到一两条消息后就 "死" 了(重点)
这是整个项目中耗时最长的一个坑。
5.1 现象
设备刚连上时能正常收到一两条指令,LED 也能亮,但过一会儿再发消息就完全没反应了。串口一直在刷 66666(主循环打印),看起来程序还在跑,但就是收不到消息。
5.2 第一步:加打印定位
在主循环关键位置加 printf:
while(1){
printf("loop start\n");
p = mqtt_getdata(&length);
printf("loop end, p=%p\n", p);
if(p != NULL){
printf("[1] before parse\n");
mqtt_parsedata(p);
printf("[2] after parse\n");
delay_ms(1 * 1000);
}
}
串口输出:
message arrived {"led_b":"on"}
loop end, p=0x2000ca64
[1] before parse
[2] after parse
loop start
loop end, p=(nil)
loop start
loop end, p=(nil)
loop start
loop end, p=(nil)
...(一直返回空)
说明:
- [1] 和 [2] 都打印了 → JSON 解析没问题,不是卡死
- 之后 p=(nil) 一直返回空 → MQTT 连接已经断了,但程序不知道
5.3 根本原因:没有心跳,被 Broker 踢下线
MQTT 协议规定:客户端在连接时会协商一个 Keep Alive 时间(默认 60 秒),如果在 1.5 倍 Keep Alive 时间内 Broker 没收到客户端的任何数据包(包括 PINGREQ 心跳),就会主动断开连接。
设备端的 MQTT 库不会自动发心跳,需要我们手动调用。原来的代码里完全没有心跳逻辑,所以连上几十秒后就被 Broker 静默踢掉了。
5.4 心跳函数
MQTT 心跳就是客户端发 PINGREQ,Broker 回 PINGRESP:
/**
@brief 发送 MQTT 心跳包
@return 0=成功, -1=失败(连接已断)
*/
int mqtt_jump(void)
{
int rc = 0;
unsigned char buf[200];
int buflen = sizeof(buf);
// 序列化 PINGREQ 报文并发送
int len = MQTTSerialize_pingreq(buf, buflen);
rc = transport_sendPacketBuffer(mysock, buf, len);
// 等待 PINGRESP 响应
if(MQTTPacket_read(buf, buflen, transport_getdata) == PINGRESP){
printf("mqtt ping packet ok\n");
rc = 0;
}else{
printf("mqtt ping packet failed\n");
rc = -1;
}
return rc;
}
5.5 最终的主循环:心跳 + 自动重连
void os_entry(){
static int length;
static char *p;
int heartbeat_cnt = 0;
ui_init();
key_entry();
WifiUartInit();
EnableWifi();
HOME_DATA da = {0};
ConnectToAP("你的WiFi名", "你的WiFi密码");
mqtt_connect("broker.emqx.io", 1883);
mqtt_publish("wj/device/to/app", da);
mqtt_subscribe("wj/app/to/device");
while(1){
p = mqtt_getdata(&length);
if(p != NULL){
/* ---------- 收到消息 ---------- */
printf("message arrived %s\n", p);
mqtt_parsedata(p);
delay_ms(1 * 1000);
heartbeat_cnt = 0; // 收到数据说明连接正常,重置计数
}else{
/* ---------- 没有消息 ---------- */
delay_ms(100);
heartbeat_cnt++;
// 连续 N 次没收到数据,认为连接断了,重连
if(heartbeat_cnt >= 10){
heartbeat_cnt = 0;
printf("reconnect...\n");
mqtt_connect("broker.emqx.io", 1883);
mqtt_subscribe("wj/app/to/device");
}
}
}
}
5.6 踩坑:计数阈值不能想当然
一开始我以为每次循环约 100ms,设了 50 次(5 秒)触发重连。结果等了 20 秒都没触发。
后来打印计数器才发现:mqtt_getdata() 内部的 MQTTPacket_read 是阻塞函数,每次调用本身就要等 1~2 秒才返回 NULL。所以实际每次循环约 2 秒,阈值要根据实际循环周期调整。
经验:阈值定多少,先用 printf 打印计数器实际观察,不要拍脑袋。
六、踩坑三:重连失败,AT+CIPSTART 超时
6.1 现象
自动重连触发了,但重连失败,串口报:
reconnect...
Sending1 to hostname
rsp AT+CIPSTART= timeout: 0
rsp AT+CIPMODE= timeout: 0
6.2 原因
WiFi 模块通过 AT 指令建立 TCP 连接:
- AT+CIPSTART:建立 TCP 连接
- AT+CIPCLOSE:关闭 TCP 连接
MQTT 掉线后,旧的 TCP 连接在 WiFi 模块里还占着没有释放,直接发 AT+CIPSTART 开新连接就会超时失败。
6.3 尝试过的错误方案
尝试 1:重新初始化 UART + WiFi
WifiUartInit(); // 报错:LL_USART_Init failed,UART 已经初始化过了
EnableWifi();
ConnectToAP(...);
UART 重复初始化直接失败。
尝试 2:只重连 WiFi
EnableWifi();
ConnectToAP(...);
报错:
rsp AT+CWMODE= timeout: 0
Failed, reason is : ERROR_WIFI_UNKNOWN
rsp AT+CWJAP= timeout: 0
因为 WiFi 本来就没断(断的是 TCP/MQTT),重复连 WiFi 也会失败。
6.4 正确思路
- WiFi 没断 → 不要动 WiFi
- 旧 TCP 没关 → 重连前先发 AT+CIPCLOSE 关掉旧连接
需要在 mqtt_connect 之前加一个关闭旧 TCP 的操作(具体函数名根据你的封装来定):
if(heartbeat_cnt >= 10){
heartbeat_cnt = 0;
printf("reconnect...\n");
// 先关闭旧 TCP 连接(AT+CIPCLOSE)
mqtt_tcp_close();
delay_ms(500);
// 再建立新连接
mqtt_connect("broker.emqx.io", 1883);
mqtt_subscribe("wj/app/to/device");
}
建议:如果条件允许,直接换成自己云服务器上搭的 Broker(如 EMQX),公共 Broker 对频繁重连有连接数限制,自建的更稳定可控。
七、踩坑四:去掉 OLED 后,程序卡死在 I2C
7.1 现象
项目后期不需要 OLED 了,把 OLED 相关代码删了一部分,结果开机后卡住:
entering kernel init...
---------------liteos_m start------------
init i2c ok
打印完 init i2c ok 就不动了。
7.2 排查过程
一开始以为是 I2C 初始化卡死,后来发现 init i2c ok 都打印出来了,说明 I2C 总线初始化本身没问题。
真正卡死的位置是在后面调用的 ui_init()(OLED 初始化)里 —— 它要往 OLED 的 I2C 地址发命令并等待 ACK 应答。
7.3 I2C 卡死原理
I2C 通信流程:
主设备(Hi3861) 从设备(OLED)
| |
|------ START + 设备地址 --------------->|
| |
|<-------- 等待 ACK 应答 ----------------|
| |
| OLED 没接 / 没初始化,无人应答 |
| |
v 驱动里没有超时判断,死循环等待 v
【卡死】
I2C 驱动的底层逻辑大概是:
while(等待ACK){
if(SDA线被从设备拉低){
break; // 收到 ACK,继续
}
// 没有超时退出机制 → 没人应答就永远等
}
7.4 解决
不接 OLED,就把调用 OLED 初始化的那行注释掉:
void os_entry(){
// ui_init(); // 不接 OLED 就注释掉,否则 I2C 等 ACK 卡死
key_entry();
...
}
注意:init i2c ok 是系统底层自动初始化 I2C 总线时打印的,这个不用管,它不会卡死。卡死的是应用层主动去访问 OLED 设备的代码。
八、完整调试方法论
经过这一天的折腾,总结出嵌入式 MQTT 调试的标准流程:
8.1 串口打印定位法
不要猜,用 printf 让程序自己告诉你卡在哪。
在每个关键步骤前后加打印:
printf("step 1: before connect\n");
mqtt_connect(...);
printf("step 2: after connect\n");
while(1){
printf("loop\n");
p = mqtt_getdata(&length);
printf("got data: %p\n", p);
if(p != NULL){
printf("before parse\n");
mqtt_parsedata(p);
printf("after parse\n");
}
}
看最后停在哪条打印,问题就出在它和下一条之间。
8.2 常见问题速查表
| 现象 | 原因 | 解决 |
| MQTTX 连不上 | Host/Port 填错、SSL 开关不对 | 用 Test-NetConnection IP 端口 测网络 |
| 消息发了设备收不到 | Topic 字符串不一致 | 两边逐字核对,或订阅 # 通配符 |
| 设备频繁被踢下线 | Client ID 和别的客户端重复 | 每个客户端用唯一 Client ID |
| 收一两条就没反应了 | 没心跳,被 Broker 踢 | 加 PINGREQ 心跳 + 断线重连 |
| 重连 AT+CIPSTART 超时 | 旧 TCP 连接没关 | 先 AT+CIPCLOSE 再重连 |
| 重连 WiFi 报 UNKNOWN | WiFi 本来就没断 | 只重连 TCP/MQTT,别动 WiFi |
| 开机卡死 | I2C 等不到从设备 ACK | 注释掉没接设备的初始化代码 |
| JSON 发送报 SyntaxError | 键名 / 字符串没加英文双引号 | 严格按 JSON 语法写 |
8.3 稳定性建议
- 心跳必须有:Keep Alive 建议设 30~60 秒,心跳间隔设为 Keep Alive 的一半
- 自动重连必须有:检测到断线后自动关闭旧连接、重新 connect、重新 subscribe
- 重连后要重新订阅:TCP 重连后之前的订阅关系就没了,必须重新 mqtt_subscribe
- 生产环境用自建 Broker:公共 Broker 有连接数和频率限制,只适合学习测试
- WiFi 信号要稳定:信号弱是掉线的重要原因,天线和路由器位置注意一下
九、最终稳定版代码结构
os_entry()
├── 硬件初始化(LED、按键等,不接的设备不要初始化)
├── WiFi 初始化(UART → EnableWifi → ConnectToAP)
├── MQTT 连接(mqtt_connect → mqtt_subscribe)
└── while(1) 主循环
├── mqtt_getdata() 轮询消息
├── 有消息 → mqtt_parsedata() 解析执行 → 重置计数
└── 无消息 → 计数累加
└── 超过阈值 → 关旧 TCP → 重连 → 重新订阅
十、写在最后
回头看,这些坑其实都不难,但在刚接触嵌入式 MQTT 的时候,每一个都能卡半天。最大的体会是:
- 嵌入式开发,串口日志是第一生产力。不要盯着代码猜,加几行 printf,问题在哪一层立刻就清楚了。
- 网络通信是 "会断的"。不要假设连接一直正常,心跳和重连是必备代码,不是可选功能。
- 协议规范要读。MQTT 的 Keep Alive 机制、I2C 的 ACK 机制,文档里都写得很清楚,踩坑后再回头看文档印象特别深。
希望这篇文章对你有帮助,欢迎在评论区交流讨论。
相关阅读:
- MQTT 协议中文版:Introduction · MQTT协议中文版
- EMQX 公共 Broker:免费 MQTT Broker:公共多区域服务 | 立即连接 | EMQ
- MQTTX 客户端下载:MQTTX:全功能 MQTT 客户端工具
更多推荐

所有评论(0)