在Cortex-M4上跑了个神经网络,然后我emo了
上个月老板丢过来一句话:"把那个手势识别模型塞进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才悟出来的。
更多推荐


所有评论(0)