21. QSPI—读写串行FLASH

本章参考资料:《dm00327659-stm32mp157-advanced-armbased-32bit-mpus-stmicroelectronics》、 《SPI总线协议介绍》。

若对SPI通讯协议不了解,可先阅读《SPI总线协议介绍》文档的内容学习。

关于FLASH存储器,请参考“常用存储器介绍”章节,实验中FLASH芯片的具体参数,请参考其规格书《W25Q128》来了解。

21.1. QSPI协议简介

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

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

  1. 间接模式:使用 QSPI 寄存器执行全部操作,即通过读取QUADSPI_DR寄存器的内容完成接受数据, 将写入到QUADSPI_DR寄存器的内容发送给外部SPI设备。

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

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

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

21.1.1. QSPI功能框图

禁止双闪存模式的QSPI功能框图见图 QUADSPI功能框图

QUADSPI功能框图

21.1.1.1. 通讯引脚

我们的开发板采用的是双闪存禁止模式连接单片QSPI Flash。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 :时钟输出,适用于两个存储器,用于通讯数据同步。它由通讯主机产生,决定了通讯的速率, 不同的设备支持的最高时钟频率不一样, 如STM32MP157的QSPI时钟频率最大为266.5M,两个设备之间通讯时,通讯速率受限于低速设备。

3) BK1_IO0 :在双线/四线模式中为双向IO,或者是单线模式中为串行输出,相当于传统SPI的MOSI线,适用于FLASH 1。

4) BK1_IO1 :在双线/四线模式中为双向IO,或者是单线模式中为串行输入,相当于传统SPI的MISO线,适用于FLASH 1 。

5) BK1_IO2 :在四线模式中为双向 IO,适用于FLASH 1。

6) BK1_IO3 :在四线模式中为双向 IO,适用于FLASH 1。

21.1.1.2. 内部信号线

QSPI的内部信号有五根,分别是内核时钟quadspi_ker_clk,外设时钟线quadspi_hclk,中断quadspi_it, FIFO阈值触发MDAM信号quadsqp_ft_trg以及操作完成触发MDMA信号quadspi_tc_trg。

  1. quadspi_ker_clk:QUADSPI的内核时钟,主要用于产生通讯过程中的时钟。

  2. quadspi_hclk:QUADSPI的外设时钟,来自于HCLK6,时钟频率最高为266MHz;

  3. quadspi_it:中断请求信号线。在达到 FIFO阈值、超时、操作完成以及发送访问错误时产生中断

  4. quadsqp_ft_trg,quadspi_tc_trg:达到FIFO阈值时或者操作完成时触发MDMA请求。

21.1.2. QSPI命令序列

QUADSPI 通过命令与 Flash 通信。每条命令包括指令、地址、交替字节、空指令和数据这五个阶段 , 任一阶段均可通过配置QUADSPI_CCR寄存器相关字段跳过, 但至少要包含指令、地址、交替字节或数据阶段之一。nCS在每条命令开始前拉低,在每条命令完成后再次拉高。 下面看看QSPI四线模式下的读命令时序, 见图 四线模式下的读命令时序

四线模式下的读命令时序

21.1.2.1. 指令阶段

指令阶段,将一条8位指令发送到外部SPI设备,该指令值可以在 QUADSPI_CCR[7:0] 寄存器的 INSTRUCTION 字段中进行配置, 指定待执行操作的类型。

尽管大多数Flash 从 IO0/SO 信号(单线SPI模式)只能以一次1位的方式接收指令, 但指令阶段可选择一次发送2位(在双线SPI模式中通过 IO0/IO1)或一次发送4位(在四线SPI模式中通过 IO0/IO1/IO2/IO3)。 这可通过QUADSPI_CCR[9:8] 寄存器中的 IMODE[1:0]字段进行配置。

若寄存器QUADSPI_CCR的字段IMODE = 00,则跳过指令阶段,命令序列从地址阶段(如果存在)开始。

21.1.2.2. 地址阶段

在地址阶段,将1-4字节发送到Flash,表明要操作的地址。 待发送的地址字节数通过QUADSPI_CCR[13:12]寄存器的ADSIZE[1:0]字段中进行配置。 在间接模式和自动轮询模式下,待发送的地址字节通过在QUADSPI_AR寄存器的ADDRESS[31:0]进行配置。 而在内存映射模式下,则通过 AXI直接给出地址。

地址阶段可一次发送1 位(在单线SPI模式中通过SO),一次发送2位(在双线SPI模式中通过IO0/IO1)或一次发送 4位(在四线SPI模式中通过 IO0/IO1/IO2/IO3)。 这可通过QUADSPI_CCR[11:10]寄存器中的ADMODE[1:0]字段进行配置。

若寄存器QUADSPI_CCR的字段ADMODE = 00,则跳过地址阶段,命令序列直接进入下一阶段(如果存在)。

21.1.2.3. 交替字节阶段

在交替字节阶段,将1-4字节发送到Flash,一般用于控制操作模式。 待发送的交替字节数在 QUADSPI_CCR[17:16] 寄存器的 ABSIZE[1:0] 字段中进行配置。 待发送的字节在QUADSPI_ABR 寄存器中指定。

交替字节阶段可一次发送1位(在单线SPI模式中通过 SO)、2位(在双线 SPI 模式中通过 IO0/IO1) 或4位(在四线SPI模式中通过 IO0/IO1/IO2/IO3)。 这可通过QUADSPI_CCR[15:14]寄存器中的ABMODE[1:0]字段进行配置。

若ABMODE = 00,则跳过交替字节阶段,命令序列直接进入下一阶段(如果存在)。 在交替字节阶段存在仅需发送单个半字节而不是一个全字节的情况下, 比如采用双线模式并且仅使用两个周期发送交替字节时。在这种情况下, 采用四线模式 (ABMODE = 11)并通过IO0/IO1发送一个半字节2(0010), 方法是 ALTERNATE 的位7和3置“1”(IO3 保持高电平)且位6和2置“0”(IO2 线保持低电平)。 此时,半字节的高2位存放在ALTERNATE的位4:3,低2位存放在位1和0中。则ALTERNATE应设置为 0x8A (1000_1010)。

21.1.2.4. 空指令周期阶段

在空指令周期阶段,给定的1-31个周期内不发送或接收任何数据,目的是当采用更高的时钟频率时,给Flash留出准备数据阶段的时间。 这一阶段中给定的周期数在QUADSPI_CCR[22:18]寄存器的DCYC[4:0]字段中指定。在SDR和DDR模式下,持续时间被指定为一定个数的全时钟周期。 若DCYC为零,则跳过空指令周期阶段,命令序列直接进入数据阶段(如果存在)。空指令周期阶段的操作模式由DMODE确定。 为确保数据信号从输出模式转变为输入模式有足够的“周转”时间,使用双线和四线模式从Flash接收数据时,至少需要指定一个空指令周期。

21.1.2.5. 数据阶段

在数据阶段,可从Flash接收或向其发送任意数量的字节。

在间接模式和自动轮询模式下,待发送/接收的字节数在QUADSPI_DLR寄存器中指定。

在间接模式下,写入的话,发送到Flash的数据写入到QUADSPI_DR寄存器。进行读取的话,通过读取QUADSPI_DR寄存器获得从Flash发送过来的数据。

在内存映射模式下,读取的数据通过AXI直接发送回Cortex或DMA。

数据阶段可一次发送/接收1位(在单线SPI模式中通过 SO)、2位(在双线SPI模式中通过 IO0/IO1)或4位(在四线SPI模式中通过 IO0/IO1/IO2/IO3)。 这可通过QUADSPI_CCR[15:14] 寄存器中的 ABMODE[1:0] 字段进行配置。若DMODE = 00,则跳过数据阶段, 命令序列在拉高 nCS 时立即完成。这一配置仅可用于仅间接写入模式。

21.2. QUADSPI 信号接口协议模式

21.2.1. 单线 SPI 模式

传统SPI模式,即单线SPI模式,通过串行的方式发送/接收数据。在此模式下,数据通过SO信号(IO0引脚)发送到Flash。 从Flash接收到的数据通过SI(IO1引脚)送达。通过将(QUADSPI_CCR中的)IMODE/ADMODE/ABMODE/DMODE字段设置为01, 可对不同的命令阶段分别进行配置,以使用此单个位模式。在每个已配置为单线模式的阶段中:

  • IO0(SO)处于输出模式

  • IO1(SI)处于输入模式(高阻抗)

  • IO2处于输出模式并强制置“0”(以禁止“写保护”功能)

  • IO3处于输出模式并强制置“1”(以禁止“保持”功能)

若DMODE=01,这对于空指令阶段也同样如此。

21.2.2. 双线 SPI 模式

在双线模式下,通过IO0/IO1信号同时发送/接收两位。

通过将QUADSPI_CCR寄存器的 IMODE/ADMODE/ABMODE/DMODE 字段设置为 10,可对不同的命令阶段分别进行配置, 以使用双线SPI模式。在每个已配置为双线模式的阶段中:

  • IO0/IO1在数据阶段进行读取操作时处于高阻态(输入),在其他情况下为输出

  • IO2处于输出模式并强制置“0”

  • IO3处于输出模式并强制置“1”

在空指令阶段,若DMODE=01,则 IO0/IO1 始终保持高阻态。

21.2.3. 四线 SPI 模式

在四线模式下,通过 IO0/IO1/IO2/IO3 信号同时发送/接收四位。通过将 QUADSPI_CCR 寄存器的 IMODE/ADMODE/ABMODE/DMODE 字段设置为 11, 可对不同的命令阶段分别进行配置,以使用四线 SPI模式。 在每个已配置为四线模式的阶段中IO0/IO1/IO2/IO3 在数据阶段进行读取操作时均处于高阻态(输入), 在其他情况下为输出。在空指令阶段中,若DMODE=11,则IO0/IO1/IO2/IO3均为高阻态。IO2和IO3仅用于Quad SPI模式。 如果未配置任何阶段使用四线SPI模式,即使QUADSPI激活,对应IO2和IO3的引脚也可用于其他功能。

21.2.4. SDR 模式

默认情况下,DDRM位 (QUADSPI_CCR[31]) 为0,QUADSPI在单倍数据速率(SDR)模式下工作。在SDR模式下, 当QUADSPI驱动IO0/SO、IO1、IO2、IO3信号时,这些信号仅在CLK的下降沿发生转变。在SDR模式下接收数据时, QUADSPI假定Flash也通过CLK的下降沿发送数据。默认情况下 (SSHIFT=0时), 将使用CLK后续的边沿(上升沿)对信号进行采样。

21.2.5. DDR 模式

若DDRM位(QUADSPI_CCR[31]) 置1,则QUADSPI在双倍数据速率 (DDR) 模式下工作。在DDR模式下, 当QUADSPI在地址/交替字节/数据阶段驱动IO0/SO、IO1、IO2、IO3信号时,将在CLK的每个上升沿和下降沿发送1位。 指令阶段不受DDRM的影响。始终通过CLK的下降沿发送指令。在DDR模式下接收数据时, QUADSPI假定Flash通过CLK的上升沿和下降沿均发送数据。若DDRM=1,固件必须清零SSHIFT位 (QUADSPI_CR[4])。 因此,在半个CLK周期后(下一个反向边沿)对信号采样。四线模式下DDR命令时序见图 四线模式下DDR命令时序

四线模式下DDR命令时序

21.3. QUADSPI 间接模式

在间接模式下,通过读写QUADSPI寄存器(QUADSPI_CCR)来发送命令; 并通过读写数据寄存器来传输数据,就如同对待其他通信外设那样。

若FMODE=00(QUADSPI_CCR[27:26]),则QUADSPI处于间接写入模式,数据在数据阶段中发送给Flash, 是通过写入数据寄存器(QUADSPI_DR)的方式来发送数据的。

若FMODE=01,则QUADSPI处于间接读取模式,在数据阶段中从Flash接收数据。是通过读取QUADSPI_DR来获取数据。

读取/写入的字节数由数据长度寄存器QUADSPI_DLR决定。

如果QUADSPI_DLR=0xFFFF_FFFF(全为“1”),则数据长度视为未定义,QUADSPI将继续传输数据, 直到到达(由FSIZE定义的)Flash 的结尾。 如果不传输任何字节,DMODE(QUADSPI_CCR[25:24])应设置为00。如果QUADSPI_DLR=0xFFFF_FFFF 并且FSIZE=0x1F(最大值为 4GB 的Flash),在此特殊情况下,传输将无限继续下去, 仅在出现终止请求或QUADSPI被禁止后停止。在读取最后一个存储器地址后(地址为 0xFFFF_FFFF), 将从地址=0x0000_0000开始继续读取。

将根据QUADSPI_CR中定义的Flash大小,在达到外部SPI的限制时,TCF置1。 当发送或接收的字节数达到编程设定值时,如果TCIE=1,则TCF置1并产生中断。在数据数量不确定的情况下,

21.3.1. 触发命令启动

从本质上讲,在用户给出命令时,命令会立即发送。根据QUADSPI的配置,在间接模式下有三种触发命令启动的方式。在出现以下情形时,命令立即启动:

  1. 对INSTRUCTION[7:0] (QUADSPI_CCR) 执行写入操作,如果不需要地址阶段(当ADMODE=00)以及数据阶段(当FMODE=01或DMODE=00)时;

  2. 对ADDRESS[31:0](QUADSPI_AR)执行写入操作,如果需要发送地址(当ADMODE=00)但不需要数据阶段(当FMODE=01或DMODE=00)时;

  3. 对DATA[31:0](QUADSPI_DR)执行写入操作,需要发送地址(当 ADMODE!=00)并且需要数据阶段时(当FMODE=00并且DMODE!=00)。

写入交替字节寄存器(QUADSPI_ABR)始终不会触发命令启动。如果需要交替字节,必须预先进行编程。如果命令启动,BUSY位(QUADSPI_SR的位5)将自动置1。

21.3.2. FIFO 和数据管理

在间接模式中,数据将通过QUADSPI内部的一个32字节FIFO。FLEVEL[5:0](QUADSPI_SR[13:8])指示FIFO目前保存了多少字节。

在间接写入模式下(FMODE=00),软件写入QUADSPI_DR时,将发送给FIFO。字写入将在FIFO中增加4个字节,半字写入增加2个字节, 而字节写入仅增加1个字节。如果软件在FIFO中加入的数据过多(超过DL[31:0]指示的值),将在写入操作结束(TCF置1)时从FIFO中清除超出的字节。

对QUADSPI_DR的字节/半字访问必须仅针对该32位寄存器的最低有效字节/半字。FTHRES[3:0]用于定义FIFO的阈值。如果达到阈值,FTF(FIFO阈值标志)置1。 在间接读取模式下,从FIFO中读取的有效字节数超过阈值时,FTF置1。从Flash中读取最后一个字节后,如果FIFO中依然有数据, 则无论FTHRES的设置为何,FTF也都会置1。在间接写入模式下,当FIFO中的空字节数超过阈值时,FTF置1。

如果FTIE=1,则FTF置1时产生中断。如果DMAEN=1,则FTF置1时启动数据传送。如果阈值条件不再为“真”(CPU或DMA传输了足够的数据后), 则FTF由HW清零。在间接模式下,当FIFO已满,QUADSPI将暂时停止从Flash读取字节以避免上溢。请注意, 只有在FIFO中的4个字节为空(FLEVEL≤11)时才会重新开始读取Flash。因此,若FTHRES≥13, 应用程序必须读取足够的字节以确保QUADSPI再次从Flash检索数据。否则,只要11<FLEVEL<FTHRES,FTF标志将保持为“0”。

21.4. QUADSPI内存映射模式

在配置为内存映射模式时,外部SPI器件被视为是内部存储器,只存在访问延迟。在此模式下,只允许对外部FLASH进行读操作。

QUADSPI外设若没有正确配置并使能,禁止访问QUADSPIFlash的存储区域。即使FLASH容量更大,寻址空间也无法超过256MB。 如果访问的地址超出FSIZE定义的范围但仍在256MB范围内,则生成总线错误。

此错误的影响具体取决于尝试进行访问的总线主器件:

  • 如果为Cortex CPU,则会在使能总线故障时发生总线故障异常,在禁止总线故障时发生硬性故障(hard fault) 异常。

  • 如果为DMA,则生成DMA传输错误,并自动禁用相应的DMA通道。

内存映射模式支持字节、半字和字访问类型,并支持芯片内执(XIP)操作,QUADSPI接受下一个微控制器访问并提前加载后面地址中的字节。 如果之后访问的是连续地址,由于值已经预取,访问将更快完成。

内存映射模式可以通过将QUADSPI_CCR寄存器中的FMODE设置为11来进入。当主器件访问存储器映射空间时,将发送已编程的指令和帧。

默认情况下,即便在很长时间内不访问FLASH,QUADSPI也不会停止预取操作,之前的读取操作将保持激活状态并且nCS保持低电平。由于nCS保持低电平时, FLASH功耗增加,应用程序可能会激活超时计数器(TCEN=1, QUADSPI_CR的位3)。从而在FIFO中写满预取的数据后, 若在TIMEOUT[15:0](QUADSPI_LPTR)个周期的时长内没有访问,则释放nCS。BUSY在第一个存储器映射访问发生时变为高电平。 由于进行预取操作,BUSY在发生超时、中止或外设禁止前不会下降。

在此模式下,FIFO用作预取缓冲区以接受线性读取,且对数据寄存器QUADSPI_DR的任何访问都会返回零。数据长度寄存器(QUADSPI_DLR)在内存映射模式下无意义。

21.5. QUADSPI Flash 配置

外部SPI Flash的参数可以通过配置寄存器(QUADSPI_DCR)实现。这里配置Flash的容量是设置FSIZE[4:0]字段,使用下面的公式定义外部存储器的大小:

\[Fcap = 2^{\left\lbrack FSIZE + 1 \right\rbrack}\]

FSIZE+1 是对Flash寻址所需的地址位数。在间接模式下,Flash容量最高可达 4GB(使用32 位进行寻址),但在内存映射模式下的可寻址空间限制为 256MB。 如果DFM=1,FSIZE表示两个Flash容量的总和。QUADSPI连续执行两条命令时,它在两条命令之间将片选信号(nCS)置为高电平默认仅一个CLK周期时长。 如果Flash需要命令之间的时间更长,可使用片选高电平时间 (CSHT) 字段指定 nCS 必须保持高电平的最少 CLK 周期数(最大为 8)。 时钟模式(CKMODE)位指示命令之间的CLK信号逻辑电平(nCS=1时)。

21.6. QSPI初始化结构体详解

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

代码清单:QSPI-1 QSPI_HandleTypeDef结构体(stm32mp157xx_hal_qspi.h文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/**
* @brief  QSPI Handle Structure definition
*/
typedef struct {
    QUADSPI_TypeDef            *Instance;        /* QSPI外设寄存器基地址*/
    QSPI_InitTypeDef           Init;             /* QSPI外设参数配置结构体*/
    uint8_t                    *pTxBuffPtr;      /* QSPI发送数据的地址*/
    __IO uint32_t              TxXferSize;       /* QSPI发送数据的大小*/
    __IO uint32_t              TxXferCount;      /* QSPI发送数据的个数*/
    uint8_t                    *pRxBuffPtr;      /* QSPI接收数据的地址*/
    __IO uint32_t              RxXferSize;       /* QSPI接受数据的大小*/
    __IO uint32_t              RxXferCount;      /* QSPI接受数据的个数*/
    MDMA_HandleTypeDef         *hmdma;           /* QSPI发送接受使能DMA配置结构体*/
    __IO HAL_LockTypeDef       Lock;             /* 锁资源*/
    __IO HAL_QSPI_StateTypeDef State;            /* QSPI工作状态*/
    __IO uint32_t              ErrorCode;        /* QSPI错误参数值*/
    uint32_t                   Timeout;          /* 等待时间*/
} QSPI_HandleTypeDef;

这些结构体成员说明如下:

(1) Instance

Instance是QUADSPI_TypeDef类型的结构体变量,存放着QSPI寄存器基地址。

(2) Init

Init是QSPI的初始化结构体,主要用来配置QSPI的双闪存模式,时钟预分频因子,FIFO的阈值。

(3) pTxBuffPtr,TxXferSize,TxXferCount

这三个参数分别为发送数据的存放地址,大小以及个数。pTxBuffPtr用一个指针指向我们需要发送的数据数组。

(4) pRxBuffPtr,RxXferSize,RxXferCount

这三个参数分别为接受数据的存放地址,大小以及个数。pRxBuffPtr用一个指针变量指向我们定义存放数据内容的数组。

(5) hmdma

MDMA_HandleTypeDef结构体变量,用于配置相关的DMA参数。

(6) Lock

该参数主要负责分配锁资源,可选择HAL_UNLOCKED或者是HAL_LOCKED两个参数。

(7) State

HAL_QSPI_StateTypeDef结构体变量,用于存放通讯过程中的工作状态。

(8) ErrorCode

QSPI的错误参数,通过该参数,用户可以了解到QSPI通讯过程中造成失败的原因。

(9) Timeout

允许等待的最大时长。一旦超出Timeout的变量值,则ErrorCode 为HAL_QSPI_ERROR_TIMEOUT,表示超出规定的时间。

代码清单:QSPI-2 QSPI_InitTypeDef初始化结构体(stm32mp157xx_hal_qspi.h文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
typedef struct {
    uint32_t ClockPrescaler;     //预分频因子
    uint32_t FifoThreshold;      //FIFO中的阈值
    uint32_t SampleShifting;     //采样移位
    uint32_t FlashSize;          //Flash大小
    uint32_t ChipSelectHighTime; //片选高电平时间
    uint32_t ClockMode;          //时钟模式
    uint32_t FlashID;            //Flash ID
    uint32_t DualFlash;          //双闪存模式
} 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:使能双闪存模式。双闪存模式可以使系统吞吐量和容量扩大一倍。

代码清单:QSPI-3 QSPI_CommandTypeDe通信配置命令结构体(stm32mp157xx_hal_qspi.h文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
typedef struct {
    uint32_t Instruction;        //指令
    uint32_t Address;            //地址
    uint32_t AlternateBytes;     //交替字节
    uint32_t AddressSize;        //地址长度
    uint32_t AlternateBytesSize; //交替字节长度
    uint32_t DummyCycles;        //空指令周期
    uint32_t InstructionMode;    //指令模式
    uint32_t AddressMode;        //地址模式
    uint32_t AlternateByteMode;  //交替字节模式
    uint32_t DataMode;           //数据模式
    uint32_t NbData;             //数据长度
    uint32_t DdrMode;            //双倍数据速率模式
    uint32_t DdrHoldHalfCycle;   //DDR保持周期
    uint32_t SIOOMode;           //仅发送指令一次模式
} 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:仅为第一条命令发送指令。

21.7. QSPI—使用单/双线模式读写串行FLASH实验

FLSAH存储器又称闪存,它与EEPROM都是掉电后数据不丢失的存储器,但FLASH存储器容量普遍大于EEPROM,现在基本取代了它的地位。 我们生活中常用的U盘、SD卡、SSD固态硬盘以及我们STM32芯片内部用于存储程序的设备,都是FLASH类型的存储器。 在存储控制上,最主要的区别是FLASH芯片只能一大片一大片地擦写,而EEPROM可以单个字节擦写。

本小节以一种使用QSPI通讯的串行FLASH存储芯片的读写实验为大家讲解STM32的QSPI使用方法。 实验中STM32的QSPI外设采用主模式,通过查询事件的方式来确保正常通讯。

21.7.1. 硬件设计

SPI串行FLASH硬件连接图

本实验板中用到的FLASH芯片(型号:W25Q128)是一种使用QSPI/SPI通讯协议的NOR FLASH存储器, 它的CS/CLK/D0/D1/D2/D3引脚分别连接到了STM32对应的QSPI引脚 QUADSPI_NCS/ QUADSPI_CLK / QUADSPI_IO0/QUADSPI_IO1/ QUADSPI_IO2/ QUADSPI_IO3上, 这些引脚都是STM32的复用引脚。对应的引脚信息可在核心板原理图上查找,如下图所示

SPI串行FLASH硬件连接图 SPI串行FLASH硬件连接图

关于FLASH芯片的更多信息,可参考其数据手册《W25Q128》来了解。若您使用的实验板FLASH的型号或控制引脚不一样, 只需根据我们的工程修改即可,程序的控制原理相同。

21.7.2. 软件设计

QUADSPI的外设时钟,来自于HCLK6,时钟频率最高为266MHz;先在时钟树上先设置QUADSPI时钟频率为266Mhz, 并将M4内核的时钟频率设置为209Mhz,如图所示

时钟树配置

在配置STM32的QSPI模式前,我们要先了解从机端的QSPI模式。本例子中可通过查阅FLASH数据手册《W25Q128》获取。 根据FLASH芯片的说明,它支持SPI模式0及模式3,支持四线模式,支持最高通讯时钟为104MHz,数据帧长度为8位。

搜索QUADSPI外设,将其分配给M4内核,这里我们选择使用单线/双模式模式,在时钟树中我们将APH6总线的时钟设置为266Mhz, 因此需要选择QUADSPI时钟分频。此处选择(2+1)分频,QUADSPI的时钟频率为88.67Mhz。 同时设置FIFO阈值为4个字节;采样移位半个周期; 设置SPI FLASH大小;为16M字节,即地址位数为23+1=24,所以取值23; 片选高电平时间至少为50ns,这里选择为5个时钟(5*1/88.67 =56.35ns); 时钟模式选择为高; 此处的配置信息和flash芯片息息相关,

QSPI配置

设置QSPI引脚相关配置,我们配置的QSPI模式为单/双线模式,因此需要将QSPI_CLK、QSPI_NCS、QSPI_IO0、QSPI_IO1所在 的PF9、PB6、PF8、PF9设置为QUADSPI引脚复用模式。

引脚配置 引脚配置

当使用单/双线模式时,需要将W25Q128的WP/IO2、HOLD/IO3所在的引脚PF6、PF7拉高,以禁止写保护和保持模式。 也可以直接硬件上连接到高电平。

引脚配置

到此关于QSPI外设的配置已经完成。

21.7.2.1. 代码分析

我们将新建“bsp_qspi_flash.c”及“bsp_qspi_ flash.h”文件, 用于编写与flash相关操作的代码,这些文件不属于STM32 HAL库的内容,是由我们自己根据应用需要编写的。也可根据自己的喜好命名。 由于本章涉及代码内容较多,完整代码打开工程阅读,这里只介绍重点内容部分。

控制FLASH的指令

FLASH芯片自定义了很多指令,我们通过控制STM32利用QSPI总线向FLASH芯片发送指令,FLASH芯片收到后就会执行相应的操作。

而这些指令,对主机端(STM32)来说,只是它遵守最基本的QSPI通讯协议发送出的数据,但在设备端(FLASH芯片)把这些数据解释成不同的意义, 所以才成为指令。查看FLASH芯片的数据手册《W25Q128》,可了解各种它定义的各种指令的功能及指令格式,见表 FLASH常用芯片指令表

FLASH常用芯片指令表

该表中的第一列为指令名,第二列为指令编码,第三至第N列的具体内容根据指令的不同而有不同的含义。其中带括号的字节参数,方向为FLASH向主机传输, 即命令响应,不带括号的则为主机向FLASH传输。表中“A0~A23”指FLASH芯片内部存储器组织的地址;“M0~M7”为厂商号(MANUFACTURER ID); “ID0-ID15”为FLASH芯片的ID;“dummy”指该处可为任意数据;“D0~D7”为FLASH内部存储矩阵的内容。

在FLSAH芯片内部,存储有固定的厂商编号(M7-M0)和不同类型FLASH芯片独有的编号(ID15-ID0),见表 FLASH数据手册的设备ID说明

FLASH数据手册的设备ID说明

通过指令表中的读ID指令“JEDEC ID”可以获取这两个编号,该指令编码为“9F h”,其中“9F h”是指16进制数“9F” (相当于C语言中的0x9F)。 紧跟指令编码的三个字节分别为FLASH芯片输出的“(M7-M0)”、“(ID15-ID8)”及“(ID7-ID0)” 。

此处我们以该指令为例,配合其指令时序图进行讲解,见图 FLASH读ID指令

FLASH读ID指令

主机首先通过DIO (对应STM32的QUADSPI_BK1_IO0)线向FLASH芯片发送第一个字节数据为“9F h”,当FLASH芯片收到该数据后, 它会解读成主机向它发送了“JEDEC指令”,然后它就作出该命令的响应:通过*DO(对应STM32的QUADSPI_BK1_IO1)线*把它的厂商ID(M7-M0)及芯片类型(ID15-0)发送给主机, 主机接收到指令响应后可进行校验。常见的应用是主机端通过读取设备ID来测试硬件是否连接正常,或用于识别设备。

对于FLASH芯片的其它指令,都是类似的,只是有的指令包含多个字节,或者响应包含更多的数据。

实际上,编写设备驱动都是有一定的规律可循的。首先我们要确定设备使用的是什么通讯协议。如上一章的EEPROM使用的是I2C,本章的FLASH使用的是QSPI。 那么我们就先根据它的通讯协议,选择好STM32的硬件模块,并进行相应的I2C或SPI模块初始化。接着,我们要了解目标设备的相关指令, 因为不同的设备,都会有相应的不同的指令。如EEPROM中会把第一个数据解释为内部存储矩阵的地址(实质就是指令)。而FLASH则定义了更多的指令, 有写指令,读指令,读ID指令等等。最后,我们根据这些指令的格式要求,使用通讯协议向设备发送指令,达到控制设备的目标。

定义FLASH指令编码表

为了方便使用,我们把FLASH芯片的常用指令编码使用宏来封装起来,后面需要发送指令编码的时候我们直接使用这些宏即可,见 代码清单:QSPI-4

代码清单:QSPI-4 FLASH指令编码表(bsp_qspi_flash.h文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/* 复位操作 */
#define RESET_ENABLE_CMD                     0x66
#define RESET_MEMORY_CMD                     0x99

/* 识别操作 */
#define READ_ID_CMD                          0x90
#define READ_JEDEC_ID_CMD                    0x9F

/* 读操作 */
#define READ_CMD                       0x03
#define FAST_READ_CMD                                  0x0B

/* 写操作 */
#define WRITE_ENABLE_CMD                     0x06
#define WRITE_DISABLE_CMD                    0x04


/* 状态寄存器 */
#define READ_STATUS_REG1_CMD                  0x05
#define READ_STATUS_REG2_CMD                  0x35
#define READ_STATUS_REG3_CMD                  0x15

#define WRITE_STATUS_REG1_CMD                 0x01
#define WRITE_STATUS_REG2_CMD                 0x31
#define WRITE_STATUS_REG3_CMD                 0x11

/* 擦除操作 */
#define SECTOR_ERASE_CMD                          0x20
#define CHIP_ERASE_CMD                        0xC7

/* 状态寄存器标志 */
#define W25Q256JV_FSR_BUSY                    ((uint8_t)0x01)    /*!< busy */
#define W25Q256JV_FSR_WREN                    ((uint8_t)0x02)    /*!< write enable */
#define W25Q256JV_FSR_QE                      ((uint8_t)0x02)    /*!< quad enable */

/* 编程操作 */
#define PAGE_PROG                                                     0x02
#define QUAD_INPUT_PAGE_PROG                                  0x32

初始化QSPI存储器

初始化好QSPI外设后,还要初始化QSPI存储器,需要先复位存储器,见 代码清单:QSPI-5

代码清单:QSPI-5 初始化QSPI存储器(bsp_qspi_flash.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/**
* @brief  初始化QSPI存储器
* @retval QSPI存储器状态
*/
uint8_t BSP_QSPI_FlASH_Init(void)
{

    /* QSPI存储器复位 */
    if (QSPI_ResetMemory() != QSPI_OK)
    {
        return QSPI_NOT_SUPPORTED;
    }

    return QSPI_OK;
}

复位QSPI

初始化好QSPI外设后,还要初始化QSPI存储器,需要先复位存储器,使能写操作,配置状态寄存器才可进行数据读写操作,见 代码清单:QSPI-6

代码清单:QSPI-6 复位QSPI(bsp_qspi_flash.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* @brief  复位QSPI存储器。
* @param  QSPIHandle: QSPI句柄
* @retval 无
*/
static uint8_t QSPI_ResetMemory()
{
    QSPI_CommandTypeDef s_command;
    /* 初始化复位使能命令 */
    s_command.InstructionMode   = QSPI_INSTRUCTION_1_LINE;
    s_command.Instruction       = RESET_ENABLE_CMD;
    s_command.AddressMode       = QSPI_ADDRESS_NONE;
    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 (HAL_QSPI_Command(&QSPIHandle, &s_command, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
    {
        return QSPI_ERROR;
    }

    /* 发送复位存储器命令 */
    s_command.Instruction = RESET_MEMORY_CMD;
    if (HAL_QSPI_Command(&QSPIHandle, &s_command, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
    {
        return QSPI_ERROR;
    }

    /* 配置自动轮询模式等待存储器就绪 */
    if (QSPI_AutoPollingMemReady(HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != QSPI_OK)
    {
        return QSPI_ERROR;
    }
    return QSPI_OK;
}

(1) QSPI_CommandTypeDef定义一个s_command结构体变量,用于配置命令。

(2) s_command命令配置:发送使能复位功能的指令,其具体参数值见上面的Flash指令编码表。跳过地址阶段,交替字节阶段,空指令阶段和数据阶段。不使能DDR模式。

(3) 调用HAL_QSPI_Command函数,来实现QSPI发送复位使能指令。

(4) 一旦使能复位FLASH功能了,我们便可以发送复位设备的FLASH指令,具体参数值见Flash指令表。

(5) 调用HAL_QSPI_AutoPolling库函数,来获取Flash的工作状态。具体内容见下面讲解。

使用QSPI写入大量数据

我们要从存取器中写入大量数据,首先要用一个指针指写入数据,并确定数据的首地址,数据大小,根据写入地址及大小判断存储器的页面, 然后通过库函数HAL_QSPI_Command发送配置命令,然后调用库函数HAL_QSPI_Transmit逐页写入数据,最后等待操作完成,我们看看它的代码实现, 见 代码清单:QSPI-7

代码清单:QSPI-7 使用QSPI读取大量数据(bsp_qspi_flash.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/**
* @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 += W25Q128JV_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       = EXT_QUAD_IN_FAST_PROG_CMD_4BYTE;
    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_1_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 + W25Q128JV_PAGE_SIZE) > end_addr) ? (end_addr - current_addr) : W25Q128JV_PAGE_SIZE;
    } while (current_addr < end_addr);
    return QSPI_OK;
}

读取FLASH芯片ID

根据“JEDEC”指令的时序,我们把读取FLASH ID的过程编写成一个函数,见 代码清单:QSPI-8

代码清单:QSPI-8 读取FLASH芯片ID(bsp_qspi_flash.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* @brief  读取FLASH ID
* @param   无
* @retval FLASH ID
*/
uint32_t QSPI_FLASH_ReadID(void)
{
    QSPI_CommandTypeDef s_command;
    uint32_t Temp = 0;
    uint8_t pData[6];
    /* 读取JEDEC ID */
    s_command.InstructionMode   = QSPI_INSTRUCTION_1_LINE;
    s_command.Instruction       = READ_JEDEC_ID_CMD;
    s_command.DataMode          = QSPI_DATA_1_LINE;
    s_command.AddressMode       = QSPI_ADDRESS_NONE;
    s_command.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
    s_command.DummyCycles       = 0;
    s_command.NbData            = 6;
    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) {
        printf("something wrong ....\r\n");
        /*
        用户可以在这里添加一些代码来处理这个错误
        */
        while (1) {

        }
    }
    if (HAL_QSPI_Receive(&QSPIHandle, pData,
        HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK) {
        printf("something wrong ....\r\n");
        /*
        用户可以在这里添加一些代码来处理这个错误
        */
        while (1) {

        }
    }

    Temp = ( pData[4] | pData[2]<<8 )| ( pData[0]<<16 );

    return Temp;
}

这段代码利用库函数HAL_QSPI_Command发送读取FLASH ID指令,再调用库函数HAL_QSPI_Receive读取3个字节,获取FLASH芯片对该指令的响应, 最后把读取到的这3个数据合并到一个变量Temp中。然后然后作为函数返回值,把该返回值与我们定义的宏“sFLASH_ID”对比,即可知道FLASH芯片是否正常。

FLASH写使能以及读取当前状态

在向FLASH芯片存储矩阵写入数据前,首先要使能写操作,通过“Write Enable”命令即可写使能,见 代码清单:QSPI-9

代码清单:QSPI-9 写使能命令(bsp_qspi_flash.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* @brief  发送写入使能,等待它有效.
* @param  QSPIHandle: QSPI句柄
* @retval 无
*/
static uint8_t QSPI_WriteEnable()
{
    QSPI_CommandTypeDef     s_command;
    QSPI_AutoPollingTypeDef s_config;
    /* 启用写操作 */
    s_command.InstructionMode   = QSPI_INSTRUCTION_1_LINE;
    s_command.Instruction       = WRITE_ENABLE_CMD;
    s_command.AddressMode       = QSPI_ADDRESS_NONE;
    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(HAL_QSPI_Command(&QSPIHandle, &s_command, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK){
        return QSPI_ERROR;
    }

    /* 配置自动轮询模式等待写启用 */
    s_config.Match           = W25Q128FV_FSR_WREN;
    s_config.Mask            = W25Q128FV_FSR_WREN;
    s_config.MatchMode       = QSPI_MATCH_MODE_AND;
    s_config.StatusBytesSize = 1;
    s_config.Interval        = 0x10;
    s_config.AutomaticStop   = QSPI_AUTOMATIC_STOP_ENABLE;

    s_command.Instruction    = READ_STATUS_REG1_CMD;
    s_command.DataMode       = QSPI_DATA_1_LINE;
    s_command.NbData         = 1;

    if(HAL_QSPI_AutoPolling(&QSPIHandle, &s_command, &s_config, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK){
        return QSPI_ERROR;
    }
    return QSPI_OK;
}

与EEPROM一样,由于FLASH芯片向内部存储矩阵写入数据需要消耗一定的时间,并不是在总线通讯结束的一瞬间完成的, 所以在写操作后需要确认FLASH芯片“空闲”时才能进行再次写入。为了表示自己的工作状态,FLASH芯片定义了一个状态寄存器,见图 FLASH芯片的状态寄存器

FLASH芯片的状态寄存器

我们只关注这个状态寄存器的第0位“BUSY”,当这个位为“1”时,表明FLASH芯片处于忙碌状态,它可能正在对内部的存储矩阵进行“擦除”或“数据写入”的操作。

利用指令表中的“Read Status Register”指令可以获取FLASH芯片状态寄存器的内容,其时序见图 读取状态寄存器的时序

读取状态寄存器的时序

只要向FLASH芯片发送了读状态寄存器的指令,FLASH芯片就会持续向主机返回最新的状态寄存器内容,直到收到SPI通讯的停止信号。 HAL库提供了具有等待FLASH芯片写入结束功能的函数,见 代码清单:QSPI-10

代码清单:QSPI-10 通过读状态寄存器等待FLASH芯片空闲(bsp_qspi_flash.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* @brief  读取存储器的SR并等待EOP
* @param  QSPIHandle: QSPI句柄
* @param  Timeout 超时
* @retval 无
*/
static uint8_t QSPI_AutoPollingMemReady(uint32_t Timeout)
{
    QSPI_CommandTypeDef     s_command;
    QSPI_AutoPollingTypeDef s_config;
    /* 配置自动轮询模式等待存储器准备就绪 */
    s_command.InstructionMode   = QSPI_INSTRUCTION_1_LINE;
    s_command.Instruction       = READ_STATUS_REG1_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.DdrMode           = QSPI_DDR_MODE_DISABLE;
    s_command.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;
    s_command.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;

    s_config.Match           = 0x00;
    s_config.Mask            = W25Q128FV_FSR_BUSY;
    s_config.MatchMode       = QSPI_MATCH_MODE_AND;
    s_config.StatusBytesSize = 1;
    s_config.Interval        = 0x10;
    s_config.AutomaticStop   = QSPI_AUTOMATIC_STOP_ENABLE;

    if (HAL_QSPI_AutoPolling(&QSPIHandle, &s_command, &s_config, Timeout) != HAL_OK) {
        return QSPI_ERROR;
    }
    return QSPI_OK;
}

这段代码直接调用HAL_QSPI_AutoPolling库函数,设定命令参数及自动轮询参数,最后设定超时返回,如果在超时等待时间内确定FLASH就绪则返回存储器就绪状态, 否则返回存储器错误。其实主要就是检查它的“W25Q128FV_FSR_BUSY”(即BUSY位),通过QUADSPI_PSMAR与QUADSPI_QUADSPI _PSMKR作‘与’运算或者是作‘或’运算。 如果满足条件才退出本函数,以便继续后面与FLASH芯片的数据通讯。

FLASH扇区擦除

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

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

本实验FLASH芯片的擦除单位

FLASH芯片的最小擦除单位为扇区(Sector),而一个块(Block)包含16个扇区,其内部存储矩阵分布见图 FLASH芯片的存储矩阵

FLASH芯片的存储矩阵

使用扇区擦除指令“Sector Erase”可控制FLASH芯片开始擦写,其指令时序见图 扇区擦除时序

扇区擦除时序

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

代码清单:QSPI-11 擦除扇区(bsp_qspi_flash.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* @brief  擦除QSPI存储器的指定扇区
* @param  SectorAddress: 需要擦除的扇区地址
* @retval QSPI存储器状态
*/
uint8_t BSP_QSPI_Erase_Sector(uint32_t SectorAddress)
{
    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           = SectorAddress;
    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(W25Q128JV_SECTOR_ERASE_MAX_TIME) != QSPI_OK) {
        return QSPI_ERROR;
    }
    return QSPI_OK;
}

21.7.2.2. main_task.c文件

Main_Config函数

Main_Config函数,主要进行flash芯片的初始化,见 代码清单:QSPI-12_ Main_Config函数。

代码清单:QSPI-16 Main_Config函数(Main_Config.c文件)
1
2
3
4
5
6
7
8
void Main_Config(void)      //配置函数
{
    BSP_QSPI_FlASH_Init();          //板载flash初始化

    //将HOLD及WP引脚设置为高
    HAL_GPIO_WritePin(GPIOF,GPIO_PIN_6,GPIO_PIN_SET);
    HAL_GPIO_WritePin(GPIOF,GPIO_PIN_7,GPIO_PIN_SET);
}

Main_Config调用了BSP_QSPI_FlASH_Init函数对W25Q128进行初始化, 同时将HOLD以及WP所在的PF6、PF7引脚拉高,禁用写保护。

Main_Task函数

代码清单:QSPI-16 Main_Task函数(Main_Config.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
void Main_Task(void)        //主要的任务函数
{

    uint32_t addr = FLASH_WriteAddress ;
    int state = QSPI_ERROR;

    printf("\r\n这是一个32M串行flash(W25Q128)间接模式读写实验(QSPI驱动) \r\n");

    /* 获取 Flash Device ID */
    DeviceID = QSPI_FLASH_ReadDeviceID();
    Delay( 200 );
    /* 获取 SPI Flash ID */
    FlashID = QSPI_FLASH_ReadID();

    /* 检验 SPI Flash ID */
    if (FlashID == sFLASH_ID)
    {
        printf("\r\n检测到QSPI FLASH W25Q128 !\r\n");
        printf("\r\n正在擦除芯片的%d~%d的内容!\r\n", addr, addr+W25Q128JV_PAGE_SIZE);

        state = BSP_QSPI_Erase_Sector(addr);
        if(state == QSPI_OK)
        printf("\r\n擦除成功!\r\n");
        else
        {
        printf("\r\n擦除失败!\r\n");
        while(1);
        }

        printf("\r\n正在向芯片%d地址写入数据,大小为%d!\r\n", addr, BufferSize);
            /* 将发送缓冲区的数据写到flash中 */
        BSP_QSPI_Write(Tx_Buffer, addr, BufferSize);

        printf("\r\n正在向芯片%d地址读取大小为%d的数据!\r\n", addr, BufferSize);
        /* 将刚刚写入的数据读出来放到接收缓冲区中 */
        if(QSPI_OK == BSP_QSPI_Read(Rx_Buffer, addr, BufferSize))
        {
            printf("\r\n读取成功!\r\n");
        }
        else
        {
            printf("\r\n读取失败!\r\n");
        }

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

        if( PASSED == TransferStatus1 )
        {
            printf("\r\n读写%d地址测试成功!\r\n", addr);
        }
        else
        {
            printf("\r\n读写%d地址测试失败!\n\r", addr);
        }
    }// if (FlashID == sFLASH_ID)
    else
    {
        //LED_RED;
        printf("\r\n获取不到 W25Q256 ID!\n\r");
    }

    while(1);

}

函数中读取FLASH芯片的ID进行校验, 若ID校验通过则向FLASH的特定地址写入测试数据,然后再从该地址读取数据,测试读写是否正常。

21.7.2.3. main主函数

main.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 int main(void)
 {

     HAL_Init();

     if(IS_ENGINEERING_BOOT_MODE())
     {

         SystemClock_Config();
     }

     MX_GPIO_Init();
     MX_USART3_UART_Init();
     MX_QUADSPI_Init();

     Main_Config();

     while (1)
     {
         Main_Task();
     }

 }

主函数的内容和之前一样,仅添加了Main_Config与Main_Task函数。

21.7.3. 下载验证

用USB线连接开发板“USB TO UART”接口跟电脑,在电脑端打开串口调试助手,把编译好的程序下载到开发板。在串口调试助手可看到FLASH测试的调试信息。