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
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
" 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
/ 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
- 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- #define W25X_WriteEnable 0x06 ! r5 j5 n7 ~7 |/ b' f7 f9 w
- #define W25X_WriteDisable 0x04
2 D& J7 P( S6 ^" U% Z- Y9 {( n - #define W25X_ReadStatusReg 0x05 ' D4 n2 @; R) z: s; _0 {
- #define W25X_WriteStatusReg 0x01 % ]4 {3 a; j& w" n4 T1 a% @0 R0 Q
- #define W25X_ReadData 0x03
8 D4 p7 A; E K2 b* D6 y - #define W25X_FastReadData 0x0B - p! P5 }" _" {
- #define W25X_FastReadDual 0x3B ) l3 f% h2 Z5 R$ ]6 T
- #define W25X_PageProgram 0x02 & o6 v! |0 k. G: B5 ?* q" c8 ^
- #define W25X_BlockErase 0xD8
2 s6 Q9 n$ v( p h - #define W25X_SectorErase 0x20
$ T9 R' ^# c* {$ }/ E* A2 r$ j - #define W25X_ChipErase 0xC7 7 F" x4 D3 M- X' k5 O: A7 B
- #define W25X_PowerDown 0xB9 9 d& ]+ r+ U+ U9 ]0 Y, h9 J+ Z0 D
- #define W25X_ReleasePowerDown 0xAB
7 E5 }# O" D$ t$ q- o$ [ - #define W25X_DeviceID 0xAB ' z( f* C4 M7 c9 s
- #define W25X_ManufactDeviceID 0x90
! W5 g, Z, w1 ~( W - #define W25X_JedecDeviceID 0x9F% Q8 n3 `# a. r# P
' 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 `
, 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" O5 b0 h$ O4 E8 w) C7 r5 P4 h
读取芯片ID
: T% j$ J( Y6 ?( J4 j) F$ |- /************************读取芯片ID*************************************/
. S* b7 F% t$ q+ y( Q8 A5 F8 r5 o1 W - uint32_t FLASH_ReadID(void)
, T" D4 S# i4 a$ \ - {3 D9 B6 i! a% b1 Z. l
- uint32_t temp,temp1,temp2,temp3;. l+ ?# ^& ?- ]/ h* S+ S
- SPI_NSS_Begin();0 r; R7 S6 n3 \, f7 V6 ? Y0 g7 [
- /* 发送取ID指令 */
" a1 `4 x# g5 F3 p6 G6 M - SPI_SendData( W25X_JedecDeviceID);% }7 f( ?4 t9 D1 N7 H6 W; y
- /* FLASH会连续发送三个字节数据 */: p2 P) C" ~2 x1 `
- temp1=SPI_SendData(Dummy_Byte);
5 y7 ~% Y9 m0 b9 R, k) {; j0 [) s - temp2=SPI_SendData(Dummy_Byte);
+ }4 I& S& u; m) Q! } - temp3=SPI_SendData(Dummy_Byte);
* a7 y E( Q* ]3 {3 Q - SPI_NSS_Stop();
" ^/ ?, q* |: s2 V$ y - /* 高为先行,将三个字节整理在一起 */* }0 ^5 G4 d% p) q
- temp=temp1<<16 | temp2<<8 | temp3;" G" ^" V/ g- o1 P, ]
- return temp;) L/ d! S$ k5 N" M1 h
- }' 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- /****************发送写使能信号****************************/9 ~* K" T3 V1 x6 ~6 r2 [
- void FLASH_WriteEnable(void)
" j0 T" h1 `( A& v+ H) W - {; F" @# x+ u% A' S
- SPI_NSS_Begin();
$ p/ Y, r `' c" Z, b) y' n: e - /* 发送相应的指令代码 */
( R2 t9 y- n$ b: H# a - SPI_SendData(W25X_WriteEnable);
4 x; z( |2 ~ l: B. S9 {+ I - SPI_NSS_Stop();' f4 [6 i( d& G+ t
- }% 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
- /******************等待,直到不忙***********************************/
: }" _$ M' d; d7 e! m - void FLASH_WaitBusy(void)
# Q% S% B% D( ^5 p# K2 { - {
n: { `" j: d0 F- G. \, O0 M - uint8_t StatusReg=0x01;& s4 e0 S; I( q
- SPI_NSS_Begin();! }' m2 q& I$ }5 k) u+ {, L0 N
- /* 读取状态寄存器中的数据,判断忙标志0x01位 置位代表忙 */7 z% b# `# v% e1 t1 z( \% q: f
- SPI_SendData(W25X_ReadStatusReg);
. ], \; |3 h1 p0 ?: i' N3 P - /* 只读取状态寄存器的BUSY位,即第一位 */- a+ y7 e6 U0 i t+ a' G u* n0 J
- while((StatusReg & 0x01) == 1)
3 g' q' i" i7 x1 b - StatusReg=SPI_SendData(Dummy_Byte); j6 n7 O# C( C; o/ p
- SPI_NSS_Stop();4 `2 s) y4 V! Y# K+ T) ^" B8 E
- }
( l) k, D: d* X+ i1 g8 V3 o - ( H* x h9 t5 M3 u% |( ~+ n
- % 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
- /******擦除扇区的内容,切记地址要对其到4kB,每个扇区的大小都是4KB********/$ F6 O1 w- T/ |& a0 ?7 G% r7 o% @
- void FLASH_SectorErase(uint32_t addr)' i5 o9 f( n3 o* M: Z8 r7 J
- {
# M' T3 D8 m( Q4 C; ]/ Q, k - /* 开始的时候要发送写使能信号*/! s/ G7 K( Z6 [2 `6 ^; H
- FLASH_WriteEnable();, P; ? p6 c0 Y- s9 G5 e& y
- SPI_NSS_Begin();; J; u# m/ t7 k# k5 S$ @3 @8 a
- /* 发送扇区擦除命令 */, a: @0 c4 ]$ C% y
- SPI_SendData(W25X_SectorErase);: d3 t$ L+ Y. y
- /* 发送扇区的地址,高位先行 */5 j, M6 O2 @2 M
- SPI_SendData((addr & 0xff0000) >> 16);
1 H V' f6 n0 z - SPI_SendData((addr & 0xff00) >> 8);1 m" m* A, b- I3 r; J
- SPI_SendData(addr & 0xff);
; v4 Z' G! n) `6 z - SPI_NSS_Stop();
$ V+ l0 i( J6 G) s) t3 m - /* 最后也要等待FLASH处理完这次的信号再退出 */# b" P3 U6 c) p( p, O: O/ D
- FLASH_WaitBusy();! j# `" \5 b1 J4 l) V+ ^3 R
- }% X9 P: U; w" z; a3 P
- / 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 {
- /************按页写入数据,但写入之前要进行擦除***********/
8 x+ K6 ]; D; X- ? - void FLASH_PageWrite(uint32_t addr , uint8_t* pBuffer ,uint8_t size)
4 d; e J) n( J/ l! d( K5 W - {1 }- |/ A5 X1 l" \7 v4 Q4 S: V
- /* 开始的时候要发送写使能信号 */
}: k) F9 ]+ {- w& K# n' j6 ^ - FLASH_WriteEnable();
$ G; P& m' T( l. A! i - SPI_NSS_Begin();! V3 p9 I9 I2 d
- /* 发送页写入命令 */( D2 A2 R, j6 y& S8 ~# s" d
- SPI_SendData(W25X_PageProgram);
3 h7 S1 X! {' H$ R* \$ S - /* 发送写入的地址,高位先行 */
, o I1 a; ]" z8 d, ` - SPI_SendData((addr & 0xff0000) >> 16);* N& h- ?0 {; E0 c7 c
- SPI_SendData((addr & 0xff00) >> 8);
) E& ~1 ^; `& e8 W - SPI_SendData(addr & 0xff);& F# H2 M+ \/ G, T4 B) l2 P0 x5 m; x
- /* 逐位发送数据 */
$ D3 S) i; R* S; g - while(size--). n' |8 e3 q( R; t2 Q
- {4 E" j i- C. H! e$ }$ }5 J, U' f
- SPI_SendData(*pBuffer);9 N* M f( F2 }" m; B0 u
- pBuffer++;" j% ^6 f" Y x) ]3 p
- }" g$ C1 Y8 P, y* n2 }& m; J
- SPI_NSS_Stop();
* H. K& y6 ]8 F1 T/ d - /* 最后也要等待FLASH处理完这次的信号再退出 */
4 w( p4 p5 t _# A- Z - FLASH_WaitBusy();
( V1 i- t1 f) A - }! s9 W/ ?6 {: [* @2 n5 q7 K
- 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- /**********************读取指定地址、指定长度的数据******************/
; ]2 |9 z# O5 a+ [4 T - /* 因为读取在了指针中,所以不需要返回值 */
: q" m7 J* b8 l5 O n - void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size)
, V& S8 U A* s/ E6 k - {8 ~/ X4 v) |' O* f
- SPI_NSS_Begin();0 j% I" G# e; n' s1 v/ V8 y6 N
- /* 发送读取命令 */
, R' A7 V- {# m2 x - SPI_SendData(W25X_ReadData);
2 P4 s# B' j" _ - /* 发送读取数据的地址,高位先行 */
$ s' J3 n- W- e, C0 h2 L' }: |7 w9 M - SPI_SendData((addr & 0xff0000) >> 16);! j! u8 L. a; X- L) J- W
- SPI_SendData((addr & 0xff00) >> 8);. e' f: x3 G' n1 x
- SPI_SendData(addr & 0xff);
& c) D1 ^# r3 m M - /* 逐位读取数据到指针上 */
$ I2 p3 i( p& F8 K' {& T - while(size--)
( }/ k( _. @) `/ S) K8 f, l+ ? - {, H5 X$ X) Z2 ^- X- W) Q1 n
- *pBuffer=SPI_SendData(Dummy_Byte);
, Z7 j4 _7 C9 l P% ^ - pBuffer++;4 U" r( `) B8 U8 ~1 x3 w1 b# ?
- }
& ~( b& ]% k2 Q' Q& W - SPI_NSS_Stop();2 @, s# {6 B/ [" G5 K8 u5 u
- }
+ X4 [( Q/ n' g2 @' Z
& @# 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 d/ Q7 l; M. K( ], N6 g
- /* 因为读取在了指针中,所以不需要返回值 */
6 \4 {; N$ t! b' \' R- u' h, [3 ` - void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size)
|7 B" Y; C1 K3 J6 A: C - {, w3 ?) U! m2 {8 J
- SPI_NSS_Begin();8 H; C! K8 z L/ [( z6 q6 r, q
- /* 发送读取命令 */
& E5 G" @% A% V& S7 s5 h - SPI_SendData(W25X_ReadData);
! u5 x. C+ K4 a9 i - /* 发送读取数据的地址,高位先行 */
* h+ w0 ~' ]0 {! g" h - SPI_SendData((addr & 0xff0000) >> 16);
4 e* p3 R$ r. L3 c! p - SPI_SendData((addr & 0xff00) >> 8);. ?+ Q( V; j* \$ T
- SPI_SendData(addr & 0xff);
& o) t( D2 N8 b$ P - /* 逐位读取数据到指针上 */" Z: ?) Q D" |
- while(size--)
5 n& s/ m0 F$ _! y# w! a$ M' B - {
9 ~. [9 `% [0 g: z; P- d# B - *pBuffer=SPI_SendData(Dummy_Byte);) {: X7 x9 v* L
- pBuffer++;1 c0 V9 u1 `. M7 b
- }
) n: S8 }9 F& T; u2 y - SPI_NSS_Stop();
: f( n: N' c; X* d8 @ - }
! ^' v* C, R/ r2 h - ! @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 }: { |