14.2.2.1. 將指令碼凍結進韌體¶
凍結(frozen) 模組是指在建置時將 .py 檔案編譯成位元組碼,並連結進韌體映像中。執行階段會直接從 flash 匯入凍結模組,完全不查看磁碟上的檔案系統。對於要出貨的產品來說,這正是放置應用程式碼的恰當位置:終端使用者沒有東西可刪、SD 卡上過時的 .py 不會覆蓋它,而且不論磁碟機上有什麼(甚至什麼都沒有),相機每次開機都會執行相同的程式碼。
本頁說明相機從重置後遵循的啟動順序,接著介紹 manifest.py 與 freeze 指令如何將應用程式烘焙進建置中。
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.py 與 main.py 解析到凍結的副本,因此即使 SD 卡上留有開發時遺留下來的過時 boot.py,出貨的相機仍會執行建置中的程式碼。
14.2.2.1.2.3. 查找順序¶
boot.py / main.py 的執行路徑與一般的 import 陳述式,兩者的覆蓋語意是 不同 的。弄清楚哪個是哪個,對生產與開發都很重要:
對於
boot.py與main.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 檔案系統。