STM32F1系列单片机的flash模拟EEPROM方案(简单易用)

目录

  • 前言
  • 方案说明
    • 设计准则
    • 设计思路
    • 使用方法
  • 源码分享
  • 试验结果
  • 注意事项

前言

在进行单片机开发的过程中,经常要求使用一些可以断电后不丢失信息的数据。STM32的片上flash就是具有断电保持功能的存储器,是单片机存储源代码的地方,其断电之后数据不丢失,所以可以利用flash来实现数据的断电保存。
但是flash的寿命是极其有限的,每次擦写flash都会消耗其寿命,而且读写过程比较繁琐,所以需要设计一套程序来管理flash的读写,尽量减少flash的损耗。

方案说明

设计准则

  • 能够存储各种类型的数据,如整形、浮点型、字符串、自定义类型等。
  • 使用方便,无需手动分配存储空间或者指定储存地址。
  • 尽可能大的利用flash空间,尽可能减少擦除次数。
  • 能适应各种容量的单片机,移植、修改配置等操作尽量简单。

设计思路

  1. 对于一个需要断电保存的变量,给其分配一页flash用于存储,并在页的开头处记录该变量的地址,占用空间大小等信息。
  2. 由于需要尽量减少擦除次数,所以该变量发生变化且需要保存最新值时,不会在flash中擦除原有的值,而是在原有值之后写入新的值,此时页的空白处渐渐减少。
  3. 当一页flash的空白处少到写不下新值的时候,仍不擦除原本的页,而是寻找新的空白页继续写。原来的页标记为废弃。
  4. 当所有可用的flash页全部分配完,没有空白页的时候,再选择一废弃的页进行擦除。把擦除的页分配给所需要的变量。
  5. 每一次向flash写入数据,数据的存储地址都会改变,给读取造成不便。为了加快找到最新存储地址的速度,创建一个数组,用于记录各个页内部最新存储数据的地址。

使用方法

  1. 根据自己的机型修改头文件,设置flash大小,用于存储的flash起始页以及范围。
  2. 使用函数 register_data(const void* pdata, uint16_t size) 来注册选中变量,pdata为该变量的地址,size为该变量占据的内存大小(单位字节)。
  3. 对于已经注册的变量,可以使用函数 save_data(const void* pdata) 来向flash写入数据,写入的值可以断电保存。
  4. 对于已经保存在flash中的变量,可以使用函数 flash_addr(const void* pdata) 来获取其存储的flash地址。
  5. 如果变量要恢复到上一次写进flash的值,可以使用函数 load_data(void* pdata) 。如果从未写入flash,则不进行任何操作。
  6. 如果之后不再需要该变量断电保存,则可以使用函数 remove_data(const void* pdata) 来注销该变量。注销之后,如果该变量没有再次注册,则不能够使用flash的存储空间来读写。

源码分享

文件:main.c

#include "delay.h"
#include "sys.h"
#include "usart.h"
#include "stm32f1_flash.h"

int32_t var_a = 0;
int main()
{
	int i = 0;
	delay_init();
	uart_init(19200);
	
	printf("test start !\r\n");
	register_data(&var_a, sizeof(var_a));//注册数据
	load_data(&var_a);//加载数据 如果之前没有保存,则无动作
	for (i = 0; i < 50; i++)
	{
		var_a += 1;//改变数据
		printf("var_a is %d\r\n", var_a);
		
		save_data(&var_a);//保存数据
		printf("address is %p\r\n", (void*)flash_addr(&var_a));
		
		delay_ms(50);
	}
	
	if (var_a > 120) 
	{
		remove_data(&var_a);//删除flash中的数据
		printf("remove \r\n");
	}
	
	printf("over\r\n");
	
	while(1);
	return 0;
}


文件:stm32f1_flash.h

#ifndef STM32F1_FLASH_H
#define STM32F1_FLASH_H
#include "stm32f10x.h"

/*****************用户根据需要设置*******************/
#define STM32_FLASH_SIZE         512         /*flash大小 单位KB*/
#define ADDR_START_PAGE         (0x08006000) /*储存数据的FLASH起始页地址*/
#define ADDR_END_PAGE           (0x08010000) /*储存数据的FLASH最后一页的后一页地址*/
/***************************************************/

void flash_init(void);
int register_data(const void* pdata, uint16_t size);/*注册数据,注册之后才能使用flash*/
int save_data(const void* pdata);/*保存数据*/
int load_data(void* pdata);/*读取数据*/
int remove_data(const void* pdata);/*在flash中删除数据*/
uint32_t flash_addr(const void* pdata);/*返回保存该变量的flash地址*/
#endif

文件:stm32f1_flash.c

#include "stm32f1_flash.h"
#include "stm32f10x_flash.h"


#define PAGE_SIZE_1KB   (0x00000400)  
#define PAGE_SIZE_2KB   (0x00000800) 

#if STM32_FLASH_SIZE < 256
#define PAGE_SIZE PAGE_SIZE_1KB
#else 
#define PAGE_SIZE PAGE_SIZE_2KB
#endif		

#define STM32_FLASH_BASE   (0x08000000) /*STM32 FLASH的起始地址*/
#define STM32_FLASH_END    ((STM32_FLASH_BASE + STM32_FLASH_SIZE * 1024)) /*STM32 FLASH的结束地址*/

#if (STM32_FLASH_BASE >= ADDR_START_PAGE) ||\
    ( ADDR_START_PAGE > ADDR_END_PAGE)    ||\
	( ADDR_END_PAGE > STM32_FLASH_END)
#error The address you set is out of range /*超出范围*/
#endif

#if ( ADDR_START_PAGE % PAGE_SIZE != 0)    ||\
	( ADDR_END_PAGE % PAGE_SIZE != 0)
#error The address must be the first address of the page /*必须是页首地址*/
#endif


#define BLANK_32         ((uint32_t)0xFFFFFFFF) /*32位未编辑区域*/
#define BLANK_16         ((uint16_t)0xFFFF)     /*16位未编辑区域*/
#define BLANK_8          ((uint8_t)0xFF)        /*8位未编辑区域*/

#define EraseTimeout          ((uint32_t)0x000B0000)
#define ProgramTimeout        ((uint32_t)0x00002000)


typedef struct PageHead{
	uint32_t target_addr;
	uint16_t data_size;
	uint16_t flag;
} PageHead;	

#define DATA_OFFSET  sizeof(PageHead)  /*数据开始位置 在页内的偏移量*/
#define blank_page(page_addr) (((PageHead*)page_addr)->target_addr == BLANK_32) /*判断为空白页*/
#define used_page(page_addr) (((PageHead*)page_addr)->flag != BLANK_16)/*判断为废弃页*/
#define using_page(page_addr, data_addr) (((PageHead*)page_addr)->target_addr == (uint32_t)data_addr && !used_page(page_addr)) /*判断为使用中的页*/
#define data_size(page_addr)  (((PageHead*)page_addr)->data_size)

static uint32_t find_new_page(void);
static uint32_t save_addr[(ADDR_END_PAGE - ADDR_START_PAGE ) / PAGE_SIZE];/*每一页对应的 数据储存地址 或者 0:空白页 1:未存储 2:废弃页*/
static uint16_t initialized = 0;/*已经初始化的标志*/


void flash_init(void)
{
	uint32_t page_addr = ADDR_START_PAGE;
	initialized = 0;
	for (page_addr = ADDR_START_PAGE; page_addr < ADDR_END_PAGE; page_addr += PAGE_SIZE)
	{
		int index = (page_addr - ADDR_START_PAGE)/PAGE_SIZE;
		if (blank_page(page_addr))
		{
			save_addr[index] = 0;/*空白页*/
		}
		else if(used_page(page_addr)) 
		{
			save_addr[index] = 2;/*废弃页*/
		}
		else
		{
			uint16_t size = data_size(page_addr);
			uint32_t last_addr = page_addr + DATA_OFFSET;
			if (*(uint16_t *)(last_addr) == BLANK_16){
				save_addr[index] = 1;/*没有存储数据*/
				continue;/*看下一页*/
			}
			while(*(uint16_t *)(last_addr + size + (size&1)) != BLANK_16)
			{
				last_addr = last_addr + size + (size&1);/*找到最后一个地址*/
			}
			save_addr[index] = last_addr;/*存储地址*/
		}
	}
	initialized = 1;
}

int register_data(const void* pdata, uint16_t size)
{
	uint32_t page_addr = ADDR_START_PAGE;
	uint32_t select_page = 0;
	PageHead* ph = 0;
	if (size + DATA_OFFSET >= PAGE_SIZE) return -1;/*数据过大*/
	if (!initialized) flash_init();/*确保已经初始化*/
	for (;page_addr < ADDR_END_PAGE; page_addr += PAGE_SIZE)
	{
		if (using_page(page_addr, pdata))
		{
			return 1;/*已经注册过了*/
		}

	}
	select_page = find_new_page();
	if (select_page == 0) return -1;/*没有页了*/
	/*开始注册数据*/
	ph = (PageHead*)select_page;
	FLASH_Unlock();
	FLASH_ProgramWord((uint32_t)&(ph->target_addr), (uint32_t)pdata);
	FLASH_ProgramHalfWord((uint32_t)&(ph->data_size), size);
	FLASH_Lock();
	FLASH_WaitForLastOperation(ProgramTimeout);
	save_addr[(select_page - ADDR_START_PAGE) / PAGE_SIZE] = 1;/*没有存储数据*/
	return 0;
}

int load_data(void* pdata)
{
	/*把数据从flash加载到静态变量*/
	uint32_t page_addr = ADDR_START_PAGE;
	if (!initialized) flash_init();/*确保已经初始化*/
	for (page_addr = ADDR_START_PAGE; page_addr < ADDR_END_PAGE; page_addr += PAGE_SIZE)
	{
		if (using_page(page_addr, pdata))
		{
			/*找到对应的存储页*/
			int index = (page_addr - ADDR_START_PAGE) / PAGE_SIZE;
			int r_bytes = 0;/*已经读取的字节数*/
			uint16_t size = data_size(page_addr);
			if (save_addr[index] == 1)
			{
				 return 0;/*没有数据可以读*/
			}
			/*把数据填入*/
			for (r_bytes = 0; r_bytes < size; r_bytes++)
			{
				((uint8_t *)pdata)[r_bytes] = ((uint8_t *)save_addr[index])[r_bytes];/*一个字节一个字节地填*/
			}
		}
	}
	return 0;/*读取成功*/
}

int save_data(const void* pdata)
{
	uint32_t page_addr = ADDR_START_PAGE;
	uint32_t target_addr = (uint32_t)pdata;
	uint32_t new_addr = 0;
	uint16_t size = 0;
	uint16_t w_bytes = 0;
	if (!initialized) flash_init();/*确保已经初始化*/
	for (page_addr = ADDR_START_PAGE; page_addr < ADDR_END_PAGE; page_addr += PAGE_SIZE)
	{
		if (using_page(page_addr, pdata))
		{
			/*找到页地址*/
			int index = (page_addr - ADDR_START_PAGE) / PAGE_SIZE;
			size = data_size(page_addr);
			
			if (save_addr[index] == 1)
			{
				new_addr = page_addr + DATA_OFFSET;/*之前还没存储数据*/
			}
			else
			{
				new_addr = save_addr[index] + size + (size&1);/*找到新地址*/
			}
			if (new_addr % PAGE_SIZE + size + (size&1) >= PAGE_SIZE)
			{
				/*旧页装不下,要找新页*/
				uint32_t find_page = find_new_page();
				PageHead *ph = 0;
				if (find_page == 0)
				{
					/*没有新页,把原来的页擦除*/
					FLASH_Unlock();
					FLASH_ErasePage(page_addr);/*擦除包含该地址的页*/
					FLASH_WaitForLastOperation(ProgramTimeout);
					FLASH_Lock();
				}
				else
				{
					/*找到新页,原来的页废弃*/
					save_addr[index] = 2;/*原来的页已经废弃*/
					FLASH_Unlock();
					FLASH_ProgramHalfWord((uint16_t)&((PageHead*)page_addr)->flag, 1);
					FLASH_Lock();
					page_addr = find_page;/*用新的页储存*/
				}
				
				new_addr = page_addr + DATA_OFFSET;/*新页新地址*/
				/*新页需要新页头*/
				ph = (PageHead*)page_addr;
				FLASH_Unlock();
				FLASH_ProgramWord((uint32_t)&(ph->target_addr), (uint32_t)pdata);
				FLASH_ProgramHalfWord((uint16_t)&(ph->data_size), size);
				FLASH_Lock();
			}
			
			break;
		}
	}
	if (new_addr == 0) return -1;/*之前没有注册过*/
	
	/*保存新的数据*/
	save_addr[(page_addr - ADDR_START_PAGE) / PAGE_SIZE] = new_addr;/*记录新的存储地址*/
	FLASH_Unlock();
	for (w_bytes = 0; w_bytes < size; w_bytes += 2)
	{
		FLASH_ProgramHalfWord(new_addr + w_bytes, *(uint16_t*)(target_addr + w_bytes));/*把数据填入*/
	}
	FLASH_Lock();
	FLASH_WaitForLastOperation(ProgramTimeout);
	return 0;/*保存成功*/
}

int remove_data(const void* pdata)
{
	uint32_t page_addr = ADDR_START_PAGE;
	if (!initialized) flash_init();/*确保已经初始化*/
	for (page_addr = ADDR_START_PAGE; page_addr < ADDR_END_PAGE; page_addr += PAGE_SIZE)
	{
		if (using_page(page_addr, pdata))
		{
			/*找到页地址*/
			save_addr[(page_addr - ADDR_START_PAGE) / PAGE_SIZE] = 2;/*页已经废弃*/
			FLASH_Unlock();
			FLASH_ProgramHalfWord((uint16_t)&((PageHead*)page_addr)->flag, 1);
			FLASH_Lock();
			return 0;
		}
	}
	return 0;
}

uint32_t flash_addr(const void* pdata)
{
	uint32_t page_addr = ADDR_START_PAGE;
	for (page_addr = ADDR_START_PAGE; page_addr < ADDR_END_PAGE; page_addr += PAGE_SIZE)
	{
		if (using_page(page_addr, pdata))
		{
			/*找到页地址*/
			return save_addr[(page_addr - ADDR_START_PAGE) / PAGE_SIZE];
		}
	}
	return 0;
}

static uint32_t find_new_page(void)
{
	uint32_t page_addr = ADDR_START_PAGE;
	for (page_addr = ADDR_START_PAGE; page_addr < ADDR_END_PAGE; page_addr += PAGE_SIZE)
	{
		if (blank_page(page_addr))
		{
			return page_addr;/*选到空白页*/
		}
	}
	for (page_addr = ADDR_START_PAGE; page_addr < ADDR_END_PAGE; page_addr += PAGE_SIZE)
	{
		if (used_page(page_addr))
		{
			/*擦除废弃页*/
			FLASH_Unlock();
			FLASH_ErasePage(page_addr);/*擦除该页*/
			FLASH_WaitForLastOperation(ProgramTimeout);
			FLASH_Lock();
			save_addr[(page_addr - ADDR_START_PAGE) / PAGE_SIZE] = 0;/*变成空白页*/
			return page_addr;/*选到废弃页*/
		}
	}
	return 0;/*找不到多余的页*/
	
}

试验结果

本次实验使用正点原子的STM32精英开发板,单片机型号为STM32F103ZET6。使用串口打印信息。
STM32F1系列单片机的flash模拟EEPROM方案(简单易用)_第1张图片
这是单片机重启几次之后的结果,可以看到,变量var_a的值可以达到150,说明断电保存的功能可以正常使用,否则var_a的数值不会超过50。而当var_a被注销之后,断电保存的数据也一并失效了。

注意事项

  • 由于在flash中0xffff表示的是未编辑,且程序上电第一次运行的时候,是根据最后的地址内容是否为0xffff来判断是否存储了数据的。所以,如果掉电前存储的数据为-1,65535等特殊值,那么该数据可能就会丢失。
  • 该方案不适合存储图片等大型数据,因为对数据大小的限制为一页flash能够存的下
  • 该方案每次读写都要遍历每一页,读写速度不高,所以如果对读写速度的要求很苛刻,建议自行使用哈希表等进行优化。
  • 该方案用变量的地址来识别数据,不建议用于保存局部变量的信息

你可能感兴趣的:(C语言,单片机,c语言,单片机,stm32,flash)