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,层层递进。
这张图从顶层到底层,把接口标准、时序模式、命令集、多通道多Die串起来了。你写驱动时,心里要有这张图——知道当前操作在哪个层级,才能做出正确的时序决策。
好了,本章就到这里。记住:协议是死的,板子是活的。多拿示波器看看波形,比看一百遍手册都管用。