第24章 QSPI—读写串行FLASH

本章参考资料:《STM32F76xxx参考手册》、《STM32F76xxx规格书》、库帮助文档《STM32F779xx_User_Manual.chm》及《SPI总线协议介绍》。

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

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

24.1 QSPI协议简介

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

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

    • 间接模式:使用 QSPI 寄存器执行全部操作
    • 状态轮询模式:周期性读取外部 Flash 状态寄存器,而且标志位置 1 时会产生中断(如擦除或烧写完成,会产生中断)
    • 内存映射模式:外部 Flash 映射到微控制器地址空间,从而系统将其视作内部存储器

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

24.1.1 QSPI功能框图

QSPI功能框图,双闪存模式禁止见图 24-1。

第24章 QSPI—读写串行FLASH_第1张图片

 

图 24-1 QUADSPI 功能框图(双闪存模式禁止)

我们的开发板采用的是双闪存禁止的模式连接单片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:时钟输出,适用于两个存储器,用于通讯数据同步。它由通讯主机产生,决定了通讯的速率,不同的设备支持的最高时钟频率不一样,如STM32的QSPI时钟频率最大为fpclk/2,两个设备之间通讯时,通讯速率受限于低速设备。
  3. BK1_IO0:在双线 / 四线模式中为双向 IO,单线模式中为串行输出,适用于FLASH 1。
  4. BK1_IO1:在双线 / 四线模式中为双向 IO,单线模式中为串行输入,适用于FLASH 1。
  5. BK1_IO2:在四线模式中为双向 IO,适用于 FLASH 1。
  6. BK1_IO3:在四线模式中为双向 IO,适用于 FLASH 1。

 

24.1.2 QSPI命令序列

QUADSPI 通过命令与 Flash 通信 每条命令包括指令、地址、交替字节、空指令和数据这五个阶段 任一阶段均可跳过,但至少要包含指令、地址、交替字节或数据阶段之一。nCS 在每条指令开始前下降,在每条指令完成后再次上升。先看看QSPI四线模式下的读命令时序,见图 24-2。

第24章 QSPI—读写串行FLASH_第2张图片

 

图 24-2 四线模式下的读命令时序

  1. 指令阶段

这一阶段,将在 QUADSPI_CCR[7:0] 寄存器的 INSTRUCTION 字段中配置的一条 8 位指令发送到 Flash,指定待执行操作的类型。

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

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

 

2. 地址阶段

在地址阶段,将1-4字节发送到Flash,指示操作地址。待发送的地址字节数在QUADSPI_CCR[13:12]寄存器的ADSIZE[1:0]字段中进行配置。在间接模式和自动轮询模式下,待发送的地址字节在QUADSPI_AR寄存器的ADDRESS[31:0]中指定在内存映射模式下,则通过 AHB(来自于 Cortex ® 或 DMA)直接给出地址。地址阶段可一次发送1 位(在单线SPI模式中通过SO)、2位(在双线SPI模式中通过IO0/IO1)或4位(在四线 SPI 模式中通过 IO0/IO1/IO2/IO3)。这可通过QUADSPI_CCR[11:10]寄存器中的ADMODE[1:0]字段进行配置。

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

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) 并发送一个字节,方法是 ALTERNATE 的位 7 和 3 置“1”(IO3 保持高电平)且位 6 和 2 置“0”(IO2 线保持低电平)。此时,半字节的高 2 位存放在 ALTERNATE 的位 4:3,低 2位存放在位 1 和 0 中。例如,如果半字节 2 (0010) 通过 IO0/IO1 发送,则 ALTERNATE 应设置为 0x8A (1000_1010)

4. 空指令周期阶段

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

5. 数据阶段

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

在间接模式和自动轮询模式下,待发送/接收的字节数在 QUADSPI_DLR 寄存器中指定。在间接写入模式下,发送到 Flash 的数据必须写入 QUADSPI_DR 寄存器。在间接读取模式下,通过读取 QUADSPI_DR 寄存器获得从 Flash 接收的数据。在内存映射模式下,读取的数据通过 AHB 直接发送回 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 时立即完成。这一配置仅可用于仅间接写入模式。

24.2 QUADSPI 信号接口协议模式

24.2.1 单线 SPI 模式

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

  • IO0 (SO) 处于输出模式
  • IO1 (SI) 处于输入模式(高阻抗)
  • IO2 处于输出模式并强制置“0”(以禁止“写保护”功能)
  • IO3 处于输出模式并强制置“1”(以禁止“保持”功能)

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

24.2.2 双线 SPI 模式

在双线模式下,通过 IO0/IO1 信号同时发送/接收两位。通过将 QUADSPI_CCR 寄存器的 IMODE/ADMODE/ABMODE/DMODE 字段设置为 10,可对不同的命令阶段分别进行配置,以使用双线 SPI 模式。在每个已配置为单线模式的阶段中:

  • IO0/IO1 在数据阶段进行读取操作时处于高阻态(输入),在其他情况下为输出
  • IO2 处于输出模式并强制置“0”
  • IO3 处于输出模式并强制置“1” 

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

24.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 模式,即使 UADSPI激活,对应 IO2 和 IO3 的引脚也可用于其他功能。

24.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 后续的边沿(上升沿)对信号进行采样。

24.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命令时序见图 24-3。

第24章 QSPI—读写串行FLASH_第3张图片

 

图 24-3 四线模式下DDR命令时序

 

24.2.6 双闪存模式

若 DFM 位 (QUADSPI_CR[6]) 为 1,QUADSPI 处于双闪存模式。QUADSPI 使用两个外部四线 SPI Flash(FLASH 1 和 FLASH 2),在每个周期中发送/接收 8 位(在 DDR 模式下为16 位),能够有效地将吞吐量和容量扩大一倍。每个 Flash 使用同一个 CLK 并可选择使用同一个 nCS 信号,但其 IO0、IO1、IO2 和 IO3 信号是各自独立的。双闪存模式可与单比特模式、双比特模式以及四比特模式结合使用,也可与 SDR 或 DDR 模

式相结合。Flash 的大小在 FSIZE[4:0] (QUADSPI_DCR[20:16]) 中指定,指定的值应能够反映 Flash 的总容量,即单个组件容量的 2 倍。如果地址 X 为偶数,QUADSPI 赋给地址 X 的字节是存放于 FLASH 1 的地址 X/2 中的字节,QUADSPI 赋给地址 X+1 的字节是存放于 FLASH 2 的地址 X/2 中的字节。也就是说,偶地址中的字节存储于 FLASH 1,奇地址中的字节存储于 FLASH 2。

在双闪存模式下读取 Flash 状态寄存器时,需要读取的字节数是单闪存模式下的 2 倍。这意味着在状态寄存器获取指令到达后,如果每个 Flash 给出 8 个有效位,则 QUADSPI 必须配置为 2 个字节(16 位)的数据长度,它将从每个 Flash 接收 1 个字节。如果每个 Flash 给出一个 16 位的状态,则 QUADSPI 必须配置为读取 4 字节,以在双闪存模式下可获取两个Flash 的所有状态位。结果(在数据寄存器中)的最低有效字节是 FLASH 1 状态寄存器的最低有效字节,而下一个字节是 FLASH 2 状态寄存器的最低有效字节。数据寄存器的第三个字节是 FLASH 1 的第二个字节,第四个字节是 FLASH 2 的第二个字节(Flash 具有 16 位状态寄存器时)。

偶数个字节必须始终在双闪存模式下访问。因此,若 DRM = 1,则数据长度字段(QUADSPI_DLR[0]) 的位 0 始终保持为 1。

在双闪存模式下,FLASH 1 接口信号的行为基本上与正常模式下相同。在指令、地址、交替字节以及空指令周期阶段,FLASH 2 接口信号具有与 FLASH 1 接口信号完全相同的波形。也就是说,每个 Flash 总是接收相同的指令与地址。然后,在数据阶段,BK1_IOx 和BK2_IOx 总线并行传输数据,但发送到 FLASH 1(或从其接收)的数据与 FLASH 2 中的不同。

24.3 QUADSPI 间接模式

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

若 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开始继续读取。

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

24.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。

24.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”。

24.4 QUADSPI Flash 配置

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

 

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

24.5 QSPI初始化结构体详解

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

代码清单 24-1 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 时修改该字段。

  1. FifoThreshold

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

  1. SampleShifting

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

  1. FlashSize

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

  1. ChipSelectHighTime

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

  1. ClockMode

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

  1. FlashID

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

  1. DualFlash

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

代码清单 242 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 时修改该字段。

  1. Address

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

  1. AlternateBytes

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

  1. AddressSize

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

  1. AlternateBytesSize

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

  1. DummyCycles

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

  1. InstructionMode

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

  1. AddressMode

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

  1. AlternateByteMode

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

  1. DataMode

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

  1. NbData

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

  1. DdrMode

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

  1. DdrHoldHalfCycle

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

  1. SIOOMode

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

24.6 QSPI—读写串行FLASH实验

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

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

24.6.1 硬件设计

第24章 QSPI—读写串行FLASH_第4张图片

 

图 24-4 SPI串行FLASH硬件连接图

本实验板中的FLASH芯片(型号:W25Q128)是一种使用QSPI/SPI通讯协议的NOR FLASH存储器,它的CS/CLK/D0/D1/D2/D3引脚分别连接到了STM32对应的QSPI引脚QUADSPI_BK1_NCS/ QUADSPI_CLK / QUADSPI_BK1_IO0/ QUADSPI_BK1_IO1/ QUADSPI_BK1_IO2/ QUADSPI_BK1_IO3上,这些引脚都是STM32的复用引脚。

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

24.6.2 软件设计

为了使工程更加有条理,我们把读写FLASH相关的代码独立分开存储,方便以后移植。在“工程模板”之上新建“bsp_qspi_flash.c”及“bsp_qspi_ flash.h”文件,这些文件也可根据您的喜好命名,它们不属于STM32 HAL库的内容,是由我们自己根据应用需要编写的。

  1. 编程要点

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

2)使能SPI外设的时钟;

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

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

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

6)编写测试程序,对读写数据进行校验.

2.代码分析

​​​​​​​控制FLASH的指令

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

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

表 24-1 FLASH常用芯片指令表(摘自规格书《W25Q128》)

指令

第一字节(指令编码)

第二字节

第三字节

第四字节

第五字节

第六字节

第七-N字节

Write Enable

06h

 

 

 

 

 

 

Write Disable

04h

 

 

 

 

 

 

Read Status Register

05h

(S7–S0)

 

 

 

 

 

Write Status Register

01h

(S7–S0)

 

 

 

 

 

Read Data

03h

A23–A16

A15–A8

A7–A0

(D7–D0)

(Next byte)

continuous

Fast Read

0Bh

A23–A16

A15–A8

A7–A0

dummy

(D7–D0)

(Next Byte) continuous

Fast Read Dual Output

3Bh

A23–A16

A15–A8

A7–A0

dummy

I/O = (D6,D4,D2,D0) O = (D7,D5,D3,D1)

(one byte per 4 clocks, continuous)

Page Program

02h

A23–A16

A15–A8

A7–A0

D7–D0

Next byte

Up to 256 bytes

Block Erase(64KB)

D8h

A23–A16

A15–A8

A7–A0

 

 

 

Sector Erase(4KB)

20h

A23–A16

A15–A8

A7–A0

 

 

 

Chip Erase

C7h

 

 

 

 

 

 

Power-down

B9h

 

 

 

 

 

 

Release Power- down / Device ID

ABh

dummy

dummy

dummy

(ID7-ID0)

 

 

Manufacturer/ Device ID

90h

dummy

dummy

00h

(M7-M0)

(ID7-ID0)

 

JEDEC ID

9Fh

(M7-M0)

生产厂商

(ID15-ID8)

 存储器类型

(ID7-ID0) 容量

 

 

 

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

在FLSAH芯片内部,存储有固定的厂商编号(M7-M0)和不同类型FLASH芯片独有的编号(ID15-ID0),见表 24-2。

表 24-2 FLASH数据手册的设备ID说明

FLASH型号

厂商号(M7-M0)

FLASH型号(ID15-ID0)

W25Q64

EF h

4017 h

W25Q128

EF h

4018 h

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

此处我们以该指令为例,配合其指令时序图进行讲解,见图 24-5。

第24章 QSPI—读写串行FLASH_第5张图片

 

图 24-5 FLASH读ID指令“JEDEC ID”的时序(摘自规格书《W25Q128》)

主机首先通过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芯片的常用指令编码使用宏来封装起来,后面需要发送指令编码的时候我们直接使用这些宏即可,见代码清单 24-3。

代码清单 24-3 FLASH指令编码表

1 /**

 2   * @brief  W25Q128FV 指令

 3   */

 4 /* 复位操作 */

 5 #define RESET_ENABLE_CMD                     0x66

 6 #define RESET_MEMORY_CMD                     0x99

 7

 8 #define ENTER_QPI_MODE_CMD                   0x38

 9 #define EXIT_QPI_MODE_CMD                    0xFF

10

11 /* 识别操作 */

12 #define READ_ID_CMD                          0x90

13 #define DUAL_READ_ID_CMD                     0x92

14 #define QUAD_READ_ID_CMD                     0x94

15 #define READ_JEDEC_ID_CMD                    0x9F

16

17 /* 读操作 */

18 #define READ_CMD                             0x03

19 #define FAST_READ_CMD                        0x0B

20 #define DUAL_OUT_FAST_READ_CMD               0x3B

21 #define DUAL_INOUT_FAST_READ_CMD             0xBB

22 #define QUAD_OUT_FAST_READ_CMD               0x6B

23 #define QUAD_INOUT_FAST_READ_CMD             0xEB

24

25 /* 写操作 */

26 #define WRITE_ENABLE_CMD                     0x06

27 #define WRITE_DISABLE_CMD                    0x04

28

29 /* 寄存器操作 */

30 #define READ_STATUS_REG1_CMD                  0x05

31 #define READ_STATUS_REG2_CMD                  0x35

32 #define READ_STATUS_REG3_CMD                  0x15

33

34 #define WRITE_STATUS_REG1_CMD                 0x01

35 #define WRITE_STATUS_REG2_CMD                 0x31

36 #define WRITE_STATUS_REG3_CMD                 0x11

37

38

39 /* 编程操作 */

40 #define PAGE_PROG_CMD                        0x02

41 #define QUAD_INPUT_PAGE_PROG_CMD             0x32

42 #define EXT_QUAD_IN_FAST_PROG_CMD            0x12

43

44 /* 擦除操作 */

45 #define SECTOR_ERASE_CMD                     0x20

46 #define CHIP_ERASE_CMD                       0xC7

47

48 #define PROG_ERASE_RESUME_CMD                0x7A

49 #define PROG_ERASE_SUSPEND_CMD               0x75

SPI硬件相关宏定义

我们把SPI硬件相关的配置都以宏的形式定义到 “bsp_qspi_ flash.h”文件中,见代码清单 24-4。

代码清单 24-4  SPI硬件配置相关的宏

1 #define QSPI_FLASH                         QUADSPI

 2 #define QSPI_FLASH_CLK_ENABLE()            __QSPI_CLK_ENABLE()

 3

 4 #define QSPI_FLASH_CLK_PIN                 GPIO_PIN_2

 5 #define QSPI_FLASH_CLK_GPIO_PORT           GPIOB

 6 #define QSPI_FLASH_CLK_GPIO_ENABLE()       __GPIOB_CLK_ENABLE()

 7 #define QSPI_FLASH_CLK_GPIO_AF             GPIO_AF9_QUADSPI

 8

 9 #define QSPI_FLASH_BK1_IO0_PIN             GPIO_PIN_8

10 #define QSPI_FLASH_BK1_IO0_PORT            GPIOF

11 #define QSPI_FLASH_BK1_IO0_CLK_ENABLE()    __GPIOF_CLK_ENABLE()

12 #define QSPI_FLASH_BK1_IO0_AF              GPIO_AF10_QUADSPI

13

14 #define QSPI_FLASH_BK1_IO1_PIN             GPIO_PIN_9

15 #define QSPI_FLASH_BK1_IO1_PORT            GPIOF

16 #define QSPI_FLASH_BK1_IO1_CLK_ENABLE()    __GPIOF_CLK_ENABLE()

17 #define QSPI_FLASH_BK1_IO1_AF              GPIO_AF10_QUADSPI

18

19 #define QSPI_FLASH_BK1_IO2_PIN             GPIO_PIN_7

20 #define QSPI_FLASH_BK1_IO2_PORT            GPIOF

21 #define QSPI_FLASH_BK1_IO2_CLK_ENABLE()    __GPIOF_CLK_ENABLE()

22 #define QSPI_FLASH_BK1_IO2_AF              GPIO_AF9_QUADSPI

23

24 #define QSPI_FLASH_BK1_IO3_PIN             GPIO_PIN_6

25 #define QSPI_FLASH_BK1_IO3_PORT            GPIOF

26 #define QSPI_FLASH_BK1_IO3_CLK_ENABLE()    __GPIOF_CLK_ENABLE()

27 #define QSPI_FLASH_BK1_IO3_AF              GPIO_AF9_QUADSPI

28

29 #define QSPI_FLASH_CS_PIN                 GPIO_PIN_6

30 #define QSPI_FLASH_CS_GPIO_PORT           GPIOB

31 #define QSPI_FLASH_CS_GPIO_CLK_ENABLE()   __GPIOB_CLK_ENABLE()

32 #define QSPI_FLASH_CS_GPIO_AF             GPIO_AF10_QUADSPI

以上代码根据硬件连接,把与FLASH通讯使用的QSPI 、引脚号、引脚源以及复用功能映射都以宏封装起来。

初始化QSPI的 GPIO 

利用上面的宏,编写QSPI的初始化函数,见代码清单 24-5。

代码清单 24-5 QSPI的初始化函数

 1 /**

 2   * @brief  QSPI_FLASH引脚初始化

 3   * @param  无

 4   * @retval 无

 5   */

 6 void QSPI_FLASH_Init(void)

 7 {

 8

 9     GPIO_InitTypeDef GPIO_InitStruct;

10

11     /* 使能 QSPI 及 GPIO 时钟 */

12     QSPI_FLASH_CLK_ENABLE();

13     QSPI_FLASH_CLK_GPIO_ENABLE();

14     QSPI_FLASH_BK1_IO0_CLK_ENABLE();

15     QSPI_FLASH_BK1_IO1_CLK_ENABLE();

16     QSPI_FLASH_BK1_IO2_CLK_ENABLE();

17     QSPI_FLASH_BK1_IO3_CLK_ENABLE();

18     QSPI_FLASH_CS_GPIO_CLK_ENABLE();

19

20     //设置引脚

21     /*!< 配置 QSPI_FLASH 引脚: CLK */

22     GPIO_InitStruct.Pin = QSPI_FLASH_CLK_PIN;

23     GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;

24     GPIO_InitStruct.Pull = GPIO_NOPULL;

25     GPIO_InitStruct.Speed = GPIO_SPEED_HIGH;

26     GPIO_InitStruct.Alternate = QSPI_FLASH_CLK_GPIO_AF;

27

28     HAL_GPIO_Init(QSPI_FLASH_CLK_GPIO_PORT, &GPIO_InitStruct);

29

30     /*!< 配置 QSPI_FLASH 引脚: IO0 */

31     GPIO_InitStruct.Pin = QSPI_FLASH_BK1_IO0_PIN;

32     GPIO_InitStruct.Alternate = QSPI_FLASH_BK1_IO0_AF;

33     HAL_GPIO_Init(QSPI_FLASH_BK1_IO0_PORT, &GPIO_InitStruct);

34

35     /*!< 配置 QSPI_FLASH 引脚: IO1 */

36     GPIO_InitStruct.Pin = QSPI_FLASH_BK1_IO1_PIN;

37     GPIO_InitStruct.Alternate = QSPI_FLASH_BK1_IO1_AF;

38     HAL_GPIO_Init(QSPI_FLASH_BK1_IO1_PORT, &GPIO_InitStruct);

39

40     /*!< 配置 QSPI_FLASH 引脚: IO2 */

41     GPIO_InitStruct.Pin = QSPI_FLASH_BK1_IO2_PIN;

42     GPIO_InitStruct.Alternate = QSPI_FLASH_BK1_IO2_AF;

43     HAL_GPIO_Init(QSPI_FLASH_BK1_IO2_PORT, &GPIO_InitStruct);

44

45     /*!< 配置 QSPI_FLASH 引脚: IO3 */

46     GPIO_InitStruct.Pin = QSPI_FLASH_BK1_IO3_PIN;

47     GPIO_InitStruct.Alternate = QSPI_FLASH_BK1_IO3_AF;

48     HAL_GPIO_Init(QSPI_FLASH_BK1_IO3_PORT, &GPIO_InitStruct);

49

50     /*!< 配置 SPI_FLASH_SPI 引脚: NCS */

51     GPIO_InitStruct.Pin = QSPI_FLASH_CS_PIN;

52     GPIO_InitStruct.Alternate = QSPI_FLASH_CS_GPIO_AF;

53     HAL_GPIO_Init(QSPI_FLASH_CS_GPIO_PORT, &GPIO_InitStruct);

54

55     /* QSPI_FLASH 模式配置 */

56     QSPIHandle.Instance = QUADSPI;

57     QSPIHandle.Init.ClockPrescaler = 2;

58     QSPIHandle.Init.FifoThreshold = 4;

59     QSPIHandle.Init.SampleShifting = QSPI_SAMPLE_SHIFTING_HALFCYCLE;

60     QSPIHandle.Init.FlashSize = 23;

61     QSPIHandle.Init.ChipSelectHighTime = QSPI_CS_HIGH_TIME_8_CYCLE;

62     QSPIHandle.Init.ClockMode = QSPI_CLOCK_MODE_0;

63     HAL_QSPI_Init(&QSPIHandle);

64     /*初始化QSPI接口*/

65     BSP_QSPI_Init();

66 }

与所有使用到GPIO的外设一样,都要先把使用到的GPIO引脚模式初始化,配置好复用功能。GPIO初始化流程如下:

  1. 使用GPIO_InitTypeDef定义GPIO初始化结构体变量,以便下面用于存储GPIO配置;
  2. 调用宏定义使能QSPI引脚使用的GPIO端口时钟和QSPI外设时钟。
  3. 向GPIO初始化结构体赋值,把CLK/IO0/IO1/IO2/IO3/NCS引脚初始化成复用推挽模式。
  4. 使用以上初始化结构体的配置,调用HAL_GPIO_Init函数向寄存器写入参数,完成GPIO的初始化。
  5. 以上只是配置了QSPI使用的引脚,对QSPI外设模式的配置。在配置STM32的QSPI模式前,我们要先了解从机端的QSPI模式。本例子中可通过查阅FLASH数据手册《W25Q128》获取。根据FLASH芯片的说明,它支持SPI模式0及模式3,支持四线模式,支持最高通讯时钟为104MHz,数据帧长度为8位。我们要把STM32的QSPI外设中的这些参数配置一致。见代码清单 245。
  6. 配置QSPI接口模式;时钟三分频最高通讯时钟为72MHz(Flash最高支持104MHz);FIFO 阈值为 4 个字节;采样移位半个周期;SPI FLASH 大小;W25Q128 大小为16M 字节,即这里地址位数为23+1=24,所以取值23;片选高电平时间为 1个时钟(9.2*6=55.2ns),即手册里面的 ;时钟模式选择为0;根据硬件连接选择第一片Flash;最后调用HAL_QSPI_Init函数初始化QSPI模式。

初始化QSPI存储器

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

代码清单 24-6初始化QSPI存储器

1 /**

 2   * @brief  初始化QSPI存储器

 3   * @retval QSPI存储器状态

 4   */

 5 uint8_t BSP_QSPI_Init(void)

 6 {

 7     QSPI_CommandTypeDef s_command;

 8     uint8_t value = W25Q128FV_FSR_QE;

 9

10     /* QSPI存储器复位 */

11     if (QSPI_ResetMemory() != QSPI_OK) {

12         return QSPI_NOT_SUPPORTED;

13     }

14

15     /* 使能写操作 */

16     if (QSPI_WriteEnable() != QSPI_OK) {

17         return QSPI_ERROR;

18     }

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

20     s_command.InstructionMode   = QSPI_INSTRUCTION_1_LINE;

21     s_command.Instruction       = WRITE_STATUS_REG2_CMD;

22     s_command.AddressMode       = QSPI_ADDRESS_NONE;

23     s_command.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;

24     s_command.DataMode          = QSPI_DATA_1_LINE;

25     s_command.DummyCycles       = 0;

26     s_command.NbData            = 1;

27     s_command.DdrMode           = QSPI_DDR_MODE_DISABLE;

28     s_command.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;

29     s_command.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;

30     /* 配置命令 */

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

32         return QSPI_ERROR;

33     }

34     /* 传输数据 */

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

36         return QSPI_ERROR;

37     }

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

39     if (QSPI_AutoPollingMemReady(W25Q128FV_SUBSECTOR_ERASE_MAX_TIME) != QSPI_OK) {

40         return QSPI_ERROR;

41     }

42     return QSPI_OK;

43 }

使用QSPI读取大量数据

我们要从存取器中读取大量数据,首先要用一个指针指向读回来数据,并确定数据的首地址,数据大小,通过库函数HAL_QSPI_Command发送配置命令,然后调用库函数HAL_QSPI_Receive接收数据,最后等待操作完成,我们看看它的代码实现,见代码清单 24-7。

代码清单 24-7 使用QSPI读取大量数据

1 /**

 2   * @brief  从QSPI存储器中读取大量数据.

 3   * @param  pData: 指向要读取的数据的指针

 4   * @param  ReadAddr: 读取起始地址

 5   * @param  Size: 要读取的数据大小

 6   * @retval QSPI存储器状态

 7   */

 8 uint8_t BSP_QSPI_Read(uint8_t* pData, uint32_t ReadAddr, uint32_t Size)

 9 {

10     QSPI_CommandTypeDef s_command;

11     /* 初始化读命令 */

12     s_command.InstructionMode   = QSPI_INSTRUCTION_1_LINE;

13     s_command.Instruction       = READ_CMD;

14     s_command.AddressMode       = QSPI_ADDRESS_1_LINE;

15     s_command.AddressSize       = QSPI_ADDRESS_24_BITS;

16     s_command.Address           = ReadAddr;

17     s_command.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;

18     s_command.DataMode          = QSPI_DATA_1_LINE;

19     s_command.DummyCycles       = 0;

20     s_command.NbData            = Size;

21     s_command.DdrMode           = QSPI_DDR_MODE_DISABLE;

22     s_command.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;

23     s_command.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;

24

25     /* 配置命令 */

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

27         return QSPI_ERROR;

28     }

29

30     /* 接收数据 */

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

32         return QSPI_ERROR;

33     }

34     return QSPI_OK;

35 }

使用QSPI写入大量数据

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

代码清单 24-8 使用QSPI读取大量数据

1 /**

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

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

 4   * @param  WriteAddr: 写起始地址

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

 6   * @retval QSPI存储器状态

 7   */

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

 9 {

10     QSPI_CommandTypeDef s_command;

11     uint32_t end_addr, current_size, current_addr;

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

13     current_addr = 0;

14

15     while (current_addr <= WriteAddr) {

16         current_addr += W25Q128FV_PAGE_SIZE;

17     }

18     current_size = current_addr - WriteAddr;

19

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

21     if (current_size > Size) {

22         current_size = Size;

23     }

24

25     /* 初始化地址变量 */

26     current_addr = WriteAddr;

27     end_addr = WriteAddr + Size;

28

29     /* 初始化程序命令 */

30     s_command.InstructionMode   = QSPI_INSTRUCTION_1_LINE;

31     s_command.Instruction       = QUAD_INPUT_PAGE_PROG_CMD;

32     s_command.AddressMode       = QSPI_ADDRESS_1_LINE;

33     s_command.AddressSize       = QSPI_ADDRESS_24_BITS;

34     s_command.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;

35     s_command.DataMode          = QSPI_DATA_4_LINES;

36     s_command.DummyCycles       = 0;

37     s_command.DdrMode           = QSPI_DDR_MODE_DISABLE;

38     s_command.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;

39     s_command.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;

40

41     /* 逐页执行写入 */

42     do {

43         s_command.Address = current_addr;

44         s_command.NbData  = current_size;

45

46         /* 启用写操作 */

47         if (QSPI_WriteEnable() != QSPI_OK) {

48             return QSPI_ERROR;

49         }

50

51         /* 配置命令 */

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

53             return QSPI_ERROR;

54         }

55

56         /* 传输数据 */

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

58             return QSPI_ERROR;

59         }

60

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

62         if(QSPI_AutoPollingMemReady(HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != QSPI_OK) {

63             return QSPI_ERROR;

64         }

65

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

67         current_addr += current_size;

68         pData += current_size;

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

70                        W25Q128FV_PAGE_SIZE;

71     } while (current_addr < end_addr);

72     return QSPI_OK;

73 }

读取FLASH芯片ID

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

代码清单 24-9 读取FLASH芯片ID

1 /**

 2  * @brief  读取FLASH ID

 3  * @param   无

 4  * @retval FLASH ID

 5  */

 6 uint32_t QSPI_FLASH_ReadID(void)

 7 {

 8     QSPI_CommandTypeDef s_command;

 9     uint32_t Temp = 0;

10     uint8_t pData[3];

11     /* 读取JEDEC ID */

12     s_command.InstructionMode   = QSPI_INSTRUCTION_1_LINE;

13     s_command.Instruction       = READ_JEDEC_ID_CMD;

14     s_command.AddressMode       = QSPI_ADDRESS_1_LINE;

15     s_command.AddressSize       = QSPI_ADDRESS_24_BITS;

16     s_command.DataMode          = QSPI_DATA_1_LINE;

17     s_command.AddressMode       = QSPI_ADDRESS_NONE;

18     s_command.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;

19     s_command.DummyCycles       = 0;

20     s_command.NbData            = 3;

21     s_command.DdrMode           = QSPI_DDR_MODE_DISABLE;

22     s_command.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;

23     s_command.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;

24

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

26         printf("something wrong ....\r\n");

27         /* 用户可以在这里添加一些代码来处理这个错误 */

28         while (1) {

29

30         }

31     }

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

33         printf("something wrong ....\r\n");

34         /* 用户可以在这里添加一些代码来处理这个错误 */

35         while (1) {

36

37         }

38     }

39

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

41

42     return Temp;

43 }

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

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

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

代码清单 24-10 写使能命令

1 /**

 2   * @brief  发送写入使能,等待它有效.

 3   * @param  QSPIHandle: QSPI句柄

 4   * @retval 无

 5   */

 6 static uint8_t QSPI_WriteEnable()

 7 {

 8     QSPI_CommandTypeDef     s_command;

 9     QSPI_AutoPollingTypeDef s_config;

10     /* 启用写操作 */

11     s_command.InstructionMode   = QSPI_INSTRUCTION_1_LINE;

12     s_command.Instruction       = WRITE_ENABLE_CMD;

13     s_command.AddressMode       = QSPI_ADDRESS_NONE;

14     s_command.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;

15     s_command.DataMode          = QSPI_DATA_NONE;

16     s_command.DummyCycles       = 0;

17     s_command.DdrMode           = QSPI_DDR_MODE_DISABLE;

18     s_command.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;

19     s_command.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;

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

21         return QSPI_ERROR;

22     }

23

24     /* 配置自动轮询模式等待写启用 */

25     s_config.Match           = W25Q128FV_FSR_WREN;

26     s_config.Mask            = W25Q128FV_FSR_WREN;

27     s_config.MatchMode       = QSPI_MATCH_MODE_AND;

28     s_config.StatusBytesSize = 1;

29     s_config.Interval        = 0x10;

30     s_config.AutomaticStop   = QSPI_AUTOMATIC_STOP_ENABLE;

31

32     s_command.Instruction    = READ_STATUS_REG1_CMD;

33     s_command.DataMode       = QSPI_DATA_1_LINE;

34     s_command.NbData         = 1;

35

36if(HAL_QSPI_AutoPolling(&QSPIHandle, &s_command, &s_config, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK){

37         return QSPI_ERROR;

38     }

39     return QSPI_OK;

40 }

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

第24章 QSPI—读写串行FLASH_第6张图片

 

图 24-6 FLASH芯片的状态寄存器

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

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

第24章 QSPI—读写串行FLASH_第7张图片

 

图 24-7 读取状态寄存器的时序

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

代码清单 24-11 通过读状态寄存器等待FLASH芯片空闲

1 /**

 2   * @brief  读取存储器的SR并等待EOP

 3   * @param  QSPIHandle: QSPI句柄

 4   * @param  Timeout 超时

 5   * @retval 无

 6   */

 7 static uint8_t QSPI_AutoPollingMemReady(uint32_t Timeout)

 8 {

 9     QSPI_CommandTypeDef     s_command;

10     QSPI_AutoPollingTypeDef s_config;

11     /* 配置自动轮询模式等待存储器准备就绪 */

12     s_command.InstructionMode   = QSPI_INSTRUCTION_1_LINE;

13     s_command.Instruction       = READ_STATUS_REG1_CMD;

14     s_command.AddressMode       = QSPI_ADDRESS_NONE;

15     s_command.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;

16     s_command.DataMode          = QSPI_DATA_1_LINE;

17     s_command.DummyCycles       = 0;

18     s_command.DdrMode           = QSPI_DDR_MODE_DISABLE;

19     s_command.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;

20     s_command.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;

21

22     s_config.Match           = 0x00;

23     s_config.Mask            = W25Q128FV_FSR_BUSY;

24     s_config.MatchMode       = QSPI_MATCH_MODE_AND;

25     s_config.StatusBytesSize = 1;

26     s_config.Interval        = 0x10;

27     s_config.AutomaticStop   = QSPI_AUTOMATIC_STOP_ENABLE;

28

29     if (HAL_QSPI_AutoPolling(&QSPIHandle, &s_command, &s_config, Timeout) != HAL_OK) {

30         return QSPI_ERROR;

31     }

32     return QSPI_OK;

33 }

这段代码直接调用HAL_QSPI_AutoPolling库函数,设定命令参数及自动轮询参数,最后设定超时返回,如果在超时等待时间内确定FLASH就绪则返回存储器就绪状态,否则返回存储器错误。其实主要操作就是检查它的“W25Q128FV_FSR_BUSY”(即BUSY位),一直等待到该标志表示写入结束时才退出本函数,以便继续后面与FLASH芯片的数据通讯。

FLASH扇区擦除

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

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

表 24-3 本实验FLASH芯片的擦除单位

擦除单位

大小

扇区擦除Sector Erase

4KB

块擦除Block Erase

64KB

整片擦除Chip Erase

整个芯片完全擦除

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

第24章 QSPI—读写串行FLASH_第8张图片

 

图 24-8 FLASH芯片的存储矩阵

使用扇区擦除指令“Sector Erase”可控制FLASH芯片开始擦写,其指令时序见错误!未找到引用源。

第24章 QSPI—读写串行FLASH_第9张图片

 

图 24-9 扇区擦除时序

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

代码清单 24-12 擦除扇区

1 /**

 2   * @brief  擦除QSPI存储器的指定块

 3   * @param  BlockAddress: 需要擦除的块地址

 4   * @retval QSPI存储器状态

 5   */

 6 uint8_t BSP_QSPI_Erase_Block(uint32_t BlockAddress)

 7 {

 8     QSPI_CommandTypeDef s_command;

 9     /* 初始化擦除命令 */

10     s_command.InstructionMode   = QSPI_INSTRUCTION_1_LINE;

11     s_command.Instruction       = SECTOR_ERASE_CMD;

12     s_command.AddressMode       = QSPI_ADDRESS_1_LINE;

13     s_command.AddressSize       = QSPI_ADDRESS_24_BITS;

14     s_command.Address           = BlockAddress;

15     s_command.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;

16     s_command.DataMode          = QSPI_DATA_NONE;

17     s_command.DummyCycles       = 0;

18     s_command.DdrMode           = QSPI_DDR_MODE_DISABLE;

19     s_command.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;

20     s_command.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;

21

22     /* 启用写操作 */

23     if (QSPI_WriteEnable() != QSPI_OK) {

24         return QSPI_ERROR;

25     }

26

27     /* 发送命令 */

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

29         return QSPI_ERROR;

30     }

31

32     /* 配置自动轮询模式等待擦除结束 */

33     if (QSPI_AutoPollingMemReady(W25Q128FV_SUBSECTOR_ERASE_MAX_TIME) != QSPI_OK) {

34         return QSPI_ERROR;

35     }

36     return QSPI_OK;

37 }

3.  main.函数

最后我们来编写main函数,进行FLASH芯片读写校验,见错误!未找到引用源。

代码清单 24-13 main函数

1 int main(void)

 2 {

 3     /* 使能指令缓存 */

 4     SCB_EnableICache();

 5

 6     /* 使能数据缓存 */

 7     SCB_EnableDCache();

 8

 9     /* 设定系统时钟为216MHz */

10     SystemClock_Config();

11

12     LED_GPIO_Config();

13     LED_BLUE;

14

15     /* 配置串口1为:115200 8-N-1 */

16     DEBUG_USART_Config();

17

18     printf("\r\n这是一个16M串行flash(W25Q128)实验(QSPI驱动) \r\n");

19

20     /* 16M串行flash W25Q128初始化 */

21     QSPI_FLASH_Init();

22

23     /* 获取 Flash Device ID */

24     DeviceID = QSPI_FLASH_ReadDeviceID();

25

26     Delay( 200 );

27

28     /* 获取 SPI Flash ID */

29     FlashID = QSPI_FLASH_ReadID();

30

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

32

33     /* 检验 SPI Flash ID */

34     if (FlashID == sFLASH_ID) {

35         printf("\r\n检测到QSPI FLASH W25Q128 !\r\n");

36

37         /* 擦除将要写入的 QSPI FLASH 扇区,FLASH写入前要先擦除 */

38         BSP_QSPI_Erase_Block(FLASH_SectorToErase);

39

40         /* 将发送缓冲区的数据写到flash中 */

41         BSP_QSPI_Write(Tx_Buffer, FLASH_WriteAddress, BufferSize);

42         printf("\r\n写入的数据为:\r\n%s", Tx_Buffer);

43

44         /* 将刚刚写入的数据读出来放到接收缓冲区中 */

45         BSP_QSPI_Read(Rx_Buffer, FLASH_ReadAddress, BufferSize);

46         printf("\r\n读出的数据为:\r\n%s", Rx_Buffer);

47

48         /* 检查写入的数据与读出的数据是否相等 */

49         TransferStatus1 = Buffercmp(Tx_Buffer, Rx_Buffer, BufferSize);

50

51         if ( PASSED == TransferStatus1 ) {

52             LED_GREEN;

53             printf("\r\n16M串行flash(W25Q128)测试成功!\n\r");

54         } else {

55             LED_RED;

56             printf("\r\n16M串行flash(W25Q128)测试失败!\n\r");

57         }

58     }// if (FlashID == sFLASH_ID)

59     else {

60         LED_RED;

61         printf("\r\n获取不到 W25Q128 ID!\n\r");

62     }

63

64     while (1);

65 }

函数中初始化了系统时钟、LED、串口SPI外设,然后读取FLASH芯片的ID进行校验,若ID校验通过则向FLASH的特定地址写入测试数据,然后再从该地址读取数据,测试读写是否正常。

 

注意:

由于实验板上的FLASH芯片默认已经存储了特定用途的数据,如擦除了这些数据会影响到某些程序的运行。所以我们预留了FLASH芯片的“第0扇区(0-4096地址)”专用于本实验,如非必要,请勿擦除其它地址的内容。如已擦除,可在配套资料里找到“刷外部FLASH内容”程序,根据其说明给FLASH重新写入出厂内容。

24.6.3 下载验证

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

你可能感兴趣的:(第24章 QSPI—读写串行FLASH)