最近在做一个电源项目软件,客户要求能够在掉电后保存一些数据,重新上电后能加载这些数据。数据内容只有一个字节,但每次写入时是4个字节。
具体的内容如下:
电源带有4路输出,上位机通过RS485能够控制这4路的通断,1:导通,0:断开,当上位机执行一次导通或关断操作时都要保存输出状态到Flash,如果相同的操作执行多次则只保存一次,例如:多次发送导通操作被视为是导通一次操作。
具体思路如下:
1、由于N32G031系列没有EEPROM,也没有其他外部存储器,只能使用ROM的一部分。N32G031总共有64K字节ROM,运行的程序只有十多K,空间还有很多;
2、使用片内Flash的一页来动态存储数据。Flash每页有512个字节,每4个字节写一次,则一页可以写入128次,整页写完,继续再写时才擦除页,重新从页的起始位置写入数据,如此反复,这样可以大大减小Flash页的擦除次数;
3、初始时从Flash页的指定位置读取数据(位置根据计算得到,就是找到第一个连续4个0xFF的位置),并存储在两个变量中A和B中,此时A和B中保存的数据是相同的,运行时如果上位机执行了导通和断开操作会改变B变量相关成员值,当while大循环检测到A和B的值不同时,则用B的值设置A变量,并根据计算的地址写入Flash页中。
程序主要代码如下:
结构体和联合体定义
struct STRUCT_OUTPUT_STATE{
unsigned P1 :1;
unsigned P2 :1;
unsigned P3 :1;
unsigned P4 :1;
unsigned AUX :1;
unsigned :1;
unsigned :1;
unsigned :1;
};
//used to save states of all the four channels
union OUTPUT_UNION{
struct STRUCT_OUTPUT_STATE port_state;
uint8_t allbits;
//uint8_t P1;
//uint8_t P2;
//uint8_t P3;
//uint8_t P4;
//uint32_t allbits;
};
typedef union OUTPUT_UNION Output_State;
结构体STRUCT_OUTPUT_STATE的成员P1-P4分别保存1-4路输出状态,AUX成员默认设置为0,是个辅助标志位,用于区别当所有输出都打开时(P1-P4都为1)和从Flash读取的连续4字节都是0xFF的情况。
主程序初始时读取逻辑:
page_offset = Find_Empty_Data();
if(page_offset <= 128)
{
if(page_offset > 0)
{
_OutputState_Old.allbits = (*(__IO uint32_t*)(FLASH_WRITE_START_ADDR + (page_offset - 1) * 4));//get the status
_OutputState_New.allbits = _OutputState_Old.allbits;
_OutputState_Old.port_state.AUX = 0;
_OutputState_New.port_state.AUX = 0;
}
else
{
_OutputState_Old.allbits = (*(__IO uint32_t*)(FLASH_WRITE_START_ADDR));//get the status
_OutputState_New.allbits = _OutputState_Old.allbits;
_OutputState_Old.port_state.AUX = 0;
_OutputState_New.port_state.AUX = 0;
}
}
else //can't find the address that contains the valid data
{
_OutputState_Old.allbits = (*(__IO uint32_t*)(FLASH_WRITE_START_ADDR));//get the status
_OutputState_New.allbits = _OutputState_Old.allbits;
_OutputState_Old.port_state.AUX = 0;
_OutputState_New.port_state.AUX = 0;
}
两个全局变量及Flash页起始地址定义如下:
//Flash information
#define FLASH_PAGE_SIZE ((uint16_t)0x200)
#define FLASH_WRITE_START_ADDR ((uint32_t)0x08008000)
#define FLASH_WRITE_END_ADDR ((uint32_t)0x08010000)
volatile Output_State _OutputState_Old;
volatile Output_State _OutputState_New;
//uint32_t flash_rw_delay = 0;
volatile uint16_t off_set = 0;
Find_Empty_Data函数用于查找Flash页中连续4字节都为0xFF的位置,也就是我们下一个要写入数据的位置。函数如下:
/**
*@name: Find_Empty_Data
*@description: Find the location that has not been written.
The reason why we do this is because we need to avoid erasing flash.
*@params: none
*@return: none
*/
uint16_t Find_Empty_Data(void)
{
uint16_t page_offset;
for (page_offset = 0; page_offset < 128;/*(FLASH_PAGE_SIZE/4)*/ page_offset++)
{
if (*((uint32_t*)FLASH_WRITE_START_ADDR + page_offset) == 0xFFFFFFFF) break;
}
return page_offset;
}
当变量A和B的值不同时,执行以下代码,将更新后的数据写入Flash页中指定位置:
/**
*@name: Save_Output_State
*@description: If any of the output state has been changed, we write the new value to the flash.
*@params: none
*@return: none
*/
void Save_Output_State(void)
{
if(_OutputState_Old.allbits != _OutputState_New.allbits)
{
uint32_t write_addr = 0;
off_set = Find_Empty_Data();
FLASH_Unlock();
//if(off_set>=127 && buf32 != 0xFFFFFFFF)//the last four bytes of the current page
if(off_set <= 127)
{
write_addr = FLASH_WRITE_START_ADDR + off_set * 4;
}
else //We can't find the four bytes that are all 0xFF, which means the whole page is programmed.
{
//Erase
if(FLASH_COMPL != FLASH_EraseOnePage(FLASH_WRITE_START_ADDR))
{
//TODO
}
write_addr = FLASH_WRITE_START_ADDR;
}
//flash_rw_delay = 480000;//about 10ms if FOSC = 48MHz
_OutputState_Old.allbits = _OutputState_New.allbits;
//Program
if (FLASH_COMPL != FLASH_ProgramWord(write_addr, _OutputState_New.allbits))
{
//TODO
}
/* Locks the FLASH Program Erase Controller */
FLASH_Lock();
//GPIO_WriteBit(GPIOF, GPIO_PIN_7, (Bit_OperateType)(1 - GPIO_ReadOutputDataBit(GPIOF, GPIO_PIN_7)));
}
}
这样从理论上可以支持输出状态1280万次以上的更改(Flash擦除次数以10万次计算),可以大大减少Flash页擦除的次数,延长Flash使用寿命。客户那边也说过,用上位机导通和断开输出的次数不会很频繁,所以是够用的。
之前也考虑过连续高频率执行导通和关断操作的问题,例如:如果在一段时间内连续导通和关断很多次则忽略这次操作,等稳定后才执行写入操作,后经客户沟通不用担心这个问题,所以也没有写入这个机制了。
欢迎大家发表你们的看法,还有什么更好的方案大家一起分享。