4、Flash存储器原理:NOR Flash与NAND Flash区别、Flash驱动开发要点、磨损均衡

好,咱们今天聊聊Flash。做ADAS系统,Flash是绕不开的。你想想看,Bootloader要存,应用程序要存,关键配置参数也要存。选错Flash,或者驱动写得不好,轻则升级失败,重则系统变砖。我这些年踩过的坑,有一半都跟Flash有关。

Flash存储器,说白了就是一块能电擦除、电写入的非易失性存储器。但同样是Flash,NOR和NAND完全是两码事。咱们先把这个最基础的区别搞清楚。

4.1 NOR Flash与NAND Flash:核心区别

我个人习惯把NOR Flash想象成“内存的远房亲戚”。为什么?因为它支持随机访问。你给它一个地址,它就能直接返回那个地址的数据。读操作跟读SRAM差不多,速度快,延迟低。这在ADAS里有什么用?比如代码直接在NOR Flash上执行(XIP,Execute In Place),省掉拷贝到RAM的步骤,上电就能跑。

NAND Flash呢?它更像“硬盘的微型版”。它按页(Page)读写,按块(Block)擦除。你不能直接读某个字节,得先把整页数据读到RAM里,再从中取你要的字节。写操作也一样,得先擦除整个块,再写页。这操作模式,跟NOR Flash完全不同。

我整理了一个表格,方便你对比:

特性 NOR Flash NAND Flash
读操作 随机访问,类似RAM,快 按页读取,需先读入缓冲区,较慢
写操作 按字节/字写入,但擦除前需先擦除整个扇区 按页写入,但擦除前需先擦除整个块
擦除操作 按扇区擦除(通常64KB~128KB) 按块擦除(通常128KB~1MB)
容量 较小(通常1MB~64MB) 较大(通常128MB~8GB以上)
成本 较高(每MB价格) 较低(每MB价格)
可靠性 坏块极少,位翻转概率低 出厂就有坏块,使用中还会产生新坏块,需ECC
XIP支持 支持(代码可直接运行) 不支持(需拷贝到RAM执行)
典型应用 Bootloader、关键参数存储 大容量固件存储、日志记录

看到这个表,你应该明白了。在ADAS系统里,我通常的做法是:NOR Flash放Bootloader和关键配置,因为要保证上电就能跑,而且不能出错。NAND Flash放应用程序和升级包,因为容量大、成本低。当然,有些高端芯片直接把NOR和NAND封装在一起,叫“多芯片封装”(MCP),省空间。

核心观点: 选NOR还是NAND,不是技术问题,是成本和可靠性的权衡。Bootloader必须用NOR,因为要XIP。应用程序可以用NAND,但必须做好坏块管理和ECC。

4.2 Flash驱动开发要点

驱动开发,嗯,这里要注意。很多人觉得Flash驱动就是调几个API,读、写、擦除。其实没那么简单。我见过太多因为驱动写得糙,导致升级到一半系统死掉的案例。

我个人总结,Flash驱动开发有四个关键点:

  1. 时序控制要精准:Flash操作有严格的时序要求。比如写一个字节,需要先发命令、再发地址、再发数据,最后等芯片内部完成(通过查询状态寄存器或等待忙信号)。时序不对,数据就写歪了。
  2. 擦除操作要谨慎:擦除是Flash最耗时的操作,一个扇区擦除可能要几百毫秒。而且擦除期间,整个Flash芯片都处于忙状态,不能做其他操作。所以,擦除操作必须在关中断的环境下执行,否则中断一来,时序就乱了。
  3. 写操作前必须擦除:Flash的特性是,只能从1写成0,不能从0写成1。所以写之前,必须先把目标区域擦除成全1。这个很多人会忘,尤其是从RAM开发转过来的。
  4. 错误处理要完善:Flash操作可能失败。比如电压不稳、芯片老化、坏块等。驱动必须能检测到错误(比如超时、状态寄存器异常),并返回错误码。不能死等,更不能直接崩溃。

这里给一个简单的NOR Flash写字节示例,注意看时序:

// 伪代码:写一个字节到NOR Flash
int nor_flash_write_byte(uint32_t addr, uint8_t data) {
    // 1. 发送写使能命令
    flash_send_command(CMD_WRITE_ENABLE);
    
    // 2. 发送页编程命令
    flash_send_command(CMD_PAGE_PROGRAM);
    
    // 3. 发送24位地址(高字节在前)
    flash_send_address(addr, 3);
    
    // 4. 发送数据
    flash_send_data(data);
    
    // 5. 等待操作完成(轮询状态寄存器)
    uint32_t timeout = 100000;
    while (timeout--) {
        uint8_t status = flash_read_status();
        if (status & STATUS_BUSY) {
            // 还在忙,继续等
            continue;
        }
        if (status & STATUS_WRITE_ENABLE_LATCH) {
            // 写使能锁存已清除,说明操作完成
            return 0; // 成功
        }
    }
    
    // 6. 超时处理
    return -1; // 失败
}
我的经验: 写Flash驱动时,一定要加超时机制。我曾经遇到过一颗老化的Flash,擦除操作需要2秒才完成,而我的超时设了500ms。结果每次升级都失败,排查了三天才发现是超时太短。从那以后,我习惯把超时设成典型值的10倍。

4.3 磨损均衡:为什么需要?怎么做?

磨损均衡,这个词听起来高大上,其实道理很简单。Flash的擦写次数是有限的。NOR Flash一般10万次,NAND Flash一般1万~10万次(取决于工艺)。如果你总是往同一个地址写数据,那个地址很快就报废了。而其他地址还“年轻力壮”。

为什么会这样?因为Flash的擦除操作会对氧化层造成物理损伤。每擦除一次,氧化层就薄一点。擦除次数多了,氧化层就击穿了,那个块就变成坏块了。

在ADAS系统里,哪些数据会频繁写入?最常见的就是系统日志配置参数。比如车辆每次上电,都要记录一个启动时间戳。如果不做磨损均衡,那个存时间戳的地址可能几个月就挂了。

磨损均衡的核心思想就一句话:把写操作均匀分布到所有可用的存储块上。具体怎么做?有两种常见策略:

  • 动态磨损均衡:每次写数据时,找一个擦除次数最少的块来写。这需要维护一个“擦除次数表”,记录每个块的擦除次数。写之前查表,选最年轻的块。
  • 静态磨损均衡:除了动态均衡,还会把长期不动的“冷数据”(比如Bootloader代码)搬移到擦除次数较多的块上,把“热数据”搬移到擦除次数少的块上。这样能更均匀地磨损所有块。

对于ADAS系统,我个人建议:Bootloader和应用程序所在的区域,不需要做磨损均衡,因为写一次就不动了。但参数存储区和日志区,必须做磨损均衡。而且,磨损均衡算法不能太复杂,否则会影响启动速度。我一般用“日志型”磨损均衡:把参数存储区划分成多个槽位,每次写参数时,写到下一个槽位,写满一圈后再从头开始。这样每个槽位的擦写次数就均匀了。

避坑指南: 我曾经在一个项目里,为了省事,没有做磨损均衡。结果量产半年后,有客户反馈系统频繁死机。排查发现,是参数存储区的Flash块已经擦写超过10万次,彻底报废了。从那以后,我所有涉及频繁写入的项目,必做磨损均衡。别偷懒,这个坑迟早会踩。

最后,总结一下。Flash选型,NOR还是NAND,看你的应用场景。驱动开发,时序、擦除、错误处理,一个都不能少。磨损均衡,别等到Flash报废了才想起来做。嗯,这些就是我这些年跟Flash打交道的一些心得。下一节,咱们聊聊具体的Bootloader分区设计,那又是另一个有意思的话题。