1. SSD固件架构全景:FTL层核心职责、Host接口层、NAND闪存管理层、掉电保护与数据路径

各位同学,咱们今天聊点实在的。

SSD固件,说白了就是一块硬盘的“大脑”。没有它,NAND Flash就是一堆会漏电的存储单元。我做了十几年固件,见过太多“硬件很强、固件拉胯”的项目。嗯,今天咱们就把SSD固件的骨架搭起来。

1.1 固件架构的整体视图

先看一张图,这是我个人习惯的架构分层方式。你想想看,SSD固件其实就干三件事:接请求、管映射、控闪存

SSD固件架构全景图 Host接口层 NVMe协议解析 / SATA命令处理 / PCIe DMA引擎 FTL层(闪存转换层) 逻辑地址→物理地址映射 / GC垃圾回收 / WL磨损均衡 坏块管理 / 读干扰处理 / 数据保持 NAND闪存管理层 多Plane操作 / 闪存命令调度 / 时序控制 ECC纠错 / Read Retry / Vth电压调整 掉电保护与数据路径 PLP电容管理 / 脏数据回写 / 原子写保证 全局资源管理:缓存管理 / 电源管理 / 温度监控 / 健康度统计 Host端 闪存端

这张图我建议你存下来。每次做固件开发,先想想自己当前工作在哪个层级。我在海力士带团队时,新人最容易犯的错误就是——在FTL层里折腾NAND的时序,或者在Host接口层里做GC调度。层级乱了,代码就乱了。

1.2 FTL层核心职责

FTL,全称Flash Translation Layer。说白了就是一张“地址翻译表”。

Host发来一个读命令,逻辑地址是LBA 100。FTL要回答一个问题:这个数据到底躺在NAND的哪个物理位置?

FTL的核心职责清单:

  • 地址映射:LBA → PBA,这是基本功。映射粒度可以是4KB、8KB甚至更大。
  • 垃圾回收(GC):NAND不能覆盖写,只能先擦除再写。GC就是“搬数据、腾空间”的苦力活。
  • 磨损均衡(WL):每个Block有擦除寿命上限。WL让所有Block“雨露均沾”。
  • 坏块管理:出厂坏块和运行时产生的坏块,FTL要标记并替换。
  • 读干扰与数据保持:读多了会干扰邻居,放久了会漏电。FTL要定期“刷新”。

我遇到过最头疼的事,就是GC和Host读写抢带宽。你想想看,Host正在疯狂写数据,GC也在后台搬数据,两个都在抢闪存通道。怎么调度?嗯,这里有个经验:GC的优先级要动态调整。Host空闲时GC跑快点,Host忙时GC让路。

避坑指南:

我曾经在一个项目里把GC触发阈值设得太低,结果SSD刚上电就开始GC,Host写延迟直接飙到100ms以上。后来改成“空闲窗口检测 + 动态阈值”,问题才解决。

1.3 Host接口层(NVMe/SATA)

Host接口层,就是SSD和CPU对话的“翻译官”。

NVMe和SATA,本质上是两套不同的“语言”。SATA走的是AHCI协议,队列深度只有1(NCQ可以到32,但效率依然有限)。NVMe走的是PCIe,队列深度可以到64K,而且每个队列独立中断。

特性 NVMe SATA
协议基础 PCIe + NVMe指令集 AHCI + ATA指令集
队列深度 64K队列,每队列64K命令 1队列(NCQ最多32命令)
命令提交方式 Submission Queue + Completion Queue 寄存器映射(PIO/DMA)
中断机制 MSI-X,每个队列独立中断 单中断线共享
典型延迟 ~10μs ~100μs

我个人习惯在NVMe驱动里重点关注SQ/CQ的轮询机制。很多固件工程师喜欢用中断,但中断在高IOPS场景下反而成了瓶颈。我做过一个测试:在100万IOPS下,纯中断模式CPU占用率高达40%,换成混合轮询后降到5%。

注意:

SATA虽然老,但在工业级和嵌入式领域依然大量存在。不要因为它“慢”就轻视。SATA的NCQ乱序执行,处理不好会导致数据一致性bug。我见过一个案例:NCQ命令重排序后,写完成通知发早了,Host以为数据落盘了,结果掉电丢了数据。

1.4 NAND闪存管理层

这一层,是固件里最“硬核”的部分。你想想看,NAND Flash本身是个“不靠谱”的存储介质——它会漏电、会读干扰、会写失败、会磨损。

NAND管理层的核心任务,就是把这些“不靠谱”包装成“靠谱”。

  • 多Plane操作:现代NAND有多个Plane,可以并行读写。我建议把Plane看作“小通道”,尽量让命令在多个Plane上并发执行。
  • 闪存命令调度:NAND命令有严格的时序要求。比如,读命令发出后,tR时间(读延迟)内不能发其他命令。调度器要利用这个“等待时间”做点别的事。
  • ECC纠错:NAND的原始误码率(RBER)越来越高。LDPC纠错已经是标配。我遇到过最极端的情况:一个Page读出来有200多个bit错误,LDPC硬解码失败,还得上软解码。
  • Read Retry与Vth调整:读失败时,不要直接标记坏块。先试试Read Retry,调整读取电压阈值。很多时候能救回来。

一个实用的NAND命令调度伪代码:

// NAND命令调度器核心逻辑
while (1) {
    cmd = get_next_cmd_from_queue();
    if (cmd->type == READ) {
        send_read_cmd(cmd);
        // 在tR等待期间,处理其他Plane的命令
        while (!is_read_complete()) {
            process_other_plane_cmds();
        }
        read_data_from_buffer(cmd);
    } else if (cmd->type == WRITE) {
        // 写命令需要先发数据,再发写指令
        send_data_to_nand(cmd->data);
        send_write_cmd(cmd);
        // 等待tPROG完成
        wait_for_prog_done();
    }
}

这段代码看着简单,但实际工程里要考虑的东西多得多。比如,多个Die之间的命令交织、QoS保证、紧急命令插队等等。我在海力士做过一个优化:把读命令的tR等待时间用来预取下一个命令的物理地址,IOPS提升了15%。

1.5 掉电保护与数据路径

掉电保护,是SSD固件里“一票否决”的环节。掉电保护没做好,其他功能再强也没用——数据丢了,用户直接退货。

掉电保护的核心机制:

  • PLP电容:掉电瞬间,电容里储存的电量能支撑固件把“脏数据”写回NAND。电容容量要算好:需要写回的数据量 × 写一个Page的功耗 × 安全余量。
  • 脏数据回写:DRAM缓存里还有没落盘的数据,掉电时要把它们刷到NAND的“安全区域”。这个区域通常是SLC模式的Block,写入速度快。
  • 原子写保证:一个4KB写操作,不能只写了一半就掉电。要么全写成功,要么全不写。这需要固件在写之前先记录“写日志”。

血的教训:

我曾经在一个项目里,掉电保护测试做了2000次都没问题,结果第2001次挂了。查了三天才发现:电容老化后容量下降了15%,刚好不够写完所有脏数据。从那以后,我要求所有PLP设计必须留30%以上的容量余量。

数据路径,指的是数据从Host到NAND的“旅行路线”。

典型的写数据路径:

  1. Host通过NVMe提交写命令,数据通过DMA进入DRAM缓存。
  2. FTL分配物理地址,更新映射表(先写映射表日志)。
  3. 数据从DRAM搬移到NAND的Page Buffer。
  4. NAND执行Program操作,数据写入Flash阵列。
  5. 写完成中断上报,FTL更新映射表状态为“已落盘”。
  6. Completion Queue更新,通知Host写完成。

读数据路径类似,但方向相反。这里有个关键点:读路径要尽量走缓存命中。如果数据还在DRAM里,直接返回,别去读NAND。我见过一些固件,读缓存命中率只有30%,白白浪费了DRAM。

优化建议:

数据路径上的每个环节,都要考虑“零拷贝”。DMA直接从Host内存搬到NAND Buffer,中间不要经过CPU搬运。我在一个项目里把数据路径从“CPU搬运”改成“DMA直通”,写延迟从80μs降到了25μs。

好了,第一章的内容就到这里。SSD固件架构全景,说白了就是这四层加一条数据路径。每一层都有它的坑,也有它的优化空间。后面我们会逐层深入,把每个模块的代码和算法掰开揉碎了讲。


公众号:蓝海资料掘金营,微信deep3321