1. 项目概述:SPI闪存芯片通信实战
在嵌入式系统开发中,数据存储是个永恒的话题。上周调试一个传感器项目时,我遇到了一个典型场景:传感器采集的环境数据需要先缓存再批量上传,但设备意外断电会导致最后30分钟的数据全部丢失。这时候,外挂一片SPI Flash芯片就成了最经济的解决方案。
SPI(Serial Peripheral Interface)是嵌入式领域最常用的同步串行通信协议之一,而Winbond W25Q系列Flash芯片则是市面上最常见的存储方案。这次我们就以W25Q128JVSIQ为例,手把手实现从零搭建SPI通信链路,完成芯片识别、数据读写和擦除操作。不同于理论讲解,本文会着重分享实际调试中遇到的坑和解决方案。
2. 硬件准备与电路设计
2.1 芯片选型要点
W25Q128JVSIQ是Winbond的16MB SPI Flash,支持标准SPI和QPI模式。选型时要注意几个关键参数:
- 工作电压:3V和1.8V版本引脚兼容但供电不同
- 封装尺寸:SOIC-8是最易手工焊接的封装
- 时钟频率:104MHz理论速度,但实际受PCB布线影响
重要提示:不同厂商的SPI Flash指令集可能不同,即便是同系列的W25Q64和W25Q128也有细微差异,务必核对最新版数据手册。
2.2 最小系统搭建
典型连接方案如下(以STM32F103为例):
| Flash引脚 | MCU引脚 | 作用 | 备注 |
|---|---|---|---|
| CS | PA4 | 片选 | 低电平有效 |
| DO(MISO) | PA6 | 主入从出 | 需上拉电阻 |
| DI(MOSI) | PA7 | 主出从入 | |
| CLK | PA5 | 时钟 | 长度匹配其他信号线 |
| VCC | 3.3V | 电源 | 并联0.1μF去耦电容 |
| GND | GND | 地 | 尽量靠近MCU地 |
实际布线时要注意:
- 时钟线长度不超过其他信号线150%
- 电源线宽至少0.3mm(30mil)
- 避免信号线直角走线
3. 底层驱动实现
3.1 SPI初始化配置
以STM32 HAL库为例,关键配置参数:
c复制hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER;
hspi1.Init.Direction = SPI_DIRECTION_2LINES;
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA=0
hspi1.Init.NSS = SPI_NSS_SOFT;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; // 初始用低速调试
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
时钟相位(CPHA)和极性(CPOL)的配置最容易出错:
- CPOL=0表示时钟空闲时为低电平
- CPHA=0表示数据在时钟第一个边沿采样
- 大部分SPI Flash采用Mode 0(CPOL=0,CPHA=0)或Mode 3(CPOL=1,CPHA=1)
3.2 基本指令实现
芯片识别是第一步,通过读取JEDEC ID验证通信:
c复制uint8_t cmd[4] = {0x9F, 0x00, 0x00, 0x00}; // 读ID指令
uint8_t id[3] = {0};
HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_RESET);
HAL_SPI_TransmitReceive(&hspi1, cmd, id, 4, HAL_MAX_DELAY);
HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_SET);
// 正确应返回:0xEF 0x40 0x18(厂商Winbond,容量16MB)
常见问题排查:
- 无响应:检查CS信号是否有效拉低、电源电压是否达标
- 返回0xFF:通常表示MOSI/MISO接反
- 数据错位:检查时钟极性和相位配置
4. 存储操作进阶
4.1 写使能与状态检查
SPI Flash写入前必须使能写操作,并且要等待芯片就绪:
c复制void Flash_WriteEnable(void) {
uint8_t cmd = 0x06; // WREN指令
HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_RESET);
HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY);
HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_SET);
}
uint8_t Flash_ReadStatusReg(void) {
uint8_t cmd[2] = {0x05, 0x00}; // RDSR指令
HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_RESET);
HAL_SPI_TransmitReceive(&hspi1, cmd, cmd, 2, HAL_MAX_DELAY);
HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_SET);
return cmd[1]; // 返回状态寄存器值
}
状态寄存器BIT说明:
- BIT0(WIP): 1表示忙,0表示就绪
- BIT1(WEL): 写使能锁存
- BIT2(BP0)-BIT5(BP3): 块保护设置
4.2 页编程与扇区擦除
Flash写入有两大特点:
- 只能从1变0,擦除才能恢复为1
- 必须以页(256B)为单位写入
典型写入流程:
c复制void Flash_PageProgram(uint32_t addr, uint8_t *data, uint16_t len) {
Flash_WriteEnable();
uint8_t cmd[4] = {
0x02, // PP指令
(addr >> 16) & 0xFF,
(addr >> 8) & 0xFF,
addr & 0xFF
};
HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_RESET);
HAL_SPI_Transmit(&hspi1, cmd, 4, HAL_MAX_DELAY);
HAL_SPI_Transmit(&hspi1, data, len, HAL_MAX_DELAY);
HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_SET);
while(Flash_ReadStatusReg() & 0x01); // 等待写入完成
}
擦除操作类似,但要注意:
- 扇区擦除(4KB)指令0x20
- 块擦除(64KB)指令0xD8
- 整片擦除指令0xC7(慎用)
5. 性能优化技巧
5.1 双线/四线模式切换
标准SPI是单线输出,W25Q支持双线(DO+DI)和四线(IO0-IO3)模式:
c复制void Flash_EnableQuadMode(void) {
// 先写状态寄存器使能QE位
uint8_t cmd[3] = {0x01, 0x00, 0x02}; // WRSR指令,设置QE位
Flash_WriteEnable();
HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_RESET);
HAL_SPI_Transmit(&hspi1, cmd, 3, HAL_MAX_DELAY);
HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_SET);
// 发送0xEB指令进入QSPI模式
cmd[0] = 0xEB;
HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_RESET);
HAL_SPI_Transmit(&hspi1, cmd, 1, HAL_MAX_DELAY);
// 后续传输需要改为4线模式
}
实测速度对比:
- 标准SPI(1线): 5.2MB/s @ 104MHz
- Dual SPI(2线): 10.1MB/s
- Quad SPI(4线): 20.3MB/s
5.2 DMA传输优化
对于大数据量传输,使用DMA可以释放CPU资源:
c复制void Flash_Read_DMA(uint32_t addr, uint8_t *buf, uint32_t len) {
uint8_t cmd[4] = {
0x0B, // Fast Read指令
(addr >> 16) & 0xFF,
(addr >> 8) & 0xFF,
addr & 0xFF
};
HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_RESET);
HAL_SPI_Transmit(&hspi1, cmd, 4, HAL_MAX_DELAY);
HAL_SPI_Receive_DMA(&hspi1, buf, len);
// 需要在SPI接收完成中断中拉高CS
}
注意事项:
- DMA缓冲区需要4字节对齐
- 接收完成中断中要及时关闭CS
- 超时检测建议用硬件定时器实现
6. 文件系统集成
6.1 SPIFFS移植
对于需要文件式管理的场景,SPIFFS是不错的选择:
- 修改spiffs_config.h中的配置:
c复制#define SPIFFS_FLASH_PAGE_SIZE 256
#define SPIFFS_FLASH_BLOCK_SIZE 4096
#define SPIFFS_FLASH_SIZE (16*1024*1024)
- 实现底层读写接口:
c复制static s32_t spi_flash_read(u32_t addr, u32_t size, u8_t *dst) {
Flash_Read(addr, dst, size);
return SPIFFS_OK;
}
static s32_t spi_flash_write(u32_t addr, u32_t size, u8_t *src) {
Flash_Write(addr, src, size);
return SPIFFS_OK;
}
6.2 磨损均衡策略
Flash扇区有擦写寿命限制(通常10万次),建议:
- 实现写计数日志
- 动态分配高频更新区域
- 定期检查坏块
我在项目中采用的混合策略:
- 配置区:每个参数保存3份副本,轮流写入
- 数据区:采用环形缓冲区结构
- 日志区:每个事件追加写入,满1KB后整理
7. 实测问题与解决方案
7.1 数据异常问题
现象:读取的数据偶尔出现位翻转
排查过程:
- 用逻辑分析仪抓取SPI波形
- 发现时钟线有振铃现象
- 测量信号边沿时间仅2ns(过快)
解决方案:
- 在时钟线串联33Ω电阻
- 降低SPI时钟到80MHz
- PCB改版增加终端匹配
7.2 低温工作异常
现象:-20℃以下写入失败
分析:
- 检查电源电压波动在±5%内
- 时序参数满足低温规格
- 最终发现是CS信号建立时间不足
解决方法:
- 软件增加1us延时
- 修改GPIO速度为Medium
- 改用硬件NSS信号控制
8. 扩展应用场景
8.1 固件在线升级
典型XIP(Execute In Place)方案:
- 将Flash分为两个区域:运行区+更新区
- 通过BOOTLoader验证新固件
- 使用QSPI模式直接运行代码
关键点:
- 中断向量表重映射
- 代码压缩与校验
- 双备份回滚机制
8.2 数据加密存储
硬件加密方案:
- 使用Flash的OTP区域存储密钥
- 写入前用AES-128加密
- 每个数据块添加HMAC校验
软件实现示例:
c复制void Flash_Write_Encrypted(uint32_t addr, uint8_t *data, uint16_t len) {
uint8_t encrypted[256];
AES128_ECB_encrypt(data, key, encrypted);
Flash_PageProgram(addr, encrypted, len);
uint8_t hmac[32];
HMAC_SHA256(data, len, hmac_key, hmac);
Flash_PageProgram(addr+256, hmac, 32);
}
9. 开发调试建议
-
必备工具清单:
- 逻辑分析仪(Saleae/PulseView)
- 可变电源(验证低压工作)
- 热风枪(测试温度特性)
-
调试技巧:
- 先低速(1MHz)验证基础通信
- 用已知模式测试(如全写0xAA)
- 建立寄存器读写日志
-
自动化测试脚本示例(Python+pySerial):
python复制def test_flash_erase():
ser.write(b'\x06') # WREN
ser.write(b'\xC7\x00') # Chip Erase
while ser.read(1)[0] & 0x01: pass
ser.write(b'\x03\x00\x00\x00') # Read first byte
assert ser.read(1)[0] == 0xFF
通过这个完整的SPI Flash实战方案,我们不仅实现了基础存储功能,还针对实际工业场景中的可靠性、安全性和性能问题给出了解决方案。最后分享一个经验:在复杂电磁环境中,SPI信号完整性会显著影响存储可靠性,建议在PCB布局阶段就预留π型滤波电路的位置。