还在老一套?STM32使用新KEIL5的IDE,全新开发模式RTE介绍及使用

Keil新版本出来了,推出了一种全新开发模式RTE框架( Run-Time Environment),更好用了。然而网上的教程资料竟还都是把Keil5当成Keil4来用,直接不使用这个功能。当前正点原子或野火的教程提供的例程虽有提到Keil5,但也是基本上当Keil4来用,还是传统的模式或标准库或HAL库。当然这用来学习挺好,但是如果用来开发建议还是使用下Keil5的RTE框架。

现在的单片机开发早已经变天,早已不是十年前刀耕火种的样子,开发和使用越来越友好了。单片机的开发趋势越来越接近于软件层开发,底层驱动的封装变成了芯片厂家做的工作。这是一种趋势,对于开发使用人员而言,可以更专注于需求和效率。

RTE框架直接给你提供了现成的板级驱动包和可视化模块配置,你需要做的只是配置和在驱动层之上开发应用就行了。且包含了包管理功能,包和组件使用和升级更方便了,这是一大特色,其他厂家都可以贡献和提供驱动包,其他三方都可以提供模块组件,开放共建。包括嵌入式内核你都不要移植,且可以切换想用哪个用哪个,不得不佩服老外IDE做的是真用心啊。

希望国产IDE软件也多学习学习老外,如果国产软件也能这么强大好用该多好,那时候国产芯片想不火都难。STM32的火是有原因的,因为它的操作使用上比起其他单片机都简单。这得益于ST这家公司的睿智,先是提供标准库,后HAL库,如今ST推出的STM32CubeMX配置软件,就是想让你更简单更好用,易用性上甩其他厂家几条街,就是让你只用它家的就行了,以此来笼络你,垄断你,但它只能耕stm32这一亩三分地。

单片机领域,基于Arm Cortex M内核的产品已经占据很大的一部分份额,比较知名的ST的STM32系列,NXP的LPC系列都是其中非常棒的产品。各个厂家为了用户能更好的使用这些单片机产品,都推出了各自的软件工具和SDK,以便帮助用户简单快速的开发软件。Arm公司为了防止Arm软件开发的碎片化,提出并实施了CMSIS,这个是Arm公司与多家不同的芯片和软件供应商一起紧密合作定义的,提供了内核与外设、实时操作系统和中间设备之间的通用接口。后续用户需要做的仅是在应用层上做开发就可以了。

CMSIS的整体架构:

前言

曾记得十年前使用STM32时那叫一个麻烦,参考正点原子和野火的教程学习直接操作寄存器。

其实这也没啥,毕竟这是传统的单片机的开发方式。如之前的51,avr,pic,msp430,我想这几款单片机都玩过的肯定都是古董玩家,想当年这几款单片机没少折腾我,当然都是对着手册操作寄存器,也没啥难的,就是很低效,换一种单片机就得先熟悉它的风格。

现在单片机开发变简单了许多,这归功于stm32做的不错。之所以stm32它一直很火,是因为它开创了一个先河,j降低了单片机的门槛,让单片机操作越来越简单了。ST官方最先推出了标准库,然而依旧觉得不是很简单。再后来ST官方又推出了HAL库,接着STM32CubeMX配置软件的横空出世,让STM32可视化配置硬件参数和寄存器,变更好用了。但STM32CubeMX仅用在ST的产品上,深度绑定。

这里要介绍下Keil5的全新的开发方式RTE框架的使用。

可视化的最新的keil5软件真强大,但网上提供的教程大多是基于传统keil4的开发模式,有的虽然标题提到了RTE这种全新的包管理模式,但是还是混杂着标准库或HAL库的概念让人混淆不清,还在教你如何手工添加库和文件,没体现RTE的好处和使用。

其实RET这种模式下就不要提什么标准库或HAL库了,就当是各个厂家提供好了现成的板级驱动就好。你需要在做些配置且在驱动层之上开发应用。驱动层之下不用你过多关注,驱动层和常用模块都已提供好了。这是未来的一种趋势,因此建议尝试和使用下Keil5的RTE开发模式。

我通过查阅官网拿到第一手资料,在此整理下我的总结分享给大家。后续再看到应用层直接操作寄存器和板级接口操作的,不要再这样混着用啦,这种非RTE框架模式的本意。

不知你有没有觉察到,Keil5引入的这一开发理念很先进。会成为今后的主流趋势,值得我们学习借鉴。以后软件开发者可以把精力放在快速的开发应用和创新上,驱动操作封装成了芯片厂家要做的事。当多数国人还在卷单片机寄存器操作的时候,老外又一次走在了前列。Keil5不愧是王者,IDE好用易用性上排第一。以后不但芯片硬件上可替换,软件应用层也可以做到很少改动。


以下是我的分享,希望对大家有所帮助。另外,本文中如果有需改善的地方,欢迎留言,谢谢!

什么是RTE?

Keil5 最新的 RTE 框架是一种用于配置嵌入式软件应用程序的软件组件。它提供了图形用户界面 (GUI),使开发人员可以轻松配置系统组件,如处理器外设、通信接口和内存管理,而不需要手动编写代码。相比原来使用的标准库或 HAL 库的方式,Keil5 RTE 框架的优点在于它简化了开发流程,提供了一种简单直观的配置方法,并减少了必须手动编写的代码数量,可以节省时间并减少错误。

软件组件介绍

MDK 提供软件组件(Software Components),用于使用称为运行时环境(RTE) 的框架创建应用程序。这些组件以独立于 µVision 安装的软件包形式提供。软件包由 Arm 或第三方提供。

Software Components

图为Arm Keil提供的软件包,结构清晰。软件包可以包含设备支持,包括驱动程序、CMSIS 库和中间件组件。其中的设备系列包,由各个芯片厂家提供。

设备系列包 (DFP):包含用于微控制器设备系列的 CMSIS 系统/启动、驱动程序和闪存算法。
CMSIS:包含通用 CMSIS 组件(CORE、DSP 和 NN 库,以及 RTOS 实现)。
MDK-Middleware:包含属于 MDK-Professional 一部分的中间件库。
在项目中使用软件组件:

1.使用Pack Installer安装或更新包含预构建软件组件的软件包。
2.使用窗口Manage Run-Time Environment将软件组件添加到项目中。添加的组件显示在“项目”窗口中。
3.(可选)设置组件的选项以指定属性。
4.(可选)为特定目标选择软件包。

新项目结构介绍

不要再用以往传统的手动创建所有目录,再引入HAL库的落后方式了。当然可以在创建一些应用层的目录。其他层的按照配置出来的已经很清晰了。以下举例介绍下最的项目结构。

 以上目录中,默认配置生成的是绿色显示的。Device就相当于底层驱动层,驱动层里面小写字母开头的文件,是各个厂家官方提供的固件操作功能。大写字母开头的就是驱动接口及实现。在应用层使用时最好严格分层,不要混合调用固件接口或操作寄存器了,而是应通过大写字母开头的驱动接口去配置和调用。(注:截图部分的代码仅为示例。它不好的地方是,里面掺杂使用了如GPIOD-BSRR这种寄存器操作,后文有正解。)

CMSIS是一个标准,以上CMSIS里的是标准驱动的实现,其中的串口驱动和操作系统内核放置在里面。那个Board Support算是个模块儿化组件,提供了一些通用的硬件模块操作,如以上的是个跟板子硬件相关的LED灯的操作实现,可以拿来复用,不过需要根据板子的实际管脚分配来配置。

应用层使用介绍

Board Support这个文件夹,虽然是配置出来的,但是它实际就是一个应用层功能实现。可以用来学习,看下它是如何使用GPIO接口驱动了。那么后续你的使用基本跟它类似。它的文件内容如下:

#include "Board_LED.h"
#include "GPIO_STM32F10x.h"

const GPIO_PIN_ID Pin_LED[] = {
  { GPIOE,  8 },
  { GPIOE,  9 },
  { GPIOE, 10 },
  { GPIOE, 11 },
  { GPIOE, 12 },
  { GPIOE, 13 },
  { GPIOE, 14 },
  { GPIOE, 15 },
};

#define LED_COUNT (sizeof(Pin_LED)/sizeof(GPIO_PIN_ID))


/**
  \fn          int32_t LED_Initialize (void)
  \brief       Initialize LEDs
  \returns
   - \b  0: function succeeded
   - \b -1: function failed
*/
int32_t LED_Initialize (void) {
  uint32_t n;

  /* Configure pins: Push-pull Output Mode (50 MHz) with Pull-down resistors */
  for (n = 0; n < LED_COUNT; n++) {
    GPIO_PortClock   (Pin_LED[n].port, true);
    GPIO_PinWrite    (Pin_LED[n].port, Pin_LED[n].num, 0);
    GPIO_PinConfigure(Pin_LED[n].port, Pin_LED[n].num,
                      GPIO_OUT_PUSH_PULL,
                      GPIO_MODE_OUT2MHZ);
  }

  return 0;
}

/**
  \fn          int32_t LED_Uninitialize (void)
  \brief       De-initialize LEDs
  \returns
   - \b  0: function succeeded
   - \b -1: function failed
*/
int32_t LED_Uninitialize (void) {
  uint32_t n;

  /* Configure pins: Input mode, without Pull-up/down resistors */
  for (n = 0; n < LED_COUNT; n++) {
    GPIO_PinConfigure(Pin_LED[n].port, Pin_LED[n].num,
                      GPIO_IN_FLOATING,
                      GPIO_MODE_INPUT);
  }

  return 0;
}

/**
  \fn          int32_t LED_On (uint32_t num)
  \brief       Turn on requested LED
  \param[in]   num  LED number
  \returns
   - \b  0: function succeeded
   - \b -1: function failed
*/
int32_t LED_On (uint32_t num) {
  int32_t retCode = 0;

  if (num < LED_COUNT) {
    GPIO_PinWrite(Pin_LED[num].port, Pin_LED[num].num, 1);
  }
  else {
    retCode = -1;
  }

  return retCode;
}

/**
  \fn          int32_t LED_Off (uint32_t num)
  \brief       Turn off requested LED
  \param[in]   num  LED number
  \returns
   - \b  0: function succeeded
   - \b -1: function failed
*/
int32_t LED_Off (uint32_t num) {
  int32_t retCode = 0;

  if (num < LED_COUNT) {
    GPIO_PinWrite(Pin_LED[num].port, Pin_LED[num].num, 0);
  }
  else {
    retCode = -1;
  }

  return retCode;
}

/**
  \fn          int32_t LED_SetOut (uint32_t val)
  \brief       Write value to LEDs
  \param[in]   val  value to be displayed on LEDs
  \returns
   - \b  0: function succeeded
   - \b -1: function failed
*/
int32_t LED_SetOut (uint32_t val) {
  uint32_t n;

  for (n = 0; n < LED_COUNT; n++) {
    if (val & (1<<n)) {
      LED_On (n);
    } else {
      LED_Off(n);
    }
  }

  return 0;
}

/**
  \fn          uint32_t LED_GetCount (void)
  \brief       Get number of LEDs
  \return      Number of available LEDs
*/
uint32_t LED_GetCount (void) {

  return LED_COUNT;
}

代码分析:该代码属于应用层,为应用提供了几个操作LED的接口(Board_LED.h声明功能接口),代码里包含了GPIO_STM32F10x.h驱动接口头文件,使用了GPIO接口驱动。通过它可以看出,应用层的功能封装和实现,基本不涉及寄存器操作。都是通过驱动接口配置和访问的。

main入口函数介绍

/***************************************************************************//**
*  \file       main.c
*
*  \details    LED Blinking using RTX CMSIS V2 RTOS 
*
*  \author     EmbeTronicX
*
*  \Tested with Proteus
*
* *****************************************************************************/ 
#include "RTE_Components.h"
#include  CMSIS_device_header
#include "cmsis_os2.h"
#include "stm32f10x.h"

#include "Board_LED.h"
 
/*
** This thread will turns ON and turns OFF the PORT-D LEDs with 1second delay.
**
**  Arguments:
**      arg  -> Argument of this thread. osThreadNew()'s 2nd arg has to come here. 
**   
*/
__NO_RETURN static void LED_Blink_PortD( void *arg ) 
{
  (void)arg;                            //unused variable
  //set Port D as output
  //GPIOD->MODER = 0x55555555;
  for (;;)                              //infinite for loop
  {
    //Turn ON the LED of Port-D
    LED_On(3);
    osDelay(1000);                      //1sec delay
    //Turn OFF the LED of Port-D
    LED_Off(2);
    //GPIOD->BSRR = 0xFFFF0000;
    osDelay(1000);                      //1sec delay
  }
}

/*
** This thread will turns ON and turns OFF the PORT-E LEDs with 3second delay.
**
**  Arguments:
**      arg  -> Argument of this thread. osThreadNew()'s 2nd arg has to come here. 
**   
*/
__NO_RETURN static void LED_Blink_PortE( void *arg ) 
{
  (void)arg;                            //unused variable
  //set Port E as output
  //GPIOE->MODER = 0x55555555;
  for (;;)                              //infinite for loop
  {
    //Turn ON the LED of Port-E
    //GPIOE->BSRR = 0x0000FFFF;
	LED_SetOut(0);
    osDelay(3000);                      //3sec delay
    //Turn OFF the LED of Port-E
    //GPIOE->BSRR = 0xFFFF0000;
    osDelay(3000);                      //3sec delay
	LED_SetOut(1);
  }
}

/*
** main function
**
**  Arguments:
**      none
**   
*/ 
int main (void) 
{
  // System Initialization
  SystemCoreClockUpdate();
	
  LED_Initialize();
 
  osKernelInitialize();                       // Initialize CMSIS-RTOS
  osThreadNew(LED_Blink_PortD, NULL, NULL);   // Create application main thread
  osThreadNew(LED_Blink_PortE, NULL, NULL);   // Create application main thread
  osKernelStart();                            // Start thread execution
  for (;;) 
  {
    //Dummy infinite for loop.
  }
}

以上是main入口函数的实现,你有在里面发现寄存器和特殊的硬件操作吗?没有的,应用层看不到跟某一平台的相关性,更别提会出现寄存器操作了。这跟以往的开发模式有很大的不同,结构更清晰合理了。如果你的应用中混杂了寄存器或板级的驱动接口调用,请留意这样用是否合适,有没有其他的方式。

以上代码未移植嵌入式内核就可以使用RTX嵌入式系统啦,是不是很简单和方便?如果想换系统怎么办?只需要改配置就行。如果要换单片机呢?那么也放心,你的很多应用层代码都是可以复用了,仅是改下驱动层的接口参数配置,这就是这一开发模式的好处。以上代码示例不是针对stm32这一种,在keil中支持的任一单片机我都可以轻易切换。

串口驱动使用

可以看下串口的使用有多么的简单,完全颠覆了以往传统的开发方式。只需先配置好串口硬件管脚后,剩下的就是像上位机纯软件的开发一样调用接口函数即可,以下示例可以看下够简单吧。

在RTE_device.h文件中,配置串口一的硬件管脚如下:

 使用如下(main.c):

#include <string.h>
#include "RTE_Components.h"
#include  CMSIS_device_header
#include "cmsis_os2.h"
#include "stm32f10x.h"
 
#include "Board_LED.h"
#include "Driver_USART.h"

extern ARM_DRIVER_USART Driver_USART1;
 
/* Variable definitions ------------------------------------------------------*/
static uint8_t rxBuffer[1024] = {0};
static uint8_t txBuffer[1024] = {0};
 
/* Function declarations -----------------------------------------------------*/
static void USART1_Callback(uint32_t event);
/**
  * @brief  USART1 callback function.
  * @param  event: USART events notification mask.
  * @return None.
  */
static void USART1_Callback(uint32_t event)
{
  if(event & ARM_USART_EVENT_RX_TIMEOUT)
  {
    Driver_USART1.Control(ARM_USART_ABORT_RECEIVE, 1);
    
    uint32_t length = Driver_USART1.GetRxCount();
    
    memcpy(txBuffer, rxBuffer, length);
    
    Driver_USART1.Send(txBuffer, length);
    Driver_USART1.Receive(rxBuffer, sizeof(rxBuffer));
  }
}

void uart_init()
{
	
  Driver_USART1.Initialize(USART1_Callback);
  Driver_USART1.PowerControl(ARM_POWER_FULL);
  Driver_USART1.Control(ARM_USART_MODE_ASYNCHRONOUS |
                        ARM_USART_DATA_BITS_8 |
                        ARM_USART_PARITY_NONE |
                        ARM_USART_STOP_BITS_1 |
                        ARM_USART_FLOW_CONTROL_NONE, 115200);
  Driver_USART1.Control(ARM_USART_CONTROL_TX, 1);
  Driver_USART1.Control(ARM_USART_CONTROL_RX, 1);
  
  Driver_USART1.Receive(rxBuffer, sizeof(rxBuffer));
}

关于CMSIS

CMSIS 是一个独立于供应商的硬件抽象层,适用于基于 Arm® Cortex® 处理器的微控制器。CMSIS( Cortex 微控制器软件接口标准)的主要目标是提高软件在不同微控制器和工具链之间的可移植性和可重用性。这允许来自不同来源的软件无缝集成在一起。一旦学会,CMSIS 就可以通过使用标准化的软件功能来帮助加快软件开发。

CMSIS 具有模块化结构,每个模块都可以独立使用或更新。一些模块是特定于设备的,因此对于每个 ARM Cortex 微控制器系列,都会创建一个 CMSIS 模块集合,即设备系列包 (DFP)。有助于简化软件重用、减少微控制器开发人员的学习曲线、加速项目构建和调试,从而缩短新应用程序的上市时间。

CMSIS 最初是作为基于 Arm® Cortex®-M 的处理器的独立于供应商的硬件抽象层,后来扩展为支持基于 Arm Cortex-A 的入门级处理器。为了简化访问,CMSIS 定义了通用工具接口,并通过为处理器和外围设备提供简单的软件接口来实现一致的设备支持。

CMSIS 是与各种芯片和软件供应商密切合作而定义的,它提供了一种通用方法来连接外围设备、实时操作系统和中间件组件。它旨在支持来自多个供应商的软件组件的组合。

CMSIS 的创建是为了帮助行业实现标准化。它在广泛的开发工具和微控制器中实现了一致的软件层和设备支持。

CMSIS 是在GitHub上开源和协作开发的。

CMSIS 驱动程序规范

是一种软件 API,它描述了中间件堆栈和用户应用程序的外围驱动程序接口。CMSIS-Driver API 设计为通用且独立于特定的 RTOS,使其可在广泛的受支持微控制器设备中重复使用。CMSIS-Driver API 涵盖了受支持外设类型的广泛用例,但无法考虑所有潜在用例。随着时间的推移,CMSIS-Driver API 将扩展到更多的组以涵盖新的用例。

CMSIS 软件包在带有头文件和文档的组件类CMSIS 驱动程序下发布 API 接口这些头文件是实现标准化外设驱动接口的参考。这些实现通常在组件类CMSIS 驱动程序下的相关微控制器系列的设备系列包中发布。设备系列包可能包含组件类设备中的附加接口,以使用附加设备特定接口(例如内存总线、GPIO 或 DMA)扩展此 CMSIS 驱动程序规范涵盖的标准外设驱动程序。

由上图可以看出,中间件或上层应用,通过CMSIS驱动接口,跟底层的硬件进行了隔离。建议组件和应用开发上直接使用CMSIS提供的驱动接口,最好不要直接调用Device(跟特定平台相关的标准库或HAL库)的接口,除非迫不得已。这样可以做到底层硬件的变动而不影响中间件和上层业务。

CMSIS驱动接口使用文档地址:Usage and Description

结语

最后,欢迎大家体验Keil5的全新的RTE开发模式。总之使用也很简单,关键是要转变思想,一定要有软件分层的思想,摒弃以往不好的开发习惯,尤其是长期以来习惯操作寄存器的思维惯性。让代码分封更清晰,模块化更利于复用,让维护更简单,让升级更方便。

当咱们还在卷寄存器操作的时候,老外又一次走在了前列。推广下这一单片机开发理念,这一开发理念是未来的趋势!今后人人都可以根据需要使用不同的单片机快速的进行业务开发。会一种就行,不用去卷各家不同单片机的操作。驱动封装是芯片或IDE厂家的事。国产IDE软件应该像keil学习,让广大开发者好用,才有市场,才能国产化崛起!

risc-v单片机是国产化弯道超车的机会,但就是碎片化严重,驱动封装的不太好用,且没有好用的国产IDE。要是像stm32那般好用,想不火都难。各家的IDE各自为战,重复的造轮子,没有统一的支持各个厂家的不同芯片。risc-v的世界缺乏像ARM这种的统一领导和规范,得形成合力避免重复和各自为战,才能扩大生态,开放共建实现共赢。开发者没选一个芯片就得换套IDE?就得重新熟悉一遍操作吗?

市面上的各大 RISC-V 芯片或 IP 公司,都在做自己的 IDE 用自己的工具链。那么为什么各家都在做各自的工具链?我认为还是没有形成类似于 ARM 的 CMSIS 这样的嵌入式软件接口标准,来统一管理底层软件接口,于是乎就变成了各自玩各的没有统一。这也是导致 RISC-V 软件生态薄弱,碎片化的一个重要因素,严重影响其生态。大部分的 RISC-V 厂商的开发 IDE,还是基于开源的 eclipse + gcc toolchains + openocd 的方案来开发和调试芯片产品,虽然这种方案实现快些,但是IDE太大太臃肿,且仅支持自家的芯片。如果能基于notepad--用QT实现一款,像Keil一样好用该多好。

最后,期待国产单片机开发IDE软件也能朝着更好用,易用的方向发展,变得更好。

书籍推荐
首页关注博主公众号《猫青年》,回复“书籍”,获取更多高清电子书资源!完全免费!

GitHub - imarvinle/awesome-cs-books: 🔥 经典编程书籍大全,涵盖:计算机系统与网络、系统架构、算法与数据结构、前端开发、后端开发、移动开发、数据库、测试、项目与团队、程序员职业修炼、求职面试等

其他资源

CMSIS 驱动程序规范

STM32 RTOS - GPIO Tutorial (CMSIS V2)

Documentation – Arm Developer

 CMSIS-Driver Version 2.8.0

https://www.icxbk.com/article/detail?aid=483

  • 71
    点赞
  • 189
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 22
    评论
Version: 2.12.0 (2019-07-17) Keil.STM32F7xx_DFP.2.12.0.pack Download Updated Pack to include subset of STM32Cube_FW_F7 Firmware Package version V1.15.0 using HAL Drivers V1.2.7 Added support for Low Level (LL) drivers. Corrected RTE_Device.h file (I2C3_SDA) Corrected condition for selecting HAL RCC MX_Device_h.ftl: Updated parsing of USART virtual mode Updated generation of macros: Added handling for '(' and ')' symbols Corrected launching STM32CubeMX via "play" button for existing projects overwrites with a new STM32CubeMX project file instead of loading existing. Updated Board Examples: graphics examples use Segger emWin version 5.50.0. examples enable Event Recorder in debug targets Updated LCDConf.c (ready for GUI_USE_ARGD = 1) CMSIS-Driver: CAN: Corrected SetBitrate function to leave Silent and Loopback mode as they were. Corrected SetMode function to clear Silent and Loopback mode when NORMAL mode is activated. Corrected MessageSend function to only access required data for sending. EMAC: Corrected __MEMORY_AT(x) define to be compliant with Arm Compiler 6. Corrected: ETH DMA initialization moved to enable of MAC transmitter or receiver solving netInitialize/netUnnitialize/netInitialize sequence. I2C: Corrected transfers for data sizes greater than 255 (Complete Reload handling). Corrected I2C_SlaveReceive functionality. Corrected code alignment. MCI: Added data cache handling. USART: Added check for valid pointer to USART_PIN prior to use. Corrected POWER_OFF sequence. DMA is DeInitialized after it is aborted. USB Device: Updated USBD_EndpointConfigure function to check that maximum packet size requested fits into configured FIFO (compile time configured). I/O output speed is configurable SPI: Updated SPI_TRANSFER_INFO structure - tx_buf type changed from uint8_t * to const uint8_t *. Added check for valid pointer to SPI_PIN prior to use.

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

特立独行的猫a

您的鼓励是我的创作动力

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

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

打赏作者

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

抵扣说明:

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

余额充值