Linux内核总线系统 —— 通用总线和平台设备

Linux内核输入子系统框架_Bin Watson的博客-CSDN博客 这篇文章中,我们详细分析了输入子系统。了解到了 dev 和 handler 分层的思想。而在 jz2440_输入子系统驱动程序_Bin Watson的博客-CSDN博客 这篇文章中,我们实际操作编写了一个基于输入子系统的按键驱动程序。

而这种分层思想,在内核中是非常常见的。我们也可以实现我们自己的分层驱动程序,这种驱动模型称为 bus-drv-dev 模式,总线驱动设备模型。

总线驱动程序与具体设备的驱动程序相比,总线驱动程序与核心内核代码的工作要密切得多。另外,总线驱动程序向相关的设备驱动程序提供功能和选项的方式,也不存在标准的接口。这是因为,不同的总线系统之间,使用的硬件技术可能差异很大。但这并不意味着,负责管理不同总线的代码没有共同点。相似的总线采用相似的概念,还引入了通用驱动程序模型,在一个主要数据结构的集合中管理所有系统总线,采用最小公分母的方式,尽可能降低不同总线驱动程序之间的差异。

内核支持大量总线,可能涉及多种硬件平台,也有可能只涉及一种平台。所以我不可能详细地讨论所有版本,这里我们只会仔细讨论 PCI 总线。因为其设计相对现代,而且具备一种强大的系统总线所应有的所有共同和关键要素。此外,在Linux支持的大多数体系结构上都使用了 PCI 总线。我还会讨论广泛使用、系统无关的的 USB 总线,该总线用于外设。

1. 总线驱动设备框架

在这里插入图片描述总线设备驱动模式由三个部分组成:

  1. 虚拟总线 bus;
  2. 设备 device;
  3. 驱动 driver;

bus:总线是处理器和设备之间的通道。总线有多种类型,每种总线可以挂载多个设备。
在设备模型中,所有的设备都通过总线相连,以总线来管理设备和驱动函数。总线在内核中由 bus_type 结构体表示。

device:设备就是连接在总线上的物理实体。设备是有功能之分的。具有相同功能的设备被归到一个类,如输入设备(鼠标,键盘,游戏杆等)。Linux 系统中每个设备都用一个 device 结构体的表示。

当添加一个新的 device 时,会执行下列操作:

  1. 把 device 放入 bus 的 dev 链表上;
  2. 从 bus 的 drv 链表取出每个 driver,用 bus 的 .match 函数判断 drv 能否支持 dev;
  3. 若可以支持,则调用 drv 的 .probe 函数。

driver:驱动程序是在 CPU 运行时,提供操作的软件接口。所有的设备必须有与之配套驱动程序才能正常工作。一个驱动程序可以驱动多个类似或者完全不同的设备。

当添加一个新的 driver 时,会执行下列操作:

  1. 把 driver 放入 bus 的 drv 链表上;
  2. 从 bus 的 dev 链表取出每个 device,用 bus 的 .match 函数判断 drv 能否支持 dev;
  3. 若可以支持,则调用 drv 的 .probe 函数。

总线 bus 上有两条链表,分别挂载当前总线上所有的 device 和所有的 driver。当注册(添加 device_register/driver_register)一个新的 device/driver 到总线上时,会触发总线上的 .match 函数被调用,进行该 device/driver 去和另一个链表 driver/device 上的设备进行匹配,若匹配成功就会调用 driver 中的 .probe 函数。

注意:.probe 函数是提供一种将 device 和 driver 连接起来的机制。而具体 .probe 函数如何实现,是否要记录 device 的信息?这些操作都由实现者去决定,并不是强制的。

2. 通用总线模型

2.1 数据结构定义

device

<device.h>

struct device { 
    struct klist klist_children; 
    struct klist_node knode_parent; /* 兄弟结点链表中的结点 */ 
    struct klist_node knode_driver; 
    struct klist_node knode_bus; 
    struct device * parent;
    struct kobject kobj; 
    char bus_id[BUS_ID_SIZE]; 		/* 在父总线上的位置 */ 
    /*...省略...*/ 
    struct bus_type * bus; 			/* 所在总线设备的类型 */ 
    struct device_driver *driver; 	/* 分配当前device实例的驱动程序 */ 
    void *driver_data;				/* 驱动程序的私有数据 */ 
    void *platform_data; 			/* 特定于平台的数据,设备模型代码不会访问 */ 
    /*...省略...*/ 
    void (*release)(struct device * dev); 
}; 
  • klist 和 klist_node 数据结构是我们熟悉的 list_head 数据结构的增强版,其中增加了与锁和引用计数相关的成员。

    klist 是一个表头,而 klist_node 是一个链表元素。这种类型的链表只用于通用设备模型,内核的其余部分不会使用。

  • 嵌入的 kobject 控制通用对象属性。

  • 有一些成员用于建立设备之间的层次关系

    klist_children 是一个链表的表头,该链表包含了指定设备的所有子设备。如果设备包含于父设备的 klist_children 链表中,则将 knode_parent 用作链表元素。

    parent 指向父结点的 device 实例。

    因为一个设备驱动程序能够服务多个设备(例如,系统中安装了两个相同的扩展卡),

  • knode_driver 用作链表元素,用于将所有被同一驱动程序管理的设备连接到一个链表中。

  • driver 指向控制该设备的设备驱动程序的数据结构(下面的成员包括更多相关信息)。

  • bus_id 唯一指定了该设备在宿主总线上的位置(不同总线类型使用的格式也会有所不同)。例如,设备在 PCI 总线上的位置由一个具有以下格式的字符串唯一地定义:<总线编号>:<插槽编号>.<功能编号>。

  • bus 是一个指针,指向该设备所在总线(更多信息见下文)的数据结构的实例。

  • driver_data 是驱动程序的私有成员,不能由通用代码修改。它可用于指向与设备协作必需、但又无法融入到通用方案的特定数据。platform_data 和 firmware_data 也是私有成员,可用于将特定于体系结构的数据和固件信息关联到设备。通用驱动程序模型也不会访问这些数据。

  • release 是一个析构函数,用于在设备(或device实例)不再使用时,将分配的资源释放回内核。

内核提供了标准函数 device_register,用于将一个新设备添加到内核的数据结构。该函数在下文讨论。device_get 和 device_put 一对函数用于引用计数。

driver

<driver.h>

struct device_driver { 
    const char * name;
    struct bus_type * bus;
    struct kobject kobj; 
    struct klist klist_devices; 
    struct klist_node knode_bus; 
    /*...省略...*/ 
    int (*probe) (struct device * dev); 
    int (*remove) (struct device * dev); 
    void (*shutdown) (struct device * dev); 
    int (*suspend) (struct device * dev, pm_message_t state); 
    int (*resume) (struct device * dev); 
}; 

各个成员的语义如下。

  • name 指向一个正文串,用于唯一标识该驱动程序。

  • bus 指向一个表示总线的对象,并提供特定于总线的操作(更详细的内容,请参见下文)。

  • klist_devices 是一个标准链表的表头,其中包括了该驱动程序控制的所有设备的 device 实例。链表中的各个设备通过 device->knode_driver 彼此连接。

  • knode_bus 用于连接一条公共总线上的所有设备。

  • probe 是一个函数,用于检测系统中是否存在能够用该设备驱动程序处理的设备。

  • 删除系统中的设备时会调用 remove。

  • shutdown、suspend 和 resume 用于电源管理。

驱动程序使用内核的标准函数 driver_register 注册到系统中,该函数在下文讨论。

通用驱动程序模型不仅表示了设备,还用另一个数据结构表示了总线,定义如下:

bus

<device.h>

struct bus_type { 
    const char * name;
    /*...省略...*/  
    struct kset subsys; 
    struct kset drivers; 
    struct kset devices; 
    struct klist klist_devices; 
    struct klist klist_drivers; 
    /*...省略...*/  
    int (*match)(struct device * dev, struct device_driver * drv); 
    int (*uevent)(struct device *dev, struct kobj_uevent_env *env); 
    int (*probe)(struct device * dev); 
    int (*remove)(struct device * dev); 
    void (*shutdown)(struct device * dev); 
    int (*suspend)(struct device * dev, pm_message_t state); 
    /*...省略...*/  
    int (*resume)(struct device * dev); 
    /*...省略...*/  
}; 
  • name 是总线的文本名称。特别地,它用于在 sysfs 文件系统中标识该总线。

  • 与总线关联的所有设备和驱动程序,使用 drivers 和 devices 成员,作为集合进行管理。

  • 内核创建两个链表(klist_devices 和 klist_drivers )来保存相同的数据。这些链表使内核能够快速扫描所有资源(设备和驱动程序)

  • subsys 提供与总线子系统的关联。对应的总线出现在 /sys/bus/busname。

  • match 指向一个函数,试图查找与给定设备匹配的驱动程序。

  • add 用于通知总线新设备已经添加到系统。

  • 在有必要将驱动程序关联到设备时,会调用 probe。该函数检测设备在系统中是否真正存在。

  • remove 删除驱动程序和设备之间的关联。例如,在将可热插拔的设备从系统中移除时,会调用该函数。

  • shutdown、suspend 和 resume 函数用于电源管理。

2.2 注册

注册总线

在可以注册设备及其驱动程序之前,需要有总线。因此我们从 bus_register 开始,该函数向系统添加一个新总线。

首先,通过嵌入的 kset 类型成员 subsys,将新总线添加到总线子系统:

drivers/base/bus.c

int bus_register(struct bus_type * bus) 
{ 
    int retval; 
    retval = kobject_set_name(&bus->subsys.kobj, "%s", bus->name); 
    bus->subsys.kobj.kset = &bus_subsys; 
    retval = subsystem_register(&bus->subsys); 
    /*...省略...*/  
}; 

总线需要了解相关设备及其驱动程序的所有有关信息,因此总线对二者注册了 kset。

两个 kset 分别是 drivers 和 devices,都将总线作为父结点:

drivers/base/bus.c

int bus_register(struct bus_type * bus) 
{ 
    /*...省略...*/  
    kobject_set_name(&bus->devices.kobj, "devices"); 
    bus->devices.kobj.parent = &bus->subsys.kobj; 
    retval = kset_register(&bus->devices); 
    
    kobject_set_name(&bus->drivers.kobj, "drivers"); 
    bus->drivers.kobj.parent = &bus->subsys.kobj; 
    bus->drivers.ktype = &driver_ktype; 
    retval = kset_register(&bus->drivers); 
    /*...省略...*/  
} 

注册设备

注册设备包括两个独立的步骤,如图6-22所示。具体是:初始化设备的数据结构,并将其加入到数据结构的网络中。
在这里插入图片描述

  • device_initialize主 要通过 kobj_set_kset_s(dev, devices_subsys) 将新设备添加到设备子系统

  • device_add 首先,将通过 device->parent 指定的父子关系转变为一般的内核对象层次结构

    int device_add(struct device *dev) 
    { 
        struct device *parent = NULL; 
        /*...省略...*/ 
        parent = get_device(dev->parent); 
        kobj_parent = get_device_parent(dev, parent); 
        dev->kobj.parent = kobj_parent; 
        /*...省略...*/  
    } 
    

    在设备子系统中注册该设备只需要调用一次 kobject_add,因为在 device_initialize 中已经将该设备设置为子系统的成员了。

    int device_add(struct device *dev) 
    { 
        /*...省略...*/ 
        kobject_set_name(&dev->kobj, "%s", dev->bus_id); 
        error = kobject_add(&dev->kobj); 
        /*...省略...*/  
    } 
    

    然后调用 bus_add_device 在 sysfs 中添加两个链接:一个在总线目录下指向设备,另一个在设备的目录下指向总线子系统。 bus_attach_device 试图自动探测设备。如果能够找到适当的驱动程序,则将设备添加到 bus->klist_devices。设备还需要添加到父结点的子结点链表中(此前,设备知道其父结点,但父结点不知道该子结点的存在)。

    int device_add(struct device *dev) 
    { 
        /*...省略...*/
        error = bus_add_device(dev); 
        bus_attach_device(dev); 
        if (parent) 
            klist_add_tail(&dev->knode_parent, &parent->klist_children); 
        /*...省略...*/  
    } 
    

注册设备驱动程序

在进行一些检查和初始化工作之后,driver_register 调用 bus_add_driver 将一个新驱动程序添加到一个总线。同样,驱动程序首先要有名字,然后注册到通用数据结构的框架中:

drivers/base/bus.c

int bus_add_driver(struct device_driver *drv) 
{ 
    struct bus_type * bus = bus_get(drv->bus); 
    int error = 0; 
    /*...省略...*/ 
    error = kobject_set_name(&drv->kobj, "%s", drv->name); 
    drv->kobj.kset = &bus->drivers; 
    error = kobject_register(&drv->kobj); 
    /*...省略...*/  
} 

如果总线支持自动探测,则调用 driver_attach。该函数迭代总线上的所有设备,使用驱动程序的 match 函数进行检测,确定是否有某些设备可使用该驱动程序管理。

最后,将该驱动程序添加到总线上注册的所有驱动程序的链表中。

drivers/base/bus.

int bus_add_driver(struct device_driver *drv) 
{ 
    /*...省略...*/ 
    if (drv->bus->drivers_autoprobe) 
        /* 该函数迭代总线上的所有设备,
         * 使用驱动程序的 match 函数进行检测,
         * 确定是否有某些设备可使用该驱动程序管理 
         */
        error = driver_attach(drv); 
    /*...省略...*/ 
    /* 将驱动程序添加到总线上注册的所有驱动程序的链表中 */
    klist_add_tail(&drv->knode_bus, &bus->klist_drivers); 
    /*...省略...*/ 
} 

3. 平台设备

在 Linux 内核中有多种设备类型,如:PCI、USB 等,其中一种为平台设备。

平台设备的总线是 platform_bus_type,是基于通用总线 bus_type 实现的一种内核中已经定义好的了的总线。

3.1 平台设备结构

总线

platform_bus_type 定义如下:

struct bus_type platform_bus_type = {
	.name		= "platform",
	.dev_attrs	= platform_dev_attrs,
	.match		= platform_match,
	.uevent		= platform_uevent,
	.suspend	= platform_suspend,
	.suspend_late	= platform_suspend_late,
	.resume_early	= platform_resume_early,
	.resume		= platform_resume,
};

设备:平台设备的数据结构是 platform_device,对 device 进行了扩展:

struct platform_device {
	const char	* name;
	u32		id;
	struct device	dev;
	u32		num_resources;		/* resource这数组的长度 */
	struct resource	* resource;	/* 资源数组,用于记录该设备所具有的硬件资源 */
};
  • resource:用于记录设备的资源

    struct resource {
    	resource_size_t start;
    	resource_size_t end;
    	const char *name;
    	unsigned long flags;
    	struct resource *parent, *sibling, *child;
    };
    

    一个独立的挂接在 CPU 总线上的设备单元,一般都需要一段线性的地址空间来描述设备自身。
    Linux 采用 struct resource 结构体来描述一个挂接在 CPU 总线上的设备实体(32位 CPU 的总线地址范围是 0~4G):

    • start:描述设备实体在 CPU 总线上的线性起始物理地址;
    • end:描述设备实体在 CPU 总线上的线性结尾物理地址;
    • name:描述这个设备(资源)实体的名称。这个名字开发人员可以随意起,但最好贴切;
    • flag:描述这个设备(资源)实体的一些共性和特性的标志位;

驱动:平台设备的数据结构是 platform_driver,对 device_driver 进行了扩展:

struct platform_driver {
	int (*probe)(struct platform_device *);
	int (*remove)(struct platform_device *);
	void (*shutdown)(struct platform_device *);
	int (*suspend)(struct platform_device *, pm_message_t state);
	int (*suspend_late)(struct platform_device *, pm_message_t state);
	int (*resume_early)(struct platform_device *);
	int (*resume)(struct platform_device *);
	struct device_driver driver;
};

3.2 驱动注册函数

platform_driver_register 函数分析

我们以 gpio_keys.c 为例:

在其 init 函数中,通过 platform_driver_register 函数注册一个平台设备的驱动:

static int __init gpio_keys_init(void)
{
	return platform_driver_register(&gpio_keys_device_driver);
}
  • gpio_keys_device_driver 的实现如下:

    struct platform_driver gpio_keys_device_driver = {
    	.probe		= gpio_keys_probe,
    	.remove		= __devexit_p(gpio_keys_remove),
    	.driver		= {
    		.name	= "gpio-keys",
    	}
    };
    

platform_driver_register 的定义如下:

int platform_driver_register(struct platform_driver *drv)
{
	drv->driver.bus = &platform_bus_type;	/* 将drv总线类型设置为platform_bus_type */
    /* 设置默认的操作函数 */
	if (drv->probe)
		drv->driver.probe = platform_drv_probe;
	if (drv->remove)
		drv->driver.remove = platform_drv_remove;
	if (drv->shutdown)
		drv->driver.shutdown = platform_drv_shutdown;
	if (drv->suspend)
		drv->driver.suspend = platform_drv_suspend;
	if (drv->resume)
		drv->driver.resume = platform_drv_resume;
	return driver_register(&drv->driver);	/* 注册驱动到总线 */
}

driver_register 的执行流程分析

driver_register 的执行流程如下:

driver_register(&drv->driver);
	|-> error = driver_attach(drv);
		|-> bus_for_each_dev(drv->bus, NULL, drv, __driver_attach);
		|		while ((dev = next_device(&i)) && !error)
		|			error = __driver_attach(dev, data);
		|-> __driver_attach(dev, data);	
		|		if (!dev->driver)
		|			driver_probe_device(drv, dev);
		|-> driver_probe_device(drv, dev);
		|		if (drv->bus->match && !drv->bus->match(dev, drv))
		|			goto done;
		|		ret = really_probe(dev, drv);
		|-> really_probe(dev, drv);
        |    	if (dev->bus->probe) {
        |            ret = dev->bus->probe(dev);
        |            if (ret)
        |                goto probe_failed;
        |        } else if (drv->probe) {
        |            ret = drv->probe(dev);
        |            if (ret)
        |                goto probe_failed;
        |        }
  1. 首先 driver_register 会调用 driver_attach 函数,后者会遍历 bus 上的每一个 dev,对每一个 dev 都执行 __driver_attach

  2. __driver_attach 会调用 driver_probe_device 函数。

    后者首先会使用 bus 的 match 函数,进行 driver 和 device 的匹配,若 bus 没有实现 match 函数,则会使用 driver 的 match 函数进行匹配。

  3. match 匹配成功后会调用 really_probe 函数。该函数最终会调用 bus 的 probe 函数,如果 bus 没有实现 probe 函数,则会调用 driver 的 probe 函数。

而前面我们知道 platform_bus_type 中是定义了 .match = platform_match 函数的。

platform_match

platform_match 的函数实现如下:

static int platform_match(struct device * dev, struct device_driver * drv)
{
	struct platform_device *pdev = container_of(dev, struct platform_device, dev);
	return (strncmp(pdev->name, drv->name, BUS_ID_SIZE) == 0);
}

平台设备的匹配规则很简单,使用 strncmp()比对 pdev->name 和 drv->name,如果一样就说明它们可以匹配。

那么就会调用 .probe 函数,由于 platform_bus_type 没有实现 .probe 函数,因此最终是调用 driver 的 .probe 函数。

我们以这个LED驱动程序(jz2440_基于平台设备的LED驱动程序_Bin Watson的博客-CSDN博客)为例:

static int leds_device_probe(struct platform_device *pdev)
{
	struct resource *res, *pin;
	int error;
    
	/*...省略...*/
	
	/* 根据platform_device的资源,进行ioremap操作 */
	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	pin = platform_get_resource(pdev, IORESOURCE_IO, 0);
	if (!res || !pin) {
		printk(DRIVER_NAME "leds driver get platform resource [%s] failed.\n",
			(res == NULL ? "IORESOURCE_MEM" : "IORESOURCE_IO"));
		error = -1;
		goto fail3;
	}
	gpio_con = (volatile unsigned long*)ioremap(res->start, res->end - res->start + 1);
	gpio_dat = gpio_con + 1;
	led_pin  = pin->start;

	/*...省略...*/
}

在 driver 的 .probe 函数中调用了 platform_get_resource 来获取设备中的资源,也就是 platform_device 中指定的 resource 资源。

3.3 平台设备注册

platform_device_register 函数分析

我们以 jz2440_基于平台设备的LED驱动程序_Bin Watson的博客-CSDN博客 这个程序为例分析:入口函数调用了 platform_device_register 进行注册。

static int __init leds_dev_init(void)
{
	platform_device_register(&leds_device);
	printk(KERN_INFO "leds device register.\n");
	return 0;
}
  • leds_device 是一个 platform_device 设备,其定义如下:

    static struct platform_device leds_device = {
    	.name		= DEVICE_NAME,
    	.id		  	= -1,
    	.num_resources  = ARRAY_SIZE(leds_resource),
    	.resource	  	= leds_resource,
    	.dev		= {
    		.release = leds_dev_release,
    	},
    };
    

    主要是 leds_resource,记录了该设备所具有的硬件资源,其定义如下:

    static struct resource leds_resource[] = {
    	[0] = {	/* 配置和数据 寄存器 */
    		.start = S3C2440_GPFCON,
    		.end   = S3C2440_GPFCON + 8 - 1,
    		.flags = IORESOURCE_MEM,
    	},
    	[1] = {
    		.start = 4,	/* 第4位 */
    		.end   = 4,
    		.flags = IORESOURCE_IO,
    		.name  = "led"
    	},
    };
    

    详细的成员分析参考 Linux内核 struct resource 结构体_Bin Watson的博客-CSDN博客

    在前面我们分析了平台驱动的 .probe 函数会通过 platform_get_resource 来获取平台设备的资源。通过这种操作,我们就可以将设备的资源与驱动程序进行分离,提高了驱动程序的通用性。

platform_device_register 的定义如下:

int platform_device_register(struct platform_device * pdev)
{
	device_initialize(&pdev->dev);
	return platform_device_add(pdev);
}

device_initialize 完成通用设备的初始化工作。

详细看 platform_device_add 的定义如下:

int platform_device_add(struct platform_device *pdev)
{
	/*...省略...*/
	pdev->dev.bus = &platform_bus_type;	/* 总线类型设置为platform_bus_type */
	/*...省略...*/

	for (i = 0; i < pdev->num_resources; i++) {
		struct resource *p, *r = &pdev->resource[i];

		if (r->name == NULL)	/* 如果资源没有指定名字,则设置名字为设备id */
			r->name = pdev->dev.bus_id;

		p = r->parent;
		if (!p) {
			if (r->flags & IORESOURCE_MEM)	
				p = &iomem_resource;        
			else if (r->flags & IORESOURCE_IO)
				p = &ioport_resource;
		}
		/* 将 IORESOURCE_MEM 类型的资源加入到 iomem_resource 类型的资源树上
		 * 将 IORESOURCE_IO 类型的资源加入到 ioport_resource 类型的资源树上 
		 * 这么做的目的是防止资源冲突,多个设备使用了相同的资源。
		 */
		if (p && insert_resource(p, r)) {
			printk(KERN_ERR
			       "%s: failed to claim resource %d\n",
			       pdev->dev.bus_id, i);
			ret = -EBUSY;
			goto failed;
		}
	}
	/*...省略...*/
	ret = device_add(&pdev->dev);	/* 将设备添加到内核中,这个与通用设备的相同。 */
	if (ret == 0)
		return ret;
	/*...省略...*/
}

4. 其它设备模型

Linux 内核中除了平台设备之外,还有其它类型的设备,如:

USB设备,其定义如下:

struct bus_type usb_bus_type = {
	.name =		"usb",
	.match =	usb_device_match,
	.uevent =	usb_uevent,
	.suspend =	usb_suspend,
	.resume =	usb_resume,
};

PCI设备,其定义如下:

struct bus_type pci_bus_type = {
	.name		= "pci",
	.match		= pci_bus_match,
	.uevent		= pci_uevent,
	.probe		= pci_device_probe,
	.remove		= pci_device_remove,
	.suspend	= pci_device_suspend,
	.suspend_late	= pci_device_suspend_late,
	.resume_early	= pci_device_resume_early,
	.resume		= pci_device_resume,
	.shutdown	= pci_device_shutdown,
	.dev_attrs	= pci_dev_attrs,
};

这些类型将在其它后续文章中进行分析。

参考

《深入Linux内核架构》第6章第7节。

韦东山《高级驱动第二期》。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值