深度解析 RP2040 DMA 应用:MicroPython 下 UART 零 CPU 干预数据传输

1.实验前的准备

在以下代码中,我们自定义了一个 DMA_UART_Tx 类,它用于在树莓派 Pico 上通过 UARTDMA 进行高效的串口数据发送,DMA 将数据从源地址(内存)传输到目的地址( UART 数据寄存器)中。

在下面实验中,我们需要将风雅一号板-Grove接口扩展板插入到风雅一号板-通用兼容扩展板上,同时使用 HY2.0-4P 接线连接风雅一号板-Grove接口扩展板UART1 接口和 GraftSense-基于CH340K的USB转TTL模块 上的 UART1 接口:

1.JPEG

接着使用 USB-AMiniUSB 数据线连接GraftSense-基于CH340K的USB转TTL模块上的 USB 接口和电脑,实物连接图如下所示:

2.JPEG

除此之外,我们首先需要将 MiniUSB 数据线连接到风雅一号板-通用兼容扩展板上的串口,这里,GP0GP1 是树莓派 Pico 的硬件串口 UART0 的默认引脚:

3.PNG

插入后实物图如下:

4.JPEG

引脚连接情况如下所示:

4.png

2.自定义串口 DMA 传输类的实现

这里,我们首先将自定义 UART 串口 DMA 传输类放到一个单独的文件中:

# Python env   : MicroPython v1.23.0
# -*- coding: utf-8 -*-        
# @Time    : 2024/9/7 下午11:56   
# @Author  : 李清水            
# @File    : dma_uart_tx.py       
# @Description : 自定义UART串口DMA传输

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

# 从 rp2 库导入 DMA 控制器
from rp2 import DMA
# 导入时间相关的模块
import time
# 导入硬件相关的模块
from machine import UART
# 导入读写32位内存的模块
from machine import mem32
# 导入 addressof 函数,用于获取数据的内存地址
from uctypes import addressof

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

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

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

# 自定义DMA串口发送类:使用DMA传输数据到UART完成数据发送
class DMA_UART_Tx:
    """
​    DMA_UART_Tx类,用于通过DMA(直接内存访问)将数据传输到UART完成数据发送。
​    该类封装了DMA和UART的配置与操作,支持8个数据位、无校验位、1个停止位的传输模式。
​    默认使用UART0,波特率115200,TX引脚0,RX引脚1。

​    Attributes:
​        uart_num (int): UART编号,0表示UART0,1表示UART1。
​        baudrate (int): 波特率,默认为115200。
​        tx_pin (int): TX引脚编号,默认为0。
​        rx_pin (int): RX引脚编号,默认为1。
​        uart (UART): UART实例,用于配置和操作UART。
​        dma (DMA): DMA实例,用于配置和操作DMA。
​        UART_BASE (int): UART寄存器基地址。
​        UART_UARTDR (int): UART数据寄存器地址。
​        UART_UARTFR (int): UART标志寄存器地址。
​        UART_UARTDMACR (int): UART DMA控制寄存器地址。

​    Methods:
​        __init__(self, uart_num=0, baudrate=115200, tx_pin=0, rx_pin=1):
​            初始化DMA UART类。
​        is_transmit_fifo_full(self):
​            检查UART传输FIFO是否已满。
​        is_transmit_fifo_empty(self):
​            检查UART传输FIFO是否为空。
​        is_buffer_protocol(obj):
​            判断对象是否为缓冲区协议对象。
​        enable_uart_tx_dma(self):
​            启用UART发送DMA功能。
​        dma_transmit(self, buf, wait_func=None, callback=None, blocking=False):
​            使用DMA传输数据到UART。
​    """
​    ​def __init__(self, uart_num: int = 0, baudrate: int = 115200, tx_pin: int = 0, rx_pin: int = 1) -> None:
        """
​        初始化DMA UART类,暂时只支持8个数据位、无校验位、1个停止位的传输模式。
​        默认使用UART0,波特率115200,TX引脚0,RX引脚1。

​        Args:
​            uart_num (int): 选择UART0或UART1,默认为0。
​            baudrate (int): 波特率,默认为115200。
​            tx_pin (int): TX引脚编号,默认为0。
​            rx_pin (int): RX引脚编号,默认为1。

​        Raises:
​            ValueError: 如果UART编号或波特率不合法。
​        """
​        ​# 检测UART是否可用
        if uart_num not in (0, 1):
            raise ValueError("UART number must be 0 or 1")
        # 检查波特率是不是合法值
        if baudrate not in (9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600):
            raise ValueError("Invalid baudrate")

        self.uart_num = uart_num
        self.baudrate = baudrate
        self.tx_pin = tx_pin
        self.rx_pin = rx_pin

        # 初始化 UART
        self.uart = UART(self.uart_num, baudrate=self.baudrate, tx=self.tx_pin, rx=self.rx_pin)
        self.uart.init(baudrate=self.baudrate, bits=8, parity=None, stop=1, timeout=100)

        # 初始化 DMA
        self.dma = DMA()

        # 定义 UART 寄存器地址:若是UART0,那么地址为0x40034000,否则为0x40038000
        self.UART_BASE = 0x40034000 if self.uart_num == 0 else 0x40038000

        # UART外设中 UARTDR、UARTFR、UARTCR和UARTDMACR寄存器的相关地址信息
        self.UARTDR_OFFSET = 0x000
        self.UARTFR_OFFSET = 0x018
        self.UARTDMACR_OFFSET = 0x048

        self.UART_UARTDR = self.UART_BASE + self.UARTDR_OFFSET
        self.UART_UARTFR = self.UART_BASE + self.UARTFR_OFFSET
        self.UART_UARTDMACR = self.UART_BASE + self.UARTDMACR_OFFSET

        # 使能DMA传输功能
        self.enable_uart_tx_dma()

    def is_transmit_fifo_full(self) -> bool:
        """
​        检查 UART 传输 FIFO 是否已满。

​        Returns:
​            bool: True 为满,False 为未满。
​        """

​        ​# 读取 UARTFR 寄存器的值
        reg_value = mem32[self.UART_UARTFR]
        # 提取 TXFF 位的值 (第 5 位)
        txff_bit = (reg_value >> 5) & 0x1

        # 判断 TXFF 位是否为 1(FIFO 是否已满)
        return txff_bit == 1

    def is_transmit_fifo_empty(self) -> bool:
        """
​        检查 UART 传输 FIFO 是否为空。

​        Returns:
​            bool: True 为空,False 为不为空。
​        """

​        ​# 读取 UARTFR 寄存器的值
        reg_value = mem32[self.UART_UARTFR]
        # 提取 TXFE 位的值 (第 7 位)
        txfe_bit = (reg_value >> 7) & 0x1

        # 判断 TXFE 位是否为 1(FIFO 是否为空)
        return txfe_bit == 1

    @staticmethod
    def is_buffer_protocol(obj: object) -> bool:
        """
​        判断对象是否为缓冲区协议对象。

​        Args:
​            obj (object): 传入对象。

​        Returns:
​            bool: 若为缓冲区协议对象,返回True,否则返回False。
​        """
​        ​try:
            # 尝试使用 memoryview 来创建对象的缓冲区视图
            memoryview(obj)
            return True
        except TypeError:
            return False

    def enable_uart_tx_dma(self) -> None:
        """
​        启用 UART 发送 DMA 功能,设置 UARTDMACR 寄存器的 TXDMAE 位。
​        """
​        ​reg_value = mem32[self.UART_UARTDMACR]
        reg_value |= (1 << 1)
        mem32[self.UART_UARTDMACR] = reg_value

    def dma_transmit(self, buf: object, wait_func: callable = None, callback: callable = None,
                     blocking: bool = False) -> int:
        """
​        使用DMA传输数据到UART。

​        Args:
​            buf (object): 待传输的字节数据,需要为缓冲区协议对象。
​            wait_func (callable): 传输进行中时的回调函数,可选。
​            callback (callable): 传输完成时的回调函数,可选。
​            blocking (bool): 是否阻塞等待传输完成,可选,默认为 False。

​        Returns:
​            int: 耗时,单位为微秒。

​        Raises:
​            Exception: 如果非阻塞模式下传入了 wait_func,或者 buf 不是缓冲区协议对象。
​        """

​        ​# 如果选择非阻塞模式,那么不应该传入wait_func
        if blocking == False and wait_func is not None:
            raise Exception("Blocking mode should not have wait_func!")

        # 判断buf是否为缓冲区协议对象
        if DMA_UART_Tx.is_buffer_protocol(buf) == False:
            raise Exception("buf must be a buffer protocol object!")

        # 20为DREQ_UARTO_TX请求信号的编号,22为DREQ_UART1_TX请求信号的编号
        UART_DREQ = 20 if self.uart_num == 0 else 22

        # 设置DMA控制寄存器
        ctrl = self.dma.pack_ctrl(enable=True,          # 启用 DMA
                                  size=0,               # 单次数据传输大小8-bit (byte)
                                  inc_read=True,        # 读取地址递增
                                  inc_write=False,      # 写入地址不递增
                                  treq_sel=UART_DREQ    # 设置 DMA 触发信号
                                  )

        # 配置 DMA
        self.dma.config(read=addressof(buf),     # 源地址,即数据缓冲区的内存地址
                        write=self.UART_UARTDR,  # 目标地址,即 UART 数据寄存器的地址
                        count=len(buf),          # 数据传输的字节数
                        ctrl=ctrl,               # DMA 控制寄存器配置
                        trigger=True             # 立即触发 DMA 传输
                        )

        # 记录开始时间
        start_time = time.ticks_us()
        # 启动 DMA 传输
        self.dma.active(1)

        # 阻塞模式,等待 DMA 传输完成
        if blocking == True:
            # 等待 DMA 传输完成
            while self.dma.active():
                # 检查 UARTFR 寄存器的 TXFE 位是否置位
                if not self.is_transmit_fifo_empty():
                    # 如果 TXFE 位不为 1,说明 FIFO 不为空
                    if wait_func is not None:
                        # 执行用户自定义回调函数
                        wait_func()

        if callback is not None:
            # 等待DMA传输完毕,执行用户自定义回调callback
            callback()

        # 记录结束时间
        end_time = time.ticks_us()  # 记录结束时间
        # 返回耗时
        return time.ticks_diff(end_time, start_time)

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

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

DMA_UART_Tx 类提供了如下方法和属性:

  • DMA_UART_Tx​ 类的属性​:

5.png

  • DMA_UART_Tx​ 类的方法​:

6.png

7.png

这里,我们首先查看树莓派 Pico 中串口外设的结构:

8.png

这里,图中的 32x8 transmit FIFO 和 32x12 receive FIFO 是 UART 硬件模块的一部分,但并不以直接的独立寄存器形式暴露给用户。我们需要通过 UARTDR(数据寄存器)来间接操作 FIFO,这个寄存器允许读写数据,并通过内部的硬件管理 FIFO 缓冲区的队列操作:

9.png

  • 写入 UARTDR​​寄存器的 DATA 数据位​:可以实现串口发送
  • 读取 UARTDR​​寄存器的 DATA 数据位​:可以实现读取串口接收的数据

而通过配置 UARTLCR_H 寄存器,则可以使能串口的 FIFO:

10.png

实际上,在 MicroPython 中创建并配置 UART 类的实例后,则已经使能了 FIFO 和 FIFO 中断触发阈值:

11.png

FIFO 的状态通常是通过标志寄存器 UARTFR (Flag Register) 来监控的,UARTFR 包含关于 FIFO 是否为空或已满的标志位。这样,通过读取标志位可以判断 FIFO 的状态,从而进行相应的数据传输操作:

12.png

串口外设中的 DMA 通过 UARTDMACR 来控制和协调数据传输,我们这里需要使能其 TXDMAE 数据位:

13.png

这里,使用 DMA 实现串口外设 DMA 数据发送的流程为:首先使能串口外设发送 DMA 中的 TXDMAE 数据位,接着使用 DMA 不断开启数据从内存到串口外设 UARTDR 寄存器的 DATA 数据位。

我们在初始化 DMA_UART_Tx 类时,进行如下工作:

14.png

  • 初始化 UART 配置,指定所使用的 UART 接口(uart_num),通信的波特率(baudrate),以及发送(tx_pin)和接收引脚(rx_pin)。
  • 使用 MicroPython 中的 UART 类初始化串口。self.uart_num 决定使用哪个 UART 接口,baudrate 设置通信速率,tx_pinrx_pin 则确定使用的引脚,接着使用初始化方法配置串口外设参数为 8 个数据位、不使用奇偶校验位、1 个停止位并设置超时时间为 100ms。
  • 创建一个 DMA 对象,准备在后续的传输中使用 DMA 进行内存到 UART 的传输操作。
  • 定义 UART 寄存器地址,若是使用 UART0,则基地址为 0x40034000,而 UART1 基地址为 0x40038000,并计算出 UARTDR 寄存器、UARTFR 寄存器和 UARTDMACR 寄存器的偏移量用以进行访问。
  • 调用 enable_uart_tx_dma() 函数,将 UARTDMACR 寄存器的 TXDMAE 位(位 1)置 1,启用 UART 的 DMA 发送功能。

在读写寄存器数据时,我们使用machine.mem32来读写 32 位的寄存器数据,向其中传入寄存器地址值,即可像访问内存一样,访问和配置寄存器。

我们使用 DMA 实现串口数据发送的主要方法为 dma_transmit

def dma_transmit(self, buf: object, wait_func: callable = None, callback: callable = None,
                 blocking: bool = False) -> int:
    """
​    使用DMA传输数据到UART。

​    Args:
​        buf (object): 待传输的字节数据,需要为缓冲区协议对象。
​        wait_func (callable): 传输进行中时的回调函数,可选。
​        callback (callable): 传输完成时的回调函数,可选。
​        blocking (bool): 是否阻塞等待传输完成,可选,默认为 False。

​    Returns:
​        int: 耗时,单位为微秒。

​    Raises:
​        Exception: 如果非阻塞模式下传入了 wait_func,或者 buf 不是缓冲区协议对象。
​    """
    ... ...

这里,我们需要向其中传入四个参数:传输的字节数据、传输进行中的回调函数、传输完成后的回调函数和是否阻塞等待传输完成标志量。

当选择阻塞模式时,则方法会阻塞直到 DMA 数据传输完成才返回;反之,设置为非阻塞模式时,开启 DMA 传输数据后即返回:

# 阻塞模式,等待 DMA 传输完成
if blocking == True:
    # 等待 DMA 传输完成
    while self.dma.active():
        # 检查 UARTFR 寄存器的 TXFE 位是否置位
        if not self.is_transmit_fifo_empty():
            # 如果 TXFE 位不为 1,说明 FIFO 不为空
            if wait_func is not None:
                # 执行用户自定义回调函数
                wait_func()

在该方法中,我们首先进行了入口参数验证:

  • 如果选择了非阻塞模式(blocking=False),则 wait_func 参数不应传入
# 如果选择非阻塞模式,那么不应该传入wait_func
if blocking == False and wait_func is not None:
    raise Exception("Blocking mode should not have wait_func!")
  • 验证 buf 是否为缓冲区协议对象,如果不是,抛出异常
# 判断buf是否为缓冲区协议对象
if DMA_UART_Tx.is_buffer_protocol(buf) == False:
    raise Exception("buf must be a buffer protocol object!")

这里,我们定义了一个静态方法判断其是否为缓冲区协议对象:

@staticmethod
def is_buffer_protocol(obj: object) -> bool:
    """
​    判断对象是否为缓冲区协议对象。

​    Args:
​        obj (object): 传入对象。

​    Returns:
​        bool: 若为缓冲区协议对象,返回True,否则返回False。
​    """
​    ​try:
        # 尝试使用 memoryview 来创建对象的缓冲区视图
        memoryview(obj)
        return True
    except TypeError:
        return False

我们会根据使用的串口外设编号配置 DMA 请求信号:

# 20为DREQ_UARTO_TX请求信号的编号,22为DREQ_UART1_TX请求信号的编号
UART_DREQ = 20 if self.uart_num == 0 else 22

DMA 请求信号编号如下图所示:

15.png

接着,我们配置 DMA 控制寄存器和通道:

# 设置DMA控制寄存器
ctrl = self.dma.pack_ctrl(enable=True,          # 启用 DMA
                          size=0,               # 单次数据传输大小8-bit (byte)
                          inc_read=True,        # 读取地址递增
                          inc_write=False,      # 写入地址不递增
                          treq_sel=UART_DREQ    # 设置 DMA 触发信号
                          )

# 配置 DMA
self.dma.config(read=addressof(buf),     # 源地址,即数据缓冲区的内存地址
                write=self.UART_UARTDR,  # 目标地址,即 UART 数据寄存器的地址
                count=len(buf),          # 数据传输的字节数
                ctrl=ctrl,               # DMA 控制寄存器配置
                trigger=True             # 立即触发 DMA 传输
                )

设置 DMA 的源地址为数据缓冲区的内存地址,目标地址为 UART 数据寄存器的地址,单次数据大小为 8 位(即一个字节),读取地址递增以便能够不断读取 buf 中的数据。

在启动 DMA 数据传输前,记录程序开始时间,在 DMA 传输完毕或 DMA 启动完成后,记录结束时间,返回该部分所耗费时间:

# 记录开始时间
start_time = time.ticks_us()
# 启动 DMA 传输
self.dma.active(1)

# 阻塞模式,等待 DMA 传输完成
if blocking == True:
    # 等待 DMA 传输完成
    while self.dma.active():
        # 检查 UARTFR 寄存器的 TXFE 位是否置位
        if not self.is_transmit_fifo_empty():
            # 如果 TXFE 位不为 1,说明 FIFO 不为空
            if wait_func is not None:
                # 执行用户自定义回调函数
                wait_func()

if callback is not None:
    # 等待DMA传输完毕,执行用户自定义回调callback
    callback()

# 记录结束时间
end_time = time.ticks_us()  # 记录结束时间
# 返回耗时
return time.ticks_diff(end_time, start_time)

这里,我们使用 is_transmit_fifo_empty 方法判断 UART 传输中 FIFO 是否已空,提取 TXFE 位来判断 FIFO 是否为空,从而判断当前串口数据的发送状态:

def is_transmit_fifo_empty(self) -> bool:
    """
​    检查 UART 传输 FIFO 是否为空。

​    Returns:
​        bool: True 为空,False 为不为空。
​    """

​    ​# 读取 UARTFR 寄存器的值
    reg_value = mem32[self.UART_UARTFR]
    # 提取 TXFE 位的值 (第 7 位)
    txfe_bit = (reg_value >> 7) & 0x1

    # 判断 TXFE 位是否为 1(FIFO 是否为空)
    return txfe_bit == 1

dma_transmit() 方法完整时序图如下所示:

16.png

3.实验例程

以下代码可以在我们提供的资料包中的 elegance-devkit v1\Demo\67 DMA_MemoryToPeripheral 文件夹中找到。

我们测试 DMA_UART_Tx 类的主程序如下:

# Python env   : MicroPython v1.23.0
# -*- coding: utf-8 -*-        
# @Time    : 2024/9/7 上午11:26   
# @Author  : 李清水            
# @File    : main.py       
# @Description : DMA类实验,内存到外设传输:DMA从内存写入数据到UART

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

# 导入数学计算相关模块
import math
# 导入时间相关模块
import time
# 导入自定义串口DMA传输类
from dma_uart_tx import DMA_UART_Tx
# 导入硬件相关模块
from machine import UART

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

# 数据缓冲区,待发送的数据:为一个正弦波数据
# 配置正弦波参数
amplitude = 127     # 幅度,正弦波最大值(0-255),127 对应无符号8位范围的一半,峰值
offset = 128        # 偏移量,保证波形在 0 到 255 之间
num_samples = 1000  # 正弦波采样点数
frequency = 1       # 频率,可以自行调整

# 生成正弦波数据,并将其放入 bytearray
sin_wave = bytearray(num_samples)

for i in range(num_samples):
    # 计算当前采样点的角度
    angle = 2 * math.pi * frequency * (i / num_samples)
    # 生成对应的正弦波值,放大并加上偏移量以适应 bytearray (0-255) 范围
    sine_value = int(amplitude * math.sin(angle) + offset)
    # 将正弦值放入 bytearray 缓冲区
    sin_wave[i] = sine_value

# 串口使用DMA发送数据的运行时间
uart_dma_time = 0
# 串口不使用DMA发送数据的运行时间
uart_non_dma_time = 0

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

# DMA传输等待回调函数
def uart_dma_tx_wait_func() -> None:
    """
​    DMA传输等待回调函数。

​    Returns:
​        None
​    """
​    ​print("wait dma transmit complete")

# DMA传输完成回调函数
def uart_dma_tx_complete_callback() -> None:
    """
​    DMA传输完成回调函数。

​    Returns:
​        None
​    """
​    ​print("uart dma transmit complete")

# 在 bytearray 每个元素后添加换行符 '\r\n'
def add_newline_after_each_byte(buf: bytearray) -> bytearray:
    """
​    在 bytearray 每个元素后添加换行符 '\r\n'。

​    Args:
​        buf (bytearray): 输入的 bytearray。

​    Returns:
​        bytearray: 返回一个新的包含 '\r\n' 的 bytearray。

​    Raises:
​        TypeError: 如果输入参数不是 bytearray 类型。
​    """

​    ​# 检查输入参数是否为 bytearray
    if not isinstance(buf, bytearray):
        raise TypeError("buf must be a bytearray")

    # 新建一个新的 bytearray 用于存放结果
    expanded_buf = bytearray()

    # 遍历输入缓冲区的每个字节
    for byte in buf:
        # 添加原始数据字节
        expanded_buf.append(byte)
        # 追加 '\r' 的 ASCII 值 (13)
        expanded_buf.append(13)
        # 追加 '\n' 的 ASCII 值 (10)
        expanded_buf.append(10)

    return expanded_buf

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

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

# 上电延时3s
time.sleep(3)
# 打印调试信息,表示开始主程序
print("FreakStudio: DMA Memory to Peripheral Test ")

# 实例化 DMA_UART_Tx 类,假设使用 UART0,波特率115200,TX引脚为0,RX引脚为1
dma_uart = DMA_UART_Tx(uart_num=0, baudrate=115200, tx_pin=0, rx_pin=1)

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

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

# 串口传输数据,使用DMA
# 传输数据,使用阻塞模式
dma_uart.dma_transmit(buf=sin_wave, wait_func=uart_dma_tx_wait_func, callback=uart_dma_tx_complete_callback, blocking=True)
# 传输数据,使用非阻塞模式,记录运行时间
uart_dma_time = dma_uart.dma_transmit(buf=sin_wave, blocking=False)

# 传输数据,不使用DMA
# 记录开始时间点
start_time = time.ticks_us()
# 传输数据
uart.write(sin_wave)
# 记录结束时间点
end_time = time.ticks_us()
# 计算运行时间
uart_non_dma_time = time.ticks_diff(end_time, start_time)

# 打印调试数据,表示传输完成
print("DMA Finished,run time: %d us" % uart_dma_time)
print("Non-DMA Finished,run time: %d us" % uart_non_dma_time)

这里,我们首先声明了一个缓冲区,向其中写入幅度为 127、直流偏移为 128、采样点为 1000 而频率为 1Hz 的正弦波数据;接着定义了 uart_dma_timeuart_non_dma_time 变量记录使用 DMA 和 不使用 DMA 传输数据的时间,同时,我们设置了两个回调函数,用于在 DMA 传输期间和传输结束后输出调试信息。

整个程序时序如下所示:

17.png

接着,我们打开 SerialPlot 软件,选择树莓派 Pico 的串口对应的端口:

18.png

数据格式设置如下:

19.png

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

20.png

01.gif

可以看到,使用 DMA 实现串口发送数据的 CPU 占用远远小于普通的串口数据发送,非阻塞模式下,DMA 传输完成后会调用回调函数,无需 CPU 轮询。

我们也可以导出数据的 CSV 文件,查看是不是所有数据全部接收完成:

21.png

暂时无法在飞书文档外展示此内容

00.gif

posted @ 2026-09-03 16:02  FreakStudio  阅读(8)  评论(0)    收藏  举报