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

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

[复制链接]
攻城狮Melo 发布时间:2023-3-18 15:00
FLASH简介
% T0 `" i( ^% H7 s) i" ^9 [- BFLASH俗称闪存,和EEPROM一样,都是掉电数据不丢失的非易失行存储器,但FLASH的存储容量普遍大于EEPROM,现在像如U盘、SD卡、SSD固态硬盘以及STM32芯片内部存储程序的设备都是FLASH类型的存储器。由此可见FLASH对于我们学习和工作的重要性,EEPROM可以实现单字节的擦写,而FLASH都是一大片的擦写,就像是大规模杀伤性武器,其最小擦除单:扇区的大小也是4KB。
, g3 v! m& K: T& C. |. \我们此次通过SPI对FLASH存储芯片W25Q64进行读写擦除的操作。, I+ O1 D1 b5 h/ {& z. E

% F, H- S9 W& H" E  s对于FLASH内部结构的详细说明博主会专门整理一篇博客来说明,所以关于FLASH芯片的相关原理,本文中只做简单说明,侧重代码部分。
* J0 `- x1 L" j+ SFLASH详细说明的博客链接:(没有链接就说明还没有整理出)
' l) s+ [7 D/ l5 G! E' k, ^( g8 O$ q+ N1 }, H( |
W25Q649 C' V8 x- G& S* s
W25Q64简介
$ `7 l& {. T7 s/ S3 F
就长这么个样子
* r9 S1 t3 ~3 ?! @8 H8 K  U$ _. a+ t8 s. E' Z, F5 F' p
20190814195953540.png
3 ^( O/ J, ?, ~  g2 `
$ Z0 o" X! |2 O3 I! b7 `
STM32内部原理图如下:7 m' m7 S0 A5 [+ X! `/ ^, {) v6 ~

, f& s3 d# R/ T  E7 Y
20190814200413370.png " C: L- |4 j( G

4 C: r9 M5 b: Q1 X
& b4 k8 J, P4 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 引脚可用于暂停通讯,该引脚为低电平时,通讯暂停,数据输出引脚输出高阻抗状态,时钟和数据输入引脚无效。我们直接接电源,不使用通讯暂停功能。
: N2 X1 C7 u( ?5 H( D& H8 ~. p3 k# S% h  ^5 Z3 d
W25Q64支持SPI通讯的模式0和模式3
( u. C4 M5 a4 R2 J6 Y; R3 ?+ _$ O2 Z

) H- S( G* q6 s+ b* J3 {FLASH控制指令0 j$ H2 [  n6 W& l4 s
FLASH芯片中规定了许多指令,只要SPI向FLASH发送相应的指令,FLASH就会执行相应的操作,所以我们对FLASH的一切操作都是基于这个指令集的,接下来介绍一下FLASH的控制指令:
0 r( \% e8 s. |! K
) x$ z- Q# T2 i# z- X! Y
20190814200912265.png
/ y1 u' C* [) F  X9 k6 G: f0 g
* n6 u, I- g9 a/ v( V
表中第一列为指令名,第二列为相应的指令代码,第三列及后面的内容根据指令的不同而意义不同,其中带括号的字节参数,方向为 FLASH 向主机传输,即命令响应,不带括号的则为主机向 FLASH 传输。表中“A0~A23” 指 FLASH 芯片内部存储器组织的地址; “M0~M7” 为厂商号( MANUFACTURER ID); “ID0-ID15”为 FLASH 芯片的ID;“dummy”指该处可为任意数据;“D0~D7” 为 FLASH 内部存储矩阵的内容。
& B! u9 R8 {4 F: i看起来很复杂的样子,其实只要在需要执行相应操作时来查这个表,只要能够理解这些指令的使用方法,FLASH就算学会了。+ \: A& B: D7 E( y/ ^; D

  M. g. |! P' E# i; q6 d5 Z
例如:要知道FLASH的ID,那就在指令中找对应的取ID指令“JEDEC ID”,仔细解读这个指令
$ L! n/ h: \% l2 U* X, L, o: z
20190814201942797.png
- i1 \; H6 A# n: G: K0 q8 G

5 W" x. N8 Y+ E- }- x$ b可以看出对应的指令代码为“9F”,后面的三个字节带括号,代表这三个字节就是FLASH向STM32发送的数据,即这三个字节就是FLASH的ID,然后使用SPI进行读取就可以了。6 Y2 p& p6 K& Z+ K/ |, X; g7 U
/ Q% ~3 X8 a, i4 l! }) U
我们一般是将这些指令宏定义在头文件中,便于使用:
1 O: z! D1 c" B
  1. #define W25X_WriteEnable                      0x06 ! r5 j5 n7 ~7 |/ b' f7 f9 w
  2. #define W25X_WriteDisable                      0x04
    2 D& J7 P( S6 ^" U% Z- Y9 {( n
  3. #define W25X_ReadStatusReg                      0x05 ' D4 n2 @; R) z: s; _0 {
  4. #define W25X_WriteStatusReg                      0x01 % ]4 {3 a; j& w" n4 T1 a% @0 R0 Q
  5. #define W25X_ReadData                              0x03
    8 D4 p7 A; E  K2 b* D6 y
  6. #define W25X_FastReadData                      0x0B - p! P5 }" _" {
  7. #define W25X_FastReadDual                      0x3B ) l3 f% h2 Z5 R$ ]6 T
  8. #define W25X_PageProgram                      0x02 & o6 v! |0 k. G: B5 ?* q" c8 ^
  9. #define W25X_BlockErase                              0xD8
    2 s6 Q9 n$ v( p  h
  10. #define W25X_SectorErase                      0x20
    $ T9 R' ^# c* {$ }/ E* A2 r$ j
  11. #define W25X_ChipErase                              0xC7 7 F" x4 D3 M- X' k5 O: A7 B
  12. #define W25X_PowerDown                              0xB9 9 d& ]+ r+ U+ U9 ]0 Y, h9 J+ Z0 D
  13. #define W25X_ReleasePowerDown              0xAB
    7 E5 }# O" D$ t$ q- o$ [
  14. #define W25X_DeviceID                              0xAB ' z( f* C4 M7 c9 s
  15. #define W25X_ManufactDeviceID         0x90
    ! W5 g, Z, w1 ~( W
  16. #define W25X_JedecDeviceID                      0x9F% Q8 n3 `# a. r# P

  17. ' w6 T( V3 h/ d
复制代码
* ~: e% |) i$ t6 B( R! y
FLASH内部存储结构1 W- B: q4 c/ L
FLASH的存储矩阵如图:其内存分为128块,每一块都有16个扇区,每个扇区大小为4KB,擦除数据的时候是以扇区为基本单位的。8 S: {4 t, K& i9 ?* ]' ?+ R

" a. H7 G# r& U/ |6 t8 `
20190814203311343.png , X, D. a% k& J7 t/ k# v, }! ~2 V9 K0 z
  ^3 p' b7 b& X4 |5 v: \
代码讲解
. y5 E" ?2 s) w0 B代码都是博主亲手写出来的,可以运行。
1 r% H/ {7 x5 H/ k, G* [代码部分会用到SPI的代码,关于SPI的说明之前整理过:SPI详解
- v. F$ f; `( o) a( {" f* {$ k" O
5 b0 h$ O4 E8 w) C7 r5 P4 h
读取芯片ID
: T% j$ J( Y6 ?( J4 j) F$ |
  1. /************************读取芯片ID*************************************/
    . S* b7 F% t$ q+ y( Q8 A5 F8 r5 o1 W
  2. uint32_t FLASH_ReadID(void)
    , T" D4 S# i4 a$ \
  3. {3 D9 B6 i! a% b1 Z. l
  4.         uint32_t temp,temp1,temp2,temp3;. l+ ?# ^& ?- ]/ h* S+ S
  5.         SPI_NSS_Begin();0 r; R7 S6 n3 \, f7 V6 ?  Y0 g7 [
  6.         /* 发送取ID指令 */
    " a1 `4 x# g5 F3 p6 G6 M
  7.         SPI_SendData( W25X_JedecDeviceID);% }7 f( ?4 t9 D1 N7 H6 W; y
  8.         /* FLASH会连续发送三个字节数据 */: p2 P) C" ~2 x1 `
  9.         temp1=SPI_SendData(Dummy_Byte);
    5 y7 ~% Y9 m0 b9 R, k) {; j0 [) s
  10.         temp2=SPI_SendData(Dummy_Byte);
    + }4 I& S& u; m) Q! }
  11.         temp3=SPI_SendData(Dummy_Byte);
    * a7 y  E( Q* ]3 {3 Q
  12.         SPI_NSS_Stop();
    " ^/ ?, q* |: s2 V$ y
  13.         /* 高为先行,将三个字节整理在一起 */* }0 ^5 G4 d% p) q
  14.         temp=temp1<<16 | temp2<<8 | temp3;" G" ^" V/ g- o1 P, ]
  15.         return temp;) L/ d! S$ k5 N" M1 h
  16. }' g: i7 \, \5 z5 i, q$ l% S
复制代码
6 W  {2 _* ]4 ^' D/ G, Y! s
由于SPI是全双工通信,所以接收数据和发送数据用的是同一个函数
, r; Z4 Q0 V4 j, y
) D3 ]- a( {) @
发送写使能信号
9 b+ w" r+ f8 Q
  1. /****************发送写使能信号****************************/9 ~* K" T3 V1 x6 ~6 r2 [
  2. void FLASH_WriteEnable(void)
    " j0 T" h1 `( A& v+ H) W
  3. {; F" @# x+ u% A' S
  4.         SPI_NSS_Begin();
    $ p/ Y, r  `' c" Z, b) y' n: e
  5.         /* 发送相应的指令代码 */
    ( R2 t9 y- n$ b: H# a
  6.         SPI_SendData(W25X_WriteEnable);
    4 x; z( |2 ~  l: B. S9 {+ I
  7.         SPI_NSS_Stop();' f4 [6 i( d& G+ t
  8. }% e* d! ~! d/ o( L& b5 q! o
复制代码

% [. ?% ?0 I/ r在向FLASH中执行写操作之前,都要进行写使能操作,通过发送写使能指令到FLASH来实现/ V: n1 I1 U6 B

* O5 ?' Z. {: [4 D7 U5 I等待FLASH不忙+ W7 T) x, c6 ?8 V% `) H
  1. /******************等待,直到不忙***********************************/
    : }" _$ M' d; d7 e! m
  2. void FLASH_WaitBusy(void)
    # Q% S% B% D( ^5 p# K2 {
  3. {
      n: {  `" j: d0 F- G. \, O0 M
  4.         uint8_t StatusReg=0x01;& s4 e0 S; I( q
  5.         SPI_NSS_Begin();! }' m2 q& I$ }5 k) u+ {, L0 N
  6.    /* 读取状态寄存器中的数据,判断忙标志0x01位 置位代表忙 */7 z% b# `# v% e1 t1 z( \% q: f
  7.         SPI_SendData(W25X_ReadStatusReg);      
    . ], \; |3 h1 p0 ?: i' N3 P
  8.    /* 只读取状态寄存器的BUSY位,即第一位 */- a+ y7 e6 U0 i  t+ a' G  u* n0 J
  9.         while((StatusReg & 0x01) == 1)
    3 g' q' i" i7 x1 b
  10.                 StatusReg=SPI_SendData(Dummy_Byte);   j6 n7 O# C( C; o/ p
  11.         SPI_NSS_Stop();4 `2 s) y4 V! Y# K+ T) ^" B8 E
  12. }
    ( l) k, D: d* X+ i1 g8 V3 o
  13. ( H* x  h9 t5 M3 u% |( ~+ n
  14. % v: k8 X- v) j6 R6 N$ I2 H
复制代码
/ k% @2 S' W( x- X% r7 U
FLASH在通讯的过程中需要一定的时间来执行操作,在这期间,传输数据是无效的,因为FLASH忙着呢,所以我们就要有一个函数来专门等!等到FLASH不忙了,再进行通讯,那怎么等呢?FLASH不忙了会给出一个信号——将状态寄存器的BUSY位重置(也就是0),所以我们需要不断的来检测状态寄存器中的BUSY位是否置位,利用读取寄存器状态的指令来获取状态寄存器当下的状态,然后根据寄存器的BUSY位(第1位)来判断FLASH是否处于忙碌状态。
  q4 i$ T9 z! j# C简单来说,这就是个延时函数,延时直到FLASH空闲,可以进行下一步传输。
0 W  t3 f& r: u" E3 u' H7 \& t# C" i. j( B& R

( A- E" K4 `% j4 `$ p- V擦除扇区
- A( p; ~" G0 M6 `) b
  1. /******擦除扇区的内容,切记地址要对其到4kB,每个扇区的大小都是4KB********/$ F6 O1 w- T/ |& a0 ?7 G% r7 o% @
  2. void FLASH_SectorErase(uint32_t addr)' i5 o9 f( n3 o* M: Z8 r7 J
  3. {
    # M' T3 D8 m( Q4 C; ]/ Q, k
  4.         /* 开始的时候要发送写使能信号*/! s/ G7 K( Z6 [2 `6 ^; H
  5.         FLASH_WriteEnable();, P; ?  p6 c0 Y- s9 G5 e& y
  6.         SPI_NSS_Begin();; J; u# m/ t7 k# k5 S$ @3 @8 a
  7.         /* 发送扇区擦除命令 */, a: @0 c4 ]$ C% y
  8.         SPI_SendData(W25X_SectorErase);: d3 t$ L+ Y. y
  9.         /* 发送扇区的地址,高位先行 */5 j, M6 O2 @2 M
  10.         SPI_SendData((addr & 0xff0000) >> 16);
    1 H  V' f6 n0 z
  11.         SPI_SendData((addr & 0xff00) >> 8);1 m" m* A, b- I3 r; J
  12.         SPI_SendData(addr & 0xff);
    ; v4 Z' G! n) `6 z
  13.         SPI_NSS_Stop();
    $ V+ l0 i( J6 G) s) t3 m
  14.         /* 最后也要等待FLASH处理完这次的信号再退出 */# b" P3 U6 c) p( p, O: O/ D
  15.         FLASH_WaitBusy();! j# `" \5 b1 J4 l) V+ ^3 R
  16. }% X9 P: U; w" z; a3 P
  17. / T; q! g; ~- P' Z
复制代码

. B) Z* ^2 S8 q+ C& ?扇区的擦除之前要发送一个写使能信号,先发送擦除指令,然后发送要擦除扇区的地址(分三个字节发出去),高位先行。
7 T# [$ t* \4 ~  h% C6 G4 r扇区上的内容不是1就是0,擦除的过程就是写1的过程(将一个扇区全部写1),因为在写入数据的时候,可以将1写为0,但不能将0写为1.
6 o# r1 @; V* b# c; t( v, G0 e5 V8 v( Z6 [+ b
- m2 ]  S1 h3 L0 v
写入数据4 t+ I5 Y+ F( ~# l' j6 {
  1. /************按页写入数据,但写入之前要进行擦除***********/
    8 x+ K6 ]; D; X- ?
  2. void FLASH_PageWrite(uint32_t addr , uint8_t* pBuffer ,uint8_t size)
    4 d; e  J) n( J/ l! d( K5 W
  3. {1 }- |/ A5 X1 l" \7 v4 Q4 S: V
  4.         /* 开始的时候要发送写使能信号 */
      }: k) F9 ]+ {- w& K# n' j6 ^
  5.         FLASH_WriteEnable();
    $ G; P& m' T( l. A! i
  6.         SPI_NSS_Begin();! V3 p9 I9 I2 d
  7.         /* 发送页写入命令 */( D2 A2 R, j6 y& S8 ~# s" d
  8.         SPI_SendData(W25X_PageProgram);
    3 h7 S1 X! {' H$ R* \$ S
  9.         /* 发送写入的地址,高位先行 */
    , o  I1 a; ]" z8 d, `
  10.         SPI_SendData((addr & 0xff0000) >> 16);* N& h- ?0 {; E0 c7 c
  11.         SPI_SendData((addr & 0xff00) >> 8);
    ) E& ~1 ^; `& e8 W
  12.         SPI_SendData(addr & 0xff);& F# H2 M+ \/ G, T4 B) l2 P0 x5 m; x
  13.         /* 逐位发送数据 */
    $ D3 S) i; R* S; g
  14.         while(size--). n' |8 e3 q( R; t2 Q
  15.         {4 E" j  i- C. H! e$ }$ }5 J, U' f
  16.                 SPI_SendData(*pBuffer);9 N* M  f( F2 }" m; B0 u
  17.                 pBuffer++;" j% ^6 f" Y  x) ]3 p
  18.         }" g$ C1 Y8 P, y* n2 }& m; J
  19.         SPI_NSS_Stop();
    * H. K& y6 ]8 F1 T/ d
  20.         /* 最后也要等待FLASH处理完这次的信号再退出 */
    4 w( p4 p5 t  _# A- Z
  21.         FLASH_WaitBusy();
    ( V1 i- t1 f) A
  22. }! s9 W/ ?6 {: [* @2 n5 q7 K
  23. 0 Q4 i( i! Z: K: c
复制代码

) [6 U) I8 L  @% v' T在执行写入数据的时候函数的参数有三部分:
4 y; @. x, x3 I; |+ B2 `- h6 b# a1.要写入的地址
! v% A3 z- x' r& b2.要写入数据的首地址
7 a9 B# J5 M- q* y/ |) g3.要写入数据的大小
/ d  h; |% f$ a! d( V函数在执行的过程中,首先发送一个写使能信号,然后发送写数据指令,紧接着发送数据要写入的地址,然后就是逐位发送数据了,函数最后等FLASH处理完这次操作再退出。
+ m! \, c; T6 i! P2 L: O* C! V/ w& \! Y* n  [+ j

3 Z& ]% a+ e9 u% C( R读取数据
" a% h( e  t2 u& Q! p* k+ L0 U  j
  1. /**********************读取指定地址、指定长度的数据******************/
    ; ]2 |9 z# O5 a+ [4 T
  2. /* 因为读取在了指针中,所以不需要返回值 */
    : q" m7 J* b8 l5 O  n
  3. void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size)
    , V& S8 U  A* s/ E6 k
  4. {8 ~/ X4 v) |' O* f
  5.         SPI_NSS_Begin();0 j% I" G# e; n' s1 v/ V8 y6 N
  6.         /* 发送读取命令 */
    , R' A7 V- {# m2 x
  7.         SPI_SendData(W25X_ReadData);
    2 P4 s# B' j" _
  8.         /* 发送读取数据的地址,高位先行 */
    $ s' J3 n- W- e, C0 h2 L' }: |7 w9 M
  9.         SPI_SendData((addr & 0xff0000) >> 16);! j! u8 L. a; X- L) J- W
  10.         SPI_SendData((addr & 0xff00) >> 8);. e' f: x3 G' n1 x
  11.         SPI_SendData(addr & 0xff);
    & c) D1 ^# r3 m  M
  12.         /* 逐位读取数据到指针上 */
    $ I2 p3 i( p& F8 K' {& T
  13.         while(size--)
    ( }/ k( _. @) `/ S) K8 f, l+ ?
  14.         {, H5 X$ X) Z2 ^- X- W) Q1 n
  15.                 *pBuffer=SPI_SendData(Dummy_Byte);
    , Z7 j4 _7 C9 l  P% ^
  16.                 pBuffer++;4 U" r( `) B8 U8 ~1 x3 w1 b# ?
  17.         }
    & ~( b& ]% k2 Q' Q& W
  18.         SPI_NSS_Stop();2 @, s# {6 B/ [" G5 K8 u5 u
  19. }
    + X4 [( Q/ n' g2 @' Z

  20. & @# K! [& g2 ^, t$ u9 p" ^
复制代码

2 I4 Z6 c" ?. q; E; |7 m  |在执行读出数据的时候函数的参数也有三部分:  Y6 O! z7 e0 j5 O+ w/ C
1.要读出的地址
( u; }2 G* [1 f2.读出到指定地址" e8 O! C$ B, s
3.读出数据的大小
- Q$ Z$ ]) ]* c函数执行过程,首先发送读取指令(这时就不用发送写使能了),然后读取数据的地址,然后将数据逐位读取在固定地址中(地址最好是全局变量),使用时再从全局变量地址中获取数据。2 R1 u5 G) H# r& S% I
这里涉及到函数的返回值问题,具体分析链接:返回多个变量怎么办
; W6 \& a( H5 i2 F  h0 c( f  P# r9 h+ k9 a/ o! G$ o
& P' e/ D$ S" _  s7 x
有一个问题当时困扰了博主一天,那就是发送和读取数据时,怎么把数据返回到主函数中,解决方法是,创建俩个全局变量数组,一个负责发送数据、另一个负责接收数据,这样就ok了/ u! y& N  f$ T1 V0 Y. x
附上主函数
% X7 Q3 f" Z  ]3 `- y
  1. /**********************读取指定地址、指定长度的数据******************/& ?1 d/ Q7 l; M. K( ], N6 g
  2. /* 因为读取在了指针中,所以不需要返回值 */
    6 \4 {; N$ t! b' \' R- u' h, [3 `
  3. void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size)
      |7 B" Y; C1 K3 J6 A: C
  4. {, w3 ?) U! m2 {8 J
  5.         SPI_NSS_Begin();8 H; C! K8 z  L/ [( z6 q6 r, q
  6.         /* 发送读取命令 */
    & E5 G" @% A% V& S7 s5 h
  7.         SPI_SendData(W25X_ReadData);
    ! u5 x. C+ K4 a9 i
  8.         /* 发送读取数据的地址,高位先行 */
    * h+ w0 ~' ]0 {! g" h
  9.         SPI_SendData((addr & 0xff0000) >> 16);
    4 e* p3 R$ r. L3 c! p
  10.         SPI_SendData((addr & 0xff00) >> 8);. ?+ Q( V; j* \$ T
  11.         SPI_SendData(addr & 0xff);
    & o) t( D2 N8 b$ P
  12.         /* 逐位读取数据到指针上 */" Z: ?) Q  D" |
  13.         while(size--)
    5 n& s/ m0 F$ _! y# w! a$ M' B
  14.         {
    9 ~. [9 `% [0 g: z; P- d# B
  15.                 *pBuffer=SPI_SendData(Dummy_Byte);) {: X7 x9 v* L
  16.                 pBuffer++;1 c0 V9 u1 `. M7 b
  17.         }
    ) n: S8 }9 F& T; u2 y
  18.         SPI_NSS_Stop();
    : f( n: N' c; X* d8 @
  19. }
    ! ^' v* C, R/ r2 h
  20. ! @5 _5 ]) D" ?; T3 C9 ?
复制代码

! m: N8 M2 K7 h  A9 z9 g' _  t————————————————
) t% r' q5 K; _. z! g, B版权声明:Aspirant-GQ
/ F9 J) f' ^! l( O% u7 U
- ~6 q2 [' }2 g5 L/ {如有侵权请联系删除
! V( W( h' P) B2 a% D. n& i0 A8 n4 w2 A
% H! R0 J6 j( `

* ]1 H6 a& ?2 n, q5 A7 }: {
收藏 评论0 发布时间:2023-3-18 15:00

举报

0个回答

所属标签

相似技术帖

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