- 操作系统
- 嵌入式
- 物联网
- 嵌入式OS
- RTOS
【免费下载链接】rt-thread
RT-Thread is an open source IoT Real-Time Operating System (RTOS). https://rt-thread.github.io/rt-thread/
本篇技术指南以 RT-Thread 仓库components/fal/samples/porting/目录下的移植示例为核心,完整讲解在 RT-Thread 的 FAL(Flash Abstraction Layer,Flash 抽象层)中如何移植 Flash 设备、定义 Flash 设备表与分区表,并深入剖析 NOR Flash 读写擦除等底层操作的实现思路。读完本文后,你将能够在自己的 RT-Thread 工程中独立完成片内 Flash 与片外 SPI NOR Flash 的 FAL 移植,并正确配置分区以满足引导程序、应用程序、参数存储与 OTA 下载等典型场景。
FAL 移植总体脉络:从设备到分区的两级抽象
FAL 是 RT-Thread 提供的一层 Flash 抽象组件,它把"物理 Flash 芯片"抽象为Flash 设备(flash device),再把设备按地址划分成分区(partition),向上层应用(文件系统、OTA、EasyFlash 等)提供统一、带越界保护的数据读写接口。
从源码结构看,FAL 的移植工作由浅入深分为三步:
- 定义 Flash 设备对象:实现
init/read/write/erase四个操作函数,声明struct fal_flash_dev结构体(参考 fal_flash_stm32f2_port.c 与 fal_flash_sfud_port.c)。 - 定义 Flash 设备表:在
fal_cfg.h中用FAL_FLASH_DEV_TABLE宏把设备对象组织成表(参考 fal_cfg.h)。 - 定义 Flash 分区表:在
fal_cfg.h中用FAL_PART_TABLE宏划分各分区。
整个移植与初始化的调用关系在源码中清晰可见:上层只需调用 fal.c 中的fal_init(),它内部先调用 fal_flash.c 的fal_flash_init()初始化设备表,再调用 fal_partition.c 的fal_partition_init()加载分区表并做合法性校验。FAL 的整体分层关系如下图所示。
一、定义 Flash 设备:四个操作函数与设备对象
1.1 操作函数签名与参数约定
针对 Flash 的不同操作,需要实现如下四个函数(其中init可选):
static int init(void):可选的初始化操作,用于初始化 Flash 控制器、获取芯片信息等,没有初始化过程时可为空。static int read(long offset, rt_uint8_t *buf, rt_size_t size):读取操作。
| 参数 | 描述 |
|---|---|
| offset | 读取数据的 Flash 偏移地址 |
| buf | 存放待读取数据的缓冲区 |
| size | 待读取数据的大小 |
| return | 返回实际读取的数据大小 |
static int write(long offset, const rt_uint8_t *buf, rt_size_t size):写入操作。
| 参数 | 描述 |
|---|---|
| offset | 写入数据的 Flash 偏移地址 |
| buf | 存放待写入数据的缓冲区 |
| size | 待写入数据的大小 |
| return | 返回实际写入的数据大小 |
static int erase(long offset, rt_size_t size):擦除操作。
| 参数 | 描述 |
|---|---|
| offset | 擦除区域的 Flash 偏移地址 |
| size | 擦除区域的大小 |
| return | 返回实际擦除的区域大小 |
用户需要根据自己的 Flash 情况分别实现这些操作函数。注意offset是相对该 Flash 设备起始地址的偏移,底层访问物理地址时通常需要再加上设备基地址(如FLASH_START_ADDR + offset)。
1.2 Flash 设备对象结构:struct fal_flash_dev
设备对象的核心数据结构定义在 fal_def.h 中,移植时只需按实际硬件填写各字段:
struct fal_flash_dev { char name[FAL_DEV_NAME_MAX]; /* 设备名,FAL_DEV_NAME_MAX 默认 24 */ rt_uint32_t addr; /* Flash 设备起始地址 */ rt_size_t len; /* 设备总长度(字节) */ rt_size_t blk_size; /* 块大小,即擦除的最小粒度 */ struct { int (*init)(void); int (*read)(long offset, rt_uint8_t *buf, rt_size_t size); int (*write)(long offset, const rt_uint8_t *buf, rt_size_t size); int (*erase)(long offset, rt_size_t size); } ops; /* 操作函数集合 */ rt_size_t write_gran; /* 写入最小粒度,单位:位 */ struct flash_blk blocks[FAL_DEV_BLK_MAX]; /* 块信息,FAL_DEV_BLK_MAX 默认 6 */ };其中write_gran的单位是位(bit),取值约定:1(NOR Flash)、8(STM32F2/F4)、32(STM32F1)、64(STM32L4),置 0 表示不生效;blocks数组用于描述设备内部不均匀的物理块布局,count或size为 0 时视为结束。
1.3 示例一:STM32F2 片内 Flash 设备
以 fal_flash_stm32f2_port.c 为例。STM32F2 的片内 Flash 扇区大小不均匀(前 4 个扇区各 16KB、第 5 个扇区 64KB、其余扇区各 128KB),因此该示例在文件头部定义了 12 个扇区基地址宏(ADDR_FLASH_SECTOR_0~ADDR_FLASH_SECTOR_11),并实现了两个辅助函数:
stm32_get_sector(address):根据物理地址返回对应的FLASH_Sector_n编号;stm32_get_sector_size(sector):返回指定扇区的大小(16K/64K/128K),用于擦除时累加实际擦除量。
read实现为逐字节从地址空间读取;write先FLASH_Unlock()解锁、清错误标志,再逐字节调用FLASH_ProgramByte并读回校验,最后FLASH_Lock();erase则在解锁后循环按扇区擦除,直到累计擦除量不小于请求大小。
文件底部定义的设备对象如下(注意示例源码使用了 C99 指定初始化方式):
const struct fal_flash_dev stm32f2_onchip_flash = { .name = "stm32_onchip", .addr = 0x08000000, .len = 1024*1024, .blk_size = 128*1024, .ops = {init, read, write, erase}, .write_gran = 8 };各字段含义:
"stm32_onchip":Flash 设备的名字;0x08000000:对 Flash 操作的起始地址;1024*1024:Flash 的总大小(1MB);128*1024:Flash 块/扇区大小。因为 STM32F2 各块大小不均匀,所以擦除粒度为最大块的大小:128K;{init, read, write, erase}:Flash 的操作函数,如果没有 init 初始化过程,第一个操作函数位置可以置空;write_gran = 8:表示一次最少可写 8 位(1 字节),与该芯片按字节编程的特性一致。
1.4 示例二:基于 SFUD 的片外 SPI NOR Flash 设备
fal_flash_sfud_port.c 演示了基于SFUD(万能 SPI Flash 驱动)的 Flash 设备移植,整个文件用FAL_USING_SFUD_PORT宏包裹,需配合 Kconfig 中的FAL_USING_SFUD_PORT选项启用。其特点是在init()中动态获取 SFUD 设备并回填芯片信息:
static int init(void) { #ifdef RT_USING_SFUD sfud_dev = rt_sfud_flash_find_by_dev_name(FAL_USING_NOR_FLASH_DEV_NAME); #else extern sfud_flash sfud_norflash0; sfud_dev = &sfud_norflash0; #endif if (NULL == sfud_dev) return -1; /* update the flash chip information */ nor_flash0.blk_size = sfud_dev->chip.erase_gran; nor_flash0.len = sfud_dev->chip.capacity; return 0; }即设备表的blk_size(擦除粒度)与len(容量)在运行时从 SFUD 芯片信息中获取,随后read/write/erase直接透传sfud_read/sfud_write/sfud_erase即可。该文件默认设备名为norflash0,可通过FAL_USING_NOR_FLASH_DEV_NAME宏覆盖。
二、定义 Flash 设备表
Flash 设备表定义在fal_cfg.h头文件中,定义分区表前需新建fal_cfg.h文件(FAL 的公共头文件 fal.h 会通过#include <fal_cfg.h>引入它,因此该文件必须存在于工程 include 路径中)。
参考 示例文件 samples/porting/fal_cfg.h 或如下代码:
/* ===================== Flash device Configuration ========================= */ extern const struct fal_flash_dev stm32f2_onchip_flash; extern struct fal_flash_dev nor_flash0; /* flash device table */ #define FAL_FLASH_DEV_TABLE \ { \ &stm32f2_onchip_flash, \ &nor_flash0, \ }Flash 设备表中包含两个 Flash 对象:一个为 STM32F2 的片内 Flash(stm32f2_onchip_flash),一个为片外的 Nor Flash(nor_flash0)。该宏会被 fal_flash.c 展开为static const struct fal_flash_dev * const device_table[] = FAL_FLASH_DEV_TABLE;,因此表中只能放置已定义的设备对象的地址。
从fal_flash_init()的实现(fal_flash.c)可以看到初始化时的约束:
- 编译期强制检查:
FAL_FLASH_DEV_TABLE未定义会直接报错; - 每个设备的
read/write/erase通过RT_ASSERT断言非空; - 若
ops.init非空则先执行初始化,随后打印设备信息日志。
三、定义 Flash 分区
3.1 分区表定义
Flash 分区基于 Flash 设备,每个 Flash 设备又可以有 N 个分区,这些分区的集合就是分区表。在配置分区表前,务必保证已定义好 Flash 设备及设备表。
分区表也定义在fal_cfg.h头文件中,参考 示例文件 samples/porting/fal_cfg.h 或如下代码:
#define NOR_FLASH_DEV_NAME "norflash0" /* ====================== Partition Configuration ========================== */ #ifdef FAL_PART_HAS_TABLE_CFG /* partition table */ #define FAL_PART_TABLE \ { \ {FAL_PART_MAGIC_WORD, "bl", "stm32_onchip", 0, 64*1024, 0}, \ {FAL_PART_MAGIC_WORD, "app", "stm32_onchip", 64*1024, 704*1024, 0}, \ {FAL_PART_MAGIC_WORD, "easyflash", NOR_FLASH_DEV_NAME, 0, 1024*1024, 0}, \ {FAL_PART_MAGIC_WORD, "download", NOR_FLASH_DEV_NAME, 1024*1024, 1024*1024, 0}, \ } #endif /* FAL_PART_HAS_TABLE_CFG */上面这个分区表详细描述信息如下:
| 分区名 | Flash 设备名 | 偏移地址 | 大小 | 说明 |
|---|---|---|---|---|
| "bl" | "stm32_onchip" | 0 | 64KB | 引导程序 |
| "app" | "stm32_onchip" | 64*1024 | 704KB | 应用程序 |
| "easyflash" | "norflash0" | 0 | 1MB | EasyFlash 参数存储 |
| "download" | "norflash0" | 1024*1024 | 1MB | OTA 下载区 |
3.2 分区字段与注意事项
分区结构体struct fal_partition(定义于 fal_def.h)依次包含:magic_word(魔法字)、name(分区名)、flash_name(所属 Flash 设备名)、offset(分区在 Flash 设备内部的偏移)、len(分区大小)与reserved(保留字段)。示例中的分区定义采用{FAL_PART_MAGIC_WORD, 分区名, 设备名, 偏移, 大小, 保留位}的形式。
用户需要修改的分区参数包括:分区名称、关联的 Flash 设备名、偏移地址(相对 Flash 设备内部)、大小,需要注意以下几点:
- 分区名保证不能重复;
- 关联的 Flash 设备务必已经在 Flash 设备表中定义好,并且名称一致,否则会出现无法找到 Flash 设备的错误;
- 分区的起始地址和大小不能超过 Flash 设备的地址范围,否则会导致包初始化错误。
注意:每个分区定义时,除了填写上面介绍的参数属性外,需在前面增加
FAL_PART_MAGIC_WORD属性,末尾增加0(目前用于保留功能)。
这些约束在源码中有对应的强制校验逻辑。FAL_PART_MAGIC_WORD的实际值为0x45503130(见 fal_partition.c);check_and_update_part_cache()会逐条检查分区:通过fal_flash_device_find(table[i].flash_name)查找设备(找不到则告警并跳过),并检查table[i].offset >= flash_dev->len时直接报错并中止初始化。因此设备名不一致、偏移越界都会在fal_init()阶段暴露出来。
此外,Kconfig(components/fal/Kconfig)提供了两种分区表加载模式:默认开启的FAL_PART_HAS_TABLE_CFG(编译期静态分区表,即本文所述方式);关闭后则改为运行时从 Flash 指定位置(FAL_PART_TABLE_FLASH_DEV_NAME+FAL_PART_TABLE_END_OFFSET)反向搜索 magic word 并动态加载分区表。
四、如何实现读写擦除等操作:以 NOR Flash 为例
以上完成了设备与分区的"登记",但操作函数本身才是驱动移植的重头戏。下面以 fal_norflash_port.c 为例,完整介绍读写擦除的实现思路。该文件开头声明了需要底层驱动提供的四个外部函数:norflash_init、norflash_read、norflash_write_page(页写入)与norflash_erase_sector(扇区擦除),并定义了关键参数:
#define FLASH_SECTOR_SIZE ( 4 * 1024 ) /* 扇区大小 4KB */ #define FLASH_START_ADDR 0U #define FLASH_END_ADDR 0x01000000U /* 16MB */ #define FLASH_PROGRAM_MIN_SIZE 256 /* 最少写入 256 字节 */ #define FLASH_PAGE_SIZE FLASH_PROGRAM_MIN_SIZE4.1 两个对齐宏:FAL_ALIGN_UP 与 FAL_ALIGN_DOWN
#define FAL_ALIGN_UP( size, align ) \ ( ( ( size ) + ( align ) - 1 ) - ( ( ( size ) + ( align ) - 1 ) % ( align ) ) ) #define FAL_ALIGN_DOWN( size, align ) ( ( ( size ) / ( align ) ) * ( align ) )取几个例子验证:
ALIGN_UP(16,4)=16,ALIGN_UP(15,4)=16,ALIGN_UP(17,4)=20ALIGN_DOWN(16,4)=16,ALIGN_DOWN(15,4)=12,ALIGN_DOWN(17,4)=16
不难看出,ALIGN_UP是把 size 向上取整到 align 的倍数,ALIGN_DOWN则是向下取整到 align 的倍数。它们被用于把任意起止地址映射到扇区/页边界,是整个读写实现的地基。
4.2 Flash 硬件特性:为什么需要对齐与"读改写"
移植前必须理解两类 Flash 的共同特性与差异:
- 按块擦除:NOR Flash 的块大小一般为 4K,单片机内部 Flash 的块大小为 1K、2K、16K 不等;
- 最少写入数据限制:
- NOR Flash 按页写入,一次最少写 256 个字节数据,超过则覆盖起始数据——例如第 257 个数据会覆盖第 1 个数据的位置;
- 单片机内部 Flash 一次最少写 2 个字节数据(如 STM32F105RC),且只能将地址 2 字节对齐写入,只写一个字节时给后面的字节补成 FF。
正因如此,"任意长度的写入"必须分解为"扇区对齐 + 页对齐"的多次底层操作,且当写入无法完整覆盖扇区时,需要先读出扇区旧数据、合并后再擦写,即经典的读-改-写流程。
4.3 实现擦除
static int32_t get_sector( uint32_t address ); /* 获取当前地址属于第几个扇区 */ extern void norflash_erase_sector( uint32_t saddr ); /* 擦除单个扇区的全部数据 */ /* FLASH 都是按块擦除,我们假定在调用擦除函数时,用户知道自己将会擦除扇区内的全部数据 */ static int erase( long offset, size_t size ) { int32_t cur_erase_sector; uint32_t addr = FLASH_START_ADDR + offset; uint32_t addr_down = FAL_ALIGN_DOWN( addr, FLASH_SECTOR_SIZE ); uint32_t addr_end = addr + size; uint32_t addr_end_up = FAL_ALIGN_UP( addr_end, FLASH_SECTOR_SIZE ); uint32_t cur_addr = addr_down; while ( cur_addr < addr_end_up ) { cur_erase_sector = get_sector( cur_addr ); if ( cur_erase_sector == -1 ) { /* 获取扇区失败,说明地址超出范围 */ return cur_addr - addr; } norflash_erase_sector( cur_erase_sector ); cur_addr += FLASH_SECTOR_SIZE; /* 若各扇区大小不同,需实现从当前地址获取扇区实际大小的函数 */ } return size; }实现要点:把起始地址向下对齐、结束地址向上对齐到扇区边界,然后逐个扇区调用底层擦除;get_sector返回-1表示地址越界(参考源码中该函数对FLASH_START_ADDR/FLASH_END_ADDR的范围判断),此时返回已擦除的长度。
4.4 实现读取
/* 这个比较简单,直接调用 norflash_read 即可 */ static int read( long offset, uint8_t* buf, size_t size ) { norflash_read( buf, offset + FLASH_START_ADDR, size ); return size; }读取没有对齐与擦除的限制,直接按物理地址搬运数据并返回长度。
4.5 实现写入:最关键的一步
写入是三个操作中最复杂的部分,需要同时处理"扇区对齐"(应对按块擦除)与"页对齐"(应对最小写粒度)两层问题。
/* 写入任意长数据到 NOR Flash 函数 */ static int write( long offset, const uint8_t* buf, size_t size ) { // 计算实际物理地址(相对于 Flash 起始地址的偏移) uint32_t addr = FLASH_START_ADDR + offset; // 计算起始地址的扇区向上对齐地址(例如 0x1007 -> 0x2000 当扇区大小 4K) uint32_t addr_up = FAL_ALIGN_UP( addr, FLASH_SECTOR_SIZE ); // 计算起始地址的扇区向下对齐地址(例如 0x1007 -> 0x1000) uint32_t addr_down = FAL_ALIGN_DOWN( addr, FLASH_SECTOR_SIZE ); // 计算写入结束地址 uint32_t addr_end = addr + size; // 结束地址的扇区向上对齐地址 uint32_t addr_end_up = FAL_ALIGN_UP( addr_end, FLASH_SECTOR_SIZE ); // 结束地址的扇区向下对齐地址 uint32_t addr_end_down = FAL_ALIGN_DOWN( addr_end, FLASH_SECTOR_SIZE ); uint32_t cur_addr = addr_down; // 当前处理的扇区起始地址 uint32_t max_write_len = 0; // 单次最大可写入长度 uint32_t write_len = 0; // 实际写入长度 // 地址有效性检查:结束地址超过 Flash 范围 或 起始地址在 Flash 区域外 if ( addr_end_up > FLASH_END_ADDR || ( int )addr_end_down < FLASH_START_ADDR ) return -1; // 分配扇区大小的缓冲区(用于处理部分写入时需要保存原始数据的情况) uint8_t* read_sector_buf = FAL_MALLOC( FLASH_SECTOR_SIZE ); if ( read_sector_buf == RT_NULL ) { return -2; // 内存分配失败 } // 按扇区逐个处理(从起始扇区到结束扇区) while ( cur_addr < addr_end_up ) { /* 情况1:处理起始地址不在扇区边界的情况(首扇区部分写入) */ if ( cur_addr < addr ) { // 读取整个扇区原始数据到缓冲区 read( cur_addr - FLASH_START_ADDR, read_sector_buf, FLASH_SECTOR_SIZE ); // 计算首扇区可写入的最大长度(从起始地址到扇区末尾) max_write_len = ( addr_up - addr ); // 确定实际写入长度(不超过剩余数据大小) write_len = size >= max_write_len ? max_write_len : size; // 判断是否需要擦除(检查目标区域是否包含需要从 0->1 的位) if ( judge_whether_erase( read_sector_buf + addr - cur_addr, write_len ) ){ // 需要擦除时:执行擦除->修改缓冲区->写入整个扇区 norflash_erase_sector( get_sector( cur_addr ) ); // 将新数据合并到缓冲区对应位置 FAL_MEMCPY( read_sector_buf + ( addr - cur_addr ), buf, write_len ); // 写入整个扇区 write_sector( cur_addr, read_sector_buf, FLASH_SECTOR_SIZE ); } else { // 无需擦除时直接写入数据(NOR Flash 允许直接写入 0 位) write_sector( addr, buf, write_len ); } buf += write_len; // 移动数据指针 } /* 情况2:处理结束地址不在扇区边界的情况(末扇区部分写入) */ else if ( cur_addr == addr_end_down ) { // 读取整个扇区原始数据 read( cur_addr - FLASH_START_ADDR, read_sector_buf, FLASH_SECTOR_SIZE ); // 计算最大可写入长度(整个扇区) max_write_len = FLASH_SECTOR_SIZE; // 计算实际需要写入的长度(从扇区起始到结束地址) write_len = addr_end - cur_addr; write_len = write_len >= max_write_len ? max_write_len : write_len; // 判断是否需要擦除 if ( judge_whether_erase( read_sector_buf, write_len ) ) { // 需要擦除时:合并数据->擦除->写入整个扇区 FAL_MEMCPY( read_sector_buf, buf, write_len ); norflash_erase_sector( get_sector( cur_addr ) ); write_sector( cur_addr, read_sector_buf, FLASH_SECTOR_SIZE ); } else { // 直接写入数据 write_sector( cur_addr, buf, write_len ); } } /* 情况3:完整扇区写入(中间扇区) */ else { // 直接擦除整个扇区(完整覆盖不需要保留数据) norflash_erase_sector( get_sector( cur_addr ) ); // 写入整个扇区数据 write_sector( cur_addr, buf, FLASH_SECTOR_SIZE ); buf += FLASH_SECTOR_SIZE; // 移动数据指针 } cur_addr += FLASH_SECTOR_SIZE; // 移动到下一个扇区 } FAL_FREE( read_sector_buf ); // 释放缓冲区内存 return size; // 返回成功写入的字节数 }关键逻辑说明:
- 地址对齐处理:通过向上/向下对齐计算确定实际需要操作的扇区范围;
- 三种写入场景:
- 首扇区部分写入:需要读取原始数据,合并新数据后判断擦除必要性;
- 中间完整扇区:直接擦除后全量写入,提高效率;
- 末扇区部分写入:处理方式类似首扇区,但数据位置不同;
- 擦除判断:通过
judge_whether_erase函数检测是否需要执行擦除操作(基于 NOR Flash 的特性,只有需要将 0 变为 1 时才必须擦除)。该函数的实现就是扫描缓冲区中是否存在非0xFF的字节(见源码):
static uint32_t judge_whether_erase( uint8_t* sector_buf, uint16_t len ) { uint8_t* p = sector_buf; for ( size_t i = 0; i < len; i++ ) { if ( p[ i ] != 0xFF ) return 1; } return 0; }- 数据合并:使用临时缓冲区保存原始数据,仅修改需要写入的部分,最大限度减少擦除操作(擦除会缩短 Flash 寿命,能省则省);
- 内存管理:动态分配扇区大小的缓冲区,处理完成后立即释放;源码注释提示,若不使用内存分配,也可定义一个
static的FLASH_SECTOR_SIZE长度缓冲区。
4.6 补齐最后一块拼图:扇区写入函数 write_sector
到这里工作似乎做完了,但我们还没有"写入扇区"的函数,只有页写入函数norflash_write_page。扇区写入逻辑和任意写入逻辑基本相同,区别仅在于对齐粒度从扇区换成了页:
/* 扇区写入函数:处理按页对齐的 NOR Flash 写入操作 */ static int write_sector( long offset, const uint8_t* buf, size_t size ) { // 计算实际物理地址(FLASH 起始地址 + 偏移量) uint32_t addr = FLASH_START_ADDR + offset; // 计算地址的页对齐上边界和下边界(按 FLASH_PAGE_SIZE 对齐) uint32_t addr_up = FAL_ALIGN_UP( addr, FLASH_PAGE_SIZE ); uint32_t addr_down = FAL_ALIGN_DOWN( addr, FLASH_PAGE_SIZE ); // 计算写入结束地址及其页对齐边界 uint32_t addr_end = addr + size; uint32_t addr_end_up = FAL_ALIGN_UP( addr_end, FLASH_PAGE_SIZE ); uint32_t addr_end_down = FAL_ALIGN_DOWN( addr_end, FLASH_PAGE_SIZE ); // 初始化当前处理地址和长度变量 uint32_t cur_addr = addr_down; // 从页对齐起始地址开始处理 uint32_t max_write_len = 0; // 单次最大可写入长度 uint32_t write_len = 0; // 实际写入长度 // 循环处理所有需要写入的页 while ( cur_addr < addr_end_up ) { // 处理起始未对齐部分(跨页起始边界) if ( cur_addr < addr ) { // 计算当前页剩余可写空间(页结束地址 - 实际起始地址) max_write_len = ( addr_up - addr ); // 取实际剩余长度和总长度的最小值 write_len = size >= max_write_len ? max_write_len : size; // 执行页写入:参数依次是数据指针、物理地址、写入长度 norflash_write_page( buf, addr, write_len ); buf += write_len; // 移动数据指针 } // 处理结束未对齐部分(跨页结束边界) else if ( cur_addr == addr_end_down ) { // 单页最大写入长度 max_write_len = FLASH_PAGE_SIZE; // 计算实际需要写入的长度(结束地址 - 当前页起始地址) write_len = addr_end - cur_addr; // 确保不超过页最大长度 write_len = write_len >= max_write_len ? max_write_len : write_len; // 执行页写入 norflash_write_page( buf, cur_addr, write_len ); } // 处理完整页写入 else { // 整页写入(FLASH_PAGE_SIZE 长度) norflash_write_page( buf, cur_addr, FLASH_PAGE_SIZE ); buf += FLASH_PAGE_SIZE; // 移动数据指针整页长度 } // 移动到下一页起始地址 cur_addr += FLASH_PAGE_SIZE; } return size; // 返回成功写入的总字节数 }至此,write的完整调用链就闭环了:write()按扇区做对齐与读-改-写合并,内部调用write_sector();write_sector()再按页(256 字节)拆分,最终落到norflash_write_page()。两层对齐分别对应"按块擦除"与"最少写入字节数"两个硬件约束,这种思路对大部分 Flash 驱动来说是通用的。
最后,将四个操作函数组装成设备对象:
const struct fal_flash_dev norflash0 = { .name = "norflash0", .addr = FLASH_START_ADDR, .len = FLASH_END_ADDR - FLASH_START_ADDR, .blk_size = FLASH_SECTOR_SIZE, .ops = { init, read, write, erase }, .write_gran = 1, };注意这里write_gran = 1(位),与 NOR Flash 支持按位写 0、按页编程的特性一致。
五、移植完成后的验证与调试
移植完成后,需要确认 FAL 能正确初始化并找到所有设备与分区:
- 编译期检查:
fal_cfg.h必须被 fal.h 正确引入,且定义好FAL_FLASH_DEV_TABLE;若开启静态分区表还需定义FAL_PART_TABLE(见 fal_flash.c 与 fal_partition.c 中的#error检查)。 - 初始化入口:在系统初始化流程中调用
fal_init()(返回值为分区总数),内部依次完成设备初始化与分区校验,成功后会打印RT-Thread Flash Abstraction Layer initialize success.(fal.c)。 - 查看分区表:开启
FAL_USING_DEBUG(Kconfig 中默认随RT_USING_DEBUG打开)后,fal_partition_init()会调用fal_show_part_table()打印完整的"名称 / Flash 设备 / 偏移 / 长度"表格,便于核对移植结果(fal_partition.c)。 - 相关 Kconfig 配置项(components/fal/Kconfig):
FAL_USING_SFUD_PORT启用 SFUD 移植文件,FAL_USING_NOR_FLASH_DEV_NAME指定其设备名,FAL_DEV_NAME_MAX(默认 24)与FAL_DEV_BLK_MAX(默认 6)分别控制名称长度上限与设备块数上限,设备名较长或设备较多时应相应调大。 - 上层 API 验证:初始化成功后即可通过
fal_partition_find("app")获取分区,再调用fal_partition_read/write/erase或fal_partition_erase_all读写数据(接口声明见 fal.h);这些接口在分区层也做了地址越界检查(addr + size > part->len时拒绝操作并报错),可作为移植正确性的第一道验证。
结语
FAL 的移植本质上只做三件事:写对四个操作函数、登记设备表、划分分区表。其中操作函数的质量取决于对 Flash 硬件特性的理解——扇区擦除、页最小写入、部分写入时的读-改-写合并与擦除必要性判断。本文以components/fal/samples/porting/下的 STM32F2 片内 Flash、SFUD 片外 SPI Flash 与手写 NOR Flash 驱动三个示例为主线,从结构定义、宏展开到初始化校验逐层拆解,这套"设备 → 设备表 → 分区表"的移植流程同样适用于其它厂商芯片,希望对你手头的 BSP 移植工作有所帮助。
- 操作系统
- 嵌入式
- 物联网
- 嵌入式OS
- RTOS
【免费下载链接】rt-thread
RT-Thread is an open source IoT Real-Time Operating System (RTOS). https://rt-thread.github.io/rt-thread/
相关推荐
RT-Thread FAL 完整 API 指南:Flash 抽象层的设备查找、分区读写与设备适配
RT Thread FAL 完整 API 指南:Flash 抽象层的设备查找、分区读写与设备适配 本篇技术指南围绕 RT Thread 开源物联网实时操作系统中
操作系统嵌入式物联网嵌入式OSRTOSRT-Thread 设备树 fdt 组件实战:fdt_dump / fdt_test 示例程序与 DTB 解析输出详解
RT Thread 设备树 fdt 组件实战:fdt_dump / fdt_test 示例程序与 DTB 解析输出详解 RT Thread 的 fdt 组件基于
操作系统嵌入式物联网嵌入式OSRTOSRT-Thread 内核移植指南:libcpu 架构移植与 BSP 板级移植实战(以 Cortex-M 为例)
RT Thread 内核移植指南:libcpu 架构移植与 BSP 板级移植实战(以 Cortex M 为例) RT Thread 作为一款面向物联网的开源实时
操作系统嵌入式物联网嵌入式OSRTOS
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考