MicroPython 科普:流设备 vs 块设备,理解串口等 I/O 设备的底层差异

1.select 模块

考虑一下以下情况:我们正在制作一个无线运动传感器,可以检测运动数据、检测温湿度数据、蓝牙将数据传输到手机上。树莓派 Pico 上一个串口连接了陀螺仪传感器、另一个串口连接了温湿度传感器。

此时,陀螺仪传感器和温湿度传感器会定期给树莓派 Pico 传送数据,我们要在有数据可以读取时读取数据,而在没有数据读取时进行其他操作(例如将接收到的传感器数据发送给蓝牙芯片)。要实现该功能,我们可以在 while 循环中不断使用两个 UART.any()方法不断进行判断,在任意一个串口有可以接收的数据时开启接收,在接收到数据后才进行其他操作。此时,相当于程序一直卡到了等待接收串口数据上,若是一直未接收到数据(例如由于硬件电路故障导致外部温湿度传感器损坏,无法发送数据)则程序会阻塞。这个问题可以通过使用多线程运行数据接收任务、数据发送任务和其他任务、使用协程、使用定时器开启多个定时执行的任务来解决,但是使用 select 模块来完成监测串口状态变化并且在有数据可以接收时进行对应操作无疑是最简洁的方法,select 模块提供了一种有效的方式来同时监控多个IO对象的状态,并在有数据可读或可写时进行相应的处理。

同时想象以下更复杂的情况,该设备还要求使用 socket 进行网络通信完成数据收发,此时使用其他方法监测两个串口和 socket 端口实现十分麻烦且不简洁。

select 模块是 Python 标准库中的一个模块,它提供了 ​I/O多路复用的基本功能。 在 Python 中,I/O 多路复用是指能够同时监控多个文件描述符(例如套接字)的状态变化(如可读、可写等),并 在任何一个文件描述符就绪时执行相应的操作。这种技术可以大大提高程序在处理大量并发连接时的性能和效率。使用 select 模块可以避免线程或进程的开销,因为它可以在单个线程中处理多个 I/O 事件。这对于高并发的网络服务器来说非常重要,因为线程或进程的创建和销毁都需要消耗大量的资源。

在 MicroPython 中,select 模块具有以下方法:

简单来说,我们需要先使用 select.poll()创建 Poll 类的实例,即用于监测以字节流方式读写数据设备的轮询对象,然后使用 Poll.register 方法将需要监测的设备注册到轮询对象中。接着,我们使用 Poll.poll 方法来监听事件,Poll.poll 方法会阻塞等待,直到有一个或多个设备准备好进行 I/O 操作(如读或写)。当有事件发生时,Poll.poll 方法会返回一个包含事件对象的列表。每个事件对象都有一个 fd 属性,表示发生事件的设备,以及一个 events 属性,表示该设备的事件类型。我们可以通过读取 fd 属性和 events 属性判断事件的类型和设备,执行对应操作。

2.使用 select 模块监测串口事件

在下面实验中,我们需要将通过 MiniUSB 数据线将 风雅一号板-通用兼容扩展板UART0USB 接口与电脑上的 USB 接口连接:

在串口通信中,使用 select 模块来监控串口的活动状态的工作流程如下:

  1. 创建串口对象​: 使用 machine.UART 创建串口对象,并配置波特率、数据位、校验位等参数。
  2. 创建 poll​​对象​: 使用 select.poll() 创建一个 poll 对象,用于轮询和注册串口事件。
  3. 注册串口设备​: 将串口设备注册到 poll 对象中,监控 POLLIN 事件,表示当串口有数据可读时,轮询机制会被触发。
  4. 轮询串口事件​: 使用 poll.poll() 进行轮询,等待串口设备的 I/O 事件。轮询方法会阻塞,直到串口有数据可读,或者超时后返回。
  5. 处理串口数据​: 一旦 poll() 返回事件,程序就可以通过 uart.read() 读取串口数据进行处理。

在以下代码中,我们通过 select 模块进行串口事件的监听和数据读取,处理串口错误、设备断开等事件,并将接收到的数据存储到缓冲区中,代码还会在缓冲区数据满时进行清空操作,确保不会出现数据溢出的情况。

示例代码如下,在配套资料的文件夹:elegance-devkit v1\Demo\10 UART_SelectRead 中:

# Python env   : MicroPython v1.23.0               
# -*- coding: utf-8 -*-        
# @Time    : 2024/6/27 上午12:08   
# @Author  : 李清水            
# @File    : main.py       
# @Description : Select类实验,主要完成使用select模块进行串口状态读取和数据接收等操作

# ======================================== 导入相关模块 ========================================
# 硬件相关的模块
from machine import UART, Pin
# 时间相关的模块
import time
# 导入select模块
import select

# ======================================== 全局变量 ============================================

# 创建一个串口数据缓冲区
uart_buffer = []
# 串口数据缓冲区最大长度
uart_buffer_max_len = 100

# ======================================== 功能函数 ============================================

def handle_uart_error(event: int) -> None:
    """
​    处理串口错误事件。

​    当串口发生错误时,调用该方法进行错误处理。它会尝试重置串口连接,
​    并重新初始化串口设置以确保通信恢复正常。

​    Args:
​        event (int): 事件类型标识。只有当事件类型为 `POLLERR` 时才会调用该函数。
​    """
​    ​if event & select.POLLERR:
        print("UART error occurred, resetting UART")
        # 错误时熄灭LED指示灯
        LED.off()
        # 可能的恢复操作,关闭串口然后重新初始化
        uart.deinit()
        # 等待1秒后重新初始化
        time.sleep(1)
        uart.init(bits=8, parity=None, stop=1, tx=0, rx=1, timeout=5)
        # 重新亮起LED,表示恢复工作
        LED.on()

def handle_uart_disconnect(event: int) -> None:
    """
​    处理串口断开事件。

​    当串口连接断开时,调用该方法进行断开处理。它会停止串口操作并熄灭LED指示灯,
​    同时准备好重新连接串口。

​    Args:
​        event (int): 事件类型标识。只有当事件类型为 `POLLHUP` 时才会调用该函数。
​    """
​    ​if event & select.POLLHUP:
        print("UART disconnected, resetting UART")
        # 断开时熄灭LED
        LED.off()
        # 停止串口操作
        uart.deinit()
        # 等待1秒,准备重新连接
        time.sleep(1)

# ======================================== 自定义类 ============================================

# ======================================== 初始化配置 ==========================================

# 上电延时3s
time.sleep(3)
# 打印调试消息
print("FreakStudio: Using select module to read UART data")

# 创建串口对象,设置波特率为9600
uart: UART = UART(0, 9600)
# 初始化uart对象,数据位为8,无校验位,停止位为1
# 设置输入引脚为GPIO1,输出引脚为GPIO2
# GPIO1和GPIO2短接,串口0自发自收
# 设置串口超时时间为5ms
uart.init(bits=8,
          parity=None,
          stop=1,
          tx=0,
          rx=1,
          timeout=5)

# 设置GPIO 25为LED输出引脚,下拉电阻使能
LED: Pin = Pin(25, Pin.OUT, Pin.PULL_DOWN)

# 创建一个轮询对象,用于监视串口的活动状态
uart_read_poll: select.poll = select.poll()
# 将串口对象注册到轮询对象中
# select.POLLIN 是一个常量,表示可读事件
# select.POLLERR 是一个常量,表示错误事件
# select.POLLHUP 是一个常量,表示设备断开事件
# 当串口设备上有数据可读时,uart_read_poll 对象会被触发
uart_read_poll.register(uart, select.POLLIN | select.POLLERR | select.POLLHUP)

# ========================================  主程序  ============================================

while True:

    # 点亮LED灯
    LED.on()

    # 使用Python的select模块中的poll方法来监听串口事件
    # poll方法会阻塞等待,直到串口上有数据可以接收
    # 设定超时时间为50ms,50ms内无事件发生则返回
    events = uart_read_poll.poll(50)

    # events是一个包含设备对象的列表,包括fd属性和events属性
    print('events =', events)

    # 如果有事件发生,处理这些事件
    if events:
        # 循环遍历事件列表
        for event in events:
            # 打印发生事件的设备和事件类型
            print(" fd attributes/the name of the device          : ",event[0])
            print(" events property/the trigger type of the event : ",event[1])

            # 若是uart对象有事件发生,则处理该事件
            if event[0] == uart:

                # 调用handle_uart_error方法处理串口错误事件
                handle_uart_error(event[1])

                # 调用handle_uart_disconnect方法处理串口断开事件
                handle_uart_disconnect(event[1])

                # 若是uart对象有可读数据,则读取并打印接收到的数据
                if event[1] & select.POLLIN:
                    # 尝试读取串口数据
                    try:
                        # 读取一个字节的串口数据
                        data = uart.read(1)
                        # 打印接收到的数据
                        print("uart recv data = ", data)
                        # 将数据添加到缓冲区
                        uart_buffer.append(data)
                        # 可以选择在此处处理或分析接收到的数据
                        # ...

                        # 若缓冲区数据满时,执行清空操作
                        if len(uart_buffer) >= uart_buffer_max_len:
                            # 清空缓冲区
                            uart_buffer.clear()

                        # 熄灭LED灯表示数据已读取完毕
                        LED.off()
                    except Exception as e:
                        print("uart read error: ", e)

在以上代码中,我们首先通过 UART 创建一个串口对象,并配置一个 LED。

然后创建了一个 select.poll 对象 uart_read_poll,该对象用于监听串口的活动状态,包括:

  • POLLIN:可读事件,表示串口有数据可以读取。
  • POLLERR:错误事件,表示串口设备发生错误,POLLERR 事件表示设备发生了某种错误(例如串口线路错误或设备驱动错误),可能导致无法正常进行数据的读取或写入操作。
  • POLLHUP:断开事件,表示串口设备断开连接(例如串口连接线断开或计算机上的虚拟串口关闭连接),这里注意,在树莓派 Pico 的 MicroPython 中,串口设备断开时,串口并不会自动发送断开通知,而且操作系统本身并不会主动检测串口的物理断开,只有在基于 USB 的虚拟串口设备断开时才可能会触发 POLLERRPOLLHUP 事件。

接着,我们使用 uart_read_poll.register 方法将串口对象注册到 poll 对象中,以便监听这些事件;在主循环中,通过 uart_read_poll.poll(50) 监听串口事件,poll 方法会阻塞,直到一个事件被触发,或者在 50 毫秒内没有事件发生则返回,这里,events 是一个包含设备对象的列表,列表中的元素即事件对象为元组的形式,即(fd 属性-设备名称,events 属性-事件类型)。

如果发生事件,程序会遍历 events 列表,检查每个事件的设备和事件类型:

  • 错误事件 (POLLERR):如果检测到串口发生错误,会调用 handle_uart_error 函数来处理错误。函数会尝试重置串口连接,重新初始化串口设置。
  • 断开事件 (POLLHUP)​ :如果检测到串口设备断开,调用 handle_uart_disconnect 函数停止串口操作并准备重新连接。
  • 可读事件 (POLLIN)​:如果串口有数据可以读取,则尝试读取一个字节的数据,并将数据添加到缓冲区中。接收到的数据会被打印出来。

同时,接收到的数据会被存储到一个名为 uart_buffer 的列表中。为了防止缓冲区溢出,设置了一个最大长度 uart_buffer_max_len,当缓冲区数据量超过这个最大值时,会清空缓冲区。

在程序的每个阶段,LED 都充当指示灯,显示系统的状态:

  • 如果事件被处理(例如读取数据),LED 会被点亮。
  • 在数据处理完成后,LED 会熄灭,表示数据读取已完成。

接着,我们将程序烧录到树莓派 Pico 中,打开终端,并通过 XCOM 串口助手发送数据,运行情况如下:

当接收到串口数据时,终端输出如下:

events = [(UART(0, baudrate=9600, bits=8, parity=None, stop=1, tx=0, rx=1, txbuf=256, rxbuf=256, timeout=5, timeout_char=2, invert=None), 1)]
fd attributes/the name of the device          :  UART(0, baudrate=9600, bits=8, parity=None, stop=1, tx=0, rx=1, txbuf=256, rxbuf=256, timeout=5, timeout_char=2, invert=None)
events property/the trigger type of the event :  1
uart recv data =  b'a'

输出了事件列表 (events)、设备名称和事件类型和串口接收的数据 (uart recv data = b'a')。

在有多个流设备进行数据接收操作时(例如三个串口都连接了外部传感器),使用 select 模块进行事件监听可以帮助我们在任意一个串口上有数据接收时正确无误地完成数据接收和进一步处理。
image.png

image.png


posted @ 2026-08-31 14:11  FreakStudio  阅读(7)  评论(0)    收藏  举报