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

文章目录

  • 原创声明
  • 前言
  • 一、MQTT 是什么?
  • 二、Cube 配置
    • 2.1 STM32 ETH 设置
    • 2.2 修改 PHY 地址
    • 2.3 LWIP 设置
    • 在这里插入图片描述
  • 三、生成工程的简单测试
    • 3.1 手动修改 MAC 地址
    • 3.2 Ping 测试
  • 四、使用 LWIP 实现 MQTT
    • 4.1 打开 MQTT LOG
    • 4.2 获得在线 MQTT 测试网站 IP
    • 4.3 修改 LWIP 错误提示打印函数
    • 4.4 关于返回值的小技巧
    • 4.5 编写 连接及其回调函数(有详细注释)
    • 4.6 编写 接收数据回调函数(有详细注释)
    • 4.7 发送接口及其回填函数(详细注释)
    • 4.8 订阅接口及其回调函数
    • 4.9 初始化函数
  • 五、测试结果
    • 5.1 发送消息到服务器
    • 5.2 接收从服务器发送下来的数据
  • 六、我踩过的坑
    • 6.1 端口号必须是 TCP 的端口号
    • 6.2 mqtt_client_connect 中的 arg
    • 6.3 修改发送 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 设置

【嵌入式实战】一文拿下 STM32 Lwip MQTT(超详细)_第1张图片

三、生成工程的简单测试

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
【嵌入式实战】一文拿下 STM32 Lwip MQTT(超详细)_第2张图片

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 打印(超详细)
【嵌入式实战】一文拿下 STM32 Lwip MQTT(超详细)_第3张图片
可以在服务器 通信猫 共享MQTT服务器 在线客户端 接收到数据:
【嵌入式实战】一文拿下 STM32 Lwip MQTT(超详细)_第4张图片

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

【嵌入式实战】一文拿下 STM32 Lwip MQTT(超详细)_第5张图片
可以在 RTT 看到接收到的数据和在服务器发送下来的数据相同,成功!!!!
【嵌入式实战】一文拿下 STM32 Lwip MQTT(超详细)_第6张图片


六、我踩过的坑

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 ✨✨✨ ,满足下我的虚荣心,谢谢!!!


更多阅读推荐
  • 【嵌入式实战】STM32+Lwip 实现 DHCP+HostName(超详细) : 在 DHCP 的基础上 Ping 域名的方式获取 DHCP 的 IP
  • 【嵌入式实战】STM32+Lwip 实现 SNTP 网络授时(超详细)
  • 【嵌入式实战】STM32+FreeRTOS+LWIP+WolfSSL 实现 HTTPS(超详细)
  • 【嵌入式小技巧】stm32 实现 Segger RTT 打印(超详细)
    我是 HinGwenWoong ,一个有着清晰目标不停奋斗的程序猿,热爱技术,喜欢分享,码字不易,如果帮到您,请帮我在屏幕下方点赞 ,您的点赞可以让技术传播得更远更广,谢谢!

授权须知

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

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

  1. 若需要修改文章的排版,请根据原文链接联系作者
  2. 再次感谢您的认可,转载请遵守如上转载须知!

你可能感兴趣的:(嵌入式SMT32,嵌入式,网络,物联网,stm32,iot)