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)。

读页的流程:

  1. 发送读命令0x00
  2. 发送5个地址周期(列地址2个,行地址3个)
  3. 发送确认命令0x30
  4. 等待tR时间(典型25μs)
  5. 读取数据
// 读页函数示例
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)。

编程流程:

  1. 发送编程命令0x80
  2. 发送5个地址周期
  3. 写入数据(整页)
  4. 发送编程确认命令0x10
  5. 等待tPROG时间(典型200-700μs)
  6. 读取状态寄存器,检查是否成功
// 页编程函数
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)。

擦除流程:

  1. 发送擦除命令0x60
  2. 发送3个行地址(块地址)
  3. 发送确认命令0xD0
  4. 等待tBERS时间(典型1.5-2ms)
  5. 检查状态
// 块擦除函数
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可以获取制造商、容量、页大小、块大小等信息。

读取流程:

  1. 发送读ID命令0x90
  2. 发送地址0x00(标准ID读取)
  3. 连续读取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驱动里最让人头疼的部分——你想想看,芯片出厂就有坏块,用着用着还会产生新坏块,怎么处理?到时候我分享几个实战中的处理策略。