【串口系列】不定长接收多种方式

目录

背景

声明

开发环境

正文

一、接收中断 + 空闲中断

二、接收中断 + T35定时器中断

T35定时器

三、空闲中断 + DMA + 循环队列


背景

         在单片机开发过程中,串口通讯是一种非常常用的串行通通讯方式,如调试、协议通信、模组驱动等都有大量的应用,而针对串口接收从技术角度可分定长数据接收及不定长数据接收。针对定长数据接收,可使用特定的起始和结束符以及长度进行帧识别,或其它有效的方式;而针对不定长数据接收为本文讨论的重点,起始不定长数据接收已包含了定长数据,因此掌握不定长数据接收是串口编程的重中之重,本文将使用3种方式由浅入深进行讨论,希望能带来一些收获。

声明

本文叙述的4中方式及代码并非适用于所有内核,仅供思路参考,实际应用中需自行验证可行性。

开发环境

主控

STM32F103VET6

IDE

KEIL

正文

         在单片机接收不定长数据时,重点是如何判断一帧数据接收完成,并能够将这一帧数据的长度和数据缓存下来,那么我们要的功能就实现了。下面通过网络分享的资源及工作时的经验,总结出以下几种思路实现供不同实际应用场合应用。

一、接收中断 + 空闲中断

    STM32单片机串口实现了空闲帧检测功能,配合接收缓冲区非空格中断,当一帧数据接收时,利用接收缓冲区非空中断将一个一个字节的数据存入我们自定义的缓存区,并维护一个变量记录帧长度。当空闲中断发生时,意味着一帧的数据已经结束,通过标志置位即可判断这一帧已经结束。

优点:在自动识别帧数据,无需主程序参与

缺点:1. 针对其它厂家或其它型号单片机,不一定支持空闲中断功能

             2. 在串口速率较高时,会导致频繁进入串口中断(接收中断)

部分程序:

/* 变量定义 */
#define DATA_RECV_MAXSIZE (128)
typedef struct data_recv_st
{
     uint8_t buff[DATA_RECV_MAXSIZE] //uint8_t* buff;
     uint16_t len;
     uint8_t  flag;
}data_recv_t;
data_recv_t data_recv;

uint8_t at_rxbuffer[1];
	
/* 串口初始化 */
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_Receive_IT(&huart1,aRxBuffer1,1);   // 开启接收中断
	__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); // 开启空闲中断
}

/* 中断函数处理 */
/**
  * @brief This function handles USART1 global interrupt / USART1 wake-up interrupt through EXTI line 25.
  */
void USART1_IRQHandler(void)
{
  if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_ORE) != RESET ) {
        __HAL_UART_CLEAR_OREFLAG(&huart1);
    }
    if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_NE) != RESET ) {
        __HAL_UART_CLEAR_NEFLAG(&huart1);
    }
    if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_FE) != RESET) {
        __HAL_UART_CLEAR_FEFLAG(&huart1);
    }
    if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_PE) != RESET) {
        __HAL_UART_CLEAR_PEFLAG(&huart1);
    }
    if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE) != RESET)
    {
        if(data_recv.len < DATA_RECV_MAXSIZE) {
            data_recv.buff[data_recv.len++] = USART1->RDR;
        } else  {
            USART1->RDR;
        }
    }
    if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET) 
    {
	   __HAL_UART_CLEAR_IDLEFLAG(&huart1);
        data_recv.flag = 1;
    }
}


二、接收中断 + T35定时器中断

         我们已经实现了接收中断 + 空闲中断的方式,但是这种方式有一个缺点,有些单片机不支持空闲中断。先回到第一种方式,STM32单片机使用空闲中断检测帧是如何实现的呢?通过判断一个字节时间内是否有接收到数据。那么我们可以人为实现,通过一个基本定时器,当接收到新字节时刷新定时器,如果没有接收到新字节,那么定时器在设定的时间后会出现更新中断,在进入这个中断后我们就可以认为一帧数据已经结束了。那么我们如何实现这个定时器呢?在modbus协议中采用了这种方式,并且有个通用的叫法:T35定时器。

T35定时器

    参考链接:https://www.cnblogs.com/mrsandstorm/p/5701867.html

   波特率:每秒钟通过信道传输的信息量称为位传输速率,也就是每秒钟传送的二进制位数,简称比特率。比特率表示有效数据的传输速率,用b/s 、bit/s、比特/秒,读作:比特每秒。如9600b/s:指总线上每秒可以传输9600个bit;

    若按照 1字节起始位 + 8字节数据位 + 2字节停止位 = 11位,那么在9600bps波特率下,可传输 9600 / 11 = 872.7273 个字节,那么一个1字节传输耗费的时间为 1 / 872.7273 = 0.0011s ,按照3.5个桢长度为超时时间计算,超时时间: 0.0011 * 3.5 = 0.0039 s,那么对于50us定时器来说,需要设置的定时周期为 0.0039s / 50us = 7.8个周期

通用计算公式:

3.5 * (1 / ( Baudrate / 11)  * 100000) / 50

  -> 3.5 * (1100000 / Baudrate) / 50

-> 3.5 * 220000 / Baudrate

     -> 7 * 220000 / (Baudrate * 2)

T35定时器实现

         基本定时器使用50us的时基,按照不同的波特率计算出需要n个50us,那么定时器的超时时间为:n*50 us

static void eMBHwInit(u32 ulBaudRate)

{

    u32 usTimerT35_50us;

    /* If baudrate > 19200 then we should use the fixed timer values

     * t35 = 1750us. Otherwise t35 must be 3.5 times the character time.

     */

    if( ulBaudRate > 19200 )

    {

        usTimerT35_50us = 100;       /* 5000us. */

    }

    else

    {

        /* The timer reload value for a character is given by:

         *

         * ChTimeValue = Ticks_per_1s / ( Baudrate / 11 )

         *             = 11 * Ticks_per_1s / Baudrate

         *             = 220000 / Baudrate

         * The reload for t3.5 is 1.5 times this value and similary

         * for t3.5.

         */

        usTimerT35_50us = ( 7UL * 220000UL ) / ( 2UL * ulBaudRate );

    }

    xMBPortTimersInit((u16)usTimerT35_50us);

}

优点:在自动识别帧数据,无需主程序参与

缺点:1. 需另外增加一个软件定时器实现T35定时器。

           2. 在串口速率较高时,会导致频繁进入串口中断(接收中断)

程序:

         该方式与第一种实现类似,第一种在空闲中断中处理的内容需转至定时器超时中断中进行并在串口中使能接收中断。

static void xMBPortTimersInit(u16 usTim1Timerout50us)
{
    TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
    u16 PrescalerValue = 0;
    TIM_RCC;
    //定时器时间基配置说明
    //HCLK为72MHz,APB1经过2分频为36MHz
    //TIM4的时钟倍频后为72MHz(硬件自动倍频,达到最大)
    //TIM4的分频系数为3599,时间基频率为72 / (1 + Prescaler) = 20KHz,基准为50us
    //TIM最大计数值为usTim1Timerout50u
    PrescalerValue = (u16) (SystemCoreClock / 20000) - 1; 
    //定时器1初始化
    TIM_TimeBaseStructure.TIM_Period = (uint16_t) usTim1Timerout50us;
    TIM_TimeBaseStructure.TIM_Prescaler = PrescalerValue;
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM, &TIM_TimeBaseStructure);
    //预装载使能
    TIM_ARRPreloadConfig(TIM, ENABLE);
    
    TIM_ClearITPendingBit(TIM,TIM_IT_Update);
    
    TIM_ITConfig(TIM, TIM_IT_Update, DISABLE);
    TIM_Cmd(TIM,  DISABLE);
}

static void eMBHwStart(void)
{
    TIM_ClearITPendingBit(TIM, TIM_IT_Update);
    TIM_ITConfig(TIM, TIM_IT_Update, DISABLE);
    TIM_SetCounter(TIM,0x0000);
    TIM_Cmd(TIM, DISABLE);
}

void uart_T35Init(u32 ulBaudRate)
{
    eMBHwInit(ulBaudRate);
    eMBHwStart();
}

三、空闲中断 + DMA + 循环队列

         在前面两种中,都有共同的缺点:在速率较高,数据量较大时会频繁进入串口中断,如果在中断中不及时处理会导致ORE错误。那么有没有办法能够解决这个问题呢?答案肯定是有的。目前的问题是接收中断导致的问题,如果不使用接收中断,是不是可以解决问题。在STM32中可以使用DMA解决,DMA实现从外设到内存的传输,这样MCU可以在主程序中处理其它任务,而且不会进入接收中断。但是有个问题,DMA传输大数据量时如果只有一个缓存区,那么必然存在数据的拷贝,因为这个缓存区需要接收新的数据,旧的帧数据必须拷贝到其它数据中保存好。这就会引入另外一个问题:https://blog.csdn.net/qq_20999867/article/details/92961110

        那么我考虑的是使用一个队列,DMA每次接收完一个缓存区后去获取下一个缓存区的地址循环接收,并且维护一个头尾节点保证设备读取正常。

优点:效率较高,支持高速率传输

缺点:实现复杂

Lib_queue.c

#include "lib_queue.h"

#include <string.h>

/**

 * @description: 创建队列

 * @return  {*} 0: 成功 -1: 失败

 * @param {data_queue_t} *queue: 队列对象

 * @param {void} *buf: 队列缓存数组指针

 * @param {unsigned short} queue_len: 队列长度

 * @param {unsigned short} itemsize: 队列单个元素大小,以字节为单位

 */

int data_queue_init(data_queue_t *queue, data_frame_complete_callback_t callback)

{

    assert_param_queue(queue, return queue_false);

    memset(queue, 0, sizeof(data_queue_t));

    queue->front = queue->rear = 0;

    queue->pop_state = QUEUE_STATE_UNLOCKED;

    queue->push_state = QUEUE_STATE_UNLOCKED;

    queue->len = DATA_QUEUE_LEN;

    queue->complete_callback = callback;

    return queue_true;

}

/**

 * @description: 获取队列已存放个数

 * @return  {*}

 * @param {data_queue_t} *queue

 */

int data_queue_use_block(data_queue_t *queue)

{

    assert_param_queue(queue, return queue_false);

    if (queue->front < queue->rear)

    {

        return (queue->front + queue->len - queue->rear);

    }

    else

    {

        return (queue->front - queue->rear);

    }

}

/**

 * @description: 获取剩余队列数据个数

 * @return  {*}

 * @param {data_queue_t} *queue

 */

int data_queue_remain_block(data_queue_t *queue)

{

    assert_param_queue(queue, return queue_false);

    if (queue->front < queue->rear)

    {

        return (queue->rear - queue->front) - 1;

    }

    else

    {

        return (queue->len - (queue->front - queue->rear)) - 1;

    }

}

/**

 * @description: 获取队列总数据个数

 * @return  {*}

 * @param {data_queue_t} *queue

 */

int data_queue_total_block(data_queue_t *queue)

{

    assert_param_queue(queue, return queue_false);

    return (queue->len - 1);

}

/**

 * @description: 判断数据是否已满

 * @return  满: queue_true 不满:queue_false

 * @param {data_queue_t} *queue

 */

int data_queue_is_full(data_queue_t *queue)

{

    assert_param_queue(queue, return queue_false);

    if (data_queue_use_block(queue) == data_queue_total_block(queue))

    {

        return queue_true;

    }

    return queue_false;

}

/**

 * @description: 判断数据是否已空

 * @return  空: queue_true 非空:queue_false

 * @param {data_queue_t} *queue

 */

int data_queue_is_empty(data_queue_t *queue)

{

    assert_param_queue(queue, return queue_false);

    if (data_queue_use_block(queue) == 0)

    {

        return queue_true;

    }

    return queue_false;

}

/**

 * @description: 获取队列空闲块存储地址,获取存储地址后配合DMA或其他方式使用

 * @return  {*}

 * @param {data_queue_t} *queue

 */

data_packet_t *data_queue_get_idleblock_addr(data_queue_t *queue)

{

    assert_param_queue(queue, return NULL);

    if (queue->push_state == QUEUE_STATE_LOCKED)

    {

        return NULL;

    }

    if (data_queue_is_full(queue) == queue_true)

    {

        return NULL;

    }

    queue->push_state = QUEUE_STATE_LOCKED;

    return (&queue->data_packet[queue->front]);

}

/**

 * @description: 数据接收完成,推入队列,实际数据已存在队列中,但需处理头计数

 * @return  {*}

 * @param {data_queue_t} *queue

 */

int data_queue_push(data_queue_t *queue)

{

    assert_param_queue(queue, return queue_false);

    if (queue->push_state != QUEUE_STATE_LOCKED)

    {

        return queue_false;

    }

    queue->front++;

    queue->front = queue->front % queue->len;

    if (queue->complete_callback != NULL)

    {

        queue->complete_callback();

    }

    queue->push_state = QUEUE_STATE_UNLOCKED;

    return queue_true;

}

/**

 * @description: 获取队列数据,并处理尾计数

 * @return  {*}

 * @param {data_queue_t} *queue

 */

data_packet_t *data_queue_pop(data_queue_t *queue)

{

    data_packet_t *p_data_pack = NULL;

    assert_param_queue(queue, return NULL);

    if (data_queue_is_empty(queue) == queue_true)

    {

        return NULL;

    }

    if (queue->pop_state == QUEUE_STATE_LOCKED)

    {

        return NULL;

    }

    queue->pop_state = QUEUE_STATE_LOCKED;

    p_data_pack = &queue->data_packet[queue->rear];

    queue->rear++;

    queue->rear = queue->rear % queue->len;

    queue->pop_state = QUEUE_STATE_UNLOCKED;

    return p_data_pack;

}

/**

  * @brief  Reports the name of the source file and the source line number

  *         where the assert_param error has occurred.

  * @param  file: pointer to the source file name

  * @param  line: assert_param error line source number

  * @retval None

  */

void assert_failed_queue(unsigned char *file, unsigned int line)

{

#ifdef PRINTE

    printf("Wrong parameters value: file %s on line %d\r\n", file, line);

#endif

}

Lib_queue.h

#ifndef __LIB_QUEUE_H

#define __LIB_QUEUE_H

#define queue_true    (0)

#define queue_false   (-1)

#ifndef NULL

#define NULL   ((void*)0)

#endif

void assert_failed_queue(unsigned char *file, unsigned int line);

#ifndef assert_param_queue

#define assert_param_queue(expr, action)   {if(expr == 0) { assert_failed_queue((unsigned char  *)__FILE__, __LINE__);action; }}

#endif

#define  DATA_PACKET_SIZE          (128)

#define  DATA_QUEUE_LEN            (10)

typedef void (*data_frame_complete_callback_t)(void);

typedef enum data_queue_lockstate_et

{

    QUEUE_STATE_UNLOCKED,

    QUEUE_STATE_LOCKED,

}data_queue_lockstate_t;

typedef struct  data_packet_st

{

    unsigned short len;

    unsigned char data[DATA_PACKET_SIZE + 4];

}data_packet_t;

typedef struct  data_queue_st

{

    data_packet_t data_packet[DATA_QUEUE_LEN];

    unsigned short front;           // 数据头,指向下一个空闲存放地址

    unsigned short rear;            // 数据尾,指向第一个数据

    unsigned short len;

    data_queue_lockstate_t  push_state;

    data_queue_lockstate_t  pop_state;

    data_frame_complete_callback_t complete_callback;

}data_queue_t;

int data_queue_init(data_queue_t *queue, data_frame_complete_callback_t callback);

int data_queue_push(data_queue_t *queue);

data_packet_t *data_queue_get_idleblock_addr(data_queue_t *queue);

data_packet_t * data_queue_pop(data_queue_t *queue);

int data_queue_use_block(data_queue_t *queue);

int data_queue_remain_block(data_queue_t *queue);

int data_queue_total_block(data_queue_t *queue);



#endif

串口初始化

data_queue_t data_queue_recive;

data_packet_t *g_cur_data_packet = NULL;



void MX_USART1_UART_Init(void)

{

    /* USER CODE BEGIN USART1_Init 0 */

    /* USER CODE END USART1_Init 0 */

    /* USER CODE BEGIN USART1_Init 1 */

    /* USER CODE END USART1_Init 1 */

    huart1.Instance = USART1;

    huart1.Init.BaudRate = 2000000;

    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();

    }

    /* USER CODE BEGIN USART1_Init 2 */

    extern void frame_complete_callback(void);

    data_queue_init(&data_queue_recive, frame_complete_callback);

    g_cur_data_packet = data_queue_get_idleblock_addr(&data_queue_recive);

    HAL_UART_Receive_DMA(&huart1, g_cur_data_packet->data, DATA_PACKET_SIZE);

    __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); //open idle interrupt

    /* USER CODE END USART1_Init 2 */

}

中断函数处理

/**

  * @brief This function handles USART1 global interrupt.

  */

void USART1_IRQHandler(void)

{

    /* USER CODE BEGIN USART1_IRQn 0 */

    /* USER CODE END USART1_IRQn 0 */

    HAL_UART_IRQHandler(&huart1);

    /* USER CODE BEGIN USART1_IRQn 1 */

    if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET)

    {

        __HAL_UART_CLEAR_IDLEFLAG(&huart1);

        HAL_UART_DMAStop(&huart1);

        g_cur_data_packet->len = DATA_PACKET_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);

        DMA1_Channel5->CNDTR = DATA_PACKET_SIZE; //set DMA recive byte

        data_queue_push(&data_queue_recive);

        g_cur_data_packet = data_queue_get_idleblock_addr(&data_queue_recive);

        if (g_cur_data_packet == NULL)

        {

            data_queue_pop(&data_queue_recive);

            g_cur_data_packet = data_queue_get_idleblock_addr(&data_queue_recive);

        }

        

        HAL_UART_Receive_DMA(&huart1, g_cur_data_packet->data, DATA_PACKET_SIZE); //enable DMA

    }

    /* USER CODE END USART1_IRQn 1 */

}


版权声明:本文为CSDN博主「Hi,Mr.Wang」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_34672688/article/details/115473035

分享不易,点个赞再走吧☺☺☺


  • 14
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论
### 回答1: CubeMX是一种用于STM32微控制器的可视化配置工具,可以用来生成代码和配置硬件外设。当我们需要在STM32上使用串口进行不定接收时,可以使用CubeMX来配置相关参数。 首先,在CubeMX中选择对应的STM32系列微控制器型号,然后进入串口配置页面。在该页面中,我们可以选择需要使用的串口(如USART1、USART2等)以及相关的引脚配置。 接下来,在串口配置页面中,我们需要选择接收模式为无中断(Polling)模式或中断(Interrupt)模式。一般情况下,使用中断模式可以更好地处理不定数据接收。 在中断模式下,我们需要配置接收缓冲区的大小。这个缓冲区的大小决定了我们能够一次接收的最大数据度。在CubeMX的串口配置页面中,我们可以设置接收缓冲区的大小,一般建议设置一个合适的大小以适应实际需求。 在代码生成完成后,我们需要编写相应的中断服务程序来处理串口接收中断。当接收数据时,中断服务程序会将数据存放在接收缓冲区中,并根据数据的具体度进行相应的处理。 总结起来,使用CubeMX进行串口不定接收,需要选择合适的串口和引脚配置,并在中断模式下设置接收缓冲区的大小。然后,通过编写中断服务程序来处理接收到的数据。这样,我们可以实现在STM32上进行不定数据串口接收。 ### 回答2: CubeMX 是 STMicroelectronics 公司开发的一款嵌入式软件工具,用于生成和配置 STM32 微控制器的初始化代码。在 CubeMX 中配置串口模块时,可以选择接收不定数据串口通信是一种常用的通信方式,通过串口可以将数据在设备之间进行传输。在接收数据时,不定意味着数据度是不确定的,可能会接收到任意度的数据。 在 CubeMX 中配置串口接收不定数据,首先需要选择串口模块,并设置合适的波特率和数据位数等参数。然后,在接收数据的功能代码中,可以使用中断或轮询的方式接收数据。 使用中断方式,在 CubeMX 中使能串口接收中断,并实现相应的中断处理函数。中断函数可以在接收数据后,根据需要进行处理,比如将数据存储到缓冲区中,或者进行其他操作。 使用轮询方式,在主循环中不断检测串口接收寄存器的状态,如果寄存器中有数据可读,则读取数据并进行处理。这种方式需要注意的是,如果没有数据可读,则可能会导致主循环一直占用 CPU 资源,因此需要设置合适的延时来避免过度占用资源。 总之,通过 CubeMX 配置串口模块接收不定数据,可以选择中断或轮询方式,根据实际需求来实现数据接收和处理。这样可以简化开发过程,提高效率。 ### 回答3: CubeMX是一款强大的嵌入式开发工具,它能够帮助开发者快速配置和生成嵌入式系统的代码框架。在使用CubeMX配置串口时,可以设置串口不定接收不定接收意味着串口可以接收不确定度的数据。具体操作如下: 首先,打开CubeMX并创建一个新的项目。然后选择所需的单片机型号和连接引脚配置。 接下来,在"Configuration"选项卡中,找到"USART"(UART)功能,并将其启用。然后选择所需的串口,例如USART1,并设置波特率等参数。 在"Configuration"选项卡的底部,有一个名为"Advanced Settings"的部分。在这里,可以找到一个名为"DMA Settings"的选项。打开该选项,并将"Rx Transfer Length"配置为不定(即设置为0)。 现在,点击工具栏上的"Project"按钮,并选择"Save"来保存配置。 生成代码后,将在生成的代码中找到相应的ISR(Interrupt Service Routine)函数,例如"USART1_IRQHandler"。这是串口接收中断服务程序的函数名。 在ISR函数中,使用相应的接收函数,例如"HAL_UART_Receive_DMA"函数,来设置接收缓冲区和缓冲区大小。由于我们将接收度设置为不定,因此可以用较大的缓冲区来接收数据。 最后,在主程序中,通过判断接收缓冲区是否有数据到达或是否已接收到完整的数据帧来处理接收数据。 以上是使用CubeMX配置串口接收不定数据的基本步骤。根据具体的开发需求和单片机型号,可能需要进行进一步的设置和配置。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hi,Mr.Wang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值