SD卡及FATFS文件系统
SD卡及FATFS文件系统
- 一,SD卡简介
- 二,SD卡基础
- 2.1 SD卡类型与规格
- 测试历程
- 1,测试开始
- 2,挂载文件系统
- 3,创建测试目录
- 4,创建并写入测试文件
- 5,读取测试文件
一,SD卡简介
SD卡以其容量大、成本适中、接口相对简单等优点,成为了扩展存储的常用选择
直接操作SD卡的物理扇区非常复杂且低效。为了方便管理SD卡上的数据,我们需要文件系统
本节我们将通过SDIO接口(或SPI模式,但本教程重点关注SDIO)使用FATFS文件系统来读写SD卡
这里的SDIO模式怎么和SPI模式平起平坐了?
SDIO模式只是局限于SD卡,SDIO 和 SPI 都是“物理总线协议”(physical bus protocols),也就是通信接口,在使用SD卡时我们可以选择这两种模式去驱动SD卡,就像前面使用iic,spi去驱动从机一样
二,SD卡基础
2.1 SD卡类型与规格
物理尺寸: 主要有标准SD卡、miniSD卡和microSD卡。嵌入式应用中microSD卡因其小巧而最为常见。
容量标准:
SDSC (Standard Capacity): 容量最高至2GB。
SDHC (High Capacity): 容量从2GB到32GB。
SDXC (Extended Capacity): 容量从32GB到2TB。
SDUC (Ultra Capacity): 容量从2TB到128TB (较新标准)。
MCU对不同容量标准的支持可能有限,务必查阅MCU和FATFS库的兼容性说明。
速度等级 (Speed Class): 表示最低写入速度,如Class 2 (2MB/s), Class 4 (4MB/s), Class 6 (6MB/s), Class 10 (10MB/s)。还有UHS (Ultra High Speed) 等级如U1 (10MB/s), U3 (30MB/s),以及视频速度等级V6, V10, V30等。选择合适的SD卡需考虑应用对读写性能的要求。
这里的容量标准是怎么来的?有什么作用?我是小白,对于速度等级完全不理解
容量标准(SDSC/SDHC/SDXC/SDUC)其实是由 SD 协会(SD Association)在不同时间制定的一系列规范
标准 | 容量范围 | 支持的文件系统 | 主流用途 |
---|---|---|---|
SDSC | up to 2 GB 字节寻址 | FAT16 | 早期数码相机、旧设备 |
SDHC | > 2 GB – 32 GB 扇区寻址 | FAT32 | 现在最常见,用于手机、树莓派、通用存储 |
SDXC | > 32 GB – 2 TB 扇区寻址 | exFAT | 高清视频录制、大文件存储(4 K/8 K) |
SDUC | > 2 TB – 128 TB 扩展寻址 | exFAT | 专业视频、数据中心、嵌入式大容量存储(新兴) |
不同的容量标准与它自己的寻址方式绑定,什么是寻址方式呢?
当主机(MCU、相机、电脑)要读写 SD 卡时,会通过 SD 命令(CMD17、CMD24 等)告诉卡“从地址 X 开始读 N 个字节”或“把 N 个字节写到地址 X”。这个“地址 X”到底表示字节偏移(byte offset)还是块/扇区编号(block number),就是“寻址方式”的区别:
1,当X表示字节时,SD卡就是字节寻址(与前面的iic中的列高地址和列低地址一样,X表示命令的同时也表示地址位置/偏移),在SD卡的命令中,命令为32位,所以可以访问取值范围 0…2³²–1(≈4 GiB)内的任意字节,但是SDSC最大只有2 GiB,所以可以访问SDSC的所有内容
2,当X表示扇区号时,SD卡就是扇区寻址,X加1就是访问一个扇区512B,X同样是32位,这样 32 位扇区号可覆盖 2³² × 512 B ≈ 2 TiB 的容量,正好对应 SDXC 卡上限
3,扩展寻址,SDUC(>2 TiB–128 TiB)卡
所以我们可以发现不同的寻址方式解决的是容量问题
除此之外,我们还可以发现不同的容量标准与不同的文件系统绑定,为什么呢?
我们先把文件系统比喻成索引系统,小容量的SD卡只需要简单的索引,而大容量的SD卡,就需要更大的索引系统
那么到底什么是文件系统呢?
文件系统就是管理文件的规则
一个成品设备如相机、手机、电脑、车载系统、嵌入式设备……它们的固件(或操作系统)里只内置了对某些文件系统/规则的“翻译器”。
如果你给它插一个它“不认识”的文件系统(比如用 NTFS、ext4、APFS 等),它就“听不懂”卡里的目录结构,看不到任何文件
SD 协会为了存储卡容量与设备兼容,就对各代 SD 卡推荐了上面的文件系统:他们不是把文件系统像固件一样“写死”在卡片里,而是将SD卡在出厂时预先格式化(format) 成了推荐的文件系统,我们用户可以自行重格式化,修改文件系统
就算是SD协会制定了SD标准,成品设备是怎么和SD标准达成一致的呢?
成品设备之所以能读取不同容量的 SD 卡,本质上在于它的软件/固件里包含了多个文件系统的支持(以及对应的 SD 协议驱动),这样才能对应 SDSC、SDHC、SDXC/SDUC 等卡型在出厂时常用的文件系统
我们平时使用成品设备时直接插入SD卡就能读取里面数据,但是对于半成品开发板STM32来说,我们还需要在使用之前先初始化 SDIO 驱动,再装FatFS 驱动
为什么会这样呢?这就是裸机和操作系统的区别了:
操作系统 vs. 裸机(Bare-metal)
相机、手机、电脑都运行着操作系统(OS)
手机常见的有 Android、iOS;电脑是 Windows、macOS、Linux。
一,操作系统里已经集成了
1,SD 卡驱动
2,文件系统模块(FAT/exFAT)
二,自动挂载:插卡后,系统就自动识别、挂载(mount)出一个盘符,你直接读写文件就好。
然而STM32 裸机没有操作系统(除非你另行移植一个 RTOS)
一,出厂出来只有最底层的寄存器、外设,需要我们用 CubeMX 配置 SDIO/SPI、引脚、时钟、DMA实现硬件初始化
二,没有SD 卡驱动,要自己去写并调用 HAL_SD_Init()
三,没有现成的文件系统,需要把 FatFS(或其他文件系统)集成进来
四,手动挂载:调用 f_mount()(FatFS)把底层驱动和文件系统连起来
五,最后 再去用 f_open()、f_read()、f_write() 这类 API 读写文件
相机/手机/电脑 跟 STM32 这类裸机 MCU 在使用 SD 卡时,有两个关键区别
文件系统本身的容量/文件数限制
FAT12/16(SDSC 卡,≤2 GiB)
FAT12 的聚簇数最多 4 096 个,簇大小最小 512 B,所以最大只到 ~2 MiB;
FAT16 的聚簇表项 16 位,最多 65 536 个簇,即使用最大 32 KiB 聚簇,也只能支持到 2 GiB 左右。
FAT32(SDHC 卡,>2 GiB–32 GiB)
FAT32 用 28 位表项,支持最多 ~268 435 456 个簇,配合最小 512 B 聚簇即可覆盖 ~128 TiB,但 Windows 默认只在 32 GiB 以下格式化为 FAT32,主要为了性能和卷标兼容性。
exFAT(SDXC、SDUC 卡,>32 GiB)
exFAT 设计时就面向超大卷(最高可到数百 PB),目录表和簇号都用了更宽的字段,支持更大的文件和更多的文件。
兼容性与规范要求
SD 规范里对不同卡型(SDSC/SDHC/SDXC/SDUC)都有“推荐使用”的文件系统:
SDSC 卡通常用 FAT16;
SDHC 卡上较小容量(≤32 GiB)依然用 FAT32;
SDXC/SDUC 卡(>32 GiB)强制或推荐用 exFAT。
这样做能保证:
主机设备(如相机、智能手机、电脑)在出厂时就能识别、读写,不用额外安装驱动;
跨平台(Windows、macOS、Linux、嵌入式系统)有一致的最低支持。
性能和资源消耗
较小的卡如果用 exFAT,会带来更多的文件系统元数据开销;
大卡若用 FAT32,目录/聚簇表过大,查找和维护速度会急剧下降,且容易产生大量碎片。
专利与授权
exFAT 是微软的专利文件系统,SDXC/SDUC 标准中包含了 exFAT 的授权条款;
对于低容量的 SDSC/SDHC 卡,使用开源的 FAT16/FAT32,无需支付专利费用,更经济。
测试历程
1,测试开始
my_printf(&huart1, "\r\n--- SD卡FATFS文件系统测试开始 ---\r\n");
2,挂载文件系统
挂载是建立硬件(SD卡)与软件(文件系统API)之间的联系
挂载前,系统无法读写SD卡中的任何文件
挂载后,系统可以通过FATFS提供的函数(如f_open、f_read等)访问SD卡中的内容
代码中使用f_mount函数完成挂载操作
if (f_mount(&SDFatFS, SDPath, 1) != FR_OK){
my_printf(&huart1, "SD卡挂载失败,请检查SD卡连接或是否初始化\r\n");return;}
my_printf(&huart1, "SD卡挂载成功\r\n");
函数解析:
FRESULT f_mount(FATFS* fs, const TCHAR* path, BYTE opt)
参数fs:指向要注册/注销的文件系统对象的指针
参数path:驱动器号,最常见的格式是 “0:”、“1:”,后面可以跟一个斜杠 /,比如 “0:/”
如果传入空字符串 “”,则表示使用默认驱动器,一般就是 0:
参数opt: 挂载选项。0 表示延迟挂载 (仅注册工作区,实际的卷初始化在首次访问时进行);1 表示立即挂载 (执行卷初始化并检查卷状态)。
这里的驱动器是什么东西?
是对底层每个存储介质(比如 SD 卡、内部 Flash、USB 盘……)的一个逻辑编号或“盘符”,它不是物理设备,他是物理设备通过diskio.c 里的读写函数注册给 FatFS时分给物理设备的编号
以上两个参数在fatfs.c 和 fatfs.h中有定义
下载调试后串口提示挂载失败:
— SD卡FATFS文件系统测试开始 —
SD卡挂载失败,请检查SD卡连接或是否初始化
检查res值:
if (f_mount(&SDFatFS, SDPath, 1) != FR_OK){my_printf(&huart1,"FR_OK = %d\n", (int)FR_OK);my_printf(&huart1,"f_mount returned: %d\n", (int)res);my_printf(&huart1, "SD卡挂载失败,请检查SD卡连接或是否初始化\r\n");return;}
调试结果:
— SD卡FATFS文件系统测试开始 —
FR_OK = 0
f_mount returned: 134432800
SD卡挂载失败,请检查SD卡连接或是否初始化
开启keil调试:
3,创建测试目录
使用函数 f_mkdir 创建一个新目录
res = f_mkdir("测试目录");if (res == FR_OK){my_printf(&huart1, "创建测试目录成功\r\n");}else if (res == FR_EXIST){my_printf(&huart1, "测试目录已存在\r\n");}else{my_printf(&huart1, "创建目录失败,错误码: %d\r\n", res);}
我们可以ff.c文件中看到FRESULT f_mkdir (
const TCHAR* path /* Pointer to the directory path */
)
这个函数的形参是const TCHAR* path,TCHAR等同于char,代表字符串常量:
单级目录名,比如 “TESTDIR” 或者 “测试目录”。
相对路径,比如 “测试目录/sub1”。
绝对路径,如果你在多驱动器环境下,可以写
“0:/DATA”、“1:/LOGS/2025”,其中前缀的 0:、1: 表示不同的挂载号
4,创建并写入测试文件
这一步涉及两个函数:
FRESULT f_open(FIL* fp, const TCHAR* path, BYTE mode)
这个函数第一个形参是FIL SDFile; 是指向文件对象的指针
形参const TCHAR *path 代表文件名或路径字符串
如果我们传入TestFileName
它会在当前挂载盘的根目录下创建/打开名为 SD_TEST.TXT 的文件。
如果你想放在子目录里,可以传 “测试目录/SD_TEST.TXT”,或者加上驱动号 “0:/SD_TEST.TXT”,前提是你已正确挂载。
形参BYTE mode 用于指定「打开方式」和「访问权限」,FatFs 里定义了一系列宏,你可以通过「按位或」来组合。常用的几个有:
宏名 | 值 | 含义 |
---|---|---|
FA_READ | 0x01 | 以读模式打开文件 |
FA_WRITE | 0x02 | 以写模式打开文件 |
FA_OPEN_EXISTING | 0x00 | 仅当文件已存在时打开,否则失败(默认行为) |
FA_CREATE_NEW | 0x04 | 创建新文件;如果文件已存在,则失败返回 FR_EXIST |
FA_CREATE_ALWAYS | 0x08 | 创建新文件;若已存在则先删除旧文件,再新建一个空文件 |
FA_OPEN_ALWAYS | 0x10 | 如果文件已存在就打开它,否则创建新文件 |
FA_OPEN_APPEND | 0x30 | 同 FA_OPEN_ALWAYS ,但文件指针定位到文件尾以追加写入 |
第二个函数是f_write
FRESULT f_write (FIL* fp, /* [in] 指向已打开文件的文件对象指针 */const void* buff, /* [in] 指向要写入数据缓冲区的指针 */UINT btw, /* [in] 要写入的字节数 */UINT* bw /* [out] 实际写入的字节数 */
);
这个函数有四个参数:
FIL* fp:指向刚才f_open打开的文件对象,文件必须已用 FA_WRITE打开
const void* buff:指向存放要写入内容的缓冲区。可以是字符串、二进制数据、结构体数组
UINT btw :要写入的最大字节数
如果 *bw < btw,说明写入过程中发生了中断或错误。
my_printf(&huart1, "创建并写入测试文件...\r\n");res = f_open(&SDFile, TestFileName, FA_CREATE_ALWAYS | FA_WRITE);if (res == FR_OK){// 写入数据res = f_write(&SDFile, WriteBuffer, strlen(WriteBuffer), &bw);if (res == FR_OK && bw == strlen(WriteBuffer)){my_printf(&huart1, "写入文件成功: %u 字节\r\n", bw);}else{my_printf(&huart1, "写入文件失败,错误码: %d\r\n", res);}// 关闭文件f_close(&SDFile);}else{my_printf(&huart1, "创建文件失败,错误码: %d\r\n", res);}
5,读取测试文件
my_printf(&huart1, "读取测试文件...\r\n");memset(ReadBuffer, 0, sizeof(ReadBuffer));res = f_open(&SDFile, TestFileName, FA_READ);if (res == FR_OK){// 读取数据res = f_read(&SDFile, ReadBuffer, sizeof(ReadBuffer) - 1, &br);if (res == FR_OK){ReadBuffer[br] = '\0'; // 确保字符串结束符my_printf(&huart1, "读取文件成功: %u 字节\r\n", br);my_printf(&huart1, "文件内容: %s\r\n", ReadBuffer);// 验证数据一致性if (strcmp(ReadBuffer, WriteBuffer) == 0){my_printf(&huart1, "数据验证成功: 读写数据一致\r\n");}else{my_printf(&huart1, "数据验证失败: 读写数据不一致\r\n");}}else{my_printf(&huart1, "读取文件失败,错误码: %d\r\n", res);}// 关闭文件f_close(&SDFile);}else{my_printf(&huart1, "打开文件失败,错误码: %d\r\n", res);}