Uboot根据Linux的驱动模型架构,也引入了Uboot的驱动模型(driver model :DM)。Uboot驱动模型与linux的设备模型比较类似,利用它可以将设备与驱动分离。对上可以为同一类设备提供统一的操作接口,对下可以为驱动提供标准的注册接口,从而提高代码的可重用性和可移植性。同时,驱动模型通过树形结构组织uboot中的所有设备,为系统对设备的统一管理提供了方便。
2.2.1 驱动模型结构
driver 结构体
驱动模型主要用于管理系统中的驱动和设备,uboot为它们提供了以下描述结构体:
driver结构体用于表示一个驱动,其定义如下:
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 (*of_to_plat)(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;
int plat_auto;
int per_child_auto;
int per_child_plat_auto;
const void *ops; /* driver-specific operations */
uint32_t flags;
#if CONFIG_IS_ENABLED(ACPIGEN)
struct acpi_ops *acpi_ops;
#endif
}
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 (*child_post_probe)(struct udevice *dev);
int (*init)(struct uclass *class);
int (*destroy)(struct uclass *class);
int priv_auto;
int per_device_auto;
int per_device_plat_auto;
int per_child_auto;
int per_child_plat_auto;
uint32_t flags;
};
03_Embedded_ARMv8 BL33 Uboot Booting Flow
ARM上电后会从BootROM(或XIP)启动第一段程序,BootROM/XIP固化着SoC厂商自己写的驱动程序,这部分代码除了一些ARM公司规定的必要启动流程,SoC厂商根据自己的SoC设计内置一些初始化的操作,我们把这一阶段称为
SoC BootROM booting
阶段。由于BootROM内部的程序没有办法重新烧写,因为在芯片的流片的之前,这部分程序必须要确定下来且经过质量测试,一旦流片这部分程序将无法重新烧写,因此是重中之重的一环。在一些Secure Boot设计中,这部分程序要固化初试的根信任信息。SoC BootROM之后的阶段,有多种叫法,中文称作
第二级程序引导
,缩写为SPL,一些公司例如德州仪器也会将这个引导的名字称为MLO。引入SPL的原因是因为SRAM的空间比较小,我们需要在正式引导之前有一个加载的程序,可以将正式的引导导入到DDR中。在某些架构下从SPL延伸出TPL的概念,我们从整个boot的过程中也将其视为SPL阶段。SPL在uboot可以找到其身影,这部分代码SoC厂商会根据自己的平台编写好自己的程序,并合入到uboot的代码仓库中。对于OEM厂可以对这部分代码根据需求进行订制和修改。ARMv8上提供了ATF固件,ATF固件可以cover整个boot流程。若我们在ARMv8上面使用安全feature,必然要使用ATF固件作为整个引导的主导程序。当然,强大uboot也会和ATF功能有所重叠,在不使用ATF情况下,uboot也可以挑起启动的重任,但是无法使用其安全feature。
SPL之后,将会引导OEM厂最熟悉的uboot或者其他品牌的引导程序。关于整个ARMv7/v8的非安全和安全启动,可以参考:#65[^2] 和 #61[^1]
本节的重点关注于ARMv8的BL33阶段的uboot booting过程:
1. uboot的初始化[^3]
在BL33阶段的uboot初始化部分很多功能化和SPL是共用的,如下图所示为uboot整体的流程,其中标注为绿色的是BL33 uboot特有的部分,其他没有标注的,可以参考( https://github.com/carloscn/blog/issues/61 2.2.2 ARMv8 uboot-spl analysising)[^1]
该流程主要包含了以下部分:
1.1 SMP多核启动
soc在启动阶段除了一些特殊情况外(如为了加快启动速度,在bl2阶段通过并行加载方式同时加载bl31、bl32和bl33镜像),一般都没有并行化需求。因此只需要一个cpu执行启动流程即可,这个cpu被称为primary cpu,而其它的cpu则统一被称为secondary cpu。为了防止secondary cpu在启动阶段的执行,它们在启动时必须要被设置为一个特定的状态。当primary cpu完成操作系统初始化,调度系统开始工作后,就可以通过一定的机制启动secondary cpu。显然secondary cpu不再需要执行启动流程代码,而只需直接跳转到内核中执行即可。故其启动的关键是如何将内核入口地址告知secondary cpu,以使其能跳转到正确的执行位置[^5]。
随着aarch64架构电源管理需求的增加(如cpu热插拔、cpu idle等),arm设计了一套标准的电源管理接口协议psci。该协议可以支持所有cpu相关的电源管理接口,而且由于电源相关操作是系统的关键功能,为了防止其被攻击,该协议将底层相关的实现都放到了secure空间,从而可提高系统的安全性[^5]。
armv8的从cpu启动包含psci和spintable两种方式[^4]。spin-table是一个比较简单的启动方法,差不多意思就是单核启动,其他核睡去的顺序执行;而psci是一个相对比较复杂的启动方法,且psci方式需要由bl31处理。
1.1.1 spin-table ^7
1.1.2 psci
除了spin-table的方式,还有psci可以供选择作为secondary cpu的启动^7。在arm的uboot源程序里面使用Kconfig的
ARMV8_SPIN_TABLE
标签可以使能SPIN_TABLE
,如果禁止那么则是PSCI启动模式[^8]。PSCI方式依托于ARM64的PSCI架构,PSCI架构不仅仅用于启动,还用于提供的一套电源管理接口,只不过启动被包含在这个架构之中[^6]。本文重点在uboot启动流程,关于SMP多核启动流程,uboot部分和kernel部分有很强的关联,故我们将这部分放在LinuxKernel专题来讲解,请参考 # 02_LinuxKernel_内核的启动(二)SMP多核处理器启动过程分析 #661.2
_main
流程分析1.2.1 gd及内存规划
在进入c语言之前,我们需要为其准备好运行环境,以及做好内存规划,这其中除了栈和堆内存之外,还需要为gd结构体分配内存空间。gd是uboot中的一个global_data类型全局变量,该变量包含了很多全局相关的参数,为各模块之间参数的传递和共享提供了方便。由于该变量在跳转到c流程之前就需要准备好,此时堆管理器尚未被初始化,所以其内存需要通过手工管理方式分配。以下为uboot内存规划相关代码:
(6)该流程主要用于初始化gd,和设置early malloc的堆管理器基地址,其代码如下:
1.2.2 重定位
一般的启动流程会由spl初始化ddr,然后将uboot加载到ddr中运行。但这并不是必须的,uboot自身其实也可以作为bl1或bl2启动镜像,此时uboot最初的启动位置不是位于ddr中(如norflash)。由于norflash的执行速度比ddr要慢的多,因此在完成ddr初始化后就需要将其搬移到ddr中,并切换到新的位置继续执行,这个流程就叫做uboot的重定位。
1.2.2.1 重定位的前提
uboot重定位依赖于位置无关代码技术,因此需要在编译和重定位时添加以下支持:
1.2.2.2 重定位基本流程
由于内核需要从内存的低地址开始运行,为了防止内核三件套(kernel、dtb和ramdisk)的加载地址与uboot运行地址重叠,因此uboot的重定位地址需要被设置到内存顶端附近。同时我们还需要为一些特定模块预留一些内存空间(比如页表空间、framebuffer等),下图就是uboot规划的重定位后内存布局:
该图中橙色部分都是需要执行重定位操作的,如uboot的代码段、数据段,以及gd、设备树等,它们都是在board_init_r阶段还需要使用的。对于gd和dtb等纯数据的重定位,只需要将数据拷贝到新的地址,并将其基地址指针切换到新地址即可。但对于代码段的重定位我们还需要考虑以下问题:
Note:
1.2.3 board_init_f
board_init_f是uboot重定位前的流程,它包括一些基础模块的初始化和重定位相关的准备工作。以下为该函数在armv8架构下可能的执行流程,图中灰色框表示该流程是可配置的,黄色框表示是必选的。
1.2.4 board_init_r
board_init_r是uboot重定位后需要执行的流程,它包含基础模块、硬件驱动以及板级特性等的初始化,并最终通过run_main_loop启动os会进入命令行窗口。
2. uboot的驱动模型
uboot在初始化完成后会为用户提供一个命令行交互接口,用户可通过该接口执行uboot定义的命令,以用于查看系统状态,设置环境变量和系统参数等。为了方便对硬件和驱动的管理,uboot还引入了类似linux内核的设备树和驱动模型特性。当然,为了增加系统的可配置性、可调试性以及可跟踪性等,它还支持环境变量、log管理、bootstage统计以及简单的ftrace等功能。下面我们将对这些特性做一简单的介绍。
2.1 设备树
设备树是一种通过dts文件来描述SoC属性,通过将设备的具体配置信息与驱动分离,以达到利用一份代码适配多款设备的机制。dts文件包含了一系列层次化结构的节点和属性,它可以通过dtc编译器编译成适合设备解析的二进制dtb文件。
uboot设备树的使用包含以下流程:为目标板添加dts文件、选择一个运行时使用的dtb文件、使能设备树。
如何为目标板添加一个dts文件
在
arch/<arch>/dts
目录下,添加一个xxx.dts文件,该文件可以从内核拷贝,或者在uboot dts目录下选择一个其它目标板的dts为基础,再根据实际需求进行修改。修改完成后,在arch/arm/dts/Makefile中为其添加编译选项:其中yyy为使用该dts的目标板
如何为目标板选择dts文件 ?
uboot的设备树文件位于
arch/<arch>/dts
目录下,可通过以下选项为目标板选择一个默认的dts文件:这是因为与内核不一样,uboot最终的镜像会和dtb打包在一个镜像文件中,因此在编译流程中就需要知道最终被使用的dtb。关于uboot镜像与dtb之间的关系将在 3. uboot的kernel封装 介绍。
通过编译命令指定dts
有时在编译时希望使用一个不是默认指定的dts,则可以通过在编译命令中添加DEVICE_TREE=zzz方式指定新的dts文件,其示例如下:
如何使能设备树
通过配置CONFIG_OF_CONTROL选项即可使能设备树的支持,uboot与dtb可以有以下几种打包组合方式:
cat u-boot-nodtb.bin u-boot.dtb >uboot.bin
2.2 驱动模型DM
Uboot根据Linux的驱动模型架构,也引入了Uboot的驱动模型(driver model :DM)。Uboot驱动模型与linux的设备模型比较类似,利用它可以将设备与驱动分离。对上可以为同一类设备提供统一的操作接口,对下可以为驱动提供标准的注册接口,从而提高代码的可重用性和可移植性。同时,驱动模型通过树形结构组织uboot中的所有设备,为系统对设备的统一管理提供了方便。
2.2.1 驱动模型结构
driver 结构体
驱动模型主要用于管理系统中的驱动和设备,uboot为它们提供了以下描述结构体:
driver结构体用于表示一个驱动,其定义如下:
驱动可以通过以下接口注册到系统中:
即其会定义一个struct driver 类型的
_u_boot_list_2_driver_2_#_name
变量,该变量在链接时需要被放在u_boot_list_2_driver_2_#_name
段中。我们再看下这些section在链接脚本中是如何存放的,以下为armv8架构链接脚本arch/arm/cpu/armv8/u-boot.lds中的定义:从定义可看到这些以.u_boot_list 开头的section都会被保存在一起,且它们会按照section的名字排序后再保存。这主要是为了便于遍历这些结构体,如我们需要遍历所有已经注册的driver,则可通过以下代码获取driver结构体的起始地址和总的driver数量。
其中ll_entry_start和ll_entry_coun的定义如下:
(1-3)定义一个
.u_boot_list_2_"#_list"_1
的段,若需要遍历driver,则该段的名字为.u_boot_list_2_driver_1
,即它位于所有实际driver section之前的位置; (1-4)定义一个.u_boot_list_2_"#_list"_3
的段,若需要遍历driver,则该段的名字为.u_boot_list_2_driver_3
,即它位于所有实际driver section之后的位置; (1-5)通过以上两个标号就可以很方便地获取驱动的起止地址和计算已注册驱动的总数。uclass_driver结构体
uclass_driver结构体用于表示一个uclass驱动,其定义如下:
其注册和遍历方式与driver完全相同,只是结构体类型和section名有所不同,以下为其定义:
udevice结构体
udevice在驱动模型中用于表示一个与驱动绑定的设备,其定义如下:
udevice在驱动模型中用于表示一个与驱动绑定的设备,其定义如下:
系统中所有的udevice结构体可以通过parent、child_head和sibling_node连接在一起,并且最终挂到gd的dm_root节点上,这样我们就可以通过gd->dm_root遍历所有的udevice设备。下图是udevice的连接关系,其中每个节点的parent指向其父节点,sibling指向其兄弟节点,而child指向子节点。
由于每个udevice都属于一个uclass,因此除了被连接到gd->dm_root链表之外,udevice还会被挂到uclass的链表中。它们之间的连接关系将在下面介绍uclass时给出。udevice是在驱动模型初始化流程中根据扫描到的设备动态创建的,在uboot中实际的设备可以通过以下两种方式定义:
uclass结构体
uclass用于表示一类具有相同功能的设备,从而可以为其抽象出统一的设备访问接口,方便其它模块对它的调用。以下为uclass的定义:
uclass将所有属于该类的设备挂到其dev_head链表上,同时系统中所有的uclass又会被挂到一张全局链表gd->uclass_root上。
2.2.2 驱动模型初始化
驱动模型初始化主要完成udevice、driver以及ucalss等之间的绑定关系,其主要包含以下部分:
该流程通过dm_init_and_scan函数实现,它会分别扫描由U_BOOT_DRVINFO以及devicetree定义的设备,为它们分配udevice结构体,并完成其与driver和uclass之间的绑定关系等操作。需要注意的是该函数在board_init_f和board_init_r中都会被调用,其中board_init_f主要是为了解析重定位前需要使用的设备节点,这种类型节点在devicetree中会增加u-boot,dm-pre-reloc属性。
2.3 环境变量与命令行
2.3.1 环境变量
环境变量可以为uboot提供在运行时动态配置参数的能力,如在命令行通过修改环境变量bootargs可以改变内核的启动参数。它以env=value格式存储,其中每条环境变量之间以’\0’结尾。根据系统的配置参数,uboot在include/env_default.h中为系统定义了一份默认的环境变量:
在该环境变量中,board可通过重新定义CONFIG_EXTRA_ENV_SETTINGS的值设置其自身的默认环境变量,如对于qemu平台,其定义位于include/configs/qemu-arm.h:
环境变量被修改后可以保存到固定的存储介质上(如flash、mmc等),以便下一次启动后加载最新的值。Uboot通过U_BOOT_ENV_LOCATION宏定义环境变量的存储位置,例如对于mmc其定义如下(env/mmc.c):
环境变量在mmc中的具体存储位置可通过配置选项或devicetree设置,如对于mmc:
下面的选项用于配置环境变量的长度及其保存的设备:
uboot对保存在固定介质中的环境变量会使用crc32校验数据的完整性,若数据被破坏了则会使用默认环境变量重新初始化环境变量的值。
2.3.2 命令行
uboot在初始化完成后可以通过按键进入命令行窗口,在该窗口可以执行像设置环境变量,下载镜像文件,启动内核等命令,这些命令的支持大大方便了uboot和内核启动相关流程的调试。uboot提供了很多内置命令,如md、mw、setenv、saveenv、tftpboot、bootm等,uboot提供了以下宏用于命令定义(include/command.h):
U_BOOT_CMD
它用于定义一个uboot命令,其定义如下:
其中参数含义如下:
U_BOOT_CMD_WITH_SUBCMDS
它用于定义一个带子命令的uboot命令,子命令可以避免主命令处理函数中包含过多的逻辑,还可以为每个子命令可以定义自身的_rep参数,以独立处理其是否可被重复执行的功能。
以下为其定义:
其固定参数如下:
U_BOOT_SUBCMD_MKENT
可变参数部分可用于定义子命令U_BOOT_SUBCMD_MKENT,其定义如下:
子命令的参数如下:
以wdt命令为例(cmd/wdt.c),其定义了主命令wdt,并且定义了子命令list、dev、start等
若我们需要自定义一个命令,可参考如下流程(以test_cmd命令为例):
3. uboot的kernel封装
uboot主要用于启动操作系统,以armv8架构下的linux为例,其启动时需要包含kernel、dtb和rootfs三部分。uboot镜像都是以它们为基础制作的,因此在介绍uboot镜像格式之前我们需要先了解一下它们的构成。
3.1 内核的几种镜像
3.1.1 vmlinux镜像
linux内核编译完成后会在根目录生成原始的内核文件为vmlinux,使用readelf工具可看到其为elf文件格式:
由于uboot引导的镜像不能包含elf头,因此该镜像不能直接被uboot使用。
3.1.2 image和zImage镜像
Image镜像是vlinux经过objcopy去头后生成的纯二进制文件,对于armv8架构其编译的Makefile如下:
aarch64-linux-gnu-objcopy -O binary -R .note -R .note.gnu.build-id -R .comment -S vmlinux Image
–O
binary:将输出二进制镜像,即会去掉elf头–R
.note:-R选项表示去掉镜像中指定的section,如这里会去掉.note、.note.gnu.build-id和.comment段–S
:去掉符号表和重定位信息,它与-R选项的功能类似,都是为了减小镜像的size因此,执行该命令后生成的Image镜像是去掉elf头,去掉.note等无用的section,以及strip过的二进制镜像。它可以被uboot的booti命令直接启动。但若要使用bootm启动,则还需要将其进一步封装为后面介绍的uimage或bootimg镜像
3.2 设备树
设备树是设备树dts源文件经过编译后生成的,其目标文件为二进制格式的dtb文件。其示例编译命令如下:
(1) –I:指定输入文件格式
(2)–O:指定输出文件格式
(3)–o:指定输出文件名
设备树还支持dtb overlay机制,即可以向设备提供一个基础dtb和多个dtbo镜像,并在启动前将它们merge为最终的dtb。在嵌入式Linux下,设备树(device tree)用来描述硬件平台的各种资源,Linux内核在启动过程中,会解析设备树,获取各种硬件资源来初始化硬件[^12]。设备树的overlay功能是指可以在系统运行期间动态修改设备树。
一般情况下,如上图所示,设备树经过DTC编译器编译为二进制的hello.dtb文件,加载到内存,随Linux内核一起启动后,一般就无法更改了。如果我们想修改设备树,需要修改hello.dts文件文件,重新编译成二进制文件:hello.dtb,然后重新启动内核,重新解析。有了设备树的overlay功能,省去了设备树的重新编译和内核重启,我们可以直接编写一个设备树插件:overlay.dts,编译成overlay.dtbo后,直接给设备树“打补丁”,在运行期间就可以动态添加节点、修改节点。
设备树的overlay功能,在很多场合都会用得到,会让我们的开发更加方便:
dtb overlay测试可以参考文献[^11]。
3.3 根文件系统
linux可以支持多种形式的根文件系统,如initrd、initramfs、基于磁盘的根文件系统等。站在启动镜像的角度看其实它们都是制作好的文件系统镜像,内核可以从特定的位置获取并挂载它们。以下是它们在启动时的基本特性:
(1)initrd: 它是一种内存文件系统,需要由bootloader预先加载到内存中,并将其内存地址传递给内核。如uboot将initrd加载到地址$initrd_addr处,则bootm参数如下:
bootm $kernel_addr $initrd_addr $fdt_addr
(2)initramfs: initramfs也是一种内存文件系统,但与initrd不同,它是与内核打包在一起的。因此不需要通过额外的参数 (3)磁盘rootfs: 磁盘根文件系统会被刷写到flash、mmc或disk的分区中,在内核启动时可在bootargs添加下面格式的参数,以指定根文件系统的位置root=/dev/xxx
因此,以上这些rootfs只有initrd是需要uboot独立加载的,故只有当rootfs为initrd时,uboot镜像打包流程才需要在镜像打包时为其单独考虑。参考[^12]
3.4 uImage
3.4.1 Legacy uimage格式
uboot最先支持legacy uimage格式的镜像,它是在内核镜像基础上添加一个64字节header生成的。该header信息用于指定镜像的一些属性,如内核的类型、压缩算法类型、加载地址、运行地址、crc完整性校验值等。其格式如下:
uboot的bootm命令会解析镜像头中的信息,并根据这些信息执行镜像校验、解压和启动等流程。以下是创建uImage的命令示例:
mkimage -A arm64 -O linux -C none -T kernel -a 0x80008000 -e 0x80008040 -n Linux_Image -d zImage uImage
但是它也有着一些缺点,如:
(1)加载流程比较繁琐,如需要分别加载内核、initrd和dtb; (2)启动参数较多,需要分别制定内核、initrd和dtb的地址; (3)在支持secure boot的系统中对secure boot的支持不足。 为此,uboot又定义了一种新的镜像格式fit uimage,用于解决上述问题。
3.4.2 Fit uimage格式
Fit uimage是使用devicetree语法来定义uimage镜像描述信息以及启动时的各种属性,这些信息被写入一个后缀名为its的源文件中。以下是一个its文件的示例:
它包含images和configurations两个顶级节点,images指定该its文件会包含哪些镜像,以及这些镜像的属性信息。configurations用于定义一系列镜像组合信息,如在本例中包含了config-1、config-2和config-3三种镜像组合方式。Its使用default属性指定启动时默认采用的配置信息,若启动时不希望使用默认配置,则可通过在启动参数中动态指定配置序号。下面我们通过kernel-1节点看下image属性的含义: (1)镜像的描述信息
(2)镜像文件的路径
(3)镜像类型,如kernel、ramdisk或fdt
(4)支持的架构
(5)支持的操作系统
(6)其使用的压缩算法
(7)加载地址
(8)运行地址
(9)完整性校验使用的hash算法 configurations的属性比较简单,就是指定某个配置下使用哪一个kernel、dtb和ramdisk镜像。Fit image除了支持完整性校验外,还可支持hash算法 + 非对称算法的secure boot方案,如以下例子:
(1)指定sha1为secure boot签名使用的hash算法,rsa2048为其使用的签名算法
(2)可能使用的验签密钥名
与设备树类似,its文件可以通过mkimage和dtc编译生成itb文件。镜像生成方式如下:
mkimage -f xxx.its xxx.itb
xxx.itb文件可以直接传给uboot,并通过bootm命令执行,如xxx.itb被加载到0x80000000,则其命令如下:
bootm 0x80000000
若需要选择非默认的镜像配置,则可通过指定配置序号实现,例如:
bootm 0x80000000#config@2
3.5 boot image
boot image是android定义的启动镜像格式,到目前为止一共定义了三个版本(v0 – v2),其中v0版本包含andr_img_hdr、kernel、ramdisk和second stage,v1版本增加了recovery dtbo/acpio,v2版本又增加了dtb。在这些镜像中second stage是可选的,而recovery dtbo只有在使用recovery分区的非AB系统中才需要,且它们都需要page对齐(通常为2k)。以下是boot image镜像的基本格式:
andr_img_hdr镜像头用于描述这些镜像的信息,如其长度、加载地址等,其定义如下:
可以使用mkbootimg.py脚本制作boot image镜像,该脚本参数比较简单,就是指定与镜像头中定义的相关参数。例如:
3.6 boot flow
bootm是uboot用于启动操作系统的命令,它的主要流程包括根据镜像头获取镜像信息,解压镜像,以及启动操作系统。以下为其主要执行流程: 以上流程最终会调用特定os的启动函数,例如需要启动armv8架构的linux,则其调用的接口为arch/arm/lib/bootm.c中的do_bootm_linux。以下为其执行流程:
Reference
[^1]:01_Embedded_ARMv7/v8 non-secure Boot Flow #61 [^2]:02_Embedded_ARMv8 ATF Secure Boot Flow (BL1/BL2/BL31) #65 [^3]:# 聊聊SOC启动(五) uboot启动流程一 [^4]:arm64 中的 spin-table 和 psci 两种启动多核流程分析 [^5]:# linux cpu管理(二) spin-table启动 [^6]:# linux cpu管理(三) psci启动
[^8]:[2/2] arm64: add better spin-table support [^9]:u-boot kconfig [^10]:# Linux内核编程12期:设备树overlay与ConfigFS文件系统 [^11]:# 树莓派设备树覆盖(dtb overlay) [^12]:# ramfs,rootfs,initramfs,initrd