用Arduino/ESP32实战SPI Flash存储从W25Q16到GD25Q128的完整指南在物联网和嵌入式开发中本地存储常常成为项目瓶颈。当ESP32的内置闪存不够用或者需要独立存储传感器数据时SPI Flash芯片如W25Q16和GD25Q128就成了性价比极高的解决方案。本文将带你从硬件连接到代码实现彻底掌握这类芯片的使用技巧。1. 硬件准备与连接SPI Flash芯片虽然型号各异但接口标准高度统一。以常见的W25Q1616Mb和GD25Q128128Mb为例它们的引脚定义几乎完全相同引脚名称功能说明开发板连接建议CS片选信号任意GPIO如D5DO数据输出MISOESP32的GPIO19WP写保护通常接高电平3.3VDI数据输入MOSIESP32的GPIO23CLK时钟信号ESP32的GPIO18HOLD暂停操作通常接高电平3.3VVCC电源3.3V开发板3.3V输出GND地线开发板GND注意不同容量的芯片工作电流可能不同GD25Q128最大工作电流约25mA建议直接使用开发板的3.3V输出避免使用电平转换模块引入额外功耗。连接示例如下以ESP32为例/* * ESP32与W25Q16连接示例 * CS - GPIO5 * DO - GPIO19 (MISO) * DI - GPIO23 (MOSI) * CLK - GPIO18 (SCK) */ #include SPI.h #define FLASH_CS 5 void setup() { pinMode(FLASH_CS, OUTPUT); digitalWrite(FLASH_CS, HIGH); // 初始保持不选中 SPI.begin(); }2. 芯片识别与初始化不同厂商的SPI Flash有独特的识别码正确读取这些信息是验证硬件连接的第一步。以下是常见芯片的ID特征厂商设备ID十六进制容量标识WinbondEF401516MbWinbondEF401764MbWinbondEF4018128MbGigaDeviceC8401516MbGigaDeviceC84018128Mb通过JEDEC ID读取命令0x9F可以获取这些信息。以下是完整的识别代码void readFlashID() { digitalWrite(FLASH_CS, LOW); SPI.transfer(0x9F); // JEDEC ID命令 byte manufacturer SPI.transfer(0); byte memoryType SPI.transfer(0); byte capacity SPI.transfer(0); digitalWrite(FLASH_CS, HIGH); Serial.print(Manufacturer ID: 0x); Serial.println(manufacturer, HEX); Serial.print(Memory Type: 0x); Serial.println(memoryType, HEX); Serial.print(Capacity: 0x); Serial.println(capacity, HEX); }遇到识别失败时可以检查以下几点SPI时钟频率是否过高建议初始使用1MHz片选信号是否有效拉低电源电压是否稳定3.3V±10%硬件连接是否有虚焊3. 基础存储操作实战3.1 页编程与读取SPI Flash的基本写入单位是页通常256字节以下是页写入和读取的典型流程// 写入一页数据256字节 void writePage(uint32_t addr, byte* data) { digitalWrite(FLASH_CS, LOW); SPI.transfer(0x06); // 写使能 digitalWrite(FLASH_CS, HIGH); digitalWrite(FLASH_CS, LOW); SPI.transfer(0x02); // 页编程命令 SPI.transfer(addr 16); SPI.transfer(addr 8); SPI.transfer(addr); for(int i0; i256; i) { SPI.transfer(data[i]); } digitalWrite(FLASH_CS, HIGH); while(readStatus() 0x01); // 等待写入完成 } // 读取数据 void readData(uint32_t addr, byte* buffer, uint16_t len) { digitalWrite(FLASH_CS, LOW); SPI.transfer(0x03); // 读数据命令 SPI.transfer(addr 16); SPI.transfer(addr 8); SPI.transfer(addr); for(int i0; ilen; i) { buffer[i] SPI.transfer(0); } digitalWrite(FLASH_CS, HIGH); }3.2 扇区擦除在写入前必须先擦除存储区域最小的擦除单位通常是4KB扇区void sectorErase(uint32_t addr) { digitalWrite(FLASH_CS, LOW); SPI.transfer(0x06); // 写使能 digitalWrite(FLASH_CS, HIGH); digitalWrite(FLASH_CS, LOW); SPI.transfer(0x20); // 扇区擦除命令 SPI.transfer(addr 16); SPI.transfer(addr 8); SPI.transfer(addr); digitalWrite(FLASH_CS, HIGH); while(readStatus() 0x01); // 等待擦除完成 }重要提示擦除操作耗时较长典型值100-400ms在此期间读取状态寄存器会返回忙状态。连续擦除多个扇区时建议加入适当延迟。4. 高级功能与性能优化4.1 四线模式QSPI部分高端芯片支持四线模式将数据传输速率提升四倍。以GD25Q128为例void enableQSPI() { digitalWrite(FLASH_CS, LOW); SPI.transfer(0x35); // 读配置寄存器 byte config SPI.transfer(0); digitalWrite(FLASH_CS, HIGH); if(!(config 0x02)) { // 检查QSPI是否已启用 digitalWrite(FLASH_CS, LOW); SPI.transfer(0x06); // 写使能 digitalWrite(FLASH_CS, HIGH); digitalWrite(FLASH_CS, LOW); SPI.transfer(0x31); // 写状态寄存器 SPI.transfer(config | 0x02); // 设置QSPI使能位 digitalWrite(FLASH_CS, HIGH); } }启用后读写操作需要使用QSPI专用命令如0xEB替代0x03。4.2 磨损均衡策略SPI Flash的每个扇区有约10万次擦写寿命长期使用时需要实现磨损均衡struct SectorInfo { uint32_t address; uint32_t eraseCount; }; void wearLevelingWrite(uint32_t baseAddr, byte* data) { static SectorInfo sectors[8]; // 8个扇区轮换 static uint8_t currentSector 0; // 初始化扇区信息 if(sectors[0].eraseCount 0) { for(int i0; i8; i) { sectors[i].address baseAddr i*4096; sectors[i].eraseCount readEraseCount(sectors[i].address); } } // 选择磨损最少的扇区 uint8_t target 0; for(int i1; i8; i) { if(sectors[i].eraseCount sectors[target].eraseCount) { target i; } } sectorErase(sectors[target].address); writePage(sectors[target].address, data); sectors[target].eraseCount; // 定期将磨损计数保存到Flash末尾 if(currentSector 8) { saveWearInfo(baseAddr 32768, sectors); // 保存到32KB位置 currentSector 0; } }5. 实际项目应用案例5.1 网页文件存储系统将ESP32的Web服务器文件存储在外部SPI Flash中bool serveFileFromFlash(WebServer server, const char* path) { FileInfo file lookupFile(path); // 自定义文件查找函数 if(!file.valid) return false; server.setContentLength(file.size); server.send(200, file.mimeType, ); byte buffer[256]; uint32_t remain file.size; uint32_t addr file.address; while(remain 0) { uint16_t chunk min(remain, 256); readData(addr, buffer, chunk); server.sendContent_P((const char*)buffer, chunk); addr chunk; remain - chunk; } return true; }5.2 传感器数据记录仪实现一个低功耗的数据记录系统struct SensorData { uint32_t timestamp; float temperature; float humidity; }; void logSensorData() { static uint32_t logAddress 0; if(logAddress 1048576) { // 1MB存储空间已满 return; } SensorData data; data.timestamp getUnixTime(); data.temperature readTemperature(); data.humidity readHumidity(); // 检查是否需要先擦除 if((logAddress % 4096) 0) { sectorErase(logAddress); } writePage(logAddress, (byte*)data); logAddress sizeof(SensorData); // 深度睡眠直到下次记录 esp_sleep_enable_timer_wakeup(300 * 1000000); // 5分钟 esp_deep_sleep_start(); }在项目开发中我发现GD25Q128的4KB擦除速度比W25Q16快约15%但写入速度基本相当。对于频繁写入的场景建议选择支持更小擦除单位如256字节的芯片型号。
别再死记硬背了!用Arduino/ESP32玩转W25Q16和GD25Q128 SPI Flash(附完整代码)
发布时间:2026/5/31 3:44:21
用Arduino/ESP32实战SPI Flash存储从W25Q16到GD25Q128的完整指南在物联网和嵌入式开发中本地存储常常成为项目瓶颈。当ESP32的内置闪存不够用或者需要独立存储传感器数据时SPI Flash芯片如W25Q16和GD25Q128就成了性价比极高的解决方案。本文将带你从硬件连接到代码实现彻底掌握这类芯片的使用技巧。1. 硬件准备与连接SPI Flash芯片虽然型号各异但接口标准高度统一。以常见的W25Q1616Mb和GD25Q128128Mb为例它们的引脚定义几乎完全相同引脚名称功能说明开发板连接建议CS片选信号任意GPIO如D5DO数据输出MISOESP32的GPIO19WP写保护通常接高电平3.3VDI数据输入MOSIESP32的GPIO23CLK时钟信号ESP32的GPIO18HOLD暂停操作通常接高电平3.3VVCC电源3.3V开发板3.3V输出GND地线开发板GND注意不同容量的芯片工作电流可能不同GD25Q128最大工作电流约25mA建议直接使用开发板的3.3V输出避免使用电平转换模块引入额外功耗。连接示例如下以ESP32为例/* * ESP32与W25Q16连接示例 * CS - GPIO5 * DO - GPIO19 (MISO) * DI - GPIO23 (MOSI) * CLK - GPIO18 (SCK) */ #include SPI.h #define FLASH_CS 5 void setup() { pinMode(FLASH_CS, OUTPUT); digitalWrite(FLASH_CS, HIGH); // 初始保持不选中 SPI.begin(); }2. 芯片识别与初始化不同厂商的SPI Flash有独特的识别码正确读取这些信息是验证硬件连接的第一步。以下是常见芯片的ID特征厂商设备ID十六进制容量标识WinbondEF401516MbWinbondEF401764MbWinbondEF4018128MbGigaDeviceC8401516MbGigaDeviceC84018128Mb通过JEDEC ID读取命令0x9F可以获取这些信息。以下是完整的识别代码void readFlashID() { digitalWrite(FLASH_CS, LOW); SPI.transfer(0x9F); // JEDEC ID命令 byte manufacturer SPI.transfer(0); byte memoryType SPI.transfer(0); byte capacity SPI.transfer(0); digitalWrite(FLASH_CS, HIGH); Serial.print(Manufacturer ID: 0x); Serial.println(manufacturer, HEX); Serial.print(Memory Type: 0x); Serial.println(memoryType, HEX); Serial.print(Capacity: 0x); Serial.println(capacity, HEX); }遇到识别失败时可以检查以下几点SPI时钟频率是否过高建议初始使用1MHz片选信号是否有效拉低电源电压是否稳定3.3V±10%硬件连接是否有虚焊3. 基础存储操作实战3.1 页编程与读取SPI Flash的基本写入单位是页通常256字节以下是页写入和读取的典型流程// 写入一页数据256字节 void writePage(uint32_t addr, byte* data) { digitalWrite(FLASH_CS, LOW); SPI.transfer(0x06); // 写使能 digitalWrite(FLASH_CS, HIGH); digitalWrite(FLASH_CS, LOW); SPI.transfer(0x02); // 页编程命令 SPI.transfer(addr 16); SPI.transfer(addr 8); SPI.transfer(addr); for(int i0; i256; i) { SPI.transfer(data[i]); } digitalWrite(FLASH_CS, HIGH); while(readStatus() 0x01); // 等待写入完成 } // 读取数据 void readData(uint32_t addr, byte* buffer, uint16_t len) { digitalWrite(FLASH_CS, LOW); SPI.transfer(0x03); // 读数据命令 SPI.transfer(addr 16); SPI.transfer(addr 8); SPI.transfer(addr); for(int i0; ilen; i) { buffer[i] SPI.transfer(0); } digitalWrite(FLASH_CS, HIGH); }3.2 扇区擦除在写入前必须先擦除存储区域最小的擦除单位通常是4KB扇区void sectorErase(uint32_t addr) { digitalWrite(FLASH_CS, LOW); SPI.transfer(0x06); // 写使能 digitalWrite(FLASH_CS, HIGH); digitalWrite(FLASH_CS, LOW); SPI.transfer(0x20); // 扇区擦除命令 SPI.transfer(addr 16); SPI.transfer(addr 8); SPI.transfer(addr); digitalWrite(FLASH_CS, HIGH); while(readStatus() 0x01); // 等待擦除完成 }重要提示擦除操作耗时较长典型值100-400ms在此期间读取状态寄存器会返回忙状态。连续擦除多个扇区时建议加入适当延迟。4. 高级功能与性能优化4.1 四线模式QSPI部分高端芯片支持四线模式将数据传输速率提升四倍。以GD25Q128为例void enableQSPI() { digitalWrite(FLASH_CS, LOW); SPI.transfer(0x35); // 读配置寄存器 byte config SPI.transfer(0); digitalWrite(FLASH_CS, HIGH); if(!(config 0x02)) { // 检查QSPI是否已启用 digitalWrite(FLASH_CS, LOW); SPI.transfer(0x06); // 写使能 digitalWrite(FLASH_CS, HIGH); digitalWrite(FLASH_CS, LOW); SPI.transfer(0x31); // 写状态寄存器 SPI.transfer(config | 0x02); // 设置QSPI使能位 digitalWrite(FLASH_CS, HIGH); } }启用后读写操作需要使用QSPI专用命令如0xEB替代0x03。4.2 磨损均衡策略SPI Flash的每个扇区有约10万次擦写寿命长期使用时需要实现磨损均衡struct SectorInfo { uint32_t address; uint32_t eraseCount; }; void wearLevelingWrite(uint32_t baseAddr, byte* data) { static SectorInfo sectors[8]; // 8个扇区轮换 static uint8_t currentSector 0; // 初始化扇区信息 if(sectors[0].eraseCount 0) { for(int i0; i8; i) { sectors[i].address baseAddr i*4096; sectors[i].eraseCount readEraseCount(sectors[i].address); } } // 选择磨损最少的扇区 uint8_t target 0; for(int i1; i8; i) { if(sectors[i].eraseCount sectors[target].eraseCount) { target i; } } sectorErase(sectors[target].address); writePage(sectors[target].address, data); sectors[target].eraseCount; // 定期将磨损计数保存到Flash末尾 if(currentSector 8) { saveWearInfo(baseAddr 32768, sectors); // 保存到32KB位置 currentSector 0; } }5. 实际项目应用案例5.1 网页文件存储系统将ESP32的Web服务器文件存储在外部SPI Flash中bool serveFileFromFlash(WebServer server, const char* path) { FileInfo file lookupFile(path); // 自定义文件查找函数 if(!file.valid) return false; server.setContentLength(file.size); server.send(200, file.mimeType, ); byte buffer[256]; uint32_t remain file.size; uint32_t addr file.address; while(remain 0) { uint16_t chunk min(remain, 256); readData(addr, buffer, chunk); server.sendContent_P((const char*)buffer, chunk); addr chunk; remain - chunk; } return true; }5.2 传感器数据记录仪实现一个低功耗的数据记录系统struct SensorData { uint32_t timestamp; float temperature; float humidity; }; void logSensorData() { static uint32_t logAddress 0; if(logAddress 1048576) { // 1MB存储空间已满 return; } SensorData data; data.timestamp getUnixTime(); data.temperature readTemperature(); data.humidity readHumidity(); // 检查是否需要先擦除 if((logAddress % 4096) 0) { sectorErase(logAddress); } writePage(logAddress, (byte*)data); logAddress sizeof(SensorData); // 深度睡眠直到下次记录 esp_sleep_enable_timer_wakeup(300 * 1000000); // 5分钟 esp_deep_sleep_start(); }在项目开发中我发现GD25Q128的4KB擦除速度比W25Q16快约15%但写入速度基本相当。对于频繁写入的场景建议选择支持更小擦除单位如256字节的芯片型号。