深度刨析 SD 卡:从结构原理、卡协议、电路到MicroPython驱动实现和文件系统挂载
1.SD 卡基本知识
1.1 SD 卡简介和分类
SD 卡是一种广泛应用于数码设备的可移除存储设备,具有小型化、低功耗和大容量的特点,它主要用于存储数据、音乐、视频、图像等信息,广泛应用于手机、相机、平板电脑、嵌入式设备等。

根据物理尺寸不同,SD 卡可以分为以下几类:

- 标准 SD 卡:尺寸为 32mm x 24mm x 2.1mm,用于大型设备如相机、录音设备等
- miniSD 卡:尺寸为 21.5mm x 20mm x 1.4mm,较少见,主要应用于早期的手机
- microSD 卡:又称 TF 卡,尺寸为 15mm x 11mm x 1mm,广泛应用于手机、无人机、Raspberry Pi 等小型设备中,它可以通过 SD 卡套转换成 SD 卡
根据容量和协议的不同,SD 卡可分为以下几种类型:

这里需要注意,不同文件系统支持的最大容量环绕应用设备均不相同:

这里,补充两个基本概念:
- 最大容量:指这个文件系统能 “管理的存储设备总空间上限”(比如 FAT12 最多管 32MB 的设备);
- 单个文件最大大小:指一个文件 “最多能存多大”(比如 FAT32 的文件不能超过 4GB,存大电影可能不够);
其中:
- FAT12 和 FAT16:主要用于小容量存储设备,适合早期的应用
- FAT32:广泛应用于 SDHC 卡和一些较大的存储设备,但文件大小限制为 4GB
- exFAT:设计用于闪存驱动器和高容量存储设备,支持更大的文件和容量,适合 SDXC 卡
- NTFS:主要用于 Windows 操作系统,支持大文件和大容量,但不常用于 SD 卡
SD 卡的 “速度等级” 是它最低写入速度的保证(速度够快才能不卡顿),对应不同使用场景,常见主要有以下几种速度等级:
- Class 2:最低写入速度 2MB/s,适合标清视频录制;
- Class 4:最低写入速度 4MB/s,适合高清(HD)视频录制;
- Class 6:最低写入速度 6MB/s,适合全高清(FHD)视频录制;
- Class 10:最低写入速度 10MB/s,适合高清视频和连续拍摄;
- UHS-II:支持更高传输速度,写入速度可达 30MB/s 以上。
1.2 SD 卡基本结构和工作原理
SD 卡的机械结构包括塑料外壳、金属引脚、内存芯片、控制器组成,其外壳通常使用坚固的塑料材料,能够抵御物理冲击和环境因素:

SD 卡像个 “带智能管家的 U 盘”,通过下面各个单元完成工作:
- 存储核心:用 NAND 闪存存数据(类似手机里的存储芯片);
- 智能管家(控制器):
- 接收外部设备的指令(比如 “读 XX 文件”),然后去闪存里找数据;
- 用文件系统(FAT32/exFAT)管理数据(之前讲过的 VFS 就能对接这个);
- 用均衡擦写技术:轮流用闪存的不同区域存数据,避免某一块被频繁写入而提前损坏(像轮流用不同抽屉放东西);
- 自带错误校正:自动检测数据里的小错误并修复,避免读错内容。
SD 卡底部有多个金属引脚,用于数据传输、电源供给和信号控制,标准 SD 卡有 9 个引脚,microSD 则有 8 个:

SD 卡的 9 个引脚可以分为四个数据传输引脚 DAT0~DAT3、命令引脚 CMD、地引脚 VSS 和供电引脚 VDD,并且大部分 SD 卡还有一个写保护 WP(Write Protect)开关在标准 SD 卡中:
在介绍各个引脚作用之前,我们需要知道 SD 卡有两种常用操作模式:
- SD Mode:高速但引脚多、通信复杂,一般用在手机 / 相机等专业设备;
- SPI Mode:引脚少、通信简单,是嵌入式编程(比如用开发板接 SD 卡)的常用模式,新手重点看这个模式即可。
下面是每个引脚在两种模式下的对应功能:


上表中引脚类型的简写含义如下:
-
I:输入(SD 卡从外部设备 “接收” 信号); -
O:输出(SD 卡向外部设备 “发送” 信号); -
I/O:双向(既能收又能发); -
S:电源 / 地(接供电或接地)。SD 卡侧面有个硬件开关,同时内部会检测特定引脚(通常是引脚 8)的电平:
-
当 WP 引脚被拉低(检测到特定电平)时,SD 卡会 “锁死” 写入功能,只能读数据;
-
开关拨回后,才能恢复读写。
除了引脚、控制器和 NAND 闪存外,SD 卡内部还有多个寄存器管理卡的基本信息、状态、配置和操作条件,如下图所示:

SD 卡的内部结构是 “分层协作” 的,各部分功能明确:
- 引脚 + 接口驱动:对应图里的 “引脚” 区域,是 SD 卡和外部设备(比如开发板)的 “连接线接口”,负责传输命令、数据、电源 ;
- SD 卡控制器(Card interface controller)是 SD 卡的 “智能中控”,核心作用是:
- 接收外部设备的指令(比如 “读 XX 文件”“查卡的信息”);
- 读取 / 修改寄存器里的信息;
- 管理 NAND 闪存的读写;
- 执行均衡擦写、错误校正等功能(保护卡的寿命和数据)。
- 寄存器对应图里的 “寄存器” 区域,里面存着 SD 卡的身份、状态、能力等关键信息,控制器会随时读取这些信息,外部设备也能通过指令获取这些内容。
- NAND 闪存(Memory core)是 SD 卡的 “存储本体”,实际的数据(照片、文件)都存在这里 —— 相当于储物箱里的 “格子”,由控制器统一管理读写。
以下是一些关键寄存器及其功能:

外部设备(比如单片机)和 SD 卡的协作流程是:
- 外部设备通过 “引脚” 给 SD 卡控制器发指令(比如 “读取你的身份证号”);
- 控制器从对应的寄存器(比如 CID)里取出信息,通过引脚传回给外部设备;
- 若要读写数据,控制器会根据指令,管理 NAND 闪存的 “格子”(分配空间、存 / 取数据);
- 过程中,控制器会用 “均衡擦写”(轮流用不同格子存数据,避免某格坏得快)、“错误校正”(自动修数据里的小错误)保护 SD 卡和数据。
1.3 SD 卡的工作模式
可以把主机(比如开发板 / 手机)和 SD 卡的交互类比成 “手机连新 U 盘”:
- 卡识别模式:手机和新 U 盘 “互相认识、配对准备” 的过程;
- 数据传输模式:配对完成后,手机往 U 盘存文件 / 从 U 盘读文件的过程。
SD 卡的工作模式主要包括两个阶段:卡识别模式(Card Identification Mode)和数据传输模式(Data Transfer Mode),在卡识别模式下,主机的目标是识别并初始化 SD 卡,使其准备好进入数据传输模式,数据传输模式是真正读写数据的阶段:

流程图里的 “Idle/Ready/Ident” 是 SD 卡的状态(当前所处的阶段),“CMD0/CMD8” 是主机给 SD 卡发的指令,整个流程是:
- 初始状态:空闲(Idle State)
- 当 SD 卡插主机(或上电),主机会发「CMD0(复位命令)」—— 相当于主机说:“你好,重置一下,准备开始沟通!”;
- 此时 SD 卡会进入 “空闲状态”,同时选择操作模式(SPI 模式或 SD 总线模式),等主机下一个指令。
- 检查电压:发 CMD8
- 主机发「CMD8」—— 相当于问:“你支持我这里的电压吗?”;
- 如果 SD 卡不支持这个电压,就会回到空闲状态;支持的话,就继续下一步。
- 询问准备:发 ACMD41
- 主机发「ACMD41」—— 相当于问:“你准备好了吗?”;
- 若 SD 卡准备好,会进入 “准备状态(Ready State)”;若没准备好,会返回 “忙” 的信号,主机等一会儿再问。
- 要 “身份证”:发 CMD2
- 主机发「CMD2」—— 相当于说:“把你的唯一标识(CID 寄存器,像身份证号)给我!”;
- SD 卡进入 “识别状态(Identification State)”,把自己的 CID(制造商、序列号等信息)传给主机。
- 分配 “临时编号”:发 CMD3
- 主机发「CMD3」—— 相当于说:“给你个临时编号(RCA,像快递取件码),后面叫你用这个!”;
- SD 卡进入 “待命状态(Stand-by State)”,拿到这个 RCA(方便主机后续快速找到它)。
- 选中卡:发 CMD7,进入数据传输模式
- 主机发「CMD7」—— 相当于说:“就选你了,准备开始传数据!”;
- 此时 SD 卡从 “卡识别模式” 切换到 “数据传输模式”,可以开始读写文件了。
除此之外,还有一些特别的流程:
- 若 SD 卡是老的 MMC 卡(多媒体卡),会走 “CMD1” 的流程,不用 CMD8/ACMD41;
- 「CMD15」是 “取消选中卡”,相当于主机说 “我不用你了”,卡会回到之前的状态。

1.4 SD 卡的通信协议和命令响应模型
SD 卡的通信协议主要包括 SD 总线协议和 SPI 协议:
- SD 总线协议:使用多个数据线(通常为 4 条数据线)进行并行数据传输,支持多种传输速度等级,如 SD、SDHC、SDXC 等,通过通过发送命令和参数进行数据传输,命令以 CMD 开头;
- SPI 协议:使用 4 根引脚(CLK、MOSI、MISO、CS)进行串行数据传输,命令以单字节格式发送,数据以字节流形式传输。
![image.png]()
1.4.1 SD 总线协议中的命令响应模型
SD 卡通信采用命令响应模式,主机向 SD 卡发送指令来启动特定操作,SD 卡针对接收到的命令进行响应,其中,指令 CMD 和响应 RESPONSE 通过 CMD 线串行传输,而数据 DATA 通过数据线(DAT 线)进行传输,命令与数据传输互不干扰。

指令 CMD 的格式如下所示,包括起始位、传输位、命令内容(命令和地址信息或参数)以及停止位:

SD 总线协议的命令帧为固定 48 位长度,帧结构从高位到低位依次定义:
- 起始位(1bit):固定为
0,标识命令帧开始; - 传输位(1bit):固定为
1,标识该帧为 “主机到 SD 卡” 的命令; - 命令码(6bit):标识命令类型,如 CMD0 对应
0x00、CMD2 对应0x02; - 命令参数(32bit):随命令类型携带的配置信息,如地址、容量参数、状态掩码等;
- CRC 校验位(7bit):对前 41 位数据进行 CRC7 校验,确保命令传输的正确性;
- 停止位(1bit):固定为
1,标识命令帧结束。
SD 卡的命令分为普通命令(CMD) 和应用层命令(ACMD) 两类,执行逻辑存在明确区别:
-
普通命令(CMD):可直接由主机发送,SD 卡接收后无需前置条件即可解析执行;
-
应用层命令(ACMD):属于 SD 卡的扩展功能命令,不能直接发送。主机必须先发送 CMD55,待 SD 卡返回有效响应后,再发送 ACMD 命令,否则 SD 卡将无法识别。
常用普通命令如下:
以下是 SD 卡协议中常见的 ACMD 命令列表:

SD 卡接收到有效命令后,必须通过 CMD 线返回响应帧,响应帧根据功能和长度分为 4 种标准类型,根据内容的不同有四种编码方案,响应长度为 48 或 136 位:

这里,我们总结了 SD 卡的常见响应类型:

需要注意,所有响应帧均包含 CRC 校验位,主机需校验 CRC 无误后,方可判定响应有效。
SD 卡的数据传输基于块传输机制,数据帧格式与总线宽度相关,支持单块传输和多块传输两种模式。

其中:
- 单块传输:主机通过 CMD17(读)/CMD24(写)命令触发,SD 卡仅传输 1 个数据块,传输完成后自动结束,无需额外终止命令;
- 多块传输:主机通过 CMD18(读)/CMD25(写)命令触发,SD 卡连续传输多个数据块,直到主机发送 CMD12(停止传输命令) 后,才终止数据传输。该模式适用于大文件的连续读写,可提升传输效率。
数据帧的传输分为普通数据传输和宽数据传输两种方式,对应不同的总线宽度:
- 普通数据传输:采用 1 线总线(仅 DAT0),数据以字节为单位串行传输,每个字节包含 8 位数据位,搭配起始位、停止位和 CRC16 校验位,适用于低速传输场景;
- 宽数据传输:采用 4 线总线(DAT0~DAT3),将 1 字节(8 位)数据拆分为 4 组,每组 2 位,通过 4 条数据线并行传输,传输速率为 1 线模式的 4 倍,适用于高速传输场景。

1.4.2 SPI 协议中的命令响应模型
与 SD 总线协议相似,SPI 协议也是由命令、响应和数据块标记组成,所有主机与卡之间的通信都由主机(主控设备)控制,主机通过将 CS 信号拉低启动每个传输事务,SPI 的数据块可以大到一个写入块,也可以小到一个字节,对于 SDHC 和 SDXC 卡,块长度固定为 512 字节。需要注意的是,写保护命令(CMD28、CMD29 和 CMD30)在 SPI 模式下不支持。

SPI 模式下,卡片初始化流程如下:
- 上电与模式选择:上电后 SD 卡进入
Idle State(空闲状态),主机发送CMD0(复位命令):- 若 CS 信号拉低(
CS Asserted),SD 卡进入 SPI Operation Mode; - 否则进入 SD 总线模式。
- 若 CS 信号拉低(
- 电压兼容性校验:遵循 2.00 版规范的主机需发送
CMD8:- 若 SD 卡返回 “Non supported voltage range”(电压不支持),主机停止访问;
- 若电压支持,发送
CMD58读取 OCR 寄存器,获取 SD 卡支持的电压范围。
- SD 卡初始化:主机发送
ACMD41(需先发送 CMD55):- 若 SD 卡返回 “busy”(图中 “card is busy”),表示内部初始化中,主机需等待直至卡就绪;
- 初始化完成后,主机可通过
CMD58再次读取 OCR,确认卡容量信息(CCS 标识)。
- 有效性校验:若 SD 卡未响应
CMD8/ACMD41,则判定为 “Not SD Memory Card”,主机停止操作。
在 SPI 模式下,SD 卡支持单块读取(Single Block Read)和多块读取(Multiple Block Read)操作,分别对应 SD 卡协议中的 CMD17 和 CMD18 命令:

时序流程为:主机(DataIn)→ 发送 CMD17 命令 → SD 卡(DataOut)→ 返回 R1 响应 → SD 卡发送数据块 + CRC → 事务结束。

时序流程为:主机(DataIn)→ 发送 CMD18 命令 → SD 卡(DataOut)→ 返回 R1 响应 → SD 卡连续发送多个数据块 + CRC → 主机发送 CMD12(停止命令)→ SD 卡返回 R1 响应 → 事务结束。
同样的,SD 卡支持单块写入(Single Block Write)和多块写入(Multiple Block Write)命令,分别对应 SD 卡协议中的 CMD24 和 CMD25 命令:

主机(DataIn)→ 发送 CMD24 命令 → SD 卡(DataOut)→ 返回 R1 响应 → 主机发送起始块令牌 + 数据块 → SD 卡返回数据响应 + busy 信号(表示正在写入)→ 事务结束。

主机(DataIn)→ 发送 CMD25 命令 → SD 卡(DataOut)→ 返回 R1 响应 → 主机连续发送起始块令牌 + 多个数据块 → 主机发送停止令牌 → SD 卡返回 busy 信号 → 事务结束。
在 SPI 模式下,SD 卡的命令、响应和数据的格式与标准 SD 模式有所不同,每个命令为 6 字节,从 01(起始位)开始,接着是命令号(6 位),然后是 32 位的参数,最后是 7 位的 CRC 校验和 1 位停止位:


具体命令功能和参数可以查看 SD 卡规范中的 Table 7-3 : Commands and Arguments:


应用命令 ACMD 的功能和参数可以查看规范中的 Table 7-4 : Application Specific Commands used/reserved by SD Memory Card - SPI Mode:

响应 RESPONSE 通常为 1 字节,称为 R1 响应,在某些情况下,可能需要 2 字节或更多的响应。
R1 响应的结构如下图所示,包括空闲状态指示位、擦除复位指示位、非法命令指示位等:

R2 响应由 2 个字节组成,第一个字节为 R1,第二个字节包含更多错误标志和一些标志:

R3 响应用于 OCR 寄存器读取,包含 R1 响应和 OCR 数据:

还有一个 R7 响应,是针对 SEND_IF_COND 命令(CMD8),为 5 字节长,该响应的第一个字节与 R1 类型响应的结构相同,用于指示命令的状态和错误信息,其余的四个字节包含卡的工作电压信息和检查模式的回显:

不同响应对比如下:

数据令牌是 SD 卡读写操作中承载实际数据的单元,其通用结构遵循 “起始标志 + 数据 + 校验和” 的格式,同时需遵守固定的传输规则:
-
组成部分:
- 起始标志(1 字节):标识数据块的开始(单块 / 多块对应不同值);
- 数据段(2~513 字节):实际存储的用户数据(SDHC/SDXC 卡固定为 512 字节);
- 校验和(2 字节):16 位 CRC16 校验值,用于验证数据传输的正确性。
-
传输规则:
- 字节传输顺序为 MSB 优先(高位字节先传输);
- 数据长度根据卡类型可调整(普通 SD 卡为 1~512 字节,SDHC/SDXC 固定为 512 字节)。
根据读写操作类型(单块 / 多块),数据令牌的起始标志会使用不同值,对应不同的传输场景:
![image.png]()

当进行多个数据令牌写入和读取时,首先使用 0xFC 标志着数据令牌的开始,并在数据传输结束时使用 0xFD:

数据传输完成后,SD 卡会通过响应令牌 / 错误令牌反馈结果:

下面是数据响应令牌:

其中状态位定义如下:
- '010':数据被接受
- '101':因为 CRC 错误导致数据被拒绝
- '110':因为写入错误导致数据被拒绝
在多块写入操作中,如果发生任何错误(CRC 错误或写入错误),主机应使用 CMD12 停止数据传输;如果发生写入错误(响应为‘110’),主机可以发送 CMD13 (SEND_STATUS)来获取写入问题的原因,或者通过 ACMD22 找出成功写入的块数量。
如果读操作失败且 SD 卡无法提供所需数据,卡会发送一个数据错误令牌,长度为 1 字节,其格式如下:

其中,4 个最低有效位(LSB)与 R2 响应格式中的错误位相同,表示不同的错误状态。
1.5 SD 卡的存储结构简介
通过外部通信接口(SD 总线或 SPI 总线),我们可以访问 SD 卡的存储单元:

SD 卡的存储结构分为内部硬件架构与逻辑 / 物理存储单元两部分:
- SD 卡的内部硬件架构:由单芯片控制器与 Flash 存储模块构成,二者协同实现数据的通信与存储:
- 单芯片控制器(SanDisk Single Chip Controller):是 SD 卡的 “控制中枢”
- 对外:通过 “SD Bus/SPI Bus Interface” 与外部主机(如开发板)建立通信;
- 对内:通过 “Control” 信号管理 Flash 存储模块的读写逻辑,同时通过 “Data In/Out” 与 Flash 模块传输数据。
- Flash 存储模块(Flash Modules):是 SD 卡的 “实际存储载体”,所有用户数据均存储于此,其读写操作由控制器统一调度。
- 单芯片控制器(SanDisk Single Chip Controller):是 SD 卡的 “控制中枢”
- 存储的核心单元:SD 卡的存储单元分为逻辑可寻址单元(Sector)与物理存储单元(Block),二者的定义与关系如下:
- 扇区(Sector)
- 是 SD 卡的最小可寻址单位(主机访问 SD 卡时,只能以 Sector 为最小操作粒度);
- 大小通常为 512 字节;
- 每个 Sector 包含多个 Block(Block 是物理存储的基本单元)。
- 块(Block)
- 是 SD 卡向 Flash 存储模块写入数据的基本物理单元;
- 大小通常为 512 字节或 4KB;
- 实际数据写入时,控制器会将数据按 Block 组织后,再写入 Flash 模块。
- 扇区(Sector)
- 写保护与专用区域:核心包括 “写保护组” 与 “保护区域”:
- 写保护组(WP Group)
- 是 SD 卡实现写保护功能的逻辑区域,以扇区为基本单元;
- 当 SD 卡处于写保护状态时,WP Group 内的所有扇区会被禁止写入 / 修改(如右图中的 WP Group 0、WP Group 1 等)。
- 保护区域(Protected Area)
- 是用于版权保护的专用存储区域,同样以扇区为单元;
- 该区域的访问权限受 SD 卡协议限制,非授权主机无法修改其中的数据。
- 写保护组(WP Group)
- 内部缓冲区(Buffer):SD 卡内部集成了临时数据缓冲区(Buffer),其核心作用是:
- 暂存待读写的数据,减少对 Flash 存储模块的直接读写操作(降低 Flash 的擦写损耗);
- 批量调度数据传输,提升整体读写效率(如主机连续读写数据时,Buffer 可暂存数据后统一与 Flash 交互)。
2.自定义 SD 卡读写类的实现
在以下代码中,我们实现了 SD 卡的初始化、命令发送、数据读取等功能,通过 SPI 通信接口与 SD 卡进行交互,确保卡片能够正常工作。
示例代码如下:
# Python env : MicroPython v1.23.0
# -*- coding: utf-8 -*-
# @Time : 2024/9/30 下午12:11
# @Author : 李清水
# @File : sdcard.py
# @Description : 自定义用于SD卡读写的SDCard类
# 参考代码:https://github.com/micropython/micropython-lib/blob/master/micropython/drivers/storage/sdcard/sdcard.py#L291
# ======================================== 导入相关模块 ========================================
# 导入MicroPython相关模块
from micropython import const
# 导入时间相关的模块
import time
# 导入硬件相关模块
from machine import SPI, Pin
# ======================================== 全局变量 ============================================
# 定义SD卡相关常量
CMD_TIMEOUT = const(100) # 命令超时时间常量
R1_IDLE_STATE = const(1 << 0) # R1响应状态:空闲状态
R1_ILLEGAL_COMMAND = const(1 << 2) # R1响应状态:非法命令
TOKEN_CMD25 = const(0xFC) # 数据传输命令令牌
TOKEN_STOP_TRAN = const(0xFD) # 停止传输令牌
TOKEN_DATA = const(0xFE) # 数据令牌
# ======================================== 功能函数 ============================================
# ======================================== 自定义类 ============================================
# 自定义SD卡操作类
class SDCard:
"""
SDCard类,用于通过SPI总线操作SD/MMC存储卡。
该类封装了SD卡底层通信协议,支持标准容量卡(SDSC)和高容量卡(SDHC/SDXC)的初始化、
数据块读写和擦除操作。实现了完整的SPI模式协议栈,包括卡识别模式和数据传输模式。
Attributes:
spi (SPI): SPI接口实例,用于与SD卡通信
cs (Pin): 片选引脚实例,用于选择SD卡
sectors (int): 卡片的总扇区数(只读)
cdv (int): 块寻址因子(1表示块寻址,512表示字节寻址)
cmdbuf (bytearray): 命令缓冲区(6字节)
dummybuf (bytearray): 数据缓冲区(512字节)
tokenbuf (bytearray): 令牌缓冲区(1字节)
Methods:
__init__(self, spi: machine.SPI, cs: machine.Pin, baudrate: int = 1320000) -> None:
初始化SD卡控制器
init_spi(self, baudrate: int) -> None:
配置SPI接口参数
init_card(self, baudrate: int) -> None:
执行完整初始化流程
cmd(self, cmd: int, arg: int, crc: int, final: int = 0, release: bool = True, skip1: bool = False) -> int:
发送SD卡命令并获取响应
readinto(self, buf: bytearray) -> None:
读取数据块到缓冲区
write(self, token: int, buf: bytearray) -> bool:
写入数据块到卡片
write_token(self, token: int) -> None:
发送控制令牌
erase_block(self, block_number: int) -> None:
擦除指定数据块
init_card_v1(self) -> None:
初始化标准容量SD卡
init_card_v2(self) -> None:
初始化高容量SD卡
Constants:
CMD_TIMEOUT: 命令超时重试次数(100次)
R1_IDLE_STATE: 卡片空闲状态标志(0x01)
R1_ILLEGAL_COMMAND: 非法命令标志(0x04)
TOKEN_CMD25: 多块写入开始令牌(0xFC)
TOKEN_STOP_TRAN: 停止传输令牌(0xFD)
TOKEN_DATA: 单块数据令牌(0xFE)
"""
def __init__(self, spi: SPI, cs: Pin, baudrate: int = 1320000) -> None:
"""
初始化SD卡控制器。
Args:
spi (machine.SPI): 配置好的SPI接口对象
cs (machine.Pin): 片选引脚对象
baudrate (int, optional): SPI波特率,默认为1320000
Returns:
None
Raises:
OSError: 如果初始化过程中出现硬件错误
ValueError: 如果波特率超出有效范围
"""
# 存储SPI接口
self.spi = spi
# 存储CS引脚
self.cs = cs
# 创建命令缓冲区,长度为6个字节
self.cmdbuf = bytearray(6)
# 创建虚拟数据缓冲区,长度为512个字节
self.dummybuf = bytearray(512)
# 创建令牌缓冲区,长度为1个字节
self.tokenbuf = bytearray(1)
# 初始化虚拟数据缓冲区为全FF
for i in range(512):
self.dummybuf[i] = 0xFF
# 创建虚拟数据缓冲区的内存视图
self.dummybuf_memoryview = memoryview(self.dummybuf)
# 初始化SD卡
self.init_card(baudrate)
def init_spi(self, baudrate: int) -> None:
"""
配置SPI接口参数。
Args:
baudrate (int): 目标通信速率(Hz)
Returns:
None
Raises:
ValueError: 如果波特率超出硬件支持范围
"""
# 初始化SPI
self.spi.init(baudrate=baudrate, phase=0, polarity=0)
def init_card(self, baudrate: int) -> None:
"""
执行完整的SD卡初始化流程。
Args:
baudrate (int): 目标数据传输波特率
Returns:
None
Raises:
OSError: 如果出现以下情况:
- 卡片无响应
- 版本识别失败
- CSD格式不支持
- 块大小设置失败
"""
# 初始化CS引脚,设置CS引脚为输出高电平
self.cs.init(self.cs.OUT, value=1)
# 初始化SPI总线;使用低数据速率进行初始化
self.init_spi(100000)
# 发送命令前的空闲周期,保持CS高
for i in range(16):
# 发送16个空字节
self.spi.write(b"\xff")
# CMD0: 初始化卡片;应返回_IDLE_STATE(允许5次尝试)
for _ in range(5):
if self.cmd(0, 0, 0x95) == R1_IDLE_STATE:
break
else:
# 如果未找到SD卡,则引发异常
raise OSError("no SD card")
# CMD8: 确定卡片版本
r = self.cmd(8, 0x01AA, 0x87, 4)
if r == R1_IDLE_STATE:
# 初始化v2版本的卡片
self.init_card_v2()
elif r == (R1_IDLE_STATE | R1_ILLEGAL_COMMAND):
# 初始化v1版本的卡片
self.init_card_v1()
else:
# 无法确定SD卡版本
raise OSError("couldn't determine SD card version")
# 获取扇区数量
# CMD9: 响应R2(R1字节 + 16字节块读取)
if self.cmd(9, 0, 0, 0, False) != 0:
# 如果没有响应,则引发异常
raise OSError("no response from SD card")
# 创建CSD缓冲区
csd = bytearray(16)
# 读取CSD数据
self.readinto(csd)
# CSD版本2.0
if csd[0] & 0xC0 == 0x40:
# 计算扇区数量
self.sectors = ((csd[8] << 8 | csd[9]) + 1) * 1024
# CSD版本1.0(旧,<=2GB)
elif csd[0] & 0xC0 == 0x00:
# 获取C_SIZE
c_size = (csd[6] & 0b11) << 10 | csd[7] << 2 | csd[8] >> 6
# 获取C_SIZE_MULT
c_size_mult = (csd[9] & 0b11) << 1 | csd[10] >> 7
# 获取读取块长度
read_bl_len = csd[5] & 0b1111
# 计算容量
capacity = (c_size + 1) * (2 ** (c_size_mult + 2)) * (2**read_bl_len)
# 计算扇区数量
self.sectors = capacity // 512
else:
# 不支持的CSD格式
raise OSError("SD card CSD format not supported")
# CMD16: 设置块长度为512个字节
if self.cmd(16, 512, 0) != 0:
# 无法设置块大小
raise OSError("can't set 512 block size")
# 现在设置为高数据速率
self.init_spi(baudrate)
def init_card_v1(self) -> None:
"""
初始化标准容量SD卡(SDSC)。
Args:
None
Returns:
None
Raises:
OSError: 如果初始化超时(100次尝试)
"""
for i in range(CMD_TIMEOUT):
time.sleep_ms(50)
# 发送命令55
self.cmd(55, 0, 0)
# 尝试初始化卡片
# SDSC卡,使用字节寻址方式进行读/写/擦除命令
if self.cmd(41, 0, 0) == 0:
# 设置块大小为512个字节
self.cdv = 512
# 成功返回
return
# 超时引发异常
raise OSError("timeout waiting for v1 card")
def init_card_v2(self) -> None:
"""
初始化高容量SD卡(SDHC/SDXC)。
Args:
None
Returns:
None
Raises:
OSError: 如果初始化超时(100次尝试)
"""
for i in range(CMD_TIMEOUT):
# 等待50毫秒
time.sleep_ms(50)
# 发送命令58以获取OCR
self.cmd(58, 0, 0, 4)
# 发送命令55以准备初始化
self.cmd(55, 0, 0)
# 尝试初始化SDHC/SDXC卡
if self.cmd(41, 0x40000000, 0) == 0:
# 4个字节响应,负值表示保留首字节
self.cmd(58, 0, 0, -4)
# 获取OCR的首字节
ocr = self.tokenbuf[0]
# SDSC卡,使用字节寻址方式
if not ocr & 0x40:
# 设置块大小为512个字节
self.cdv = 512
else:
# SDHC/SDXC卡,使用块寻址方式
# 设置块大小为1个字节
self.cdv = 1
# 成功返回
return
# 超时引发异常
raise OSError("timeout waiting for v2 card")
def cmd(self, cmd: int, arg: int, crc: int, final: int = 0, release: bool = True, skip1: bool = False) -> int:
"""
发送SD卡命令并获取响应。
Args:
cmd (int): 命令编号(0-63)
arg (int): 32位命令参数
crc (int): CRC校验值
final (int, optional): 额外读取字节数,默认为0
release (bool, optional): 是否释放CS,默认为True
skip1 (bool, optional): 是否跳过首字节,默认为False
Returns:
int: 响应状态:
- 0x00: 正常
- 0x01: 空闲状态
- -1: 超时
Raises:
OSError: 如果SPI通信失败
"""
# 拉低CS引脚,开始通信
self.cs(0)
# 创建并发送命令
buf = self.cmdbuf
# 设置命令头
buf[0] = 0x40 | cmd
# 设置命令参数(高字节)
buf[1] = arg >> 24
# 设置命令参数(次高字节)
buf[2] = arg >> 16
# 设置命令参数(次低字节)
buf[3] = arg >> 8
# 设置命令参数(低字节)
buf[4] = arg
# 设置CRC校验
buf[5] = crc
# 通过SPI发送命令
self.spi.write(buf)
# 如果需要跳过第一次响应
if skip1:
# 读取一个字节,丢弃
self.spi.readinto(self.tokenbuf, 0xFF)
# 等待响应 (response[7] == 0)
for i in range(CMD_TIMEOUT):
# 读取响应字节
self.spi.readinto(self.tokenbuf, 0xFF)
# 获取响应
response = self.tokenbuf[0]
# 检查响应是否有效
if not (response & 0x80):
# 处理大端整数的情况
# 如果final为负
if final < 0:
# 读取并丢弃一个字节
self.spi.readinto(self.tokenbuf, 0xFF)
# 更新final值
final = -1 - final
# 读取额外的字节
for j in range(final):
# 发送空字节以保持时序
self.spi.write(b"\xff")
# 如果需要释放CS信号
if release:
# 拉高CS引脚
self.cs(1)
# 发送空字节
self.spi.write(b"\xff")
# 返回有效响应
return response
# 超时处理
# 拉高CS引脚
self.cs(1)
# 发送空字节
self.spi.write(b"\xff")
# 返回-1表示超时
return -1
def readinto(self, buf: bytearray) -> None:
"""
读取数据块到缓冲区。
Args:
buf (bytearray): 目标缓冲区(必须512字节)
Returns:
None
Raises:
ValueError: 如果缓冲区长度错误
OSError: 如果读取超时或校验失败
"""
# 拉低CS引脚,开始通信
self.cs(0)
# 读取直到开始字节 (0xff)
for i in range(CMD_TIMEOUT):
# 读取一个字节
self.spi.readinto(self.tokenbuf, 0xFF)
# 检查是否为数据开始标志
if self.tokenbuf[0] == TOKEN_DATA:
# 找到开始字节,退出循环
break
# 等待1毫秒
time.sleep_ms(1)
else:
# 拉高CS引脚
self.cs(1)
# 抛出超时错误
raise OSError("timeout waiting for response")
# 读取数据,创建内存视图
mv = self.dummybuf_memoryview
# 如果缓冲区长度不匹配
if len(buf) != len(mv):
# 切片到缓冲区长度
mv = mv[: len(buf)]
# 读取数据到buf
self.spi.write_readinto(mv, buf)
# 发送空字节
self.spi.write(b"\xff")
self.spi.write(b"\xff")
# 拉高CS引脚
self.cs(1)
# 发送空字节
self.spi.write(b"\xff")
def write(self, token: int, buf: bytearray) -> bool:
"""
写入数据块到卡片。
Args:
token (int): 写入令牌(0xFC/0xFD/0xFE)
buf (bytearray): 源数据(必须512字节)
Returns:
bool: 写入结果:
- True: 成功
- False: 失败
Raises:
ValueError: 如果数据长度错误
OSError: 如果卡片响应异常
"""
# 拉低CS引脚,开始通信
self.cs(0)
# 发送: 块开始标记,数据,校验和
self.spi.read(1, token)
self.spi.write(buf)
self.spi.write(b"\xff")
self.spi.write(b"\xff")
# 检查响应是否为成功标志
if (self.spi.read(1, 0xFF)[0] & 0x1F) != 0x05:
# 拉高CS引脚
self.cs(1)
# 发送空字节
self.spi.write(b"\xff")
# 返回,写入失败
return
# 等待写入完成
while self.spi.read(1, 0xFF)[0] == 0:
# 等待直到写入完成
pass
# 拉高CS引脚
self.cs(1)
# 发送空字节
self.spi.write(b"\xff")
def write_token(self, token: int) -> None:
"""
发送控制令牌。
Args:
token (int): 控制令牌(0xFC/0xFD/0xFE)
Returns:
None
Raises:
OSError: 如果卡片未响应
"""
# 拉低CS引脚,开始通信
self.cs(0)
# 发送标记
self.spi.read(1, token)
# 发送空字节
self.spi.write(b"\xff")
# 等待写入完成
while self.spi.read(1, 0xFF)[0] == 0x00:
# 等待直到写入完成
pass
# 拉高CS引脚
self.cs(1)
# 发送空字节
self.spi.write(b"\xff")
def erase_block(self, block_number: int) -> None:
"""
擦除指定数据块。
Args:
block_number (int): 块编号(LBA格式)
Returns:
None
Raises:
ValueError: 如果块号超出范围
OSError: 如果擦除命令失败
"""
# 拉低CS引脚,开始通信
self.cs(0)
# 发送擦除命令 CMD32 (erase start) 和 CMD33 (erase end)
# 开始擦除
self.cmd(32, block_number * 512, 0)
# 结束擦除
self.cmd(33, block_number * 512, 0)
# 发送擦除命令 CMD38 (erase)
self.cmd(38, 0, 0)
# 等待擦除完成
while True:
# 查询状态
response = self.cmd(13, 0, 0)
# 擦除完成
if response == 0:
break
# 拉高CS引脚
self.cs(1)
# 发送空字节
self.spi.write(b"\xff")
# ======================================== 初始化配置 ==========================================
# ======================================== 主程序 ============================================

2.1 初始化方法
在自定义的 SDCard 类中,首先进行初始化操作,设置 SPI 通信和 CS 引脚,创建多个缓冲区以支持后续操作:
def __init__(self, spi: SPI, cs: Pin, baudrate: int = 1320000) -> None:
"""
初始化SD卡控制器。
Args:
spi (machine.SPI): 配置好的SPI接口对象
cs (machine.Pin): 片选引脚对象
baudrate (int, optional): SPI波特率,默认为1320000
Returns:
None
Raises:
OSError: 如果初始化过程中出现硬件错误
ValueError: 如果波特率超出有效范围
"""
# 存储SPI接口
self.spi = spi
# 存储CS引脚
self.cs = cs
# 创建命令缓冲区,长度为6个字节
self.cmdbuf = bytearray(6)
# 创建虚拟数据缓冲区,长度为512个字节
self.dummybuf = bytearray(512)
# 创建令牌缓冲区,长度为1个字节
self.tokenbuf = bytearray(1)
# 初始化虚拟数据缓冲区为全FF
for i in range(512):
self.dummybuf[i] = 0xFF
# 创建虚拟数据缓冲区的内存视图
self.dummybuf_memoryview = memoryview(self.dummybuf)
# 初始化SD卡
self.init_card(baudrate)
具体工作流程如下所示:

真正进行初始化 SD 卡的方法是 init_card 方法,该函数负责检测、初始化 SD 卡,并判断卡片类型(SDSC 或 SDHC/SDXC):

首先将片选引脚 CS 设为高电平(1),表示未选择 SD 卡:
def init_card(self, baudrate: int) -> None:
"""
执行完整的SD卡初始化流程。
Args:
baudrate (int): 目标数据传输波特率
Returns:
None
Raises:
OSError: 如果出现以下情况:
- 卡片无响应
- 版本识别失败
- CSD格式不支持
- 块大小设置失败
"""
# 初始化CS引脚,设置CS引脚为输出高电平
self.cs.init(self.cs.OUT, value=1)
接着低速率(100kHz)初始化 SPI,因为 SD 卡在上电后需要低速通信:
# 初始化SPI总线;使用低数据速率进行初始化
self.init_spi(100000)
然后发送 16 字节 0xFF,让 SD 卡完成内部上电并进入 SPI 通信模式:
# 发送命令前的空闲周期,保持CS高
for i in range(16):
# 发送16个空字节
self.spi.write(b"\xff")
然后循环发送 5 次 CMD0 进入 SPI 模式,如果没有获得期望响应则抛出 OSError("no SD card")。
接着发送 CMD8 检查 SD 版本:
# CMD8: 确定卡片版本
r = self.cmd(8, 0x01AA, 0x87, 4)
if r == R1_IDLE_STATE:
# 初始化v2版本的卡片
self.init_card_v2()
elif r == (R1_IDLE_STATE | R1_ILLEGAL_COMMAND):
# 初始化v1版本的卡片
self.init_card_v1()
else:
# 无法确定SD卡版本
raise OSError("couldn't determine SD card version")
- 如果返回
R1_IDLE_STATE(0x01)则为新版 SD 卡,调用self.init_card_v2(); - 如果返回
R1_ILLEGAL_COMMAND(0x04)则为旧版 SD 卡,调用self.init_card_v1(); - 如果二者都不是,抛出异常
"couldn't determine SD card version"。
新版 SD 卡(≥2GB)采用块寻址模式或字节寻址模式,init_card_v2() 进行如下操作:

def init_card_v2(self) -> None:
"""
初始化高容量SD卡(SDHC/SDXC)。
Args:
None
Returns:
None
Raises:
OSError: 如果初始化超时(100次尝试)
"""
for i in range(CMD_TIMEOUT):
# 等待50毫秒
time.sleep_ms(50)
# 发送命令58以获取OCR
self.cmd(58, 0, 0, 4)
# 发送命令55以准备初始化
self.cmd(55, 0, 0)
# 尝试初始化SDHC/SDXC卡
if self.cmd(41, 0x40000000, 0) == 0:
# 4个字节响应,负值表示保留首字节
self.cmd(58, 0, 0, -4)
# 获取OCR的首字节
ocr = self.tokenbuf[0]
# SDSC卡,使用字节寻址方式
if not ocr & 0x40:
# 设置块大小为512个字节
self.cdv = 512
else:
# SDHC/SDXC卡,使用块寻址方式
# 设置块大小为1个字节
self.cdv = 1
# 成功返回
return
# 超时引发异常
raise OSError("timeout waiting for v2 card")
- 首先发送
CMD58读取 OCR 寄存器(确定是否是 SDHC/SDXC); - 接着发送
CMD55 + CMD41指令进行 SDHC/SDXC 初始化; - 如果确定是 SDSC 卡(字节寻址),设置块寻址因子
cdv = 512; - 如果确定是 SDHC/SDXC 卡(块寻址),设置块寻址因子
cdv = 1。
旧版 SD 卡(≤2GB)均采用字节寻址模式,发送 CMD55 + CMD41 循环等待 SD 卡完成初始化,然后设置块寻址因子 cdv = 1:

def init_card_v1(self) -> None:
"""
初始化标准容量SD卡(SDSC)。
Args:
None
Returns:
None
Raises:
OSError: 如果初始化超时(100次尝试)
"""
for i in range(CMD_TIMEOUT):
time.sleep_ms(50)
# 发送命令55
self.cmd(55, 0, 0)
# 尝试初始化卡片
# SDSC卡,使用字节寻址方式进行读/写/擦除命令
if self.cmd(41, 0, 0) == 0:
# 设置块大小为512个字节
self.cdv = 512
# 成功返回
return
# 超时引发异常
raise OSError("timeout waiting for v1 card")
接着 init_card 方法发送 CMD9(读取 16 字节 CSD 寄存器数据)获取容量信息:
# 获取扇区数量
# CMD9: 响应R2(R1字节 + 16字节块读取)
if self.cmd(9, 0, 0, 0, False) != 0:
# 如果没有响应,则引发异常
raise OSError("no response from SD card")
# 创建CSD缓冲区
csd = bytearray(16)
# 读取CSD数据
self.readinto(csd)
接着通过计算 SD 卡扇区数:
# CSD版本2.0
if csd[0] & 0xC0 == 0x40:
# 计算扇区数量
self.sectors = ((csd[8] << 8 | csd[9]) + 1) * 1024
# CSD版本1.0(旧,<=2GB)
elif csd[0] & 0xC0 == 0x00:
# 获取C_SIZE
c_size = (csd[6] & 0b11) << 10 | csd[7] << 2 | csd[8] >> 6
# 获取C_SIZE_MULT
c_size_mult = (csd[9] & 0b11) << 1 | csd[10] >> 7
# 获取读取块长度
read_bl_len = csd[5] & 0b1111
# 计算容量
capacity = (c_size + 1) * (2 ** (c_size_mult + 2)) * (2**read_bl_len)
# 计算扇区数量
self.sectors = capacity // 512
else:
# 不支持的CSD格式
raise OSError("SD card CSD format not supported")
- 如果为 SDSC 卡,直接读取寄存器数据计算扇区数量即可;
- 如果为 SDHC / SDXC 卡,需要计算字节容量并转换为扇区数。
设置完成块长度为 512 个字节后,初始化完成,调整 SPI 设置以实现高数据传输速率:
# CMD16: 设置块长度为512个字节
if self.cmd(16, 512, 0) != 0:
# 无法设置块大小
raise OSError("can't set 512 block size")
# 现在设置为高数据速率
self.init_spi(baudrate)
2.2 命令发送方法
cmd 方法用于向 SD 卡发送命令并获取响应,整个流程可以分为五个主要阶段:

- 拉低 CS(片选),启动 SPI 通信;
- 构造并发送命令;
- 等待 SD 卡响应(超时则返回
-1); - 读取可选额外数据;
- 释放 CS 并返回响应。
这里,我们首先拉低 CS 引脚以开始通信,然后构建命令缓冲区,包括命令编号、参数和 CRC 校验值,并通过 SPI 发送命令:
def cmd(self, cmd: int, arg: int, crc: int, final: int = 0, release: bool = True, skip1: bool = False) -> int:
"""
发送SD卡命令并获取响应。
Args:
cmd (int): 命令编号(0-63)
arg (int): 32位命令参数
crc (int): CRC校验值
final (int, optional): 额外读取字节数,默认为0
release (bool, optional): 是否释放CS,默认为True
skip1 (bool, optional): 是否跳过首字节,默认为False
Returns:
int: 响应状态:
- 0x00: 正常
- 0x01: 空闲状态
- -1: 超时
Raises:
OSError: 如果SPI通信失败
"""
# 拉低CS引脚,开始通信
self.cs(0)
# 创建并发送命令
buf = self.cmdbuf
# 设置命令头
buf[0] = 0x40 | cmd
# 设置命令参数(高字节)
buf[1] = arg >> 24
# 设置命令参数(次高字节)
buf[2] = arg >> 16
# 设置命令参数(次低字节)
buf[3] = arg >> 8
# 设置命令参数(低字节)
buf[4] = arg
# 设置CRC校验
buf[5] = crc
# 通过SPI发送命令
self.spi.write(buf)
如果设置了跳过第一次响应,则会丢弃第一个字节。接着,进入循环以读取 SD 卡的响应,直到接收到有效响应或超时,若响应有效,根据需要读取额外字节并释放 CS 信号:
# 如果需要跳过第一次响应
if skip1:
# 读取一个字节,丢弃
self.spi.readinto(self.tokenbuf, 0xFF)
# 等待响应 (response[7] == 0)
for i in range(CMD_TIMEOUT):
# 读取响应字节
self.spi.readinto(self.tokenbuf, 0xFF)
# 获取响应
response = self.tokenbuf[0]
# 检查响应是否有效
if not (response & 0x80):
# 处理大端整数的情况
# 如果final为负
if final < 0:
# 读取并丢弃一个字节
self.spi.readinto(self.tokenbuf, 0xFF)
# 更新final值
final = -1 - final
# 读取额外的字节
for j in range(final):
# 发送空字节以保持时序
self.spi.write(b"\xff")
# 如果需要释放CS信号
if release:
# 拉高CS引脚
self.cs(1)
# 发送空字节
self.spi.write(b"\xff")
# 返回有效响应
return response
最终,返回 SD 卡的响应字节或在超时情况下返回-1:
# 超时处理
# 拉高CS引脚
self.cs(1)
# 发送空字节
self.spi.write(b"\xff")
# 返回-1表示超时
return -1
2.3 数据块读取方法
readinto 方法用于从 SD 卡读取一个 512 字节大小的数据块并存入指定的缓冲区,首先,拉低 CS 引脚以启动通信,然后进入循环,不断读取字节,直到检测到数据开始标志(0xFF),如果在设定的超时时间内未找到开始字节,则拉高 CS 引脚并抛出超时错误:
def readinto(self, buf: bytearray) -> None:
"""
读取数据块到缓冲区。
Args:
buf (bytearray): 目标缓冲区(必须512字节)
Returns:
None
Raises:
ValueError: 如果缓冲区长度错误
OSError: 如果读取超时或校验失败
"""
# 拉低CS引脚,开始通信
self.cs(0)
# 读取直到开始字节 (0xff)
for i in range(CMD_TIMEOUT):
# 读取一个字节
self.spi.readinto(self.tokenbuf, 0xFF)
# 检查是否为数据开始标志
if self.tokenbuf[0] == TOKEN_DATA:
# 找到开始字节,退出循环
break
# 等待1毫秒
time.sleep_ms(1)
else:
# 拉高CS引脚
self.cs(1)
# 抛出超时错误
raise OSError("timeout waiting for response")
找到开始字节后,创建内存视图,并检查缓冲区的长度是否匹配,若不匹配,则进行切片;随后,通过 SPI 将数据从 SD 卡读取到提供的缓冲区,并发送两个空字节:
# 读取数据,创建内存视图
mv = self.dummybuf_memoryview
# 如果缓冲区长度不匹配
if len(buf) != len(mv):
# 切片到缓冲区长度
mv = mv[: len(buf)]
# 读取数据到buf
self.spi.write_readinto(mv, buf)
# 发送空字节
self.spi.write(b"\xff")
self.spi.write(b"\xff")
# 拉高CS引脚
self.cs(1)
# 发送空字节
self.spi.write(b"\xff")
整体时序图如下所示:

2.4 数据块读取方法
write 方法用于向 SD 卡写入一个 512 字节的数据块,首先,拉低 CS 引脚以开始通信,然后发送写入的开始令牌、数据和两个空字节:
def write(self, token: int, buf: bytearray) -> bool:
"""
写入数据块到卡片。
Args:
token (int): 写入令牌(0xFC/0xFD/0xFE)
buf (bytearray): 源数据(必须512字节)
Returns:
bool: 写入结果:
- True: 成功
- False: 失败
Raises:
ValueError: 如果数据长度错误
OSError: 如果卡片响应异常
"""
# 拉低CS引脚,开始通信
self.cs(0)
# 发送: 块开始标记,数据,校验和
self.spi.read(1, token)
self.spi.write(buf)
self.spi.write(b"\xff")
self.spi.write(b"\xff")
接着,检查响应以确认写入是否成功,若响应不符合成功标志,则拉高 CS 引脚并发送空字节后返回,若响应为成功,方法将等待直到写入完成:
# 检查响应是否为成功标志
if (self.spi.read(1, 0xFF)[0] & 0x1F) != 0x05:
# 拉高CS引脚
self.cs(1)
# 发送空字节
self.spi.write(b"\xff")
# 返回,写入失败
return
# 等待写入完成
while self.spi.read(1, 0xFF)[0] == 0:
# 等待直到写入完成
pass
完成后,再次拉高 CS 引脚并发送空字节,以结束通信:
# 拉高CS引脚
self.cs(1)
# 发送空字节
self.spi.write(b"\xff")
整体时序图如下所示:

2.5 数据令牌写入方法
write_token 方法用于发送控制令牌(token),并等待 SD 卡完成内部操作,首先拉低 CS 引脚,发送开始令牌和一个空字节,然后等待写入完成;最后,拉高 CS 引脚并发送空字节,结束通信:
def write_token(self, token: int) -> None:
"""
发送控制令牌。
Args:
token (int): 控制令牌(0xFC/0xFD/0xFE)
Returns:
None
Raises:
OSError: 如果卡片未响应
"""
# 拉低CS引脚,开始通信
self.cs(0)
# 发送标记
self.spi.read(1, token)
# 发送空字节
self.spi.write(b"\xff")
# 等待写入完成
while self.spi.read(1, 0xFF)[0] == 0x00:
# 等待直到写入完成
pass
# 拉高CS引脚
self.cs(1)
# 发送空字节
self.spi.write(b"\xff")
2.6 数据擦除方法
erase_block 方法用于擦除 SD 卡上的指定数据块,首先,拉低 CS 引脚以开始通信,发送擦除命令 CMD32(擦除开始)和 CMD33(擦除结束),指定要擦除的块号,通过将块号乘以 512 以转换为字节偏移:
def erase_block(self, block_number: int) -> None:
"""
擦除指定数据块。
Args:
block_number (int): 块编号(LBA格式)
Returns:
None
Raises:
ValueError: 如果块号超出范围
OSError: 如果擦除命令失败
"""
# 拉低CS引脚,开始通信
self.cs(0)
# 发送擦除命令 CMD32 (erase start) 和 CMD33 (erase end)
# 开始擦除
self.cmd(32, block_number * 512, 0)
# 结束擦除
self.cmd(33, block_number * 512, 0)
接着,发送 CMD38(擦除命令)来实际执行擦除操作,方法随后进入一个循环,持续查询擦除状态,通过发送 CMD13 命令直到返回值为 0,表示擦除完成;最后,拉高 CS 引脚,发送一个空字节以结束通信:
# 等待擦除完成
while True:
# 查询状态
response = self.cmd(13, 0, 0)
# 擦除完成
if response == 0:
break
# 拉高CS引脚
self.cs(1)
# 发送空字节
self.spi.write(b"\xff")
3.SD 卡挂载文件系统
在以下代码中,我们实现了初始化 SD 卡并将其挂载为文件系统的操作,并创建了一个 CSV 文件写入网站名称和网址数据。所谓 CSV 文件,可以想象成一本简单的电子表格,但它没有 Excel 那样丰富的功能和复杂的格式,而只是一个用逗号分隔的文本文件,CSV 每行表示一条记录,记录中的字段由逗号分隔,其数据以纯文本形式存储:

CSV 文件使用 EXCEL 打开时非常类似于表格文件
3.1 实验前的准备
首先我们需要将风雅一号板-以太网与SD卡扩展板插入到风雅一号板-通用兼容扩展板上,并且将风雅一号板-以太网与SD卡扩展板上的 SW1 拨码开关全部打开

除此之外我们还需要将 SD 卡插入 SD 卡模块上的 SD 卡槽,SD 卡槽内部有电气连接和控制电路,可以将 SD 卡与微控制器连接:

各个引脚功能如下:

每个引脚的功能和在 SPI 模式下的角色为:

其余,另一个 VSS 引脚 Pin6 需要接地,而 Pin8 和 Pin9 需要接高电平。并且需要注意的是,SPI 总线上 MOSI、MISO、SCK 和 CS 信号需要接上拉电阻,以保证在未驱动状态时保持在高电平,并且在 VDD 供电部分需要注意连接 0.1µF 的陶瓷电容和 10µF 的电解电容来给电源进行滤波。
电路原理图如下所示:

实物图如下所示:
SD 卡槽和树莓派 Pico 连接如下表所示:

3.2 SD 卡块设备类的实现
要想实现 SD 卡挂载文件系统,首先要实现 VFS 虚拟文件系统中要求的块设备,这里,我们定义了一个用于与 SD 卡进行块设备操作的 SDCARDBlockDevice 类,提供了对 SD 卡的数据块读写、控制和管理功能,示例代码如下:
# Python env : MicroPython v1.23.0
# -*- coding: utf-8 -*-
# @Time : 2024/10/1 上午11:13
# @Author : 李清水
# @File : sd_block_dev.py
# @Description : 自定义SD卡块设备类
# 参考代码:https://github.com/micropython/micropython-lib/blob/master/micropython/drivers/storage/sdcard/sdcard.py#L291
# ======================================== 导入相关模块 ========================================
# 导入虚拟文件块设备的抽象基类
from AbstractBlockDevInterface import AbstractBlockDev
# 导入自定义SD卡读写类中定义的一些常量
from sdcard import TOKEN_CMD25,TOKEN_STOP_TRAN,TOKEN_DATA
# 导入自定义SD卡读写类
from sdcard import SDCard
# ======================================== 全局变量 ============================================
# 定义错误码常量,符合 POSIX 标准的错误码
ENOERR = 0 # 操作成功
EPERM = 1 # 操作不允许
EIO = 5 # I/O错误
ENODEV = 19 # 无效设备/块号
EROFS = 30 # 只读文件系统
EINVAL = 22 # 无效参数
ENOSPC = 28 # 没有空间
# ======================================== 功能函数 ============================================
# ======================================== 自定义类 ============================================
class SDCARDBlockDevice(AbstractBlockDev):
"""
SDCARDBlockDevice 类,实现 SD 卡块设备的读写操作。
该类继承自 AbstractBlockDev,封装了对 SDCard 设备的块级读写操作,
提供标准的 readblocks、writeblocks 和 ioctl 方法,符合 MicroPython 块设备接口规范。
Attributes:
sdcard (SDCard): 关联的 SDCard 实例,用于执行实际的 SD 卡操作。
Methods:
__init__(self, sdcard: SDCard):
初始化 SDCARDBlockDevice 实例,绑定一个 SDCard 设备。
readblocks(self, block_num: int, buf: bytearray, offset: int = 0):
读取 SD 卡的一个或多个块的数据到缓冲区。
writeblocks(self, block_num: int, buf: bytearray, offset: int = 0):
将缓冲区数据写入 SD 卡的一个或多个块。
ioctl(self, op: int, arg: int) -> int:
设备控制接口,用于执行初始化、同步、获取块信息等操作。
"""
def __init__(self, sdcard: SDCard) -> None:
"""
初始化 SDCARDBlockDevice 实例。
Args:
sdcard (SDCard): 传入的 SDCard 实例。
Raises:
ValueError: 如果传入的 sdcard 不是 SDCard 实例。
"""
# 检查传入的SDCard实例是否有效
if not isinstance(sdcard, SDCard):
raise ValueError("Invalid SDCard instance")
# 调用父类构造函数
super().__init__()
# 保存传入的SDCard实例
self.sdcard = sdcard
def readblocks(self, block_num: int, buf: bytearray, offset: int = 0) -> None:
"""
从 SD 卡读取数据块到缓冲区。
Args:
block_num (int): 起始块号。
buf (bytearray): 用于存储读取数据的缓冲区,长度必须是 512 字节的倍数。
offset (int, optional): 数据偏移量,默认为 0。
Raises:
OSError: 如果缓冲区长度不是 512 的倍数,或者读取失败。
"""
# 检查缓冲区是否有效并且是否长度为512个字节
if not buf or len(buf) % 512 != 0:
raise OSError(EINVAL, "Buffer length must be multiple of 512")
# 解决共享总线问题,确保在开始事务之前MOSI为高电平
self.sdcard.spi.write(b"\xff")
# 计算需要读取的块数
nblocks = len(buf) // 512
# 确保缓冲区长度有效
assert nblocks and not len(buf) % 512, "Buffer length is invalid"
if nblocks == 1:
# CMD17: 设置单个块的读取地址
if self.sdcard.cmd(17, block_num * self.sdcard.cdv, 0, release=False) != 0:
# 释放卡片
self.sdcard.cs(1)
# EIO错误
raise OSError(EIO, "Failed to read singe block")
# 接收数据并释放卡片
self.sdcard.readinto(buf)
else:
# CMD18: 设置多个块的读取地址
if self.sdcard.cmd(18, block_num * self.sdcard.cdv, 0, release=False) != 0:
# 释放卡片
self.sdcard.cs(1)
# EIO错误
raise OSError(EIO, "Failed to read multi blocks")
# 数据偏移量
offset = 0
# 创建内存视图
mv = memoryview(buf)
while nblocks:
# 接收数据并释放卡片
self.sdcard.readinto(mv[offset : offset + 512])
# 更新偏移量
offset += 512
# 减少剩余块数
nblocks -= 1
if self.sdcard.cmd(12, 0, 0xFF, skip1=True):
# EIO错误
raise OSError(EIO, "Failed to stop multi blocks")
def writeblocks(self, block_num: int, buf: bytearray, offset: int = 0) -> None:
"""
将数据块从缓冲区写入 SD 卡。
Args:
block_num (int): 起始块号。
buf (bytearray): 要写入的数据,长度必须是 512 字节的倍数。
offset (int, optional): 数据偏移量,默认为 0。
Raises:
OSError: 如果缓冲区长度不是 512 的倍数,或者写入失败。
"""
# 检查缓冲区是否有效并且是否长度为512字节
if not buf or len(buf) % 512 != 0:
raise OSError(EINVAL, "Buffer length must be multiple of 512")
# 解决共享总线问题,确保在开始事务之前MOSI为高电平
self.sdcard.spi.write(b"\xff")
# 计算需要写入的块数
nblocks, err = divmod(len(buf), 512)
# 确保缓冲区长度有效
assert nblocks and not err, "Buffer length is invalid"
if nblocks == 1:
# CMD24: 设置单个块的写入地址
if self.sdcard.cmd(24, block_num * self.sdcard.cdv, 0) != 0:
# EIO错误
raise OSError(EIO, "Failed to write singe block")
# 发送数据
self.sdcard.write(TOKEN_DATA, buf)
else:
# CMD25: 设置第一个块的写入地址
if self.sdcard.cmd(25, block_num * self.sdcard.cdv, 0) != 0:
# EIO错误
raise OSError(EIO, "Failed to write multi blocks")
# 发送数据
offset = 0
# 创建内存视图
mv = memoryview(buf)
while nblocks:
# 发送每块数据
self.sdcard.write(TOKEN_CMD25, mv[offset : offset + 512])
# 更新偏移量
offset += 512
# 减少剩余块数
nblocks -= 1
# 发送停止传输命令
self.sdcard.write_token(TOKEN_STOP_TRAN)
def ioctl(self, op: int, arg: int) -> int:
"""
控制块设备并查询其参数。
Args:
op (int): 操作码,参考 AbstractBlockDev 定义。
arg (int): 附加参数。
Returns:
int: 操作结果,成功返回 0,或相应的块数/字节数。
Raises:
OSError: 如果操作码无效或者擦除块号无效。
"""
# 初始化设备
if op == AbstractBlockDev.IOCTL_INIT:
# 执行初始化操作
self.sdcard.init_card(1320000)
# 成功返回0
return ENOERR
# 关闭设备
elif op == AbstractBlockDev.IOCTL_SHUTDOWN:
return ENOERR
# 同步设备
elif op == AbstractBlockDev.IOCTL_SYNC:
return ENOERR
# 获取块数
elif op == AbstractBlockDev.IOCTL_BLK_COUNT:
# 返回扇区数
return self.sdcard.sectors
# 获取块大小(字节)
elif op == AbstractBlockDev.IOCTL_BLK_SIZE:
# 返回512个字节
return 512
# 擦除块
elif op == AbstractBlockDev.IOCTL_BLK_ERASE:
# 擦除指定块
if arg < 0 or arg >= self.sdcard.sectors:
# 如果块号无效,抛出错误
raise OSError(ENOSPC, "Invalid block number")
self.sdcard.erase_block(arg)
return ENOERR
else:
# 无效的操作码,抛出异常
raise OSError(EINVAL, "Invalid ioctl operation")
# ======================================== 初始化配置 ==========================================
# ======================================== 主程序 ============================================
该类提供了如下方法:
- 初始化: 在构造函数
init中,传入的SDCard实例被保存为对象属性,以便在后续方法中调用。通过调用父类构造函数进行初始化。 - 读取数据块
readblocks方法:用于从 SD 卡读取数据块到提供的缓冲区,根据要读取的块数,使用CMD17命令读取单个块,或者使用CMD18命令读取多个块。在读取过程中,确保 MOSI 信号为高电平;然后,根据块号计算读取的起始地址,调用 SD 卡实例的方法进行实际的数据读取,并在读取完成后释放卡片。

- 写入数据块
writeblocks方法:用于将缓冲区中的数据写入 SD 卡。根据要写入的块数,使用CMD24命令写入单个块,或者使用CMD25命令写入多个块;计算要写入的块数,并通过 SD 卡实例的方法进行数据写入,最后发送停止传输命令以结束写入操作。

- 控制操作
ioctl方法:实现了设备的控制操作,包括初始化、关闭、同步、获取块数和块大小、以及擦除指定块等功能,每个操作通过操作码op识别,执行相应的操作并返回结果。

3.3 应用实验
以下代码可以在我们提供的资料包中的 elegance-devkit v1\Demo\73 FileSys_SDCard 文件夹中找到。
主程序中,代码如下:
# Python env : MicroPython v1.23.0
# -*- coding: utf-8 -*-
# @Time : 2024/9/30 下午12:13
# @Author : 李清水
# @File : main.py
# @Description : 文件系统类实验,使用SD卡挂载文件系统并读写
# ======================================== 导入相关模块 ========================================
# 导入硬件相关模块
from machine import SPI, Pin
# 导入时间相关模块
import time
# 导入自定义SD卡块设备类
from sd_block_dev import SDCARDBlockDevice
# 导入自定义SD卡读写类
from sdcard import SDCard
# 导入虚拟文件类
import vfs
# 导入文件系统操作类
import os
# ======================================== 全局变量 ============================================
# 定义嵌入式知识学习网站及其网址
websites = [
("Embedded.com", "https://www.embedded.com"),
("Microchip", "https://www.microchip.com"),
("ARM Developer", "https://developer.arm.com"),
("SparkFun", "https://www.sparkfun.com"),
("Adafruit", "https://www.adafruit.com"),
("Embedded Systems Academy", "https://www.esacademy.com"),
("Electronics Hub", "https://www.electronicshub.org"),
]
# 定义CSV文件地址
csv_file_path = '/sdcard/embedded_websites.csv'
# ======================================== 功能函数 ============================================
# ======================================== 自定义类 ============================================
# ======================================== 初始化配置 ==========================================
# 上电延时3s
time.sleep(3)
# 打印调试消息
print("FreakStudio: Mount the SD Card to the file system")
# 初始化SPI类,设置波特率、极性、相位、时钟引脚、数据引脚
spi = SPI(1, baudrate=1320000, polarity=0, phase=0, sck=Pin(10), mosi=Pin(11), miso=Pin(12))
# 初始化SD卡类,使用GPIO9作为片选引脚
sdcard = SDCard(spi, cs=Pin(13))
# 创建块设备,使用SD卡,块大小为512个字节
block_device = SDCARDBlockDevice(sdcard = sdcard)
# 在块设备上创建一个 FAT 文件系统
vfs.VfsFat.mkfs(block_device)
# 将块设备挂载到虚拟文件系统的 /sdcard 目录
vfs.mount(block_device, '/sdcard')
# 打印当前目录
print("Current Directory : ",os.listdir())
# ======================================== 主程序 ============================================
# 写入 CSV 文件
with open(csv_file_path, 'w') as f:
# 写入表头
f.write("Website Name,URL\n")
for name, url in websites:
# 写入每一行
f.write(f"{name},{url}\n")
# 打印文件位置
print(f"CSV file written to SD card as '{csv_file_path}'.")
在主程序中,我们依次进行如下操作:

- 创建
SPI实例,设置波特率、极性、相位及各引脚(SCK、MOSI、MISO);创建SDCard实例,使用指定的SPI接口和GPIO作为片选引脚。 - 使用
SDCARDBlockDevice将 SD 卡封装为块设备,块大小为 512 字节。 - 使用
vfs.VfsFat.mkfs(block_device)格式化块设备,创建 FAT 文件系统。 - 使用
vfs.mount(block_device, '/sdcard')将块设备挂载到虚拟文件系统的/sdcard目录。 - 使用
with open(csv_file_path, 'w') as f:以写入模式打开指定路径的 CSV 文件,首先写入表头"Website Name,URL\n",然后使用循环遍历网站信息,逐行写入每个网站的名称和 URL。 - 最后,打印确认信息,表示 CSV 文件已成功写入 SD 卡,并给出文件路径。
烧录代码,打开终端,输出如下:

可以看到,CSV 文件已经成功写入到 /sdcard 目录中,为了查看其中内容,我们需要将 SD 卡拔下,通过读卡器将 SD 卡与电脑进行连接,需要注意此时先卸载文件系统:
os.umount('/sdcard')
卸载操作会强制将所有缓存数据刷写到 SD 卡,确保物理存储与实际数据一致。
同时需要注意打开 SD 卡上的写保护开关,以避免 SD 卡插入电脑后,其文件系统组织被改变,比如更新文件分配表(FAT)、创建隐藏文件(如系统文件)或改变文件的属性。
将读卡器插入电脑后,会发现出现一个 U 盘,其中有刚刚我们通过树莓派 Pico 写入的 CSV 文件:

打开 CSV 文件,可以看到树莓派 Pico 成功写入了嵌入式学习网站和对应网址:

当将 SD 卡从读卡器上拔下再次插入到 SD 卡模块上的 SD 卡槽上时,需要重新挂载文件系统:
vfs.mount(block_device, '/sdcard')
我们可以在 REPL 中输入下面代码验证是否重新挂载成功和文件是否保存完好:
# 读取CSV文件并打印内容
with open(csv_file_path, 'r') as f:
for line in f:
# 移除行尾的换行符并分割字段
parts = line.strip().split(',')
if len(parts) == 2:
print(f"Website: {parts[0]:<30} URL: {parts[1]}")
else:
# 打印表头或其他行
print(line.strip())
终端输出如下,可以看到成功读取之前保存的 CSV 文件:



SD 卡是一种广泛应用于数码设备的可移除存储设备,具有小型化、低功耗和大容量的特点,它主要用于存储数据、音乐、视频、图像等信息,广泛应用于手机、相机、平板电脑、嵌入式设备等。


浙公网安备 33010602011771号