请选择 进入手机版 | 继续访问电脑版

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

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

[复制链接]
攻城狮Melo 发布时间:2023-3-18 15:00
FLASH简介3 ?; l1 r  k0 O0 U8 v. @) `" W
FLASH俗称闪存,和EEPROM一样,都是掉电数据不丢失的非易失行存储器,但FLASH的存储容量普遍大于EEPROM,现在像如U盘、SD卡、SSD固态硬盘以及STM32芯片内部存储程序的设备都是FLASH类型的存储器。由此可见FLASH对于我们学习和工作的重要性,EEPROM可以实现单字节的擦写,而FLASH都是一大片的擦写,就像是大规模杀伤性武器,其最小擦除单:扇区的大小也是4KB。3 S. |2 n5 [# ~5 W. R
我们此次通过SPI对FLASH存储芯片W25Q64进行读写擦除的操作。6 d: R1 q1 [% O: {4 Q$ b
# g; {* a8 W+ h; d' [8 S
对于FLASH内部结构的详细说明博主会专门整理一篇博客来说明,所以关于FLASH芯片的相关原理,本文中只做简单说明,侧重代码部分。+ k( K; t! S/ B, B0 f$ ?9 ?
FLASH详细说明的博客链接:(没有链接就说明还没有整理出)
8 l) ?( c: G2 j4 Y1 A
4 D$ w7 C1 t! ^W25Q64" W) o: w- B) c7 F. X
W25Q64简介
+ s/ e: {# I4 R
就长这么个样子
; k, l- Y+ [9 k8 c4 S
1 r, D: F2 r  P+ O
20190814195953540.png
! ]: s+ K6 Y  e5 \
1 w$ V5 a; G9 A5 \: G7 X
STM32内部原理图如下:
9 K4 d5 F# U. n! k" @; I" z- _2 T5 h  Y1 ~3 |
20190814200413370.png
" m/ V0 T+ k) u/ U* u! S
0 _' Q3 `" f5 o+ j

; ?9 m! M6 h8 C$ ]W25Q64是一种使用SPI通信协议的NOR FLASH存储器,它 的CS/CLK/DIO/DO 引 脚 分 别 连 接 到 了 STM32 对 应 的 SPI 引 脚NSS/SCK/MOSI/MISO 上,其中 STM32 的 NSS 引脚是一个普通的 GPIO,不是 SPI 的专用NSS 引脚,所以程序中我们要使用软件控制的方式。FLASH 芯片中还有 WP 和 HOLD 引脚。 WP 引脚可控制写保护功能,当该引脚为低电平时,禁止写入数据。我们直接接电源,不使用写保护功能。 HOLD 引脚可用于暂停通讯,该引脚为低电平时,通讯暂停,数据输出引脚输出高阻抗状态,时钟和数据输入引脚无效。我们直接接电源,不使用通讯暂停功能。
% S- h- R- t4 G5 M1 W5 w! C0 w, \! I/ c( I" ^% \$ X( j
W25Q64支持SPI通讯的模式0和模式3
& X. \- i, @# q- o/ Z1 M/ Z1 [7 U8 j
4 v% _7 |- i8 y
FLASH控制指令" o" d4 L- a. N. p. S
FLASH芯片中规定了许多指令,只要SPI向FLASH发送相应的指令,FLASH就会执行相应的操作,所以我们对FLASH的一切操作都是基于这个指令集的,接下来介绍一下FLASH的控制指令:
% U0 {% a& v% T) P' v; q9 R8 ~9 x* A* H( V$ N: D& i
20190814200912265.png % p8 W. |* n7 W, }2 M+ A

6 u: }- m  w& W: y表中第一列为指令名,第二列为相应的指令代码,第三列及后面的内容根据指令的不同而意义不同,其中带括号的字节参数,方向为 FLASH 向主机传输,即命令响应,不带括号的则为主机向 FLASH 传输。表中“A0~A23” 指 FLASH 芯片内部存储器组织的地址; “M0~M7” 为厂商号( MANUFACTURER ID); “ID0-ID15”为 FLASH 芯片的ID;“dummy”指该处可为任意数据;“D0~D7” 为 FLASH 内部存储矩阵的内容。
8 |+ \" A0 Z1 G; i看起来很复杂的样子,其实只要在需要执行相应操作时来查这个表,只要能够理解这些指令的使用方法,FLASH就算学会了。
: M. y/ {6 ?6 C* }! O3 q
7 K- J4 N; @6 T+ H
例如:要知道FLASH的ID,那就在指令中找对应的取ID指令“JEDEC ID”,仔细解读这个指令
* u& \7 E& f: E, S0 }) G2 d
20190814201942797.png
$ i4 H% N7 z- {/ n# |0 J; b
" q& O2 _& @- ^! G  }( j2 d
可以看出对应的指令代码为“9F”,后面的三个字节带括号,代表这三个字节就是FLASH向STM32发送的数据,即这三个字节就是FLASH的ID,然后使用SPI进行读取就可以了。4 z( }+ r% e- C$ f, w$ l

$ T$ A9 K3 D- B0 F我们一般是将这些指令宏定义在头文件中,便于使用:! z* P; n8 W' h" S, W0 J
  1. #define W25X_WriteEnable                      0x06
    5 m( Q; V; S2 v9 `  F8 Z
  2. #define W25X_WriteDisable                      0x04 7 U+ k$ d# m& c' s; G* k
  3. #define W25X_ReadStatusReg                      0x05
    ! @! [: W# l, V+ j$ \! U6 t- W
  4. #define W25X_WriteStatusReg                      0x01
    : C: L8 }+ s0 [" S4 g6 x" z0 ~
  5. #define W25X_ReadData                              0x03
    5 `! O. \" u" l2 T7 o& p+ c. A
  6. #define W25X_FastReadData                      0x0B 3 x# r5 P. C/ v& s
  7. #define W25X_FastReadDual                      0x3B 9 a. r8 G8 U; x4 H0 x( l
  8. #define W25X_PageProgram                      0x02
    1 p# |7 r$ m9 S
  9. #define W25X_BlockErase                              0xD8
    . B1 D6 S% z) E. T
  10. #define W25X_SectorErase                      0x20 # w# H. `4 Q5 S; l4 `
  11. #define W25X_ChipErase                              0xC7
    * l% d# E" S$ ]* b' O+ n
  12. #define W25X_PowerDown                              0xB9 5 _5 T+ h0 ~: `$ U6 m  T6 ~
  13. #define W25X_ReleasePowerDown              0xAB
    1 h4 m  ^% G, y3 S- B* ?! S9 ^
  14. #define W25X_DeviceID                              0xAB
    ( U5 k1 ~& x# C" i' C
  15. #define W25X_ManufactDeviceID         0x90
    % N% T9 O* V  W9 U% _0 x
  16. #define W25X_JedecDeviceID                      0x9F2 X* D) F& n0 R1 Z* o% w

  17. + ^, F0 v0 x( {3 {
复制代码

4 l6 S, u. ~! p- x* a- s, ^FLASH内部存储结构
1 D# N; v3 X1 L2 [/ YFLASH的存储矩阵如图:其内存分为128块,每一块都有16个扇区,每个扇区大小为4KB,擦除数据的时候是以扇区为基本单位的。# j. A* n  j  b8 c1 X" |$ p: i

: O% }7 a# A& @/ k+ Q  d
20190814203311343.png
# m/ u0 P6 g: ^1 A7 t/ r: N+ L0 W" P2 M! F3 A, H  Y. C; I8 e
代码讲解
3 U- V9 y; Y& k) r代码都是博主亲手写出来的,可以运行。
' L% w9 g" z4 Q, ^" O" S代码部分会用到SPI的代码,关于SPI的说明之前整理过:SPI详解
3 z% l! L$ z6 p2 t

$ K5 X0 ^8 O9 G' B读取芯片ID
& K5 M; t4 m7 V  a( j8 z
  1. /************************读取芯片ID*************************************/
    " b5 z! k  D! M9 w  S1 g% z8 i- j
  2. uint32_t FLASH_ReadID(void)
    , ^$ k8 [# o9 ?5 F7 ~6 U( k* t
  3. {8 i. Y4 B: Y, w
  4.         uint32_t temp,temp1,temp2,temp3;
    6 A. s, [/ p! g5 Y+ @/ Y$ x1 c+ ^
  5.         SPI_NSS_Begin();0 l, A4 m' n- J. ~, p" X
  6.         /* 发送取ID指令 */5 @( a- x, p3 ]" R
  7.         SPI_SendData( W25X_JedecDeviceID);5 p9 ?, k- E$ ]
  8.         /* FLASH会连续发送三个字节数据 */
      W/ _/ n2 [; x9 u, T
  9.         temp1=SPI_SendData(Dummy_Byte);
    " S9 O5 S) ]' ]  Y& E
  10.         temp2=SPI_SendData(Dummy_Byte);
    ! t+ `6 z5 ?( \1 I! m
  11.         temp3=SPI_SendData(Dummy_Byte);8 d* G8 }& J# D  G3 }
  12.         SPI_NSS_Stop();
    * j% _0 E4 x+ ?/ k) D! r
  13.         /* 高为先行,将三个字节整理在一起 */; e, O3 G+ K4 `6 n6 [
  14.         temp=temp1<<16 | temp2<<8 | temp3;5 I. J# ^. [, z
  15.         return temp;1 B1 O% M: X( W& c, s
  16. }
    & w5 o! D0 w$ l* u5 \
复制代码

" B2 {3 J$ ?; x! K: h) Y由于SPI是全双工通信,所以接收数据和发送数据用的是同一个函数7 p- W7 M) w5 l0 b- `9 K0 H
) a, Z7 h  d* [7 k7 h. _
发送写使能信号
/ B$ W; _+ T. \
  1. /****************发送写使能信号****************************/6 Y6 Z; w0 w9 p# H# m1 j
  2. void FLASH_WriteEnable(void)3 l! P0 ?0 P( B7 o7 i8 }/ K6 r
  3. {* e4 p5 I$ D3 X4 [& l
  4.         SPI_NSS_Begin();
    ( x! |% l" u3 X% V$ [7 H6 F
  5.         /* 发送相应的指令代码 */
    ; [' I" k! V, q) D
  6.         SPI_SendData(W25X_WriteEnable);! A' F( H3 u8 a
  7.         SPI_NSS_Stop();
    # c2 v$ X0 n7 y  z- U2 ~) U, G
  8. }
    # k3 L8 f+ `1 r% @, C% P3 e2 i
复制代码

) X* C7 i: y) Q; A0 h1 x  B9 g在向FLASH中执行写操作之前,都要进行写使能操作,通过发送写使能指令到FLASH来实现
* B5 D0 b1 c+ n2 l

& p9 l6 B8 ]: I等待FLASH不忙- d# c+ b  E9 e- M9 C5 E3 ^
  1. /******************等待,直到不忙***********************************/
    6 A; c+ d* _9 r4 @: s% L4 I5 V4 U
  2. void FLASH_WaitBusy(void)
    : K& H+ h* p, |; r
  3. {$ P8 E* q5 U: q9 x  h: c6 Y' [' v
  4.         uint8_t StatusReg=0x01;
    ) B8 w; n) {/ l) T0 I* K
  5.         SPI_NSS_Begin();( K  G" V" \$ `/ E4 i8 m( Y' h+ M
  6.    /* 读取状态寄存器中的数据,判断忙标志0x01位 置位代表忙 */
    / w& c5 o3 E5 o2 h4 q0 e3 {
  7.         SPI_SendData(W25X_ReadStatusReg);      ; V7 v2 R3 ]! I) d2 O' @2 C: ]
  8.    /* 只读取状态寄存器的BUSY位,即第一位 */
    $ m# f* V. b4 w  r# u  J/ b$ }! E
  9.         while((StatusReg & 0x01) == 1)
    , I/ @, p3 ~0 \$ e
  10.                 StatusReg=SPI_SendData(Dummy_Byte);
    . _" e7 a' m+ a6 H9 O
  11.         SPI_NSS_Stop();
    ( p2 T- R: r* G* H  x6 n/ m
  12. }; K" s) }0 w/ V5 C. p; g: u1 j# x0 @
  13. ) m. r8 Z* l0 y' @4 U' h

  14. 0 I; \* ~! s! f5 M
复制代码

" X/ E  |, E/ ?) t* F8 K- ^FLASH在通讯的过程中需要一定的时间来执行操作,在这期间,传输数据是无效的,因为FLASH忙着呢,所以我们就要有一个函数来专门等!等到FLASH不忙了,再进行通讯,那怎么等呢?FLASH不忙了会给出一个信号——将状态寄存器的BUSY位重置(也就是0),所以我们需要不断的来检测状态寄存器中的BUSY位是否置位,利用读取寄存器状态的指令来获取状态寄存器当下的状态,然后根据寄存器的BUSY位(第1位)来判断FLASH是否处于忙碌状态。
5 V5 h/ {2 T( u, o0 V简单来说,这就是个延时函数,延时直到FLASH空闲,可以进行下一步传输。
6 U* m4 a2 ~# q6 P  l# C; B
5 I6 Q* M/ `0 K  _/ F
. ?+ _+ ~  y& C' c3 V8 A' h+ h
擦除扇区

3 Y- O9 q; q4 P' H) F
  1. /******擦除扇区的内容,切记地址要对其到4kB,每个扇区的大小都是4KB********/# S# U9 K$ K  D* i* n6 Z& |. J+ y; F/ j
  2. void FLASH_SectorErase(uint32_t addr)
    % K8 z! y1 E) X, t+ O# L- y6 `' r
  3. {
    5 X9 A+ \. F3 u, ]1 {! w' j
  4.         /* 开始的时候要发送写使能信号*/6 V" b0 a1 r% y- Z/ n7 }# d' t5 O
  5.         FLASH_WriteEnable();
    7 p$ J- L9 o+ w8 ~" g0 U4 C
  6.         SPI_NSS_Begin();0 ~! z  B- S5 `' n% @. e
  7.         /* 发送扇区擦除命令 */; N2 C* d1 d: a7 d& U. s
  8.         SPI_SendData(W25X_SectorErase);
    , _" Y3 Y# Q, G& p. y4 W: A
  9.         /* 发送扇区的地址,高位先行 */
    / _$ U0 s4 b+ _8 I$ U5 \1 }; ~0 R
  10.         SPI_SendData((addr & 0xff0000) >> 16);
    $ Y0 H1 I. D0 W# I# g. g
  11.         SPI_SendData((addr & 0xff00) >> 8);& n& H3 S) J/ _, G; o
  12.         SPI_SendData(addr & 0xff);
    / n2 C2 P6 x3 s1 r* |, b
  13.         SPI_NSS_Stop();2 i  ~# ~& N' [% S6 d& t9 T
  14.         /* 最后也要等待FLASH处理完这次的信号再退出 */
    & U+ `3 g6 ]3 q5 X3 ^" _: P7 G2 p# t! |/ ?
  15.         FLASH_WaitBusy();: _4 n8 _& v* p" P& N( s
  16. }4 Y; m5 Y% c3 C
  17. - m! [1 y1 P5 R7 \0 j
复制代码

; i1 J6 b5 I6 i7 b扇区的擦除之前要发送一个写使能信号,先发送擦除指令,然后发送要擦除扇区的地址(分三个字节发出去),高位先行。4 V+ `1 t' y2 P$ i3 Y! f" q
扇区上的内容不是1就是0,擦除的过程就是写1的过程(将一个扇区全部写1),因为在写入数据的时候,可以将1写为0,但不能将0写为1.
9 z6 ?, l" v% D  O3 Y: K7 t: ?" T1 f4 Z8 ^( i

; P! @! a7 s% r! H# M写入数据& E% h# a) o- N: c
  1. /************按页写入数据,但写入之前要进行擦除***********/
    - Y6 i0 Y. J; d& S) q
  2. void FLASH_PageWrite(uint32_t addr , uint8_t* pBuffer ,uint8_t size)4 N+ V: A2 F& ^' W; e' u' t
  3. {
    & W  B8 z# ]1 I6 |, B
  4.         /* 开始的时候要发送写使能信号 */- a& ~/ I% F5 W0 l
  5.         FLASH_WriteEnable();: {' m" z. p6 t* R& F' E
  6.         SPI_NSS_Begin();
    ) ~7 V7 ^4 f, u+ N! H* h+ Y& U" a
  7.         /* 发送页写入命令 */
    ; O7 S7 L) @( T% \
  8.         SPI_SendData(W25X_PageProgram);
    - g/ p7 d2 l; a" g7 V2 j
  9.         /* 发送写入的地址,高位先行 */# O$ P; _  ]- g( ?+ x  |
  10.         SPI_SendData((addr & 0xff0000) >> 16);7 D+ y6 D5 j" ]8 ^' l  Q1 b* W1 f
  11.         SPI_SendData((addr & 0xff00) >> 8);
      f7 t. G, ?9 B( S  H
  12.         SPI_SendData(addr & 0xff);& j- B+ ]$ t) p/ I6 G. V1 G) S
  13.         /* 逐位发送数据 */
    % E. z& O4 @9 `( x, k
  14.         while(size--): k/ Y+ A  B1 Y5 E, Q; I, U0 @9 _
  15.         {7 k3 g- _, `. Y- \7 I
  16.                 SPI_SendData(*pBuffer);, {; s' }  g0 V7 g4 T* T; Y' \
  17.                 pBuffer++;
    * n. _8 I5 Y- L. f" j
  18.         }
      F! H: F+ q: G0 R, o1 X8 |1 L
  19.         SPI_NSS_Stop();
    " C' I9 F: }' M9 Z
  20.         /* 最后也要等待FLASH处理完这次的信号再退出 */
      z$ ?4 j; e1 r! o
  21.         FLASH_WaitBusy();5 x' i  E, {( D2 ^5 ]0 A! M! T
  22. }
    1 ]" u2 ^8 a2 k1 P

  23. * n; w, v2 S7 E: \: u
复制代码

  c+ z+ n. y% g在执行写入数据的时候函数的参数有三部分:
% n9 A5 ~; u, ?  ^3 x1.要写入的地址: L  \5 w, p1 s' A) Y7 o8 z' ~
2.要写入数据的首地址
' O5 z7 ?. U2 F4 k2 E2 L3.要写入数据的大小
3 V% ~, d. c& B8 l' W1 ]4 ?% j函数在执行的过程中,首先发送一个写使能信号,然后发送写数据指令,紧接着发送数据要写入的地址,然后就是逐位发送数据了,函数最后等FLASH处理完这次操作再退出。
# w- V* Z8 c2 v. F* {1 h1 X7 P2 l9 h2 o9 `5 k
# U4 w) [$ T% g3 J9 I3 q
读取数据9 T! E1 }: |8 B6 B$ R) M% m8 l
  1. /**********************读取指定地址、指定长度的数据******************/
    6 y7 b. _& y& l) ^+ f5 Y9 j5 G
  2. /* 因为读取在了指针中,所以不需要返回值 */5 G) q5 C# E" I- t8 i& S0 k
  3. void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size)/ h. ?$ l6 [6 m
  4. {" G5 `5 ^; c- J) s; f! w( N, b
  5.         SPI_NSS_Begin();4 |. q' L, W9 a# @1 ]$ }
  6.         /* 发送读取命令 */
    ! r/ v  T5 i. b7 X  z9 T% Z
  7.         SPI_SendData(W25X_ReadData);. o& l9 Q4 s0 ^
  8.         /* 发送读取数据的地址,高位先行 */
    & j9 W, F: d7 J) L
  9.         SPI_SendData((addr & 0xff0000) >> 16);' r8 `: w1 [4 T2 E5 ]
  10.         SPI_SendData((addr & 0xff00) >> 8);
    4 z2 @7 c3 T9 n# D& i8 F
  11.         SPI_SendData(addr & 0xff);* z  h3 ^9 N9 M- `
  12.         /* 逐位读取数据到指针上 */
    6 G4 b8 T8 o7 |, F
  13.         while(size--)
    ( n- y! v7 A' e! o2 ]. t* u: `0 K
  14.         {
    ' J& o2 Q' d  X% ]- d0 ]$ B
  15.                 *pBuffer=SPI_SendData(Dummy_Byte);5 {$ d- t( x) _! g  a7 M, B+ L4 O
  16.                 pBuffer++;  b' y! r4 ]2 n. e: h0 W5 M
  17.         }9 p, o7 V, C  z4 L; Y
  18.         SPI_NSS_Stop();
    9 h5 N2 o4 x* ~; S# g6 }1 e
  19. }" l& V5 x5 M& a3 m

  20. 7 \3 }" ]! x; U1 s$ `# ~3 A5 i& O
复制代码
1 X* W' _. m6 o# S* S, P$ J
在执行读出数据的时候函数的参数也有三部分:
; d  `+ t$ D" ]0 j, A) C1.要读出的地址: X: {5 \2 }- P# s; u
2.读出到指定地址
# t/ N. a. Y, T# [3.读出数据的大小
$ f' k, g6 y" w/ ]函数执行过程,首先发送读取指令(这时就不用发送写使能了),然后读取数据的地址,然后将数据逐位读取在固定地址中(地址最好是全局变量),使用时再从全局变量地址中获取数据。
# l$ G- }# _9 a0 A5 U/ d* X2 q这里涉及到函数的返回值问题,具体分析链接:返回多个变量怎么办
; X& C5 u" u+ i9 l. r
6 F" H1 z5 k6 ^9 L* i2 }; ~2 E" Y9 V8 \# W
有一个问题当时困扰了博主一天,那就是发送和读取数据时,怎么把数据返回到主函数中,解决方法是,创建俩个全局变量数组,一个负责发送数据、另一个负责接收数据,这样就ok了
8 G8 c2 S! w$ Y- Z# R0 B. T4 u! a附上主函数6 l" m4 Y; J+ B2 O2 [$ g' C
  1. /**********************读取指定地址、指定长度的数据******************/
    ; H5 g. s! j+ F% d, [
  2. /* 因为读取在了指针中,所以不需要返回值 */# B; f- F% W* Z' V, `
  3. void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size)
    2 j3 k) v. t% @. {7 g- q  F! l
  4. {4 w  R+ x0 s& B5 `' y4 ]* M
  5.         SPI_NSS_Begin();6 l' U. q6 }# I
  6.         /* 发送读取命令 */  k) p7 ]- m$ h# b/ L0 `# p: t
  7.         SPI_SendData(W25X_ReadData);
    8 D! Z4 G) B/ d4 n$ ]' q8 {
  8.         /* 发送读取数据的地址,高位先行 */: _- ^6 e0 i  s7 T6 B0 e5 |" P) E
  9.         SPI_SendData((addr & 0xff0000) >> 16);
    $ c0 a* w: ~) g) d/ X8 ?
  10.         SPI_SendData((addr & 0xff00) >> 8);
    " o2 v* o9 Z9 Y. g
  11.         SPI_SendData(addr & 0xff);- Q2 @! x7 n; Z
  12.         /* 逐位读取数据到指针上 */2 s( F" U- i% W  F  ^
  13.         while(size--)3 A& q7 _( J" l! w
  14.         {
    5 N; N0 g9 e% C/ o9 J3 V3 M
  15.                 *pBuffer=SPI_SendData(Dummy_Byte);
    : U$ @: ^/ S) u. m# G, q" s
  16.                 pBuffer++;& C8 b2 a  m$ C, [: p# q
  17.         }0 Z$ y; |# W9 S6 ^3 M5 e
  18.         SPI_NSS_Stop();% w3 I8 j" i9 G
  19. }0 d9 F  i9 Y/ [, U. C( P
  20. 9 m4 q( E& m& D/ x( X
复制代码
' M6 K4 Y& m& G# k% @9 w9 R8 C, y
————————————————
0 ?- i$ \5 P. R( d; {. {4 p2 ]# a# _版权声明:Aspirant-GQ
/ ?& T  s$ ]+ i9 C& j( A4 e$ z: o; S1 l" b! y  S0 I
如有侵权请联系删除
( Q1 ?. f( ]$ u& r: v
  G0 B) D, l; y  V1 z5 O( i( ^% _; y+ F3 \1 ^$ C
' S) q) Y% K- Y+ x! D6 V0 G" S1 O
收藏 评论0 发布时间:2023-3-18 15:00

举报

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