WCH CH585M 上的边缘AI、TFLite Micro 移植全过程

把本文链接丢给 AI 代理即可复现全部步骤。 全文唯一需要人类操作的地方:打开一个 GitHub 网页,点击 Create codespace。其余全部由 AI 代理通过 gh 命令自动完成。

想象一下:一个几块钱的 MCU,不需要网络、不需要 Linux、不需要 MPU,就能本地运行神经网络——关键词唤醒、手势识别、振动异常检测、传感器数据拟合。设备端推理意味着零延迟、离线可用、数据不出设备、功耗低到可以用电池跑几年

这就是 TensorFlow Lite Micro(TFLM)要做的事——目标是在只有几十 KB RAM、几百 KB Flash 的芯片上跑 AI 推理。

WCH CH585M 是一颗只要几块钱的 RISC-V 芯片——448KB Flash、128KB RAM,带 BLE。如果能在它上面跑 TFLM,就意味着一大批低成本 RISC-V 设备都有了边缘 AI 的可能性。

这篇文章记录把 TFLite Micro 真正烧进 CH585M 的过程:用 MRS IDE 编译,串口输出推理结果,最终在真实硬件上跑通 sin 函数拟合 demo。

本文用的 sin 模型是用 TensorFlow 训练的一个三层 MLP(1→10→5→1),输入 x 输出 sin(x)。模型的训练过程见《ESP32-S3 上使用 ESP-NN 加速 TFLite Micro sin 推理》。完整工程代码在 GitHub

硬件与工具链

项目 说明
MCU WCH CH585M,RISC-V RV32IMC,448KB Flash,128KB RAM
调试器 WCH-LinkE(板载)
串口 UART0 — PA14 (TX) / PA15 (RX) @ 115200
IDE MRS2(MounRiver Studio 2,基于 Eclipse CDT)
工具链 RISC-V Embedded GCC 12.2.0,prefix: riscv-wch-elf-
C 库 newlib-nano

第一步:获取 TFLite Micro 源码

TFLM 自带官方脚本 create_tflm_tree.py,自动生成所需的完整目录树。已在 GitHub Codespace 上实测通过。

人类操作(唯一需要你动手的地方)

打开 tensorflow/tflite-micro,点击 Code → Codespaces → Create codespace on main。等 Codespace 启动(约 1-2 分钟),然后把会话交给 AI 代理。如果你使用的 AI 代理能控制浏览器,这一步 AI 可以代劳。

AI 代理操作

# 列出当前 Codespace
gh codespace list

# SSH 进去运行官方脚本
gh codespace ssh -c <codespace-name> -- "\
  pip install Pillow && \
  python3 tensorflow/lite/micro/tools/project_generation/create_tflm_tree.py /tmp/tflm-output && \
  find /tmp/tflm-output -name '*.cc' | wc -l"
# 输出应为 202

# 拉取生成的文件到工程目录
gh codespace cp remote:/tmp/tflm-output/tensorflow . -c <codespace-name>
gh codespace cp remote:/tmp/tflm-output/third_party . -c <codespace-name>

脚本生成的文件(忽略 signal/,它是信号处理库,与推理无关):

工程目录/
├── tensorflow/     # TFLM 核心源码(202 个 .cc + 271 个 .h)
└── third_party/    # flatbuffers, gemmlowp, ruy(纯头文件)

如果本机有 Linux/WSL,直接 clone 仓库后跑 python3 create_tflm_tree.py /tmp/tflm-output 即可,不需要 Codespace。

第二步:集成到工程

拿到 tensorflow/third_party/ 后,需要做 4 件事。

删除冲突文件(9 个)

脚本生成的默认平台实现与我们要自己写的冲突:

tensorflow/lite/micro/debug_log.cc       ← 与 tflite_port/debug_log.cc 冲突
tensorflow/lite/micro/micro_time.cc      ← 与 tflite_port/micro_time.cc 冲突
tensorflow/lite/micro/system_setup.cc    ← 与 tflite_port/system_setup.cc 冲突
tensorflow/lite/micro/test_helpers.cc    ← 测试辅助,不需要
tensorflow/lite/micro/test_helper_custom_ops.cc
tensorflow/lite/micro/fake_micro_context.cc
tensorflow/lite/micro/mock_micro_graph.cc
tensorflow/lite/micro/recording_micro_allocator.cc
tensorflow/lite/micro/hexdump.cc

新增 include 路径(2 个)

脚本生成的 third_party 目录结构比手动复制多了一层:

路径 原因
third_party/flatbuffers/include flatbuffers 在 include/ 子目录下
third_party/ruy ruy 路径多了一层 ruy/

新增编译宏(4 个)

newlib-nano 缺少部分 C++ 标准数学函数:

TF_LITE_STATIC_MEMORY               # 禁用动态内存
TF_LITE_USE_GLOBAL_CMATH_FUNCTIONS  # 缺少 std::round / std::expm1
TF_LITE_USE_GLOBAL_MAX              # 缺少 std::fmax
TF_LITE_USE_GLOBAL_MIN              # 缺少 std::fmin

Makefile 用通配符自动发现文件

不再手动列举文件名,用 $(wildcard ...) 自动匹配:

TFLM_SRCS := $(filter-out \
    %/debug_log.cc %/micro_time.cc %/system_setup.cc \
    %/test_helpers.cc %/test_helper_custom_ops.cc \
    %/fake_micro_context.cc %/mock_micro_graph.cc \
    %/recording_micro_allocator.cc %/hexdump.cc, \
    $(wildcard tensorflow/lite/micro/*.cc \
               tensorflow/lite/micro/arena_allocator/*.cc \
               tensorflow/lite/micro/memory_planner/*.cc \
               tensorflow/lite/micro/kernels/*.cc \
               tensorflow/lite/micro/tflite_bridge/*.cc \
               tensorflow/lite/core/api/*.cc \
               tensorflow/lite/core/c/*.cc \
               tensorflow/lite/kernels/*.cc \
               tensorflow/lite/kernels/internal/*.cc \
               tensorflow/lite/kernels/internal/reference/*.cc \
               tensorflow/compiler/mlir/lite/core/api/*.cc \
               tensorflow/compiler/mlir/lite/schema/*.cc))

不用担心文件太多——链接器 --gc-sections 会自动丢弃未使用的代码段。编译了全部 120+ 个算子内核后,最终固件 text 段只有 72KB。

第三步:写三个平台抽象函数

TFLite Micro 把平台相关部分收敛到三个函数。这是移植中唯一需要手写的 C++ 代码

DebugLog

// tflite_port/debug_log.cc
#include "tensorflow/lite/micro/debug_log.h"
#include <cstdio>

extern "C" void DebugLog(const char* format, va_list args) {
  vfprintf(stderr, format, args);
}

extern "C" int DebugVsnprintf(char* buffer, size_t buf_size,
                               const char* format, va_list vlist) {
  return vsnprintf(buffer, buf_size, format, vlist);
}

TFLM 所有日志走 DebugLog()。printf 已被 CH58x HAL 层重定向到 UART(_write()CH58x_sys.c 中实现)。

InitializeTarget

// tflite_port/system_setup.cc
#include "tensorflow/lite/micro/system_setup.h"

namespace tflite {
void InitializeTarget() {
  // 时钟和 UART 已在 main() 中初始化
}
}

GetCurrentTimeTicks

// tflite_port/micro_time.cc
#include "tensorflow/lite/micro/micro_time.h"

extern "C" { extern uint32_t SystemCoreClock; }

namespace tflite {
uint32_t ticks_per_second() {
  return (SystemCoreClock / 1000) ? (SystemCoreClock / 1000) : 1;
}
uint32_t GetCurrentTimeTicks() {
  volatile uint32_t dummy = 0;
  return dummy;
}
}

不用引用 CH58x_common.h,用 extern "C" 声明 SystemCoreClock 即可,避免引入不必要 HAL 依赖。

第四步:用 MRS IDE 创建工程

  1. 打开 MRS IDE,创建一个 CH585M 空白工程
  2. 告诉 AI 代理工程路径
  3. AI 代理将 tensorflow/third_party/tflite_port/src/main.ccmodel_data.h 放入工程

MRS IDE 使用 Eclipse CDT 的 .cproject 管理编译选项。AI 代理需在 IDE 中配置以下项(或直接编辑 .cproject 文件):

C++ 编译选项

配置项
C++ 标准 gnu++17
异常 -fno-exceptions
RTTI -fno-rtti
静态对象线程安全 -fno-threadsafe-statics

预处理器宏

位置 原因
C++ TF_LITE_STATIC_MEMORY 禁用动态内存
C++ TF_LITE_USE_GLOBAL_CMATH_FUNCTIONS newlib-nano 缺少 std::round
C++ TF_LITE_USE_GLOBAL_MAX newlib-nano 缺少 std::fmax
C++ TF_LITE_USE_GLOBAL_MIN newlib-nano 缺少 std::fmin
C DEBUG=0 printf 输出到 UART0

DEBUG=0 决定 printf 走哪个串口。CH58x_sys.c 中的 _write()#if DEBUG == Debug_UART0 选择 UART0(PA14/PA15),这是板子上唯一引出的串口。

include 路径

${workspace_loc:/${ProjName}}
${workspace_loc:/${ProjName}/StdPeriphDriver/inc}
${workspace_loc:/${ProjName}/RVMSIS}
${workspace_loc:/${ProjName}/third_party}
${workspace_loc:/${ProjName}/third_party/gemmlowp}
${workspace_loc:/${ProjName}/third_party/flatbuffers/include}
${workspace_loc:/${ProjName}/third_party/ruy}

链接器

第三方依赖

三组外部库全部由 create_tflm_tree.py 自动生成,只需添加 include 路径,不编译任何源码:

用途 路径
FlatBuffers 解析 .tflite 模型文件 third_party/flatbuffers/include/
gemmlowp 定点运算模板 third_party/gemmlowp/
ruy profiling(ScopeLabel 空类) third_party/ruy/

ruy 的 instrumentation.h 自带 #ifdef RUY_PROFILER 分支,未定义该宏时回退到空类,开箱即用。

Demo 主程序

#include "CH58x_common.h"
#include "tensorflow/lite/micro/micro_interpreter.h"
#include "tensorflow/lite/micro/micro_mutable_op_resolver.h"
#include "tensorflow/lite/micro/system_setup.h"
#include "tensorflow/lite/schema/schema_generated.h"
#include "model_data.h"

namespace {

using OpResolver = tflite::MicroMutableOpResolver<3>;

TfLiteStatus RegisterOps(OpResolver& op_resolver) {
  TF_LITE_ENSURE_STATUS(op_resolver.AddFullyConnected());
  TF_LITE_ENSURE_STATUS(op_resolver.AddTanh());
  TF_LITE_ENSURE_STATUS(op_resolver.AddReshape());
  return kTfLiteOk;
}

constexpr int kTensorArenaSize = 8 * 1024;
static uint8_t tensor_arena[kTensorArenaSize] __attribute__((aligned(16)));

void RunInference() {
  const tflite::Model* model = ::tflite::GetModel(g_model_data);
  if (model->version() != TFLITE_SCHEMA_VERSION) {
    printf("Bad model version\r\n");
    return;
  }

  OpResolver op_resolver;
  if (RegisterOps(op_resolver) != kTfLiteOk) {
    printf("Op register fail\r\n");
    return;
  }

  tflite::MicroInterpreter interpreter(model, op_resolver,
                                       tensor_arena, kTensorArenaSize);
  if (interpreter.AllocateTensors() != kTfLiteOk) {
    printf("Alloc fail\r\n");
    return;
  }

  TfLiteTensor* input  = interpreter.input(0);
  TfLiteTensor* output = interpreter.output(0);

  printf("\r\n=== TFLite sin demo ===\r\n");

  float test_x[] = {0.0f, 0.5f, 1.0f, 1.5f, 2.0f, -1.0f, 3.14f};
  for (int i = 0; i < 7; i++) {
    input->data.f[0] = test_x[i];
    if (interpreter.Invoke() != kTfLiteOk) {
      printf("Invoke fail\r\n");
      return;
    }
    float y = output->data.f[0];
    printf("x=%.2f -> y=%.6f\r\n", test_x[i], y);
  }

  printf("=== Done ===\r\n");
}

}  // namespace

extern "C" int main() {
  HSECFG_Capacitance(HSECap_18p);
  SetSysClock(SYSCLK_FREQ);

  GPIOA_SetBits(GPIO_Pin_14);
  GPIOPinRemap(ENABLE, RB_PIN_UART0);
  GPIOA_ModeCfg(GPIO_Pin_15, GPIO_ModeIN_PU);
  GPIOA_ModeCfg(GPIO_Pin_14, GPIO_ModeOut_PP_5mA);
  UART0_DefInit();
  UART0_BaudRateCfg(115200);

  DelayMs(1000);
  printf("CH585M TFLite boot\r\n");

  tflite::InitializeTarget();
  RunInference();

  while (1) {}
}

要点:

编译结果

   text	   data	    bss	    dec	    hex
  71916	    844	   8240	  81000	  13c68
区域 大小 使用率
Flash (448KB) ~72KB 16%
RAM (128KB) ~9KB 7%

120+ 个算子内核全部编译,--gc-sections 自动丢弃未用代码。8KB bss 段主要是 tensor_arena,实际峰值使用约 3KB。

踩坑记录

  1. flatbuffers/flatbuffers.h 找不到 — 脚本的 flatbuffers 多一层 include/ 子目录。加 -Ithird_party/flatbuffers/include
  2. ruy/profiler/instrumentation.h 找不到 — 脚本的 ruy 路径是 third_party/ruy/ruy/...。加 -Ithird_party/ruy
  3. 'fmax'/'fmin' is not a member of 'std' — newlib-nano 不提供这两个函数。加宏 TF_LITE_USE_GLOBAL_MAXTF_LITE_USE_GLOBAL_MIN
  4. 'round' is not a member of 'std' — 同理,加宏 TF_LITE_USE_GLOBAL_CMATH_FUNCTIONS
  5. 串口无输出_write() 通过 DEBUG 宏选择串口。DEBUG=0 = UART0,DEBUG=1 = UART1。
  6. 串口浮点数空白 — newlib-nano 默认禁用 %f。C 和 C++ 链接器都勾选 "Use float with nano printf"(-u _printf_float),增加约 12KB code size。
  7. FLASH_EEPROM_CMD undefined — CH58x HAL 依赖 WCH ISP 库,链接加 -lISP585

串口输出

串口输出截图

sin(0) ≈ 0, sin(1) ≈ 0.84, sin(3.14) ≈ 0,7 个测试点全部正确。

移植成本

TFLite Micro 的架构决定了移植工作量极小:

  1. 文件获取create_tflm_tree.py 一把生成,AI 代理通过 gh codespace 全自动完成
  2. 集成:删 9 个冲突文件,加 2 个 include 路径、4 个编译宏、-u _printf_float
  3. 手写代码:只有 3 个平台抽象函数(共约 30 行)+ main.cc
  4. 算子:不需要手动精简,--gc-sections 自动丢弃未用代码
  5. 内存模型:静态 arena + GreedyMemoryPlanner,零动态分配

换模型只需改 RegisterOps() 添加对应算子注册,其余不变。