raspberrypi / pico-examples

BSD 3-Clause "New" or "Revised" License
2.94k stars 844 forks source link

After inserting and removing the SD card, an error occurs when deleting a file #560

Closed leezisheng closed 1 month ago

leezisheng commented 1 month ago

Port, board and/or hardware RP2

MicroPython version MicroPython v1.23.0 on RP2

Reproduction After inserting and removing the SD card, an error occurs when deleting a file." But it is possible to successfully delete the file.

image

The code is as follows: main.py is used to test SD card read and write operations, sdcard.py is used to implement SD card read and write functionality, sd_block_dev.py is used to implement the SD card block device, and AbstractBlockDevInterface.py is used to implement the block device interface.

The implementation of SD card read and write functionality is referenced from the repository: https://github.com/micropython/micropython-lib/blob/master/micropython/drivers/storage/sdcard/sdcard.py

# Python env   : MicroPython v1.23.0
# -*- coding: utf-8 -*-        
# @Time    : 2024/9/30 下午12:13   
# @Author  : 李清水            
# @File    : main.py       
# @Description : 文件系统类实验,使用SD卡挂载文件系统并读写

# ======================================== 导入相关模块 ========================================

# 导入硬件相关模块
from machine import SPI, Pin
# 导入时间相关模块
import time
# 导入自定义SD卡块设备类
from sd_block_dev import SDCARDBlockDevice
# 导入自定义SD卡读写类
from sdcard import SDCard
# 导入虚拟文件类
import vfs
# 导入文件系统操作类
import os

# ======================================== 全局变量 ============================================

# 定义嵌入式知识学习网站及其网址
websites = [
    ("Embedded.com", "https://www.embedded.com"),
    ("Microchip", "https://www.microchip.com"),
    ("ARM Developer", "https://developer.arm.com"),
    ("SparkFun", "https://www.sparkfun.com"),
    ("Adafruit", "https://www.adafruit.com"),
    ("Embedded Systems Academy", "https://www.esacademy.com"),
    ("Electronics Hub", "https://www.electronicshub.org"),
]

# 定义CSV文件地址
csv_file_path = '/sdcard/embedded_websites.csv'

# ======================================== 功能函数 ============================================

# ======================================== 自定义类 ============================================

# ======================================== 初始化配置 ==========================================

# 上电延时3s
time.sleep(3)
# 打印调试消息
print("FreakStudio: Mount the SD Card to the file system")

# 初始化SPI类,设置波特率、极性、相位、时钟引脚、数据引脚
spi = SPI(0, baudrate=1320000, polarity=0, phase=0, sck=Pin(2), mosi=Pin(3), miso=Pin(4))
# 初始化SD卡类,使用GPIO9作为片选引脚
sdcard = SDCard(spi, cs=Pin(5))

# 创建块设备,使用SD卡,块大小为512个字节
block_device = SDCARDBlockDevice(sdcard = sdcard)
# 在块设备上创建一个 FAT 文件系统
vfs.VfsFat.mkfs(block_device)
# 将块设备挂载到虚拟文件系统的 /sdcard 目录
vfs.mount(block_device, '/sdcard')
# 打印当前目录
print("Current Directory : ",os.listdir())

# ========================================  主程序  ============================================

# 写入 CSV 文件
with open(csv_file_path, 'w') as f:
    # 写入表头
    f.write("Website Name,URL\n")
    for name, url in websites:
        # 写入每一行
        f.write(f"{name},{url}\n")

# 打印文件位置
print(f"CSV file written to SD card as '{csv_file_path}'.")

# Python env   : MicroPython v1.23.0
# -*- coding: utf-8 -*-        
# @Time    : 2024/9/30 下午12:11   
# @Author  : 李清水            
# @File    : sdcard.py       
# @Description : 自定义用于SD卡读写的SDCard类
# 参考代码:https://github.com/micropython/micropython-lib/blob/master/micropython/drivers/storage/sdcard/sdcard.py#L291

# ======================================== 导入相关模块 ========================================

# 导入MicroPython相关模块
from micropython import const
# 导入时间相关的模块
import time

# ======================================== 全局变量 ============================================

# ======================================== 功能函数 ============================================

# ======================================== 自定义类 ============================================

# 定义类变量
CMD_TIMEOUT = const(100)            # 命令超时时间常量
R1_IDLE_STATE = const(1 << 0)       # R1响应状态:空闲状态
R1_ILLEGAL_COMMAND = const(1 << 2)  # R1响应状态:非法命令
TOKEN_CMD25 = const(0xFC)           # 数据传输命令令牌
TOKEN_STOP_TRAN = const(0xFD)       # 停止传输令牌
TOKEN_DATA = const(0xFE)            # 数据令牌

# 自定义SD卡操作类
class SDCard:
    def __init__(self, spi, cs, baudrate=1320000):
        '''
        初始化SD卡类,设置SPI和CS引脚,并初始化卡片
        :param spi  [machine.SPI]: SPI接口对象
        :param cs   [machine.Pin]: CS引脚对象
        :param baudrate     [int]: 波特率,默认为1320000
        '''
        # 存储SPI接口
        self.spi = spi
        # 存储CS引脚
        self.cs = cs

        # 创建命令缓冲区,长度为6个字节
        self.cmdbuf = bytearray(6)
        # 创建虚拟数据缓冲区,长度为512个字节
        self.dummybuf = bytearray(512)
        # 创建令牌缓冲区,长度为1个字节
        self.tokenbuf = bytearray(1)

        # 初始化虚拟数据缓冲区为全FF
        for i in range(512):
            self.dummybuf[i] = 0xFF

        # 创建虚拟数据缓冲区的内存视图
        self.dummybuf_memoryview = memoryview(self.dummybuf)

        # 初始化SD卡
        self.init_card(baudrate)

    def init_spi(self, baudrate):
        '''
        初始化SPI接口
        :param baudrate [int]: 波特率
        :return : None
        '''
        # 初始化SPI
        self.spi.init(baudrate=baudrate, phase=0, polarity=0)

    def init_card(self, baudrate):
        '''
        初始化SD卡的过程,包括发送命令并获取卡片信息
        :param baudrate: 波特率
        :return : None
        '''
        # 初始化CS引脚,设置CS引脚为输出高电平
        self.cs.init(self.cs.OUT, value=1)

        # 初始化SPI总线;使用低数据速率进行初始化
        self.init_spi(100000)

        # 发送命令前的空闲周期,保持CS高
        for i in range(16):
            # 发送16个空字节
            self.spi.write(b"\xff")

        # CMD0: 初始化卡片;应返回_IDLE_STATE(允许5次尝试)
        for _ in range(5):
            if self.cmd(0, 0, 0x95) == R1_IDLE_STATE:
                break
        else:
            # 如果未找到SD卡,则引发异常
            raise OSError("no SD card")

        # CMD8: 确定卡片版本
        r = self.cmd(8, 0x01AA, 0x87, 4)
        if r == R1_IDLE_STATE:
            # 初始化v2版本的卡片
            self.init_card_v2()
        elif r == (R1_IDLE_STATE | R1_ILLEGAL_COMMAND):
            # 初始化v1版本的卡片
            self.init_card_v1()
        else:
            # 无法确定SD卡版本
            raise OSError("couldn't determine SD card version")

        # 获取扇区数量
        # CMD9: 响应R2(R1字节 + 16字节块读取)
        if self.cmd(9, 0, 0, 0, False) != 0:
            # 如果没有响应,则引发异常
            raise OSError("no response from SD card")

        # 创建CSD缓冲区
        csd = bytearray(16)
        # 读取CSD数据
        self.readinto(csd)

        # CSD版本2.0
        if csd[0] & 0xC0 == 0x40:
            # 计算扇区数量
            self.sectors = ((csd[8] << 8 | csd[9]) + 1) * 1024
            # CSD版本1.0(旧,<=2GB)
        elif csd[0] & 0xC0 == 0x00:
            # 获取C_SIZE
            c_size = (csd[6] & 0b11) << 10 | csd[7] << 2 | csd[8] >> 6
            # 获取C_SIZE_MULT
            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:
            # 不支持的CSD格式
            raise OSError("SD card CSD format not supported")

        # CMD16: 设置块长度为512个字节
        if self.cmd(16, 512, 0) != 0:
            # 无法设置块大小
            raise OSError("can't set 512 block size")

        # 现在设置为高数据速率
        self.init_spi(baudrate)

    def init_card_v1(self):
        '''
        初始化v1版本的SD卡
        :return : None
        '''
        for i in range(CMD_TIMEOUT):
            time.sleep_ms(50)
            # 发送命令55
            self.cmd(55, 0, 0)

            # 尝试初始化卡片
            # SDSC卡,使用字节寻址方式进行读/写/擦除命令
            if self.cmd(41, 0, 0) == 0:
                # 设置块大小为512个字节
                self.cdv = 512
                # 成功返回
                return
        # 超时引发异常
        raise OSError("timeout waiting for v1 card")

    def init_card_v2(self):
        '''
        初始化v2版本的SD卡
        :return : None
        '''
        for i in range(CMD_TIMEOUT):
            # 等待50毫秒
            time.sleep_ms(50)
            # 发送命令58以获取OCR
            self.cmd(58, 0, 0, 4)
            # 发送命令55以准备初始化
            self.cmd(55, 0, 0)

            # 尝试初始化SDHC/SDXC卡
            if self.cmd(41, 0x40000000, 0) == 0:
                # 4个字节响应,负值表示保留首字节
                self.cmd(58, 0, 0, -4)
                # 获取OCR的首字节
                ocr = self.tokenbuf[0]
                # SDSC卡,使用字节寻址方式
                if not ocr & 0x40:
                    # 设置块大小为512个字节
                    self.cdv = 512
                else:
                    # SDHC/SDXC卡,使用块寻址方式
                    # 设置块大小为1个字节
                    self.cdv = 1
                # 成功返回
                return

        # 超时引发异常
        raise OSError("timeout waiting for v2 card")

    def cmd(self, cmd, arg, crc, final=0, release=True, skip1=False):
        '''
        发送命令到SD卡并等待响应
        :param cmd      [int]: 命令编号
        :param arg      [int]: 命令参数
        :param crc      [int]: CRC校验值
        :param final    [int]: 期望读取的字节数
        :param release [bool]: 是否释放CS信号
        :param skip1   [bool]: 是否跳过第一次响应
        :return         [int]: SD卡的响应字节
        '''
        # 拉低CS引脚,开始通信
        self.cs(0)

        # 创建并发送命令
        buf = self.cmdbuf

        # 设置命令头
        buf[0] = 0x40 | cmd
        # 设置命令参数(高字节)
        buf[1] = arg >> 24
        # 设置命令参数(次高字节)
        buf[2] = arg >> 16
        # 设置命令参数(次低字节)
        buf[3] = arg >> 8
        # 设置命令参数(低字节)
        buf[4] = arg
        # 设置CRC校验
        buf[5] = crc

        # 通过SPI发送命令
        self.spi.write(buf)

        # 如果需要跳过第一次响应
        if skip1:
            # 读取一个字节,丢弃
            self.spi.readinto(self.tokenbuf, 0xFF)

        # 等待响应 (response[7] == 0)
        for i in range(CMD_TIMEOUT):
            # 读取响应字节
            self.spi.readinto(self.tokenbuf, 0xFF)
            # 获取响应
            response = self.tokenbuf[0]

            # 检查响应是否有效
            if not (response & 0x80):
                # 处理大端整数的情况
                # 如果final为负
                if final < 0:
                    # 读取并丢弃一个字节
                    self.spi.readinto(self.tokenbuf, 0xFF)
                    # 更新final值
                    final = -1 - final

                # 读取额外的字节
                for j in range(final):
                    # 发送空字节以保持时序
                    self.spi.write(b"\xff")

                # 如果需要释放CS信号
                if release:
                    # 拉高CS引脚
                    self.cs(1)
                    # 发送空字节
                    self.spi.write(b"\xff")
                # 返回有效响应
                return response

        # 超时处理
        # 拉高CS引脚
        self.cs(1)
        # 发送空字节
        self.spi.write(b"\xff")
        # 返回-1表示超时
        return -1

    def readinto(self, buf):
        '''
        从SD卡读取数据到给定缓冲区
        :param buf [bytearray]: 用于存放读取数据的缓冲区
        :return : None
        '''
        # 拉低CS引脚,开始通信
        self.cs(0)

        # 读取直到开始字节 (0xff)
        for i in range(CMD_TIMEOUT):
            # 读取一个字节
            self.spi.readinto(self.tokenbuf, 0xFF)
            # 检查是否为数据开始标志
            if self.tokenbuf[0] == TOKEN_DATA:
                # 找到开始字节,退出循环
                break
            # 等待1毫秒
            time.sleep_ms(1)
        else:
            # 拉高CS引脚
            self.cs(1)
            # 抛出超时错误
            raise OSError("timeout waiting for response")

        # 读取数据,创建内存视图
        mv = self.dummybuf_memoryview
        # 如果缓冲区长度不匹配
        if len(buf) != len(mv):
            # 切片到缓冲区长度
            mv = mv[: len(buf)]
        # 读取数据到buf
        self.spi.write_readinto(mv, buf)

        # 发送空字节
        self.spi.write(b"\xff")
        self.spi.write(b"\xff")

        # 拉高CS引脚
        self.cs(1)
        # 发送空字节
        self.spi.write(b"\xff")

    def write(self, token, buf):
        '''
        向SD卡写入数据
        :param token [int]: 写入的开始标记
        :param buf   [bytearray]: 要写入的数据
        :return : None
        '''
        # 拉低CS引脚,开始通信
        self.cs(0)

        # 发送: 块开始标记,数据,校验和
        self.spi.read(1, token)
        self.spi.write(buf)
        self.spi.write(b"\xff")
        self.spi.write(b"\xff")

        # 检查响应是否为成功标志
        if (self.spi.read(1, 0xFF)[0] & 0x1F) != 0x05:
            # 拉高CS引脚
            self.cs(1)
            # 发送空字节
            self.spi.write(b"\xff")
            # 返回,写入失败
            return

        # 等待写入完成
        while self.spi.read(1, 0xFF)[0] == 0:
            # 等待直到写入完成
            pass

        # 拉高CS引脚
        self.cs(1)
        # 发送空字节
        self.spi.write(b"\xff")

    def write_token(self, token):
        '''
        向SD卡写入数据令牌
        :param token [int]: 要写入的开始令牌
        :return : None
        '''
        # 拉低CS引脚,开始通信
        self.cs(0)
        # 发送标记
        self.spi.read(1, token)
        # 发送空字节
        self.spi.write(b"\xff")

        # 等待写入完成
        while self.spi.read(1, 0xFF)[0] == 0x00:
            # 等待直到写入完成
            pass

        # 拉高CS引脚
        self.cs(1)
        # 发送空字节
        self.spi.write(b"\xff")

    def erase_block(self, block_number):
        '''
        擦除指定的块
        :param block_number [int]: 要擦除的块号
        :return: None
        '''
        # 拉低CS引脚,开始通信
        self.cs(0)

        # 发送擦除命令 CMD32 (erase start) 和 CMD33 (erase end)
        # 开始擦除
        self.cmd(32, block_number * 512, 0)
        # 结束擦除
        self.cmd(33, block_number * 512, 0)

        # 发送擦除命令 CMD38 (erase)
        self.cmd(38, 0, 0)

        # 等待擦除完成
        while True:
            # 查询状态
            response = self.cmd(13, 0, 0)
            # 擦除完成
            if response == 0:
                break

        # 拉高CS引脚
        self.cs(1)
        # 发送空字节
        self.spi.write(b"\xff")

# ======================================== 初始化配置 ==========================================

# ========================================  主程序  ============================================

# Python env   : MicroPython v1.23.0
# -*- coding: utf-8 -*-        
# @Time    : 2024/9/18 下午12:15   
# @Author  : 李清水            
# @File    : AbstractBlockDevInterface.py.py       
# @Description : 定义了块设备的抽象基类 AbstractBlockDev

# ======================================== 导入相关模块 =========================================

# ======================================== 全局变量 ============================================

# ======================================== 功能函数 ============================================

# ======================================== 自定义类 ============================================

# 自定义抽象基类 AbstractBlockDev,表示块设备的基本操作接口
class AbstractBlockDev:
    def __init__(self):
        '''
        初始化抽象块设备
        '''
        pass

    def readblocks(self, block_num, buf):
        '''
        从设备读取数据块到缓冲区
        :param block_num      [int]: 起始块号
        :param buf      [bytearray]: 用于存储读取数据的缓冲区
        :return: None
        '''
        raise NotImplementedError("sub class must implement this method")

    def writeblocks(self, block_num, buf):
        '''
        将数据块从缓冲区写入设备
        :param block_num      [int]: 起始块号
        :param buf      [bytearray]: 用于存储读取数据的缓冲区
        :return: None
        '''
        raise NotImplementedError("sub class must implement this method")

    def ioctl(self, op, arg):
        '''
        控制块设备并查询其参数
        :param op  [int]: 操作码
        :param arg      : 附加参数
        :return         : 操作结果
        '''
        raise NotImplementedError("sub class must implement this method")

# ======================================== 初始化配置 ==========================================

# ========================================  主程序  ===========================================

# Python env   : MicroPython v1.23.0
# -*- coding: utf-8 -*-        
# @Time    : 2024/10/1 上午11:13   
# @Author  : 李清水            
# @File    : sd_block_dev.py       
# @Description : 自定义SD卡块设备类
# 参考代码:https://github.com/micropython/micropython-lib/blob/master/micropython/drivers/storage/sdcard/sdcard.py#L291

# ======================================== 导入相关模块 ========================================

# 导入虚拟文件块设备的抽象基类
from AbstractBlockDevInterface import AbstractBlockDev
# 导入自定义SD卡读写类中定义的一些常量
from sdcard import TOKEN_CMD25,TOKEN_STOP_TRAN,TOKEN_DATA

# ======================================== 全局变量 ============================================

# ======================================== 功能函数 ============================================

# ======================================== 自定义类 ============================================

class SDCARDBlockDevice(AbstractBlockDev):
    def __init__(self, sdcard):
        '''
        初始化SDCARDBlockDevice实例
        :param sdcard [SDCard]: 传入的SDCard实例
        '''
        # 调用父类构造函数
        super().__init__()

        # 保存传入的SDCard实例
        self.sdcard = sdcard

    def readblocks(self, block_num, buf):
        '''
        从SD卡读取数据块到缓冲区
        :param block_num [int]: 起始块号
        :param buf       [bytearray]: 用于存储读取数据的缓冲区
        :return: None
        '''
        # 解决共享总线问题,确保在开始事务之前MOSI为高电平
        self.sdcard.spi.write(b"\xff")

        # 计算需要读取的块数
        nblocks = len(buf) // 512
        # 确保缓冲区长度有效
        assert nblocks and not len(buf) % 512, "Buffer length is invalid"

        if nblocks == 1:
            # CMD17: 设置单个块的读取地址
            if self.sdcard.cmd(17, block_num * self.sdcard.cdv, 0, release=False) != 0:
                # 释放卡片
                self.sdcard.cs(1)
                # EIO错误
                raise OSError(5)
            # 接收数据并释放卡片
            self.sdcard.readinto(buf)
        else:
            # CMD18: 设置多个块的读取地址
            if self.sdcard.cmd(18, block_num * self.sdcard.cdv, 0, release=False) != 0:
                # 释放卡片
                self.sdcard.cs(1)
                # EIO错误
                raise OSError(5)
            # 数据偏移量
            offset = 0

            # 创建内存视图
            mv = memoryview(buf)
            while nblocks:
                # 接收数据并释放卡片
                self.sdcard.readinto(mv[offset : offset + 512])
                # 更新偏移量
                offset += 512
                # 减少剩余块数
                nblocks -= 1
            if self.sdcard.cmd(12, 0, 0xFF, skip1=True):
                # EIO错误
                raise OSError(5)

    def writeblocks(self, block_num, buf):
        '''
        将数据块从缓冲区写入SD卡
        :param block_num [int]: 起始块号
        :param buf       [bytearray]: 要写入的数据
        :return: None
        '''
        # 解决共享总线问题,确保在开始事务之前MOSI为高电平
        self.sdcard.spi.write(b"\xff")

        # 计算需要写入的块数
        nblocks, err = divmod(len(buf), 512)
        # 确保缓冲区长度有效
        assert nblocks and not err, "Buffer length is invalid"
        if nblocks == 1:
            # CMD24: 设置单个块的写入地址
            if self.sdcard.cmd(24, block_num * self.sdcard.cdv, 0) != 0:
                # EIO错误
                raise OSError(5)

            # 发送数据
            self.sdcard.write(TOKEN_DATA, buf)
        else:
            # CMD25: 设置第一个块的写入地址
            if self.sdcard.cmd(25, block_num * self.sdcard.cdv, 0) != 0:
                # EIO错误
                raise OSError(5)

            # 发送数据
            offset = 0

            # 创建内存视图
            mv = memoryview(buf)
            while nblocks:
                # 发送每块数据
                self.sdcard.write(TOKEN_CMD25, mv[offset : offset + 512])
                # 更新偏移量
                offset += 512
                # 减少剩余块数
                nblocks -= 1
            # 发送停止传输命令
            self.sdcard.write_token(TOKEN_STOP_TRAN)

    def ioctl(self, op, arg):
        '''
        控制块设备并查询其参数
        :param op  [int]: 操作码
        :param arg : 附加参数
        :return: 操作结果
        '''
        # 初始化设备
        if op == 1:
            # 执行初始化操作
            self.sdcard.init_card(1320000)
            # 成功返回0
            return 0
        # 关闭设备
        elif op == 2:
            return 0
        # 同步设备
        elif op == 3:
            return 0
        # 获取块数
        elif op == 4:
            # 返回扇区数
            return self.sdcard.sectors
        # 获取块大小(字节)
        elif op == 5:
            # 返回512个字节
            return 512
        # 擦除块
        elif op == 6:
            # 擦除指定块
            if arg < 0 or arg >= self.sdcard.sectors:
                # 如果块号无效,抛出错误
                raise OSError("Invalid block number")
            self.sdcard.erase_block(arg)
            return 0
        else:
            # 无效的操作码,抛出异常
            raise OSError("Invalid ioctl operation")

# ======================================== 初始化配置 ==========================================

# ========================================  主程序  ============================================`
lurch commented 1 month ago

MicroPython v1.23.0 on RP2

Problems with MicroPython should be reported to https://github.com/orgs/micropython/discussions