深入理解 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.cffunicode.c),不再编译 portable/ 下的任何文件。此时,示例工程必须自行提供 diskio.cdiskio.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 整体思路

  1. 在示例 CMakeLists 中设置 CONFIG_FATFS_CUSTOM_PORTABLE=1,屏蔽 SDK 内置 portable
  2. 在示例中创建自己的 port/ 目录,实现完整的 disk I/O 层
  3. 利用 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_SDDEV_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 访问所需的两个关键问题:

  1. cache 行对齐:如果用户 buffer 不满足 cache 行对齐,自动通过中间缓冲区逐扇区搬运
  2. 系统地址转换:通过 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

适合使用的场景

  1. 对接 SDK 未内置的存储介质(如 SPI Nor Flash、NAND Flash 等)
  2. 不想修改 SDK middleware 目录,确保 SDK 版本升级不会产生合并冲突
  3. 快速验证/原型开发,先用自定义 portable 跑通逻辑,后续再考虑是否合入 SDK
  4. 项目有特殊的 disk I/O 需求(如自定义的 wear leveling 层、加密层等)

不需要使用的场景

  1. 只使用 SD 卡、USB U 盘等 SDK 已支持的介质——直接用内置 portable 即可
  2. 多个示例工程需要共享同一种存储介质适配——应该考虑将适配代码合入 SDK portable,避免重复维护

从 CUSTOM_PORTABLE 演进到内置 portable

当自定义介质适配经过充分验证后,可以考虑将其合入 SDK 的 portable/ 目录,使其成为内置支持的介质。此时需要:

  1. portable/ 下创建对应的子目录(如 spi_nor/),文件命名遵循 hpm_*_disk.c/h 规范
  2. diskio.h 中添加 DEV_SPI_NOR 等新的驱动号定义
  3. diskio.c 的各 switch 分支中添加对应的条件编译守护
  4. portable/CMakeLists.txt 中添加 sdk_inc_ifdef / sdk_src_ifdef
  5. middleware/fatfs/CMakeLists.txt 中添加 CONFIG 宏到编译定义的映射
  6. 移除示例中的 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.cdiskio.h 及具体介质驱动
  • 适用场景:对接 SDK 未内置的存储介质,或不想修改 middleware 目录

这种设计既保证了 SDK 内置方案的完整性(SD 卡、USB 等开箱即用),又为开发者提供了灵活的扩展路径,是"约定优于配置"与"开放优于封闭"的良好平衡。

0
0

订阅

发表回复 0

Your email address will not be published. Required fields are marked *

captcha
Enter the characters shown in the image:
Reload

This CAPTCHA helps ensure that you are human. Please enter the requested characters.