首页 > 其他分享 >《MiniPRO H750开发指南》第四十六章 FATFS实验

《MiniPRO H750开发指南》第四十六章 FATFS实验

时间:2022-09-28 10:34:46浏览次数:61  
标签:HAL MiniPRO FLASH 模式 FATFS 寄存器 QUADSPI H750 QSPI

第三十六章 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,用于发送和接收。​
  • 允许 8、16 和 32 位数据访问。​
  • 具有适用于间接模式操作的DMA通道。​
  • 在达到 FIFO 阈值、超时、操作完成以及发生访问错误时产生中断。​

36.1.1.1 QSPI框图

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

《MiniPRO H750开发指南》第四十六章 FATFS实验_数据


图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时钟频率为220MHZ。quadspi_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开发指南》第四十六章 FATFS实验_内存映射_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开发指南》第四十六章 FATFS实验_数据_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开发指南》第四十六章 FATFS实验_数据_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开发指南》第四十六章 FATFS实验_内存映射_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开发指南》第四十六章 FATFS实验_内存映射_06


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

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

《MiniPRO H750开发指南》第四十六章 FATFS实验_寄存器_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芯片,比如有:W25Q128、BY25Q128、NM25Q128,它们是来自不同的厂商的同种规格的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开发指南》第四十六章 FATFS实验_内存映射_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 - PA1、KEY1 - PA15​

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

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

3. 原理图

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

《MiniPRO H750开发指南》第四十六章 FATFS实验_数据_09


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

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

36.3 程序设计

36.3.1 QSPI的HAL库驱动

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

1. HAL_QSPI_Init函数

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

HAL_StatusTypeDef HAL_QSPI_Init(QSPI_HandleTypeDef *hqspi);
  • 函数描述:用于初始化QSPI。​
  • 函数形参:形参1是QSPI_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命令。​
  • 函数形参:形参1是QSPI_HandleTypeDef结构体类型指针变量。​形参2是QSPI_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);
  • 函数描述:该函数用来接收数据。​
  • 函数形参:形参1是QSPI_HandleTypeDef结构体类型指针变量。​
    形参2是uint8_t类型指针变量,存放接收数据缓冲区指针。 ​
    形参3设置操作超时时间。​
  • 函数返回值:HAL_StatusTypeDef枚举类型的值。​4. HAL_QSPI_Transmit函数QSPI发送数据函数,其声明如下:​
HAL_StatusTypeDef HAL_QSPI_Transmit (QSPI_HandleTypeDef *hqspi, ​
uint8_t *pData, uint32_t Timeout);
  • 函数描述:该函数用来发送数据。​
  • 函数形参:形参1是QSPI_HandleTypeDef结构体类型指针变量。​
    形参2是uint8_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开发指南》第四十六章 FATFS实验_内存映射_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.c、norflash.h、norflash_ex.c和norflash_ex.h。 ​

因为STM32H7不支持QSPI接口读时写,因此我们新建了norflash_ex.c和norflash_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_mmap和norflash_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_id、norflash_ex_erase_chip和norflash_ex_erase_sector函数加入USMART控制,大家还可以把其他的函数加进来,这样,我们就可以通过串口调试助手,操作NOR FLASH,方便大家测试。norflash_ex_erase_chip函数大家谨慎调用,因为会把NOR FLASH的程序指令也擦除掉,会导致死机。如果不使用分散加载,就没关系。​

36.4 下载验证

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

《MiniPRO H750开发指南》第四十六章 FATFS实验_寄存器_11


图36.4.1 QSPI实验程序运行效果图​

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

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


图36.4.2操作后的显示效果图​

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


标签:HAL,MiniPRO,FLASH,模式,FATFS,寄存器,QUADSPI,H750,QSPI
From: https://blog.51cto.com/u_15046463/5718431

相关文章