Skip to content

概述

MQTT(Message Queuing Telemetry Transport)是物联网最主流的发布/订阅轻量消息协议:设备作为客户端连接 Broker(消息代理,消息的中转站),按主题(Topic)发布(publish)和订阅(subscribe)消息,双方无需知道对方存在,天然支持一对多、多对一通信。相比 HTTP,MQTT 报文开销小、支持 QoS 分级与离线消息,适合弱网、低功耗设备。本教程演示:连接公共 Broker,发布/订阅 /topic/qos0/topic/qos1 主题并打印收到的数据。

用大白话讲:MQTT 就像微信订阅公众号——设备「关注」(订阅)某个主题,公众号发文章(发布消息)后,所有关注的人都能收到,作者不需要知道谁在看。中间的「公众号平台」就是 Broker(消息中转站),主题(Topic)就是「频道名」。开发板连上 Broker 后,发布和订阅都通过主题完成。

本教程基于安信可官方 SDKAi-Thinker-Open/Ai-Thinker-WB2,版本 release_bl_iot_sdk_1.6.40)的官方示例 applications/protocols/mqtt/tcp 编写,代码可在本地 SDK 中直接找到。

🎯本页目标通过 axk_mqtt 组件连接 Broker、发布与订阅主题,掌握 MQTT 客户端初始化、事件回调与收发流程。
🧰前置条件① Ai-WB2 开发板一块(Type-C 数据线)② 2.4GHz 路由器(能访问公网)③ 电脑一台(MQTT 调试工具,如 MQTTX)④ 已按 [SDK 安装](../sdk/sdk_intro) 完成环境搭建,并完成 [连接 Wi-Fi](./wifi_connect)。
🔗相关章节加密版 MQTT 见 [MQTTS](./mqtts);HTTP 上报见 [HTTP POST](./http_post)。

进入示例工程

打开终端,进入官方 mqtt/tcp 示例工程目录:

cd ~/Ai-Thinker-WB2/applications/protocols/mqtt/tcp

说明:cd 是「进入目录」的命令,进入官方示例工程;后面所有 make 命令都要在这个目录下执行。

修改路由器参数与 Broker 地址

打开 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.iomqtt://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_3data 两条消息即成功;如果一直没出现 MQTT_EVENT_CONNECTED,先确认开发板已 GOT IP(联网)、电脑 MQTT 工具能连上同一 Broker(目标服务器可达),否则见文末 FAQ。

💡 同一主题可被多台设备同时订阅——在另一台设备/另一个工具会话发布消息,所有订阅者(含开发板)都会收到,这就是 MQTT 一对多通信。

代码执行流程

例程从启动到运行的完整流程如下(图中的循环箭头表示反复执行):


本文 API 汇总

axk_mqtt_client_init(&config)

创建 MQTT 客户端(配置 uri/事件回调),返回句柄。

参数

  • configaxk_mqtt_client_config_t 结构体指针,.urimqtt://host:port)与 .event_handle(事件回调)必填

返回值:成功返回客户端句柄(axk_mqtt_client_handle_t);失败返回 NULL

axk_mqtt_client_start(client)

启动客户端并异步连接 Broker。

参数

  • clientaxk_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_unsubscribe(client, topic)

取消订阅主题。

参数

  • client:客户端句柄
  • topic:主题字符串

返回值:成功返回 0;失败返回负值错误码

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 完整代码
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.iomqtt://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_3data 两条消息,开发板能收到工具发布的主题消息,即 MQTT 验证通过。

遇到问题?

如有其他问题,请到统一的提问与讨论区:Ai-Thinker Discussions

Released under the MIT License. Build Time 2026-09-11 14:52:23