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>-- 最佳化等級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,讓建置指令碼可以在處理程序內驅動編譯器,而不必親自手動 fork 子處理程序::
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查得)。若不相符,請從 Git 儲存庫於mpy-cross --version所回報的標籤(或雜湊)處簽出,並重新編譯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 |
為求完整,下表顯示 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.py 與 mpy-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 元素。