尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32MP1运行时DDR容量检测:从启动链到Linux的完整方案

STM32MP1运行时DDR容量检测:从启动链到Linux的完整方案 之前有个项目硬件工程师说这批板子可能有两种DDR容量希望固件能自动识别别每次改配置。问到能不能在STM32MP1的启动阶段做一个类似PC BIOS的东西先把DDR大小检测出来再告诉Linux。当时我第一反应是——思路可以但你不能指望BootROM帮你做。ST的BootROM只负责加载FSBLDDR是FSBL通过一串参数表初始化出来的。这就带来一个很现实的问题参数表里写的容量是多少系统就认为多少它不会去数颗粒。但如果你真的想运行时检测不是不行只是这件事要做在DDR初始化成功之后并且要比想象中更小心。这篇文章会从STM32MP1的启动链路讲起说清楚为什么常规方案做不到再具体给出一个可落地的运行时检测思路——从FSBL里做地址线扫描检测到真实容量后通过设备树动态覆盖的方式交给Linux最后再聊一聊产品化时更省心的选择。适合正在做多DDR容量兼容的嵌入式工程师也适合想理解BootROM、TF-A、U-Boot如何协同工作的朋友。1. 这个问题的本质STM32MP1的“BIOS”到底在哪一层1.1 没有传统BIOS但启动链路里每个阶段都可能成为“BIOS”PC里的BIOS负责最底层的硬件初始化和自检把内存容量、存储配置等固定信息交给操作系统。STM32MP1没有这个东西它从复位到Linux起来靠的是一串接力BootROM、FSBL、SSBL再往后才是内核。BootROM固化在芯片内部上电后根据启动引脚从SD卡、eMMC或QSPI等介质里把FSBL读出来放到内部SRAM里执行。这里有个关键点BootROM本身不初始化DDR所以它加载的FSBL必须是小体积、能在SRAM里跑的。STM32MP1的SRAM有限所以第二阶段代码通常又被设计成可运行的BL2也就是TF-A的第一阶段。FSBL在这个体系里的角色特别像BIOS完成内存初始化之后跳到引导扇区那一步。它要做的事情包括初始化时钟、复位控制器、配置DDR、初始化串口然后加载下一阶段镜像。问题恰恰出在这BIOS在PC上会通过SPD信息去探测内存条有多少容量而STM32MP1的FSBL读取的是一张编译时定死的DDR配置表它不会去问颗粒“你是谁”。1.2 DDR参数表是如何“固化”容量的STM32MP1的DDR初始化通常不是U-Boot做的而是在TF-A的BL2阶段完成。ST提供了一套基于CubeMX导出的DDR参数包含DDR控制器寄存器配置、DDRPHYC时序参数、ODT阻抗、ZQ校准结果还有地址映射寄存器。这套参数会被编译成一个大结构体铺进固件里。你只要打开ST官方生成的ddr_init.c文件就能看到密密麻麻的寄存器赋值其中地址映射相关的寄存器例如ADDRMAP系列直接决定了bank、row、column的位数容量就是这些位数乘出来的。比如一个DDR3L 16bit颗粒如果row15、bank3、column10换算出来就是2Gb如果row14就是1Gb。你换了一颗更大容量的颗粒参数表里的row位数就得改否则控制器用旧地址映射去访问高地址可能直接访问到“不存在”的区域。所以常规做法是硬件定了什么颗粒固件就编译什么参数两者必须一对一。1.3 为什么标准流程不支持运行时识别第一个原因是DDR控制器初始化的顺序问题。你要做“运行时识别”前提是先有能用的DDR可DDR没有参数表就没法初始化这是一个鸡生蛋蛋生鸡的问题。你不能在完全未知的情况下靠程序自己猜出一套时序参数来。容量可以后面再猜但基础参数必须先给定。第二个原因是地址空间的安全性。如果DDR控制器的地址映射是固定死的那么程序访问一个超出实际颗粒容量的地址时行为没有统一标准。有的SoC会返回全0有的会挂起总线有的可能触发Data Abort。STM32MP1的AXI互联遇到非法DDR访问时通常不会立刻让你舒服地拿到“测试失败”的返回值反而可能把整个系统拖进异常。这也是很多人尝试做内存探测时发现代码莫名其妙跑飞的根本原因。第三个原因是产品工程上的思维惯性。大多数开发板、评估板的固件都是针对具体硬件编译的没人会预想到同一种PCB上会焊不同容量的DDR。所以ST的官方支持包里根本没有“运行时检测DDR容量”的现成代码你需要自己动手。2. 运行时检测DDR容量技术上是可行的但有前提2.1 地址线镜像测试的原理容量检测最经典的办法是“地址线镜像测试”。在DDR这类线性可寻址存储介质上如果地址线只有N根真正连到了颗粒那么超过N根地址线所在的地址空间会被镜像回低地址。举个例子假设最小容量是64MB你往0xC0000000写一个pattern A再往0xC4000000也就是64MB处写一个不同的pattern B然后去读0xC0000000。如果读到的是B说明0xC4000000是一个独立的物理地址内存容量大于64MB如果读到的还是A说明0xC4000000的访问被硬件映射到了同一个物理位置也就是说颗粒实际只有64MB。用这个思路可以从一个已知的最小容量开始指数级往上探测直到找到镜像边界。更高效的做法是用二分法把整个可能容量范围不断折半每次只测试边界点。总体来说写pattern、读回、比较三步就能判断一个边界是否存在。你需要注意的一点是测试必须写在DDR初始化成功之后否则连DDR控制器的PHY都没有进入稳定状态读回的结果就是乱码。2.2 一个前提容量可探测时序参数不能自动适配这是所有想实现该功能的人最容易忽略的地方。地址线镜像测试只能告诉你“当前DDR控制器寻址范围下实际物理颗粒覆盖到哪”它测不出颗粒的CAS延迟、tRCD、tRP、Vref等时序参数是否匹配当前配置。如果你的两块板子用的是同型号的DDR3L颗粒只是容量不同那么时序参数大概率相同地址映射不同而已。这种情况下运行时检测容量完全可行。但如果其中一块板子换成了DDR4或者换了另一个厂牌、另一组的时序参数你就算测出容量是1GB系统跑起来也可能不稳定甚至启动阶段就hang在DDR training。因此运行时检测只能解决“容量维度”的差异不能替代DDR training去做颗粒自适应。2.3 什么时候该考虑运行时检测什么时候不该用结合我自己踩过的坑适合用运行时检测的场景大概是这样的同一个硬件平台PCB上有两种BOM可选项DDR颗粒均来自同一家原厂数据位宽相同行列bank不同但工作频率和CL等核心参数一致。这种情况在储能、工控、边缘网关产品里很常见主要为了分摊不同成本档位的需求。运行时检测能把一套固件通吃两种容量省去维护多个固件版本的工作量。不适合的场景也很多需要兼容DDR3L和DDR4、需要同时兼容三家颗粒的快速迭代、或者板子设计得比较紧凑导致DDR信号质量不好需要每一颗颗粒都单独做training。这时候你再去搞运行时检测就是在给自己挖坑。前者会碰到“检测到了容量但系统还是挂”后者会掩盖硬件本身的不稳定性。产品化开发稳定压倒一切复杂的自动检测往往不如多几套固定配置来得可靠。3. 从FSBL到Linux检测结果如何传递3.1 方案一U-Boot动态修改设备树memory节点Linux内核在ARM平台上对物理内存的认知主要来自设备树里的/memory节点。设备树里会有一个类似reg 0x0 0xC0000000 0x0 0x40000000的属性表示从0xC0000000开始共1GB的DDR。U-Boot在启动内核之前通常会把设备树加载到内存中解析并调用fdt相关函数。如果我们要动态修改内存大小最好的入口就在这里。可以在U-Boot的板级初始化文件里写一个ft_board_setup函数在这个函数被调用时检查一个全局变量或者环境变量ddr_size然后用fdt_setprop去更新/memory节点的reg属性。还可以直接在U-Boot命令行里执行fdt set /memory reg 0x0 0xC0000000 0x0 0x20000000来临时验证。这样做的好处是内核完全感知不到异常它会把你覆盖后的内存大小当作硬件真实值/proc/iomem、CMA分配、DMA区划分都会跟着正确走。3.2 方案二bootargs传mem参数不推荐Linux内核自带一个启动参数memsize可以直接告诉内核物理内存的可用大小。比如在bootargs里加上mem512M内核就会忽略设备树中的内存大小只使用512MB。这个方法看起来最简单但用起来非常难受。首先mem参数会破坏内存节点和内核内存描述的一致性。很多驱动在申请DMA buffer时会依赖设备树内存布局一旦被强制覆盖可能出现虚拟地址和物理地址对应的内存区域不匹配。其次是mem在实际使用时会有“隐式预留”的副作用内核自身的内存探查逻辑会把这个参数解释成手动限制可能导致崩溃后无法正确oops。更麻烦的是这个参数对CMA、memremap等特性不够友好。所以这个方案只适合在开发板上临时验证一下U-Boot传值是否生效不建议上产线。3.3 方案三共享内存或寄存器传递如果检测动作放在TF-A里检测结果要穿过安全世界和普通世界的边界然后才能到U-Boot。STM32MP1有RTC backup寄存器可以用TF-A在运行Secure Monitor时能访问U-Boot在Non-secure世界也能通过同样的寄存器地址读取。你可以把探测到的容量值按字节写到其中一个backup寄存器U-Boot启动早期读回来再决定如何设置设备树。还有一种办法是约定一个DDR地址比如0xC0000000往上偏移64KB的某个地址把探测结果作为一个结构体放进去。U-Boot只要读到这个地址再解析就行。这种方式要求你充分了解整个启动阶段的内存使用情况如果U-Boot在跳转前覆盖了这个地址那结果就白做了。所以我更倾向于用backup寄存器干净、不占用DDR而且掉电后还能保留一段时间。3.4 三种方案的取舍针对开发效率、可靠性、维护成本三条线我整理了一个简单对照方案实现复杂度可靠性推荐程度U-Boot修改设备树memory节点中需要写板级函数高内核感知正确首选bootargs传mem参数低改环境变量即可低副作用多仅调试TF-A写backup寄存器较高跨世界传递高适合安全场景项目级可用实际在产品里最终都应该落到设备树的方式。哪怕你用了backup寄存器传递也还是要把它变成设备树的reg属性。让Linux通过标准路径感知内存是维护不折腾的正确方向。4. 在STM32MP1上实现探测的完整思路伪代码级4.1 探测代码放在哪个执行阶段推荐放在TF-A的BL2阶段也就是FSBL里。原因有两个第一此时DDR刚刚由参数表初始化完成时钟和PHY是稳定的第二这时还没有跳到U-Boot和Linux内存里的数据还没有被复杂的allocator和MMU代码污染探测动作产生的副作用最小。具体可以挂在BL2的bl2_plat_handle_post_image_load之后或者自定义一个初始化函数在DDRddr_program成功返回后调用。STM32MP1的TF-A代码里有一个stm32mp1_ddr_ops结构体ddr_program就是其中一个函数你可以在这个函数返回后插入探测逻辑。如果你对TF-A不熟退一步也可以放在U-Boot SPL阶段。但STM32MP1的标准启动里SPL不是必须的很多方案默认走TF-A所以我建议直接啃TF-A的代码后面会省很多事。4.2 探测时的访问安全和cache处理TF-A自己会开启MMU和Cache这是最容易被忽略的一个坑。你在探测DDR时如果直接用普通C语言的指针读写访问的地址可能被缓存写入pattern后立刻读回可能读的还是cache里的值而不是真实的DDR内容。这样就完全测不出镜像了。正确做法是在探测前把探测区域映射为Non-cacheable或者Device类型。TF-A有一个动态页表机制可以用mmap_add_dynamic_region把某段物理地址映射成MT_DEVICE属性。或者更粗暴一点在探测前执行data cache invalidate在每次写入之后执行clean and invalidate操作。后一种方式实现快但容易漏前一种方式更干净建议用。一个典型的探测函数伪代码如下uint64_t ddr_probe_size(uintptr_t base, uint64_t min_size, uint64_t max_size) { uintptr_t addr_low base; uintptr_t addr_high; uint64_t size min_size; uint32_t pattern_low 0xA5A5A5A5; uint32_t pattern_high 0x5A5A5A5A; while (size max_size) { addr_high base size; mmap_add_dynamic_region(base, base, size * 2, MT_NON_CACHEABLE); mmio_write_32(addr_low, pattern_low); mmio_write_32(addr_high, pattern_high); if (mmio_read_32(addr_low) pattern_high) { size 1; } else { break; } } return size; }但这只是示意真实现场要为Data Abort做保护还要考虑地址映射范围不能覆盖到外设地址。你可以先读DDR控制器的配置寄存器拿到它认为的最大可寻址范围只在这个范围里做探测不要跨越边界。4.3 把探测集成到U-Boot或TF-A的实操步骤在TF-A里完成探测后需要把结果带出来。我的建议是使用backup寄存器上一节已经提过。代码大致就是uintptr_t ddr_size_addr 0x5C00A14C; /* 以某颗STM32MP1的backup寄存器为例 */ mmio_write_32(ddr_size_addr, detected_size);然后U-Boot那边在board_init或者dram_init阶段读取同一个寄存器返回给U-Boot内存模型int dram_init(void) { uint32_t size_mb mmio_read_32(0x5C00A14C) / 0x100000; gd-ram_size size_mb * 0x100000; return 0; }这样U-Boot的bdinfo、memory命令都能直接看到正确容量。如果你的U-Boot设备树要从现有DTB中获得初始内存可以在这个函数里同时更新fdt的/memory节点或者交给后面的ft_board_setup。完整链路走下来就是TF-A探测DDR容量 - 写入backup寄存器 - U-Boot读取并更新设备树 - Linux通过设备树拿到真实内存大小。整个过程在几毫秒里完成对启动时间影响很小。4.4 验证结果如何确认内核拿到了正确容量启动Linux后第一件事执行dmesg | grep -i memory应该能看到“Memory: 512MB available”之类的字样再执行free -h确认总内存是我们预期的512MB或1GB。如果想确认设备树信息可以在U-Boot启动Linux前停在命令行执行fdt print /memory看reg属性是不是被覆盖成了正确值。我习惯写一个小脚本在内核启动后用cat /proc/iomem检查物理内存的range如果起始地址和大小都对就说明这条传递链路是通的。另外还可以在U-Boot里用md命令读backup寄存器确认TF-A确实写入了值。这几个验证点都通过这个功能才算真正完成。5. 实测中容易踩的坑5.1 访问非法地址不一定马上死但会留下隐患开发时最容易踩的坑是“看起来没死实际已经异常了”。你往一个不存在的DDR地址写数据系统可能没有立刻崩溃但总线已经回了一个错误响应。这个错误响应可能会在后续某个时刻被其他外设访问触发导致莫名其妙的稳定问题。比如初始化时探测了一段不该碰的地址后面U-Boot启动内核时突然hang住定位半天都找不到原因。所以我的习惯是缩小探测范围不要尝试扫描整个4GB地址空间而是先通过DDR控制器的address map寄存器算出实际可能的最大容量。比如配置寄存器显示row是15列是10那最大就是2Gb然后再在2Gb范围的前半段和后半段做镜像判断。范围越小误踩坑的概率越低。5.2 cache一致性探测内存时必须绕过cache我前面提过cache的问题但这里值得再强调一遍。就算你映射成了Non-cacheable也还要确认自己的读写操作是真正落到DDR上的。ARM Cortex-A7对Device类型的访问是禁止写入合并的所以用mmio写函数能保证每次访问都走AXI总线。但你如果顺手开了编译器优化普通指针访问可能会被缓存到寄存器那就白测了。用volatile关键字或者直接用ST提供的mmio_write_32/mmio_read_32函数可以避免这个问题。5.3 TF-A传递信息给U-Boot的约定当你选择用backup寄存器传递时一定要确认U-Boot的代码路径里没有在读取前重置寄存器。有些调试过程中用户会手动执行reset命令这时backup寄存器可能会被清零。另外如果产品支持standby低功耗模式backup域一般是持续供电的但在深度睡眠或掉电模式下不一定可靠。最好在U-Boot启动时加一个magic number判断比如先写入一个固定值0xDDB1读取时校验它防止读到残留垃圾数据。若选择共享DDR地址传递需要关注TF-A的内存隔离配置。开启TrustZone后部分DDR区域会被标记为SecureNon-secure的U-Boot读不到。所以共享区要么放在Non-secure DDR要么你在TF-A里通过配置把这块区域属性改成Non-secure。否则U-Boot读到的全是零问题更隐蔽。5.4 安全生命周期和签名启动带来的限制STM32MP1支持安全启动闭源产品通常在烧录时让芯片进入TrustZone模式并且所有启动镜像都会被校验和签名。你在TF-A里加了探测代码就意味着整个BL2镜像都要重新编译、重新签名而且需要确保新的镜像没有破坏安全边界。否则芯片可能直接拒绝启动。如果开发阶段先不关心安全把get_boot_mode和生命周期相关开关都配成Open状态可以跳过签名校验。但量产时需要走一遍完整的签名和烧录流程。我的建议是在做运行时检测功能的同时把签名脚本也搭好避免后面产品化时因为镜像校验失败来回折腾。6. 如果不做运行时检测产品化还有哪些务实方案6.1 多设备树以形如board-variant的方式加载其实很多产品最终都走这条路各自编译512MB和1GB两套设备树U-Boot启动早期检查某个GPIO拉高拉低然后选择不同的DTB文件加载。这个方案不需要改DDR初始化代码也不碰内存探测逻辑稳定性最高。需要做的只是在U-Boot环境里增加一个变量比如board_variant通过board_late_init设置然后bootcmd根据这个变量选择fdtfile...。权衡的地方在于要维护两份设备树未来DDR配置变了两份都得同步更新。但如果项目本身已经有多套硬件变体多一份并不多多少成本可靠性却高很多。6.2 硬件配置引脚选择在PCB上预留1~2根配置电阻上电后通过GPIO电平决定当前是哪一种DDR容量。这是最简单粗暴的方案硬件工程师只需把一颗电阻变更一下固件完全不用改动。唯一需要注意的是GPIO所在bank在上电瞬间的状态要确保在U-Boot读取时电平已经稳定。有的方案也会用拨码开关代替固定电阻方便开发阶段切换测试。我个人建议在客户现场尽量用贴片电阻防止用户随意拨动导致选错容量进而引发稳定性问题。6.3 eFUSE或OTP区域固化容量标识STM32MP1本身有OTPOne-Time Programmable区域可以在烧录步骤中写入一个标识比如容量等级、内存类型、颗粒厂商。BL2启动时读取OTP然后根据标识选择对应的DDR参数表。这种方法的好处是自动化程度高且不容易被误操作改掉。代价是OTP一旦写入就不能再改烧录顺序和NXP的i.MX fuse方案类似需要严格设计和验证。6.4 启动后检查内存容量并保留冗余设计如果有人硬件已经量产了才发现容量选错也可以通过软件补救。做法是固定按最大容量配置DDR控制器但内核启动后检查真实可用内存用CMA或memmapnnKssK参数把不可用的区域预留掉。这不算自动检测更多是让系统“带伤运行”能临时恢复功能但浪费地址空间且对DDR控制器的压力更大。从本质上说运行时检测是一个“看起来很美”的功能但它并没有绕过DDR初始化必须参数化这个事实。产品化时更值得投入精力的是让参数配置和硬件识别的层次更清晰而不是写一段很酷的探测代码。所以回到标题里的问题STM32MP1能不能做类似BIOS的运行时DDR检测并传给Linux能做但它是有限定条件的。我的建议是先确认你要适配的DDR颗粒之间是否只有容量差如果是再去啃TF-A那部分代码如果连颗粒品牌和时序都可能变就别在运行时检测上耗时间了老老实实做多套配置加硬件识别产线和运维都会感谢你。
返回列表