ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

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

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_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 参与,或使用中断方式替代轮询等待;在数据量较大、实时性要求较高的场景中,这些优化会带来明显收益。

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

返回列表