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
有什么用呢?我觉得至少有两个作用:
- 提高性能,毕竟是已经编译后的代码,不用费劲再解释编译一遍了。
- 一定程度上保护源代码,防止代码被随意修改。
当然,不好的地方就是它的兼容性,对固件和运行环境有版本要求,使用的时候要考虑这一点。