4. 模型转换与验证:从ONNX到STM32的最后一公里

模型训练好了,量化也做完了,接下来就是最关键的环节——把它变成STM32能跑的东西。这一步我称之为“最后一公里”,很多项目就是在这段路上翻的车。

STM32Cube.AI这个工具,说白了就是一个“翻译官”。它把你的神经网络模型翻译成C代码,让MCU能理解、能执行。但翻译质量怎么样?占多少内存?跑一次要多久?这些都得靠后面的分析报告和仿真验证来回答。

4.1 使用STM32Cube.AI转换工具

我个人习惯用命令行方式调用Cube.AI,这样方便集成到自动化流程里。当然,X-CUBE-AI的图形界面也很直观,适合第一次上手。

4.1.1 命令行转换

先装好X-CUBE-AI扩展包,然后打开命令行工具。基本命令长这样:

stm32ai generate -m model.onnx -o output_dir --compression 1.0 --allocate-inputs --allocate-outputs

参数说明:

  • -m:输入模型文件,支持ONNX、Keras、TFLite等格式
  • -o:输出目录,生成的C代码会放在这里
  • --compression:压缩因子,1.0表示不压缩,越小压缩越狠
  • --allocate-inputs/outputs:让工具自动分配输入输出缓冲区
我的习惯:第一次转换先用默认参数,看看报告再说。别一上来就调压缩,容易把模型搞残。

4.1.2 图形界面操作

如果你更喜欢点鼠标,可以在STM32CubeMX里打开X-CUBE-AI插件。流程很简单:

  1. 新建或打开一个STM32工程
  2. 在"Software Packs"里选择X-CUBE-AI
  3. 点击"Add Network",导入你的模型文件
  4. 配置输入输出形状、量化参数
  5. 点击"Analyze"生成报告
  6. 点击"Generate Code"生成C代码

嗯,这里要注意:图形界面生成的代码和命令行是一样的,只是操作方式不同。我建议团队里统一用一种方式,避免版本混乱。

4.2 模型分析报告解读

转换完成后,Cube.AI会生成一份分析报告。这份报告就是你的“体检单”,告诉你模型在MCU上到底行不行。

报告里最重要的三个指标:RAM占用、Flash占用、推理周期数。我一个个说。

4.2.1 RAM占用

RAM主要分三块:

  • 输入输出缓冲区:模型输入和输出的数据存放区
  • 中间激活值:每层计算产生的临时数据
  • 权重缓冲区:如果权重放在RAM里(比如做动态量化),这部分也要算

举个例子,我做过一个MobileNetV2的移植,输入是224x224x3,输出是1000类。报告里RAM占用大概是这样:

内存区域 大小(KB) 说明
输入缓冲区 150.5 224*224*3*1字节(INT8)
输出缓冲区 1.0 1000*1字节
中间激活值 892.3 各层激活值总和,峰值在瓶颈层
权重缓冲区 0 权重放Flash,不占RAM
总计 1043.8 约1MB
我曾经踩过的坑:有一次报告显示RAM只用了800KB,我心想STM32H743有1MB RAM,够用。结果实际跑起来就崩了。后来发现,报告里没算系统堆栈、RTOS任务栈、DMA缓冲区这些开销。所以,报告数值要打7折,留出余量。

4.2.2 Flash占用

Flash主要存两样东西:权重参数和代码。

权重参数占大头,尤其是全连接层。代码部分包括Cube.AI生成的推理引擎、激活函数实现、内存管理函数等。

我见过一个极端案例:一个ResNet-50模型,权重用FP32存,光权重就占了98MB。而STM32F4的Flash才1MB,根本放不下。后来量化到INT8,权重降到24.5MB,还是放不下。最后换成MobileNetV3,才勉强塞进去。

所以,看Flash占用的时候,先看权重部分。如果超了,要么换小模型,要么做更狠的量化或剪枝。

4.2.3 推理周期数

周期数就是模型跑一次需要多少个CPU时钟周期。结合主频,就能算出推理时间。

公式很简单:推理时间(ms) = 周期数 / 主频(Hz) * 1000

比如周期数是50,000,000,主频是400MHz,那推理时间就是125ms。嗯,这个速度对于实时控制来说有点慢,得优化。

报告里还会给出每层的周期数分布,方便你找瓶颈。我一般先看卷积层,尤其是depthwise卷积,在STM32上效率不高。

关键指标速查:
  • RAM占用 < 芯片RAM的70%
  • Flash占用 < 芯片Flash的80%
  • 推理周期数 < 应用要求的实时性阈值

4.3 PC端仿真验证

报告看着没问题,不代表实际跑起来就没问题。我习惯先在PC上做仿真验证,确认模型转换后的数值精度。

Cube.AI提供了PC仿真库,可以在Windows/Linux上运行生成的C代码,和MCU上跑的效果一模一样。

4.3.1 仿真环境搭建

步骤很简单:

  1. 从X-CUBE-AI安装目录找到PC_Simulator文件夹
  2. 把生成的C代码拷贝进去
  3. 用CMake编译,生成可执行文件
  4. 准备测试数据(二进制格式,和模型输入对齐)
  5. 运行仿真,输出推理结果

我一般写个Python脚本,把仿真结果和原始模型(比如PyTorch)的输出做对比,算一下余弦相似度或均方误差。

# 对比脚本示例
import numpy as np

# 加载原始模型输出(PyTorch)
orig_output = np.load('pytorch_output.npy')

# 加载仿真输出(Cube.AI)
sim_output = np.fromfile('sim_output.bin', dtype=np.int8)

# 反量化(如果模型是INT8)
scale = 0.0039
zero_point = 128
sim_output_float = (sim_output.astype(np.float32) - zero_point) * scale

# 计算余弦相似度
cos_sim = np.dot(orig_output.flatten(), sim_output_float.flatten()) / (
    np.linalg.norm(orig_output.flatten()) * np.linalg.norm(sim_output_float.flatten())
)
print(f'Cosine similarity: {cos_sim:.4f}')
我的经验:余弦相似度在0.99以上,说明量化精度损失很小。如果低于0.95,就要检查量化参数或者模型结构了。

4.3.2 常见问题排查

仿真过程中,我遇到过几个典型问题:

  • 输出全是0:检查输入数据格式,是不是字节序不对?或者量化参数没对齐?
  • 输出全是128:INT8量化时,128对应0值。可能是权重初始化有问题,或者模型根本没训练好。
  • 输出和原始模型差很多:先确认量化校准数据集是否覆盖了实际场景。我有个项目用白天数据校准,结果晚上识别就崩了。

仿真验证通过后,我才会把代码烧到板子上。这一步虽然麻烦,但能省掉很多调试时间。你想想看,在板子上用串口打印调试信息,哪有在PC上跑Python脚本方便?

4.4 本章知识体系

下面这张图总结了模型转换与验证的完整流程:

模型转换与验证流程 原始模型(ONNX/Keras) STM32Cube.AI 转换 分析报告 生成C代码 RAM/Flash/周期数 是否达标? PC端仿真验证 通过? → 烧录到板子 不通过则返回调整:换模型 / 改量化 / 剪枝 / 调内存分配 直到RAM、Flash、周期数全部达标,仿真精度符合要求

整个流程是个闭环。报告不达标就回去改模型或量化参数,仿真精度不对就检查数据或量化配置。别想着一步到位,我做过最顺利的项目也迭代了3轮。