本帖最后由 yang377156216 于 2022-5-8 00:46 编辑
#申请原创# @21小跑堂
整体概览上一篇记录了 Flash 模拟 EEPROM 的应用,用于存储较少数的用户参数比较适合,它作为操作 Flash 的一种底层存储介质,此外,还可以用 EasyFlash / FlashDB 等 KV 数据库作为介质,还可以用文件系统充当媒介。后两种类型更加适合用于保存大量的日志数据,或者是不同数据类型的数据。本文将记录我移植开源的 EasyFlash 组件,使用内嵌 Flash 存储空间来实现用户数据存储记录的功能的过程。主要分为以下几个部分内容:
一、MM32F0273D7P 的内嵌 Flash 介绍MM32F0273D7P 芯片内嵌高达 128K Bytes 的片内 Main Flash,还提供了选项字节块与系统启动块(支持芯片Boot 引导),还有保留的保密空间,提供了特殊应用的场景下的使用。闪存的控制器支持读操作、页擦除、整片擦除,可通过 16 位(半字)方式编程写入闪存,其擦写寿命可达 20000 次。闪存控制器在读取数据时,支持带预取缓冲器的数据接口,以支持 MCU 运行在更高的主频。 主闪存区域有以下特性: 由 64 位宽的存储单元组成,既可以存代码又可以存数据,用户代码可以对主存储器进行擦除、编程和读取操作 按 128 页(每页 1K 字节)或 32 个写保护块(每块 4K 字节)划分 可按页(每 页1K 字节)擦除(Page Erase),也可以全片擦除 支持读保护功能,结合自身的 UID 以及一些随机数的加密算法可以增强产品固件的安全性 以 4 页(4K 字节)为单位作为 1 个写保护块来设置写保护,写保护区域划分如下图所示: 加上读写保护后,经常会对访问权限有所疑问,特意整理了一下,如下:
二、EasyFlash 是什么EasyFlash 是 RTT 大佬 armink (Mculover666)的第二款开源软件,自 2015 年初正式开源出来,至今已经经历了 7 年多时间,该组件稳定版本也迭代到 V4.1.0 了。它已经被诸多工程师应用于自己的产品上,可见 EasyFlash 的成熟性已经得到了很多行业认可。它是一款开源的轻量级嵌入式 Flash存储器库,方便开发者更加轻松的实现基于Flash存储器的常见应用开发。非常适合智能家居、可穿戴、工控、医疗、物联网等需要断电存储功能的产品,资源占用极低,支持各种 MCU 片上存储器。主要包括三大实用功能 : EasyFlash 不仅能够实现对产品的设定参数或运行日志 等信息的掉电保存功能,还封装了简洁的增加、删除、修改及查询方法, 降低了开发者对产品参数的处理难度,也保证了产品在后期升级时拥有更好的扩展性。让 Flash 变为 NoSQL(非关系型数据库)模型的小型键值(Key-Value)存储数据库。 目前 ENV 功能有两种主要模式,一种为 V4.0 版本带来的 NG(Next Generation) 模式,还有一种为延续 V3.0 版本的 Legacy 模式。NG 模式相比较于 Legacy 模式具有以下新特性: 更小的资源占用,内存占用几乎为0;V4.0 以前版本会使用额外的 RAM 空间进行缓存,最终调用 save 接口,统一擦除扇区再存储到 Flash 上 ENV 的值类型支持任意类型、任意长度,相当于直接 memcpy 变量至 Flash;V4.0 之前只支持存储字符串 ENV 操作效率比以前的模式高,充分利用剩余空闲区域,擦除次数及操作时间显著降低 原生支持磨损平衡、掉电保护功能;V4.0 之前需要占用额外的 Flash 扇区,每次保存 ENV 都需要重新擦写整个 Flash 扇区,降低了 Flash 的使用效率和使用寿命 ENV 支持增量升级,固件升级后 ENV 也支持升级 大数据存储、数据加密、数据压缩等功能已经列入规划内,在后续版本即将实现
整个 ENV 的数据结构如下图: 整个 EasyFlash ENV 相关的用户操作 API 接口如下,可以进行简单的增删改查: EasyLogger 是一款超轻量级、高性能的 C/C++ 日志库,非常适合对资源敏感的软件项目,方便开发人员快速定位、查找系统发生崩溃或死机的原因。同时配合 EasyLogger + CmBacktrace 开源库一起使用,轻松实现系统运行和死机状况的日志存储于 Flash 功能,并且还能够使用芯片自带的 RTC 功能使日志带有日期和时间信息。EasyLogger 相较于 log4c、zlog 这些知名的 C/C++ 日志库,它的功能更加简单,提供给用户的接口更少,但上手会很快,更多实用功能支持以插件(Flash、File等)形式进行动态扩展。 EasyLogger 主要特性如下: 支持用户自定义输出方式(例如:终端、文件、数据库、串口、RS-485、Flash等等) 日志内容可包含级别、时间戳、线程信息、进程信息等 日志输出被设计为线程安全的方式,并支持异步输出和缓冲输出模式 支持多种操作系统(例如:RT-Thread、uCOS、Linux、Windows等等),也支持裸机平台 日志支持RAW格式(未经过格式化的原始日志)、支持HEXDUMP 支持按标签、级别、关键词进行动态过滤 各级别日志支持不同颜色显示,用户也可以根据自己的喜好,在 elog_cfg.h 对各个级别日志的颜色及字体风格进行单独设置 扩展性强,支持以插件的形式扩展新功能
整个 EasyLogger 相关的用户操作 API 接口如下: EasyFlash 库还封装了 IAP (In-Application Programming)功能常用的接口,支持 CRC32 校验,同时支持 Bootloader 及 Application 的升级。在备份区额外地增加了一块区域专门用于暂存更新后的应用程序,加上使用 Ymodem 串口传输文件协议,利用 ENV 保存升级过程的参数以及一系列的判断流程即可完成 IAP 框架。 整个 EasyFlash 将备份区划分如下: 三、EasyFlash 三大功能在 MM32F0273D7P 上实现硬件资源如下:
软件资源如下:
- EasyFlash 源码包(获取地址:https://github.com/armink/EasyFlash )
- EasyLogger 源码包(获取地址:https://github.com/armink/EasyLogger )
- 一份已经调试好的 KEIL template 工程代码包
- JLink_Windows_V670g.exe 以及 灵动官方 J-Flash 插件安装包 (让 J-Link 可以搜索到 MM32F0273D7P 这颗芯片,不然只能选择 Cortex M0)
- CH340 USB-Serial Port Driver
- MindMotion.MM32F0270_DFP.0.0.1.pack
首先进行 EasyFlash 主体功能 ENV 的实现。
1. 先解压上面下载好的源码包,可以看到文件的目录结构大致如下:
2. 将源文件 .c .h 以及 port 相关的 .c 文件夹拷贝到项目中,这里将所有涉及到的文件全部放到示例工程中去,并且添加 \easyflash\inc\ 文件夹到编译的头文件目录列表中。
3. 实现底层驱动必须接口。/** * Flash port for hardware initialize. * * @param default_env default ENV set for user * @param default_env_size default ENV size * * [url=home.php?mod=space&uid=266161]@return[/url] result */
EfErrCode ef_port_init(ef_env const **default_env, size_t *default_env_size)
{
EfErrCode result = EF_NO_ERR;
*default_env = default_env_set;
*default_env_size = sizeof(default_env_set) / sizeof(default_env_set[0]);
return result;
}
/** * Read data from flash. * [url=home.php?mod=space&uid=536309]@NOTE[/url] This operation's units is word. * * @param addr flash address * @param buf buffer to store read data * @param size read bytes size * * [url=home.php?mod=space&uid=266161]@return[/url] result */
EfErrCode ef_port_read(uint32_t addr, uint32_t *buf, size_t size)
{
EfErrCode result = EF_NO_ERR;
/* You can add your code under here. */
uint8_t *Data = (uint8_t *)buf;
for(size_t i = 0; i < size; i++, addr++, Data++)
{
*Data = *(uint8_t *)addr;
}
return result;
}
/** * Erase data on flash. * [url=home.php?mod=space&uid=536309]@NOTE[/url] This operation is irreversible. * @note This operation's units is different which on many chips. * * @param addr flash address * @param size erase bytes size * * @return result */
EfErrCode ef_port_erase(uint32_t addr, size_t size)
{
EfErrCode result = EF_NO_ERR;
/* make sure the start address is a multiple of EF_ERASE_MIN_SIZE */
EF_ASSERT(addr % EF_ERASE_MIN_SIZE == 0);
/* You can add your code under here. */
FLASH_Status Status;
size_t Number;
Number = size / 1024;
if((size % 1024) != 0) Number++;
FLASH_Unlock();
FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);
for(size_t i = 0; i < Number; i++)
{
Status = FLASH_ErasePage(addr + 1024 * i);
FLASH_ClearFlag(FLASH_FLAG_EOP);
if(Status != FLASH_COMPLETE)
{
printf("\r\nErase Error!!!");
result = EF_ERASE_ERR; break;
}
}
FLASH_Lock();
return result;
}
/** * Write data to flash. * @note This operation's units is word. * @note This operation must after erase. [url=home.php?mod=space&uid=8537]@see[/url] flash_erase. * * @param addr flash address * @param buf the write data buffer * @param size write bytes size * * @return result */
EfErrCode ef_port_write(uint32_t addr, const uint32_t *buf, size_t size)
{
EfErrCode result = EF_NO_ERR;
EF_ASSERT(size % 4 == 0);
/* You can add your code under here. */
FLASH_Unlock();
FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);
for(size_t i = 0; i < size; i+=4, buf++, addr+=4)
{
FLASH_ProgramWord(addr, *buf);
FLASH_ClearFlag(FLASH_FLAG_EOP);
uint32_t Data = *(uint32_t *)addr;
if(Data != *buf)
{
printf("\r\nWrite Error!!!");
result = EF_WRITE_ERR; break;
}
}
FLASH_Lock();
return result;
}
/**
* lock the ENV ram cache
*/
void ef_port_env_lock(void)
{
/* You can add your code under here. */
__disable_irq();
}
/** * unlock the ENV ram cache */
void ef_port_env_unlock(void)
{
/* You can add your code under here. */
__enable_irq();
}
static char log_buf[128];
/** * This function is print flash debug info. * * @param file the file which has call this function * @param line the line number which has call this function * @param format output format * @param ... args * */
void ef_log_debug(const char *file, const long line, const char *format, ...)
{
#ifdef PRINT_DEBUG
va_list args;
/* args point to the first variable parameter */
va_start(args, format);
/* You can add your code under here. */
ef_print("\r\n[Debug](%s:%ld) ", file, line);
vsprintf(log_buf, format, args);
ef_print("%s", log_buf);
printf("\r\n");
va_end(args);
#endif
}
/** * This function is print flash routine info. * * @param format output format * @param ... args */
void ef_log_info(const char *format, ...)
{
va_list args;
/* args point to the first variable parameter */
va_start(args, format);
/* You can add your code under here. */
ef_print("\r\n[LogInfo]");
/* must use vprintf to print */
vsprintf(log_buf, format, args);
ef_print("%s", log_buf);
printf("\r\n");
va_end(args);
}
/** * This function is print flash non-package info. * * @param format output format * @param ... args */
void ef_print(const char *format, ...)
{
va_list args;
/* args point to the first variable parameter */
va_start(args, format);
/* You can add your code under here. */
vsprintf(log_buf, format, args);
printf("%s", log_buf);
va_end(args);
}
/* default environment variables set for user */
static const ef_env default_env_set[] =
{
{"startup_times", "0"},
{"pressed_times", "0"},
};
4. 修改项目中的 ef_cfg.h 文件中的参数,开启、关闭、修改对应的宏,主要包括最小擦除单元、写入粒度、备份区起始地址以及 ENV 区容量大小等等。
5. 将系统启动次数和按键次数作为 2 个 ENV 变量进行存储、取出以及修改后再存储的实验。
/******************************************************************************* * [url=home.php?mod=space&uid=247401]@brief[/url] * @param * @retval * [url=home.php?mod=space&uid=93590]@Attention[/url] *******************************************************************************/
void EasyFlash_Demo(void)
{
uint32_t startup_times = 0;
char *old_startup_times, new_startup_times[30] = {0};
old_startup_times = ef_get_env("startup_times");
startup_times = atol(old_startup_times);
startup_times++;
sprintf(new_startup_times, "%d", startup_times);
printf("\r\nThe system now startup %d times\r\n\r\n", startup_times);
ef_set_env("startup_times", new_startup_times);
ef_save_env();
}
/*******************************************************************************
* [url=home.php?mod=space&uid=247401]@brief[/url]
* @param
* @retval
* [url=home.php?mod=space&uid=93590]@Attention[/url]
*******************************************************************************/
void KEY_Handler(void)
{
uint32_t pressed_times = 0;
char *old_pressed_times, new_pressed_times[30] = {0};
old_pressed_times = ef_get_env("pressed_times");
pressed_times = atol(old_pressed_times);
pressed_times++;
sprintf(new_pressed_times, "%d", pressed_times);
printf("The Key Pressed %d times\r\n", pressed_times);
ef_set_env("pressed_times", new_pressed_times);
ef_save_env();
}
移植好后进行测试,按下按键一次后复位再次按下按键,正常情况下会有如下打印信息输出:
接着进行 Easyflash+EasyLogger 的log功能实现。 1. 将 EasyLogger 源码中所有文件夹拷贝到项目中,并且添加好头文件路径到编译目录中。
2. 将 EasyLogger 的几个接口进行实现,主要包括获取当前时间,这里使用 RTC 日历和时钟作为时间戳内容;输出接口配置为串口打印以及 Flash 保存 ;以及格式化并且启动 log 区。
if(elog_init() == EF_NO_ERR)
{
elog_set_fmt(ELOG_LVL_ASSERT, ELOG_FMT_ALL & ~ELOG_FMT_P_INFO);
elog_set_fmt(ELOG_LVL_ERROR, ELOG_FMT_LVL | (ELOG_FMT_TAG | ELOG_FMT_TIME));
elog_set_fmt(ELOG_LVL_WARN, ELOG_FMT_LVL | (ELOG_FMT_TAG | ELOG_FMT_TIME));
elog_set_fmt(ELOG_LVL_INFO, ELOG_FMT_LVL | (ELOG_FMT_TAG | ELOG_FMT_TIME));
elog_set_fmt(ELOG_LVL_DEBUG, ELOG_FMT_ALL & ~(ELOG_FMT_FUNC | ELOG_FMT_P_INFO));
elog_set_fmt(ELOG_LVL_VERBOSE, ELOG_FMT_ALL & ~(ELOG_FMT_FUNC | ELOG_FMT_P_INFO));
/* set EasyLogger assert hook */
elog_assert_set_hook(elog_user_assert_hook);
/* initialize EasyLogger Flash plugin */
elog_flash_init();
/* start EasyLogger */
elog_start();
}
3. 将 EasyLogger 的 elog_cfg.h 文件中的几个重要参数进行配置,主要有缓冲区大小、输出开关、输出级别等等,功能多多参数多多,修改宏定义即可。
4. 使用 shell elog_test 命令进行 elog 内容的更新,再用 elog_flash 的相关命令进行读取、清除、写入等对 Flash log 区的操作,还可以使用 elog 相关命令开启/关闭 log 功能。
具体代码在此不贴了,下图为实际测试结果,可以看出带 RTC 时间戳的日志内容在芯片复位后仍旧保存好了:
最后进行 EasyFlash 的 IAP 功能实现。这个 IAP 功能的流程为 boot 启动后先读取 ENV 区域 IAP 相关的参数,如果 app 程序中改写了 ENV 参数为需要进行 boot 升级,则初始化好外设后转到 update 函数进行下载,成功通过 Ymodem 串口文件传输协议获取到更新 app 程序后,将其从备份区拷贝至 app 区,然后将 ENV 参数改为直接 jump 到 app ,然后复位;复位后如果读到参数为无需 boot升级那么直接跳转到 app 中。
需要使用到 ef_erase_bak_app 和 ef_write_data_to_bak 两个接口函数,在 Ymodem 接收 app.bin 文件过程中进行数据的擦写,然后使用 ef_erase_user_app 和 ef_copy_app_from_bak 两个接口函数将更新的 app 程序从备份区搬运至 app 运行区。在此过程中还可以多加 1 个 ENV 参数用来保存是否有正常拷贝的状态,防止掉电过程中还在进行拷贝使得升级后的 app 程序没有完全被复制到运行区,此时可以通过该状态来进行判断,决定是否还需要重新下载更新后的 app或者重新进行复制操作。
测试时,未实现 app 区域的代码,只是用 32k 的规则数据来替代,且将 Flash 划分为 boot 区域大小 40k ,app 区域大小 44k (起始地址为 0x0800A000),整个备份区域大小 44k ,此处仅贴出关键代码:/******************************************************************************
* @brief Download a file using the ymodem protocol
* @param Buf: Address of the first byte
* @retval The size of the file
* @attention None
******************************************************************************/
static int32_t Ymodem_Download(uint8_t *Buf)
{
uint8_t PacketData[PACKET_1K_SIZE + PACKET_OVERHEAD];
uint8_t FileSize[FILE_SIZE_LENGTH];
uint8_t *pFilePtr;
uint8_t *pBufPtr ;
int32_t PacketLength, PacketsReceived, Errors, SessionBegin, Size = 0;
int32_t SessionDone, FileDone ;
uint32_t RamSource;
int32_t i;
for (SessionDone = 0, Errors = 0, SessionBegin = 0; ;)
{
for (PacketsReceived = 0, FileDone = 0, pBufPtr = Buf; ;)
{
switch (Receive_Packet(PacketData, &PacketLength, NAK_TIMEOUT))
{
case 0: /* 1K data received successfully */
Errors = 0;
switch (PacketLength)
{
/* Abort by sender */
case - 1:
shellPortWrite(ACK);
return 0;
/* End of transmission */
case 0:
shellPortWrite(ACK);
FileDone = 1;
break;
/* Normal packet */
default:
if ((PacketData[PACKET_SEQNO_INDEX] & 0xff) != \
(PacketsReceived & 0xff))
{
shellPortWrite(NAK);
}
else
{ /* File name (first package) */
if (PacketsReceived == 0)
{
/* Filename packet */
if (PacketData[PACKET_HEADER] != 0)/* File name */
{
/* Filename packet has valid data */
for (i = 0, pFilePtr = PacketData + \
PACKET_HEADER; (*pFilePtr != 0) && \
(i < FILE_NAME_LENGTH);)
{ /* Save file name */
FileName[i++] = *pFilePtr++;
}
FileName[i++] = '\0';
for (i = 0, pFilePtr ++; (*pFilePtr != ' ') && \
(i < (FILE_SIZE_LENGTH - 1));)
{ /* File size */
FileSize[i++] = *pFilePtr++;
}
FileSize[i++] = '\0';
/* Convert a string to an integer */
Str2Int(FileSize, &Size);
/* Test the size of the image to be sent */
/* Image size is greater than Flash size */
if (Size > (USER_FLASH_SIZE + 1))
{
/* End session */
shellPortWrite(CA);
shellPortWrite(CA);
return -1;
}
/* erase user application area */
ef_erase_bak_app(Size);
shellPortWrite(ACK);
shellPortWrite(CRC16);
}
/* Filename packet is empty, end session */
else
{
shellPortWrite(ACK);
FileDone = 1;
SessionDone = 1;
break;
}
}
/* Data packet */
else
{
/* Start receiving data after saving file information */
memcpy(pBufPtr, PacketData + PACKET_HEADER, \
PacketLength);
RamSource = (uint32_t)Buf;
/* write data of application to backup section */
if(ef_write_data_to_bak((uint8_t *)RamSource, PacketLength, &update_file_cur_size, Size) == EF_NO_ERR)
{
shellPortWrite(ACK);
}
else
{/*An error occurred while writing to Flash memory*/
/* End session */
shellPortWrite(CA);
shellPortWrite(CA);
return -2;
}
}
PacketsReceived ++;
SessionBegin = 1;
}
}
break;
case 1:
shellPortWrite(CA);
shellPortWrite(CA);
return -3;
default:
if (SessionBegin > 0)
{
Errors ++;
}
if (Errors > MAX_ERRORS)
{
shellPortWrite(CA);
shellPortWrite(CA);
return 0;
}
shellPortWrite(CRC16);
break;
}
if (FileDone != 0)
{
break;
}
}
if (SessionDone != 0)
{
break;
}
}
return (int32_t)Size;
}
/**
* update command for Letter-shell command.
*/
void EasyFlash_Ymodem_IAP(void)
{
char c_file_size[11] = {0};
printf("Please select a update file and use Ymodem to send.\r\n");
UART_Cmd(UART1, DISABLE);
UART_ClearITPendingBit(UART1,UART_IT_RXIEN);
UART_ITConfig(UART1, UART_IT_RXIEN, DISABLE);
UART_Cmd(UART1, ENABLE);
if (!Ymodem_Receive())
{
/* wait some time for terminal response finish */
SysTick_DelayMS(1000);
/* set need copy application from backup section flag is 1, backup application length */
ef_set_env("iap_need_copy_app", "1");
sprintf(c_file_size, "%d", update_file_total_size);
ef_set_env("iap_copy_app_size", c_file_size);
ef_save_env();
/* copy downloaded application to application entry */
if (ef_erase_user_app(APPLICATION_ADDRESS, update_file_total_size)
|| ef_copy_app_from_bak(APPLICATION_ADDRESS, update_file_total_size)) {
printf("Update user app fail.\n");
}
else
{
printf("Update user app success.\n");
}
/* clean need copy application from backup section flag */
ef_set_env("iap_need_copy_app", "0");
ef_set_env("iap_copy_app_size", "0");
ef_save_env();
}
else
{
/* wait some time for terminal response finish */
SysTick_DelayMS(1000);
printf("Update user app fail.\n");
}
UART_Cmd(UART1, DISABLE);
UART_ITConfig(UART1, UART_IT_RXIEN, ENABLE);
UART_Cmd(UART1, ENABLE);
}
SHELL_EXPORT_CMD(update, EasyFlash_Ymodem_IAP, EasyFlash Ymodem IAP);
可以通过 shell jump 命令和 update 命令进行跳转至 app 以及 更新 app 程序的功能,并且查验 Flash 对应地址的数据情况来确认串口升级功能是调通的,正常操作流程如下 GIF : 到此已经完成了所有功能的实验,初步感受了一把 EasyFlash 带来的存储美学,后面会再对其升级进阶版 FlashDB 进行移植,并且将其 ENV 功能用于实际项目中。
四、附件内容附件资源包中有以下内容可供参考下载: - MM32F0273D7P LQFP64 封装芯片官方最小系统板原理图 —— 1. MM32_LQFP64_CoreBoard V1.0.pdf
- MM32F0273D7P 官方库例程 —— 2. MM32F0270_Libsamples.zip
- EasyFlash ENV/LOG/IAP 全功能代码包 —— 3. MM32F0273D7P_EasyFlash_ENV_LOG_IAP_LetterShell.zip
五、参考资源本文创作参阅学习了以下下资源,在此声明感谢! https://mculover666.blog.csdn.net/article/details/105715982 https://zhuanlan.zhihu.com/p/136168426
|
站在巨人的肩膀上,让基于FLASH储存器应用的开发变得更为简单灵活,如果可以测试擦写速度会更好。