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

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

[复制链接]
攻城狮Melo 发布时间:2023-3-18 15:00
FLASH简介
/ z! u, R% z! BFLASH俗称闪存,和EEPROM一样,都是掉电数据不丢失的非易失行存储器,但FLASH的存储容量普遍大于EEPROM,现在像如U盘、SD卡、SSD固态硬盘以及STM32芯片内部存储程序的设备都是FLASH类型的存储器。由此可见FLASH对于我们学习和工作的重要性,EEPROM可以实现单字节的擦写,而FLASH都是一大片的擦写,就像是大规模杀伤性武器,其最小擦除单:扇区的大小也是4KB。
. h% G. j1 n0 u4 L3 t我们此次通过SPI对FLASH存储芯片W25Q64进行读写擦除的操作。
6 |6 K) {/ a# e4 Y8 x
2 F, I/ N& ?0 a# V/ G3 Q对于FLASH内部结构的详细说明博主会专门整理一篇博客来说明,所以关于FLASH芯片的相关原理,本文中只做简单说明,侧重代码部分。
3 a0 t' C( n% a- t8 N, cFLASH详细说明的博客链接:(没有链接就说明还没有整理出)
; E5 k6 ?" s" F9 y
5 j1 x& T' [; _3 D+ p0 n9 H; ?W25Q64
2 U. R6 L8 n9 ~. V6 A8 L$ V$ ]W25Q64简介

1 @; M2 i- d! g# h就长这么个样子' Q$ V. ~3 I  l* G# n( i' u2 d

) I  g" p' ^: B) d  q4 y
20190814195953540.png % e% w$ s' z1 ~1 |1 ?7 Z! q) J
, R, Y: D/ P- K* W" i- g% {: K
STM32内部原理图如下:2 [3 ?5 _, f0 T/ @* _

8 s9 `( k/ r! d: `
20190814200413370.png / m/ K# D. h# g! v3 [- h. F5 B$ |

+ h  O0 a0 G1 R# ]- _9 v
7 a$ s; d5 \& ~8 s, Q! p
W25Q64是一种使用SPI通信协议的NOR FLASH存储器,它 的CS/CLK/DIO/DO 引 脚 分 别 连 接 到 了 STM32 对 应 的 SPI 引 脚NSS/SCK/MOSI/MISO 上,其中 STM32 的 NSS 引脚是一个普通的 GPIO,不是 SPI 的专用NSS 引脚,所以程序中我们要使用软件控制的方式。FLASH 芯片中还有 WP 和 HOLD 引脚。 WP 引脚可控制写保护功能,当该引脚为低电平时,禁止写入数据。我们直接接电源,不使用写保护功能。 HOLD 引脚可用于暂停通讯,该引脚为低电平时,通讯暂停,数据输出引脚输出高阻抗状态,时钟和数据输入引脚无效。我们直接接电源,不使用通讯暂停功能。) Z' O! N- ~# O

$ |( U+ P; B* m7 Q, L
W25Q64支持SPI通讯的模式0和模式3
: o, l4 f# m1 _; {7 j& G% \4 G  s3 s! g* K

! N1 L" B. U# c4 j6 U3 sFLASH控制指令" |' G$ z& @6 N& Y6 j; u) {- h+ h! [
FLASH芯片中规定了许多指令,只要SPI向FLASH发送相应的指令,FLASH就会执行相应的操作,所以我们对FLASH的一切操作都是基于这个指令集的,接下来介绍一下FLASH的控制指令:$ G0 ~' W' M. F6 D2 v
! K- _5 J: h) t  l, H7 B) u4 g1 _
20190814200912265.png
+ U2 Q$ `, E+ m( @* l) d' \

9 j) `$ A0 g3 E) b9 N表中第一列为指令名,第二列为相应的指令代码,第三列及后面的内容根据指令的不同而意义不同,其中带括号的字节参数,方向为 FLASH 向主机传输,即命令响应,不带括号的则为主机向 FLASH 传输。表中“A0~A23” 指 FLASH 芯片内部存储器组织的地址; “M0~M7” 为厂商号( MANUFACTURER ID); “ID0-ID15”为 FLASH 芯片的ID;“dummy”指该处可为任意数据;“D0~D7” 为 FLASH 内部存储矩阵的内容。  i& j5 a; J0 a0 r2 V2 M) d
看起来很复杂的样子,其实只要在需要执行相应操作时来查这个表,只要能够理解这些指令的使用方法,FLASH就算学会了。1 l3 g; j7 o# h$ Z

1 G5 V! W  s( B9 G% X: a( W
例如:要知道FLASH的ID,那就在指令中找对应的取ID指令“JEDEC ID”,仔细解读这个指令
7 O' L! {. N8 S+ d* n
20190814201942797.png $ @1 b  v7 `% \/ T
7 Z" B, c* O, m8 N. i* C
可以看出对应的指令代码为“9F”,后面的三个字节带括号,代表这三个字节就是FLASH向STM32发送的数据,即这三个字节就是FLASH的ID,然后使用SPI进行读取就可以了。3 X8 F9 s9 r6 x7 @- T' M8 f
) }) |3 j) }8 M( i' H6 W* i4 U
我们一般是将这些指令宏定义在头文件中,便于使用:& i/ H5 K. e" @/ N+ t% \
  1. #define W25X_WriteEnable                      0x06
    8 I* Y5 u" k, f
  2. #define W25X_WriteDisable                      0x04 9 S7 i6 w: t# D. r9 Z! k% E9 r
  3. #define W25X_ReadStatusReg                      0x05
    % }- r! C  H% C
  4. #define W25X_WriteStatusReg                      0x01 : l4 A9 K; Q: _% y
  5. #define W25X_ReadData                              0x03 8 L. x% A; L) W( @# W; m
  6. #define W25X_FastReadData                      0x0B
    " c9 Y- ~0 y+ Y" {. @3 V
  7. #define W25X_FastReadDual                      0x3B * N5 A# l& {8 \! H0 t4 k
  8. #define W25X_PageProgram                      0x02
    : y4 u9 N+ z+ R2 F2 u8 K
  9. #define W25X_BlockErase                              0xD8
    * H; @: U- r, z7 }' ?
  10. #define W25X_SectorErase                      0x20 % @/ D$ ~8 S8 d9 _1 k
  11. #define W25X_ChipErase                              0xC7 . R. ]; q5 C3 I( g
  12. #define W25X_PowerDown                              0xB9
    . y4 [* _$ H8 z5 p$ O" a7 B: k
  13. #define W25X_ReleasePowerDown              0xAB
    1 C1 |! j* B/ i4 ]) R
  14. #define W25X_DeviceID                              0xAB
      D6 Z# c  H; A. t2 Y6 x
  15. #define W25X_ManufactDeviceID         0x90
    5 a4 j4 e1 ]6 X/ Y
  16. #define W25X_JedecDeviceID                      0x9F$ N1 Q$ t* Y; c- C) v* H
  17. # b, Y2 K% z: b& j
复制代码
! e0 [" w% q7 ]2 Z0 l& R; p
FLASH内部存储结构
; v' \. @7 q2 AFLASH的存储矩阵如图:其内存分为128块,每一块都有16个扇区,每个扇区大小为4KB,擦除数据的时候是以扇区为基本单位的。2 v4 }# v) z+ ]# g

: ?5 s3 m1 D* E5 ^* D
20190814203311343.png
5 q- s- l- P$ h! Q: O  O/ c+ }
! o& R7 \! {, C6 m5 ~' x9 F代码讲解
+ N; Q3 {( O) |$ v代码都是博主亲手写出来的,可以运行。0 d2 N- s0 Q* Z, H/ v1 U$ g
代码部分会用到SPI的代码,关于SPI的说明之前整理过:SPI详解3 t* k/ k- b; n* O' I9 m

0 x8 o; @+ w, U3 k8 \5 y( [. ~) i读取芯片ID
! P8 P) M  {! a1 h& L0 |% M
  1. /************************读取芯片ID*************************************/3 z: `: k: y% q. p0 ]: B8 y8 n
  2. uint32_t FLASH_ReadID(void)
    , I- c' H  `# c8 I& u6 m
  3. {
    2 F, l: L6 K" X7 T
  4.         uint32_t temp,temp1,temp2,temp3;
    " @/ w) A  U2 ~
  5.         SPI_NSS_Begin();& e" z/ V. u  A  U& \
  6.         /* 发送取ID指令 */
    9 Q4 z3 l, P, j
  7.         SPI_SendData( W25X_JedecDeviceID);" E& n# \5 H& e1 Y. J& U$ n' {; R9 t
  8.         /* FLASH会连续发送三个字节数据 */1 @! O9 Q& r7 [- ?* F. ~
  9.         temp1=SPI_SendData(Dummy_Byte);" Z- S# {; c" t/ @
  10.         temp2=SPI_SendData(Dummy_Byte);( j8 Y) U% l; k6 g  N( \/ a
  11.         temp3=SPI_SendData(Dummy_Byte);
    ! d+ ]5 a! @2 D: R; Z3 @
  12.         SPI_NSS_Stop();! G* x+ o  j4 V0 e# F+ _
  13.         /* 高为先行,将三个字节整理在一起 */
    6 U/ P( {9 [: s: Y! ]# b
  14.         temp=temp1<<16 | temp2<<8 | temp3;( E2 W% c% c' ^1 M( E( S. k4 m& r1 B
  15.         return temp;
    % `( i2 w9 r" c
  16. }+ D$ U, K; c+ |7 ^3 w
复制代码

# |" i5 l) `9 o8 d4 P5 M由于SPI是全双工通信,所以接收数据和发送数据用的是同一个函数
4 \7 ]+ Z0 }9 r& ?
' _7 p3 K2 B3 C" z0 e
发送写使能信号
( _* K1 t9 q8 w) Q- r" c# }6 k
  1. /****************发送写使能信号****************************/
    . ~( i5 O, }) {1 i: F( ^: @- X
  2. void FLASH_WriteEnable(void)
    ! T' u4 l, v- p1 d% Z
  3. {
    ) n7 ?9 t* m2 o- t1 V
  4.         SPI_NSS_Begin();+ X4 J, k9 l- O; E7 D; t
  5.         /* 发送相应的指令代码 */
    ! `% r7 _+ J+ }) d; x3 ]; }1 e3 a( X
  6.         SPI_SendData(W25X_WriteEnable);& C5 q( }( y- O2 f) I5 f
  7.         SPI_NSS_Stop();9 [9 i! d' ^* h" D3 E
  8. }
    5 ~: ~5 ?1 x( p' J% e
复制代码

" x+ A: ~( k; G$ h8 C- U在向FLASH中执行写操作之前,都要进行写使能操作,通过发送写使能指令到FLASH来实现) C" e$ R- |3 n# P* H

9 r  |; l1 ?) F5 `等待FLASH不忙
* i+ K# C/ C. s; F+ m  m' A* l
  1. /******************等待,直到不忙***********************************/
    ! U4 {4 l- h2 d! k
  2. void FLASH_WaitBusy(void)" x; ]' w4 m- z2 H( U
  3. {
    / M  F7 [# ]' H( `
  4.         uint8_t StatusReg=0x01;" j% Y; `- |6 B
  5.         SPI_NSS_Begin();% u1 {- ~3 r1 x" Z$ X* X/ U
  6.    /* 读取状态寄存器中的数据,判断忙标志0x01位 置位代表忙 */
    : v& O$ ]' o% E/ B/ \- M2 a* c( G
  7.         SPI_SendData(W25X_ReadStatusReg);      5 Q% W2 D% X( E7 y8 b/ D9 B1 ^
  8.    /* 只读取状态寄存器的BUSY位,即第一位 */
    1 e% {: \* e% j8 ~
  9.         while((StatusReg & 0x01) == 1)
      }" Z* N( Y. h) v
  10.                 StatusReg=SPI_SendData(Dummy_Byte);
    ; ?" Y0 W1 V, v, c" U8 f" N) r  h( [  s
  11.         SPI_NSS_Stop();: R& q( j/ Z4 b- L% f! x
  12. }- m0 E; L( l/ J: w

  13. $ w7 v% X( U* g8 x9 F0 v1 U
  14. ; _! g* K/ y0 h
复制代码

* I7 {3 C$ f) b8 x$ C" rFLASH在通讯的过程中需要一定的时间来执行操作,在这期间,传输数据是无效的,因为FLASH忙着呢,所以我们就要有一个函数来专门等!等到FLASH不忙了,再进行通讯,那怎么等呢?FLASH不忙了会给出一个信号——将状态寄存器的BUSY位重置(也就是0),所以我们需要不断的来检测状态寄存器中的BUSY位是否置位,利用读取寄存器状态的指令来获取状态寄存器当下的状态,然后根据寄存器的BUSY位(第1位)来判断FLASH是否处于忙碌状态。
: t$ s. P/ r, s0 I% |简单来说,这就是个延时函数,延时直到FLASH空闲,可以进行下一步传输。
* Y) R2 z8 S' W4 `: p9 ]! u/ e9 {8 y. q7 t7 a; f
4 V" n' E! p! y$ P  c
擦除扇区
% m( ~4 L. ~% U! t* c9 i/ _, w
  1. /******擦除扇区的内容,切记地址要对其到4kB,每个扇区的大小都是4KB********/
    + o2 [! G4 [" T5 R) h: G
  2. void FLASH_SectorErase(uint32_t addr)
    7 i7 p/ }6 V2 S. q2 ^
  3. {
    4 Z) T; _# _3 F- P
  4.         /* 开始的时候要发送写使能信号*/
    ) O+ J+ _+ Y5 k9 L! [# Y# n
  5.         FLASH_WriteEnable();2 b$ v0 m$ s: u* M
  6.         SPI_NSS_Begin();
    * o: z# Y; P' ]9 I: L, R- T
  7.         /* 发送扇区擦除命令 */
    5 p. J' I9 i; l2 ]9 y
  8.         SPI_SendData(W25X_SectorErase);/ Z3 I4 ~2 W# {* ?
  9.         /* 发送扇区的地址,高位先行 */
    $ v4 k) T8 S  `
  10.         SPI_SendData((addr & 0xff0000) >> 16);
    3 ~7 W0 h  F3 T0 z
  11.         SPI_SendData((addr & 0xff00) >> 8);( D$ \" P% W% H2 ^
  12.         SPI_SendData(addr & 0xff);
    ! L5 @' C( ~* Z3 N* z, }
  13.         SPI_NSS_Stop();
    0 P0 X" y! {. C" N
  14.         /* 最后也要等待FLASH处理完这次的信号再退出 */
    - Q3 `6 E: V. y
  15.         FLASH_WaitBusy();' Y" {) ?' z- |: U, w
  16. }
    , X, O, O0 a6 D# I& D5 F: i3 U
  17. + m, k5 B( l* K: m: O2 O+ z% I9 r
复制代码

. W/ n4 g/ r) D" Q# i0 L扇区的擦除之前要发送一个写使能信号,先发送擦除指令,然后发送要擦除扇区的地址(分三个字节发出去),高位先行。
  g1 _  E% D" i& l$ |扇区上的内容不是1就是0,擦除的过程就是写1的过程(将一个扇区全部写1),因为在写入数据的时候,可以将1写为0,但不能将0写为1.' ~/ s) m5 a: |, |8 @$ Z7 H

, w0 i/ F9 a- u& x7 Z9 Z
+ U+ B& H4 |6 L$ p& l& {, v
写入数据% _# O" q% v5 B( z; S8 J
  1. /************按页写入数据,但写入之前要进行擦除***********/
    ; t8 Z8 ?$ B, E- |$ c" i
  2. void FLASH_PageWrite(uint32_t addr , uint8_t* pBuffer ,uint8_t size)
    : `, K7 n- [8 t/ T8 i; [/ E
  3. {
    : E6 ~7 L0 V  X0 h$ J( `: r
  4.         /* 开始的时候要发送写使能信号 */- {4 A# p( j* D, n, i
  5.         FLASH_WriteEnable();
    9 H8 c, W  Q2 g4 Q4 ]% _
  6.         SPI_NSS_Begin();
    / G% W3 R7 v' ^! a
  7.         /* 发送页写入命令 */
    3 j9 q- ~: l5 u( x  i$ j0 k8 X& G
  8.         SPI_SendData(W25X_PageProgram);
    8 }$ ]! t4 P! n' b6 F8 T
  9.         /* 发送写入的地址,高位先行 */
    " R3 h) y: H7 |, L
  10.         SPI_SendData((addr & 0xff0000) >> 16);
    & o$ _4 l: K+ \' g/ `
  11.         SPI_SendData((addr & 0xff00) >> 8);
    ! \& L5 K/ A9 m0 }3 o
  12.         SPI_SendData(addr & 0xff);' h5 m( ~" o& z
  13.         /* 逐位发送数据 */
    : P4 O  ^3 e7 j2 \# l: C# f# W8 F
  14.         while(size--)
    ! W2 F$ h- I4 S# I& v' L! K
  15.         {
    * f5 y+ F, J. P, `
  16.                 SPI_SendData(*pBuffer);
      ?: w0 K' b- I
  17.                 pBuffer++;6 @- N( c  R; a) ^
  18.         }
    ( r' ^  c( O5 H
  19.         SPI_NSS_Stop();
    % c! L3 B  @+ K& o
  20.         /* 最后也要等待FLASH处理完这次的信号再退出 */8 v* |. Z7 U0 ?. j; g3 T" q: z3 V
  21.         FLASH_WaitBusy();8 s0 h( p/ t5 D
  22. }% a) u9 J4 V; g: A) X" t9 C
  23. 9 v$ z) H! H) X, d6 D+ L
复制代码
& w: F& x, g' b: K
在执行写入数据的时候函数的参数有三部分:: ^9 p- G( y9 F  a& [0 M. n9 d: k% Q
1.要写入的地址
, H0 b  \# x& _6 g! S2.要写入数据的首地址! e5 R- W: W0 v2 T6 F
3.要写入数据的大小9 y/ D1 Z: i; @- u% f9 v, ~; @
函数在执行的过程中,首先发送一个写使能信号,然后发送写数据指令,紧接着发送数据要写入的地址,然后就是逐位发送数据了,函数最后等FLASH处理完这次操作再退出。7 ?* f/ C" V! U$ n2 P: {5 z

  [" y. l9 S' |" [7 Y0 N* Z

2 A4 g" H% q4 L. N读取数据
- \5 P7 i* Y+ C) J! e( \  M4 Q7 ~
  1. /**********************读取指定地址、指定长度的数据******************/8 {8 R- L- s. k- |+ b& z
  2. /* 因为读取在了指针中,所以不需要返回值 */& v1 g9 a. j# O- g6 z
  3. void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size). V1 ~, ]  ?  g) K3 m. d2 k
  4. {$ q$ x9 H  Z- F
  5.         SPI_NSS_Begin();. c3 y7 n8 R: b: N& y/ o2 S
  6.         /* 发送读取命令 */6 [( m% V; A" \% m% X8 m% ]6 `
  7.         SPI_SendData(W25X_ReadData);
    : M8 i/ [" E( C! Y
  8.         /* 发送读取数据的地址,高位先行 */0 S; ~, V$ E$ ^# |, Q/ z
  9.         SPI_SendData((addr & 0xff0000) >> 16);' n/ `4 V3 q+ x5 |9 x) t
  10.         SPI_SendData((addr & 0xff00) >> 8);
    7 `& e1 J* N6 m( |/ Z' Z/ w8 x# c0 ?" f. T
  11.         SPI_SendData(addr & 0xff);
    1 i& q6 ^) z! F
  12.         /* 逐位读取数据到指针上 */
    4 }. }) [7 H1 R! y0 N. u
  13.         while(size--)- Q& P1 d- {$ a: _
  14.         {
    + u+ ]- \5 b* C/ u
  15.                 *pBuffer=SPI_SendData(Dummy_Byte);) L7 R# D0 ?8 [- s( U
  16.                 pBuffer++;  p! G) l' T* J2 w
  17.         }
    4 Q+ p3 m. W6 X4 l
  18.         SPI_NSS_Stop();6 H! [; a$ y, C" T, J$ i  ?8 u$ |
  19. }
    % @# @+ J) o1 C$ J- T4 A- `

  20. ! j1 v% A% M- C5 Q
复制代码
: W& f. N# M8 V# g; I
在执行读出数据的时候函数的参数也有三部分:
3 J  i4 ?. [' w/ |* r1.要读出的地址
! i  N* d% H1 h! R; g" v/ I2.读出到指定地址4 U# N. F3 J0 O
3.读出数据的大小
, q, k/ y8 @8 w+ E9 A; @函数执行过程,首先发送读取指令(这时就不用发送写使能了),然后读取数据的地址,然后将数据逐位读取在固定地址中(地址最好是全局变量),使用时再从全局变量地址中获取数据。: W: q& L7 s1 T, U
这里涉及到函数的返回值问题,具体分析链接:返回多个变量怎么办! c! ]2 C, K! I5 P6 a
. ^1 S& d( S1 j

4 F4 G) [5 f$ ]% i* ~% }) U有一个问题当时困扰了博主一天,那就是发送和读取数据时,怎么把数据返回到主函数中,解决方法是,创建俩个全局变量数组,一个负责发送数据、另一个负责接收数据,这样就ok了" |$ v( J3 d% m2 K3 w, B
附上主函数
' n' y' X9 F' p' ?
  1. /**********************读取指定地址、指定长度的数据******************/
    1 f9 o$ d: P; ~6 c9 b. _$ m
  2. /* 因为读取在了指针中,所以不需要返回值 */! F3 ^2 L3 c' q0 N: |: i# h* o
  3. void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size)
      [' E% t5 V6 p2 r( {
  4. {2 ^0 R1 n7 C* l) k# Y3 X7 F! X
  5.         SPI_NSS_Begin();4 k! [( `1 g- j% i
  6.         /* 发送读取命令 */, ?4 ~: B" Y8 H3 K0 L, o; Q
  7.         SPI_SendData(W25X_ReadData);6 ?& w0 M' q; _
  8.         /* 发送读取数据的地址,高位先行 */
    ( k! @. t$ `! `. O2 h
  9.         SPI_SendData((addr & 0xff0000) >> 16);
    6 l  z( Q* ?9 ~" H
  10.         SPI_SendData((addr & 0xff00) >> 8);6 c6 `4 u9 |9 x6 n
  11.         SPI_SendData(addr & 0xff);
    9 Y! D- S4 u- w  ^# d* G# P) H2 H( j
  12.         /* 逐位读取数据到指针上 */3 Z, c8 ~6 B3 d$ G4 [
  13.         while(size--)
    ! _* ^/ ^7 m- T) j& |7 H9 W& s% f  S
  14.         {
    & v1 A0 A1 E% o; Z' d
  15.                 *pBuffer=SPI_SendData(Dummy_Byte);
    6 {0 V$ z- _4 {
  16.                 pBuffer++;$ g1 K$ s& ]% x9 a
  17.         }3 p3 @1 Y1 b) ?: F5 i# x
  18.         SPI_NSS_Stop();
    % N# f' p% b" c: q" m
  19. }
    4 V# l, c; ^2 K+ n
  20. 7 V% S; q% i4 h7 G/ k: f+ v+ F" E( J$ X
复制代码

6 X0 m$ X) c* }: I0 K2 a* x————————————————
: J) G) H. l3 ~( F& K版权声明:Aspirant-GQ
, T, f& ]$ V1 I1 P8 F/ n- j( H# ]& Z1 _& p  e
如有侵权请联系删除* d# \. Z! @% g7 Y

: J% h; D7 D/ l' h# G. u
* m! [" D- _  R! f: X! r( M/ f$ h7 D% d# N3 d& n$ j
收藏 评论0 发布时间:2023-3-18 15:00

举报

0个回答
关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
st-img 微信公众号
st-img 手机版