手上有块 MIPS 板子想让它做关键词唤醒第一反应多半是TFLite Micro 那套东西不是给 Cortex-M 用的吗。能跑但你翻开tensorflow/lite/micro/tools/make/targets/目录会发现从 ARM 到 RISC-V 到 CEVA官方给了十几个 target 文件唯独没有 MIPS。这不是歧视是没人提 PR。所以要做的事很清楚自己补一份 target 描述文件然后让make这条命令把交叉编译器、TFLite Micro 的静态库、micro_speech 例程和量化模型串起来一次性吐出一个能在 MIPS 上执行的二进制。这篇内容我会按真实动手顺序讲先确认工具链、再拆解 TFLM 的 Make 体系、然后逐段写那个.inc文件、接着处理一条命令跑下来会遇到的报错最后聊语音识别链路在 MIPS 上的实际耗时和调优空间。适合手上有 MIPS32 平台、想做离线语音唤醒或者任何 TinyML 推理、又不想被官方没支持劝退的嵌入式开发看。1. 为什么要在 MIPS 上跑 TFLite Micro先分清能跑和跑得动1.1 目标场景一颗没有 NPU 的 MIPS 核要做关键词唤醒典型场景是这样的板子上是一颗主频 500MHz 到 1.2GHz 的 MIPS32 处理器可能是君正 XBurst 系列、龙芯 1 系列也可能是路由器方案里的 24Kc。它带几十兆 DDR跑精简的 Linux 或者干脆裸机有一个 I2S 或者模拟麦克风接口。你要实现的功能是一直听听到你好设备这类词就拉一个 GPIO 或者发一条消息。这种场景下选择面其实很窄。云端识别要联网功耗和隐私都不合适大模型推理框架在这类平台上根本装不下最后剩下的就是 TFLite Micro 加一个几十 KB 的量化模型。它的定位本来就是给内存紧张的设备做定点推理MIPS32 只要有个几十 KB 的可用 RAM 和基本的乘法指令就落在它的能力范围里。1.2 TFLite Micro 的官方支持地图里MIPS 是空白的TFLM 的官方构建方式经历过几次迁移早期是 Make后来主推 Bazel再后来又加了 CMake。Make 这条路现在被官方标注成 legacy但它的好处是依赖最少、改起来最直接——不需要装 Bazel、不需要处理一堆 WORKSPACE 里的网络依赖一个.inc文件就能描述一个新平台。它的平台适配机制很朴素Makefile里有一句include $(MAKEFILE_DIR)/targets/$(TARGET)_makefile.inc。你传TARGETmips_xburst它就去找mips_xburst_makefile.inc。这个文件里你要定义工具链前缀、编译选项、链接选项剩下的事由通用 Makefile 完成。所以给 MIPS 加支持本质上就是写一个文件而不是改框架。顺带说一句如果你的 TFLM 版本已经比较新Make 目录可能被标记为不再维护某些例程也搬到了别的构建系统里。我建议选一个 commit 之后不要频繁更新把这一套跑通比追新重要得多。1.3 动手前必须锁死的三个约束在敲命令之前有三件事必须先确定否则后面会反复返工。第一是端序。MIPS 有大端和小端两种形态而 TFLite 的模型文件是 flatbuffer里面的字段偏移是小端存的。如果你的工具链和目标板是大端模型加载阶段就会读出乱七八糟的数值推理结果是垃圾而且不报错。这个坑我踩过表现形式是模型跑完了但输出永远是同一类。第二是浮点 ABI。很多 MIPS 核心没有硬件浮点单元工具链默认可能是硬浮点链接到目标板上跑起来就是SIGILL。要么在编译时就统一用-msoft-float要么确保整条链路内核、库、应用用的浮点 ABI 完全一致。第三是可用内存。TFLM 需要一块连续的内存池arena和模型本身占的空间。模型 20KB、arena 2KB 只是推理的底线还要加上音频前端、双缓冲和栈。开工前先算一遍别等编好了才发现 RAM 不够。2. 工具链不是随便挑一个MIPS 交叉编译的前置确认2.1 三种工具链来源与各自的坑MIPS 交叉编译器大致有三个来源选择不同后面踩的坑也不同。来源典型前缀优点需要注意厂商 SDK 自带mips-linux-uclibc-gnu-等与芯片 ABI 完全匹配链接脚本和库都是现成的版本老可能只有 GCC 4.9 甚至更早C11 支持要确认发行版交叉包mips-linux-gnu-/mipsel-linux-gnu-安装方便版本新默认 ABI 可能和你的板子不一致端序也可能相反自己用 Buildroot 构建自定义完全可控libc 和 ABI 都按需裁剪构建耗时长第一次配错要重来我的建议是如果板子厂商给了 SDK优先用它的编译器先把流程跑通因为 ABI 一定是对的。等整条链路验证完了再考虑换成更新的工具链去拿更好的优化。反过来做你会同时面对编译不过和跑不起来两类问题排查成本翻倍。2.2 用三条命令验证工具链基本可用不要上来就编 TFLM先用一个最小程序验证工具链。# 1. 编译器能不能起来目标架构是不是你想要的 mips-linux-gnu-gcc -v 21 | tail -3 # 2. 编一个能打印的静态程序顺手验证浮点 ABI mips-linux-gnu-gcc -marchmips32r2 -msoft-float -static -o /tmp/hello /tmp/hello.c # 3. 看产物到底是什么货色 file /tmp/hello readelf -A /tmp/helloreadelf -A在 MIPS 上会打印出 MIPS ABI 相关的属性包括 ISA 版本、浮点 ABI、用到的 ASE 扩展。这一步非常关键如果它显示Hard float而你的板子没有 FPU那后面所有的链接都得加-msoft-float而且 libc 也得是软浮点版本混着用一定会出问题。file的输出会告诉你端序和动态/静态链接状态。想要省事就统一走静态链接-static加上去把 libc、libm、libstdc 全部打进去。体积会大几百 KB但在有 DDR 的 MIPS 上不算什么换来的是不用折腾目标板的库路径和版本冲突。2.3 端序、浮点 ABI、指令集用 readelf 反查而不是靠猜这里有个实用技巧拿到任何一份目标板上已有的可执行文件用readelf -h看端序用readelf -A看 ISA 和浮点 ABI你的编译选项就照着抄。readelf -h /path/to/onboard/binary | grep -E Data|Machine readelf -A /path/to/onboard/binary输出里的little endian或者big endian直接决定你要不要加-ELFP ABI: Soft float或者Hard float决定-msoft-float加不加ISA 那一行会告诉你目标是mips32r1还是mips32r2。这比翻数据手册快得多也比问同事靠谱。3. 拆开 TFLM 的 Make 体系一条命令背后发生了什么3.1 目录结构与 makefile 的包含顺序TFLM 独立仓库的根目录下构建相关的东西集中在tensorflow/lite/micro/tools/make/。这里有几个关键文件主Makefile、downloads/目录存放下载的第三方源码、targets/目录各平台描述文件。主Makefile的执行顺序大致是先定义一堆路径变量再根据TARGET包含对应的.inc文件然后收集源码列表核心库、算子、例程最后用规则去编译和链接。源码列表是分层收集的Makefile.inc分散在各个子目录里比如tensorflow/lite/micro/kernels/Makefile.inc、tensorflow/lite/micro/examples/micro_speech/Makefile.inc。主Makefile会把这些都 include 进来。理解这一点很重要你改的不是某个大文件而是通过定义变量去影响它的行为。所以写.inc文件时尽量只加变量和选项不要去改通用规则。3.2 third_party_downloads最容易跳过也最容易翻车的一步TFLM 依赖平铺特化库flatbuffers、Gemmlowp、ruy、kissfft 等第三方代码这些不在仓库里需要下载。make -f tensorflow/lite/micro/tools/make/Makefile third_party_downloads这一步必须做而且必须在联网环境下先做一次。它会拉起 Python 脚本去拉取指定版本的源码包放到downloads/目录。之后你就可以断网编译了因为源码已经在本地。这里有个常见的翻车点如果你在 CI 或者内网环境里第一次编译就没跑这一步make 会在解析阶段报一堆找不到某个头文件或者没有规则可生成某个目标。看起来像是你的.inc写错了实际上只是依赖没下载。我的做法是把third_party_downloads单独跑一次跑完把整个downloads/目录打包备份。后面换机器或者重装环境直接解压回去省掉一次网络折腾。3.3 flatc 这类宿主工具为什么不能交叉编译flatbuffers 会附带一个flatc它是编译期工具用来把.fbsschema 生成 C 头文件。这个工具必须用宿主机的编译器编不能交叉编译。TFLM 的 Makefile 在这一点上处理得比较清楚一般不需要你干预但有一个例外如果你的构建环境里有CC或CXX环境变量被全局设成了交叉编译器宿主工具也会被交叉掉然后你在执行阶段会看到cannot execute binary file: Exec format error。排查方法很直接找到构建目录里生成的flatc用file看一眼。如果是ELF 32-bit LSB, MIPS那说明它被交叉编译了把环境变量清掉重新来。unset CC CXX AR make -f tensorflow/lite/micro/tools/make/Makefile third_party_downloads3.4 一个 TARGET 文件需要接管哪些变量自定义 target 文件要负责的变量不多但每一个都关键。按重要性排一下工具链前缀和路径TARGET_TOOLCHAIN_PREFIX可选TARGET_TOOLCHAIN_ROOT。平台编译选项通常写进PLATFORM_FLAGS再追加到CCFLAGS和CXXFLAGS。链接选项LDFLAGS。架构标记TARGET_ARCH主要影响一些条件判断。特殊头文件路径TARGET_SPECIFIC_HEADER_PATH用来放平台相关的替代头文件。另外还有个OPTIMIZED_KERNEL_DIR它决定用哪套优化算子。ARM 平台会填cmsis_nn用 CMSIS-NN 的汇编和指令优化。MIPS 上没有对应的库这一项留空走参考实现。这一点要有心理预期参考实现是纯 C 写的能用但不算快后面的性能优化空间主要在这里。4. 手写 mips_xburst_makefile.inc逐段说明4.1 文件位置、命名规则与自动加载路径文件放在tensorflow/lite/micro/tools/make/targets/下命名规则是$(TARGET)_makefile.inc。你打算用TARGETmips_xburst文件名就写成mips_xburst_makefile.inc。有一点我要提醒这个文件名是你的私有资产升级 TFLM 的时候很容易被覆盖或者冲突。建议把它放进版本控制并且在自己的分支上维护。如果团队里多个人协作最好在 README 里写清楚这个文件的存在否则新同事拉代码会发现为什么这个目录多了一个文件。4.2 编译、链接、归档变量的分工下面是一份可以直接拿去改的骨架我按用途分组注释。# tensorflow/lite/micro/tools/make/targets/mips_xburst_makefile.inc # 架构标记影响部分条件编译 TARGET_ARCH : mips # 工具链前缀。若 PATH 里已经有交叉编译器root 可以留空 TARGET_TOOLCHAIN_PREFIX : mips-linux-gnu- # TARGET_TOOLCHAIN_ROOT : /opt/mips-toolchain/bin/ # 平台通用编译选项 PLATFORM_FLAGS : \ -marchmips32r2 \ -msoft-float \ -mno-branch-likely \ -ffunction-sections \ -fdata-sections \ -fno-common # 若工具链默认大端而目标是 little endian取消下一行注释 # PLATFORM_FLAGS -EL CCFLAGS $(PLATFORM_FLAGS) CXXFLAGS $(PLATFORM_FLAGS) # 链接静态链接回收无用段 LDFLAGS $(PLATFORM_FLAGS) \ -static \ -Wl,--gc-sections \ -Wl,--no-undefined # 没有 CMSIS-NN 这类加速库走参考算子 OPTIMIZED_KERNEL_DIR :TARGET_ARCH在部分 Makefile 分支里会被用来判断是否启用某些源码填mips是安全的。-Wl,--no-undefined是个好习惯它会让链接器在遇到未定义符号时直接报错而不是留下一个运行期才崩的二进制。-fno-common是为了避免 GCC 10 以后默认行为变化带来的重复符号问题加上更保险。4.3 关键编译选项的取舍-O3 / -msoft-float / -static / -mdspr2优化等级我一般先用-O3。TFLM 的通用COMMON_FLAGS里已经带了-O3你在平台选项里再指定一次也没冲突。要不要开-funroll-loops看你代码体积的容忍度它在卷积内层循环上确实有效但会把二进制撑大不少。-msoft-float是分水岭。板子有 FPU 就换成-mhard-float并且确保整条链路一致没有 FPU 就必须软浮点。这里有个容易忽略的点即使 TFLM 的推理部分主要是定点运算音频前端里仍然可能有浮点参与的环节软浮点会让这些地方明显变慢。后面第 6 节会具体说。-static我强烈建议加上理由前面说过。动态链接在目标板上要求 libc 版本完全匹配一旦不匹配报错信息通常是version GLIBC_2.xx not found排查起来很费时间。-mdspr2这个选项要单独说。如果芯片是 24KEc 或者带 DSP ASE 的核心开它能显著加速乘加密集型代码。但它有两个前提一是工具链得支持GCC 4.4 以上一般都有二是运行环境得能正确处理 DSP 状态。在 Linux 上跑的时候如果内核没有在上下文切换时保存 DSP 相关寄存器任务切换后会出现偶发性的结果错误这种 bug 极难定位。我的建议是先用通用指令集跑通并验证精度再决定要不要开 DSP 优化而且一定要在真实负载下压测一段时间。4.4 裸机才需要关心的链接脚本与入口如果你的目标板跑的是 Linux这一节可以跳过-static就够了。如果是裸机或者 RTOS 上没有 MMU 的环境就得指定链接脚本和入口。LDFLAGS -Wl,-T,boards/mips_xburst/link.ld LDFLAGS -Wl,-Map,build/mips_xburst.map LDFLAGS -nostartfiles链接脚本里要定义内存布局把.text放到代码段地址.data和.bss放到 RAM 地址。-nostartfiles之后你得自己提供_start通常是一小段汇编设置栈指针、清零.bss、调用main。这部分和 TFLM 本身无关但少了它链接会报找不到_start。裸机环境还有一件事malloc和new可能不可用。TFLM 本身用的是一个静态内存池不依赖堆所以只要你不自己调用堆分配就没问题。5. 一条命令跑通全流程以及它可能报的错5.1 完整命令拆解前面都准备好之后实际构建命令其实很短make -f tensorflow/lite/micro/tools/make/Makefile \ TARGETmips_xburst \ TARGET_TOOLCHAIN_ROOT/opt/mips-toolchain/bin/ \ micro_speech_test逐段解释一下-f指定 Makefile 路径因为 TFLM 不在仓库根目录放 MakefileTARGET触发包含我们写的.inc文件TARGET_TOOLCHAIN_ROOT给出编译器所在目录最后micro_speech_test是要构建的目标名。产物通常在tensorflow/lite/micro/tools/make/gen/mips_xburst_arch/bin/下。目录名里带了 target 和架构这样多个平台可以共存互不干扰。如果你还想在宿主机上先跑一遍验证逻辑TFLM 提供了对应的宿主机测试目标名字通常是在原目标前加test_或者直接在宿主机 TARGET 下构建。我一般先用宿主机目标确认例程本身没问题再切到交叉编译这样能把框架问题和平台问题分开。5.2 从 No targets specified and no makefile found 开始的排查链路这条报错信息出现频率非常高它几乎总是同一类原因你在错误的目录下敲了命令或者路径写错了。make: *** No targets specified and no makefile found. Stop.排查顺序我整理成一条链路照着走报错特征真实原因处理方式No targets specified and no makefile found当前目录没有 Makefile且-f路径不存在pwd确认在仓库根目录ls确认-f后面的文件真的存在No rule to make target xxx目标名拼错或者该例程不在当前版本里直接跑make -f Makefile help或看 Makefile 里定义的伪目标fatal error: flatbuffers/flatbuffers.h第三方依赖没下载先跑third_party_downloadsundefined reference tooperator new链接时没带 C 运行库加上-lstdc或调整链接顺序cannot find -lm / -lc工具链的 sysroot 路径不对检查TARGET_TOOLCHAIN_ROOT确认该目录下有libExec format error构建期宿主工具被交叉编译了清理环境变量CC/CXX删除构建目录重来region .text overflowed链接脚本里的段大小不够调整链接脚本或开启-ffunction-sections--gc-sections瘦身关于链接顺序补一句C 静态库的链接顺序是敏感的-lstdc要放在目标文件之后、-lm之前。如果你手动往LDFLAGS里加库顺序写反了一样报未定义符号而且报的符号看起来完全无关。这个坑我调过两个小时。5.3 产物检查file、readelf、qemu 试跑编完之后先别急着往板子上搬三步验证。file gen/mips_xburst_*/bin/micro_speech_test readelf -A gen/mips_xburst_*/bin/micro_speech_test | head -20 readelf -d gen/mips_xburst_*/bin/micro_speech_test第一条确认架构和端序。第二条确认 ISA 和浮点 ABI 和你预期一致。第三条如果显示There is no dynamic section in this file说明静态链接成功如果打印出一堆NEEDED那-static没生效回头检查LDFLAGS有没有被后面的赋值覆盖掉。最后是试跑。如果宿主机上装了 QEMU 的用户态模拟qemu-mipsel-static ./gen/mips_xburst_*/bin/micro_speech_test能跑出测试通过的输出说明指令集、ABI 和逻辑都没问题可以上板了。QEMU 跑不了的原因大多是端序不对用qemu-mips而不是qemu-mipsel或者需要-L指定 sysroot。6. 语音识别链路的性能账前端和推理谁更贵6.1 把耗时拆成三段分别计时micro_speech 例程的流水线大致是PCM 采样进来先做特征提取分帧、加窗、FFT、滤波器组、对数压缩再把频谱图喂给模型做推理最后做后处理判断是不是关键词。想优化第一步是知道时间花在哪。我的做法是在三段之间插桩用clock_gettime(CLOCK_MONOTONIC, ...)取时间戳跑几百次取平均。别用clock()它统计的是 CPU 时间在多任务环境下容易被调度干扰。我遇到过的分布是特征提取和推理各占一半上下具体比例取决于主频和是否开了 DSP 优化。这个结论和很多人的直觉相反——大家以为推理是大头实际上前端的常数不小因为它是逐样本流的处理FFT 和滤波器组都是 O(N) 以上的运算。6.2 arena 大小与模型输入尺寸的匹配TFLM 的内存池大小是编译期常量micro_speech 例程里一般写的是 2KB 或者更大。这个值必须大于等于模型所有张量加上中间缓冲的最大峰值需求。判断方法很直接把 arena 调小跑到某一步会返回kTfLiteError说明内存不够。想要拿到准确的数字可以在初始化后用interpreter.arena_used_bytes()打出来。再说输入尺寸。micro_speech 用的是 16kHz 单声道 PCM30ms 窗、20ms 步长的分帧特征图是 40×40 的定点频谱老版本是 43×49看 checkout 的版本。这个尺寸已经很小了但在软浮点的 MIPS 上每帧还是要算一次 FFT。如果主频实在低可以考虑降采样或者减少滤波器组通道数代价是识别率下降。6.3 定点化与热点算子替换TFLM 的参考算子是大而全的模板实现为了通用性牺牲了不少性能。想提速有几个层次最省事的是编译器层面-O3加-funroll-loops能拿到 10% 到 20%。再往上一层是把全连接和卷积里最内层的乘加循环换成你自己写的定点版本用int32累加配合位移做重定标。这一层要小心饱和和舍入改完必须和参考实现做逐层对比看最大误差有没有超过可接受范围。最后一层是 DSP 指令。如果芯片支持用内联汇编写几个乘加累加效果最明显。但前面说过DSP 状态的处理要确认清楚。6.4 一组实测数据与调优前后对照下面这组数字来自一块 1GHz 左右的 MIPS32 平台软浮点DDR 带宽一般。数值是量级参考你的板子必须自己测。阶段优化前-O3后热点算子替换后特征提取每 20ms 音频约 6 ms约 4 ms约 3 ms模型推理每帧约 7 ms约 4.5 ms约 2.5 ms单帧总耗时约 13 ms约 8.5 ms约 5.5 ms峰值内存约 60 KB约 60 KB约 62 KB这张表想说的是单帧必须处理完 20ms 的音频所以总耗时只要低于 20ms 就能实时。优化前已经勉强够用优化后有两倍多的余量可以用来做更大一点的模型或者跑更复杂的后处理。值得一提的是优化后内存略涨是因为我换进去的算子用了额外的临时缓冲。这就是典型的空间换时间嵌入式上这种取舍很常见。7. 从 demo 到能用的固件7.1 换成自己的关键词模型例程自带的模型只认几个固定的词真要落地得换成自己的。流程是收集几百到几千条自己关键词的音频用 Python 侧的工具做特征提取和训练导出成 TFLite再量化成 int8最后用xxd -i或者类似的工具转成 C 数组参与编译。这里有两个坑。一是量化时的代表数据集必须贴近实际采集条件如果训练用的是安静环境的录音现场有风扇噪声识别率会掉得很厉害。二是输入张量的形状和归一化方式必须和固件里前端输出的特征完全一致哪怕差一个尺寸也会在解释器初始化时报错。另外记得把模型改成自己数组名之后同步改固件里引用的符号名。这个错误编译器会拦住你属于友好的一类。7.2 麦克风数据流与缓冲设计真实产品里音频流是连续的而推理是分帧的。常见做法是双缓冲中断或者 DMA 往缓冲 A 写处理线程从缓冲 B 读两边交换指针。这样避免了在中断里做重活也避免了数据竞争。缓冲的大小要按帧长和步长来定。以 16kHz、30ms 窗为例一帧是 480 个采样点。如果步长 20ms相邻两帧有 160 个点是重叠的处理时要保留前一次的尾部数据。这个细节很多人在第一次实现时都会漏掉表现是识别率明显低于 PC 上的测试结果。7.3 上线前值得再过一遍的几条最后一节说几个我实际踩过的点。第一先在板子上跑长稳测试至少连续跑几小时看有没有内存泄漏或者偶发的识别异常。定点推理不会泄露内存但你的音频缓冲逻辑可能会。第二留意温度和主频的关系。很多 MIPS 芯片在温度升高后会降频如果你的单帧耗时本来就在 20ms 边缘降频之后就会开始丢帧。留足余量比事后调参省事。第三把前端和推理的时间戳日志保留在固件里通过串口或者日志接口输出。上线之后如果客户反馈有时候不灵这些数据是唯一能帮你定位的线索。我个人在几个 MIPS 项目里最深的体会是交叉编译这件事难点从来不在写那个.inc文件而在前面确认端序和浮点 ABI 的那半小时。这半小时省下来后面可能要多花两天。工具链检查、readelf -A、QEMU 试跑这三步看着麻烦实际上是最划算的投入。至于 Make 被官方标为 legacy 这件事我的态度是它是能被完全掌控的构建方式只要你的项目不追新这条命令能稳定用很多年。
