STM32 本身没有自带 EEPROM,但是 STM32 具有 IAP(在应用编程)功能,所以我们可 以把它的 FLASH 当成 EEPROM 来使用。本章,我们将利用 STM32 内部的 FLASH 来实现NOR FLASH(EEPROM)(实验类似的效果,不过这次我们是将数据直接存放在 STM32 内部,而不是存放在 NOR FLASH。
STM32 FLASH 简介
不同型号的 STM32,其 FLASH 容量也有所不同,最小的只有 16K 字节,最大的则达到了 1024K 字节。我们的精英 STM32 开发板选择的是 STM32F103ZET6 的 FLASH 容量为 512K 字 节,属于大容量产品,大容量产品的闪存模块组织如表 43.1.1 所示:STM32 的闪存模块由主存储器、信息块和闪存存储器接口寄存器等 3 部分组成。
主存储器,该部分用来存放代码和数据常数(如 const 类型的数据)。对于大容量产品,其 被划分为 256 页,每一页 2K 字节(注意,小容量和中容量产品则每页只有 1K 字节)。从上图可 以看出主存储器的起始地址就是 0x08000000,B0、B1 都接 GND 的时候,就是从 0x08000000 开始运行代码的。
信息块,该部分分为 2 个小部分,其中启动程序代码,用来存储 ST 自带的启动程序,用来 串口下载代码,当 B0 接 3V3,B1 接 GND 的时候,运行的就是这部分代码。用户选中字节, 则一般用于配置写保护、读保护等功能,本章不作介绍了。
闪存存储器接口寄存器,该部分用于控制闪存读写等,是整个闪存模块的控制结构。
对主存储器和信息块的写入由内嵌的闪存编程/擦除控制器(FPEC)管理;编程与擦除的高 电压由内部产生。
在执行闪存写操作时,任何对闪存的读操作都会锁住总线,在写操作完成后读操作才能正确地进行。既在进行写或擦除操作时,不能进行代码或数据的读取操作。
闪存的读取
内置闪存模块可以在通用地址空间直接寻址,任何 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 闪存的编程和擦除。
闪存的编程和擦除
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 编程过程如图 43.1.2.1 所示:
1)检查 FLASH_CR 的 LOCK 是否解锁,如果没有则先解锁
2)检查 FLASH_SR 寄存器的 BSY 位,以确认没有其他正在进行的编程操作
3)设置 FLASH_CR 寄存器的 PG 位为‘1’
4)在指定的地址写入要编程的半字
5)等待 BSY 位变为‘0’
6)读出写入地址并验证数据
前面提到,我们在 STM32 的 FLASH 编程的时候,要先判断缩写地址是否被擦出了,所以, 我们有必要再介绍一下 STM32 的闪存擦除,STM32 的闪存擦除分为两种:页擦除和整片擦除。 页擦除过程如图 43.1.2.2 所示:从上图可以看出,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 页擦除功能,整片擦除功能我们在这里就不介绍了。
HAL库函数介绍
#ifndef __STMFLASH_H
#define __STMFLASH_H
#include "./SYSTEM/sys/sys.h"
/* FLASH起始地址 */
#define STM32_FLASH_SIZE 0x80000 /* STM32 FLASH 总大小 */
#define STM32_FLASH_BASE 0x08000000 /* 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
/* FLASH解锁键值 */
#define STM32_FLASH_KEY1 0X45670123
#define STM32_FLASH_KEY2 0XCDEF89AB
/* 静态函数(仅限stmflash.c调用) */
static void stmflash_unlock(void); /* 解锁STM32 内部FLASH */
static void stmflash_lock(void); /* 锁定STM32 内部FLASH */
static uint8_t stmflash_get_error_status(void); /* 获取FLASH错误状态 */
static uint8_t stmflash_wait_done(uint32_t time); /* 等待操作完成 */
static uint8_t stmflash_erase_sector(uint32_t saddr); /* 擦除扇区 */
static uint8_t stmflash_write_halfword(uint32_t faddr, uint16_t data); /* FLASH写半字 */
/* 接口函数(外部可调用) */
uint16_t stmflash_read_halfword(uint32_t faddr); /* FLASH读半字 */
void stmflash_read(uint32_t raddr, uint16_t *pbuf, uint16_t length); /* 从指定地址开始读出指定长度的数据 */
void stmflash_write(uint32_t waddr, uint16_t *pbuf, uint16_t length); /* 在FLASH 指定位置, 写入指定长度的数据(自动擦除) */
#endif
STM32_FLASH_BASE 和 STM32_FLASH_SIZE 分别是 FLASH 的起始地址和 FLASH 总大 小,这两个宏定义随着芯片是固定的,我们开发板 F103 芯片的 FLASH 是 512K 字节,所以 STM32_FLASH_SIZE 宏定义值为 0x80000。
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/STMFLASH/stmflash.h"
/**
* @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个字节 */
}
}
/**
* @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; /* 指向下一个半字 */
}
}
/**
* @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; /* 扇区地址 0~255 for STM32F103ZET6 */
secoff = (offaddr % STM32_SECTOR_SIZE) / 2; /* 在扇区内的偏移(2个字节为基本单位.) */
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.Banks = FLASH_BANK_1;
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 函数代码。
该函数的实现依靠 flash 的 HAL 库驱动 HAL_FLASH_Program 进行实现。由于前面已经对 HAL_FLASH_Program 进行说明,这里就不作展开说明了。
接下来,讲解一下 STMFLASH 读相关的函数,写函数也有调用到读函数。
前面也提及到 STM32 对 FLASH 写入,其写入地址的值必须是 0xFFFFFFFF,所以读函数 主要是读取地址的值,以给写函数调用检验,确保能写入成功。读函数实现比较简单,这里就 不做展开了。
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/KEY/key.h"
#include "./BSP/STMFLASH/stmflash.h"
/* 要写入到STM32 FLASH的字符串数组 */
const uint8_t g_text_buf[] = {"STM32F FLASH TEST"};
#define TEXT_LENTH sizeof(g_text_buf) /* 数组长度 */
/*SIZE表示半字长(2字节), 大小必须是2的整数倍, 如果不是的话, 强制对齐到2的整数倍 */
#define SIZE TEXT_LENTH / 2 + ((TEXT_LENTH % 2) ? 1 : 0)
#define FLASH_SAVE_ADDR 0X08070000 /* 设置FLASH 保存地址(必须为偶数,且其值要大于本代码所占用FLASH的大小 + 0X08000000) */
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 */
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_show_string(30, 150, 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 指定地址开始的连续空间数据。