Ficheiros .mpy do MicroPython¶
O MicroPython define o conceito de ficheiro .mpy, que é um formato de ficheiro binário contêiner que armazena código pré-compilado e que pode ser importado como um módulo .py normal. O ficheiro foo.mpy pode ser importado através de import foo, desde que foo.mpy possa ser encontrado pelo mecanismo de importação da forma habitual. Normalmente, cada diretório listado em sys.path é pesquisado por ordem. Ao pesquisar num diretório específico, é procurado primeiro foo.py e, se não for encontrado, é procurado foo.mpy; a pesquisa continua no diretório seguinte caso nenhum seja encontrado. Assim sendo, foo.py tem precedência sobre foo.mpy.
Estes ficheiros .mpy podem conter bytecode, que normalmente é gerado a partir de ficheiros fonte Python (ficheiros .py) através do programa mpy-cross. Para algumas arquiteturas, um ficheiro .mpy pode também conter código máquina nativo, que pode ser gerado de várias formas, nomeadamente a partir de código fonte em C.
O compilador mpy-cross¶
mpy-cross é o compilador cruzado que converte um ficheiro fonte .py num contêiner binário .mpy pronto para ser importado na câmara. Faz parte da árvore de código-fonte do MicroPython (a mesma utilizada para compilar o firmware da câmara) e está também disponível como pacote pip para utilização no lado do anfitrião sem necessidade de uma verificação completa do firmware:
$ pip install --user mpy-cross
Ou através do pipx:
$ pipx install mpy-cross
Após a instalação, invoque-o sobre um único ficheiro fonte:
$ mpy-cross foo.py
Isto produz foo.mpy no diretório atual, pronto para ser copiado para o sistema de ficheiros da câmara a par de outros módulos ou para integrar numa imagem ROMFS.
As opções de linha de comandos mais úteis:
-o <path>– caminho de saída para o ficheiro.mpygerado (por defeito, o nome do ficheiro de entrada com a extensão substituída;-o -escreve para stdout).-O<n>– nível de otimização0a3. O valor por defeito0preserva asserções e localizações completas do código-fonte;3remove asserções e docstrings e reescreve blocosif __debug__. O nível controla a mesma superfíciemicropython.opt_levelque o ambiente de execução expõe.-march=<arch>– arquitetura nativa alvo para funções decoradas com@nativee@viper. Obrigatório quando o código-fonte utiliza esses decoradores. O valor deve corresponder à classe de MCU da câmara: selecione-o a partir da lista quempy-cross --helpapresenta, ou leia-o na câmara em tempo de execução comsys.implementation._mpy.-s <path>– cadeia de caracteres do caminho de origem incorporado nas informações de depuração do.mpy. Útil quando o caminho em disco difere do caminho de importação que o ficheiro deve apresentar nos rastreamentos de pilha.-X emit=bytecode|native|viper– escolhe o emissor predefinido para o módulo inteiro (uma alternativa por função aos decoradores@native/@viper).--version– apresenta a versão do formato.mpyque este binário emite. Esse número deve corresponder à versão suportada pelo ambiente de execução da câmara (consulte a tabela de versões abaixo) ou a importação irá gerarValueError('incompatible .mpy file').
Execute mpy-cross --help para ver a lista completa de opções.
O pacote pip também expõe uma pequena API de módulo Python para que os scripts de compilação possam invocar o compilador em processo em vez de lançar um subprocesso manualmente:
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 e mpy_cross.mpy_version são os três pontos de entrada; mpy_cross.CrossCompileError transporta o stderr do compilador quando algo corre mal. As constantes de arquitetura (NATIVE_ARCH_ARMV7EMSP, NATIVE_ARCH_ARMV7EMDP, etc.) correspondem às cadeias de caracteres que a opção -march aceita.
Versões e compatibilidade dos ficheiros .mpy¶
Um dado ficheiro .mpy pode ou não ser compatível com um determinado sistema MicroPython. A compatibilidade baseia-se no seguinte:
Versão do ficheiro .mpy: a versão do ficheiro deve corresponder à versão suportada pelo sistema que o carrega.
Sub-versão do ficheiro .mpy: se o ficheiro .mpy contiver código máquina nativo, a sub-versão do ficheiro deve corresponder à versão suportada pelo sistema que o carrega. Caso contrário, se não existir código máquina nativo no ficheiro .mpy, a sub-versão é ignorada durante o carregamento.
Bits de inteiro pequeno: o ficheiro .mpy requer um número mínimo de bits num small integer e o sistema que o carrega deve suportar, pelo menos, esse número de bits.
Arquitetura nativa: se o ficheiro .mpy contiver código máquina nativo, este especificará a arquitetura desse código máquina e o sistema que o carrega deve suportar a execução do código dessa arquitetura.
Se um sistema MicroPython suportar a importação de ficheiros .mpy, o campo sys.implementation._mpy existirá e devolverá um inteiro que codifica a versão (8 bits inferiores), as funcionalidades e a arquitetura nativa.
Tentar importar um ficheiro .mpy que falhe num dos quatro primeiros testes irá gerar ValueError('incompatible .mpy file'). Tentar importar um ficheiro .mpy que falhe no teste de arquitetura nativa (se contiver código máquina nativo) irá gerar ValueError('incompatible .mpy arch').
Se a importação de um ficheiro .mpy falhar, experimente o seguinte:
Determine a versão e as opções .mpy suportadas pelo seu sistema MicroPython executando:
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()
Verifique a validade do ficheiro .mpy inspecionando os dois primeiros bytes do ficheiro. O primeiro byte deve ser um “M” maiúsculo e o segundo byte será o número de versão, que deve corresponder à versão do sistema indicada acima. Se não corresponder, reconstrua o ficheiro .mpy.
Verifique se a versão .mpy do sistema corresponde à versão emitida pelo
mpy-crossutilizado para compilar o ficheiro .mpy, obtida através dempy-cross --version. Se não corresponder, recompile ompy-crossa partir do repositório Git com checkout na etiqueta (ou hash) reportada pormpy-cross --version.Certifique-se de que está a utilizar as opções corretas do
mpy-cross, obtidas pelo código acima ou inspecionando a variável MakefileMPY_CROSS_FLAGSpara o port que está a utilizar.Se o terceiro byte do ficheiro .mpy tiver o bit #6 definido, verifique se o vuint dos bits de opções específicos da arquitetura codificados é compatível com o alvo para o qual está a importar o ficheiro.
A tabela seguinte mostra a correspondência entre as versões do MicroPython e as versões .mpy.
Versão do MicroPython |
Versão .mpy |
|---|---|
v1.23.0 e superior |
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 maior exaustividade, a tabela seguinte indica o commit do repositório principal do MicroPython em que a versão .mpy foi alterada.
Alteração da versão .mpy |
Commit Git |
|---|---|
6.2 para 6.3 |
bdbc869f9ea200c0d28b2bc7bfb60acd9d884e1b |
6.1 para 6.2 |
6967ff3c581a66f73e9f3d78975f47528db39980 |
6 para 6.1 |
d94141e1473aebae0d3c63aeaa8397651ad6fa01 |
5 para 6 |
f2040bfc7ee033e48acef9f289790f3b4e6b74e5 |
4 para 5 |
5716c5cf65e9b2cb46c2906f40302401bdd27517 |
3 para 4 |
9a5f92ea72754c01cc03e5efcdfe94021120531e |
2 para 3 |
ff93fd4f50321c6190e1659b19e64fef3045a484 |
1 para 2 |
dd11af209d226b7d18d5148b239662e30ed60bad |
0 para 1 |
6a11048af1d01c78bdacddadd1b72dc7ba7c6478 |
versão inicial 0 |
d8c834c95d506db979ec871417de90b7951edc30 |
Codificação binária dos ficheiros .mpy¶
Os ficheiros .mpy do MicroPython são um formato de contêiner binário com objetos de código (bytecode e código máquina nativo) armazenados internamente numa hierarquia aninhada. O código do módulo exterior é armazenado primeiro, seguindo-se os seus filhos. Cada filho pode ter outros filhos, por exemplo no caso de uma classe com métodos, ou de uma função que define uma expressão lambda ou uma compreensão. Para manter os ficheiros pequenos enquanto ainda se permite uma grande gama de valores possíveis, utiliza o conceito de inteiro sem sinal de codificação variável (vuint) em muitos lugares. De forma semelhante à codificação UTF-8, esta codificação armazena 7 bits por byte com o 8.º bit (MSB) definido se um ou mais bytes se seguirem. Os bits do inteiro sem sinal são armazenados no vuint em forma LSB.
O nível superior de um ficheiro .mpy é composto por três partes:
O cabeçalho.
As tabelas globais de qstr e constantes.
O raw-code para o âmbito exterior do módulo. Este âmbito exterior é executado quando o ficheiro .mpy é importado.
Pode inspecionar o conteúdo de um ficheiro .mpy utilizando mpy-tool.py, por exemplo (executado a partir da raiz do repositório principal do MicroPython):
$ ./tools/mpy-tool.py -xd myfile.mpy
O cabeçalho¶
O cabeçalho .mpy é:
tamanho |
campo |
|---|---|
byte |
valor 0x4d (ASCII “M”) |
byte |
número de versão principal .mpy |
byte |
opções de funcionalidades, arquitetura nativa, número de versão menor (era apenas opções de funcionalidades em versões anteriores) |
byte |
número de bits num inteiro pequeno |
O terceiro byte é dividido da seguinte forma (MSB primeiro):
bit |
significado |
|---|---|
7 |
reservado, deve ser 0 |
6 |
um vuint de opções específicas da arquitetura segue-se ao cabeçalho |
5..2 |
número de arquitetura nativa |
1..0 |
número de versão menor |
Opções específicas da arquitetura¶
Se o bit #6 do byte de opções de funcionalidades do cabeçalho estiver definido, um vuint contendo informações opcionais específicas da arquitetura seguir-se-á ao cabeçalho. O conteúdo deste inteiro depende da arquitetura nativa para a qual o ficheiro se destina.
Atualmente, é utilizado para armazenar quais as extensões do processador RISC-V que o ficheiro MPY necessita para funcionar corretamente além de I, M, C e Zicsr. Diferentes variantes do ArmV7 são identificadas pelo seu número de arquitetura nativa, mas reutilizar esse mecanismo complicaria as coisas para RV32 e RV64.
Os ficheiros MPY destinados a RV32 ou RV64 que não necessitam de extensões específicas do processador não precisam de fornecer um inteiro de opções (juntamente com a definição do bit adequado no cabeçalho). A ausência de um valor de opções para ficheiros MPY RV32 e RV64 é utilizada para indicar que não são necessárias extensões específicas e poupa um byte no binário de saída final.
Consulte também a opção de linha de comandos -march-flags tanto em mpy-tool.py como em mpy-cross, e a opção de linha de comandos --arch-flags em mpy_ld.py para definir este valor ao criar ficheiros MPY.
As tabelas globais de qstr e constantes¶
Um ficheiro .mpy contém uma única tabela de qstr e uma única tabela de objetos constantes. Estas são globais ao ficheiro .mpy e são referenciadas por todos os objetos raw-code aninhados. A tabela de qstr mapeia o número de qstr interno (interno ao ficheiro .mpy) para o número de qstr resolvido do ambiente de execução para o qual o ficheiro .mpy é importado. Isto liga o ficheiro .mpy ao resto do sistema em que é executado. A tabela de objetos constantes é preenchida com referências a todos os objetos constantes de que o ficheiro .mpy necessita.
tamanho |
campo |
|---|---|
vuint |
número de qstrs |
vuint |
número de objetos constantes |
… |
dados qstr |
… |
objetos constantes codificados |
Elementos raw-code¶
Um elemento raw-code contém código, seja bytecode ou código máquina nativo. O seu conteúdo é:
tamanho |
campo |
|---|---|
vuint |
tipo, tamanho e se existem sub-elementos raw-code |
… |
código (bytecode ou código máquina) |
vuint |
número de sub-elementos raw-code (apenas se não for zero) |
… |
sub-elementos raw-code |
O primeiro vuint num elemento raw-code codifica o tipo de código armazenado neste elemento (os dois bits menos significativos), se este raw-code tem filhos (o terceiro bit menos significativo) e o comprimento do código que se segue (a quantidade de RAM a alocar para ele).
A seguir ao vuint vem o próprio código. A menos que o tipo de código seja código viper com reposicionamentos, este código é dados constantes e não precisa de ser modificado.
Se este raw-code tiver filhos (conforme indicado por um bit no primeiro vuint), a seguir ao código vem um vuint que conta o número de sub-elementos raw-code.
Por fim, quaisquer sub-elementos raw-code são armazenados recursivamente.