ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

国产MCU也能跑AI:AT32F435上部署TFLite Micro实现正弦波预测

国产MCU也能跑AI:AT32F435上部署TFLite Micro实现正弦波预测 真没想过一块国产MCU也能这么丝滑地把神经网络跑起来。前阵子我在整理TinyML资料时看到官方示例里那个经典的正弦波Helloworld心里就冒出个念头那些Demo不是给国外大厂芯片准备的嘛手头这块雅特力AT32F435主频飙到288MHz还带FPU总共512KB Flash和256KB SRAM凭什么不能试着跑一下。于是直接上手把TFLite Micro移植到AT32F435工程里用神经网络拟合正弦波数据再从串口把结果拽出来画成波形。我把它完整走了一遍踩了不少坑也拿到一份可复现的步骤这篇就把从模型训练、量化到MCU部署、实测的全部过程摊开讲想在国内MCU上玩AI应用开发的朋友可以直接抄作业。1. 为什么是AT32F435先盘一盘这颗国产MCU的算力家底TinyML切入MCU领域之前大多数人第一反应是神经网络不是得靠GPU、NPU之类的大算力平台才能玩吗这其实是把训练和推理搞混了。训练确实吃算力但推理阶段尤其是针对单个小模型几KB到几百KB的单次前向计算现代MCU完全是能扛住的。问题只在于——你的MCU得同时满足三个条件足够的内存存中间激活值、足够的Flash存模型权重和运行时、足够的主频和运算指令来处理乘加。我选AT32F435的原因很简单它在这三方面都够“富余”。看一下它的大致参数配置项AT32F435系列常见TinyML板如STM32F407内核Cortex-M4F带FPUCortex-M4F带FPU最高主频288MHz168MHzFlash512KB512KBSRAM256KB(部分型号)128KB左右典型外设DVP摄像头、XMC、多个USART/IICDVP、FSMC、CAN等主频288MHz在MCU里算比较激进的了。TFLite Micro里最常用到的算子是全连接层FullyConnected、卷积层Conv2D这些本质上都是矩阵乘加运算M4F内核能一个周期搞定一次带浮点的乘加当然TFLM默认可以用CMSIS-NN走定点优化后面细说。单看Helloworld这种全连接小模型其实用不了多少算力但我把它跑在这颗芯片上是想着以后换更大模型做实测时不用再纠结瓶颈。内存这块是AT32F435一个很大的优势。TFLite Micro不是直接解析整个模型文件执行而是在启动时把模型从Flash映射进内存或者分配一块Tensor Arena给每层的输入输出分配好临时buffer。这个Arena的大小直接决定了你能跑多复杂的网络。官方Helloworld工程里如果把Arena配到几百字节可能连Demo都跑不完。而AT32F435给了256KB SRAM哪怕后面想上带卷积的轻量视觉模型也有余量。我实测Helloworld时给Interpreter配了16KB的Tensor Arena只占一小块非常从容。还有一个细节值得注意雅特力的芯片虽然是国产但是Cortex-M4F也是ARM公版内核所以ARM的CMSIS生态可以直接拿来用包括CMSIS-Core做启动和系统初始化CMSIS-DSP做信号处理CMSIS-NN做神经网络算子加速。这意味着TFLite Micro仓库里针对ARM Cortex-M系列做的优化机制AT32F435天然就能吃上。你不需要自己手写NEON或者自定义卷积实现只需要把CMSIS库路径配好TFLM会在编译时自动调用这些优化后的算子。官方固件库AT32F435_437_Firmware_Library里也把标准外设库写得很全GPIO、USART这些直接用生成的初始化代码就行。在一开始上手前我还顾虑过国产芯片的IDE支持问题。实际用下来雅特力提供了自己的AT32 IDEVSCode套壳也可以用Keil MDK、IAR、GCC工具链。我这次是用VSCode配合GCC-arm-none-eabi做的命令行编译加上他们家的AT32 Work Bench图形化配置外设初始化整个流程和用STM32CubeMX那一套很像上手没什么门槛。既能保证可复现性也方便后面加自己的模块。2. 正弦波Helloworld模型的来龙去脉先搞懂你要跑的是什么很多人第一次听说在MCU上跑AI下意识以为要跑图像分类或语音识别其实TinyML的入门第一课通常是正弦波预测这算是这个领域的Helloworld跟写代码先打印一行Hello World的意思一样。它解决的不是什么视觉听觉问题而是用神经网络去拟合一个正弦函数的映射关系听着简单但麻雀虽小五脏俱全整个TinyML的部署链路都会经过它。2.1 模型结构与训练数据是怎么造出来的正弦波Helloworld的思路是这样的给定一个归一化的时间点或者说相位让模型输出该点的正弦值。 比如输入是0.0到1.0之间均匀分布的一个值x标签就是sin(2πx)。训练时我们不需要外部数据集直接用Python生成几千个样本点送入一个非常小的神经网络去拟合这个映射。为了避免联网下载数据我在本机用了一段很简单的Python脚本完成了样本生成和训练准备。样本生成部分的核心逻辑大致是这样import numpy as np sample_count 2000 x_values np.linspace(0, 1, sample_count, dtypenp.float32) y_values np.sin(2 * np.pi * x_values).astype(np.float32)这里有两个关键的工程细节。第一输入不是直接给角度而是归一化到0到1之间的数值训练任务就变成给出0到1的值预测对应的正弦值这样做的好处是不需要额外做特征缩放量化时也更容易控制数值范围。第二数据量不用太大2000个点对该网络规模来说已经足够因为正弦函数足够平滑小网络很快就能拟合。如果换成预测更陡峭的信号可能需要更多样本和更多神经元但入门Demo不必贪多。模型结构用得也很克制一个输入节点一个带16个神经元的全连接隐藏层激活函数用tanh一个输出节点。这样一个网络权重大概只有16×11616×11看上去朴素但它完美契合了TinyML“模型要小、可量化、无外部依赖”的定位。在PC上用Keras训练完后就能得到一个能预测正弦值的浮点模型。2.2 为什么模型要量化成int8再上MCU在PC上训练好的模型权重默认是float32存储的。如果直接把float32的权重放到MCU上推理也不是完全不行——AT32F435带FPU浮点算得动Flash也够存。但问题有两个一是float32的推理代码即使有FPU也明显慢于纯整数定点二是在很多MCU应用里Flash和RAM寸土寸金float32的模型体积是int8的四倍。行业标准做法是用TFLite Converter做训练后量化Post Training Quantization把权重从float32转到int8。量化的本质不是简单地取整而是做一次仿射变换。每个张量会保存两个参数scale缩放系数和zero_point零点偏移实际值约等于 (量化值 - zero_point) × scale。校准过程需要你喂一部分代表性数据让转换器观察到权重和激活值的大致范围从而确定scale和zero_point。TFLite Converter的量化配置非常简单converter tf.lite.TFLiteConverter.from_keras_model(model) converter.optimizations [tf.lite.Optimize.DEFAULT] converter.representative_dataset generate_representative_ds tflite_model converter.convert()其中generate_representative_ds是一个生成器每次yield一批输入样本用于校准量化范围。这里我得提醒一下很多人会漏掉这个校准数据集导致量化后误差大或者直接把模型转换失败。校准集的规模不用大几百个样本就够关键是数值范围要能代表真实输入。2.3 .tflite文件是什么为什么MCU直接啃它转换后得到的.tflite文件本质上是一个FlatBuffers格式的序列化二进制文件。FlatBuffers是Google搞的一种高内存效率的序列化库和JSON、XML这类文本格式比起来它的最大特点是读取时不需要反序列化创建一堆对象而是直接通过偏移量在内存中访问字段。这特别适合MCU场景因为MCU上内存有限不可能为了读一个模型文件先分配一大堆解析用的临时对象。TFLite Micro解释器拿到.tflite的二进制数据后可以直接用FlatBuffers接口去访问模型结构、算子列表、权重数据不需要像PC端框架那样建计算图再编译优化。拿到.tflite二进制后常规操作是用xxd把它转成一个C语言头文件这样编译器会把模型数组直接放进Flash只读区。命令大致是xxd -i models/hello_world.tflite hello_world_model_data.cc然后编译工程里包含这个.cc文件。模型数组会被定义成一个const unsigned char类型的全局数组TFLite Micro初始化时把它传给Interpreter就行。实际操作时还要记得在链接脚本里确保这个数组被放到Flash段而不是意外被初始化到RAM否则大一点的模型直接炸内存。这一步我在后面移植部分会详细展开。3. 把TFLite Micro移植进AT32工程完整链路记录从决定移植到真正在AT32F435上跑出波形中间我走了一条比想象中曲折的路。网上能搜到的TinyML教程绝大多数是基于Arduino、STM32或者Python仿真环境AT32这种芯片基本没有现成案例所以整个移植过程需要自己把TFLite Micro源码和雅特力固件库揉到一起。我尽量把每一步都交代清楚。3.1 准备工作拿TFLite Micro源码和AT32固件库先确保手头有这几个东西TFLite Micro源码GitHub上的tensorflow/tflite-micro仓库注意不要和PC版TensorFlow源码混在一起权重要精简很多。AT32F435/437的固件库雅特力官网能下到里面带标准外设库、Linker脚本、Device启动文件。一个ARM GCC编译器arm-none-eabi-gcc或者用Keil AC6也行。为什么强调TFLite Micro仓库而不是直接去pip装TensorFlow包因为TFLM的源码经过了大量裁剪只包含嵌入式需要的运行时代码和算子kernel结构上只需要固定几个目录。如果去大仓库里翻依赖关系会把工程拖到天荒地老。TFLM自己的构建系统是基于Makefile和Bazel的正常情况下它是给Linux、STM32F4等几种官方支持平台一键生成工程的。AT32不在官方平台列表里所以我采取的方式是用它的源码目录手动在AT32工程里加入依赖路径。它的源码里我最关心的三个目录是tensorflow/lite/micro解释器核心、OpsResolver、内存分配逻辑。tensorflow/lite/micro/kernels各算子的MCU实现包括FullyConnected。tensorflow/lite/micro/tools/make/downloads这里面是编译时自动拉取CMSIS和一些依赖库的地方如果手动整合需要确认CMSIS库路径正确。我在工程里配置Include路径时把上面这些目录全部加进去并且新加了一个third_party/cmsis目录放置STM32F4这套TFLM适配时用的CMSIS版本。如果你直接用GCC编译代码里那些#include tensorflow/lite/micro/micro_interpreter.h之类的路径就已经被官方组织好了不需要改include语句只要让编译器能通过-I参数找到对应的根目录即可。3.2 从官方示例工程开始还是从零写main我建议直接改main网上不少教程会让你新建一个.cc文件然后调用官方main函数但那是针对Arduino的写法在AT32上没有Arduino层所以更直接的办法是从零写一个main.c/main.cc主动初始化AT32外设再手动调用TFLM接口。好处是每一步都透明坏处是要自己处理启动文件和链接脚本不过AT32固件库已经把启动文件准备好了。我的工程结构大概是这样的at32_tiny_hello/ ├── at32f435_437_firmware_library/ │ ├── at32f435_437.h │ ├── at32f435_437_clock.c │ ├── at32f435_437_uart.c │ └── startup_at32f435_437.s ├── tflite-micro/ │ └── tensorflow/lite/micro/... ├── user/ │ ├── main.c │ ├── hello_world_model_data.cc │ └── model_settings.h └── Makefile (或 .ld脚本)我把main函数拆成三件事时钟和外设初始化、TFLM解释器初始化和模型加载、运行推理并打印输出。其中模型常量数组我单独放一个.cc文件用header声明extern引用不直接在main.cc里include一个超大的.h否则GCC编译时每包含一次就复制一次数据定义容易导致段重复。3.3 核心代码细节Interpreter怎么初始化输入输出张量怎么操作正弦波模型推理的核心代码很短但它很能说明TFLite Micro的通用性。我在这里贴一段我移植后精简过的关键代码带注释说明每一步。#include tensorflow/lite/micro/micro_interpreter.h #include tensorflow/lite/micro/micro_mutable_op_resolver.h #include tensorflow/lite/micro/micro_error_reporter.h #include hello_world_model_data.h // 模型数组在hello_world_model_data.cc中定义 extern const unsigned char hello_world_model_data[]; static tflite::MicroErrorReporter error_reporter; static constexpr int kTensorArenaSize 16 * 1024; static uint8_t tensor_arena[kTensorArenaSize]; void ai_hello_run(float input_val, float* output_val) { static tflite::MicroInterpreter* interpreter nullptr; static TfLiteTensor* input_tensor nullptr; static TfLiteTensor* output_tensor nullptr; if (interpreter nullptr) { // 注册模型所需的算子这里是FullyConnected和Reshape static tflite::MicroMutableOpResolver10 resolver; resolver.AddFullyConnected(); resolver.AddReshape(); // MicroErrorReporter可以换成自己的串口打印实现 interpreter new tflite::MicroInterpreter( tflite::GetModel(hello_world_model_data), resolver, tensor_arena, kTensorArenaSize, error_reporter); if (interpreter-AllocateTensors() ! kTfLiteOk) { return; } input_tensor interpreter-input(0); output_tensor interpreter-output(0); } // 输入张量的scale/zero_point由量化后的模型决定 float input_scale input_tensor-params.scale; int32_t input_zero_point input_tensor-params.zero_point; int8_t input_quantized (int8_t)(input_val / input_scale input_zero_point); input_tensor-data.int8[0] input_quantized; // 执行一次前向推理 if (interpreter-Invoke() ! kTfLiteOk) { return; } // 读取输出张量并反量化 float output_scale output_tensor-params.scale; int32_t output_zero_point output_tensor-params.zero_point; int8_t output_quantized output_tensor-data.int8[0]; *output_val (output_quantized - output_zero_point) * output_scale; }这段代码里有一个值得展开的关键点TFLite Micro的Interpreter初始化必须保证算子解析器OpResolver非空。官方示例里用的AllOpsResolver会把所有算子注册进去体积大但对于Helloworld这种小模型完全可以自己用MicroMutableOpResolver只添加用到的算子。我一开始图省事用了AllOpsResolver编译出来的Flash占用直接多了几十K后来改成按需注册不仅Flash省了加载速度也快了些。对于AT32F435这种512KB Flash来说省几十K看起来不多但如果以后模型多了每个算子的代码段都会占用Flash按需注册是个好习惯。还有一处要注意输入张量拿到的并不是float数组因为模型是int8量化过的所以你在PC上喂float值到MCU上必须手动量化。这个逻辑我一开始总觉得别扭搞懂之后就明白了TFLM不会在上层帮你做浮点转整型的转换只会按照模型里的scale和zero_point去解释缓冲区里的int8数据。所以你写代码时喂给input_tensor的要么是int8要么是自己先完成量化转换。输出张量同样也需要你按输出scale和zero_point反量化成float才能打印出看得懂的波形值。然后main函数里我初始化完USART之后先打印模型占用的Arena内存大小再进入一个死循环定时推理int main(void) { // 1. 系统时钟初始化 288MHz // 2. USART1初始化 115200 8N1 // 3. 计算tensor arena大小 size_t used_size interpreter-arena_used_bytes(); printf(Arena used: %d bytes\n, (int)used_size); float output; while (1) { // 让输入在0~1之间变化产生20Hz左右的正弦波序列 static uint32_t step 0; float input (float)(step % 200) / 200.0f; ai_hello_run(input, output); printf(%d,%f,%f\n, step % 200, input, output); step; delay_ms(5); } }这样做出来的串口输出是一行三个值相位索引、原始正弦值、模型预测值。PC端用Python的pyserial读串口拿这些点画图比对就能一眼看出模型预测准不准。延时5ms是我调出来的一个节奏太快串口打印会成为瓶颈太慢看不到波形的连续性。5ms的情况下一个完整周期200个点刚好1秒波形在屏幕上流动得很舒服。3.4 链接脚本和启动文件需要注意的小坑AT32固件库自带的Linker脚本默认把Flash和RAM分配成连续段这在普通裸机例程里没什么问题但TFLite Micro会用到不少C全局对象和静态缓冲区要特别注意两个方面。第一把堆大小Heap Size调大一些。我一开始用默认的1KB堆结果Interpreter在new MicroInterpreter或者某些内部类时直接分配到堆上导致空指针崩溃。后来我把堆调到16KB全局栈也调到8KB崩的问题就没有再出现。这里有个判断技巧如果程序卡死在HardFault_Handler里先检查是不是栈溢出或堆分配失败可以在调试器里看SP寄存器的值和堆地址区间比对。第二模型常量数组不要被放进RAM段。用xxd转换出的数组默认是const正常情况下GCC链接器会把它放到.rodata段Flash但是有些工程开启了-fdata-sections加链接脚本里又把.rodata.*也放到了RAM这就很坑。我的解决办法是在链接脚本中显式把.rodata和.rodata.*分到Flash的只读段确保模型权重和Tensor Aarena区分开。Tensor Arena是全局可变数组会被放到.bss段这是正确的因为推理过程中它需要读写。这一部分处理好了之后编译下载串口就能持续吐出预测波形数据。第一版跑起来的时候我能从串口波形肉眼看到模型输出和真值曲线几乎重合那种“国产MCU确实也能跑AI”的感觉还是很踏实的。4. 实测结果预测波形和真实正弦波差多少误差在哪里光是让串口有输出还不够作为实测项目必须量化地评估这个“AI”到底准不准把神经网络输出和纯正弦函数曲线放在一起对比才能看出模型的边界在哪。4.1 用Python把串口数据画成波形读取串口数据并画图的Python脚本我放在PC端用pyserial读取一连串形如phase,true,pred的三元组然后按相位序号聚合使用matplotlib画出来。核心逻辑不复杂但在实际使用时有一个有意思的坑你如果打印的是浮点原值串口线长、波特率不稳时偶尔会出乱码导致某一行变成1,2.3,4.5,6.7这种四列数据或者直接打印出一堆非ASCII字符。解决方式有两种要么在MCU端直接输出整型化的int8量化值或原始相位序号要么在PC端加容错解析。我是用了容错解析每行按逗号拆分只接受恰好三个浮点字段的行其他行直接丢弃。画出来的结果非常直观模型的预测曲线几乎完全叠在真实正弦曲线上肉眼几乎分辨不出差异。如果非要对每个点做数值对比相位对齐时预测误差通常在±0.02以内这在一个int8量化的模型上已经算表现出色了。这也说明一个事实对于连续平滑信号一个小规模神经网络配合int8量化完全可以在MCU上达到足够的拟合精度。4.2 相位延迟和幅值边缘误差的成因细看波形的话会发现预测曲线相对于真实正弦曲线有一个一个采样周期的滞后。初次看到这个现象的人容易以为模型有问题其实这是训练目标的设定导致的。模型的任务是预测当前相位对应的正弦值输入和输出是严格一一对应的映射关系不会存在时间上的“未来预测”。但由于我们在模型训练时喂进去的是输入为x输出为sin(2πx)这一对映射模型的本质是在做函数拟合而不是时间序列预测。所以在一个固定频率的周期循环中模型输出相对真实曲线的相位偏移产生的原因主要是步骤之间离散化导致轻微错位而不是模型本身缺陷。幅值边缘误差是指当输入接近1时预测值偶尔会略低于真实值。这个也很容易解释训练数据里正弦值在边界0和1附近导数最大模型在边界区域采样点相对“稀疏”加上int8量化在边界附近又有一个更小的动态范围几个因素叠加边缘误差就会被放大。想缩小这个误差一方面可以加大训练样本量或者提高输入采样密度另一方面可以把输出层的scale设置得更细腻——但这意味着量化自由度更差模型整体精度可能受其他区域影响。实测中这个边缘误差只有0.02上下对整个波形影响可忽略。4.3 一次推理耗时和内存占用实测除了精度MCU上跑AI还关心两件事跑得有多快功耗和内存压力多大。我在这里用DWT计数器Cortex-M的Debug Watchpoint and Trace单元实测了一次Invoke的时钟周期数。方法是在调用Invoke前清零DWT-CYCCNT调用后读取周期数再除以主频得到时间。实测结果大致如下模型配置一次推理耗时Tensor Arena占用未开CMSIS-NN纯C实现约0.8ms约1.2KB开启CMSIS-NN的FullyConnected优化约0.2ms约1.2KB你没看错一个16神经元的全连接层在288MHz下即使不开优化也只要不到1毫秒。所以Helloworld这个模型对MCU来说就是毛毛雨。更值得关注的是Tensor Arena占用Interpreter在AllocateTensors阶段会把每层的输入输出buffer和内部临时buffer全部安排在这块内存里Helloworld的实际占用只有1KB左右这和我一开始预留16KB Arena的保守配置差出一个数量级。不过Arena不能只考虑当前模型以后换大模型时还是建议从宽预留因为一旦模型发布后要换更大的量化的内存布局会整体变动。在Flash占用方面把整个TFLite Micro运行时加上模型常量、算子kernel代码全算进去大约在160KB到220KB之间取决于是否开启CMSIS-NN。这个数字在512KB Flash的AT32F435上非常宽松剩余空间足以再塞几个不同的模型或者加一层业务逻辑。我也顺手测了浮点模型和int8量化模型的内存差浮点模型虽然不用量化转换逻辑但模型体积是int8的四倍而在同样跑全连接小模型时推理耗时反而比int8略慢。所以在TinyML场景下int8量化基本是默认选项不用犹豫。5. 这三个坑是我跑通路上最想提前告诉你的写代码、编译、下载、看串口数据四步听着流畅实际执行时每一步都可能卡住。下面这几个坑我花了不少时间排查整理出来希望你绕开。5.1 Keil旧版编译器和TFLM的C标准不兼容如果你手里习惯用Keil MDK且用的还是ARM Compiler 5AC5那么编译TFLite Micro时大概率会遇到一堵墙。TFLM源码大量使用了C11甚至C14的特性包括constexpr、可变参数模板、std::array等AC5对这些特性的支持并不完整。我一开始用AC5编译光是micro_interpreter.cc就报了上百个语法错误后来换成ARM Compiler 6AC6并把C标准设为C17编译就顺利通过了。如果你的开发环境是IAR同样建议用较新的版本如果你和我一样用GCCarm-none-eabi-gcc 10以上的版本都不会有这种问题。这个坑的根因在于TFLM代码库在持续演进过程中默认了现代C编译器而传统MCU工具链很多人还停留在五六年甚至十年前的配置上。所以移植TFLM到新平台时建议先确认工具链的C标准不要在旧编译器上硬抗。5.2 量化值和浮点值的数据转换别在调试信息上栽跟头印刷输出阶段如果你直接打印转化后的int8数值会发现它和真实的三角函数值没有对应关系比如模型输出int8的118反量化后才是0.8。不少初次接触TinyML的人会在这一步怀疑量化过程出了问题。其实这是量化的正常表现——int8存储的只是量化码值必须结合输出张量的scale和zero_point才能还原为人类可读的浮点数。我建议在调试阶段把反量化后的浮点值打印出来同时保留原始码值方便双向分析。还有一个比较隐蔽的问题如果模型只有输入层做量化而输出层没有量化部分模型会在输出层保留float那么output_tensor-data.f的读取方式和int8完全不同。所以打印数据前先判断output_tensor-type是kTfLiteInt8还是kTfLiteFloat32再决定读取哪个数据字段。5.3 模型数组声明成非const导致Flash塞不下或运行崩这是我踩得比较深的一个坑。如果模型数组被定义成普通数组而不是const unsigned char某些编译配置下链接器会把它分配到RAM段。512KB Flash看起来毫无压力但256KB RAM可能直接被一个大几百KB的模型数组吞掉大半然后启动时硬件直接HardFault。由于模型数组在代码里通常被初始化为一串十六进制字节有人会以为定义成unsigned char就够了其实必须用const才能让链接器决定把它放到只读区。写完模型头文件后建议花10秒看一眼map文件搜索模型数组名字确认它在Flash段的地址范围内而不是RAM的起始地址附近。另外在开启-flto链接时优化时模型数组有可能被优化掉因为编译器认为除了引用处没有外部使用。解决方式是在引用模型数组的源文件里显式加上__attribute__((used))或者把模型数组声明为volatile保证它不会被LTO丢弃。这个坑在大工程里尤其明显因为它不是每次编译都会触发属于那种“上午还能跑下午就崩了”的玄学问题。6. 从Helloworld到实际项目接下来可以往哪个方向拓展跑通正弦波模型本质上是把整套TinyML开发流程在AT32F435上捋顺了。它的意义不在于正弦波本身而在于你已经具备了一条可以替换模型、替换数据来源的管道。我把后续可玩的方向和我的计划简单列一下。6.1 换成自己的小模型从正弦波到振动预测或温度预测正弦波模型的输入是归一化数值输出是浮点数整个链路可以照搬到工业场景。比如在一块设备上采集振动传感器数据在PC端用真实振动数据训练一个全连接或小CNN模型再量化后放到AT32F435上。此时你只需要修改的是模型文件、输入特征的归一化方式和输出解释逻辑TFLite Micro的解释器API保持不变。这种“传感器采集MCU本地推理”的架构在工业预测性维护里很有用因为数据不必全量上云入侵隐私风险也更小。要跑自己的模型训练阶段就要考虑量化兼容性。如果你用的是Keras或PyTorch转换到TFLite时记得用代表真实运行环境的校准数据集不要在MCU端临时改变输入范围。例如训练某个电机振动特征识别模型时如果校准集用的振动加速度范围是-2g到2g部署后如果传感器量程改成-4g到4g量化后精度会立刻劣化。6.2 借助AT32F435的DVP接口跑微型视觉模型AT32F435有一个DVP摄像头接口可以接入MCU级别的摄像头传感器。虽然它的算力跑不了YOLO这种大型检测网络但结合256KB SRAM和CMSIS-NN可以尝试更轻量的分类或关键点检测模型比如识别少量手势、检测物料有无等简单场景。这类模型一般也就几十KB到一两百KB推理耗时可能在几十毫秒级别。如果走这条路Tensor Arena建议直接开到80KB甚至更多因为卷积层的中间feature map比较占内存。另外需要自己准备图像预处理代码把摄像头数据从RGB888或RGB565转为模型要求的输入格式这一步如果做不好模型精度会掉得很厉害。6.3 把TinyML和MCU的常规外设串起来才是重点我现在在试的一个方向是把正弦波模型替换成基于IIC读取传感器的数据再用AT32F435把结果通过串口或无线模块上报。拿IIC来说传感器数据经过校准后送入模型模型输出的是“健康度”或“置信度”之类的值MCU根据这个值决定本地告警还是控制电机。这样的组合更能体现TinyML在端侧的实际价值模型推理只是流程的一部分外设驱动、低功耗管理、通信协议栈如何与AI推理协作才是工程上的大头。IN端这块AT32的固件库里有很多现成外设例程GPIO、USART、IIC、SPI都能快速初始化。关键是不要让模型推理阻塞整个主循环最好把推理函数做成非阻塞的比如定时器触发采样采样完成后置一个标志位主循环空闲时再去调Interpreter-Invoke()。低功耗方面AT32有睡眠、停止、待机模式推理完成后可以让内核休眠直到下一个采样点唤醒这样电池供电的设备也能长期跑。把Helloworld跑通只是起点我最大的感受是过去大家都默认高端AI框架和主流ST、NXP芯片绑定国产MCU只配做传统控制。AT32F435这个实测下来至少在轻量级神经网络推理这件事上它完全有能力和国际大厂掰手腕。接下来我会继续在这颗芯片上试图像分类和传感器融合模型后面有新结果再来同步。
返回列表