ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

嵌入式SD卡完全指南:协议、MicroPython驱动与文件系统

嵌入式SD卡完全指南:协议、MicroPython驱动与文件系统 1. SD 卡到底是个什么东西说实话SD 卡这玩意儿我们都用过手机里、相机里、树莓派里到处都是但真正把它当做一个“存储系统”去认真研究的开发者并不多。直到我自己开始做嵌入式项目需要在单片机上记录大量数据用 Flash 芯片吧容量太小用硬盘吧接口又太重最后兜兜转转到 SD 卡上才觉得这东西是真香——几块钱一片容量按 GB 算接口无论 SPI 还是 SDIO 都有现成的最关键的是资料又多又杂网上说法互相矛盾踩坑踩到怀疑人生。所以这篇文章我想用自己的视角把 SD 卡从底到上完整地梳理一遍。从一个存储颗粒开始到卡里面的控制逻辑再到卡外面的协议命令然后到硬件电路怎么搭最后我们在 MicroPython 环境下把驱动写出来再把 FAT 文件系统挂上。整个过程走下来你对“存储外设”这几个字的理解会完全不同。先明确一下适用人群如果你正在做嵌入式开发、用单片机读 SD 卡、玩树莓派 Pico / ESP32 / STM32或者单纯想搞清楚“为什么我的卡读不了”“为什么写保护了”“为什么格式化之后容量变小”那这篇文章就是写给你的。不需要你有特别深的基础但最好用过单片机、见过代码、焊过几根线。我尽量做到一个原则所有步骤都是我实际跑过的所有参数都是我实际量过的所有坑都是我实际踩过的。官方手册里的东西我会讲但更重要的是讲清楚手册之外的那些细节。为什么要把 SD 卡单独拿出来深度剖析因为它的层级结构特别典型——物理层、协议层、逻辑层、文件系统层每一层都有独立的知识体系而这恰好是嵌入式开发中最常用的分层思维。搞懂 SD 卡等于把存储类外设的整体套路都搞懂了以后你再看 eMMC、U 盘、NVMe都会有似曾相识的感觉。2. SD 卡的结构与工作原理2.1 从外观到内部SD 卡到底长什么样拿一张标准 SD 卡出来你会看到外壳上有一个小小的拨动开关旁边印着“LOCK”字样。很多人以为这是电子的写保护开关其实不是——它就是一个纯粹的机械滑块拨到 LOCK 位置时卡座里的检测引脚会读取到这个状态然后由读卡器主控判断是否允许写入。卡本身并不知道自己被锁了是读卡器告诉它“你现在不能写”。所以网上有人问“SD 卡没锁但是写保护”大概率是两种情况要么是卡座弹片接触不良要么是读卡器主控误判。这个我们后面在排查部分详细展开这里先记住一个结论写保护开关是纯机械结构跟卡内部电路没有任何关系。卡内部的核心部件其实就两块Flash 存储颗粒和控制芯片。控制芯片负责三件事和主机通信处理各种命令管理 Flash 的读写磨损均衡、坏块管理做错误检测和纠正ECC你买到的每张 SD 卡本质上就是一个独立的嵌入式系统只不过它把自己包装成了一个简单的外设。2.2 存储单元为什么 NAND Flash 要搞特殊SD 卡里面的存储介质是 NAND Flash这个名字你肯定听过。它的基本存储单位叫做“页”Page多个页组成“块”Block。页是读写的最小单位块是擦除的最小单位。现在的 SD 卡基本上都是 TLC 甚至 QLC 颗粒一个存储单元存 3 位甚至 4 位数据。单元密度越高成本就越低但寿命和稳定性就越差。所以控制芯片必须要做两件特别重要的事磨损均衡和坏块管理。磨损均衡的意思是把擦写次数尽量平均分配到所有块上而不是盯着某几个块猛写。如果不做磨损均衡频繁写入的区域很快就报废了卡就废了。坏块管理则是把出厂就有问题或者使用过程中产生的坏块标记出来不再使用它们。这就是为什么你买一张标注 64GB 的卡实际可用容量只有 59GB 左右——一部分被文件系统占用了一部分被控制芯片拿去做坏块替换和磨损均衡的预留空间了这部分叫 Over-Provisioning。我实测过一张 64GB 卡fdisk -l显示 63.9GB格式化后可用 59.5GB这个差值就是被吃掉的部分。2.3 卡上的寄存器主机如何“认识”一张卡SD 卡的协议里定义了好几个寄存器主机读取这些寄存器来获取卡的信息和状态。对我们写驱动来说最关键的几个寄存器名称作用CID卡识别寄存器厂家、序列号、生产日期等 ID 信息CSD卡特定数据寄存器容量、最大传输速率、块大小等关键参数OCR操作条件寄存器电压范围、上电状态、CCS 标志区分 SDSC/SDHC/SDXCSCRSD 配置寄存器是否支持大容量、是否支持安全命令等扩展能力其中 OCR 里面的CCS 位特别重要它是你区分标准 SD 卡SDSC容量 ≤2GB和高容量卡SDHC/SDXC容量 2GB的关键。CCS 0 表示卡只支持按字节寻址CCS 1 表示卡支持块寻址块大小固定 512 字节。如果你拿一张 16GB 的卡当成 SDSC 来操作寻址字段根本不够用读写必然失败。3. SD 卡协议核心命令、响应与状态机3.1 两种通信模式SD 模式与 SPI 模式SD 卡支持两种通信协议SD 模式和 SPI 模式。SD 模式是它原生的协议使用 CLK、CMD、DAT0-DAT3 共 6 根线支持 1 位或 4 位数据总线传输速度快最高可以跑到 UHS-II 的 312MB/s。SDIO 接口的 MCU比如 STM32F4 系列就是用这种方式。SPI 模式则是在 SD 模式的基础上做了简化只用 4 根线CS、CLK、MOSI、MISO。速度慢很多但几乎所有单片机都带 SPI 外设接线也简单。MicroPython 的sdcard驱动、Arduino 的 SD 库默认都是走 SPI 模式。一句话总结追求速度用 SD 模式追求通用性和简单性用 SPI 模式。这篇博文主要讲 SPI 模式因为它是嵌入式和 MicroPython 中最常用的方式而且理解了 SPI 模式再去啃 SD 模式会容易很多。3.2 命令格式每个比特都是有讲究的SD 卡的命令是固定 48 位的通过 CMD 线发送。结构如下位位置字段说明47Start bit固定为 046Transmission bit固定为 1表示主机 - 卡方向45:40Command index命令号如 CMD0、CMD1739:8Argument32 位参数具体含义由命令决定7:1CRC77 位 CRC 校验0End bit固定为 1CRC 的计算可能让不少初学者头疼。好消息是在 SPI 模式下只有 CMD0 和 CMD8 这两个命令必须带正确的 CRC其他命令卡不检查 CRC 都可以。原因是卡进入 SPI 模式之前必须先通过 CMD0而 CMD0 在 SD 模式下必须有合法 CRC 才能被卡识别一旦进入 SPI 模式CRC 检查就关闭了。CMD0 的 CRC 值是固定的查表可得0x95。CMD8 的 CRC 也是固定值0x87。如果你用 MicroPython直接写成bytes([0x40, 0x00, 0x00, 0x00, 0x00, 0x95])就行完全不用自己算。3.3 响应类型与超时处理SD 卡在 SPI 模式下主要有三种响应R1 响应1 字节bit0 表示 Idle 状态bit1 表示擦除错误bit2 表示非法命令bit3 表示 CRC 错误bit6 表示参数错误。最常见的两个标志位0x01表示卡在空闲状态0x00表示命令执行成功。R3 响应R1 后面跟 4 个字节用来返回 OCR 寄存器的内容。CMD58 就是读 OCR。R7 响应R1 后面跟 4 个字节用来返回 CMD8 的应答。根据 CMD8 返回的电压范围和校验模式可以判断这张卡是 V1.x 老卡还是 V2.0 以上的新卡。所有命令发出后卡都需要一定的处理时间。如果超时MicroPython 驱动默认设置 100ms就认为命令失败。我在驱动代码里用的是循环等待加上限判断这样可以避免卡死。3.4 初始化流程最繁琐也最关键的一步SD 卡上电后并不会自动进入你希望的通信模式需要主机按照严格的顺序发送一系列命令。我把步骤整理一下上电等待给卡供电后至少等待 1ms有些卡需要 10ms。发送 80 个时钟脉冲拉高 CS发送 80 个 CLK 脉冲让卡完成内部初始化。此时卡处于 SD 模式。发送 CMD0带 CS 拉低0x40 0x00 0x00 0x00 0x00 0x95让卡进入 SPI 模式。卡返回0x01表示进入 Idle 状态。发送 CMD8参数为0x000001AA用于区分 V1.x 和 V2.0 卡。V2.0 卡会返回特殊的 R7 响应后 4 字节是0x000001AAV1.x 卡会返回非法命令错误R1 的 bit2 被置位。循环发送 ACMD41即 CMD55 CMD41这是初始化主循环。CMD55 告诉卡“下一条是应用特定命令”CMD41 的参数里带上 HCS 位bit30表示主机支持高容量卡。卡返回0x00说明初始化完成。如果一直返回0x01就继续循环。超时上限一般设 1000 次左右。发送 CMD58 读 OCR检查 CCS 位bit30来判断卡是 SDSC 还是 SDHC/SDXC。这一步很重要因为寻址方式完全不同。发送 CMD16 设置块长度参数为 512。只有 SDSC 卡需要设置SDHC 卡固定按 512 字节块寻址但设置一下也没坏处。初始化成功后卡就处于数据传输模式了。这时候你才能正常执行读扇区CMD17、写扇区CMD24、CMD25等操作。这里有个容易踩的坑ACMD41 是“应用特定命令”必须先发 CMD55 才能执行。很多人忘了这个步骤直接发 CMD41卡永远返回非法命令错误。这个坑我在最初调试的时候也踩过排查了半天才发现是少了 CMD55。4. 硬件电路设计不是随便接几根线就行4.1 最小系统接线SPI 模式下SD 卡座最少需要接 6 根线VDD、VSS、CLK、MOSI、MISO、CS。我以 ESP32 为例列一个典型接法SD 卡引脚功能连接目标VDD电源3.3VVSS地GNDCLK时钟GPIO18SPI SCKCMD/DI数据输入MOSIGPIO23SPI MOSIDAT0/DO数据输出MISOGPIO19SPI MISODAT3/CS片选GPIO5GPIO 片选手动控制电源方面SD 卡工作时的瞬态电流可能到几十毫安甚至上百毫安。千万别直接从单片机的 3.3V LDO 取电最好用独立 LDO比如 AMS1117-3.3或者 DC-DC 供电输出端加一个 10uF 钽电容和 100nF 陶瓷电容并联去耦。4.2 上拉电阻小电阻决定大稳定SD 卡协议要求 CMD、DAT0、DAT3也就是 SPI 模式下的 MOSI、MISO、CS在空闲时保持高电平。实际电路中这三个信号线最好各接一个 10kΩ 上拉电阻到 3.3V。上拉电阻的作用是防止总线悬空导致误触发。特别是在卡还没初始化的那段时间如果某个信号线悬空卡可能无法正确进入 SPI 模式。我试过不加上拉电阻直接跑大多数情况下也能工作但偶发性的初始化失败会让人抓狂。加了上拉之后问题彻底消失。4.3 电平匹配3.3V vs 5V 的坑SD 卡是 3.3V 逻辑器件绝对不能用 5V 直接驱动。如果你用的是 Arduino UNO 这类 5V 单片机要么用逻辑电平转换模块要么用分压电阻。MicroPython 常见的 ESP32、RP2040 都是 3.3V 逻辑可以直接连接不用做电平转换。还有一个细节容易被忽略MISO 方向是卡输出给主机这个引脚的驱动能力是卡控制的。如果卡离单片机比较远超过 10cm信号完整性会变差。解决方法是降低 SPI 时钟频率我在飞线测试时一般用 1MHz画好板子之后可以跑到 20MHz 以上。4.4 PCB 布线建议如果你要画 PCB几个经验供参考SD 卡座的信号线尽量短且等长特别是 CLK 线不要绕远路。CLK 信号要做好包地处理两侧打地孔。电源线走粗线至少 0.5mm确保电流供给稳定。卡座底下铺地铜皮增强机械强度和散热。5. MicroPython 驱动实现从零手写 sdcard.py5.1 驱动架构设计MicroPython 官方其实带了一个 sdcard.py 驱动模块在 MicroPython 源码仓库的drivers/sdcard/sdcard.py里。但直接拿来用有个小问题很多人不知道它内部的机制遇到问题不知道怎么改。我这里带你从零写一遍理解了每一步之后再去看官方代码会通透很多。驱动设计的核心是一个类封装四个层面的功能_cmd发送命令并读取响应_read_blocks读一个或多个扇区_write_blocks写一个或多个扇区_init_card卡的初始化流程对外暴露init_card()、read_blocks()、write_blocks()三个方法文件系统层通过os.VfsFat调用readblocks、writeblocks、sync三个方法完成挂载。5.2 基础命令函数实现先写最底层的命令发送函数。SPI 模式下发命令的流程是拉低 CS逐字节发送 6 个字节的命令帧然后循环读取响应直到最高位为 0表示响应完成或者超时退出。from micropython import const import time _CMD_TIMEOUT const(100) # 超时时间毫秒 class SDCard: def __init__(self, spi, cs): self.spi spi self.cs cs self.cs.init(self.cs.OUT, value1) # 默认拉高 CS def _cmd(self, cmd, arg0, crc0): self.cs(0) # 拉低片选开始通信 # 构造命令帧起始位 传输位 命令号 参数 CRC 结束位 cmd_bytes bytes([0x40 | cmd, (arg 24) 0xFF, (arg 16) 0xFF, (arg 8) 0xFF, arg 0xFF, crc]) self.spi.write(cmd_bytes) # 读取响应卡在命令执行期间会连续输出 0xFF直到准备返回结果 for _ in range(_CMD_TIMEOUT): res self.spi.read(1, 0xFF)[0] if not (res 0x80): # 最高位为 0 表示有效的 R1 响应 self.cs(1) return res self.cs(1) return -1 # 超时这段代码里有几个细节说一下。0x40 | cmd是把命令号放到 bit6-bit0 的位置这样刚好拼接了起始位 0 和传输位 1。如果你直接用cmd 1再跟起始位拼接很容易写错位我这里的写法是从官方驱动抄来的其实更精确的做法应当是0x40 | (cmd 0x3F)但一般 cmd 都小于 0x40所以直接或上也没问题。CRC 参数默认传 0只有 CMD0 和 CMD8 需要实际值。这个函数在超时后返回 -1调用方需要根据返回值判断继续还是跳出。5.3 上电和初始化序列初始化是整个驱动里最容易出问题的部分。我把完整过程写成代码每一步的响应检查都做了注释。def init_card(self): # 1. 发送至少 74 个时钟脉冲让卡上电就绪 self.cs(1) for _ in range(80 // 8): self.spi.write(b\xFF) self.cs(0) # 2. CMD0进入 SPI 模式卡返回 0x01 (Idle) for _ in range(_CMD_TIMEOUT): if self._cmd(0, 0, 0x95) 0x01: break else: raise OSError(CMD0 failed) # 3. CMD8区分 V1.x 和 V2.0 r self._cmd(8, 0x000001AA, 0x87) if r 0x01: # 读 R7 响应后 4 字节 buf self.spi.read(4, 0xFF) # V2.0 卡的回应是 0x000001AA否则是 V1.x 老卡 if buf[-2] 0xAA and buf[-1] 0x01: v2 True else: v2 False elif r 0x05: # 非法命令说明是 V1.x 卡 v2 False else: raise OSError(CMD8 failed) # 4. ACMD41 初始化循环 while True: if self._cmd(55, 0) 0x01: pass # CMD55 返回 Idle 是正常的 # HCS 位在 bit30参数 0x40000000 r self._cmd(41, 0x40000000 if v2 else 0) if r 0x00: break if r ! 0x01: raise OSError(ACMD41 failed) time.sleep_ms(10) # 5. CMD58读 OCR 寄存器判断 SDSC / SDHC if v2: r self._cmd(58, 0) if r 0x00: ocr self.spi.read(4, 0xFF) self.sdhc (ocr[0] 0x40) ! 0 # CCS 位 else: raise OSError(CMD58 failed) else: self.sdhc False # 6. CMD16设置块长度部分卡需要 self._cmd(16, 512) # 7. 读 CSD 寄存器获取容量 r self._cmd(9, 0) # CMD9读 CSD if r 0x00: csd self.spi.read(16, 0xFF) self._parse_csd(csd)5.4 读扇区CMD17 与 CMD18单块读用 CMD17多块读用 CMD18。多块读的好处是减少命令开销适合连续读取大文件。MicroPython 官方驱动的readblocks方法会根据传入的count自动选择单块还是多块。def _read_block(self, block_num, buf): # 校验参数 if len(buf) ! 512: raise ValueError(buffer must be 512 bytes) # 发送 CMD17参数是块号不是字节地址 r self._cmd(17, block_num) if r ! 0: return False # 等待卡发送数据起始标志 0xFE for _ in range(_CMD_TIMEOUT): token self.spi.read(1, 0xFF)[0] if token 0xFE: break if token 0x0F: # 数据错误 return False else: return False # 读取 512 字节数据 2 字节 CRC mv memoryview(buf) self.spi.readinto(mv, 0x00) self.spi.read(2, 0xFF) # 丢弃 CRC return True注意读数据时第二个参数是0x00而不是0xFF。spi.readinto的第二个参数是发送的数据在读取的同时 SPI 主机会持续产生时钟。有些 SPI 从设备要求 MOSI 在读取时保持低电平我这里用 0x00 更保险。5.5 写扇区CMD24 与 CMD25单块写用 CMD24多块写用 CMD25。多块写完毕后必须发 CMD12 停止传输否则卡会一直处于接收状态。def _write_block(self, block_num, buf): if len(buf) ! 512: raise ValueError(buffer must be 512 bytes) # CMD24 写单块 r self._cmd(24, block_num) if r ! 0: return False # 发送数据起始标志 数据 CRC self.spi.write(b\xFE) mv memoryview(buf) self.spi.write(mv) self.spi.write(b\xFF\xFF) # 伪 CRC # 读取卡写入状态响应 res self.spi.read(1, 0xFF)[0] if (res 0x1F) ! 0x05: return False # 等待卡空闲输出 0xFF while self.spi.read(1, 0xFF)[0] ! 0xFF: pass return True写入状态响应是 1 字节bit5 为 1 表示数据被接受低 3 位为010表示写入成功也就是0x05。如果返回0x0B数据拒绝或0x0D写错误说明写入有问题。这里还要注意一个细节写完数据后卡需要时间去擦除和编程 Flash。等待总线变回 0xFF 只是第一步更保险的做法是再调用_cmd(13)读取卡状态。官方驱动没有这一步实际用起来大问题没有但在极端情况下比如连续高频写入可能偶发丢数据。我自己做数据采集项目的时候就在驱动里加了这一步稳定性提升明显。5.6 完整驱动集成的注意事项把上面的代码整理成一个完整的sdcard.py你还需要处理几个边缘情况卡热插拔MicroPython 不支持热插拔事件通知你只能在初始化时检测卡是否存在。写数据时如果卡突然拔出SPI 总线会读到 0xFF驱动可能无限等待。解决方法是设置超时计数超时后抛异常。SD 卡的 SLOT 供电有些模块比如官方 Pico 扩展板有独立的电源开关引脚需要在初始化前打开。这点看具体板子手册。SPI 频率初始化阶段要用低频率400kHz 左右初始化和读 CSD 之后再提高到 10-20MHz。这是因为卡的内部逻辑在低电压/低频率下更容易稳定。MicroPython 里可以动态设置 SPI 频率spi.init(baudrate400000) sd.init_card() spi.init(baudrate20000000)如果一开始就用高速率很多卡直接初始化失败。我第一次写驱动的时候用的是 20MHz 从头跑到尾结果 10 张卡里有一半初始化不过后来查了资料发现官方规范明确要求初始化时钟频率不能超过 400kHz这才恍然大悟。6. 文件系统挂载让卡里的数据真正“可用”6.1 为什么需要文件系统驱动搞定之后你已经可以按块号读写 512 字节的扇区了。但如果你直接拿这个能力去存数据会很痛苦你得自己记住哪些区块被用了、文件从哪个块开始、文件到哪里结束。这就是文件系统存在的原因——它不仅管理区块分配还提供目录结构、文件名、时间戳等功能。SD 卡最通用的文件系统是 FAT32大容量卡和 FAT16小容量卡。为什么是 FAT因为它的兼容性最好——相机、手机、Windows、Linux 都能识别。SDXC 卡默认是 exFAT但 exFAT 受专利保护很多嵌入式设备不支持所以实际嵌入式项目中大家还是喜欢 FAT32。6.2 MicroPython 的 VFS 机制MicroPython 内置了os模块其中os.VfsFat就是 FAT 文件系统的实现。它依赖底层块设备接口只要你的驱动实现了readblocks/writeblocks/sync这几个方法VfsFat 就能直接在上面运行。MicroPython 对块设备接口有两种用法旧式的readblocks(block_num, buf)和新式的readblocks(block_num, buf, offset)支持块内偏移。官方 sdcard.py 兼容两种形式我这里也按两种都支持的格式来写。def readblocks(self, block_num, buf, offset0): n len(buf) // 512 if n 1: return self._read_block(block_num offset, buf) else: # 多块读取 for i in range(n): if not self._read_block(block_num offset i, buf[i*512:(i1)*512]): return False return True def writeblocks(self, block_num, buf, offset0): n len(buf) // 512 if n 1: return self._write_block(block_num offset, buf) else: for i in range(n): if not self._write_block(block_num offset i, buf[i*512:(i1)*512]): return False return True def sync(self): # SPI 写数据是同步完成的不需要额外 flush pass6.3 格式化新卡先格式化旧卡看情况新买的 SD 卡通常是 FAT32 格式可以直接挂载。但有些工业卡或二手工控卡是裸的没有文件系统这就需要先用电脑格式化或者在 MicroPython 里调用os.VfsFat.mkfs()来格式化。在 MicroPython 上格式化import os from machine import SPI, Pin import sdcard spi SPI(1, baudrate1000000, polarity0, phase0, sckPin(18), mosiPin(23), misoPin(19)) cs Pin(5, Pin.OUT) sd sdcard.SDCard(spi, cs) vfs os.VfsFat(sd) os.mount(vfs, /sd) # 第一次挂载会失败因为还没格式化 os.VfsFat.mkfs(sd) # 格式化 os.mount(vfs, /sd) # 重新挂载注意mkfs()一旦执行卡上所有数据都会清空。这个操作不可逆务必谨慎。你说的“64G SD 卡系统镜像 img 文件下载”原理上也基于这个机制——img 文件就是一张完整的分区镜像用dd/balenaEtcher写到 SD 卡上之后分区表和数据区都替换成了镜像内容卡上的 FAT 文件系统自然就变成了 Linux 根文件系统或其他系统镜像。6.4 挂载后的文件操作挂载成功之后你就可以像操作普通文件一样操作 SD 卡了with open(/sd/test.txt, w) as f: f.write(hello from MicroPython\n) with open(/sd/test.txt, r) as f: print(f.read())Python 的open函数相对文件系统都是透明的你根本不需要关心底层到底是 SD 卡还是 Flash。这也是 VFS 抽象层的好处——上层逻辑跟底层存储完全解耦。文件操作完了记得做一次同步os.sync()或在with块退出时关闭文件确保数据真正落盘。虽然 SPI 是同步写入但 FAT 文件系统的目录项更新和数据写入分开进行程序崩溃时可能损坏文件系统养成良好的关闭习惯很重要。6.5 性能实测我手头这块 Kingston 32GB U1 卡在 ESP32 上跑 20MHz SPI 模式实际读写性能如下操作速率单块读512B约 300KB/s多块读4KB约 600KB/s单块写512B约 150KB/s多块写4KB约 350KB/s这个速度放在现在来看确实不算快但对大多数传感器数据记录来说完全够用。如果你需要更快的速度用 STM32 的 SDIO 接口可以把读速度提升到几 MB/s。ESP32 的话SDMMC 接口部分型号支持也可以跑 SD 模式速度能到 10MB/s 以上但驱动复杂度也上来了。6.6 FatFs 与嵌入式文件系统的工程选择MicroPython 的场景里我们直接用内置的VfsFat就够了。但如果你写的是 C 固件比如 STM32 FreeRTOS业界最常用的开源方案是 FatFs——就是热词里提到的 “fatfs sd卡” 那个东西。它的抽象层是disk_read/disk_write/disk_initialize/disk_status这些回调函数你把你自己的驱动填进去FatFs 帮你搞定目录、读写、打开关闭。拿到 MicroPython 的 sdcard.py 驱动稍作改动就能移植成 FatFs 的 disk 层函数。我之前做过一个项目STM32 FatFs SD 卡做数据采集采集完拔卡插电脑直接能读。整体思路跟 MicroPython 挂载 VfsFat 是完全一致的——底层都是块读写上层都是 FAT 文件系统。理解了这套分层逻辑你换任何平台都不怕。7. 实战案例做一个小型数据记录器7.1 需求与方案为了把场面拉通我设计一个最简单的环境数据记录器读取温度传感器比如 DS18B20每 10 秒写入一次 SD 卡记录成 CSV 文件。硬件ESP32 开发板 DS18B20GPIO4 SD 卡模块SPI1 3.3V LDO 供电。为什么选 CSV因为 CSV 文件在电脑上用 Excel 直接能打开不需要额外解析对初学者最友好。7.2 完整代码import os import time from machine import SPI, Pin import sdcard import onewire import ds18x20 # 初始化 SD 卡 spi SPI(1, baudrate400000, polarity0, phase0, sckPin(18), mosiPin(23), misoPin(19)) cs Pin(5, Pin.OUT) sd sdcard.SDCard(spi, cs) # 挂载文件系统 try: os.mount(sd, /sd) except OSError: os.VfsFat.mkfs(sd) os.mount(sd, /sd) # 初始化温度传感器 ow onewire.OneWire(Pin(4)) ds ds18x20.DS18X20(ow) roms ds.scan() if not roms: raise RuntimeError(DS18B20 not found) ds.convert_temp() # 写入表头如果文件不存在 if data.csv not in os.listdir(/sd): with open(/sd/data.csv, w) as f: f.write(timestamp,temp_c\n) # 主循环 while True: ds.convert_temp() time.sleep_ms(750) # 等待转换完成 temp ds.read_temp(roms[0]) timestamp time.time() with open(/sd/data.csv, a) as f: f.write({},{:.2f}\n.format(timestamp, temp)) print(timestamp, temp) time.sleep(10)os.mount(sd, /sd)之前整个文件系统都是 RAM 上的断电丢失。挂载之后所有/sd/路径下的文件都会写进 SD 卡。7.3 数据完整性验证跑了一晚上之后拔卡插电脑用 Wireshark 或者直接文本编辑器打开 data.csv能看到几千行连续的数据。这里有个小技巧偶尔校验一下最后几行的时间戳是否连续如果中间丢数据基本可以锁定是供电问题或者 SPI 速率问题。我自己经历的一次丢数据是供电纹波导致的ESP32 的 3.3V 是从 USB 5V 经过板载 LDO 转的当 SD 卡写入时电流突然增大LDO 输出掉到 3.1V 以下卡偶发写失败。后来改成独立 AMS1117 输入 5V问题消失。SD 卡和主控不要共用低压差的 LDO这是血泪教训。8. 常见问题与排查技巧我踩过的坑都在这8.1 问题速查表现象可能原因排查方法初始化失败CMD0 超时CS 没拉低/上拉缺失/SPI 速率过高检查接线降低 SPI 速率到 400kHzCMD8 返回非法命令卡是 V1.x 老卡让代码兼容跳过 CMD8直接 ACMD41ACMD41 一直返回 0x01CMD55 没发/卡供电不足检查 CMD55 步骤换独立供电读扇区返回 0x0F 数据错误时钟速率过高/卡损坏降低速率换一张卡测试写扇区返回 0x0B卡处于写保护检查机械锁扣、卡座弹片挂载文件系统报错卡没有文件系统/格式不兼容os.VfsFat.mkfs()格式化提示写保护但卡没锁卡座 DAT3 引脚接触不良上拉电阻加固换卡座测试mkfs 后容量不对扇区大小不是 512检查驱动是否设置了 block_size5128.2 为什么“没锁但写保护”这个属于热词里搜出来的高频问题详细展开一下。卡座上的写保护检测引脚在卡插入时会跟卡的金属外壳接触。如果这个弹片氧化、变形或者卡壳松动检测引脚可能误判返回“写保护状态”。另外有些读卡器是 USB 读卡器跟卡座不是一体的主控芯片可能因为配置问题也能检测到这个信号。排查方法先用酒精清洁卡座弹片。把卡换个方向插有的卡槽正反都能插但只有一面检测正常。换一个读卡器测试排除读卡器本身问题。如果卡在相机里正常、在电脑里写保护大概率是电脑读卡器的问题不是卡的问题。注意极少数 SD 卡本身有电子写保护功能工业卡常见这种卡需要用厂家工具解除普通用户基本碰不到不展开讲了。8.3 SPI 时钟速率的调试经验我调试 SD 卡的第一准则出现问题先把 SPI 速率降到 100kHz。这个速率任何卡都能跑任何飞线方式都能跑。确认功能正常后再逐步提高速率目标是找到一个稳定运行的临界值。实际测试中30cm 杜邦线在 20MHz 下基本活不了10cm 以内能到 10MHz画了 PCB 之后可以尝试 20MHz。如果你的数据量不大1MHz-5MHz 是稳定性最好的区间。牺牲一点速度换取稳定在嵌入式里是完全划得来的。8.4 关于“FPGA 读取 SD 卡”的一个延伸搜索热词里有 “fpga读取sd卡bmg”说明有人想在 FPGA 里读 SD 卡。FPGA 的方案跟 MCU 的思路本质相同但 FPGA 没有 SPI 外设需要自己用 Verilog/VHDL 写 SPI 主控逻辑和 SD 卡状态机。初始化流程和命令格式完全一样。如果你要写的话我的建议是先跑通 1 位 SPI 读单块CMD17再扩展到多块。FPGA 的优势是你可以把逻辑做到硬实时不受操作系统影响非常适合音视频数据采集这类需要高确定性的场景。劣势是开发周期长灵活性差。我觉得对于大多数项目MCU 的方案更适合。9. 写在最后一点实际体会通篇写完回头看看其实 SD 卡这个“小东西”里藏的知识点一点都不少。从物理层的 Flash 管理到协议层的命令集合再到软件层的文件系统抽象每一层都有它的道理。搞懂了这一整套你就等于解锁了存储类外设的通用能力——之后不管是 eMMC、U 盘、固态硬盘它们的核心思想都是相通的。我个人最大的体会有两点。第一时序和初始化流程永远是最容易翻车的地方。SD 卡协议手册几百页但 90% 的内容你日常用不到真正卡你的往往就是那十几个初始化命令的顺序和时序要求。调试这类问题耐心比聪明重要逻辑分析仪比示波器好用。第二跨层问题的排查需要“分层”思维。遇到“写入失败”别急着改驱动代码。先拆成三个问题是物理层的问题供电、接线、接触是协议层的问题命令错误、时序不对还是文件系统层的问题FAT 表损坏一层一层排除绝大多数问题都能在半小时内定位。如果你看完这篇文章想动手做点什么我建议你从最基础的地方开始拿一块 ESP32、一张旧 SD 卡、几根杜邦线把官方 sdcard.py 跑起来然后自己尝试修改读多块的逻辑加一个 CSV 记录功能。等你真正把数据写进卡里、拔出来、在电脑上看到自己记录的文件时那种“哦原来数据是这样落盘的”的踏实感是单纯看任何教程都给不了的。
返回列表