3.5. DTS

ZX 平台上,U-Boot 与 Kernel 共用一份 DTS 配置,目前 DTB 的编译放在 U-Boot 阶段进行。项目相关的 DTS 文件存放路径:

  • m4-openwrt/package/boot/uboot-m4/src/arch/riscv/dts

  • xxx-board.dts

  • xxx-board-u-boot.dtsi

DTS 编译时,相关的源文件会被链接到 arch/riscv/dts/ 进行编译。

U-Boot 的 DTS 配置可参考 U-Boot 官方文档:

doc/README.fdt-control

3.5.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 数据只需从存储介质读取一次,从而加快启动速度。

3.5.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’

在 ZX 平台上,对于需要在 SPL 中使用的设备,需要配置属性 u-boot,dm-pre-reloc , 具体配置文件在:

  • 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.5.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_zx.c

#include <dt-structs.h>

struct aic_serial_plat {
    struct dtd_zx_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                     = "zx_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_zx_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           = "zx_aic_uart", // 扫描时,通过名字进行绑定
    .platdata       = &dtv_serial_at_19210000,
    .platdata_size  = sizeof(dtv_serial_at_19210000),
};

更详细的说明请参考官方文档:

doc/driver-model/of-plat.rst

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