10.2 TFLM 运行时与模型嵌入
本阶段涵盖两项内容:将 TensorFlow Lite Micro (TFLM) 运行时集成到 Q2390 / IQ2390 MCU 固件中,以及将预先构建的 int8 模型(在第 1 阶段生成)直接嵌入固件镜像。其中包括在 SDLLVM/RISC-V 上所需的、不易察觉的 C++ 运行时设置。10.2.1 概述
集成包含五个不同部分:- 获取 TFLM 源码 — TFLM 源码未随仓库提供(not vendored),必须手动获取
- 检查模型并验证算子支持 — 从模型中提取算子类型,并确认 TFLM 支持这些算子
- 配置 LLVM libc++ — SDLLVM 默认不配置 libc++;需要进行 5 项特定修复
- 创建推理模块 — 构建包含嵌入式模型 C 数组、推理框架(
tflite_infer.cc)、Kconfig 头文件和 CMakeLists.txt 的模块 - 集成到固件构建中 — 在
main.c中调用tflite_infer_run(),并在 CMake 中注册该模块
10.2.2. 第 1 部分 — 获取 TFLite Micro 源码
路径约定: 在本阶段中,TFLM 模块桩文件(<wasp_proc>指 MCU 固件工作区的根目录,即包含zephyr/、modules/、config/等的目录。凡出现此处,请替换为您的实际路径(例如/path/to/your_workspace/wasp_proc)。
CMakeLists.txt、Kconfig)位于 wasp_proc/zephyr/zephyr/modules/tflite-micro/,但实际的 C++ 源码树并不存在,必须手动克隆。
tflite-micro 项目在 west 清单(zephyr/zephyr/submanifests/optional.yaml)中定义于 optional 组,而 Zephyr 自身的 west.yml 通过 group-filter: [-optional] 显式禁用了该组。这意味着 west update tflite-micro 将无法工作,请改用 git clone。
10.2.2.1. 克隆源码
在 Docker 构建容器内:10.2.2.2. 验证
10.2.3. 第 2 部分 — 检查模型并验证算子支持
在编写任何固件代码之前,我们需要从模型中获取两项信息:- 模型使用了哪些算子 — TFLM 运行时要求在运行推理之前显式注册模型使用的每个算子。如果缺少任何算子,固件将在启动时因无法解析算子而报错。此步骤可确定模型所需的确切算子集合,以便您在 第 5 部分 中集成推理框架时知道需要注册哪些算子。
- 输入/输出量化参数(scale + zero_point)— 如果模型采用 int8 量化,则浮点输入在传递给推理框架之前必须预先量化为 int8。
10.2.3.1. 选项 A — flatc(完整拓扑解码 — 推荐)
使用 TFLM schema 将模型二进制解码为人类可读的 JSON。可获得:所有张量的算子类型、张量名称、形状和量化参数。
步骤 1 — 运行 flatc 解码模型:
cnn1d_minimal_int8.json。
步骤 2 — 从 JSON 中提取算子列表:
10.2.3.2. 选项 B — tf.lite.Interpreter(仅量化参数,无需 flatc)
在 flatc 不可用时使用。可获得输入/输出量化参数(scale、zero_point)。不会暴露算子类型 — 请使用选项 A 获取算子列表。
tf.lite.Interpreter不会暴露算子类型 — 请使用选项 A(flatc)获取算子列表。
10.2.3.3. 此 TFLM 构建支持的所有算子
TFLM 所支持算子的权威来源是micro_mutable_op_resolver.h — 该文件中声明的每个 Add*() 方法都对应所下载的确切修订版本所支持的一个算子。
zephyr_20240627 的完整支持算子列表 — 共 114 个算子:
每个算子对应的 resolver 方法为Add+ 算子名称 — 例如Conv2D→AddConv2D(),FullyConnected→AddFullyConnected()。
10.2.3.4. 参考 — 示例模型已确认的算子
cnn1d_minimal_int8.tflite 中的所有算子、对应的 TFLM 内核文件以及 resolver 方法:
如果某个算子未注册,您将看到以下错误之一:
编译时(方法名称错误或缺失):
AllocateTensors() 阶段(算子未注册):
10.2.4. 第 3 部分 — 配置 LLVM libc++(5 步修复)
10.2.4.1. 默认方法为何失败
要使用 TFLM(以 C++ 编写),Zephyr 需要 C++ 标准库。自然的起点是启用CONFIG_LIBCXX_LIBCPP=y。然而在 SDLLVM 上,这会触发一条破坏构建的 Kconfig 依赖链。该故障会表现为一个完全不相关的内核文件中的错误:
10.2.4.2. 4 步修复
10.2.4.2.1. 步骤 1 — 从 TFLM Kconfig 中移除 select REQUIRES_FULL_LIBCPP
文件:zephyr/zephyr/modules/tflite-micro/Kconfig
从 config TENSORFLOW_LITE_MICRO 内部移除 select REQUIRES_FULL_LIBCPP 这一行。这是依赖级联的根源 — 移除它可防止 Zephyr 在 SDLLVM 上触发已损坏的 REQUIRES_FULL_LIBC 路径。
10.2.4.2.2. 步骤 2 — 使用 EXTERNAL_MODULE_LIBCPP,而非 EXTERNAL_LIBCPP
此处无需操作 — 该设置包含在第 2.4.4 节创建的新文件中。
10.2.4.2.3. 步骤 3 — 通过 -I 而非 -isystem 注入 libc++ 包含路径
此处无需操作 — 该调用是第 2.4.3 节中添加的完整代码块的一部分。
-I 目录会先于任何 -isystem 目录被搜索。通过以 -I BEFORE 方式注入 libc++ 头文件,clang 会首先找到 libc++ 自身的 <stdio.h>/<string.h> 包装头文件,它们再通过 #include_next 链接到 musl — 这正是 C++ 翻译单元的正确解析顺序。
10.2.4.2.4. 步骤 4 — 使用 -DNDEBUG 编译 C++ 翻译单元
此处无需操作 — 该标志是第 2.4.3 节中添加的完整代码块的一部分。
musl 的 assert() 宏会展开为 __assert_fail(),而 picolibc 并不提供该函数。-DNDEBUG 会在 C++ 翻译单元中编译掉所有 assert 调用。其作用范围限定为 $<$<COMPILE_LANGUAGE:CXX>:-DNDEBUG>,因此 C 代码不受影响。
10.2.4.3. 完整的 CMake 配置代码块
文件:modules/hal/qcom/core/config/CMakeLists.txt(已有文件 — 在末尾添加此完整代码块)。该代码块以 CONFIG_EXTERNAL_MODULE_LIBCPP 为条件,因此仅在合并 TFLM Kconfig 片段时才会生效。
10.2.4.4. shikra_lpaicp_tflite.conf
10.2.4.4.1. 步骤 1 — 创建 shikra_lpaicp_tflite.conf
在 zephyr/kernel/config/shikra_lpaicp_tflite.conf 创建新文件:
10.2.4.4.2. 步骤 2 — 在 config.yml 中注册该文件
文件:zephyr/kernel/config/config.yml(已有文件 — 添加一个新条目):
为什么使用单独的文件,而不是添加到shikra_lpaicp.conf? 现有条目使用正则表达式(^|shikra_)lpaicp_.*,它同时匹配硬件目标(SHIKRA_LPAICP_TEST)和使用 Zephyr-SDK GCC 的 QEMU 仿真变体。将 TFLM 特定于 SDLLVM 的 libc++ 配置添加到shikra_lpaicp.conf会破坏基于 GCC 的 QEMU 构建。范围更窄的正则表达式shikra_lpaicp_.*仅匹配硬件目标。
10.2.5. 第 4 部分 — 创建推理模块
10.2.5.1. 步骤 1 — 生成 src/model_data.cpp 和 src/input_data.cpp
首先创建模块目录,然后将第 1 阶段生成的模型和输入文件转换为 C 字节数组。请在包含第 1 阶段源文件的目录中运行这些命令。<wasp_proc> 替换为您的 wasp_proc MCU 代码库路径。
生成的文件如下所示:
模型数组上的alignas(8): TFLM 的 FlatBuffers 解析器要求模型字节数组至少按 4 字节对齐。无论链接器将该符号放置在何处,alignas(8)都能保证满足这一要求。
10.2.5.2. 步骤 2 — 创建 inc/tflite_infer.h
文件:modules/hal/qcom/core/tflite_infer/inc/tflite_infer.h(新文件)。
10.2.5.2.1. 调整 TFLI_ARENA_KB
TFLI_ARENA_KB 设置张量 arena 的大小 — 这是 TFLM 用于输入/输出张量、层间中间激活缓冲区及其内部分配器元数据的连续内存块。它在 tflite_infer_run() 开始时通过 k_malloc 从系统堆中分配。
取值不当时会出现的问题:
这两种故障都会导致
tflite_infer_run() 提前返回 — g_tfli_batches 保持为 0,所有延迟全局变量保持其初始值。
如何确定合适的值 — 使用 g_tfli_arena_used 进行测量:
- 将
TFLI_ARENA_KB设置为一个充裕的初始值(例如32)— 足够大以保证AllocateTensors()成功。 - 构建、烧写、启动,并在 T32 中运行
read_tflite_infer.cmm。 - 确认
g_tfli_status == 0且g_tfli_batches >= 1。 - 从 T32 Var.View 窗口读取
g_tfli_arena_used— 这是 TFLM 所需的确切字节数。 - 计算最小安全值并更新头文件:
- 重新构建并重新烧写。验证
g_tfli_arena_used仍小于g_tfli_arena_size。
参考模型的配置值:TFLI_ARENA_KB = 4(4,096 字节)。这是参考代码库中cnn1d_minimal_int8.tflite所使用的值。成功运行后,请在目标设备上测量g_tfli_arena_used,以确认您的模型的使用情况。
TFLI_ARENA_KB 对 RAM 的影响:
arena 在运行时从系统堆中分配 — 它不会直接增加静态镜像大小。但是,堆的后备缓冲区(kheap_buf__system_heap)是一个静态链接段,其大小由 CONFIG_HEAP_MEM_POOL_SIZE 决定(在 shikra_lpaicp.conf 中设置为 32768)。仅减小 TFLI_ARENA_KB 并不会减少静态 RAM。若要回收静态 RAM,还需相应降低 CONFIG_HEAP_MEM_POOL_SIZE — 确保新的大小能够覆盖 TFLI_ARENA_KB * 1024 + 16 以及其他所有 k_malloc 调用方的需求。
10.2.5.3. 步骤 3 — 创建 CMakeLists.txt
文件:modules/hal/qcom/core/tflite_infer/CMakeLists.txt(新文件)。以 CONFIG_TENSORFLOW_LITE_MICRO 为条件。
10.2.5.4. 步骤 4 — 创建 src/tflite_infer.cc
文件:modules/hal/qcom/core/tflite_infer/src/tflite_infer.cc(新文件)。每个批次运行 TFLI_ITERS 次计时的 Invoke() 调用(在一次预热之后),然后将每批次以微秒、QTMR tick 和 CPU 周期表示的最小/平均/最大延迟发布到 g_tfli_* volatile 全局变量中。
关于__attribute__((retain))的说明:g_tfli_arena_size和g_tfli_iters仅在初始化时写入,固件代码从不读取它们。如果没有__attribute__((retain)),即使它们是volatile,链接器的--gc-sections过程也会悄无声息地丢弃它们的 ELF 段 —volatile可防止编译器将其优化掉,但无法防止链接器 GC。retain属性会将该段标记为始终存活,从而确保这些符号对 T32 保持可见。
10.2.6. 第 5 部分 — 集成到固件构建中
10.2.6.1. 步骤 1 — 将 tflite_infer_run() 集成到 main.c 中
文件:modules/hal/qcom/core/main/src/main.c
放置位置:tflite_infer_run()放在CONFIG_QC_CLK_READY代码块之后。如果该代码块不存在,则将其直接放在初始的LOG_INF/printk行之后,效果相同。
10.2.6.2. 步骤 2 — 在 CMake 中注册该模块
文件:modules/hal/qcom/core/config/CMakeLists.txt(已有文件 — 在 add_subdirectory_ifdef 代码块末尾添加一行):
add_subdirectory_ifdef 行之后:
10.2.7. RAM 说明
在添加 TFLM 之前,Shikra LPAICP 固件大约有 40 KB 的空闲 RAM。TFLM 的静态代码占用(3 个算子子集约 25 KB)、模型数据(约 2 KB)以及堆分配可能会使镜像接近 3 MB 的上限。 如果构建失败并出现:zephyr/kernel/config/config.yml 中注释掉 log.conf:
log.conf 会启用 Zephyr 的延迟日志子系统:一个 16 KB 的环形缓冲区以及一个具有 4 KB 栈的专用后台线程,共占用约 20 KB 静态 RAM。由于此固件使用 T32 RAM 控制台进行输出(无 UART),延迟日志线程没有输出路径。将其注释掉可回收约 20 KB,同时 shikra_lpaicp.conf 中的 CONFIG_LOG_MODE_MINIMAL=y 仍保持生效。
10.2.8. Kconfig 标志参考
为 TFLM 集成而添加或修改的所有 Kconfig 符号:
推理框架参数(
TFLI_ARENA_KB、TFLI_ITERS、TFLI_BATCH_SLEEP_MS)是 inc/tflite_infer.h 中的 #define 常量。直接编辑该文件即可进行调整,无需重新编译其他任何文件。有关 arena 大小确定步骤,请参阅第 2.5.2.1 节。
