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。

2万+

被折叠的 条评论
为什么被折叠?



