【转】Linux设备驱动模型之platform总线详解

1、什么是platform(平台)总线?

    相对于USB、PCI、I2C、SPI等物理总线来说,platform总线是一种虚拟、抽象出来的总线,实际中并不存在这样的总线。

    那为什么需要platform总线呢?其实是Linux设备驱动模型为了保持设备驱动的统一性而虚拟出来的总线。因为对于usb设备、i2c设备、pci设备、spi设备等等,他们与cpu的通信都是直接挂在相应的总线下面与我们的cpu进行数据交互的,但是在我们的嵌入式系统当中,并不是所有的设备都能够归属于这些常见的总线,在嵌入式系统里面,SoC系统中集成的独立的外设控制器、挂接在SoC内存空间的外设却不依附与此类总线。所以Linux驱动模型为了保持完整性,将这些设备挂在一条虚拟的总线上(platform总线),而不至于使得有些设备挂在总线上,另一些设备没有挂在总线上。

platform总线相关代码:driver\base\platform.c 文件

相关结构体定义:include\linux\platform_device.h 文件中

2、platform总线管理下的2个重要方面

(1)两个结构体platform_device和platform_driver

    对于任何一种Linux设备驱动模型下的总线都由两个部分组成:描述设备相关的结构体和描述驱动相关的结构体,在platform总线下就是platform_device和platform_driver,下面是对两个结构体的各个元素进行分析:

platform_device结构体:(include\linux\platform_device.h)

struct platform_device {           //  platform总线设备
    const char    * name;          //  平台设备的名字
    int        id;                 //   ID 是用来区分如果设备名字相同的时候(通过在后面添加一个数字来代表不同的设备,因为有时候有这种需求)
    struct device    dev;          //   内置的device结构体
    u32        num_resources;      //   资源结构体数量
    struct resource    * resource; //   指向一个资源结构体数组

    const struct platform_device_id    *id_entry; //  用来进行与设备驱动匹配用的id_table表

    /* arch specific additions */
    struct pdev_archdata    archdata;             //  自留地    添加自己的东西
};

platform_device结构体中的struct resource结构体分析:

struct resource {      // 资源结构体
    resource_size_t start;      // 资源的起始值,如果是地址,那么是物理地址,不是虚拟地址
    resource_size_t end;        // 资源的结束值,如果是地址,那么是物理地址,不是虚拟地址
    const char *name;           // 资源名
    unsigned long flags;        // 资源的标示,用来识别不同的资源
    struct resource *parent, *sibling, *child;   // 资源指针,可以构成链表
};

platform_driver结构体:(include\linux\platform_device.h)

struct platform_driver {
    int (*probe)(struct platform_device *);     //  这个probe函数其实和  device_driver中的是一样的功能,但是一般是使用device_driver中的那个
    int (*remove)(struct platform_device *);    //  卸载平台设备驱动的时候会调用这个函数,但是device_driver下面也有,具体调用的是谁这个就得分析了
    void (*shutdown)(struct platform_device *);
    int (*suspend)(struct platform_device *, pm_message_t state);
    int (*resume)(struct platform_device *);
    struct device_driver driver;                //   内置的device_driver 结构体 
    const struct platform_device_id *id_table;  //  该设备驱动支持的设备的列表  他是通过这个指针去指向  platform_device_id 类型的数组
};

(2)两组接口函数(driver\base\platform.c)

int platform_driver_register(struct platform_driver *);       // 用来注册我们的设备驱动    
void platform_driver_unregister(struct platform_driver *);  // 用来卸载我们的设备驱动
int platform_device_register(struct platform_device *);      // 用来注册我们的设备      
void platform_device_unregister(struct platform_device *); // 用来卸载我们的设备

3、platform平台总线的初始化

    (1)platform平台总线的注册初始化:  platform_bus_init

int __init platform_bus_init(void)
{
	int error;

	early_platform_cleanup();     //  进行一些早期的平台清理

	error = device_register(&platform_bus);    //  注册设备 (在/sys/devices/目录下建立 platform目录对应的设备对象  /sys/devices/platform/)
	if (error)
		return error;
	error =  bus_register(&platform_bus_type);    //  总线注册
	if (error)
		device_unregister(&platform_bus);
	of_platform_register_reconfig_notifier();
	return error;
}

    (2)相关结构体

struct bus_type {
    const char        *name;                     //  总线名字
    struct bus_attribute    *bus_attrs;          //  该总线的属性
    struct device_attribute    *dev_attrs;       //  该总线下设备的属性
    struct driver_attribute    *drv_attrs;       //  该总线下设备驱动的属性

    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);

    const struct dev_pm_ops *pm;  //  电源管理相关的

    struct bus_type_private *p;   //  总线的私有数据  p->subsys.kobj 表示该总线在驱动模型中对应的对象
};
struct bus_type_private {
    struct kset subsys;                //  这个是bus主要的kset
    struct kset *drivers_kset;         //  这个kset指针用来指向该总线的 drivers目录的
    struct kset *devices_kset;         //  这个kse指针用来指向该总线的devices目录的
    struct klist klist_devices;        //  用来挂接该总线下的设备的一个链表头
    struct klist klist_drivers;        //   用来挂接该总线下的设备驱动的一个链表头
    struct blocking_notifier_head bus_notifier;
    unsigned int drivers_autoprobe:1;  //   是否需要在设备驱动注册时候子自动匹配设备
    struct bus_type *bus;              //  指向本bus结构体
};

    (3)函数详解

    bus_register:

int bus_register(struct bus_type *bus)
{
    int retval;
    struct bus_type_private *priv;                               //  定义一个bus_type_private 结构体指针

    priv = kzalloc(sizeof(struct bus_type_private), GFP_KERNEL); //   申请分配内存
    if (!priv)
        return -ENOMEM;

    priv->bus = bus;            //   使用 priv->bus 指向我们传进来的bus
    bus->p = priv;              //   通过  bus->p  指向priv    这里其实就是将bus与priv建立关系,这个跟之前的device、class的设计是一样的

    BLOCKING_INIT_NOTIFIER_HEAD(&priv->bus_notifier);

    retval = kobject_set_name(&priv->subsys.kobj, "%s", bus->name);  //   给我们的bus在设备驱动模型中的对象设置名字   bus->p->subsys.kobj
    if (retval)
        goto out;

//   这里就是对bus的私有数据进行一些填充
    priv->subsys.kobj.kset = bus_kset;      //  设置bus对象的父对象     也就是  /sys/bus 这目录 作为他的上层目录  所有的具体的总线类型对象都是在这个目录下
    priv->subsys.kobj.ktype = &bus_ktype;   //  设置bus对象的  对象类型为 bus_ktype
    priv->drivers_autoprobe = 1;            //  配置为在注册设备或者是注册设备驱动时自动进行配置    这个就决定了为什么我们在注册设备或者是设备驱动能够进行自动匹配

    retval = kset_register(&priv->subsys);  //  注册kset结构体(内部会调用kobject_add_internal函数,也就是将bus对象添加到 /sys/bus/目录下, /sys/bus/xxx_busType  对应具体的总线)
    if (retval)
        goto out;

    retval = bus_create_file(bus, &bus_attr_uevent);  //  在该bus下建立属性文件   (对应的就是 bus下的 uevent属性)
    if (retval)
        goto bus_uevent_fail;

    priv->devices_kset = kset_create_and_add("devices", NULL, //  在具体总线的目录下创建 kset 容器对象   /sys/bus/xxx_busType/devices
                         &priv->subsys.kobj);                 //  通过priv->devices_kset指针去指向 这个目录对应的对象
    if (!priv->devices_kset) {
        retval = -ENOMEM;
        goto bus_devices_fail;
    }

    priv->drivers_kset = kset_create_and_add("drivers", NULL,   //   /sys/bus/xxx_busType/drivers
                         &priv->subsys.kobj);                   //   通过 priv->drivers_kset 指针去指向  这个目录对应的对象
    if (!priv->drivers_kset) {
        retval = -ENOMEM;
        goto bus_drivers_fail;
    }

    klist_init(&priv->klist_devices, klist_devices_get, klist_devices_put);  //  初始化链表  klist
    klist_init(&priv->klist_drivers, NULL, NULL);                            //  初始化链表  klist

    retval = add_probe_files(bus);    //  添加探针文件   其实内部做的还是添加属性文件   /sys/bus/xxx_busType/drivers_probe    /sys/bus/xxx_busType/drivers_autoprobe
    if (retval)
        goto bus_probe_files_fail;

    retval = bus_add_attrs(bus);      //  根据 bus->bus_attrs 中的属性设置来添加属性文件
    if (retval)
        goto bus_attrs_fail;

    pr_debug("bus: '%s': registered\n", bus->name);
    return 0;

bus_attrs_fail:
    remove_probe_files(bus);
bus_probe_files_fail:
    kset_unregister(bus->p->drivers_kset);
bus_drivers_fail:
    kset_unregister(bus->p->devices_kset);
bus_devices_fail:
    bus_remove_file(bus, &bus_attr_uevent);
bus_uevent_fail:
    kset_unregister(&bus->p->subsys);
    kfree(bus->p);
out:
    bus->p = NULL;
    return retval;
}

4、platform平台设备注册

(1)platform平台总线注册函数:  platform_device_register

/**
 * platform_device_register - add a platform-level device
 * @pdev: platform device we're adding
 */
int platform_device_register(struct platform_device *pdev)
{
	device_initialize(&pdev->dev);
	arch_setup_pdev_archdata(pdev);
	return platform_device_add(pdev);
}

(2)函数分析

int platform_device_add(struct platform_device *pdev)
{
    int i, ret = 0;

    if (!pdev)
        return -EINVAL;

    if (!pdev->dev.parent)              
        pdev->dev.parent = &platform_bus;       //  将平台设备的父设备设置为 platform_bus (对应的就是  /sys/devices/platform 这个目录)

    pdev->dev.bus = &platform_bus_type;         //  设置平台设备挂接在 platform总线下     platform_bus_type

    if (pdev->id != -1)
        dev_set_name(&pdev->dev, "%s.%d", pdev->name,  pdev->id); //  给平台设备对应的对象设置名字  name.id  (如果我们的 pdev->id 设置不等于-1时)
    else
        dev_set_name(&pdev->dev, "%s", pdev->name);

//   下面的for 循环是对平台设备资源的一些处理
    for (i = 0; i < pdev->num_resources; i++) {
        struct resource *p, *r = &pdev->resource[i];

        if (r->name == NULL)
            r->name = dev_name(&pdev->dev);

        p = r->parent;
        if (!p) {
            if (resource_type(r) == IORESOURCE_MEM)
                p = &iomem_resource;
            else if (resource_type(r) == IORESOURCE_IO)
                p = &ioport_resource;
        }

        if (p && insert_resource(p, r)) {
            printk(KERN_ERR
                   "%s: failed to claim resource %d\n",
                   dev_name(&pdev->dev), i);
            ret = -EBUSY;
            goto failed;
        }
    }
//

    pr_debug("Registering platform device '%s'. Parent at %s\n",
         dev_name(&pdev->dev), dev_name(pdev->dev.parent));

    ret = device_add(&pdev->dev);    //   将平台设备添加到系统中去  /sys/devices/platform/xxx
    if (ret == 0)
        return ret;

 failed:
    while (--i >= 0) {
        struct resource *r = &pdev->resource[i];
        unsigned long type = resource_type(r);

        if (type == IORESOURCE_MEM || type == IORESOURCE_IO)
            release_resource(r);
    }

    return ret;
}

5、platform平台设备驱动注册

(1)platform平台设备驱动注册函数: platform_driver_register

/**
 * __platform_driver_register - register a driver for platform-level devices
 * @drv: platform driver structure
 * @owner: owning module/driver
 */
int __platform_driver_register(struct platform_driver *drv,
				struct module *owner)
{
	drv->driver.owner = owner;
	drv->driver.bus = &platform_bus_type;    //  设置设备驱动 挂接在 platform平台总线下

        //  下面做的就是对 drv 中的函数指针进行填充 
	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;

	return driver_register(&drv->driver);    //  注册设备驱动
}

driver_register:

 

int driver_register(struct device_driver *drv)
{
    int ret;
    struct device_driver *other;           //    定义一个设备驱动指针  other

    BUG_ON(!drv->bus->p);

    if ((drv->bus->probe && drv->probe) ||
        (drv->bus->remove && drv->remove) ||
        (drv->bus->shutdown && drv->shutdown))
        printk(KERN_WARNING "Driver '%s' needs updating - please use "
            "bus_type methods\n", drv->name);

    other = driver_find(drv->name, drv->bus);  //   这个函数其实进行了一个校验  比对当前的 总线下是否存在名字和现在需要注册的设备驱动的名字相同的设备驱动
    if (other) {
        put_driver(other);                     //   如果名字相同 直接打印错误  并退出
        printk(KERN_ERR "Error: Driver '%s' is already registered, "
            "aborting...\n", drv->name);
        return -EBUSY;
    }

    ret = bus_add_driver(drv);                  //   在总线挂接设备驱动  就是将设备驱动对应的kobj对象与组织建立关系
    if (ret)
        return ret;
    ret = driver_add_groups(drv, drv->groups);   //  
    if (ret)
        bus_remove_driver(drv);
    return ret;
}

bus_add_driver:

int bus_add_driver(struct device_driver *drv)
{
    struct bus_type *bus;             //  定义一个bus_type 结构体指针
    struct driver_private *priv;      //   定义一个 driver_private  指针
    int error = 0;

    bus = bus_get(drv->bus);       //   获取 drv的bus
    if (!bus)
        return -EINVAL;

    pr_debug("bus: '%s': add driver %s\n", bus->name, drv->name);

    priv = kzalloc(sizeof(*priv), GFP_KERNEL);  //  给priv 申请分配内存空间
    if (!priv) {
        error = -ENOMEM;
        goto out_put_bus;
    }
    klist_init(&priv->klist_devices, NULL, NULL);  //  初始化 priv->klist_devices 链表
    priv->driver = drv;                            //  使用 priv->driver  指向 drv
    drv->p = priv;                                 //   使用drv->p 指向 priv    这两步见多了  ,跟之前分析的是一样的意思  就是建立关系
    priv->kobj.kset = bus->p->drivers_kset;        //   设置设备驱动对象的父对象(  也就是指向一个 kset )    父对象就是   /sys/bus/bus_type/drivers/  这个目录对应的对象
    error = kobject_init_and_add(&priv->kobj, &driver_ktype, NULL, //  添加kobject 对象到目录层次中     就能够在  /sys/bus/bus_type/drivers/ 目录中看到设备驱动对应的文件了
                     "%s", drv->name);                             //  priv->kobj->ktype =  driver_ktype     对象类型
    if (error)
        goto out_unregister;

    if (drv->bus->p->drivers_autoprobe) {       //  如果定义了自动匹配设备标志位    则在线下面进行自动匹配
        error = driver_attach(drv);             //  尝试将驱动绑定到设备 也就是通过这个函数进行设备与设备驱动的匹配
        if (error)
            goto out_unregister;
    }
    klist_add_tail(&priv->knode_bus, &bus->p->klist_drivers);  //  链表挂接:   priv->knode_bus  挂接到  bus->p->klist_drivers 链表头上去
    module_add_driver(drv->owner, drv);

    error = driver_create_file(drv, &driver_attr_uevent);   //  建立属性文件:   uevent
    if (error) {
        printk(KERN_ERR "%s: uevent attr (%s) failed\n",
            __func__, drv->name);
    }
    error = driver_add_attrs(bus, drv);                    //  根据总线的   bus->drv_attrs  来建立属性文件
    if (error) {
        /* How the hell do we get out of this pickle? Give up */
        printk(KERN_ERR "%s: driver_add_attrs(%s) failed\n",
            __func__, drv->name);
    }

    if (!drv->suppress_bind_attrs) {
        error = add_bind_files(drv);
        if (error) {
            /* Ditto */
            printk(KERN_ERR "%s: add_bind_files(%s) failed\n",
                __func__, drv->name);
        }
    }

    kobject_uevent(&priv->kobj, KOBJ_ADD);
    return 0;

out_unregister:
    kobject_put(&priv->kobj);
    kfree(drv->p);
    drv->p = NULL;
out_put_bus:
    bus_put(bus);
    return error;
}

driver_attach:

int driver_attach(struct device_driver *drv)
{
    return bus_for_each_dev(drv->bus, NULL, drv, __driver_attach);   //  这个函数的功能就是:   依次去匹配bus总线下的各个设备
}


int bus_for_each_dev(struct bus_type *bus, struct device *start,
             void *data, int (*fn)(struct device *, void *))
{
    struct klist_iter i;                //  定义一个klist_iter 结构体变量  包含:  struct klist   和  struct klist_node
    struct device *dev;             
    int error = 0;

    if (!bus)
        return -EINVAL;

    klist_iter_init_node(&bus->p->klist_devices, &i,      //  这个函数的功能就是将 klist_devices  和  knode_bus填充到 i 变量中
                 (start ? &start->p->knode_bus : NULL));
    while ((dev = next_device(&i)) && !error)       //  依次返回出总线上的各个设备结构体device
        error = fn(dev, data);                      //  对于每一个设备和设备驱动都调用fn这个函数  直道成功  或者全部都匹配不上 
    klist_iter_exit(&i);
    return error;
}



static int __driver_attach(struct device *dev, void *data)
{
    struct device_driver *drv = data;      //  定义一个device_driver 指针

    /*
     * Lock device and try to bind to it. We drop the error
     * here and always return 0, because we need to keep trying
     * to bind to devices and some drivers will return an error
     * simply if it didn't support the device.
     *
     * driver_probe_device() will spit a warning if there
     * is an error.
     */

    if (!driver_match_device(drv, dev))           //  通过这个函数进行匹配  调用总线下的match 函数
        return 0;

    if (dev->parent)    /* Needed for USB */
        device_lock(dev->parent);
    device_lock(dev);
    if (!dev->driver)
        driver_probe_device(drv, dev);   //  调用probe函数
    device_unlock(dev);
    if (dev->parent)
        device_unlock(dev->parent);

    return 0;
}



int driver_probe_device(struct device_driver *drv, struct device *dev)
{
    int ret = 0;

    if (!device_is_registered(dev))     //  判断这个设备是否已经注册了
        return -ENODEV;

    pr_debug("bus: '%s': %s: matched device %s with driver %s\n",
         drv->bus->name, __func__, dev_name(dev), drv->name);

    pm_runtime_get_noresume(dev);
    pm_runtime_barrier(dev);
    ret = really_probe(dev, drv);       //    在这个函数中就会调用设备驱动或者是总线下的 probe  函数
    pm_runtime_put_sync(dev);

    return ret;
}


static int really_probe(struct device *dev, struct device_driver *drv)
{
    int ret = 0;

    atomic_inc(&probe_count);
    pr_debug("bus: '%s': %s: probing driver %s with device %s\n",
         drv->bus->name, __func__, drv->name, dev_name(dev));
    WARN_ON(!list_empty(&dev->devres_head));

    dev->driver = drv;                  //   使用 dev->driver  指针去指向 drv  这就使得这两者建立了一种关系
    if (driver_sysfs_add(dev)) {
        printk(KERN_ERR "%s: driver_sysfs_add(%s) failed\n",
            __func__, dev_name(dev));
        goto probe_failed;
    }

    if (dev->bus->probe) {             //   如果总线下的probe函数存在 则调用优先调用这个函数
        ret = dev->bus->probe(dev);
        if (ret)
            goto probe_failed;
    } else if (drv->probe) {            //  否则调用设备驱动中的probe函数
        ret = drv->probe(dev);  //   所以由此可知:  总线中的probe函数具有更高的优先级
        if (ret)
            goto probe_failed;
    }

    driver_bound(dev);
    ret = 1;
    pr_debug("bus: '%s': %s: bound device %s to driver %s\n",
         drv->bus->name, __func__, dev_name(dev), drv->name);
    goto done;

probe_failed:
    devres_release_all(dev);
    driver_sysfs_remove(dev);
    dev->driver = NULL;

    if (ret != -ENODEV && ret != -ENXIO) {
        /* driver matched but the probe failed */
        printk(KERN_WARNING
               "%s: probe of %s failed with error %d\n",
               drv->name, dev_name(dev), ret);
    }
    /*
     * Ignore errors returned by ->probe so that the next driver can try
     * its luck.
     */
    ret = 0;
done:
    atomic_dec(&probe_count);
    wake_up(&probe_waitqueue);
    return ret;
}

上面说到了当注册platform平台设备驱动时会进行自动匹配的原理,那么当我们注册platform平台设备时进行自动匹配的代码在哪里呢?

其实这个之前在分析device_create函数时就已经分析过了,只不过没有去详细的分析:

/**********************************************/

platform_device_add

    device_add

        bus_probe_device     //  关键就在这个函数

/*********************************************/

函数分析:

void bus_probe_device(struct device *dev)
{
    struct bus_type *bus = dev->bus;             //  获取设备中的总线类型   bus_type
    int ret;

    if (bus && bus->p->drivers_autoprobe) {       //  如果总线存在  并且  设置了自动进行设备与设备驱动匹配标志位
        ret = device_attach(dev);                           //  则调用这个函数进行匹配
        WARN_ON(ret < 0);
    }
}




int device_attach(struct device *dev)
{
    int ret = 0;

    device_lock(dev);
    if (dev->driver) {    //    如果我们的设备早就绑定了设备驱动     那么执行下面的
        ret = device_bind_driver(dev);
        if (ret == 0)
            ret = 1;
        else {
            dev->driver = NULL;
            ret = 0;
        }
    } else {     //   我们就分析这条
        pm_runtime_get_noresume(dev);
        ret = bus_for_each_drv(dev->bus, NULL, dev, __device_attach);   //  遍历总线的链表匹配对应的设备驱动
        pm_runtime_put_sync(dev);
    }
    device_unlock(dev);
    return ret;
}
//  到这里之后就和上面的其实是一样的了

总结:  所以由此可知,当我们不管是先注册设备还是先注册设备驱动都会进行一次设备与设备驱动的匹配过程,匹配成功之后就会调用probe函数,匹配的原理就是去遍历总线下的相应的链表来找到挂接在他下面的设备或者设备驱动,所以由此可以看出来,这个东西的设计其实是很美的。

6、platform总线下的匹配函数

(1)platform_match函数

static int platform_match(struct device *dev, struct device_driver *drv) // 总线下的设备与设备驱动的匹配函数
{
    struct platform_device *pdev = to_platform_device(dev);      //  通过device  变量获取到 platform_device 
    struct platform_driver *pdrv = to_platform_driver(drv);      //   通过 driver 获取  platform_driver

    /* match against the id table first */
    if (pdrv->id_table)    //   如果pdrv中的id_table 表存在
        return platform_match_id(pdrv->id_table, pdev) != NULL;  //  匹配id_table
 
    /* fall-back to driver name match */   //  第二个就是指直接匹配 pdev->name     drv->name  名字是否形同
    return (strcmp(pdev->name, drv->name) == 0);
}




static const struct platform_device_id *platform_match_id(
            const struct platform_device_id *id,
            struct platform_device *pdev)
{
    while (id->name[0]) {  //  循环去比较id_table数组中的各个id名字是否与pdev->name 相同
        if (strcmp(pdev->name, id->name) == 0) {
            pdev->id_entry = id;       // 将id_table数组中的名字匹配上的 这个数组项 指针赋值给 pdev->id_entry
            return id;         //  返回这个指针
        }
        id++;
    }
    return NULL;
}

总结: 由上面可知platform总线下设备与设备驱动的匹配原理就是通过名字进行匹配的,先去匹配platform_driver中的id_table表中的各个名字与platform_device->name名字是否相同,如果相同表示匹配成功直接返回,否则直接匹配platform_driver->name与platform_driver->name是否相同,相同则匹配成功,否则失败。

 

转自:https://www.cnblogs.com/deng-tao/p/6026373.html

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值