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[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) << 10 | csd[7] << 2 | csd[8] >> 6
- 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:
- 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("[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:
- # 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[0]
- if not ocr & 0x40:
- self.cdv = 512
- else:
- 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):
- 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[0] == _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)[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
- 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显示效果图 |