[Linux学习笔记]Uboot-DM的分析过程

前言

本文针对RK3506平台下uboot的设备模型(device model)进行相关分析与记录,阅读前需有以下基础:

  • 手头有一份编译通过的Uboot源码,能够方便查看函数原型与调用关系
  • 理解Uboot的启动流程
  • 理解U-boot中的内存布局以及linklist数据结构
  • 理解Linux内核中的list数据结构与相关API
  • 理解Linux基于devicetree的驱动开发流程

有以上基础后才方便看懂以下内容,否则请按需补全:

1.DM数据结构

1.1 gd中的DM数据结构

详见文件include/asm-generic/global_data.h

C 复制代码
typedef struct global_data {
   ...
#ifdef CONFIG_DM
    struct udevice	*dm_root;	/* Root instance for Driver Model */
    struct udevice	*dm_root_f;	/* Pre-relocation root instance */
    struct list_head      uclass_root;	/* Head of core tree */
#endif
   ...
} gd_t;

1.2 struct list_head

详见文件include/linux/list.h

C 复制代码
struct list_head {
	struct list_head *next, *prev;
};

是典型的linux风格双向链表数据结构节点。

相关的几个重要接口:

C 复制代码
#define hlist_entry(ptr, type, member) container_of(ptr,type,member)

#define hlist_for_each(pos, head) \
	for (pos = (head)->first; pos && ({ prefetch(pos->next); 1; }); \
	     pos = pos->next)

#define hlist_for_each_safe(pos, n, head) \
	for (pos = (head)->first; pos && ({ n = pos->next; 1; }); \
	     pos = n)

/**
 * hlist_for_each_entry	- iterate over list of given type
 * @tpos:	the type * to use as a loop cursor.
 * @pos:	the &struct hlist_node to use as a loop cursor.
 * @head:	the head for your list.
 * @member:	the name of the hlist_node within the struct.
 */
#define hlist_for_each_entry(tpos, pos, head, member)			 \
	for (pos = (head)->first;					 \
	     pos && ({ prefetch(pos->next); 1;}) &&			 \
		({ tpos = hlist_entry(pos, typeof(*tpos), member); 1;}); \
	     pos = pos->next)

/**
 * hlist_for_each_entry_continue - iterate over a hlist continuing after current point
 * @tpos:	the type * to use as a loop cursor.
 * @pos:	the &struct hlist_node to use as a loop cursor.
 * @member:	the name of the hlist_node within the struct.
 */
#define hlist_for_each_entry_continue(tpos, pos, member)		 \
	for (pos = (pos)->next;						 \
	     pos && ({ prefetch(pos->next); 1;}) &&			 \
		({ tpos = hlist_entry(pos, typeof(*tpos), member); 1;}); \
	     pos = pos->next)

/**
 * hlist_for_each_entry_from - iterate over a hlist continuing from current point
 * @tpos:	the type * to use as a loop cursor.
 * @pos:	the &struct hlist_node to use as a loop cursor.
 * @member:	the name of the hlist_node within the struct.
 */
#define hlist_for_each_entry_from(tpos, pos, member)			 \
	for (; pos && ({ prefetch(pos->next); 1;}) &&			 \
		({ tpos = hlist_entry(pos, typeof(*tpos), member); 1;}); \
	     pos = pos->next)

/**
 * hlist_for_each_entry_safe - iterate over list of given type safe against removal of list entry
 * @tpos:	the type * to use as a loop cursor.
 * @pos:	the &struct hlist_node to use as a loop cursor.
 * @n:		another &struct hlist_node to use as temporary storage
 * @head:	the head for your list.
 * @member:	the name of the hlist_node within the struct.
 */
#define hlist_for_each_entry_safe(tpos, pos, n, head, member)		 \
	for (pos = (head)->first;					 \
	     pos && ({ n = pos->next; 1; }) &&				 \
		({ tpos = hlist_entry(pos, typeof(*tpos), member); 1;}); \
	     pos = n)

由此可见uclass_root将是一个串联的链表。

1.3 struct udevice

struct udevice包含了一个设备节点所具备的内容,详见文件include/dm/device.h

C 复制代码
/**
 * struct udevice - An instance of a driver
 *
 * This holds information about a device, which is a driver bound to a
 * particular port or peripheral (essentially a driver instance).
 *
 * A device will come into existence through a 'bind' call, either due to
 * a U_BOOT_DEVICE() macro (in which case platdata is non-NULL) or a node
 * in the device tree (in which case of_offset is >= 0). In the latter case
 * we translate the device tree information into platdata in a function
 * implemented by the driver ofdata_to_platdata method (called just before the
 * probe method if the device has a device tree node.
 *
 * All three of platdata, priv and uclass_priv can be allocated by the
 * driver, or you can use the auto_alloc_size members of struct driver and
 * struct uclass_driver to have driver model do this automatically.
 *
 * @driver: The driver used by this device
 * @name: Name of device, typically the FDT node name
 * @platdata: Configuration data for this device
 * @parent_platdata: The parent bus's configuration data for this device
 * @uclass_platdata: The uclass's configuration data for this device
 * @node: Reference to device tree node for this device
 * @driver_data: Driver data word for the entry that matched this device with
 *		its driver
 * @parent: Parent of this device, or NULL for the top level device
 * @priv: Private data for this device
 * @uclass: Pointer to uclass for this device
 * @uclass_priv: The uclass's private data for this device
 * @parent_priv: The parent's private data for this device
 * @uclass_node: Used by uclass to link its devices
 * @child_head: List of children of this device
 * @sibling_node: Next device in list of all devices
 * @flags: Flags for this device DM_FLAG_...
 * @req_seq: Requested sequence number for this device (-1 = any)
 * @seq: Allocated sequence number for this device (-1 = none). This is set up
 * when the device is probed and will be unique within the device's uclass.
 * @devres_head: List of memory allocations associated with this device.
 *		When CONFIG_DEVRES is enabled, devm_kmalloc() and friends will
 *		add to this list. Memory so-allocated will be freed
 *		automatically when the device is removed / unbound
 */
struct udevice {
    const struct driver *driver;
    const char *name;
    void *platdata;
    void *parent_platdata;
    void *uclass_platdata;
    ofnode node;
    ulong driver_data;
    struct udevice *parent;
    void *priv;
    struct uclass *uclass;
    void *uclass_priv;
    void *parent_priv;
    struct list_head uclass_node;
    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
    };

1.4 struct driver

struct driver包含了驱动程序的接口,详见文件iclude/dm/device.h

C 复制代码
/**
 * struct driver - A driver for a feature or peripheral
 *
 * This holds methods for setting up a new device, and also removing it.
 * The device needs information to set itself up - this is provided either
 * by platdata or a device tree node (which we find by looking up
 * matching compatible strings with of_match).
 *
 * Drivers all belong to a uclass, representing a class of devices of the
 * same type. Common elements of the drivers can be implemented in the uclass,
 * or the uclass can provide a consistent interface to the drivers within
 * it.
 *
 * @name: Device name
 * @id: Identiies the uclass we belong to
 * @of_match: List of compatible strings to match, and any identifying data
 * for each.
 * @bind: Called to bind a device to its driver
 * @probe: Called to probe a device, i.e. activate it
 * @remove: Called to remove a device, i.e. de-activate it
 * @unbind: Called to unbind a device from its driver
 * @ofdata_to_platdata: Called before probe to decode device tree data
 * @child_post_bind: Called after a new child has been bound
 * @child_pre_probe: Called before a child device is probed. The device has
 * memory allocated but it has not yet been probed.
 * @child_post_remove: Called after a child device is removed. The device
 * has memory allocated but its device_remove() method has been called.
 * @priv_auto_alloc_size: If non-zero this is the size of the private data
 * to be allocated in the device's ->priv pointer. If zero, then the driver
 * is responsible for allocating any data required.
 * @platdata_auto_alloc_size: If non-zero this is the size of the
 * platform data to be allocated in the device's ->platdata pointer.
 * This is typically only useful for device-tree-aware drivers (those with
 * an of_match), since drivers which use platdata will have the data
 * provided in the U_BOOT_DEVICE() instantiation.
 * @per_child_auto_alloc_size: Each device can hold private data owned by
 * its parent. If required this will be automatically allocated if this
 * value is non-zero.
 * @per_child_platdata_auto_alloc_size: A bus likes to store information about
 * its children. If non-zero this is the size of this data, to be allocated
 * in the child's parent_platdata pointer.
 * @ops: Driver-specific operations. This is typically a list of function
 * pointers defined by the driver, to implement driver functions required by
 * the uclass.
 * @flags: driver flags - see DM_FLAGS_...
 */
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;
};

1.5 struct uclass

struct uclass定义了驱动类,详见文件include/dm/uclass.h

C 复制代码
/**
 * struct uclass - a U-Boot drive class, collecting together similar drivers
 *
 * A uclass provides an interface to a particular function, which is
 * implemented by one or more drivers. Every driver belongs to a uclass even
 * if it is the only driver in that uclass. An example uclass is GPIO, which
 * provides the ability to change read inputs, set and clear outputs, etc.
 * There may be drivers for on-chip SoC GPIO banks, I2C GPIO expanders and
 * PMIC IO lines, all made available in a unified way through the uclass.
 *
 * @priv: Private data for this uclass
 * @uc_drv: The driver for the uclass itself, not to be confused with a
 * 'struct driver'
 * @dev_head: List of devices in this uclass (devices are attached to their
 * uclass when their bind method is called)
 * @sibling_node: Next uclass in the linked list of uclasses
 */
struct uclass {
	void *priv;
	struct uclass_driver *uc_drv;
	struct list_head dev_head;
	struct list_head sibling_node;
#ifdef CONFIG_USING_KERNEL_DTB_V2
	struct list_head *u_boot_dev_head;
#endif
};

展开看struct uclass_driver的定义如下:

C 复制代码
/**
 * struct uclass_driver - Driver for the uclass
 *
 * A uclass_driver provides a consistent interface to a set of related
 * drivers.
 *
 * @name: Name of uclass driver
 * @id: ID number of this uclass
 * @post_bind: Called after a new device is bound to this uclass
 * @pre_unbind: Called before a device is unbound from this uclass
 * @pre_probe: Called before a new device is probed
 * @post_probe: Called after a new device is probed
 * @pre_remove: Called before a device is removed
 * @child_post_bind: Called after a child is bound to a device in this uclass
 * @init: Called to set up the uclass
 * @destroy: Called to destroy the uclass
 * @priv_auto_alloc_size: If non-zero this is the size of the private data
 * to be allocated in the uclass's ->priv pointer. If zero, then the uclass
 * driver is responsible for allocating any data required.
 * @per_device_auto_alloc_size: Each device can hold private data owned
 * by the uclass. If required this will be automatically allocated if this
 * value is non-zero.
 * @per_device_platdata_auto_alloc_size: Each device can hold platform data
 * owned by the uclass as 'dev->uclass_platdata'. If the value is non-zero,
 * then this will be automatically allocated.
 * @per_child_auto_alloc_size: Each child device (of a parent in this
 * uclass) can hold parent data for the device/uclass. This value is only
 * used as a falback if this member is 0 in the driver.
 * @per_child_platdata_auto_alloc_size: A bus likes to store information about
 * its children. If non-zero this is the size of this data, to be allocated
 * in the child device's parent_platdata pointer. This value is only used as
 * a falback if this member is 0 in the driver.
 * @ops: Uclass operations, providing the consistent interface to devices
 * within the uclass.
 * @flags: Flags for this uclass (DM_UC_...)
 */
struct uclass_driver {
	const char *name;
	enum uclass_id id;
	int (*post_bind)(struct udevice *dev);
	int (*pre_unbind)(struct udevice *dev);
	int (*pre_probe)(struct udevice *dev);
	int (*post_probe)(struct udevice *dev);
	int (*pre_remove)(struct udevice *dev);
	int (*child_post_bind)(struct udevice *dev);
	int (*child_pre_probe)(struct udevice *dev);
	int (*init)(struct uclass *class);
	int (*destroy)(struct uclass *class);
	int priv_auto_alloc_size;
	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;
};

struct uclass_driver管理的是这个驱动大类的共性行为,当某一个driverbind被调用时,如果绑定流程前面部分都执行成功,并且该class_driver实现了post_bind,那么它会在driver->bind()成功返回之后被调用。

2.DM初始化流程

2.1SPL阶段下的DM初始化流程

追踪调用链

C 复制代码
board_init_f(ulong dummy)
    spl_early_init()
        spl_common_init(true)
            dm_init_and_scan(!CONFIG_IS_ENABLED(OF_PLATDATA))

重点在于dm_init_and_scan()

C 复制代码
int dm_init_and_scan(bool pre_reloc_only)
{
    int ret;

    ret = dm_init(IS_ENABLED(CONFIG_OF_LIVE));
    if (ret) {
            debug("dm_init() failed: %d\n", ret);
            return ret;
    }
    ret = dm_scan_platdata(pre_reloc_only);
    if (ret) {
            debug("dm_scan_platdata() failed: %d\n", ret);
            return ret;
    }

    if (CONFIG_IS_ENABLED(OF_CONTROL) && !CONFIG_IS_ENABLED(OF_PLATDATA)) {
            ret = dm_extended_scan_fdt(gd->fdt_blob, pre_reloc_only);
            if (ret) {
                    debug("dm_extended_scan_dt() failed: %d\n", ret);
                    return ret;
            }
    }

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

    return 0;
}

此处dm_init_and_scan分为4个部分,dm_init(),dm_scan_platdata(),dm_extenede_scan_dt()dm_scan_other()

2.2第一部分dm_init()

先看dm_init(),位于文件drivers/core/root.c

C 复制代码
int dm_init(bool of_live)
{
    int ret;

    if (gd->dm_root) {
            dm_warn("Virtual root driver already exists!\n");
            return -EINVAL;
    }
    INIT_LIST_HEAD(&DM_UCLASS_ROOT_NON_CONST);

#if defined(CONFIG_NEEDS_MANUAL_RELOC)
    fix_drivers();
    fix_uclass();
    fix_devices();
#endif

    ret = device_bind_by_name(NULL, false, &root_info, &DM_ROOT_NON_CONST);
    if (ret)
            return ret;
    #if CONFIG_IS_ENABLED(OF_CONTROL)
    # if CONFIG_IS_ENABLED(OF_LIVE)
    if (of_live)
        DM_ROOT_NON_CONST->node = np_to_ofnode(gd->of_root);
    else
    #endif
        DM_ROOT_NON_CONST->node = offset_to_ofnode(0);
    #endif
    ret = device_probe(DM_ROOT_NON_CONST);
    if (ret)
        return ret;

    return 0;
}

可见dm_init() 是 DM 框架真正开始建立运行时骨架的地方。它本身不负责扫描具体设备,而是先把 DM 后续扫描和绑定所依赖的基础结构搭起来。

从代码上看,dm_init() 大致可以拆成下面几步:

2.2.1 检查gd->dm_root是否已存在

C 复制代码
if (gd->dm_root) 
{  
    dm_warn("Virtual root driver already exists!\n");  
    return -EINVAL;  
}

这里首先检查 gd->dm_root
gd->dm_root 保存的是 DM 设备树的虚拟根设备。如果它已经存在,说明根节点已经初始化过了,此时再次初始化就属于重复操作,因此直接返回错误。

这里也说明了一点:

  • gd->dm_root 不是链表头
  • 它是一个 struct udevice *
  • 它指向的是整棵 DM 设备树的根设备

也就是说,后续所有顶层设备,最终都会作为它的子设备挂到它下面。

2.2.2 初始化uclass总链表表头

C 复制代码
INIT_LIST_HEAD(&DM_UCLASS_ROOT_NON_CONST);

这一步用于初始化 uclass 总链表 的表头,展开宏定义与函数声明:

C 复制代码
/* Cast away any volatile pointer */
#define DM_ROOT_NON_CONST		(((gd_t *)gd)->dm_root)
#define DM_UCLASS_ROOT_NON_CONST	(((gd_t *)gd)->uclass_root)

static inline void INIT_LIST_HEAD(struct list_head *list)
{
	list->next = list;
	list->prev = list;
}

结合前面的数据结构分析可以知道:

  • gd->uclass_rootuclass链表的表头
  • 每个 struct uclass 通过自己的 sibling_node 挂到这条链表上

dm_init() 刚开始时,这条链表还没有任何真正的 uclass 节点,所以必须先把表头初始化好。

否则后面在绑定设备、创建对应 uclass 时,就没有合法的挂接位置。

2.2.3 手动重定位修正

C 复制代码
#if defined(CONFIG_NEEDS_MANUAL_RELOC)
    fix_drivers();
    fix_uclass();
    fix_devices();
#endif

这一段只在 CONFIG_NEEDS_MANUAL_RELOC 打开时生效。

它的作用是对编译阶段放在固定地址表中的若干指针进行运行时重定位修正,保证搬移到 RAM 之后,这些指针仍然能正确访问。

这里分别修正了三类对象:

  • fix_drivers():修正所有 driver 相关指针
  • fix_uclass():修正所有 uclass_driver 相关指针
  • fix_devices():修正静态设备描述信息

2.2.4 绑定虚拟根设备 root

C 复制代码
ret = device_bind_by_name(NULL, false, &root_info, &DM_ROOT_NON_CONST);
if (ret)
    return ret;

这是 dm_init() 中最关键的一步。

它调用 device_bind_by_name(),根据 root_info 中给出的名字,创建出一个 root device ,并保存到 DM_ROOT_NON_CONST,也就是 gd->dm_root

接口参数列表如下:

C 复制代码
int device_bind_by_name(struct udevice *parent, bool pre_reloc_only,
			const struct driver_info *info, struct udevice **devp)

这里的几个关键点是:

  • parent 传入的是 NULL。这说明当前正在创建的是最顶层根设备,它自己没有父设备。
  • root_info 指向的是 root_driver
  • 绑定过程中会自动建立三层关系

当 root 设备被 bind 时,会同时建立:

  • udevice -> driver
  • udevice -> uclass
  • uclass -> gd->uclass_root

2.2.5 为根设备设置ofnode

C 复制代码
#if CONFIG_IS_ENABLED(OF_CONTROL)
# if CONFIG_IS_ENABLED(OF_LIVE)
    if (of_live)
        DM_ROOT_NON_CONST->node = np_to_ofnode(gd->of_root);
    else
# endif
        DM_ROOT_NON_CONST->node = offset_to_ofnode(0);
#endif

这一段用于给刚刚创建出来的root device绑定设备树节点信息。

根据配置不同,有两种情况:

1)live device tree 模式
C 复制代码
DM_ROOT_NON_CONST->node = np_to_ofnode(gd->of_root);

这表示使用的是运行时展开后的live tree,根节点来自 gd->of_root

2)普通 flat dtb 模式
C 复制代码
DM_ROOT_NON_CONST->node = offset_to_ofnode(0);

这里直接把设备树偏移 0 处作为根节点,也就是 dtb 的 / 根节点。

这一步的意义在于:

  • gd->dm_root 对应到设备树根节点 /
  • 后续扫描 dtb 时,顶层设备节点都可以从这个根节点开始向下展开

所以,root device既是 driver model 的逻辑根设备 ,也和 设备树根节点 对应起来了。

2.2.6 对根设备执行 probe

C 复制代码
ret = device_probe(DM_ROOT_NON_CONST);
if (ret)
        return ret;

根设备完成 bind 后,还需要进一步 probe

bindprobe 的区别可以简单理解为:

  • bind:把设备对象建立起来,并接入 driver model 的各种链表关系
  • probe:真正让这个设备进入"已激活、可工作"状态

对 root device 来说,它不是一个真实硬件外设,因此这里的 probe 重点不在硬件初始化,而在于:

  • 完整走通 DM 的设备激活流程
  • 让后续设备可以安全地挂接到这个根设备之下
  • 为后面的 dm_scan_platdata()dm_extended_scan_fdt() 打好基础

可以理解为:

到这一步,DM 的"根骨架"才算真正准备完成。

2.3第二部分dm_scan_platdata

先贴代码,依然位于文件drivers/core/root.c:

C 复制代码
int dm_scan_platdata(bool pre_reloc_only)
{
	int ret;

	ret = lists_bind_drivers(DM_ROOT_NON_CONST, pre_reloc_only);
	if (ret == -ENOENT) {
		dm_warn("Some drivers were not found\n");
		ret = 0;
	}

	return ret;
}

调用链干净,来看看lists_bind_drivers,注意pre_reloc_only = true,位于文件drivers/core/lists.c:

C 复制代码
int lists_bind_drivers(struct udevice *parent, bool pre_reloc_only)
{
	struct driver_info *info =
		ll_entry_start(struct driver_info, driver_info);
	const int n_ents = ll_entry_count(struct driver_info, driver_info);
	struct driver_info *entry;
	struct udevice *dev;
	int result = 0;
	int ret;

	for (entry = info; entry != info + n_ents; entry++) {
		ret = device_bind_by_name(parent, pre_reloc_only, entry, &dev);
		if (ret && ret != -EPERM) {
			dm_warn("No match for driver '%s'\n", entry->name);
			if (!result || ret != -ENOENT)
				result = ret;
		}
	}

	return result;
}

从整体逻辑上看,这个函数可以概括成一句话:

从链接器收集起来的 driver_info 表中,逐项取出设备描述,尝试把它们绑定到 parent 下面。

在当前场景下:

  • parent = DM_ROOT_NON_CONST
  • pre_reloc_only = true

所以这里实际做的是:

把所有编译期静态注册的顶层设备,尝试挂到 gd->dm_root 下面,但只处理允许在 pre-reloc 阶段出现的那部分设备。

2.4第三部分dm_extended_scan_fdt

C 复制代码
int dm_extended_scan_fdt(const void *blob, bool pre_reloc_only)
{
	int node, ret;

	ret = dm_scan_fdt(gd->fdt_blob, pre_reloc_only);
	if (ret) {
		debug("dm_scan_fdt() failed: %d\n", ret);
		return ret;
	}

	/* bind fixed-clock */
	node = ofnode_to_offset(ofnode_path("/clocks"));
	/* if no DT "clocks" node, no need to go further */
	if (node < 0)
		return ret;

	ret = dm_scan_fdt_node(gd->dm_root, gd->fdt_blob, node,
			       pre_reloc_only);
	if (ret)
		debug("dm_scan_fdt_node() failed: %d\n", ret);

	return ret;
}

从函数定义来看,dm_extended_scan_fdt() 可以分成两个部分:

  1. 调用 dm_scan_fdt(),扫描设备树 / 根节点下的顶层子节点(注意只是顶层子节点)
  2. 如果存在 /clocks 节点,再调用 dm_scan_fdt_node(),扫描 /clocks 下面的子节点

因此这一阶段是在前面 dm_scan_platdata() 处理完静态platdata之后,继续把dtb 中描述的设备节点 转换成udevice并接入DM。U-Boot文档对dm_init_and_scan()的总描述也是:初始化根结构后,再从 platform data 和 FDT 中扫描并绑定设备。

2.5第四部分dm_scan_other

C 复制代码
__weak int dm_scan_other(bool pre_reloc_only)
{
	return 0;
}

当前面 dm_scan_platdata()dm_extended_scan_fdt() 都执行完之后,

如果平台还想手工补充一些设备,就在 dm_scan_other() 里自己完成。

2.6 Proper阶段下的DM初始化流程

依然先看board_f阶段,直接看调用链

C 复制代码
void board_init_f(ulong boot_flags)
    initcall_run_list(init_sequence_f)
        initf_dm()

来看initf_dm()

C 复制代码
static int initf_dm(void)
{
#if defined(CONFIG_DM) && CONFIG_VAL(SYS_MALLOC_F_LEN)
	int ret;

	bootstage_start(BOOTSTATE_ID_ACCUM_DM_F, "dm_f");
	ret = dm_init_and_scan(true);
	bootstage_accum(BOOTSTATE_ID_ACCUM_DM_F);
	if (ret)
		return ret;
#endif
#ifdef CONFIG_TIMER_EARLY
	ret = dm_timer_init();
	if (ret)
		return ret;
#endif

	return 0;
}

发现又是调用的dm_init_and_scan,且pre_reloc_only = true。内部逻辑与SPL一致。

不同之处在于board_r阶段,老样子,先看调用链:

C 复制代码
void board_init_r(gd_t *new_gd, ulong dest_addr)
    initcall_run_list(init_sequence_r)
        static int initr_dm(void)

看到函数原型的瞬间就明白是怎么回事了:

C 复制代码
#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;
	gd->dm_root = NULL;
#ifdef CONFIG_TIMER
	gd->timer = NULL;
#endif
	bootstage_start(BOOTSTATE_ID_ACCUM_DM_R, "dm_r");
	ret = dm_init_and_scan(false);
	bootstage_accum(BOOTSTATE_ID_ACCUM_DM_R);
	if (ret)
		return ret;
#ifdef CONFIG_TIMER_EARLY
	ret = dm_timer_init();
	if (ret)
		return ret;
#endif

	return 0;
}
#endif

这里做了两件事:

1)先保存旧的 pre-reloc DM
C 复制代码
gd->dm_root_f = gd->dm_root;
gd->dm_root = NULL;

这里把 board_init_f() 阶段建立的那套 pre-reloc root 保存到 gd->dm_root_f,然后把当前 gd->dm_root 清空,准备重新建树。源码注释也明确说明了这一点。

2)再调用 dm_init_and_scan(false)
C 复制代码
ret = dm_init_and_scan(false);

这里参数变成了 false,含义和前面相反:

这一次不再只限于 pre-reloc 设备 ,而是重新扫描、重新绑定完整设备集合。dm_init_and_scan() 的官方 API 说明也是:参数为 false 时,绑定所有驱动。

如果把 initf_dm()initr_dm() 串起来,U-Boot proper 阶段下的 DM 初始化主线可以概括为:

C 复制代码
board_init_f()
    -> initf_dm()
        -> dm_init_and_scan(true)
        -> dm_autoprobe()

board_init_r()
    -> initr_dm()
        -> 保存旧的 gd->dm_root 到 gd->dm_root_f
        -> 清空 gd->dm_root
        -> dm_init_and_scan(false)

也就是说:

  • initf_dm():先解析、建立 pre-reloc DM
  • initr_dm():进入 relocation 后,重新解析并重建完整 DM

2.7总结

dm_init_and_scan() 本身来看,其内部四个阶段可以概括为:

C 复制代码
dm_init()
    -> 搭骨架,初始化uclass根链表,创建root device,并触发root device的probe

dm_scan_platdata()
    -> 接入编译期静态注册的platdata设备

dm_extended_scan_fdt()
    -> 接入设备树中的顶层节点,以及/clocks下的子节点

dm_scan_other()
    -> 留给board/平台代码补充特殊设备

2.7.1 从 SPL 阶段看

SPL 阶段调用的是:

C 复制代码
dm_init_and_scan(true)

其核心特点是:

  • 只处理 pre-reloc 阶段允许出现的设备
  • dm_init() 负责先建立 DM 的根结构
  • dm_scan_platdata()dm_extended_scan_fdt() 再把静态 platdata 和设备树中的可用设备逐步 bind 到 DM 中
  • dm_scan_other() 则为平台特殊设备保留扩展入口

因此,SPL 阶段的 DM 初始化本质上是在建立一套"早期最小可用 DM"

先建立 Driver Model 的根结构,再把不同来源(platdata / FDT / board 特殊补充)的早期可用设备逐步 bind 到统一的 DM 框架中。

需要注意的是,在 dm_init_and_scan() 扫描结束后:

  • root device 已经完成 probe
  • 其余通过 platdata/FDT 扫描出来的普通设备,通常只是完成了 bind
  • 它们已经建立了和 driveruclassparent 的关系,但通常还没有真正触发 probe

也就是说,SPL 阶段更偏向于:

先把设备对象和组织关系建起来,而不是立即把所有设备都激活。


2.7.2 从 U-Boot proper 阶段看

U-Boot proper 阶段下,DM 初始化实际上分成两段:

C 复制代码
initf_dm()
    -> dm_init_and_scan(true)
    -> dm_autoprobe()

initr_dm()
    -> 保存旧的pre-reloc DM到gd->dm_root_f
    -> 清空gd->dm_root
    -> dm_init_and_scan(false)

因此,Proper 阶段并不是只做一次 DM 初始化,而是:

第一段:initf_dm()
  • board_init_f() 阶段执行
  • 调用 dm_init_and_scan(true)
  • 先建立一套 pre-reloc DM
  • 随后通过 dm_autoprobe(),对部分带有 DM_FLAG_PROBE_AFTER_BIND 的设备自动触发 probe
第二段:initr_dm()
  • 在 relocation 完成后的 board_init_r() 阶段执行
  • 先把旧的 pre-reloc DM 保存到 gd->dm_root_f
  • 然后重新调用:
C 复制代码
dm_init_and_scan(false)
  • 重新扫描并建立一套 post-reloc / 完整 DM

因此,Proper 阶段的 DM 初始化本质上不是"沿用旧树继续补",而是"先建早期版,再重建正式版"

2.7.3 整体结论

综合来看,可以把 DM 初始化流程归纳为下面两句话:

SPL 阶段建立的是一套"早期最小可用 DM",用于满足启动前期最基本的设备访问需求。
U-Boot proper 阶段则会先建立一套 pre-reloc DM,用于支撑 relocation 之前的初始化;待 relocation 完成后,再重新扫描并建立一套完整的 post-reloc DM。

如果再从数据结构角度压缩成一句话,就是:

DM 初始化的本质,是先建立 uclassroot device 这两层根结构,再把来自 platdata、FDT 以及 board 特殊补充逻辑的设备,逐步 bind 进统一的 Driver Model 框架中;其中 root device 会在初始化阶段立即 probe,而普通设备通常先 bind、后按需 probe。

3.运行时的按需probe过程

前面分析的 dm_init_and_scan(),主要解决的是把设备 bind 进 DM 框架 的问题。

而在真正运行过程中,很多设备并不会在扫描阶段立刻 probe,而是在第一次被实际访问时 才触发 probe。U-Boot 官方设计文档明确把这种机制称为 lazy probe:为了节省启动时间和内存,很多设备只有在真正需要时才被激活。

3.1 为什么是按需probe

U-Boot 不是操作系统,而是启动加载器。一次启动过程中,很多设备可能根本不会被真正使用。

如果在初始化阶段把所有设备都统一 probe,会带来额外的时间开销、内存占用,甚至会引入不必要的依赖链。因此 U-Boot 的 Driver Model 采用的是"先 bind、后按需 probe"的策略:先把设备对象和组织关系建好,等真正访问该设备时,再通过 device_probe() 把它激活。

所以可以把 bindprobe 理解成两步:

C 复制代码
bind
    -> 先把 udevice 建出来
    -> 建立 device / driver / uclass / parent 关系
    -> 挂入各种链表

probe
    -> 真正激活设备
    -> 调用驱动的 probe()
    -> 让设备进入可用状态

3.2 运行时按需probe的常见触发入口

运行时最常见的触发方式,不是直接手工调用 device_probe(),而是通过各种 "获取设备" 的 DM 接口。

U-Boot 官方 API 文档对很多这类接口都写得非常明确:返回设备之前,会先 probe,使其 ready for use。例如:

  • uclass_get_device()
  • uclass_get_device_by_seq()
  • uclass_get_device_by_name()
  • uclass_get_device_by_ofnode()
  • uclass_get_device_by_of_path()
  • uclass_get_device_by_phandle()
  • uclass_get_device_by_driver()
    等接口,文档都明确说明"The device is probed to activate it ready for use"。

因此,运行时典型的逻辑其实是:

C 复制代码
业务代码想用某个设备
    -> 调用 uclass_get_device_xxx(...)
    -> DM 先确保该设备已 probe
    -> 然后把"可直接使用"的 udevice 返回给调用方

这也是为什么很多上层代码看起来只是"拿一个设备",但底层实际上已经隐含触发了按需 probe


3.3 运行时按需probe的几种典型场景

3.3.1 按序号获取设备时触发probe

例如某些 uclass 代码会直接通过序号获取设备:

C 复制代码
uclass_get_device_by_seq(UCLASS_ETH, 0, &dev);

这类接口的语义不是"只查一下指针",而是"返回一个已经可用的设备 "。

官方文档对 uclass_get_device_by_seq() 的说明就是:找到设备后,会先 probe,再返回。

所以这类路径属于典型的:

lookup device -> if needed, probe device -> return active device


3.3.2 设备已找到,但尚未激活时显式调用 device_probe()

除了 uclass_get_device_xxx() 这类"获取即 probe"的接口之外,还有很多子系统会先找到设备对象,再判断它是否已经激活;如果没有激活,就手工触发 device_probe()

例如 SPI 子系统中,就有这样的逻辑:

C 复制代码
if (!device_active(dev)) {
    ret = device_probe(dev);
    if (ret)
        goto err;
}

这段代码在 drivers/spi/spi-uclass.c 中实际存在,说明 SPI 设备在运行时第一次真正要使用时,会先检查 device_active(),若还未激活,则立即 probe

以太网子系统里也有同样模式。eth_set_dev() 在设置当前网卡时,如果传入的设备尚未激活,就会尝试 device_probe(dev);失败则把设备置空。

MMC/BLK 这一侧也类似。mmc_get_blk() 先找 MMC 设备下面的 block 子设备,再显式调用 device_probe(blk),成功后才把块设备返回出去。

这说明运行时按需 probe 的一个非常典型模式就是:

C 复制代码
先找到 udevice
    -> 如果已经 active,直接用
    -> 如果还没 active,调用 device_probe()

3.4 最终都会汇聚到 device_probe()

不管是通过 uclass_get_device_xxx() 间接触发,还是上层代码手工判断 device_active() 后再显式调用,运行时真正完成"设备激活"的核心入口,最后都会落到:

C 复制代码
device_probe(dev)

U-Boot 的 Driver Model 设计文档对 device_probe() 的核心步骤总结得很清楚。按主线来看,激活一个设备大致要经历下面几步:

C 复制代码
1. 先确保所有父设备都已经probe
2. 调用该设备驱动自己的 probe()
3. 将该设备标记为 activated / active
4. 调用 uclass 的 post_probe()(如果存在)

文档还特别强调:一个设备不可能在它的所有父设备尚未激活时单独被激活。比如一个 I2C 从设备要工作,前提一定是它所在的 I2C 控制器/总线设备已经先被 probe。

所以,运行时"按需 probe"并不是只激活某一个孤立节点,而往往会沿着父链一路向上,把依赖路径先补齐。

3.5 从调用者视角看,按需probe到底发生了什么

如果从上层业务代码的角度看,运行时按需 probe 的典型过程可以概括成:

C 复制代码
业务代码要访问某个设备
    -> 先通过 uclass / parent-child 关系找到对应 udevice
    -> 如果设备尚未 active,则调用 device_probe()
    -> device_probe() 先递归保证父设备 active
    -> 再执行当前设备驱动的 probe()
    -> 成功后返回一个已经 ready for use 的设备

因此,运行时的"第一次访问"往往就是"第一次真正激活"的时刻。

这和初始化阶段的"先 bind 进框架"正好形成分工:

  • 初始化阶段解决"设备对象是否存在"
  • 运行时按需 probe 解决"设备是否真正可用"

3.6 与初始化阶段的关系

这一点和你前面第二章的结论正好能接起来:

  • dm_init_and_scan() 阶段,重点是把设备从 platdata / FDT / board hook 中 bind 进 DM
  • 普通设备在这一阶段通常并不会统一立刻 probe
  • 到了运行时,谁先被真正访问,谁就先触发按需 probe

当然也有例外。

例如 U-Boot 设计文档提到,某些设备如果在 bind 后就被标记了 DM_FLAG_PROBE_AFTER_BIND,那么它们会在 dm_autoprobe() 阶段被自动 probe,而不是等运行时第一次访问才 probe。这个机制主要用于那些"平台一开始就必须工作"的关键设备。

所以更完整地说:

普通设备默认走"运行时按需 probe";只有少数被显式标记为必须提前激活的设备,才会走 autoprobe。

3.7 小结

运行时按需 probe 的核心可以概括为一句话:

初始化阶段先 bind,真正使用阶段再 probe。

从 Driver Model 的运行机制看,这一过程通常表现为:

C 复制代码
获取设备
    -> 检查是否已active
    -> 若未active,则调用device_probe()
    -> 先保证父设备已probe
    -> 再执行当前设备驱动的probe()
    -> 返回可直接使用的设备

因此,DM 在运行时真正起作用的关键不只是"设备已经被扫描进来",更在于:

当上层第一次真正访问某个设备时,Driver Model 会沿着 device / parent / uclass 关系,把这条依赖链上需要的设备逐步激活。

相关推荐
想唱rap2 小时前
计算机网络基础
linux·计算机网络·mysql·ubuntu·bash
fetasty2 小时前
chroot的Linux服务配置-当云服务器真正用起来
android·linux·服务器
BullSmall2 小时前
linux 系统下模拟网络丢失和ping延迟,可以怎么来模拟
linux·网络·php
brahmsjiang2 小时前
理解Android AOT编译与内存映射:从Zygote启动到页表权限隔离
android·linux·zygote
亚空间仓鼠2 小时前
OpenEuler系统常用服务(十)
linux·运维·服务器·网络
艾莉丝努力练剑2 小时前
【Linux线程】Linux系统多线程(四):线程ID及进程地址空间布局,线程封装
java·linux·运维·服务器·c语言·c++·学习
dddddppppp1232 小时前
linux head.s 从第一条指令到start_kernel
linux·运维·服务器
BioRunYiXue2 小时前
AlphaGenome:DeepMind 新作,基因组学迎来 Alpha 时刻
java·linux·运维·网络·数据库·人工智能·eclipse
十五年专注C++开发2 小时前
windows和linux使用system启动进程是一样的吗?
linux·c++·windows·system