1. 项目概述为什么要在RT-Thread上折腾FAL和片上Flash搞嵌入式开发的尤其是用STM32的兄弟估计都遇到过类似的需求设备运行中需要保存一些参数比如Wi-Fi的SSID密码、校准数据、运行日志或者OTA升级时的固件备份。最开始很多人包括我可能会图省事直接上外部EEPROM或者SPI Flash。但回头一想STM32F407这类芯片内部自带高达1MB的Flash放着不用岂不是浪费直接用片上Flash存数据能省下一颗外置芯片的成本和PCB面积对于成本敏感或者空间紧凑的项目来说诱惑力巨大。然而直接操作STM32的片上Flash可不是调用一个fwrite那么简单。你需要面对擦除的最小单位扇区、写入的最小单位字或半字、擦写寿命限制、以及最头疼的——如何在实时操作系统RT-Thread中安全、高效、方便地管理这些操作。如果每个应用都自己写一套底层驱动代码会变得混乱且难以维护。这时候RT-Thread的FAL (Flash Abstraction Layer) 组件就派上用场了。简单说FAL就是一个“闪存抽象层”。它把不同种类、不同位置的Flash比如片内Flash、片外SPI Flash、片外QSPI Flash统一管理起来向上层应用提供一套标准的、类似文件系统的API如fal_read,fal_write,fal_erase。对于开发者而言你不再需要关心当前操作的是STM32F407的内部Flash还是W25Q64调用同样的接口就行。这对于实现OTA、文件系统LittleFS、FATFS、甚至简单的参数存储库如EasyFlash都至关重要。本次要做的就是在RT-Thread Nano或标准版上为STM32F407的片上Flash配置好FAL驱动让你能像操作普通存储设备一样安全可靠地读写芯片内部的宝贵空间。2. 核心思路与FAL组件架构解析在动手写代码之前我们必须先吃透FAL的设计思想。它不是一个简单的驱动封装而是一个清晰的分层架构。理解了这个后面配置起来才能心中有数出了问题也知道该去哪一层排查。2.1 FAL的三层模型FAL从下到上分为三层Flash设备、分区、操作接口。Flash设备层 (Flash Device)这是最底层直接和硬件打交道。它定义了一块物理上连续的Flash存储区域并实现了针对这块区域的最基本操作初始化、读取、写入、擦除。对于STM32F407的片上Flash我们就需要在这一层实现一个“设备”。FAL已经为我们提供了很多现成的设备驱动比如fal_flash_stm32f2_port.cSTM32F2/F4系列通用我们通常只需要稍作修改就能用。分区表层 (Partition Table)这是FAL的核心管理概念。一块物理Flash设备比如1MB的内部Flash可能被划分为多个逻辑分区每个分区有独立的名称、起始偏移、大小和所属的Flash设备。例如我们可以把1MB的Flash划分为bootloader: 0x08000000 - 0x0800FFFF (64KB)存放引导程序。app: 0x08010000 - 0x0807FFFF (448KB)存放主应用程序。download: 0x08080000 - 0x080BFFFF (256KB)存放OTA下载的临时固件。params: 0x080C0000 - 0x080FFFFF (256KB)存放系统参数。 分区信息通常以一个结构体数组的形式定义在代码中。上层应用通过分区名如params来访问特定区域完全不用关心具体的物理地址。操作接口层 (API)这是对开发者暴露的接口。FAL提供了两类主要API设备操作API如fal_flash_read/write/erase需要指定具体的Flash设备对象。更底层一般不直接使用。分区操作API如fal_partition_read/write/erase通过分区名来操作这是推荐的使用方式。此外还有fal_partition_find用于查找分区等。这种分层的好处是解耦和灵活。更换Flash芯片比如从片内换到片外QSPI时你只需要更换或新增Flash设备驱动并更新分区表的设备指向上层的应用代码几乎不用动。2.2 关键配置文件的角色在RT-Thread项目中FAL的配置主要通过两个文件完成rtconfig.h通过宏定义来开启FAL组件及其相关功能。例如#define RT_USING_FAL #define FAL_DEBUG_CONFIG #define FAL_PART_HAS_TABLE_CFG #define FAL_USING_SFUD_PORT // 如果你同时使用SFUD驱动外部SPI Flashfal_cfg.h这是核心配置文件。我们需要在这里做三件大事定义并注册我们使用的Flash设备struct fal_flash_dev。定义分区表struct fal_partition。声明设备表和分区表。我们的主要工作就是围绕fal_cfg.h展开。3. 针对STM32F407的Flash设备驱动实现现在进入实战环节。STM32F407VET6的Flash主存储块大小为1MB扇区结构比较复杂不同容量的扇区大小不同。这是我们配置的基础。3.1 扇区划分与地址规划首先必须查数据手册Reference Manual RM0090。以1MB型号为例其扇区划分如下Sector 0: 0x0800 0000 - 0x0800 3FFF (16 KB)Sector 1: 0x0800 4000 - 0x0800 7FFF (16 KB)Sector 2: 0x0800 8000 - 0x0800 BFFF (16 KB)Sector 3: 0x0800 C000 - 0x0800 FFFF (16 KB)Sector 4: 0x0801 0000 - 0x0801 FFFF (64 KB)Sector 5: 0x0802 0000 - 0x0803 FFFF (128 KB)Sector 6: 0x0804 0000 - 0x0805 FFFF (128 KB)Sector 7: 0x0806 0000 - 0x0807 FFFF (128 KB)Sector 8: 0x0808 0000 - 0x0809 FFFF (128 KB)Sector 9: 0x080A 0000 - 0x080B FFFF (128 KB)Sector 10: 0x080C 0000 - 0x080D FFFF (128 KB)Sector 11: 0x080E 0000 - 0x080F FFFF (128 KB)注意这个扇区结构是硬件决定的不可更改。在规划分区时分区的起始和结束地址必须严格对齐到扇区边界。否则擦除和写入操作会失败甚至导致硬件错误。一个典型的、兼顾了Bootloader、OTA和参数存储的地址规划如下bootloader: Sector 0-3 (0x08000000 - 0x0800FFFF, 64KB)app: Sector 4-5 (0x08010000 - 0x0803FFFF, 192KB)download: Sector 6-7 (0x08040000 - 0x0807FFFF, 256KB)params: Sector 8 (0x08080000 - 0x0809FFFF, 128KB) 这里预留了Sector 9-11作为后续扩展。app分区只用了192KB你需要根据你的实际固件大小调整。确保app分区足够存放你的应用程序。3.2 移植与修改Flash设备驱动RT-Thread的fal软件包中已经包含了STM32F2/F4系列的通用端口文件fal_flash_stm32f2_port.c。我们通常不需要从头写而是“移植”它。获取驱动文件通过RT-Thread Env工具或Studio的包管理器添加fal软件包。它通常会把这个端口文件放到项目的packages/fal-latest/ports目录下。复制到项目一个好的做法是将fal_flash_stm32f2_port.c和对应的头文件复制到你项目的applications或libraries目录下与你的应用代码放在一起方便修改和管理。关键修改点Flash大小和扇区信息检查stm32f2_onchip_flash结构体中的len总长度和blk_size擦除最小单元。对于STM32F407 1MBlen应为0x1000001MB。blk_size是“块”大小FAL的擦除操作基于“块”。为了简化我们通常将“块”定义为“扇区”。但STM32F4的扇区大小不统一所以这个端口文件内部会有一个更详细的sector_info表来处理不同大小的扇区。你需要确认这个表是否覆盖了你的芯片所有扇区。擦写函数端口文件已经实现了stm32_flash_read/write/erase函数它们内部调用了HAL库的HAL_FLASH_Program和HAL_FLASHEx_Erase。你需要确保你的工程已经正确包含了STM32的HAL库或标准外设库并且Flash相关的时钟已经初始化通常在上电后默认就是开启的。设备名称stm32f2_onchip_flash结构体中的.name字段很重要比如这里叫stm32_onchip。后续在分区表里分区所属的设备名就要和这个对应上。实操心得直接使用官方端口文件时务必仔细阅读文件开头的注释并核对sector_info数组。我曾经遇到过因为sector_info表没有正确包含所有扇区导致擦除高地址分区时一直失败的情况。最简单的验证方法是在初始化后调用fal_flash_init或直接测试擦写。4. 分区表定义与系统集成设备驱动准备好后接下来就是在fal_cfg.h中定义设备和分区并将FAL集成到系统中。4.1 编写fal_cfg.h这是整个配置的灵魂。我们创建一个fal_cfg.h文件通常放在ports目录下或项目根目录。#ifndef _FAL_CFG_H_ #define _FAL_CFG_H_ #include rtthread.h #include board.h /* Flash device Configuration */ extern const struct fal_flash_dev stm32f2_onchip_flash; /* flash device table */ #define FAL_FLASH_DEV_TABLE \ { \ stm32f2_onchip_flash, \ } /* Partition Configuration */ #ifdef FAL_PART_HAS_TABLE_CFG /* 分区表定义名称 Flash设备名 偏移 大小 标志 */ #define FAL_PART_TABLE \ { \ {FAL_PART_MAGIC_WORD, bootloader, stm32_onchip, 0x08000000, 64 * 1024, 0}, \ {FAL_PART_MAGIC_WORD, app, stm32_onchip, 0x08010000, 192 * 1024, 0}, \ {FAL_PART_MAGIC_WORD, download, stm32_onchip, 0x08040000, 256 * 1024, 0}, \ {FAL_PART_MAGIC_WORD, params, stm32_onchip, 0x08080000, 128 * 1024, 0}, \ } #endif /* FAL_PART_HAS_TABLE_CFG */ #endif /* _FAL_CFG_H_ */代码解析与注意事项extern ... stm32f2_onchip_flash;声明外部定义的Flash设备对象它是在fal_flash_stm32f2_port.c中定义的。FAL_FLASH_DEV_TABLEFlash设备注册表。如果有多个Flash如片内片外SPI就在这里用逗号隔开依次添加。FAL_PART_TABLE在FAL_PART_HAS_TABLE_CFG宏开启时生效。每个分区是一个结构体FAL_PART_MAGIC_WORD魔数固定这么写。bootloader分区名后续API通过这个名字查找分区。stm32_onchip必须与Flash设备对象中的.name字段完全一致这是最容易出错的地方之一。0x08000000分区的起始绝对地址。64 * 1024分区大小单位字节。这里用计算表达式更清晰。0标志位一般填0。重要提示确保app分区的起始地址0x08010000与你的工程链接脚本.ld文件中应用程序的起始地址完全一致否则程序无法运行。在Keil或IAR中这通常在“Target Options”里设置在GCC如STM32CubeIDE中需要修改链接脚本的FLASH区域起始地址。4.2 系统初始化与调用配置好后需要在系统启动时初始化FAL。在rtconfig.h中开启宏#define RT_USING_FAL #define FAL_DEBUG_CONFIG // 调试阶段建议开启会打印更多信息 #define FAL_PART_HAS_TABLE_CFG // 使用我们定义的分区表在应用代码中初始化例如在main.c的main函数里或创建一个独立的fal_init.c#include fal.h int fal_init(void) { /* 初始化FAL库 */ fal_init(); /* 检查分区表是否正常 */ fal_show_part_table(); return 0; } /* 使用MSH命令导出方便调试 */ MSH_CMD_EXPORT(fal_init, init fal);调用fal_init()会自动注册我们fal_cfg.h中定义的设备和分区。fal_show_part_table()是一个非常有用的调试函数它会在终端打印出所有已注册的分区信息让你一目了然地检查配置是否正确。编译与下载编译工程时务必确保链接地址和分区地址匹配。下载程序时不要擦除整个芯片否则会清掉bootloader分区如果已有。在下载配置中通常可以指定擦除范围如Erase Sectors而不是Erase Full Chip或者通过调试器脚本只擦除app分区。5. 应用层读写操作与高级用法FAL初始化成功后我们就可以在应用层像操作文件一样使用Flash了。5.1 基础读写擦除示例#include fal.h void test_fal_operations(void) { int ret; size_t len; const struct fal_partition *part RT_NULL; uint8_t write_buf[256]; uint8_t read_buf[256]; /* 1. 查找分区 */ part fal_partition_find(params); if (part RT_NULL) { rt_kprintf(Error: Partition params not found!\n); return; } rt_kprintf(Found partition: %s, size: %ld\n, part-name, part-len); /* 2. 准备测试数据 */ for (int i 0; i sizeof(write_buf); i) { write_buf[i] i 0xFF; } /* 3. 擦除分区必须先擦除才能写*/ ret fal_partition_erase(part, 0, part-len); // 擦除整个分区 if (ret 0) { rt_kprintf(Erase partition failed! ret %d\n, ret); return; } rt_kprintf(Partition erased.\n); /* 4. 写入数据 */ len fal_partition_write(part, 0, write_buf, sizeof(write_buf)); if (len ! sizeof(write_buf)) { rt_kprintf(Write data failed! written: %d\n, len); return; } rt_kprintf(Write %d bytes OK.\n, len); /* 5. 读取数据并校验 */ rt_memset(read_buf, 0, sizeof(read_buf)); len fal_partition_read(part, 0, read_buf, sizeof(read_buf)); if (len ! sizeof(read_buf)) { rt_kprintf(Read data failed! read: %d\n, len); return; } if (rt_memcmp(write_buf, read_buf, sizeof(write_buf)) 0) { rt_kprintf(Data verification PASSED!\n); } else { rt_kprintf(Data verification FAILED!\n); } } MSH_CMD_EXPORT(test_fal_operations, test fal r/w/e);5.2 结合文件系统LittleFSFAL更强大的地方在于可以作为底层存储驱动挂载文件系统。以RT-Thread常用的LittleFS为例在rtconfig.h中开启LittleFS#define RT_USING_DFS #define RT_USING_DFS_ELMFAT #define RT_USING_DFS_LITTLEFS #define LFS_READ_SIZE 256 #define LFS_PROG_SIZE 256 #define LFS_BLOCK_SIZE 4096 // 与Flash扇区大小对齐取最大公约数 #define LFS_CACHE_SIZE 256 #define LFS_LOOKAHEAD_SIZE 16在应用代码中格式化和挂载#include dfs_fs.h int mnt_init(void) { struct rt_device *flash_dev; /* 获取名为 params 的分区对应的块设备 */ flash_dev fal_blk_device_create(params); if (flash_dev RT_NULL) { rt_kprintf(Cant create block device on params partition.\n); return -RT_ERROR; } /* 第一次使用时需要格式化 */ if (dfs_mkfs(lfs, params) ! 0) { // 注意这里格式化的是分区名 rt_kprintf(mkfs failed, maybe already formatted.\n); } /* 挂载到 /flash 目录 */ if (dfs_mount(params, /flash, lfs, 0, 0) 0) { rt_kprintf(LittleFS mounted on /flash successfully.\n); } else { rt_kprintf(LittleFS mount failed!\n); return -RT_ERROR; } return RT_EOK; } INIT_APP_EXPORT(mnt_init); // 自动初始化挂载成功后你就可以使用标准的POSIX文件APIopen,read,write,close在/flash目录下操作文件了所有数据都会被存储到STM32的内部Flash中。5.3 结合EasyFlash管理参数如果你只是想管理键值对参数EasyFlash是比直接操作文件更轻量、更专业的选择。它底层依赖FAL。通过Env工具添加EasyFlash软件包。在rtconfig.h中配置EasyFlash#define PKG_USING_EASYFLASH #define EF_START_ADDR (0x08080000) // 必须等于你分配给EasyFlash的分区起始地址 /* 其他EF_XXX_SIZE参数根据分区大小调整 */初始化#include easyflash.h int ef_init(void) { easyflash_init(); rt_kprintf(EasyFlash initialized.\n); /* 示例保存和读取一个参数 */ ef_set_env(device_id, 123456); char id[32] {0}; ef_get_env(device_id, id, sizeof(id)); rt_kprintf(device_id: %s\n, id); return RT_EOK; } INIT_APP_EXPORT(ef_init);6. 深度避坑指南与疑难排查配置和使用过程中你会遇到各种坑。下面是我踩过的一些以及解决方法。6.1 常见问题速查表问题现象可能原因排查步骤与解决方案fal_partition_find返回NULL1. 分区名拼写错误。2.fal_init()未调用或调用失败。3.fal_cfg.h中分区表未正确编译。1. 检查代码中分区名字符串是否与FAL_PART_TABLE中完全一致。2. 确保fal_init()在调用find之前执行并检查其返回值。3. 调用fal_show_part_table()看分区是否被打印出来。擦除或写入失败返回错误码1. 地址未对齐擦除需扇区对齐写入需字/半字对齐。2. 目标地址处于写保护状态如系统Bootloader区域。3. Flash硬件故障或寿命耗尽概率极低。1.最重要检查传入fal_partition_erase/write的偏移地址(offset)和长度。对于STM32F4写入地址必须是4字节字对齐数据长度最好是4的倍数。擦除地址必须是扇区起始地址。2. 检查芯片的写保护选项字节Option Bytes确保要操作的扇区未被保护。可以使用STM32CubeProgrammer工具查看和修改。3. 使用HAL_FLASH_Unlock/Lock相关函数检查HAL库状态。程序下载后无法运行或运行异常1. 应用程序链接地址与app分区地址不匹配。2. 中断向量表地址未重映射如果Bootloader跳转。3. 擦除了正在运行的程序自身所在的扇区。1. 核对IDE中的Flash起始地址设置和链接脚本(.ld文件)中的ORIGIN值必须与app分区起始地址一致。2. 如果使用了Bootloader在跳转到App前需要调用SCB-VTOR APP_ADDRESS 0xFFFFFF80;重设向量表偏移。3.绝对不要在运行时擦除当前代码所在的扇区这会导致立即崩溃。OTA时新固件应写入download分区由Bootloader负责搬运和切换。数据写入后读回来部分正确部分错误1. 写入的数据类型与函数要求不符。2. 发生了“读改写”操作而未先擦除。1. STM32的Flash写入函数HAL_FLASH_Program要求数据是uint64_t双字、uint32_t字或uint16_t半字。FAL端口驱动已经处理了这些但如果你直接调用底层函数需注意。2. Flash特性是只能把1写成0不能把0写成1。所以写入前必须擦除擦除后全为1。确保每次写入前目标区域已被擦除。fal_show_part_table打印信息异常1. Flash设备驱动初始化失败。2. 分区表地址或大小计算溢出。1. 检查Flash设备驱动中的sector_info表是否完整覆盖了分区地址范围。2. 仔细计算分区起始地址和大小确保没有超出Flash设备总长度且地址是递增的。6.2 高级技巧与注意事项优化写寿命STM32F407的Flash典型擦写寿命是1万次。频繁擦写固定区域会使其提前失效。对策磨损均衡对于参数存储可以使用EasyFlash它内部实现了简单的磨损均衡算法。日志式存储不直接覆盖旧数据而是追加新数据标记旧数据无效。定期进行垃圾回收整理碎片。LittleFS文件系统本身就采用了这种策略。减少擦写频率合并多次参数变更一次性写入。使用RAM缓存定时或事件触发时再写回Flash。中断与多线程安全Flash擦写操作耗时很长擦除一个128KB扇区可能上百毫秒。在此期间如果发生中断或其他任务抢占可能导致操作失败或系统不稳定。关键操作加锁在调用fal_partition_erase/write前后使用RT-Thread的调度器锁(rt_enter_critical/rt_exit_critical)或互斥锁确保操作原子性。放在低优先级线程将Flash操作放在一个专门的、低优先级的线程中避免阻塞关键任务。电源稳定性Flash操作期间系统电压必须稳定。在电池供电或可能突然掉电的场景中需要增加硬件掉电检测电路和软件保护。在检测到电压过低时应立即终止Flash操作。调试利器fal命令RT-Thread的FAL组件提供了丰富的MSH命令务必善用fal显示所有分区信息。fal probe [part_name]切换当前操作的分区。fal read addr size/fal write addr data/fal erase addr size直接在shell中读写擦除测试极其方便。配置FAL的过程本质上是在RT-Thread生态下为你的芯片建立一套稳定的存储管理标准。一旦打通后续无论是加文件系统、做OTA还是存参数都会变得非常顺畅。虽然前期配置需要仔细核对地址、对齐这些细节有点繁琐但这份投入在项目后期会带来巨大的维护和扩展便利性。