|
前言 本文主要介绍STM32的内部Flash擦除方式和擦除长文件的功能函数怎样编写。并且介绍一些注意事项,如只想擦除当前地址,却发现上下地址都出现了擦除等问题。阅读完本文可以使你能够正常的完成Flash擦除。并对擦除时会影响的地址大小有一个深入的认识,并在对页擦除时,页的起始地址和大小有所了解。
* }5 k3 Y; b, V" x/ _) t/ k( q9 s
介绍STM32 FLASH不同型号的 STM32,其 FLASH 容量也有所不同,最小的只有 16K 字节,最大的则达到了1024K 字节。本次实验选用的STM32 开发板是F103ZET6,其 FLASH 容量为 512K 字节,属于大容量产品(另外还有中容量和小容量产品),大容量产品的闪存模块组织如图 所示:
2 T6 Z' l5 W& |. I9 w
& D5 M) [% Y2 W% X4 }# a
STM32 的闪存模块由:主存储器、信息块和闪存存储器接口寄存器等 3 部分组成。
- Q v( i F6 U6 H& N主存储器,该部分用来存放代码和数据常数(如 const 类型的数据)。对于大容量产品,其被划分为 256 页,每页 2K 字节。注意,小容量和中容量产品则每页只有 1K 字节。从上图可以看出主存储器的起始地址就是0X08000000, B0、B1 都接 GND 的时候,就是从 0X08000000开始运行代码的。
E W9 X% I% l( [/ Y5 A8 @& F信息块,该部分分为 2 个小部分,其中启动程序代码,是用来存储 ST 自带的启动程序,用于串口下载代码,当 B0 接 V3.3,B1 接 GND 的时候,运行的就是这部分代码。用户选择字节,则一般用于配置写保护、读保护等功能。
- ^! M8 h" Y2 o( K: ]" z, l闪存存储器接口寄存器,该部分用于控制闪存读写等,是整个闪存模块的控制机构。对主存储器和信息块的写入由内嵌的闪存编程/擦除控制器(FPEC)管理;编程与擦除的高电压由内部产生。
7 R2 w; R! M" w+ q4 c) [# X' {' G/ m在执行闪存写操作时,任何对闪存的读操作都会锁住总线,在写操作完成后读操作才能正确地进行;既在进行写或擦除操作时,不能进行代码或数据的读取操作。
q) _# d6 Y# U- J闪存的编程和擦除STM32 的闪存编程是由 FPEC(闪存编程和擦除控制器)模块处理的,这个模块包含 7 个
1 Z2 z' A' t' {: M# i+ u9 {32 位寄存器,他们分别是: FPEC 键寄存器(FLASH_KEYR) 选择字节键寄存器(FLASH_OPTKEYR) 闪存控制寄存器(FLASH_CR) 闪存状态寄存器(FLASH_SR) 闪存地址寄存器(FLASH_AR) 选择字节寄存器(FLASH_OBR) 写保护寄存器(FLASH_WRPR) `: k p3 Y+ J" b/ p( }
STM32 复位后,FPEC 模块是被保护的,不能写入 FLASH_CR 寄存器;通过写入特定的序列到 FLASH_KEYR 寄存器可以打开 FPEC 模块,只有在写保护被解除后,我们才能操作相关寄存器。
5 z, M2 D$ |' u: I+ aSTM32 闪存的编程每次必须写入 16 位(不能单纯的写入 8 位数据哦!),当 FLASH_CR 寄存器的 PG 位为’1’时,在一个闪存地址写入一个半字将启动一次编程;写入任何非半字的数据,FPEC 都会产生总线错误。在编程过程中(BSY 位为’1’),任何读写闪存的操作都会使 CPU暂停,直到此次闪存编程结束。* ]% K, Y) K2 r4 Z0 G, A- q W3 z
同样,STM32 的 FLASH 在编程的时候,也必须要求其写入地址的 FLASH 是被擦除了的(也就是其值必须是 0XFFFF),否则无法写入,在FLASH_SR 寄存器的 PGERR 位将得到一个警告。 F1 h; e' c P1 r
在我们日常的开发中STM32的Flash擦除最常用的就是页擦除,所以我们在这里着重介绍一下页擦除。
4 ~ S8 r( `- G! |- p8 sSTM32 的页擦除顺序为: 检查 FLASH_CR 的 LOCK 是否解锁,如果没有则先解锁 检查 FLASH_SR 寄存器的 BSY 位,以确认没有其他正在进行的闪存操作 设置 FLASH_CR 寄存器的 PER 位为’1’ 用 FLASH_AR 寄存器选择要擦除的页4 ^$ O j; ]8 r& @" [5 t$ [" D
-设置 FLASH_CR 寄存器的 STRT 位为’1’ 等待 BSY 位变为’0’ 读出被擦除的页并做验证0 G& A5 V* ?; c$ y9 J
该寄存器我们本章只用到了它的 LOCK、STRT、PER 和 PG 等 4 个位。+ ^% P2 A( T, c; |
LOCK 位,该位用于指示 FLASH_CR 寄存器是否被锁住,该位在检测到正确的解锁序列后,硬件将其清零。在一次不成功的解锁操作后,在下次系统复位之前,该位将不再改变。3 o9 \0 @- u$ M
STRT 位,该位用于开始一次擦除操作。在该位写入 1 ,将执行一次擦除操作。
* Z. ]# u) G, o# K; nPER 位,该位用于选择页擦除操作,在页擦除的时候,需要将该位置1。1 I9 J+ v1 a; J
PG 位,该位用于选择编程操作,在往 FLASH 写数据的时候,该位需要置1。
, e$ u/ i5 U! F/ S0 f: Y/ N Flash擦除的标准库函数解锁函数:void FLASH_Unlock(void);6 ]! Q" P$ q6 e3 r! ~; Z! l
对 FLASH 进行写操作前必须先解锁,解锁操作也就是必须在 FLASH_KEYR 寄存器写入特定的序列,固件库函数实现很简单:只需要直接调用 FLASH_Unlock();即可。 锁定函数:void FLASH_Lock(void);8 a$ X) }# z. k9 h7 e7 K$ ^
有解锁当然就有上锁,为了保护Flash,读写和擦除全部需要的Flash后需要上锁,只需要调用:FLASH_Lock(); 擦除函数6 C) T/ G4 d! v2 c1 `
固件库我们主要使用两个 FLASH 擦除函数:2 O' ?0 k2 g. x5 n0 x
FLASH_Status FLASH_ErasePage(uint32_t Page_Address);! r- {% j( ^% }: d& C% Z
FLASH_Status FLASH_EraseAllPages(void);" j% H% c! b) f6 \
顾名思义,第一个函数是页擦除函数,根据页地址擦除特定的页数据。
. o$ f: w% C3 b# X, r* L第二个函数是擦除所有的页数据。 获取 FLASH 状态
- ]- [' W: }) S6 y# |主要是用的函数是:FLASH_Status FLASH_GetStatus(void);
. p7 y6 r* y" z' [' C0 B* g5 Q返回值是通过枚举类型定义的,分别为:$ \, E/ x9 r/ P. d9 [$ q/ Q% N+ f" w
FLASH_BUSY = 1,//忙
' a2 ^; J( E7 M4 YFLASH_ERROR_PG,//编程错误* Z9 C- E5 N2 l& y) n6 d
FLASH_ERROR_WRP,//写保护错误
/ p" ~: d( n& [9 nFLASH_COMPLETE,//操作完成5 ^. q; Q$ F" S+ G, I
FLASH_TIMEOUT//操作超时 等待操作完成函数4 K& I* a4 S2 R9 U. @
在执行闪存写操作时,任何对闪存的读操作都会锁住总线,在写操作完成后读操作才能正确地进行;既在进行写或擦除操作时,不能进行代码或数据的读取操作。1 ~5 R: @0 g% H$ ?
所以在每次操作之前,我们都要等待上一次操作完成这次操作才能开始。使用的函数是:FLASH_Status FLASH_WaitForLastOperation(uint32_t Timeout)入口参数为等待时间,返回值是 FLASH 的状态,这个很容易理解,这个函数本身我们在固件库中使用得不多,但是在固件库函数体中间可以多次看到。 ! D$ [. `1 N0 v. r
软件设计
: `* X5 ] y4 @: r直接使用固件库函数擦除当前地址所在的内容直接使用固件库擦除选定的地址的内容,每次会擦除选定地址的当前页。+ d6 r) d9 @5 d, i
注意:这里有一个很容易混淆的点,擦除当前页,并不是擦除从这个地址之后的一页,而是STM32规定的该地址所在的页。不知道的可以看Flash的图,也可以自己计算,其实就是0x0800 0000 每次加2k(中小容量加1K),比如0x0800 0810,此时就会擦除0x0800 0800-0x0800 0FFF的所有内容,而不是0x0800 0810到0x0800 100F的内容。 7 j9 t% `1 N( z* U+ |% ^( F" U) k
9 V7 Z6 ^! {4 ^5 Q+ c5 s$ Z
- FLASH_Unlock(); //解锁
; a, t) o3 {, s) | M6 K - FLASH_ErasePage(0X08003A98);% v% x# o9 S3 t3 N
- FLASH_Lock();//上锁
复制代码此语句就可擦除0x0800 3A98所在页的全部内容。
. B5 h: A: a: U F( e" m& r可能有人疑惑,难道不能擦除单个字节的内容嘛,很遗憾,确实不能,并非是程序的问题,而是硬件设计,就是按页擦除。 8 b& Q3 |( k. `, E* h( L
擦除对应地址和大小的Flash我们在开发中,不可能每次都计算用擦除多少页的地址,或者要擦除的范围是多少,这里我们就可以编写一个函数来帮我们实现。 - m+ _ R, N) Z0 y: ?
- void STMFLASH_Erase(u32 EraseAddr,u16 NumToErase) . o0 F; E9 r4 X
- {
# D; ?% B; N0 b" p1 b. b% ~ - u32 secpos; //扇区地址7 [7 I9 W6 F: C( c) r
- u16 secoff; //扇区内偏移地址(16位字计算)& R& q* p2 P' R8 y! V3 J
- u16 secremain; //扇区内剩余地址(16位字计算) . L) @8 N" U2 u3 U1 ]0 X
- u16 i;
% N! `0 m2 z6 J ]+ _" I. u1 b+ R - u32 offaddr; //去掉0X08000000后的地址9 F' O# I# p: Z( r/ h$ B
- if(EraseAddr<STM32_FLASH_BASE||(EraseAddr>=(STM32_FLASH_BASE+1024*STM32_FLASH_SIZE)))return;//非法地址
) a. o- E$ O" A$ c; M+ X4 }: R - FLASH_Unlock(); //解锁! ?- X) o$ \/ `, j0 P z
- offaddr=EraseAddr-STM32_FLASH_BASE; //实际偏移地址.2 Z7 K% D3 \+ _. \# P
- secpos=offaddr/STM_SECTOR_SIZE; //扇区地址 0~127 for STM32F103RBT6; k3 z* b" J( [- F7 j5 p$ z* [
- secoff=(offaddr%STM_SECTOR_SIZE)/2; //在扇区内的偏移(2个字节为基本单位.)6 Z- l- T; ]9 C+ F& {
- secremain=STM_SECTOR_SIZE/2-secoff; //扇区剩余空间大小 / b; n; j7 I7 {7 ^" \% s
- if(NumToErase<=secremain)1 j6 S# v1 c& K6 {& R4 ]# w
- secremain=NumToErase;//不大于该扇区范围; u4 l, T c8 y4 z
- while(1)
: `+ R1 {" e# h! `' } - { FLASH_ErasePage(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE);//擦除这个扇区
" j+ X8 X6 M1 r* E - if(NumToErase==secremain)
0 `: z: U+ c2 j# i1 E4 c3 u0 O - break; //擦除结束了
3 r9 j# K; }5 K6 c: x0 ` - else //擦除未结束$ R/ p, Z" X! u9 o3 u# d
- {8 I8 S" @7 h# y* Y# ^
- secpos++; //扇区地址增1
( A6 I2 S6 r, L9 p- e; g6 f# x - secoff=0; //偏移位置为0
( Y8 ? F( V, X5 q0 c - EraseAddr+=secremain; //地址偏移 ( A0 Z; [6 _' O) R! i5 a6 l
- NumToErase-=secremain; //字节(16位)数递减
7 G& B9 F) t! G& [ - if(NumToErase>(STM_SECTOR_SIZE/2))- e4 M3 A1 _! ~, f% h/ k
- secremain=STM_SECTOR_SIZE/2;//下一个扇区还是擦除不完
) J0 Z2 T3 y& _2 t - else secremain=NumToErase;//下一个扇区可以擦除完了6 U* Y5 p z w* u8 A
- }
$ }% h+ Y% W0 A6 C0 \& D K - };
3 ?4 p3 g5 ~5 v! T5 B5 v - FLASH_Lock();//上锁4 r& y- W& b- O- p k
- }
复制代码 & U! w2 Z, F5 c# D, V
调用本函数就可以擦除EraseAddr地址开始,NumToErase大小的Flash了。注意NumToErase是16位,也就是半字(两个字节),如果你打算擦除1980字节的程序,NumToErase就应该是990。
$ m a5 f8 M- v2 a4 v& _/ E6 W9 Q因为函数中已经编写了解锁和上锁,所以就不用在使用时再加了,直接调用STMFLASH_Erase(0X0x0800 0810,2049);& r' V* V D/ y0 Q' z' S
这一句程序可以实现擦除0x0800 0810开始4098字节所在页的内容。
7 b4 | P. q, H$ z" j# ^1 ^注意,这里的擦除也是所在页,并不是正好擦除该地址后面4098字节的内容。如果我们填入的起始地址不是STM32设定的某页的起始地址,那么擦除的时候,就会也把前面的一部分Flash内容进行擦除,比如STMFLASH_Erase(0X0x0800 0810,2049); 就是擦除了0x0800 0800-0x0800 0FFF(第二页)和0x0800 1000-0x0801 17FF(第三页)的所有内容。
6 s/ {6 `* R: d& ~可以看出来,虽然我们只想擦除0X0x0800 0810后4098个字节的全部内容,但我们不可避免的擦除了4K的全部内容,编写程序时,需要注意。
( T2 S4 L, t1 Z+ X 如何查看Flash的内容Keil的软件调试中,有专门可以查看所连接的板子的Flash的内容。 首先我们点击调试按钮,如下图
: K8 X" o! K& o* J* M ?8 a; z, Y& J
。
( p( Y9 D2 L0 W
( J P1 K4 X2 P# \+ L
之后我们在View中找到memory Windows,选择memory1。之后我们就可以看到右下角出现Flash的内容,第一次打开是空白,因为还没有选定地址,会出现下图内容。 & C( t% ?& I- S2 z# Q
" M3 _0 A' O5 y1 ^( L
我们在地址处填写想查看的地址,比如查看0x0800 3A98,输入完成后点击回车,就会出现Flash地址对应的内容。 7 r+ V. R0 u Z- R6 S! F
3 @" J$ S1 o# u
转载自:跋扈洋
' C0 Q w" L T( k |