14.2.2.1. 将脚本冻结到固件中¶
冻结(frozen)模块是指被编译为字节码并在构建时链接进固件镜像的 .py 文件。运行时会直接从闪存导入冻结模块,完全不查看磁盘上的文件系统。对于已发布的产品而言,这正是应用代码应当存放的位置:终端用户没有可删除的内容,SD 卡上陈旧的 .py 文件也无法覆盖它,并且无论驱动器上有什么(如果有的话),摄像头每次启动都运行相同的代码。
本页介绍摄像头遵循的启动顺序,然后说明 manifest.py 与 freeze 指令如何将应用烘焙进构建。
14.2.2.1.1. 启动顺序¶
摄像头从复位状态恢复时,会运行什么、何时运行:
引导加载程序(bootloader)。 上电会进入一个短暂的 DFU 窗口,IDE 利用该窗口推送固件更新。该窗口在数秒后关闭,引导加载程序将控制权交给 MicroPython。正在运行的脚本可以通过调用
machine.bootloader()按需重新进入该窗口。冻结文件系统初始化。 在任何应用代码运行之前,运行时会先挂载各个文件系统。内部闪存挂载在
/flash(如果其中没有任何内容,则格式化为空白)。如果存在 SD 卡 并且 内部闪存上 不 存在名为SKIPSD的标记文件,则 SD 卡挂载在/sdcard。当构建包含 ROMFS 时,它会被自动挂载在/rom。工作目录被设置为启动目录(如果 SD 卡已挂载则为/sdcard,否则为/flash),并且sys.path会填入/flash、/flash/lib、/sdcard、/sdcard/lib、/rom和/rom/lib。驻留在闪存中的这套初始化由一个名为_boot.py的冻结模块处理——它属于移植与板级基础设施,而非应用钩子。应用不会自定义_boot.py;这由构建负责。从 IDE 向闪存中放入一个SKIPSD文件,是让摄像头从内部闪存而非 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子目录——然后才是冻结模块。闪存或 SD 卡上一个同名的foo.py确实 会覆盖冻结的foo。这正是开发上的便利:把修好的模块放到卡上,软复位,无需重新烧录即可看到变化。
如果某个已发布的产品想要禁止导入时“文件系统覆盖冻结模块”的行为,可以在 boot.py 中尽早清空 sys.path:
import sys
sys.path.clear()
当 sys.path 为空时,所有导入都只从冻结模块解析;闪存、SD 卡或 ROMFS 上的任何内容都无法遮蔽它们。
14.2.2.1.2.4. 资产问题¶
冻结对于代码很好用。但它 不 适合处理大型二进制资产:机器学习模型文件、标签表、JSON 配置、图像模板。把这些作为 Python 字面量嵌入会使源码膨胀、重新编译缓慢,并且把字节码容器浪费在解释器反正只会原样读取的数据上。构建 ROMFS 镜像 页面介绍了填补这一空缺的只读闪存文件系统。