DTS
Artinchip 平台上,U-Boot 与 Kernel 共用一份 DTS 配置,目前 DTB 的编译放在 U-Boot 阶段进行。项目相关的 DTS 文件存放路径:
target/<IC>/common/*.dtsi
target/<IC>/<Board>/board.dts
target/<IC>/<Board>/board-u-boot.dtsi
target/<IC>/<Board>/board.dts
DTS 编译时,相关的源文件会被链接到 arch/riscv/dts/
进行编译。
U-Boot 的 DTS 配置可参考 U-Boot 官方文档:
doc/README.fdt-control
1. 两个 DTB
U-Boot 编译期间,DTS 被编译产生两个 dtb 文件:
- u-boot-spl.dtb
- u-boot.dtb
这两个 DTB 分别保存在三个不同位置,给 SPL, U-Boot, Kernel 三个阶段使用。
SPL DTB 的保存
在编译阶段 u-boot-spl.dtb 会被合并到 u-boot-spl.bin 的尾部,并生成 u-boot-spl-dtb.bin。 SPL 程序通过链接脚本中的变量 __bss_end
访问 DTB 数据。具体请参考 lib/fdtdec.c
。
开发者可以通 过重新定义 board_fdt_blob_setup()
的方式,修改加载 dtb 的方式和规则。
U-Boot DTB 的保存
在编译阶段 u-boot.dtb 会被合并到 u-boot.bin 的尾部,并生成 u-boot-dtb.bin。 U-Boot 程序通过链接脚本中的变量 _end
访问 DTB 数据。具体请参考 lib/fdtdec.c
。
开发者可以通过重新定义 board_fdt_blob_setup()
的方式,修改加载 dtb 的方式和规则。
Kernel DTB 的保存
Kernel 所用的 DTB 通常保存在单独分区,或者与 Kernel 一起保存在 FIT Image 包中, U-Boot 通过读取分区或者解析 FIT Image 格式来加载 Kernel DTB 数据。
注解
通常情况下 U-Boot 使用的 DTB 与 Kernel 所使用的 DTB 是相同的,做启动优化的时候, 这部分可以做一些优化,使得 DTB 数据只需从存储介质读取一次,从而加快启动速度。
2. SPL DTS 的配置
由于 SPL 仅使用 U-Boot DTS 中很少部分的设备配置,为了减少内存占用,SPL DTB 在编译时会使用 fdtgrep 工具对 U-Boot DTS 进行过滤,以产生一个更小的 DTB。 过滤后的 DTB 仅包含:
- the mandatory nodes (/alias, /chosen, /config)
- the nodes with one pre-relocation property: ‘u-boot,dm-pre-reloc’ or ‘u-boot,dm-spl’
在 Artinchip 平台上,对于需要在 SPL 中使用的设备,需要配置属性 u-boot,dm-pre-reloc
, 具体配置文件在:
target/<IC>/<Board>/board-u-boot.dtsi
参考示例:
&ccu {
u-boot,dm-pre-reloc;
};
&rst {
u-boot,dm-pre-reloc;
};
&osc24m {
u-boot,dm-pre-reloc;
};
&rc1m {
u-boot,dm-pre-reloc;
};
更多描述可参考文档:
doc/README.SPL
3. PLATDATA
SPL 使用 DTS 对平台上的设备进行配置,通常 SPL 支持 DTB 解析并不会占用太多的内存空间, 大约需要 3KB。但是对于一些 SPL 可用内存空间十分之有限的平台,节省 DTB 解析库 所占用的空间是十分之有吸引力的。
为此,U-Boot 提供另外一种设备配置方式,即通过 C 代码手动定义 Platform data, 其包含对设备的配置,然后通过 U_BOOT_DEIVCE() 声明设备。 如 drivers/demo/demo-pdata.c
中的示例:
static const struct dm_demo_pdata red_square = {
.colour = "red",
.sides = 4.
};
static const struct dm_demo_pdata green_triangle = {
.colour = "green",
.sides = 3.
};
static const struct dm_demo_pdata yellow_hexagon = {
.colour = "yellow",
.sides = 6.
};
U_BOOT_DEVICE(demo1) = {
.name = "demo_simple_drv",
.platdata = &red_square,
};
U_BOOT_DEVICE(demo2) = {
.name = "demo_shape_drv",
.platdata = &green_triangle,
};
U_BOOT_DEVICE(demo3) = {
.name = "demo_simple_drv",
.platdata = &yellow_hexagon,
};
这种方式可以节省空间,但需要在 DTS 之外新增一种配置方式,对于配置管理带来新的麻烦。 U-Boot 还提供另外一种功能,可以在编译时将 DTS 转为 C 语言的 Platform Data 数据结构, 然后在驱动源码中可以直接引用对应的配置。
其工作原理如下:
- SPL 编译时,首先通过 fdtgrep 生成 SPL 使用的 DTS
- 然后使用 dtoc.py 工具,将 DTS 配置转为 C 语言数据结构,在编译目录下生成:
include/generated/dt-structs-gen.h
spl/dts/dt-platdata.c
- 驱动中通过包含
dt-structs.h
来引用生成的数据结构 - 数据结构的名字根据 DTS 中的命名,按照一定规则生成,驱动引用时需要注意
使用该功能需要使能下面的配置:
- CONFIG_SPL_OF_PLATDATA
示例可参考:
drivers/serial/serial_artinchip.c
#include <dt-structs.h>
struct aic_serial_plat {
struct dtd_artinchip_aic_uart dtplat; // 数据类型编译时根据 DTS 配置生成
struct ns16550_platdata plat;
};
static int aic_serial_probe(struct udevice *dev)
{
struct aic_serial_plat *plat = dev_get_platdata(dev);
plat->plat.base = plat->dtplat.reg[0]; // 直接使用数据结构中的配置
plat->plat.reg_shift = plat->dtplat.reg_shift;
plat->plat.clock = plat->dtplat.clock_frequency;
plat->plat.fcr = UART_FCR_DEFVAL;
dev->platdata = &plat->plat;
return ns16550_serial_probe(dev);
}
U_BOOT_DRIVER(aic_serial) = {
.name = "artinchip_aic_uart", // 注意此处的名字规则
.id = UCLASS_SERIAL,
.priv_auto_alloc_size = sizeof(struct NS16550),
.platdata_auto_alloc_size = sizeof(struct aic_serial_plat),
.probe = aic_serial_probe,
.ops = &ns16550_serial_ops,
.flags = DM_FLAG_PRE_RELOC,
};
spl/dts/dt-platdata.c
中生成的配置信息为:
static const struct dtd_artinchip_aic_uart dtv_serial_at_19210000 = {
.clock_frequency = 0x16e3600,
.clocks = {
{&dtv_clock_at_18020000, {53}},},
.dma_names = {"rx", "tx"},
.dmas = {0x8, 0x10, 0x8, 0x10},
.reg = {0x19210000, 0x400},
.reg_io_width = 0x4,
.reg_shift = 0x2,
.resets = {0x3, 0x20},
};
U_BOOT_DEVICE(serial_at_19210000) = {
.name = "artinchip_aic_uart", // 扫描时,通过名字进行绑定
.platdata = &dtv_serial_at_19210000,
.platdata_size = sizeof(dtv_serial_at_19210000),
};
更详细的说明请参考官方文档:
doc/driver-model/of-plat.rst
4. 扫描和绑定
在 SPL 和 U-Boot 初始化阶段,需要对设备驱动模型进行初始化,并且需要扫描 DTS 或者 Platform Data 的配置,实现设备和驱动的绑定。注意,在此阶段仅完成设备和驱动的匹配和绑定, 并不会 probe 激活具体的设备。设备 Probe 在设备使用时才被触发。
SPL 的设备驱动初始化流程:
_start // arch/riscv/cpu/start.S
|-> board_init_f // arch/riscv/lib/spl.c
|-> spl_early_init() // common/spl/spl.c
|-> spl_common_init(setup_malloc = true) // common/spl/spl.c
|-> fdtdec_setup(); // lib/fdtdec.c 获取dtb的地址,并验证合法性
|-> dm_init_and_scan(!CONFIG_IS_ENABLED(OF_PLATDATA));
| // 初始化驱动模型的根节点,扫描设备树创建udevice,uclass
U-Boot 的设备驱动初始化分为两个阶段: initf_dm
& initr_dm
。
前一阶段只处理标记了 u-boot,dm-pre-reloc
的设备或者标记了 DM_FLAG_PRE_RELOC
的驱动; 后一阶段处理所有的设备和驱动。
_start // arch/riscv/cpu/start.S
|-> board_init_f() // common/board_f.c
|-> fdtdec_setup(); // lib/fdtdec.c
| |-> gd->fdt_blob = board_fdt_blob_setup(); // lib/fdtdec.c
| |-> fdtdec_prepare_fdt();
|
|-> initf_dm() // common/board_f.c
| |-> dm_init_and_scan(true); // drivers/core/root.c
|
|-> setup_reloc() // common/board_f.c
|-> jump_to_copy()
|-> relocate_code(gd->start_addr_sp, gd->new_gd, gd->relocaddr); // arch/riscv/cpu/start.S
| // RISCV 上的实现,relocate 之后,函数不返回,直接跳转到 board_init_r 执行
|-> invalidate_icache_all()
|-> flush_dcache_all()
|-> board_init_r // common/board_r.c
|-> initr_dm() // 初始化设备驱动,这次扫描所有的设备,并且绑定到匹配的驱动上
|-> dm_init_and_scan(false);
具体的扫描和绑定过程:
dm_init_and_scan(!CONFIG_IS_ENABLED(OF_PLATDATA)); // drivers/core/root.c
|-> dm_init(); // driver model, initiate virtual root driver
| |-> device_bind_by_name()
| | | // drivers/core/device.c
| | | // 加载"root_driver", gd->dm_root
| | |-> lists_driver_lookup_name
| | | // drivers/core/lists.c
| | | // 采用 U_BOOT_DRIVER(name) 声明的 driver
| | |
| | |-> device_bind_common(); // drivers/core/device.c
| | |-> uclass_get(&uc)
| | |-> uclass_bind_device(dev)
| | |-> drv->bind(dev)
| | |-> parent->driver->child_post_bind(dev)
| | |-> uc->uc_drv->post_bind(dev)
| |
| |-> device_probe(gd->dm_root) // drivers/core/device.c
| |-> uclass_resolve_seq(dev)
|
|-> dm_scan(pre_reloc_only);
|-> dm_scan_plat(pre_reloc_only);
| | // 在 SPL OF_PLATDATA 的情况,扫描和绑定由 U_BOOT_DEVICE 声明的驱动。
| |-> lists_bind_drivers(); // drivers/core/lists.c
| |-> bind_drivers_pass();
| |-> device_bind_by_name(); // 通过名字匹配设备和驱动
|
|-> dm_extended_scan(pre_reloc_only);
| |-> dm_scan_fdt(pre_reloc_only); // 扫描 设备树并与设备驱动建立联系
| | |-> dm_scan_fdt_node(gd->dm_root, ofnode_root(), pre_reloc_only); //扫描设备树并绑定root节点下的设备
| | |-> ofnode_first_subnode(parent_node) // 获取设备树的第一个子节点
| | |-> ofnode_next_subnode(node) // 遍历所有的子节点
| | |-> ofnode_is_enabled(node) // 判断设备树的子节点是否使能
| | |-> lists_bind_fdt(parent, node, NULL, pre_reloc_only); // 绑定设备树节点,创建新的udevicd drivers/core/lists.c
| | |-> ofnode_get_property(node, "compatible", &compat_length); // 获取compatible
| | |-> driver_check_compatible() // 和driver比较compatible值
| | |-> device_bind_with_driver_data() // 创建一个设备并绑定到driver drivers/core/device.c
| | |-> device_bind_common() // 创建初始化udevice 与对应的uclass,driver绑定
| |
| | // nodes = /chosen /clocks /firmware 一些节点本身不是设备,但包含一些设备,遍历其包含的设备
| |-> dm_scan_fdt_ofnode_path(nodes[i], pre_reloc_only);
| |-> ofnode_path(path); // 找到节点下包含的设备
| |-> dm_scan_fdt_node(gd->dm_root, node, pre_reloc_only);
|
|-> dm_scan_other(pre_reloc_only);
| // 扫描使用者自定义的节点 nothing
对于 DTS 中 soc
节点下的设备,在初始化时统一挂载到 simple-bus
中,在 soc
设备绑定了 simple-bus 驱动后,触发对 soc
的子设备的扫描和绑定。
device_bind_common();
|-> uc->uc_drv->post_bind(dev)
simple_bus_post_bind(); // drivers/core/simple-bus.c
|-> dm_scan_fdt_dev(dev); // drivers/core/root.c
|-> dm_scan_fdt_node();
在完成扫描和绑定所有的设备和驱动之后,系统处于可用状态,可以激活(probe)具体的设备并且使用。