【嵌入式实战】一文拿下 STM32 Lwip MQTT(超详细)

原创声明

本文为 HinGwenWoong 原创,如果这篇文章对您有帮助,欢迎转载,转载请阅读文末的【授权须知】,感谢您对 HinGwenWoong 文章的认可!


前言

如今的时代发展很快,万物互联成为趋势,每个产品都需要连接到网络,MQTT这种及其轻量级的传输协议逐渐使用广泛,下面我为大家介绍如何使用 STM32 使用 LWIP 实现 MQTT

我是 HinGwenWoong ,一个有着清晰目标不停奋斗的程序猿,热爱技术,喜欢分享,码字不易,如果帮到您,请帮我在屏幕下方点赞 👍 ,您的点赞可以让技术传播得更远更广,谢谢!


一、MQTT 是什么?

MQTT是机器对机器(M2M)/物联网(IoT)连接协议。它被设计为一个极其轻量级发布/订阅消息传输协议。对于需要较小代码占用空间和/或网络带宽非常宝贵的远程连接非常有用,是专为受限设备和低带宽、高延迟或不可靠的网络而设计。这些原则也使该协议成为新兴的“机器到机器”(M2M)或物联网(IoT)世界的连接设备,以及带宽和电池功率非常高的移动应用的理想选择。例如,它已被用于通过卫星链路与代理通信的传感器、与医疗服务提供者的拨号连接,以及一系列家庭自动化和小型设备场景。它也是移动应用的理想选择,因为它体积小,功耗低,数据包最小,并且可以有效地将信息分配给一个或多个接收器。

——摘自 MQTT中文网


二、Cube 配置

2.1 STM32 ETH 设置

在这里插入图片描述

2.2 修改 PHY 地址

本项目使用的是 LAN8720 芯片,需要修改 PHY Address0
在这里插入图片描述

2.3 LWIP 设置

在这里插入图片描述

三、生成工程的简单测试

3.1 手动修改 MAC 地址

Cube 生成的 MAC 地址是固定的,防止和测试环境中的其他设备相撞,需要打开文件 ethernetif.c 手动修改 MAC 地址,我这里提取了 芯片ID作为MAC地址的最后几位,这里是 STM32F767 的芯片ID的地址 0x1FF0F420
在这里插入图片描述

uint32_t sn0 = *(uint32_t *)(0x1FF0F420);//STM32 cpu id
MACAddr[3] = (sn0 >> 16) & 0xFF;
MACAddr[4] = (sn0 >> 8) & 0xFFF;
MACAddr[5] = sn0 & 0xFF;

3.2 Ping 测试

编译 -> 烧录 到单片机里面,拿一条和 PC 在同一局域网内的网线,根据 MX_LWIP_Init()函数下面设置的 IP 测试 ping 功能,下面是成功的结果图:
在这里插入图片描述


四、使用 LWIP 实现 MQTT

4.1 打开 MQTT LOG

打开文件 lwipopts.h 加入如下代码,如何使用 JLink 实现 Segger log 可以按参考我之前的文章: [【嵌入式小技巧】stm32 实现 Segger RTT 打印(超详细)]

#define LWIP_DEBUG
#include "bsp_printlog.h"
#undef LWIP_PLATFORM_DIAG
#define LWIP_PLATFORM_DIAG(x) do {print_log x;} while(0)
//打开 MQTT DEBUG 模式以便更好观察
#define MQTT_DEBUG LWIP_DBG_ON

4.2 获得在线 MQTT 测试网站 IP

这里使用的是 通信猫 共享MQTT服务器 在线客户端,方便快捷
首先使用电脑 CMD 根据 通信猫 的域名解析出 IP,这里暂时没用到 lwip 的自动域名解析,获得 IP = 120.76.100.197
在这里插入图片描述

4.3 修改 LWIP 错误提示打印函数

打开文件 cc.h ,修改 LWIP_PLATFORM_ASSERT 宏定义,否则没有重定义 printf 的前提下,LWIP 出错的时候会导致系统直接死机

#include "bsp_printlog.h"
#define LWIP_PLATFORM_ASSERT(x) do {print_log("Assertion \"%s\" failed at line %d in %s\n", \
x, __LINE__, __FILE__); } while(0)

4.4 关于返回值的小技巧

将 lwip 接口的返回值放到 lwip_strerr 函数,会打印函数的错误提示,前提是需要开启 LWIP_DEBUG

print_log("mqtt state : %s",lwip_strerr(ret));

4.5 编写 连接及其回调函数(有详细注释)

/*!
* @brief MQTT 连接成功的处理函数,需要的话在应用层重新定义
*
* @param [in1] : MQTT 连接句柄
* @param [in2] : MQTT 连接参数指针
*
* @retval: None
*/
__weak void mqtt_conn_suc_proc(mqtt_client_t *client, void *arg)
{
    //这里是连接成功之后进行 订阅操作
    char test_sub_topic[] = "/public/TEST/AidenHinGwenWong_sub"; //订阅 topic
    bsp_mqtt_subscribe(client, test_sub_topic, 0);               //订阅接口函数
}

/*!
* @brief MQTT 处理失败调用的函数
*
* @param [in1] : MQTT 连接句柄
* @param [in2] : MQTT 连接参数指针
*
* @retval: None
*/
__weak void mqtt_error_process_callback(mqtt_client_t *client, void *arg)
{
    /* 这里可以做重连操作,根据 clent 指针可以知道是哪个MQTT的连接句柄 */
}

/*!
* @brief MQTT 连接状态的回调函数
*
* @param [in] : MQTT 连接句柄
* @param [in] : 用户提供的回调参数指针
* @param [in] : MQTT 连接状态
* @retval: None
*/
static void bsp_mqtt_connection_cb(mqtt_client_t *client, void *arg, mqtt_connection_status_t status)
{
    if (client == NULL)
    {
        //错误返回
        print_log("bsp_mqtt_connection_cb: condition error @entry\n");
        return;
    }
    if (status == MQTT_CONNECT_ACCEPTED) //Successfully connected
    {
        print_log("bsp_mqtt_connection_cb: Successfully connected\n");

        // 注册接收数据的回调函数
        mqtt_set_inpub_callback(client, bsp_mqtt_incoming_publish_cb, bsp_mqtt_incoming_data_cb, arg);

        //成功处理函数
        mqtt_conn_suc_proc(client, arg);
    }
    else
    {
        print_log("bsp_mqtt_connection_cb: Fail connected, status = %s\n", lwip_strerr(status));
        //错误处理
        mqtt_error_process_callback(client, arg);
    }
}

/*!
* @brief 连接到 mqtt 服务器
* 执行条件:无
*
* @param [in] : None
*
* @retval: 连接状态,如果返回不是 ERR_OK 则需要重新连接
*/
static err_t bsp_mqtt_connect(void)
{
    print_log("bsp_mqtt_connect: Enter!\n");
    err_t ret;
    struct mqtt_connect_client_info_t mqtt_connect_info = {

                "AidenHinGwenWong_MQTT_Test",        /* 这里需要修改,以免在同一个服务器两个相同ID会发生冲突 */
                NULL,                                /* MQTT 服务器用户名 */
                NULL,                                /* MQTT 服务器密码 */
                60,                                  /* 与 MQTT 服务器保持连接时间,时间超过未发送数据会断开 */
                "/public/TEST/AidenHinGwenWong_pub", /* MQTT遗嘱的消息发送topic */
                "Offline_pls_check",                 /* MQTT遗嘱的消息,断开服务器的时候会发送 */
                0,                                   /* MQTT遗嘱的消息 Qos */
                0                                    /* MQTT遗嘱的消息 Retain */
    };

    ip_addr_t server_ip;
    ip4_addr_set_u32(&server_ip, ipaddr_addr("120.76.100.197")); //MQTT服务器IP
    uint16_t server_port = 18830;                                //注意这里是 MQTT 的 TCP 连接方式的端口号!!!!
    if (s__mqtt_client_instance == NULL)
    {
        // 句柄==NULL 才申请空间,否则无需重复申请
        s__mqtt_client_instance = mqtt_client_new();
    }
    if (s__mqtt_client_instance == NULL)
    {
        //防止申请失败
        print_log("bsp_mqtt_connect: s__mqtt_client_instance malloc fail @@!!!\n");
        return ERR_MEM;
    }
    //进行连接,注意:如果需要带入 arg ,arg必须是全局变量,局部变量指针会被回收,大坑!!!!!
    ret = mqtt_client_connect(s__mqtt_client_instance, &server_ip, server_port,\
                              bsp_mqtt_connection_cb, NULL, &mqtt_connect_info);
    /******************
	小提示:连接错误不需要做任何操作,mqtt_client_connect 中注册的回调函数里面做判断并进行对应的操作
	*****************/

    print_log("bsp_mqtt_connect: connect to mqtt %s\n", lwip_strerr(ret));
    return ret;
}

4.6 编写 接收数据回调函数(有详细注释)

/*!
* @brief mqtt 接收数据处理函数接口,需要在应用层进行处理
* 执行条件:mqtt连接成功
*
* @param [in1] : 用户提供的回调参数指针
* @param [in2] : 接收的数据指针
* @param [in3] : 接收数据长度
* @retval: 处理的结果
*/
__weak int mqtt_rec_data_process(void *arg, char *rec_buf, uint64_t buf_len)
{
    print_log("recv_buffer = %s\n", rec_buf);
    return 0;
}

/*!
* @brief MQTT 接收到数据的回调函数
* 执行条件:MQTT 连接成功
*
* @param [in1] : 用户提供的回调参数指针
* @param [in2] : MQTT 收到的分包数据指针
* @param [in3] : MQTT 分包数据长度
* @param [in4] : MQTT 数据包的标志位
* @retval: None
*/
static void bsp_mqtt_incoming_data_cb(void *arg, const u8_t *data, u16_t len, u8_t flags)
{
    if ((data == NULL) || (len == 0))
    {
        //错误返回
        print_log("mqtt_client_incoming_data_cb: condition error @entry\n");
        return;
    }

    if (s__mqtt_recv_buffer_g.recv_len + len < sizeof(s__mqtt_recv_buffer_g.recv_buffer))
    {
        //将接收到的数据加入buffer中
        snprintf(&s__mqtt_recv_buffer_g.recv_buffer[s__mqtt_recv_buffer_g.recv_len], len, "%s", data);
        s__mqtt_recv_buffer_g.recv_len += len;
    }

    if ((flags & MQTT_DATA_FLAG_LAST) == MQTT_DATA_FLAG_LAST) //接收的是最后的分包数据——接收已经完成
    {
        //处理数据
        mqtt_rec_data_process(arg, s__mqtt_recv_buffer_g.recv_buffer, s__mqtt_recv_buffer_g.recv_len);

        //已接收字节计数归0
        s__mqtt_recv_buffer_g.recv_len = 0;

        //清空接收buffer
        memset(s__mqtt_recv_buffer_g.recv_buffer, 0, sizeof(s__mqtt_recv_buffer_g.recv_buffer));
    }
    print_log("mqtt_client_incoming_data_cb:reveiving incomming data.\n");
}

/*!
* @brief MQTT 接收到数据的回调函数
* 执行条件:MQTT 连接成功
*
* @param [in] : 用户提供的回调参数指针
* @param [in] : MQTT 收到数据的topic
* @param [in] : MQTT 收到数据的总长度
* @retval: None
*/
static void bsp_mqtt_incoming_publish_cb(void *arg, const char *topic, u32_t tot_len)
{
    if ((topic == NULL) || (tot_len == 0))
    {
        //错误返回
        print_log("bsp_mqtt_incoming_publish_cb: condition error @entry\n");
        return;
    }
    print_log("bsp_mqtt_incoming_publish_cb: topic = %s.\n", topic);
    print_log("bsp_mqtt_incoming_publish_cb: tot_len = %d.\n", tot_len);
    s__mqtt_recv_buffer_g.recv_total = tot_len; //需要接收的总字节
    s__mqtt_recv_buffer_g.recv_len = 0;         //已接收字节计数归0

    //清空接收buffer
    memset(s__mqtt_recv_buffer_g.recv_buffer, 0, sizeof(s__mqtt_recv_buffer_g.recv_buffer));
}

4.7 发送接口及其回填函数(详细注释)

/*!
* @brief MQTT 发送数据的回调函数
* 执行条件:MQTT 连接成功
*
* @param [in] : 用户提供的回调参数指针
* @param [in] : MQTT 发送的结果:成功或者可能的错误
* @retval: None
*/
static void mqtt_client_pub_request_cb(void *arg, err_t result)
{
    //传进来的 arg 还原
    mqtt_client_t *client = (mqtt_client_t *)arg;

    if (result != ERR_OK)
    {
        print_log("mqtt_client_pub_request_cb: c002: Publish FAIL, result = %s\n", lwip_strerr(result));

        //错误处理
        mqtt_error_process_callback(client, arg);
    }
    else
    {
        print_log("mqtt_client_pub_request_cb: c005: Publish complete!\n");
    }
}

/*!
* @brief 发送消息到服务器的接口函数
* 执行条件:无
*
* @param [in1] : mqtt 连接句柄
* @param [in2] : mqtt 发送 topic 指针
* @param [in3] : 发送数据包指针
* @param [in4] : 数据包长度
* @param [in5] : qos
* @param [in6] : retain
* @retval: 发送状态
* @note: 有可能发送不成功但是现实返回值是 0 ,需要判断回调函数 mqtt_client_pub_request_cb 是否 result == ERR_OK
*/
err_t bsp_mqtt_publish(mqtt_client_t *client, char *pub_topic, char *pub_buf, uint16_t data_len, uint8_t qos, uint8_t retain)
{
    if ((client == NULL) || (pub_topic == NULL) || (pub_buf == NULL) || \
    	(data_len == 0) || (qos > 2) || (retain > 1))
    {
        print_log("bsp_mqtt_publish: input error@@");
        return ERR_VAL;
    }

    //判断是否连接状态
    if (mqtt_client_is_connected(client) != pdTRUE)
    {
        print_log("bsp_mqtt_publish: client is not connected\n");
        return ERR_CONN;
    }

    err_t err;
#ifdef USE_MQTT_MUTEX
    // 创建 mqtt 发送互斥锁
    if (s__mqtt_publish_mutex == NULL)
    {
        print_log("bsp_mqtt_publish: create mqtt mutex ! \n");
        s__mqtt_publish_mutex = xSemaphoreCreateMutex();
    }
    if (xSemaphoreTake(s__mqtt_publish_mutex, portMAX_DELAY) == pdPASS)
#endif /* USE_MQTT_MUTEX */
    {
        err = mqtt_publish(client, pub_topic, pub_buf, data_len,\
                           qos, retain, mqtt_client_pub_request_cb, (void *)client);
        print_log("bsp_mqtt_publish: mqtt_publish err = %s\n", lwip_strerr(err));
#ifdef USE_MQTT_MUTEX
        print_log("bsp_mqtt_publish: mqtt_publish xSemaphoreTake\n");
        xSemaphoreGive(s__mqtt_publish_mutex);
#endif /* USE_MQTT_MUTEX */
    }
    return err;
}

4.8 订阅接口及其回调函数

/*!
* @brief MQTT 订阅的回调函数
* 执行条件:MQTT 连接成功
*
* @param [in] : 用户提供的回调参数指针
* @param [in] : MQTT 订阅结果
* @retval: None
*/
static void bsp_mqtt_request_cb(void *arg, err_t err)
{
    if (arg == NULL)
    {
        print_log("bsp_mqtt_request_cb: input error@@\n");
        return;
    }
    mqtt_client_t *client = (mqtt_client_t *)arg;
    if (err != ERR_OK)
    {
        print_log("bsp_mqtt_request_cb: FAIL sub, sub again, err = %s\n", lwip_strerr(err));
        //错误处理
        mqtt_error_process_callback(client, arg);
    }
    else
    {
        print_log("bsp_mqtt_request_cb: sub SUCCESS!\n");
    }
}

/*!
* @brief mqtt 订阅
* 执行条件:连接成功
*
* @param [in1] : mqtt 连接句柄
* @param [in2] : mqtt 发送 topic 指针
* @param [in5] : qos
* @retval: 订阅状态
*/
static err_t bsp_mqtt_subscribe(mqtt_client_t *mqtt_client, char *sub_topic, uint8_t qos)
{
    print_log("bsp_mqtt_subscribe: Enter\n");

    if ((mqtt_client == NULL) || (sub_topic == NULL) || (qos > 2))
    {
        print_log("bsp_mqtt_subscribe: input error@@\n");
        return ERR_VAL;
    }
    if (mqtt_client_is_connected(mqtt_client) != pdTRUE)
    {
        print_log("bsp_mqtt_subscribe: mqtt is not connected, return ERR_CLSD.\n");
        return ERR_CLSD;
    }
    err_t err;

    //订阅及注册回调函数
    err = mqtt_subscribe(mqtt_client, sub_topic, qos, bsp_mqtt_request_cb, (void *)mqtt_client);
    if (err != ERR_OK)
    {
        print_log("bsp_mqtt_subscribe: mqtt_subscribe Fail, return:%s \n", lwip_strerr(err));
    }
    else
    {
        print_log("bsp_mqtt_subscribe: mqtt_subscribe SUCCESS, reason: %s\n", lwip_strerr(err));
    }
    return err;
}

4.9 初始化函数

/*!
* @brief 封装 MQTT 初始化接口
* 执行条件:无
*
* @retval: 无
*/
void bsp_mqtt_init(void)
{
    print_log("Mqtt init...\n");

    // 连接服务器
    bsp_mqtt_connect();

    // 发送消息到服务器
    char message_test[] = "Hello mqtt server";
    for (int i = 0; i < 10; i++)
    {
        bsp_mqtt_publish(s__mqtt_client_instance, "/public/TEST/AidenHinGwenWong_pub",\
                         message_test, sizeof(message_test), 1, 0);
        vTaskDelay(1000);
    }
}

五、测试结果

5.1 发送消息到服务器

打开 RTT 窗口,连接 RTTSTM32 ,烧录代码,
如何使用 JLink 实现 Segger log 可以按参考我之前的文章: 【嵌入式小技巧】stm32 实现 Segger RTT 打印(超详细)
在这里插入图片描述
可以在服务器 通信猫 共享MQTT服务器 在线客户端 接收到数据:
在这里插入图片描述

5.2 接收从服务器发送下来的数据

在这里插入图片描述
可以在 RTT 看到接收到的数据和在服务器发送下来的数据相同,成功!!!!
在这里插入图片描述


六、我踩过的坑

6.1 端口号必须是 TCP 的端口号

一般的 MQTT 服务器都会有 2个端口号,一个是 TCP 协议的,另外一个是 Websocket 协议的,使用 LWIPMQTT 需要使用 TCP 的端口号。

6.2 mqtt_client_connect 中的 arg

arg 指针必须是全局变量,局部变量指针会被回收!!!!!

6.3 修改发送 MQTT 包大小:

打开 mqtt_opts.h,里面有关于 MQTT 的配置,默认发送字节最大是 256,增大发送字节数只需要修改 MQTT_OUTPUT_RINGBUF_SIZE 宏定义后面的数字即可


总结

以上是 STM32+Lwip 实现 MQTT 的全部内容。项目文件已经上传到 GitHub :STM32_LWIP_MQTT如果有帮助请大家帮忙右上角点个 star ✨✨✨ ,满足下我的虚荣心,谢谢!!!


更多阅读推荐

授权须知

  1. 原创文章在推送两天后才可进行转载
  2. 转载文章,禁止声明原创
  3. 不允许直接二次转载,转载请根据原文链接联系作者
  4. 若无需改版,在文首清楚标注作者及来源/原文链接,并删除【原创声明】,即可直接转载。
    但对于未注明转载来源/原文链接的文章,我将保留追述的权利。

作者:HinGwenWoong
一个有着清晰目标不停奋斗的程序猿,热爱技术,喜欢分享,共同进步!
CSDN: HinGwenWoong
原文链接:【嵌入式实战】STM32+Lwip 实现 MQTT(超详细步骤+代码注释,内含避坑提示)

  1. 若需要修改文章的排版,请根据原文链接联系作者
  2. 再次感谢您的认可,转载请遵守如上转载须知!
  • 27
    点赞
  • 165
    收藏
    觉得还不错? 一键收藏
  • 21
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值