你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

MiniPro STM32H750 开发指南_V1.1-QSPI实验

[复制链接]
STMCU小助手 发布时间:2022-10-7 22:24
QSPI实验
本章,我们将介绍STM32H750的QSPI功能,并使用STM32H750自带的QSPI来实现对外部NOR FLASH的读写,并将结果显示在LCD模块上。

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所示:

d8b302ad53e345db9def7c897a3cedee.png

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

9de93a86ebce47009a1660425d2aec11.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),具体如下表所示:
104bc8b71bd1464fa16328b89bd11d92.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所示:

c755a29932c74353ab498630e80158cb.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,则表示无需发送指令。
②地址阶段
此阶段可以发送14字节地址给FLASH芯片,指示要操作的地址。地址字节长度由QUADSPI_CCR[13:12]寄存器的ADSIZE[1:0]字段指定,03表示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=21
FSIZE+1是对Flash寻址所需的地址位数。Fcap表示FLASH的容量,单位为字节,在间接模式下,最高支持4GB(使用32位进行寻址)容量的FLASH芯片。但是在内存映射模式下的可寻址空间限制为256MB。
QSPI连续执行两条命令时,它在两条命令之间将片选信号 (nCS) 置为高电平默认仅一个 CLK周期。某些FLASH需要命令之间的时间更长,可以通过寄存器QUADSPI_DCR[10:8]的CSHT[2:0](选高电平时间)这3个位设置高电平时长:07表示18个时钟周期(最大为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所示:

35d153dd5b954cd19b65fb812e50be6d.png

图36.1.1.5.1 QUADSPI_CR寄存器
该寄存器我们只关心需要用到的一些位(下同),首先是PRESCALER[7:0],用于设置AHB时钟预分频器:0255,表示0256分频。我们使用的W25Q128最大支持104Mhz的时钟,这里我们设置PRESCALER=2,即3分频,得到QSPI时钟为72Mhz(216/3)。
FTHRES[4:0],用于设置FIFO阈值,范围为031,表示FIFO的阈值为132字节。
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所示:

f2db90ba4b4f4083a2fc5e04699a4de2.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所示:

0c67727622474763b4081b99327b462d.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所示:

22eb85ed41c74afda8a17300b8ad494e.png

图36.1.1.5.4 QUADSPI_ FCR寄存器
该寄存器,我们一般只用到CTCF位,用于清除QSPI的传输完成标志。
QUADSPI通信配置寄存器(QUADSPI_ CCR)
QUADSPI通信配置寄存器描述如图36.1.1.5.5所示:

617cebe81c76488c9e4e9a3045f24cef.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所示:

339ea32194e54beaa633069446500d7b.png

1,在QPI模式下dummy时钟的个数,由读参数控制位P[5:4]位控制。
2,传输的数据量,只要不停的给时钟就可以持续传输,对W25X_PageProgram指令,则单次传输最多不超过256字节,否则将覆盖之前写入的数据。
表36.1.2.1 W25Q128指令
上表列出了本章我们驱动W25Q128所需要用到的所有指令和对应的参数,注意SPI模式和QPI模式下时钟数的区别,可知QPI模式比SPI模式所需要的时钟数少的多,所以速度也快得多。接下来我们简单介绍一下这些指令。
首先,前面6个指令,是用来读取/写入状态寄存器13的。在读取的时候,读取S23S0的数据,在写入的时候,写入S23S0。而S23S0则由三部分组成:S23S16,S15S8,S7~S0即状态寄存器3、2、1,如表36.1.2.2所示:

b1c5e051153a480398745967a634e1ea.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所示:

61ad36f26a334717ae7eb6eaeb452919.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的连接关系,如下图所示:

d2f611c0e6f24f9eb68b3b5b326e5015.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结构体类型指针变量,其定义如下:
  1. typedef struct
  2. {
  3.   QUADSPI_TypeDef                            *Instance;            /* QSPI寄存器基址 */  
  4.   QSPI_InitTypeDef                      Init;                  /* QSPI参数配置结构体 */   
  5.   uint8_t                                *pTxBuffPtr;          /* 要发送数据的地址 */      
  6.   __IO uint16_t                         TxXferSize;           /* 要发送数据的大小 */      
  7.   __IO uint16_t                         TxXferCount;          /* 剩余要发送数据的个数 */      
  8.   uint8_t                                *pRxBuffPtr;          /* 要接收数据的地址 */      
  9.   __IO uint16_t                         RxXferSize;           /* 要接收数据的大小 */      
  10.   __IO uint16_t                         RxXferCount;          /* 剩余要接收数据的个数 */      
  11.   DMA_HandleTypeDef                    * hmdma;               /* DMA配置结构体 */        
  12.   __IO HAL_LockTypeDef                 Lock;                  /* 锁对象 */      
  13.   __IO HAL_QSPI_StateTypeDef          State;                /* QSPI通信状态 */      
  14.   __IO uint32_t                         ErrorCode;            /* 错误代码 */      
  15.   uint32_t                               Timeout;              /* 配置QSPI内存访问超时时间 */      
  16. }QSPI_HandleTypeDef;
复制代码

Instance:用于设置QSPI寄存器基地址,设置为QUADSPI即可,这个官方已经为我们做好了宏定义。
Init:用于设置QSPI的相关参数,QSPI_InitTypeDef结构体下面再进行详细讲解。
pTxBuffPtr、TxXferSize和TxXferCount:分别用于设置 QSPI 发送缓冲指针、发送数据量和发送剩余数据量。
pRxBuffPtr、RxXferSize和RxXferCount:分别用于设置接收缓冲指针、接收数据量和接收剩余数据量。
hmdma:用于配置相关的 DMA 参数。
Lock:用于分配锁资源,可选择 HAL_UNLOCKED 或者是 HAL_LOCKED两个参数。
State:用于存放通讯过程中的工作状态。
ErrorCode:通过该参数,用户可以了解到QSPI通讯过程中通信失败的原因。
Timeout:用于设置超时时间。QSPI访问时间一旦超出 Timeout这个变量值,那么ErrorCode成员变量就会被赋值为HAL_QSPI_ERROR_TIMEOUT,表示操作超时。
下面重点来了解QSPI_InitTypeDef结构体的内容,其定义如下:
  1. typedef struct
  2. {
  3.   uint32_t ClockPrescaler;                      /* 时钟预分频系数 */                          
  4.   uint32_t FifoThreshold;                       /* 设置FIFO阈值级别 */      
  5.   uint32_t SampleShifting;                      /* 设置采样移位 */
  6.   uint32_t FlashSize;                            /* 设置FLASH大小 */         
  7.   uint32_t ChipSelectHighTime;                 /* 设置片选高电平时间 */
  8.   uint32_t ClockMode;                            /* 设置时钟模式 */         
  9.   uint32_t FlashID;                              /* 闪存ID,第一片还是第二片 */               
  10.   uint32_t DualFlash;                           /* 双闪存模式设置 */                       
  11. }QSPI_InitTypeDef;
复制代码

ClockPrescaler:用于设置预分频系数,对应QUADSPI_CR寄存器的PRESCALER[7:0]位,取值范围是 0~255。仅可在 BUSY = 0 时修改该字段。
FifoThreshold:用于设置FIFO阈值级别,可设置范围为 0~31,对应QUADSPI_CR寄存器的FTHRES[4:0]位。
SampleShifting:用于设置采样移位,对应QUADSPI_CR寄存器的SSHIFT位。使用该位是考虑到外部信号延迟时,推迟数据采样。可以取值 QSPI_SAMPLE_SHIFTING_NONE(即0):不发生移位;QSPI_SAMPLE_SHIFTING_HALFCYCLE(即1):移位半个周期。在DDR模式下 (DDRM = 1),固件必须确保SSHIFT = 0。
FlashSize:用于设置FLASH大小,对应QUADSPI_ DCR寄存器的FSIZE[4:0]位,可设置的范围是:0到31之间的整数。FLASH 中的字节数= 2 [FSIZE+1]。在间接模式下,FLASH容量最高可达4GB(使用 32 位进行寻址),但在内存映射模式下的可寻址空间限制为256MB。
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周期数。
ClockMode:用于设置时钟模式,对应QUADSPI_DCR寄存器CKMODE位,指示 CLK在命令之间(nCS = 1 时)的电平,可以选择的参数是:QSPI_CLOCK_MODE_0(表示模式0)或者QSPI_CLOCK_MODE_3(表示模式3)。模式 0是:nCS为高电平(片选释放)时,CLK 必须保持低电平。模式3是:nCS 为高电平(片选释放)时,CLK 必须保持高电平。
FlashID:用于选择Flash1或者Flash2,单闪存模式下选择QSPI_FLASH_ID_1(表示Flash1)。
DualFlash:用于使能双闪存模式,QSPI_DUALFLASH_DISABLE:禁止双闪存模式;QSPI_DUALFLASH_ENABLE:使能双闪存模式。对应QUADSPI_CR寄存器DFM位。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
注意事项:
QSPI的MSP初始化函数HAL_QSPI_MspInit,该函数声明如下:
void HAL_QSPI_MspInit(QSPI_HandleTypeDef *hqspi);
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结构体类型指针变量,其定义如下:
  1. typedef struct
  2. {
  3.   uint32_t Instruction;                   /* 指令 */                                    
  4.   uint32_t Address;                        /* 地址 */                              
  5.   uint32_t AlternateBytes;               /* 交替字节 */                             
  6.   uint32_t AddressSize;                   /* 地址长度 */                                    
  7.   uint32_t AlternateBytesSize;           /* 交替字节大小 */                     
  8.   uint32_t DummyCycles;                   /* 控指令周期数 */                     
  9.   uint32_t InstructionMode;              /* 指令模式 */                             
  10.   uint32_t AddressMode;                   /* 地址模式 */                        
  11.   uint32_t AlternateByteMode;            /* 交替字节模式 */            
  12.   uint32_t DataMode;                       /* 数据模式 */  
  13.   uint32_t NbData;                         /* 数据长度 */                                                                     
  14.   uint32_t DdrMode;                        /* 指定地址、备用字节和数据阶段的双数据速率模式 */                          
  15.   uint32_t DdrHoldHalfCycle;             /* 指定DDR模式下数据保持的周期 */                                                        
  16.   uint32_t SIOOMode;                      /* 指定发送指令仅一次模式 */                        
  17. }QSPI_CommandTypeDef;
复制代码

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

8d2468196e6f4b43babaa8a0564eb317.png

图36.3.2.1 QSPI实验程序流程图

36.3.3 程序解析
1.QSPI驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。QSPI驱动源码包括两个文件:qspi.c和qspi.h。
qspi.h头文件对QSPI相关引脚做了宏定义,该宏定义如下:
/* QSPI 相关 引脚 定义 */
  1. #define QSPI_BK1_CLK_GPIO_PORT          GPIOB
  2. #define QSPI_BK1_CLK_GPIO_PIN           GPIO_PIN_2
  3. #define QSPI_BK1_CLK_GPIO_AF            GPIO_AF9_QUADSPI
  4. #define QSPI_BK1_CLK_GPIO_CLK_ENABLE()  
  5. do{ __HAL_RCC_GPIOB_CLK_ENABLE; }while(0)   /* PB口时钟使能 */

  6. #define QSPI_BK1_NCS_GPIO_PORT          GPIOB
  7. #define QSPI_BK1_NCS_GPIO_PIN           GPIO_PIN_6
  8. #define QSPI_BK1_NCS_GPIO_AF            GPIO_AF10_QUADSPI
  9. #define QSPI_BK1_NCS_GPIO_CLK_ENABLE()  
  10. do{ __HAL_RCC_GPIOB_CLK_ENABLE; }while(0)   /* PB口时钟使能 */

  11. #define QSPI_BK1_IO0_GPIO_PORT          GPIOD
  12. #define QSPI_BK1_IO0_GPIO_PIN           GPIO_PIN_11
  13. #define QSPI_BK1_IO0_GPIO_AF            GPIO_AF9_QUADSPI
  14. #define QSPI_BK1_IO0_GPIO_CLK_ENABLE()
  15. do{ __HAL_RCC_GPIOD_CLK_ENABLE; }while(0)  /* PD口时钟使能 */

  16. #define QSPI_BK1_IO1_GPIO_PORT          GPIOD
  17. #define QSPI_BK1_IO1_GPIO_PIN           GPIO_PIN_12
  18. #define QSPI_BK1_IO1_GPIO_AF            GPIO_AF9_QUADSPI
  19. #define QSPI_BK1_IO1_GPIO_CLK_ENABLE()
  20. do{ __HAL_RCC_GPIOD_CLK_ENABLE; }while(0)   /* PD口时钟使能 */

  21. #define QSPI_BK1_IO2_GPIO_PORT          GPIOD
  22. #define QSPI_BK1_IO2_GPIO_PIN           GPIO_PIN_13
  23. #define QSPI_BK1_IO2_GPIO_AF            GPIO_AF9_QUADSPI
  24. #define QSPI_BK1_IO2_GPIO_CLK_ENABLE()  
  25. do{ __HAL_RCC_GPIOD_CLK_ENABLE; }while(0)   /* PD口时钟使能 */

  26. #define QSPI_BK1_IO3_GPIO_PORT          GPIOE
  27. #define QSPI_BK1_IO3_GPIO_PIN           GPIO_PIN_2
  28. #define QSPI_BK1_IO3_GPIO_AF            GPIO_AF9_QUADSPI
  29. #define QSPI_BK1_IO3_GPIO_CLK_ENABLE()  
复制代码

do{ __HAL_RCC_GPIOE_CLK_ENABLE; }while(0) /* PE口时钟使能 */
注意这6个GPIO都是用到复用功能,对应引脚的复用功能情况请看《STM32H750VBT6.pdf》数据手册79页之后的端口复用功能表格。
下面我们开始介绍qspi.c的程序,首先是QSPI接口初始化函数,其定义如下:

  1. /**
  2. * @brief              初始化QSPI接口
  3. * @param               无
  4. * @retval              0, 成功; 1, 失败.
  5. */
  6. uint8_t qspi_init(void)
  7. {
  8.     g_qspi_handle.Instance = QUADSPI;       /* QSPI */
  9. /* QPSI分频比,BY25Q128最大频率为108M,所以此处应该为2,QSPI频率就为
  10. 220/(1+1)=110MHZ稍微有点超频,可以正常就好,不行就只能降低频率 */
  11.     g_qspi_handle.Init.ClockPrescaler = 1;
  12. g_qspi_handle.Init.FifoThreshold = 4;  /* FIFO阈值为4个字节 */
  13. /* 采样移位半个周期(DDR模式下,必须设置为0) */
  14. g_qspi_handle.Init.SampleShifting = QSPI_SAMPLE_SHIFTING_HALFCYCLE;
  15. /* SPI FLASH大小,BY25Q128大小为32M字节,2^25,所以取权值25-1=24 */
  16. g_qspi_handle.Init.FlashSize = 25-1;         
  17. /* 片选高电平时间为3个时钟(9.1*3=27.3ns),即手册里面的tSHSL参数 */
  18.     g_qspi_handle.Init.ChipSelectHighTime = QSPI_CS_HIGH_TIME_3_CYCLE;
  19.     g_qspi_handle.Init.ClockMode = QSPI_CLOCK_MODE_3;        /* 模式3 */
  20.     g_qspi_handle.Init.FlashID = QSPI_FLASH_ID_1;             /* 第一片flash */
  21.     g_qspi_handle.Init.DualFlash = QSPI_DUALFLASH_DISABLE;  /* 禁止双闪存模式 */
  22.     if(HAL_QSPI_Init(&g_qspi_handle) == HAL_OK)
  23.     {
  24.         return 0;      /* QSPI初始化成功 */
  25.     }
  26.     else
  27.     {
  28.         return 1;
  29.     }
  30. }
复制代码
这里我们需要注意的是,QSPI的时钟源在sys_stm32_clock_init函数中已经选择了PLL2R(我们设置为220MHZ),这里就不需要再选择时钟源了,只需要选择预分频系数就可以确定QSPI的时钟频率。时钟模式选择模式3,在未进行任何操作时 CLK 升至高电平。我们用单闪存模式,FlashID要选择QSPI_FLASH_ID_1。
我们用HAL_QSPI_MspInit函数来编写QSPI时钟和IO配置等代码,其定义如下:

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

  10.     __HAL_RCC_QSPI_CLK_ENABLE();      /* 使能QSPI时钟 */
  11.     __HAL_RCC_GPIOB_CLK_ENABLE();     /* GPIOB时钟使能 */
  12.     __HAL_RCC_GPIOD_CLK_ENABLE();     /* GPIOD时钟使能 */
  13.     __HAL_RCC_GPIOE_CLK_ENABLE();     /* GPIOE时钟使能 */

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

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

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

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

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

  37. gpio_init_struct.Pin = QSPI_BK1_IO3_GPIO_PIN;
  38. /* 初始化QSPI_BK1_IO3引脚 */
  39.     HAL_GPIO_Init(QSPI_BK1_IO3_GPIO_PORT, &gpio_init_struct);   
  40. }
复制代码

这里初始化的6个引脚全部都要配置为复用功能模式,以及使能QSPI和相应IO时钟。
接下来介绍QSPI发送命令函数,其定义如下:

  1. /**
  2. * @brief       QSPI发送命令
  3. * @param       cmd : 要发送的指令
  4. * @param       addr: 发送到的目的地址
  5. * @param       mode: 模式,详细位定义如下:
  6. *  @arg mode[1:0]: 指令模式;00,无指令;01,单线传输指令;10,双线传输指令;11,四线传输指令.
  7. *  @arg mode[3:2]: 地址模式;00,无地址;01,单线传输地址;10,双线传输地址;11,四线传输地址.
  8. *  @arg mode[5:4]: 地址长度;00,8位地址;01,16位地址; 10,24位地址;   11,32位地址.
  9. *  @arg mode[7:6]: 数据模式;00,无数据; 01,单线传输数据;10,双线传输数据;11,四线传输数据.
  10. * @param       dmcycle: 空指令周期数
  11. * @retval      无
  12. */
  13. void qspi_send_cmd(uint8_t cmd, uint32_t addr, uint8_t mode, uint8_t dmcycle)
  14. {
  15.     QSPI_CommandTypeDef qspi_command_handle;

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

  19.     if(((mode >> 0) & 0x03) == 0)
  20.     qspi_command_handle.InstructionMode = QSPI_INSTRUCTION_NONE;   /* 指令模式 */
  21.     else if(((mode >> 0) & 0x03) == 1)
  22.     qspi_command_handle.InstructionMode = QSPI_INSTRUCTION_1_LINE; /* 指令模式 */
  23.     else if(((mode >> 0) & 0x03) == 2)
  24.     qspi_command_handle.InstructionMode = QSPI_INSTRUCTION_2_LINES;/* 指令模式 */
  25.     else if(((mode >> 0) & 0x03) == 3)
  26.     qspi_command_handle.InstructionMode = QSPI_INSTRUCTION_4_LINES;/* 指令模式 */

  27.     if(((mode >> 2) & 0x03) == 0)
  28.     qspi_command_handle.AddressMode = QSPI_ADDRESS_NONE;                   /* 地址模式 */
  29.     else if(((mode >> 2) & 0x03) == 1)
  30.     qspi_command_handle.AddressMode = QSPI_ADDRESS_1_LINE;                /* 地址模式 */
  31.     else if(((mode >> 2) & 0x03) == 2)
  32.     qspi_command_handle.AddressMode = QSPI_ADDRESS_2_LINES;               /* 地址模式 */
  33.     else if(((mode >> 2) & 0x03) == 3)
  34.     qspi_command_handle.AddressMode = QSPI_ADDRESS_4_LINES;               /* 地址模式 */

  35.     if(((mode >> 4)&0x03) == 0)
  36.     qspi_command_handle.AddressSize = QSPI_ADDRESS_8_BITS;                /* 地址长度 */
  37.     else if(((mode >> 4) & 0x03) == 1)
  38.     qspi_command_handle.AddressSize = QSPI_ADDRESS_16_BITS;               /* 地址长度 */
  39.     else if(((mode >> 4) & 0x03) == 2)
  40.     qspi_command_handle.AddressSize = QSPI_ADDRESS_24_BITS;               /* 地址长度 */
  41.     else if(((mode >> 4) & 0x03) == 3)
  42.     qspi_command_handle.AddressSize = QSPI_ADDRESS_32_BITS;               /* 地址长度 */

  43.     if(((mode >> 6) & 0x03) == 0)
  44.     qspi_command_handle.DataMode=QSPI_DATA_NONE;                           /* 数据模式 */
  45.     else if(((mode >> 6) & 0x03) == 1)
  46.     qspi_command_handle.DataMode = QSPI_DATA_1_LINE;                      /* 数据模式 */
  47.     else if(((mode >> 6) & 0x03) == 2)
  48.     qspi_command_handle.DataMode = QSPI_DATA_2_LINES;                     /* 数据模式 */
  49.     else if(((mode >> 6) & 0x03) == 3)
  50.     qspi_command_handle.DataMode = QSPI_DATA_4_LINES;                     /* 数据模式 */

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

  55.     HAL_QSPI_Command(&g_qspi_handle, &qspi_command_handle, 5000);
  56. }
复制代码

该函数主要就是配置QSPI_CommandTypeDef结构体的参数,并调用HAL_QSPI_Command函数配置发送命令,是一个重要的基础函数。
接下来介绍的是QSPI接收函数,其定义如下:

  1. /**
  2. * @brief               QSPI接收指定长度的数据
  3. * @param               buf     : 接收数据缓冲区首地址
  4. * @param               datalen : 要传输的数据长度
  5. * @retval              0, 成功; 其他, 错误代码.
  6. */
  7. uint8_t qspi_receive(uint8_t *buf, uint32_t datalen)
  8. {
  9.     g_qspi_handle.Instance->DLR = datalen - 1;   /* 配置数据长度 */
  10.     if (HAL_QSPI_Receive(&g_qspi_handle, buf, 5000) == HAL_OK)
  11.     {
  12.         return 0;
  13.     }
  14.     else
  15.     {
  16.         return 1;
  17.     }
  18. }
复制代码

该函数首先把要接收数据的长度赋值到QUADSPI数据长度寄存器(QUADSPI_DLR)中,然后通过调用HAL_QSPI_Receive函数接收数据。
接下来介绍的是QSPI发送函数,其定义如下:

  1. /**
  2. * @brief              QSPI发送指定长度的数据
  3. * @param               buf     : 发送数据缓冲区首地址
  4. * @param               datalen : 要传输的数据长度
  5. * @retval              0, 成功; 其他, 错误代码.
  6. */
  7. uint8_t qspi_transmit(uint8_t *buf, uint32_t datalen)
  8. {
  9.     g_qspi_handle.Instance->DLR = datalen - 1; /* 配置数据长度 */
  10.     if (HAL_QSPI_Transmit(&g_qspi_handle, buf, 5000) == HAL_OK)
  11.     {
  12.         return 0;
  13.     }
  14.     else
  15.     {
  16.         return 1;
  17.     }
  18. }
复制代码

该函数首先把要发送数据的长度赋值到QUADSPI数据长度寄存器(QUADSPI_DLR)中,然后通过调用HAL_QSPI_Transmit函数发送数据。
最后要介绍的一个函数是等待状态标志函数,其定义如下:

  1. /**
  2. * @brief               等待状态标志
  3. * @param               flag : 需要等待的标志位
  4. * @param               sta  : 需要等待的状态
  5. * @param               wtime: 等待时间
  6. * @retval              0, 等待成功; 1, 等待失败.
  7. */
  8. uint8_t qspi_wait_flag(uint32_t flag, uint8_t sta, uint32_t wtime)
  9. {
  10.     uint8_t flagsta = 0;

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

  14.         if (flagsta == sta)break;

  15.         wtime--;
  16.     }

  17.     if (wtime)return 0;
  18.     else return 1;
  19. }
复制代码

该函数可以设置一段时钟等待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初始化函数,其定义如下:

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

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

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

  21.              norflash_write_enable();            /* 写使能 */
  22.              /* QPI,使能4字节地址指令,地址为0,无数据_8位地址_无地址_单线传输指令,
  23. 无空周期,0个字节数据 */
  24.             qspi_send_cmd(FLASH_Enable4ByteAddr, 0, (0 << 6) | (0 << 4)
  25. | (0 << 2) | (1 << 0), 0);
  26.         }
  27.     }
  28.     //printf("ID:%x\r\n", g_norflash_type);
  29. }
复制代码

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

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

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

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

  13. void norflash_write(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
  14. {
  15.     uint32_t secpos;
  16.     uint16_t secoff;
  17.     uint16_t secremain;
  18.     uint16_t i;
  19.     uint8_t *norflash_buf;

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

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

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

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

  39.         if (i < secremain)   /* 需要擦除 */
  40.         {
  41.             norflash_erase_sector(secpos);          /* 擦除这个扇区 */
  42.             for (i = 0; i < secremain; i++)         /* 复制 */
  43.             {
  44.                 norflash_buf[i + secoff] = pbuf<i>;
  45.             }
  46. /* 写入整个扇区 */
  47.             norflash_write_nocheck(norflash_buf, secpos * 4096, 4096);  
  48.         }
  49.         else                /* 写已经擦除了的,直接写入扇区剩余区间. */
  50.         {
  51.             norflash_write_nocheck(pbuf, addr, secremain);  /* 直接写扇区 */
  52.         }

  53.         if (datalen == secremain)
  54.         {
  55.             break;          /* 写入结束了 */
  56.         }
  57.         else                /* 写入未结束 */
  58.         {
  59.             secpos++;                        /* 扇区地址增1 */
  60.             secoff = 0;                      /* 偏移位置为0 */

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

  64.             if (datalen > 4096)
  65.             {
  66.                 secremain = 4096;           /* 下一个扇区还是写不完 */
  67.             }
  68.             else
  69.             {
  70.                 secremain = datalen;        /* 下一个扇区可以写完了 */
  71.             }
  72.         }
  73.     }
  74. }</i>
复制代码

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

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

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

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

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

  15.     QUADSPI->FCR |= 1 << 1;

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

该函数使QSPI接口进入内存映射模式。内存映射模式:外部 FLASH 映射到微控制器地址空间,从而系统将其视作内部存储器。
接下来要介绍的是QSPI接口退出内存映射模式函数,其定义如下:

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

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

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

  22.     return res;
  23. }
复制代码

该函数使QSPI接口退出内存映射模式。norflash_ex_enter_mmap和norflash_ex_exit_mmap是成对存在的函数,也是norflash_ex.c文件中最重要的函数。
接下来介绍QSPI FLASH写入数据函数,其定义如下:

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

  14.     if (res == 0)
  15.     {
  16.         norflash_write(pbuf, addr, datalen);
  17.     }

  18.     norflash_ex_enter_mmap();       /* 进入内存映射模式 */
  19.     return res;
  20. }
复制代码

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

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

  16.     for (i = 0; i < datalen; i++)
  17.     {
  18.         pbuf = *(volatile uint8_t *)(addr + i);
  19.     }
  20.     INTX_ENABLE();      /* 开启中断 */
  21. }
复制代码

从QSPI FLASH 读取数据就没有写入这么麻烦了,因为不需要考虑STM32H7不支持QSPI接口读时写的问题,但是仍然有要注意的问题。首先是我们使用内存映射模式读取数据的话,还需要加上QSPI的基址。QSPI的基址在qspi_code.scf文件中定义,是0X90000000,所以这里要在QSPI FLASH开始读取的地址上,再加上基址0X90000000。读取的过程是不允许被打断的,所以还要关闭所有中断,读取完成才打开所有中断。
norflash _ex.c文件我们就介绍这四个函数,其他请大家自行查阅。
3. main.c代码
在main.c里面编写如下代码:
/* 要写入到FLASH的字符串数组 */

  1. const uint8_t g_text_buf[] = {"MiniPRO STM32H7 QSPI TEST"};
  2. #define TEXT_SIZE       sizeof(g_text_buf)  /* TEXT字符串长度 */

  3. int main(void)
  4. {
  5.     uint8_t key;
  6.     uint16_t i = 0;
  7.     uint8_t datatemp[TEXT_SIZE];
  8.     uint32_t flashsize;
  9.     uint16_t id = 0;

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

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

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

  32.     while ((id == 0) || (id == 0XFFFF)) /* 检测不到FLASH芯片 */
  33.     {
  34.         lcd_show_string(30, 130, 200, 16, 16, "FLASH Check Failed!", RED);
  35.         delay_ms(500);
  36.         lcd_show_string(30, 130, 200, 16, 16, "Please Check!      ", RED);
  37.         delay_ms(500);
  38.         LED0_TOGGLE();      /* LED0闪烁 */
  39. }

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

  42.     while (1)
  43.     {
  44.         key = key_scan(0);

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

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

  65.         i++;

  66.         if (i == 20)
  67.         {
  68.             LED0_TOGGLE();      /* LED0闪烁 */
  69.             i = 0;
  70.         }

  71.         delay_ms(10);
  72.     }
  73. }
复制代码

在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所示:

f86852a731b6482185373842d548181a.png

图36.4.1 QSPI实验程序运行效果图
通过先按KEY1按键写入数据,然后按KEY0读取数据,得到如图36.4.2所示:

f2bb00e6374742d1acde3a42bfed9770.png

图36.4.2 操作后的显示效果图
程序在开机的时候会检测NOR FLASH是否存在,如果不存在则会在LCD模块上显示错误信息,同时LED0慢闪。
————————————————
版权声明:正点原子



收藏 评论0 发布时间:2022-10-7 22:24

举报

0个回答

所属标签

相似分享

官网相关资源

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
st-img 微信公众号
st-img 手机版