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 中分配所有 tensor

Arena 内部做了几层抽象:

整个分配过程发生在 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 passed

Test 2 — 连续推理稳定性:循环调用 Invoke() 1000 次,验证 arena 内存复用没有累积问题。全部正常完成。

Test 3 — 性能分析:MicroProfiler 记录了每个算子的 tick 数,模拟了真实 MCU 上用硬件定时器做性能 profiling 的流程。

从模拟到真实 MCU

GCC 模拟验证了整体链路跑得通。部署到真实芯片还有几步要走:

这次模拟移植的核心收获:TFLite Micro 的架构设计让移植到新平台非常轻量——只需要实现 3 个平台函数、提供 3 个第三方库的头文件路径、配好编译器 flag。其余代码全部是平台无关的 C++。