去年接了个活儿,客户说要在STM32F4上跑一个手势识别模型。

我当时第一反应是——你认真的?这片子主频168MHz,RAM 192KB,跑AI?

但客户是甲方,甲方说行那就试试呗。

然后我花了一整个晚上,翻车三次,才终于跑起来。今天聊聊这三道坎儿。


第一翻:选了TensorFlow Lite Micro,发现装都装不上

最早的想法很简单:TFLite Micro嘛,谷歌官方出品,总不会太离谱。

结果编译环境先把我干趴下了。

# 克隆仓库
git clone --depth 1 https://github.com/tensorflow/tflite-micro.git
cd tflite-micro

然后按文档跑:

make -f tensorflow/lite/micro/tools/make/Makefile TARGET=cortex_m_generic

跑了一堆下载,emmm...这玩意儿把XNNPACK、flatbuffers全家桶全拉下来了。下载完我看了眼,光算子库编译中间文件就吃了快800MB。一个几KB的模型,背后拖了一个庞然大物。

而且编译完生成的可执行文件,光是个sin检测demo就28KB。28KB跑个sin函数???

回头看了眼客户的BOM成本要求——STM32F407的Flash才512KB,算上通信协议栈、外设驱动,分给模型的预算不到80KB

这条路走不通。


第二翻:换方案,自己手写推理

既然框架太胖,那我就手撸推理代码呗。

模型是客户给的,一个三层全连接网络(16→32→16→5),ReLU激活,softmax输出。权重表是现成的,我就一个for循环乘加完事。

写了个原型:

// 手撸推理引擎,别笑,虽然原始但管用
// 输入:16维float数组
// 输出:5个分类的置信度

#define INPUT_DIM 16
#define HIDDEN1_SIZE 32
#define HIDDEN2_SIZE 16
#define OUTPUT_DIM 5

// 权重结构——直接把训练好的参数编译进固件
// 省掉文件读取和动态分配的麻烦
static const float w1[INPUT_DIM * HIDDEN1_SIZE] = { /* 512个浮点数,这里省略 */ };
static const float b1[HIDDEN1_SIZE] = { /* 32个偏置 */ };
// w2, w3, b2, b3 同理...

// ReLU:取0和输入的较大值
static inline float relu(float x) { return x > 0 ? x : fmaxf(x, 0.0f); }

void infer(float* input, float* output) {
    float h1[HIDDEN1_SIZE], h2[HIDDEN2_SIZE];

    // 第一层:全连接 + ReLU
    for (int i = 0; i < HIDDEN1_SIZE; i++) {
        h1[i] = b1[i];
        for (int j = 0; j < INPUT_DIM; j++)
            h1[i] += w1[i * INPUT_DIM + j] * input[j];
        h1[i] = relu(h1[i]);
    }
    // 第二、三层类似...
}

在PC上仿真跑通之后,烧进STM32——跑一次推理用了16.7毫秒

16.7毫秒一次,还只是三层小网络。客户要求30FPS处理摄像头画面,也就是每帧33毫秒内要完成前后处理+推理+通信+显示。推理这一步就吃掉一半预算了,后面啥都干不了。

我又打开代码看了半天,发现问题出在浮点运算上——STM32F4没有硬件FPU的单精度加速(虽然有FPU但效率一般),而且浮点数占4字节,cache利用率太差。


第三翻:量化,真不是改个数据类型那么简单

既然浮点慢,那就量化成int8呗。

我照着TensorFlow的量化文档改,把权重和激活值都映射到[-128, 127]范围。

然后发现一个最头疼的问题:ReLU之后的值分布不均匀

因为ReLU把所有负数清零,所以量化后的有效比特位只有正半轴。如果缩放因子算不对,8位精度里一大半都浪费了。

我踩的坑是:在PC上用float跑了前向传播算好scale和zero_point,手填进代码,然后精度从原来的95.3%掉到了82.1%。

// 量化推理中的一层——最坑的就是这个scale
// 如果scale算得不对,13%的准确率说没就没
void infer_quantized(int8_t* input_q, float input_scale, int input_zp,
                     int8_t* output_q, float* output_f) {
    // 反量化到float做乘加(部分量化的常见做法)
    float h1[HIDDEN1_SIZE];
    for (int i = 0; i < HIDDEN1_SIZE; i++) {
        float acc = b1_float[i];
        for (int j = 0; j < INPUT_DIM; j++) {
            float in_val = (input_q[j] - input_zp) * input_scale;
            acc += w1_float[i * INPUT_DIM + j] * in_val;
        }
        h1[i] = relu(acc);
    }
    // ...
}

折腾到凌晨两点才搞明白——校准集不够代表性。我拿100张图算的scale,但实际场景里光照变化导致输入分布偏移,ReLU层的输出被截断了太多信息。

最后我换了个方案:不自己手写量化校准,而是用STM32Cube.AI工具链,输入h5模型直接自动生成优化代码。生成出来的推理函数只有2.3KB的代码量,一次推理8.1毫秒,比我自己手写的还快了将近两倍。


复盘

这三道坎儿,每一道都是"我寻思这很容易啊"然后又啪啪打脸。

第一翻告诉我:不要以为框架越官方的越好用。TFLite Mic罗在资源受限的MCU上,很多抽象层反而是包袱。

第二翻告诉我:手写推理虽然酷,但你对硬件的了解往往比你想象的要少。浮点运算在MCU上的真实代价、cache miss的影响,这些数据手册上都有,但你不踩一脚是不会记住的。

第三翻告诉我:量化是门玄学,校准集的覆盖比模型结构本身更重要

最后说一句:STM32跑AI确实能跑,但不要用PC的思维去规划嵌入式AI的资源预算。一个在电脑上毫秒级的运算,放到MCU上可能就是生死线。

现在回头看,那晚的三次翻车其实挺值的——至少以后不管是选方案还是做技术评估,心里有数了。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐