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

【经验分享】STM32F7QSPI学习笔记——读写N25Q128

[复制链接]
STMCU小助手 发布时间:2021-12-16 21:00
一、QSPI接口介绍2 U6 h( g" B2 H, z5 _
4 a/ V; C7 e1 ?" t
QUADSPI 是一种专用的通信接口,连接单、双或四(条数据线)SPI Flash 存储介质。该接口可以在以下三种模式下工作:
! ^3 ?( `; |7 K' n# m! m' H0 @# n) o, z0 j; _9 L1 M* h9 g' R) O* F
 间接模式:使用 QUADSPI 寄存器执行全部操作,支持对FALSH进行读、写、擦除、锁定等操作。# ?( }/ H5 z& g- B9 ?1 C
# n* F0 A, @/ R1 t5 C
 状态轮询模式:周期性读取外部 Flash 状态寄存器,而且标志位置 1 时会产生中断(如擦除或烧写完成,会产生中断)- g. S( G6 p: k! B) m" V

6 ]" S$ V: q( k 内存映射模式:外部 Flash 映射到微控制器地址空间,从而系统将其视作内部存储器  Y$ m8 `" m5 v: R. r) i1 ?. j

" H6 a/ I5 J1 t( w2 O采用双闪存模式时,将同时访问两个 Quad-SPI Flash,吞吐量和容量均可提高二倍。$ H( d7 s0 k1 n* ?) \6 i
( ^1 d# W$ S1 g( F/ X- M
内存映射模式支持XIP,即可以将代码放在外部FLASH运行,缺点就是有延迟,没有内部运行速度快,且外部FALSH只能读,不能写入或擦除。
& S. E" t* ^/ x/ d& q$ C3 N
# K4 |& G3 I9 X6 {& ^内存映射模式下QUADSPI 外设若没有正确配置并使能,禁止访问 QUADSPI Flash 的存储区域。即使 Flash 容量更大,寻址空间也无法超过 256MB。
7 B7 V, L, G) ^. t& c( [5 z9 ?9 p. {& Z9 ], C: @: t
二、 STM32QSPI 功能框图: {! T1 w/ C5 {1 \7 E. `; a

8 @1 f4 H: R! I# G& j
20191006091636349.png

# f* t$ X1 j2 s+ i, N6 M8 m9 A: p& N& v3 C, m" e5 Q
STM32F7系列支持双QSPI接口,可以接两个外部FLASH,本文只介绍接一个FALSH的情况。QSPI使用6根线通信,其中数据线4根,1根时钟线,1根片选线,但是程序里是可以配置工作在几线模式。, M, L& e  F, b( F3 O" T
  `$ [# I5 H( E2 v
三、QUADSPI  命令序列( G: e# E# F, `# c) g$ a

0 V4 q* ?( S+ w2 h9 aQUADSPI 通过命令与 Flash 通信 每条命令包括指令、地址、交替字节、空指令和数据这五
  _& b. b/ p. M个阶段,任一阶段均可跳过,但至少要包含指令、地址、交替字节或数据阶段之一。4 i( z- _0 W1 P; a; m3 `; }" i* E
nCS 在每条指令开始前下降,在每条指令完成后再次上升。
! e$ @3 r$ s- _  A0 f/ c4 O) N0 a! a$ Y2 g4 y6 n% [
20191006092242666.png
( D- M4 t! r$ ^* T  Q# s  r& ~

- Q7 B3 d: X& Q8 g上图清晰的展示了一次QSPI通信过程,它完整的包含了通信的5个阶段,但不是每次通信都需要包含这5个阶段.
$ y2 F. ^, \( c/ `, Q2 Y# j7 Y- M" e' q# ~
交替字节阶段,将 1-4 字节发送到 Flash,一般用于控制操作模式。|正常读写是不需要的,所以这个阶段可以省略(结构体中该参数置0);
) ?1 I7 e5 o: O' e$ W8 m7 f9 b! P7 t- F5 n3 I
空指令周期阶段,给定的 1-31 个周期内不发送或接收任何数据,目的是当采用更高% [0 U) z' B4 }7 E1 H% _; S
的时钟频率时,给 Flash 留出准备数据阶段的时间。$ @0 ]/ @1 r9 x/ b' {4 O" H+ ~
若 DummyCycles为零,则跳过空指令周期阶段,命令序列直接进入数据阶段(如果存在)。4 E7 I4 P8 u% G5 o. G' d: b
  h& Q" F- b5 ^0 M: n# W
此外,并不是每次通信都需要包含以上各个阶段,例如,打开写使能指令,只有指令阶段,其他阶段的参数都置0.再比如,读芯片ID,只有指令和数据阶段,具体包含哪些阶段需要参照芯片手册配置。7 Y" C/ F8 [/ x4 G

/ n( N9 B" c( N7 h: ?四、QUADSPI  信号接口协议模式
8 @1 k# b- b# x0 l8 C5 ^8 _: u* }4 n# Z# `( R% N
下面提到的几线模式都是指数据线的根数
7 v3 v+ w3 U* B( n1 e  S! O9 \4 r( p# e3 i* ~
单线 SPI  模式(即普通SPI通信,等同于使用MISO、MOSI、CLK、CS方式通信):
5 q4 C8 h! X" s2 D# f: g* W传统 SPI 模式允许串行发送/接收单独的 1 位。在此模式下,数据通过 SO 信号(其 I/O 与
" u! g0 q! _; d# BIO0 共享)发送到 Flash。从 Flash 接收到的数据通过 SI(其 I/O 与 IO1 共享)送达。
% G+ T6 C& S' \7 r9 V通过将(QUADSPI_CCR 中的)IMODE/ADMODE/ABMODE/DMODE 字段设置为 01,可7 i$ ]& R0 W# a  _4 F, D
对不同的命令阶段分别进行配置,以使用此单个位模式。1 C. D' O; J! \6 n4 f; H
在每个已配置为单线模式的阶段中:$ s4 Y) @* J% u5 w; r! s
 IO0 (SO) 处于输出模式# R' K* K) I  P8 C0 m
 IO1 (SI) 处于输入模式(高阻抗)
6 y: T7 S- g% s: @" R! N0 H IO2 处于输出模式并强制置“0”(以禁止“写保护”功能), w( Y4 T' q7 {3 P6 A; a, w
 IO3 处于输出模式并强制置“1”(以禁止“保持”功能), I3 i! Q: w& \* G0 m; R  C

, A% b3 t; H. w* e" o0 l: f& p7 m& x双线 SPI  模式:5 D/ o/ n' y3 @; E
在双线模式下,通过 IO0/IO1 信号同时发送/接收两位。1 T7 c8 c' o. U
通过将 QUADSPI_CCR 寄存器的 IMODE/ADMODE/ABMODE/DMODE 字段设置为 10,可
- c9 R4 s* g" ^$ p, Z2 z对不同的命令阶段分别进行配置,以使用双线 SPI 模式。& e7 D( V4 z' }* T4 s
在每个已配置为双线模式的阶段中:& ]$ Q! U% i3 M
 IO0/IO1 在数据阶段进行读取操作时处于高阻态(输入),在其他情况下为输出
2 K; \/ d. Y" ? IO2 处于输出模式并强制置“0”
# ^5 O4 [8 J$ l8 [' r$ X8 o( ` IO3 处于输出模式并强制置“1”
7 f: H2 }* l2 i  l0 g8 F# W* Q在空指令阶段,若 DMODE = 01,则 IO0/IO1 始终保持高阻态。8 e% D; i' o5 G$ |) O: c% {, U, H

' u) n  S) L5 Y$ k; W# T9 }0 x
! Y* F1 B6 l) n: D四线 SPI  模式:* F2 T: O4 f, f& p0 N% H
在四线模式下,通过 IO0/IO1/IO2/IO3 信号同时发送/接收四位。4 x0 I6 d5 T) s2 N0 k* l
通过将 QUADSPI_CCR 寄存器的 IMODE/ADMODE/ABMODE/DMODE 字段设置为 11,可
  l- r) X- Q& z( }1 B" [* L对不同的命令阶段分别进行配置,以使用四线 SPI 模式。
! ?/ ~9 }5 \7 m9 @- ^- ^在每个已配置为四线模式的阶段中,IO0/IO1/IO2/IO3 在数据阶段进行读取操作时均处于高
  C% ?9 y5 ~7 E5 a1 f' B阻态(输入),在其他情况下为输出。9 j/ `1 U3 `* V$ v0 t4 _
在空指令阶段中,若 DMODE = 11,则 IO0/IO1/IO2/IO3 均为高阻态。7 Q  M- y* \! F# T; j
IO2 和 IO3 仅用于 Quad SPI 模式 如果未配置任何阶段使用四线 SPI 模式,即使 QUADSPI
, c: u6 j* `5 Y+ }+ P  i激活,对应 IO2 和 IO3 的引脚也可用于其他功能。
1 P8 v, P( I: }+ t6 h
8 u: G" q* H2 Q  C# C6 X4 L+ H% N0 y& p: M4 Q9 Q1 j
SDR  模式(默认工作模式)
3 R6 C/ \) l2 g( C  A4 P默认情况下,QUADSPI 在单倍数据速率 (SDR) 模式下工作。$ g2 |$ X  a3 e4 ]8 J; q7 B
在 SDR 模式下,当 QUADSPI 驱动 IO0/SO、IO1、IO2、IO3 信号时,这些信号仅在 CLK# K( U2 k$ ~6 r3 K9 k8 c$ i. N$ T% E" D
的下降沿发生转变。
( V8 R6 V( `$ a7 y5 W/ B: k在 SDR 模式下接收数据时,QUADSPI 假定 Flash 也通过 CLK 的下降沿发送数据。默认情
# ?4 N6 [$ n! M, u9 R+ l况下 (SSHIFT = 0 时),将使用 CLK 后续的边沿(上升沿)对信号进行采样。
3 U9 g& y5 Z, q* m! n' q8 }
8 L# v6 U! Z# B3 i% h7 X# t; X7 _& C# D! }
DDR  模式- p3 H* v" [8 m7 |+ L- N/ M
在 DDR 模式下,当 QUADSPI 在地址/交替字节/数据阶段驱动 IO0/SO、IO1、IO2、IO3 信9 J' X7 |% f: G% S9 _: e" d
号时,将在 CLK 的每个上升沿和下降沿发送 1 位。( S' u2 p8 g/ D
指令阶段不受 DDRM 的影响。始终通过 CLK 的下降沿发送指令。
% G/ A( W9 }: M- o2 r, x在 DDR 模式下接收数据时,QUADSPI 假定 Flash 通过 CLK 的上升沿和下降沿均发送数$ [4 M: _( }7 d5 ]6 D
据。若 DDRM = 1,固件必须清零 SSHIFT 位 (QUADSPI_CR[4])。因此,在半个 CLK 周期5 I* f7 t# E- V4 h! |$ ~) N% d
后(下一个反向边沿)对信号采样。
# X1 N# T* Q3 J4 l  q2 j- p2 U$ I$ F. p+ `2 m
五、QSPI 配置(HAL库)
7 [# y, B+ d4 d/ Q3 S1 P9 v- _& e
5 x' H+ q7 n# |& c1.首先根据硬件电路,配置相关引脚,开启引脚和QSPI时钟: N; {3 G. }% a, [# N

- E% N9 L! b2 |
20191006121236849.png
- c2 U/ c# h- [

& T  ~/ Q; U* @& I8 ^# Z
  1. void HAL_QSPI_MspInit(QSPI_HandleTypeDef* qspiHandle)! ?" P) F+ |' \% T2 H3 V
  2. {
    ) S1 D" ^/ e3 y) R1 |4 K

  3. 4 z5 v- @8 C$ j: j- z! n
  4.   GPIO_InitTypeDef GPIO_InitStruct = {0};  d$ y) `, I0 f$ `# P. ?
  5.   if(qspiHandle->Instance==QUADSPI)+ E5 A3 W6 \7 Y  v
  6.   {
    ) ?5 F$ i3 _0 c; Y
  7.   /* USER CODE BEGIN QUADSPI_MspInit 0 */
    3 e1 X$ X! j. m1 K
  8. 5 n) H- p$ f0 c& F. ]
  9.   /* USER CODE END QUADSPI_MspInit 0 */
    2 T" P+ E* G4 b; d4 U2 s5 }2 w) R
  10.     /* QUADSPI clock enable */+ x+ N# x% o+ a; e) Q+ f
  11.     __HAL_RCC_QSPI_CLK_ENABLE();
    $ u$ c( q7 l, P* T# A$ l$ q

  12. , X+ A. `5 [( c1 A. w- G  \
  13.     __HAL_RCC_GPIOE_CLK_ENABLE();4 n4 A5 m: g- |2 ]' W
  14.     __HAL_RCC_GPIOB_CLK_ENABLE();
    + n1 q8 y. F# B# X7 W2 y# ]
  15.     __HAL_RCC_GPIOD_CLK_ENABLE();4 s* F( ]7 c& t3 t
  16.     /**QUADSPI GPIO Configuration   
      ?3 }+ E9 @* E/ ^- _( A- a
  17.     PE2     ------> QUADSPI_BK1_IO2% F$ L8 l" T( S/ H0 `! E4 I
  18.     PB6     ------> QUADSPI_BK1_NCS4 L* Q1 a, u2 m* n) y! `. ^
  19.     PB2     ------> QUADSPI_CLK. s+ K3 P4 j  w; U" n8 e; |0 O
  20.     PD12     ------> QUADSPI_BK1_IO1
    7 _2 w! N; _" w9 T5 r" O
  21.     PD13     ------> QUADSPI_BK1_IO3
    % B: |) I" |9 O9 K
  22.     PD11     ------> QUADSPI_BK1_IO0
    0 s+ j+ b3 F% G7 Y
  23.     */0 |# N; s' a& j7 o9 G7 S; C
  24.     GPIO_InitStruct.Pin = GPIO_PIN_2;! v" H2 x- W" _
  25.     GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;9 O' G* u0 E2 _) j9 |
  26.     GPIO_InitStruct.Pull = GPIO_NOPULL;( k/ }2 L+ D* O, g5 {+ u2 L$ b
  27.     GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
    1 ?. J$ {" }9 E1 W5 ]
  28.     GPIO_InitStruct.Alternate = GPIO_AF9_QUADSPI;$ p* G: I, m4 ~% k
  29.     HAL_GPIO_Init(GPIOE, &GPIO_InitStruct);$ g7 K. Y" f) V
  30. $ W4 c3 e. a: a( Q9 {0 C% a
  31.     GPIO_InitStruct.Pin = GPIO_PIN_6;- I1 g3 s# j, U$ L  L3 b# ~7 `3 `
  32.     GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
      I! V( G) A# C; L; {. _
  33.     GPIO_InitStruct.Pull = GPIO_NOPULL;& |0 O! K- s* V) [% d0 S+ r" P
  34.     GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;: r$ ~/ V" ?9 q' g: ~
  35.     GPIO_InitStruct.Alternate = GPIO_AF10_QUADSPI;
    $ {. s, U& y7 {% g' m* h6 w
  36.     HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    0 Q9 r7 Q( r* Y" s* g8 S$ H

  37. $ e' v4 ]5 b  Y$ Y" Z
  38.     GPIO_InitStruct.Pin = GPIO_PIN_2;0 [- [& s4 x, W( z, M8 {0 }
  39.     GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    ) E/ E* L1 v! y+ K% T
  40.     GPIO_InitStruct.Pull = GPIO_NOPULL;
    / p5 W( L2 Z% S( {! }# u
  41.     GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;- ?1 D: l$ n/ T. Y$ l; t
  42.     GPIO_InitStruct.Alternate = GPIO_AF9_QUADSPI;% j- m2 O2 r5 b# h5 E! N5 d
  43.     HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);& J- `$ _* Q! z+ P, Y! R/ j; c
  44. ' \# T, B) L: t& \* F: l. v) l
  45.     GPIO_InitStruct.Pin = GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_11;
    ; D. Z  V( w/ @
  46.     GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;0 c: o5 Y. V  J- U
  47.     GPIO_InitStruct.Pull = GPIO_NOPULL;2 x% M: u- {  b
  48.     GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
    $ X, T. b; b# I. X+ M7 L. \
  49.     GPIO_InitStruct.Alternate = GPIO_AF9_QUADSPI;2 c# y4 M' o8 D* f2 V4 y6 y) ]
  50.     HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
    : s4 ?! y9 ?& y8 U

  51. $ u( d( _2 A  G
  52.   /* USER CODE BEGIN QUADSPI_MspInit 1 */
    ( B5 d' I( o- i$ M

  53. : l7 R( D) a$ h, G: A
  54.   /* USER CODE END QUADSPI_MspInit 1 */
    8 {. G" S! X2 i& P7 c
  55.   }' |0 H* g" l+ d1 ?) r( p+ X
  56. }
复制代码
, j( q  t! g& G, V4 z
2.接着初始化SPI外设$ O) p& v9 t0 j
  1. 6 s0 l, x* e0 B& z& N
  2. QSPI_HandleTypeDef hqspi;
    * t2 |" ~* k2 ]

  3. * P" C' Y- t3 X
  4. /* QUADSPI init function */
    & n6 ]* S% t( X5 V
  5. void MX_QUADSPI_Init(void)3 ]) N4 V( \/ i5 b( j
  6. {
    7 s$ y" n1 B3 s9 w
  7.   hqspi.Instance = QUADSPI;//QSPI外设: [7 K- B) ^& n" N4 [4 }+ e' j
  8.   hqspi.Init.ClockPrescaler = 2;//QPSI分频比,N25Q128最大频率为104M,此处216/(2+1)=72M
      |3 C( `: E4 E# N" ~4 D/ Y: ]
  9.   hqspi.Init.FifoThreshold = 4;//FIFO阈值为4个字节8 R: Y" k- x( p
  10.   hqspi.Init.SampleShifting = QSPI_SAMPLE_SHIFTING_HALFCYCLE;//采样移位半个周期(DDR模式下,必须设置为0)
    / t' Q% B4 b0 i( B( N
  11.   hqspi.Init.FlashSize = POSITION_VAL(0X1000000)-1;//SPI FLASH大小,N25Q128大小为16M字节' a+ _' n+ F  c2 W
  12.   hqspi.Init.ChipSelectHighTime = QSPI_CS_HIGH_TIME_6_CYCLE;//片选高电平时间为6个时钟(1/72*6=55.2ns),即手册里面的tSHSL参数" F+ g) t# }3 e$ K% ]
  13.   hqspi.Init.ClockMode = QSPI_CLOCK_MODE_0;//时钟模式0! A8 W$ u& A$ @5 B3 ]& k* t
  14.   hqspi.Init.FlashID = QSPI_FLASH_ID_1;//第一片flash
    # Q  ^! |( n0 v$ @2 j' n  e4 v, o0 A
  15.   hqspi.Init.DualFlash = QSPI_DUALFLASH_DISABLE;//禁止双FLASH模式" q6 z3 M% q# S% Z2 z$ M  p1 U
  16.   if (HAL_QSPI_Init(&hqspi) != HAL_OK)//初始化QSPI外设& H7 n, z3 m$ K% b( u
  17.   {; t  z( o# }( _+ y9 b
  18.     Error_Handler();0 V) C$ L8 |& u. n
  19.   }: n1 u+ h) ]9 u0 c& v: f  {0 E
  20. }
复制代码
' o& z1 m; [7 k/ q6 |
3.由于使用的是间接模式,我们对FLASH的所有操作都需要通过FLASH内部寄存器来完成,也就是通过向FALSH发送对应的指令来获得我们想要的结果,此时的FALSH就是一个普通的SPI协议外设,所以接下来所有对FALSH所有的操作都需要先发送相关指令,我们需要配置指令参数(指令类型、地址、数据宽度、地址宽度等等)。- m1 `+ }1 @; e$ _1 D3 A, y

. M! V& [6 O8 e  d8 W6 y( _" z我们首先需要配置下面的命令参数结构体:2 u! b! P# R# d* D2 J" s

0 {9 r% @3 g9 p9 v
20191006122437823.png

* P6 v3 S9 y5 Z# f8 }* |  `2 ^, i+ k0 Y' W
Instruction:指令类型(读、写、擦除等等)
/ m- X0 ?9 D) R6 Q& d- z) n: C' l; l/ p, L8 s5 x
Address:需要读取或写入数据的起始地址& [) K; J* ~0 D! f

/ t8 Y+ p* Q6 oNbData:需要读取或写入数据的长度$ E$ L5 _$ Z* v7 q0 e4 I, O
0 d6 `8 m" X4 R! A  t/ r4 H
DataMode:数据传输模式,就是数据用几根线传输(1、2、4线可选),后面的AddreMode类似,就是用几根线传输地址,InstructionMode就是用几根线传输指令。
8 T1 M6 n0 p$ \( t) R3 m; M5 w& o
$ ?3 @! a' y* y- h1 r' z2 Y这三个参数都需要根据芯片数据手册来选择,手册上说明支持该模式才可以选择,否则会通信错误,另外,数据传输模式的不同,对应的指令类型也会不同,比如N25Q128 READ指令对应单线传输模式,Quad I/O Fast Read则对应四线传输模式,这些对应关系手册上都有详细说明!!!
1 {% \! G! M' G( K) d' }+ s' V5 |2 a2 Y7 O
4.读取芯片JEDEC ID
; X  S" R" R! z8 J, R) @8 l; }0 S, c+ L1 Z: x) h. v4 D* S
首先查看手册上读取JEDEC ID的时序:* S' {3 L9 s" }8 r+ U

+ ]2 f  K/ O+ R
20191006131644114.png

4 j; v& ^; {2 e$ q& J3 y/ w( T, H! I( \1 f; ^* i5 |
可以看出,读ID这条指令是只有指令阶段和数据阶段,且都是单线传输模式,所以
& A3 n' I+ F5 k; j9 d/ b
9 z1 A. U* ?! P8 k: o: f  dInstructionMode   = QSPI_INSTRUCTION_1_LINE;//指令单线传输模式
5 l4 @, A; E  n% B0 l7 b" B7 n0 h0 o4 ]# {0 j$ b
DataMode = QSPI_DATA_1_LINE;//数据单线传输模式( {3 G9 f3 X7 s& e1 \9 v' w0 H

! Y( L2 ]+ k4 Q* k( [5 q" GAddress和AddressMode 这两个参数的值都置0,表示无地址。# ?: Y: ~4 g7 ~9 x9 Q8 y; a* i
. S* U: ]2 x0 \) E9 S
NbData :是想要读取的数据长度+1,ID是3个字节,这里令NbData=36 _  F0 b" Z6 R) M4 ^2 R

3 i. |! w2 O9 w6 O4 A+ x其他值和下面代码保持一致即可。
* z. l) [3 v2 m3 V. N
3 X2 q" f& F6 b然后 使用HAL_QSPI_Command函数将读ID指令发送给FALSH,等待发送完成: B7 T2 O8 u2 `" z& I4 c5 T$ F
+ y2 ]0 {7 m/ T
最后 调用HAL_QSPI_Receive,接受FALSH返回的数据,即芯片ID
- e4 G# _. d( W+ ]& [5 E- o/ y. J6 l/ j
  1. /**
    / q; {' v4 U) b4 J! e4 g) n2 Y# G
  2. * @brief  读取FLASH ID
    * m" [$ W$ e9 |4 F# e
  3. * @param   无& a. O7 J3 j8 s2 C+ x% H2 @/ w7 ]
  4. * @retval FLASH ID
    * i! q3 i, c3 C% f4 P
  5. */
    " W1 x& _3 Z: w* o) J/ _
  6. uint32_t QSPI_FLASH_ReadID(void)
    8 A: z% N% M+ v4 X- _+ b  y
  7. {; x) I  b2 `% i! J2 R
  8.      QSPI_CommandTypeDef s_command;% ^" U& R5 }+ F8 m, x1 \

  9. ( H' c, }' K9 z$ T, n) y4 w
  10.      uint32_t Temp = 0;
    , \$ ?. J' e" `- {: B4 i  n, E% z
  11. 9 J: {6 R4 h- V7 m0 R
  12.      uint8_t pData[3];4 a' O; V& J6 |: O' i. H
  13. # ?8 [8 h2 i; q  P2 M
  14.      /* 读取JEDEC ID */- c6 G- Y% E0 H0 U  h

  15. 5 w3 w7 G  ?  Z) B2 F
  16.      s_command.InstructionMode   = QSPI_INSTRUCTION_1_LINE;//指令阶段的操作模式,00:无指令;01:单线传输指令;10:双线传输指令;11:四线传输指令。
    3 [0 J0 ?1 |' x% v% b, H0 W! u9 V

  17. 7 y/ j: _  {; S5 O7 f; ^9 e
  18.      s_command.Instruction       = READ_JEDEC_ID_CMD;//读取芯片ID指令2 d0 @. U4 a8 Y/ N% z3 m% F2 M, F
  19. 6 B* v( M& T5 }. p/ H, A1 T* c
  20.      s_command.AddressSize       = QSPI_ADDRESS_24_BITS;//定义地址长度
    / J* \! S* z+ c- g) L# E

  21. 6 N& e0 h7 C1 j
  22.      s_command.DataMode          = QSPI_DATA_1_LINE;//定义数据阶段的操作模式,00:无数据;01:单线传输数据;10:双线传输数据;11:四线传输数据。
    5 Y" h2 P- }3 R9 I+ s. G* J5 j
  23. , R. K; A$ x+ g  A
  24.      s_command.AddressMode       = 0;//地址阶段的操作模式,00:无地址;01:单线传输地址;10:双线传输地址;11:四线传输地址。6 U- Z+ ^* J& ~7 V% H+ b/ c
  25.          
    - r7 f# z0 r" p% ?) z, w
  26.      s_command.Address           = 0;   
    * i$ \2 H% R) b& z- q+ Z% p2 `0 b
  27.         
    ) w/ y+ O7 V+ ?6 t) E, A' C8 A
  28.      s_command.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;//交替字节长度1 E! A7 a( ?2 y" A5 p4 G$ R

  29. & I8 n  ]! W* }8 a0 m  P+ g
  30.      s_command.DummyCycles       = 0;//空指令阶段的持续时间,在 SDR 和 DDR 模式下,它指定 CLK 周期数 (0-31)。
    0 W+ S: w, y& C2 t3 x8 g

  31. + j: W6 n0 K$ R0 Y
  32.      s_command.NbData            = 3;//设置数据长度,在间接模式和状态轮询模式下待检索的数据数量(值 + 1)。对状态轮询模式应使用不大于 3 的值(表示 4 字节)。) W/ Z4 a" N  g; b! c8 H2 I$ G
  33. " z" [: z/ P4 c! L
  34.      s_command.DdrMode           = QSPI_DDR_MODE_DISABLE;//地址、交替字节和数据阶段设置 DDR 模式,0:禁止 DDR 模式;1:使能 DDR 模式。0 h7 g6 L3 s, @& K
  35. ; {$ m* t& z% R  P+ q
  36.      s_command.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;//设置DDR 模式下数据输出延迟 1/4 个 QUADSPI 输出时钟周期,0:使用模拟延迟来延迟数据输出;1:数据输出延迟 1/4 个 QUADSPI 输出时钟周期。7 w: |$ b$ ~( [% A( q3 d* J
  37.                  
    / ?9 H* S+ e: A: S/ z& z
  38.      s_command.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;//设置仅发送指令一次模式,。IMODE = 00 时,该位不起作用。0:在每个事务中发送指令;1:仅为第一条命令发送指令。. o6 y& M& Q  R0 b+ B3 y2 ]
  39. 3 d; Y5 n- H. ~" a( X+ l
  40.      if(HAL_QSPI_Command(&hqspi, &s_command, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
    . Y* _' {7 I% K6 _* B* E. t
  41.                  {
    9 I% r/ W# \) i3 o9 C4 M
  42.          #if  QSPI_DEBUG
    3 r! N' p  ]/ U" |) S  |9 l2 F9 U
  43.                      printf("send read flash ID command error!\r\n");$ [' J: @- P2 [
  44.                #endif- A$ N6 {3 r( D1 S$ A
  45.          /* 用户可以在这里添加一些代码来处理这个错误 */' J2 W) ~/ N7 J9 g9 ~1 Q9 x. l, c9 D+ S
  46.          while (1) {}
    % u4 E4 y- f# `+ @% ~" U' O/ C
  47.      }
    4 S# Y4 o; N. Z) o- w' W
  48.      if(HAL_QSPI_Receive(&hqspi, pData, HAL_QPSI_TIMEOUT_DEFAULT_VALUE)!= HAL_OK) 3 A& P: N% h3 q& {5 b1 Z/ `
  49.                  {
    5 _" l% h2 a0 w8 u3 ~
  50.          #if  QSPI_DEBUG* }  B3 n7 W! Y4 W+ O+ B* V
  51.                      printf("read qspi flash ID error!\r\n");
      \4 Y/ s9 r1 v$ t; S
  52.                #endif/ N! ~" b4 r2 Y& b
  53.          /* 用户可以在这里添加一些代码来处理这个错误 */) |8 v. `) U# x' F; `
  54.          while (1) {}7 g6 e1 `0 p6 g9 K( c& @' U
  55.      }9 e- Z5 m  g3 U
  56.      Temp = ( pData[2] | pData[1]<<8 )| ( pData[0]<<16 );9 h' _. ^/ U4 v- D5 s& L
  57.      return Temp;
    3 [* s6 \5 X5 t1 W4 m
  58. }
复制代码

) T) p! j( l9 F 5.FALSH 写使能
0 [, N; x8 J9 u! W: }
6 e- K: P# S" C$ H7 c/ j在对FALSH进行写之前,需要打开写使能开关(实际上就是发送写使能指令):
) e2 U& p6 E% j3 d% h! |0 |8 t8 _
类似的,需要查看芯片手册,写使能的时序0 D4 _) j1 K' D4 M7 m; g
- C$ |- g+ G$ e* F- }' f. R
20191006134057767.png
" I# I* s0 f- I6 V  e3 t
3 W) I% P3 |4 i6 j# M
可以看到这个指令时序更简单,仅仅有指令阶段,所以地址和数据的参数都应置0 : d- e  e2 k1 B
2 D4 [( }* ?+ s+ _$ W. U3 D
  1. /**
    / \& N+ p, V: h( {, S2 @% g6 v  J8 l
  2.   * @brief  QSPI FALSH写使能
    5 s7 @) j4 U: O: e
  3.   * @param  None
    . m/ a+ ?; K0 L1 G5 ?
  4.   * @retval HAL_OK
    ( {; o, E0 e, A* D- o4 W/ {# {
  5. */
    0 O3 |8 }! l; L  q
  6. static HAL_StatusTypeDef QSPI_WriteEnable(void)" e, h5 W- |/ u8 V3 w
  7. {
    : P2 T( _8 f! v, U4 B  V* w( E' u
  8.   QSPI_CommandTypeDef     sCommand;5 O/ v  F0 V( O. N. M( t
  9.   QSPI_AutoPollingTypeDef sConfig;2 A1 d, `4 D6 U" `! k' v
  10. ) i! d7 X- _, l  _' i& b. q
  11.   /* Enable write operations ------------------------------------------ */
    . l3 h& r+ T6 W" B6 `! W6 ~
  12.   sCommand.InstructionMode   = QSPI_INSTRUCTION_1_LINE;
    2 K3 I6 I. L+ j# I
  13.   sCommand.Instruction       = WRITE_ENABLE_CMD;( G/ J3 D9 T/ n. G5 k+ v* X
  14.   sCommand.AddressMode       = QSPI_ADDRESS_NONE;
      k5 T5 f. l( Y3 m( N  W8 n! y; w
  15.   sCommand.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
    , `% }' |( Y0 V8 u/ f5 L
  16.   sCommand.DataMode          = QSPI_DATA_NONE;
    & x/ o" B+ e+ V+ r& r: W1 a
  17.   sCommand.DummyCycles       = 0;1 s; J5 z% g  X- [$ d
  18.   sCommand.DdrMode           = QSPI_DDR_MODE_DISABLE;) X' N! D& U/ `! M  P% G/ W
  19.   sCommand.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;
    / w( B9 i4 Q8 {6 g% ^
  20.   sCommand.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;
    + T2 [1 `4 X. t( ]
  21. ; l- p& @: f( h3 E) c6 Q/ Y1 [
  22.   if (HAL_QSPI_Command(&hqspi, &sCommand, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
    ( H4 c) ?5 C; B
  23.   {
    3 Y5 j1 `$ s5 s
  24.     return HAL_ERROR;
    1 p- T# q( k, F) |/ Z- y$ }
  25.   }
    ( M2 a$ |. A. }
  26. " d$ W1 ~% L5 S* j
  27.   /* Configure automatic polling mode to wait for write enabling ---- */  
    ! s. W+ B  ]- }! M
  28.   sConfig.Match           = 0x02;
    1 A  K( K0 O9 W5 f- i1 Q
  29.   sConfig.Mask            = 0x02;
    + n- L) T, r6 e! G8 ^( ?" S4 D2 L
  30.   sConfig.MatchMode       = QSPI_MATCH_MODE_AND;
    ( }8 N& R2 _; S1 v/ F1 p8 K8 j7 @
  31.   sConfig.StatusBytesSize = 1;' d# a$ \; Y5 P7 b
  32.   sConfig.Interval        = 0x10;! j6 ]% r. O1 P5 _9 W% P2 ^
  33.   sConfig.AutomaticStop   = QSPI_AUTOMATIC_STOP_ENABLE;
    + y1 W- z5 H: n" m5 u% U
  34. 5 o* y7 `2 Z( ]8 w: C
  35.   sCommand.Instruction    = READ_STATUS_REG1_CMD;6 X& l+ I% Q5 b/ p* D
  36.   sCommand.DataMode       = QSPI_DATA_1_LINE;  _- d$ ~3 X4 \! T5 g: v

  37. 1 t" ?5 d8 i: U
  38.   if (HAL_QSPI_AutoPolling(&hqspi, &sCommand, &sConfig, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
    ) z- F# f# J/ @1 ~8 j3 V# T
  39.   {7 |2 v+ F9 P% f; ?  H4 Z( F9 a
  40.                 return HAL_ERROR;        
    , n0 y2 S0 P( @5 A  H, ~
  41.   }
    1 E, Z: [  |% z; d
  42.         return HAL_OK;- K2 ]6 i8 v" r. T$ I0 X1 K, g
  43. }
复制代码
8 S# I5 F3 B; q  e  r
这里需要讲下 QUADSPI  状态标志轮询模式:# V( D" `$ H% d/ B  P
在自动轮询模式下,QUADSPI 周期性启动命令以读取一定数量的状态字节(最多 4 个)。) `& a( f5 ^5 l" w; a
可屏蔽接收的字节以隔离一些状态位,从而在所选的位具有定义的值时可产生中断。' y! `& [- H4 [( h" t  q2 ?* m
对 Flash 的访问最初与在间接读取模式下相同:如果不需要地址 (AMODE = 00),则在写入% Q6 H5 Q2 n2 k2 S/ e
QUADSPI_CCR 时即开始访问。否则,如果需要地址,则在写入 QUADSPI_AR 时开始第
$ t* T- i$ e1 C' o2 `4 w一次访问。BUSY 在此时变为高电平,即使在周期性访问期间也保持不变。
! X2 ?& O0 ~8 u! n) A. d在自动轮询模式下,MASK[31:0] (QUADSPI_PSMAR) 的内容用于屏蔽来自 Flash 的数据。
2 B( f7 D# |% V) e. S" u5 F如果 MASK[n] = 0,则屏蔽结果的位 n,从而不考虑该位。如果 MASK[n] = 1 并且位 [n] 的内
* G) a; I% f; [& n( }容与 MATCH[n] (QUADSPI_PSMAR) 相同,说明存在位 n 匹配。/ d+ d" u8 u, n6 q
如果轮询匹配模式位 (PMM, QUADSPI_CR[23]) 为 0,将激活“AND”匹配模式。这意味着$ f- u3 s+ X- X, `: Y
状态匹配标志 (SMF) 仅在全部未屏蔽位均存在匹配时置 1。7 K7 L# p5 l6 I
如果 PMM = 1,则激活“OR”匹配模式。这意味着 SMF 在任意未屏蔽位存在匹配时置 1。9 l7 O. _8 D: a8 o
如果 SMIE = 1,则在 SMF 置 1 时调用一个中断。+ E  t% y% H+ @9 F
如果自动轮询模式停止 (APMS) 位置 1,则操作停止并且 BUSY 位在检测到匹配时清零。否
+ f  F) y6 e/ G1 @8 c/ Z则,BUSY 位保持为“1”,在发生中止或禁止 QUADSPI (EN = 0) 前继续进行周期性4 D6 F1 y3 G/ o) s+ u! C
访问。# t2 E* H, }, m6 p- `6 M
数据寄存器 (QUADSPI_DR) 包含最新接收的状态字节(FIFO 停用)。数据寄存器的内容不  C0 y' f  ]: X# \$ A4 r8 r
受匹配逻辑所用屏蔽方法的影响。FTF 状态位在新一次状态读取完成后置 1,并且 FTF 在数5 ]6 z* f" M) Z! x# p
据读取后清零。8 d; F7 T6 t4 q6 w6 H* A' e" h0 g) l

9 }3 J# d5 v* N" ]: i, e简单的说,就是CPU自动查询FALSH指定的标志位是否置位或清零,通过调用HAL_QSPI_AutoPolling函数,CPU将自动查询FALSH写使能标志位是否被置位,成功返回HAL_OK,否则返回HAL_ERROR,这样就省去了我们自己发读寄存器指令查询的麻烦了。" x: e8 q( B: v1 h
+ K( j0 J8 Q( ?
6.读FALSH
- [* Y$ V0 {) \9 W& j
9 n6 m3 I- ]% [/ r( a$ |* W这里仅介绍4线模式下的读FALSH流程,首先查看手册4线模式读时序! @6 ~3 E5 o; i. q) w$ ^1 i
& J5 b: s: I9 V# c+ `
20191006135244693.png
1 w+ c( [* u; N4 F) L1 s) ^( u6 m
3 o; t$ P* u- c" U( s. i0 U: V4 Y
从时序图可以看出,指令是1线模式,地址是4线模式,空周期(DummyCycles)为10,数据也是4线模式,还有此时的读指令是QUAD_INOUT_FAST_READ_CMD。所以相关参数配置如下:
# j: k5 Q( Z1 Z  q& }( W4 _* }
8 L: h6 T0 F( ^% W. Z( _! k- S: x" y- e
  1. /**
    + V+ ^5 [) z  j# _& N- b9 l
  2. * @brief  从QSPI存储器中读取大量数据.6 M) I$ `' I$ C* K+ o
  3. * @param  pData: 指向要读取的数据的指针
    ; K: X/ [; K! E
  4. * @param  ReadAddr: 读取起始地址( d/ U7 H9 Q5 v4 ~
  5. * @param  Size: 要读取的数据大小4 [4 N, ^; a1 M2 I5 [4 l
  6. * @retval QSPI存储器状态! I' k9 V) D; x! N, a' Q3 @6 D
  7. */0 I/ q# `$ n9 F: j, k" x( ^6 w9 w* h
  8. HAL_StatusTypeDef BSP_QSPI_Read(uint8_t* pData, uint32_t ReadAddr, uint32_t Size)0 |' Q: F3 W9 b5 u, E$ ]' h& f
  9. {
    % l& z$ M. g7 v+ c; H7 e: s
  10.      QSPI_CommandTypeDef s_command;, p1 r/ P2 `+ j& T
  11. . K" X5 M5 j3 w8 o1 s: M
  12.      /* 初始化读命令 */4 l) @" @4 C6 G; w  m* }
  13. * Y" I5 A2 F$ s/ v
  14.      s_command.InstructionMode   = QSPI_INSTRUCTION_1_LINE;! a3 Z# A3 @+ I0 f

  15. 4 u8 T7 o  g0 k1 P% _8 P+ J
  16.      s_command.Instruction       = QUAD_INOUT_FAST_READ_CMD;3 b) H; E7 b8 {) j; |& }8 t

  17. ! R+ n+ _1 A3 _3 d  q
  18.      s_command.AddressMode       = QSPI_ADDRESS_4_LINES;
    , b: h+ R. B: \! e) T3 E9 k
  19. , a5 {# V: `9 i0 C5 e
  20.      s_command.AddressSize       = QSPI_ADDRESS_24_BITS;7 k: z7 X+ h- W  q
  21. / \7 U+ v& w2 {* y9 N+ F
  22.      s_command.Address           = ReadAddr;
    9 m. x% Y8 l4 o; G* n+ ]

  23. 8 ^) o* W0 v; h  B8 B3 h
  24.      s_command.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;8 r% c. \$ p9 m# g
  25. + n8 a1 r' I8 j; W
  26.      s_command.DataMode          = QSPI_DATA_4_LINES;7 Z, I$ C4 p' K. W) R

  27. & i, y5 r* M9 J5 ]  J) P. l
  28.      s_command.DummyCycles       = 10;
    . e" e+ d. h3 Y. ~
  29. ( ^$ C9 ^/ x# d1 r3 T1 @% R
  30.      s_command.NbData            = Size;1 P& C: y- y4 `9 j6 _9 I! `; M% W
  31. 4 h' [. @) k) q: J& A3 E% h0 q  W
  32.      s_command.DdrMode           = QSPI_DDR_MODE_DISABLE;
    , c- N% f$ B. z, R% S2 v
  33. : W  t" X! q' A0 s2 N
  34.      s_command.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;7 h" h7 }. _1 ]! N1 o
  35. / T6 [" k8 d1 X0 `6 I; p, ?
  36.      s_command.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;
    9 l) S* _5 c* T! ?0 x3 m1 D% L; g
  37.      /* 配置命令 */# f0 N" ~8 a' N2 `% f# I1 }. z
  38. - k9 A4 V: D8 H; e6 o! c2 D, r! j' W
  39.    if (HAL_QSPI_Command(&hqspi, &s_command, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
    4 X5 d4 f* }$ f! Z) f* Y; ?* U" \
  40.          {1 r3 q1 {! `- E( y  H; ~
  41.                 #if  QSPI_DEBUG
      \% Q8 f! }+ s, M  Y8 q
  42.                 printf("R-send read command error!\r\n");, m7 T' o) d; F. Q
  43.           #endif
    2 C& f# q8 T) z$ V0 k, z9 n1 \
  44.     return HAL_ERROR;0 D! s! |' a( q7 s  ?
  45.    }  + V: k) o3 ~8 ?" H3 \: J5 F
  46.    /* 接收数据 */+ j  @0 w1 I2 O1 G/ ^  `5 _/ |6 }: O
  47.    if(HAL_QSPI_Receive(&hqspi, pData, HAL_QPSI_TIMEOUT_DEFAULT_VALUE)!= HAL_OK) # ]7 P2 {$ U6 b  ]! E
  48.          {
    / T0 k; d( K9 Y  o7 t
  49.                 #if  QSPI_DEBUG+ x' G: ]  Z9 @
  50.                 printf("R-read data error!\r\n");+ L" a1 ?" z$ k: X" [% c6 V2 M
  51.           #endif9 Z: F! V6 t2 K4 o. o
  52.     return HAL_ERROR;
    . J1 k! y' c4 T: ?2 {- H
  53.    }, W- [8 Q  X; J# a7 y8 e
  54.    return HAL_OK;
    & m* a# e3 f3 \4 ^8 T+ I( p
  55. }
复制代码
% W% I/ a5 H" L: ]+ @; R, n+ Z
本函数实现了从FALSH指定地址读取指定数量字节的功能
9 z. O3 B  u4 U: ~( _
' ~4 c, R3 M6 f8 s8 e 7.写FALSH
' U  e, D5 ^8 \- c2 E6 [6 u1 P8 x4 J1 D% L. X3 p
同读FALSH类似,我们需要在手册中找到4线写FALSH的时序:' l: F0 g- l: q' i8 c" C9 z
" |8 ~2 C2 X! Z1 _. [
20191006135836784.png
- A* J+ [3 W8 y) a) V
0 u  s# N  M1 [: A. ~& Y( Y" M
根据时序图配置如下:1线指令,1线地址,24bit地址,4线数据,没有空周期 ! ?) m% B6 H! T5 i1 C; Y/ R' U5 o

8 N6 @6 z+ ]3 I) y' M) u& B
  1. /**
    $ a2 D2 G0 G9 S, D9 Y8 Z( f
  2. * @brief  将大量数据写入QSPI存储器
    ; D# `  b( L/ k# q' _
  3. * @param  pData: 指向要写入数据的指针
    # a: c  r* r( a
  4. * @param  WriteAddr: 写起始地址' f* d2 P9 Y: h9 A" z" G
  5. * @param  Size: 要写入的数据大小0 P; U- a) Q8 ~, b/ i; u+ t
  6. * @retval QSPI存储器状态
    8 V$ g* o7 ]( b0 p1 ?# E; i) z7 e. q
  7. */
    0 l2 j3 @3 F9 j! Q
  8. HAL_StatusTypeDef BSP_QSPI_Write(uint8_t* pData, uint32_t WriteAddr, uint32_t Size)
    ( \0 i5 f( }/ b
  9. {7 f5 X0 l. O. ]* B4 N. [% J2 E2 f
  10.                         QSPI_CommandTypeDef s_command;. I% x; q" S3 Z  P* c
  11.                         uint32_t end_addr, current_size, current_addr;  q& l1 M: R6 P& T# o. A8 |0 u; l
  12.             unsigned char sector_start,sector_end,i;
    8 ]" N& I+ H% t& n
  13.         / S/ W3 k, {' W! q4 S3 ~
  14.                         /* 计算写入地址和页面末尾之间的大小 */( Y3 R- w( N  I+ \$ n) C8 l, N' \0 b
  15.                         current_addr = 0;
    , j+ Y: Y) w( C" B$ Z* |& ?* C  _
  16.         0 h* {. {# S; I- B0 e3 z: {6 ^
  17.                         while (current_addr <= WriteAddr)
    - Q6 M* |9 J8 A" p
  18.                         {
    : J$ n$ U, c& s$ [" }" ~" C
  19.                                         current_addr += N25Q128FV_PAGE_SIZE;7 T1 @% D$ P3 _
  20.                         }7 M- P! B5 ^. Q1 h$ m! G3 U/ Z
  21.                   current_size = current_addr - WriteAddr;
    $ ?. R* P3 I2 \% L5 f' K7 J9 y4 F
  22.      /* 检查数据的大小是否小于页面中的剩余位置 */6 @5 R) |" x+ T& E5 d, j
  23.      if (current_size > Size) ' _7 P+ K+ b( X( }/ Q& P. A
  24.                  {
    2 V  d8 Z+ y1 Y! L
  25.          current_size = Size;& w1 v7 }; n) n
  26.      }; Y) f: d3 M" w
  27.      /* 初始化地址变量 *// c# p2 v! F4 m8 D; P
  28.      current_addr = WriteAddr;) j+ Q: ?; N  b6 y; j
  29.      end_addr = WriteAddr + Size;
    ; O$ o9 l" b) g4 w, f( {! ]
  30.                  
    1 ?6 p# k* L1 M: t7 J1 B
  31.                  sector_start = current_addr >> 16;2 ?- k& _1 `2 h; p
  32.                  sector_end = end_addr >> 16;# Y5 u' Y0 r0 L& W- _
  33.                  
    / u$ w7 F# u- J
  34.                  for( i = sector_start; i<=sector_end ;i++)7 V  ]1 e. |2 l8 Y% p' F
  35.                  {
    " ]& o" N" ?' q2 G9 P3 w! R& B
  36.                     BSP_QSPI_Erase_Block(i); //擦除对应删除
    3 f6 j+ s7 z" U
  37.                  }
    . Q) }$ Q3 ^5 d/ N5 B
  38.                  # x1 }1 T5 o! M1 I# w
  39.      /* 初始化程序命令 */" `# L$ o2 z4 n) W
  40.      s_command.InstructionMode   = QSPI_INSTRUCTION_1_LINE;
      s: Q+ N5 Q9 g& [- L; t

  41. 7 i; Z  w* i3 m( o
  42.      s_command.Instruction       = QUAD_INPUT_PAGE_PROG_CMD;5 b7 O& O5 S* i( N7 {; Q
  43. % t9 N6 f9 o! z" B! q6 T
  44.      s_command.AddressMode       = QSPI_ADDRESS_1_LINE;8 h2 ^* ^- z# F& h6 M$ l  r
  45. 5 e0 }; s$ A- m5 I0 X( x$ p
  46.      s_command.AddressSize       = QSPI_ADDRESS_24_BITS;
    0 f. h9 p# T" G  d) P' \

  47. & I* Q/ f' \. M
  48.      s_command.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;# ^, U: F* d# X

  49. % }; H; N# i0 j. b  |' P
  50.      s_command.DataMode          = QSPI_DATA_4_LINES;, K2 s& f. _4 N3 _. b/ h

  51. 0 y9 P$ f+ g, I9 M' W
  52.      s_command.DummyCycles       = 0;
    , c; h$ e* X' z1 ~2 k
  53. * ~6 f1 m- h8 A9 T7 b( @
  54.      s_command.DdrMode           = QSPI_DDR_MODE_DISABLE;
    + P- g" C+ _; `& |- L4 g. n
  55. ) [( p* L; a& D, [! Y3 C
  56.      s_command.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;
    & U. o: X- N3 t" H. a

  57. 6 }  O4 r8 f4 I( p+ j! Y# `
  58.      s_command.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;
    6 S2 a. c- }6 n) X
  59. / @- m" O0 I- ^; J7 I. h, `2 T
  60.      /* 逐页执行写入 */# n' s8 ~# _8 I7 B7 a
  61.      do {6 H( t( {3 e, @- Q' K8 w1 e( F1 q& k

  62. ( _8 p& p4 V8 B% [& t1 b
  63.          s_command.Address = current_addr;; u; C- u3 [7 }7 P' C: k
  64.          s_command.NbData  = current_size;: o* d( I" V5 ~( c7 g+ M
  65.          /* 启用写操作 */- c. M7 t; O$ e2 m3 Q- s
  66. ! O! A8 W3 T/ S  r
  67.          if (QSPI_WriteEnable() != HAL_OK) + C- O0 u6 B! [6 |6 J
  68.                                  {- y* D6 X' D8 {. L, n2 k
  69.                                                 #if  QSPI_DEBUG" A( E2 @3 U! N7 G& Y  ?0 Q2 ?
  70.                                                 printf("W-Qspi write enable error!\r\n");# k, o! r, Q, q& [7 p0 Y2 u, i* j
  71.                                                 #endif" T! `! _' ]  t9 e& V. U
  72.             return HAL_ERROR;& x. Y4 g2 ^# W% b( E
  73.          }
    0 T1 v7 G/ a$ y/ [1 u- S
  74.          /* 配置命令 */' ^8 w% R! W' M
  75.         if(HAL_QSPI_Command(&hqspi, &s_command, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)* ]' o) y5 V2 v3 Z7 ^
  76.                 {                                 
    0 L- g7 h% G- t2 {( W) Q) r7 X2 b7 r+ ~
  77.                                           #if  QSPI_DEBUG
    1 \5 C: `. B7 e" o* r
  78.                                                 printf("W-Qspi send Command error!\r\n");7 S6 b( h4 i4 b7 A0 v
  79.                                                 #endif
    $ c7 |4 `# g1 @5 G$ D0 g- W/ i. Q
  80.             return HAL_ERROR;6 _! z9 ~6 F$ O9 ~1 ?
  81.          }
      l- s1 \6 V- U5 g5 X9 z1 T
  82.          /* 传输数据 */
    % ?1 f* R# ~3 z0 `: I
  83.          if(HAL_QSPI_Transmit(&hqspi, pData, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK) + Q' v( G& r# F- Q# k* K9 O, s
  84.                                  {6 ]2 g8 J8 N/ ]- z5 N2 ?
  85.                                           #if  QSPI_DEBUG/ Z- q! u6 _5 q0 G" O, w! p5 P! P; J
  86.                                                 printf("W-Qspi Transmit data error!\r\n");
    $ h6 b/ \% E5 \$ y  K! A+ d* ?4 Y
  87.                                                 #endif& s. Z# Z2 B- G6 J4 m! M2 I
  88.             return HAL_ERROR;
    8 C4 w8 g5 Y3 g5 [0 P$ a5 d( ~5 U
  89.          }
    $ L. q  R/ w" K0 D. z0 G
  90.          /* 配置自动轮询模式等待程序结束 *// V/ v' I& b- ?; B! {" M' W2 @
  91. ' W3 K2 U8 H  q8 ]& r
  92.          if(QSPI_AutoPollingMemReady(HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)# f8 C2 [% O. D1 M) J1 G
  93.                                  {                                         4 \; C* v" y. B
  94.                                                 #if  QSPI_DEBUG- A% d/ S0 z9 V) h9 [
  95.                                                 printf("W-AutoPollingMemReady error!\r\n");* Q, Q% K) S* R
  96.                                                 #endif
    4 h) p$ k% s* f5 g& h4 i
  97.             return HAL_ERROR;
    & w, d3 x; M: w* _4 ^& s
  98.          }
    $ M2 _" S( c/ `; D, W+ w8 N
  99.          /* 更新下一页编程的地址和大小变量 */
    . `0 ?- N8 B; o- r

  100. : d% L% E; m; z. F% K" K: _' ?
  101.          current_addr += current_size;) U* H9 M+ z' r

  102. 8 E: u7 D0 ^$ g$ a
  103.          pData += current_size;
    0 T7 ~- Y# O* O. i! g
  104. $ q+ l& M0 [9 r0 `* F6 Y( ~
  105.          current_size = ((current_addr + N25Q128FV_PAGE_SIZE) > end_addr) ? (end_addr-current_addr) :
    * Y1 ?# \; F* G8 ^4 I3 g

  106. 8 ?* E! C4 G3 E- G2 A% Z. Z/ `8 ^
  107.                         N25Q128FV_PAGE_SIZE;
    . `- J% M, R1 M

  108. ; i, f" C; @: v8 k6 v/ @# t
  109.      } while (current_addr < end_addr);
    0 p" V) B- X$ _4 L4 c6 f
  110.      return HAL_OK;
    ( o5 s) V# q1 u$ V
  111. }
复制代码
: |8 z  D3 R- r) t3 Q
该函数实现从指定地址写入指定数量的字节。
; Z+ ]: X: T9 u- `3 b+ j; i3 ^0 ~5 E) Y3 L% ?
8.擦除FALSH0 ?( f+ b! T% E" M( ?
- N' e9 g3 h% g
第一种情况是按扇区擦除,这也是N25Q128最小的擦除单位(N25Q128共有256个扇区,每个扇区64KB)& W5 \) }2 k* A  E- d% @' `
/ S* i* F. z2 |# N9 w
20191006140420479.png

; R5 X1 w3 n! Y
9 V$ g1 j. w5 @- w/ m( J# N参数设置为1线指令 ,1线地址,24bit地址,无数据阶段
6 _( m4 n. ?8 ^5 \6 A0 U4 ?+ B# I$ I/ t: h) L% u; s% j+ m
  1. /**
    . [+ \( N0 B) K, Z. `: s' [
  2. * @brief  擦除QSPI存储器的指定扇区3 N: G2 p+ m  R" ~- G/ ~8 i
  3. * @param  BlockAddress: 需要擦除的扇区地址7 m4 x5 h% j/ E
  4. * @retval QSPI存储器状态
    / x8 A& b. x1 w
  5. */
    - U7 {, b- s8 }  \
  6. HAL_StatusTypeDef BSP_QSPI_Erase_Sector(uint32_t Sector)+ ?& {( X6 {* h0 v) T9 |4 _
  7. {
    % ^- W0 K- _: W* G, o! Z
  8.            uint32_t SectorAddress=Sector << 16;//Sector*65535算出该扇区对应的地址" [3 f2 }% S# t7 Y3 ^( X+ J
  9.         # ^  f1 \% q+ N0 r7 O& ?
  10.      QSPI_CommandTypeDef s_command;+ d) c+ d2 Q+ h% [7 t
  11.      /* 初始化擦除命令 */
      S2 R! ~6 ^) A% Z
  12.         7 X* U  f# c& P0 d! I
  13.      s_command.InstructionMode   = QSPI_INSTRUCTION_1_LINE;
    # J+ W4 T$ [6 q5 D) E1 |( {  M

  14. 8 J; s0 h. y$ ~' w! }0 O9 G
  15.      s_command.Instruction       = SECTOR_ERASE_CMD;//按扇区擦除/ ^) X) f! A9 t+ H8 a) ?5 w/ c
  16. 7 t' ~( |6 ?6 n1 Q# Y3 c  ?
  17.      s_command.AddressMode       = QSPI_ADDRESS_1_LINE;/ R, K# x1 J$ E0 @1 L9 i5 D
  18. % d/ t5 M/ W; l5 Y
  19.      s_command.AddressSize       = QSPI_ADDRESS_24_BITS;
      j. y; W6 {- q5 B
  20. / u5 s" \' D0 C! N6 `1 Z0 P
  21.      s_command.Address           = SectorAddress;: ^- M3 s8 k* F9 r. i- }
  22. / K' q$ i5 n4 w& q! H) Z
  23.      s_command.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;+ D5 y; I4 C' [+ u% H( ]
  24. / T5 W- x: n- s9 z% n0 W
  25.      s_command.DataMode          = QSPI_DATA_NONE;
    0 I0 u  F! n' _

  26. 7 F& ^! w8 w0 P8 r  C4 C
  27.      s_command.DummyCycles       = 0;
    7 `+ W4 \+ n! g$ t3 ^" P$ e2 O
  28. ) N- ^, k3 w9 q+ ], y+ e
  29.      s_command.DdrMode           = QSPI_DDR_MODE_DISABLE;: f  s" {+ p2 b

  30. , `2 r* e1 y% ^! D4 U4 j* P
  31.      s_command.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;: l$ j' c; P6 r8 F3 U  X: l! R

  32. ( |& E5 n4 ^/ i, r6 r( u7 t
  33.      s_command.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;
    . z1 K! o% y! K+ A) M

  34. & O, U9 a  N9 Z6 i5 ]
  35.      /* 启用写操作 */5 x, b' {4 g# l% V( F( f+ c

  36. ! {3 v% W; I" c6 H" U$ l$ }
  37.      if (QSPI_WriteEnable() != HAL_OK) - ^1 ]. L  Y6 L2 W$ h( d  h- _9 _
  38.                  {7 B8 a1 i& k/ G6 M6 r
  39.          return HAL_ERROR;
    . b, w# b1 T! E5 \2 K" `( \
  40.      }8 ^) o4 P* }) K- @* o0 e/ d
  41.      /* 发送命令 */
    ; p( H8 T& \: V! k
  42.      if(HAL_QSPI_Command(&hqspi, &s_command, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
    $ y' y0 D" D8 z. |' |: m# E
  43.      {
    9 q& O5 j  A) T+ U4 c) G9 k) T
  44.          return HAL_ERROR;  W4 ]4 ~* ~+ w7 k
  45.      }9 j2 v# @  {/ K' F  ?: T
  46.      /* 配置自动轮询模式等待擦除结束 */0 n& b+ O. x+ b  }& P% a4 |

  47. 1 E# G6 o2 c; }/ A/ t
  48.      if (QSPI_AutoPollingMemReady(HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
    " q6 @/ U# @2 y  d; |8 {
  49.      {
    . z& _8 I5 T9 }" K$ t# u% m
  50.                                 #if  QSPI_DEBUG* X0 D  U* |1 S' y
  51.                                 printf("erase sector error!\r\n");
    6 x. ?9 _9 @9 k: P) `
  52.                                 #endif
    5 i( C3 C3 C- l" r; r4 c$ S
  53.         return HAL_ERROR;
    2 {* L& ^' X% d3 x$ T6 I" q
  54.      }
    - k& |+ X! B6 l- h
  55.     return HAL_OK;
    % ^5 O9 Y6 v; q% C1 H+ e
  56. }
复制代码

. a; [" ]% |* C% z第二种情况是全片擦除,时序如下:
# S( E: w5 o* J' U: I. @/ U+ K" C' O% [6 ~6 }
20191006140919805.png

# D, I: H' k" M! c# o  A+ }, k: y- {9 ~5 `. r- b/ G9 B
全片擦除仅包含指令阶段,对于N25Q128来说,全片擦除耗时可达48s。) Q1 O0 ~; N( h) _

  i( c% B9 x8 w1 L5 ^# m7 D" X
  1. /**; c1 r2 V; w) e5 i
  2.   * @brief  全片擦除  N25Q128全片擦除耗时48s$ Q1 q8 F5 V: r, p. N
  3.   * @retval QSPI memory status7 ^% v8 B7 v" g  a' W
  4.   */
    0 ^. N2 K' Q, f) x( n
  5. uint8_t BSP_QSPI_Erase_Chip(void)! I6 y9 S, O: h# b& C# f
  6. {+ k9 t: j0 n6 M- h5 w$ f  [
  7.   QSPI_CommandTypeDef s_command;/ b0 r2 Q: j. W! F
  8. % s2 U0 C( j' _
  9.   /* Initialize the erase command */
    8 I; p- E6 I3 Y: E! H% a3 i, a% J
  10.   s_command.InstructionMode   = QSPI_INSTRUCTION_1_LINE;7 B- C; x8 d) K2 `
  11.   s_command.Instruction       = CHIP_ERASE_CMD;
    6 g' p3 k8 p, S0 i; {
  12.   s_command.AddressMode       = QSPI_ADDRESS_NONE;
    : T8 z1 _% y% r; b
  13.   s_command.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
    $ Q% [, S3 c, {8 P; t3 t
  14.   s_command.DataMode          = QSPI_DATA_NONE;8 i6 N/ @, J9 K  h# A& J
  15.   s_command.DummyCycles       = 0;8 x! y, i" f% u, K2 I9 {% a7 V
  16.   s_command.DdrMode           = QSPI_DDR_MODE_DISABLE;
    6 J. }/ S; d% q# v. L
  17.   s_command.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;, ~; |7 C. Z! J6 X* s( G# r
  18.   s_command.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;5 `1 n  r/ D1 E" @" E

  19. & L2 Q! h! L/ W  T+ u- y
  20.   /* Enable write operations */
    - j2 @! q  I. k7 x
  21.   if (QSPI_WriteEnable() != HAL_OK)
    + E! U' C" w3 c5 a
  22.   {
    4 C3 A$ L" M; o4 K; N  j% q
  23.     return HAL_ERROR;
    ) q. k$ [' F1 I! ]" v7 h# F- q
  24.   }% G0 V( k# D9 ^4 U8 e: t

  25. 2 [; N' }' G" u; t+ s
  26.   /* Send the command */9 x8 \% s$ ?( X, ^( F( a3 l, Z
  27.   if (HAL_QSPI_Command(&hqspi, &s_command, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)! a4 w, y' x' D# n) [6 l/ g  W
  28.   {
    ; I; w2 R% |0 F2 j  q/ `
  29.     return HAL_ERROR;
    * t: O! f2 ?. v% j& n2 M* K3 E
  30.   }
    + Q1 O* g- g7 `, ?3 @

  31. 6 M. }, {% P8 j0 ^5 u$ _
  32.   /* Configure automatic polling mode to wait for end of erase */  # i# O' e4 i3 v- x
  33.   if (QSPI_AutoPollingMemReady(N25Q128A_BULK_ERASE_MAX_TIME) != HAL_OK)
    0 R5 k" X/ T3 @/ G
  34.   {' w8 c3 }) V- Z
  35.     return HAL_ERROR;. B% ~$ m0 H, b6 f) [
  36.   }% s- z% Y& S/ p  b) F

  37. % B, b7 k. u& d3 s1 C4 p
  38.   return HAL_OK;
    5 f3 E  @6 y- ~, Z: d5 h3 A
  39. }
复制代码
/ K; W6 s6 p' y$ t
9.测试程序- H& K6 t( S5 B; k; ?

& z5 z% X  j' ^: K/ y4 O5 ~/ s6 i3 I测试芯片写入和读取是否成功,首先向0x1FFFF地址连续写入4096个字节,然后再读取出来,对比两次数据是否一致。. d2 M, l& n' x/ g
" S. I8 r6 S0 p: V
  1. unsigned char write[4096];
    + S! l6 u/ e" X. F' W2 ]# B
  2. unsigned char read[4096];   ) F4 N5 ^5 C; v9 K* B+ G
  3. void N25Qxx_Test(void)  Y+ e* B' C- b! }/ l& K6 Z0 U# P
  4. {
    $ T, q6 G. X. X. d9 X# W
  5.   uint32_t i = 0;
    , ^5 n, L/ I; ]& r( s: S# t; g
  6. 8 q' g5 }  A* X4 f5 g2 S
  7.         for(i = 0;i<sizeof(write);i++); o, B+ Q) }0 [1 O
  8.         {6 K/ {9 y, @3 T$ v; _
  9.     write=i;
    $ ?& b& ~$ j5 M
  10.         }, r% {0 c5 [0 m) l; C! V
  11.         ) E" {1 `; K, o$ C
  12.         printf("write data:\r\n");        6 X; F) x8 t, i) j. G
  13.   for(i = 0;i<sizeof(write);i++)
    7 I3 j+ m6 n# V/ Y  A
  14.         {( ~3 G+ i: [: b4 I1 }- P2 D
  15.                 printf("%d ",write);+ Q% W9 x- ?" p  o6 k$ V' z
  16.         }* u  M2 @- b% b; S& X- c( |
  17.   printf("\r\n");
    6 _; _" k5 c7 ~7 E! `9 k  ]
  18.         printf("\r\n");
    : {0 F( W9 m: C  w  \+ c3 a
  19.         printf("\r\n");2 X, H8 C+ w9 Y* C6 m
  20.         
    ! j8 T  O/ |2 Y1 L2 g% ~1 x
  21.         BSP_QSPI_Write(write,0x1FFFF,sizeof(write));
    1 @4 S3 {2 @6 a$ v
  22.   BSP_QSPI_Read(read,0x1FFFF,sizeof(write));        0 F. w0 q9 V# {! g& A
  23.         / m7 u) {  j9 h- y. v
  24.         printf("Read from QSPI:\r\n");
    " O& ^' N1 b4 e1 X' R8 e( l5 G
  25.         
    8 k- P  [% M- D; t; j4 v
  26.   for(i = 0;i<sizeof(read);i++)
    ! T( a7 Y+ X1 R0 Z
  27.         {
    ) X& N2 q( s! S9 l: z2 m+ L, \/ L
  28.                 printf("%d ",read);2 ?* d* B* B- l7 X$ E
  29.         }
    + m* o" p8 k9 [
  30.   printf("\r\n");; U2 @1 O; ^; E+ I& u, e
  31. }
复制代码

9 l8 X" S" M8 C; f& Z写入的数据(部分截图):
5 o6 j& _5 `, l* w
" N0 a* P0 R; \9 Q9 y. t3 [+ j
20191006141636369.png
" y( M. a; b0 \1 b+ f
4 A9 I) t3 B* U9 e( [( X4 j8 [
读取的数据(部分截图):2 }4 d) A1 P+ D/ i8 D; L) {, n. y
7 K7 k% }. `: o( j5 A. `- ^  ~# d
20191006141537697.png

9 j$ V0 `: S8 A. _: K3 S2 V  ]! U) \" L
截取相同的部分,两次数据完全一致,写入和读取成功。5 F, E% i- \5 N8 r; I' z/ S

/ R) m4 N3 I0 P# ~$ \最后,讲几点需要注意的地方:! @9 v; ]) B8 \: H

# R2 E% Q' N" |+ P4 u1.N25Q128 与  W25Q128 有很多不同的地方,特别是4线模式下,二者很多地方不同,代码不可混用,比如W25Q128的很多指令都是支持4线的,但是N25Q128的指令阶段均不支持4线模式。
8 b% |: H- [7 R
5 I% k" z7 ?2 {2.硬件上使用了4线SPI,但是仍然可以使用单线SPI操作,只是速度慢了点,其他的没什么影响。: a5 p, G/ I/ @1 t

2 P3 u9 S0 s" \" H; }3.N25Q128在配置为内存映射模式时,外部 SPI 器件被视为是内部存储器。QUADSPI 外设若没有正确配置并使能,禁止访问 QUADSPI Flash 的存储区域。即使 Flash 容量更大,寻址空间也无法超过 256MB。如果访问的地址超出 FSIZE 定义的范围但仍在 256MB 范围内,则生成 AHB 错误。此错误的影响具体取决于尝试进行访问的 AHB 主设备:! ?7 C$ @3 n$ V2 i7 ^5 B: [
 如果为 Cortex ® CPU,则生成硬性故障 (Hard fault) 中断
% P4 j6 d; R! c1 [% ?# X! ? 如果为 DMA,则生成 DMA 传输错误,并自动禁用相应的 DMA 通道。
& o6 M; m# O# }; I0 X5 D' X0 R# s- ~0 f
4.N25Q128包含248个主扇区,每个扇区64KB,还有8个64KB的启动扇区,这8个启动扇区又可分为128个子扇区,每个子扇区大小为4KB,所有的扇区擦除都可以按照一次64KB大小擦除,但是这8个启动扇区也可以按照4KB一次擦除(需要使用SUBSECTOR_ERASE_CMD指令擦除)。
$ q9 W7 R" ?1 w/ a* e+ X! f3 d8 ]: i" {7 @, |% t
5.N25Q128分TOP和Bottom架构,两种架构的区别就是启动扇区的位置不同,TOP架构的启动扇区是248~255(地址范围:F80000 ~FFFFFF),0~248为主扇区。Bottom架构的启动扇区是0~7(地址范围:0~7FFFF),8~255是主扇区。这两中架构可以通过完整的型号区分。  b+ I. Y' |( I$ [6 h$ v9 a5 O

$ }7 K$ |0 ~- Z5 [3 w7 B! ]7 C" F/ Q
20200316162249870.jpg
% A7 C* g$ [4 S) W, s) C

* f& _% N+ I  e& l8 F* H# ^9 D6.N25Q128和W25Q128 不可以相互替换,除非使用单线SPI通信,且只能使用F80000 ~FFFFFF或者0~7FFFF范围内的内存。
4 [! k5 W0 u4 I3 ]# J( D/ F5 Q9 f3 ^* Z. u: G, d+ W( I" \
7.N25Q128的指令在不同spi模式(单线、双线、四线)下有所不同,使用时一定要特别注意!!!
1 p1 _; {: a& ^4 n2 P
+ `' E; F$ S. H- j6 x  d8 L  r3 k5 L
) N# z# N: N6 V" ?$ l1 t+ V8 X
赞 收藏 评论0 发布时间:2021-12-16 21:00

举报

0个回答

所属标签

相似技术帖

官网相关资源

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版