深度刨析 SD 卡:从结构原理、卡协议、电路到MicroPython驱动实现和文件系统挂载

1.SD 卡基本知识

SD 卡通信协议解剖:CMD 命令及其响应消息结构

1.1 SD 卡简介和分类

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

ScreenShot_2026-06-26_060536_085.png

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

2.PNG

  • 标准 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 卡可分为以下几种类型:

3.PNG

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

4.PNG

这里,补充两个基本概念:

  • 最大容量​:指这个文件系统能 “管理的存储设备总空间上限”(比如 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 卡的机械结构包括塑料外壳、金属引脚、内存芯片、控制器组成,其外壳通常使用坚固的塑料材料,能够抵御物理冲击和环境因素:

5.png

SD 卡像个 “带智能管家的 U 盘”,通过下面各个单元完成工作:

  • 存储核心​:用 NAND 闪存存数据(类似手机里的存储芯片);
  • 智能管家(控制器)​:
    • 接收外部设备的指令(比如 “读 XX 文件”),然后去闪存里找数据;
    • 用文件系统(FAT32/exFAT)管理数据(之前讲过的 VFS 就能对接这个);
    • 用均衡擦写技术:轮流用闪存的不同区域存数据,避免某一块被频繁写入而提前损坏(像轮流用不同抽屉放东西);
    • 自带错误校正:自动检测数据里的小错误并修复,避免读错内容。

SD 卡底部有多个金属引脚,用于数据传输、电源供给和信号控制,标准 SD 卡有 9 个引脚,microSD 则有 8 个:

6.png

SD 卡的 9 个引脚可以分为四个数据传输引脚 DAT0~DAT3、命令引脚 CMD、地引脚 VSS 和供电引脚 VDD,并且大部分 SD 卡还有一个写保护 WP(Write Protect)开关在标准 SD 卡中:
7.png在介绍各个引脚作用之前,我们需要知道 SD 卡有两种常用操作模式:

  • SD Mode​:高速但引脚多、通信复杂,一般用在手机 / 相机等专业设备;
  • SPI Mode​:引脚少、通信简单,是嵌入式编程(比如用开发板接 SD 卡)的常用模式,新手重点看这个模式即可。

下面是每个引脚在两种模式下的对应功能:

image.png
image.png

上表中引脚类型的简写含义如下:

  • I:输入(SD 卡从外部设备 “接收” 信号);

  • O:输出(SD 卡向外部设备 “发送” 信号);

  • I/O:双向(既能收又能发);

  • S:电源 / 地(接供电或接地)。

    SD 卡侧面有个硬件开关,同时内部会检测特定引脚(通常是引脚 8)的电平:

  • 当 WP 引脚被拉低(检测到特定电平)时,SD 卡会 “锁死” 写入功能,只能读数据;

  • 开关拨回后,才能恢复读写。

除了引脚、控制器和 NAND 闪存外,SD 卡内部还有多个寄存器管理卡的基本信息、状态、配置和操作条件,如下图所示:

8.png

SD 卡的内部结构是 “分层协作” 的,各部分功能明确:

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

以下是一些关键寄存器及其功能:

9.png

外部设备(比如单片机)和 SD 卡的协作流程是:

  1. 外部设备通过 “引脚” 给 SD 卡控制器发指令(比如 “读取你的身份证号”);
  2. 控制器从对应的寄存器(比如 CID)里取出信息,通过引脚传回给外部设备;
  3. 若要读写数据,控制器会根据指令,管理 NAND 闪存的 “格子”(分配空间、存 / 取数据);
  4. 过程中,控制器会用 “均衡擦写”(轮流用不同格子存数据,避免某格坏得快)、“错误校正”(自动修数据里的小错误)保护 SD 卡和数据。

1.3 SD 卡的工作模式

可以把主机(比如开发板 / 手机)和 SD 卡的交互类比成 “手机连新 U 盘”:

  • ​卡识别模式:​手机和新 U 盘 “互相认识、配对准备” 的过程;
  • ​数据传输模式:​配对完成后,手机往 U 盘存文件 / 从 U 盘读文件的过程。

SD 卡的工作模式主要包括两个阶段:卡识别模式(Card Identification Mode)和数据传输模式(Data Transfer Mode),在卡识别模式下,主机的目标是识别并初始化 SD 卡,使其准备好进入数据传输模式,数据传输模式是真正读写数据的阶段:

10.png

流程图里的 “Idle/Ready/Ident” 是 SD 卡的状态(当前所处的阶段),“CMD0/CMD8” 是主机给 SD 卡发的指令,整个流程是:

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

除此之外,还有一些特别的流程:

  • 若 SD 卡是老的 MMC 卡(多媒体卡),会走 “CMD1” 的流程,不用 CMD8/ACMD41;
  • 「CMD15」是 “取消选中卡”,相当于主机说 “我不用你了”,卡会回到之前的状态。

11.png

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 线)进行传输,命令与数据传输互不干扰。

12.png

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

13.png

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 命令列表:

14.png

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

15.png

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

需要注意,所有响应帧均包含 CRC 校验位,主机需校验 CRC 无误后,方可判定响应有效。

SD 卡的数据传输基于块传输机制,数据帧格式与总线宽度相关,支持单块传输和多块传输两种模式。

16.png

其中:

  • ​单块传输:​主机通过 CMD17(读)/CMD24(写)命令触发,SD 卡仅传输 1 个数据块,传输完成后自动结束,无需额外终止命令;
  • ​多块传输:​主机通过 CMD18(读)/CMD25(写)命令触发,SD 卡连续传输多个数据块,直到主机发送 CMD12(停止传输命令) 后,才终止数据传输。该模式适用于大文件的连续读写,可提升传输效率。

数据帧的传输分为普通数据传输和宽数据传输两种方式,对应不同的总线宽度:

  • 普通数据传输​:采用 1 线总线(仅 DAT0),数据以字节为单位串行传输,每个字节包含 8 位数据位,搭配起始位、停止位和 CRC16 校验位,适用于低速传输场景;
  • 宽数据传输​:采用 4 线总线(DAT0~DAT3),将 1 字节(8 位)数据拆分为 4 组,每组 2 位,通过 4 条数据线并行传输,传输速率为 1 线模式的 4 倍,适用于高速传输场景。

17.png

1.4.2 SPI 协议中的命令响应模型

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

18.png

SPI 模式下,卡片初始化流程如下:

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

在 SPI 模式下,SD 卡支持单块读取(Single Block Read)和多块读取(Multiple Block Read)操作,分别对应 SD 卡协议中的 CMD17CMD18 命令:

19.png

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

20.png

时序流程为:主机(DataIn)→ 发送 CMD18 命令 → SD 卡(DataOut)→ 返回 R1 响应 → SD 卡连续发送多个数据块 + CRC → 主机发送 CMD12(停止命令)→ SD 卡返回 R1 响应 → 事务结束。

同样的,SD 卡支持单块写入(Single Block Write)和多块写入(Multiple Block Write)命令,分别对应 SD 卡协议中的 CMD24CMD25 命令:

21.png

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

22.png

主机(DataIn)→ 发送 CMD25 命令 → SD 卡(DataOut)→ 返回 R1 响应 → 主机连续发送起始块令牌 + 多个数据块 → 主机发送停止令牌 → SD 卡返回 busy 信号 → 事务结束。

在 SPI 模式下,SD 卡的命令、响应和数据的格式与标准 SD 模式有所不同,每个命令为 6 字节,从 01(起始位)开始,接着是命令号(6 位),然后是 32 位的参数,最后是 7 位的 CRC 校验和 1 位停止位:

23.png

image.png

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

24.png

image.png

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

25.png

响应 RESPONSE 通常为 1 字节,称为 R1 响应,在某些情况下,可能需要 2 字节或更多的响应。

R1 响应的结构如下图所示,包括空闲状态指示位、擦除复位指示位、非法命令指示位等:

26.png

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

27.png

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

28.png

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

29.png

不同响应对比如下:
image.png

数据令牌是 SD 卡读写操作中承载实际数据的单元,其通用结构遵循 “起始标志 + 数据 + 校验和” 的格式,同时需遵守固定的传输规则:

  • 组成部分:

    • ​起始标志(1 字节):​标识数据块的开始(单块 / 多块对应不同值);
    • ​数据段(2~513 字节):​实际存储的用户数据(SDHC/SDXC 卡固定为 512 字节);
    • ​校验和(2 字节):​16 位 CRC16 校验值,用于验证数据传输的正确性。
  • 传输规则:

    • 字节传输顺序为 MSB 优先(高位字节先传输);
    • 数据长度根据卡类型可调整(普通 SD 卡为 1~512 字节,SDHC/SDXC 固定为 512 字节)。

    根据读写操作类型(单块 / 多块),数据令牌的起始标志会使用不同值,对应不同的传输场景:
    image.png

30.png

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

31.png

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

33.png

其中状态位定义如下:

  • ​'010':​数据被接受
  • ​'101':​因为 CRC 错误导致数据被拒绝
  • ​'110':​因为写入错误导致数据被拒绝

在多块写入操作中,如果发生任何错误(CRC 错误或写入错误),主机应使用 CMD12 停止数据传输;如果发生写入错误(响应为‘110’),主机可以发送 CMD13 (SEND_STATUS)来获取写入问题的原因,或者通过 ACMD22 找出成功写入的块数量。

如果读操作失败且 SD 卡无法提供所需数据,卡会发送一个数据错误令牌,长度为 1 字节,其格式如下:

34.png

其中,4 个最低有效位(LSB)与 R2 响应格式中的错误位相同,表示不同的错误状态。

1.5 SD 卡的存储结构简介

通过外部通信接口(SD 总线或 SPI 总线),我们可以访问 SD 卡的存储单元:

35.png

SD 卡的存储结构分为内部硬件架构与逻辑 / 物理存储单元两部分:

  • ​SD 卡的内部硬件架构:​由单芯片控制器与 Flash 存储模块构成,二者协同实现数据的通信与存储:
    • ​单芯片控制器(SanDisk Single Chip Controller):​是 SD 卡的 “控制中枢”
      • ​对外:​通过 “SD Bus/SPI Bus Interface” 与外部主机(如开发板)建立通信;
      • ​对内:​通过 “Control” 信号管理 Flash 存储模块的读写逻辑,同时通过 “Data In/Out” 与 Flash 模块传输数据。
    • ​Flash 存储模块(Flash Modules):​是 SD 卡的 “实际存储载体”,所有用户数据均存储于此,其读写操作由控制器统一调度。
  • ​存储的核心单元:​SD 卡的存储单元分为逻辑可寻址单元(Sector)与物理存储单元(Block),二者的定义与关系如下:
    • 扇区(Sector)
      • 是 SD 卡的最小可寻址单位(主机访问 SD 卡时,只能以 Sector 为最小操作粒度);
      • 大小通常为 512 字节;
      • 每个 Sector 包含多个 Block(Block 是物理存储的基本单元)。
    • 块(Block)
      • 是 SD 卡向 Flash 存储模块写入数据的基本物理单元;
      • 大小通常为 512 字节或 4KB;
      • 实际数据写入时,控制器会将数据按 Block 组织后,再写入 Flash 模块。
  • ​写保护与专用区域:​核心包括 “写保护组” 与 “保护区域”:
    • 写保护组(WP Group)
      • 是 SD 卡实现写保护功能的逻辑区域,以扇区为基本单元;
      • 当 SD 卡处于写保护状态时,WP Group 内的所有扇区会被禁止写入 / 修改(如右图中的 WP Group 0、WP Group 1 等)。
    • 保护区域(Protected Area)
      • 是用于版权保护的专用存储区域,同样以扇区为单元;
      • 该区域的访问权限受 SD 卡协议限制,非授权主机无法修改其中的数据。
  • ​内部缓冲区(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")

# ======================================== 初始化配置 ==========================================

# ========================================  主程序  ============================================

36.png

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)

具体工作流程如下所示:

37.png

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

38.png

首先将片选引脚 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() 进行如下操作:

39.png

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

40.png

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 卡发送命令并获取响应,整个流程可以分为五个主要阶段:

41.png

  1. 拉低 CS(片选),启动 SPI 通信;
  2. 构造并发送命令;
  3. 等待 SD 卡响应(超时则返回 -1);
  4. 读取可选额外数据;
  5. 释放 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")

整体时序图如下所示:

42.png

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")

整体时序图如下所示:

43.png

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 每行表示一条记录,记录中的字段由逗号分隔,其数据以纯文本形式存储:

44.png
CSV 文件使用 EXCEL 打开时非常类似于表格文件

3.1 实验前的准备

首先我们需要将风雅一号板-以太网与SD卡扩展板插入到风雅一号板-通用兼容扩展板上,并且将风雅一号板-以太网与SD卡扩展板上的 SW1 拨码开关全部打开

45.jfif

46.png

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

47.png

各个引脚功能如下:

48.png

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

其余,另一个 VSS 引脚 Pin6 需要接地,而 Pin8Pin9 需要接高电平。并且需要注意的是,SPI 总线上 MOSIMISOSCKCS 信号需要接上拉电阻,以保证在未驱动状态时保持在高电平,并且在 VDD 供电部分需要注意连接 0.1µF 的陶瓷电容和 10µF 的电解电容来给电源进行滤波。

电路原理图如下所示:

50.png

实物图如下所示:

51.jfif

52.jfif

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

53.png

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 卡实例的方法进行实际的数据读取,并在读取完成后释放卡片。

54.png

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

55.png

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

56.png

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}'.")

在主程序中,我们依次进行如下操作:

57.png

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

烧录代码,打开终端,输出如下:

58.png

可以看到,CSV 文件已经成功写入到 /sdcard 目录中,为了查看其中内容,我们需要将 SD 卡拔下,通过读卡器将 SD 卡与电脑进行连接,需要注意此时先卸载文件系统:

os.umount('/sdcard')

卸载操作会强制将所有缓存数据刷写到 SD 卡,确保物理存储与实际数据一致。

同时需要注意打开 SD 卡上的写保护开关,以避免 SD 卡插入电脑后,其文件系统组织被改变,比如更新文件分配表(FAT)、创建隐藏文件(如系统文件)或改变文件的属性。

将读卡器插入电脑后,会发现出现一个 U 盘,其中有刚刚我们通过树莓派 Pico 写入的 CSV 文件:

59.png

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

60.png

当将 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 文件:

61.png

00.gif

posted @ 2026-09-03 18:33  FreakStudio  阅读(13)  评论(0)    收藏  举报