终于搞懂了!EEPROM 也能挂文件系统,MicroPython 让 AT24C256 像 SD 卡一样读写文件

1.实验前的准备

我们需要将风雅一号板-环境与存储采集板插入到风雅一号板-通用兼容扩展板上,同时打开风雅一号板-环境与存储采集板SWITCH1 拨码开关的 SCLSDA 选项:

1.JPEG

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

2.PNG

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

3.PNG

设备地址计算如下:

$$
\text{设备地址} = 0b1010 , 0000 + (0 \times 2^2 + 0 \times 2^1 + 0 \times 2^0) = 0x50
$$

树莓派 Pico 与外部 EEPROM 模块(AT24C256 芯片)连接如下表所示:

4.png

同时将在讲解 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 是否满足 AT24CXX EEPROM 的页大小要求,即必须是 8 的整数倍;然后验证传入的 eeprom 是否是 AT24CXX 实例,确保是有效的 EEPROM 设备,在确认无误后,初始化父类 AbstractBlockDev,同时记录 eeprom 设备实例,设置 block_size,并标记设备状态为关闭(device_open = False)。

5.png

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

6.png

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

7.png

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

8.png

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)

在这里,我们进行了如下工作:

9.png

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

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

10.png

在终端循环输出 EEPROM is powered on.Waiting... 时,我们可以拔出风雅一号板-环境与存储采集板断电;等待片刻后,再次插入风雅一号板-环境与存储采集板上电。

可以看到 EEPROM 芯片上文件系统中的数据读写功能和掉电数据保存功能能正常工作。

mmexport1782426352006.gif

我们可以使用下面命令测试擦除功能是否正常工作:

os.remove('eeprom/config.json')

以上指令删除了 config.json 文件,再输入下面指令查看 eeprom 文件夹下文件:

os.listdir('eeprom')

可以看到,此时文件被删除:
12.png

posted @ 2026-09-03 18:43  FreakStudio  阅读(14)  评论(0)    收藏  举报