Archivos .mpy de MicroPython¶
MicroPython define el concepto de archivo .mpy, que es un formato de archivo contenedor binario que almacena código precompilado y que puede importarse como un módulo .py normal. El archivo foo.mpy puede importarse mediante import foo, siempre que la maquinaria de importación pueda encontrar foo.mpy de la forma habitual. Normalmente, se busca en orden en cada directorio listado en sys.path. Al buscar en un directorio concreto, primero se busca foo.py y, si no se encuentra, entonces se busca foo.mpy; la búsqueda continúa en el siguiente directorio si no se encuentra ninguno. Por tanto, foo.py tendrá prioridad sobre foo.mpy.
Estos archivos .mpy pueden contener bytecode, que normalmente se genera a partir de archivos fuente de Python (archivos .py) mediante el programa mpy-cross. Para algunas arquitecturas, un archivo .mpy también puede contener código máquina nativo, que puede generarse de varias maneras, en particular a partir de código fuente C.
El compilador mpy-cross¶
mpy-cross es el compilador cruzado que convierte un archivo fuente .py en un contenedor binario .mpy listo para importarse en la cámara. Forma parte del árbol de fuentes de MicroPython (el mismo que se usa para compilar el firmware de la cámara) y también se publica como paquete pip para su uso en el host sin una descarga completa del firmware:
$ pip install --user mpy-cross
O mediante pipx:
$ pipx install mpy-cross
Una vez instalado, invócalo sobre un único archivo fuente:
$ mpy-cross foo.py
Esto produce foo.mpy en el directorio actual, listo para copiarse en el sistema de archivos de la cámara junto a otros módulos o para incorporarse a una imagen ROMFS.
Las opciones de línea de comandos más útiles:
-o <path>– ruta de salida del.mpygenerado (por defecto, el nombre del archivo de entrada con la extensión reemplazada;-o -escribe en stdout).-O<n>– nivel de optimización de0a3. El valor por defecto0conserva las aserciones y la información completa de ubicación en el código fuente;3elimina las aserciones y los docstrings y reescribe los bloquesif __debug__. El nivel controla la misma superficiemicropython.opt_levelque expone el runtime.-march=<arch>– arquitectura nativa de destino para las funciones decoradas con@nativey@viper. Es obligatoria cuando el código fuente usa esos decoradores. El valor debe coincidir con la clase de MCU de la cámara: elígelo de la lista que imprimempy-cross --helpo léelo de la cámara en tiempo de ejecución consys.implementation._mpy.-s <path>– cadena de ruta fuente incrustada en la información de depuración del.mpy. Útil cuando la ruta en disco difiere de la ruta de importación bajo la que el archivo debería mostrarse en los rastreos de pila.-X emit=bytecode|native|viper– elige el emisor por defecto para todo el módulo (una alternativa por función a los decoradores@native/@viper).--version– imprime la versión del formato.mpyque emite este binario. Ese número debe coincidir con la versión que admite el runtime de la cámara (consulta la tabla de versiones más abajo) o la importación lanzaráValueError('incompatible .mpy file').
Ejecuta mpy-cross --help para ver la lista completa de opciones.
El paquete pip también expone una pequeña API de módulo de Python para que los scripts de compilación puedan controlar el compilador en el mismo proceso en lugar de bifurcar manualmente un subproceso:
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 y mpy_cross.mpy_version son los tres puntos de entrada; mpy_cross.CrossCompileError transporta el stderr del compilador cuando algo va mal. Las constantes de arquitectura (NATIVE_ARCH_ARMV7EMSP, NATIVE_ARCH_ARMV7EMDP, etc.) coinciden con las cadenas que acepta la opción -march.
Versionado y compatibilidad de los archivos .mpy¶
Un archivo .mpy dado puede ser compatible o no con un sistema MicroPython concreto. La compatibilidad se basa en lo siguiente:
Versión del archivo .mpy: la versión del archivo debe coincidir con la versión que admite el sistema que lo carga.
Subversión del archivo .mpy: si el archivo .mpy contiene código máquina nativo, la subversión del archivo debe coincidir con la versión que admite el sistema que lo carga. En caso contrario, si no hay código máquina nativo en el archivo .mpy, la subversión se ignora durante la carga.
Bits de los enteros pequeños: el archivo .mpy requerirá un número mínimo de bits en un small integer y el sistema que lo carga debe admitir al menos esa cantidad de bits.
Arquitectura nativa: si el archivo .mpy contiene código máquina nativo, especificará la arquitectura de ese código máquina y el sistema que lo carga debe admitir la ejecución del código de esa arquitectura.
Si un sistema MicroPython admite la importación de archivos .mpy, el campo sys.implementation._mpy existirá y devolverá un entero que codifica la versión (los 8 bits inferiores), las características y la arquitectura nativa.
Intentar importar un archivo .mpy que falle una de las primeras cuatro pruebas lanzará ValueError('incompatible .mpy file'). Intentar importar un archivo .mpy que falle la prueba de arquitectura nativa (si contiene código máquina nativo) lanzará ValueError('incompatible .mpy arch').
Si la importación de un archivo .mpy falla, prueba lo siguiente:
Determina la versión .mpy y las opciones que admite tu sistema MicroPython ejecutando:
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()
Comprueba la validez del archivo .mpy inspeccionando los dos primeros bytes del archivo. El primer byte debe ser una “M” mayúscula y el segundo byte será el número de versión, que debe coincidir con la versión del sistema obtenida arriba. Si no coincide, vuelve a compilar el archivo .mpy.
Comprueba si la versión .mpy del sistema coincide con la versión emitida por el
mpy-crossque se usó para compilar el archivo .mpy, que se obtiene conmpy-cross --version. Si no coincide, vuelve a compilarmpy-crossdesde el repositorio Git extraído en la etiqueta (o hash) que informampy-cross --version.Asegúrate de estar usando las opciones correctas de
mpy-cross, que se obtienen con el código anterior o inspeccionando la variable de MakefileMPY_CROSS_FLAGSdel port que estás usando.Si el tercer byte del archivo .mpy tiene activado el bit n.º 6, comprueba si el vuint de bits de indicadores específicos de la arquitectura codificado es compatible con el destino en el que estás importando el archivo.
La siguiente tabla muestra la correspondencia entre la versión de MicroPython y la versión .mpy.
Versión de MicroPython |
versión .mpy |
|---|---|
v1.23.0 y superiores |
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 |
Para mayor completitud, la siguiente tabla muestra el commit de Git del repositorio principal de MicroPython en el que se cambió la versión .mpy.
cambio de versión .mpy |
commit de Git |
|---|---|
6.2 a 6.3 |
bdbc869f9ea200c0d28b2bc7bfb60acd9d884e1b |
6.1 a 6.2 |
6967ff3c581a66f73e9f3d78975f47528db39980 |
6 a 6.1 |
d94141e1473aebae0d3c63aeaa8397651ad6fa01 |
5 a 6 |
f2040bfc7ee033e48acef9f289790f3b4e6b74e5 |
4 a 5 |
5716c5cf65e9b2cb46c2906f40302401bdd27517 |
3 a 4 |
9a5f92ea72754c01cc03e5efcdfe94021120531e |
2 a 3 |
ff93fd4f50321c6190e1659b19e64fef3045a484 |
1 a 2 |
dd11af209d226b7d18d5148b239662e30ed60bad |
0 a 1 |
6a11048af1d01c78bdacddadd1b72dc7ba7c6478 |
versión inicial 0 |
d8c834c95d506db979ec871417de90b7951edc30 |
Codificación binaria de los archivos .mpy¶
Los archivos .mpy de MicroPython son un formato contenedor binario con objetos de código (bytecode y código máquina nativo) almacenados internamente en una jerarquía anidada. El código del módulo exterior se almacena primero y, a continuación, le siguen sus hijos. Cada hijo puede tener más hijos, por ejemplo en el caso de una clase con métodos, o de una función que define una lambda o una comprensión. Para mantener los archivos pequeños sin dejar de ofrecer un amplio rango de valores posibles, utiliza el concepto de entero sin signo codificado de forma variable (vuint) en muchos lugares. De forma similar a la codificación UTF-8, esta codificación almacena 7 bits por byte con el 8.º bit (MSB) activado si le siguen uno o más bytes. Los bits del entero sin signo se almacenan en el vuint en forma LSB.
El nivel superior de un archivo .mpy consta de tres partes:
La cabecera.
Las tablas globales de qstr y de constantes.
El código en bruto (raw-code) del ámbito exterior del módulo. Este ámbito exterior se ejecuta cuando se importa el archivo .mpy.
Puedes inspeccionar el contenido de un archivo .mpy usando mpy-tool.py, por ejemplo (ejecutado desde la raíz del repositorio principal de MicroPython):
$ ./tools/mpy-tool.py -xd myfile.mpy
La cabecera¶
La cabecera del .mpy es:
tamaño |
campo |
|---|---|
byte |
valor 0x4d (ASCII “M”) |
byte |
número de versión mayor del .mpy |
byte |
indicadores de características, arquitectura nativa, número de versión menor (eran indicadores de características en versiones anteriores) |
byte |
número de bits en un entero pequeño |
El tercer byte se divide de la siguiente manera (MSB primero):
bit |
significado |
|---|---|
7 |
reservado, debe ser 0 |
6 |
tras la cabecera sigue un vuint de indicadores específicos de la arquitectura |
5..2 |
número de arquitectura nativa |
1..0 |
número de versión menor |
Indicadores específicos de la arquitectura¶
Si el bit n.º 6 del byte de indicadores de características de la cabecera está activado, tras la cabecera seguirá un vuint que contiene información opcional específica de la arquitectura. El contenido de este entero depende de la arquitectura nativa a la que va destinado el archivo.
Actualmente esto se usa para almacenar qué extensiones del procesador RISC-V necesita el archivo MPY para funcionar correctamente, además de I, M, C y Zicsr. Las distintas variantes de ArmV7 se identifican por su número de arquitectura nativa, pero reutilizar ese mecanismo complicaría las cosas para RV32 y RV64.
Los archivos MPY destinados a RV32 o RV64 que no necesitan ninguna extensión concreta del procesador no necesitan proporcionar un entero de indicadores (junto con la activación del bit apropiado en la cabecera). La ausencia de un valor de indicadores para los archivos MPY de RV32 y RV64 se usa para indicar que no se necesita ninguna extensión específica, y ahorra un byte en el binario de salida final.
Consulta también la opción de línea de comandos -march-flags tanto en mpy-tool.py como en mpy-cross, y la opción de línea de comandos --arch-flags en mpy_ld.py para establecer este valor al crear archivos MPY.
Las tablas globales de qstr y de constantes¶
Un archivo .mpy contiene una única tabla de qstr y una única tabla de objetos constantes. Estas son globales al archivo .mpy y todos los objetos de código en bruto anidados las referencian. La tabla de qstr asigna el número de qstr interno (interno al archivo .mpy) al número de qstr resuelto del runtime en el que se importa el archivo .mpy. Esto vincula el archivo .mpy con el resto del sistema dentro del cual se ejecuta. La tabla de objetos constantes se rellena con referencias a todos los objetos constantes que necesita el archivo .mpy.
tamaño |
campo |
|---|---|
vuint |
número de qstr |
vuint |
número de objetos constantes |
… |
datos de qstr |
… |
objetos constantes codificados |
Elementos de código en bruto¶
Un elemento de código en bruto contiene código, ya sea bytecode o código máquina nativo. Su contenido es:
tamaño |
campo |
|---|---|
vuint |
tipo, tamaño y si hay subelementos de código en bruto |
… |
código (bytecode o código máquina) |
vuint |
número de subelementos de código en bruto (solo si es distinto de cero) |
… |
subelementos de código en bruto |
El primer vuint de un elemento de código en bruto codifica el tipo de código almacenado en este elemento (los dos bits menos significativos), si este código en bruto tiene hijos (el tercer bit menos significativo) y la longitud del código que sigue (la cantidad de RAM que se le debe asignar).
Tras el vuint viene el propio código. A menos que el tipo de código sea código viper con reubicaciones, este código es dato constante y no necesita modificarse.
Si este código en bruto tiene hijos (según indica un bit del primer vuint), tras el código viene un vuint que cuenta el número de subelementos de código en bruto.
Por último, se almacenan, de forma recursiva, los subelementos de código en bruto que haya.