终于搞懂了!EEPROM 也能挂文件系统,MicroPython 让 AT24C256 像 SD 卡一样读写文件
1.实验前的准备
我们需要将风雅一号板-环境与存储采集板插入到风雅一号板-通用兼容扩展板上,同时打开风雅一号板-环境与存储采集板上 SWITCH1 拨码开关的 SCL 和 SDA 选项:

你可以通过调节风雅一号板-环境与存储采集板上 ADDR 拨码开关设置其地址:

默认情况下,我们使用的外部 EEPROM 芯片 AT24C256 中 A2、A1 和 A0 引脚均连接在 GND(内部下拉)上:

设备地址计算如下:
$$
\text{设备地址} = 0b1010 , 0000 + (0 \times 2^2 + 0 \times 2^1 + 0 \times 2^0) = 0x50
$$
树莓派 Pico 与外部 EEPROM 模块(AT24C256 芯片)连接如下表所示:

同时将在讲解 I2C 协议时我们自定义的 AT24CXX 类也放入一个单独的 Python 文件 at24cxx.py 中,示例代码如下:
# Python env : MicroPython v1.23.0
# -*- coding: utf-8 -*-
# @Time : 2024/9/27 下午10:42
# @Author : 李清水
# @File : at24cxx.py
# @Description : 实现了一个AT24CXX类,用于操作AT24CXX系列EEPROM
# ======================================== 导入相关模块 =========================================
# 硬件相关的模块
from machine import I2C, Pin
# 时间相关的模块
import time
# ======================================== 全局变量 ============================================
# ======================================== 功能函数 ============================================
# ======================================== 自定义类 ============================================
class AT24CXX:
"""
AT24CXX类,用于通过I2C总线操作AT24CXX系列EEPROM芯片,支持多种容量的EEPROM。
该类封装了对AT24CXX EEPROM的I2C通信,提供了字节和页面写入、读取、擦除数据等功能。
支持不同容量(从32KiB到64KiB)的EEPROM芯片,通过I2C进行数据的读写操作。
Attributes:
i2c (I2C): I2C实例,用于与AT24CXX芯片通信。
chip_size (int): EEPROM芯片的大小,默认64KiB。
addr (int): I2C设备地址,默认值为0x50。
max_address (int): 设备的最大可访问地址,取决于EEPROM的大小。
Methods:
__init__(self, i2c, chip_size=AT24C512, addr=0x50):
初始化 AT24CXX 类实例。
write_byte(self, address: int, data: int) -> None:
向指定地址写入一个字节。
read_byte(self, address: int) -> int:
从指定地址读取一个字节。
write_page(self, address: int, data: bytes) -> None:
向指定地址写入一页数据。
read_sequence(self, start_address: int, length: int) -> bytes:
顺序读取指定长度的数据。
"""
# 用于AT24CXX系列EEPROM的不同容量
AT24C32 = 4096 # 4KiB
AT24C64 = 8192 # 8KiB
AT24C128 = 16384 # 16KiB
AT24C256 = 32768 # 32KiB
AT24C512 = 65536 # 64KiB
def __init__(self, i2c, chip_size: int = AT24C512, addr: int = 0x50) -> None:
"""
初始化 AT24CXX 类实例。
Args:
i2c (I2C): 用于与 AT24CXX 芯片通信的 I2C 实例。
chip_size (int): EEPROM 芯片的大小,默认为 AT24C512。
addr (int): I2C 设备地址,默认为 0x50。
Raises:
ValueError: 如果提供的 chip_size 不在支持的范围内。
"""
# 判断 EEPROM 芯片容量是否在 AT24CXX 类定义的范围内
if chip_size not in [AT24CXX.AT24C32, AT24CXX.AT24C64, AT24CXX.AT24C128,
AT24CXX.AT24C256, AT24CXX.AT24C512]:
raise ValueError("chip_size is not in the range of AT24CXX")
self.i2c = i2c
self.chip_size = chip_size
self.addr = addr
# 用户可以操作芯片的最大地址
self.max_address = chip_size - 1
def write_byte(self, address: int, data: int) -> None:
"""
向指定地址写入一个字节。
Args:
address (int): 写入的地址。
data (int): 要写入的数据,范围0-255。
Raises:
ValueError: 如果地址或数据超出有效范围。
"""
# 检查地址是否在有效范围内
if address < 0 or address > self.max_address:
raise ValueError('address is out of range')
# 检查数据是否在有效范围内
if data < 0 or data > 255:
raise ValueError("data must be 0-255")
# 从用户指定内存地址address开始,将bytes([data])写入设备地址为addr的EEPROM
# 内存地址为16位,两个字节
self.i2c.writeto_mem(self.addr, address, bytes([data]), addrsize=16)
# 延时5ms,等待EEPROM写入完成
time.sleep_ms(5)
def read_byte(self, address: int) -> int:
"""
从指定地址读取一个字节。
Args:
address (int): 读取的地址。
Returns:
int: 读取的数据。
Raises:
ValueError: 如果地址超出有效范围。
"""
# 检查地址是否在有效范围内
if address < 0 or address > self.max_address:
raise ValueError("address is out of range")
# 从指定地址读取一个字节
value_read = self.i2c.readfrom_mem(self.addr, address, 1, addrsize=16)
# 转换为整数并返回,使用大端序进行转换
return int.from_bytes(value_read, "big")
def write_page(self, address: int, data: bytes) -> None:
"""
向指定地址写入一页数据,处理跨页情况。
Args:
address (int): 写入的起始地址。
data (bytes): 要写入的数据,最大长度不受限制。
Raises:
ValueError: 如果地址、数据长度或数据超出有效范围。
"""
# 检查地址是否在有效范围内
if address < 0 or address > self.max_address:
raise ValueError("address is out of range")
# 检查列表中数据是否超出范围
for i in data:
if i < 0 or i > 255:
raise ValueError("data must be 0-255")
# 结合起始地址检查data长度是否超出范围
if address + len(data) > self.max_address:
raise ValueError("data exceeds maximum limit")
# 获取起始页的边界
page_boundary = (address // 64 + 1) * 64
# 分段写入数据
while data:
# 计算当前写入的字节数
write_length = min(len(data), page_boundary - address)
# 向指定地址写入一页数据
self.i2c.writeto_mem(self.addr, address, data[:write_length], addrsize=16)
# 写入后延时以确保完成
time.sleep_ms(5)
# 更新地址和数据
address += write_length
data = data[write_length:]
# 更新页边界
page_boundary = (address // 64 + 1) * 64
# 如果当前地址超出最大地址,则停止写入
if address > self.max_address:
raise ValueError("address exceeds maximum limit")
def read_sequence(self, start_address: int, length: int) -> bytes:
"""
顺序读取指定长度的数据,AT24CXX可以跨页读取,不会受到页边界限制。
Args:
start_address (int): 读取的起始地址。
length (int): 读取的字节数。
Returns:
bytes: 读取的数据。
Raises:
ValueError: 如果起始地址和长度超出有效范围。
"""
# 检查起始地址和长度是否在有效范围内
if start_address < 0 or (start_address + length) > self.max_address:
raise ValueError("address is out of range")
# 从指定起始地址读取指定长度的数据
return self.i2c.readfrom_mem(self.addr, start_address, length, addrsize=16)
# ======================================== 初始化配置 ==========================================
# ======================================== 主程序 ===========================================
2.EEPROM 块设备类的实现
这里,我们首先定义一个 EEPROMBlockDevice 类对 EEPROM(如 AT24C256)进行块设备管理,提供块的读写操作和设备控制功能,该类和 RAM 块设备 RAMBlockDev 类一样继承自抽象基类 AbstractBlockDev,实现了读、写、控制和擦除块的功能。
示例代码如下所示:
# Python env : MicroPython v1.23.0
# -*- coding: utf-8 -*-
# @Time : 2024/9/27 下午10:45
# @Author : 李清水
# @File : eeprom_block_dev.py
# @Description : 定义了一个EEPROMBlockDevice类,包括块的读写操作和设备控制功能
# ======================================== 导入相关模块 =========================================
# 导入虚拟文件块设备的抽象基类
from AbstractBlockDevInterface import AbstractBlockDev
# 导入AT24CXX类
from at24cxx import AT24CXX
# ======================================== 全局变量 ============================================
# 定义错误码常量,符合 POSIX 标准的错误码
ENOERR = 0 # 操作成功
EPERM = 1 # 操作不允许
EIO = 5 # I/O错误
ENODEV = 19 # 无效设备/块号
EROFS = 30 # 只读文件系统
EINVAL = 22 # 无效参数
ENOSPC = 28 # 没有空间
# ======================================== 功能函数 ============================================
# ======================================== 自定义类 ============================================
# 自定义EEPROM块设备类,继承自抽象块设备类
class EEPROMBlockDevice(AbstractBlockDev):
"""
EEPROM块设备实现类,提供符合MicroPython文件系统要求的块设备接口。
该类实现了AbstractBlockDev抽象接口,支持通过EEPROM存储器模拟块设备,
可用于挂载MicroPython的文件系统(如FAT、LittleFS等)。
Attributes:
eeprom (AT24CXX): AT24CXX系列EEPROM驱动实例
block_size (int): 块大小(字节),必须是EEPROM页大小的整数倍
_is_initialized (bool): 设备初始化状态标志
Methods:
__init__(self, eeprom, block_size=512):
初始化EEPROM块设备实例
readblocks(self, block_num, buf, offset=0):
从指定块读取数据到缓冲区
writeblocks(self, block_num, buf, offset=0):
写入数据到指定块或擦除块
ioctl(self, op, arg):
设备控制操作
_validate_block_num(self, block_num):
验证块号有效性(内部方法)
Note:
- 块大小必须是EEPROM页大小的整数倍(通常为8的倍数)
- 写操作前需要确保块已擦除(0xFF)
- 所有方法都是线程安全的
"""
def __init__(self, eeprom: AT24CXX, block_size: int = 512) -> None:
"""
初始化EEPROM块设备实例。
Args:
eeprom (AT24CXX): AT24CXX系列EEPROM驱动实例
block_size (int, optional): 块大小(字节),必须与文件系统块大小对齐。默认为512,
同时必须是EEPROM页大小的整数倍。
Returns:
None
Raises:
ValueError (EINVAL): 如果块大小不满足要求
OSError (ENODEV): 如果EEPROM设备无效
"""
# 检查EEPROM的页大小是否为块大小的整数倍
# AT24CXX 系列 EEPROM 的页大小为8字节的整数倍,因此块大小必须是8的整数倍
if block_size % 8 != 0 or block_size < 8:
raise ValueError(EINVAL, "Block size must be multiple of 8")
# 检查传入的eeprom是否为AT24CXX系列EEPROM
if not isinstance(eeprom, AT24CXX):
raise OSError(ENODEV, "Invalid EEPROM device")
# 父类的初始化函数
super().__init__()
# 保存EEPROM实例
self.eeprom = eeprom
# 设置块大小
self.block_size = block_size
# 初始化设备状态为未开启
self.device_open = False
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:
ValueError (EINVAL): 如果offset≠0或buf长度无效
OSError (EIO): 如果读取失败
OSError (ENODEV): 如果块号无效
"""
# 检查偏移量是否为0
if offset != 0:
raise ValueError(EINVAL, "Offset must be 0")
# 检查缓冲区长度是否超过块大小
if len(buf) > self.block_size:
raise ValueError(EINVAL, "Buffer length exceeds block size")
# 检查块号是否有效
if block_num < 0 or block_num >= self.ioctl(4, 0):
raise OSError(ENODEV, "Invalid block number")
# 读取数据
try:
# 计算写入的起始地址
addr = block_num * self.block_size
# 将读取的数据写入缓冲区
buf[:] = self.eeprom.read_sequence(addr, len(buf))
except Exception as e:
# 打印错误信息
raise OSError(EIO, "Read failed") from e
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:
ValueError (EINVAL): 如果参数无效
OSError (EIO): 写入失败
OSError (ENOSPC): 块号越界
"""
# 检查偏移量是否为0
if offset != 0:
raise ValueError(EINVAL, "Offset must be 0")
# 计算需要写入的数据长度
length = len(buf)
# 检查数据读取长度
if length > self.block_size:
raise ValueError(EINVAL, "Buffer too large")
# 检查块号是否有效
if block_num < 0 or block_num >= self.ioctl(4, 0):
raise OSError(ENOSPC, "Block number out of range")
# 尝试写入数据
try:
# 计算写入的起始地址
addr = block_num * self.block_size
# 如果buf为None,则擦除块
if buf is None:
self.eeprom.write_page(addr, bytearray([0xFF]*self.block_size))
else:
# 写入数据
self.eeprom.write_page(addr, buf)
except Exception as e:
raise OSError(EIO, "Write failed") from e
def ioctl(self, op: int, arg: int) -> int | None:
"""
设备控制操作。
Args:
op (int): 操作码(使用AbstractBlockDev常量):
- IOCTL_INIT (1): 初始化设备
- IOCTL_SHUTDOWN (2): 关闭设备
- IOCTL_SYNC (3): 数据同步
- IOCTL_BLK_COUNT (4): 获取总块数
- IOCTL_BLK_SIZE (5): 获取块大小
- IOCTL_BLK_ERASE (6): 擦除指定块
arg (int): 操作参数(依赖op):
- IOCTL_BLK_ERASE: 要擦除的块号
- 其他操作可忽略
Returns:
int | None:
- IOCTL_BLK_COUNT: 返回总块数
- IOCTL_BLK_SIZE: 返回块大小
- 其他成功操作返回0
- 不支持的op返回None
Raises:
ValueError: 如果擦除块号无效
NotImplementedError: 如果操作码不支持
"""
# 初始化设备
if op == AbstractBlockDev.IOCTL_INIT:
# 设置设备为开启状态
self.device_open = True
# 返回成功
return ENOERR
# 关闭设备
elif op == AbstractBlockDev.IOCTL_SHUTDOWN:
# 设置设备为关闭状态
self.device_open = False
# 返回成功
return ENOERR
# 同步设备
elif op == AbstractBlockDev.IOCTL_SYNC:
# 进行同步操作(如果需要)
# 返回成功
return ENOERR
# 获取块数量
elif op == AbstractBlockDev.IOCTL_BLK_COUNT:
# 计算并返回块数量
return self.eeprom.chip_size // self.block_size
# 获取块大小
elif op == AbstractBlockDev.IOCTL_BLK_SIZE:
# 返回块大小
return self.block_size
# 擦除指定块
elif op == AbstractBlockDev.IOCTL_BLK_ERASE:
# 获取要擦除的块号
block_num = arg
# 检查块号是否有效
if block_num < 0 or block_num >= self.eeprom.chip_size // self.block_size:
raise ValueError(EINVAL, "Invalid block number")
# 擦除指定块
self.writeblocks(arg, None)
# 返回成功
return ENOERR
else:
# 不支持的操作码
raise NotImplementedError("Unsupported ioctl operation")
# ======================================== 初始化配置 ==========================================
# ======================================== 主程序 ===========================================
这里,EEPROMBlockDevice 类具有以下方法:
__init__方法:该方法首先检查block_size是否满足AT24CXXEEPROM 的页大小要求,即必须是 8 的整数倍;然后验证传入的eeprom是否是AT24CXX实例,确保是有效的 EEPROM 设备,在确认无误后,初始化父类AbstractBlockDev,同时记录eeprom设备实例,设置block_size,并标记设备状态为关闭(device_open = False)。

readblocks方法:该方法用于读取指定块的数据到缓冲区,首先计算EEPROM中块的起始地址:addr = block_num * block_size,接着调用self.eeprom.read_sequence(addr, len(buf))从 EEPROM 读取数据,同时将读取的数据写入buf,用于上层文件系统访问,这里注意,AT24CXX可以跨页读取,不会受到页边界限制。

writeblocks方法:该方法用于向指定块写入数据,或者擦除块,首先计算块的起始地址:addr = block_num * block_size,如果buf为None,则执行擦除操作(写入全0xFF);否则,将buf数据写入 EEPROM;这里需要注意,在 eepromAT24CXX的驱动类中已经解决了跨页写入情况,所以这里无需担心块大小大于 EEPROM 页大小情况下需要分页写入的情况。

ioctl方法:该方法用于执行块设备的控制命令,例如初始化、同步、获取信息等,这几个功能与RAMBlockDev类类似,而擦除指定块时,则是创建一个全是0xFF的缓冲区并写入相应的块。

3.应用实验
以下代码可以在我们提供的资料包中的 elegance-devkit v1\Demo\74 FileSys_EEPROM 文件夹中找到。
在主程序中,我们在 AT24C256 EEPROM 芯片上创建并挂载一个 FAT 文件系统,以便存储和读取文件数据,同时测试了在 EEPROM 芯片上文件系统中的数据读写功能,也测试了 EEPROM 芯片的掉电数据保存功能。
示例代码如下:
# Python env : MicroPython v1.23.0
# -*- coding: utf-8 -*-
# @Time : 2024/9/27 下午10:43
# @Author : 李清水
# @File : mian.py
# @Description : 虚拟文件系统类使用,在外部EEPROM芯片上挂载文件系统
# ======================================== 导入相关模块 =========================================
# 硬件相关的模块
from machine import I2C, Pin
# 时间相关的模块
import time
# 导入AT24CXX类
from at24cxx import AT24CXX
# 导入自定义EEPROM块设备类
from eeprom_block_dev import EEPROMBlockDevice
# 导入虚拟文件类
import vfs
# 导入文件操作相关模块
import os
# 导入JSON模块
import json
# ======================================== 全局变量 ============================================
# AT24C256芯片地址为0x50,即0b1010000
# 高7位为地址位,低1位为读写控制位,0为写,1为读
AT24C256_ADDRESS = 0x50
# ======================================== 功能函数 ============================================
# ======================================== 自定义类 ============================================
# ======================================== 初始化配置 ==========================================
# 上电延时3s
time.sleep(3)
# 打印调试消息
print("FreakStudio: Mount the external EEPROM to the file system")
# 创建硬件I2C的实例,使用I2C0外设,时钟频率为400KHz,SDA引脚为4,SCL引脚为5
i2c_at24c256 = I2C(id=0, sda=Pin(4), scl=Pin(5), freq=400000)
# 创建AT24C256的实例
at24c256 = AT24CXX(i2c_at24c256, AT24CXX.AT24C256, AT24C256_ADDRESS)
# 创建块设备,使用外部EEPROM芯片,块大小为512字节,总共有64个块
block_device = EEPROMBlockDevice(eeprom=at24c256, block_size=512)
# 在块设备上创建一个 FAT 文件系统
vfs.VfsFat.mkfs(block_device)
# 将块设备挂载到虚拟文件系统的 /eeprom 目录
vfs.mount(block_device, '/eeprom')
# ======================================== 主程序 ===========================================
# 使用os模块查看当前目录并打印
print("Directory contents :",os.listdir())
# 配置数据
config_data = {
"micropython_version": "v1.23.0",
"required_modules": ["machine", "time", "vfs", "os", "json"],
"author": "leeqingshui",
"company": "FreakStudio"
}
# 将配置数据转换为JSON格式
json_data = json.dumps(config_data)
# 创建配置文件并写入数据
with open('/eeprom/config.json', 'w') as file:
# 写入JSON数据
file.write(json_data)
# 打印调试信息
print("Config file created.")
# 读取文件
try:
# 打开配置文件进行读取
with open('/eeprom/config.json', 'r') as file:
# 读取文件内容
content = file.read()
# 打印文件内容
print("Config file content:")
print(content)
# 捕获文件操作异常
except OSError as e:
# 打印错误信息
print("Error reading file:", e)
# 监测EEPROM电源状态,这里我们会拔下EEPROM电源
# 之后再接上,测试其是否能实现掉电后数据保存
print("Monitoring EEPROM power...")
# 无限循环,判断EEPROM是否掉电
while True:
try:
# 尝试扫描I2C设备
devices = i2c_at24c256.scan()
# 检查EEPROM的地址是否在设备地址列表中
if AT24C256_ADDRESS in devices:
print("EEPROM is powered on.Waiting...")
else:
# 若是不在设备地址列表中,说明EEPROM掉电
print("EEPROM is powered off.")
# 退出循环
break
# 等待1秒再检查
time.sleep(1)
# 捕获异常
except Exception as e:
# 打印扫描错误信息
print("Error scanning I2C devices:", e)
# 等待1秒再尝试
time.sleep(1)
# 无限循环,上电后读取EEPROM数据
while True:
# 扫描I2C设备
devices = i2c_at24c256.scan()
# 检查EEPROM的地址是否在设备地址列表中
if AT24C256_ADDRESS in devices:
# 打印EEPROM重新上电的消息
print("EEPROM is powered on.")
# 读取配置文件
try:
# 打开配置文件进行读取
with open('/eeprom/config.json', 'r') as file:
# 读取文件内容
content = file.read()
# 打印文件内容
print("Config file content after power restoration:")
print(content)
# 捕获文件操作异常
except OSError as e:
print("Error reading file after power restoration:", e)
# 退出循环
break
# 等待1秒后继续扫描
time.sleep(1)
在这里,我们进行了如下工作:

- 使用定义的
AT24C256实例创建一个块设备,定义块大小为 512 字节。 - 格式化块设备为 FAT 文件系统,并将其挂载到虚拟文件系统的
/eeprom目录。 - 通过字典定义一个配置数据,并将其转换为 JSON 格式,然后将其写入
/eeprom/config.json文件中。 - 打开配置文件进行读取并打印文件内容。
- 进入无限循环,持续检查 EEPROM 的电源状态,若是 EEPROM 掉电,则跳出循环。
- 进入另一个无限循环,持续扫描 I2C 设备,如果检测到了 EEPROM 已重新上电,尝试打开配置文件并读取内容,结束程序。
烧录代码,打开终端,显示如下:

在终端循环输出 EEPROM is powered on.Waiting... 时,我们可以拔出风雅一号板-环境与存储采集板断电;等待片刻后,再次插入风雅一号板-环境与存储采集板上电。
可以看到 EEPROM 芯片上文件系统中的数据读写功能和掉电数据保存功能能正常工作。

我们可以使用下面命令测试擦除功能是否正常工作:
os.remove('eeprom/config.json')
以上指令删除了 config.json 文件,再输入下面指令查看 eeprom 文件夹下文件:
os.listdir('eeprom')
可以看到,此时文件被删除:


我们需要将**`风雅一号板-环境与存储采集板`**插入到**`风雅一号板-通用兼容扩展板`**上,同时打开**`风雅一号板-环境与存储采集板`**上 `SWITCH1` 拨码开关的 `SCL` 和 `SDA` 选项:
浙公网安备 33010602011771号