3、NAND Flash接口协议:异步/同步时序,ONFI与Toggle标准,命令/地址/数据总线复用

好,咱们今天聊聊NAND Flash的接口协议。说实话,这部分内容在芯片验证和驱动开发中,是绕不开的核心。你想想看,CPU要跟Flash打交道,总得有个规矩吧?这个规矩就是接口协议。

我个人习惯把接口协议分成两大部分来看:一是时序(异步还是同步),二是标准(ONFI还是Toggle)。另外还有个很关键的设计——总线复用。咱们一个一个说。

3.1 异步时序 vs 同步时序

先讲异步时序。这是最基础的玩法,也是我最早接触的NAND接口。

异步时序,说白了就是靠信号的电平跳变来传递信息。没有时钟线,全靠控制信号的先后顺序。常用的控制信号有:

  • CE#(Chip Enable):片选,选中哪个芯片干活
  • CLE(Command Latch Enable):命令锁存使能
  • ALE(Address Latch Enable):地址锁存使能
  • WE#(Write Enable):写使能,下降沿锁存数据
  • RE#(Read Enable):读使能,下降沿输出数据
  • WP#(Write Protect):写保护
  • R/B#(Ready/Busy):忙指示

异步时序的典型操作流程是这样的:

// 发送命令 0x80(页编程)
CE# = 0;          // 选中芯片
CLE = 1;          // 命令锁存模式
ALE = 0;          // 地址不锁存
WE# = 0;          // 拉低WE#
// 此时数据总线上放 0x80
WE# = 1;          // 上升沿,命令被锁存
CLE = 0;          // 结束命令阶段

嗯,这里要注意:异步时序的时序参数都是绝对时间。比如 tWC(写周期时间)最小要多少纳秒,tRC(读周期时间)最小多少纳秒。我在项目中遇到过,有些新手工程师把时序参数设得太紧,结果高温下就跑挂了。所以异步时序的验证,一定要做PVT(工艺-电压-温度)全覆盖。

警告:异步时序的tWP(WE#脉冲宽度)和tRP(RE#脉冲宽度)是硬约束。我曾经因为tWP少设了2ns,导致批量芯片在低温下写数据出错。后来查了三天才找到原因。

同步时序就高级一些了。它引入了时钟信号(CLK),所有操作都跟时钟沿对齐。同步模式下,数据可以双沿采样(DDR),也就是时钟的上升沿和下降沿都传数据。这样一来,吞吐量直接翻倍。

同步时序的关键信号多了个CLK和DQS(数据选通)。DQS在读取时由Flash驱动,写入时由控制器驱动。这个设计其实挺巧妙的——用DQS来保证数据采样的窗口足够宽。

我建议你在做同步时序验证时,重点关注DQS与CLK的相位关系。ONFI标准要求DQS与CLK有固定的相位偏移,Toggle标准则要求DQS与数据边沿对齐。搞混了的话,数据采样会出大问题。

3.2 ONFI标准 vs Toggle标准

这两个标准,说白了就是两大阵营的玩法。ONFI(Open NAND Flash Interface)是Intel、Micron他们推的,Toggle是Samsung、Toshiba他们推的。虽然目标都是让Flash跑得更快,但实现方式有差异。

特性 ONFI标准 Toggle标准
时钟来源 控制器提供CLK 控制器提供CLK,DQS由Flash驱动
数据采样 CLK边沿采样 DQS边沿采样
DQS相位 与CLK有固定偏移 与数据边沿对齐
最大速率 ONFI 5.0可达3600 MT/s Toggle 5.0可达3600 MT/s
命令集 标准化命令集 兼容ONFI命令集,有扩展

我个人觉得,ONFI的标准化程度更高,命令集定义得很清楚。Toggle则更灵活,但兼容性验证要更仔细。我在一个项目中同时用了ONFI和Toggle的Flash,结果发现Toggle的读时序参数跟ONFI不完全一样,驱动里得做条件编译。

小技巧:如果你在做SoC设计,建议把ONFI和Toggle的时序参数做成可配置的寄存器。这样换Flash型号时,改寄存器值就行,不用改RTL代码。我之前的项目就是这么干的,省了不少事。

3.3 命令/地址/数据总线复用

这个设计很有意思。NAND Flash的引脚有限,所以把命令、地址、数据都走同一组总线(通常是8位或16位)。靠CLE和ALE信号来区分当前总线上是什么内容。

具体来说:

  • CLE=1, ALE=0:总线上是命令
  • CLE=0, ALE=1:总线上是地址
  • CLE=0, ALE=0:总线上是数据

你想想看,一个页编程操作,要发命令、发5个地址周期、再发数据。整个过程都是在这组总线上完成的。所以驱动里必须严格按顺序来:

// 页编程示例(以8位总线为例)
// 1. 发命令 0x80
set_CLE(1); set_ALE(0);
write_bus(0x80);
toggle_WE();

// 2. 发地址(5个周期)
set_CLE(0); set_ALE(1);
write_bus(column_addr & 0xFF);      // 列地址低8位
toggle_WE();
write_bus((column_addr >> 8) & 0xFF); // 列地址高8位
toggle_WE();
write_bus(row_addr & 0xFF);          // 行地址低8位
toggle_WE();
write_bus((row_addr >> 8) & 0xFF);   // 行地址中8位
toggle_WE();
write_bus((row_addr >> 16) & 0xFF);  // 行地址高8位
toggle_WE();

// 3. 发数据
set_CLE(0); set_ALE(0);
for(i=0; i<page_size; i++) {
    write_bus(data[i]);
    toggle_WE();
}

// 4. 发确认命令 0x10
set_CLE(1); set_ALE(0);
write_bus(0x10);
toggle_WE();

这里有个坑:地址周期数取决于Flash的容量和总线宽度。8位总线通常要5个地址周期(2列+3行),16位总线可能只要4个。我曾经因为地址周期数搞错,导致读写数据错位,查了半天才发现是地址映射问题。

重点:总线复用的核心在于时序的精确控制。CLE和ALE的建立时间(tCS、tALS)和保持时间(tCH、tALH)必须满足Flash datasheet的要求。我在验证时,会专门写一个时序检查的testbench,遍历所有PVT corner。

嗯,最后说一句。接口协议这部分,光看文档是不够的。我建议你拿一块开发板,用逻辑分析仪抓一下实际的波形。看看CLE、ALE、WE#、RE#这些信号是不是真的按协议在跳。我当年就是这么干的,抓完波形后,很多疑惑就解开了。