14.2.2.1. 將指令碼凍結進韌體

凍結(frozen) 模組是指在建置時將 .py 檔案編譯成位元組碼,並連結進韌體映像中。執行階段會直接從 flash 匯入凍結模組,完全不查看磁碟上的檔案系統。對於要出貨的產品來說,這正是放置應用程式碼的恰當位置:終端使用者沒有東西可刪、SD 卡上過時的 .py 不會覆蓋它,而且不論磁碟機上有什麼(甚至什麼都沒有),相機每次開機都會執行相同的程式碼。

本頁說明相機從重置後遵循的啟動順序,接著介紹 manifest.pyfreeze 指令如何將應用程式烘焙進建置中。

14.2.2.1.1. 啟動順序

相機剛從重置狀態出來時,會執行什麼、何時執行:

  • 開機載入程式。 上電後會進入一段短暫的 DFU 視窗,IDE 利用此視窗來推送韌體更新。這個視窗在數秒後關閉,開機載入程式接著將控制權交給 MicroPython。執行中的指令碼可以呼叫 machine.bootloader() 隨時重新進入此視窗。

  • 凍結檔案系統初始化。 在任何應用程式碼執行之前,執行階段會先把各個檔案系統掛載起來。內部 flash 會掛載在 /flash(若其中沒有任何內容,則格式化為空白)。若存在 SD 卡, 內部 flash 上 不存在 名為 SKIPSD 的標記檔案,則 SD 卡會掛載在 /sdcard。當建置含有 ROMFS 時,會自動掛載在 /rom。工作目錄會設為開機目錄(若 SD 卡已掛載則為 /sdcard,否則為 /flash),而 sys.path 會被填入 /flash/flash/lib/sdcard/sdcard/lib/rom/rom/lib。常駐於 flash 的設定工作由一個名為 _boot.py 的凍結模組處理——這屬於連接埠與板子的基礎架構,並非應用程式的掛鉤點。應用程式不會去客製化 _boot.py;建置才會。從 IDE 將一個 SKIPSD 檔案放到 flash 上,是讓相機改從內部 flash 開機(而非 SD 卡)的受支援做法。

  • REPL 前置設定。 boot.py 會在 每一次 軟重置時執行——包含冷開機、從 REPL 按下 Ctrl-D、執行中的指令碼返回,以及看門狗復原——並且都在 REPL 變得可存取 之前。它的職責是準備好系統其餘部分賴以執行的環境:也就是 REPL、應用程式以及任何復原工具都需要事先就緒才能運作的那類設定。它並不是應用程式本身的所在之處。main.py 才是應用程式的進入點。

  • 主迴圈。 main.py 是應用程式的主迴圈。它在冷開機時執行一次,緊接在 boot.py 之後。後續的軟重置 不會 重新執行它——相機改為落到 REPL。這種不對稱性對開發很重要(按下 Ctrl-D 會落到 REPL 而不重新執行迴圈,開發者得以檢視狀態),但對生產環境則無關緊要:佈署於現場的相機所遇到的是上電、看門狗與硬重置,這些全都是會重新進入冷開機路徑並再次執行 main.py 的硬體重置。

14.2.2.1.2. 凍結進韌體

板子的凍結模組集合是在韌體樹中的 boards/<TARGET>/manifest.py 內宣告的。manifest 是一個小型的 Python 檔案,會呼叫少數幾個指令:

  • freeze("$(OMV_LIB_DIR)/", "foo.py") —— 將單一個 foo.py 烘焙進建置中。

  • package("mylib", base_path="...") —— 烘焙一個多檔案的 Python 套件,並在指定的基底路徑下保留其目錄結構。

  • include("...") —— 引入另一個 manifest 檔案。板子的 manifest 利用這個指令來共用共通的模組集合。

  • require("logging") —— 依名稱引入一個具名的上游 micropython-lib 模組。

一個最精簡的應用程式 manifest,會為每一個頂層指令碼加上一行 freeze,並為應用程式所依賴的每一個套件加上一行 package

14.2.2.1.2.1. 原始碼所在位置

應用程式原始碼位於韌體樹中的 scripts/libraries/ 之下,與建置已經會凍結的那些模組放在一起。manifest 變數 $(OMV_LIB_DIR) 會展開為該路徑,因此 manifest 項目可以保持簡短。編輯 manifest 本來就是樹內操作,所以將原始碼也放在樹內,可避免在路徑解析上還要另外周旋一個獨立的專案儲存庫。

對於一個出貨單一 main.py 外加一個支援套件的應用程式,典型的配置如下:

scripts/libraries/
    main.py
    my_lib/
        __init__.py
        helpers.py

而在板子的 boards/<TARGET>/manifest.py 中,為該指令碼加上一行 freeze,為該套件加上一行 package:

freeze("$(OMV_LIB_DIR)/", "main.py")
package("my_lib", base_path="$(OMV_LIB_DIR)/my_lib")

單檔指令碼——此處是 main.py,但同樣的規則也適用於 boot.py 或任何獨立的輔助檔案——使用 freeze。多檔套件則使用 package。增加另一個指令碼就是多一行 freeze;增加另一個套件就是多一行 package

14.2.2.1.2.2. 建置與燒錄

manifest 就位後,完全按照 韌體章節 所述的方式建置韌體:

make -j$(nproc) -C lib/micropython/mpy-cross   # once, builds the cross-compiler
make -j$(nproc) TARGET=<TARGET>                # builds the firmware

輸出會落在 build/<TARGET>/bin/ 中:

build/<TARGET>/bin/
    firmware.bin     # flash through the IDE
    romfs0.img       # flash through the IDE in a separate step

透過 IDE 燒錄 .bin.img,會得到一台應用程式已成為建置一部分的相機。

上述啟動順序正是讓這種烘焙生效的關鍵:執行階段在尚未檢查檔案系統之前,就會將 boot.pymain.py 解析到凍結的副本,因此即使 SD 卡上留有開發時遺留下來的過時 boot.py,出貨的相機仍會執行建置中的程式碼。

14.2.2.1.2.3. 查找順序

boot.py / main.py 的執行路徑與一般的 import 陳述式,兩者的覆蓋語意是 不同 的。弄清楚哪個是哪個,對生產與開發都很重要:

  • 對於 boot.pymain.py:執行階段會先尋找 凍結 的副本,然後才是檔案系統。凍結的 boot.py 無法藉由在 SD 卡上放一份來覆蓋——持有相機的人若不重新燒錄,就無法更改進入點。

  • 對於 import foo:執行階段會先搜尋 sys.path——其涵蓋 /flash/sdcard/rom 以及它們的 lib 子目錄——然後才是凍結模組。flash 或 SD 上一個同名的 foo.py 確實 會覆蓋凍結的 foo。這正是開發上的便利:把修正過的模組放到卡上、軟重置,無需重新燒錄就能看到變更。

若出貨產品想要抑制這種「檔案系統覆蓋凍結模組」的匯入行為,可以在 boot.py 中提早清空 sys.path:

import sys

sys.path.clear()

sys.path 為空時,所有匯入都只會從凍結模組解析;flash、SD 或 ROMFS 上的任何東西都無法遮蔽它們。

14.2.2.1.2.4. 資產問題

凍結對程式碼來說很棒。但對大型二進位資產卻 理想:機器學習模型檔、標籤表、JSON 設定、影像範本等。把這些當成 Python 字面值嵌入,會讓原始碼膨脹、重新編譯緩慢,並把位元組碼容器浪費在直譯器反正只會原樣讀取的資料上。建置 ROMFS 映像 頁面介紹了填補這個缺口的唯讀 flash 檔案系統。