STM32F103ZET6 SPI驱动W25Q16 Flash存储实战详解

1. 硬件连接与SPI基础

大家好,我是老李,在嵌入式行业摸爬滚打十多年了。今天咱们来聊聊怎么用STM32F103ZET6的SPI接口驱动W25Q16 Flash芯片。这玩意儿在项目里经常用到,比如存储日志、保存配置参数或者做数据缓存,特别实用。

先说说硬件连接。W25Q16是Winbond公司的一款16Mb(2MB)容量的SPI Flash,支持标准SPI接口。和STM32F103ZET6连接特别简单,就4根线:

W25Q16引脚功能说明STM32F103ZET6引脚工作模式
CS#片选(低有效)PB12通用推挽输出
DO数据输出PB14浮空输入
DI数据输入PB15复用推挽输出
CLK时钟PB13复用推挽输出

这里有个实战经验要分享:WP#(写保护)和HOLD#(暂停)引脚如果不用,最好直接接到VCC(3.3V),避免意外触发保护状态。我当年就踩过这个坑,调试了半天才发现是WP#引脚悬空导致写操作失败。

SPI通信有4种工作模式,取决于时钟极性(CPOL)和时钟相位(CPHA)的组合。W25Q16支持Mode 0和Mode 3,常用的是Mode 0(CPOL=0,CPHA=0)。什么意思呢?就是时钟空闲时为低电平,在上升沿采样数据。这种模式最稳定,兼容性也最好。

2. SPI外设配置详解

配置STM32的SPI外设其实不难,但有些细节需要注意。STM32F103ZET6有3个SPI接口,我们用的是SPI2,挂在APB1总线上,最高时钟频率是36MHz。

先来看初始化代码的关键部分:

void SPI2_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    SPI_InitTypeDef SPI_InitStructure;
    
    // 使能时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);
    
    // 配置SPI引脚
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  // 复用推挽输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);
    
    // SPI参数配置
    SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
    SPI_InitStructure.SPI_Mode = SPI_Mode_Master;      // 主机模式
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;  // 8位数据
    SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;         // 空闲时低电平
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;       // 上升沿采样
    SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;          // 软件控制片选
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;  // 18MHz
    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // 高位先行
    SPI_Init(SPI2, &SPI_InitStructure);
    
    SPI_Cmd(SPI2, ENABLE);
}

这里有几个实战经验分享:第一,GPIO速度一定要设置为50MHz,否则高速传输时可能会出问题。第二,波特率分频器我通常先用SPI_BaudRatePrescaler_2(18MHz)测试,如果通信不稳定再降低速度。第三,软件控制片选(SPI_NSS_Soft)比硬件控制更灵活,特别是在多从设备的情况下。

SPI数据传输函数也很重要:

u8 SPI2_ReadWriteByte(u8 TxData)
{
    while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET); // 等待发送缓冲区空
    SPI_I2S_SendData(SPI2, TxData);                                // 发送数据
    
    while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET); // 等待接收完成
    return SPI_I2S_ReceiveData(SPI2);                              // 返回接收到的数据
}

这个函数实现了全双工通信,发送一个字节的同时接收一个字节。在实际使用中,如果只想要接收数据,可以发送0xFF或者0x00这样的虚拟数据。

3. W25Q16特性与指令集

W25Q16虽然容量不大,但功能很完整。它内部组织为32个块(Block),每个块包含16个扇区(Sector),每个扇区是4KB。也就是说总容量是32×16×4KB=2MB。

擦除操作有三种粒度:扇区擦除(4KB)、块擦除(32KB/64KB)和整片擦除。写操作是按页进行的,每页256字节。这里要特别注意:Flash存储器在写入前必须先擦除,擦除后的值全是0xFF。

W25Q16的指令集很丰富,常用的有:

  • 0x06:写使能(Write Enable)
  • 0x04:写禁止(Write Disable)
  • 0x05:读状态寄存器
  • 0x03:读数据
  • 0x02:页编程
  • 0x20:扇区擦除
  • 0x90读器件ID

每个操作都要遵循严格的时序。比如写操作前必须先发写使能指令,操作完成后要检查状态寄存器的BUSY位。我建议把这些常用指令用宏定义好:

#define W25X_WriteEnable      0x06
#define W25X_WriteDisable     0x04
#define W25X_ReadStatusReg    0x05
#define W25X_WriteStatusReg   0x01
#define W25X_ReadData         0x03
#define W25X_PageProgram      0x02
#define W25X_SectorErase      0x20
#define W25X_ChipErase        0xC7

读状态寄存器的函数特别有用,可以判断Flash是否忙:

u8 W25QXX_ReadSR(void)
{
    u8 byte;
    W25QXX_CS = 0;                         // 片选使能
    SPI2_ReadWriteByte(W25X_ReadStatusReg); // 发送读状态寄存器指令
    byte = SPI2_ReadWriteByte(0xFF);        // 读取状态值
    W25QXX_CS = 1;                         // 取消片选
    return byte;
}

状态寄存器的第0位是BUSY位,为1表示正在执行编程或擦除操作。在实际操作中,我通常这样等待操作完成:

void W25QXX_Wait_Busy(void)
{
    while ((W25QXX_ReadSR() & 0x01) == 0x01); // 等待BUSY位清零
}

4. 底层驱动函数实现

驱动函数是操作Flash的核心,一定要写稳定可靠。先看读函数:

void W25QXX_Read(u8* pBuffer, u32 ReadAddr, u16 NumByteToRead)
{
    u16 i;
    W25QXX_CS = 0;                               // 使能器件
    SPI2_ReadWriteByte(W25X_ReadData);           // 发送读取命令
    SPI2_ReadWriteByte((u8)((ReadAddr) >> 16));  // 发送地址高8位
    SPI2_ReadWriteByte((u8)((ReadAddr) >> 8));   // 发送地址中8位
    SPI2_ReadWriteByte((u8)ReadAddr);            // 发送地址低8位
    
    for (i = 0; i < NumByteToRead; i++) {
        pBuffer[i] = SPI2_ReadWriteByte(0XFF);   // 循环读取数据
    }
    W25QXX_CS = 1;                               // 取消片选
}

读操作相对简单,不需要提前擦除,也不用发写使能。但要注意地址是24位的,因为W25Q16的容量是2MB,需要3个字节的地址。

写操作就复杂多了,因为要按页写入,而且写入前必须确保该区域已经擦除:

void W25QXX_Write_Page(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{
    u16 i;
    W25QXX_Write_Enable();                      // 写使能
    W25QXX_CS = 0;                              // 使能器件
    SPI2_ReadWriteByte(W25X_PageProgram);        // 发送页编程指令
    SPI2_ReadWriteByte((u8)((WriteAddr) >> 16)); // 发送地址高8位
    SPI2_ReadWriteByte((u8)((WriteAddr) >> 8));  // 发送地址中8位
    SPI2_ReadWriteByte((u8)WriteAddr);           // 发送地址低8位
    
    for (i = 0; i < NumByteToWrite; i++) {
        SPI2_ReadWriteByte(pBuffer[i]);          // 循环写入数据
    }
    W25QXX_CS = 1;                              // 取消片选
    W25QXX_Wait_Busy();                          // 等待写入完成
}

这里有个重要限制:如果写入数据跨页了,地址会回绕到当前页开头。所以一次写入不能超过256字节,而且最好按页对齐写入。

擦除函数也很重要,扇区擦除是最常用的:

void W25QXX_Erase_Sector(u32 Dst_Addr)
{
    Dst_Addr *= 4096;  // 扇区地址转换为字节地址
    W25QXX_Write_Enable();                      // 写使能
    W25QXX_CS = 0;                              // 使能器件
    SPI2_ReadWriteByte(W25X_SectorErase);        // 发送扇区擦除指令
    SPI2_ReadWriteByte((u8)((Dst_Addr) >> 16)); // 发送地址高8位
    SPI2_ReadWriteByte((u8)((Dst_Addr) >> 8));  // 发送地址中8位
    SPI2_ReadWriteByte((u8)Dst_Addr);           // 发送地址低8位
    W25QXX_CS = 1;                              // 取消片选
    W25QXX_Wait_Busy();                          // 等待擦除完成
}

扇区擦除需要一定时间,典型值是100-400ms,所以一定要等待操作完成后再进行下一步操作。

5. 高级功能与优化技巧

除了基本读写,W25Q16还有一些高级功能。比如四线SPI模式(Quad SPI),可以让数据传输速率翻倍。但需要先设置状态寄存器的QE位:

void W25QXX_Enable_QuadMode(void)
{
    u8 sr;
    W25QXX_Write_Enable();          // 写使能
    sr = W25QXX_ReadSR();           // 读取状态寄存器
    sr |= 0x40;                     // 设置QE位
    W25QXX_Write_SR(sr);           // 写回状态寄存器
}

启用四线模式后,数据传输可以用4根数据线同时进行,速度提升明显。但硬件连接也需要相应增加3根数据线。

低功耗模式在电池供电的设备中很有用:

void W25QXX_PowerDown(void)
{
    W25QXX_CS = 0;                         // 使能器件
    SPI2_ReadWriteByte(W25X_PowerDown);     // 发送掉电命令
    W25QXX_CS = 1;                         // 取消片选
    delay_us(3);                           // 等待进入掉电模式
}

void W25QXX_WAKEUP(void)
{
    W25QXX_CS = 0;                         // 使能器件
    SPI2_ReadWriteByte(W25X_ReleasePowerDown); // 发送唤醒命令
    W25QXX_CS = 1;                         // 取消片选
    delay_us(3);                           // 等待唤醒完成
}

进入掉电模式后,电流消耗可以从几mA降到几μA,对省电很有帮助。

在实际项目中,我还总结了一些优化技巧:第一,尽量批量读写数据,减少操作次数。第二,合理规划数据存储位置,避免频繁擦写同一个扇区。第三,重要数据最好有备份机制,防止意外丢失。

6. 实战应用与调试技巧

最后来讲讲实际应用和调试中遇到的问题。首先是如何初始化整个系统:

void W25QXX_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);
    GPIO_SetBits(GPIOB, GPIO_Pin_12);  // 初始状态不选中
    
    SPI2_Init();  // 初始化SPI接口
    W25QXX_TYPE = W25QXX_ReadID();  // 读取芯片ID确认通信正常
}

初始化完成后,建议先读取芯片ID确认通信正常:

u16 W25QXX_ReadID(void)
{
    u16 Temp = 0;
    W25QXX_CS = 0;
    SPI2_ReadWriteByte(0x90);  // 发送读ID命令
    SPI2_ReadWriteByte(0x00);
    SPI2_ReadWriteByte(0x00);
    SPI2_ReadWriteByte(0x00);
    Temp |= SPI2_ReadWriteByte(0xFF) << 8;
    Temp |= SPI2_ReadWriteByte(0xFF);
    W25QXX_CS = 1;
    return Temp;
}

W25Q16的ID应该是0xEF14,如果读到的值不对,说明硬件连接或SPI配置有问题。

在实际项目中,我经常用到带擦除的写入函数:

void W25QXX_Write(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{
    u32 secpos;
    u16 secoff;
    u16 secremain;
    u16 i;
    u8 * W25QXX_BUF;
    
    W25QXX_BUF = W25QXX_BUFFER;
    secpos = WriteAddr / 4096;    // 扇区地址
    secoff = WriteAddr % 4096;    // 扇区内偏移
    secremain = 4096 - secoff;    // 扇区剩余空间
    
    // 实际实现中这里会有完整的读写擦除逻辑
    // ...
}

这个函数会自动处理擦除和写入,保证数据正确性,但效率相对较低。如果对速度要求高,可以自己管理擦除操作。

调试SPI通信时,逻辑分析仪是神器。可以清楚地看到时钟、数据线的波形,判断时序是否正确。如果没有逻辑分析仪,也可以用示波器看基本的时钟和数据信号。

常见问题排查:如果通信完全不通,先检查硬件连接和电源;如果能读ID但不能读写数据,检查SPI模式设置;如果写操作失败,检查写保护引脚和写使能指令。

最后提醒一点:Flash的擦写次数有限(通常10万次左右),不要在同一个区域频繁擦写。最好使用均衡磨损算法,或者把经常修改的数据放在RAM中,定期写入Flash。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值