第4章:BSP启动阶段内存初始化(Boot ROM → SPL → U-Boot)
车载SoC上电后,内存子系统还是一片混沌。DDR控制器没配、MMU没开、页表没建——这时候CPU连局部变量都不敢声明。我见过不少刚入行的工程师,上来就写内存操作,结果板子直接挂死在启动阶段。说白了,你得先让内存"活过来"。
这一章,咱们就拆解从Boot ROM到SPL再到U-Boot这条链路上,DDRC初始化和MMU临时页表建立的关键步骤。嗯,这里面的坑,我踩过不少。
4.1 启动流程概览:三段式接力
车载SoC的启动,不是一步到位的。它分三个阶段,像接力赛一样传递控制权:
| 阶段 | 存储介质 | 主要任务 | 内存状态 |
|---|---|---|---|
| Boot ROM | 片上ROM(不可改) | 加载SPL到SRAM,验签 | DDR未初始化,仅用SRAM |
| SPL | 片上SRAM | 初始化DDRC,加载U-Boot | DDR刚配好,MMU仍关闭 |
| U-Boot | DDR | 建立页表,开启MMU,加载内核 | DDR就绪,MMU使能 |
你想想看,Boot ROM只有几十KB的片上SRAM可用,连个像样的内存管理都做不了。所以它只干一件事:把SPL从Flash里搬进SRAM,验签通过后跳转。SPL呢,它跑在SRAM里,但它的使命是初始化DDR控制器——这一步搞不定,后面全白搭。
关键认知:SPL是DDR初始化的"拓荒者"。它必须在SRAM的狭小空间内完成DDRC的时序配置、ZQ校准、DQS门控训练。我早期做的一个项目,SPL代码超过SRAM容量,硬是优化到只剩32KB——每一行汇编都得精打细算。
4.2 DDRC初始化:让内存"活过来"
DDR控制器初始化,说白了就是告诉DDR颗粒:咱们用什么频率、什么时序、什么模式通信。这个过程我习惯分成四步:
- 时钟与复位——给DDR PHY和控制器供时钟,释放复位
- PHY初始化——配置DQS门控、ZQ校准、ODT阻抗
- 控制器配置——设置时序参数(tRCD、tCL、tRP等)、内存映射
- 训练与验证——写读测试,确保数据线无误
我曾经在一个项目中,DDRC初始化后读写总是不稳定。查了三天,发现是DQS门控的延迟窗口没调对——颗粒的DQS信号比时钟早了半个周期。嗯,这种问题示波器看不出来,得靠DDR PHY内部的训练结果寄存器。
4.2.1 时序参数配置示例
以LPDDR4为例,SPL里配置DDRC的代码大概长这样:
/* 配置DDRC时序寄存器 */
writel(0x001A001A, DDRC_BASE + TIMING_1); /* tRCD=26, tRP=26 */
writel(0x00320032, DDRC_BASE + TIMING_2); /* tRAS=50, tWR=50 */
writel(0x000F000F, DDRC_BASE + TIMING_3); /* tRFC=15, tFAW=15 */
/* 配置内存映射:4GB地址空间,2个rank */
writel(0x00000003, DDRC_BASE + ADDR_MAP); /* rank=2, bank=8, col=10 */
/* 启动DDRC */
writel(0x00000001, DDRC_BASE + CTRL); /* 使能控制器 */
注意:这些时序参数必须与DDR颗粒的数据手册严格对应。我见过有人直接复制参考板的配置,结果板子跑在低温下就挂——因为温度变化会影响DDR时序裕量。建议在SPL阶段做一次温度补偿校准。
4.3 MMU临时页表建立:给CPU一张"地图"
DDR初始化完成后,U-Boot就可以在DDR里跑了。但这时候MMU还是关闭的——CPU直接物理寻址。为什么要开MMU?说白了,为了性能和安全。车载SoC里,外设地址空间和DDR地址空间经常重叠,不开MMU没法做地址隔离。
U-Boot启动早期,会建立一套临时页表。我习惯用一级页表(L1),因为简单、快速。ARMv8架构下,页表基地址放在TTBR0_EL2寄存器里。
4.3.1 页表建立步骤
- 清零页表内存——确保所有表项初始为无效
- 映射DDR区域——设置为普通内存,可缓存、可写
- 映射外设区域——设置为设备内存,不可缓存、不可推测
- 设置页表基地址——写TTBR0_EL2
- 使能MMU——置位SCTLR_EL2的M位
你想想看,如果外设区域被映射成可缓存的,CPU读UART状态寄存器时可能读到缓存里的旧值——这bug查起来能让人崩溃。我早期就踩过这个坑,UART轮询死活收不到数据,最后发现是MMU把UART地址映射成了普通内存。
4.3.2 临时页表示例代码
/* 页表基地址,对齐到4KB */
#define PAGE_TABLE_BASE 0x80000000
void setup_page_table(void)
{
uint64_t *pte = (uint64_t *)PAGE_TABLE_BASE;
int i;
/* 清零所有表项 */
for (i = 0; i < 512; i++)
pte[i] = 0;
/* 映射DDR: 0x80000000 - 0xFFFFFFFF */
/* 属性: 普通内存, 可缓存, 可写, 可执行 */
for (i = 0x800; i < 0x1000; i++) {
pte[i] = (i << 21) | 0x701; /* 1GB块映射 */
}
/* 映射外设: 0x40000000 - 0x4FFFFFFF */
/* 属性: 设备内存, 不可缓存, 不可推测 */
for (i = 0x400; i < 0x500; i++) {
pte[i] = (i << 21) | 0x401; /* 设备内存属性 */
}
/* 设置页表基地址 */
asm volatile("msr ttbr0_el2, %0" : : "r" (PAGE_TABLE_BASE));
/* 使能MMU */
uint64_t sctlr;
asm volatile("mrs %0, sctlr_el2" : "=r" (sctlr));
sctlr |= 0x1; /* 置位M位 */
asm volatile("msr sctlr_el2, %0" : : "r" (sctlr));
}
个人经验:临时页表不要映射整个4GB地址空间。只映射U-Boot需要的DDR区域和必要外设,能减少页表大小。我一般只映射512MB DDR + 256MB外设,页表只占4KB,刚好一个页面。
4.4 核心流程图:启动阶段内存初始化
下面这张图,把整个流程串起来了。我建议你对照着看代码,理解每个阶段的内存状态变化。
4.5 避坑指南:我踩过的三个坑
做BSP这么多年,内存初始化这块的坑,我基本都踩了一遍。挑三个典型的说说:
- DDRC时序参数写错——有一次我把tRCD和tRP的值写反了,板子在常温下能跑,一进高低温箱就挂。后来我养成了习惯:每个时序参数都加注释,标明数据手册的页码。
- 页表映射覆盖了Boot ROM区域——U-Boot里建立页表时,我不小心把0x00000000开始的区域映射成了普通内存。结果Boot ROM的中断向量表被覆盖,系统一触发异常就死机。嗯,这个bug查了我两天。
- MMU使能后TLB没刷——修改页表后忘了执行TLB失效操作,CPU拿着旧的页表缓存继续跑。我建议每次修改页表后,都执行一次
tlbi vmalle1指令。
总结一下:DDRC初始化是SPL的核心使命,MMU页表建立是U-Boot的早期关键任务。这两个环节出问题,系统连内核都看不到。我个人的习惯是,在SPL里加一段DDR读写测试,在U-Boot里加一段页表验证——确保每一步都走稳了,再往下走。