TVM 裸机部署实战:基于 Cortex-M55 与 CMSIS-NN 运行人员检测模型
导读
本指南讲解如何在 TVM 开源仓库自带的示例工程中,把量化(int8)神经网络模型编译到 Arm Cortex-M55 裸机(bare metal)环境,并借助 CMSIS-NN 库在 Arm Corstone-300 固定虚拟平台(FVP)上完成一次完整的人员检测推理。读者将掌握从环境准备、tvmc 编译参数、C 头文件数据生成到裸机应用构建与仿真运行的完整链路,并了解 TVM 中 cmsis-nn 目标后端的源码级工作原理。
示例工程位于 apps/microtvm/cmsisnn,核心文件包括一键脚本 run_demo.sh、裸机主程序 src/demo_bare_metal.c、图像转换脚本 convert_image.py 与构建规则 Makefile。
前置条件(Prerequisites)
在 TVM 官方 Docker 容器内运行
如果直接在 TVM 自带的 ci_cpu Docker 容器中运行本示例,下列软件均已被预先安装,可跳过本节的安装步骤。
自行搭建环境时需要的软件
不在 ci_cpu 容器中运行时,需要手动安装以下软件:
- 基于 Arm Corstone-300 的固定虚拟平台(FVP):用于在没有真实硬件的情况下仿真 Cortex-M55 + Ethos-U55 系统。
- cmake 3.19.5:构建 UART 驱动等子组件所需的最低版本。
- Arm 提供的 GCC 工具链(arm-none-eabi):交叉编译裸机程序。
- Arm Ethos-U NPU 驱动栈(core_platform):提供 Corstone-300 平台的外设驱动,如 UART。
- CMSIS:Cortex-M 软件接口标准,提供设备启动代码与内核头文件。
- CMSIS-NN:Arm 官方的高效神经网络内核库,本示例的算子代码最终由它执行。
上述软件均可以通过执行 docker/install/ubuntu_install_ethosu_driver_stack.sh 一键安装,这是官方 CI 镜像所采用的安装脚本。
Python 依赖
进入示例目录后,安装本目录下 requirements.txt 列出的 Python 库:
pip install -r ./requirements.txt
该文件包含了 ethos-u-vela(Ethos-U 编译器)、numpy、Pillow(图像处理)、tflite(模型解析)以及 tvm.micro 相关组件运行所需的依赖。
TVM 本身的安装
两种方式任选其一:
-
源码编译:按官方安装指引编译 TVM,并且在
config.cmake中必须开启以下三个选项:set(USE_CMSISNN ON) set(USE_MICRO ON) set(USE_LLVM ON)USE_CMSISNN决定是否编译cmsis-nn后端。开启后,构建系统会在编译命令中加入-DTVM_USE_CMSISNN宏,并把 src/relay/backend/contrib/cmsisnn 下的全部源码纳入编译,详见 cmake/modules/contrib/CMSISNN.cmake。USE_MICRO启用 microTVM 支持,编译生成的模型运行时依赖 CRT(C Runtime)。USE_LLVM用于代码生成与编译基础设施。
-
通过 TLCPack 安装:直接使用预编译的 TVM 二进制包,跳过源码编译。
更新 PATH 环境变量
需要把 cmake 3.19.5 与 FVP 的可执行文件目录加入 PATH。假设安装在 /opt/arm 下:
export PATH=/opt/arm/FVP_Corstone_SSE-300/models/Linux64_GCC-6.4:/opt/arm/cmake/bin:$PATH
注意 FVP 目录中 Linux64_GCC-6.4 是随 FVP 发行的 GCC 版本化目录名,路径需与实际安装版本一致。
一键运行裸机演示
在 apps/microtvm/cmsisnn 目录下执行:
./run_demo.sh
如果 Ethos-U 平台与 CMSIS 并未安装在默认位置 /opt/arm/ethosu,可以通过命令行参数显式指定,例如:
./run_demo.sh --cmsis_path /home/tvm-user/cmsis \
--ethosu_platform_path /home/tvm-user/ethosu/core_platform
run_demo.sh 支持的完整参数如下(对应脚本内的参数解析逻辑):
| 参数 | 作用 | 默认值 |
|---|---|---|
-h, --help | 打印用法说明 | — |
--cmsis_path CMSIS_PATH | 设置 CMSIS 根目录 | /opt/arm/ethosu/cmsis |
--ethosu_platform_path ETHOSU_PLATFORM_PATH | 设置 Ethos-U core_platform 根目录 | /opt/arm/ethosu/core_platform |
--fvp_path FVP_PATH | 设置 FVP 安装路径(脚本会拼接 models/Linux64_GCC-6.4 子目录加入 PATH) | 依赖系统 PATH |
--cmake_path CMAKE | 指定 cmake 可执行文件 | cmake |
脚本执行的完整流程
运行 run_demo.sh 会依次完成以下步骤:
- 清理并重建构建目录:执行
make cleanall后创建build/目录。 - 下载量化模型:从 TensorFlow 官方仓库下载 int8 量化的人员检测模型
person_detect.tflite。 - 使用 tvmc 编译模型:为目标
cmsis-nn,c(Cortex-M55 CPU + CMSIS-NN)编译,并以 Model Library Format(mlf)输出。 - 下载测试图像:下载
person.bmp作为输入图像。 - 生成 C 头文件:运行 convert_image.py 生成
inputs.h(图像数据)与outputs.h(推理结果存储区)。 - 构建裸机应用:执行
make生成build/demo。 - 在 FVP 上仿真运行:启动 Corstone-300 FVP 运行
demo。 - 输出检测结果:程序打印
Person detected.或No person detected.。
tvmc 编译命令解析
脚本第 121-133 行是模型编译的核心:
python3 -m tvm.driver.tvmc compile --target=cmsis-nn,c \
--target-cmsis-nn-mcpu=cortex-m55 \
--target-c-mcpu=cortex-m55 \
--runtime=crt \
--executor=aot \
--executor-aot-interface-api=c \
--executor-aot-unpacked-api=1 \
--pass-config tir.usmp.enable=1 \
--pass-config tir.usmp.algorithm=hill_climb \
--pass-config tir.disable_storage_rewrite=1 \
--pass-config tir.disable_vectorize=1 ./person_detect.tflite \
--output-format=mlf \
--module-name=detection
各参数含义如下:
--target=cmsis-nn,c:同时指定两个代码生成目标。cmsis-nn目标负责把可融合的卷积、池化等算子卸载给 CMSIS-NN 内核,其余算子由c目标生成标准 C 代码。cmsis-nn目标通过TVM_REGISTER_TARGET_KIND("cmsis-nn")注册(见 src/relay/backend/contrib/cmsisnn/target.cc),其mcpu属性由 src/relay/backend/contrib/cmsisnn/compiler_attrs.cc 中的配置解析逻辑消费。--target-cmsis-nn-mcpu=cortex-m55/--target-c-mcpu=cortex-m55:为两个目标分别指定 CPU 微架构,生成的代码将使用 Cortex-M55 的指令集与 DSP 扩展。--runtime=crt:使用 microTVM 的 C Runtime(CRT)作为运行时。--executor=aot:选择 AOT(Ahead-Of-Time)执行器,把推理逻辑静态展开为tvmgen_detection_run这样的入口函数。--executor-aot-interface-api=c:生成 C 语言风格的结构体输入输出接口。--executor-aot-unpacked-api=1:使用 unpacked API,避免装箱(boxing)开销。--pass-config tir.usmp.enable=1/tir.usmp.algorithm=hill_climb:开启 Unified Static Memory Planner(USMP)静态内存规划,并使用hill_climb贪心算法对内存进行复用,以降低 SRAM 占用。--pass-config tir.disable_storage_rewrite=1:禁用存储重写 pass,配合 USMP 工作。--pass-config tir.disable_vectorize=1:禁用向量化(CMSIS-NN 场景下不依赖 TVM 自身向量化)。--output-format=mlf:以 Model Library Format 输出,产出module.tar,随后tar -xf module.tar解包。--module-name=detection:生成的运行时符号统一以detection为后缀(如tvmgen_detection_run、tvmgen_detection_inputs)。
深入理解代码生成与 CMSIS-NN 后端
TVM 的 CMSIS-NN 支持并非简单的"调用库函数",而是一条完整的编译流水线。从源码结构看,src/relay/backend/contrib/cmsisnn 目录中的模块分别承担:
- compiler_attrs.cc:注册
cmsis-nn目标的编译器配置(mcpu、mattr、debug_last_error),并据配置构造 Target 对象。 - convolutions.cc:将 Relay 的卷积算子映射到 CMSIS-NN 的卷积内核。
- compute_luts.cc:为 int8 激活函数生成查找表(LUT),这是 CMSIS-NN 高效执行激活的常用手段。
- fuse_pads.cc:将 Padding 操作融合进卷积,减少中间张量。
- relay_to_tir.cc:把卸载后的 Relay 子图翻译为 TIR。
- tir_to_runtime.cc:把 TIR 生成 CMSIS-NN 库函数调用序列。
- extract_constants.cc 与 generate_constants.cc:抽取并生成权重常量(如 int8 量化权重、偏置)。
结合前文 tvmc 的 cmsis-nn,c 双目标编译参数,可以推断实际流程为:Relay 图先经算子融合 pass 把可卸载子图标记给 cmsis-nn 编译器,再由上述模块生成调用 CMSIS-NN 函数的 C 代码,其余算子落入通用 c 目标。这种分工正是"CNN 计算密集部分走 DSP 加速库、控制流与通用算子走标准 C"的典型嵌入式部署策略。
裸机主程序分析
编译产物会生成模型专属头文件 tvmgen_detection.h(声明 tvmgen_detection_run 与输入输出结构体),demo 主程序 src/demo_bare_metal.c 直接调用它:
#include <tvm_runtime.h>
#include <tvmgen_detection.h>
#include "uart_stdout.h"
#include "inputs.h"
#include "outputs.h"
int main(int argc, char** argv) {
UartStdOutInit();
printf("Starting Demo\n");
struct tvmgen_detection_outputs detection_outputs = {
.MobilenetV1_Predictions_Reshape_1 = output,
};
struct tvmgen_detection_inputs detection_inputs = {
.input = input,
};
tvmgen_detection_run(&detection_inputs, &detection_outputs);
if (output[1] > output[0]) {
printf("Person detected.\n");
} else {
printf("No person detected.\n");
}
printf("EXITTHESIM\n");
while (1 == 1)
;
return 0;
}
要点:
- 模型入口
tvmgen_detection_run由 AOT 执行器生成,输入输出通过结构体直接传递,接口名中的detection来自 tvmc 的--module-name=detection。 - 输入
input与输出output数组分别来自 convert_image.py 生成的inputs.h与outputs.h。 - 结果判断逻辑是二分类比大小:输出张量第二个元素大于第一个元素即判定"检测到人"。
EXITTHESIM是 FVP 的退出约定字符串,仿真器收到该串后自动结束(见脚本中的-C mps3_board.uart0.shutdown_tag=\"EXITTHESIM\")。
microTVM 平台抽象层
include/tvm_runtime.h 提供了 CRT 在该裸机平台上的适配实现:
TVMPlatformAbort:出错时打印错误码并输出EXITTHESIM终止仿真。TVMPlatformMemoryAllocate/TVMPlatformMemoryFree:本例中返回kTvmErrorFunctionCallNotImplemented,因为 AOT + unpacked API 下所有缓冲区均为静态分配,不需要动态内存。TVMLogf:把日志重定向到vfprintf(stdout, ...),经 UART 输出。TVMFuncRegisterGlobal:AOT 场景直接返回 0(无需注册全局函数)。
这些函数是 microTVM 平台接口(Platform Abstraction Layer)在裸机上的最小实现,任何新硬件移植都要补齐它们。
图像转换:把图片变成 C 数组
convert_image.py 的作用是把输入图像转换为模型所需的 int8 数据并生成两个头文件:
python3 ./convert_image.py ./build/input_image.bmp
处理细节:
- 缩放:使用 Pillow 把图片缩放到 224×224(
Image.open(img_path).resize((224, 224))),与 MobileNetV1 输入尺寸一致。 - 加批维度:
np.expand_dims(img_data, axis=0)得到 NCHW 四维张量。 - 量化偏移:
img_data - 128后转为np.int8,对应模型训练时的输入量化方案(uint8像素减去 128 映射到int8)。 - 生成 inputs.h:把字节以
\xHH十六进制转义序列写入 C 字符串字面量,并声明input数组:const size_t input_len = ...; __attribute__((section(".data.tvm"), aligned(16))) int8_t input[] = "...";section(".data.tvm")配合 corstone300.ld 链接脚本,将大数组放置在 DDR 段(见下文内存布局)。 - 生成 outputs.h:预分配长度为 2 的
int8_t output数组存放推理结果。 - 生成 crt_config.h:调用
tvm.micro.copy_crt_config_header在build/crt_config/下生成 CRT 配置头文件。
构建系统:Makefile 与交叉编译
Makefile 负责把四部分产物链接为裸机可执行文件:
- 生成的代码:
build/codegen/host/src/*.c(tvmc mlf 产物中的模型 C 代码),打包为libcodegen.a。 - CMSIS 启动代码:
CMSIS_PATH/Device/ARM/ARMCM55/Source/*.c,打包为libcmsis_startup.a。 - CMSIS-NN 内核:
find ${CMSIS_PATH}/CMSIS-NN/Source/*/*.c递归收集的所有内核源码,打包为libcmsis_nn.a。 - UART 驱动:从 Ethos-U core_platform 的
drivers/uart经 cmake 交叉编译生成libethosu_uart_cmsdk_apb.a。
关键编译选项:
PKG_COMPILE_OPTS = -g -Wall -O2 -Wno-incompatible-pointer-types -Wno-format \
-mcpu=cortex-m55 -mthumb -mfloat-abi=hard -std=gnu99
-mcpu=cortex-m55 -mfloat-abi=hard 启用 Cortex-M55 的 FPU;链接时使用 -specs=nosys.specs -static -T corstone300.ld(无操作系统环境 + 静态链接 + 自定义链接脚本)。
交叉编译工具链文件 arm-none-eabi-gcc.cmake 把编译器固定为 arm-none-eabi-gcc/g++,处理器设为 cortex-m55,并根据 +fp/+nofp 特性后缀自动决定 -mfloat-abi 取值;同时禁用 C++ 异常与 RTTI 以适配嵌入式环境。
链接脚本中的内存布局
corstone300.ld 定义了 Corstone-300 系统的内存映射:
| 内存区域 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| ITCM | 0x00000000 | 512KB | 存放 .text 代码(CPU 只读) |
| DTCM | 0x20000000 | 512KB | 存放 .data、.bss、栈与堆 |
| SSE-300 SRAM | 0x21000000 | 2MB | 可读写(NPU 可访问) |
| Data SRAM | 0x01000000 | 2MB | 可读写 |
| DDR | 0x60000000 | 32MB | 大容量可读写(NPU 可访问) |
链接脚本把 .rodata.tvm 与 .data.tvm 段(即模型权重与输入图像所在段)单独放入 .ddr 段,规避了 ITCM/DTCM 容量限制;同时在脚本末尾用 ASSERT(__StackLimit >= __bss_end__, "region DTCM overflowed with stack") 检查 DTCM 是否溢出。
使用自己的图片
convert_image.py 接收一个命令行参数:待转换图片的路径。演示默认使用的测试图片由 run_demo.sh 从 TensorFlow 仓库下载:
curl -sS https://raw.githubusercontent.com/tensorflow/tflite-micro/main/tensorflow/lite/micro/examples/person_detection/testdata/person.bmp -o input_image.bmp
若要换成自己的图片,只需修改 run_demo.sh 中这一行为自己图片的下载/复制命令,或直接把图片放到 build/ 目录后手动执行:
python3 ./convert_image.py ./build/your_image.bmp
注意脚本会强制缩放到 224×224,因此任意分辨率的图片均可使用;而 no_person.bmp 等负样本同样被注释保留在脚本中,可用来验证"未检测到人"的输出路径。
在 FVP 上运行与结果验证
脚本末尾通过以下命令启动仿真:
FVP_Corstone_SSE-300_Ethos-U55 -C cpu0.CFGDTCMSZ=15 \
-C cpu0.CFGITCMSZ=15 -C mps3_board.uart0.out_file=\"-\" \
-C mps3_board.uart0.shutdown_tag=\"EXITTHESIM\" \
-C mps3_board.visualisation.disable-visualisation=1 \
-C mps3_board.telnetterminal0.start_telnet=0 \
... \
./build/demo
关键 FVP 配置项说明:
cpu0.CFGDTCMSZ=15/cpu0.CFGITCMSZ=15:配置 DTCM 与 ITCM 的地址解码大小(0x80000,即 512KB,与链接脚本一致)。mps3_board.uart0.out_file=\"-\":把 UART 输出重定向到 stdout,使printf结果直接显示在终端。mps3_board.uart0.shutdown_tag=\"EXITTHESIM\":UART 收到EXITTHESIM字符串后仿真器自动退出(对应 demo 主程序最后的printf("EXITTHESIM\n"))。mps3_board.visualisation.disable-visualisation=1:关闭可视化窗口,适合无头(headless)环境。telnetterminal*.start_telnet=0:关闭无关的 telnet 终端,避免干扰。
正常运行结束时,终端会依次出现 Starting Demo、Running detection inference、Person detected.(或 No person detected.)以及 EXITTHESIM,随后 FVP 自行关闭。
小结
本示例完整覆盖了 TVM 面向 Cortex-M 裸机 + CMSIS-NN 的部署链路:环境搭建(Docker 脚本或手动安装)→ tvmc 双目标编译(cmsis-nn,c + AOT/CRT/USMP 系列 pass 配置)→ 图像转 C 数组(convert_image.py)→ Makefile 交叉链接(CMSIS 启动代码 + CMSIS-NN 内核 + UART 驱动 + 模型代码)→ Corstone-300 FVP 仿真验证。通过本文涉及的源码路径,读者还可以进一步阅读 src/relay/backend/contrib/cmsisnn 下的算子映射与常量生成实现,或参考 apps/microtvm/ethosu 等相邻示例对比不同后端的部署方式。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



