HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)


前言

一、SPI 接口和通讯协议

1.1 什么是SPI

  串行外设接口(Serial Peripheral Interface,SPI)是一种传输速率比较高的串行接口,一些ADC芯片、Flash存储器芯片采用SPI接口,MCU通过SPI接口与这些外围器件通信。

1.2 SPI 的引脚信息

  SPI接口的设置分为主设备(Master)和从设备(Slave),一个主设备可以连接一个或多个从设备。SPI通信的连接方式如图1-1 所示,SPI的主设备也可以称为主机,从设备也可称为从机。
在这里插入图片描述
                      图1-1 SPI通信的连接方式
SP接口有3个基本信号,功能表述如下:
(1)MOSI(Master Output Slave Input),主设备输出/从设备输入信号。MOSI是主设备的串行数据输出,SI是从设备的串行数据输入,主设备和从设备的这两个信号相连。
(2)MISO(Master Iuput Slave Output),主设备输入/从设备输出信号,MI是主设备的串行数据输入,SO是从设备的串行数据输出,主设备和从设备的这两个信号连接。
(3)SCK,串行时钟信号。时钟信号总是由主设备产生。
  除了这3个必需的信号,从设备还有一个从设备选择信号CS(Chip Select;也称NSS位,Slave Select),用于选择与主设备进行通信的特定从设备。低电平表示选中从设备,高电平表示未选中。当一个SPI通信网络里有多个SPI从设备时(如图1-1),主设备就通过控制各个从设备的CS信号来保证同一时刻只能一个SPI从设备在线通信,未被选中的SPI从设备的接口引脚是高阻态。SPI主设备可以使用普通的GPIO输出引脚连接从设备的CS引脚,控制从设备的片选信号。

1.3 SPI 的工作原理(了解即可)

  在主机和从机都有一个串行移位寄存器,主机通过向它的 SPI 串行寄存器写入一个字节来发起一次传输。串行移位寄存器通过 MOSI 信号线将字节传送给从机,从机也将自己的串行移位寄存器中的内容通过 MISO 信号线返回给主机。这样,两个移位寄存器中的内容就被交换。外设的写操作和读操作是同步完成的。
在这里插入图片描述
                        图1-2 SPI通信原理
在这里插入图片描述
                        图1-3 SPI结构框图

1.4 SPI 传输协议

  SPI数据传输是在时钟信号SCK驱动下的串行数据传输,SPI的传输协议定义了SPI通信的起始信号、结束信号、数据有效性时钟同步等环节。SPI每次传输的数据帧长度是8位或16位,一般是最高有效位(Most Significant Bit,MSB)先行
  SPI通信有四种时序,由SPI控制寄存器SPI_CR1中的CPOL位和CPOA位控制。
  CPHL(Clock Polarity)时钟极性,控制SCK引脚在空闲状态的电平。如果CPOL为0,则空闲时SCK为低电平;如果CPOL为1,则空闲时SCK为高电平。
  CPHA(Clock Phase)时钟相位。如果CPHA为0,则在SCK的第一个边沿对数据采样;如果CPHA为1,则在CSK的第二个边沿对数据采样。
                        表1-1 SPI的4种时序模式

SPI时序模式CPOL时钟极性CPHA时钟相位空闲时SCK电平采样时刻
模式000低电平第1个跳变沿
模式101低电平第2个跳变沿
模式210高电平第1个跳变沿
模式311高电平第2个跳变沿

  图1-4所示的是CPHA为0时的数据传输时序图。NSS位从高变低是数据传输的起始信号,NSS从低变高是数据数据传输的结束信号,图中是MSB先行的方式。

在这里插入图片描述
                    图1-4 CPHA为0时的数据传输时序图
  CPHA设置为0表示在SCK的第一个边沿读取数据,读取数据的时刻(捕获选通时刻)就是图1-4中虚线表示的时刻。根据CPOL的取值不同,读取数据的时刻发生在SCK的下跳沿(CPOL为1)时刻或上跳沿(CPOL为0)时刻。MISO、MOSI上的数据是在读取数据的SCK前一个跳变沿时刻发生变化的。
  图1-5所示的是CPHA为1时的数据传输时序图。CPHA为1表示在SCK的第2个边沿读取数据。也就是图1-5中虚线表示的时刻。根据CPOL的取值不同,读取数据的时刻发生在SCK上跳沿(CPOL为1)时刻或下跳沿(CPOL为0)时刻。MISO、MOSI上的数据是在读取数据的SCK前一个跳变沿时刻发生的。
在这里插入图片描述
                      图1-5 CPHA为1时的数据传输时序图
  在使用SPI接口通信时,主设备和从设备的SPI时序一定要一致,否则无法正常通信,由CPOL和CPOA的不同组合构成了4种SPI时序模式,如表1-1所示。如果使用硬件SPI接口,只需要设置正确的SPI时序模式,底层的通信时序由SPI硬件处理。有时候需要用普通GPIO引脚模拟SPI接口,这称为软件模拟SPI结接口

1.5 STM32F407的SPI接口

  STM32F407新芯片上有3个硬件SPI接口,除了支持SPI通信协议,还支持I2S音频协议。STM32F407的SPI接口有如下的特性。

  • 数据帧长度可选择8位或16位。
  • 可设置为主模式或从模式。
  • SPI支持全双工通信。
  • 可设置8种预分频值用于产生通信波频率,波特率最高位为SPI所在APB总线的频率的二分之一。STM32F407上的SPI1在APB2总线上,SPI2和SPI3在APB1总线上。
  • 可设置时钟极性(CPOL)和时钟相位(CPHA),也就是4种SPI时序模式都支持。
  • 可设置MSB先行(Most Significant Bit,高位先行:首先发送数据最高位,依次发送到最低位)或LSB先行(Least Significant Bit,低位先行:先发送数据的最低位,然后依次发送到最高位)。一般会采用MSR(高位先行)的模式。
  • 可以使用硬件CRC效验。
  • 可触发中断的主模式故障、上溢和CRC错误标志。
  • 发送和接收具有独立的DMA请求,DMA传输具有1字节发送和接收缓冲区。
      MCU的SPI接口实现了SPI硬件通信协议,也就是保证数据帧的正确接收和发送,如同UART接口实现底层数据帧的收发一样。SPI主设备和从设备之间具体的通信内容则需要两者之间规定通信协议,如同串口设备之间的通信协议一样。

补充:全双工、单工以及半双工传输方式的理解
(1)全双工(Full Duplex):在全双工模式下,主设备和从设备可以同时进行数据的发送和接收。主设备通过MOSI线发送数据给从设备,并通过MISO线接收从设备返回的数据。这种模式允许双向传输,在同一时钟周期内可以同时进行发送和接收操作,如PI。
(2)半双工(Half Duplex):在半双工模式下,主设备和从设备交替进行数据的发送和接收。通信双方不能同时发送和接收数据,而是通过切换发送和接收模式来实现双向传输。在每次通信中,首先主设备发送数据给从设备,然后切换为接收模式,从设备发送数据给主设备,如UART、I2C、CAN。
(3)单工(Simplex):在单工模式下,数据只能在一个方向上进行传输。通常情况下,SPI总线用于双向通信,因此单工模式在SPI中并不常见。
在这里插入图片描述


二、SPI 的HAL库驱动程序

2.1 SPI 寄存器操作的宏函数

  SPI的驱动程序头文件是 stm32f4xx_hal_spi.h。SPI寄存器操作的宏函数如表2-1所示,宏函数中的参数_HANDLE_是具体某个SPI接口的对象指针,参数 _ INTERRUPT_ 是SPI的中断事件类型,参数_FLAG_是时间中断标志。
                       表2-1 SPI寄存器操作的宏函数

函数名功能描述
__HAL_SPI_DISABLE( __ HANDLE __)禁用某个SPI接口
__ HAL_SPI_ENABLE(__ HANDLE __)启用某个SPI接口
__ HAL_SPI_DISABLE_IT ( __ HANDLE__, __ INTERRUPT __ ) )禁用某个中断事件源,不允许事件产生硬件中断
__ HAL_SPI_ENABLE_IT(__ HANDLE __, __ INTERRUPT __ )开启某个中断事件源,允许事件产生硬件中断
__ HAL_SPI_GET_IT_SOURCE(__ HANDLE __, __ INTERRUPT __ )检查某个中断事件源是否被允许产生硬件中断
__ HAL_SPI_GET_FLAG (__ HANDLE__, __ FLAG __ )获取某个事件的中断标志 ,检查事件是否发生
__ HAL_SPI_CLEAR_CRCERRFLAG(__ HANDLE __)清除CRC校验错误中断标志
__ HAL_SPI_CLEAR_FREFLAG( __ HANDLE__)清除主模式故障中断标志
__ HAL_SPI_CLEAR_MODFFLAG(__ HANDLE __) )清除主模式故障中断标志
__ HAL_SPI_CLEAR_OVRFLAG(__ HANDLE__) )清除溢出错误中断标志

2.2 SPI 初始化和阻塞器数据传输

  SPI 接口初始化、状态查询和阻塞式数据传输的函数列表如表2-2所示。
                   表2-2 SPI接口初始化和阻塞式数据传输相关函数

函数名功能描述
HAL_SPI_Init()SPI初始化,配置SPI接口函数
HAL_SPI_MspInit()SPI的MSP初始化函数,重新实现时一般用于SPI接口引脚GPIO初始化和中断设置
HAL_SPI_GetState()返回SPI接口当前状态,返回值是枚举类型SPI_HandleTypeDef
HAL_SPI_GetError()返回SPI接口最后的错误码,错误码有一组宏定义
HAL_SPI_Transmit()阻塞式发送一个缓冲区的数据
HAL_SPI_Receive()阻塞式接收指定长度的数据保存到缓冲区
HAL_SPI_TransmitReceive()阻塞式同时发送和接收一定长度的数据

2.2.1 SPI 接口初始化

  函数 HAL_SPI_Init() 用于具体某个SPI接口的初始化,其原型定义如下:

HAL_StatusTypeDef HAL_SPI_Init(SPI_HandleTypeDef *hspi);

  其中,参数hspi是SPI外设对象指针。hspi->Init是SPI_HandleTypeDef 结构体类型,存储了SPI接口的通信参数。

2.2.2 阻塞式数据发送和接收

  SPI是一种主/行通信方式,通信完全由SPI主机控制,因为SPI主机控制了时钟信号SCK。SPI主机与从机之间一般是应答式通信,主机先用函数 HAL_SPI_Transmit() 在MOSI线上发送指令或数据,忽略MISO线上传入的数据;从机接收指令或数据后会返回响应数据,主机通过函数 HAL_SPI_Receive() 在MISO线上接收响应数据,接收时不会在MOSI线上发送有效数据。
  函数 HAL_SPI_Transmit() 用于发送数据,其原型定义如下:

HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);

  其中,参数hspi是SPI外设对象指针,pData是输出数据缓冲区指针;Size是缓冲区数据的字节数,Timeout是超时等待时间,单位是系统滴答信号节拍数,默认情况下就是ms。函数 HAL_SPI_Transmit() 是阻塞式执行的,也就直到数据发送完成或超时时间后才返回。函数HAL_OK表示发送成功,返回HAL_TIMOUT表示发送超时。
  函数 HAL_SPI_Receive() 用于从SPI接口接收数据,其原型定义如下:

HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);

  其中,参数pData是接收数据缓冲区,Size是要接收的字节数,Timeout是超时等待时间。

2.3 中断方式发送数据

  SPI接口能以中断的方式传输数据,是非阻塞数据传输。中断方式数据传输的相关函数、产生的中断事件类型、对应的中断回调函数等如表2-3所示。中断事件类型用中断事件使能控制位的宏定义表示。
                     表2-3 SPI中断方式数据传输相关函数

函数名功能描述产生的中断事件类型对应的回调函数
HAL_SPI_Transmit_IT()中断方式发送一个缓冲区的数据SPI_IT_TXEHAL_SPI_TxCpltCallback()
HAL_SPI_Receive_IT()中断方式节后指定长度的数据保存到缓冲区SPI_IT_RXNEHAL_SPI_RxCpltCallback()
HAL_SPI_TransmitReceive_IT()中断方式接收指定长度的数据保存到缓冲区SPI_IT_TXE和SPI_IT_RXNHAL_SPI_TxRxCpltCallback()
前三个中断方式传输函数前3个中断模式传输函数都可能产生SPI_IT_ERR中断事件SPI_IT_ERRHAL_SPI_ErrorCallback()
HAL_SPI_IRQHandler()SPI中断ISR里调用的通道处理函数————
HAL_SPI_Abort()取消非阻塞式数据传输,本函数以阻塞模式运行————
HAL_SPI_Abort_IT()取消非阻塞式数据传输,本函数以阻塞模式运行——HAL_SPI_AbortCpltCallback()

  HAL_SPI_Transmit_IT() 用于发送一个缓冲区的数据,发送完成后,会产生发送完成中断事件(SPI_IT_TXE),对应的回调函数是HAL_SPI_TxCpltCallback()
  函数HAL_SPI_Receive_IT() 用于接收指定长度的数据保存到缓冲区,接收完成后,会产生接收完成中断事件(SPI_IT_RXNE),对应的回调函数是HAL_SPI_RxCpltCallback()
  函数HAL_SPI_TransmitReceive_IT() 是发送和接收同时进行,由它启动的数据传输会产生 SPI_IT_TXE 和 SPI_IT_RXN 中断事件,但是有专门的回调函数HAL_SPI_TxRxCpltCallback()
  上面3个函数的原型定义如下:

HAL_StatusTypeDef HAL_SPI_Transmit_IT(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_SPI_Receive_IT(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_SPI_TransmitReceive_IT(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData,uint16_t Size);

  上面的3个函数都是非阻塞式的,函数返回HAL_OK只是表示函数操作成功,并不表示数据传输完成,只有对应的回调函数被调用才表明数据传输完成 。
  函数HAL_SPI_AbortCpltCallback()是SPI中断ISR里调用的通用处理函数,它会根据中断事件类型调用相应的回调函数。在SPI的HAL驱动程序中。回调函数是用SPI外设对象变量的函数指针重定向的,在启动传输的函数里,为回调函数指针赋值,使用时只需要了解表2-3的对应关系即可。
  函数HAL_SPI_Abort() 用于取消非阻塞数据传输过程,包括中断方式和DMA方式,这个函数自身以阻塞模式运行。
  函数HAL_SPI_Abort_IT() 用于取消非阻塞式数据传输过程,包括中断方式和DMA方式,这个函数自身以中断模式运行,所以有回调函数HAL_SPI_AbortCpltCallback()

2.4 DMA方式数据传输

  SPI的发送和接收有各自的DMA请求,能以DMA方式进行数据的发送发送和接收。DMA方式传输时需要触发DMA流的中断事件,主要有DMA传输完成中断事件。SPI的DMA方式数据传输的相关函数如表2-4 所示。DMA流的中断事件的宏定义可查。
                     表2-4 SPI的DMA方式数据传输的系相关函数

DMA方式功能函数函数功能DMA流中断事件对应的回调函数
HAL_SPI_Transmit_DMA()DMA方式发送数据DMA传输完成HAL_SPI_TxCpltCallback()
HAL_SPI_Transmit_DMA()DMA方式发送数据DMA传输半完成HAL_SPI_TxHalfCpltCallback()
HAL_SPI_Receive_DMA()DMA方式接收数据DMA传输完成HAL_SPI_RxCpltCallback()
HAL_SPI_Receive_DMA()DMA方式接收数据DMA传输半完成HAL_SPI_RxHalfCpltCallback()
HAL_SPI_TransmitReceive_DMA()DMA方式发送/接收数据DMA传输完成HAL_SPI_TxRxCpltCallback()
HAL_SPI_TransmitReceive_DMA()DMA方式发送/接收数据DMA传输半完成HAL_SPI_TxRxHalfCpltCallback()
前3个DMA方式传输函数()DMA传输错误中断事件DMA传输错误

  启动DMA方式发送和接收数据的两个函数的原型分别定义如下:

HAL_StatusTypeDef HAL_SPI_Transmit_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_SPI_Receive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);

  其中,hspi是SPI外设对象指针,pData是用于DMA数据发送或接收的数据缓冲区指针,Size是缓冲区的大小,因为SPI接口传输的基本数据单位是字节,所以缓冲区元素类型是uint8_t,缓冲区大小的单位是字节

三、Flash存储芯片 W25Q128

   为了进一步的了解SPI通信的原理,我们通过一款SPI通信的Flash存储芯片来进行实验。Flash 是常见的用于存储数据的半导体器件, 它具有容量大、可重复擦写、按“扇区/块” 擦除、掉电后数据可继续保存的特性。常见的 Flash 主要有 NOR Flash 和 Nand Flash 两种类型,它们的特性如表3-1所示。 NOR 和 NAND 是两种数字门电路, 可以简单地认为 Flash 内部存储单元使用哪种门作存储单元就是哪类型的 Flash。
                  表3-1 NOR Flash和Nand Flash对比

特性NOR FLASHNAND FLASH
容量较小较大
同容量存储器成本较贵较便宜
擦除单元以“扇区/块”擦除以“扇区/块”擦除
读写单元可以基于字节读写必须以“块”为单位读写
读取速度较高较低
写入速度较低较高
集成度较低较高
介质类型随机存储连续存储
地址线和数据线独立分开共用
坏块较少较多
是否支持XIP支持不支持
应用举例25Qxx、程序ROMEMMC、SSD、U盘等

3.1 硬件接口和连接

   W25Q128是一个Flash存储芯片,容量为128Mbit(位),也就是16MB(字节)。W25Q128支持标准SPI,还支持Dual/Qual SPI。若W25Q128工作于Dual/Qual SPI通信模式,需要连接的MCU也支持Dual/Qual SPI通信。具有QUADSPI接口的MCU才支持Dual/Qual SPI通信,如STM32F214、STM32F469等。
   STM32F407只有标准SPI接口,不支持Dual/Qual SPI通信通信。开发板上有一个W25Q128芯片,通过标准SPI接口与STM32F407的SPI1接口连接,电路如图3-1。
在这里插入图片描述
                      图3-1 W25Q128电路

  W25Q128的各个引脚的功能描述如下

  • SO、SI、CLK这3个SPI引脚与MCU的SPI1接口的相应引脚连接,占用PB4、PB5、PB3 引脚。
  • 片选信号CS与MCU的PB14连接,由MCU通过GPIO引脚PB14的输出控制W25Q128的片选状态。
  • WP是写保护设置引脚,WP为低电平时,禁止修改内部的状态寄存器,与状态寄存器的一些位配合使用,可以对内部的一些存储区域进行写保护。电路中将WP接高电平,也就是不使用此写保护信号。
  • HOLD是硬件保持信号引脚,当器件被选中时,如果HOLD输入为低电平,那么DO引脚变为高组态,DI和CLK的输入被忽略。当HOLE引脚输入高电平时,SPI的操作又继续。这里将HOLD引脚接电源,就是不使用保持功能。
      W25Q128支持SPI模式0和模式3。在MCU与W25Q128通信时,设置使用SPI1模式3,即设置CPOL = 1,CPOA = 1
      开发板上的W25Q128与STM32F407的SPI1连接,因为SPI1接口要用到PB4、PB5、PB3引脚,而5线的JTAG接口要用到PB3、PB4,所以在使用SPI1接口时,系统的Dubug接口不能设置为JTAG接口,只能设置为SW接口,所以,为避免出现错误,在程序中所有例程都用的SW调试接口。
    注:
      Flash 也有对应的缺点,我们在使用过程中需要尽量去规避这些问题: 一是 Flash 的使用寿命, 另一个是可能的位反转。使用寿命体现在: 读写上是 FLASH 的擦除次数都是有限的(NOR Flash 普遍是 10 万次左右),当它的使用接近寿命的时候,可能会出现写操作失败。 由于 NAND 通常是整块擦写,块内有一位失效整个块就会失效,这被称为坏块。使用 NAND Flash 最好通过算法扫描介质找出坏块并标记为不可用, 因为坏块上的数据是不准确的。位反转是数据位写入时为 1, 但经过一定时间的环境变化后可能实际变为 0 的情况, 反之亦然。 位反转的原因很多,可能是器件特性也可能与环境、 干扰有关。

3.2 存储空间划分

  W25Q128总容量为16MB,使用24位地址线,地址范围为0x000 000 ~0xFFF FFF。
  16MB分为256个块(Block),每个块的大小为64KB,16位偏移地址,快内偏移地址范围是0x0000~0XFFFF。
  每个块又分为16个扇区(Sector),共4096个扇区,每个扇区的大小为4KB,12位偏移地址,扇区内偏移地址的范围是0x000~0xFFF。
  每个扇区又分为16个页(Page),共65536个页,每个页的大小为256个字节,8位偏移地址,页内偏移地址范围是0x00~0xFF。
在这里插入图片描述
                      图3-2 存储空间划分

3.3 数据读写的原则

  从W25Q128读取数据时,用户可以从任意地址开始读取任意长度的数据。从W25Q128写数据时,用户可以从任意地址开始写数据,但是一次SPI通信写入的数据范围不能超过一个页的边界。所以,如果从页的起始地址开始写数据,一次最多可写入一个页的数据,即256个字节。如果一次写入的数据超过页的边界,会再从页的起始位置开始写
  向存储区域写入数据时,存储区域必须是被擦除过的,也就是存储内容是0xFF,否则写入的数据操作无效。用户可以对整个器件、某个块、某个扇区进行擦除操作,但是不能对单个页进行擦除。

3.4 操作指令

  SPI的硬件层和传输协议只是规定了传输一个数据帧的方法,对于具体的SPI期间的操作由器件规定的操作指令实现。W25Q128制定了很多的操作指令,用以实现各种功能。
  W25Q128的操作指令由一字节或多字节组成,指令的第一个字节是指令码,其后跟随的是指令的参数或返回的数据。W25Q128常用的几个指令如表3-1所示,其全部指令和详细解释见W25Q128的数据手册。表3-1 中用括号表示的部分表示返回的数据,A23~A0是24位的全局地址,dummy表示必须发送的无效字节数据,一般发送0x00。

                 表3-1 W25Q128常用指令

指令名称BYTE1指令码BYTE2BYTE3BYTE4BYTE5BYTE6
写使能0x06——————————
读状态寄存器10x05(S7~S0)————————
读状态寄存器20x35(S15~S8)————————
读厂家和设备ID0x90dummydummy0x00(MF7~MF0)(ID7~ID0)
读64位序列号0x4Bdummydummydummydummy(ID63 ~ ID0)
器件擦除0xC7/0x60——————————
块擦除0xD8A23~A16A15~A8A7~A0————
扇区擦除0x20A23~A16A15~A8A7~A0————
写数据(页编程)0x02A23~A16A15~A8A7~A0D7~D0——
读数据0x03A23~A16A15~A8A7~A0(D7~D0)——
快速读数据0x0BA23~A16A15~A8A7~A0dummy(D7~D0)

下面以几个指令为例,说明指令传输的过程,以及返回数据的读取等原理。

3.4.1 "写使能"指令

  “写使能”指令(指令码0x06)只有一个指令码,其传输过程如图3-1所示,一个指令总是从片选信号CS由高变低的跳变开始,片选信号CS由低变高的跳变中结束。
在这里插入图片描述
                  图3-1 单字节“写使能”指令的时序

在这里插入图片描述

  CS变为低电平后,MCU向W25Q128传输1字节数据0x06,然后结束SPI传输即可。W25Q128接收数据后,根据指令码判断指令类型,并进行相应的处理。“写使能”指令是将状态寄存器1的WEL位设置为1,在擦除芯片、擦除扇区等操作前必须执行“写使能”指令。
  无返回数据的指令的操作都有此类似,就是连续将指令码、指令参数发送给W25Q128即可。

3.4.2 “读数据”指令

  “读数据”指令(指令码0x03)运用从某个地址开始读取一定个数的字节数据,其中时序如图3-2所示。地址A23~A0是24位全局地址,分为3个字节,在发送指令码0x03后,再发送3个字节的地址数据。然后MCU开始从DO线上读取数据,一次读取一个字节,可以连续读取,W25Q128会自动返回下一个地址的数据。

在这里插入图片描述
                    图3-2 “读数据”指令的时序

在这里插入图片描述

3.4.3 “写数据”指令

  “ 写数据 ” 指令(指令码0x02)就是数据手册上的“页编程”指令,用于向任意地址写入一定长度的数据。“写数据”指令的时序如图3-3所示,图中是向一个页一次写入256个字节的数据。一个页的容量是256字节,写数据操作操作一次最多写256字节。如果数据长度超过256个字节,会从页的起始位置开始继续写。

在这里插入图片描述
                    图3-3 “写数据”指令的时序
在这里插入图片描述

  “写数据”指令的起始地址可以是任意地址,数据长度也可以小于256,但如果写的过程中地址超过页的边界,就会从页的起始地址开始继续写。写数据操作的存储单元必须是被擦除过的,也就是内容是0xFF。如果存储单元的内容不是0xFF,那么重新写入数据无效。所以,已经写过的存储区域是不能重复写入的,需要擦除后才能再次写入。

3.5 状态寄存器

  W125Q128有3个状态寄存器(status register),用于对器件的一些参数进行配置,或返回器件的当前状态信息。下面是W25Q128的状态寄存器SR1,其各个位的定义见表
                 表3-2 状态寄存器SR1各个位的定义

位编号位名称功能说明存储特性读/写特性
S7SRP0状态寄存器保护位0非易失可写
S6SEC扇区保护非易失可写
S5TB顶/底保护非易失可写
S4BP2块保护2非易失可写
S3BP1快保护1非易失可写
S2BP0快保护0非易失可写
S1WEL写使能锁存易失可写
S0BUSY有正在进行的擦除或写操作易失只读

  通过读状态寄存器SR1的指令(指令码0x05),我们可以读取SR1的内容。状态寄存器中的某些位是可写的,是指可以通过写状态寄存器的指令修改这些位的内容;这些位是非易失的,是指修改的内容可永久保存,掉电也不会丢失。
SR1中有2个位在编程中经常用到:WEL位与BUSY位。
  写使能锁存(Write Enable Latch,WEL)位是只读的。器件上电后,WEL位是0。只有当WEL位是1时,才能进行擦除芯片、擦除扇区、页编程操作。这些操作执行完后,WEL位自动变为0。只有执行“写操作”指令(指令码0x06)后,WEL位才变1。所以,在进行擦除芯片、擦除扇区、页编程等操作之前,“写使能”指令是必须先执行的
  BUSY位是只读的,表示器件是否处于忙的状态。如果BUSY位是1,表示器件正在执行页编程、扇区擦除、器件擦除等操作。此时,除了“读状态寄存器”指令和“擦除/编程挂起”指令,器件会忽略其他任何指令。当正在执行的页编程、擦除等指令执行完成以后,BUSY位自动变为0,这意味着可以继续执行其他指令了。

四、示例:轮询方式读写W25Q128

4.1 实例功能

  开发板上的W25Q128芯片的电路如图1-1所示,与STM32F407的SPI1接口连接,占用PB3、PB4、PB5引脚,W25Q128的片选信号CS与MCU的PB14连接。在本示例中,会根据这个接口电路,为W25Q128编写常用操作的驱动程序,并且测试轮询方式读写W25Q128。示例功能与操作流程如下。

  • 使用SPI1接口读写SPI1接口读写Flash存储器W25Q128。
  • 使用阻塞模式SPI传输函数编写W25Q128常用功能的驱动程序。
  • 通过模拟菜单测试擦除整个芯片、擦除块、写入数据和读取数据等操作。

4.2 CubeMX配置

4.2.1 SPI1的CubeMx设置

  SPI的模式和参数设置界面如图4-1所示。SPI的模式设置只有两个参数,用于设置SPI1的工作模式和硬件NSS信号。
在这里插入图片描述

                    图4-1 SPI1的模式和参数设置
(1)Mode,工作模式。有多种工作模式可选:作为主机时,一般选择为Full-Duplex Master(全双工主机);作为从机时,一般选择为Full-Duplex Slave(全双工从机)。所谓全双工(Full-Duplex),是指使用MISO线和MOSI线可以同时接收和发送,相应的还有Half-Duplex(半双工),就是只使用一根数据线,这根数据线既可以发送有可接收,但是需要分时使用发送和接收功能。在本例中,MCU作为主机,并且有MISO和MOSI两根串行信号线,所以选择Full-Duplex。
(2)Hardware NSS Output Singal ,硬件NSS信号。有3种选项,"Disable"选项表示不使用硬件NSS信号,而是使用软件方式控制NSS信号 ;Hardware NSS Intput Signal表示硬件NSS输入信号,Hardware Output Single表示硬件NSS输出信号,SPI主机输出片选信号时选择此选项。本示例用一个单独的GPIO引脚PB14作为主机的片选信号,所以设置为Disable。
  SPI1的参数设置为3组,这些参数的设置应该与W25Q128的SPI通信参数对应。W25Q128的SPI通信使用8位数据,MSB先行,支持SPI0和SPI3。
(1)Basic Parameters组,基本参数。
  ① Frame Format,帧格式。有Motorola和TI两个选项。但只能选Motorla。这个参数对应控制控制寄存器SPI_CR2的FRF位。
   "Motorola"选项表示使用Motorola SPI帧格式。在Motorola SPI帧格式中,数据传输是以两个时钟边沿进行的,其中一个时钟边沿用于数据采样,另一个时钟边沿用于数据传输。数据的有效位数可以在配置中指定。
   "TI"选项表示使用TI SPI帧格式。在TI SPI帧格式中,数据传输是以一个时钟边沿进行的,数据在该时钟边沿上同时进行采样和传输。数据的有效位数可以在配置中指定。
  ②Data Size,数据大小。数据帧的位数,可选8位或16位。本示例选择8位。
  ③First Bit,首先要传的位。可选MSB First(高位先行)或LSB Frist(低位先行)。本示例选择MSB First。
(2)Clock Parameters组,时钟参数。
  ①Prescaler(for Buaud Rate),用于产生波特率的预分频系数。有8个可选预分频系数,从2到256。SPI的时钟频率就是所在APB总线的时钟频率,SPI1在APB2总线上。最高频率是84MHz。
  ②Baud Rate,波特率。设置预分频系数后。CubeMx会自动根据APB总线频率和分频系数计算波特率。本示例中的APB2总线频率为84MHz,分频系数为8,所以波特率为10.5 Mbit/s。另外,根据W25Q128的数据手册,读数据指令(0x03)支持的最高频率是33MHz,但是经过测试,如果波特率超过12.5 Mbit/s时,读取数据就会偶尔发生错误,而波特率为5.25Mbit/s(分频系数为16)时传输很稳定。
  ③Clock Polarity,时钟极性。可选项为High和Low。本示例使用SPI模式3,所以选择High,
  ④Clock Phase,时钟相位。可选项为1 Edge和 2Edge。本示例使用SPI模式3,即在第2跳变沿采样数据,所以选择2 Edge。
  图1-1中的CPOL和CPHA的设置对应于SPI模式3,因为W25Q128同时也支持SPI模式0,所以设置CPOL为Low,CPHA为1Edge也是可以的。

(3) Advance Parameters组,高级参数。
  ①CRC Calcution,CRC(循环冗余校验)计算。 STM32F407的SPI通信可以在传输数据的最后加上1个字节的CRC计算结果,在发生CRC错误时可以产生中断。若不使用,就选择Disabled.
  ②NSS Signal Type,NSS信号类型。这个参数的选项是由模式设置里面的Hardware Nss Signal的选择结果决定的。当模式设置里选择Hardware NSS Signal 为Disabe时,这个参数的选项就只能是Sofware,表示用软件产生NSS输出信号,即本例用PB14输出信号作为从机的片选信号。
  启用SPI1后,CubeMx将自动将自动分配PA5、PA6、PA7作为SPI1的3个信号引脚,但是从图3-1中可以看出,实际用到的引脚是PB3、PB4、PB5引脚(如图4-2),所以在配置引脚时一定要注意,要按照原理图上的引脚进行配置,或者在Categories配置后一定要检查一遍引脚是都正确。
在这里插入图片描述
                    图4-2 SPI1的GPIO引脚配置

  本示例使用SPI的阻塞式数据传输方式,不使用SPI的中断,多以无需开启全局中断。

4.2.2 其余GPIO引脚的配置

  该工程通过串口1将W25Q128的操作结果打印出来,串口1具体配置如图4-3所示,需要注意的是,串口工具的配置需要和CubeMx里面的USART配置保持一致。
在这里插入图片描述

                    图4-3 串口配置

  GPIO引脚包括LED灯引脚配置(PF9、PF10),其余引脚包括按键、蜂鸣器配置和项目《HAL库STM32常用外设教程(二)—— GPIO输入\输出》里面的引脚配置相同(有源码提供),如图4-4所示,此处不再赘述。
在这里插入图片描述
                    图4-4 GPIO配置

4.3 程序设计

  在CubeMx里面生成keil的代码以后,在keil里面打开项目,代码如下。

4.3.1 SPI1初始化

  配置好CubeMx后,生成工程并打开,在spi.c的文件里是关于SPI外设的一些基础配置,其中定义了SPI1的初始化函数MX_SPI1_Init(),相关代码及其对应的STM32CubeMx里面的配置如下:
在这里插入图片描述

  上述程序定义了一个SPI_HandleTypeDef 结构体类型变量hspi1,这是表示SPI1的外设对象变量。函数MX_SPI1_Init()设置了hspi1各成员变量的值,其代码与CubeMx的配置是对应的。程序中的注释说明了每个成员变量的意义。
在这里插入图片描述

  HAL_SPI_MspInit()是SPI的 MSP函数(“MCU Support Package”,微控制器支持包),在函数MX_SPI1_Init()里被调用,其主要作用是开启SPI1的时钟,并对其3个复用引脚进行GPIO设置。程序中的注释说明了每个成员变量的意义。

4.3.3 W25Q128的驱动程序

  为方便对W25Q128进行操作,我们将W25Q128常用的一些功能编写为函数,也就是实现3.4节介绍的W25Q128常用操作指令。例如擦除芯片、擦除扇区、读取数据、写入数据等,这就是W25Q128的驱动程序。
  注意W25Q128驱动程序与SPI接口的HAL库驱动程序有区别。SPI的HAL驱动程序实现了SPI接口数据传输的基本功能,是SPI硬件层的驱动;而W25Q128驱动程序是根据W25Q128的指令定义,实现器件具体功能操作的一系列函数。W25Q128驱动程序要用到SPI硬件层的HAL驱动程序,要通过SPI的HAL驱动程序实现数据帧的收发
  我们在项目里创建一个名为User的子目录,创建文件w25flash.c和w25flash.h,驱动程序文件w25flash.c和w25flash.h是根据W25Q128的数据手册编写的,并将其存放在这个子目录里。将子目录User添加到项目的头文件和源文件搜索路径。具体的w25flash.c和w25flash.h在文章末尾的网盘文件里有提供。

4.3 W25Q128的功能描述

4.3.1 主程序

  下面我们使用W25Q128的驱动程序。
(1)在项目中添加代码,对W25Q128进行功能测试。添加完w25flash文件,如图4-5所示。

在这里插入图片描述

图4-5 GPIO配置

(2)在主程序中添加用户代码,完成后文件main.c的代码如下:
  程序在完成外设初始化之后,调用Flash_TestReadStatus()读取Flash芯片的器件ID、状态寄存器SR1和SR2的值,并进行打印,在进入while循环之前,打印出了如下的一个菜单,菜单内如如下:

在while循环里面,程序检测按键输入,对于4个按键分别进行响应,实现下面的功能:

  • KEY0键按下时,调用函数Flash_EraseChip()擦除整个器件,擦除操作大约需要30s,不要经常才出整个扇区。
  • KEY2键按下时,调用Flash_EraseBlock64K()擦除Block0 ,测试写入数据之前应该先擦除。
  • KEY_UP键按下时,调用函数Flash_TestWrite() 从Page0和Page1写入数据。
  • KEY1键按下时,调用函数Flash_TestRead()从Page0和Page1读取数据。
    函数Flash_EraseChip()Flash_EraseBlock64K()是驱动文件w25flash.h中定义的函数。
    函数Flash_TestReadStatus()Flash_TestWrite()Flash_TestRead()是在文件spi.h中定义的测试函数,在后续的步骤中就。
	Flash_TestReadStatus();		/* 读取器件ID并分析芯片类型 */
	KEYS curKey = ScanPressedKey(KEY_WAIT_ALWAYS);		/* 获取按下的是哪一个按键 */
	  switch(curKey)
	  {
	  case KEY0:
		  LED0_Toggle();									/* 翻转LED0 */
			Flash_EraseChip();							/* 擦除整个器件,大约需要25时间 */
			printf("Chip is erased\n");
		  break;
	  case KEY2:
		  LED1_Toggle();									/* 翻转LED1 */
			uint32_t globalAddr	=	0;
			Flash_EraseBlock64K(globalAddr);/* 擦除块 */
			printf("Block0 is erased\n");
		  break;
	  case KEY_UP:
		  LED0_Toggle();									/* 翻转LED0 */
		  LED1_Toggle();									/* 翻转LED1 */
			Flash_TestWrite();							/* Flash写操作 */
		  break;
	  case KEY1:
//		  Buzzer_Toggle();							/* 蜂鸣器 */
			Flash_TestRead();								/* Flash读操作 */
		  break;
		 default:
			 break;
	  }

在这里插入图片描述

4.3.1 W25Q128功能测试函数的实现

  接下来我们利用w25flash.c里面的的W25Q128驱动函数 在spi.c里面实现上一小节中提到的三个函数,即在W25Q128驱动函数上再封装一层来实现我们想要的功能:
(1)函数Flash_TestReadStatus()就是调用W25Q128驱动程序中的3个函数,分别读取器件ID、状态寄存器SR1和SR2。
  函数Flash_ReadID()返回厂家和产品ID,这在器件的手册上可以查到,如W25Q128的ID是0xEF17,如果其他芯片可继续在case里面进行添加,
在这里插入图片描述
代码如下(示例):

/*
 * @brief 读取器件ID、状态寄存器SR1和SR2
 * @param
 */
void Flash_TestReadStatus(void)
{
	uint16_t devID = Flash_ReadID();																/* 读取器件ID */
	uint8_t tempStrDevID[50];																				/* 用来接字符串 */
	sprintf((char*)tempStrDevID,"Device ID = 0x%04X",devID);       	/* 将一个设备ID(devID)格式化为字符串,并存储在 tempStrDevID 变量中 */
	printf("%s\n",tempStrDevID);																		/* 打印tempStrDevID */
	
	switch(devID){																									/* 判读flash的型号 */
		case 0xEF17:
			printf("The chip is W25q128\n");
		break;
		default:
			printf("The chip is Unknow\n");
		break;
	}
	/* 读寄存器SR1的内容 */
	uint8_t tempStrSR1[50];
	sprintf((char*)tempStrSR1,"Status Reg1 = 0x%x",Flash_ReadSR1());  /* 将Flash_ReadSR1函数的返回值直接进行拼接 */
	printf("%s\n",tempStrSR1);
	
		/* 读寄存器SR2的内容 */
	uint8_t tempStrSR2[50];
	sprintf((char*)tempStrSR2,"Status Reg1 = 0x%x",Flash_ReadSR2());  /* 将Flash_ReadSR1函数的返回值直接进行拼接 */
	printf("%s\n",tempStrSR2);
}

(2)函数Flash_TestWrite()的功能是在Page0和Page1里写入数据,写入数据的存储空间必须是被擦除过的,在Page0里面写入的是两个字符串,分别在Page0的起始位置以及偏移100的位置,对一个页是可以分多次写入的,只要写入的存储的存储单元是被擦除过的。对于Page1则从页的起始地址写入了256字节的数据,写入的内容等于偏移地址的大小,即0~255。
在这里插入图片描述

/*
 * @brief  写flash测试
 * @param
 */
void Flash_TestWrite(void)
{
	uint8_t BlockNo 		= 0;																									/* 表示块号 */
	uint8_t SubSectorNo = 0;																									/* 表示扇区 */
	uint8_t  SubPageNo 	= 0;																									/* 表示页号 */
	uint32_t memAddress = 0;  																								/* 存储地址 */
	
	memAddress = Flash_Addr_byBlockSectorPage( BlockNo,SubSectorNo,SubPageNo);/* 计算内存地址,并将结果存储在memAddress变量中 */
	
	uint8_t bufStr1[30] = "Hello from brginning";
	Flash_WriteInPage(memAddress,bufStr1,strlen("Hello from brginning")+1);		/* 在指定的内存地址写入bufStr1(第0页的起始地址) */
	printf("Write in page 0 = %s\n",bufStr1);
	
	uint8_t bufStr2[30] = "Hello in page";
	Flash_WriteInPage(memAddress + 100,bufStr2,strlen("Hello from brginning")+1); /* 上一个位置偏移100个位置写入字符bufStr2 */
	printf("Write in page 100 = %s\n",bufStr2);
	
	uint8_t bufPage[FLASH_PAGE_SIZE];    					  
	for(uint16_t i = 0;i < FLASH_PAGE_SIZE;++i){  /* 使用for循环将0 到 FLASH_PAGE_SIZE-1填充到bufPage数组 */
		bufPage[i] = i;
	}
	
	SubPageNo = 1; 																														/* 再到第1页 */
	memAddress = Flash_Addr_byBlockSectorPage( BlockNo,SubSectorNo,SubPageNo);/* 计算第一页的地址 */
	Flash_WriteInPage(memAddress ,bufPage,FLASH_PAGE_SIZE);  									/* 将填充好的 bufPage 数据写入到内存地址中 */
	printf("Write 0~255 in page1\n ");
}

(3)函数Flash_TestRead()的功能是从Page0和Page1里读取数据 ,即从页的起始地址、偏移12的地址、偏移136的地址、偏移210的地址读出Flash_TestWrite()函数里写入的数值,可以测试读出的内容和写入的是否一致。

加粗样式

/*
 * @brief   读flash测试
 * @param
 */
void Flash_TestRead(void)
{
	uint8_t BlockNo 		= 0;																									/* 表示块号 */
	uint8_t SubSectorNo = 0;																									/* 表示扇区 */
	uint8_t  SubPageNo 	= 0;																									/* 表示页号 */
	uint32_t memAddress = Flash_Addr_byBlockSectorPage( BlockNo,SubSectorNo,SubPageNo);
	uint8_t bufStr[50];  
	Flash_ReadBytes(memAddress,bufStr,50);  																	/* 读50字节 */
	printf("Read in page 0 = %s\n",bufStr);																		/* 将50个字符串打印出来 */

	Flash_ReadBytes(memAddress + 100,bufStr,50);  														/* 读50字节 */
	printf("Read in page 100 = %s\n",bufStr);																	/* 将50个字符串打印出来 */
	
  SubPageNo 	= 1;
	memAddress = Flash_Addr_byBlockSectorPage( BlockNo,SubSectorNo,SubPageNo);/* 计算第一页的地址 */
	uint8_t randData12 = Flash_ReadOneByte(memAddress + 12);									/* 读取地址中偏移量为12的字节数据,并将其存储在randData12变量中 */
	uint8_t randData136 = Flash_ReadOneByte(memAddress + 136);								/* 读取地址中偏移量为136的字节数据,并将其存储在randData136变量中 */
	uint8_t randData210= Flash_ReadOneByte(memAddress + 210);									/* 读取地址中偏移量为210的字节数据,并将其存储在randData210变量中 */
	
	char tempStrRandData[100]="";      																				/* 使用前先清空 */
	sprintf((char*)tempStrRandData,"Page1[12] = %d,Page1[136] = %d,Page1[210] = %d",randData12,randData136,randData210); 
	printf("%s\n",tempStrRandData);
}

4.4 示例结果

  将完成的程序下载到开发板上,如图5-1所示按下相应的按键,就会得到图5-2串口得到的结果,其中打印多次是因为按键没有进行消抖操作。
请添加图片描述

图5-1 开发板按键

在这里插入图片描述

图5-2 按键按下后串口收到的指令

五、总结

  通过本文讲解了SPI通信,其中涉及了SPI的原理、HAL库的相关驱动函数,其中涉及了SPI轮询、中断、DMA三种方式。然后又通过Flash芯片W25Q128作为示例来讲解SPI通信,讲解了W25Q128的部分指令,轮询方式读写W25Q128,其中涉及的SPI的CubeMx配置应当熟悉掌握。


项目源码:HAL库STM32常用外设教程(九)—— SPI (读写W25Q128)

参考书籍:
《STM32Cube高效开发教程(基础篇)》王维波
《STM32F4xx中文参考手册》
《STM32F407 探索者开发指南》


  人最宝贵的东西是生命,生命对人来说只有一次.因此,人的一生应当这样度过:当一个人回首往事时,不因虚度年华而悔恨,也不因碌碌无为而羞愧;这样,在他临死的时候,能够说,我把整个生命和全部精力都献给了人生最宝贵的事业——为人类的解放而奋斗。

——《钢铁是怎样练成的》

根据引用\[1\]中提供的信息,可以使用STM32HAL库来模拟SPI方式驱动W25Q128存储芯片。首先需要进行模拟SPI方式的IO配置,然后使用相应的驱动代码来实现功能。 W25Q128是一款SPI接口的NOR Flash芯片,具有128 Mbit的存储空间,相当于16M字节。NOR Flash是一种常用的用于存储数据的半导体器件,具有容量大、可重复擦写、按扇区/块擦除、掉电后数据可继续保存的特性。Flash的物理特性是只能写0,不能直接写1,写1需要进行擦除操作。 根据引用\[3\]中的实验,可以通过硬件接线将W25Q128模块与STM32连接起来,其中VCC接3.3V,CS接PA4,CLK接PA5,DO接PA6,DI接PA7。然后可以使用CubeMX进行相应的配置。 要获取W25Q128的ID,可以使用SPI通信协议来读取芯片的ID寄存器。具体的代码实现可以参考引用\[1\]中提供的驱动代码。 #### 引用[.reference_title] - *1* [STM32CubeMX | STM32使用HAL库模拟SPI方式驱动W25Q128存芯片](https://blog.csdn.net/qq153471503/article/details/106895933)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [STM32SPIW25Q128](https://blog.csdn.net/weixin_49001476/article/details/130909856)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值