MicroPython .mpy 文件¶
MicroPython 定义了 .mpy 文件的概念,它是一种保存预编译代码的二进制容器文件格式,并且可以像普通的 .py 模块一样被导入。文件 foo.mpy 可以通过 import foo 导入,只要导入机制能以通常的方式找到 foo.mpy 即可。通常,会按顺序搜索 sys.path 中列出的每个目录。在搜索某个特定目录时,会首先查找 foo.py,如果未找到则查找 foo.mpy,如果两者都未找到则继续在下一个目录中搜索。因此,foo.py 的优先级高于 foo.mpy。
这些 .mpy 文件可以包含 bytecode,它通常是通过 mpy-cross 程序从 Python 源文件(.py 文件)生成的。对于某些架构,.mpy 文件还可以包含原生机器码,它可以通过多种方式生成,最常见的是从 C 源代码生成。
mpy-cross 编译器¶
mpy-cross 是一个交叉编译器,它将 .py 源文件转换为可在摄像头上导入的 .mpy 二进制容器。它是 MicroPython 源代码树(与构建摄像头固件所用的源代码树相同)的一部分,并且也作为 pip 包发布,以便在不进行完整固件检出的情况下用于主机端:
$ pip install --user mpy-cross
或者通过 pipx 安装:
$ pipx install mpy-cross
安装完成后,对单个源文件调用它:
$ mpy-cross foo.py
这会在当前目录中生成 foo.mpy,可以将其与其他模块一起复制到摄像头的文件系统中,或者用于生成 ROMFS 镜像。
最常用的命令行选项:
-o <path>—— 生成的.mpy的输出路径(默认为输入文件名并替换扩展名;-o -写入到标准输出)。-O<n>—— 优化级别0到3。默认的0会保留断言和完整的源代码位置;3会去除断言和文档字符串,并重写if __debug__代码块。该级别控制的是运行时所暴露的同一个micropython.opt_level接口。-march=<arch>—— 用于@native和@viper装饰的函数的目标原生架构。当源代码使用这些装饰器时为必需项。该值必须与摄像头的 MCU 类别相匹配:可从mpy-cross --help打印的列表中选取,或在运行时通过sys.implementation._mpy从摄像头上读取。-s <path>—— 嵌入到.mpy调试信息中的源路径字符串。当磁盘上的路径与该文件在回溯中应显示的导入路径不同时很有用。-X emit=bytecode|native|viper—— 为整个模块选择默认的发射器(这是@native/@viper装饰器的一种逐函数替代方案)。--version—— 打印该二进制文件发射的.mpy格式版本。该版本号必须与摄像头运行时所支持的版本相匹配(参见下方的发布对照表),否则导入将引发ValueError('incompatible .mpy file')。
运行 mpy-cross --help 可查看完整的标志列表。
该 pip 包还暴露了一个小型 Python 模块 API,以便构建脚本可以在进程内驱动编译器,而不必手动派生子进程:
import mpy_cross
mpy_cross.compile('foo.py', dest='build/foo.mpy', opt=3,
march=mpy_cross.NATIVE_ARCH_ARMV7EMSP)
mpy_cross.compile、mpy_cross.run 和 mpy_cross.mpy_version 是三个入口点;当出现错误时,mpy_cross.CrossCompileError 会携带编译器的 stderr。架构常量(NATIVE_ARCH_ARMV7EMSP、NATIVE_ARCH_ARMV7EMDP 等)与 -march 标志所接受的字符串相对应。
.mpy 文件的版本控制与兼容性¶
给定的 .mpy 文件可能与给定的 MicroPython 系统兼容,也可能不兼容。兼容性基于以下几点:
.mpy 文件的版本:文件的版本必须与加载它的系统所支持的版本相匹配。
.mpy 文件的子版本:如果 .mpy 文件包含原生机器码,则文件的子版本必须与加载它的系统所支持的版本相匹配。否则,如果 .mpy 文件中没有原生机器码,则在加载时忽略子版本。
小整数位数:.mpy 文件要求 small integer 具有最少的位数,而加载它的系统必须至少支持这么多的位数。
原生架构:如果 .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', 'rv32imc', 'rv64imc'][(sys_mpy >> 10) & 0x0F] print('mpy version:', sys_mpy & 0xff) print('mpy sub-version:', sys_mpy >> 8 & 3) print('mpy flags:', end='') if arch: print(' -march=' + arch, end='') if (sys_mpy >> 16) != 0: print(' -march-flags=' + (sys_mpy >> 16), end='') print()
通过检查文件的前两个字节来检验 .mpy 文件的有效性。第一个字节应为大写的 'M',第二个字节为版本号,它应与上述的系统版本相匹配。如果不匹配,则重新构建 .mpy 文件。
检查系统的 .mpy 版本是否与用于构建该 .mpy 文件的
mpy-cross所发射的版本相匹配,后者可通过mpy-cross --version查得。如果不匹配,则从在mpy-cross --version所报告的标签(或哈希)处检出的 Git 仓库重新编译mpy-cross。确保你使用了正确的
mpy-cross标志,这些标志可通过上面的代码查得,或通过检查你所用端口的MPY_CROSS_FLAGSMakefile 变量查得。如果 .mpy 文件的第三个字节设置了第 6 位,则检查所编码的架构特定标志位 vuint 是否与你导入该文件的目标兼容。
下表显示了 MicroPython 发布版本与 .mpy 版本之间的对应关系。
MicroPython 发布版本 |
.mpy 版本 |
|---|---|
v1.23.0 及以上 |
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 |
为完整起见,下表显示了 .mpy 版本发生变化时主 MicroPython 仓库的 Git 提交。
.mpy 版本变更 |
Git 提交 |
|---|---|
6.2 到 6.3 |
bdbc869f9ea200c0d28b2bc7bfb60acd9d884e1b |
6.1 到 6.2 |
6967ff3c581a66f73e9f3d78975f47528db39980 |
6 到 6.1 |
d94141e1473aebae0d3c63aeaa8397651ad6fa01 |
5 到 6 |
f2040bfc7ee033e48acef9f289790f3b4e6b74e5 |
4 到 5 |
5716c5cf65e9b2cb46c2906f40302401bdd27517 |
3 到 4 |
9a5f92ea72754c01cc03e5efcdfe94021120531e |
2 到 3 |
ff93fd4f50321c6190e1659b19e64fef3045a484 |
1 到 2 |
dd11af209d226b7d18d5148b239662e30ed60bad |
0 到 1 |
6a11048af1d01c78bdacddadd1b72dc7ba7c6478 |
初始版本 0 |
d8c834c95d506db979ec871417de90b7951edc30 |
.mpy 文件的二进制编码¶
MicroPython .mpy 文件是一种二进制容器格式,其中的代码对象(字节码和原生机器码)以嵌套的层次结构存储于内部。最外层模块的代码先存储,然后其子项跟随其后。每个子项可以有进一步的子项,例如一个类含有方法的情况,或者一个函数定义了 lambda 或推导式的情况。为了在保持文件较小的同时仍能提供较大范围的可能取值,它在许多地方使用了可变编码无符号整数(vuint)的概念。与 UTF-8 编码类似,这种编码每个字节存储 7 位,如果后面还有一个或多个字节,则第 8 位(MSB)置位。无符号整数的各位以 LSB 在前的形式存储在 vuint 中。
.mpy 文件的顶层由三部分组成:
头部。
全局 qstr 表和常量表。
模块外层作用域的原始代码(raw-code)。该外层作用域在导入 .mpy 文件时执行。
你可以使用 mpy-tool.py 检查 .mpy 文件的内容,例如(在主 MicroPython 仓库的根目录下运行):
$ ./tools/mpy-tool.py -xd myfile.mpy
头部¶
.mpy 头部如下:
大小 |
字段 |
|---|---|
字节 |
值 0x4d(ASCII 'M') |
字节 |
.mpy 主版本号 |
字节 |
特性标志、原生架构、次版本号(在较旧版本中为特性标志) |
字节 |
小整数的位数 |
第三个字节按如下方式拆分(MSB 在前):
位 |
含义 |
|---|---|
7 |
保留,必须为 0 |
6 |
头部之后跟随一个架构特定标志 vuint |
5..2 |
原生架构编号 |
1..0 |
次版本号 |
架构特定标志¶
如果头部的特性标志字节的第 6 位被置位,则头部之后将跟随一个包含可选架构特定信息的 vuint。该整数的内容取决于该文件所针对的原生架构。
这目前用于存储 MPY 文件为正确运行所需的 RISC-V 处理器扩展(除 I、M、C 和 Zicsr 之外)。不同风格的 ArmV7 通过其原生架构编号来标识,但对 RV32 和 RV64 而言,重用该机制会使事情复杂化。
针对 RV32 或 RV64 且不需要任何特定处理器扩展的 MPY 文件无需提供标志整数(同时在头部设置相应的位)。RV32 和 RV64 MPY 文件缺少标志值用于表示不需要任何特定扩展,并且在最终的输出二进制文件中节省一个字节。
另请参阅 mpy-tool.py 和 mpy-cross 中的 -march-flags 命令行选项,以及 mpy_ld.py 中的 --arch-flags 命令行选项,它们用于在创建 MPY 文件时设置该值。
全局 qstr 表和常量表¶
.mpy 文件包含一个 qstr 表和一个常量对象表。它们对于该 .mpy 文件而言是全局的,所有嵌套的原始代码对象都会引用它们。qstr 表将内部 qstr 编号(在 .mpy 文件内部)映射到 .mpy 文件被导入到的运行时的已解析 qstr 编号。这将 .mpy 文件与其执行所在的系统的其余部分关联起来。常量对象表填充了对该 .mpy 文件所需的所有常量对象的引用。
大小 |
字段 |
|---|---|
vuint |
qstr 的数量 |
vuint |
常量对象的数量 |
... |
qstr 数据 |
... |
已编码的常量对象 |
原始代码元素¶
原始代码元素包含代码,可以是字节码或原生机器码。其内容为:
大小 |
字段 |
|---|---|
vuint |
类型、大小以及是否存在子原始代码元素 |
... |
代码(字节码或机器码) |
vuint |
子原始代码元素的数量(仅当非零时) |
... |
子原始代码元素 |
原始代码元素中的第一个 vuint 编码了存储在该元素中的代码类型(最低两位)、该原始代码是否有子项(最低第三位),以及随后代码的长度(为其分配的 RAM 数量)。
vuint 之后是代码本身。除非代码类型是带有重定位的 viper 代码,否则该代码是常量数据,无需修改。
如果该原始代码有任何子项(由第一个 vuint 中的某一位指示),则代码之后跟随一个用于计数子原始代码元素数量的 vuint。
最后,所有子原始代码元素以递归方式存储。