欢迎光临
我们一直在努力

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

在前一章节中,我们深入探讨了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_ENABLE0x06
#define W25Q64_WRITE_DISABLE0x04
#define W25Q64_READ_STATUS_REGISTER_1 0x05
#define W25Q64_READ_STATUS_REGISTER_2 0x35
#define W25Q64_WRITE_STATUS_REGISTER 0x01
#define W25Q64_PAGE_PROGRAM0x02
#define W25Q64_QUAD_PAGE_PROGRAM0x32
#define W25Q64_BLOCK_ERASE_64KB0xD8
#define W25Q64_BLOCK_ERASE_32KB0x52
#define W25Q64_SECTOR_ERASE_4KB0x20
#define W25Q64_CHIP_ERASE0xC7
#define W25Q64_ERASE_SUSPEND0x75
#define W25Q64_ERASE_RESUME0x7A
#define W25Q64_POWER_DOWN0xB9
#define W25Q64_HIGH_PERFORMANCE_MODE 0xA3
#define W25Q64_CONTINUOUS_READ_MODE_RESET 0xFF
#define W25Q64_RELEASE_POWER_DOWN_OR_HPM 0xAB
#define W25Q64_MANUFACTURER0x90
#define W25Q64_READ_UNIQUE0x4B
#define W25Q64_JEDEC_ID0x9F
#define W25Q64_READ_DATA0x03
#define W25Q64_FAST_READ0x0B
#define W25Q64_FAST_READ_DUAL_OUTPUT 0x3B
#define W25Q64_FAST_READ_DUAL_IO0xBB
#define W25Q64_FAST_READ_QUAD_OUTPUT 0x6B
#define W25Q64_FAST_READ_QUAD_IO0xEB
#define W25Q64_OCTAL_WORD_READ_QUAD_IO 0xE3
#define W25Q64_DUMMY_BYTE0xFF
#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 参与,或使用中断方式替代轮询等待;在数据量较大、实时性要求较高的场景中,这些优化会带来明显收益。

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

赞(0)
未经允许不得转载:171主机测评 » SPI通信外设与W25Q64 (硬件SPI)(STM32标准库学习笔记)
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址