(1)实验平台:
普中STM32F103 朱雀、玄武开发板
https://item.taobao.com/item.htm?id=620302685024(2)资料下载 :普中科技-各型号产品资料下载链接
上一章我们介绍了 STM32F1 的 SPI 与外部 FLASH 通信, 实现了外部 FLASH数据的读写操作, 这一章我们来学习下 STM32F1 内部的 FLASH, 通过内部 FLASH实现数据读写操作, 同上一章实验效果一样, 内部 FLASH 保存的数据也具有掉电不丢失功能。 本章要实现的功能是: 使用 KEY_UP 和 KEY1 键控制内部 FLASH 的写入和读取, 并将数据显示在 TFTLCD 和串口助手上, 同时控制 DS0 指示灯不断闪烁, 提示系统正常运行。 本章分为如下几部分内容:
[42.1 STM32F1 内部 FLASH 介绍](#42.1 STM32F1 内部 FLASH 介绍)
[42.2 内部 FLASH 操作步骤](#42.2 内部 FLASH 操作步骤)
[42.3 硬件设计](#42.3 硬件设计)
[42.4.1 FLASH 读数据函数](#42.4.1 FLASH 读数据函数)
[42.4.2 FLASH 写数据函数](#42.4.2 FLASH 写数据函数)
[42.4.3 主函数](#42.4.3 主函数)
[42.5 实验现象](#42.5 实验现象)
42.1 STM32F1 内部 FLASH 介绍
不同型号的 STM32, 其 FLASH 容量也有所不同, 最小的只有 16K 字节, 最大的则达到了 1024K 字节。 我们 STM32F1 开发板使用的芯片是 STM32F103ZET6,其 FLASH 容量为 512K 字节, 属于大容量芯片。 大容量产品的 Flash 模块组织结构如下图所示:

STM32F1 的闪存(Flash) 模块由: 主存储器、 信息块和闪存存储器接口寄存器等 3 部分组成。 下面我们就来介绍下这些组成部分:
①主存储器: 该部分用来存放代码和数据常数(如 const 类型的数据) 。对于大容量产品, 其被划分为 256 页, 每页 2K 字节。 注意, 小容量和中容量产品则每页只有 1K 字节。 从上图可以看出主存储器的起始地址就是0X08000000, BOOT0、 BOOT1 都接 GND 的时候, 就是从 0X08000000 开始运行代码的。
②信息块: 该部分分为 2 个小部分, 其中启动程序代码, 是用来存储 ST 自带的启动程序, 用于串口下载代码, 当 BOOT0 接 V3.3, BOOT1 接 GND 的时候,运行的就是这部分代码。 用户选择字节, 则一般用于配置写保护、 读保护等功能,这里我们不作介绍, 大家可以百度了解。
③闪存存储器接口寄存器: 部分用于控制闪存读写等, 是整个闪存模块的控制机构。
对主存储器和信息块的写入由内嵌的闪存编程/擦除控制器(FPEC)管理; 编程与擦除的高电压由内部产生。
在执行闪存写操作时, 任何对闪存的读操作都会锁住总线, 在写操作完成后读操作才能正确地进行; 既在进行写或擦除操作时, 不能进行代码或数据的读取操作。
下面我们就来看下如何对闪存进行读取、 编程和擦除。
(1) 闪存的读取
STM32F1 可通过内部的 I-Code 指令总线或 D-Code 数据总线访问内置闪存模块, 本章我们主要讲解数据读写, 即通过 D-Code 数据总线来访问内部闪存模块。 为了准确读取 Flash 数据, 必须根据 CPU 时钟 (HCLK) 频率和器件电源电压在 Flash 存取控制寄存器 (FLASH_ACR)中正确地设置等待周期数(LATENCY)。 当电源电压低于 2.1V 时, 必须关闭预取缓冲器。 Flash 等待周期与 CPU 时钟频率之间的对应关系, 如下图所示:

等待周期通过 FLASH_ACR 寄存器的 LATENCY2:0三个位设置。 系统复位后, CPU 时钟频率为内部 16M RC 振荡器, LATENCY 默认是 0, 即 1 个等待周期。 供电电压, 我们一般是 3.3V, 所以, 在我们设置 72Mhz 频率作为 CPU 时钟之前, 必须先设置 LATENCY 为 3, 否则 FLASH 读写可能出错, 导致死机。
STM23F1 的 FLASH 读取是很简单的。 例如, 我们要从地址 addr, 读取一个字(字节为 8 位, 半字为 16 位, 字为 32 位) , 可以使用如下方法来读取:
data=*(vu32*)addr;
将 addr 强制转换为 vu32 指针, 然后取该指针所指向的地址的值, 即得到了 addr 地址内的值。 类似的, 将上面的 vu32 改为 vu16, 即可读取指定地址的一个半字。 相对 FLASH 读取来说, STM32F1 FLASH 的写就复杂一点了, 下面我们介绍 STM32F1 闪存的编程和擦除。
(2) 闪存的编程和擦除
STM32 的闪存编程是由 FPEC(闪存编程和擦除控制器) 模块处理的, 这个模块包含 7 个 32 位寄存器, 他们分别是:
① FPEC 键寄存器(FLASH_KEYR)
② 选择字节键寄存器(FLASH_OPTKEYR)
③ 闪存控制寄存器(FLASH_CR)
④ 闪存状态寄存器(FLASH_SR)
⑤ 闪存地址寄存器(FLASH_AR)
⑥ 选择字节寄存器(FLASH_OBR)
⑦ 写保护寄存器(FLASH_WRPR)
其中 FPEC 键寄存器总共有 3 个键值:
RDPRT=0X000000A5
KEY1=0X45670123
KEY2=0XCDEF89AB
STM32 复位后, FPEC 模块是被保护的, 不能写入 FLASH_CR 寄存器; 通过写入特定的序列到 FLASH_KEYR 寄存器可以打开 FPEC 模块(即写入 KEY1 和KEY2) , 只有在写保护被解除后, 我们才能操作相关寄存器。
STM32 闪存的编程每次必须写入 16 位(不能单纯的写入 8 位数据) , 当FLASH_CR 寄存器的 PG 位为' 1' 时, 在一个闪存地址写入一个半字将启动一次编程; 写入任何非半字的数据, FPEC 都会产生总线错误。 在编程过程中(BSY 位为' 1' ), 任何读写闪存的操作都会使 CPU 暂停, 直到此次闪存编程结束。
同样, STM32 的 FLASH 在编程的时候, 也必须要求其写入地址的 FLASH 是被擦除了的(也就是其值必须是 0XFFFF) , 否则无法写入, 在 FLASH_SR 寄存器的 PGERR 位将得到一个警告。
STM32 的 FLASH 编程过程如下:

从上图可以得到闪存的编程顺序如下:
① 检查 FLASH_CR 的 LOCK 是否解锁, 如果没有则先解锁
② 检查 FLASH_SR 寄存器的 BSY 位, 以确认没有其他正在进行的编程操作
③ 设置 FLASH_CR 寄存器的 PG 位为' 1'
④ 在指定的地址写入要编程的半字
⑤ 等待 BSY 位变为' 0'
⑥ 读出写入的地址并验证数据
前面提到, 我们在 STM32 的 FLASH 编程的时候, 要先判断缩写地址是否被擦除了, 所以我们有必要再介绍一下 STM32 的闪存擦除, STM32 的闪存擦除分为两种: 页擦除和整片擦除。 页擦除过程如下所示:

从上图可以看出, STM32 的页擦除顺序为:
①检查 FLASH_CR 的 LOCK 是否解锁, 如果没有则先解锁
②检查 FLASH_SR 寄存器中的 BSY 位, 确保当前未执行任何 FLASH 操作
③设置 FLASH_CR 寄存器的 PER 位为' 1'
④用 FLASH_AR 寄存器选择要擦除的页
⑤设置 FLASH_CR 寄存器的 STRT 位为' 1'
⑥等待 BSY 位变为' 0'
⑦读出被擦除的页并做验证
我们只用到了 STM32 的页擦除功能, 整片擦除功能我们在这里就不介绍了。通过以上了解, 我们基本上知道了 STM32 闪存的读写所要执行的步骤了, 由于篇幅限制, 本章并没有 STM32F1 内部 FLASH 相关寄存器进行介绍, 大家可以参考"\8--STM32 相关资料\STM32F10xxx 闪存编程参考手册" 内容, 里面有详细的讲解
42.2 内部 FLASH 操作步骤
通过上面的介绍, 我们基本上知道了 STM32F1 闪存的读写所要执行的步骤了, 接下来, 我们就来介绍如何使用库函数对它进行操作。 这个也是在编写程序中必须要了解的。 具体步骤如下:(闪存 Flash 相关库函数在 stm32f10x_flash.c和 stm32f10x_flash.h 文件中)
(1) 解锁和锁定
前面我们介绍了在对 FLASH 进行写操作前必须先解锁, 解锁操作也就是必须在 FLASH_KEYR 寄存器写入特定的序列(0X45670123 和 0XCDEF89AB) ,固件库提供了一个解锁函数, 其实就是封装了对 FLASH_KEYR 寄存器的操作。
解锁库函数是:
cpp
void FLASH_Unlock(void);
在对 FLASH 写操作完成之后, 我们要锁定 FLASH, 使用的库函数是:
cpp
void FLASH_Lock(void);
(2) 写操作
FLASH 解锁后, 我们就可以开始写操作, 固件库内提供了 3 个 FLASH 写函数:
cpp
FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data);
FLASH_Status FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data);
FLASH_Status FLASH_ProgramOptionByteData(uint32_t Address, uint8_t Data);
从函数名来看也很好理解这几个函数的功能, 分别是在对应的地址 Address内写入字, 半字, 用户选项字节。 这里需要说明, 32 位字节写入实际上是写入的两次 16 位数据, 写完第一次后地址+2, 这与我们前面讲解的 STM32 闪存的编程每次必须写入 16 位并不矛盾。 写入 8 位实际也是占用的两个地址了, 跟写入 16 位基本上没啥区别。 这些函数的内部实现过程, 实际就是按照我们介绍的编程步骤来实现的。 有兴趣的同学可以进入函数体看看, 这样会加深理解。
(3) 擦除操作
在对 FLASH 写操作的时候, 还会用到 FLASH 擦除操作, 固件库内也提供了 3个 FLASH 擦除函数:
cpp
FLASH_Status FLASH_ErasePage(uint32_t Page_Address);
FLASH_Status FLASH_EraseAllPages(void);
FLASH_Status FLASH_EraseOptionBytes(void);
对于前面两个函数比较好理解, 第一个函数是页擦除函数, 根据页地址擦除特定的页数据, 第二个函数是擦除所有的页数据, 第三个函数是擦除用户选择字节数据。
(4) 获取 FLASH 状态
在对 FLASH 进行读写及擦除操作时, 我们可能需要获取 FLASH 当前的状态,获取 FLASH 状态主要调用的函数是:
cpp
FLASH_Status FLASH_GetStatus(void);
返回值是通过枚举类型定义的:
cpp
typedef enum
{
FLASH_BUSY = 1,//忙
FLASH_ERROR_PG,//编程错误
FLASH_ERROR_WRP,//写保护错误
FLASH_COMPLETE,//操作完成
FLASH_TIMEOUT//操作超时
}FLASH_Status;
从这里面我们可以看到 FLASH 操作的几个状态。
(5) 等待操作完成
在执行闪存写操作时, 任何对闪存的读操作都会锁住总线, 在写操作完成后读操作才能正确地进行; 即在进行写或擦除操作时, 不能进行代码或数据的读取操作。 所以在每次操作之前, 我们都要等待上一次操作完成这次操作才能开始。使用的函数是:
cpp
FLASH_Status FLASH_WaitForLastOperation(uint32_t Timeout);
口参数为等待时间, 返回值是 FLASH 的状态, 这个很容易理解, 这个函数本身我们在固件库中使用得不多, 但是在固件库函数体中间可以多次看到。
(6) 读取 FLASH 指定地址数据
读取 FLASH 指定地址的数据函数固件库并没有提供, 因此这一步操作我们需要自己编写, 其实很简单, 通过 C 语言指针即可完成, 这里我们提供一个从指定地址读取一个字的函数:
cpp
//读取指定地址的半字(16 位数据)
//faddr:读地址(此地址必须为 2 的倍数!!)
//返回值:对应数据.
vu16 STM32_FLASH_ReadHalfWord(u32 faddr)
{
return *(vu16*)faddr;
}
通过以上几个步骤的操作, 我们就可以对 STM32F1 内部 FLASH 进行读写数据了
42.3 硬件设计
本实验使用到硬件资源如下:
(1) DS0 指示灯
(2) KEY_UP 和 KEY1 按键
(3) 串口 1
(4) TFTLCD 模块
(5) STM32F1 内部 FLASH
DS0 指示灯、 KEY_UP 和 KEY1 按键、 串口 1、 TFTLCD 模块电路在前面章节都介绍过, 这里就不多说, 至于 STM32F1 内部 FLASH 它属于 STM32F1 芯片内部的资源, 只要通过软件配置好即可使用。 DS0 指示灯用来提示系统运行状态, KEY_UP和 KEY1 按键用来控制内部 FLASH 数据的读写, TFTLCD 模块和串口 1 用来显示读写的数据。
42.4 软件设计
本章所要实现的功能是: 使用 KEY_UP 和 KEY1 键控制内部 FLASH 的写入和读取, 并将数据显示在 TFTLCD 和串口助手上, 同时控制 DS0 指示灯不断闪烁, 提示系统正常运行。 本章我们使用的是 STM32F1 的内部 FLASH, 程序框架如下:
(1) 编写 FLASH 读数据函数
(2) 编写 FLASH 写数据函数
(3) 编写主函数
前面我们已经介绍如何操作内部 FLASH, 这里并没有像以前章节那样要初始化, 因为我们只需要对对应的 FLASH 地址操作即可。 下面我们打开"\4--实验程序\1--基础实验\34-STM32 内部 Flash 实验" 工程, 在 APP 工程组中可以看到添加了 stm32_flash.c 文件(里面包含了内部 FLASH 驱动程序) , 在StdPeriph_Driver 工程组中添加了 stm32f10x_flash.c 库文件。 FLASH 操作的库函数都放在 stm32f10x_flash.c 和 stm32f10x_flash.h 文件中, 所以使用到内部FLASH 就必须加入 stm32f10x_flash.c 文件, 同时还要包含对应的头文件路径。
这里我们分析几个重要函数, 其他部分程序大家可以打开工程查看。
42.4.1 FLASH 读数据函数
在前面介绍 FLASH 操作步骤时, 我们就讲解如何读取 FLASH 地址内数据, 其实很简单, 使用指针操作即可, 为了能够在 FLASH 任意地址读取任意个数字数据,我们也对这个读取函数进行封装, 代码如下:
cpp
//从指定地址开始读出指定长度的数据
//ReadAddr:起始地址
//pBuffer:数据指针
//NumToWrite:半字(16 位)数
void STM32_FLASH_Read(u32 ReadAddr,u16 *pBuffer,u16 NumToRead)
{
u16 i;
for(i=0;i<NumToRead;i++)
{
pBuffer[i]=STM32_FLASH_ReadHalfWord(ReadAddr);//读取 2 个字节.
ReadAddr+=2;//偏移 2 个字节.
}
}
函数参数有 3 个, 第一个为要读取数据的起始地址, 第二个用来保存读取数据(通常使用一个数组来保存) , 第三个为要读取的数据半字个数, 注意: 这里读取的是半字, 即一个半字是 2 个字节, 所以函数内每读取一个半字, 地址就增加 2。
函数内调用了 STM32_FLASH_ReadHalfWord 函数, 这个函数其实就是前面我们介绍的读取地址内数据方法, 此函数代码如下:
cpp
//读取指定地址的半字(16 位数据)
//faddr:读地址(此地址必须为 2 的倍数!!)
//返回值:对应数据.
vu16 STM32_FLASH_ReadHalfWord(u32 faddr)
{
return *(vu16*)faddr;
}
42.4.2 FLASH 写数据函数
FLASH 有读操作, 当然也就是写操作, FLASH 的写操作比较复杂, 我们也对FLASH 写操作使用一个函数封装, 代码如下:
cpp
//从指定地址开始写入指定长度的数据
//WriteAddr:起始地址(此地址必须为2的倍数)
//pBuffer:数据指针
//NumToWrite:半字(16位)数(就是要写入的16位数据的个数.)
#if STM32_FLASH_SIZE<256
#define STM32_SECTOR_SIZE 1024 //字节
#else
#define STM32_SECTOR_SIZE 2048
#endif
u16 STM32_FLASH_BUF[STM32_SECTOR_SIZE/2];//最多是2K字节
void STM32_FLASH_Write(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite)
{
u32 secpos; //扇区地址
u16 secoff; //扇区内偏移地址(16位字计算)
u16 secremain; //扇区内剩余地址(16位字计算)
u16 i;
u32 offaddr; //去掉0X08000000后的地址
if(WriteAddr<STM32_FLASH_BASE||(WriteAddr>=(STM32_FLASH_BASE+1024*STM32_FLASH_SIZE)))return;//非法地址
FLASH_Unlock(); //解锁
offaddr=WriteAddr-STM32_FLASH_BASE; //实际偏移地址.
secpos=offaddr/STM32_SECTOR_SIZE; //扇区地址 0~127 for STM32F103RBT6
secoff=(offaddr%STM32_SECTOR_SIZE)/2; //在扇区内的偏移(2个字节为基本单位.)
secremain=STM32_SECTOR_SIZE/2-secoff; //扇区剩余空间大小
if(NumToWrite<=secremain)secremain=NumToWrite;//不大于该扇区范围
while(1)
{
STM32_FLASH_Read(secpos*STM32_SECTOR_SIZE+STM32_FLASH_BASE,STM32_FLASH_BUF,STM32_SECTOR_SIZE/2);//读出整个扇区的内容
for(i=0;i<secremain;i++)//校验数据
{
if(STM32_FLASH_BUF[secoff+i]!=0XFFFF)
break;//需要擦除
}
if(i<secremain)//需要擦除
{
FLASH_ErasePage(secpos*STM32_SECTOR_SIZE+STM32_FLASH_BASE);//擦除这个扇区
for(i=0;i<secremain;i++)//复制
{
STM32_FLASH_BUF[i+secoff]=pBuffer[i];
}
STM32_FLASH_Write_NoCheck(secpos*STM32_SECTOR_SIZE+STM32_FLASH_BASE,STM32_FLASH_BUF,STM32_SECTOR_SIZE/2);//写入整个扇区
}
else
STM32_FLASH_Write_NoCheck(WriteAddr,pBuffer,secremain);//写已经擦除了的,直接写入扇区剩余区间.
if(NumToWrite==secremain)
break;//写入结束了
else//写入未结束
{
secpos++; //扇区地址增1
secoff=0; //偏移位置为0
pBuffer+=secremain; //指针偏移
WriteAddr+=secremain; //写地址偏移
NumToWrite-=secremain; //字节(16位)数递减
if(NumToWrite>(STM32_SECTOR_SIZE/2))
secremain=STM32_SECTOR_SIZE/2;//下一个扇区还是写不完
else
secremain=NumToWrite;//下一个扇区可以写完了
}
}
FLASH_Lock();//上锁
}
此函数也有 3 参数, 意义同上一个读取数据函数一样, 用于指定地址写入指定长度的数据。 该函数的实现基本类似上一章的 EN25QXX_Flash_Write 函数,不过该函数使用的时候, 有 2 个地方需要注意:
①写入地址必须是用户代码区以外的地址。
如果你写入数据的地址在存储用户代码地址范围内, 那将导致你的代码被冲掉, 可想而知你运行的程序可能就被破坏, 从而很可能出现死机的情况。 所以建议大家使用该函数的时候, 写入地址定位到用户代码占用扇区以外的扇区, 比较保险。 通常选择后面几个扇区。
②写入地址必须是 2 的倍数, 即每次写入必须是 16 位。
42.4.3 主函数
编写好内部 FLASH 读写函数后, 接下来就可以编写主函数了, 代码如下:
cpp
#include "system.h"
#include "SysTick.h"
#include "led.h"
#include "usart.h"
#include "tftlcd.h"
#include "key.h"
#include "stm32_flash.h"
#define STM32_FLASH_SAVE_ADDR 0X08070000 //设置FLASH 保存地址(必须为偶数,且其值要大于本代码所占用FLASH的大小+0X08000000)
const u8 text_buf[]="www.prechin.net";
#define TEXTLEN sizeof(text_buf)
int main()
{
u8 i=0;
u8 key;
u8 read_buf[TEXTLEN];
SysTick_Init(72);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //中断优先级分组 分2组
LED_Init();
USART1_Init(115200);
TFTLCD_Init(); //LCD初始化
KEY_Init();
FRONT_COLOR=BLACK;
LCD_ShowString(10,10,tftlcd_data.width,tftlcd_data.height,16,"PRECHIN STM32F1");
LCD_ShowString(10,30,tftlcd_data.width,tftlcd_data.height,16,"www.prechin.net");
LCD_ShowString(10,50,tftlcd_data.width,tftlcd_data.height,16,"STM32_Flash Test");
LCD_ShowString(10,70,tftlcd_data.width,tftlcd_data.height,16,"K_UP:Write KEY1:Read");
FRONT_COLOR=RED;
LCD_ShowString(10,130,tftlcd_data.width,tftlcd_data.height,16,"Write:");
LCD_ShowString(10,150,tftlcd_data.width,tftlcd_data.height,16,"Read :");
while(1)
{
key=KEY_Scan(0);
if(key==KEY_UP_PRESS)
{
STM32_FLASH_Write(STM32_FLASH_SAVE_ADDR,(u16*)text_buf,TEXTLEN);
printf("写入数据为:%s\r\n",text_buf);
LCD_ShowString(10+6*8,130,tftlcd_data.width,tftlcd_data.height,16,(u8 *)text_buf);
}
if(key==KEY1_PRESS)
{
STM32_FLASH_Read(STM32_FLASH_SAVE_ADDR,(u16 *)read_buf,TEXTLEN);
printf("读取数据为:%s\r\n",read_buf);
LCD_ShowString(10+6*8,150,tftlcd_data.width,tftlcd_data.height,16,read_buf);
}
i++;
if(i%20==0)
{
LED1=!LED1;
}
delay_ms(10);
}
}
主函数实现的功能很简单, 首先调用之前编写好的硬件初始化函数, 包括SysTick 系统时钟, 中断分组, LED 初始化等。 然后在 TFTLCD 上显示一些提示信息。 最后进入 while 循环, 调用 KEY_Scan 函数, 不断检测 KEY_UP 和 KEY1 按键是否按下, 如果 KEY_UP 键按下, 就将 text_buf 数组内的数据从内部 FLASH 的STM32_FLASH_SAVE_ADDR 地址处开始写入, STM32_FLASH_SAVE_ADDR 是我们定义的一个宏, 地址为 0X08070000。 这里需要注意: 写入的起始地址必须为偶数,且所在扇区要大于代码所占用到的扇区, 否则写操作的时候可能会导致擦除整个扇区, 从而引起部分程序丢失引起死机。 如果 KEY1 键按下, 就从STM32_FLASH_SAVE_ADDR 地址处读取数据, 保存在 read_buf 数组内, FLASH 写入和读取的数据在 TFTLCD 上显示, 并可通过串口打印输出, 同时 DS0 指示灯会间隔 200ms 闪烁, 提示系统正常运行。
42.5 实验现象
将工程程序编译后下载到开发板内, 可以看到 DS0 指示灯不断闪烁, 表示程序正常运行。 当按下 KEY_UP 键, 写入到数据显示在 TFTLCD 上, 当按下 KEY1 键将写入的数据读取出来, 同时显示在 TFTLCD 上。 如果想在串口调试助手上看到输出信息, 必须设置好串口助手, 前面很多章节都介绍, 这里不多说。 实验现象如下图所示:

实验说明: 如果自己编写程序操作内部 FLASH 时一定要小心, 避免把芯片锁死。 如果芯片锁死的话是很麻烦的。
课后作业
(1) 使用内部 FLASH 实现上一章"课后作业" 的功能。