Linux驱动开发(速记版)--平台总线

第四十七章 平台总线模型介绍

47.1 什么是平台总线?

        平台总线是Linux内核中的一种虚拟机制,用于连接和匹配平台设备与对应的平台驱动。它简化了设备与驱动之间的绑定过程,提高了系统对硬件的适配性和扩展性。

        当设备或驱动被注册时,平台总线会自动寻找并绑定匹配的组件,让设备能被正确初始化和控制。这种机制增强了嵌入式系统的灵活性和可移植性。

设备、平台总线、驱动的关系

47.2 平台总线的优势

引入平台总线模型后,解决了多设备驱动管理中的重复和冗余问题。其优势包括:

        设备与驱动分离:清晰划分设备和驱动的代码,提高了代码的可读性和维护性。

        增强代码重用性:允许同类型设备共享同一驱动代码,通过配置不同的设备文件来适应不同硬件,减少重复编写。

        减少重复性代码:避免为每个设备编写相似驱动代码,简化了开发流程。

        提高可移植性:使驱动不依赖于特定标准总线,便于跨平台移植和重用。

第四十八章 注册 platform 设备

48.1 注册 platform 设备

48.1.1 platform_device_register() 函数

platform_device_register() 函数是 Linux 内核中用于注册平台设备的一个关键函数。

platform_device结构体包含设备名称设备资源设备 ID 等信息,描述和标识平台设备。

// 平台总线设备注册函数原型  
int platform_device_register(struct platform_device *pdev)  
{  
    // 初始化设备结构体      
    device_initialize(&pdev->dev);  //设置设备的引用计数、类型等基本信息。
  
    // 根据架构设置特定数据(可选)  
    arch_setup_pdev_archdata(pdev);  
  
    // 将平台设备添加到内核中  
    return platform_device_add(pdev); //包括将其添加到设备层级结构和 platform 总线 
}

// 在头文件中的声明  
#include <linux/platform_device.h>  
extern void platform_device_register(struct platform_device *);
/*平台设备结构体*/
struct platform_device {  
    const char *name;     // 设备名称  
    int id;               // 设备ID号  
    struct device dev;    // 内嵌的具体的device结构体,表示设备在设备模型中的抽象  
    u32 num_resources;    // 资源数量  
    struct resource *resource; // 指向设备的资源描述符数组
                               // 资源描述符包含了设备的物理地址、大小、类型等信息。  
    const struct platform_device_id *id_entry; // 用于匹配设备和驱动程序的ID结构体
                                               // 通过该结构体可以实现设备和驱动的自动匹配。  
    // ... 其他成员,如指向设备对应的平台驱动程序的指针等  
};

48.1.2 platform_device_unregister() 函数

        platform_device_unregister() 函数用于从内核中取消注册并移除已经注册的平台设备

        这个函数通过减少设备的引用计数和将其从设备层级结构移除来完成清理工作。

/*从平台总线移除已经注册的设备*/
void platform_device_unregister(struct platform_device *pdev)  
{  
    // 将设备从platform总线的设备列表中移除  
    platform_device_del(pdev);  
  
    // 减少设备的引用计数,必要时释放设备结构体和相关资源  
    platform_device_put(pdev);  
}

// 在头文件中的声明  
#include <linux/platform_device.h>  
extern void platform_device_unregister(struct platform_device *);

48.1.3 platform_device 结构体

        platform_device 结构体是 Linux 内核中用于描述和管理平台设备的数据结构。

        它包含了设备名称设备ID资源信息以及与其他设备模型和架构相关的数据。

/*平台总线设备结构体*/
struct platform_device {  
    const char *name;       // 设备的名称,用于唯一标识  
    int id;                 // 设备的ID,区分同种设备的不同实例,可选,-1 表示不使用  
    bool id_auto;           // ID 是否自动生成,通常不是直接操作的对象  
    struct device dev;      // 基本的设备管理和操作结构体,必须有效且实现 release 方法  
    u32 num_resources;      // 设备资源的数量  
    struct resource *resource; // 指向资源数组的指针  
    const struct platform_device_id *id_entry; // 用于匹配设备和驱动的ID表项  
    char *driver_override;  // 强制指定驱动名称  
    struct mfd_cell *mfd_cell; // 指向多功能设备(MFD)单元的指针  
    struct pdev_archdata archdata; // 存储特定于架构的设备数据  
};
/*资源结构体*/
struct resource {  
    resource_size_t start;  // 资源的起始物理地址  
    resource_size_t end;    // 资源的结束物理地址  
    const char *name;       // 资源的名称  
    unsigned long flags;    // 资源标志,如 IORESOURCE_MEM(内存资源)  
    // ... 其他成员  
};

        在实际使用中,platform_device 的创建和注册通常由内核的初始化代码、设备树(Device Tree)或特定的初始化函数完成,而不是直接在代码中手动构造。

        platform_device_register() 函数用于注册设备

        platform_device_unregister() 用于注销设备

48.1.4 resource 结构体

        resource 结构体用于在 Linux 内核中描述和管理系统资源,如内存区域、I/O 端口、中断等。它定义了资源的起始和结束地址名称标志位,以及与其他资源的父子或兄弟关系

/*资源结构体*/
struct resource {  
    resource_size_t start;    // 资源的起始地址  
    resource_size_t end;      // 资源的结束地址  
    const char *name;         // 资源的名称  
    unsigned long flags;      // 资源的标志位,用于表示属性、特征等  
    // ... 其他成员和宏定义略  
};

/*
示例标志位
#define IORESOURCE_IO       0x00000100  // I/O 端口资源  
#define IORESOURCE_MEM      0x00000200  // 内存资源  
  
// 资源属性和特征相关  
#define IORESOURCE_READONLY 0x00000400  // 资源是只读的  
#define IORESOURCE_CACHEABLE 0x00000800 // 资源支持缓存  
  
// 其他状态和控制  
#define IORESOURCE_DISABLED 0x00001000  // 资源当前被禁用  
#define IORESOURCE_BUSY     0x00002000  // 驱动程序将此资源标记为繁忙
*/

48.2 注册平台总线设备实验

        本实验将注册一个名为 "my_platform_device" 的平台设备。

        当注册平台设备时,该驱动程序提供了两个资源:

        一个内存资源和一个中断资源。这些资源被定义在名为 my_resources 的结构体数组中。

        内存资源:

                起始地址:MEM_START_ADDR(0xFDD60000)

                结束地址:MEM_END_ADDR(0xFDD60004)

                标记:IORESOURCE_MEM

        中断资源:

                中断资源号:IRQ_NUMBER(101)

                标记:IORESOURCE_IRQ

/*注册平台总线设备代码,platform_device.c 代码*/
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/ioport.h>
#define MEM_START_ADDR 0xFDD60000  //设备资源起始地址
#define MEM_END_ADDR 0xFDD60004    //设备资源结束地址  总共4字节
#define IRQ_NUMBER 101             //中断资源号

/* resource结构体数组定义,resources[0]放内存资源,resources[1]放中断资源 */
static struct resource my_resources[] = {
    {
        .start = MEM_START_ADDR, // 内存资源起始地址
        .end = MEM_END_ADDR, // 内存资源结束地址
        .flags = IORESOURCE_MEM, // 标记为内存资源
    },
    {
        .start = IRQ_NUMBER, // 中断资源号
        .end = IRQ_NUMBER, // 中断资源号
        .flags = IORESOURCE_IRQ, // 标记为中断资源
    },
};

/*平台设备释放函数*/
static void my_platform_device_release(struct device *dev)
{
    // 释放各种资源,由plat_form结构体的release成员指向该函数,在
    // platform_device_unregister()被调用且设备被成功注销时执行
}

/*定义平台设备结构体*/
static struct platform_device my_platform_device = {
    .name = "my_platform_device", // 设备名称
    .id = -1, // 设备 ID
    .num_resources = ARRAY_SIZE(my_resources), // 资源数量
    .resource = my_resources, // 资源数组
    .dev.release = my_platform_device_release, // 释放资源的回调函数
};
//platform_device结构体中的.dev.release字段被设置为一个函数,
//那么当platform_device_unregister()被调用且设备被成功注销时,这个函数将被执行。

/*驱动入口*/
static int __init my_platform_device_init(void)
{
    int ret;
    ret = platform_device_register(&my_platform_device); // 注册平台设备
    if (ret) {
        printk(KERN_ERR "Failed to register platform device\n");
        return ret;
    }
    printk(KERN_INFO "Platform device registered\n");
    return 0;
}

/*驱动出口*/
static void __exit my_platform_device_exit(void)
{
    platform_device_unregister(&my_platform_device); // 注销平台设备
    printk(KERN_INFO "Platform device unregistered\n");
}
module_init(my_platform_device_init);
module_exit(my_platform_device_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

        来到/sys/bus/platform/devices 目录下,可以看到我们创建的 my_platform_device 平台总线设备。

第四十九章 注册 platform 驱动实验

49.1 注册 platform 驱动

49.1.1 platform_driver_register() 函数

        platform_driver_register() 在 Linux 内核中用于注册平台驱动程序到内核。

#include <linux/platform_device.h>  

/*注册平台驱动程序*/
int platform_driver_register(struct platform_driver *driver);
#define platform_driver_register(drv) \  
    __platform_driver_register(drv, THIS_MODULE)

int __platform_driver_register(struct platform_driver *drv,   //平台驱动结构体
                               struct module *owner)          //所属模块
{  
    drv->driver.owner = owner;                // 将当前模块与平台驱动程序关联  
    drv->driver.bus = &platform_bus_type;     // 设置总线类型为平台总线  
    drv->driver.probe = platform_drv_probe;   // 设置探测函数,发现匹配设备时回调  
    drv->driver.remove = platform_drv_remove; // 设置移除函数,移除设备时回调  
    drv->driver.shutdown = platform_drv_shutdown; // 设置关机函数,系统关机时调用  
    return driver_register(&drv->driver);     // 注册驱动程序到内核  
}

49.1.2 platform_driver_unregister() 函数

  platform_device_unregister() 用于从 Linux 内核中注销平台设备,以进行清理和释放操作。

#include <linux/platform_device.h> 

/*从内核注销平台驱动*/ 
void platform_driver_unregister(struct platform_driver *pdrv);
/*从内核注销平台驱动*/
void platform_driver_unregister(struct platform_driver *drv)  
{  
    driver_unregister(&drv->driver);  
}
/*移除平台驱动底层代码*/
void driver_unregister(struct device_driver *drv)  
{  
    /*参数检查:验证传入的设备驱动程序指针是否有效。*/
    if (!drv || !drv->p) {  
        printk(KERN_WARNING "Unexpected driver unregister!\n");  
        return;  
    }  
  
    /*移除属性组:从内核中移除与设备驱动程序关联的属性组*/
    if (drv->groups)  
        sysfs_remove_groups(&drv->p->kobj, drv->groups);  
    
    /*从总线中移除驱动程序,可能调用remove相关回调*/
    bus_remove_driver(drv);  
  
    kfree(drv->p->klist_devices.k_list);  
    kfree(drv->p);  
  
    module_put(drv->owner);  
}  

49.1.3 platform_driver 结构体

        platform_driver 结构体是 Linux 内核中用于编写平台设备驱动程序的关键数据结构,它包含驱动的匹配设备ID表,和一系列驱动操作相关的回调函数

/*平台总线驱动结构体*/
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); // 挂起函数  
    int (*resume)(struct platform_device *); // 恢复函数  
  
    struct device_driver driver; // 通用设备驱动程序数据,如驱动程序的名称、总线类型等。
                                 // 其中,name 参数需要与 platform_device 的 .name 参数相同,
                                 // 以便匹配成功并调用 probe 函数。 
    const struct platform_device_id *id_table; // 设备ID表,用于匹配设备和驱动 
                                //确定哪个平台设备与该驱动程序匹配。其优先级高于 driver.name。 
    bool prevent_deferred_probe; // 是否禁用延迟探测  
};

探测函数probe:系统检测到与驱动程序匹配的平台设备时,调用此函数初始化和配置设备。

移除函数remove:当平台设备从系统中移除时,调用此函数来执行清理和释放资源的操作。

关闭函数shutdown:当系统关闭时,调用此函数来执行与平台设备相关的关闭操作。

挂起函数suspend:系统进入挂起状态时,执行与平台设备相关的挂起操作。

恢复函数resume:系统从挂起状态恢复时,执行与平台设备相关的恢复操作。

device_driver结构体

        与设备驱动程序相关的通用数据,如驱动程序的名称、总线类型等。其中,name 参数需要与 platform_device 的 .name 参数相同,以便匹配成功并调用 probe 函数。

设备与驱动关联表id_table

        通过该表,可以确定哪个平台设备与该驱动程序匹配。其优先级高于 driver.name。

探测函数禁用布尔值

        如果设置为 true,则延迟探测将被禁用。

/*平台设备id匹配表*/
static const struct platform_device_id my_id_table[] = {  
    { "my_device", 0 },  
    { },  
};

49.2 注册平台总线驱动实验

// 平台设备的探测函数
static int my_platform_probe(struct platform_device *pdev)
{
    printk(KERN_INFO "my_platform_probe: Probing platform device\n");
    // 添加设备特定的操作
    // ... return 0;
}

// 平台设备的移除函数
static int my_platform_remove(struct platform_device *pdev)
{
    printk(KERN_INFO "my_platform_remove: Removing platform device\n");
    // 清理设备特定的操作
    // ... return 0;
}

// 定义平台驱动结构体
static struct platform_driver my_platform_driver = {
    .probe = my_platform_probe, .remove = my_platform_remove, 
    .driver = {
        .name = "my_platform_device", 
        .owner = THIS_MODULE, 
    }
};

// 模块初始化函数
static int __init my_platform_driver_init(void)
{
    int ret;
    // 注册平台驱动
    ret = platform_driver_register(&my_platform_driver);
    if (ret) {
        printk(KERN_ERR "Failed to register platform driver\n");
        return ret;
    }
    printk(KERN_INFO "my_platform_driver: Platform driver initialized\n");
    return 0;
}

// 模块退出函数
static void __exit my_platform_driver_exit(void)
{
    // 注销平台驱动
    platform_driver_unregister(&my_platform_driver);
    printk(KERN_INFO "my_platform_driver: Platform driver exited\n");
}

        平台总线设备驱动用来处理平台总线设备的生命周期,包括探测、移除、系统关闭、挂起、恢复这几个阶段。

        首先加载平台设备.ko文件,     来到/sys/bus/platform/devices 目录下,可以看到我们创建的 my_platform_device 平台总线设备文件夹

        然后加载平台设备驱动.ko文件,来到/sys/bus/platform/drivers 目录下,可以看到我们创建的 my_platform_driver 平台驱动文件夹就成功生成了。

第五十章 probe 函数编写

        驱动是要控制硬件的,因此正确做法是在 platform_driver的 probe函数中获得设备硬件资源。

50.1 获取 device 资源

方法1:直接访问 platform_device结构体的资源数组

        在Linux内核中,platform_device结构体包含了一个 resource数组,该数组存储了设备的硬件资源信息。驱动程序可以直接访问这个数组来获取所需的资源。

/*直接访问platform_device结构体的resource数组获取资源*/
if (pdev->num_resources >= 2) {  
    struct resource *res_mem = &pdev->resource[0]; // 获取内存资源  
    struct resource *res_irq = &pdev->resource[1]; // 获取中断资源  
    printk("Memory Resource: start = 0x%llx, end = 0x%llx\n", res_mem->start, res_mem->end);  
    printk("IRQ Resource: number = %lld\n", res_irq->start);  
}

方法2:使用 platform_get_resource()获取硬件资源

        platform_get_resource()提供了一种更灵活的方式来获取 platform_device的资源。它允许驱动程序根据资源的类型和索引来获取特定的资源。

/*获取平台设备的资源*/
struct resource *platform_get_resource(struct platform_device *pdev, //平台设备结构体
                                       unsigned int type,            //资源类型宏
                                       unsigned int num);            //资源编号
/*
示例标志位
#define IORESOURCE_IO       0x00000100  // I/O 端口资源  
#define IORESOURCE_MEM      0x00000200  // 内存资源  
  
// 资源属性和特征相关  
#define IORESOURCE_READONLY 0x00000400  // 资源是只读的  
#define IORESOURCE_CACHEABLE 0x00000800 // 资源支持缓存  
  
// 其他状态和控制  
#define IORESOURCE_DISABLED 0x00001000  // 资源当前被禁用  
#define IORESOURCE_BUSY     0x00002000  // 驱动程序将此资源标记为繁忙
*/
struct resource *res_mem;  
res_mem = platform_get_resource(pdev, IORESOURCE_MEM, 0); // 获取第一个内存资源  
if (res_mem) {  
    printk("Memory Resource: start = 0x%llx, end = 0x%llx\n", res_mem->start, res_mem->end);  
} else {  
    // 处理获取内存资源失败的情况  
    printk("Failed to get memory resource\n");  
}

第五十三章 点亮 LED 灯(平台总线)

        编写完成的 platform_led.c 代码。注册平台总线设备模块。

        open设备文件以后,向设备文件写入1,通过寄存器点灯,写入0,通过寄存器关灯。        

        在 模块入口函数中,通过

        platform_driver_register(&my_platform_driver); 注册平台驱动

        在 平台驱动的 probe函数中,注册字符设备cdev。完成整套逻辑。

struct device_test{
    dev_t dev_num; //设备号
    int major ; //主设备号
    int minor ; //次设备号
    struct cdev cdev_test; // cdev
    struct class *class; //类
    struct device *device; //设备
    char kbuf[32];
    unsigned int *vir_gpio_dr;
};

struct device_test dev1;

/*file_operations结构体填充部分*/
/*打开设备函数*/
static int cdev_test_open(struct inode *inode, struct file *file)
{
    file->private_data=&dev1;//设置私有数据
    printk("This is cdev_test_open\r\n");
    return 0;
}

/*向设备写入数据函数*/
static ssize_t cdev_test_write(struct file *file,
                               const char __user *buf,
                               size_t size,
                               loff_t *off)
{
    struct device_test *test_dev=(struct device_test *)file->private_data;
    // copy_from_user:用户空间向内核空间传数据
    if (copy_from_user(test_dev->kbuf, buf, size) != 0) 
    {
        printk("copy_from_user error\r\n");
        return -1;
    }

    //如果应用层传入的数据是 1,则打开灯
    if(test_dev->kbuf[0]==1){ 
        *(test_dev->vir_gpio_dr) = 0x8000c040; //设置数据寄存器的地址
        printk("test_dev->kbuf [0] is %d\n",test_dev->kbuf[0]); //打印传入的数据
    }
    //如果应用层传入的数据是 0,则关闭灯
    else if(test_dev->kbuf[0]==0) 
    { *(test_dev->vir_gpio_dr) = 0x80004040; //设置数据寄存器的地址
        printk("test_dev->kbuf [0] is %d\n",test_dev->kbuf[0]); //打印传入的数据
    }
    return 0;
}

/**从设备读取数据*/
static ssize_t cdev_test_read(struct file *file, 
                              char __user *buf, 
                              size_t size, 
                              loff_t *off)
{
    struct device_test *test_dev=(struct device_test *)file->private_data;
    // copy_to_user:内核空间向用户空间传数据
    if (copy_to_user(buf, test_dev->kbuf, strlen( test_dev->kbuf)) != 0) 
    {
        printk("copy_to_user error\r\n");
        return -1;
    }
    printk("This is cdev_test_read\r\n");
    return 0;
}

/*用户调用close关闭设备文件时,release函数被调用*/
static int cdev_test_release(struct inode *inode, struct file *file)
{
    printk("This is cdev_test_release\r\n");
    return 0;
}

/*设备操作函数*/
struct file_operations cdev_test_fops = {
    .owner = THIS_MODULE, //将 owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
    .open = cdev_test_open, //将 open 字段指向 chrdev_open(...)函数
    .read = cdev_test_read, //将 open 字段指向 chrdev_read(...)函数
    .write = cdev_test_write, //将 open 字段指向 chrdev_write(...)函数
    .release = cdev_test_release, //将 open 字段指向 chrdev_release(...)函数
};

/*平台设备驱动函数编写*/
static int my_platform_driver_probe(struct platform_device *pdev)
{
    struct resource *res_mem;
    int ret;
    res_mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res_mem) {
        dev_err(&pdev->dev, "Failed to get memory resource\n");
        return -ENODEV;
    }

    /*注册字符设备驱动*/
    /*1 创建设备号*/
    ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "alloc_name"); //动态分配设备号
    if (ret < 0)
    {
            goto err_chrdev;
    }

    printk("alloc_chrdev_region is ok\n");
    dev1.major = MAJOR(dev1.dev_num); //获取主设备号
    dev1.minor = MINOR(dev1.dev_num); //获取次设备号
    printk("major is %d \r\n", dev1.major); //打印主设备号
    printk("minor is %d \r\n", dev1.minor); //打印次设备号

    /*2 初始化 cdev*/
    dev1.cdev_test.owner = THIS_MODULE;
    cdev_init(&dev1.cdev_test, &cdev_test_fops);

    /*3 添加一个 cdev,完成字符设备注册到内核*/
    ret = cdev_add(&dev1.cdev_test, dev1.dev_num, 1);
    if(ret<0)
    {
        goto err_chr_add;
    }

    /*4 创建设备类(设备节点)*/
    dev1. class = class_create(THIS_MODULE, "test");
    if(IS_ERR(dev1.class))
    {
        ret=PTR_ERR(dev1.class);
        goto err_class_create;
    }

    /*5 创建设备*/
    dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test");
    if(IS_ERR(dev1.device))
    {
        ret=PTR_ERR(dev1.device);
        goto err_device_create;
    }

    dev1.vir_gpio_dr=ioremap(res_mem->start,4); //将物理地址转化为虚拟地址
    if(IS_ERR(dev1.vir_gpio_dr))
    {
        ret=PTR_ERR(dev1.vir_gpio_dr); //PTR_ERR()来返回错误代码
        goto err_ioremap;
    }
    return 0;

err_ioremap:
    iounmap(dev1.vir_gpio_dr);
err_device_create:
    class_destroy(dev1.class); //删除类
err_class_create:
    cdev_del(&dev1.cdev_test); //删除 cdev
err_chr_add:
    unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
err_chrdev:
    return ret;
}

/*设备驱动remove函数*/
static int my_platform_driver_remove(struct platform_device *pdev)
{
    // 设备移除操作
    return 0;
}

static struct platform_driver my_platform_driver = {
    .driver = {
        .name = "my_platform_device", // 与 platform_device.c 中的设备名称匹配
        .owner = THIS_MODULE, },
        .probe = my_platform_driver_probe,
        .remove = my_platform_driver_remove, };

/*模块入口函数*/
static int __init my_platform_driver_init(void)
{
    int ret;
    ret = platform_driver_register(&my_platform_driver); // 注册平台驱动
    if (ret) {
        printk("Failed to register platform driver\n");
        return ret;
    }
    printk("Platform driver registered\n");
    return 0;
}

/*模块出口函数*/
static void __exit my_platform_driver_exit(void)
{
    /*注销字符设备*/
    unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
    cdev_del(&dev1.cdev_test); //删除 cdev
    device_destroy(dev1.class, dev1.dev_num); //删除设备
    class_destroy(dev1.class); //删除类
    platform_driver_unregister(&my_platform_driver); // 注销平台驱动
    printk("Platform driver unregistered\n");
}

module_init(my_platform_driver_init);
module_exit(my_platform_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

大象荒野

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

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

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

打赏作者

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

抵扣说明:

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

余额充值