TFLite Micro 的 GCC 模拟移植
在单片机上跑 AI 推理是一个越来越常见的需求——设备端处理意味着低延迟、无需网络、数据隐私可控。TensorFlow Lite Micro (TFLM) 就是为此设计的:它是 TensorFlow Lite 的微控制器分支,可以在只有几十 KB RAM 的芯片上运行神经网络。
但 TFLM 的官方构建依赖 Bazel 和 Makefile,目标平台是 ARM Cortex-M 配合嵌入式工具链。如果我想在 Windows 上用 GCC 先跑通一遍,理解它的内部结构,需要做什么?
这篇文章记录了一次"模拟移植"过程:用 MinGW GCC 编译器,在 Windows 上把 TFLite Micro 编译成一个独立可执行文件,用静态内存池替代 malloc,用嵌入 C 数组替代文件系统,最终跑通 hello_world 模型的 sin(x) 推理。
移植目标与约束
真实 MCU 环境有几个硬约束:
| 约束 | MCU 实际情况 | 模拟方式 |
|---|---|---|
| 无操作系统 | 裸机或 RTOS,无 POSIX | 只用 <cstdio>、<cmath>、<ctime> |
| 无动态内存 | 没有 malloc/free,用静态 buffer | uint8_t tensor_arena[3000] 静态数组 |
| 无文件系统 | 模型不能从文件加载 | 模型转 C 字节数组嵌入代码 |
| 无异常/RTTI | C++ 异常和 RTTI 开销太大 | -fno-exceptions -fno-rtti |
| 代码体积敏感 | Flash 通常只有几百 KB | -ffunction-sections -Wl,--gc-sections |
编译器用 MinGW-w64 自带的 GCC 8.1.0,项目目录结构如下:
port/mcu/
├── platform/ # 平台抽象层(3个文件)
│ ├── mcu_debug_log.cc # 日志输出 → vfprintf(stderr)
│ ├── mcu_system_setup.cc # 系统初始化 → 空函数
│ └── mcu_micro_time.cc # 计时器 → clock()
├── demo/
│ ├── mcu_demo.cc # 主程序
│ └── model_data.h # hello_world 模型嵌入
├── mcu_config.h # 编译配置
├── Makefile / build.ps1 # 构建脚本
└── build/
└── mcu_demo.exe # 最终产物 290KB平台抽象层:三个函数打通 TFLM 与裸机
TFLite Micro 的设计把平台相关的部分抽象成了几个函数,移植时只需要实现它们。核心有三个:
DebugLog — TFLM 内部所有日志输出都走这个函数。在真实 MCU 上它应该写 UART;在模拟环境里直接用 vfprintf(stderr, ...):
extern "C" void DebugLog(const char* format, va_list args) {
vfprintf(stderr, format, args);
}InitializeTarget — 芯片上电后的初始化入口(配时钟、配 GPIO 等)。模拟环境不需要做任何事:
void InitializeTarget() {
// 真实 MCU:配置系统时钟、GPIO、UART 等
}micro_time — 提供 tick 计数器,用于性能分析。模拟环境用 clock(),真实 MCU 用硬件定时器(SysTick):
uint32_t ticks_per_second() { return CLOCKS_PER_SEC; }
uint32_t GetCurrentTimeTicks() { return static_cast<uint32_t>(clock()); }这三个文件替换了 TFLM 源码中的默认实现,其余 TFLM 代码不需要任何修改。
依赖管理:flatbuffers、gemmlowp、ruy
TFLM 有三层外部依赖:
FlatBuffers — 模型解析。TFLite 模型(.tflite 文件)是 FlatBuffers 格式的序列化数据。TFLM 用 schema_generated.h(由 FlatBuffers 编译器生成)来解析模型结构。需要下载 FlatBuffers 库(约 3MB 的头文件),提供 flatbuffers/flatbuffers.h 的 include 路径。
gemmlowp — 定点运算。量化推理(int8)需要定点乘法、饱和运算等。TFLM 的 common.h 大量使用 gemmlowp::FixedPoint 模板类和配套的算术函数。这个库很小,只有头文件。
ruy — 矩阵乘法优化库。TFLM 的参考内核只引用了它的 profiler 头文件(ruy/profiler/instrumentation.h),实际不调用任何 ruy 函数。写一个空桩即可:
namespace ruy { namespace profiler {
inline void ScopeLabel(const char*) {}
}}静态内存:Arena 分配器
TFLM 不使用 malloc。推理所需的所有内存——输入输出 tensor、中间计算结果、算子参数——全部从一个静态字节数组中分配:
constexpr int kTensorArenaSize = 3000;
uint8_t tensor_arena[kTensorArenaSize]; // 静态数组,放在 .bss 段
tflite::MicroInterpreter interpreter(model, op_resolver,
tensor_arena, kTensorArenaSize);
interpreter.AllocateTensors(); // 从 arena 中分配所有 tensorArena 内部做了几层抽象:
- MemoryPlanner — 规划 buffer 的生命周期,决定哪些 tensor 可以复用同一块内存。默认使用 GreedyMemoryPlanner,离线计算最优复用方案。
- BufferAllocator — 执行实际的内存分配。有三种:
SingleArenaBufferAllocator(可复用临时 buffer)、PersistentArenaBufferAllocator(持久分配的权重/参数)、NonPersistentArenaBufferAllocator(仅单次推理有效的 scratch buffer)。
整个分配过程发生在 AllocateTensors() 中,推理时不再有任何内存操作。
模型嵌入:.tflite 转 C 数组
没有文件系统,模型文件需要以 C 字节数组的形式嵌入代码。用 PowerShell 把 hello_world 模型(3164 字节)转换:
$bytes = [System.IO.File]::ReadAllBytes("hello_world_float.tflite")
# 输出 unsigned char g_model_data[] = {0x1C, 0x00, ...};然后在代码中直接使用:
#include "model_data.h"
const tflite::Model* model = ::tflite::GetModel(g_model_data);GetModel() 不拷贝数据——它只是把指针 cast 成 FlatBuffers 的 Model 结构体,零开销。
算子注册:只注册需要的
TFLM 的 MicroMutableOpResolver 采用模板参数指定最大算子数量,所有注册信息放在栈上,不触发堆分配:
using DemoOpResolver = tflite::MicroMutableOpResolver<1>;
TfLiteStatus RegisterOps(DemoOpResolver& op_resolver) {
TF_LITE_ENSURE_STATUS(op_resolver.AddFullyConnected());
return kTfLiteOk;
}hello_world 模型只有一个 FullyConnected 算子(后面接 ReLU 激活,但 ReLU 被融合到 FullyConnected 的激活参数中,不需要单独注册)。链接器会把没用到的算子代码全部 strip 掉。
编译链接:41 个目标文件
最终要编译的源文件分布在几个模块中:
| 模块 | 文件数 | 说明 |
|---|---|---|
| Core | 16 | micro_interpreter、micro_allocator、micro_op_resolver 等 |
| Arena | 3 | single/persistent/non_persistent 三种 allocator |
| MemoryPlanner | 3 | greedy、linear planner 及 shim |
| Kernels | 7 | fully_connected、reshape、kernel_util、micro_tensor_utils、lite kernel_util |
| API | 5 | flatbuffer_conversions、schema_utils、common、error_reporter、array |
| Internal | 4 | quantization_util、portable_tensor_utils、common、tensor_ctypes |
| Platform | 3 | 我们的平台抽象层 |
| Demo | 1 | 主程序 |
编译命令的核心参数:
g++ -std=c++17 \
-fno-exceptions -fno-rtti \
-ffunction-sections -fdata-sections \
-DTF_LITE_USE_CTIME \
-O2 \
[41个.cc文件] \
-Wl,--gc-sections -lm \
-o mcu_demo.exe链接后得到 290KB 的可执行文件。如果设置 -Os 并 strip 符号表,还能更小。
完整 Demo 代码
把前面各节的内容串起来,就是一个完整的推理程序。这个 demo 直接编译即可运行,不依赖任何外部文件(模型已嵌入),不触发堆分配(全部栈上或静态区):
// mcu_demo.cc — TFLite Micro MCU Port Demo
// 模型: y = sin(x), 算子: FullyConnected + ReLU
#include <cstdio>
#include <cmath>
#include "tensorflow/lite/core/c/common.h"
#include "tensorflow/lite/micro/micro_interpreter.h"
#include "tensorflow/lite/micro/micro_log.h"
#include "tensorflow/lite/micro/micro_mutable_op_resolver.h"
#include "tensorflow/lite/micro/micro_profiler.h"
#include "tensorflow/lite/micro/system_setup.h"
#include "tensorflow/lite/schema/schema_generated.h"
#include "model_data.h" // g_model_data[] 字节数组
namespace {
using DemoOpResolver = tflite::MicroMutableOpResolver<1>;
TfLiteStatus RegisterOps(DemoOpResolver& op_resolver) {
TF_LITE_ENSURE_STATUS(op_resolver.AddFullyConnected());
return kTfLiteOk;
}
// Test 1: 浮点模型推理, 验证 sin(x) 拟合精度
TfLiteStatus RunFloatModelInference() {
MicroPrintf("\n=== Test 1: Float Model Inference ===");
const tflite::Model* model = ::tflite::GetModel(g_model_data);
TFLITE_CHECK_EQ(model->version(), TFLITE_SCHEMA_VERSION);
DemoOpResolver op_resolver;
TF_LITE_ENSURE_STATUS(RegisterOps(op_resolver));
// MCU 关键: 静态 arena, 不调 malloc
constexpr int kTensorArenaSize = 3000;
static uint8_t tensor_arena[kTensorArenaSize];
tflite::MicroInterpreter interpreter(
model, op_resolver, tensor_arena, kTensorArenaSize);
TF_LITE_ENSURE_STATUS(interpreter.AllocateTensors());
TfLiteTensor* input = interpreter.input(0);
TfLiteTensor* output = interpreter.output(0);
constexpr int kNumTests = 7;
const float test_inputs[kNumTests] =
{0.0f, 0.5f, 1.0f, 1.5f, 2.0f, 3.0f, 5.0f};
const float epsilon = 0.10f;
int passed = 0;
for (int i = 0; i < kNumTests; ++i) {
input->data.f[0] = test_inputs[i];
TF_LITE_ENSURE_STATUS(interpreter.Invoke());
float y_pred = interpreter.output(0)->data.f[0];
float y_true = sinf(test_inputs[i]);
float error = fabsf(y_true - y_pred);
if (error < epsilon) ++passed;
MicroPrintf(" sin(%5.2f) = %+7.4f | pred = %+7.4f | err = %6.4f %s",
test_inputs[i], y_true, y_pred, error,
error < epsilon ? "OK" : "FAIL");
}
return (passed == kNumTests) ? kTfLiteOk : kTfLiteError;
}
// Test 2: 1000 次连续推理, 验证 arena 内存复用稳定性
TfLiteStatus RunMultipleInvocations() {
const tflite::Model* model = ::tflite::GetModel(g_model_data);
DemoOpResolver op_resolver;
TF_LITE_ENSURE_STATUS(RegisterOps(op_resolver));
constexpr int kTensorArenaSize = 3000;
static uint8_t tensor_arena[kTensorArenaSize];
tflite::MicroInterpreter interpreter(
model, op_resolver, tensor_arena, kTensorArenaSize);
TF_LITE_ENSURE_STATUS(interpreter.AllocateTensors());
constexpr int kIters = 1000;
volatile float sum = 0.0f; // volatile 防编译器优化
for (int i = 0; i < kIters; ++i) {
interpreter.input(0)->data.f[0] = static_cast<float>(i) * 0.01f;
TF_LITE_ENSURE_STATUS(interpreter.Invoke());
sum += interpreter.output(0)->data.f[0];
}
MicroPrintf(" Ran %d inferences, checksum: %f", kIters,
static_cast<float>(sum));
return kTfLiteOk;
}
} // namespace
int main() {
tflite::InitializeTarget();
MicroPrintf("========================================");
MicroPrintf(" TFLite Micro — MCU Port Demo (GCC)");
MicroPrintf(" Model: hello_world (y = sin(x))");
MicroPrintf("========================================");
if (RunFloatModelInference() != kTfLiteOk) return -1;
if (RunMultipleInvocations() != kTfLiteOk) return -1;
MicroPrintf("\n========================================");
MicroPrintf(" ALL TESTS PASSED");
MicroPrintf("========================================");
return 0;
}配套的平台抽象层三个文件:
// mcu_debug_log.cc — 日志输出
extern "C" void DebugLog(const char* format, va_list args) {
vfprintf(stderr, format, args); // 真实 MCU 写 UART
}
extern "C" int DebugVsnprintf(char* buf, size_t n, const char* fmt,
va_list vl) {
return vsnprintf(buf, n, fmt, vl);
}// mcu_system_setup.cc — 系统初始化
#include "tensorflow/lite/micro/system_setup.h"
namespace tflite {
void InitializeTarget() {
// 真实 MCU: 配时钟、GPIO、UART
}
} // namespace tflite// mcu_micro_time.cc — 计时器
#include "tensorflow/lite/micro/micro_time.h"
#include <ctime>
namespace tflite {
uint32_t ticks_per_second() { return CLOCKS_PER_SEC; }
uint32_t GetCurrentTimeTicks() { return (uint32_t)clock(); }
} // namespace tflite推理验证
Demo 跑了三个测试:
Test 1 — 浮点模型推理:输入 7 个值(0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 5.0),对比模型输出和标准 sin(x),误差阈值 0.10。全部通过。
sin( 0.00) = +0.0000 | predicted = +0.0264 | error = 0.0264 OK
sin( 0.50) = +0.4794 | predicted = +0.4540 | error = 0.0254 OK
sin( 1.00) = +0.8415 | predicted = +0.8630 | error = 0.0216 OK
sin( 1.50) = +0.9975 | predicted = +0.9816 | error = 0.0158 OK
sin( 2.00) = +0.9093 | predicted = +0.8872 | error = 0.0221 OK
sin( 3.00) = +0.1411 | predicted = +0.1276 | error = 0.0135 OK
sin( 5.00) = -0.9589 | predicted = -0.9565 | error = 0.0024 OK
Result: 7 / 7 tests passedTest 2 — 连续推理稳定性:循环调用 Invoke() 1000 次,验证 arena 内存复用没有累积问题。全部正常完成。
Test 3 — 性能分析:MicroProfiler 记录了每个算子的 tick 数,模拟了真实 MCU 上用硬件定时器做性能 profiling 的流程。
从模拟到真实 MCU
GCC 模拟验证了整体链路跑得通。部署到真实芯片还有几步要走:
- 算子优化:参考内核(
reference/目录)是纯 C++ 的可移植实现。真实部署会用 CMSIS-NN(ARM)、Xtensa(ESP32-S3)、或 Ethos-U(Arm NPU)的优化版本替换。例如 CMSIS-NN 的 FullyConnected 使用 SIMD 指令,比参考实现快 4-5 倍。 - 日志通道:
DebugLog改成 UART DMA 输出,避免 printf 阻塞推理时序。 - 内存规划:用
RecordingMicroInterpreter跑一次推理,记录实际的 arena 使用峰值,把kTensorArenaSize调到刚好够用。 - 模型量化:浮点模型在 Cortex-M4 上没有 FPU 的情况下很慢。用 TFLite Converter 把模型量化成 int8,推理速度能提升一个数量级,同时模型体积减半。
这次模拟移植的核心收获:TFLite Micro 的架构设计让移植到新平台非常轻量——只需要实现 3 个平台函数、提供 3 个第三方库的头文件路径、配好编译器 flag。其余代码全部是平台无关的 C++。