4. NAND Flash基本操作:Read Page,Program Page,Erase Block,Reset,Read ID
好,咱们今天来聊聊NAND Flash最核心的几个操作。说实话,这些操作就像你学开车时的挂挡、踩油门、踩刹车——看似基础,但每个动作背后都有门道。我当年刚接触NAND驱动时,就因为在Erase操作上少等了一个状态查询,结果整片数据全乱了。嗯,从那以后,我对这些基本操作就再也不敢马虎了。
4.1 操作前的准备:认识命令、地址与数据周期
NAND Flash的通信,说白了就是三个周期:命令周期、地址周期、数据周期。你想想看,芯片怎么知道你要干嘛?全靠命令码。比如0x00表示读,0x80表示写,0x60表示擦除。
每个操作都遵循固定的时序:
- 命令周期:CLE拉高,ALE拉低,IO口送命令码
- 地址周期:CLE拉低,ALE拉高,IO口送地址(通常分5个周期)
- 数据周期:CLE和ALE都拉低,IO口读写数据
我个人习惯:写驱动时,先把这三个周期的宏定义写好。比如:#define CMD_READ 0x00,这样后面代码可读性高,也不容易写错命令码。
4.2 Read Page(页读取)
读操作是最常用的。NAND Flash以页为单位读取,典型页大小是2KB或4KB,外加OOB区域(Out-Of-Band,通常64B或128B)。
读页的流程:
- 发送读命令0x00
- 发送5个地址周期(列地址2个,行地址3个)
- 发送确认命令0x30
- 等待tR时间(典型25μs)
- 读取数据
// 读页函数示例
void nand_read_page(uint32_t page_addr, uint8_t *buffer) {
uint8_t col_addr_low = 0x00; // 列地址低位,通常从0开始
uint8_t col_addr_high = 0x00; // 列地址高位
uint8_t row_addr[3]; // 行地址(页地址)
row_addr[0] = page_addr & 0xFF;
row_addr[1] = (page_addr >> 8) & 0xFF;
row_addr[2] = (page_addr >> 16) & 0xFF;
// 发送命令0x00
nand_send_cmd(0x00);
// 发送地址:2列 + 3行
nand_send_addr(col_addr_low);
nand_send_addr(col_addr_high);
nand_send_addr(row_addr[0]);
nand_send_addr(row_addr[1]);
nand_send_addr(row_addr[2]);
// 发送读确认命令
nand_send_cmd(0x30);
// 等待tR(查询R/B引脚或轮询状态)
while(nand_is_busy());
// 读取整页数据
for(int i = 0; i < PAGE_SIZE; i++) {
buffer[i] = nand_read_byte();
}
}
避坑指南:我曾经在读取OOB区域时,忘记把列地址设置到页末尾。结果读出来的全是主存区的数据。记住,OOB的起始列地址 = 主存区大小,比如2KB页就是0x0800。
4.3 Program Page(页编程)
写操作在NAND里叫Program。注意,NAND只能从1写成0,不能从0写成1。所以写之前必须先擦除(把整块变成全1)。
编程流程:
- 发送编程命令0x80
- 发送5个地址周期
- 写入数据(整页)
- 发送编程确认命令0x10
- 等待tPROG时间(典型200-700μs)
- 读取状态寄存器,检查是否成功
// 页编程函数
int nand_program_page(uint32_t page_addr, uint8_t *data) {
uint8_t status;
// 第一步:发送编程命令
nand_send_cmd(0x80);
// 第二步:发送地址
nand_send_addr(0x00); // 列地址低位
nand_send_addr(0x00); // 列地址高位
nand_send_addr(page_addr & 0xFF);
nand_send_addr((page_addr >> 8) & 0xFF);
nand_send_addr((page_addr >> 16) & 0xFF);
// 第三步:写入数据
for(int i = 0; i < PAGE_SIZE; i++) {
nand_write_byte(data[i]);
}
// 第四步:发送确认命令
nand_send_cmd(0x10);
// 第五步:等待编程完成
while(nand_is_busy());
// 第六步:检查状态
nand_send_cmd(0x70); // 读状态命令
status = nand_read_byte();
if(status & 0x01) {
return -1; // 编程失败
}
return 0; // 成功
}
注意:编程失败后,这个页就废了。NAND Flash不允许对同一个页重复编程。我见过有人想省事,在同一个页上写两次数据,结果第二次直接报错。正确的做法是写到新的页,或者先擦除整个块。
4.4 Erase Block(块擦除)
擦除是NAND里最耗时的操作,也是破坏性最大的。一次擦除一个块(通常128页或256页)。擦除后,整个块的数据变成全1(0xFF)。
擦除流程:
- 发送擦除命令0x60
- 发送3个行地址(块地址)
- 发送确认命令0xD0
- 等待tBERS时间(典型1.5-2ms)
- 检查状态
// 块擦除函数
int nand_erase_block(uint32_t block_addr) {
uint8_t status;
// 注意:块地址需要左移,因为一个块包含多个页
uint32_t page_addr = block_addr << PAGES_PER_BLOCK_SHIFT;
// 发送擦除命令
nand_send_cmd(0x60);
// 发送块地址(3个行地址)
nand_send_addr(page_addr & 0xFF);
nand_send_addr((page_addr >> 8) & 0xFF);
nand_send_addr((page_addr >> 16) & 0xFF);
// 发送确认命令
nand_send_cmd(0xD0);
// 等待擦除完成
while(nand_is_busy());
// 检查状态
nand_send_cmd(0x70);
status = nand_read_byte();
if(status & 0x01) {
return -1; // 擦除失败
}
return 0;
}
我踩过的坑:有一次我擦除时,地址传错了,传的是页地址而不是块地址。结果擦除命令执行了,但擦除的是错误的块。更惨的是,那个块里存着文件系统的关键数据。所以,擦除前一定要做地址校验。
4.5 Reset(复位)
复位操作很简单,但很重要。当NAND Flash卡死、状态异常或上电初始化时,发一个复位命令就能让它回到初始状态。
// 复位函数
void nand_reset(void) {
nand_send_cmd(0xFF);
// 等待tRST时间(典型5μs)
delay_us(10);
// 复位后,建议重新读取ID确认芯片正常
}
什么时候用复位?
- 芯片上电后,先复位一次确保状态稳定
- 操作超时或状态异常时,尝试复位恢复
- 切换操作模式前(比如从读切换到写)
小技巧:复位后,我习惯读一下状态寄存器。如果状态不是0xC0(就绪且无错误),说明芯片可能有问题,需要进一步排查。
4.6 Read ID(读取ID)
Read ID是识别NAND Flash型号的关键操作。不同厂商、不同型号的芯片,ID码不同。通过ID可以获取制造商、容量、页大小、块大小等信息。
读取流程:
- 发送读ID命令0x90
- 发送地址0x00(标准ID读取)
- 连续读取5个字节的ID数据
// 读取NAND ID
void nand_read_id(uint8_t *id) {
nand_send_cmd(0x90);
nand_send_addr(0x00);
for(int i = 0; i < 5; i++) {
id[i] = nand_read_byte();
}
}
// 解析ID示例
void parse_nand_id(uint8_t *id) {
printf("Maker ID: 0x%02X\n", id[0]);
printf("Device ID: 0x%02X\n", id[1]);
printf("3rd Byte: 0x%02X\n", id[2]);
printf("4th Byte: 0x%02X\n", id[3]);
printf("5th Byte: 0x%02X\n", id[4]);
// 常见厂商ID
switch(id[0]) {
case 0xEC: printf("Manufacturer: Samsung\n"); break;
case 0x98: printf("Manufacturer: Toshiba\n"); break;
case 0x2C: printf("Manufacturer: Micron\n"); break;
case 0xAD: printf("Manufacturer: Hynix\n"); break;
default: printf("Unknown manufacturer\n");
}
}
| ID字节 | 含义 | 示例值 |
|---|---|---|
| Byte 0 | 制造商ID | 0xEC (Samsung) |
| Byte 1 | 设备ID | 0xF1 (K9F2G08) |
| Byte 2 | 内部配置 | 0x00 |
| Byte 3 | 页大小/块大小 | 0x95 (2KB页, 128KB块) |
| Byte 4 | 容量信息 | 0x02 (256MB) |
重要提醒:有些芯片支持扩展ID读取(地址0x20或0x40),可以获取更多参数。但标准驱动里,读5个字节就够用了。我遇到过一颗国产芯片,它的ID前5个字节和三星一模一样,但后面字节不同。所以,如果要做兼容性设计,建议读全8个字节。
4.7 综合示例:上电初始化流程
好了,把上面这些操作串起来,就是一个完整的NAND Flash初始化流程:
void nand_init(void) {
uint8_t id[5];
// 1. 复位芯片
nand_reset();
// 2. 读取ID,确认芯片型号
nand_read_id(id);
if(id[0] != EXPECTED_MAKER_ID) {
printf("Error: NAND chip not detected!\n");
return;
}
// 3. 根据ID配置参数(页大小、块大小等)
configure_nand_params(id);
// 4. 可选:检查坏块标记
// 通常在第一个块(块0)的OOB区域读取坏块标记
printf("NAND Flash initialized successfully.\n");
}
嗯,到这里,NAND Flash的五个基本操作就讲完了。说白了,读、写、擦、复位、读ID,就是你和NAND芯片打交道的全部基础。我个人觉得,把这些操作写熟练了,后面做FTL(Flash Translation Layer)和文件系统驱动时,心里就有底了。
下一章,咱们聊聊坏块管理。这可是NAND驱动里最让人头疼的部分——你想想看,芯片出厂就有坏块,用着用着还会产生新坏块,怎么处理?到时候我分享几个实战中的处理策略。