1. 项目概述
最近在ESP32平台上开发一个需要存储大量配置数据的项目,发现内部Flash空间捉襟见肘。这时候外置SPI Flash就成了救命稻草,而W25Q64这颗64Mbit(8MB)的存储芯片刚好能满足需求。今天我就来分享如何在ESP-IDF环境下,用短短五分钟快速搭建W25Q64的驱动框架。
W25Q64是Winbond公司推出的SPI接口NOR Flash,支持标准SPI、Dual SPI和Quad SPI三种通信模式。在嵌入式领域,它常被用作固件存储、数据记录或文件系统载体。ESP-IDF作为乐鑫官方的开发框架,已经内置了对SPI外设的良好支持,这让我们可以快速实现与W25Q64的通信。
2. 硬件准备与连接
2.1 硬件选型要点
选择W25Q64时要注意后缀版本,常见的有W25Q64JV(3V供电)和W25Q64FV(1.8V供电)。ESP32开发板通常采用3.3V电平,所以建议选用W25Q64JV型号。如果手头只有FV版本,需要额外添加电平转换电路。
2.2 典型接线方案
ESP32与W25Q64的标准SPI接线如下:
| ESP32引脚 | W25Q64引脚 | 备注 |
|---|---|---|
| GPIO23 | SCLK | SPI时钟线 |
| GPIO19 | MISO | 主入从出 |
| GPIO18 | MOSI | 主出从入 |
| GPIO5 | CS | 片选信号(可自定义) |
| 3.3V | VCC | 电源 |
| GND | GND | 地线 |
| GPIO21 | HOLD | 保持信号(可选) |
| GPIO22 | WP | 写保护(可选) |
注意:HOLD和WP引脚如果不使用,建议上拉到VCC。CS引脚可以根据实际需求选择其他GPIO,但要避免使用SPI默认的CS引脚(GPIO16)。
2.3 硬件布局建议
在实际PCB布局时:
- 尽量缩短SCLK走线长度,避免信号完整性问题
- MISO/MOSI建议等长布线
- 在VCC附近放置0.1μF去耦电容
- 如果走线较长(>10cm),建议串联22Ω电阻进行阻抗匹配
3. ESP-IDF环境配置
3.1 创建基础项目
首先使用ESP-IDF的模板创建一个新项目:
bash复制idf.py create-project spi_w25q64
cd spi_w25q64
3.2 配置SPI总线参数
在main/Kconfig.projbuild中添加SPI配置选项:
code复制config W25Q64_SPI_HOST
int "SPI Host Number"
range 0 2
default 1
help
Select SPI host controller (SPI1 recommended)
config W25Q64_CS_GPIO
int "CS GPIO Number"
range 0 33
default 5
help
GPIO number for CS signal
3.3 修改sdkconfig.defaults
设置默认SPI参数:
code复制CONFIG_W25Q64_SPI_HOST=1
CONFIG_W25Q64_CS_GPIO=5
CONFIG_SPI_MASTER_IN_IRAM=y
4. SPI驱动实现
4.1 SPI初始化代码
在main/main.c中添加以下初始化代码:
c复制#include "driver/spi_master.h"
#include "esp_log.h"
static const char* TAG = "W25Q64";
spi_device_handle_t spi;
void init_spi() {
spi_bus_config_t buscfg = {
.miso_io_num = GPIO_NUM_19,
.mosi_io_num = GPIO_NUM_18,
.sclk_io_num = GPIO_NUM_23,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 4096,
};
spi_device_interface_config_t devcfg = {
.clock_speed_hz = 10*1000*1000, // 10MHz
.mode = 0, // SPI mode 0
.spics_io_num = CONFIG_W25Q64_CS_GPIO,
.queue_size = 7,
.command_bits = 8,
.address_bits = 24,
};
ESP_ERROR_CHECK(spi_bus_initialize(CONFIG_W25Q64_SPI_HOST, &buscfg, 1));
ESP_ERROR_CHECK(spi_bus_add_device(CONFIG_W25Q64_SPI_HOST, &devcfg, &spi));
ESP_LOGI(TAG, "SPI initialized successfully");
}
4.2 W25Q64基本操作函数
实现基础的读写功能:
c复制#define W25Q64_CMD_READ_DATA 0x03
#define W25Q64_CMD_PAGE_PROGRAM 0x02
#define W25Q64_CMD_SECTOR_ERASE 0x20
void w25q64_read(uint32_t addr, uint8_t* data, size_t len) {
spi_transaction_t trans = {
.cmd = W25Q64_CMD_READ_DATA,
.addr = addr,
.length = len * 8,
.rx_buffer = data,
};
ESP_ERROR_CHECK(spi_device_transmit(spi, &trans));
}
void w25q64_write_page(uint32_t addr, uint8_t* data, size_t len) {
// 必须先擦除才能写入
spi_transaction_t trans = {
.cmd = W25Q64_CMD_PAGE_PROGRAM,
.addr = addr,
.length = len * 8,
.tx_buffer = data,
};
ESP_ERROR_CHECK(spi_device_transmit(spi, &trans));
// 等待写入完成
w25q64_wait_ready();
}
void w25q64_sector_erase(uint32_t addr) {
spi_transaction_t trans = {
.cmd = W25Q64_CMD_SECTOR_ERASE,
.addr = addr,
};
ESP_ERROR_CHECK(spi_device_transmit(spi, &trans));
w25q64_wait_ready();
}
4.3 状态检测与等待函数
实现状态检测功能:
c复制#define W25Q64_CMD_READ_STATUS1 0x05
bool w25q64_is_busy() {
uint8_t status;
spi_transaction_t trans = {
.cmd = W25Q64_CMD_READ_STATUS1,
.length = 8,
.rx_buffer = &status,
};
ESP_ERROR_CHECK(spi_device_transmit(spi, &trans));
return (status & 0x01);
}
void w25q64_wait_ready() {
while(w25q64_is_busy()) {
vTaskDelay(pdMS_TO_TICKS(1));
}
}
5. 功能测试与验证
5.1 基本读写测试
在app_main()中添加测试代码:
c复制void app_main() {
init_spi();
// 测试数据
uint8_t write_data[256];
uint8_t read_data[256];
memset(write_data, 0xAA, sizeof(write_data));
// 擦除第一个扇区(4KB)
w25q64_sector_erase(0x000000);
// 写入测试数据
w25q64_write_page(0x000000, write_data, sizeof(write_data));
// 读取验证
w25q64_read(0x000000, read_data, sizeof(read_data));
if(memcmp(write_data, read_data, sizeof(write_data)) == 0) {
ESP_LOGI(TAG, "Read/Write test passed!");
} else {
ESP_LOGE(TAG, "Read/Write test failed!");
}
}
5.2 性能优化技巧
-
提高SPI时钟频率:
- 初始测试使用10MHz,稳定后可尝试提升到40MHz
- 修改
devcfg.clock_speed_hz值并重新测试
-
启用DMA传输:
c复制
buscfg.dma_chan = SPI_DMA_CH_AUTO; -
使用Quad SPI模式(需要硬件支持):
c复制#define W25Q64_CMD_ENABLE_QPI 0x38 void w25q64_enable_qpi() { spi_transaction_t trans = { .cmd = W25Q64_CMD_ENABLE_QPI, }; ESP_ERROR_CHECK(spi_device_transmit(spi, &trans)); }
6. 常见问题排查
6.1 无法检测到芯片
- 检查电源电压(3.3V±10%)
- 用逻辑分析仪抓取SPI波形,确认CS信号是否正常
- 尝试降低SPI时钟频率(如1MHz)
- 检查PCB连接,特别是MISO/MOSI是否接反
6.2 写入数据异常
- 确保在写入前已执行扇区擦除
- 检查写入地址是否对齐(页编程必须256字节对齐)
- 写入后等待足够时间(典型页编程时间1-3ms)
6.3 读取速度慢
- 启用Fast Read命令(0x0B):
c复制#define W25Q64_CMD_FAST_READ 0x0B void w25q64_fast_read(uint32_t addr, uint8_t* data, size_t len) { spi_transaction_t trans = { .cmd = W25Q64_CMD_FAST_READ, .addr = addr, .length = len * 8, .rx_buffer = data, }; ESP_ERROR_CHECK(spi_device_transmit(spi, &trans)); } - 增加SPI时钟频率
- 使用DMA传输大数据块
7. 进阶应用建议
7.1 集成文件系统
可以将W25Q64用作SPIFFS或LittleFS的存储介质:
c复制#include "esp_vfs.h"
#include "esp_spiffs.h"
void init_spiffs() {
esp_vfs_spiffs_conf_t conf = {
.base_path = "/spiffs",
.partition_label = NULL,
.max_files = 5,
.format_if_mount_failed = true
};
// 注册SPI Flash为块设备
esp_partition_t part = {
.type = ESP_PARTITION_TYPE_DATA,
.subtype = ESP_PARTITION_SUBTYPE_DATA_SPIFFS,
.address = 0,
.size = 8*1024*1024,
.label = "storage",
.encrypted = false
};
ESP_ERROR_CHECK(esp_vfs_spiffs_register(&conf));
}
7.2 实现OTA升级
将W25Q64作为OTA存储分区:
c复制// 在partitions.csv中添加
# Name, Type, SubType, Offset, Size, Flags
otadata, data, ota, 0x10000, 0x2000,
ota_0, app, ota_0, 0x20000, 0x1A0000,
ota_1, app, ota_1, 0x1C0000, 0x1A0000,
storage, data, spiffs, 0x360000, 0x4A0000,
7.3 低功耗优化
- 进入深度睡眠前执行掉电命令:
c复制#define W25Q64_CMD_POWER_DOWN 0xB9 void w25q64_power_down() { spi_transaction_t trans = { .cmd = W25Q64_CMD_POWER_DOWN, }; ESP_ERROR_CHECK(spi_device_transmit(spi, &trans)); } - 唤醒后执行释放掉电命令:
c复制#define W25Q64_CMD_RELEASE_POWER_DOWN 0xAB void w25q64_release_power_down() { spi_transaction_t trans = { .cmd = W25Q64_CMD_RELEASE_POWER_DOWN, }; ESP_ERROR_CHECK(spi_device_transmit(spi, &trans)); vTaskDelay(pdMS_TO_TICKS(3)); // 等待唤醒时间 }
在实际项目中,我发现W25Q64的页编程操作有两点需要特别注意:一是必须确保目标区域已经擦除,二是连续写入不能跨页(每256字节为一页)。曾经因为忽略这两点导致数据写入异常,调试了很久才发现问题所在。建议在写入函数中添加地址检查逻辑,可以避免这类问题。