
写SD卡这个主题其实憋了很久。前阵子有个项目要用单片机做本地数据记录板载 Flash 只有 4MB换大容量 Flash 价格直接翻几倍最后我还是选了 SD 卡——几块钱一张、容量按 GB 起步、接口简单几乎没有更适合做数据落地的外设了。不过真上手才发现从硬件电路到卡协议再到 MicroPython 里的驱动和文件系统挂载每一个环节都有不少坑。这篇文章就把我从结构原理、卡协议、电路设计到 MicroPython 驱动实现和文件系统挂载的完整折腾过程整理出来希望能帮到正准备用 SD 卡做项目的朋友。1. 先把 SD 卡拆开看硬件结构里藏着哪些关键信息1.1 一张 SD 卡从外到里都有什么很多人把 SD 卡当成一个黑盒子只知道往里面写数据其实它内部的结构对我们的开发方式影响很大。先从外观说起标准 SD 卡尺寸是 32mm × 24mm × 2.1mm而 microSD 卡只有 15mm × 11mm × 1mm。我们开发中常用的卡座有两种一种是直接插标准 SD 卡的大卡座另一种是 microSD 卡座加转接板。体积敏感的项目基本都用 microSD配合转接小板也能插到标准卡座里。打开外壳SD 卡里面其实是一块小型 PCB上面主要两个芯片一个是主控芯片另一个是 NAND Flash 存储颗粒。主控芯片负责对外接口协议转换、坏块管理、ECC 校验和磨损均衡。这意味着我们使用 SD 卡时不用像操作裸 NAND Flash 那样去管理坏块和擦写均衡这是 SD 卡最大的优势——硬件层面已经帮你把这些脏活累活全干完了。金属触点部分标准 SD 卡有 9 个引脚microSD 通常是 8 个引脚。SD 总线模式下引脚功能如下引脚名称说明1DAT2数据线 22CD/DAT3卡检测 / 数据线 33CMD命令线4VDD电源 3.3V5CLK时钟6VSS地7DAT0数据线 08DAT1数据线 1如果把 SD 卡接到 SPI 模式引脚映射会变DAT0 变成 MISO数据输出CMD 变成 MOSI数据输入CLK 还是时钟DAT3 变成 CS片选DAT1 和 DAT2 则空闲不用。这种复用设计非常巧妙让不支持 SD 总线协议的 MCU 也能通过 SPI 接口轻松读写 SD 卡。1.2 SDSC、SDHC、SDXC 到底差在哪SD 卡联盟把卡分成了三代SDSC标准容量、SDHC高容量和 SDXC扩展容量。容量上大致划分是SDSC 最大 2GBSDHC 是 4GB 到 32GBSDXC 从 64GB 起步现在最大能到 2TB。不仅仅容量不同它们的内部协议和寄存器设定也有差异。SDSC 使用字节寻址SDHC 和 SDXC 使用块寻址块大小固定 512 字节。初始化时主机需要在 ACMD41 命令里设置 HCSHost Capacity Support位来声明自己支持高容量卡SDHC/SDXC 卡只有在收到 HCS1 的 ACMD41 才会回应这直接影响后续的地址计算方式。另一个容易忽略的点是出厂文件系统。SDSC 卡出厂通常格式化为 FAT12 或 FAT16SDHC 卡出厂一般是 FAT32而 SDXC 卡出厂是 exFAT。对 MCU 开发者来说如果固件里的文件系统栈不支持 exFAT插上一张 64GB 的 SDXC 卡可能会连不了盘。这不是卡坏了而是文件系统不兼容。实际项目里我建议直接把卡重新格式化SDHC 用 FAT32小容量 SDSC 用 FAT16省去一堆麻烦。1.3 这些结构对我们做嵌入式开发意味着什么理解了 SD 卡内部结构至少有三件事直接影响到我们的开发决策。第一SD 卡主控承担了底层的 Flash 管理我们可以用简单的 read_block、write_block 命令做随机读写而不需要知道坏块位置、不需要做擦除操作开发效率比裸 NAND Flash 高一个量级。第二SD 卡有内部缓存和写加速机制硬件层在写数据时会做磨损均衡和缓存整理这意味着我们发出的写完成应答不一定代表数据已经物理落到 Flash 上。严格的数据完整性场景中重要数据写入后最好做一次回读验证或者关机前通过 sync/flush 操作强制落盘。第三不同卡的速度等级差异非常大。卡面上通常会标 Class 2、Class 4、Class 6、Class 10或者 U1、U3、A1、A2 这些标识。Class 10 代表最低连续写速度 10MB/sA1/A2 则是针对随机读写应用的性能评级。如果项目需要频繁记录日志建议选 A1 以上的卡普通存储卡在大量小文件随机写入时性能会掉得很厉害。这点我在后面的性能实测里会再展开。2. 卡协议不是黑魔法命令、寄存器与初始化流程2.1 两种工作模式怎么选SD 卡支持两种通信模式SD 总线模式和 SPI 模式。SD 总线模式是原生模式支持 1 位或 4 位并行数据传输最高时钟可达 50MHz吞吐量可以做到几十 MB/sSPI 模式则是为了兼容没有 SD 总线控制器的主控芯片数据只能逐位传输最高时钟一般也只能到 20MHz 左右实际吞吐量差一个数量级。那什么时候用 SPI 模式如果项目只是记录日志、配置文件、小批量数据对速率不敏感而 MCU 刚好有 SPI 外设那 SPI 模式是最省事的方案。所有 MicroPython 开发板基本都支持 SPI不需要特殊驱动。什么时候用 SD 模式如果需要跑摄像头连拍、音频录制、高速数据采集这种持续大数据量写入SPI 模式的速率大概率撑不住这时要么用带 SDIO 接口的 MCU要么用 FPGA 去读 SD 卡或者用支持 SD 总线模式的单片机库。另外要注意SD 总线模式需要额外的上拉电阻和更规范的 PCB 布局SPI 模式对时序的要求相对宽松更适合手工搭线和快速原型验证。我们这篇文章后面讲 MicroPython 驱动用的就是 SPI 模式这也是 MicroPython 生态里最通用的做法。2.2 几张重要的寄存器表SD 卡内部有几组寄存器主机通过命令读取初始化时尤其依赖它们寄存器全称主要作用OCROperation Conditions Register记录卡支持的电压范围、是否上电完成CIDCard Identification Register卡的唯一标识包含厂商、产品名、序列号CSDCard Specific Data Register卡的具体参数如容量、最大传输速率、块大小SCRSD Configuration Register卡支持的协议版本、安全能力和命令集CSD 寄存器特别需要理解。不同容量规格的卡CSD 的结构不一样。SDSC 卡的容量字段是 C_SIZE、C_SIZE_MULT、READ_BL_LEN 三个字段的组合计算而 SDHC/SDXC 卡的 CSD 结构更简单C_SIZE 字段直接跟块数挂钩然后用 C_SIZE 1 再乘 512KB 就能算出总容量。这也是为什么老驱动程序在识别高容量卡时经常出问题的原因它可能还在用旧公式计算容量。OCR 寄存器里的 BUSY 位bit31在初始化 ACMD41 轮询时很有用卡在完成上电初始化前 BUSY0完成后才返回 1。主机的初始化代码一般就是循环发送 ACMD41直到读到 BUSY1 才认为卡初始化完成。2.3 最关键的初始化序列在 MicroPython 里手写 SD 卡驱动前先把初始化序列理清楚。整个过程在 SPI 模式下通常是这样的第一步上电后主机要先拉低 CS然后发送至少 74 个时钟脉冲给卡内部一个稳定的上电状态和时钟同步过程。这一步很多人容易忽略直接发 CMD0 会导致初始化不稳定。第二步发送 CMD0GO_IDLE_STATE把卡复位到空闲状态。SPI 模式下CMD0 的 CRC 字段必须正确发送因为此时卡还没有进入 SPI 模式仍按照 SD 模式期待着正确 CRC。这也是 SD 卡 SPI 协议里少数几个必须校验 CRC 的命令之一。第三步发送 CMD8SEND_IF_COND用来确认卡的工作电压范围。SDHC/SDXC 卡支持这条路卡收到 CMD8 后会把接口条件原样返回。如果卡不支持 CMD8会返回非法命令错误说明它是一张老式 SDSC 卡。第四步循环发送 CMD55 ACMD41这是真正的初始化命令。ACMD41 的参数里要设置 HCS 位即给参数的最高 bit30 置 1表明主机支持高容量卡。卡在初始化完成前会返回忙状态主机要不断重发 CMD55/ACMD41直到卡的回应里出现 READY 标志。第五步用 CMD58 读取 OCR 寄存器确认卡是否支持当前电压同时检查 CCS 位OCR bit30。CCS1 说明是高容量卡后续用块地址CCS0 是标准容量卡用字节地址但实际编程时统一按块地址处理也没问题。完成初始化后SPI 时钟可以从初始的 400kHz 提高到 10MHz 甚至更高再通过 CMD17 读单块、CMD24 写单块操作数据。这套流程在几乎所有 SD 卡上都是通用的自己心里有数后再看开源驱动代码就会非常顺畅。2.4 单块读写命令与应答机制SD 卡的 SPI 模式命令包固定是 6 个字节。第一个字节高位是 0 和传输位 1低 6 位是命令索引接下来 4 个字节是参数大端序最后一个字节是 CRC 和结束位。实际操作时MicroPython 的 sdcard.py 驱动里写得很清楚发送 buf[0] 0x40 | cmd后面的参数按命令填CRC 通常只在 CMD0 和 CMD8 时需要认真计算其余命令在 SPI 模式下卡不检查 CRC。卡的应答分好几种SPI 模式下最常见的是 R1 类型单字节返回bit0 为 1 表示卡处于空闲状态bit1 到 bit6 分别对应各类错误位。初始化时发完 CMD0 后回应的 R1 应该是 0x01表示进入 idle 状态ACMD41 轮询过程中R1 的 bit0 会一直为 1直到初始化完成后变为 0此时 R1 就是 0x00表示就绪且无错误。单块读用 CMD17参数是块地址单块写用 CMD24参数同样是块地址。写命令发出后主机还要等待卡返回一个数据响应令牌然后等待卡完成内部写操作busy 信号数据线被拉低直到写完。如果没有等待 busy 信号就立刻发下一条命令最典型的故障就是写着写着卡变得没有响应或者数据错乱。这一块在调试时是要重点盯住的。3. 电路设计给 SD 卡一个稳定可靠的家3.1 供电电路LDO 和 DC-DC 都能用但要分清场景SD 卡标称工作电压是 2.7V 到 3.6V典型值 3.3V。看起来要求不严格但实际对电压瞬态跌落非常敏感。卡在写数据时内部 Flash 编程电流会突然拉高如果供电无法及时补偿电压跌落超出范围轻则写数据出错重则整个卡固件死机。供电方案选型上有个原则如果系统主电源就是 5V 且电流富余用一个 AMS1117-3.3 这类 LDO 是最稳的成本低、纹波小只要注意输入输出压差dropout voltage别太大就行。如果系统是从 12V 或 24V 母线降压到 3.3V那就得用 DC-DC Buck 电路热词里大家常问的 buck 电路、boost 电路就在这个场景出现。Buck 转换效率高但输出纹波比 LDO 大特别是开关纹波如果耦合到 SD 卡电源线上会直接影响高速信号眼图质量。解决办法是在 Buck 输出后面加一级 LC π 型滤波或者干脆 Buck 降到 3.6V 再接一个低 dropout 的 LDO 到 3.3V两全其美。我实测过一个教训某次用 Buck 直接给 SD 卡供电空载初始化一切正常一写大文件就随机报错用示波器量 VDD 发现写数据瞬间有 200mV 的周期性跌落频率跟 Buck 开关频率一致。后来在 SD 卡座电源引脚旁边加了 100nF 陶瓷电容和 10uF 钽电容再把 Buck 的反馈采样点尽量靠近负载端问题才消失。3.2 上拉电阻、滤波电容与信号完整性SD 卡电路设计里上拉电阻是另一个容易忽略的点。SD 总线模式下CMD、DAT0-DAT3 必须通过上拉电阻拉到 VDD阻值推荐 10kΩ 到 47kΩSPI 模式下CS对应 DAT3、MOSI对应 CMD和 CLK 也应该上拉防止引脚悬空导致卡误判状态。上拉电阻的作用不仅仅是电平稳定还保证 SD 卡在初始化阶段能正确识别总线上没有数据的空闲状态。滤波电容也不能省。SD 卡座旁边至少要有 100nF 的陶瓷电容靠近 VDD 引脚放置如果空间允许再加一颗 10uF 的电容处理大电流瞬态。另外在卡座处加一个小的磁珠或者串联电阻几十欧姆做 EMI 防护对过 EMC 测试会有帮助。静电防护方面如果设备需要频繁插拔 SD 卡建议在数据线上加 TVS 二极管阵列不然静电打坏主控引脚或者卡的例子并不少见。信号完整性上CLK 信号应该尽量短走线避免跟电源线长距离平行如果是双层板CLK 和 DAT 线的参考平面一定要完整不要跨分割区。高速模式下数据线和时钟线之间的距离也要保持一致减少相对延迟。这些点做好后SPI 时钟就能稳定跑得更高SD 总线模式下更是决定成败的关键。3.3 一张可直接抄作业的参考电路下面这张表是我项目里常用的参考连接方式以带 SPI 接口的 MCU 为例SD 卡引脚功能接到 MCU附加元件VDD3.3V 电源3.3V100nF 10uF 去耦电容VSS地GND接地CLKSPI 时钟SPI_SCK10kΩ 上拉到 3.3VCMD/DAT0SPI MOSISPI_MOSI10kΩ 上拉到 3.3VDAT1SPI MISOSPI_MISO可上拉也可不加DAT3/CSSPI 片选任意 GPIO10kΩ 上拉到 3.3VCD卡检测GPIO 输入10kΩ 上拉WP写保护GPIO 输入10kΩ 上拉 100nF 电容注意卡检测 CD 和写保护 WP 这两个引脚很容易被忽略。很多卡座里这两个脚是断开的必须靠外部上拉才能读到确定电平如果电路里直接悬空不接读到的内容就是浮空随机的。我之前遇到SD卡没锁但是写保护的问题查到最后就是 WP 引脚没有接上拉MCU 读到低电平误判为写保护开启。3.4 布线与抗干扰的几点实测经验布线的时候还有几个细节我从实际项目里总结出来。第一SD 卡座的卡检测信号如果直接给中断引脚用插拔瞬间会有机械抖动最好加一个 100nF 电容加软件消抖不然系统会反复触发插卡/拔卡中断这在热词里的按键保护电路和防抖电路讨论中是同一个原理。电容时间常数选 1ms 到 10ms 之间比较合理太大会影响插拔响应太小又滤不掉抖动。第二如果系统里还有其他大电流外设比如电机驱动、继电器一定要把 SD 卡供电和这些负载的电源在源头分开至少不要让 SD 卡 3.3V 跟继电器共用一根长走线。我遇到过一次非常诡异的问题继电器一吸合SD 卡上的时间戳数据就丢几条抓波形才发现吸合瞬间电源线上出现接近 1V 的毛刺。后来在继电器驱动端加了续流二极管并单独拉了一路 3.3V 给 SD 卡系统问题彻底消失。第三高速布线时不要小看 CLK 信号。有些同学把 SPI 时钟线跟 MOSI 线并列走了好几厘米结果在 20MHz 时出现回勾和过冲还以为是卡的问题实际上是串扰导致的。把两根线拉开距离、中间加地线隔离问题就消失了。如果示波器能看到明显的过冲振铃可以适当降低 SPI 速率或者串联一个几十欧姆的电阻在时钟线输出端用来抑制反射。4. MicroPython 驱动从零手写到底层核心解析4.1 准备工作与硬件连接MicroPython 生态里有很多开发板可以跑 SD 卡不同板子的差异主要体现在是否能直接用现成驱动。ESP32、ESP8266 这类板子通常需要使用社区通用的sdcard.py驱动文件而像 STM32 的某些 MicroPython 固件已经内置了machine.SD类可以直接调用。Raspberry Pi PicoRP2040没有硬件 SDIO只能走 SPI 模式也需要用sdcard.py。硬件连接上SPI 模式的四根线接法是固定的SCK 接 SPI 时钟MOSI 接卡的数据输入CMD 引脚MISO 接卡的数据输出DAT0 引脚CS 接任意 GPIO。这里有个细节SD 卡 SPI 模式的工作时序通常要求 CPOL0、CPHA0空闲时钟为低、上升沿采样但如果某些读卡器或者转接板设计不标准可能需要换成 CPOL1、CPHA0 才能正常通信。遇到初始化失败时多试试极性组合是排查方法之一。4.2 初始化、读单块、写单块核心逻辑看一下常见sdcard.py驱动的核心逻辑本质上就是我前面讲的协议要点。初始化函数里第一步先拉低 CS、发送一系列空时钟然后发 CMD0 进入 SPI 模式接着发 CMD8 探测高容量支持再通过 CMD55ACMD41 循环等待卡就绪。这个过程如果用代码来写大概是这样的def _init_card(self, cs): self.cs_high() self._clock(80) # 发送至少 80 个空闲时钟 self.cmd(0, 0, 0x95) # CMD0CRC0x95 self.cmd(8, 0x1AA, 0x87) # CMD8检测电平支持 while True: r self.cmd(55, 0, 0) # CMD55 前缀 if r 1: raise OSError(SD card init failed) r self.cmd(41, 0x40000000, 0) # ACMD41HCS1 if r 0: break读单块的核心是发送 CMD17然后等待数据令牌 0xFE。下面的代码从卡里读一个 512 字节的块def read_block(self, block_num): self.cs_low() self.cmd(17, block_num, 0) # 发送 CMD17 READ_SINGLE_BLOCK while True: token self._read_byte() if token 0xFE: # 数据起始令牌 data self._read_bytes(512) self._read_bytes(2) # CRC 忽略 break if token 0x00: # 数据错误 raise OSError(read error) self.cs_high() return data写单块则是发 CMD24然后主机发送数据起始令牌 0xFE 512 字节数据 2 字节 CRC再等待卡返回数据响应字节和 busy 信号。def write_block(self, block_num, buf): self.cs_low() self.cmd(24, block_num, 0) # 发送 CMD24 WRITE_BLOCK self._write_byte(0xFE) # 数据起始令牌 self._write_bytes(buf) self._write_bytes(b\x00\x00) # 伪 CRC resp self._read_byte() # 数据响应 while self._read_byte() ! 0xFF: # 等待忙信号结束 pass self.cs_high()写这块代码时最容易犯的错误就是漏掉忙信号等待。有些卡写入时间比较长如果没等忙信号结束就发下一条命令卡会把后续命令当成写入数据导致数据完全错乱。4.3 完整驱动代码与调用示例在 MicroPython 环境下的完整调用示例是这样的。先准备 SPI 和片选引脚import machine import sdcard import os # 初始化 SPI速率先用 400kHz初始化完成后可提高 spi machine.SPI(1, baudrate400000, polarity0, phase0, sckmachine.Pin(18), mosimachine.Pin(23), misomachine.Pin(19)) cs machine.Pin(5, machine.Pin.OUT) # 挂载 SD 卡 sd sdcard.SDCard(spi, cs) os.mount(sd, /sd) print(SD card mounted)如果你的固件里没有内置 sdcard 模块直接把sdcard.py放到开发板的文件系统根目录或者用ampy这类工具上传到板子上然后 import 就行。挂载成功后就可以像普通文件系统一样用了。4.4 驱动调优的实测建议调优方面我给出几条实测经验。第一初始化时用 400kHz 低速是为了兼容性初始化完成后可以把spi.baudrate提高到 10MHz 甚至 20MHz。多数情况下 10MHz 是非常稳的20MHz 要看布线和卡质量如果出现随机读错误就回退到 10MHz。第二SPI 模式下的 MISO 数据线可以不加外部上拉但有些卡在掉电状态下 MISO 会出现高阻态如果主控内部上拉不够强读数可能飘。稳妥起见MISO 也加一个 10kΩ 上拉电阻。第三MicroPython 里sdcard.py驱动默认使用 512 字节的块大小常用卡都是这个规格不需要修改。如果驱动中读写的缓冲区分配在函数内部频繁调用会反复申请内存导致碎片化效率不高。可以考虑外部传入缓冲区或者自己复制一个驱动修改成静态缓冲区版本。5. 文件系统挂载让数据真正存得进、读得出5.1 文件系统选型FAT16、FAT32 还是 exFATSD 卡出厂自带文件系统开发者通常直接使用。但这里面的坑相当多我详细讲讲。FAT16 支持最大 2GB 分区块大小可以是 512B 到 32KB一般用于小容量 SD 卡FAT32 支持 2GB 到 32GB 的分区是 SDHC 卡的标准格式exFAT 主要用于 SDXC 卡64GB 及以上支持超大文件和分区但 MicroPython 的 esp32 固件默认不带 exFAT 支持因为底层的 FatFs 编译选项没开。很多用户插上 64GB 卡发现识别不了十有八九就是 exFAT 的问题。我的建议是在 MicroPython 平台上32GB 及以下的卡直接格式化成 FAT3216GB 以下用 FAT32 也没问题如果非要上 64GB 的卡先确认固件是否支持 exFAT否则老老实实用 32GB 卡或者把 64GB 卡重新分区格式化成 FAT32Windows 下标准格式化工具可能不给选项需要用第三方分区工具或者 SD Card Formatter 的特殊选项。从稳定性和兼容性角度MCU 场景并不推荐非用超大容量 SD 卡不可毕竟 32GB 存数据日志已经非常充裕了。5.2 os.mount 挂载流程MicroPython 的虚拟文件系统VFS机制类似 Linux 的挂载理念用os.mount把 SD 卡挂载到指定路径下。底层支持 FAT 文件系统底层代码就是 FatFs 的移植。挂载时要注意一次只能挂一个文件系统到同一个路径而且卸载前要确保没有打开的文件句柄。实际挂载代码就是这样import os os.mount(sd, /sd, readonlyFalse)挂载完成后SD 卡上的目录就可以直接用/sd开头访问了。有个细节值得注意MicroPython 里os.mount挂载后默认根路径/还是板子的内部 Flash所以建议统一用/sd作为 SD 卡根路径这样路径可读性好也不容易跟内部文件系统搞混。如果 SD 卡没有格式化过或者格式不是 FAT 系列挂载时会抛出OSError: Invalid filesystem之类的错误。此时开发者需要在电脑上格式化 SD 卡为 FAT32再插入板子重新挂载。这个坑特别常见尤其是新买的卡出厂可能是 exFAT。5.3 写入与读取的实际代码挂载完成后的数据操作就跟普通文件系统一模一样# 写入数据 with open(/sd/data.txt, w) as f: f.write(hello sd card\n) # 追加写入 with open(/sd/data.txt, a) as f: f.write(append line\n) # 读取数据 with open(/sd/data.txt, r) as f: content f.read() print(content) # 创建目录 os.mkdir(/sd/logs) # 目录列表 print(os.listdir(/sd))写入二进制数据同样支持直接用f.write(bytes_data)即可。对于日志类应用建议以追加模式打开文件写一条数据后立即关闭虽然效率低一些但能保证每条日志独立落盘系统断电也不会把缓冲数据全丢掉。如果要写大数据块比如采集一批传感器数据后再写入文件可以先把数据累积到 bytearray然后用f.write(buf)一次性写入比逐字节写入快很多。实际项目中我一般会在内存里攒 1KB 到 4KB 再批量写入配合 10MHz SPI写入速度可以稳定在几百 KB/s 级别对大多数记录类应用绰绰有余。5.4 性能和寿命的几点参考性能上SPI 模式下 10MHz 时钟的理论极限大约是 1.25MB/s实际上因为命令开销、忙等待、FatFs 文件系统管理开销实测连续写入大概在 300KB/s 到 700KB/s 之间。20MHz 时钟下可以再高一些但未必能翻倍瓶颈往往在卡的写 Flash 速度和 SPI 命令交互往返上。寿命上SD 卡内部主控的磨损均衡算法比较成熟普通日志写入场景可以用很久。但要注意下面几点不要频繁创建和删除小文件会加重磨损尽量避免在写入过程中断电虽然 FatFs 断电恢复能力尚可但日志尾部出现半条记录是常事重要数据建议每次写入后用f.flush()强制同步或者做双buffer交替写入一份损坏还有备份可用。另一个技术点是整卡镜像。有热词提到64G sd卡系统镜像img文件下载这通常是在树莓派这类 Linux 单板电脑上用 SD 卡做系统启动盘的场景。把 SD 卡做成 img 镜像在 Linux 下用dd、Windows 下用 Win32DiskImager 直接读整卡生成镜像然后量产时再烧录到多张卡上。MCU 项目里同样可以把包含日志程序、配置文件的 SD 卡做成模板镜像节省现场配置时间。不过要注意批量拷贝时最好在写入完成后重新挂载一次做校验避免 Dirty Bit 残留导致文件系统异常。6. 常见问题与排查技巧实录6.1 典型故障速查表把这段时间踩过的坑整理成表格方便大家直接对照排查。现象可能原因排查顺序与解决思路初始化超时CMD0 无响应SPI 极性相位不对、CS 没拉低、电源不稳定先用示波器看 SPI 波形尝试 CPOL/CPHA 组合检查 CS 是否正常测量 3.3V 电压ACMD41 一直返回非 0电压不匹配、卡损坏、SPI 速率过高降低 SPI 时钟到 100kHz 以下换卡验证检查 CMD8 返回是否正确区分 SDSC/SDHC挂载时报 Invalid filesystem卡是 exFAT 或未格式化电脑上重新格式化 FAT32小容量卡选 FAT16写入后读回来数据不对忙信号没等、SPI 速率过高、供电跌落检查写命令后是否等待 busy降低速率检查供电电容提示写保护无法写入但写保护开关明明没锁WP 检测引脚悬空或电平误判检查 WP 引脚是否接上拉测量电平软件里把该引脚配置为输入下拉或上拉并再次读取文件系统目录项丢失或乱码写入过程中断电、劣质卡掉数据用f.flush()强制同步重要数据做冗余备份换质量可靠的品牌卡读取速度远低于预期用了劣质扩容卡、SPI 本就受限用 H2testw 或f3验证卡的真实容量和速度换 Class 10 / A1 卡微型卡座插拔后系统死机卡检测抖动、热插拔瞬间电平不稳软件加消抖CD 脚加 RC 滤波避免带电热插拔尽量断电操作6.2 几个容易忽略的细节坑第一个细节是os.mount失败后的重挂载。如果 SD 卡在运行过程中被拔掉MicroPython 的 VFS 状态可能变得混乱直接再插上调用os.mount可能报错。稳妥做法是用try/except捕获异常然后执行os.umount(/sd)再重新挂载。设计上尽量避免热插拔这符合 SD 卡规范的实际使用场景。第二个细节是格式化工具的选择。Windows 自带的格式化工具在制作大容量 FAT32 时可能会受限推荐用 SD 卡官方组织发布的 SD Card Formatter 工具它会把卡的分区结构、引导区、文件系统参数设置得最标准能减少不少莫名其妙的兼容性问题。如果你在开发板上老是挂载失败换这个工具重新格式化往往能直接解决问题。第三个细节是供电。前面多次提到 SD 卡对供电的敏感性这里再强调一次如果系统里有显示屏、无线模块、电机等大电流外设SD 卡写入瞬间的电压跌落很可能让这些外设也跟着出现异常。排查这类问题最好的工具是示波器没有示波器的话可以尝试在 SD 卡供电处并联一颗较大的电解电容比如 100uF做储能补偿也能有效缓解。第四个细节是热词里出现的fpga读取sd卡bmg这类场景。FPGA 读取 SD 卡的原理跟 MCU 类似也是实现 SD/SPI 协议状态机但因为 FPGA 没有现成的 FatFs 这类文件系统库通常会用 Verilog 实现底层协议然后配合软核或者直接用开源 IP 读整块扇区。这个方向比 MCU 复杂不少如果只是想用 FPGA 高速读取 SD 卡数据建议先熟悉本篇文章里面的协议细节再用状态机去实现 CMD 发送和应答解析会顺很多。6.3 最后再说点个人体会做 SD 卡开发这么久最大的体会是SD 卡看起来简单真出问题的时候问题往往不在卡本身而在供电、时序、电平这几个外围环节。很多朋友一上来就怀疑卡坏了然后换卡、换型号实际上问题出在电路上。所以调试的顺序建议是先量供电再看波形最后怀疑协议代码。顺序反了容易把自己绕进死胡同。另外就是别贪图高速率。SPI 模式下能稳定跑 10MHz 已经能满足大多数数据记录需求非要上 20MHz 甚至更高得先把 PCB 布线和信号质量做好。稳定性永远比峰值性能重要特别是在无人值守的设备上跑一个月数据不丢比跑一天速度快一倍有价值得多。最后分享一个小建议给 SD 卡做读写操作时写完重要数据后顺手读回来校验一下。成本不高但能用很简单的逻辑避免很多隐性问题。尤其是批量出货的项目每台设备开机自检时做一次写入回读测试能提前发现大量潜在故障这比售后返修省心太多了。