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

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

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

[复制链接]
攻城狮Melo 发布时间:2023-3-18 15:00
FLASH简介- `9 H9 ^5 W5 N
FLASH俗称闪存,和EEPROM一样,都是掉电数据不丢失的非易失行存储器,但FLASH的存储容量普遍大于EEPROM,现在像如U盘、SD卡、SSD固态硬盘以及STM32芯片内部存储程序的设备都是FLASH类型的存储器。由此可见FLASH对于我们学习和工作的重要性,EEPROM可以实现单字节的擦写,而FLASH都是一大片的擦写,就像是大规模杀伤性武器,其最小擦除单:扇区的大小也是4KB。; e: C/ |) i2 x7 K( E6 u; d
我们此次通过SPI对FLASH存储芯片W25Q64进行读写擦除的操作。% s2 ], Q/ Y& x
( l. z% o  K5 \8 }- L. `
对于FLASH内部结构的详细说明博主会专门整理一篇博客来说明,所以关于FLASH芯片的相关原理,本文中只做简单说明,侧重代码部分。' I9 r8 H' I6 ^: c
FLASH详细说明的博客链接:(没有链接就说明还没有整理出)9 m/ a7 ?. Y9 Y

8 q0 M" r3 j; ?W25Q64
% T' K2 q! {. r  Q  EW25Q64简介

/ V; R; ?  g+ F就长这么个样子# K, [' u$ H5 `% ]& N9 {

# \! M( @2 j& D  D! _
20190814195953540.png % I% L! x% i8 k# P. Z
/ ?' S: V& R2 v* E3 S
STM32内部原理图如下:, N' T+ s- b0 A: N: _$ E* }
# X) c1 ]. R9 T8 ^9 I# ?$ b
20190814200413370.png 6 a1 r: E# q# Y% n7 x

# x4 J& g% p5 X7 k+ D) P% R9 Q

4 e; a( |' L* I0 a- k4 a: AW25Q64是一种使用SPI通信协议的NOR FLASH存储器,它 的CS/CLK/DIO/DO 引 脚 分 别 连 接 到 了 STM32 对 应 的 SPI 引 脚NSS/SCK/MOSI/MISO 上,其中 STM32 的 NSS 引脚是一个普通的 GPIO,不是 SPI 的专用NSS 引脚,所以程序中我们要使用软件控制的方式。FLASH 芯片中还有 WP 和 HOLD 引脚。 WP 引脚可控制写保护功能,当该引脚为低电平时,禁止写入数据。我们直接接电源,不使用写保护功能。 HOLD 引脚可用于暂停通讯,该引脚为低电平时,通讯暂停,数据输出引脚输出高阻抗状态,时钟和数据输入引脚无效。我们直接接电源,不使用通讯暂停功能。
2 @" ^$ e0 @9 N/ q6 o! G* O( E5 F. i" l3 h  O: o
W25Q64支持SPI通讯的模式0和模式3
! q* E7 O, D! C- w4 C" m8 U
$ O! ~. }- M! }/ c! c0 ~
# K  G( T1 u( l8 _% k' w+ d
FLASH控制指令
7 u! ^( R. @/ p& ~  o$ `$ B, UFLASH芯片中规定了许多指令,只要SPI向FLASH发送相应的指令,FLASH就会执行相应的操作,所以我们对FLASH的一切操作都是基于这个指令集的,接下来介绍一下FLASH的控制指令:: b4 l3 O7 f% x- B  b
+ P, B% F* H( x" V3 g% j, t6 v- ~
20190814200912265.png 3 b0 y% o2 i8 I/ R
4 K4 L* @5 W. \" L( q" T% s2 {. ?
表中第一列为指令名,第二列为相应的指令代码,第三列及后面的内容根据指令的不同而意义不同,其中带括号的字节参数,方向为 FLASH 向主机传输,即命令响应,不带括号的则为主机向 FLASH 传输。表中“A0~A23” 指 FLASH 芯片内部存储器组织的地址; “M0~M7” 为厂商号( MANUFACTURER ID); “ID0-ID15”为 FLASH 芯片的ID;“dummy”指该处可为任意数据;“D0~D7” 为 FLASH 内部存储矩阵的内容。
+ C) }5 Q0 ]4 H3 B9 u# c5 M看起来很复杂的样子,其实只要在需要执行相应操作时来查这个表,只要能够理解这些指令的使用方法,FLASH就算学会了。
. `9 x1 R" K3 f3 v* ^6 ^- ]2 {# T7 F
例如:要知道FLASH的ID,那就在指令中找对应的取ID指令“JEDEC ID”,仔细解读这个指令
" {0 T  W. m" u6 [
20190814201942797.png
! c3 Y& [0 `1 s! w8 q/ [
  P1 q3 d' L& W7 o, C
可以看出对应的指令代码为“9F”,后面的三个字节带括号,代表这三个字节就是FLASH向STM32发送的数据,即这三个字节就是FLASH的ID,然后使用SPI进行读取就可以了。" M8 ~, _: r$ h' }/ C3 s

& S% G% o7 W4 K, r" o  R; G我们一般是将这些指令宏定义在头文件中,便于使用:2 C8 }, \# u% p
  1. #define W25X_WriteEnable                      0x06 : R2 D6 {1 c9 F6 L: s6 W/ I
  2. #define W25X_WriteDisable                      0x04 " d+ N* N8 U9 r* j; w$ w
  3. #define W25X_ReadStatusReg                      0x05
    , j3 O- q# C4 t9 O
  4. #define W25X_WriteStatusReg                      0x01 9 W, ~7 v7 n8 I
  5. #define W25X_ReadData                              0x03 & \$ h* X) q# J' K' E# L
  6. #define W25X_FastReadData                      0x0B 9 u6 `- o( \# }$ T1 O( k
  7. #define W25X_FastReadDual                      0x3B
    & d7 @- ]7 {$ P& X; }2 Q7 m
  8. #define W25X_PageProgram                      0x02 # ]) Q, y& j: V$ \' R: t. T0 u
  9. #define W25X_BlockErase                              0xD8
    & D( e2 l1 x8 I
  10. #define W25X_SectorErase                      0x20
    ) ], q2 ~8 ~' ^7 Y  V) V$ j
  11. #define W25X_ChipErase                              0xC7 ) i% W  r2 f/ X$ g6 ]0 q4 p0 G" b
  12. #define W25X_PowerDown                              0xB9
    % O* H3 Z$ Q8 m1 ~
  13. #define W25X_ReleasePowerDown              0xAB
      H: T* Y' o+ u" q. `
  14. #define W25X_DeviceID                              0xAB
    + L) ^! @+ ^3 h9 ?5 C
  15. #define W25X_ManufactDeviceID         0x90
    % ]' L# e& k6 z/ K( x( O
  16. #define W25X_JedecDeviceID                      0x9F
    3 F& \( b1 D) t& w
  17. 8 v- T+ E! u: q* q' R
复制代码
% k. E; C8 A9 ]! `8 C& ?$ z
FLASH内部存储结构
- l. Y( X6 F, Z0 l/ iFLASH的存储矩阵如图:其内存分为128块,每一块都有16个扇区,每个扇区大小为4KB,擦除数据的时候是以扇区为基本单位的。
- {  W- K- K/ S6 }& a& f, u* `) o! a0 r
20190814203311343.png ; ]% i- m$ n1 y; @- b6 j

. W! V# Q6 l' E* r0 h% Q- M. Z) `代码讲解
' {0 P: a/ h1 Z9 x, H3 j代码都是博主亲手写出来的,可以运行。1 V6 t6 N2 B8 S3 J5 k: W
代码部分会用到SPI的代码,关于SPI的说明之前整理过:SPI详解) @, J9 F( j( W+ ?* c5 j
% s0 c: h" l% S$ L
读取芯片ID" }# g" Z  f/ _0 f
  1. /************************读取芯片ID*************************************/! V. Q* Y# J( Y& X3 ~
  2. uint32_t FLASH_ReadID(void), ~( o# Z3 a& B/ s! O% X9 |+ p9 v
  3. {+ `. B! h' J# i; T5 i* ]
  4.         uint32_t temp,temp1,temp2,temp3;* T8 h% u0 _! N: B; k
  5.         SPI_NSS_Begin();
    / m/ u( E2 I( L/ c7 ?% F
  6.         /* 发送取ID指令 */0 r+ B0 o# E$ ]
  7.         SPI_SendData( W25X_JedecDeviceID);8 F3 f  s/ r3 R: q0 U$ _
  8.         /* FLASH会连续发送三个字节数据 */
    ; b2 c1 h2 j3 j. Y
  9.         temp1=SPI_SendData(Dummy_Byte);
    ! u" }+ ?7 ]6 q1 j- O
  10.         temp2=SPI_SendData(Dummy_Byte);$ v: i( _) Z, Z: q1 P3 S
  11.         temp3=SPI_SendData(Dummy_Byte);, ^0 J; a, v8 o" y- q  @2 V  e2 @5 T
  12.         SPI_NSS_Stop();/ E" K- R/ |) E
  13.         /* 高为先行,将三个字节整理在一起 */6 b0 S6 d% `; n1 D  Q/ w
  14.         temp=temp1<<16 | temp2<<8 | temp3;# H4 t9 {7 @. f0 l$ w5 s
  15.         return temp;
    & z: I) W# m4 L# Y6 u9 _
  16. }
    ; k" s* \7 L6 `( j$ T
复制代码
5 V8 c7 i9 \; H. E9 n' _  F
由于SPI是全双工通信,所以接收数据和发送数据用的是同一个函数
- F+ {: ^- d' a3 Q- Q! L1 g

& d5 w7 J3 W  u) i! _, Q, i发送写使能信号
( _1 ~2 ~; k5 W2 E
  1. /****************发送写使能信号****************************/
    8 k! N: L+ w) T
  2. void FLASH_WriteEnable(void)5 |/ {0 C2 I6 m2 w* `  d8 o, W
  3. {
    9 a  f# O  r4 V# i! h/ h
  4.         SPI_NSS_Begin();
    ' p( T# ?- q  y9 y9 w  M
  5.         /* 发送相应的指令代码 */
    ' w+ I! ?, E% P, D- y
  6.         SPI_SendData(W25X_WriteEnable);9 A; j- G# p9 {0 k# `
  7.         SPI_NSS_Stop();  s4 h! I/ F' D* r7 T
  8. }
    ) B( ^# n2 }1 u, z! O* T# |/ }
复制代码
& @2 V) G! U6 J7 g* H5 R  H3 f
在向FLASH中执行写操作之前,都要进行写使能操作,通过发送写使能指令到FLASH来实现% z) ^! [9 L8 `" A4 ^: [8 ?
' H0 T0 o7 m7 Z1 _. [2 k- r
等待FLASH不忙- ~/ H" Q& j- R  R+ O4 ^  ~& U+ {9 M8 W
  1. /******************等待,直到不忙***********************************/  s% t+ n1 c8 }, L9 l" G( }. P% h
  2. void FLASH_WaitBusy(void)) ~0 i9 d0 l: C. Q' j
  3. {- C+ \: e1 e8 t4 ]( t& [
  4.         uint8_t StatusReg=0x01;
    * A2 X! J: r9 ?% s
  5.         SPI_NSS_Begin();
    + H1 X/ D# [/ u7 u* b6 A
  6.    /* 读取状态寄存器中的数据,判断忙标志0x01位 置位代表忙 */
    0 k" M8 q" J  x. F7 K
  7.         SPI_SendData(W25X_ReadStatusReg);      , O0 e4 G% v* p0 ~; M- D1 P$ D
  8.    /* 只读取状态寄存器的BUSY位,即第一位 */
    * A; r" J4 I$ k# p; ]1 W$ V
  9.         while((StatusReg & 0x01) == 1) 2 v- ?; d% s7 a$ S: ?# R; a
  10.                 StatusReg=SPI_SendData(Dummy_Byte);
    ) r4 E! C6 i' @5 r9 F. e
  11.         SPI_NSS_Stop();7 N; |7 V1 m  o' ]3 h% N
  12. }
    * [/ C! S* E5 p% J1 y

  13. 4 f( t: C4 z! O! @0 e
  14. ) I, ^# I& Q9 t5 }7 t1 c! y! `
复制代码

3 P' z2 q  h; j' r. nFLASH在通讯的过程中需要一定的时间来执行操作,在这期间,传输数据是无效的,因为FLASH忙着呢,所以我们就要有一个函数来专门等!等到FLASH不忙了,再进行通讯,那怎么等呢?FLASH不忙了会给出一个信号——将状态寄存器的BUSY位重置(也就是0),所以我们需要不断的来检测状态寄存器中的BUSY位是否置位,利用读取寄存器状态的指令来获取状态寄存器当下的状态,然后根据寄存器的BUSY位(第1位)来判断FLASH是否处于忙碌状态。
! D4 o% w% t# a4 C简单来说,这就是个延时函数,延时直到FLASH空闲,可以进行下一步传输。) g8 _' a% s7 k( ~* {. l" n
) f' j  c3 a9 ~, u9 z) O  n
: M+ F9 P9 D2 ~: h
擦除扇区
2 ]' H% B! W) e$ B
  1. /******擦除扇区的内容,切记地址要对其到4kB,每个扇区的大小都是4KB********/4 o$ r$ T* U8 S/ v8 u% _/ ]$ ~
  2. void FLASH_SectorErase(uint32_t addr)3 n, e7 D/ b/ f8 O  m' `0 A( a
  3. {
    # W. c# I3 H3 J# t8 e3 \3 M
  4.         /* 开始的时候要发送写使能信号*/
    ) ~6 s9 }* i2 y6 f4 t
  5.         FLASH_WriteEnable();4 p8 l3 b: y5 A$ ?. y9 g. c
  6.         SPI_NSS_Begin();- ]; `  x( X0 j& o
  7.         /* 发送扇区擦除命令 */
    & |9 t& L# N* r
  8.         SPI_SendData(W25X_SectorErase);
      M- l; a& W! I4 q
  9.         /* 发送扇区的地址,高位先行 */
    2 x  z. K; C0 h) `* `0 J& Y
  10.         SPI_SendData((addr & 0xff0000) >> 16);+ n+ I' g* q1 ~' u# Q7 d* t2 n' F
  11.         SPI_SendData((addr & 0xff00) >> 8);
    : t* L9 `  `( j- x" M  u
  12.         SPI_SendData(addr & 0xff);
    * F/ r4 E, q  d) s
  13.         SPI_NSS_Stop();9 t) a" P0 Y/ ^
  14.         /* 最后也要等待FLASH处理完这次的信号再退出 */$ t2 n) F8 z9 V, ^4 S( c+ V
  15.         FLASH_WaitBusy();
    4 ]" Q2 I- r; j2 M. w5 ]( a
  16. }
      |3 y1 a' g) A! F
  17. 0 p% x  ]" ~; O! r7 f
复制代码

& C4 J" U7 B: s扇区的擦除之前要发送一个写使能信号,先发送擦除指令,然后发送要擦除扇区的地址(分三个字节发出去),高位先行。
  n1 `' Z/ U: P4 h# q5 T扇区上的内容不是1就是0,擦除的过程就是写1的过程(将一个扇区全部写1),因为在写入数据的时候,可以将1写为0,但不能将0写为1.# |% {; A  V4 x& j! [+ q

/ C) p' t4 S. ?8 Y. y
0 a, x& L1 E  Y* X  a9 s' b
写入数据, }$ Y  s- o9 W+ C
  1. /************按页写入数据,但写入之前要进行擦除***********/
    " ]" v6 w& [/ u
  2. void FLASH_PageWrite(uint32_t addr , uint8_t* pBuffer ,uint8_t size)
    1 ?$ |; L* }0 d0 K( @
  3. {  w1 `1 a7 P: O( T
  4.         /* 开始的时候要发送写使能信号 */
    . d$ a$ c: m8 U+ d
  5.         FLASH_WriteEnable();- ?1 v- ?; m( D4 q0 g8 |
  6.         SPI_NSS_Begin();* L4 W% u2 R7 k5 P$ n4 B$ V
  7.         /* 发送页写入命令 */0 c: T; ^1 N" Y/ T
  8.         SPI_SendData(W25X_PageProgram);
    & x. i% c, l9 j- `  a! p$ C
  9.         /* 发送写入的地址,高位先行 */
    3 ~! o& w4 C# c
  10.         SPI_SendData((addr & 0xff0000) >> 16);
    - I( r- [* h+ H* g
  11.         SPI_SendData((addr & 0xff00) >> 8);
    8 g9 \! U" y7 ]7 z
  12.         SPI_SendData(addr & 0xff);# I" K7 F# U; t4 G( s5 `
  13.         /* 逐位发送数据 */
    * Y1 f% ]! v( `" X
  14.         while(size--)7 T0 o1 J8 E% F) K
  15.         {  e5 N6 u% a; ]8 |; ~
  16.                 SPI_SendData(*pBuffer);
    & B* a: e0 H- U3 B7 k; e. S7 u! n
  17.                 pBuffer++;
    # L- F3 q* l5 I, Z; r
  18.         }8 S6 t( {# d- o+ j1 e8 \
  19.         SPI_NSS_Stop();  V3 {) g1 Z+ n+ }, X
  20.         /* 最后也要等待FLASH处理完这次的信号再退出 */
    # ^. x( n3 z/ C- U' F, K
  21.         FLASH_WaitBusy();
    9 E- w: r3 k$ I) m$ H6 m: c
  22. }
    2 t' b8 d9 p$ h
  23. # t2 E( O8 g% H+ J1 |
复制代码

/ U, t% W9 K) ]& n, [+ B2 u* f在执行写入数据的时候函数的参数有三部分:
) }% [* C, S7 F9 V. c& M6 B  ^1.要写入的地址+ W" R0 h+ f3 I1 R4 D
2.要写入数据的首地址
" Z: Q8 w& G* u0 u+ P$ n0 ~: B3.要写入数据的大小: c2 P5 ^2 c/ [# Y" g
函数在执行的过程中,首先发送一个写使能信号,然后发送写数据指令,紧接着发送数据要写入的地址,然后就是逐位发送数据了,函数最后等FLASH处理完这次操作再退出。" [& U4 f8 h0 _4 u, C; E7 s
1 A; Z0 r3 r: P/ \! {: N! k

; @- w+ B, |. t读取数据
7 r. i7 J1 j7 r, y2 h$ K
  1. /**********************读取指定地址、指定长度的数据******************/
    3 T" M$ u, t7 }3 K( |
  2. /* 因为读取在了指针中,所以不需要返回值 */
    * e3 S. K9 A- {" U" D
  3. void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size)3 R6 E" f" f8 R" N% a1 ^
  4. {" d! D8 g8 z  N. z3 `) ~: I
  5.         SPI_NSS_Begin();
    1 q: ?6 g" t3 |7 b6 F+ T% p" A. o6 r5 N
  6.         /* 发送读取命令 */
    ; _, R- p# n$ j5 q: J9 N6 _6 o
  7.         SPI_SendData(W25X_ReadData);/ R  d' @6 g. G5 L% y& d2 l
  8.         /* 发送读取数据的地址,高位先行 */
    ) s4 J# \+ G: Y8 o- x
  9.         SPI_SendData((addr & 0xff0000) >> 16);" G- \: N/ R3 D3 A
  10.         SPI_SendData((addr & 0xff00) >> 8);! k3 |% t4 n$ q/ e
  11.         SPI_SendData(addr & 0xff);  x1 w; F7 P3 p2 P9 X8 U- r
  12.         /* 逐位读取数据到指针上 */, |1 A9 R% p' P" \, k& m2 ^; T
  13.         while(size--)
    % w  C/ Q+ U$ k7 R; c8 c
  14.         {
    $ x" }8 i' j0 u2 p: l
  15.                 *pBuffer=SPI_SendData(Dummy_Byte);
    $ J. w+ q8 M. i% S! ?3 C# B/ ]
  16.                 pBuffer++;) h" n; d/ l% b$ N
  17.         }
    1 O4 a8 U, o& \" q+ q
  18.         SPI_NSS_Stop();5 z1 r6 j$ W1 @7 i4 L
  19. }
    ) J' \6 U4 I1 }: N& [1 A

  20. 0 D- Q4 j/ l4 }2 E. L
复制代码

- o3 {- V, I1 `: Z5 {在执行读出数据的时候函数的参数也有三部分:
7 ?+ G5 h5 o. t! I4 x1.要读出的地址, c9 t2 a0 v# s/ r
2.读出到指定地址- ?% p5 W+ ^" y  N
3.读出数据的大小7 [& O3 V( ]! Q/ w! h! V# l" ^
函数执行过程,首先发送读取指令(这时就不用发送写使能了),然后读取数据的地址,然后将数据逐位读取在固定地址中(地址最好是全局变量),使用时再从全局变量地址中获取数据。- |$ r) `" C2 B
这里涉及到函数的返回值问题,具体分析链接:返回多个变量怎么办
0 f/ u9 q7 b, b+ s
9 y9 b- Q& @6 B
5 O; ]* ]+ V5 u1 T& x有一个问题当时困扰了博主一天,那就是发送和读取数据时,怎么把数据返回到主函数中,解决方法是,创建俩个全局变量数组,一个负责发送数据、另一个负责接收数据,这样就ok了
) B& ^+ N+ L# H9 X; `* v' `附上主函数
  A7 x, l" Z' l5 z
  1. /**********************读取指定地址、指定长度的数据******************/3 C, @: \& U* ~
  2. /* 因为读取在了指针中,所以不需要返回值 */
    4 [2 _4 n: ?& k0 A
  3. void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size)
    , c) Q4 ]- M* ?$ P9 A! c7 |
  4. {
      h6 s) m( G0 }/ }: R- {0 C
  5.         SPI_NSS_Begin();
    8 m* p; b$ `( W# B. T7 z) Y, g2 v
  6.         /* 发送读取命令 */. e5 ]5 ^# [8 v8 \, s' @
  7.         SPI_SendData(W25X_ReadData);2 c0 q& X, q0 Z6 E/ R7 f& |
  8.         /* 发送读取数据的地址,高位先行 *// K* q+ Q5 X9 D( V5 t: v' L3 Y& t
  9.         SPI_SendData((addr & 0xff0000) >> 16);
    0 F& F; A* Y7 B9 P1 D0 i" Y
  10.         SPI_SendData((addr & 0xff00) >> 8);
    6 o3 J4 ?& N0 A; P% I
  11.         SPI_SendData(addr & 0xff);' ]9 V3 |2 d/ ~2 ]/ V" w
  12.         /* 逐位读取数据到指针上 */: O3 w7 y, c7 o9 `- z
  13.         while(size--)% \  _: }7 e+ O6 P" l- c
  14.         {
    ' k" m- {! l( _% E3 q5 N4 |0 [
  15.                 *pBuffer=SPI_SendData(Dummy_Byte);. @! ?- e+ ]# S/ V9 {
  16.                 pBuffer++;
    , r& \+ h: Y+ m" s& B% H% l; ?' g
  17.         }) |4 H/ e1 T1 l! h$ r& o  H) W  M
  18.         SPI_NSS_Stop();
    " A2 R& V0 A1 @9 J* L
  19. }& g2 R# n2 Y, q
  20. . i( Q* W) a8 ?/ \! ~
复制代码

3 M' Z  e# g0 @& z5 D" ~8 u————————————————
9 p2 c% g. E' B1 _) q  U版权声明:Aspirant-GQ
  t" M' l" m2 M/ W
6 y9 K8 R* T6 u. g) ]0 c) Z如有侵权请联系删除9 G/ {7 N% o  R. ~5 z9 u8 j
, H- F  k$ y6 s  S  D5 H
4 ~8 F: d5 S: Z: r; E

8 y8 ^+ U. k. K0 Z0 Y" X# @
收藏 评论0 发布时间:2023-3-18 15:00

举报

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