MicroPython RS232 实战:软件流控(XON/XOFF)实现串口可靠通信,解决数据溢出难题解

1.RS232 通信基础知识

RS-232 是一种广泛使用的串行通信协议,最初由电子工业协会(EIA)在 1960 年代定义,它用于在计算机和外部设备(如调制解调器、打印机、传感器等)之间进行数据传输。

正如我们上文中在讲到串口通信协议层中说过的,RS-232 标准和我们在串口通信中使用的 TTL 电气标准只是在使用的逻辑电平标准上有所不同,而在具体的串口协议上,即串口通讯的数据包结构、错误检测与纠正和速率匹配与同步等内容,二者是相同的:

  • RS-232​:使用高电平和低电平信号,标准定义高电平为 +3V 到 +25V,低电平为-3V 到-25V,由于信号电平较高,RS-232 适合长距离传输
  • TTL-UART​:使用 TTL 电平,通常高电平为 +2.5V 到 +5V(通常为 5V),低电平为 0V,TTL 信号电平适合短距离通信

2.MAX3232 芯片介绍

这里,我们使用 MAX3232 芯片将 TTL 电平(0V 至 +5V)转换为 RS-232 标准的电平(±3V 至 ±25V),MAX3232 是一款常用的 RS-232 至 TTL(或 CMOS)电平转换器芯片,支持高达 250kbps 的数据传输速率和两个通道的 RS-232 至 TTL(或 CMOS)电平转换,常用于微控制器与计算机、调制解调器、打印机等设备之间的串行通信。

MAX3232 芯片引脚名称和功能如下:

根据功能分为以下三类:

  • 电荷泵相关​​引脚​:这些引脚用于内部电荷泵的工作,通过外部电容器生成所需的电压 (+5.5V 和 -5.5V),为 RS-232 驱动器提供合适的电平。
  • 电源与接地​​引脚​:为 MAX3232 提供电源和地,确保芯片的正常工作,供电电压范围为 +3.0V 至 +5.5V。
  • 数据传输引脚​:用于 TTL/CMOS 和 RS-232 电平之间的转换,包括 RS-232 接收器输入、输出以及 TTL/CMOS 驱动器输入、输出。

其工作原理可以简单概括为:MAX3232 通过内置的电荷泵电路和反相器,将输入的低电压(如 3.3V 或 5V)转换成符合 RS-232 标准的高电压(通常为 +5V 至 +15V 和 -5V 至 -15V)。

这里,内部电荷泵工作需要的电容容值对应大小与供电电压相关:

RS-232 通信往往使用 DB9 连接器,DB9 连接器有 9 个引脚,每个引脚在 RS-232 通信中有特定的功能,各个引脚编号和功能如下:

这里,我们仅使用 DB9 接口的 RXD、TXD 和 GND 引脚。

DB9 连接器有两种类型:

  • 公头(Male) ​ :有 9 个凸起的金属引脚
  • 母头(Female) ​ :有 9 个小孔,用于接受公头引脚

3.USB 转 RS232 适配器

通常,计算机或设备的串行端口使用的是 DB9 母头,而串行电缆的一端为 DB9 公头;在我们的开发板上使用的 DB9 连接器为母头,在当前的电脑上,DB9 连接器早已消失不见,但虚拟串口(通过 USB 转串口的方式)仍然可以使用,我们可以通过 USB 转 DB9 适配器完成单片机和电脑通过 RS232 协议的通信。

这里 USB 转 RS232 的适配器往往使用内置 FT232 芯片来完成不同通信协议的转换,FT232 是一款由 FTDI 公司设计的 USB 转串口芯片,它广泛用于嵌入式系统、微控制器开发和调试,以及将传统的 RS-232 或 TTL 串行接口转换为 USB 接口,允许现代设备与老式串行设备进行通信。

FT232 芯片在大多数操作系统中需要安装 FTDI 提供的驱动程序,具体可以从 FTDI 的官方网站下载:

https://ftdichip.com/drivers/vcp-drivers/

这里,我们提供的 USB 转 RS232 适配器使用的是国产的 MS3020 芯片,同样需要安装对应驱动程序,如果不安装对应驱动程序直接将 USB 转 RS232 适配器连接到电脑上时无法进行正常通信,打开设备管理器,显示如下:

驱动程序下载链接如下:

http://www.dtech.cn/download/download.php?class3=410

也可以在我们提供的工具软件中找到对应驱动程序:

打开驱动安装说明 PDF 文件,按照操作步骤安装即可:

安装成功后,再次点击设备管理器,可以发现原先设备上的小感叹号消失,能够正常使用,在对应的串口通信软件助手中,也可以正常选择其端口:

4.应用实验

在以下代码中,我们使用自定义 RS232 数据收发类完成主设备(树莓派 Pico)和从设备(电脑)的点对点通信,并在自定义类中实现了​软件流控​(Software Flow Control)功能,所谓软件流控,又叫​XON/XOFF 流控​,是一种基于软件的串行通信流控制机制,它使用特定的控制字符来启动或停止数据传输,这些控制字符通过串口通信线路发送,不需要额外的物理连接,完全依赖于串行数据线。

软件流控通常使用以下两个 ASCII 控制字符:

  • XON (0x11,CTRL-Q) ​ :表示“继续发送数据”(发送方可以继续传输)
  • XOFF (0x13,CTRL-S) ​ :表示“暂停发送数据”(发送方应暂停传输,避免接收方缓冲区溢出)

那么什么时候使用这两个 ASCII 控制字符呢?下面我们简单说明一下应用场景:

  • 接收方发送 XOFF​:当接收方的缓冲区快要满了时,接收方发送 XOFF 信号,通知发送方暂停发送数据
  • 发送方暂停发送​:发送方接收到 XOFF 信号后,立即暂停发送数据,等待进一步指示
  • 接收方发送 XON​:当接收方处理完数据,腾出缓冲区空间后,它会发送 XON 信号,告诉发送方可以继续发送数据
  • 发送方继续发送​:发送方收到 XON 信号后,恢复数据发送,直到再次接收到 XOFF 或数据发送完成

在下面实验中,我们需要将 风雅一号板-Grove接口扩展板插入到风雅一号板-串口电平转换板上,同时打开 RS232-SW 拨码开关上 RS232-TXDRS232-RXD 选项,我们使用串口外设 0 与风雅一号板-串口电平转换板上RS232 芯片的 TTL1 接口连接,设置树莓派 Pico 接收引脚为 GPIO17,发送引脚为 GPIO16,:

接着,我们使用 USB 转 RS232 适配器连接 风雅一号板-串口电平转换板RS232 Interface 接口上,实物连接图如下所示:

我们首先自定义一个具有软件流控功能的 RS232 通信类,示例代码如下:

# 自定义RS232串口通信类,使用软件流控
class RS232:
    """
​    自定义RS232串口通信类,支持软件流控(XON/XOFF协议)。

​    该类实现了RS232串口的发送和接收功能,支持基于XON/XOFF的流控制机制。流控制用于在数据传输过程中,
​    根据接收缓冲区的状态来控制数据的发送与暂停,防止接收缓冲区溢出。通过该类,可以进行串口数据的发送、接收和流控管理。

​    Attributes:
​        uart (UART): 串口实例,代表用于通信的硬件接口。
​        tx_buffer (bytearray): 发送缓冲区,用于存储待发送的数据。
​        rx_buffer (bytearray): 接收缓冲区,用于存储接收到的数据。
​        rx_pos (int): 接收缓冲区的指针,标记接收数据的位置。
​        rx_buf_size (int): 接收缓冲区的大小,表示接收缓冲区能够存储的最大数据量。
​        tx_pos (int): 发送缓冲区的指针,标记已准备发送数据的位置。
​        tx_buf_size (int): 发送缓冲区的大小,表示发送缓冲区能够存储的最大数据量。
​        xon (bool): 软件流控状态,表示当前是否可以发送数据。`True`表示可以发送,`False`表示需要暂停。

​    Methods:
​        send(data: str) -> None:
​            发送数据到串口。支持XON/XOFF流控,发送过程中如果流控为暂停状态,将等待直到XON信号恢复。

​        receive() -> bytearray:
​            从串口接收数据。接收过程中如果接收缓冲区已满,则会发送XOFF信号暂停数据传输。
​    """

​    ​# 软件流程相关类变量:两个 ASCII 控制字符
    # XON (CTRL-Q)
    XON = b'\x11'
    # XOFF (CTRL-S)
    XOFF = b'\x13'

    def __init__(self, uart, rx_buf_size=256, tx_buf_size=256):
        """
​        初始化串口对象。

​        Args:
​            uart (UART): 使用的串口实例。
​            rx_buf_size (int, optional): 接收缓冲区的大小,默认值为256。
​            tx_buf_size (int, optional): 发送缓冲区的大小,默认值为256。

​        Returns:
​            None
​        """

​        ​# 将传入的 UART 对象保存
        self.uart = uart
        # 发送缓冲区,用于存储待发送的数据
        self.tx_buffer = bytearray(tx_buf_size)
        # 接收缓冲区,用于存储接收到的数据
        self.rx_buffer = bytearray(rx_buf_size)

        # 接收缓冲区的指针,跟踪数据位置
        self.rx_pos = 0
        # 接收缓冲区大小
        self.rx_buf_size = rx_buf_size

        # 发送缓冲区的指针,跟踪数据位置
        self.tx_pos = 0
        # 发送缓冲区大小
        self.tx_buf_size = tx_buf_size

        # XON 状态,表示可以发送数据
        self.xon = True

    def send(self, data : str) -> None:
        """
​        发送数据。

​        该函数将传入的数据发送到串口,遵循XON/XOFF流控机制。如果当前流控不允许
​        发送数据,函数会等待直到流控解除。

​        Args:
​            data (str): 待发送的数据字符串。

​        Returns:
​            None
​        """

​        ​# 如果data不是字符串类型,进行数据转换
        if not isinstance(data, str):
            data = str(data)

        # 如果数据长度超过发送缓冲区大小,进行截断
        if len(data) > self.tx_buf_size:
            data = data[0:self.tx_buf_size]

        # 将待发送的数据加入到发送缓冲区中
        # 按照当前的发送缓冲区指针 self.tx_pos 插入数据
        self.tx_buffer[self.tx_pos:self.tx_pos+len(data)] = data.encode('utf-8')
        # 更新发送缓冲区指针
        self.tx_pos += len(data)

        # 遍历发送缓冲区中的数据
        for i in range(self.tx_pos):

            # 如果 XOFF 被激活,等待 XON 信号继续传输
            while not self.xon:
                time.sleep(0.01)  # 等待 10 毫秒

            # 发送单字节,确保为 bytes 类型
            self.uart.write(bytes([self.tx_buffer[i]]))

        # 发送完毕后,通过重置指针清空发送缓冲区
        self.tx_pos = 0

    def receive(self) -> bytearray:
        """
​        接收数据。

​        该函数检查串口接收缓冲区,读取接收到的数据并返回。若接收缓冲区已满,
​        则通过XOFF信号通知暂停数据传输。

​        Returns:
​            bytearray: 接收到的数据,作为字节数组返回。
​        """

​        ​# 循环检查 UART 接收缓冲区是否有数据
        while uart.any():

            # 逐字节接收数据
            byte = self.uart.read(1)

            # 判断是否是 XON 或 XOFF 信号
            if byte == RS232.XON:
                # 收到 XON,允许继续传输
                self.xon = True
            elif byte == RS232.XOFF:
                # 收到 XOFF,暂停传输
                self.xon = False
            else:
                # 接收到的实际数据存储到接收缓冲区
                self.rx_buffer[self.rx_pos] = byte[0]
                # 移动缓冲区指针
                self.rx_pos += 1

                # 缓冲区已满,发送 XOFF 信号
                if self.rx_pos >= self.rx_buf_size:
                    self.uart.write(RS232.XOFF)
                    break
        # 截取数据
        data = self.rx_buffer[0:self.rx_pos]
        # 重置接收缓冲区指针
        self.rx_pos = 0
        # 返回接收的数据
        return data

自定义 RS232 类的工作原理如下:

  • 发送数据 ​send() ​:首先检查 data 是否是字符串,如果不是则将其转换为字符串,将数据编码为 UTF-8 字节串,并存入发送缓冲区 tx_buffer 中;接着将数据逐字节发送,在每次发送数据之前,检查 xon 状态,如果 xonFalse(即收到 XOFF 信号),则暂停发送,直到收到 XON 信号为止;数据发送完后,将发送缓冲区指针 tx_pos 重置为 0
  • 接收数据receive():通过 self.uart.any() 检查是否有接收到的数据,并逐个读取数据;然后进行 XON/XOFF 信号检测和接收数据存储,如果接收缓冲区满(即 rx_pos 等于或超过 rx_buf_size),则发送 XOFF 信号给发送方,告知其暂停数据传输,最后返回已接收到的所有数据,并重置接收缓冲区指针 rx_pos

完整代码如下所示,在配套资料的文件夹:elegance-devkit v1\Demo\11 UART_RS232 中:

# Python env   : MicroPython v1.23.0
# -*- coding: utf-8 -*-        
# @Time    : 2024/9/22 下午11:23   
# @Author  : 李清水            
# @File    : main.py       
# @Description : UART类实验,RS232串口通信实现软件流控功能

# ======================================== 导入相关模块 =========================================

# 硬件相关的模块
from machine import UART, Pin, Timer
# 时间相关的模块
import time

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

# 串口发送计数变量
send_count: int = 0

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

# 定时器回调函数,用于定时接收数据
def receive_data(timer: Timer) -> None:
    """
​    定时器回调函数,用于定时接收数据。

​    该函数会检查串口接收的数据,并根据流控状态控制LED灯的开关。
​    如果流控信号允许发送数据,LED会熄灭;如果流控信号禁止发送数据,LED会亮起。

​    Args:
​        timer (Timer): 定时器实例。

​    Returns:
​        None
​    """
​    ​global rs232,LED

    # 接收数据
    data: bytearray = rs232.receive()

    # 判断是否接收到数据
    if len(data) > 0:
        # 将接收到的数据转换为十六进制并打印
        hex_data = [hex(byte) for byte in data]
        print("Received data:", hex_data)

    # 判断rs232的流控状态
    if rs232.xon:
        # 如果可以发送数据,熄灭 LED
        LED.value(0)
    else:
        # 如果不能发送数据,点亮 LED
        LED.value(1)

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

# 自定义RS232串口通信类,使用软件流控
class RS232:
    """
​    自定义RS232串口通信类,支持软件流控(XON/XOFF协议)。

​    该类实现了RS232串口的发送和接收功能,支持基于XON/XOFF的流控制机制。流控制用于在数据传输过程中,
​    根据接收缓冲区的状态来控制数据的发送与暂停,防止接收缓冲区溢出。通过该类,可以进行串口数据的发送、接收和流控管理。

​    Attributes:
​        uart (UART): 串口实例,代表用于通信的硬件接口。
​        tx_buffer (bytearray): 发送缓冲区,用于存储待发送的数据。
​        rx_buffer (bytearray): 接收缓冲区,用于存储接收到的数据。
​        rx_pos (int): 接收缓冲区的指针,标记接收数据的位置。
​        rx_buf_size (int): 接收缓冲区的大小,表示接收缓冲区能够存储的最大数据量。
​        tx_pos (int): 发送缓冲区的指针,标记已准备发送数据的位置。
​        tx_buf_size (int): 发送缓冲区的大小,表示发送缓冲区能够存储的最大数据量。
​        xon (bool): 软件流控状态,表示当前是否可以发送数据。`True`表示可以发送,`False`表示需要暂停。

​    Methods:
​        send(data: str) -> None:
​            发送数据到串口。支持XON/XOFF流控,发送过程中如果流控为暂停状态,将等待直到XON信号恢复。

​        receive() -> bytearray:
​            从串口接收数据。接收过程中如果接收缓冲区已满,则会发送XOFF信号暂停数据传输。
​    """

​    ​# 软件流程相关类变量:两个 ASCII 控制字符
    # XON (CTRL-Q)
    XON = b'\x11'
    # XOFF (CTRL-S)
    XOFF = b'\x13'

    def __init__(self, uart, rx_buf_size=256, tx_buf_size=256):
        """
​        初始化串口对象。

​        Args:
​            uart (UART): 使用的串口实例。
​            rx_buf_size (int, optional): 接收缓冲区的大小,默认值为256。
​            tx_buf_size (int, optional): 发送缓冲区的大小,默认值为256。

​        Returns:
​            None
​        """

​        ​# 将传入的 UART 对象保存
        self.uart = uart
        # 发送缓冲区,用于存储待发送的数据
        self.tx_buffer = bytearray(tx_buf_size)
        # 接收缓冲区,用于存储接收到的数据
        self.rx_buffer = bytearray(rx_buf_size)

        # 接收缓冲区的指针,跟踪数据位置
        self.rx_pos = 0
        # 接收缓冲区大小
        self.rx_buf_size = rx_buf_size

        # 发送缓冲区的指针,跟踪数据位置
        self.tx_pos = 0
        # 发送缓冲区大小
        self.tx_buf_size = tx_buf_size

        # XON 状态,表示可以发送数据
        self.xon = True

    def send(self, data : str) -> None:
        """
​        发送数据。

​        该函数将传入的数据发送到串口,遵循XON/XOFF流控机制。如果当前流控不允许
​        发送数据,函数会等待直到流控解除。

​        Args:
​            data (str): 待发送的数据字符串。

​        Returns:
​            None
​        """

​        ​# 如果data不是字符串类型,进行数据转换
        if not isinstance(data, str):
            data = str(data)

        # 如果数据长度超过发送缓冲区大小,进行截断
        if len(data) > self.tx_buf_size:
            data = data[0:self.tx_buf_size]

        # 将待发送的数据加入到发送缓冲区中
        # 按照当前的发送缓冲区指针 self.tx_pos 插入数据
        self.tx_buffer[self.tx_pos:self.tx_pos+len(data)] = data.encode('utf-8')
        # 更新发送缓冲区指针
        self.tx_pos += len(data)

        # 遍历发送缓冲区中的数据
        for i in range(self.tx_pos):

            # 如果 XOFF 被激活,等待 XON 信号继续传输
            while not self.xon:
                time.sleep(0.01)  # 等待 10 毫秒

            # 发送单字节,确保为 bytes 类型
            self.uart.write(bytes([self.tx_buffer[i]]))

        # 发送完毕后,通过重置指针清空发送缓冲区
        self.tx_pos = 0

    def receive(self) -> bytearray:
        """
​        接收数据。

​        该函数检查串口接收缓冲区,读取接收到的数据并返回。若接收缓冲区已满,
​        则通过XOFF信号通知暂停数据传输。

​        Returns:
​            bytearray: 接收到的数据,作为字节数组返回。
​        """

​        ​# 循环检查 UART 接收缓冲区是否有数据
        while uart.any():

            # 逐字节接收数据
            byte = self.uart.read(1)

            # 判断是否是 XON 或 XOFF 信号
            if byte == RS232.XON:
                # 收到 XON,允许继续传输
                self.xon = True
            elif byte == RS232.XOFF:
                # 收到 XOFF,暂停传输
                self.xon = False
            else:
                # 接收到的实际数据存储到接收缓冲区
                self.rx_buffer[self.rx_pos] = byte[0]
                # 移动缓冲区指针
                self.rx_pos += 1

                # 缓冲区已满,发送 XOFF 信号
                if self.rx_pos >= self.rx_buf_size:
                    self.uart.write(RS232.XOFF)
                    break
        # 截取数据
        data = self.rx_buffer[0:self.rx_pos]
        # 重置接收缓冲区指针
        self.rx_pos = 0
        # 返回接收的数据
        return data

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

# 上电延时3s
time.sleep(3)
# 打印调试消息
print("FreakStudio: RS232 Software Flow Control Demo")

# 创建串口对象,设置波特率为115200
uart: UART = UART(0, 115200)
# 初始化uart对象,波特率为115200,数据位为8,无校验位,停止位为1
# 设置接收引脚为GPIO17,发送引脚为GPIO16
# 设置串口超时时间为100ms
uart.init(baudrate  = 115200,
          bits      = 8,
          parity    = None,
          stop      = 1,
          tx        = 16,
          rx        = 17,
          timeout   = 100)

# 初始化RS232对象,设置接收缓冲区大小为32,发送缓冲区大小为32
rs232: RS232 = RS232(uart, 32, 32)

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

# 创建定时器对象,每隔10ms接收数据
timer: Timer = Timer(-1)
timer.init(period=10, mode=Timer.PERIODIC, callback=receive_data)

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

# 循环执行
while True:
    # 发送固定数据,每次发送完成都换行
    rs232.send("RS232 Send Data :" + str(send_count)+'\r\n')
    # 发送计数变量递增
    send_count = send_count + 1
    # 延时1s
    time.sleep(1)

这里,我们在主循环中通过自定义类中数据发送方法定时 1s 左右发送一次数据,定时器每 10 毫秒检查一次是否有接收数据,若有接收数据则将其输出到终端上,并通过 LED 指示当前串口的流控状态。

这里,定时器可以简单理解为一个定时执行对应回调函数 receive_data 的执行流,它是周期性触发的,会在设定的时间间隔内定时调用某个函数并且不受 time.sleep 延时方法的影响,在该程序中,通过定时器的周期性调用,我们可以实现对串口数据的持续监听。

烧录代码,打开终端,显示如下:

打开串口助手,设置波特率为 115200,其他参数默认即可,选择以下名称对应 COM 口:

MacroSilicon USB Serial Ports

这里,使用串口助手进行通信时,注意以字符形式进行显示,而发送时以 16 进制进行发送,并且这里不要选择“发送新行”选项:

可以看到,自定义 RS232 通信类定时 1s 进行数据发送。我们尝试发送 16 进制数据:

13 22 75 85 65 21 32

可以看到,在终端上除了流控字符 XOFF (0x13) 外,其余数据均显示,同时在串口助手软件上可以看到树莓派 Pico 停止了数据发送并且树莓派 Pico 上板载 LED 灯亮起:

再次发送数据:11 22 33 44 55 66,其中 XON (0x11) 流控字符表示 PC 端可以接收数据:

可以发现,此时树莓派 Pico 的 RS232 通信又重新开始,同时板载 LED 灯熄灭:

我们这里,发送一个数据长度超过设定数据缓冲区的 16 进制数据:

11 22 33 44 55 66 77 88 99 00 11 22 33 44 55 66 77 88 99 00 11 22 33 44 55 66 77 88 99 00 11 22 33 44 55 66 77 88 99 00 11 22 33 44 55 66 77 88 99 00 11 22 33 44 55 66 77 88 99 00

并提前设置,串口助手为 16 进制显示:

然后发送数据,可以看到树莓派 Pico 发送了流控字符 XOFF (0x13)

并且,树莓派 Pico 接收数据大小始终没有超过设置的缓冲区大小:

经以上测试,可以看到我们自定义的 RS232 软件流控通信类可以完成基本的传输中断和启动功能,相比于硬件流控,软件流控简单易用并且无需多余接线,但是软件流控也存在一些缺点:依赖于特殊的控制字符来管理数据传输,会占用一部分传输带宽,同时 XON0x11)和 XOFF0x13)是特定的控制字符,但如果传输的数据本身包含这些字符,它们可能会被误解为流控指令,从而引发错误的传输暂停或恢复。


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