SPI通信外设与W25Q64 (硬件SPI)(STM32标准库学习笔记)

本文基于STM32标准库,详解硬件SPI外设配置与W25Q64 Flash存储器的高效通信实现。涵盖SPI外设特性(如8/16位帧、CPOL/CPHA模式、DMA支持)、主模式全双工传输机制、TXE/RXNE标志位管理、GPIO复用配置流程,以及W25Q64读ID、页编程、扇区擦除等关键指令封装。强调硬件SPI相较软件模拟在性能、CPU占用和波形稳定性上的显著优势。

在前一章节中,我们深入探讨了SPI通信协议的基本原理,并通过软件模拟的方式实现了与W25Q64 Flash存储器的交互。这一过程虽然有助于理解SPI时序的底层逻辑,但在实际应用中存在占用CPU资源多、通信速率受限等问题。随着系统对数据传输效率和实时性要求的提升,单纯依赖GPIO位操作来模拟SPI已难以满足高性能需求。

本章将转向STM32内置的硬件SPI外设,全面解析如何利用其专用硬件模块实现与W25Q64的高效通信。相比软件SPI,硬件SPI不仅显著降低主控处理器的负担,还能支持更高的通信速率,确保数据稳定可靠地传输。通过配置STM32的SPI外设寄存器、设置工作模式(如主/从模式、时钟极性与相位)、合理管理片选信号以及结合中断或DMA机制,可以构建出更加高效、稳定的Flash读写系统。

本节内容将以标准库为基础,详细展示硬件SPI外设的初始化流程、关键寄存器配置方法及实际应用中的注意事项,帮助读者完成从“手动模拟”到“硬件驱动”的思维跃迁,为后续复杂外设集成与系统优化打下坚实基础。

如果觉得有收获可以为我的文章点点小赞,这将激励我创作更好的内容!谢谢!

 一、SPI通信外设与W25Q64                                              

1.1 SPI外设简介

STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担

可配置8位/16位数据帧、高位先行/低位先行

时钟频率: fPCLK / (2, 4, 8, 16, 32, 64, 128, 256)

支持多主机模型、主或从操作

可精简为半双工/单工通信

支持DMA

兼容I2S协议

STM32F103C8T6 硬件SPI资源:SPI1、SPI2

1.2 SPI框图

1.3 SPI基本结构

1.4 主模式全双工连续传输

1.5 非连续传输

1.6 软件/硬件波形对比

软件:

硬件:

1.7 实例一 11.2.1 硬件SPI读写W25Q64

可能用到的库函数:

//恢复缺省配置、初始化、结构体变量初始化

void SPI_I2S_DeInit(SPI_TypeDef* SPIx);

void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);

void SPI_StructInit(SPI_InitTypeDef* SPI_InitStruct);

//SPI_Cmd外设使能

void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);

//SPI_I2S_ITConfig中断使能、SPI_I2S_DMACmd DMA使能

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);

//SPI_I2S_SendData 发送数据(写DR数据寄存器)、uint16_t SPI_I2S_ReceiveData 读取数据(读DR数据寄存器)

void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);

uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);

//获取标志位和清除标志位

主要用SPI_I2S_GetFlagStatus来获取TXE和RXNE标志位的状态

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);

SPI初始化的流程

第一步,开启时钟,开启SPI和GPIO的时钟;

第二步,初始化GPIO口,其中SCK和MOSI,是由硬件外设控制的输出信号,所以配置为复用推挽输出。MISO,是硬件外设的输入信号,可以配置为上拉输入。SS引脚,SS是软件控制的输出信号,所以配置为通用推挽输出,这就是GPIO口的初始化配置;

第三步,配置SPI外设,这一块,使用一个结构体选参数即可,调用一下SPI_Init,这里面的各种参数,比如,8位/16位数据帧、高位先行/低位先行,SPI模式几、主机还是从机等等;

第四步,开关控制,调用SPl_Cmd,给SPl使能即可。

交换一个字节(模式0)的结构框图

CPOL=0:空闲状态时,SCK为低电平

CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据

非连续传输的结构框图

重要的标志位在stm32数据手册里的描述:

发送缓冲器空闲标志(TXE)

此标志为’1’时表明发送缓冲器为空,可以写下一个待发送的数据进入缓冲器中。当写入SPI_DR 时,TXE标志被清除。

接收缓冲器非空(RXNE)

此标志为’1’时表明在接收缓冲器中包含有效的接收数据。读SPI数据寄存器可以清除此标志。

根据以上描述,在使用硬件SPI的SPI_I2S_SendData和SPI_I2S_ReceiveData函数时,此时硬件会自动帮我们把之前置一TXE/RXNE标志位自动清零,不需要我们在软件程序中手动清除标志位

MySPI.c↓

-----------------------------

#include "stm32f10x.h"                  // Device header
/引脚配置层/
/**
函    数:SPI写SS引脚电平
参    数:BitValue 协议层传入的当前需要写入SS的电平,范围0~1
返 回 值:无
注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SS为低电平,当BitValue为1时,需要置SS为高电平
*/
void MySPI_W_SS(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)BitValue);		//根据BitValue,设置SS引脚的电平
}
/**
函    数:SPI初始化
参    数:无
返 回 值:无
注意事项:此函数需要用户实现内容,实现SS、SCK、MOSI和MISO引脚的初始化
*/
void MySPI_Init(void)
{
/开启时钟/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);		//开启GPIOA的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,	ENABLE);		//开启SPI1的时钟
/GPIO初始化/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);										//将PA4引脚初始化为推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);										//将PA5和PA7引脚初始化为复用推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);										//将PA6引脚初始化为上拉输入
/SPI初始化/
SPI_InitTypeDef SPI_InitStructure;											//定义结构体变量
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;						//模式,选择为SPI主模式
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;	//方向,选择2线全双工
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;				//数据宽度,选择为8位
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;			//先行位,选择高位先行
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;	//波特率分频,选择128分频
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;							//SPI极性,选择低极性
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;						//SPI相位,选择第一个时钟边沿采样,极性和相位决定选择SPI模式0
SPI_InitStructure.SPI_NSS  = SPI_NSS_Soft;							//NSS,选择由软件控制
SPI_InitStructure.SPI_CRCPolynomial = 7;  							//CRC多项式,暂时用不到,给默认值7
SPI_Init(SPI1,&SPI_InitStructure);        							//将结构体变量交给SPI_Init,配置SPI1
/SPI使能/
SPI_Cmd(SPI1,ENABLE);																		//使能SPI1,开始运行
/设置默认电平/              										
MySPI_W_SS(1);                													//SS默认高电平
}
/协议层/
/**
函    数:SPI起始
参    数:无
返 回 值:无
*/
void MySPI_Start(void)
{
MySPI_W_SS(0);							//拉低SS,开始时序
}
/**
函    数:SPI终止
参    数:无
返 回 值:无
*/	
void MySPI_Stop(void)
{
MySPI_W_SS(1);							//拉高SS,终止时序
}
/**
函    数:SPI交换传输一个字节,使用SPI模式0
参    数:ByteSend 要发送的一个字节
返 回 值:接收的一个字节
*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{			
while ( SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) != SET );	//等待发送数据寄存器空
SPI_I2S_SendData(SPI1,ByteSend);																//写入数据到发送数据寄存器,开始产生时序
while ( SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) != SET );	//等待接收数据寄存器非空
return SPI_I2S_ReceiveData(SPI1);																//读取接收到的数据并返回
}

-----------------------------

W25Q64_Ins.h↓

-----------------------------

#ifndef __W25Q64_INS_H
#define __W25Q64_INS_H
#define W25Q64_WRITE_ENABLE							0x06
#define W25Q64_WRITE_DISABLE						0x04
#define W25Q64_READ_STATUS_REGISTER_1			    0x05
#define W25Q64_READ_STATUS_REGISTER_2			    0x35
#define W25Q64_WRITE_STATUS_REGISTER			    0x01
#define W25Q64_PAGE_PROGRAM							0x02
#define W25Q64_QUAD_PAGE_PROGRAM					0x32
#define W25Q64_BLOCK_ERASE_64KB						0xD8
#define W25Q64_BLOCK_ERASE_32KB						0x52
#define W25Q64_SECTOR_ERASE_4KB						0x20
#define W25Q64_CHIP_ERASE							0xC7
#define W25Q64_ERASE_SUSPEND						0x75
#define W25Q64_ERASE_RESUME							0x7A
#define W25Q64_POWER_DOWN							0xB9
#define W25Q64_HIGH_PERFORMANCE_MODE			    0xA3
#define W25Q64_CONTINUOUS_READ_MODE_RESET	        0xFF
#define W25Q64_RELEASE_POWER_DOWN_OR_HPM	        0xAB
#define W25Q64_MANUFACTURER							0x90
#define W25Q64_READ_UNIQUE							0x4B
#define W25Q64_JEDEC_ID								0x9F
#define W25Q64_READ_DATA							0x03
#define W25Q64_FAST_READ							0x0B
#define W25Q64_FAST_READ_DUAL_OUTPUT			    0x3B
#define W25Q64_FAST_READ_DUAL_IO					0xBB
#define W25Q64_FAST_READ_QUAD_OUTPUT			    0x6B
#define W25Q64_FAST_READ_QUAD_IO					0xEB
#define W25Q64_OCTAL_WORD_READ_QUAD_IO		        0xE3
#define W25Q64_DUMMY_BYTE							0xFF
#endif

-----------------------------

W25Q64.c↓

-----------------------------

#include "stm32f10x.h"                  // Device header
#include "MySPI.h"
#include "W25Q64_Ins.h"
/**
函    数:W25Q64初始化
参    数:无
返 回 值:无
*/
void W25Q64_Init(void)
{
MySPI_Init();					//先初始化底层的SPI
}
/**
函    数:W25Q64读取ID号
参    数:MID 工厂ID,使用输出参数的形式返回
参    数:DID 设备ID,使用输出参数的形式返回
返 回 值:无
*/
void W25Q64_ReadID(uint8_t *MID,uint16_t *DID)
{
MySPI_Start();														//SPI起始
MySPI_SwapByte(W25Q64_JEDEC_ID);					//交换发送读取ID的指令
*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);	//交换接收MID,通过输出参数返回
*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);	//交换接收DID高8位
*DID <<= 8;																//高8位移到高位
*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);//或上交换接收DID的低8位,通过输出参数返回
MySPI_Stop();															//SPI终止
}
/**
函    数:W25Q64写使能
参    数:无
返 回 值:无
*/
void W25Q64_WriteEnable(void)
{
MySPI_Start();													//SPI起始
MySPI_SwapByte(W25Q64_WRITE_ENABLE);		//交换发送写使能的指令
MySPI_Stop();														//SPI终止
}
/**
函    数:W25Q64等待忙
参    数:无
返 回 值:无
*/
void W25Q64_WaitBusy(void)
{
uint32_t Timeout;
MySPI_Start();													//SPI起始
MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);				//交换发送读状态寄存器1的指令
Timeout = 100000;												//给定超时计数时间
while ( (MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01 )	//循环等待忙标志位
{
Timeout --;														//等待时,计数值自减
if(Timeout == 0)											//自减到0后,等待超时
{
/超时的错误处理代码,可以添加到此处/
break;															//跳出等待,不等了
}
}
MySPI_Stop();														//SPI终止
}
/**
函    数:W25Q64页编程
参    数:Address 页编程的起始地址,范围:0x000000~0x7FFFFF
参    数:DataArray	用于写入数据的数组
参    数:Count 要写入数据的数量,范围:0~256
返 回 值:无
注意事项:写入的地址范围不能跨页
*/
void W25Q64_PageProgram(uint32_t Address,uint8_t *DataArray,uint16_t Count)
{
W25Q64_WriteEnable();								//写使能
uint16_t i;
MySPI_Start();											//SPI起始
MySPI_SwapByte(W25Q64_PAGE_PROGRAM);//交换发送页编程的指令
MySPI_SwapByte(Address >> 16);      //交换发送地址23~16位
MySPI_SwapByte(Address >> 8);       //交换发送地址15~8位
MySPI_SwapByte(Address);						//交换发送地址7~0位
for(i=0;i<Count;i++)                //循环Count次
{
MySPI_SwapByte(DataArray[i]);			//依次在起始地址后写入数据
}
MySPI_Stop();												//SPI终止
W25Q64_WaitBusy();									//等待忙
}
/**
函    数:W25Q64扇区擦除(4KB)
参    数:Address 指定扇区的地址,范围:0x000000~0x7FFFFF
返 回 值:无
*/
void W25Q64_SectorErase(uint32_t Address)
{
W25Q64_WriteEnable();										//写使能
MySPI_Start();													//SPI起始
MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);//交换发送扇区擦除的指令
MySPI_SwapByte(Address >> 16);          //交换发送地址23~16位
MySPI_SwapByte(Address >> 8);           //交换发送地址15~8位
MySPI_SwapByte(Address);                //交换发送地址7~0位
MySPI_Stop();														//SPI终止
W25Q64_WaitBusy();											//等待忙
}
/**
函    数:W25Q64读取数据
参    数:Address 读取数据的起始地址,范围:0x000000~0x7FFFFF
参    数:DataArray 用于接收读取数据的数组,通过输出参数返回
参    数:Count 要读取数据的数量,范围:0~0x800000
返 回 值:无
*/
void W25Q64_ReadData(uint32_t Address,uint8_t *DataArray,uint32_t Count)
{
uint32_t i;
MySPI_Start();													//SPI起始
MySPI_SwapByte(W25Q64_READ_DATA);				//交换发送读取数据的指令
MySPI_SwapByte(Address >> 16);   				//交换发送地址23~16位
MySPI_SwapByte(Address >> 8);  			  	//交换发送地址15~8位
MySPI_SwapByte(Address);       				  //交换发送地址7~0位
for(i=0;i<Count;i++)           			  	//循环Count次
{
DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);	//依次在起始地址后读取数据
}
MySPI_Stop();														//SPI终止
}

-----------------------------

main.c↓

-----------------------------

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "W25Q64.h"
uint8_t MID;							//定义用于存放MID号的变量
uint16_t DID;							//定义用于存放DID号的变量
uint8_t ArrayWrite[] = {0xFF,0xFE,0xFD,0xFC};	//定义要写入数据的测试数组
uint8_t ArrayRead[4] = {0x00,0x00,0x00,0x00};	//定义要读取数据的测试数组
int main(void)
{
/模块初始化/	
OLED_Init();						//OLED初始化
W25Q64_Init();					//W25Q64初始化
/*显示静态字符串*/	
OLED_ShowString(1,1,"MID:   DID:   ");
OLED_ShowString(2,1,"W:");
OLED_ShowString(3,1,"R:");

/*显示ID号*/
W25Q64_ReadID(&amp;MID,&amp;DID);			//获取W25Q64的ID号
OLED_ShowHexNum(1,5,MID,2);		//显示MID
OLED_ShowHexNum(1,12,DID,4);	//显示DID

/*W25Q64功能函数测试*/
W25Q64_SectorErase(0x000000);	//扇区擦除
W25Q64_PageProgram(0x000000,ArrayWrite,4);	//将写入数据的测试数组写入到W25Q64中

W25Q64_ReadData(0x000000,ArrayRead,4);			//读取刚写入的测试数据到读取数据的测试数组中

/*显示数据*/
OLED_ShowHexNum(2,3,	ArrayWrite[0],2);			//显示写入数据的测试数组
OLED_ShowHexNum(2,6,	ArrayWrite[1],2);			//
OLED_ShowHexNum(2,9,	ArrayWrite[2],2);			//
OLED_ShowHexNum(2,12,	ArrayWrite[3],2);			//

OLED_ShowHexNum(3,3,	ArrayRead[0],2);			//显示读取数据的测试数组
OLED_ShowHexNum(3,6,	ArrayRead[1],2);			//
OLED_ShowHexNum(3,9,	ArrayRead[2],2);			//
OLED_ShowHexNum(3,12,	ArrayRead[3],2);			//

while(1)
{
	
}
}

-----------------------------

二、总结

本文围绕 STM32 的硬件 SPI 外设与 W25Q64 Flash 芯片展开,从外设原理到代码落地完整走了一遍流程,核心内容可以归纳为以下几点:

  • SPI 外设特性:STM32 内部集成硬件 SPI 收发电路,可自动完成时钟生成与数据收发,支持 8 位/16 位数据帧、高位/低位先行、多种时钟分频、多主机模型、半双工/单工精简、DMA 传输以及 I2S 协议兼容;STM32F103C8T6 上可用资源为 SPI1 和 SPI2。
  • 时序与模式:SPI 通信的关键在于 CPOL 与 CPHA 的组合,二者共同决定采样边沿。模式 0(CPOL=0、CPHA=0)下,空闲时 SCK 为低电平,第一个边沿移入数据、第二个边沿移出数据,是最常用的配置。
  • 连续传输与非连续传输:连续传输依靠 TXE 与 RXNE 标志位配合,在发送缓冲器空时写入下一个数据、接收缓冲器非空时读出数据,从而实现数据流的无缝衔接;非连续传输则在每个字节之间等待标志位,实现简单但效率略低。
  • 软硬件波形对比:软件模拟 SPI 时序灵活但占用 CPU 且速率受限,硬件 SPI 由外设自动产生时钟与移位操作,波形更规整、速率更高,适合对性能和稳定性有要求的场景。
  • 初始化流程:开启 SPI 与 GPIO 时钟 → 配置 GPIO(SCK、MOSI 复用推挽输出,MISO 上拉输入,SS 通用推挽输出)→ 通过结构体配置 SPI 参数并调用 SPI_Init → 调用 SPI_Cmd 使能外设。
  • W25Q64 读写实践:通过 MySPI_SwapByte 完成字节级交换,封装出读 ID、写使能、等待忙、页编程、扇区擦除和读数据等操作,最终在 main 函数中完成擦除、写入、读取并用 OLED 显示验证结果。

整体来看,硬件 SPI 的价值在于把时序细节交给外设处理,软件层只需关注标志位判断与协议指令的收发。掌握这套“底层引脚配置 + 协议层字节交换 + 器件层指令封装”的分层思路后,再迁移到其他 SPI 器件(如传感器、显示屏、SD 卡等)时,只需替换器件层的指令与流程,底层代码基本可以复用。

如果后续需要进一步提升性能,可以考虑引入 DMA 减少 CPU 参与,或使用中断方式替代轮询等待;在数据量较大、实时性要求较高的场景中,这些优化会带来明显收益。

都看到这了点点赞吧!求求了!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值