上个月老板丢过来一句话:"把那个手势识别模型塞进STM32."

我当时就愣了。模型是Keras训的,2层卷积加全连接,参数加起来几十万个浮点数——扔在PC笔记本上跑得好好的,张嘴就要塞进一个只有256KB RAM的Cortex-M4?

开玩笑呢。

但老板的话就是需求。硬着头皮上吧。


模型量化,第一个坑就够深的

翻了一圈文档,TensorFlow Lite Micro是唯一靠谱的选择。但量化这个事,远比我想象的复杂。

一开始我这么写:

converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
tflite_model = converter.convert()

你觉得量化完就全是int8了?天真。这么干出来的模型里头某些层还是float——它偷偷fallback回去了。你根本不知道,直到你在MCU上一跑,RAM直接爆炸。因为float计算需要的临时buffer比int8大了四倍,一来就超了。

查了两天才发现要指定full integer quantization,还得塞一个representative dataset让它校准量化范围:

def representative_dataset():
    for data in tf.data.Dataset.from_tensor_slices(x_train).batch(1).take(100):
        yield [tf.dtypes.cast(data, tf.float32)]

converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_dataset
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.int8
converter.inference_output_type = tf.int8
tflite_quant_model = converter.convert()

模型从1.2MB缩到320KB。代价是准确率从92%掉到85%。85%其实凑合能用,但如果你的产品需要高可靠性——比如工业缺陷检测或者涉及人身安全——你得想清楚接不接受这个tradeoff。我后来试了用更多数据做量化校准,把精度拉回到88%,也算勉强能接受了。


内存分配,我最惨的一次翻车

转好的模型烧进STM32F407。一上电,HardFault。

debug了两天。原因说出来你可能不信——tensor arena给太小了。TFLite Micro要求你预先划一块内存池给它,它所有中间计算结果都塞这里面。你猜多大合适?没有文档告诉你,你得算,或者试。

static uint8_t tensor_arena[64 * 1024];

我给了64KB,模型直接报kTfLiteError。这个错误码没有任何附加信息,就一个enum值。翻了源码才发现是内存不够——某个卷积层的输出feature map加上权重缓冲区,算下来需要80KB以上。

最后改成128KB才跑起来:

static uint8_t tensor_arena[128 * 1024];

但这个选择很痛。F407总共就192KB SRAM。128KB给了TFLite,剩下64KB给FreeRTOS加业务逻辑——任务栈稍大一点就溢出。我后来不得不把GUI的buffer砍掉三分之二,把几个全局数组改成malloc动态分配,才算把内存管明白。

网上有人建议先放一个超大的arena,跑一次看error message里打印的arena size,再往下调。可惜我没开那个logging宏,白白浪费了一天。


跑一次要半秒?这谁顶得住

程序跑起来那一刻还挺激动的。然后测推理时间——470ms。一帧32x32的单通道灰度图,推理了将近半秒。做个手势识别,手都挥酸了屏幕还没反应。

优化三步走。

第一步,开CMSIS-NN。TFLite Micro编译时加上-DCMSIS_NN宏就成,同样的模型直接降到120ms。CMSIS-NN用了arm汇编优化的矩阵乘法和激活函数,效率比纯C翻了将近四倍。前提是你得在CubeMX里把CMSIS包勾上,不然link阶段报undefined reference。

第二步,输入输出全用int8。我之前偷懒把输入层设成float32,等于每次推理前要做一次类型转换——纯纯浪费算力。改完又省了30ms。

第三步,换芯片。STM32F407没有硬件乘累加器,跑NN真的太勉强。后来换成STM32G474,带FPU和CORDIC协处理器,推理时间直接掉到55ms,总算能看了。

其实选芯片的时候就应该算清楚。F407是十年前的老架构了,设计初衷就没考虑过AI推理。G4系列虽然也不算什么AI专用芯片,但至少外设新、带硬件乘加,跑tinyML够用。如果再往上,H7系列带DSP和双精度FPU,推理速度还能再翻倍。


功耗是最后一道坎

跑推理的时候芯片吃45mA电流,对电池供电的设备来说有点大了。更何况如果连续做推理,那电流根本下不来。

我的做法是空闲时进stop模式。推理完马上调用HAL_PWR_EnterSTOPMode,电流直接从45mA掉到几个µA。反正手势识别一秒判断个两三次就够了,没必要让CPU一直傻转。

几个关键点:进stop之前把所有外设时钟关了,GPIO设成analog模式减少漏电流。唤醒用RTC定时器或者外部中断按键。实测平均功耗能压到2mA以下——一颗18650电池大概2500mAh,算下来能撑一个多月。

如果你是做电池供电的产品,这个功耗优化必须从第一天就考虑。有人最后才发现功耗压不下去,改电路改到哭。


说到底嵌入式AI没那么多玄乎的东西。模型量化、内存规划、硬件选型、功耗优化——跟做其他嵌入式开发没啥本质区别,都是在资源受限的大前提下做加减法。只不过AI这一套工具链还不够成熟,坑比想象中多了那么几个。

别问我怎么知道的。问就是焊了好几块板子烧了好几个jlink debugger才悟出来的。

Logo

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

更多推荐