十、W5100S/W5500+RP2040之MicroPython开发<MQTT示例>

2023-12-22 09:02:06

1. 前言

??在这个智能硬件和物联网时代,MicroPython和树莓派PICO正以其独特的优势引领着嵌入式开发的新潮流。MicroPython作为一种精简优化的Python 3语言,为微控制器和嵌入式设备提供了高效开发和简易调试的

??当我们结合WIZnet W5100S/W5500网络模块,MicroPython和树莓派PICO的开发潜力被进一步放大。这两款模块都内置了TCP/IP协议栈,使得在嵌入式设备上实现网络连接变得更加容易。无论是进行数据传输、远程控制,还是构建物联网应用,它们都提供了强大的支持。

??本章我们将以WIZnet W5100S为例,以MicroPython的开发方式进行MQTT回环通信示例

2. 相关网络信息

2.1 简介

??MQTT是一种基于标准的消息传输协议,或者说是一套用于机器间通信的规则。智能传感器、可穿戴设备以及其他物联网(IoT)设备通常需要在资源受限、带宽有限的网络上进行数据的发送和接收。这些IoT设备使用MQTT进行数据传输,因为它易于实现并且可以高效地传输IoT数据。MQTT支持设备到云以及云到设备之间的消息传输。

在这里插入图片描述

2.2 工作原理

MQTT的通信包括以下四个步骤,其中订阅和发布步骤可能有多次:

1.客户端连接到MQTT服务器:客户端通过TCP/IP协议与MQTT服务器建立连接。在连接过程中,客户端需要向服务器提供客户端标识(ClientId),以及用户名和密码等信息。如果连接成功,客户端与服务器将建立会话(Session),并开始进行数据传输。

2.订阅主题:在建立连接后,客户端可以订阅感兴趣的主题,以便接收发布到这些主题上的消息。订阅操作可以通过MQTT协议中的SUBSCRIBE报文来实现,报文中需要指定要订阅的主题和QoS(质量服务)等级。服务器将返回SUBACK报文,表示订阅成功或失败。

3.发布消息:在建立连接后,客户端可以向指定的主题发布消息。发布操作可以通过MQTT协议中的PUBLISH报文来实现,报文中需要指定要发布的主题和消息内容。服务器将返回PUBACK报文,表示消息发布成功或失败。

4.断开连接:客户端可以主动断开与MQTT服务器的连接。在断开连接之前,客户端需要向服务器发送DISCONNECT报文。服务器将返回DISCONNECT报文,表示连接断开成功。

在这里插入图片描述

2.3 优点

  • 轻量级和高效:MQTT在IoT设备上的实现需要最小的资源,因此甚至可以在小型微控制器上使用。例如,最小的MQTT控制消息可以只有两个数据字节。MQTT消息头也很小,可以优化网络带宽。
  • 可扩展:MQTT实现需要最小的代码量,操作中消耗的电力非常少。该协议还具有内置功能,支持与大量IoT设备的通信。因此,你可以实现MQTT协议,与数百万这样的设备连接。
  • 可靠:许多IoT设备通过低带宽、高延迟的不可靠蜂窝网络连接。MQTT具有内置功能,可以减少IoT设备重新连接到云的时间。它还定义了三种不同的服务质量等级,以确保IoT用例的可靠性——最多一次(0),至少一次(1),恰好一次(2)。
  • 安全:MQTT使开发人员可以使用现代认证协议(如OAuth、TLS1.3、客户管理证书等)轻松加密消息并验证设备和用户。
  • 支持良好:Python等多种语言都对MQTT协议实现提供了广泛的支持。因此,开发人员可以在任何类型的应用中快速实现它,只需最少的编码。
  • 发布-订阅模式:可以轻松实现一对多通信。

在这里插入图片描述

2.4 应用

  • 物联网M2M通信,物联网大数据采集:MQTT协议可以实现设备间的高效通信和大规模数据的实时采集。
  • Android消息推送,WEB消息推送:MQTT协议可以用于实现Android和Web平台的实时消息推送。
  • 智能硬件、智能家具、智能电器:MQTT协议可以用于智能硬件、智能家具、智能电器等设备的控制和管理。
  • 车联网通信,电动车站桩采集:在车联网领域,MQTT协议可以用于车辆和基础设施之间的通信,以及电动车充电站的数据采集。
  • 智慧城市、远程医疗、远程教育:在智慧城市建设、远程医疗和远程教育等领域,MQTT协议可以实现设备间的高效通信和大规模数据的实时采集。
  • 电力、石油与能源等行业市场:在电力、石油和能源等行业,MQTT协议可以用于设备的远程监控和数据采集。

总的来说,MQTT协议因其轻量、简单、开放和易于实现的特点,使其在物联网领域有着广泛的应用。

在这里插入图片描述

3. WIZnet以太网芯片

WIZnet 主流硬件协议栈以太网芯片参数对比

ModelEmbedded CoreHost I/FTX/RX BufferHW SocketNetwork Performance
W5100STCP/IPv4, MAC & PHY8bit BUS, SPI16KB4Max 25Mbps
W6100TCP/IPv4/IPv6, MAC & PHY8bit BUS, Fast SPI32KB8Max 25Mbps
W5500TCP/IPv4, MAC & PHYFast SPI32KB8Max 15Mbps
  • W5100S/W6100 支持 8bit数据总线接口,网络传输速度会优于W5500。
  • W6100 支持IPV6,与W5100S 硬件兼容,若已使用W5100S的用户需要支持IPv6,可以Pin to Pin兼容。
  • W5500 拥有比 W5100S更多的 Socket数量以及发送与接收缓存

相较于软件协议栈,WIZnet的硬件协议栈以太网芯片有以下优点

  1. 硬件TCP/IP协议栈:WIZnet的硬件协议栈芯片提供了一种硬件实现的TCP/IP协议栈,这种硬件实现的协议栈比软件实现的协议栈具有更好的性能和稳定性。
  2. 不需要额外的嵌入式系统软件栈和内存资源:由于所有的以太网传输和接收操作都由独立的以太网控制器处理,因此不需要额外的嵌入式系统软件栈和内存资源。
  3. 抵抗网络环境变化和DDoS攻击:与易受网络环境变化和DDoS攻击影响的软件TCP/IP协议栈相比,硬件协议栈芯片能够提供更稳定的以太网性能。
  4. 适用于低规格的嵌入式系统:即使在低规格的嵌入式系统中,使用WIZnet的硬件协议栈芯片也可以比使用软件TCP/IP协议栈的高规格系统显示出更高效的互联网应用操作性能。

在这里插入图片描述

4. Modbus TCP通信示例讲解以及使用

4.1 程序流程图

在这里插入图片描述

4.2 测试准备

软件:

  • Thonny
  • MQTTX

硬件:

  • W5100S IO模块 + RP2040 树莓派Pico开发板 或者 WIZnet W5100S-EVB-Pico开发板
  • Micro USB 接口的数据线
  • 网线

4.3 连接方式

  • 通过数据线连接PC的USB口
  • 当使用W5100S/W5500 IO模块连接RP2040时
    • RP2040 GPIO 16 <----> W5100S/W5500 MISO
    • RP2040 GPIO 17 <----> W5100S/W5500 CS
    • RP2040 GPIO 18 <----> W5100S/W5500 SCK
    • RP2040 GPIO 19 <----> W5100S/W5500 MOSI
    • RP2040 GPIO 20 <----> W5100S/W5500 RST
  • 通过网线直接连接PC网口(或:PC和设备都通过网线连接交换机或路由器LAN口)

4.4 相关代码

??我们直接打开mqtt.py文件。

第一步:可以看到在w5x00_init()函数中,进行了SPI的初始化。以及将spi相关引脚和复位引脚注册到库中,后续则是激活网络,并使用DHCP配置网络地址信息,当DHCP失败时,则配置静态网络地址信息。当未配置成功时,会打印出网络地址相关寄存器的信息,可以帮助我们更好的排查问题。

第二步:尝试连接MQTT服务器,如果连接失败则进入复位程序。

第三步:订阅主题,绑定消息回调函数,并开启定时器定时进行保活。

第四步:等待接收消息,如果接收到了消息则进行回环处理。

此外,还需将umqttsimple.py库保存到开发板中,否则会导致运行出错。

#mqtt.py file
from umqttsimple import MQTTClient
from usocket import socket
from machine import Pin,SPI,Timer
import network
import time
import json

#mqtt config
mqtt_params = {}
mqtt_params['url'] = 'test.mosquitto.org'
mqtt_params['port'] = 1883
mqtt_params['clientid'] = 'W5100S'
mqtt_params['pubtopic'] = '/W5100S/pub'
mqtt_params['subtopic'] = '/W5100S/sub'
mqtt_params['pubqos'] = 0
mqtt_params['subqos'] = 0

timer_1s_count =  0
tim = Timer()
client = None

"""
W5x00 chip initialization.
 
param: None
returns: None

"""
def w5x00_init():
    spi=SPI(0,2_000_000, mosi=Pin(19),miso=Pin(16),sck=Pin(18))
    nic = network.WIZNET5K(spi,Pin(17),Pin(20)) #spi,cs,reset pin
    nic.active(True)
    
    try:
        #DHCP
        print("\r\nConfiguring DHCP")
        nic.ifconfig('dhcp')
    except:
        #None DHCP
        print("\r\nDHCP fails, use static configuration")
        nic.ifconfig(('192.168.1.20','255.255.255.0','192.168.1.1','8.8.8.8'))#Set static network address information
    
    #Print network address information
    print("IP         :",nic.ifconfig()[0])
    print("Subnet Mask:",nic.ifconfig()[1])
    print("Gateway    :",nic.ifconfig()[2])
    print("DNS        :",nic.ifconfig()[3],"\r\n")
    
    #If there is no network connection, the register address information is printed
    while not nic.isconnected():
        time.sleep(1)
        print(nic.regs())

"""
Subscribe to the topic message callback function. This function is entered when a message is received from a subscribed topic.
 
param1: The topic on which the callback is triggered
param2: Message content
returns: None

"""
def sub_cb(topic, msg):
    topic = topic.decode('utf-8')
    msg = msg.decode('utf-8')
    if topic == mqtt_params['subtopic']:
        global client
        print("\r\ntopic:",topic,"\r\nrecv:", msg)
        client.publish(mqtt_params['pubtopic'],msg,qos = mqtt_params['pubqos'])
        print('\r\ntopic:',mqtt_params['pubtopic'],'\r\nsend:',msg)
    
"""
Connect to the MQTT server.
 
param: None
returns: None

"""
def mqtt_connect():
    client = MQTTClient(mqtt_params['clientid'], mqtt_params['url'], mqtt_params['port'],keepalive=60)
    
    client.connect()
    print('Connected to %s MQTT Broker'%(mqtt_params['url']))
    return client

"""
Connection error handler.
 
param: None
returns: None

"""
def reconnect():
    print('Failed to connected to Broker. Reconnecting...')
    time.sleep(5)
    machine.reset()

"""
1-second timer callback function.
 
param1: class timer
returns: None

"""
def tick(timer):
    global timer_1s_count
    global client
    timer_1s_count += 1
    if timer_1s_count >= 30:
           timer_1s_count = 0 
           client.ping()

"""
Subscribe to Topics.
 
param: client object
returns: None

"""
def subscribe(client):
    client.set_callback(sub_cb)
    client.subscribe(mqtt_params['subtopic'],mqtt_params['subqos'])
    print('subscribed to %s'%mqtt_params['subtopic'])
 
    
def main():
    global client
    print("WIZnet chip MQTT example")
    w5x00_init()
    
    try: 
        client = mqtt_connect()
    except OSError as e:
        reconnect()
        
    subscribe(client)
    
    tim.init(freq=1, callback=tick)
    
    while True:
        client.wait_msg()
        
        
    client.disconnect()

if __name__ == "__main__":
    main()
#umqttsimple.py file
import usocket as socket
import ustruct as struct
from ubinascii import hexlify


class MQTTException(Exception):
    pass


class MQTTClient:
    def __init__(
        self,
        client_id,
        server,
        port=0,
        user=None,
        password=None,
        keepalive=0,
        ssl=False,
        ssl_params={},
    ):
        if port == 0:
            port = 8883 if ssl else 1883
        self.client_id = client_id
        self.sock = None
        self.server = server
        self.port = port
        self.ssl = ssl
        self.ssl_params = ssl_params
        self.pid = 0
        self.cb = None
        self.user = user
        self.pswd = password
        self.keepalive = keepalive
        self.lw_topic = None
        self.lw_msg = None
        self.lw_qos = 0
        self.lw_retain = False

    def _send_str(self, s):
        self.sock.write(struct.pack("!H", len(s)))
        self.sock.write(s)

    def _recv_len(self):
        n = 0
        sh = 0
        while 1:
            b = self.sock.read(1)[0]
            n |= (b & 0x7F) << sh
            if not b & 0x80:
                return n
            sh += 7

    def set_callback(self, f):
        self.cb = f

    def set_last_will(self, topic, msg, retain=False, qos=0):
        assert 0 <= qos <= 2
        assert topic
        self.lw_topic = topic
        self.lw_msg = msg
        self.lw_qos = qos
        self.lw_retain = retain

    def connect(self, clean_session=True):
        self.sock = socket.socket()
        addr = socket.getaddrinfo(self.server, self.port)[0][-1]
        self.sock.connect(addr)
        if self.ssl:
            import ussl

            self.sock = ussl.wrap_socket(self.sock, **self.ssl_params)
        premsg = bytearray(b"\x10\0\0\0\0\0")
        msg = bytearray(b"\x04MQTT\x04\x02\0\0")

        sz = 10 + 2 + len(self.client_id)
        msg[6] = clean_session << 1
        if self.user is not None:
            sz += 2 + len(self.user) + 2 + len(self.pswd)
            msg[6] |= 0xC0
        if self.keepalive:
            assert self.keepalive < 65536
            msg[7] |= self.keepalive >> 8
            msg[8] |= self.keepalive & 0x00FF
        if self.lw_topic:
            sz += 2 + len(self.lw_topic) + 2 + len(self.lw_msg)
            msg[6] |= 0x4 | (self.lw_qos & 0x1) << 3 | (self.lw_qos & 0x2) << 3
            msg[6] |= self.lw_retain << 5

        i = 1
        while sz > 0x7F:
            premsg[i] = (sz & 0x7F) | 0x80
            sz >>= 7
            i += 1
        premsg[i] = sz

        self.sock.write(premsg, i + 2)
        self.sock.write(msg)
        # print(hex(len(msg)), hexlify(msg, ":"))
        self._send_str(self.client_id)
        if self.lw_topic:
            self._send_str(self.lw_topic)
            self._send_str(self.lw_msg)
        if self.user is not None:
            self._send_str(self.user)
            self._send_str(self.pswd)
        resp = self.sock.read(4)
        assert resp[0] == 0x20 and resp[1] == 0x02
        if resp[3] != 0:
            raise MQTTException(resp[3])
        return resp[2] & 1

    def disconnect(self):
        self.sock.write(b"\xe0\0")
        self.sock.close()

    def ping(self):
        self.sock.write(b"\xc0\0")

    def publish(self, topic, msg, retain=False, qos=0):
        pkt = bytearray(b"\x30\0\0\0")
        pkt[0] |= qos << 1 | retain
        sz = 2 + len(topic) + len(msg)
        if qos > 0:
            sz += 2
        assert sz < 2097152
        i = 1
        while sz > 0x7F:
            pkt[i] = (sz & 0x7F) | 0x80
            sz >>= 7
            i += 1
        pkt[i] = sz
        # print(hex(len(pkt)), hexlify(pkt, ":"))
        self.sock.write(pkt, i + 1)
        self._send_str(topic)
        if qos > 0:
            self.pid += 1
            pid = self.pid
            struct.pack_into("!H", pkt, 0, pid)
            self.sock.write(pkt, 2)
        self.sock.write(msg)
        if qos == 1:
            while 1:
                op = self.wait_msg()
                if op == 0x40:
                    sz = self.sock.read(1)
                    assert sz == b"\x02"
                    rcv_pid = self.sock.read(2)
                    rcv_pid = rcv_pid[0] << 8 | rcv_pid[1]
                    if pid == rcv_pid:
                        return
        elif qos == 2:
            assert 0

    def subscribe(self, topic, qos=0):
        assert self.cb is not None, "Subscribe callback is not set"
        pkt = bytearray(b"\x82\0\0\0")
        self.pid += 1
        struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self.pid)
        # print(hex(len(pkt)), hexlify(pkt, ":"))
        self.sock.write(pkt)
        self._send_str(topic)
        self.sock.write(qos.to_bytes(1, "little"))
        while 1:
            op = self.wait_msg()
            if op == 0x90:
                resp = self.sock.read(4)
                # print(resp)
                assert resp[1] == pkt[2] and resp[2] == pkt[3]
                if resp[3] == 0x80:
                    raise MQTTException(resp[3])
                return

    # Wait for a single incoming MQTT message and process it.
    # Subscribed messages are delivered to a callback previously
    # set by .set_callback() method. Other (internal) MQTT
    # messages processed internally.
    def wait_msg(self):
        res = self.sock.read(1)
#         self.sock.setblocking(True)
        if res is None:
            return None
        if res == b"":
            raise OSError(-1)
        if res == b"\xd0":  # PINGRESP
            sz = self.sock.read(1)[0]
            assert sz == 0
            return None
        op = res[0]
        if op & 0xF0 != 0x30:
            return op
        sz = self._recv_len()
        topic_len = self.sock.read(2)
        topic_len = (topic_len[0] << 8) | topic_len[1]
        topic = self.sock.read(topic_len)
        sz -= topic_len + 2
        if op & 6:
            pid = self.sock.read(2)
            pid = pid[0] << 8 | pid[1]
            sz -= 2
        msg = self.sock.read(sz)
        self.cb(topic, msg)
        if op & 6 == 2:
            pkt = bytearray(b"\x40\x02\0\0")
            struct.pack_into("!H", pkt, 2, pid)
            self.sock.write(pkt)
        elif op & 6 == 4:
            assert 0
        return op

    # Checks whether a pending message from server is available.
    # If not, returns immediately with None. Otherwise, does
    # the same processing as wait_msg.
    def check_msg(self):
#         self.sock.setblocking(False)
        return self.wait_msg()

4.5 烧录验证

要测试以太网示例,必须将开发环境配置为使用Raspberry Pi Pico。

  • 所需的开发环境
  • 如果你必须编译MicroPython,则必须使用Linux或Unix环境。

第一步:将程序复制到Thonny中,然后选择环境为Raspberry Pi Pico。

第二步:将umqttsimple.py库文件保存到开发板中。

在这里插入图片描述

第三步:运行程序,并打开MQTTX,连接相同的服务器,然后订阅主题为开发板的发布主题,发布主题为开发板的订阅主题。

第四步:在MQTTX发送消息观察回环测试效果。

注意:因为MicroPython的print函数是启用了stdout缓冲的,所以有时候并不会第一时间打印出内容。

在这里插入图片描述

5. 注意事项

  • 如果采用的是WIZnet的W5500来实现本章的示例,则只需烧录W5500的固件并运行示例程序即可。

6. 相关链接

WIZnet官网

本章例程链接

想了解更多,评论留言哦!

文章来源:https://blog.csdn.net/WIZnet2012/article/details/135085014
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。