一、SPI 简介1、SPISPISerial Peripheral Interface串行外设接口是 STM32 内置的高速、全双工、同步串行通信总线是嵌入式中传输速度最快、使用最广泛的板级通信协议主要用于高速外设数据交互。相较于 IIC、UARTSPI 传输速率极高、时序简单、稳定性强是屏幕、SPI Flash、SD 卡、高速传感器、以太网模块的核心通信总线。2、SPI 核心硬件特点四线通信标配SCLK、MOSI、MISO、NSS。同步通信完全跟随 SCLK 时钟节拍传输数据。全双工通信收发可同时进行效率远超 IIC。高速传输最高可达几十 Mbps远超 IIC、串口。主从架构一个主机可挂载多个从机依靠片选区分设备。无应答机制纯高速移位传输无校验、无应答。3、四根信号线功能详解SCLK时钟线主机输出时钟控制数据传输速率与时序。MOSI主机输出、从机输入主机发送数据、从机接收。MISO主机输入、从机输出从机发送数据、主机接收。NSS/CS片选线低电平选中设备高电平断开通信。4、SPI 总线架构一对一架构一个主机对应一个从机最简用法。一主多从架构共用SCLK、MOSI、MISO独立CS片选分时通信。5、典型应用场景显示设备SPI OLED、TFTLCD、墨水屏高速刷屏。存储设备W25Qxx 系列 SPI Flash、SD卡数据读写。高速传感器SPI ADC、陀螺仪、气压传感器。无线模块ESP8266、NRF24L01 射频通信。二、SPI 模块详解1、SPI功能框图① 外部物理引脚区橙色边框向外引出 SPI 硬件 4 路引脚是 MCU 和 W25Q64 交互的物理通道MOSI主机输出、从机输入STM32 往 W25Q64 下发指令、地址、待写入数据MISO主机输入、从机输出接收 W25Q64 传回的 ID、存储数据、状态信息SCKSPI 同步时钟输出波特率发生器产生时钟从此引脚输出把控双方采样时序NSS片选引脚可选硬件自动控制 / 软件 GPIO 控制两种模式驱动 W25Q64 项目普遍采用软件模式外部 GPIO 拉低选中 FLASH。② 时钟发生主控区域紫色模块波特率发生器依据SPI_CR1寄存器内BR[2:0]三位分频配置对 APB 总线时钟分频生成 SCK 时钟SPI1 时钟源自 APB2最高 72MHz、SPI2/3 源自 APB1最高 36MHz分频档位支持 2~256 八档可调适配 W25Q64 不同通信速率需求主控制电路读取SPI_CR1所有配置位CPOL、CPHA、MSTR 主从位、SSM/SSI 片选模式、LSBFIRST 字节序、DFF 数据宽度、双向模式等统筹整个 SPI 模块的工作模式、采样边沿、NSS 控制逻辑。③ 数据移位收发缓冲区绿色数据通路这是 SPI 完成全双工收发的核心数据流路径发送缓冲区CPU 经由地址数据总线写入待发送字节底层对接 SPI_DR 寄存器移位寄存器按照 LSBFIRST 设置选择 MSB 先行适配 W25Q64 必须选用该模式/LSB 先行并行数据转换成串行比特从 MOSI 逐位输出与此同时 MISO 侧把从机发来的串行比特并行锁存实现同步全双工交换接收缓冲区缓存移位寄存器接收完成的数据CPU 通过总线读取 SPI_DR 即可拿到 FLASH 传回的数据关键细节SPI 一次移位动作就同步完成一发一收哪怕我们仅想要向 W25Q64 发送指令硬件层面依旧会同步完成一次接收因此代码里读写接口必须设计成「写一个字节同时拿回返回字节」的形式。④ 寄存器控制与状态反馈区黄色寄存器模块1SPI_CR1主控制寄存器框图把 CR1 全部有效位做了可视化排布LSBFIRST、SPE外设总开关、BR2~BR0分频位、MSTR主从选择、CPOL、CPHA、BIDIMODE、BIDIOE、CRCEN、CRCNEXT、DFF帧宽、RXONLY、SSM、SSI所有时序、模式、帧格式配置均在此寄存器完成。2SPI_CR2中断、DMA 配置寄存器位分布TXEIE发送空中断、RXNEIE接收中断、ERRIE错误中断、SSOE硬件NSS输出开关、TXDMAEN发送DMA使能、RXDMAEN接收DMA使能轮询驱动 W25Q64全部中断、DMA 位保持关闭大批量刷屏、大批量写入 FLASH开启对应的 DMA 通道降低 CPU 占用3SPI_SR状态只读寄存器代码轮询判断传输的依据关键标志位适配 FLASH 开发场景BSY总线忙位BSY1 代表移位寄存器正在传输不可发起新的读写寄存器版代码必须等待 BSY 清零TXE发送缓存为空TXE 置 1 才可以往 SPI_DR 写入下一条发给 W25Q64 的数据RXNE接收缓存有效置 1 表示已经收到 FLASH 返回的数据可以读取 DROVR接收溢出、MODF 主从模式冲突、CRCERR 校验失败用于排查 SPI 总线通信故障。2、SPI工作原理SPI 本质是双向移位寄存器同步交换数据。主机拉低片选选中从机主机输出 SCLK 时钟在每个时钟边沿主机与从机同时交换 1bit 数据实现全双工同步收发8/16bit 传输完成后结束一次通信。3、工作模式SPI 模式由 CPOL时钟极性 和 CPHA时钟相位 组合决定必须与从机设备完全一致否则通信乱码、失败。工作模式CPOL 时钟极性CPHA 时钟相位数据采样规则常用设备Mode 00空闲低电平0第一个边沿采样上升沿采样、下降沿移位OLED、TFTLCD、NRF24L01Mode 10空闲低电平1第二个边沿采样下降沿采样、上升沿移位部分传感器Mode 21空闲高电平0第一个边沿采样下降沿采样、上升沿移位W25Q Flash 常用Mode 31空闲高电平1第二个边沿采样上升沿采样、下降沿移位高速外设4、时钟分频与传输速率SPI 时钟来源于 APB 总线时钟通过预分频得到通信时钟支持 2/4/8/16/32/64/128/256 分频。主频72M的F103SPI最高速率可达 36Mbps是嵌入式最快板内通信总线。5、数据传输规则支持 高位先行 MSB默认、最常用、低位先行 LSB。支持 8bit / 16bit 数据帧格式。全双工收发同时进行发数据必然同时收数据。6、SPI 与 IIC / UART 核心区别通信方式SPIIICUART通信类型同步全双工同步半双工异步全双工信号线4根2根2根速度极高Mbps级低100K/400K中百K级拓扑多片选、多设备地址寻址多设备一对一通信应答机制无应答ACK应答无硬件应答三、CubeMX 标准 SPI 配置步骤1、CubeMX配置面板2、配置 Mode下拉选项对应SPI_InitTypeDef成员工作原理Full-Duplex MasterMode SPI_MODE_MASTERDirection SPI_DIRECTION_2LINES双线 MOSI/MISO 独立主动输出时钟可同步下发指令、读取返回数据。Full-Duplex SlaveMode SPI_MODE_SLAVEDirection SPI_DIRECTION_2LINESSTM32 作为从设备接收外部 MCU 的时钟信号。Half-Duplex MasterMode SPI_MODE_MASTERDirection SPI_DIRECTION_1LINE单线双向传输收发分时复用一根引脚。Half-Duplex SlaveMode SPI_MODE_SLAVEDirection SPI_DIRECTION_1LINE单线模式下 STM32 充当从机。Receive Only MasterMode SPI_MODE_MASTERDirection SPI_DIRECTION_2LINES_RXONLY主机仅接收 MISO 的数据无法主动下发 FLASH 指令。Receive Only SlaveMode SPI_MODE_SLAVE开启仅接收模式STM32 作为从端只接收外部下发数据。Transmit Only MasterMode SPI_MODE_MASTER仅启用 MOSI 发送通路主机只能发送指令拿不到 FLASH 传回的 ID、状态寄存器、存储数据。Transmit Only SlaveMode SPI_MODE_SLAVE仅向外发送数据STM32 充当从端发送数据。3、Basic Parameters基础参数组界面配置项取值**SPI_InitTypeDef **结构体赋值说明Frame FormatMotorolahspi1.Init.TIMode SPI_TIMODE_DISABLEMotorola 原生遵循的标准 SPI 协议Data Size8 Bitshspi1.Init.DataSize SPI_DATASIZE_8BIT传输数据宽度First BitMSB Firsthspi1.Init.FirstBit SPI_FIRSTBIT_MSB数据传输规则高位先行 MSB4、Clock Parameters时钟参数组SPI 时序核心配置时钟分频系数:Prescaler 4 根据需要调整SPI1 挂载 APB2 总线主频为 72MHz分频系数 472MHz / 4 18MHz界面显示 Baud Rate18.0MBits/s。工作模式配置:Clock Polarity (CPOL) Low、Clock Phase (CPHA) 1 Edge5、Advanced Parameters高级参数组CRC校验CRC Calculation Disabledhspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE;CS选择软件片选模式MODEHardware NSS Signal DisabledNSS Signal Type Software6、关键参数说明Clock Prescaler时钟分频速度越快传输越快需兼容外设。CPOL/CPHA严格匹配从机设备时序。NSS Software软件控制片选工程最稳定。First BitMSB 高位优先几乎所有设备通用。7、配置注意事项时序模式必须和从机完全一致否则百分百乱码。多设备共用SPI总线时必须分时拉低对应CS片选。高速通信时降低分频过高防止外设时序不支持导致丢数。SPI无硬件应答通信异常需软件重试机制。四、SPI编程模块1、SPI结构体1.底层硬件寄存器结构体 SPI_TypeDef映射 STM32 片上 SPI 外设全部物理寄存器地址是寄存器操作、标准库 / LL/HAL 库底层操作的硬件入口。typedef struct { __IO uint16_t CR1; // 控制寄存器1 uint16_t RESERVED0; __IO uint16_t CR2; // 控制寄存器2 uint16_t RESERVED1; __IO uint16_t SR; // 状态寄存器 uint16_t RESERVED2; __IO uint16_t DR; // 数据寄存器 uint16_t RESERVED3; __IO uint16_t CRCPR; // CRC多项式寄存器 uint16_t RESERVED4; __IO uint16_t RXCRCR; // 接收CRC寄存器 uint16_t RESERVED5; __IO uint16_t TXCRCR; // 发送CRC寄存器 } SPI_TypeDef;2.初始化配置结构体 SPI_InitTypeDef统一封装 SPI 所有时序、帧格式、总线模式参数CubeMX 自动填充作为HAL_SPI_Init()入参完成外设初始化。typedef struct { uint32_t Mode; // 主/从模式 uint32_t Direction; // 传输方向全双工/单线 uint32_t DataSize; // 数据位宽 8bit/16bit uint32_t CLKPolarity; // CPOL 时钟极性 uint32_t CLKPhase; // CPHA 时钟相位 uint32_t NSS; // 软件/硬件片选模式 uint32_t BaudRatePrescaler; // APB时钟分频系数 uint32_t FirstBit; // MSB/LSB先行 uint32_t TIMode; // TI专属同步模式开关 uint32_t CRCCalculation; // CRC校验使能 uint32_t CRCPoly; // CRC多项式值 } SPI_InitTypeDef;3.外设句柄结构体 SPI_HandleTypeDef全局唯一 SPI 外设管理句柄整合寄存器基地址、时序配置、总线状态、DMA 资源、中断回调所有 HAL SPI API 均以此结构体指针作为入参。typedef struct __SPI_HandleTypeDef { SPI_TypeDef *Instance; // 外设基地址 SPI1/SPI2/SPI3 SPI_InitTypeDef Init; // 初始化配置子结构体 HAL_LockTypeDef Lock; // 多任务互斥锁 __IO HAL_SPI_StateTypeDef State; // SPI运行状态机 __IO uint32_t ErrorCode; // 硬件故障错误标志 DMA_HandleTypeDef *hdmatx; // 发送DMA句柄指针 DMA_HandleTypeDef *hdmarx; // 接收DMA句柄指针 // 用户回调函数指针中断/DMA传输完成触发 void (*TxCpltCallback)(struct __SPI_HandleTypeDef *hspi); void (*RxCpltCallback)(struct __SPI_HandleTypeDef *hspi); void (*TxRxCpltCallback)(struct __SPI_HandleTypeDef *hspi); void (*TxHalfCpltCallback)(struct __SPI_HandleTypeDef *hspi); void (*RxHalfCpltCallback)(struct __SPI_HandleTypeDef *hspi); void (*ErrorCallback)(struct __SPI_HandleTypeDef *hspi); void (*AbortCpltCallback)(struct __SPI_HandleTypeDef *hspi); } SPI_HandleTypeDef;4.状态枚举 HAL_SPI_StateTypeDeftypedef enum { HAL_SPI_STATE_RESET 0x00U, // 外设未初始化 HAL_SPI_STATE_READY 0x20U, // 空闲就绪可发起传输 HAL_SPI_STATE_BUSY_TX 0x21U, // 正在发送数据 HAL_SPI_STATE_BUSY_RX 0x22U, // 正在接收数据 HAL_SPI_STATE_BUSY_TX_RX 0x23U, // 全双工同步收发中 HAL_SPI_STATE_ERROR 0xE0U, // 总线传输故障 HAL_SPI_STATE_ABORT 0xF0U // 传输中止状态 } HAL_SPI_StateTypeDef;5.互斥锁结构体 HAL_LockTypeDeftypedef enum { HAL_UNLOCKED 0x00U, // 无占用可访问SPI HAL_LOCKED 0x01U // 正在使用其他任务阻塞等待 } HAL_LockTypeDef;6.SPI 错误码宏对应 ErrorCode#define HAL_SPI_ERROR_NONE 0x00000000U // 无错误 #define HAL_SPI_ERROR_MODF 0x00000001U // 主从模式冲突硬件NSS常见故障 #define HAL_SPI_ERROR_OVR 0x00000002U // 接收数据溢出 #define HAL_SPI_ERROR_CRC 0x00000004U // CRC校验错误 #define HAL_SPI_ERROR_DMA 0x00000008U // DMA传输异常 #define HAL_SPI_ERROR_TIMEOUT 0x00000010U // 传输超时2、HAL_SPI_API1.初始化、注销底层外设函数HAL_StatusTypeDef HAL_SPI_Init(SPI_HandleTypeDef *hspi); 功能读取句柄内SPI_InitTypeDef配置完成 SPI 寄存器时序、模式、分频初始化开启外设使能位 SPECubeMX 生成MX_SPIx_Init核心调用函数。 HAL_StatusTypeDef HAL_SPI_DeInit(SPI_HandleTypeDef *hspi); 功能复位 SPI 外设所有寄存器关闭 SPE 使能清空句柄状态与错误标志。 void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi); 功能底层硬件初始化回调MspMCU Support Package由HAL_SPI_Init内部自动调用CubeMX 自动生成内部完成 APB 总线时钟开启、SCLK/MOSI/MISO 引脚复用推挽配置。 void HAL_SPI_MspDeInit(SPI_HandleTypeDef *hspi); 功能底层硬件资源释放回调HAL_SPI_DeInit内部自动调用关闭 SPI 总线时钟GPIO 恢复普通输入模式。2.动态注册 / 注销回调函数可选功能宏开关 USE_HAL_SPI_REGISTER_CALLBACKS 1UHAL_StatusTypeDef HAL_SPI_RegisterCallback(SPI_HandleTypeDef *hspi, HAL_SPI_CallbackIDTypeDef CallbackID, pSPI_CallbackTypeDef pCallback); 功能运行时动态注册自定义回调函数替代全局弱定义回调支持多实例 SPI 分时复用不同业务逻辑。 HAL_StatusTypeDef HAL_SPI_UnRegisterCallback(SPI_HandleTypeDef *hspi, HAL_SPI_CallbackIDTypeDef CallbackID); 功能注销已注册的指定 ID 回调恢复默认空回调。3.阻塞轮询 PollingHAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, const uint8_t *pData, uint16_t Size, uint32_t Timeout); 功能单向阻塞发送仅输出 MOSI丢弃 MISO 返回数据全程轮询 TXE/BSY 标志超时返回HAL_TIMEOUT。 HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout); 功能单向阻塞接收持续输出空时钟脉冲读取 MISO 数据忽略 MOSI 输出内容。 HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, const uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout); 功能全双工同步收发每发送 1 字节同时接收 1 字节SPI 标准通信核心 API。4.中断非阻塞 IT异步不阻塞主循环HAL_StatusTypeDef HAL_SPI_Transmit_IT(SPI_HandleTypeDef *hspi, const uint8_t *pData, uint16_t Size); 功能开启 SPI 发送中断函数立即返回全部数据发送完成触发HAL_SPI_TxCpltCallback。 HAL_StatusTypeDef HAL_SPI_Receive_IT(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size); 功能开启 SPI 接收中断收到指定长度数据触发HAL_SPI_RxCpltCallback。 HAL_StatusTypeDef HAL_SPI_TransmitReceive_IT(SPI_HandleTypeDef *hspi, const uint8_t *pTxData, uint8_t *pRxData, uint16_t Size); 功能中断模式全双工同步收发传输完成触发HAL_SPI_TxRxCpltCallback。5.DMA 高速传输零 CPU 搬运大批量 FLASH 读写HAL_StatusTypeDef HAL_SPI_Transmit_DMA(SPI_HandleTypeDef *hspi, const uint8_t *pData, uint16_t Size); 功能DMA 硬件自动搬运内存数据至 SPI 发送寄存器CPU 不参与字节搬运。 HAL_StatusTypeDef HAL_SPI_Receive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size); 功能DMA 将 MISO 接收数据直接存入内存数组高速批量读取 FLASH。 HAL_StatusTypeDef HAL_SPI_TransmitReceive_DMA(SPI_HandleTypeDef *hspi, const uint8_t *pTxData, uint8_t *pRxData, uint16_t Size); 功能DMA 全双工同步收发超大容量连续读写最优方案。 HAL_StatusTypeDef HAL_SPI_DMAPause(SPI_HandleTypeDef *hspi); 功能暂停当前 DMA 传输缓存数据保留可恢复继续传输。 HAL_StatusTypeDef HAL_SPI_DMAResume(SPI_HandleTypeDef *hspi); 功能恢复被暂停的 DMA 传输从暂停位置继续搬运数据。 HAL_StatusTypeDef HAL_SPI_DMAStop(SPI_HandleTypeDef *hspi); 功能彻底关闭 DMA 通道清空 DMA 寄存器与句柄传输状态。6.传输中止函数总线卡死、异常恢复HAL_StatusTypeDef HAL_SPI_Abort(SPI_HandleTypeDef *hspi); 功能阻塞式中止当前任意模式传输等待总线空闲后返回同步释放 SPI。 HAL_StatusTypeDef HAL_SPI_Abort_IT(SPI_HandleTypeDef *hspi); 功能异步中止中断 / DMA 传输函数立即返回中止完成触发HAL_SPI_AbortCpltCallback。7.中断服务总入口stm32f1xx_it.c 中断函数调用void HAL_SPI_IRQHandler(SPI_HandleTypeDef *hspi); 功能SPI 全局中断统一处理入口自动区分 TXE/RXNE 传输中断、OVR/MODF 错误中断内部分发对应回调函数。8.系统弱定义回调函数用户可重写中断 / DMA 完成触发void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi); 单向发送全部数据完成回调。 void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi); 单向接收指定长度数据完成回调。 void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi); 全双工同步收发全部字节完成回调。 void HAL_SPI_TxHalfCpltCallback(SPI_HandleTypeDef *hspi); DMA 发送缓冲区一半数据传输完成双缓冲分片处理大数据。 void HAL_SPI_RxHalfCpltCallback(SPI_HandleTypeDef *hspi); DMA 接收缓冲区一半数据接收完成。 void HAL_SPI_TxRxHalfCpltCallback(SPI_HandleTypeDef *hspi); DMA 全双工收发半缓冲区完成回调。 void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi); 通信故障触发MODF 主从冲突、OVR 溢出、DMA 异常、超时可调用HAL_SPI_GetError()定位故障。 void HAL_SPI_AbortCpltCallback(SPI_HandleTypeDef *hspi); 调用HAL_SPI_Abort_IT异步中止传输完成后触发。9.外设状态、错误查询工具函数HAL_SPI_StateTypeDef HAL_SPI_GetState(const SPI_HandleTypeDef *hspi); 功能读取 SPI 实时运行状态复位 / 就绪 / 发送忙 / 接收忙 / 收发忙 / 错误 / 中止发起新传输前判断总线是否空闲避免并发冲突。 uint32_t HAL_SPI_GetError(const SPI_HandleTypeDef *hspi); 功能读取句柄内部错误标志位返回HAL_SPI_ERROR_MODF/OVR/CRC/DMA/TIMEOUT等掩码用于排查 W25Q64 读写失败、无返回数据等故障。五、SPI 应用实例1、硬件原理图FLASH(W25Q64)2、CubeMX配置1.系统时钟配置启用 HSE 外部晶振PLL 倍频得到系统主频 72MHzAPB2 预分频设置为 1SPI1 总线时钟维持 72MHz。2.SPI1 参数配置Parameter Settings分类配置项取值Basic ParametersModeFull-Duplex MasterFrame FormatMotorolaData Size8 BitsFirst BitMSB FirstClock ParametersPrescaler418MHz 上限速率CPOLLowCPHA1 EdgeAdvanced ParametersCRC CalculationDisabledNSS Signal TypeSoftware3.NVIC 配置采用阻塞轮询方案时SPI 事件中断、错误中断全部取消勾选后续使用 DMA / 中断异步传输再开启中断并分配合适优先级。4.DMA 配置按需开启小体量参数存储DMA 保持关闭大批量写入字库、位图资源开启 SPI1 TX DMA大数据回读额外开启 RX DMADMA 位宽统一设置 Byte。5.GPIO 细节配置PA4GPIO_Output推挽输出初始电平 High默认取消片选。PA5/PA6/PA7CubeMX 自动分配 SPI1 复用推挽无需手动修改。6.串口配置USART1 PA9/PA10异步串口波特率 1152008N1用于 printf 打印调试信息。7.工程生成设置选择 HAL 库生成分开的.c/.h 文件。3、工程源码1. w25q64.h 驱动头文件#ifndef __W25Q64_H #define __W25Q64_H #include main.h #include spi.h // 片选控制宏 PA4 #define W25Q_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define W25Q_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) // W25Q64 标准指令集 #define W25Q_WRITE_EN 0x06U #define W25Q_WRITE_DIS 0x04U #define W25Q_READ_STATUS_REG 0x05U #define W25Q_PAGE_PROG 0x02U #define W25Q_READ_DATA 0x03U #define W25Q_SECTOR_ERASE 0x20U #define W25Q_READ_JEDEC_ID 0x9FU // W25Q64 容量参数 #define W25Q_PAGE_SIZE 256U #define W25Q_SECTOR_SIZE 4096U #define W25Q_ADDR_MAX 0x7FFFFFU // 底层单字节全双工收发 HAL_StatusTypeDef W25Q_SPI_RW(uint8_t tx_data, uint8_t *rx_data); // 读取JEDEC设备IDW25Q64标准ID0xEF4017 uint32_t W25Q_Read_JedecID(void); // 轮询等待FLASH擦写内部操作完成 HAL_StatusTypeDef W25Q_Wait_Busy(uint32_t timeout_ms); // 扇区擦除最小擦除单元4KB HAL_StatusTypeDef W25Q_Sector_Erase(uint32_t addr); // 页写入单次写入长度不可超过256字节 HAL_StatusTypeDef W25Q_Page_Write(uint32_t addr, uint8_t *buf, uint16_t len); // 连续任意长度读取存储数据 HAL_StatusTypeDef W25Q_Buffer_Read(uint32_t addr, uint8_t *buf, uint32_t len); #endif2. w25q64.c 驱动实现文件#include w25q64.h extern SPI_HandleTypeDef hspi1; /** * brief SPI单字节同步收发 * param tx_data 发送字节 * param rx_data 接收字节缓存传NULL则丢弃接收数据 * retval HAL状态码 */ HAL_StatusTypeDef W25Q_SPI_RW(uint8_t tx_data, uint8_t *rx_data) { uint8_t rx_tmp 0xFF; HAL_StatusTypeDef ret HAL_SPI_TransmitReceive(hspi1, tx_data, rx_tmp, 1, 100); if(rx_data ! NULL) *rx_data rx_tmp; return ret; } /** * brief 读取W25Q64 JEDEC ID正常返回0xEF4017 * retval 24位ID读取失败返回0xFFFFFF */ uint32_t W25Q_Read_JedecID(void) { uint8_t id_buf[3] {0}; W25Q_CS_LOW(); W25Q_SPI_RW(W25Q_READ_JEDEC_ID, NULL); W25Q_SPI_RW(0x00, id_buf[0]); W25Q_SPI_RW(0x00, id_buf[1]); W25Q_SPI_RW(0x00, id_buf[2]); W25Q_CS_HIGH(); return ((uint32_t)id_buf[0] 16) | ((uint32_t)id_buf[1] 8) | id_buf[2]; } /** * brief 轮询状态寄存器BUSY位等待擦写完成 * param timeout_ms 超时毫秒 * retval HAL_OK / HAL_TIMEOUT */ HAL_StatusTypeDef W25Q_Wait_Busy(uint32_t timeout_ms) { uint8_t sta_reg 0xFF; uint32_t tick_start HAL_GetTick(); while(1) { W25Q_CS_LOW(); W25Q_SPI_RW(W25Q_READ_STATUS_REG, NULL); W25Q_SPI_RW(0x00, sta_reg); W25Q_CS_HIGH(); // BIT0BUSY位0代表芯片空闲 if((sta_reg 0x01U) 0U) return HAL_OK; if(HAL_GetTick() - tick_start timeout_ms) return HAL_TIMEOUT; } } /** * brief 扇区擦除 4KB * param addr 目标扇区任意地址 */ HAL_StatusTypeDef W25Q_Sector_Erase(uint32_t addr) { HAL_StatusTypeDef ret; addr W25Q_ADDR_MAX; // 1. 发送写使能指令 W25Q_CS_LOW(); W25Q_SPI_RW(W25Q_WRITE_EN, NULL); W25Q_CS_HIGH(); HAL_Delay(1); // 2. 发送扇区擦除指令24位地址 W25Q_CS_LOW(); W25Q_SPI_RW(W25Q_SECTOR_ERASE, NULL); W25Q_SPI_RW((addr 16) 0xFFU, NULL); W25Q_SPI_RW((addr 8) 0xFFU, NULL); W25Q_SPI_RW(addr 0xFFU, NULL); W25Q_CS_HIGH(); // 3. 等待擦除完成最大300ms ret W25Q_Wait_Busy(300); return ret; } /** * brief 单页写入单次长度≤256字节 * param addr 写入起始地址 * param buf 待写入数据数组 * param len 写入字节长度 */ HAL_StatusTypeDef W25Q_Page_Write(uint32_t addr, uint8_t *buf, uint16_t len) { HAL_StatusTypeDef ret; addr W25Q_ADDR_MAX; // 限制不跨页防止数据覆盖 uint16_t page_remain W25Q_PAGE_SIZE - (addr % W25Q_PAGE_SIZE); if(len page_remain) len page_remain; // 写使能 W25Q_CS_LOW(); W25Q_SPI_RW(W25Q_WRITE_EN, NULL); W25Q_CS_HIGH(); HAL_Delay(1); // 发送页写指令24位地址 W25Q_CS_LOW(); W25Q_SPI_RW(W25Q_PAGE_PROG, NULL); W25Q_SPI_RW((addr 16) 0xFFU, NULL); W25Q_SPI_RW((addr 8) 0xFFU, NULL); W25Q_SPI_RW(addr 0xFFU, NULL); // 循环写入数据 for(uint16_t i 0; i len; i) { W25Q_SPI_RW(buf[i], NULL); } W25Q_CS_HIGH(); // 等待编程完成 ret W25Q_Wait_Busy(100); return ret; } /** * brief 连续批量读取FLASH存储数据 * param addr 读取起始地址 * param buf 接收缓存数组 * param len 读取字节总数 */ HAL_StatusTypeDef W25Q_Buffer_Read(uint32_t addr, uint8_t *buf, uint32_t len) { HAL_StatusTypeDef ret; addr W25Q_ADDR_MAX; uint8_t cmd_tx[4] {W25Q_READ_DATA, (addr16)0xFFU, (addr8)0xFFU, addr0xFFU}; W25Q_CS_LOW(); // 先发4字节读指令 24位地址 ret HAL_SPI_Transmit(hspi1, cmd_tx, 4, 100); if(ret ! HAL_OK) { W25Q_CS_HIGH(); return ret; } // 持续读取目标长度数据 ret HAL_SPI_Receive(hspi1, buf, len, 200); W25Q_CS_HIGH(); return ret; }3. usart.c printf 打印重定向#include stdio.h extern UART_HandleTypeDef huart1; // printf串口输出重定向 int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, HAL_MAX_DELAY); return ch; }4. main.c 主函数测试#include main.h #include spi.h #include usart.h #include gpio.h #include w25q64.h #include stdio.h #include string.h // 测试写入数据 uint8_t test_write_buf[] STM32F103ZET6 W25Q64 SPI HAL; uint8_t test_read_buf[128] {0}; int main(void) { // HAL库初始化、系统时钟、外设初始化 HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_SPI1_Init(); MX_USART1_UART_Init(); printf( F103ZET6 W25Q64 FLASH TEST \r\n); // 1、读取JEDEC ID检测板载FLASH硬件是否正常 uint32_t chip_id W25Q_Read_JedecID(); printf(W25Q64 JEDEC ID 0x%06X\r\n, chip_id); if(chip_id 0xFFFFFFU) { printf(ERROR: FLASH No Response! Check SPI Wiring CS\r\n); } else if(chip_id 0xEF4017U) { printf(Detect W25Q64 Success!\r\n); } else { printf(Unknown FLASH Chip ID\r\n); } // 2、擦除0号扇区4KB printf(Start Sector Erase 0x000000\r\n); W25Q_Sector_Erase(0x000000U); printf(Erase Complete\r\n); // 3、向0地址写入测试字符串 printf(Write Data:%s\r\n, test_write_buf); W25Q_Page_Write(0x000000U, test_write_buf, sizeof(test_write_buf)); // 4、读取0地址数据存入缓存 memset(test_read_buf, 0, sizeof(test_read_buf)); W25Q_Buffer_Read(0x000000U, test_read_buf, sizeof(test_write_buf)); printf(Read Data:%s\r\n, test_read_buf); // 5、读写数据校验 if(memcmp(test_write_buf, test_read_buf, sizeof(test_write_buf)) 0) { printf(Read Write Verify PASS\r\n); } else { printf(Read Write Verify FAIL\r\n); } // 主循环每秒打印运行状态 while (1) { HAL_Delay(1000); printf(Run......\r\n); } }4、实验结果六、SPI 核心要点与避坑大全1、必考核心知识点SPI 四线同步全双工高速总线传输速度为嵌入式常规总线最快。核心时序由 CPOL、CPHA 组合成4种工作模式必须与外设匹配。无硬件应答、无校验通信可靠性依赖软件逻辑。多设备共用总线依靠独立CS片选分时复用通信。高速大批量传输必须使用 DMA避免 CPU 阻塞卡顿。2、高频坑点时序模式不匹配导致百分百乱码、设备无响应。多设备通信未严格分时操作CS导致数据串扰。时钟频率过高外设不支持导致丢数、花屏。忘记拉低/拉高片选通信完全失效。阻塞式传输刷屏导致程序卡顿、帧率低。未做超时与重试偶尔通信异常无法自愈。3、工程最佳实践统一使用软件CS片选兼容性、灵活性最强。屏幕、Flash大批量传输优先使用 SPIDMA。初始化时序严格对照外设手册优先Mode0/Mode2。通信失败增加软件重试机制提升稳定性。低速调试用阻塞API高速工程用DMA非阻塞。4、总结SPI是高速同步全双工串行总线依靠SCLK同步时钟、MOSI/MISO双向传输、CS片选选通设备通过CPOL/CPHA配置四种时序模式具备传输速度快、稳定性高的特点搭配DMA可实现高速流式数据传输是屏幕、Flash、高速传感器的核心通信外设。七、全篇总结SPI 是嵌入式开发中高速通信的核心外设是屏幕驱动、存储读写、高速外设交互的必备技能。掌握 SPI 四种时序模式、CubeMX参数配置、阻塞/DMA全套API、多设备总线复用逻辑即可完成绝大多数高速外设项目开发是嵌入式进阶、面试的核心重点考点。
HAL_SPI (FLASH)
一、SPI 简介1、SPISPISerial Peripheral Interface串行外设接口是 STM32 内置的高速、全双工、同步串行通信总线是嵌入式中传输速度最快、使用最广泛的板级通信协议主要用于高速外设数据交互。相较于 IIC、UARTSPI 传输速率极高、时序简单、稳定性强是屏幕、SPI Flash、SD 卡、高速传感器、以太网模块的核心通信总线。2、SPI 核心硬件特点四线通信标配SCLK、MOSI、MISO、NSS。同步通信完全跟随 SCLK 时钟节拍传输数据。全双工通信收发可同时进行效率远超 IIC。高速传输最高可达几十 Mbps远超 IIC、串口。主从架构一个主机可挂载多个从机依靠片选区分设备。无应答机制纯高速移位传输无校验、无应答。3、四根信号线功能详解SCLK时钟线主机输出时钟控制数据传输速率与时序。MOSI主机输出、从机输入主机发送数据、从机接收。MISO主机输入、从机输出从机发送数据、主机接收。NSS/CS片选线低电平选中设备高电平断开通信。4、SPI 总线架构一对一架构一个主机对应一个从机最简用法。一主多从架构共用SCLK、MOSI、MISO独立CS片选分时通信。5、典型应用场景显示设备SPI OLED、TFTLCD、墨水屏高速刷屏。存储设备W25Qxx 系列 SPI Flash、SD卡数据读写。高速传感器SPI ADC、陀螺仪、气压传感器。无线模块ESP8266、NRF24L01 射频通信。二、SPI 模块详解1、SPI功能框图① 外部物理引脚区橙色边框向外引出 SPI 硬件 4 路引脚是 MCU 和 W25Q64 交互的物理通道MOSI主机输出、从机输入STM32 往 W25Q64 下发指令、地址、待写入数据MISO主机输入、从机输出接收 W25Q64 传回的 ID、存储数据、状态信息SCKSPI 同步时钟输出波特率发生器产生时钟从此引脚输出把控双方采样时序NSS片选引脚可选硬件自动控制 / 软件 GPIO 控制两种模式驱动 W25Q64 项目普遍采用软件模式外部 GPIO 拉低选中 FLASH。② 时钟发生主控区域紫色模块波特率发生器依据SPI_CR1寄存器内BR[2:0]三位分频配置对 APB 总线时钟分频生成 SCK 时钟SPI1 时钟源自 APB2最高 72MHz、SPI2/3 源自 APB1最高 36MHz分频档位支持 2~256 八档可调适配 W25Q64 不同通信速率需求主控制电路读取SPI_CR1所有配置位CPOL、CPHA、MSTR 主从位、SSM/SSI 片选模式、LSBFIRST 字节序、DFF 数据宽度、双向模式等统筹整个 SPI 模块的工作模式、采样边沿、NSS 控制逻辑。③ 数据移位收发缓冲区绿色数据通路这是 SPI 完成全双工收发的核心数据流路径发送缓冲区CPU 经由地址数据总线写入待发送字节底层对接 SPI_DR 寄存器移位寄存器按照 LSBFIRST 设置选择 MSB 先行适配 W25Q64 必须选用该模式/LSB 先行并行数据转换成串行比特从 MOSI 逐位输出与此同时 MISO 侧把从机发来的串行比特并行锁存实现同步全双工交换接收缓冲区缓存移位寄存器接收完成的数据CPU 通过总线读取 SPI_DR 即可拿到 FLASH 传回的数据关键细节SPI 一次移位动作就同步完成一发一收哪怕我们仅想要向 W25Q64 发送指令硬件层面依旧会同步完成一次接收因此代码里读写接口必须设计成「写一个字节同时拿回返回字节」的形式。④ 寄存器控制与状态反馈区黄色寄存器模块1SPI_CR1主控制寄存器框图把 CR1 全部有效位做了可视化排布LSBFIRST、SPE外设总开关、BR2~BR0分频位、MSTR主从选择、CPOL、CPHA、BIDIMODE、BIDIOE、CRCEN、CRCNEXT、DFF帧宽、RXONLY、SSM、SSI所有时序、模式、帧格式配置均在此寄存器完成。2SPI_CR2中断、DMA 配置寄存器位分布TXEIE发送空中断、RXNEIE接收中断、ERRIE错误中断、SSOE硬件NSS输出开关、TXDMAEN发送DMA使能、RXDMAEN接收DMA使能轮询驱动 W25Q64全部中断、DMA 位保持关闭大批量刷屏、大批量写入 FLASH开启对应的 DMA 通道降低 CPU 占用3SPI_SR状态只读寄存器代码轮询判断传输的依据关键标志位适配 FLASH 开发场景BSY总线忙位BSY1 代表移位寄存器正在传输不可发起新的读写寄存器版代码必须等待 BSY 清零TXE发送缓存为空TXE 置 1 才可以往 SPI_DR 写入下一条发给 W25Q64 的数据RXNE接收缓存有效置 1 表示已经收到 FLASH 返回的数据可以读取 DROVR接收溢出、MODF 主从模式冲突、CRCERR 校验失败用于排查 SPI 总线通信故障。2、SPI工作原理SPI 本质是双向移位寄存器同步交换数据。主机拉低片选选中从机主机输出 SCLK 时钟在每个时钟边沿主机与从机同时交换 1bit 数据实现全双工同步收发8/16bit 传输完成后结束一次通信。3、工作模式SPI 模式由 CPOL时钟极性 和 CPHA时钟相位 组合决定必须与从机设备完全一致否则通信乱码、失败。工作模式CPOL 时钟极性CPHA 时钟相位数据采样规则常用设备Mode 00空闲低电平0第一个边沿采样上升沿采样、下降沿移位OLED、TFTLCD、NRF24L01Mode 10空闲低电平1第二个边沿采样下降沿采样、上升沿移位部分传感器Mode 21空闲高电平0第一个边沿采样下降沿采样、上升沿移位W25Q Flash 常用Mode 31空闲高电平1第二个边沿采样上升沿采样、下降沿移位高速外设4、时钟分频与传输速率SPI 时钟来源于 APB 总线时钟通过预分频得到通信时钟支持 2/4/8/16/32/64/128/256 分频。主频72M的F103SPI最高速率可达 36Mbps是嵌入式最快板内通信总线。5、数据传输规则支持 高位先行 MSB默认、最常用、低位先行 LSB。支持 8bit / 16bit 数据帧格式。全双工收发同时进行发数据必然同时收数据。6、SPI 与 IIC / UART 核心区别通信方式SPIIICUART通信类型同步全双工同步半双工异步全双工信号线4根2根2根速度极高Mbps级低100K/400K中百K级拓扑多片选、多设备地址寻址多设备一对一通信应答机制无应答ACK应答无硬件应答三、CubeMX 标准 SPI 配置步骤1、CubeMX配置面板2、配置 Mode下拉选项对应SPI_InitTypeDef成员工作原理Full-Duplex MasterMode SPI_MODE_MASTERDirection SPI_DIRECTION_2LINES双线 MOSI/MISO 独立主动输出时钟可同步下发指令、读取返回数据。Full-Duplex SlaveMode SPI_MODE_SLAVEDirection SPI_DIRECTION_2LINESSTM32 作为从设备接收外部 MCU 的时钟信号。Half-Duplex MasterMode SPI_MODE_MASTERDirection SPI_DIRECTION_1LINE单线双向传输收发分时复用一根引脚。Half-Duplex SlaveMode SPI_MODE_SLAVEDirection SPI_DIRECTION_1LINE单线模式下 STM32 充当从机。Receive Only MasterMode SPI_MODE_MASTERDirection SPI_DIRECTION_2LINES_RXONLY主机仅接收 MISO 的数据无法主动下发 FLASH 指令。Receive Only SlaveMode SPI_MODE_SLAVE开启仅接收模式STM32 作为从端只接收外部下发数据。Transmit Only MasterMode SPI_MODE_MASTER仅启用 MOSI 发送通路主机只能发送指令拿不到 FLASH 传回的 ID、状态寄存器、存储数据。Transmit Only SlaveMode SPI_MODE_SLAVE仅向外发送数据STM32 充当从端发送数据。3、Basic Parameters基础参数组界面配置项取值**SPI_InitTypeDef **结构体赋值说明Frame FormatMotorolahspi1.Init.TIMode SPI_TIMODE_DISABLEMotorola 原生遵循的标准 SPI 协议Data Size8 Bitshspi1.Init.DataSize SPI_DATASIZE_8BIT传输数据宽度First BitMSB Firsthspi1.Init.FirstBit SPI_FIRSTBIT_MSB数据传输规则高位先行 MSB4、Clock Parameters时钟参数组SPI 时序核心配置时钟分频系数:Prescaler 4 根据需要调整SPI1 挂载 APB2 总线主频为 72MHz分频系数 472MHz / 4 18MHz界面显示 Baud Rate18.0MBits/s。工作模式配置:Clock Polarity (CPOL) Low、Clock Phase (CPHA) 1 Edge5、Advanced Parameters高级参数组CRC校验CRC Calculation Disabledhspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE;CS选择软件片选模式MODEHardware NSS Signal DisabledNSS Signal Type Software6、关键参数说明Clock Prescaler时钟分频速度越快传输越快需兼容外设。CPOL/CPHA严格匹配从机设备时序。NSS Software软件控制片选工程最稳定。First BitMSB 高位优先几乎所有设备通用。7、配置注意事项时序模式必须和从机完全一致否则百分百乱码。多设备共用SPI总线时必须分时拉低对应CS片选。高速通信时降低分频过高防止外设时序不支持导致丢数。SPI无硬件应答通信异常需软件重试机制。四、SPI编程模块1、SPI结构体1.底层硬件寄存器结构体 SPI_TypeDef映射 STM32 片上 SPI 外设全部物理寄存器地址是寄存器操作、标准库 / LL/HAL 库底层操作的硬件入口。typedef struct { __IO uint16_t CR1; // 控制寄存器1 uint16_t RESERVED0; __IO uint16_t CR2; // 控制寄存器2 uint16_t RESERVED1; __IO uint16_t SR; // 状态寄存器 uint16_t RESERVED2; __IO uint16_t DR; // 数据寄存器 uint16_t RESERVED3; __IO uint16_t CRCPR; // CRC多项式寄存器 uint16_t RESERVED4; __IO uint16_t RXCRCR; // 接收CRC寄存器 uint16_t RESERVED5; __IO uint16_t TXCRCR; // 发送CRC寄存器 } SPI_TypeDef;2.初始化配置结构体 SPI_InitTypeDef统一封装 SPI 所有时序、帧格式、总线模式参数CubeMX 自动填充作为HAL_SPI_Init()入参完成外设初始化。typedef struct { uint32_t Mode; // 主/从模式 uint32_t Direction; // 传输方向全双工/单线 uint32_t DataSize; // 数据位宽 8bit/16bit uint32_t CLKPolarity; // CPOL 时钟极性 uint32_t CLKPhase; // CPHA 时钟相位 uint32_t NSS; // 软件/硬件片选模式 uint32_t BaudRatePrescaler; // APB时钟分频系数 uint32_t FirstBit; // MSB/LSB先行 uint32_t TIMode; // TI专属同步模式开关 uint32_t CRCCalculation; // CRC校验使能 uint32_t CRCPoly; // CRC多项式值 } SPI_InitTypeDef;3.外设句柄结构体 SPI_HandleTypeDef全局唯一 SPI 外设管理句柄整合寄存器基地址、时序配置、总线状态、DMA 资源、中断回调所有 HAL SPI API 均以此结构体指针作为入参。typedef struct __SPI_HandleTypeDef { SPI_TypeDef *Instance; // 外设基地址 SPI1/SPI2/SPI3 SPI_InitTypeDef Init; // 初始化配置子结构体 HAL_LockTypeDef Lock; // 多任务互斥锁 __IO HAL_SPI_StateTypeDef State; // SPI运行状态机 __IO uint32_t ErrorCode; // 硬件故障错误标志 DMA_HandleTypeDef *hdmatx; // 发送DMA句柄指针 DMA_HandleTypeDef *hdmarx; // 接收DMA句柄指针 // 用户回调函数指针中断/DMA传输完成触发 void (*TxCpltCallback)(struct __SPI_HandleTypeDef *hspi); void (*RxCpltCallback)(struct __SPI_HandleTypeDef *hspi); void (*TxRxCpltCallback)(struct __SPI_HandleTypeDef *hspi); void (*TxHalfCpltCallback)(struct __SPI_HandleTypeDef *hspi); void (*RxHalfCpltCallback)(struct __SPI_HandleTypeDef *hspi); void (*ErrorCallback)(struct __SPI_HandleTypeDef *hspi); void (*AbortCpltCallback)(struct __SPI_HandleTypeDef *hspi); } SPI_HandleTypeDef;4.状态枚举 HAL_SPI_StateTypeDeftypedef enum { HAL_SPI_STATE_RESET 0x00U, // 外设未初始化 HAL_SPI_STATE_READY 0x20U, // 空闲就绪可发起传输 HAL_SPI_STATE_BUSY_TX 0x21U, // 正在发送数据 HAL_SPI_STATE_BUSY_RX 0x22U, // 正在接收数据 HAL_SPI_STATE_BUSY_TX_RX 0x23U, // 全双工同步收发中 HAL_SPI_STATE_ERROR 0xE0U, // 总线传输故障 HAL_SPI_STATE_ABORT 0xF0U // 传输中止状态 } HAL_SPI_StateTypeDef;5.互斥锁结构体 HAL_LockTypeDeftypedef enum { HAL_UNLOCKED 0x00U, // 无占用可访问SPI HAL_LOCKED 0x01U // 正在使用其他任务阻塞等待 } HAL_LockTypeDef;6.SPI 错误码宏对应 ErrorCode#define HAL_SPI_ERROR_NONE 0x00000000U // 无错误 #define HAL_SPI_ERROR_MODF 0x00000001U // 主从模式冲突硬件NSS常见故障 #define HAL_SPI_ERROR_OVR 0x00000002U // 接收数据溢出 #define HAL_SPI_ERROR_CRC 0x00000004U // CRC校验错误 #define HAL_SPI_ERROR_DMA 0x00000008U // DMA传输异常 #define HAL_SPI_ERROR_TIMEOUT 0x00000010U // 传输超时2、HAL_SPI_API1.初始化、注销底层外设函数HAL_StatusTypeDef HAL_SPI_Init(SPI_HandleTypeDef *hspi); 功能读取句柄内SPI_InitTypeDef配置完成 SPI 寄存器时序、模式、分频初始化开启外设使能位 SPECubeMX 生成MX_SPIx_Init核心调用函数。 HAL_StatusTypeDef HAL_SPI_DeInit(SPI_HandleTypeDef *hspi); 功能复位 SPI 外设所有寄存器关闭 SPE 使能清空句柄状态与错误标志。 void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi); 功能底层硬件初始化回调MspMCU Support Package由HAL_SPI_Init内部自动调用CubeMX 自动生成内部完成 APB 总线时钟开启、SCLK/MOSI/MISO 引脚复用推挽配置。 void HAL_SPI_MspDeInit(SPI_HandleTypeDef *hspi); 功能底层硬件资源释放回调HAL_SPI_DeInit内部自动调用关闭 SPI 总线时钟GPIO 恢复普通输入模式。2.动态注册 / 注销回调函数可选功能宏开关 USE_HAL_SPI_REGISTER_CALLBACKS 1UHAL_StatusTypeDef HAL_SPI_RegisterCallback(SPI_HandleTypeDef *hspi, HAL_SPI_CallbackIDTypeDef CallbackID, pSPI_CallbackTypeDef pCallback); 功能运行时动态注册自定义回调函数替代全局弱定义回调支持多实例 SPI 分时复用不同业务逻辑。 HAL_StatusTypeDef HAL_SPI_UnRegisterCallback(SPI_HandleTypeDef *hspi, HAL_SPI_CallbackIDTypeDef CallbackID); 功能注销已注册的指定 ID 回调恢复默认空回调。3.阻塞轮询 PollingHAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, const uint8_t *pData, uint16_t Size, uint32_t Timeout); 功能单向阻塞发送仅输出 MOSI丢弃 MISO 返回数据全程轮询 TXE/BSY 标志超时返回HAL_TIMEOUT。 HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout); 功能单向阻塞接收持续输出空时钟脉冲读取 MISO 数据忽略 MOSI 输出内容。 HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, const uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout); 功能全双工同步收发每发送 1 字节同时接收 1 字节SPI 标准通信核心 API。4.中断非阻塞 IT异步不阻塞主循环HAL_StatusTypeDef HAL_SPI_Transmit_IT(SPI_HandleTypeDef *hspi, const uint8_t *pData, uint16_t Size); 功能开启 SPI 发送中断函数立即返回全部数据发送完成触发HAL_SPI_TxCpltCallback。 HAL_StatusTypeDef HAL_SPI_Receive_IT(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size); 功能开启 SPI 接收中断收到指定长度数据触发HAL_SPI_RxCpltCallback。 HAL_StatusTypeDef HAL_SPI_TransmitReceive_IT(SPI_HandleTypeDef *hspi, const uint8_t *pTxData, uint8_t *pRxData, uint16_t Size); 功能中断模式全双工同步收发传输完成触发HAL_SPI_TxRxCpltCallback。5.DMA 高速传输零 CPU 搬运大批量 FLASH 读写HAL_StatusTypeDef HAL_SPI_Transmit_DMA(SPI_HandleTypeDef *hspi, const uint8_t *pData, uint16_t Size); 功能DMA 硬件自动搬运内存数据至 SPI 发送寄存器CPU 不参与字节搬运。 HAL_StatusTypeDef HAL_SPI_Receive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size); 功能DMA 将 MISO 接收数据直接存入内存数组高速批量读取 FLASH。 HAL_StatusTypeDef HAL_SPI_TransmitReceive_DMA(SPI_HandleTypeDef *hspi, const uint8_t *pTxData, uint8_t *pRxData, uint16_t Size); 功能DMA 全双工同步收发超大容量连续读写最优方案。 HAL_StatusTypeDef HAL_SPI_DMAPause(SPI_HandleTypeDef *hspi); 功能暂停当前 DMA 传输缓存数据保留可恢复继续传输。 HAL_StatusTypeDef HAL_SPI_DMAResume(SPI_HandleTypeDef *hspi); 功能恢复被暂停的 DMA 传输从暂停位置继续搬运数据。 HAL_StatusTypeDef HAL_SPI_DMAStop(SPI_HandleTypeDef *hspi); 功能彻底关闭 DMA 通道清空 DMA 寄存器与句柄传输状态。6.传输中止函数总线卡死、异常恢复HAL_StatusTypeDef HAL_SPI_Abort(SPI_HandleTypeDef *hspi); 功能阻塞式中止当前任意模式传输等待总线空闲后返回同步释放 SPI。 HAL_StatusTypeDef HAL_SPI_Abort_IT(SPI_HandleTypeDef *hspi); 功能异步中止中断 / DMA 传输函数立即返回中止完成触发HAL_SPI_AbortCpltCallback。7.中断服务总入口stm32f1xx_it.c 中断函数调用void HAL_SPI_IRQHandler(SPI_HandleTypeDef *hspi); 功能SPI 全局中断统一处理入口自动区分 TXE/RXNE 传输中断、OVR/MODF 错误中断内部分发对应回调函数。8.系统弱定义回调函数用户可重写中断 / DMA 完成触发void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi); 单向发送全部数据完成回调。 void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi); 单向接收指定长度数据完成回调。 void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi); 全双工同步收发全部字节完成回调。 void HAL_SPI_TxHalfCpltCallback(SPI_HandleTypeDef *hspi); DMA 发送缓冲区一半数据传输完成双缓冲分片处理大数据。 void HAL_SPI_RxHalfCpltCallback(SPI_HandleTypeDef *hspi); DMA 接收缓冲区一半数据接收完成。 void HAL_SPI_TxRxHalfCpltCallback(SPI_HandleTypeDef *hspi); DMA 全双工收发半缓冲区完成回调。 void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi); 通信故障触发MODF 主从冲突、OVR 溢出、DMA 异常、超时可调用HAL_SPI_GetError()定位故障。 void HAL_SPI_AbortCpltCallback(SPI_HandleTypeDef *hspi); 调用HAL_SPI_Abort_IT异步中止传输完成后触发。9.外设状态、错误查询工具函数HAL_SPI_StateTypeDef HAL_SPI_GetState(const SPI_HandleTypeDef *hspi); 功能读取 SPI 实时运行状态复位 / 就绪 / 发送忙 / 接收忙 / 收发忙 / 错误 / 中止发起新传输前判断总线是否空闲避免并发冲突。 uint32_t HAL_SPI_GetError(const SPI_HandleTypeDef *hspi); 功能读取句柄内部错误标志位返回HAL_SPI_ERROR_MODF/OVR/CRC/DMA/TIMEOUT等掩码用于排查 W25Q64 读写失败、无返回数据等故障。五、SPI 应用实例1、硬件原理图FLASH(W25Q64)2、CubeMX配置1.系统时钟配置启用 HSE 外部晶振PLL 倍频得到系统主频 72MHzAPB2 预分频设置为 1SPI1 总线时钟维持 72MHz。2.SPI1 参数配置Parameter Settings分类配置项取值Basic ParametersModeFull-Duplex MasterFrame FormatMotorolaData Size8 BitsFirst BitMSB FirstClock ParametersPrescaler418MHz 上限速率CPOLLowCPHA1 EdgeAdvanced ParametersCRC CalculationDisabledNSS Signal TypeSoftware3.NVIC 配置采用阻塞轮询方案时SPI 事件中断、错误中断全部取消勾选后续使用 DMA / 中断异步传输再开启中断并分配合适优先级。4.DMA 配置按需开启小体量参数存储DMA 保持关闭大批量写入字库、位图资源开启 SPI1 TX DMA大数据回读额外开启 RX DMADMA 位宽统一设置 Byte。5.GPIO 细节配置PA4GPIO_Output推挽输出初始电平 High默认取消片选。PA5/PA6/PA7CubeMX 自动分配 SPI1 复用推挽无需手动修改。6.串口配置USART1 PA9/PA10异步串口波特率 1152008N1用于 printf 打印调试信息。7.工程生成设置选择 HAL 库生成分开的.c/.h 文件。3、工程源码1. w25q64.h 驱动头文件#ifndef __W25Q64_H #define __W25Q64_H #include main.h #include spi.h // 片选控制宏 PA4 #define W25Q_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define W25Q_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) // W25Q64 标准指令集 #define W25Q_WRITE_EN 0x06U #define W25Q_WRITE_DIS 0x04U #define W25Q_READ_STATUS_REG 0x05U #define W25Q_PAGE_PROG 0x02U #define W25Q_READ_DATA 0x03U #define W25Q_SECTOR_ERASE 0x20U #define W25Q_READ_JEDEC_ID 0x9FU // W25Q64 容量参数 #define W25Q_PAGE_SIZE 256U #define W25Q_SECTOR_SIZE 4096U #define W25Q_ADDR_MAX 0x7FFFFFU // 底层单字节全双工收发 HAL_StatusTypeDef W25Q_SPI_RW(uint8_t tx_data, uint8_t *rx_data); // 读取JEDEC设备IDW25Q64标准ID0xEF4017 uint32_t W25Q_Read_JedecID(void); // 轮询等待FLASH擦写内部操作完成 HAL_StatusTypeDef W25Q_Wait_Busy(uint32_t timeout_ms); // 扇区擦除最小擦除单元4KB HAL_StatusTypeDef W25Q_Sector_Erase(uint32_t addr); // 页写入单次写入长度不可超过256字节 HAL_StatusTypeDef W25Q_Page_Write(uint32_t addr, uint8_t *buf, uint16_t len); // 连续任意长度读取存储数据 HAL_StatusTypeDef W25Q_Buffer_Read(uint32_t addr, uint8_t *buf, uint32_t len); #endif2. w25q64.c 驱动实现文件#include w25q64.h extern SPI_HandleTypeDef hspi1; /** * brief SPI单字节同步收发 * param tx_data 发送字节 * param rx_data 接收字节缓存传NULL则丢弃接收数据 * retval HAL状态码 */ HAL_StatusTypeDef W25Q_SPI_RW(uint8_t tx_data, uint8_t *rx_data) { uint8_t rx_tmp 0xFF; HAL_StatusTypeDef ret HAL_SPI_TransmitReceive(hspi1, tx_data, rx_tmp, 1, 100); if(rx_data ! NULL) *rx_data rx_tmp; return ret; } /** * brief 读取W25Q64 JEDEC ID正常返回0xEF4017 * retval 24位ID读取失败返回0xFFFFFF */ uint32_t W25Q_Read_JedecID(void) { uint8_t id_buf[3] {0}; W25Q_CS_LOW(); W25Q_SPI_RW(W25Q_READ_JEDEC_ID, NULL); W25Q_SPI_RW(0x00, id_buf[0]); W25Q_SPI_RW(0x00, id_buf[1]); W25Q_SPI_RW(0x00, id_buf[2]); W25Q_CS_HIGH(); return ((uint32_t)id_buf[0] 16) | ((uint32_t)id_buf[1] 8) | id_buf[2]; } /** * brief 轮询状态寄存器BUSY位等待擦写完成 * param timeout_ms 超时毫秒 * retval HAL_OK / HAL_TIMEOUT */ HAL_StatusTypeDef W25Q_Wait_Busy(uint32_t timeout_ms) { uint8_t sta_reg 0xFF; uint32_t tick_start HAL_GetTick(); while(1) { W25Q_CS_LOW(); W25Q_SPI_RW(W25Q_READ_STATUS_REG, NULL); W25Q_SPI_RW(0x00, sta_reg); W25Q_CS_HIGH(); // BIT0BUSY位0代表芯片空闲 if((sta_reg 0x01U) 0U) return HAL_OK; if(HAL_GetTick() - tick_start timeout_ms) return HAL_TIMEOUT; } } /** * brief 扇区擦除 4KB * param addr 目标扇区任意地址 */ HAL_StatusTypeDef W25Q_Sector_Erase(uint32_t addr) { HAL_StatusTypeDef ret; addr W25Q_ADDR_MAX; // 1. 发送写使能指令 W25Q_CS_LOW(); W25Q_SPI_RW(W25Q_WRITE_EN, NULL); W25Q_CS_HIGH(); HAL_Delay(1); // 2. 发送扇区擦除指令24位地址 W25Q_CS_LOW(); W25Q_SPI_RW(W25Q_SECTOR_ERASE, NULL); W25Q_SPI_RW((addr 16) 0xFFU, NULL); W25Q_SPI_RW((addr 8) 0xFFU, NULL); W25Q_SPI_RW(addr 0xFFU, NULL); W25Q_CS_HIGH(); // 3. 等待擦除完成最大300ms ret W25Q_Wait_Busy(300); return ret; } /** * brief 单页写入单次长度≤256字节 * param addr 写入起始地址 * param buf 待写入数据数组 * param len 写入字节长度 */ HAL_StatusTypeDef W25Q_Page_Write(uint32_t addr, uint8_t *buf, uint16_t len) { HAL_StatusTypeDef ret; addr W25Q_ADDR_MAX; // 限制不跨页防止数据覆盖 uint16_t page_remain W25Q_PAGE_SIZE - (addr % W25Q_PAGE_SIZE); if(len page_remain) len page_remain; // 写使能 W25Q_CS_LOW(); W25Q_SPI_RW(W25Q_WRITE_EN, NULL); W25Q_CS_HIGH(); HAL_Delay(1); // 发送页写指令24位地址 W25Q_CS_LOW(); W25Q_SPI_RW(W25Q_PAGE_PROG, NULL); W25Q_SPI_RW((addr 16) 0xFFU, NULL); W25Q_SPI_RW((addr 8) 0xFFU, NULL); W25Q_SPI_RW(addr 0xFFU, NULL); // 循环写入数据 for(uint16_t i 0; i len; i) { W25Q_SPI_RW(buf[i], NULL); } W25Q_CS_HIGH(); // 等待编程完成 ret W25Q_Wait_Busy(100); return ret; } /** * brief 连续批量读取FLASH存储数据 * param addr 读取起始地址 * param buf 接收缓存数组 * param len 读取字节总数 */ HAL_StatusTypeDef W25Q_Buffer_Read(uint32_t addr, uint8_t *buf, uint32_t len) { HAL_StatusTypeDef ret; addr W25Q_ADDR_MAX; uint8_t cmd_tx[4] {W25Q_READ_DATA, (addr16)0xFFU, (addr8)0xFFU, addr0xFFU}; W25Q_CS_LOW(); // 先发4字节读指令 24位地址 ret HAL_SPI_Transmit(hspi1, cmd_tx, 4, 100); if(ret ! HAL_OK) { W25Q_CS_HIGH(); return ret; } // 持续读取目标长度数据 ret HAL_SPI_Receive(hspi1, buf, len, 200); W25Q_CS_HIGH(); return ret; }3. usart.c printf 打印重定向#include stdio.h extern UART_HandleTypeDef huart1; // printf串口输出重定向 int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, HAL_MAX_DELAY); return ch; }4. main.c 主函数测试#include main.h #include spi.h #include usart.h #include gpio.h #include w25q64.h #include stdio.h #include string.h // 测试写入数据 uint8_t test_write_buf[] STM32F103ZET6 W25Q64 SPI HAL; uint8_t test_read_buf[128] {0}; int main(void) { // HAL库初始化、系统时钟、外设初始化 HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_SPI1_Init(); MX_USART1_UART_Init(); printf( F103ZET6 W25Q64 FLASH TEST \r\n); // 1、读取JEDEC ID检测板载FLASH硬件是否正常 uint32_t chip_id W25Q_Read_JedecID(); printf(W25Q64 JEDEC ID 0x%06X\r\n, chip_id); if(chip_id 0xFFFFFFU) { printf(ERROR: FLASH No Response! Check SPI Wiring CS\r\n); } else if(chip_id 0xEF4017U) { printf(Detect W25Q64 Success!\r\n); } else { printf(Unknown FLASH Chip ID\r\n); } // 2、擦除0号扇区4KB printf(Start Sector Erase 0x000000\r\n); W25Q_Sector_Erase(0x000000U); printf(Erase Complete\r\n); // 3、向0地址写入测试字符串 printf(Write Data:%s\r\n, test_write_buf); W25Q_Page_Write(0x000000U, test_write_buf, sizeof(test_write_buf)); // 4、读取0地址数据存入缓存 memset(test_read_buf, 0, sizeof(test_read_buf)); W25Q_Buffer_Read(0x000000U, test_read_buf, sizeof(test_write_buf)); printf(Read Data:%s\r\n, test_read_buf); // 5、读写数据校验 if(memcmp(test_write_buf, test_read_buf, sizeof(test_write_buf)) 0) { printf(Read Write Verify PASS\r\n); } else { printf(Read Write Verify FAIL\r\n); } // 主循环每秒打印运行状态 while (1) { HAL_Delay(1000); printf(Run......\r\n); } }4、实验结果六、SPI 核心要点与避坑大全1、必考核心知识点SPI 四线同步全双工高速总线传输速度为嵌入式常规总线最快。核心时序由 CPOL、CPHA 组合成4种工作模式必须与外设匹配。无硬件应答、无校验通信可靠性依赖软件逻辑。多设备共用总线依靠独立CS片选分时复用通信。高速大批量传输必须使用 DMA避免 CPU 阻塞卡顿。2、高频坑点时序模式不匹配导致百分百乱码、设备无响应。多设备通信未严格分时操作CS导致数据串扰。时钟频率过高外设不支持导致丢数、花屏。忘记拉低/拉高片选通信完全失效。阻塞式传输刷屏导致程序卡顿、帧率低。未做超时与重试偶尔通信异常无法自愈。3、工程最佳实践统一使用软件CS片选兼容性、灵活性最强。屏幕、Flash大批量传输优先使用 SPIDMA。初始化时序严格对照外设手册优先Mode0/Mode2。通信失败增加软件重试机制提升稳定性。低速调试用阻塞API高速工程用DMA非阻塞。4、总结SPI是高速同步全双工串行总线依靠SCLK同步时钟、MOSI/MISO双向传输、CS片选选通设备通过CPOL/CPHA配置四种时序模式具备传输速度快、稳定性高的特点搭配DMA可实现高速流式数据传输是屏幕、Flash、高速传感器的核心通信外设。七、全篇总结SPI 是嵌入式开发中高速通信的核心外设是屏幕驱动、存储读写、高速外设交互的必备技能。掌握 SPI 四种时序模式、CubeMX参数配置、阻塞/DMA全套API、多设备总线复用逻辑即可完成绝大多数高速外设项目开发是嵌入式进阶、面试的核心重点考点。