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 .mpy gerado (por defeito, o nome do ficheiro de entrada com a extensão substituída; -o - escreve para stdout).

  • -O<n> – nível de otimização 0 a 3. O valor por defeito 0 preserva asserções e localizações completas do código-fonte; 3 remove asserções e docstrings e reescreve blocos if __debug__. O nível controla a mesma superfície micropython.opt_level que o ambiente de execução expõe.

  • -march=<arch> – arquitetura nativa alvo para funções decoradas com @native e @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 que mpy-cross --help apresenta, ou leia-o na câmara em tempo de execução com sys.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 .mpy que 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á gerar ValueError('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-cross utilizado para compilar o ficheiro .mpy, obtida através de mpy-cross --version. Se não corresponder, recompile o mpy-cross a partir do repositório Git com checkout na etiqueta (ou hash) reportada por mpy-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 Makefile MPY_CROSS_FLAGS para 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.