ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SSD主控上电后DDR要初始化哪些数据结构?启动流程拆解

SSD主控上电后DDR要初始化哪些数据结构?启动流程拆解 聊一个看着有点硬核、但做 SSD 主控固件的人都绕不开的问题主控上电之后DDR 里到底要先初始化哪些数据结构才能正常操作 NAND、响应主机的读写我第一次被问到这个问题是在一次面试里对方以为我在考链表和哈希的八股其实我想听的是他从上电到 Ready 的完整视角。后来带人做固件 Bring-up我发现很多人栽在同一个地方知道要初始化 L2P、坏块表这些名词但不知道每张表为什么必须在 DDR、占多大空间、加载要多久。这篇文章就把这套启动期数据结构的“全家福”拆开讲清楚。我以一块典型的 1TB 消费级 TLC SSD 作为参考对象规模尽量给到具体数字耗时给到可参考的数量级。适合刚接触 SSD 固件开发、做主控验证或者想弄明白 SSD 为什么开机快慢不一的同行阅读。1. 从主控上电到固件接管DDR 在启动链路上扮演的角色1.1 上电后的前十几毫秒其实不在 DDR主控加电后真正第一个跑起来的不是你的主固件而是一小段固化在芯片内部的 Boot ROM。这段代码通常只有几十 KB能干的事情非常有限配 PLL 时钟、配复位信号、初始化必要的 GPIO 和 UART然后把第二段启动代码从 SPI NOR Flash 或者 NAND 的固定启动分区读进内部 SRAM。为什么第一步一定要放在 SRAM而不是 DDR因为 DDR PHY 此时还没有做任何训练DDR 控制器不知道读写时序该怎么定DQS 和 DQ 之间的相位关系也没校准代码放到 DDR 里根本没法定向执行。所以在这一阶段DDR 里的数据结构可以说是“完全不存在”的你能用的只有主控内部那几百 KB 甚至只有几十 KB 的草稿纸。这一段看起来不起眼但它决定了一个很实际的约束Boot ROM 阶段能加载的第二级引导程序体积非常有限所有 DDR 初始化参数、主固件头信息、NAND 时序参数都必须在这个尺寸限制内打包。我见过不少启动镜像因为往 SPL 里塞了太多调试信息导致超过内部 SRAM 容量编译过了但跑起来就踩内存这是很典型的低级坑。1.2 DDR 训练完成固件才算真正“进场”Boot ROM 把第二级引导程序跑起来之后才开始做 DDR 的 PHY 初始化和训练。这一步不是软件工程师眼里单纯配几个寄存器它包含频率设置、ZQ 校准、ODT 配置、DQS/DQ 的 delay line 校准、读写眼图扫描等一整套操作。固件会往 DDR 里写测试图案再读回来比对目的是找到每个 bit lane 的采样窗口中心确保在温度和电压波动下依然能稳定读写。这一步的耗时通常有几十毫秒而且和温度关系很大。冷机状态下的训练时间往往比热机要长如果硬件布线质量差还会出现训练失败需要重试的情况。很多刚接触主控的同事不理解为什么同一个镜像放在不同板卡上启动速度不一样其实差异经常就出在 DDR 训练的重试次数上。DDR 训练通过之后主固件镜像才能被加载进 DDR全局变量、BSS 段、堆栈也才可以放到大容量内存里。到了这一步固件才算真正“进场”后续那些 FTL 表、坏块表、日志缓冲区才开始在 DDR 里安家。1.3 为什么固件和 FTL 表非要抢着住进 DDR你可以把 DDR 理解成固件的工作台。FTL 需要维护全盘逻辑地址到物理地址的映射这个表如果放在 NAND 里每来一条读命令都要先读 NAND 拿映射结果延迟直接从微秒级恶化到几百微秒级随机性能直接归零。所以映射表必须常驻 DDR这是 SSD 设计里绕不开的铁律。至于为什么不用 SRAM 替代原因更简单成本。SRAM 密度低、价格贵一颗主控里能放下的 SRAM 撑死几十 MB想装下 1TB 盘的完整映射表完全不现实。所以大容量 SSD 普遍外挂 DDR 颗粒主控内部 SRAM 只保留最关键的启动代码和少量热数据缓存。2. 常驻 DDR 的数据结构我习惯分成四类来记如果你在面试时被问到“启动时要初始化哪些数据结构”别像背八股一样从数组链表开始数。更好的思路是按职责分四类映射类、NAND 管理类、运行期缓存类、掉电恢复类。前两类负责回答“数据在哪”后两类负责回答“怎么跑得快”和“断电了怎么办”。2.1 映射类L2P 表最重P2L 表最容易被忽略映射类里最核心的当然是 L2P 表也就是 Logical-to-Physical 映射表。这个表的每一项把主机发来的 LBA 映射到 NAND 上的物理位置包括 Channel、LUN、Block、Page有的实现还会带页内偏移。常见表项大小是 4 字节映射粒度通常取 4KB、8KB 或者 16KB。粒度越小随机写性能越好但表越大粒度越大表越小但小文件场景会吃亏。这个trade-off后面算规模时会看得更直观。L2P 的初始化不是简单地把内存清零而是要把它恢复成和 NAND 上数据一致的状态。正常启动时固件会从 NAND 上的 checkpoint 区域把 L2P 读回内存再配合日志做增量回放。如果 checkpoint 和日志都没了那就只能整盘扫描重建耗时几十秒甚至几分钟这通常只作为灾难恢复路径存在。和 L2P 相对的是 P2L 反查表也就是从物理页位置反查它存的是哪个 LBA。GC 搬运数据、磨损均衡、掉电恢复都需要它。P2L 的特点是可以按 Block 懒加载不用整张表一次性全建好所以它的常驻内存压力比 L2P 小很多。但如果你在设计阶段把它漏了等到调 GC 和掉电恢复时再补代价会相当大。2.2 NAND 管理类坏块表、块状态表、磨损计数这一类数据结构负责记录 NAND 本身的健康状态最典型的是 BBT 坏块表。NAND 出厂就有坏块使用过程中还会持续产生新坏块所以固件必须在 DDR 里维护一份运行时的坏块 bitmap。它的体量不大按每个 Die 一个 bitmap 来算通常只有几十 KB 到几百 KB但访问频率很高不能每次都去读 NAND 原始坏块标记。因为块状态的多样性固件还要为每个物理 Block 维护状态机常见状态包括 Free、Open、Closed、Bad、Retired 等。同时每个 Block 还要记录磨损计数也就是 Erase Count用于磨损均衡算法决策。这里容易有个误解有人以为 1TB 盘的物理块数量很大管理表会占用大量内存。实际算一下1TB 用户容量加上预留空间物理容量大概在 1.1TB 到 1.2TB 左右按一颗消费级 TLC 的 Block 大小 12MB 到 14MB 估算物理 Block 数量也就在 10 万个上下每个 Block 用 4 到 8 字节管理整体也就 1MB 上下。真正占内存的是 Open Block 的页级管理。固件要为正在写入的物理块维护哪些页已经写过、哪些页还没写的 bitmap这种结构在多个通道并行写入时会成倍增长但量级也就几 MB 到十几 MB和 L2P 相比依然是小头。2.3 运行期类写缓存、GC 缓冲、DMA 描述符、命令上下文这一类数据结构不是 FTL 的核心逻辑表但没有它们主机命令根本跑不起来。写缓存是其中最显眼的大块头。消费级 SSD 的写缓存通常要能容纳若干个 Super Block 的数据条带普遍配置在 16MB 到 64MB 之间。它的作用是把主机的小块随机写先攒成大块连续写再统一刷到 NAND。GC 缓冲和写缓存类似垃圾回收时要把搬移的数据暂存在 DDR 里空间需求也不小。除了这两块每个通道的 DMA 描述符环形缓冲、中断状态寄存器影子、NVMe SQ/CQ 的上下文、MMIO Doorbell 的影子备份都属于运行期必须常驻的结构。它们的单个体量不大但数量多加起来也有 2MB 到 8MB。这一类结构的初始化动作主要集中在启动阶段建链表、清计数、配置 DMA 引擎的基地址、把队列头尾指针复位。它们的耗时通常不是问题问题在于初始化顺序。如果 DMA 描述符指向的内存还没准备好或者中断向量表没建立后面任何一个外设中断都可能导致固件跑飞。2.4 掉电恢复类日志、checkpoint、GC 状态这才是隐藏的大头掉电恢复类结构是启动初始化里最容易被低估的部分。L2P 表在 DDR 里是易失的突然掉电后内存内容全部丢失固件必须靠 NAND 上持久化的日志和 checkpoint 来恢复映射关系。启动时固件要把日志区域读入 DDR建立回放缓冲区一条一条应用日志里的写记录最终把 L2P 补到最新状态。日志区在 DDR 里占的空间通常是 16MB 到 64MB具体取决于主控支持的掉电保护电容容量、NAND 写带宽和主机队列深度。日志太大浪费 DDR太小又限制掉电场景下的恢复窗口。除此之外GC 进度、磨损均衡状态、甚至部分安全引擎的密钥上下文也需要在启动时恢复到内存。GC 恢复的逻辑尤其麻烦因为搬运了一半的数据块不能简单当作不存在也不能当作已经完成必须根据日志精确判断哪些页已经搬走、哪些页还留在原地。这一类恢复代码往往是整个启动流程里最复杂、最容易出 bug 的部分。3. 规模到底多大用一块 1TB 消费盘把账算明白聊完分类我们上具体数字。假设场景是一块 1TB 用户容量的消费级 TLC SSD外挂 DDR4 颗粒Op 预留按 7% 到 15%物理 NAND 容量在 1.1TB 到 1.2TB 左右。3.1 L2P 表没有玄学就是一个乘法L2P 表的大小取决于两个参数映射粒度、每个表项字节数。1TB 用户容量相当于 1,099,511,627,776 字节。如果映射粒度是 4KB那么 LBA 数量就是 1TB 除以 4KB等于 268,435,456 个每个表项 4 字节算下来正好 1GiB。这个数字对于消费级主控来说太大了所以必须在粒度上做取舍。映射粒度表项数量每项大小L2P 总大小4KB268,435,4564B1024MiB8KB134,217,7284B512MiB16KB67,108,8644B256MiB这就是为什么你会看到同样标称 1TB 的 SSD有的配 512MB DDR有的配 1GB DDR。配 1GB DDR 的盘更倾向于用 4KB 或 8KB 映射粒度随机小写性能更好配 512MB DDR 的盘通常把映射粒度提到 16KB或者对 L2P 做压缩和分级管理。不能说哪种一定更好它本质上是成本、性能和固件复杂度的三方平衡。3.2 除 L2P 外其它结构的占用量级L2P 并不是唯一需要算账的项。我把典型 1TB 消费盘启动后的 DDR 内存分布列出来你可以直接参考数据结构估算依据内存量级L2P 映射表16KB 粒度和 4B 表项256MBP2L 反查表按 Block 懒加载均值1~4MBBBT 坏块表每 Die bitmap 加索引0.5~1MB块状态磨损计数约 10 万个物理 Block0.5~2MB写缓存2~4 个 Super Block16~64MBGC 缓冲垃圾回收搬移暂存16~32MB掉电日志区日志回放缓冲区16~64MB固件镜像堆栈代码段、只读数据、堆8~16MBNVMe/DMA 上下文队列、描述符、影子门铃2~8MB把这些加起来一个比较典型的 1TB 消费级方案大概会吃掉 300MB 到 410MB 的内存刚好落在单颗 512MB DDR4 颗粒的可用范围里。如果你把 L2P 粒度降到 8KB总内存需求就接近 600MB那就必须上 1GB 颗粒成本会明显上升。需要提醒的是L2P 大小只跟主机可见的 LBA 数量有关跟 OP 预留多少没关系。NAND 物理容量增加只会影响坏块表、块状态表这些按物理 Block 数量计算的结构但 Block 数量的量级摆在那里增加 20% 也就多几百 KB完全不是瓶颈。3.3 企业盘为什么动不动配 2GB 甚至更大 DDR消费盘可以用 16KB 粒度压内存企业盘为什么很少这么做因为企业盘的工作负载里4KB 随机读和随机写占比极高映射粒度太粗会导致一个映射项覆盖 16KB 地址范围当主机只读其中 4KB 时固件要么把 16KB 都取出来要么做额外的前后台拆分逻辑性能损耗很难看。所以企业级方案宁可把 DDR 加到大也要保住 4KB 映射粒度。另一个原因是企业盘的掉电恢复要求更严格。日志区必须够大才能保证在掉电保护电容放电时间内所有未落盘的数据都能留下完整痕迹。日志区一扩DDR 需求自然就上去了。所以“DDR 多大算够”没有一个固定答案它是由逻辑容量、映射粒度、性能目标、掉电恢复需求共同决定的。4. 耗时都耗在哪了典型启动过程拆解规模算完之后再看耗时。很多人以为 SSD 启动慢是因为固件镜像太大其实固件加载在启动总时间里通常不是大头真正的大头几乎永远是映射表的加载和恢复。4.1 一条典型的时间轴阶段典型耗时说明电源稳定、PLL 锁定1~5ms硬件上电时序Boot ROM 执行、加载 SPL5~20ms从 SPI NOR 或 NAND 读入DDR PHY 初始化与训练20~100ms冷机可能更慢主固件镜像加载10~100ms从 NAND 备份区读取时更长NAND 基础元数据加载1~10msBBT、几何参数、坏块表日志加载与 L2P 恢复100~2000ms通常是最大头NVMe/主机接口初始化50~200ms可与恢复阶段部分重叠从这张表能看出来启动时间从几百毫秒到两三秒的差距主要来自日志加载与 L2P 恢复这一段。这也是固件优化最该花力气的地方。4.2 L2P 恢复为什么这么慢假设 L2P 压缩后的 checkpoint 大小是 256MB固件从 NAND 以并行多通道读取实际带宽假如能做到 800MB/s 到 1GB/s单纯把 checkpoint 读完就要 250ms 到 320ms。如果 checkpoint 是 512MB时间直接翻倍。读完还要校验每页 CRC再和内存里的 L2P 主表做合并。更麻烦的是掉电日志回放。如果上次是干净关机日志区里基本是空的回放很快如果上次是突然掉电日志区里可能积累了最近几秒甚至更长时间的所有写记录回放一条一条翻译成 P2L 更新再写进 L2P这就不是线性读能解决的了。极端情况下一个没优化好的掉电恢复路径可以把启动时间拖到几秒甚至更久。这里插一句个人经验我见过一个项目把启动时间从 2.4 秒优化到 0.5 秒以内核心动作其实是三个——压缩 checkpoint 大小、把日志恢复改成增量回放、把 checkpoint 加载改成多通道流水线。任何一条单拎出来都不算高深但合在一起效果非常明显。4.3 主机留给你的时间窗口没那么宽虽然 NVMe 规范允许控制器在 Reset 后有一段很长的准备时间但真实系统里你根本不可能按那个上限去耗。现代操作系统和硬件 Watchdog 对存储设备的上电超时判定越来越严格企业级存储甚至会把超时时间压到几百毫秒到一两秒。消费级虽然宽松一些但这个时间太大也会影响整机启动体验和评测数据。所以作为固件工程师最需要盯的两个统计值就是干净关机后的 Ready 时间以及异常掉电后的 Ready 时间。前者代表正常体验后者代表最坏情况。只优化前者、忽略后者是很多启动优化项目后期返工的原因。5. 初始化踩坑和优化经验最后分享几个我在实际项目里反复踩过、也看别人踩过的坑。这些内容不一定写在芯片手册里但遇到一次就能让你记住很久。5.1 大表不要一上来就 memsetDDR 上电后的内容是完全随机的理论上任何一块内存要使用前都应该初始化。但如果你拿到一个 256MB 的 L2P buffer二话不说先 memset 清零这 80ms 就出去了。按 DDR 写带宽 3.2GB/s 算清 256MB 需要 80ms 左右对于理想目标 500ms 启动时间来说这 80ms 占了六分之一纯粹是浪费。更聪明的做法是给大表做“按需初始化”。像 L2P 这种表既然启动后会从 checkpoint 恢复那么 checkpoint 覆盖到的表项会被读取覆盖真正需要额外清零的只是 checkpoint 没盖住的部分。可以用一个很小的 ownership bitmap 记录哪些表项区域已经有效启动时只清这张小图开销从 80ms 降到了几毫秒。写数据时再按 bitmap 决定是否需要先补零。这种“惰性初始化”思路在启动路径上非常管用。5.2 DDR 训练失败别只报个错就死循环DDR 训练不是每次都能一次通过的。低温、电压波动、板级走线串扰都可能导致训练结果落在采样窗口边缘甚至直接失败。传统做法是失败后重试但如果你重试逻辑写得太简单比如原地无限循环那生产测试阶段会卡死在一批“低温不太稳”的板卡上无法自动筛选。我建议至少在启动流程里加入两档降级第一档保持原频率重试两三次第二档降到低速 DDR 模式牺牲一点启动性能换取稳定。同时把失败原因写到专门的 boot log 分区里方便产线和固件团队追溯问题。看到这里你可能会觉得这是硬件问题、不是软件问题但在量产项目中这种边缘条件下的处理策略恰恰是固件质量的一部分。5.3 加载表一定要设计成可流式、可校验L2P checkpoint 可能分布在多个 NAND Channel 上加载时天然适合并行。但很多第一次写的 loader 会犯一个错误先等整张表都搬完再做 CRC 校验发现错了再整张重来。这在小表上问题不大在 256MB 的大表上会浪费非常多时间。正确做法是把 checkpoint 按固定大小分片每个分片带独立的 CRC 或序列号。固件从不同 Channel 流水线读取分片每读完一片就校验一片失败只重读那一片。这样既利用了 NAND 多通道带宽又避免了单页损坏导致整表重读。我在调启动性能时经常先拿这个并行度看有没有跑满很多时候光靠调整 DMA 描述符的排布就能省下 30% 到 50% 的加载时间。5.4 能重叠的初始化就别串行DDR 训练看上去很独立但它开始之前有些 PLL 和电源管理动作其实是可以在 Boot ROM 阶段就准备好的。同样NAND 控制器的初始化也没必要等 DDR 训练完成后再做因为 NAND 的时序参数在 SPL 阶段就能拿到手。Boot ROM 完全可以先把 NAND 控制器初始化好等 DDR Ready 后直接发起固件 DMA而不是在那里干等。有些主控支持 DDR PHY training 和 SPL 阶段的外部设备枚举并行执行能省下 20 到 30ms。这类优化看起来不大但在和竞品比启动时间时20ms 可能就是一个档次的差距。每次我在优化启动时间时最后都会回到同一句话DDR 里的表不能只想着“能用”还要想清楚“怎么被加载、怎么被恢复、怎么被清账”。把这三个问题想透了启动时间自然不会太难看。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进