U-Boot(2)——initialization of DM and device driver
0.briefly speaking
这篇文章仔细研究一下U-Boot在启动过程中对设备树的处理和对设备驱动模型的初始化过程。无论是在U-Boot SPL阶段还是U-Boot Proper阶段,都会涉及到不同的对设备树的解析和设备驱动的加载过程,这里我们以SPL阶段为例对这个过程进行展开和研究。
1.故事的开始——spl_common_init
首先,在这里简单提及一下在进入spl_common_init之前发生的故事作为一个引子,随后深入代码细节:
- 芯片上电复位之后,CPU会从某个确定的地址开始抓取指令。一般而言,上电之后的PC(Program Counter)寄存器一般指向芯片内部一块只读的非易失性存储区(Read-Only Memory, ROM),在对CPU和必要的外设进行初始化之后,从非易失存储器(如FLASH/SD卡)等加载下一阶段引导程序(U-Boot SPL)到片上的一小块RAM(On-Chip Memory)。(注意,这里的RAM并不是指DDR,它更小且更靠近CPU,仅需要片上总线即可实现单周期访问)。最后,跳入下一阶段引导程序U-Boot SPL。
- 进入U-Boot SPL初期,首先会根据芯片指令集架构的不同执行一段初始化汇编代码(start.S)。在这段汇编代码的前半段,为CPU设置了程序执行所需要的栈、用彩票算法选中初始化主核心、为全局数据结构体(global data)分配了空间,随后就会跳入第一段初始化程序(board_init_f),下面这个函数spl_common_init就是进入board_init_f之后执行的第一段初始化代码。
static int spl_common_init(bool setup_malloc)
{
// ... 以上的初始化代码从略,它们和本文的关系不大 ...
/*-------------------以下是对设备树的处理和设备驱动程序的加载-------------------- */
// 1.fdtdec_setup用来确定可用的设备树文件来源
if (CONFIG_IS_ENABLED(OF_REAL)) {
ret = fdtdec_setup();
if (ret) {
debug("fdtdec_setup() returned error %d\n", ret);
return ret;
}
}
// 2.DM的初始化和设备驱动的绑定(bind)和探测(probe)
if (CONFIG_IS_ENABLED(DM)) {
bootstage_start(BOOTSTAGE_ID_ACCUM_DM_SPL,
spl_phase() == PHASE_TPL ? "dm tpl" : "dm_spl");
/* With CONFIG_SPL_OF_PLATDATA, bring in all devices */
ret = dm_init_and_scan(!CONFIG_IS_ENABLED(OF_PLATDATA));
bootstage_accum(BOOTSTAGE_ID_ACCUM_DM_SPL);
if (ret) {
debug("dm_init_and_scan() returned error %d\n", ret);
return ret;
}
}
return 0;
}
接下来,以上述两个函数为出发点,展开两个话题:
- 我们从哪里获得设备树?
- 在U-Boot中如何初始化和加载设备驱动模型(DM Model)?
2.fdtdec_setup——确定设备树的来源
总的来说,此函数尝试从不同的地方获取设备树,并对设备树的合法性进行检查,以及作出必要的调整。请注意此函数从不同的源地址获取设备树,请注意排序越靠后,优先级反而越高。结合此顺序,越往后的源地址定制化程度也越高。
/----------------------------------------------------------------------------------------------------------/
// 确定设备树的不同来源
fdtdec_setup
// 1.从独立的内存区域获取设备树
fdt_find_separate
// 2.从嵌入Image的内存区域获取设备树
dtb_dt_embedded
// 3.从板级自定义的地址获取设备树
gd->fdt_blob = board_fdt_blob_setup(&ret);
// 4.U-Boot Proper阶段可以从环境变量中获取设备树
addr = env_get_hex(“fdtcontroladdr”, 0);
// 收尾工作:对设备树做板级调整
fdtdec_board_setup(gd->fdt_blob);
/----------------------------------------------------------------------------------------------------------/
int fdtdec_setup(void)
{
int ret = -ENOENT;
/* If allowing a bloblist, check that first */
// condition 1: 如果打开了bloblist特性,首先查看是否可以在bloblist里找到之前启动已经解析过的设备树数据
// 大部分芯片平台都没有打开这个功能,这里从略解释
// 请参见: https://docs.u-boot.org/en/stable/develop/bloblist.html以及bloblist.h文件顶部的注释
// 简而言之,bloblist是一个在多个启动阶段之间共享启动状态的机制
// 这里就是尝试在bloblist中取出存放的设备树
if (CONFIG_IS_ENABLED(BLOBLIST)) {
ret = bloblist_maybe_init();
if (!ret) {
gd->fdt_blob = bloblist_find(BLOBLISTT_CONTROL_FDT, 0);
if (gd->fdt_blob) {
gd->fdt_src = FDTSRC_BLOBLIST;
log_debug("Devicetree is in bloblist at %p\n",
gd->fdt_blob);
} else {
log_debug("No FDT found in bloblist\n");
ret = -ENOENT;
}
}
}
/* Otherwise, the devicetree is typically appended to U-Boot */
// condition 2: 如果设备树的数据放置在某个指定的位置上,将其取出
if (ret) {
// 2.1 CONFIG_OF_SEPARATE:设备树放置在SPL Image里的特定位置
if (IS_ENABLED(CONFIG_OF_SEPARATE)) {
gd->fdt_blob = fdt_find_separate();
gd->fdt_src = FDTSRC_SEPARATE;
// 2.2 否则设备树嵌入在SPL Image内部
} else { /* embed dtb in ELF file for testing / development */
gd->fdt_blob = dtb_dt_embedded();
gd->fdt_src = FDTSRC_EMBED;
}
}
/* Allow the board to override the fdt address. */
// condition 3: 如果板子重载了设备树的位置,则可以覆盖
// 这是一个板级可重载的函数,由芯片供应商自行实现
if (IS_ENABLED(CONFIG_OF_BOARD)) {
gd->fdt_blob = board_fdt_blob_setup(&ret);
if (!ret)
gd->fdt_src = FDTSRC_BOARD;
else if (ret != -EEXIST)
return ret;
}
/* Allow the early environment to override the fdt address */
// condition 4: 对于U-Boot Proper等启动阶段
// 也可以从环境变量fdtcontroladdr中获取设备树的数据
if (!IS_ENABLED(CONFIG_SPL_BUILD)) {
ulong addr;
addr = env_get_hex("fdtcontroladdr", 0);
if (addr) {
gd->fdt_blob = map_sysmem(addr, 0);
gd->fdt_src = FDTSRC_ENV;
}
}
// 如果开启了multi_dtb选项,这里还需要进一步定位板子最合适的设备树
if (CONFIG_IS_ENABLED(MULTI_DTB_FIT))
setup_multi_dtb_fit();
// 设备树合法性检查
ret = fdtdec_prepare_fdt(gd->fdt_blob);
// 板级自定义函数,允许开发板对设备树内容进行进一步的修改
if (!ret)
ret = fdtdec_board_setup(gd->fdt_blob);
oftree_reset();
return ret;
}
2.1 fdt_find_separate——从独立的内存区域获取设备树
这段代码的逻辑非常简单,它会根据配置项的不同选择从不同的内存区域直接获取设备树的相关数据。在设备树与SPL Image分开放置(seperate)的情况下,根据启动阶段的不同和SPL配置的不同,可进一步分为三种情况:
- A:SPL阶段,且BSS段与SPL Image分离放置(CONFIG_SPL_SEPARATE_BSS)
- B:SPL阶段,且BSS段与SPL Image合并放置
- C:U-Boot Proper阶段
首先列出这个函数及其注释,随后对三种情况做进一步说明。
static void *fdt_find_separate(void)
{
void *fdt_blob = NULL;
if (IS_ENABLED(CONFIG_SANDBOX))
return NULL;
// 在U-Boot SPL阶段,设备树依据配置项的不同,可以被放置到两个地方:
#ifdef CONFIG_SPL_BUILD
/* FDT is at end of BSS unless it is in a different memory region */
// 如果SPL将BSS段剥离为独立的一块内存,那么设备树在这种情况下紧跟放置在镜像数据之后
if (IS_ENABLED(CONFIG_SPL_SEPARATE_BSS))
fdt_blob = (ulong *)_image_binary_end;
// 否则设备树数据放置在BSS段的结尾
else
fdt_blob = (ulong *)__bss_end;
#else
/* FDT is at end of image */
// 如果是U-Boot Proper阶段,那么设备树放置在image末尾
fdt_blob = (ulong *)_end;
if (_DEBUG && !fdtdec_prepare_fdt(fdt_blob)) {
// DEBUG的相关逻辑,这里从略...
}
#endif
return fdt_blob;
}
A.SPL阶段,且BSS段与SPL Image分离放置
这种情况下需要打开CONFIG_SPL_SEPARATE_BSS配置,这个配置项的作用可以在common/spl/Kconfig文件中找到,如下所示:
config SPL_SEPARATE_BSS
bool "BSS section is in a different memory region from text"
help
Some platforms need a large BSS region in SPL and can provide this
because RAM is already set up. In this case BSS can be moved to RAM.
This option should then be enabled so that the correct device tree
location is used. Normally we put the device tree at the end of BSS
but with this option enabled, it goes at _image_binary_end.
简而言之,这个宏提供了将SPL Image和它的BSS段分开放置的可能。这出于一种要尽可能精简SPL Image大小的考量,因为BSS段存放的是全局变量和静态变量,在程序执行的开始会被初始化为全0,因此只要这个段大小不变,这部分数据放在哪里对于程序的执行是没有区别的。
因此如果片上RAM大小有限,或者某个特定芯片的Bootloader非常特殊,需要大量的全局数据或静态数据,那么可以开启此配置。此后,BSS段会被放置在一块与SPL Image完全独立的存储区域中,这个区域的地址由CONFIG_SPL_BSS_START_ADDR指定。从链接脚本u-boot-spl.lds可窥见一斑:
# 在链接脚本中,SPL image要占用的内存空间与BSS要占用的地址空间是区分开的
# 开发者可以自己指定CONFIG_SPL_BSS_START_ADDR,进而将BSS段分配到另外一块存储区域
MEMORY { .spl_mem : ORIGIN = IMAGE_TEXT_BASE, LENGTH = IMAGE_MAX_SIZE }
MEMORY { .bss_mem : ORIGIN = CONFIG_SPL_BSS_START_ADDR, \
LENGTH = CONFIG_SPL_BSS_MAX_SIZE }
在这种情况下,设备树(DTB文件)就紧紧地贴在SPL Image的后面(_image_binary_end),这就是上面fdt_find_separate函数中要从这个地址取出设备树的原因。
那么,设备树是何时被放置到SPL Image末尾的呢?答案位于Makefile.spl中,这段代码将SPL Image的生成逻辑写得非常清楚,当CONFIG_SPL_SEPARATE_BSS打开时,直接将生成的SPL Image和dtb拼接到一起就形成了最终的SPL Image。这样,在生成的SPL Image中
# Build the .dtb file if needed
# - OF_REAL is enabled
# - we have either OF_SEPARATE or OF_HOSTFILE
build_dtb :=
# 满足以下两个条件:
# 1.如果SPL阶段打开了OF_READ,也就是SPL阶段拥有自己的一份设备树
# 2.设备树单独存放
# 这种情况下才需要单独构建设备树
ifneq ($(CONFIG_$(SPL_TPL_)OF_REAL),)
ifneq ($(CONFIG_OF_SEPARATE)$(CONFIG_SANDBOX),)
build_dtb := y
endif
endif
# 如果需要构建dtb
ifneq ($(build_dtb),)
# 这种情况下,spl-dtb.bin由不包含设备树的spl image和设备树拼接而成
# 使用cat命令完成最简单的拼接动作
$(obj)/$(SPL_BIN)-dtb.bin: $(obj)/$(SPL_BIN)-nodtb.bin \
# [!]注意:如果没有打开SEPERATE_BSS,则在spl image和dtb之间还要加一个padding的文件
# 这个padding的文件,就是为BSS段留下指定大小的空间
$(if $(CONFIG_$(SPL_TPL_)SEPARATE_BSS),,$(obj)/$(SPL_BIN)-pad.bin) \
$(FINAL_DTB_CONTAINER) FORCE
$(call if_changed,cat)
# 最终生成的SPL Image就是上面包含了dtb的版本,只是在发生修改时再拷贝一份
$(obj)/$(SPL_BIN).bin: $(obj)/$(SPL_BIN)-dtb.bin FORCE
$(call if_changed,copy)
else
$(obj)/$(SPL_BIN).bin: $(obj)/$(SPL_BIN)-nodtb.bin FORCE
$(call if_changed,copy)
endif
# Create a file that pads from the end of u-boot-spl-nodtb.bin to bss_end
# 使用dd命令生成一个padding文件,大小是BSS段的大小,内容为全0
$(obj)/$(SPL_BIN)-pad.bin: $(obj)/$(SPL_BIN)
@bss_size_str=$(shell $(NM) $< | awk 'BEGIN {size = 0} /__bss_size/ {size = $$1} END {print "ibase=16; " toupper(size)}' | bc); \
dd if=/dev/zero of=$@ bs=1 count=$${bss_size_str} 2>/dev/null;
B. SPL阶段,且BSS段与SPL Image合并放置
这种情况下,在SPL Image之后会给BSS段保留一整块内存区域,在生成Image时会附加一整块值为全0的padding内容(请参见上面的Makefile),其大小等于BSS段的大小。这样,设备树就会被“挤到”BSS段的padding之后,因此即使在代码开始阶段对BSS进行清零动作,也不必担心会将设备树数据破坏,所以可以看到在fdt_find_separate函数中我们从__bss_end加载了设备树文件。
C.U-Boot Proper阶段
这种情况下,设备树非常确定地放置在U-Boot Proper镜像数据的结尾,这里之所以不需要像U-Boot SPL阶段那样分开存放BSS或是加上padding,是因为U-Boot Proper在运行时,容量更大的DDR一般已经初始化完成了,因此有充足的存储空间供U-Boot Proper执重定位动作(relocation)。重定位过程中会将设备树搬运到新的内存,并给BSS段留下了足够的空间,这样既保护了设备树,又给BSS段留下了内存空间。
2.2 dtb_dt_embedded——提取嵌入到image内的设备树
这种情况下,设备树直接被包含进SPL Image的镜像文件中,成为了SPL的一部分,而非像SEPERATE情况下那样“拼接”到SPL Image之后。此时要提取设备树,只需要将其在镜像中的地址直接返回即可。
此外,__dtb_dt_begin和__dtb_dt_spl_begin分别是U-Boot Proper阶段和U-Boot SPL阶段的设备树放置地址,这两个地址的声明是以汇编代码的形式指定的,具体细节在这里不再展开。可以参见U-Boot中的scripts/Makefile.lib文件中对cmd_dt_S_dtb命令的定义。
extern u8 __dtb_dt_begin[]; /* embedded device tree blob */
extern u8 __dtb_dt_spl_begin[]; /* embedded device tree blob for SPL/TPL */
static inline u8 *dtb_dt_embedded(void)
{
#ifdef CONFIG_OF_EMBED
# ifdef CONFIG_SPL_BUILD
return __dtb_dt_spl_begin;
# else
return __dtb_dt_begin;
# endif
#else
return NULL;
#endif
}
3.dm_init_and_scan
在深入讲解设备驱动相关的初始化和设备扫描过程之前,需要对U-Boot DM模型的组织结构做一些最基本的了解和介绍。这是一个正交的组织结构(树状拓扑+链表分类,每一个设备udevice同时在两种拓扑中占据位置),整体结构如下图所示,我们在这里讲述4个基本概念,和它们之间的组织关系:
- uclass
U-Boot中对uclass的定义是:a U-Boot drive class, collecting together similar drivers(一个U-Boot的驱动类,集合了相似的驱动)。事实上,uclass是对具备某种特定功能的一大类外设的整体抽象。某一个uclass类聚集了散落在树状拓扑中各个角落的,具有类似功能的全部外设,而所有的uclass串成了一个链表,链表的头保存在全局数据结构体的uclass_root字段。以下图为例,uclass_1串接起了udevice下面位于树状拓扑第一层的udevice1_1、udevice1_2、udevice1_3这三个设备,而uclass_2则只串起了位于第二层树的udevice2_1,uclass_3同理类推。 - uclass_driver
uclass_driver是对于uclass这种一整类具有类似功能的设备提供的统一访问接口,它以生命期的先后顺序(例如:post_bind、pre_unbind等)组织起一整套贯穿设备生命期始终的访问接口。一方面,它会集中做一些同类设备都会完成的统一动作。另外一方面,它也会经由传入的设备指针(udevice*)来调用具体设备驱动程序中的动作。这样一来,uclass统一的API就完成了动作路由,动作拆解到了每一个设备实例上,这也是多态性(polymorphism)的体现。
上述uclass、uclass_driver构成了正交拓扑的第一层:链表分类,也就是将系统中存在的所有设备全部按照类型进行划分。
-
udevice
udevice,顾名思义就是一个实际的设备,或者说一个真实的驱动程序(driver)的实例(instance)。一个设备在整个DM模型中既处于uclass分类设备的链表中,也处于树状结构中,这棵树的树根被记录在全局数据中的dm_root字段中。一个udevice结构体中还包含着指向其兄弟节点、父亲节点、所属类(uclass)节点的指针,以及一些驱动必需的私有数据。 -
driver
driver就是一个真实的设备驱动了,里面包含着实际操作寄存器的一系列动作,一个设备驱动可以同时例化出多个设备。也就是,这些设备共享一套驱动,如下图中的udevice1_1和udevice1_2共享udevice_driver_1这份驱动。除此之外,uclass提供的,基于生命期的高度抽象的统一驱动接口也需要调用具体的驱动程序(driver)来完成实际的动作。
上述udevice、driver构成了正交拓扑的第二层:树形拓扑,也就是将系统中存在的所有设备按照设备树的描述以及可插拔总线(USB,PCIe等)的扫描情况,组织成了一棵树。

现在,让我们看看具体的DM模型初始化过程。对具体函数的讲解都列在下面,每一个子标题都列出了函数所处的调用深度层级,对照下面的调用层次图可以更加方便地确定函数之间的调用关系和所处层次。
/----------------------------------------------------------------------------------------------------------/
// 驱动模型的初始化与设备的扫描
dm_init_and_scan
// 初始化uclass,绑定并初始化根设备root
dm_init
// 按照名称匹配驱动,并绑定根设备
device_bind_by_name
// [!]: 绑定设备的通用函数
device_bind_common
// [!]: 初始化设备的通用函数,这里用来初始化根设备
device_probe
// 扫描并绑定设备
dm_scan
// 扫描并绑定所有静态声明的设备
dm_scan_plat
// 多次重入来绑定所有静态声明的设备
lists_bind_drivers
// 一次扫描并绑定静态设备
bind_drivers_pass
// 绑定设备树中“根设备”的直接子设备
dm_extended_scan
// 绑定一个设备的直接子设备
dm_scan_fdt_node
// 用compatible属性匹配合适驱动并完成设备绑定
lists_bind_fdt
// 深度优先初始化所有外设
dm_probe_devices
/----------------------------------------------------------------------------------------------------------/
int dm_init_and_scan(bool pre_reloc_only)
{
int ret;
// 初始化uclass结构体,绑定并初始化根设备
ret = dm_init(CONFIG_IS_ENABLED(OF_LIVE));
if (ret) {
debug("dm_init() failed: %d\n", ret);
return ret;
}
// 扫描并绑定设备
if (!CONFIG_IS_ENABLED(OF_PLATDATA_INST)) {
ret = dm_scan(pre_reloc_only);
if (ret) {
log_debug("dm_scan() failed: %d\n", ret);
return ret;
}
}
// 如有必要,触发事件系统
if (CONFIG_IS_ENABLED(DM_EVENT)) {
ret = event_notify_null(gd->flags & GD_FLG_RELOC ?
EVT_DM_POST_INIT_R :
EVT_DM_POST_INIT_F);
if (ret)
return log_msg_ret("ev", ret);
}
return 0;
}
3.1 [1] dm_init——初始化uclass,绑定并初始化根设备root
这段代码负责整个DM的初始化,首先这段代码初始化了整个uclass的链表根部gd->uclass_root,此字段指向整个分类链表的根节点。此后,这个函数调用了两个函数,来分别绑定(bind)和初始化(probe)根设备。
绑定(bind)更多程度上是一个软件概念,它整体的过程是扫描设备树获取设备基本信息,并在软件世界中抽象出一个实例。而初始化(probe)的过程则涉及到对具体硬件寄存器的配置,在下面我们将详细讲解这两个函数。
int dm_init(bool of_live)
{
int ret;
// 如果dm_root指针不为空,说明根设备已经存在
// dm_root是整个U-Boot设备树形拓扑的根(详细见上)
if (gd->dm_root) {
dm_warn("Virtual root driver already exists!\n");
return -EINVAL;
}
// 初始化uclass的根节点,并将其保存到gd中
if (CONFIG_IS_ENABLED(OF_PLATDATA_INST)) {
gd->uclass_root = &uclass_head;
} else {
// 本质上这个地址指向gd中的一个字段
// #define DM_UCLASS_ROOT_S_NON_CONST (((gd_t *)gd)->uclass_root_s)
gd->uclass_root = &DM_UCLASS_ROOT_S_NON_CONST;
INIT_LIST_HEAD(DM_UCLASS_ROOT_NON_CONST);
}
// 如果开启了OF_PLATDATA_INST,就直接从内存的特定位置直接获取dm_root
if (CONFIG_IS_ENABLED(OF_PLATDATA_INST)) {
ret = dm_setup_inst();
if (ret) {
log_debug("dm_setup_inst() failed: %d\n", ret);
return ret;
}
// 一般情况:
} else {
// 绑定root设备
ret = device_bind_by_name(NULL, false, &root_info,
&DM_ROOT_NON_CONST);
if (ret)
return ret;
// 将设备树节点放置到设备结构体中
if (CONFIG_IS_ENABLED(OF_CONTROL))
dev_set_ofnode(DM_ROOT_NON_CONST, ofnode_root());
// 初始化根设备
ret = device_probe(DM_ROOT_NON_CONST);
if (ret)
return ret;
}
INIT_LIST_HEAD((struct list_head *)&gd->dmtag_list);
return 0;
}
3.2 [2] device_bind_by_name——按照名称匹配驱动,并绑定设备
此函数本质上是device_bind_common函数的更外层封装,首先此函数使用传入的driver_info信息在驱动链表中查找对应的驱动,随后绑定(bind)这个设备,绑定的具体动作细节包含在device_bind_common函数中,在此不再赘述。
int device_bind_by_name(struct udevice *parent, bool pre_reloc_only,
const struct driver_info *info, struct udevice **devp)
{
struct driver *drv;
uint plat_size = 0;
int ret;
// 在已经编译的驱动链表中,寻找指定名称的设备驱动
drv = lists_driver_lookup_name(info->name);
if (!drv)
return -ENOENT;
// 如果当前设备在重定位完成之前不需要绑定,则直接返回
// 在没有执行重定位之前,可用的RAM大小非常有限,此时有些非必要的外设驱动可以不加载
if (pre_reloc_only && !(drv->flags & DM_FLAG_PRE_RELOC))
return -EPERM;
#if CONFIG_IS_ENABLED(OF_PLATDATA)
plat_size = info->plat_size;
#endif
// [!]:绑定(bind)一个新的设备
// 创建udevice结构体,分配数据空间,并将其组织到上述的正交结构(分类链表+树形结构)中
ret = device_bind_common( parent, // struct udevice *parent
drv, // const struct driver *drv
info->name, // const char *name
(void *)info->plat, // void *plat
0, // ulong driver_data
ofnode_null(), // ofnode node
plat_size, // uint of_plat_size
devp); // struct udevice **devp
if (ret)
return ret;
return ret;
}
3.3 [3] device_bind_common——绑定设备的通用函数(※)
以下这个函数是驱动绑定一个设备的通用函数(其实,也可以说是从驱动例化出一个真实设备的过程),关于绑定的基本概念已经在上面做了讲述,以下是绑定设备的具体动作:
为探测到的设备分配一个udevice结构体,初始化其中的字段,为设备和驱动分配数据空间(也包括父设备的一些空间,它们存放在子设备中),并将设备插入我们上述提到的双重正交拓扑结构中。
下面的代码也基本上是按照这样的思路来一一进行的,详见下面的注释:
static int device_bind_common(struct udevice *parent,
const struct driver *drv,
const char *name, void *plat,
ulong driver_data, ofnode node,
uint of_plat_size, struct udevice **devp)
{
struct udevice *dev;
struct uclass *uc;
int size, ret = 0;
bool auto_seq = true;
void *ptr;
if (CONFIG_IS_ENABLED(OF_PLATDATA_NO_BIND))
return -ENOSYS;
// 清空指针
if (devp)
*devp = NULL;
if (!name)
return -EINVAL;
// 循着驱动的ID,寻找驱动所属的uclass类
ret = uclass_get(drv->id, &uc);
if (ret) {
debug("Missing uclass for driver %s\n", drv->name);
return ret;
}
// 为设备分配一个udevice结构体
dev = calloc(1, sizeof(struct udevice));
if (!dev)
return -ENOMEM;
// 初始化udevice的三个指针:兄弟链表指针、孩子节点指针
INIT_LIST_HEAD(&dev->sibling_node);
INIT_LIST_HEAD(&dev->child_head);
INIT_LIST_HEAD(&dev->uclass_node);
#if CONFIG_IS_ENABLED(DEVRES)
INIT_LIST_HEAD(&dev->devres_head);
#endif
// 填充一些必要的信息
dev_set_plat(dev, plat);
dev->driver_data = driver_data;
dev->name = name;
dev_set_ofnode(dev, node);
dev->parent = parent;
dev->driver = drv;
dev->uclass = uc;
// 决定设备的顺序编号
// 设备的顺序编号初始化为-1
dev->seq_ = -1;
// 是否可以从设备树中读取顺序号别名
if (CONFIG_IS_ENABLED(DM_SEQ_ALIAS) &&
(uc->uc_drv->flags & DM_UC_FLAG_SEQ_ALIAS)) {
/*
* Some devices, such as a SPI bus, I2C bus and serial ports
* are numbered using aliases.
*/
// 从设备树中的alias节点中寻找对应设备的编号
// aliases {
// /* 将标有 uart2 标签的节点,识别为串口组中的第 1 号 */
// serial1 = &uart2;
// };
if (CONFIG_IS_ENABLED(OF_CONTROL) &&
!CONFIG_IS_ENABLED(OF_PLATDATA)) {
if (uc->uc_drv->name && ofnode_valid(node)) {
if (!dev_read_alias_seq(dev, &dev->seq_)) {
auto_seq = false;
log_debug(" - seq=%d\n", dev->seq_);
}
}
}
}
// 如果设备树中没有找到别名,则从当前uclass中分配一个顺序编号
if (auto_seq && !(uc->uc_drv->flags & DM_UC_FLAG_NO_AUTO_SEQ))
dev->seq_ = uclass_find_next_free_seq(uc);
/* 1.首先,满足自身的一些空间分配需求 */
/* Check if we need to allocate plat */
// plat_auto中记录着需要自动分配的平台数据空间大小
if (drv->plat_auto) {
// 如果传入的driver_info中的平台数据结构是空指针
// 那么说明需要分配空间
bool alloc = !plat;
/*
* For of-platdata, we try use the existing data, but if
* plat_auto is larger, we must allocate a new space
*/
// 对于OF_PLATDATA的情况,需要考虑一种特殊情况
// 也就是当设备树中的静态数据大小,小于驱动中声明的自动分配的大小时
// 需要重新分配存储空间
if (CONFIG_IS_ENABLED(OF_PLATDATA)) {
if (of_plat_size)
dev_or_flags(dev, DM_FLAG_OF_PLATDATA);
if (of_plat_size < drv->plat_auto)
alloc = true;
}
// 如果需要分配新的空间
if (alloc) {
// 使用calloc分配一块数据空间,并设置标志DM_FLAG_ALLOC_PDATA
// 设置完DM_FLAG_ALLOC_PDATA标志之后,在释放驱动时会自动调用free
dev_or_flags(dev, DM_FLAG_ALLOC_PDATA);
ptr = calloc(1, drv->plat_auto);
if (!ptr) {
ret = -ENOMEM;
goto fail_alloc1;
}
/*
* For of-platdata, copy the old plat into the new
* space
*/
// 对于OF_PLATDATA情况下,原有的那部分数据不能丢
// 既然分配了一个更大的空间,那就拷贝过去
if (CONFIG_IS_ENABLED(OF_PLATDATA) && plat)
memcpy(ptr, plat, of_plat_size);
// 将新分配出来的空间放置在dev->plat_字段中
dev_set_plat(dev, ptr);
}
}
// 如果所属类的驱动(uclass_driver)的每一个设备都有一块要自动分配的空间
size = uc->uc_drv->per_device_plat_auto;
// 那么就把这块空间分配出来,和上面分配plat_auto空间差不多
if (size) {
dev_or_flags(dev, DM_FLAG_ALLOC_UCLASS_PDATA);
ptr = calloc(1, size);
if (!ptr) {
ret = -ENOMEM;
goto fail_alloc2;
}
// 将新分配出来的空间放置在dev->uclass_plat_字段中
dev_set_uclass_plat(dev, ptr);
}
/* 2.接下来,满足父亲设备的一些空间分配需求 */
// 除了虚拟的root设备,其他设备都有父亲节点
// 有些字段记录在子设备中,但却被父设备访问和使用
if (parent) {
// 首先看父设备实例本身(instance)是否有要记录子节点信息的需求
size = parent->driver->per_child_plat_auto;
// 如果父设备没有,则向上fallback到父设备所在的uclass的需求
if (!size)
size = parent->uclass->uc_drv->per_child_plat_auto;
// 分配空间
if (size) {
dev_or_flags(dev, DM_FLAG_ALLOC_PARENT_PDATA);
ptr = calloc(1, size);
if (!ptr) {
ret = -ENOMEM;
goto fail_alloc3;
}
// 将新分配出来的空间放置在dev->parent_plat_字段中
dev_set_parent_plat(dev, ptr);
}
/* put dev into parent's successor list */
// 将当前设备的sibling node插入到父亲设备的child_head中
// [!]:自此此设备进入了树形拓扑的管理范围
list_add_tail(&dev->sibling_node, &parent->child_head);
}
// 这一步就是在分类链表中插入设备节点
// [!]:自此此设备进入了链表拓扑的管理范围
ret = uclass_bind_device(dev);
if (ret)
goto fail_uclass_bind;
/* if we fail to bind we remove device from successors and free it */
// 3.三个级别的bind钩子函数,在完成bind动作时依次调用,从前往后依次完成
// A.设备驱动程序本身的bind操作
if (drv->bind) {
ret = drv->bind(dev);
if (ret)
goto fail_bind;
}
// B.父设备的child_post_bind动作
if (parent && parent->driver->child_post_bind) {
ret = parent->driver->child_post_bind(dev);
if (ret)
goto fail_child_post_bind;
}
// C.uclass_driver级别的post_bind动作
if (uc->uc_drv->post_bind) {
ret = uc->uc_drv->post_bind(dev);
if (ret)
goto fail_uclass_post_bind;
}
if (parent)
pr_debug("Bound device %s to %s\n", dev->name, parent->name);
// 将设备指针传出
if (devp)
*devp = dev;
// 设备已经bind完成的标志,但是还没有初始化(probe)
dev_or_flags(dev, DM_FLAG_BOUND);
return 0;
/* ...失败处理函数从略... */
fail_uclass_post_bind:
fail_child_post_bind:
fail_bind:
fail_uclass_bind:
fail_alloc3:
fail_alloc2:
fail_alloc1:
}
3.4 [2] device_probe——初始化设备的通用函数(※)
上面我们讲过,对DM模型的初始化动作分为两步:bind和probe,bind过程在上面已经讲过,它更多是软件层面的一些概念,例如分配结构体和初始化其中的一些字段等。而probe的过程是真实存在的对硬件进行访问和初始化的过程。以下这个函数就是U-Boot中对设备进行初始化的主函数,它从上到下完成了如下任务:
- 向上递归解析设备树中的信息到dev->plat_字段中(这里的递归解析指的是,任何一个设备都会从自身开始向上,查看父设备是否也已经完成同样的动作,一直回溯到整个树形拓扑的根)
- 尝试向上递归初始化父设备(device_probe(dev->parent))
- 如果必要,初始化设备的PINCTRL,IOMMU和CLK配置,计算总线地址空间的翻译
- 执行一系列probe的钩子函数(自身驱动的probe函数也在这里被调用)
int device_probe(struct udevice *dev)
{
const struct driver *drv;
int ret;
if (!dev)
return -EINVAL;
// 如果设备已经激活,则不必执行下面的函数
if (dev_get_flags(dev) & DM_FLAG_ACTIVATED)
return 0;
// 如果存在,唤醒pre_probe的监听钩子函数
// 执行一些必要的准备工作
ret = device_notify(dev, EVT_DM_PRE_PROBE);
if (ret)
return ret;
drv = dev->driver;
assert(drv);
// 解析设备树中的信息到dev->plat_字段
// 这个函数本身也是一个递归函数,它会首先调用父设备的同名函数
ret = device_of_to_plat(dev);
if (ret)
goto fail;
/* Ensure all parents are probed */
// 如果当前设备存在父设备,尝试首先probe父设备
// 注意:这里递归调用了device_probe函数本身
if (dev->parent) {
ret = device_probe(dev->parent);
if (ret)
goto fail;
/*
* The device might have already been probed during
* the call to device_probe() on its parent device
* (e.g. PCI bridge devices). Test the flags again
* so that we don't mess up the device.
*/
// 当前设备有可能在父设备probe时就已经被顺带probe了
// 这种情况无需执行下面的probe逻辑
if (dev_get_flags(dev) & DM_FLAG_ACTIVATED)
return 0;
}
// 挂上设备已经被激活的标志
dev_or_flags(dev, DM_FLAG_ACTIVATED);
// 找到设备的电源域,并打开设备的供电
if (CONFIG_IS_ENABLED(POWER_DOMAIN) && dev->parent &&
(device_get_uclass_id(dev) != UCLASS_POWER_DOMAIN) &&
!(drv->flags & DM_FLAG_DEFAULT_PD_CTRL_OFF)) {
ret = dev_power_domain_on(dev);
if (ret)
goto fail;
}
/*
* Process pinctrl for everything except the root device, and
* continue regardless of the result of pinctrl. Don't process pinctrl
* settings for pinctrl devices since the device may not yet be
* probed.
*
* This call can produce some non-intuitive results. For example, on an
* x86 device where dev is the main PCI bus, the pinctrl device may be
* child or grandchild of that bus, meaning that the child will be
* probed here. If the child happens to be the P2SB and the pinctrl
* device is a child of that, then both the pinctrl and P2SB will be
* probed by this call. This works because the DM_FLAG_ACTIVATED flag
* is set just above. However, the PCI bus' probe() method and
* associated uclass methods have not yet been called.
*/
// 将设备的引脚(pinctrl)切换到default状态指定的电气水平
if (dev->parent && device_get_uclass_id(dev) != UCLASS_PINCTRL) {
ret = pinctrl_select_state(dev, "default");
if (ret && ret != -ENOSYS)
log_debug("Device '%s' failed to configure default pinctrl: %d (%s)\n",
dev->name, ret, errno_str(ret));
}
// 如果支持IOMMU,则使能它
if (CONFIG_IS_ENABLED(IOMMU) && dev->parent &&
(device_get_uclass_id(dev) != UCLASS_IOMMU)) {
ret = dev_iommu_enable(dev);
if (ret)
goto fail;
}
// 获取并填充当前设备
// 如果父总线存在dma-ranges的话,则解析CPU地址空间到总线地址空间的换算关系
// 得到这个地址换算关系之后,CPU可以便捷地访问总线地址空间
ret = device_get_dma_constraints(dev);
if (ret)
goto fail;
/* 一系列的钩子函数 */
// 1.uclass级别的pre_probe动作
ret = uclass_pre_probe_device(dev);
if (ret)
goto fail;
// 2.父设备的child_pre_probe函数
if (dev->parent && dev->parent->driver->child_pre_probe) {
ret = dev->parent->driver->child_pre_probe(dev);
if (ret)
goto fail;
}
/* Only handle devices that have a valid ofnode */
// 配置设备的时钟并配置到默认频率
// 这对于很多设备是能正常工作的前提
if (dev_has_ofnode(dev)) {
/*
* Process 'assigned-{clocks/clock-parents/clock-rates}'
* properties
*/
ret = clk_set_defaults(dev, CLK_DEFAULTS_PRE);
if (ret)
goto fail;
}
// 3.[!]注意这里调用驱动程序的probe函数
if (drv->probe) {
ret = drv->probe(dev);
if (ret)
goto fail;
}
// 4.uclass级别的post_probe动作
ret = uclass_post_probe_device(dev);
if (ret)
goto fail_uclass;
// 对于PINCTRL设备的额外配置,将其配置为default状态
if (dev->parent && device_get_uclass_id(dev) == UCLASS_PINCTRL) {
ret = pinctrl_select_state(dev, "default");
if (ret && ret != -ENOSYS)
log_debug("Device '%s' failed to configure default pinctrl: %d (%s)\n",
dev->name, ret, errno_str(ret));
}
// 如果存在,唤醒post_probe的监听钩子函数
ret = device_notify(dev, EVT_DM_POST_PROBE);
if (ret)
goto fail_event;
return 0;
// 错误处理从略
fail_event:
fail_uclass:
fail:
}
3.5 [1] dm_scan——扫描并绑定设备
这个函数完成的事情非常明晰,首先它对两种不同声明途径的设备进行了绑定:
- 静态声明的设备(U_BOOT_DRVINFO声明的)
- 设备树中声明的根节点的直接子设备
随后,从根节点开始对设备树进行一个完整的初始化,但请注意,并不是树中的所有设备都会在这里被立即初始化,大部分设备的初始化动作实际上发生在它们被引用的时候,详见后面对dm_probe_devices函数的详解。
static int dm_scan(bool pre_reloc_only)
{
int ret;
// 类型1:绑定静态声明的设备
ret = dm_scan_plat(pre_reloc_only);
if (ret) {
debug("dm_scan_plat() failed: %d\n", ret);
return ret;
}
// 类型2:存在设备树时,绑定设备树中根节点的直接子设备(递归深度为1)
if (CONFIG_IS_ENABLED(OF_REAL)) {
ret = dm_extended_scan(pre_reloc_only);
if (ret) {
debug("dm_extended_scan() failed: %d\n", ret);
return ret;
}
}
// 类型3:这是一个可由板级重载的弱函数
// 专门用来补充一些在静态声明(DRIVER_INFO)和设备树之外存在的,需要额外绑定的设备
ret = dm_scan_other(pre_reloc_only);
if (ret)
return ret;
// 从根节点向下,深度优先地完成子设备的初始化
return dm_probe_devices(gd->dm_root, pre_reloc_only);
}
3.6 [2] dm_scan_plat——扫描并绑定所有静态声明的设备
此函数首先分配了一段空间给dm_driver_rt,随后就开始执行对静态声明设备的扫描和绑定工作。
int dm_scan_plat(bool pre_reloc_only)
{
int ret;
// 为driver_rt分配空间,这块空间在开启OF_PLATDATA,但是不开启OF_PLATDATA_INST有用
// 详见后面的bind_drivers_pass函数,里面会调用driver_rt
// 它会被用来记录每一个driver_info绑定了哪一个设备udeivce
if (CONFIG_IS_ENABLED(OF_PLATDATA_DRIVER_RT)) {
struct driver_rt *dyn;
int n_ents;
n_ents = ll_entry_count(struct driver_info, driver_info);
dyn = calloc(n_ents, sizeof(struct driver_rt));
if (!dyn)
return -ENOMEM;
gd_set_dm_driver_rt(dyn);
}
// 此函数将会执行多遍动作来绑定所有静态声明的设备
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;
}
3.7 [3] lists_bind_drivers——多次重入来绑定所有静态声明的设备
此函数的本质就是多次重入bind_drivers_pass函数来多次探测静态声明的设备信息,这里的注释表明如果不是开启了OF_PLATDATA_PARENT选项,那么可以确定第一遍(first pass)扫描静态信息时就可以完成所有设备的绑定。
因为只有在开启OF_PLATDATA时,每一个driver_info才会记录自己的父设备是谁(可以理解为此时实际上是用数组静态存储了一个树状结构),此时存在着父设备还没有被绑定的可能,所以需要先绑定父设备,所以需要反复多次重入这个函数,直到父设备被绑定才可以进一步绑定子设备。
int lists_bind_drivers(struct udevice *parent, bool pre_reloc_only)
{
int result = 0;
int pass;
/*
* 10 passes is 10 levels deep in the devicetree, which is plenty. If
* OF_PLATDATA_PARENT is not enabled, then bind_drivers_pass() will
* always succeed on the first pass.
*/
// 10次是一个经验值,也就是假定设备树最大深度不会超过10
// 在不开启OF_PLATDATA_PARENT的情况下,所有传入的设备的父亲设备(parent)都是root
/*
考虑到driver_info的定义,只有在OF_PLATDATA打开时才会在这里记录父设备的parent_idx
struct driver_info {
const char *name;
const void *plat;
#if CONFIG_IS_ENABLED(OF_PLATDATA)
unsigned short plat_size;
short parent_idx;
#endif
};
*/
for (pass = 0; pass < 10; pass++) {
int ret;
ret = bind_drivers_pass(parent, pre_reloc_only);
if (!result || result == -EAGAIN)
result = ret;
if (ret != -EAGAIN)
break;
}
return result;
}
3.8 [4] bind_drivers_pass——一次扫描并绑定静态设备
这段代码是针对静态声明的driver_info进行绑定的函数,所谓静态声明指的是在代码中使用U_BOOT_DRVINFO宏声明的设备信息(也包括使用dtoc生成的)。这些设备驱动信息会被以数组形式存储在某一段指定的地址空间里,可以通过ll_entry_start宏直接指向这段地址的开头。
这段代码通过遍历这个数组,逐个为driver_info创建对应的设备结构体和device,并绑定设备驱动。稍显特殊的是OF_PLATDATA配置项打开的情况,这里存在当前设备的父设备还没有被绑定的可能,这种情况下需要置位missing_parent标志,并再次重入这个函数。通过多轮重入,最终完成先父后子的设备绑定过程。
static int bind_drivers_pass(struct udevice *parent, bool pre_reloc_only)
{
// 声明的driver_info的链表都放在这段连续内存数组中(linker list)
struct driver_info *info =
ll_entry_start(struct driver_info, driver_info);
const int n_ents = ll_entry_count(struct driver_info, driver_info);
bool missing_parent = false;
int result = 0;
int idx;
/*
* Do one iteration through the driver_info records. For of-platdata,
* bind only devices whose parent is already bound. If we find any
* device we can't bind, set missing_parent to true, which will cause
* this function to be called again.
*/
// 遍历整个driver_info数组
for (idx = 0; idx < n_ents; idx++) {
struct udevice *par = parent;
const struct driver_info *entry = info + idx;
// 找出对应的driver_rt结构体
// 这是对应的驱动程序运行时数据,里面存放着指向设备的指针
struct driver_rt *drt = gd_dm_driver_rt() + idx;
struct udevice *dev;
int ret;
// OF_PLATDATA配置项打开的情况
if (CONFIG_IS_ENABLED(OF_PLATDATA)) {
int parent_idx = driver_info_parent_id(entry);
// 如果driver_rt中已经记录了设备信息,说明已经被扫描过
if (drt->dev)
continue;
// 如果存在父设备
if (CONFIG_IS_ENABLED(OF_PLATDATA_PARENT) &&
parent_idx != -1) {
struct driver_rt *parent_drt;
// 但是,父设备还没有被扫描,则置位missing_parent标志
parent_drt = gd_dm_driver_rt() + parent_idx;
if (!parent_drt->dev) {
missing_parent = true;
continue;
}
// 否则取出父设备,覆盖入口参数
par = parent_drt->dev;
}
}
// [!]绑定这个设备
ret = device_bind_by_name(par, pre_reloc_only, entry, &dev);
// 绑定完成后,将设备设置进driver_rt结构体中
if (!ret) {
if (CONFIG_IS_ENABLED(OF_PLATDATA))
drt->dev = dev;
} else if (ret != -EPERM) {
dm_warn("No match for driver '%s'\n", entry->name);
if (!result || ret != -ENOENT)
result = ret;
}
}
return result ? result : missing_parent ? -EAGAIN : 0;
}
3.9 [2] dm_extended_scan——绑定设备树中根设备的直接子设备
此函数完成的任务是绑定所有在设备树中声明的根设备的直接子设备(也就是设备树的直接孩子节点,递归深度为1)。这个动作分为两步:
首先它扫描一遍设备树,根据所有直接子设备的compatible属性对设备驱动进行匹配。其次,它会对一些特殊节点(/chosen, /clocks, /firmware)进行进一步的探测, 将它们下面的直接子设备也当做根设备的直接子设备,使用compatible完成驱动绑定的动作。
int dm_extended_scan(bool pre_reloc_only)
{
int ret, i;
const char * const nodes[] = {
"/chosen",
"/clocks",
"/firmware"
};
// 这个函数就是dm_scan_fdt_node的封装
// 也即:dm_scan_fdt_node(gd->dm_root, ofnode_root(), pre_reloc_only);
ret = dm_scan_fdt(pre_reloc_only);
if (ret) {
debug("dm_scan_fdt() failed: %d\n", ret);
return ret;
}
/* Some nodes aren't devices themselves but may contain some */
// 一些节点本身不是设备,但它们内部可能包含一些设备节点
for (i = 0; i < ARRAY_SIZE(nodes); i++) {
// 这个函数本质也是通过节点名首先获得ofnode
// 随后再调用dm_scan_fdt_node(gd->dm_root, node, pre_reloc_only);
ret = dm_scan_fdt_ofnode_path(nodes[i], pre_reloc_only);
if (ret) {
debug("dm_scan_fdt() scan for %s failed: %d\n",
nodes[i], ret);
return ret;
}
}
return ret;
}
3.10 [3] dm_scan_fdt_node——绑定一个设备的直接子设备
此函数的核心就是对于当前节点(parent_node)而言,遍历它在设备树中的直接子节点(注意,不深入递归更深层的孙节点),面向更深层设备的扫描动作往往在所处uclass中post_bind钩子函数中实现,而绑定动作的细节集中在lists_bind_fdt函数中,这个函数会按照设备树中的compatible属性来为每一个设备匹配对应的驱动程序。
static int dm_scan_fdt_node(struct udevice *parent, ofnode parent_node,
bool pre_reloc_only)
{
int ret = 0, err = 0;
ofnode node;
// 如果父设备节点为空,直接返回
if (!ofnode_valid(parent_node))
return 0;
// 遍历当前父设备的每一个直接子节点(递归深度为1)
for (node = ofnode_first_subnode(parent_node);
ofnode_valid(node);
node = ofnode_next_subnode(node)) {
const char *node_name = ofnode_get_name(node);
// 如果设备树中status是disabled,那么不处理这个设备节点
if (!ofnode_is_enabled(node)) {
pr_debug(" - ignoring disabled device\n");
continue;
}
// [!]在所有备选项中寻找适合当前设备的驱动,并绑定
err = lists_bind_fdt(parent, node, NULL, NULL, pre_reloc_only);
if (err && !ret) {
ret = err;
debug("%s: ret=%d\n", node_name, ret);
}
}
if (ret)
dm_warn("Some drivers failed to bind\n");
return ret;
}
3.11 [4] lists_bind_fdt——绑定设备树中的设备(使用compatible属性)
这段代码完成的事情非常简单,就是一个两层的for循环,外层循环是设备树的compatible属性构成的字符串列表,内层循环是保存着所有驱动的一个数组,通过匹配compatible字符串,为每一个设备树中的节点找到对应的驱动程序。
这个链表是在编译+链接期生成的,U_BOOT_DRIVER宏会生成一个具有特定名称前缀的驱动声明,并在链接期收集所有U-Boot中声明的驱动放置到某一个具有特定名称的内存段中,这也刚好就是ll_entry_start(struct driver, driver)这个宏返回的数组开头。
int lists_bind_fdt(struct udevice *parent, ofnode node, struct udevice **devp,
struct driver *drv, bool pre_reloc_only)
{
// 全局的驱动列表,所有的驱动都被记录在这个链接脚本指定的空间中
struct driver *driver = ll_entry_start(struct driver, driver);
const int n_ents = ll_entry_count(struct driver, driver);
const struct udevice_id *id;
struct driver *entry;
struct udevice *dev;
bool found = false;
const char *name, *compat_list, *compat;
int compat_length, i;
int result = 0;
int ret = 0;
if (devp)
*devp = NULL;
// 获取当前节点的名字并打印出来
name = ofnode_get_name(node);
log_debug("bind node %s\n", name);
// 获取节点的compatible属性,返回值是一个链表
compat_list = ofnode_get_property(node, "compatible", &compat_length);
if (!compat_list) {
if (compat_length == -FDT_ERR_NOTFOUND) {
log_debug("Device '%s' has no compatible string\n",
name);
return 0;
}
dm_warn("Device tree error at node '%s'\n", name);
return compat_length;
}
/*
* Walk through the compatible string list, attempting to match each
* compatible string in order such that we match in order of priority
* from the first string to the last.
*/
for (i = 0; i < compat_length; i += strlen(compat) + 1) {
// 取出compatible字符串,compat中记录的是当前正在匹配的字符串
compat = compat_list + i;
log_debug(" - attempt to match compatible string '%s'\n",
compat);
id = NULL;
// 遍历驱动链表
for (entry = driver; entry != driver + n_ents; entry++) {
// 如果入口参数指定了要使用的驱动
if (drv) {
if (drv != entry)
continue;
if (!entry->of_match)
break;
}
// 检查compat字符串是否处于当前驱动的兼容列表(of_match)里
// 匹配成功的项放入id中
ret = driver_check_compatible(entry->of_match, &id,
compat);
// 匹配成功则返回0
if (!ret)
break;
} // end of for (entry = driver; entry != driver + n_ents; entry++)
// 如果当前 compat没有driver匹配,尝试下一个compat_list中的项
if (entry == driver + n_ents)
continue;
// pre_reloc_only打开时,跳过那些不需要在重定位前绑定的设备节点
if (pre_reloc_only) {
if (!ofnode_pre_reloc(node) &&
!(entry->flags & DM_FLAG_PRE_RELOC)) {
log_debug("Skipping device pre-relocation\n");
return 0;
}
}
// 打印log,表明设备已经成功匹配到了驱动程序
if (entry->of_match)
log_debug(" - found match at '%s': '%s' matches '%s'\n",
entry->name, entry->of_match->compatible,
id->compatible);
// [!] 绑定这个device
// device_bind_with_driver_data就是device_bind_common的简单封装
// 请参见3.3节
ret = device_bind_with_driver_data(parent, entry, name,
id ? id->data : 0, node,
&dev);
// 如果绑定失败,在这里打印log
if (ret == -ENODEV) {
log_debug("Driver '%s' refuses to bind\n", entry->name);
continue;
}
if (ret) {
dm_warn("Error binding driver '%s': %d\n", entry->name,
ret);
return log_msg_ret("bind", ret);
// 匹配成功的情况下,将指针指向这个设备
} else {
found = true;
if (devp)
*devp = dev;
}
break;
}
// 遍历完所有的compatible字符串,依然找不到对应的驱动
// 打印log,报告仍无法找到驱动
if (!found && !result && ret != -ENODEV)
log_debug("No match for node '%s'\n", name);
return result;
}
3.12 [2] dm_probe_devices——深度优先初始化外设
这段代码做的事情就是从根设备开始,深度遍历地向下递归并尝试初始化(probe)子设备。但值得注意的是,这里大部分设备并不会立即执行probe动作,而是仅有DM_FLAG_PROBE_AFTER_BIND这个标志置位的设备会在绑定之后立即probe。
事实上,大部分设备是在被使用时才初始化的,这是一种推迟初始化的“懒策略”,一个设备被probe的时机有如下几个:
- DM_FLAG_PROBE_AFTER_BIND置位的设备,初次完成绑定时就会立即probe。
- 当一个设备初始化时(进入device_probe函数时),递归向上的初始化会使得其所处的一整条父子链上的设备都被probe。
- 当尝试获取一个设备时(uclass/device_get_device_by_xxx),如果这个设备还没有被初始化,那么在共同的收尾函数uclass/device_get_device_tail中会统一对设备进行一次初始化。
- 对于一些特殊设备,也有可能在设备驱动中就显式调用device_probe完成初始化。
static int dm_probe_devices(struct udevice *dev, bool pre_reloc_only)
{
// 获取设备对应的设备树节点
ofnode node = dev_ofnode(dev);
struct udevice *child;
int ret;
// 如果此函数只在重定位前初始化(probe)
// 但是设备或其对应的驱动不包含pre_reloc标志,则直接probe子设备节点
if (pre_reloc_only &&
(!ofnode_valid(node) || !ofnode_pre_reloc(node)) &&
!(dev->driver->flags & DM_FLAG_PRE_RELOC))
goto probe_children;
// 对于设备本身,只有在DM_FLAG_PROBE_AFTER_BIND置位时
// 才会立即执行本设备的probe
//
if (dev_get_flags(dev) & DM_FLAG_PROBE_AFTER_BIND) {
ret = device_probe(dev);
if (ret)
return ret;
}
// 递归初始化子设备,这本质上是一个深度优先遍历
// 也就是,此函数会优先初始化
probe_children:
list_for_each_entry(child, &dev->child_head, sibling_node)
dm_probe_devices(child, pre_reloc_only);
return 0;
}
4.总结
在本文中,我们详细讲解了在U-Boot SPL启动过程中,找寻设备树文件的过程以及初始化设备驱动模型(DM)的过程和方法。
其中,设备树根据配置的不同以及所处的不同启动阶段,会放置在不同的地址段。
而DM的初始化过程,本质上涉及到绑定(bind)和初始化(probe)两个过程,前者更多是软件上的概念,也就是为一个设备创建一个udevice结构体,对其进行初始化并关联到某一个驱动(driver)。后者往往与硬件关联更加紧密,初始化的过程往往伴随着对硬件寄存器的配置。
最后,理解DM本质上是一个正交的双重结构(链表设备分类 + 树状拓扑)。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)