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

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

[复制链接]
攻城狮Melo 发布时间:2023-3-18 15:00
FLASH简介
' c4 U0 a! W; x# M! f! ^7 FFLASH俗称闪存,和EEPROM一样,都是掉电数据不丢失的非易失行存储器,但FLASH的存储容量普遍大于EEPROM,现在像如U盘、SD卡、SSD固态硬盘以及STM32芯片内部存储程序的设备都是FLASH类型的存储器。由此可见FLASH对于我们学习和工作的重要性,EEPROM可以实现单字节的擦写,而FLASH都是一大片的擦写,就像是大规模杀伤性武器,其最小擦除单:扇区的大小也是4KB。
/ [- l% K% [; o2 _* m; R我们此次通过SPI对FLASH存储芯片W25Q64进行读写擦除的操作。. d6 _1 t/ {" Y% Z/ A5 z% Z

/ w  }- A! {. y7 _对于FLASH内部结构的详细说明博主会专门整理一篇博客来说明,所以关于FLASH芯片的相关原理,本文中只做简单说明,侧重代码部分。
+ y0 F/ T$ Y7 h, v, hFLASH详细说明的博客链接:(没有链接就说明还没有整理出)
6 U' e5 G; g! g; W8 I
# o, R) y- S% L& g- d5 {* D( o; QW25Q642 o* L. K/ K# P/ w0 s, e/ D* a
W25Q64简介

8 x' g) H3 }- G- Y7 A" @) k就长这么个样子
  [/ r3 x4 }$ Y7 Z; f/ ^4 V8 P7 w1 B) {% h3 y
20190814195953540.png
# M/ K8 M( h' U  L; {/ j7 H
8 ?5 `4 e  Z7 Z2 Q' w
STM32内部原理图如下:8 I  h' j4 I: k7 J" v

1 N5 h1 h4 b* Y6 L! n  t
20190814200413370.png
8 X+ Z+ B  D) L. a! H1 v" F9 B8 B% o8 o9 B) m/ {# r9 j& i% B

& ~1 x0 U4 g+ qW25Q64是一种使用SPI通信协议的NOR FLASH存储器,它 的CS/CLK/DIO/DO 引 脚 分 别 连 接 到 了 STM32 对 应 的 SPI 引 脚NSS/SCK/MOSI/MISO 上,其中 STM32 的 NSS 引脚是一个普通的 GPIO,不是 SPI 的专用NSS 引脚,所以程序中我们要使用软件控制的方式。FLASH 芯片中还有 WP 和 HOLD 引脚。 WP 引脚可控制写保护功能,当该引脚为低电平时,禁止写入数据。我们直接接电源,不使用写保护功能。 HOLD 引脚可用于暂停通讯,该引脚为低电平时,通讯暂停,数据输出引脚输出高阻抗状态,时钟和数据输入引脚无效。我们直接接电源,不使用通讯暂停功能。
( t9 c" U1 J! _$ _; V  L2 P
+ Z" C) X0 p8 }5 M+ Y2 C
W25Q64支持SPI通讯的模式0和模式3
7 P2 `6 @) r4 X, o% j) f+ M, n) i0 z7 V* }9 H
) g: ^9 V8 f( x6 a
FLASH控制指令% P2 h1 ]8 I& W, x+ o
FLASH芯片中规定了许多指令,只要SPI向FLASH发送相应的指令,FLASH就会执行相应的操作,所以我们对FLASH的一切操作都是基于这个指令集的,接下来介绍一下FLASH的控制指令:; f1 k% |: m& t# j# L
5 ]& L6 B0 ?# m! ]+ {
20190814200912265.png 3 ]: M& W6 {9 R' `

) j7 Y$ A/ ]7 R5 M8 I表中第一列为指令名,第二列为相应的指令代码,第三列及后面的内容根据指令的不同而意义不同,其中带括号的字节参数,方向为 FLASH 向主机传输,即命令响应,不带括号的则为主机向 FLASH 传输。表中“A0~A23” 指 FLASH 芯片内部存储器组织的地址; “M0~M7” 为厂商号( MANUFACTURER ID); “ID0-ID15”为 FLASH 芯片的ID;“dummy”指该处可为任意数据;“D0~D7” 为 FLASH 内部存储矩阵的内容。
5 l8 v. Y( i$ C看起来很复杂的样子,其实只要在需要执行相应操作时来查这个表,只要能够理解这些指令的使用方法,FLASH就算学会了。) I: w7 l6 \) ]
2 l4 F& Y2 C- Y9 }8 k& h
例如:要知道FLASH的ID,那就在指令中找对应的取ID指令“JEDEC ID”,仔细解读这个指令, r& J' l6 }; a
20190814201942797.png 7 g* c! o+ i, x) U# L6 x

$ [! m! g, @7 E. ?1 f) J可以看出对应的指令代码为“9F”,后面的三个字节带括号,代表这三个字节就是FLASH向STM32发送的数据,即这三个字节就是FLASH的ID,然后使用SPI进行读取就可以了。
* p' `2 L  p5 M' V0 v, _' T& ^
4 W" H9 P) l2 v- O  S& _
我们一般是将这些指令宏定义在头文件中,便于使用:- N- \4 @0 b- m9 _2 V7 u
  1. #define W25X_WriteEnable                      0x06
    6 y! M4 R( v$ V: [, y) T7 i
  2. #define W25X_WriteDisable                      0x04
    ) T8 w$ Y1 V0 p' s. B
  3. #define W25X_ReadStatusReg                      0x05
      \& @! |* N2 O* d2 m/ @9 G
  4. #define W25X_WriteStatusReg                      0x01 / j$ L9 E! R" E
  5. #define W25X_ReadData                              0x03 ; i! @; f, V9 [  v1 W* |
  6. #define W25X_FastReadData                      0x0B 2 S$ a1 |# X( w8 z' d
  7. #define W25X_FastReadDual                      0x3B ' j% U* z2 Q; n4 Y! L- J
  8. #define W25X_PageProgram                      0x02
    % k! t2 \( ]0 C, ]8 ?  x; g
  9. #define W25X_BlockErase                              0xD8
    9 B8 @$ ?& c7 @
  10. #define W25X_SectorErase                      0x20 ) I8 ]# d- Z! C- @$ _0 G
  11. #define W25X_ChipErase                              0xC7
    ; L' ^' U0 }! w
  12. #define W25X_PowerDown                              0xB9
    # M+ \6 b8 Q# N/ Q3 i) w% |
  13. #define W25X_ReleasePowerDown              0xAB & t' z/ H$ M* ^% c; I4 `6 I9 j
  14. #define W25X_DeviceID                              0xAB 8 u) I& g$ Z9 I& h+ _
  15. #define W25X_ManufactDeviceID         0x90
    + _- u- u! X; _" S
  16. #define W25X_JedecDeviceID                      0x9F
    " w, E8 Y, V! i4 [1 l% W

  17. ( Z6 N. L& X  d3 P4 ^% c" ~8 e
复制代码

1 o" c6 S5 D& }/ K& l. aFLASH内部存储结构
( m, e2 |8 e" ~2 c% R% EFLASH的存储矩阵如图:其内存分为128块,每一块都有16个扇区,每个扇区大小为4KB,擦除数据的时候是以扇区为基本单位的。* k6 ]9 i3 s, G2 u) y( J* y
3 k$ l4 j7 n5 a9 u
20190814203311343.png
& i! X) r0 Z) G0 e- k  h5 q+ T. I0 [) C
代码讲解7 X' L9 ^: Z& i6 v7 N4 Y$ G6 ^
代码都是博主亲手写出来的,可以运行。
/ d& ^/ O; g7 b% H8 G& K6 p+ O3 m代码部分会用到SPI的代码,关于SPI的说明之前整理过:SPI详解* b0 S" y2 X) h! R" ?$ t% ~; \

7 d9 F" L. b' |读取芯片ID: J' d! P( C  j
  1. /************************读取芯片ID*************************************/3 e$ Z5 T* G4 i, l
  2. uint32_t FLASH_ReadID(void)5 s. T4 t  K. o& U, E( W8 e+ m# N. L
  3. {- @. S; p% Z$ j9 y3 I! s$ G
  4.         uint32_t temp,temp1,temp2,temp3;
    2 \8 v- R1 W. s! h0 L1 ^7 |% f! t
  5.         SPI_NSS_Begin();; e" M" z% [+ M( l$ s
  6.         /* 发送取ID指令 */; g  ?( H+ r% W( f# z9 c) F0 C% l* T
  7.         SPI_SendData( W25X_JedecDeviceID);% n/ ?+ J6 O* H, A" s% u- a- Y
  8.         /* FLASH会连续发送三个字节数据 */8 ~1 T5 h- [2 C. I) Z
  9.         temp1=SPI_SendData(Dummy_Byte);
    3 w  f& L" [: b
  10.         temp2=SPI_SendData(Dummy_Byte);
    : L$ y, D7 e6 t
  11.         temp3=SPI_SendData(Dummy_Byte);1 X6 B; h  R$ K. S" n
  12.         SPI_NSS_Stop();. d0 X, R* \8 `% j
  13.         /* 高为先行,将三个字节整理在一起 */3 i6 y" b( m5 [. y1 Z$ v# r" R
  14.         temp=temp1<<16 | temp2<<8 | temp3;
    # T0 |) ]2 Y* {# X# W( z
  15.         return temp;) I+ D" W0 O, y  b5 N- [
  16. }/ j& I% c: e! J1 L& T7 y7 e( ]
复制代码

+ }( F6 }  n1 s) [. ~9 a由于SPI是全双工通信,所以接收数据和发送数据用的是同一个函数
  h% |- A6 e/ B5 U# I
0 l) w, h" {" Q8 }+ k7 Q1 ], M
发送写使能信号
5 U4 P& b8 Y4 \! V. g, `- a
  1. /****************发送写使能信号****************************/
    " T( w4 u/ D3 o) v1 ?
  2. void FLASH_WriteEnable(void)+ H/ K! S* Y1 s, |' g& R
  3. {
    : B2 m6 _& |0 M$ ^. r5 r0 a
  4.         SPI_NSS_Begin();
    ) C. X; j& {, E4 c+ c3 _7 [2 Q
  5.         /* 发送相应的指令代码 */
    0 s/ r7 v- D, q& L/ ]
  6.         SPI_SendData(W25X_WriteEnable);- @6 o3 J; z+ t4 l( Z3 m
  7.         SPI_NSS_Stop();
    ! [8 H6 M. G& {+ C/ k3 @) `0 Q
  8. }
    ' g, H* {7 c' p9 m
复制代码

8 t( r! F- x0 Y* O在向FLASH中执行写操作之前,都要进行写使能操作,通过发送写使能指令到FLASH来实现
8 [, j, E' |* H9 U$ f: T  D
; b) K1 ~% T! w! w
等待FLASH不忙
' T' C) r0 l( o2 w( N. Z/ }# M
  1. /******************等待,直到不忙***********************************/
    + D1 _3 K' `9 W7 g/ P$ k5 f+ s8 Q
  2. void FLASH_WaitBusy(void)# x! g- \) T  `: Y
  3. {
    ( `( P( U. G9 R" n* P
  4.         uint8_t StatusReg=0x01;
    # N2 |8 M2 b- o9 ~/ r' C
  5.         SPI_NSS_Begin();
    * Z. L9 i; e& j* \
  6.    /* 读取状态寄存器中的数据,判断忙标志0x01位 置位代表忙 */
    3 F! g2 |. n. c" }9 ^
  7.         SPI_SendData(W25X_ReadStatusReg);      
    6 u# J% U% R) I6 h# S) f
  8.    /* 只读取状态寄存器的BUSY位,即第一位 */
    / P% A: |8 ?& R9 m% P
  9.         while((StatusReg & 0x01) == 1)
    ' R3 Y8 I4 i5 Q2 T3 {
  10.                 StatusReg=SPI_SendData(Dummy_Byte);
    5 w5 I( D; ]* c. _- P/ h. h/ t
  11.         SPI_NSS_Stop();
    6 X+ t1 a8 _. g
  12. }  N; L( d. u& S8 E) k2 ^

  13.   n9 U7 u6 q. s/ B2 U
  14. $ K6 Q' s" K3 i" r5 \' }5 A# Q% Q
复制代码
/ C" X& y9 ^7 F& d4 i
FLASH在通讯的过程中需要一定的时间来执行操作,在这期间,传输数据是无效的,因为FLASH忙着呢,所以我们就要有一个函数来专门等!等到FLASH不忙了,再进行通讯,那怎么等呢?FLASH不忙了会给出一个信号——将状态寄存器的BUSY位重置(也就是0),所以我们需要不断的来检测状态寄存器中的BUSY位是否置位,利用读取寄存器状态的指令来获取状态寄存器当下的状态,然后根据寄存器的BUSY位(第1位)来判断FLASH是否处于忙碌状态。* L7 `) q* h0 n/ s+ I
简单来说,这就是个延时函数,延时直到FLASH空闲,可以进行下一步传输。
* v. Y4 C1 M3 R$ `3 u0 ^' B% {
' L3 I5 ~" l9 M. \
擦除扇区

# I3 _8 M3 \$ b: u1 h+ A! H6 H
  1. /******擦除扇区的内容,切记地址要对其到4kB,每个扇区的大小都是4KB********/
    : {/ U# H" L6 z
  2. void FLASH_SectorErase(uint32_t addr)& L0 X1 ^5 O1 Y
  3. {* R2 Y; r5 K/ S5 y% {# r+ {9 k
  4.         /* 开始的时候要发送写使能信号*/
    6 M% V% r$ O( [# ]0 t4 s$ M
  5.         FLASH_WriteEnable();
    ; `" ^( ]+ Z, Y2 S1 M9 R
  6.         SPI_NSS_Begin();' v8 `" _/ y, U8 |" r& ]; W
  7.         /* 发送扇区擦除命令 */
    3 l& L/ m. l" b9 M- T8 G
  8.         SPI_SendData(W25X_SectorErase);+ `) p6 g& S. ]' n
  9.         /* 发送扇区的地址,高位先行 *// G, V- i5 |" X0 z
  10.         SPI_SendData((addr & 0xff0000) >> 16);. B( F6 J  W2 _: F# J  p, @
  11.         SPI_SendData((addr & 0xff00) >> 8);
    $ U- @0 ^2 G" L6 a' R2 v
  12.         SPI_SendData(addr & 0xff);- c9 o9 k8 |- P
  13.         SPI_NSS_Stop();6 K1 R: j# x; W
  14.         /* 最后也要等待FLASH处理完这次的信号再退出 *// Q4 d9 X$ k! I  y- \! G
  15.         FLASH_WaitBusy();
    ' t3 e+ k2 e: m# F3 q
  16. }8 r# l" j& g2 }& k) c

  17. & P) B9 T- j* R7 G9 a7 O9 V' ]; X
复制代码

+ A  L  b7 M8 k  u4 {  N扇区的擦除之前要发送一个写使能信号,先发送擦除指令,然后发送要擦除扇区的地址(分三个字节发出去),高位先行。" u9 }6 L6 U8 M. j' `  O2 Y
扇区上的内容不是1就是0,擦除的过程就是写1的过程(将一个扇区全部写1),因为在写入数据的时候,可以将1写为0,但不能将0写为1.
) R$ s- I& E8 F; w
/ {* t2 n5 q6 h
" @4 O/ M* m+ z! T) N& `% O5 y
写入数据$ \0 m+ q: C* j6 M
  1. /************按页写入数据,但写入之前要进行擦除***********/
    ' J6 b: Y, r3 `% c  X: d- d7 V
  2. void FLASH_PageWrite(uint32_t addr , uint8_t* pBuffer ,uint8_t size)
    ! B# U: j7 \* e
  3. {9 f0 s4 {4 f6 d
  4.         /* 开始的时候要发送写使能信号 */
    0 Q: N: A4 ]% h4 N
  5.         FLASH_WriteEnable();
    ; v  u- m8 K0 v9 |/ ]7 r4 H& O
  6.         SPI_NSS_Begin();0 n. w* t2 R5 O$ }4 d
  7.         /* 发送页写入命令 */
    0 @' ~0 \6 a6 V3 L9 j& C
  8.         SPI_SendData(W25X_PageProgram);5 Q8 ]$ ~$ P/ g) }2 V4 n3 }( v
  9.         /* 发送写入的地址,高位先行 */
    " ]& q% n9 Z5 H* T4 s
  10.         SPI_SendData((addr & 0xff0000) >> 16);
    ' E) H8 X7 F3 u# k
  11.         SPI_SendData((addr & 0xff00) >> 8);. A/ |! X2 c; j% p+ o5 D
  12.         SPI_SendData(addr & 0xff);
    " A' B2 x+ b' v  F' [
  13.         /* 逐位发送数据 */
    ) d/ F  ^3 r3 D& @! T/ `6 G
  14.         while(size--)$ W  m1 s5 v1 C1 p
  15.         {+ K+ p9 g% |& x. ?4 J( k/ j
  16.                 SPI_SendData(*pBuffer);
    . D" E6 k2 L: C1 t! f5 w8 W* S% y
  17.                 pBuffer++;7 \* k; h* U$ S* Z& ?
  18.         }) d+ H' B* O" y3 c4 Q' `; f
  19.         SPI_NSS_Stop();: [6 j: C: Y  R) F. e% T0 R
  20.         /* 最后也要等待FLASH处理完这次的信号再退出 */( v; J- y8 ?( j+ t; A+ l  z5 t2 M
  21.         FLASH_WaitBusy();) t: J& e' ~. l# W* n# U' b/ Q; f
  22. }8 `- m! S" {/ h3 Z& o/ ^. W

  23. / p+ X9 h8 C" d6 }! P3 s5 \5 n
复制代码

3 q% x  l" G0 R, ^$ W+ o2 S在执行写入数据的时候函数的参数有三部分:
, Y; r; a, R* ]! o1.要写入的地址8 F. O" b) J0 e3 i' N
2.要写入数据的首地址
( q% n! T. [/ Q: B3.要写入数据的大小
/ {7 B7 T4 T- k3 H' m+ ^函数在执行的过程中,首先发送一个写使能信号,然后发送写数据指令,紧接着发送数据要写入的地址,然后就是逐位发送数据了,函数最后等FLASH处理完这次操作再退出。
. E7 [- z  w6 E
7 ]" l, v! K& s& m2 E" ]  P; f
6 @; q/ K1 r5 Y! f
读取数据5 ^+ g+ Q9 }! x  _
  1. /**********************读取指定地址、指定长度的数据******************/8 p! C5 P% b- ~
  2. /* 因为读取在了指针中,所以不需要返回值 */: f3 @1 y0 N- ?+ d8 `! {: W
  3. void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size)
    , l; o7 P5 ^5 z5 O3 u
  4. {
    7 k1 i6 n+ P7 r- Q& D& e& K
  5.         SPI_NSS_Begin();
    ) I' L9 f) X! e+ V7 x
  6.         /* 发送读取命令 */- F( H$ Z5 ?7 c& k$ Q5 s, @2 u+ T
  7.         SPI_SendData(W25X_ReadData);: i4 w2 N6 z% B5 p) ~5 R9 ~1 H
  8.         /* 发送读取数据的地址,高位先行 */
    2 m1 F0 ^& ~1 p' l. j: U( n& N
  9.         SPI_SendData((addr & 0xff0000) >> 16);( n4 s, J( t1 D% d6 [, \
  10.         SPI_SendData((addr & 0xff00) >> 8);
    , U) I* [- Y% F8 C. o
  11.         SPI_SendData(addr & 0xff);  K+ M9 u4 e  X3 z! T* M
  12.         /* 逐位读取数据到指针上 */) _# K1 c, R+ r, P
  13.         while(size--)
    4 \) T( @. d: V. A9 Q$ V
  14.         {# q( a$ G3 Q/ Z5 i, l% ~* }
  15.                 *pBuffer=SPI_SendData(Dummy_Byte);0 f! f4 I, E) E% i; l* @
  16.                 pBuffer++;8 a! \* R/ `* |3 V' `& E* C3 J
  17.         }
      E+ _* r- K. @9 e
  18.         SPI_NSS_Stop();
      ]9 S) w* x9 J$ |$ J9 k0 R
  19. }
    . l3 o0 f' q* X- E: d0 n) l, P

  20. ( Y$ t1 u# P$ I* n
复制代码

" e# Z% U: A8 _7 t在执行读出数据的时候函数的参数也有三部分:/ R- E; d( H/ f% R1 i3 |+ L
1.要读出的地址
! |* k- M' |- O' X! I5 `2.读出到指定地址
6 c7 m0 \% M  L5 [, O3 e3.读出数据的大小3 E7 \5 d# A% D0 C
函数执行过程,首先发送读取指令(这时就不用发送写使能了),然后读取数据的地址,然后将数据逐位读取在固定地址中(地址最好是全局变量),使用时再从全局变量地址中获取数据。
7 }9 `! }- ^9 {) i& s$ a这里涉及到函数的返回值问题,具体分析链接:返回多个变量怎么办7 Q( M3 E" C2 F4 I% A" f

! N& r5 T7 _/ P" w( T% S2 ^6 i- f1 D; m# n/ R* e& L1 h% O
有一个问题当时困扰了博主一天,那就是发送和读取数据时,怎么把数据返回到主函数中,解决方法是,创建俩个全局变量数组,一个负责发送数据、另一个负责接收数据,这样就ok了% H, |7 ?2 t0 w1 l
附上主函数% s. h$ _3 ~5 r: d, Q
  1. /**********************读取指定地址、指定长度的数据******************/1 l% o1 p% p( a, J
  2. /* 因为读取在了指针中,所以不需要返回值 */
    9 A! ?% X% @/ U4 Y9 e" T" b8 ]
  3. void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size)# w. K" s  H' h# Y; g
  4. {
    . v0 d8 u1 ^: T
  5.         SPI_NSS_Begin();
    . m: N+ u4 \- n7 w7 n. T1 d7 f; \
  6.         /* 发送读取命令 */
    ( L0 Z8 X" p) T* h# U, c6 I0 l$ D
  7.         SPI_SendData(W25X_ReadData);3 _; y( i2 ]- b- a" W3 H
  8.         /* 发送读取数据的地址,高位先行 */8 ~* @+ p" B8 C, W  [
  9.         SPI_SendData((addr & 0xff0000) >> 16);3 T4 T/ o2 i3 B. P/ v
  10.         SPI_SendData((addr & 0xff00) >> 8);- H( a4 F$ s7 f1 P, Q9 ~1 M& o
  11.         SPI_SendData(addr & 0xff);1 e# B7 O8 m! N3 H
  12.         /* 逐位读取数据到指针上 */. L3 k- I* n8 a6 v$ A; q6 H" a& L
  13.         while(size--)
    ' Z5 J# F2 ?2 u* ~* ]3 `
  14.         {
    % m) J9 S) U/ o
  15.                 *pBuffer=SPI_SendData(Dummy_Byte);9 k# T. R; G, ~/ u4 `( Z
  16.                 pBuffer++;1 ^5 \; j/ @5 y9 a0 m" w
  17.         }
    ; f& Y# H: N9 e% B0 d9 j
  18.         SPI_NSS_Stop();
    7 T% S  w8 F6 k" }) _/ A
  19. }# @+ d- j8 ~2 v- D) W  S

  20. ( Z* f2 R% E0 C9 U( }8 W+ e2 u
复制代码
3 R- l: Y4 N  f# B
————————————————# O) X' d: @+ l
版权声明:Aspirant-GQ. r/ I& l# L* Z- ~# H5 u- ~2 \
/ r$ b  `/ A$ r) `1 t  L
如有侵权请联系删除$ Q4 G0 j+ |4 Z. {. K6 S
# @" c2 y6 q' a: N& t
# k5 A' [) c, D# B$ x( N% ]/ k
; Y2 \, Y% C( ~2 ?: d: ]. X- H1 l
收藏 评论0 发布时间:2023-3-18 15:00

举报

0个回答

所属标签

相似分享

官网相关资源

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32Cube扩展软件包
意法半导体边缘AI套件
ST - 理想汽车豪华SUV案例
ST意法半导体智能家居案例
STM32 ARM Cortex 32位微控制器
关注我们
st-img 微信公众号
st-img 手机版