拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32边缘AI部署实战:从ONNX模型到CUBE-AI推理全流程

STM32边缘AI部署实战:从ONNX模型到CUBE-AI推理全流程

1. 为什么要在STM32上跑AI模型

1.1 从“云端推理”到“边缘推理”的转变

过去几年,做AI应用的主流思路是把数据传到云端服务器,在GPU集群上跑推理,再把结果返回到终端设备。这套模式在带宽充足、延迟不敏感的场景下没问题,但一旦落到工业现场、消费电子、车载设备这些领域,麻烦就来了:网络不稳定、数据隐私敏感、响应延迟要求高、长期流量成本压不住。于是“边缘AI”这个概念被反复提起,而STM32作为嵌入式领域出货量最大的MCU家族之一,自然成了很多人尝试部署轻量级AI模型的首选平台。

我最早接触这个方向是因为一个电机异常检测的项目。客户要求在不联网的前提下,用振动传感器采集数据,实时判断电机运行状态。一开始想用树莓派,但成本、功耗、供货周期都不合适,最后选了一颗STM32F407,配合CUBE-AI把一个小型神经网络塞进去,整个方案BOM成本压到了原来方案的三分之一。从那以后,我陆续在STM32F4、F7、H7、L4、U5等多个系列上部署过模型,踩了不少坑,也总结了一些相对成熟的流程。

这篇文章面向的是有一定STM32开发基础、想尝试AI模型部署但不知道从哪下手的工程师。我会把整个流程拆开,从模型训练、格式转换、量化、CUBE-AI集成到最终在板子上跑起来,每一步都给出可复现的操作和参数说明。10分钟是理想情况下的时间,前提是你已经有一个训练好的模型和一块能用的开发板。

1.2 STM32做AI推理的硬件底子

不是所有STM32都能跑AI模型,选型的时候要看几个关键指标。首先是Flash和RAM,模型权重和中间激活值都要占空间,一个几十KB的模型在F103上基本没戏,但在F407或H743上就很轻松。其次是主频和是否有FPU(浮点运算单元),带FPU的芯片做浮点推理速度会快很多,比如F4系列有单精度FPU,H7系列有双精度FPU,而F0、F1这些老型号没有FPU,跑浮点模型会非常吃力。再就是是否有DSP指令集,Cortex-M4和M7都支持DSP扩展,做卷积、矩阵乘法这类操作时能明显加速。

下面这张表是我实际用过的几款芯片在跑同一个关键词识别模型时的表现,模型输入是40x10的MFCC特征,输出是12类关键词,量化到int8,供你选型时参考:

芯片型号主频FPUDSPFlash占用RAM占用单次推理耗时
STM32F103C872MHz无无38KB12KB约420ms
STM32F407VG168MHz单精度有36KB10KB约28ms
STM32F767ZI216MHz双精度有36KB10KB约18ms
STM32H743ZI480MHz双精度有36KB10KB约9ms
STM32L4R5ZI120MHz单精度有36KB10KB约45ms

从表里能看出来,F103虽然能跑,但420ms的延迟在很多实时场景下是不可接受的。F407是性价比拐点,28ms已经能满足大部分中低速控制场景。H743适合对延迟要求苛刻的应用,比如音频实时处理。L4系列主打低功耗,适合电池供电的场合,但速度会慢一些。

注意:上表中的Flash和RAM占用是CUBE-AI生成代码后的实际编译结果,不同版本的CUBE-AI和不同的优化等级会有差异,建议以你实际工程编译后的map文件为准。

2. 模型准备:从训练到ONNX

2.1 模型选型与训练框架

STM32上能跑的模型有几个硬约束:参数量不能太大,一般控制在100KB以内比较稳妥;输入维度不能太高,比如图像输入224x224x3对MCU来说就太重了,通常要降到32x32或48x48;网络结构要简单,避免复杂的自定义算子,因为CUBE-AI对算子的支持是有限的。

我常用的训练框架是TensorFlow/Keras和PyTorch。Keras的好处是API简洁,导出ONNX或直接导出SavedModel都方便。PyTorch在学术界更流行,导出ONNX的流程也很成熟。不管用哪个框架,最终目标都是得到一个ONNX格式的模型文件,因为CUBE-AI对ONNX的支持最完善。

以一个简单的振动分类模型为例,输入是三轴加速度计的256点采样,输出是5种状态(正常、不平衡、松动、轴承故障、齿轮故障)。网络结构用三层一维卷积加两层全连接,参数量控制在20KB左右。训练的时候要注意,输入数据要做归一化,把原始加速度值映射到[-1, 1]或[0, 1]区间,这样量化后的精度损失会小很多。

2.2 导出ONNX的实操细节

从PyTorch导出ONNX,核心代码就几行:

import torch import torch.onnx # 假设model是你的网络,input是示例输入 model.eval() dummy_input = torch.randn(1, 3, 256) # batch=1, 三轴, 256点 torch.onnx.export( model, dummy_input, "vibration_model.onnx", input_names=["accel_input"], output_names=["state_output"], opset_version=11, dynamic_axes=None # MCU上batch固定为1,不需要动态轴 )

这里有几个关键点。opset_version建议用11或12,太新的版本CUBE-AI可能不支持,太老的版本某些算子导出会有问题。dynamic_axes一定要设为None或者不设置,因为MCU上batch size固定为1,动态轴会引入额外的复杂度。model.eval()必须调用,否则BatchNorm和Dropout层的行为会和推理时不一致。

导出完成后,用onnxruntime验证一下模型是否能正常推理,输入输出shape是否符合预期:

import onnxruntime as ort import numpy as np sess = ort.InferenceSession("vibration_model.onnx") input_name = sess.get_inputs()[0].name output_name = sess.get_outputs()[0].name test_input = np.random.randn(1, 3, 256).astype(np.float32) result = sess.run([output_name], {input_name: test_input}) print(result[0].shape) # 应该是(1, 5)

如果这一步报错,说明ONNX模型本身有问题,先解决再往下走,不要带着问题模型去跑CUBE-AI,否则后面排查起来更麻烦。

2.3 量化:int8到底怎么选

量化是MCU部署AI模型的关键步骤。STM32的CUBE-AI支持float32和int8两种主要的数据类型。float32精度高但占用大、速度慢;int8占用小、速度快,但会有精度损失。实际项目中,我几乎总是优先考虑int8,只有在精度实在压不住的情况下才退回float32。

CUBE-AI的量化是训练后量化(Post-Training Quantization),不需要重新训练模型。它需要一个校准数据集,通常从训练集里随机抽100到500个样本就够了。校准数据要覆盖各种工况,比如振动模型里要包含正常和各种故障状态的样本,否则量化后的模型在某些类别上会严重偏移。

量化的核心参数是quantize选项,在CUBE-AI的配置里可以选int8、float32或者int8_float32混合模式。混合模式是指部分层用int8,部分层用float32,适合那些对精度特别敏感的层。我一般先用纯int8跑一遍,看精度下降多少,如果下降在2%以内就接受,超过5%就考虑混合模式或者调整网络结构。

实操心得:量化后的模型精度验证一定要用独立的测试集,不要用校准集本身。我见过有人用校准集验证,精度看起来很好,但实际部署后效果很差,就是因为校准集过拟合了。

3. CUBE-AI集成:从ONNX到C代码

3.1 STM32CubeMX和CUBE-AI的安装配置

CUBE-AI是ST官方推出的工具,集成在STM32CubeMX里。如果你用的是STM32CubeIDE,它也内置了CUBE-AI插件。我习惯用CubeMX独立版,因为版本更新更及时,而且可以单独管理CUBE-AI的版本。

安装步骤大致是:先装STM32CubeMX,然后在Help菜单里选Manage embedded software packages,找到X-CUBE-AI这个包,勾选你需要的版本。截至我写这篇文章的时候,X-CUBE-AI已经更新到8.x版本,对ONNX的算子支持比早期版本好了很多。安装完成后,新建工程或者打开已有工程,在Middleware里就能看到X-CUBE-AI的选项。

这里有个坑要注意:CUBE-AI的版本和CubeMX的版本有对应关系,不是随便组合都能用。比如CubeMX 6.8配CUBE-AI 8.0是稳定的,但配7.x可能会报错。建议直接用CubeMX推荐的默认版本,不要强行混搭。

3.2 添加模型和配置参数

在CubeMX里启用X-CUBE-AI后,界面会多出一个AI选项卡。点击Add network,选择你导出的ONNX文件。CUBE-AI会自动分析模型结构,显示每一层的类型、输入输出shape、参数量等信息。这时候要重点看几个地方:

第一,有没有不支持的算子。如果某个层显示为红色或者有警告,说明CUBE-AI不支持这个算子,需要回到模型层面替换成支持的算子。常见的坑包括自定义的激活函数、非标准的池化方式、复杂的reshape操作等。

第二,输入输出的数据类型和shape。CUBE-AI默认会把输入设为float32,如果你要用int8推理,需要在配置里把输入类型改成int8,同时注意输入数据的归一化方式要和训练时一致。

第三,内存分配。CUBE-AI会给出一个预估的Flash和RAM占用,你可以调整memory pool的大小。如果RAM不够,可以尝试开启use activation buffer选项,让中间激活值复用同一块内存,代价是推理速度会稍微慢一点。

配置完成后,点击Generate Code,CUBE-AI会在工程里生成一系列文件,包括网络权重、推理引擎、API接口等。生成的文件通常在Middlewares/ST/AI目录下,核心的API在ai_platform.h和app_x-cube-ai.h里。

3.3 生成代码的结构解析

CUBE-AI生成的代码结构比较清晰,主要分三部分。第一部分是模型定义,在network.c和network.h里,包含了每一层的权重数据和网络拓扑。第二部分是推理引擎,在ai_platform.c里,负责调度各层的计算。第三部分是应用接口,在app_x-cube-ai.c里,提供了初始化、推理、获取结果的函数。

关键API有三个:

// 初始化AI模型 ai_handle ai_model_init(void); // 执行推理 ai_error ai_model_run(ai_handle handle, const ai_buffer* input, ai_buffer* output); // 获取推理结果 ai_buffer* ai_model_get_output(ai_handle handle);

实际使用的时候,你需要在main函数里先调用初始化,然后在主循环或定时器中断里填充输入buffer,调用推理函数,最后读取输出buffer。输入buffer的填充要注意数据格式,如果是int8量化模型,输入值要按量化参数缩放成int8整数。

4. 在STM32上跑通第一个推理

4.1 工程搭建与编译

假设你已经用CubeMX生成了一个带CUBE-AI的工程,接下来要做的就是把工程导入到你的IDE里编译。我用的是STM32CubeIDE,因为它是免费的,而且和CubeMX无缝集成。如果你习惯用Keil或IAR,也可以,但要注意CUBE-AI生成的代码对编译器的C标准有要求,建议用C11或更高。

编译之前检查几个地方:一是堆栈大小,AI推理会用到比较大的栈空间,建议把主栈调到至少4KB,堆调到至少8KB;二是优化等级,建议用-O2或-O3,-O0会让推理速度慢好几倍;三是浮点ABI,如果芯片有FPU,要在编译选项里开启hard float,否则浮点运算会用软件模拟,速度差一个数量级。

编译通过后,先别急着跑推理,用调试器看一下生成的模型信息。CUBE-AI提供了一个ai_model_info()函数,可以打印出模型的输入输出shape、参数量、量化参数等。把这些信息和你在PC上验证的ONNX模型对比一下,确认一致后再往下走。

4.2 输入数据的预处理

MCU上的输入数据通常来自传感器,比如加速度计、麦克风、摄像头。这些原始数据不能直接喂给模型,需要做预处理。以振动模型为例,加速度计输出的是三轴16位整数,要先转成浮点,再做归一化,最后按模型要求的shape排列。

预处理这一步很容易出问题。我遇到过好几次推理结果完全不对,最后发现是输入数据的排列顺序和训练时不一致。比如训练时数据是[通道][采样点]的格式,部署时不小心写成了[采样点][通道],模型完全懵了。解决办法是在PC上用同样的预处理代码跑一遍,把结果和MCU上的结果对比,确认一致后再继续。

如果模型是int8量化的,输入数据还要做量化。CUBE-AI会给出输入的scale和zero_point参数,你需要按这个公式把浮点值转成int8:

int8_value = round(float_value / scale) + zero_point

这个计算可以在PC上预先算好,也可以放在MCU上实时算。如果传感器数据变化不快,建议在MCU上实时算,灵活性更好。

4.3 推理执行与结果解析

推理函数的调用比较简单,但有几个细节要注意。第一,输入buffer和输出buffer要用CUBE-AI提供的ai_buffer结构体,不要自己随便定义数组。第二,推理函数是阻塞的,调用后会一直等到计算完成才返回,所以在实时性要求高的场景下,要考虑把推理放在低优先级任务里,或者用DMA加中断的方式做数据搬运。

推理完成后,输出buffer里是量化后的int8值,需要反量化回浮点才能得到有意义的分类概率。反量化公式是:

float_value = (int8_value - zero_point) * scale

如果是分类任务,通常还要做softmax把输出转成概率。但注意,CUBE-AI生成的模型如果最后一层是softmax,输出已经是概率了,不需要再做一次。如果不确定,可以看模型结构里最后一层的类型。

避坑技巧:第一次跑推理的时候,建议先用一组已知结果的数据做验证。比如从测试集里拿一个样本,在PC上跑一遍记录输出,然后在MCU上跑同样的样本,对比两者的输出差异。如果差异在合理范围内(比如最大绝对误差小于0.05),说明部署成功。如果差异很大,就要逐步排查是预处理、量化还是推理引擎的问题。

5. 性能优化与内存压缩

5.1 推理速度的优化手段

推理速度是MCU部署AI模型时最常被问到的问题。优化手段分几个层面。最直接的是提高主频,把芯片跑到最高频率,比如F407从168MHz超到180MHz(虽然官方不推荐长期超频),H743从480MHz跑到550MHz。但超频有风险,量产项目不建议。

第二个层面是优化模型结构。减少层数、减小通道数、用深度可分离卷积替代标准卷积,这些都能显著降低计算量。我做过一个对比,把标准卷积换成深度可分离卷积后,参数量降到原来的三分之一,推理速度提升了一倍多,精度只掉了1.5%。

第三个层面是用CUBE-AI的优化选项。CUBE-AI在生成代码时有几个优化等级,比如balanced、time、ram。选time会生成更快的代码,但占用更多Flash;选ram会压缩内存占用,但速度慢一些。根据你的实际约束来选。

第四个层面是用CMSIS-NN库。CUBE-AI生成的代码底层会调用CMSIS-NN的优化函数,但前提是你的芯片支持DSP指令集。如果芯片是Cortex-M4或M7,确保在工程里启用了CMSIS-NN,否则会退回到普通的C实现,速度差很多。

5.2 内存占用的压缩策略

内存是另一个瓶颈。STM32F407有192KB RAM,听起来不少,但模型权重、中间激活值、输入输出buffer、再加上你的应用程序,很容易就超了。压缩内存的策略有几个:

一是用int8量化,权重和激活值都从4字节降到1字节,直接省75%的内存。二是开启activation buffer复用,让不同层的中间结果共用同一块内存,CUBE-AI会自动计算最优的内存复用方案。三是把模型权重放到外部Flash,运行时按需加载,但这会增加推理延迟,适合对速度不敏感的场景。

还有一个容易被忽略的点是堆栈大小。AI推理的调用栈可能很深,特别是层数多的模型。如果栈太小,会出现莫名其妙的hard fault。我一般会把主栈设成8KB,堆设成16KB,然后根据实际使用情况调整。

5.3 精度与速度的权衡

精度和速度永远是一对矛盾。我的经验是,先确定你能接受的最低精度,然后在这个约束下尽量优化速度。比如振动分类模型,如果客户要求准确率不低于95%,那量化后的模型必须达到这个线,达不到就退回float32或者调整网络结构。

量化精度损失主要来自两个方面:权重的不均匀分布和激活值的动态范围。如果某一层的权重集中在很小的范围内,量化后很多值会变成0,信息就丢了。解决办法是在训练时加入量化感知训练(QAT),让模型提前适应量化误差。不过QAT需要重新训练,流程更复杂,适合对精度要求极高的场景。

6. 常见问题与排查实录

6.1 推理结果完全不对

这是最常见的问题,原因通常有几个。第一,输入数据格式不对,比如通道顺序、归一化方式、量化参数和训练时不一致。第二,模型导出ONNX时出了问题,比如opset版本不匹配、某些层被错误简化。第三,CUBE-AI版本和模型不兼容,某些算子被错误处理。

排查方法是从PC端开始,逐级对比。先在PC上用ONNX Runtime跑一遍,记录输出。然后在PC上用CUBE-AI的模拟器跑一遍(CUBE-AI提供了PC端的验证工具),对比输出。如果这两步一致,说明模型和CUBE-AI都没问题,问题出在MCU端的预处理或数据搬运。如果这两步不一致,说明ONNX导出或CUBE-AI配置有问题。

6.2 编译报错和链接错误

CUBE-AI生成的代码对编译器和链接器有一些要求。常见的报错包括:找不到ai_platform.h,说明头文件路径没加对;undefined reference to ai_model_init,说明生成的源文件没加入编译;region RAM overflowed,说明内存不够,需要调整链接脚本或压缩模型。

链接脚本的问题在STM32上很常见,特别是用CubeMX生成的工程,默认的链接脚本可能没有给AI模型留足够的空间。你需要手动修改.ld文件,把AI相关的段放到合适的位置。如果用的是H7系列,还要注意DTCM、AXI SRAM、SRAM1/2/3的分配,不同内存区域的访问速度不一样,把权重放在AXI SRAM、激活值放在DTCM通常是最优的。

6.3 推理速度比预期慢

如果推理速度比预期慢很多,先检查编译优化等级。我见过有人用-O0编译,推理耗时是-O2的5倍。然后检查FPU是否启用,没有FPU的话浮点运算会慢得离谱。再检查CMSIS-NN是否启用,没有DSP指令集加速的话,卷积运算会慢很多。

还有一个隐藏的坑是中断干扰。如果推理过程中频繁被高优先级中断打断,实际耗时会被拉长。解决办法是把推理放在临界区里,或者用DMA把数据搬运和推理并行起来。

6.4 常见问题速查表

现象可能原因排查方法解决方案
推理结果全为同一类输入数据未归一化或量化参数错误对比PC和MCU的输入buffer检查归一化和量化公式
编译报错找不到AI头文件头文件路径未添加查看编译输出在IDE里添加Middlewares/ST/AI路径
链接报错RAM溢出模型太大或堆栈设置过小查看map文件压缩模型或调整链接脚本
推理速度极慢优化等级为-O0或FPU未启用查看编译选项改为-O2并启用hard float
运行中hard fault栈溢出或内存越界用调试器查看fault寄存器增大栈空间,检查buffer大小
量化后精度骤降校准集不具代表性用测试集验证更换校准集或改用混合量化

最后分享一个我踩过的坑:有一次模型在F407上跑得好好的,换到F103上就完全不对。排查了半天发现是F103没有FPU,CUBE-AI生成的代码默认用浮点,在F103上浮点运算被软件模拟,不仅慢,而且某些中间结果因为精度问题出现了溢出。解决办法是在CUBE-AI配置里把数据类型改成纯int8,避免任何浮点运算。所以换芯片的时候,一定要重新检查CUBE-AI的配置,不要直接复制工程。

返回列表