搜索
bottom↓
回复: 0

《MiniPro STM32H750 开发指南》第三十六章 QSPI实验

[复制链接]

出0入234汤圆

发表于 2022-9-16 11:02:42 | 显示全部楼层 |阅读模式
本帖最后由 正点原子 于 2022-9-16 11:01 编辑

1)实验平台:正点原子MiniPro STM32H750开发板
2)平台购买链接:https://detail.tmall.com/item.htm?id=677017430560
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boar
4)MiniPro STM32H750技术交流QQ群:756580169 lQLPJxaFi2zaB4UWWrDAMgIsFEW2pwLb3abnwDMA_90_22.png
lQDPJxaFi2nfFizMjM0CbLCPlxn_FVheIQLb3aGrwFQA_620_140.jpg

lQLPJxaFi2nfFhLMkM0BXrDNvOUyeU_FPgLb3aGvQNIA_350_144.png


第三十六章 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所示:
第三十六章 QSPI实验823.png
图36.1.1.1.1 STM32H7 QSPI框图

上图左边可以看到QSPI连接到64位 AXI 总线以及32位AHB总线上,此外还有5条QSPI的内部信号,如下表所示:
lQLPJxawvINMoMrMqc0CvrBiM9BDO8_jJwMioZXhQLAA_702_169.png
表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),具体如下表所示:
lQLPJxawvJlaXMvMxM0C1rCUUrioykvbOwMioblZwBQA_726_196.png
表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所示:
第三十六章 QSPI实验2345.png
图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~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,则表示无需发送地址。
②交替字节(复用字节)阶段
此阶段可以发送1~4字节数据给FLASH芯片,一般用于控制操作模式。待发送的交替字节数由QUADSPI_CCR[17:16]寄存器的ABSIZE[1:0]位配置。待发送的数据由QUADSPI_ABR寄存器中指定。交替字节同样可以以单线/双线/四线模式发送,通过QUADSPI_CCR[15:14]寄存器的ABMODE[1:0]这两个位配置,ABMODE[1:0]=00,则跳过交替字节阶段。
③空指令周期阶段
在空指令周期阶段,在给定的1~31个周期内不发送或接收任何数据,目的是当采用更高的时钟频率时,给FLASH芯片留出准备数据阶段的时间。这一阶段中给定的周期数由QUADSPI_CCR[22:18]寄存器的DCYC[4:0]位配置。 若DCYC为零,则跳过空指令周期阶段,命令序列直接进入下一个阶段。
④数据阶段
此阶段可以从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中可用。
①内存映射模式
在配置为内存映射模式时,外部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_CR)
QUADSPI控制寄存器描述如图36.1.1.5.1所示:
1C05A384-59A2-4097-B6EA-33EEDC1BE3FE.png
图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_ DCR)
QUADSPI器件配置寄存器描述如图36.1.1.5.2所示:
第三十六章 QSPI实验6808.png
图36.1.1.5.2 QUADSPI_ DCR寄存器

该寄存器可以设置FLASH芯片的容量(FSIZE)、片选高电平时间(CSHT)和时钟模式(CKMODE)等,这些位的设置说明见前面的36.1.1.4小节有详细讲解。
QUADSPI状态寄存器(QUADSPI_ SR)
QUADSPI状态寄存器描述如图36.1.1.5.3所示:
第三十六章 QSPI实验6977.png
图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_ FCR)
QUADSPI标志清零寄存器描述如图36.1.1.5.4所示:
第三十六章 QSPI实验7299.png
图36.1.1.5.4 QUADSPI_ FCR寄存器

该寄存器,我们一般只用到CTCF位,用于清除QSPI的传输完成标志。
QUADSPI通信配置寄存器(QUADSPI_ CCR)
QUADSPI通信配置寄存器描述如图36.1.1.5.5所示:
第三十六章 QSPI实验7425.png
图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所示:
lQLPJxawvLoisgHNAfjNAueweGASRNLTLCEDIqHvBoCtAA_743_504.png  

                                            

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所示:
lQLPJxawvPD4x4TMqs0CzLCsnFYHDMhMfgMiokiiAIIA_716_170.png
表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所示:
第三十六章 QSPI实验11482.png
表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的连接关系,如下图所示:
第三十六章 QSPI实验12522.png
图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 程序流程图
第三十六章 QSPI实验21984.png
图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发送命令函数,其定义如下:
/**
* @brief       QSPI发送命令
* @param       cmd : 要发送的指令
* @param       addr: 发送到的目的地址
* @param       mode: 模式,详细位定义如下:
*  @arg mode[1:0]: 指令模式;00,无指令;01,单线传输指令;10,双线传输指令;11,四线传输指令.
*  @arg mode[3:2]: 地址模式;00,无地址;01,单线传输地址;10,双线传输地址;11,四线传输地址.
*  @arg mode[5:4]: 地址长度;00,8位地址;01,16位地址; 10,24位地址;   11,32位地址.
*  @arg mode[7:6]: 数据模式;00,无数据; 01,单线传输数据;10,双线传输数据;11,四线传输数据.
* @param       dmcycle: 空指令周期数
* @retval      无
*/
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;
            }
/* 写入整个扇区 */
            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
= *(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所示:
第三十六章 QSPI实验44091.png
图36.4.1 QSPI实验程序运行效果图

通过先按KEY1按键写入数据,然后按KEY0读取数据,得到如图36.4.2所示:
第三十六章 QSPI实验44156.png
图36.4.2 操作后的显示效果图

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


回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-4-18 10:14

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表