CC2540的HAL层UART学习笔记

    本文以TI的BLE1.3.2示例工程SimpleBLEPeripheral作关于UART的备忘笔记。

    首先看hal_board_cfg.h,其中有关于底层驱动的默认配置:

/* Driver Configuration */

/* Set to TRUE enable H/W TIMER usage, FALSE disable it */
#ifndef HAL_TIMER
#define HAL_TIMER FALSE
#endif

/* Set to TRUE enable ADC usage, FALSE disable it */
#ifndef HAL_ADC
#define HAL_ADC TRUE
#endif

/* Set to TRUE enable DMA usage, FALSE disable it */
#ifndef HAL_DMA
#define HAL_DMA TRUE
#endif

/* Set to TRUE enable Flash access, FALSE disable it */
#ifndef HAL_FLASH
#define HAL_FLASH TRUE
#endif

/* Set to TRUE enable AES usage, FALSE disable it */
#ifndef HAL_AES
#define HAL_AES TRUE
#endif

#ifndef HAL_AES_DMA
#define HAL_AES_DMA TRUE
#endif

/* Set to TRUE enable LCD usage, FALSE disable it */
#ifndef HAL_LCD
#define HAL_LCD TRUE
#endif

/* Set to TRUE enable LED usage, FALSE disable it */
#ifndef HAL_LED
#define HAL_LED TRUE
#endif
#if (!defined BLINK_LEDS) && (HAL_LED == TRUE)
#define BLINK_LEDS
#endif

/* Set to TRUE enable KEY usage, FALSE disable it */
#ifndef HAL_KEY
#define HAL_KEY TRUE
#endif

/* Set to TRUE enable UART usage, FALSE disable it */
#ifndef HAL_UART
#if (defined ZAPP_P1) || (defined ZAPP_P2) || (defined ZTOOL_P1) || (defined ZTOOL_P2)
#define HAL_UART TRUE
#else
#define HAL_UART FALSE
#endif
#endif

#if HAL_UART
// Always prefer to use DMA over ISR.
#if HAL_DMA
  #ifndef HAL_UART_DMA
    #if (defined ZAPP_P1) || (defined ZTOOL_P1)
      #define HAL_UART_DMA  1
    #elif (defined ZAPP_P2) || (defined ZTOOL_P2)
      #define HAL_UART_DMA  2
    #else
      #define HAL_UART_DMA  1
    #endif
  #endif
  #ifndef HAL_UART_ISR
    #define HAL_UART_ISR  0
  #endif
#else
  #ifndef HAL_UART_ISR
    #if (defined ZAPP_P1) || (defined ZTOOL_P1)
      #define HAL_UART_ISR  1
    #elif (defined ZAPP_P2) || (defined ZTOOL_P2)
      #define HAL_UART_ISR  2
    #else
      #define HAL_UART_ISR  1
    #endif
  #endif
  #ifndef HAL_UART_DMA
    #define HAL_UART_DMA  0
  #endif
#endif

// Used to set P2 priority - USART0 over USART1 if both are defined.
#if ((HAL_UART_DMA == 1) || (HAL_UART_ISR == 1))
#define HAL_UART_PRIPO             0x00
#else
#define HAL_UART_PRIPO             0x40
#endif

#else
#define HAL_UART_DMA  0
#define HAL_UART_ISR  0
#endif

#if !defined HAL_UART_SPI
#define HAL_UART_SPI  0
#endif

#ifdef __cplusplus
}
#endif

#endif /* HAL_BOARD_CFG_H */

    工程中默认的配置是:

INT_HEAP_LEN=3072
HALNODEBUG
OSAL_CBTIMER_NUM_TASKS=1
HAL_AES_DMA=TRUE
HAL_DMA=TRUE
POWER_SAVING
xPLUS_BROADCASTER
HAL_LCD=TRUE
HAL_LED=FALSE

    可知只定义了HAL_DMA=TRUE,根据hal_board_cfg.h文件可知默认下工程并没有使能UART。若要使能UART,需要定义HAL_UART=TRUE。此时,HAL_DMA=TRUE且HAL_UART=TRUE,根据配置文件可知此时宏HAL_UART_DMA的值为1。修改完成后编译,编译器警告:


    原来这个示例工程包含了按键的应用,从配置文件里面我们发现:

/* Set to TRUE enable KEY usage, FALSE disable it */
#ifndef HAL_KEY
#define HAL_KEY TRUE
#endif

    好吧,没办法,只能在编译选项里面定义HAL_KEY=FALSE(因为我不太喜欢动官方的源文件,所以就没有直接在配置文件里面修改)。再次编译,OK!没问题,完美通过。

    下面着重探究下UART在DMA使能下的具体操作。

    前面已经提到,定义HAL_UART=1后,配置文件默认使用UART的DMA方式,IRS中断方式是关闭的,另外可别同时在同一个Port(P0或P1)使能UART和SPI哦!

    实际上,宏HAL_UART_DMA并不是两个值(TRUE or FALSE),而是有3个值(0,1,2)。当HAL_UART_DMA=1时,程序使用UART0的第一引脚配置(Alt 1)。


    工程默认情况下是有低功耗的,在POWER_SAVING已定义的情况下,DMA_PM=1,使能CT脚的下降沿触发中断。CT、RT脚是当UART使用流控制时的引脚,CT脚为输入、RT脚为输出,连接方法是:


   数据手册是这么说的The RTS output is driven low when the receive register is empty and reception is enabled. Transmission of a byte does not occur before the CTS input goes low.

下面将举个例子来理解下cc2540的流控制,当A和B的Uart连接时,在双方没有数据交互时,A、B的RTS均输出低电平,因为他们的接收寄存器都为空。假设A比B先发送数据,那么B收到数据后它的RTS脚输出高(A的CTS为高),此时A停止发送数据,待B的DMA把数据从Uart的接受寄存器提取到上层后(某个内存空间,一般是接收队列),B的RTS重新变为低电平(接收寄存器变为空),此时因为A的CTS为低电平,A继续发送一个字节的数据...如此往复的过程,直至A把数据发送完成,反之亦然。此外,在流控制下,A和B同样是可以实现全双工通讯的。总结下,A发送数据,则和A的CTS(B的RTS)信号有关;B发送数据时则和B的CTS(A的RTS)有关。

    如果我们要使用HAL的UART,则需要做如下设置:

   首先是要设置相应的宏定义:

HAL_UART=TRUE
HAL_KEY=FALSE
    然后,在SimpleBLEPeripheral.c文件包含hal_uart.h头文件,并在SimpleBLEPeripheral_ProcessEvent()函数的SBP_START_DEVICE_EVT事件中添加代码:

	/*
	 *  UART Port open
	 */
	{
	  halUARTCfg_t config;

	  config.baudRate = HAL_UART_BR_115200;
	  config.flowControl = TRUE;
	  config.callBackFunc = uartRxCB;

	  HalUARTOpen(HAL_UART_PORT_0, &config);  /* 打开串口 */
	}

    其中uartRxCB需要自己定义一下,一般声明成静态函数。回调函数的格式要参照typedef void (*halUARTCBack_t) (uint8 port, uint8 event);

    这样就成功打开了串口0,并且使用115200波特率,并且关闭流控制,并将自定义的回调函数uartRxCB()传递给HAL,当有关UART的事件发生后可以进行相应的处理。前面已经提到:UART默认下是使用DMA来对数据继续进行传输的,我们先来了解下UART在DMA方式下的工作原理(想了解CC2540 DMA的工作原理可以点击这里)。

    查看HAL的UART关于DMA的配置代码,UART的TX 和 RX 的DMA通道配置如下:

UART TX通道

通道号             4

SRCADDR            存放待发送数据的首地址

DESTADDR           0x70C1(UART0的发送寄存器地址)

VLEN               0x00 - 使用LEN字段来规定需要传输的字节数

LEN                待发送数据的数量(这里是字节数)

WORDSIZE           设置以字节为单位传输

TMODE              single模式 - 一次传输完成后该DMA通道disarm
TRIG               UART数据发送完毕后触发中断
SRCINC             传输完一字节数据后源地址+1byte(对应于以字节为单位传输)
DESTINC            传输完一字节数据后目的地址不变
IRQMASK            使能DMA中断
M8                 0x00, 使用8个位表示传输动态传输的数据量
PRIORITY           高优先级


UART RX通道

通道号             3

SRCADDR            0x70C1(UART0的接收寄存器地址)

DESTADDR           dmaCfg.rxBuf

VLEN               使用LEN字段来规定需要传输的字节数

LEN                最大传输的数据量,这里是128字节

WORDSIZE           以字(16 bit)为单位传输

TMODE              single-repeated模式,当接收完传输的数据后重新arm该DMA通道
TRIG               UART接收完毕后触发
SRCINC             传输完一字节数据后源地址不变
DESTINC            传输完一字节数据后目的地址+1(16-bit)
IRQMASK            禁止DMA中断
M8                 LEN字段所有比特位用于表示字节数

PRIORITY           高优先级

    

    我们先看一个重要的数据结构uartDMACfg_t:

typedef struct
{
  uint16 rxBuf[HAL_UART_DMA_RX_MAX];
  rxIdx_t rxHead;
  rxIdx_t rxTail;
#if HAL_UART_DMA_IDLE
  uint8 rxTick;
#endif

#if HAL_UART_TX_BY_ISR
  uint8 txBuf[HAL_UART_DMA_TX_MAX];
  volatile txIdx_t txHead;
  txIdx_t txTail;
  uint8 txMT;
#else
  uint8 txBuf[2][HAL_UART_DMA_TX_MAX];
  txIdx_t txIdx[2];
  uint8 txMT;    // Indication that at least one of two Tx buffers are free.
  uint8 txTick;  // ST ticks of delay to allow at least one byte-time at a given baud rate.
  uint8 txTrig;  // Flag indicating that Tx should be manually triggered after txTick expires.

  // Although all of the txVars above are modified by the Tx-done ISR, only this one should need
  // the special volatile consideration by the optimizer (critical sections protect the rest).
  volatile uint8 txSel;
#endif
} uartDMACfg_t;

    我们主要看当宏HAL_UART_TXBY_ISR=0时有效的数据结构成员,其中txBuf有两个:txBuf[0]、txBuf[1],txIdx[0]或txIdx[1]表示对应的Buffer中含有的待发送的数据字节数,txMT表示至少有一个buffer是空的,txTrig表示需要手动触发DMA,txSel这个比较关键,官方代码在操作这个成员的时候也非常小心,txSel指示当前可用的buffer,即未被作为DMA通道发送源的buffer。

    当上层需要发送len个字节的数据时,调用api HalUARTWrite(uint8 port, uint8 *buf, uint16 len),最终执行如下代码:

<span style="font-size:14px;">/******************************************************************************
 * @fn      HalUARTWriteDMA
 *
 * @brief   Write a buffer to the UART, enforcing an all or none policy if the requested length
 *          exceeds the space available.
 *
 * @param   buf - pointer to the buffer that will be written, not freed
 *          len - length of
 *
 * @return  length of the buffer that was sent
 *****************************************************************************/
static uint16 HalUARTWriteDMA(uint8 *buf, uint16 len)
{
#if HAL_UART_TX_BY_ISR
  // Enforce all or none.
  if (HAL_UART_DMA_TX_AVAIL() < len)
  {
    return 0;
  }

  for (uint16 cnt = 0; cnt < len; cnt++)
  {
    dmaCfg.txBuf[dmaCfg.txTail] = *buf++;
    dmaCfg.txMT = 0;

    if (dmaCfg.txTail >= HAL_UART_DMA_TX_MAX-1)
    {
      dmaCfg.txTail = 0;
    }
    else
    {
      dmaCfg.txTail++;
    }

    // Keep re-enabling ISR as it might be keeping up with this loop due to other ints.
    IEN2 |= UTXxIE;
  }
#else
  txIdx_t txIdx;
  uint8 txSel;
  halIntState_t his;

  HAL_ENTER_CRITICAL_SECTION(his);
  txSel = dmaCfg.txSel;
  txIdx = dmaCfg.txIdx[txSel];
  HAL_EXIT_CRITICAL_SECTION(his);

  // Enforce all or none.
  if ((len + txIdx) > HAL_UART_DMA_TX_MAX)
  {
    return 0;
  }

  (void)memcpy(&(dmaCfg.txBuf[txSel][txIdx]), buf, len);

  HAL_ENTER_CRITICAL_SECTION(his);
  /* If an ongoing DMA Tx finished while this buffer was being *appended*, then another DMA Tx
   * will have already been started on this buffer, but it did not include the bytes just appended.
   * Therefore these bytes have to be re-copied to the start of the new working buffer.
   */
  if (txSel != dmaCfg.txSel)
  {
    HAL_EXIT_CRITICAL_SECTION(his);
    txSel ^= 1;

    (void)memcpy(&(dmaCfg.txBuf[txSel][0]), buf, len);
    HAL_ENTER_CRITICAL_SECTION(his);
    dmaCfg.txIdx[txSel] = len;
  }
  else
  {
    dmaCfg.txIdx[txSel] = txIdx + len;
  }

  // If there is no ongoing DMA Tx, then the channel must be armed here.
  if (dmaCfg.txIdx[(txSel ^ 1)] == 0)
  {
    HAL_EXIT_CRITICAL_SECTION(his);
    HalUARTArmTxDMA();
  }
  else
  {
    dmaCfg.txMT = FALSE;
    HAL_EXIT_CRITICAL_SECTION(his);
  }
#endif

  return len;
}</span>

    正常情况下,代码会选择txSel指定的buffer进行数据装填,当DMA通道被ARM以后,txSel被按位取反,即指向另外一个buffer。下面我们来考虑另外一种情况:

    假设buffer[0]正在进行DMA传输(此时txSel=1),buffer[1]已经被写了一些数据(此时txIdx[1]不为0),上层再次请求发送数据时将继续向buffer[1]写数据,但在写txIdx之前,若buffer[0]的DMA传输完成,根据中断代码(后面介绍)将会继续对buffer[1]的数据进行发送,此时上层刚刚写入的数据是无法被发送的,因此只能在buffer[0]重新写数据。见如下代码:

<span style="font-size:14px;">  HAL_ENTER_CRITICAL_SECTION(his);
  /* If an ongoing DMA Tx finished while this buffer was being *appended*, then another DMA Tx
   * will have already been started on this buffer, but it did not include the bytes just appended.
   * Therefore these bytes have to be re-copied to the start of the new working buffer.
   */
  if (txSel != dmaCfg.txSel)
  {
    HAL_EXIT_CRITICAL_SECTION(his);
    txSel ^= 1;

    (void)memcpy(&(dmaCfg.txBuf[txSel][0]), buf, len);
    HAL_ENTER_CRITICAL_SECTION(his);
    dmaCfg.txIdx[txSel] = len;
  }
  else
  {
    dmaCfg.txIdx[txSel] = txIdx + len;
  }</span>

    因为之前已经提过,当UART接收到数据,且DMA完成数据搬移后,DMA是不会触发中断干预CPU的,OSAL会在Hal_ProcessPoll()的HalUARTPoll()的HalUARTPollDMA()对UART的DMA接收BUFFER进行轮询处理,以下是代码:

/******************************************************************************
 * @fn      HalUARTPollDMA
 *
 * @brief   Poll a USART module implemented by DMA, including the hybrid solution in which the Rx
 *          is driven by DMA but the Tx is driven by ISR.
 *
 * @param   none
 *
 * @return  none
 *****************************************************************************/
static void HalUARTPollDMA(void)
{
  uint8 evt = 0;
  uint16 cnt;

#if DMA_PM
  PxIEN &= ~DMA_RDYIn_BIT;  // Clear to not race with DMA_RDY_IN ISR.
  {
    if (dmaRdyIsr || HAL_UART_DMA_RDY_IN() || HalUARTBusyDMA())
    {
      // Master may have timed-out the SRDY asserted state & may need a new edge.
#if HAL_UART_TX_BY_ISR
      if (!HAL_UART_DMA_RDY_IN() && (dmaCfg.txHead != dmaCfg.txTail))
#else
      if (!HAL_UART_DMA_RDY_IN() && ((dmaCfg.txIdx[0] != 0) || (dmaCfg.txIdx[1] != 0)))
#endif
      {
        HAL_UART_DMA_CLR_RDY_OUT();
      }
      dmaRdyIsr = 0;

      if (dmaRdyDly == 0)
      {
        (void)osal_set_event(Hal_TaskID, HAL_PWRMGR_HOLD_EVENT);
      }

      if ((dmaRdyDly = ST0) == 0)  // Reserve zero to signify that the delay expired.
      {
        dmaRdyDly = 0xFF;
      }
      HAL_UART_DMA_SET_RDY_OUT();
    }
    else if ((dmaRdyDly != 0) && (!DMA_PM_DLY || ((uint8)(ST0 - dmaRdyDly) > DMA_PM_DLY)))
    {
      dmaRdyDly = 0;
      (void)osal_set_event(Hal_TaskID, HAL_PWRMGR_CONSERVE_EVENT);
    }
  }
  PxIEN |= DMA_RDYIn_BIT;
#endif

#if !HAL_UART_TX_BY_ISR
  HalUARTPollTxTrigDMA();
#endif

  if (!HAL_UART_DMA_NEW_RX_BYTE(dmaCfg.rxHead))
  {
    if (HAL_UART_DMA_NEW_RX_BYTE(uartRxBug))
    {
      do {
        HAL_UART_RX_IDX_T_INCR(dmaCfg.rxHead);
      } while (!HAL_UART_DMA_NEW_RX_BYTE(dmaCfg.rxHead));

      uartRxBug = dmaCfg.rxHead;
      dmaCfg.rxTail = dmaCfg.rxHead;
    }
    HAL_UART_RX_IDX_T_INCR(uartRxBug);
  }

  cnt = HalUARTRxAvailDMA();  // Wait to call until after the above DMA Rx bug work-around.

#if HAL_UART_DMA_IDLE
  if (dmaCfg.rxTick)
  {
    // Use the LSB of the sleep timer (ST0 must be read first anyway) to measure the Rx timeout.
    if ((ST0 - dmaCfg.rxTick) > HAL_UART_DMA_IDLE)
    {
      dmaCfg.rxTick = 0;
      evt = HAL_UART_RX_TIMEOUT;
    }
  }
  else if (cnt != 0)
  {
    if ((dmaCfg.rxTick = ST0) == 0)  // Zero signifies that the Rx timeout is not running.
    {
      dmaCfg.rxTick = 0xFF;
    }
  }
#else
  if (cnt != 0)
  {
    evt = HAL_UART_RX_TIMEOUT;
  }
#endif

  if (cnt >= HAL_UART_DMA_FULL)
  {
    evt |= HAL_UART_RX_FULL;
  }
  else if (cnt >= HAL_UART_DMA_HIGH)
  {
    evt |= HAL_UART_RX_ABOUT_FULL;

    if (!DMA_PM && (UxUCR & UCR_FLOW))
    {
      HAL_UART_DMA_CLR_RDY_OUT();  // Disable Rx flow.
    }
  }

  if (dmaCfg.txMT)
  {
    dmaCfg.txMT = FALSE;
    evt |= HAL_UART_TX_EMPTY;
  }

  if ((evt != 0) && (dmaCfg.uartCB != NULL))
  {
    dmaCfg.uartCB(HAL_UART_DMA-1, evt);
  }

  if (DMA_PM && (dmaRdyDly == 0) && !HalUARTBusyDMA())
  {
    HAL_UART_DMA_CLR_RDY_OUT();
  }
}

    读这段代码,头有点大了:

  if (!HAL_UART_DMA_NEW_RX_BYTE(dmaCfg.rxHead))
  {
    if (HAL_UART_DMA_NEW_RX_BYTE(uartRxBug))
    {
      do {
        HAL_UART_RX_IDX_T_INCR(dmaCfg.rxHead);
      } while (!HAL_UART_DMA_NEW_RX_BYTE(dmaCfg.rxHead));

      uartRxBug = dmaCfg.rxHead;
      dmaCfg.rxTail = dmaCfg.rxHead;
    }
    HAL_UART_RX_IDX_T_INCR(uartRxBug);
  }

    这段完全没明白,HAL_UART_DMA_NEW_RX_BYTE的宏定义是

#define HAL_UART_DMA_NEW_RX_BYTE(IDX)  ((uint8)DMA_PAD == HI_UINT16(dmaCfg.rxBuf[(IDX)]))

    其中,DMA_PAD实际就是UART外设的波特率设置寄存器U0BAUD,我很疑惑,为什么要用到U0BAUD寄存器?后来,经过仔细的在线调试才发现其中的奥秘:

    首先,我打开了UART,但是我没有在UART接收回调里做任何处理,之后我向设备发送2个相同的字节0xAA,在调试器里我看到:


    联想到DMA,因为用于UART接收的DMA通道是一次传送16-bit的数据的,然后源地址是寄存器U0BUF的地址,它只有1个字节,因此自然而然U0BAUD被'不小心'传过去了,但实际上,虽然rxBuf数组的类型是uint16的,但其实它的每个元素的低字节用于存储接收到的数据,高地址为当前U0BAUD寄存器的值。其实,U0BAUD的值被放在高8位,就是表明这个位置存放了新接收的数据而且这个数据没有被应用层处理过。有了这个线索,后面的代码就好理解了。先看函数HalUARTRxAvailDMA():

/**************************************************************************************************
 * @fn      HalUARTRxAvailDMA()
 *
 * @brief   Calculate Rx Buffer length - the number of bytes in the buffer.
 *
 * @param   none
 *
 * @return  length of current Rx Buffer
 **************************************************************************************************/
static uint16 HalUARTRxAvailDMA(void)
{
  // First, synchronize the Rx tail marker with where the DMA Rx engine is working.
  rxIdx_t tail = dmaCfg.rxTail;

  do
  {
    if (!HAL_UART_DMA_NEW_RX_BYTE(tail))
    {
      break;
    }

    HAL_UART_RX_IDX_T_INCR(tail);
  } while (tail != dmaCfg.rxHead);

  dmaCfg.rxTail = tail;

  uint16 cnt = tail - dmaCfg.rxHead;

  // If the DMA Rx may have overrun the circular queue, investigate further.
  if ((cnt == 0) && HAL_UART_DMA_NEW_RX_BYTE(tail))
  {
    /* Ascertain whether this polling is racing with the DMA Rx which may have clocked in a byte
     * since walking the tail. The Rx queue has wrapped only if the byte before the head is new.
     */
    tail = dmaCfg.rxHead;
    HAL_UART_RX_IDX_T_DECR(tail);

    if (HAL_UART_DMA_NEW_RX_BYTE(tail))
    {
      if (HAL_UART_RX_FLUSH)
      {
        (void)memset(dmaCfg.rxBuf, (DMA_PAD ^ 0xFF), HAL_UART_DMA_RX_MAX*2);

        uartRxBug = dmaCfg.rxHead;
        dmaCfg.rxTail = dmaCfg.rxHead;
      }
      else
      {
        cnt = HAL_UART_DMA_RX_MAX;
      }
    }
    else
    {
      cnt = 1;
    }
  }
  else if (cnt > HAL_UART_DMA_RX_MAX)  // If the tail has wrapped at the end of the Rx queue.
  {
    cnt += HAL_UART_DMA_RX_MAX;
  }

  return cnt;
}
  
    首先要明确一点,DMA把UART接收到的数据搬移到dmaCfg.rxBuf后, dmaCfg.rxTail和 dmaCfg.rxHead的值是不变的,所以函数的开头就需要先更新 dmaCfg.rxT ail的值,以下代码段干的就是这个。

  do
  {
    if (!HAL_UART_DMA_NEW_RX_BYTE(tail))
    {
      break;
    }


    HAL_UART_RX_IDX_T_INCR(tail);
  } while (tail != dmaCfg.rxHead);


  dmaCfg.rxTail = tail;

    接下来,局部变量cnt就计算出当前 dmaCfg.rxBuf有多少字节数据未处理了。

uint16 cnt = tail - dmaCfg.rxHead;

    如果我继续发数据,直到所有的 dmaCfg.rxBuf被写满:


    我们发现此时rxTail的值已经从下标0~127轮询了一遍,此时if ((cnt == 0) && HAL_UART_DMA_NEW_RX_BYTE(tail))条件满足了,说明缓存已经满了,接下来的处理就是把dmaCfg.rxBuf全部设置为U0BAUD的值的取反,相当于BUFFEER清空,并设置dmaCfg.rxHead=dmaCfg.rxTail。












  • 6
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值