正点原子 发表于 3 小时前

《ESP32-S3使用指南—MicroPython版 V1.0》第二十九章 SD卡实验


1)实验平台:正点原子ESP32S3开发板
2)购买链接:https://detail.tmall.com/item.htm?id=768499342659
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-347618-1-1.html
4)正点原子官方B站:https://space.bilibili.com/394620890
5)正点原子手把手教你学ESP32S3快速入门视频教程:https://www.bilibili.com/video/BV1sH4y1W7Tc
6)正点原子FPGA交流群:132780729


第二十九章 SD卡实验

       SD卡是最常见的小型可移动存储介质之一,它有多种尺寸和物理外形。MMC卡是类似的可移动存储设备,而eMMC设备是电气类似的存储设备,旨在嵌入其他系统。所有这三种形式共享一个通用协议来与它们的主机系统进行通信,并且高级支持对它们来说看起来都是一样的。因此,在MicroPython中,它们被抽象为一个名为machine.SDCard的对象。在本章中,我们将通过machine.SDCard对象来驱动这些存储设备。
       本章分为如下几个小节:
       29.1 SD卡操作模块
       29.2 硬件设计
       29.3 软件设计
       29.4 下载验证

       29.1 SD卡操作模块
       在MicroPython中,uos模块和machine.SDCard对象都与SD卡操作有关。uos模块提供了一些用于操作文件系统的函数,例如listdir(列出目录中的文件)、mkdir(创建目录)、chdir(切换当前工作目录)、getcwd(获取当前工作目录)、remove(删除文件)、rename(重命名文件或文件夹)、stat(获取文件或文件夹的状态信息)、chmod(改变文件或文件夹的权限)、utime(修改文件或文件夹的访问时间和修改时间)以及system(执行系统命令)。
       而machine.SDCard对象是MicroPython中用于表示SD卡的抽象对象。它提供了一组统一的API来与SD卡进行通信,包括初始化SD卡、获取SD卡信息、读取和写入块等操作。在使用MicroPython进行SD卡操作时,通常会结合使用uos模块和machine.SDCard对象。
       现在,作者将对这些模块的构造函数和对象的使用方法进行讲解。

       29.1.1 uos模块
       MicroPython的内置模块uos主要提供文件系统操作服务。该模块实现了CPython模块的一个子集,同时具有一些特殊的特点和限制。以下是其主要特点和应用场景,以及需要注意的事项。

       一、主要特点:

       1,提供基本的“操作系统”服务,如获取系统信息、生成随机数、更改或获取当前目录、列出或创建或删除或重命名文件或目录等。

       2,提供了文件系统访问和挂载的功能,例如在虚拟文件系统中挂载多个“真实”文件系统,或者使用不同类型的文件系统格式,如FAT、littlefs等。

       3,提供终端重定向和复制的功能,例如在给定的类似流对象上复制或切换MicroPython终端(REPL)。

       4,uos模块使用的文件系统操作语法是CPython os模块的一个子集,也是POSIX标准文件系统操作的一个子集。支持的函数和方法有:uname、urandom、chdir、getcwd、ilistdir、listdir、mkdir、remove、rmdir、rename、stat、statvfs、sync、mount、umount、dupterm等。不支持的有:chown、chmod、link、symlink等。

       二、应用场景:

       1,对文件系统进行管理和操作,例如创建或删除或修改文件或目录,或者获取文件或目录的属性或状态。

       2,对不同类型的存储设备进行访问和挂载,例如使用SD卡或SPI闪存等外部存储设备扩展内部存储空间,或者使用不同的文件系统格式适应不同的性能和兼容性需求。

       3,对终端进行重定向和复制,例如在不同的通信接口上使用MicroPython终端(REPL),或者在多个终端上同时输出或输入数据。

       三、需注意事项:

       1,uos模块的功能和性能可能因不同的端口而异,因此在开发可移植的MicroPython应用程序时,应该尽量避免依赖特定的文件系统操作语法或结果。

       2,uos模块在编译和执行文件系统操作时可能会消耗较多的内存和时间,因此在处理大量或复杂的文件或目录时,应该注意优化代码和资源管理。

       3,uos模块在编译和执行文件系统操作时可能会遇到无效或不合法的路径、参数、数据等,或者文件错误、权限错误、设备错误等异常情况。这些情况会引发OSError或ValueError异常,并给出错误信息。应该使用try-except语句来捕获并处理这些异常。

       下面我们打开Thonny软件,在Shell交互窗口下使用dir命令获取uos模块提供的函数,如下所示:
>>> import machine, uos
>>> dir(uos)
['__class__', '__name__', 'remove', 'VfsFat', 'VfsLfs2', '__dict__', 'chdir', 'dupterm', 'dupterm_notify', 'getcwd', 'ilistdir', 'listdir', 'mkdir', 'mount', 'rename', 'rmdir', 'stat', 'statvfs', 'sync', 'umount', 'uname', 'unlink', 'urandom']
>>>       可以看到。MicroPython的uos模块提供了十几种方法(函数),这些方法足够我们去操作SD卡了。

       29.1.2 machine.SDCard对象
       machine.SDCard是一个用于表示SD卡的抽象对象,它提供了一组统一的API来访问和操作SD卡。通过使用machine.SDCard对象,开发人员可以轻松地读取和写入SD卡上块的地址、获取SD卡的容量和可用空间等操作。下面讲解的是SDCard对象的构造函数与方法。

       一、SDCard构造函数
       SDCard的构造对象方法如下:
class machine.SDCard(slot=1, width=1, cd=None, wp=None, sck=None,
miso=None, mosi=None, cs=None, freq=20000000)
使用示例:sd = machine.SDCard(slot=2,width=8,sck=12, miso=13, mosi=11, cs=2)       该构造方法的参数描述,如下表所示。

表29.1.1 machine.SDCard构造方法参数描述
       返回值:SDCard对象。
       上表的slot参数选择接口如下表所示。

表29.1.2 Slot端号分配
       从上表可知,如果我们使用SPI驱动SD卡,则选择2与3号端口。
       注意:正点原子ESP32-S3开发板的SD卡使用的是SPI协议进行通信,所以SD卡构造函数中,我们仅使用上表绿色的参数。

       二、SDCard类的方法
       打开Thonny软件,在Shell交互窗口下使用dir命令获取SDCard类的方法,如下所示:
>>> import machine
>>> dir(machine.SDCard)
['__class__', '__name__', '__bases__', '__del__', '__dict__', 'deinit', 'info', 'ioctl', 'readblocks', 'writeblocks']
>>>       可以看到。通过使用machine.SDCard对象,开发人员可以轻松地读取和写入SD卡上块的地址、获取SD卡的容量和可用空间等操作。

       29.2 硬件设计

       1. 例程功能
       本章实验功能简介:系统打开SD卡根目录下的test.txt文件,然后在此文件下写入“Hello ALIENTEK”字符串数据,写入完成后读取此文件的内容,并输出至SPILCD显示屏上。

       2. 硬件资源

       1)XL9555
              IIC_INT-IO0(需在P5连接IO0)
              IIC_SDA-IO41
              IIC_SCL-IO42

       2)SPILCD
              CS-IO21
              SCK-IO12
              SDA-IO11
              DC-IO40(在P5端口,使用跳线帽将IO_SET和LCD_DC相连)
              PWR- IO1_3(XL9555)
              RST- IO1_2(XL9555)

       3)SD
              CS-IO2
              SCK-IO12
              MOSI-IO11
              MISO-IO13

       3. 原理图
       SD接口与ESP32-S3的连接关系,如下图所示:

图29.2.1 SD接口与ESP32-S3的连接电路图
       29.3 软件设计

       29.3.1 程序流程图
       程序流程图能帮助我们更好的理解一个工程的功能和实现的过程,对学习和设计工程有很好的主导作用。下面看看本实验的程序流程图。

图29.3.1.1 程序流程图
       29.3.2 程序解析
       本书籍的代码都在main.py脚本下编写的,读者可在光盘资料下找到对应的源码。SD卡实验main.py源码如下:import machine, uos
import time


"""
* @brief       程序入口
* @param       无
* @retval      无
"""
if__name__ == '__main__':
   
    # Slot 2 uses pins sck = 12, cs = 2, miso = 13, mosi = 11
    sd = machine.SDCard(slot=2,width=8,sck=12, miso = 13, mosi=11, cs=2)
    # 重新当前系统文件目录
    print('挂载SD前的系统目录:{}'.format(uos.listdir()))
    # 使用uos.VfsFat类创建一个FAT文件系统对象
    vfs = uos.VfsFat(sd)
    # 挂在到SD/sd
    uos.mount(vfs,'/sd')
    # 重新查询系统文件目录
    print('挂载SD后的系统目录:{}'.format(uos.listdir()))
    # 打开test.txt文件,如果SD卡目录没有会重新创建test.txt文件
    with open("/sd/test.txt", "w") as f:
      for i in range(1, 50):
            # 从这个文件写数据
            f.write(str(i)+"\n")
    # 从sd卡目录下读取test.txt文件内容
    with open("/sd/test.txt", "r") as f:
      # 打印读取的内容
      print(f.read())
    # 卸載SD卡
    uos.umount('/sd')       首先,我们需要实例化SD卡对象,并配置SPI协议通信和SPI管脚。然后,使用uos.VfsFat类创建一个FAT文件系统对象,并调用uos.mount方法挂载SD卡。接着,我们需要检查SD卡的挂载是否成功。如果挂载失败,系统目录将不显示SD卡目录。最后,我们需要打开SD卡根目录下的test.txt文件,在此文件下写入1到50的数字,写入完成后,读取此文件的内容,并在串口上输出读取的内容。
       在使用这种方法驱动SD卡的情况下,可能会引发一个问题,导致SPI LCD显示屏无法正常显示。问题的根源在于正点原子ESP32-S3开发板的SD卡与SPI LCD共享一个SPI接口。在这种情况下,MicroPython提供的machine_sdcard.c驱动并没有返回SPI控制块,这使得LCD驱动无法获取到SPI控制块以调用SPI收发函数发送相关的数据和命令。
       因此,为了实现SD卡与SPI LCD的兼容性,作者引用了别人的一个用于驱动SD卡Python脚本(SPI接口)。通过该脚本,可以在实例化SPI的情况下驱动SD卡和SPI LCD显示屏。下面是sdcard.py脚本的代码示例:
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, baudrate=20000000):
      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 = 0xFF
      self.dummybuf_memoryview = memoryview(self.dummybuf)

      # initialise the card
      self.init_card(baudrate)

    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, baudrate):

      # 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 & 0xC0 == 0x40:# CSD version 2.0
            self.sectors = ((csd << 8 | csd) + 1) * 1024
      elif csd & 0xC0 == 0x00:# CSD version 1.0 (old, <=2GB)
            c_size = (csd & 0b11) << 10 | csd << 2 | csd >> 6
            c_size_mult = (csd & 0b11) << 1 | csd >> 7
            read_bl_len = csd & 0b1111
            capacity = (c_size + 1) * (2 ** (c_size_mult + 2))
* (2**read_bl_len)
            self.sectors = capacity // 512
      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(baudrate)

    def init_card_v1(self):
      for i in range(_CMD_TIMEOUT):
            self.cmd(55, 0, 0)
            if self.cmd(41, 0, 0) == 0:
                # SDSC card, uses byte addressing in read/write/erase commands
                self.cdv = 512
                # print(" 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:
# 4-byte response, negative means keep the first byte
                self.cmd(58, 0, 0, -4)
# get first byte of response, which is OCR
                ocr = self.tokenbuf
                if not ocr & 0x40:
                  self.cdv = 512
                else:
                  self.cdv = 1
                # print(" 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 = 0x40 | cmd
      buf = arg >> 24
      buf = arg >> 16
      buf = arg >> 8
      buf = arg
      buf = crc
      self.spi.write(buf)

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

      # wait for the response (response == 0)
      for i in range(_CMD_TIMEOUT):
            self.spi.readinto(self.tokenbuf, 0xFF)
            response = self.tokenbuf
            if not (response & 0x80):
                if final < 0:
                  self.spi.readinto(self.tokenbuf, 0xFF)
                  final = -1 - final
                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 == _TOKEN_DATA:
                break
            time.sleep_ms(1)
      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) & 0x1F) != 0x05:
            self.cs(1)
            self.spi.write(b"\xff")
            return

      # wait for write to finish
      while self.spi.read(1, 0xFF) == 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) == 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 += 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 += 512
                nblocks -= 1
            self.write_token(_TOKEN_STOP_TRAN)

    def ioctl(self, op, arg):
      if op == 4:# get number of blocks
            return self.sectors
      if op == 5:# get block size in bytes
            return 512       可以看到,这个脚本通过使用SD命令来实现对SD卡的读写操作。在脚本的构造函数中,需要传入三个参数来实例化SD卡对象,分别是SPI控制块、CS片选管脚以及SPI的速率。这些参数在实例化SD卡对象时是必需的。此外,值得注意的是,驱动SD卡的速率一般不能超过24M,相关内容可以参考SD卡数据手册。
       首先,作者在Thonny软件中新建了一个文本文件。然后,在此文本文件中粘贴了上述的SD卡驱动代码,并将其命名为sdcard.py文件。接着,将这个文件保存到了MicroPython设备中,如下图所示:

图29.3.2.1 添加sdcard.py脚本
       在main.py脚本的测试代码,如下所示:
from machine import Pin,SPI,I2C
from sdcard import SDCard
import atk_xl9555 as io_ex
import atk_lcd as lcd
import time
import uos

"""
* @brief       程序入口
* @param       无
* @retval      无
"""
if __name__ == '__main__':
   
    x = 0
    # IIC初始化
    i2c0 = I2C(0, scl = Pin(42), sda = Pin(41), freq = 400000)
    # XL9555初始化
    xl9555 = io_ex.init(i2c0)
   
    # 复位LCD
    xl9555.write_bit(io_ex.SLCD_RST,0)
    time.sleep_ms(100)
    xl9555.write_bit(io_ex.SLCD_RST,1)
    time.sleep_ms(100)
   
    # 初始化SPI
    spi = SPI(2,baudrate = 20000000, sck = Pin(12), mosi=Pin(11), miso=Pin(13))
    # 初始化LCD,lcd = 0为正点原子2.4寸屏幕;lcd = 1为正点原子1.3寸SPILCD屏幕;
display = lcd.init(spi,dc = Pin(40,Pin.OUT,Pin.PULL_UP,value = 1),
cs = Pin(21,Pin.OUT,Pin.PULL_UP,value = 1),dir = 1,lcd = 0)
    # 打开背光
    xl9555.write_bit(io_ex.SLCD_PWR,1)
    time.sleep_ms(100)
    sd = SDCard(spi,Pin(2,Pin.OUT))
    # 实验信息
    display.string(30, 50, 240, 32, 32, "ESP32-S3",lcd.RED)
    display.string(30, 80, 240, 24, 24, "SD TEST",lcd.RED)
    display.string(30, 110, 240, 16, 16, "ATOM@ALIENTEK",lcd.RED)
    display.string(30, 130, 200, 16, 16, "File Read:", lcd.BLUE)
    # 挂在到SD/sd
    uos.mount(sd,'/sd')
    # 重新查询系统文件目录
    print('挂载SD后的系统目录:{}'.format(uos.listdir()))
    with open("/sd/test.txt", "w") as f:
            f.write(str("Hello ALIENTEK"))

    # 从sd卡目录下读取hello.txt文件内容
    with open("/sd/test.txt", "r") as f:
      # 打印读取的内容
      data = f.read()
   
    display.string(130, 130, 200, 16, 16, str(data), lcd.BLUE)
    # 卸載SD卡
    uos.umount('/sd')       在上述源代码中,首先实例化了IIC、XL9555、SPI、LCD和SD卡对象。接着,系统显示了实验信息。随后,使用了uos模块的方法来挂载SD卡。最后,在SD卡的目录下创建了一个名为test.txt的文件,并将字符串数据“Hello ALIENTEK”写入该文件。同时,读取了该文件的内容,并将其显示在LCD显示屏上。

       29.4 下载验证
       程序下载到开发板后,可以看到SPILCD显示SD卡目录下的test.txt文件的内容,如下图所示。

图30.4.1 RGBLCD显示效果图
页: [1]
查看完整版本: 《ESP32-S3使用指南—MicroPython版 V1.0》第二十九章 SD卡实验