|
第三十四章 OSPI实验
1)实验平台:正点原子Mini STM32H562开发板
2)章节摘自【正点原子】STM32H562开发指南 V1.0
3)购买链接: https://detail.tmall.com/item.htm?id=753606659612
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/stm32/zdyz_stm32h562_mini.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子STM32开发板技术交流群:414052738
本章,我们将介绍STM32H562的OSPI功能,并使用STM32H562自带的OSPI来实现对外部NOR FLASH的读写,并将结果显示在LCD模块上。
本章分为如下几个小节:
34.1 OSPI及NOR FLASH芯片简介
34.2 硬件设计
34.3 程序设计
34.4 下载验证
34.1 OSPI及NOR FLASH芯片简介
34.1.1 OSPI简介
介绍OSPI前,我们了解一下QSPI。QSPI是Quad SPI的缩写,是Motorola公司推出SPI接口后的一种扩展接口,较SPI应用更为广泛。在 SPI 协议的基础上,Motorola 公司对其功能进行了增加,增加了队列传输机制,推出了队列串行外围接口协议(即 QSPI 协议)。QSPI 是一种专用的通信接口,连接单、双或四(条数据线)SPI FLASH存储器。
OSPI是Octo SPI的缩写,是Quad SPI的8位扩展版,与QSPI相比,OSPI拥有更高的数据吞吐量。OSPI支持Single SPI(传统SPI)、双SPI、Quad SPI、Dual Quad SPI和Octo SPI的SPI FLASH存储器,本实验中,我们用的是四线SPI模式。
STM32H5具有OSPI接口,支持如下三种工作模式:
1、间接模式:使用OSPI寄存器执行全部操作。
2、状态轮询模式:周期性读取外部FLASH状态寄存器,当标志位置1时会产生中断(如
擦除或烧写完成,产生中断)。
3、内存映射模式:外部FLASH映射到微控制器地址空间,从而系统将其视作内部存储器。
STM32H5的OSPI接口具有如下特点:
支持三种工作模式:间接模式、自动状态轮询模式、内存映射模式
支持内存映射方式的读写
外部(P)SRAM内存支持
支持单、双、四、八线通信
双存储器配置,OSPI的8跟线可以同时访问两个四线存储器,支持同时发送/接收
支持SDR(单数据速率)和DTR(双传输速率)
支持数据频闪
完全可编程的操作码
完全可编程的帧格式
支持在读方向上对内存的封装式访问
支持HyperBus
集成FIFO接收和传输
支持异步总线时钟与内核时钟
允许8位、16位和32位数据访问
支持DMA协议
间接模式操作的DMA通道
在FIFO阈值,超时,操作完成和访问错误上产生中断
AHB接口的事务接受限制为一个:接口只接受AHB总线上的下一个传输,之前的传输在内存端完成。
34.1.1.1 OSPI四线单闪存模式框图
STM32H5的OSPI 四线单闪存模式的功能框图如图34.1.1.1.1所示:
图34.1.1.1.1 STM32H5的OSPI四线单闪存模式的功能框图
下面列出OSPI的内部信号,如下表所示:
表34.1.1.1.1 QSPI内部信号
octospi_ker_ck,是用于通讯过程的时钟。可以选择的时钟源有:rcc_hclk4、pll1_q_ck、pll2_r_ck和per_ck,实验中我们选择pll2_r_ck。我们通过HAL_XSPI_MspInit函数配置pll2_r_ck时钟频率为220MHz。quadspi_ker_ck还需要经过一个分频器出来的时钟频率才作为QSPI的实际使用的时钟频率,该分频器的分频系数由QUADSPI_CR寄存器的PRESCALER[7:0]位设置,范围是:0~255。
octospi_hclk,用于操作OSPI寄存器的时钟。时钟源来自HCLK,同样是经过sys_stm32_clock_init函数的配置,配置后HCLK时钟频率为250MHz。
octospi_dma,DMA请求信号。
octospi_it,全局中断线。在达到FIFO阈值、超时、操作完成以及发送访问错误时,可产生中断。
图34.1.1.1.1中间灰色部分就是QUADSPI 内核。
我们重点看看上图右边的OSPI接口引脚,通过6根线与QSPI FLASH芯片连接,包括:4根数据线(IO0~3)、1根时钟线(CLK)和1根片选线(NCS),具体如下表所示:
表34.1.1.1.2 QSPI模式接口引脚
我们知道普通的SPI通信一般只有一根数据线(MOSI/MISO,发送/接收用),而QSPI则具有4根数据线,所以QSPI的速率至少是普通SPI的4倍,可以大大提高通信速率。如果使用双闪存模式,同时访问两个Quad-SPI Flash,速度可以再翻一番。我们开发板板载了一个Quad-SPI Flash,所以我们使用单闪存模式,双闪存模式就不具体介绍了,感兴趣请自行查看手册。
接下来,我们给大家简单介绍一下STM32H5 OSPI接口的的几个重要知识点。
34.1.1.2 OSPI命令序列
OSPI 通过命令与FLASH通信,每条命令包括:指令、地址、交替字节、空指令和数据这五个阶段,任一阶段均可通过配置OCTOSPI_CCR寄存器相关字段跳过,但至少要包含指令、地址、交替字节或数据阶段之一。
nCS 在每条指令开始前下降,在每条指令完成后再次上升。OSPI四线模式下的读命令时序,如图34.1.1.2.1所示:
图34.1.1.2.1 四线模式下OSPI读命令时序
从上图可以看出一次OSPI传输的5个阶段,接下来我们分别介绍。
①指令阶段
此阶段通过OCTOSPI_CCR[7:0]寄存器的INSTRUCTION字段指定一个8位指令发送到FLASH。注意,指令阶段,一般是通过IO0单线发送,但是也可以配置为双线/四线/八线发送指令,可以通过OCTOSPI_CCR[2:0]寄存器的IMODE[2:0]这三个位进行配置,如IMODE[2:0]=000,则表示无需发送指令。
②地址阶段
此阶段可以发送1~4字节地址给FLASH芯片,指示要操作的地址。地址字节长度由OCTOSPI_CCR[13:12]寄存器的ADSIZE[1:0]字段指定,00:8位地址、01:16位地址、10:24位地址、11:32位地址。在间接模式和轮询模式下,待发送的地址由OCTOSPI_AR寄存器指定。地址阶段同样可以以单线/双线/四线/八线模式发送,通过OCTOSPI_CCR[10:8]寄存器的ADMODE[2:0]这两个位进行配置,如ADMODE [2:0]=000,则表示无需发送地址。
③交替字节(复用字节)阶段
此阶段可以发送1~4字节数据给FLASH芯片,一般用于控制操作模式。待发送的交替字节数由OCTOSPI_CCR[21:20]寄存器的ABSIZE[1:0]位配置。待发送的数据由OCTOSPI_ABR寄存器中指定。交替字节同样可以以单线/双线/四线/八线模式发送,通过OCTOSPI_CCR[18:16]寄存器的ABMODE[2:0]这两个位配置,ABMODE[2:0]=000,则跳过交替字节阶段。
④空指令周期阶段
在空指令周期阶段,在给定的1~31个周期内不发送或接收任何数据,目的是当采用更高的时钟频率时,给FLASH芯片留出准备数据阶段的时间。这一阶段中给定的周期数由OCTOSPI_TCR[4:0]寄存器的DCYC[4:0]位配置。 若DCYC为零,则跳过空指令周期阶段,命令序列直接进入下一个阶段。
⑤数据阶段
此阶段可以从FLASH读取/写入任意字节数量的数据。在间接模式和自动轮询模式下,待发送/接收的字节数由OCTOSPI_DLR寄存器指定。在间接写入模式下,发送到FLASH的数据必须写入OCTOSPI_DR寄存器。在间接读取模式下,通过读取OCTOSPI_DR寄存器获得从 FLASH接收的数据。数据阶段同样可以以单线/双线/四线/八线模式发送,通过OCTOSPI_CCR[26:24]寄存器的DMODE [2:0]这两个位进行配置,如DMODE [2:0]=000,则表示无数据。
以上就是OSPI数据传输的5个阶段,其中交替字节阶段我们一般用不到,可以省略(通过设置ABMODE[2:0]=000)。
另外说明一下,OCTOSPI信号接口协议模式包括:单线SPI模式、双线SPI模式、四线SPI模式、八线SPI模式、SDR模式、DDR模式和双闪存模式。这些模式请大家自行参考官方手册。
34.1.1.3 OSPI三种功能模式
前面已经提到过OSPI的三种功能模式:间接模式、状态标志轮询模式和内存映射模式。下面对这三个功能模式分别简单介绍。
①间接模式
在间接模式下,通过写入OCTOSPI寄存器来触发命令,通过读写数据寄存器来传输数据。
当FMODE=00 (OCTOSPI_CR[29:28])时,OCTOSPI处于间接写入模式,在数据阶段,将数据写入数据寄存器(OCTOSPI_DR),即可写入数据到FLASH。
当FMODE=01时,OCTOSPI处于间接读取模式,在数据阶段,读取OCTOSPI_DR寄存器,即可读取FLASH里面的数据。
读/写字节数由数据长度寄存器(OCTOSPI_DLR)指定。当OCTOSPI_DLR=0xFFFFFFFF时,则数据长度视为未定义,OCTOSPI将持续传输数据,直到到达FLASH结尾(FLASH容量由 OCTOSPI_DCR1[20:16]寄存器的DEVSIZE[4:0]位定义)。如果不传输任何数据,则DMODE[2:0] (OCTOSPI_CCR[26:24])应设置为000。
当发送或接收的字节数(数据量)达到编程设定值时,如果TCIE=1,则TCF置1并产生中断。在数据量不确定的情况下,将根据DEVSIZE [4:0]定义的FLASH大小,在达到外部SPI FLASH的限制时,TCF置1。
在间接模式下有三种触发命令启动的方式,分别是:
(1)当不需要发送地址(ADMODE[2:0]==000)和数据(DMODE[2:0]==000)时,对INSTRUCTION[32:0](OCTOSPI _IR)执行写入操作。
(2)当需要发送地址(ADMODE[2:0]!=000),但不需要发送数据(DMODE[2:0]==000),对ADDRESS[31:0](OCTOSPI _AR)执行写入操作。
(3)当需要发送地址(ADMODE[2:0]!=000)和数据(DMODE[2:0]!=000)时,对DATA[31:0] (OCTOSPI _DR)执行写入操作。
如果命令启动,BUSY位(OCTOSPI _SR的第5位)将自动置1。
②状态标志轮询模式
将FMODE字段(OCTOSPI_CR[29:28]) 设置为10,使能状态标志轮询模式。在此模式下,将发送编程的帧并周期性检索数据。每帧中读取的最大数据量为4字节。如果OCTOSPI _DLR请求更多的数据,则忽略多余部分并仅读取4个字节。在 OCTOSPI_PIR 寄存器指定周期性。
在检索到状态数据后,可在内部进行处理,以达到以下目的:
(1)将状态匹配标志位置 1,如果使能,还将产生中断。
(2)自动停止周期性检索状态字节。
接收到的值可通过存储于OCTOSPI_PSMKR寄存器中的值来进行屏蔽,并与存储在 OCTOSPI_PSMAR寄存器中的值进行或运算或与运算。
若是存在匹配,则状态匹配标志置1,并且在使能了中断的情况下还将产生中断;如果AMPS 位置1,则OCTOSPI自动停止。
在任何情况下,最新的检索值都在OCTOSPI_DR中可用。
③内存映射模式
在配置为内存映射模式时,外部FLASH器件被视作内部存储器,只是存在访问延迟。在该模式下,仅允许对外部 FLASH 执行读取操作。将OCTOSPI_CR寄存器中的FMODE设置为11可进入内存映射模式。当主器件访问存储器映射空间时,将发送已编程的指令和帧。另外数据长度寄存器(OCTOSPI_DLR)在内存映射模式中无意义。
OCTOSPI外设若没有正确配置并使能,禁止访问QUADSPI Flash的存储区域。即使FLASH容量更大,寻址空间也无法超过256MB。如果访问的地址超出FSIZE定义的范围但仍在256MB 范围内,则生成总线错误。此错误的影响具体取决于尝试进行访问的总线主器件:
(1)如果为Cortex® CPU,则会在使能总线故障时发生总线故障异常,在禁止总线故障时发生硬性故障(hard fault) 异常。
(2)如果为 DMA,则生成 DMA传输错误,并自动禁用相应的 DMA 通道。
内存映射模式支持字节、半字和字访问类型,并支持芯片内执(XIP)操作,OCTOSPI接受下一个微控制器访问并提前加载后面地址中的字节。如果之后访问的是连续地址,由于值已经预取,访问将更快完成。
默认情况下,即便在很长时间内不访问FLASH,OCTOSPI也不会停止预取操作,之前的读取操作将保持激活状态并且 nCS 保持低电平。由于 nCS保持低电平时,FLASH 功耗增加,应用程序可能会激活超时计数器(TCEN = 1, OCTOSPI_CR 的位 3)。从而在 FIFO中写满预取的数据后,若在 TIMEOUT[15:0] (OCTOSPI_LPTR) 个周期的时长内没有访问,则释放 nCS。BUSY在第一个存储器映射访问发生时变为高电平。由于进行预取操作,BUSY在发生超时、中止或外设禁止前不会下降。
34.1.1.4 OSPI FLASH配置
SPI FLASH芯片的相关参数通过器件配置寄存器 (OCTOSPI_DCR1) 来进行设置。寄存器OCTOSPI_DCR1[20:16]的DEVSIZE[4:0]这5个位,用于指定外部存储器的大小,计算公式为:
Fcap=2^[DEVSIZE+1] DEVSIZE+1是对Flash寻址所需的地址位数。Fcap表示FLASH的容量,单位为字节,在间接模式下,最高支持4GB(使用32位进行寻址)容量的FLASH芯片。但是在内存映射模式下的可寻址空间限制为256MB。
OSPI连续执行两条命令时,它在两条命令之间将片选信号 (nCS) 置为高电平默认仅一个 CLK周期。某些FLASH需要命令之间的时间更长,可以通过寄存器OCTOSPI_DCR1[13:8]的CSHT[5:0](选高电平时间)这5个位设置高电平时长:0~63表示1~64个时钟周期。
时钟模式,用于指定在nCS为高电平时,CLK的时钟极性。通过寄存器OCTOSPI_DCR1的CKMODE位指定:当CKMODE=0时,CLK在nCS为高电平期间保持低电平,称之为模式0;当CKMODE=1时,CLK在nCS为高电平期间保持高电平,称之为模式3。
34.1.1.5 OCTOSPI寄存器
OCTOSPI控制寄存器(OCTOSPI_CR)
OCTOSPI控制寄存器描述如图34.1.1.5.1所示:
图34.1.1.5.1 OCTOSPI_CR寄存器
FMODE[1:0],用于设置功能模式:00,间接写入模式;01,间接读取模式;10,自动轮询模式;11,内存映射模式;我们使用间接模式,所以这两位根据需要,可设置为00或01。
FTHRES[4:0],用于设置FIFO阈值,范围为0~31,表示FIFO的阈值为1~32字节。
MSEL位,用于选择FLASH,我们的25Q128连接在STM32H5的OSPI IO[3:0]上面,所以设置此位为0即可。
DMM位,用于设置双闪存模式,我们用的是单闪存模式,所以设置此位为0即可。
ABORT位,用于终止QSPI的当前传输,设置为1即可终止当前传输,在读写FLASH数据的时候,可能会用到。
EN位,用于控制OSPI的使能,我们需要用到OSPI接口,所以必须设置此位为1。
OCTOSPI器件配置寄存器1(OCTOSPI_ DCR1)
OCTOSPI器件配置寄存器1描述如图34.1.1.5.2所示:
图34.1.1.5.2 OCTOSPI_ DCR1寄存器
该寄存器可以设置FLASH芯片的容量(FSIZE)、片选高电平时间(CSHT)和时钟模式(CKMODE)等,这些位的设置说明见前面的34.1.1.4小节有详细讲解。
OCTOSPI器件配置寄存器2(OCTOSPI_ DCR2)
OCTOSPI器件配置寄存器2描述如图34.1.1.5.3所示:
图34.1.1.5.3 OCTOSPI_ DCR2寄存器
PRESCALER[7:0]用于设置OSPI内核时钟预分频器:0~255,表示1~256分频。我们使用的W25Q128最大支持104Mhz的时钟,这里我们设置PRESCALER=1,即2分频,得到OSPI时钟为110MHz。
OCTOSPI状态寄存器(OCTOSPI_ SR)
OCTOSPI状态寄存器描述如图34.1.1.5.4所示:
图34.1.1.5.4 OCTOSPI_ SR寄存器
BUSY位,指示操作是否忙。当该位为1时,表示OSPI正在执行操作。在操作完成或者FIFO为空的时候,该位自动清零。
FTF位,表示FIFO是否到达阈值。在间接模式下,若达到FIFO阈值,或从FLASH读取完成后,FIFO中留有数据时,该位置1。只要阈值条件不再为“真”,该位就自动清零。
TCF位,表示传输是否完成。在间接模式下,当传输的数据数量达到编程设定值,或在任何模式下传输中止时,该位置1。向OCTOSPI_FCR寄存器的CTCF位写1,可以清零此位。
OCTOSPI标志清零寄存器(OCTOSPI_ FCR)
OCTOSPI标志清零寄存器描述如图34.1.1.5.5所示:
图34.1.1.5.5 OCTOSPI_ FCR寄存器
该寄存器,我们一般只用到CTCF位,用于清除OSPI的传输完成标志。
OCTOSPI通信配置寄存器(OCTOSPI_ CCR)
OCTOSPI通信配置寄存器描述如图34.1.1.5.6所示:
图34.1.1.5.6 OCTOSPI_ CCR寄存器
DDTR位,用于设置双倍率模式(DTR),我们没用到双倍率模式,所以设置此位为0。
DMODE[2:0],用于设置数据模式:000,无数据;001,单线传输数据;010,双线传输数据;011,四线传输数据;100,八线传输数据;我们设置为000或011。
ABMODE[2:0],用于设置交替字节模式,我们一般设置为000,表示无交替字节。
ADMODE[2:0],用于设置地址模式:000,无地址;001,单线传输地址;010,双线传输地址;011,四线传输地址;100,八线传输地址;我们设置为000或011。
IMODE[2:0],用于设置指令模式:000,无指令;001,单线传输指令;010,双线传输指令;011,四线传输指令;100,八线传输指令;我们设置为000或011。
注意,以上这些位的配置,都必须在OCTOSPI_SR寄存器的BUSY位为0时才可配置。
OCTOSPI时序配置寄存器(OCTOSPI_ TCR)
OCTOSPI时序配置寄存器描述如图34.1.1.5.7所示:
图34.1.1.5.7 OCTOSPI_ TCR寄存器
SSHIFT位,用于设置采样移位,默认情况下, QSPI 接口在 FLASH 驱动数据后过半个 CLK周期开始采集数据。使用该位,可考虑外部信号延迟,推迟数据采集。我们一般设置此位为 1,移位半个周期采集,确保数据稳定。
DCYC[4:0],用于设置空指令周期数,可以控制空指令阶段的持续时间,设置范围为:0~31。设置为0,则表示没有空指令周期。
接下来,我们介绍一下OSPI数据长度寄存器:OCTOSPI_DLR。该寄存器是一个32位寄存器,可以设置的数据长度范围为:0~0xFFFFFFFF,当OCTOSPI _DLR!=0XFFFFFFFF时,表示传输的字节长度(+1);当OCTOSPI_DLR==0XFFFFFFFF时,表示不限传输长度,直到到达由DEVSIZE定义的FLASH结尾。
接下来,我们介绍一下OSPI地址寄存器:OCTOSPI_AR。该寄存器为一个32位寄存器,用于指定发送到FLASH的地址。
接下来,我们介绍一下OSPI数据寄存器:OCTOSPI_DR。该寄存器为一个32位寄存器,用于指定与外部SPI FLASH设备交换的数据。该寄存器支持字、半字和字节访问。
在间接写入模式下,写入该寄存器的数据在数据阶段发送到FLASH,在此之前则存储于FIFO,如果 FIFO 满了,则暂停写入,直到 FIFO 具有足够的空间接受要写入的数据才继续。
在间接模式下,读取该寄存器可获得(通过FIFO)已从FLASH接收的数据。如果FIFO所含字节数比读取操作要求的字节数少,且BUSY=1,则暂停读取,直到足够的数据出现或传输完成才继续。
最后,我们介绍一下OSPI命令寄存器:OCTOSPI_IR。该寄存器为一个32位寄存器,用于设置将要发送给FLASH的指令。
34.1.2 NOR FLASH芯片简介
NOR FLASH芯片有很多种芯片型号,在我们的norflash.h头文件中有定义芯片ID的宏定义,对应的就是不同型号的NOR FLASH芯片,比如有:W25Q128、BY25Q128、NM25Q128等,它们是来自不同的厂商的同种规格的NOR FLASH芯片,内存空间都是128M位,即16M字节。它们的很多参数、操作都是一样的,所以我们的实验都是兼容它们的。由于这么多的芯片我们就不一一进行介绍了,就拿其中一款型号进行介绍即可,其它的型号都是类似的。
W25Q128是一款大容量SPI FLASH产品,其容量为16MB。它将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),本实验我们将使用STM32H5的OSPI四线接口模式来实现对W25Q128的驱动。
接下来,我们介绍一下本实验驱动W25Q128需要用到的一些指令,如表34.1.2.1所示:
1,在QPI模式下dummy时钟的个数,由读参数控制位P[5:4]位控制。
2,传输的数据量,只要不停的给时钟就可以持续传输,对W25X_PageProgram指令,则单次传输最多不超过256字节,否则将覆盖之前写入的数据。
表34.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,如表34.1.2.2所示:
表34.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],这两个位的描述如表34.1.2.3所示:
表34.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文件中。
34.2 硬件设计
1. 例程功能
通过KEY1按键来控制norflash的写入,通过按键KEY0来控制norflash的读取。并在LCD模块上面显示相关信息。我们还可以通过USMART控制读取norflash的ID、擦除某个扇区或整片擦除。LED0闪烁用于提示程序正在运行。
2. 硬件资源
1)LED灯
LED0 - PB4
2)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)
3)正点原子2.8/3.5/4.3/7寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
4)独立按键 :KEY0 - PA1、KEY1 - PA15
5)OSPI(PB2/PB6/PD11/PD12/PD13/PE2)
6)norflash(QSPI FLASH芯片,连接在OSPI接口上)
3. 原理图
板载的QSPI FLASH芯片与STM32H562的连接关系,如下图所示:
图34.2.1 QSPI FLASH芯片与STM32H562连接示意图
本实验支持多种型号的QSPI FLASH芯片,比如:BY25Q128/NM25Q128/W25Q128等等,具体请看norflash.h文件的宏定义,程序上只需要稍微修改一下,后面讲解程序的时候会说到。
34.3 程序设计
34.3.1 OSPI的HAL库驱动
OSPI在HAL库中的驱动代码在stm32h5xx_hal_xspi.c文件(及其头文件)中。
1. HAL_XSPI_Init函数
OSPI的初始化函数,其声明如下:
- HAL_StatusTypeDef HAL_XSPI_Init(XSPI_HandleTypeDef *hxspi);
复制代码 函数描述:
用于初始化OSPI。
函数形参:
形参1是XSPI_HandleTypeDef结构体类型指针变量,其定义如下:
- typedef struct
- {
- XSPI_TypeDef *Instance; /* XSPI 寄存器基地址 */
- XSPI_InitTypeDef Init; /* XSPI 初始化参数 */
- uint8_t *pBuffPtr; /* 要发送数据的地址 */
- __IO uint32_t XferSize; /* 要发送数据的大小 */
- __IO uint32_t XferCount; /* 剩余要发送数据的个数 */
- DMA_HandleTypeDef *hdmatx; /* 发送DMA通道句柄 */
- DMA_HandleTypeDef *hdmarx; /* 接收DMA通道句柄 */
- __IO uint32_t State; /* XSPI通信状态 */
- __IO uint32_t ErrorCode; /* 错误代码 */
- uint32_t Timeout; /* 配置XSPI内存访问超时时间 */
- }QSPI_HandleTypeDef;
复制代码 1) Instance:用于设置OSPI寄存器基地址,如:设置为OCTOSPI1。
2) Init:用于设置OSPI的相关参数,XSPI_InitTypeDef结构体下面再进行详细讲解。
3) pTxBuffPtr、TxXferSize和TxXferCount:分别用于设置 OSPI 发送缓冲指针、发送数据量和发送剩余数据量。
4) hdmatx和hdmarx:分别是发送和接收DMA通道句柄指针。
5) State:用于指示OSPI通信状态。
6) ErrorCode:通过该参数,用户可以了解到OSPI通讯过程中通信失败的原因。
7) Timeout:用于设置超时时间。OSPI访问时间一旦超出 Timeout这个变量值,那么ErrorCode成员变量就会被赋值为HAL_XSPI_ERROR_TIMEOUT,表示操作超时。
下面来了解一下XSPI_InitTypeDef结构体的内容,其定义如下:
- typedef struct
- {
- uint32_t FifoThresholdByte; /* 指定FIFO阈值级别 */
- uint32_t MemoryMode; /* 指定内存模式 */
- uint32_t MemoryType; /* 指定连接到XSPI的外部设备类型 */
- uint32_t MemorySize; /* 指定连接到XSPI的外部设备的大小 */
- uint32_t ChipSelectHighTimeCycle; /* 指定片选高电平时间 */
- uint32_t FreeRunningClock; /* 指定启用或不启用自由运行时钟 */
- uint32_t ClockMode; /* 指定时钟模式 */
- uint32_t WrapSize; /* 表示配置内存的封装大小 */
- uint32_t ClockPrescaler; /* 指定预分频系数 */
- uint32_t SampleShifting; /* 设置采样移位 */
- uint32_t DelayHoldQuarterCycle; /* 设置允许保持1/4周期的数据 */
- uint32_t ChipSelectBoundary; /* 启用事务边界特性和定义释放芯片选择的字节边界 */
- uint32_t DelayBlockBypass; /* 使能延迟块旁路,因此采样不受影响通过延迟块 */
- uint32_t Refresh; /* 启用了刷新率特性*/
- } XSPI_InitTypeDef;
复制代码 函数返回值:
HAL_StatusTypeDef枚举类型的值。
注意事项:
OSPI的MSP初始化函数HAL_XSPI_MspInit,该函数声明如下:
- void HAL_XSPI_MspInit(XSPI_HandleTypeDef *ospi);
复制代码
2. HAL_XSPI_Command函数
OSPI设置命令配置函数,其声明如下:
- HAL_StatusTypeDef HAL_XSPI_Command(XSPI_HandleTypeDef *hxspi,
- XSPI_RegularCmdTypeDef *const pCmd, uint32_t Timeout);
复制代码 函数描述:
该函数用来配置OSPI命令。
函数形参:
形参1是XSPI_HandleTypeDef结构体类型指针变量。
形参2是XSPI_RegularCmdTypeDef结构体类型指针变量,其定义如下:
- typedef struct
- {
- uint32_t OperationType; /* 指示XSPI操作模式 */
- uint32_t IOSelect; /* 指示用于与外部flash交换数据的IO个数 */
- uint32_t Instruction; /* 要发送到设备的指令 */
- uint32_t InstructionMode; /* 指示指令的模式 */
- uint32_t InstructionWidth; /* 表示指令的宽度 */
- uint32_t InstructionDTRMode; /* 启用或不启用指令阶段的DTR模式 */
- uint32_t Address; /* 要发送到设备的地址 */
- uint32_t AddressMode; /* 表示地址模式 */
- uint32_t AddressWidth; /* 表示地址宽度 */
- uint32_t AddressDTRMode; /* 启用或不启用地址阶段的DTR模式 */
- uint32_t AlternateBytes; /* 要发送到设备的交替字节 */
- uint32_t AlternateBytesMode; /* 表示交替字节的模式 */
- uint32_t AlternateBytesWidth; /* 表示交替字节的宽度 */
- uint32_t AlternateBytesDTRMode; /* 启用或不启用交替字节阶段的DTR模式 */
- uint32_t DataMode; /* 表示数据模式 */
- uint32_t DataLength; /* 表示要传输的数据量 */
- uint32_t DataDTRMode; /* 启用或不启用数据阶段的DTR模式 */
- uint32_t DummyCycles; /* 表示在数据阶段之前插入的虚拟周期的数量 */
- uint32_t DQSMode; /* 启用或不启用数据频闪管理 */
- uint32_t SIOOMode; /* 启用或不启用SIOO模式 */
- } XSPI_RegularCmdTypeDef;
复制代码 形参3用于设置超时时间。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
3. HAL_XSPI_Receive函数
OSPI接收数据函数,其声明如下:
- HAL_StatusTypeDef HAL_XSPI_Receive(XSPI_HandleTypeDef *hxspi,
- uint8_t *const pData, uint32_t Timeout);
复制代码 函数描述:
该函数用来接收数据。
函数形参:
形参1是XSPI_HandleTypeDef结构体类型指针变量。
形参2是uint8_t类型指针变量,存放接收数据缓冲区指针。
形参3设置操作超时时间。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
4. HAL_XSPI_Transmit函数
OSPI发送数据函数,其声明如下:
- HAL_StatusTypeDef HAL_XSPI_Transmit(XSPI_HandleTypeDef *hxspi,
- uint8_t *const pData, uint32_t Timeout);
复制代码 函数描述:
该函数用来发送数据。
函数形参:
形参1是XSPI_HandleTypeDef结构体类型指针变量。
形参2是uint8_t类型指针变量,存放发送数据缓冲区指针。
形参3设置操作超时时间。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
OSPI初始化配置步骤
1)开启OSPI接口和相关IO的时钟,设置IO口的复用功能。
要使用OSPI,要先开启其时钟,然后根据我们要使用的OSPI IO端口,开启对应的GPIO时钟,并初始化相关IO口的复用功能(选择OSPI复用功能)。
OSPI1时钟使能方法为:
- __HAL_RCC_OSPI1_CLK_ENABLE (); /* 使能OSPI1时钟 */
复制代码 和其它外设处理方法一样,HAL库提供了OSPI的初始化回调函数HAL_XSPI_MspInit,用于存放时钟使能和IO口初始化等相关代码。
2)设置OSPI相关参数。
第二步主要设置两个寄存器:OCTOSPI_CR、OCTOSPI_DCR1和OCTOSPI_DCR2,控制OSPI的时钟、片选参数、FLASH容量和时钟模式等参数,设定SPI FLASH的工作条件。最后,使能OSPI,完成对OSPI的初始化。HAL库中设置OSPI相关参数函数为HAL_XSPI_Init,该函数声明为:
HAL_StatusTypeDef HAL_XSPI_Init(XSPI_HandleTypeDef *hqspi);
对于HAL_XSPI_Init函数使用范例请参考程序源码。
OSPI发送命令步骤
1)等待OSPI空闲。
在OSPI发送命令前,必须先等待OSPI空闲,通过判断OCTOSPI _SR寄存器的BUSY位为0,来确定。
2)设置命令参数。
此部分主要是通过通信配置寄存器(OCTOSPI _CCR)设置,将OSPI配置为:每次都发送指令、间接写模式,根据具体需要设置:指令、地址、空周期和数据等的传输位宽等信息。如果需要发送地址,则配置地址寄存器(OCTOSPI_AR)。
在配置完成以后,即可启动发送。如果不需要传输数据,则需要等待命令发送完成(等待OCTOSPI_SR寄存器的TCF位为1)。
在HAL库中上述两个步骤是通过函数HAL_XSPI_Command来实现,该函数声明为:
- HAL_StatusTypeDef HAL_XSPI_Command(XSPI_HandleTypeDef *hxspi,
- XSPI_RegularCmdTypeDef *const pCmd, uint32_t Timeout);
复制代码 OSPI读数据步骤
1)设置数据传输长度。
通过设置数据长度寄存器(OCTOSPI_DLR),配置需要传输的字节数。
2)设置OSPI工作模式并设置地址。
因为要读取数据,所以,设置OCTOSPI_CCR寄存器的FMODE[1:0]位为01,工作在间接读取模式。然后,通过地址寄存器(OCTOSPI_AR),设置我们将要读取的数据的首地址。
3)读取数据。
在发送完地址以后,就可以读取数据了,不过要等待数据准备好,通过判断OCTOSPI_SR寄存器的FTF和TCF位,当这两个位任意一个位为1的时候,我们就可以读取OCTOSPI_DR寄存器来获取从FLASH读到的数据。
最后,在所有数据接收完成以后,终止传输(ABORT),清除传输完成标志位(TCF)。
HAL库中,读取数据是通过函数HAL_XSPI_Receive来实现的,该函数声明为:
- HAL_StatusTypeDef HAL_XSPI_Receive(XSPI_HandleTypeDef *hxspi,
- uint8_t *const pData, uint32_t Timeout);
复制代码 在调用该函数读取数据之前,我们会先调用上个步骤讲解的函数HAL_XSPI_Command来指定读取数据的存放空间。
OSPI写数据步骤
1)设置数据传输长度。
通过设置数据长度寄存器(OCTOSPI_DLR),配置需要传输的字节数。
2)设置OSPI工作模式并设置地址。
因为要读取数据,所以,设置OCTOSPI_CCR寄存器的FMODE[1:0]位为00,工作在间接写入模式。然后,通过地址寄存器(OCTOSPI_AR),设置我们将要写入的数据的首地址。
3)写入数据。
在发送完地址以后,就可以写入数据了,不过要等待FIFO不满,通过判断OCTOSPI_SR寄存器的FTF位,当这个位为1的时候,表示FIFO可以写入数据,此时往OCTOSPI_DR写入需要发送的数据,就可以实现写入数据到FLASH。
最后,在所有数据写入完成以后,终止传输(ABORT),清除传输完成标志位(TCF)。
在HAL库中,OSPI发送数据是通过函数HAL_XSPI_Transmit来实现的,该函数声明为:
- HAL_StatusTypeDef HAL_XSPI_Transmit(XSPI_HandleTypeDef *hxspi,
- uint8_t *const pData, uint32_t Timeout);
复制代码 同理,在调用该函数发送数据之前,我们会先调用HAL_XSPI_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时钟频率)。
34.3.2 程序流程图
图34.3.2.1 OSPI实验程序流程图
34.3.3 程序解析
1. QSPI驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。OSPI驱动源码包括两个文件:ospi.c和ospi.h。
ospi.h头文件对OSPI相关引脚做了宏定义,该宏定义如下:
- /* OSPI 相关 引脚 定义 */
- #define OSPI1_CLK_GPIO_PORT GPIOB
- #define OSPI1_CLK_GPIO_PIN GPIO_PIN_2
- #define OSPI1_CLK_GPIO_AF GPIO_AF9_OCTOSPI1
- #define OSPI1_CLK_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE; }while(0)
- #define OSPI1_NCS_GPIO_PORT GPIOB
- #define OSPI1_NCS_GPIO_PIN GPIO_PIN_6
- #define OSPI1_NCS_GPIO_AF GPIO_AF10_OCTOSPI1
- #define OSPI1_NCS_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE; }while(0)
- #define OSPI1_IO0_GPIO_PORT GPIOD
- #define OSPI1_IO0_GPIO_PIN GPIO_PIN_11
- #define OSPI1_IO0_GPIO_AF GPIO_AF9_OCTOSPI1
- #define OSPI1_IO0_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOD_CLK_ENABLE; }while(0)
- #define OSPI1_IO1_GPIO_PORT GPIOD
- #define OSPI1_IO1_GPIO_PIN GPIO_PIN_12
- #define OSPI1_IO1_GPIO_AF GPIO_AF9_OCTOSPI1
- #define OSPI1_IO1_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOD_CLK_ENABLE; }while(0)
- #define OSPI1_IO2_GPIO_PORT GPIOE
- #define OSPI1_IO2_GPIO_PIN GPIO_PIN_2
- #define OSPI1_IO2_GPIO_AF GPIO_AF9_OCTOSPI1
- #define OSPI1_IO2_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOE_CLK_ENABLE; }while(0)
- #define OSPI1_IO3_GPIO_PORT GPIOD
- #define OSPI1_IO3_GPIO_PIN GPIO_PIN_13
- #define OSPI1_IO3_GPIO_AF GPIO_AF9_OCTOSPI1
- #define OSPI1_IO3_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOD_CLK_ENABLE; }while(0)
复制代码 注意这6个GPIO都是用到复用功能,对应引脚的复用功能情况请看《STM32H562VIT6.pdf》数据手册78页之后的端口复用功能表格。
下面我们开始介绍ospi.c的程序,首先是OSPI接口初始化函数,其定义如下:
- /**
- * @brief 初始化OSPI接口
- * [url=home.php?mod=space&uid=271674]@param[/url] 无
- * @retval 0, 成功; 1, 失败.
- */
- uint8_t ospi_init(void)
- {
- g_ospi_handle.Instance = OCTOSPI1; /* OCTOSPI1 */
- g_ospi_handle.Init.FifoThresholdByte = 4; /* FIFO阈值为4个字节,范围:1~32 */
- g_ospi_handle.Init.MemoryMode = HAL_XSPI_SINGLE_MEM; /* 禁用双内存模式 */
- g_ospi_handle.Init.MemoryType = HAL_XSPI_MEMTYPE_APMEM; /* 标准模式 */
- g_ospi_handle.Init.MemorySize = HAL_XSPI_SIZE_128MB;/* 所连设备内存大小为16MB*/
- /* 片选高电平时间为3个时钟(9.1*3=27.3ns),即手册里面的tSHSL参数 */
- g_ospi_handle.Init.ChipSelectHighTimeCycle = 3;
- /* 关闭空闲运行时钟 */
- g_ospi_handle.Init.FreeRunningClock = HAL_XSPI_FREERUNCLK_DISABLE;
- /* 当NCS高时(空闲),CLK为高电平 */
- g_ospi_handle.Init.ClockMode = HAL_XSPI_CLOCK_MODE_3;
- /* 内存不支持包装读取 */
- g_ospi_handle.Init.WrapSize = HAL_XSPI_WRAP_NOT_SUPPORTED;
- /* 设置OSPI时钟分频系数为1+1,即220M/2=110Mhz,9.1ns */
- g_ospi_handle.Init.ClockPrescaler = 1;
- /* 采样移位半个周期(DDR模式下,必须设置为0) */
- g_ospi_handle.Init.SampleShifting = HAL_XSPI_SAMPLE_SHIFT_HALFCYCLE;
- /* 不延迟等待 */
- g_ospi_handle.Init.DelayHoldQuarterCycle = HAL_XSPI_DHQC_DISABLE;
- /* 禁用边界 */
- g_ospi_handle.Init.ChipSelectBoundary = HAL_XSPI_BONDARYOF_NONE;
- /* 绕过延迟块,即DLYBYP位=1 */
- g_ospi_handle.Init.DelayBlockBypass = HAL_XSPI_DELAY_BLOCK_BYPASS;
- g_ospi_handle.Init.Refresh = 0; /* 不启用刷新率特性 */
- if (HAL_XSPI_Init(&g_ospi_handle) == HAL_OK)
- {
- return 0; /* OSPI初始化成功 */
- }
- else
- {
- return 1; /* OSPI初始化失败 */
- }
- }
复制代码 我们用HAL_XSPI_MspInit函数来编写OSPI时钟和IO配置等代码,其定义如下:
- /**
- * @brief OSPI底层驱动,引脚配置,时钟使能
- * @param ospi:OSPI句柄
- * [url=home.php?mod=space&uid=60778]@note[/url] 此函数会被HAL_XSPI_Init()调用
- * @retval 无
- */
- void HAL_XSPI_MspInit(XSPI_HandleTypeDef *ospi)
- {
- GPIO_InitTypeDef gpio_init_struct = {0};
- RCC_PeriphCLKInitTypeDef rcc_periph_clk_init = {0};
- if (ospi->Instance == OCTOSPI1)
- {
- __HAL_RCC_OSPI1_CLK_ENABLE(); /* 使能OSPI1时钟 */
- __HAL_RCC_GPIOB_CLK_ENABLE(); /* GPIOB时钟使能 */
- __HAL_RCC_GPIOD_CLK_ENABLE(); /* GPIOD时钟使能 */
- __HAL_RCC_GPIOE_CLK_ENABLE(); /* GPIOE时钟使能 */
- /*
- * 设置OSPI时钟源
- * 配置PLL2的R分频输出, 为220Mhz
- * 配置OSPI时钟源是PLL2R
- */
- rcc_periph_clk_init.PeriphClockSelection = RCC_PERIPHCLK_OSPI;
- rcc_periph_clk_init.PLL2.PLL2M = 4;
- rcc_periph_clk_init.PLL2.PLL2N = 220;
- rcc_periph_clk_init.PLL2.PLL2P = 2;
- rcc_periph_clk_init.PLL2.PLL2Q = 2;
- rcc_periph_clk_init.PLL2.PLL2R = 2;
- rcc_periph_clk_init.PLL2.PLL2RGE = RCC_PLL2VCIRANGE_0;
- rcc_periph_clk_init.PLL2.PLL2VCOSEL = RCC_PLL2VCOWIDE;
- rcc_periph_clk_init.PLL2.PLL2FRACN = 0;
- rcc_periph_clk_init.OspiClockSelection = RCC_OSPICLKSOURCE_PLL2R;
- HAL_RCCEx_PeriphCLKConfig(&rcc_periph_clk_init);
- gpio_init_struct.Pin = OSPI1_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 = OSPI1_NCS_GPIO_AF;
- HAL_GPIO_Init(OSPI1_NCS_GPIO_PORT, &gpio_init_struct);
- gpio_init_struct.Pin = OSPI1_CLK_GPIO_PIN;
- gpio_init_struct.Alternate = OSPI1_CLK_GPIO_AF;
- HAL_GPIO_Init(OSPI1_CLK_GPIO_PORT, &gpio_init_struct);
-
- gpio_init_struct.Pin = OSPI1_IO0_GPIO_PIN;
- HAL_GPIO_Init(OSPI1_IO0_GPIO_PORT, &gpio_init_struct);
-
- gpio_init_struct.Pin = OSPI1_IO1_GPIO_PIN;
- HAL_GPIO_Init(OSPI1_IO1_GPIO_PORT, &gpio_init_struct);
-
- gpio_init_struct.Pin = OSPI1_IO2_GPIO_PIN;
- HAL_GPIO_Init(OSPI1_IO2_GPIO_PORT, &gpio_init_struct);
-
- gpio_init_struct.Pin = OSPI1_IO3_GPIO_PIN;
- HAL_GPIO_Init(OSPI1_IO3_GPIO_PORT, &gpio_init_struct);
- }
- }
复制代码 这里初始化的6个引脚全部都要配置为复用功能模式,以及使能OSPI和相应IO时钟。
接下来介绍OSPI发送命令函数,其定义如下:
- /**
- * @brief OSPI发送命令
- * @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 ospi_send_cmd(uint8_t cmd, uint32_t addr, uint8_t mode, uint8_t dmcycle)
- {
- XSPI_RegularCmdTypeDef ospi_regular_cmd = {0};
- ospi_regular_cmd.OperationType = HAL_XSPI_OPTYPE_COMMON_CFG; /* 通用配置 */
- ospi_regular_cmd.IOSelect = HAL_XSPI_SELECT_IO_3_0; /* 通过IO[3:0]交换数据 */
- /* 指令相关配置 */
- ospi_regular_cmd.Instruction = cmd; /* 发送到设备的指令 */
- if(((mode >> 0) & 0x03) == 0) /* 指令模式 */
- ospi_regular_cmd.InstructionMode = HAL_XSPI_INSTRUCTION_NONE; /* 无指令 */
- else if(((mode >> 0) & 0x03) == 1)
- ospi_regular_cmd.InstructionMode = HAL_XSPI_INSTRUCTION_1_LINE;/* 单线指令 */
- else if(((mode >> 0) & 0x03) == 2)
- ospi_regular_cmd.InstructionMode = HAL_XSPI_INSTRUCTION_2_LINES;/* 双线指令 */
- else if(((mode >> 0) & 0x03) == 3)
- ospi_regular_cmd.InstructionMode = HAL_XSPI_INSTRUCTION_4_LINES;/* 四线指令 */
- ospi_regular_cmd.InstructionWidth = HAL_XSPI_INSTRUCTION_8_BITS;/* 8线指令 */
- /* 不启用指令阶段的DTR模式 */
- ospi_regular_cmd.InstructionDTRMode = HAL_XSPI_INSTRUCTION_DTR_DISABLE;
- /* 地址相关配置 */
- ospi_regular_cmd.Address = addr; /* 发送到设备的地址 */
- if(((mode >> 2) & 0x03) == 0) /* 地址模式 */
- ospi_regular_cmd.AddressMode = HAL_XSPI_ADDRESS_NONE; /* 无地址 */
- else if(((mode >> 2) & 0x03) == 1)
- ospi_regular_cmd.AddressMode = HAL_XSPI_ADDRESS_1_LINE; /* 单线传输地址 */
- else if(((mode >> 2) & 0x03) == 2)
- ospi_regular_cmd.AddressMode = HAL_XSPI_ADDRESS_2_LINES; /* 双线传输地址 */
- else if(((mode >> 2) & 0x03) == 3)
- ospi_regular_cmd.AddressMode = HAL_XSPI_ADDRESS_4_LINES; /* 四线传输地址 */
- if(((mode >> 4)&0x03) == 0) /* 地址宽度 */
- ospi_regular_cmd.AddressWidth = HAL_XSPI_ADDRESS_8_BITS; /* 8位地址宽度 */
- else if(((mode >> 4) & 0x03) == 1)
- ospi_regular_cmd.AddressWidth = HAL_XSPI_ADDRESS_16_BITS; /* 16位地址宽度 */
- else if(((mode >> 4) & 0x03) == 2)
- ospi_regular_cmd.AddressWidth = HAL_XSPI_ADDRESS_24_BITS; /* 24位地址宽度 */
- else if(((mode >> 4) & 0x03) == 3)
- ospi_regular_cmd.AddressWidth = HAL_XSPI_ADDRESS_32_BITS; /* 32位地址宽度 */
- /* 不启用地址阶段的DTR模式 */
- ospi_regular_cmd.AddressDTRMode = HAL_XSPI_ADDRESS_DTR_DISABLE;
- /* 交替字节相关配置,没用到 */
- ospi_regular_cmd.AlternateBytes = 0; /* 发送到设备的交替字节 */
- ospi_regular_cmd.AlternateBytesMode = HAL_XSPI_ALT_BYTES_NONE;/* 无交替字节 */
- /* 交替字节宽度 */
- ospi_regular_cmd.AlternateBytesWidth = HAL_XSPI_ALT_BYTES_16_BITS;
- /* DTR模式在交替字节阶段被禁用 */
- ospi_regular_cmd.AlternateBytesDTRMode = HAL_XSPI_ALT_BYTES_DTR_DISABLE;
- /* 数据相关配置 */
- if(((mode >> 6) & 0x03) == 0) /* 数据模式 */
- ospi_regular_cmd.DataMode=HAL_XSPI_DATA_NONE; /* 无数据 */
- else if(((mode >> 6) & 0x03) == 1)
- ospi_regular_cmd.DataMode = HAL_XSPI_DATA_1_LINE; /* 单线传输数据 */
- else if(((mode >> 6) & 0x03) == 2)
- ospi_regular_cmd.DataMode = HAL_XSPI_DATA_2_LINES; /* 双线传输数据 */
- else if(((mode >> 6) & 0x03) == 3)
- ospi_regular_cmd.DataMode = HAL_XSPI_DATA_4_LINES; /* 四线传输数据 */
- ospi_regular_cmd.DataLength = 1; /* 传输的数据量(暂时写1) */
- /* 不启用数据阶段的DTR模式 */
- ospi_regular_cmd.DataDTRMode = HAL_XSPI_DATA_DTR_DISABLE;
- ospi_regular_cmd.DummyCycles = dmcycle; /* 设置空指令周期数 */
- ospi_regular_cmd.DQSMode = HAL_XSPI_DQS_DISABLE; /* 不启用数据频闪管理 */
- ospi_regular_cmd.SIOOMode = HAL_XSPI_SIOO_INST_EVERY_CMD;/* 每次都发送指令 */
- HAL_XSPI_Command(&g_ospi_handle, &ospi_regular_cmd,
- HAL_XSPI_TIMEOUT_DEFAULT_VALUE);
- }
复制代码 该函数主要就是配置XSPI_RegularCmdTypeDef结构体的参数,是一个重要的基础函数。
接下来介绍的是OSPI接收函数,其定义如下:
- /**
- * @brief OSPI接收指定长度的数据
- * @param buf : 接收数据缓冲区首地址
- * @param datalen : 要传输的数据长度
- * @retval 0, 成功; 其他, 错误代码.
- */
- uint8_t ospi_receive(uint8_t *buf, uint32_t datalen)
- {
- g_ospi_handle.Instance->DLR = datalen - 1; /* 配置数据长度 */
- if (HAL_XSPI_Receive(&g_ospi_handle, buf,
- HAL_XSPI_TIMEOUT_DEFAULT_VALUE) == HAL_OK)
- {
- return 0;
- }
- else
- {
- return 1;
- }
- }
复制代码 该函数首先把要接收数据的长度赋值到OCTOSPI数据长度寄存器(OCTOSPI_DLR)中,然后通过调用HAL_XSPI_Receive函数接收数据。
接下来介绍的是OSPI发送函数,其定义如下:
- /**
- * @brief OSPI发送指定长度的数据
- * @param buf : 发送数据缓冲区首地址
- * @param datalen : 要传输的数据长度
- * @retval 0, 成功; 其他, 错误代码.
- */
- uint8_t ospi_transmit(uint8_t *buf, uint32_t datalen)
- {
- g_ospi_handle.Instance->DLR = datalen - 1; /* 配置数据长度 */
- if (HAL_XSPI_Transmit(&g_ospi_handle, buf,
- HAL_XSPI_TIMEOUT_DEFAULT_VALUE) == HAL_OK)
- {
- return 0;
- }
- else
- {
- return 1;
- }
- }
复制代码 该函数首先把要发送数据的长度赋值到OCTOSPI数据长度寄存器(OCTOSPI_DLR)中,然后通过调用HAL_XSPI_Transmit函数发送数据。
2. NORFLASH驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。NORFLASH驱动源码包括两个文件:norflash.c和norflash.h。
由于这部分代码量会比较多,这里就不一一贴出来介绍。介绍几个重点,其余的请自行查看源码。首先是norflash.h头文件中,我们做了一个FLASH芯片列表(宏定义),这些宏定义是一些支持的FLASH芯片的ID。接下来是FLASH芯片指令表的宏定义,这个请参考FLASH芯片手册比对得到。
下面介绍norflash.c文件几个重要的函数,首先是NOR FLASH初始化函数,其定义如下:
- /**
- * @brief 初始化SPI NOR FLASH
- * @param 无
- * @retval 无
- */
- void norflash_init(void)
- {
- uint8_t temp;
- ospi_init(); /* 初始化OSPI */
- norflash_qspi_disable(); /* 退出OPI模式(避免芯片之前进入这个模式,导致下载失败) */
- 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(); /* 写使能 */
- /* OSPI,使能4字节地址指令,地址为0,无数据_8位地址_无地址_单线传输指令,
- 无空周期,0个字节数据 */
- ospi_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函数,初始化STM32H562的OSPI接口。然后退出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个数据 */
- ospi_send_cmd(FLASH_FastReadQuad, addr, (3 << 6) | (g_norflash_addrw << 4)
- | (3 << 2) | (1 << 0), 6);
- ospi_receive(pbuf, datalen);
- }
复制代码 该函数用于从NOR FLASH的指定地址读出指定长度的数据,由于NOR FLASH支持以任意地址(但是不能超过NOR FLASH的地址范围)开始读取数据,所以,这个代码相对来说就比较简单了,通过ospi_send_cmd函数,发送FLASH_FastReadQuad指令,并发送读数据首地址(addr),然后通过ospi_receive函数循环读取数据,存放在pbuf里面。
接下来,我们介绍写入NOR FLASH函数,其定义如下:
- /**
- * @brief 写SPI FLASH
- * @note 在指定地址开始写入指定长度的数据 , 该函数带擦除操作!
- * SPI FLASH 一般是: 256个字节为一个Page, 4Kbytes为一个Sector,
- * 16个扇区为1个Block,擦除的最小单位为Sector.
- *
- * @param pbuf : 数据存储区
- * @param addr : 开始写入的地址(最大32bit)
- * @param datalen : 要写入的字节数(最大65535)
- * @retval 无
- */
- uint8_t g_norflash_buf[4096]; /* 扇区缓存 */
- void norflash_write(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
- {
- uint32_t secpos;
- uint16_t secoff;
- uint16_t secremain;
- uint16_t i;
- uint8_t *norflash_buf;
- norflash_buf = g_norflash_buf;
- secpos = addr / 4096; /* 扇区地址 */
- secoff = addr % 4096; /* 在扇区内的偏移 */
- secremain = 4096 - secoff; /* 扇区剩余空间大小 */
- //printf("ad:%X,nb:%X\r\n", addr, datalen); /* 测试用 */
- if (datalen <= secremain)
- {
- secremain = datalen; /* 不大于4096个字节 */
- }
- while (1)
- {
- norflash_read(norflash_buf, secpos * 4096, 4096); /* 读出整个扇区的内容 */
- for (i = 0; i < secremain; i++) /* 校验数据 */
- {
- if (norflash_buf[secoff + i] != 0XFF)
- {
- break; /* 需要擦除, 直接退出for循环 */
- }
- }
- if (i < secremain) /* 需要擦除 */
- {
- norflash_erase_sector(secpos); /* 擦除这个扇区 */
- for (i = 0; i < secremain; i++) /* 复制 */
- {
- norflash_buf[i + secoff] = pbuf[i];
- }
- /* 写入整个扇区 */
- norflash_write_nocheck(norflash_buf, secpos * 4096, 4096);
- }
- else /* 写已经擦除了的,直接写入扇区剩余区间. */
- {
- norflash_write_nocheck(pbuf, addr, secremain); /* 直接写扇区 */
- }
- if (datalen == secremain)
- {
- break; /* 写入结束了 */
- }
- else /* 写入未结束 */
- {
- secpos++; /* 扇区地址增1 */
- secoff = 0; /* 偏移位置为0 */
- pbuf += secremain; /* 指针偏移 */
- addr += secremain; /* 写地址偏移 */
- datalen -= secremain; /* 字节数递减 */
- if (datalen > 4096)
- {
- secremain = 4096; /* 下一个扇区还是写不完 */
- }
- else
- {
- secremain = datalen; /* 下一个扇区可以写完了 */
- }
- }
- }
- }
复制代码 该函数可以在NOR FLASH的任意地址开始写入任意长度(必须不超过NOR FLASH的容量)的数据。我们这里简单介绍一下思路:先获得首地址(addr)所在的扇区,并计算在扇区内的偏移,然后判断要写入的数据长度是否超过本扇区所剩下的长度,如果不超过,再先看看是否要擦除,如果不要,则直接写入数据即可,如果要则读出整个扇区,在偏移处开始写入指定长度的数据,然后擦除这个扇区,再一次性写入。当所需要写入的数据长度超过一个扇区的长度的时候,我们先按照前面的步骤把扇区剩余部分写完,再在新扇区内执行同样的操作,如此循环,直到写入结束。这里我们还定义了一个g_norflash_buf的全局数组,用于擦除时缓存扇区内的数据。
norflash.c文件我们就介绍这三个函数,其它函数请大家自行查阅源码。
3. main.c代码
在main.c里面编写如下代码:
- /* 要写入到FLASH的字符串数组 */
- const uint8_t g_text_buf[] = {"STM32 OSPI 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;
- HAL_Init(); /* HAL库初始化 */
- HAL_ICACHE_Enable(); /* 使能指令缓存 */
- sys_stm32_clock_init(250, 4, 2, 2); /* 系统时钟初始化, 250Mhz */
- delay_init(250); /* 延时初始化 */
- usart_init(115200); /* 串口初始化 */
- usmart_dev.init(250); /* USMART初始化 */
- led_init(); /* LED初始化 */
- lcd_init(); /* LCD初始化 */
- key_init(); /* 按键初始化 */
- norflash_init(); /* NORFLASH初始化 */
- lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
- lcd_show_string(30, 70, 200, 16, 16, "SPI 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_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, "SPI FLASH Ready!", BLUE);
- flashsize = 16 * 1024 * 1024; /* FLASH 大小为16M字节 */
- while (1)
- {
- key = key_scan(0);
- if(key == KEY1_PRES) /* KEY1按下,写入 */
- {
- lcd_fill(0, 170, 239, 319, WHITE); /* 清除半屏 */
- lcd_show_string(30, 150, 200, 16, 16, "Start Write FLASH....", BLUE);
- /* 从倒数第200个地址处开始,写入SIZE长度的数据 */
- norflash_write((uint8_t*)g_text_buf, flashsize - 200, 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);
- /* 从倒数第200个地址处开始,读出SIZE个字节 */
- norflash_read(datatemp, flashsize - 200, 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);
- printf("datatemp:%s \r\n", datatemp);
- }
- 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的程序指令也擦除掉,会导致死机。如果不使用分散加载,就没关系。
34.4 下载验证
将程序下载到开发板后,可以看到LED0不停的闪烁,提示程序已经在运行了。LCD显示的内容如图34.4.1所示:
图34.4.1 QSPI实验程序运行效果图
通过先按KEY1按键写入数据,然后按KEY0读取数据,得到如图34.4.2所示:
图34.4.2 操作后的显示效果图
程序在开机的时候会检测NOR FLASH是否存在,如果不存在则会在LCD模块上显示错误信息,同时LED0慢闪。 |