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! _
% 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
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- ~
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 [
! 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
- #define W25X_WriteEnable 0x06 : R2 D6 {1 c9 F6 L: s6 W/ I
- #define W25X_WriteDisable 0x04 " d+ N* N8 U9 r* j; w$ w
- #define W25X_ReadStatusReg 0x05
, j3 O- q# C4 t9 O - #define W25X_WriteStatusReg 0x01 9 W, ~7 v7 n8 I
- #define W25X_ReadData 0x03 & \$ h* X) q# J' K' E# L
- #define W25X_FastReadData 0x0B 9 u6 `- o( \# }$ T1 O( k
- #define W25X_FastReadDual 0x3B
& d7 @- ]7 {$ P& X; }2 Q7 m - #define W25X_PageProgram 0x02 # ]) Q, y& j: V$ \' R: t. T0 u
- #define W25X_BlockErase 0xD8
& D( e2 l1 x8 I - #define W25X_SectorErase 0x20
) ], q2 ~8 ~' ^7 Y V) V$ j - #define W25X_ChipErase 0xC7 ) i% W r2 f/ X$ g6 ]0 q4 p0 G" b
- #define W25X_PowerDown 0xB9
% O* H3 Z$ Q8 m1 ~ - #define W25X_ReleasePowerDown 0xAB
H: T* Y' o+ u" q. ` - #define W25X_DeviceID 0xAB
+ L) ^! @+ ^3 h9 ?5 C - #define W25X_ManufactDeviceID 0x90
% ]' L# e& k6 z/ K( x( O - #define W25X_JedecDeviceID 0x9F
3 F& \( b1 D) t& w - 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
; ]% 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
- /************************读取芯片ID*************************************/! V. Q* Y# J( Y& X3 ~
- uint32_t FLASH_ReadID(void), ~( o# Z3 a& B/ s! O% X9 |+ p9 v
- {+ `. B! h' J# i; T5 i* ]
- uint32_t temp,temp1,temp2,temp3;* T8 h% u0 _! N: B; k
- SPI_NSS_Begin();
/ m/ u( E2 I( L/ c7 ?% F - /* 发送取ID指令 */0 r+ B0 o# E$ ]
- SPI_SendData( W25X_JedecDeviceID);8 F3 f s/ r3 R: q0 U$ _
- /* FLASH会连续发送三个字节数据 */
; b2 c1 h2 j3 j. Y - temp1=SPI_SendData(Dummy_Byte);
! u" }+ ?7 ]6 q1 j- O - temp2=SPI_SendData(Dummy_Byte);$ v: i( _) Z, Z: q1 P3 S
- temp3=SPI_SendData(Dummy_Byte);, ^0 J; a, v8 o" y- q @2 V e2 @5 T
- SPI_NSS_Stop();/ E" K- R/ |) E
- /* 高为先行,将三个字节整理在一起 */6 b0 S6 d% `; n1 D Q/ w
- temp=temp1<<16 | temp2<<8 | temp3;# H4 t9 {7 @. f0 l$ w5 s
- return temp;
& z: I) W# m4 L# Y6 u9 _ - }
; 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- /****************发送写使能信号****************************/
8 k! N: L+ w) T - void FLASH_WriteEnable(void)5 |/ {0 C2 I6 m2 w* ` d8 o, W
- {
9 a f# O r4 V# i! h/ h - SPI_NSS_Begin();
' p( T# ?- q y9 y9 w M - /* 发送相应的指令代码 */
' w+ I! ?, E% P, D- y - SPI_SendData(W25X_WriteEnable);9 A; j- G# p9 {0 k# `
- SPI_NSS_Stop(); s4 h! I/ F' D* r7 T
- }
) 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
- /******************等待,直到不忙***********************************/ s% t+ n1 c8 }, L9 l" G( }. P% h
- void FLASH_WaitBusy(void)) ~0 i9 d0 l: C. Q' j
- {- C+ \: e1 e8 t4 ]( t& [
- uint8_t StatusReg=0x01;
* A2 X! J: r9 ?% s - SPI_NSS_Begin();
+ H1 X/ D# [/ u7 u* b6 A - /* 读取状态寄存器中的数据,判断忙标志0x01位 置位代表忙 */
0 k" M8 q" J x. F7 K - SPI_SendData(W25X_ReadStatusReg); , O0 e4 G% v* p0 ~; M- D1 P$ D
- /* 只读取状态寄存器的BUSY位,即第一位 */
* A; r" J4 I$ k# p; ]1 W$ V - while((StatusReg & 0x01) == 1) 2 v- ?; d% s7 a$ S: ?# R; a
- StatusReg=SPI_SendData(Dummy_Byte);
) r4 E! C6 i' @5 r9 F. e - SPI_NSS_Stop();7 N; |7 V1 m o' ]3 h% N
- }
* [/ C! S* E5 p% J1 y
4 f( t: C4 z! O! @0 e- ) 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
- /******擦除扇区的内容,切记地址要对其到4kB,每个扇区的大小都是4KB********/4 o$ r$ T* U8 S/ v8 u% _/ ]$ ~
- void FLASH_SectorErase(uint32_t addr)3 n, e7 D/ b/ f8 O m' `0 A( a
- {
# W. c# I3 H3 J# t8 e3 \3 M - /* 开始的时候要发送写使能信号*/
) ~6 s9 }* i2 y6 f4 t - FLASH_WriteEnable();4 p8 l3 b: y5 A$ ?. y9 g. c
- SPI_NSS_Begin();- ]; ` x( X0 j& o
- /* 发送扇区擦除命令 */
& |9 t& L# N* r - SPI_SendData(W25X_SectorErase);
M- l; a& W! I4 q - /* 发送扇区的地址,高位先行 */
2 x z. K; C0 h) `* `0 J& Y - SPI_SendData((addr & 0xff0000) >> 16);+ n+ I' g* q1 ~' u# Q7 d* t2 n' F
- SPI_SendData((addr & 0xff00) >> 8);
: t* L9 ` `( j- x" M u - SPI_SendData(addr & 0xff);
* F/ r4 E, q d) s - SPI_NSS_Stop();9 t) a" P0 Y/ ^
- /* 最后也要等待FLASH处理完这次的信号再退出 */$ t2 n) F8 z9 V, ^4 S( c+ V
- FLASH_WaitBusy();
4 ]" Q2 I- r; j2 M. w5 ]( a - }
|3 y1 a' g) A! F - 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. y0 a, x& L1 E Y* X a9 s' b
写入数据, }$ Y s- o9 W+ C
- /************按页写入数据,但写入之前要进行擦除***********/
" ]" v6 w& [/ u - void FLASH_PageWrite(uint32_t addr , uint8_t* pBuffer ,uint8_t size)
1 ?$ |; L* }0 d0 K( @ - { w1 `1 a7 P: O( T
- /* 开始的时候要发送写使能信号 */
. d$ a$ c: m8 U+ d - FLASH_WriteEnable();- ?1 v- ?; m( D4 q0 g8 |
- SPI_NSS_Begin();* L4 W% u2 R7 k5 P$ n4 B$ V
- /* 发送页写入命令 */0 c: T; ^1 N" Y/ T
- SPI_SendData(W25X_PageProgram);
& x. i% c, l9 j- ` a! p$ C - /* 发送写入的地址,高位先行 */
3 ~! o& w4 C# c - SPI_SendData((addr & 0xff0000) >> 16);
- I( r- [* h+ H* g - SPI_SendData((addr & 0xff00) >> 8);
8 g9 \! U" y7 ]7 z - SPI_SendData(addr & 0xff);# I" K7 F# U; t4 G( s5 `
- /* 逐位发送数据 */
* Y1 f% ]! v( `" X - while(size--)7 T0 o1 J8 E% F) K
- { e5 N6 u% a; ]8 |; ~
- SPI_SendData(*pBuffer);
& B* a: e0 H- U3 B7 k; e. S7 u! n - pBuffer++;
# L- F3 q* l5 I, Z; r - }8 S6 t( {# d- o+ j1 e8 \
- SPI_NSS_Stop(); V3 {) g1 Z+ n+ }, X
- /* 最后也要等待FLASH处理完这次的信号再退出 */
# ^. x( n3 z/ C- U' F, K - FLASH_WaitBusy();
9 E- w: r3 k$ I) m$ H6 m: c - }
2 t' b8 d9 p$ h - # 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- /**********************读取指定地址、指定长度的数据******************/
3 T" M$ u, t7 }3 K( | - /* 因为读取在了指针中,所以不需要返回值 */
* e3 S. K9 A- {" U" D - void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size)3 R6 E" f" f8 R" N% a1 ^
- {" d! D8 g8 z N. z3 `) ~: I
- SPI_NSS_Begin();
1 q: ?6 g" t3 |7 b6 F+ T% p" A. o6 r5 N - /* 发送读取命令 */
; _, R- p# n$ j5 q: J9 N6 _6 o - SPI_SendData(W25X_ReadData);/ R d' @6 g. G5 L% y& d2 l
- /* 发送读取数据的地址,高位先行 */
) s4 J# \+ G: Y8 o- x - SPI_SendData((addr & 0xff0000) >> 16);" G- \: N/ R3 D3 A
- SPI_SendData((addr & 0xff00) >> 8);! k3 |% t4 n$ q/ e
- SPI_SendData(addr & 0xff); x1 w; F7 P3 p2 P9 X8 U- r
- /* 逐位读取数据到指针上 */, |1 A9 R% p' P" \, k& m2 ^; T
- while(size--)
% w C/ Q+ U$ k7 R; c8 c - {
$ x" }8 i' j0 u2 p: l - *pBuffer=SPI_SendData(Dummy_Byte);
$ J. w+ q8 M. i% S! ?3 C# B/ ] - pBuffer++;) h" n; d/ l% b$ N
- }
1 O4 a8 U, o& \" q+ q - SPI_NSS_Stop();5 z1 r6 j$ W1 @7 i4 L
- }
) J' \6 U4 I1 }: N& [1 A
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- /**********************读取指定地址、指定长度的数据******************/3 C, @: \& U* ~
- /* 因为读取在了指针中,所以不需要返回值 */
4 [2 _4 n: ?& k0 A - void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size)
, c) Q4 ]- M* ?$ P9 A! c7 | - {
h6 s) m( G0 }/ }: R- {0 C - SPI_NSS_Begin();
8 m* p; b$ `( W# B. T7 z) Y, g2 v - /* 发送读取命令 */. e5 ]5 ^# [8 v8 \, s' @
- SPI_SendData(W25X_ReadData);2 c0 q& X, q0 Z6 E/ R7 f& |
- /* 发送读取数据的地址,高位先行 *// K* q+ Q5 X9 D( V5 t: v' L3 Y& t
- SPI_SendData((addr & 0xff0000) >> 16);
0 F& F; A* Y7 B9 P1 D0 i" Y - SPI_SendData((addr & 0xff00) >> 8);
6 o3 J4 ?& N0 A; P% I - SPI_SendData(addr & 0xff);' ]9 V3 |2 d/ ~2 ]/ V" w
- /* 逐位读取数据到指针上 */: O3 w7 y, c7 o9 `- z
- while(size--)% \ _: }7 e+ O6 P" l- c
- {
' k" m- {! l( _% E3 q5 N4 |0 [ - *pBuffer=SPI_SendData(Dummy_Byte);. @! ?- e+ ]# S/ V9 {
- pBuffer++;
, r& \+ h: Y+ m" s& B% H% l; ?' g - }) |4 H/ e1 T1 l! h$ r& o H) W M
- SPI_NSS_Stop();
" A2 R& V0 A1 @9 J* L - }& g2 R# n2 Y, q
- . 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# @ |