1. 从一颗传感器说起:为什么要把 AI 塞进设备里
前阵子帮一个做工业设备的朋友看方案,他手里有一台振动监测仪,原本的做法是把传感器采到的数据通过总线传到上位机,再由上位机跑算法判断设备状态。问题出在现场:一台设备上挂了十几个测点,采样率一高,总线带宽就吃紧,上位机那边排队处理,延迟从几十毫秒涨到几百毫秒,等报警出来,轴承可能已经开始磨了。他问我,能不能让传感器自己"聪明"一点,采完数据就地判断,只把结论传上去。
这个问题其实指向了这几年嵌入式领域一个很实在的方向——嵌入式人工智能,英文里常说的TinyML也是这个范畴。简单讲,就是把原本跑在云端或者工控机上的机器学习模型,压缩、裁剪之后部署到MCU(微控制器)这类资源极其有限的芯片上,让设备在本地完成推理,而不是什么都往上传。传感器是这套逻辑里最典型的载体:它本来就是数据源头,把 AI 能力直接嵌进去,等于让"眼睛"和"耳朵"自己带上了判断力。
我写这篇东西,不是要讲什么宏大叙事,而是想把这条链路上真正会卡人的地方摊开说清楚:模型怎么从训练框架落到 MCU 上、内存和算力怎么抠、传感器数据怎么喂进去、推理结果怎么用。适合正在做传感器课程设计的学生、做物联网设备开发的工程师,以及想给现有硬件加一点"智能"但不知道从哪下手的人。哪怕你之前只写过单片机裸机程序,没碰过机器学习,看完也能理出一条可执行的路径。
2. 嵌入式 AI 的整体设计思路:为什么不能照搬云端那套
2.1 云端推理和端侧推理的本质差别
很多人第一次接触这个方向,脑子里想的还是"把模型跑起来"这件事,但云端和端侧的约束完全不是一个量级。云端推理你关心的是吞吐、并发、GPU 利用率,内存几个 G 起步,模型几百兆也无所谓。端侧推理面对的是另一套账本:一颗常见的 Cortex-M4 MCU,主频几十到一百多兆,SRAM 可能只有 64KB 到 256KB,Flash 也就 512KB 到 1MB。你要把模型、中间张量、输入缓冲区、系统栈全部塞进这个空间里。
我习惯用一个类比:云端跑模型像在仓库里搬货,地方大,叉车随便开;端侧跑模型像在电梯里搬货,你得先把货拆成能进电梯的尺寸,还得算好一次能搬几件。这个"拆"和"算"就是嵌入式 AI 的核心工作。
具体到数字上,一个普通的卷积神经网络动辄几百万参数,按 float32 存就是几 MB 到几十 MB,直接放进 MCU 的 Flash 都放不下,更别说推理时还要额外的中间激活值。所以端侧方案的第一原则是模型必须为硬件量身定做,而不是训练完再想办法塞。
2.2 三种主流落地路线的取舍
实际项目里,把 AI 放进传感器大致有三条路,各有各的适用场景,选错了后面全是坑。
第一条是纯 MCU 端侧推理。模型量化成 int8,用 TensorFlow Lite for Microcontrollers 或者 CMSIS-NN 这类库跑在 MCU 上。优点是成本低、功耗低、响应快,一颗几块钱的 MCU 就能干活。缺点是模型容量受限,复杂任务做不了,适合关键词识别、简单振动分类、异常检测这类任务。
第二条是MCU 加专用加速器。有些芯片内置了 NPU 或者 DSP 扩展,比如带 Ethos-U 的 Cortex-M 系列,或者带 Hexagon DSP 的方案。算力比纯 MCU 高一个数量级,能跑稍大的模型,功耗仍然可控。代价是开发工具链更复杂,模型部署要过厂商的编译器。
第三条是传感器加边缘网关。传感器本身只做采集,把数据送到附近一个算力更强的边缘设备(比如带 GPU 的小盒子)上推理。这条路的模型自由度最高,但延迟和成本也上去了,适合多传感器融合的复杂场景。
我个人的经验是,先看任务的实时性要求和数据量。如果要求毫秒级响应、单点数据量不大,优先考虑纯 MCU;如果模型确实压不下来,再往上升级。别一上来就上网关,很多时候是杀鸡用牛刀。
2.3 传感器侧的特殊约束
传感器这个载体还有它自己的麻烦。第一是数据质量,传感器信号往往带噪声、漂移、温漂,模型在实验室数据上表现好,到现场就崩。第二是采样与推理的节奏,传感器是连续采样的,但推理不能每个采样点都跑一次,得做窗口切分,窗口长度和步长直接影响延迟和准确率。第三是功耗预算,很多传感器节点靠电池供电,推理一次耗多少电、多久跑一次,直接决定电池能撑几个月。
这三点决定了嵌入式 AI 在传感器上的设计不是"模型越小越好",而是在准确率、延迟、功耗、内存之间找平衡点。下面几节我会把每个环节拆开讲。
3. 核心细节解析:模型、数据、硬件三件事怎么对齐
3.1 模型压缩:量化、剪枝、知识蒸馏的实际效果
模型压缩是绕不开的第一步。我按实际用下来的感受排个序。
量化是最有效也最常用的手段。把 float32 的权重和激活值转成 int8,模型体积直接降到四分之一,推理速度通常能提升两到四倍,因为整数运算在 MCU 上比浮点快得多。代价是精度会掉一点,通常掉 1% 到 3%,做得好可以控制在 1% 以内。量化的关键是校准,你需要一批有代表性的数据跑一遍,统计每层激活值的动态范围,确定缩放因子。校准数据选得不好,精度掉得会很厉害。
剪枝是把模型里贡献小的权重去掉,让模型变稀疏。理论上能减小体积,但实际在 MCU 上收益有限,因为稀疏矩阵的运算不一定比稠密快,除非硬件专门支持。我一般只在模型实在压不下来时才考虑。
知识蒸馏是拿一个大模型教一个小模型,让小模型学到接近大模型的表现。这个在端侧很有用,但训练成本高,需要同时维护两个模型。适合对精度要求高、又有训练资源的场景。
我的建议是:先量化,量化后精度不够再考虑蒸馏,剪枝放最后。大部分传感器任务,量化后的模型就够用了。
3.2 数据窗口与特征工程:别让模型输在起跑线
传感器数据和图像不一样,它是时间序列。你不能把一个采样点丢给模型,得切成窗口。窗口长度怎么定?这取决于你要检测的事件持续时间。比如检测电机轴承故障,故障特征频率可能在几百赫兹,窗口至少要覆盖几个周期,通常取 256 到 1024 个采样点。步长决定推理频率,步长等于窗口长度就是无重叠,步长小于窗口长度就是有重叠,重叠能提高召回率但增加计算量。
特征工程这块,很多人想直接上原始数据让模型自己学,但在 MCU 上这往往不划算。原始振动信号直接进 CNN,第一层计算量就很大。更实际的做法是先做传统特征提取,比如时域的均方根、峰值因子、峭度,频域的 FFT 后取几个频带能量,把这些特征组成一个几十维的向量,再喂给一个小型全连接网络或者决策树。这样模型能小到几 KB,推理一次几毫秒,精度还不差。
我做过一个对比:同样检测三种电机状态,原始信号进 CNN 的模型是 120KB,准确率 94%;手工特征加全连接网络的模型是 8KB,准确率 92%。在 MCU 上,后者明显更实用。
3.3 硬件选型:算力、内存、外设的三角平衡
选 MCU 的时候,别只看主频。我列几个真正影响嵌入式 AI 的指标。
| 指标 | 为什么重要 | 常见参考值 |
|---|---|---|
| SRAM 大小 | 决定中间张量能开多大 | 至少 64KB,推荐 128KB 以上 |
| Flash 大小 | 决定模型能存多大 | 至少 256KB,推荐 512KB 以上 |
| 是否带 FPU | 影响浮点运算速度 | 有 FPU 能省不少事 |
| 是否带 DSP 指令 | 影响定点运算效率 | CMSIS-NN 能利用上 |
| 主频 | 影响推理延迟 | 80MHz 以上比较从容 |
| 外设接口 | 决定传感器怎么接 | I2C、SPI、ADC 按需 |
如果预算允许,带 Cortex-M4 或 M7 内核、有 FPU 和 DSP 指令的芯片是首选。M0 也能跑,但基本只能跑极小的模型,而且得全定点。我踩过的坑是:一开始选了颗 M0,模型量化后跑起来,推理一次要 80ms,采样窗口都来不及处理,最后只能换芯片。
4. 实操过程:从训练到部署的完整链路
4.1 训练环境的搭建与模型定义
训练还是在 PC 上做,用 TensorFlow 或者 PyTorch 都行。我习惯用 TensorFlow,因为后面转 TFLite 比较顺。环境用 conda 建一个隔离的,Python 3.8 到 3.10 都行,太新的版本有些库还没跟上。
模型定义的时候就要想着端侧约束。几个原则:层数别太深,超过 10 层的网络在 MCU 上基本跑不动;通道数别太大,卷积层通道数控制在 32 以内;避免大核卷积,5x5 以上尽量用两个 3x3 替代;全连接层节点数别超过 256。这些约束在训练时就要遵守,不然训练完再改结构等于重来。
import tensorflow as tf model = tf.keras.Sequential([ tf.keras.layers.Conv1D(16, 3, activation='relu', input_shape=(256, 3)), tf.keras.layers.MaxPooling1D(2), tf.keras.layers.Conv1D(32, 3, activation='relu'), tf.keras.layers.MaxPooling1D(2), tf.keras.layers.Flatten(), tf.keras.layers.Dense(32, activation='relu'), tf.keras.layers.Dense(3, activation='softmax') ])这个模型输入是 256 个采样点、3 个通道(比如三轴加速度),输出 3 分类。参数量大概几万,量化后几十 KB,M4 上跑没问题。
4.2 训练、量化与转换的具体步骤
训练本身没什么特别的,注意数据要按类别均衡,传感器数据往往正常样本远多于异常样本,得做重采样或者加权。训练完先看 float 模型的准确率,作为基准。
量化用 TensorFlow 的 TFLite 转换器,关键是提供代表性数据集。这个数据集要从训练集里抽,覆盖各种工况,一般 100 到 500 个样本就够。
def representative_dataset(): for i in range(200): yield [train_data[i:i+1].astype('float32')] converter = tf.lite.TFLiteConverter.from_keras_model(model) 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_model = converter.convert() with open('model.tflite', 'wb') as f: f.write(tflite_model)转完之后一定要验证量化模型的精度,用测试集跑一遍,和 float 模型对比。如果掉得超过 3%,回去检查校准数据或者调整模型结构。
4.3 把模型部署到 MCU 上
TFLite 模型是个二进制文件,需要用xxd转成 C 数组,或者用厂商工具直接生成。然后集成 TFLite Micro 运行时到工程里。
xxd -i model.tflite > model_data.cc在 MCU 代码里,初始化解释器、分配张量内存、喂数据、取结果。这里最容易出问题的是内存分配。TFLite Micro 需要一个 arena,大小要够放所有中间张量。太小会报错,太大浪费 SRAM。我一般先用一个偏大的值跑通,再用interpreter->arena_used_bytes()看实际用了多少,然后调到刚好够用加一点余量。
constexpr int kTensorArenaSize = 60 * 1024; static uint8_t tensor_arena[kTensorArenaSize]; tflite::MicroInterpreter interpreter( model, resolver, tensor_arena, kTensorArenaSize); interpreter.AllocateTensors(); int8_t* input = interpreter.input(0)->data.int8; // 填充 input interpreter.Invoke(); int8_t* output = interpreter.output(0)->data.int8;推理延迟实测下来,上面那个模型在 80MHz 的 M4 上大概 15ms 一次,完全够用。
4.4 传感器数据接入与推理调度
传感器接入这块,看接口类型。I2C 的传感器读起来简单,但速率有限,适合低速信号。SPI 快,适合高速采样。模拟传感器走 ADC,要注意采样率和分辨率。
推理调度我一般用一个定时器触发,比如每 100ms 采一批数据,凑够一个窗口就跑一次推理。窗口之间可以重叠,提高响应速度。注意推理期间不要阻塞采样,可以用 DMA 把采样数据搬到缓冲区,CPU 专心跑推理。
功耗方面,如果电池供电,可以让 MCU 大部分时间休眠,定时器唤醒采样和推理,跑完继续睡。实测下来,这种间歇工作模式能把平均电流压到几百微安,电池撑几个月没问题。
5. 常见问题与排查技巧实录
5.1 模型部署阶段的典型报错
问题一:AllocateTensors 返回错误。九成是 arena 太小。先加大到 128KB 试,跑通后看实际用量再缩。也有可能是模型里有 MCU 不支持的算子,用resolver注册算子时确认一下。
问题二:推理结果全是同一个类别。多半是输入数据没归一化。训练时输入是归一化到 0 到 1 或者 -1 到 1 的,部署时忘了做同样的处理,模型看到的分布完全不对。检查输入量化参数,确保喂进去的 int8 值范围正确。
问题三:精度比 PC 上掉很多。先确认量化校准数据是否有代表性,再检查输入输出的量化参数是否和训练时一致。有时候是某些层的激活值范围太宽,量化后分辨率不够,可以考虑对那层单独处理或者换激活函数。
5.2 现场运行时的疑难杂症
问题四:实验室好好的,现场就误报。传感器信号受温度、安装方式、背景噪声影响很大。解决办法是在现场采一批数据做微调,或者加一些鲁棒性处理,比如滑动平均、异常值剔除。我一般会在推理结果上加一个连续确认机制,连续几次都判为异常才报警,能过滤掉大部分偶发误报。
问题五:推理偶尔超时。检查是不是有中断打断了推理,或者 DMA 和 CPU 抢总线。可以把推理放在低优先级任务里,或者用双缓冲,一个缓冲区采样时另一个跑推理。
问题六:功耗比预期高。用电流表测一下各个阶段的电流,通常是某个外设没关,或者休眠模式没进对。MCU 的休眠有好几档,选最深的那档,唤醒源只留定时器。
5.3 一份速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 分配张量失败 | arena 太小 / 算子不支持 | 加大 arena,检查算子注册 |
| 输出恒定 | 输入未归一化 / 量化参数错 | 核对输入范围 |
| 精度骤降 | 校准数据无代表性 | 重选校准集 |
| 现场误报多 | 数据分布偏移 | 现场数据微调,加确认机制 |
| 推理超时 | 中断干扰 / 总线竞争 | 调整任务优先级,双缓冲 |
| 功耗偏高 | 外设未关 / 休眠档位浅 | 逐项测电流,选深休眠 |
6. 几个容易被忽略的实操心得
做这个方向几年,有些经验是文档里不会写的,但实际项目里特别值钱。
第一,别迷信模型准确率。传感器任务里,一个 90% 准确率但稳定的模型,比 95% 但偶尔抽风的模型有用得多。工业场景里误报的代价往往比漏报还高,因为误报多了工人就不信了。
第二,留好升级通道。模型是要迭代的,现场数据采回来重新训练是常态。设计的时候把模型数据放在单独的 Flash 区域,留个接口能通过串口或者无线更新,别把模型烧死在代码里。
第三,推理结果要带置信度。TFLite 输出的 softmax 值可以当置信度用,低于阈值的就丢弃或者标记为不确定,别硬报。这个在异常检测里特别重要,因为异常样本本来就少,模型容易过度自信。
第四,采样和推理的时钟要统一。我见过一个项目,采样用内部 RC 振荡器,推理用外部晶振,时间长了窗口对不齐,数据错位,模型表现忽好忽坏。统一时钟源,或者用硬件定时器触发采样。
第五,测试要覆盖边界工况。传感器在低温、高温、电压偏低时的表现可能完全不同。我一般会在高低温箱里跑一遍,确认模型和硬件都扛得住。
这套东西说到底,核心就一句话:让模型去适应硬件和数据,而不是反过来。嵌入式 AI 在传感器上的价值,不在于模型多先进,而在于它能不能在几块钱的芯片上、在电池供电的条件下、在嘈杂的现场环境里,稳定地做出一个够用的判断。把这件事做扎实,比追新模型有意义得多。