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 - 會寫入 stdout)。

  • -O<n> -- 最佳化等級 03。預設值 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,讓建置指令碼可以在處理程序內驅動編譯器,而不必親自手動 fork 子處理程序::

import mpy_cross

mpy_cross.compile('foo.py', dest='build/foo.mpy', opt=3,
                  march=mpy_cross.NATIVE_ARCH_ARMV7EMSP)

mpy_cross.compilempy_cross.runmpy_cross.mpy_version 是三個進入點;mpy_cross.CrossCompileError 會在發生錯誤時攜帶編譯器的 stderr 輸出。架構常數(NATIVE_ARCH_ARMV7EMSPNATIVE_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 查得)。若不相符,請從 Git 儲存庫於 mpy-cross --version 所回報的標籤(或雜湊)處簽出,並重新編譯 mpy-cross

  • 確認您使用的是正確的 mpy-cross 旗標,可透過上述程式碼查得,或檢查您所使用之埠的 MPY_CROSS_FLAGS Makefile 變數。

  • 若 .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

為求完整,下表顯示 MicroPython 主儲存庫中變更 .mpy 版本時所對應的 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 或生成式(comprehension)的情況。為了在維持檔案小巧的同時仍提供大範圍的可能值,它在許多地方採用了可變長度編碼的無號整數(variably-encoded-unsigned-integer,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 檔案除了 I、M、C 與 Zicsr 之外,還需要哪些 RISC-V 處理器擴充功能才能正確運作。不同版本的 ArmV7 是以其原生架構編號來識別,但若重用該機制則會使 RV32 與 RV64 的處理變得複雜。

針對 RV32 或 RV64 且不需要任何特定處理器擴充功能的 MPY 檔案,不需要提供旗標整數(同時也需在標頭中設定適當的位元)。RV32 與 RV64 MPY 檔案缺少旗標值,即用來表示不需要任何特定擴充功能,並可在最終輸出的二進位檔中節省一個位元組。

另請參閱 mpy-tool.pympy-cross 中的 -march-flags 命令列選項,以及 mpy_ld.py 中用於在建立 MPY 檔案時設定此值的 --arch-flags 命令列選項。

全域 qstr 與常數表

.mpy 檔案包含單一個 qstr 表與單一個常數物件表。這些表對 .mpy 檔案而言是全域的,會被所有巢狀的 raw-code 物件參照。qstr 表將內部 qstr 編號(.mpy 檔案內部使用)對應到 .mpy 檔案被匯入之執行階段中已解析的 qstr 編號。這會將 .mpy 檔案與其執行所在的其餘系統連結起來。常數物件表中則填入 .mpy 檔案所需之所有常數物件的參照。

大小

欄位

vuint

qstr 的數量

vuint

常數物件的數量

...

qstr 資料

...

已編碼的常數物件

原始程式碼元素(raw code elements)

raw-code 元素包含程式碼,可能是位元組碼或原生機器碼。其內容為:

大小

欄位

vuint

類型、大小以及是否有子 raw-code 元素

...

程式碼(位元組碼或機器碼)

vuint

子 raw-code 元素的數量(僅在非零時出現)

...

子 raw-code 元素

raw-code 元素中的第一個 vuint 編碼了此元素所儲存程式碼的類型(最低有效的兩個位元)、此 raw-code 是否有任何子項(第三低有效的位元),以及後續程式碼的長度(為其配置的 RAM 量)。

vuint 之後接的是程式碼本身。除非程式碼類型是帶有重定位的 viper 程式碼,否則此程式碼為常數資料,無需修改。

若此 raw-code 有任何子項(如第一個 vuint 中某位元所指示),則程式碼之後接一個用以計算子 raw-code 元素數量的 vuint。

最後,會以遞迴方式儲存所有子 raw-code 元素。