概述
MQTT(Message Queuing Telemetry Transport)是物联网最主流的发布/订阅轻量消息协议:设备作为客户端连接 Broker(消息代理,消息的中转站),按主题(Topic)发布(publish)和订阅(subscribe)消息,双方无需知道对方存在,天然支持一对多、多对一通信。相比 HTTP,MQTT 报文开销小、支持 QoS 分级与离线消息,适合弱网、低功耗设备。本教程演示:连接公共 Broker,发布/订阅 /topic/qos0、/topic/qos1 主题并打印收到的数据。
用大白话讲:MQTT 就像微信订阅公众号——设备「关注」(订阅)某个主题,公众号发文章(发布消息)后,所有关注的人都能收到,作者不需要知道谁在看。中间的「公众号平台」就是 Broker(消息中转站),主题(Topic)就是「频道名」。开发板连上 Broker 后,发布和订阅都通过主题完成。
本教程基于安信可官方 SDK(Ai-Thinker-Open/Ai-Thinker-WB2,版本
release_bl_iot_sdk_1.6.40)的官方示例applications/protocols/mqtt/tcp编写,代码可在本地 SDK 中直接找到。
打开终端,进入官方 mqtt/tcp 示例工程目录:
cd ~/Ai-Thinker-WB2/applications/protocols/mqtt/tcp
说明:
cd是「进入目录」的命令,进入官方示例工程;后面所有make命令都要在这个目录下执行。
打开 tcp/main.c,修改开头的 SSID/密码(同 连接 Wi-Fi)。
打开 tcp/demo.c,修改 Broker 地址:
axk_mqtt_client_config_t mqtt_cfg = {
.uri = "mqtt://mqtt.eclipseprojects.io",
.event_handle = event_cb,
};
⚠️ 官方默认
mqtt.eclipseprojects.io为公共测试服务器,目前可能已停止服务。建议改用可用的公共 Broker(如mqtt://broker.emqx.io、mqtt://test.mosquitto.org)或自建 Broker,URI 格式为mqtt://主机:端口(默认端口 1883)。
打开 tcp/demo.c,本步完整代码已移至文末,见:
📜 完整代码 — 位于本页「完整代码」章节,默认折叠,点击展开,与官方示例(
applications/protocols/mqtt/tcp/tcp/demo.c)完全一致。
代码要点:
| 代码 | 作用 |
|---|---|
axk_mqtt_client_config_t { .uri, .event_handle } |
填好 Broker 地址和回调,填错就连不上任何服务器 |
axk_mqtt_client_init(&mqtt_cfg) |
按配置创建客户端,创建失败后面全做不了 |
axk_mqtt_client_start(client) |
启动并异步连接 Broker,连接是自动进行的 |
MQTT_EVENT_CONNECTED |
连接成功的通知,发布/订阅必须等它到了再做 |
axk_mqtt_client_publish(client, topic, data, len, qos, retain) |
向主题发消息,订阅了该主题的设备都会收到 |
axk_mqtt_client_subscribe(client, topic, qos) |
订阅主题,不订阅就收不到该主题的消息 |
axk_mqtt_client_unsubscribe(client, topic) |
取消订阅,之后不再收到该主题消息 |
MQTT_EVENT_DATA |
收到消息的通知,用 event->topic/data 取内容 |
MQTT_EVENT_ERROR |
出错通知,看错误码才知道是网络还是 TLS 问题 |
💡 MQTT 关键概念:主题用
/分级(如/topic/qos0),支持通配符+(单级)、#(多级);QoS 0=最多一次(不确认)、1=至少一次(可能重复)、2=恰好一次(开销最大);retain=1 时 Broker 会为后续订阅者保留最后一条消息。官方示例流程:连接后发布 qos1 → 订阅两个主题 → 取消订阅 qos1 → 订阅确认后再发布 qos0。
在工程目录执行编译:
make -j8
说明:
make是「编译」命令,把代码变成开发板能运行的固件(烧进开发板的程序);-j8表示用 8 个 CPU 核并行编译,更快。
编译成功后生成固件 build_out/tcp.bin。
开发板保持 USB 连接,确认串口设备号后执行烧录:
make flash p=/dev/ttyUSB0 b=921600
说明:
make flash是「烧录」命令,把编译好的固件下载进开发板;p=后面是串口设备号(Linux 下常为/dev/ttyUSB0,Windows 下是COM3之类,以你电脑实际为准),b=921600是烧录波特率(串口传数据的速度),保持默认即可。
⏳ 烧录过程中按提示长按开发板 EN 键进入下载模式,等待进度条完成即烧录成功。
先在电脑打开 MQTT 调试工具(如 MQTTX),连接到同一 Broker(如 broker.emqx.io:1883),并订阅 /topic/qos0、/topic/qos1 主题。
烧录完成后开发板自动重启运行,串口(波特率 921600,串口传数据的「语速」,两边必须一致)打印:
[APP] [EVT] GOT IP 5594
MQTT_EVENT_CONNECTED
sent publish successful, msg_id=1
sent subscribe successful, msg_id=2
sent subscribe successful, msg_id=3
sent unsubscribe successful, msg_id=4
MQTT_EVENT_SUBSCRIBED, msg_id=3
sent publish successful, msg_id=5
电脑 MQTT 工具将收到两条消息:/topic/qos1 上的 data_3、/topic/qos0 上的 data——说明发布/订阅链路全部打通。
反向验证:在电脑工具上向 /topic/qos0 发布任意消息(如 hello wb2),开发板串口打印:
MQTT_EVENT_DATA
TOPIC=/topic/qos0
DATA=hello wb2
双向通信验证通过。看到 MQTT_EVENT_CONNECTED 且电脑工具收到 data_3、data 两条消息即成功;如果一直没出现 MQTT_EVENT_CONNECTED,先确认开发板已 GOT IP(联网)、电脑 MQTT 工具能连上同一 Broker(目标服务器可达),否则见文末 FAQ。
💡 同一主题可被多台设备同时订阅——在另一台设备/另一个工具会话发布消息,所有订阅者(含开发板)都会收到,这就是 MQTT 一对多通信。
代码执行流程
例程从启动到运行的完整流程如下(图中的循环箭头表示反复执行):
本文 API 汇总
axk_mqtt_client_init(&config)
创建 MQTT 客户端(配置 uri/事件回调),返回句柄。
参数:
config:axk_mqtt_client_config_t结构体指针,.uri(mqtt://host:port)与.event_handle(事件回调)必填
返回值:成功返回客户端句柄(axk_mqtt_client_handle_t);失败返回 NULL
axk_mqtt_client_start(client)
启动客户端并异步连接 Broker。
参数:
client:axk_mqtt_client_init返回的客户端句柄
返回值:成功返回 0;失败返回负值错误码
axk_mqtt_client_publish(client, topic, data, len, qos, retain)
发布消息到主题。
参数:
client:客户端句柄topic:主题字符串data:消息数据指针len:消息长度qos:QoS 等级(0/1/2)retain:是否保留消息(1保留 /0不保留)
返回值:成功返回消息 ID(msg_id);失败返回负值
axk_mqtt_client_subscribe(client, topic, qos)
订阅主题。
参数:
client:客户端句柄topic:主题字符串qos:QoS 等级(0/1/2)
返回值:成功返回消息 ID(msg_id);失败返回负值
axk_mqtt_client_config_t
客户端配置结构体。
参数:
.uri:Broker 地址,如"mqtt://host:1883".event_handle:事件回调函数
返回值:无(结构体,非函数)
axk_mqtt_event_handle_t
事件句柄结构体(回调入参)。
参数:
event_id:事件类型(与MQTT_EVENT_*宏比较)topic/topic_len:主题及长度data/data_len:消息数据及长度msg_id:消息 ID(与订阅/发布返回值对应)error_handle:错误信息
返回值:无(结构体,非函数)
MQTT_EVENT_*
事件宏:MQTT_EVENT_CONNECTED / DISCONNECTED / SUBSCRIBED / UNSUBSCRIBED / PUBLISHED / DATA / ERROR。
参数:
- 回调中通过
event->event_id与这些宏比较判断事件类型
返回值:无(宏定义)
📌 事件回调字段:
MQTT_EVENT_DATA时用event->topic(长度topic_len)、event->data(长度data_len);订阅/发布确认用event->msg_id与调用返回值对应。回调中不要做耗时操作(打印、处理等应交给任务队列)。
完整代码
以下为 tcp/demo.c 完整源码,与官方示例(applications/protocols/mqtt/tcp/tcp/demo.c)完全一致:
📜 点击展开 tcp/demo.c 完整代码
#include <stdio.h>
#include <FreeRTOS.h>
#include <task.h>
#include <mqtt_client.h>
#include "blog.h"
static void log_error_if_nonzero(const char *message, int error_code)
{
if (error_code != 0) {
blog_error("Last error %s: 0x%x", message, error_code);
}
}
static axk_err_t event_cb(axk_mqtt_event_handle_t event)
{
int32_t event_id;
axk_mqtt_client_handle_t client = event->client;
event_id = event->event_id;
blog_debug("Event dispatched, event_id=%d", event_id);
int msg_id;
switch ((axk_mqtt_event_id_t)event_id) {
case MQTT_EVENT_CONNECTED:
blog_info("MQTT_EVENT_CONNECTED");
msg_id = axk_mqtt_client_publish(client, "/topic/qos1", "data_3", 0, 1, 0);
blog_info("sent publish successful, msg_id=%d", msg_id);
msg_id = axk_mqtt_client_subscribe(client, "/topic/qos0", 0);
blog_info("sent subscribe successful, msg_id=%d", msg_id);
msg_id = axk_mqtt_client_subscribe(client, "/topic/qos1", 1);
blog_info("sent subscribe successful, msg_id=%d", msg_id);
msg_id = axk_mqtt_client_unsubscribe(client, "/topic/qos1");
blog_info("sent unsubscribe successful, msg_id=%d", msg_id);
break;
case MQTT_EVENT_DISCONNECTED:
blog_info("MQTT_EVENT_DISCONNECTED");
break;
case MQTT_EVENT_SUBSCRIBED:
blog_info("MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id);
msg_id = axk_mqtt_client_publish(client, "/topic/qos0", "data", 0, 0, 0);
blog_info("sent publish successful, msg_id=%d", msg_id);
break;
case MQTT_EVENT_UNSUBSCRIBED:
blog_info("MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_PUBLISHED:
blog_info("MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_DATA:
blog_info("MQTT_EVENT_DATA");
printf("TOPIC=%.*s\r\n", event->topic_len, event->topic);
printf("DATA=%.*s\r\n", event->data_len, event->data);
break;
case MQTT_EVENT_ERROR:
blog_info("MQTT_EVENT_ERROR");
if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) {
log_error_if_nonzero("reported from axk-tls", event->error_handle->axk_tls_last_axk_err);
log_error_if_nonzero("reported from tls stack", event->error_handle->axk_tls_stack_err);
log_error_if_nonzero("captured as transport's socket errno", event->error_handle->axk_transport_sock_errno);
blog_info("Last errno string (%s)", strerror(event->error_handle->axk_transport_sock_errno));
}
break;
default:
blog_info("Other event id:%d", event->event_id);
break;
}
return AXK_OK;
}
void mqtt_start(void)
{
axk_mqtt_client_config_t mqtt_cfg = {
.uri = "mqtt://mqtt.eclipseprojects.io",
.event_handle = event_cb,
};
axk_mqtt_client_handle_t client = axk_mqtt_client_init(&mqtt_cfg);
axk_mqtt_client_start(client);
}常见问题与踩坑提示
⚠️ 一直收不到 MQTT_EVENT_CONNECTED
原因:Broker 不可达、域名解析失败、或公共 Broker 已停服(官方默认 mqtt.eclipseprojects.io 目前可能已关闭)
解决:改用 mqtt://broker.emqx.io 或 mqtt://test.mosquitto.org;先电脑 MQTT 工具测试同一 Broker 是否可连
⚠️ MQTT_EVENT_ERROR 反复出现
原因:TCP 连接被拒/超时、Broker 要求认证、或网络环境拦截 1883 端口
解决:看错误回调中 axk_transport_sock_errno 与 errno 字符串定位;确认路由器可访问公网 1883 端口;需要账号密码的 Broker 用 .username/.password 配置
⚠️ 电脑工具能连 Broker,开发板连不上
原因:开发板侧 DNS 缓存/网络未就绪,或启动时机不对
解决:确认在 GOT_IP 之后才调用 mqtt_start()(官方示例即在 GOT_IP 回调中调用);等待几秒重试
⚠️ 发布成功但电脑收不到
原因:主题不匹配(含大小写)、QoS 设置问题、或电脑订阅时机晚于 retain=0 的发布
解决:核对主题字符串完全一致(/topic/qos0 以 / 开头);先在电脑订阅好再烧录开发板;调试阶段发布可用 retain=1 便于复查
⚠️ 事件回调里做大量处理导致消息丢失
原因:回调阻塞影响收包
解决:回调中只拷贝数据,用队列/任务交给业务线程处理(官方回调只打印)
⚠️ 一直打印 Connecting 不 GOT IP(连不上路由器)
原因:SSID/密码写错、路由器是 5GHz 频段、或信号过弱
解决:核对 tcp/main.c 里的 SSID/密码;确认路由器开 2.4GHz;开发板靠近路由器;可先单独跑 连接 Wi-Fi 验证联网
⚠️ 串口找不到设备 / 打不开
原因:USB 转串口驱动未装、权限不足,或数据线只能充电不能传数据
解决:Linux 用 lsusb/dmesg 查看设备,权限不足可 sudo chmod 666 /dev/ttyUSB0;Windows 装驱动后到设备管理器查 COM 口;换一根能传数据的线
⚠️ 烧录一直等待 / 失败
原因:未进入下载模式、波特率不对、或串口号填错
解决:烧录时按提示长按 EN 键进入下载模式;确认 p=/dev/ttyUSB0 换成你实际的串口;换 USB 口或数据线重试
运行自检
串口打印 MQTT_EVENT_CONNECTED 且电脑工具收到 data_3、data 两条消息,开发板能收到工具发布的主题消息,即 MQTT 验证通过。
遇到问题?
如有其他问题,请到统一的提问与讨论区:Ai-Thinker Discussions

