跳到主要内容

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)具体的设备并且使用。