STM32 SPI实战:5分钟搞定W25X16 Flash读写(附完整代码)

STM32 SPI实战:5分钟搞定W25X16 Flash读写(附完整代码)

在嵌入式开发中,SPI(Serial Peripheral Interface)是一种常见的高速全双工通信协议,广泛应用于Flash存储、传感器、显示屏等外设的连接。本文将带你快速实现STM32与W25X16 Flash芯片的SPI通信,从硬件连接到代码实现,手把手教你完成芯片ID读取操作。

1. 硬件连接与SPI基础

W25X16是一款16M-bit的串行Flash存储器,采用SPI接口进行通信。在开始编码前,我们需要确保硬件连接正确:

  • SCK(PA5): 时钟信号线
  • MISO(PA6): 主设备输入,从设备输出
  • MOSI(PA7): 主设备输出,从设备输入
  • CS(PA2): 片选信号(低电平有效)

SPI通信有四种工作模式,由CPOL(时钟极性)和CPHA(时钟相位)决定:

模式CPOLCPHA空闲时钟数据采样边沿
000低电平上升沿
101低电平下降沿
210高电平下降沿
311高电平上升沿

W25X16支持模式0和模式3,本文示例将使用模式3(CPOL=1,CPHA=1)。

2. STM32 SPI初始化配置

以下是使用STM32标准外设库配置SPI1为主设备的代码:

void SPI_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; // 使能时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); // 配置SPI引脚 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; // SCK, MOSI GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; // MISO GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); // 配置CS引脚 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_SetBits(GPIOA, GPIO_Pin_2); // CS高电平 // SPI参数配置 SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; // 模式3 SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_InitStructure.SPI_CRCPolynomial = 7; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }

提示:SPI时钟预分频器(BaudRatePrescaler)应根据实际需求设置,对于W25X16,最高支持80MHz时钟频率。

3. SPI数据收发函数实现

SPI通信是全双工的,发送和接收同时进行。以下是基本的字节收发函数:

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

4. W25X16 Flash操作实战

4.1 读取设备ID

W25X16提供了几个重要的识别指令,其中读取设备ID(0x90)是最常用的:

uint16_t W25X16_ReadID(void) { uint16_t id = 0; // 拉低CS GPIO_ResetBits(GPIOA, GPIO_Pin_2); // 发送读取ID指令 SPI_ReadWriteByte(0x90); // 发送3字节空地址 SPI_ReadWriteByte(0x00); SPI_ReadWriteByte(0x00); SPI_ReadWriteByte(0x00); // 读取2字节ID id = SPI_ReadWriteByte(0xFF) << 8; id |= SPI_ReadWriteByte(0xFF); // 拉高CS GPIO_SetBits(GPIOA, GPIO_Pin_2); return id; }

W25X16的设备ID应为0xEF14,其中0xEF表示厂商(Winbond),0x14表示设备型号。

4.2 常用Flash操作指令

W25X16支持多种操作指令,以下是几个常用的:

指令名称指令代码描述
写使能0x06允许写入操作
写禁止0x04禁止写入操作
读数据0x03读取Flash数据
页编程0x02写入一页数据(256字节)
扇区擦除0x20擦除4KB扇区
芯片擦除0xC7擦除整个芯片

注意:在执行写入或擦除操作前,必须先发送写使能指令(0x06),并且需要检查忙状态位。

4.3 状态寄存器读取

W25X16有一个状态寄存器,可以通过以下代码读取:

uint8_t W25X16_ReadSR(void) { uint8_t sr; GPIO_ResetBits(GPIOA, GPIO_Pin_2); SPI_ReadWriteByte(0x05); // 读状态寄存器指令 sr = SPI_ReadWriteByte(0xFF); GPIO_SetBits(GPIOA, GPIO_Pin_2); return sr; }

状态寄存器中最重要的位是BUSY位(bit0),当芯片执行写入或擦除操作时,该位为1,操作完成后自动清零。

5. 完整示例:读取ID并打印

结合USART输出,我们可以创建一个完整的示例来验证SPI通信:

#include "stm32f10x.h" #include <stdio.h> // 初始化USART1 void USART1_Init(void) { // USART初始化代码... } // 发送一个字符 void USART1_SendChar(char ch) { // 字符发送代码... } // 发送字符串 void USART1_SendString(char *str) { while(*str) { USART1_SendChar(*str++); } } int main(void) { uint16_t flash_id; char buf[32]; // 初始化外设 USART1_Init(); SPI_Init(); // 读取Flash ID flash_id = W25X16_ReadID(); // 打印结果 sprintf(buf, "Flash ID: 0x%04X\r\n", flash_id); USART1_SendString(buf); while(1) { // 主循环 } }

在实际项目中遇到最多的问题是SPI时钟相位和极性的配置错误,导致通信失败。通过逻辑分析仪抓取波形可以快速定位这类问题。