目录
1 前言
随着物联网技术的快速发展,MQTT(Message Queuing Telemetry Transport)协议已成为一种广泛使用的通讯协议,它适用于设备间低带宽、高延迟、不可靠的网络通信。
W5500是一款集成全硬件 TCP/IP 协议栈的嵌入式以太网控制器,同时也是一颗工业级以太网控制芯片。在以太网应用中使用 W5500 + MQTT应用协议让用户可以更加方便地在设备之间实现远程连接和通信。本教程将介绍W5500以太网MQTT应用的基本原理、使用步骤、应用实例以及注意事项,帮助读者更好地掌握这一技术。
2 什么是MQTT协议?
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议)是一种基于发布-订阅模式的轻量级通讯协议,它构建于TCP/IP协议上,由IBM在1999年发布。MQTT最大长处在于,能够以很少的代码和有限的带宽,为衔接远程设备供给实时可靠的音讯效劳。
2.1 特点
- 轻量级:MQTT协议设计简单,协议头部开销小,适用于资源受限的设备和网络。
- 低带宽消耗:很小的传输消耗和协议数据交换,最大限度减少网络流量。
- 异步通信:客户端可以随时发布和订阅消息,无需等待对方的响应。
- 发布-订阅模式:消息发布者将消息发布到特定的主题,而订阅者则订阅感兴趣的主题。这种模式支持松耦合的通信和灵活的消息传递。
2.2 应用
MQTT协议在以太网中的应用场景非常广泛,它可以应用于以下几个领域:
- 智能家居:在智能家居领域,MQTT协议可以用于连接家电设备,如空调、照明、窗帘等,通过以太网实现远程控制和定时任务的执行。
- 智能城市:在智能城市领域,MQTT协议可以用于连接各种城市设施,如交通信号灯、环境监测站、智能井盖等,实现城市数据的采集和设备的远程控制。
- 工业物联网:在工业物联网领域,MQTT协议可以用于连接各种工业设备,如传感器、摄像头、PLC等,实现设备的远程监控、故障预警和生产数据的采集。
- 能源管理:在能源管理领域,MQTT协议可以用于连接各种能源设备,如太阳能、风能、储能等设备,实现能源数据的采集和设备的远程控制。
- 物流行业:在物流行业中,MQTT协议可以用于连接物流设备和系统,如仓库管理、运输车辆、物流机器人等,实现货物的实时监控和追踪以及运输信息的采集和共享。
- 医疗行业:在医疗行业中,MQTT协议可以用于连接医疗设备和系统,如医疗传感器、诊断仪器、电子病历等,实现医疗数据的采集和共享以及设备的远程控制。
2.3 身份
MQTT通信过程中,共有三种身份:
- 发布者(Publisher)
- 服务器(Broker)
- 订阅者(Subscriber)
2.4 消息质量等级
MQTT设计了3个QoS等级
- QoS 0:消息最多传递一次,如果当时客户端不可用,则会丢失该消息。
- QoS 1:消息传递至少 1 次。
- QoS 2:消息仅传送一次。
QoS 0是发布者发送完消息之后,不再关心对方有没有收到,也不设置任何重发机制。
QoS 1包含了简单的重发机制,发布者发送完消息之后会一直等待接收者的ACK,如果没有收到ACK则一直重发,这种模式保证消息至少到达一次,但无法保证消息重复。
QoS 2设计了重发和重复消息发现机制,保证消息到达对方并严格只到达了一次。
MQTT的协议报文以及更详细介绍请参考:MQTT3.1.1协议手册
2.5 遗嘱消息
客户端的遗嘱只在意外断线时才会发布,如果客户端正常的断开了与服务端的连接,这个遗嘱机制是不会启动的,服务端也不会将客户端的遗嘱公布。
遗嘱消息可以看作是一个简化版的 PUBLISH 消息,他也包含 Topic, Payload, QoS 等字段。遗嘱消息会在设备与服务端连接时,通过 CONNECT 报文指定,然后在设备意外断线时由服务端将该遗嘱消息发布到连接时指定的遗嘱主题(Will Topic)上。
一般建议设置遗嘱消息内容为client offline!在我们的客户端连接上服务器时,也向遗嘱主题发布一条client online消息。
3 硬件介绍
如果我们采用传统以太网方式接入到以太网中实现MQTT协议的应用,就需要按照特定的方式进行接线。这不仅在硬件上比较复杂,而且还需要编写软件协议栈,这不仅会增加MCU的负载,影响系统的运行速度和稳定性。还会增加研发周期和维护成本。
为此,我向大家推荐一款开发板W5500-EVB-Pico,它是一款搭载了以太网芯片的高性能、低成本的开发板。主控芯片采用的是树莓派的RP2040,搭载了双核M0架构处理器,频率最高可达133MHz,还拥有264KB高速SRAM和2MB的板载闪存以及丰富的外设资源。
其搭载的以太网芯片W5500是一款高性价比的工业级以太网芯片,拥有全球独一无二的全硬件TCP/IP协议栈专利技术。TCP/IP协议的内容都交给W5500芯片的硬件协议栈处理,减少系统CPU资源占用,提高系统运行效率和稳定性,后期维护也更加方便。在开发过程中,我们无需深究协议底层的交互以及组包过程,只需处理应用层即可。它还拥有8个独立的硬件socket,可以同时进行通信互不干扰,满足大多数应用场景的需求。无论是工业使用还是学习,W5500-EVB-Pico都是一个非常不错的选择!
此外,W5500这款以太网芯片供货稳定,久经市场考验,反馈都特别好,简单稳定,易于上手,可以帮助我们缩短开发周期,项目快速落地!
4 硬件接线
W5500芯片和RP2040芯片在开发板内部通过SPI0的GPIO16,GPIO17,GPIO18,GPIO19连接,GPIO20连接至W5500芯片的复位脚。
5 代码编写
程序的运行框图如下所示:
我们使用的是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文件中定义:
#define MQTT_SOCKET 1 /* socket used by MQTT */
/* 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};
(如需测试,仅需修改以上三个部分的内容即可)
接下来在主函数中,我们只需要根据运行框图编写代码即可。
1.W5500芯片初始化,SPI初始化和链路检测
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;
2.MQTT连接初始化
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 = MQTT_CONN;
/* mqtt_init() function */
/* 如不需遗嘱主题,将第12行data.willFlag = 0。并删除13-21行即可。
void mqtt_init(void)
{
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
}
3.MQTT连接
ret = MQTTConnect(&c, &data); /* Connect to the MQTT server */
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 == SUCCESSS ? "success" : "failed");
if (ret != SUCCESSS)
{
mqtt_run_status = ERROR;
}
else
{
mqtt_run_status = MQTT_SUB;
}
4.MQTT订阅主题
/* 第1个参数为连接的客户端,第2个参数为订阅主题,第3个参数为订阅主题QoS,第4个参数为订阅接收消息回调函数 */
ret = MQTTSubscribe(&c, mqtt_params.subtopic, mqtt_params.subQoS, messageArrived); /* Subscribe to Topics */
printf("Subscribing to %s\r\n", mqtt_params.subtopic);
printf("Subscribed:%s\r\n\r\n", ret == SUCCESSS ? "success" : "failed");
if (ret != SUCCESSS)
{
mqtt_run_status = ERROR;
}
else
{
mqtt_run_status = MQTT_PUB_ONLINE;
}
/* 接收回调函数如下 */
/* 回环处理,该函数接收到订阅主题的消息后,再发布一条消息出去 */
void messageArrived(MessageData *md)
{
int ret;
char topicname[64] = {0};
char msg[512] = {0};
MQTTMessage pubmessage = {
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);
/* Message loopback processing */
pubmessage.qos = mqtt_params.pubQoS;
pubmessage.payload = msg;
pubmessage.payloadlen = strlen(pubmessage.payload);
ret = MQTTPublish(&c, mqtt_params.pubtopic, &pubmessage);
if (ret != SUCCESSS)
{
mqtt_run_status = ERROR;
}
else
{
printf("publish:%s,%s\r\n\r\n", mqtt_params.pubtopic, msg);
}
}
5.MQTT发布上线消息
pubmessage.qos = 0;
pubmessage.payload = "W5500 online!";
pubmessage.payloadlen = strlen(pubmessage.payload);
ret = MQTTPublish(&c, mqtt_params.willtopic, &pubmessage); /* Publish message */
if (ret != SUCCESSS)
{
mqtt_run_status = ERROR;
}
else
{
printf("publish:%s,%s\r\n\r\n", mqtt_params.willtopic, pubmessage.payload);
mqtt_run_status = MQTT_KEEPALIVE;
}
6.MQTT心跳处理
if (MQTTYield(&c, 30) != SUCCESSS) /* keepalive MQTT */
{
mqtt_run_status = ERROR;
}
sleep_ms(100);
6 最终现象
在MQTTX工具上,我们新建连接,服务器和端口号以及MQTT版本都设置成与开发板一致即可。并添加一个订阅W5500_pub(即开发板发布的主题)和W5500_will(即开发板遗嘱主题)。
我们按住RUN运行按钮然后用USB先连接到电脑,此时开发板会虚拟成U盘,我们只需要把编译好的文件复制进U盘中即可。
在网线没有连接至开发板时,USB会一直提示网络接口未连接。
在接入网线之后,会打印网络地址信息以及连接订阅状态,并向遗嘱主题发布一条客户端上线消息。
此时MQTTX工具上便可收到来自开发板上线的消息。
我们在对话框下面将MQTTX的发布主题改为W5500_sub(即开发板的订阅主题),并发布一条消息。
开发板上也是同样的,将接收到的消息以及发布的消息通过USB打印出来。
最后,我们将开发板上的网线断开,服务器发现开发板没有定时发送心跳包,认为异常断开,会向遗嘱主题发送开发板掉线消息。
7 总结
至此,我们通过简单配置开发板之后实现了连接MQTT服务器,并且发布了一条消息给其他客户端,也能接收到来自订阅的消息。在我们的使用过程中,可以直接将例程中的初始化以及发布,订阅函数进行移植,并根据自己的业务需求进行修改即可。总而言之,硬件集成了TCP/IP协议栈的W5500芯片可以帮助我们在开发时无需太过关注协议的底层及组包过程,只需要按照官方提供的库进行传入我们的参数即可,这帮助我们的项目快速落地。对初次接触以太网模块的小伙伴们也比较友好,会更容易上手。
8 项目链接
- 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
- MQTT1.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