【STM32】HAL库 STM32CubeMX教程十一---DMA (串口DMA发送接收)

前言:
本系列教程将 对应外设原理,HAL库与STM32CubeMX结合在一起讲解,使您可以更快速的学会各个模块的使用

所用工具:

1、芯片: STM32F407ZET6/ STM32F103ZET6

2、STM32CubeMx软件

3、IDE: MDK-Keil软件

4、STM32F1xx/STM32F4xxHAL库

知识概括:

通过本篇博客您将学到:

DMA工作原理

STM32CubeMX创建DMA例程

HAL库定时器DMA函数库

注意:关于cubemx的DMA配置,在DMA原理介绍中全部都有所讲解,如果有哪里不懂,请仔细阅读原理详解部分。

DMA的基本介绍

什么是DMA (DMA的基本定义)

DMA,全称Direct Memory Access,即直接存储器访问。

DMA传输将数据从一个地址空间复制到另一个地址空间,提供在外设和存储器之间或者存储器和存储器之间的高速数据传输

我们知道CPU有转移数据、计算、控制程序转移等很多功能,系统运作的核心就是CPU

CPU无时不刻的在处理着大量的事务,但有些事情却没有那么重要,比方说数据的复制和存储数据,如果我们把这部分的CPU资源拿出来,让CPU去处理其他的复杂计算事务,是不是能够更好的利用CPU的资源呢?

因此:转移数据(尤其是转移大量数据)是可以不需要CPU参与。比如希望外设A的数据拷贝到外设B,只要给两种外设提供一条数据通路,直接让数据由A拷贝到B 不经过CPU的处理
在这里插入图片描述
 DMA就是基于以上设想设计的,它的作用就是解决大量数据转移过度消耗CPU资源的问题。有了DMA使CPU更专注于更加实用的操作–计算、控制等。

DMA定义:

DMA用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须CPU的干预,通过DMA数据可以快速地移动。这就节省了CPU的资源来做其他操作。

DMA传输方式

DMA的作用就是实现数据的直接传输,而去掉了传统数据传输需要CPU寄存器参与的环节,主要涉及四种情况的数据传输,但本质上是一样的,都是从内存的某一区域传输到内存的另一区域(外设的数据寄存器本质上就是内存的一个存储单元)。四种情况的数据传输如下:

  • 外设到内存
  • 内存到外设
  • 内存到内存
  • 外设到外设

DMA传输参数

我们知道,数据传输,首先需要的是1 数据的源地址 2 数据传输位置的目标地址 ,3 传递数据多少的数据传输量 ,4 进行多少次传输的传输模式 DMA所需要的核心参数,便是这四个

当用户将参数设置好,主要涉及源地址、目标地址、传输数据量这三个,DMA控制器就会启动数据传输,当剩余传输数据量为0时 达到传输终点,结束DMA传输 ,当然,DMA 还有循环传输模式 当到达传输终点时会重新启动DMA传输。
  
也就是说只要剩余传输数据量不是0,而且DMA是启动状态,那么就会发生数据传输。  
在这里插入图片描述

DMA的主要特征

每个通道都直接连接专用的硬件DMA请求,每个通道都同样支持软件触发。这些功能通过软件来配置;

  • 在同一个DMA模块上,多个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),优先权设置相等时由硬件决定(请求0优先于请求1,依此类推);
  • 独立数据源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和目标地址必须按数据传输宽度对齐;
  • 支持循环的缓冲器管理;
  • 每个通道都有3个事件标志(DMA半传输、DMA传输完成和DMA传输出错),这3个事件标志逻辑或成为一个单独的中断请求;
  • 存储器和存储器间的传输、外设和存储器、存储器和外设之间的传输;
  • 闪存、SRAM、外设的SRAM、APB1、APB2和AHB外设均可作为访问的源和目标;
  • 可编程的数据传输数目:最大为65535。

STM32少个DMA资源?

对于大容量的STM32芯片有2个DMA控制器 两个DMA控制器,DMA1有7个通道,DMA2有5个通道。
每个通道都可以配置一些外设的地址。

①DMA1 controller

从外设(TIMx[x=1、2、3、4]、ADC1、SPI1、SPI/I2S2、I2Cx[x=1、2]和USARTx[x=1、2、3])产生的7个DMA请求,通过逻辑或输入到DMA1控制器 其中每个通道都对应着具体的外设:
在这里插入图片描述
在这里插入图片描述
② DMA2 controller

从外设(TIMx[5、6、7、8]、ADC3、SPI/I2S3、UART4、DAC通道1、2和SDIO)产生的5个请求,经逻辑或输入到DMA2控制器,其中每个通道都对应着具体的外设:
在这里插入图片描述
在这里插入图片描述

这些在下方系统框图中也可以清晰地看到

DMA工作系统框图

在这里插入图片描述
上方的框图,我们可以看到STM32内核,存储器,外设及DMA的连接,这些硬件最终通过各种各样的线连接到总线矩阵中,硬件结构之间的数据转移都经过总线矩阵的协调,使各个外设和谐的使用总线来传输数据。
我们对他来进行一点一点的分析:

下面看有与没有DMA的情况下,ADC采集的数据是怎样存放到SRAM中的?

没有DMA

1.如果没有DMA,CPU传输数据还要以内核作为中转站,比如要将ADC采集的数据转移到到SRAM中,这个过程是这样的:

内核通过DCode经过总线矩阵协调,从获取AHB存储的外设ADC采集的数据,

然后内核再通过DCode经过总线矩阵协调把数据存放到内存SRAM中。

在这里插入图片描述

有DMA传输

有DMA的话,

  1. DMA传输时外设对DMA控制器发出请求。
  2. DMA控制器收到请求,触发DMA工作。
  3. DMA控制器从AHB外设获取ADC采集的数据,存储到DMA通道中
  4. DMA控制器的DMA总线与总线矩阵协调,使用AHB把外设ADC采集的数据经由DMA通道存放到SRAM中,这个数据的传输过程中,完全不需要内核的参与,也就是不需要CPU的参与,

在这里插入图片描述

我们把上面的步骤专业一点介绍:

在发生一个事件后,外设向DMA控制器发送一个请求信号。DMA控制器根据通道的优先权处理请求。当DMA控制器开始访问发出请求的外设时,DMA控制器立即发送给它一个应答信号。当从DMA控制器得到应答信号时,外设立即释放它的请求。一旦外设释放了这个请求,DMA控制器同时撤销应答信号。DMA传输结束,如果有更多的请求时,外设可以启动下一个周期。

总之,每次DMA传送由3个操作组成:

  • 从外设数据寄存器或者从当前外设/存储器地址寄存器指示的存储器地址取数据,第一次传输时的开始地址是DMA_CPARx或DMA_CMARx寄存器指定的外设基地址或存储器单元;
  • 存数据到外设数据寄存器或者当前外设/存储器地址寄存器指示的存储器地址,第一次传输时的开始地址是DMA_CPARx或DMA_CMARx寄存器指定的外设基地址或存储器单元;
  • 执行一次DMA_CNDTRx寄存器的递减操作,该寄存器包含未完成的操作数目

DMA传输方式

方法1:DMA_Mode_Normal正常模式,

当一次DMA数据传输完后,停止DMA传送 ,也就是只传输一次
  
方法2:DMA_Mode_Circular循环传输模式

当传输结束时,硬件自动会将传输数据量寄存器进行重装,进行下一轮的数据传输。 也就是多次传输模式

仲裁器

在这里插入图片描述
仲裁器的作用是确定各个DMA传输的优先级

仲裁器根据通道请求的优先级来启动外设/存储器的访问。

优先权管理分2个阶段:

软件:每个通道的优先权可以在DMA_CCRx寄存器中设置,有4个等级:

  • 最高优先级
  • 高优先级
  • 中等优先级
  • 低优先级;

硬件:如果2个请求有相同的软件优先级,则较低编号的通道比较高编号的通道有较高的优先权。比如:如果软件优先级相同,通道2优先于通道4。

注意: 在大容量产品和互联型产品中,DMA1控制器拥有高于DMA2控制器的优先级。

指针递增模式

根据 DMA_SxCR 寄存器中 PINC 和 MINC 位的状态,外设和存储器指针在每次传输后可以自动向后递增或保持常量。当设置为增量模式时,下一个要传输的地址将是前一个地址加上增量值

通过单个寄存器访问外设源或目标数据时,禁止递增模式十分有用。

如果使能了递增模式,则根据在 DMA_SxCR 寄存器 PSIZE 或 MSIZE 位中编程的数据宽度,下一次传输的地址将是前一次传输的地址递增 1个数据宽度、2个数据宽度或 4个数据宽度。

存储器到存储器模式

DMA通道的操作可以在没有外设请求的情况下进行,这种操作就是存储器到存储器模式。

当设置了DMA_CCRx寄存器中的MEM2MEM位之后,在软件设置了DMA_CCRx寄存器中的EN位启动DMA通道时,DMA传输将马上开始。当DMA_CNDTRx寄存器变为0时,DMA传输结束。存储器到存储器模式不能与循环模式同时使用。

这里要注意仅 DMA2 的外设接口可以访问存储器,所以仅 DMA2 控制器支持存储器到存储器的传输,DMA1 不支持。

存储器到存储器模式不能与循环模式同时使用。

DMA中断

每个DMA通道都可以在DMA传输过半、传输完成和传输错误时产生中断。为应用的灵活性考虑,通过设置寄存器的不同位来打开这些中断。

在这里插入图片描述
使没开启,我们也可以通过查询这些位来获得当前 DMA 传输的状态。这里我们常用的是 TCIFx位,即数据流 x 的 DMA 传输完成与否标志。

DMA库函数配置过程:

1、使能DMA时钟:RCC_AHBPeriphClockCmd();

2、初始化DMA通道:DMA_Init();

//设置通道;传输地址;传输方向;传输数据的数目;传输数据宽度;传输模式;优先级;是否开启存储器到存储器。

3、使能外设DMA;

4、使能DMA通道传输;

5、查询DMA传输状态。

关于DMA的介绍我们仅介绍到这里,如果需要更详细的了解DMA原理 DMA寄存器以及库函数 可以参考这篇文章
《【STM32】 DMA原理,步骤超细详解,一文看懂DMA》




下面我们将介绍CubeMx 如何创建DMA

具体流程如下:
在这里插入图片描述
我们以USART1 的DMA传输为例

工程创建

1设置RCC
在这里插入图片描述
设置高速外部时钟HSE 选择外部时钟源

2设置串口
在这里插入图片描述

  • 1点击USATR1
  • 2设置MODE为异步通信(Asynchronous)
  • 3基础参数:波特率为115200 Bits/s。传输数据长度为8 Bit。奇偶检验无,停止位1 接收和发送都使能
  • 4GPIO引脚自动设置 USART1_RX/USART_TX
  • 5 NVIC Settings 一栏使能接收中断
    ​​
    在这里插入图片描述
    关于串口部分的讲解可以参考: 【STM32】HAL库 STM32CubeMX教程四—UART串口通信详解

3 DMA设置

在这里插入图片描述
根据DMA通道预览可以知道,我们用的USART1 的TX RX 分别对应DMA1 的通道4和通道5

  • 点击DMASettings 点击 Add 添加通道
  • 选择USART_RX USART_TX 传输速率设置为中速
  • DMA传输模式为正常模式
  • DMA内存地址自增,每次增加一个Byte(字节)

1DMA基础设置

右侧点击System Core 点击DMA
在这里插入图片描述
DMA RequestDMA传输的对应外设

注意: 如果你是在DMA设置界面添加DMA 而没有开启对应外设的话 ,默认为MENTOMEN

Channel DMA传输通道设置
DMA1 : DMA1 Channel 0~DMA1 Channel 7
DMA2: DMA2 Channel 1~DMA1 Channel 5

Dirction : DMA传输方向
四种传输方向:

  • 外设到内存 Peripheral To Memory
  • 内存到外设 Memory To Peripheral
  • 内存到内存 Memory To Memory
  • 外设到外设 Peripheral To Peripheral

Priority: 传输速度

  • 最高优先级 Very Hight
  • 高优先级 Hight
  • 中等优先级 Medium
  • 低优先级;Low

2DMA传输模式
在这里插入图片描述
Normal:正常模式
当一次DMA数据传输完后,停止DMA传送 ,也就是只传输一次

Circular: 循环模式

传输完成后又重新开始继续传输,不断循环永不停止

3DMA指针递增设置
在这里插入图片描述
Increment Address:地址指针递增(上方有介绍)。

左侧Src Memory 表示外设地址寄存器

功能:设置传输数据的时候外设地址是不变还是递增。如果设置 为递增,那么下一次传输的时候地址加 Data Width个字节,

右侧Dst Memory 表示内存地址寄存器

功能:设置传输数据时候内存地址是否递增。如果设置 为递增,那么下一次传输的时候地址加 Data Width个字节,

这个Src Memory一样,只不过针对的是内存。


串口发送数据是将数据不断存进固定外设地址串口的发送数据寄存器(USARTx_TDR)。所以外设的地址是不递增。

而内存储器存储的是要发送的数据,所以地址指针要递增,保证数据依次被发出

在这里插入图片描述

串口数据发送寄存器只能存储8bit,每次发送一个字节,所以数据长度选择Byte。在这里插入图片描述

就是要注意DMA的传输方向别弄错了,到底是PERIPHERIAL到MEMORY还是MEMORY到PERIPHERIAL或者说是Memory到Memory要配置正确。尤其是在用CubeMx配置时,这里有个默认配置是PERIPHERIAL到MEMORY。如果说你的真实意图根本不是从PERIPHERIAL到MEMORY,而你无意中使用了这个默认配置,结果可想而知,DMA传输根本没法正常运行。

4时钟源设置

​​​​在这里插入图片描述
我的是 外部晶振为8MHz

  • 1选择外部时钟HSE 8MHz
  • 2PLL锁相环倍频9倍
  • 3系统时钟来源选择为PLL
  • 4设置APB1分频器为 /2
  • 5 使能CSS监视时钟

32的时钟树框图 如果不懂的话请看《【STM32】系统时钟RCC详解(超详细,超全面)》

5项目文件设置

在这里插入图片描述

  • 1 设置项目名称
  • 2 设置存储路径
  • 3 选择所用IDE

在这里插入图片描述
5创建工程文件

然后点击GENERATE CODE 创建工程

配置下载工具
新建的工程所有配置都是默认的 我们需要自行选择下载模式,勾选上下载后复位运行

在这里插入图片描述

测试例程1

在main.C中添加:

 /* USER CODE BEGIN Init */
	uint8_t Senbuff[] = "\r\n**** Serial Output Message by DMA ***\r\n   UART DMA Test \r\n   Zxiaoxuan";  //定义数据发送数组
  /* USER CODE END Init */
  • 1
  • 2
  • 3

while循环:

  while (1)
  {
    /* USER CODE END WHILE */
			HAL_UART_Transmit_DMA(&huart1, (uint8_t *)Senbuff, sizeof(Senbuff));
	        HAL_Delay(1000);
    /* USER CODE BEGIN 3 */
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

串口助手测试正常:
在这里插入图片描述

注意:如果不开启串口中断,则程序只能发送一次数据,程序不能判断DMA传输是否完成,USART一直处于busy状态。

HAL库UARTDMA函数库介绍

1、串口发送/接收函数

  • HAL_UART_Transmit();串口发送数据,使用超时管理机制
  • HAL_UART_Receive();串口接收数据,使用超时管理机制
  • HAL_UART_Transmit_IT();串口中断模式发送
  • HAL_UART_Receive_IT();串口中断模式接收
  • HAL_UART_Transmit_DMA();串口DMA模式发送
  • HAL_UART_Transmit_DMA();串口DMA模式接收
  • HAL_UART_DMAPause() 暂停串口DMA
  • HAL_UART_DMAResume(); 恢复串口DMA
  • HAL_UART_DMAStop(); 结束串口DMA

因为这部分函数在讲解USART的时候就已经讲解了,所以我们这里不做过多介绍,如果不同的话请看UART的对应博文,很详细。

串口DMA发送数据

 HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
  • 1

功能:串口通过DMA发送指定长度的数据。

参数:

  • UART_HandleTypeDef *huart UATR的别名 如 : UART_HandleTypeDef huart1; 别名就是huart1
  • *pData 需要发送的数据
  • Size 发送的字节数

举例:

HAL_UART_Transmit_DMA(&huart1, (uint8_t *)Senbuff, sizeof(Senbuff));  //串口发送Senbuff数组
  • 1

串口DMA接收数据

HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
  • 1

功能:串口通过DMA接受指定长度的数据。

参数:

  • UART_HandleTypeDef *huart UATR的别名 如 : UART_HandleTypeDef huart1; 别名就是huart1
  • *pData 需要存放接收数据的数组
  • Size 接受的字节数

举例:

HAL_UART_Transmit_DMA(&huart1, (uint8_t *)Recbuff, sizeof(Recbuff));  //串口发送Senbuff数组
  • 1

串口DMA恢复函数

HAL_UART_DMAResume(&huart1)
  • 1

作用: 恢复DMA的传输

返回值: 0 正在恢复 1 完成DMA恢复

测试例程2

STM32 IDLE 接收空闲中断

STM32的IDLE的中断产生条件:在串口无数据接收的情况下,不会产生,当清除IDLE标志位后,必须有接收到第一个数据后,才开始触发,一但接收的数据断流,没有接收到数据,即产生IDLE中断

本例程功能:

使用DMA+串口接受空闲中断 实现将接收的数据完整发送到上位机的功能

例程代码:

uart.c

volatile uint8_t rx_len = 0;  //接收一帧数据的长度
volatile uint8_t recv_end_flag = 0; //一帧数据接收完成标志
uint8_t rx_buffer[100]={0};  //接收数据缓存数组
  • 1
  • 2
  • 3
void MX_USART1_UART_Init(void)
{

huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
//下方为自己添加的代码
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); //使能IDLE中断

//DMA接收函数,此句一定要加,不加接收不到第一次传进来的实数据,是空的,且此时接收到的数据长度为缓存器的数据长度
HAL_UART_Receive_DMA(&huart1,rx_buffer,BUFFER_SIZE);

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

uart.h

extern UART_HandleTypeDef huart1;
extern DMA_HandleTypeDef hdma_usart1_rx;
extern DMA_HandleTypeDef hdma_usart1_tx;
/* USER CODE BEGIN Private defines */

#define BUFFER_SIZE 100
extern volatile uint8_t rx_len ; //接收一帧数据的长度
extern volatile uint8_t recv_end_flag; //一帧数据接收完成标志
extern uint8_t rx_buffer[100]; //接收数据缓存数组

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

main.c

/*
*********************************************************************************************************
* 函 数 名: DMA_Usart_Send
* 功能说明: 串口发送功能函数
* 形  参: buf,len
* 返 回 值: 无
*********************************************************************************************************
*/
void DMA_Usart_Send(uint8_t *buf,uint8_t len)//串口发送封装
{
 if(HAL_UART_Transmit_DMA(&huart1, buf,len)!= HAL_OK) //判断是否发送正常,如果出现异常则进入异常中断函数
  {
   Error_Handler();
  }

}

/*


  • 函 数 名: DMA_Usart1_Read
  • 功能说明: 串口接收功能函数
  • 形 参: Data,len
  • 返 回 值: 无

/
void DMA_Usart1_Read(uint8_t
Data,uint8_t len)//串口接收封装
{
HAL_UART_Receive_DMA(&huart1,Data,len);//重新打开DMA接收
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

while循环

 while (1)
  {
    /* USER CODE END WHILE */
<span class="token comment">/* USER CODE BEGIN 3 */</span>
	 <span class="token keyword">if</span><span class="token punctuation">(</span>recv_end_flag <span class="token operator">==</span> <span class="token number">1</span><span class="token punctuation">)</span>  <span class="token comment">//接收完成标志</span>
	<span class="token punctuation">{<!-- --></span>
		
		
		<span class="token function">DMA_Usart_Send</span><span class="token punctuation">(</span>rx_buffer<span class="token punctuation">,</span> rx_len<span class="token punctuation">)</span><span class="token punctuation">;</span>
		rx_len <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span><span class="token comment">//清除计数</span>
		recv_end_flag <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span><span class="token comment">//清除接收结束标志位</span>

// for(uint8_t i=0;i<rx_len;i++)
// {
// rx_buffer[i]=0;//清接收缓存
// }
memset(rx_buffer,0,rx_len);
}
HAL_UART_Receive_DMA(&huart1,rx_buffer,BUFFER_SIZE);//重新打开DMA接收
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

stm32f1xx_it.c中

#include "usart.h"

void USART1_IRQHandler(void)
{
uint32_t tmp_flag = 0;
uint32_t temp;
tmp_flag =__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE); //获取IDLE标志位
if((tmp_flag != RESET))//idle标志被置位
{
__HAL_UART_CLEAR_IDLEFLAG(&huart1);//清除标志位
//temp = huart1.Instance->SR; //清除状态寄存器SR,读取SR寄存器可以实现清除SR寄存器的功能
//temp = huart1.Instance->DR; //读取数据寄存器中的数据
//这两句和上面那句等效
HAL_UART_DMAStop(&huart1); //
temp = __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);// 获取DMA中未传输的数据个数
//temp = hdma_usart1_rx.Instance->NDTR;//读取NDTR寄存器 获取DMA中未传输的数据个数,
//这句和上面那句等效
rx_len = BUFFER_SIZE - temp; //总计数减去未传输的数据个数,得到已经接收的数据个数
recv_end_flag = 1; // 接受完成标志位置1
}
HAL_UART_IRQHandler(&huart1);

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

测试正常:
在这里插入图片描述

完整例程下载:ZXiaoxuanSTM32-DMA-IDLE

历程详解

因为本文章已经一万余字了,足足写了好几天,翻手册也翻了好久,所以我们将本例程的详解分到另一篇文章中去,使整体结构更好一点,也方便大家阅读

正在码字中

详解包括:

  • 中断原理讲解
  • 例程流程详解
  • 库函数分析详解
  • 对应寄存器介绍
  • 对应函数介绍

已经更新,请参看:
STM32 HAL CubeMX 串口IDLE接收空闲中断+DMA
在这里插入图片描述

正在码字中…在这里插入图片描述

posted @ 2023-07-21 08:38  SymPny  阅读(4236)  评论(0编辑  收藏  举报