1. 项目概述
在嵌入式系统开发,尤其是基于NXP Layerscape这类高性能多核处理器的项目中,启动流程的稳定性和可靠性是项目成功的基石。很多开发者,尤其是从应用层转向底层开发的工程师,常常对从按下电源键到Linux内核接管系统这“黑盒”般的几秒钟里发生了什么感到困惑。启动失败、DDR无法正确初始化、系统在TF-A阶段就卡住,这些问题往往让人无从下手。今天,我就结合自己多年在Layerscape平台上的踩坑经验,为你彻底拆解TF-A的启动流程,并聚焦于其中最核心也最容易出错的环节——DDR初始化。我们将不仅仅停留在理论,而是深入到代码和配置层面,手把手带你理解如何为不同的内存硬件(DIMM、固定DDR)编写初始化代码,以及如何利用NXP官方的Flexbuild工具链高效地构建、更新和部署整个系统镜像。无论你是正在评估Layerscape平台,还是已经深陷启动调试的泥潭,这篇文章都将为你提供一条清晰的路径。
2. TF-A启动流程深度解析
要理解DDR初始化,必须先把它放在整个启动链条中来看。ARMv8-A架构定义了一套从高特权级(EL3)到低特权级(EL0)的启动序列,TF-A(Trusted Firmware-A)正是这套规范在NXP平台上的具体实现。它不是一个单一的二进制文件,而是一系列阶段(Stage)的集合,每个阶段各司其职。
2.1 启动阶段全景图与职责划分
从SoC上电复位开始,到U-Boot准备好引导Linux内核,整个过程可以清晰地划分为以下几个阶段,它们像接力棒一样传递控制权:
-
BL1 (Boot ROM) : 这是固化在芯片内部的代码,开发者无法修改。它的职责非常固定:读取引脚状态确定启动设备(如QSPI NOR Flash、SD卡),加载启动设备偏移0处的初始程序到芯片内部的OCRAM(On-Chip RAM)并执行。在Layerscape的TF-A流程中,这个初始程序就是 BL2与RCW的融合体——
bl2_<boot_mode>.pbl。RCW(Reset Configuration Word)负责配置SerDes、时钟、引脚复用等最底层的硬件属性。 -
BL2 (Trusted Boot Firmware) : 这是开发者可以介入的第一个关键阶段,运行在EL3。它从OCRAM中开始执行,其核心任务之一就是 初始化DDR内存控制器 。为什么是BL2来做?因为后续的BL31、U-Boot等镜像体积较大,必须被加载到容量大得多的DDR中才能运行,而DDR在复位后是未配置、不可用的状态。因此,BL2必须在自身代码还在OCRAM中时,就完成对DDR控制器的编程,为加载后续镜像准备好“舞台”。初始化完成后,BL2会从启动设备(如Flash)加载FIP(Firmware Image Package)镜像到DDR中,并验证其完整性(如果使能了安全启动)。
-
BL31 (Runtime Firmware) : 这是常驻在DDR中、运行在EL3的“安全世界”固件。它提供了电源管理、CPU热插拔、安全监控等运行时服务。BL2会将控制权移交给它。如果系统配置了OP-TEE(一种可信执行环境操作系统),BL31还会负责加载并跳转到BL32。
-
BL32 (Optional, Trusted OS) : 例如OP-TEE,运行在安全世界的EL1。它处理来自普通世界(Linux)的安全请求。初始化完成后,会将控制权交还给BL31。
-
BL33 (Normal World Bootloader) : 这就是我们熟悉的U-Boot(或UEFI),运行在非安全世界的EL2。BL31最终会将控制权交给它。此时,DDR已经可用,U-Boot可以愉快地使用大内存进行各种外设初始化、加载设备树和Linux内核了。
一个重要的变化 :在旧的PPA启动流程中,DDR初始化是由U-Boot完成的。而在TF-A流程中,这个职责被前移到了BL2。这意味着, 所有关于内存频率、时序、拓扑结构的配置,现在都需要在ATF(ARM Trusted Firmware)的代码树中完成,而不是在U-Boot中 。这是迁移到TF-A时需要适应的最大改变之一。
2.2 关键镜像文件剖析:bl2.pbl与fip.bin
理解这些镜像文件的构成,对于调试和定制至关重要。
-
bl2_<boot_mode>.pbl: 这是上电后第一个被加载执行的软件实体。它不是一个“纯”的BL2镜像,而是由两个部分拼接(Concatenate)而成:-
RCW二进制文件
(
rcw_<boot_mode>.bin): 由RCW源码(.bin文件)编译生成,定义了最底层的硬件配置。 -
BL2二进制文件
(
bl2.bin): 由ATF源码编译生成,包含平台初始化(主要是DDR初始化)代码。pbl工具(在ATF源码中)将这两者合并,并在头部添加必要的元信息,生成最终的.pbl文件。所以,当你更新了DDR初始化代码后,必须重新编译并生成新的bl2.pbl文件。
-
RCW二进制文件
(
-
fip.bin: 这是一个容器镜像,由fiptool工具打包生成,内部包含了:-
bl31.bin: EL3运行时固件。 -
bl32.bin(可选): 如tee.bin(OP-TEE)。 -
bl33.bin: 即u-boot.bin。fip.bin被BL2加载到DDR中指定的地址。由于它是一个打包文件,单独更新U-Boot或OP-TEE后,都需要重新打包生成新的fip.bin。
-
启动流程与镜像文件的对应关系
可以概括为:Boot ROM从Flash的0偏移处加载并执行
bl2_sd.pbl
(假设SD卡启动) -> BL2初始化DDR -> BL2从Flash的后续偏移(如SD卡的0x800块)加载
fip.bin
到DDR -> BL2跳转到DDR中的BL31执行 -> 最终链式加载到U-Boot。
3. DDR初始化:从原理到代码实践
DDR初始化是BL2阶段最核心的任务,也是硬件相关度最高、最容易出问题的地方。其本质是通过配置DDR内存控制器(DDRC)和物理层(PHY)的一系列寄存器,让它们能够以正确的时序和频率驱动主板上的内存颗粒或内存条。
3.1 初始化流程与硬件配置模式
DDR初始化的代码位于ATF源码的特定平台目录下,例如对于LS1046ARDB板,路径是
<atf_dir>/plat/nxp/soc-ls1046/ls1046ardb/ddr_init.c
。这个文件中的
init_ddr()
函数是入口点。
根据板级设计使用的内存类型不同,初始化方式主要分为三种,你需要根据硬件原理图来选择:
-
DIMM模式 (带SPD) :这是最“智能”的方式。主板使用标准的内存条(DIMM),上面有一颗小的EEPROM,称为SPD(Serial Presence Detect),里面存储了该内存条的所有时序参数(如CL、tRCD、tRP等)。在
init_ddr()函数中,你需要正确配置info结构体,指定控制器的数量、每个控制器的DIMM数量以及SPD的I2C总线地址。NXP的DDR驱动库会通过I2C读取SPD信息,并自动计算并配置控制器寄存器。这种方式通用性强,更换内存条后通常无需修改代码。 -
Mock DIMM模式 (静态时序) :当板载内存是直接焊接在板上的离散内存颗粒,且没有SPD时使用。你需要手动定义一套完整的DDR时序参数结构体(
struct dimm_params),并在ddr_get_ddr_params()函数中将其传递给驱动。这些参数必须严格按照你所选用内存颗粒的数据手册(Datasheet)来填写。任何一个参数错误都可能导致内存不稳定甚至无法启动。 -
离散DDR模式 (静态寄存器值) :这是一种更底层、更直接的方式。你需要直接定义一个DDR控制器寄存器配置数组(
struct ddr_cfg_regs),并在board_static_ddr()函数中将其复制到驱动中。这些寄存器的值通常来自官方的参考配置或寄存器配置工具(如NXP的DDR配置工具)。对于LX2160A等复杂平台,还需要额外提供struct dimm_params来告知PHY驱动内存的基本拓扑信息(如位宽、Rank数等)。
3.2 代码实战:为LS1046ARDB配置离散DDR
假设我们为LS1046ARDB开发一款定制板,上面焊接了���颗镁光(Micron)的DDR4内存颗粒,组成64位总线,总容量1GB。我们采用 离散DDR模式 进行配置。
首先,在平台定义头文件
platform_def.h
中启用静态DDR配置并定义基础时钟:
#define NXP_DDRCLK_FREQ 100000000 // DDR参考时钟,通常为100MHz
#define NUM_OF_DDRC 1 // LS1046A只有一个DDR控制器
#define CONFIG_STATIC_DDR // 启用静态DDR配置宏
接着,在
ddr_init.c
中实现关键的
board_static_ddr
函数。这里的寄存器值需要从何而来?最可靠的方法是使用NXP提供的
DDR配置工具(如DDR Register Configuration Aid)
。你输入内存颗粒型号、目标频率(如1600MT/s)、PCB拓扑等信息,工具会生成一份完整的寄存器列表。以下是一个针对1600MT/s的简化示例:
#ifdef CONFIG_STATIC_DDR
const struct ddr_cfg_regs static_1600 = {
// CS0配置:使能、地址掩码等
.cs[0].config = 0x80040322,
.cs[0].bnds = 0x3F, // 假设容量1GB,计算得出的地址边界
// SDRAM配置:内存类型(DDR4)、突发长度、驱动强度等
.sdram_cfg[0] = 0xC50C0000,
.sdram_cfg[1] = 0x401100,
// 时序配置:这是核心,根据数据手册计算
.timing_cfg[0] = 0x91550018, // 包含tRAS, tWR, tRTP等
.timing_cfg[1] = 0xBBB48C42, // 包含tRFC1, tRFC2, tRFC4等
.timing_cfg[2] = 0x48C111, // 包含tWTR_S, tWTR_L等
.timing_cfg[3] = 0x10C1000, // 包含tFAW等
// 更多时序、ZQ校准、写均衡(Write Leveling)配置...
.zq_cntl = 0x8A090705,
.wrlvl_cntl[0] = 0x8675F607,
.wrlvl_cntl[1] = 0x7090807,
};
long long board_static_ddr(struct ddr_info *priv)
{
// 将静态配置拷贝到驱动内部结构体
memcpy(&priv->ddr_reg, &static_1600, sizeof(static_1600));
// 返回初始化成功的DDR总容量(单位:字节)
// 1GB = 1024*1024*1024 = 0x40000000
return 0x40000000LL;
}
#endif
关键点与避坑指南 :
-
时序参数计算
:
timing_cfg系列寄存器中的值不是随意填写的。每个字段都对应一个以时钟周期为单位的时序参数。例如,tRAS(行激活时间)可能是37.5ns,在DDR时钟周期为1.25ns (800MHz)时,换算成周期数就是30个周期。你需要根据数据手册中的AC Timing表格,逐一计算并填充到正确的寄存器位域。一个常见的错误是单位混淆(ns vs ps)或周期数计算错误。 -
写均衡(Write Leveling)
:这是DDR4/LPDDR4初始化中至关重要的一步,用于补偿时钟(CK)与数据选通(DQS)信号在PCB上的走线延迟差异。
wrlvl_cntl寄存器控制此过程。如果配置不当,会导致数据写入错误。务必参考硬件设计提供的走线长度信息,或依赖控制器自动训练的结果(部分高级驱动支持)。 -
容量计算与映射
:
cs[0].bnds寄存器定义了片选0(CS0)的地址范围。其值需要根据内存总容量和内部Bank、Row、Column的地址位宽来计算。错误的配置会导致系统只能访问部分内存,或者访问越界造成系统崩溃。 -
调试手段
:当DDR初始化失败时,BL2通常会挂起,串口无输出。此时,最有效的调试方法是使用仿真器(如JTAG)连接芯片,单步跟踪
init_ddr()函数的执行,查看在哪个具体的寄存器写入后系统死机。同时,仔细核对寄存器值与官方参考设计或配置工具的输出是否一致。
4. 使用Flexbuild构建与部署系统镜像
手动管理ATF、U-Boot、RCW等多个仓库的编译和组合非常繁琐。NXP的Flexbuild工具链正是为了解决这个问题而生,它提供了一个统一的构建环境,可以一键生成从BL2到根文件系统的所有镜像。
4.1 Flexbuild核心工作流解析
Flexbuild的核心思想是“描述即构建”。你通过一个顶层命令
flex-builder
和配置文件,指定目标机器(
-m
)、启动方式(
-b
)和要构建的组件,它就会自动完成代码拉取、配置、编译和打包。
一个典型的完整系统构建流程如下:
# 1. 设置环境
$ source setup.env
# 2. 清理旧构建(可选,但推荐)
$ flex-builder -i clean
# 3. 为特定机器构建所有镜像(包括ATF, U-Boot, Kernel, Rootfs)
$ flex-build -m ls1046ardb
这条命令会执行一系列幕后操作:根据
ls1046ardb
的配置,选择对应的RCW源码、ATF平台代码、U-Boot defconfig,编译内核,并最终生成我们需要的
bl2_<mode>.pbl
、
fip.bin
、
bootpartition.tgz
(包含内核和设备树)和根文件系统。
4.2 针对TF-A与DDR开发的定制化构建
当我们专注于修改ATF中的DDR初始化代码时,不需要每次都构建完整的系统。
场景一:仅修改并编译ATF(BL2)
# 进入ATF源码目录进行修改
$ cd packages/firmware/atf
# ... 修改 plat/nxp/soc-ls1046/ls1046ardb/ddr_init.c ...
# 使用flex-builder单独编译ATF组件
$ flex-builder -c atf
编译完成后,Flexbuild会在其输出目录(如
build/images/ls1046ardb/
)下更新
bl2_<mode>.pbl
文件。接下来,你需要将这个新的pbl文件部署到启动设备上。
场景二:构建复合固件(Combined Firmware) 复合固件是一个将RCW、BL2、BL31、U-Boot等所有启动阶段镜像打包成一个单一文件,方便一次性烧录。这在量产时非常有用。
# 为ls1046ardb构建SD卡启动的复合固件
$ flex-builder -i mkfw -m ls1046ardb -b sd
生成的固件通常命名为
ls1046ardb_sd_fw.bin
。你可以使用
dd
命令或U-Boot的
mmc write
命令将其直接写入SD卡的起始扇区。
4.3 系统镜像的部署与更新策略
将编译好的镜像烧录到目标板是最后一步。Flexbuild提供了
flex-installer
工具来简化这个过程。
全量烧录到SD卡
:
假设你的SD卡在Linux主机上识别为
/dev/sdb
。
# -b: 指定bootpartition包(含内核、设备树、可能包含ATF)
# -r: 指定根文件系统包
# -d: 指定目标设备
$ flex-installer -b bootpartition_LS_arm64_lts_5.4.tgz \
-r rootfs_lsdk2004_ubuntu_20.04_arm64.tgz \
-d /dev/sdb
注意 :这个命令会 格式化 整个SD卡并重新分区,请确保设备路径正确无误。
增量更新(网络部署) : 在开发阶段,频繁修改内核或应用时,通过网络更新效率最高。这要求目标板已运行着一个基础系统并能通过网络访问。
# 1. 在主机上,连接到目标板
$ flex-builder connect 192.168.1.100
# 2. 单独编译内核
$ flex-builder -c linux
# 3. 将新内核和模块推送到目标板
$ flex-builder push kernel 192.168.1.100
# 4. 断开连接
$ flex-builder disconnect 192.168.1.100
# 5. 重启目标板生效
这种方式只更新
/boot
分区的内容,不会影响根文件系统,速度极快。
手动烧录TF-A镜像
:
有时你需要手动将编译好的
bl2_sd.pbl
和
fip.bin
烧录到SD卡。你需要知道它们在SD卡上的精确偏移(通常由RCW决定):
# 假设bl2_sd.pbl需要写入SD卡的第8个扇区(偏移512*8=4096字节),大小为160KB
$ sudo dd if=bl2_sd.pbl of=/dev/sdb seek=8 bs=512 conv=fsync
# 假设fip.bin需要写入第2048个扇区(偏移1MB),大小为1MB
$ sudo dd if=fip.bin of=/dev/sdb seek=2048 bs=512 conv=fsync
这些偏移量信息通常可以在板级硬件手册或ATF平台的
platform_def.h
��件中找到,定义为
BL2_BASE
和
FIP_BASE
。
5. 调试技巧与常见问题排查实录
DDR初始化和TF-A启动的调试是嵌入式开发中的“硬骨头”。以下是我在实践中总结出的问题排查清单和技巧。
5.1 DDR初始化失败的症状与诊断
-
症��� :系统上电后,串口没有任何输出,或者输出少量乱码后停止。
- 排查思路 :这是最典型的DDR初始化失败症状。首先确认硬件:电源、时钟、复位信号是否正常?内存颗粒焊接是否有问题?使用示波器测量DDR参考时钟和主要信号线。
-
软件排查
:
-
检查RCW
:确认RCW配置的DDR控制器类型、数据位宽、时钟频率与硬件设计一致。一个错误的
DDRC0_PRTCL(协议)设置就会导致控制器无法工作。 -
简化配置
:在
ddr_init.c中,尝试将内存频率降到最低档(如800MT/s),关闭所有高级特性(如ECC、DBI),使用最保守的时序参数。先追求“点亮”,再优化。 -
启用调试输出
:在ATF中,编译前在
platform_def.h或common.mk中增加DEBUG=1的定义,并确保串口驱动已初始化。这样可以在init_ddr()函数中加入INFO()或NOTICE()打印,观察执行到哪一步挂掉。
-
检查RCW
:确认RCW配置的DDR控制器类型、数据位宽、时钟频率与硬件设计一致。一个错误的
-
症状 :系统能启动到U-Boot,但
bdinfo命令显示的内存容量不对,或者md(内存显示)命令访问某些地址时系统崩溃。-
排查思路
:DDR初始化部分成功,但配置有误。
-
检查容量配置
:仔细核对
board_static_ddr()函数返回的容量值,以及cs[0].bnds等地址范围寄存器。容量计算错误是常见原因。 -
检查Rank和片选
:如果是多Rank或多片选(CS)设计,确保在
info结构体或静态配置中正确设置了NUM_OF_DDRC和DDRC_NUM_DIMM(或等效配置)。漏掉一个Rank会导致只初始化了一半内存。 -
进行内存测试
:在U-Boot中,使用
mtest命令对内存进行读写测试。如果测试在特定地址失败,可能暗示地址线连接有问题或时序参数(如tFAW,tRRD)在高压高温下不稳定。
-
检查容量配置
:仔细核对
-
排查思路
:DDR初始化部分成功,但配置有误。
5.2 TF-A阶段启动卡住的问题定位
-
症状 :BL2有打印(如“NOTICE: BL2: v2.4...”),但随后停止,没有跳转到BL31的打印。
-
排查思路
:BL2成功运行,但在加载或验证FIP镜像时出错。
-
检查FIP镜像地址和大小
:确认BL2尝试加载
fip.bin的源地址(Flash偏移)和目的地址(DDR中的加载地址)是否正确。这些地址在platform_def.h中由FIP_BASE和FIP_SIZE等宏定义。 -
验证FIP镜像完整性
:在主机上使用
fiptool工具解包fip.bin,确认其中的bl31.bin和u-boot.bin是有效的、针对当前平台编译的。fiptool unpack fip.bin。 -
检查DDR初始化结果
:虽然BL2有打印,但可能DDR初始化并未完全成功,只是勉强能运行BL2的代码。尝试在
init_ddr()函数返回前,增加一个简单的DDR读写测试循环,验证DDR的可用性。
-
检查FIP镜像地址和大小
:确认BL2尝试加载
-
排查思路
:BL2成功运行,但在加载或验证FIP镜像时出错。
-
症状 :BL31有打印,但没有跳转到U-Boot。
-
排查思路
:BL31运行成功,但在准备非安全世界环境或加载BL33时出错。
-
检查BL33入口地址
:BL31会将控制权跳转到
bl33_image_ep_info结构体中定义的入口地址。确保这个地址与U-Boot链接脚本(u-boot.lds)中定义的入口点一致。 -
检查U-Boot镜像格式
:TF-A期望的
bl33.bin是纯二进制文件。确保你使用的是u-boot.bin,而不是u-boot(ELF格式)或u-boot.img。 -
查看BL31错误码
:TF-A的BL31在遇到严重错误时,可能会将错误码写入某个通用寄存器(如x0)或特定内存地址。查阅ATF源码中
report_error()函数的实现,了解如何获取和解析错误码。
-
检查BL33入口地址
:BL31会将控制权跳转到
-
排查思路
:BL31运行成功,但在准备非安全世界环境或加载BL33时出错。
5.3 实用调试工具与技巧
- JTAG仿真器 :这是最强大的底层调试工具。通过JTAG,你可以在BL1/BL2代码运行的早期阶段就设置断点,单步执行,查看和修改所有寄存器及内存内容。对于分析DDR控制器配置寄存器是否被正确写入尤其有效。
-
串口日志的极限利用
:在代码的关键路径上大量添加
NOTICE()打印。虽然会增加镜像大小,但在早期调试时是无价之宝。可以考虑将调试信息输出到一个小的、在DDR初始化前就可用的内部SRAM缓冲区,然后在后续阶段一次性打印出来。 -
版本一致性检查
:确保你使用的ATF、U-Boot、RCW源码标签(tag)是相互兼容的。混合使用不同LSDK版本的组件是导致各种诡异问题的常见根源。Flexbuild通过
repo-tag机制管理这一点,如果你手动编译,务必注意。 - 参考设计的重要性 :NXP官方评估板(RDB)的DDR配置是经过严格测试的“黄金参考”。在为自己的定制板配置DDR时,首先以对应平台的RDB配置为模板,然后根据你的硬件差异(如颗粒型号、PCB层数、走线长度)进行最小化的必要修改,这远比从零开始要稳妥。
整个TF-A启动和DDR初始化的过程,是一个对硬件和软件理解要求极高的系统工程。它没有太多的捷径,需要的是严谨的态度:仔细阅读数据手册、参考设计、源码注释,善用调试工具,以及最重要的——保持耐心,对每一个配置参数都做到心中有数。当你第一次看到自己配置的DDR成功被识别,系统顺利跳转到U-Boot命令行时,那种成就感无疑是巨大的。希望这篇结合了原理与实战的解析,能为你点亮这过程中的一盏灯。
347

被折叠的 条评论
为什么被折叠?



