【LINUX驱动框架学习】Linux GPIO驱动以及底层实现方式

GPIO(通用目的输入/输出端口)是一种灵活的软件控制的数字信号。大多数的嵌入式
处理器都引出一组或多组的 GPIO,并且部分普通管脚通过配置可以复用为 GPIO。利用可
编程逻辑器件,或总线(如 I 2 C、SPI)转 GPIO 芯片,也可以扩展系统的 GPIO。不管是何
种 GPIO,GPIOLIB 为内核和用户层都提供了标准的操作方法。
GPIOLIB 的接口十分简洁。在 GPIOLIB,所有的 GPIO 都是用整形的 GPIO 编号标识。
只要获得要操作 GPIO 的编号,就可以调用 GPIOLIB 提供的方法操作 GPIO。

GPIOLIB 的内核接口

GPIOLIB 的内核接口是指:若某些 GPIO 在 GPIOLIB 框架下被驱动后,GPIOLIB 为内
核的其它代码操作该 GPIO 而提供的标准接口。

1. GPIO 的申请和释放

GPIO 在使用前,必须先调用 gpio_request()函数申请 GPIO:

int gpio_request(unsigned gpio, const char *label);

该函数的 gpio 参数为 GPIO 编号;label 参数为 GPIO 的标识字符串,可以随意设定。
若该函数调用成功,将返回 0 值;否则返回非 0 值。gpio_request()函数调用失败的原因可能
为 GPIO 的编号不存在,或在其它地方已经申请了该 GPIO 编号而还没有释放。
当 GPIO 使用完成后,应当调用 gpio_free()函数释放 GPIO:
void gpio_free(unsigned gpio);

2. GPIO 的输出控制

在操作 GPIO 输出信号前,需要调用 gpio_direction_output()函数把 GPIO 设置为输出方
向:

int gpio_direction_output(unsigned gpio, int value);

把 GPIO 设置为输出方向后,参数 value 为默认的输出电平:1 为高电平;0 为低电平。
GPIO 被设置为输出方向后,就可以调用 gpio_set_value()函数控制 GPIO 输出高电平或
低电平:

void gpio_set_value(unsigned gpio, int value);

该函数的 value 参数可取值为:1 为高电平;0 为低电平。

3. GPIO 的输入控制

当需要从 GPIO 读取输入电平状态前,需要调用 gpio_direction_input()函数设置 GPIO 为
输入方向:

int gpio_direction_input(unsigned gpio);

在 GPIO 被设置为输入方向后,就可以调用 gpio_get_value()函数读取 GPIO 的输入电平
状态:

int gpio_get_value(unsigned gpio);

该函数的返回值为 GPIO 的输入电平状态:1 为高电平;0 为低电平。

4. GPIO 的中断映射

大多数的嵌入式处理器的 GPIO 引脚在被设置为输入方向后,可以用于外部中断信号的
输入。这些中断号和 GPIO 编号通常有对应关系,因此 GPIOLIB 为这些 GPIO 提供了
gpio_to_irq()函数用于通过 GPIO 编号而获得该 GPIO 中断号:

int gpio_to_irq(unsigned gpio);

gpio_to_irq()函数调用完成后,返回 GPIO 中断号。
由于并不是所有的 GPIO 都可以作为外部中断信号输入端口,所以 gpio_to_irq()函数不
是对所有的 GPIO 都强制实现的

基于这些函数可以在屏蔽底层寄存器的情况下写驱动

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/ioctl.h>
#include <linux/delay.h>
#include <linux/bcd.h>
#include <linux/capability.h>
#include <linux/rtc.h>
#include <linux/cdev.h>
#include <linux/miscdevice.h>
#include <linux/gpio.h>
#include <linux/slab.h>

#include <../arch/arm/mach-mx28/mx28_pins.h>
#include "gpio_driver.h"


struct gpio_info {
	u32  	 	   pin;
	char 	 	   pin_name[20];
	struct miscdevice *pmiscdev;
};

static struct gpio_info *gpio_info_file[255];

static struct gpio_info *all_gpios_info;

static struct gpio_info all_gpios_info_283[] ={
        {PINID_AUART0_CTS,   "gpio-DURX",       NULL},
        {PINID_AUART0_RTS,   "gpio-DUTX",       NULL},
        {PINID_AUART0_RX,    "gpio-URX0",       NULL},
        {PINID_AUART0_TX,    "gpio-UTX0",       NULL},
        {PINID_AUART1_RX,    "gpio-URX1",       NULL},
        {PINID_AUART1_TX,    "gpio-UTX1",       NULL},
        {PINID_SSP2_SCK,     "gpio-URX2",       NULL},
        {PINID_SSP2_MOSI,    "gpio-UTX2",       NULL},
        {PINID_SSP2_MISO,    "gpio-URX3",       NULL},
        {PINID_SSP2_SS0,     "gpio-UTX3",       NULL},
        {PINID_SAIF0_BITCLK, "gpio-URX4",       NULL},
        {PINID_SAIF0_SDATA0, "gpio-UTX4",       NULL},
        /* modify by luozhizhuo*/
        {PINID_GPMI_RDY3,    "gpio-CRX0", 	NULL},
        {PINID_GPMI_RDY2,    "gpio-CTX0", 	NULL},
        {PINID_GPMI_CE3N,    "gpio-CRX1", 	NULL},
        {PINID_GPMI_CE2N,    "gpio-CTX1", 	NULL},
        {PINID_LCD_D22,      "gpio-RUN",        NULL},
        {PINID_LCD_D23,      "gpio-ERR",    	NULL},
        /*end modify*/
        {PINID_SSP3_MISO,    "gpio-MISO",       NULL},
        {PINID_SSP3_MOSI,    "gpio-MOSI",       NULL},
        {PINID_SSP3_SCK,     "gpio-CLK",        NULL},
        {PINID_SSP3_SS0,     "gpio-CS",         NULL},
        {PINID_PWM1,         "gpio-SDA",        NULL},
        {PINID_PWM0,         "gpio-SCL",        NULL},
        {PINID_LCD_D17,      "gpio-P1.17",      NULL},
        {PINID_LCD_D18,      "gpio-P1.18",      NULL},
        {PINID_SSP0_DATA4,   "gpio-P2.4",       NULL},
        {PINID_SSP0_DATA5,   "gpio-P2.5",       NULL},
        {PINID_SSP0_DATA6,   "gpio-P2.6",       NULL},
        {PINID_SSP0_DATA7,   "gpio-P2.7",       NULL},
        {PINID_SSP1_SCK,     "gpio-P2.12",      NULL},
        {PINID_SSP1_CMD,     "gpio-P2.13",      NULL},
        {PINID_SSP1_DATA0,   "gpio-P2.14",      NULL},
        {PINID_SSP1_DATA3,   "gpio-P2.15",      NULL},
        {PINID_SAIF0_MCLK,   "gpio-P3.20",      NULL},
        {PINID_SAIF0_LRCLK,  "gpio-P3.21",      NULL},
        {PINID_SAIF1_SDATA0, "gpio-P3.26",      NULL},
        {PINID_SPDIF,        "gpio-P3.27",      NULL},
        {0,                "",          NULL},   //the end
};


/*--------------------------------------------------------------------------------------------------------
*/
static int gpio_open(struct inode *inode, struct file *filp);
static int  gpio_release(struct inode *inode, struct file *filp);
ssize_t gpio_write(struct file *filp, const char __user *buf, size_t count,
                loff_t *f_pos);
static int gpio_ioctl(struct inode *inode,struct file *flip,unsigned int command,unsigned long arg);
static int gpio_init(void);
static void gpio_exit(void);

/*--------------------------------------------------------------------------------------------------------
*/

static int gpio_open(struct inode *inode, struct file *filp)
{
	struct gpio_info *gpio_info_tmp;
	u32 minor = iminor(inode);

	gpio_info_tmp = gpio_info_file[minor];

	gpio_free(MXS_PIN_TO_GPIO(gpio_info_tmp->pin));
	if (gpio_request(MXS_PIN_TO_GPIO(gpio_info_tmp->pin), gpio_info_tmp->pin_name)) {
		printk("request %s gpio faile \n", gpio_info_tmp->pin_name);
		return -1;
	}

	filp->private_data = gpio_info_file[minor];

	return 0;
}

static int  gpio_release(struct inode *inode, struct file *filp)
{
	struct gpio_info *gpio_info_tmp = (struct gpio_info *)filp->private_data;

	gpio_free(MXS_PIN_TO_GPIO(gpio_info_tmp->pin));

	return 0;
}


ssize_t gpio_write(struct file *filp, const char __user *buf, size_t count,
                loff_t *f_pos)
{
	struct gpio_info *gpio_info_tmp = (struct gpio_info *)filp->private_data;
	char data[2];

	//printk("make: %s \n", gpio_info_tmp->pin_name);
	copy_from_user(data, buf, 2);

	data[0] = data[0] - '0';

        if (data[0] == 1 || data[0] == 0) {
                gpio_direction_output(MXS_PIN_TO_GPIO(gpio_info_tmp->pin), data[0]);
        }



	return count;
}


static ssize_t gpio_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
	struct gpio_info *gpio_info_tmp = (struct gpio_info *)file->private_data;
	int  value = 0;
	char data[3];
	static int flg	= 0;

	if (flg == 1) {
		flg = 0;
		return 0;
	}

	gpio_direction_input(MXS_PIN_TO_GPIO(gpio_info_tmp->pin));
	value = gpio_get_value(MXS_PIN_TO_GPIO(gpio_info_tmp->pin));
	data[0] = value ? 1 : 0;

	data[0] = data[0] + '0';
	data[1] = '\n';
	data[3] = -1;

	copy_to_user(buf, data, 2);
	
	flg = 1;
        return 3;
}

static int gpio_ioctl(struct inode *inode,struct file *flip,unsigned int command,unsigned long arg)
{
	struct gpio_info *gpio_info_tmp = (struct gpio_info *)flip->private_data;
	int  data = 0;
	
	switch (command) {
	case SET_GPIO_HIGHT: 
		gpio_direction_output(MXS_PIN_TO_GPIO(gpio_info_tmp->pin), 1);
		break;
	case SET_GPIO_LOW:
		gpio_direction_output(MXS_PIN_TO_GPIO(gpio_info_tmp->pin), 0);
		break;
	case GET_GPIO_VALUE:
		gpio_direction_input(MXS_PIN_TO_GPIO(gpio_info_tmp->pin));
		data = gpio_get_value(MXS_PIN_TO_GPIO(gpio_info_tmp->pin));
		data = data ? 1 : 0;
		copy_to_user((void *)arg, (void *)(&data), sizeof(int));
		break;
	default:
		printk("cmd error \n");
		
		return -1;
	}

	return 0;
}


static struct file_operations gpio_fops={
	.owner		= THIS_MODULE,
	.open 		= gpio_open,
	.write		= gpio_write,
	.read		= gpio_read,
	.release	= gpio_release,
	.ioctl		= gpio_ioctl,
};

	

static int __init gpio_init(void)
{
	int i = 0;
	int ret = 0;

	all_gpios_info = all_gpios_info_283;

	for (i = 0; all_gpios_info[i].pin != 0; i++) {
		all_gpios_info[i].pmiscdev = kmalloc(sizeof(struct miscdevice), GFP_KERNEL);
		if (all_gpios_info[i].pmiscdev == NULL) {
			printk("unable to malloc memory \n");
			return -1;
		}

		
		memset(all_gpios_info[i].pmiscdev, 0, sizeof(struct miscdevice));
		all_gpios_info[i].pmiscdev->name  = all_gpios_info[i].pin_name;
		all_gpios_info[i].pmiscdev->fops  = &gpio_fops;	
		all_gpios_info[i].pmiscdev->minor = MISC_DYNAMIC_MINOR;

		ret = misc_register(all_gpios_info[i].pmiscdev);
		if (ret) {
			printk("misc regist faile \n");
			return -1;
		}

		gpio_info_file[all_gpios_info[i].pmiscdev->minor] = &(all_gpios_info[i]);
		
		printk("build device i:%d dev:/dev/%s \n", i, all_gpios_info[i].pmiscdev->name);
	}
		printk("zlg EasyARM-imx283 gpio driver up. \n");


	return 0;
}

static void __exit gpio_exit(void)
{
	int i = 0;

	for (i = 0; all_gpios_info[i].pin != 0; i++) {
		misc_deregister(all_gpios_info[i].pmiscdev);
	}
	printk("zlg EasyARM-imx28xx gpio driver down.\n");
}

module_init(gpio_init);
module_exit(gpio_exit);

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("zhuguojun, ZhiYuan Electronics Co, Ltd.");
MODULE_DESCRIPTION("GPIO DRIVER FOR MAGICARM270.");

底层实现方式(以MX28为例)

对于GPIOLIB底层实现的源码位于drivers/gpiolib.c

GPIOLIB 对每组 GPIO 都用一个 gpio_chip 对象来实现其驱动。gpio_chip 也称为 GPIO
控制器,其定义

struct gpio_chip {
const char *label;
struct device  *dev;
struct module  *owner;
int  (*request)(struct gpio_chip *chip, unsigned offset);
void (*free)(struct gpio_chip *chip, unsigned offset);
int  (*direction_input)(struct gpio_chip *chip, unsigned offset);
int  (*get)(struct gpio_chip *chip, unsigned offset);
int  (*direction_output)(struct gpio_chip *chip, unsigned offset, int value);
int  (*set_debounce)(struct gpio_chip *chip, unsigned offset, unsigned debounce);
void (*set)(struct gpio_chip *chip, unsigned offset, int value);
int  (*to_irq)(struct gpio_chip *chip, unsigned offset);
void (*dbg_show)(struct seq_file *s, struct gpio_chip *chip);
int  base;
u16 ngpio;
const char *const *names;
unsigned  can_sleep:1;
unsigned  exported:1;
};

下面介绍 gpio_chip 的部分成员:
base 该组 GPIO 的起始值。
ngpio 该组 GPIO 的数量。
owner 该成员表示所有者,一般设置为 THIS_MODULE。
request在对该组的GPIO调用gpio_request()函数时,该成员指向的实现函数将被调用。
在该成员指向的实现函数中,通常需要执行指定 GPIO 的初始化操作。
在实现函数中都是用索引值来区别组内的 GPIO。索引值是指组内的某一 GPIO 编号相
对于该组 GPIO 起始值(base)的偏移量,例如,组内第 1 个 GPIO 的索引值为 0、第 2 个
GPIO 的索引值为 1„„ 实现函数的 offset 参数为要操作 GPIO 的索引值(以下相同)。
free 在对该组的 GPIO 调用 gpio_free()函数时,该成员指向的实现函数将被调用。在该
成员的实现函数中,通常需要执行指定 GPIO 硬件资源的释放操作。
direction_input 在对该组的 GPIO 调用 gpio_direction_input()函数时,该成员指向的实
现函数将被调用。在该成员的实现函数中,需要把指定的 GPIO 设置为输入方向。
get 在对该组的 GPIO 调用 gpio_get_value()函数时,该成员指向的实现函数将被调用。
在该成员的实现函数中,需要返回指定 GPIO 的电平输入状态。
direction_output 在对该组的 GPIO 调用 gpio_direction_output()函数时,该成员指向的实
现函数将被调用。在该成员的实现函数中,需要把指定的 GPIO 设置为输出方向。
set 在对该组的 GPIO 调用 gpio_set_value()函数时,该成员指向的实现函数将被调用。
在该成员的实现函数中,需要把指定的 GPIO 设置为指定的电平输出状态。

以mx28为例查看如何实现gpiolib
mx28

gpio是以数字序号为索引的IMX283内部驱动实现了一个1-160 5*32的gpiolib
所以外部接口函数都可以通过一个数字访问到IO口

通过下面这个结构体管理gpio_chip
struct mxs_gpio_port {
int id;
int irq;
int child_irq;
struct mxs_gpio_chip *chip;
struct gpio_chip port;
};

可以通过下面这个函数将gpio_chip添加到内核中

int __init mxs_add_gpio_port(struct mxs_gpio_port *port)
{
	int i, ret;
	if (!(port && port->chip))
		return -EINVAL;

	if (mxs_valid_gpio(port))
		return -EINVAL;

	if (mxs_gpios[port->id])
		return -EBUSY;

	mxs_gpios[port->id] = port;

	port->port.base = port->id * PINS_PER_BANK;
	port->port.ngpio = PINS_PER_BANK;
	port->port.can_sleep = 1;
	port->port.exported = 1;
	port->port.to_irq = mxs_gpio_to_irq;
	port->port.direction_input = mxs_gpio_input;
	port->port.direction_output = mxs_gpio_output;
	port->port.get = mxs_gpio_get;
	port->port.set = mxs_gpio_set;
	port->port.request = mxs_gpio_request;
	port->port.free = mxs_gpio_free;
	port->port.owner = THIS_MODULE;
	ret = gpiochip_add(&port->port);
	if (ret < 0)
		return ret;

	if (port->child_irq < 0)
		return 0;

	for (i = 0; i < PINS_PER_BANK; i++) {
		gpio_irq_chip.mask(port->child_irq + i);
		set_irq_chip(port->child_irq + i, &gpio_irq_chip);
		set_irq_handler(port->child_irq + i, handle_level_irq);
		set_irq_flags(port->child_irq + i, IRQF_VALID);
	}
	set_irq_chained_handler(port->irq, mxs_gpio_irq_handler);
	set_irq_data(port->irq, port);
	return ret;
};

自己实现一个GPIO控制器

这里为 GPIO3_4 和 GPIO3_5 安排的 GPIO 编号分别为 160161。这两个 GPIO 在同
一个 GPIO 组里,该组的 GPIO 编号起始编号为 160。实现该组的 GPIO 控制器代码如程序

struct gpio_chip mx28_gpio_chip = {
.label = "example_gpio",
.owner  = THIS_MODULE,
.base = 160,
|  .ngpio  = 2,
.request  = mxs_gpio_request,
.free = mxs_gpio_free,
.direction_input = mxs_gpio_input,
.get = mxs_gpio_get,
.direction_output = mxs_gpio_output,
.set = mxs_gpio_set,
143
.exported = 1,
};
由于该组的 GPIO 编号范围已经超出了内核源码定义的最大值,所以必须把
MXS_ARCH_NR_GPIOS 宏定义的值改为足够大的值:
#define MXS_ARCH_NR_GPIOS (160 + 2)
然后编译内核,把新内核固件烧写到目标机中。
下面介绍 mx28_gpio_chip 各成员函数的实现:
## request 的实现
request 成员的实现函数为 mxs_gpio_request(),该函数的代码如程序清单 4.7 所示。当
内核对编号为 160 ~ 161 的 GPIO 调用 gpio_request()函数时,该函数将被调用。
程序清单 4.7 mxs_gpio_request()函数的实现代码
static int mxs_gpio_request(struct gpio_chip *chip, unsigned int pin)
{
void __iomem *addr = PINCTRL_BASE_ADDR;
pin += 4;
__raw_writel(0x3 << pin * 2, addr + HW_PINCTRL_MUXSEL6_SET);  /* set as GPIO  */
return 0;
}
pin 参数为要申请 GPIO 编号的索引值:对于编号为 160 的 GPIO,pin 的值为 0;对于
编号为 161 的 GPIO,pin 的值为 1。由于 GPIO3_4 和 GPIO3_5 在 HW_PINCTRL_MUXSEL6
寄存器的操作位分别在 8 ~ 910 ~ 11,所以在该函数中需要根据 GPIO 的索引值来设置
寄存器的相应位。
## direction_input 的实现
direction_input成员的实现函数为mxs_gpio_input(),该函数的代码如程序清单 4.8所示。
当内核对编号为 160 ~ 161 的 GPIO 调用 gpio_direction_input ()函数时,该函数将被调用。

static int mxs_gpio_input(struct gpio_chip *chip, unsigned int index)
{
void __iomem *base = PINCTRL_BASE_ADDR;
index += 4;
__raw_writel(1 << index, base + HW_PINCTRL_DOE3_CLR);
return 0;
}
index 参数为要操作 GPIO 的索引值。在该函数中,需要根据传入 GPIO 的索引值在
HW_PINCTRL_DOE3 寄存器的正确位设置为 0,以控制相应的 GPIO 为输入工作状态。
 
## get 的实现

get 成员的实现函数为 mxs_gpio_get(),该函数的代码如程序清单 4.9 所示。当内核对编
号为 160 ~ 161 的 GPIO 调用 gpio_get_value()函数时,该函数将被调用。
程序清单 4.9 mxs_gpio_get()函数的实现代码
static int mxs_gpio_get(struct gpio_chip *chip, unsigned int index)
{
unsigned int data;
void __iomem *base = PINCTRL_BASE_ADDR;
index += 4;
data = __raw_readl(base + HW_PINCTRL_DIN3);
return data & (1 << index);
}
在该函数中,需要根据传入GPIO的索引值从HW_PINCTRL_DIN3寄存器的正确位中,
获得 GPIO 的输入电位状态。
## direction_output 的实现
direction_output 成员的实现函数为 mxs_gpio_output(),该函数的代码如程序清单 4.10
所示。当内核对编号为 160 ~ 161 的 GPIO 调用 gpio_direction_output ()函数时,该函数将被
调用。
程序清单 4.10 mxs_gpio_output()函数的实现代码
static int mxs_gpio_output(struct gpio_chip *chip, unsigned int index, int v)
{
void __iomem *base = PINCTRL_BASE_ADDR;
index += 4;
__raw_writel(1 << index, base + HW_PINCTRL_DOE3_SET); /* 设置为输出工作模式  */
if (v) {  /* 当 v 为非 0 时, 设置 GPIO 输出高电平 */
__raw_writel(1 << index, base + HW_PINCTRL_DOUT3_SET);
} else { /* 当 v 为 0 时,设置 GPIO 输出低电平 */
__raw_writel(1 << index, base + HW_PINCTRL_DOUT3_CLR);
}
return 0;
}
参数 v 为 GPIO 被设置为输出工作模式后,默认的输出值:0 为输出低电平,非 0 为输
出高电平。在该函数中,需要根据输入 GPIO 的索引值,在 HW_PINCTRL_DOE3 寄存器把
GPIO 设置为输出工作状态,然后在 HW_PINCTRL_DOUT3 寄存器设置默认的输出电平。
##  set 的实现
set 成员的实现函数为 mxs_gpio_set(),该函数的实现代码如程序清单 4.11 所示。当内
核对编号为 160 ~ 161 的 GPIO 调用 gpio_set_value ()函数时,该函数将被调用。

程序清单 4.11 mxs_gpio_set()函数的实现代码
static void mxs_gpio_set(struct gpio_chip *chip, unsigned int index, int v)
{
void __iomem *base = PINCTRL_BASE_ADDR;
index += 4;
if (v) {  /* 设置 GPIO 输出高电平 */
__raw_writel(1 << index, base + HW_PINCTRL_DOUT3_SET);
} else { /* 设置 GPIO 输出低电平 */
__raw_writel(1 << index, base + HW_PINCTRL_DOUT3_CLR);
}
}
在该函数中,需要根据输入 GPIO 的索引值,在 HW_PINCTRL_DOUT3 寄存器设置输
出电平。
##  GPIO 控制器的注册和注销
在模块的初始化函数中,需要注册 GPIO 控制器,如程序清单 4.12 所示。
程序清单 4.12 注册 GPIO 控制器的实现代码
static int __init mx28_gpio_init(void)
{
int ret = 0;
ret = gpiochip_add(&mx28_gpio_chip);
if (ret) {
printk("add example gpio faile:%d \n", ret);
goto out;
}
printk("add example gpio success... \n");
out:
return ret;
}
module_init(mx28_gpio_init);

static void __exit mx28_gpio_exit(void)
{
gpiochip_remove(&mx28_gpio_chip);
printk("remove example gpio... \n");
}
module_exit(mx28_gpio_exit);

装载模块以后可以在sys/class/gpio/export目录下找到对应的设备文件

  • 1
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

与光同程

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

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

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

打赏作者

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

抵扣说明:

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

余额充值