# 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** 进行保护 ![](./img/3.png) * 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; } ``` * 代码运行结果如下 ![](./img/8.png) ##### CMD9 * 命令作用:查看 CSD 寄存器 * 命令组成:不重要 * 参数选择:无参数 * 命令返回:R1,然后返回 16 字节的 CSD 寄存器值 * 注意:在真实返回 CSD 前,需要先等到返回 **0xFE 起始令牌**,接受到 0xFE 后,之后的字节才是 CSD 寄存器的值 ![](./img/12.png) 可以根据 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 ![](./img/10.png) * 示例代码 ```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,表示上电成功,且该卡属于高容量卡。 ![](./img/11.png) * 命令验证:执行完该命令后,执行 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 寄存器 ![](./img/6.png) #### 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 校验位 ![](./img/14.png) * 读操作异常的操作时序: 此时不会返回任何数据,只会返回一个特殊的数据错误令牌 ![](./img/15.png) #### 2)读多个 block * 命令:**CMD18** * 读取多个字节的操作时序,若要停止读取,则发送 **CMD12** 命令即可退出数据传输 ![](./img/16.png) ### 3. 写数据 SPI 模式下,有两种写命令,写单个 block 和写多个 block。 #### 1)写单个 block * 命令:CMD24 * 操作时序 ![](./img/19.png) * 主机发送 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 * 操作时序 ![](./img/23.png) * 先发送 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 * 组成 ![](./img/1.png) ### CSD:卡特定数据寄存器 * 这是 **Version2.0 版本**的 可以从第 127 ~ 126 两位看出自己的 SD 卡属于那种容量和那个版本 * 提供了有关访问存储卡内容的信息 * 128 bit ![](./img/2.png) ### 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 卡的底层驱动实现好,这里给个实例 ![](./img/25.png) 其中标红的都是必须要实现的,其实说到底就三大类:**初始化函数、读函数、写函数** ### 2)文件结构 当我们从官网下载好 FATFS 代码后,其文件结构如下: ![](./img/26.png) * `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` 的最开始的一些宏定义需要注释一些,只需保留如下图的内容即可 ![](./img/27.png) ### 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) { } } ```