QSPI实验4 n9 F; P2 O2 r1 x" C& r- }+ B
本章,我们将介绍STM32H750的QSPI功能,并使用STM32H750自带的QSPI来实现对外部NOR FLASH的读写,并将结果显示在LCD模块上。, ^) }- s; D, E
$ U g4 B: F7 G) Z \# s$ V% ^) m9 c
36.1 QSPI及NOR FLASH芯片简介
; ^7 m: x( X/ j9 F36.1.1 QSPI简介
) N+ N: {/ _5 p$ W- s% Z: w1 d# N' _4 xQSPI是Quad SPI的缩写,是Motorola公司推出SPI接口后的一种扩展接口,较SPI应用更为广泛。在 SPI 协议的基础上,Motorola 公司对其功能进行了增加,增加了队列传输机制,推出了队列串行外围接口协议(即 QSPI 协议)。QSPI 是一种专用的通信接口,连接单、双或四(条数据线)SPI FLASH存储器。STM32H7具有QSPI接口,支持如下三种工作模式:- V& V3 m4 z2 {
1、间接模式:使用QSPI寄存器执行全部操作。/ N f! R- ]8 r: s; I% d- ~; ^, a
2、状态轮询模式:周期性读取外部FLASH状态寄存器,当标志位置1时会产生中断(如
6 w4 h1 u) q0 U8 d, ?擦除或烧写完成,产生中断)。0 @) G6 A' [" S1 h; Z2 }
3、内存映射模式:外部FLASH映射到微控制器地址空间,从而系统将其视作内部存储器。
6 z9 }+ b7 R! F% I$ zSTM32H7的QSPI接口具有如下特点:( Y1 o5 x6 X5 D; g ^
支持三种工作模式:间接模式、状态轮询模式和内存映射模式。
% t, [% z7 g* \- b支持双闪存模式,可以并行访问两个FLASH,可同时发送/接收8位数据。
8 O/ A* ? Q% E) f3 C支持SDR(单倍率速率)和DDR(双倍率速率)模式。+ Y8 P* T8 M+ N$ A
针对间接模式和内存映射模式,完全可编程操作码。
# t. l" V% v& g针对间接模式和内存映射模式,完全可编程帧格式。1 f' u Y: ^' P& Z% f
集成 FIFO,用于发送和接收。- ~& P% @* S- A4 Y F
允许 8、16 和 32 位数据访问。
2 N6 M) P& s& O. r* }3 j具有适用于间接模式操作的DMA通道。
: l) [0 ^/ q4 V( ?: i N在达到 FIFO 阈值、超时、操作完成以及发生访问错误时产生中断。+ o4 ~/ ]2 E0 @- z s3 W P4 ^. u
36.1.1.1 QSPI框图
`3 c3 m% I: g. v+ S' ^ x5 PSTM32H7的QSPI单闪存模式的功能框图如图36.1.1.1.1所示:
1 w2 |+ {! P: R: d, Z+ M
; L4 S/ ^* l1 h4 ?4 D# p' ^
" N3 { g! n3 v9 K4 K! p5 {. ~) l, `- j1 u# @2 O1 p7 G
图36.1.1.1.1 STM32H7 QSPI框图; A) X1 L2 E4 J! s
上图左边可以看到QSPI连接到64位 AXI 总线以及32位AHB总线上,此外还有5条QSPI的内部信号,如下表所示:6 ^1 q7 V7 t u" f( A/ e: k4 `
_' L6 U1 U% `6 ?" b
8 k1 S' L3 n& s; ?- F$ l# d
0 G, m& {( A* n8 T9 g; A; V7 b0 P表36.1.1.1.1 QSPI内部信号
6 i9 j- W, l, D. X7 _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。
+ Z1 V( R" a8 G2 u: X4 Gquadspi_hclk,用于操作QUADSPI寄存器的时钟。时钟源来自HCLK3,同样是经过sys_stm32_clock_init函数的配置,配置后HCLK3时钟频率为240MHZ。
# [( c2 ~* @% l% wquadspi_it,中断请求信号线。在达到FIFO阈值、超时、操作完成以及发送访问错误时产生中断。% B. c3 _* q2 k) j
quadsqp_ft_trg,达到FIFO阈值时触发MDMA请求。/ W% Y$ k$ E5 T2 } V
quadspi_tc_trg,操作完成时触发MDMA请求' N% {& O4 l o$ r+ n6 }
上图中间部分就是QUADSPI 内核。
; C4 m0 T9 f" p4 h我们重点看看上图右边的QSPI接口引脚,通过6根线与SPI FLASH芯片连接,包括:4根数据线(IO0~3)、1根时钟线(CLK)和1根片选线(nCS),具体如下表所示:
: v8 V S9 c0 U1 U4 h
1 s3 o7 b: B5 k' v1 V" s! d. X1 C! g0 f- }+ [6 B
表36.1.1.1.2 QSPI接口引脚
1 x# L/ s X0 D6 R& y我们知道普通的SPI通信一般只有一根数据线(MOSI/MISO,发送/接收用),而QSPI则具有4根数据线,所以QSPI的速率至少是普通SPI的4倍,可以大大提高通信速率。如果使用双闪存模式,同时访问两个Quad-SPI Flash,速度可以再翻一番。我们开发板板载了一个Quad-SPI Flash,所以我们使用单闪存模式,双闪存模式就不具体介绍了,感兴趣请自行查看手册。
6 W6 R. ]; h- B/ B; N4 O接下来,我们给大家简单介绍一下STM32H7 QSPI接口的的几个重要知识点。1 j" `) p" _! M I& @' A/ Q$ v" g; w
36.1.1.2 QSPI命令序列, f/ X: h7 \0 h: b1 T2 J
QSPI 通过命令与FLASH通信,每条命令包括:指令、地址、交替字节、空指令和数据这五个阶段,任一阶段均可通过配置QUADSPI_CCR寄存器相关字段跳过,但至少要包含指令、地址、交替字节或数据阶段之一。! _% \6 ]# u1 l' B9 R5 J3 y4 \3 N
nCS 在每条指令开始前下降,在每条指令完成后再次上升。QSPI四线模式下的读命令时序,如图36.1.1.2.1所示:
$ h8 E& |- g4 T# |3 I# o" ^2 ^
4 t9 n- v9 y! B, Q* Q
; e0 Z' {2 [; c
$ t! d# } D* i图36.1.1.2.1 四线模式下QSPI读命令时序+ t- k1 m$ V M7 h3 v
从上图可以看出一次QSPI传输的5个阶段,接下来我们分别介绍。
) d. S1 R6 X Y+ K* J5 L# G① 指令阶段% m5 ?# D* V7 D/ Z, P
此阶段通过QUADSPI_CCR[7:0]寄存器的INSTRUCTION字段指定一个8位指令发送到FLASH。注意,指令阶段,一般是通过IO0单线发送,但是也可以配置为双线/四线发送指令,可以通过QUADSPI_CCR[9:8]寄存器的IMODE[1:0]这两个位进行配置,如IMODE[1:0]=00,则表示无需发送指令。% _6 {# p7 l, \/ \4 J7 t. p
②地址阶段
; p0 {$ W+ j U% e) ?$ F7 q; j此阶段可以发送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,则表示无需发送地址。
& u4 r- [( V8 I4 F& m" M" E6 Z4 ~③交替字节(复用字节)阶段' M5 r! z; F- e1 s+ A' h/ m" ?6 J% V) F
此阶段可以发送1~4字节数据给FLASH芯片,一般用于控制操作模式。待发送的交替字节数由QUADSPI_CCR[17:16]寄存器的ABSIZE[1:0]位配置。待发送的数据由QUADSPI_ABR寄存器中指定。交替字节同样可以以单线/双线/四线模式发送,通过QUADSPI_CCR[15:14]寄存器的ABMODE[1:0]这两个位配置,ABMODE[1:0]=00,则跳过交替字节阶段。
3 t$ R q. R* v/ E④空指令周期阶段
# J$ g1 j; ~9 F) r8 n1 D在空指令周期阶段,在给定的1~31个周期内不发送或接收任何数据,目的是当采用更高的时钟频率时,给FLASH芯片留出准备数据阶段的时间。这一阶段中给定的周期数由QUADSPI_CCR[22:18]寄存器的DCYC[4:0]位配置。 若DCYC为零,则跳过空指令周期阶段,命令序列直接进入下一个阶段。
- t4 R: S: G `8 X5 U⑤数据阶段
! `' }0 }( c t& m) Q) V此阶段可以从FLASH读取/写入任意字节数量的数据。在间接模式和自动轮询模式下,待发送/接收的字节数由QUADSPI_DLR寄存器指定。在间接写入模式下,发送到FLASH的数据必须写入QUADSPI_DR寄存器。在间接读取模式下,通过读取QUADSPI_DR寄存器获得从 FLASH接收的数据。数据阶段同样可以以单线/双线/四线模式发送,通过QUADSPI_CCR[25:24]寄存器的DMODE [1:0]这两个位进行配置,如DMODE [1:0]=00,则表示无数据。
2 ]! c1 @% q( w6 ?7 t- y# h9 {8 V以上就是QSPI数据传输的5个阶段,其中交替字节阶段我们一般用不到,可以省略(通过设置ABMODE[1:0]=00)。' g# B6 O- W2 F0 E
另外说明一下,QUADSPI信号接口协议模式包括:单线SPI模式、双线SPI模式、四线SPI模式、SDR模式、DDR模式和双闪存模式。这些模式请大家自行参考官方手册。1 q* T4 Q3 O. h) A* h; n
36.1.1.3 QSPI三种功能模式
# |& o; y/ |/ N1 w" P3 C# \( [前面已经提到过QSPI的三种功能模式:间接模式、状态标志轮询模式和内存映射模式。下面对这三个功能模式分别简单介绍。
4 e$ s% J( o0 [& @! p- A① 间接模式 i: \4 m. a4 `# m5 }0 o
在间接模式下,通过写入QUADSPI寄存器来触发命令,通过读写数据寄存器来传输数据。
! u( X: N# H$ C3 u) s当FMODE=00 (QUADSPI_CCR[27:26])时,QUADSPI处于间接写入模式,在数据阶段,将数据写入数据寄存器(QUADSPI_DR),即可写入数据到FLASH。3 t* K% G1 x) s$ O, e
当FMODE=01时,QUADSPI处于间接读取模式,在数据阶段,读取QUADSPI_DR寄存器,即可读取FLASH里面的数据。
+ Z/ i2 q& T, }9 i6 H, Q读/写字节数由数据长度寄存器(QUADSPI_DLR)指定。当QUADSPI_DLR=0xFFFFFFFF时,则数据长度视为未定义,QUADSPI 将持续传输数据,直到到达FLASH结尾(FLASH容量由 QUADSPI_DCR[20:16]寄存器的FSIZE[4:0]位定义)。如果不传输任何数据,则DMODE[1:0] (QUADSPI_CCR[25:24])应设置为00。2 v$ I2 q2 }+ x
当发送或接收的字节数(数据量)达到编程设定值时,如果TCIE=1,则TCF置1并产生中断。在数据量不确定的情况下,将根据FSIZE[4:0]定义的FLASH大小,在达到外部SPI FLASH的限制时,TCF置1。
: f8 P/ m& a; t在间接模式下有三种触发命令启动的方式,分别是:2 [, j3 _& T( Q
(1)当不需要发送地址(ADMODE[1:0]==00)和数据(DMODE[1:0]==00)时,对INSTRUCTION[7:0](QUADSPI_CCR[7:0])执行写入操作。1 _: `3 P! x0 u8 N2 u. A
(2)当需要发送地址(ADMODE[1:0]!=00),但不需要发送数据(DMODE[1:0]==00),对ADDRESS[31:0](QUADSPI_AR)执行写入操作。
0 _$ M+ p+ }! U- ~. V5 T7 r5 u(3)当需要发送地址(ADMODE[1:0]!=00)和数据(DMODE[1:0]!=00)时,对DATA[31:0] (QUADSPI_DR)执行写入操作。* k0 u( x- ]# u/ D W
如果命令启动,BUSY位(QUADSPI_SR的第5位)将自动置1。9 Y# V6 l5 T7 |; t
②状态标志轮询模式
1 z5 f# u% F [7 S将FMODE字段(QUADSPI_CCR[27:26]) 设置为10,使能状态标志轮询模式。在此模式下,将发送编程的帧并周期性检索数据。每帧中读取的最大数据量为4字节。如果QUADSPI_DLR请求更多的数据,则忽略多余部分并仅读取4个字节。在 QUADSPI_PISR 寄存器指定周期性。/ a, o Q1 n2 [7 x5 W
在检索到状态数据后,可在内部进行处理,以达到以下目的:
' e! C5 u) [2 s(1)将状态匹配标志位置 1,如果使能,还将产生中断.
# q9 A/ h+ p8 _# }' I- i$ K(2)自动停止周期性检索状态字节。2 n5 R$ q7 G$ _: E E
接收到的值可通过存储于QUADSPI_PSMKR寄存器中的值来进行屏蔽,并与存储在 QUADSPI_PSMAR寄存器中的值进行或运算或与运算。/ H0 b! {) @! W9 _7 g- T
若是存在匹配,则状态匹配标志置1,并且在使能了中断的情况下还将产生中断;如果AMPS 位置1,则QUADSPI自动停止。
/ e# i' F% A1 v1 V) T6 V, Y5 |) J在任何情况下,最新的检索值都在QUADSPI_DR中可用。
2 F$ [; F+ K I; m( J9 F①内存映射模式0 c9 u6 H/ ~4 _/ i
在配置为内存映射模式时,外部FLASH器件被视作内部存储器,只是存在访问延迟。在该模式下,仅允许对外部 FLASH 执行读取操作。将QUADSPI_CCR寄存器中的FMODE设置为11可进入内存映射模式。当主器件访问存储器映射空间时,将发送已编程的指令和帧。另外数据长度寄存器(QUADSPI_DLR)在内存映射模式中无意义。
/ K: H4 |$ I/ H" E9 V* e& v! SQUADSPI外设若没有正确配置并使能,禁止访问QUADSPI Flash的存储区域。即使FLASH容量更大,寻址空间也无法超过256MB。如果访问的地址超出FSIZE定义的范围但仍在256MB 范围内,则生成总线错误。此错误的影响具体取决于尝试进行访问的总线主器件:
& I% P a; F& b(1)如果为Cortex® CPU,则会在使能总线故障时发生总线故障异常,在禁止总线故障时发生硬性故障(hard fault) 异常。* b% V- O+ H* q- G6 A; I+ m B8 P
(2)如果为 DMA,则生成 DMA传输错误,并自动禁用相应的 DMA 通道。
! }# W" X# E/ M% X4 ?# q6 }1 C$ G0 ?! e7 I内存映射模式支持字节、半字和字访问类型,并支持芯片内执(XIP)操作,QUADSPI接受下一个微控制器访问并提前加载后面地址中的字节。如果之后访问的是连续地址,由于值已经预取,访问将更快完成。
( A. f2 P# N! w$ w; w$ E# ?默认情况下,即便在很长时间内不访问FLASH,QUADSPI也不会停止预取操作,之前的读取操作将保持激活状态并且 nCS 保持低电平。由于 nCS保持低电平时,FLASH 功耗增加,应用程序可能会激活超时计数器(TCEN = 1, QUADSPI_CR 的位 3)。从而在 FIFO中写满预取的数据后,若在 TIMEOUT[15:0] (QUADSPI_LPTR) 个周期的时长内没有访问,则释放 nCS。BUSY在第一个存储器映射访问发生时变为高电平。由于进行预取操作,BUSY在发生超时、中止或外设禁止前不会下降。! ?. E3 X9 s2 E' ~/ J' Y* r' A/ `
36.1.1.4 QSPI FLASH配置
5 X! P' p% V& ^SPI FLASH芯片的相关参数通过器件配置寄存器 (QUADSPI_DCR) 来进行设置。寄存器QUADSPI_DCR[20:16]的FSIZE[4:0]这5个位,用于指定外部存储器的大小,计算公式为:, O$ y# `* w' J
Fcap=21
: p$ A: z* \; p# DFSIZE+1是对Flash寻址所需的地址位数。Fcap表示FLASH的容量,单位为字节,在间接模式下,最高支持4GB(使用32位进行寻址)容量的FLASH芯片。但是在内存映射模式下的可寻址空间限制为256MB。8 a. ]) r. S3 ]% X/ V
QSPI连续执行两条命令时,它在两条命令之间将片选信号 (nCS) 置为高电平默认仅一个 CLK周期。某些FLASH需要命令之间的时间更长,可以通过寄存器QUADSPI_DCR[10:8]的CSHT[2:0](选高电平时间)这3个位设置高电平时长:07表示18个时钟周期(最大为8)。0 q o2 F) w e5 z9 w! J
时钟模式,用于指定在nCS为高电平时,CLK的时钟极性。通过寄存器QUADSPI_DCR[0]的CKMODE位指定:当CKMODE=0时,CLK在nCS为高电平期间保持低电平,称之为模式0;当CKMODE=1时,CLK在nCS为高电平期间保持高电平,称之为模式3。4 ?' a7 U- S5 z
36.1.1.5 QUADSPI寄存器2 ?9 R3 ]$ }/ R( O: ?3 t) e
QUADSPI控制寄存器(QUADSPI_CR)
& ~3 E1 M6 N5 i2 o' Z! }QUADSPI控制寄存器描述如图36.1.1.5.1所示:
/ C ~) ?# u- [9 o3 F, l" ?) c( @7 b4 H( s- e
% o" Z5 J1 V# x+ O# u# |* b
9 c' F6 D% P1 |4 z4 M图36.1.1.5.1 QUADSPI_CR寄存器
6 Q3 K3 L8 s ] s* ~ s/ H该寄存器我们只关心需要用到的一些位(下同),首先是PRESCALER[7:0],用于设置AHB时钟预分频器:0255,表示0256分频。我们使用的W25Q128最大支持104Mhz的时钟,这里我们设置PRESCALER=2,即3分频,得到QSPI时钟为72Mhz(216/3)。! _' K: ~$ v" @! D8 N6 ^6 [
FTHRES[4:0],用于设置FIFO阈值,范围为031,表示FIFO的阈值为132字节。4 }) l/ L, Q( l& X: T. G2 P9 Q
FSEL位,用于选择FLASH,我们的W25Q128连接在STM32H7的QSPI BK1上面,所以设置此位为0即可。
) e/ T1 S; x v; I2 m e# |& DDFM位,用于设置双闪存模式,我们用的是单闪存模式,所以设置此位为0即可。
+ q& j' A# i" f8 Y; USSHIFT位,用于设置采样移位,默认情况下,QSPI接口在FLASH驱动数据后过半个CLK 周期开始采集数据。使用该位,可考虑外部信号延迟,推迟数据采集。我们一般设置此位为1,移位半个周期采集,确保数据稳定。
1 J; W) D) q* v( W7 Y+ T" ]% BABORT位,用于终止QSPI的当前传输,设置为1即可终止当前传输,在读写FLASH数据的时候,可能会用到。
9 `; }* [: z8 a; h* W; l7 H7 jEN位,用于控制QSPI的使能,我们需要用到QSPI接口,所以必须设置此位为1。1 t# f7 v: w$ @
QUADSPI器件配置寄存器(QUADSPI_ DCR). a$ ?7 L' ]8 ]
QUADSPI器件配置寄存器描述如图36.1.1.5.2所示:/ w4 w' e( l9 n w# ]. M5 X0 m. ^
9 P' D7 |3 C: j6 N) [
) K2 b/ r4 H2 x/ a
' {. F' e6 _9 F' z4 W, l图36.1.1.5.2 QUADSPI_ DCR寄存器' {# y8 Y. {" A
该寄存器可以设置FLASH芯片的容量(FSIZE)、片选高电平时间(CSHT)和时钟模式(CKMODE)等,这些位的设置说明见前面的36.1.1.4小节有详细讲解。! N" D. m) q- E+ O7 p1 \8 r5 K2 ^- t
QUADSPI状态寄存器(QUADSPI_ SR)
& ?0 ]- ?2 N8 @& \QUADSPI状态寄存器描述如图36.1.1.5.3所示:
h2 |! @' M9 _# [, T
( K! C- a5 R( C7 @" P% z# k5 h
& |; L. `/ o9 t9 C% Y$ ?$ ~0 {; y5 z7 Q$ H
图36.1.1.5.3 QUADSPI_ SR寄存器2 l2 z# q/ ?+ X1 m3 j: N
BUSY位,指示操作是否忙。当该位为1时,表示QSPI正在执行操作。在操作完成或者FIFO为空的时候,该位自动清零。
" N, M' k+ `8 F7 U4 eFTF位,表示FIFO是否到达阈值。在间接模式下,若达到FIFO阈值,或从FLASH读取完成后,FIFO中留有数据时,该位置1。只要阈值条件不再为“真”,该位就自动清零。
- U- S! h* S9 X8 O! |% M4 wTCF位,表示传输是否完成。在间接模式下,当传输的数据数量达到编程设定值,或在任何模式下传输中止时,该位置1。向QUADSPI_FCR寄存器的CTCF位写1,可以清零此位。
0 w: D+ A1 n3 M K" ^& U+ CQUADSPI标志清零寄存器(QUADSPI_ FCR)
[% |2 J/ V: |. P; i; o, qQUADSPI标志清零寄存器描述如图36.1.1.5.4所示:
' p2 [1 D( n+ |- R) T/ v3 U4 B$ t" p" h, E, ^; r1 ?9 x' i7 l5 |
% _4 ^- F6 g+ {& Q: J) l
( o2 ^. [) p8 S! G9 ]* W" [图36.1.1.5.4 QUADSPI_ FCR寄存器
, r) T x. Z. @% M Z该寄存器,我们一般只用到CTCF位,用于清除QSPI的传输完成标志。
) d+ R4 @* D$ q! Y! R( f0 `QUADSPI通信配置寄存器(QUADSPI_ CCR)
. B% p; @: T+ o [/ w- \( z8 XQUADSPI通信配置寄存器描述如图36.1.1.5.5所示:
# z j* u# C9 g- B& u# [+ V2 t
+ b2 c2 ~1 ]/ q) N, o) \8 D& c
. @. `8 K3 k$ o. ]
/ L8 P& o. e' O+ v7 j图36.1.1.5.5 QUADSPI_ CCR寄存器- ?/ m) J, B- `) s6 Z/ v
DDRM位,用于设置双倍率模式(DDR),我们没用到双倍率模式,所以设置此位为0。2 V. r$ p4 J, ~1 M/ q8 }- h
SIOO位,用于设置指令是否只发送一次,我们需要每次都发送指令,所以设置此位为0。
3 r3 R. O* a! F& s3 O6 u8 IFMODE[1:0],这两个位用于设置功能模式:00,间接写入模式;01,间接读取模式;10,自动轮询模式;11,内存映射模式;我们使用间接模式,所以此位根据需要设置为00/01。
2 Z' [* l3 s# G6 VDMODE[1:0],这两个位用于设置数据模式:00,无数据;01,单线传输数据;10,双线传输数据;11,四线传输数据;我们一般设置为00/11。7 T4 A$ _0 ?# `/ a$ y$ |: Z
DCYC[4:0],这5个位用于设置空指令周期数,可以控制空指令阶段的持续时间,设置范围为:0~31。设置为0,则表示没有空指令周期。/ w" ?. y& z( w7 \
ABMODE[1:0],这两个位用于设置交替字节模式,我们一般设置为0,表示无交替字节。1 m* \0 _4 s6 q
ADMODE[1:0],这两个位用于设置地址模式:00,无地址;01,单线传输地址;10,双线传输地址;11,四线传输地址;我们一般设置为00/11。! q$ @+ A& e- }9 L
IMODE[1:0],这两个位用于设置指令模式:00,无指令;01,单线传输指令;10,双线传输指令;11,四线传输指令;我们一般设置为00/11。
$ A p1 B. u; B/ N, @INSTRUCTION[7:0],这8个位用于设置将要发送给FLASH的指令。
! z: R: O F* Y* s- B) H1 u7 Q注意,以上这些位的配置,都必须在QUADSPI_SR寄存器的BUSY位为0时才可配置。
9 X* {% L( m4 f1 C3 P$ ?0 D7 N) |接下来,我们看QSPI数据长度寄存器:QUADSPI_DLR,该寄存器为一个32位寄存器,可以设置的数据长度范围为:0~0XFFFFFFFF,当QUADSPI_DLR!=0XFFFFFFFF时,表示传输的字节长度(+1);当QUADSPI_DLR==0XFFFFFFFF时,表示不限传输长度,直到到达由FSIZE定义的FLASH结尾。! X9 l+ F5 v/ P& k
接下来,我们看QSPI地址寄存器:QUADSPI_AR,该寄存器为一个32位寄存器,用于指定发送到FLASH的地址。) a; s9 n4 g* F- N, R6 L% L
接下来,我们看QSPI数据寄存器:QUADSPI_DR,该寄存器为一个32位寄存器,用于指定与外部SPI FLASH设备交换的数据。该寄存器支持字、半字和字节访问。& W- J; _) C4 r7 t1 J
在间接写入模式下,写入该寄存器的数据在数据阶段发送到FLASH,在此之前则存储于FIFO,如果 FIFO 满了,则暂停写入,直到 FIFO 具有足够的空间接受要写入的数据才继续。
; {" E( T$ l1 Z t/ M% ]在间接模式下,读取该寄存器可获得(通过FIFO)已从FLASH接收的数据。如果FIFO所含字节数比读取操作要求的字节数少,且BUSY=1,则暂停读取,直到足够的数据出现或传输完成才继续。
t$ c" F9 y1 W+ m36.1.2 NOR FLASH芯片简介' h4 Z( {# @. d# S3 b
NOR FLASH芯片有很多种芯片型号,在我们的norflash.h头文件中有定义芯片ID的宏定义,对应的就是不同型号的NOR FLASH芯片,比如有:W25Q128、BY25Q128、NM25Q128,它们是来自不同的厂商的同种规格的NOR FLASH芯片,内存空间都是128M字,即16M字节。它们的很多参数、操作都是一样的,所以我们的实验都是兼容它们的。
- c9 E: V9 s- e由于这么多的芯片我们就不一一进行介绍了,就拿其中一款型号进行介绍即可,其他的型号都是类似的。' i/ e& q; u" ~ z
W25Q128是一款大容量SPI FLASH产品,其容量为16M。它将16M字节的容量分为256个块(Block),每一个块大小为64K字节,每个块又分为16个扇区(Sector),每一个扇区16页,每页256个字节,即每个扇区4K字节。W25Q128的最小擦除单位为一个扇区,也就是每次必须擦除4K个字节。这样我们需要给W25Q128开辟一个至少4K的缓存区,这样对SRAM要求比较高,要求芯片必须有4K以上SRAM才能很好的操作。: r7 T9 h r ^7 w. O1 \
W25Q128的擦写周期多达10W次,具有20年的数据保存期限,支持电压为2.7~3.6V,W25Q128支持标准的SPI,还支持双输出/四输出SPI和QPI(QPI即QSPI),最高时钟频率可达104Mhz(双输出时相当于208Mhz,四输出时相当于416M),本实验我们将使用STM32H7的QSPI接口来实现对W25Q128的驱动。7 b" T" [/ H, ~* L( l& @( C
接下来,我们介绍一下本实验驱动W25Q128需要用到的一些指令,如表36.1.2.1所示:
; I. m" m4 d" N* r. i3 N% y3 R* O& h( w, T5 k: @
H8 o; P4 U3 V# u1 p6 }% Y; P
" U+ {0 D. y' ~5 |3 c z
1,在QPI模式下dummy时钟的个数,由读参数控制位P[5:4]位控制。
# }% u; P# W" R7 d) o2,传输的数据量,只要不停的给时钟就可以持续传输,对W25X_PageProgram指令,则单次传输最多不超过256字节,否则将覆盖之前写入的数据。8 e& ]% {5 N3 H8 J3 [
表36.1.2.1 W25Q128指令* [. r; J* o3 j% c% I3 }: j
上表列出了本章我们驱动W25Q128所需要用到的所有指令和对应的参数,注意SPI模式和QPI模式下时钟数的区别,可知QPI模式比SPI模式所需要的时钟数少的多,所以速度也快得多。接下来我们简单介绍一下这些指令。- F% M: Y) C5 B" j6 B' E
首先,前面6个指令,是用来读取/写入状态寄存器13的。在读取的时候,读取S23S0的数据,在写入的时候,写入S23S0。而S23S0则由三部分组成:S23S16,S15S8,S7~S0即状态寄存器3、2、1,如表36.1.2.2所示:
: b# m- t" H' t4 R% A! |: y; c8 o+ O
9 _: E; d7 [7 ], B6 a+ T. `. y3 l( w! D! [2 M5 Z/ q
表36.1.2.2 W25Q128状态寄存器5 M/ l( k) ~5 t& G" m( U
这三个状态寄存器,我们只关心我们需要用到的一些位:ADS、QE和BUSY位。其他位的说明,请看W25Q128的数据手册。7 E( W; L+ U2 I
ADS位,表示W25Q128当前的地址模式,是一个只读位,当ADS=0的时候,表示当前是3字节地址模式,当ADS=1的时候,表示当前是4字节地址模式,我们需要使用4字节地址模式,所以在读取到该位为0的时候,必须通过W25X_Enable4ByteAddr指令,设置为4字节地址模式。
# b% S( d6 u" S# }/ N1 j% s$ FQE位,用于使能4线模式(Quad),此位可读可写,并且是可以保存的(掉电后可以继续保持上一次的值)。在本章,我们需要用到4线模式,所以在读到该位为0的时候,必须通过W25X_WriteStatusReg2指令设置此位为1,表示使能4线模式。
5 Q( F t Y$ G0 s. e; wBUSY位,用于表示擦除/编程操作是否正在进行,当擦除/编程操作正在进行时,此位为1,此时W25Q128不接受任何指令,当擦除/编程操作完成时,此位为0。此位为只读位,我们在执行某些操作的时候,必须等待此位为0。
8 U+ `" l9 h1 M; a, q) GW25X_ManufactDeviceID指令,用于读取W25Q128的ID,可以用于判断W25Q128是否正常。对于W25Q128来说:MF[7:0]=0XEF,ID[7:0]=0X18。
1 B9 V, E+ y; E; ^" T: W! AW25X_EnterQPIMode指令,用于设置W25Q128进入QPI模式。上电时,W25Q128默认是SPI模式,我们需要通过该指令设置其进入QPI模式。注意:在发送该指令之前,必须先设置状态寄存器2的QE位为1!!; q" \' F$ A4 a8 L2 U
W25X_Enable4ByteAddr指令,用于设置W25Q128进入4字节地址模式。当读取到ADS位为0的时候,我们必须通过此指令将W25Q128设置为4字节地址模式,否则将只能访问16MB的地址空间。4 z7 Q R. @2 {+ I: J
W25X_SetReadParam指令,可以用于设置读参数控制位P[5:4],这两个位的描述如表36.1.2.3所示:$ \: c* q# r- p, Z4 }
5 q2 ?' d- v, o; g
) s9 M) c; f1 e# D& ^7 I' e0 R7 }; J5 r
表36.1.2.3 W25Q128读参数控制位
2 M0 N* F$ p+ l7 f为了让W25Q128可以工作在最大频率下,我们这里设置P[5:4]=11,即可工作在104Mhz的时钟频率下。此时,读取数据时的dummy时钟个数为8个(参见W25X_FastReadData指令)。
- W2 C& ?$ {! l" QW25X_WriteEnable指令,用于设置W25Q128写使能。在执行擦除、编程、写状态寄存器等操作之前,都必须通过该指令,设置W25Q128写使能,否则无法写入。# R) X* { H0 i0 M4 b
W25X_FastReadData指令,用于读取FLASH数据,在发送完该指令以后,就可以读取W25Q128的数据了。该指令发送完成后,我们可以持续读取FLASH里面的数据,只要不停的给时钟,就可以不停的读取数据。7 B7 z0 b% V o. ~% \/ |5 V0 f& ^6 w
W25X_PageProgram指令,用于编程FLASH(写入数据到FLASH),该指令发送完成后,最多可以一次写入256字节到W25Q128,超过256字节则需要多次发送该指令。
; i/ I2 s( R4 S6 a! C( X0 [' W3 @! LW25X_SectorErase指令,用于擦除一个扇区(4KB)的数据。因为FLASH具有只可以写0,不可以写1的特性,所以在写入数据的时候,一般需要先擦除(归1),再写。W25Q128的最小擦除单位为一个扇区(4KB)。该指令在写入数据的时候,经常要有用。
+ @3 P1 x6 G4 w3 F, N7 YW25X_ChipErase指令,用于全片擦除W25Q128。 N" O, h( v( @8 T3 d
为了在程序上方便使用,我们把FLASH芯片的常用指令编码定义为宏定义的形式,存放在norflash.h文件中。
3 N1 J* s+ W& o& M( x
! i$ ^. }, Q; x0 y- m. ?0 T9 p& e36.2 硬件设计# ?: f7 r1 r, G6 e
1.例程功能) _: H" P, x* n, x E
通过KEY1按键来控制norflash的写入,通过按键KEY0来控制norflash的读取。并在LCD模块上面显示相关信息。我们还可以通过USMART控制读取norflash的ID、擦除某个扇区或整片擦除。LED0闪烁用于提示程序正在运行。
7 S5 y7 X# V3 A( e! w" W2.硬件资源
3 B. V4 V& d h: l+ M; `! k1)RGB灯
. G3 D5 D0 e3 I3 J4 J. D0 X' t1 KRED :LED0 - PB42 ]% R1 v1 t& u* K. {0 a% O, u3 e& F
2)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)8 i3 B$ M( z5 e; C8 y
3)正点原子2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)3 a' H Y' I7 N% s
4)独立按键 :KEY0 - PA1、KEY1 - PA15
B3 l8 t5 I( U& |# ^- R5)QSPI(PB2/PB6/PD11/PD12/PD13/PE2)
5 Y' k5 A2 \4 ]2 ]8 R6)norflash(QSPI FLASH芯片,连接在QSPI接口上)6 C/ l& h2 {, z4 c
3.原理图9 ~4 A8 ^9 `, D
板载的QSPI FLASH芯片与STM32H750的连接关系,如下图所示:' o+ P7 Z5 L- P( `+ ~: d
3 Q& Z" ~) D: k! F( m* q
* n$ ~: c( C, T8 @) l8 `8 u( o; k, O, t3 E w
图36.2.1 QSPI FLASH芯片与STM32H750连接示意图
: j" `9 C1 Q2 b7 x* j- h7 l本实验支持多种型号的QSPI FLASH芯片,比如:BY25Q128/NM25Q128/W25Q128等等,具体请看norflash.h文件的宏定义,程序上只需要稍微修改一下,后面讲解程序的时候会说到。
7 A! p5 O- Z; o4 A8 o7 o* B+ u6 m* e1 J7 T
36.3 程序设计
U, D5 b( } ]- E# E# Z36.3.1 QSPI的HAL库驱动
8 @0 }* Q0 ?" h# ]8 t Z9 d3 LQSPI在HAL库中的驱动代码在stm32h7xx_hal_qspi.c文件(及其头文件)中。
9 b4 G) r! B& g1.HAL_QSPI_Init函数
. `( }& T' u! dQSPI的初始化函数,其声明如下:8 K& M0 U8 K- T) r) i2 a. f
HAL_StatusTypeDef HAL_QSPI_Init(QSPI_HandleTypeDef *hqspi);
* ^2 L5 B1 w; z; Q* ^函数描述:) B5 o4 V5 ]) C
用于初始化QSPI。
- ?0 Z! E6 y0 c* r函数形参:
0 B) Z' g" n& V8 ]9 g1 ^# ^形参1是QSPI_HandleTypeDef结构体类型指针变量,其定义如下:' L B) }1 e* `, T; F
- typedef struct
+ p, u! `$ w7 c' ^ - {3 ^+ s6 ]- o4 o( ^
- QUADSPI_TypeDef *Instance; /* QSPI寄存器基址 */ # [# t0 l2 q% l! g9 {
- QSPI_InitTypeDef Init; /* QSPI参数配置结构体 */
, R2 I& j6 D8 v5 w, W- X - uint8_t *pTxBuffPtr; /* 要发送数据的地址 */
% o9 h. p# t9 b - __IO uint16_t TxXferSize; /* 要发送数据的大小 */
$ a/ t/ |1 U* I. }$ m - __IO uint16_t TxXferCount; /* 剩余要发送数据的个数 */ ( H! e2 v" o* d7 c P
- uint8_t *pRxBuffPtr; /* 要接收数据的地址 */ 4 i/ d; g5 G; n
- __IO uint16_t RxXferSize; /* 要接收数据的大小 */
6 G7 C2 \* Q- O - __IO uint16_t RxXferCount; /* 剩余要接收数据的个数 */
3 c& K9 g4 z% J - DMA_HandleTypeDef * hmdma; /* DMA配置结构体 */ ' j: ]& q: i: y0 X9 B" q7 h
- __IO HAL_LockTypeDef Lock; /* 锁对象 */ : Q8 v9 x, {+ N1 A6 v. u: C* W
- __IO HAL_QSPI_StateTypeDef State; /* QSPI通信状态 */
3 O( [/ x; A- e% P9 H - __IO uint32_t ErrorCode; /* 错误代码 */
% A& ~$ H' Y+ b. q; F - uint32_t Timeout; /* 配置QSPI内存访问超时时间 */
- z' g! A( J% A' x+ A - }QSPI_HandleTypeDef;; ]4 e2 p0 d- w0 O- v
复制代码 3 P" U" k0 F! i& y. N
Instance:用于设置QSPI寄存器基地址,设置为QUADSPI即可,这个官方已经为我们做好了宏定义。
& q% u3 [% [. G0 hInit:用于设置QSPI的相关参数,QSPI_InitTypeDef结构体下面再进行详细讲解。7 d; X' z8 ~8 @4 {
pTxBuffPtr、TxXferSize和TxXferCount:分别用于设置 QSPI 发送缓冲指针、发送数据量和发送剩余数据量。$ M- Z K- C9 `5 R* B
pRxBuffPtr、RxXferSize和RxXferCount:分别用于设置接收缓冲指针、接收数据量和接收剩余数据量。/ m- _/ W9 f( o1 _
hmdma:用于配置相关的 DMA 参数。
) v1 t. j1 L7 {( t/ y4 d& ELock:用于分配锁资源,可选择 HAL_UNLOCKED 或者是 HAL_LOCKED两个参数。, X6 X6 ^# w1 q6 ~' x0 I* M) W
State:用于存放通讯过程中的工作状态。 n4 `9 y/ Z. S1 Q+ C! y( i! m
ErrorCode:通过该参数,用户可以了解到QSPI通讯过程中通信失败的原因。+ H! A& ], w4 r2 z9 a: I! T/ _- p
Timeout:用于设置超时时间。QSPI访问时间一旦超出 Timeout这个变量值,那么ErrorCode成员变量就会被赋值为HAL_QSPI_ERROR_TIMEOUT,表示操作超时。+ p9 G; C) g6 T8 G
下面重点来了解QSPI_InitTypeDef结构体的内容,其定义如下:
1 t, }9 w5 U# Q/ g- typedef struct! K, k/ p5 D1 Q" \) Y' \! T8 [
- {
% X* i8 P( C0 i2 N W! e+ F) p - uint32_t ClockPrescaler; /* 时钟预分频系数 */ , h) M* B0 Y: I% G- z- V( Z
- uint32_t FifoThreshold; /* 设置FIFO阈值级别 */ $ y' s- Q+ i7 W; u6 h
- uint32_t SampleShifting; /* 设置采样移位 */7 N6 j4 ?$ W' a; y- ?) I
- uint32_t FlashSize; /* 设置FLASH大小 */ 9 e5 Y; O z0 U% L3 v* b9 ]
- uint32_t ChipSelectHighTime; /* 设置片选高电平时间 */# v( G- F" c- Z: q7 M9 [/ w- ^
- uint32_t ClockMode; /* 设置时钟模式 */ - M3 u0 ^+ ~4 P ~( ^8 K
- uint32_t FlashID; /* 闪存ID,第一片还是第二片 */ 9 p8 T. \; ^5 ~$ t/ C$ Z6 l/ A
- uint32_t DualFlash; /* 双闪存模式设置 */
+ ^9 _ B8 K# G( \& m4 o) y! d - }QSPI_InitTypeDef;
复制代码
4 b& U1 p$ H0 a3 c) M0 Z# F4 RClockPrescaler:用于设置预分频系数,对应QUADSPI_CR寄存器的PRESCALER[7:0]位,取值范围是 0~255。仅可在 BUSY = 0 时修改该字段。8 V. a" ^$ g: Q1 n
FifoThreshold:用于设置FIFO阈值级别,可设置范围为 0~31,对应QUADSPI_CR寄存器的FTHRES[4:0]位。
; c! T5 R7 w1 h, xSampleShifting:用于设置采样移位,对应QUADSPI_CR寄存器的SSHIFT位。使用该位是考虑到外部信号延迟时,推迟数据采样。可以取值 QSPI_SAMPLE_SHIFTING_NONE(即0):不发生移位;QSPI_SAMPLE_SHIFTING_HALFCYCLE(即1):移位半个周期。在DDR模式下 (DDRM = 1),固件必须确保SSHIFT = 0。- n& G. D& x, M8 q/ ?2 b
FlashSize:用于设置FLASH大小,对应QUADSPI_ DCR寄存器的FSIZE[4:0]位,可设置的范围是:0到31之间的整数。FLASH 中的字节数= 2 [FSIZE+1]。在间接模式下,FLASH容量最高可达4GB(使用 32 位进行寻址),但在内存映射模式下的可寻址空间限制为256MB。9 m: g2 h; Y5 v5 A3 S- H Y6 R1 x
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周期数。
+ y& ]! a& e1 N. G8 ?* PClockMode:用于设置时钟模式,对应QUADSPI_DCR寄存器CKMODE位,指示 CLK在命令之间(nCS = 1 时)的电平,可以选择的参数是:QSPI_CLOCK_MODE_0(表示模式0)或者QSPI_CLOCK_MODE_3(表示模式3)。模式 0是:nCS为高电平(片选释放)时,CLK 必须保持低电平。模式3是:nCS 为高电平(片选释放)时,CLK 必须保持高电平。
) v; s0 k& Z" g" qFlashID:用于选择Flash1或者Flash2,单闪存模式下选择QSPI_FLASH_ID_1(表示Flash1)。
1 L$ t3 r i8 PDualFlash:用于使能双闪存模式,QSPI_DUALFLASH_DISABLE:禁止双闪存模式;QSPI_DUALFLASH_ENABLE:使能双闪存模式。对应QUADSPI_CR寄存器DFM位。) X4 B9 ~8 i `) a6 M7 ~
函数返回值:* _# x- g8 e; E0 \& w: X
HAL_StatusTypeDef枚举类型的值。. j- `" D4 P4 m! h4 C& u
注意事项:/ r) R: V9 N3 ^5 a9 U+ m
QSPI的MSP初始化函数HAL_QSPI_MspInit,该函数声明如下:3 _7 n3 \; K; l5 x2 ?
void HAL_QSPI_MspInit(QSPI_HandleTypeDef *hqspi);" g- [7 p) F; _5 q
HAL_QSPI_Command函数* @1 c; @- ^# r Y$ u$ t, F
QSPI设置命令配置函数,其声明如下:
& F. l5 H9 D9 v+ N3 T3 Q" ^HAL_StatusTypeDef HAL_QSPI_Command(QSPI_HandleTypeDef *hqspi,
5 d- @' R2 ]4 z1 s$ V# o3 cQSPI_CommandTypeDef *cmd, uint32_t Timeout);# G2 Z0 L4 P( q! W. Y
函数描述:8 P2 @# o3 U7 d* X
该函数用来配置QSPI命令。
5 D( R3 A9 M+ R W* M* i函数形参:! ?2 u8 A, u3 d- p( R7 h5 T) b
形参1是QSPI_HandleTypeDef结构体类型指针变量。
: s" B' f9 B! N* o形参2是QSPI_CommandTypeDef结构体类型指针变量,其定义如下:+ ]4 w$ ^/ d7 j8 k
- typedef struct0 j- K/ Z7 r/ y/ |
- {
8 t4 ~! m1 p5 a1 ~: I: H - uint32_t Instruction; /* 指令 */
, x" @# f/ ~, j. y L* m - uint32_t Address; /* 地址 */ l5 b: c# H8 E' _% T& C- v8 J& j
- uint32_t AlternateBytes; /* 交替字节 */
' H4 c( P, @9 P+ E/ ^ - uint32_t AddressSize; /* 地址长度 */ . K4 z1 b; q( T( O& _) J: L$ W! f
- uint32_t AlternateBytesSize; /* 交替字节大小 */
, |& Q- x; t4 X" ?$ h - uint32_t DummyCycles; /* 控指令周期数 */
# h( N! e; `# j5 ?2 ~5 E0 i0 e& b) E, p - uint32_t InstructionMode; /* 指令模式 */ 0 s! M- {8 L2 q6 r6 w! z& r
- uint32_t AddressMode; /* 地址模式 */ " N4 m: Q- l" Q" L& f: D2 B
- uint32_t AlternateByteMode; /* 交替字节模式 */ 8 w) R0 |0 p0 f! R: P, V* r
- uint32_t DataMode; /* 数据模式 */
) b0 s2 N5 _$ N) X* B" o3 K/ k - uint32_t NbData; /* 数据长度 */ 6 E/ ?8 M* ?# \, a9 a" t
- uint32_t DdrMode; /* 指定地址、备用字节和数据阶段的双数据速率模式 */
, {9 O# v. {* g3 y - uint32_t DdrHoldHalfCycle; /* 指定DDR模式下数据保持的周期 */
0 {3 I; z; q5 w! D% ^# }; Z - uint32_t SIOOMode; /* 指定发送指令仅一次模式 */ 1 E" y! X2 @8 W
- }QSPI_CommandTypeDef;
复制代码
: b6 @; u! r( M+ ?! ?& PInstruction:设置通信指令,指定要发送到外部QSPI设备的指令,指令表定义在norflash.h里。
1 D2 W' G/ b/ r0 C( {) f- YAddress:指定要发送到外部QSPI设备的地址,BUSY = 0 或 FMODE = 11(内存映射模式)时,将忽略写入该字段。在双闪存模式下,由于地址始终为偶地址,ADDRESS[0] 自动保持为“0”。2 f( ?; g. o ^" i" }
AlternateBytes:指定要在地址后立即发送到外部QSPI设备的可选数据。
+ @. d% W: W# M% s' a0 g9 HAddressSize:定义地址长度,可以是8位,16位,24位或者32位。
" a( ~! r4 {+ d: y" {AlternateBytesSize:定义交替字节长度,可以是8位,16位,24位或者32位。9 B, A7 x+ o' M
DummyCycles:定义空指令阶段持续周期,SDR和DDR模式下,指定CLK周期数(0~31)。, V" {6 ?; o0 \6 Z: X
InstructionMode:用于指定指令阶段模式,如下四种:# Y7 d2 T& p; r) K. a: g
QSPI_INSTRUCTION_NONE:无指令;4 Q; O/ F- h. c9 E' ]2 G$ ?
QSPI_INSTRUCTION_1_LINE:单线传输指令;
1 g* `4 n: b$ y6 gQSPI_INSTRUCTION_2_LINES:双线传输指令;' r* e2 k _/ s7 g y/ b1 w
QSPI_INSTRUCTION_4_LINES:四线传输指令。% c0 v& u7 B) H A4 ~
AddressMode:指定地址模式,如下四种:( s3 x+ \; {/ P
QSPI_ADDRESS_NONE:无地址;QSPI_ADDRESS_1_LINE:单线传输地址;QSPI_ADDRESS_2_LINES:双线传输地址;QSPI_ADDRESS_4_LINES:四线传输地址。: B) p% }1 I$ @( r" d: ~0 D' ^
AlternateByteMode:指定交替字节模式,如下四种:
6 x& s- j; g' F: v* s' h/ L" dQSPI_ALTERNATE_BYTES_NONE:无交替字节;
5 N! X$ R! M* i$ @% j3 a0 H4 M, K( {+ mQSPI_ALTERNATE_BYTES_1_LINE:单线传输交替字节;0 G0 B9 H' p2 H4 j8 s6 {) U0 |* _
QSPI_ALTERNATE_BYTES_2_LINES:双线传输交替字节;8 `* c0 m2 c* b: B+ |9 x
QSPI_ALTERNATE_BYTES_4_LINES:四线传输交替字节。( ^( M4 K( A, y7 h; ~2 O! Z
DataMode:指定数据模式,如下四种:0 b( P* K0 O2 _3 L
QSPI_DATA_NONE:无数据;QSPI_DATA_1_LINE:单线传输数据;QSPI_DATA_2_LINES:双线传输数据;QSPI_DATA_4_LINES:四线传输数据。
# D9 s' }/ O5 {+ V/ [3 M6 v0 gNbData:用于设置数据长度,在间接模式和状态轮询模式下待检索的数据数量(值 + 1)。对状态轮询模式应使用不大于 3 的值(表示 4 字节)。; F, r, K9 a9 K( ?1 y* J
DdrMode:为地址、交替字节和数据阶段设置 DDR 模式,可以选择的值是:
) n# K! f/ Y1 q( m. n5 r3 mQSPI_DDR_MODE_DISABLE:禁止 DDR 模式;
; K& n7 N3 Z: r; n! q2 S6 X% VQSPI_DDR_MODE_ENABLE:使能DDR 模式。
, q# _8 O7 M) x5 w; sDdrHoldHalfCycle:用于设置 DDR 模式下数据输出延迟 1/4 个 QUADSPI 输出时钟周期,可选值如下:* d5 A3 l) V! e8 ^! W' e" J
QSPI_DDR_HHC_ANALOG_DELAY:使用模拟延迟来延迟数据输出;1 n% `, g, J2 w# U% K
QSPI_DDR_HHC_HALF_CLK_DELAY:数据输出延迟 1/4 个 QUADSPI 输出时钟周期。$ \7 A- w1 s8 ^" ^! ~, \3 e! ?
SIOOMode:设置是否开启仅发送指令一次模式,可选值如下:$ s) @ G1 W8 k! I, g& M) ]% N/ j
QSPI_SIOO_INST_EVERY_CMD:在每个事务中发送指令;$ M# w9 }# r4 z9 b
QSPI_SIOO_INST_ONLY_FIRST_CMD:仅为第一条命令发送指令。
; O+ {% ^; b$ d; y: v- [形参3用于设置超时时间。
9 w0 u4 ? S0 B2 U0 F! V+ m函数返回值: E$ I$ }" P! `! Y7 d5 V
HAL_StatusTypeDef枚举类型的值。
$ W7 e7 @9 ?. `( k$ l7 Y; C1 AHAL_QSPI_Receive函数- `) h4 Y5 F0 X$ p, }; f( l0 }
QSPI接收数据函数,其声明如下:0 v m; j( |8 G) l/ E
HAL_StatusTypeDef HAL_QSPI_Receive(QSPI_HandleTypeDef *hqspi,
6 I: L, f$ W+ h& |' n' tuint8_t *pData, uint32_t Timeout);
, p& Z3 ]0 g9 h( r, I函数描述:
) u8 {, R. X" f5 N+ d该函数用来接收数据。
) Y% p2 Q+ ?/ w$ b l函数形参:- n$ G6 w/ T8 B# d8 w/ p+ W* |3 j3 b
形参1是QSPI_HandleTypeDef结构体类型指针变量。. S+ r5 K9 l: G! Y0 l$ u" H
形参2是uint8_t类型指针变量,存放接收数据缓冲区指针。
* X7 D. h9 E4 d! A形参3设置操作超时时间。
c$ R, H. P3 j, Z函数返回值:5 z: ]& r; c. i' n( P# e
HAL_StatusTypeDef枚举类型的值。
: y: F& d/ m F3 D) e! DHAL_QSPI_Transmit函数: i/ ^# V S* p- J C1 x) ^
QSPI发送数据函数,其声明如下:
, i& l4 H, R# x3 fHAL_StatusTypeDef HAL_QSPI_Transmit (QSPI_HandleTypeDef *hqspi,
" z8 X7 @1 O T. \, uuint8_t pData, uint32_t Timeout);
0 C) z* T' |1 e函数描述:
: b: V' j1 t; p: J5 r8 q) P该函数用来发送数据。
2 W. `6 [. M- F0 ?0 ?函数形参:
: ^% K! ~ q; m$ ]: O形参1是QSPI_HandleTypeDef结构体类型指针变量。
+ l+ n: m) [; f) K, E6 o形参2是uint8_t类型指针变量,存放发送数据缓冲区指针。7 r/ `( Y% u6 n, h
形参3设置操作超时时间。
0 r" T O7 e4 a函数返回值:
, w. M" x1 |% NHAL_StatusTypeDef枚举类型的值。- @# Y# w4 x: p, A& c& {( t7 j
QSPI初始化配置步骤- L- A; A1 H. C: e$ ]2 g
1)开启QSPI接口和相关IO的时钟,设置IO口的复用功能。, W# r4 r# Z/ S2 R7 G: L; j) t
要使用QSPI,肯定要先开启其时钟(由AHB3ENR控制),然后根据我们使用的QSPI IO口,开启对应IO口的时钟,并初始化相关IO口的复用功能(选择QSPI复用功能)。5 p4 G0 J6 b" b# U/ z! a- _
QSPI时钟使能方法为:$ w+ Z% e3 ?/ ~" O6 Y+ Z
__HAL_RCC_QSPI_CLK_ENABLE(); / 使能QSPI时钟 */
, K5 `: n8 Z' {' M% v这里大家要注意,和其他外设处理方法一样,HAL库提供了QSPI的初始化回调函数HAL_QSPI_MspInit,一般用来编写与MCU相关的初始化操作。时钟使能和IO口初始化一般在回调函数中编写。1 [6 M6 l6 t! j% L ~, |
2)设置QSPI相关参数。
9 I1 ?1 ~/ m' K6 j: q$ a7 f9 E, X& O此部分需要设置两个寄存器:QUADSPI_CR和QUADSPI_DCR,控制QSPI的时钟、片选参数、FLASH容量和时钟模式等参数,设定SPI FLASH的工作条件。最后,使能QSPI,完成对QSPI的初始化。HAL库中设置QSPI相关参数函数为HAL_QSPI_Init,该函数声明为:1 ~$ N' h* x5 g- V
HAL_StatusTypeDef HAL_QSPI_Init(QSPI_HandleTypeDef *hqspi);
. y( A! S9 L5 _) _9 @( }3 zQSPI_HandleTypeDef结构体这些成员变量是用来配置QUADSPI_CR寄存器和QUADSPI_DCR寄存器相应位,大家可以结合这两个寄存器的位定义和结构体定义来理解。: S' o- L G3 ~. m: H }0 E
对于HAL_QSPI_Init函数使用范例请参考后面33.3软件设置部分程序源码。
9 A8 f1 a' F, F( \' iQSPI发送命令步骤8 p4 K3 W) g+ t( Y- u( R
1)等待QSPI空闲。
' y' ^+ s- N, N3 C$ X# G' o. c在QSPI发送命令前,必须先等待QSPI空闲,通过判断QUADSPI_SR寄存器的BUSY位为0,来确定。
% Y+ Z0 H4 E2 W) O1 |2)设置命令参数。
8 a( h, n% N- k' c此部分主要是通过通信配置寄存器(QUADSPI_CCR)设置,将QSPI配置为:每次都发送指令、间接写模式,根据具体需要设置:指令、地址、空周期和数据等的传输位宽等信息。如果需要发送地址,则配置地址寄存器(QUADSPI_AR)。
* ]- u& z/ T( }$ F在配置完成以后,即可启动发送。如果不需要传输数据,则需要等待命令发送完成(等待QUADSPI_SR寄存器的TCF位为1)。! W9 V2 u& K- Z) e4 W& }3 V
在HAL库中上述两个步骤是通过函数HAL_QSPI_Command来实现,该函数声明为:) ^7 K; Q' {0 U& A/ T
HAL_StatusTypeDef HAL_QSPI_Command(QSPI_HandleTypeDef *hqspi,+ K G! ~* \- J' w- R
QSPI_CommandTypeDef *cmd, uint32_t Timeout);
" j' ^; l; q# k3 Z9 FQSPI读数据步骤
, g0 D, A P5 r* P" c1 P1)设置数据传输长度。
# y6 N& k! H( _- _7 _3 e9 o1 i通过设置数据长度寄存器(QUADSPI_DLR),配置需要传输的字节数。
2 |$ R" J/ }& j3 C/ l8 I2 e% b2)设置QSPI工作模式并设置地址。( C' P r9 \; N$ l
因为要读取数据,所以,设置QUADSPI_CCR寄存器的FMODE[1:0]位为01,工作在间接读取模式。然后,通过地址寄存器(QUADSPI_AR),设置我们将要读取的数据的首地址。 y5 {* z! h& S$ h/ _1 j
3)读取数据。
S5 d7 p% T9 E5 e在发送完地址以后,就可以读取数据了,不过要等待数据准备好,通过判断QUADSPI_SR寄存器的FTF和TCF位,当这两个位任意一个位为1的时候,我们就可以读取QUADSPI_DR寄存器来获取从FLASH读到的数据。
2 i w) `. V7 m: {最后,在所有数据接收完成以后,终止传输(ABORT),清除传输完成标志位(TCF)。3 @! B# v) y' B6 M: ?
HAL库中,读取数据是通过函数HAL_QSPI_Receive来实现的,该函数声明为:
+ z. W- w s: ZHAL_StatusTypeDef HAL_QSPI_Receive(QSPI_HandleTypeDef *hqspi,3 j! C; _7 V5 S% F- @5 _" S/ p- @
uint8_t *pData, uint32_t Timeout);
+ B* ^/ I- d: S在调用该函数读取数据之前,我们会先调用上个步骤讲解的函数HAL_QSPI_Command来指定读取数据的存放空间。
2 z8 G' F& v5 S5 `1 E% \QSPI写数据步骤+ V5 D3 C' M/ n0 m/ ~: z; O. L0 i
1)设置数据传输长度。
, g) \. r( S! @; G b6 l, c7 E9 }+ h通过设置数据长度寄存器(QUADSPI_DLR),配置需要传输的字节数。
( _$ s; V; \' c7 P4 L3 G2)设置QSPI工作模式并设置地址。. K- q* ^( ~. w% f
因为要读取数据,所以,设置QUADSPI_CCR寄存器的FMODE[1:0]位为00,工作在间接写入模式。然后,通过地址寄存器(QUADSPI_AR),设置我们将要写入的数据的首地址。
8 P0 y2 S6 t* @2 n! w1 d: Q B3)写入数据。
3 a+ U# t* A3 A在发送完地址以后,就可以写入数据了,不过要等待FIFO不满,通过判断QUADSPI_SR寄存器的FTF位,当这个位为1的时候,表示FIFO可以写入数据,此时往QUADSPI_DR写入需要发送的数据,就可以实现写入数据到FLASH。
k, E; z$ i8 s0 z$ d: G- o" h最后,在所有数据写入完成以后,终止传输(ABORT),清除传输完成标志位(TCF)。
( t. p% U0 }& o+ }在HAL库中,QSPI发送数据是通过函数HAL_QSPI_Transmit来实现的,该函数声明为:
' y5 t# I* i2 \/ `HAL_StatusTypeDef HAL_QSPI_Transmit (QSPI_HandleTypeDef *hqspi,
z6 Z9 U: E& s( o# O% ]9 E+ ouint8_t *pData, uint32_t Timeout);
8 `8 ~7 ~/ ~, \1 n* V8 \% S1 [5 X同理,在调用该函数发送数据之前,我们会先调用HAL_QSPI_Command函数来指定要写入数据的存储地址信息。
% C+ n/ c$ V9 f+ \: T* J+ L# P5 LFLASH芯片初始化步骤# [2 l6 p5 z& h+ ^0 c7 T) n
1)使能QPI模式。
+ x6 w1 s% Z& O4 q. b( x$ N& o6 q因为我们是通过QSPI访问W25Q128的,所以先设置W25Q128工作在QPI模式下。通过FLASH_EnterQPIMode指令控制。注意:在该指令发送之前,必须先使能W25Q128的QE位。1 j- N/ @0 }2 r7 F* C; P4 }- D: h7 B
2)设置4字节地址模式。
" F. t8 a5 E8 {. f5 v* n0 A$ f# GW25Q128上电后,一般默认是3字节地址模式,我们需要通FLASH_Enable4ByteAddr指令,设置其为四字节地址模式,否则只能访问16MB的地址空间。
. U4 E; f; M, c8 J. Y3)设置读参数。
' o) B% d" t/ A0 \' q7 @1 ~% Q$ P T这一步,我们通过FLASH_SetReadParam指令,将P[5:4]设置为11,以支持最高速度访问W25Q128(8个dummy,104M时钟频率)。2 z2 b9 [( ^. y0 B+ N5 X! \
36.3.2 程序流程图
4 X- V, |$ X8 m$ J; C
) w4 Y& q2 A+ @7 G' X, w2 P. u; Z. R: V
; I- [6 t- G9 P/ \1 j! n: ]8 w+ T$ Q1 q8 U7 Y& z
图36.3.2.1 QSPI实验程序流程图; @, I. I6 ]0 |; E: Q% |1 f- d
8 X& ^* o# J: N) W5 W2 g2 l36.3.3 程序解析
; T e% b6 R/ f1.QSPI驱动代码
* H, R6 T$ [! g+ |/ A这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。QSPI驱动源码包括两个文件:qspi.c和qspi.h。8 h( Z( T( o/ m6 N$ a: d. B
qspi.h头文件对QSPI相关引脚做了宏定义,该宏定义如下:2 r: r: t. f4 G( A8 x( Y) i
/* QSPI 相关 引脚 定义 */( c0 m$ ^- j5 @ F
- #define QSPI_BK1_CLK_GPIO_PORT GPIOB
# P; q% N+ ^% S* V - #define QSPI_BK1_CLK_GPIO_PIN GPIO_PIN_2# Z/ a( Q. j. g% Z. D" T
- #define QSPI_BK1_CLK_GPIO_AF GPIO_AF9_QUADSPI
8 g! g% O- L0 P - #define QSPI_BK1_CLK_GPIO_CLK_ENABLE()
* A! A! v0 M; X5 _. ~. q! E - do{ __HAL_RCC_GPIOB_CLK_ENABLE; }while(0) /* PB口时钟使能 */& }# z; b7 K& \# M
& v# z$ F9 ]; u/ g ~; i* O6 N- #define QSPI_BK1_NCS_GPIO_PORT GPIOB
1 L6 h* |+ n( R2 O, G - #define QSPI_BK1_NCS_GPIO_PIN GPIO_PIN_6& e2 a/ g+ n2 m: N* b8 C& \ `
- #define QSPI_BK1_NCS_GPIO_AF GPIO_AF10_QUADSPI b2 h& d, x% l) I3 V6 e3 T
- #define QSPI_BK1_NCS_GPIO_CLK_ENABLE() 7 W# K! h2 l* a2 G/ E
- do{ __HAL_RCC_GPIOB_CLK_ENABLE; }while(0) /* PB口时钟使能 */
/ U E8 o7 k- D- W* M
: h3 r1 a) A4 K* U z _- #define QSPI_BK1_IO0_GPIO_PORT GPIOD
4 d4 W2 `3 w. X1 P - #define QSPI_BK1_IO0_GPIO_PIN GPIO_PIN_11
6 |5 ?9 {; S- Q" {# o: K% l4 h6 g6 l - #define QSPI_BK1_IO0_GPIO_AF GPIO_AF9_QUADSPI
; f Q* a! \7 j x8 z/ C - #define QSPI_BK1_IO0_GPIO_CLK_ENABLE() # e w/ p/ X5 f0 Y% u
- do{ __HAL_RCC_GPIOD_CLK_ENABLE; }while(0) /* PD口时钟使能 */
6 J6 O9 A% i, r+ K* R$ y( H ?1 y* r - - `: ]* o* L9 `$ C
- #define QSPI_BK1_IO1_GPIO_PORT GPIOD
+ a6 w% b, W T3 |+ [* A' | - #define QSPI_BK1_IO1_GPIO_PIN GPIO_PIN_12
! I I9 Z5 ~) [0 g$ t' ^0 K# C) Q - #define QSPI_BK1_IO1_GPIO_AF GPIO_AF9_QUADSPI
& l( o; O' Y5 D, c7 s, q" H9 Z - #define QSPI_BK1_IO1_GPIO_CLK_ENABLE() $ t, E F2 l# E$ \0 o0 S
- do{ __HAL_RCC_GPIOD_CLK_ENABLE; }while(0) /* PD口时钟使能 */
% y/ F6 p, s* o& T; ^9 ? - 5 I7 b/ K& D5 x1 u$ z( @
- #define QSPI_BK1_IO2_GPIO_PORT GPIOD/ R. `; E% H, u7 _- @* l2 y% ~
- #define QSPI_BK1_IO2_GPIO_PIN GPIO_PIN_13& X; W! E. Q) C" F k9 u: I
- #define QSPI_BK1_IO2_GPIO_AF GPIO_AF9_QUADSPI+ j" W9 `! a+ @2 D& @, f% s9 M
- #define QSPI_BK1_IO2_GPIO_CLK_ENABLE()
+ y! N1 d. z3 M$ B& J1 k - do{ __HAL_RCC_GPIOD_CLK_ENABLE; }while(0) /* PD口时钟使能 */
5 D0 I" d5 d& ~0 K5 U; ~7 X
1 k7 M; ?2 n2 x3 p5 T- #define QSPI_BK1_IO3_GPIO_PORT GPIOE: \( A9 _- ^) P; [7 Q# z/ ^
- #define QSPI_BK1_IO3_GPIO_PIN GPIO_PIN_20 m- N8 X/ ^6 F; o# e$ B
- #define QSPI_BK1_IO3_GPIO_AF GPIO_AF9_QUADSPI6 Z3 S$ ~* e: n) G
- #define QSPI_BK1_IO3_GPIO_CLK_ENABLE() # z5 l; \, J* E
复制代码 + I3 ^2 v9 E, a# @7 }4 N' F
do{ __HAL_RCC_GPIOE_CLK_ENABLE; }while(0) /* PE口时钟使能 */$ W% g8 y6 I4 D/ {2 w+ d5 ~
注意这6个GPIO都是用到复用功能,对应引脚的复用功能情况请看《STM32H750VBT6.pdf》数据手册79页之后的端口复用功能表格。
7 h$ ] }$ I! O0 d+ Y/ }下面我们开始介绍qspi.c的程序,首先是QSPI接口初始化函数,其定义如下:1 l) L0 _7 S U7 t/ I: l
5 K% v! A% {4 h0 f
- /**
8 Z9 l+ a$ w' v - * @brief 初始化QSPI接口# ?% M, K W" O+ J) L! r
- * @param 无6 _6 ?! p3 y5 W B
- * @retval 0, 成功; 1, 失败.
v K4 I. r$ K! ?+ M - */3 d. X/ ?2 j5 U) g6 y
- uint8_t qspi_init(void)
! _% X9 J0 j( ~; @4 m0 L' c; [0 c - {
1 }0 C2 @1 S9 \: q - g_qspi_handle.Instance = QUADSPI; /* QSPI */
- f7 b3 J, s" k - /* QPSI分频比,BY25Q128最大频率为108M,所以此处应该为2,QSPI频率就为
# V V! P9 J% }; c' T9 ] w: R" _ - 220/(1+1)=110MHZ稍微有点超频,可以正常就好,不行就只能降低频率 */. }2 ?7 H% C! W( f
- g_qspi_handle.Init.ClockPrescaler = 1;
0 G3 ]6 M, o: I - g_qspi_handle.Init.FifoThreshold = 4; /* FIFO阈值为4个字节 */
% p Z1 ?# g, u% Y) @ - /* 采样移位半个周期(DDR模式下,必须设置为0) */
+ ^0 k, Z9 }. y5 v - g_qspi_handle.Init.SampleShifting = QSPI_SAMPLE_SHIFTING_HALFCYCLE;
m& W* \& N& d - /* SPI FLASH大小,BY25Q128大小为32M字节,2^25,所以取权值25-1=24 */, x' h3 n8 a' `& [8 I( Y m
- g_qspi_handle.Init.FlashSize = 25-1; ' R7 m5 R& ~9 h0 W9 R' q8 k5 H
- /* 片选高电平时间为3个时钟(9.1*3=27.3ns),即手册里面的tSHSL参数 */ & f8 Y5 N1 ^6 J5 _. e# l) _
- g_qspi_handle.Init.ChipSelectHighTime = QSPI_CS_HIGH_TIME_3_CYCLE; & ^/ @% u* B' ^( [* K1 H
- g_qspi_handle.Init.ClockMode = QSPI_CLOCK_MODE_3; /* 模式3 */
c# [4 R7 t: ]# i e- x6 |; F - g_qspi_handle.Init.FlashID = QSPI_FLASH_ID_1; /* 第一片flash */( R& v! U7 g1 n8 z$ N7 g9 F. c0 n
- g_qspi_handle.Init.DualFlash = QSPI_DUALFLASH_DISABLE; /* 禁止双闪存模式 */
0 b5 w% ? s3 X! } - if(HAL_QSPI_Init(&g_qspi_handle) == HAL_OK) 7 k! c5 ]7 p1 o. d
- {2 L) k2 N P# p8 h' N) `
- return 0; /* QSPI初始化成功 */5 p1 k9 I, e" T' c4 o e1 C
- }
) a$ I8 E* G5 p# B. n+ }& N - else
0 w8 a) H: p& B" Y5 Q. S# U - {$ W5 `$ ?; L. d: g7 I- `% o4 w
- return 1;
! l2 _7 k! a/ N5 P8 d' j: O2 _ - }
' m' _) y! X! Q: R z4 J% E - }
复制代码 这里我们需要注意的是,QSPI的时钟源在sys_stm32_clock_init函数中已经选择了PLL2R(我们设置为220MHZ),这里就不需要再选择时钟源了,只需要选择预分频系数就可以确定QSPI的时钟频率。时钟模式选择模式3,在未进行任何操作时 CLK 升至高电平。我们用单闪存模式,FlashID要选择QSPI_FLASH_ID_1。* Q/ n# B _" X" |
我们用HAL_QSPI_MspInit函数来编写QSPI时钟和IO配置等代码,其定义如下:2 |0 [# U! U5 r; f( b8 C8 k! V
$ I9 v: ~$ `+ W7 w( N1 k
- /**# _8 ?, A5 r# ?4 l( x C$ @
- * @brief QSPI底层驱动,引脚配置,时钟使能+ z E' }3 @4 g, t8 ?- a/ g/ l
- * @param hqspi:QSPI句柄& s. ^1 I6 @* Z$ R! g5 @& y9 F
- * @note 此函数会被HAL_QSPI_Init()调用; O5 m* v, e; a, F( W
- * @retval 0, 成功; 1, 失败.( u; I; q6 g7 s, r }! d
- */
, s+ ?/ k, E. ~" F" o1 L - void HAL_QSPI_MspInit(QSPI_HandleTypeDef *hqspi): z6 t1 f* ^8 ?2 T! y+ ~* g
- {+ P2 H, i/ G" m& H4 V( ?
- GPIO_InitTypeDef gpio_init_struct;! a: V2 a8 |8 o6 d, k$ D
- 6 L2 C/ U/ S/ I8 I) i
- __HAL_RCC_QSPI_CLK_ENABLE(); /* 使能QSPI时钟 */3 ?" ^8 H7 T/ j9 b3 F
- __HAL_RCC_GPIOB_CLK_ENABLE(); /* GPIOB时钟使能 */
9 S& `2 h3 L( t. |- m - __HAL_RCC_GPIOD_CLK_ENABLE(); /* GPIOD时钟使能 */8 N; w' x* l" Y3 ` D7 I/ Y8 F- _
- __HAL_RCC_GPIOE_CLK_ENABLE(); /* GPIOE时钟使能 */' S% c- x+ U9 V/ J* t
- / b9 g* i8 `* Z( b1 q, j( ~
- gpio_init_struct.Pin = QSPI_BK1_NCS_GPIO_PIN;, j# u! ^6 j0 n! T+ l2 ~% a
- gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用 */$ l% K$ `' ]- V& p" X9 H
- gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
# s2 |( d0 U9 b2 L* Q - gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; /* 高速 */
) z9 `! r3 M: x - gpio_init_struct.Alternate = GPIO_AF10_QUADSPI; /* 复用为QSPI */* K& y0 x& Y/ l0 _& h
- /* 初始化QSPI_BK1_NCS引脚 */
' M, `; u- c5 s - HAL_GPIO_Init(QSPI_BK1_NCS_GPIO_PORT, &gpio_init_struct);
& R; C1 ~: q$ D- j9 F8 B - . U/ |4 i7 ^% a! G5 s( k9 t; p
- gpio_init_struct.Pin = QSPI_BK1_CLK_GPIO_PIN;
+ ^! `, Z. ~+ x - gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用 */
( i5 |: i% a! Q* n3 k - gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */6 ? W9 }& o# L+ {& R5 _$ Y
- gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; /* 高速 */
/ u) ]. ^, M- |2 h9 E2 A4 x& X - gpio_init_struct.Alternate = GPIO_AF9_QUADSPI; /* 复用为QSPI */
- r! R4 C+ C3 j" A - /* 初始化QSPI_BK1_CLK引脚 */' [: v9 @& a3 I5 j8 c
- HAL_GPIO_Init(QSPI_BK1_CLK_GPIO_PORT, &gpio_init_struct); : P" f* L( N) t+ I& O' v9 C2 m! l
- 3 Y, f3 C0 `8 [4 R( n+ k6 x
- gpio_init_struct.Pin = QSPI_BK1_IO0_GPIO_PIN; k# x) j& z! X* N
- /* 初始化QSPI_BK1_IO0引脚 */* ]; h, b, K( j1 [: x
- HAL_GPIO_Init(QSPI_BK1_IO0_GPIO_PORT, &gpio_init_struct);
) G6 {8 X( V8 Q0 r; Q* k
: M- d& S% U: \5 y( M1 x- gpio_init_struct.Pin = QSPI_BK1_IO1_GPIO_PIN;
7 h+ z7 d) |4 J, _( F) k6 x - /* 初始化QSPI_BK1_IO1引脚 */1 c, P; R4 F2 k _7 c+ L
- HAL_GPIO_Init(QSPI_BK1_IO1_GPIO_PORT, &gpio_init_struct); 2 \* @. _' l* W" h$ ^
- g2 [# t9 T3 X) `
- gpio_init_struct.Pin = QSPI_BK1_IO2_GPIO_PIN;
! i) T8 w" ]# k; p - /* 初始化QSPI_BK1_IO2引脚 */
% c) R T o6 z- R - HAL_GPIO_Init(QSPI_BK1_IO2_GPIO_PORT, &gpio_init_struct); & T6 y! @7 j3 e8 l0 ^# u) [# {
- 4 I; `) U G. o6 X j6 M
- gpio_init_struct.Pin = QSPI_BK1_IO3_GPIO_PIN;
" l0 _* ^' H( Z - /* 初始化QSPI_BK1_IO3引脚 */. \: l# B' g# S
- HAL_GPIO_Init(QSPI_BK1_IO3_GPIO_PORT, &gpio_init_struct); w! a& c: y, Y# Y' q
- }" J7 a8 |% @1 K7 r( Y2 X6 U# z
复制代码 + _% R+ Y& B! L& P! z7 _
这里初始化的6个引脚全部都要配置为复用功能模式,以及使能QSPI和相应IO时钟。
, v$ w! [1 _; }' Q0 s接下来介绍QSPI发送命令函数,其定义如下:7 R0 y" s3 z) o6 o! A
T' F& [- h5 j: {! a
- /**) V8 n$ t7 F( o2 @) d2 Z5 q
- * @brief QSPI发送命令5 r. {: G+ D9 O8 y4 r
- * @param cmd : 要发送的指令8 @7 j5 {! @. a# g. B0 ^2 v7 ]
- * @param addr: 发送到的目的地址+ ~" W/ c2 a" |9 E1 }
- * @param mode: 模式,详细位定义如下:. w7 ~! b5 S! B' A) s2 _3 V" e
- * @arg mode[1:0]: 指令模式;00,无指令;01,单线传输指令;10,双线传输指令;11,四线传输指令.
6 o. I/ ^/ c& e/ T - * @arg mode[3:2]: 地址模式;00,无地址;01,单线传输地址;10,双线传输地址;11,四线传输地址.: [# O. ?1 D$ C
- * @arg mode[5:4]: 地址长度;00,8位地址;01,16位地址; 10,24位地址; 11,32位地址.
$ z P ~; f( s7 O( y8 k0 c - * @arg mode[7:6]: 数据模式;00,无数据; 01,单线传输数据;10,双线传输数据;11,四线传输数据.
; x% P; ?4 f ^" T6 U0 B - * @param dmcycle: 空指令周期数0 `' T3 {2 Z2 B8 r
- * @retval 无
1 C% W! K7 e) O4 l( F' l6 n - */* J# W- B$ C! D4 o/ c
- void qspi_send_cmd(uint8_t cmd, uint32_t addr, uint8_t mode, uint8_t dmcycle)
, i. |( x: ~: a - {1 g2 D0 L. ?5 k& l0 d
- QSPI_CommandTypeDef qspi_command_handle;1 d/ V$ S' }* X! u8 A& k/ e) p3 W3 H
- 2 ~6 U2 {' Q( `+ V; J" y' H
- qspi_command_handle.Instruction = cmd; /* 指令 */
5 v* H" d$ N" I4 o4 u1 r* M - qspi_command_handle.Address = addr; /* 地址 */; y, u5 [" ? O
- qspi_command_handle.DummyCycles = dmcycle; /* 设置空指令周期数 */
3 e2 m2 O2 h+ _8 Y, I9 `" [ - - Y: v* z+ g, w- q
- if(((mode >> 0) & 0x03) == 0)9 _( o' P9 x2 a
- qspi_command_handle.InstructionMode = QSPI_INSTRUCTION_NONE; /* 指令模式 */
9 l; N/ [% W: `+ N v - else if(((mode >> 0) & 0x03) == 1)
. ~" R- u) M7 U1 j - qspi_command_handle.InstructionMode = QSPI_INSTRUCTION_1_LINE; /* 指令模式 */
/ _% H P+ Z( J6 N7 u4 ~- e3 z( m- l - else if(((mode >> 0) & 0x03) == 2). `. I- c1 n. P j) S
- qspi_command_handle.InstructionMode = QSPI_INSTRUCTION_2_LINES;/* 指令模式 */
- L( R! Z* x5 f0 l4 t1 `1 b# k - else if(((mode >> 0) & 0x03) == 3)
7 @7 H8 D0 a: k( X5 N2 V, F( C5 P* u - qspi_command_handle.InstructionMode = QSPI_INSTRUCTION_4_LINES;/* 指令模式 */
( f' S8 Y9 \% u6 T" E
! K6 e8 n! u$ D, M$ X; z# ]0 n- if(((mode >> 2) & 0x03) == 0)
! ]; d6 J- p4 _0 u6 j, h) o - qspi_command_handle.AddressMode = QSPI_ADDRESS_NONE; /* 地址模式 */$ M7 ~: o$ ]% W( y- k1 V% ^
- else if(((mode >> 2) & 0x03) == 1)* N6 b: y& o: c( V# V8 v
- qspi_command_handle.AddressMode = QSPI_ADDRESS_1_LINE; /* 地址模式 */
$ q+ H4 {8 a9 t, ]& P2 D: u - else if(((mode >> 2) & 0x03) == 2)
6 ?5 c4 B0 Y. ~( N/ K% r' r" o - qspi_command_handle.AddressMode = QSPI_ADDRESS_2_LINES; /* 地址模式 */0 r/ v2 I: N7 _. R; ~& y/ x
- else if(((mode >> 2) & 0x03) == 3)) M, U. \" ?8 f) T$ \
- qspi_command_handle.AddressMode = QSPI_ADDRESS_4_LINES; /* 地址模式 */
/ k( T) |% _4 ^0 K. ?& |% b, M2 ^! p
* t! o* O& x; T! z9 I- if(((mode >> 4)&0x03) == 0)( u3 v8 P, ^, `1 ]9 l# e
- qspi_command_handle.AddressSize = QSPI_ADDRESS_8_BITS; /* 地址长度 */# q, p. A( I4 \# ~/ ?
- else if(((mode >> 4) & 0x03) == 1)& d% W0 g9 l( C4 v+ k
- qspi_command_handle.AddressSize = QSPI_ADDRESS_16_BITS; /* 地址长度 */
7 f6 [2 z' x* F4 Y, m - else if(((mode >> 4) & 0x03) == 2)5 N O g" A5 u
- qspi_command_handle.AddressSize = QSPI_ADDRESS_24_BITS; /* 地址长度 */: t, Y, u8 ]# r9 X: p
- else if(((mode >> 4) & 0x03) == 3)
* d: x) L* z7 k - qspi_command_handle.AddressSize = QSPI_ADDRESS_32_BITS; /* 地址长度 */2 I8 Q2 o, U4 ?6 ~0 M
; p% z1 q( Z; x0 B+ g. G6 V- if(((mode >> 6) & 0x03) == 0). u- \" I" h- L# x- q
- qspi_command_handle.DataMode=QSPI_DATA_NONE; /* 数据模式 */
z9 Y8 H- L8 ]/ {' V - else if(((mode >> 6) & 0x03) == 1)
6 U: D' C ]6 a3 K- V - qspi_command_handle.DataMode = QSPI_DATA_1_LINE; /* 数据模式 */
3 D) [# M) ?; ` - else if(((mode >> 6) & 0x03) == 2)/ K5 L* W1 V% O ~; i
- qspi_command_handle.DataMode = QSPI_DATA_2_LINES; /* 数据模式 */6 ^& K' m" f7 K [
- else if(((mode >> 6) & 0x03) == 3)$ {& O0 N5 M( ` I3 k
- qspi_command_handle.DataMode = QSPI_DATA_4_LINES; /* 数据模式 */4 ~0 N" B+ U& h& |* D& |7 i6 l
% l* r3 Y! D% ~* n y2 N- ?" N# V- qspi_command_handle.SIOOMode = QSPI_SIOO_INST_EVERY_CMD; /* 每次都发送指令 */# G! R) w. W: ^$ @) W& j
- qspi_command_handle.AlternateByteMode=QSPI_ALTERNATE_BYTES_NONE;/*无交替字节*/2 ~6 x3 s& _5 d" M# g
- qspi_command_handle.DdrMode = QSPI_DDR_MODE_DISABLE; /* 关闭DDR模式 */4 r0 _! s9 _2 n
- qspi_command_handle.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY;# b5 W; D, v( L# Y1 {
3 E6 }' n% }0 K k/ ~: {5 e* O- HAL_QSPI_Command(&g_qspi_handle, &qspi_command_handle, 5000);
4 g, Z+ g5 Q) J - }
6 k. l: X' x% i4 J; @, ]* V
复制代码 ! L' J; Q3 P! [7 G) N% T* i" o
该函数主要就是配置QSPI_CommandTypeDef结构体的参数,并调用HAL_QSPI_Command函数配置发送命令,是一个重要的基础函数。% M2 Z1 z: v: Z& t/ z0 N: \% ~; k
接下来介绍的是QSPI接收函数,其定义如下:2 z' U. X: t, b) e+ f8 u
0 z0 G; \* ]% b" R5 n9 G- /**/ F; f$ T2 N2 J- o* i* [/ P$ p4 T
- * @brief QSPI接收指定长度的数据
" U [, Y5 @ ?* ? - * @param buf : 接收数据缓冲区首地址
/ n; \' }5 k- |5 k; D1 q+ v3 z - * @param datalen : 要传输的数据长度) m7 b. c/ s, h) k# Q
- * @retval 0, 成功; 其他, 错误代码.) B+ \3 h( D& c
- *// H6 {! x0 b: D3 M/ l, L9 E9 w
- uint8_t qspi_receive(uint8_t *buf, uint32_t datalen)* e( [" }5 G8 l+ ^ O0 z$ T
- {
. \' |; E/ G/ g2 x% H - g_qspi_handle.Instance->DLR = datalen - 1; /* 配置数据长度 */
! C8 H7 c) u4 x4 X# n - if (HAL_QSPI_Receive(&g_qspi_handle, buf, 5000) == HAL_OK)
) K$ p% w& z( A - {8 w/ w3 F5 N E$ X. T% P! @5 B
- return 0;
# W2 f1 Y9 w8 }. X* s# X$ ~5 z - }
8 j2 _4 W v9 s+ A" h! w4 L - else4 h$ n) N2 W0 W2 t2 f6 q/ D
- {
l: H* ?, ^& u v+ V6 Q( g2 ], q - return 1;
4 l1 ]7 x' T0 Q# e0 ~ - }6 b# H; R: K1 M' v7 I6 O3 E
- }
复制代码
4 I9 B" {& i8 u$ E* A该函数首先把要接收数据的长度赋值到QUADSPI数据长度寄存器(QUADSPI_DLR)中,然后通过调用HAL_QSPI_Receive函数接收数据。/ G" J M Y: h& z/ p5 ^: }
接下来介绍的是QSPI发送函数,其定义如下:# ], T+ ~2 ~; M4 V! S
: i C3 n% U! S: @, u
- /**
* o+ F1 t" ` d7 |2 j- s - * @brief QSPI发送指定长度的数据! T, O' a7 z3 a, Z9 O" o+ G
- * @param buf : 发送数据缓冲区首地址
' I+ n- J, E! s% n; r9 ?$ p0 t% X - * @param datalen : 要传输的数据长度
& n! w E& I- ` - * @retval 0, 成功; 其他, 错误代码.
" @9 m+ W: ~9 | B - */
4 l! @: X5 H4 ?1 `4 A6 a- r5 h - uint8_t qspi_transmit(uint8_t *buf, uint32_t datalen)6 k V7 l+ W/ l+ [# U
- {9 i) x; m b( C: y, z
- g_qspi_handle.Instance->DLR = datalen - 1; /* 配置数据长度 */
/ h5 u# g* G8 I( P( _( ~ o t - if (HAL_QSPI_Transmit(&g_qspi_handle, buf, 5000) == HAL_OK)
0 L5 {; w9 B1 D( I. U - {
+ u& O$ c$ F- z( B6 d, _) O - return 0;- ]/ s; z2 ]) Q& o
- }% I2 g6 F, T1 w# P
- else! T- V3 D" f2 m- n7 \ F
- {6 F9 ^& Q6 d+ b& e4 u0 B+ x
- return 1;
3 q! w9 Y ?6 M - }8 I2 T2 H5 H6 W/ y: ~' s5 M
- }
复制代码
7 V8 g. A0 c: j1 x3 {0 S该函数首先把要发送数据的长度赋值到QUADSPI数据长度寄存器(QUADSPI_DLR)中,然后通过调用HAL_QSPI_Transmit函数发送数据。
0 ]* f* _- C! h A$ \6 e2 B最后要介绍的一个函数是等待状态标志函数,其定义如下:& }6 j" |+ v! c6 I' N; K8 A' N: [2 @
& e0 D6 c J- n2 _; Q8 E M- /**. ^& _% t6 a U; R, X! `
- * @brief 等待状态标志4 K7 o& b# `9 ?2 e
- * @param flag : 需要等待的标志位
, I3 P2 X2 w0 s) X) L, ]& J+ z9 d - * @param sta : 需要等待的状态! }8 X1 D7 q. p4 @9 i5 X O
- * @param wtime: 等待时间2 B1 S" |' g- T# h! ~9 @& ]# a1 _
- * @retval 0, 等待成功; 1, 等待失败.3 W8 E+ V2 f$ ^* i
- */# U' d; g4 N$ R% l/ I
- uint8_t qspi_wait_flag(uint32_t flag, uint8_t sta, uint32_t wtime)& |& r) D1 b$ I/ G* H- {; `& \" K
- {
& A2 N* _( k) C9 h2 f' y0 f. B - uint8_t flagsta = 0;( p$ X3 ^, h" X, q0 E
- . |% m4 T5 C" z6 ~0 o
- while (wtime)$ t1 z8 u: M. d" `+ F& L
- {
# ]9 s+ h7 p& s - flagsta = (QUADSPI->SR & flag) ? 1 : 0; /* 获取状态标志 */4 J- k# U: w( U ^! @8 p* Q
- 7 S9 c! d* w$ `
- if (flagsta == sta)break;6 g, f- l$ p4 y. U' i
- ) \1 L/ W4 g2 u; m. _
- wtime--;/ _) J* ?0 q- R7 w; ^* L# Y
- }# |, b9 F! X% ]) G+ I
- 1 f0 N5 t' {2 r( _1 z7 `% d
- if (wtime)return 0;
: X1 D9 d7 H6 j; W - else return 1;& V# {5 Y2 i* B" z
- }
复制代码
! y9 Q/ s( L9 P# Z$ }$ S: x1 C该函数可以设置一段时钟等待QUADSPI状态寄存器(QUADSPI_SR)的任意位为0,或者为1。然后通过返回值,判断等待是否成功,0表示等待成功;1表示等待失败。( l- F! [7 X0 V4 I) k! o# r
2. NORFLASH驱动代码# p* V0 p2 |* M: m7 @
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。NORFLASH驱动源码包括两个文件:norflash.c、norflash.h、norflash_ex.c和norflash_ex.h。, t! C* X( M7 }" s* R& b1 J
因为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数据写入。; H! E. I: ?* F, l
由于这部分代码量会比较多,这里就不一一贴出来介绍。介绍几个重点,其余的请自行查看源码。首先是norflash.h头文件中,我们做了一个FLASH芯片列表(宏定义),这些宏定义是一些支持的FLASH芯片的ID。接下来是FLASH芯片指令表的宏定义,这个请参考FLASH芯片手册比对得到。norflash_ex.h头文件只是一些函数声明,就不介绍了。: J5 X: m3 c( ?5 F6 [
下面介绍norflash.c文件几个重要的函数,首先是NOR FLASH初始化函数,其定义如下:! |0 Z, w- {: p. G7 h3 O
+ U ]; d+ g% m: |" I- [* U& z- /**8 |3 q, D" `; e% Q( I
- * @brief 初始化SPI NOR FLASH3 _" }$ C$ Q. z; M- c5 t6 k2 D3 u
- * @param 无% U; R% x, ?4 w2 B) O: f" W* f! ^
- * @retval 无
' Y8 X( s/ {5 o' W- S - */4 a3 |: W8 I' z. o
- void norflash_init(void): a0 n8 M* M) q D+ t5 G w
- {
) g, M2 ?* Y: ~* Z# p' f: R: `& j- Y - uint8_t temp;
+ A, m0 S* X3 B& w - qspi_init(); /* 初始化QSPI */1 V, o( ^9 B; R; c
- norflash_qspi_disable(); /* 退出QPI模式(避免芯片之前进入这个模式,导致下载失败) */7 c' w5 p6 h3 t/ B9 u o6 \7 q
- norflash_qe_enable(); /* 使能QE位 */* X. P! h% ~! l9 X8 O
- g_norflash_type = norflash_read_id();/* 读取FLASH ID. */, C' ?! V. _" I6 p. B
) _* P$ l8 h& _ Z7 o- if (g_norflash_type == W25Q256) /* SPI FLASH为W25Q256, 必须使能4字节地址模式 */
. e$ ~) d, E/ y2 s6 H9 a% {$ Q& ? - {
3 r5 x# }/ x% m) Q+ h - temp = norflash_read_sr(3); /* 读取状态寄存器3,判断地址模式 */6 i' [2 F+ V; ^2 f
- / h! j* }0 A5 q" ^) o4 f0 K
- if ((temp & 0X01) == 0) /* 如果不是4字节地址模式,则进入4字节地址模式 */
+ e9 \2 A! Z4 W' v4 F0 m9 z( ~ - {2 _: @7 e2 K: f- D1 M/ q
- norflash_write_enable(); /* 写使能 */1 c' T! W1 D2 ^; A4 E1 K- t @
- temp |= 1 << 1; /* ADP=1, 上电4位地址模式 */5 n* \# m, P+ a+ K. v6 Y( \# }
- norflash_write_sr(3, temp); /* 写SR3 */
) h$ j: o0 v. O0 f& C. r; v6 ^ - 7 e' ~+ E1 c6 T$ C7 j
- norflash_write_enable(); /* 写使能 */
& M ?: f$ s* |( A9 A - /* QPI,使能4字节地址指令,地址为0,无数据_8位地址_无地址_单线传输指令,- `6 d3 p6 P' P7 J6 _$ b2 G* h
- 无空周期,0个字节数据 */
3 {- k% A1 Y1 d6 U- s - qspi_send_cmd(FLASH_Enable4ByteAddr, 0, (0 << 6) | (0 << 4)
6 s0 P7 C1 Z9 j - | (0 << 2) | (1 << 0), 0);
6 }+ T0 u0 t b& F) j# M) d# z# p' | - }
" F' W( u% K& o1 i+ | - }
/ X: k$ g( h. u( G2 Z3 v - //printf("ID:%x\r\n", g_norflash_type);
- a. F) d. G* ~* v# l' }. |4 l - }
复制代码 2 s, W+ V: ?9 p9 u+ R4 K
该函数用于初始化NOR FLASH,首先调用qspi_init函数,初始化STM32H750的QSPI接口。然后退出QPI模式(避免芯片之前进入这个模式,导致下载失败),使能FLASH的QE位,使能IO2/IO3。最后读取FLASH ID,如果SPI FLASH为W25Q256,还必须使能4字节地址模式。调用本函数在初始化完成以后,我们便可以通过QSPI接口读写NOR FLASH的数据了。- `3 u$ P9 B* V2 v; k4 V, L$ R. Y; f
接下来介绍读取SPI FLASH函数,其定义如下:3 v& O6 a) ? |# a# \) K
: a& q) {# b/ a; s& @4 R- /**& A$ i/ e ?! R% t2 ]$ j# l3 s
- * @brief 读取SPI FLASH,仅支持QSPI模式& c% A* ?# h8 |6 E. [7 G& x- W! y
- * @note 在指定地址开始读取指定长度的数据
* @6 d, f* x$ p( E. r - * @param pbuf : 数据存储区+ o" ]6 S1 z6 J; l) A) C
- * @param addr : 开始读取的地址(最大32bit)% {& {; I% M1 ^" i7 E
- * @param datalen : 要读取的字节数(最大65535)8 o7 y0 C6 K6 h. Y' H; T
- * @retval 无* v f3 r1 S0 y+ y8 f) {, Y* k- V
- */
2 o @+ T; r) x6 L( c - void norflash_read(uint8_t *pbuf, uint32_t addr, uint16_t datalen)) T& P; D* V; o% R" _9 K
- {9 d" i. q K- I! c$ r
- /* QSPI,快速读数据,地址为addr,4线传输数据_24/32位地址_4线传输地址_1线传输指令,
% s4 e" b; s, t5 k! } - 6空周期,datalen个数据 */* v8 U: B/ y% e; k0 K
- qspi_send_cmd(FLASH_FastReadQuad, addr, (3 << 6) | (g_norflash_addrw << 4)
& S3 i* a/ g7 s" ^$ R) {& h. U - | (3 << 2) | (1 << 0), 6);
& O s5 c$ N$ j0 s& n - qspi_receive(pbuf, datalen);
: x: N8 c( `* V( w8 i% z( l4 ]5 d/ i - }
/ ?* }$ U9 e, N8 F1 V2 O% x, e+ @
复制代码
' G+ d9 [ W Y% s) V ~该函数用于从NOR FLASH的指定地址读出指定长度的数据,由于NOR FLASH支持以任意地址(但是不能超过NOR FLASH的地址范围)开始读取数据,所以,这个代码相对来说就比较简单了,通过qspi_send_cmd函数,发送FLASH_FastReadQuad指令,并发送读数据首地址(addr),然后通过qspi_receive函数循环读取数据,存放在pbuf里面。; g) r$ Z' `0 P
接下来,我们介绍写入NOR FLASH函数,其定义如下:
- S1 h/ C, @+ }0 Q) @3 M/ L
( e; a' y7 c: u7 y4 J1 ?0 W& i* i- /**
8 \% t6 r1 H y! i - * @brief 写SPI FLASH
0 D3 X0 [. R- ?2 W+ H9 X* f - * @note 在指定地址开始写入指定长度的数据 , 该函数带擦除操作!
- A; I- C4 q: H9 u - * SPI FLASH 一般是: 256个字节为一个Page, 4Kbytes为一个Sector, 6 `( t- `! e& F% ]+ D4 ?- H5 Y+ V0 P
- 16个扇区为1个Block
; [$ K. b4 G8 e7 X2 |4 W) T - * 擦除的最小单位为Sector.
T% i, I) i# o2 I8 X - * @param pbuf : 数据存储区
+ u$ I/ r0 Q3 Z$ X/ B$ k! u+ F8 q - * @param addr : 开始写入的地址(最大32bit)/ G6 u$ L6 S6 e* \
- * @param datalen : 要写入的字节数(最大65535)
/ H# E8 O% q& R& U - * @retval 无
2 h/ _( v/ D8 ~ - */
* q! F5 ?. q. p# j - uint8_t g_norflash_buf[4096]; /* 扇区缓存 */
" b! X, D: k4 _# r3 N9 S5 s
3 J5 M; ^. z/ g3 I# U3 o- void norflash_write(uint8_t *pbuf, uint32_t addr, uint16_t datalen)& i- H* m5 V/ a$ f, d4 k4 A
- {
: F5 ] h. D* N+ n g. k! r - uint32_t secpos;, k2 U8 a. F( `. ?; B/ w: u
- uint16_t secoff;; N( z! L. n* ^ O5 T2 p
- uint16_t secremain;
9 H9 y9 f0 Q2 M- v( U - uint16_t i;, f, ?( ~' u' b' Y! }% C# c
- uint8_t *norflash_buf;
3 }' H* Q) ]0 E2 ^ - $ Z& Y. W( U5 U9 l5 g' H& \
- norflash_buf = g_norflash_buf;9 w+ T# }2 w1 S7 o) I
- secpos = addr / 4096; /* 扇区地址 */
4 Z6 A2 N8 `& X/ D) q$ p+ D. i - secoff = addr % 4096; /* 在扇区内的偏移 */4 N# _. n' \0 K/ g4 P; D' F4 ~+ D7 \
- secremain = 4096 - secoff; /* 扇区剩余空间大小 */; D6 I* T5 }) I9 W$ `
5 a5 p% T2 L5 ~: b! h1 g$ h- //printf("ad:%X,nb:%X\r\n", addr, datalen); /* 测试用 */2 o# _6 n/ _5 ]* a
- if (datalen <= secremain)5 | c: p/ P9 X
- {3 h D* t- o: I& y5 E C( W. J( j1 e
- secremain = datalen; /* 不大于4096个字节 */
0 z( l1 \, m. P% ^6 k0 a E# G - }( U$ V2 t, _) G5 Y. `! U6 A. ?
j; r! @: [* j" B+ g- while (1)
4 C, g2 w" ~* n a% q; T1 f - {
: t6 M% Q- M- i) P - norflash_read(norflash_buf, secpos * 4096, 4096); /* 读出整个扇区的内容 */
" }7 o9 x7 u2 Q2 `7 V - 7 a; t' H! z9 A$ D" ~2 _8 f3 b% X7 y
- for (i = 0; i < secremain; i++) /* 校验数据 */
2 D% B, a N* |+ `' U0 W1 o4 J - {4 e3 p; X6 Z. u- G8 U5 g8 m
- if (norflash_buf[secoff + i] != 0XFF)
. D9 U+ |5 e+ w! G3 k - {
+ S( y3 V/ s& P" m9 i; z - break; /* 需要擦除, 直接退出for循环 */) G+ c! v) m: u9 w
- }: h: Q, ?8 V0 Q' P
- }
8 S) n: K/ d: j8 F; q k - ( C; ~- ^8 {2 |) _3 @$ D' A
- if (i < secremain) /* 需要擦除 */. z2 F; o. z2 E6 R& Z
- {6 k" [3 z" g" m% i& t
- norflash_erase_sector(secpos); /* 擦除这个扇区 */( T4 f. n0 Q8 @4 X
- for (i = 0; i < secremain; i++) /* 复制 */+ o, W s; u, h+ Z
- {
9 t9 v5 a Q8 I& e9 ]4 N4 h - norflash_buf[i + secoff] = pbuf<i>;
* Q1 A. ~) ]6 f# m7 G+ H |' G - }6 `: ]5 a; I" t5 b
- /* 写入整个扇区 */
% X/ l# g* H# p" A7 f" r - norflash_write_nocheck(norflash_buf, secpos * 4096, 4096); * Q2 [' ?7 b% s) q
- }& L; N) R4 B! g3 D. ~ W' d
- else /* 写已经擦除了的,直接写入扇区剩余区间. */
4 T4 D- s9 C# U. G+ `* ] - {- j6 t, D; O7 S P8 ~* D" T9 n
- norflash_write_nocheck(pbuf, addr, secremain); /* 直接写扇区 */. O- U$ @# x& w: N% }+ a
- }, _; B( h+ O. M
- ; ^( E. r0 N+ i; R- m7 w) l+ x5 b, Z( o
- if (datalen == secremain)' z# d0 N7 t4 u- i' E3 u$ P9 u0 N1 k
- {
& h* Y4 E- z! H! o+ i - break; /* 写入结束了 */
& I1 N1 Q& o/ c' C - }
O! C3 ], V3 n# T" \, v- o - else /* 写入未结束 */
3 z4 j0 P( b: n F - {" ]+ v) v! T( M$ {! C' e
- secpos++; /* 扇区地址增1 */
, u5 s d$ s5 `: s, O* ^6 ^ - secoff = 0; /* 偏移位置为0 */
, L+ F9 @3 k; L, s& C$ p - ; U' U$ Y# {% D$ S; q. I/ A; u
- pbuf += secremain; /* 指针偏移 */5 g5 k) f3 F( m i7 \
- addr += secremain; /* 写地址偏移 */1 o; r" z4 V+ |, g7 o
- datalen -= secremain; /* 字节数递减 */
( O* c- A3 L7 [1 ^0 r3 q
" `+ L4 J1 D* H6 }7 ]8 g- if (datalen > 4096)/ ]. }, D5 c1 a/ {/ t
- {* k# w# N B; p
- secremain = 4096; /* 下一个扇区还是写不完 */
3 w8 M. u" F2 u# ^ a) D1 r" J - }0 d }- n/ q9 t& Q! P' [
- else+ ~) M3 i) X4 {8 n
- {
9 @# h; c5 ^7 N* s2 E- ~% ~4 m - secremain = datalen; /* 下一个扇区可以写完了 */0 o, ~- @; p: W! d9 Y
- }
6 V. A7 ~0 x" e - }, c M& u. g" K/ Y0 e+ t: [
- }
5 H4 v- p9 O) i% m( b1 |! ]- H, G - }</i>
复制代码
6 {/ a. `% @) g. ]该函数可以在NOR FLASH的任意地址开始写入任意长度(必须不超过NOR FLASH的容量)的数据。我们这里简单介绍一下思路:先获得首地址(addr)所在的扇区,并计算在扇区内的偏移,然后判断要写入的数据长度是否超过本扇区所剩下的长度,如果不超过,再先看看是否要擦除,如果不要,则直接写入数据即可,如果要则读出整个扇区,在偏移处开始写入指定长度的数据,然后擦除这个扇区,再一次性写入。当所需要写入的数据长度超过一个扇区的长度的时候,我们先按照前面的步骤把扇区剩余部分写完,再在新扇区内执行同样的操作,如此循环,直到写入结束。这里我们还定义了一个g_norflash_buf的全局数组,用于擦除时缓存扇区内的数据。
! P2 [4 |/ }1 e1 w0 Knorflash.c文件我们就介绍这三个函数,其他请大家自行查阅。下面再介绍norflash_ex.c文件的几个重要函数。首先是QSPI接口进入内存映射模式函数,其定义如下:$ e& ?6 @8 a$ ]) U# Z* F
1 |1 y. h9 f2 Y- /**
1 M( _2 y l( h+ Y7 R - * @brief QSPI接口进入内存映射模式' s( ]5 f9 B A7 w! ~
- * @note 调用该函数之前务必已经初始化了QSPI接口
6 u2 f+ n3 |, ^ - * sys_qspi_enable_memmapmode or norflash_init
8 j! n5 D9 B% D9 B0 K4 Q - * @param 无
. O: _+ R0 T6 e# E+ R) H+ x% t - * @retval 无
5 h3 _6 i; ]1 m4 M$ r$ Z3 | - */
1 F5 h/ P7 e+ k - static void norflash_ex_enter_mmap(void)
+ L9 B9 B2 r: v9 V - {8 D, B* N+ o; f
- uint32_t tempreg = 0;$ n+ [5 w% K* C% a# w) i! W! B# W7 }! M
; v: M+ [8 `8 Z2 l! e# R0 m- /* BY/W25QXX 写使能(0X06指令) */
$ H# J, o: r9 W1 u - while (QUADSPI->SR & (1 << 5)); /* 等待BUSY位清零 */8 N/ f2 M( z, F% z
& J& j/ }' y" l. G) ^- QUADSPI->CCR = 0X00000106; /* 发送0X06指令,BY/W25QXX写使能 */
, K6 D8 l d, i7 z, y - 3 B: K, K, C1 y% k( B
- while ((QUADSPI->SR & (1 << 1)) == 0); /* 等待指令发送完成 */' t0 [+ ~- c/ F) M6 `5 W3 c; _+ N
2 O o2 B4 `5 u( F5 P- QUADSPI->FCR |= 1 << 1;
: w* i+ A4 o( |+ ]5 j9 u - ! N7 l; n+ ` W# k, W
- if (qspi_wait_flag(1 << 5, 0, 0XFFFF) == 0) /* 等待BUSY空闲 */5 \$ d3 U3 u+ y. L3 x o
- {
% |6 [# s+ Z% i! R8 p& [+ U9 ? - tempreg =0XEB;/*INSTRUCTION[7:0]=0XEB,发送0XEB指令(Fast Read QUAD I/O)*/+ Y H# m+ v9 F7 m |9 t
- tempreg |= 1 << 8; /* IMODE[1:0]=1,单线传输指令 */2 `. q$ H1 U. C& N- Y
- tempreg |= 3 << 10; /* ADDRESS[1:0]=3,四线传输地址 */ 8 x% P/ [! q: \6 o9 A
- tempreg |=(uint32_t)g_norflash_addrw<<12;/*ADSIZE[1:0]=2,24/32位地址长度*/
: z& L+ R! N9 [; T* D7 L2 r - tempreg |= 3 << 14; /* ABMODE[1:0]=3,四线传输交替字节 */
# f5 Y' r5 Y0 m. r2 R - tempreg |= 0 << 16; /* ABSIZE[1:0]=0,8位交替字节(M0~M7) */& X6 b* O& V0 l1 o, k- X
- tempreg |= 4 << 18; /* DCYC[4:0]=4,4个dummy周期 */
7 F2 p$ b" Z# x/ P6 j5 P% W - tempreg |= 3 << 24; /* DMODE[1:0]=3,四线传输数据 */
; z1 U! F0 o( f- | H - tempreg |= 3 << 26; /* FMODE[1:0]=3,内存映射模式 */
1 Z! ^% Z, f. f" [ } - QUADSPI->CCR = tempreg;/* 设置CCR寄存器 */
. g6 Q# n/ O6 Q. {$ Y+ c - }- N$ \- Y1 w1 b$ r! {
- INTX_ENABLE(); /* 开启中断 */
. j# G3 E' H/ A$ f) s% S - }
复制代码 1 g9 V4 y& V& Z/ l1 S; f
该函数使QSPI接口进入内存映射模式。内存映射模式:外部 FLASH 映射到微控制器地址空间,从而系统将其视作内部存储器。
) \. m! p0 m; [1 ?接下来要介绍的是QSPI接口退出内存映射模式函数,其定义如下:
# p6 Y7 z, d) b3 k4 f* m7 h0 A: Q0 x$ `, f
- /** x, \4 ^$ u( y" Y* G/ H! Y
- * @brief QSPI接口退出内存映射模式; v) H; V' q6 d9 I
- * @note 调用该函数之前务必已经初始化了QSPI接口
f" b! F1 _/ o' Q+ I) b2 ~ - * sys_qspi_enable_memmapmode or norflash_init6 r! L' I' u& Y& X k R
- * @param 无4 Z& l5 z- X& _* W% R* {0 Q) A
- * @retval 0, OK; 其他, 错误代码
. E# G9 r) h8 ?. b' l - */9 a I1 g, x/ r! c
- static uint8_t norflash_ex_exit_mmap(void)
# m: P2 N! F8 p7 I& |0 J - {
# i1 w6 n$ F9 i3 e ^( w - uint8_t res = 0;8 ]# Z( w7 ]% ?0 i5 ~( e9 z
- / ?! K: o- L8 p- I% Z' E- D
- INTX_DISABLE(); /* 关闭中断 */
! }4 f! `& R+ N$ V+ ~ - SCB_InvalidateICache(); /* 清空I CACHE */
7 S1 G& z# ^! ?2 o+ B+ ~ - SCB_InvalidateDCache(); /* 清空D CACHE */* } \" W7 T: r
- QUADSPI->CR &= ~(1 << 0); /* 关闭 QSPI 接口 */$ `0 ]( H. ], d9 ^" ^- t) W0 ~! V/ g
- QUADSPI->CR |= 1 << 1; /* 退出MEMMAPED模式 */
; K# I: G0 e) \ - res = qspi_wait_flag(1 << 5, 0, 0XFFFF); /* 等待BUSY空闲 */
9 z0 K% q6 P, q% D( U/ o. J/ I- P; k - 0 ^9 d+ u( T' U$ K8 K' i" e
- if (res == 0)
1 E2 V+ Y5 ]" U" [& [! Y/ z% m: v0 a8 z - {
4 c6 ^! B+ C- G7 E0 k$ O; x1 l q - QUADSPI->CCR = 0; /* CCR寄存器清零 */
! o1 K; Q8 w0 A - QUADSPI->CR |= 1 << 0; /* 使能 QSPI 接口 */
, A+ y, Q X6 P - }" F! l- m( e' q- h
- 8 m2 H. c1 H4 e! T7 G" g
- return res;( P, r( m# n* f/ U
- }
复制代码
5 ^ D7 U P( b- s$ |% V) ^* W该函数使QSPI接口退出内存映射模式。norflash_ex_enter_mmap和norflash_ex_exit_mmap是成对存在的函数,也是norflash_ex.c文件中最重要的函数。
8 z: m C8 ~& d" A' v- \接下来介绍QSPI FLASH写入数据函数,其定义如下:
' G) E. W; }' |/ ^) v/ n! Z) {! d; q% [
- /*** q' s0 v, n2 k- q, o" ]9 s4 Q- l
- * @brief 往 QSPI FLASH写入数据6 s& h; d V9 g3 U) H
- * @note 在指定地址开始写入指定长度的数据
8 M3 |1 f& j& F+ ?8 ~, Q - * 该函数带擦除操作!% M8 V! P. [ W
- * @param pbuf : 数据存储区
# `% O7 d9 K: i. t; C. a6 @ - * @param addr : 开始写入的地址(最大32bit)
1 }; l. L0 j* l0 C1 o$ r8 n - * @param datalen : 要写入的字节数(最大65535)
" _5 M# p- u9 Z( h - * @retval 0, OK; 其他, 错误代码, m) n- Z) U: P: {/ A* P; ]
- */* e% g% {' \1 Z( {1 Z/ i9 g: q0 ]
- uint8_t norflash_ex_write(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
3 S& M3 X3 n' ^: o - {+ V+ S- C. i5 p! B, e# r# j0 S( j
- uint8_t res = 0;/ |/ Q4 S4 d+ n& ]$ x5 Q- d7 T' b
- res = norflash_ex_exit_mmap(); /* 退出内存映射模式 */% V' Z @5 F) `, R T; K
- $ ~ \& B Y# I. y& p5 L; P
- if (res == 0)2 I% H* f$ W% C, ]2 `) m
- {
8 C! J+ C) `8 ]7 Z2 K3 { - norflash_write(pbuf, addr, datalen);2 p( o- h! O' m( h
- }
5 v, e4 @" j9 m3 P- ^* Q; F, y
`4 p0 a7 D: c- norflash_ex_enter_mmap(); /* 进入内存映射模式 */
: L- o/ Y% s& e( a* m5 L% b" p - return res;5 ^. f7 x( V! r# }8 P/ T
- } i4 e, z1 c3 c
复制代码 0 i: [( w9 H" e# _9 h
因为STM32H7不支持QSPI接口读时写,所以往 QSPI FLASH写入数据前,需要先调用norflash_ex_exit_mmap函数退出内存映射模式。退出内存映射模式,CPU就不会在QSPI FLASH里读取程序指令,即避免了QSPI接口读(指令)时写。写好后,再进入内存映射模式。该思路也就是norflash_ex_write函数的操作过程。
( f8 g d; L8 v r+ j! T接下来介绍从QSPI FLASH读取数据函数,其定义如下:
2 i7 z1 B# b( v4 @& i! h
$ ?2 a% @: O9 B0 y- /**; Z6 G4 i% ^3 D; z) S& m* t
- * @brief 从 QSPI FLASH 读取数据
) h3 Q+ g* O3 w- Y$ W' J! a$ @ - * @note 在指定地址开始读取指定长度的数据(必须处于内存映射模式下,才可以执行)/ V. e3 ]2 M3 t9 S; G! r
- *; W$ Z" j9 P ?) j- y5 {* |. \; N
- * @param pbuf : 数据存储区; e' C% J$ C; }( T$ s W- t! L
- * @param addr : 开始读取的地址(最大32bit)
( P" B8 [# H) [& Q - * @param datalen : 要读取的字节数(最大65535)$ F% e7 P& g+ {* `& F H
- * @retval 0, OK; 其他, 错误代码
* F9 o: {$ E; N. {, p i - */' M! I+ P6 s/ o& |& M5 B6 M9 t6 f
- void norflash_ex_read(uint8_t *pbuf, uint32_t addr, uint16_t datalen)1 z1 S5 w6 @6 ^5 T/ w. j; O
- {9 y) S5 t4 @2 y- k
- uint16_t i = 0;
2 F, Z. ~4 j- t2 i5 p - /* 使用内存映射模式读取,QSPI的基址是0X90000000,所以这里要加上基址 */
3 _- y$ d ?5 J+ m: M) c - addr += 0X90000000;
5 S$ T$ i! v0 P9 w! v& Q9 H2 x - INTX_DISABLE(); /* 关闭中断 */$ r1 z( ?- g# u
3 }6 u/ p: d8 H3 J+ b" c- for (i = 0; i < datalen; i++)
0 m$ N3 f) s1 B+ h - {
$ e; b ~; |' i8 E: [ - pbuf = *(volatile uint8_t *)(addr + i);
( V. E2 ^% _9 n- O- R' b5 ^$ |) M y - }4 D' O6 {: d) b( m4 P
- INTX_ENABLE(); /* 开启中断 */* z* D8 g4 ~# D& _: K0 X
- }
复制代码
4 G" K8 M: ?" R从QSPI FLASH 读取数据就没有写入这么麻烦了,因为不需要考虑STM32H7不支持QSPI接口读时写的问题,但是仍然有要注意的问题。首先是我们使用内存映射模式读取数据的话,还需要加上QSPI的基址。QSPI的基址在qspi_code.scf文件中定义,是0X90000000,所以这里要在QSPI FLASH开始读取的地址上,再加上基址0X90000000。读取的过程是不允许被打断的,所以还要关闭所有中断,读取完成才打开所有中断。9 `( ]( r0 O0 I* H$ {) K( h
norflash _ex.c文件我们就介绍这四个函数,其他请大家自行查阅。* B+ K" e$ t! A& l5 D
3. main.c代码
% V0 }3 v) \# H" K( O" ^$ ?在main.c里面编写如下代码:/ T9 C" s y8 y( D
/* 要写入到FLASH的字符串数组 */
1 x! n$ f5 _5 e# L Y. J. E' ~3 W( G: _
- const uint8_t g_text_buf[] = {"MiniPRO STM32H7 QSPI TEST"};/ v& \8 v/ x* T! J* q
- #define TEXT_SIZE sizeof(g_text_buf) /* TEXT字符串长度 */
, k3 R( Z7 N- v6 F& Z0 Z
O/ r( M; u+ @4 q- int main(void)5 w; C7 P: K! _( l& i$ h2 K: F
- {
9 t( ]5 c- J/ t# R( a - uint8_t key;8 L+ B8 R) n7 t. m) }0 a# s( B2 }# Y% `! e
- uint16_t i = 0;, Q) I( E8 ^* u4 `1 T" P& a) D5 t
- uint8_t datatemp[TEXT_SIZE];. T. l# J _+ X$ G! v4 x
- uint32_t flashsize;3 ]. z# q- N3 |0 W( c1 `% U5 ^
- uint16_t id = 0;
" i! a4 n2 H) _+ o: {- G
' e X% K) [8 {+ i# E4 Y- sys_cache_enable(); /* 打开L1-Cache */
7 o/ L/ i. }! q - HAL_Init(); /* 初始化HAL库 */2 p" {* |( J6 O" \5 e4 H _$ _
- sys_stm32_clock_init(240, 2, 2, 4); /* 设置时钟, 480Mhz */" l8 ^- L2 e# u* Y( n4 o, w
- delay_init(480); /* 延时初始化 */2 e/ b4 ^" ^0 z$ X2 f
- usart_init(115200); /* 串口初始化为115200 */+ p! D$ p. |7 Q3 G1 O
- usmart_dev.init(240); /* 初始化USMART */
/ K9 j ]0 ?0 B9 ^$ q* a8 ? - mpu_memory_protection(); /* 保护相关存储区域 */" Q' `, w9 S$ v
- led_init(); /* 初始化LED */: m3 r) g9 [/ s5 p* g
- lcd_init(); /* 初始化LCD */
; }6 h! t9 M2 |, h' W2 @ - key_init(); /* 初始化按键 */2 Z h! S3 j' d3 \, B
- /* 6 W6 C i/ }: C+ q% J8 O
- * 不需要调用norflash_init函数了,因为sys.c的sys_qspi_enable_memmapmode函数已+ X6 B9 t+ r& V* D+ T! h( t
- * 经初始化了QSPI接口,如果再调用,则内存映射模式的设置被破坏,导致QSPI代码执行异常!
! R' h ]& t, V# h- [" k/ s - * 除非不用分散加载,所有代码放内部FLASH,才可以调用该函数!否则将导致异常!
) N Y u, h* y6 q - */0 R8 @! a0 ^2 f& i" S: y" ?
- //norflash_init();$ C$ P: Y' `) `6 z. P4 R
- % l9 b& l7 K) z& y6 j8 {( D
- lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
8 E$ t* R: ?7 Q6 h: I* Z - lcd_show_string(30, 70, 200, 16, 16, "QSPI TEST", RED);; a2 {7 H2 R3 N0 ^8 a
- lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);$ |+ g$ W- Z, W S
- /* 显示提示信息 */
. l* Y; d$ U' {6 e6 ~, G - lcd_show_string(30, 110, 200, 16, 16, "KEY1:Write KEY0:Read", RED);
0 c W3 u& I2 t" B* Z6 x7 y1 C - * m2 W; C* C! e) V+ U' O
- id = norflash_ex_read_id(); /* 读取FLASH ID */+ {, e. L. o1 q. F3 q
O& M: U3 u: U0 J6 s2 p+ Z- while ((id == 0) || (id == 0XFFFF)) /* 检测不到FLASH芯片 */' H* \. V* y9 m3 ^) h8 p
- {$ i# i! {/ G0 G9 F5 N+ Z
- lcd_show_string(30, 130, 200, 16, 16, "FLASH Check Failed!", RED);+ K& a: b5 d/ V6 w4 Q
- delay_ms(500);9 m$ k9 g( X2 R S) i
- lcd_show_string(30, 130, 200, 16, 16, "Please Check! ", RED);! k+ `- |" x8 z S0 s
- delay_ms(500);; o# E# Y. a( l1 D" G! j, k
- LED0_TOGGLE(); /* LED0闪烁 */
4 m, p7 ?! E( \; [2 h8 ?, a. L# Z6 { - }
% G0 ]5 y/ q, f' H
( U G- j3 b- ~) U/ Z- lcd_show_string(30, 130, 200, 16, 16, "QSPI FLASH Ready!", BLUE);5 J# S+ ]$ W& n# d/ ~
- flashsize = 16 * 1024 * 1024; /* FLASH 大小为16M字节 */
8 d' X" ]9 P& p5 {( `: i
$ Y+ k! C2 i" `; N% M8 ]6 f- while (1)
& ^7 ?4 _- j& o# P: q2 T7 C, n - {
% |7 c$ a# e& Q0 ^) W: Y! X- ^) _ - key = key_scan(0);
2 A, O: M/ G W. X' C
$ F0 W) ?( }/ P- E8 j- if (key == KEY1_PRES) /* KEY1按下,写入 */, e: I6 }+ H1 b
- {2 I2 ~" ], j/ T; E5 s' A* A4 ?: J
- lcd_fill(0, 150, 239, 319, WHITE); /* 清除半屏 */
; z* {8 J3 P. r1 a - lcd_show_string(30, 150, 200, 16, 16, "Start Write FLASH....", BLUE);7 c" ^# q$ M! z7 ~ P
- sprintf((char *)datatemp, "%s%d", (char *)g_text_buf, i);
* p" ~. N& a9 C- r& b - /* 从倒数第100个地址处开始,写入SIZE长度的数据 */
q" i7 j) h7 s+ m3 C4 K3 Z - norflash_ex_write((uint8_t *)datatemp, flashsize - 100, TEXT_SIZE);
& a6 b# y5 M; ^" e6 m - /* 提示传送完成 */
& r! E Q+ z& @5 |% i - lcd_show_string(30, 150, 200, 16, 16, "FLASH Write Finished!", BLUE); $ l7 _& h: w' Z8 U, B7 y4 e( u
- }
% u. d+ e. N7 \
. J/ g1 C2 z* \1 r0 s- d" C7 `- if (key == KEY0_PRES) /* KEY0按下,读取字符串并显示 */
4 r; M. p/ k$ U# N - {
5 u7 q' Z" ~7 M$ _4 i - lcd_show_string(30, 150, 200, 16, 16, "Start Read FLASH.... ", BLUE);. S+ g9 ^: c2 m2 I$ _/ G& E8 z' r$ |
- /* 从倒数第100个地址处开始,读出SIZE个字节 */
# P6 B$ i6 {* Y' ?8 d" g( t# i) T! x - norflash_ex_read(datatemp, flashsize - 100, TEXT_SIZE);
, k# ]* H- Q* J ~4 D - /* 提示传送完成 */ ' E* z) ]# ^; ~: Z0 H
- lcd_show_string(30, 150, 200, 16, 16, "The Data Readed Is: ", BLUE);
) C3 a* x3 @8 }, g3 t - /* 显示读到的字符串 *// t9 R, H' I( `4 E' l, R7 c) l
- lcd_show_string(30, 170, 200, 16, 16, (char *)datatemp, BLUE);
: K+ t% w' N0 Y* }2 L - }
) x- H( l: S1 q1 o7 o
9 ?$ D- r, f% s- i++;; e9 y$ Y' s+ U9 _# v
5 A& f2 ~* I4 l' A( l$ i9 k) X- if (i == 20)
- k1 B& Q* y% U2 l/ k F2 ` - {
! f3 Z8 a5 ]5 U# p( F - LED0_TOGGLE(); /* LED0闪烁 */
* F( ^& Q* t: o$ }2 M - i = 0; X7 g/ O% X. H9 E
- }9 K0 ]& r. P- G' W( o2 k
' \6 h1 K" G* p, Y* v- delay_ms(10);
/ f: P( v3 m2 q, E - }( x4 m& K+ E' p; K! K2 A
- }
复制代码
* C4 x8 S, _2 M7 [5 W在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按下,读取刚才写入的字符串并显示。0 _# g ]5 z1 x9 E
最后,我们将norflash_ex_read_id、norflash_ex_erase_chip和norflash_ex_erase_sector函数加入USMART控制,大家还可以把其他的函数加进来,这样,我们就可以通过串口调试助手,操作NOR FLASH,方便大家测试。norflash_ex_erase_chip函数大家谨慎调用,因为会把NOR FLASH的程序指令也擦除掉,会导致死机。如果不使用分散加载,就没关系。 `6 @2 C! C% k' s+ W& L
36.4 下载验证
* S4 X0 N `0 H/ g+ T! k$ d将程序下载到开发板后,可以看到LED0不停的闪烁,提示程序已经在运行了。LCD显示的内容如图36.4.1所示:
2 B. e5 l* m! l5 K4 Z
5 R) J: J6 k* B' u! R0 {
) _/ D4 Z& S1 g7 A6 e
2 J2 s3 w4 J/ d- H* \
图36.4.1 QSPI实验程序运行效果图
0 V+ I& ~0 m4 v+ \7 O% M0 N; B: }# r% L通过先按KEY1按键写入数据,然后按KEY0读取数据,得到如图36.4.2所示:
' e- P U: `/ J2 s% Y
! n% @: {+ z8 e V, Q
a% d. y9 D! e8 A7 @- B6 O9 e7 v& C( I1 }1 p; m0 J v
图36.4.2 操作后的显示效果图5 n$ X0 N. q/ E
程序在开机的时候会检测NOR FLASH是否存在,如果不存在则会在LCD模块上显示错误信息,同时LED0慢闪。
3 ? z x5 ]& J0 F* u+ a) f" G————————————————
4 A* l. |6 \, ?9 n版权声明:正点原子* T1 i: w4 }2 N
; X, z" C: f$ b3 u @% ]$ l
& K r6 a' O0 q0 \9 m4 ?7 N. }! i7 r: n4 x5 x
|