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