STM32 QSPI通信学习笔记:详解与实践

一、SPI Flash与QSPI FLASH

1、首先说说FLASH,不管是QSPI Flash还是SPI Flash说的实际上是一种闪存芯片,比如最常见的W25Q128(下图),真正不同的是SPI协议与QSPI协议罢了。可以看到下图的芯片,2、3、5、7这四个引脚是可以复用的,根据不同的通信协议变换功能。

W25Q128 是华邦公司推出的大容量 SPI FLASH 产品,W25Q128 的容量为 128Mb,该系列还有 W25Q80/16/32/64 等。

W25Q128 将 16M 的容量分为 256 个块(Block),每个块大小为 64K 字节,每个块又分为16 个扇区(Sector),每个扇区 4K 个字节。W25Q128 的最小擦除单位为一个扇区,也就是每次必须擦除 4K 个字节。这样我们需要给 W25Q128 开辟一个至少 4K 的缓存区,这样对 SRAM 要求比较高,要求芯片必须有 4K 以上 SRAM 才能很好的操作(SRAM:静态随机存取存储器(Static Random-Access Memory,SRAM)是随机存取存储器的一种。所谓的“静态”,是指这种存储器只要保持通电,里面储存的数据就可以恒常保持。)。

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

  1. SPI与FLASH的通信方式:

SPI 是英语Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口。是Motorola(摩托罗拉)首先在其MC68HCXX系列处理器上定义的。

SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,主要应用在 EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。

如下图所示,标椎的SPI接口一般使用四条信号线通信:

SDI(数据输入),SDO(数据输出),SCK(时钟),CS(片选)

MISO: 主设备输入/从设备输出引脚。该引脚在从模式下发送数据,在主模式下接收数据。

MOSI: 主设备输出/从设备输入引脚。该引脚在主模式下发送数据,在从模式下接收数据。

SCLK:串行时钟信号,由主设备产生。

CS/SS:从设备片选信号,由主设备控制。它的功能是用来作为“片选引脚”,也就是选择指定的从设备,让主设备可以单独地与特定从设备通讯,避免数据线上的冲突。

  1. QSPI与FLASH的通信方式:

所谓QSPI是Queued SPI的简写,是Motorola公司推出的SPI接口的扩展,比SPI应用更加广泛。在SPI协议的基础上,Motorola公司对其功能进行了增强,增加了队列传输机制,推出了队列串行外围接口协议(即QSPI协议)。QSPI 是一种专用的通信接口,连接单、双或四(条数据线)SPI Flash 存储介质。

该接口可以在以下三种模式下工作:

① 间接模式:使用 QSPI 寄存器执行全部操作

② 状态轮询模式:周期性读取外部 Flash 状态寄存器,而且标志位置 1 时会产生中断(如擦除或烧写完成,会产生中断)

③ 内存映射模式:外部 Flash 映射到微控制器地址空间,从而系统将其视作内部存储器

采用双闪存模式时,将同时访问两个 Quad-SPI Flash,吞吐量和容量均可提高二倍。

二、QSPI协议功能框图

QSPI 使用 6 个信号连接Flash,分别是四个数据线BK1_IO0~BK1_IO3,一个时钟输出CLK,一个片选输出(低电平有效)BK1_nCS,它们的作用介绍如下:

(1) BK1_nCS:片选输出(低电平有效),适用于 FLASH 1。如果 QSPI 始终在双闪存模式下工作,则其也可用于 FLASH 2从设备选择信号线。QSPI通讯以BK1_nCS线置低电平为开始信号,以BK1_nCS线被拉高作为结束信号。

(2) CLK:时钟输出,适用于两个存储器,用于通讯数据同步。它由通讯主机产生,决定了通讯的速率,不同的设备支持的最高时钟频率不一样,如STM32的QSPI时钟频率最大为fpclk/2,两个设备之间通讯时,通讯速率受限于低速设备。

(3) BK1_IO0:在双线 / 四线模式中为双向 IO,单线模式中为串行输出。

(4) BK1_IO1:在双线 / 四线模式中为双向 IO,单线模式中为串行输入。

(5) BK1_IO2:在四线模式中为双向 IO。

(6) BK1_IO3:在四线模式中为双向 IO。

二、QSPI初始化结构体详解

跟其它外设一样,STM32 HAL库提供了QSPI初始化结构体及初始化函数来配置SPI外设。初始化结构体及函数定义在库文件“stm32f7xx_hal_spi.h”及“stm32f7xx_hal _spi.c”中,编程时我们可以结合这两个文件内的注释使用或参考库帮助文档。了解初始化结构体后我们就能对SPI外设运用自如了,见代码清单 241。

a、 QSPI_InitTypeDef初始化结构体

1 typedef struct {
 2     uint32_t ClockPrescaler;     //预分频因子
 3     uint32_t FifoThreshold;      //FIFO中的阈值
 4     uint32_t SampleShifting;     //采样移位
 5     uint32_t FlashSize;          //Flash大小
 6     uint32_t ChipSelectHighTime; //片选高电平时间
 7     uint32_t ClockMode;          //时钟模式
 8     uint32_t FlashID;            //Flash ID
 9     uint32_t DualFlash;          //双闪存模式
10 } QSPI_InitTypeDef;

这些结构体成员说明如下,其中括号内的文字是对应参数在STM32 HAL库中定义的宏:

(1) ClockPrescaler

本成员设置预分频因子,对应寄存器QUADSPI_CR [31:24]即PRESCALER[7:0],取值范围是0—255,可以实现1—256级别的分频。仅可在 BUSY = 0 时修改该字段。

(2) FifoThreshold

本成员设置FIFO 阈值级别,对应寄存器QUADSPI_CR [12:8]即FTHRES[4:0],定义在间接模式下 FIFO 中将导致 FIFO 阈值标志(FTF,QUADSPI_SR[2])置 1 的字节数阈值。

(3) SampleShifting

本成员设置采样,对应寄存器QUADSPI_CR [4],默认情况下,QUADSPI 在 Flash 驱动数据后过半个 CLK 周期开始采集数据。使用该位,可考虑外部信号延迟,推迟数据采集。可以取值0:不发生移位;1:移位半个周期。在 DDR 模式下 (DDRM = 1),固件必须确保 SSHIFT = 0。

(4) FlashSize

本成员设置FLASH大小,对应寄存器QUADSPI_CCR [20:16]的FSIZE[4:0]位。定义外部存储器的大小,简介模式Flash容量最高可达4GB(32位寻址),但是在内存映射模式下限制为256MB,如果是双闪存则可以达到512MB。

(5) ChipSelectHighTime

本成员设置片选高电平时间,对应寄存器QUADSPI_CR [10:8]的CSHT[2:0]位,定义片选 (nCS) 在发送至 Flash 的命令之间必须保持高电平的最少 CLK 周期数。可以取值1~8个周期。

(6) ClockMode

本成员设置时钟模式,对应寄存器QUADSPI_CR [0]位,指示CLK在命令之间的电平,可以选模式0,1: nCS 为高电平(片选释放)时,CLK 必须保持低电平;或者模式3 ,1:nCS 为高电平(片选释放)时,CLK 必须保持高电平。

(7) FlashID

本成员用于选择Flash1或者Flash2,单闪存模式下选择需要访问的flash。

(8) DualFlash

本成员用于激活双闪存模式,0:禁止双闪存模式;1:使能双闪存模式。双闪存模式可以使系统吞吐量和容量扩大一倍。

b、QSPI_CommandTypeDe通信配置命令结构体

1 typedef struct {
 2     uint32_t Instruction;        //指令
 3     uint32_t Address;            //地址
 4     uint32_t AlternateBytes;     //交替字节
 5     uint32_t AddressSize;        //地址长度
 6     uint32_t AlternateBytesSize; //交替字节长度
 7     uint32_t DummyCycles;        //空指令周期
 8     uint32_t InstructionMode;    //指令模式
 9     uint32_t AddressMode;        //地址模式
10     uint32_t AlternateByteMode;  //交替字节模式
11     uint32_t DataMode;           //数据模式
12     uint32_t NbData;             //数据长度
13     uint32_t DdrMode;            //双倍数据速率模式
14     uint32_t DdrHoldHalfCycle;   //DDR保持周期
15     uint32_t SIOOMode;           //仅发送指令一次模式
16 } QSPI_CommandTypeDef; 

这些结构体成员说明如下,其中括号内的文字是对应参数在STM32 HAL库中定义的宏:

(1) Instruction

本成员设置通信指令,指定要发送到外部 SPI 设备的指令。仅可在 BUSY = 0 时修改该字段。

(2) Address

本成员指定要发送到外部 Flash 的地址,BUSY = 0 或 FMODE = 11(内存映射模式)时,将忽略写入该字段。在双闪存模式下,由于地址始终为偶地址,ADDRESS[0] 自动保持为“0”。

(3) AlternateBytes

本成员指定要在地址后立即发送到外部 SPI 设备的可选数据,仅可在 BUSY = 0 时修改该字段。

(4) AddressSize

本成员定义地址长度,可以是8位,16位,24位或者32位。

(5) AlternateBytesSize

本成员定义交替字节长度,可以是8位,16位,24位或者32位。

(6) DummyCycles

本成员定义空指令阶段的持续时间,在 SDR 和 DDR 模式下,它指定 CLK 周期数 (0-31)。

(7) InstructionMode

本成员定义指令阶段的操作模式,00:无指令;01:单线传输指令;10:双线传输指令;11:四线传输指令。

(8) AddressMode

本成员定义地址阶段的操作模式,00:无地址;01:单线传输地址;10:双线传输地址;11:四线传输地址。

(9) AlternateByteMode

本成员定义交替字节阶段的操作模式00:无交替字节;01:单线传输交替字节;10:双线传输交替字节;11:四线传输交替字节。

(10) DataMode

本成员定义数据阶段的操作模式,00:无数据;01:单线传输数据;10:双线传输数据;11:四线传输数据。该字段还定义空指令阶段的操作模式。

(11) NbData

本成员设置数据长度,在间接模式和状态轮询模式下待检索的数据数量(值 + 1)。对状态轮询模式应使用不大于 3 的值(表示 4 字节)。

(12) DdrMode

本成员为地址、交替字节和数据阶段设置 DDR 模式,0:禁止 DDR 模式;1:使能 DDR 模式。

(13) DdrHoldHalfCycle

本成员设置DDR 模式下数据输出延迟 1/4 个 QUADSPI 输出时钟周期,0:使用模拟延迟来延迟数据输出;1:数据输出延迟 1/4 个 QUADSPI 输出时钟周期。仅在 DDR 模式下激活。

(14) SIOOMode

本成员设置仅发送指令一次模式,。IMODE = 00 时,该位不起作用。0:在每个事务中发送指令;1:仅为第一条命令发送指令。

三、QSPI—读写串行FLASH实验

(1) 初始化通讯使用的目标引脚及端口时钟;

      GPIO_InitTypeDef GPIO_InitStruct;

     /* 使能 QSPI 及 GPIO 时钟 */
     QSPI_FLASH_CLK_ENABLE();
     QSPI_FLASH_CLK_GPIO_ENABLE();
     QSPI_FLASH_BK1_IO0_CLK_ENABLE();
     QSPI_FLASH_BK1_IO1_CLK_ENABLE();
     QSPI_FLASH_BK1_IO2_CLK_ENABLE();
     QSPI_FLASH_BK1_IO3_CLK_ENABLE();
     QSPI_FLASH_CS_GPIO_CLK_ENABLE();

(2) 配置SPI外设的模式、地址、速率等参数并使能SPI外设;

     /* QSPI_FLASH 模式配置 */
     QSPIHandle.Instance = QUADSPI;
     QSPIHandle.Init.ClockPrescaler = 2;
     QSPIHandle.Init.FifoThreshold = 4;
     QSPIHandle.Init.SampleShifting = QSPI_SAMPLE_SHIFTING_HALFCYCLE;
     QSPIHandle.Init.FlashSize = 23;
     QSPIHandle.Init.ChipSelectHighTime = QSPI_CS_HIGH_TIME_8_CYCLE;
     QSPIHandle.Init.ClockMode = QSPI_CLOCK_MODE_0;
     HAL_QSPI_Init(&QSPIHandle);

(3)初始化QSPI接口;

uint8_t BSP_QSPI_Init(void)

  {

      QSPI_CommandTypeDef s_command;

      uint8_t value = W25Q128FV_FSR_QE;

     /* QSPI存储器复位 */

     if (QSPI_ResetMemory() != QSPI_OK) {

         return QSPI_NOT_SUPPORTED;

     }

     /* 使能写操作 */

     if (QSPI_WriteEnable() != QSPI_OK) {

         return QSPI_ERROR;

     }

     /* 设置四路使能的状态寄存器,使能四通道IO2和IO3引脚 */

     s_command.InstructionMode   = QSPI_INSTRUCTION_1_LINE;

     s_command.Instruction       = WRITE_STATUS_REG2_CMD;

     s_command.AddressMode       = QSPI_ADDRESS_NONE;

     s_command.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;

     s_command.DataMode          = QSPI_DATA_1_LINE;

     s_command.DummyCycles       = 0;

     s_command.NbData            = 1;

     s_command.DdrMode           = QSPI_DDR_MODE_DISABLE;

     s_command.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;

     s_command.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;

     /* 配置命令 */

if (HAL_QSPI_Command(&QSPIHandle, &s_command, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK){

         return QSPI_ERROR;

     }

     /* 传输数据 */

if (HAL_QSPI_Transmit(&QSPIHandle, &value, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK){

         return QSPI_ERROR;

     }

    /* 自动轮询模式等待存储器就绪 */

     if (QSPI_AutoPollingMemReady(W25Q128FV_SUBSECTOR_ERASE_MAX_TIME) != QSPI_OK) {

         return QSPI_ERROR;

     }
     return QSPI_OK;
 }

(4) 编写基本SPI按字节收发的函数;

 /**接收函数
    * @brief  从QSPI存储器中读取大量数据.
    * @param  pData: 指向要读取的数据的指针
    * @param  ReadAddr: 读取起始地址
    * @param  Size: 要读取的数据大小
    * @retval QSPI存储器状态
    */
 uint8_t BSP_QSPI_Read(uint8_t* pData, uint32_t ReadAddr, uint32_t Size)
  {
     QSPI_CommandTypeDef s_command;
     /* 初始化读命令 */
     s_command.InstructionMode   = QSPI_INSTRUCTION_1_LINE;
     s_command.Instruction       = READ_CMD;
     s_command.AddressMode       = QSPI_ADDRESS_1_LINE;
     s_command.AddressSize       = QSPI_ADDRESS_24_BITS;
     s_command.Address           = ReadAddr;
     s_command.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
     s_command.DataMode          = QSPI_DATA_1_LINE;
     s_command.DummyCycles       = 0;
     s_command.NbData            = Size;
     s_command.DdrMode           = QSPI_DDR_MODE_DISABLE;
     s_command.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;
     s_command.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;

     /* 配置命令 */
if (HAL_QSPI_Command(&QSPIHandle, &s_command, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK){
         return QSPI_ERROR;
     }

     /* 接收数据 */
     if(HAL_QSPI_Receive(&QSPIHandle, pData, HAL_QPSI_TIMEOUT_DEFAULT_VALUE)!= HAL_OK) {
         return QSPI_ERROR;
     }
     return QSPI_OK;
 }



发送函数
 /**

   * @brief  将大量数据写入QSPI存储器

   * @param  pData: 指向要写入数据的指针

   * @param  WriteAddr: 写起始地址

   * @param  Size: 要写入的数据大小

   * @retval QSPI存储器状态

   */

 uint8_t BSP_QSPI_Write(uint8_t* pData, uint32_t WriteAddr, uint32_t Size)

 {
     QSPI_CommandTypeDef s_command;

     uint32_t end_addr, current_size, current_addr;

     /* 计算写入地址和页面末尾之间的大小 */

     current_addr = 0;

     while (current_addr <= WriteAddr) {

         current_addr += W25Q128FV_PAGE_SIZE;

     }

     current_size = current_addr - WriteAddr;

     /* 检查数据的大小是否小于页面中的剩余位置 */

     if (current_size > Size) {

         current_size = Size;

     }

     /* 初始化地址变量 */

     current_addr = WriteAddr;

     end_addr = WriteAddr + Size;

     /* 初始化程序命令 */

     s_command.InstructionMode   = QSPI_INSTRUCTION_1_LINE;

     s_command.Instruction       = QUAD_INPUT_PAGE_PROG_CMD;

     s_command.AddressMode       = QSPI_ADDRESS_1_LINE;

     s_command.AddressSize       = QSPI_ADDRESS_24_BITS;

     s_command.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;

     s_command.DataMode          = QSPI_DATA_4_LINES;

     s_command.DummyCycles       = 0;

     s_command.DdrMode           = QSPI_DDR_MODE_DISABLE;

     s_command.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;

     s_command.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;

     /* 逐页执行写入 */

     do {

         s_command.Address = current_addr;

         s_command.NbData  = current_size;

         /* 启用写操作 */

         if (QSPI_WriteEnable() != QSPI_OK) {

             return QSPI_ERROR;
         }

         /* 配置命令 */

if(HAL_QSPI_Command(&QSPIHandle, &s_command, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK) {

             return QSPI_ERROR;

         }

        /* 传输数据 */

if(HAL_QSPI_Transmit(&QSPIHandle, pData, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK) {

             return QSPI_ERROR;
         }

         /* 配置自动轮询模式等待程序结束 */

         if(QSPI_AutoPollingMemReady(HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != QSPI_OK) {

             return QSPI_ERROR;

         }

         /* 更新下一页编程的地址和大小变量 */

         current_addr += current_size;

         pData += current_size;

         current_size = ((current_addr + W25Q128FV_PAGE_SIZE) > end_addr) ? (end_addr-current_addr) : W25Q128FV_PAGE_SIZE;

     } while (current_addr < end_addr);

     return QSPI_OK;
 }

(5) 编写对FLASH擦除及读写操作的的函数;

由于FLASH存储器的特性决定了它只能把原来为“1”的数据位改写成“0”,而原来为“0”的数据位不能直接改写为“1”。所以这里涉及到数据“擦除”的概念,在写入前,必须要对目标存储矩阵进行擦除操作,把矩阵中的数据位擦除为“1”,在数据写入的时候,如果要存储数据“1”,那就不修改存储矩阵 ,在要存储数据“0”时,才更改该位。

通常,对存储矩阵擦除的基本操作单位都是多个字节进行,如本例子中的FLASH芯片支持“扇区擦除”、“块擦除”以及“整片擦除”,

扇区擦除指令的第一个字节为指令编码,紧接着发送的3个字节用于表示要擦除的存储矩阵地址。要注意的是在扇区擦除指令前,还需要先发送“写使能”指令,发送扇区擦除指令后,通过读取寄存器状态等待扇区擦除操作完毕。

/**
    * @brief  擦除QSPI存储器的指定块
    * @param  BlockAddress: 需要擦除的块地址
    * @retval QSPI存储器状态
    */
uint8_t BSP_QSPI_Erase_Block(uint32_t BlockAddress)
{
      QSPI_CommandTypeDef s_command;
      /* 初始化擦除命令 */
     s_command.InstructionMode   = QSPI_INSTRUCTION_1_LINE;
     s_command.Instruction       = SECTOR_ERASE_CMD;
     s_command.AddressMode       = QSPI_ADDRESS_1_LINE;
     s_command.AddressSize       = QSPI_ADDRESS_24_BITS;
     s_command.Address           = BlockAddress;
     s_command.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
     s_command.DataMode          = QSPI_DATA_NONE;
     s_command.DummyCycles       = 0;
     s_command.DdrMode           = QSPI_DDR_MODE_DISABLE;
     s_command.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;
     s_command.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;
     /* 启用写操作 */
     if (QSPI_WriteEnable() != QSPI_OK) {
         return QSPI_ERROR;
     }

     /* 发送命令 */
if(HAL_QSPI_Command(&QSPIHandle, &s_command, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK) {
         return QSPI_ERROR;
     }

     /* 配置自动轮询模式等待擦除结束 */
     if (QSPI_AutoPollingMemReady(W25Q128FV_SUBSECTOR_ERASE_MAX_TIME) != QSPI_OK) {
         return QSPI_ERROR;
     }
     return QSPI_OK;
}
物联沃分享整理
物联沃-IOTWORD物联网 » STM32 QSPI通信学习笔记:详解与实践

发表评论