目录
1 前言
2 什么是MQTT协议?
2.1 特点
2.2 应用
2.3 身份
2.4 消息质量等级
2.5 遗嘱消息
3 硬件介绍
4 硬件接线
5 代码编写
6 移植说明
7 最终现象
8 总结
9 项目链接
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 特点
- 开放消息协议,简单易实现
- 发布订阅模式,一对多消息发布
- 基于 TCP/IP 网络连接
- 1 字节固定报头,2 字节心跳报文,报文结构紧凑
- 消息 QoS 支持,可靠传输保证
2.2 应用
MQTT 协议广泛应用于物联网、移动互联网、智能硬件、车联网、电力能源等领域。
- 物联网 M2M 通信,物联网大数据采集
- Android 消息推送,WEB 消息推送
- 移动即时消息,例如 Facebook Messenger
- 智能硬件、智能家具、智能电器
- 车联网通信,电动车站桩采集
- 智慧城市、远程医疗、远程教育
- 电力、石油与能源等行业市场
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设计了重发和重复消息发现机制,保证消息到达对方并严格只到达了一次。
2.5 遗嘱消息
客户端的遗嘱只在意外断线时才会发布,如果客户端正常的断开了与服务端的连接,这个遗嘱机制是不会启动的,服务端也不会将客户端的遗嘱公布。
遗嘱消息可以看作是一个简化版的 PUBLISH 消息,他也包含 Topic, Payload, QoS 等字段。遗嘱消息会在设备与服务端连接时,通过 CONNECT 报文指定,然后在设备意外断线时由服务端将该遗嘱消息发布到连接时指定的遗嘱主题(Will Topic)上。
一般建议设置遗嘱消息内容为client offline!在我们的客户端连接上服务器时,也向遗嘱主题发布一条client online消息。这样遗嘱主题可以告诉我们客户端的一个在线状态。这也是在我们嵌入式应用中为什么喜欢使用MQTT协议通信的原因之一。
3 硬件介绍
如果采用传统以太网方式我们接入到以太网中实现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这款以太网芯片,供货稳定,久经市场考验,反馈都特别不错,简单稳定,易于上手,可以帮助我们缩短开发周期,项目快速落地!
4 硬件接线
5 代码编写
程序的运行框图如下所示:
我们使用的是WIZnet官方的ioLibrary_Driver库。该库支持的协议丰富,操作简单,芯片在硬件上集成了TCP/IP协议栈,该库又封装好了TCP/IP层之上的协议,我们只需简单调用相应函数即可完成协议的应用。
该例程实现了连接MQTT,进行发布消息,以及监听我们订阅的主题下发的消息,通过USB的方式打印出来。
首先进行spi初始化,然后是链路状态检测
/* Pin definition */
#define PIN_SCK 18
#define PIN_MOSI 19
#define PIN_MISO 16
#define PIN_CS 17
#define PIN_RST 20
/* W5500 chip is selected */
static inline void wizchip_select(void)
{
gpio_put(PIN_CS, 0);
}
/* Cancel chip selection W5500 chip */
static inline void wizchip_deselect(void)
{
gpio_put(PIN_CS, 1);
}
/* reset W5500 chip */
void wizchip_reset(void)
{
gpio_set_dir(PIN_RST, GPIO_OUT);
gpio_put(PIN_RST, 0);
sleep_ms(100);
gpio_put(PIN_RST, 1);
sleep_ms(100);
bi_decl(bi_1pin_with_name(PIN_RST, "W5500 RESET"));
}
/* SPI reads the W5500 chip */
static uint8_t wizchip_read(void)
{
uint8_t rx_data = 0;
uint8_t tx_data = 0xFF;
spi_read_blocking(SPI_PORT, tx_data, &rx_data, 1);
return rx_data;
}
/* SPI writes the W5500 chip */
static void wizchip_write(uint8_t tx_data)
{
spi_write_blocking(SPI_PORT, &tx_data, 1);
}
static void wizchip_spi_initialize(void)
{
/* The SPI is initialized with a rate of 5MHz */
spi_init(SPI_PORT, 5000 * 1000);
/* Set the pins to SPI mode */
gpio_set_function(PIN_SCK, GPIO_FUNC_SPI);
gpio_set_function(PIN_MOSI, GPIO_FUNC_SPI);
gpio_set_function(PIN_MISO, GPIO_FUNC_SPI);
/* Initialize the CS pin to the output mode */
gpio_init(PIN_CS);
gpio_set_dir(PIN_CS, GPIO_OUT);
gpio_put(PIN_CS, 1);
}
void wizchip_initialize(void)
{
uint8_t temp;
wizchip_spi_initialize();
/* Deselect the chip: chip select high */
wizchip_deselect();
/* CS function register */
reg_wizchip_cs_cbfunc(wizchip_select, wizchip_deselect);
/* SPI function register */
reg_wizchip_spi_cbfunc(wizchip_read, wizchip_write);
wizchip_reset();
/* Read version register */
if (getVERSIONR() != 0x04)
{
printf("ACCESS ERR : VERSION != 0x04, read value = 0x%02x\n", getVERSIONR());
while (1)
{
sleep_ms(100);
}
}
/* Check PHY link status */
do
{
if (ctlwizchip(CW_GET_PHYLINK, (void *)&temp) == -1)
{
printf(" Unknown PHY link status\n");
return;
}
if (temp == PHY_LINK_ON)
{
printf("PHY link\n");
}
if (temp == PHY_LINK_OFF)
{
printf("PHY is not connected\r\n");
sleep_ms(1000);
}
} while (temp == PHY_LINK_OFF);
}
我们定义网络地址以及MQTT参数如下:
#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};
/* 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连接函数
void mqtt_conn(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
/* Connect MQTT, the maximum number of connections does not exceed the set value */
for (int i = 0; i < conn_max_err; i++)
{
connOK = MQTTConnect(&c, &data);
printf("Connected:%s\r\n", connOK == 0 ? "success" : "failed");
if (!connOK)
{
break;
}
sleep_ms(1000);
}
if (connOK)
{
while (1)
;
}
}
编写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 */
for (int i = 0; i < sub_max_err; i++)
{
subOK = MQTTSubscribe(&c, subtopic, QoS, messageHandler);
printf("Subscribing to %s\r\n", subtopic);
printf("Subscribed:%s\r\n", subOK == 0 ? "success" : "failed");
if (!subOK)
{
break;
}
sleep_ms(1000);
}
if (subOK)
{
while (1)
;
}
}
再编写我们的订阅主题的消息回调函数
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)
{
MQTTMessage pubmessage = {
.qos = QoS,
.retained = 0,
.dup = 0,
.id = 0,
};
pubmessage.payload = msg;
pubmessage.payloadlen = strlen(pubmessage.payload);
MQTTPublish(&c, pubtopic, &pubmessage);
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;
}
最后我们在主函数中初始化后依次调用即可
int main()
{
struct repeating_timer timer;
stdio_init_all();
sleep_ms(3000);
printf("W5500 mqtt example.\r\n");
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 */
add_repeating_timer_ms(1, repeating_timer_callback, NULL, &timer); /* Turns on a 1-millisecond timer */
mqtt_conn(); /* Connect to the MQTT server */
mqtt_sub(mqtt_params.subtopic, mqtt_params.subQoS, messageArrived); /* Subscribe to Topics */
mqtt_sendmsg(mqtt_params.willtopic, mqtt_params.willQoS, "W5500 online!"); /* Release the online news */
while (true)
{
MQTTYield(&c, 30); /* keepalive MQTT */
sleep_ms(100);
}
}
6 移植说明
如果你想在你的开发板上加入W5500芯片实现以太网功能,则你可以购买一个W5500IO模块。基于例程进行移植。步骤如下
- 将工程中的ioLibrary_Driver文件夹移植到你的工程下。
- 将port文件夹下的w5500_spi.c以及w5500_spi.h文件进行移植并对应修改
- 将mqtt_client.c文件移植到你的项目中,并对应修改
如需移植教程请在评论区留言:移植教程;后续我会根据反馈情况发布一个移植教程。
7 最终现象
我们按住RUN运行按钮然后用USB先连接到电脑,此时开发板会虚拟成U盘,我们只需要把编译好的文件复制进U盘中即可。
在网线没有连接至开发板时,USB会一直提示网络接口未连接。
在接入网线之后,会打印网络地址信息以及连接订阅状态,并向遗嘱主题发布一条客户端上线消息。
此时MQTTX工具上便可收到来自开发板上线的消息。
我们在对话框下面将MQTTX的发布主题改为W5500_sub(即开发板的订阅主题),并发布一条消息。
开发板上也是同样的,将接收到的消息以及发布的消息通过USB打印出来。
最后,我们将开发板上的网线断开,服务器发现开发板没有定时发送心跳包,认为异常断开,会向遗嘱主题发送开发板掉线消息。
8 总结
至此,我们通过简单配置开发板之后实现了连接MQTT服务器,并且发布了一条消息给其他客户端,也能接收到来自订阅的消息。在我们的使用过程中,可以直接将例程中的初始化以及发布,订阅函数进行移植,并根据自己的业务需求进行修改即可。总而言之,硬件集成了TCP/IP协议栈的W5500芯片可以帮助我们在开发时无需太过关注协议的底层及组包过程,只需要按照官方提供的库进行传入我们的参数即可,这帮助我们的项目快速落地。对初次接触以太网模块的小伙伴们也比较友好,会更容易上手。
9 项目链接
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