1. W25Q64 Flash存储器基础解析
W25Q64是Winbond公司推出的一款64M-bit(8MB)串行Flash存储器,采用SPI接口通信。这款芯片在嵌入式系统中广泛应用,特别适合需要非易失性存储的场景。让我们先拆解它的核心特性:
1.1 存储结构与寻址方式
W25Q64内部采用分块管理架构,包含:
- 128个块(Block),每块64KB
- 每个块分为16个扇区(Sector),每扇区4KB
- 每扇区包含16页(Page),每页256字节
这种层级结构直接影响擦写操作:
- 最小擦除单位:扇区(4KB)
- 最小编程单位:页(256字节)
- 读取可以按字节进行
重要提示:Flash存储器必须先擦除后写入,且擦除操作会将目标区域所有位设置为1,而写入只能将1改为0。这意味着已写入数据的区域需要先擦除才能重新写入。
1.2 SPI通信接口详解
W25Q64支持标准SPI模式0和模式3(CPOL=0/CPHA=0或CPOL=1/CPHA=1)。实际使用中需要注意:
-
时钟极性配置:
- 模式0:SCK空闲时为低电平,数据在上升沿采样
- 模式3:SCK空闲时为高电平,数据在下降沿采样
-
数据传输特性:
- 全双工通信(同时收发)
- MSB优先传输
- 典型时钟频率可达104MHz(在Fast Read模式下)
-
片选信号(CS#)控制:
- 低电平有效
- 每个命令必须以CS#拉低开始,以CS#拉高结束
- 连续传输时CS#必须保持低电平
2. 硬件设计与接口配置
2.1 STM32与W25Q64连接方案
典型硬件连接如下表所示:
| W25Q64引脚 | STM32引脚 | 功能说明 |
|---|---|---|
| CS# | PA15 | 片选信号 |
| DO(IO1) | PB4(MISO) | 数据输出 |
| WP#(IO2) | NC | 写保护(可悬空) |
| DI(IO0) | PB5(MOSI) | 数据输入 |
| CLK | PB3(SCK) | 时钟信号 |
| HOLD#(IO3) | NC | 保持(可悬空) |
| VCC | 3.3V | 电源 |
| GND | GND | 地线 |
2.2 STM32 SPI外设初始化代码解析
c复制void App_SPI1_Init(void)
{
// 1. GPIO引脚配置
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOA, ENABLE);
// 重映射SPI1引脚(PB3-PB5)
GPIO_PinRemapConfig(GPIO_Remap_SPI1, ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;
// SCK(PB3)配置为复用推挽输出
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_3;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStruct);
// MISO(PB4)配置为上拉输入
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOB, &GPIO_InitStruct);
// MOSI(PB5)配置为复用推挽输出
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOB, &GPIO_InitStruct);
// CS(PA15)配置为普通推挽输出
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_15;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_SetBits(GPIOA, GPIO_Pin_15); // 初始状态不选中
// 2. SPI外设配置
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
SPI_InitTypeDef SPI_InitStruct;
SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStruct.SPI_Mode = SPI_Mode_Master;
SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low; // 模式0
SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge;
SPI_InitStruct.SPI_NSS = SPI_NSS_Soft; // 软件控制NSS
SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_64; // 约1.125MHz @72MHz
SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_Init(SPI1, &SPI_InitStruct);
SPI_Cmd(SPI1, ENABLE);
}
关键配置说明:
- 波特率预分频选择64,在72MHz系统时钟下产生约1.125MHz的SCK频率
- 采用软件NSS模式,手动控制CS引脚
- 数据格式为8位,MSB优先
- 使用SPI模式0(CPOL=0, CPHA=0)
3. W25Q64底层驱动实现
3.1 基本SPI数据传输函数
c复制void App_SPI_MasterTransmitReceive(SPI_TypeDef *SPIx, const uint8_t *pDataTx, uint8_t *pDataRx, uint16_t Size)
{
SPI_Cmd(SPIx, ENABLE);
for(uint16_t i = 0; i < Size; i++)
{
// 等待发送缓冲区空
while(SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_TXE) == RESET);
// 写入数据启动传输
SPI_I2S_SendData(SPIx, pDataTx[i]);
// 等待接收缓冲区非空
while(SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_RXNE) == RESET);
// 读取接收到的数据
pDataRx[i] = SPI_I2S_ReceiveData(SPIx);
}
SPI_Cmd(SPIx, DISABLE);
}
3.2 Flash写操作完整流程
写操作必须遵循特定顺序:
- 写使能(0x06):必须先发送此命令才能进行编程或擦除
- 扇区擦除(0x20):擦除目标扇区(4KB)
- 等待擦除完成:轮询状态寄存器直到BUSY位清零
- 再次写使能:擦除后需要重新使能写操作
- 页编程(0x02):写入数据(不超过256字节)
- 等待编程完成:轮询状态寄存器直到BUSY位清零
对应的代码实现:
c复制void App_W25Q64_SaveByte(uint8_t Byte)
{
uint8_t buffer[10];
// 1. 写使能
buffer[0] = 0x06; // WREN命令
GPIO_ResetBits(GPIOA, GPIO_Pin_15);
App_SPI_MasterTransmitReceive(SPI1, buffer, buffer, 1);
GPIO_SetBits(GPIOA, GPIO_Pin_15);
// 2. 扇区擦除(地址0x000000)
buffer[0] = 0x20; // Sector Erase
buffer[1] = 0x00;
buffer[2] = 0x00;
buffer[3] = 0x00;
GPIO_ResetBits(GPIOA, GPIO_Pin_15);
App_SPI_MasterTransmitReceive(SPI1, buffer, buffer, 4);
GPIO_SetBits(GPIOA, GPIO_Pin_15);
// 3. 等待擦除完成
while(1)
{
GPIO_ResetBits(GPIOA, GPIO_Pin_15);
buffer[0] = 0x05; // Read Status Register 1
App_SPI_MasterTransmitReceive(SPI1, buffer, buffer, 1);
buffer[0] = 0xFF; // Dummy byte
App_SPI_MasterTransmitReceive(SPI1, buffer, buffer, 1);
GPIO_SetBits(GPIOA, GPIO_Pin_15);
if((buffer[0] & 0x01) == 0) break; // 检查BUSY位
}
// 4. 再次写使能
buffer[0] = 0x06;
GPIO_ResetBits(GPIOA, GPIO_Pin_15);
App_SPI_MasterTransmitReceive(SPI1, buffer, buffer, 1);
GPIO_SetBits(GPIOA, GPIO_Pin_15);
// 5. 页编程(地址0x000000)
buffer[0] = 0x02; // Page Program
buffer[1] = 0x00;
buffer[2] = 0x00;
buffer[3] = 0x00;
buffer[4] = Byte; // 要写入的数据
GPIO_ResetBits(GPIOA, GPIO_Pin_15);
App_SPI_MasterTransmitReceive(SPI1, buffer, buffer, 5);
GPIO_SetBits(GPIOA, GPIO_Pin_15);
// 6. 等待编程完成
while(1)
{
GPIO_ResetBits(GPIOA, GPIO_Pin_15);
buffer[0] = 0x05;
App_SPI_MasterTransmitReceive(SPI1, buffer, buffer, 1);
buffer[0] = 0xFF;
App_SPI_MasterTransmitReceive(SPI1, buffer, buffer, 1);
GPIO_SetBits(GPIOA, GPIO_Pin_15);
if((buffer[0] & 0x01) == 0) break;
}
}
3.3 Flash读操作实现
读取操作相对简单,直接发送读命令(0x03)后跟24位地址即可:
c复制uint8_t App_W25Q64_LoadByte(void)
{
uint8_t buffer[10];
GPIO_ResetBits(GPIOA, GPIO_Pin_15);
// 发送读命令和地址
buffer[0] = 0x03; // Read Data
buffer[1] = 0x00;
buffer[2] = 0x00;
buffer[3] = 0x00;
App_SPI_MasterTransmitReceive(SPI1, buffer, buffer, 4);
// 读取数据
buffer[0] = 0xFF; // Dummy byte
App_SPI_MasterTransmitReceive(SPI1, buffer, buffer, 1);
GPIO_SetBits(GPIOA, GPIO_Pin_15);
return buffer[0];
}
4. 应用层实现 - LED状态持久化
4.1 硬件外设初始化
c复制void App_OnBoardLED_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏输出
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOC, &GPIO_InitStruct);
}
void App_Button_Init(void)
{
Button_InitTypeDef Button_InitStruct = {0};
Button_InitStruct.GPIOx = GPIOA;
Button_InitStruct.GPIO_Pin = GPIO_Pin_0;
Button_InitStruct.button_clicked_cb = button_clicked_cb;
My_Button_Init(&button, &Button_InitStruct);
}
4.2 状态保存与恢复逻辑
c复制int main(void)
{
// 外设初始化
App_SPI1_Init();
App_OnBoardLED_Init();
App_Button_Init();
// 从Flash加载保存的状态
uint8_t saved_state = App_W25Q64_LoadByte();
// 恢复LED状态
if(saved_state == 0)
{
GPIO_SetBits(GPIOC, GPIO_Pin_13); // LED灭
}
else
{
GPIO_ResetBits(GPIOC, GPIO_Pin_13); // LED亮
}
while(1)
{
My_Button_Proc(&button); // 处理按钮事件
}
}
// 按钮回调函数
void button_clicked_cb(uint8_t clicks)
{
if(clicks == 1) // 单击
{
if(GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_13) == Bit_SET)
{
GPIO_ResetBits(GPIOC, GPIO_Pin_13); // 点亮LED
App_W25Q64_SaveByte(0x01); // 保存状态1
}
else
{
GPIO_SetBits(GPIOC, GPIO_Pin_13); // 熄灭LED
App_W25Q64_SaveByte(0x00); // 保存状态0
}
}
}
5. 关键问题与调试技巧
5.1 常见问题排查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| SPI无响应 | 1. 硬件连接错误 2. SPI配置错误 3. CS信号未正确控制 |
1. 检查接线 2. 确认SPI模式与Flash支持的模式一致 3. 用逻辑分析仪抓取波形 |
| 写入失败 | 1. 未发送写使能命令 2. 目标区域未擦除 3. 写保护启用 |
1. 确保每次写操作前发送WREN(0x06) 2. 写入前必须擦除目标扇区 3. 检查WP#引脚电平 |
| 读取数据错误 | 1. 时钟相位配置错误 2. 时序不满足要求 3. 电源噪声 |
1. 确认CPHA配置正确 2. 降低SPI时钟频率测试 3. 增加电源滤波电容 |
| 擦除/编程超时 | 1. 未正确轮询状态寄存器 2. Flash芯片损坏 |
1. 检查BUSY位轮询逻辑 2. 更换芯片测试 |
5.2 调试经验分享
-
逻辑分析仪的使用:
- 连接SCK、MOSI、MISO、CS四根信号线
- 设置采样率至少为SPI时钟频率的4倍
- 解码SPI协议时可直观看到命令、地址和数据
-
状态寄存器读取技巧:
c复制uint8_t Read_Status_Register(void) { uint8_t status; GPIO_ResetBits(GPIOA, GPIO_Pin_15); App_SPI_MasterTransmitReceive(SPI1, "\x05", &status, 1); // 0x05命令 App_SPI_MasterTransmitReceive(SPI1, "\xFF", &status, 1); // 读状态 GPIO_SetBits(GPIOA, GPIO_Pin_15); return status; }状态寄存器各位含义:
- BIT0: BUSY(1=忙, 0=就绪)
- BIT1: WEL(写使能锁存)
- BIT2: BP0(块保护)
- BIT3: BP1
- BIT4: BP2
- BIT5: TB(顶部/底部块保护)
- BIT6: SEC(扇区/块保护)
- BIT7: SRP(状态寄存器保护)
-
Flash寿命优化建议:
- 避免频繁擦写同一扇区(典型擦除寿命约10万次)
- 实现磨损均衡算法(如将数据轮流写入不同扇区)
- 对于频繁更新的小数据,可先缓存到RAM,定期批量写入
-
低功耗设计考虑:
- 不操作时拉高CS#使Flash进入待机模式
- 需要极低功耗时可发送Deep Power Down命令(0xB9)
- 唤醒时发送Release DP命令(0xAB)并等待至少20us
6. 功能扩展与进阶应用
6.1 多字节读写实现
当前示例仅演示单字节操作,实际应用中常需要读写多字节数据:
c复制// 写入多字节数据(不超过256字节)
void W25Q64_WritePage(uint32_t addr, uint8_t *data, uint16_t len)
{
uint8_t cmd[4];
// 1. 写使能
W25Q64_WriteEnable();
// 2. 发送页编程命令和地址
cmd[0] = 0x02;
cmd[1] = (addr >> 16) & 0xFF;
cmd[2] = (addr >> 8) & 0xFF;
cmd[3] = addr & 0xFF;
GPIO_ResetBits(GPIOA, GPIO_Pin_15);
App_SPI_MasterTransmitReceive(SPI1, cmd, NULL, 4);
// 3. 发送数据
App_SPI_MasterTransmitReceive(SPI1, data, NULL, len);
GPIO_SetBits(GPIOA, GPIO_Pin_15);
// 4. 等待编程完成
W25Q64_WaitForReady();
}
// 读取多字节数据
void W25Q64_ReadData(uint32_t addr, uint8_t *buf, uint16_t len)
{
uint8_t cmd[4];
cmd[0] = 0x03;
cmd[1] = (addr >> 16) & 0xFF;
cmd[2] = (addr >> 8) & 0xFF;
cmd[3] = addr & 0xFF;
GPIO_ResetBits(GPIOA, GPIO_Pin_15);
App_SPI_MasterTransmitReceive(SPI1, cmd, NULL, 4);
App_SPI_MasterTransmitReceive(SPI1, NULL, buf, len);
GPIO_SetBits(GPIOA, GPIO_Pin_15);
}
6.2 文件系统集成
对于需要管理大量数据的应用,可以考虑集成轻量级文件系统:
-
LittleFS:
- 专为嵌入式设计
- 掉电安全
- 磨损均衡
-
FATFS:
- 兼容PC文件系统
- 适合需要与电脑交换数据的场景
集成示例:
c复制#include "ff.h"
FATFS fs;
FIL file;
void Filesystem_Init(void)
{
// 挂载文件系统
f_mount(&fs, "", 1);
// 打开文件
f_open(&file, "config.txt", FA_READ | FA_WRITE | FA_OPEN_ALWAYS);
// 写入数据
f_printf(&file, "LED state: %d\n", GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_13));
// 关闭文件
f_close(&file);
}
6.3 加密存储实现
对于敏感数据,可在写入前进行加密:
c复制#include "tiny-AES-c/aes.h"
void Encrypted_Write(uint32_t addr, uint8_t *data, uint16_t len, uint8_t *key)
{
uint8_t encrypted[16];
struct AES_ctx ctx;
AES_init_ctx(&ctx, key);
// 分块加密(16字节为一块)
for(int i=0; i<len; i+=16)
{
uint8_t block_len = (len-i)>16 ? 16 : (len-i);
memcpy(encrypted, data+i, block_len);
// 填充不足16字节的部分
if(block_len < 16)
memset(encrypted+block_len, 16-block_len, 16-block_len);
AES_ECB_encrypt(&ctx, encrypted);
W25Q64_WritePage(addr+i, encrypted, 16);
}
}
7. 性能优化技巧
7.1 SPI时钟优化
W25Q64支持最高104MHz时钟(在Fast Read模式下),但实际使用中需考虑:
- 信号完整性:高频时需注意PCB布线(等长、阻抗匹配)
- 电源噪声:高频操作需要更稳定的电源
- 测试方法:从低频开始逐步提高,观察稳定性
修改SPI时钟配置:
c复制// 在SPI初始化中将预分频改为4,得到18MHz时钟
SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;
7.2 DMA传输优化
对于大数据量传输,使用DMA可大幅降低CPU占用:
c复制void SPI1_DMA_Init(void)
{
// 启用DMA时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 配置DMA通道
DMA_InitTypeDef DMA_InitStruct;
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR;
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)buffer;
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralDST; // 内存到外设
DMA_InitStruct.DMA_BufferSize = 256;
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;
DMA_InitStruct.DMA_Priority = DMA_Priority_High;
DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel3, &DMA_InitStruct);
// 启用SPI DMA请求
SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE);
}
7.3 双缓冲技术
对于实时性要求高的应用,可采用双缓冲机制:
c复制uint8_t buffer1[256], buffer2[256];
uint8_t *active_buf = buffer1;
uint8_t *ready_buf = buffer2;
void Data_Process(void)
{
// 处理ready_buf中的数据
ProcessData(ready_buf);
// 交换缓冲区
uint8_t *temp = active_buf;
active_buf = ready_buf;
ready_buf = temp;
// 启动active_buf的DMA传输
DMA_SetCurrDataCounter(DMA1_Channel3, 256);
DMA_Cmd(DMA1_Channel3, ENABLE);
}
8. 项目实战建议
8.1 实际应用中的设计考量
-
错误处理机制:
- 添加超时检测防止死等
- 实现重试机制应对偶发失败
- 记录错误日志便于诊断
-
电源管理:
- 添加掉电检测电路
- 实现紧急数据保存功能
- 考虑超级电容作为后备电源
-
数据校验:
- 添加CRC校验确保数据完整性
- 实现数据回读验证机制
- 考虑ECC纠错码方案
8.2 测试方案设计
-
单元测试:
- 验证单字节读写正确性
- 测试跨页写入边界条件
- 验证擦除后全FF特性
-
压力测试:
- 连续擦写测试Flash寿命
- 高低温环境测试(-40℃~85℃)
- 电源波动测试(3.0V~3.6V)
-
性能测试:
- 测量不同时钟频率下的传输速度
- 评估DMA传输的CPU占用率
- 测试文件系统操作耗时
8.3 生产编程考虑
-
批量烧录方案:
- 使用SWD接口直接编程Flash
- 开发自动化测试夹具
- 实现序列号自动写入
-
固件升级设计:
- 实现Bootloader支持OTA
- 设计双Bank固件存储
- 添加固件校验机制
-
寿命管理:
- 记录扇区擦写次数
- 实现动态磨损均衡
- 提前预警寿命到期