yang377156216 发表于 2022-5-8 00:36

EasyFlash 是什么?还能支持 IAP ?移到 MM32F0273 上试试

本帖最后由 yang377156216 于 2022-5-8 00:46 编辑

#申请原创#@21小跑堂

整体概览上一篇记录了 Flash 模拟 EEPROM 的应用,用于存储较少数的用户参数比较适合,它作为操作 Flash 的一种底层存储介质,此外,还可以用 EasyFlash / FlashDB 等 KV 数据库作为介质,还可以用文件系统充当媒介。后两种类型更加适合用于保存大量的日志数据,或者是不同数据类型的数据。本文将记录我移植开源的 EasyFlash 组件,使用内嵌 Flash 存储空间来实现用户数据存储记录的功能的过程。主要分为以下几个部分内容:
[*]MM32F0273D7P 的内嵌 Flash 介绍
[*]EasyFlash 是什么
[*]EasyFlash 三大功能在 MM32F0273D7P 上实现
[*]附件内容
[*]参考资源

一、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 片上存储器。主要包括三大实用功能 :
[*]ENV 快速保存产品参数,支持 写平衡(磨损平衡)及掉电保护功能
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 接口如下,可以进行简单的增删改查:
[*]Log 无需文件系统,日志可直接存储在 Flash 上
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 接口如下:
[*]IAP 支持在线升级功能
EasyFlash 库还封装了 IAP (In-Application Programming)功能常用的接口,支持 CRC32 校验,同时支持 Bootloader 及 Application 的升级。在备份区额外地增加了一块区域专门用于暂存更新后的应用程序,加上使用 Ymodem 串口传输文件协议,利用 ENV 保存升级过程的参数以及一系列的判断流程即可完成 IAP 框架。整个 EasyFlash 将备份区划分如下:三、EasyFlash 三大功能在 MM32F0273D7P 上实现硬件资源如下:
[*]一块 MM32F0273D7P 芯片的核心板,这里选用灵动微官方提供的红色最小系统板
[*]一个 DAP-Link 调试器,我选用的是创芯工坊的 PW-Link ,既能供电又能虚拟出串口


软件资源如下:
- 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. 实现底层驱动必须接口。
[*]EasyFlash 初始化。可以传递默认环境变量并且初始化 EasyFlash 所需的资源等等。
/** * Flash port for hardware initialize. * * @param default_env default ENV set for user * @param default_env_size default ENV size * * @return 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);
    return result;
}
[*]EasyFlash 读取。最小单位为4个字节。
/** * Read data from flash. * @NOTE This operation's units is word. * * @param addr flash address * @param buf buffer to store read data * @param size read bytes size * * @return 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;
}
[*]EasyFlash 擦除。最小单位为一页即为 1024 字节。
/** * Erase data on flash. * @NOTE 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;
}
[*]EasyFlash 写入。最小单位为 4 字节。
/** * Write data to flash. * @note This operation's units is word. * @note This operation must after erase. @see 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;
}
[*]对环境变量缓冲区加锁/解锁。为了保证 RAM 缓冲区在并发执行的安全性,所以需要对其进行加锁(如果项目的使用场景不存在并发情况,则可以忽略)。有操作系统时可以使用获取/释放信号量来加锁/解锁,裸机时可以通过关闭/开启全局中断来加锁/解锁。这里使用的为裸机程序。
/**
* 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();
}
[*]EasyFlash 打印调试日志信息
static char log_buf;

/** * 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(%s:%ld) ", file, line);
    vsprintf(log_buf, format, args);
    ef_print("%s", log_buf);
    printf("\r\n");
    va_end(args);
#endif
}
[*]EasyFlash 打印普通日志信息
/** * 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");
    /* must use vprintf to print */
    vsprintf(log_buf, format, args);
    ef_print("%s", log_buf);
    printf("\r\n");
    va_end(args);
}
[*]EasyFlash 无格式打印信息
/** * 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);
}
[*]EasyFlash 默认环境变量集合定义。
/* 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 变量进行存储、取出以及修改后再存储的实验。
/******************************************************************************* * @brief      * @param      * @retval       * @Attention   *******************************************************************************/
void EasyFlash_Demo(void)
{
    uint32_t startup_times = 0;
    char *old_startup_times, new_startup_times = {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();
}

/*******************************************************************************
* @brief      
* @param      
* @retval      
* @Attention   
*******************************************************************************/
void KEY_Handler(void)
{
    uint32_t pressed_times = 0;
    char *old_pressed_times, new_pressed_times = {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 ,此处仅贴出关键代码:
[*]

/******************************************************************************
* @briefDownload a file using the ymodem protocol
* @paramBuf: Address of the first byte
* @retval The size of the file
* @attentionNone
******************************************************************************/
static int32_t Ymodem_Download(uint8_t *Buf)
{
    uint8_t PacketData;
    uint8_t FileSize;
    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 & 0xff) !=             \
                  (PacketsReceived & 0xff))
                  {
                        shellPortWrite(NAK);
                  }
                  else
                  {/* File name (first package) */
                        if (PacketsReceived == 0)
                        {
                            /* Filename packet */
                            if (PacketData != 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 = *pFilePtr++;
                              }
                              FileName = '\0';
                              for (i = 0, pFilePtr ++; (*pFilePtr != ' ') && \
                              (i < (FILE_SIZE_LENGTH - 1));)
                              {/* File size */
                                    FileSize = *pFilePtr++;
                              }
                              FileSize = '\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 = {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/105715982https://zhuanlan.zhihu.com/p/136168426




guijial511 发表于 2022-5-11 20:01

给力,学习了。

sparrow054 发表于 2022-5-17 20:46

真是大神啊

tpgf 发表于 2022-6-3 08:48

第一次了解EasyFlash

drer 发表于 2022-6-3 09:01

一般什么种类的flash支持iap啊

qcliu 发表于 2022-6-3 09:12

非常郁闷单位不让安装啊

coshi 发表于 2022-6-3 09:21

看着介绍是移植无压力啊

kxsi 发表于 2022-6-3 09:36

这个工具用起来顺手吗

wiba 发表于 2022-6-3 09:44

其他型号的也都支持吧

vincent25 发表于 2025-5-20 16:55

写的挺好,思路清晰,把每一种功能都实例化,感谢分享。
页: [1]
查看完整版本: EasyFlash 是什么?还能支持 IAP ?移到 MM32F0273 上试试