在STM32上跑AI模型?我折腾了一宿,翻了三次车
去年接了个活儿客户说要在STM32F4上跑一个手势识别模型。我当时第一反应是——你认真的这片子主频168MHzRAM 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 TARGETcortex_m_generic跑了一堆下载emmm...这玩意儿把XNNPACK、flatbuffers全家桶全拉下来了。下载完我看了眼光算子库编译中间文件就吃了快800MB。一个几KB的模型背后拖了一个庞然大物。而且编译完生成的可执行文件光是个sin检测demo就28KB。28KB跑个sin函数回头看了眼客户的BOM成本要求——STM32F407的Flash才512KB算上通信协议栈、外设驱动分给模型的预算不到80KB。这条路走不通。第二翻换方案自己手写推理既然框架太胖那我就手撸推理代码呗。模型是客户给的一个三层全连接网络16→32→16→5ReLU激活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上可能就是生死线。现在回头看那晚的三次翻车其实挺值的——至少以后不管是选方案还是做技术评估心里有数了。