文件系统移植与集成:从理论到实践的完整指南¶
学习目标¶
完成本教程后,你将能够:
- 理解文件系统移植的基本原理和架构
- 掌握FATFS的移植流程和配置方法
- 熟练实现底层磁盘I/O接口
- 掌握SD卡SPI模式和SDIO模式的驱动实现
- 能够进行文件系统的测试和验证
- 掌握性能优化和故障排除技巧
- 完成一个完整的SD卡文件系统项目
- 理解多文件系统共存的实现方法
前置要求¶
在开始学习之前,建议你具备:
知识要求: - 熟悉C语言编程和指针操作 - 了解FAT文件系统的基本原理 - 理解Flash和SD卡的存储特性 - 掌握SPI或SDIO通信协议 - 了解RTOS的基本使用(可选)
技能要求: - 能够编写嵌入式C代码 - 会使用HAL库或标准外设库 - 熟悉文件操作的基本概念 - 能够使用调试工具和逻辑分析仪 - 了解内存管理和缓冲区操作
开发环境: - STM32F4或F7开发板 - SD卡模块或开发板自带SD卡槽 - Keil MDK或STM32CubeIDE - SD卡(建议8GB以下,FAT32格式) - 串口调试工具 - 逻辑分析仪(可选)
文件系统移植概述¶
什么是文件系统移植¶
文件系统移植是将通用文件系统(如FATFS、LittleFS)适配到特定硬件平台的过程。移植的核心是实现文件系统与底层硬件之间的接口层,使文件系统能够正确访问存储介质。
移植的本质: - 文件系统提供统一的文件操作API - 底层硬件提供存储介质访问能力 - 移植层连接两者,实现数据的读写
文件系统架构¶
完整的文件系统架构:
┌─────────────────────────────────────────────────┐
│ 应用层 (Application Layer) │
│ fopen, fread, fwrite, fclose... │
└─────────────────────────────────────────────────┘
↕
┌─────────────────────────────────────────────────┐
│ 文件系统层 (File System Layer) │
│ FATFS / LittleFS / SPIFFS │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 文件管理 │ │ 目录管理 │ │
│ └──────────────┘ └──────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ FAT表管理 │ │ 缓存管理 │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────┘
↕
┌─────────────────────────────────────────────────┐
│ 磁盘I/O接口层 (Disk I/O Interface) │
│ disk_initialize, disk_read, disk_write... │
│ ← 这是移植的核心部分 │
└─────────────────────────────────────────────────┘
↕
┌─────────────────────────────────────────────────┐
│ 硬件驱动层 (Hardware Driver Layer) │
│ SPI/SDIO驱动、DMA控制、中断处理 │
└─────────────────────────────────────────────────┘
↕
┌─────────────────────────────────────────────────┐
│ 硬件层 (Hardware Layer) │
│ SD卡、SPI Flash、eMMC │
└─────────────────────────────────────────────────┘
移植流程概览¶
标准移植流程:
- 准备阶段
- 获取文件系统源码
- 准备硬件驱动
-
配置开发环境
-
接口实现
- 实现磁盘初始化接口
- 实现磁盘读写接口
-
实现磁盘控制接口
-
配置优化
- 配置文件系统参数
- 优化缓存和性能
-
启用可选功能
-
测试验证
- 基本功能测试
- 性能测试
-
稳定性测试
-
集成应用
- 封装应用接口
- 错误处理
- 文档编写
准备工作¶
硬件准备¶
| 名称 | 数量 | 说明 | 参考型号 |
|---|---|---|---|
| 开发板 | 1 | STM32F4或F7系列 | STM32F407VG / STM32F746NG |
| SD卡模块 | 1 | SPI接口或SDIO接口 | - |
| SD卡 | 1 | 建议8GB以下 | SanDisk Class 10 |
| 杜邦线 | 若干 | 连接SD卡模块 | - |
| USB线 | 1 | 供电和调试 | - |
软件准备¶
必需软件: - STM32CubeIDE 或 Keil MDK - FATFS源码:http://elm-chan.org/fsw/ff/00index_e.html - STM32 HAL库 - 串口调试助手
可选工具: - 逻辑分析仪(调试SPI/SDIO通信) - SD卡格式化工具 - 文件对比工具
电路连接¶
SPI模式连接¶
SD卡SPI接口连接示意图:
STM32F407 SD卡模块
PB13 (SCK) ────────► CLK
PB14 (MISO) ◄──────── MISO (DO)
PB15 (MOSI) ────────► MOSI (DI)
PB12 (CS) ────────► CS
3.3V ────────► VCC
GND ────────► GND
连接说明: - 使用SPI2接口(可根据实际情况调整) - CS片选可以使用任意GPIO - 确保SD卡供电为3.3V - 建议在MISO线上加10KΩ上拉电阻
SDIO模式连接¶
SD卡SDIO接口连接示意图:
STM32F407 SD卡
PC12 (CLK) ────────► CLK
PC8 (D0) ◄──────► DAT0
PC9 (D1) ◄──────► DAT1
PC10 (D2) ◄──────► DAT2
PC11 (D3) ◄──────► DAT3
PD2 (CMD) ◄──────► CMD
3.3V ────────► VCC
GND ────────► GND
连接说明: - SDIO接口支持4位数据传输 - 速度比SPI模式快得多 - 需要使用专用的SDIO引脚 - 所有数据线需要上拉电阻(10KΩ-47KΩ)
步骤1:获取和配置FATFS源码¶
1.1 下载FATFS¶
访问FATFS官网下载最新版本: - 官网:http://elm-chan.org/fsw/ff/00index_e.html - 推荐版本:R0.15(稳定版)
需要的文件:
ff15/source/
├── ff.c # FATFS核心实现
├── ff.h # FATFS头文件
├── ffconf.h # FATFS配置文件
├── diskio.c # 磁盘I/O接口模板
├── diskio.h # 磁盘I/O接口头文件
└── ffunicode.c # Unicode支持(可选)
1.2 添加到工程¶
STM32CubeIDE步骤:
-
在工程中创建文件夹结构
-
添加包含路径
- 右键项目 → Properties → C/C++ Build → Settings
- MCU GCC Compiler → Include paths
-
添加
Middlewares/Third_Party/FatFs/src -
编译测试
1.3 配置ffconf.h¶
打开 ffconf.h 进行配置:
/*---------------------------------------------------------------------------/
/ FatFs功能配置
/---------------------------------------------------------------------------*/
#define FF_FS_READONLY 0 /* 0:读写模式 1:只读模式 */
#define FF_FS_MINIMIZE 0 /* 0:完整功能 1-3:精简功能 */
#define FF_USE_STRFUNC 2 /* 0:禁用 1:启用 2:启用+LFN */
#define FF_USE_FIND 1 /* 启用f_findfirst/f_findnext */
#define FF_USE_MKFS 1 /* 启用f_mkfs格式化功能 */
#define FF_USE_FASTSEEK 1 /* 启用快速定位 */
#define FF_USE_EXPAND 0 /* 启用f_expand */
#define FF_USE_CHMOD 1 /* 启用f_chmod */
#define FF_USE_LABEL 1 /* 启用卷标功能 */
#define FF_USE_FORWARD 0 /* 启用f_forward */
/*---------------------------------------------------------------------------/
/ 本地化配置
/---------------------------------------------------------------------------*/
#define FF_CODE_PAGE 936 /* 简体中文GBK */
#define FF_USE_LFN 2 /* 0:禁用长文件名 1:静态 2:动态 3:栈 */
#define FF_MAX_LFN 255 /* 最大长文件名长度 */
#define FF_LFN_UNICODE 0 /* 0:ANSI/OEM 1:Unicode UTF-16 */
#define FF_LFN_BUF 255 /* 长文件名缓冲区大小 */
#define FF_SFN_BUF 12 /* 短文件名缓冲区大小 */
/*---------------------------------------------------------------------------/
/ 驱动器配置
/---------------------------------------------------------------------------*/
#define FF_FS_RPATH 0 /* 相对路径支持 */
#define FF_VOLUMES 1 /* 逻辑驱动器数量 (1-10) */
#define FF_STR_VOLUME_ID 0 /* 0:数字 1:字符串 */
#define FF_MULTI_PARTITION 0 /* 多分区支持 */
#define FF_MIN_SS 512 /* 最小扇区大小 */
#define FF_MAX_SS 512 /* 最大扇区大小 */
#define FF_USE_TRIM 0 /* 启用TRIM命令 */
#define FF_FS_NOFSINFO 0 /* 0:使用FSINFO 1:不使用 */
/*---------------------------------------------------------------------------/
/ 系统配置
/---------------------------------------------------------------------------*/
#define FF_FS_TINY 0 /* 0:普通模式 1:Tiny模式 */
#define FF_FS_EXFAT 0 /* 启用exFAT支持 */
#define FF_FS_NORTC 0 /* 0:使用RTC 1:不使用 */
#define FF_NORTC_MON 1 /* 默认月份 */
#define FF_NORTC_MDAY 1 /* 默认日期 */
#define FF_NORTC_YEAR 2024 /* 默认年份 */
#define FF_FS_LOCK 0 /* 文件锁定功能 */
#define FF_FS_REENTRANT 0 /* 0:不可重入 1:可重入 */
#define FF_FS_TIMEOUT 1000 /* 超时时间(ms) */
#define FF_SYNC_t HANDLE /* 同步对象类型 */
配置说明:
- FF_USE_LFN = 2: 使用动态内存分配支持长文件名
- FF_CODE_PAGE = 936: 支持中文文件名(GBK编码)
- FF_USE_MKFS = 1: 启用格式化功能
- FF_FS_REENTRANT = 0: 单线程模式(RTOS环境需设为1)
步骤2:实现SD卡SPI驱动¶
2.1 SD卡SPI驱动头文件¶
创建 sd_spi.h:
#ifndef SD_SPI_H
#define SD_SPI_H
#include "main.h"
#include <stdint.h>
/* SD卡命令定义 */
#define CMD0 0 /* GO_IDLE_STATE */
#define CMD1 1 /* SEND_OP_COND (MMC) */
#define ACMD41 0x29 /* SEND_OP_COND (SDC) */
#define CMD8 8 /* SEND_IF_COND */
#define CMD9 9 /* SEND_CSD */
#define CMD10 10 /* SEND_CID */
#define CMD12 12 /* STOP_TRANSMISSION */
#define CMD16 16 /* SET_BLOCKLEN */
#define CMD17 17 /* READ_SINGLE_BLOCK */
#define CMD18 18 /* READ_MULTIPLE_BLOCK */
#define CMD23 23 /* SET_BLOCK_COUNT (MMC) */
#define ACMD23 0x17 /* SET_WR_BLK_ERASE_COUNT (SDC) */
#define CMD24 24 /* WRITE_BLOCK */
#define CMD25 25 /* WRITE_MULTIPLE_BLOCK */
#define CMD32 32 /* ERASE_ER_BLK_START */
#define CMD33 33 /* ERASE_ER_BLK_END */
#define CMD38 38 /* ERASE */
#define CMD55 55 /* APP_CMD */
#define CMD58 58 /* READ_OCR */
/* SD卡响应类型 */
#define SD_RESPONSE_NO_ERROR 0x00
#define SD_IN_IDLE_STATE 0x01
#define SD_ERASE_RESET 0x02
#define SD_ILLEGAL_COMMAND 0x04
#define SD_COM_CRC_ERROR 0x08
#define SD_ERASE_SEQUENCE_ERROR 0x10
#define SD_ADDRESS_ERROR 0x20
#define SD_PARAMETER_ERROR 0x40
/* SD卡类型 */
#define SD_TYPE_MMC 0x01
#define SD_TYPE_V1 0x02
#define SD_TYPE_V2 0x04
#define SD_TYPE_V2HC 0x06
/* 函数声明 */
uint8_t SD_Init(void);
uint8_t SD_ReadSingleBlock(uint32_t sector, uint8_t *buffer);
uint8_t SD_WriteSingleBlock(uint32_t sector, const uint8_t *buffer);
uint8_t SD_ReadMultipleBlocks(uint32_t sector, uint8_t *buffer, uint32_t count);
uint8_t SD_WriteMultipleBlocks(uint32_t sector, const uint8_t *buffer, uint32_t count);
uint8_t SD_GetCardInfo(uint8_t *cid, uint8_t *csd);
#endif /* SD_SPI_H */
2.2 SD卡SPI驱动实现¶
创建 sd_spi.c:
#include "sd_spi.h"
#include "spi.h" // HAL SPI驱动
/* SD卡片选引脚定义 */
#define SD_CS_LOW() HAL_GPIO_WritePin(SD_CS_GPIO_Port, SD_CS_Pin, GPIO_PIN_RESET)
#define SD_CS_HIGH() HAL_GPIO_WritePin(SD_CS_GPIO_Port, SD_CS_Pin, GPIO_PIN_SET)
/* 全局变量 */
static uint8_t sd_type = 0;
/**
* @brief SPI发送接收一个字节
*/
static uint8_t SPI_TransmitReceive(uint8_t data)
{
uint8_t rx_data;
HAL_SPI_TransmitReceive(&hspi2, &data, &rx_data, 1, 100);
return rx_data;
}
/**
* @brief 等待SD卡就绪
*/
static uint8_t SD_WaitReady(void)
{
uint8_t res;
uint16_t timeout = 0;
do {
res = SPI_TransmitReceive(0xFF);
timeout++;
if (timeout > 5000) {
return 1; // 超时
}
} while (res != 0xFF);
return 0; // 就绪
}
/**
* @brief 发送SD卡命令
*/
static uint8_t SD_SendCommand(uint8_t cmd, uint32_t arg, uint8_t crc)
{
uint8_t res;
uint8_t retry = 0;
// 等待SD卡就绪
if (SD_WaitReady()) {
return 0xFF;
}
// 发送命令包
SPI_TransmitReceive(cmd | 0x40); // 命令索引
SPI_TransmitReceive((uint8_t)(arg >> 24)); // 参数[31:24]
SPI_TransmitReceive((uint8_t)(arg >> 16)); // 参数[23:16]
SPI_TransmitReceive((uint8_t)(arg >> 8)); // 参数[15:8]
SPI_TransmitReceive((uint8_t)arg); // 参数[7:0]
SPI_TransmitReceive(crc); // CRC校验
// 等待响应
if (cmd == CMD12) {
SPI_TransmitReceive(0xFF); // CMD12需要额外的字节
}
// 读取响应
do {
res = SPI_TransmitReceive(0xFF);
retry++;
} while ((res & 0x80) && retry < 10);
return res;
}
/**
* @brief 初始化SD卡
*/
uint8_t SD_Init(void)
{
uint8_t res;
uint8_t retry;
uint8_t buf[4];
// 片选拉高
SD_CS_HIGH();
// 发送至少74个时钟脉冲
for (uint8_t i = 0; i < 10; i++) {
SPI_TransmitReceive(0xFF);
}
// 进入IDLE状态
retry = 0;
do {
res = SD_SendCommand(CMD0, 0, 0x95);
retry++;
if (retry > 200) {
return 1; // 初始化失败
}
} while (res != SD_IN_IDLE_STATE);
// 检查SD卡版本
res = SD_SendCommand(CMD8, 0x1AA, 0x87);
if (res == SD_IN_IDLE_STATE) {
// SD V2.0
for (uint8_t i = 0; i < 4; i++) {
buf[i] = SPI_TransmitReceive(0xFF);
}
if (buf[2] == 0x01 && buf[3] == 0xAA) {
// 初始化SD V2.0
retry = 0;
do {
SD_SendCommand(CMD55, 0, 0xFF);
res = SD_SendCommand(ACMD41, 0x40000000, 0xFF);
retry++;
if (retry > 200) {
return 1;
}
} while (res != SD_RESPONSE_NO_ERROR);
// 检查CCS位
res = SD_SendCommand(CMD58, 0, 0xFF);
if (res == SD_RESPONSE_NO_ERROR) {
for (uint8_t i = 0; i < 4; i++) {
buf[i] = SPI_TransmitReceive(0xFF);
}
if (buf[0] & 0x40) {
sd_type = SD_TYPE_V2HC; // SDHC
} else {
sd_type = SD_TYPE_V2; // SD V2.0
}
}
}
} else {
// SD V1.0 或 MMC
SD_SendCommand(CMD55, 0, 0xFF);
res = SD_SendCommand(ACMD41, 0, 0xFF);
if (res <= 1) {
// SD V1.0
sd_type = SD_TYPE_V1;
retry = 0;
do {
SD_SendCommand(CMD55, 0, 0xFF);
res = SD_SendCommand(ACMD41, 0, 0xFF);
retry++;
if (retry > 200) {
return 1;
}
} while (res != SD_RESPONSE_NO_ERROR);
} else {
// MMC
sd_type = SD_TYPE_MMC;
retry = 0;
do {
res = SD_SendCommand(CMD1, 0, 0xFF);
retry++;
if (retry > 200) {
return 1;
}
} while (res != SD_RESPONSE_NO_ERROR);
}
// 设置块大小为512字节
res = SD_SendCommand(CMD16, 512, 0xFF);
if (res != SD_RESPONSE_NO_ERROR) {
return 1;
}
}
SD_CS_HIGH();
SPI_TransmitReceive(0xFF);
return 0; // 初始化成功
}
/**
* @brief 读取单个扇区
*/
uint8_t SD_ReadSingleBlock(uint32_t sector, uint8_t *buffer)
{
uint8_t res;
uint16_t retry;
// 对于非SDHC卡,需要转换为字节地址
if (sd_type != SD_TYPE_V2HC) {
sector *= 512;
}
SD_CS_LOW();
// 发送读命令
res = SD_SendCommand(CMD17, sector, 0xFF);
if (res != SD_RESPONSE_NO_ERROR) {
SD_CS_HIGH();
return 1;
}
// 等待数据令牌
retry = 0;
do {
res = SPI_TransmitReceive(0xFF);
retry++;
if (retry > 5000) {
SD_CS_HIGH();
return 1;
}
} while (res != 0xFE);
// 读取512字节数据
for (uint16_t i = 0; i < 512; i++) {
buffer[i] = SPI_TransmitReceive(0xFF);
}
// 读取CRC(忽略)
SPI_TransmitReceive(0xFF);
SPI_TransmitReceive(0xFF);
SD_CS_HIGH();
SPI_TransmitReceive(0xFF);
return 0;
}
/**
* @brief 写入单个扇区
*/
uint8_t SD_WriteSingleBlock(uint32_t sector, const uint8_t *buffer)
{
uint8_t res;
uint16_t retry;
// 对于非SDHC卡,需要转换为字节地址
if (sd_type != SD_TYPE_V2HC) {
sector *= 512;
}
SD_CS_LOW();
// 发送写命令
res = SD_SendCommand(CMD24, sector, 0xFF);
if (res != SD_RESPONSE_NO_ERROR) {
SD_CS_HIGH();
return 1;
}
// 发送数据令牌
SPI_TransmitReceive(0xFE);
// 写入512字节数据
for (uint16_t i = 0; i < 512; i++) {
SPI_TransmitReceive(buffer[i]);
}
// 发送CRC(忽略)
SPI_TransmitReceive(0xFF);
SPI_TransmitReceive(0xFF);
// 读取数据响应
res = SPI_TransmitReceive(0xFF);
if ((res & 0x1F) != 0x05) {
SD_CS_HIGH();
return 1;
}
// 等待写入完成
retry = 0;
while (SPI_TransmitReceive(0xFF) == 0x00) {
retry++;
if (retry > 50000) {
SD_CS_HIGH();
return 1;
}
}
SD_CS_HIGH();
SPI_TransmitReceive(0xFF);
return 0;
}
代码说明:
- SD_Init(): 初始化SD卡,识别卡类型
- SD_SendCommand(): 发送SD卡命令
- SD_ReadSingleBlock(): 读取单个扇区(512字节)
- SD_WriteSingleBlock(): 写入单个扇区
- 支持SD V1.0、SD V2.0和SDHC卡
步骤3:实现FATFS磁盘I/O接口¶
3.1 理解diskio接口¶
FATFS通过 diskio.c 中定义的接口访问底层存储设备。需要实现以下5个函数:
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); // 磁盘控制
参数说明:
- pdrv: 物理驱动器号(0-9)
- buff: 数据缓冲区
- sector: 扇区号(LBA地址)
- count: 扇区数量
- cmd: 控制命令
3.2 实现diskio.c¶
修改 diskio.c 文件:
#include "ff.h"
#include "diskio.h"
#include "sd_spi.h"
#include <string.h>
/* 磁盘状态 */
static volatile DSTATUS Stat = STA_NOINIT;
/**
* @brief 初始化磁盘
* @param pdrv: 物理驱动器号
* @retval 磁盘状态
*/
DSTATUS disk_initialize(BYTE pdrv)
{
uint8_t res;
switch (pdrv) {
case 0: // SD卡
res = SD_Init();
if (res == 0) {
Stat &= ~STA_NOINIT; // 清除未初始化标志
} else {
Stat = STA_NOINIT;
}
break;
default:
Stat = STA_NOINIT;
break;
}
return Stat;
}
/**
* @brief 获取磁盘状态
* @param pdrv: 物理驱动器号
* @retval 磁盘状态
*/
DSTATUS disk_status(BYTE pdrv)
{
switch (pdrv) {
case 0:
return Stat;
default:
return STA_NOINIT;
}
}
/**
* @brief 读取扇区
* @param pdrv: 物理驱动器号
* @param buff: 数据缓冲区
* @param sector: 起始扇区号
* @param count: 扇区数量
* @retval 操作结果
*/
DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count)
{
uint8_t res;
if (pdrv != 0 || count == 0) {
return RES_PARERR;
}
if (Stat & STA_NOINIT) {
return RES_NOTRDY;
}
if (count == 1) {
// 读取单个扇区
res = SD_ReadSingleBlock(sector, buff);
} else {
// 读取多个扇区
res = SD_ReadMultipleBlocks(sector, buff, count);
}
if (res == 0) {
return RES_OK;
} else {
return RES_ERROR;
}
}
/**
* @brief 写入扇区
* @param pdrv: 物理驱动器号
* @param buff: 数据缓冲区
* @param sector: 起始扇区号
* @param count: 扇区数量
* @retval 操作结果
*/
#if FF_FS_READONLY == 0
DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count)
{
uint8_t res;
if (pdrv != 0 || count == 0) {
return RES_PARERR;
}
if (Stat & STA_NOINIT) {
return RES_NOTRDY;
}
if (Stat & STA_PROTECT) {
return RES_WRPRT;
}
if (count == 1) {
// 写入单个扇区
res = SD_WriteSingleBlock(sector, buff);
} else {
// 写入多个扇区
res = SD_WriteMultipleBlocks(sector, buff, count);
}
if (res == 0) {
return RES_OK;
} else {
return RES_ERROR;
}
}
#endif
/**
* @brief 磁盘I/O控制
* @param pdrv: 物理驱动器号
* @param cmd: 控制命令
* @param buff: 数据缓冲区
* @retval 操作结果
*/
DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff)
{
DRESULT res = RES_ERROR;
if (pdrv != 0) {
return RES_PARERR;
}
if (Stat & STA_NOINIT) {
return RES_NOTRDY;
}
switch (cmd) {
case CTRL_SYNC:
// 同步缓存(SD卡不需要)
res = RES_OK;
break;
case GET_SECTOR_COUNT:
// 获取扇区数量
// 这里需要读取SD卡的CSD寄存器
*(LBA_t*)buff = 0; // 需要实现
res = RES_OK;
break;
case GET_SECTOR_SIZE:
// 获取扇区大小
*(WORD*)buff = 512;
res = RES_OK;
break;
case GET_BLOCK_SIZE:
// 获取擦除块大小
*(DWORD*)buff = 1; // 1个扇区
res = RES_OK;
break;
default:
res = RES_PARERR;
break;
}
return res;
}
/**
* @brief 获取当前时间(用于文件时间戳)
* @retval 当前时间(FAT格式)
*/
DWORD get_fattime(void)
{
// 返回固定时间:2024-01-15 12:00:00
// 格式:bit31:25=年(0-127, +1980), bit24:21=月(1-12), bit20:16=日(1-31)
// bit15:11=时(0-23), bit10:5=分(0-59), bit4:0=秒/2(0-29)
return ((DWORD)(2024 - 1980) << 25) // 年
| ((DWORD)1 << 21) // 月
| ((DWORD)15 << 16) // 日
| ((DWORD)12 << 11) // 时
| ((DWORD)0 << 5) // 分
| ((DWORD)0 >> 1); // 秒
// 实际应用中应该从RTC读取真实时间
}
代码说明:
- disk_initialize(): 调用SD卡初始化函数
- disk_read(): 调用SD卡读取函数
- disk_write(): 调用SD卡写入函数
- disk_ioctl(): 处理控制命令
- get_fattime(): 提供文件时间戳
3.3 实现多扇区读写(可选优化)¶
在 sd_spi.c 中添加多扇区读写函数:
/**
* @brief 读取多个扇区
*/
uint8_t SD_ReadMultipleBlocks(uint32_t sector, uint8_t *buffer, uint32_t count)
{
uint8_t res;
uint16_t retry;
// 对于非SDHC卡,需要转换为字节地址
if (sd_type != SD_TYPE_V2HC) {
sector *= 512;
}
SD_CS_LOW();
// 发送读多块命令
res = SD_SendCommand(CMD18, sector, 0xFF);
if (res != SD_RESPONSE_NO_ERROR) {
SD_CS_HIGH();
return 1;
}
// 读取多个块
for (uint32_t i = 0; i < count; i++) {
// 等待数据令牌
retry = 0;
do {
res = SPI_TransmitReceive(0xFF);
retry++;
if (retry > 5000) {
SD_SendCommand(CMD12, 0, 0xFF); // 停止传输
SD_CS_HIGH();
return 1;
}
} while (res != 0xFE);
// 读取512字节数据
for (uint16_t j = 0; j < 512; j++) {
buffer[i * 512 + j] = SPI_TransmitReceive(0xFF);
}
// 读取CRC(忽略)
SPI_TransmitReceive(0xFF);
SPI_TransmitReceive(0xFF);
}
// 停止传输
SD_SendCommand(CMD12, 0, 0xFF);
SD_CS_HIGH();
SPI_TransmitReceive(0xFF);
return 0;
}
/**
* @brief 写入多个扇区
*/
uint8_t SD_WriteMultipleBlocks(uint32_t sector, const uint8_t *buffer, uint32_t count)
{
uint8_t res;
uint16_t retry;
// 对于非SDHC卡,需要转换为字节地址
if (sd_type != SD_TYPE_V2HC) {
sector *= 512;
}
SD_CS_LOW();
// 预擦除(可选,提高性能)
SD_SendCommand(CMD55, 0, 0xFF);
SD_SendCommand(ACMD23, count, 0xFF);
// 发送写多块命令
res = SD_SendCommand(CMD25, sector, 0xFF);
if (res != SD_RESPONSE_NO_ERROR) {
SD_CS_HIGH();
return 1;
}
// 写入多个块
for (uint32_t i = 0; i < count; i++) {
// 发送数据令牌
SPI_TransmitReceive(0xFC);
// 写入512字节数据
for (uint16_t j = 0; j < 512; j++) {
SPI_TransmitReceive(buffer[i * 512 + j]);
}
// 发送CRC(忽略)
SPI_TransmitReceive(0xFF);
SPI_TransmitReceive(0xFF);
// 读取数据响应
res = SPI_TransmitReceive(0xFF);
if ((res & 0x1F) != 0x05) {
SD_SendCommand(CMD12, 0, 0xFF); // 停止传输
SD_CS_HIGH();
return 1;
}
// 等待写入完成
retry = 0;
while (SPI_TransmitReceive(0xFF) == 0x00) {
retry++;
if (retry > 50000) {
SD_SendCommand(CMD12, 0, 0xFF);
SD_CS_HIGH();
return 1;
}
}
}
// 发送停止令牌
SPI_TransmitReceive(0xFD);
// 等待完成
retry = 0;
while (SPI_TransmitReceive(0xFF) == 0x00) {
retry++;
if (retry > 50000) {
SD_CS_HIGH();
return 1;
}
}
SD_CS_HIGH();
SPI_TransmitReceive(0xFF);
return 0;
}
性能优化说明: - 多扇区读写减少了命令开销 - 预擦除命令提高写入性能 - 适合大文件的连续读写
步骤4:应用层封装和测试¶
4.1 创建应用层接口¶
创建 fatfs_app.c:
#include "ff.h"
#include "diskio.h"
#include <stdio.h>
#include <string.h>
/* FATFS对象 */
static FATFS fs;
static FIL file;
static DIR dir;
static FILINFO fno;
/**
* @brief 挂载文件系统
* @retval 0:成功 其他:失败
*/
int FATFS_Mount(void)
{
FRESULT res;
// 挂载文件系统
res = f_mount(&fs, "0:", 1);
if (res != FR_OK) {
printf("Mount failed: %d\n", res);
// 如果挂载失败,尝试格式化
if (res == FR_NO_FILESYSTEM) {
printf("No filesystem found, formatting...\n");
BYTE work[FF_MAX_SS];
res = f_mkfs("0:", 0, work, sizeof(work));
if (res != FR_OK) {
printf("Format failed: %d\n", res);
return -1;
}
// 重新挂载
res = f_mount(&fs, "0:", 1);
if (res != FR_OK) {
printf("Mount failed after format: %d\n", res);
return -1;
}
printf("Format and mount successful\n");
} else {
return -1;
}
} else {
printf("Mount successful\n");
}
return 0;
}
/**
* @brief 卸载文件系统
*/
void FATFS_Unmount(void)
{
f_mount(NULL, "0:", 0);
printf("Unmounted\n");
}
/**
* @brief 获取文件系统信息
*/
void FATFS_GetInfo(void)
{
FRESULT res;
DWORD fre_clust, fre_sect, tot_sect;
FATFS *fs_ptr;
// 获取卷信息
res = f_getfree("0:", &fre_clust, &fs_ptr);
if (res != FR_OK) {
printf("Get info failed: %d\n", res);
return;
}
// 计算容量
tot_sect = (fs_ptr->n_fatent - 2) * fs_ptr->csize;
fre_sect = fre_clust * fs_ptr->csize;
printf("=== File System Info ===\n");
printf("Total: %lu KB\n", tot_sect / 2);
printf("Free: %lu KB\n", fre_sect / 2);
printf("Used: %lu KB\n", (tot_sect - fre_sect) / 2);
printf("Usage: %lu%%\n", ((tot_sect - fre_sect) * 100) / tot_sect);
printf("========================\n");
}
/**
* @brief 写入文件测试
*/
int FATFS_WriteTest(void)
{
FRESULT res;
UINT bw;
const char *text = "Hello, FATFS!\nThis is a test file.\n";
printf("Writing file...\n");
// 打开文件(创建或覆盖)
res = f_open(&file, "0:/test.txt", FA_CREATE_ALWAYS | FA_WRITE);
if (res != FR_OK) {
printf("Open failed: %d\n", res);
return -1;
}
// 写入数据
res = f_write(&file, text, strlen(text), &bw);
if (res != FR_OK) {
printf("Write failed: %d\n", res);
f_close(&file);
return -1;
}
printf("Written %u bytes\n", bw);
// 关闭文件
f_close(&file);
return 0;
}
/**
* @brief 读取文件测试
*/
int FATFS_ReadTest(void)
{
FRESULT res;
UINT br;
char buffer[128];
printf("Reading file...\n");
// 打开文件(只读)
res = f_open(&file, "0:/test.txt", FA_READ);
if (res != FR_OK) {
printf("Open failed: %d\n", res);
return -1;
}
// 读取数据
res = f_read(&file, buffer, sizeof(buffer) - 1, &br);
if (res != FR_OK) {
printf("Read failed: %d\n", res);
f_close(&file);
return -1;
}
buffer[br] = '\0';
printf("Read %u bytes:\n%s\n", br, buffer);
// 关闭文件
f_close(&file);
return 0;
}
/**
* @brief 列出目录内容
*/
void FATFS_ListDir(const char *path)
{
FRESULT res;
printf("=== Directory: %s ===\n", path);
// 打开目录
res = f_opendir(&dir, path);
if (res != FR_OK) {
printf("Open dir failed: %d\n", res);
return;
}
// 遍历目录
while (1) {
res = f_readdir(&dir, &fno);
if (res != FR_OK || fno.fname[0] == 0) {
break;
}
if (fno.fattrib & AM_DIR) {
printf(" [DIR] %s\n", fno.fname);
} else {
printf(" [FILE] %-20s %10lu bytes\n", fno.fname, fno.fsize);
}
}
f_closedir(&dir);
printf("======================\n");
}
/**
* @brief 创建目录测试
*/
int FATFS_MkdirTest(void)
{
FRESULT res;
printf("Creating directory...\n");
res = f_mkdir("0:/test_dir");
if (res != FR_OK && res != FR_EXIST) {
printf("Mkdir failed: %d\n", res);
return -1;
}
printf("Directory created\n");
return 0;
}
/**
* @brief 删除文件测试
*/
int FATFS_DeleteTest(void)
{
FRESULT res;
printf("Deleting file...\n");
res = f_unlink("0:/test.txt");
if (res != FR_OK) {
printf("Delete failed: %d\n", res);
return -1;
}
printf("File deleted\n");
return 0;
}
/**
* @brief 性能测试
*/
void FATFS_PerformanceTest(void)
{
FRESULT res;
UINT bw, br;
uint8_t buffer[512];
uint32_t start_tick, end_tick;
const uint32_t test_size = 1024 * 100; // 100KB
printf("=== Performance Test ===\n");
// 准备测试数据
for (uint16_t i = 0; i < 512; i++) {
buffer[i] = i & 0xFF;
}
// 写入测试
start_tick = HAL_GetTick();
res = f_open(&file, "0:/perf_test.dat", FA_CREATE_ALWAYS | FA_WRITE);
if (res == FR_OK) {
for (uint32_t i = 0; i < test_size / 512; i++) {
f_write(&file, buffer, 512, &bw);
}
f_close(&file);
}
end_tick = HAL_GetTick();
uint32_t write_time = end_tick - start_tick;
float write_speed = (test_size / 1024.0) / (write_time / 1000.0);
printf("Write: %lu ms (%.2f KB/s)\n", write_time, write_speed);
// 读取测试
start_tick = HAL_GetTick();
res = f_open(&file, "0:/perf_test.dat", FA_READ);
if (res == FR_OK) {
for (uint32_t i = 0; i < test_size / 512; i++) {
f_read(&file, buffer, 512, &br);
}
f_close(&file);
}
end_tick = HAL_GetTick();
uint32_t read_time = end_tick - start_tick;
float read_speed = (test_size / 1024.0) / (read_time / 1000.0);
printf("Read: %lu ms (%.2f KB/s)\n", read_time, read_speed);
printf("========================\n");
}
4.2 主程序测试¶
在 main.c 中添加测试代码:
#include "main.h"
#include "fatfs_app.h"
int main(void)
{
// 系统初始化
HAL_Init();
SystemClock_Config();
// 初始化外设
MX_GPIO_Init();
MX_SPI2_Init();
MX_USART1_UART_Init();
printf("\n=== FATFS Porting Test ===\n");
// 挂载文件系统
if (FATFS_Mount() != 0) {
printf("Mount failed, check SD card\n");
Error_Handler();
}
// 获取文件系统信息
FATFS_GetInfo();
// 写入文件测试
FATFS_WriteTest();
// 读取文件测试
FATFS_ReadTest();
// 创建目录测试
FATFS_MkdirTest();
// 列出根目录
FATFS_ListDir("0:/");
// 性能测试
FATFS_PerformanceTest();
// 删除测试文件
FATFS_DeleteTest();
// 卸载文件系统
FATFS_Unmount();
printf("=== Test Complete ===\n");
while (1) {
HAL_Delay(1000);
}
}
预期输出:
=== FATFS Porting Test ===
Mount successful
=== File System Info ===
Total: 7680000 KB
Free: 7679488 KB
Used: 512 KB
Usage: 0%
========================
Writing file...
Written 42 bytes
Reading file...
Read 42 bytes:
Hello, FATFS!
This is a test file.
Creating directory...
Directory created
=== Directory: 0:/ ===
[DIR] test_dir
[FILE] test.txt 42 bytes
[FILE] perf_test.dat 102400 bytes
======================
=== Performance Test ===
Write: 1250 ms (82.00 KB/s)
Read: 450 ms (228.00 KB/s)
========================
Deleting file...
File deleted
Unmounted
=== Test Complete ===
步骤5:SDIO模式实现(高性能方案)¶
5.1 SDIO vs SPI对比¶
| 特性 | SPI模式 | SDIO模式 |
|---|---|---|
| 接线数量 | 4根(CLK, MISO, MOSI, CS) | 6根(CLK, CMD, D0-D3) |
| 数据宽度 | 1位 | 1位或4位 |
| 最大速度 | 25MHz | 50MHz(SDR)/ 104MHz(DDR) |
| 理论带宽 | 3.125 MB/s | 25 MB/s(4位@50MHz) |
| 实际速度 | 50-100 KB/s | 2-5 MB/s |
| CPU占用 | 高(软件控制) | 低(DMA支持) |
| 适用场景 | 简单应用 | 高性能应用 |
5.2 SDIO驱动实现¶
创建 sd_sdio.c:
#include "sd_sdio.h"
#include "stm32f4xx_hal.h"
/* SDIO句柄 */
extern SD_HandleTypeDef hsd;
/* DMA缓冲区(必须4字节对齐) */
__attribute__((aligned(4))) static uint8_t sd_buffer[512];
/**
* @brief 初始化SD卡(SDIO模式)
*/
uint8_t SD_SDIO_Init(void)
{
HAL_StatusTypeDef status;
// 初始化SDIO外设
status = HAL_SD_Init(&hsd);
if (status != HAL_OK) {
return 1;
}
// 配置为4位宽总线
status = HAL_SD_ConfigWideBusOperation(&hsd, SDIO_BUS_WIDE_4B);
if (status != HAL_OK) {
return 1;
}
return 0;
}
/**
* @brief 读取单个扇区(SDIO + DMA)
*/
uint8_t SD_SDIO_ReadBlocks(uint32_t sector, uint8_t *buffer, uint32_t count)
{
HAL_StatusTypeDef status;
uint32_t timeout = 0;
// 使用DMA读取
status = HAL_SD_ReadBlocks_DMA(&hsd, buffer, sector, count);
if (status != HAL_OK) {
return 1;
}
// 等待传输完成
while (HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER) {
timeout++;
if (timeout > 10000) {
return 1;
}
HAL_Delay(1);
}
return 0;
}
/**
* @brief 写入扇区(SDIO + DMA)
*/
uint8_t SD_SDIO_WriteBlocks(uint32_t sector, const uint8_t *buffer, uint32_t count)
{
HAL_StatusTypeDef status;
uint32_t timeout = 0;
// 使用DMA写入
status = HAL_SD_WriteBlocks_DMA(&hsd, (uint8_t*)buffer, sector, count);
if (status != HAL_OK) {
return 1;
}
// 等待传输完成
while (HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER) {
timeout++;
if (timeout > 10000) {
return 1;
}
HAL_Delay(1);
}
return 0;
}
/**
* @brief 获取SD卡信息
*/
uint8_t SD_SDIO_GetCardInfo(HAL_SD_CardInfoTypeDef *card_info)
{
HAL_StatusTypeDef status;
status = HAL_SD_GetCardInfo(&hsd, card_info);
if (status != HAL_OK) {
return 1;
}
return 0;
}
5.3 修改diskio.c支持SDIO¶
/* 在diskio.c中添加SDIO支持 */
#ifdef USE_SDIO_MODE
#include "sd_sdio.h"
DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count)
{
uint8_t res;
if (pdrv != 0 || count == 0) {
return RES_PARERR;
}
if (Stat & STA_NOINIT) {
return RES_NOTRDY;
}
// 使用SDIO读取
res = SD_SDIO_ReadBlocks(sector, buff, count);
if (res == 0) {
return RES_OK;
} else {
return RES_ERROR;
}
}
DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count)
{
uint8_t res;
if (pdrv != 0 || count == 0) {
return RES_PARERR;
}
if (Stat & STA_NOINIT) {
return RES_NOTRDY;
}
if (Stat & STA_PROTECT) {
return RES_WRPRT;
}
// 使用SDIO写入
res = SD_SDIO_WriteBlocks(sector, buff, count);
if (res == 0) {
return RES_OK;
} else {
return RES_ERROR;
}
}
#endif
5.4 STM32CubeMX配置SDIO¶
配置步骤:
- 启用SDIO外设
- Pinout & Configuration → Connectivity → SDIO
-
Mode: SD 4 bits Wide bus
-
配置DMA
- DMA Settings → Add
- DMA Request: SDIO_RX, Direction: Peripheral to Memory
- DMA Request: SDIO_TX, Direction: Memory to Peripheral
- Priority: High
-
Mode: Normal
-
配置时钟
- Clock Configuration
-
SDIO Clock: 48MHz(最大)
-
生成代码
- Project → Generate Code
性能对比: - SPI模式:50-100 KB/s - SDIO模式(1位):500-800 KB/s - SDIO模式(4位):2-5 MB/s
步骤6:性能优化¶
6.1 缓存优化¶
增大文件系统缓存:
在 ffconf.h 中:
使用快速定位:
// 启用快速定位功能
#define FF_USE_FASTSEEK 1
// 使用示例
DWORD clmt[SZ_TBL]; // 簇链表
file.cltbl = clmt;
clmt[0] = SZ_TBL;
f_lseek(&file, CREATE_LINKMAP); // 创建链表
6.2 DMA优化¶
使用DMA传输:
/**
* @brief 使用DMA的高速读取
*/
FRESULT fast_read_file(const char *path, uint8_t *buffer, uint32_t size)
{
FRESULT res;
FIL file;
UINT br;
// 打开文件
res = f_open(&file, path, FA_READ);
if (res != FR_OK) {
return res;
}
// 使用DMA读取(buffer必须4字节对齐)
res = f_read(&file, buffer, size, &br);
f_close(&file);
return res;
}
6.3 批量操作优化¶
批量写入:
/**
* @brief 批量写入优化
*/
void batch_write_example(void)
{
FIL file;
UINT bw;
uint8_t buffer[4096]; // 使用大缓冲区
f_open(&file, "0:/large_file.dat", FA_CREATE_ALWAYS | FA_WRITE);
// 批量写入,减少f_write调用次数
for (int i = 0; i < 100; i++) {
// 准备数据
memset(buffer, i, sizeof(buffer));
// 一次写入4KB
f_write(&file, buffer, sizeof(buffer), &bw);
}
f_close(&file);
}
6.4 性能测试对比¶
/**
* @brief 性能对比测试
*/
void performance_comparison(void)
{
uint32_t start, end;
UINT bw;
FIL file;
uint8_t buffer[512];
printf("=== Performance Comparison ===\n");
// 测试1:小缓冲区写入
start = HAL_GetTick();
f_open(&file, "0:/test1.dat", FA_CREATE_ALWAYS | FA_WRITE);
for (int i = 0; i < 200; i++) {
f_write(&file, buffer, 512, &bw);
}
f_close(&file);
end = HAL_GetTick();
printf("Small buffer (512B): %lu ms\n", end - start);
// 测试2:大缓冲区写入
uint8_t large_buffer[4096];
start = HAL_GetTick();
f_open(&file, "0:/test2.dat", FA_CREATE_ALWAYS | FA_WRITE);
for (int i = 0; i < 25; i++) {
f_write(&file, large_buffer, 4096, &bw);
}
f_close(&file);
end = HAL_GetTick();
printf("Large buffer (4KB): %lu ms\n", end - start);
printf("==============================\n");
}
优化效果: - 小缓冲区:~2000ms - 大缓冲区:~800ms - 性能提升:2.5倍
步骤7:故障排除¶
问题1:挂载失败¶
现象:
可能原因: - SD卡未插入或接触不良 - SD卡格式不正确 - SPI/SDIO通信失败 - 驱动初始化失败
解决方法:
-
检查硬件连接
-
检查SD卡格式
- 使用电脑格式化为FAT32
- 分配单元大小:4096字节
-
确保SD卡容量≤32GB
-
降低SPI速度
-
检查电源
- 确保SD卡供电稳定(3.3V)
- 添加去耦电容(0.1uF + 10uF)
问题2:文件读写失败¶
现象:
可能原因: - 磁盘I/O接口实现错误 - 扇区地址计算错误 - 超时时间设置不当 - DMA配置错误
解决方法:
-
添加详细日志
-
检查地址转换
-
增加超时时间
问题3:中文文件名乱码¶
现象: 文件名显示为乱码或问号
解决方法:
-
配置代码页
-
启用长文件名
-
添加Unicode支持(可选)
问题4:性能低下¶
现象: 读写速度只有几KB/s
优化方法:
-
提高SPI速度
-
使用多扇区读写
-
使用SDIO模式
- 切换到SDIO接口
- 启用4位数据总线
-
使用DMA传输
-
增大缓冲区
问题5:文件系统损坏¶
现象:
恢复方法:
-
尝试重新格式化
-
使用电脑修复
- Windows: chkdsk /f
- Linux: fsck.vfat
-
Mac: diskutil repairVolume
-
备份重要数据
调试技巧¶
1. 使用逻辑分析仪
监控SPI/SDIO信号: - CLK时钟信号 - MOSI/MISO数据线 - CS片选信号 - 命令和响应时序
2. 添加详细日志
#define DEBUG_FATFS 1
#if DEBUG_FATFS
#define FATFS_LOG(fmt, ...) printf("[FATFS] " fmt "\n", ##__VA_ARGS__)
#else
#define FATFS_LOG(fmt, ...)
#endif
// 使用示例
FATFS_LOG("Mounting filesystem...");
FATFS_LOG("Mount result: %d", res);
3. 单步调试
在关键函数设置断点:
- disk_initialize()
- disk_read()
- disk_write()
- SD_SendCommand()
4. 性能分析
void profile_operation(void)
{
uint32_t start = HAL_GetTick();
// 执行操作
f_open(&file, "0:/test.txt", FA_READ);
uint32_t end = HAL_GetTick();
printf("Operation took %lu ms\n", end - start);
}
总结¶
通过本教程,你学习了:
- ✅ 文件系统移植的完整流程和架构
- ✅ FATFS的配置和接口实现方法
- ✅ SD卡SPI模式和SDIO模式的驱动开发
- ✅ 磁盘I/O接口的实现技巧
- ✅ 文件系统的测试和验证方法
- ✅ 性能优化和故障排除技巧
- ✅ 完整的SD卡文件系统项目实现
进阶挑战¶
尝试以下挑战来巩固学习:
- 挑战1:实现多文件系统支持
- 同时挂载SD卡和SPI Flash
-
实现文件在不同存储介质间的复制
-
挑战2:添加文件系统保护
- 实现写保护检测
- 添加文件访问权限控制
-
实现文件加密存储
-
挑战3:优化性能
- 实现预读缓存
- 使用DMA零拷贝传输
-
实现异步文件操作
-
挑战4:移植其他文件系统
- 移植LittleFS到SD卡
- 对比FATFS和LittleFS的性能
- 实现文件系统切换功能
完整代码¶
完整的项目代码可以在这里下载: - GitHub仓库:fatfs-porting-example - 包含SPI和SDIO两种实现 - 包含完整的测试代码
下一步¶
建议继续学习:
- Flash文件系统设计 - 学习Flash文件系统的设计原理
- 高性能文件系统实现 - 学习高性能文件系统的实现
- 数据持久化与掉电保护 - 学习数据可靠性保证
参考资料¶
- 官方文档
- FATFS官方网站
- SD卡规范
-
技术文章
- "SD Card SPI Mode Implementation"
- "SDIO Interface Programming Guide"
-
"File System Performance Optimization"
-
开源项目
- FATFS源码
- STM32 SD卡示例
反馈:如果你在学习过程中遇到问题,欢迎在评论区留言!
作者: 嵌入式知识平台
更新日期: 2024-01-15
版本: 1.0