
目录
一、SPI简介
SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线。同步,全双工。支持总线挂载多设备(一主多从)。
四根通信线:
- SCK(Serial Clock)串行时钟线;
- MOSI(Master Output Slave Input)主机输出从机输入;
- MISO(Master Input Slave Output)主机输入从机输出;
- SS(Slave Select)从机选择(若是有多个从机,有几个从机就有几条SS线,可见硬件电路中的连接图)。

- 所有SPI设备的SCK、MOSI、MISO分别连在一起;
- 主机另外引出多条SS控制线,分别接到各从机的SS引脚;
- 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入。
每个从设备都有独立的这一条SS信号线,本信号线独占主机的一个引脚,即有多少个从设备,就有多少条片选信号线。12C协议中通过设备地址来寻址、选中总线上的某个设备并与其进行通讯:而SPI协议中没有设备地址,它使用SS信号线来寻址,当主机要选择从设备时,把该从设备的SS信号线设置为低电平,该从设备即被选中,即片选有效,接着主机开始与被选中的从设备进行SPI通讯。所以SPI通讯以SS线置低电平为开始信号,以SS线被拉高作为结束信号。
二、SPI协议层
基本时序图,这里之列举出了一种,对于①和⑥好理解就是开始和结束,但是对于②③④⑤的触发和采样起始SPI没有硬性的要求必须是上升沿触发还是下降沿触发,或者是上升沿采样还是下降沿采样,这个需要我们自己进行规定,其中需要引出两个概念:

时钟极性(CPOL):是指SPI设备处于空闲状态时,SCK信号线的电平信号(即SPI通讯开始前,NSS线为高电平的SCK的状态)。CPOL=0时,SCK在空闲状态为低电平;COPL=1时,SCK在空闲状态为高电平。
时钟相位(CPHA):是指数据的采样的时刻,当CPHA=0时,MOSI或者MISO数据线上的信号将会在SCK时钟线的“奇数边沿”被采样;当CPHA=1时,数据线将会在SCK的“偶数边沿”采样。


由于CPOL有0和1两种状态,CPHA有0和1两种状态,那么我们组合一下就是SPI的四种工作模式:
| SPI模式 | CPOL | CPHA | 空闲时SCK时钟 | 采样时刻 |
| 模式0 | 0 | 0 | 低电平 | 奇数边沿 |
| 模式1 | 0 | 1 | 低电平 | 偶数边沿 |
| 模式2 | 1 | 0 | 高电平 | 奇数边沿 |
| 模式3 | 1 | 1 | 高电平 | 偶数边沿 |
2.1 起始条件
对应的序号①,SS从高电平切换到低电平:

2.2 终止条件
对应序号⑥,SS从低电平切换到高电平:

2.3 模式0
CPOL=0:空闲状态时,SCK为低电平
CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据

2.4 模式1
CPOL=0:空闲状态时,SCK为低电平
CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据

2.5 模式2
CPOL=1:空闲状态时,SCK为高电平
CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据

2.6 模式3
CPOL=1:空闲状态时,SCK为高电平
CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据

三、SPI外设
STM32的SPI外设可用作通讯的主机及从机,支持最高的SCK时钟频率为fpaa/2 (STM32F10x型号的芯片默认fpclkt为72MHz,fpclk2为36MHz),完全支持SPI协议的4种模式,数据帧长度可设置为8位或16位,可设置数据MSB先行或LSB先行。它还支持双线全双工、双线单向以及单线模式。

3.1 通讯引脚
STM32芯片有多个SPI外设,它们的SPI通讯信号引出到不同的GPIO引脚上,使用时必须配置到这些指定的引脚:

对于具体芯片引脚所在位置根据所使用的芯片手册进行查看(这里以ZET6为例):
| 引脚 | SPI编号 | 功能 | ||
| SPI1 | SPI2 | SPI3 | ||
| NSS | PA4 | PB12 | PA15下载口的TDI | 从设备选择。这是一个可选的引脚,用来选择主/从设备。它的功能是用来作为“片选引脚”,让主设备可以单独地与特定从设备通讯,避免数据线上的冲突。从设备的NSS引脚可以由主设备的一个标准I/O引脚来驱动。一旦被使能(SSOE位),NSS引脚也可以作为输出引脚,并在SPI处于主模式时拉低;此时,所有的SPI设备,如果它们的NSS引脚连接到主设备的NSS引脚,则会检测到低电平,如果它们被设置为NSS硬件模式,就会自动进入从设备状态。当配置为主设备、NSS配置为输入引脚(MSTR=1,SSOE=0)时,如果NSS被拉低,则这个SPI设备进入主模式失败状态:即MSTR位被自动清除,此设备进入从模式 |
| CLK | PA5 | PB13 | PB3下载口的TDO | 串口时钟,作为主设备的输出,从设备的输入 |
| MISO | PA6 | PB14 | PB4下载口的NTRST | 主设备输入/从设备输出引脚。该引脚在从模式下发送数据,在主模式下接收数据。 |
| MOSI | PA7 | PB15 | PB5 | 主设备输出/从设备输入引脚。该引脚在主模式下发送数据,在从模式下接收数据。 |
其中SPI1是APB2上的设备,最高通信速率达36Mbtis/s,SPI2、SPI3是APB1上的设备,最高通信速率为18Mbits/s。除了通讯速率,在其它功能上没有差异:

3.2 时钟控制逻辑
SCK线的时钟信号,由波特率发生器根据“控制寄存器CR1”中的BR[0:2]位控制,该位是对fpclk时钟的分频因子,对fpclk的分频结果就是SCK引脚的输出时钟频率(图在数据手册23.5.1):

举个例子,对于SPI1来说,其挂载在APB2总线上,其时钟频率为72MHz,假如我们将DR位写为000,那么SCK时钟信号就会72MHz/2=36MHz,我们可以通过BR位来动态调整时钟频率。
3.3 数据控制逻辑
SPI的MOSI及MISO都连接到数据移位寄存器上,数据移位寄存器的数据来源来源于接收缓冲区及发送缓冲区:
- 通过写SPI的“数据寄存器DR”把数据填充到发送缓冲区中。
- 通过读“数据寄存器DR”,可以获取接收缓冲区中的内容。

3.4 整体控制逻辑
整体控制逻辑负责协调整个SPI外设,控制逻辑的工作模式根据“控制寄存器(CR1/CR2)”的参数而改变,基本的控制参数包括前面提到的SPI模式、波特率、LSB先行、主从模式、单双向模式等等,具体可以通过数据手册进行查看:

3.5 通讯过程
简单来说想要发送数据,首先将数据放到发送缓冲区,然后发送;想要接收数据,将数据放到接收缓冲区,然后接收:

四、SPI相关库函数
这里只是介绍一些常用的,更多函数可以产看固件库手册:
链接: https://pan.baidu.com/s/16L6NxIPSKreqdnLR5kH0Ow?pwd=txjy
提取码: txjy
4.1 初始化结构体
typedef struct
{
uint16_t SPI_Direction; /*!< 指定SPI单向或双向数据模式。
该参数可以是 @ref SPI_data_direction 的值 */
uint16_t SPI_Mode; /*!< 指定SPI工作模式。
该参数可以是 @ref SPI_mode 的值 */
uint16_t SPI_DataSize; /*!< 指定SPI数据大小。
该参数可以是 @ref SPI_data_size 的值 */
uint16_t SPI_CPOL; /*!< 指定串行时钟稳态。
该参数可以是 @ref SPI_Clock_Polarity 的值 */
uint16_t SPI_CPHA; /*!< 指定位捕获的时钟有效边沿。
该参数可以是 @ref SPI_Clock_Phase 的值 */
uint16_t SPI_NSS; /*!< 指定NSS信号是由硬件(NSS引脚)管理
还是通过使用SSI位的软件管理。
该参数可以是 @ref SPI_Slave_Select_management 的值 */
uint16_t SPI_BaudRatePrescaler; /*!< 指定用于配置发送和接收SCK时钟的
波特率预分频值。
该参数可以是 @ref SPI_BaudRate_Prescaler 的值。
@注意 通信时钟源自主时钟。从设备时钟不需要设置。*/
uint16_t SPI_FirstBit; /*!< 指定数据传输从最高有效位(MSB)还是最低有效位(LSB)开始。
该参数可以是 @ref SPI_MSB_LSB_transmission 的值 */
uint16_t SPI_CRCPolynomial; /*!< 指定用于CRC计算的多项式。*/
}SPI_InitTypeDef;

4.1.1 SPI_Direction
指定SPI的数据传输方式:
/** @defgroup SPI_data_direction
* @{
*/
#define SPI_Direction_2Lines_FullDuplex ((uint16_t)0x0000) /*!< 双线全双工模式 */
#define SPI_Direction_2Lines_RxOnly ((uint16_t)0x0400) /*!< 双线只接收模式 */
#define SPI_Direction_1Line_Rx ((uint16_t)0x8000) /*!< 单线只接收模式 */
#define SPI_Direction_1Line_Tx ((uint16_t)0xC000) /*!< 单线只发送模式 */
#define IS_SPI_DIRECTION_MODE(MODE) (((MODE) == SPI_Direction_2Lines_FullDuplex) || \
((MODE) == SPI_Direction_2Lines_RxOnly) || \
((MODE) == SPI_Direction_1Line_Rx) || \
((MODE) == SPI_Direction_1Line_Tx)) /*!< 检查SPI方向模式是否有效 */
4.1.2 SPI_Mode
指定SPI的工作模式:
/** @defgroup SPI_mode
* @{
*/
#define SPI_Mode_Master ((uint16_t)0x0104) /*!< 主模式 */
#define SPI_Mode_Slave ((uint16_t)0x0000) /*!< 从模式 */
#define IS_SPI_MODE(MODE) (((MODE) == SPI_Mode_Master) || \
((MODE) == SPI_Mode_Slave)) /*!< 检查SPI主从模式是否有效 */
本成员设置SPI工作在主机模式(SPI_Mode_Master)或从机模式(SPI_Mode_Slave ),这两个模式的最大区别为SPI的SCK信号线的时序,SCK的时序是由通讯中的主机产生的。若被配置为从机模式,STM32的SPI外设将接受外来的SCK信号。
4.1.3 SPI_DataSize
指定SPI数据大小:
/** @defgroup SPI_data_size
* @{
*/
#define SPI_DataSize_16b ((uint16_t)0x0800) /*!< 16位数据帧 */
#define SPI_DataSize_8b ((uint16_t)0x0000) /*!< 8位数据帧 */
#define IS_SPI_DATASIZE(DATASIZE) (((DATASIZE) == SPI_DataSize_16b) || \
((DATASIZE) == SPI_DataSize_8b)) /*!< 检查SPI数据大小是否有效 */
4.1.4 SPI_CPOL
指定串行时钟稳态:
/** @defgroup SPI_Clock_Polarity
* @{
*/
#define SPI_CPOL_Low ((uint16_t)0x0000) /*!< 时钟空闲状态为低电平 */
#define SPI_CPOL_High ((uint16_t)0x0002) /*!< 时钟空闲状态为高电平 */
#define IS_SPI_CPOL(CPOL) (((CPOL) == SPI_CPOL_Low) || \
((CPOL) == SPI_CPOL_High)) /*!< 检查SPI时钟极性是否有效 */
4.1.5 SPI_CPHA
指定位捕获的时钟有效边沿:
/** @defgroup SPI_Clock_Phase
* @{
*/
#define SPI_CPHA_1Edge ((uint16_t)0x0000) /*!< 在第一个时钟边沿进行数据采样 */
#define SPI_CPHA_2Edge ((uint16_t)0x0001) /*!< 在第二个时钟边沿进行数据采样 */
#define IS_SPI_CPHA(CPHA) (((CPHA) == SPI_CPHA_1Edge) || \
((CPHA) == SPI_CPHA_2Edge)) /*!< 检查SPI时钟相位是否有效 */
4.1.6 SPI_NSS
指定NSS信号是由硬件(NSS引脚)管理,还是通过使用SSI位的软件管理:
/** @defgroup SPI_Slave_Select_management
* @{
*/
#define SPI_NSS_Soft ((uint16_t)0x0200) /*!< 软件NSS管理 */
#define SPI_NSS_Hard ((uint16_t)0x0000) /*!< 硬件NSS管理 */
#define IS_SPI_NSS(NSS) (((NSS) == SPI_NSS_Soft) || \
((NSS) == SPI_NSS_Hard)) /*!< 检查SPI从设备选择管理方式是否有效 */
4.1.7 SPI_BaudRatePrescaler
指定用于配置发送和接收SCK时钟的波特率预分频值。通信时钟源自主时钟。从设备时钟不需要设置:
/** @defgroup SPI_BaudRate_Prescaler
* @{
*/
#define SPI_BaudRatePrescaler_2 ((uint16_t)0x0000) /*!< 波特率预分频值:2分频 */
#define SPI_BaudRatePrescaler_4 ((uint16_t)0x0008) /*!< 波特率预分频值:4分频 */
#define SPI_BaudRatePrescaler_8 ((uint16_t)0x0010) /*!< 波特率预分频值:8分频 */
#define SPI_BaudRatePrescaler_16 ((uint16_t)0x0018) /*!< 波特率预分频值:16分频 */
#define SPI_BaudRatePrescaler_32 ((uint16_t)0x0020) /*!< 波特率预分频值:32分频 */
#define SPI_BaudRatePrescaler_64 ((uint16_t)0x0028) /*!< 波特率预分频值:64分频 */
#define SPI_BaudRatePrescaler_128 ((uint16_t)0x0030) /*!< 波特率预分频值:128分频 */
#define SPI_BaudRatePrescaler_256 ((uint16_t)0x0038) /*!< 波特率预分频值:256分频 */
#define IS_SPI_BAUDRATE_PRESCALER(PRESCALER) (((PRESCALER) == SPI_BaudRatePrescaler_2) || \
((PRESCALER) == SPI_BaudRatePrescaler_4) || \
((PRESCALER) == SPI_BaudRatePrescaler_8) || \
((PRESCALER) == SPI_BaudRatePrescaler_16) || \
((PRESCALER) == SPI_BaudRatePrescaler_32) || \
((PRESCALer) == SPI_BaudRatePrescaler_64) || \
((PRESCALER) == SPI_BaudRatePrescaler_128) || \
((PRESCALER) == SPI_BaudRatePrescaler_256)) /*!< 检查SPI波特率预分频值是否有效 */
4.1.8 SPI_FirstBit
指定数据传输从最高有效位(MSB)还是最低有效位(LSB)开始:
/** @defgroup SPI_MSB_LSB_transmission
* @{
*/
#define SPI_FirstBit_MSB ((uint16_t)0x0000) /*!< 数据传输从最高有效位(MSB)开始 */
#define SPI_FirstBit_LSB ((uint16_t)0x0080) /*!< 数据传输从最低有效位(LSB)开始 */
#define IS_SPI_FIRST_BIT(BIT) (((BIT) == SPI_FirstBit_MSB) || \
((BIT) == SPI_FirstBit_LSB)) /*!< 检查SPI数据传输位顺序是否有效 */
4.1.9 SPI_CRCPolynomial
指定用于CRC计算的多项式,如果不使用CRC功能,数值写0即可。如果使用举个例子:
// 设置CRC多项式(例如使用CRC-8标准多项式)
SPI_InitStructure.SPI_CRCPolynomial = 0x07;
//常用多项式:0x07 (CRC-8)、0x1021 (CRC-16)、0x4C11DB7 (CRC-32)
对于CRC不熟悉的可以参考:
4.2 控制和状态管理函数
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);
功能:使能或禁用SPI外设
参数:NewState - ENABLE 或 DISABLE
4.3 中断和DMA控制函数
void SPI_I2S_ITConfig(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT, FunctionalState NewState);
功能:使能或禁用指定的SPI/I2S中断
参数:SPI_I2S_IT - 要使能的中断源
void SPI_I2S_DMACmd(SPI_TypeDef* SPIx, uint16_t SPI_I2S_DMAReq, FunctionalState NewState);
功能:使能或禁用SPI/I2S的DMA请求
参数:SPI_I2S_DMAReq - 要使能的DMA请求
4.4 数据收发函
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
功能:通过SPI/I2S外设发送数据
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);
功能:从SPI/I2S外设接收数据
返回值:接收到的数据
4.5 标志位和中断处理函数
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
//功能:检查指定的SPI/I2S标志位是否被设置
void SPI_I2S_ClearFlag(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
//功能:清除SPI/I2S的标志位
ITStatus SPI_I2S_GetITStatus(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);
//功能:检查指定的SPI/I2S中断是否发生
void SPI_I2S_ClearITPendingBit(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);
//功能:清除SPI/I2S的中断挂起位
五、SPI代码编写
5.1 引脚初始化
我们先找到这个表格(这里以ZET6为例):
| 引脚 | SPI编号 | 功能 | ||
| SPI1 | SPI2 | SPI3 | ||
| NSS | PA4 | PB12 | PA15下载口的TDI | 从设备选择。这是一个可选的引脚,用来选择主/从设备。它的功能是用来作为“片选引脚”,让主设备可以单独地与特定从设备通讯,避免数据线上的冲突。从设备的NSS引脚可以由主设备的一个标准I/O引脚来驱动。一旦被使能(SSOE位),NSS引脚也可以作为输出引脚,并在SPI处于主模式时拉低;此时,所有的SPI设备,如果它们的NSS引脚连接到主设备的NSS引脚,则会检测到低电平,如果它们被设置为NSS硬件模式,就会自动进入从设备状态。当配置为主设备、NSS配置为输入引脚(MSTR=1,SSOE=0)时,如果NSS被拉低,则这个SPI设备进入主模式失败状态:即MSTR位被自动清除,此设备进入从模式 |
| CLK | PA5 | PB13 | PB3下载口的TDO | 串口时钟,作为主设备的输出,从设备的输入 |
| MISO | PA6 | PB14 | PB4下载口的NTRST | 主设备输入/从设备输出引脚。该引脚在从模式下发送数据,在主模式下接收数据。 |
| MOSI | PA7 | PB15 | PB5 | 主设备输出/从设备输入引脚。该引脚在主模式下发送数据,在从模式下接收数据。 |
这里我使用SPI1的引脚,为了代码的维护下,我们对其进行宏定义操作:
#define SPIx SPI1
#define SPI_APBxClock_FUN RCC_APB2PeriphClockCmd
#define SPI_CLK RCC_APB2Periph_SPI1
#define SPI_GPIO_APBxClock_FUN RCC_APB2PeriphClockCmd
#define SPI_SCK_PORT GPIOA
#define SPI_SCK_PIN GPIO_Pin_5
#define SPI_MOSI_PORT GPIOA
#define SPI_MOSI_PIN GPIO_Pin_7
#define SPI_MISO_PORT GPIOA
#define SPI_MISO_PIN GPIO_Pin_6
#define SPI_GPIO_CLK RCC_APB2Periph_GPIOA
#define SPI_CS_PORT GPIOA
#define SPI_CS_PIN GPIO_Pin_4
//CS引脚配置
#define SPI_CS_HIGH GPIO_SetBits(SPI_CS_PORT,SPI_CS_PIN);
#define SPI_CS_LOW GPIO_ResetBits(SPI_CS_PORT,SPI_CS_PIN);
对引脚进行初始化函数:
static void SPI_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* 使能与SPI 有关的时钟 */
SPI_APBxClock_FUN ( SPI_CLK, ENABLE );
SPI_GPIO_APBxClock_FUN ( SPI_GPIO_CLK, ENABLE );
/* MISO MOSI SCK*/
GPIO_InitStructure.GPIO_Pin = SPI_SCK_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(SPI_SCK_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = SPI_MOSI_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(SPI_MOSI_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = SPI_MISO_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(SPI_MISO_PORT, &GPIO_InitStructure);
//初始化CS引脚,使用软件控制,所以直接设置成推挽输出
GPIO_InitStructure.GPIO_Pin = SPI_CS_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(SPI_CS_PORT, &GPIO_InitStructure);
SPI_CS_HIGH;
}
5.2 工作模式配置
这里我配置模式3的工作模式:

SCK空闲时为高电平,在第二个边沿进行采样:
//工作模式配置
static void SPI_Mode_Config(void)
{
SPI_InitTypeDef SPI_InitStructure;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2 ;// SPI 波特率预分频器,设置SPI时钟频率 = 系统时钟/2
//SPI 使用模式3
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;// SPI 时钟相位配置,选择第二个边沿进行数据采样
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;// SPI 时钟极性配置,选择高电平空闲
SPI_InitStructure.SPI_CRCPolynomial = 0;//不使用CRC功能
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;// SPI 数据帧格式,设置为8位数据
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;//双线全双工
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;// SPI 数据传输顺序,设置为高位先行(MSB First)
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;// SPI 工作模式,设置为主设备模式
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;// NSS(片选)信号管理,设置为软件控制
SPI_Init(SPIx,&SPI_InitStructure); //写入配置到寄存器
SPI_Cmd(SPIx,ENABLE);//使能SPI
}
5.3 起始条件
就是将SS引脚拉低:
void SPI_Start(void)
{
SPI_CS_LOW;//拉低SS,开始时序
}
5.4 终止条件
就是将SS引脚拉高:
void SPI_Stop(void)
{
SPI_CS_HIGH;//拉高SS,终止时序
}
5.5 发送并接收一个字节
主要用到这两个函数:
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);//功能:通过SPI/I2S外设发送数据
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);//功能:从SPI/I2S外设接收数据
为了防止程序一直卡死在等待发送或者等待接收,增加超时等待机制:
/*等待超时时间*/
#define SPIT_FLAG_TIMEOUT ((uint32_t)0x1000)
#define SPIT_LONG_TIMEOUT ((uint32_t)(10 * SPIT_FLAG_TIMEOUT))
static __IO uint32_t SPITimeout = SPIT_LONG_TIMEOUT;
完整函数:
//发送并接收一个字节
uint8_t SPI_FLASH_SendByte(uint8_t data)
{
SPITimeout = SPIT_FLAG_TIMEOUT;
//检查并等待至TX缓冲区为空
while(SPI_I2S_GetFlagStatus(SPIx,SPI_I2S_FLAG_TXE) == RESET)
{
if((SPITimeout--) == 0)
return SPI_TIMEOUT_UserCallback(0);
}
//程序执行到此处,TX缓冲区已空
SPI_I2S_SendData (SPIx,data);
SPITimeout = SPIT_FLAG_TIMEOUT;
//检查并等待至RX缓冲区为非空
while(SPI_I2S_GetFlagStatus(SPIx,SPI_I2S_FLAG_RXNE) == RESET)
{
if((SPITimeout--) == 0)
return SPI_TIMEOUT_UserCallback(1);
}
//程序执行到此处,说明数据发送完毕,并接收到一字字节
return SPI_I2S_ReceiveData(SPIx);
}
六、W25Q64
6.1 简介
W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器,常应用于数据存储、字库存储、固件程序存储等场景。

存储介质:Nor Flash(闪存)
时钟频率:80MHz / 160MHz (Dual SPI) / 320MHz (Quad SPI)

| 芯片型号 | 存储容量(24位地址) |
| W25Q40 | 4Mbit / 512KByte |
| W25Q80 | 8Mbit / 1MByte |
| W25Q16 | 16Mbit / 2MByte |
| W25Q32 | 32Mbit / 4MByte |
| W25Q64 | 64Mbit / 8MByte |
| W25Q128 | 128Mbit / 16MByte |
| W25Q256 | 256Mbit / 32MByte |
对于上述数据怎么来的呢?我们可以参考数据手册,对于W25Q64其是 64Mbit 的容量,而1数据位是8字节,那么 64Mbit 就是 8MByte:

6.2 引脚定义
芯片手册的引脚图如下:

| 引脚 | 功能 | 补充 |
| VCC、GND | 电源(2.7~3.6V) | |
| CS(SS) | SPI片选,低电平有效 | 当 /CS 为高电平时,设备处于未选中状态,串行数据输出(DO 或 IO0、IO1、IO2、IO3)引脚处于高阻态。当设备未选中时,除非正在进行内部擦除、编程或状态寄存器操作,否则设备功耗将处于待机水平。 当 /CS 为低电平时,设备将被选中,功耗将增加到活动水平,并且可以向设备写入指令和从设备读取数据。 上电后,在接受新指令之前,/CS 必须从高电平变为低电平。/CS 输入在上电时必须跟踪 VCC 电源电平,如有需要,可在 /CS 上使用上拉电阻来实现这一点。 |
| CLK(SCK) | SPI时钟 | 串行时钟(CLK)SPI 串行时钟输入(CLK)引脚为串行输入和输出操作提供时序。 |
| DI(MOSI) | SPI主机输出从机输入 | W25Q64BV 支持标准 SPI、双 SPI 和四 SPI 操作。标准 SPI 指令使用单向 DI(输入)引脚,在串行时钟(CLK)输入引脚的上升沿将指令、地址或数据串行写入设备。标准 SPI 还使用单向 DO(输出)引脚在时钟(CLK)下降沿从设备读取数据或状态。双线和四线 SPI 指令使用双向 IO 引脚在时钟(CLK)上升沿向设备串行写入指令、地址或数据,并在时钟(CLK)下降沿从设备读取数据或状态。四线 SPI 指令要求状态寄存器 2 中的非易失性四线使能位(QE)被设置。当 QE=1 时,WP 引脚变为 IO2,/HOLD 引脚变为 IO3。 |
| DO(MISO) | SPI主机输入从机输出 | |
| WP | 写保护,低电平有效 | 写保护(/WP)引脚可用于防止状态寄存器被写入。与状态寄存器的块保护(SEC、TB、BP2、BP1 和 BPO)位以及状态寄存器保护(SRP)位配合使用时,可以对部分或整个存储阵列进行硬件保护。/WP 引脚为低电平有效。当状态寄存器 2 的 QE 位设置为四线 IO 时,/WP 引脚(硬件写保护)功能不可用,因为此引脚用于 IO2。 |
| HOLD | 数据保持 | /HOLD 引脚允许在设备被选中时将其暂停。当 /HOLD 被拉低时。当 /CS 为低电平时,DO 引脚将处于高阻态,DI 和 CLK 引脚上的信号将被忽略(无关紧要)。当 /HOLD 被置为高电平时,设备操作可以恢复。当多个设备共享相同的 SPI 信号时,HOLD 功能可能很有用。/HOLD 引脚为低电平有效。当状态寄存器 2 的 QE 位设置为四线 I/O 时,HOLD 引脚功能不可用,因为此引脚用于 IO3。 |

常规外围电路设计:

6.3 设计框图
我们分上下两部分介绍,完整的设计框图:

6.3.1 控制逻辑
先说一下控制逻辑部分:

首先左侧框住部分和芯片引脚相连,可以参考上方电路图,然后 Write Control Logic 配合 WP 引脚进行写保护的操作,其状态会被状态寄存器记录下来:

- BUSY位:忙位,是只读位,位于状态寄存器中的S0。当执行页编程、扇区擦除、块擦除、芯片擦除、写状态寄存器等指令时,该位将自动置 1。此时,除了读状态寄存器指令,其他指令都忽略;当页编程、扇区擦除、块擦除、芯片擦除和写状态寄存器等指令执行完毕之后,该位将自动清 0,表示芯片可以接收其他指令了。
- WEL位:写保护位,是只读位,位于状态寄存器中的S1。执行完写使能指令后,该位将置 1。当芯片处于写保护状态下,该位为 0。
- BP2、BP1、 BP0位:块保护位,是可读可写位,分别位于状态寄存器的S4、S3、S2,可以用 写状态寄存器指令置位 这些块保护位。在默认状态下,这些位都为 0,即 块处于 未保护状态下。可以设置块为没有保护、部分保护或者全部保护等状态。
- TB位:底部和顶部块的保护位,是可读可写位,位于状态寄存器的 S5。该位默认为 0,表明顶部和底部块 处于未被保护状态下,可以用 写状态寄存器指令置位该位。当 SPR位为 1 或 /WP引脚 为低电平时,这些位不可以被更改。
- 保留位:位于状态寄存器的 S6,读取状态寄存器值时,该位为 0。
- SRP位:状态寄存器保护位,是可读可写位,位于状态寄存器的 S7。该位结合 /P引脚 可以禁止写状态寄存器功能。该位默认值为0。当SRP=0时,/WP引脚 不能控制状态寄存器的写禁止;当 SRP=1 且 /P=0时,写状态寄存器指令失效;当SRP=1 且 /P=1 时,可以执行写状态寄存器指令。
实现掉电不丢失部分,W25Q64 的每一个存储单元(存储一个比特 0 或 1)在物理上都是一个 “浮栅MOSFET晶体管”。
- 浮栅:这是一个被绝缘体(二氧化硅)完全包围、与外界物理隔离的导电栅极。电荷一旦被注入,就无法轻易逃逸。
- 电荷的囚笼:你可以把浮栅想象成一个“电子陷阱”。在无外力作用下,被困在里面的电子可以稳定地保存很多年(通常超过20年)。
状态如何定义?
- 状态 ‘1’ (已擦除):浮栅内没有电子。此时晶体管的阈值电压较低,容易导通。
- 状态 ‘0’ (已编程):通过高压,将电子注入到浮栅中。这些电子的存在会改变晶体管的特性,使其阈值电压变高,不易导通。
接着页地址锁存计数器和字节地址锁存计数器。
- 页地址锁存计数器:它锁存的是 “页”的编号。对于 8MB 的芯片,总页数为 8,388,608 / 256 = 32,768 页。所以这个计数器可以理解为一个从 0 到 32767 的计数器。这个计数器保存的是地址的 高16位(A23-A8)。它决定了当前读取操作位于 哪一页。
- 字节地址锁存计数器:它锁存的是在一个页内部的 “字节”位置。由于一页只有 256 字节,所以这个计数器是一个 8 位计数器,范围是 0 到 255。这个计数器保存的是地址的 低8位(A7-A0)。它决定了当前读取操作从该页的 哪个字节 开始。
简单来说,页地址锁存计数器,管理 “在哪一页”,在字节计数器溢出时递增。字节地址锁存计数器,管理 “在页内的哪个字节”,每读取一个字节就递增一次。
6.3.2 存储逻辑
首先我们先看一些这个介绍:

简单翻译一下,W25Q64BV 阵列由 32,768 个可编程页组成,每页 256 字节。每次最多可编程 256 字节。页可以以 16 页为一组(扇区擦除)、128 页为一组(32KB 块擦除)、256 页为一组(64KB 块擦除)或整个芯片(芯片擦除)的方式进行擦除。W25Q64BV 分别具有 2048 个可擦除扇区和 128 个可擦除块。较小的 4KB 扇区在需要数据和参数存储的应用中提供了更大的灵活性:

每块,每扇区都有自己的地址空间:

6.4 指令集
这些是我们想要操作W25Q64是需要的一些指令集,下面对常用的一些进行讲述:

| 指令值 | 中文名称 | 核心作用 | 典型使用场景 |
|---|---|---|---|
| 0x06 | 写使能 | 开启 Flash 的写入 / 擦除权限(所有写 / 擦除操作前必须先执行此指令) | 页编程、扇区擦除、块擦除、整片擦除、写状态寄存器前调用 |
| 0x04 | 写禁用 | 关闭 Flash 的写入 / 擦除权限(防止误操作) | 写 / 擦除操作完成后,主动禁用写权限,提升安全性 |
| 0x05 | 读状态寄存器 | 读取 Flash 的状态寄存器(核心位:BIT0 = 忙标志,BIT1 = 写使能锁存) | 等待擦除 / 写入完成(查 BIT0)、验证写使能是否生效(查 BIT1) |
| 0x01 | 写状态寄存器 | 配置 Flash 的状态寄存器(如写保护、Quad SPI 模式等) | 启用 / 禁用 Flash 的硬件写保护(WP 引脚)、切换 SPI 通信模式(单 / 双 / 四线) |
| 0x03 | 普通读数据 | 从指定地址读取任意长度数据(最常用的读指令) | 常规数据读取(速度适中,时序简单,兼容性最好) |
| 0x0B | 快速读数据 | 高速读数据(比普通读多 1 个 dummy 字节,支持更高 SPI 时钟) | 对读取速度有要求的场景(如固件升级、数据批量读取) |
| 0x3B | 双线快速读 | 用 2 根数据线(MISO/MOSI)同时读数据,速度翻倍 | 高速数据读取(需硬件支持双线模式,Flash 的 DIO 引脚需接 MCU) |
| 0x02 | 页编程(写数据) | 向指定页写入最多 256 字节数据(写入前必须先擦除) | 单页数据写入(W25Q64 每页 256 字节,地址需页对齐) |
| 0xD8 | 64KB 块擦除 | 擦除指定 64KB 块的所有数据(擦除后字节为 0xFF) | 批量数据更新(擦除效率比扇区擦除高,适合大段数据写入前的擦除) |
| 0x20 | 4KB 扇区擦除 | 擦除指定 4KB 扇区的所有数据(最常用的擦除指令) | 小批量数据更新(W25Q64 每扇区 4KB,地址需扇区对齐) |
| 0xC7 | 整片擦除 | 擦除 Flash 所有数据(8M 字节) | 初始化 Flash、全量数据更新(慎用!W25Q64 擦除耗时约 10 秒) |
| 0xB9 | 掉电模式 | 进入低功耗模式(电流从 mA 级降至 μA 级) | 设备休眠时降低功耗(需先执行 ReleasePowerDown 才能恢复通信) |
| 0xAB | 释放掉电模式 | 退出掉电模式,同时可读取 Flash 的 Device ID | 从休眠唤醒后恢复 Flash 通信、快速读取 Device ID |
| 0xAB | 读 Device ID | 单独读取 Flash 的设备 ID(与 ReleasePowerDown 指令值相同,用途不同) | 快速识别 Flash 型号(需配合 dummy 字节) |
| 0x90 | 读厂商 + 设备 ID | 读取厂商 ID(0xEF = 华邦)+ 设备 ID(0x16=W25Q64) | 验证 Flash 型号(最常用的 ID 读取指令,兼容性最好) |
| 0x9F | 读 JEDEC ID | 读取厂商 ID + 内存类型 + 容量 ID(0xEF 0x40 0x17=W25Q64) | 标准化识别 Flash(JEDEC 标准,支持不同品牌 Flash 的通用识别) |
6.4.1 写使能指令(06h)
功能:将状态寄存器中的 WEL (Write Enable Latch) 位置 '1'。
前置条件:在执行页编程、擦除(扇区/块/芯片)或写状态寄存器指令前,必须先执行此指令。
时序:
- 将 /CS 引脚拉至低电平。
- 通过 DI 引脚发送 1 字节指令码 06h。数据在 CLK 的上升沿被锁存。
- 将 /CS 引脚拉至高电平,完成指令。

6.4.2 写禁止指令(04h)
功能:将状态寄存器中的 WEL 位清零 ('0')。
时序:
- 将 /CS 引脚拉至低电平。
- 通过 DI 引脚发送 1 字节指令码 04h。
- 将 /CS 引脚拉至高电平,完成指令。
在写状态寄存器、页编程、擦除指令执行完成后,WEL 位会自动清零。

6.4.3 读状态寄存器指令(05h)
功能:读取状态寄存器的当前值。
时序:
- 将 /CS 引脚拉至低电平。
- 通过 DI 引脚发送 1 字节指令码 05h(在 CLK 上升沿锁存)。
- 芯片随即通过 DO 引脚输出状态寄存器的值(在 CLK 下降沿输出,MSB first)。
需要注意的是:
- 此指令可在任何时候执行,包括芯片忙于编程或擦除操作时。
- 通过读取 BUSY 位,可判断内部操作是否完成,从而确定芯片是否能接收下一条指令。
- 只要 /CS 保持为低,状态寄存器数据将持续输出。指令在 /CS 拉高后结束。

6.4.4 写状态寄存器指令(01h)
功能:读取状态寄存器的当前值。
时序:
- 将 /CS 引脚拉至低电平。
- 通过 DI 引脚发送 1 字节指令码 05h(在 CLK 上升沿锁存)。
- 芯片随即通过 DO 引脚输出状态寄存器的值(在 CLK 下降沿输出,MSB first)。
需要注意的是:
- 此指令可在任何时候执行,包括芯片忙于编程或擦除操作时。
- 通过读取 BUSY 位,可判断内部操作是否完成,从而确定芯片是否能接收下一条指令。
- 只要 /CS 保持为低,状态寄存器数据将持续输出。指令在 /CS 拉高后结束。

6.4.5 读数据指令(03h)
功能:从指定地址开始读取一个或多个字节的数据。
时序:
- 将 /CS 引脚拉至低电平。
- 通过 DI 引脚依次发送:指令码 03h、24 位地址(A23-A0)。
- 芯片从该地址开始,在 CLK 的下降沿通过 DO 引脚输出数据(MSB first)。
需要注意的是:
- 地址自动递增:输出当前地址数据后,地址计数器自动加1,并连续输出下一地址的数据,形成数据流。只要时钟持续且 /CS 为低,即可连续读取整个存储器。
- 指令在 /CS 拉高后结束。
- 当芯片正在执行编程、擦除或写状态寄存器操作时,此指令无效。

七、W25Q64代码编写
对于W25Q64来说,其使用主要通过指令集进行操作,我们通过SPI收发数据进行像W25Q64发送相关指令,进行想要的数据操作:

我们将这些指令进行宏定义操作:
#define W25X_WriteEnable 0x06 //写使能(写/擦除前必须执行)
#define W25X_WriteDisable 0x04 //写禁用(防止误操作)
#define W25X_ReadStatusReg 0x05 //读状态寄存器(查忙标志/写使能)
#define W25X_WriteStatusReg 0x01 //写状态寄存器(配置写保护/通信模式)
#define W25X_ReadData 0x03 //普通读数据(常规读取,兼容性好)
#define W25X_FastReadData 0x0B //快速读数据(高速读取,需dummy字节)
#define W25X_FastReadDual 0x3B //双线快速读(2线传输,速度翻倍)
#define W25X_PageProgram 0x02 //页编程(写数据,每页最大256字节)
#define W25X_BlockErase 0xD8 //64KB块擦除(批量擦除,效率高)
#define W25X_SectorErase 0x20 //4KB扇区擦除(常用擦除指令)
#define W25X_ChipErase 0xC7 //整片擦除(擦除所有数据,慎用)
#define W25X_PowerDown 0xB9 //掉电模式(低功耗)
#define W25X_ReleasePowerDown 0xAB //释放掉电模式(恢复通信)
#define W25X_DeviceID 0xAB //读Device ID(仅设备编号)
#define W25X_ManufactDeviceID 0x90 //读厂商+设备ID(验证Flash型号)
#define W25X_JedecDeviceID 0x9F //读JEDEC ID(标准ID,含容量信息)
7.1 写使能
其实代码非常简单,先拉低SS引脚,开始通讯,然后发送0x06命令给W25Q64,然后在拉低引脚即可:
//向FLASH发送写使能命令
void SPI_FLASH_WriteEnable(void)
{
SPI_Start();//SS引脚拉低,开始通讯
SPI_FLASH_SendByte(W25X_WriteEnable);//发送写使能命令
SPI_Stop();//SS引脚拉高,结束通讯
}
7.2 读标准ID
同样,首先拉低SS引脚开始SPI通讯,然后使用上面写的收发数据函数进行发送0x9F的命令,读取标准ID,然后我们将读取的ID打印出来:
//读取FLASH ID
uint32_t SPI_FLASH_ReadID(void)
{
uint32_t FLASH_ID_Group = 0;//完整ID组合
uint32_t FLASH_ID1 = 0, FLASH_ID2 = 0, FLASH_ID3 = 0;//读取ID数据存放
SPI_Start();//SS引脚拉低,开始通讯
SPI_FLASH_SendByte(W25X_JedecDeviceID);//发送JEDEC指令,读取ID
FLASH_ID1 = SPI_FLASH_SendByte(Dummy_Byte);//读取一个字节数据,读取制造商ID
FLASH_ID2 = SPI_FLASH_SendByte(Dummy_Byte);//读取一个字节数据,读取存储器类型
FLASH_ID3 = SPI_FLASH_SendByte(Dummy_Byte);//读取一个字节数据,读取容量
SPI_Stop();//SS引脚拉高,结束通讯
FLASH_ID_Group = (FLASH_ID1 << 16) | (FLASH_ID2 << 8) | FLASH_ID3;//把数据组合起来,作为函数的返回值
return FLASH_ID_Group;
}
主函数调用一下:
//读取ID
id_example = SPI_FLASH_ReadID();
printf("id = 0x%x \r\n",id_example);

可以发现正常打印,说明芯片正常连接上了。
7.3 读状态寄存器
主要调用命令为0x05,读取 Flash 的状态寄存器(核心位:BIT0 = 忙标志,BIT1 = 写使能锁存),直到 “忙标志” 清零,确保 Flash 完成内部的擦除 / 写入操作后,再执行下一条指令,其代码实现,主要靠SPI是全双工,发1字节的同时必收1字节的特性,我们在发送读状态寄存器命令后,不断发送空字节0xFF,那么我们就可以通过MISO引脚接受Flash返回的状态值,直到状态寄存器的BIT0(WIP位)为0,也就是空闲状态才截止:
//等待WIP(BUSY)标志被置0,即等待到FLASH内部数据写入完毕
void SPI_FLASH_WaitForWriteEnd(void)
{
uint8_t FLASH_Status = 0;
SPI_Start();//SS引脚拉低,开始通讯
SPI_FLASH_SendByte(W25X_ReadStatusReg);//发送 读状态寄存器 命令
/* 若FLASH忙碌,则等待 */
do
{
FLASH_Status = SPI_FLASH_SendByte(Dummy_Byte);//读取FLASH芯片的状态寄存器
}
while ((FLASH_Status & WIP_Flag) == SET);//正在写入标志
SPI_Stop();//SS引脚拉高,结束通讯
}
这里使用do-while循环的原因是do-while 是 “先执行一次,再判断条件”,确保至少读取一次状态寄存器(避免 Flash 刚进入忙状态就跳过循环)。
7.4 整片擦除
要进行Flash的擦除操作,需要先向Flash发送写使能命令,这样做的主要原因是 W25X 系列 Flash 的硬件安全机制,擦除 / 写入属于 “修改数据” 的高危操作,Flash 默认禁止此类操作,必须通过 “写使能指令(0x06)” 主动解锁,否则擦除 / 写入指令会被 Flash 直接忽略。
然后等写使能命令完成后发送整片擦除命令(0xC7):
void SPI_FLASH_BulkErase(void)
{
SPI_FLASH_WriteEnable();//发送FLASH写使能命令
SPI_FLASH_WaitForWriteEnd();//等待写使能命令完毕
SPI_Start();//SS引脚拉低,开始通讯
SPI_FLASH_SendByte(W25X_ChipErase);//发送整块擦除指令
SPI_Stop();//SS引脚拉高,结束通讯
SPI_FLASH_WaitForWriteEnd();//等待擦除完毕
}
7.5 扇区擦除
其原理和整片擦除一样,只不过整片擦除是全擦除不需要考虑你要擦哪里,但是扇区擦除就需要告诉Flash你要擦哪里(一片扇区为4K大小):
void SPI_FLASH_SectorErase(u32 SectorAddr)
{
SPI_FLASH_WriteEnable();//发送FLASH写使能命令
SPI_FLASH_WaitForWriteEnd();//等待写使能命令完毕
SPI_Start();//SS引脚拉低,开始通讯
SPI_FLASH_SendByte(W25X_SectorErase);//发送扇区擦除指令,4K擦除
SPI_FLASH_SendByte((SectorAddr & 0xFF0000) >> 16);//发送擦除扇区地址的高位
SPI_FLASH_SendByte((SectorAddr & 0xFF00) >> 8);//发送擦除扇区地址的中位
SPI_FLASH_SendByte(SectorAddr & 0xFF);//发送擦除扇区地址的低位
SPI_Stop();//SS引脚拉高,结束通讯
SPI_FLASH_WaitForWriteEnd();//等待擦除完毕
}
7.6 读数据
向Flash发送读指令,然后告诉Flash你要读哪里,要读多长,读数据也是采用字节替换,向Flash连续发送0xFF,进行数据交换:
//读取FLASH数据
//pBuffer,存储读出数据的指针
//ReadAddr,读取地址
//NumByteToRead,读取数据长度
void SPI_FLASH_BufferRead(u8* pBuffer, u32 ReadAddr, u16 NumByteToRead)
{
SPI_Start();//SS引脚拉低,开始通讯
SPI_FLASH_SendByte(W25X_ReadData);//发送读指令
SPI_FLASH_SendByte((ReadAddr & 0xFF0000) >> 16);//发送读地址高位
SPI_FLASH_SendByte((ReadAddr& 0xFF00) >> 8);//发送读地址中位
SPI_FLASH_SendByte(ReadAddr & 0xFF);//发送读地址低位
/* 读取数据 */
while (NumByteToRead--)
{
*pBuffer = SPI_FLASH_SendByte(Dummy_Byte);//读取一个字节
pBuffer++;//指向下一个字节缓冲区
}
SPI_Stop();//SS引脚拉高,结束通讯
}
7.7 写一页数据
和上面流程一样,发送写使能,等待写使能,然后发送写指令,发送地址,告诉Flash要写入哪里,然后发送当前要写入的数据:
void SPI_FLASH_PageWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{
SPI_FLASH_WriteEnable();//发送FLASH写使能命令
SPI_FLASH_WaitForWriteEnd();//等待写使能命令完毕
SPI_Start();//SS引脚拉低,开始通讯
SPI_FLASH_SendByte(W25X_PageProgram);//写页写指令
SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16);//发送写地址的高位
SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 8);//发送写地址的中位
SPI_FLASH_SendByte(WriteAddr & 0xFF);//发送写地址的低位
//数据长度检查,如果超出页大小打印输出错误
//并且保持数据在256,不进行覆盖之前数据
if(NumByteToWrite > SPI_FLASH_PerWritePageSize)
{
NumByteToWrite = SPI_FLASH_PerWritePageSize;
printf("SPI_FLASH_PageWrite too large!");
}
/* 写入数据*/
while (NumByteToWrite--)
{
SPI_FLASH_SendByte(*pBuffer);//发送当前要写入的字节数据
pBuffer++;//指向下一字节数据
}
SPI_Stop();//SS引脚拉高,结束通讯
SPI_FLASH_WaitForWriteEnd();//等待写入完毕
}
这里我们需要知道一件事,上面介绍W25Q64的时候,其数据手册也说了,对于W25Q64来说其每次写操作最多操作256字节,超出的部分要么丢掉,要么将数据放到前面,覆盖掉之前的数据,那么我们如果既想要之前的数据,又不想后面的数据丢失要怎么办呢?我们可以进行跨页写入。
页写入必须从页起始地址开始,且单页最多写 256 字节!!!
7.8 连续写数据
实际应用中,写入的起始地址可能不对齐、数据长度可能跨页(比如从地址 0x0000FF 写 300 字节,会跨 0x000000~0x0000FF 页和 0x000100~0x0001FF 页)。那么我们要如何实现支持任意起始地址、任意长度数据的写入(哪怕数据跨多个页)呢?
首先创建几个变量用来存放一些基础信息:
uint8_t Addr = 0;//地址在页内的偏移量
uint8_t count = 0;//当前页剩余空间
uint8_t NumOfPage = 0;//完整页数
uint8_t NumOfSingle = 0;//剩余字节数
首先我们计算起始地址在当前页的偏移,比如:WriteAddr=0x0000FF → Addr=255(WriteAddr,写入地址),若WriteAddr是256的整数倍(如0x000100)→ Addr=0(地址对齐):
Addr = WriteAddr % SPI_FLASH_PageSize;
然后计算当前页可用剩余空间,通过256(一页空间)减去当前页已用的空间:
count = SPI_FLASH_PageSize - Addr;
计算完整页数,以及剩余不满一页的字节数:
NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;//写入数据长度除以一页大小(256字节)计算出要写多少整数页
NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;//对一页运算求余,计算出剩余不满一页的字节数
首先处理刚好页对齐的数据(起始地址刚好是页起始),也就是addr等于0的数据,此时有两种状态:
一种数据长度不到一页,此时直接调用上面写一页数据的函数即可:
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
另一种多页,此时根据上面计算得到的完整页数,逐页写入,没写完一页数据地址后移一页,数据指针后移一页,最后在将剩余不满一页的字节写入:
while (NumOfPage--)//先把整数页都写了
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
WriteAddr += SPI_FLASH_PageSize;// 地址后移1页
pBuffer += SPI_FLASH_PageSize;// 数据指针后移1页
}
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);//若有多余的不满一页的数据,把它写完
若地址不对齐,也就是addr不等于0,不是从起始地址开始写入的,这个情况就比较复杂,我们先考虑,要写入的数据长度小于一页数据,不过就算要写入的数据小于一页数据。也可能比剩余空间大或者小。
举个例子,当前页的起始地址为1,但是其中已经写了200个字节的数据了,还剩56个数据,这时我们在想往当前页写入数据:
① 假如要写10个数据是不是,剩下的空间可以直接写入,不需要在跨别的页写入了
② 但是如果我们要写入70个数据,是不是还需要到下一页进行写入。
③ 如果我们写入的数据大于一页,例如600,我们是不是可以将这600个数据分成56+544,前56个数据写入当前页,后544个数据可以按照从页起始位置开始写。
我们先看情况一:当前剩余空间足够,这样我们只需要调用写一页数据的函数,告诉Flash我们要写入的数据,地址,以及长度即可:
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
情况二:当前剩余空间不足,且要写入的数据不够一页,我们将当前空间写满,多余数据从下一页起始位置开始写入:
temp = NumOfSingle - count; // 剩余要写到下一页的字节数
SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);// 先写满当前页
WriteAddr += count; // 地址后移到下一页起始
pBuffer += count; // 数据指针后移1字节
SPI_FLASH_PageWrite(pBuffer, WriteAddr, temp);// 写剩余字节到下一页
情况三:要写入的数据大于一页,我们先扣除当前剩余页的空间,然后重新计算剩余的整页数和不够一页的字节数据:
// 步骤1:先扣掉当前页剩余空间(count=56),重新计算整页/剩余数
NumByteToWrite -= count; // 600-56=544字节
NumOfPage = NumByteToWrite / SPI_FLASH_PageSize; // 544/256=2整页
NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize; // 544%256=32字节
// 步骤2:先写当前页剩余的56字节,让地址对齐到下一页
SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);
// 步骤3:地址/指针后移,接下来按“对齐逻辑”写
WriteAddr += count;
pBuffer += count;
// 步骤4:循环写整页(2个256字节)
while (NumOfPage--)
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
WriteAddr += SPI_FLASH_PageSize;
pBuffer += SPI_FLASH_PageSize;
}
// 步骤5:写最后剩余的32字节
if (NumOfSingle != 0)
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);
}
7.9 主函数
我们故意不对齐数据读写一下看看:
#include "stm32f10x.h"
#include "Delay.h"
#include "Bsp_LED_Gpio.h"
#include "Bsp_Usartx.h"
#include "SPI_gpio.h"
#include "W25Qx_Flash.h"
uint8_t write_buf[1000] = {0}; // 写入缓冲区
uint8_t read_buf[1000] = {0}; // 读取缓冲区
int main(void)
{
LED_GPIO_Config();//LED引脚初始化
USART_Config();//串口初始化
SPI_FLASH_Init();//SPI初始化
uint32_t id_example;
printf("这是一个SPI读取W25Q64测试\r\n");
//读取ID
id_example = SPI_FLASH_ReadID();
printf("id = 0x%x \r\n",id_example);
uint16_t i = 0;
printf("\r\n构造累加测试数据...\r\n");
for(i=0; i<1000; i++)
{
write_buf[i] = i & 0xFF; // 累加值(0~255循环,便于验证)
}
SPI_FLASH_BulkErase();//全片擦除
printf("\r\n正在写入数据...\r\n");
SPI_FLASH_BufferWrite(write_buf,0x000001,1000);//写入数据故意不对齐
printf("\r\n正在读取数据...\r\n");
SPI_FLASH_BufferRead(read_buf, 0x000000, 1010);
for(i=0; i<1010; i++)
{
printf("%02X ",read_buf[i]);
}
while(1)
{
}
}
数据能正常读写:



浙公网安备 33010602011771号