1. 项目概述
最近在玩ESP32-C6开发板时,发现ESP-IDF框架中的SPI驱动接口有了重大更新。作为一个嵌入式开发者,我决定通过驱动经典的W25Q64 Flash芯片来验证新版SPI接口的使用方法。W25Q64是一款8MB容量的SPI Flash,在物联网设备中广泛用于固件存储和数据记录。
2. 硬件准备
2.1 所需材料
- ESP32-C6开发板(其他ESP32系列也可)
- W25Q64 Flash模块
- 杜邦线若干
- 面包板(可选)
2.2 硬件连接
W25Q64与ESP32的典型连接方式如下:
| W25Q64引脚 | ESP32引脚 | 功能说明 |
|---|---|---|
| CS | GPIO22 | 片选信号 |
| DO | GPIO19 | 数据输出 |
| WP | 不接 | 写保护 |
| GND | GND | 地线 |
| DI | GPIO20 | 数据输入 |
| CLK | GPIO21 | 时钟信号 |
| HOLD | 不接 | 保持信号 |
| VCC | 3.3V | 电源 |
注意:不同ESP32开发板的可用GPIO可能不同,请参考具体型号的引脚定义
3. ESP-IDF SPI驱动详解
3.1 SPI总线初始化
新版ESP-IDF的SPI驱动分为两个主要步骤:总线初始化和设备添加。
首先需要包含必要的头文件:
c复制#include <driver/spi_common.h>
#include <driver/spi_master.h>
初始化SPI总线使用spi_bus_initialize()函数:
c复制spi_bus_config_t bus_cfg = {
.miso_io_num = 19,
.mosi_io_num = 20,
.sclk_io_num = 21,
.quadhd_io_num = -1, // 不使用QHD
.quadwp_io_num = -1, // 不使用QWP
.max_transfer_sz = 4096 // DMA传输最大字节数
};
ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &bus_cfg, SPI_DMA_CH_AUTO));
关键参数说明:
SPI2_HOST:选择SPI控制器,ESP32-C6上SPI0/1通常被内部使用max_transfer_sz:设置DMA传输大小,建议设为4096的整数倍quadhd_io_num和quadwp_io_num:在标准SPI模式下设为-1
3.2 添加SPI设备
初始化总线后,需要添加具体的SPI设备:
c复制spi_device_interface_config_t dev_cfg = {
.command_bits = 8, // 命令位宽
.address_bits = 24, // 地址位宽
.dummy_bits = 0, // 空周期数
.mode = 0, // SPI模式0
.clock_speed_hz = 10*1000*1000, // 10MHz时钟
.spics_io_num = 22, // CS引脚
.queue_size = 1, // 传输队列大小
};
spi_device_handle_t handle;
ESP_ERROR_CHECK(spi_bus_add_device(SPI2_HOST, &dev_cfg, &handle));
实际项目中,建议将handle保存为全局变量以便后续使用
4. W25Q64驱动实现
4.1 基本操作函数
4.1.1 读取设备ID
W25Q64的设备ID可以通过0x9F命令读取:
c复制void w25q64_read_id(uint8_t *id) {
spi_transaction_ext_t trans = {
.address_bits = 0,
.base = {
.cmd = 0x9F,
.length = 24, // 3字节
.rx_buffer = id,
.flags = SPI_TRANS_VARIABLE_ADDR
}
};
ESP_ERROR_CHECK(spi_device_transmit(handle, &trans.base));
}
典型输出应为0xEF、0x40、0x17,分别代表制造商、内存类型和容量。
4.1.2 写使能与状态检查
W25Q64在执行写操作前需要先使能写入:
c复制void w25q64_write_enable() {
spi_transaction_ext_t trans = {
.address_bits = 0,
.base = {
.cmd = 0x06, // 写使能命令
.length = 0,
.flags = SPI_TRANS_VARIABLE_ADDR
}
};
ESP_ERROR_CHECK(spi_device_transmit(handle, &trans.base));
}
uint8_t w25q64_read_status() {
uint8_t status;
spi_transaction_ext_t trans = {
.address_bits = 0,
.base = {
.cmd = 0x05, // 读状态命令
.length = 8,
.rx_buffer = &status,
.flags = SPI_TRANS_VARIABLE_ADDR
}
};
ESP_ERROR_CHECK(spi_device_transmit(handle, &trans.base));
return status;
}
状态寄存器说明:
- Bit0 (BUSY): 1表示忙,0表示就绪
- Bit1 (WEL): 写使能锁存
4.2 数据擦除操作
W25Q64支持多种擦除粒度:
4.2.1 扇区擦除(4KB)
c复制void w25q64_sector_erase(uint32_t addr) {
w25q64_write_enable();
spi_transaction_t trans = {
.cmd = 0x20, // 扇区擦除命令
.addr = addr,
.length = 0
};
ESP_ERROR_CHECK(spi_device_transmit(handle, &trans));
// 等待擦除完成
while(w25q64_read_status() & 0x01);
}
4.2.2 块擦除(32KB/64KB)
c复制void w25q64_block_erase(uint32_t addr, bool is_64k) {
w25q64_write_enable();
spi_transaction_t trans = {
.cmd = is_64k ? 0xD8 : 0x52, // 64KB/32KB擦除命令
.addr = addr,
.length = 0
};
ESP_ERROR_CHECK(spi_device_transmit(handle, &trans));
while(w25q64_read_status() & 0x01);
}
4.2.3 整片擦除
c复制void w25q64_chip_erase() {
w25q64_write_enable();
spi_transaction_ext_t trans = {
.address_bits = 0,
.base = {
.cmd = 0xC7, // 整片擦除命令
.length = 0,
.flags = SPI_TRANS_VARIABLE_ADDR | SPI_TRANS_VARIABLE_CMD
}
};
ESP_ERROR_CHECK(spi_device_transmit(handle, &trans.base));
while(w25q64_read_status() & 0x01);
}
整片擦除约需5秒,期间不要断电
4.3 数据读写操作
4.3.1 页编程(最大256字节)
c复制void w25q64_page_program(uint32_t addr, uint8_t *data, size_t len) {
assert(len <= 256); // 不超过页大小
w25q64_write_enable();
spi_transaction_t trans = {
.cmd = 0x02, // 页编程命令
.addr = addr,
.length = len * 8,
.tx_buffer = data
};
ESP_ERROR_CHECK(spi_device_transmit(handle, &trans));
while(w25q64_read_status() & 0x01);
}
4.3.2 数据读取
c复制void w25q64_read_data(uint32_t addr, uint8_t *buf, size_t len) {
spi_transaction_t trans = {
.cmd = 0x03, // 读数据命令
.addr = addr,
.length = len * 8,
.rx_buffer = buf
};
ESP_ERROR_CHECK(spi_device_transmit(handle, &trans));
}
5. 高级功能与优化
5.1 提高传输效率
5.1.1 使用DMA传输
在总线初始化时已启用DMA,对于大数据传输:
c复制#define BUF_SIZE 4096
uint8_t *dma_buf = heap_caps_malloc(BUF_SIZE, MALLOC_CAP_DMA);
// 使用dma_buf进行传输
5.1.2 队列传输
通过设置queue_size可以实现传输队列:
c复制spi_device_interface_config_t dev_cfg = {
// ...其他配置
.queue_size = 3 // 允许3个事务排队
};
5.2 电源管理
W25Q64支持低功耗模式:
c复制void w25q64_power_down() {
spi_transaction_ext_t trans = {
.address_bits = 0,
.base = {
.cmd = 0xB9, // 掉电命令
.length = 0,
.flags = SPI_TRANS_VARIABLE_ADDR
}
};
ESP_ERROR_CHECK(spi_device_transmit(handle, &trans.base));
}
void w25q64_release_power_down() {
spi_transaction_ext_t trans = {
.address_bits = 0,
.base = {
.cmd = 0xAB, // 唤醒命令
.length = 0,
.flags = SPI_TRANS_VARIABLE_ADDR
}
};
ESP_ERROR_CHECK(spi_device_transmit(handle, &trans.base));
}
6. 常见问题排查
6.1 初始化失败
- 检查SPI引脚配置是否正确
- 确认SPI控制器(SPI2_HOST)是否可用
- 检查DMA内存分配是否成功
6.2 数据传输错误
- 确认SPI模式(0/1/2/3)与W25Q64一致
- 检查时钟速度是否在器件支持范围内
- 验证CS信号是否正确拉低/拉高
6.3 写操作失败
- 确保已调用写使能命令
- 检查状态寄存器的WEL位
- 确认目标地址已擦除(全为0xFF)
6.4 性能优化建议
- 对于频繁的小数据读写,使用预分配的缓冲区
- 合理设置SPI时钟速度(最高133MHz)
- 考虑使用中断而非轮询检查状态
7. 完整示例代码
以下是一个完整的W25Q64测试例程:
c复制#include <stdio.h>
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "esp_log.h"
spi_device_handle_t w25q64_handle;
void w25q64_init() {
// SPI总线初始化
spi_bus_config_t bus_cfg = {
.miso_io_num = 19,
.mosi_io_num = 20,
.sclk_io_num = 21,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 4096
};
ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &bus_cfg, SPI_DMA_CH_AUTO));
// 添加SPI设备
spi_device_interface_config_t dev_cfg = {
.command_bits = 8,
.address_bits = 24,
.dummy_bits = 0,
.mode = 0,
.clock_speed_hz = 10*1000*1000,
.spics_io_num = 22,
.queue_size = 3
};
ESP_ERROR_CHECK(spi_bus_add_device(SPI2_HOST, &dev_cfg, &w25q64_handle));
}
void app_main() {
w25q64_init();
// 读取ID
uint8_t id[3];
w25q64_read_id(id);
printf("Manufacturer ID: 0x%02X\n", id[0]);
printf("Memory Type: 0x%02X\n", id[1]);
printf("Capacity: 0x%02X\n", id[2]);
// 测试读写
uint8_t test_data[256] = {0};
uint8_t read_back[256] = {0};
// 填充测试数据
for(int i=0; i<256; i++) {
test_data[i] = i;
}
// 擦除第一个扇区
w25q64_sector_erase(0x000000);
// 写入数据
w25q64_page_program(0x000000, test_data, 256);
// 读取验证
w25q64_read_data(0x000000, read_back, 256);
// 比较数据
bool match = true;
for(int i=0; i<256; i++) {
if(test_data[i] != read_back[i]) {
match = false;
break;
}
}
printf("Data verification %s\n", match ? "PASSED" : "FAILED");
}
8. 项目扩展思路
- 实现文件系统:基于W25Q64实现FATFS或LittleFS文件系统
- OTA升级:将W25Q64作为固件存储介质实现无线升级
- 数据记录器:定期将传感器数据存储到Flash中
- 加密存储:结合ESP32的加密引擎实现安全存储
在实际项目中,我发现新版ESP-IDF的SPI接口虽然学习曲线较陡,但一旦掌握后非常灵活高效。特别是对于需要同时管理多个SPI设备的场景,队列机制能显著提高传输效率。