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

文章目录

  • 前言
  • 一、SPI 接口和通讯协议
  • 1.1 什么是SPI
  • 1.2 SPI 的引脚信息
  • 1.3 SPI 的工作原理(了解即可)
  • 1.4 SPI 传输协议
  • 1.5 STM32F407的SPI接口
  • 二、SPI 的HAL库驱动程序
  • 2.1 SPI 寄存器操作的宏函数
  • 2.2 SPI 初始化和阻塞器数据传输
  • 2.2.1 SPI 接口初始化
  • 2.2.2 阻塞式数据发送和接收
  • 2.3 中断方式发送数据
  • 2.4 DMA方式数据传输
  • 三、Flash存储芯片 W25Q128
  • 3.1 硬件接口和连接
  • 3.2 存储空间划分
  • 3.3 数据读写的原则
  • 3.4 操作指令
  • 3.4.1 "写使能"指令
  • 3.4.2 “读数据”指令
  • 3.4.3 “写数据”指令
  • 3.5 状态寄存器
  • 四、示例:轮询方式读写W25Q128
  • 4.1 实例功能
  • 4.2 CubeMX配置
  • 4.2.1 SPI1的CubeMx设置
  • 4.2.2 其余GPIO引脚的配置
  • 4.3 程序设计
  • 4.3.1 SPI1初始化
  • 4.3.3 W25Q128的驱动程序
  • 4.3 W25Q128的功能描述
  • 4.3.1 主程序
  • 4.3.1 W25Q128功能测试函数的实现
  • 4.4 示例结果
  • 五、总结

  • 前言

    一、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电平 采样时刻
    模式0 0 0 低电平 第1个跳变沿
    模式1 0 1 低电平 第2个跳变沿
    模式2 1 0 高电平 第1个跳变沿
    模式3 1 1 高电平 第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_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个中断模式传输函数都可能产生SPI_IT_ERR中断事件 SPI_IT_ERR HAL_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 FLASH NAND FLASH
    容量 较小 较大
    同容量存储器成本 较贵 较便宜
    擦除单元 以“扇区/块”擦除 以“扇区/块”擦除
    读写单元 可以基于字节读写 必须以“块”为单位读写
    读取速度 较高 较低
    写入速度 较低 较高
    集成度 较低 较高
    介质类型 随机存储 连续存储
    地址线和数据线 独立分开 共用
    坏块 较少 较多
    是否支持XIP 支持 不支持
    应用举例 25Qxx、程序ROM EMMC、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指令码 BYTE2 BYTE3 BYTE4 BYTE5 BYTE6
    写使能 0x06 —— —— —— —— ——
    读状态寄存器1 0x05 (S7~S0) —— —— —— ——
    读状态寄存器2 0x35 (S15~S8) —— —— —— ——
    读厂家和设备ID 0x90 dummy dummy 0x00 (MF7~MF0) (ID7~ID0)
    读64位序列号 0x4B dummy dummy dummy dummy (ID63 ~ ID0)
    器件擦除 0xC7/0x60 —— —— —— —— ——
    块擦除 0xD8 A23~A16 A15~A8 A7~A0 —— ——
    扇区擦除 0x20 A23~A16 A15~A8 A7~A0 —— ——
    写数据(页编程) 0x02 A23~A16 A15~A8 A7~A0 D7~D0 ——
    读数据 0x03 A23~A16 A15~A8 A7~A0 (D7~D0) ——
    快速读数据 0x0B A23~A16 A15~A8 A7~A0 dummy (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各个位的定义

    位编号 位名称 功能说明 存储特性 读/写特性
    S7 SRP0 状态寄存器保护位0 非易失 可写
    S6 SEC 扇区保护 非易失 可写
    S5 TB 顶/底保护 非易失 可写
    S4 BP2 块保护2 非易失 可写
    S3 BP1 快保护1 非易失 可写
    S2 BP0 快保护0 非易失 可写
    S1 WEL 写使能锁存 易失 可写
    S0 BUSY 有正在进行的擦除或写操作 易失 只读

      通过读状态寄存器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 探索者开发指南》


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

    作者:printf_01

    物联沃分享整理
    物联沃-IOTWORD物联网 » HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

    发表回复