目录
1 前言
2 什么是MQTT协议?
2.1 特点
2.2 应用
2.3 身份
2.4 消息质量等级
2.5 遗嘱消息
3 硬件介绍
4 硬件接线
5 代码编写
6 移植说明
7 最终现象
8 总结
9 项目链接
随着物联网技术的快速发展,MQTT(Message Queuing Telemetry Transport)协议已成为一种广泛使用的通讯协议,它适用于设备间低带宽、高延迟、不可靠的网络通信。
W5500是一款集成全硬件 TCP/IP 协议栈的嵌入式以太网控制器,同时也是一颗工业级以太网控制芯片。在以太网应用中使用 W5500 + MQTT应用协议让用户可以更加方便地在设备之间实现远程连接和通信。本教程将介绍W5500以太网MQTT应用的基本原理、使用步骤、应用实例以及注意事项,帮助读者更好地掌握这一技术。
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议)是一种基于发布-订阅模式的轻量级通讯协议,它构建于TCP/IP协议上,由IBM在1999年发布。MQTT最大长处在于,能够以很少的代码和有限的带宽,为衔接远程设备供给实时可靠的音讯效劳。
MQTT 协议广泛应用于物联网、移动互联网、智能硬件、车联网、电力能源等领域。
MQTT通信过程中,共有三种身份:
MQTT设计了3个QoS等级
QoS 0是发布者发送完消息之后,不再关心对方有没有收到,也不设置任何重发机制。
QoS 1包含了简单的重发机制,发布者发送完消息之后会一直等待接收者的ACK,如果没有收到ACK则一直重发,这种模式保证消息至少到达一次,但无法保证消息重复。
QoS 2设计了重发和重复消息发现机制,保证消息到达对方并严格只到达了一次。
MQTT的协议报文以及更详细介绍请参考:MQTT3.1.1协议手册
客户端的遗嘱只在意外断线时才会发布,如果客户端正常的断开了与服务端的连接,这个遗嘱机制是不会启动的,服务端也不会将客户端的遗嘱公布。
遗嘱消息可以看作是一个简化版的 PUBLISH 消息,他也包含 Topic, Payload, QoS 等字段。遗嘱消息会在设备与服务端连接时,通过 CONNECT 报文指定,然后在设备意外断线时由服务端将该遗嘱消息发布到连接时指定的遗嘱主题(Will Topic)上。
一般建议设置遗嘱消息内容为client offline!在我们的客户端连接上服务器时,也向遗嘱主题发布一条client online消息。
如果采用传统以太网方式我们接入到以太网中实现MQTT协议的应用,那我们需要按照下面方式进行接线。不仅硬件上比较复杂,而且还需要编写软件协议栈,以及应用层协议交互的代码。
在这里我向大家推荐一块开发板,它硬件上集成了MCU+MAC+PHY+RJ45,还拥有硬件TCP/IP协议栈。在我们要开发和学习嵌入式设备入网和交互时,仅需这一块开发板就行了。
W5500-EVB-Pico是一款搭载了以太网芯片的高性能、低成本的开发板。主控芯片采用的是树莓派的RP2040,搭载了双核M0架构处理器,频率最高可达133MHz,还拥有264KB高速SRAM和2MB的板载闪存以及丰富的外设资源。
其搭载的以太网芯片W5500是一款高性价比的以太网芯片,更是拥有全球独一无二的全硬件TCP/IP协议栈专利技术,在我们开发过程中无需深究协议的交互以及组包过程,只需处理应用层即可!还拥有8个独立的硬件socket,可以同时进行通信互不干扰,无论是工业使用还是学习,W5500-EVB-Pico都是一个非常不错的选择!
并且W5500这款以太网芯片,供货稳定,久经市场考验,反馈都特别不错,简单稳定,易于上手,可以帮助我们缩短开发周期,项目快速落地!
程序的运行框图如下所示:
我们使用的是WIZnet官方的ioLibrary_Driver库。该库支持的协议丰富,操作简单,芯片在硬件上集成了TCP/IP协议栈,该库又封装好了TCP/IP层之上的协议,我们只需简单调用相应函数即可完成协议的应用。
在开发板上RP2040芯片通过内部连线到W5500芯片上,SPI引脚定义在w5500_spi.h文件中,如下所示:
/* Pin definition */
#define PIN_SCK 18
#define PIN_MOSI 19
#define PIN_MISO 16
#define PIN_CS 17
#define PIN_RST 20
MQTT连接参数以及订阅参数在mqtt_client.c文件中定义:
/* MQTT parameter assignment */
mqttconn mqtt_params = {
.server_ip = {54, 244, 173, 190},
.port = 1883,
.clientid = "WIZnet_W5500_EVB_Pico",
.username = "W5500",
.passwd = "W5500",
.pubtopic = "W5500_pub",
.pubQoS = 0,
.subtopic = "W5500_sub",
.subQoS = 0,
.willtopic = "W5500_will",
.willQoS = 0,
.willmsg = "W5500 offline!",
};
网络地址信息在mqtt_client.c文件中定义:
/* Network address information */
static wiz_NetInfo net_info = {
.mac = {0x00, 0x08, 0x22, 0x82, 0xed, 0x2e},
.ip = {192, 168, 1, 20},
.sn = {255, 255, 255, 0},
.gw = {192, 168, 1, 1},
.dns = {8, 8, 8, 8},
.dhcp = NETINFO_STATIC};
(如需测试,仅需修改以上三个部分的内容即可)
接下来在主函数中,我们只需要根据运行框图编写代码即可。
int main()::
{
struct repeating_timer timer;
stdio_init_all();
sleep_ms(3000);
printf("W5500 mqtt example.\r\n");
while (true)
{
switch (run_status)
{
case INIT:
/* w5500 chip and SPI initialization */
wizchip_initialize(); /* Initialize the SPI and PHY detection */
wizchip_setnetinfo(&net_info); /* Set network address information */
print_network_information(net_info); /* Print network address information */
run_status = MQTT_INIT;
break;
case MQTT_INIT:
add_repeating_timer_ms(1, repeating_timer_callback, NULL, &timer); /* Turns on a 1-millisecond timer */
mqtt_init(); /* MQTT client and connection parameters initialization */
run_status = CONN;
break;
case CONN:
mqtt_conn();
run_status = SUB;
break;
case SUB:
mqtt_sub(mqtt_params.subtopic, mqtt_params.subQoS, messageArrived); /* Subscribe to Topics */
run_status = PUB_ONLINE;
break;
case PUB_ONLINE:
mqtt_sendmsg(mqtt_params.willtopic, 0, "W5500 online!"); /* Release the online news */
run_status = KEEPALIVE;
break;
case KEEPALIVE:
MQTTYield(&c, 30); /* keepalive MQTT */
sleep_ms(100);
break;
default:
break;
}
}
}
MQTT连接初始化函数如下所示,如不需
void mqtt_init(void)
{
MQTTPacket_connectData data = MQTTPacket_connectData_initializer; /* MQTT client structure initialization */
MQTTPacket_willOptions willdata = MQTTPacket_willOptions_initializer; /* Will subject struct initialization */
NewNetwork(&n, MQTT_SOCKET); /* Specifies the socket to which the MQTT is connected */
ConnectNetwork(&n, mqtt_params.server_ip, mqtt_params.port); /* Specifies the address and port to connect to the MQTT server */
MQTTClientInit(&c, &n, 1000, mqtt_send_buff, MQTT_SEND_BUFF_SIZE, mqtt_recv_buff, MQTT_RECV_BUFF_SIZE); /* MQTT client initialization */
data.willFlag = 1; /* will flag */
willdata.qos = mqtt_params.willQoS; /* will QoS */
willdata.topicName.lenstring.data = mqtt_params.willtopic; /* will topic */
willdata.topicName.lenstring.len = strlen(willdata.topicName.lenstring.data); /* will topic len */
willdata.message.lenstring.data = mqtt_params.willmsg; /* will message */
willdata.message.lenstring.len = strlen(willdata.message.lenstring.data); /* will message len */
willdata.retained = 0;
willdata.struct_version = 3;
data.will = willdata;
data.MQTTVersion = 4; // Server version,The 4 represents version 3.1.1
data.clientID.cstring = mqtt_params.clientid; // clientid
data.username.cstring = mqtt_params.username; // username
data.password.cstring = mqtt_params.passwd; // password
data.keepAliveInterval = 30; // keepalive
data.cleansession = 1; // clean session flag
}
MQTT连接函数如下所示:
void mqtt_conn(void)
{
/* Connect MQTT, the maximum number of connections does not exceed the set value */
int ret, i;
for (i = 0; i < conn_max_err; i++)
{
ret = MQTTConnect(&c, &data);
printf("Connect to the MQTT server: %d.%d.%d.%d:%d\r\n", mqtt_params.server_ip[0], mqtt_params.server_ip[1], mqtt_params.server_ip[2], mqtt_params.server_ip[3], mqtt_params.port);
printf("Connected:%s\r\n\r\n", ret == 0 ? "success" : "failed");
if (!ret)
{
break;
}
sleep_ms(1000);
}
if (ret)
{
while (1)
{
printf("Failed to connect to the MQTT server!");
sleep_ms(1000);
}
}
}
mqtt订阅参数如下,参1表示订阅主题名,参2表示订阅QoS等级,参3表示接收该主题时的消息回调函数
void mqtt_sub(char *subtopic, int QoS, messageHandler messageHandler)
{
/* Subscribe to topics, the maximum number of subscriptions does not exceed the set value */
int ret, i;
for (i = 0; i < sub_max_err; i++)
{
ret = MQTTSubscribe(&c, subtopic, QoS, messageHandler);
printf("Subscribing to %s\r\n", subtopic);
printf("Subscribed:%s\r\n\r\n", ret == 0 ? "success" : "failed");
if (!ret)
{
break;
}
sleep_ms(1000);
}
if (ret)
{
while (1)
{
printf("Subscription failure!");
sleep_ms(1000);
}
}
}
订阅函数的消息回调函数如下所示:
void messageArrived(MessageData *md)
{
char topicname[64] = {0};
char msg[512] = {0};
sprintf(topicname, "%.*s", (int)md->topicName->lenstring.len, md->topicName->lenstring.data);
sprintf(msg, "%.*s", (int)md->message->payloadlen, (char *)md->message->payload);
printf("recv:%s,%s\r\n\r\n", topicname, msg);
mqtt_sendmsg(mqtt_params.pubtopic, 0, msg);
}
发布消息函数如下,参1表示发布主题,参2表示发布质量等级,参3表示发布消息内容
void mqtt_sendmsg(char *pubtopic, int QoS, char *msg)
{
int ret;
MQTTMessage pubmessage = {
.qos = QoS,
.retained = 0,
.dup = 0,
.id = 0,
};
pubmessage.payload = msg;
pubmessage.payloadlen = strlen(pubmessage.payload);
ret = MQTTPublish(&c, pubtopic, &pubmessage);
if (ret)
{
printf("Failed to send message!");
}
else
{
printf("publish:%s,%s\r\n\r\n", pubtopic, pubmessage.payload);
}
}
1ms定时器回调函数如下,必须把mqtt库中的1ms定时器注册进来
bool repeating_timer_callback(struct repeating_timer *t)
{
MilliTimer_Handler(); /* Register the 1mm MQTT timer */
return true;
}
如果你想在你的开发板上加入W5500芯片实现以太网功能,则你可以购买一个W5500IO模块。基于例程进行移植。步骤如下
如需移植教程请在评论区留言:移植教程;后续我会根据反馈情况发布一个移植教程。
在MQTTX工具上,我们新建连接,服务器和端口号以及MQTT版本都设置成与开发板一致即可。并添加一个订阅W5500_pub(即开发板发布的主题)和W5500_will(即开发板遗嘱主题)。
我们按住RUN运行按钮然后用USB先连接到电脑,此时开发板会虚拟成U盘,我们只需要把编译好的文件复制进U盘中即可。
在网线没有连接至开发板时,USB会一直提示网络接口未连接。
在接入网线之后,会打印网络地址信息以及连接订阅状态,并向遗嘱主题发布一条客户端上线消息。
此时MQTTX工具上便可收到来自开发板上线的消息。
我们在对话框下面将MQTTX的发布主题改为W5500_sub(即开发板的订阅主题),并发布一条消息。
开发板上也是同样的,将接收到的消息以及发布的消息通过USB打印出来。
最后,我们将开发板上的网线断开,服务器发现开发板没有定时发送心跳包,认为异常断开,会向遗嘱主题发送开发板掉线消息。
至此,我们通过简单配置开发板之后实现了连接MQTT服务器,并且发布了一条消息给其他客户端,也能接收到来自订阅的消息。在我们的使用过程中,可以直接将例程中的初始化以及发布,订阅函数进行移植,并根据自己的业务需求进行修改即可。总而言之,硬件集成了TCP/IP协议栈的W5500芯片可以帮助我们在开发时无需太过关注协议的底层及组包过程,只需要按照官方提供的库进行传入我们的参数即可,这帮助我们的项目快速落地。对初次接触以太网模块的小伙伴们也比较友好,会更容易上手。
MQTT例程https://gitee.com/wiznet-hk/w5500-evb-pico-routine/tree/master/examples/mqtt_client
开发板资料http://docs.wiznet.io/Product/iEthernet/W5500/w5500-evb-pico
MQTT3.1.1 协议手册http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.pdf
ioLibrary_Driver库https://github.com/Wiznet/ioLibrary_Driver/tree/ce4a7b6d07541bf0ba9f91e369276b38faa619bd