FLASH简介& t$ \& T: b' c
FLASH俗称闪存,和EEPROM一样,都是掉电数据不丢失的非易失行存储器,但FLASH的存储容量普遍大于EEPROM,现在像如U盘、SD卡、SSD固态硬盘以及STM32芯片内部存储程序的设备都是FLASH类型的存储器。由此可见FLASH对于我们学习和工作的重要性,EEPROM可以实现单字节的擦写,而FLASH都是一大片的擦写,就像是大规模杀伤性武器,其最小擦除单:扇区的大小也是4KB。$ u8 n* X; V2 x2 ?% c
我们此次通过SPI对FLASH存储芯片W25Q64进行读写擦除的操作。
: d4 f$ n+ J0 {* U k/ i2 v/ p/ S6 v) t/ i
对于FLASH内部结构的详细说明博主会专门整理一篇博客来说明,所以关于FLASH芯片的相关原理,本文中只做简单说明,侧重代码部分。
, B6 q; [: c' X& l% V6 t9 GFLASH详细说明的博客链接:(没有链接就说明还没有整理出)& G: |4 \* W( [7 H$ j, L
@" u( b' O6 V8 e9 T
W25Q641 |- Z/ S7 n; V- E& p3 \8 O" p
W25Q64简介
3 C8 K. w; l8 k* H- P7 I就长这么个样子
Y* j0 U$ F) h/ }# W' t5 k' e& N: T* F8 j% a1 V, K* u2 p" ^
" W" l& a9 z4 O; }5 x3 Z/ u6 J5 w: _. G8 w$ e3 }
STM32内部原理图如下:. `2 I6 l, k; r3 t: I% y+ A" Y
, U1 D$ h7 F0 \$ `* }4 v
6 p: W2 J* | ?! b: u$ q( @4 S
& i* ]$ l3 u7 I6 [3 D% o
0 L# D5 u% T0 {6 {- a. i4 b+ K/ VW25Q64是一种使用SPI通信协议的NOR FLASH存储器,它 的CS/CLK/DIO/DO 引 脚 分 别 连 接 到 了 STM32 对 应 的 SPI 引 脚NSS/SCK/MOSI/MISO 上,其中 STM32 的 NSS 引脚是一个普通的 GPIO,不是 SPI 的专用NSS 引脚,所以程序中我们要使用软件控制的方式。FLASH 芯片中还有 WP 和 HOLD 引脚。 WP 引脚可控制写保护功能,当该引脚为低电平时,禁止写入数据。我们直接接电源,不使用写保护功能。 HOLD 引脚可用于暂停通讯,该引脚为低电平时,通讯暂停,数据输出引脚输出高阻抗状态,时钟和数据输入引脚无效。我们直接接电源,不使用通讯暂停功能。) `* ~% w2 r ]% Y, O: f7 s% g; ~
7 R$ Z# V' \# N
W25Q64支持SPI通讯的模式0和模式3$ P5 S+ B3 p. i9 X7 Z. q1 x! }, Y
% j* P* O) ]: E9 ~# u
: S8 b. [# Z0 p1 _. X& XFLASH控制指令3 v; c9 o% t4 ]# x' M
FLASH芯片中规定了许多指令,只要SPI向FLASH发送相应的指令,FLASH就会执行相应的操作,所以我们对FLASH的一切操作都是基于这个指令集的,接下来介绍一下FLASH的控制指令:
# T2 k5 \0 g s B! f
" N. L* x: D# R
% j( V, d* l" g
) J2 ~' r; \& U, I' Y- z B) S$ I表中第一列为指令名,第二列为相应的指令代码,第三列及后面的内容根据指令的不同而意义不同,其中带括号的字节参数,方向为 FLASH 向主机传输,即命令响应,不带括号的则为主机向 FLASH 传输。表中“A0~A23” 指 FLASH 芯片内部存储器组织的地址; “M0~M7” 为厂商号( MANUFACTURER ID); “ID0-ID15”为 FLASH 芯片的ID;“dummy”指该处可为任意数据;“D0~D7” 为 FLASH 内部存储矩阵的内容。
% V! J2 b9 f2 m看起来很复杂的样子,其实只要在需要执行相应操作时来查这个表,只要能够理解这些指令的使用方法,FLASH就算学会了。
. s0 q: y* M/ t7 R0 m6 v2 U3 A
例如:要知道FLASH的ID,那就在指令中找对应的取ID指令“JEDEC ID”,仔细解读这个指令 r3 o/ O) x& n( E) j8 j4 W- g" Y: g
+ |. v% ?1 @0 ^! f
* ]: f6 j+ q! n) U+ }- O( J
可以看出对应的指令代码为“9F”,后面的三个字节带括号,代表这三个字节就是FLASH向STM32发送的数据,即这三个字节就是FLASH的ID,然后使用SPI进行读取就可以了。. A8 }8 j9 N) y
5 W5 [1 ?* Y" e# t- Y我们一般是将这些指令宏定义在头文件中,便于使用:8 s3 ~$ B3 B) N% v
- #define W25X_WriteEnable 0x06 6 Y+ v; B( o1 b
- #define W25X_WriteDisable 0x04
% \$ |1 P3 R1 I, Z: u' P - #define W25X_ReadStatusReg 0x05 # b4 c+ P) {# Y1 N8 ^3 M' Y b0 W
- #define W25X_WriteStatusReg 0x01
% k o( R# v& ?2 l9 k - #define W25X_ReadData 0x03
! n* c% w! W' t W* g; J3 E - #define W25X_FastReadData 0x0B
8 z+ h' O( i( r7 T! f& b - #define W25X_FastReadDual 0x3B
2 I" u4 `- ?' {9 M; G7 G8 E - #define W25X_PageProgram 0x02
9 e0 w, s5 M! q" o' O* F: E - #define W25X_BlockErase 0xD8 & o7 k! F/ b) E. I2 u0 w; B+ T
- #define W25X_SectorErase 0x20 3 S! s6 B( o- O. ]
- #define W25X_ChipErase 0xC7 : U6 Q8 d# ?6 ^4 ?& Z# a4 I
- #define W25X_PowerDown 0xB9 ; Z: K5 |* u, z0 P4 ^& T$ w9 `- S$ }
- #define W25X_ReleasePowerDown 0xAB 8 m+ ]* |# \3 y# t- h1 n! H
- #define W25X_DeviceID 0xAB
& i0 d1 c1 u# J - #define W25X_ManufactDeviceID 0x90 # u& s3 V- ^* `
- #define W25X_JedecDeviceID 0x9F
; G5 h5 _7 h% J+ d7 d; F* M' I& e - & o( p2 J, g9 s1 k& g. j
复制代码 8 E6 Y( Z7 j. c/ N
FLASH内部存储结构
# @$ Y Y5 _# r7 L- ~FLASH的存储矩阵如图:其内存分为128块,每一块都有16个扇区,每个扇区大小为4KB,擦除数据的时候是以扇区为基本单位的。' r5 a# z( U2 v' p; H% s! D, h
, K# {3 k" i/ O0 r E
. y9 a0 u9 p- n0 B: k
/ v3 y, x5 U) u代码讲解( U! M3 h; v9 p; C! A- r
代码都是博主亲手写出来的,可以运行。
9 |; Q4 J. _* ^ s6 ` I7 W* v代码部分会用到SPI的代码,关于SPI的说明之前整理过:SPI详解
* T' U& v. w9 b7 a% M5 T& h! I
- T4 Z a: _" h5 U7 Q D J, c读取芯片ID
& X0 w) k& g! d! `* c# {4 J8 p- /************************读取芯片ID*************************************/" q' ?) t$ U7 D) B, |
- uint32_t FLASH_ReadID(void)
! }9 g$ C5 \# G* V - {# G: z5 g9 m/ I& z7 K- z A
- uint32_t temp,temp1,temp2,temp3;
. b1 w2 V/ N1 o( v; p - SPI_NSS_Begin();% W& {) r9 z/ h) G" F, d. M
- /* 发送取ID指令 */
# z9 x. s8 H7 W - SPI_SendData( W25X_JedecDeviceID);% c L% p( s- i' ?, }1 D0 K0 t
- /* FLASH会连续发送三个字节数据 */# R1 e1 r$ j% J- s7 w
- temp1=SPI_SendData(Dummy_Byte); [4 b& Z& U3 f) z% Y
- temp2=SPI_SendData(Dummy_Byte);
# n! I: _) B C5 L! R# l - temp3=SPI_SendData(Dummy_Byte);% ^# I( Y" |9 J( m1 I0 R J. q
- SPI_NSS_Stop();
' n! M7 y4 F5 u. u, f - /* 高为先行,将三个字节整理在一起 */
) D3 }3 B. s- }& D, b. d& N7 ` - temp=temp1<<16 | temp2<<8 | temp3;8 a- U. f9 s# f, Q# l2 ?5 @
- return temp;
/ B3 Q; L% P w3 X- t" X8 B$ {' e+ F) d - }
8 z9 Q; @, f$ B; A: Q8 ?- H5 ^
复制代码
5 A8 \+ N \' z0 I# t8 l l由于SPI是全双工通信,所以接收数据和发送数据用的是同一个函数
O: }: H0 a) v5 L! F) n% w
7 ^6 P' D% ~9 z+ v/ T5 V, H发送写使能信号0 W, ?5 a% |, z
- /****************发送写使能信号****************************/
) }: _* I/ K3 S3 ]/ ~ - void FLASH_WriteEnable(void)) i( }% w% u2 k0 t Z' c7 Y
- {3 ^- a( B" T9 m. ^& ~$ h
- SPI_NSS_Begin();
/ t0 J9 j' y, A4 T( b; w1 r - /* 发送相应的指令代码 */* R( U' C9 k* ~4 ?" e
- SPI_SendData(W25X_WriteEnable);: n3 u* S* u$ G( s; L
- SPI_NSS_Stop();
. q5 u0 C# R) i0 J" p+ n5 B - }
3 G* t( }- S d7 _5 x% p! ?
复制代码 , L4 w3 T7 G" W0 x N6 I2 s" o2 S( ?
在向FLASH中执行写操作之前,都要进行写使能操作,通过发送写使能指令到FLASH来实现" U6 o2 A$ f) v$ B; e9 h- }+ X* ]
! a% d# a+ Z* H5 M等待FLASH不忙
( s7 S2 ]: M( D I- /******************等待,直到不忙***********************************/6 L4 D' v) O3 e1 N, Z, V8 w8 J8 ?0 i
- void FLASH_WaitBusy(void)5 w% h' B' {0 M3 g$ i
- {
7 l7 z$ c" c- h0 e: E - uint8_t StatusReg=0x01;
1 F8 K F6 x; `2 G - SPI_NSS_Begin();
* X8 {' T# K/ k: o0 ~2 {+ @ - /* 读取状态寄存器中的数据,判断忙标志0x01位 置位代表忙 */6 _- ]9 H0 j G# ]. L* O3 M
- SPI_SendData(W25X_ReadStatusReg); 1 W" x" W" ]5 u' w
- /* 只读取状态寄存器的BUSY位,即第一位 */0 N0 w B/ S/ G% V5 z! ?
- while((StatusReg & 0x01) == 1) ) t" f+ x/ `$ a) L, [0 n' O. R$ q
- StatusReg=SPI_SendData(Dummy_Byte);
- ~1 |& _+ V( M6 P - SPI_NSS_Stop();
! ~ C; `: o3 I9 B - }- _8 @3 m0 b9 P# l5 T4 c
; ?4 Y" A7 F/ U P- * u8 T+ H$ b8 G& W/ E* O; I8 q! a
复制代码
* V2 C1 d7 s: z" R( F% ~# nFLASH在通讯的过程中需要一定的时间来执行操作,在这期间,传输数据是无效的,因为FLASH忙着呢,所以我们就要有一个函数来专门等!等到FLASH不忙了,再进行通讯,那怎么等呢?FLASH不忙了会给出一个信号——将状态寄存器的BUSY位重置(也就是0),所以我们需要不断的来检测状态寄存器中的BUSY位是否置位,利用读取寄存器状态的指令来获取状态寄存器当下的状态,然后根据寄存器的BUSY位(第1位)来判断FLASH是否处于忙碌状态。( ~: \8 j2 \- L- b1 a
简单来说,这就是个延时函数,延时直到FLASH空闲,可以进行下一步传输。
0 C3 K0 ^ b* a! O. s
. Y8 I9 P0 N. L, y6 {9 l: G# C4 z, k4 G3 W0 w9 ]+ ]$ F
擦除扇区
( T+ y% r( B' ?4 m2 R- /******擦除扇区的内容,切记地址要对其到4kB,每个扇区的大小都是4KB********/
/ ? r. n3 v4 Y6 \2 p( M+ T - void FLASH_SectorErase(uint32_t addr)
b. W" h2 C& s% t - {
/ r$ _2 ~& @$ L - /* 开始的时候要发送写使能信号*/, R# X7 v) c1 _ ]6 u
- FLASH_WriteEnable();: g1 u; y7 {4 p
- SPI_NSS_Begin();' C: z: x3 P8 c0 H
- /* 发送扇区擦除命令 */
6 R: ^: S. L+ j G - SPI_SendData(W25X_SectorErase);
& @" U7 C Y- k' s - /* 发送扇区的地址,高位先行 */
$ l6 K. ~: e5 F - SPI_SendData((addr & 0xff0000) >> 16);( |( m" B) v G1 m
- SPI_SendData((addr & 0xff00) >> 8);
( K+ i; y6 _; K: ^ L - SPI_SendData(addr & 0xff);2 |' U' h5 N; s& r+ U
- SPI_NSS_Stop();
- _ U* z! f7 t A0 y3 D0 t - /* 最后也要等待FLASH处理完这次的信号再退出 */
6 f- l% o+ d- I1 l: S7 @ - FLASH_WaitBusy();5 v& {5 k, q; l3 @, V9 |5 v# D
- }
' I+ J" a( M- H+ v3 b* _* L) H
5 B: |+ A3 [1 g L. \
复制代码
1 H1 F3 C5 f% h4 }* i; n扇区的擦除之前要发送一个写使能信号,先发送擦除指令,然后发送要擦除扇区的地址(分三个字节发出去),高位先行。7 |! V" |) S3 y/ ^, n7 ]0 X
扇区上的内容不是1就是0,擦除的过程就是写1的过程(将一个扇区全部写1),因为在写入数据的时候,可以将1写为0,但不能将0写为1.) @6 k, M% B! t# F# x
: l* o$ D8 d1 o% j9 S8 s
! ?# G- y! b' r8 s5 f& Z A写入数据3 R X# N$ E3 k$ I" S) _+ C8 b, |4 m
- /************按页写入数据,但写入之前要进行擦除***********/
3 L$ a, O4 {) k - void FLASH_PageWrite(uint32_t addr , uint8_t* pBuffer ,uint8_t size)3 v5 m! b W+ P* t- b6 F: T
- {. f0 n2 [5 r4 u$ Z9 [
- /* 开始的时候要发送写使能信号 */* T: A- n& t8 Y& @9 L( \
- FLASH_WriteEnable();, U1 ~1 D8 w( J
- SPI_NSS_Begin();' M+ j3 N' s+ k9 x
- /* 发送页写入命令 */
" X* q* ^$ q* y8 m. G9 f - SPI_SendData(W25X_PageProgram);3 n6 ]5 T/ c7 N: [* P. k' O2 n
- /* 发送写入的地址,高位先行 */
* u1 \/ Q* S" J0 L- M8 D - SPI_SendData((addr & 0xff0000) >> 16);: `, {. L7 Y E3 r' O: p
- SPI_SendData((addr & 0xff00) >> 8);
+ Q, W$ ~+ A, _ - SPI_SendData(addr & 0xff);
# _1 A3 z5 F! E" m' }, w, ` - /* 逐位发送数据 */2 d7 T# H0 X: L: v- P1 W% A8 X
- while(size--)7 e/ R1 e7 o$ ~8 s8 Q
- {
# ~ Q r0 d3 y3 Z* L, y - SPI_SendData(*pBuffer); X6 a5 y7 Q) u8 ~+ s# A
- pBuffer++;
" w+ c5 Y0 L9 x5 N - }
5 y4 ~8 i: e! y5 `% ^/ X( s' W, L - SPI_NSS_Stop();
; }: M' O" K; d! s - /* 最后也要等待FLASH处理完这次的信号再退出 */
# W+ d/ X, V9 i2 M3 x: k - FLASH_WaitBusy();
9 g2 S1 S# n$ r G; H# O ^( R - }
& z+ V3 x. ? w$ s8 r9 s7 ] - - d( `9 u$ U0 I2 Q0 a5 c3 a ~
复制代码 3 X- T- l4 @$ g7 V7 V2 Z$ `
在执行写入数据的时候函数的参数有三部分:1 }+ c6 [- G% E! L. J; @
1.要写入的地址4 J( R1 ^) h! f1 n) C+ p' C
2.要写入数据的首地址# B/ C, n$ l I
3.要写入数据的大小6 X* `1 I# [" h: |4 m, a
函数在执行的过程中,首先发送一个写使能信号,然后发送写数据指令,紧接着发送数据要写入的地址,然后就是逐位发送数据了,函数最后等FLASH处理完这次操作再退出。
# L6 ^2 W( W& i) @2 l# w* E9 x0 d" z; j1 O# i T
4 k, n# d$ c6 M8 J; T$ H+ ?( ^
读取数据" K5 s! J9 c- r% b( z" O
- /**********************读取指定地址、指定长度的数据******************/
% C7 P' x4 N- a F2 X% A - /* 因为读取在了指针中,所以不需要返回值 */9 L9 [5 i! R+ x$ g/ E
- void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size)* N4 t8 p) G6 @- y* G+ ?. U
- {; D6 e* w5 r" s: l0 Y+ V( P2 I4 I/ V
- SPI_NSS_Begin();
+ T0 O: q- n% @9 V8 ]' e4 h - /* 发送读取命令 */
& L5 s0 i! G1 a8 U- n( F) L# l - SPI_SendData(W25X_ReadData);* s6 y2 U6 y/ H0 {
- /* 发送读取数据的地址,高位先行 */
! \$ F- _/ @$ P0 }7 q( z2 T& o5 C& t5 X - SPI_SendData((addr & 0xff0000) >> 16);4 z' R2 w9 n/ l3 R; q% F7 v) ~
- SPI_SendData((addr & 0xff00) >> 8);
: U; j3 u3 V5 Q( W5 B - SPI_SendData(addr & 0xff);
7 s# `3 o9 Y8 c! x1 p7 r - /* 逐位读取数据到指针上 *// Z2 [( I! c/ n( ], T
- while(size--)3 k+ o2 R" J2 U; v, g( R
- {0 j- t( |! {0 T" F) ?/ y" f
- *pBuffer=SPI_SendData(Dummy_Byte);1 a! M6 S- W9 V& Y+ G& f; D( h
- pBuffer++;
; U6 C5 j, H( s% F2 H - }
8 z- a6 X& Z! `$ A) | - SPI_NSS_Stop();
+ z/ D& a# w. N5 I: ^; h - }
" M" j$ j3 W( C/ H& Q, m - 0 g% u+ f: t+ a
复制代码 $ z, r; `$ ^0 J; t, [$ h2 G7 f6 y
在执行读出数据的时候函数的参数也有三部分:
1 G) |4 P# f4 G# N* X* O1.要读出的地址
* t; P" }" I# X! u! _+ L2.读出到指定地址
: O/ O9 S% ^, c7 N. [6 c* _; w6 N3.读出数据的大小
+ I2 Q; x) e# T. l& B% z# i" K4 {函数执行过程,首先发送读取指令(这时就不用发送写使能了),然后读取数据的地址,然后将数据逐位读取在固定地址中(地址最好是全局变量),使用时再从全局变量地址中获取数据。
8 Y9 F4 @/ O! g$ u- Z/ G! [9 a这里涉及到函数的返回值问题,具体分析链接:返回多个变量怎么办, {9 X1 F1 Y d
. c: u6 ?1 J, n( _注/ N Z9 M% a" I/ N
有一个问题当时困扰了博主一天,那就是发送和读取数据时,怎么把数据返回到主函数中,解决方法是,创建俩个全局变量数组,一个负责发送数据、另一个负责接收数据,这样就ok了+ O% r# m- W' s+ b* ]- ]# r
附上主函数
' Y( Q" p3 Y; Z0 p/ S: U- /**********************读取指定地址、指定长度的数据******************/ a a) |3 J2 l, Q$ ^" [) w
- /* 因为读取在了指针中,所以不需要返回值 */
- \/ H/ O0 J! }( u; @$ X- n/ o" ~ - void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size)6 G/ A( H" r/ B
- {
; F2 |- J7 t$ u# E. M9 V2 h+ C9 t - SPI_NSS_Begin();; M8 G" v3 j( T0 f9 r' G8 Z
- /* 发送读取命令 */- j1 m) \6 \* ~7 p8 j
- SPI_SendData(W25X_ReadData);8 U4 g* X1 Z! f! c/ g, o' l2 [
- /* 发送读取数据的地址,高位先行 */
/ `! C/ J' V, i( o4 m6 t2 ^8 S3 ~& E - SPI_SendData((addr & 0xff0000) >> 16);
8 {9 l, N' j2 l3 g - SPI_SendData((addr & 0xff00) >> 8);
% _ `% N# G V+ V/ h% v2 r: V - SPI_SendData(addr & 0xff);* \5 b) ~+ F3 _" \1 P' F+ K
- /* 逐位读取数据到指针上 */2 e4 ~6 d2 l* u, X$ e- v" v3 M
- while(size--)
7 B7 q: S& x4 w# W( a; f3 v8 v, ] - {
$ ~. M5 ]+ a5 L6 H7 x8 T3 j& G1 s - *pBuffer=SPI_SendData(Dummy_Byte);
3 O' a& m% }* s3 Q9 P( X - pBuffer++;: R6 I6 `+ G: Y; ?1 I* f& H
- }( \( b4 b. x; F! W
- SPI_NSS_Stop();
_' D1 w* M. {2 p- B0 | - }
4 d9 T4 A5 G3 z$ U - ( V# w; h& S& M$ p
复制代码
% d, X# _, j5 o6 u; {————————————————
* @% S3 b1 U$ f: s8 N' p# r v版权声明:Aspirant-GQ; m5 M6 m& L4 F7 G: ~) r: |, ^
! H* C+ |0 O- b/ B0 O8 ?7 C
如有侵权请联系删除( i: }. f7 B$ O
, R2 e- U- L+ H- I! t" }
0 j% m: K: Z# ~4 f d: L% ~( G0 t4 Z4 c- b& q6 L9 o
|