STM32F407移植EasyFlash:嵌入式Flash键值存储与磨损均衡实战
1. 项目概述为什么要在STM32F407上折腾EasyFlash最近在做一个基于STM32F407的物联网终端设备功能上需要记录一些运行参数、用户配置还得在意外断电后能恢复现场。最开始想着用片内Flash模拟EEPROM自己写读写擦除逻辑但很快就遇到了麻烦存储空间得自己规划管理写之前要先擦除一大块还得考虑磨损均衡和掉电保护代码越写越复杂调试起来也头疼。这时候就想到了嵌入式圈里口碑不错的开源组件——EasyFlash。它本质上是一个轻量级的嵌入式Flash存储器库帮你把底层Flash的物理操作封装成了简单的“键值对”API你只需要关心存什么、取什么底下的存储管理、磨损均衡、掉电保护它都帮你搞定了。这对于需要可靠存储少量数据的STM32项目来说简直是“雪中送炭”。所以这个“移植过程记录”就是把我如何把EasyFlash这个“好帮手”请到STM32F407这片“土地”上安家落户的整个过程包括遇到的坑、解决的思路、最终的配置都详详细细地记下来。如果你也在为F4系列芯片的Flash存储管理发愁希望这篇记录能给你一条清晰的路径让你少走弯路快速用上这个利器。2. 移植前的核心思路与方案选型2.1 理解EasyFlash的架构与F407的Flash特性动手之前得先摸清两边的情况。EasyFlash的设计很清晰分为两层环境抽象层ENV这是我们主要使用的部分提供类似ef_set_env、ef_get_env的API让我们以“键值对”的方式操作数据。它内部实现了磨损均衡、掉电保护等高级功能。移植层Porting这是我们需要动手改造的部分。EasyFlash要操作Flash必须知道具体芯片的Flash大小、扇区结构、读写擦除接口。它通过一个ef_port.c文件和相关头文件来定义这些硬件相关的操作。再看STM32F407VET6这款芯片它拥有高达512KB的片内主存储Flash地址0x0800 0000开始以及一个独立的64KB的CCM RAM内核耦合内存地址0x1000 0000。Flash被划分为多个扇区Sector不同容量的F407扇区划分不同这是我们移植时需要精确对应的关键。为什么选择片内Flash而不是外置EEPROM或SPI Flash对于存储量不大通常几十KB足够、但对成本和PCB面积敏感的项目片内Flash是首选。它无需额外元器件可靠性高。EasyFlash的磨损均衡算法能有效延长片内Flash的寿命使其足以应对频繁的参数更新场景。当然如果你的数据量巨大超过几百KB或者写操作极其频繁外置存储器仍是更好选择。2.2 确定存储分区与地址规划这是移植中最关键的一步规划不好后面全是坑。我的规划原则是避开程序区预留升级空间方便管理。以我的STM32F407VET6512KB Flash为例结合常用的BootloaderIAP升级方案我做了如下规划程序存储区Application假设我的应用程序编译后大约200KB我从0x0800 0000开始存放。为了给Bootloader和未来程序增大留余地我决定应用程序只使用前256KB的空间Sector 0-3。EasyFlash环境变量区ENV Area我需要一块专门的Flash区域来存储键值对数据。我选择了最后一个扇区Sector 11。对于512KB的F407Sector 11的起始地址是0x0807 0000大小是128KB。这看起来很大但EasyFlash内部会将其作为“最小存储单元”进行管理实际存储效率很高且大空间有利于磨损均衡。Bootloader预留区虽然本次移植不涉及但好的习惯是预留。Sector 016KB或Sector 0-132KB常留给Bootloader。因此最终的内存映射规划如下0x0800 0000 - 0x0803 FFFF(256KB): 应用程序区Sector 0-30x0804 0000 - 0x0806 FFFF(192KB): 预留或其它数据区Sector 4-100x0807 0000 - 0x0807 FFFF(128KB):EasyFlash环境变量区Sector 11注意务必查阅你所使用具体型号的《参考手册》确认Flash扇区的准确划分。STM32F407xx系列中512KB和1MB版本的扇区地址是不同的绝不能照搬。2.3 获取与准备EasyFlash源码从GitHubRT-Thread-packages/easyflash或Gitee镜像下载最新稳定版的EasyFlash源码。核心文件我们只需要easyflash/inc/easyflash.h- 主要用户API头文件easyflash/src/ef_env.c,ef_env_legacy.c,ef_iap.c,ef_log.c,ef_utils.c- 核心功能源码easyflash/port/ef_port.c-移植关键文件需要重写easyflash/port/ef_port.h- 移植配置头文件将inc、src和port文件夹拷贝到你的项目工程目录下并添加到项目的头文件包含路径和编译源文件中。3. 移植层ef_port.c的详细实现与解析3.1 基础宏定义配置ef_port.h首先修改ef_port.h它定义了EasyFlash所需的平台相关宏。#ifndef _EF_PORT_H_ #define _EF_PORT_H_ #include stdint.h #include stm32f4xx_hal.h // 引入HAL库或标准外设库头文件 /* 使能不同类型的EasyFlash功能 */ #define EF_USING_ENV // 必须使能使用环境变量功能 // #define EF_ENV_USING_WL_MODE // 使能磨损均衡模式推荐 #define EF_USING_IAP // 可选使用IAP应用内编程功能 // #define EF_USING_LOG // 可选使用日志功能 /* 环境变量功能设置 */ #ifndef EF_ENV_USING_WL_MODE #define EF_ENV_USING_WL_MODE // 强烈建议使能磨损均衡 #endif #define EF_STR_ENV_VALUE_MAX_SIZE 256 // 单个环境变量值的最大长度 #define EF_ENV_AREA_SIZE (128 * 1024) // 为ENV分配的Flash总大小对应Sector 11的128KB /* IAP功能设置 */ #define EF_ERASE_MIN_SIZE (128 * 1024) // 擦除的最小单元对于F407一次最少擦除一个扇区这里设为Sector 11的大小 #define EF_WRITE_GRAN (1) // 写入粒度字节 /* 日志功能设置 */ // #define EF_LOG_AREA_SIZE (4 * 1024) // 如果使能LOG定义其大小 /* 移植接口函数声明 */ EfErrCode ef_port_init(ef_env const **default_env, size_t *default_env_size); EfErrCode ef_port_read(uint32_t addr, uint32_t *buf, size_t size); EfErrCode ef_port_erase(uint32_t addr, size_t size); EfErrCode ef_port_write(uint32_t addr, const uint32_t *buf, size_t size); void ef_port_env_lock(void); void ef_port_env_unlock(void); #endif /* _EF_PORT_H_ */关键点解析EF_ENV_USING_WL_MODE务必使能。在此模式下EasyFlash会将我们分配的大扇区128KB在逻辑上模拟成多个小扇区进行轮换写入实现磨损均衡极大延长Flash寿命。EF_ENV_AREA_SIZE必须等于你分配给ENV的Flash物理区域的实际字节大小。这里就是Sector 11的128KB131072字节。EF_ERASE_MIN_SIZE对于STM32F4擦除以扇区为单位。这里设置为128KB意味着EasyFlash在需要擦除时会一次性擦除整个ENV区。在磨损均衡模式下这是由库内部管理的我们只需提供这个最小擦除单位。3.2 硬件接口函数实现ef_port.c这是移植的核心我们需要实现ef_port.c中的几个硬件相关函数。#include ef_port.h #include string.h /* 定义EasyFlash环境变量区的起始地址对应Sector 11的起始地址 */ #define EF_ENV_AREA_START_ADDR ((uint32_t)0x08070000) /* 默认环境变量可选 */ static const ef_env default_env_set[] { {device_id, STM32F407-001, EF_STR_ENV_VALUE_MAX_SIZE}, {boot_count, 0, EF_STR_ENV_VALUE_MAX_SIZE}, /* 可以在此添加更多默认键值对 */ }; static const size_t default_env_set_size sizeof(default_env_set) / sizeof(default_env_set[0]); /** * EasyFlash移植初始化 * 在此函数中初始化Flash外设如果HAL库未自动初始化并返回默认环境变量集。 */ EfErrCode ef_port_init(ef_env const **default_env, size_t *default_env_size) { /* 对于STM32通常HAL库在main()初始化阶段已经初始化了Flash相关的时钟和设置。 这里我们通常不需要额外操作除非有特殊需求。*/ /* 检查Flash地址是否对齐到扇区起始地址可选但建议的检查 */ if (EF_ENV_AREA_START_ADDR % (128*1024) ! 0) { // 地址不对齐返回错误 return EF_READ_ERR; } /* 将默认环境变量集返回给EasyFlash核心 */ if (default_env ! NULL default_env_size ! NULL) { *default_env default_env_set; *default_env_size default_env_set_size; } return EF_NO_ERR; } /** * 从指定Flash地址读取数据 * param addr Flash内的起始地址相对EF_ENV_AREA_START_ADDR的偏移地址 * param buf 数据读取缓冲区 * param size 读取数据大小 */ EfErrCode ef_port_read(uint32_t addr, uint32_t *buf, size_t size) { uint32_t read_addr EF_ENV_AREA_START_ADDR addr; const uint32_t *source (const uint32_t *)read_addr; /* 安全检查确保读取范围在ENV区内 */ if (addr size EF_ENV_AREA_SIZE) { return EF_READ_ERR; } /* 直接内存拷贝因为Flash在代码区可读 */ memcpy(buf, source, size); return EF_NO_ERR; } /** * 擦除指定地址和大小的Flash区域 * param addr 起始地址相对偏移 * param size 擦除大小必须为EF_ERASE_MIN_SIZE的整数倍 */ EfErrCode ef_port_erase(uint32_t addr, size_t size) { HAL_StatusTypeDef hal_status; uint32_t start_sector_addr EF_ENV_AREA_START_ADDR addr; uint32_t end_addr start_sector_addr size; uint32_t current_addr start_sector_addr; /* 安全检查与对齐检查 */ if (addr % EF_ERASE_MIN_SIZE ! 0 || size % EF_ERASE_MIN_SIZE ! 0) { return EF_ERASE_ERR; } if (addr size EF_ENV_AREA_SIZE) { return EF_ERASE_ERR; } /* 解锁Flash控制寄存器 */ HAL_FLASH_Unlock(); /* 清除可能的错误标志 */ __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_OPERR | FLASH_FLAG_WRPERR | FLASH_FLAG_PGAERR | FLASH_FLAG_PGPERR | FLASH_FLAG_PGSERR); /* 循环擦除每一个扇区 */ while (current_addr end_addr) { // 获取当前地址对应的扇区编号 uint32_t sector FLASH_SECTOR_11; // 因为我们只用了Sector 11 // 注意如果你的ENV区跨多个物理扇区这里需要根据current_addr计算sector编号 FLASH_EraseInitTypeDef EraseInitStruct; uint32_t SectorError 0; EraseInitStruct.TypeErase FLASH_TYPEERASE_SECTORS; EraseInitStruct.Sector sector; EraseInitStruct.NbSectors 1; // 一次擦除一个扇区 EraseInitStruct.VoltageRange FLASH_VOLTAGE_RANGE_3; // 对于F407电压范围3对应2.7V-3.6V hal_status HAL_FLASHEx_Erase(EraseInitStruct, SectorError); if (hal_status ! HAL_OK) { HAL_FLASH_Lock(); return EF_ERASE_ERR; } current_addr EF_ERASE_MIN_SIZE; // 移动到下一个可擦除单元 } HAL_FLASH_Lock(); return EF_NO_ERR; } /** * 向指定Flash地址写入数据 * param addr 起始地址相对偏移 * param buf 数据缓冲区 * param size 写入数据大小必须为EF_WRITE_GRAN的整数倍即1字节的倍数 */ EfErrCode ef_port_write(uint32_t addr, const uint32_t *buf, size_t size) { HAL_StatusTypeDef hal_status; uint32_t write_addr EF_ENV_AREA_START_ADDR addr; const uint8_t *source (const uint8_t *)buf; // 按字节写入 /* 安全检查 */ if (addr size EF_ENV_AREA_SIZE) { return EF_WRITE_ERR; } /* 解锁Flash */ HAL_FLASH_Unlock(); /* 循环按字节或按字编程 */ for (size_t i 0; i size; i) { // STM32F4 Flash编程必须以16位半字或32位字为单位。 // 为了简化我们通常确保buf是32位对齐的并按字写入。 // 但EasyFlash传入的buf和size可能不保证字对齐。更健壮的做法是处理对齐。 // 这里展示一个简化版本假设按字节写入实际HAL库函数内部会处理。 // 更佳实践是使用HAL_FLASH_Program函数并处理地址和数据的对齐。 // 简化版使用HAL_FLASH_Program进行字节编程实际上HAL库可能以字为单位 // 注意此简化方法可能效率不高仅用于演示。生产代码需考虑对齐和效率。 hal_status HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE, write_addr i, source[i]); if (hal_status ! HAL_OK) { HAL_FLASH_Lock(); __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_PGERR | FLASH_FLAG_WRPERR); // 清除错误标志 return EF_WRITE_ERR; } } HAL_FLASH_Lock(); return EF_NO_ERR; } /* 环境变量操作锁用于RTOS环境或防止重入。在裸机环境下可简单实现或留空。 */ void ef_port_env_lock(void) { // 如果是裸机程序且确保不会在中断中调用ef_set/env_get可以什么都不做。 // 如果使用了RTOS这里应获取一个互斥锁mutex。 // __disable_irq(); // 一种简单的裸机锁方式但不推荐长期关中断 } void ef_port_env_unlock(void) { // __enable_irq(); // 对应解锁 }关键点与避坑指南地址转换ef_port_read/write/erase函数中的addr参数是相对于EF_ENV_AREA_START_ADDR的偏移地址不是绝对地址。在函数内部必须加上基地址才能得到物理Flash地址。这是最容易出错的地方之一。擦除函数实现ef_port_erase的参数size在磨损均衡模式下EasyFlash内部会保证其是EF_ERASE_MIN_SIZE的整数倍。我们的实现必须进行对齐检查。对于F407擦除必须以扇区为单位HAL_FLASHEx_Erase一次可以擦除一个或多个连续扇区。写入函数实现ef_port_write的效率和对齐问题是难点。上述简化版按字节编程效率极低且可能因地址不对齐导致硬件错误。生产环境推荐以下优化确保buf是32位对齐的__align(4)。在调用ef_port_write前由上层保证写入的addr是4字节对齐的EasyFlash内部在磨损均衡模式下通常能保证。在ef_port_write内部先处理可能的前部非对齐字节按字节写然后以字32位为单位循环写入中间对齐部分最后处理尾部非对齐字节。使用HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, data)进行字编程。Flash解锁与上锁每次擦写操作前后必须配对调用HAL_FLASH_Unlock()和HAL_FLASH_Lock()。并且在每次操作序列开始前最好清除之前的错误标志__HAL_FLASH_CLEAR_FLAG(...)。锁函数在裸机系统中如果主循环和中断都可能调用EasyFlash API则需要一个简单的锁来防止重入。最粗暴的方法是开关全局中断但会影响实时性。更好的方法是在调用ef_set/env_get的临界区用开关中断保护或者确保中断服务程序中不调用这些函数。4. 工程集成、初始化与基础API测试4.1 集成到MDK/IAR/STM32CubeIDE工程添加文件将easyflash/src/下的所有.c文件、easyflash/port/ef_port.c添加到你的工程的源文件组。将easyflash/inc和easyflash/port添加到头文件包含路径。配置Flash烧写算法在IDE的调试/烧写配置中确保你的Flash烧写算法覆盖了整个Flash范围包括我们使用的Sector 11。通常默认算法包含全部扇区但检查一下更保险。修改链接脚本可选但重要为了防止编译器将代码或常量存放到我们预留的EasyFlash区域Sector 11我们需要在链接脚本如STM32CubeIDE的.ld文件或Keil的Scatter File中明确排除该区域。Keil MDK示例在Options for Target - Linker中使用以下分散加载文件片段LR_IROM1 0x08000000 0x00040000 { ; 应用程序区256KB ER_IROM1 0x08000000 0x00040000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) } RW_IRAM1 0x20000000 0x00020000 { .ANY (RW ZI) } } ; 明确将0x08070000开始的128KB排除在自动分配之外STM32CubeIDE (GCC) 示例在STM32F407VETx_FLASH.ld的MEMORY部分可以定义一个不用于默认区域的内存块或者确保.text、.data等段不会落到该地址。4.2 初始化与基础测试代码在main.c的初始化部分在初始化硬件外设之后进入主循环之前添加EasyFlash初始化代码。#include easyflash.h int main(void) { HAL_Init(); SystemClock_Config(); // ... 初始化其他外设如UART用于打印调试信息 printf(EasyFlash Porting Test on STM32F407\r\n); /* 初始化EasyFlash */ if (easyflash_init() EF_NO_ERR) { printf(EasyFlash Init Success!\r\n); } else { printf(EasyFlash Init Failed!\r\n); while(1); // 初始化失败停机检查 } /* 测试1写入一个环境变量 */ if (ef_set_env(test_key, hello_easyflash) EF_NO_ERR) { printf(Set env test_key success.\r\n); } /* 测试2读取并打印该环境变量 */ char value[EF_STR_ENV_VALUE_MAX_SIZE] {0}; if (ef_get_env(test_key, value, sizeof(value)) EF_NO_ERR) { printf(Get env test_key %s\r\n, value); } /* 测试3更新一个已存在的环境变量会触发磨损均衡 */ if (ef_set_env(boot_count, 100) EF_NO_ERR) { printf(Update env boot_count success.\r\n); } char boot_count[32] {0}; ef_get_env(boot_count, boot_count, sizeof(boot_count)); printf(Current boot_count: %s\r\n, boot_count); /* 测试4保存当前所有环境变量到Flash在磨损均衡模式下ef_set_env可能不会立即保存 */ ef_save_env(); // 可以定期调用或在某些关键操作后调用 while (1) { // 主循环 } }4.3 编译、下载与现象观察编译确保无编译错误。特别注意ef_port.c中是否包含了正确的芯片头文件如stm32f4xx_hal_flash.h。下载将程序下载到芯片。观察通过串口助手查看打印信息。如果看到“EasyFlash Init Success!”以及后续的设置、读取成功信息说明移植基本成功。验证持久化关键测试复位或重新上电后再次运行程序。观察boot_count的值是否被成功保存并读取出来。如果每次复位后boot_count都能在上次的基础上递增你可以在代码里实现自增那就证明EasyFlash的存储功能完全正常。5. 高级功能配置、问题排查与性能优化5.1 启用与配置磨损均衡Wear-Leveling模式在ef_port.h中我们已经定义了EF_ENV_USING_WL_MODE。磨损均衡模式是EasyFlash的精华它通过将大的物理扇区模拟成多个小的逻辑扇区并采用“写时追加、满时回收”的策略使得Flash的擦写次数均匀分布寿命提升几个数量级。工作原理简述EasyFlash将你分配的EF_ENV_AREA_SIZE如128KB在逻辑上划分为多个固定大小的“扇区”可配置通常4KB。当你调用ef_set_env更新一个变量时EasyFlash不会在原地覆盖而是在当前活动的逻辑扇区末尾追加写入一个新的键值记录并将旧记录标记为无效。当活动逻辑扇区写满时EasyFlash会寻找一个空闲的逻辑扇区将所有有效的键值记录搬运过去然后擦除旧的、充满无效数据的逻辑扇区。如此循环实现了擦写操作在多个逻辑扇区间的均衡。配置参数在ef_cfg.h或ef_port.h中#define EF_ENV_USING_WL_MODE // 启用 #define EF_ERASE_GRAN (4096) // 磨损均衡内部管理的逻辑扇区大小建议4KB #define EF_ENV_USING_CACHE // 启用缓存提升读取速度 #define EF_ENV_CACHE_SIZE (2048) // 环境变量缓存大小启用缓存后频繁读取的环境变量会保存在RAM中速度极快只有在缓存未命中时才去读Flash。5.2 常见问题排查实录问题1初始化失败返回EF_INIT_FAILED。可能原因1ef_port_init函数中返回了错误。检查你的EF_ENV_AREA_START_ADDR地址是否正确是否对齐到物理扇区起始地址。可能原因2Flash硬件操作失败。检查ef_port_read函数在最开始的读取测试EasyFlash内部会读取魔术字是否成功。确保你的EF_ENV_AREA_START_ADDR地址区域没有被链接器分配其他内容如代码或常量。解决方法检查链接脚本确保该区域被排除。可能原因3堆栈大小不足。EasyFlash内部会使用一些动态内存例如在磨损均衡模式下。增大启动文件如startup_stm32f407xx.s中定义的堆Heap大小例如从默认的0x200增加到0x800或更大。问题2写入环境变量成功但复位后数据丢失。可能原因1没有调用ef_save_env()。在磨损均衡模式下ef_set_env可能只是更新了内存中的缓存需要调用ef_save_env()才会将脏数据真正写入Flash并执行必要的扇区回收操作。解决方法在每次设置完一批变量后或系统空闲时或准备进入低功耗前调用ef_save_env()。可能原因2写入或擦除函数ef_port_write/erase有bug但没返回错误。解决方法在ef_port_write和ef_port_erase函数中在HAL_FLASH_Program和HAL_FLASHEx_Erase调用后仔细检查返回值并可以读取回写的数据进行验证。可能原因3Flash被意外擦写。检查程序中是否有其他代码如Bootloader、错误的指针访问操作了EasyFlash所在的扇区。问题3系统运行一段时间后HardFault或数据错乱。可能原因1ef_port_write函数中的地址或数据对齐问题。STM32F4的Flash编程要求目标地址必须对齐到字4字节或半字2字节。解决方法实现一个健壮的ef_port_write处理非对齐起始地址、非对齐长度的情况。可能原因2中断打断了Flash擦写操作。Flash擦写期间不能响应任何中断包括SysTick。解决方法在ef_port_erase和ef_port_write函数内部在HAL_FLASH_Unlock()之后立即关闭全局中断__disable_irq()在HAL_FLASH_Lock()之后再开启__enable_irq()。注意这会短暂影响系统实时性确保擦写操作尽快完成。可能原因3堆栈或内存溢出。增大堆栈大小。问题4EasyFlash操作速度慢影响主循环。可能原因频繁调用ef_save_env()而Flash擦写本身是耗时操作擦除一个128KB扇区可能需要上百毫秒。优化策略批量操作集中修改多个环境变量最后调用一次ef_save_env()。异步保存在RTOS系统中可以创建一个低优先级的任务专门负责调用ef_save_env()。主任务通过信号量或消息队列触发保存请求。减少保存频率非关键数据可以定时保存如每10分钟或在系统空闲时保存。优化ef_port_write使用字编程代替字节编程减少函数调用和循环次数。5.3 性能优化与进阶技巧使用CCM RAM作为缓存针对STM32F4STM32F407的64KB CCM RAM只能由内核通过D-Bus访问速度极快且不会被DMA打扰。可以将EasyFlash的缓存如果使能了EF_ENV_USING_CACHE或者频繁访问的变量放到CCM RAM中。需要在链接脚本中定义CCM RAM区域并将相关数组用__attribute__((section(.ccmram)))指定到该区域。合理设置逻辑扇区大小EF_ERASE_GRAN逻辑扇区大小影响磨损均衡的粒度。设置太小如1KB会浪费空间存储元数据设置太大如32KB回收时搬运有效数据的时间长且可能造成空间浪费。4KB或8KB是一个比较平衡的选择。监控Flash寿命EasyFlash提供了ef_get_env_blob等API你可以读取其内部的元数据区需查阅源码了解结构估算出Flash已擦写的大致次数。这对于产品寿命预测很有价值。备份与恢复机制对于极其重要的参数除了存储在EasyFlash中还可以在初始化时读取并备份到另一个存储区如另一个Flash扇区、外置EEPROM实现双保险。移植过程就像一次精细的外科手术需要对供体EasyFlash和受体STM32F407都有清晰的了解。最重要的不是一次成功而是在遇到每一个编译错误、运行异常、数据丢失问题时能够依据原理通过逻辑分析、分段测试比如先单独测试Flash擦写函数等方法定位问题。当你看到设备历经多次断电重启关键数据依然完好如初时那种成就感就是对这次移植工作最好的回报。希望这份详细的记录能成为你手术台旁一份可靠的导航图。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2625175.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!