深入理解 HPM SDK 中的FATFS的 CONFIG_FATFS_CUSTOM_PORTABLE
一、FATFS 与存储介质的对接
FATFS 是面向嵌入式系统的轻量级 FAT 文件系统模块,采用 ANSI C 编写,与硬件 I/O 层完全分离。这种设计意味着 FATFS 本身不关心底层存储介质是 SD 卡、USB 设备还是 SPI Nor Flash——它只关心一套标准的 disk I/O 接口。
在 HPM SDK 中,FATFS 被组织为 middleware 组件,路径为 middleware/fatfs/,其核心结构如下:
middleware/fatfs/
├── CMakeLists.txt
└── src/
├── common/ # FATFS 核心源码(ff.c、ffunicode.c 等)
└── portable/ # 存储介质适配层(disk I/O 实现)
├── CMakeLists.txt
├── diskio.c # 统一的 disk I/O 分发层
├── diskio.h # 驱动号定义、接口声明
├── sdxc/ # SDXC 接口 SD 卡驱动
├── spi_sd/ # SPI 接口 SD 卡驱动
└── usb/ # USB 大容量存储驱动
portable/ 目录就是 FATFS 与具体存储介质之间的"桥梁"。 HPM SDK 已经内置了 SD 卡、eMMC、USB U 盘等常见介质的适配代码,开发者只需在 CMakeLists 中启用对应的 CONFIG 宏即可开箱即用。
二、CONFIG_FATFS_CUSTOM_PORTABLE 是什么
当你需要对接一种 HPM SDK 尚未内置支持的存储介质(比如 SPI Nor Flash),就面临一个选择:
- 方案 A:修改
middleware/fatfs/src/portable/中的代码,添加新介质的适配 - 方案 B:不碰 middleware,在自己的示例工程中实现完整的 disk I/O 层
CONFIG_FATFS_CUSTOM_PORTABLE 就是方案 B 的开关。
2.1 工作原理
在 middleware/fatfs/CMakeLists.txt 中有这样一行关键逻辑:
add_subdirectory_ifndef(CONFIG_FATFS_CUSTOM_PORTABLE src/portable)
add_subdirectory_ifndef 是 HPM SDK 的 CMake 扩展宏,语义是:如果 CONFIG_FATFS_CUSTOM_PORTABLE 未定义,才将 src/portable 加入构建。
也就是说:
| CONFIG_FATFS_CUSTOM_PORTABLE | portable 目录 | 效果 |
|---|---|---|
| 未设置 | 加入构建 | 使用 SDK 内置的 disk I/O 实现(SD 卡、USB 等) |
| 设置为 1 | 不加入构建 | 完全由示例工程自己提供 disk I/O 实现 |
当 CONFIG_FATFS_CUSTOM_PORTABLE=1 时,middleware 只编译 FATFS 核心(ff.c、ffunicode.c),不再编译 portable/ 下的任何文件。此时,示例工程必须自行提供 diskio.c、diskio.h 以及具体存储介质的驱动文件。
2.2 两种方案对比
| 方案 A:修改 portable | 方案 B:CONFIG_FATFS_CUSTOM_PORTABLE | |
|---|---|---|
| 是否侵入 SDK | 是 | 否 |
| SDK 升级兼容性 | 可能产生冲突 | 完全无冲突 |
| 代码复用性 | 高(所有工程可共享) | 低(每个示例各自维护) |
| 适用场景 | 通用存储介质,值得合入 SDK | 临时/实验性介质,或不想动 SDK |
| 实现复杂度 | 需要适配 portable 框架的 cache 对齐逻辑 | 自由实现,可简化 |
三、实战:用 CONFIG_FATFS_CUSTOM_PORTABLE 对接 SPI Nor Flash
下面以 SPI Nor Flash 为例,完整演示如何使用 CONFIG_FATFS_CUSTOM_PORTABLE 将自定义存储介质对接到 FATFS。
3.1 整体思路
- 在示例 CMakeLists 中设置
CONFIG_FATFS_CUSTOM_PORTABLE=1,屏蔽 SDK 内置 portable - 在示例中创建自己的
port/目录,实现完整的 disk I/O 层 - 利用
serial_nor组件完成 SPI Nor Flash 的底层读写
3.2 工程结构
本文涉及的完整示例代码可在以下仓库获取:
https://github.com/hpmicro/hpm_sdk_extra/tree/main/demos/spi_nor_flash/nor_flash_fatfs
samples/spi_nor_flash/nor_flash_fatfs/
├── CMakeLists.txt
├── fatfs/
│ ├── CMakeLists.txt # fatfs 子目录构建
│ └── port/ # 自定义 disk I/O 实现
│ ├── diskio.c
│ ├── diskio.h
│ ├── spi_nor_disk.c
│ └── spi_nor_disk.h
├── inc/
└── src/
3.3 第一步:启用 CONFIG_FATFS_CUSTOM_PORTABLE
在示例的主 CMakeLists.txt 中:
cmake_minimum_required(VERSION 3.13)
set(CONFIG_SPI_NOR_FLASH 1)
set(CONFIG_FATFS_CUSTOM_PORTABLE 1) # 关键:屏蔽 SDK 内置 portable
set(CONFIG_FATFS 1)
set(CONFIG_DMA_MGR 1)
set(USE_FREERTOS 1)
if (USE_FREERTOS)
set(CONFIG_FREERTOS 1)
set(CONFIG_FREERTOS_TIMER_RESOURCE_GPTMR 1)
endif()
# ... 其余配置
设置 CONFIG_FATFS_CUSTOM_PORTABLE=1 后,middleware/fatfs/CMakeLists.txt 中的 add_subdirectory_ifndef 条件不满足,src/portable 不会被编译。
3.4 第二步:实现 diskio.h
在自定义的 port/diskio.h 中,需要定义存储介质编号和 disk I/O 接口声明。SDK 内置的 diskio.h 已经定义了 DEV_SD、DEV_USB 等编号,我们只需补充 SPI Nor Flash 的编号:
#ifndef _DISKIO_DEFINED
#define _DISKIO_DEFINED
#include "ff.h"
#include <stdbool.h>
/* 状态与返回值定义(与 FATFS 标准一致) */
typedef BYTE DSTATUS;
typedef enum {
RES_OK = 0,
RES_ERROR,
RES_WRPRT,
RES_NOTRDY,
RES_PARERR
} DRESULT;
/* 存储介质编号 —— 在 SDK 已有的基础上添加 DEV_SPI_NOR */
#define DEV_SD (3U)
#define DEV_RAM (2U)
#define DEV_MMC (1U)
#define DEV_USB (0U)
#define DEV_USB_MSC_0 (5U)
#define DEV_USB_MSC_1 (6U)
#define DEV_SPI_NOR (7U) /* 新增:SPI Nor Flash */
/* Disk 状态位 */
#define STA_NOINIT (0x01)
#define STA_NODISK (0x02)
#define STA_PROTECT (0x04)
/* ioctl 命令码 */
#define CTRL_SYNC (0U)
#define GET_SECTOR_COUNT (1U)
#define GET_SECTOR_SIZE (2U)
#define GET_BLOCK_SIZE (3U)
#define CTRL_TRIM (4U)
/* ... 其余标准命令码 */
#ifdef __cplusplus
extern "C" {
#endif
/* disk I/O 接口声明 */
void disk_deinitialize(BYTE pdrv);
DSTATUS disk_initialize(BYTE pdrv);
DSTATUS disk_status(BYTE pdrv);
DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count);
DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count);
DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff);
static inline bool disk_is_ready(BYTE pdrv)
{
return ((disk_status(pdrv) & (STA_NOINIT | STA_NODISK)) == 0);
}
#ifdef __cplusplus
}
#endif
#endif
3.5 第三步:实现 SPI Nor Flash 的 disk 驱动
spi_nor_disk.c 是 SPI Nor Flash 存储介质的核心驱动,负责将 FATFS 的扇区读写请求转化为 SPI Nor Flash 的地址读写操作。
#include "ffconf.h"
#include "diskio.h"
#include "hpm_l1c_drv.h"
#include "hpm_serial_nor.h"
#include "hpm_serial_nor_host_port.h"
#include "board.h"
#if defined(USE_FREERTOS) && USE_FREERTOS
#include "FreeRTOS.h"
#include "task.h"
#endif
#define MAX_ALIGNED_BUF_SIZE (4096U)
/* Cache 行对齐的缓冲区,用于非对齐地址的读写 */
ATTR_ALIGN(HPM_L1C_CACHELINE_SIZE) uint8_t g_aligned_buf[MAX_ALIGNED_BUF_SIZE];
static hpm_serial_nor_t nor_flash_dev = {0};
static hpm_serial_nor_info_t flash_info;
static hpm_stat_t nor_init_sta;
DSTATUS spi_nor_disk_initialize(BYTE pdrv)
{
if (pdrv != DEV_SPI_NOR) {
return STA_NOINIT;
}
serial_nor_get_board_host(&nor_flash_dev.host);
board_init_spi_clock(nor_flash_dev.host.host_param.param.host_base);
serial_nor_spi_pins_init(nor_flash_dev.host.host_param.param.host_base);
nor_init_sta = hpm_serial_nor_init(&nor_flash_dev, &flash_info);
return (nor_init_sta == status_success) ? RES_OK : RES_ERROR;
}
DSTATUS spi_nor_disk_deinitialize(BYTE pdrv)
{
if (pdrv != DEV_SPI_NOR) {
return STA_NOINIT;
}
return RES_OK;
}
DSTATUS spi_nor_disk_status(BYTE pdrv)
{
if (pdrv != DEV_SPI_NOR) {
return STA_NOINIT;
}
return (nor_init_sta == status_success) ? RES_OK : RES_NOTRDY;
}
读操作需要注意 SPI Nor Flash 是按字节寻址的,需要将 FATFS 的扇区号转换为字节地址。同时需要处理 buffer 非 cache 行对齐的情况:
DSTATUS spi_nor_disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count)
{
if (pdrv != DEV_SPI_NOR) {
return RES_PARERR;
}
hpm_stat_t stat;
uint32_t sector_size = FF_MAX_SS;
uint32_t remaining_size = sector_size * count;
uint32_t start_sector_addr = sector_size * sector;
if (((uint32_t)buff % HPM_L1C_CACHELINE_SIZE) != 0) {
/* buffer 非 cache 对齐,通过中间缓冲区读取 */
uint32_t sys_aligned_buf_addr =
core_local_mem_to_sys_address(BOARD_RUNNING_CORE, (uint32_t)&g_aligned_buf);
while (remaining_size > 0) {
uint32_t read_size = MIN(MAX_ALIGNED_BUF_SIZE, remaining_size);
stat = hpm_serial_nor_read(&nor_flash_dev,
(uint8_t *)sys_aligned_buf_addr, read_size, start_sector_addr);
if (stat != status_success) {
return RES_ERROR;
}
l1c_dc_invalidate(sys_aligned_buf_addr, read_size);
memcpy(buff, g_aligned_buf, read_size);
buff += read_size;
start_sector_addr += read_size;
remaining_size -= read_size;
}
} else {
/* buffer 已 cache 对齐,直接读取 */
stat = hpm_serial_nor_read(&nor_flash_dev, (uint8_t *)buff,
remaining_size, start_sector_addr);
if (stat != status_success) {
return RES_ERROR;
}
}
return RES_OK;
}
写操作需要先擦除再编程,同样需要处理 cache 对齐:
DSTATUS spi_nor_disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count)
{
if (pdrv != DEV_SPI_NOR) {
return RES_PARERR;
}
hpm_stat_t stat;
uint32_t sector_size = FF_MAX_SS;
uint32_t remaining_size = sector_size * count;
uint32_t start_sector_addr = sector_size * sector;
uint16_t page_size = nor_flash_dev.flash_info.page_size;
/* 先擦除 */
stat = hpm_serial_nor_erase_blocking(&nor_flash_dev, start_sector_addr, remaining_size);
if (stat != status_success) {
return RES_ERROR;
}
/* 再编程 */
if (((uint32_t)buff % HPM_L1C_CACHELINE_SIZE) != 0) {
/* 非 cache 对齐:按 page 拷贝到对齐缓冲区后逐页写入 */
uint32_t sys_aligned_buf_addr =
core_local_mem_to_sys_address(BOARD_RUNNING_CORE, (uint32_t)&g_aligned_buf);
while (remaining_size > 0) {
uint32_t write_size = MIN(page_size, remaining_size);
memcpy((uint8_t *)&g_aligned_buf, buff, write_size);
l1c_dc_flush(sys_aligned_buf_addr, write_size);
stat = hpm_serial_nor_page_program_nonblocking(&nor_flash_dev,
(uint8_t *)sys_aligned_buf_addr, write_size, start_sector_addr);
if (stat != status_success) {
return RES_ERROR;
}
while (hpm_serial_nor_is_busy(&nor_flash_dev) == status_spi_nor_flash_is_busy) {
#if defined(USE_FREERTOS) && USE_FREERTOS
vTaskDelay(pdMS_TO_TICKS(1));
#endif
}
buff += write_size;
start_sector_addr += write_size;
remaining_size -= write_size;
}
} else {
/* cache 对齐:直接按块写入 */
stat = hpm_serial_nor_program_blocking(&nor_flash_dev,
(uint8_t *)buff, remaining_size, start_sector_addr);
if (stat != status_success) {
return RES_ERROR;
}
}
return RES_OK;
}
ioctl 操作用于返回存储介质的几何信息:
DRESULT spi_nor_disk_ioctl(BYTE pdrv, BYTE cmd, void *buff)
{
DRESULT result = RES_PARERR;
do {
HPM_BREAK_IF((pdrv != DEV_SPI_NOR) || ((cmd != CTRL_SYNC) && (buff == NULL)));
result = RES_OK;
switch (cmd) {
case GET_SECTOR_COUNT:
*(uint32_t *)buff = (flash_info.size_in_kbytes / flash_info.sector_size_kbytes) * 1024;
break;
case GET_SECTOR_SIZE:
*(uint32_t *)buff = FF_MAX_SS;
break;
case GET_BLOCK_SIZE:
*(uint32_t *)buff = (flash_info.sector_size_kbytes * 1024);
break;
case CTRL_SYNC:
result = RES_OK;
break;
default:
result = RES_PARERR;
break;
}
} while (false);
return result;
}
3.6 第四步:实现 diskio.c 分发层
diskio.c 是 FATFS 调用的统一入口,它根据 pdrv(物理驱动器号)将请求分发到对应的存储介质驱动。当只有 SPI Nor Flash 一种介质时,逻辑非常简单:
#include "diskio.h"
#include "spi_nor_disk.h"
DSTATUS disk_status(BYTE pdrv)
{
DSTATUS stat = STA_NOINIT;
switch (pdrv) {
case DEV_SPI_NOR:
stat = spi_nor_disk_status(pdrv);
break;
default:
break;
}
return stat;
}
void disk_deinitialize(BYTE pdrv)
{
switch (pdrv) {
case DEV_SPI_NOR:
spi_nor_disk_deinitialize(pdrv);
break;
default:
break;
}
}
DSTATUS disk_initialize(BYTE pdrv)
{
DSTATUS stat = STA_NOINIT;
switch (pdrv) {
case DEV_SPI_NOR:
stat = spi_nor_disk_initialize(pdrv);
break;
default:
break;
}
return stat;
}
DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count)
{
DRESULT res = RES_ERROR;
switch (pdrv) {
case DEV_SPI_NOR:
res = spi_nor_disk_read(pdrv, buff, sector, count);
break;
default:
res = RES_PARERR;
break;
}
return res;
}
#if FF_FS_READONLY == 0
DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count)
{
DRESULT res = RES_ERROR;
switch (pdrv) {
case DEV_SPI_NOR:
res = spi_nor_disk_write(pdrv, buff, sector, count);
break;
default:
res = RES_PARERR;
break;
}
return res;
}
#endif
DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff)
{
DRESULT res = RES_ERROR;
switch (pdrv) {
case DEV_SPI_NOR:
res = spi_nor_disk_ioctl(pdrv, cmd, buff);
break;
default:
res = RES_ERROR;
break;
}
return res;
}
3.7 第五步:配置 fatfs 子目录的构建
在 fatfs/CMakeLists.txt 中,将自定义的 port 目录加入构建:
# Copyright (c) 2026 HPMicro
# SPDX-License-Identifier: BSD-3-Clause
sdk_inc(port)
sdk_inc(spi_flash/common/port)
sdk_inc(spi_flash/common/port/${BOARD})
sdk_app_src(port/diskio.c)
sdk_app_src(port/spi_nor_disk.c)
四、SDK 内置 portable 的运行机制
理解了 CONFIG_FATFS_CUSTOM_PORTABLE 的用法后,再来看 SDK 内置 portable 的工作方式,能更好地理解两种方案的关系。
4.1 内置 portable 的分层架构
SDK 内置的 portable/diskio.c 采用了两层分发的设计:
FATFS 上层(ff.c)
│
▼
disk_read() / disk_write() ← 第 1 层:处理 cache 对齐、sys 地址转换
│
▼
disk_read_private() / disk_write_private() ← 第 2 层:按 pdrv 分发到具体介质
│
├── sd_disk_read()
├── spi_sd_disk_read()
├── usb_disk_read()
└── emmc_disk_read()
第 1 层统一处理了 HPM MCU 架构下 DMA 访问所需的两个关键问题:
- cache 行对齐:如果用户 buffer 不满足 cache 行对齐,自动通过中间缓冲区逐扇区搬运
- 系统地址转换:通过
core_local_mem_to_sys_address()将 CPU 本地地址转为 DMA 可访问的系统地址
第 2 层才是纯粹的存储介质分发。
4.2 条件编译守护
内置 portable 通过条件编译宏精确控制每种介质的启用:
| CMake CONFIG 宏 | 编译定义 | 控制的介质 |
|---|---|---|
CONFIG_SDMMC |
SD_FATFS_ENABLE=1 |
SDXC 接口 SD 卡 |
CONFIG_SDMMC + CONFIG_HPM_SPI_SDCARD |
SD_FATFS_ENABLE=1 |
SPI 接口 SD 卡 |
CONFIG_USB_FATFS |
USB_FATFS_ENABLE |
USB 大容量存储 |
| 无 | MMC_FATFS_ENABLE |
eMMC |
对应到 middleware/fatfs/CMakeLists.txt 中的转换逻辑:
if(DEFINED CONFIG_SDMMC)
sdk_compile_definitions(-DSD_FATFS_ENABLE=1)
endif()
而在 portable/diskio.c 中,每种介质的代码分支都由对应的宏守护:
#if defined(SD_FATFS_ENABLE) && SD_FATFS_ENABLE
case DEV_SD:
stat = sd_disk_initialize(pdrv);
break;
#endif
这意味着:即使 portable 目录中包含了所有介质的代码,未启用的介质在编译时会被完全裁剪,不会占用任何 Flash 空间。
五、何时选择 CONFIG_FATFS_CUSTOM_PORTABLE
适合使用的场景
- 对接 SDK 未内置的存储介质(如 SPI Nor Flash、NAND Flash 等)
- 不想修改 SDK middleware 目录,确保 SDK 版本升级不会产生合并冲突
- 快速验证/原型开发,先用自定义 portable 跑通逻辑,后续再考虑是否合入 SDK
- 项目有特殊的 disk I/O 需求(如自定义的 wear leveling 层、加密层等)
不需要使用的场景
- 只使用 SD 卡、USB U 盘等 SDK 已支持的介质——直接用内置 portable 即可
- 多个示例工程需要共享同一种存储介质适配——应该考虑将适配代码合入 SDK portable,避免重复维护
从 CUSTOM_PORTABLE 演进到内置 portable
当自定义介质适配经过充分验证后,可以考虑将其合入 SDK 的 portable/ 目录,使其成为内置支持的介质。此时需要:
- 在
portable/下创建对应的子目录(如spi_nor/),文件命名遵循hpm_*_disk.c/h规范 - 在
diskio.h中添加DEV_SPI_NOR等新的驱动号定义 - 在
diskio.c的各 switch 分支中添加对应的条件编译守护 - 在
portable/CMakeLists.txt中添加sdk_inc_ifdef/sdk_src_ifdef - 在
middleware/fatfs/CMakeLists.txt中添加 CONFIG 宏到编译定义的映射 - 移除示例中的
CONFIG_FATFS_CUSTOM_PORTABLE,改用新的 CONFIG 宏
合入后,disk I/O 层的 cache 对齐、sys 地址转换等通用逻辑由 diskio.c 第 1 层统一处理,具体介质驱动只需关注纯粹的读写逻辑,代码更简洁。
六、总结
CONFIG_FATFS_CUSTOM_PORTABLE 是 HPM SDK 在 FATFS 框架下提供的一种非侵入式自定义机制:
- 核心作用:设置后屏蔽 SDK 内置的
portable/目录,由示例工程完全接管 disk I/O 实现 - 实现原理:
add_subdirectory_ifndef(CONFIG_FATFS_CUSTOM_PORTABLE src/portable) - 使用方式:在示例 CMakeLists 中
set(CONFIG_FATFS_CUSTOM_PORTABLE 1),然后自行实现diskio.c、diskio.h及具体介质驱动 - 适用场景:对接 SDK 未内置的存储介质,或不想修改 middleware 目录
这种设计既保证了 SDK 内置方案的完整性(SD 卡、USB 等开箱即用),又为开发者提供了灵活的扩展路径,是"约定优于配置"与"开放优于封闭"的良好平衡。