本帖最后由 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
|