SD 卡实验

罗大富 BigRich大约 10 分钟ESP32Python

这节课我们学习如何使用 MicroPython 控制 SD 卡模块。

实验原理

SD卡(Secure Digital Card)是一种常见的可移动存储设备,用于存储和传输数据。它是一种闪存存储卡,具有较小的尺寸、高存储容量和可擦写的特性。

SD 卡具有以下主要特点:

  • 尺寸小:SD 卡采用了较小的尺寸,便于携带和使用。标准尺寸的 SD 卡尺寸为 32mm × 24mm × 2.1mm,而微型 SD 卡和迷你 SD 卡则更小。
  • 高存储容量:SD 卡的存储容量可以从几百兆字节到数百千兆字节不等。现代的 SD 卡通常具有较大的存储容量,可以满足各种数据存储需求。
  • 可擦写性:SD 卡可以被多次擦写和重新写入,使其非常适合存储和传输数据。用户可以根据需要将数据写入 SD 卡,并随时进行修改或删除。
  • 高速传输:SD 卡支持高速数据传输,以满足对快速读写速度的需求。不同类型的 SD 卡可能具有不同的传输速度标准,例如 SDSC、SDHC 和 SDXC 等。
  • 兼容性:SD 卡具有广泛的兼容性,可以在许多设备上使用,例如数字相机、移动电话、音频播放器、电脑等。通过适配器,SD 卡还可以与其他类型的存储设备接口兼容。

SD 卡通常用于存储照片、音频、视频、文档和其他文件。它们广泛应用于数码相机、移动设备、嵌入式系统和各种消费电子产品中。在使用 SD 卡时,需要注意保护数据的安全性和完整性,避免数据丢失或损坏。

把 SD 卡通过读卡器连接到电脑,右击 SD 卡选择 格式化 选项,之后,我们可以看到 文件系统 选项,如下图

SD 卡使用的文件系统是指在 SD 卡上组织和管理文件和文件夹的方法。常见的 SD 卡文件系统有 FAT16、FAT32 和 exFAT 等。

  • FAT16(File Allocation Table 16):FAT16 是一种较早的文件系统,支持最大容量为 2GB 的存储设备。它使用 16 位的文件分配表来记录文件的存储位置和状态。FAT16 文件系统有一定的局限性,无法处理大容量存储设备和单个文件超过 2GB 的情况。
  • FAT32(File Allocation Table 32):FAT32 是一种较为常见的文件系统,支持最大容量为 2TB 的存储设备。它采用 32 位的文件分配表,可以更有效地管理存储空间和文件索引。FAT32 文件系统被广泛用于移动存储设备、数码相机和其他消费电子设备。
  • exFAT(Extended File Allocation Table):exFAT 是一种针对大容量存储设备设计的文件系统。它支持最大容量为 128PB 的存储设备和单个文件大小为 16EB 。exFAT 文件系统在支持大容量和大文件的同时,还具有较好的兼容性,可以在 Windows、Mac 和 Linux 等多个操作系统上使用。

SD 卡文件系统负责管理文件和目录的存储和访问。它使用文件分配表来记录文件的物理位置和状态,以及目录结构来组织文件和子目录。通过文件系统,用户可以方便地创建、读取、写入和删除文件,实现对存储设备中数据的管理和访问。

在使用 SD 卡时,需要选择适合的文件系统,根据存储设备的容量和应用需求进行设置。同时,还需要注意在使用过程中正确地操作文件系统,避免数据损坏和文件丢失的风险。

需要注意的是,在 SD 卡的文件系统选项中,存在 NTFS 选项,这个并不是 SD 卡的文件系统,如果你使用该选项格式化 SD 卡,会导致 SD 卡模块读取不到内容。

NTFS(New Technology File System) 是一种现代的文件系统,最早由微软引入并用于 Windows NT 操作系统及其后续版本。它具有许多先进的功能和优势,适用于处理大容量磁盘驱动器和大文件。

以下是 NTFS 的一些特点:

  • 支持大容量存储:NTFS 支持非常大的磁盘容量,可以处理多 TB 级别的存储设备。
  • 高性能:NTFS 采用了先进的索引结构和数据组织方式,具有快速读取和写入文件的能力,可以提供较高的数据访问性能。
  • 安全性:NTFS 支持文件和文件夹级别的访问控制,可以设置权限和加密保护,保障数据的安全性。
  • 容错能力:NTFS 具有容错和恢复功能,可以自动修复文件系统错误和数据损坏,并提供一致性和完整性保护。
  • 支持大文件:NTFS 支持单个文件的最大大小为 16EB(1EB = 1024PB),可以处理非常大的文件。
  • 支持文件压缩和加密:NTFS 提供了文件压缩和加密的功能,可以节省存储空间并保护敏感数据的安全性。

NTFS 是在 Windows 操作系统中广泛使用的文件系统,适合用于处理大容量存储和大文件的场景。它在性能、安全性和功能方面都有一定的优势,可以满足现代计算机系统对文件系统的要求。

接着我们来了解一下 SD 卡模块,SD 卡模块通过标准的 SPI 协议与单片机进行连接,如果我们采用双线或者三线 SPI 协议就无法实现全双工的数据读写功能。唯一需要注意的是 SD 卡模块的 VCC 接 5V 电源引脚。

硬件电路设计

物料清单(BOM 表):

材料名称数量
SD 卡模块1
SD 卡1
杜邦线(跳线)若干
面包板1

软件程序设计

想要使用 MicroPython 操作 SD 卡模块,需要使用第三方模块,大家可以在 GitHub 对应的 MicroPython 驱动库open in new window下载,或者复制下面的代码并把以下代码上传到 MicroPython 设备中的 libs 目录下:

"""
MicroPython driver for SD cards using SPI bus.

Requires an SPI bus and a CS pin.  Provides readblocks and writeblocks
methods so the device can be mounted as a filesystem.

Example usage on pyboard:

    import pyb, sdcard, os
    sd = sdcard.SDCard(pyb.SPI(1), pyb.Pin.board.X5)
    pyb.mount(sd, '/sd2')
    os.listdir('/')

Example usage on ESP8266:

    import machine, sdcard, os
    sd = sdcard.SDCard(machine.SPI(1), machine.Pin(15))
    os.mount(sd, '/sd')
    os.listdir('/')

"""

from micropython import const
import time


_CMD_TIMEOUT = const(100)

_R1_IDLE_STATE = const(1 << 0)
# R1_ERASE_RESET = const(1 << 1)
_R1_ILLEGAL_COMMAND = const(1 << 2)
# R1_COM_CRC_ERROR = const(1 << 3)
# R1_ERASE_SEQUENCE_ERROR = const(1 << 4)
# R1_ADDRESS_ERROR = const(1 << 5)
# R1_PARAMETER_ERROR = const(1 << 6)
_TOKEN_CMD25 = const(0xFC)
_TOKEN_STOP_TRAN = const(0xFD)
_TOKEN_DATA = const(0xFE)


class SDCard:
    def __init__(self, spi, cs):
        self.spi = spi
        self.cs = cs

        self.cmdbuf = bytearray(6)
        self.dummybuf = bytearray(512)
        self.tokenbuf = bytearray(1)
        for i in range(512):
            self.dummybuf[i] = 0xFF
        self.dummybuf_memoryview = memoryview(self.dummybuf)

        # initialise the card
        self.init_card()

    def init_spi(self, baudrate):
        try:
            master = self.spi.MASTER
        except AttributeError:
            # on ESP8266
            self.spi.init(baudrate=baudrate, phase=0, polarity=0)
        else:
            # on pyboard
            self.spi.init(master, baudrate=baudrate, phase=0, polarity=0)

    def init_card(self):
        # init CS pin
        self.cs.init(self.cs.OUT, value=1)

        # init SPI bus; use low data rate for initialisation
        self.init_spi(100000)

        # clock card at least 100 cycles with cs high
        for i in range(16):
            self.spi.write(b"\xff")

        # CMD0: init card; should return _R1_IDLE_STATE (allow 5 attempts)
        for _ in range(5):
            if self.cmd(0, 0, 0x95) == _R1_IDLE_STATE:
                break
        else:
            raise OSError("no SD card")

        # CMD8: determine card version
        r = self.cmd(8, 0x01AA, 0x87, 4)
        if r == _R1_IDLE_STATE:
            self.init_card_v2()
        elif r == (_R1_IDLE_STATE | _R1_ILLEGAL_COMMAND):
            self.init_card_v1()
        else:
            raise OSError("couldn't determine SD card version")

        # get the number of sectors
        # CMD9: response R2 (R1 byte + 16-byte block read)
        if self.cmd(9, 0, 0, 0, False) != 0:
            raise OSError("no response from SD card")
        csd = bytearray(16)
        self.readinto(csd)
        if csd[0] & 0xC0 == 0x40:  # CSD version 2.0
            self.sectors = ((csd[8] << 8 | csd[9]) + 1) * 1024
        elif csd[0] & 0xC0 == 0x00:  # CSD version 1.0 (old, <=2GB)
            c_size = csd[6] & 0b11 | csd[7] << 2 | (csd[8] & 0b11000000) << 4
            c_size_mult = ((csd[9] & 0b11) << 1) | csd[10] >> 7
            self.sectors = (c_size + 1) * (2 ** (c_size_mult + 2))
        else:
            raise OSError("SD card CSD format not supported")
        # print('sectors', self.sectors)

        # CMD16: set block length to 512 bytes
        if self.cmd(16, 512, 0) != 0:
            raise OSError("can't set 512 block size")

        # set to high data rate now that it's initialised
        self.init_spi(1320000)

    def init_card_v1(self):
        for i in range(_CMD_TIMEOUT):
            self.cmd(55, 0, 0)
            if self.cmd(41, 0, 0) == 0:
                self.cdv = 512
                # print("[SDCard] v1 card")
                return
        raise OSError("timeout waiting for v1 card")

    def init_card_v2(self):
        for i in range(_CMD_TIMEOUT):
            time.sleep_ms(50)
            self.cmd(58, 0, 0, 4)
            self.cmd(55, 0, 0)
            if self.cmd(41, 0x40000000, 0) == 0:
                self.cmd(58, 0, 0, 4)
                self.cdv = 1
                # print("[SDCard] v2 card")
                return
        raise OSError("timeout waiting for v2 card")

    def cmd(self, cmd, arg, crc, final=0, release=True, skip1=False):
        self.cs(0)

        # create and send the command
        buf = self.cmdbuf
        buf[0] = 0x40 | cmd
        buf[1] = arg >> 24
        buf[2] = arg >> 16
        buf[3] = arg >> 8
        buf[4] = arg
        buf[5] = crc
        self.spi.write(buf)

        if skip1:
            self.spi.readinto(self.tokenbuf, 0xFF)

        # wait for the response (response[7] == 0)
        for i in range(_CMD_TIMEOUT):
            self.spi.readinto(self.tokenbuf, 0xFF)
            response = self.tokenbuf[0]
            if not (response & 0x80):
                # this could be a big-endian integer that we are getting here
                for j in range(final):
                    self.spi.write(b"\xff")
                if release:
                    self.cs(1)
                    self.spi.write(b"\xff")
                return response

        # timeout
        self.cs(1)
        self.spi.write(b"\xff")
        return -1

    def readinto(self, buf):
        self.cs(0)

        # read until start byte (0xff)
        for i in range(_CMD_TIMEOUT):
            self.spi.readinto(self.tokenbuf, 0xFF)
            if self.tokenbuf[0] == _TOKEN_DATA:
                break
        else:
            self.cs(1)
            raise OSError("timeout waiting for response")

        # read data
        mv = self.dummybuf_memoryview
        if len(buf) != len(mv):
            mv = mv[: len(buf)]
        self.spi.write_readinto(mv, buf)

        # read checksum
        self.spi.write(b"\xff")
        self.spi.write(b"\xff")

        self.cs(1)
        self.spi.write(b"\xff")

    def write(self, token, buf):
        self.cs(0)

        # send: start of block, data, checksum
        self.spi.read(1, token)
        self.spi.write(buf)
        self.spi.write(b"\xff")
        self.spi.write(b"\xff")

        # check the response
        if (self.spi.read(1, 0xFF)[0] & 0x1F) != 0x05:
            self.cs(1)
            self.spi.write(b"\xff")
            return

        # wait for write to finish
        while self.spi.read(1, 0xFF)[0] == 0:
            pass

        self.cs(1)
        self.spi.write(b"\xff")

    def write_token(self, token):
        self.cs(0)
        self.spi.read(1, token)
        self.spi.write(b"\xff")
        # wait for write to finish
        while self.spi.read(1, 0xFF)[0] == 0x00:
            pass

        self.cs(1)
        self.spi.write(b"\xff")

    def readblocks(self, block_num, buf):
        nblocks = len(buf) // 512
        assert nblocks and not len(buf) % 512, "Buffer length is invalid"
        if nblocks == 1:
            # CMD17: set read address for single block
            if self.cmd(17, block_num * self.cdv, 0, release=False) != 0:
                # release the card
                self.cs(1)
                raise OSError(5)  # EIO
            # receive the data and release card
            self.readinto(buf)
        else:
            # CMD18: set read address for multiple blocks
            if self.cmd(18, block_num * self.cdv, 0, release=False) != 0:
                # release the card
                self.cs(1)
                raise OSError(5)  # EIO
            offset = 0
            mv = memoryview(buf)
            while nblocks:
                # receive the data and release card
                self.readinto(mv[offset : offset + 512])
                offset += 512
                nblocks -= 1
            if self.cmd(12, 0, 0xFF, skip1=True):
                raise OSError(5)  # EIO

    def writeblocks(self, block_num, buf):
        nblocks, err = divmod(len(buf), 512)
        assert nblocks and not err, "Buffer length is invalid"
        if nblocks == 1:
            # CMD24: set write address for single block
            if self.cmd(24, block_num * self.cdv, 0) != 0:
                raise OSError(5)  # EIO

            # send the data
            self.write(_TOKEN_DATA, buf)
        else:
            # CMD25: set write address for first block
            if self.cmd(25, block_num * self.cdv, 0) != 0:
                raise OSError(5)  # EIO
            # send the data
            offset = 0
            mv = memoryview(buf)
            while nblocks:
                self.write(_TOKEN_CMD25, mv[offset : offset + 512])
                offset += 512
                nblocks -= 1
            self.write_token(_TOKEN_STOP_TRAN)

    def ioctl(self, op, arg):
        if op == 4:  # get number of blocks
            return self.sectors

上传并保存代码之后,我们就可以对 SD 卡中的数据进行增删改查了,代码如下:

import os
from machine import Pin, SoftSPI
from libs.sdcard import SDCard

# 初始化SD卡模块
spi = SoftSPI(-1, miso=Pin(19), mosi=Pin(23), sck=Pin(18))
sd = SDCard(spi, Pin(5))

# 挂载文件系统
vfs = os.VfsFat(sd)
os.mount(vfs, "/sd")

# 打印 SD 卡内容
print(f'文件列表:{os.listdir()}')

# 创建新文件并写入数据
print('创建并写入数据')
file_path = "/sd/data.txt"
with open(file_path, "w") as file:
    file.write("Hello, World!")
    
# 创建新文件后,重新打印 SD 卡内容
print(f'文件列表:{os.listdir()}')

# 读取文件内容并进行处理
print('读取文件内容')
with open(file_path, "r") as file:
    content = file.read()
    print(content)

# 追加数据到现有文件中
print('追加写入数据')
with open(file_path, "a") as file:
    file.write(" Appended data")

# 读取文件内容并进行处理
print('读取文件内容')
with open(file_path, "r") as file:
    content = file.read()
    print(content)

# 删除文件
print('删除文件')
os.remove(file_path)

# 删除文件后,重新打印 SD 卡内容
print(f'文件列表:{os.listdir()}')
上次编辑于:
贡献者: Luo