MCU 卷积神经网络部署 · 深度技术指南

MCU 卷积神经网络部署 · 深度技术指南

作者:智汇嵌入式实验室 · 7yewh

本指南以实际部署代码为核心,深入解析 CNN 在 Cortex-M4 MCU 上的完整推理流程。

内容基于 ST Edge AI 生成的代码(app_x-cube-ai.cnetwork.h)和实际优化经验。

注意 :本文档中的模型参数(输入/输出维度、通道数等)为教学示例,

实际部署时请以 ST Edge AI 生成的 network.h 宏定义为准。


目录

  1. 整体流程与递进关系
  2. [模型分析 --- 能否部署到 MCU](#模型分析 — 能否部署到 MCU)
  3. 网络层逐层解析
  4. 硬件预算详解
  5. 生成代码深度走读
  6. 固件集成实战
  7. 推理速度优化(实战记录)
  8. [PC 端对比验证](#PC 端对比验证)
  9. 架构对比与未来方向

第1章 整体流程与递进关系

1.1 五步部署流程

复制代码
Step 1  训练模型(PyTorch)
        | 产出:.pt / .pth 权重文件
        | 关键:模型结构必须对 MCU 友好(常规算子、静态 shape)
        v
Step 2  导出 ONNX                      <-- 最容易翻车的关卡
        | 产出:model.onnx
        | 关键:opset=13、dynamic_axes=None、输入 4D 固定
        v
Step 3  工具链转换(ST Edge AI Studio) <-- 另一个关卡
        | 产出:network.c/.h(推理图)+ network_data.c(权重)
        | 关键:算子全部在白名单内才能成功
        v
Step 4  MCU 工程集成                    <-- 工程量最大
        | 产出:可编译的完整 Keil/CMake 工程
        | 关键:实现 acquire_and_process_data() 和 post_process()
        v
Step 5  验证(PC 对比 + 上板)          <-- 最终检验
        | 工具:verify_mcu_vs_onnx.py
        | 关键:PC 端 ONNX 输出 ≈ MCU 推理输出(误差 < 5%)

1.2 为什么是这个顺序

每一步的输入 是上一步的输出,不能跳步:

  • 没有正确的 ONNX,工具链就没法转换
  • 没有转换后的 C 代码,MCU 工程就没法集成
  • 没有集成完,就没有 MCU 输出可以验证

哪步最重要?

Step 2(ONNX 导出)和 Step 4(集成)最容易出问题:

  • Step 2:模型结构设计错了,整个链路都得推倒重来(常见坑:原始 5D 模型直接失败)
  • Step 4:两个桩函数没实现,数据预处理写错,MCU 跑起来但输出全是错的

1.3 ST Edge AI 对模型的要求

要求 说明
文件格式 ONNX(推荐)、Keras .h5、TFLite 等
输入维度 最多 4D,格式 [N, C, H, W],N(batch)必须 =1 且固定
shape 必须静态 所有维度在导出时固定,不能有动态维度(dynamic_axes=None
opset 版本 ONNX opset 7~20(推荐 13)
算子白名单 Conv / FC / ReLU / Sigmoid / Flatten / Transpose 等常规算子
数值格式 float32(工具内部可选做 int8 量化)

工具根据目标芯片自动决定运行时库版本、激活缓冲区大小、是否启用 SIMD 优化。


第2章 模型分析

2.1 关键信息速查

以下为示例模型的典型参数(实际值以 network.h 生成宏为准):

属性 示例值 对应宏名
输入形状 [1, 6, 10, 8](6 通道,10 行,8 列) STAI_NETWORK_IN_1_SHAPE
输出形状 [1, 4](Fx, Fy, Fz, Fn 四个力分量) STAI_NETWORK_OUT_1_SHAPE
网络节点数 12(含 Transpose/ReLU/Sigmoid) STAI_NETWORK_NODES_NUM
权重大小 253,072 字节(约 247 KB) STAI_NETWORK_WEIGHT_1_SIZE_BYTES
总 MACs 约 776,000 STAI_NETWORK_MACC_NUM
激活缓冲区 11,520 字节 STAI_NETWORK_ACTIVATION_1_SIZE_BYTES
输入大小 1,920 字节(480 x float32) STAI_NETWORK_IN_1_SIZE_BYTES
输出大小 16 字节(4 x float32) STAI_NETWORK_OUT_1_SIZE_BYTES

2.2 MCU 友好度检查

MCU 喜欢的模型特征(部署前必须确认全部满足):

  • 静态输入尺寸(所有维度固定,无动态 shape)
  • 常规算子:Conv / FC / ReLU / Sigmoid / Flatten / Transpose
  • 单分支线性图(无 Attention、无动态索引、无控制流)

2.3 不支持的算子(导出时需避免)

不支持的算子 原因 替代方案
Gather(动态索引) 运行时确定索引,工具链无法推导 shape 逻辑搬到 MCU 固件中
Reshape(allowzero=1) PyTorch 默认导出,ST Edge AI 不认 导出时指定 allowzero=0 或用 Flatten
LayerNorm / GroupNorm 需要运行时计算均值方差 用 BatchNorm(可融合进 Conv)
Attention(QKV) 矩阵乘尺寸是序列长度的平方 用 CNN 替代
LSTM / GRU 有隐状态,运行时不一定支持 用 TCN 或多帧展开
5D+ 张量 超过 4D 限制 导出前 reshape 成 4D
Loop / If 静态图不支持控制流 条件逻辑写在固件里

第3章 网络层逐层解析

3.1 完整网络结构(从 ONNX 图推算)

复制代码
输入: [1, 6, 10, 8]  (batch=1, 6通道, 10行, 8列, float32 = 1,920 B)
       |
  [Transpose] NCHW -> NHWC
       |
       v
  Layer 1: Conv1  W<24x6x3x3>  stride=1, padding=1
    H_out = (10-3+2x1)/1+1 = 10
    W_out = (8-3+2x1)/1+1 = 8
    输出: [1, 10, 8, 24] = 7,680 B
    MACs: 3x3x6x24x10x8 = 103,680
       |
  [ReLU] max(0, x)
       |
       v
  Layer 3: Conv2  W<24x24x3x3>  stride=1, padding=1
    输出: [1, 10, 8, 24] = 7,680 B
    MACs: 3x3x24x24x10x8 = 414,720      <-- 主要算力瓶颈(53.4%)
       |
  [ReLU]
       |
       v
  Layer 5: Conv3  W<48x24x3x3>  stride=2, padding=1     <-- 关键!步长=2
    H_out = (10-3+2)/2+1 = 5    <-- 减半
    W_out = (8-3+2)/2+1 = 4     <-- 减半
    输出: [1, 5, 4, 48] = 3,840 B
    MACs: 3x3x24x48x5x4 = 207,360
       |
  [ReLU]
       |
       v
  Layer 7: Flatten  48x5x4 = 960 -> [1, 960] = 3,840 B
    数据不动,只是从三维数组换成一维数组读
       |
       v
  Layer 8: Dense1 (Gemm)  W<48x960>
    960个输入 x 48个输出 = 46,080次 MAC
    权重: 46,128 x 4B = 180.2 KB(占总权重 71.2%)    <-- 权重瓶颈
    输出: [1, 48] = 192 B
       |
  [ReLU]
       |
       v
  Layer 10: Dense2 (Gemm)  W<4x48>
    输出: [1, 4] = 16 B
       |
  [Sigmoid] 1/(1+e^-x)  压到 [0, 1]
       |
       v
  输出: [Fx_norm, Fy_norm, Fz_norm, Fn_norm]  ∈ [0, 1]
  -> 反归一化后得到实际力值

3.2 各算子详解

Conv2D(二维卷积)--- 提取空间特征

用一个 3x3 的"滤镜"在输入上滑动扫描,每滑一格做一次乘加运算。多个滤镜检测不同特征。

以 Conv1 为例,用 C 语言理解计算过程:

c 复制代码
/* Conv1: 输入 [10,8,6] -> 输出 [10,8,24] */
for (k = 0; k < 24; k++)          /* 24 个输出通道(特征检测器) */
    for (r = 0; r < 10; r++)      /* 每行 */
        for (c = 0; c < 8; c++)   /* 每列 */
            sum = bias[k];
            for (i = 0; i < 3; i++)    /* 3x3 卷积核 */
                for (j = 0; j < 3; j++)
                    for (ch = 0; ch < 6; ch++)  /* 6 个输入通道 */
                        sum += weight[k][i][j][ch] * input[r+i][c+j][ch];
            output[r][c][k] = sum;     /* 这就是一个 MAC */

输出尺寸公式:(input - kernel + 2 x padding) / stride + 1

ReLU(线性整流)--- 激活函数
c 复制代码
output = (input > 0) ? input : 0;  /* max(0, x) */

作用:引入非线性。没有它,多层卷积叠加仍等价于一层线性变换,模型无法学习复杂规律。

Flatten(展平)--- 多维转一维

Conv3 输出 [1, 5, 4, 48] 是三维张量,全连接层需要一维输入:

复制代码
[1, 5, 4, 48]  ->  [1, 960]     5 x 4 x 48 = 960

C 语言理解:把三维数组用指针当一维数组访问,数据内存地址不变,只是访问方式变了。

Dense / Gemm(全连接)--- 综合所有特征

输入每个值都与输出每个值有连接。本质是矩阵乘法:

c 复制代码
/* Dense1: 输入 [960] -> 输出 [48] */
for (j = 0; j < 48; j++) {          /* 48 个输出神经元 */
    sum = bias[j];
    for (i = 0; i < 960; i++)       /* 与 960 个输入全连接 */
        sum += weight[j][i] * input[i];   /* 一次 MAC */
    output[j] = sum;
}

Dense1 是权重最大的层(180 KB,占 71.2%),因为 960 x 48 = 46,080 个参数。

Sigmoid(输出激活)--- 归一化到 [0, 1]
复制代码
output = 1 / (1 + e^(-x))

x = -inf  ->  output ≈ 0.0
x =  0    ->  output = 0.5
x = +inf  ->  output ≈ 1.0

本模型用 Sigmoid 是因为输出要表示"归一化的力值"(0~1 范围),MCU 再做反归一化映射到实际值。

3.3 归一化 / 反归一化的因果关系

问题根源:传感器数值和网络期望的数值范围不匹配。

复制代码
原因1:ADC 输出整数,范围 [0, 4095](12位 ADC)

原因2:神经网络训练时用归一化后的数据(0~1 范围)
       网络学到:"输入=0.5 -> 力应输出=0.8"
       不是:"输入=2048 -> 力应输出=3277"

原因3:Sigmoid 输出范围天然 [0, 1],不可能输出 2048 这样的大数

结论:MCU 必须在推理前把数据压到 [0,1](归一化)
     推理后再把 [0,1] 还原为实际力值(反归一化)

具体数字说明:

复制代码
第一步:归一化(输入前)
  ADC 读到: 2048(整数,0~4095)
  / 4095.0
  -> 0.5001(浮点,0~1)  <-- 网络看到的输入

第二步:网络推理
  输入 [1, 6, 10, 8] 的归一化数据
  -> 输出 [0.82, 0.47, 0.91, 0.65]  <-- Sigmoid 保证在 0~1

第三步:反归一化(输出后)
  Fz 单向力,范围 [0, 4095]:
    Fz = 0.91 x 4095 = 3726
  Fx/Fy 双向力,范围 [-4095, +4095]:
    Fx = (0.82 - 0.5) x 2 x 4095 = +2621
    Fy = (0.47 - 0.5) x 2 x 4095 = -246
  Fn 法向力,范围 [0, 4095]:
    Fn = 0.65 x 4095 = 2662

总结: 模型在 [0,1] 世界里学习和输出,传感器在 [0,4095] 世界里工作。归一化 = 翻译成网络的语言;反归一化 = 翻译回硬件的语言。


第4章 硬件预算详解

4.1 总体资源占用

资源 STM32G4 总量 AI 占用 剩余
Flash 512 KB 247 KB 权重 + 6 KB 代码 = 253 KB 259 KB
主 SRAM(0x2000_0000) 96 KB 11.5 KB 激活缓冲区 84.5 KB
CCMRAM(0x1000_0000) 32 KB 可放激活缓冲区 + 帧缓冲区 ---
FPU 单精度 float32 float32 推理有硬件加速 ---
DSP 扩展(SIMD) 2 路 int16 ST-AI 运行时已利用 ---
D-Cache / I-Cache app_config.h 必须设为 0 ---

4.2 激活缓冲区怎么算出来的

推理时每层需要同时持有输入和输出,但上一层算完后输入可以释放。ST Edge AI 分析所有层的内存占用,找到峰值最大的时刻:

c 复制代码
#define STAI_NETWORK_ACTIVATION_1_SIZE_BYTES (11520)  /* ST-AI 自动计算 */

峰值出现在 Conv1(输入 + 输出同时存在):

复制代码
计算 Conv1 时:
  输入 buffer:1x6x10x8x4B   = 1,920 B   <-- 还在读,不能释放
  输出 buffer:1x10x8x24x4B  = 7,680 B   <-- 正在写
  临时工作区 + 对齐开销       ≈ 1,920 B
  -----------------------------------------
  峰值合计                   = 11,520 B   <-- 就是这个数

4.3 Overlay(覆盖复用)原理

所有层共用同一块 11,520 字节缓冲区,像流水线一样复用:

复制代码
时刻1: [  输入 1,920B  |  Conv1输出 7,680B  |  工作区  ]  <-- 峰值 11,520B
时刻2: [  Conv1输出(现为Conv2输入)  |  Conv2输出(覆盖写)  ]
时刻3: [  Conv3输出 3,840B  |          空闲           ]
时刻4: [  Dense 192B | 空 ]
...

结果:整个推理只需这一块 RAM,不是每层单独分配

在代码中对应:

c 复制代码
/* app_x-cube-ai.c */
STAI_ALIGNED(32)
static uint8_t heap_overlay_pool[STAI_NETWORK_ACTIVATION_1_SIZE_BYTES];
stai_ptr data_activations[] = { heap_overlay_pool };

4.4 权重分布

参数量 权重大小 占比
Conv1 (6->24) 6x24x3x3 + 24 = 1,320 5.2 KB 2.1%
Conv2 (24->24) 24x24x3x3 + 24 = 5,208 20.3 KB 8.2%
Conv3 (24->48) 24x48x3x3 + 48 = 10,416 40.7 KB 16.5%
Dense1 (960->48) 960x48 + 48 = 46,128 180.2 KB 71.2%
Dense2 (48->4) 48x4 + 4 = 196 0.8 KB 0.3%
合计 63,268 247 KB 100%

Dense1 占 71.2% 权重是全连接层的本质代价:960 个输入 x 48 个输出,每一对都有一个权重。

4.5 MACs 手算验证

Conv 层:kernel_H x kernel_W x in_ch x out_ch x output_H x output_W

复制代码
Conv1: 3x3 x 6  x 24 x 10x8 = 103,680
Conv2: 3x3 x 24 x 24 x 10x8 = 414,720   <-- 最耗时!
Conv3: 3x3 x 24 x 48 x 5x4  = 207,360   (stride=2,输出 5x4)
Dense1:     960 x 48          =  46,080
Dense2:      48 x 4           =     192
-----------------------------------------------
合计                            772,032
+ Transpose/ReLU/Sigmoid/偏置  ≈  +3,968
≈ 776,000

4.6 数值格式对比

格式 每参数字节 Dense1 大小 全模型大小 适用场景
float32 4 B 180 KB 247 KB 有 FPU,Flash 放得下
int8 1 B 45 KB 62 KB 需 SIMD 加速,精度可接受
float16 2 B 90 KB 124 KB Cortex-M4 不支持原生 f16

第5章 生成代码深度走读

5.1 文件职责一览

文件 来源 是否需要修改 作用
app_x-cube-ai.c ST Edge AI 模板 --- 实现桩函数 推理初始化 + 运行 + 数据桥接
app_x-cube-ai.h 模板 声明 Init/Process/Deinit 入口
app_config.h 模板 --- 修正平台参数 D-Cache/I-Cache/ExtRAM 配置
bsp_ai.h 模板 BSP 桥接:HAL + AI 头文件
network.h 自动生成 网络规格宏(尺寸/MACs/shape)
network.c 自动生成 推理图实现(算子调用序列)
network_data.c 自动生成 权重数据(const 存 Flash)
stai.h ST-AI SDK 平台 API 类型和常量定义

5.2 内存布局分析

c 复制代码
/* === 网络上下文(不透明结构体,由 ST-AI 内部管理) === */
STAI_NETWORK_CONTEXT_DECLARE(network_context, STAI_NETWORK_CONTEXT_SIZE);
/* 包含:magic, signature, flags, return_code, callback,
   activations[1], weights[1], inputs[1], outputs[1] */

/* === 激活缓冲区(Overlay 复用池) === */
STAI_ALIGNED(32)
static uint8_t heap_overlay_pool[STAI_NETWORK_ACTIVATION_1_SIZE_BYTES];

/* === I/O 指针(由 aiInit 填充) === */
static stai_ptr stai_input[STAI_NETWORK_IN_NUM];
static stai_ptr stai_output[STAI_NETWORK_OUT_NUM];

I/O 缓冲区采用 allocate-inputs / allocate-outputs 模式:ST-AI 内部分配缓冲区,通过 stai_network_get_inputs/get_outputs 获取地址。代码中 stai_network_set_inputs/set_outputs 被注释掉,就是这个原因。

5.3 初始化流程 6 步详解

c 复制代码
int aiInit(void) {
    /* Step 1: 初始化运行时库
     * 加载 NetworkRuntime_CM4_GCC.a 中的内核函数表 */
    stai_runtime_init();

    /* Step 2: 初始化网络上下文
     * 将 network.c 中的拓扑结构绑定到 network_context */
    stai_network_init(network_context);

    /* Step 3: 绑定激活缓冲区
     * 告诉运行时中间计算结果放在哪块 RAM
     * 这块 RAM 被所有层 Overlay 复用 */
    stai_network_set_activations(network_context, data_activations,
                                 STAI_NETWORK_ACTIVATIONS_NUM);

    /* Step 4: 获取输入缓冲区地址
     * stai_input[0] 将指向运行时内部分配的输入 buffer */
    stai_network_get_inputs(network_context, stai_input, &_in_size);

    /* Step 5: 获取输出缓冲区地址
     * stai_output[0] 将指向运行时内部分配的输出 buffer */
    stai_network_get_outputs(network_context, stai_output, &_out_size);

    return 0;
}

5.4 推理执行

c 复制代码
int aiRun() {
    stai_return_code ret_code;

    /* 同步阻塞推理:从 stai_input 读取数据,经过 12 层计算,结果写入 stai_output */
    ret_code = stai_network_run(network_context, STAI_MODE_SYNC);

    if (ret_code != STAI_SUCCESS) {
        ret_code = stai_network_get_error(network_context);  /* 获取详细错误码 */
    }
    return 0;
}

STAI_MODE_SYNC:阻塞直到推理完成。MCU 单核场景推荐。

5.5 桩函数实现参考

生成代码中 acquire_and_process_data()post_process()空函数,需要开发者根据实际硬件实现。

数据采集(输入前处理)
c 复制代码
int acquire_and_process_data() {
    float *input = (float *)stai_input[0];

    /* 从传感器采集数据,归一化后填入输入缓冲区
     * 示例:6 通道 x 10 行 x 8 列 = 480 个 float32 */
    for (int ch = 0; ch < 6; ch++)
        for (int r = 0; r < 10; r++)
            for (int c = 0; c < 8; c++)
                input[ch * 80 + r * 8 + c] = adc_value / 4095.0f;  /* 归一化 [0,1] */

    return 0;
}
输出后处理(反归一化)
c 复制代码
int post_process() {
    float *output = (float *)stai_output[0];

    /* Sigmoid 输出 [0,1] -> 反归一化为实际力值 */
    float Fx = (output[0] - 0.5f) * 2.0f * 4095.0f;  /* 双向力 [-4095, +4095] */
    float Fy = (output[1] - 0.5f) * 2.0f * 4095.0f;  /* 双向力 */
    float Fz = output[2] * 4095.0f;                    /* 单向力 [0, 4095] */
    float Fn = output[3] * 4095.0f;                    /* 法向力 [0, 4095] */

    /* 发送到通信总线 或 存入结果结构体 */
    return 0;
}

5.6 主循环改造

生成代码的 main_loop() 包含 while(1) 死循环,必须去掉

c 复制代码
/* 原始(不可用) */
void main_loop() {
    while (1) {                 /* <-- AI 独占 CPU,其他功能全部停摆 */
        acquire_and_process_data();
        aiRun();
        post_process();
    }
}

/* 改造后:在固件主循环中单次调用 */
while (1) {
    /* 传感器采集 */
    sensor_acquisition();

    /* AI 推理(单次) */
    acquire_and_process_data();
    aiRun();
    post_process();

    /* 通信发送 */
    can_send_data();
}

第6章 固件集成实战

6.1 集成清单

序号 步骤 操作 关键点
1 添加源文件 network.c, network_data.c, app_x-cube-ai.c 加入工程编译
2 添加头文件路径 AI/App/, generated/, Middlewares/ST/AI/Inc/ 编译器 Include Path
3 链接运行时库 NetworkRuntime_CM4_GCC.a 加入链接器输入
4 修正 app_config.h D-Cache=0, I-Cache=0, ExtRAM=0, Overdrive=0 G4 无这些硬件
5 调用初始化 aiInit()main() 初始化阶段 必须在推理前调用
6 实现桩函数 acquire_and_process_data() + post_process() 归一化 + 反归一化
7 去掉 while(1) main_loop() 改为单次调用 不能让 AI 独占 CPU
8 CCMRAM 优化 heap_overlay_pool 放入 .ccmram 修改链接脚本
9 开启 Flash 预取 PREFETCH_ENABLE = 1 减少 Flash 等待周期影响
10 验证 比对 PC 端 ONNX 输出 误差应 < 5%

6.2 app_config.h 修正

ST Edge AI 模板默认面向 STM32N6/H7 高端芯片,4 个配置必须改为 0:

c 复制代码
/* 改后(适配 STM32G4) */
#define USE_MCU_DCACHE      0   /* G4 无 D-Cache */
#define USE_MCU_ICACHE      0   /* G4 无 I-Cache */
#define USE_EXTERNAL_RAM    0   /* G4 无外部 RAM */
#define USE_OVERDRIVE       0   /* G4 无超频模式,最高 170MHz */

不修正会导致运行时初始化失败或访问不存在的硬件寄存器。

6.3 CCMRAM 链接脚本配置

c 复制代码
/* 链接脚本新增 CCMRAM 区域 */
MEMORY {
  RAM    (xrw) : ORIGIN = 0x20000000, LENGTH = 112K
  CCMRAM (rw)  : ORIGIN = 0x10000000, LENGTH = 32K   /* CPU 专用,零等待 */
  FLASH  (rx)  : ORIGIN = 0x08000000, LENGTH = 512K
}

SECTIONS {
  .ccmram : {
    *(.ccmram)
    *(.ccmram*)
  } >CCMRAM
}

源码中标记需要放入 CCMRAM 的缓冲区:

c 复制代码
/* 激活缓冲区 -> CCMRAM,零等待访问 */
static uint8_t ai_activation_pool[STAI_NETWORK_ACTIVATION_1_SIZE_BYTES]
    __attribute__((section(".ccmram")));

注意: CCMRAM 不能被 DMA 访问,只能放 CPU 独占的数据。传感器 DMA 缓冲区必须留在主 SRAM。

6.4 ONNX 导出要点

python 复制代码
import torch

model.eval()
dummy = torch.randn(1, 6, 10, 8)  # 单帧 4D 输入
torch.onnx.export(
    model, dummy, "model.onnx",
    opset_version=13,             # ST Edge AI 要求 >= 11
    dynamic_axes=None,            # 禁用动态维度
    do_constant_folding=True,     # 常量折叠优化
    input_names=["input"],
    output_names=["output"],
)

第7章 推理速度优化

7.1 性能瓶颈分析

复制代码
示例模型参数:约 776,000 MACs,float32,12 层
MCU:STM32G4,Cortex-M4F
     FPU 单精度(1 float MAC/cycle)
     无 D-Cache/I-Cache(但有 ART Accelerator)
     Flash 4 等待周期

理论下限:776K MACs / 160MHz = 4.9ms(纯算力,无访存延迟)
实际测量:远高于理论值

差距原因:
  1. Flash 读权重有 4 个等待周期(权重约 247KB 全在 Flash)
  2. 主 SRAM 访问有 AHB 总线争用(DMA 也在用)
  3. 中断打断(FDCAN、TIM 等)
  4. 非推理代码开销(传感器采集、数据打包、通信发送)

7.2 已实施优化

优化1:CCMRAM 放置关键缓冲区

原理: CCMRAM(0x10000000,32KB)通过 CPU 专用 D-bus 直连,零等待周期,且不与 DMA 争用 AHB 总线。

c 复制代码
/* 激活缓冲区移到 CCMRAM */
static uint8_t ai_activation_pool[STAI_NETWORK_ACTIVATION_1_SIZE_BYTES]
    __attribute__((section(".ccmram")));

效果: 激活读写零等待,减少 AHB 总线争用,提速约 10-15%。

优化2:开启 Flash 预取(ART Accelerator)

原理: ART Accelerator 的预取缓冲区可在 CPU 执行当前指令时提前从 Flash 加载下一条。

c 复制代码
/* stm32g4xx_hal_conf.h */
#define  PREFETCH_ENABLE              1U   /* 改前是 0,必须开启 */
#define  INSTRUCTION_CACHE_ENABLE     1U   /* 已开 */
#define  DATA_CACHE_ENABLE            1U   /* 已开 */

效果: 减少 Flash 4 等待周期的影响,提速约 10-20%。

7.3 评估过但未采用的方案

帧跳过(每 N 帧推理一次)

未采用原因: 业务要求每帧都必须推理,跳帧导致力输出有延迟,反馈不连续。

int8 量化(Post-Training Quantization)
复制代码
预期效果:
  权重 247 KB -> 约 62 KB
  推理速度提升 4-8 倍(SIMD 对 int8 更友好)

未采用原因:力值精度要求高,int8 存在精度损失风险

备注:如果未来要启用 int8 量化,固件代码完全不用改,
只需替换 generated/ 下的 network.c、network.h、network_data.c

7.4 ST-AI 优化策略选项

策略 含义 代码体积 推理速度
size 优先缩小生成代码 最小 最慢
balanced 平衡(默认) 中等 中等
speed 优先最快推理 最大(循环展开) 最快

7.5 中断对推理的影响

复制代码
MCU 单核处理器,推理在主循环顺序执行。

无中断:
  t=0ms: Conv1 -> Conv2 -> ... -> 推理完成

有高优先级中断每 1ms 打断:
  推理被反复打断,总时间可能膨胀数倍

解决方案:
  1. 推理期间临时关闭低优先级中断
  2. 使用 DMA 处理外设(主循环不等待)
  3. 合理设置中断优先级

7.6 优化效果汇总

优化项 状态 预期加速 说明
CCMRAM 放置缓冲区 已实施 10-15% 零等待访问
Flash PREFETCH 已实施 10-20% 减少 Flash 等待
I-Cache / D-Cache 已开启 含在基准内 HAL_CONF 中已启用
优化选 "Time" 可尝试 5-15% CubeAI 生成时选 Time
int8 量化 暂不用 4-8 倍 精度要求高
帧跳过 不用 N 倍 业务要求每帧推理
训练更小模型 未来方向 2-8 倍 减通道/DepthwiseConv

7.7 根本性加速方向(需重新训练模型)

MACs 数量是瓶颈根因。MCU 端优化只能挤出 20-40%,质的飞跃需从模型架构入手:

复制代码
方案A:减少 Conv 通道数
  Conv: 24->12,Dense: 48->16
  MACs 可减少 4-7 倍

方案B:用 DepthwiseConv 替代 Conv2D
  MobileNet 风格:DepthwiseConv + PointwiseConv
  MACs 减少 3-8 倍,精度损失通常很小

方案C:减少输入窗口
  减少输入帧数,计算量减少约 30%
  需重新训练,可能影响时序特征提取能力

第8章 PC 端对比验证

8.1 四级对比链

复制代码
基准1: PyTorch 原始输出          <-- 训练基准
          | 对比:导出是否正确
基准2: ONNX Runtime 输出        <-- verify_mcu_vs_onnx.py
          | 对比:量化损失多少
基准3: 量化后 int8 输出          <-- 如果做了量化
          | 对比:MCU 推理是否正确
基准4: MCU 推理输出(通信打印)  <-- 最终目标

8.2 验证脚本示例

python 复制代码
import numpy as np
import onnxruntime as ort

# 1. 用传感器数据构造输入(归一化)
input_data = (raw_data / 4095.0).astype(np.float32)   # [1, 6, 10, 8]

# 2. ONNX Runtime 推理
session = ort.InferenceSession("model.onnx")
sigmoid_out = session.run(None, {"input": input_data})[0]  # [Fx, Fy, Fz, Fn] in [0,1]

# 3. 反归一化(与 MCU post_process 完全一致)
Fx = (sigmoid_out[0][0] - 0.5) * 2 * 4095   # 双向力
Fy = (sigmoid_out[0][1] - 0.5) * 2 * 4095
Fz = sigmoid_out[0][2] * 4095                 # 单向力
Fn = sigmoid_out[0][3] * 4095                 # 法向力

# 4. 与 MCU 实测值对比
print(f"PC:  Fx={Fx:.1f}, Fy={Fy:.1f}, Fz={Fz:.1f}, Fn={Fn:.1f}")
print(f"MCU: Fx={mcu_fx}, Fy={mcu_fy}, Fz={mcu_fz}, Fn={mcu_fn}")
print(f"误差: < 5% 即为通过")

第9章 架构对比与未来方向

9.1 MCU 架构对 AI 推理的影响

模块 作用 STM32G4 现状
FPU 浮点硬件加速 有,单精度 float32
Cache 减少 Flash/RAM 访问延迟 无(用 ART Accelerator 补偿)
SIMD/DSP 单指令多数据并行 2 路 int16
CCMRAM CPU 专用零等待内存 32 KB

无 FPU 时的影响:

复制代码
有 FPU:776K MAC x 3 cycles / 128MHz ≈ 18ms -> 优化后约 5ms
无 FPU:776K MAC x 20 cycles / 128MHz ≈ 121ms(软件模拟浮点)

9.2 不同架构推理速度对比

架构 代表芯片系列 FPU Cache SIMD 推理速度趋势
Cortex-M4F STM32G4 float32 2 路 int16 基准
Cortex-M7 STM32H7 float32+64bit 256KB 同 M4 约 2 倍(Cache 加速)
Cortex-M33+Helium STM32U5 16 路 int8 int8 量化后约 13 倍

G4 -> H7 只快 2 倍,因为两者 SIMD 相同,Cache 只解决带宽问题。真正的飞跃在 M33+Helium:int8 量化 + 16 路 SIMD = 13 倍加速。

9.3 运行时与 Kernel 关系

复制代码
模型格式(文件)          推理运行时(执行引擎)         底层 kernel
-----------------        ----------------------        ----------------
ONNX (.onnx)      ->    ST Edge AI Runtime      ->    CMSIS-NN (M4 优化)
TFLite (.tflite)  ->    LiteRT for MCU (TFLM)   ->    CMSIS-NN
自定义             ->    自己写的解析器            ->    自己的数学函数

9.4 模型瘦身方向

方向 方法 预期效果 代价
减参数 Dense1 输出减半,Conv 通道减半 权重减半,MACs 减少 4-7 倍 需重新训练
int8 量化 float32 -> int8 PTQ 权重缩小 4 倍,推理快 4-8 倍 精度损失风险
DepthwiseConv 替代标准 Conv2D MACs 减少 3-8 倍 需修改网络定义
减输入窗口 减少输入帧数/通道 计算量减少约 30% 可能影响精度

智汇嵌入式实验室 · 7yewh

相关推荐
无垠的广袤2 小时前
【ChatECNU 大语言模型】基于 Linux 开发板的 OpenClaw 部署方案
linux·人工智能·语言模型
zzb15802 小时前
Agent学习-ReAct框架
java·人工智能·python·机器学习·ai
明月(Alioo)2 小时前
开发机上通过Ollama安装了qwen2.5:7b-instruct大模型后curl请求示例
ai·aigc·agent
YYYing.2 小时前
【Linux/C++多线程篇(二) 】给线程装上“红绿灯”:通俗易懂的同步互斥机制讲解 & C++ 11下的多线程
linux·c语言·c++·经验分享·ubuntu
荆楚闲人2 小时前
ubuntu下实现自动以root用户开机无密码方式进入桌面
linux·运维·ubuntu
liweiweili1263 小时前
lsof 查看写入日志文件的进程是什么
linux
国科安芯3 小时前
抗辐照ASP4644四通道降压稳压器在商业卫星通信处理模块的应用研究
单片机·嵌入式硬件·安全·fpga开发·架构·安全性测试
charlie1145141913 小时前
嵌入式现代C++开发——三路比较运算符
开发语言·c++·学习·算法·嵌入式·编程指南
陈皮糖..3 小时前
Ansible实战教程----使用Ansible角色源码编译部署nginx服务
linux·运维·nginx·自动化·云计算·ansible