# sd_driver
**Repository Path**: lewque/sd_driver
## Basic Information
- **Project Name**: sd_driver
- **Description**: Micro SD + SPI + FATFS + 标准库
- **Primary Language**: Unknown
- **License**: MulanPSL-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 1
- **Created**: 2026-01-01
- **Last Updated**: 2026-01-01
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# Micro SD + SPI + FATFS + 标准库下的数据手册解读
网上寻找,百般无果。定心潜修,解密探究。拨云见日。偶遇风雨,迎难而上。栈溢强敌,耐心排错。坚持不懈,方现光明。
参考资料:
* 官方权威手册:[Physical-Layer-Simplified-SpecificationV6.0.pdf](https://www.taterli.com/wp-content/uploads/2017/05/Physical-Layer-Simplified-SpecificationV6.0.pdf)
* 入门教学:[Lecture 12: SPI and SD cards](https://www.dejazzer.com/ee379/lecture_notes/lec12_sd_card.pdf)
## 一、SD 卡容量分类
| 类型缩写 | 全称 | 中文名称 | 容量范围 | 文件系统 | 发布时间 |
| -------- | -------------------------------- | -------------- | --------------- | ------------- | -------- |
| **SDSC** | Secure Digital Standard Capacity | 标准容量 SD 卡 | **128MB – 2GB** | FAT12 / FAT16 | 1999 年 |
| **SDHC** | Secure Digital High Capacity | 高容量 SD 卡 | **4GB – 32GB** | FAT32 | 2006 年 |
| **SDXC** | Secure Digital eXtended Capacity | 扩展容量 SD 卡 | **64GB – 2TB** | exFAT | 2009 年 |
| **SDUC** | Secure Digital Ultra Capacity | 超大容量 SD 卡 | **2TB – 128TB** | exFAT | 2018 年 |
我们接下来的内容都是基于 SDHC 和 SDXC 的。
## 二、SPI 模式
SD 卡有两种控制方式,一种是 **SD 模式**,也是默认模式。另一种是 **SPI 模式**,需要在初始化时进入该模式才行。接下来的内容都是在 SPI 模式下的相关操作。
## 三、命令和响应
对于任何外设模块,都有其对于的命令和响应,这样才能和外设进行交互与控制。SD 卡也不例外,接下来我们就解析一下其相关命令和对应的响应
### 1. 命令分类
命令分为两大类:**CMD** 和 **ACMD**(Application CMD),对于所有的 ACMD 命令,要想执行,其必须先执行 **APP_CMD(CMD55)**才行。
### 2. 命令
#### 1)命令组成
参考文档第 217 页
所有 SD 存储卡命令长度均为 **6 字节**。命令传输总是从命令码字对应的比特串的**最高位开始**。所有的命令都由 **CRC** 进行保护

* 47 ~ 46:固定为 0 和 1
* 45 ~ 40:**6 bit,命令的序号**,例如 CMD0,则为 000000,一般和 47 ~ 46 共同组成一个 byte
* 39 ~ 8:**32 bit,参数位**
* 7 ~ 1:7位,CRC 校验码
* 0:1 位,结束位
#### 2)命令类别
一个命令可以涉及多个类别,具体的类别有 12 种,从 0 ~ 11:
* 0:basic:基础
* 1:reserved:保留
* 2:block read:块读
* 3:reserved:保留
* 4:block write:块写
* 5:erase:擦除
* 6:write protection:写保护
* 7:lock card:卡锁定
* 8:application specific:应用相关
* 9:IO mode:IO 模式
* 10:switch:交换
* 11:reserved:保留
这里**仅做了解即可**,不同的命令涉及的类别不同。
#### 3)命令发送代码参考
```c
/* 命令构成: 48bit
47~46: 01
45~40: 命令序号: 6bit
39~8: 命令参数: 32bit
7~1: CRC校验码: 7bit
0: 1
SD卡响应数据:40bit
39: 0
若命令被成功传输和处理,只需读取39位是否为0即可
@return
0x: 响应成功
0xFF: 响应失败
*/
uint8_t SD_SendCmd(uint8_t cmd, uint32_t arg, uint8_t crc, uint8_t keep_cs_low) {
uint8_t r;
uint16_t retry = 0;
CS_LOW;
SPI1_SwapByte(cmd | 0x40); // 47和46位是01,加上cmd的序号6位,共8位,因此|=0x40
SPI1_SwapByte((uint8_t)(arg >> 24));
SPI1_SwapByte((uint8_t)(arg >> 16));
SPI1_SwapByte((uint8_t)(arg >> 8));
SPI1_SwapByte((uint8_t)arg);
SPI1_SwapByte(crc); // CMD0 必须用 0x95,其他初始化阶段随便填
if(cmd == SD_CMD_STOP_TRANSMISSION) SPI1_SwapByte(0xFF); // STOP_TRAN 吃掉一个字节
do {
r = SPI1_SwapByte(0xFF);
if((r & 0x80) == 0) { // 最高位为0 → 响应命令
if(!keep_cs_low) CS_HIGH; // 只有不需要后续数据的命令才拉高
return r;
}
Delay_us(100);
retry++;
} while(retry < 1000);
CS_HIGH;
return 0xFF;
}
```
其中 `CS_LOW` 和 `CS_HIGH` 就是置位 CS 信号引脚即可,`SPI1_SwapByte` 就是 SPI 写入/读取一个字节,SD_CMD_xxx 是命令的序号宏定义,参考手册自行定义即可。
#### 4)具体命令
这是核心部分,也是如何与 SD 卡进行命令交互的关键
在 SPI 模式下,部分命令是不被支持的,因此我们只需了解支持的命令即可,总共有 CMD0 ~ CMD63,没有提及的就是不支持的
关于响应,可以参考响应部分了解具体的响应解读
| CMD 序号 | 参数 | 响应 | 简称 | 命令描述 |
| -------- | ---------------------------------------- | ---- | -------------------- | ------------------------------------------------------------ |
| CMD0 | 无 | R1 | GO_IDLE_STATE | 重置SD卡使其进入SPI模式 |
| CMD8 | [31:12]保留 [11:8]供电电压 [7:0]检查模式 | R7 | SEND_IF_COND | 发送SD存储卡接口条件,其中包括主机的供电电压信息,并询问被访问的卡是否可以在供电电压范围内运行。参数保留应置0即可 |
| CMD9 | 无 | R1 | SEND_CSD | 让 SD 卡发送其CSD寄存器内容(16 Bytes) |
| CMD12 | 无 | R1b | STOP_TRANSMISSION | 停止多块读取 |
| CMD13 | 无 | R2 | SEND_STATUS | 在写入block后,让SD发送状态寄存器,检查数据是否真的写入成功 |
| CMD17 | [31:0]起始逻辑扇区号 | R1 | READ_SINGLE_BLOCK | 读取一个 block |
| CMD18 | [31:0]起始逻辑扇区号 | R1 | READ_MULTIPLE_BLOCK | 连续读取多个 block,并在接受到 CMD12 时停止 |
| CMD24 | [31:0]起始逻辑扇区号 | R1 | WRITE_BLOCK | 写入一个 block 大小的数据 |
| CMD25 | [31:0]起始逻辑扇区号 | R1 | WRITE_MULTIPLE_BLOCK | 连续写入多个 block |
| CMD55 | 无 | R1 | APP_CMD | 进入ACMD命令模式 |
| CMD58 | 无 | R3 | READ_OCR | 读取OCR寄存器 |
| CMD59 | [0:0]CRC开关标志 | R1 | CRC_ON_OFF | 开关CRC,1 开,0 关 |
| ACMD序号 | 参数 | 响应 | 简称 | 命令描述 |
| -------- | --------------------------- | ---- | --------------- | ----------------------------------------------------------- |
| ACMD41 | [31]保留 [30]HCS [29:0]保留 | R1 | SD_SEND_OP_COND | 发送高容量支持信息(HCS=1) 以及启动初始化过程,保留为置0即可 |
**所有的命令都有一个 CRC 校验,除了 CMD0(0x95)和 CMD8(0x87) 外,一律使用 0xFF**
##### CMD0
* 命令作用:重置 SD 卡,进入 SPI 模式
* 注意:**初始化阶段的 SPI 时钟要小于 400 KHz**,若使用的是 SPI1,其在 APB2 上,总线时钟是 72MHz,因此**分频 256** 即可,其 **CRC 必须是 0x95**
##### CMD8
* 命令作用:验证 SD 是否能在给定工作电压下正常工作
* 注意:CMD8 命令一些**老卡可能不支持**,SPI 时钟小于 400KHz ,其 **CRC 必须是 0x87**
* 命令组成
* 参数选择:一般选择 **0x1AA** 即可,其中 1 是待验证工作电压。AA 是校验序列,其实随便写都行。
* 命令返回:R7,若是校验正确,则会将我们的参数原样返回。例如我们参数是:0x000001AA,那么会返回 [00 00 01 AA],先高位后低位
* 测试代码:
```c
void SD_Validate_Voltage(void)
{
uint8_t r1;
uint8_t resp[4];
// keep_cs_low = 1,保证能读到后面的 4 字节
r1 = SD_SendCmd(SD_CMD_SEND_IF_COND, 0x000001AA, 0x87, 1);
if(r1 != 0x01) {
// 卡损坏,或不支持CMD8命令
CS_HIGH;
return;
}
printf("CMD8 R1 = [%02X]\r\n", r1);
// 连续读 4 字节回显(这时候 CS 还是低的)
resp[0] = SPI1_SwapByte(0xFF);
resp[1] = SPI1_SwapByte(0xFF);
resp[2] = SPI1_SwapByte(0xFF);
resp[3] = SPI1_SwapByte(0xFF);
// 原样打印出来
printf("CMD8 R7 back view = [%02X %02X %02X %02X]\r\n",
resp[0], resp[1], resp[2], resp[3]);
CS_HIGH;
}
```
* 代码运行结果如下

##### CMD9
* 命令作用:查看 CSD 寄存器
* 命令组成:不重要
* 参数选择:无参数
* 命令返回:R1,然后返回 16 字节的 CSD 寄存器值
* 注意:在真实返回 CSD 前,需要先等到返回 **0xFE 起始令牌**,接受到 0xFE 后,之后的字节才是 CSD 寄存器的值

可以根据 OCR 寄存器的对照表去对比检查一下。我们此处只需关系第 6 个字节的后 4 位,即 9 ,其就是 READ_BL_LEN,因此该 SD 卡的 block 的大小就是:$2^{READ\_BL\_LEN} = 2^9 = 512$
##### CMD17
* 命令作用:读取一个 block
* 命令组成:
* 参数选择:[31~0]扇区地址(一般对于 SDHC 和 SDXC 其 sector 和 block 都是 512 Bytes)
* 命令返回:R1,从 sector 开始,一个 block 长度的数据
可以看出,我们成功读取了第 0 个 sector 的数据,注意最后两个字节的数据:**55 AA**,是一般 SD 卡出厂默认的值。最好**不要对 sector 0 做任何写入操作,特别是最后两个字节 55 AA**。我们可以读取 sector 0 来验证即可。
##### CMD18
* 命令作用:读取连续多个 block
* 命令组成:
* 参数选择:[31~0]扇区地址
* 命令返回:R1,从 sector 开始,连续多个 block 长度的数据
* 注意:当想停止读取时,需要传送 CMD12 命令
这是我们连续读取了 sector 0 和 sector 1
##### CMD24
* 命令作用:写一个 block
* 命令组成:不重要
* 参数选择:[31~0]扇区地址
* 命令返回:R1 + data_response + busy
* 注意:发送 block 数据时,先发送 Start Block Token(**0xFE**),然后收到 data_response(**0x05**),最后进入 busy 状态,收到的数据全是 **0x00**,直到非 0x00 时退出 busy,block 写入结束。而后可以使用 CMD13 进行验证
写入前,扇区 1 的数据全为 0,写入 0 ~ 255,写两次,总共 512 字节。可以看到 CMD13 返回的是 [00 FF],高位字节是 00 表示正确无错误,此时低位字节一般会填入 FF 做默认,我们只需关注高位字节即可
##### CMD25
* 命令作用:连续写入多个 block
* 命令组成:不重要
* 参数选择:起始的扇区号
* 命令返回:R1
* 注意:先发送 CMD25,收到 R1 后,在每次发送 block 前,一定要先发送 Start Block Token:**0xFC**,然后发送 512 Bytes 的 block,再之是 2 Bytes 的 CRC。然后等待 data_response(**0x05**),并等待 busy 结束,然后就可以发送下一个 block。当要结束时,只需发送 Stop Tran Token:**0xFD**,然后等待 busy 结束,一定要延时 **50ms + 80 个 clock** 确保写入稳定
可以看到,最初连续两个扇区的内容都是 0,然后我们连续写入后,再读取发现数据成功写入
##### CMD58
* 命令作用:查看 OCR 寄存器
* 命令组成:不重要
* 参数选择:无参数
* 命令返回:R3,OCR 寄存器值,32 bit

* 示例代码
```c
void SD_Read_OCR(void) {
uint8_t r1;
uint8_t resp[4];
r1 = SD_SendCmd(SD_CMD_SEND_OCR, 0, 0x87, 1);
if(r1 != 0x01) {
// 读取失败
printf("CMD58 R1 = [%02X]\r\n", r1);
CS_HIGH;
return;
}
resp[0] = SPI1_SwapByte(0xFF);
resp[1] = SPI1_SwapByte(0xFF);
resp[2] = SPI1_SwapByte(0xFF);
resp[3] = SPI1_SwapByte(0xFF);
printf("CMD58 R3 back view = [%02X %02X %02X %02X]\r\n",
resp[0], resp[1], resp[2], resp[3]);
CS_HIGH;
}
```
##### CMD59
* 命令作用:CRC 校验开关
* 命令组成:不重要
* 参数选择:[0:0]关闭:0,打开:1
* 命令返回:R1
* 注意:CRC 校验关闭会加快传输速度,但是发送命令,读写数据时的 CRC 还是要发送的
---
##### ACMD41
* 命令作用:将 SD 卡退出空闲状态
* 注意:执行该命令前必须先执行 CMD8,并且该命令是 ACMD 命令,需要每次执行前先执行 CMD55 进入 ACMD 模式才行。该命令要执行需要在循环中执行,直到返回的 R1 = 0x00 即可。SPI 时钟小于 400KHz
* 命令组成:`(0100 0000 0000 0000 0000 0000 0000 0000)b` 即 `4 << 28`,其中 30 位是 `HCS` 标志,表示 SD 卡是否是高容量,这位填写 1 即可
* 参数选择:HCS 位
* 命令返回:R1
ACMD41执行后,可以看到最初返回的是 0x01,即 in idle state 位依旧是 1,第二次返回的是 0x00,表示退出了空闲状态。然后我们执行 CMD58 读取 OCR,可以看到,高 4 位是 C,即 1100,表示上电成功,且该卡属于高容量卡。

* 命令验证:执行完该命令后,执行 CMD58,首先返回的 R1 部分的最低位会从 1 变成 0,因为退出了 idle state。然后返回的 OCR 寄存器的值中,可以看到 31 和 30 位会置 1,其中 31 位是上电成功标志,30 位是 CCS,表示该卡属于:SDHC、SDXC 或 SDUC
### 2. 响应
每个命令发出后都会有对应的响应,响应是多字节并且先传输高位,但是当发生“非命令错误或CRC错误时”,只会响应第一个字节
#### 1)R1
除 SEND_ STATUS 命令(CMD16)外,此响应在每条命令后发送,其一个字节长,最高位恒为 0,其它位是错误指示位
* **在空闲状态(Idle State)**:SD 处于空闲状态,并正在执行初始化过程。
* **擦除复位(Erase Reset)**: 在执行擦除操作前,因接收到一个不属于擦除序列的命令,导致擦除序列被清除。
* **非法命令(Illegal Command)**: 检测到一个非法的命令代码。
* **通信 CRC 错误(Communication CRC Error)**: 上一条命令的 CRC 校验失败。
* **擦除序列错误(Erase Sequence Error)**: 擦除命令的执行顺序出现错误。
* **地址错误(Address Error)**: 命令中使用了未对齐的地址,该地址与数据块长度不匹配。
* **参数错误(Parameter Error)**: 命令的参数(例如地址、块长度)超出了该 SD 所允许的范围。
#### 2)R1b
该响应令牌**与 R1 格式相同**,并可**选择性地附加忙(busy)信号**。 忙信号令牌可以包含任意数量的字节:值为 0 表示 SD 正忙;非零值表示 SD 已准备好接收下一条命令。
#### 3)R2
两个字节,作为 SEND_STATUS 命令的响应
* 第一个字节和 R1 响应相同
* **擦除参数错误(Erase param)**:擦除操作中选择了无效的扇区或组。
* **写保护违规(Write protect violation)**:命令试图向一个被写保护的块执行写入操作。
* **卡内 ECC 校验失败(Card ECC failed)**:卡片内部使用了 ECC(错误校正码)进行纠错,但未能成功纠正数据错误。
* **卡控制器错误(CC error)**:卡片内部控制器发生错误。
* **通用错误(Error)**:在操作过程中发生了通用性错误或未知错误。
* **写保护擦除跳过 / 锁定/解锁命令失败(Write protect erase skip | lock/unlock command failed)**:该状态位具有双重功能:
- 当主机尝试擦除一个写保护的扇区时,该位被置位;
- 或者在执行卡片锁定/解锁操作时,若命令序列错误或密码错误,该位也会被置位。
* **卡片已锁定(Card is locked)**:当用户将卡片锁定时,该位被置位;当卡片被解锁后,该位被清除(复位)。
#### 4)R3
五个字节,作为 READ_OCR 命令的响应,第一个字节和 R1 相同,其余四个字节包含 OCR 寄存器

#### 5)R4 & R5
这些响应格式保留给 I/O 模式:参考 SDIO
#### 6)R7
5 个字节,作为 SEND_IF_COND 命令(CMD8)的响应,第一个字节和 R1 相同,其余四个字节会**回显**工作电压和校验模式
## 四、数据读写
### 1. 前置知识
读写数据会涉及额外的传输位:Start Block Token 和 Stop Tran Token,具体的定义如下:
* 单 Block 的读、写 + 多 Block 的读,数据组成:**[0xFE] + [512字节数据] + [2字节CRC]**
* 读时:我们根据是否读取到 0xFE 来判断是否开始读取数据
* 写时:我们写入 0xFE 来通知 SD 卡,接下来是开始传输要写入的数据
* 多 Block 的读,数据组成:**[0xFC] + [多个 block] + [0xFD]**
### 2. 读数据
在 SPI 模式下可以执行两种读操作,一个是**读单个 block**,一个是**读多个 block**。并且读取的 block 的长度是可以设置的(CMD16),但是对于高容量 SD 即,**SDHC 和 SDXC**,读取的 block 长度强制**固定为 512 Bytes**
对于 block 的长度,我们只需要知道目前大多数 SD 卡的 block 是固定为 512 Bytes 的,具体怎么查看,我们可以读取 CSD 寄存器的 [83 ~ 80] 位即 READ_BL_LEN,具体的命令是 CMD9。因此涉及到 block 长度的,有以下几个方面
* READ_BL_LEN:SD 卡的 block 大小
* CMD16:在标准容量卡中(SDSC),可以设置 block 的大小
* READ_BL_PARTIAL:在 CSD 中紧挨着 READ_BL_LEN 的下一个 bit。仍旧只针对标准容量卡(SDSC),如果其是 1,则 block 的大小可以设置为 1 ~ 512 字节。起始地址也可以是有效地址范围内的任何字节地址,但是前提是每个 block 应该包含在同一个物理扇区内。
我们只需要知道,目前市面上的卡几乎都是 SDHC 或 SDXC,因此完全不需要考虑任何与 block 大小相关的方面,只需要记住,**block 为 512 字节**,访问数据的起始地址必须是 **512字节块的整数倍**。
#### 1)读单个 block
* 命令:**CMD17**
* 正确读时的操作时序:
每个 block 后会后缀一个 16 bit 的 CRC 校验位

* 读操作异常的操作时序:
此时不会返回任何数据,只会返回一个特殊的数据错误令牌

#### 2)读多个 block
* 命令:**CMD18**
* 读取多个字节的操作时序,若要停止读取,则发送 **CMD12** 命令即可退出数据传输

### 3. 写数据
SPI 模式下,有两种写命令,写单个 block 和写多个 block。
#### 1)写单个 block
* 命令:CMD24
* 操作时序

* 主机发送 CMD24,SD 卡返回一个 R1 响应,然后等待主机发送 data block
* 主机发送的 data block 的长度由 CMD16 设置,但是对于 SDHC 和 SDXC 其长度就是 **512 Bytes**。但是此处要注意,data block 必须携带一个 **Start Block(0xFE)**,其长度是 1 Byte。并且 data block 后携带 2 Bytes 的 CRC
* SD 卡正确接受到 data block 后, 先返回一个 data_response 响应,然后会进入编程模式,该 data block 会被编程,此时 SD 卡会向主机发送 **busy tokens**,此时会持续拉低 MISO 线,因此接受的数据都是 0x00,当**读取非 0x00 时即已经退出 busy 状态**
* 当编程结束后,主机需要使用 CMD13 命令来检查编程结果,以确保是否真的写入成功
#### 2)写多个 block
* 命令:CMD25
* 操作时序

* 先发送 CMD25 命令,SD 卡响应,后等待主机发送数据
* 初始发送 Start Block Token 即:**0xFC**,然后接着发送 data block(512 Bytes) + CRC (2Bytes)。之后等待 SD 卡的 data_response(0x05)
* SD 进入编程阶段,此时 SD 卡处于 busy 状态,此状态下即使重置 CS 片选信号也不会终止编程过程。
* 如果 SD 卡在编程阶段完成前,被重新片选了,MISO 会被强行拉低,然后拒绝所有的命令。若使用 CMD0 重置 SD 卡,则会终止所有等待中或活跃的编程操作。这会导致 SD 卡内数据损坏,应该避免此类操作
* 在发送完最后一个 block 后,立刻发送 Stop Tran Token 即:**0xFD**
* 最后的关键,再发送完 Stop Tran Token 后,并且已经等待完 busy 了。此时卡内部的数据很可能没有稳定,如果不进行延时等待,会出现在写完后立即读取验证时会出现读取错误问题。因此**要等到一定的延时 + clock 时钟后再结束写操作**,确保卡内部写操作稳定。推荐:**延时 50ms + 80 个 clock**
## 五、初始化阶段
SD 卡要进行初始化,期间要将 SD 卡切换成 SPI 模式,并且让 SD 退出空闲模式进入上电工作状态。我们可以看官方的初始化流程图如下:
推荐的顺序是:CMD0 => CMD8 => ACMD41 => CMD58(选做)
* CMD0:重置 SD,进入 SPI 模式
* CMD8:验证工作电压
* ACMD41:退出空闲模式,进入工作模式
* CMD58:读取 OCR 寄存器进行验证
**其中 CMD0、CMD8、ACMD41 是必须要执行的且要按序执行**,这也是初始化阶段的三个命令
注意:**初始化阶段 SPI 的 时钟线频率应该小于 400KHz**,如果使用的是 SPI1,其在 APB2 总线,因此时钟频率是 72MHz,此时我们要做 **256 分频**。也就是我们在初始化 SPI 偏上外设时,应该设置**波特率分频为 256**。在**初始化阶段后**,我们推荐将 SPI 时钟频率设置为 **9MHz,也就是 8 分频即可**。当然也可以设置为 4 分频,总之要确保能正常读取数据。经测试全程 256 分频没有任何问题,但设置到 4 分频就无法读取数据了,8 分频可以正常读取,因此和 SD 卡本身有关,总之在初始化后可以把 SPI 时钟频率调高些,**找到适合自己 SD 卡的频率即可**。
## 六、寄存器
### CID:卡识别号寄存器
* 128 bit
* 组成

### CSD:卡特定数据寄存器
* 这是 **Version2.0 版本**的
可以从第 127 ~ 126 两位看出自己的 SD 卡属于那种容量和那个版本
* 提供了有关访问存储卡内容的信息
* 128 bit

### OCR:操作状态寄存器
## 七、FATFS 文件系统移植
测试环境:**STM32F103C8T6 + 16GB Micro SD + SPI 模式**
| GPIO | 功能 |
| ---- | ---- |
| PA4 | CS |
| PA5 | SCK |
| PA6 | MISO |
| PA7 | MOSI |
首先,请将**栈空间大小设置为 2KB**(默认是 1KB),原因是因为:在 FATFS 中,有一个 `f_mount()` 函数,其作用是挂载文件系统,但是该方法内嵌套的子方法较多,很可能会产生栈溢出,最典型的情况就是卡在 `f_mount()` 中的 `mount_volume()`。导致 `f_mount()` 一直无法结束。
### 1)前提
要移植 FATFS 系统,我们首先要先将 SD 卡的底层驱动实现好,这里给个实例

其中标红的都是必须要实现的,其实说到底就三大类:**初始化函数、读函数、写函数**
### 2)文件结构
当我们从官网下载好 FATFS 代码后,其文件结构如下:

* `diskio.c + diskio.h`:**重点修改**,其是 ff.c 要调用的方法接口,具体的实现根据个人的底层驱动不同而不同
* `ffconf.h`:FATFS 文件系统的核心配置,**修改一下宏定义即可**
* `ff.c + ff.h`:FATFS 相关的所以方法,**ff.h 小修一下即可**
* `ffunicode.c`:文件名相关的编码和方法,涉及到长文件名模式和中文文件名支持,**无需修改,使用即可**
### 3)移植过程
#### 1. diskio.c + diskio.h
首先 `diskio.c` 中有方法:
```c
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);
```
这五个方法必须**使用自己的底层驱动来实现**
* `disk_initialize`:其中调用 `SD_Enter_SPI_Mode + SD_Validate_Voltage + SD_Init` 即可
* `disk_status`:无需在意,直接返回 `RES_OK` 即可
* `disk_read`:将 `SD_Read_Single_Block + SD_Read_Multiple_Block` 移植即可
* `disk_write`:将 `SD_Write_Single_Block + SD_Write_Multiple_Block` 移植即可
* `disk_ioctl`:其中参数 cmd 有 5 个是 FATFS 要使用的,分别如下:
```c
#define CTRL_SYNC 0 /* Complete pending write process (needed at FF_FS_READONLY == 0) */
#define GET_SECTOR_COUNT 1 /* Get media size (needed at FF_USE_MKFS == 1) */
#define GET_SECTOR_SIZE 2 /* Get sector size (needed at FF_MAX_SS != FF_MIN_SS) */
#define GET_BLOCK_SIZE 3 /* Get erase block size (needed at FF_USE_MKFS == 1) */
#define CTRL_TRIM 4 /* Inform device that the data on the block of sectors is no longer used (needed at FF_USE_TRIM == 1) */
```
只需要写个 switch 即可,**所以命令都返回 RES_OK**
* CTRL_SYNC:不处理
* GET_SECTOR_COUNT:获取扇区数量,需要在初始化成功后,执行 `SD_Read_CSD` 函数读取 CSD 寄存器,其中 [69:48] 共 22 位记录了 SD 卡的扇区数相关信息,需要对数据做一定的处理,示例代码如下:
```c
uint8_t csd[16];
if (SD_Read_CSD(csd) == 0) {
if ((csd[0] & 0xC0) == 0x40) { // SDHC or SDXC
uint32_t c_size = ((uint32_t)(csd[7] & 0x3F) << 16) |
(csd[8] << 8) | csd[9];
card_capacity_sectors = (uint64_t)(c_size + 1) * 1024;
} else { // SDSC (≤2GB)
uint32_t c_size = ((csd[6] & 0x03) << 10) |
(csd[7] << 2) |
(csd[8] >> 6);
uint32_t c_size_mult = ((csd[9] & 0x03) << 1) |
(csd[10] >> 7);
uint32_t block_len = csd[5] & 0x0F;
uint32_t mult = 1 << (c_size_mult + 2);
card_capacity_sectors = (uint64_t)(c_size + 1) * mult * (1 << block_len) / 512;
}
}
```
* GET_SECTOR_SIZE:给 buff 写 512 即可
* GET_BLOCK_SIZE:给 buff 写 1 即可
* CTRL_TRIM:不处理
在 `diskio.c` 的头部添加:
`diskio.h` 不需要做任何修改
#### 2. ffconf.h
开启长文件名、文件名只支持英文。
这个宏定义**根据自己的具体需要可作调整**,每个宏的具体作用可以搜一下或看注释
```c
/*---------------------------------------------------------------------------/
/ FatFs Functional Configurations
/---------------------------------------------------------------------------*/
#define FFCONF_DEF 86631 /* Revision ID */
/*---------------------------------------------------------------------------/
/ Function Configurations
/---------------------------------------------------------------------------*/
#define FF_FS_READONLY 0
#define FF_FS_MINIMIZE 0
#define FF_USE_FIND 1
#define FF_USE_MKFS 1
#define FF_USE_FASTSEEK 0
#define FF_USE_EXPAND 0
#define FF_USE_CHMOD 0
#define FF_USE_LABEL 0
#define FF_USE_FORWARD 0
#define FF_USE_STRFUNC 1
#define FF_PRINT_LLI 0
#define FF_PRINT_FLOAT 0
#define FF_STRF_ENCODE 0
/*---------------------------------------------------------------------------/
/ Locale and Namespace Configurations
/---------------------------------------------------------------------------*/
#define FF_CODE_PAGE 437
#define FF_USE_LFN 1
#define FF_MAX_LFN 255
#define FF_LFN_UNICODE 0
#define FF_LFN_BUF 255
#define FF_SFN_BUF 12
#define FF_FS_RPATH 0
/*---------------------------------------------------------------------------/
/ Drive/Volume Configurations
/---------------------------------------------------------------------------*/
#define FF_VOLUMES 1
#define FF_STR_VOLUME_ID 0
#define FF_VOLUME_STRS "SD"
#define FF_MULTI_PARTITION 0
#define FF_MIN_SS 512
#define FF_MAX_SS 512
#define FF_LBA64 0
#define FF_MIN_GPT 0x10000000
#define FF_USE_TRIM 0
/*---------------------------------------------------------------------------/
/ System Configurations
/---------------------------------------------------------------------------*/
#define FF_FS_TINY 0
#define FF_FS_EXFAT 1
#define FF_FS_NORTC 1
#define FF_NORTC_MON 11
#define FF_NORTC_MDAY 19
#define FF_NORTC_YEAR 2025
#define FF_FS_NOFSINFO 0
#define FF_FS_LOCK 0
#define FF_FS_REENTRANT 0
#define FF_FS_TIMEOUT 1000
#define FF_SYNC_t HANDLE
```
#### 3. ff.h
`ff.h` 的最开始的一些宏定义需要注释一些,只需保留如下图的内容即可

### 4)驱动编写
接下来只需要编写一下 FATFS 的初始化驱动即可,我们添加 `fatfs.c` 和 `fatfs.h` 即可
```c
#include "fatfs.h"
#include "diskio.h"
#include
#include "mircosd.h"
/* 特别注意,f_mount() 函数内部会嵌套调用多个函数,非常容易导致栈溢出,因此一定将栈大小修改为2KB(startup_stm32f10x_md.s) */
FATFS g_FatFs; // 全局文件系统对象
const MKFS_PARM fmt_opt = {
FM_FAT32, // 文件系统类型:FM_FAT32 或 FM_EXFAT
0, // 对齐(0=自动)
0, // 不指定簇大小(0=自动)
0 // 不指定分区起始扇区
};
/**
* FATFS初始化
* @return 0: 成功, 1: SD卡初始化失败, 2: 格式化失败, 3: 挂载失败
*/
uint8_t FATFS_Init(void) {
FRESULT fr;
fr = f_mount(&g_FatFs, "", 1);
if (fr == FR_OK) {
// printf("FatFs mount success!\r\n");
return 0;
} else {
// 挂载失败,尝试格式化
uint8_t work[FF_MAX_SS];
fr = f_mkfs("", &fmt_opt, work, sizeof(work));
if (fr != FR_OK) {
// printf("Format failed: %d\r\n", fr);
return 2;
} else {
// printf("Format success! Remount...\r\n");
}
// 重新挂载
fr = f_mount(&g_FatFs, "", 1);
if (fr == FR_OK) {
// printf("FatFs mount success after format!\r\n");
return 0;
} else {
// printf("Still failed after format: %d\r\n", fr);
return 1;
}
}
}
```
```c
#ifndef __FATFS_H
#define __FATFS_H
#include "ff.h"
uint8_t FATFS_Init(void);
extern FATFS g_FatFs;
#endif
```
至此我们的移植工作就结束了
### 5)测试
```c
#include "usart.h"
#include "diskio.h"
#include "fatfs.h"
#include "mircosd.h"
int main(void) {
SPI1_Init();
uint8_t r = FATFS_Init();
printf("FATFS_Init: %d\r\n", r);
if (r == 0) {
FIL file;
FRESULT fr;
UINT bw;
fr = f_open(&file, "zpeakj.txt", FA_CREATE_ALWAYS | FA_WRITE);
printf("f_open: %d\r\n", fr);
if (fr != FR_OK) {
} else {
const char *text = "Me Zpeakj!\r\n"
"路漫漫其修远兮,吾将上下而求索!\r\n"
"road long, life short, save your time, find your belief.\r\n";
fr = f_write(&file, text, strlen(text), &bw);
if (fr != FR_OK || bw != strlen(text)) {
printf("f_write failed: %d, written: %d\r\n", fr, bw);
} else {
printf("Write success: %d bytes\r\n", bw);
}
// 4. 关闭文件(必须!否则数据可能不落盘)
f_close(&file);
}
}
while (1) {
}
}
```