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

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

[复制链接]
攻城狮Melo 发布时间:2023-3-18 15:00
FLASH简介. [2 s: I# `; Q3 P/ z) q
FLASH俗称闪存,和EEPROM一样,都是掉电数据不丢失的非易失行存储器,但FLASH的存储容量普遍大于EEPROM,现在像如U盘、SD卡、SSD固态硬盘以及STM32芯片内部存储程序的设备都是FLASH类型的存储器。由此可见FLASH对于我们学习和工作的重要性,EEPROM可以实现单字节的擦写,而FLASH都是一大片的擦写,就像是大规模杀伤性武器,其最小擦除单:扇区的大小也是4KB。+ _( Z) a& @- {8 m+ \8 @
我们此次通过SPI对FLASH存储芯片W25Q64进行读写擦除的操作。* j! p9 |" B; V0 C6 M3 @7 `
; M7 L/ B2 R, f6 ]/ p* l7 M+ |: W
对于FLASH内部结构的详细说明博主会专门整理一篇博客来说明,所以关于FLASH芯片的相关原理,本文中只做简单说明,侧重代码部分。) ]; w6 M' {" a) s- P
FLASH详细说明的博客链接:(没有链接就说明还没有整理出)
8 q! [. ^2 h5 k) W: `  W3 y- t# X7 e) c1 b: u& S8 ]
W25Q64
' C8 I1 m1 s- |! v0 EW25Q64简介

' B' D2 Y7 p9 Z8 m0 R& r- f) r' _$ U就长这么个样子1 V6 g* X1 v7 c" L

2 a* D# t' s& s( |# \6 p
20190814195953540.png * K7 Y! N% s$ [2 Y5 R
: n8 A, F$ C! o) Y( m9 j3 f
STM32内部原理图如下:
0 ?6 H9 h  P! n9 q
, x2 J) ~% d& u1 V) u* @; \5 S
20190814200413370.png
4 k1 U. b6 B8 @( r0 b5 S- F
# @3 S: {8 S& s( Y  H, h

! T8 e5 w) ?4 v1 _W25Q64是一种使用SPI通信协议的NOR FLASH存储器,它 的CS/CLK/DIO/DO 引 脚 分 别 连 接 到 了 STM32 对 应 的 SPI 引 脚NSS/SCK/MOSI/MISO 上,其中 STM32 的 NSS 引脚是一个普通的 GPIO,不是 SPI 的专用NSS 引脚,所以程序中我们要使用软件控制的方式。FLASH 芯片中还有 WP 和 HOLD 引脚。 WP 引脚可控制写保护功能,当该引脚为低电平时,禁止写入数据。我们直接接电源,不使用写保护功能。 HOLD 引脚可用于暂停通讯,该引脚为低电平时,通讯暂停,数据输出引脚输出高阻抗状态,时钟和数据输入引脚无效。我们直接接电源,不使用通讯暂停功能。4 n0 X7 N) ^% {. I
" z( ]/ T9 C+ g4 i# g
W25Q64支持SPI通讯的模式0和模式3& j* Q9 T1 t6 R6 _: D) s2 d% C

' L+ H/ p/ c1 n4 _. K* j
* o, V6 P2 T4 S! S& X
FLASH控制指令
3 O, t% m2 R  m9 a1 B$ }$ MFLASH芯片中规定了许多指令,只要SPI向FLASH发送相应的指令,FLASH就会执行相应的操作,所以我们对FLASH的一切操作都是基于这个指令集的,接下来介绍一下FLASH的控制指令:
$ a# e% m( F% v2 i7 C, a
5 q8 |& z# e# ^9 M, t1 V# R
20190814200912265.png 7 _; c7 L  M! B8 h* x! [, Q

6 A  a1 S1 q( t表中第一列为指令名,第二列为相应的指令代码,第三列及后面的内容根据指令的不同而意义不同,其中带括号的字节参数,方向为 FLASH 向主机传输,即命令响应,不带括号的则为主机向 FLASH 传输。表中“A0~A23” 指 FLASH 芯片内部存储器组织的地址; “M0~M7” 为厂商号( MANUFACTURER ID); “ID0-ID15”为 FLASH 芯片的ID;“dummy”指该处可为任意数据;“D0~D7” 为 FLASH 内部存储矩阵的内容。4 e, g- T& H, C* H, ~
看起来很复杂的样子,其实只要在需要执行相应操作时来查这个表,只要能够理解这些指令的使用方法,FLASH就算学会了。, F- e$ h8 M0 \: O- z7 H

" e; Q4 }; R* u, q0 S: R4 L
例如:要知道FLASH的ID,那就在指令中找对应的取ID指令“JEDEC ID”,仔细解读这个指令9 z. n3 T! h+ G2 f+ x
20190814201942797.png
# f- e+ t- ^& d$ O- P1 ~
3 H1 n, _) E8 w( G, A# m
可以看出对应的指令代码为“9F”,后面的三个字节带括号,代表这三个字节就是FLASH向STM32发送的数据,即这三个字节就是FLASH的ID,然后使用SPI进行读取就可以了。
: z3 H6 P; T$ a% y( n" M% K
* A3 y* D& ~8 B! S
我们一般是将这些指令宏定义在头文件中,便于使用:
2 l7 l# x* f0 [1 U
  1. #define W25X_WriteEnable                      0x06 # Z& W: O  V) T# h% v. V! k3 s( M
  2. #define W25X_WriteDisable                      0x04 : ]. J% T1 a9 p4 S9 P; Z
  3. #define W25X_ReadStatusReg                      0x05 7 F, j" t/ u0 ^* O  l) T
  4. #define W25X_WriteStatusReg                      0x01
    % {- G8 N* |' o, l9 i8 N$ o
  5. #define W25X_ReadData                              0x03 ; Q% P2 u" [5 y6 b
  6. #define W25X_FastReadData                      0x0B
    ' G" K5 Z  K$ \
  7. #define W25X_FastReadDual                      0x3B
    + s! `, C: d1 R2 Q! q. U' Y
  8. #define W25X_PageProgram                      0x02 7 v$ s6 h3 a1 s3 f2 z  ^% \) a& G& l
  9. #define W25X_BlockErase                              0xD8
    ( Z! L# W. A  ^( R8 N/ V+ ?
  10. #define W25X_SectorErase                      0x20 0 ^5 d# n( N# ^: {4 U
  11. #define W25X_ChipErase                              0xC7
    / T- \7 B+ m. G$ u1 _6 c( A3 k
  12. #define W25X_PowerDown                              0xB9 " M' [5 d- ?" {6 W$ _0 d- e; e
  13. #define W25X_ReleasePowerDown              0xAB
    ( t2 \/ j. a1 |8 Z+ O
  14. #define W25X_DeviceID                              0xAB
    ) }; z! d4 O& h5 U; U
  15. #define W25X_ManufactDeviceID         0x90
    . I/ }; o1 A9 I7 F& ^/ }
  16. #define W25X_JedecDeviceID                      0x9F6 k9 m) R& k  l0 W/ r

  17. 6 \2 a# |% C+ g( H
复制代码
4 e0 G8 O% |: J- [( |' Z8 X
FLASH内部存储结构
2 P, f# G+ [- T0 FFLASH的存储矩阵如图:其内存分为128块,每一块都有16个扇区,每个扇区大小为4KB,擦除数据的时候是以扇区为基本单位的。
9 k3 n7 |& j6 `/ l+ z8 e# m. X% \8 X, C4 K
20190814203311343.png
0 H$ q% k# B+ |4 R6 G1 ?" ^7 e/ v3 F  m$ K# d
代码讲解" ~% ^, ^# N! h8 [6 V
代码都是博主亲手写出来的,可以运行。
0 d* u9 V# x/ o% H代码部分会用到SPI的代码,关于SPI的说明之前整理过:SPI详解
3 p/ j6 ~. D9 N% e% \3 w
1 k/ N2 t$ Q# d) i
读取芯片ID
9 `- z1 D3 L( _' _- f/ H: y' j6 l
  1. /************************读取芯片ID*************************************/6 L/ A, ]+ {+ y* \. x* Z$ f  `: r; P
  2. uint32_t FLASH_ReadID(void)
    0 z: w/ Y6 H- D7 m
  3. {, X" R( h, k# m! P; L. }
  4.         uint32_t temp,temp1,temp2,temp3;
    3 e0 @2 a# L4 o& ^) G, w- j+ y
  5.         SPI_NSS_Begin();
    8 f4 t  {+ U! p6 H" b7 |( P
  6.         /* 发送取ID指令 */
    # g: k7 q# r' p6 J0 }1 C, b
  7.         SPI_SendData( W25X_JedecDeviceID);
    * m' ?7 V8 ]; Q4 u$ A# A
  8.         /* FLASH会连续发送三个字节数据 */% K1 i; ^5 h5 e) O: @- B, F: a8 t( q
  9.         temp1=SPI_SendData(Dummy_Byte);: s$ ~6 o+ Y$ q! b* j
  10.         temp2=SPI_SendData(Dummy_Byte);' G8 |! F, ^. u
  11.         temp3=SPI_SendData(Dummy_Byte);
    7 N$ M* e$ _3 s+ L2 g
  12.         SPI_NSS_Stop();8 e( H0 [# W) A% Z
  13.         /* 高为先行,将三个字节整理在一起 */
      F3 j/ w: H0 M2 q# y& z- a/ h
  14.         temp=temp1<<16 | temp2<<8 | temp3;; t+ _; W) `* d/ W
  15.         return temp;
    % i. S( X3 Z9 `5 C0 Z! L" Z
  16. }
    . |: c8 j( ]& ^+ b
复制代码
0 T7 ^% p9 c% u
由于SPI是全双工通信,所以接收数据和发送数据用的是同一个函数
7 Y8 D! ?2 s* r

  Q5 N$ r4 [: T发送写使能信号
3 K, ?4 v/ b+ j: a; ?+ v  |! _! I6 M
  1. /****************发送写使能信号****************************/' O% R; }8 b3 v8 w5 N
  2. void FLASH_WriteEnable(void)6 [( z) G, ?6 T5 M
  3. {
    ; B! z+ \/ p! r' N$ D" u
  4.         SPI_NSS_Begin();. W; a# \- x/ w2 y
  5.         /* 发送相应的指令代码 */0 G7 x; O+ {- h7 T8 i
  6.         SPI_SendData(W25X_WriteEnable);3 l; r7 Z) Q$ U9 ~" Y1 @# G
  7.         SPI_NSS_Stop();
    * I6 N/ Y% ~( ^0 @" k+ W
  8. }3 Q' I" \7 y; D& [$ u" `" D
复制代码
9 F% T% J/ W# h% I$ T$ V$ K! s
在向FLASH中执行写操作之前,都要进行写使能操作,通过发送写使能指令到FLASH来实现7 S+ f# d  h4 A/ w' z8 w: z2 ^

/ q( k! }; i% I等待FLASH不忙
# @/ y5 @, I  t; _% H/ u. N
  1. /******************等待,直到不忙***********************************/
    * @) u. E* i. P0 R: Q( }. _
  2. void FLASH_WaitBusy(void)
    8 |: E+ P( A  {; T- [7 u3 I8 Z* y/ e
  3. {1 `+ G# F% H% G# j( H
  4.         uint8_t StatusReg=0x01;- W" m: n% z. h. h; q
  5.         SPI_NSS_Begin();
    9 R) m& ~  t% H, N
  6.    /* 读取状态寄存器中的数据,判断忙标志0x01位 置位代表忙 */7 H, ?! R9 G! _3 \) Q6 p2 ^
  7.         SPI_SendData(W25X_ReadStatusReg);      / @  A6 ^: y3 f% H# F/ D
  8.    /* 只读取状态寄存器的BUSY位,即第一位 */
    * d* U) s5 p5 D2 m- ^8 B4 M& N8 b
  9.         while((StatusReg & 0x01) == 1) 2 R$ V) {2 y% _, W6 J
  10.                 StatusReg=SPI_SendData(Dummy_Byte); ) f- L" V4 ]) d2 G
  11.         SPI_NSS_Stop();
    ' Q- x/ G1 B+ p+ U* a, R" _  ?- D
  12. }
    ( z5 u6 Y# x  _. r, {) {
  13. ) [* \/ P/ z: ]* \

  14. 5 g% O! K1 e& }9 b4 k3 X
复制代码
4 r0 E* q9 C% u/ L4 _# I) j
FLASH在通讯的过程中需要一定的时间来执行操作,在这期间,传输数据是无效的,因为FLASH忙着呢,所以我们就要有一个函数来专门等!等到FLASH不忙了,再进行通讯,那怎么等呢?FLASH不忙了会给出一个信号——将状态寄存器的BUSY位重置(也就是0),所以我们需要不断的来检测状态寄存器中的BUSY位是否置位,利用读取寄存器状态的指令来获取状态寄存器当下的状态,然后根据寄存器的BUSY位(第1位)来判断FLASH是否处于忙碌状态。
( z1 I0 k! O0 P* A7 E  z0 i简单来说,这就是个延时函数,延时直到FLASH空闲,可以进行下一步传输。
( j& [$ ?/ w5 K4 [9 Z0 v$ Q" E& K
1 Q/ Q# u* O" c/ X, @( B' v
. F. ?% j% ]1 O+ j6 l
擦除扇区
/ V( b6 k) R7 G
  1. /******擦除扇区的内容,切记地址要对其到4kB,每个扇区的大小都是4KB********// \" ?* q6 H' c% A9 I1 G. q
  2. void FLASH_SectorErase(uint32_t addr), w: l# r9 S5 M& Z7 E! p" L% C
  3. {
    - K: C: S: l6 J' G2 a1 q
  4.         /* 开始的时候要发送写使能信号*/
    3 C! E% Y9 w  y  h3 I8 N
  5.         FLASH_WriteEnable();4 E8 M, g/ s' R0 c
  6.         SPI_NSS_Begin();4 c7 G/ h# k6 T; o! [
  7.         /* 发送扇区擦除命令 */: W2 {  {/ ^8 v$ ]1 z" c
  8.         SPI_SendData(W25X_SectorErase);
    3 [1 B% ]3 U4 K9 u& q! Z9 _: k
  9.         /* 发送扇区的地址,高位先行 */# F( S# ?9 M7 @/ ?# g
  10.         SPI_SendData((addr & 0xff0000) >> 16);
    " V+ D0 @8 N, Z! u
  11.         SPI_SendData((addr & 0xff00) >> 8);- l9 o2 m8 ?# s5 z1 {- K8 K
  12.         SPI_SendData(addr & 0xff);
    # X- q( f7 I0 K# y* F0 w; `
  13.         SPI_NSS_Stop();7 @( e: K0 f. f7 e* @# L
  14.         /* 最后也要等待FLASH处理完这次的信号再退出 */5 z7 g3 s4 S% s( I2 h9 G, d8 h
  15.         FLASH_WaitBusy();
    3 m3 O( W) E/ L  [, d; J
  16. }
    + F( R; w& r* j6 J1 i
  17. % |! E/ S3 d8 h+ p
复制代码

# C, V$ L1 y- H扇区的擦除之前要发送一个写使能信号,先发送擦除指令,然后发送要擦除扇区的地址(分三个字节发出去),高位先行。9 v$ N2 l/ b7 q7 H
扇区上的内容不是1就是0,擦除的过程就是写1的过程(将一个扇区全部写1),因为在写入数据的时候,可以将1写为0,但不能将0写为1.) b% ]4 n2 t8 u% r- y

" }1 p0 y2 i, }3 u. W5 G& ~/ V$ r
) M2 t! b9 Q. s$ w' u8 R
写入数据
6 t+ E1 V! Q5 Z6 D# A
  1. /************按页写入数据,但写入之前要进行擦除***********/. `, t8 p7 T' R7 n1 }
  2. void FLASH_PageWrite(uint32_t addr , uint8_t* pBuffer ,uint8_t size)+ m& Z$ y* K' ^+ F5 Y3 a
  3. {
    : f% S6 O& K* m/ B( _9 u4 z! l1 F
  4.         /* 开始的时候要发送写使能信号 */
    6 Q8 R7 i9 j' j5 c6 f; L) C: h- a
  5.         FLASH_WriteEnable();
    ' Y( \+ h! P" ]5 x8 q
  6.         SPI_NSS_Begin();
    $ Y0 v: {# P  h" z8 l
  7.         /* 发送页写入命令 */
    2 c# Q6 n" c5 m- [: X
  8.         SPI_SendData(W25X_PageProgram);1 Y) r; }" y: r* J
  9.         /* 发送写入的地址,高位先行 */6 F1 T) h5 R; Z* k& E, V4 B
  10.         SPI_SendData((addr & 0xff0000) >> 16);
    - A( W: ]% d- c2 t1 N( t2 m
  11.         SPI_SendData((addr & 0xff00) >> 8);9 F8 B  s+ T# m& A" \
  12.         SPI_SendData(addr & 0xff);
    , |5 n; b6 ?6 H! u! k
  13.         /* 逐位发送数据 */3 a% r0 }( M1 S% q2 e  }1 O6 F
  14.         while(size--)
    5 l' J9 \4 t1 ?* _. I% q$ h
  15.         {" c% i% a; C* Y; e2 v7 w" D: f
  16.                 SPI_SendData(*pBuffer);8 M1 s0 D% p1 r& {/ i+ ~& B
  17.                 pBuffer++;' B4 r0 P- h+ g% r4 X  I. v
  18.         }* k% ?7 \  H9 u( F
  19.         SPI_NSS_Stop();
    : p5 W( Y% K  [0 w3 z7 A
  20.         /* 最后也要等待FLASH处理完这次的信号再退出 */
    & `' p7 [  G# t# h+ \7 N
  21.         FLASH_WaitBusy();1 x% a8 i3 S. ]
  22. }
    * U  {3 [+ B5 H4 `# @9 l
  23. , g5 g) ~/ c! Z* B6 k; j0 \
复制代码
6 b* ~$ H8 w' m- A- T
在执行写入数据的时候函数的参数有三部分:
7 y" K2 ^6 \& E6 M1.要写入的地址
: t1 v2 r7 r8 O2.要写入数据的首地址
# [/ [! k3 t8 N3.要写入数据的大小! n# U0 F6 x8 c! E% O. K: C
函数在执行的过程中,首先发送一个写使能信号,然后发送写数据指令,紧接着发送数据要写入的地址,然后就是逐位发送数据了,函数最后等FLASH处理完这次操作再退出。, `3 j: f( \; s* N  E% R) P

& r# P# r) X+ y, s
1 K6 g7 `! \# d+ {; X8 G- f/ w
读取数据
/ l% o5 E+ }7 w' |: t6 A! \
  1. /**********************读取指定地址、指定长度的数据******************/
    . K. S. I' D) S/ S5 Y/ D$ E0 w
  2. /* 因为读取在了指针中,所以不需要返回值 */
    8 A* H( O% e  D: ?. D' ]
  3. void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size)# }% B) b1 q3 H
  4. {
    8 R7 M4 T* X, L* A! ~
  5.         SPI_NSS_Begin();8 m$ g4 p8 |/ c4 m
  6.         /* 发送读取命令 */0 ?9 D$ d8 F, V1 A+ B* _. z
  7.         SPI_SendData(W25X_ReadData);7 h0 J: r# `6 S' K  M' W
  8.         /* 发送读取数据的地址,高位先行 */8 N+ N+ f- g# o4 k
  9.         SPI_SendData((addr & 0xff0000) >> 16);
    8 E# v/ u# W' ~8 C
  10.         SPI_SendData((addr & 0xff00) >> 8);/ H* A: v2 ?5 _; O
  11.         SPI_SendData(addr & 0xff);5 |1 Q% ~5 @$ t! z7 G
  12.         /* 逐位读取数据到指针上 */
    + G/ i' P/ }" ?' }3 v4 s
  13.         while(size--): d+ Z: o. `; I$ ^, i
  14.         {# \" D9 t$ Z* w' T. \
  15.                 *pBuffer=SPI_SendData(Dummy_Byte);8 Q9 J, |3 c( O( s6 D
  16.                 pBuffer++;
    2 X1 B$ }1 _4 k8 c+ N8 K" P& a
  17.         }4 P1 @0 v, w1 c5 b
  18.         SPI_NSS_Stop();
    ) M; h2 D) z5 X' q1 c! D
  19. }
    5 X( B7 ?. G" p9 R9 t
  20. 7 s( y' b' e. L1 L# H/ D
复制代码
, o( a5 @& }) N
在执行读出数据的时候函数的参数也有三部分:
" t4 \: c" Y8 F1.要读出的地址4 y, b0 R; U1 E
2.读出到指定地址
) g( g4 f+ v. |+ w3.读出数据的大小% u* V! r* Q  k; f" A
函数执行过程,首先发送读取指令(这时就不用发送写使能了),然后读取数据的地址,然后将数据逐位读取在固定地址中(地址最好是全局变量),使用时再从全局变量地址中获取数据。
0 O; C4 l; y. {- ]! b. I  V; R/ |这里涉及到函数的返回值问题,具体分析链接:返回多个变量怎么办% [- m& r3 e! f
5 v9 b4 a) ~% g) T
注2 V4 L( w3 e# l, f) w: d# o: g
有一个问题当时困扰了博主一天,那就是发送和读取数据时,怎么把数据返回到主函数中,解决方法是,创建俩个全局变量数组,一个负责发送数据、另一个负责接收数据,这样就ok了( j; E' s4 \- G- ^% i8 Y
附上主函数
8 _6 \3 y' G8 p) _; ~8 O: C5 M
  1. /**********************读取指定地址、指定长度的数据******************/0 v# l7 k+ d: s6 m
  2. /* 因为读取在了指针中,所以不需要返回值 */3 {7 e& x) p) U- d3 g5 K
  3. void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size)
      u- \) j# M5 ]& ^9 }! ]; W4 @
  4. {
    ' J( v. P9 F0 k$ p( D
  5.         SPI_NSS_Begin();
    . m6 N1 Q. f. M0 T' Y* W9 y- {
  6.         /* 发送读取命令 */
    0 g* t0 U: q  B' H: j
  7.         SPI_SendData(W25X_ReadData);
    9 f" \4 ^  ~3 R3 P1 \5 e
  8.         /* 发送读取数据的地址,高位先行 */. n, e7 F# R9 I3 P7 S
  9.         SPI_SendData((addr & 0xff0000) >> 16);
    & p! _2 c+ D. t4 E1 q2 s' @
  10.         SPI_SendData((addr & 0xff00) >> 8);( p1 Q* F% M0 m3 h5 K
  11.         SPI_SendData(addr & 0xff);( @! Z& _; s9 H, w
  12.         /* 逐位读取数据到指针上 */
    " o% R/ _* |! B/ B) X7 O2 h
  13.         while(size--)
    * q% ], W, d2 v
  14.         {7 s$ }, T% E1 H5 `" ?1 e; Z
  15.                 *pBuffer=SPI_SendData(Dummy_Byte);
    ( U# g# u8 h. u! c: t; x
  16.                 pBuffer++;
      \0 [; M( t# l' G" E8 l
  17.         }
    2 v+ h+ J% H! [* b( A
  18.         SPI_NSS_Stop();2 O$ q6 U; {  c& \
  19. }, Z7 w/ ^1 L' t# v9 ^

  20. - Z* t8 m% i& N
复制代码
% B  g8 j" \& B  d9 f9 L2 Q1 y, l
————————————————$ S. k% U3 J: V& D- Q0 w( K
版权声明:Aspirant-GQ
. |( H: W3 O% V& b
/ M+ ?" L' z; h: X# f如有侵权请联系删除
: f# d( R) e/ E" j6 t7 C% W0 I7 e0 O1 S
' z' o) X: W8 W: V3 |
5 S+ x% j) b/ f
赞 收藏 评论0 发布时间:2023-3-18 15:00

举报

0个回答

所属标签

相似技术帖

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