
1. 为什么要在STM32上跑AI模型而不是把数据传到云端很多人第一次听到在STM32上部署AI模型这个说法第一反应是STM32那点主频和内存跑得动吗我一开始也是这个反应。但实际做过几个项目之后我发现这件事不但可行而且在某些场景下比把数据传到云端推理要靠谱得多。先说清楚STM32的定位。它是一颗MCU不是MPU没有MMU没有操作系统裸机或者跑个RTOS主频通常在几十MHz到几百MHz之间SRAM从几KB到几百KBFlash从几十KB到2MB左右。拿它去跑ResNet这种大模型当然不现实但跑一个几层的小型神经网络——比如关键词识别、简单的手势分类、振动异常检测、电机故障判别——是完全够用的。那为什么要在端侧做推理我总结下来有三个实打实的理由。第一是延迟。数据采集和推理在同一个芯片上完成中间不需要经过串口打包、WiFi发送、服务器排队、结果回传这一整条链路。工业场景里一个振动异常检测如果走云端端到端延迟轻松上到几百毫秒甚至秒级而本地推理可以做到毫秒级响应。对于需要快速切断电机的保护类应用这个差距是致命的。第二是隐私和成本。数据不出设备就不存在上传过程中的泄露风险也不需要为每一次推理付云端的算力费用。尤其是批量部署的设备比如几百台分布在现场的监测节点每台都往云端传数据流量费和服务器费用加起来是笔不小的开销。第三是离线可用。现场没有网络、网络不稳定、或者不允许联网的场景太多了。本地推理不依赖任何外部条件上电就能工作。当然STM32跑AI也有它的边界。模型不能太大参数量通常控制在几万到几十万级别输入特征不能太复杂一般是你自己提取好的特征向量而不是原始的高分辨率图像推理框架要足够轻量不能依赖Python运行时。理解这些边界后面的选型和部署才不会走弯路。这篇文章我会把整个流程拆开讲从模型训练、导出ONNX、用STM32Cube.AI转换成C代码、集成到工程里、再到实际调优。中间会重点讲那些文档里不会写、但实际做的时候一定会踩的坑。2. 工具链选型CUBE-AI、ONNX和STM32Cube IDE到底怎么配合2.1 三个工具各自的角色先把这三个东西的关系理清楚不然很容易搞混。ONNX是模型的中间表示格式。你在PyTorch或者TensorFlow里训练完模型不能直接把训练框架的模型文件丢给STM32得先转成一个通用的、与框架无关的格式ONNX就是干这个的。它相当于一个翻译中转站把不同训练框架的模型统一成一种描述方式。STM32Cube.AI现在叫X-CUBE-AI是ST官方提供的转换工具。它读取ONNX文件分析网络结构然后生成可以在STM32上运行的C代码。这个工具会做几件事把浮点权重转成定点或者量化格式、生成推理所需的算子实现、计算内存占用、给出每一层的性能估算。STM32Cube IDE是你最终写业务代码、编译、下载、调试的地方。Cube.AI生成的代码是以库的形式集成进来的你在IDE里调用它提供的API完成推理。三者的关系可以这样理解ONNX是原材料Cube.AI是加工厂Cube IDE是装配车间。2.2 版本匹配这件事比想象中重要我踩过的第一个大坑就是版本不匹配。Cube.AI对ONNX的算子支持是跟着版本走的你用比较新的PyTorch导出的ONNX里面可能包含一些Cube.AI当前版本还不支持的算子转换的时候直接报错。我的建议是先确定Cube.AI的版本再倒推去选PyTorch和ONNX的版本。比如你装的是Cube.AI 8.x那PyTorch用1.13到2.0之间的版本、ONNX用1.13到1.14之间兼容性会好很多。不要一上来就装最新的PyTorch很容易在转换环节卡住。另外Cube.AI既可以作为Cube IDE的插件使用也可以作为独立的命令行工具使用。我个人的习惯是先用命令行工具做一次转换验证确认模型能过再集成到IDE里。命令行工具报错信息更详细排查起来方便。2.3 模型格式的选择浮点还是量化Cube.AI支持几种模型精度浮点32位、浮点16位、定点8位。选择哪种取决于你的芯片有没有FPU浮点运算单元以及你对精度的要求。如果芯片带FPU比如STM32F4、F7、H7系列浮点模型跑起来问题不大但内存占用和Flash占用会比较大。如果芯片没有FPU比如F1、F0系列浮点运算会非常慢这时候就必须用量化模型。定点8位量化是最省资源的方案模型体积能压到浮点的四分之一推理速度也快很多。但量化会带来精度损失需要在训练阶段就做好量化感知训练QAT或者在转换后用一批测试数据验证精度是否可接受。我的经验是先用浮点模型跑通整个流程确认端到端没问题再尝试量化。一上来就搞量化出了问题你分不清是流程问题还是量化问题。3. 从训练到ONNX模型导出环节的实操细节3.1 训练阶段就要为部署做准备很多人训练模型的时候只关心准确率等到要部署了才发现模型结构太复杂、用了不支持的算子、输入输出格式不对。这些问题如果在训练阶段就注意后面能省大量时间。具体来说训练时要注意这几点控制模型规模。全连接层不要堆太多卷积核数量不要太大。一个实用的参考是总参数量控制在10万以内STM32F4级别基本能跑控制在5万以内F1级别也有希望。避免使用冷门算子。像自定义的激活函数、特殊的归一化层Cube.AI很可能不支持。尽量用ReLU、Sigmoid、Tanh这些标准激活函数。固定输入尺寸。动态shape在嵌入式端是灾难输入张量的维度必须完全固定。输入最好是特征向量而不是原始信号。比如做振动检测你在PC端先做好FFT提取频域特征把特征向量作为模型输入而不是把原始时域波形丢进去让模型自己学。这样模型可以做得非常小。3.2 PyTorch导出ONNX的完整代码假设你已经训练好了一个PyTorch模型导出的核心代码如下import torch import torch.onnx # 加载训练好的模型 model MyNet() model.load_state_dict(torch.load(model.pth)) model.eval() # 构造一个符合输入维度的假数据 dummy_input torch.randn(1, 1, 128) # batch1, channel1, length128 # 导出ONNX torch.onnx.export( model, dummy_input, model.onnx, export_paramsTrue, opset_version13, # 建议用11到13兼容性好 do_constant_foldingTrue, input_names[input], output_names[output], dynamic_axesNone # 不要开动态轴 )这里有几个细节值得说。opset_version不要选太高。我试过opset 17导出的模型里有些算子Cube.AI不认。11到13是比较稳的区间。dynamic_axes一定要设成None或者不传。有些教程会让你设置动态batch但在嵌入式端batch永远是1动态轴只会给转换添麻烦。导出之后强烈建议用onnxruntime在PC上跑一遍确认输出和PyTorch一致。这一步能帮你排除掉导出环节的问题避免后面在Cube.AI里排查半天发现是导出就错了。import onnxruntime as ort import numpy as np sess ort.InferenceSession(model.onnx) test_input np.random.randn(1, 1, 128).astype(np.float32) result sess.run(None, {input: test_input}) print(result[0].shape)3.3 用onnxsim做一次图简化导出的ONNX图里经常有一些冗余节点比如多余的Transpose、Identity、Constant节点。这些节点在PC上无所谓但在STM32上会白白消耗内存和算力。用onnx-simplifier做一次简化能去掉不少冗余pip install onnx-simplifier python -m onnxsim model.onnx model_sim.onnx简化后再用onnxruntime验证一遍精度确认没有变化。我遇到过简化后输出有微小差异的情况一般是浮点误差但如果差异很大就要检查是不是简化过程改动了计算逻辑。4. Cube.AI转换从ONNX到C代码的关键步骤4.1 在Cube IDE里启用X-CUBE-AI如果你用的是STM32Cube IDEX-CUBE-AI是以插件形式存在的。安装方式是在IDE的Help菜单里找到Manage Embedded Software Packages然后勾选X-CUBE-AI。安装完成后新建或者打开一个工程在工程上右键就能看到STM32CubeAI相关的选项。我建议先在CubeMX里把芯片型号、时钟、外设配置好生成基础工程再往里加AI部分。不要在一个空工程里直接搞AI外设没配好后面调试很麻烦。4.2 添加模型并分析在Cube.AI的界面里添加你导出的ONNX文件工具会自动分析模型结构给出几个关键指标指标含义关注点MACC乘加运算次数反映计算量越小越好Weights权重占用的Flash决定模型能不能装下Activations中间激活占用的RAM决定运行时内存够不够Flash总占用权重代码对比芯片Flash容量RAM总占用激活栈对比芯片SRAM容量这几个数字出来之后第一件事是对比你的芯片资源。如果Flash或者RAM超了就得回去改模型结构或者换更大资源的芯片。我遇到过一次模型权重只有80KB但激活占了200KB而芯片只有128KB SRAM直接跑不起来。后来把中间层的通道数减半激活降到90KB才通过。所以激活内存往往比权重更容易成为瓶颈尤其是全连接层比较多的模型。4.3 生成代码时的选项Cube.AI生成代码时会有几个选项Report生成一份详细的分析报告包含每一层的资源占用和耗时估算。这个一定要生成调优的时候全靠它。Application template可以生成一个带示例调用的模板方便你快速上手。但正式项目里我一般不用模板自己写调用逻辑更清晰。Compression可以对权重做压缩减少Flash占用但会增加一点解压时间。Flash紧张的时候可以开。生成之后工程里会多出一个X-CUBE-AI目录里面是推理引擎的源码和你的模型数据。4.4 调用推理的代码结构Cube.AI生成的API大致是这样的#include app_x-cube-ai.h #include ai_platform.h // 创建模型实例 ai_handle network AI_HANDLE_NULL; ai_error err ai_my_model_create(network, AI_MY_MODEL_DATA_CONFIG); if (err.type ! AI_ERROR_NONE) { // 创建失败处理 } // 初始化 const ai_handle acts[] { ai_my_model_activations }; ai_my_model_init(network, acts, 1); // 准备输入输出buffer ai_buffer* ai_input ai_my_model_inputs_get(network, NULL); ai_buffer* ai_output ai_my_model_outputs_get(network, NULL); float input_data[128]; float output_data[4]; ai_input[0].data AI_HANDLE_PTR(input_data); ai_output[0].data AI_HANDLE_PTR(output_data); // 执行推理 ai_i32 n_batch ai_my_model_run(network, ai_input, ai_output); if (n_batch ! 1) { // 推理失败处理 }这段代码看起来简单但实际用的时候有几个地方容易出问题。第一ai_input[0].data指向的buffer其数据类型要和模型输入类型匹配。如果模型是浮点的就用float数组如果是量化的就要用int8数组并且要注意量化参数的换算。第二输入数据的预处理要和训练时完全一致。训练时如果做了归一化推理时也要做同样的归一化。我见过有人训练时把数据归一化到0到1推理时忘了做结果输出完全不对。第三推理是阻塞的ai_my_model_run会一直占用CPU直到算完。如果对实时性有要求要么把推理放在低优先级任务里要么用Cube.AI提供的异步接口。5. 实测性能与内存调优那些报告里不会告诉你的事5.1 实测耗时和理论估算的差距Cube.AI的报告里会给一个每层的耗时估算但那个估算是基于理想情况的。实际跑起来耗时会受几个因素影响Flash等待周期。如果CPU主频跑得比Flash访问速度快就需要插入等待周期这会拖慢取指和取数。开ART加速器或者把关键代码放到RAM里执行能缓解。Cache命中率。H7系列有Cache如果模型数据频繁换入换出Cache miss会明显拖慢速度。DMA和CPU争抢总线。如果推理的同时还有DMA在搬数据总线仲裁会带来额外延迟。我的实测经验是报告里的估算值乘以1.5到2是比较接近实际的数字。比如报告说1ms实际可能在1.5到2ms之间。5.2 内存不够时的几个优化方向如果激活内存超了可以按下面的顺序尝试减小中间层通道数。这是最直接有效的办法通道数减半激活内存大致减半。用全局池化替代全连接。全连接层的激活占用往往很大用全局平均池化能显著降低。缩短输入序列长度。如果输入是时序信号把窗口长度从256降到128激活内存也会跟着降。开启Cube.AI的内存复用。Cube.AI默认会做激活内存的复用分析但有时候需要手动调整。在配置里可以指定是否允许复用。换更大RAM的芯片。如果上面都试过了还是不够那就只能换芯片了。F4系列里不同型号的SRAM差别很大选型的时候要留足余量。5.3 量化模型的精度验证如果你用了int8量化一定要做精度验证。方法是在PC上用一批测试数据跑浮点模型记录输出再用同样的数据跑量化模型Cube.AI提供了PC端的验证工具对比两者的输出差异。# 用Cube.AI的Python验证工具 from ai_runner import Runner runner Runner(model_int8.onnx) runner.init() for data, label in test_dataset: output runner.run(data) # 对比output和浮点模型的输出如果精度掉得太多可以考虑在训练时加入量化感知训练、调整量化时的校准数据集、或者对敏感层保持浮点。6. 踩坑实录从转换失败到推理结果不对的完整排查链路6.1 转换阶段报unsupported operator这是最常见的问题。Cube.AI报某个算子不支持你需要在报告里找到是哪个算子。回到PyTorch看这个算子对应的是哪一层。用支持的算子替换它。比如某些自定义的激活函数换成ReLU或者HardSigmoid。重新导出ONNX重新转换。如果实在找不到替代方案可以考虑把这个算子拆成几个基础算子的组合。比如Swish可以拆成Sigmoid乘以x。6.2 转换成功但推理结果全是0或者NaN这种情况通常是输入数据的问题。排查步骤检查输入buffer有没有正确赋值。有时候指针赋了但数据没填进去。检查输入数据的范围。如果模型期望的是归一化后的数据你喂了原始数据输出可能就饱和了。检查量化参数。如果是量化模型输入需要按照量化公式换算成int8输出也要反量化回浮点。这一步很容易搞错。6.3 推理结果在PC上对在STM32上不对如果PC上验证过ONNX模型是对的但STM32上跑出来不对问题通常出在数据预处理不一致。PC上验证时用的预处理和STM32上的预处理要完全一样。浮点精度差异。PC上是双精度STM32上是单精度某些数值敏感的模型会有差异。可以在PC上用float32验证一下。内存越界。激活buffer或者输入输出buffer越界会破坏其他数据。检查一下Cube.AI报告里的内存占用确认栈空间够不够。6.4 推理速度比预期慢很多除了前面说的Flash等待周期和Cache问题还有一个容易被忽略的点编译优化等级。Cube IDE默认可能是-O0或者-Og改成-O2或者-O3能明显提升推理速度。但要注意高优化等级可能会让调试变困难建议在最终版本再用。另外如果芯片支持开启指令Cache和数据Cache对推理速度提升也很明显。7. 这套方案适合什么场景不适合什么场景做了几个项目之后我对STM32跑AI的适用边界有了比较清晰的认识。适合的场景输入是低维特征向量几十到几百维、模型是浅层网络几层到十几层、对延迟敏感、需要离线运行、批量部署对成本敏感。典型应用包括电机故障检测、简单的手势识别、关键词唤醒、传感器数据分类。不适合的场景输入是高分辨率图像、模型是深层网络几十层以上、需要频繁更新模型、对精度要求极高。这些场景还是得上MPU或者带NPU的专用芯片。STM32跑AI不是要替代云端而是在特定场景下提供一个更合适的方案。理解它的能力边界比盲目追求把大模型塞进MCU要有意义得多。最后分享一个我在实际项目里养成的习惯每次改完模型结构先在PC上用onnxruntime跑一遍完整测试集确认精度没问题再走Cube.AI转换。这样能把问题定位在训练侧还是部署侧省掉大量来回折腾的时间。