4、Flash基本操作命令:Read、Program、Erase命令集、状态寄存器读取、ID读取
好,咱们今天来聊聊Flash操作中最核心的几个命令。说白了,你玩Flash,翻来覆去就这几件事:读数据、写数据、擦数据,外加看看芯片状态、认认芯片是谁。
我个人习惯把Flash操作比作「在黑板上写字」。读操作就是看黑板上的字,写操作就是往上写新字,擦操作就是把整块黑板擦干净。嗯,这个比喻虽然简单,但很贴切。
4.1 Read命令:最基础的操作
读操作是所有操作里最简单的。你给个地址,Flash就把那个地址的数据吐出来。我刚开始做驱动时,第一个调通的就是读命令——毕竟读不出来,后面啥都别谈。
常见的读命令格式是这样的:
// 伪代码示例:Read操作
CS = 0; // 片选拉低,选中芯片
SPI_Write(0x03); // 发送Read命令码 0x03
SPI_Write(addr >> 16); // 发送24位地址的高8位
SPI_Write(addr >> 8); // 发送中间8位
SPI_Write(addr & 0xFF); // 发送低8位
data = SPI_Read(); // 读取一个字节数据
CS = 1; // 片选拉高,结束操作
这里有个细节要注意:地址是24位的。为什么?因为大部分SPI Flash的容量在16MB以内,24位地址刚好够用。如果你用的是大容量Flash(比如32MB以上),那就得用4字节地址模式,命令码是0x13。
关键点:读操作不需要先擦除。你随时可以读任何地址,哪怕那个地址是空的或者已经写过数据。这是Flash和EEPROM的一个区别——EEPROM可以按字节改写,Flash不行。
4.2 Program命令:写数据到Flash
写操作就有点讲究了。Flash的写操作叫Program,不是Write。为什么?因为Flash的写入机制是「把1变成0」,而不是「把0变成1」。把1变成0很容易,反过来就难了——那得靠擦除操作。
我遇到过不少新手,上来就写数据,结果发现写不进去。为啥?因为那片区域之前写过数据,里面还有0,你没法把0再写成1。必须先擦除,把整块变成全1,然后再写。
常见的Page Program命令格式:
// 伪代码示例:Page Program操作
CS = 0;
SPI_Write(0x02); // Page Program命令码 0x02
SPI_Write(addr >> 16); // 地址高8位
SPI_Write(addr >> 8); // 地址中8位
SPI_Write(addr & 0xFF); // 地址低8位
for(i = 0; i < 256; i++) {
SPI_Write(data[i]); // 写入一页数据,最多256字节
}
CS = 1;
注意:Page Program一次最多写256字节,也就是一页。你不能跨页写。如果你想写的数据超过256字节,得分多次写,每次换页地址。我曾经在一个项目里踩过这个坑——数据写到一半,地址跨页了,结果后面的数据全写到了页首,把之前的数据覆盖了。
4.3 Erase命令:擦除操作
擦除操作是Flash特有的。它把整块区域恢复成全1状态。擦除的单位比较大,不像读和写可以按字节来。
常见的擦除命令有三种:
| 擦除类型 | 命令码 | 擦除大小 | 典型时间 |
|---|---|---|---|
| Sector Erase | 0x20 | 4KB | ~45ms |
| Block Erase (32KB) | 0x52 | 32KB | ~120ms |
| Block Erase (64KB) | 0xD8 | 64KB | ~250ms |
| Chip Erase | 0xC7 | 整片 | ~几秒 |
你想想看,擦除时间比读写慢得多。一个Sector擦除要45ms,而读一个字节只要几十纳秒。所以设计分区策略时,要尽量减少擦除次数。我一般建议:能追加写就别擦,能大块擦就别小块擦。
// 伪代码示例:Sector Erase操作
CS = 0;
SPI_Write(0x06); // 先发送Write Enable命令
CS = 1;
CS = 0;
SPI_Write(0x20); // Sector Erase命令码
SPI_Write(addr >> 16); // 地址高8位
SPI_Write(addr >> 8); // 地址中8位
SPI_Write(addr & 0xFF); // 地址低8位
CS = 1;
// 等待擦除完成
while(Read_Status() & 0x01); // 检查BUSY位
小技巧:擦除前一定要先发Write Enable(0x06)命令。这是Flash的保护机制——防止误操作把数据擦没了。我见过有人忘了这步,折腾了半天才发现问题。
4.4 状态寄存器读取
状态寄存器是Flash的「健康报告」。你发一个命令,它告诉你当前忙不忙、写保护有没有开、擦除有没有出错。
读取状态寄存器的命令是0x05:
// 读取状态寄存器
CS = 0;
SPI_Write(0x05); // Read Status Register命令
status = SPI_Read(); // 读取状态值
CS = 1;
// 检查各个位
if(status & 0x01) {
// BUSY位为1,芯片正忙
}
if(status & 0x02) {
// WEL位为1,写使能已打开
}
if(status & 0x20) {
// EPE位为1,擦除/编程出错
}
状态寄存器各位的含义:
| 位 | 名称 | 说明 |
|---|---|---|
| Bit 0 | BUSY | 1=芯片忙,0=空闲 |
| Bit 1 | WEL | 1=写使能已打开 |
| Bit 2 | BP0 | 块保护位0 |
| Bit 3 | BP1 | 块保护位1 |
| Bit 4 | BP2 | 块保护位2 |
| Bit 5 | EPE | 擦除/编程错误标志 |
| Bit 6 | SRWD | 状态寄存器写保护 |
我个人习惯在每次Program或Erase操作后都读一下状态寄存器,检查有没有出错。尤其是批量生产时,偶尔会遇到个别Flash芯片擦除失败的情况。提前发现,提前处理,省得后面排查问题。
4.5 ID读取:识别芯片身份
每个Flash芯片都有一个唯一的ID。读ID命令是0x9F,返回三个字节:厂商ID、内存类型ID、容量ID。
// 读取JEDEC ID
CS = 0;
SPI_Write(0x9F); // Read JEDEC ID命令
manufacturer = SPI_Read(); // 厂商ID,比如0xEF是Winbond
memory_type = SPI_Read(); // 内存类型
capacity = SPI_Read(); // 容量,比如0x14表示16MB
CS = 1;
常见的厂商ID:
- 0xEF — Winbond(华邦)
- 0xC8 — GigaDevice(兆易创新)
- 0x1C — EON(宜扬)
- 0x20 — Micron(美光)
- 0xBF — Macronix(旺宏)
为什么要读ID?我举个例子。有一次项目量产,采购换了批Flash芯片,说是兼容的。结果程序跑起来各种奇怪问题。我一读ID,发现厂商ID变了,时序参数不一样。从那以后,我坚持在驱动初始化时先读ID校验,不匹配就直接报错。这叫「防呆设计」。
核心总结:Flash的基本操作就这四类——读、写、擦、查状态。读操作最简单,写操作要记得先擦除,擦除操作最耗时,状态寄存器是调试的好帮手,ID读取是兼容性检查的第一道防线。把这几个命令吃透了,Flash驱动开发就入门了。
嗯,下一章咱们聊聊Flash的时序参数和性能优化。到时候我会分享一些实际项目中调优的经验,比如怎么把读写速度从2MB/s提到10MB/s以上。敬请期待。