linux GPIO子系统.

简介:

  gpio源自GPIO控制器, 通常一个芯片会有多个GPIO控制器. GPIO驱动就是GPIO控制器驱动. 这部分驱动通常由

芯片原厂编写,不同芯片厂商的GPIO控制器差异很大, 但是他们基于linux内核会相同的架构编写,实现方式大同小异.

对于普通开发者来说,只需要知道怎么使用(申请,释放,设置输入输出...)就可以了,但是了解GPIO控制器驱动实现方式对深入理解

GPIO是有帮助的.

    首先看下GPIO控制器驱动是如何编写的,然后介绍驱动中如何使用GPIO,最后介绍sysfs,proc,debugfs下的GPIO相关内容(如果有的话.)

1. gpio控制器驱动

        struct gpio_chip 代表一个GPIO控制器(include/linux/gpio/driver.h), 定义如下:

1.1 gpio_chip 结构

        

/**
 * struct gpio_chip 是GPIO控制器的抽象, 代表一个GPIO控制器.
*/
struct gpio_chip {
	const char		*label;   //标签
	struct gpio_device	*gpiodev; //gpio_chip 关联的gpio_deivce, 一个gpiochip 就是一个gpio_device.
	struct device		*parent;  // gpio_device的父设备, 通常是注册这个chip设备.
	struct module		*owner;   // 所有者

	/**
	 * GPIO控制器驱动要实现的GPIO操作函数. 当然不必实现全部的功能.
	*/
	int			(*request)(struct gpio_chip *chip,unsigned offset);
	void		(*free)(struct gpio_chip *chip,unsigned offset);
	int			(*get_direction)(struct gpio_chip *chip,unsigned offset);
	int			(*direction_input)(struct gpio_chip *chip,unsigned offset);
	int			(*direction_output)(struct gpio_chip *chip,unsigned offset, int value);
	int			(*get)(struct gpio_chip *chip,unsigned offset);
	void		(*set)(struct gpio_chip *chip,unsigned offset, int value);
	void		(*set_multiple)(struct gpio_chip *chip,unsigned long *mask,unsigned long *bits);
	int			(*set_config)(struct gpio_chip *chip, unsigned offset,unsigned long config);
	int			(*to_irq)(struct gpio_chip *chip,unsigned offset);
	void		(*dbg_show)(struct seq_file *s,struct gpio_chip *chip);

	/**
	 * 一个gpio_chip 管理一组GPIO, 系统中所有GPIO拥有唯一的编号.
	 * base:指定当前gpio_chip 管理的这组GPIO的初始编号,=-1 表示使用动态分配的初始编号
	 * ngpio: 这ge gpio_chip 管理的GPIO数量.
	*/
	int			base;  
	u16			ngpio;
	const char		*const *names; //别名, 通常不设置.
	bool			can_sleep;

#if IS_ENABLED(CONFIG_GPIO_GENERIC)
	unsigned long (*read_reg)(void __iomem *reg);
	void (*write_reg)(void __iomem *reg, unsigned long data);
	unsigned long (*pin2mask)(struct gpio_chip *gc, unsigned int pin);
	void __iomem *reg_dat;
	void __iomem *reg_set;
	void __iomem *reg_clr;
	void __iomem *reg_dir;
	int bgpio_bits;
	spinlock_t bgpio_lock;
	unsigned long bgpio_data;
	unsigned long bgpio_dir;
#endif

	/*由GPIO管理中断,处理中断事件.*/
#ifdef CONFIG_GPIOLIB_IRQCHIP
	/*
	 * With CONFIG_GPIOLIB_IRQCHIP we get an irqchip inside the gpiolib
	 * to handle IRQs for most practical cases.
	 */

	/**
	 * @irq:
	 *
	 * Integrates interrupt chip functionality with the GPIO chip. Can be
	 * used to handle IRQs for most practical cases.
	 */
	struct gpio_irq_chip irq;
#endif


	/**
	 * 使用GPIO 设备树, 如果开启则可以使用
	 * of_get_name_gpio... 等等 of相关函数
	*/
#if defined(CONFIG_OF_GPIO)
	/*
	 * If CONFIG_OF is enabled, then all GPIO controllers described in the
	 * device tree automatically may have an OF translation
	 */

	/**
	 * @of_node:
	 * GPIO 控制器的设备树节点.
	 */
	struct device_node *of_node;

	/**
	 * @of_gpio_n_cells:
	 * 描述一个GPIO(不是gpio_chip)用到的关键字数量,
	 * Number of cells used to form the GPIO specifier.
	 */
	unsigned int of_gpio_n_cells;

	/**
	 * @of_xlate:
	 *
	 * Callback to translate a device tree GPIO specifier into a chip-
	 * relative GPIO number and flags.
	 */
	int (*of_xlate)(struct gpio_chip *gc,
			const struct of_phandle_args *gpiospec, u32 *flags);
#endif
};

1.2 注册一个gpio_chip

        驱动中只需要填充一个 struct chio_chip结构体然后调用注册函数即可,重点是注册函数做了什么.

static int sprd_gpio_probe(struct platform_device *pdev)
{
	struct gpio_irq_chip *irq;
	struct sprd_gpio *sprd_gpio;
	struct resource *res;
	int ret;

	sprd_gpio = devm_kzalloc(&pdev->dev, sizeof(*sprd_gpio), GFP_KERNEL);
	if (!sprd_gpio)
		return -ENOMEM;

	sprd_gpio->irq = platform_get_irq(pdev, 0);
	if (sprd_gpio->irq < 0) {
		dev_err(&pdev->dev, "Failed to get GPIO interrupt.\n");
		return sprd_gpio->irq;
	}

	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	sprd_gpio->base = devm_ioremap_resource(&pdev->dev, res);
	if (IS_ERR(sprd_gpio->base))
		return PTR_ERR(sprd_gpio->base);

	spin_lock_init(&sprd_gpio->lock);

	sprd_gpio->chip.label = dev_name(&pdev->dev);
	sprd_gpio->chip.ngpio = SPRD_GPIO_NR;
	sprd_gpio->chip.base = -1;
	sprd_gpio->chip.parent = &pdev->dev;
	sprd_gpio->chip.of_node = pdev->dev.of_node;
	sprd_gpio->chip.request = sprd_gpio_request;
	sprd_gpio->chip.free = sprd_gpio_free;
	sprd_gpio->chip.get = sprd_gpio_get;
	sprd_gpio->chip.set = sprd_gpio_set;
	sprd_gpio->chip.direction_input = sprd_gpio_direction_input;
	sprd_gpio->chip.direction_output = sprd_gpio_direction_output;

	irq = &sprd_gpio->chip.irq;
	irq->chip = &sprd_gpio_irqchip;
	irq->handler = handle_bad_irq;
	irq->default_type = IRQ_TYPE_NONE;
	irq->parent_handler = sprd_gpio_irq_handler;
	irq->parent_handler_data = sprd_gpio;
	irq->num_parents = 1;
	irq->parents = &sprd_gpio->irq;

	ret = devm_gpiochip_add_data(&pdev->dev, &sprd_gpio->chip, sprd_gpio);
	if (ret < 0) {
		dev_err(&pdev->dev, "Could not register gpiochip %d\n", ret);
		return ret;
	}

	platform_set_drvdata(pdev, sprd_gpio);
	return 0;
}

重点是注册函数,调用关系如下:evm_gpiochip_add_data() ->  gpiochip_add_data(chip, data); ->  gpiochip_add_data_with_key(chip, data, NULL)

函数如下:

int gpiochip_add_data_with_key(struct gpio_chip *chip, void *data,
			       struct lock_class_key *key)
{
	unsigned long	flags;
	int		status = 0;
	unsigned	i;
	int		base = chip->base;
	struct gpio_device *gdev;

	/*
	 * First: allocate and populate the internal stat container, and
	 * set up the struct device.
	 */
	gdev = kzalloc(sizeof(*gdev), GFP_KERNEL);
	if (!gdev)
		return -ENOMEM;
	gdev->dev.bus = &gpio_bus_type;
	gdev->chip = chip;
	chip->gpiodev = gdev;
	if (chip->parent) {
		gdev->dev.parent = chip->parent;
		gdev->dev.of_node = chip->parent->of_node;
	}

#ifdef CONFIG_OF_GPIO
	/* If the gpiochip has an assigned OF node this takes precedence */
	if (chip->of_node)
		gdev->dev.of_node = chip->of_node;
#endif

	/**
	 * 使用编号管理工具生成一个从0开始的编号.  例如 gpiochip0
	*/
	gdev->id = ida_simple_get(&gpio_ida, 0, 0, GFP_KERNEL);
	if (gdev->id < 0) {
		status = gdev->id;
		goto err_free_gdev;
	}
	dev_set_name(&gdev->dev, "gpiochip%d", gdev->id);  //设置了 dev de kobject
	device_initialize(&gdev->dev);
	dev_set_drvdata(&gdev->dev, gdev);
	if (chip->parent && chip->parent->driver)
		gdev->owner = chip->parent->driver->owner;
	else if (chip->owner)
		/* TODO: remove chip->owner */
		gdev->owner = chip->owner;
	else
		gdev->owner = THIS_MODULE;

	/**
	 * 为每个引脚 struct gpio_desc	*descs 申请空间
	 * kcalloc 申请的是连续的初始化为0的空间,这点很重要,后面会使用数组放温暖这些数据.
	*/
	gdev->descs = kcalloc(chip->ngpio, sizeof(gdev->descs[0]), GFP_KERNEL);  
	if (!gdev->descs) {
		status = -ENOMEM;
		goto err_free_ida;
	}

	if (chip->ngpio == 0) {
		chip_err(chip, "tried to insert a GPIO chip with zero lines\n");
		status = -EINVAL;
		goto err_free_descs;
	}

	if (chip->label)
		gdev->label = kstrdup(chip->label, GFP_KERNEL);
	else
		gdev->label = kstrdup("unknown", GFP_KERNEL);
	if (!gdev->label) {
		status = -ENOMEM;
		goto err_free_descs;
	}

	/*设置gpio_device 支持多少个引脚*/
	gdev->ngpio = chip->ngpio;
	gdev->data = data;

	spin_lock_irqsave(&gpio_lock, flags);

	/*
	 * TODO: this allocates a Linux GPIO number base in the global
	 * GPIO numberspace for this chip. In the long run we want to
	 * get *rid* of this numberspace and use only descriptors, but
	 * it may be a pipe dream. It will not happen before we get rid
	 * of the sysfs interface anyways.
	 */
	if (base < 0) {
		base = gpiochip_find_base(chip->ngpio);
		if (base < 0) {
			status = base;
			spin_unlock_irqrestore(&gpio_lock, flags);
			goto err_free_label;
		}
		/*
		 * TODO: it should not be necessary to reflect the assigned
		 * base outside of the GPIO subsystem. Go over drivers and
		 * see if anyone makes use of this, else drop this and assign
		 * a poison instead.
		 */
		chip->base = base;
	}
	gdev->base = base;

	/*所有的GPIO设备都在一个 gpio_devices 链表里*/
	status = gpiodev_add_to_list(gdev);
	if (status) {
		spin_unlock_irqrestore(&gpio_lock, flags);
		goto err_free_label;
	}

	spin_unlock_irqrestore(&gpio_lock, flags);

	for (i = 0; i < chip->ngpio; i++) {
		struct gpio_desc *desc = &gdev->descs[i];

		desc->gdev = gdev;
		/*
		 * REVISIT: most hardware initializes GPIOs as inputs
		 * (often with pullups enabled) so power usage is
		 * minimized. Linux code should set the gpio direction
		 * first thing; but until it does, and in case
		 * chip->get_direction is not set, we may expose the
		 * wrong direction in sysfs.
		 */

		if (chip->get_direction) {
			/*
			 * If we have .get_direction, set up the initial
			 * direction flag from the hardware.
			 */
			int dir = chip->get_direction(chip, i);

			if (!dir)
				set_bit(FLAG_IS_OUT, &desc->flags);
		} else if (!chip->direction_input) {
			/*
			 * If the chip lacks the .direction_input callback
			 * we logically assume all lines are outputs.
			 */
			set_bit(FLAG_IS_OUT, &desc->flags);
		}
	}

#ifdef CONFIG_PINCTRL
	INIT_LIST_HEAD(&gdev->pin_ranges);
#endif

	status = gpiochip_set_desc_names(chip);
	if (status)
		goto err_remove_from_list;

	status = gpiochip_irqchip_init_valid_mask(chip);
	if (status)
		goto err_remove_from_list;

	status = gpiochip_add_irqchip(chip, key);
	if (status)
		goto err_remove_chip;

	status = of_gpiochip_add(chip);
	if (status)
		goto err_remove_chip;

	acpi_gpiochip_add(chip);

	/*
	 * By first adding the chardev, and then adding the device,
	 * we get a device node entry in sysfs under
	 * /sys/bus/gpio/devices/gpiochipN/dev that can be used for
	 * coldplug of device nodes and other udev business.
	 * We can do this only if gpiolib has been initialized.
	 * Otherwise, defer until later.
	 */
	if (gpiolib_initialized) {
		status = gpiochip_setup_dev(gdev);
		if (status)
			goto err_remove_chip;
	}
	return 0;

err_remove_chip:
	acpi_gpiochip_remove(chip);
	gpiochip_free_hogs(chip);
	of_gpiochip_remove(chip);
	gpiochip_irqchip_free_valid_mask(chip);
err_remove_from_list:
	spin_lock_irqsave(&gpio_lock, flags);
	list_del(&gdev->list);
	spin_unlock_irqrestore(&gpio_lock, flags);
err_free_label:
	kfree(gdev->label);
err_free_descs:
	kfree(gdev->descs);
err_free_ida:
	ida_simple_remove(&gpio_ida, gdev->id);
err_free_gdev:
	/* failures here can mean systems won't boot... */
	pr_err("%s: GPIOs %d..%d (%s) failed to register\n", __func__,
	       gdev->base, gdev->base + gdev->ngpio - 1,
	       chip->label ? : "generic");
	kfree(gdev);
	return status;
}

函数gpiochip_add_data_with_key()用于注册一个gpio设备struct gpio_device *gdev 代表一个软件上的gpio_device,  dev_set_name(&gdev->dev, "gpiochip%d", gdev->id); 设置了设备的名称, 系统启动后可以在 ls -l /dev 看到相应字符设备. 

struct gpio_device 结构体如下:

struct gpio_device {
	int			id;
	struct device		dev;   
	struct cdev		chrdev;   //gpio_deivce的字符设备.
	struct device		*mockdev;
	struct module		*owner;
	struct gpio_chip	*chip;   //gpio_device 管理的额gpio_chip描述结构体
	struct gpio_desc	*descs;  //设备支持的所有的gpio, 一个 struct gpio_desc 代表一个gpio 实例
	int			base;
	u16			ngpio;    //总共有多少个GPIO
	char			*label;
	void			*data;
	struct list_head        list;

#ifdef CONFIG_PINCTRL
	/*
	 * If CONFIG_PINCTRL is enabled, then gpio controllers can optionally
	 * describe the actual pin range which they serve in an SoC. This
	 * information would be used by pinctrl subsystem to configure
	 * corresponding pins for gpio usage.
	 */
	struct list_head pin_ranges;
#endif
};

可以看到它包裹了一个字符设备, 这个字符设备将会注册到系统, 调用的是gpiochip_setup_dev(gdev);函数, 

函数根据主设备号和gpio_device->id 生成一个设备号, (GPIO核心代码已经申请了gpio_devt 设备号, 子设备号

数量是255),调用cdev_device_add 函数注册进系统, 可以通过 ls -l /dev 看到注册的设备

最后调用gpiochip_sysfs_register() 添加sysfs接口, 可以通过 ls /sys/class/gpio/ 查看sysfs接口

代码实现如下:

static int gpiochip_setup_dev(struct gpio_device *gdev)
{
	int status;

	/*初始化字符设备*/
	cdev_init(&gdev->chrdev, &gpio_fileops);
	gdev->chrdev.owner = THIS_MODULE;
	gdev->dev.devt = MKDEV(MAJOR(gpio_devt), gdev->id);

	/*把字符设备注册进系统*/
	status = cdev_device_add(&gdev->chrdev, &gdev->dev);
	if (status)
		return status;

	chip_dbg(gdev->chip, "added GPIO chardev (%d:%d)\n",
		 MAJOR(gpio_devt), gdev->id);

	/*创建 sysfs 文件接口*/
	status = gpiochip_sysfs_register(gdev);
	if (status)
		goto err_remove_device;

	/* From this point, the .release() function cleans up gpio_device */
	gdev->dev.release = gpiodevice_release;
	pr_debug("%s: registered GPIOs %d to %d on device: %s (%s)\n",
		 __func__, gdev->base, gdev->base + gdev->ngpio - 1,
		 dev_name(&gdev->dev), gdev->chip->label ? : "generic");

	return 0;

err_remove_device:
	cdev_device_del(&gdev->chrdev, &gdev->dev);
	return status;
}

1.3 注册sysfs接口

前面看到调用gpiochip_sysfs_register(gdev) 函数注册了sysfs接口.主要是指定了sysfs接口的目录,以及几个基础属性,源码如下:

int gpiochip_sysfs_register(struct gpio_device *gdev)
{
	struct device	*dev;
	struct device	*parent;
	struct gpio_chip *chip = gdev->chip;

	/*
	 * Many systems add gpio chips for SOC support very early,
	 * before driver model support is available.  In those cases we
	 * register later, in gpiolib_sysfs_init() ... here we just
	 * verify that _some_ field of gpio_class got initialized.
	 */
	if (!gpio_class.p)
		return 0;

	/*
	 * For sysfs backward compatibility we need to preserve this
	 * preferred parenting to the gpio_chip parent field, if set.
	 */
	if (chip->parent)
		parent = chip->parent;
	else
		parent = &gdev->dev;

	/* use chip->base for the ID; it's already known to be unique */
	/**
	 * gpio_class 是提前初始化的class, name ="gpio", 所以 gpiochip的sys接口文件位于/sys/class/gpio目录下
	 * gpiochip_groups: gpiochipXXX 文件夹下支持的属性.
	 * static struct attribute *gpiochip_attrs[] = {
		&dev_attr_base.attr,
		&dev_attr_label.attr,
		&dev_attr_ngpio.attr,
		NULL,
		};
		ATTRIBUTE_GROUPS(gpiochip);
	*/
	dev = device_create_with_groups(&gpio_class, parent,
					MKDEV(0, 0),
					chip, gpiochip_groups,
					"gpiochip%d", chip->base);
	if (IS_ERR(dev))
		return PTR_ERR(dev);

	mutex_lock(&sysfs_lock);
	gdev->mockdev = dev;
	mutex_unlock(&sysfs_lock);

	return 0;
}

1.4 sysfs的export接口.

 1.3小节介绍到, device_create_with_group 函数创建了sysfs接口, 它所属的类是gpio_class gpio_class 相关代码位于drivers/gpio/gpiolib-sysfs.c 目录. 它有两个重要的属性, 如下:

static struct attribute *gpio_class_attrs[] = {
	&class_attr_export.attr,
	&class_attr_unexport.attr,
	NULL,
};
ATTRIBUTE_GROUPS(gpio_class);

这两个属性就是 应用层导出引脚的底层实现,

2. 驱动程序中使用GPIO

        在驱动中使用通常要借助设备树, 通常的定义如下:

/*1*/
gpio = <&gpio2 RK_PD7 GPIO_ACTIVE_HIGH>;
/*2*/
vbus-gpio = <&gpio3 26 GPIO_ACTIVE_HIGH>;
/*3*/
gpio = <&gpio0 RK_PB4 GPIO_ACTIVE_HIGH>;
pinctrl-names = "default";
pinctrl-0 = <&usbotg_pwren_h>;
            

如何在设备树中定义GPIO可以参考Documentation/devicetree/bindings/gpio.

2.1基于GPIO编号的API

        每个GPIO被逻辑上分配了一个GPIO编号, 例如进入/sys/class/gpio/gpiochipxxx 目录cat base

cat ngpio 可以查看一个 gpio-chip 支持的起始GPIO编号和总的GPIO数量. 不同平台是不一样. 例如vbus-gpio = <&gpio3 26 GPIO_ACTIVE_HIGH>; 可能的编号可以是 (3 -1) * 32 + 26 其中的3是gpio_chip的编号.幸运的是通常不需要我么手动计算.of_get_named_gpio()函数可以根据名字找到GPIO并返回GPIO编号.

        GPIO相关API如下:

//namexxx = <&gpio1 6 GPIO_ACTIVE_HIGH>
of_get_named_gpio(np, "namexxxxxxx", 0);// 根据设备树中定义的名字返回gpio编号.
gpio_request() //of_get_named_gpio() 只是找到GPIO编号, gpio_request获取了才能用,如果已经获取过,就会失败.
gpio_free();
gpio_is_valid();// 判断GPIO 是否可用.
gpiod_direction_output();   //设置方向
gpiod_direction_input();
gpio_set_value()  //仅对输出的有效,设置高低. 设置的是active的值.
gpio_get_value()  //获取GPIO的高低.
gpio_get_value_cansleep(); 

/*获取并使用一个GPIO的流程*/
GPIO_number = of_get_named_gpio(np, "name_xxx", 0);
if (gpio_is_valid(GPIO_number)) {
	rc = gpio_request(dev_data->ear_dirl_gpio, "musb vbus detect");
	if (rc) 
	{
	}
	gpio_direction_output(GPIO_number,0);
} else {
}

gpio_set_value();

 2.2 基于struct gpio_desc 的API

struct gpio_desc 用于描述一个GPIO, GPIO控制器驱动中已经申请了gpio_device实例. 结构体示例如下:

struct gpio_desc {
	struct gpio_device	*gdev;
	unsigned long		flags;
/* flag symbols are bit numbers */
#define FLAG_REQUESTED	0
#define FLAG_IS_OUT	1
#define FLAG_EXPORT	2	/* protected by sysfs_lock */
#define FLAG_SYSFS	3	/* exported via /sys/class/gpio/control */
#define FLAG_ACTIVE_LOW	6	/* value has active low */
#define FLAG_OPEN_DRAIN	7	/* Gpio is open drain type */
#define FLAG_OPEN_SOURCE 8	/* Gpio is open source type */
#define FLAG_USED_AS_IRQ 9	/* GPIO is connected to an IRQ */
#define FLAG_IS_HOGGED	11	/* GPIO is hogged */
#define FLAG_SLEEP_MAY_LOOSE_VALUE 12	/* GPIO may loose value in sleep */

	/* Connection label */
	const char		*label;
	/* Name of the GPIO */
	const char		*name;
};

函数struct gpio_desc *__must_check devm_gpiod_get_optional(struct device *dev,
                               const char *con_id,
                               enum gpiod_flags flags)

区别of_get_named_gpio(np, "namexxxxxxx", 0); 这里的 con_id 也是名字, 但是这里规定在设备树里定义的的名字必须以 -gpio 或 -gpios结尾. 这里的con_id 是出掉-gpio(s) 之后的名字.如下所示

powerdown-gpios = <&gpio7 12 GPIO_ACTIVE_HIGH>;
sensor->pwdn_gpio = devm_gpiod_get_optional(dev, "powerdown",
						    GPIOD_OUT_HIGH);

其他相关API如下,

devm_gpiod_get_optional() //获取成功后GPIO就会被占用.
void gpiod_put(struct gpio_desc *desc)//释放
gpiod_direction_output();  //输出
gpiod_direction_output_raw();
gpiod_set_raw_value();//输出值
gpiod_set_raw_array_value();
gpiod_set_raw_value_cansleep();//可在休眠上下文
gpiod_set_raw_array_value_cansleep();
gpiod_direction_input(); //输入
gpiod_get_value();
gpiod_get_value_cansleep()
gpiod_get_raw_value();
gpiod_get_raw_value_cansleep()

2.3 GPIO编号和gpio_desc之间的转换

        查看源码,可以看到GPIO编号和gpio_desc之间的转换

3. 应用层接口

3.1 sysfs接口

        在1.3有介绍sysfs接口,这里简单说明:

echo 79> /sys/class/gpio/export       //导出编号为79 的gpio 如果成功会再 gpio 目录下生成gpio79文件

进入 gpio79目录:

echo in/out/low/high > direction //设置引脚输入方向,out默认输出低电平,

cat value或echo 1/0 > value 获取或设置GPIO电平

使用如上命令设置GPIO.

3.2 debug 接口

cat /sys/kernel/debug/gpio 可以查看GPIO使用情况

cat /proc/interrupts   //也可以查看中断相关的信息, 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值