[uboot] uboot流程系列:
[project X] tiny210(s5pv210)上电启动流程(BL0-BL2)
[project X] tiny210(s5pv210)从存储设备加载代码到DDR
[uboot] (第一章)uboot流程——概述
[uboot] (第二章)uboot流程——uboot-spl编译流程
[uboot] (第三章)uboot流程——uboot-spl代码流程
[uboot] (第四章)uboot流程——uboot编译流程
[uboot] (第五章)uboot流程——uboot启动流程
[uboot] (番外篇)global_data介绍
[uboot] (番外篇)uboot relocation介绍
[uboot] (番外篇)uboot之fdt介绍

建议先看《[uboot] (番外篇)uboot之fdt介绍》,了解一下uboot的fdt的功能,在驱动模型中会使用到。

==============================================================================================================
一、说明
1、uboot的驱动模型简单介绍

uboot引入了驱动模型(driver model),这种驱动模型为驱动的定义和访问接口提供了统一的方法。提高了驱动之间的兼容性以及访问的标准型。
uboot驱动模型和kernel中的设备驱动模型类似,但是又有所区别。
在后续我们将驱动模型(driver model)简称为DM,其实在uboot里面也是这样简称的。

具体细节建议参考./doc/driver-model/README.txt
2、如何使能uboot的DM功能

(1)配置CONFIG_DM
在configs/tiny210_defconfig中定义了如下:

CONFIG_DM=y

(2)使能相应的uclass driver的config。
DM和uclass是息息相关的,如果我们希望在某个模块引入DM,那么就需要使用相应模块的uclass driver来代替旧版的通用driver。
关于uclass我们会在后续继续说明。
以serial为例,为了在serial中引入DM,我在configs/tiny210_defconfig0中打开了CONFIG_DM_SERIAL宏,如下

CONFIG_DM_SERIAL=y

看driver/serial/Makefile

ifdef CONFIG_DM_SERIAL
obj-y += serial-uclass.o ## 引入dm的serial core驱动
else
obj-y += serial.o ## 通用的serial core驱动
endif
## 可以发现编译出来的serial core的驱动代码是不一样的。

(3)对应设备驱动也要引入dm的功能
其设备驱动主要是实现和底层交互,为uclass层提供接口。后续再具体说明。

__后续都以serial-uclass进行说明
二、uboot DM整体架构
1、DM的四个组成部分

uboot的DM主要有四个组成部分

udevice
    简单就是指设备对象,可以理解为kernel中的device。
    driver
    udevice的驱动,可以理解为kernel中的device_driver。和底层硬件设备通信,并且为设备提供面向上层的接口。
    uclass
    先看一下README.txt中关于uclass的说明:

Uclass - a group of devices which operate in the same way. A uclass provides
        a way of accessing individual devices within the group, but always
        using the same interface. For example a GPIO uclass provides
        operations for get/set value. An I2C uclass may have 10 I2C ports,
        4 with one driver, and 6 with another.

uclass,使用相同方式的操作集的device的组。相当于是一种抽象。uclass为那些使用相同接口的设备提供了统一的接口。
例如,GPIO uclass提供了get/set接口。再例如,一个I2C uclass下可能有10个I2C端口,4个使用一个驱动,另外6个使用另外一个驱动。

uclass_driver
    对应uclass的驱动程序。主要提供uclass操作时,如绑定udevice时的一些操作。

2、调用关系框架图

DM下的接口调用流程
3、相互之间的关系

结合上图来看:

上层接口都是和uclass的接口直接通讯。
    uclass可以理解为一些具有相同属性的udevice对外操作的接口,uclass的驱动是uclass_driver,主要为上层提供接口。
    udevice的是指具体设备的抽象,对应驱动是driver,driver主要负责和硬件通信,为uclass提供实际的操作集。
    udevice找到对应的uclass的方式主要是通过:udevice对应的driver的id和uclass对应的uclass_driver的id是否匹配。
    udevice会和uclass绑定。driver会和udevice绑定。uclass_driver会和uclass绑定。

这里先简单介绍一下:uclass和udevice都是动态生成的。在解析fdt中的设备的时候,会动态生成udevice。
然后找到udevice对应的driver,通过driver中的uclass id得到uclass_driver id。从uclass链表中查找对应的uclass是否已经生成,没有生成的话则动态生成uclass。
4、GD中和DM相关的部分

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;

三、DM四个主要组成部分详细介绍

后续以数据结构、如何定义、存放位置、如何获取四个部分进行说明进行说明
0、uclass id

每一种uclass都有自己对应的ID号。定义于其uclass_driver中。其附属的udevice的driver中的uclass id必须与其一致。
所有uclass id定义于include/dm/uclass-id.h中
列出部分id如下

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 */
}

1、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是uboot自动生成。并且不是所有uclass都会生成,有对应uclass driver并且有被udevice匹配到的uclass才会生成。
    具体参考后面的uboot DM初始化一节。或者参考uclass_add实现。

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

(4)如何获取、API
    直接遍历链表gd->uclass_root链表并且根据uclass id来获取到相应的uclass。
    具体uclass_get-》uclass_find实现了这个功能。
    有如下API:

int uclass_get(enum uclass_id key, struct uclass **ucp);
// 从gd->uclass_root链表获取对应的uclass

2、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;   // 标识为
};

(2)如何定义
    通过UCLASS_DRIVER来定义uclass_driver.
    以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实现如下:

#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)))
关于ll_entry_declare我们在《[uboot] (第六章)uboot流程——命令行模式以及命令处理介绍》已经介绍过了

最终得到一个如下结构体

struct uclass_driver  _u_boot_list_2_uclass_2_serial = {
    .id        = UCLASS_SERIAL,   // 设置对应的uclass id
    .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_list_2_uclass_2_serial段中。

(3)存放位置
    通过上述,我们知道serial的uclass_driver结构体_u_boot_list_2_uclass_2_serial被存放到.u_boot_list_2_uclass_2_serial段中。
    通过查看u-boot.map得到如下

.u_boot_list_2_uclass_1
                0x23e368e0        0x0 drivers/built-in.o
.u_boot_list_2_uclass_2_gpio
                0x23e368e0       0x48 drivers/gpio/built-in.o
                0x23e368e0                _u_boot_list_2_uclass_2_gpio  // gpio uclass driver的符号
.u_boot_list_2_uclass_2_root
                0x23e36928       0x48 drivers/built-in.o
                0x23e36928                _u_boot_list_2_uclass_2_root // root uclass drvier的符号
.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
.u_boot_list_2_uclass_3
                0x23e36a00        0x0 drivers/built-in.o
                0x23e36a00                . = ALIGN (0x4)

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

(4)如何获取、API
    想要获取uclass_driver需要先获取uclass_driver table。
    可以通过以下宏来获取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。

3、udevice

(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后动态生成,后续在“uboot DM的初始化”一节中具体说明。

(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、driver

和uclass_driver方式是相似的。

(1)数据结构
    include/dm/device.h

struct driver {
    char *name;    // 驱动名
    enum uclass_id id;  // 对应的uclass id
    const struct udevice_id *of_match;    // compatible字符串的匹配表,用于和device tree里面的设备节点匹配
    int (*bind)(struct udevice *dev);   // 用于绑定目标设备到该driver中
    int (*probe)(struct udevice *dev);   // 用于probe目标设备,激活
    int (*remove)(struct udevice *dev); // 用于remove目标设备。禁用
    int (*unbind)(struct udevice *dev); // 用于解绑目标设备到该driver中
    int (*ofdata_to_platdata)(struct udevice *dev); // 在probe之前,解析对应udevice的dts节点,转化成udevice的平台数据
    int (*child_post_bind)(struct udevice *dev); // 如果目标设备的一个子设备被绑定之后,调用
    int (*child_pre_probe)(struct udevice *dev); // 在目标设备的一个子设备被probe之前,调用
    int (*child_post_remove)(struct udevice *dev); // 在目标设备的一个子设备被remove之后,调用
    int priv_auto_alloc_size; //需要分配多少空间作为其udevice的私有数据
    int platdata_auto_alloc_size; //需要分配多少空间作为其udevice的平台数据
    int per_child_auto_alloc_size;  // 对于目标设备的每个子设备需要分配多少空间作为父设备的私有数据
    int per_child_platdata_auto_alloc_size; // 对于目标设备的每个子设备需要分配多少空间作为父设备的平台数据
    const void *ops;    /* driver-specific operations */ // 操作集合的指针,提供给uclass使用,没有规定操作集的格式,由具体uclass决定
    uint32_t flags; // 一些标志位
};

(2)如何定义
    通过U_BOOT_DRIVER来定义一个driver
    以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,
};

U_BOOT_DRIVER实现如下:

#define U_BOOT_DRIVER(__name)                        \
    ll_entry_declare(struct driver, __name, driver)

#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)))
关于ll_entry_declare我们在《[uboot] (第六章)uboot流程——命令行模式以及命令处理介绍》已经介绍过了

最终得到如下一个结构体

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,
};

并且存放在.u_boot_list_2_driver_2_serial_s5p段中

(3)存放位置
    通过上述,我们知道serial_s5p的driver结构体_u_boot_list_2_driver_2_serial_s5p被存放在.u_boot_list_2_driver_2_serial_s5p段中。
    通过查看u-boot.map得到如下

.u_boot_list_2_driver_1
                0x23e36754        0x0 drivers/built-in.o
.u_boot_list_2_driver_2_gpio_exynos
                0x23e36754       0x44 drivers/gpio/built-in.o
                0x23e36754                _u_boot_list_2_driver_2_gpio_exynos
.u_boot_list_2_driver_2_root_driver
                0x23e36798       0x44 drivers/built-in.o
                0x23e36798                _u_boot_list_2_driver_2_root_driver
.u_boot_list_2_driver_2_serial_s5p
                0x23e367dc       0x44 drivers/serial/built-in.o
                0x23e367dc                _u_boot_list_2_driver_2_serial_s5p
.u_boot_list_2_driver_2_simple_bus_drv
                0x23e36820       0x44 drivers/built-in.o
                0x23e36820                _u_boot_list_2_driver_2_simple_bus_drv
.u_boot_list_2_driver_3
                0x23e36864        0x0 drivers/built-in.o

最终,所有driver结构体以列表的形式被放在.u_boot_list_2_driver_1和.u_boot_list_2_driver_3的区间中。
这个列表简称driver table。

(4)如何获取、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。

四、DM的一些API整理

先看一下前面一节理解一下。
1、uclass相关API

int uclass_get(enum uclass_id key, struct uclass **ucp);
// 从gd->uclass_root链表获取对应的uclass

2、uclass_driver相关API

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

3、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 device_bind(struct udevice *parent, const struct driver *drv,
        const char *name, void *platdata, int of_offset,
        struct udevice **devp)
// 初始化一个udevice,并将其与其uclass、driver绑定。

int device_bind_by_name(struct udevice *parent, bool pre_reloc_only,
            const struct driver_info *info, struct udevice **devp)
// 通过name获取driver并且调用device_bind对udevice初始化,并将其与其uclass、driver绑定。

int uclass_bind_device(struct udevice *dev)
// 绑定udevice到其对应的uclass的设备链表中
{
    uc = dev->uclass;
    list_add_tail(&dev->uclass_node, &uc->dev_head);
}

int uclass_get_device(enum uclass_id id, int index, struct udevice **devp); // 通过索引从uclass中获取udevice,注意,在获取的过程中就会对设备进行probe
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、driver相关API

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

五、uboot 设备的表达
1、说明

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

通过直接定义平台设备(这种方式基本上不使用)
    通过在设备树添加设备信息

注意:这里只是设备的定义,最终还是会被uboot解析成udevice结构体的。
2、直接定义平台设备(这种方式除了根设备外基本上不使用)

(1)通过U_BOOT_DEVICE宏来进行定义或者直接定义struct driver_info结构体
(2)U_BOOT_DEVICE宏以ns16550_serial为例

U_BOOT_DEVICE(overo_uart) = {
    "ns16550_serial",
    &overo_serial
};

U_BOOT_DEVICE实现如下:
和上述的U_BOOT_DRIVER类似,这里不详细说明了

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

(3)直接定义struct driver_info结构体,以根设备为例
uboot会创建一个根设备root,作为所有设备的祖设备
root的定义如下:

static const struct driver_info root_info = {
    .name       = "root_driver",
};

3、在设备树添加设备信息

在对应的dts文件中添加相应的设备节点和信息,以tiny210的serial为例:
arch/arm/dts/s5pv210-tiny210.dts

/dts-v1/;
#include "skeleton.dtsi"
/{
        aliases {
                console = "/serial@e2900000";
        };

serial@e2900000 {
                compatible = "samsung,exynos4210-uart";
                reg = <0xe2900000 0x100>;
                interrupts = <0 51 0>;
                id = <0>;
        };  
};

dts的内容这里不多说了。
六、uboot DM的初始化

关于下面可能使用到的一些FDT的API可以参考一下《[uboot] (番外篇)uboot之fdt介绍》。
关于下面可能使用到一些DM的API可以参考一下上述第四节。
1、主要工作

DM的初始化
        创建根设备root的udevice,存放在gd->dm_root中。
        根设备其实是一个虚拟设备,主要是为uboot的其他设备提供一个挂载点。
        初始化uclass链表gd->uclass_root

DM中udevice和uclass的解析
        udevice的创建和uclass的创建
        udevice和uclass的绑定
        uclass_driver和uclass的绑定
        driver和udevice的绑定
        部分driver函数的调用

2、入口说明

dm初始化的接口在dm_init_and_scan中。
可以发现在uboot relocate之前的initf_dm和之后的initr_dm都调用了这个函数。

static int initf_dm(void)
{
#if defined(CONFIG_DM) && defined(CONFIG_SYS_MALLOC_F_LEN)
    int ret;
    ret = dm_init_and_scan(true); // 调用dm_init_and_scan对DM进行初始化和设备的解析
    if (ret)
        return ret;
#endif
    return 0;
}

#ifdef CONFIG_DM
static int initr_dm(void)
{
    int ret;
    /* Save the pre-reloc driver model and start a new one */
    gd->dm_root_f = gd->dm_root; // 存储relocate之前的根设备
    gd->dm_root = NULL;
    ret = dm_init_and_scan(false); // 调用dm_init_and_scan对DM进行初始化和设备的解析
    if (ret)
        return ret;
    return 0;
}
#endif

主要区别在于参数。
首先说明一下dts节点中的“u-boot,dm-pre-reloc”属性,当设置了这个属性时,则表示这个设备在relocate之前就需要使用。
当dm_init_and_scan的参数为true时,只会对带有“u-boot,dm-pre-reloc”属性的节点进行解析。而当参数为false的时候,则会对所有节点都进行解析。
由于“u-boot,dm-pre-reloc”的情况比较少,所以这里只学习参数为false的情况。也就是initr_dm里面的dm_init_and_scan(false);。
2、dm_init_and_scan说明

driver/core/root.c

int dm_init_and_scan(bool pre_reloc_only)
{
    int ret;

ret = dm_init();    // DM的初始化
    if (ret) {
        debug("dm_init() failed: %d\n", ret);
        return ret;
    }
    ret = dm_scan_platdata(pre_reloc_only); //  从平台设备中解析udevice和uclass
    if (ret) {
        debug("dm_scan_platdata() failed: %d\n", ret);
        return ret;
    }

if (CONFIG_IS_ENABLED(OF_CONTROL)) {
        ret = dm_scan_fdt(gd->fdt_blob, pre_reloc_only); // 从dtb中解析udevice和uclass
        if (ret) {
            debug("dm_scan_fdt() failed: %d\n", ret);
            return ret;
        }
    }

ret = dm_scan_other(pre_reloc_only);
    if (ret)
        return ret;

return 0;
}

3、DM的初始化——dm_init

对应代码如下:
driver/core/root.c

#define DM_ROOT_NON_CONST       (((gd_t *)gd)->dm_root) // 宏定义根设备指针gd->dm_root
#define DM_UCLASS_ROOT_NON_CONST    (((gd_t *)gd)->uclass_root) // 宏定义gd->uclass_root,uclass的链表

int dm_init(void)
{
    int ret;

if (gd->dm_root) {
    // 根设备已经存在,说明DM已经初始化过了
        dm_warn("Virtual root driver already exists!\n");
        return -EINVAL;
    }

INIT_LIST_HEAD(&DM_UCLASS_ROOT_NON_CONST);
        // 初始化uclass链表

ret = device_bind_by_name(NULL, false, &root_info, &DM_ROOT_NON_CONST);
        // DM_ROOT_NON_CONST是指根设备udevice,root_info是表示根设备的设备信息
        // device_bind_by_name会查找和设备信息匹配的driver,然后创建对应的udevice和uclass并进行绑定,最后放在DM_ROOT_NON_CONST中。
        // device_bind_by_name后续我们会进行说明,这里我们暂时只需要了解root根设备的udevice以及对应的uclass都已经创建完成。

if (ret)
        return ret;
#if CONFIG_IS_ENABLED(OF_CONTROL)
    DM_ROOT_NON_CONST->of_offset = 0;
#endif
    ret = device_probe(DM_ROOT_NON_CONST);
        // 对根设备执行probe操作,
        // device_probe后续再进行说明

if (ret)
        return ret;

return 0;
}

这里就完成的DM的初始化了
(1)创建根设备root的udevice,存放在gd->dm_root中。
(2)初始化uclass链表gd->uclass_root
4、从平台设备中解析udevice和uclass——dm_scan_platdata

跳过。
5、从dtb中解析udevice和uclass——dm_scan_fdt

关于fdt以及一些对应API请参考《[uboot] (番外篇)uboot之fdt介绍》。
对应代码如下(后续我们忽略pre_reloc_only=true的情况):
driver/core/root.c

int dm_scan_fdt(const void *blob, bool pre_reloc_only)
// 此时传进来的参数blob=gd->fdt_blob, pre_reloc_only=0
{
    return dm_scan_fdt_node(gd->dm_root, blob, 0, pre_reloc_only);
// 直接调用dm_scan_fdt_node
}

int dm_scan_fdt_node(struct udevice *parent, const void *blob, int offset,
             bool pre_reloc_only)
// 此时传进来的参数
// parent=gd->dm_root,表示以root设备作为父设备开始解析
// blob=gd->fdt_blob,指定了对应的dtb
// offset=0,从偏移0的节点开始扫描
// pre_reloc_only=0,不只是解析relotion之前的设备
{
    int ret = 0, err;

/*  以下步骤相当于是遍历每一个dts节点并且调用lists_bind_fdt对其进行解析 */

for (offset = fdt_first_subnode(blob, offset);
        // 获得blob设备树的offset偏移下的节点的第一个子节点
         offset > 0;
         offset = fdt_next_subnode(blob, offset)) {
               // 循环查找下一个子节点
        if (!fdtdec_get_is_enabled(blob, offset)) {
                        // 判断节点状态是否是disable,如果是的话直接忽略
            dm_dbg("   - ignoring disabled device\n");
            continue;
        }
        err = lists_bind_fdt(parent, blob, offset, NULL);
                // 解析绑定这个节点,dm_scan_fdt的核心,下面具体分析
        if (err && !ret) {
            ret = err;
            debug("%s: ret=%d\n", fdt_get_name(blob, offset, NULL),
                  ret);
        }
    }
    return ret;
}

lists_bind_fdt是从dtb中解析udevice和uclass的核心。
其具体实现如下:
driver/core/lists.c

int lists_bind_fdt(struct udevice *parent, const void *blob, int offset,
           struct udevice **devp)
// parent指定了父设备,通过blob和offset可以获得对应的设备的dts节点,对应udevice结构通过devp返回
{
    struct driver *driver = ll_entry_start(struct driver, driver);
// 获取driver table地址
    const int n_ents = ll_entry_count(struct driver, driver);
// 获取driver table长度
    const struct udevice_id *id;
    struct driver *entry;
    struct udevice *dev;
    bool found = false;
    const char *name;
    int result = 0;
    int ret = 0;

dm_dbg("bind node %s\n", fdt_get_name(blob, offset, NULL));
// 打印当前解析的节点的名称
    if (devp)
        *devp = NULL;
    for (entry = driver; entry != driver + n_ents; entry++) {
// 遍历driver table中的所有driver,具体参考三、4一节
        ret = driver_check_compatible(blob, offset, entry->of_match,
                          &id);
// 判断driver中的compatibile字段和dts节点是否匹配
        name = fdt_get_name(blob, offset, NULL);
// 获取节点名称
        if (ret == -ENOENT) {
            continue;
        } else if (ret == -ENODEV) {
            dm_dbg("Device '%s' has no compatible string\n", name);
            break;
        } else if (ret) {
            dm_warn("Device tree error at offset %d\n", offset);
            result = ret;
            break;
        }

dm_dbg("   - found match at '%s'\n", entry->name);
        ret = device_bind(parent, entry, name, NULL, offset, &dev);
// 找到对应的driver,调用device_bind进行绑定,会在这个函数中创建对应udevice和uclass并切进行绑定,后面继续说明
        if (ret) {
            dm_warn("Error binding driver '%s': %d\n", entry->name,
                ret);
            return ret;
        } else {
            dev->driver_data = id->data;
            found = true;
            if (devp)
                *devp = dev;
// 将udevice设置到devp指向的地方中,进行返回
        }
        break;
    }

if (!found && !result && ret != -ENODEV) {
        dm_dbg("No match for node '%s'\n",
               fdt_get_name(blob, offset, NULL));
    }

return result;
}

在device_bind中实现了udevice和uclass的创建和绑定以及一些初始化操作,这里专门学习一下device_bind。
device_bind的实现如下(去除部分代码)
driver/core/device.c

int device_bind(struct udevice *parent, const struct driver *drv,
        const char *name, void *platdata, int of_offset,
        struct udevice **devp)
// parent:父设备
// drv:设备对应的driver
// name:设备名称
// platdata:设备的平台数据指针
// of_offset:在dtb中的偏移,即代表了其dts节点
// devp:所创建的udevice的指针,用于返回
{
    struct udevice *dev;
    struct uclass *uc;
    int size, ret = 0;

ret = uclass_get(drv->id, &uc);
        // 获取driver id对应的uclass,如果uclass原先并不存在,那么会在这里创建uclass并其uclass_driver进行绑定

dev = calloc(1, sizeof(struct udevice));
        // 分配一个udevice

dev->platdata = platdata; // 设置udevice的平台数据指针
    dev->name = name; // 设置udevice的name
    dev->of_offset = of_offset; // 设置udevice的dts节点偏移
    dev->parent = parent; // 设置udevice的父设备
    dev->driver = drv;    // 设置udevice的对应的driver,相当于driver和udevice的绑定
    dev->uclass = uc;    // 设置udevice的所属uclass

dev->seq = -1;
    dev->req_seq = -1;
    if (CONFIG_IS_ENABLED(OF_CONTROL) && CONFIG_IS_ENABLED(DM_SEQ_ALIAS)) {
        /*
         * Some devices, such as a SPI bus, I2C bus and serial ports
         * are numbered using aliases.
         *
         * This is just a 'requested' sequence, and will be
         * resolved (and ->seq updated) when the device is probed.
         */
        if (uc->uc_drv->flags & DM_UC_FLAG_SEQ_ALIAS) {
            if (uc->uc_drv->name && of_offset != -1) {
                fdtdec_get_alias_seq(gd->fdt_blob,
                        uc->uc_drv->name, of_offset,
                        &dev->req_seq);
            }
                    // 设置udevice的alias请求序号
        }
    }

if (!dev->platdata && drv->platdata_auto_alloc_size) {
        dev->flags |= DM_FLAG_ALLOC_PDATA;
        dev->platdata = calloc(1, drv->platdata_auto_alloc_size);
                // 为udevice分配平台数据的空间,由driver中的platdata_auto_alloc_size决定
    }

size = uc->uc_drv->per_device_platdata_auto_alloc_size;
    if (size) {
        dev->flags |= DM_FLAG_ALLOC_UCLASS_PDATA;
        dev->uclass_platdata = calloc(1, size);
                // 为udevice分配给其所属uclass使用的平台数据的空间,由所属uclass的driver中的per_device_platdata_auto_alloc_size决定
    }

/* put dev into parent's successor list */
    if (parent)
        list_add_tail(&dev->sibling_node, &parent->child_head);
        // 添加到父设备的子设备链表中

ret = uclass_bind_device(dev);
        // uclass和udevice进行绑定,主要是实现了将udevice链接到uclass的设备链表中

/* if we fail to bind we remove device from successors and free it */
    if (drv->bind) {
        ret = drv->bind(dev);
        // 执行udevice对应driver的bind函数
    }

if (parent && parent->driver->child_post_bind) {
        ret = parent->driver->child_post_bind(dev);
        // 执行父设备的driver的child_post_bind函数
    }
    if (uc->uc_drv->post_bind) {
        ret = uc->uc_drv->post_bind(dev);
        if (ret)
            goto fail_uclass_post_bind;
        // 执行所属uclass的post_bind函数
    }

if (devp)
        *devp = dev;
        // 将udevice进行返回

dev->flags |= DM_FLAG_BOUND;
        // 设置已经绑定的标志
        // 后续可以通过dev->flags & DM_FLAG_ACTIVATED或者device_active宏来判断设备是否已经被激活

return 0;

上述就完成了dtb的解析,udevice和uclass的创建,以及各个组成部分的绑定关系。
注意,这里只是绑定,即调用了driver的bind函数,但是设备还没有真正激活,也就是还没有执行设备的probe函数。
七、DM工作流程

经过前面的DM初始化以及设备解析之后,我们只是建立了udevice和uclass之间的绑定关系。但是此时udevice还没有被probe,其对应设备还没有被激活。
激活一个设备主要是通过device_probe函数,所以在介绍DM的工作流程前,先说明device_probe函数。
1、device_probe

driver/core/device.c

int device_probe(struct udevice *dev)
{
    const struct driver *drv;
    int size = 0;
    int ret;
    int seq;

if (dev->flags & DM_FLAG_ACTIVATED)
        return 0;
// 表示这个设备已经被激活了

drv = dev->driver;
    assert(drv);
// 获取这个设备对应的driver

/* Allocate private data if requested and not reentered */
    if (drv->priv_auto_alloc_size && !dev->priv) {
        dev->priv = alloc_priv(drv->priv_auto_alloc_size, drv->flags);
// 为设备分配私有数据
    }

/* Allocate private data if requested and not reentered */
    size = dev->uclass->uc_drv->per_device_auto_alloc_size;
    if (size && !dev->uclass_priv) {
        dev->uclass_priv = calloc(1, size);
// 为设备所属uclass分配私有数据
    }

// 这里过滤父设备的probe

seq = uclass_resolve_seq(dev);
    if (seq < 0) {
        ret = seq;
        goto fail;
    }
    dev->seq = seq;

dev->flags |= DM_FLAG_ACTIVATED;
// 设置udevice的激活标志

ret = uclass_pre_probe_device(dev);
// uclass在probe device之前的一些函数的调用

if (drv->ofdata_to_platdata && dev->of_offset >= 0) {
        ret = drv->ofdata_to_platdata(dev);
// 调用driver中的ofdata_to_platdata将dts信息转化为设备的平台数据
    }

if (drv->probe) {
        ret = drv->probe(dev);
// 调用driver的probe函数,到这里设备才真正激活了
    }

ret = uclass_post_probe_device(dev);

return ret;
}

主要工作归纳如下:

分配设备的私有数据
    对父设备进行probe
    执行probe device之前uclass需要调用的一些函数
    调用driver的ofdata_to_platdata,将dts信息转化为设备的平台数据
    调用driver的probe函数
    执行probe device之后uclass需要调用的一些函数

2、通过uclass来获取一个udevice并且进行probe

通过uclass来获取一个udevice并且进行probe有如下接口
driver/core/uclass.c

int uclass_get_device(enum uclass_id id, int index, struct udevice **devp)  //通过索引从uclass的设备链表中获取udevice,并且进行probe
int uclass_get_device_by_name(enum uclass_id id, const char *name,
                  struct udevice **devp) //通过设备名从uclass的设备链表中获取udevice,并且进行probe
int uclass_get_device_by_seq(enum uclass_id id, int seq, struct udevice **devp) //通过序号从uclass的设备链表中获取udevice,并且进行probe
int uclass_get_device_by_of_offset(enum uclass_id id, int node,
                   struct udevice **devp) //通过dts节点的偏移从uclass的设备链表中获取udevice,并且进行probe
int uclass_get_device_by_phandle(enum uclass_id id, struct udevice *parent,
                 const char *name, struct udevice **devp) //通过设备的“phandle”属性从uclass的设备链表中获取udevice,并且进行probe
int uclass_first_device(enum uclass_id id, struct udevice **devp) //从uclass的设备链表中获取第一个udevice,并且进行probe
int uclass_next_device(struct udevice **devp) //从uclass的设备链表中获取下一个udevice,并且进行probe

这些接口主要是获取设备的方法上有所区别,但是probe设备的方法都是一样的,都是通过调用uclass_get_device_tail->device_probe来probe设备的。
以uclass_get_device为例

int uclass_get_device(enum uclass_id id, int index, struct udevice **devp)
{
    struct udevice *dev;
    int ret;

*devp = NULL;
    ret = uclass_find_device(id, index, &dev); //通过索引从uclass的设备链表中获取对应的udevice
    return uclass_get_device_tail(dev, ret, devp); // 调用uclass_get_device_tail进行设备的get,最终会调用device_probe来对设备进行probe
}

int uclass_get_device_tail(struct udevice *dev, int ret,
                  struct udevice **devp)
{
    ret = device_probe(dev);
// 调用device_probe对设备进行probe,这个函数在前面说明过了
    if (ret)
        return ret;

*devp = dev;

return 0;
}

3、工作流程简单说明

serial-uclass较为简单,我们以serial-uclass为例

(0)代码支持
    < 1 > serial-uclass.c中定义一个uclass_driver

UCLASS_DRIVER(serial) = {
    .id        = UCLASS_SERIAL, //注意这里的uclass id
    .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),
};

< 2 > 定义s5pv210的serial的dts节点

serial@e2900000 {
                compatible = "samsung,exynos4210-uart"; //注意这里的compatible
                reg = <0xe2900000 0x100>;
                interrupts = <0 51 0>;
                id = <0>;
        };

< 3 > 定义设备驱动

U_BOOT_DRIVER(serial_s5p) = {
    .name    = "serial_s5p",
    .id    = UCLASS_SERIAL, //注意这里的uclass id
    .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,
};

static const struct udevice_id s5p_serial_ids[] = {
    { .compatible = "samsung,exynos4210-uart" }, //注意这里的compatible
    { }
};

(1)udevice和对应uclass的创建
    在DM初始化的过程中uboot自己创建对应的udevice和uclass。
    具体参考“六、uboot DM的初始化”

(2)udevice和对应uclass的绑定
    在DM初始化的过程中uboot自己实现将udevice绑定到对应的uclass中。
    具体参考“六、uboot DM的初始化”

(3)对应udevice的probe
    由模块自己实现。例如serial则需要在serial的初始化过程中,选择需要的udevice进行probe。
    serial-uclass只是操作作为console的serial,并不具有通用性,这里简单的了解下。
    代码如下,过滤掉无关代码
    driver/serial/serial-uclass.c

int serial_init(void)
{
    serial_find_console_or_panic(); // 调用serial_find_console_or_panic进行作为console的serial的初始化
    gd->flags |= GD_FLG_SERIAL_READY;

return 0;
}

static void serial_find_console_or_panic(void)
{
    const void *blob = gd->fdt_blob;
    struct udevice *dev;
    int node;

if (CONFIG_IS_ENABLED(OF_CONTROL) && blob) {
        /* Check for a chosen console */

// 这里过滤掉获取指定的serial的dts节点的代码

if (!uclass_get_device_by_of_offset(UCLASS_SERIAL, node,
                            &dev)) {
// 这里调用uclass_get_device_by_of_offset,通过dts节点的偏移从uclass的设备链表中获取udevice,并且进行probe。
// 注意,是在这里完成设备的probe的!!!
            gd->cur_serial_dev = dev;
// 将udevice存储在gd->cur_serial_dev,后续uclass中可以直接通过gd->cur_serial_dev获取到对应的设备并且进行操作
// 但是注意,这种并不是通用做法!!!
            return;
        }
    }
}
    (4)uclass的接口调用
        可以通过先从root_uclass链表中提取对应的uclass,然后通过uclass->uclass_driver->ops来进行接口调用,这种方法比较具有通用性。
        可以通过调用uclass直接expert的接口,不推荐,但是serial-uclass使用的是这种方式。
        这部分应该属于serial core,但是也放在了serial-uclass.c中实现。
        以serial_putc调用为例,serial-uclass使用如下:

void serial_putc(char ch)
{
    if (gd->cur_serial_dev)
        _serial_putc(gd->cur_serial_dev, ch);// 将console对应的serial的udevice作为参数传入
}

static void _serial_putc(struct udevice *dev, char ch)
{
    struct dm_serial_ops *ops = serial_get_ops(dev);// 获取设备对应的driver函数的ops操作集
    int err;

do {
        err = ops->putc(dev, ch); // 以udevice为参数,调用ops中对应的操作函数
    } while (err == -EAGAIN);
}

到此整个流程简单介绍到这。

这里几乎都是纸上谈兵,后续会来一篇gpio-uclass的使用实战。
————————————————
版权声明:本文为CSDN博主「ooonebook」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/ooonebook/article/details/53234020

[uboot] (番外篇)uboot 驱动模型(转)重要的更多相关文章

  1. [uboot] (番外篇)uboot串口&console&stdio设备工作流程 (转)

    [uboot] uboot流程系列:[project X] tiny210(s5pv210)上电启动流程(BL0-BL2)[project X] tiny210(s5pv210)从存储设备加载代码到D ...

  2. [uboot] (番外篇)uboot之fdt介绍

    http://blog.csdn.net/ooonebook/article/details/53206623 以下例子都以project X项目tiny210(s5pv210平台,armv7架构)为 ...

  3. [uboot] (番外篇)uboot之fdt介绍 (转)

    以下例子都以project X项目tiny210(s5pv210平台,armv7架构)为例 [uboot] uboot流程系列:[project X] tiny210(s5pv210)上电启动流程(B ...

  4. 【番外篇】ASP.NET MVC快速入门之免费jQuery控件库(MVC5+EF6)

    目录 [第一篇]ASP.NET MVC快速入门之数据库操作(MVC5+EF6) [第二篇]ASP.NET MVC快速入门之数据注解(MVC5+EF6) [第三篇]ASP.NET MVC快速入门之安全策 ...

  5. 给深度学习入门者的Python快速教程 - 番外篇之Python-OpenCV

    这次博客园的排版彻底残了..高清版请移步: https://zhuanlan.zhihu.com/p/24425116 本篇是前面两篇教程: 给深度学习入门者的Python快速教程 - 基础篇 给深度 ...

  6. 可视化(番外篇)——在Eclipse RCP中玩转OpenGL

    最近在看有关Eclipse RCP方面的东西,鉴于Gephi是使用opengl作为绘图引擎,所以,萌生了在Eclipse RCP下添加画布,使用opengl绘图的想法,网上有博文详细介绍这方面的内容, ...

  7. 可视化(番外篇)——SWT总结

    本篇主要介绍如何在SWT下构建一个应用,如何安装SWT Designer并破解已进行SWT的可视化编程,Display以及Shell为何物.有何用,SWT中的常用组件.面板容器以及事件模型等. 1.可 ...

  8. C++雾中风景番外篇:理解C++的复杂声明与声明解析

    在学习C系列语言的过程之中,理解C/C++的复杂声明一直是初学者很困扰的问题.笔者初学之时也深受困扰,对很多规则死记硬背.后续在阅读<C专家编程>之后,尝试在编译器的角度来理解C/C++的 ...

  9. python之爬虫--番外篇(一)进程,线程的初步了解

    整理这番外篇的原因是希望能够让爬虫的朋友更加理解这块内容,因为爬虫爬取数据可能很简单,但是如何高效持久的爬,利用进程,线程,以及异步IO,其实很多人和我一样,故整理此系列番外篇 一.进程 程序并不能单 ...

随机推荐

  1. Linux中脚本运行错误(坏的解释器:没有那个文件或目录)

    原因: 在Linux中有时候我们将在Windows下编写的脚本拷贝到Linux环境中运行时会出现运行不了的情况. 主要还是Windows的换行符为\r\n,而Linux环境中的换行符号为\n. 解决办 ...

  2. 记日杂-log4net组件使用

    下面我给大家介绍一下记日杂-log4net组件使用,当程序发布有服务器上,有时出现了错误, 都不知道出现在那,所以log4net组件很好解决这个问题. 1.添加开发包,并对log4net.dll的引用 ...

  3. SQLite基础-7.子句(一)

    目录 SQLite子句(一) 1. WHERE子句 2. LIKE子句 3. GLOB 子句 4. Oreder By 子句 SQLite子句(一) 1. WHERE子句 WHERE 子句后面跟着条件 ...

  4. phpstudy 最新版linux 面板 web防火墙后门防护功能教程

    phpstudy linux 面板针对服务器和网站做了全面的安全防护措施,尽可能的防范网站被入侵,留置后门风险,本篇文章着重介绍phpstudy linux 面板其中的一项安全功能 [网站防火墙]之[ ...

  5. MySQL8在CentOS7上的安装

    Install_CentOS7_MySQL8_binary.sh #!/bin/bash MySQL_Package=mysql-8.0.16-linux-glibc2.12-x86_64.tar.x ...

  6. 欧拉函数小结 hdu2588+

    从费马小定理到欧拉定理 欧拉公式 再到欧拉函数.,. 小结一下欧拉函数吧 对正整数n,欧拉函数是小于n的正整数中与n互质的数的数目(φ(1)=1)----定义 欧拉函数的基本公式其中pi为x的素因子 ...

  7. O054、Attach Volume 操作(Part II)

    参考https://www.cnblogs.com/CloudMan6/p/5631328.html     计算节点作为iSCSI initiator 访问存储节点 iSCSI Target 上的v ...

  8. ubuntu 编译zbar 静态库

    wget http://downloads.sourceforge.net/project/zbar/zbar/0.10/zbar-0.10.tar.gz tar -zvxf zbar-0.10.ta ...

  9. java 计算中位数方法

    最近工作需要 要求把python的代码写成java版本,python中有一个np.median()求中位数的方法,java决定手写一个 先说说什么是中位数: 中位数就是中间的那个数, 如果一个集合是奇 ...

  10. vue中使用proxy配置不同端口和ip接口

    问题描述: 使用vue-cli创建的项目,开发地址是localhost:8080,由于后台开发不同的模块,导致每个模块请求的ip和端口号不一致 例如:http://192.168.10.22:8081 ...