3. Bootloader移植(U-Boot):U-Boot源码结构分析、板级配置与设备树修改、从SD卡/eMMC/NOR Flash启动的适配
好,咱们进入第三讲。Bootloader移植,说白了就是让芯片上电后能正确地把系统内核加载起来。U-Boot是当前车规级Linux系统里最主流的方案,没有之一。我这些年经手的项目,从NXP i.MX8到TI TDA4,清一色都是U-Boot。
为什么选它?因为U-Boot的生态太成熟了。你想想看,几乎所有的车规级SoC厂商都会提供U-Boot的板级支持包。你拿到开发板的第一件事,就是先把U-Boot跑起来。嗯,这里要注意,U-Boot的移植工作做得好不好,直接决定了后面Linux内核能不能稳定启动。
3.1 U-Boot源码结构分析
我第一次接触U-Boot源码时,说实话有点懵。目录太多了。但摸清楚规律后,你会发现它的组织方式其实很清晰。
核心目录就这几个:
- arch/:架构相关代码。ARM、RISC-V、x86都在这里。车规级芯片99%是ARM Cortex-A系列,所以重点关注arch/arm/。
- board/:板级相关代码。每个厂商的板子在这里都有对应的目录,比如board/freescale/imx8mp_evk/。
- configs/:默认配置文件。每个板子对应一个defconfig文件,比如imx8mp_evk_defconfig。
- include/configs/:板级配置头文件。这里存放的是传统的板级配置宏定义。
- dts/:设备树源文件。U-Boot现在也全面拥抱设备树了,和Linux内核共用一套dts。
- drivers/:驱动代码。串口、MMC、网络、NAND Flash等驱动都在这里。
- common/:通用功能代码。比如命令行解析、环境变量管理。
我个人习惯,拿到一个新板子,先看configs/下的defconfig文件。这个文件决定了U-Boot编译时启用了哪些功能。举个例子:
CONFIG_SYS_TEXT_BASE=0x40000000
CONFIG_DEFAULT_DEVICE_TREE="imx8mp-evk"
CONFIG_BOOTCOMMAND="run distro_bootcmd"
CONFIG_ENV_IS_IN_MMC=y
这里CONFIG_SYS_TEXT_BASE是U-Boot的链接地址,不能随便改。我在项目中遇到过有人把这个地址改错了,结果U-Boot死活跑不起来,最后发现是地址和DDR初始化配置对不上。
关键点:U-Boot的启动流程是:SPL(Secondary Program Loader)→ U-Boot proper。SPL负责初始化DDR和时钟,然后加载完整的U-Boot。车规级芯片通常要求启动时间在几百毫秒以内,所以SPL的优化很关键。
3.2 板级配置与设备树修改
板级配置,以前是用头文件里的宏定义来搞的。比如include/configs/imx8mp_evk.h里会定义:
#define CONFIG_SYS_LOAD_ADDR 0x40480000
#define CONFIG_SYS_MMC_ENV_DEV 0
#define CONFIG_EXTRA_ENV_SETTINGS \
"loadaddr=0x40480000\0" \
"fdt_addr=0x43000000\0" \
"bootargs=console=ttymxc1,115200 root=/dev/mmcblk1p2 rootwait rw\0"
但现在U-Boot越来越倾向于用设备树来配置硬件信息。为什么呢?因为设备树更灵活,不用重新编译U-Boot就能改配置。我建议你尽量把硬件相关的参数放到dts里,而不是硬编码在头文件中。
设备树的修改,主要关注这几个节点:
- chosen:启动参数,比如bootargs、stdout-path。
- memory:DDR内存布局。车规级芯片的DDR大小和地址范围必须和硬件设计一致。
- mmc:SD卡/eMMC控制器配置。时钟频率、总线宽度、电压等级。
- spi:NOR Flash控制器配置。
- pinctrl:引脚复用配置。这个最容易出错,我踩过不少坑。
举个例子,修改imx8mp-evk.dts中的MMC节点:
&usdhc2 {
pinctrl-names = "default", "state_100mhz", "state_200mhz";
pinctrl-0 = <&pinctrl_usdhc2>;
pinctrl-1 = <&pinctrl_usdhc2_100mhz>;
pinctrl-2 = <&pinctrl_usdhc2_200mhz>;
bus-width = <4>;
non-removable;
status = "okay";
};
这里non-removable表示eMMC是焊在板子上的,不是可插拔的SD卡。车规级产品基本都是eMMC,所以这个属性一定要加上。我曾经见过有人忘了加,结果系统启动时反复检测卡是否插入,浪费了宝贵的启动时间。
个人经验:修改设备树后,一定要用dtc工具反编译检查一下。我遇到过dts语法错误导致U-Boot无法解析设备树的情况。命令是:dtc -I dtb -O dts -o output.dts input.dtb
3.3 从SD卡/eMMC/NOR Flash启动的适配
车规级产品的启动介质,常见的就是这三种。每种介质的适配方法都不一样,我分别说一下。
3.3.1 SD卡启动
SD卡启动是最简单的,也是开发阶段最常用的。U-Boot需要把镜像烧录到SD卡的特定偏移位置。比如i.MX8MP的IVT(Image Vector Table)偏移是0x0,U-Boot proper的偏移是0x42。
烧录命令示例:
sudo dd if=u-boot.imx of=/dev/sdb bs=1k seek=1 conv=fsync
这里seek=1表示跳过第一个1KB块。为什么?因为第一个块要留给分区表。嗯,这个细节很多人会忽略。
3.3.2 eMMC启动
eMMC启动和SD卡类似,但eMMC有boot分区。车规级芯片通常支持从eMMC的boot分区启动。你需要把U-Boot烧录到boot分区:
mmc dev 0
mmc partconf 0 1 1 1
mmc write ${loadaddr} 0x0 0x800
这里mmc partconf命令配置eMMC的boot分区使能。我在项目中遇到过,有些eMMC芯片出厂时boot分区是关闭的,需要先使能才能启动。
避坑指南:我曾经在某个项目中,eMMC启动总是失败。排查了两天才发现,是eMMC的RST_n信号时序不满足要求。U-Boot在初始化eMMC时,需要等待RST_n信号稳定后才能发送CMD0。这个时序在车规级芯片的数据手册里都有,一定要仔细看。
3.3.3 NOR Flash启动
NOR Flash启动在车规级产品中主要用于存储SPL。因为NOR Flash的读取速度快,而且支持XIP(eXecute In Place),SPL可以直接在NOR Flash上运行,不用拷贝到DDR。
NOR Flash的适配主要关注两点:
- SPI控制器配置:时钟频率、模式(Mode 0/3)、数据位宽。
- Flash参数:JEDEC ID、扇区大小、擦除时间。
U-Boot中NOR Flash的驱动通常在drivers/spi/下。你需要确保CONFIG_SPI_FLASH和对应的Flash型号宏定义打开了。
配置示例:
CONFIG_SPI_FLASH=y
CONFIG_SPI_FLASH_MACRONIX=y
CONFIG_SF_DEFAULT_SPEED=40000000
这里CONFIG_SF_DEFAULT_SPEED是SPI Flash的默认时钟频率。车规级芯片的SPI时钟通常可以跑到40MHz甚至更高。但要注意,PCB走线长度和信号完整性会影响实际能跑多快。我建议你从20MHz开始调试,稳定后再逐步提高。
3.4 启动适配的常见问题
最后,我总结几个启动适配中容易踩的坑:
- 时钟配置错误:U-Boot SPL阶段的时钟配置必须和硬件设计一致。尤其是DDR时钟,频率不对直接导致系统挂死。
- 引脚复用冲突:设备树中的pinctrl配置不能和硬件原理图冲突。我建议你对照原理图逐引脚检查。
- 启动介质优先级:车规级芯片通常有多个启动源,通过BOOT_MODE引脚选择。确保你的硬件设计选择了正确的启动介质。
- 环境变量存储:U-Boot的环境变量默认存储在启动介质中。如果存储位置不对,会导致环境变量丢失,系统无法正常启动。
好了,这一讲的内容就到这里。下一讲我们会深入Linux内核的移植,到时候会涉及更多设备树和驱动的细节。记住,U-Boot移植是基础,基础打牢了,后面的工作才会顺利。