第一次接触ESP32的SPI外设时,我被它灵活的配置选项弄得有点懵。作为嵌入式开发者,我们经常需要扩展存储空间,而SPI接口的FLASH芯片是最常见的选择。ESP32内置了4个SPI控制器,其中SPI2和SPI3(也叫HSPI和VSPI)可供用户自由使用,每个控制器最多能驱动3个从设备。
SPI协议本身并不复杂,但ESP-IDF提供的API确实需要花些时间熟悉。我刚开始使用时,最困惑的是三个核心结构体的分工:
记得第一次调试时,我忘了设置CS信号的保持时间,导致FLASH芯片经常丢失最后一个字节的数据。后来在数据手册中发现,有些FLASH芯片需要CS信号在传输结束后保持几个时钟周期的低电平。
配置SPI总线的第一步是确定引脚映射。ESP32的SPI2和SPI3有固定的GPIO映射,但也可以通过GPIO矩阵灵活配置。我建议优先使用默认的IOMUX引脚,因为它们能提供更好的信号完整性:
c复制const spi_bus_config_t buscfg = {
.mosi_io_num = GPIO_NUM_23, // SPI3默认MOSI
.miso_io_num = GPIO_NUM_19, // SPI3默认MISO
.sclk_io_num = GPIO_NUM_18, // SPI3默认SCLK
.quadwp_io_num = -1, // 不使用QSPI WP
.quadhd_io_num = -1, // 不使用QSPI HD
.max_transfer_sz = 4096 // 最大传输大小
};
初始化总线时,DMA通道的选择很关键。对于FLASH操作,我推荐使用DMA通道1:
c复制ESP_ERROR_CHECK(spi_bus_initialize(SPI3_HOST, &buscfg, 1));
接下来需要配置FLASH设备的通信参数。这里有个坑要注意:不同厂商的FLASH芯片对时序要求差异很大。以Winbond W25Q128为例:
c复制const spi_device_interface_config_t devcfg = {
.command_bits = 8, // 标准FLASH指令长度
.address_bits = 24, // 24位地址
.dummy_bits = 8, // 读数据时的dummy周期
.clock_speed_hz = 20*1000*1000, // 初始用20MHz
.mode = 0, // SPI模式0
.spics_io_num = GPIO_NUM_5, // 自定义CS引脚
.queue_size = 7 // 传输队列深度
};
添加设备时,一定要保存返回的设备句柄,后续所有操作都依赖它:
c复制spi_device_handle_t handle;
ESP_ERROR_CHECK(spi_bus_add_device(SPI3_HOST, &devcfg, &handle));
识别FLASH芯片是第一步。我通常先用低速时钟(如1MHz)进行初始通信,确认设备响应后再提高速度:
c复制esp_flash_t *flash_chip;
const esp_flash_spi_device_config_t cfg = {
.host_id = SPI3_HOST,
.cs_io_num = GPIO_NUM_5,
.io_mode = SPI_FLASH_DIO,
.speed = ESP_FLASH_5MHZ
};
ESP_ERROR_CHECK(spi_bus_add_flash_device(&flash_chip, &cfg));
ESP_ERROR_CHECK(esp_flash_init(flash_chip));
FLASH的读写有几点需要特别注意:
这是我常用的安全写入函数:
c复制esp_err_t safe_flash_write(esp_flash_t *chip, uint32_t addr, const void *src, size_t size) {
uint32_t sector_size = 4096; // 典型扇区大小
uint8_t *buffer = malloc(sector_size);
while(size > 0) {
uint32_t sector_start = addr & ~(sector_size-1);
uint32_t offset = addr - sector_start;
uint32_t chunk_size = MIN(size, sector_size - offset);
// 读取原内容
ESP_ERROR_CHECK(esp_flash_read(chip, buffer, sector_start, sector_size));
// 修改数据
memcpy(buffer + offset, src, chunk_size);
// 擦除后写入
ESP_ERROR_CHECK(esp_flash_erase_region(chip, sector_start, sector_size));
ESP_ERROR_CHECK(esp_flash_write(chip, buffer, sector_start, sector_size));
addr += chunk_size;
src = (uint8_t*)src + chunk_size;
size -= chunk_size;
}
free(buffer);
return ESP_OK;
}
经过实测,我发现以下几个参数对性能影响最大:
这是我的优化配置示例:
c复制const spi_device_interface_config_t hi_perf_cfg = {
.clock_speed_hz = 40*1000*1000, // 提升到40MHz
.queue_size = 16, // 更深队列
.duty_cycle_pos = 128, // 50%占空比
.input_delay_ns = 20 // 补偿输入延迟
};
根据应用场景选择合适的传输模式很重要:
我在一个数据采集项目中对比过两种模式:
遇到初始化失败时,建议按以下步骤排查:
读写数据不一致是常见问题,我的排查清单包括:
记得有一次,因为忽略了FLASH芯片的页编程限制(256字节),导致跨页写入时数据错乱。后来增加了页边界检查才解决问题。
对于需要频繁读取的数据,可以将其映射到内存空间:
c复制const void *map_ptr;
size_t map_size;
ESP_ERROR_CHECK(esp_flash_mmap(flash_chip, 0x10000, 8192,
SPI_FLASH_MMAP_DATA, &map_ptr, &map_size));
使用完毕后记得解除映射:
c复制ESP_ERROR_CHECK(esp_flash_munmap(map_ptr));
内存映射虽然方便,但有两点需要注意:
在最近的智能家居网关项目中,我使用SPI FLASH存储设备配置和日志。遇到最棘手的问题是写操作导致的系统延迟波动。最终通过以下方案解决:
具体实现时,我创建了一个写入任务和环形缓冲区:
c复制typedef struct {
uint32_t addr;
uint8_t data[256];
size_t len;
} flash_op_t;
QueueHandle_t flash_queue;
void flash_writer_task(void *arg) {
flash_op_t op;
while(1) {
if(xQueueReceive(flash_queue, &op, portMAX_DELAY)) {
ESP_ERROR_CHECK(esp_flash_write(flash_chip, op.data, op.addr, op.len));
}
}
}
这种设计将写操作对主程序的影响降到了最低。