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

基于STM32的语经验分享—SPI读写FLASH

[复制链接]
攻城狮Melo 发布时间:2023-3-18 15:00
FLASH简介
* c: u- L! A3 r& q2 P/ b4 d# m/ a4 qFLASH俗称闪存,和EEPROM一样,都是掉电数据不丢失的非易失行存储器,但FLASH的存储容量普遍大于EEPROM,现在像如U盘、SD卡、SSD固态硬盘以及STM32芯片内部存储程序的设备都是FLASH类型的存储器。由此可见FLASH对于我们学习和工作的重要性,EEPROM可以实现单字节的擦写,而FLASH都是一大片的擦写,就像是大规模杀伤性武器,其最小擦除单:扇区的大小也是4KB。
' m* _% L( C% F5 k7 c我们此次通过SPI对FLASH存储芯片W25Q64进行读写擦除的操作。9 z7 Z7 t# q% R  u$ O! H: p

! r* D9 q% {# A  {对于FLASH内部结构的详细说明博主会专门整理一篇博客来说明,所以关于FLASH芯片的相关原理,本文中只做简单说明,侧重代码部分。& z* r' K1 c2 ?: u2 m
FLASH详细说明的博客链接:(没有链接就说明还没有整理出)
2 d9 w. ^9 e# E! h- j% }5 Q4 L6 m% B, a4 W
W25Q64
6 L1 z6 S+ w% U$ i' MW25Q64简介
# t9 a2 U# s6 a! g* b4 h3 y
就长这么个样子2 X2 A% ]/ x& V, {+ [1 A% s# r
  U6 L% b1 R: z* t
20190814195953540.png
6 Z& T9 K5 I6 w% d- I) F3 i

6 p: r8 `5 P+ ^1 M, B* }8 ^STM32内部原理图如下:  |0 X% x9 x' G7 Z4 F( h# g) P
) ^# O! r2 H8 X# f6 J" |
20190814200413370.png
$ P5 \3 h6 q; b1 \: ^! [2 S
4 k# ^* [5 W: I
8 q0 q4 u* K# Z1 E
W25Q64是一种使用SPI通信协议的NOR FLASH存储器,它 的CS/CLK/DIO/DO 引 脚 分 别 连 接 到 了 STM32 对 应 的 SPI 引 脚NSS/SCK/MOSI/MISO 上,其中 STM32 的 NSS 引脚是一个普通的 GPIO,不是 SPI 的专用NSS 引脚,所以程序中我们要使用软件控制的方式。FLASH 芯片中还有 WP 和 HOLD 引脚。 WP 引脚可控制写保护功能,当该引脚为低电平时,禁止写入数据。我们直接接电源,不使用写保护功能。 HOLD 引脚可用于暂停通讯,该引脚为低电平时,通讯暂停,数据输出引脚输出高阻抗状态,时钟和数据输入引脚无效。我们直接接电源,不使用通讯暂停功能。
+ t% q8 I9 d) m! o* w) l5 q- a! ]
W25Q64支持SPI通讯的模式0和模式3- h' y' ~# ]9 E) b. W
; G7 p  ^' c  n

- m) f( i) b% n. Y3 d" aFLASH控制指令. g; i" s0 J7 o( Q, R6 {& Q, q
FLASH芯片中规定了许多指令,只要SPI向FLASH发送相应的指令,FLASH就会执行相应的操作,所以我们对FLASH的一切操作都是基于这个指令集的,接下来介绍一下FLASH的控制指令:
2 H2 d9 i, i, j5 V6 G+ e" S. S9 q" t+ s. A( R5 K2 _* |
20190814200912265.png 4 a( p7 o% K' z

' J3 ?; J3 D# d表中第一列为指令名,第二列为相应的指令代码,第三列及后面的内容根据指令的不同而意义不同,其中带括号的字节参数,方向为 FLASH 向主机传输,即命令响应,不带括号的则为主机向 FLASH 传输。表中“A0~A23” 指 FLASH 芯片内部存储器组织的地址; “M0~M7” 为厂商号( MANUFACTURER ID); “ID0-ID15”为 FLASH 芯片的ID;“dummy”指该处可为任意数据;“D0~D7” 为 FLASH 内部存储矩阵的内容。
0 B/ I  J4 @6 Q* B看起来很复杂的样子,其实只要在需要执行相应操作时来查这个表,只要能够理解这些指令的使用方法,FLASH就算学会了。: Y& D% ^3 H5 Z8 L/ c( r

1 H+ H+ X! Q  m( Y: g) r/ }: V
例如:要知道FLASH的ID,那就在指令中找对应的取ID指令“JEDEC ID”,仔细解读这个指令, s; G, k/ K* n# H; u& H
20190814201942797.png
9 F/ Y7 b  B) b5 i
2 V7 e  }6 m  e: {
可以看出对应的指令代码为“9F”,后面的三个字节带括号,代表这三个字节就是FLASH向STM32发送的数据,即这三个字节就是FLASH的ID,然后使用SPI进行读取就可以了。, X, g5 U) O  g8 W" s! L8 M
8 C& k7 x- S* g
我们一般是将这些指令宏定义在头文件中,便于使用:
/ ?& X; A7 a- T8 m% u
  1. #define W25X_WriteEnable                      0x06
    1 G2 k# o7 T, J  {. F( i8 T
  2. #define W25X_WriteDisable                      0x04
    0 |2 L5 I0 i( ~8 g" ?  y3 d/ {! o
  3. #define W25X_ReadStatusReg                      0x05
    1 {1 E1 M; Y% Z3 l
  4. #define W25X_WriteStatusReg                      0x01 # a1 N# F4 M9 K  W
  5. #define W25X_ReadData                              0x03
    & A4 Z/ P- S" [, n. C7 G
  6. #define W25X_FastReadData                      0x0B 2 C8 T3 W+ I6 K% x" @. }' F& {" ?
  7. #define W25X_FastReadDual                      0x3B / M8 t3 u/ N. p$ M- w+ Y) p* V9 H/ T7 ~
  8. #define W25X_PageProgram                      0x02
    4 u5 }; h7 C% X! e) D+ N
  9. #define W25X_BlockErase                              0xD8
    : n, x) S+ Q5 _7 ^+ m4 ^2 H
  10. #define W25X_SectorErase                      0x20
    ' N& I' ?+ i# G( K
  11. #define W25X_ChipErase                              0xC7
    8 B2 p2 A0 ^8 H: ~) X, ]
  12. #define W25X_PowerDown                              0xB9 ( W- g7 J2 |) {
  13. #define W25X_ReleasePowerDown              0xAB
    " P3 V2 S' r) w0 G
  14. #define W25X_DeviceID                              0xAB
    ) V, ^, V) J) ?  W6 @
  15. #define W25X_ManufactDeviceID         0x90 5 v7 N3 Y; O' o% Y' C
  16. #define W25X_JedecDeviceID                      0x9F2 C, }8 k: a! }6 e; b( I4 Q
  17. ' H8 X) W7 U0 N4 Z6 a# l
复制代码
7 A6 o# K) ~* y  P# U% X% r
FLASH内部存储结构
% T1 U% r6 x+ [7 @2 H+ }FLASH的存储矩阵如图:其内存分为128块,每一块都有16个扇区,每个扇区大小为4KB,擦除数据的时候是以扇区为基本单位的。* a( t# Z1 }0 h5 Z: o. X; Z

8 z3 \* a; Z0 H- g: z: \4 e* ~
20190814203311343.png % G; O6 E& E/ h" L7 n  S- D: Y$ S# V9 [

) [3 n  A* X' C6 N1 L# O, R代码讲解
# m3 `4 B9 c: G; G* S6 N7 f) m代码都是博主亲手写出来的,可以运行。+ e6 Z- U9 U% I
代码部分会用到SPI的代码,关于SPI的说明之前整理过:SPI详解
7 h& E# o& |8 h2 N+ J  D( L5 R
1 c% f# s% u% j4 ?0 @4 N
读取芯片ID
5 b6 t) b( T5 G$ F0 C* {5 _2 {
  1. /************************读取芯片ID*************************************/
    8 |2 y; H, ]/ }
  2. uint32_t FLASH_ReadID(void)1 u- _. J. `% Q+ i2 U& }
  3. {
    , w9 ~( r% P2 h2 n. C
  4.         uint32_t temp,temp1,temp2,temp3;& I$ B- g0 u+ ?9 M2 ]
  5.         SPI_NSS_Begin();5 J8 [5 B) i- t0 h$ Y
  6.         /* 发送取ID指令 */
      y* @1 R3 }: _, q; U& C
  7.         SPI_SendData( W25X_JedecDeviceID);
    ) r5 g# M$ c" g8 A
  8.         /* FLASH会连续发送三个字节数据 */
    4 f  K% q- |( y) S
  9.         temp1=SPI_SendData(Dummy_Byte);# \" {# O; j3 E3 d0 t6 d/ H
  10.         temp2=SPI_SendData(Dummy_Byte);! L# u% Q- }2 u7 b" C. ~. y
  11.         temp3=SPI_SendData(Dummy_Byte);
    0 ^- |2 c% d' p8 O# u  F% J% p
  12.         SPI_NSS_Stop();* i& ]' F  @; @- Q) e1 u
  13.         /* 高为先行,将三个字节整理在一起 */8 B# `% T7 |9 Y
  14.         temp=temp1<<16 | temp2<<8 | temp3;2 t+ h7 B& X2 D1 J7 D; X
  15.         return temp;! Z% @: V# W; s5 {, o2 H$ c5 l+ J
  16. }3 [. j5 @+ s. N  s: g/ ~
复制代码

) k! v; u6 O' ~$ @3 N由于SPI是全双工通信,所以接收数据和发送数据用的是同一个函数
8 h, [1 M! z* w) V, W
0 ]& \) `! d. @
发送写使能信号
' q3 l" C3 A4 y3 p4 Z) _
  1. /****************发送写使能信号****************************/0 x: ]( k% g) h7 b
  2. void FLASH_WriteEnable(void): O1 O* {9 ]4 N- j
  3. {
    : m  K1 p+ r) l4 F
  4.         SPI_NSS_Begin();: b& J) v4 P: U9 @
  5.         /* 发送相应的指令代码 */6 k2 b& [4 {! X9 z) X) e* d
  6.         SPI_SendData(W25X_WriteEnable);; s  P" M. a7 W: _# H" A7 t9 n3 p# @1 x
  7.         SPI_NSS_Stop();- R0 R: e8 }+ D8 Y& X( v. D+ ], m
  8. }  g' q! m/ s$ v9 l2 u, v
复制代码
9 v5 W8 \+ u1 }3 b7 I- U6 s1 I
在向FLASH中执行写操作之前,都要进行写使能操作,通过发送写使能指令到FLASH来实现: L3 f- Z/ H* z/ u5 _8 b! ?, q
6 J8 I1 t" L/ c# _9 f6 ]6 |
等待FLASH不忙
& V3 v" |9 E* j# I: e  u. s6 n% r
  1. /******************等待,直到不忙***********************************/
    ' {5 d5 t( }- t7 t& f6 h
  2. void FLASH_WaitBusy(void)
    ' L* t. \. W. V6 R! B2 Z. _. D
  3. {6 u4 k3 O# [0 F, r, H3 ]& m4 Z
  4.         uint8_t StatusReg=0x01;
      G7 K0 P8 r  Z, F7 ?2 x) M
  5.         SPI_NSS_Begin();* [  Y" t+ E; V
  6.    /* 读取状态寄存器中的数据,判断忙标志0x01位 置位代表忙 */; n. }0 y0 W& H5 ]( ?! r
  7.         SPI_SendData(W25X_ReadStatusReg);      ) F8 D' ?; Z1 g' B5 |
  8.    /* 只读取状态寄存器的BUSY位,即第一位 */8 q( _- a/ R7 p
  9.         while((StatusReg & 0x01) == 1)
    # b. g" d4 f0 h# F4 W. W: l$ Y1 t  p8 n
  10.                 StatusReg=SPI_SendData(Dummy_Byte);
    1 E+ P) E& u8 {' t6 I, j. n
  11.         SPI_NSS_Stop();
    ) }: Y$ s6 U+ D& K) ~
  12. }& R6 T2 s0 b2 c& R7 q$ X0 Q5 l- r; e

  13. 4 f2 t; |- A# H5 x' q& r3 J2 I

  14. 9 Y% f. P" E. q3 h9 V) `
复制代码
9 M. ?2 z/ u3 l: F5 S( O
FLASH在通讯的过程中需要一定的时间来执行操作,在这期间,传输数据是无效的,因为FLASH忙着呢,所以我们就要有一个函数来专门等!等到FLASH不忙了,再进行通讯,那怎么等呢?FLASH不忙了会给出一个信号——将状态寄存器的BUSY位重置(也就是0),所以我们需要不断的来检测状态寄存器中的BUSY位是否置位,利用读取寄存器状态的指令来获取状态寄存器当下的状态,然后根据寄存器的BUSY位(第1位)来判断FLASH是否处于忙碌状态。
; P/ m! k) |0 a5 H& i6 u简单来说,这就是个延时函数,延时直到FLASH空闲,可以进行下一步传输。% z9 V& G% x9 r1 J8 L
+ K3 v/ ~9 c, j8 D( }7 M- C

4 h5 V5 E2 q* I& ]9 Y擦除扇区

6 {8 g1 y& D; W0 G# g
  1. /******擦除扇区的内容,切记地址要对其到4kB,每个扇区的大小都是4KB********/
    , _. c9 B; l& K5 J# ^, m
  2. void FLASH_SectorErase(uint32_t addr)
    % C2 n4 Q' D- y
  3. {
    - X" f- c+ C4 p8 g7 a& T- z; E
  4.         /* 开始的时候要发送写使能信号*/# |; B( U7 Y% ?$ F2 w
  5.         FLASH_WriteEnable();
    5 M& C8 n9 E9 [" C1 X0 U
  6.         SPI_NSS_Begin();3 S2 C* q; r8 p4 m4 W
  7.         /* 发送扇区擦除命令 *// ]# E0 L- B+ _
  8.         SPI_SendData(W25X_SectorErase);3 _5 h  A2 I- z
  9.         /* 发送扇区的地址,高位先行 */. w, Y2 K3 |  r' M6 D1 y* M1 y
  10.         SPI_SendData((addr & 0xff0000) >> 16);
    & J  r0 _) i  ?8 Q( V, \
  11.         SPI_SendData((addr & 0xff00) >> 8);
    8 H  K5 o  x6 z0 [0 t, L* W
  12.         SPI_SendData(addr & 0xff);% t  j8 {! o7 V, Z$ \
  13.         SPI_NSS_Stop();2 |) p' S1 a2 s. _4 C' q
  14.         /* 最后也要等待FLASH处理完这次的信号再退出 */; w% n- E" Y9 y  K3 Q
  15.         FLASH_WaitBusy();
    * ?: |2 @* q4 R' N- W3 X
  16. }
    % w; Q/ _  m( I6 H% X

  17. & |, g( E4 q4 ?3 h3 e0 h
复制代码
- Q$ k4 q, F3 A8 j" a
扇区的擦除之前要发送一个写使能信号,先发送擦除指令,然后发送要擦除扇区的地址(分三个字节发出去),高位先行。
# h: C3 V, {/ h6 B扇区上的内容不是1就是0,擦除的过程就是写1的过程(将一个扇区全部写1),因为在写入数据的时候,可以将1写为0,但不能将0写为1.2 n6 k, Z; K8 e5 Q6 t
% _# J3 p- R6 L& w

5 ]( ^: d6 r' R% v2 w* x* I写入数据
2 L( F2 O! Z. |8 R
  1. /************按页写入数据,但写入之前要进行擦除***********/
    ( j- o: K  }# F- N7 }6 d: g6 s
  2. void FLASH_PageWrite(uint32_t addr , uint8_t* pBuffer ,uint8_t size)
    $ t  t$ d; n, z# {; A
  3. {7 ?+ t) N1 h; J2 \7 {
  4.         /* 开始的时候要发送写使能信号 */
    4 w4 {+ z# O  z9 }& y
  5.         FLASH_WriteEnable();
    . }- V5 @# v$ I) D4 r( w" B% k
  6.         SPI_NSS_Begin();
    ! B, P9 b: s: A7 U3 c! D( u
  7.         /* 发送页写入命令 */' L( h" |' \+ H! Y  y7 J6 j7 N
  8.         SPI_SendData(W25X_PageProgram);1 s5 w8 k2 o0 s* E
  9.         /* 发送写入的地址,高位先行 */
    9 |! I% G  K4 t0 P
  10.         SPI_SendData((addr & 0xff0000) >> 16);
    & v* \3 b+ z' C
  11.         SPI_SendData((addr & 0xff00) >> 8);3 r  [9 x6 D% H
  12.         SPI_SendData(addr & 0xff);
    8 M5 u8 a' l2 ]# ]  d
  13.         /* 逐位发送数据 */6 Q. E, X3 M$ X5 x3 L- K
  14.         while(size--)( k. s. _2 ]* t
  15.         {# }+ S+ A  N/ K
  16.                 SPI_SendData(*pBuffer);2 m& b- X+ @0 H8 W- p3 x1 j
  17.                 pBuffer++;
    ; L7 H7 Z- K$ ]) }* c# {* f
  18.         }
    ; O1 X$ S. J. e4 j% }
  19.         SPI_NSS_Stop();* E+ j& z: Z" B. \, M+ e; `9 N
  20.         /* 最后也要等待FLASH处理完这次的信号再退出 */
    7 c( R( ^$ M" d9 l: x, M1 @* m# M& q
  21.         FLASH_WaitBusy();9 \! e3 C( ]3 j' M3 M' W
  22. }3 ~8 p" [8 \0 H- T+ s

  23. ! b  A" j3 ]! f: f
复制代码

2 l+ e/ q; b7 V6 \8 A4 {在执行写入数据的时候函数的参数有三部分:( z3 A! w9 k- h& k; W- H" q
1.要写入的地址  p% V6 l4 }+ {( O/ C  n, w! U! @8 d! S
2.要写入数据的首地址
; y1 u" z' w) h; C" j3.要写入数据的大小* m6 i9 a% B; _, Q( q6 a( g
函数在执行的过程中,首先发送一个写使能信号,然后发送写数据指令,紧接着发送数据要写入的地址,然后就是逐位发送数据了,函数最后等FLASH处理完这次操作再退出。+ C' }: t" [: A4 |
# A/ C) I8 i& b8 k; G+ c
2 U# e. V2 [, M7 u
读取数据# ^- M2 N$ }$ D6 `( n  @9 {* @0 e
  1. /**********************读取指定地址、指定长度的数据******************/
    * k6 o; o2 ^- \
  2. /* 因为读取在了指针中,所以不需要返回值 */
    3 ]9 S  v- _  K7 C1 ?, c" ]0 D
  3. void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size)
    7 u( b- k+ M3 P. _! u
  4. {. K. v5 U* r  A1 j
  5.         SPI_NSS_Begin();$ J, Q; e  O! \; [8 `4 V" u
  6.         /* 发送读取命令 */
    8 l6 X) X+ S4 ?! m
  7.         SPI_SendData(W25X_ReadData);
    9 @' q) \& n; R  h
  8.         /* 发送读取数据的地址,高位先行 */
    $ A2 B  q; ^0 K6 m& D5 p7 B, S
  9.         SPI_SendData((addr & 0xff0000) >> 16);* @7 L' z9 H3 {6 w, ^* q
  10.         SPI_SendData((addr & 0xff00) >> 8);
    3 J6 o7 L5 t, c0 y# e( }+ G
  11.         SPI_SendData(addr & 0xff);
    " p" l. e, K+ Y! z
  12.         /* 逐位读取数据到指针上 */7 ?$ z1 c$ Z6 H4 e8 M
  13.         while(size--)( j7 ?2 n' K* M7 k  w" R* Y
  14.         {% n8 y4 A- F  {4 a6 v
  15.                 *pBuffer=SPI_SendData(Dummy_Byte);# W3 }: q0 O% k  b5 T
  16.                 pBuffer++;  ]' @! R+ v3 }( a* k8 D5 b  ^
  17.         }
    , ?2 L6 N; j+ j# ~. ?& x
  18.         SPI_NSS_Stop();
    0 t" a6 ], r# J& s% g
  19. }
    2 X8 L9 N2 w+ h  F: I1 Z

  20. $ ?0 q' K7 l* }( @
复制代码

' I; j/ l9 i) P# f7 w" O# m7 a' J在执行读出数据的时候函数的参数也有三部分:6 _. x# ^# G; O
1.要读出的地址
. f9 O( C* R: q2.读出到指定地址
! Z. {/ J9 r7 }8 t* n& m3.读出数据的大小7 {; I0 y8 l3 {: ?& L/ G3 D/ H
函数执行过程,首先发送读取指令(这时就不用发送写使能了),然后读取数据的地址,然后将数据逐位读取在固定地址中(地址最好是全局变量),使用时再从全局变量地址中获取数据。, ?' y  ]8 H4 O" j+ g
这里涉及到函数的返回值问题,具体分析链接:返回多个变量怎么办5 I; F- s0 E/ `

4 [: j0 |$ Q, p0 A1 \2 v# u1 V! s6 R7 ~% i3 k
有一个问题当时困扰了博主一天,那就是发送和读取数据时,怎么把数据返回到主函数中,解决方法是,创建俩个全局变量数组,一个负责发送数据、另一个负责接收数据,这样就ok了/ G* j) L: n0 y. G
附上主函数: M! W% L: T9 X+ ?% k
  1. /**********************读取指定地址、指定长度的数据******************/
    / ?6 c4 K6 \8 ~8 F2 k. F
  2. /* 因为读取在了指针中,所以不需要返回值 */- ], E, y! ^% X1 Y1 ~; Z+ I0 P
  3. void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size)
    % @! }, r- ~8 V- k& d3 b" @; h! o
  4. {
    * I! j) N/ t  [% c) j: p& X
  5.         SPI_NSS_Begin();
    / j8 A7 V: o1 `1 D
  6.         /* 发送读取命令 */
    $ [: U9 k" N9 r: F6 N
  7.         SPI_SendData(W25X_ReadData);) D) r- B" h& r: ?# k! A
  8.         /* 发送读取数据的地址,高位先行 */# b8 ^7 w* y* n+ [: G2 [
  9.         SPI_SendData((addr & 0xff0000) >> 16);  _* {% J5 y0 \9 c
  10.         SPI_SendData((addr & 0xff00) >> 8);8 B$ R% n# L! A# A9 J% Q
  11.         SPI_SendData(addr & 0xff);
      n4 i; W+ K, \6 X
  12.         /* 逐位读取数据到指针上 */
    ; S1 e/ I8 I) J% E) T+ K' o2 I8 @
  13.         while(size--)+ b- K3 L, t% j" [6 X
  14.         {
    9 X3 T5 S2 A) ^% m  \8 Y
  15.                 *pBuffer=SPI_SendData(Dummy_Byte);; S, K: |) ^" B; X3 W& E' }/ x; ^
  16.                 pBuffer++;
    4 m* {* F% n; Z. j4 i7 K! b
  17.         }& L# }2 e2 r! r
  18.         SPI_NSS_Stop();; s% `) c% V! d
  19. }
    7 G0 ?& ~; C5 {3 {8 R) s' G6 t9 T

  20. 6 d" [3 W# {2 m& e
复制代码

/ o3 j' p" ~, o' L; Q! f————————————————3 h3 X; T. I7 B$ u3 u
版权声明:Aspirant-GQ
) u' }+ d$ l5 ^7 a- n$ y
" g$ k% H( H/ C2 A8 H% n/ d如有侵权请联系删除
% _! x& t  R1 J' O  k( |4 b5 H% d+ K, ?" ]

% x7 }: A- d/ _: A2 a$ Z1 T2 y
: b4 z$ C; m4 X' G. Y
收藏 评论0 发布时间:2023-3-18 15:00

举报

0个回答
关于意法半导体
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
招聘信息
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
关注我们
st-img 微信公众号
st-img 手机版