NAND 多平面、Cache 命令与 Die 交错
从账面带宽与真实带宽的缺口出发,讲清多平面交错读如何把 tR 折进流水、Cache Program 如何把 tPROG 藏起来、Die 与 LUN 交错如何再叠一层并行,以及如何从 datasheet 参数估算出真实有效带宽 —— 一份给存储电路与固件工程师的技术探讨 | 编制日期 2026-10-14 | 版本号 Rev 1.0
几乎每一份 NAND 数据手册的第一页都会写一个很漂亮的接口速率,工程师也很自然地把它乘以总线位宽,得到一个「账面带宽」。但把实测数据摆在旁边时,两者往往差出一倍甚至更多。缺口不在接口上,也不在控制器不够快,而在于账面值只统计了总线真正在传数据的那一小段时间 —— 介质把一页数据从阵列搬到页缓存所花的 tR、把电荷打进浮栅所花的 tPROG、把整个块清掉所花的 tBERS,一次都没有进入这个算式。本文要做的,就是把这段被漏掉的时间摊开来看清楚,并给出把其它操作塞进这段时间的三种手段:多平面交错、Cache 命令、Die 交错。这是三种层次不同的并行,不是三个同义词。
结论:NAND 的真实带宽不取决于接口有多快,而取决于介质忙的那段时间里,总线上有没有别的事可做。多平面交错让同一颗 Die 里的另一个平面在你等待时干活,Cache 命令让上一条命令的编程与下一条命令的数据载入重叠,Die 交错让另一颗 Die 在你等待时用同一条总线输出数据。三者做的是同一件事 —— 填满单平面操作留下的总线空闲窗口 —— 但代价、约束与失效模式各不相同,叠加时也不能简单相乘。
报告总结
面向做 NAND 器件选型、控制器固件设计、存储性能调优与带宽验收的工程师,本文回答五类在具体项目里反复出现的问题:
- 账面带宽与实测带宽的缺口到底在哪里。不是「手册虚标」,而是两套口径在统计不同的东西。第 1 章把一次操作拆成命令、传输、等待、收尾四段,明确指出账面值只覆盖其中一段,并把「有效带宽」的定义与度量口径固定下来 —— 口径不写清楚的性能数字,无论多精确都没有意义。
- 单平面操作的天花板有多低。在动手做任何并行优化之前,必须先知道串行基线是什么。第 2 章给出单 Die 单 Plane 的命令流水模型,说明总线的忙与介质的忙在这条链上几乎不重叠,以及为什么提高接口频率的收益会迅速递减。
- 三种并行手段各自的机制、约束与代价。第 3、4 章讲多平面架构与交错读,第 5 章讲 Cache 读与 Cache Program 的两段式模型,第 6 章讲 Die 交错与多 LUN 并发。每一章都给出「它能藏掉什么」「它藏不掉什么」「它对地址与命令序列提出了什么约束」。
- 怎么从 datasheet 参数估算真实带宽。第 9 章给出一套只用公开时序参数就能算出的估算方法,以及测量时必须避开的陷阱(热身不足、缓存命中、队列深度不匹配、把接口速率当成介质速率等)。第 11 章用四个典型场景把账本算给你看。
- 设计核查与常见错误。第 12 章给出设计前必须回答的十个问题、交错配置的常见错误清单与验收测试建议,附录 B 汇总全部估算公式,附录 C 给参数表阅读指引与命令成对性核对表。
全文不给出具体 opcode 数值、寄存器地址与位域位置,涉及数值处一律标注「以正式 datasheet / 规范为准」;可写标准名(如 ONFI)与标准命令名(如 Read Page、Program Page、Read Cache、Multi-Plane 系命令),不写厂商排名与商业数据。文中所有量级表达都用相对关系,曲线形状与拐点位置均为示意。
读者路径
一、并行不是免费的。多平面操作对地址有约束(通常要求不同平面上的页地址满足某种对齐关系),对命令有约束(必须成对或成组发出),对错误处理有约束(一个平面失败会影响整组)。二、三种并行不等价也不能简单相乘。多平面交错在同一颗 Die 内部共享部分电路,Die 交错在不同 Die 之间共享总线,二者的瓶颈位置不同,叠加时的上限由更窄的那一处决定。三、本文所有数值都是量级与相对关系。任何要写进规格书、验收报告或设计文档的数字,都必须以对应型号的正式 datasheet 与实测报告为准。
1. 带宽的账面值与真实值
本章不谈任何具体命令,只做一件事:把「带宽」这个词的两个含义分开。一个是写在手册首页、由接口速率与总线位宽相乘得到的账面值;另一个是用户在真实负载下测到的有效值。二者的差不是误差,是两套口径在统计不同的东西。把这个缺口量化出来,后面所有并行手段的收益才有可比的基准。
1.1 标称带宽的三个隐含前提
把接口速率乘以总线位宽,得到的数字在物理上并没有错 —— 它确实是总线在传输数据那一瞬间的吞吐能力。问题在于,任何一份标称带宽要能等价于真实吞吐,都必须同时满足三个前提,而这三条在真实器件上一条都不成立。
前提一:介质能够按这个速率持续供给数据。也就是说,当控制器把一页读完、想要接着读下一页时,下一页的数据必须已经在页缓存里等着。实际情况是:控制器发完读命令之后,介质需要先花 tR 把整页数据从存储阵列感测、放大、锁存进页缓存,然后才能开始往总线上送。在这个 tR 期间,总线上一字节都没有。前提一在单平面操作下必然被违反。
前提二:一次操作里的等待时间不占用总线,或者说等待时间可以忽略。即使在最理想的实现里(命令完全流水化、没有软件轮询开销),等待段依然以「总线空闲」的形式存在于时间轴上。它不占用总线,但它在占用时间。而吞吐是数据量除以时间,时间轴上的任何一段空白都会直接压低吞吐。把「不占用总线」等同于「不影响带宽」是这里最常见的逻辑跳跃。
前提三:器件内部的并行度能被主机负载用满。现代 NAND 器件内部普遍有相当高的并行资源(多个平面、多个 Die、多个 LUN)。但只有当主机提交的请求足够多、地址分布足够分散、调度器足够聪明时,这些并行资源才真正被同时点燃。单线程、顺序、小深度的负载,会把一颗内部并行度很高的器件跑成一颗串行器件。
| 隐含前提 | 账面值假设它成立 | 真实器件上的情况 | 违反后的后果 | 谁能补救 |
|---|---|---|---|---|
| 介质能持续供给数据 | 下一页永远已在缓存里 | 每读一页都要先付一次 tR | 输出流被切成一段一段,中间是空的 | 多平面交错读 / Cache 读 |
| 等待时间可忽略 | 等待段短到不影响吞吐 | tPROG 与 tBERS 远长于传输段 | 吞吐被等待段主导,与接口速率脱钩 | Cache Program / Die 交错 |
| 内部并行度能用满 | 负载天然分散到所有并行单元 | 串行负载只点燃其中一个单元 | 器件跑成串行器件,带宽掉到几分之一 | 队列深度 / 调度器 / 地址布局 |
| 操作之间无间隙 | 命令可以背靠背发出 | 命令之间还有轮询与判定的开销 | 短操作被固定开销吃掉大部分收益 | 状态轮询优化 / 命令队列 |
| 时序参数不随环境变 | 典型值就是工作值 | 温度、磨损、电压都会改写时序参数 | 高温与寿命末期带宽明显低于常温 | 温度补偿 / 时序降额 |
这五条里,前两条决定了「有没有缺口」,后三条决定了「缺口有多深」。值得强调的是最后一条:时序参数不是常数。tR 与 tPROG 会随温度、随擦写循环次数、随器件个体差异而变化,datasheet 给出的通常是特定条件下的典型值与最大值。做带宽预算时如果只按典型值算,就会得到一个在常温新片上都勉强成立、在高温老片上完全不成立的乐观数字。这一点的展开见第 10 章 10.3。
1.2 一次操作的四段分解:命令、传输、等待、收尾
要量化缺口,先要把「一次操作」这个黑盒拆开。无论读、写还是擦除,任何一次 NAND 操作在时间轴上都可以切成四段,而且这四段的归属方完全不同 —— 有的段总线在忙、介质在闲,有的段反过来。图 1 把这件事画了出来。
命令段:控制器把操作码与地址周期送上总线,介质接收并译码。这一段总线是忙的,介质在做事(译码与寻址),但两者做的是同一件事的上下游,无法分离。命令段的长度与接口速率强相关,但它本身极短,通常不是瓶颈。
传输段:真正的数据搬家。读操作时是介质把页缓存里的数据串行移位到总线上;写操作时是控制器把数据从总线载入介质的页缓存(或数据寄存器)。这一段总线和介质同时在忙,也正是账面带宽唯一统计的那一段。它的长度约等于「页大小 / 接口速率」,是唯一能用提频直接压缩的一段。
等待段:介质在内部做物理动作 —— 读时是把阵列里的一页感测并锁存进页缓存(tR),写时是把页缓存里的电荷真正打进单元并完成验证(tPROG),擦时是对整个块施加擦除脉冲并验证(tBERS)。这一段总线完全空闲,介质完全忙碌。它是三者中最长的一段,也是本文唯一关心的一段。
收尾段:操作完成的判定与清理。读时可能是内部 ECC 的解码与纠错、状态寄存器的更新;写时必然有一次编程状态读取(判断这次编程是成功还是失败)。这一段总线又忙起来,但很短。它能不能被重叠,取决于协议是否允许「不等这一条收尾就发下一条」。
| 段 | 总线状态 | 介质状态 | 长度的主导因素 | 能否被提频压缩 | 能否被并行隐藏 |
|---|---|---|---|---|---|
| 命令段 | 忙 | 译码与寻址 | 接口速率与地址周期数 | 能,收益很小 | 可与前一条操作的收尾重叠 |
| 传输段 | 忙 | 同步参与 | 页大小 / 接口速率 | 能,收益明显 | 不能 —— 总线已被占满 |
| 等待段 | 完全空闲 | tR / tPROG / tBERS | 介质物理特性,与接口无关 | 完全不能 | 能 —— 这是全部并行手段的目标 |
| 收尾段 | 忙 | 状态更新与校验 | 协议规定的判据与轮询方式 | 能,收益很小 | 能与下一条命令重叠 |
表里最需要记住的是「等待段」那一行:它完全不能被提频压缩,但完全能被并行隐藏。这两个性质决定了本文的全部走向 —— 凡是想靠换更快的接口解决问题的思路都会落空,凡是想靠「让别的操作在这段时间里干活」解决问题的思路都有可能成立。
还有一点容易被忽略:四段的顺序不是固定的。读操作是「命令 → 等待(tR)→ 传输(输出)→ 收尾」,写操作是「命令 → 传输(载入)→ 等待(tPROG)→ 收尾」。写操作的传输段在等待段之前,这带来一个重要的不对称:写的数据载入一结束,总线就空出来可以服务别人,但这次写要等 tPROG 走完才算数;而读必须等 tR 走完才有数据可传。正是这个不对称,让写流水(Cache Program)比读流水更容易做深 —— 第 5 章会回到这一点。
1.3 tR、tPROG、tBERS 藏在哪里
三个时间参数都藏在等待段,但它们的性质差别很大,对带宽的影响方式也不同。分清三者,是做带宽账本的第一步。
- tR(阵列读时间):从介质收到读命令到整页数据被锁存进页缓存的时间。它出现在每一次页读的等待段,直接决定顺序读的流水能有多密。tR 的量级通常在几十微秒量级,具体以 datasheet 为准。关键性质:它与接口速率无关,也与页大小基本无关 —— 无论这一页有多大,感测与锁存整页的耗时主要由阵列物理决定。
- tPROG(编程时间):从数据载入完成、编程命令确认,到单元被真正写好并通过验证的时间。它出现在每一次页编程的等待段,是写带宽的主要杀手。tPROG 通常显著长于 tR,且在多层单元(每单元存更多比特)上明显更长 —— 因为需要更多次增量编程脉冲与验证。关键性质:它随写入的数据类型、随器件磨损程度、随温度变化,是一个分布而不是一个常数。
- tBERS(块擦除时间):擦除整个块所需的时间。它是三者中最长的,通常比 tPROG 再高一个量级。关键性质:它以块为单位,而读与写以页为单位,所以擦除的等待段一旦出现在关键路径上,影响范围远大于单次读写。
| 参数 | 藏在哪个操作的等待段 | 量级关系(以 datasheet 为准) | 主要可变因素 | 被哪种并行手段针对 |
|---|---|---|---|---|
| tR | 页读 | 三者中最短,几十微秒量级 | 温度、读重试、器件个体差异 | 多平面交错读 / Cache 读 / Die 交错读 |
| tPROG | 页编程 | 显著长于 tR | 单元层数、数据内容、磨损、温度 | Cache Program / 多平面写 / Die 交错写 |
| tBERS | 块擦除 | 三者中最长,比 tPROG 高一个量级 | 块位置、磨损、温度 | 多平面擦除 / 把擦除挪到后台 |
| tWB / 命令忙时间 | 命令段与收尾段 | 极短,纳秒到微秒量级 | 接口速率 | 命令流水化与队列 |
| tRR / 总线释放 | 操作之间的间隙 | 短但每次都要付 | 接口速率、总线负载 | 命令队列与减少轮询 |
这里必须澄清一个常见的误解:把 tR 与「读的延迟」混为一谈。tR 是介质内部的动作时间,不是主机感受到的读延迟。主机读延迟还要加上命令段、传输段、收尾段,以及排队时间。反过来,一次操作的延迟高,不等于带宽低 —— 如果多笔操作能重叠,单笔延迟可以很高而整体吞吐依然很好。带宽账本关心的是「单位时间完成了多少笔」,延迟账本关心的是「一笔要多久」,两者必须分开记。第 4 章 4.4 会给一个完整的延迟账本示例。
因为 tBERS 远长于 tPROG 远长于 tR,所以对带宽影响最大的是写而不是读,是擦除而不是写。这解释了为什么工程上「把擦除挪出关键路径」的收益往往大于「把读做得更快」:擦除一次,阻塞的是整个块的时间;而它完全可以被安排在后台,只要在前台需要这个块之前完成即可。第 7 章专门讨论这一点。
1.4 有效带宽的定义与度量口径
既然账面值不完整,就需要一个能替代它的量。本文统一使用有效带宽这个词,定义如下:
有效带宽 = 一次观测窗口内真正交付给上层的数据量 / 这个窗口的端到端时间。
这个定义看起来平淡,但它有三个必须显式声明的自由度,任何一个不说清楚,数字就不可比:
- 分子算什么。是纯用户数据,还是包含 ECC 校验与 spare 区的物理数据量?是否扣除因纠错失败而重读的部分?是否扣除被缓存命中、根本没有访问介质的那部分?
- 分母从哪算到哪。从命令发起到最后一次数据字节离开总线?还是包含命令排队时间?是否包含后台操作(垃圾回收、读刷新、磨损均衡)占用的时间?
- 观测窗口有多长。窗口太短会被冷启动、缓存预热、突发调度等瞬态污染;窗口太长又会掩盖抖动。窗口必须长到足以覆盖稳态。
这三个自由度在不同测试里被默认成不同值,是「同一颗器件测出三个不同带宽」这类争议的主要来源。本文约定:除特别说明外,分子为上层真正收到的用户数据量(不含 ECC 与 spare,不含缓存命中未访问介质的部分),分母为从第一条命令发起到最后一条命令完成的端到端时间(含排队与轮询开销,不含与本次负载无关的后台操作),观测窗口为足以穿透稳态的长度。
| 口径 | 分子(数据量) | 分母(时间) | 会得到什么数字 | 适用场合 | 容易踩的坑 |
|---|---|---|---|---|---|
| 接口瞬时带宽 | 总线上传输的字节数 | 只算总线忙的时间 | 最高,等于账面值 | 评估接口是否成为瓶颈 | 被当成器件带宽,乐观数倍 |
| 介质有效带宽 | 用户数据量 | 命令发起到完成,含等待段 | 中等,是本文的默认口径 | 评估器件与调度器的综合表现 | 未扣除缓存命中会偏乐观 |
| 应用有效带宽 | 应用真正拿到的数据量 | 含排队、含文件系统开销 | 最低,接近用户感受 | 评估端到端体验 | 混入了主机侧开销,难归因 |
| 稳态带宽 | 稳态窗口内的用户数据量 | 稳态窗口长度 | 最低但最可信 | 验收与寿命核算 | 窗口不够长会得到冷态数字 |
| 峰值带宽 | 任一短窗口的最大值 | 该短窗口长度 | 很高但不可持续 | 说明上限,不能用于承诺 | 被写进规格书造成纠纷 |
一个没有口径的带宽数字,和一个没有单位的物理量一样没有意义。本文建议的最小报告集合是:读写比例、请求大小、队列深度、地址分布(顺序 / 随机 / 局部)、观测窗口长度、是否扣除缓存命中、器件磨损与温度状态。少了任何一项,这个数字都无法与别人的数字比较。第 9 章 9.5 会给一个完整的报告格式模板。
1.5 为什么提频救不了慢介质
这是本章最实用的一条结论,也是最容易在评审现场被反复争论的一条。先看一个清晰的推理链。
一次页读的总时间 约等于 命令段 + tR + 传输段 + 收尾段。其中只有命令段与传输段随接口速率缩短。假设传输段占总时间的比例为 r,那么把接口速率提高到原来的 k 倍,总时间只会缩短到原来的 1 - r + r/k 倍。当 r 很小(介质很慢)时,无论 k 多大,总时间几乎不变。
举个量级上的直觉:如果 tR 与传输段处于同一量级,把接口速率翻倍,总时间大约缩短四分之一,带宽提升约三分之一 —— 看起来还不错。但如果介质慢一个量级(tPROG 远长于传输段,这是 TLC 及以上器件的常态),把接口速率翻倍,总时间的缩短可能只有百分之几。这时再提频,买到的只是更贵的接口、更难的布线与更严的信号完整性要求。
| 介质相对速度 | 传输段占总时间的大致比例 | 接口速率翻倍带来的带宽提升 | 提频是否值得 | 应该改做什么 |
|---|---|---|---|---|
| 快介质(传输段与等待段同量级) | 约一半 | 约三分之一 | 值得,但收益已开始递减 | 可继续提频,同时做多平面 |
| 中等介质(等待段约为传输段数倍) | 约四分之一到三分之一 | 约百分之十几到二十几 | 边际收益,需权衡成本 | 优先做交错与 Cache 命令 |
| 慢介质(等待段比传输段高一个量级) | 约十分之一及以下 | 约百分之几 | 基本不值得 | 只能靠并行度与后台化 |
| 擦除主导的场景 | 占比极低 | 几乎为零 | 完全不值得 | 把擦除挪出关键路径 |
| 随机小页场景 | 传输段被命令开销淹没 | 约百分之几 | 不值得 | 增大请求粒度与队列深度 |
提频并不是毫无价值 —— 它压缩的是传输段,而传输段恰恰是总线被占用的那一段时间。在做了多平面交错或 Die 交错之后,总线要在多个并行单元之间分时复用,此时总线占用率的下降会直接提高「总线能同时供养多少个并行单元」。换句话说:提频的收益要在并行度做起来之后才兑现。先提频后做并行,顺序反了,钱花了看不到效果。
另一个常被忽略的点是:提频往往伴随严格的时序裕量与信号完整性要求(见同系列《ONFI / Toggle 高速接口 SI》篇)。在布线能力或器件等级不支持的情况下强行提频,换来的是偶发的重读与纠错,反而把有效带宽拉低。
把第 1 章收成一句话:账面带宽统计的是传输段,真实带宽受制于等待段,而等待段既不能靠提频压缩,也不能靠更快的控制器消除,只能靠「在它发生的时候让别的操作也在发生」来掩埋。第 2 章先把不掩埋时的天花板算清楚,从第 3 章开始进入掩埋的手段。
一、账面值只覆盖传输段,等待段(tR / tPROG / tBERS)一次都没被算进去。
二、等待段不能被提频压缩,但能被并行隐藏 —— 这一条决定全文走向。
三、任何带宽数字必须同时给出分子口径、分母口径与观测窗口,否则不可比。
2. 单平面操作的性能天花板
在讨论任何并行手段之前,必须先把串行基线钉死。本章回答一个看似简单的问题:一颗只有单个 Die、单个平面的 NAND,在理想控制器下,带宽最高能到多少?答案是「一页的数据量除以四段之和」,而这个和由 tR 或 tPROG 主导。知道了这条基线,后面每加一层并行,收益才有了可比的参照。
2.1 页读入缓存的串行链
一次最简单的页读,在命令层面是一条严格的串行链,任何一环都不能跳过,也不能与另一环重叠:
- 发起读命令。控制器送出 Read Page 命令与完整的地址周期(列地址 + 行地址,行地址里含块号与页号)。总线忙,介质接收。
- 等待 tR。介质把选中字线上的一整页单元同时感测,经灵敏放大器放大、判决,锁存进该平面的页缓存(也叫数据寄存器)。总线空闲,介质忙。这一步结束后,介质通常会通过 Ready/Busy 信号或状态位告知「数据已就绪」。
- 串行输出。控制器切换总线方向,按列地址顺序把页缓存里的数据字节逐个读出。总线忙,介质同步配合。这一步的时间约等于页大小除以接口速率,也与起始列地址有关(可以只读页的一部分)。
- 收尾。内部 ECC 对读出的数据做解码与纠错(若器件内置纠错),并把结果体现在状态信息里;介质回到可接受新命令的状态。
这条链上有一个细节值得单独指出:第 2 步与第 3 步之间,介质内部的页缓存是两套东西。感测锁存的那一级与往外串行移出的那一级在结构上是分开的,这正是 Cache 读(第 5 章)能够成立的物理基础 —— 可以在输出上一页的同时,让阵列开始感测下一页。但在普通读命令下,这个结构没被利用起来,第 2 步与第 3 步严格串行。
| 链上的环节 | 耗时由什么决定 | 总线 | 介质 | 能否与下一笔重叠(普通读命令下) |
|---|---|---|---|---|
| 发起读命令 | 接口速率、地址周期数 | 忙 | 译码寻址 | 能与上一笔的收尾部分重叠 |
| 等待 tR | 阵列感测与锁存时间 | 空闲 | 忙 | 不能 —— 普通读命令不允许 |
| 串行输出 | 页大小 / 接口速率 | 忙 | 同步输出 | 不能 —— 总线被本笔占着 |
| 收尾与纠错 | ECC 解码方式与纠错量 | 忙或空闲 | 状态更新 | 能与下一笔的命令段重叠 |
| 操作间间隙 | 轮询与调度开销 | 忙 | 空闲 | 能被命令队列消除 |
表里「能否重叠」那一列是全章的钥匙。普通读命令之所以慢,不是因为每一段长,而是因为协议不允许重叠:必须等这一笔完全结束,才能发下一笔。后面几章讲的每一种手段,本质上都是在协议允许的范围内,把「不能」改成「能」。
2.2 页编程与 tPROG 占空比
页编程的串行链与读结构相同、顺序不同:发起 Program Page 命令与地址 → 把数据从总线载入介质的页缓存(这一步总线忙,是传输段) → 发出编程确认命令 → 等待 tPROG(总线空闲,介质忙) → 读状态判断成功与否(收尾段)。
这里引入一个非常好用的量:占空比。定义「某段占一次操作总时间的比例」。对读操作,等待段占比 = tR /(命令 + tR + 输出 + 收尾);对写操作,等待段占比 = tPROG /(命令 + 载入 + tPROG + 收尾)。占空比越高,说明这次操作里总线空闲得越久,也就意味着并行手段的理论空间越大。
写操作的等待段占比通常显著高于读操作,原因是 tPROG 远长于 tR,而且随着单元层数增加(每单元存更多比特),tPROG 增长得比 tR 更快。这带来一个反直觉但重要的推论:器件越「慢」,并行优化的收益空间反而越大,因为等待段占比更高。同时它也意味着,慢器件对「有没有做并行」极度敏感 —— 同一颗 TLC 器件,做了与没做 Cache Program,写带宽可以差出好几倍。
| 操作类型 | 等待段是什么 | 等待段占空比的典型水平 | 传输段位置 | 并行优化的理论空间 | 主要约束 |
|---|---|---|---|---|---|
| 页读(单平面) | tR | 中等,通常与传输段同量级或略高 | 在等待段之后 | 中等 | 输出顺序与总线占用 |
| 页编程(单平面) | tPROG | 高,通常显著高于读 | 在等待段之前 | 大 | 编程失败的判定与回退 |
| 块擦除(单平面) | tBERS | 极高 | 基本没有传输段 | 很大,但单位粒度是块 | 块粒度导致难以流水化 |
| 状态读取 | 几乎没有等待段 | 极低 | 几乎全是传输段 | 无意义 | 轮询频率本身就是开销 |
| 读状态后的重试 | 额外的感测时间 | 随重试次数线性上升 | 与读相同 | 越大说明器件越不健康 | 重试会打断流水 |
还有一条必须写进账本的东西:tPROG 不是一个常数,而是一个分布。同一颗器件上的不同页、不同数据内容、不同磨损阶段,实际编程时间会有明显离散性。做流水设计时如果按典型值排程,就会不断遇到「这一笔比预期慢」的情况,流水被打断、后面的操作被迫等待。工程上的处理办法是按偏保守的分位数排程,而不是按典型值 —— 这会略微降低理论效率,但换来流水的稳定性。第 10 章 10.4 会讨论参数表怎么配。
2.3 单 Die 单 Plane 的命令流水模型
图 2 把三次连续页读画在同一条时间轴上,分「总线」与「介质」两条泳道。可以清楚看到三件事。
- 每一拍内部,总线的忙与介质的忙几乎不重叠。总线忙的是命令段与输出段那两段(图里的 CMD 与 OUT),介质真正忙的是 tR 那一段。这两段时间互不覆盖,所以总时间就是它们之和,一秒都没有省下来。
- 每一拍之间也没有重叠。第 N 拍的输出刚结束,第 N+1 拍的命令才开始。中间的间隙是收尾段与轮询开销。在命令队列不被使用的情况下,这个间隙是纯损失。
- 总线空闲窗口(图里红色虚线框)是全部优化的目标。它的长度就是 tR 的量级。谁能在这段时间里让总线干点别的,谁就把带宽提上去了。
把这张图抽象成公式,就是本章的核心结论。设一页的用户数据量为 D,命令段耗时为 Tcmd,等待段为 Twait(读时是 tR,写时是 tPROG),传输段为 Txfer,收尾与间隙为 Ttail,则单平面带宽上限为:
BW_single = D / (Tcmd + Twait + Txfer + Ttail)
而账面带宽是 D / Txfer。两者之比就是「缺口倍数」。由于 Twait 通常远大于其余三项之和,这个缺口倍数很容易达到数倍 —— 这正是实测与标称差出一个量级的来源。
它只有一个用途:给任何优化设一个不可逾越的上限,并告诉你优化该往哪个方向使劲。如果一个方案号称把带宽提升了数倍,先用这个公式算一下它的理论空间 —— 若理论空间只有百分之几十,那么声称的数倍一定来自口径变化(比如把缓存命中算进了分子),而不是真实提升。第 9 章 9.4 会列出这类伪影。
2.4 Ready/Busy 轮询与总线空闲
等待段结束时,控制器必须知道「介质已经好了」。这件事看起来琐碎,但在单平面模型下它直接吃掉一部分可用带宽,因为轮询本身要占用总线时间,且轮询的粒度决定了等待段的实际长度。
常见的判据有三类。第一类是专用 Ready/Busy 引脚(并行 NAND 上的 R/B# 一类信号):它是独立的硬件信号,查询它不占用数据总线,控制器可以在等待期间去服务别的 Die 或做别的事,代价是需要额外的引脚与布线,且它通常只反映「这个 Target 忙不忙」,粒度较粗。
第二类是读状态寄存器 / 读状态命令:通过总线发一条状态查询命令并读回若干位。它的粒度可以很细(能区分读完成、编程完成、编程失败、擦除完成等),代价是占用总线、需要额外的命令周期,而且如果轮询太频繁,会挤占本可用于数据传输的总线时间;如果轮询太稀疏,会把等待段人为拉长(实际已完成,但要等下一次轮询才知道)。
第三类是协议层的命令队列与完成通知(如 ONFI 体系里的相关机制):控制器把多条命令排进队列,由器件在有进展时主动告知,控制器不必轮询。这是效率最高的方式,代价是控制器与器件都要支持,且实现复杂度明显上升。第 8 章 8.3 会展开。
| 判据方式 | 是否占用数据总线 | 粒度 | 对等待段实际长度的影响 | 实现成本 | 适用场合 |
|---|---|---|---|---|---|
| 专用 Ready/Busy 引脚 | 不占用 | 粗,通常只反映忙闲 | 几乎不增加 | 需要额外引脚与布线 | 并行 NAND 的传统做法 |
| 读状态命令轮询 | 占用,每次查询都要发命令 | 细,可区分多种完成与失败状态 | 取决于轮询间隔,可能显著拉长 | 低,所有器件都支持 | SPI NAND 与低成本方案 |
| 命令队列 + 完成通知 | 基本不占用 | 细,可区分到具体命令 | 最小 | 高,需要双方支持 | 追求高并行的控制器 |
| 固定延时盲等 | 不占用 | 无,纯靠时序余量 | 必须按最坏情况留足余量 | 极低 | 仅用于原型验证,不推荐量产 |
| 中断式通知 | 不占用 | 中 | 小,但有中断响应延迟 | 中,需要中断线与处理 | 主机侧负载较轻时 |
轮询太密:总线被查询命令占满,数据传输被挤掉,实测带宽下降,而且这种下降不会体现在任何器件参数里,只表现为「莫名地跑不满」。轮询太疏:等待段被人为拉长,等待段时间越长,这个绝对误差的影响越小,但对短操作(状态读、小页读)影响很大。经验做法是按等待段长度的某个小比例设置轮询间隔,并在温度升高、时序参数变差时自动收紧 —— 具体比例以实测为准。
2.5 单平面情况下的带宽上限估算
把前面四节合起来,可以给单平面情形下一个完整的可执行估算流程。这套流程只需要 datasheet 上的公开时序参数,不需要任何实测,因此可以在选型阶段就用来判断「这颗器件值不值得做并行优化」以及「做了之后大概能到多少」。
- 确定页大小与用户数据量 D。注意区分物理页的 spare 区与用户可见部分,分子只算用户数据。
- 查出等待段参数。读用 tR,写用 tPROG,擦用 tBERS。注意 datasheet 给的是典型值还是最大值,做预算要用接近最大值的那一档,并考虑温度与磨损的额外降额。
- 算出传输段。Txfer 约等于 D / 接口速率(注意接口速率要换算成字节每微秒,并扣除协议开销与总线周转空闲)。
- 估算命令段与收尾段。命令段按地址周期数与接口周期算;收尾段按状态查询命令的开销加 ECC 解码时间算,若 datasheet 未给出,按与命令段同量级估计并留出余量。
- 相加求倒数,得到单平面上限。BW = D /(四段之和)。
- 与账面值对比,得到缺口倍数。缺口倍数 =(四段之和)/ Txfer。这个数字就是并行手段的理论空间上限。
下面这张表给出参数敏感性,说明哪一项的不确定性对结果影响最大 —— 它决定了你在 datasheet 上该花时间核对哪一项。
| 参数 | 典型不确定性 | 对单平面带宽的影响权重 | 该不该花时间核对 | 备注 |
|---|---|---|---|---|
| 等待段 tR / tPROG | 典型值与最大值可能差很多 | 决定性,通常主导总时间 | 必须核对,且要问清测试条件 | 温度与磨损会进一步拉长 |
| 接口速率 | 通常明确 | 只影响传输段,权重随介质变慢而下降 | 核对,但别指望它救场 | 要扣除协议开销 |
| 页大小 | 明确 | 分子与传输段同时受影响 | 核对即可 | 大页有利于摊薄固定开销 |
| 命令段开销 | 常被忽略 | 小,但对短操作显著 | 核对地址周期数 | 多平面命令的地址周期更多 |
| 收尾与轮询 | 取决于实现方式 | 中,取决于轮询策略 | 必须核对,这是控制器可控项 | 是少数可优化的纯软件项 |
| 总线周转空闲 | 常被漏算 | 小到中 | 核对时序参数 | 双向总线换向需要空闲周期 |
单 Die 单 Plane 的带宽上限,等于一页数据量除以四段之和;而四段之中,等待段既最长、又不能靠提频压缩、还完全不占用总线。这个「总线空闲而介质忙碌」的窗口有多长,就决定了后面所有并行手段往上能叠多高。带着这个基线进入第 3 章:第一种掩埋手段是让同一颗 Die 里的另一个平面,在你等待的时候也开始干活。
3. 多平面架构与寻址模型
第一种掩埋手段是「让同一颗 Die 里的另一个平面在等待期间干活」。要用好它,必须先回答两个问题:物理上哪些东西是独立的、共享到什么程度;协议上怎么点名某一个平面。本章讲前者(层级)与后者(地址),并给出多平面操作的合法组合与失败模式 —— 多平面不是「想对几个平面操作就对几个」,它对地址有硬约束。
3.1 Plane、Die、Target、LUN 的层级关系
NAND 器件内部的并行资源是一棵树。理解这棵树的关键不在于记住名字,而在于回答一个问题:两个操作各自处在这棵树的哪个分支上,它们之间共享了哪一级的资源。共享得越少,并行度越高。
从外到内,常见的层级是这样的:
- Target(也常按 CE# 称呼):总线上的一个片选目标。控制器通过片选信号在这若干个 Target 之间做选择。同一条总线上可以挂多个 Target,它们共享总线,但各自有独立的命令接口与状态。
- LUN(逻辑单元):能够独立接收并执行命令的最小单位。这是并行语义上最重要的一级 —— 「独立执行」意味着它可以有自己的状态机、自己的忙闲,别的 LUN 在忙不影响它接收新命令。第 6 章讲的 Die 交错,本质上就是 LUN 级的交错。
- Die(物理硅片):一颗封装里的一个物理裸片。在很多器件上,一个 LUN 就对应一个 Die,两者常常被混用;但在多 Die 堆叠封装里,一个 Target 下可能有多个 Die,此时二者不等价。
- Plane(平面):Die 内部的阵列分区。每个平面有自己的页缓存(数据寄存器),这是多平面操作能并行的物理基础 —— 两个平面可以同时把各自的页感测进各自的缓存。但同一 Die 内的多个平面仍然共享更高层的资源(命令译码、电荷泵、总线接口等),这是多平面并行度有上限的原因。
- Block 与 Page:真正的存储组织单位,也是多平面操作最终作用到的对象。
| 层级 | 独立性体现在哪 | 与谁共享资源 | 能支撑哪种并行 | 常见混用与坑 |
|---|---|---|---|---|
| Target / CE# | 可被片选单独选中 | 与总线上其它 Target 共享总线带宽 | 通道级 / Target 级交错 | 片选切换有建立保持时间,频繁切换有代价 |
| LUN | 能独立接收并执行命令与状态机 | 共享总线与部分接口逻辑 | Die 交错(第 6 章) | 文档里 LUN 与 Die 常互换,须核对语义 |
| Die | 物理上独立的硅片 | 多 Die 堆叠时共享封装与部分引脚 | Die 交错 | 多 Die 封装下 Target 与 Die 不是一对一 |
| Plane | 独立的页缓存与阵列分区 | 共享命令译码、电荷泵、总线接口 | 多平面交错(第 4、5 章) | 平面数上限由器件定,软件调不出来 |
| Block / Page | 阵列内的组织单位 | 同一平面内的块共享该平面的全部资源 | 不构成并行 | 把块级并行误当平面级并行 |
表里最容易踩的坑是术语混用。不同厂商、不同代际的文档对 LUN 与 Die、Target 与 CE# 的用法并不统一,有的文档甚至在同一份文件里切换口径。可靠的核对方法是看它「能不能独立接收并执行命令」:能,就是 LUN 级;不能、只是阵列分区,就是 Plane 级。名字不重要,语义重要。
3.2 页地址中的 Plane 选择位
物理层级解决「谁能并行」,地址解决「怎么点名」。NAND 的行地址(选块与选页的那部分地址)里,通常专门有若干位用来选择平面。理解这几点就够了:
- 这些位的位置与宽度由器件决定。它们未必在行地址的最低位,也未必连续;位数由器件支持的平面数决定(支持两个平面至少需要一位)。具体位置与位序以正式 datasheet 为准,本文不给出任何位域位置。
- 多平面操作送的是「同一份块页地址 + 多个平面选择」。也就是说,参与一次多平面操作的各页,其块内相对位置(页号)与块地址必须是相同的,只有平面选择位不同。这是多平面操作最核心的地址约束,见 3.3。
- 单平面操作时这些位照常出现。它们只是恰好选中了某一个平面。换句话说,多平面命令与单平面命令在地址形式上往往差别不大,差别在于命令本身是否承诺「我要同时对多个平面下手」,以及控制器是否在同一个操作里送出了多组平面选择。
- 列地址不受平面选择影响。列地址选择的是页内的字节偏移,与选哪个平面无关。所以它不参与多平面约束。
拿到一颗器件,先问三个问题:它有几个平面?行地址里哪几位选平面?多平面操作时是否要求各页的块地址与页地址完全相同?这三个问题的答案决定了你后面所有调度策略的形态。如果 datasheet 对第三点语焉不详,就按「必须完全相同」来设计 —— 这是绝大多数器件的口径,保守设计不会错。
3.3 多平面操作的地址约束与边界条件
多平面操作不是「任意挑两个页一起操作」。器件对参与同一个多平面操作的各页地址有明确约束,违反约束的后果不是「变慢」,而是操作被拒绝、行为未定义,或者写到不该写的地方。这一节列清常见的约束类型。
约束一:页地址必须相同。参与同一次多平面操作的各页,其块内页号必须一致。这是最常见的约束,理由是地址译码逻辑通常把「平面选择」放在地址通路的最末一级,各平面并行执行同一套行译码。
约束二:块地址在多数器件上也要求相同。即参与操作的各页必须位于各个平面的同一块号上。这一条比第一条更严格,也更容易被忽略。少数器件允许块地址不同但页地址相同,具体以 datasheet 为准。
约束三:块的类型必须相同。参与多平面擦除的各块必须都是同一类(例如都是普通数据块,或者都在同一个分区内)。把不同类型的块(例如某些器件上末尾的块有不同用途)混进同一次多平面擦除,可能导致部分块擦除失败而状态寄存器只报一个错。
约束四:坏块与已标记的块不能参与。如果参与操作的某个块是坏块,整组操作都会失败,而且失败信息可能不足以定位到是哪一个平面出了问题。控制器必须在成组之前做坏块筛选。
约束五:边界与地址回绕。当连续流水的地址跨越块边界、平面边界或 Die 边界时,成组关系必须重新计算。一个常见的实现错误是「按页号连续地自动成组」,结果跨越边界时把不同块号的页组在了一起。
| 约束类型 | 具体内容 | 违反后的典型表现 | 控制器侧该怎么防 | 严重程度 |
|---|---|---|---|---|
| 页地址相同 | 各平面的块内页号必须一致 | 操作被拒绝或部分平面未执行 | 成组前校验页号相等 | 高 |
| 块地址相同 | 多数器件要求块号也一致 | 写到错误的块 / 未定义行为 | 成组前校验块号相等 | 极高 |
| 块类型相同 | 不能混入特殊用途块 | 部分块擦除失败且难定位 | 维护块类型位图,成组时过滤 | 高 |
| 排除坏块 | 坏块不能参与成组 | 整组失败,错误信息不足 | 成组前查坏块表 | 高 |
| 边界重算 | 跨块 / 跨平面 / 跨 Die 要重算组 | 跨边界后组合非法 | 成组逻辑以块为最小单位重新发起 | 中高 |
| 平面数不超限 | 成组数不能超过器件支持的平面数 | 多余的组被忽略或报错 | 从参数表读取平面数上限 | 中 |
这里给一条工程经验:把成组逻辑建立在「块」而不是「页流」上。也就是说,调度器在决定一次多平面操作包含哪些页时,应当先确定块、再在块内按页号推进,并且在跨块时强制结束当前组、重新成组。这样写出来的代码天然满足前两条约束,也天然处理了边界问题。反过来,如果按「连续的页流」自动切分成组,就必须在每一组生成时做一次完整的地址校验,代码更容易出错,也更难评审。
3.4 多平面读、写、擦除的合法组合
多平面操作在读、写、擦除三类操作上的支持程度并不对称。一般来说,擦除最容易成组(因为它不涉及数据传输,只是对多个块同时施加擦除脉冲),读次之,写最复杂(因为要涉及数据载入、编程确认与失败判定的组合)。下面这张表给出三类操作的组合规则与注意点。
| 操作类型 | 是否能成组 | 成组后共享什么 | 成组后不共享什么 | 主要注意点 |
|---|---|---|---|---|
| 页读 | 能,多平面读命令 | 命令译码与总线输出阶段 | 各平面的页缓存与感测电路 | 输出顺序固定,且输出阶段总线独占 |
| 页编程(普通) | 能,多平面编程命令 | 数据载入阶段与编程启动 | 各平面的编程执行与验证 | 任一个平面失败会影响整组的状态判定 |
| 页编程(Cache 模式) | 能,多平面 + Cache 组合 | 数据载入与上一组编程的等待段 | 各平面的编程执行 | 组合规则最复杂,见第 5 章 5.3 |
| 块擦除 | 最容易成组 | 命令与擦除脉冲的启动 | 各块的实际擦除过程 | 块地址约束最严,且要排除特殊块 |
| 读状态 / 读回 | 通常不成组 | 无 | 无 | 状态要按参与的每个单元分别读 |
| 复位与模式切换 | 通常作用于整个 Target | 整个 Target 一起生效 | 无 | 会打断正在进行的流水 |
还有一个必须写清楚的边界:多平面操作内部不能混操作类型。不能在同一次多平面操作里让一个平面读、另一个平面写;也不能让一个平面编程、另一个平面擦除。多平面命令的语义是「同一类操作,作用在多个平面上」。想让读与写重叠,那是Die 交错(第 6 章)或命令队列要解决的问题,不是多平面命令能解决的。
这一点经常被误解。多平面交错的并行是同构并行:同一类操作,在多个平面上同时推进。而 Die 交错的并行可以是异构并行:一颗 Die 在编程的同时,另一颗 Die 在读。二者的适用场景完全不同 —— 前者适合主机负载本身是同构的大批量读或大批量写,后者适合读写混合的负载。
3.5 跨 Plane 边界操作的失败模式
当成组逻辑出错、或者器件状态与控制器假设不一致时,多平面操作会以一些特征性的方式失败。这些失败模式的共同特点是:症状出现在离根因很远的地方,而且状态寄存器给出的信息往往不足以唯一定位。认识它们,能显著缩短排查时间。
| 失败模式 | 触发条件 | 直接症状 | 为什么难定位 | 排查切入点 |
|---|---|---|---|---|
| 部分平面未执行 | 成组地址不满足约束,器件只执行了其中一部分 | 读回的数据一半是旧的 | 状态位可能仍报成功 | 读回比对,而不是只看状态 |
| 整组失败但只报一次错 | 某一个平面编程或擦除失败 | 整组标记为失败 | 不知道是哪个平面 | 逐平面单独重发一次定位 |
| 数据错位 | 输出顺序假设与实际不一致 | 两个平面的数据被拼接错位 | 校验和可能碰巧通过 | 用已知图案做顺序验证 |
| 跨边界组非法 | 连续流成组时跨块未重算 | 偶发写入错误,与地址相关 | 只在特定地址段复现 | 把触发地址打印出来找规律 |
| 坏块混入成组 | 坏块表未及时更新 | 整组失败,且失败率随时间上升 | 坏块是运行期新产生的 | 核对坏块表的更新时机 |
| 状态读取时机错误 | 在组内某个平面还没完成时就读了状态 | 误判成功,数据实际未写好 | 与时序强相关,难稳定复现 | 严格按照协议要求的判据与时机 |
多平面操作的错误处理粒度是「组」,不是「页」。这意味着一个平面的失败会连累整组,而回退动作也必须按组来做。设计时必须提前回答:组失败后是整组重发、还是拆成单平面逐个重发?重发时地址要不要变(例如写到另一个块)?元数据怎么记录「这个组只成功了一部分」?
把这三个问题留到出错时才想,几乎一定会写出不一致的回退路径。
多平面并行是同构并行:同一类操作,作用在多个平面上,靠各自独立的页缓存实现感测与编程阶段的 overlapping。它对地址的约束是硬性的(页号、通常还有块号必须相同),错误处理的粒度是组而不是页。带着这三条进入第 4 章 —— 那里把多平面读的流水画出来,并把收益算清楚。
4. 多平面交错读:把 tR 折进流水
知道了多平面的物理与协议基础,现在算第一笔账:交错的收益到底是什么。本章给命令序列、流水图、深度与效率的关系、输出顺序的保证、延迟账本,以及一个可以直接套用的有效带宽估算式。核心结论是:交错省掉的是 tR 的重叠部分,而输出阶段仍然要依次占用总线 —— 这个不对称决定了收益的天花板。
4.1 交错读的命令序列与流水图
一次多平面交错读的命令序列,与两次独立的单平面读相比,差别只有一处:第二条读命令必须在第一条的数据开始输出之前发出。正是这一处差别,让两个平面的阵列感测(tR)在时间上重叠起来。
典型的序列是这样的(命令名以规范为准,此处只讲顺序):
- 发起第一个平面的读命令,地址里带上第一个平面的选择位。介质开始对平面 0 做行译码。
- 不等平面 0 的 tR 走完,立刻发起第二个平面的读命令,地址里带上第二个平面的选择位。这一步有严格的时间窗口:必须在平面 0 的数据开始输出之前完成,否则重叠失败。
- 两个平面同时进行阵列感测。总线上没有数据,但两个平面各自都在把整页感测并锁存进自己的页缓存。这就是被折进流水的时间。
- 依次输出两个平面的数据。先输出先发命令的那个平面,再输出后发的那个。这一段总线是满的,两次输出串行。
- 收尾与状态判定。两个平面各自完成,状态按协议规定读取。
| 序列环节 | 单平面读(两次) | 两平面交错读 | 差值来源 | 是否可被进一步压缩 |
|---|---|---|---|---|
| 命令段 | 两次,分别发起 | 两次,背靠背发起 | 基本相同,交错略省间隙 | 能,靠命令队列 |
| 等待 tR | 两次串行,各付一次 | 一次,两者重叠 | 这是全部收益的来源 | 能,靠加大深度 |
| 数据输出 | 两次串行 | 两次串行,顺序固定 | 没有差别 | 不能 —— 总线已被占满 |
| 收尾与状态 | 两次 | 两次(或按组合并一次) | 略省 | 能,靠合并状态读取 |
| 操作间间隙 | 两次间隙 | 一次间隙 | 省一次 | 能,靠流水化 |
表里最后一行值得单独说:交错不仅省了 tR,还顺带省了一次操作间间隙。这个收益在单看 tR 时容易被忽略,但在小页、高频次的负载下,间隙占的比例并不小,省下来的部分可能相当可观。反过来,这也说明大页负载下交错的相对收益更高吗?不 —— 恰恰相反:大页时传输段更长,tR 占比下降,交错的相对收益变小。第 11 章 11.1 会把这笔账算清楚。
4.2 交错深度与 tR 隐藏效率
把两个平面换成 N 个平面,就是交错深度 N。设一次单平面读的总时间为 T1 = Tcmd + tR + Txfer,那么深度 N 的交错读总时间约为:
TN 约等于 N × Tcmd + tR + N × Txfer
对应的每页平均时间为 Tcmd + tR/N + Txfer,有效带宽提升倍数为:
增益(N) = (Tcmd + tR + Txfer) / (Tcmd + tR/N + Txfer)
这个式子的形状非常明确:深度为 1 时增益是 1;深度趋于无穷时,增益趋于 (Tcmd + tR + Txfer) / (Tcmd + Txfer),即「总时间 / 非等待段时间」,也就是第 2 章说的缺口倍数。所以缺口倍数既是并行手段的理论上限,也是这条曲线的渐近线。
| 交错深度 | 每页平均等待段 | 理论增益(示意) | 实测常见增益(示意) | 差距来源 | 工程评价 |
|---|---|---|---|---|---|
| 1(基线) | tR | 1.0× | 1.0× | 无 | 串行基线,一切比较的参照 |
| 2 | tR / 2 | 约 1.8× | 约 1.6× ~ 1.8× | 命令段翻倍开始显现 | 性价比最高的一级 |
| 3 | tR / 3 | 约 2.4× | 约 2.0× ~ 2.3× | 共享资源竞争 | 收益仍然明显 |
| 4 | tR / 4 | 约 2.8× | 约 2.3× ~ 2.6× | 总线占用与缓冲压力 | 常见器件的实际上限 |
| 超过器件平面数 | 不适用 | 无意义 | 无意义 | 硬件不支持 | 必须用 Die 交错再叠一层 |
| 深度不足(有资源但没填满) | 介于两者之间 | 低于理论 | 明显低于理论 | 地址分布不分散 / 队列太浅 | 最常见的实际瓶颈 |
器件支持 4 个平面,但实测增益只有 1.5 倍 —— 这种情况极少是器件问题,几乎总是深度没有被真正填满。三个高频原因:① 队列深度不够,主机一次只提交一两笔请求,调度器没有足够的候选去凑满一组;② 地址分布集中,连续的请求落在同一个平面的相邻区域,凑不出跨平面的组合;③ 成组约束太严,例如强制要求块号相同,导致大量请求无法成组。
排查顺序建议:先看队列深度 → 再看地址分布 → 最后看成组命中率。第 10 章 10.1 会讲调度器怎么提高成组率。
4.3 读数据输出的顺序保证
多平面读的输出顺序是协议规定的,不是控制器可选的。通常的规则是:按发出命令的顺序,先发的平面先输出。这条规则看起来简单,但它对控制器的缓冲区管理提出了明确要求,忽略它会造成一类非常隐蔽的数据错位。
为什么顺序不能随便换?因为页缓存是硬件资源:平面 0 的数据被感测进平面 0 的页缓存,平面 1 的在平面 1 的页缓存。总线上一次只能选通其中一个往外移。器件内部并没有一个「任意读取某个页缓存」的自由仲裁 —— 输出顺序固化在命令序列里。
这对控制器提出三点要求:
- 接收缓冲必须按输出顺序排列。如果主机请求 A 映射到了平面 1、请求 B 映射到了平面 0,那么物理上先出来的是 B 的数据。控制器必须知道这一点,把数据送到正确的主机缓冲。
- 主机侧的完成通知顺序不等于提交顺序。在多平面与多 Die 混合的调度下,先提交的请求可能后完成。上层协议如果假设了顺序完成(某些简化的固件会这样假设),就会出现偶发的错误。
- 出错时的重排必须显式处理。如果某一组里前一个平面失败、需要重读,那么后一个平面的数据可能已经输出。此时要么把它丢弃重来,要么先把已输出的数据落地 —— 不能假设「失败就整组重来」。
| 顺序相关的环节 | 协议保证什么 | 控制器必须做什么 | 出错会怎样 | 如何验证 |
|---|---|---|---|---|
| 组内的输出顺序 | 按命令发出顺序依次输出 | 按此顺序安排接收缓冲 | 两个平面的数据错位拼接 | 用已知图案填满两页再读回比对 |
| 组间的完成顺序 | 通常按组推进,但可被调度打乱 | 不要假设 FIFO 完成 | 上层收到乱序数据 | 打乱提交顺序做压力测试 |
| 失败后的剩余数据 | 协议不保证可以跳过 | 显式决定丢弃还是落地 | 缓冲泄漏或数据丢失 | 注入失败观察回退路径 |
| 随机读的部分输出 | 可以只读页的一部分(列地址) | 记录每笔请求的实际列范围 | 读到多余或缺失的字节 | 列地址边界用例 |
| 跨 Die 的组合顺序 | 不同 Die 之间顺序由调度器决定 | 维护每笔请求的目标 Die 记录 | 跨 Die 数据混淆 | 多 Die 并发压力测试 |
验证顺序保证的方法很直接:用可辨识的图案填满多个平面(例如每个平面写入不同的固定图案),然后按各种提交顺序读回,逐字节比对。这个测试要覆盖「提交顺序与输出顺序相反」这一最坏情况。很多只在顺序负载下验证过的固件,在第一次遇到乱序负载时就会暴露问题。
4.4 交错读的延迟账本
带宽账本回答「单位时间完成多少笔」,延迟账本回答「一笔要多久」。这两个账本必须分开记,因为它们对交错的响应是相反的。这一点非常重要,也是很多性能争议的来源。
就单笔延迟而言,交错不但没有改善,通常还会略微变差:因为在交错模式下,第一笔数据要等到「整组命令发完 + tR + 排在它前面的平面输出完」才能开始传输。如果它排在组内第二个,它还要额外等第一个平面的输出走完。也就是说,组内越靠后的平面,单笔延迟越高。
就整体吞吐而言,交错明显改善:因为整组的总时间被 tR 的重叠压短了,单位时间交付的数据量上升。
于是就有了那个经典的现象:吞吐上升的同时,平均延迟上升、尾部延迟上升得更明显。对于吞吐型负载(大文件拷贝、扫描、备份),这是纯赚;对于延迟敏感型负载(小随机读、交互式响应),这可能是亏的。第 11 章 11.2 会量化这个塌陷。
| 账本项目 | 单平面读 | 两平面交错读(组内第 1 个) | 两平面交错读(组内第 2 个) | 趋势 |
|---|---|---|---|---|
| 命令发出到数据开始输出 | tR | 命令 ×2 + tR | 命令 ×2 + tR + 第 1 个的输出 | 变差 |
| 单笔端到端延迟 | T1 | 略高于 T1 | 明显高于 T1 | 变差,且组内位置越后越差 |
| 组内平均延迟 | T1 | 约 1.0 ~ 1.3 × T1 | 同左 | 略变差 |
| 单位时间交付页数 | 1 / T1 | 约 1.6 ~ 1.8 / T1 | 同左 | 明显变好 |
| 尾部延迟(P99 一类) | 由重试与排队决定 | 叠加了组内排队 | 叠加更多 | 变差,且更依赖调度 |
| 对交互式负载的友好度 | 较高 | 中 | 较低 | 下降 |
如果负载是单笔延迟敏感、并发度低的(例如小随机读、元数据查询、交互式响应),强行成组会让每一笔都等组内其它成员,得不偿失。成熟调度器的做法是动态决定成组与否:队列里有足够多可以成组的同构请求时才成组,队列浅时直接单发。这个判断逻辑的收益,在很多真实负载下比「把深度调到最大」更大。第 10 章 10.1 展开。
4.5 多平面读有效带宽估算
把本章的公式整理成可以直接套用的估算流程。它只需要 datasheet 上的 tR 与接口速率,加上页大小,就能给出量级正确的结果。
- 算出传输段:Txfer 约等于 页大小 / 有效接口速率(扣除协议开销与总线周转)。
- 查出等待段:tR,取接近最大值的那一档,并按温度与磨损留出降额。
- 算出命令段:Tcmd 约等于 命令与地址周期数 × 接口周期;多平面命令的地址周期通常更多,不要沿用单平面的数。
- 确定可用深度 N:取「器件支持的平面数」与「调度器实际能凑出的深度」两者中较小的那个 —— 后者常常是短板。
- 代入:BW_mp = N × D / (N × Tcmd + tR + N × Txfer + Ttail)。
- 对比基线:增益 = BW_mp / BW_single,与第 4.2 节的表对照,看是否落在合理区间。若不落在合理区间,先查深度有没有被填满。
| 输入量 | 从哪里来 | 常见误差来源 | 对结果的影响 | 核对优先级 |
|---|---|---|---|---|
| 页大小 D | datasheet | 混淆物理页与用户可见部分 | 线性影响分子 | 高 |
| tR | datasheet 时序表 | 用典型值代替最大值;忽略温度降额 | 主导结果 | 最高 |
| 接口速率 | datasheet / 控制器配置 | 未扣除协议开销与总线周转 | 只影响传输段 | 中 |
| 命令周期数 | datasheet 命令表 | 多平面命令地址周期更多 | 小,但深度大时会被放大 | 中 |
| 实际深度 N | 调度器统计 | 队列浅 / 地址集中 / 成组约束 | 往往是最大误差源 | 最高 |
| 尾段时间 Ttail | 实测或估计 | 轮询策略不同差异很大 | 中小 | 中 |
多平面交错读省掉的是 tR 的重叠部分,输出阶段依然串行;因此它的增益受限于 tR 在总时间里的占比,渐近线就是第 2 章的缺口倍数。深度 1 → 2 是最划算的一步;再往上受制于器件平面数与共享资源。它提升吞吐,但会推高单笔与尾部延迟 —— 对吞吐型负载是纯赚,对延迟敏感型负载要慎用。
第 5 章处理写侧:那里因为传输段在等待段之前,流水可以做得更深,机制也完全不同。