MicroPython RP2 端口 rp2.Flash 类详解:SPI Flash 的低层访问、块协议实现与分区配置
【免费下载链接】micropythonMicroPython - a lean and efficient Python implementation for microcontrollers and constrained systems项目地址: https://gitcode.com/gh_mirrors/mi/micropython
本文围绕 MicroPythonrp2端口(Raspberry Pi Pico / RP2040、RP2350 平台)中的rp2.Flash类展开,系统讲解它的构造函数、readblocks/writeblocks/ioctl三个方法的块协议语义,并结合 rp2_flash.c 源码解析其 4K 扇区模型、擦写临界区处理、PSRAM 分块写入与MICROPY_HW_FLASH_STORAGE_BYTES分区配置机制。读完后你可以直接用它定制文件系统布局或为应用实现低层存储系统,并理解每次读/写块在硬件层面到底发生了什么。
何时需要 rp2.Flash
rp2.Flash是对板载 SPI flash 存储的直接访问接口,定义于 rp2.Flash.rst。官方文档明确指出:
- 在大多数情况下,要在设备上保存持久化数据,应使用更高层的抽象,例如通过 Python 标准文件 API 访问的文件系统;
rp2.Flash这类接口适合两类场景:定制文件系统配置(见 文件系统文档),或为应用实现低层存储系统。
也就是说,日常写文件用open()即可;只有当你需要绕过文件系统直接操作 flash 扇区(例如自建 KV 存储、管理多个存储分区、调试 VFS 块设备行为)时,才直接使用这个类。
构造函数:单例与子分区对象
.. class:: Flash()Flash()返回访问 SPI flash 的单例对象。对应实现见 rp2_flash.c 中的rp2_flash_make_new:
- 不带参数调用时,返回全局单例
rp2_flash_obj,它覆盖从MICROPY_HW_FLASH_STORAGE_BASE开始的整个MICROPY_HW_FLASH_STORAGE_BYTES存储分区; - 当前源码还支持两个仅关键字参数
start和len(默认均为 -1),用于构造覆盖 flash 存储区间子范围的Flash对象:start:起始字节偏移,必须在[0, MICROPY_HW_FLASH_STORAGE_BYTES)范围内且为块大小(4096 字节)的整数倍,否则抛出ValueError;len:长度(字节),必须为正数、start + len <= MICROPY_HW_FLASH_STORAGE_BYTES且为 4096 的整数倍,否则抛出ValueError;len = -1表示一直延伸到存储分区末尾。
import rp2 # 默认单例:访问整个 MicroPython 存储分区 flash = rp2.Flash() # 也可以取存储分区中 [1 MiB, 2 MiB) 的子范围 sub = rp2.Flash(start=1024 * 1024, len=1024 * 1024)从源码结构看,每个Flash对象只持有两个字段:flash_base(相对存储分区基址的偏移)和flash_size(对象定义),因此构造开销极低。
方法详解:readblocks / writeblocks / ioctl
rp2.Flash的三个方法实现了 vfs.AbstractBlockDev 定义的简单接口与扩展接口("block protocol"):
.. method:: Flash.readblocks(block_num, buf) Flash.readblocks(block_num, buf, offset) .. method:: Flash.writeblocks(block_num, buf) Flash.writeblocks(block_num, buf, offset) .. method:: Flash.ioctl(cmd, arg)readblocks(block_num, buf[, offset])
- 第一个形式:从块索引block_num开始,读取buf长度对应的整块数据(buf长度必须是块大小的整数倍);
- 第二个形式(扩展接口):允许在块内任意字节偏移offset处读取任意长度,用于需要子块读写的文件系统(如 littlefs)。
实现上就是直接从 XIP 映射地址XIP_BASE + flash_base + block_num * 4096 + offset做memcpy(readblocks 实现)。由于 flash 内容本就常驻 XIP 地址空间,读取无需任何 flash 控制器操作。值得注意的细节:每次读块后会调用mp_event_handle_nowait(),源码注释说明这是为了避免启动阶段设备忙于加载文件或扫描文件系统时 USB 注册失败——该调用会在需要时驱动 TinyUSB 任务。
writeblocks(block_num, buf[, offset])
写操作是 flash 存储中最复杂的部分,两种形式的语义与行为不同:
writeblocks(block_num, buf)(3 参数形式):写入块对齐的数据,方法内部先执行擦除(flash_range_erase),再编程写入(flash_range_program);writeblocks(block_num, buf, offset)(4 参数扩展形式):只写指定字节范围内的数据,不会擦除,调用方必须确保目标块已通过ioctl的块擦除命令提前擦好。
writeblocks 实现 还有两点硬件层面的处理:
- PSRAM 分块写入:当启用了
MICROPY_HW_ENABLE_PSRAM且数据源缓冲区位于 PSRAM/flash(非 SRAM)地址范围时,无法直接编程,源码会用一个FLASH_PAGE_SIZE(256 字节)大小的 SRAM 暂存缓冲区copy_buffer分块中转写入;数据源在 SRAM 时则直接调用flash_range_program。 - 临界区保护:每次擦除/编程都包裹在
begin_critical_flash_section()/end_critical_flash_section()中,见下文"擦写临界区"。
ioctl(cmd, arg)
ioctl支持块设备协议定义的标准命令,实现 中可确认的行为如下:
| 命令 | 参数 | rp2.Flash 的行为 |
|---|---|---|
MP_BLOCKDEV_IOCTL_INIT | 无 | 无操作,返回 0 |
MP_BLOCKDEV_IOCTL_DEINIT | 无 | 无操作,返回 0 |
MP_BLOCKDEV_IOCTL_SYNC | 无 | 无操作,返回 0 |
MP_BLOCKDEV_IOCTL_BLOCK_COUNT | 无 | 返回flash_size / 4096,即块数 |
MP_BLOCKDEV_IOCTL_BLOCK_SIZE | 无 | 返回 4096 |
MP_BLOCKDEV_IOCTL_BLOCK_ERASE | 块号 | 擦除指定块(4096 字节) |
块大小 4096 并非任意设定,源码中直接定义为BLOCK_SIZE_BYTES (FLASH_SECTOR_SIZE)(第 43 行)——一个块就是 SPI flash 的一个扇区,因为扇区是 flash 擦除的最小单位。擦除单个块时同样走flash_range_erase并包裹在临界区内。
源码级原理:擦写临界区与时钟时序
为什么擦写要"停世界"
RP2 系列的 CPU 直接从 flash 取指(XIP,execute in place)。执行擦除/编程命令时必须关闭 XIP,因此 begin_critical_flash_section 做了三件事:
- 若另一核心(core1)在运行,用
multicore_lockout_start_blocking()挂起它,防止其执行 flash 中的代码;use_multicore_lockout()还额外检查core1_entry != NULL,这是针对 pico-sdk #2201 的 workaround(core1 复位后multicore_lockout_victim_is_initialized仍可能误报); save_and_disable_interrupts()关中断,保存中断状态;- 若启用 PSRAM,先把维护空间(maintenance space)上段写入 0 以清理 XIP 缓存,提交 PSRAM 中的脏数据。
结束后 end_critical_flash_section 会恢复中断并重新调用rp2_flash_set_timing_internal()——因为 ROM 的编程函数会把 flash 时序寄存器重置为默认值。
Flash 时序自适应
rp2_flash_set_timing_internal 根据系统时钟重算 flash 时钟分频,目标频率由MICROPY_HW_FLASH_MAX_FREQ决定,未定义时默认为SYS_CLK_HZ / PICO_FLASH_SPI_CLKDIV(boot ROM 默认 CLKDIV 为 4),即 第 51-62 行。两条芯片路径的差异:
- RP2040(SSI):硬件只支持偶数分频,奇数分频会 +1;流程是等待 SSI 空闲 → 关闭 SSI → 写
baudr→ 重新使能; - RP2350(QMI):无忙标志,需先自旋等待 flash 片选处于非选中状态,再一次性写
QMI_M1_TIMING寄存器(RX 采样延迟取等于分频值,注释说明该时序在 133 MHz 下较紧但对 Winbond 闪存芯片可用),最后强制一次 XIP 读以使时序生效。
对外接口rp2_flash_set_timing()/rp2_flash_set_timing_for_freq(clock_hz)由 rp2_flash.h 声明,在系统时钟变更时被调用。
分区模型与构建配置
地址布局
存储分区在 flash 中的位置由 rp2_flash.c 计算,且编译期用static_assert保证合法(ROMFS 与存储分区大小都必须是 4K 的整数倍,且BASE + SIZE <= PICO_FLASH_SIZE_BYTES):
| 区域 | 基址 | 大小 |
|---|---|---|
| MicroPython 存储分区 | MICROPY_HW_FLASH_STORAGE_BASE,未定义时为PICO_FLASH_SIZE_BYTES - MICROPY_HW_FLASH_STORAGE_BYTES(即紧挨芯片 flash 末尾) | MICROPY_HW_FLASH_STORAGE_BYTES |
| ROMFS 分区(只读) | MICROPY_HW_ROMFS_BASE,紧邻存储分区下方,位于代码空间上端 | MICROPY_HW_ROMFS_BYTES,默认 0 |
此外,源码用 Pico SDK 的bi_decl(bi_block_device(...))把这两个分区标注进固件的二进制元数据(binary info):存储分区标记为可读写(不可重格式化),ROMFS 分区标记为只读(第 94-115 行),可供上位机工具解析固件的分区布局。
当启用 ROMFS 时,mp_vfs_rom_ioctl 会向 VFS ROM 接口报告 1 个只读段,并把静态的rp2_flash_romfs_obj作为块设备返回——这就是 ROMFS 分区能被挂载为只读文件系统的底层支撑。
每块板子的实际取值
MICROPY_HW_FLASH_STORAGE_BYTES由板级配置mpconfigboard.cmake给出(CMake 构建时经 CMakeLists.txt 传入,并通过--defsym导出给链接期使用的代码)。例如 ADAFRUIT_FEATHER_RP2040 配置为7340032(7 MiB),ARDUINO_NANO_RP2040_CONNECT 为14680064(14 MiB)。也就是说同一份rp2.Flash代码在不同板子上管理的空间大小由构建配置决定。
块协议视角与 VFS 的关系
rp2.Flash是 vfs.rst 所定义块设备协议的标准实现者,可以直接作为vfs.FatDevc/vfs.LfsBlockDev等文件系统块设备的后端。协议要点与rp2.Flash的对应关系:
readblocks(block_num, buf)的块数由buf长度决定,buf必须是块大小的整数倍;writeblocks的块对齐形式要求被写块先擦除——rp2.Flash在 3 参数形式中自动完成了这一步;- 扩展接口(带offset)供 littlefs 这类需要子块写入控制的文件系统使用,
rp2.Flash两种形式都实现了; - 成功时返回
None或 0,失败时应返回对应OSErrorerrno 的负数。rp2.Flash当前实现读写成功路径均返回None,且擦/写返回值标注了TODO check return value(见源码注释),可以推断返回值检查仍是后续可完善点。
实际产品中,RP2 端口的文件系统正是构建在这套接口之上,定制文件系统布局的完整说明见 文件系统文档。
实战示例:查询与擦写一个块
下面是一个可直接在 Pico 上运行的最小示例,演示块设备查询、擦除与写入读回(注意:写入的目标块必须落在 MicroPython 存储分区内,切勿覆盖运行中的文件系统数据):
import rp2 flash = rp2.Flash() # 查询块大小与块数(MP_BLOCKDEV_IOCTL_BLOCK_SIZE / BLOCK_COUNT) block_size = flash.ioctl(5, 0) # 4096 nblocks = flash.ioctl(4, 0) # flash_size // 4096 print("block_size:", block_size, " nblocks:", nblocks) # 选择存储分区末尾的一个块做试验 block_num = nblocks - 1 # 擦除该块(MP_BLOCKDEV_IOCTL_BLOCK_ERASE) flash.ioctl(6, block_num) # 写入一个完整块(3 参数形式内部也会先擦除) buf = bytearray(block_size) buf[:10] = b"Hello RP2!" flash.writeblocks(block_num, buf) # 读回验证 rd = bytearray(block_size) flash.readblocks(block_num, rd) print("readback:", bytes(rd[:10])) # 扩展接口:块内偏移写(需先保证块已擦除) flash.ioctl(6, block_num) # 先擦 flash.writeblocks(block_num, b"extended", 512) # 从块内 512 字节处写ioctl的命令号与 vfs.rst 中AbstractBlockDev文档列出的MP_BLOCKDEV_IOCTL_*常量一一对应,运行时也可以用vfs模块中定义的常量替代裸数字,提高可读性。
关键结论
rp2.Flash是rp2端口访问板载 SPI flash 的块设备接口,实现readblocks/writeblocks(含扩展接口)与ioctl三个方法,块大小固定为 flash 扇区 4096 字节;- 它管理的空间由
MICROPY_HW_FLASH_STORAGE_BYTES/MICROPY_HW_FLASH_STORAGE_BASE决定,默认位于芯片 flash 的末尾,各板子的具体数值见对应mpconfigboard.cmake; - 擦除与编程操作运行在"关中断 + 挂起另一核心"的临界区内,因为 XIP 取指必须让位于 flash 命令;启用 PSRAM 时写入会经 256 字节 SRAM 缓冲区分块中转;
- 日常持久化请用标准文件 API;
rp2.Flash面向的是定制文件系统配置和自建低层存储这类进阶场景,其完整协议规范见 vfs 块设备接口文档。
【免费下载链接】micropythonMicroPython - a lean and efficient Python implementation for microcontrollers and constrained systems项目地址: https://gitcode.com/gh_mirrors/mi/micropython
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考