MicroPython 简易块设备类实现:手把手教你从抽象接口到挂载 FAT 文件系统

1.块设备抽象基类的实现

前面我们讲到,MicroPython 没有提供块设备的操作方法,但是提供了一个抽象的块设备接口类,我们自定义块设备操作类需要符合 vfs.AbstractBlockDev 抽象类中定义的接口,才能实现在虚拟文件系统(VFS)中操作文件和存储设备。

以下是一个简单的 RAM 块设备实现示例(不建议实际使用,仅作演示):

class RAMBlockDev:
    def __init__(self, block_size, num_blocks):
        self.block_size = block_size
        self.data = bytearray(block_size * num_blocks)

    def readblocks(self, block_num, buf):
        for i in range(len(buf)):
            buf[i] = self.data[block_num * self.block_size + i]

    def writeblocks(self, block_num, buf):
        for i in range(len(buf)):
            self.data[block_num * self.block_size + i] = buf[i]

    def ioctl(self, op, arg):
        if op == 4:  # 获取块数
            return len(self.data) // self.block_size
        if op == 5:  # 获取块大小
            return self.block_size

这里,vfs.AbstractBlockDev 是一个抽象基类,定义了块设备应该实现的基本接口,任何自定义块设备类必须实现这些方法,才能通过 VFS 使用它进行文件系统操作(如 littlefsfatfs 等);而 RAMBlockDev 类是一个具体的实现,它继承了 vfs.AbstractBlockDev 的接口,表示一个使用内存作为存储介质的块设备,它模拟了在块设备上进行读取和写入操作的行为,并提供了相应的 ioctl 实现来获取块设备的大小、块数等信息的方法。

在以下示例代码中,我们首先创建一个 AbstractBlockDevInterface.py 文件用于放置抽象基类:

# 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:
    """
​    AbstractBlockDev 类,定义符合 MicroPython 规范的块设备操作接口。

​    该类为块设备提供抽象接口,支持简单接口(块对齐操作)和扩展接口(任意偏移操作),
​    兼容 MicroPython 文件系统(如 FAT 和 littlefs)的驱动需求。实现者需至少支持
​    ioctl(4) 块数量查询,若需支持 littlefs 还需实现 ioctl(6) 块擦除功能。

​    Attributes:
​        (由具体子类实现以下属性)
​        block_size (int): 块大小(字节),通常通过 ioctl(5) 暴露
​        block_count (int): 总块数,通常通过 ioctl(4) 暴露

​    Methods:
​        __init__():
​            初始化块设备抽象基类
​        readblocks(block_num, buf, offset=0):
​            从设备读取数据块到缓冲区
​        writeblocks(block_num, buf, offset=0):
​            将数据写入设备或擦除块
​        ioctl(op, arg):
​            控制设备操作和查询参数

​    Note:
​        - 简单接口:要求缓冲区长度与块大小对齐(offset=0)
​        - 扩展接口:支持任意偏移和长度(offset≠0)
​        - 写入时必须遵守:
​            - 简单接口需自动处理擦除
​            - 扩展接口禁止隐式擦除
​    """
​    ​# 标准操作码常量(类属性)
    IOCTL_INIT = 1      # 设备初始化
    IOCTL_SHUTDOWN = 2  # 设备关闭
    IOCTL_SYNC = 3      # 数据同步
    IOCTL_BLK_COUNT = 4 # 获取块数量(必须实现)
    IOCTL_BLK_SIZE = 5  # 获取块大小(可选)
    IOCTL_BLK_ERASE = 6 # 块擦除(littlefs必需)

    def __init__(self) -> None:
        """
​        初始化块设备抽象基类。

​        Args:
​            None

​        Returns:
​            None

​        Note:
​            - 必须至少支持 ioctl(4) 获取块数量
​            - 实现 littlefs 需额外支持 ioctl(6) 块擦除
​        """
​        ​pass

    def readblocks(self, block_num: int, buf: bytearray, offset: int = 0) -> None:
        """
​        从设备读取数据块到缓冲区。

​        Args:
​            block_num (int): 起始块号(从 0 开始)。
​            buf (bytearray): 存储读取数据的缓冲区。
​            offset (int, optional): 块内字节偏移量(默认 0 表示完整块读取)。

​        Returns:
​            None

​        Raises:
​            OSError: 如果 block_num 无效(ENODEV)。
​            OSError: 如果读取失败(EIO)。
​            NotImplementedError: 如果扩展接口未实现(当 offset≠0)。
​        """
​        ​raise NotImplementedError("sub class must implement this method")

    def writeblocks(self, block_num: int, buf: bytearray | None, offset: int = 0) -> None:
        """
​        将数据写入块设备。

​        Args:
​            block_num (int): 目标块号(从 0 开始)
​            buf (bytearray | None): 要写入的数据(None 表示擦除块)
​            offset (int, optional): 块内字节偏移量(默认 0 表示完整块写入)

​        Returns:
​            None

​        Raises:
​            OSError: 如果 block_num 无效(ENODEV)
​            OSError: 如果写入失败(EIO)
​            OSError: 如果设备为只读(EROFS)
​            NotImplementedError: 如果扩展接口未实现(当 offset≠0)
​        """
​        ​raise NotImplementedError("sub class must implement this method")

    def ioctl(self, op: int, arg: int) -> int | None:
        """
​        设备控制操作。

​        Args:
​            op (int): 操作码(应使用类常量):
​                - `AbstractBlockDev.IOCTL_INIT` (1)       : 设备初始化
​                - `AbstractBlockDev.IOCTL_SHUTDOWN` (2)   : 设备关闭
​                - `AbstractBlockDev.IOCTL_SYNC` (3)       : 数据同步
​                - `AbstractBlockDev.IOCTL_BLK_COUNT` (4)  : 获取设备总块数(必须实现)
​                - `AbstractBlockDev.IOCTL_BLK_SIZE` (5)   : 获取块大小(可选)
​                - `AbstractBlockDev.IOCTL_BLK_ERASE` (6)  : 擦除块(littlefs 需要)

​            arg (int): 操作参数(依赖 op),部分操作无需参数:
​                - `IOCTL_BLK_ERASE` (6) 需要传入要擦除的块号
​                - 其他操作可传 `0` 或 `None`,由子类具体定义

​        Returns:
​            int | None:
​                - `IOCTL_BLK_COUNT` (4): 返回设备的总块数(int)
​                - `IOCTL_BLK_SIZE` (5) : 返回块大小(int)
​                - `IOCTL_BLK_ERASE` (6): 擦除成功返回 `0`
​                - 其他操作默认返回 `None`

​        Raises:
​            NotImplementedError: 如果子类未实现该方法
​        """
​        ​raise NotImplementedError("sub class must implement this method")

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

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

该类设计的目的在于统一不同存储介质(Flash、RAM、SD 卡等)操作方法的同时支持 FAT/littlefs 等文件系统的底层驱动需求,并且支持提供简单接口(块对齐)和扩展接口(任意偏移)。

这里,我们按照官方文档中要求的方法定义 vfs.AbstractBlockDev 类:

1.png

在读块设备数据 readblocks 方法和写块设备数据 writeblocks 方法中,将偏移量 offset 设置为默认参数以支持非块对齐读取和任意长度数据的写入,并且对于设备控制操作 ioctl 方法,在类属性中定义了标准操作码常量:

# 标准操作码常量(类属性)
IOCTL_INIT = 1      # 设备初始化
IOCTL_SHUTDOWN = 2  # 设备关闭
IOCTL_SYNC = 3      # 数据同步
IOCTL_BLK_COUNT = 4 # 获取块数量(必须实现)
IOCTL_BLK_SIZE = 5  # 获取块大小(可选)
IOCTL_BLK_ERASE = 6 # 块擦除(littlefs必需)

2.RAM 块设备类的实现

具体的块设备类(如 RAM 块设备、SD 卡等)需要继承抽象基类 vfs.AbstractBlockDev,并实现其中的方法,我们在以下代码中实现了一个简单的 RAM 块设备 RAMBlockDevRAMBlockDev 类放在单独的.py 文件中:

# Python env   : MicroPython v1.23.0
# -*- coding: utf-8 -*-        
# @Time    : 2024/9/18 下午12:15   
# @Author  : 李清水            
# @File    : ram_block_dev.py.py       
# @Description : 继承 AbstractBlockDev 类,实现了一个简单的 RAM 块设备 RAMBlockDev,包括块的读写操作和设备控制功能

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

# 导入虚拟文件块设备的抽象基类
from AbstractBlockDevInterface import AbstractBlockDev

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

# 定义错误码常量,符合 POSIX 标准的错误码
ENOERR = 0       # 操作成功
EPERM = 1        # 操作不允许
EIO = 5          # I/O错误
ENODEV = 19      # 无效设备/块号
EROFS = 30       # 只读文件系统
EINVAL = 22      # 无效参数

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

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

class RAMBlockDev(AbstractBlockDev):
    """
​    RAM 模拟的块设备实现,使用内存字节数组作为存储介质

​    Attributes:
​        block_size (int): 每个块的字节大小(只读)
​        num_blocks (int): 设备总块数(只读)
​        initialized (bool): 设备初始化状态标志
​    """

​    ​def __init__(self, block_size: int, num_blocks: int) -> None:
        """
​        初始化RAM块设备

​        Args:
​            block_size (int): 每块字节大小(必须≥512)
​            num_blocks (int): 总块数(必须≥1)

​        Raises:
​            ValueError: 当block_size<512或num_blocks<1时抛出
​        """
​        ​# 检查参数
        if block_size < 512:
            raise ValueError("Block size must be ≥512 bytes")
        if num_blocks < 1:
            raise ValueError("At least 1 block required")

        # 块大小
        self.block_size = block_size
        # 块数量
        self.num_blocks = num_blocks
        # 创建一个字节数组作为设备存储
        self.data = bytearray(block_size * num_blocks)
        # 设备初始化状态标志
        self.initialized = False

    def readblocks(self, block_num: int, buf: bytearray, offset: int = 0) -> None:
        """
​        从设备读取数据(支持扩展接口)

​        Args:
​            block_num (int): 起始块号(0 ≤ block_num < num_blocks)
​            buf (bytearray): 接收数据的缓冲区
​            offset (int): 块内字节偏移量(默认为0)

​        Returns:
​            None

​        Raises:
​            OSError: 如果 block_num 越界(使用 ENODEV)。
​            OSError: 如果读取超出当前块范围(使用 EIO)。
​        """
​        ​# 检查 block_num 是否在合法范围内
        if not 0 <= block_num < self.num_blocks:
            raise OSError(ENODEV)

        # 计算读取的起始位置
        start = block_num * self.block_size + offset
        # 计算读取的结束位置
        end = start + len(buf)

        # 确保读取的数据不超出该块范围
        if end > (block_num + 1) * self.block_size:
            raise OSError(EIO)

        # 从设备存储中读取数据到缓冲区
        buf[:] = self.data[start:end]

    def writeblocks(self, block_num: int, buf: bytearray | None, offset: int = 0) -> None:
        """
​        写入数据到设备(支持擦除操作)

​        Args:
​            block_num (int): 目标块号(0 ≤ block_num < num_blocks)
​            buf (bytearray|None):
​                - bytearray: 要写入的数据
​                - None: 擦除指定块(offset必须为0)
​            offset (int): 块内字节偏移量(默认为0)

​        Returns:
​            None

​        Raises:
​            OSError: 如果 block_num 越界(使用 ENODEV)。
​            OSError: 如果写入超出当前块范围(使用 EIO)。
​            OSError: 如果擦除操作时 offset ≠ 0(使用 OSError(22) 表示无效参数)。
​        """
​        ​# 检查 block_num 是否在合法范围内
        if not 0 <= block_num < self.num_blocks:
            raise OSError(ENODEV)

        # 执行块擦除操作
        if buf is None:
            if offset != 0:
                # 无效参数,擦除必须整块进行
                raise OSError(EINVAL)
            # 擦除指定块
            start = block_num * self.block_size
            end = (block_num + 1) * self.block_size
            self.data[start:end] = bytes(self.block_size)
            return

        # 计算写入的起始位置
        start = block_num * self.block_size
        # 计算写入的结束位置
        end = start + len(buf)

        # 确保写入数据不超出该块范围
        if end > (block_num + 1) * self.block_size:
            raise OSError(EIO)

        # 将数据写入设备存储
        self.data[start:end] = buf

    def ioctl(self, op: int, arg: int | None = None) -> int | None:
        """
​        设备控制操作。

​        Args:
​            op (int): 操作码(应使用类常量):
​                - AbstractBlockDev.IOCTL_INIT (1)       : 初始化设备。
​                - AbstractBlockDev.IOCTL_SHUTDOWN (2)   : 关闭设备。
​                - AbstractBlockDev.IOCTL_SYNC (3)       : 数据同步。
​                - AbstractBlockDev.IOCTL_BLK_COUNT (4)  : 获取设备总块数(必须实现)。
​                - AbstractBlockDev.IOCTL_BLK_SIZE (5)   : 获取块大小(可选)。
​                - AbstractBlockDev.IOCTL_BLK_ERASE (6)  : 擦除块(littlefs 需要)。
​            arg (int | None, optional): 操作参数,依 op 而定:
​                - 对于 IOCTL_BLK_ERASE (6):传入要擦除的块号(int)。
​                - 对于其他操作:可传 None 或 0,由子类具体定义。

​        Returns:
​            int | None:
​                - 对于 IOCTL_BLK_COUNT (4):返回设备总块数(int)。
​                - 对于 IOCTL_BLK_SIZE (5) :返回块大小(int)。
​                - 对于 IOCTL_BLK_ERASE (6):擦除成功返回 0。
​                - 对于其他操作:默认返回 0 或 None。

​        Raises:
​            OSError: 如果擦除时块号越界(使用 ENODEV)。
​            OSError: 如果 op 不受支持(使用 EPERM)。
​        """
​        ​# 处理不同的操作
        # 初始化设备
        if op == AbstractBlockDev.IOCTL_INIT:
            self.initialized = True
            # 返回0表示初始化成功
            return 0
        # 关闭设备
        elif op == AbstractBlockDev.IOCTL_SHUTDOWN:
            self.initialized = False
            return 0
        # 同步设备
        elif op == AbstractBlockDev.IOCTL_SYNC:
            # 在RAM块设备中,写入操作会立即生效,所以不需要实现额外的同步操作
            # 返回0表示同步成功
            return 0
        # 获取块数
        elif op == AbstractBlockDev.IOCTL_BLK_COUNT:
            # 返回块的总数量
            return self.num_blocks
        # 获取块大小
        elif op == AbstractBlockDev.IOCTL_BLK_SIZE:
            # 返回块的大小
            return self.block_size
        # 擦除块
        elif op == AbstractBlockDev.IOCTL_BLK_ERASE:
            # 检查参数是否正确传入
            if arg is None:
                raise OSError(ENODEV)
            # 获取要擦除的块号
            block_num = arg
            if 0 <= block_num < self.num_blocks:
                # 计算擦除的起始位置
                start = block_num * self.block_size
                # 计算擦除的结束位置
                end = (block_num+1)*self.block_size
                # 将指定块的数据设置为全0(擦除)
                self.data[start:end] = bytes(self.block_size)
                # 返回0表示擦除成功
                return 0
            else:
                # 块号超出范围
                raise OSError(ENODEV)
        # 处理不支持的操作码
        else:
            # 抛出异常
            raise OSError(EPERM)

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

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

RAMBlockDev 类模拟了一个基于 RAM 的块设备,使用内存中的 bytearray 来存储数据:

  • 在初始化方法中,用 bytearray 创建一个内存缓冲区,模拟块设备的存储区域
  • 提供 readblocks()writeblocks()ioctl() 接口,确保与 MicroPython 的 VFS(Virtual File System)兼容。
  • 在控制和查询操作中,接收对应操作码和参数执行对应命令,支持设备初始化、同步、擦除等操作。

在初始化方法中,将多个块组织为连续的字节数组:

self.data = bytearray(block_size * num_blocks)

则可以得到一个块寻址公式,即 块N的起始地址 = N * block_size

在读取操作 (readblocks)中,依次进行如下操作:

  1. 计算读取的起始位置: start = block_num * self.block_size + offset
  2. 计算读取的结束位置: end = start + len(buf)
  3. 边界检查:
    1. block_num 是否超出范围(抛出 OSError(ENODEV))。
    2. end 不能超过 block_num 的块大小,否则抛 OSError(EIO)
  4. ​数据复制:​buf[:] = self.data[start:end]:将 bytearray 切片赋值给 buf,避免 Python 额外分配内存。

在写入操作 (writeblocks)中,流程类似读取操作 (readblocks),首先进行边界检查,接着计算写入地址的起始范围和结束范围,写入时直接修改 bytearray 以避免创建额外对象,除此之外,buf=None 时,执行擦除操作(将整个块填充为 0x00)。

在控制命令 ioctl() 中,根据输入的设备管理指令和入口参数来进行对应操作:

2.png

  • IOCTL_INIT​​ (1):​设备初始化,设置 self.initialized = True
  • IOCTL_SHUTDOWN​​ (2):​设备关闭,设置 self.initialized = False
  • IOCTL_SYNC​​ (3):​在 RAM 中无需同步,直接返回 0
  • IOCTL_BLK_COUNT​​ (4):​返回 num_blocks 以供文件系统查询。
  • IOCTL_BLK_SIZE​​ (5):​返回 block_size
  • IOCTL_BLK_ERASE​​ (6):​擦除单个块(设置为全 0x00)。
  • 其他操作返回 OSError(EPERM),表示不允许。

除此之外,我们定义了符合 POSIX 标准(Portable Operating System Interface,便携式操作系统接口)的错误码(errno),用于表示系统调用或库函数执行失败的原因:

  • ENOERR = 0​ (无错误)​—— 操作成功。
  • EPERM = 1​ (Operation not permitted)​—— 没有权限执行操作,例如普通用户尝试修改系统文件。
  • EIO = 5​ (I/O error)​—— 输入/输出错误,例如磁盘读写失败。
  • ENODEV = 19​ (No such device)​—— 设备不存在,通常是尝试访问未连接的外部设备。
  • EROFS = 30​ (Read-only file system)​—— 试图在只读文件系统上执行写入操作。
  • EINVAL = 22​ (Invalid argument)​—— 传递的参数无效,例如除零操作或越界访问。

我们可以在如下链接中进一步了解:

3.png

https://docs.particle.io/reference/device-os/api/debugging/posix-errors/

3.应用实验

以下代码可以在我们提供的资料包中的 elegance-devkit v1\Demo\72 FileSys_BlockDev 文件夹找到。

这个块设备可以用来创建文件系统并挂载,也可以创建文件或读写文件,在以下代码中,我们测试基于 RAM 的块设备 RAMBlockDev 类的功能,包括写入、读取和控制操作的验证,示例代码如下:

# Python env   : MicroPython v1.23.0
# -*- coding: utf-8 -*-        
# @Time    : 2024/9/18 下午12:15   
# @Author  : 李清水            
# @File    : main.py       
# @Description : 测试 RAMBlockDev 类的功能,包括写入、读取和控制操作的验证

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

# 导入自定义块设备实现类
from ram_block_dev import RAMBlockDev
# 导入虚拟文件类
import vfs
# 导入时间相关模块
import time

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

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

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

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

# 上电延时3s
time.sleep(3)
# 打印调试信息
print("FreakStudio : RAMBlockDev Write and Read Test")

# 创建一个块大小为512字节,共50个块的RAM块设备
bdev = RAMBlockDev(512, 50)
# 在块设备上创建一个 FAT 文件系统
vfs.VfsFat.mkfs(bdev)
# 将块设备挂载到虚拟文件系统的 /ramdisk 目录
vfs.mount(bdev, '/ramdisk')

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

# 在创建的虚拟文件系统中,创建一个文件 hello.txt,并打开文件
with open('/ramdisk/hello.txt', 'w') as f:
    # 写入内容
    f.write('FreakStudio : Hello World')

# 读取文件内容并打印
print(open('/ramdisk/hello.txt').read())

在这段代码中,我们首先创建一个块大小为 512 字节,总共 50 个块的 RAM 块设备,在块设备上创建一个 FAT 文件系统并将块设备挂载到 /ramdisk 目录,使其可以作为文件系统使用;然后在挂载的虚拟文件系统 /ramdisk 中创建一个名为 hello.txt 的文件并打开,向其中写入数据并读取。

烧录代码,打开终端,显示如下:

4.png

可以看到,成功创建了文件系统和文件,向文件中写入的数据被成功读取。

我们可以尝试读取 RAM 块设备 bdev 的块数据,首先创建一个大小为 512 字节的 bytearray 缓冲区:

buf = bytearray(512)

接着使用 bdevreadblocks 方法读取块数据,首先读取第 0 块数据,这是引导扇区数据:

>>> bdev.readblocks(0, buf, 0)
>>> buf
bytearray(b'\xeb\xfe\x90MSDOS5.0\x00\x02\x01\x01\x00\x01\x00\x022\x00\xf8\x01\x00?\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x00).\x00!RNO NAME    FAT     \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00U\xaa')

输出数据内容包括引导扇区签名、OEM 标识、BPB 关键参数等:

5.png

  • ​引导扇区签名:​内容为 bytearray(b'\xeb\xfe\x90MSDOS5.0'),FAT 文件系统的引导扇区以 \xEB 开头,它是一个短跳转指令(JMP),用于跳过引导代码的部分内容,\x90 为填充字节。
  • ​OEM 标识:​MSDOS5.0 表示这是 MSDOS 5.0 兼容的 FAT 文件系统。
  • ​BPB 关键字段:​内容含义如下:
\x00\x02   # 每个扇区的字节数(通常为 512,即 0x0200)
\x01       # 每个簇的扇区数(1 个扇区)
\x01\x00   # 保留扇区数(通常为 1)
\x01       # FAT 表的个数(通常 2)
\x00\x02   # 根目录的条目数(32 个根目录条目)
2\x00\xf8  # 介质描述符(0xF8 表示硬盘)

除此之外还包括隐藏扇区数、每 FAT 表扇区数等信息。

  • ​扩展 BPB 字段:​NO NAME 表示未命名卷,FAT12 表示格式化类型。
  • ​引导扇区结束标志:​0x55 0xAA 是引导扇区的结束标志。

接着读取第一块数据,这是 FAT 表数据:

>>> bdev.readblocks(1, buf, 0)
>>> buf
bytearray(b'\xf8\xff\xff\xff\x0f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')

输出数据内容包括保留簇和文件簇链信息:

6.png

  • 保留簇:\xf8\xff\xff\xff 是 FAT 的保留簇(0xFFF8),表示该扇区是 FAT 介质描述符。
  • ​文件簇链信息:​之后的 \x0f\x00\x00\x00... 是 FAT 记录,表示已分配和空闲的簇,这里簇 2 为 0x000F,表示文件 hello.txt 独占簇 2。

然后我们读取第二块数据,这里为 FAT 目录区:

>>> bdev.readblocks(2, buf, 0)
>>> buf
bytearray(b'HELLO   TXT \x18\x00.\x00!R\x00\x00\x00\x00.\x00!R\x02\x00\x19\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')

输出数据内容包括目前已经存储的文件目录信息:

7.png

  • ​文件名:​HELLO TXT 表示文件名,符合 FAT16/FAT32 的 8.3 文件名格式,HELLO 为 8 字节的文件名,而 TXT 为 3 字节的扩展名。
  • ​文件属性:​0x180x00 为文件属性位和保留位。
  • 文件信息:.\x00!R\x00\x00\x00\x00.\x00!R\x02\x00 表示文件创建时间、起始簇号、大小等信息。

我们可以通过下面代码尝试寻找存储 hello.txt 文件内容的数据块:

search_str = b'FreakStudio : Hello World'

for block_num in range(50):  # 扫描所有50个块
    buf = bytearray(512)
    bdev.readblocks(block_num, buf, 0)
    if search_str in buf:
        print(f"Found file content in block: {block_num}")
        break

输出结果为 34,我们查看第 34 个块中内容:

Found file content in block: 34
>>> bdev.readblocks(34, buf, 0)
>>> buf
bytearray(b'FreakStudio : Hello World\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')

可以看到验证无误,hello.txt 文件内容存储在第 34 个数据块中。

00.gif

实际上,在文件系统格式化时,主要流程可以概括为:

10.png

  1. 调用 bdev.ioctl(4) 获取块数量;
  2. 调用 bdev.ioctl(5) 获取块大小;
  3. 在存储介质写入 FAT 文件系统元数据:包括引导扇区、FAT 表、根目录区等。

在写入文件时,进行如下操作:

11.png

  1. 文件系统查找空闲簇(调用 bdev.readblocks() 读取 FAT 表);
  2. 分配簇链并更新 FAT 表(调用 bdev.writeblocks());
  3. 写入文件内容。

在读取文件时,进行如下操作:

12.png

  1. 通过目录项定位文件起始簇。
  2. 调用 bdev.readblocks() 读取文件数据。
  3. 返回内容给应用层。
posted @ 2026-09-03 17:53  FreakStudio  阅读(10)  评论(0)    收藏  举报