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

发布时间:2026/11/2 11:14:40
SPI通信外设与W25Q64 (硬件SPI)(STM32标准库学习笔记) 在前一章节中我们深入探讨了SPI通信协议的基本原理并通过软件模拟的方式实现了与W25Q64 Flash存储器的交互。这一过程虽然有助于理解SPI时序的底层逻辑但在实际应用中存在占用CPU资源多、通信速率受限等问题。随着系统对数据传输效率和实时性要求的提升单纯依赖GPIO位操作来模拟SPI已难以满足高性能需求。本章将转向STM32内置的硬件SPI外设全面解析如何利用其专用硬件模块实现与W25Q64的高效通信。相比软件SPI硬件SPI不仅显著降低主控处理器的负担还能支持更高的通信速率确保数据稳定可靠地传输。通过配置STM32的SPI外设寄存器、设置工作模式如主/从模式、时钟极性与相位、合理管理片选信号以及结合中断或DMA机制可以构建出更加高效、稳定的Flash读写系统。本节内容将以标准库为基础详细展示硬件SPI外设的初始化流程、关键寄存器配置方法及实际应用中的注意事项帮助读者完成从“手动模拟”到“硬件驱动”的思维跃迁为后续复杂外设集成与系统优化打下坚实基础。如果觉得有收获可以为我的文章点点小赞这将激励我创作更好的内容谢谢一、SPI通信外设与W25Q641.1 SPI外设简介STM32内部集成了硬件SPI收发电路可以由硬件自动执行时钟生成、数据收发等功能减轻CPU的负担可配置8位/16位数据帧、高位先行/低位先行时钟频率 fPCLK / (2, 4, 8, 16, 32, 64, 128, 256)支持多主机模型、主或从操作可精简为半双工/单工通信支持DMA兼容I2S协议STM32F103C8T6 硬件SPI资源SPI1、SPI21.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的结构框图CPOL0空闲状态时SCK为低电平CPHA0SCK第一个边沿移入数据第二个边沿移出数据非连续传输的结构框图重要的标志位在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(i0;iCount;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(i0;iCount;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 的组合二者共同决定采样边沿。模式 0CPOL0、CPHA0下空闲时 SCK 为低电平第一个边沿移入数据、第二个边沿移出数据是最常用的配置。连续传输与非连续传输连续传输依靠 TXE 与 RXNE 标志位配合在发送缓冲器空时写入下一个数据、接收缓冲器非空时读出数据从而实现数据流的无缝衔接非连续传输则在每个字节之间等待标志位实现简单但效率略低。软硬件波形对比软件模拟 SPI 时序灵活但占用 CPU 且速率受限硬件 SPI 由外设自动产生时钟与移位操作波形更规整、速率更高适合对性能和稳定性有要求的场景。初始化流程开启 SPI 与 GPIO 时钟 → 配置 GPIOSCK、MOSI 复用推挽输出MISO 上拉输入SS 通用推挽输出→ 通过结构体配置 SPI 参数并调用SPI_Init→ 调用SPI_Cmd使能外设。W25Q64 读写实践通过MySPI_SwapByte完成字节级交换封装出读 ID、写使能、等待忙、页编程、扇区擦除和读数据等操作最终在 main 函数中完成擦除、写入、读取并用 OLED 显示验证结果。整体来看硬件 SPI 的价值在于把时序细节交给外设处理软件层只需关注标志位判断与协议指令的收发。掌握这套“底层引脚配置 协议层字节交换 器件层指令封装”的分层思路后再迁移到其他 SPI 器件如传感器、显示屏、SD 卡等时只需替换器件层的指令与流程底层代码基本可以复用。如果后续需要进一步提升性能可以考虑引入 DMA 减少 CPU 参与或使用中断方式替代轮询等待在数据量较大、实时性要求较高的场景中这些优化会带来明显收益。都看到这了点点赞吧求求了

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询