FLASH简介. [2 s: I# `; Q3 P/ z) q
FLASH俗称闪存,和EEPROM一样,都是掉电数据不丢失的非易失行存储器,但FLASH的存储容量普遍大于EEPROM,现在像如U盘、SD卡、SSD固态硬盘以及STM32芯片内部存储程序的设备都是FLASH类型的存储器。由此可见FLASH对于我们学习和工作的重要性,EEPROM可以实现单字节的擦写,而FLASH都是一大片的擦写,就像是大规模杀伤性武器,其最小擦除单:扇区的大小也是4KB。+ _( Z) a& @- {8 m+ \8 @
我们此次通过SPI对FLASH存储芯片W25Q64进行读写擦除的操作。* j! p9 |" B; V0 C6 M3 @7 `
; M7 L/ B2 R, f6 ]/ p* l7 M+ |: W
对于FLASH内部结构的详细说明博主会专门整理一篇博客来说明,所以关于FLASH芯片的相关原理,本文中只做简单说明,侧重代码部分。) ]; w6 M' {" a) s- P
FLASH详细说明的博客链接:(没有链接就说明还没有整理出)
8 q! [. ^2 h5 k) W: ` W3 y- t# X7 e) c1 b: u& S8 ]
W25Q64
' C8 I1 m1 s- |! v0 EW25Q64简介
' B' D2 Y7 p9 Z8 m0 R& r- f) r' _$ U就长这么个样子1 V6 g* X1 v7 c" L
2 a* D# t' s& s( |# \6 p
* K7 Y! N% s$ [2 Y5 R
: n8 A, F$ C! o) Y( m9 j3 f
STM32内部原理图如下:
0 ?6 H9 h P! n9 q
, x2 J) ~% d& u1 V) u* @; \5 S
4 k1 U. b6 B8 @( r0 b5 S- F
# @3 S: {8 S& s( Y H, h
! T8 e5 w) ?4 v1 _W25Q64是一种使用SPI通信协议的NOR FLASH存储器,它 的CS/CLK/DIO/DO 引 脚 分 别 连 接 到 了 STM32 对 应 的 SPI 引 脚NSS/SCK/MOSI/MISO 上,其中 STM32 的 NSS 引脚是一个普通的 GPIO,不是 SPI 的专用NSS 引脚,所以程序中我们要使用软件控制的方式。FLASH 芯片中还有 WP 和 HOLD 引脚。 WP 引脚可控制写保护功能,当该引脚为低电平时,禁止写入数据。我们直接接电源,不使用写保护功能。 HOLD 引脚可用于暂停通讯,该引脚为低电平时,通讯暂停,数据输出引脚输出高阻抗状态,时钟和数据输入引脚无效。我们直接接电源,不使用通讯暂停功能。4 n0 X7 N) ^% {. I
" z( ]/ T9 C+ g4 i# g
W25Q64支持SPI通讯的模式0和模式3& j* Q9 T1 t6 R6 _: D) s2 d% C
' L+ H/ p/ c1 n4 _. K* j* o, V6 P2 T4 S! S& X
FLASH控制指令
3 O, t% m2 R m9 a1 B$ }$ MFLASH芯片中规定了许多指令,只要SPI向FLASH发送相应的指令,FLASH就会执行相应的操作,所以我们对FLASH的一切操作都是基于这个指令集的,接下来介绍一下FLASH的控制指令:
$ a# e% m( F% v2 i7 C, a
5 q8 |& z# e# ^9 M, t1 V# R
7 _; c7 L M! B8 h* x! [, Q
6 A a1 S1 q( t表中第一列为指令名,第二列为相应的指令代码,第三列及后面的内容根据指令的不同而意义不同,其中带括号的字节参数,方向为 FLASH 向主机传输,即命令响应,不带括号的则为主机向 FLASH 传输。表中“A0~A23” 指 FLASH 芯片内部存储器组织的地址; “M0~M7” 为厂商号( MANUFACTURER ID); “ID0-ID15”为 FLASH 芯片的ID;“dummy”指该处可为任意数据;“D0~D7” 为 FLASH 内部存储矩阵的内容。4 e, g- T& H, C* H, ~
看起来很复杂的样子,其实只要在需要执行相应操作时来查这个表,只要能够理解这些指令的使用方法,FLASH就算学会了。, F- e$ h8 M0 \: O- z7 H
" e; Q4 }; R* u, q0 S: R4 L例如:要知道FLASH的ID,那就在指令中找对应的取ID指令“JEDEC ID”,仔细解读这个指令9 z. n3 T! h+ G2 f+ x
# f- e+ t- ^& d$ O- P1 ~3 H1 n, _) E8 w( G, A# m
可以看出对应的指令代码为“9F”,后面的三个字节带括号,代表这三个字节就是FLASH向STM32发送的数据,即这三个字节就是FLASH的ID,然后使用SPI进行读取就可以了。
: z3 H6 P; T$ a% y( n" M% K* A3 y* D& ~8 B! S
我们一般是将这些指令宏定义在头文件中,便于使用:
2 l7 l# x* f0 [1 U- #define W25X_WriteEnable 0x06 # Z& W: O V) T# h% v. V! k3 s( M
- #define W25X_WriteDisable 0x04 : ]. J% T1 a9 p4 S9 P; Z
- #define W25X_ReadStatusReg 0x05 7 F, j" t/ u0 ^* O l) T
- #define W25X_WriteStatusReg 0x01
% {- G8 N* |' o, l9 i8 N$ o - #define W25X_ReadData 0x03 ; Q% P2 u" [5 y6 b
- #define W25X_FastReadData 0x0B
' G" K5 Z K$ \ - #define W25X_FastReadDual 0x3B
+ s! `, C: d1 R2 Q! q. U' Y - #define W25X_PageProgram 0x02 7 v$ s6 h3 a1 s3 f2 z ^% \) a& G& l
- #define W25X_BlockErase 0xD8
( Z! L# W. A ^( R8 N/ V+ ? - #define W25X_SectorErase 0x20 0 ^5 d# n( N# ^: {4 U
- #define W25X_ChipErase 0xC7
/ T- \7 B+ m. G$ u1 _6 c( A3 k - #define W25X_PowerDown 0xB9 " M' [5 d- ?" {6 W$ _0 d- e; e
- #define W25X_ReleasePowerDown 0xAB
( t2 \/ j. a1 |8 Z+ O - #define W25X_DeviceID 0xAB
) }; z! d4 O& h5 U; U - #define W25X_ManufactDeviceID 0x90
. I/ }; o1 A9 I7 F& ^/ } - #define W25X_JedecDeviceID 0x9F6 k9 m) R& k l0 W/ r
6 \2 a# |% C+ g( H
复制代码 4 e0 G8 O% |: J- [( |' Z8 X
FLASH内部存储结构
2 P, f# G+ [- T0 FFLASH的存储矩阵如图:其内存分为128块,每一块都有16个扇区,每个扇区大小为4KB,擦除数据的时候是以扇区为基本单位的。
9 k3 n7 |& j6 `/ l+ z8 e# m. X% \8 X, C4 K
0 H$ q% k# B+ |4 R6 G1 ?" ^7 e/ v3 F m$ K# d
代码讲解" ~% ^, ^# N! h8 [6 V
代码都是博主亲手写出来的,可以运行。
0 d* u9 V# x/ o% H代码部分会用到SPI的代码,关于SPI的说明之前整理过:SPI详解
3 p/ j6 ~. D9 N% e% \3 w1 k/ N2 t$ Q# d) i
读取芯片ID
9 `- z1 D3 L( _' _- f/ H: y' j6 l- /************************读取芯片ID*************************************/6 L/ A, ]+ {+ y* \. x* Z$ f `: r; P
- uint32_t FLASH_ReadID(void)
0 z: w/ Y6 H- D7 m - {, X" R( h, k# m! P; L. }
- uint32_t temp,temp1,temp2,temp3;
3 e0 @2 a# L4 o& ^) G, w- j+ y - SPI_NSS_Begin();
8 f4 t {+ U! p6 H" b7 |( P - /* 发送取ID指令 */
# g: k7 q# r' p6 J0 }1 C, b - SPI_SendData( W25X_JedecDeviceID);
* m' ?7 V8 ]; Q4 u$ A# A - /* FLASH会连续发送三个字节数据 */% K1 i; ^5 h5 e) O: @- B, F: a8 t( q
- temp1=SPI_SendData(Dummy_Byte);: s$ ~6 o+ Y$ q! b* j
- temp2=SPI_SendData(Dummy_Byte);' G8 |! F, ^. u
- temp3=SPI_SendData(Dummy_Byte);
7 N$ M* e$ _3 s+ L2 g - SPI_NSS_Stop();8 e( H0 [# W) A% Z
- /* 高为先行,将三个字节整理在一起 */
F3 j/ w: H0 M2 q# y& z- a/ h - temp=temp1<<16 | temp2<<8 | temp3;; t+ _; W) `* d/ W
- return temp;
% i. S( X3 Z9 `5 C0 Z! L" Z - }
. |: c8 j( ]& ^+ b
复制代码 0 T7 ^% p9 c% u
由于SPI是全双工通信,所以接收数据和发送数据用的是同一个函数
7 Y8 D! ?2 s* r
Q5 N$ r4 [: T发送写使能信号
3 K, ?4 v/ b+ j: a; ?+ v |! _! I6 M- /****************发送写使能信号****************************/' O% R; }8 b3 v8 w5 N
- void FLASH_WriteEnable(void)6 [( z) G, ?6 T5 M
- {
; B! z+ \/ p! r' N$ D" u - SPI_NSS_Begin();. W; a# \- x/ w2 y
- /* 发送相应的指令代码 */0 G7 x; O+ {- h7 T8 i
- SPI_SendData(W25X_WriteEnable);3 l; r7 Z) Q$ U9 ~" Y1 @# G
- SPI_NSS_Stop();
* I6 N/ Y% ~( ^0 @" k+ W - }3 Q' I" \7 y; D& [$ u" `" D
复制代码 9 F% T% J/ W# h% I$ T$ V$ K! s
在向FLASH中执行写操作之前,都要进行写使能操作,通过发送写使能指令到FLASH来实现7 S+ f# d h4 A/ w' z8 w: z2 ^
/ q( k! }; i% I等待FLASH不忙
# @/ y5 @, I t; _% H/ u. N- /******************等待,直到不忙***********************************/
* @) u. E* i. P0 R: Q( }. _ - void FLASH_WaitBusy(void)
8 |: E+ P( A {; T- [7 u3 I8 Z* y/ e - {1 `+ G# F% H% G# j( H
- uint8_t StatusReg=0x01;- W" m: n% z. h. h; q
- SPI_NSS_Begin();
9 R) m& ~ t% H, N - /* 读取状态寄存器中的数据,判断忙标志0x01位 置位代表忙 */7 H, ?! R9 G! _3 \) Q6 p2 ^
- SPI_SendData(W25X_ReadStatusReg); / @ A6 ^: y3 f% H# F/ D
- /* 只读取状态寄存器的BUSY位,即第一位 */
* d* U) s5 p5 D2 m- ^8 B4 M& N8 b - while((StatusReg & 0x01) == 1) 2 R$ V) {2 y% _, W6 J
- StatusReg=SPI_SendData(Dummy_Byte); ) f- L" V4 ]) d2 G
- SPI_NSS_Stop();
' Q- x/ G1 B+ p+ U* a, R" _ ?- D - }
( z5 u6 Y# x _. r, {) { - ) [* \/ P/ z: ]* \
5 g% O! K1 e& }9 b4 k3 X
复制代码 4 r0 E* q9 C% u/ L4 _# I) j
FLASH在通讯的过程中需要一定的时间来执行操作,在这期间,传输数据是无效的,因为FLASH忙着呢,所以我们就要有一个函数来专门等!等到FLASH不忙了,再进行通讯,那怎么等呢?FLASH不忙了会给出一个信号——将状态寄存器的BUSY位重置(也就是0),所以我们需要不断的来检测状态寄存器中的BUSY位是否置位,利用读取寄存器状态的指令来获取状态寄存器当下的状态,然后根据寄存器的BUSY位(第1位)来判断FLASH是否处于忙碌状态。
( z1 I0 k! O0 P* A7 E z0 i简单来说,这就是个延时函数,延时直到FLASH空闲,可以进行下一步传输。
( j& [$ ?/ w5 K4 [9 Z0 v$ Q" E& K
1 Q/ Q# u* O" c/ X, @( B' v. F. ?% j% ]1 O+ j6 l
擦除扇区/ V( b6 k) R7 G
- /******擦除扇区的内容,切记地址要对其到4kB,每个扇区的大小都是4KB********// \" ?* q6 H' c% A9 I1 G. q
- void FLASH_SectorErase(uint32_t addr), w: l# r9 S5 M& Z7 E! p" L% C
- {
- K: C: S: l6 J' G2 a1 q - /* 开始的时候要发送写使能信号*/
3 C! E% Y9 w y h3 I8 N - FLASH_WriteEnable();4 E8 M, g/ s' R0 c
- SPI_NSS_Begin();4 c7 G/ h# k6 T; o! [
- /* 发送扇区擦除命令 */: W2 { {/ ^8 v$ ]1 z" c
- SPI_SendData(W25X_SectorErase);
3 [1 B% ]3 U4 K9 u& q! Z9 _: k - /* 发送扇区的地址,高位先行 */# F( S# ?9 M7 @/ ?# g
- SPI_SendData((addr & 0xff0000) >> 16);
" V+ D0 @8 N, Z! u - SPI_SendData((addr & 0xff00) >> 8);- l9 o2 m8 ?# s5 z1 {- K8 K
- SPI_SendData(addr & 0xff);
# X- q( f7 I0 K# y* F0 w; ` - SPI_NSS_Stop();7 @( e: K0 f. f7 e* @# L
- /* 最后也要等待FLASH处理完这次的信号再退出 */5 z7 g3 s4 S% s( I2 h9 G, d8 h
- FLASH_WaitBusy();
3 m3 O( W) E/ L [, d; J - }
+ F( R; w& r* j6 J1 i - % |! E/ S3 d8 h+ p
复制代码
# C, V$ L1 y- H扇区的擦除之前要发送一个写使能信号,先发送擦除指令,然后发送要擦除扇区的地址(分三个字节发出去),高位先行。9 v$ N2 l/ b7 q7 H
扇区上的内容不是1就是0,擦除的过程就是写1的过程(将一个扇区全部写1),因为在写入数据的时候,可以将1写为0,但不能将0写为1.) b% ]4 n2 t8 u% r- y
" }1 p0 y2 i, }3 u. W5 G& ~/ V$ r) M2 t! b9 Q. s$ w' u8 R
写入数据
6 t+ E1 V! Q5 Z6 D# A- /************按页写入数据,但写入之前要进行擦除***********/. `, t8 p7 T' R7 n1 }
- void FLASH_PageWrite(uint32_t addr , uint8_t* pBuffer ,uint8_t size)+ m& Z$ y* K' ^+ F5 Y3 a
- {
: f% S6 O& K* m/ B( _9 u4 z! l1 F - /* 开始的时候要发送写使能信号 */
6 Q8 R7 i9 j' j5 c6 f; L) C: h- a - FLASH_WriteEnable();
' Y( \+ h! P" ]5 x8 q - SPI_NSS_Begin();
$ Y0 v: {# P h" z8 l - /* 发送页写入命令 */
2 c# Q6 n" c5 m- [: X - SPI_SendData(W25X_PageProgram);1 Y) r; }" y: r* J
- /* 发送写入的地址,高位先行 */6 F1 T) h5 R; Z* k& E, V4 B
- SPI_SendData((addr & 0xff0000) >> 16);
- A( W: ]% d- c2 t1 N( t2 m - SPI_SendData((addr & 0xff00) >> 8);9 F8 B s+ T# m& A" \
- SPI_SendData(addr & 0xff);
, |5 n; b6 ?6 H! u! k - /* 逐位发送数据 */3 a% r0 }( M1 S% q2 e }1 O6 F
- while(size--)
5 l' J9 \4 t1 ?* _. I% q$ h - {" c% i% a; C* Y; e2 v7 w" D: f
- SPI_SendData(*pBuffer);8 M1 s0 D% p1 r& {/ i+ ~& B
- pBuffer++;' B4 r0 P- h+ g% r4 X I. v
- }* k% ?7 \ H9 u( F
- SPI_NSS_Stop();
: p5 W( Y% K [0 w3 z7 A - /* 最后也要等待FLASH处理完这次的信号再退出 */
& `' p7 [ G# t# h+ \7 N - FLASH_WaitBusy();1 x% a8 i3 S. ]
- }
* U {3 [+ B5 H4 `# @9 l - , g5 g) ~/ c! Z* B6 k; j0 \
复制代码 6 b* ~$ H8 w' m- A- T
在执行写入数据的时候函数的参数有三部分:
7 y" K2 ^6 \& E6 M1.要写入的地址
: t1 v2 r7 r8 O2.要写入数据的首地址
# [/ [! k3 t8 N3.要写入数据的大小! n# U0 F6 x8 c! E% O. K: C
函数在执行的过程中,首先发送一个写使能信号,然后发送写数据指令,紧接着发送数据要写入的地址,然后就是逐位发送数据了,函数最后等FLASH处理完这次操作再退出。, `3 j: f( \; s* N E% R) P
& r# P# r) X+ y, s1 K6 g7 `! \# d+ {; X8 G- f/ w
读取数据
/ l% o5 E+ }7 w' |: t6 A! \- /**********************读取指定地址、指定长度的数据******************/
. K. S. I' D) S/ S5 Y/ D$ E0 w - /* 因为读取在了指针中,所以不需要返回值 */
8 A* H( O% e D: ?. D' ] - void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size)# }% B) b1 q3 H
- {
8 R7 M4 T* X, L* A! ~ - SPI_NSS_Begin();8 m$ g4 p8 |/ c4 m
- /* 发送读取命令 */0 ?9 D$ d8 F, V1 A+ B* _. z
- SPI_SendData(W25X_ReadData);7 h0 J: r# `6 S' K M' W
- /* 发送读取数据的地址,高位先行 */8 N+ N+ f- g# o4 k
- SPI_SendData((addr & 0xff0000) >> 16);
8 E# v/ u# W' ~8 C - SPI_SendData((addr & 0xff00) >> 8);/ H* A: v2 ?5 _; O
- SPI_SendData(addr & 0xff);5 |1 Q% ~5 @$ t! z7 G
- /* 逐位读取数据到指针上 */
+ G/ i' P/ }" ?' }3 v4 s - while(size--): d+ Z: o. `; I$ ^, i
- {# \" D9 t$ Z* w' T. \
- *pBuffer=SPI_SendData(Dummy_Byte);8 Q9 J, |3 c( O( s6 D
- pBuffer++;
2 X1 B$ }1 _4 k8 c+ N8 K" P& a - }4 P1 @0 v, w1 c5 b
- SPI_NSS_Stop();
) M; h2 D) z5 X' q1 c! D - }
5 X( B7 ?. G" p9 R9 t - 7 s( y' b' e. L1 L# H/ D
复制代码 , o( a5 @& }) N
在执行读出数据的时候函数的参数也有三部分:
" t4 \: c" Y8 F1.要读出的地址4 y, b0 R; U1 E
2.读出到指定地址
) g( g4 f+ v. |+ w3.读出数据的大小% u* V! r* Q k; f" A
函数执行过程,首先发送读取指令(这时就不用发送写使能了),然后读取数据的地址,然后将数据逐位读取在固定地址中(地址最好是全局变量),使用时再从全局变量地址中获取数据。
0 O; C4 l; y. {- ]! b. I V; R/ |这里涉及到函数的返回值问题,具体分析链接:返回多个变量怎么办% [- m& r3 e! f
5 v9 b4 a) ~% g) T
注2 V4 L( w3 e# l, f) w: d# o: g
有一个问题当时困扰了博主一天,那就是发送和读取数据时,怎么把数据返回到主函数中,解决方法是,创建俩个全局变量数组,一个负责发送数据、另一个负责接收数据,这样就ok了( j; E' s4 \- G- ^% i8 Y
附上主函数
8 _6 \3 y' G8 p) _; ~8 O: C5 M- /**********************读取指定地址、指定长度的数据******************/0 v# l7 k+ d: s6 m
- /* 因为读取在了指针中,所以不需要返回值 */3 {7 e& x) p) U- d3 g5 K
- void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size)
u- \) j# M5 ]& ^9 }! ]; W4 @ - {
' J( v. P9 F0 k$ p( D - SPI_NSS_Begin();
. m6 N1 Q. f. M0 T' Y* W9 y- { - /* 发送读取命令 */
0 g* t0 U: q B' H: j - SPI_SendData(W25X_ReadData);
9 f" \4 ^ ~3 R3 P1 \5 e - /* 发送读取数据的地址,高位先行 */. n, e7 F# R9 I3 P7 S
- SPI_SendData((addr & 0xff0000) >> 16);
& p! _2 c+ D. t4 E1 q2 s' @ - SPI_SendData((addr & 0xff00) >> 8);( p1 Q* F% M0 m3 h5 K
- SPI_SendData(addr & 0xff);( @! Z& _; s9 H, w
- /* 逐位读取数据到指针上 */
" o% R/ _* |! B/ B) X7 O2 h - while(size--)
* q% ], W, d2 v - {7 s$ }, T% E1 H5 `" ?1 e; Z
- *pBuffer=SPI_SendData(Dummy_Byte);
( U# g# u8 h. u! c: t; x - pBuffer++;
\0 [; M( t# l' G" E8 l - }
2 v+ h+ J% H! [* b( A - SPI_NSS_Stop();2 O$ q6 U; { c& \
- }, Z7 w/ ^1 L' t# v9 ^
- Z* t8 m% i& N
复制代码 % B g8 j" \& B d9 f9 L2 Q1 y, l
————————————————$ S. k% U3 J: V& D- Q0 w( K
版权声明:Aspirant-GQ
. |( H: W3 O% V& b
/ M+ ?" L' z; h: X# f如有侵权请联系删除
: f# d( R) e/ E" j6 t7 C% W0 I7 e0 O1 S
' z' o) X: W8 W: V3 |
5 S+ x% j) b/ f
|