
做了快十年嵌入式看过太多工程师把 AI 和 MCU 分成两个世界AI 是云端 GPU 的专属玩具MCU 最多做做数据采集。但这个认知在 TinyML 出现之后就该被推翻了。我在 AT32F435 这颗国产 Cortex-M4F 芯片上把 TensorFlow Lite for Microcontrollers 的 Helloworld 正弦波模型完整跑通了一次从环境搭建、模型移植到最终的 LED 呼吸灯效果都做了实测验证。整个过程走下来我的感受是真正的 AI 落地不能只看服务器端的浮点算力把推理拉到几十毫瓦功耗的 MCU 上跑才是边缘智能最有价值的地方。这篇文章把我从零到一的过程、踩过的坑和最终的实测数据全部复盘一遍给那些想在国产 MCU 上试水 TinyML 的工程师们一个可直接参考的路径。1. 选型逻辑为什么我拿AT32F435这颗国产MCU来跑TinyML1.1 先拆解TinyML对MCU硬件的底线要求很多同学一听『MCU跑AI』第一反应是得用什么高端芯片。实际上 TinyML 这个方向之所以能火恰恰是因为它对硬件的要求低到了一个非常亲民的位置。以 TensorFlow Lite for Microcontrollers简称 TFLM官方给出的 Helloworld 为例模型本身是一个 1-16-1 的三层全连接网络输入一个值、中间 16 个神经元、输出一个值。量化成 int8 之后整个模型体积也就 3KB 左右TensorArena 内存缓冲区给到 2KB 以上就足够跑推理。这背后的需求逻辑其实非常清晰模型足够小所以 Flash 的占用不是瓶颈推理过程没有卷积主要是矩阵乘法和 ReLU 激活函数所以内核需要有 FPU 或者至少能高效处理定点的乘加操作中间层的临时数据很少所以 SRAM 只需要几 KB 级别。任何一颗带硬件浮点单元的 Cortex-M4/M33/M7 内核芯片理论上都能轻松胜任。1.2 AT32F435的硬参数到底够不够打AT32F435 是雅特力推出的一款基于 ARM Cortex-M4F 内核的高性能 MCU。我手上这颗是 AT32F435ZMT7主频可以直接拉到 288MHzFlash 512KBSRAM 最大 256KB我没用到这么大但跑 TinyML 绰绰有余。Cortex-M4F 这个内核本身就带 FPU 浮点单元和 DSP 指令集对矩阵乘法和信号处理这类负载有天然的加速优势。单从跑 TinyML 的角度看这颗芯片的算力冗余非常大。我用 Helloworld 这个例子做评测推理一次的主循环耗时大概在几百微秒级别288MHz 的主频加上 FPU 的加持全连接层的一轮矩阵乘法根本来不及形成性能压力。1.3 和其他常见选型的对比我也不是没想过直接用 STM32F407 这类经典板子或者用 ESP32 带 WiFi 的方案。但最终选择了 AT32F435有几个很实在的原因。我用下面这个表格直接对比一下芯片型号内核主频Flash/SRAMTinyML适配度额外考虑STM32F407Cortex-M4F168MHz1MB/192KB适合资料多价格偏高传统方案ESP32Xtensa LX6240MHz4MB/520KB适合有TFLM官方支持无线功能在这个场景用不上AT32F435Cortex-M4F288MHz512KB/256KB非常适合国产化场景、性价比高RP2040Cortex-M0133MHz无Flash/264KB勉强无FPU只适合极简模型选 AT32F435 的逻辑其实不复杂主频在同等定位芯片里属于第一梯队Flash/SRAM 大得完全不担心模型放不下Cortex-M4F 的内核对 TFLM 的算子底层实现有硬件加速加持再加上国产化替代的大趋势下这颗芯片的供货和价格都非常友好。做产品落地的工程师应该都懂我这个考虑。2. 正弦波模型拆解TinyML的Hello World到底在做什么2.1 模型任务定义用神经网络去拟合一个数学函数TFLM 官方把正弦波预测这个例子定义为嵌入式 AI 的 Helloworld思路非常直接给模型输入一个 0 到 2π 之间的相位值 x模型输出 y sin(x) 的近似值。看起来有点大材小用——一个数学函数而已直接调用数学库的 sin() 不就行了但这里是为了验证 AI 在 MCU 上的完整工作链路数据输入、模型加载、算子执行、结果输出以及如何把一个神经网络真正部署到资源受限的嵌入式环境。用正弦波做例子是因为它的输出是一个连续的非线性曲线神经网络需要学习这种非线性映射关系任务简单到可以肉眼验证同时又完整覆盖了训练、量化、部署的全流程。2.2 从Keras训练到int8量化官方模型的训练过程其实是在 Linux 环境下用 Keras 完成的。训练数据结构很简单在 -π 到 π 或 0 到 2π 的区间内采样若干个 x 值对应的标签 y sin(x)其中部分样本加了噪声防止模型过拟合。网络结构就是经典的三层全连接第一层 1 个输入节点隐藏层 16 个神经元用 ReLU 激活输出层 1 个线性节点。训练完成后关键步骤是量化。TFLM 在 MCU 上默认跑的是 int8 量化模型因为浮点模型在部分低端 MCU 上是跑不动的即便有 FPU单纯从功耗和内存角度考虑int8 也是最优解。量化的过程用 TensorFlow Lite Converter 做 post-training quantization把权重从 float32 压缩到 int8同时记录每个张量的 scale 和 zero_point 两个参数后续推理时要用这两个参数反量化得到最终的浮点结果。量化之后的模型文件大概只有 3KB一个 C 数组就能装下这也是 MCU 能轻松承载的根本原因。2.3 模型文件最终长什么样把 Keras 模型转成 TensorFlow Lite 格式后再用 xxd 或者 TFLM 的工具把它转成一个 C 源文件里面就是一个 int8 的字节数组。Helloworld 例程中这个数组被命名为 g_model 或者 model_data我在做移植的时候直接在官方仓库里提取了这份模型数组放到自己的 AT32 工程里。我建议第一次做移植的同学不要自己去从头练一个模型直接用官方已经量化好的模型文件跑通链路然后再去折腾自训练。用官方模型可以排除掉训练环节的变量让排错范围集中在部署和推理部分。3. 工程搭建与TFLM源码移植一半时间花在环境上3.1 TFLM源码的正确获取方式TFLM 现在的代码托管在 tensorflow/tflite-micro 仓库下早期是 TensorFlow 主仓库的 tensorflow/lite/micro 子目录。我建议直接用 GitHub 上的独立仓库版本迭代很快而且官方配套了很好的示例。拿到源码之后不要急着往工程里拖先确认一下目录结构。TFLM 的源码是平铺式的tensorflow/lite/micro 下包含所有核心推理引擎代码examples/hello_world 下则是官方示例包含 main.cc、hello_world_model_data.cc 这些核心文件。移植的时候主要把 micro 目录下的核心代码、模型数组文件、以及你用的板子对应的 debug_log 实现抠出来就行。3.2 在AT32工程里搭建TFLM运行骨架我用的是 Keil MDK 工程来跑 AT32F435 的 BSP 基础工程。先把 AT32F43x 标准外设库工程建好然后在工程里新建一个 TFLM 分组把 TFLM 源码中需要编译的 C/C 文件全部加进去。这一步最大的坑是文件依赖。TFLM 源码内部各文件之间互相 include 很频繁如果你只挑一部分文件编译很快会遇到一堆找不到头文件的报错。我个人的做法是直接把整个 tensorflow/lite 目录加进 include path编译时让编译器自动处理依赖关系配合 Keil 的 One ELF Section per Function 选项链接器会自动把没用到的函数剔除掉这样既能保证编译通过又能控制最终固件大小。3.3 打开FPU和优化级别否则推理速度会慢得怀疑人生TFLM 的矩阵乘和量化算子对编译器优化极度敏感。我在做 Helloworld 实测时做过对比立创的 ARM GCC 默认 O0 优化下一次推理耗时接近 2ms但把优化级别开到 O2 之后瞬间降到几百微秒。原因很简单矩阵乘的核心循环在没有优化的情况下会频繁产生内存访问和冗余计算O2 以上的优化会让编译器自动利用 Cortex-M4F 的 FPU 寄存器进行向量化计算。另外一定记得在工程里打开硬件浮点选项。GCC 的编译选项中要加-mcpucortex-m4 -mfpufpv4-sp-d16 -mfloat-abihardKeil 里则在 Target 选项里把 Floating Point Hardware 选成 Single Precision。这个选项没弄对浮点运行库会回退到软件模拟性能直接下降一个数量级。4. 实际点亮呼吸灯推理结果如何变成LED亮度4.1 核心推理代码的调用逻辑TFLM 在 MCU 上的推理接口非常简洁核心就是四个步骤加载模型、创建解释器、设置输入、执行推理。官方 Helloworld 的代码结构大致如下#include tensorflow/lite/micro/micro_interpreter.h #include tensorflow/lite/micro/micro_mutable_op_resolver.h // 模型数据 const unsigned char* model_data g_model; const tflite::Model* model tflite::GetModel(model_data); // 算子解析器注册模型用到的算子 static tflite::MicroMutableOpResolver10 resolver; resolver.AddFullyConnected(); resolver.AddQuantize(); resolver.AddDequantize(); // 张量内存缓冲 static uint8_t tensor_arena[2 * 1024]; // 创建解释器 tflite::MicroInterpreter interpreter(model, resolver, tensor_arena, sizeof(tensor_arena)); // 获取输入输出张量 TfLiteTensor* input interpreter.input(0); TfLiteTensor* output interpreter.output(0); // 推理循环 input-data.float32[0] x; interpreter.Invoke(); float y output-data.float32[0];需要注意的一点是当前的模型文件如果是从官方仓库直接拿的量化模型输入输出张量类型是 int8需要通过 input-params.scale 和 zero_point 做量化再喂进去我一开始忽略了这一步导致输出全是乱码。4.2 把正弦波预测变成呼吸灯的实际映射跑模型只是第一步把预测结果变成一个肉眼可见、可交互验证的输出才算真正跑通了 Helloworld。我的做法是主循环里不断递增 x 的值每次增加 0.1 弧度把它喂给模型得到一个 y 值范围在 -1 到 1 之间再把 y 映射为 PWM 占空比控制一个 LED 的亮度。由于 x 是线性递增的模型预测的 y 就呈现出正弦波形态LED 的亮度也同步呈现呼吸灯的起伏效果。相位步进的大小直接影响呼吸灯的频率我用 0.1 弧度每步完整周期大概 63 步主频 288MHz 跑一遍主循环完全在几十毫秒以内看起来非常流畅。4.3 串口输出验证模型预测值和理论值能对得上吗光看呼吸灯亮暗只能说明『有反应』不能证明『预测准』。我同时通过串口把模型输入 x、预测输出 y、理论 sin(x) 三组数据打印出来方便做定量对比。实测下来在 -1 到 1 的输出区间内模型预测值和理论值的误差基本控制在 0.05 以内波形轮廓和真实正弦曲线高度一致。这个精度对于 Helloworld 级别的 16 神经元隐藏层网络来说完全符合预期。等到后面换成更大规模模型或者换用 LSTM/Transformer 这类复杂结构才需要重新评估精度表现。5. 实测数据与踩坑排查预测误差、耗时和那些反直觉的bug5.1 实测数据记录下面是我在 AT32F435 上跑 Helloworld 模型记录的几组数据输入 x 从 0 到 π 之间取几个点做对照输入x (弧度)理论sin(x)模型预测值绝对误差000.010.010.50.4790.470.0091.00.8410.820.0211.571.0000.950.052.00.9090.870.0392.50.5980.610.0123.140.0010.050.05误差最大出现在峰值附近这是全连接网络逼近非线性曲线的正常表现如果要更高的精度可以加宽隐藏层或者增加训练轮次。就 Helloworld 这个示例任务而言这个精度已经足够验证整条链路。推理耗时的实测我通过 GPIO 翻转配合示波器测量单次推理在 288MHz 下约为 0.35ms比在 STM32F407 上的实测值约 0.8ms快了一倍左右这主要归功于主频优势。5.2 坑一模型输出恒为0或乱码问题出在量化的反向操作第一次跑起来的时候串口打印的预测值全是 0 或者明显乱码。排查了很久才发现我用的模型数组是官方仓库里的 int8 量化模型但我在代码里却按照 float 的方式来读取输入输出。int8 模型输入输出的 TfLiteTensor-data.data 是一个 int8 指针而且存储的值是量化后的整数必须通过 params.scale 和 params.zero_point 做转换才能还原成真实的浮点正弦值。float real_val (output-data.int8[0] - output-params.zero_point) * output-params.scale;这个坑几乎是每个初用 TFLM 的人都会踩一次。解决办法就是先打印一下 input 和 output 张量的 type 和 params 值确认数据类型后再决定用哪种方式读写。5.3 坑二编译通过但上电直接卡死TensorArena内存对齐第二个比较隐蔽的问题是 TensorArena 的内存对齐。TFLM 的内部算子对张量地址的对齐要求很高通常要求 4 字节甚至 16 字节对齐。如果你在 Keil 工程里直接定义一个普通全局数组作为 tensor_arena它的对齐方式取决于编译器的默认设置一旦对齐不对Invoke 的时候会触发 HardFault表现就是上电后程序直接卡死。解决办法有两种一是用 C11 的alignas(16)指定字节对齐属性二是在 Keil 的启动文件或链接脚本里使用__attribute__((aligned(16)))。我最终用的是__attribute__((aligned(16))) static uint8_t tensor_arena[2 * 1024];改完之后HardFault 的问题立即消失。5.4 坑三Flash不够用的裁减思路虽然 Helloworld 模型本身只有 3KB但 TFLM 运行时库编译出来之后接近 40KB如果再加上 AT32 的外设库和协议栈Flash 压力就开始出现了。这也是很多人在 MCU 上跑 TinyML 会遇到的共性问题。裁减思路主要围绕算子注册表来想MicroMutableOpResolver 默认可以注册的算子上限是根据模板参数决定的如果你只用了 FullyConnected, 那尽可能把句柄数调到实际用到的数量给编译器的优化去掉多余的算子。再配合 Keil 的--split_sections选项、使用优化等级 O2 以上的方式基本能把 TFLM 运行时裁剪到 20KB 左右。对于 512KB Flash 的 AT32F435 来说这完全不是瓶颈了。5.5 那些反直觉的性能认知我实测之后对 MCU 跑 AI 这件事有了几个新的认识。第一量化模型在带 FPU 的 MCU 上跑速度反而可能比纯浮点模型慢因为 int8 的乘加需要额外的 scale 计算。所以如果你的 MCU 内存充裕、又有硬件 FPU直接用 float 模型反而可以简化代码、减少踩坑。第二TinyML 的性能瓶颈往往不在算子本身而在于内存访问效率。TensorArena 的内存布局、D-Cache 的命中率对耗时的影响比微调模型结构更明显。第三MCU 的真正价值在超低功耗AT32F435 正常跑推理时的功耗远低于任何云端的单位成本这才是 TinyML 的终极竞争力。收尾再聊几句心里话实际上把 TFLM 的 Helloworld 跑通只是 TinyML 知识体系里最小的一块拼图。我在 AT32F435 上走这一圈下来最大的收获倒不是测出了多好看的推理耗时而是对一个事实有了更实感的确认国产 MCU 在 AI 这条路线上一点没落下无论是主频、内存、Flash 资源的硬件底子还是从标准库到 IDE 的支持都已经到了可以支撑量产级 TinyML 应用的程度。后面如果继续往下走我觉得可以先从两个方向深入一是换成能处理二维数据的 CNN 模型做图像识别体验一下卷积算子的部署难度二是研究一下 TFLM 自带的内存规划工具看看怎么把 TensorArena 压榨到最小。真心建议手头有国产 Cortex-M 开发板的同学把这个 Helloworld 例程翻出来跑一遍。实际动手之后你会发现芯片原厂和开源社区已经把最难的路都铺好了你只需要把第一块砖放上去。