第四十五章 FLASH模拟EEPROM实验
STM32本身没有自带EEPROM,但是STM32具有IAP(在应用编程)功能,所以我们可以把它的FLASH当成EEPROM来使用。本章,我们将利用STM32内部的FLASH来实现第三十六章实验类似的效果,不过这次我们是将数据直接存放在STM32内部,而不是存放在NOR FLASH。
本章分为如下几个小节:
45.1 STM32 FLASH简介
45.2 硬件设计
45.3 软件设计
45.4 下载验证
45.1 STM32 FLASH简介
不同型号的STM32,其FLASH容量也有所不同,最小的只有16K字节,最大的则达到了1024K字节。战舰开发板选择的是STM32F103ZET6,其FLASH容量为512K字节,属于大容量产品(另外还有中容量和小容量产品),大容量产品的闪存模块组织如表45.1.1所示:
表45.1.1 大容量产品闪存模块组织表
STM32的闪存模块由主存储器、信息块和闪存存储器接口寄存器等3部分组成。
主存储器,该部分用来存放代码和数据常数(如const类型的数据)。对于大容量产品,其被划分为256页,每一页2K字节(注意:小容量和中容量产品每页只有1K字节)。从上表可以看出主存储器的起始地址就是0x08000000,B0、B1都接GND的时候,就是从0x08000000开始运行代码的。
信息块,该部分分为2个小部分,其中启动程序代码,用来存储ST自带的启动程序,用来串口下载代码,当B0接3V3,B1接GND的时候,运行的就是这部分代码。用户选中字节,则一般用于配置写保护、读保护等功能,本章不作介绍了。
闪存存储器接口寄存器,该部分用于控制闪存读写等,是整个闪存模块的控制结构。
对主存储器和信息块的写入由内嵌的闪存编程/擦除控制器(FPEC)管理;编程与擦除的高电压由内部产生。
在执行闪存写操作时,任何对闪存的读操作都会锁住总线,在写操作完成后读操作才能正确地进行。既在进行写或擦除操作时,不能进行代码或数据的读取操作。
45.1.1 闪存的读取
内置闪存模块可以在通用地址空间直接寻址,任何32位数据的读操作都能访问闪存模块的内容并得到相应的数据。读接口在闪存端包含一个读控制器,还包含一个AHB接口与CPU衔接。这个接口的主要工作是产生读内存的控制信号并预取CPU要求的指令块,预取指令块仅用于在I-Code总线上的取指操作,数据常量是通过D-Code总线访问的。这两条总线的访问目标是相同的闪存模块,访问D-Code将比预取指令优先级高。
这里要特别留意一个闪存等待时间,因为CPU运行速度比FLASH快得多,STM32F103的FLASH最快访问速度≤24Mhz,如果CPU频率超过这个速度,那么必须加入等待时间,比如我们一般使用72Mhz的主频,那么FLASH等待周期就必须设置为2,该设置通过FLASH_ACR寄存器设置。
例如,我们要从地址addr,读取一个半字(半字为16位,字为32位),可以通过如下的语句读取:
data = (vu16)addr;
将addr强制转换为vu16指针,然后取该指针所指向的地址的值,即得到了addr地址的值。类似的,将上面的vu16改为vu8,即可读取指定地址的一个字节。相对FLASH读取来说,STM32 FLASH的写就复杂一点了。下面我们介绍STM32闪存的编程和擦除。
45.1.2 闪存的编程和擦除
STM32的闪存编程是由FPEC(闪存编程和擦除控制器)模块处理的,这个模块包含7个32位寄存器,它们分别是:
FPEC键寄存器(FLASH_KEYR)
选择字节键寄存器(FLASH_OPTKEYR)
闪存控制寄存器(FLASH_CR)
闪存状态寄存器(FLASH_SR)
闪存地址寄存器(FLASH_AR)
选择字节寄存器(FLASH_WRPR)
其中FPEC键寄存器总共有3个键值:
RDPRT键 = 0X0000 00A5
KEY1 = 0X4567 0123
KEY2 = 0XCDEF 89AB
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编程过程如图45.1.2.1所示:
图45.1.2.1 STM32闪存编程过程
从上图可以得到闪存的编程顺序如下:
1)检查FLASH_CR的LOCK是否解锁,如果没有则先解锁
2)检查FLASH_SR寄存器的BSY位,以确认没有其他正在进行的编程操作
3)设置FLASH_CR寄存器的PG位为‘1’
4)在指定的地址写入要编程的半字
5)等待BSY位变为‘0’
6)读出写入地址并验证数据
前面提到,我们在STM32的FLASH编程的时候,要先判断缩写地址是否被擦出了,所以,我们有必要再介绍一下STM32的闪存擦除,STM32的闪存擦除分为两种:页擦除和整片擦除。页擦除过程如图45.1.2.2所示:
图45.1.2.2 STM32闪存页擦除过程
从上图可以看出,STM32的页擦除顺序为:
1)检查FLASH_CR和LOCK是否解锁,如果没有则先解锁
2)检查FLASH_SR寄存器的BSY位,以确认没有其他正在进行的闪存操作
3)设置FLASH_CR寄存器的PER位为‘1’
4)用FLASH_AR寄存器选择要擦除的页
5)设置FLASH_CR寄存器的STRT位为‘1’
6)等待BSY位变为‘0’
7)读出被擦除的页并做验证
本章我们只用到了STM32页擦除功能,整片擦除功能我们在这里就不介绍了。
45.1.3 FLASH寄存器
通过上面的讲解,我们基本对STM32闪存的读写执行步骤有所了解。接下来,我们介绍本实验需要用到的一些FLASH寄存器。
FPEC键寄存器(FLASH_KEYR)
FPEC键寄存器描述如图45.1.3.2所示:
图45.1.3.2 FLASH_KEYR寄存器
该寄存器主要用来解锁FPEC,必须在该寄存器写入特定的序列(KEY1和KEY2)解锁后,才能对FLASH_CR寄存器进行写操作。
FLASH控制寄存器(FLASH_CR)
FLASH控制寄存器描述如图45.1.3.3所示:
图45.1.3.3 FLASH_CR寄存器
该寄存器我们本章只用到了它的LOCK、STRT、PER和PG等4个位。
LOCK位,该位用于指示FLASH_CR寄存器是否被锁住,该位在检测到正确的解锁序列后,硬件将其清零。在一次不成功的解锁操作后,在下次系统复位之前,该位将不再改变。
STRT位,该位用于开始一次擦除操作。在该位写入1,将执行一次擦除操作。
PER位,该位用于选择页擦除操作,在页擦除的时候,需要将该位置1。
PG位,该位用于选择编程操作,在往FLASH写数据的时候,该位需要置1。
其他位,我们就不在这里介绍了,请大家参考《STM32F10xxx闪存编程参考手册》。
闪存状态寄存器(FLASH_SR)
闪存状态寄存器描述如图45.1.3.4所示:
图45.1.3.4 FLASH_SR寄存器
该寄存器主要用来指示当前FPEC的操作编程状态。由于寄存器中描述比较详细,这里就不重复了。
闪存地址寄存器(FLASH_AR)
闪存地址寄存器描述如图45.1.3.5所示:
图45.1.3.5 FLASH_AR寄存器
该寄存器在本章,我们主要用来设置要擦除的页。
关于STM32 FLASH的介绍,我们就介绍到这里。更详细的介绍,可以参考《STM32F10xxx闪存编程参考手册》。
45.2 硬件设计
- 例程功能
按键KEY1控制写入FLASH的操作,按键KEY0控制读出操作,并在TFTLCD模块上显示相关信息,还可以借助USMART进行读取或者写入操作。LED0闪烁用于提示程序正在运行。 - 硬件资源
1)LED灯
LED0 – PB5
2)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)
3)正点原子 2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
4)独立按键
KEY0 – PE4 KEY1 – PE3
45.3 程序设计
45.3.1 FLASH的HAL库驱动
FLASH在HAL库中的驱动代码在stm32f1xx_hal_flash.c和stm32f1xx_hal_flash_ex.c文件(及其头文件)中。 - HAL_FLASH_Unlock函数
解锁闪存控制寄存器访问的函数,其声明如下:
HAL_StatusTypeDef HAL_FLASH_Unlock(void);
函数描述:
用于解锁闪存控制寄存器的访问,在对FLASH进行写操作前必须先解锁,解锁操作也就是必须在FLASH_KEYR寄存器写入特定的序列(KEY1和KEY2)。
函数形参:
无
函数返回值:
HAL_StatusTypeDef枚举类型的值。 - HAL_FLASH_Lock函数
锁定闪存控制寄存器访问的函数,其声明如下:
HAL_StatusTypeDef HAL_FLASH_Lock (void);
函数描述:
用于锁定闪存控制寄存器的访问。
函数形参:
无
函数返回值:
HAL_StatusTypeDef枚举类型的值。 - HAL_FLASH_Program函数
闪存写操作函数,其声明如下:
HAL_StatusTypeDef HAL_FLASHEx_Program(uint32_t TypeProgram, uint32_t Address,
uint64_t Data);
函数描述:
该函数用于FLASH的写入。
函数形参:
形参1是TypeProgram用来区分要写入的数据类型,取值可为字节、半字、字和双字,用户根据写入数据类型选择即可。
形参2是Address用来设置要写入数据的FLASH地址。
形参3是Data是要写入的数据类型。该参数默认64位,如果你要写入小于64位的数据,比如16位,程序会进行类型转换。
函数返回值:
HAL_StatusTypeDef枚举类型的值。 - HAL_FLASHEx_Erase函数
闪存擦除函数,其声明如下:
HAL_StatusTypeDef HAL_FLASHEx_Erase(FLASH_EraseInitTypeDef *pEraseInit,
uint32_t *SectorError);
函数描述:
该函数用于大量擦除或擦除指定的闪存扇区。
函数形参:
形参1是FLASH_EraseInitTypeDef结构体类型指针变量。
typedef struct
{
uint32_t TypeErase; /* 擦除类型(Page擦除 / BANK级别批量擦除) */
uint32_t Banks; /* 擦除的Bank编号(批量擦除时才有效) */
uint32_t PageAddress; /* 擦除页面地址 */
uint32_t NbPages; /* 擦除的页面数 */
} FLASH_EraseInitTypeDef;
成员变量TypeErase用来设置擦除类型,是page擦除还是BANK级别的批量擦除,取值为FLASH_TYPEERASE_PAGES或者FLASH_TYPEERASE_MASSERASE,这个比较好理解,如果一次擦除一个Bank下面的所有Page,那么需要选择FLASH_TYPEERASE_MASSERASE。成员变量Banks用来设置要擦除的Bank编号,这个只有设置为批量擦除的时候才有效。成员变量PageAddress用来设置要擦除页面的地址。成员变量NbPages用来设置要擦除的页面数。
形参2是uint32_t类型指针变量,存放错误码,0xFFFFFFFF值表示扇区已被正确擦除,其它值表示擦除过程中的错误扇区。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
5. FLASH_WaitForLastOperation函数
等待FLASH操作完成函数,其声明如下:
HAL_StatusTypeDef FLASH_WaitForLastOperation(uint32_t Timeout);
函数描述:
该函数用于等待FLASH操作完成。
函数形参:
形参1是FLASH操作超时时间。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
45.3.2 程序流程图
图45.3.2.1 FLASH模拟EEPROM实验程序流程图
45.3.3 程序解析
- STM FLASH驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。STM FLASH驱动源码包括两个文件:stmflash.c和stmflash.h。
stmflash.h头文件做了一些比较重要的宏定义,定义如下:
/* FLASH起始地址 */
#define STM32_FLASH_BASE 0x08000000 /* STM32 FLASH 起始地址 */
#define STM32_FLASH_SIZE 0x80000 /* STM32 FLASH 总大小 */
/* STM32F103扇区大小 */
#if STM32_FLASH_SIZE < 256 * 1024
#define STM32_SECTOR_SIZE 1024 /* 容量小于256K的F103, 扇区大小为1K字节 */
#else
#define STM32_SECTOR_SIZE 2048 /* 容量大于等于256K的F103, 扇区大小为2K字节 */
#endif
STM32_FLASH_BASE和STM32_FLASH_SIZE分别是FLASH的起始地址和FLASH总大小,这两个宏定义随着芯片是固定的,我们战舰开发板的F103芯片FLASH是512K字节,所以STM32_FLASH_SIZE宏定义值为0x80000。
下面我们开始介绍stmflash.c的程序,下面先介绍一下stmflash写操作函数,源码如下:
/**
* @brief 在FLASH 指定位置, 写入指定长度的数据(自动擦除)
* @note 该函数往 STM32 内部 FLASH 指定位置写入指定长度的数据
* 该函数会先检测要写入的扇区是否是空(全0XFFFF)的?, 如果
* 不是, 则先擦除, 如果是, 则直接往扇区里面写入数据.
* 数据长度不足扇区时,自动被回擦除前的数据
* @param waddr : 起始地址 (此地址必须为2的倍数!!,否则写入出错!)
* @param pbuf : 数据指针
* @param length : 要写入的 半字(16位)数
* @retval 无
*/
uint16_t g_flashbuf[STM32_SECTOR_SIZE / 2]; /* 最多是2K字节 */
void stmflash_write(uint32_t waddr, uint16_t *pbuf, uint16_t length)
{
uint32_t secpos; /* 扇区地址 */
uint16_t secoff; /* 扇区内偏移地址(16位字计算) */
uint16_t secremain; /* 扇区内剩余地址(16位字计算) */
uint16_t i;
uint32_t offaddr; /* 去掉0X08000000后的地址 */
FLASH_EraseInitTypeDef flash_eraseop;
uint32_t erase_addr; /* 擦除错误,这个值为发生错误的扇区地址 */
if(waddr<STM32_FLASH_BASE||(waddr>=(STM32_FLASH_BASE+1024*STM32_FLASH_SIZE)))
{
return; /* 非法地址 */
}
HAL_FLASH_Unlock(); /* FLASH解锁 */
offaddr = waddr - STM32_FLASH_BASE; /* 实际偏移地址 */
secpos = offaddr / STM32_SECTOR_SIZE; /* 得到扇区编号 */
secoff = (offaddr % STM32_SECTOR_SIZE) / 2; /* 在扇区内的偏移(2B为基本单位) */
secremain = STM32_SECTOR_SIZE / 2 - secoff; /* 扇区剩余空间大小 */
if (length <= secremain)
{
secremain = length; /* 不大于该扇区范围 */
}
while (1)
{
stmflash_read(secpos * STM32_SECTOR_SIZE + STM32_FLASH_BASE,
g_flashbuf, STM32_SECTOR_SIZE / 2); /* 读出整个扇区的内容 */
for (i = 0; i < secremain; i++) /* 校验数据 */
{
if (g_flashbuf[secoff + i] != 0XFFFF)
{
break; /* 需要擦除 */
}
}
if (i < secremain) /* 需要擦除 */
{
flash_eraseop.TypeErase = FLASH_TYPEERASE_PAGES; /* 选择页擦除 */
flash_eraseop.NbPages = 1; /* 要擦除的页数 */
flash_eraseop.PageAddress = secpos * STM32_SECTOR_SIZE +
STM32_FLASH_BASE; /* 要擦除的起始地址 */
HAL_FLASHEx_Erase(&flash_eraseop, &erase_addr);
for (i = 0; i < secremain; i++) /* 复制 */
{
g_flashbuf[i + secoff] = pbuf[i];
}
stmflash_write_nocheck(secpos * STM32_SECTOR_SIZE + STM32_FLASH_BASE,
g_flashbuf, STM32_SECTOR_SIZE / 2); /* 写入整个扇区 */
}
else
{ /* 写已经擦除了的,直接写入扇区剩余区间 */
stmflash_write_nocheck(waddr, pbuf, secremain);
}
if (length == secremain)
{
break; /* 写入结束了 */
}
else /* 写入未结束 */
{
secpos++; /* 扇区地址增1 */
secoff = 0; /* 偏移位置为0 */
pbuf += secremain; /* 指针偏移 */
waddr += secremain * 2; /* 写地址偏移(16位数据地址,需要*2) */
length -= secremain; /* 字节(16位)数递减 */
if (length > (STM32_SECTOR_SIZE / 2))
{
secremain = STM32_SECTOR_SIZE / 2; /* 下一个扇区还是写不完 */
}
else
{
secremain = length; /* 下一个扇区可以写完了 */
}
}
}
HAL_FLASH_Lock(); /* 上锁 */
}
该函数用于在STM32的指定地址写入指定长度的数据。函数的实现基本类似SPI章节的norflash_write函数,不过该函数对于写入地址是有要求,必须保证以下两点:
1、写入地址必须是用户代码区以外的地址。
2、写入地址必须是2的倍数。
第1点比较好理解,如果把用户代码给擦了,可想而知你运行的程序可能就被废了,从而很可能出现死机的情况。第2点则是STM32 FLASH的要求,每次必须写入16位,如果你写的地址不是2的倍数,那么写入的数据,可能就不是写在你要写的地址了。
另外,该函数的g_flashbuf数组,也是根据所用STM32的FLASH容量来确定的,战舰STM32开发板的FLASH是512K字节,所以STM_SECTOR_SIZE的值为2048,故该数组大小为2K字节。
stmflash_write函数实质是调用stmflash_write_nocheck函数进行实现,下面再来看一下stmflash_write函数代码,其代码如下:
/**
* @brief 不检查的写入
这个函数的假设已经把原来的扇区擦除过再写入
* @param waddr : 起始地址 (此地址必须为2的倍数!!,否则写入出错!)
* @param pbuf : 数据指针
* @param length : 要写入的 半字(16位)数
* @retval 无
*/
void stmflash_write_nocheck(uint32_t waddr, uint16_t *pbuf, uint16_t length)
{
uint16_t i;
for (i = 0; i < length; i++)
{
HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, waddr, pbuf[i]);
waddr += 2; /* 指向下一个半字 */
}
}
该函数的实现依靠flash的HAL库驱动HAL_FLASH_Program进行实现。由于前面已经对HAL_FLASH_Program进行说明,这里就不作展开说明了。
接下来,讲解一下STM FLASH读相关的函数,写函数也有调用到读函数,其代码如下:
/**
* @brief 从指定地址读取一个半字 (16位数据)
* @param faddr : 读取地址 (此地址必须为2的倍数!!)
* @retval 读取到的数据 (16位)
*/
uint16_t stmflash_read_halfword(uint32_t faddr)
{
return *(volatile uint16_t *)faddr;
}
/**
* @brief 从指定地址开始读出指定长度的数据
* @param raddr : 起始地址
* @param pbuf : 数据指针
* @param length: 要读取的半字(16位)数,即2个字节的整数倍
* @retval 无
*/
void stmflash_read(uint32_t raddr, uint16_t *pbuf, uint16_t length)
{
uint16_t i;
for (i = 0; i < length; i++)
{
pbuf[i] = stmflash_read_halfword(raddr); /* 读取2个字节 */
raddr += 2; /* 偏移2个字节 */
}
}
前面也提及到STM32对FLASH写入,其写入地址的值必须是0xFFFFFFFF,所以读函数主要是读取地址的值,以给写函数调用检验,确保能写入成功。读函数实现比较简单,这里就不做展开了。
2. main.c代码
在main.c里面编写如下代码:
const uint8_t g_text_buf[] = {“STM32 FLASH TEST”}; /* 要写入的FLASH字符串数组 */
#define TEXT_LENTH sizeof(g_text_buf) /* 数组长度 */
/* SIZE表示半字长(2字节), 大小必须是2的整数倍, 如果不是的话, 强制对齐到2的整数倍 */
#define SIZE TEXT_LENTH / 2 + ((TEXT_LENTH % 2) ? 1 : 0)
/* 设置FLASH 保存地址(必须为偶数,且其值要大于本代码所占用FLASH的大小 + 0X08000000) */
#define FLASH_SAVE_ADDR 0X08070000
int main(void)
{
uint8_t key = 0;
uint16_t i = 0;
uint8_t datatemp[SIZE];
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
delay_init(72); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
usmart_dev.init(72); /* 初始化USMART */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
key_init(); /* 初始化按键 */
lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
lcd_show_string(30, 70, 200, 16, 16, "FLASH EEPROM TEST", RED);
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
lcd_show_string(30, 110, 200, 16, 16, "KEY1:Write KEY0:Read", RED);
while (1)
{
key = key_scan(0);
if (key == KEY1_PRES) /* KEY1按下,写入STM32 FLASH */
{
lcd_fill(0, 150, 239, 319, WHITE);
lcd_show_string(30, 160, 200, 16, 16, "Start Write FLASH....", RED);
stmflash_write(FLASH_SAVE_ADDR, (uint16_t *)g_text_buf, SIZE);
lcd_show_string(30, 150, 200, 16, 16, "FLASH Write Finished!", RED);
}
if (key == KEY0_PRES) /* KEY0按下,读取字符串并显示 */
{
lcd_show_string(30, 150, 200, 16, 16, "Start Read FLASH.... ", RED);
stmflash_read(FLASH_SAVE_ADDR, (uint16_t *)datatemp, SIZE);
lcd_show_string(30, 150, 200, 16, 16, "The Data Readed Is: ", RED);
lcd_show_string(30, 170, 200, 16, 16, (char *)datatemp, BLUE);
}
i++;
delay_ms(10);
if (i == 20)
{
LED0_TOGGLE(); /* 提示系统正在运行 */
i = 0;
}
}
}
主函数代码逻辑比较简单,当检测到按键KEY1按下后往FLASH指定地址开始的连续地址空间写入一段数据,当检测到按键KEY0按下后读取FLASH指定地址开始的连续空间数据。
最后,我们将stmflash_read_word和test_write函数加入USMART控制,这样,我们就可以通过串口调试助手,调用STM32F103的FLASH读写函数,方便测试。
45.4 下载验证
将程序下载到开发板后,可以看到LED0不停的闪烁,提示程序已经在运行了。LCD显示的内容如图45.4.1所示:
图45.4.1程序运行效果图
通过先按KEY1按键写入数据,然后按KEY0读取数据,得到如图45.4.2所示:
图45.4.2 操作后的显示效果图
本实验的测试,我们还可以借助USMART,调用:stmflash_read_word和test_write函数进行测试!