尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

国产MCU跑TinyML:AT32F435正弦波模型部署全流程指南

国产MCU跑TinyML:AT32F435正弦波模型部署全流程指南 最近把雅特力的 AT32F435 开发板翻出来跑了 TinyML 的 HelloWorld 正弦波模型例程。这个例程在各类 Cortex-M 板子上跑的人不少但在国产 MCU 上完整跑通的记录不算多所以我干脆把整个流程、代码改动和踩坑点都留一份。如果你手头正好有 AT32F435 或者类似的国产 MCU又想试试 TinyML 到底是怎么在单片机上跑神经网络的这篇文章可以直接照着抄。我先把结论放在前面整套流程走下来比想象中顺AT32F435 的 288MHz 主频和片上 SRAM 对 TinyML 来说非常够用跑这个正弦波回归模型基本就是“秒出结果”。中间最折腾的不是 MCU 本身而是 TensorFlow Lite for MicrocontrollersTFLM这套工具链和工程集成方式。下面我会把模型获取、转换、代码实现、实测数据和常见问题全部拆开讲。1. 项目定位与整体思路拆解1.1 TinyML 的 HelloWorld 到底在做什么很多人第一次接触 TinyML都是从官方这个正弦波模型开始的。它本质上是个回归任务给模型一个数值输入比如 0 到 2π 之间的某个相位模型输出对应的 sin 值。神经网络本身很小只有一层全连接加一些非线性激活参数量在几百个左右量化之后整个模型只有几 KB刚好适合在 MCU 的 Flash 和 RAM 上跑。这个例程为什么叫 HelloWorld因为它把 TinyML 的核心链路都串起来了先用 Python/Keras 训练一个模型再通过 TensorFlow Lite Converter 转换成 .tflite 格式最后用 xxd 或者官方脚本把模型变成 C 语言数组塞进 MCU 工程里。MCU 端做的事情很简单——准备输入、调用解释器推理、读出输出、把结果打印到串口。在 PC 上训练一个 sin 函数模型听起来很“杀鸡用牛刀”但这个例程的真正价值在于验证整条工具链是否通畅。模型再小它走的路和跑视觉模型、语音唤醒模型是完全一样的。所以你在 AT32F435 上把这个例程跑通了等于已经把 TinyML 在国产 MCU 上的部署环境全部打通了后面换更大的模型只是改改 buffer 和算子依赖的问题。1.2 为什么选 AT32F435 这颗国产 MCUAT32F435 是雅特力推出的高性能 Cortex-M4F 内核 MCU主频最高能到 288MHz片上 Flash 从 256KB 到 512KB 可选SRAM 最大可以到 512KB。这个配置在国产 MCU 里属于第一梯队放 TinyML 这个场景里甚至有点“性能过剩”。我选它的原因有三个。第一是主频TinyML 推理非常吃主频288MHz 比常见的 72MHz 级 MCU 快好几倍跑同一个模型体感差异很明显第二是 SRAM 大TFLM 的推理过程需要在 RAM 里分配 tensor arena模型稍微复杂一点就要十几 KB 甚至几十 KBAT32F435 的大 SRAM 让这块非常从容第三是生态雅特力官方提供了完善的固件库、Arduino 支持和 AT32 Work Bench 图形化配置工具集成第三方库时不需要自己从零搭工程。对比同级别的 STM32F4 系列AT32F435 在频率和 SRAM 上都有优势而且价格更友好。对国产 MCU 有采购需求或者单纯想“用更少的钱跑相同模型”的开发者来说这确实是个值得认真评估的平台。当然选择它也有需要适应的差异点比如外设寄存器和 STM32 不是完全兼容库函数风格也不同移植时需要稍微调整一下。1.3 整个项目的工作流拆解从零到跑通我分成四个阶段。第一阶段是准备工具链包括安装开发环境、TFLM 源码和 Python 侧依赖第二阶段是准备模型有两种方式一种是直接用官方预训练好的正弦波模型 C 文件另一种是自己训练并转换成 C 数组后者的好处是能完整理解模型怎么来的第三阶段是写 MCU 端代码把模型数组、TFLM 运行时和串口输出拼起来第四阶段是编译烧录、看串口数据、对比预测值和真实 sin 值的误差。这个流程每阶段都有容易踩的坑。比如工具链版本不匹配会导致模型解析失败串口输出没重定向会导致什么都看不到tensor arena 太小会直接崩溃。后面我会把每个阶段的关键细节都交代清楚。2. 软硬件准备与开发环境搭建2.1 需要准备的硬件清单与板型说明硬件方面其实很简单核心就是一整块 AT32F435 开发板。我用的是雅特力官方 AT-START-F435 板卡上面板载了 AT32F435 主控、一颗 LED、一个用户按键和 USB 转串口电路调试和供电都靠板载的 USB 口完成不需要额外买调试器。如果你手里的是其他型号的 AT32F435 板子或者干脆是自己画的板子也没关系只要满足几个条件就行主控是 AT32F435 系列、板子有串口引出PA9/PA10 或者你自定义的 USART 引脚、备用一个 USB-TTL 工具用于连接串口。板载 DAP-Link 的版本其实更方便插上 USB 就能同时完成供电、调试和串口通信。关于引脚官方 AT-START-F435 板卡的 USART1 一般默认接到 PA9TX和 PA10RX波特率习惯用 115200。不同板子可能不一样建议先看原理图确认串口引脚否则后面数据出不来容易怀疑人生。2.2 开发环境选择Arduino 还是 Keil MDKTFLM 官方例程在 GitHub 上有一份 Arduino 版本理论上可以在 Arduino IDE 里直接编译。但实际用到国产 MCU 上时Arduino 生态就需要额外安装雅特力的第三方板卡支持包。这个流程不复杂但依赖 Arduino IDE 的板卡管理器网络状况很多时候会卡在下载环节。我最终选择的是 Keil MDK 路线。原因很实际Keil 是嵌入式工程师最熟悉的工具雅特力官方提供的 AT32 Firmware Library 和例程也都是基于 Keil 的把 TFLM 源码文件夹拖进工程直接添加编译就行网络依赖少可控性强。TFLM 本身是纯 C 源码Keil 的 AC6 编译器对 C 11 支持得很好编译 Traditional 例程没有任何问题。Arduino 路线也不是不行适合想快速验证的人Keil 路线更适合后续要持续开发、要接传感器数据做真实推理的人。两条路线的本质差别只在于怎么组织工程和编译选项模型逻辑完全一样。2.3 创建工程、配置时钟与外设我创建工程的方式是直接复制雅特力官方例程里的一个基础模板这样做的好处是启动文件、链接脚本和系统时钟配置都是现成的。AT32F435 的系统时钟默认配置到 288MHz需要在 system_at32f435_437.c 里确认一下宏定义是否启用了最高主频。然后是外设配置这个项目其实只用到三个东西时钟、串口、FPU。串口用 USART1配置成 115200-8-N-1注意开启串口发送中断或者直接使用阻塞发送都行反正是调试输出不追求速度。FPU 需要在启动文件里确认已启用如果用的是官方模板一般默认就是开的。这里有一个很容易被忽视的点TFLM 推理代码在编译时如果开启了 -O2 或 -O3 优化很多本来能工作的代码可能会因为未定义行为出问题但如果不开优化推理速度会慢不少。AT32F435 的主频足够高我直接用了 -O2稳定性没有问题。后面如果跑更大的模型再考虑逐层优化。3. 模型获取、转换并与固件集成3.1 理解正弦波模型的数据生成与训练如果你只想“跑通”直接下载官方 sine_model_data.cc 就行。但如果你想知道这个文件是怎么来的这里有必要说一下。正弦波模型的训练数据非常简单在 0 到 2π 之间均匀取若干个点作为 x对应的 y sin(x) 作为标签。用一个极小的全连接网络去拟合这个映射关系。训练几百轮之后模型的输出和真实 sin 值误差会非常小一般来说小于 0.05 就够用了。我在 Python 侧重新训练了一份主要是为了验证量化流程。下面是核心训练代码非常短import numpy as np import tensorflow as tf # 生成训练数据 x np.linspace(0, 2 * np.pi, 1000, dtypenp.float32) y np.sin(x).astype(np.float32) # 构造一个很浅的网络 model tf.keras.Sequential([ tf.keras.layers.Dense(16, activationrelu, input_shape(1,)), tf.keras.layers.Dense(16, activationrelu), tf.keras.layers.Dense(1) ]) model.compile(optimizeradam, lossmse) model.fit(x, y, epochs300, verbose0) model.save(sine_model.h5)训练完成后的 .h5 文件不能直接烧进 MCU需要先转换成 TensorFlow Lite 格式。转换时可以做量化把参数从 float32 压到 int8。对于 MCU 来说量化几乎是必须的否则 Flash 占用和计算量都会明显增大。3.2 模型量化转换从 .h5 到 .tflite 再到 C 数组转换这一步是 TinyML 工具链里最容易出问题的地方。我用的转换脚本如下import tensorflow as tf model tf.keras.models.load_model(sine_model.h5) converter tf.lite.TFLiteConverter.from_keras_model(model) converter.optimizations [tf.lite.Optimize.DEFAULT] converter.representative_dataset representative_dataset # 需要定义一个校准数据集 converter.target_spec.supported_ops [tf.lite.OpsSet.TFLITE_BUILTINS_INT8] converter.inference_input_type tf.int8 converter.inference_output_type tf.int8 tflite_model converter.convert() with open(sine_model.tflite, wb) as f: f.write(tflite_model)其中 representative_dataset 需要提供一组足够代表真实输入范围的样本用于校准量化参数一般取训练集的一部分即可def representative_dataset(): for _ in range(100): data np.random.uniform(0, 2 * np.pi, size(1, 1)).astype(np.float32) yield [data]转换完成后用 xxd 把 .tflite 文件转换成 C 数组xxd -i sine_model.tflite sine_model_data.cc生成的 .cc 文件里会有一个很长的 const unsigned char 数组这就是拷贝到 MCU 工程里的模型本体。文件开头会有类似unsigned char sine_model_tflite[]的符号名后续代码里直接用这个符号。这里有个很重要的版本问题TensorFlow 版本和 TFLM 的 schema 版本必须匹配。如果你用最新版 TensorFlow 转换出来的模型老版本的 TFLM 解析器会直接报错并拒绝解析。建议先确认你拉取的 TFLM 源码对应哪个 TensorFlow 版本然后尽量对齐或者使用官方预训练好的 C 文件来规避这个问题。3.3 把模型和运行时集成到 MCU 工程我拉取 TFLM 源码的方式是直接从 GitHub 克隆 tensorflow/tflite-micro 仓库。这个仓库是独立维护的比从整个 TensorFlow 仓库里找路径更清晰。集成时只需要把tensorflow/lite/micro目录拷贝到工程里并把所有 .cc 和 .h 文件添加进 Keil 工程。我一开始图省事只添加了官方 Arduino 例程里列出来的那几个文件结果编译时疯狂报 undefined reference。后来直接把整个 micro 目录拖进去让编译器自己处理依赖才消停。Keil 对多文件工程支持得很好不用担心文件太多的问题。在工程配置里需要注意一个宏定义TF_LITE_STATIC_MEMORY。有的平台需要定义这个宏来启用静态内存分配避免依赖 malloc 和 free。AT32F435 的 SRAM 够大不定义也能跑但为了保证在裸机上稳定运行我建议还是定义上。模型文件放好后TFLM 在编译期不会去“解析”模型它只是把模型数组当作普通数据放进 Flash。真正的解析发生在运行时解析工作由 MicroInterpreter 完成。所以如果模型数组有问题编译一般不会报错问题会集中在运行时的断言和报错信息上。4. 核心代码实现与关键参数解读4.1 HelloWorld 推理主流程的实现逻辑MCU 端代码的核心流程非常清晰分五步拿到模型指针、创建算子解析器、创建解释器、分配张量内存、循环推理。先贴主要的代码骨架#include tensorflow/lite/micro/all_ops_resolver.h #include tensorflow/lite/micro/micro_interpreter.h #include tensorflow/lite/schema/schema_generated.h #include sine_model_data.cc // 模型使用 int8 量化输入输出时需要额外做零点偏移转换 constexpr int kTensorArenaSize 16 * 1024; alignas(16) uint8_t tensor_arena[kTensorArenaSize]; tflite::MicroInterpreter* interpreter nullptr; TfLiteTensor* input_tensor nullptr; TfLiteTensor* output_tensor nullptr; void tflm_setup() { static tflite::AllOpsResolver resolver; static tflite::MicroInterpreter static_interpreter( tflite::GetModel(sine_model_tflite), resolver, tensor_arena, kTensorArenaSize ); interpreter static_interpreter; TfLiteStatus allocate_status interpreter-AllocateTensors(); if (allocate_status ! kTfLiteOk) { // 处理内存分配失败 while(1); } input_tensor interpreter-input(0); output_tensor interpreter-output(0); }这里我用的是AllOpsResolver它会注册 TFLM 支持的所有算子。在真正的产品里为了省 Flash 会改用MicroMutableOpResolver按需注册但这个正弦波模型本身很小AllOpsResolver 增加的体积完全可以接受。实测下来 Flash 占用增加了几十 KB对 AT32F435 来说不算事。4.2 输入输出张量的量化处理正弦波模型在转换时用了 int8 量化所以喂给模型的输入和模型吐出来的输出都不是浮点数而是带零点偏移的 int8 整数。转换关系是real_value (int8_value - zero_point) * scale反过来输入浮点数时也要先转换成 int8float input_val current_x; // 0 ~ 2π 之间的某个相位 int8_t quantized_input (int8_t)(input_val / input_tensor-params.scale input_tensor-params.zero_point); input_tensor-data.int8[0] quantized_input;输出端拿到的是 int8 值需要反量化回浮点int8_t quantized_output output_tensor-data.int8[0]; float output_val (quantized_output - output_tensor-params.zero_point) * output_tensor-params.scale;这个量化细节特别容易踩坑。如果你直接把浮点输入塞进data.int8[0]或者直接把输出的 int8 当成浮点打印看到的结果会是一堆乱码或者完全对不上的数字。如果不做量化模型转换时保持 float32 输入输出代码会简单很多直接操作data.f[0]就行。但为了更贴近实际产品形态我还是保留了 int8 量化方案这样等以后跑更大的模型时这套代码可以直接复用。4.3 串口输出调试与验证数据串口输出部分我用的是 USART1 阻塞发送。在 Keil 下如果只是单纯打印单个字符可以直接操作数据寄存器如果想用 printf需要重定向 fputc 函数int fputc(int ch, FILE *f) { while ((USART1-STS USART_STS_TDBE) 0); USART1-DATA ch; return ch; }AT32 的寄存器位名和 STM32 有差异我一开始想当然按 STM32 的 TXE 位写结果编译报错查了库才发现雅特力的状态寄存器位名是 TDBE。这种小细节在国产 MCU 移植时很常见遇到错误优先查官方固件库头文件。输出内容我设计成了表格友好的格式每个采样点打印三列分别是输入相位、模型预测值、真实 sin 值。数据用浮点格式打印波特率 115200。采集几百个点之后可以把串口数据拷贝到 Python 或 Excel 里画曲线直观对比预测和真值的重合度。4.4 内存、缓冲区与运行时配置要点TFLM 的推理不依赖动态内存分配所有中间张量都放在我们提前声明好的 tensor_arena 里。arena 太小会导致 AllocateTensors 失败太大则浪费 SRAM。这个模型很小我开始只给了 4KB运行后报错说内存不足。后来参考 TFLM 提供的interpreter-arena_used_bytes()接口打印出来发现实际需要约 8KB。保险起见我把 arena 设为 16KB既没浪费太多 SRAM也留足了余量。一定要记得在 tensor_arena 前面加alignas(16)。TFLM 对张量对齐有要求不对齐可能导致未对齐访问在 Cortex-M4F 上轻则性能下降重则 HardFault。这个问题非常隐蔽我一度以为是模型解析失败排查了半天才发现是对齐没写。5. 实测运行结果与效果分析5.1 编译产物的资源占用编译完成后我查了 Keil 的 Map 文件整体资源占用如下项目占用Flash 总占用约 147 KBSRAM 总占用约 28 KB其中 tensor_arena16 KB模型数组约 4.2 KB这个数据说明一个问题TinyML 哪怕模型本身只有几 KB光运行时TFLM 解释器加算子库的 Flash 占用就很可观。如果你的 MCU Flash 低于 128KB跑 TFLM 会比较吃力。AT32F435 的 Flash 起步 256KB完全没压力。如果后续想继续压缩 Flash可以使用 MicroMutableOpResolver 按需注册算子再把不需要的调试信息去掉整体可以压到 80KB 左右。但这个优化不是必须的项目原型阶段我建议优先保证可读性。5.2 串口实测数据与误差分析我让程序从 0 开始每隔 0.05 弧度采集一次输入推理一次并打印预测值和真实 sin 值。截取几组典型数据输入相位 (rad)模型预测值真实 sin 值绝对误差0.000-0.0020.0000.0020.3140.3090.3090.0000.6280.5870.5880.0011.2560.9520.9510.0011.8840.9510.9500.0012.8260.3120.3100.0023.768-0.589-0.5880.001整体误差在 0.002 左右这个精度已经相当好了用于验证链路完全足够。误差来源主要是 int8 量化带来的精度损失以及模型本身拟合能力的极限。如果你想追求更高精度可以把模型改成 float32 输入输出误差能进一步缩小代价是 Flash 占用和推理时间略微增加。3.768 rad 以后那段曲线模型预测值和真实值出现轻微偏差这是量化误差在边界处的典型表现。整体来说AT32F435 跑这个模型的输出相当稳定没有出现抖动或跳变。5.3 推理性能与优化空间我没有用高性能模式去测极限但这颗 MCU 在 288MHz 下跑这个模型单次推理的耗时基本在亚毫秒级。我大概估算了一下一次完整的量化推理大约几十微秒到一两百微秒即使加上串口打印开销也可以轻松跑到上千次推理每秒。如果你后续跑更大的模型比如 MobileNet 这类视觉模型性能瓶颈就会变成卷积算子的计算量。这时候可以启用 CMSIS-NN 优化TFLM 在 Cortex-M 平台上有对应的 optimized kernel 可以加速。AT32F435 的 Cortex-M4F 有 DSP 和 FPU 指令理论上是能吃到这批优化红利的。我还没有做完整测试但选型时确认过这一点这也是我选它做 TinyML 平台的一个重要原因。性能优化不是这个项目的重点但知道优化方向和上限在哪里对下一步选型和架构设计很有帮助。6. 常见问题排查与经验记录6.1 串口没有输出或者输出乱码这类问题 90% 出在串口配置上。先确认波特率是否一致AT-START-F435 板载串口默认 115200不要改成 9600 还一头雾水。其次确认串口引脚是否选对我一开始误以为调试串口在 PB6/PB7结果板子实际用的是 PA9/PA10改过来之后立刻正常。还有一种情况是 printf 重定向没生效。Keil 下如果不重定向 fputcprintf 默认会走半主机模式程序会卡在底层。用微库MicroLIB可以减少很多麻烦但重定向 fputc 仍然是必须的。检查一下工程里有没有类似#pragma import(__use_no_semihosting)的配置或者干脆直接操作串口寄存器打印字符串绕开 printf。6.2 模型解析失败或者 AllocateTensors 报错模型解析失败通常表现为Failed to get model或者Schema version mismatch。这个问题我在前面提过根因是 TensorFlow 转换出来的模型 schema 版本和 TFLM 支持的不一致。解决办法有两个一是对齐版本二是直接使用官方预训练好的 sine_model_data.cc或者重新安装对应版本的 TensorFlow 再转换。AllocateTensors 报错则基本都是 tensor_arena 不够。TFLM 的错误信息有时不够直观建议在代码里先调用interpreter-arena_used_bytes()看看实际使用了多少然后把数组调大到超过这个值。还有一种情况是你在写 arena 时没有按 16 字节对齐某些平台会直接断言失败。6.3 模型输出结果完全不对且没有规律如果编译烧录都成功串口也有数据但预测值明显和 sin 曲线对不上优先检查量化转换逻辑。我在调试时发现输入没有除以 scale 再加 zero_point直接把浮点数强转成 int8结果模型输出全乱。量化公式看着简单但最容易在“整数和浮点来回切”的过程中写错。建议把你输入侧的转换单独封装成函数输出侧单独封装成函数不要散落在主循环里。另外如果你训练模型或转换模型时用的数据范围不是 0 到 2π推理时输入的尺度会不一致也会导致结果偏差。正弦波模型对输入范围非常敏感输入超过训练范围后网络外推能力近乎为零。一定要保证输入数据和训练数据的分布一致。6.4 几个我必须单独说说的细节第一个是版本锁定。TFLM 是一个活跃迭代的仓库每个月都在变。我第一次拉最新代码编译就报了一堆接口错误后来干脆切换到官方 release 分支把 tensorflow 和 tflite-micro 版本绑定世界才安静下来。建议你在集成 TFLM 时记录好 git commit 号否则以后升级会一脸懵。第二个是 HardFault。AT32F435 出现 HardFault 时优先检查数组是否越界、指针是否空、对齐是否正确。TFLM 自身比较严谨问题多半出在我们写的业务代码里。可以在 Keil 的 Fault Report 里看 PC 寄存器的值快速定位到是哪个函数崩的。第三个是国产 MCU 的“兼容性错觉”。AT32F435 虽然很多地方和 STM32F4 相似但寄存器、库函数、启动文件都有差异。不要盲目把 STM32 的工程拿过来直接改型号最好从雅特力官方例程开始在它的基础上加东西。我这次就是从官方模板起步的所以整套流程走下来比较顺没有在底层初始化上浪费太多时间。这个项目虽然只是 TinyML 的 HelloWorld但它的意义在于把“训练-转换-部署-验证”这条链路完整打通了。后面不管你是想跑数字分类、关键词唤醒还是更复杂的传感器数据分析流程都是同一套。我个人在实际操作中最深的体会是TinyML 真正的门槛不在模型训练也不在单片机编程而在把这两套原本各玩各的技术栈融合到一根数据线上。AT32F435 这颗国产芯片让这个融合过程轻松了不少希望这篇记录也能帮你少走几步弯路。
返回列表