【ARM Cortex-M开发实战指南(基础篇)】第22章 SPI

开发环境:
MDK:Keil 5.30
STM32CubeMX:V6.4.0
MCU:STM32F103ZET6

22.1 SPI简介

SPI,是Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口。是Motorola首先在其MC68HCXX系列处理器上定义的。是一种高速全双工的通信总线,它由摩托罗拉公司提出,当前最新的为 V04.01—2004 版。它被广泛地使用在ADC、LCD 等设备与 MCU 间通信的场合。SPI接口主要应用在 EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,正是出于这种简单易用的特性,现在越来越多的芯片集成了这种通信协议。

22.1.1 SPI 信号线

SPI包含4条总线,SPI总线包含4条总线,分别为SS、SCK、MOSI、MISO。它们的作用介绍如下:
1)SS(Slave Select):片选信号线,当有多个SPI设备与MCU相连时,每个设备的这个片选信号线是与 MCU 单独的引脚相连的,而其他的SCK、MOSI、MISO 线则为多个设备并联到相同的 SPI 总线上,见下图。当SS信号线为低电平时,片选有效,开始SPI 通信。

p9S7qG8.png

2)SCK (Serial Clock):时钟信号线,由主通信设备产生,不同的设备支持的时钟频率不一样,如 STM32 的 SPI 时钟频率最大为 fPCLK /2。

3)MOSI (Master Output, Slave Input):主设备输出 / 从设备输入引脚。主机的数据从这条信号线输出,从机由这条信号线读入数据,即这条线上数据的方向为主机到从机。

4)MISO(Master Input, Slave Output):主设备输入 / 从设备输出引脚。主机从这条信号线读入数据,从机的数据则由这条信号线输出,即在这条线上数据的方向为从机到主机。

22.1.2 SPI模式

SPI通信中可作为从机也可以作为主机,这取决于硬件设计和软件设置。

当器件作为主机时,使用一个IO引脚拉低相应从机的选择引脚(NSS),传输的起始由主机发送数据来启动,时钟(SCK)信号由主机产生。通过MOSI发送数据,同时通过MISO引脚接收从机发出的数据。

当器件作为从机时,传输在从机选择引脚(NSS)被主机拉低后开始,接收主机输出的时钟信号,在读取主机数据的同时通过MISO引脚输出数据。

根据 SPI 时钟极性(CPOL)和时钟相位(CPHA) 配置的不同,分为 4 种 SPI 模式。

时钟极性是指 SPI 通信设备处于空闲状态时(也可以认为这是 SPI 通信开始时,即SS 为低电平时),SCK 信号线的电平信号。CPOL=0 时, SCK 在空闲状态时为低电平,CPOL=1 时则相反。

时钟相位是指数据采样的时刻,当 CPHA=0 时,MOSI 或 MISO 数据线上的信号将会在 SCK 时钟线的奇数边沿被采样。当 CPHA=1 时,数据线在 SCK 的偶数边沿采样。

p9S7xqs.md.png

我们来分析这个 CPHA=0 的时序图。首先,由主机把片选信号线SS 拉低,即为图中的SS (O)时序,意为主机输出,SS (I)时序实际上也是SS 线信号,SS (I)时序表示从机接收到SS 片选被拉低的信号。

在SS 被拉低的时刻,SCK 分为两种情况,若我们设置为 CPOL=0,则 SCK 时序在这个时刻为低电平,若设置为CPOL=1,则 SCK 在这个时刻为高电平。

无论CPOL=0 还是=1,因为我们配置的时钟相位CPHA=0,在采样时刻的时序中我们可以看到,采样时刻都是在SCK的奇数边沿(注意奇数边沿有时为下降沿,有时为上升沿)。因此,MOSI 和 MISO 数据线的有效信号在SCK的奇数边沿保持不变,这个信号将在SCK 奇数边沿时被采集,在非采样时刻,MOSI和MISO的有效信号才发生切换。

对于CPHA=1的情况也很类似,但数据信号的采样时刻为偶数边沿,其时序图见下图。使用 SPI 协议通信时,主机和从机的时序要保持一致,即两者都选择相同的SPI模式。

p9SHpaq.md.png

22.1.3 SPI特性

STM32的小容量有一个SPI接口,中容量有2个,大容量有3个接口,其特性如下所示。

 3线全双工同步传输;
 8或16位传输帧格式选择;
 主或从操作,支持多主模式;
 8个主模式波特率预分频系数(最大为fPCLK/2);
 主模式和从模式下均可以由软件或硬件进行NSS管理:主/从操作模式的动态改变;
 可编程的时钟极性和相位;
 可编程的数据顺序,MSB在前或LSB在前;
 可触发中断的专用发送和接收标志;
 SPI总线忙状态标志;
 支持可靠通信的硬件CRC;
 可触发中断的主模式故障、过载以及CRC错误标志;
 支持DMA功能的1字节发送和接收缓冲器:产生发送和接受请求。

22.2 SPI架构

下图所示为STM32的SPI 架构图,可以看到 MISO 数据线接收到的信号经移位寄存器处理后把数据转移到接收缓冲区,然后这个数据就可以由我们的软件从接收缓冲区读出了。

p9SHFRU.md.jpg

当要发送数据时,我们把数据写入发送缓冲区,硬件将会把它用移位寄存器处理后输出到 MOSI 数据线。
SCK 的时钟信号则由波特率发生器产生,我们可以通过波特率控制位(BR)来控制它输出的波特率。

控制寄存器 CR1 掌管着主控制电路,STM32 的 SPI 模块的协议设置(时钟极性、相位等)就是由它来制定的。而控制寄存器 CR2 则用于设置各种中断使能。

最后为 NSS 引脚,这个引脚扮演着 SPI 协议中的SS 片选信号线的角色,如果我们把 NSS 引脚配置为硬件自动控制,SPI 模块能够自动判别它能否成为 SPI 的主机,或自动进入 SPI 从机模式。但实际上我们用得更多的是由软件控制某些 GPIO 引脚单独作为SS信号,这个 GPIO 引脚可以随便选择。

通常SPI通过4个引脚与外部器件相连:
● MISO:主设备输入/从设备输出引脚。该引脚在从模式下发送数据,在主模式下接收数据。
● MOSI:主设备输出/从设备输入引脚。该引脚在主模式下发送数据,在从模式下接收数据。
● SCK: 串口时钟,作为主设备的输出,从设备的输入。
● NSS: 从设备选择。这是一个可选的引脚,用来选择主/从设备。它的功能是用来作为“片选引脚”,让主设备可以单独地与特定从设备通讯,避免数据线上的冲突。从设备的NSS引脚可以由主设备的一个标准I/O引脚来驱动。

22.3 SPI工作原理

22.3.1 (NSS)输入输出管理

 (NSS)输出管理
对于每个SPI的NSS可以输入,也可以输出。所谓输入,就是NSS的电平信号给自己,所谓输出,就是将NSS的电平信号发送出去,给从机。配置为输出,还是不输出,我们可以通过SPI_CR2寄存器的SSOE位。当SSOE=1时,并且SPI处于主模式控制时(MSTR=1),NSS就输出低电平,也就是拉低,因此当其他SPI设备的NSS引脚与它相连,必然接收到低电平,则片选成功,都成为从设备了。

 (NSS)输入管理
NSS软件模式:

 SPI主机:
需要设置SPI_CR1寄存器的SSM=1和SSI=1,SSM=1是为了使能软件管理,NSS有内部和外部引脚。这时候外部引脚留作他用(可以用来作为GPIO驱动从设备的片选信号)。内部NSS引脚电平则通过SPI_CRL寄存器的SSI位来驱动。SSI=1是为了使NSS内电平为高电平。为什么主设备的内部NSS电平要为1呢?

STM32手册上说,要保持MSTR=1和SPE=1,也就是说要保持主机模式,只有NSS接到高电平信号时,这两位才能保持置‘1’。

 SPI从机:
NSS引脚在完成字节传输之前必须连接到一个低电平信号。在软件模式下,则需要设置SPI_CR1寄存器的SSM=1(软件管理使能)和SSI=0.

NSS硬件模式:
对于主机,我们的NSS可以直接接到高电平.对于从机,NSS接低就可以。

22.3.2 单主和单从应用

p9SHVsJ.png

从上图可以看出,主机和从机都有一个串行移位寄存器,主机通过向它的SPI串行寄存器写入一个字节发起一次传输。寄存器通过MOSI信号将字节传给从机,从机也将自己的移位寄存器中的内容通过MISO信号返还给主机。这样,两个移位寄存器中下的内容就被交换,外设的写操作是同步完成的。如果只进行写操作,主机只需忽略接收到的字节;反之,若主机要读取从机的一个,就必须发送一个空字节来引发从机的传输。

22.3.3 时钟信号的相位和极性

SPI_CR寄存器的CPOL和CPHA位,能够组合成四种可能的时序关系。CPOL(时钟极性)位控制在没有数据传输时时钟的空闲状态电平,此位对主模式和从模式下的设备都有效。如果CPOL被清’0’,SCK引脚在空闲状态保持低电平;如果CPOL被置’1’,SCK引脚在空闲状态保持高电平。如果CPHA(时钟相位)位被置’1’,SCK时钟的第二个边沿(CPOL位为0时就是下降沿,CPOL位为’1’时就是上升沿)进行数据位的采样,数据在第二个时钟边沿被锁存。如果CPHA位被清’0’,SCK时钟的第一边沿(CPOL位为’0’时就是下降沿,CPOL位为’1’时就是上升沿)进行数据位采样,数据在第一个时钟边沿被锁存。

CPOL时钟极性和CPHA时钟相位的组合选择数据捕捉的时钟边沿。

p9SHmZR.md.jpg

p9SHnd1.md.jpg

22.3.4 数据帧格式

根据SPI_CR1寄存器中的LSBFIRST位,输出数据位时可以MSB在先也可以LSB在先。根据SPI_CR1寄存器的DFF位,每个数据帧可以是8位或是16位。所选择的数据帧格式对发送和/或接收都有效。

22.3.5 SPI主从模式工作原理

配置SPI主模式的步骤如下:
1.设置SPI_CR1寄存器的BR[2:0]位,来定义串行时钟波特率。
2.选择CPOL和CPHA位,定义数据传输和串行时钟间的相位关系。
3.设置DFF位来定义8或16位数据帧格式。
4.配置SPI_CR1寄存器的LSBFIRST位定义帧格式。
5.如果NSS引脚需要工作在输入模式,硬件模式中在整个数据帧传输期间应把NSS引脚连接到高电平;在软件模式中,需设置SPI_CR1寄存器的SSM=1和SSI=1。如果NSS引脚工作在输出模式,则只需设置SSOE=1位。
6.设置MSTR=1和SPE=1,只当NSS引脚被连到高电平,这些位才能保持置位。

配置SPI从模式的步骤如下:
1.设置DFF位以定义数据帧格式为8位或16位。
2.定义数据传输和串行时钟之间的相位关系。
3.帧格式必须和主设备相同,MSB在前还是LSB在前取决于SPI_CR1寄存器中的LSBFIRST位。
4.硬件模式下,在完整的数据帧(8位或16位)发送过程中,NSS引脚必须为低电平。软件模式下,设置SPI_CR1寄存器中的SSM=1,SSI=0。
5.MSTR=0位,设置SPE=1,使相应引脚工作于SPI模式下。

22.3.6 状态标志

应用程序通过3个状态标志可以完全监控SPI总线的状态。
1.发送缓冲器空闲标志(TXE)
此标志为’1’时表明发送缓冲器为空,可以写下一个待发送的数据进入缓冲器中。当写入SPI_DR时,TXE标志被清除。
2.接收缓冲器非空(RXNE)
此标志为’1’时表明在接收缓冲器中包含有效的接收数据。读SPI数据寄存器可以清除此标志。
3.忙(Busy)标志
BSY标志由硬件设置与清除(写入此位无效果),此标志表明SPI通信层的状态。

22.3.7 SPI中断

SPI的相关中断标志如下:

p9SHlRO.md.jpg

22.4 硬件连接

W25Q128 是华邦公司推出的一款 SPI 接口的 NOR Flash 芯片,其存储空间为 128Mbit,相当于 16M 字节。

W25Q128 可以支持 SPI 的模式 0 和模式 3,也就是 CPOL=0/CPHA=0 和CPOL=1/CPHA=1 这两种模式。
W25Q128 将 16M 的容量分为 256 个块(Block),每个块大小为 64K 字节,每个块又分为 16个扇区(Sector),每个扇区 4K 个字节。 W25Q128 的最少擦除单位为一个扇区,也就是每次必须擦除 4K 个字节。操作需要给 W25Q128 开辟一个至少 4K 的缓存区,对 SRAM 要求比较高,要求芯片必须有 4K 以上 SRAM 才能很好的操作。该芯片支持Standard SPI,Dual/Quad I/O SPI。

W25Q128 的擦写周期多达 10W 次,具有 20 年的数据保存期限,支持电压为 2.7~3.6V,W25Q128 支持标准的 SPI,还支持双输出/四输出的 SPI,最大 SPI 时钟可以到 80Mhz(双输出时相当于 160Mhz,四输出时相当于 320M)。

W25Q128 内部有一个“SPI Command & Control Logic”,可以通过 SPI 接口向其发送指令,从而执行相应操作。

【注】
①、Flash 写入数据时和 EEPROM 类似,不能跨页写入,一次最多写入一页,W25Q128的一页是 256 字节。写入数据一旦跨页,必须在写满上一页的时候,等待 Flash 将数据从缓存搬移到非易失区,重新再次往里写。
②、Flash 有一个特点,就是可以将 1 写成 0,但是不能将 0 写成 1,要想将 0 写成 1,必须进行擦除操作。因此通常要改写某部分空间的数据,必须首先进行一定物理存储空间擦除,最小的擦除空间,通常称之为扇区,扇区擦除就是将这整个扇区每个字节全部变成 0xFF。

我的开发板选用的Flash是W25Q128,容量为16M,挂载在SPI1上,如下图所示。

p9SHGsH.png

22.5 SPI具体代码实现-标准库

首先是SPI的硬件初始化。

/**
  * @brief  串行FLASH初始化,初始化串行Flash底层驱动GPIO和SPI外设
  * @param  None
  * @retval None
  */
void SPI_FLASH_Init(void)
{
    SPI_InitTypeDef  SPI_InitStructure;
    GPIO_InitTypeDef GPIO_InitStructure;

    /* 使能GPIO和SPI时钟 */
    FLASH_SPI_APBxClock_FUN ( FLASH_SPI_CLK, ENABLE );
    FLASH_SPI_SCK_APBxClock_FUN ( FLASH_SPI_SCK_CLK, ENABLE );
    FLASH_SPI_MISO_APBxClock_FUN ( FLASH_SPI_MISO_CLK, ENABLE );
    FLASH_SPI_MOSI_APBxClock_FUN ( FLASH_SPI_MOSI_CLK, ENABLE );
    FLASH_SPI_CS_APBxClock_FUN ( FLASH_SPI_CS_CLK, ENABLE );

    /* 配置SPI功能引脚:SCK 时钟引脚 */
    GPIO_InitStructure.GPIO_Pin = FLASH_SPI_SCK_PIN;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;  
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_Init(FLASH_SPI_SCK_PORT, &GPIO_InitStructure);

    /* 配置SPI功能引脚:MISO 主机输入从机输出引脚 */
    GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MISO_PIN;
    GPIO_Init(FLASH_SPI_MISO_PORT, &GPIO_InitStructure);

    /* 配置SPI功能引脚:MISO 主机输出从机输入引脚 */
    GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MOSI_PIN;
    GPIO_Init(FLASH_SPI_MOSI_PORT, &GPIO_InitStructure);

    /* 配置SPI功能引脚:CS 串行Flash片选引脚 */
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Pin = FLASH_SPI_CS_PIN;
    GPIO_Init(FLASH_SPI_CS_PORT, &GPIO_InitStructure);

    /* 首先禁用串行Flash,等需要操作串行Flash时再使能即可 */
    FLASH_SPI_CS_DISABLE();

    /* SPI外设配置 */
    /* 
     * FLASH芯片:
     * 在CLK上升沿时到DIO数据采样输入. 
     * 在CLK下降沿时在DIO进行数据输出。
     * 据此设置CPOL CPHA 
     */
    SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; /*双线双向全双工*/
    SPI_InitStructure.SPI_Mode = SPI_Mode_Master; /* SPI Master 模式*/
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; /*SPI 发送接收 8 位帧结构*/
    SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; /*串行同步时钟的空闲状态为高电平*/
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; /*第二个跳变沿数据被采样*/
    SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; /*NSS 信号由软件控制*/
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; /*SPI分频系数*/
    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; /*数据传输从 MSB 位开始*/
    SPI_InitStructure.SPI_CRCPolynomial = 7; /*CRC 值计算的多项式*/
    SPI_Init(FLASH_SPIx , &SPI_InitStructure);

    /* 使能SPI外设  */
    SPI_Cmd(FLASH_SPIx , ENABLE);
}

SPI的硬件初始化最重要的函数就是SPI_Init()。

void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct)

其中SPI参数配置的结构体为SPI_InitTypeDef。

/** 
  * @brief  SPI Init structure definition  
  */

typedef struct
{
  uint16_t SPI_Direction;           /*!< Specifies the SPI unidirectional or bidirectional data mode.
                                         This parameter can be a value of @ref SPI_data_direction */

  uint16_t SPI_Mode;                /*!< Specifies the SPI operating mode.
                                         This parameter can be a value of @ref SPI_mode */

  uint16_t SPI_DataSize;            /*!< Specifies the SPI data size.
                                         This parameter can be a value of @ref SPI_data_size */

  uint16_t SPI_CPOL;                /*!< Specifies the serial clock steady state.
                                         This parameter can be a value of @ref SPI_Clock_Polarity */

  uint16_t SPI_CPHA;                /*!< Specifies the clock active edge for the bit capture.
                                         This parameter can be a value of @ref SPI_Clock_Phase */

  uint16_t SPI_NSS;                 /*!< Specifies whether the NSS signal is managed by
                                         hardware (NSS pin) or by software using the SSI bit.
                                         This parameter can be a value of @ref SPI_Slave_Select_management */

  uint16_t SPI_BaudRatePrescaler;   /*!< Specifies the Baud Rate prescaler value which will be
                                         used to configure the transmit and receive SCK clock.
                                         This parameter can be a value of @ref SPI_BaudRate_Prescaler.
                                         @note The communication clock is derived from the master
                                               clock. The slave clock does not need to be set. */

  uint16_t SPI_FirstBit;            /*!< Specifies whether data transfers start from MSB or LSB bit.
                                         This parameter can be a value of @ref SPI_MSB_LSB_transmission */

  uint16_t SPI_CRCPolynomial;       /*!< Specifies the polynomial used for the CRC calculation. */
}SPI_InitTypeDef;

SPI_InitTypeDef结构体成员变量如下:

 SPI_Direction 用来设置 SPI 的通信方式,可以选择为半双工,全双工,以及串行发和串行收方式,这里设置的全双工(SPI_Direction_2Lines_FullDuplex)。
 SPI_Mode 用来设置 SPI 的主从模式。SCK 的时序是由通讯中的主机产生的。若被配置为从机模式, STM32 的 SPI 外设将接受外来的 SCK 信号。
 SPI_DataSize为 8 位还是 16 位帧格式选择项。
 SPI_CPOL 用来设置时钟极性。时钟极性 CPOL 成员,可设置为高电平(SPI_CPOL_High)或低电平(SPI_CPOL_Low )
 SPI_CPHA 用来设置时钟相位,就是选择在串行同步时钟的第几个跳变沿(上升或下降)数据被采样,可以为第一个或者第二个条边沿采集。时钟相位 CPHA 则可以设置为 SPI_CPHA_1Edge(在 SCK 的奇数边沿采集数据) 或SPI_CPHA_2Edge (在 SCK 的偶数边沿采集数据) 。
 SPI_NSS 设置NSS 信号由硬件(NSS 管脚)还是软件控制。可以选择为硬件模式(SPI_NSS_Hard )与软件模式(SPI_NSS_Soft ),在硬件模式中的 SPI 片选信号由 SPI 硬件自动产生,而软件模式则需要我们亲自把相应的 GPIO 端口拉高或置低产生非片选和片选信号。实际中软件模式应用比较多。
 SPI_BaudRatePrescaler设置 SPI 波特率预分频值决定 SPI 的时钟的参数,从不分频道 256 分频 8 个可选值。传输速度为 36M/4=9MHz。2-156,凡是2的几次方都可以。
 SPI_FirstBit设置数据传输顺序是 MSB 位在前还是 LSB 位在前
 SPI_CRCPolynomial 来设置 CRC 校验多项式,提高通信可靠性,大于 1 即可。

SPI Flash的读写操作如下:

/**
  * @brief  从串行Flash读取一个字节数据
  * @param  None
  * @retval uint8_t:读取到的数据
  */
uint8_t SPI_FLASH_ReadByte(void)
{
    return (SPI_FLASH_SendByte(Dummy_Byte));
}

/**
  * @brief  往串行Flash读取写入一个字节数据并接收一个字节数据
  * @param  byte:待发送数据
  * @retval None
  */
uint8_t SPI_FLASH_SendByte(uint8_t byte)
{
    /* 循环等待直到SPI 数据寄存器DR为空,即当DR寄存器不为空时持续等待 */
    while (SPI_I2S_GetFlagStatus(FLASH_SPIx , SPI_I2S_FLAG_TXE) == RESET);

    /* 通过SPI外设发送一个字节数据 */
    SPI_I2S_SendData(FLASH_SPIx , byte);

    /* 等待接收到数据 */
    while (SPI_I2S_GetFlagStatus(FLASH_SPIx , SPI_I2S_FLAG_RXNE) == RESET);

    /* 读取SPI总线接收到一个字节数据并返回 */
    return SPI_I2S_ReceiveData(FLASH_SPIx );
}

发送数据前要等待发送缓冲区为空,靠TXE标志判断,所以开始的while循环是等待发送缓冲区为空,同时,等待接收缓冲区是否有数据,靠RXNE标志来判断,把接收缓冲区的数据作为返回值返回。由于发送和接收是同时进行的,而且要接收一个数据时必须在有效的SCK下,而只有发送数据才能产生有效的SCK,所以接收数据的函数时在发送数据的函数的基础上,将发送的数据设置为Dummy_Byte假数据来骗取有效的SCK。

SPI Flash读写Buffer操作如下:

/**
  * @brief  往串行FLASH写入数据,调用本函数写入数据前需要先擦除扇区
  * @param  pBuffer:待写入数据的指针
  *         WriteAddr:写入地址
  *         NumByteToWrite:写入数据长度
  * @retval None
  */
void SPI_FLASH_BufferWrite(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
    uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0;

    Addr = WriteAddr % SPI_FLASH_PageSize;
    count = SPI_FLASH_PageSize - Addr;
    NumOfPage =  NumByteToWrite / SPI_FLASH_PageSize;
    NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;

    if (Addr == 0) /* 若地址与 SPI_FLASH_PageSize 对齐  */
    {
        if (NumOfPage == 0) /* NumByteToWrite < SPI_FLASH_PageSize */
        {
            SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
        }
        else /* NumByteToWrite > SPI_FLASH_PageSize */
        {
            while (NumOfPage--)
            {
                SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
                WriteAddr +=  SPI_FLASH_PageSize;
                pBuffer += SPI_FLASH_PageSize;
            }

            SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);
        }
    }
    else /* 若地址与 SPI_FLASH_PageSize 不对齐 */
    {
        if (NumOfPage == 0) /* NumByteToWrite < SPI_FLASH_PageSize */
        {
            if (NumOfSingle > count) /* (NumByteToWrite + WriteAddr) > SPI_FLASH_PageSize */
            {
                temp = NumOfSingle - count;

                SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);
                WriteAddr +=  count;
                pBuffer += count;

                SPI_FLASH_PageWrite(pBuffer, WriteAddr, temp);
            }
            else
            {
                SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
            }
        }
        else /* NumByteToWrite > SPI_FLASH_PageSize */
        {
            NumByteToWrite -= count;
            NumOfPage =  NumByteToWrite / SPI_FLASH_PageSize;
            NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;

            SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);
            WriteAddr +=  count;
            pBuffer += count;

            while (NumOfPage--)
            {
                SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
                WriteAddr +=  SPI_FLASH_PageSize;
                pBuffer += SPI_FLASH_PageSize;
            }

            if (NumOfSingle != 0)
            {
                SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);
            }
        }
    }
}

/**
  * @brief  从串行Flash读取数据
  * @param  pBuffer:存放读取到数据的指针
  *         ReadAddr:读取数据目标地址
  *         NumByteToRead:读取数据长度
  * @retval None
  */
void SPI_FLASH_BufferRead(uint8_t* pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead)
{
    /* 选择串行FLASH: CS低电平 */
    FLASH_SPI_CS_ENABLE();

    /* 发送 读 指令 */
    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++;
    }
    /* 禁用串行FLASH: CS 高电平 */
    FLASH_SPI_CS_DISABLE();
}

主函数代码如下:

/**
  * @brief  main function
  * @param  None
  * @retval None
  */
int main(void)
{
    SysTick_Init();

    /* USART1 config 115200 8-N-1 */
    USART_Config();

    /*初始化LED*/
    LED_GPIO_Config();  

    /* 调用格式化输出函数打印输出数据 */
    printf("16M byte串行flash(W25Q128)读写测试实验\n");  

    /* 16M串行flash W25Q128初始化 */
    SPI_FLASH_Init();

    /* Get SPI Flash Device ID */
    DeviceID = SPI_FLASH_ReadDeviceID();

    Delay_ms( 1 );

    /* Get SPI Flash ID */
    FlashID = SPI_FLASH_ReadID();

    printf("FlashID is 0x%X,  Manufacturer Device ID is 0x%X\n", FlashID, DeviceID);

    /* Check the SPI Flash ID */
    if (FlashID == SPI_FLASH_ID)  /* #define  sFLASH_ID  0XEF4018 */
    {
        printf("检测到华邦串行flash W25Q128 !\r\n");

        /* 擦除SPI的扇区以写入 */
        SPI_FLASH_SectorErase(FLASH_SectorToErase);

        /* 将发送缓冲区的数据写到flash中 */     
        SPI_FLASH_BufferWrite(Tx_Buffer, FLASH_WriteAddress, BufferSize);
        SPI_FLASH_BufferWrite(Tx_Buffer, 252, BufferSize);
        printf("写入的数据为:\n%s \r\n", Tx_Buffer);

        /* 将刚刚写入的数据读出来放到接收缓冲区中 */
        SPI_FLASH_BufferRead(Rx_Buffer, FLASH_ReadAddress, BufferSize);
        printf("读出的数据为:\n %s\n", Rx_Buffer);

        /* 检查写入的数据与读出的数据是否相等 */
        TransferStatus1 = Buffercmp(Tx_Buffer, Rx_Buffer, BufferSize);

        if( PASSED == TransferStatus1 )
        {
            printf("16M串行flash(W25Q128)测试成功!\r\n");
            LED1_ON;
        }
        else
        {
            printf("16M串行flash(W25Q128)测试失败!\r\n");
            LED2_ON;
        }
    }
    else
    {
        printf("获取不到 W25Q128 ID!\n");
        LED3_ON;
    }

    /* 无限循环 */
    while (1)
    {
    }
}

首先对SPI进行初始化,然后就极性FLASH的读取,完整代码请参看源码。

22.6 SPI具体代码实现-HAL库

22.6.1 STM32Cube生成工程

我们在串口的例子的基础上进行配置。

串口通信(HAL库)

本文使用SPI1,因此这里使能SPI1,设置为全双工通信。

p9SHUot.md.png

接下来就是设置SPI1的参数,如下图所示:

p9SHwJf.md.png

选择后将PA4作为片选信号,也就是NSS信号,产生启停信号。

p9p5rGD.md.png

22.6.2 SPI具体代码分析

主函数代码如下:

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  MX_SPI1_Init();
  /* USER CODE BEGIN 2 */

  /* 调用格式化输出函数打印输出数据 */
  printf("这是一个16M byte串行flash(W25Q128)读写测试实验\r\n"); 

  /* Get SPI Flash Device ID */
  DeviceID = SPI_FLASH_ReadDeviceID(&hspi1);

  HAL_Delay(100);

  /* Get SPI Flash ID */
  FlashID = SPI_FLASH_ReadID(&hspi1);

  printf("FlashID is 0x%X,  Manufacturer Device ID is 0x%X\r\n", FlashID, DeviceID);

  /* Check the SPI Flash ID */
  if (FlashID == SPI_FLASH_ID)  /* #define  sFLASH_ID  0XEF4018 */
  {
    printf("检测到华邦串行flash W25Q128\r\n");

    /* 擦除SPI的扇区以写入 */
    SPI_FLASH_SectorErase(&hspi1, FLASH_SectorToErase);

    /* 将发送缓冲区的数据写到flash中 */
    SPI_FLASH_BufferWrite(&hspi1, Tx_Buffer, FLASH_WriteAddress, BufferSize);
    SPI_FLASH_BufferWrite(&hspi1, Tx_Buffer, 252, BufferSize);
    printf("写入的数据为:%s\r\n", Tx_Buffer);

    /* 将刚刚写入的数据读出来放到接收缓冲区中 */
    SPI_FLASH_BufferRead(&hspi1, Rx_Buffer, FLASH_ReadAddress, BufferSize);
    printf("读出的数据为:%s\r\n", Rx_Buffer);

    /* 检查写入的数据与读出的数据是否相等 */
    TransferStatus1 = Buffercmp(Tx_Buffer, Rx_Buffer, BufferSize);

    if( PASSED == TransferStatus1 )
    {
      printf("16M串行flash(W25Q128)测试成功!\r\n");
    }
    else
    {
      printf("16M串行flash(W25Q128)测试失败!\r\n");
    }
  }
  else
  {
    printf("获取不到 W25Q128 ID!\r\n");
  }

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

HAL的实现逻辑和标准库一样,这里就不在赘述了。

22.7 实验现象

在电脑端打开串口调试助手工具,设置参数为115200 8-N-1。下载完程序之后,在串口调试助手窗口可接收到信息。

p9p5hIf.md.png

Related posts

Leave a Comment