Linux学习笔记(17.3)——基于pinctrl和gpio子系统的按键驱动

  1. 为什么要引入pinctrl和gpio子系统?
    我认为有这么一个根本出发点——让你的程序更有生命力,不要为了目标板的一点小小的变更而忙得焦头烂额。所以,
    1)当你的按键驱动程序要去适配不同的SOC时,要做到不改变驱动程序,就要把驱动程序分为上层的按键驱动程序“空壳”(即没有实际对硬件操作的框架)和下层的具体SOC的驱动程序(实现实际所需硬件驱动,并注册给上层以便于其调用)的上下两层,这样一来,当目标SOC变更时换掉下层的SOC驱动程序即可,这就是分层;但,更进一步地,
    2)当目标SOC一致而外围电路发生变化时,如果按照上面第1)条更改下层的驱动程序,你仍然会为写一些重复的代码而觉得焦头烂额。这就引入了分离的思想——即将下层的驱动程序分为目标板资源程序文件和对应的SOC驱动程序文件,这样,只要目标板的SOC不变更,只需要修改目标板资源文件即可,这就是分离;但,更进一步地,
    3)既然要将资源文件与驱动文件分离,就会存在驱动对于资源文件的依懒或资源文件对驱动文件的依懒(要看工程师的编程习惯),依懒总会带来麻烦——不要依懒我,我也不要依懒你,我们都依懒“接口”吧。于是就有了上层的platform平台设备总线接口,当需要修改驱动程序或设备(资源)文件时修改就好了,platform平台设备总线bus会自动匹配对两者,即当注册platform_device时会自动搜索平台总线下匹配的platform_driver,当注册platform_driver时自动搜索平台总线下匹配的platform_device,然后将资源用于驱动的对象,这就是platform平台设备驱动总线接口;但更进一步地,
    4)因为linux使用了宏内核,随着arm处理器的迅猛发展,使用arm处理器的产品千变万化层出不穷,而这又造成了大量的资源文件(被Linus称为“垃圾”的东西)使得内核变得雍肿不堪。为了不将这些大量的重复的资源文件放入内核,于是就有了基于open firmware的设备树,资源文件以设备树文件形式存在而不再放入内核,而在内核启动时就将其转换为platform_device文件,内核雍肿的问题得以解决;但更进一步地,
    5)设备树的属性定义自由度非常大——为了匹配platform_driver驱动程序可以设置各种属性,驱动开发人员认识水平和经验各不相同,编写出来的驱动程序也多种多样,这些变化往往影响驱动程序的复用。于是,pinctrl子系统、gpio子系统、input子系统等设备树的标准化属性便应运而生。简单而言,pinctrl子系统用于选择某芯片引脚用于什么作用,比如将imx6ull的GPIO1.IO18用作IO功能,则可以这样定义设备树:
pinctrl_key:keygrp {
		fsl,pins = <
				MX6UL_PAD_UART1_CTS_B__GPIO1_IO18	0xF080	/* KEY0 */ 
		>;
};

key {
		#address-cells = <1>;
		#size-cells = <1>;
		compatible = "glen-key"; 

		pinctrl-names = "default";
		pinctrl-0 = <&pinctrl_key>;				/* 属性设置按键所使用的PIN对应的pinctrl节点 */
		...
};

在内核启动后,imx6ull的GPIO1.IO18就用作IO功能了,这个过程不需要普通驱动开发人员来处理,而需要芯片厂商的驱动开发人员设计对应的pinctrl底层驱动程序,在此按住不表。说回gpio子系统,其用于设置GPIO引脚方向(输入还是输出)、读值──获得电平状态,写值──输出高低电平。有了gpio子系统,普通驱动开发人员只需要调用其接口即可,同样地,具体到如何操作寄存器、读写什么值这些工作由芯片厂商的驱动开发人员设计——因为芯片原厂对这些最熟悉。这里面内核上层提供了pinctrl、gpio接口函数,也就是一堆“空壳”(框架),具体SOC的pinctrl、gpio函数的实现要芯片厂商的驱动开发工程师设计好后注册给这些“空壳”(框架)调用。这就是pinctrl、gpio子系统。但更进一步地,
5)基于RISC-V指令集的SOC发展迅猛,可以猜测在未来不远的时间会得到广泛地应用,那么发展一种跨SOC的程序框架成为非常大的可能,拭目以待吧。
扯远了,本文基于pinctrl和gpio子系统的按键驱动仍保留了button_drv.c、imx6_io_drv.c、button_drv.h文件。
2. 按键设备驱动(框架)文件 与上文无差异
button_drv.c文件实现与硬件有关的按键驱动,主要实现struct file_operations类型相关函数及其实例的注册/解除注册。其中的.read、.open、.release成员要调用下层驱动:

/**
 * 文件    : button_drv.c
 * 作者    : glen  
 * 描述    : button driver文件
 */
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#include "button_drv.h"

struct button_drv btn_drv = {
    .hw_ops = NULL,
    .name = "gbtn",
    .major = 0,
};

ssize_t button_drv_read (struct file *filp, char __user *buf, size_t size, loff_t *pos)
{
    int ret;
    int minor = iminor(filp->f_inode);
    struct btn_hw_ops *ops = (struct btn_hw_ops *)filp->private_data;
    //struct btn_hw_ops *ops = btn_drv.hw_ops;
    char kval;

    size = (size >= 1) ? 1 : 0;
    if (ops == NULL) {
        printk("Please register button hardware operations instance %s %s, %d\n", __FILE__, __FUNCTION__, __LINE__);
        return -EIO;
    }

    if (minor >= ops->num) {
        printk("Reading button value error %s %s, %d\n", __FILE__, __FUNCTION__, __LINE__);
        return -EINVAL;
    }

    if (ops->read(minor, &kval) == 0) {
        ret = copy_to_user(buf, &kval, size);
        printk("Read button value successfully:");
        return size;
    }
    return 0;
}

int button_drv_open (struct inode *nd, struct file *filp)
{
    int minor = iminor(nd);
    struct btn_hw_ops *ops = btn_drv.hw_ops;
    filp->private_data = btn_drv.hw_ops;

    if (ops == NULL) {
        printk("Please register button hardware operations instance %s %s, %d\n", __FILE__, __FUNCTION__, __LINE__);
        return -EIO;
    }

    if (minor >= ops->num) {
        printk("Openning button driver error %s %s, %d\n", __FILE__, __FUNCTION__, __LINE__);
        return -EINVAL;
    }

    if (ops->open(minor) == 0)
        printk("Open button driver successfully:");

    return 0;
}

int button_drv_release (struct inode *nd, struct file *filp)
{
    int minor = iminor(nd);
    struct btn_hw_ops *ops = (struct btn_hw_ops *)filp->private_data;

    if (ops == NULL) {
        printk("Please register button hardware operations instance %s %s, %d\n", __FILE__, __FUNCTION__, __LINE__);
        return -EIO;
    }

    if (minor >= ops->num) {
        printk("Closing button driver error %s %s, %d\n", __FILE__, __FUNCTION__, __LINE__);
        return -EINVAL;
    }

    if (ops->close(minor) == 0) {
        printk("Close button driver successfully:");
        filp->private_data = NULL;
    }

    return 0;
}

unsigned int button_drv_poll (struct file *filp, struct poll_table_struct * wait)
{
    return 0;
}

static struct file_operations button_drv_ops = {
    .owner   = THIS_MODULE,
    .read    = button_drv_read,
    .open    = button_drv_open,
    .release = button_drv_release,
    .poll    = button_drv_poll,
};

static int __init button_drv_init(void)
{
    btn_drv.major = register_chrdev(btn_drv.major, btn_drv.name, &button_drv_ops);
    btn_drv.class = class_create(THIS_MODULE, btn_drv.name);
    if (IS_ERR(btn_drv.class)) {
        printk("Class create error!\n");
        unregister_chrdev(btn_drv.major, btn_drv.name);
    }
    printk(" %s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    return 0;
}
module_init(button_drv_init);

static void __exit button_drv_exit(void)
{
    /* device_destroy */
    class_destroy(btn_drv.class);
    unregister_chrdev(btn_drv.major, btn_drv.name);
    printk(" %s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
}
module_exit(button_drv_exit);

int button_hw_ops_register(struct btn_hw_ops *ops)
{
    if (btn_drv.hw_ops)
        return -EIO;

    btn_drv.hw_ops = ops;
    printk("Register button hardware operations instance successfully\n");
    return 0;
}
EXPORT_SYMBOL(button_hw_ops_register);

void button_hw_ops_unregister(struct btn_hw_ops *ops)
{
    if (btn_drv.hw_ops)
        btn_drv.hw_ops = NULL;
    printk("Unregister button hardware operations instance successfully\n");
}
EXPORT_SYMBOL(button_hw_ops_unregister);

void button_device_create(int idx)
{
    device_create(btn_drv.class, NULL, MKDEV(btn_drv.major, idx), NULL, "gbtn%d", idx);
}
EXPORT_SYMBOL(button_device_create);

void button_device_destroy(int idx)
{
    device_destroy(btn_drv.class, MKDEV(btn_drv.major, idx));
}
EXPORT_SYMBOL(button_device_destroy);

/* insert author information for module */
MODULE_AUTHOR("glen");
/* insert license for module */
MODULE_LICENSE("GPL");

其中包含的button_drv.h文件提供了与下层soc驱动共用的结构体,注册/销毁soc驱动等接口函数,以便在button_drv.c文件中调用——驱动上层和下层都依懒于接口。

/**
 * 文件    : button_drv.h
 * 作者    : glen  
 * 描述    : button driver头文件
 */
#ifndef __BUTTON_DRV_H__
#define __BUTTON_DRV_H__

struct btn_hw_ops {
    u32 num;
    int (* read) (int idx, char *kval);
    int (* open) (int idx);
    int (* close) (int idx);
};

struct button_drv {
    struct btn_hw_ops *hw_ops;
    struct class      *class;
    char  *name;
    int major;
};

int button_hw_ops_register(struct btn_hw_ops *ops);
void button_hw_ops_unregister(struct btn_hw_ops *ops);
void button_device_create(int idx);
void button_device_destroy(int idx);

#endif // !__BUTTON_DRV_H__

button_hw_ops_register/button_hw_ops_unregister函数用于注册、解注册实际SOC驱动,button_device_create/button_device_destroy函数用于创建、销毁次设备号。由于这些用于SOC的驱动工作已由芯片厂商驱动工程师完成,这里看起来有些多余,不过我还是想过渡一下。
3. SOC相关的IO驱动文件
imx6_io_drv.c获取IO资源(这里获取的是按键的IO信息),并将资源用于实现按键的open、read、close等操作:

/**
 * 文件    : imx6_io_drv.c
 * 作者    : glen  
 * 描述    : imx6ull io driver文件
 */
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#include "button_drv.h"
// #include "imx6_io_drv.h"

// static struct btn_dev_res __btn_dev_res[16];
static struct gpio_descs *p_gbtn = NULL;

static int imx6_io_read  (int idx, char *kval);
static int imx6_io_open  (int idx);
static int imx6_io_close (int idx);

static struct btn_hw_ops btn_hw_oper = {
    .open  = imx6_io_open,
    .read  = imx6_io_read,
    .close = imx6_io_close,
};

static int imx6_io_read  (int idx, char *kval)
{
    if (idx >= btn_hw_oper.num) 
        return -EINVAL;

    *kval = gpiod_get_value(p_gbtn->desc[idx]);

    return 0;
}

static int imx6_io_open  (int idx)
{
    int ret;

    if (idx >= btn_hw_oper.num) 
        return -EINVAL;
    ret = gpiod_direction_input(p_gbtn->desc[idx]);
    if (ret) 
        printk("Set the button pin as input error %s %s, %d\n", __FILE__, __FUNCTION__, __LINE__);
    else 
        printk("Set the button%d pin as input successfully!\n", idx);
    return ret;
}

static int imx6_io_close (int idx)
{
    if (idx >= btn_hw_oper.num) 
        return -EINVAL;

    return 0;
}

int btn_hw_drv_probe (struct platform_device *pdev)
{
    int i;

    p_gbtn = gpiod_get_array_optional(&pdev->dev, "gbtn", GPIOD_ASIS);

    if (p_gbtn == NULL) {
        printk("Get \"gbtn-gpios\" property failed! \n");
        return -EIO;
    }

    printk("Get %d gpio members from the \"gbtn gpios\" property!\n", p_gbtn->ndescs);
    btn_hw_oper.num = p_gbtn->ndescs;

    for (i = 0; i < p_gbtn->ndescs; i++) 
        button_device_create(i);
    
    return 0;
}

int btn_hw_drv_remove(struct platform_device *pdev)
{
    button_hw_ops_unregister(NULL);
    gpiod_put_array(p_gbtn);
    button_device_destroy(btn_hw_oper.num);
    btn_hw_oper.num = 0;

    return 0;
}

static const struct of_device_id gbtns_id[] = {
    {.compatible = "glen,gbtn"},
    { },
};

static struct platform_driver btn_hw_drv = {
    .driver = {
        .name = "gbtn",
        .of_match_table = gbtns_id,
    },
    .probe = btn_hw_drv_probe,
    .remove = btn_hw_drv_remove,
};

static int __init imx6_io_drv_init(void)
{
    int ret;
    ret = platform_driver_register(&btn_hw_drv);

    if (ret)
        pr_err("Unable to initialize imx6 io driver\n");
    else
        pr_info("The imx6 io driver is registered.\n");

    button_hw_ops_register(&btn_hw_oper);
    return ret;
}
module_init(imx6_io_drv_init);

static void __exit imx6_io_drv_exit(void)
{
    platform_driver_unregister(&btn_hw_drv);
    pr_info("The imx6 io driver is unregistered.\n");
}
module_exit(imx6_io_drv_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("glen");

可以看出,用于实现按键的open、read、close等操作已经非常简单。
4. 更改设备树文件按键相关资源

     pinctrl_key:keygrp {
        fsl,pins = <
            MX6UL_PAD_UART1_CTS_B__GPIO1_IO18	0xF080	/* KEY0 */ 
            MX6UL_PAD_GPIO1_IO03__GPIO1_IO03    0xF080	/* KEY1  此按键不存在 */
		>;
	 };
	 /* 添加基于pinctrl的gbtns设备节点 */
	 gbtns {
	     compatible = "glen,gbtn";
	     #address-cells = <1>;
	
	     pinctrl-names = "default";
	     pinctrl-0 = <&pinctrl_key>;
	
	     gpio-controller;
	     #gpio-cells = <2>;
	     gbtn-gpios = <&gpio1 18 GPIO_ACTIVE_LOW>, /* button0 */
	                  <&gpio1 3 GPIO_ACTIVE_LOW>;  /* button1 */
	
	 };

与上一篇文章相比,这里用了pinctrl和gpio,使得设备树变得更简单。
5. 应用程序(不作修改)

/*
 * 文件名   :  button_drv_test.c
 * 作者     :  glen
 * 描述     :  button_drv应用程序
 */

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

/**
 * @brief   : main函数
 * @par     : argc  argv数组元素的个数
 *            argv  参数数组
 * @retval  : 0 成功    其它 失败
 */
int main(int argc, char *argv[])
{
    int fd, ret;
    char *filename;
    char kval;

    if (argc != 2) {
        printf("Error Usage!\r\n");
        return -1;
    }

    filename = argv[1];

    /* 打开驱动文件 */
    fd = open(filename, O_RDWR);
    if (fd < 0) {
        printf("Can't open file %s\r\n", filename);
        return -1;
    }

    while (1) {
        ret = read(fd, &kval, 1);
        if (ret <= 0) {
            printf("Read glen button value failed!\r\n");
            close(fd);
            return -1;
        } else {
            printf("The glen button value is: %d!\r\n", kval);
        }
        sleep(1);
    }

    /* 关闭文件 */
    ret = close(fd);
    if (ret < 0) {
        printf("file %s close failed!\r\n", argv[1]);
        return -1;
    }
    return 0;
}
  1. 在alientek_linux_alpha开发板实测验证如下
/ # cd drv_module/
/drv_module # ls
btn_drv_test    button_drv.ko   imx6_io_drv.ko
/drv_module # insmod button_drv.ko
 /home/glen/linux/imx6ull/glen/3_3_button_pinctrl/button_drv.c button_drv_init line 120
/drv_module # insmod imx6_io_drv.ko
Get 2 gpio members from the "gbtn gpios" property!
The imx6 io driver is registered.
Register button hardware operations instance successfully
/drv_module # ./random: nonblocking pool is initialized
/drv_module # ./btn_drv_test /dev/gbtn0
Set the button0 pin as input successfully!
Open button driver successfully:Read button value successfully:The glen button value is: 0!
Read button value successfully:The glen button value is: 0!
Read button value successfully:The glen button value is: 0!
Read button value successfully:The glen button value is: 0!
Read button value successfully:The glen button value is: 0!
Read button value successfully:The glen button value is: 0!
Read button value successfully:The glen button value is: 1!
Read button value successfully:The glen button value is: 1!
Read button value successfully:The glen button value is: 1!
Read button value successfully:The glen button value is: 1!
Read button value successfully:The glen button value is: 1!
Read button value successfully:The glen button value is: 0!
Read button value successfully:The glen button value is: 0!
^CClose button driver successfully:
/drv_module # rmmod imx6_io_drv.ko
Register button hardware operations instance successfully
The imx6 io driver is unregistered.
/drv_module # rmmod button_drv.ko
 /home/glen/linux/imx6ull/glen/3_3_button_pinctrl/button_drv.c button_drv_exit line 130
  1. 下面将imx6_io_drv.c当中的实现并入到button_drv.c文件,与上面验证的效果相同
/**
 * 文件    : button_drv.c
 * 作者    : glen  
 * 描述    : button driver文件
 */
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#include "button_drv.h"

struct button_drv btn_drv = {
    .p_gbtn = NULL,
    .name = "gbtn",
    .major = 0,
};

ssize_t button_drv_read (struct file *filp, char __user *buf, size_t size, loff_t *pos)
{
    int minor = iminor(filp->f_inode);
    struct gpio_descs *ops = (struct gpio_descs *)filp->private_data;

    char kval;

    size = (size >= 1) ? 1 : 0;
    if (ops == NULL) {
        printk("Please register button instance %s %s, %d\n", __FILE__, __FUNCTION__, __LINE__);
        return -EIO;
    }

    if (minor >= ops->ndescs) {
        printk("Reading button value error %s %s, %d\n", __FILE__, __FUNCTION__, __LINE__);
        return -EINVAL;
    }

    kval = gpiod_get_value(ops->desc[minor]);

    if (copy_to_user(buf, &kval, size))
        return -EFAULT;

    printk("Read button%d value successfully:", minor);
    return size;
}

int button_drv_open (struct inode *nd, struct file *filp)
{
    int ret;
    int minor = iminor(nd);
    struct gpio_descs *ops = btn_drv.p_gbtn;
    

    if (ops == NULL) {
        printk("Please register button instance %s %s, %d\n", __FILE__, __FUNCTION__, __LINE__);
        return -EIO;
    }

    if (minor >= ops->ndescs) {
        printk("Openning button driver error %s %s, %d\n", __FILE__, __FUNCTION__, __LINE__);
        return -EINVAL;
    }

    ret = gpiod_direction_input(ops->desc[minor]);
    if (ret) 
        printk("Set the button pin as input error %s %s, %d\n", __FILE__, __FUNCTION__, __LINE__);
    else 
        printk("Set the button%d pin as input successfully!\n", minor);

    filp->private_data = btn_drv.p_gbtn;

    return 0;
}

int button_drv_release (struct inode *nd, struct file *filp)
{
    int minor = iminor(nd);
    struct gpio_descs *ops = (struct gpio_descs *)filp->private_data;

    if (ops == NULL) {
        printk("Please register button instance %s %s, %d\n", __FILE__, __FUNCTION__, __LINE__);
        return -EIO;
    }

    if (minor >= ops->ndescs) {
        printk("Closing button driver error %s %s, %d\n", __FILE__, __FUNCTION__, __LINE__);
        return -EINVAL;
    }

    filp->private_data = NULL;

    return 0;
}

unsigned int button_drv_poll (struct file *filp, struct poll_table_struct * wait)
{
    return 0;
}

static struct file_operations button_drv_ops = {
    .owner   = THIS_MODULE,
    .read    = button_drv_read,
    .open    = button_drv_open,
    .release = button_drv_release,
    .poll    = button_drv_poll,
};

int btn_hw_drv_probe (struct platform_device *pdev)
{
    int i;

    btn_drv.p_gbtn = gpiod_get_array_optional(&pdev->dev, "gbtn", GPIOD_ASIS);

    if (btn_drv.p_gbtn == NULL) {
        printk("Get \"gbtn-gpios\" property failed! \n");
        return -EIO;
    }

    printk("Get %d gpio members from the \"gbtn gpios\" property!\n", btn_drv.p_gbtn->ndescs);

    btn_drv.major = register_chrdev(btn_drv.major, btn_drv.name, &button_drv_ops);
    btn_drv.class = class_create(THIS_MODULE, btn_drv.name);
    if (IS_ERR(btn_drv.class)) {
        printk("Class create error!\n");
        unregister_chrdev(btn_drv.major, btn_drv.name);
    }
    printk(" %s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

    for (i = 0; i < btn_drv.p_gbtn->ndescs; i++) 
        device_create(btn_drv.class, NULL, MKDEV(btn_drv.major, i), NULL, "gbtn%d", i);
    
    return 0;
}

int btn_hw_drv_remove(struct platform_device *pdev)
{
    int i;
    for (i = 0; i < btn_drv.p_gbtn->ndescs; i++)
        device_destroy(btn_drv.class, MKDEV(btn_drv.major, i));
    gpiod_put_array(btn_drv.p_gbtn);
    return 0;
}

static const struct of_device_id gbtns_id[] = {
    {.compatible = "glen,gbtn"},
    { },
};

static struct platform_driver btn_hw_drv = {
    .driver = {
        .name = "gbtn",
        .of_match_table = gbtns_id,
    },
    .probe = btn_hw_drv_probe,
    .remove = btn_hw_drv_remove,
};

static int __init button_drv_init(void)
{
    int ret;
    ret = platform_driver_register(&btn_hw_drv);
    if (ret)
        pr_err("Unable to initialize button driver\n");
    else
        pr_info("The button driver is registered.\n");

    
    return 0;
}
module_init(button_drv_init);

static void __exit button_drv_exit(void)
{
    platform_driver_unregister(&btn_hw_drv);
    class_destroy(btn_drv.class);
    unregister_chrdev(btn_drv.major, btn_drv.name);
    printk(" %s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
}
module_exit(button_drv_exit);

/* insert author information for module */
MODULE_AUTHOR("glen");
/* insert license for module */
MODULE_LICENSE("GPL");
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值