4. TF-A与U-Boot板级移植基础

在STM32MP系列平台上,从芯片上电到Linux内核启动,中间要经过两段关键的引导程序:TF-A(Trusted Firmware-A)和U-Boot。TF-A负责建立安全运行环境、初始化DDR、加载并校验后续镜像;U-Boot则承担板级外设初始化、镜像加载和启动参数传递。板级移植的核心工作,就是让这两段引导程序“认识”你的板子——知道DDR怎么配、串口在哪、从哪里启动、设备树长什么样。本节围绕TF-A启动阶段、U-Boot板级文件结构、设备树与配置头文件、编译烧录与串口控制台调试四条主线展开。

一、TF-A启动阶段:从ROM Code到BL33

STM32MP1的启动链遵循ARM Trusted Firmware的分级模型。芯片内部固化的ROM Code(BL0)最先运行,它根据BOOT引脚和OTP配置选择启动介质(eMMC、SD卡、NAND、串口/USB),把FSBL(First Stage Boot Loader)加载到SYSRAM中执行。在STM32MP1上,FSBL就是TF-A的BL2阶段。

BL2运行在SYSRAM(约256KB)中,主要完成三件事:初始化时钟树、初始化DDR控制器、把后续镜像从启动介质搬到DDR。DDR初始化参数来自设备树中的DDR节点,包括时序参数、容量、位宽等,这部分参数一旦配错,BL2会在DDR训练阶段卡死,串口通常只输出到“DDR init”附近就停止。BL2随后加载BL31(运行时服务,处理安全监控和电源管理)、BL32(OP-TEE,可选)和BL33(即U-Boot),并通过SMC指令逐级跳转。

BL31常驻DDR,负责PSCI电源管理、系统复位和安全世界切换;BL33即U-Boot的SPL或完整U-Boot。理解这条链路的意义在于:当串口停在某个阶段时,你能迅速判断是DDR参数、镜像签名还是加载地址出了问题。

STM32MP1 启动链:ROM Code → TF-A → U-Boot → Kernel ROM Code (BL0) 读BOOT引脚/OTP 加载FSBL到SYSRAM TF-A BL2 (FSBL) 时钟树 + DDR初始化 加载BL31/BL32/BL33 BL31 (Runtime) PSCI电源管理 安全世界切换 BL33 (U-Boot) 板级外设初始化 加载Kernel/DTB Linux Kernel启动 运行位置与镜像布局 SYSRAM (256KB) BL2 运行区 DDR (如 512MB @ 0xC0000000) BL31 | BL32(OP-TEE) | BL33(U-Boot) | Kernel | DTB | RootFS 串口调试观察点 BL2: "DDR init" → BL31: "BL31: v2.x" → BL33: "U-Boot SPL/2021.10" → Kernel: "Starting kernel ..."

二、U-Boot板级文件结构

U-Boot的板级支持集中在几个固定位置。以STM32MP15x为例,板级目录位于board/st/stm32mp1/,其中board.c包含板级初始化函数board_init()、DDR相关回调、以及环境变量默认值;stm32mp1.c处理电源、USB、按键等板级特性。配置头文件位于include/configs/stm32mp1.h,定义环境变量、启动命令、内存布局等。

设备树文件位于arch/arm/dts/,如stm32mp157a-dk1.dts及其包含的stm32mp15xx-dkx.dtsi、stm32mp151.dtsi。U-Boot的设备树与Linux设备树同源,但U-Boot只使用其中一部分节点(串口、MMC、以太网、PMIC等),内核启动时会把控制权交给Linux自己的设备树。移植时通常先复制一份参考板的dts,修改模型名、内存大小、串口引脚和启动介质节点。

配置层面,U-Boot使用defconfig文件(如stm32mp15_defconfig)和Kconfig体系。板级差异通过CONFIG_TARGET_STM32MP15、CONFIG_DEFAULT_DEVICE_TREE等选项控制。编译时make stm32mp15_defconfig生成.config,再make DEVICE_TREE=stm32mp157a-dk1指定设备树。

三、设备树与配置头文件

设备树是板级移植的“地图”。以串口为例,U-Boot需要知道使用哪个UART、引脚如何复用、时钟从哪来。典型节点如下:

&usart3 {
    pinctrl-names = "default";
    pinctrl-0 = <&usart3_pins_a>;
    status = "okay";
};

&usart3_pins_a {
    pins1 {
        pinmux = <STM32_PINMUX('B', 10, AF7)>;  /* USART3_TX */
    };
    pins2 {
        pinmux = <STM32_PINMUX('B', 11, AF7)>;  /* USART3_RX */
    };
};

配置头文件stm32mp1.h中常见宏包括CONFIG_SYS_MMC_ENV_DEV(环境变量存储设备)、CONFIG_BOOTCOMMAND(默认启动命令)、CONFIG_EXTRA_ENV_SETTINGS(自定义环境变量)。例如把启动命令改为从eMMC读取内核:

#define CONFIG_BOOTCOMMAND \
    "mmc dev 1; " \
    "ext4load mmc 1:4 0xc2000000 /boot/uImage; " \
    "ext4load mmc 1:4 0xc4000000 /boot/stm32mp157a-custom.dtb; " \
    "bootm 0xc2000000 - 0xc4000000"

这里0xc2000000是内核加载地址,0xc4000000是设备树地址,需与DDR布局一致。地址写错会导致bootm报“Bad Magic Number”或内核解压失败。

四、编译烧录与串口控制台调试

典型编译流程分两步。TF-A:进入tf-a-stm32mp目录,执行make ARM_ARCH_MAJOR=7 ARCH=aarch32 PLAT=stm32mp1 STM32MP15=1 DTB_FILE_NAME=stm32mp157a-custom.dtb,生成tf-a-stm32mp157a-custom.stm32。U-Boot:进入u-boot-stm32mp目录,执行make stm32mp15_defconfig和make DEVICE_TREE=stm32mp157a-custom all,生成u-boot.stm32和u-boot-spl.stm32。

烧录到SD卡时,使用dd写入分区:

dd if=tf-a-stm32mp157a-custom.stm32 of=/dev/sdX bs=1M seek=0 conv=fsync
dd if=u-boot-spl.stm32 of=/dev/sdX bs=1M seek=1 conv=fsync
dd if=u-boot.stm32 of=/dev/sdX bs=1M seek=3 conv=fsync

串口控制台是调试的第一现场。STM32MP1默认使用UART4(DK1板为USART3,视板而定),波特率115200,8N1。上电后应依次看到:

NOTICE:  CPU: STM32MP157AAC Rev.Z
NOTICE:  Model: STMicroelectronics STM32MP157A-DK1
NOTICE:  Board: MB1272 Var1.0 Rev.C-01
INFO:    BL2: v2.6(debug):...
INFO:    BL2: Loading image id 3
INFO:    BL2: Loading image id 5
NOTICE:  BL2: Booting BL31
NOTICE:  BL31: v2.6(debug):...
U-Boot SPL 2021.10 (Jan 01 2024 - 00:00:00)
U-Boot 2021.10 (Jan 01 2024 - 00:00:00 +0000)
Hit any key to stop autoboot:  0

若串口无输出,先检查波特率、TX/RX是否接反、引脚复用是否被其他功能占用;若停在“DDR init”之后,重点核对DDR设备树参数;若U-Boot能启动但找不到内核,检查bootcmd中的分区号和加载地址。进入U-Boot命令行后,可用mmc list、mmc dev 1、ext4ls mmc 1:4 /boot逐级确认存储和文件系统,再用printenv查看环境变量,最后boot验证启动。

调试建议:先让参考板的TF-A和U-Boot在自制板上跑通串口,再逐步替换DDR参数和设备树节点。每次只改一个变量,便于定位问题。

板级移植不是一次写对,而是“串口有输出→DDR稳定→U-Boot命令行可用→内核能加载”的逐级收敛过程。掌握TF-A启动阶段、U-Boot文件结构、设备树与配置头文件、编译烧录与串口调试这四块,就具备了STM32MP平台板级适配的基本能力。