第13章 SPI通信协议全解:底层时序、4种工作模式与W25Qxx Flash芯片读写实战

前言

上一章我们深入掌握了I2C低速串行总线的底层原理与工业外设驱动开发,而SPI是嵌入式开发中高速外设通信的核心总线,是大容量Flash、高速传感器、显示屏、ADC/DAC等外设的首选通信方式。对应51单片机开发,我们通常通过软件翻转IO模拟SPI时序,存在时序精度差、CPU占用率100%、传输速度上限低、全双工收发逻辑复杂的痛点;而STM32内置3路硬件SPI控制器,支持全双工同步通信、可编程波特率、4种工作模式,可与DMA无缝联动实现零CPU占用的高速传输,完美适配工业场景下大容量数据、高速率的通信需求。新手入门SPI普遍面临三大痛点:4种工作模式混淆导致通信异常、Flash读写数据错乱、高速传输丢包、多从机总线冲突。本章将严格遵循「先寄存器原理拆解,再HAL库封装逻辑」的顺序,联动51单片机对应知识点,从底层时序到工业级实战全面吃透SPI通信,完成W25Qxx系列Flash芯片的全功能驱动开发。

本章目录

  • 一、本章学习目标
  • 二、核心知识点
    • 2.1 SPI协议基础与51单片机驱动方案核心对比
    • 2.2 SPI协议底层时序与4种工作模式核心规则
    • 2.3 SPI多从机总线架构与寻址逻辑
    • 2.4 STM32硬件SPI核心寄存器与C语言位操作联动
    • 2.5 HAL库SPI封装逻辑与核心API深度解析
  • 三、STM32CubeMX+Keil5保姆式实操:W25Qxx Flash驱动全场景实战
    • 3.1 工程创建与基础配置
    • 3.2 SPI外设与GPIO图形化配置
    • 3.3 工程代码生成与W25Qxx驱动文件移植
    • 3.4 Flash读写擦除全功能业务代码编写
    • 3.5 编译、烧录与效果验证
  • 四、保姆式排错指南
  • 五、我的踩坑记录
  • 六、课后小练习(附完整标准答案)
    • 6.1 基础巩固练习
    • 6.2 进阶实战练习
  • 七、核心知识点速记
  • 八、本章小结

一、本章学习目标

  1. 掌握SPI串行通信协议的底层工作原理、全双工同步通信机制与4种工作模式的核心规则,对比51单片机软件模拟方案的核心差异,理解SPI在工业高速通信中的工程价值
  2. 吃透STM32硬件SPI的内核架构、寄存器级工作原理,联动C语言位操作、指针知识点,能独立实现寄存器级的SPI初始化与全双工读写操作
  3. 掌握HAL库SPI的封装逻辑与核心API使用,区分阻塞/中断/DMA三种传输模式的选型逻辑,适配不同工业开发场景的速率与实时性需求
  4. 熟练完成W25Qxx系列Flash芯片的全功能驱动开发,实现芯片ID读取、扇区擦除、页编程、随机读、批量读写等核心功能,完成大容量数据掉电存储实战
  5. 能独立排查SPI通信无响应、数据错乱、Flash读写失败、DMA传输异常等高频问题,掌握高速SPI通信的硬件与软件优化方案

二、核心知识点

2.1 SPI协议基础与51单片机驱动方案核心对比

术语通俗解释 :SPI全称串行外设接口,是一种四线式全双工同步串行通信总线,由摩托罗拉公司推出,通过SCK(串行时钟)、MOSI(主发从收)、MISO(主收从发)、CS(片选)四根线实现主设备与从设备之间的高速同步通信。类比两台同步工作的移位寄存器:主设备通过SCK提供统一的时钟节拍,每一个时钟周期,主从设备同时移位发送1位数据、接收1位数据,实现收发同步完成,全双工通信。

SPI协议核心特性:

  • 四线制:SCK、MOSI、MISO、CS,极简硬件设计,节省IO资源;
  • 主从架构:通信由主机发起,从机响应,支持单主机多从机架构,通过独立CS引脚寻址从机;
  • 全双工通信:同一时钟周期内,主机与从机可同时发送和接收数据,传输效率远高于I2C半双工通信;
  • 同步通信:收发双方共用主机提供的SCK时钟,无需约定波特率,无时钟误差问题,通信稳定性强;
  • 速率上限高:STM32F103 SPI最高时钟频率为36MHz(系统主频72MHz的1/2),传输速率可达36Mbps,是I2C快速模式的90倍;
  • 无硬件应答机制:通信可靠性由软件协议保证,无需硬件应答,时序更简单,传输效率更高。

我们从51单片机的软件模拟方案出发,无缝衔接理解STM32硬件SPI的核心优势:

驱动方案 51单片机软件模拟SPI STM32硬件SPI 对开发的核心影响
时序实现 手动翻转IO口,通过延时控制SCK时序,精度差,受主频、中断干扰极大 硬件控制器自动生成标准时序,精度高,不受软件、中断干扰,支持可编程波特率 软件模拟时序高速下极易出现数据错位,不同主频芯片需重新调整延时;硬件时序兼容性极强,代码可跨芯片移植,支持超高速稳定传输
CPU占用 通信全程CPU需循环翻转IO、读写数据,占用率100%,无法同步执行其他任务 仅需配置初始参数,数据收发全程硬件自动完成,配合DMA可实现零CPU占用 软件模拟方案大容量数据传输时CPU完全卡死,无法同步执行采集、控制逻辑;硬件方案传输期间CPU可正常执行核心业务,系统实时性大幅提升
传输效率 半双工收发,最高速率约500Kbps,全双工逻辑复杂,极易出错 原生全双工同步收发,最高速率36Mbps,传输效率提升70倍以上 软件模拟方案无法实现大容量Flash、高速显示屏的流畅驱动;硬件SPI可实现毫秒级的大容量数据读写,适配工业高速数据存储场景
多从机支持 多从机切换需重新编写时序代码,兼容性差,极易出现总线冲突 原生支持多从机架构,通过独立CS引脚切换从机,硬件保证总线隔离,无冲突风险 软件模拟方案一条总线最多挂载2-3个从机;硬件SPI一条总线可挂载数十个从机,完美适配工业多外设场景
开发难度 需开发者完全掌握协议底层时序,手动实现SCK时序、全双工收发,代码量大,逻辑复杂 HAL库已封装标准通信API,仅需调用收发函数即可完成全双工通信,代码极简,稳定性强 软件模拟方案开发周期长,易出bug;硬件方案可快速实现外设驱动,聚焦业务逻辑开发,效率提升数十倍

2.2 SPI协议底层时序与4种工作模式核心规则

1. SPI总线核心信号线定义
信号线 名称 方向 核心功能
SCK 串行时钟线 主机→从机 主机输出同步时钟,控制通信节拍,决定通信速率
MOSI 主发从收数据线 主机→从机 主机向从机发送数据,从机接收数据
MISO 主收从发数据线 从机→主机 从机向主机发送数据,主机接收数据
CS/SS 片选线 主机→从机 主机拉低对应从机的CS引脚,选中目标从机,通信结束后拉高,一条总线每个从机对应独立的CS引脚

核心规则:SPI通信必须先拉低目标从机的CS引脚,选中从机后才能发起通信,通信全程CS必须保持低电平,通信结束后必须拉高CS引脚,释放总线。

2. SPI全双工通信底层原理

SPI的核心是主从机同步移位寄存器,主机与从机各有一个移位寄存器,通过MOSI、MISO线首尾相连,形成一个环形移位链路。

  • 通信前,主机与从机将待发送的数据写入各自的移位寄存器;
  • 主机输出SCK时钟,每一个时钟周期,移位寄存器移位1位,主机通过MOSI线向从机发送1位数据,同时从机通过MISO线向主机发送1位数据;
  • 8个时钟周期后,主机与从机的移位寄存器完成一次数据交换,实现了1字节的全双工收发,主机发送1字节的同时,必然收到从机的1字节数据。

通俗类比:两个面对面的人,手里各有8张牌,每喊一个节拍(SCK时钟),两人同时把自己最左边的牌递给对方,8个节拍后,两人手里的牌完成了完整交换,实现了同时发送和接收。

3. SPI 4种工作模式核心规则

SPI的4种工作模式由时钟极性CPOL时钟相位CPHA两个参数组合决定,核心是定义SCK时钟的空闲电平与数据采样、更新的时刻,这是新手入门的头号难点,也是通信异常的核心根源。

核心参数定义
  • CPOL(时钟极性) :定义SPI总线空闲时SCK的电平状态
    • CPOL=0:总线空闲时SCK为低电平
    • CPOL=1:总线空闲时SCK为高电平
  • CPHA(时钟相位) :定义数据采样与更新的时钟边沿
    • CPHA=0:在SCK的第一个跳变沿(上升沿/下降沿)采样数据,第二个跳变沿更新数据
    • CPHA=1:在SCK的第二个跳变沿采样数据,第一个跳变沿更新数据
4种工作模式详解
工作模式 CPOL CPHA 空闲SCK电平 数据采样时刻 数据更新时刻 工业常用度
模式0 0 0 低电平 SCK上升沿 SCK下降沿 最高,W25Qxx、OLED、绝大多数传感器均使用此模式
模式1 0 1 低电平 SCK下降沿 SCK上升沿 极少使用
模式2 1 0 高电平 SCK下降沿 SCK上升沿 较少使用
模式3 1 1 高电平 SCK上升沿 SCK下降沿 常用,部分高速Flash、显示屏使用此模式

核心选型规则:SPI主机的工作模式必须与从机完全一致,否则会出现数据采样错误,导致通信完全异常。开发前必须查阅从机芯片手册,确认其支持的SPI工作模式,严格匹配CPOL与CPHA参数。

4. 完整的SPI单字节读写时序(模式0)

工业最常用的模式0完整时序流程:

  1. 总线空闲状态:SCK为低电平,CS为高电平,MOSI/MISO电平任意;
  2. 主机拉低目标从机的CS引脚,选中从机,启动一次通信;
  3. 主机准备待发送的1字节数据,写入SPI数据寄存器;
  4. 主机输出SCK时钟,每个时钟周期:
    • SCK上升沿:主机采样MISO线上的从机数据,从机采样MOSI线上的主机数据;
    • SCK下降沿:主机更新MOSI线上的下一位数据,从机更新MISO线上的下一位数据;
  5. 8个时钟周期后,1字节数据收发完成,主机收到从机发送的1字节数据;
  6. 主机拉高CS引脚,结束本次通信,总线回到空闲状态。

2.3 SPI多从机总线架构与寻址逻辑

SPI总线支持单主机多从机架构,核心分为两种接线方式,适配不同的应用场景:

1. 独立片选模式(工业首选)
  • 接线方式:所有从机的SCK、MOSI、MISO引脚分别并联到主机的对应引脚,每个从机的CS引脚单独连接到主机的一个GPIO引脚,主机通过拉低对应从机的CS引脚选中目标设备。
  • 核心优势:总线隔离性好,同一时间只有一个从机被选中,无总线冲突风险,时序简单,兼容性强,支持任意数量的从机(仅受主机GPIO数量限制)。
  • 适用场景:绝大多数工业开发场景,多外设、高稳定性需求的项目。
2. 菊花链模式(级联模式)
  • 接线方式:主机SCK并联到所有从机,主机MOSI接第一个从机的MOSI,第一个从机的MISO接第二个从机的MOSI,以此类推,最后一个从机的MISO接主机的MISO,所有从机共用一个CS引脚。
  • 核心优势:节省主机GPIO引脚,仅需1个CS引脚即可控制多个从机。
  • 核心劣势:通信数据需经过所有从机的移位寄存器,传输延迟大,仅支持同步收发,无法单独寻址单个从机,兼容性差。
  • 适用场景:LED点阵、多通道DAC等需要同步控制的串行级联设备。

SPI与I2C多从机架构核心对比

特性 SPI独立片选模式 I2C总线
寻址方式 硬件CS引脚寻址,每个从机独立GPIO 软件7位地址寻址,无需额外GPIO
总线冲突 无冲突,同一时间仅一个从机被选中 存在地址冲突风险,需保证从机地址唯一
GPIO占用 3条公共线+每个从机1条CS线,从机越多占用GPIO越多 仅需2条线,与从机数量无关
通信效率 全双工高速通信,无地址、应答开销,效率极高 半双工通信,需地址、应答开销,效率较低
多机通信 仅支持单主机多从机,不支持多主机 支持多主机多从机架构

2.4 STM32硬件SPI核心寄存器与C语言位操作联动

STM32F103内置3个SPI控制器,SPI1挂载在APB2总线(最高72MHz),SPI2/SPI3挂载在APB1总线(最高36MHz)。我们以SPI1为例,拆解核心寄存器的功能与C语言位操作实现,联动51单片机软件模拟逻辑,实现无缝衔接。

寄存器名称 结构体成员 读写属性 核心功能与位操作详解
控制寄存器1 SPI1->CR1 读写 SPI核心配置寄存器,关键位: - 位0(CPHA):时钟相位配置,0=模式0/2,1=模式1/3 - 位1(CPOL):时钟极性配置,0=空闲低电平,1=空闲高电平 - 位2(MSTR):主从模式选择,1=主机模式,0=从机模式 - 位3-5(BR):波特率分频,000=2分频,001=4分频,以此类推,72MHz主频下2分频得到36MHz最高速率 - 位6(SPE):SPI使能位,写1开启SPI外设 - 位7(LSBFIRST):数据位序,0=高位在前(MSB),1=低位在前(LSB) - 位8(SSI):软件内部从机选择,软件CS模式使用 - 位9(SSM):软件从机管理使能,1=软件CS模式,0=硬件CS模式 - 位10(RXONLY):只读模式,0=全双工模式 - 位11(DFF):数据帧格式,0=8位数据,1=16位数据
控制寄存器2 SPI1->CR2 读写 中断与DMA控制寄存器,关键位: - 位0(RXDMAEN):接收DMA使能位 - 位1(TXDMAEN):发送DMA使能位 - 位6(TXEIE):发送缓冲区空中断使能 - 位7(RXNEIE):接收缓冲区非空中断使能
状态寄存器 SPI1->SR 只读 SPI状态标志位,关键位: - 位0(RXNE):接收缓冲区非空,收到1字节数据后置1,读取DR寄存器后清零 - 位1(TXE):发送缓冲区空,可写入下一个发送数据 - 位7(BSY):总线忙标志位,通信期间置1,通信完成后清零
数据寄存器 SPI1->DR 读写 低8/16位有效,写入数据启动发送,读取数据获取接收缓冲区的内容,全双工收发的核心寄存器

C语言寄存器操作完整示例:SPI1主机模式初始化(模式0,8位数据,高位在前,18MHz波特率)与全双工字节读写函数,完全对应51单片机软件模拟逻辑

c 复制代码
#include "stm32f1xx.h"

// SPI1主机模式初始化,模式0,8位,MSB在前,18MHz波特率
void SPI1_Init(void)
{
  // 1. 开启GPIOA与SPI1时钟
  RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_SPI1EN;
  
  // 2. 配置SPI引脚:PA5(SCK)、PA7(MOSI)复用推挽输出,PA6(MISO)浮空输入,PA4(CS)推挽输出
  GPIOA->CRL &= ~(GPIO_CRL_MODE4 | GPIO_CRL_CNF4 | GPIO_CRL_MODE5 | GPIO_CRL_CNF5 | GPIO_CRL_MODE6 | GPIO_CRL_CNF6 | GPIO_CRL_MODE7 | GPIO_CRL_CNF7);
  // PA4(CS):推挽输出,50MHz
  GPIOA->CRL |= GPIO_CRL_MODE4_1;
  // PA5(SCK)、PA7(MOSI):复用推挽输出,50MHz
  GPIOA->CRL |= GPIO_CRL_MODE5_1 | GPIO_CRL_CNF5_1 | GPIO_CRL_MODE7_1 | GPIO_CRL_CNF7_1;
  // PA6(MISO):浮空输入
  GPIOA->CRL |= GPIO_CRL_CNF6_0;
  // 3. CS引脚默认拉高
  GPIOA->BSRR = GPIO_BSRR_BS4;
  
  // 4. 配置SPI1 CR1寄存器:主机模式,模式0,8位,MSB在前,4分频(72/4=18MHz)
  SPI1->CR1 = 0;
  SPI1->CR1 |= SPI_CR1_MSTR | SPI_CR1_BR_1 | SPI_CR1_SSM | SPI_CR1_SSI;
  // 5. 开启SPI外设
  SPI1->CR1 |= SPI_CR1_SPE;
}

// SPI全双工读写1字节数据
uint8_t SPI_ReadWrite_Byte(uint8_t tx_data)
{
  // 等待发送缓冲区空
  while(!(SPI1->SR & SPI_SR_TXE));
  // 写入发送数据,启动传输
  SPI1->DR = tx_data;
  // 等待接收完成
  while(!(SPI1->SR & SPI_SR_RXNE));
  // 返回读取到的数据
  return (uint8_t)SPI1->DR;
}

// W25Qxx读芯片ID示例
#define W25QXX_CMD_READ_ID 0x90
uint16_t W25Qxx_Read_ID(void)
{
  uint16_t id;
  // 拉低CS,选中芯片
  GPIOA->BSRR = GPIO_BSRR_BR4;
  // 发送读ID命令
  SPI_ReadWrite_Byte(W25QXX_CMD_READ_ID);
  // 发送3字节地址0x000000
  SPI_ReadWrite_Byte(0x00);
  SPI_ReadWrite_Byte(0x00);
  SPI_ReadWrite_Byte(0x00);
  // 读取厂商ID和设备ID
  uint8_t manu_id = SPI_ReadWrite_Byte(0xFF);
  uint8_t dev_id = SPI_ReadWrite_Byte(0xFF);
  // 拉高CS,结束通信
  GPIOA->BSRR = GPIO_BSRR_BS4;
  id = (manu_id << 8) | dev_id;
  return id;
}

2.5 HAL库SPI封装逻辑与核心API深度解析

HAL库将SPI的底层寄存器操作封装为标准化的结构体与API函数,无需手动处理时序、标志位,仅需简单调用即可完成稳定的全双工通信,大幅提升开发效率。

1. SPI核心配置结构体

HAL库用SPI_HandleTypeDef结构体封装SPI外设的所有配置参数,与寄存器一一对应,联动C语言结构体知识点:

c 复制代码
typedef struct {
  SPI_TypeDef                 *Instance;    // SPI外设基地址,SPI1/SPI2/SPI3
  SPI_InitTypeDef             Init;         // SPI核心初始化参数
  uint8_t                     *pTxBuffPtr;  // 发送缓存指针
  uint16_t                    TxXferSize;   // 发送数据长度
  __IO uint16_t               TxXferCount;  // 剩余发送长度
  uint8_t                     *pRxBuffPtr;  // 接收缓存指针
  uint16_t                    RxXferSize;   // 接收数据长度
  __IO uint16_t               RxXferCount;  // 剩余接收长度
  DMA_HandleTypeDef           *hdmatx;      // 发送DMA句柄
  DMA_HandleTypeDef           *hdmarx;      // 接收DMA句柄
  HAL_LockTypeDef             Lock;         // 锁保护
  __IO HAL_SPI_StateTypeDef   State;        // SPI运行状态
  __IO uint32_t               ErrorCode;    // 错误代码
} SPI_HandleTypeDef;

// SPI核心初始化参数结构体
typedef struct {
  uint32_t Mode;                // 主从模式,SPI_MODE_MASTER主机模式
  uint32_t Direction;           // 通信方向,SPI_DIRECTION_2LINES全双工
  uint32_t DataSize;            // 数据位宽,SPI_DATASIZE_8BIT
  uint32_t CLKPolarity;         // 时钟极性CPOL,SPI_POLARITY_LOW对应CPOL=0
  uint32_t CLKPhase;            // 时钟相位CPHA,SPI_PHASE_1EDGE对应CPHA=0
  uint32_t NSS;                 // 片选模式,SPI_NSS_SOFT软件CS模式
  uint32_t BaudRatePrescaler;   // 波特率分频,SPI_BAUDRATEPRESCALER_4
  uint32_t FirstBit;            // 位序,SPI_FIRSTBIT_MSB高位在前
  uint32_t TIMode;              // TI模式,关闭
  uint32_t CRCCalculation;      // CRC校验,关闭
  uint32_t CRCPolynomial;       // CRC多项式
} SPI_InitTypeDef;
2. HAL库SPI核心API与底层对应关系

工业开发中最常用的全双工通信API,核心分为阻塞、中断、DMA三类,适配不同场景:

HAL库API函数 核心功能 传输模式 适用场景
HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout) 阻塞式发送数据,发送完成后返回 轮询阻塞 短数据、低频率发送,如寄存器配置、命令发送
HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout) 阻塞式接收数据,接收完成后返回 轮询阻塞 纯接收场景,如传感器数据读取
HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout) 阻塞式全双工收发,发送与接收同步完成 轮询阻塞 绝大多数SPI外设的读写操作,如Flash读写、寄存器配置,工业开发最常用
HAL_SPI_Transmit_IT() 中断模式发送数据,非阻塞,发送完成触发回调 中断非阻塞 长数据发送,不希望阻塞主循环的场景
HAL_SPI_Receive_IT() 中断模式接收数据,非阻塞,接收完成触发回调 中断非阻塞 长数据接收,不希望阻塞主循环的场景
HAL_SPI_TransmitReceive_IT() 中断模式全双工收发,非阻塞,收发完成触发回调 中断非阻塞 中等长度数据全双工收发,实时性要求较高的场景
HAL_SPI_Transmit_DMA() DMA模式发送数据,全程零CPU占用,发送完成触发回调 DMA非阻塞 高速大批量数据发送,如显示屏全屏刷新、固件升级
HAL_SPI_Receive_DMA() DMA模式接收数据,全程零CPU占用,接收完成触发回调 DMA非阻塞 高速大批量数据接收,如高速ADC采样、Flash批量读取
HAL_SPI_TransmitReceive_DMA() DMA模式全双工收发,全程零CPU占用,收发完成触发回调 DMA非阻塞 高速全双工大批量数据传输,工业高速数据存储场景

核心回调函数

c 复制代码
// 发送完成回调函数
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi);
// 接收完成回调函数
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi);
// 全双工收发完成回调函数
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi);
// 传输错误回调函数
void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi);

核心使用规则:SPI通信前必须拉低对应从机的CS引脚,通信全程保持低电平,通信完成后必须拉高CS引脚,释放总线。


三、STM32CubeMX+Keil5保姆式实操:W25Qxx Flash驱动全场景实战

本次实操适配STM32F103C8T6核心板,以工业最常用的W25Q64JV Flash芯片(8MB容量)为例,实现芯片ID读取、扇区擦除、页编程、随机读、批量掉电存储全功能开发,全程无跳步,零基础可零报错跟随完成。

硬件说明

外设型号 引脚分配 核心参数 接线说明
W25Q64JV Flash CS→PA4、SCK→PA5、MOSI→PA7、MISO→PA6 64Mbit/8MB容量,SPI模式0/3,最高时钟频率133MHz VCC接3.3V,GND接核心板GND,WP、HOLD引脚接3.3V,严禁接5V
串口USART1 PA9(TX)、PA10(RX) 波特率115200,8位数据、1位停止位、无校验 接USB-TTL模块,TX-RX交叉接线,GND共地

3.1 工程创建与基础配置

  1. 打开STM32CubeMX,点击ACCESS TO MCU SELECTOR,搜索选择STM32F103C8T6,点击Start Project创建工程。
  2. 调试接口配置:点击左侧System Core -> SYS,Debug选项选择Serial Wire,开启SWD串行调试。
  3. 时钟配置:点击RCC,HSE选项选择Crystal/Ceramic Resonator(外部8MHz晶振);进入Clock Configuration选项卡,配置PLL倍频为x9,系统时钟设置为72MHz,APB2总线时钟72MHz,无红色错误提示。

3.2 SPI外设与GPIO图形化配置

  1. SPI1配置:
    • 点击左侧Connectivity -> SPI1,Mode选择Full-Duplex Master(全双工主机模式);
    • 配置参数:
      • Mode:Full-Duplex Master
      • Hardware NSS Signal:Disable(软件CS模式,更灵活)
      • Data Size:8 Bits
      • First Bit:MSB First
      • Clock Polarity (CPOL):Low(CPOL=0)
      • Clock Phase (CPHA):1 Edge(CPHA=0,模式0,匹配W25Qxx)
      • Baud Rate Prescaler:4分频,72MHz/4=18MHz,符合W25Qxx时序要求
      • CRC Calculation:Disabled
    • 引脚自动映射为PA5(SPI1_SCK)、PA6(SPI1_MISO)、PA7(SPI1_MOSI),均为复用推挽输出模式。
  2. GPIO配置:
    • PA4引脚选择GPIO_Output,配置为推挽输出、高速、默认高电平,User Label设为SPI_CS
  3. USART1配置:
    • 点击左侧Connectivity -> USART1,Mode选择Asynchronous
    • 配置参数:Baud Rate=115200,Word Length=8 Bits,Parity=None,Stop Bits=1;
  4. DMA配置(SPI1高速传输):
    • 点击SPI1配置界面的DMA Settings,点击Add添加2个DMA通道:
      1. 发送DMA:DMA Request=SPI1_TX,Channel=DMA1 Channel 3,Direction=Memory To Peripheral,Priority=High,Mode=Normal,Data Width=Byte,Memory地址自增;
      2. 接收DMA:DMA Request=SPI1_RX,Channel=DMA1 Channel 2,Direction=Peripheral To Memory,Priority=High,Mode=Normal,Data Width=Byte,Memory地址自增;
  5. NVIC配置:
    • 点击左侧System Core -> NVIC,优先级分组选择Priority Group 2
    • 勾选SPI1 global interruptUSART1 global interruptDMA1 channel2 global interruptDMA1 channel3 global interrupt,抢占优先级均设为1。

3.3 工程代码生成与W25Qxx驱动文件移植

  1. 工程生成配置:进入Project Manager,设置全英文无空格的工程名与保存路径,Toolchain/IDE选择MDK-ARM V5;进入Code Generator,勾选Generate peripheral initialization as a pair of '.c/.h' files per peripheralKeep User Code when re-generating,点击GENERATE CODE生成工程,完成后点击Open Project打开Keil5工程。
  2. 驱动文件移植:
    • 在工程中新建w25qxx.cw25qxx.h两个文件,添加到工程中;
    • 所有驱动代码均适配STM32F103C8T6与HAL库,可直接复制使用,核心代码见下文。
w25qxx.h头文件
c 复制代码
#ifndef __W25QXX_H
#define __W25QXX_H

#include "stm32f1xx_hal.h"
#include "spi.h"
#include <string.h>

// W25Qxx指令定义
#define W25QXX_CMD_WRITE_ENABLE    0x06  // 写使能
#define W25QXX_CMD_WRITE_DISABLE   0x04  // 写禁用
#define W25QXX_CMD_READ_STATUS1    0x05  // 读状态寄存器1
#define W25QXX_CMD_WRITE_STATUS1   0x01  // 写状态寄存器1
#define W25QXX_CMD_PAGE_PROGRAM    0x02  // 页编程
#define W25QXX_CMD_SECTOR_ERASE    0x20  // 扇区擦除(4KB)
#define W25QXX_CMD_BLOCK_ERASE     0xD8  // 块擦除(64KB)
#define W25QXX_CMD_CHIP_ERASE      0xC7  // 全片擦除
#define W25QXX_CMD_READ_DATA       0x03  // 读数据
#define W25QXX_CMD_FAST_READ       0x0B  // 快速读数据
#define W25QXX_CMD_READ_MANU_ID    0x90  // 读厂商ID
#define W25QXX_CMD_READ_JEDEC_ID   0x9F  // 读JEDEC ID

// W25Qxx芯片型号定义
#define W25Q64  0XEF17  // W25Q64,8MB容量
#define W25Q128 0XEF18  // W25Q128,16MB容量

// CS引脚操作宏定义
#define W25QXX_CS_LOW()  HAL_GPIO_WritePin(SPI_CS_GPIO_Port, SPI_CS_Pin, GPIO_PIN_RESET)
#define W25QXX_CS_HIGH() HAL_GPIO_WritePin(SPI_CS_GPIO_Port, SPI_CS_Pin, GPIO_PIN_SET)

// 函数声明
uint16_t W25Qxx_Read_ID(void);
uint32_t W25Qxx_Read_JEDEC_ID(void);
void W25Qxx_Wait_Busy(void);
void W25Qxx_Write_Enable(void);
void W25Qxx_Sector_Erase(uint32_t sector_addr);
void W25Qxx_Block_Erase(uint32_t block_addr);
void W25Qxx_Chip_Erase(void);
void W25Qxx_Page_Write(uint32_t addr, uint8_t *buf, uint16_t len);
void W25Qxx_Read_Data(uint32_t addr, uint8_t *buf, uint16_t len);
void W25Qxx_Write_Data(uint32_t addr, uint8_t *buf, uint32_t len);

#endif
w25qxx.c驱动文件
c 复制代码
#include "w25qxx.h"

// 芯片ID缓存
static uint16_t w25qxx_id = 0;

// 读取厂商ID与设备ID
uint16_t W25Qxx_Read_ID(void)
{
  uint8_t tx_buf[4] = {W25QXX_CMD_READ_MANU_ID, 0x00, 0x00, 0x00};
  uint8_t rx_buf[4] = {0};
  W25QXX_CS_LOW();
  HAL_SPI_TransmitReceive(&hspi1, tx_buf, rx_buf, 4, 100);
  W25QXX_CS_HIGH();
  w25qxx_id = (rx_buf[2] << 8) | rx_buf[3];
  return w25qxx_id;
}

// 读取JEDEC ID
uint32_t W25Qxx_Read_JEDEC_ID(void)
{
  uint8_t tx_buf[4] = {W25QXX_CMD_READ_JEDEC_ID, 0xFF, 0xFF, 0xFF};
  uint8_t rx_buf[4] = {0};
  W25QXX_CS_LOW();
  HAL_SPI_TransmitReceive(&hspi1, tx_buf, rx_buf, 4, 100);
  W25QXX_CS_HIGH();
  return (rx_buf[1] << 16) | (rx_buf[2] << 8) | rx_buf[3];
}

// 等待芯片忙状态结束
void W25Qxx_Wait_Busy(void)
{
  uint8_t status = 0;
  W25QXX_CS_LOW();
  HAL_SPI_Transmit(&hspi1, (uint8_t *)&W25QXX_CMD_READ_STATUS1, 1, 100);
  do
  {
    HAL_SPI_Receive(&hspi1, &status, 1, 100);
  } while(status & 0x01); // 位0为1表示忙
  W25QXX_CS_HIGH();
}

// 写使能
void W25Qxx_Write_Enable(void)
{
  W25QXX_CS_LOW();
  HAL_SPI_Transmit(&hspi1, (uint8_t *)&W25QXX_CMD_WRITE_ENABLE, 1, 100);
  W25QXX_CS_HIGH();
}

// 扇区擦除,4KB/扇区,addr为扇区首地址
void W25Qxx_Sector_Erase(uint32_t sector_addr)
{
  sector_addr *= 4096; // 转换为字节地址
  W25Qxx_Write_Enable();
  W25Qxx_Wait_Busy();
  uint8_t tx_buf[4] = {W25QXX_CMD_SECTOR_ERASE, (sector_addr >> 16) & 0xFF, (sector_addr >> 8) & 0xFF, sector_addr & 0xFF};
  W25QXX_CS_LOW();
  HAL_SPI_Transmit(&hspi1, tx_buf, 4, 100);
  W25QXX_CS_HIGH();
  W25Qxx_Wait_Busy(); // 等待擦除完成
}

// 块擦除,64KB/块
void W25Qxx_Block_Erase(uint32_t block_addr)
{
  block_addr *= 65536;
  W25Qxx_Write_Enable();
  W25Qxx_Wait_Busy();
  uint8_t tx_buf[4] = {W25QXX_CMD_BLOCK_ERASE, (block_addr >> 16) & 0xFF, (block_addr >> 8) & 0xFF, block_addr & 0xFF};
  W25QXX_CS_LOW();
  HAL_SPI_Transmit(&hspi1, tx_buf, 4, 100);
  W25QXX_CS_HIGH();
  W25Qxx_Wait_Busy();
}

// 全片擦除
void W25Qxx_Chip_Erase(void)
{
  W25Qxx_Write_Enable();
  W25Qxx_Wait_Busy();
  W25QXX_CS_LOW();
  HAL_SPI_Transmit(&hspi1, (uint8_t *)&W25QXX_CMD_CHIP_ERASE, 1, 100);
  W25QXX_CS_HIGH();
  W25Qxx_Wait_Busy(); // 全片擦除耗时约10秒
}

// 页编程,最多256字节/页,不能跨页
void W25Qxx_Page_Write(uint32_t addr, uint8_t *buf, uint16_t len)
{
  if(len > 256) len = 256;
  W25Qxx_Write_Enable();
  W25Qxx_Wait_Busy();
  uint8_t tx_buf[4] = {W25QXX_CMD_PAGE_PROGRAM, (addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF};
  W25QXX_CS_LOW();
  HAL_SPI_Transmit(&hspi1, tx_buf, 4, 100);
  HAL_SPI_Transmit(&hspi1, buf, len, 100);
  W25QXX_CS_HIGH();
  W25Qxx_Wait_Busy();
}

// 读数据,无长度限制
void W25Qxx_Read_Data(uint32_t addr, uint8_t *buf, uint16_t len)
{
  uint8_t tx_buf[4] = {W25QXX_CMD_READ_DATA, (addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF};
  W25QXX_CS_LOW();
  HAL_SPI_Transmit(&hspi1, tx_buf, 4, 100);
  HAL_SPI_Receive(&hspi1, buf, len, 100);
  W25QXX_CS_HIGH();
}

// 任意地址写数据,自动处理跨页、擦除
void W25Qxx_Write_Data(uint32_t addr, uint8_t *buf, uint32_t len)
{
  uint32_t page_remain = 256 - (addr % 256); // 当前页剩余字节数
  if(len <= page_remain) page_remain = len;
  while(1)
  {
    W25Qxx_Page_Write(addr, buf, page_remain);
    if(page_remain == len) break; // 写入完成
    buf += page_remain;
    addr += page_remain;
    len -= page_remain;
    page_remain = (len > 256) ? 256 : len;
  }
}

3.4 Flash读写擦除全功能业务代码编写

在main.c文件中编写业务代码,所有代码必须写在用户代码区,避免重新生成时被覆盖。

c 复制代码
/* USER CODE BEGIN Includes */
#include "w25qxx.h"
#include <stdio.h>
/* USER CODE END Includes */

/* USER CODE BEGIN PV */
// 重定向printf到USART1
int fputc(int ch, FILE *f)
{
  HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
  return ch;
}

// 测试数据
uint8_t write_buf[256] = "STM32 SPI W25Qxx Flash Test! 掉电存储测试数据";
uint8_t read_buf[256] = {0};

// 掉电存储结构体
typedef struct {
  uint32_t boot_count; // 开机次数
  float param1;        // 校准参数1
  float param2;        // 校准参数2
  char device_name[16];// 设备名称
} Device_Param_Typedef;

Device_Param_Typedef dev_param = {0, 1.25f, 3.78f, "STM32_Device"};
Device_Param_Typedef read_param = {0};
/* USER CODE END PV */

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_SPI1_Init();
  MX_USART1_UART_Init();

  /* USER CODE BEGIN 2 */
  printf("===== STM32 SPI W25Qxx Test Start =====\r\n");
  
  // ===================== 1. 读取芯片ID =====================
  uint16_t chip_id = W25Qxx_Read_ID();
  uint32_t jedec_id = W25Qxx_Read_JEDEC_ID();
  printf("Chip ID: 0x%04X\r\n", chip_id);
  printf("JEDEC ID: 0x%06lX\r\n", jedec_id);
  if(chip_id == W25Q64)
    printf("Chip Model: W25Q64JV, 8MB Capacity\r\n");
  else
    printf("Chip Not Recognized!\r\n");

  // ===================== 2. 扇区擦除 =====================
  printf("Start Erase Sector 0...\r\n");
  W25Qxx_Sector_Erase(0); // 擦除第0扇区,地址0x00000000
  printf("Sector Erase Complete!\r\n");

  // ===================== 3. 写入数据 =====================
  printf("Write Data: %s\r\n", write_buf);
  W25Qxx_Write_Data(0x00000000, write_buf, sizeof(write_buf));
  printf("Write Complete!\r\n");

  // ===================== 4. 读取数据 =====================
  W25Qxx_Read_Data(0x00000000, read_buf, sizeof(read_buf));
  printf("Read Data: %s\r\n", read_buf);

  // ===================== 5. 数据校验 =====================
  if(memcmp(write_buf, read_buf, sizeof(write_buf)) == 0)
    printf("Data Read/Write Test Success!\r\n");
  else
    printf("Data Read/Write Test Failed!\r\n");

  // ===================== 6. 结构体掉电存储 =====================
  printf("===== Device Param Storage Test =====\r\n");
  // 读取开机次数,累加后写入
  W25Qxx_Read_Data(0x00001000, (uint8_t *)&read_param, sizeof(Device_Param_Typedef));
  dev_param.boot_count = read_param.boot_count + 1;
  printf("Boot Count: %ld\r\n", dev_param.boot_count);
  // 擦除扇区1,写入新参数
  W25Qxx_Sector_Erase(1);
  W25Qxx_Write_Data(0x00001000, (uint8_t *)&dev_param, sizeof(Device_Param_Typedef));
  printf("Device Param Write Complete!\r\n");
  /* USER CODE END 2 */

  while (1)
  {
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
    HAL_Delay(1000);
  }
  /* USER CODE END 3 */
}

3.5 编译、烧录与效果验证

  1. 点击Keil顶部Build按钮(F7快捷键)编译工程,底部提示0 Error(s), 0 Warning(s),说明编译成功。
  2. 仿真器配置:点击魔法棒图标(Alt+F7快捷键),Debug选项卡选择ST-Link Debugger,Settings中确认Port为SW,能识别到芯片;Flash Download选项卡勾选Reset and Run,点击OK保存。
  3. 硬件接线核心注意事项:
    • W25Qxx芯片必须接3.3V供电,严禁接5V,否则会烧毁芯片;
    • WP、HOLD引脚必须接3.3V,禁止悬空,否则会出现通信异常;
    • 所有外设GND必须与核心板GND可靠共地,保证电平基准一致;
    • SPI接线严格对应:CS→PA4、SCK→PA5、MOSI→PA7、MISO→PA6,严禁接反。
  4. 效果验证:
    • 打开串口助手,配置115200波特率、8位数据、1位停止位、无校验,打开串口;
    • 核心板上电后,串口打印芯片ID、JEDEC ID,识别W25Q64芯片;
    • 完成扇区擦除、数据写入、读取校验,打印写入与读取的数据,测试成功;
    • 打印开机次数,每次重启核心板,开机次数自动累加,实现掉电不丢失存储;
    • 断电重启核心板,重新读取写入的数据,与写入内容完全一致,验证掉电存储功能正常。

四、保姆式排错指南

异常现象/报错信息 核心根因 一步到位解决方法
SPI通信无响应,读取芯片ID全是0xFF 1. 接线错误,SCK/MOSI/MISO接反;2. CS引脚未拉低,未选中芯片;3. 工作模式不匹配,CPOL/CPHA与从机不一致;4. 芯片未供电、未共地,电平基准错误;5. SPI外设未使能,时钟配置错误 1. 严格核对接线:SCK→SCK、MOSI→MOSI、MISO→MISO,严禁接反;2. 通信前必须拉低CS引脚,全程保持低电平,通信结束后拉高;3. 查阅从机手册,严格匹配CPOL/CPHA参数,模式0是绝大多数外设的默认配置;4. 确认芯片3.3V供电正常,GND与核心板可靠共地;5. 检查CubeMX配置,确认SPI外设已使能,时钟分频配置正确
Flash写入数据后,读取的数据与写入不一致,只有0能正确写入 1. 写入数据前未执行擦除操作,Flash只能将1改写为0,不能将0改写为1;2. 写入后未等待忙状态结束,就发起读操作;3. 未开启写使能,写入操作未生效;4. 写入数据跨页,地址回卷覆盖 1. Flash写入前必须先擦除对应扇区,擦除后所有位为1,才能写入数据;2. 每次写入/擦除操作后,必须调用W25Qxx_Wait_Busy()等待芯片操作完成;3. 写入前必须调用W25Qxx_Write_Enable()开启写使能;4. 页写入最多256字节,不能跨页,跨页需分多次写入
SPI通信数据错位,高低位颠倒,接收数据异常 1. 位序配置错误,配置为LSB低位在前,而从机要求MSB高位在前;2. 时钟分频过高,超过从机最大支持频率;3. 总线走线过长,信号畸变,无终端匹配电阻;4. 通信期间CS引脚电平波动,被意外拉高 1. 配置SPI为MSB First高位在前,与从机位序一致;2. 降低SPI波特率分频,如改为8分频/16分频,匹配从机最大时钟频率;3. 缩短总线走线长度,SCK/MOSI/MISO线串联33Ω终端匹配电阻;4. 确保CS引脚配置为推挽输出,通信全程保持稳定低电平,无干扰
DMA模式SPI传输卡死,程序进入HardFault 1. 传输数组定义为局部变量,栈空间溢出;2. 传输长度与数组长度不匹配,内存越界;3. CS引脚控制错误,DMA传输期间未拉低CS;4. DMA传输完成后未等待SPI总线空闲,提前拉高CS 1. 传输用的数组必须定义为全局变量,禁止使用函数内的局部数组;2. 严格核对传输长度,不得超过数组的最大长度,避免内存越界;3. DMA传输前必须拉低CS引脚,传输完成回调中再拉高;4. DMA传输完成后,等待SPI的BSY标志位清零,确认总线空闲后再拉高CS
多从机SPI总线,只有一个设备能正常通信,其他设备无响应 1. 多个从机共用同一个CS引脚,同时被选中,总线冲突;2. 未使用的从机CS引脚未拉高,一直处于选中状态,干扰总线;3. 多个从机的MISO引脚同时输出,总线电平冲突;4. SPI工作模式与部分从机不匹配 1. 每个从机必须使用独立的CS引脚,同一时间仅拉低一个从机的CS引脚;2. 所有从机的CS引脚默认拉高,仅通信时拉低对应从机的CS;3. 从机MISO引脚为开漏输出,需外接上拉电阻,避免多设备输出冲突;4. 确保所有从机的SPI工作模式一致,或切换模式后再与对应从机通信
SPI通信偶尔丢包,数据时对时错,稳定性差 1. 未接共地,电平基准波动;2. 电源噪声大,无滤波电容;3. SCK时钟频率过高,信号完整性差;4. 未处理SPI状态标志位,读写时序错误 1. 所有从机与主机必须可靠共地,避免电平漂移;2. 芯片VCC引脚就近接100nF滤波电容到GND,滤除电源噪声;3. 降低SPI时钟频率,提升通信稳定性;4. 严格遵循SPI读写时序,等待TXE/RXNE标志位后再读写数据寄存器

五、我的踩坑记录

  1. 踩坑现象 :第一次调试W25Qxx,读取芯片ID全是0xFF,SPI通信完全无响应,换了软件模拟SPI就正常,硬件SPI一直不行。
    底层原因 :我在CubeMX里配置SPI时,CPHA选成了2 Edge,CPOL是Low,相当于模式1,而W25Qxx默认支持模式0和模式3,模式不匹配,从机无法正确采样数据,自然不会响应。同时我忘记了SPI通信前必须拉低CS引脚,直接发起了传输,从机根本没被选中,51单片机软件模拟时我是先拉低CS的,换了硬件SPI反而忘了这个核心步骤。
    最终解决方案:将SPI配置改为模式0(CPOL=Low,CPHA=1 Edge),严格匹配W25Qxx的时序要求,同时在每次SPI通信前拉低CS引脚,通信结束后拉高,重新烧录后,芯片ID读取正常,通信完全稳定。

  2. 踩坑现象 :向Flash写入数据后,读取出来的数据大部分是错的,只有0的位置是对的,1的位置全变成了0,写入0x55,读出来是0x44。
    底层原因 :我完全忽略了Flash的写入特性:Flash的存储单元只能将1改写为0,不能将0改写为1,写入前必须先擦除对应扇区,擦除后所有位都是1,才能正常写入数据。我直接向未擦除的扇区写入数据,原本是0的位无法改写为1,导致写入的数据完全错乱。51单片机用的AT24C02 EEPROM是字节改写,无需擦除,我照搬了这个逻辑,完全没考虑Flash的擦除特性,踩了大坑。
    最终解决方案:在写入数据前,先擦除对应地址的扇区,确保所有位为1后再执行写入操作,修改后写入与读取的数据完全一致,无错乱。

  3. 踩坑现象 :页写入数据时,只要写入长度超过256字节,前256字节正常,后面的数据全是错的,甚至覆盖了前面的内容。
    底层原因 :W25Qxx的页编程最大只能写入256字节,且不能跨页,一旦写入的地址超过当前页的边界,地址会自动回卷到当前页的起始地址,导致后面的数据覆盖了当前页的开头内容。我误以为连续写入可以自动跨页,直接写入了512字节数据,导致数据回卷覆盖,出现错乱。
    最终解决方案:重写写入函数,增加跨页判断,计算当前页的剩余字节数,分多次页写入,每次写入不超过当前页的剩余字节数,修改后任意长度的写入都完全正常,无跨页覆盖问题。

  4. 踩坑现象 :SPI高速分频下(2分频36MHz),通信偶尔出错,数据时对时错,降低到8分频后就完全正常。
    底层原因 :我用的杜邦线长度超过20cm,36MHz的高速信号在长线上出现了严重的信号畸变,上升沿和下降沿变缓,不满足SPI的时序要求,导致从机采样错误。同时芯片VCC引脚没有接滤波电容,电源噪声耦合到了信号线上,加剧了信号畸变。
    最终解决方案:缩短杜邦线长度到10cm以内,在W25Qxx的VCC引脚就近接了一个100nF的陶瓷滤波电容到GND,同时在SCK、MOSI线上串联了33Ω的终端匹配电阻,修改后即使2分频36MHz,通信也完全稳定,无丢包、无错乱。


六、课后小练习(附完整标准答案)

6.1 基础巩固练习

练习1:实现SPI Flash在线检测函数,自动识别W25Qxx芯片型号与容量,串口打印。

标准答案

c 复制代码
// W25Qxx芯片型号列表
typedef struct {
  uint16_t id;
  char *name;
  uint32_t capacity; // 单位KB
} W25Qxx_Model_Typedef;

W25Qxx_Model_Typedef w25qxx_model_list[] = {
  {0xEF14, "W25Q80", 1024},
  {0xEF15, "W25Q16", 2048},
  {0xEF16, "W25Q32", 4096},
  {0xEF17, "W25Q64", 8192},
  {0xEF18, "W25Q128", 16384},
  {0, NULL, 0}
};

// 芯片检测函数
void W25Qxx_Detect_Chip(void)
{
  uint16_t chip_id = W25Qxx_Read_ID();
  printf("Chip ID: 0x%04X\r\n", chip_id);
  for(uint8_t i=0; w25qxx_model_list[i].name != NULL; i++)
  {
    if(w25qxx_model_list[i].id == chip_id)
    {
      printf("Chip Model: %s\r\n", w25qxx_model_list[i].name);
      printf("Capacity: %ld KB / %ld MB\r\n", w25qxx_model_list[i].capacity, w25qxx_model_list[i].capacity/1024);
      return;
    }
  }
  printf("Unknown Chip Model!\r\n");
}

// 主函数调用
/* USER CODE BEGIN 2 */
W25Qxx_Detect_Chip();
/* USER CODE END 2 */
练习2:基于SPI Flash实现设备参数掉电存储,支持参数的读取、写入、恢复出厂设置功能。

标准答案

c 复制代码
/* USER CODE BEGIN PV */
#define PARAM_ADDR 0x00001000 // 参数存储地址
#define PARAM_SECTOR 1         // 参数存储扇区

// 设备参数结构体
typedef struct {
  uint8_t  valid_flag;    // 有效标志,0xAA表示有效
  float    kp;            // PID参数
  float    ki;
  float    kd;
  uint16_t sample_rate;   // 采样率
  char     device_sn[16]; // 设备序列号
} Device_Config_Typedef;

Device_Config_Typedef dev_config = {0};
// 出厂默认参数
const Device_Config_Typedef default_config = {
  .valid_flag = 0xAA,
  .kp = 2.5f,
  .ki = 0.8f,
  .kd = 0.2f,
  .sample_rate = 100,
  .device_sn = "STM32_001"
};

// 读取参数
uint8_t Dev_Config_Read(void)
{
  W25Qxx_Read_Data(PARAM_ADDR, (uint8_t *)&dev_config, sizeof(Device_Config_Typedef));
  if(dev_config.valid_flag != 0xAA)
  {
    printf("Config Invalid, Restore Default!\r\n");
    memcpy(&dev_config, &default_config, sizeof(Device_Config_Typedef));
    return 1;
  }
  printf("Config Read Success!\r\n");
  return 0;
}

// 写入参数
void Dev_Config_Write(void)
{
  dev_config.valid_flag = 0xAA;
  W25Qxx_Sector_Erase(PARAM_SECTOR);
  W25Qxx_Write_Data(PARAM_ADDR, (uint8_t *)&dev_config, sizeof(Device_Config_Typedef));
  printf("Config Write Success!\r\n");
}

// 恢复出厂设置
void Dev_Config_Restore_Default(void)
{
  memcpy(&dev_config, &default_config, sizeof(Device_Config_Typedef));
  Dev_Config_Write();
  printf("Restore Default Config Success!\r\n");
}
/* USER CODE END PV */
练习3:实现SPI中断模式批量读取Flash数据,非阻塞式传输,传输完成后在回调函数中进行数据校验。

标准答案

c 复制代码
/* USER CODE BEGIN PV */
#define READ_BUF_SIZE 512
uint8_t it_read_buf[READ_BUF_SIZE] = {0};
uint8_t it_read_done = 0;
/* USER CODE END PV */

/* USER CODE BEGIN 0 */
// SPI接收完成回调
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi)
{
  if(hspi->Instance == SPI1)
  {
    it_read_done = 1;
    W25QXX_CS_HIGH(); // 接收完成后拉高CS
  }
}
/* USER CODE END 0 */

// 中断模式读数据函数
void W25Qxx_Read_Data_IT(uint32_t addr, uint8_t *buf, uint16_t len)
{
  uint8_t tx_buf[4] = {W25QXX_CMD_READ_DATA, (addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF};
  it_read_done = 0;
  W25QXX_CS_LOW();
  // 发送读命令
  HAL_SPI_Transmit(&hspi1, tx_buf, 4, 100);
  // 中断模式接收数据
  HAL_SPI_Receive_IT(&hspi1, buf, len);
}

// 主函数调用
/* USER CODE BEGIN 2 */
W25Qxx_Read_Data_IT(0x00000000, it_read_buf, READ_BUF_SIZE);
/* USER CODE END 2 */

/* USER CODE BEGIN WHILE */
while (1)
{
  if(it_read_done == 1)
  {
    printf("IT Read Complete!\r\n");
    // 数据校验
    if(memcmp(it_read_buf, write_buf, READ_BUF_SIZE) == 0)
      printf("IT Read Test Success!\r\n");
    else
      printf("IT Read Test Failed!\r\n");
    it_read_done = 0;
  }
  HAL_Delay(10);
}
/* USER CODE END WHILE */

6.2 进阶实战练习

练习1:基于SPI+DMA实现Flash高速批量读写,双缓冲模式实现无缝连续数据采集与存储。

标准答案

c 复制代码
/* USER CODE BEGIN PV */
#define BUF_SIZE 4096
uint8_t dma_tx_buf[BUF_SIZE] = {0};
uint8_t dma_rx_buf[BUF_SIZE] = {0};
uint8_t dma_done = 0;
uint32_t write_addr = 0;
/* USER CODE END PV */

/* USER CODE BEGIN 0 */
// DMA收发完成回调
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
{
  if(hspi->Instance == SPI1)
  {
    dma_done = 1;
    W25QXX_CS_HIGH();
  }
}
/* USER CODE END 0 */

// DMA模式读数据
void W25Qxx_Read_Data_DMA(uint32_t addr, uint8_t *buf, uint16_t len)
{
  uint8_t tx_buf[4] = {W25QXX_CMD_FAST_READ, (addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF, 0x00};
  dma_done = 0;
  W25QXX_CS_LOW();
  HAL_SPI_Transmit(&hspi1, tx_buf, 5, 100); // 快速读命令+1字节哑周期
  HAL_SPI_Receive_DMA(&hspi1, buf, len);
}

// 主循环双缓冲存储
/* USER CODE BEGIN WHILE */
while (1)
{
  // 模拟采集数据,填充发送缓存
  for(uint32_t i=0; i<BUF_SIZE; i++)
  {
    dma_tx_buf[i] = (uint8_t)(i + HAL_GetTick() % 256);
  }
  // 擦除对应扇区
  W25Qxx_Sector_Erase(write_addr / 4096);
  // 写入数据
  W25Qxx_Write_Data(write_addr, dma_tx_buf, BUF_SIZE);
  printf("Write Data To Address: 0x%08lX\r\n", write_addr);
  // DMA读取数据
  W25Qxx_Read_Data_DMA(write_addr, dma_rx_buf, BUF_SIZE);
  // 等待DMA完成
  while(!dma_done);
  // 数据校验
  if(memcmp(dma_tx_buf, dma_rx_buf, BUF_SIZE) == 0)
    printf("DMA Read/Write Success!\r\n");
  else
    printf("DMA Read/Write Failed!\r\n");
  // 地址递增
  write_addr += BUF_SIZE;
  if(write_addr >= 8*1024*1024) write_addr = 0; // 8MB容量循环
  HAL_Delay(1000);
}
/* USER CODE END WHILE */
练习2:实现W25Qxx Flash的循环日志存储系统,支持日志写入、读取、清空,掉电不丢失。

标准答案

c 复制代码
/* USER CODE BEGIN PV */
#define LOG_START_ADDR 0x00100000 // 日志存储起始地址,1MB偏移
#define LOG_SECTOR_SIZE 4096
#define LOG_ENTRY_SIZE 64
#define LOG_MAX_ENTRY 1024 // 最多1024条日志
#define LOG_MAGIC 0x5A5A    // 日志有效标志

// 日志条目结构体
typedef struct {
  uint16_t magic;       // 有效标志
  uint32_t timestamp;   // 时间戳
  uint8_t  level;       // 日志等级
  char     content[56]; // 日志内容
} Log_Entry_Typedef;

Log_Entry_Typedef log_entry = {0};
uint32_t log_count = 0;
/* USER CODE END PV */

// 查找下一个空的日志条目地址
uint32_t Log_Find_Empty_Entry(void)
{
  Log_Entry_Typedef temp_entry;
  for(uint32_t i=0; i<LOG_MAX_ENTRY; i++)
  {
    uint32_t addr = LOG_START_ADDR + i * LOG_ENTRY_SIZE;
    W25Qxx_Read_Data(addr, (uint8_t *)&temp_entry, LOG_ENTRY_SIZE);
    if(temp_entry.magic != LOG_MAGIC)
    {
      log_count = i;
      return addr;
    }
  }
  return 0xFFFFFFFF; // 日志满
}

// 写入一条日志
uint8_t Log_Write(uint8_t level, char *content)
{
  uint32_t addr = Log_Find_Empty_Entry();
  if(addr == 0xFFFFFFFF) return 1; // 日志满
  // 填充日志条目
  log_entry.magic = LOG_MAGIC;
  log_entry.timestamp = HAL_GetTick() / 1000;
  log_entry.level = level;
  strncpy(log_entry.content, content, 55);
  // 写入日志
  W25Qxx_Write_Data(addr, (uint8_t *)&log_entry, LOG_ENTRY_SIZE);
  log_count++;
  printf("Log Write Success, Total Log: %ld\r\n", log_count);
  return 0;
}

// 读取指定序号的日志
uint8_t Log_Read(uint32_t index, Log_Entry_Typedef *entry)
{
  if(index >= log_count) return 1;
  uint32_t addr = LOG_START_ADDR + index * LOG_ENTRY_SIZE;
  W25Qxx_Read_Data(addr, (uint8_t *)entry, LOG_ENTRY_SIZE);
  if(entry->magic != LOG_MAGIC) return 1;
  return 0;
}

// 清空所有日志
void Log_Clear_All(void)
{
  printf("Start Clear All Log...\r\n");
  // 擦除日志区域所有扇区
  uint32_t sector_start = LOG_START_ADDR / LOG_SECTOR_SIZE;
  uint32_t sector_end = (LOG_START_ADDR + LOG_MAX_ENTRY * LOG_ENTRY_SIZE) / LOG_SECTOR_SIZE;
  for(uint32_t i=sector_start; i<=sector_end; i++)
  {
    W25Qxx_Sector_Erase(i);
  }
  log_count = 0;
  printf("Log Clear Complete!\r\n");
}

七、核心知识点速记

  1. SPI是四线全双工同步串行总线,SCK为同步时钟,MOSI主发从收,MISO主收从发,CS片选,通信前必须拉低CS,结束后拉高。
  2. SPI 4种工作模式由CPOL(时钟极性)和CPHA(时钟相位)决定,工业最常用模式0(CPOL=0、CPHA=0),主机模式必须与从机完全一致,否则通信异常。
  3. SPI全双工通信的核心是同步移位寄存器,每一个时钟周期,主机与从机同时发送和接收1位数据,8个时钟周期完成1字节的收发交换。
  4. W25Qxx Flash写入前必须先擦除,最小擦除单位是4KB扇区,页编程最大256字节,不能跨页,只能将1改写为0,不能将0改写为1。
  5. HAL库SPI核心API:HAL_SPI_TransmitReceive()全双工阻塞收发,工业开发最常用;高速大批量数据传输使用DMA模式,实现零CPU占用。
  6. SPI多从机架构必须使用独立CS引脚,同一时间仅拉低一个从机的CS,避免总线冲突,未使用的从机CS必须保持高电平。
  7. SPI最高速率:SPI1挂载APB2总线,最高36MHz(72MHz主频2分频),SPI2/SPI3挂载APB1总线,最高18MHz。
  8. SPI无硬件应答机制,通信可靠性由软件协议保证,需通过芯片ID、校验和等方式验证通信的正确性。
  9. SPI通信异常头号排查顺序:CS引脚时序→CPOL/CPHA模式匹配→接线与共地→时钟频率→电源滤波。

八、本章小结

本章我们深入拆解了SPI串行通信协议的底层时序与4种工作模式,对比了51单片机软件模拟与STM32硬件SPI的核心差异,掌握了硬件SPI的寄存器级原理与HAL库封装逻辑,完成了W25Qxx系列Flash芯片的全功能驱动开发,实现了大容量数据掉电存储、循环日志系统等工业级实战,解决了通信无响应、数据错乱、读写失败等高频问题。SPI是高速外设的核心通信总线,下一章我们将学习CAN总线通信,掌握工业现场最常用的差分串行总线的底层原理与双机通信实战,适配工业控制场景的高可靠性通信需求。

相关推荐
悠哉悠哉愿意2 小时前
【单片机复习笔记】第十六届省赛复盘
笔记·单片机·嵌入式硬件
2401_878530212 小时前
自定义内存布局控制
开发语言·c++·算法
wjs20242 小时前
SQLite 子查询
开发语言
AndrewMe82112 小时前
detailed-docx:一个能保住格式的 Word 文档操作库
开发语言·python·word
IT方大同2 小时前
RT_thread(RTOS实时操作系统)线程的创建与切换
c语言·开发语言·嵌入式硬件
智算菩萨2 小时前
【OpenGL】6 真实感光照渲染实战:Phong模型、材质系统与PBR基础
开发语言·python·游戏引擎·游戏程序·pygame·材质·opengl
jinanwuhuaguo2 小时前
OpenClaw深度沟通渠道-全景深度解构
大数据·开发语言·人工智能·openclaw
是翔仔呐2 小时前
第14章 CAN总线通信全解:底层原理、帧结构与双机CAN通信实战
c语言·开发语言·stm32·单片机·嵌入式硬件·学习·gitee
客卿1232 小时前
用两个栈实现队列
android·java·开发语言