【STM32】SPI接口原理与配置(提供完整实例代码)_stm32 spi
这篇文章是针对 SPI 接口原理与配置 的系统性整理与讲解,内容分为四大部分:① SPI接口原理;② SPI寄存器库函数配置;③ 结合硬件原理图和代码针对W25Qxx配置的讲解。④ 基于 STM32 标准库的 W25Qxx SPI Flash 驱动完整代码 实例。适用于 STM32 系列 MCU 和 W25Qxx FLASH 的开发场景。
📌第一部分:SPI 接口原理
SPI 是英语 Serial Peripheral interface 的缩写,顾名思义就是串行外围设备接口。是Motorola首先在其MC68HCXX系列处理器上定义的。应用于:Flash、EEPROM、RTC、ADC、DAC、DSP、LCD 等设备。
参考资料:ST官方SPI资料:《STM32中文参考手册V10》第23章 串行外设接口SPI
SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,主要应用在 EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。
SPI内部硬件结构简明图
SPI接口一般使用4条线通信:
- MISO 主设备数据输入,从设备数据输出。
- MOSI 主设备数据输出,从设备数据输入。
- SCLK 时钟信号,由主设备产生。
- CS 从设备片选信号,由主设备控制。
SPI 通信通过移位寄存器实现数据交换,主机每发送一个字节的同时,从机也返回一个字节(同步收发)。
SPI接口框图
SPI工作原理总结
① 硬件上为4根线。
② 主机和从机都有一个串行移位寄存器,主机通过向它的SPI串行寄存器写入一个字节来发起一次传输。
③ 串行移位寄存器通过MOSI信号线将字节传送给从机,从机也将自己的串行移位寄存器中的内容通过MISO信号线返回给主机。这样,两个移位寄存器中的内容就被交换。
④ 外设的写操作和读操作是同步完成的。如果只进行写操作,主机只需忽略接收到的字节;反之,若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输。
SPI特征
STM32 SPI接口可配置为支持SPI协议或者支持I2S音频协议,默认是SPI模式。可以通过软件切换到I2S方式。
支持 8位/16位 数据帧主从模式可选可配置 MSB 或 LSB 先传时钟分频器和极性/相位可调支持 DMA、中断、CRC 校验支持多 SPI 模块(SPI1、SPI2、SPI3)
2种NSS模式
🔌 NSS(片选)管理
- 软件 NSS 管理: 由程序控制 NSS 引脚电平;
- 硬件 NSS 管理: 自动拉低/拉高 CS 信号(当 SPI_CR2.SSOE 置位)。
⚙️ 时钟相位与极性(CPOL/CPHA)
CPOL/CPHA 的组合决定了数据在 SCK 的哪个边沿采样与传输。
数据帧格式
📶 SPI 状态标志
🧨 SPI 中断
🧭 SPI 引脚 & GPIO 配置(3个SPI)
SPI1 Mini板子用SPI1
SPI2 战舰/精英用SPI2
SPI3
SPI引脚配置(中文参考手册8.11小节)
模式选择(Mini板 SPI1 引脚配置):
📌第二部分:SPI 库函数与寄存器配置
常用寄存器
寄存器功能SPI控制寄存器1(SPI_CR1)控制寄存器1(主从、CPOL、CPHA、速度等)SPI控制寄存器2(SPI_CR2)控制寄存器2(中断、DMA配置)SPI状态寄存器(SPI_SR)状态寄存器(TXE、RXNE、BSY)SPI数据寄存器(SPI_DR)数据寄存器(收发数据)SPI_I2S配置寄存器(SPI_I2S_CFGR)SPI_I2S预分频寄存器(SPI_I2SPR)
SPI相关库函数:
void SPI_I2S_DeInit(SPI_TypeDef* SPIx);void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);void SPI_I2S_ITConfig(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT, FunctionalState NewState);void SPI_I2S_DMACmd(SPI_TypeDef* SPIx, uint16_t SPI_I2S_DMAReq, FunctionalState NewState);void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);void SPI_DataSizeConfig(SPI_TypeDef* SPIx, uint16_t SPI_DataSize);FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);void SPI_I2S_ClearFlag(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);ITStatus SPI_I2S_GetITStatus(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);void SPI_I2S_ClearITPendingBit(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);
程序配置过程:
① 配置相关引脚的复用功能,使能 SPIx 时钟
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);
② 初始化SPIx,设置SPIx工作模式void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);
③ 使能SPIxvoid SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);
④ SPI传输数据void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx) ;
⑤ 查看SPI传输状态SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE);
⚙️ SPI 初始化流程
// 1. GPIO 配置GPIO_Init(GPIOA, &GPIO_InitStructure);// 2. SPI 参数结构体初始化SPI_InitTypeDef SPI_InitStructure;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_Low;SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8;SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;SPI_Init(SPI1, &SPI_InitStructure);// 3. 启用 SPISPI_Cmd(SPI1, ENABLE);
📤 SPI 数据收发
// 发送SPI_I2S_SendData(SPI1, 0x55);while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);// 接收while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);uint8_t data = SPI_I2S_ReceiveData(SPI1);
📌第三部分:W25Qxx 配置与操作
📌 W25Qxx 概述
容量:W25Q64 为 8MB,W25Q128 为 16MB
擦除单位:4KB 扇区(Sector)= 4096 字节
支持页编程、扇区擦除、块擦除、读 JEDEC ID 等功能
最小写单位:页(Page)= 256 字节
最小擦除单位:扇区(Sector)= 4KB
硬件连接
STM32:
战舰/精英:
Mini:
W25Q128(W25Q64) 将16M(8M) 的容量分为 256(128) 个块 (Block),每个块大小为 64K 字节,每个块又分为 16 个扇区(Sector),每个扇区4K个字节。W25Qxx的最小擦除单位为一个扇区,也就是每次必须擦除4K个字节。这样我们需要给W25Qxx开辟一个至少4K的缓存区,这样对SRAM要求比较高,要求芯片必须有4K以上SRAM才能很好的操作。
(查手册)W25Qxx命令
在我的资源中上传了
W25Q64数据手册.pdf
,需要的朋友可跳转至我的资源
区自行下载。
目录:
常用命令
0x06
0x04
0x05
0x01
0x03
0x02
0x20
0xD8
0xC7
0x9F
写入流程解析
写入一个地址流程:
- 判断目标地址中的数据是否为 0xFF
- 是 → 直接写入
- 否 → 先 擦除扇区,再写入
- 若跨扇区写入:
- 需要分段处理,逐个扇区处理
- 使用缓存区
W25QXX_BUFFER
:
- 读取 → 修改 → 擦除 → 写入缓存 → 一次性写入
W25QXX_Write函数思路
① 根据要写的起始地址,确定要写的起始区域的Sector号以及在起始 sector中的偏移量。
② 根据要写的起始地址和字节数,确定要写的数据是否跨sector。
③ 确定好要操作的sector以及sector的地址范围。
④ 对每一个sector,先遍历要写的地址区域保存的数据是不是0xff
,如果都是,就不用擦除。如果有不是0xff
的区域,先读出里面的数据,保存在缓存W25QXX_BUFFER,然后擦除里面的内容。然后把这个sector
要操作的数据,写到缓存。最后一次性吧缓存W25QXX_BUFFER的数据写到这个对应的sector
。
示例:W25QXX 函数伪代码
void W25QXX_Write(uint8_t* buffer, uint32_t addr, uint16_t size){ // 1. 判断是否跨扇区 // 2. 遍历每个扇区 // 3. 若不为0xFF:擦除扇区、读缓存、写缓存 // 4. 最后写入数据}
第四部分:基于 STM32 标准库的 W25Qxx SPI Flash 驱动完整代码
示例一:
驱动支持:
- ✅ 页写(Page Program)
- ✅ 字节读(Read Data)
- ✅ 扇区擦除(4KB)
- ✅ JEDEC ID 读取
适用于 W25Q64/W25Q128 等 Winbond 系列 Flash,支持 SPI 接口。
📦 文件结构
W25Qxx_Driver/├── w25qxx.h└── w25qxx.c
w25qxx.h
头文件
#ifndef __W25QXX_H__#define __W25QXX_H__#include \"stm32f10x.h\"// SPI & GPIO 定义(可按实际修改)#define W25QXX_SPI SPI1#define W25QXX_CS_PORT GPIOA#define W25QXX_CS_PIN GPIO_Pin_4// 指令集#define W25X_WRITE_ENABLE 0x06 #define W25X_WRITE_DISABLE 0x04 #define W25X_READ_STATUS_REG1 0x05 #define W25X_READ_DATA 0x03 #define W25X_PAGE_PROGRAM 0x02 #define W25X_SECTOR_ERASE 0x20 #define W25X_CHIP_ERASE 0xC7 #define W25X_READ_JEDEC_ID 0x9F #define W25QXX_PAGE_SIZE 256#define W25QXX_SECTOR_SIZE 4096// 函数声明void W25QXX_Init(void);void W25QXX_WriteEnable(void);uint8_t W25QXX_ReadSR(void);uint32_t W25QXX_ReadID(void);void W25QXX_ReadData(uint8_t *buf, uint32_t addr, uint16_t len);void W25QXX_PageProgram(uint8_t *buf, uint32_t addr, uint16_t len);void W25QXX_SectorErase(uint32_t addr);#endif
w25qxx.c
源文件
#include \"w25qxx.h\"// ⏹ 片选控制#define W25QXX_CS_LOW() GPIO_ResetBits(W25QXX_CS_PORT, W25QXX_CS_PIN)#define W25QXX_CS_HIGH() GPIO_SetBits(W25QXX_CS_PORT, W25QXX_CS_PIN)// SPI发送接收static uint8_t SPI1_Transfer(uint8_t data){ while(SPI_I2S_GetFlagStatus(W25QXX_SPI, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(W25QXX_SPI, data); while(SPI_I2S_GetFlagStatus(W25QXX_SPI, SPI_I2S_FLAG_RXNE) == RESET); return SPI_I2S_ReceiveData(W25QXX_SPI);}// 初始化Flash驱动void W25QXX_Init(void){ // SPI 初始化由外部完成 W25QXX_CS_HIGH();}// 写使能void W25QXX_WriteEnable(void){ W25QXX_CS_LOW(); SPI1_Transfer(W25X_WRITE_ENABLE); W25QXX_CS_HIGH();}// 读取状态寄存器1uint8_t W25QXX_ReadSR(void){ uint8_t status = 0; W25QXX_CS_LOW(); SPI1_Transfer(W25X_READ_STATUS_REG1); status = SPI1_Transfer(0xFF); W25QXX_CS_HIGH(); return status;}// 等待Flash空闲static void W25QXX_WaitBusy(void){ while(W25QXX_ReadSR() & 0x01); // BUSY位为1表示忙}// 读取 JEDEC IDuint32_t W25QXX_ReadID(void){ uint32_t id = 0; W25QXX_CS_LOW(); SPI1_Transfer(W25X_READ_JEDEC_ID); id |= SPI1_Transfer(0xFF) << 16; id |= SPI1_Transfer(0xFF) << 8; id |= SPI1_Transfer(0xFF); W25QXX_CS_HIGH(); return id;}// 扇区擦除(4KB)void W25QXX_SectorErase(uint32_t addr){ W25QXX_WriteEnable(); W25QXX_CS_LOW(); SPI1_Transfer(W25X_SECTOR_ERASE); SPI1_Transfer((addr >> 16) & 0xFF); SPI1_Transfer((addr >> 8) & 0xFF); SPI1_Transfer(addr & 0xFF); W25QXX_CS_HIGH(); W25QXX_WaitBusy();}// 页编程(最多256字节,不能跨页)void W25QXX_PageProgram(uint8_t *buf, uint32_t addr, uint16_t len){ if(len > 256) len = 256; // 限制最大长度 W25QXX_WriteEnable(); W25QXX_CS_LOW(); SPI1_Transfer(W25X_PAGE_PROGRAM); SPI1_Transfer((addr >> 16) & 0xFF); SPI1_Transfer((addr >> 8) & 0xFF); SPI1_Transfer(addr & 0xFF); for(uint16_t i = 0; i < len; i++) { SPI1_Transfer(buf[i]); } W25QXX_CS_HIGH(); W25QXX_WaitBusy();}// 读取数据void W25QXX_ReadData(uint8_t *buf, uint32_t addr, uint16_t len){ W25QXX_CS_LOW(); SPI1_Transfer(W25X_READ_DATA); SPI1_Transfer((addr >> 16) & 0xFF); SPI1_Transfer((addr >> 8) & 0xFF); SPI1_Transfer(addr & 0xFF); for(uint16_t i = 0; i < len; i++) { buf[i] = SPI1_Transfer(0xFF); } W25QXX_CS_HIGH();}
示例:使用 W25Qxx 驱动
#include \"w25qxx.h\"uint8_t tx_buf[256] = {0};uint8_t rx_buf[256] = {0};void Flash_Test(void){ // 填充数据 for(int i = 0; i < 256; i++) tx_buf[i] = i; // 擦除扇区 W25QXX_SectorErase(0x000000); // 写入数据 W25QXX_PageProgram(tx_buf, 0x000000, 256); // 读取数据 W25QXX_ReadData(rx_buf, 0x000000, 256);}
注意:页写不可跨页(256字节为单位)写前必须 Write Enable写/擦除后需 WaitBusy()擦除单位为 4KB(0x1000)扩展支持(可按需添加):功能建议命令读取状态寄存器20x35页写跨页处理W25QXX_Write() 封装多页写块擦除(64KB)0xD8高速读(Fast Read)0x0BJEDEC ID校验0xEF4017(W25Q64)/ 0xEF4018(W25Q128)等
示例二:
实验完整代码(标准库实现)(模块化)
实验目的:学习STM32 SPI接口的使用,驱动W25Q128实现SPI FLASH数据读写.硬件资源:1,DS0(连接在PB5)2,串口1(波特率:115200,PA9/PA10连接在板载USB转串口芯片CH340上面)3,ALIENTEK 2.8/3.5/4.3/7寸TFTLCD模块(通过FSMC驱动,FSMC_NE4接LCD片选/A10接RS) 4,按键KEY0(PE4)/KEY1(PE3)5,SPI2(PB12/13/14/15)6,W25Q128(SPI FLASH芯片,连接在SPI2上)实验现象:本实验通过KEY1按键来控制W25Q128的写入,通过另外一个按键KEY0来控制W25Q128的读取。并在LCD模块上面显示相关信息。DS0提示程序正在运行。同时,我们可以通过USMART控制读取W25QXX的ID或者整片擦除。
因为在 Keil 软件中,函数设计是模块划分的(代码整理):
📄 spi.h
#ifndef __SPI_H#define __SPI_H#include \"sys.h\"#define SPIx SPI1 void User_SPI_Init(void); //初始化SPI口void SPI_SetSpeed(u8 SpeedSet); //设置SPI速度 u8 SPI_ReadWriteByte(u8 TxData);//SPI总线读写一个字节 #endif
📄 spi.c
#include \"spi.h\" //以下是SPI模块的初始化代码,配置成主机模式,访问SD Card/W25Q64/NRF24L01(数据手册可在我的资源中下载 W25Q64数据手册.pdf) //SPI口初始化//这里针是对SPIx的初始化void User_SPI_Init(void){ GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE );//PORTB时钟使能 RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE );// GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //PA5 / PA7 复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOAGPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 ;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //PA6 上拉输入GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA GPIO_SetBits(GPIOA, GPIO_Pin_5 | GPIO_Pin_7 ); //PB13/14/15上拉 默认输出高电平SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //设置SPI单向或者双向的数据模式:SPI设置为双线双向全双工SPI_InitStructure.SPI_Mode = SPI_Mode_Master;//设置SPI工作模式:设置为主SPISPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;//设置SPI的数据大小:SPI发送接收8位帧结构SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;//串行同步时钟的空闲状态为高电平SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;//串行同步时钟的第二个跳变沿(上升或下降)数据被采样SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;//NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;//定义波特率预分频的值:波特率预分频值为256SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;//指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始 ***SPI_InitStructure.SPI_CRCPolynomial = 7;//CRC值计算的多项式 *****SPI_Init(SPIx, &SPI_InitStructure); //根据SPI_InitStruct中指定的参数初始化外设SPIx寄存器 SPI_Cmd(SPIx, ENABLE); //使能SPI外设SPI_ReadWriteByte(0xff);//启动传输 } //SPI 速度设置函数//SpeedSet://SPI_BaudRatePrescaler_2 2分频 //SPI_BaudRatePrescaler_8 8分频 //SPI_BaudRatePrescaler_16 16分频 //SPI_BaudRatePrescaler_256 256分频 void SPI_SetSpeed(u8 SPI_BaudRatePrescaler){ assert_param(IS_SPI_BAUDRATE_PRESCALER(SPI_BaudRatePrescaler));SPIx->CR1&=0XFFC7;SPIx->CR1|=SPI_BaudRatePrescaler;//设置SPIx速度 SPI_Cmd(SPIx,ENABLE); } //SPIx 读写一个字节//TxData:要写入的字节//返回值:读取到的字节u8 SPI_ReadWriteByte(u8 TxData){u8 retry=0; while (SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_TXE) == RESET) //检查指定的SPI标志位设置与否:发送缓存空标志位{retry++;if(retry>200)return 0;} SPI_I2S_SendData(SPIx, TxData); //通过外设SPIx发送一个数据retry=0;while (SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_RXNE) == RESET) //检查指定的SPI标志位设置与否:接受缓存非空标志位{retry++;if(retry>200)return 0;} return SPI_I2S_ReceiveData(SPIx); //返回通过SPIx最近接收的数据 }
📄 w25qxx.h
#ifndef __FLASH_H#define __FLASH_H #include \"sys.h\" //W25X系列/Q系列芯片列表 //W25Q80 ID 0XEF13//W25Q16 ID 0XEF14//W25Q32 ID 0XEF15//W25Q64 ID 0XEF16//W25Q128 ID 0XEF17#define W25Q80 0XEF13 #define W25Q16 0XEF14#define W25Q32 0XEF15#define W25Q64 0XEF16#define W25Q1280XEF17#define NM25Q80 0X5213 #define NM25Q16 0X5214#define NM25Q32 0X5215#define NM25Q64 0X5216#define NM25Q1280X5217#define NM25Q256 0X5218extern u16 W25QXX_TYPE;//定义W25QXX芯片型号 #defineW25QXX_CS PAout(4) //W25QXX的片选信号 //////////////////////////////////////////////////////////////////////////// //指令表#define W25X_WriteEnable0x06 #define W25X_WriteDisable0x04 #define W25X_ReadStatusReg0x05 #define W25X_WriteStatusReg0x01 #define W25X_ReadData0x03 #define W25X_FastReadData0x0B #define W25X_FastReadDual0x3B #define W25X_PageProgram0x02 #define W25X_BlockErase0xD8 #define W25X_SectorErase0x20 #define W25X_ChipErase0xC7 #define W25X_PowerDown0xB9 #define W25X_ReleasePowerDown0xAB #define W25X_DeviceID0xAB #define W25X_ManufactDeviceID0x90 #define W25X_JedecDeviceID0x9F void W25QXX_Init(void);u16 W25QXX_ReadID(void); //读取FLASH IDu8 W25QXX_ReadSR(void); //读取状态寄存器 void W25QXX_Write_SR(u8 sr); //写状态寄存器void W25QXX_Write_Enable(void); //写使能 void W25QXX_Write_Disable(void);//写保护void W25QXX_Write_NoCheck(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite);void W25QXX_Read(u8* pBuffer,u32 ReadAddr,u16 NumByteToRead); //读取flashvoid W25QXX_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite);//写入flashvoid W25QXX_Erase_Chip(void); //整片擦除void W25QXX_Erase_Sector(u32 Dst_Addr);//扇区擦除void W25QXX_Wait_Busy(void); //等待空闲void W25QXX_PowerDown(void); //进入掉电模式void W25QXX_WAKEUP(void);//唤醒#endif
📄 w25qxx.c
#include \"w25qxx.h\" #include \"spi.h\"#include \"delay.h\"#include \"usart.h\"u16 W25QXX_TYPE=W25Q128;//默认是W25Q128//4Kbytes为一个Sector//16个扇区为1个Block//W25Q128//容量为16M字节,共有128个Block,4096个Sector //初始化SPI FLASH的IO口void W25QXX_Init(void){ GPIO_InitTypeDef GPIO_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE );//PORTB时钟使能 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; // PA4 推挽 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_SetBits(GPIOA,GPIO_Pin_4); W25QXX_CS=1;//SPI FLASH不选中User_SPI_Init(); //初始化SPISPI_SetSpeed(SPI_BaudRatePrescaler_2);//设置为18M时钟,高速模式W25QXX_TYPE=W25QXX_ReadID();//读取FLASH ID. } //读取W25QXX的状态寄存器//BIT7 6 5 4 3 2 1 0//SPR RV TB BP2 BP1 BP0 WEL BUSY//SPR:默认0,状态寄存器保护位,配合WP使用//TB,BP2,BP1,BP0:FLASH区域写保护设置//WEL:写使能锁定//BUSY:忙标记位(1,忙;0,空闲)//默认:0x00u8 W25QXX_ReadSR(void) { u8 byte=0; W25QXX_CS=0; //使能器件 SPI_ReadWriteByte(W25X_ReadStatusReg); //发送读取状态寄存器命令 byte=SPI_ReadWriteByte(0Xff); //读取一个字节 W25QXX_CS=1; //取消片选 return byte; } //写W25QXX状态寄存器//只有SPR,TB,BP2,BP1,BP0(bit 7,5,4,3,2)可以写!!!void W25QXX_Write_SR(u8 sr) { W25QXX_CS=0; //使能器件 SPI_ReadWriteByte(W25X_WriteStatusReg);//发送写取状态寄存器命令 SPI_ReadWriteByte(sr); //写入一个字节 W25QXX_CS=1; //取消片选 } //W25QXX写使能//将WEL置位 void W25QXX_Write_Enable(void) {W25QXX_CS=0; //使能器件 SPI_ReadWriteByte(W25X_WriteEnable); //发送写使能 W25QXX_CS=1; //取消片选 } //W25QXX写禁止//将WEL清零 void W25QXX_Write_Disable(void) { W25QXX_CS=0; //使能器件 SPI_ReadWriteByte(W25X_WriteDisable); //发送写禁止指令 W25QXX_CS=1; //取消片选 } //读取芯片ID//返回值如下: //0XEF13,表示芯片型号为W25Q80 //0XEF14,表示芯片型号为W25Q16 //0XEF15,表示芯片型号为W25Q32 //0XEF16,表示芯片型号为W25Q64 //0XEF17,表示芯片型号为W25Q128 u16 W25QXX_ReadID(void){u16 Temp = 0; W25QXX_CS=0; SPI_ReadWriteByte(0x90);//发送读取ID命令 SPI_ReadWriteByte(0x00); SPI_ReadWriteByte(0x00); SPI_ReadWriteByte(0x00); Temp|=SPI_ReadWriteByte(0xFF)<<8; Temp|=SPI_ReadWriteByte(0xFF); W25QXX_CS=1; return Temp;} //读取SPI FLASH //在指定地址开始读取指定长度的数据//pBuffer:数据存储区//ReadAddr:开始读取的地址(24bit)//NumByteToRead:要读取的字节数(最大65535)void W25QXX_Read(u8* pBuffer,u32 ReadAddr,u16 NumByteToRead) { u16 i; W25QXX_CS=0; //使能器件 SPI_ReadWriteByte(W25X_ReadData); //发送读取命令 SPI_ReadWriteByte((u8)((ReadAddr)>>16)); //发送24bit地址 SPI_ReadWriteByte((u8)((ReadAddr)>>8)); SPI_ReadWriteByte((u8)ReadAddr); for(i=0;i<NumByteToRead;i++){ pBuffer[i]=SPI_ReadWriteByte(0XFF); //循环读数 }W25QXX_CS=1; } //SPI在一页(0~65535)内写入少于256个字节的数据//在指定地址开始写入最大256字节的数据//pBuffer:数据存储区//WriteAddr:开始写入的地址(24bit)//NumByteToWrite:要写入的字节数(最大256),该数不应该超过该页的剩余字节数!!! void W25QXX_Write_Page(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite){ u16 i; W25QXX_Write_Enable(); //SET WEL W25QXX_CS=0; //使能器件 SPI_ReadWriteByte(W25X_PageProgram); //发送写页命令 SPI_ReadWriteByte((u8)((WriteAddr)>>16)); //发送24bit地址 SPI_ReadWriteByte((u8)((WriteAddr)>>8)); SPI_ReadWriteByte((u8)WriteAddr); for(i=0;i<NumByteToWrite;i++)SPI_ReadWriteByte(pBuffer[i]);//循环写数 W25QXX_CS=1; //取消片选 W25QXX_Wait_Busy(); //等待写入结束} //无检验写SPI FLASH //必须确保所写的地址范围内的数据全部为0XFF,否则在非0XFF处写入的数据将失败!//具有自动换页功能 //在指定地址开始写入指定长度的数据,但是要确保地址不越界!//pBuffer:数据存储区//WriteAddr:开始写入的地址(24bit)//NumByteToWrite:要写入的字节数(最大65535)//CHECK OKvoid W25QXX_Write_NoCheck(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite) { u16 pageremain; pageremain=256-WriteAddr%256; //单页剩余的字节数 if(NumByteToWrite<=pageremain)pageremain=NumByteToWrite;//不大于256个字节while(1){ W25QXX_Write_Page(pBuffer,WriteAddr,pageremain);if(NumByteToWrite==pageremain)break;//写入结束了 else //NumByteToWrite>pageremain{pBuffer+=pageremain;WriteAddr+=pageremain;NumByteToWrite-=pageremain; //减去已经写入了的字节数if(NumByteToWrite>256)pageremain=256; //一次可以写入256个字节else pageremain=NumByteToWrite; //不够256个字节了}}; } //写SPI FLASH //在指定地址开始写入指定长度的数据//该函数带擦除操作!//pBuffer:数据存储区//WriteAddr:开始写入的地址(24bit)//NumByteToWrite:要写入的字节数(最大65535) u8 W25QXX_BUFFER[4096]; 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;//扇区剩余空间大小 //printf(\"ad:%X,nb:%X\\r\\n\",WriteAddr,NumByteToWrite);//测试用 if(NumByteToWrite<=secremain)secremain=NumByteToWrite;//不大于4096个字节while(1) {W25QXX_Read(W25QXX_BUF,secpos*4096,4096);//读出整个扇区的内容for(i=0;i<secremain;i++)//校验数据{if(W25QXX_BUF[secoff+i]!=0XFF)break;//需要擦除 }if(i<secremain)//需要擦除{W25QXX_Erase_Sector(secpos);//擦除这个扇区for(i=0;i<secremain;i++) //复制{W25QXX_BUF[i+secoff]=pBuffer[i]; }W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096);//写入整个扇区 }else W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);//写已经擦除了的,直接写入扇区剩余区间. if(NumByteToWrite==secremain)break;//写入结束了else//写入未结束{secpos++;//扇区地址增1secoff=0;//偏移位置为0 pBuffer+=secremain; //指针偏移WriteAddr+=secremain;//写地址偏移 NumByteToWrite-=secremain;//字节数递减if(NumByteToWrite>4096)secremain=4096;//下一个扇区还是写不完else secremain=NumByteToWrite;//下一个扇区可以写完了} }; }//擦除整个芯片 //等待时间超长...void W25QXX_Erase_Chip(void) { W25QXX_Write_Enable(); //SET WEL W25QXX_Wait_Busy(); W25QXX_CS=0; //使能器件 SPI_ReadWriteByte(W25X_ChipErase); //发送片擦除命令 W25QXX_CS=1; //取消片选 W25QXX_Wait_Busy(); //等待芯片擦除结束} //擦除一个扇区//Dst_Addr:扇区地址 根据实际容量设置//擦除一个山区的最少时间:150msvoid W25QXX_Erase_Sector(u32 Dst_Addr) { //监视falsh擦除情况,测试用 printf(\"fe:%x\\r\\n\",Dst_Addr); Dst_Addr*=4096; W25QXX_Write_Enable(); //SET WEL W25QXX_Wait_Busy(); W25QXX_CS=0; //使能器件 SPI_ReadWriteByte(W25X_SectorErase); //发送扇区擦除指令 SPI_ReadWriteByte((u8)((Dst_Addr)>>16)); //发送24bit地址 SPI_ReadWriteByte((u8)((Dst_Addr)>>8)); SPI_ReadWriteByte((u8)Dst_Addr); W25QXX_CS=1; //取消片选 W25QXX_Wait_Busy(); //等待擦除完成} //等待空闲void W25QXX_Wait_Busy(void) { while((W25QXX_ReadSR()&0x01)==0x01); // 等待BUSY位清空} //进入掉电模式void W25QXX_PowerDown(void) { W25QXX_CS=0; //使能器件 SPI_ReadWriteByte(W25X_PowerDown); //发送掉电命令 W25QXX_CS=1; //取消片选 delay_us(3); //等待TPD } //唤醒void W25QXX_WAKEUP(void) { W25QXX_CS=0; //使能器件 SPI_ReadWriteByte(W25X_ReleasePowerDown);// send W25X_PowerDown command 0xAB W25QXX_CS=1; //取消片选 delay_us(3); //等待TRES1}
📄 key.h
#ifndef __KEY_H#define __KEY_H #include \"sys.h\"//#define KEY0 PEin(4) //PE4//#define KEY1 PEin(3)//PE3 //#define WK_UP PAin(0)//PA0 WK_UP#define KEY0 GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_1)//读取按键0//#define KEY1 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3)//读取按键1#define WK_UP GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)//读取按键3(WK_UP) #define KEY0_PRES 1//KEY0按下#define KEY1_PRES 2//KEY1按下#define WKUP_PRES 3//KEY_UP按下(即WK_UP/KEY_UP)void KEY_Init(void);//IO初始化u8 KEY_Scan(u8); //按键扫描函数#endif
📄 key.c
#include \"stm32f10x.h\"#include \"key.h\"#include \"sys.h\" #include \"delay.h\" //按键初始化函数void KEY_Init(void) //IO初始化{ GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOC,ENABLE);//使能PORTA,PORTE时钟GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;//KEY0-KEY1GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //设置成上拉输入 GPIO_Init(GPIOC, &GPIO_InitStructure);//初始化GPIOE4,3//初始化 WK_UP-->GPIOA.0 下拉输入GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //PA0设置成输入,默认下拉 GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.0}//按键处理函数//返回按键值//mode:0,不支持连续按;1,支持连续按;//0,没有任何按键按下//1,KEY0按下//2,KEY1按下//3,KEY3按下 WK_UP//注意此函数有响应优先级,KEY0>KEY1>KEY_UP!!u8 KEY_Scan(u8 mode){ static u8 key_up=1;//按键按松开标志if(mode)key_up=1; //支持连按 if(key_up&&(KEY0 == 0 || WK_UP == 1)){delay_ms(10);//去抖动 key_up=0;if(KEY0==0)return KEY0_PRES;else if(KEY0 == 0)return KEY0_PRES;else if(WK_UP == 0)return WKUP_PRES;}else if(KEY0==1 && WK_UP==1)key_up=1; return 0;// 无按键按下}
📄 main.c
#include \"led.h\"#include \"delay.h\"#include \"key.h\"#include \"sys.h\"#include \"usart.h\" #include \"w25qxx.h\" //要写入到W25Q64的字符串数组const u8 TEXT_Buffer[]={\"练习两年半的实习生!\"};#define SIZE sizeof(TEXT_Buffer) int main(void) { u8 key;u16 i=0;u8 datatemp[SIZE];u32 FLASH_SIZE; u16 id = 0; delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组2:2位抢占优先级,2位响应优先级uart_init(115200); //串口初始化为115200LED_Init(); //初始化与LED连接的硬件接口KEY_Init();//按键初始化 W25QXX_Init();//W25QXX初始化printf(\"WKUP_PRES:Write KEY0:Read \\r\\n\");while(1){id = W25QXX_ReadID();if (id == NM25Q32 || id == W25Q32)break;printf(\"W25Q32 Check Failed! \\r\\n\");delay_ms(500);printf(\"Please Check! \\r\\n\");delay_ms(500);LED0=!LED0;//DS0闪烁}printf(\"W25Q32 Ready! \\r\\n\");FLASH_SIZE=32*1024*1024;//FLASH 大小为16M字节while(1){key=KEY_Scan(0);if(key==WKUP_PRES)//KEY1按下,写入W25QXX{printf(\"Start Write W25Q32.... \\r\\n\");W25QXX_Write((u8*)TEXT_Buffer,FLASH_SIZE-100,SIZE);//从倒数第100个地址处开始,写入SIZE长度的数据printf(\"W25Q32 Write Finished! \\r\\n\");}if(key==KEY0_PRES)//KEY0按下,读取字符串并显示{printf(\"Start Read W25Q32.... \\r\\n\");W25QXX_Read(datatemp,FLASH_SIZE-100,SIZE);//从倒数第100个地址处开始,读出SIZE个字节printf(\"The Data Readed Is: %s \\r\\n\",datatemp);}i++;delay_ms(10);if(i==20){LED0=!LED0;//提示系统正在运行i=0;} }}
GPIO + SPI_Init + SPI_Cmd
0x06
),擦除单位为4KB以上。这便是 SPI 接口原理与配置 完整的系统性整理与解析,以及在开发中使用 STM32 系列 MCU 和 W25Qxx FLASH 的驱动开发代码。
以上,欢迎有从事同行业的电子信息工程、互联网通信、嵌入式开发的朋友共同探讨与提问,我可以提供实战演示或模板库。希望内容能够对你产生帮助!