FDCAN应用说明

FDCAN应用说明

概述

最近用到了FDCAN,下面对FDCAN的一些使用注意事项进行分析。

一些FDCAN的基本概念可以看这些文章。一文搞懂CAN FD总线协议帧格式 - 知乎STM32H743使用FDCAN+TJA1042T配合CubeMX及HAL库实现仲裁段1M速率,数据段5M速率的FDCAN总线传输记录_stm32h743 can cubemx-CSDN博客

假设作为软件工程师,不想关注具体里面的东西,可以简单理解为每个CAN交互报文都是如下格式。ID和CRC是协议必须带的,Data0-Datan是实际要发送的内容。

1742992170894

对于FDCAN和CAN的差异,从软件侧来看,主要包括如下。由于ID和CRC要占不少东西,而且还有抢占的问题,所以一般软件能用FDCAN就用FDCAN。

CANFDCAN
Data最大长度(Bytes)864
Data段最大速度1Mbps5Mbps(其他段最大还是1Mbps)
PHY芯片普通的TJA1050就行如果要大于1Mbps,用TJA1042

ST芯片时钟配置

在STM32H5上面使用时,里面有2个FDCAN控制器,可独立使用。

但是FDCAN2的时钟配置好像必须一样,不然行为就很奇怪。其他配置没细的尝试,应该主要和红框有关,其他影响不大。

1742990990123

某种情况下,IO有限,只能使用FDCAN2时,分频系数必须配置为DIV1

1742991169787

ST芯片FDCAN重置

FDCAN有个比较麻烦的地方,就是其是有ACK机制的,也就是发送的数据包如果没ACK(或者没接PHY转换芯片,其会监控TX和RX信号),数据包会一直在队列中。

但是应用某些情况下需要把FIFO中的数据包都清空。可以先调用HAL_FDCAN_Stop接口,再调用HAL_FDCAN_Start接口,即可重置。


void FDCAN_DeConfig(FDCAN_HandleTypeDef *hfdcan)
{
  if (HAL_FDCAN_Stop(hfdcan) != HAL_OK)
  {
    while(1);
  }
}
 
void FDCAN_Config(FDCAN_HandleTypeDef *hfdcan)
{
  ...
  
  /* Start the FDCAN module */
  if (HAL_FDCAN_Start(hfdcan) != HAL_OK)
  {
    while(1);
  }
}

void fdcan_control_restart(FDCAN_HandleTypeDef *hfdcan)
{
  FDCAN_DeConfig(hfdcan);
  FDCAN_Config(hfdcan);
}

FDCAN的TX和RX

需要注意,当有PHY芯片时,如果用逻辑分析仪看MCU的TX和RX信号,会发现TX发送任何数据,RX就会收到与之对应的数据。

这也是为什么MCU可以做到冲突规避的原理。

注意,可以把总线所有设备都拿掉,直接看TX,RX的信号。

1742991856440

FDCAN的ID唯一性

注意,注意,注意,这里每个设备最好都用一个唯一的ID来总线上交互,不然会丢包。

由于总线上每个设备都可以随时发起数据包请求,当然应用协议上做好限制,交互是响应式(Request+Response)的请求模式的话, 允许所有设备用一个ID进行交互。

因为当设备的ID相同时,假设同时发起来FDCAN请求,那FDCAN的硬件仲裁机制将无法正常工作,这时总线上很可能有多个设备同时工作,有些设备的数据包可能没发送出去,但是硬件却认为数据包已经发送成功了。这个在大规模数据压力测试时出现会恶心死。

FDCAN的拓扑结构

标准的CAN网络要求有一个总线,所有设备都接入到总线中,总线的起点和终点需要放置120欧的电阻。

1742992961448

但是实际项目中基本都不是这种情况,一般都是星形网络,支持节点的热插拔。

在这种网络中,并没有像汽车那种一根长长的总线。总线可能就是一个PCB板,留了很多接入点,设备可以用线缆连入,这个情况下,总线长度可能不到10cm,支线的长度却有2-5m的长度,这个情况下,因为不确定什么时候有设备加入,也无法决定哪个子节点需要加120欧电阻。

那这个情况下,就直接所有子节点都不加120欧电阻。通过示波器查看电压,可以看看各个节点的压降都还不错。

此外,既然已经非标了,就别使用太高速的CAN频率,使用250kbps或者500kbps即可。

1742993262907

FDCAN测试设备

协议分析怎么能少了仪器,虽然示波器已经能看很多东西了,但是业务问题还是有这种抓包仪器会好分析一些。200-300就可以抓包看了。多通道USB转CAN分析仪调试器CAN盒CANFD总线CANopen J1939OBD ZLG-淘宝网

个人买了一个2通道的就足够用了。

1742994231445

安装上位机软件可以抓到交互的数据包。

96f18474b3f7c3b96234c4415905684

FDCAN总线示波器波形

可以看看大佬写的文章,写得很好了。用示波器排查CAN的各种错误帧(1) - 知乎

最好在每个终端节点旁边都测测其看到的差分电平压降,只要不衰减太厉害,所设计的电路和can网络拓扑就没什么问题。下面借鉴大佬的一个图,上面的相对干净了。

确保上升沿和下降沿迅速切换,别有斜坡(别用太大的电容)。

img

一个简易FDCAN控制器代码

#include <stdint.h>
#include <string.h>






FDCAN_HandleTypeDef hfdcan1;
FDCAN_HandleTypeDef hfdcan2;


/**
  * @brief This function handles FDCAN1 interrupt 0.
  */
 void FDCAN1_IT0_IRQHandler(void)
 {
   /* USER CODE BEGIN FDCAN1_IT0_IRQn 0 */
 
   /* USER CODE END FDCAN1_IT0_IRQn 0 */
   HAL_FDCAN_IRQHandler(&hfdcan1);
   /* USER CODE BEGIN FDCAN1_IT0_IRQn 1 */
 
   /* USER CODE END FDCAN1_IT0_IRQn 1 */
 }
 
/**
  * @brief This function handles FDCAN2 interrupt 0.
  */
void FDCAN2_IT0_IRQHandler(void)
{
  /* USER CODE BEGIN FDCAN2_IT0_IRQn 0 */

  /* USER CODE END FDCAN2_IT0_IRQn 0 */
  HAL_FDCAN_IRQHandler(&hfdcan2);
  /* USER CODE BEGIN FDCAN2_IT0_IRQn 1 */

  /* USER CODE END FDCAN2_IT0_IRQn 1 */
}
 
/**
  * @brief FDCAN1 Initialization Function
  * @param None
  * @retval None
  */
 static void MX_FDCAN1_Init(void)
 {
 
   /* USER CODE BEGIN FDCAN1_Init 0 */
 
   /* USER CODE END FDCAN1_Init 0 */
 
   /* USER CODE BEGIN FDCAN1_Init 1 */
 
   /* USER CODE END FDCAN1_Init 1 */
   hfdcan1.Instance = FDCAN1;
   hfdcan1.Init.ClockDivider = FDCAN_CLOCK_DIV2;
   hfdcan1.Init.FrameFormat = FDCAN_FRAME_FD_NO_BRS;
   hfdcan1.Init.Mode = FDCAN_MODE_NORMAL;
   hfdcan1.Init.AutoRetransmission = ENABLE;
   hfdcan1.Init.TransmitPause = DISABLE;
   hfdcan1.Init.ProtocolException = DISABLE;
   hfdcan1.Init.NominalPrescaler = 1;
   hfdcan1.Init.NominalSyncJumpWidth = 62;
   hfdcan1.Init.NominalTimeSeg1 = 187;
   hfdcan1.Init.NominalTimeSeg2 = 62;
   hfdcan1.Init.DataPrescaler = 2;
   hfdcan1.Init.DataSyncJumpWidth = 5;
   hfdcan1.Init.DataTimeSeg1 = 19;
   hfdcan1.Init.DataTimeSeg2 = 5;
   hfdcan1.Init.StdFiltersNbr = 1;
   hfdcan1.Init.ExtFiltersNbr = 0;
   hfdcan1.Init.TxFifoQueueMode = FDCAN_TX_FIFO_OPERATION;
   if (HAL_FDCAN_Init(&hfdcan1) != HAL_OK)
   {
    while(1);
   }
   /* USER CODE BEGIN FDCAN1_Init 2 */
 
   /* USER CODE END FDCAN1_Init 2 */
 
 }

/**
  * @brief FDCAN2 Initialization Function
  * @param None
  * @retval None
  */
 static void MX_FDCAN2_Init(void)
 {
 
   /* USER CODE BEGIN FDCAN2_Init 0 */
 
   /* USER CODE END FDCAN2_Init 0 */
 
   /* USER CODE BEGIN FDCAN2_Init 1 */
 
   /* USER CODE END FDCAN2_Init 1 */
   hfdcan2.Instance = FDCAN2;
   hfdcan2.Init.ClockDivider = FDCAN_CLOCK_DIV2;
   hfdcan2.Init.FrameFormat = FDCAN_FRAME_FD_NO_BRS;
   hfdcan2.Init.Mode = FDCAN_MODE_NORMAL;
   hfdcan2.Init.AutoRetransmission = ENABLE;
   hfdcan2.Init.TransmitPause = DISABLE;
   hfdcan2.Init.ProtocolException = DISABLE;
   hfdcan2.Init.NominalPrescaler = 1;
   hfdcan2.Init.NominalSyncJumpWidth = 62;
   hfdcan2.Init.NominalTimeSeg1 = 187;
   hfdcan2.Init.NominalTimeSeg2 = 62;
   hfdcan2.Init.DataPrescaler = 2;
   hfdcan2.Init.DataSyncJumpWidth = 5;
   hfdcan2.Init.DataTimeSeg1 = 19;
   hfdcan2.Init.DataTimeSeg2 = 5;
   hfdcan2.Init.StdFiltersNbr = 1;
   hfdcan2.Init.ExtFiltersNbr = 0;
   hfdcan2.Init.TxFifoQueueMode = FDCAN_TX_FIFO_OPERATION;
   if (HAL_FDCAN_Init(&hfdcan2) != HAL_OK)
   {
     while(1);
   }
   /* USER CODE BEGIN FDCAN2_Init 2 */
 
   /* USER CODE END FDCAN2_Init 2 */
 
 }
 
 
void FDCAN_DeConfig(FDCAN_HandleTypeDef *hfdcan)
{
  if (HAL_FDCAN_Stop(hfdcan) != HAL_OK)
  {
    while(1);
  }
}
 
void FDCAN_Config(FDCAN_HandleTypeDef *hfdcan)
{
  FDCAN_FilterTypeDef sFilterConfig;
  /* Configure Rx filter */
  sFilterConfig.IdType = FDCAN_STANDARD_ID;
  sFilterConfig.FilterIndex = 0;
  sFilterConfig.FilterType = FDCAN_FILTER_RANGE;
  if(hfdcan->Instance == FDCAN1)
  {
    sFilterConfig.FilterConfig = FDCAN_FILTER_TO_RXFIFO0;
    // sFilterConfig.FilterConfig = FDCAN_FILTER_TO_RXFIFO1;
  }
  else if(hfdcan->Instance == FDCAN2)
  {
    sFilterConfig.FilterConfig = FDCAN_FILTER_TO_RXFIFO1;
  }
  sFilterConfig.FilterID1 = 0x00000000;
  sFilterConfig.FilterID2 = 0x000007FF;
  if (HAL_FDCAN_ConfigFilter(hfdcan, &sFilterConfig) != HAL_OK)
  {
    while(1);
  }

  /* Configure global filter:
     Filter all remote frames with STD and EXT ID
     Reject non matching frames with STD ID and EXT ID */
  if (HAL_FDCAN_ConfigGlobalFilter(hfdcan, FDCAN_REJECT, FDCAN_REJECT, FDCAN_FILTER_REMOTE, FDCAN_FILTER_REMOTE) != HAL_OK)
  {
    while(1);
  }

  /* Activate Rx FIFO 0 new message notification on both FDCAN instances */
  // if (HAL_FDCAN_ActivateNotification(&hfdcan1, FDCAN_IT_RX_FIFO0_NEW_MESSAGE |
  //   FDCAN_IT_RAM_ACCESS_FAILURE | FDCAN_IT_ERROR_LOGGING_OVERFLOW | FDCAN_IT_RAM_WATCHDOG | FDCAN_IT_ARB_PROTOCOL_ERROR | FDCAN_IT_DATA_PROTOCOL_ERROR | FDCAN_IT_RESERVED_ADDRESS_ACCESS 
  //   | FDCAN_IT_ERROR_PASSIVE | FDCAN_IT_ERROR_WARNING | FDCAN_IT_BUS_OFF
  //   , 0) != HAL_OK)
  // {
  //   while(1);
  // }
  if(hfdcan->Instance == FDCAN1)
  {
    if (HAL_FDCAN_ActivateNotification(hfdcan, FDCAN_IT_RX_FIFO0_NEW_MESSAGE | FDCAN_IT_RX_FIFO0_MESSAGE_LOST | FDCAN_IT_RX_FIFO0_FULL, 0) != HAL_OK)
    {
      while(1);
    }
  }
  else
  {
    if (HAL_FDCAN_ActivateNotification(hfdcan, FDCAN_IT_RX_FIFO1_NEW_MESSAGE | FDCAN_IT_RX_FIFO1_MESSAGE_LOST | FDCAN_IT_RX_FIFO1_FULL, 0) != HAL_OK)
    {
      while(1);
    }
  }

  // if (HAL_FDCAN_ActivateNotification(hfdcan, FDCAN_IT_BUS_OFF, 0) != HAL_OK)
  // {
  //   while(1);
  // }

  /* Configure and enable Tx Delay Compensation, required for BRS mode.
        TdcOffset default recommended value: DataTimeSeg1 * DataPrescaler
        TdcFilter default recommended value: 0 */
  HAL_FDCAN_ConfigTxDelayCompensation(hfdcan, hfdcan->Init.DataPrescaler * hfdcan->Init.DataTimeSeg1, 0);
  HAL_FDCAN_EnableTxDelayCompensation(hfdcan);
  
  /* Start the FDCAN module */
  if (HAL_FDCAN_Start(hfdcan) != HAL_OK)
  {
    while(1);
  }
}

// void HAL_FDCAN_ErrorCallback(FDCAN_HandleTypeDef *hfdcan)
// {
//   printf("HAL_FDCAN_ErrorCallback, Error Code = 0x%x, State = 0x%x\r\n", hfdcan->ErrorCode, hfdcan->State);
// }

// void HAL_FDCAN_ErrorStatusCallback(FDCAN_HandleTypeDef *hfdcan, uint32_t ErrorStatusITs)
// {
//   printf("HAL_FDCAN_ErrorStatusCallback, Error Code = 0x%x, State = 0x%x, ErrorStatusITs = 0x%x\r\n", hfdcan->ErrorCode, hfdcan->State, ErrorStatusITs);
// }


int fdcan_control_get_tx_len_by_real_len(uint32_t real_len)
{
  if(real_len <= 0)
  {
    return FDCAN_DLC_BYTES_0;
  }
  else if(real_len <= 1)
  {
    return FDCAN_DLC_BYTES_1;
  }
  else if(real_len <= 2)
  {
    return FDCAN_DLC_BYTES_2;
  }
  else if(real_len <= 3)
  {
    return FDCAN_DLC_BYTES_3;
  }
  else if(real_len <= 4)
  {
    return FDCAN_DLC_BYTES_4;
  }
  else if(real_len <= 5)
  {
    return FDCAN_DLC_BYTES_5;
  }
  else if(real_len <= 6)
  {
    return FDCAN_DLC_BYTES_6;
  }
  else if(real_len <= 7)
  {
    return FDCAN_DLC_BYTES_7;
  }
  else if(real_len <= 8)
  {
    return FDCAN_DLC_BYTES_8;
  }
  else if(real_len <= 12)
  {
    return FDCAN_DLC_BYTES_12;
  }
  else if(real_len <= 16)
  {
    return FDCAN_DLC_BYTES_16;
  }
  else if(real_len <= 20)
  {
    return FDCAN_DLC_BYTES_20;
  }
  else if(real_len <= 24)
  {
    return FDCAN_DLC_BYTES_24;
  }
  else if(real_len <= 32)
  {
    return FDCAN_DLC_BYTES_32;
  }
  else if(real_len <= 48)
  {
    return FDCAN_DLC_BYTES_48;
  }
  
  return FDCAN_DLC_BYTES_64;
}

void fdcan_control_restart(FDCAN_HandleTypeDef *hfdcan)
{
  int index = hfdcan->Instance == FDCAN1 ? 0 : 1;
  EASY_LOG_ERR("fdcan_control_restart, index: %d\r\n", index);

  // reconfig fdcan
  FDCAN_DeConfig(hfdcan);
  FDCAN_Config(hfdcan);
}

int fdcan_control_tx_packet(FDCAN_HandleTypeDef *hfdcan, uint16_t id, const uint8_t *data, uint32_t len)
{
  uint8_t data_buf[64] = {0};
  int index = hfdcan->Instance == FDCAN1 ? 0 : 1;
  if(len > 64)
  {
    len = 64;
  }
  memcpy(data_buf, data, len);

  FDCAN_TxHeaderTypeDef TxHeader;
  TxHeader.Identifier = id;                 // CAN ID
  TxHeader.IdType = FDCAN_STANDARD_ID;         // 标准ID
  TxHeader.TxFrameType = FDCAN_DATA_FRAME;
  TxHeader.DataLength = fdcan_control_get_tx_len_by_real_len(len);          // 发送长度:64byte
  TxHeader.ErrorStateIndicator = FDCAN_ESI_ACTIVE;
  TxHeader.BitRateSwitch = FDCAN_BRS_OFF;
  TxHeader.FDFormat = FDCAN_FD_CAN;            // CANFD
  TxHeader.TxEventFifoControl = FDCAN_NO_TX_EVENTS;  
  TxHeader.MessageMarker = 0;


  int ret = 0;
  ret = HAL_FDCAN_AddMessageToTxFifoQ(hfdcan, &TxHeader, data_buf);
  if(ret != HAL_OK)
  {
    return 0;
  }
  else
  {
  }

  return len;
}

void HAL_FDCAN_TxBufferCompleteCallback(FDCAN_HandleTypeDef *hfdcan, uint32_t BufferIndexes)
{
    EASY_LOG_DBG("Tx complete\r\n");
}

__EASY_WEAK__ void fdcan1_control_rx_packet(uint32_t can_id, uint8_t *data, uint32_t len)
{

}

__EASY_WEAK__ void fdcan2_control_rx_packet(uint32_t can_id, uint8_t *data, uint32_t len)
{

}

static const uint8_t DLCtoBytes[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64};
void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo0ITs)
{
  FDCAN_RxHeaderTypeDef RxHeader;
  uint8_t data_buf[64] = {0};
  uint16_t data_len = 0;

  if((RxFifo0ITs & FDCAN_IT_RX_FIFO0_NEW_MESSAGE) != RESET)
  {
    // int work_cnt = 0;
    /* Retrieve Rx messages from RX FIFO0 */
    /* Check if FIFO 0 receive at least one message */
    while (HAL_FDCAN_GetRxFifoFillLevel(hfdcan, FDCAN_RX_FIFO0))
    {
      // work_cnt ++;
      // if(work_cnt > 1)
      // {
      //   printf("!!!!!!!work_cnt = %d\r\n", work_cnt);
      // }
      if (HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO0, &RxHeader, data_buf) != HAL_OK)
      {
        while(1);
      }

      data_len = DLCtoBytes[RxHeader.DataLength];
  
      // printf("Identifier = 0x%x\r\n", RxHeader.Identifier);
      // printf("DataLength = %d(0x%x)\r\n", data_len, RxHeader.DataLength);
      // printf("FDFormat = 0x%x\r\n", RxHeader.FDFormat);
      // printf("BitRateSwitch = %d\r\n", RxHeader.BitRateSwitch);
      // printf("ErrorStateIndicator = 0x%x\r\n", RxHeader.ErrorStateIndicator);
      // for(int i = 0; i < data_len; i++)
      // {
      //   printf("Data[%d] = 0x%x\r\n", i, can1_rxbuf[i]);
      // }

      if(hfdcan->Instance == FDCAN1)
      {
        fdcan1_control_rx_packet(RxHeader.Identifier, data_buf, data_len);
      }
      else if(hfdcan->Instance == FDCAN2)
      {
        fdcan2_control_rx_packet(RxHeader.Identifier, data_buf, data_len);
      }
    }

    /* Retrieve Rx messages from RX FIFO0 */
    // status = HAL_FDCAN_GetRxMessage(&hfdcan, FDCAN_RX_FIFO0, &RxHeader, RxData);
    // if (HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO0, &RxHeader, data_buf) != HAL_OK)
    // {
    //   while(1);
    // }

    
  }
  
  if((RxFifo0ITs & FDCAN_IT_RX_FIFO0_MESSAGE_LOST) != RESET)
  {
   EASY_LOG_DBG("Rx FIFO0 message lost\r\n");
  }
  
  if((RxFifo0ITs & FDCAN_IT_RX_FIFO0_FULL) != RESET)
  {
   EASY_LOG_DBG("Rx FIFO0 message full\r\n");
  }
}



void HAL_FDCAN_RxFifo1Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo1ITs)
{
  FDCAN_RxHeaderTypeDef RxHeader;
  uint8_t data_buf[64] = {0};
  uint16_t data_len = 0;

  if((RxFifo1ITs & FDCAN_IT_RX_FIFO1_NEW_MESSAGE) != RESET)
  {
    // int work_cnt = 0;
    /* Retrieve Rx messages from RX FIFO1 */
    /* Check if FIFO 0 receive at least one message */
    while (HAL_FDCAN_GetRxFifoFillLevel(hfdcan, FDCAN_RX_FIFO1))
    {
      // work_cnt ++;
      // if(work_cnt > 1)
      // {
      //   printf("!!!!!!!work_cnt = %d\r\n", work_cnt);
      // }
      if (HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO1, &RxHeader, data_buf) != HAL_OK)
      {
        while(1);
      }

      data_len = DLCtoBytes[RxHeader.DataLength];
  
      // printf("Identifier = 0x%x\r\n", RxHeader.Identifier);
      // printf("DataLength = %d(0x%x)\r\n", data_len, RxHeader.DataLength);
      // printf("FDFormat = 0x%x\r\n", RxHeader.FDFormat);
      // printf("BitRateSwitch = %d\r\n", RxHeader.BitRateSwitch);
      // printf("ErrorStateIndicator = 0x%x\r\n", RxHeader.ErrorStateIndicator);
      // for(int i = 0; i < data_len; i++)
      // {
      //   printf("Data[%d] = 0x%x\r\n", i, can1_rxbuf[i]);
      // }

      if(hfdcan->Instance == FDCAN1)
      {
        fdcan1_control_rx_packet(RxHeader.Identifier, data_buf, data_len);
      }
      else if(hfdcan->Instance == FDCAN2)
      {
        fdcan2_control_rx_packet(RxHeader.Identifier, data_buf, data_len);
      }
    }

    /* Retrieve Rx messages from RX FIFO1 */
    // status = HAL_FDCAN_GetRxMessage(&hfdcan, FDCAN_RX_FIFO1, &RxHeader, RxData);
    // if (HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO1, &RxHeader, data_buf) != HAL_OK)
    // {
    //   while(1);
    // }

    
  }
  
  if((RxFifo1ITs & FDCAN_IT_RX_FIFO1_MESSAGE_LOST) != RESET)
  {
   EASY_LOG_DBG("Rx FIFO1 message lost\r\n");
  }
  
  if((RxFifo1ITs & FDCAN_IT_RX_FIFO1_FULL) != RESET)
  {
   EASY_LOG_DBG("Rx FIFO1 message full\r\n");
  }
}



int fdcan_control_init(void)
{
    EASY_LOG_DBG("fdcan_control_init\n");

    MX_FDCAN1_Init();
    MX_FDCAN2_Init();

    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

CoderBob

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

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

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

打赏作者

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

抵扣说明:

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

余额充值