Uboot Driver Model

1.Driver Model Introduction

  Why driver model? 源自Driver Model Conversion

  U-Boot originally had a pretty ad-hoc device driver system. While some subsystems have their own framework (e.g. mmc, block devices, stdio), others are implemented as bare function calls to be provided by whichever driver shows up. This limits us to one driver of a particular type (e.g. you cannot have USB2 and USB3 ports at the same time as they use different drivers).

  Driver model aims to provide a consistent model for drivers and the platform data / device tree information needed by drivers to instantiate devices. In particular:

  • Hierarchical, allowing a device to have children and parents
  • Replaces #define CONFIGs with board-supplied platform data or device tree
  • Devices organized by class, e.g. I2C, SPI, LCD
  • Efficient implementation, suitable in principle for pre-relocation and SPL
  • Devices are known very early but are only activated (‘probed’) when used
  • More scalable as the U-Boot code base grows

  Uboot引入驱动模型(driver model),这种驱动模型为驱动的定义和访问接口提供了统一的方法,提高了驱动之间的兼容性以及访问的标准型,Uboot驱动模型和linux kernel的设备驱动模型相类似,但是又有所区别。

  uboot driver model设计目标包括:

  • 提供统一设备驱动框架,降低设备驱动的开发复杂度
  • 提供设备树视图
  • 支持设备组
  • 支持设备lazy init
  • 支持设备驱动沙盒测试
  • 较小的系统开销(内存和CPU)

1.1.The modular concept

  The driver core design is done with modularity in mind. The long-term plan is to extend this modularity to allow loading not only drivers, but various other objects into U-Boot at runtime – like commands, support for other boards etc.

1.2.Driver core initialization stages

  The drivers have to be initialized in two stages, since the U-Boot bootloader runs in two stages itself.

  • The first stage is the one which is executed before the bootloader itself is relocated.
  • The second stage then happens after relocation.

1.2.1.First stage

  The first stage runs after the bootloader did very basic hardware init. This means the stack pointer was configured, caches disabled and that’s about it. The problem with this part is the memory management isn’t running at all. To make things even worse, at this point, the RAM is still likely uninitialized and therefore unavailable.

1.2.2.Second stage

  At this stage, the bootloader has initialized RAM and is running from it’s final location. Dynamic memory allocations are working at this point. Most of the driver initialization is executed here.

2.Uboot DM Enable

1>.add the config

configs/xxx_defconfig:
+CONFIG_DM=y //drivers/core/Kconfig

2>.add the uclass driver config

  DM和uclass是息息相关的,如果希望在某个模块引入DM,那么就需要使用相应模块的uclass driver来代替旧版的通用driver。 以serial为例:

configs/xxx_defconfig:
+CONFIG_DM_SERIAL=y

Mafile定义:

driver/serial/Makefile:
ifdef CONFIG_DM_SERIAL
obj-y += serial-uclass.o          //引入DM serial core驱动
else
obj-y += serial.o                // 通用的serial core驱动
endif

3>.对应设备驱动也要引入dm的功能

  设备驱动主要是实现和底层交互,为uclass层提供相应的接口函数。

  233 U_BOOT_DRIVER(serial_msm) = {
  234     .name   = "serial_msm",
  235     .id = UCLASS_SERIAL,
  236     .of_match = msm_serial_ids,
  237     .ofdata_to_platdata = msm_serial_ofdata_to_platdata,
  238     .priv_auto_alloc_size = sizeof(struct msm_serial_data),
  239     .probe = msm_serial_probe,
  240     .ops    = &msm_serial_ops,
  241 }; 

3.DM模型

  DM模型包含:uclass,udevice,driver,udevice和driver绑定,设备层级关系。

3.1.设计对象分析

  对象的设计出于设计模块化的考虑,分为静态形式和运行态形式。

  • 静态表达形式的对象是离散的,和系统的其他对象隔离开,减小对象的复杂度,利于模块化设计,遵循人类表达习惯。
  • 运行态形式的对象是把所有对象组合成层次视图,有着清晰的数据关联视图,方便系统运行时数据的流动。

3.1.1.静态表达形式

  • device: FDT 或者静态数据结构U_BOOT_DEVICE(以数据段形式组织)
  • driver: 静态数据结构U_BOOT_DRIVER(以数据段形式组织)

3.1.2.运行态形式

  • uclass_driver: 设备组公共行为对象,作为uclass的一个属性
  • uclass:设备组公共属性对象(以链表形式组织),外部顶层对象,作为udevice的一个属性
  • udevice: 设备对象(以链表形式组织)
  • driver: 驱动对象。作为udevice的一个属性

3.2.uclass、udevice

框架图如下所示:
在这里插入图片描述
3.2.1.uclass

  uclass代表一个类,同一类设备属于同一个uclass,拥有相同的uclass ID。以RTC为例,市面上RTC芯片很多,由不同的厂家生产,其内存寄存器定义甚至访问接口都不一样,所以RTC的driver肯定是不一样的,但是从功能的角度来说,它们都是用来记录时间的,所以它们都属于rtc-class。

  • 从驱动角度来看,uclass driver实现通用的处理逻辑。
  • 从设备角度来看,同一类的设备拥有相同的uclass ID,并全部挂在该uclass下。
  • 从层级结构来看,uclass 起到非常好的承上启下的作用,它既能屏蔽具体设备个体间的差异性,向用户提供统一的接口,又能为同一类的设备定义统一的处理函数,具体的设备驱动只需要实现这些处理函数即可,从而简化的设备驱动的开发。

3.2.2.udevice

  device拥有设备资源,提供三种方式进行bind:

  • 静态编译方式
  • 设备树方式(推荐)
  • 传参方式

3.2.2.1.静态编译方式(Binding with Platform Data)

  platform data is the old way of doing things. It is basically a C structure which is passed to drivers to tell them about platform-specific settings like the address of its registers, bus speed, etc. Device tree is now the preferred way of handling this.Unless you have a good reason not to use device tree (the main one being you need serial support in SPL and don’t have enough SRAM for the cut-down device tree and libfdt libraries) you should stay away from platform data.

  Platform data is like Linux platform data, if you are familiar with that. It provides the board-specific information to start up a device.

  在代码中调用U_BOOT_DEVICE宏来定义设备资源,即是一个设备实例。以tegra gpio为例:

arch/arm/mach-at91/arm926ejs/at91sam9260_devices.c:
/* Platform data for the GPIOs */
  static const struct at91_port_platdata at91sam9260_plat[] = { 
      { ATMEL_BASE_PIOA, "PA" },
      { ATMEL_BASE_PIOB, "PB" },
      { ATMEL_BASE_PIOC, "PC" },
  };
  
  U_BOOT_DEVICES(at91sam9260_gpios) = { 
      { "gpio_at91", &at91sam9260_plat[0] },
      { "gpio_at91", &at91sam9260_plat[1] },
      { "gpio_at91", &at91sam9260_plat[2] },
  };

该定义使用struct driver_info进行定义,除了特殊的情况,这种方式基本不用。

  /**
   * struct driver_info - Information required to instantiate a device
   *
   * NOTE: Avoid using this except in extreme circumstances, where device tree
   * is not feasible (e.g. serial driver in SPL where <8KB of SRAM is                                        
   * available). U-Boot's driver model uses device tree for configuration.
   */
  struct driver_info {
      const char *name;
      const void *platdata;
  #if CONFIG_IS_ENABLED(OF_PLATDATA)
      uint platdata_size;
  #endif
  };

3.2.2.2.设备数方式(Binding with Device Tree)
在这里插入图片描述
  将设备描述信息写在对应的DTS文件中,DTS文件被编译成DTB文件,然后跟uboot 二进制文件合并在一起,uboot启动的时候,会解析DTB文件,将所有的设备描述信息解析出来,该方式定义设备资源信息是目前比较流行的方案。

  了解这两种方式请参考- ./doc/driver-model/of-plat.txt.

3.2.2.3.参数的方式

  通过命令行或者接口将设备资源信息传递进来,非常灵活。

3.3.DMSoftware Framework
在这里插入图片描述

如上图所示:

  • uclass 理解为一些具有相同属性的udevice对外操作的接口,uclass的驱动是uclass_driver,主要为上层提供接口。
  • udevice 具体设备的抽象,对应驱动是driver,driver主要负责和硬件通信,为uclass提供实际的操作集。
  • udevice如何匹配uclass:udevice对应的driver的id和uclass对应的uclass_driver的id是否匹配。
  • bind关系
    • udevice和uclass
    • driver和udevice
    • uclass_driver和uclass

3.3.1.GD中和DM相关的部分

//include/asm-generic/global_data.h
 typedef struct global_data {
#ifdef CONFIG_DM
    struct udevice  *dm_root;   /* Root instance for Driver Model */  // DM中的根设备,也是uboot中第一个创建的udevice,也就对应了dts里的根节点。
    struct udevice  *dm_root_f; /* Pre-relocation root instance */ // 在relocation之前DM中的根设备
    struct list_head uclass_root;   /* Head of core tree */          // uclass链表,所有被udevice匹配的uclass都会被挂载到这个链表上
#endif
} gd_t;

4.DM四个主要组成部分

4.1.uclass id

  每一种uclass都有唯一的ID number,在uclass_driver中进行定义,该值必须与udevice的driver中的uclass id一一对应, 这个id也是用来获取uclass的标志。uclass id定义如下:

include/dm/uclass-id.h: 
enum uclass_id {
    /* These are used internally by driver model */
    UCLASS_ROOT = 0,
    UCLASS_DEMO,
    UCLASS_CLK,         /* Clock source, e.g. used by peripherals */
    UCLASS_PINCTRL,     /* Pinctrl (pin muxing/configuration) device */
    UCLASS_SERIAL,      /* Serial UART */
    ...
}

4.2. struct uclass

1>.结构体:

struct uclass {
    void *priv;                      // uclass的私有数据指针
    struct uclass_driver *uc_drv; // 对应的uclass driver
    struct list_head dev_head; // 链表头,连接所属的所有udevice
    struct list_head sibling_node; // 链表节点,用于把uclass连接到uclass_root链表上
};

2>.获取uclass

int uclass_get(enum uclass_id key, struct uclass **ucp);

  直接遍历链表gd->uclass_root链表,并且根据uclass id来获取到相应的uclass。 详细实现如下所示:

struct uclass *uclass_find(enum uclass_id key)
{
      struct uclass *uc;
  
     if (!gd->dm_root)
         return NULL;

     list_for_each_entry(uc, &gd->uclass_root, sibling_node) {
        if (uc->uc_drv->id == key)
              return uc;
      }
  
     return NULL;
}   

2>.如何定义

  uclass是uboot自动生成,并且不是所有uclass都会生成,有对应uclass driver并且有被udevice匹配到的uclass才会生成。

3>.存放位置
  所有生成的uclass都会被挂载gd->uclass_root链表上。

4.3.uclass_driver

1>.结构体:

include/dm/uclass.h:
struct uclass_driver {
    const char *name; // 该uclass_driver的命令
    enum uclass_id id; // 对应的uclass id
/* 以下函数指针主要是调用时机的区别 */
    int (*post_bind)(struct udevice *dev); // 在udevice被绑定到该uclass之后调用
    int (*pre_unbind)(struct udevice *dev); // 在udevice被解绑出该uclass之前调用
    int (*pre_probe)(struct udevice *dev); // 在该uclass的一个udevice进行probe之前调用
    int (*post_probe)(struct udevice *dev); // 在该uclass的一个udevice进行probe之后调用
    int (*pre_remove)(struct udevice *dev);// 在该uclass的一个udevice进行remove之前调用
    int (*child_post_bind)(struct udevice *dev); // 在该uclass的一个udevice的一个子设备被绑定到该udevice之后调用
    int (*child_pre_probe)(struct udevice *dev); // 在该uclass的一个udevice的一个子设备进行probe之前调用
    int (*init)(struct uclass *class); // 安装该uclass的时候调用
    int (*destroy)(struct uclass *class); // 销毁该uclass的时候调用
    int priv_auto_alloc_size; // 需要为对应的uclass分配多少私有数据
    int per_device_auto_alloc_size; //
    int per_device_platdata_auto_alloc_size; //
    int per_child_auto_alloc_size; //
    int per_child_platdata_auto_alloc_size;  //
    const void *ops; //操作集合
    uint32_t flags;   // 标识为
};

#define UCLASS_DRIVER(__name)                       \
    ll_entry_declare(struct uclass_driver, __name, uclass)

#define ll_entry_declare(_type, _name, _list)               \
    _type _u_boot_list_2_##_list##_2_##_name __aligned(4)       \
            __attribute__((unused,              \
            section(".u_boot_list_2_"#_list"_2_"#_name)))

2>.定义以serial-uclass为例:

UCLASS_DRIVER(serial) = {
    .id        = UCLASS_SERIAL,
    .name        = "serial",
    .flags        = DM_UC_FLAG_SEQ_ALIAS,   
    .post_probe    = serial_post_probe,
    .pre_remove    = serial_pre_remove,
    .per_device_auto_alloc_size = sizeof(struct serial_dev_priv),
};

  UCLASS_DRIVER展开最终得到如下结构体(存放在.u_boot_list_2_uclass_2_serial段):

struct uclass_driver  _u_boot_list_2_uclass_2_serial = {
    .id        = UCLASS_SERIAL,   
    .name        = "serial",
    .flags        = DM_UC_FLAG_SEQ_ALIAS,   
    .post_probe    = serial_post_probe,
    .pre_remove    = serial_pre_remove,
    .per_device_auto_alloc_size = sizeof(struct serial_dev_priv),
}

查看生成的u-boot.map:

.u_boot_list_2_uclass_2_serial
                0x23e36970       0x48 drivers/serial/built-in.o
                0x23e36970                _u_boot_list_2_uclass_2_serial // serial uclass driver的符号
.u_boot_list_2_uclass_2_simple_bus
                0x23e369b8       0x48 drivers/built-in.o
                0x23e369b8                _u_boot_list_2_uclass_2_simple_bus

  最终,所有uclass driver结构体以列表的形式被放在.u_boot_list_2_uclass_1和.u_boot_list_2_uclass_3的区间中。

3>.获取API

  想要获取uclass_driver需要先获取uclass_driver table,通过以下宏来获取

    struct uclass_driver *uclass =
        ll_entry_start(struct uclass_driver, uclass); 
// 会根据.u_boot_list_2_uclass_1的段地址来得到uclass_driver table的地址

    const int n_ents = ll_entry_count(struct uclass_driver, uclass);
// 获得uclass_driver table的长度

  接着通过遍历这个uclass_driver table,得到相应的uclass_driver。 有如下API:

struct uclass_driver *lists_uclass_lookup(enum uclass_id id)
// 从uclass_driver table中获取uclass id为id的uclass_driver。

4.4.udevice

  uboot 通过两种方法来添加设备

  • 直接定义平台设备(这种方式被抛弃)
  • 使用device tree

4.4.1直接定义平台设备

struct driver_info {
      const char *name;
      const void *platdata;
  #if CONFIG_IS_ENABLED(OF_PLATDATA)
      uint platdata_size;
  #endif
};

#define U_BOOT_DEVICE(__name)                       \
    ll_entry_declare(struct driver_info, __name, driver_info)

/* Declare a list of devices. The argument is a driver_info[] array */
#define U_BOOT_DEVICES(__name)                      \
    ll_entry_declare_list(struct driver_info, __name, driver_info)

除了根设备使用外,其他基本不使用:

drivers/core/root.c :
 static const struct driver_info root_info = {
      .name       = "root_driver",
  };

 /* This is the root driver - all drivers are children of this */
  U_BOOT_DRIVER(root_driver) = { 
      .name   = "root_driver",
      .id = UCLASS_ROOT,
  };
  
  /* This is the root uclass */
  UCLASS_DRIVER(root) = { 
      .name   = "root",
      .id = UCLASS_ROOT,
  };

4.4.2.Device tree

  While platdata is useful, a more flexible way of providing device data is by using device tree. In U-Boot you should use this where possible. Avoid sending patches which make use of the U_BOOT_DEVICE() macro unless strictly necessary.

1>.数据结构:

include/dm/device.h
struct udevice {
    const struct driver *driver; // 该udevice对应的driver
    const char *name;           // 设备名
    void *platdata;                  // 该udevice的平台数据
    void *parent_platdata;    // 提供给父设备使用的平台数据
    void *uclass_platdata;    // 提供给所属uclass使用的平台数据
    int of_offset;           // 该udevice的dtb节点偏移,代表了dtb里面的这个节点node
    ulong driver_data;       // 驱动数据
    struct udevice *parent; // 父设备
    void *priv;             // 私有数据的指针
    struct uclass *uclass; // 所属uclass
    void *uclass_priv; // 提供给所属uclass使用的私有数据指针
    void *parent_priv; // 提供给其父设备使用的私有数据指针
    struct list_head uclass_node; // 用于连接到其所属uclass的链表上
    struct list_head child_head; // 链表头,连接其子设备
    struct list_head sibling_node; // 用于连接到其父设备的链表上
    uint32_t flags; // 标识
    int req_seq;
    int seq;
#ifdef CONFIG_DEVRES
    struct list_head devres_head;
#endif
};

2>.定义

  在dtb存在的情况下,由uboot解析dtb后动态生成。

3>.存放位置

  • 连接到对应uclass中
    也就是会连接到uclass->dev_head中
  • 连接到父设备的子设备链表中
    也就是会连接到udevice->child_head中,并且最终的根设备是gd->dm_root这个根设备。

4>.获取API

  从uclass中获取udevice 遍历uclass->dev_head,获取对应的udevice。有如下API:

#define uclass_foreach_dev(pos, uc) \
    list_for_each_entry(pos, &uc->dev_head, uclass_node)

#define uclass_foreach_dev_safe(pos, next, uc)  \
    list_for_each_entry_safe(pos, next, &uc->dev_head, uclass_node)

int uclass_get_device(enum uclass_id id, int index, struct udevice **devp); // 通过索引从uclass中获取udevice
int uclass_get_device_by_name(enum uclass_id id, const char *name, // 通过设备名从uclass中获取udevice
                  struct udevice **devp);
int uclass_get_device_by_seq(enum uclass_id id, int seq, struct udevice **devp);
int uclass_get_device_by_of_offset(enum uclass_id id, int node,
                   struct udevice **devp);
int uclass_get_device_by_phandle(enum uclass_id id, struct udevice *parent,
                 const char *name, struct udevice **devp);
int uclass_first_device(enum uclass_id id, struct udevice **devp);
int uclass_first_device_err(enum uclass_id id, struct udevice **devp);
int uclass_next_device(struct udevice **devp);
int uclass_resolve_seq(struct udevice *dev);

4.5.driver

1>.数据结构

include/dm/device.h:
  struct driver {
      char *name;
      enum uclass_id id;
      const struct udevice_id *of_match;
      int (*bind)(struct udevice *dev);
      int (*probe)(struct udevice *dev);
      int (*remove)(struct udevice *dev);
      int (*unbind)(struct udevice *dev);
      int (*ofdata_to_platdata)(struct udevice *dev);
      int (*child_post_bind)(struct udevice *dev);
      int (*child_pre_probe)(struct udevice *dev);
      int (*child_post_remove)(struct udevice *dev);
      int priv_auto_alloc_size;
      int platdata_auto_alloc_size;
      int per_child_auto_alloc_size;
      int per_child_platdata_auto_alloc_size;
      const void *ops;    /* driver-specific operations */
      uint32_t flags;
  };

2>.如何定义

以s5pv210为例:

driver/serial/serial_s5p.c
U_BOOT_DRIVER(serial_s5p) = {
    .name    = "serial_s5p",
    .id    = UCLASS_SERIAL,
    .of_match = s5p_serial_ids,
    .ofdata_to_platdata = s5p_serial_ofdata_to_platdata,
    .platdata_auto_alloc_size = sizeof(struct s5p_serial_platdata),
    .probe = s5p_serial_probe,
    .ops    = &s5p_serial_ops,
    .flags = DM_FLAG_PRE_RELOC,
};

  The driver will contain a structure located in a separate section, which will allow linker to create a list of compiled-in drivers at compile time. Let’s call this list “driver_list”. You can refer to the symobl in the u-boot.map after compilation.

  #define ll_entry_declare(_type, _name, _list)               \
      _type _u_boot_list_2_##_list##_2_##_name __aligned(4)       \
              __attribute__((unused,              \
              section(".u_boot_list_2_"#_list"_2_"#_name)))

  /* Declare a new U-Boot driver */
  #define U_BOOT_DRIVER(__name)                       \                                                    
      ll_entry_declare(struct driver, __name, driver)

最终得到结构体:(存放在.u_boot_list_2_driver_2_serial_s5p段)

struct driver _u_boot_list_2_driver_2_serial_s5p= {
    .name    = "serial_s5p",
    .id    = UCLASS_SERIAL,
    .of_match = s5p_serial_ids,
    .ofdata_to_platdata = s5p_serial_ofdata_to_platdata,
    .platdata_auto_alloc_size = sizeof(struct s5p_serial_platdata),
    .probe = s5p_serial_probe,
    .ops    = &s5p_serial_ops,
    .flags = DM_FLAG_PRE_RELOC,
};

3>.获取API
  想要获取driver需要先获取driver table。 可以通过以下宏来获取driver table

   struct driver *drv =
        ll_entry_start(struct driver, driver);
// 会根据.u_boot_list_2_driver_1的段地址来得到uclass_driver table的地址

    const int n_ents = ll_entry_count(struct driver, driver);
// 获得driver table的长度

接着通过遍历这个driver table,得到相应的driver。

struct driver *lists_driver_lookup_name(const char *name)
// 从driver table中获取名字为name的driver。

代码参考:
doc/driver-model/README.txt
include/dm/device.h
include/dm/uclass.h
include/dm/uclass-id.h

### U-Boot 网络驱动开发概述 U-Boot 是一种广泛使用的引导加载程序,其核心功能之一是支持多种硬件设备的初始化和配置。在网络驱动方面,U-Boot 提供了一个模块化的框架来实现对不同网络芯片的支持[^1]。 #### 1. 网络驱动的基础架构 U-Boot 中的网络驱动基于 Device Model (DM) 架构设计。Device Model 是 U-Boot 的一个重要特性,它提供了一种统一的方式来管理设备及其驱动程序。对于网络驱动而言,开发者可以通过 DM 框架注册新的网卡驱动并完成必要的初始化工作[^2]。 以下是典型的 U-Boot 网络驱动开发流程: #### 2. 创建自定义网络驱动 为了支持一个新的网络接口控制器(NIC),需要编写相应的驱动代码。通常情况下,这涉及以下几个部分: - **头文件声明**:定义数据结构以及函数原型。 - **驱动实现**:包括 `probe` 函数用于初始化硬件资源;`send` 和 `recv` 方法分别处理发送和接收包的操作。 - **设备树绑定**:描述该 NIC 所需的寄存器地址范围和其他属性。 下面展示一段简化版的伪代码作为参考: ```c #include <common.h> #include <dm.h> // 定义私有数据结构体 struct my_net_dev { void *base_addr; }; static int my_probe(struct udevice *dev) { struct my_net_dev *priv = dev_get_priv(dev); priv->base_addr = dm_ioremap(dev, CONFIG_MY_NET_BASE_ADDR); if (!priv->base_addr) return -ENOMEM; // 初始化硬件... return 0; } static const struct eth_ops my_eth_ops = { .start = my_start, .stop = my_stop, .sendpacket = my_send_packet, .recvpacket = my_recv_packet, }; static const struct udevice_id my_ids[] = { { .compatible = "mycompany,my-net-controller" }, { } }; U_BOOT_DRIVER(my_network_driver) = { .name = "my_network", .id = UCLASS_ETH, .of_match = my_ids, .ops = &my_eth_ops, .priv_auto_alloc_size = sizeof(struct my_net_dev), .probe = my_probe, }; ``` 上述代码片段展示了如何构建一个基本的网络驱动程序,并利用 DM API 进行内存映射、中断设置等功能调用。 #### 3. 设备树中的配置 除了驱动本身外,还需要更新设备树源文件(DTS)。例如,在 DTS 文件中添加如下节点即可指定新加入的以太网控制器参数: ```dts ethernet@80000000 { compatible = "mycompany,my-net-controller"; reg = <0x80000000 0x1000>; /* 基础物理地址及长度 */ interrupts = <17>; }; ``` 此段落说明了如何通过修改 `.dts` 来适配具体的硬件平台需求[^3]。 #### 4. 测试与验证 编译完成后,可通过命令行工具测试网络连接状态。常用指令如 `ping`, `dhcp` 或者手动配置 IP 地址等方式确认通信正常运行。 --- ### 总结 综上所述,针对 U-Boot 下的新网络驱动开发主要围绕着 DM 模型展开,从驱动编码到设备树调整均不可或缺。借助官方文档和技术社区分享的经验案例能够加速整个过程的学习曲线[^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值