第三十六章 QSPI实验​


本章,我们将介绍STM32H750的QSPI功能,并使用STM32H750自带的QSPI来实现对外部NOR FLASH的读写,并将结果显示在LCD模块上。

本章分为如下几个小节:

36.1 QSPI及NOR FLASH芯片简介

36.2 硬件设计

36.3 程序设计

36.4 下载验证



36.1 QSPI及NOR FLASH芯片简介

36.1.1 QSPI简介

QSPI是Quad SPI的缩写,是Motorola公司推出SPI接口后的一种扩展接口,较SPI应用更为广泛。在 SPI 协议的基础上,Motorola 公司对其功能进行了增加,增加了队列传输机制,推出了队列串行外围接口协议(即 QSPI 协议)。QSPI 是一种专用的通信接口,连接单、双或四(条数据线)SPI FLASH存储器。STM32H7具有QSPI接口,支持如下三种工作模式:

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

2、状态轮询模式:周期性读取外部FLASH状态寄存器,当标志位置1时会产生中断(如

擦除或烧写完成,产生中断)。

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

STM32H7的QSPI接口具有如下特点:

  • 支持三种工作模式:间接模式、状态轮询模式和内存映射模式。
  • 支持双闪存模式,可以并行访问两个FLASH,可同时发送/接收8位数据。
  • 支持SDR(单倍率速率)和DDR(双倍率速率)模式。
  • 针对间接模式和内存映射模式,完全可编程操作码。
  • 针对间接模式和内存映射模式,完全可编程帧格式。
  • 集成 FIFO,用于发送和接收。
  • 允许 816 32 位数据访问。
  • 具有适用于间接模式操作的DMA通道。
  • 在达到 FIFO 阈值、超时、操作完成以及发生访问错误时产生中断。

36.1.1.1 QSPI框图

STM32H7的QSPI单闪存模式的功能框图如图36.1.1.1.1所示:

《MiniPRO H750开发指南》第三十六章 QSPI实验_寄存器


图36.1.1.1.1框图

上图左边可以看到QSPI连接到64位 AXI 总线以及32位AHB总线上,此外还有5条QSPI的内部信号,如下表所示:

信号名称

信号类型

说明

quadspi_ker_ck

数字输入

QUADSPI 内核时钟

quadspi_hclk

数字输入

QUADSPI 寄存器接口时钟

quadspi_it

数字输出

QUADSPI 全局中断

quadspi_ft_trg

数字输出

MDMA 的 QUADSPI FIFO阈值触发信号

quadspi_tc_trg

数字输出

MDMA 的 QUADSPI 传输完成触发信号

表36.1.1.1.1 QSPI内部信号

quadspi_ker_ck,用于通讯过程的时钟。可以选择的时钟源有:HCLK3(即AHP3)、PLL1Q、PLL2R和 PER_CK,实验中我们选择PLL2R。经过sys_stm32_clock_init函数的配置,PLL2R时钟频率为220MHZquadspi_ker_ck还需要经过一个分频器出来的时钟频率才作为QSPI的实际使用的时钟频率,该分频器的分频系数由QUADSPI_CR寄存器的PRESCALER[7:0]位设置,范围是:0~255

quadspi_hclk用于操作QUADSPI寄存器的时钟。时钟源来自HCLK3,同样是经过sys_stm32_clock_init函数的配置,配置后HCLK3时钟频率为240MHZ

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

quadsqp_ft_trg,达到FIFO阈值时触发MDMA请求。

quadspi_tc_trg,操作完成时触发MDMA请求

上图中间部分就是QUADSPI 内核。

我们重点看看上图右边的QSPI接口引脚,通过6根线与SPI FLASH芯片连接,包括:4根数据线(IO0~3)、1根时钟线(CLK)和1根片选线(nCS),具体如下表所示:

引脚名称

信号类型

说明

CLK

数字输出

时钟,由主机产生,用于通讯数据同步,决定通信速率

BK1_IO0/SO

数字输入/输出

在双线/四线模式中为双向 IO,单线模式中为串行输出

BK1_IO1/SI

数字输入/输出

在双线/四线模式中为双向 IO,单线模式中为串行输入

BK1_IO2

数字输入/输出

在四线模式中为双向 IO

BK1_IO3

数字输入/输出

在四线模式中为双向 IO

BK1_nCS

数字输出

片选输出(低电平有效)

表36.1.1.1.2 QSPI接口引脚

我们知道普通的SPI通信一般只有一根数据线(MOSI/MISO,发送/接收用),而QSPI则具有4根数据线,所以QSPI的速率至少是普通SPI的4倍,可以大大提高通信速率。如果使用双闪存模式,同时访问两个Quad-SPI Flash,速度可以再翻一番。我们开发板板载了一个Quad-SPI Flash,所以我们使用单闪存模式,双闪存模式就不具体介绍了,感兴趣请自行查看手册。

接下来,我们给大家简单介绍一下STM32H7 QSPI接口的的几个重要知识点。

36.1.1.2 QSPI命令序列

QSPI 通过命令与FLASH通信,每条命令包括:指令、地址、交替字节、空指令和数据这五个阶段,任一阶段均可通过配置QUADSPI_CCR寄存器相关字段跳过,但至少要包含指令、地址、交替字节或数据阶段之一。

nCS 在每条指令开始前下降,在每条指令完成后再次上升。QSPI四线模式下的读命令时序,如图36.1.1.2.1所示:

《MiniPRO H750开发指南》第三十六章 QSPI实验_数据_02


图36.1.1.2.1四线模式下QSPI读命令时序

从上图可以看出一次QSPI传输的5个阶段,接下来我们分别介绍。

① 指令阶段

此阶段通过QUADSPI_CCR[7:0]寄存器的INSTRUCTION字段指定一个8位指令发送到FLASH。注意,指令阶段,一般是通过IO0单线发送,但是也可以配置为双线/四线发送指令,可以通过QUADSPI_CCR[9:8]寄存器的IMODE[1:0]这两个位进行配置,如IMODE[1:0]=00,则表示无需发送指令。

  1. 地址阶段 此阶段可以发送1~4字节地址给FLASH芯片,指示要操作的地址。地址字节长度由QUADSPI_CCR[13:12]寄存器的ADSIZE[1:0]字段指定,0~3表示1~4字节地址长度。在间接模式和轮询模式下,待发送的地址由QUADSPI_AR寄存器指定。地址阶段同样可以以单线/双线/四线模式发送,通过QUADSPI_CCR[11:10]寄存器的ADMODE[1:0]这两个位进行配置,如ADMODE [1:0]=00,则表示无需发送地址。
  2. 交替字节(复用字节)阶段此阶段可以发送1~4字节数据给FLASH芯片,一般用于控制操作模式。待发送的交替字节数由QUADSPI_CCR[17:16]寄存器的ABSIZE[1:0]位配置。待发送的数据由QUADSPI_ABR寄存器中指定。交替字节同样可以以单线/双线/四线模式发送,通过QUADSPI_CCR[15:14]寄存器的ABMODE[1:0]这两个位配置,ABMODE[1:0]=00,则跳过交替字节阶段。
  3. 空指令周期阶段在空指令周期阶段,在给定的1~31个周期内不发送或接收任何数据,目的是当采用更高的时钟频率时,给FLASH芯片留出准备数据阶段的时间。这一阶段中给定的周期数由QUADSPI_CCR[22:18]寄存器的DCYC[4:0]位配置。 若DCYC为零,则跳过空指令周期阶段,命令序列直接进入下一个阶段。
  4. 数据阶段

此阶段可以从FLASH读取/写入任意字节数量的数据。在间接模式和自动轮询模式下,待发送/接收的字节数由QUADSPI_DLR寄存器指定。在间接写入模式下,发送到FLASH的数据必须写入QUADSPI_DR寄存器。在间接读取模式下,通过读取QUADSPI_DR寄存器获得从 FLASH接收的数据。数据阶段同样可以以单线/双线/四线模式发送,通过QUADSPI_CCR[25:24]寄存器的DMODE [1:0]这两个位进行配置,如DMODE [1:0]=00,则表示无数据。

以上就是QSPI数据传输的5个阶段,其中交替字节阶段我们一般用不到,可以省略(通过设置ABMODE[1:0]=00)。

另外说明一下,QUADSPI信号接口协议模式包括:单线SPI模式、双线SPI模式、四线SPI模式、SDR模式、DDR模式和双闪存模式。这些模式请大家自行参考官方手册。

36.1.1.3 QSPI三种功能模式

前面已经提到过QSPI的三种功能模式:间接模式、状态标志轮询模式和内存映射模式。下面对这三个功能模式分别简单介绍。

① 间接模式

在间接模式下,通过写入QUADSPI寄存器来触发命令,通过读写数据寄存器来传输数据。

当FMODE=00 (QUADSPI_CCR[27:26])时,QUADSPI处于间接写入模式,在数据阶段,将数据写入数据寄存器(QUADSPI_DR),即可写入数据到FLASH。

当FMODE=01时,QUADSPI处于间接读取模式,在数据阶段,读取QUADSPI_DR寄存器,即可读取FLASH里面的数据。

读/写字节数由数据长度寄存器(QUADSPI_DLR)指定。当QUADSPI_DLR=0xFFFFFFFF时,则数据长度视为未定义,QUADSPI 将持续传输数据,直到到达FLASH结尾(FLASH容量由 QUADSPI_DCR[20:16]寄存器的FSIZE[4:0]位定义)。如果不传输任何数据,则DMODE[1:0] (QUADSPI_CCR[25:24])应设置为00。

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

在间接模式下有三种触发命令启动的方式,分别是:

(1)当不需要发送地址(ADMODE[1:0]==00)和数据(DMODE[1:0]==00)时,对INSTRUCTION[7:0](QUADSPI_CCR[7:0])执行写入操作。

2)当需要发送地址(ADMODE[1:0]!=00),但不需要发送数据(DMODE[1:0]==00),对ADDRESS[31:0](QUADSPI_AR)执行写入操作。

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

如果命令启动,BUSY位(QUADSPI_SR的第5位)将自动置1。

②状态标志轮询模式

将FMODE字段(QUADSPI_CCR[27:26]) 设置为10,使能状态标志轮询模式。在此模式下,将发送编程的帧并周期性检索数据。每帧中读取的最大数据量为4字节。如果QUADSPI_DLR请求更多的数据,则忽略多余部分并仅读取4个字节。在 QUADSPI_PISR 寄存器指定周期性。

在检索到状态数据后,可在内部进行处理,以达到以下目的:

(1)将状态匹配标志位置 1,如果使能,还将产生中断.

(2)自动停止周期性检索状态字节。

接收到的值可通过存储于QUADSPI_PSMKR寄存器中的值来进行屏蔽,并与存储在 QUADSPI_PSMAR寄存器中的值进行或运算或与运算。

若是存在匹配,则状态匹配标志置1,并且在使能了中断的情况下还将产生中断;如果AMPS 位置1,则QUADSPI自动停止。

在任何情况下,最新的检索值都在QUADSPI_DR中可用。

  1. 内存映射模式

在配置为内存映射模式时,外部FLASH器件被视作内部存储器,只是存在访问延迟。在该模式下,仅允许对外部 FLASH 执行读取操作。将QUADSPI_CCR寄存器中的FMODE设置为11可进入内存映射模式。当主器件访问存储器映射空间时,将发送已编程的指令和帧。另外数据长度寄存器(QUADSPI_DLR)在内存映射模式中无意义。

QUADSPI外设若没有正确配置并使能,禁止访问QUADSPI Flash的存储区域。即使FLASH容量更大,寻址空间也无法超过256MB。如果访问的地址超出FSIZE定义的范围但仍在256MB 范围内,则生成总线错误。此错误的影响具体取决于尝试进行访问的总线主器件:

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

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

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

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

36.1.1.4 QSPI FLASH配置

SPI FLASH芯片的相关参数通过器件配置寄存器 (QUADSPI_DCR) 来进行设置。寄存器QUADSPI_DCR[20:16]的FSIZE[4:0]这5个位,用于指定外部存储器的大小,计算公式为:

Fcap=2^[FSIZE+1]

FSIZE+1是对Flash寻址所需的地址位数。Fcap表示FLASH的容量,单位为字节,在间接模式下,最高支持4GB(使用32位进行寻址)容量的FLASH芯片。但是在内存映射模式下的可寻址空间限制为256MB。

QSPI连续执行两条命令时,它在两条命令之间将片选信号 (nCS) 置为高电平默认仅一个 CLK周期。某些FLASH需要命令之间的时间更长,可以通过寄存器QUADSPI_DCR[10:8]的CSHT[2:0](选高电平时间)这3个位设置高电平时长:0~7表示1~8个时钟周期(最大为8)。

时钟模式,用于指定在nCS为高电平时,CLK的时钟极性。通过寄存器QUADSPI_DCR[0]的CKMODE位指定:当CKMODE=0时,CLK在nCS为高电平期间保持低电平,称之为模式0;当CKMODE=1时,CLK在nCS为高电平期间保持高电平,称之为模式3。

36.1.1.5 QUADSPI寄存器

  • QUADSPI控制寄存器(QUADSPI_CRQUADSPI控制寄存器描述如图36.1.1.5.1所示:
  • 《MiniPRO H750开发指南》第三十六章 QSPI实验_数据_03


  • 图36.1.1.5.1 QUADSPI_CR寄存器
    该寄存器我们只关心需要用到的一些位(下同),首先是PRESCALER[7:0],用于设置AHB时钟预分频器:0~255,表示0~256分频。我们使用的W25Q128最大支持104Mhz的时钟,这里我们设置PRESCALER=2,即3分频,得到QSPI时钟为72Mhz(216/3)。
    FTHRES[4:0],用于设置FIFO阈值,范围为0~31,表示FIFO的阈值为1~32字节。
    FSEL位,用于选择FLASH,我们的W25Q128连接在STM32H7的QSPI BK1上面,所以设置此位为0即可。
    DFM位,用于设置双闪存模式,我们用的是单闪存模式,所以设置此位为0即可。
    SSHIFT位,用于设置采样移位,默认情况下,QSPI接口在FLASH驱动数据后过半个CLK 周期开始采集数据。使用该位,可考虑外部信号延迟,推迟数据采集。我们一般设置此位为1,移位半个周期采集,确保数据稳定。
    ABORT位,用于终止QSPI的当前传输,设置为1即可终止当前传输,在读写FLASH数据的时候,可能会用到。
    EN位,用于控制QSPI的使能,我们需要用到QSPI接口,所以必须设置此位为1。
  • QUADSPI器件配置寄存器(QUADSPI_ DCRQUADSPI器件配置寄存器描述如图36.1.1.5.2所示:
  • 《MiniPRO H750开发指南》第三十六章 QSPI实验_数据_04


  • 图36.1.1.5.2 QUADSPI_ DCR寄存器
    该寄存器可以设置FLASH芯片的容量(FSIZE)、片选高电平时间(CSHT)和时钟模式(CKMODE)等,这些位的设置说明见前面的36.1.1.4小节有详细讲解。
  • QUADSPI状态寄存器(QUADSPI_ SRQUADSPI状态寄存器描述如图36.1.1.5.3所示:
  • 《MiniPRO H750开发指南》第三十六章 QSPI实验_寄存器_05


  • 图36.1.1.5.3 QUADSPI_ SR寄存器
    BUSY位,指示操作是否忙。当该位为1时,表示QSPI正在执行操作。在操作完成或者FIFO为空的时候,该位自动清零。
    FTF位,表示FIFO是否到达阈值。在间接模式下,若达到FIFO阈值,或从FLASH读取完成后,FIFO中留有数据时,该位置1。只要阈值条件不再为“真”,该位就自动清零。
    TCF位,表示传输是否完成。在间接模式下,当传输的数据数量达到编程设定值,或在任何模式下传输中止时,该位置1。向QUADSPI_FCR寄存器的CTCF位写1,可以清零此位。
  • QUADSPI标志清零寄存器(QUADSPI_ FCRQUADSPI标志清零寄存器描述如图36.1.1.5.4所示:
  • 《MiniPRO H750开发指南》第三十六章 QSPI实验_数据_06


  • 图36.1.1.5.4 QUADSPI_ FCR寄存器
    该寄存器,我们一般只用到CTCF位,用于清除QSPI的传输完成标志。
  • QUADSPI通信配置寄存器(QUADSPI_ CCR

QUADSPI通信配置寄存器描述如图36.1.1.5.5所示:

《MiniPRO H750开发指南》第三十六章 QSPI实验_数据_07


图36.1.1.5.5 QUADSPI_ CCR寄存器

DDRM位,用于设置双倍率模式(DDR),我们没用到双倍率模式,所以设置此位为0。

SIOO位,用于设置指令是否只发送一次,我们需要每次都发送指令,所以设置此位为0。

FMODE[1:0],这两个位用于设置功能模式:00,间接写入模式;01,间接读取模式;10,自动轮询模式;11,内存映射模式;我们使用间接模式,所以此位根据需要设置为00/01。

DMODE[1:0],这两个位用于设置数据模式:00,无数据;01,单线传输数据;10,双线传输数据;11,四线传输数据;我们一般设置为00/11。

DCYC[4:0],这5个位用于设置空指令周期数,可以控制空指令阶段的持续时间,设置范围为:0~31。设置为0,则表示没有空指令周期。

ABMODE[1:0],这两个位用于设置交替字节模式,我们一般设置为0,表示无交替字节。

ADMODE[1:0],这两个位用于设置地址模式:00,无地址;01,单线传输地址;10,双线传输地址;11,四线传输地址;我们一般设置为00/11。

IMODE[1:0],这两个位用于设置指令模式:00,无指令;01,单线传输指令;10,双线传输指令;11,四线传输指令;我们一般设置为00/11。

INSTRUCTION[7:0],这8个位用于设置将要发送给FLASH的指令。

注意,以上这些位的配置,都必须在QUADSPI_SR寄存器的BUSY位为0时才可配置。

接下来,我们看QSPI数据长度寄存器:QUADSPI_DLR,该寄存器为一个32位寄存器,可以设置的数据长度范围为:0~0XFFFFFFFF,当QUADSPI_DLR!=0XFFFFFFFF时,表示传输的字节长度(+1);当QUADSPI_DLR==0XFFFFFFFF时,表示不限传输长度,直到到达由FSIZE定义的FLASH结尾。

接下来,我们看QSPI地址寄存器:QUADSPI_AR,该寄存器为一个32位寄存器,用于指定发送到FLASH的地址。

接下来,我们看QSPI数据寄存器:QUADSPI_DR,该寄存器为一个32位寄存器,用于指定与外部SPI FLASH设备交换的数据。该寄存器支持字、半字和字节访问。

在间接写入模式下,写入该寄存器的数据在数据阶段发送到FLASH,在此之前则存储于FIFO,如果 FIFO 满了,则暂停写入,直到 FIFO 具有足够的空间接受要写入的数据才继续。

在间接模式下,读取该寄存器可获得(通过FIFO)已从FLASH接收的数据。如果FIFO所含字节数比读取操作要求的字节数少,且BUSY=1,则暂停读取,直到足够的数据出现或传输完成才继续。

36.1.2 NOR FLASH芯片简介

NOR FLASH芯片有很多种芯片型号,在我们的norflash.h头文件中有定义芯片ID的宏定义,对应的就是不同型号的NOR FLASH芯片,比如有:W25Q128BY25Q128NM25Q128,它们是来自不同的厂商的同种规格的NOR FLASH芯片,内存空间都是128M字,即16M字节。它们的很多参数、操作都是一样的,所以我们的实验都是兼容它们的。

由于这么多的芯片我们就不一一进行介绍了,就拿其中一款型号进行介绍即可,其他的型号都是类似的。

W25Q128是一款大容量SPI FLASH产品,其容量为16M。它将16M字节的容量分为256个块(Block),每一个块大小为64K字节,每个块又分为16个扇区(Sector),每一个扇区16页,每页256个字节,即每个扇区4K字节。W25Q128的最小擦除单位为一个扇区,也就是每次必须擦除4K个字节。这样我们需要给W25Q128开辟一个至少4K的缓存区,这样对SRAM要求比较高,要求芯片必须有4K以上SRAM才能很好的操作。

W25Q128的擦写周期多达10W次,具有20年的数据保存期限,支持电压为2.7~3.6V,W25Q128支持标准的SPI,还支持双输出/四输出SPI和QPI(QPI即QSPI),最高时钟频率可达104Mhz(双输出时相当于208Mhz,四输出时相当于416M),本实验我们将使用STM32H7的QSPI接口来实现对W25Q128的驱动。

接下来,我们介绍一下本实验驱动W25Q128需要用到的一些指令,如表36.1.2.1所示:

输入/输出数据

字节1

字节2

字节3

字节4

字节5

字节6

字节7

时钟数

SPI模式

0-7

8-15

16-23

24-31

32-39

40-47

48-55

QPI模式

0,1

2,3

4,5

6,7

8,9

10,11

12,13

W25X_ReadStatusReg1

0X05

S7-S0






W25X_ReadStatusReg2

0X35

S15-S8






W25X_ReadStatusReg3

0X15

S23-S16






W25X_WriteStatusReg1

0X01

S7-S0






W25X_WriteStatusReg2

0X31

S15-S8






W25X_WriteStatusReg3

0X11

S23-S16






W25X_ManufactDeviceID

0X90

Dummy

Dummy

0X00

MF7-MF0

ID7-ID0


W25X_EnterQPIMode

0X38







W25X_Enable4ByteAddr

0XB7







W25X_SetReadParam

0XC0

P7-P0






W25X_WriteEnable

0X06







W25X_FastReadData

0X0B

A31-A24

A23-A16

A15-A8

A7-A0

Dummy1

D7-D02

W25X_PageProgram

0X02

A31-A24

A23-A16

A15-A8

A7-A0

D7-D02

D7-D02

W25X_SectorErase

0X20

A31-A24

A23-A16

A15-A8

A7-A0



W25X_ChipErase

0XC7







1,在QPI模式下dummy时钟的个数,由读参数控制位P[5:4]位控制。

2,传输的数据量,只要不停的给时钟就可以持续传输,对W25X_PageProgram指令,则单次传输最多不超过256字节,否则将覆盖之前写入的数据。

36.1.2.1 W25Q128指令

上表列出了本章我们驱动W25Q128所需要用到的所有指令和对应的参数,注意SPI模式和QPI模式下时钟数的区别,可知QPI模式比SPI模式所需要的时钟数少的多,所以速度也快得多。接下来我们简单介绍一下这些指令。

首先,前面6个指令,是用来读取/写入状态寄存器1~3的。在读取的时候,读取S23~S0的数据,在写入的时候,写入S23~S0。而S23~S0则由三部分组成:S23~S16,S15~S8,S7~S0即状态寄存器3、2、1,如表36.1.2.2所示:

状态寄存器3

S23

S22

S21

S20

S19

S18

S17

S16

位说明

HOLD/RST

DRV1

DRV0



WPS

ADP

ADS

状态寄存器2

S15

S14

S13

S12

S11

S10

S9

S8

位说明

SUS

CMP

LB3

LB2

LB1


QE

SRP1

状态寄存器1

S7

S6

S5

S4

S3

S2

S1

S0

位说明

SRP0

TB

BP3

BP2

BP1

BP0


BUSY

36.1.2.2 W25Q128状态寄存器

这三个状态寄存器,我们只关心我们需要用到的一些位:ADS、QE和BUSY位。其他位的说明,请看W25Q128的数据手册。

ADS位,表示W25Q128当前的地址模式,是一个只读位,当ADS=0的时候,表示当前是3字节地址模式,当ADS=1的时候,表示当前是4字节地址模式,我们需要使用4字节地址模式,所以在读取到该位为0的时候,必须通过W25X_Enable4ByteAddr指令,设置为4字节地址模式。

QE位,用于使能4线模式(Quad),此位可读可写,并且是可以保存的(掉电后可以继续保持上一次的值)。在本章,我们需要用到4线模式,所以在读到该位为0的时候,必须通过W25X_WriteStatusReg2指令设置此位为1,表示使能4线模式。

BUSY位,用于表示擦除/编程操作是否正在进行,当擦除/编程操作正在进行时,此位为1,此时W25Q128不接受任何指令,当擦除/编程操作完成时,此位为0。此位为只读位,我们在执行某些操作的时候,必须等待此位为0。

W25X_ManufactDeviceID指令,用于读取W25Q128的ID,可以用于判断W25Q128是否正常。对于W25Q128来说:MF[7:0]=0XEF,ID[7:0]=0X18。

W25X_EnterQPIMode指令,用于设置W25Q128进入QPI模式。上电时,W25Q128默认是SPI模式,我们需要通过该指令设置其进入QPI模式。注意:在发送该指令之前,必须先设置状态寄存器2的QE位为1!!

W25X_Enable4ByteAddr指令,用于设置W25Q128进入4字节地址模式。当读取到ADS位为0的时候,我们必须通过此指令将W25Q128设置为4字节地址模式,否则将只能访问16MB的地址空间。

W25X_SetReadParam指令,可以用于设置读参数控制位P[5:4],这两个位的描述如表36.1.2.3所示:

《MiniPRO H750开发指南》第三十六章 QSPI实验_内存映射_08


36.1.2.3 W25Q128读参数控制位

为了让W25Q128可以工作在最大频率下,我们这里设置P[5:4]=11,即可工作在104Mhz的时钟频率下。此时,读取数据时的dummy时钟个数为8个(参见W25X_FastReadData指令)。

W25X_WriteEnable指令,用于设置W25Q128写使能。在执行擦除、编程、写状态寄存器等操作之前,都必须通过该指令,设置W25Q128写使能,否则无法写入。

W25X_FastReadData指令,用于读取FLASH数据,在发送完该指令以后,就可以读取W25Q128的数据了。该指令发送完成后,我们可以持续读取FLASH里面的数据,只要不停的给时钟,就可以不停的读取数据。

W25X_PageProgram指令,用于编程FLASH(写入数据到FLASH),该指令发送完成后,最多可以一次写入256字节到W25Q128,超过256字节则需要多次发送该指令。

W25X_SectorErase指令,用于擦除一个扇区(4KB)的数据。因为FLASH具有只可以写0,不可以写1的特性,所以在写入数据的时候,一般需要先擦除(归1),再写。W25Q128的最小擦除单位为一个扇区(4KB)。该指令在写入数据的时候,经常要有用。

W25X_ChipErase指令,用于全片擦除W25Q128

为了在程序上方便使用,我们把FLASH芯片的常用指令编码定义为宏定义的形式,存放在norflash.h文件中。

36.2 硬件设计

1. 例程功能

通过KEY1按键来控制norflash的写入,通过按键KEY0来控制norflash的读取。并在LCD模块上面显示相关信息。我们还可以通过USMART控制读取norflash的ID、擦除某个扇区或整片擦除。LED0闪烁用于提示程序正在运行。

2. 硬件资源

1)RGB灯

RED LED0 - PB4

2)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)

3)正点原子2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)

4)独立按键 :KEY0 - PA1KEY1 - PA15

5QSPI(PB2/PB6/PD11/PD12/PD13/PE2)

6)norflash(QSPI FLASH芯片,连接在QSPI接口上)

3. 原理图

板载的QSPI FLASH芯片与STM32H750的连接关系,如下图所示:

《MiniPRO H750开发指南》第三十六章 QSPI实验_寄存器_09


36.2.1 QSPI FLASH芯片与STM32H750连接示意图

本实验支持多种型号的QSPI FLASH芯片,比如:BY25Q128/NM25Q128/W25Q128等等,具体请看norflash.h文件的宏定义,程序上只需要稍微修改一下,后面讲解程序的时候会说到。

36.3 程序设计

36.3.1 QSPI的HAL库驱动

QSPIHAL库中的驱动代码在stm32h7xx_hal_qspi.c文件(及其头文件)中。

1. HAL_QSPI_Init函数

QSPI的初始化函数,其声明如下:

HAL_StatusTypeDef HAL_QSPI_Init(QSPI_HandleTypeDef *hqspi);
  • 函数描述:用于初始化QSPI
  • 函数形参:形参1QSPI_HandleTypeDef结构体类型指针变量,其定义如下:
typedef struct​
{​
QUADSPI_TypeDef *Instance; /* QSPI寄存器基址 */ ​
QSPI_InitTypeDef Init; /* QSPI参数配置结构体 */ ​
uint8_t *pTxBuffPtr; /* 要发送数据的地址 */ ​
__IO uint16_t TxXferSize; /* 要发送数据的大小 */ ​
__IO uint16_t TxXferCount; /* 剩余要发送数据的个数 ​
uint8_t *pRxBuffPtr; /* 要接收数据的地址 */ ​
__IO uint16_t RxXferSize; /* 要接收数据的大小 */ ​
__IO uint16_t RxXferCount; /* 剩余要接收数据的个数 ​
DMA_HandleTypeDef * hmdma; /* DMA配置结构体 ​
__IO HAL_LockTypeDef Lock; /* 锁对象 */ ​
__IO HAL_QSPI_StateTypeDef State; /* QSPI通信状态 */ ​
__IO uint32_t ErrorCode; /* 错误代码 */ ​
uint32_t Timeout; /* 配置QSPI内存访问超时时间 */ ​
}QSPI_HandleTypeDef;
  • 1) Instance:用于设置QSPI寄存器基地址,设置为QUADSPI即可,这个官方已经为我们做好了宏定义。2) Init:用于设置QSPI的相关参数,QSPI_InitTypeDef结构体下面再进行详细讲解。3) pTxBuffPtr、TxXferSize和TxXferCount:分别用于设置 QSPI 发送缓冲指针、发送数据量和发送剩余数据量。4) pRxBuffPtr、RxXferSize和RxXferCount:分别用于设置接收缓冲指针、接收数据量和接收剩余数据量。5) hmdma:用于配置相关的 DMA 参数。6) Lock:用于分配锁资源,可选择 HAL_UNLOCKED 或者是 HAL_LOCKED两个参数。7) State:用于存放通讯过程中的工作状态。8) ErrorCode:通过该参数,用户可以了解到QSPI通讯过程中通信失败的原因。9) Timeout:用于设置超时时间。QSPI访问时间一旦超出 Timeout这个变量值,那么ErrorCode成员变量就会被赋值为HAL_QSPI_ERROR_TIMEOUT,表示操作超时。下面重点来了解QSPI_InitTypeDef结构体的内容,其定义如下:
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; /* 闪存ID,第一片还是第二片 */
uint32_t DualFlash; /* 双闪存模式设置 */
}QSPI_InitTypeDef;

  • 1) ClockPrescaler:用于设置预分频系数,对应QUADSPI_CR寄存器的PRESCALER[7:0]位,取值范围是 0~255。仅可在 BUSY = 0 时修改该字段。
    2) FifoThreshold:用于设置FIFO阈值级别,可设置范围为 0~31,对应QUADSPI_CR寄存器的FTHRES[4:0]位。
    3) SampleShifting:用于设置采样移位,对应QUADSPI_CR寄存器的SSHIFT位。使用该位是考虑到外部信号延迟时,推迟数据采样。可以取值QSPI_SAMPLE_SHIFTING_NONE(即0):不发生移位;QSPI_SAMPLE_SHIFTING_HALFCYCLE(即1):移位半个周期。在DDR模式下 (DDRM = 1),固件必须确保SSHIFT = 0。
    4) FlashSize:用于设置FLASH大小,对应QUADSPI_ DCR寄存器的FSIZE[4:0]位,可设置的范围是:0到31之间的整数。FLASH 中的字节数= 2 [FSIZE+1]。在间接模式下,FLASH容量最高可达4GB(使用 32 位进行寻址),但在内存映射模式下的可寻址空间限制为256MB。
    5) ChipSelectHighTime:用于设置片选高电平时间,取值范围:QSPI_CS_HIGH_TIME_1_CYCLE ~ QSPI_CS_HIGH_TIME_8_CYCLE,表示 1~8个周期,对应QUADSPI_DCR寄存器的CSHT[2:0]位。CSHT+1定义片选 (nCS) 在发送至 Flash 的命令之间必须保持高电平的最少CLK周期数。
    6) ClockMode:用于设置时钟模式,对应QUADSPI_DCR寄存器CKMODE位,指示 CLK在命令之间(nCS = 1 时)的电平,可以选择的参数是:QSPI_CLOCK_MODE_0(表示模式0)或者QSPI_CLOCK_MODE_3(表示模式3)。模式 0是:nCS为高电平(片选释放)时,CLK 必须保持低电平。模式3是:nCS 为高电平(片选释放)时,CLK 必须保持高电平。
    7) FlashID:用于选择Flash1或者Flash2,单闪存模式下选择QSPI_FLASH_ID_1(表示Flash1)。
    8) DualFlash:用于使能双闪存模式,QSPI_DUALFLASH_DISABLE:禁止双闪存模式;QSPI_DUALFLASH_ENABLE:使能双闪存模式。对应QUADSPI_CR寄存器DFM位。
  • 函数返回值:HAL_StatusTypeDef枚举类型的值。
  • 注意事项:QSPI的MSP初始化函数HAL_QSPI_MspInit,该函数声明如下:
void HAL_QSPI_MspInit(QSPI_HandleTypeDef *hqspi);
  • 2. HAL_QSPI_Command函数QSPI设置命令配置函数,其声明如下:
• HAL_StatusTypeDef HAL_QSPI_Command(QSPI_HandleTypeDef *hqspi, 
QSPI_CommandTypeDef *cmd, uint32_t Timeout);
  • 函数描述:该函数用来配置QSPI命令
  • 函数形参:形参1QSPI_HandleTypeDef结构体类型指针变量。形参2QSPI_CommandTypeDef结构体类型指针变量,其定义如下:
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;

  • 1) Instruction:设置通信指令,指定要发送到外部QSPI设备的指令,指令表定义在norflash.h里。
    2) Address:指定要发送到外部QSPI设备的地址,BUSY = 0 或 FMODE = 11(内存映射模式)时,将忽略写入该字段。在双闪存模式下,由于地址始终为偶地址,ADDRESS[0] 自动保持为“0”。
    3) AlternateBytes:指定要在地址后立即发送到外部QSPI设备的可选数据。
    4) AddressSize:定义地址长度,可以是8位,16位,24位或者32位。
    5) AlternateBytesSize:定义交替字节长度,可以是8位,16位,24位或者32位。
    6) DummyCycles:定义空指令阶段持续周期,SDR和DDR模式下,指定CLK周期数(0~31)
    7) InstructionMode:用于指定指令阶段模式,如下四种:
    QSPI_INSTRUCTION_NONE:无指令;
    QSPI_INSTRUCTION_1_LINE:单线传输指令;
    QSPI_INSTRUCTION_2_LINES:双线传输指令;
    QSPI_INSTRUCTION_4_LINES:四线传输指令。
    8) AddressMode:指定地址模式,如下四种:
    QSPI_ADDRESS_NONE:无地址;QSPI_ADDRESS_1_LINE:单线传输地址;QSPI_ADDRESS_2_LINES:双线传输地址;QSPI_ADDRESS_4_LINES:四线传输地址。
    9) AlternateByteMode:指定交替字节模式,如下四种:
    QSPI_ALTERNATE_BYTES_NONE:无交替字节;
    QSPI_ALTERNATE_BYTES_1_LINE:单线传输交替字节;
    QSPI_ALTERNATE_BYTES_2_LINES:双线传输交替字节;
    QSPI_ALTERNATE_BYTES_4_LINES:四线传输交替字节。
    10) DataMode:指定数据模式,如下四种:
    QSPI_DATA_NONE:无数据;QSPI_DATA_1_LINE:单线传输数据;QSPI_DATA_2_LINES:双线传输数据;QSPI_DATA_4_LINES:四线传输数据。
    11) NbData:用于设置数据长度,在间接模式和状态轮询模式下待检索的数据数量(值 + 1)。对状态轮询模式应使用不大于 3 的值(表示 4 字节)。
    12) DdrMode:为地址、交替字节和数据阶段设置 DDR 模式,可以选择的值是:
    QSPI_DDR_MODE_DISABLE:禁止 DDR 模式;
    QSPI_DDR_MODE_ENABLE:使能DDR 模式。
    13) DdrHoldHalfCycle:用于设置 DDR 模式下数据输出延迟 1/4 个 QUADSPI 输出时钟周期,可选值如下:
    QSPI_DDR_HHC_ANALOG_DELAY:使用模拟延迟来延迟数据输出;
    QSPI_DDR_HHC_HALF_CLK_DELAY:数据输出延迟 1/4 个 QUADSPI 输出时钟周期。
    14) SIOOMode:设置是否开启仅发送指令一次模式,可选值如下:
    QSPI_SIOO_INST_EVERY_CMD:在每个事务中发送指令;
    QSPI_SIOO_INST_ONLY_FIRST_CMD:仅为第一条命令发送指令。
    形参3用于设置超时时间。
  • 函数返回值:HAL_StatusTypeDef枚举类型的值。3. HAL_QSPI_Receive函数QSPI接收数据函数,其声明如下:
HAL_StatusTypeDef HAL_QSPI_Receive(QSPI_HandleTypeDef *hqspi, ​
uint8_t *pData, uint32_t Timeout);
  • 函数描述:该函数用来接收数据
  • 函数形参:形参1QSPI_HandleTypeDef结构体类型指针变量。
    形参2uint8_t类型指针变量,存放接收数据缓冲区指针。
    形参3设置操作超时时间。
  • 函数返回值:HAL_StatusTypeDef枚举类型的值。4. HAL_QSPI_Transmit函数QSPI发送数据函数,其声明如下:
HAL_StatusTypeDef HAL_QSPI_Transmit (QSPI_HandleTypeDef *hqspi, ​
uint8_t *pData, uint32_t Timeout);
  • 函数描述:该函数用来发送数据
  • 函数形参:形参1QSPI_HandleTypeDef结构体类型指针变量。
    形参2uint8_t类型指针变量,存放发送数据缓冲区指针。
    形参3设置操作超时时间。
  • 函数返回值:

HAL_StatusTypeDef枚举类型的值。

QSPI初始化配置步骤

1)开启QSPI接口和相关IO的时钟,设置IO口的复用功能。

要使用QSPI,肯定要先开启其时钟(由AHB3ENR控制),然后根据我们使用的QSPI IO口,开启对应IO口的时钟,并初始化相关IO口的复用功能(选择QSPI复用功能)。

QSPI时钟使能方法为:

__HAL_RCC_QSPI_CLK_ENABLE(); /* 使能QSPI时钟 */

这里大家要注意,和其他外设处理方法一样,HAL库提供了QSPI的初始化回调函数HAL_QSPI_MspInit,一般用来编写与MCU相关的初始化操作。时钟使能和IO口初始化一般在回调函数中编写。

2)设置QSPI相关参数。

此部分需要设置两个寄存器:QUADSPI_CR和QUADSPI_DCR,控制QSPI的时钟、片选参数、FLASH容量和时钟模式等参数,设定SPI FLASH的工作条件。最后,使能QSPI,完成对QSPI的初始化。HAL库中设置QSPI相关参数函数为HAL_QSPI_Init,该函数声明为:

HAL_StatusTypeDef HAL_QSPI_Init(QSPI_HandleTypeDef *hqspi);

QSPI_HandleTypeDef结构体这些成员变量是用来配置QUADSPI_CR寄存器和QUADSPI_DCR寄存器相应位,大家可以结合这两个寄存器的位定义和结构体定义来理解。

对于HAL_QSPI_Init函数使用范例请参考后面33.3软件设置部分程序源码。

QSPI发送命令步骤

1)等待QSPI空闲。

在QSPI发送命令前,必须先等待QSPI空闲,通过判断QUADSPI_SR寄存器的BUSY位为0,来确定。

2)设置命令参数。

此部分主要是通过通信配置寄存器(QUADSPI_CCR)设置,将QSPI配置为:每次都发送指令、间接写模式,根据具体需要设置:指令、地址、空周期和数据等的传输位宽等信息。如果需要发送地址,则配置地址寄存器(QUADSPI_AR)。

在配置完成以后,即可启动发送。如果不需要传输数据,则需要等待命令发送完成(等待QUADSPI_SR寄存器的TCF位为1)。

在HAL库中上述两个步骤是通过函数HAL_QSPI_Command来实现,该函数声明为:

HAL_StatusTypeDef HAL_QSPI_Command(QSPI_HandleTypeDef *hqspi, ​
QSPI_CommandTypeDef *cmd, uint32_t Timeout);

QSPI读数据步骤

1)设置数据传输长度。

通过设置数据长度寄存器(QUADSPI_DLR),配置需要传输的字节数。

2)设置QSPI工作模式并设置地址。

因为要读取数据,所以,设置QUADSPI_CCR寄存器的FMODE[1:0]位为01,工作在间接读取模式。然后,通过地址寄存器(QUADSPI_AR),设置我们将要读取的数据的首地址。

3)读取数据。

在发送完地址以后,就可以读取数据了,不过要等待数据准备好,通过判断QUADSPI_SR寄存器的FTF和TCF位,当这两个位任意一个位为1的时候,我们就可以读取QUADSPI_DR寄存器来获取从FLASH读到的数据。

最后,在所有数据接收完成以后,终止传输(ABORT),清除传输完成标志位(TCF)。

HAL库中,读取数据是通过函数HAL_QSPI_Receive来实现的,该函数声明为:

HAL_StatusTypeDef HAL_QSPI_Receive(QSPI_HandleTypeDef *hqspi, ​
uint8_t *pData, uint32_t Timeout);

在调用该函数读取数据之前,我们会先调用上个步骤讲解的函数HAL_QSPI_Command来指定读取数据的存放空间。

QSPI写数据步骤

1)设置数据传输长度。

通过设置数据长度寄存器(QUADSPI_DLR),配置需要传输的字节数。

2)设置QSPI工作模式并设置地址。

因为要读取数据,所以,设置QUADSPI_CCR寄存器的FMODE[1:0]位为00,工作在间接写入模式。然后,通过地址寄存器(QUADSPI_AR),设置我们将要写入的数据的首地址。

3)写入数据。

在发送完地址以后,就可以写入数据了,不过要等待FIFO不满,通过判断QUADSPI_SR寄存器的FTF位,当这个位为1的时候,表示FIFO可以写入数据,此时往QUADSPI_DR写入需要发送的数据,就可以实现写入数据到FLASH。

最后,在所有数据写入完成以后,终止传输(ABORT),清除传输完成标志位(TCF)。

在HAL库中,QSPI发送数据是通过函数HAL_QSPI_Transmit来实现的,该函数声明为:

HAL_StatusTypeDef HAL_QSPI_Transmit (QSPI_HandleTypeDef *hqspi, ​
uint8_t *pData, uint32_t Timeout);

同理,在调用该函数发送数据之前,我们会先调用HAL_QSPI_Command函数来指定要写入数据的存储地址信息。

FLASH芯片初始化步骤

1)使能QPI模式。

因为我们是通过QSPI访问W25Q128的,所以先设置W25Q128工作在QPI模式下。通过FLASH_EnterQPIMode指令控制。注意:在该指令发送之前,必须先使能W25Q128的QE位。

2)设置4字节地址模式。

W25Q128上电后,一般默认是3字节地址模式,我们需要通FLASH_Enable4ByteAddr指令,设置其为四字节地址模式,否则只能访问16MB的地址空间。

3)设置读参数。

这一步,我们通过FLASH_SetReadParam指令,将P[5:4]设置为11,以支持最高速度访问W25Q128(8个dummy,104M时钟频率)。

36.3.2 程序流程图

《MiniPRO H750开发指南》第三十六章 QSPI实验_寄存器_10


36.3.2.1 QSPI实验程序流程图

36.3.3 程序解析

1. QSPI驱动代码

这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。QSPI驱动源码包括两个文件:qspi.c和qspi.h。

qspi.h头文件对QSPI相关引脚做了宏定义,该宏定义如下:

/* QSPI 相关 引脚 定义 */​
#define QSPI_BK1_CLK_GPIO_PORT GPIOB​
#define QSPI_BK1_CLK_GPIO_PIN GPIO_PIN_2​
#define QSPI_BK1_CLK_GPIO_AF GPIO_AF9_QUADSPI​
#define QSPI_BK1_CLK_GPIO_CLK_ENABLE() ​
do{ __HAL_RCC_GPIOB_CLK_ENABLE; }while(0) /* PB口时钟使能 */​

#define QSPI_BK1_NCS_GPIO_PORT GPIOB​
#define QSPI_BK1_NCS_GPIO_PIN GPIO_PIN_6​
#define QSPI_BK1_NCS_GPIO_AF GPIO_AF10_QUADSPI​
#define QSPI_BK1_NCS_GPIO_CLK_ENABLE() ​
do{ __HAL_RCC_GPIOB_CLK_ENABLE; }while(0) /* PB口时钟使能 */​

#define QSPI_BK1_IO0_GPIO_PORT GPIOD​
#define QSPI_BK1_IO0_GPIO_PIN GPIO_PIN_11​
#define QSPI_BK1_IO0_GPIO_AF GPIO_AF9_QUADSPI​
#define QSPI_BK1_IO0_GPIO_CLK_ENABLE() ​
do{ __HAL_RCC_GPIOD_CLK_ENABLE; }while(0) /* PD口时钟使能 */​

#define QSPI_BK1_IO1_GPIO_PORT GPIOD​
#define QSPI_BK1_IO1_GPIO_PIN GPIO_PIN_12​
#define QSPI_BK1_IO1_GPIO_AF GPIO_AF9_QUADSPI​
#define QSPI_BK1_IO1_GPIO_CLK_ENABLE() ​
do{ __HAL_RCC_GPIOD_CLK_ENABLE; }while(0) /* PD口时钟使能 */​

#define QSPI_BK1_IO2_GPIO_PORT GPIOD​
#define QSPI_BK1_IO2_GPIO_PIN GPIO_PIN_13​
#define QSPI_BK1_IO2_GPIO_AF GPIO_AF9_QUADSPI​
#define QSPI_BK1_IO2_GPIO_CLK_ENABLE() ​
do{ __HAL_RCC_GPIOD_CLK_ENABLE; }while(0) /* PD口时钟使能 */​

#define QSPI_BK1_IO3_GPIO_PORT GPIOE​
#define QSPI_BK1_IO3_GPIO_PIN GPIO_PIN_2​
#define QSPI_BK1_IO3_GPIO_AF GPIO_AF9_QUADSPI​
#define QSPI_BK1_IO3_GPIO_CLK_ENABLE() ​
do{ __HAL_RCC_GPIOE_CLK_ENABLE; }while(0) /* PE口时钟使能 */

注意这6个GPIO都是用到复用功能,对应引脚的复用功能情况请看STM32H750VBT6.pdf》数据手册79页之后的端口复用功能表格。

下面我们开始介绍qspi.c的程序,首先是QSPI接口初始化函数,其定义如下:

/**​
* @brief 初始化QSPI接口​
* @param 无​
* @retval 0, 成功; 1, 失败.​
*/​
uint8_t qspi_init(void)​
{​
g_qspi_handle.Instance = QUADSPI; /* QSPI */​
/* QPSI分频比,BY25Q128最大频率为108M,所以此处应该为2,QSPI频率就为​
220/(1+1)=110MHZ稍微有点超频,可以正常就好,不行就只能降低频率 */​
g_qspi_handle.Init.ClockPrescaler = 1;​
g_qspi_handle.Init.FifoThreshold = 4; /* FIFO阈值为4个字节 */​
/* 采样移位半个周期(DDR模式下,必须设置为0) */​
g_qspi_handle.Init.SampleShifting = QSPI_SAMPLE_SHIFTING_HALFCYCLE;​
/* SPI FLASH大小,BY25Q128大小为32M字节,2^25,所以取权值25-1=24 */​
g_qspi_handle.Init.FlashSize = 25-1; ​
/* 片选高电平时间为3个时钟(9.1*3=27.3ns),即手册里面的tSHSL参数 */ ​
g_qspi_handle.Init.ChipSelectHighTime = QSPI_CS_HIGH_TIME_3_CYCLE; ​
g_qspi_handle.Init.ClockMode = QSPI_CLOCK_MODE_3; /* 模式3 */​
g_qspi_handle.Init.FlashID = QSPI_FLASH_ID_1; /* 第一片flash */​
g_qspi_handle.Init.DualFlash = QSPI_DUALFLASH_DISABLE; /* 禁止双闪存模式 */​
if(HAL_QSPI_Init(&g_qspi_handle) == HAL_OK) ​
{​
return 0; /* QSPI初始化成功 */​
}​
else​
{​
return 1;​
}​
}​

这里我们需要注意的是,QSPI的时钟源在sys_stm32_clock_init函数中已经选择了PLL2R(我们设置为220MHZ),这里就不需要再选择时钟源了,只需要选择预分频系数就可以确定QSPI的时钟频率。时钟模式选择模式3,在未进行任何操作时 CLK 升至高电平。我们用单闪存模式,FlashID要选择QSPI_FLASH_ID_1

我们用HAL_QSPI_MspInit函数来编写QSPI时钟和IO配置等代码,其定义如下:

/**​
* @brief QSPI底层驱动,引脚配置,时钟使能​
* @param hqspi:QSPI句柄​
* @note 此函数会被HAL_QSPI_Init()调用​
* @retval 0, 成功; 1, 失败.​
*/​
void HAL_QSPI_MspInit(QSPI_HandleTypeDef *hqspi)​
{​
GPIO_InitTypeDef gpio_init_struct;​

__HAL_RCC_QSPI_CLK_ENABLE(); /* 使能QSPI时钟 */​
__HAL_RCC_GPIOB_CLK_ENABLE(); /* GPIOB时钟使能 */​
__HAL_RCC_GPIOD_CLK_ENABLE(); /* GPIOD时钟使能 */​
__HAL_RCC_GPIOE_CLK_ENABLE(); /* GPIOE时钟使能 */​

gpio_init_struct.Pin = QSPI_BK1_NCS_GPIO_PIN;​
gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用 */​
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */​
gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; /* 高速 */​
gpio_init_struct.Alternate = GPIO_AF10_QUADSPI; /* 复用为QSPI */​
/* 初始化QSPI_BK1_NCS引脚 */​
HAL_GPIO_Init(QSPI_BK1_NCS_GPIO_PORT, &gpio_init_struct);​

gpio_init_struct.Pin = QSPI_BK1_CLK_GPIO_PIN;​
gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用 */​
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */​
gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; /* 高速 */​
gpio_init_struct.Alternate = GPIO_AF9_QUADSPI; /* 复用为QSPI */​
/* 初始化QSPI_BK1_CLK引脚 */​
HAL_GPIO_Init(QSPI_BK1_CLK_GPIO_PORT, &gpio_init_struct); ​

gpio_init_struct.Pin = QSPI_BK1_IO0_GPIO_PIN;​
/* 初始化QSPI_BK1_IO0引脚 */​
HAL_GPIO_Init(QSPI_BK1_IO0_GPIO_PORT, &gpio_init_struct); ​

gpio_init_struct.Pin = QSPI_BK1_IO1_GPIO_PIN;​
/* 初始化QSPI_BK1_IO1引脚 */​
HAL_GPIO_Init(QSPI_BK1_IO1_GPIO_PORT, &gpio_init_struct); ​

gpio_init_struct.Pin = QSPI_BK1_IO2_GPIO_PIN;​
/* 初始化QSPI_BK1_IO2引脚 */​
HAL_GPIO_Init(QSPI_BK1_IO2_GPIO_PORT, &gpio_init_struct); ​

gpio_init_struct.Pin = QSPI_BK1_IO3_GPIO_PIN;​
/* 初始化QSPI_BK1_IO3引脚 */​
HAL_GPIO_Init(QSPI_BK1_IO3_GPIO_PORT, &gpio_init_struct); ​
}

这里初始化的6个引脚全部都要配置为复用功能模式,以及使能QSPI和相应IO时钟。

接下来介绍QSPI发送命令函数,其定义如下:

/**​
发送命令​
要发送的指令​
发送到的目的地址​
模式,详细位定义如下:​
指令模式;00,无指令;01,单线传输指令;10,双线传输指令;11,四线传输指令.​
地址模式;00,无地址;01,单线传输地址;10,双线传输地址;11,四线传输地址.​
地址长度;00,8位地址;01,16位地址; 10,24位地址; 11,32位地址.​
数据模式;00,无数据; 01,单线传输数据;10,双线传输数据;11,四线传输数据.​
空指令周期数​
无​
*/​
void qspi_send_cmd(uint8_t cmd, uint32_t addr, uint8_t mode, uint8_t dmcycle)​
{​
QSPI_CommandTypeDef qspi_command_handle;​

qspi_command_handle.Instruction = cmd; /* 指令 */​
qspi_command_handle.Address = addr; /* 地址 */​
qspi_command_handle.DummyCycles = dmcycle; /* 设置空指令周期数 */​

if(((mode >> 0) & 0x03) == 0)​
qspi_command_handle.InstructionMode = QSPI_INSTRUCTION_NONE; /* 指令模式 */​
else if(((mode >> 0) & 0x03) == 1)​
qspi_command_handle.InstructionMode = QSPI_INSTRUCTION_1_LINE; /* 指令模式 */​
else if(((mode >> 0) & 0x03) == 2)​
qspi_command_handle.InstructionMode = QSPI_INSTRUCTION_2_LINES;/* 指令模式 */​
else if(((mode >> 0) & 0x03) == 3)​
qspi_command_handle.InstructionMode = QSPI_INSTRUCTION_4_LINES;/* 指令模式 */​

if(((mode >> 2) & 0x03) == 0)​
qspi_command_handle.AddressMode = QSPI_ADDRESS_NONE; /* 地址模式 */​
else if(((mode >> 2) & 0x03) == 1)​
qspi_command_handle.AddressMode = QSPI_ADDRESS_1_LINE; /* 地址模式 */​
else if(((mode >> 2) & 0x03) == 2)​
qspi_command_handle.AddressMode = QSPI_ADDRESS_2_LINES; /* 地址模式 */​
else if(((mode >> 2) & 0x03) == 3)​
qspi_command_handle.AddressMode = QSPI_ADDRESS_4_LINES; /* 地址模式 */​

if(((mode >> 4)&0x03) == 0)​
qspi_command_handle.AddressSize = QSPI_ADDRESS_8_BITS; /* 地址长度 */​
else if(((mode >> 4) & 0x03) == 1)​
qspi_command_handle.AddressSize = QSPI_ADDRESS_16_BITS; /* 地址长度 */​
else if(((mode >> 4) & 0x03) == 2)​
qspi_command_handle.AddressSize = QSPI_ADDRESS_24_BITS; /* 地址长度 */​
else if(((mode >> 4) & 0x03) == 3)​
qspi_command_handle.AddressSize = QSPI_ADDRESS_32_BITS; /* 地址长度 */​

if(((mode >> 6) & 0x03) == 0)​
qspi_command_handle.DataMode=QSPI_DATA_NONE; /* 数据模式 */​
else if(((mode >> 6) & 0x03) == 1)​
qspi_command_handle.DataMode = QSPI_DATA_1_LINE; /* 数据模式 */​
else if(((mode >> 6) & 0x03) == 2)​
qspi_command_handle.DataMode = QSPI_DATA_2_LINES; /* 数据模式 */​
else if(((mode >> 6) & 0x03) == 3)​
qspi_command_handle.DataMode = QSPI_DATA_4_LINES; /* 数据模式 */​

qspi_command_handle.SIOOMode = QSPI_SIOO_INST_EVERY_CMD; /* 每次都发送指令 */​
qspi_command_handle.AlternateByteMode=QSPI_ALTERNATE_BYTES_NONE;/*无交替字节*/​
qspi_command_handle.DdrMode = QSPI_DDR_MODE_DISABLE; /* 关闭DDR模式 */​
qspi_command_handle.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY;​

HAL_QSPI_Command(&g_qspi_handle, &qspi_command_handle, 5000);​
}

该函数主要就是配置QSPI_CommandTypeDef结构体的参数,并调用HAL_QSPI_Command函数配置发送命令,是一个重要的基础函数。

接下来介绍的是QSPI接收函数,其定义如下:

/**​
* @brief QSPI接收指定长度的数据​
* @param buf : 接收数据缓冲区首地址​
* @param datalen : 要传输的数据长度​
* @retval 0, 成功; 其他, 错误代码.​
*/​
uint8_t qspi_receive(uint8_t *buf, uint32_t datalen)​
{​
g_qspi_handle.Instance->DLR = datalen - 1; /* 配置数据长度 */​
if (HAL_QSPI_Receive(&g_qspi_handle, buf, 5000) == HAL_OK) ​
{​
return 0;​
}​
else​
{​
return 1;​
}​
}

该函数首先把要接收数据的长度赋值到QUADSPI数据长度寄存器(QUADSPI_DLR)中,然后通过调用HAL_QSPI_Receive函数接收数据。

接下来介绍的是QSPI发送函数,其定义如下:

/**​
* @brief QSPI发送指定长度的数据​
* @param buf : 发送数据缓冲区首地址​
* @param datalen : 要传输的数据长度​
* @retval 0, 成功; 其他, 错误代码.​
*/​
uint8_t qspi_transmit(uint8_t *buf, uint32_t datalen)​
{​
g_qspi_handle.Instance->DLR = datalen - 1; /* 配置数据长度 */​
if (HAL_QSPI_Transmit(&g_qspi_handle, buf, 5000) == HAL_OK)​
{​
return 0;​
}​
else​
{​
return 1;​
}​
}

该函数首先把要发送数据的长度赋值到QUADSPI数据长度寄存器(QUADSPI_DLR)中,然后通过调用HAL_QSPI_Transmit函数发送数据。

最后要介绍的一个函数是等待状态标志函数,其定义如下:

/**​
* @brief 等待状态标志​
* @param flag : 需要等待的标志位​
* @param sta : 需要等待的状态​
* @param wtime: 等待时间​
* @retval 0, 等待成功; 1, 等待失败.​
*/​
uint8_t qspi_wait_flag(uint32_t flag, uint8_t sta, uint32_t wtime)​
{​
uint8_t flagsta = 0;​

while (wtime)​
{​
flagsta = (QUADSPI->SR & flag) ? 1 : 0; /* 获取状态标志 */​

if (flagsta == sta)break;​

wtime--;​
}​

if (wtime)return 0;​
else return 1;​
}

该函数可以设置一段时钟等待QUADSPI状态寄存器(QUADSPI_SR)的任意位为0,或者为1。然后通过返回值,判断等待是否成功,0表示等待成功;1表示等待失败。

2. NORFLASH驱动代码

这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。NORFLASH驱动源码包括两个文件:norflash.cnorflash.hnorflash_ex.cnorflash_ex.h

因为STM32H7不支持QSPI接口读时写,因此我们新建了norflash_ex.cnorflash_ex.h文件存放NOR FLASH驱动的拓展代码。该代码用于实现QSPI FLASH的数据写入,原理是:qspi.c、norflash.c和norflash_ex.c等3部分代码全部存储在H7的内部FLASH,我们需要保证操作QSPI FLASH的时候,CPU不会访问存放在QSPI FLASH的代码就可以实现QSPI FLASH数据写入。

由于这部分代码量会比较多,这里就不一一贴出来介绍。介绍几个重点,其余的请自行查看源码。首先是norflash.h头文件中,我们做了一个FLASH芯片列表(宏定义),这些宏定义是一些支持的FLASH芯片的ID。接下来是FLASH芯片指令表的宏定义,这个请参考FLASH芯片手册比对得到。norflash_ex.h头文件只是一些函数声明,就不介绍了。

下面介绍norflash.c文件几个重要的函数,首先是NOR FLASH初始化函数,其定义如下:

/**​
* @brief 初始化SPI NOR FLASH​
* @param 无​
* @retval 无​
*/​
void norflash_init(void)​
{​
uint8_t temp;​
qspi_init(); /* 初始化QSPI */​
norflash_qspi_disable(); /* 退出QPI模式(避免芯片之前进入这个模式,导致下载失败) */​
norflash_qe_enable(); /* 使能QE位 */​
g_norflash_type = norflash_read_id();/* 读取FLASH ID. */​

if (g_norflash_type == W25Q256) /* SPI FLASH为W25Q256, 必须使能4字节地址模式 */​
{​
temp = norflash_read_sr(3); /* 读取状态寄存器3,判断地址模式 */​

if ((temp & 0X01) == 0) /* 如果不是4字节地址模式,则进入4字节地址模式 */​
{​
norflash_write_enable(); /* 写使能 */​
temp |= 1 << 1; /* ADP=1, 上电4位地址模式 */​
norflash_write_sr(3, temp); /* 写SR3 */​

norflash_write_enable(); /* 写使能 */​
/* QPI,使能4字节地址指令,地址为0,无数据_8位地址_无地址_单线传输指令,​
无空周期,0个字节数据 */​
qspi_send_cmd(FLASH_Enable4ByteAddr, 0, (0 << 6) | (0 << 4) ​
| (0 << 2) | (1 << 0), 0); ​
}​
}​
//printf("ID:%x\r\n", g_norflash_type);​
}

该函数用于初始化NOR FLASH,首先调用qspi_init函数,初始化STM32H750的QSPI接口。然后退出QPI模式(避免芯片之前进入这个模式,导致下载失败),使能FLASH的QE位,使能IO2/IO3。最后读取FLASH ID,如果SPI FLASH为W25Q256,还必须使能4字节地址模式。调用本函数在初始化完成以后,我们便可以通过QSPI接口读写NOR FLASH的数据了。

接下来介绍读取SPI FLASH函数,其定义如下:

/**​
* @brief 读取SPI FLASH,仅支持QSPI模式​
* @note 在指定地址开始读取指定长度的数据​
* @param pbuf : 数据存储区​
* @param addr : 开始读取的地址(最大32bit)​
* @param datalen : 要读取的字节数(最大65535)​
* @retval 无​
*/​
void norflash_read(uint8_t *pbuf, uint32_t addr, uint16_t datalen)​
{​
/* QSPI,快速读数据,地址为addr,4线传输数据_24/32位地址_4线传输地址_1线传输指令,​
6空周期,datalen个数据 */​
qspi_send_cmd(FLASH_FastReadQuad, addr, (3 << 6) | (g_norflash_addrw << 4) ​
| (3 << 2) | (1 << 0), 6);​
qspi_receive(pbuf, datalen);​
}

该函数用于从NOR FLASH的指定地址读出指定长度的数据,由于NOR FLASH支持以任意地址(但是不能超过NOR FLASH的地址范围)开始读取数据,所以,这个代码相对来说就比较简单了,通过qspi_send_cmd函数,发送FLASH_FastReadQuad指令,并发送读数据首地址(addr),然后通过qspi_receive函数循环读取数据,存放在pbuf里面。

接下来,我们介绍写入NOR FLASH函数,其定义如下:

/**​
* @brief 写SPI FLASH​
* @note 在指定地址开始写入指定长度的数据 , 该函数带擦除操作!​
* SPI FLASH 一般是: 256个字节为一个Page, 4Kbytes为一个Sector, ​
16个扇区为1个Block​
* 擦除的最小单位为Sector.​
* @param pbuf : 数据存储区​
* @param addr : 开始写入的地址(最大32bit)​
* @param datalen : 要写入的字节数(最大65535)​
* @retval 无​
*/​
uint8_t g_norflash_buf[4096]; /* 扇区缓存 */​

void norflash_write(uint8_t *pbuf, uint32_t addr, uint16_t datalen)​
{​
uint32_t secpos;​
uint16_t secoff;​
uint16_t secremain;​
uint16_t i;​
uint8_t *norflash_buf;​

norflash_buf = g_norflash_buf;​
secpos = addr / 4096; /* 扇区地址 */​
secoff = addr % 4096; /* 在扇区内的偏移 */​
secremain = 4096 - secoff; /* 扇区剩余空间大小 */​

//printf("ad:%X,nb:%X\r\n", addr, datalen); /* 测试用 */​
if (datalen <= secremain)​
{​
secremain = datalen; /* 不大于4096个字节 */​
}​

while (1)​
{​
norflash_read(norflash_buf, secpos * 4096, 4096); /* 读出整个扇区的内容 */​

for (i = 0; i < secremain; i++) /* 校验数据 */​
{​
if (norflash_buf[secoff + i] != 0XFF)​
{​
break; /* 需要擦除, 直接退出for循环 */​
}​
}​

if (i < secremain) /* 需要擦除 */​
{​
norflash_erase_sector(secpos); /* 擦除这个扇区 */​
for (i = 0; i < secremain; i++) /* 复制 */​
{​
norflash_buf[i + secoff] = pbuf[i];​
}​
/* 写入整个扇区 */​
norflash_write_nocheck(norflash_buf, secpos * 4096, 4096); ​
}​
else /* 写已经擦除了的,直接写入扇区剩余区间. */​
{​
norflash_write_nocheck(pbuf, addr, secremain); /* 直接写扇区 */​
}​

if (datalen == secremain)​
{​
break; /* 写入结束了 */​
}​
else /* 写入未结束 */​
{​
secpos++; /* 扇区地址增1 */​
secoff = 0; /* 偏移位置为0 */​

pbuf += secremain; /* 指针偏移 */​
addr += secremain; /* 写地址偏移 */​
datalen -= secremain; /* 字节数递减 */​

if (datalen > 4096)​
{​
secremain = 4096; /* 下一个扇区还是写不完 */​
}​
else​
{​
secremain = datalen; /* 下一个扇区可以写完了 */​
}​
}​
}​
}

该函数可以在NOR FLASH的任意地址开始写入任意长度(必须不超过NOR FLASH的容量)的数据。我们这里简单介绍一下思路:先获得首地址(addr)所在的扇区,并计算在扇区内的偏移,然后判断要写入的数据长度是否超过本扇区所剩下的长度,如果不超过,再先看看是否要擦除,如果不要,则直接写入数据即可,如果要则读出整个扇区,在偏移处开始写入指定长度的数据,然后擦除这个扇区,再一次性写入。当所需要写入的数据长度超过一个扇区的长度的时候,我们先按照前面的步骤把扇区剩余部分写完,再在新扇区内执行同样的操作,如此循环,直到写入结束。这里我们还定义了一个g_norflash_buf的全局数组,用于擦除时缓存扇区内的数据。

norflash.c文件我们就介绍这三个函数,其他请大家自行查阅。下面再介绍norflash_ex.c文件的几个重要函数。首先是QSPI接口进入内存映射模式函数,其定义如下:

/**​
* @brief QSPI接口进入内存映射模式​
* @note 调用该函数之前务必已经初始化了QSPI接口​
* sys_qspi_enable_memmapmode or norflash_init​
* @param 无​
* @retval 无​
*/​
static void norflash_ex_enter_mmap(void)​
{​
uint32_t tempreg = 0;​

/* BY/W25QXX 写使能(0X06指令) */​
while (QUADSPI->SR & (1 << 5)); /* 等待BUSY位清零 */​

QUADSPI->CCR = 0X00000106; /* 发送0X06指令,BY/W25QXX写使能 */​

while ((QUADSPI->SR & (1 << 1)) == 0); /* 等待指令发送完成 */​

QUADSPI->FCR |= 1 << 1;​

if (qspi_wait_flag(1 << 5, 0, 0XFFFF) == 0) /* 等待BUSY空闲 */​
{​
tempreg =0XEB;/*INSTRUCTION[7:0]=0XEB,发送0XEB指令(Fast Read QUAD I/O)*/​
tempreg |= 1 << 8; /* IMODE[1:0]=1,单线传输指令 */​
tempreg |= 3 << 10; /* ADDRESS[1:0]=3,四线传输地址 */ ​
tempreg |=(uint32_t)g_norflash_addrw<<12;/*ADSIZE[1:0]=2,24/32位地址长度*/​
tempreg |= 3 << 14; /* ABMODE[1:0]=3,四线传输交替字节 */​
tempreg |= 0 << 16; /* ABSIZE[1:0]=0,8位交替字节(M0~M7) */​
tempreg |= 4 << 18; /* DCYC[4:0]=4,4个dummy周期 */​
tempreg |= 3 << 24; /* DMODE[1:0]=3,四线传输数据 */​
tempreg |= 3 << 26; /* FMODE[1:0]=3,内存映射模式 */​
QUADSPI->CCR = tempreg;/* 设置CCR寄存器 */​
}​
INTX_ENABLE(); /* 开启中断 */​
}

该函数使QSPI接口进入内存映射模式。内存映射模式:外部 FLASH 映射到微控制器地址空间,从而系统将其视作内部存储器。

接下来要介绍的是QSPI接口退出内存映射模式函数,其定义如下:

/**​
* @brief QSPI接口退出内存映射模式​
* @note 调用该函数之前务必已经初始化了QSPI接口​
* sys_qspi_enable_memmapmode or norflash_init​
* @param 无​
* @retval 0, OK; 其他, 错误代码​
*/​
static uint8_t norflash_ex_exit_mmap(void)​
{​
uint8_t res = 0;​

INTX_DISABLE(); /* 关闭中断 */​
SCB_InvalidateICache(); /* 清空I CACHE */​
SCB_InvalidateDCache(); /* 清空D CACHE */​
QUADSPI->CR &= ~(1 << 0); /* 关闭 QSPI 接口 */​
QUADSPI->CR |= 1 << 1; /* 退出MEMMAPED模式 */​
res = qspi_wait_flag(1 << 5, 0, 0XFFFF); /* 等待BUSY空闲 */​

if (res == 0)​
{​
QUADSPI->CCR = 0; /* CCR寄存器清零 */​
QUADSPI->CR |= 1 << 0; /* 使能 QSPI 接口 */​
}​

return res;​
}

该函数使QSPI接口退出内存映射模式。norflash_ex_enter_mmapnorflash_ex_exit_mmap是成对存在的函数,也是norflash_ex.c文件中最重要的函数。

接下来介绍QSPI FLASH写入数据函数,其定义如下:

/**​
* @brief 往 QSPI FLASH写入数据​
* @note 在指定地址开始写入指定长度的数据​
* 该函数带擦除操作!​
* @param pbuf : 数据存储区​
* @param addr : 开始写入的地址(最大32bit)​
* @param datalen : 要写入的字节数(最大65535)​
* @retval 0, OK; 其他, 错误代码​
*/​
uint8_t norflash_ex_write(uint8_t *pbuf, uint32_t addr, uint16_t datalen)​
{​
uint8_t res = 0;​
res = norflash_ex_exit_mmap(); /* 退出内存映射模式 */​

if (res == 0)​
{​
norflash_write(pbuf, addr, datalen);​
}​

norflash_ex_enter_mmap(); /* 进入内存映射模式 */​
return res;​
}

因为STM32H7不支持QSPI接口读时写,所以往 QSPI FLASH写入数据前,需要先调用norflash_ex_exit_mmap函数退出内存映射模式。退出内存映射模式,CPU就不会在QSPI FLASH里读取程序指令,即避免了QSPI接口读(指令)时写。写好后,再进入内存映射模式。该思路也就是norflash_ex_write函数的操作过程。

接下来介绍从QSPI FLASH读取数据函数,其定义如下:

/**​
* @brief 从 QSPI FLASH 读取数据​
* @note 在指定地址开始读取指定长度的数据(必须处于内存映射模式下,才可以执行)​
*​
* @param pbuf : 数据存储区​
* @param addr : 开始读取的地址(最大32bit)​
* @param datalen : 要读取的字节数(最大65535)​
* @retval 0, OK; 其他, 错误代码​
*/​
void norflash_ex_read(uint8_t *pbuf, uint32_t addr, uint16_t datalen)​
{​
uint16_t i = 0;​
/* 使用内存映射模式读取,QSPI的基址是0X90000000,所以这里要加上基址 */​
addr += 0X90000000;​
INTX_DISABLE(); /* 关闭中断 */​

for (i = 0; i < datalen; i++)​
{​
pbuf[i] = *(volatile uint8_t *)(addr + i);​
}​
INTX_ENABLE(); /* 开启中断 */​
}

从QSPI FLASH 读取数据就没有写入这么麻烦了,因为不需要考虑STM32H7不支持QSPI接口读时写的问题,但是仍然有要注意的问题。首先是我们使用内存映射模式读取数据的话,还需要加上QSPI的基址。QSPI的基址在qspi_code.scf文件中定义,是0X90000000,所以这里要在QSPI FLASH开始读取的地址上,再加上基址0X90000000。读取的过程是不允许被打断的,所以还要关闭所有中断,读取完成才打开所有中断。

norflash _ex.c文件我们就介绍这四个函数,其他请大家自行查阅。

3. main.c代码

在main.c里面编写如下代码:

/* 要写入到FLASH的字符串数组 */​
const uint8_t g_text_buf[] = {"MiniPRO STM32H7 QSPI TEST"};​
#define TEXT_SIZE sizeof(g_text_buf) /* TEXT字符串长度 */​

int main(void)​
{​
uint8_t key;​
uint16_t i = 0;​
uint8_t datatemp[TEXT_SIZE];​
uint32_t flashsize;​
uint16_t id = 0;​

sys_cache_enable(); /* 打开L1-Cache */​
HAL_Init(); /* 初始化HAL库 */​
sys_stm32_clock_init(240, 2, 2, 4); /* 设置时钟, 480Mhz */​
delay_init(480); /* 延时初始化 */​
usart_init(115200); /* 串口初始化为115200 */​
usmart_dev.init(240); /* 初始化USMART */​
mpu_memory_protection(); /* 保护相关存储区域 */​
led_init(); /* 初始化LED */​
lcd_init(); /* 初始化LCD */​
key_init(); /* 初始化按键 */​
/* ​
不需要调用norflash_init函数了,因为sys.c的sys_qspi_enable_memmapmode函数已​
经初始化了QSPI接口,如果再调用,则内存映射模式的设置被破坏,导致QSPI代码执行异常!​
除非不用分散加载,所有代码放内部FLASH,才可以调用该函数!否则将导致异常!​
*/​
//norflash_init();​

lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);​
lcd_show_string(30, 70, 200, 16, 16, "QSPI TEST", RED);​
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);​
/* 显示提示信息 */​
lcd_show_string(30, 110, 200, 16, 16, "KEY1:Write KEY0:Read", RED);​

id = norflash_ex_read_id(); /* 读取FLASH ID */​

while ((id == 0) || (id == 0XFFFF)) /* 检测不到FLASH芯片 */​
{​
lcd_show_string(30, 130, 200, 16, 16, "FLASH Check Failed!", RED);​
delay_ms(500);​
lcd_show_string(30, 130, 200, 16, 16, "Please Check! ", RED);​
delay_ms(500);​
LED0_TOGGLE(); /* LED0闪烁 */​
}​

lcd_show_string(30, 130, 200, 16, 16, "QSPI FLASH Ready!", BLUE);​
flashsize = 16 * 1024 * 1024; /* FLASH 大小为16M字节 */​

while (1)​
{​
key = key_scan(0);​

if (key == KEY1_PRES) /* KEY1按下,写入 */​
{​
lcd_fill(0, 150, 239, 319, WHITE); /* 清除半屏 */​
lcd_show_string(30, 150, 200, 16, 16, "Start Write FLASH....", BLUE);​
sprintf((char *)datatemp, "%s%d", (char *)g_text_buf, i);​
/* 从倒数第100个地址处开始,写入SIZE长度的数据 */​
norflash_ex_write((uint8_t *)datatemp, flashsize - 100, TEXT_SIZE); ​
/* 提示传送完成 */ ​
lcd_show_string(30, 150, 200, 16, 16, "FLASH Write Finished!", BLUE); ​
}​

if (key == KEY0_PRES) /* KEY0按下,读取字符串并显示 */​
{​
lcd_show_string(30, 150, 200, 16, 16, "Start Read FLASH.... ", BLUE);​
/* 从倒数第100个地址处开始,读出SIZE个字节 */​
norflash_ex_read(datatemp, flashsize - 100, TEXT_SIZE); ​
/* 提示传送完成 */ ​
lcd_show_string(30, 150, 200, 16, 16, "The Data Readed Is: ", BLUE); ​
/* 显示读到的字符串 */​
lcd_show_string(30, 170, 200, 16, 16, (char *)datatemp, BLUE); ​
}​

i++;​

if (i == 20)​
{​
LED0_TOGGLE(); /* LED0闪烁 */​
i = 0;​
}​

delay_ms(10);​
}​
}

在main函数前面,我们定义了g_text_buf数组,用于存放要写入到FLASH的字符串。在main中初始化外部设备NOR FLASH需要注意,这里不需要调用norflash_init函数了,因为sys.c里面的sys_qspi_enable_memmapmode函数已经初始化了QSPI接口。如果再调用,则内存映射模式的设置被破坏,导致QSPI代码执行异常!如果不使用分散加载,即所有代码加载到内部FLASH,才可以调用norflash_init函数。后面的无限循环就是KEY1按下,就写入NOR FLASH。KEY0按下,读取刚才写入的字符串并显示。

最后,我们将norflash_ex_read_idnorflash_ex_erase_chipnorflash_ex_erase_sector函数加入USMART控制,大家还可以把其他的函数加进来,这样,我们就可以通过串口调试助手,操作NOR FLASH,方便大家测试。norflash_ex_erase_chip函数大家谨慎调用,因为会把NOR FLASH的程序指令也擦除掉,会导致死机。如果不使用分散加载,就没关系。

36.4 下载验证

将程序下载到开发板后,可以看到LED0不停的闪烁,提示程序已经在运行了。LCD显示的内容如图36.4.1所示:

《MiniPRO H750开发指南》第三十六章 QSPI实验_数据_11


36.4.1 QSPI实验程序运行效果图

通过先按KEY1按键写入数据,然后按KEY0读取数据,得到如图36.4.2所示:

《MiniPRO H750开发指南》第三十六章 QSPI实验_数据_12


36.4.2操作后的显示效果图

程序在开机的时候会检测NOR FLASH是否存在,如果不存在则会在LCD模块上显示错误信息,同时LED0慢闪。