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 使用它进行文件系统操作(如 littlefs、fatfs 等);而 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 类:

在读块设备数据 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 块设备 RAMBlockDev,RAMBlockDev 类放在单独的.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)中,依次进行如下操作:
- 计算读取的起始位置:
start = block_num * self.block_size + offset。 - 计算读取的结束位置:
end = start + len(buf)。 - 边界检查:
block_num是否超出范围(抛出OSError(ENODEV))。end不能超过block_num的块大小,否则抛OSError(EIO)。
- 数据复制:
buf[:] = self.data[start:end]:将bytearray切片赋值给buf,避免 Python 额外分配内存。
在写入操作 (writeblocks)中,流程类似读取操作 (readblocks),首先进行边界检查,接着计算写入地址的起始范围和结束范围,写入时直接修改 bytearray 以避免创建额外对象,除此之外,buf=None 时,执行擦除操作(将整个块填充为 0x00)。
在控制命令 ioctl() 中,根据输入的设备管理指令和入口参数来进行对应操作:

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)—— 传递的参数无效,例如除零操作或越界访问。
我们可以在如下链接中进一步了解:

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 的文件并打开,向其中写入数据并读取。
烧录代码,打开终端,显示如下:

可以看到,成功创建了文件系统和文件,向文件中写入的数据被成功读取。
我们可以尝试读取 RAM 块设备 bdev 的块数据,首先创建一个大小为 512 字节的 bytearray 缓冲区:
buf = bytearray(512)
接着使用 bdev 的 readblocks 方法读取块数据,首先读取第 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 关键参数等:

- 引导扇区签名:内容为
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')
输出数据内容包括保留簇和文件簇链信息:

- 保留簇:
\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')
输出数据内容包括目前已经存储的文件目录信息:

- 文件名:
HELLO TXT表示文件名,符合 FAT16/FAT32 的 8.3 文件名格式,HELLO为 8 字节的文件名,而TXT为 3 字节的扩展名。 - 文件属性:
0x18和0x00为文件属性位和保留位。 - 文件信息:
.\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 个数据块中。

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

- 调用
bdev.ioctl(4)获取块数量; - 调用
bdev.ioctl(5)获取块大小; - 在存储介质写入 FAT 文件系统元数据:包括引导扇区、FAT 表、根目录区等。
在写入文件时,进行如下操作:

- 文件系统查找空闲簇(调用
bdev.readblocks()读取 FAT 表); - 分配簇链并更新 FAT 表(调用
bdev.writeblocks()); - 写入文件内容。
在读取文件时,进行如下操作:

- 通过目录项定位文件起始簇。
- 调用
bdev.readblocks()读取文件数据。 - 返回内容给应用层。

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