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
% 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: `
/ m/ K# D. h# g! v3 [- h. F5 B$ |
+ h O0 a0 G1 R# ]- _9 v7 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, LW25Q64支持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 _
+ 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
$ @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% \
- #define W25X_WriteEnable 0x06
8 I* Y5 u" k, f - #define W25X_WriteDisable 0x04 9 S7 i6 w: t# D. r9 Z! k% E9 r
- #define W25X_ReadStatusReg 0x05
% }- r! C H% C - #define W25X_WriteStatusReg 0x01 : l4 A9 K; Q: _% y
- #define W25X_ReadData 0x03 8 L. x% A; L) W( @# W; m
- #define W25X_FastReadData 0x0B
" c9 Y- ~0 y+ Y" {. @3 V - #define W25X_FastReadDual 0x3B * N5 A# l& {8 \! H0 t4 k
- #define W25X_PageProgram 0x02
: y4 u9 N+ z+ R2 F2 u8 K - #define W25X_BlockErase 0xD8
* H; @: U- r, z7 }' ? - #define W25X_SectorErase 0x20 % @/ D$ ~8 S8 d9 _1 k
- #define W25X_ChipErase 0xC7 . R. ]; q5 C3 I( g
- #define W25X_PowerDown 0xB9
. y4 [* _$ H8 z5 p$ O" a7 B: k - #define W25X_ReleasePowerDown 0xAB
1 C1 |! j* B/ i4 ]) R - #define W25X_DeviceID 0xAB
D6 Z# c H; A. t2 Y6 x - #define W25X_ManufactDeviceID 0x90
5 a4 j4 e1 ]6 X/ Y - #define W25X_JedecDeviceID 0x9F$ N1 Q$ t* Y; c- C) v* H
- # 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
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- /************************读取芯片ID*************************************/3 z: `: k: y% q. p0 ]: B8 y8 n
- uint32_t FLASH_ReadID(void)
, I- c' H `# c8 I& u6 m - {
2 F, l: L6 K" X7 T - uint32_t temp,temp1,temp2,temp3;
" @/ w) A U2 ~ - SPI_NSS_Begin();& e" z/ V. u A U& \
- /* 发送取ID指令 */
9 Q4 z3 l, P, j - SPI_SendData( W25X_JedecDeviceID);" E& n# \5 H& e1 Y. J& U$ n' {; R9 t
- /* FLASH会连续发送三个字节数据 */1 @! O9 Q& r7 [- ?* F. ~
- temp1=SPI_SendData(Dummy_Byte);" Z- S# {; c" t/ @
- temp2=SPI_SendData(Dummy_Byte);( j8 Y) U% l; k6 g N( \/ a
- temp3=SPI_SendData(Dummy_Byte);
! d+ ]5 a! @2 D: R; Z3 @ - SPI_NSS_Stop();! G* x+ o j4 V0 e# F+ _
- /* 高为先行,将三个字节整理在一起 */
6 U/ P( {9 [: s: Y! ]# b - temp=temp1<<16 | temp2<<8 | temp3;( E2 W% c% c' ^1 M( E( S. k4 m& r1 B
- return temp;
% `( i2 w9 r" c - }+ 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- /****************发送写使能信号****************************/
. ~( i5 O, }) {1 i: F( ^: @- X - void FLASH_WriteEnable(void)
! T' u4 l, v- p1 d% Z - {
) n7 ?9 t* m2 o- t1 V - SPI_NSS_Begin();+ X4 J, k9 l- O; E7 D; t
- /* 发送相应的指令代码 */
! `% r7 _+ J+ }) d; x3 ]; }1 e3 a( X - SPI_SendData(W25X_WriteEnable);& C5 q( }( y- O2 f) I5 f
- SPI_NSS_Stop();9 [9 i! d' ^* h" D3 E
- }
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- /******************等待,直到不忙***********************************/
! U4 {4 l- h2 d! k - void FLASH_WaitBusy(void)" x; ]' w4 m- z2 H( U
- {
/ M F7 [# ]' H( ` - uint8_t StatusReg=0x01;" j% Y; `- |6 B
- SPI_NSS_Begin();% u1 {- ~3 r1 x" Z$ X* X/ U
- /* 读取状态寄存器中的数据,判断忙标志0x01位 置位代表忙 */
: v& O$ ]' o% E/ B/ \- M2 a* c( G - SPI_SendData(W25X_ReadStatusReg); 5 Q% W2 D% X( E7 y8 b/ D9 B1 ^
- /* 只读取状态寄存器的BUSY位,即第一位 */
1 e% {: \* e% j8 ~ - while((StatusReg & 0x01) == 1)
}" Z* N( Y. h) v - StatusReg=SPI_SendData(Dummy_Byte);
; ?" Y0 W1 V, v, c" U8 f" N) r h( [ s - SPI_NSS_Stop();: R& q( j/ Z4 b- L% f! x
- }- m0 E; L( l/ J: w
$ w7 v% X( U* g8 x9 F0 v1 U- ; _! 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
- /******擦除扇区的内容,切记地址要对其到4kB,每个扇区的大小都是4KB********/
+ o2 [! G4 [" T5 R) h: G - void FLASH_SectorErase(uint32_t addr)
7 i7 p/ }6 V2 S. q2 ^ - {
4 Z) T; _# _3 F- P - /* 开始的时候要发送写使能信号*/
) O+ J+ _+ Y5 k9 L! [# Y# n - FLASH_WriteEnable();2 b$ v0 m$ s: u* M
- SPI_NSS_Begin();
* o: z# Y; P' ]9 I: L, R- T - /* 发送扇区擦除命令 */
5 p. J' I9 i; l2 ]9 y - SPI_SendData(W25X_SectorErase);/ Z3 I4 ~2 W# {* ?
- /* 发送扇区的地址,高位先行 */
$ v4 k) T8 S ` - SPI_SendData((addr & 0xff0000) >> 16);
3 ~7 W0 h F3 T0 z - SPI_SendData((addr & 0xff00) >> 8);( D$ \" P% W% H2 ^
- SPI_SendData(addr & 0xff);
! L5 @' C( ~* Z3 N* z, } - SPI_NSS_Stop();
0 P0 X" y! {. C" N - /* 最后也要等待FLASH处理完这次的信号再退出 */
- Q3 `6 E: V. y - FLASH_WaitBusy();' Y" {) ?' z- |: U, w
- }
, X, O, O0 a6 D# I& D5 F: i3 U - + 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
- /************按页写入数据,但写入之前要进行擦除***********/
; t8 Z8 ?$ B, E- |$ c" i - void FLASH_PageWrite(uint32_t addr , uint8_t* pBuffer ,uint8_t size)
: `, K7 n- [8 t/ T8 i; [/ E - {
: E6 ~7 L0 V X0 h$ J( `: r - /* 开始的时候要发送写使能信号 */- {4 A# p( j* D, n, i
- FLASH_WriteEnable();
9 H8 c, W Q2 g4 Q4 ]% _ - SPI_NSS_Begin();
/ G% W3 R7 v' ^! a - /* 发送页写入命令 */
3 j9 q- ~: l5 u( x i$ j0 k8 X& G - SPI_SendData(W25X_PageProgram);
8 }$ ]! t4 P! n' b6 F8 T - /* 发送写入的地址,高位先行 */
" R3 h) y: H7 |, L - SPI_SendData((addr & 0xff0000) >> 16);
& o$ _4 l: K+ \' g/ ` - SPI_SendData((addr & 0xff00) >> 8);
! \& L5 K/ A9 m0 }3 o - SPI_SendData(addr & 0xff);' h5 m( ~" o& z
- /* 逐位发送数据 */
: P4 O ^3 e7 j2 \# l: C# f# W8 F - while(size--)
! W2 F$ h- I4 S# I& v' L! K - {
* f5 y+ F, J. P, ` - SPI_SendData(*pBuffer);
?: w0 K' b- I - pBuffer++;6 @- N( c R; a) ^
- }
( r' ^ c( O5 H - SPI_NSS_Stop();
% c! L3 B @+ K& o - /* 最后也要等待FLASH处理完这次的信号再退出 */8 v* |. Z7 U0 ?. j; g3 T" q: z3 V
- FLASH_WaitBusy();8 s0 h( p/ t5 D
- }% a) u9 J4 V; g: A) X" t9 C
- 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 ~- /**********************读取指定地址、指定长度的数据******************/8 {8 R- L- s. k- |+ b& z
- /* 因为读取在了指针中,所以不需要返回值 */& v1 g9 a. j# O- g6 z
- void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size). V1 ~, ] ? g) K3 m. d2 k
- {$ q$ x9 H Z- F
- SPI_NSS_Begin();. c3 y7 n8 R: b: N& y/ o2 S
- /* 发送读取命令 */6 [( m% V; A" \% m% X8 m% ]6 `
- SPI_SendData(W25X_ReadData);
: M8 i/ [" E( C! Y - /* 发送读取数据的地址,高位先行 */0 S; ~, V$ E$ ^# |, Q/ z
- SPI_SendData((addr & 0xff0000) >> 16);' n/ `4 V3 q+ x5 |9 x) t
- SPI_SendData((addr & 0xff00) >> 8);
7 `& e1 J* N6 m( |/ Z' Z/ w8 x# c0 ?" f. T - SPI_SendData(addr & 0xff);
1 i& q6 ^) z! F - /* 逐位读取数据到指针上 */
4 }. }) [7 H1 R! y0 N. u - while(size--)- Q& P1 d- {$ a: _
- {
+ u+ ]- \5 b* C/ u - *pBuffer=SPI_SendData(Dummy_Byte);) L7 R# D0 ?8 [- s( U
- pBuffer++; p! G) l' T* J2 w
- }
4 Q+ p3 m. W6 X4 l - SPI_NSS_Stop();6 H! [; a$ y, C" T, J$ i ?8 u$ |
- }
% @# @+ J) o1 C$ J- T4 A- `
! 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 f9 o$ d: P; ~6 c9 b. _$ m - /* 因为读取在了指针中,所以不需要返回值 */! F3 ^2 L3 c' q0 N: |: i# h* o
- void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size)
[' E% t5 V6 p2 r( { - {2 ^0 R1 n7 C* l) k# Y3 X7 F! X
- SPI_NSS_Begin();4 k! [( `1 g- j% i
- /* 发送读取命令 */, ?4 ~: B" Y8 H3 K0 L, o; Q
- SPI_SendData(W25X_ReadData);6 ?& w0 M' q; _
- /* 发送读取数据的地址,高位先行 */
( k! @. t$ `! `. O2 h - SPI_SendData((addr & 0xff0000) >> 16);
6 l z( Q* ?9 ~" H - SPI_SendData((addr & 0xff00) >> 8);6 c6 `4 u9 |9 x6 n
- SPI_SendData(addr & 0xff);
9 Y! D- S4 u- w ^# d* G# P) H2 H( j - /* 逐位读取数据到指针上 */3 Z, c8 ~6 B3 d$ G4 [
- while(size--)
! _* ^/ ^7 m- T) j& |7 H9 W& s% f S - {
& v1 A0 A1 E% o; Z' d - *pBuffer=SPI_SendData(Dummy_Byte);
6 {0 V$ z- _4 { - pBuffer++;$ g1 K$ s& ]% x9 a
- }3 p3 @1 Y1 b) ?: F5 i# x
- SPI_NSS_Stop();
% N# f' p% b" c: q" m - }
4 V# l, c; ^2 K+ n - 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
|