3. NAND Flash接口协议:ONFI与Toggle标准

做NAND驱动,第一关就是搞懂接口协议。说白了,就是芯片和控制器之间怎么说话。目前主流就两套标准:ONFI和Toggle。我早年刚入行时,总觉得协议嘛,看看手册就行。后来踩了坑才明白——时序细节才是真功夫。

3.1 ONFI与Toggle:两大阵营

ONFI(Open NAND Flash Interface)由Intel、Micron等公司推动,走的是标准化路线。Toggle由Samsung、Toshiba主导,更强调性能。两者电气特性不同,但命令集基本兼容。

特性 ONFI Toggle
发起方 Intel、Micron、SK Hynix Samsung、Toshiba(Kioxia)
同步时钟 CLK(单沿/双沿) DQS(源同步)
数据选通 DQS(与CLK对齐) DQS(与数据边沿对齐)
最高速率(当前) ONFI 5.1: 3600 MT/s Toggle 6.0: 3200 MT/s
命令集 标准命令集 与ONFI高度兼容

你想想看,实际项目中选型,往往不是性能决定一切。我记得有个项目,主控只支持ONFI,但采购非要上Samsung的Toggle颗粒。结果呢?硬生生加了一层协议转换,延迟多了几十纳秒。嗯,这种坑我建议你提前规避。

3.2 异步时序与同步时序

NAND接口有两种工作模式:异步和同步。异步模式靠RE#/WE#信号控制,每个上升沿传输一个字节。同步模式则引入时钟,数据在时钟沿批量传输。

异步模式

异步模式是基础,所有NAND都支持。信号简单:CE#、CLE、ALE、WE#、RE#、WP#、R/B#、I/O[7:0]。时序参数如tRC、tWC、tRP、tWP等,决定了访问速度。

关键时序参数(异步)

  • tRC(读周期时间):最小15ns(66MHz)
  • tWC(写周期时间):最小15ns
  • tRP(RE#脉冲宽度):最小7.5ns
  • tWP(WE#脉冲宽度):最小7.5ns

同步模式

同步模式引入CLK和DQS。数据在CLK的双沿(DDR)传输,速率翻倍。ONFI用CLK作为主时钟,DQS与CLK对齐;Toggle用DQS作为源同步时钟,数据与DQS边沿对齐。

我建议你在驱动初始化时,先读ONFI参数页或Toggle ID,确认芯片支持的最高速率。然后根据PCB走线质量,适当降频。我曾经遇到一块板子,走线长了2cm,同步模式死活跑不到400MT/s。降一档就好了——硬件工程师死活不承认,最后用示波器一量,信号眼图都闭上了。

3.3 命令集解析:Read、Program、Erase、Status

NAND命令集其实不复杂。核心就四类:读、写、擦、状态查询。每个命令由多个周期组成:命令周期、地址周期、数据周期。

Read(读)

标准读命令是0x00-0x30。先发0x00,再送5个地址周期(列地址+行地址),最后发0x30启动读操作。之后等待tR(读延迟),再读数据。

// 异步读流程示例
void nand_read_page(uint32_t row_addr, uint16_t col_addr) {
    // 1. 发送命令 0x00
    write_cmd(0x00);
    // 2. 发送5个地址周期
    write_addr(col_addr & 0xFF);           // 列地址低8位
    write_addr((col_addr >> 8) & 0x0F);    // 列地址高4位
    write_addr(row_addr & 0xFF);           // 行地址低8位
    write_addr((row_addr >> 8) & 0xFF);    // 行地址中8位
    write_addr((row_addr >> 16) & 0xFF);   // 行地址高8位
    // 3. 发送命令 0x30
    write_cmd(0x30);
    // 4. 等待 tR(读延迟)
    wait_for_ready();
    // 5. 读取数据
    for (int i = 0; i < PAGE_SIZE; i++) {
        data[i] = read_data();
    }
}

避坑指南

我曾经在调试时发现,读出来的数据总是错位。查了半天,原来是地址周期顺序搞反了。NAND地址是先送列地址(低2字节),再送行地址(高3字节)。别问我怎么知道的——示波器上抓了三个小时。

Program(写)

写命令是0x80-0x10。先发0x80,送地址,再送数据,最后发0x10启动编程。等待tPROG(编程延迟)。

// 异步写流程示例
void nand_program_page(uint32_t row_addr, uint16_t col_addr, uint8_t *data) {
    // 1. 发送命令 0x80
    write_cmd(0x80);
    // 2. 发送5个地址周期
    write_addr(col_addr & 0xFF);
    write_addr((col_addr >> 8) & 0x0F);
    write_addr(row_addr & 0xFF);
    write_addr((row_addr >> 8) & 0xFF);
    write_addr((row_addr >> 16) & 0xFF);
    // 3. 发送数据
    for (int i = 0; i < PAGE_SIZE; i++) {
        write_data(data[i]);
    }
    // 4. 发送命令 0x10
    write_cmd(0x10);
    // 5. 等待 tPROG
    wait_for_ready();
}

Erase(擦除)

擦除命令是0x60-0xD0。只送行地址(3个周期),不送列地址。因为擦除以块为单位,块大小通常是256页或512页。

void nand_erase_block(uint32_t row_addr) {
    // 1. 发送命令 0x60
    write_cmd(0x60);
    // 2. 发送3个行地址周期
    write_addr(row_addr & 0xFF);
    write_addr((row_addr >> 8) & 0xFF);
    write_addr((row_addr >> 16) & 0xFF);
    // 3. 发送命令 0xD0
    write_cmd(0xD0);
    // 4. 等待 tBERS(擦除延迟)
    wait_for_ready();
}

Status(状态查询)

状态查询命令是0x70。发完命令后,直接读I/O口,返回一个字节的状态寄存器。

位 含义
SR[0] 0=就绪,1=忙
SR[1] 0=成功,1=失败
SR[2:5] 保留
SR[6] 0=正常,1=写保护
SR[7] 0=正常,1=ECC错误

注意

状态查询命令0x70会清除R/B#引脚状态。如果你用中断方式检测R/B#,建议用0x71(只读状态,不改变引脚状态)。我有个项目就是在这里栽了跟头——中断一直触发,查了三天才发现是0x70把R/B#清掉了。

3.4 多通道与多Die操作

单颗NAND芯片性能有限。要提高吞吐量,就得用多通道和多Die并行操作。

多通道

每个通道有独立的CE#、RE#、WE#、DQS等信号。控制器可以同时操作多个通道,实现真正的并行访问。

多通道优势

  • 带宽线性叠加:4通道x400MT/s = 1600MT/s
  • 延迟隐藏:一个通道在等tR,另一个通道可以发命令
  • 坏块隔离:一个通道坏块不影响其他通道

多Die

一颗NAND封装里可能有多个Die。每个Die有独立的R/B#引脚,但共享CE#和I/O总线。多Die操作的关键是交错访问——在一个Die忙时,操作另一个Die。

我建议你设计驱动时,把多Die看作多个独立的设备。每个Die维护自己的状态机。调度器负责在Die之间切换,最大化总线利用率。

// 多Die交错读示例
void nand_interleaved_read(uint8_t die_id, uint32_t row_addr) {
    // 选择Die(通过LUN地址)
    write_cmd(0x00);
    write_addr(col_addr);
    write_addr(row_addr);
    write_cmd(0x30);
    // 不等待tR,立即切换到下一个Die
    select_die(next_die_id);
    write_cmd(0x00);
    write_addr(next_col_addr);
    write_addr(next_row_addr);
    write_cmd(0x30);
    // 等所有Die就绪后,依次读数据
    for (int i = 0; i < NUM_DIES; i++) {
        select_die(i);
        wait_for_ready();
        read_data();
    }
}

经验之谈

多Die操作时,注意Die之间的干扰。我记得有个项目,两个Die同时编程,结果互相干扰导致数据出错。后来加了500ns的延迟间隔,问题就解决了。嗯,有时候硬件手册不会告诉你这些。

3.5 知识体系总览

下面这张图,把本章的核心逻辑串起来了。从接口标准到命令集,再到多通道多Die,层层递进。

NAND Flash接口协议知识体系 NAND Flash接口协议知识体系 ONFI标准 Toggle标准 异步时序 同步时序 DDR双沿 Read (0x00-0x30) Program (0x80-0x10) Erase (0x60-0xD0) Status (0x70/0x71) 多通道并行 多Die交错 核心目标:最大化总线利用率,隐藏操作延迟

这张图从顶层到底层,把接口标准、时序模式、命令集、多通道多Die串起来了。你写驱动时,心里要有这张图——知道当前操作在哪个层级,才能做出正确的时序决策。

好了,本章就到这里。记住:协议是死的,板子是活的。多拿示波器看看波形,比看一百遍手册都管用。