MicroPython 的 mpy 文件

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

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

.mpy 文件的版本和兼容性

.mpy文件针对具体的MicroPython系统可能存在兼容性的问题。是否兼容主要取决以下几点:

  • .mpy文件的版本:文件的版本必须与加载该文件的系统所支持的版本一致。
  • .mpy文件的子版本:如果.mpy文件包含本地机器代码,则文件子版本必须与加载该文件的系统所支持的版本一致;如果没有本地机器代码,加载时则忽略子版本。
  • Small Int的位数:.mpy文件要求系统至少支持Small Int要求的位数(一般Small Int是两个字节16 Bit)。
  • 本地体系结构:如果.mpy文件包含本地机器代码,加载该文件的系统必须支持执行该体系结构的代码。

如果MicroPython系统支持导入.mpy文件,则存在sys.implementation._mpy属性,该属性为一个整数,其值是版本(低8位)、特性和本地架构编码后的数字。

导入.mpy文件时,如果未通过以上检查,将引发 ValueError('incompatible .mpy file')。如果未通过本地架构测试(文件包含本地机器代码)将引发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 --version显示的版本一致。如果不匹配,可以根据mpy-cross --version显示的标志(或哈希值)从Git仓库重新编译mpy-cross

  • 另外,要确保使用了正确的mpy-cross标志,可以通过上面的代码找到,也可以通过检查所用端口的MPY_CROSS_FLAGS Makefile变量找到。

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

MicroPython发行版本 .mpy版本
v1.22.0 或更高 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

为完整起见,下表显示了修改 .mpy 版本的 MicroPython 主版本库的Git提交的对应关系。

.mpy 版本变化 Git 提交
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
初始版本 d8c834c95d506db979ec871417de90b7951edc30

.mpy 文件的二进制编码

MicroPython.mpy文件是二进制容器格式(叫嵌套格式可能会更好理解,就是可以一层套一层),其代码对象(字节码和本地机器码)以嵌套的层次结构存储。外层模块代码首先存储,然后是其子模块。每个子模块都可能有更多的子模块,例如一个类的函数或方法中包含了匿名函数或子函数。为了保持较小的文件体积,同时提供较大的可能值范围,它在很多地方使用了可变编码无符号整数(vuint)的概念。这种编码与 utf-8 编码类似,每个字节存储 7 位,如果后面有一个或多个字节,则设置第 8 位(MSB)。无符号整数的位以LSB的形式存储在vuint中。

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

  • 文件头
  • 全局 qstr 表和常量表
  • 模块外部范围的原始代码。当导入 .mpy 文件时,外部范围将被执行。

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

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

文件头

.pyc的文件头:

大小 字段
byte 0x4d (ASCII ‘M’)
byte .mpy主版本号
byte 原生 Arch 和次版本号(旧版本中的功能标志)
byte Small int的位数

全局 qstr 和常量表

.mpy文件包含一个全局的qstr表和一个全局的常量对象表,供所有嵌套的原始代码引用。qstr表把mpy文件内部的qstr编号映射到运行时解析的qstr编号,把.mpy文件与执行系统的其他部分联系起来。常量对象表中填充了.mpy文件所需的所有常量对象的引用。

大小 字段
vuint qstr 数量
vuint 常量对象数量
qstr 数据
已编码常量对象

原始代码(Raw-Code)元素

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

大小 字段
vuint 类型,大小以及是否是子原始代码元素
代码(字节码或机器码)
vuint 子原始代码元素数量(只有非空情况下)
子原始代码

原始代码元素中的第一个vuint编码了存储代码类型(两个最小有效位)、是否有子代码(第三个最小有效位)以及代码的长度(为其分配的 RAM 容量)。

vuint之后是代码本身。除非代码类型是带有重定位的Viper代码,否则这些代码是常量数据,无需修改。

如果该原始代码有任何子代码(如第一个vuint中的一个位所示),则在代码之后会出现一个vuint,用来计算子原始代码元素的数量。

最后以递归方式存储子代码元素。

有啥用?

说到底这个.mpy有什么用呢?我觉得至少有两个作用:

  1. 提高性能,毕竟是已经编译后的代码,不用费劲再解释编译一遍了。
  2. 一定程度上保护源代码,防止代码被随意修改。

当然,不好的地方就是它的兼容性,对固件和运行环境有版本要求,使用的时候要考虑这一点。

你可能感兴趣的:(micropython,硬件,单片机,嵌入式硬件,python)