MicroPython .mpy 文件

MicroPython 定义了 .mpy 文件的概念,它是一种包含预编译代码的二进制容器文件格式,可以像普通的 .py 模块一样导入。 文件 foo.mpy 可以通过 import foo 导入,只要 foo.mpy 可以被导入机器以通常的方式找到。通常, sys.path 中列出的每个目录是按顺序搜索的。 当搜索特定目录时,首先寻找 foo.py ,如果没有找到,则寻找 foo.mpy ,如果都没有找到,则在下一个目录中继续搜索。 因此,foo.py 将优先于 foo.mpy

这些 .mpy 文件可以包含通常通过 mpy-cross 程序从 Python 源文件(.py 文件)生成的字节码。 对于某些体系结构,.mpy 文件还可以包含本地机器代码,这些代码可以通过多种方式生成,最显着的是从 C 源代码生成。

.mpy 文件的版本控制和兼容性

给定的 .mpy 文件可能与给定的 MicroPython 系统兼容,也可能不兼容。兼容性基于以下几点:

  • .mpy 文件的版本:文件的版本必须与加载它的系统支持的版本相匹配。

  • .mpy文件的子版本:如果.mpy文件包含本机机器码,则文件的子版本必须与加载它的系统支持的版本匹配。否则,如果.mpy文件中没有本机机器码,则在加载时忽略子版本。

  • 小整数位:.mpy 文件将需要小整数中的最少位数,并且加载它的系统必须至少支持这么多位。

  • 本地架构:如果 .mpy 文件包含本地机器代码,那么它将指定该机器代码的架构,并且加载它的系统必须支持该架构代码的执行。

如果 MicroPython 系统支持导入 .mpy 文件,则 sys.implementation.mpy 字段将存在并返回一个整数,该整数对版本(低 8 位)、功能和本机架构进行编码。

尝试导入前四个测试之一失败的 .mpy 文件将引发 ValueError('incompatible .mpy file')。 尝试导入未通过本机架构测试(如果它包含本机机器代码)的 .mpy 文件将引发 ValueError('incompatible .mpy arch')

如果导入.mpy文件失败,请尝试以下操作:

  • 通过执行以下命令确定 MicroPython 系统支持的 .mpy 版本和标志:

    import sys
    sys_mpy = sys.implementation._mpy
    arch = [None, 'x86', 'x64',
        'armv6', 'armv6m', 'armv7m', 'armv7em', 'armv7emsp', 'armv7emdp',
        'xtensa', 'xtensawin'][sys_mpy >> 10]
    print('mpy version:', sys_mpy & 0xff)
    print('mpy sub-version:', sys_mpy >> 8 & 3)
    print('mpy flags:', end='')
    if arch:
        print(' -march=' + arch, end='')
    print()
    
  • 通过检查文件的前两个字节来检查 .mpy 文件的有效性。 第一个字节应该是大写的 ‘M’ ,第二个字节是版本号,它应该与上面的系统版本匹配。如果不匹配,则重建 .mpy 文件。

  • 检查系统 .mpy 版本是否与 mpy-cross 发出的版本匹配,该版本用于构建 .mpy 文件,由 mpy-cross --version 找到。如果它不匹配,则从 mpy-cross --version 报告的标记(或哈希)处检出的 Git 存储库中重新编译 mpy-cross

  • 确保您使用了正确的 mpy-cross 标志,由上面的代码找到,或者通过检查您正在使用的移植版本的 MPY_CROSS_FLAGS Makefile 变量。

下表显示了 MicroPython 版本和 .mpy 版本之间的对应关系。

MicroPython 发布

.mpy 版本

v1.23.0 and up

6.3

v1.22.x

6.2

v1.20 - v1.21.0

6.1

v1.19.x

6

v1.12 - v1.18

5

v1.11

4

v1.9.3 - v1.10

3

v1.9 - v1.9.2

2

v1.5.1 - v1.8.7

0

为完整起见,下表显示了主 MicroPython 存储库的 Git 提交,在该存储库中 .mpy 版本发生了更改。

.mpy 版本变更

Git commit

6.2 to 6.3

bdbc869f9ea200c0d28b2bc7bfb60acd9d884e1b

6.1 to 6.2

6967ff3c581a66f73e9f3d78975f47528db39980

6 to 6.1

d94141e1473aebae0d3c63aeaa8397651ad6fa01

5 to 6

f2040bfc7ee033e48acef9f289790f3b4e6b74e5

4 to 5

5716c5cf65e9b2cb46c2906f40302401bdd27517

3 to 4

9a5f92ea72754c01cc03e5efcdfe94021120531e

2 to 3

ff93fd4f50321c6190e1659b19e64fef3045a484

1 to 2

dd11af209d226b7d18d5148b239662e30ed60bad

0 to 1

6a11048af1d01c78bdacddadd1b72dc7ba7c6478

初始版本 0

d8c834c95d506db979ec871417de90b7951edc30

.mpy文件的二进制编码

MicroPython .mpy 文件是一种二进制容器格式,代码对象(字节码和本机机器码)内部存储在嵌套层次结构中。 首先存储外部模块的代码,然后存储它的子模块。每个子类可以有更多的子类,例如具有方法的类,或者定义lambda或推导式的函数。为了保持文件较小,同时仍提供大范围的可能值,它在许多地方使用了可变编码无符号整数 (vuint) 的概念。 与 utf-8 编码类似,这种编码每字节存储 7 位,如果后面跟着一个或多个字节,则设置第 8 位 (MSB)。 无符号整数的位以 LSB 形式存储在 vuint 中。

.mpy文件的顶层由三部分组成:

  • 标题。

  • 全局qstr表和常量表。

  • 模块外部作用域的原始代码。这个外部作用域在导入.mpy文件时执行。

你可以使用 mpy-tool.py 来检查.mpy文件的内容,例如(从主MicroPython存储库的根目录运行):

$ ./tools/mpy-tool.py -xd myfile.mpy

标题

.mpy标题是:

尺寸

byte

value 0x4d (ASCII ‘M’)

byte

.mpy 主要版本号

byte

原生arch和次要版本号(旧版本中的特性标志)

byte

小 int 中的位数

全局qstr表和常量表

一个.mpy文件包含一个qstr表和一个常量对象表。这些对.mpy文件是全局的,它们被所有嵌套的原始代码对象引用。qstr表将内部qstr号(在.mpy文件内部)映射到导入.mpy文件的运行时的解析后的qstr号。这将.mpy文件与它在其中执行的系统的其余部分链接起来。使用对.mpy文件所需的所有常量对象的引用填充常量对象表。

尺寸

vuint

qstr的数量

vuint

常量对象的数量

qstr数据

编码常量对象

原始代码元素

原始代码元素包含代码,字节码或本地机器代码。 其内容是:

尺寸

vuint

类型、大小以及是否存在子原始代码元素

代码(字节码或机器代码)

vuint

子原始代码元素的数量(仅当非零时)

子原始代码元素

原始代码元素中的第一个 vuint 编码存储在该元素中的代码类型(两个最低有效位),该原始代码是否有任何子代码(第三个最低有效位),以及后面代码的长度(为其分配的RAM量)。

在vuint之后是代码本身。除非代码类型是带有重定位的viper代码,否则该代码是常量数据,不需要修改。

如果此原始代码有任何子代码(如第一个vuint中的位所示),则代码后面是一个vuint,用于计算子原始代码元素的数量。

最后,递归地存储所有sub-raw-code元素。