TinyML 初探:在 MCU 上部署异常检测模型,实现本地振动故障诊断

摘要: 工业设备的振动监测是边缘 AI 最典型的应用场景之一------传统方案把振动数据上传云端分析,延迟高、流量贵、隐私差;而本地推理需要在只有几百 KB RAM 的 MCU 上跑模型。本文用一个真实的电机故障诊断项目,从零演示在 MCU 上部署 TinyML 模型的完整流程------数据采集、特征工程、模型训练、量化、转换、部署到 STM32。包含 TensorFlow Lite for Microcontrollers 的实际使用、CMSIS-NN 加速、内存优化和性能实测。这是付费专栏第一季的收尾篇,也是第二季《嵌入式边缘 AI 实战》的开篇导引。建议收藏。

一、引子:一个"看起来很美好"的云端方案

去年做一款工业电机监测设备,需求是:实时监测电机振动,提前预警轴承故障。

第一版方案很"标准":

  • STM32F407 采集振动传感器(加速度计)数据,1kHz 采样

  • 通过 4G 模块上传到云端

  • 云端跑 AI 模型分析

  • 发现异常后推送告警

方案很快遇到了三个问题:

问题一:流量成本。 三轴加速度计,1kHz 采样,每轴 16 位,一天的数据量:

复制代码
3 轴 × 1000 Hz × 2 字节 × 86400 秒 = 518 MB/天

每台设备每天 500MB 流量,1000 台设备就是 500GB/天。 4G 流量费一年 5 万多元。

问题二:延迟。 数据上传 + 云端推理 + 告警下发,端到端延迟 2-5 秒。对于高速旋转的电机,轴承损坏可能在几秒内就从"早期"发展到"严重"。

问题三:隐私。 客户是军工企业,振动数据属于敏感信息,不允许出厂区。

结论:必须本地推理。

但本地推理要解决一个核心问题:MCU 只有 192KB RAM、1MB Flash,怎么跑 AI 模型?

这就是 TinyML 要解决的问题。

二、TinyML 的核心思路

2.1 为什么 MCU 能跑 AI

很多人对"MCU 跑 AI"有误解,认为这是不可能的事。其实只要模型足够小,MCU 完全能跑。

关键认知:

  • 不是所有 AI 都需要 GPT 那种大模型------很多工业场景只需要简单分类器

  • 模型可以压缩:量化(float32 → int8)能减 4 倍

  • 特征工程比模型复杂更重要:好的特征能让简单模型达到好效果

  • 硬件加速可用:Cortex-M4/M7 的 DSP 指令、CMSIS-NN 库

一个典型的 MCU 异常检测模型:

这种规模的模型,STM32F4 完全能跑。

2.2 TinyML 的三大技术

本项目的核心是量化,这也是最成熟、最容易落地的技术。

2.3 整体流程

复制代码
1. 数据采集(振动信号)
    ↓
2. 特征工程(FFT、时域统计)
    ↓
3. PC 端训练模型(Keras)
    ↓
4. 模型量化(TFLite Converter)
    ↓
5. 转换为 C 数组(xxd)
    ↓
6. 集成到 MCU 工程(TFLite Micro)
    ↓
7. 部署 + 优化(CMSIS-NN)
    ↓
8. 实测验证

三、数据采集与特征工程

3.1 硬件选型

3.2 数据采集

采集正常和故障两种状态下的振动数据。

复制代码
// vibration_capture.c
#include "stm32f4xx_hal.h"

#define SAMPLE_RATE      1000
#define SAMPLE_DURATION  1     // 采集 1 秒 = 1000 个点

typedef struct {
    int16_t x;
    int16_t y;
    int16_t z;
} accel_sample_t;

accel_sample_t g_buffer[SAMPLE_RATE * SAMPLE_DURATION];

void CaptureVibration(void)
{
    uint32_t start = HAL_GetTick();
    uint32_t idx = 0;

    while (idx < SAMPLE_RATE * SAMPLE_DURATION) {
        uint32_t now = HAL_GetTick();
        if (now - start >= idx) {
            // 每 1ms 采一次
            ADXL345_Read(&g_buffer[idx].x, &g_buffer[idx].y, &g_buffer[idx].z);
            idx++;
        }
    }
}

采集数据量: 每个状态 100 组,每组 1 秒。

这些数据通过 UART 传输到 PC,保存为 CSV。

3.3 特征工程

关键点:不要把原始振动信号直接喂给模型,而是提取特征。

原因:

  • 原始信号 1000×3=3000 个点,模型输入太大会占用大量 RAM

  • 特征能大幅压缩输入,比如 FFT 的前 20 个峰值 = 20 个特征

  • 特征有物理意义,可解释性强

本项目提取的特征:

python 复制代码
# feature_extraction.py
import numpy as np
from scipy.fft import fft

def extract_features(samples: np.ndarray) -> np.ndarray:
    """从振动信号提取特征

    samples: shape (N, 3),N 个采样点,3 轴
    返回: shape (F,),F 个特征
    """
    features = []
    for axis in range(3):
        signal = samples[:, axis].astype(np.float32)

        # 时域特征
        features.append(np.mean(signal))
        features.append(np.std(signal))
        features.append(np.max(np.abs(signal)))
        features.append(np.sqrt(np.mean(signal ** 2)))  # RMS
        # 峰峰值
        features.append(np.max(signal) - np.min(signal))
        # 峭度(Kurtosis,对冲击敏感)
        features.append(np.mean(((signal - np.mean(signal)) / (np.std(signal) + 1e-8)) ** 4))

        # 频域特征(FFT)
        spectrum = np.abs(fft(signal))[:len(signal) // 2]
        # 前 5 个峰值频率对应的幅值
        top5_idx = np.argsort(spectrum)[-5:]
        features.extend(spectrum[top5_idx])
        # 频谱质心
        features.append(np.sum(np.arange(len(spectrum)) * spectrum) / (np.sum(spectrum) + 1e-8))

    return np.array(features, dtype=np.float32)

每轴提取 12 个特征,3 轴共 36 个特征。

为什么这些特征有用:

轴承故障的典型特征: 在轴承故障特征频率(BPFO、BPFI)处出现峰值。峭度和 FFT 峰值是最重要的特征。

3.4 数据集准备

python 复制代码
# prepare_dataset.py
import numpy as np
import pandas as pd
from glob import glob

def load_dataset():
    X = []
    y = []

    # 加载正常样本
    for f in glob('data/normal/*.csv'):
        samples = pd.read_csv(f).values
        features = extract_features(samples)
        X.append(features)
        y.append(0)  # 0 = 正常

    # 加载轻微异常样本
    for f in glob('data/minor/*.csv'):
        samples = pd.read_csv(f).values
        features = extract_features(samples)
        X.append(features)
        y.append(1)  # 1 = 轻微异常

    # 加载严重故障样本
    for f in glob('data/severe/*.csv'):
        samples = pd.read_csv(f).values
        features = extract_features(samples)
        X.append(features)
        y.append(2)  # 2 = 严重故障

    X = np.array(X, dtype=np.float32)
    y = np.array(y, dtype=np.int32)

    # 打乱顺序
    idx = np.random.permutation(len(X))
    X, y = X[idx], y[idx]

    # 划分训练集/测试集(8:2)
    split = int(len(X) * 0.8)
    return X[:split], y[:split], X[split:], y[split:]

if __name__ == '__main__':
    X_train, y_train, X_test, y_test = load_dataset()
    np.save('X_train.npy', X_train)
    np.save('y_train.npy', y_train)
    np.save('X_test.npy', X_test)
    np.save('y_test.npy', y_test)

    print(f'训练集: {X_train.shape}, 测试集: {X_test.shape}')
    print(f'特征维度: {X_train.shape[1]}')

四、PC 端训练模型

4.1 模型设计

关键原则:

关键原则:

  • 模型要小:< 100KB(量化后)

  • 输入要小:本项目 36 个特征

  • 输出要简单:3 分类

  • 层数要少:2-3 层全连接就够

python 复制代码
# train_model.py
import numpy as np
import tensorflow as tf
from tensorflow import keras

def build_model(input_dim: int, num_classes: int) -> keras.Model:
    """构建小型全连接网络"""
    model = keras.Sequential([
        keras.layers.Input(shape=(input_dim,)),

        # 第一层:36 -> 32
        keras.layers.Dense(32, activation='relu'),
        keras.layers.Dropout(0.2),

        # 第二层:32 -> 16
        keras.layers.Dense(16, activation='relu'),

        # 输出层:16 -> 3
        keras.layers.Dense(num_classes, activation='softmax'),
    ])
    return model

def main():
    # 加载数据
    X_train = np.load('X_train.npy')
    y_train = np.load('y_train.npy')
    X_test = np.load('X_test.npy')
    y_test = np.load('y_test.npy')

    # 特征归一化(重要!)
    mean = X_train.mean(axis=0)
    std = X_train.std(axis=0) + 1e-8
    X_train = (X_train - mean) / std
    X_test = (X_test - mean) / std

    # 保存归一化参数(部署时要一起嵌入)
    np.save('norm_mean.npy', mean)
    np.save('norm_std.npy', std)

    # 构建模型
    model = build_model(X_train.shape[1], 3)
    model.summary()

    # 编译
    model.compile(
        optimizer=keras.optimizers.Adam(learning_rate=0.001),
        loss='sparse_categorical_crossentropy',
        metrics=['accuracy'],
    )

    # 训练
    history = model.fit(
        X_train, y_train,
        validation_data=(X_test, y_test),
        epochs=100,
        batch_size=16,
        verbose=1,
    )

    # 评估
    loss, acc = model.evaluate(X_test, y_test, verbose=0)
    print(f'测试集准确率: {acc:.4f}')

    # 保存模型
    model.save('vibration_model.h5')

if __name__ == '__main__':
    main()

典型训练结果:

python 复制代码
Epoch 100/100
8/8 [==============================] - 0s 5ms/step - loss: 0.0321 - accuracy: 0.9917
测试集准确率: 0.9833

未量化的模型:

  • 参数量:36×32 + 32 + 32×16 + 16 + 16×3 + 3 = 1727 个参数

  • 模型大小:1727 × 4 字节 = 6.9 KB

已经很小了,但我们可以继续压缩。

4.2 模型量化

量化的核心:把 float32 权重和激活值转成 int8。

复制代码
# quantize_model.py
import numpy as np
import tensorflow as tf

def representative_dataset():
    """提供代表性样本用于量化校准"""
    X_train = np.load('X_train.npy')
    mean = np.load('norm_mean.npy')
    std = np.load('norm_std.npy')
    X_train = (X_train - mean) / std

    for i in range(100):
        yield [X_train[i:i+1].astype(np.float32)]

def main():
    # 加载模型
    model = tf.keras.models.load_model('vibration_model.h5')

    # 转换为 TFLite
    converter = tf.lite.TFLiteConverter.from_keras_model(model)

    # 启用量化
    converter.optimizations = [tf.lite.Optimize.DEFAULT]
    converter.representative_dataset = representative_dataset

    # 强制输入输出也是 int8
    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('vibration_model_int8.tflite', 'wb') as f:
        f.write(tflite_model)

    print(f'量化后模型大小: {len(tflite_model)} 字节')

if __name__ == '__main__':
    main()

量化效果:

精度只掉 0.5%,体积减少 74%,绝对值得。

4.3 转成 C 数组

复制代码
# 把 .tflite 文件转成 C 数组
xxd -i vibration_model_int8.tflite > vibration_model.cc

生成的 C 文件:

复制代码
unsigned char vibration_model_int8_tflite[] = {
  0x1c, 0x00, 0x00, 0x00, 0x54, 0x46, 0x4c, 0x33, 0x00, 0x00, 0x12, 0x00,
  // ... 省略
};
unsigned int vibration_model_int8_tflite_len = 1840;

这个 C 数组就是要嵌入到固件里的模型。

五、MCU 端部署

5.1 集成 TensorFlow Lite Micro

TFLite Micro 是 Google 为 MCU 设计的轻量级推理引擎。

获取源码:

复制代码
git clone https://github.com/tensorflow/tflite-micro.git

需要加入工程的源文件:

复制代码
tflite-micro/tensorflow/lite/
├── micro/
│   ├── micro_interpreter.cc
│   ├── micro_error_reporter.cc
│   ├── micro_allocator.cc
│   ├── micro_string.cc
│   ├── memory_planner/
│   │   └── greedy_memory_planner.cc
│   └── kernels/
│       ├── fully_connected.cc
│       ├── softmax.cc
│       └── ...
└── ...

通过 Makefile 集成:

复制代码
# Makefile
TFLM_DIR = tflite-micro/tensorflow/lite

C_SOURCES += \
    $(TFLM_DIR)/micro/micro_interpreter.cc \
    $(TFLM_DIR)/micro/micro_error_reporter.cc \
    $(TFLM_DIR)/micro/micro_allocator.cc \
    $(TFLM_DIR)/micro/micro_string.cc \
    $(TFLM_DIR)/micro/memory_planner/greedy_memory_planner.cc \
    $(TFLM_DIR)/micro/kernels/fully_connected.cc \
    $(TFLM_DIR)/micro/kernels/softmax.cc \
    $(TFLM_DIR)/micro/kernels/quantize.cc \
    $(TFLM_DIR)/micro/kernels/dequantize.cc

C_INCLUDES += \
    -I$(TFLM_DIR)/.. \
    -I$(TFLM_DIR)/micro \
    -I$(TFLM_DIR)/micro/kernels

CFLAGS += -DTF_LITE_STATIC_MEMORY

5.2 MCU 端推理代码

复制代码
// vibration_inference.cc
#include "tensorflow/lite/micro/micro_interpreter.h"
#include "tensorflow/lite/micro/micro_error_reporter.h"
#include "tensorflow/lite/micro/micro_mutable_op_resolver.h"
#include "tensorflow/lite/schema/schema_generated.h"

#include "vibration_model.h"    // 包含模型数组

// 模型和解释器
static const tflite::Model* s_model = nullptr;
static tflite::MicroInterpreter* s_interpreter = nullptr;
static TfLiteTensor* s_input = nullptr;
static TfLiteTensor* s_output = nullptr;

// 张量竞技场(Tensor Arena):模型运行时的工作内存
// 大小需要根据模型确定,从 8KB 开始试
static constexpr int kTensorArenaSize = 8 * 1024;
static uint8_t s_tensor_arena[kTensorArenaSize];

// 输出分类名称
static const char* kClassNames[] = {"正常", "轻微异常", "严重故障"};

int Vibration_Init(void)
{
    // 1. 加载模型
    s_model = tflite::GetModel(vibration_model_int8_tflite);
    if (s_model->version() != TFLITE_SCHEMA_VERSION) {
        return -1;
    }

    // 2. 注册算子(只注册模型用到的,节省 Flash)
    static tflite::MicroMutableOpResolver<4> resolver;
    resolver.AddFullyConnected();
    resolver.AddSoftmax();
    resolver.AddQuantize();
    resolver.AddDequantize();

    // 3. 创建解释器
    static tflite::MicroErrorReporter error_reporter;
    static tflite::MicroInterpreter static_interpreter(
        s_model, resolver, s_tensor_arena, kTensorArenaSize, &error_reporter);
    s_interpreter = &static_interpreter;

    // 4. 分配张量
    if (s_interpreter->AllocateTensors() != kTfLiteOk) {
        return -2;
    }

    // 5. 获取输入输出张量
    s_input = s_interpreter->input(0);
    s_output = s_interpreter->output(0);

    return 0;
}

// 执行推理
int Vibration_Inference(const float *features, int feature_count,
                        float *probabilities, int *predicted_class)
{
    if (feature_count != s_input->dims->data[1]) {
        return -1;
    }

    // 输入量化参数
    float input_scale = s_input->params.scale;
    int32_t input_zero_point = s_input->params.zero_point;

    // 1. 把 float 特征量化成 int8
    for (int i = 0; i < feature_count; i++) {
        int32_t quantized = (int32_t)(features[i] / input_scale) + input_zero_point;
        // 限制到 int8 范围
        if (quantized > 127) quantized = 127;
        if (quantized < -128) quantized = -128;
        s_input->data.int8[i] = (int8_t)quantized;
    }

    // 2. 执行推理
    if (s_interpreter->Invoke() != kTfLiteOk) {
        return -2;
    }

    // 3. 输出去量化
    float output_scale = s_output->params.scale;
    int32_t output_zero_point = s_output->params.zero_point;

    int best_class = 0;
    float best_score = -1.0f;
    for (int i = 0; i < s_output->dims->data[1]; i++) {
        int8_t raw = s_output->data.int8[i];
        float score = (raw - output_zero_point) * output_scale;
        probabilities[i] = score;
        if (score > best_score) {
            best_score = score;
            best_class = i;
        }
    }
    *predicted_class = best_class;

    return 0;
}

5.3 集成到 FreeRTOS 任务

复制代码
// vibration_task.c
#include "FreeRTOS.h"
#include "task.h"

extern int Vibration_Init(void);
extern int Vibration_Inference(const float *features, int feature_count,
                                float *probabilities, int *predicted_class);

void vVibrationTask(void *pvParameters)
{
    // 初始化
    if (Vibration_Init() != 0) {
        // 错误处理
        vTaskDelete(NULL);
    }

    float features[36];
    float probabilities[3];
    int predicted_class;

    while (1) {
        // 1. 采集振动数据
        accel_sample_t samples[1000];
        CaptureVibration(samples);

        // 2. 提取特征
        ExtractFeatures(samples, features);

        // 3. 归一化(用 PC 端保存的参数)
        for (int i = 0; i < 36; i++) {
            features[i] = (features[i] - g_norm_mean[i]) / g_norm_std[i];
        }

        // 4. 推理
        if (Vibration_Inference(features, 36, probabilities, &predicted_class) == 0) {
            // 5. 处理结果
            if (predicted_class == 2) {
                // 严重故障,触发告警
                TriggerAlarm("严重故障!");
            } else if (predicted_class == 1) {
                // 轻微异常,记录
                LogWarning("轻微异常");
            }
        }

        // 每秒采一次
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

注意: 特征提取函数 ExtractFeatures 在 MCU 上实现,需要用 ARM 的 DSP 库或自己实现 FFT。这是 MCU 端的核心难点。

六、MCU 端特征提取

6.1 时域特征

直接计算,不需要额外库。

复制代码
// feature_extract.c
#include <math.h>
#include <string.h>

typedef struct {
    int16_t x;
    int16_t y;
    int16_t z;
} accel_sample_t;

static void extract_time_domain(const accel_sample_t *samples, int count,
                                 int axis, float *features)
{
    // 提取某一轴的时域特征
    int32_t sum = 0;
    int16_t max_v = -32768, min_v = 32767;
    int32_t sum_sq = 0;

    for (int i = 0; i < count; i++) {
        int16_t v = (axis == 0) ? samples[i].x :
                    (axis == 1) ? samples[i].y : samples[i].z;
        sum += v;
        sum_sq += (int32_t)v * v;
        if (v > max_v) max_v = v;
        if (v < min_v) min_v = v;
    }

    float mean = (float)sum / count;
    float rms = sqrtf((float)sum_sq / count);
    float peak_to_peak = (float)(max_v - min_v);

    // 标准差
    float var = 0;
    for (int i = 0; i < count; i++) {
        int16_t v = (axis == 0) ? samples[i].x :
                    (axis == 1) ? samples[i].y : samples[i].z;
        float d = v - mean;
        var += d * d;
    }
    float std = sqrtf(var / count);

    // 峭度
    float kurt_sum = 0;
    for (int i = 0; i < count; i++) {
        int16_t v = (axis == 0) ? samples[i].x :
                    (axis == 1) ? samples[i].y : samples[i].z;
        float d = (v - mean) / (std + 1e-8f);
        kurt_sum += d * d * d * d;
    }
    float kurtosis = kurt_sum / count;

    features[0] = mean;
    features[1] = std;
    features[2] = (float)(max_v > -min_v ? max_v : -min_v);  // 峰值
    features[3] = rms;
    features[4] = peak_to_peak;
    features[5] = kurtosis;
}

6.2 频域特征

用 CMSIS-DSP 库做 FFT。

复制代码
#include "arm_math.h"

#define FFT_SIZE 1024

// FFT 缓冲区
static float32_t s_fft_input[FFT_SIZE];
static float32_t s_fft_output[FFT_SIZE];
static arm_rfft_fast_instance_f32 s_fft_instance;

void FFT_Init(void)
{
    arm_rfft_fast_init_f32(&s_fft_instance, FFT_SIZE);
}

static void extract_frequency_domain(const accel_sample_t *samples, int count,
                                      int axis, float *features)
{
    // 1. 填充 FFT 输入(加窗)
    for (int i = 0; i < FFT_SIZE && i < count; i++) {
        int16_t v = (axis == 0) ? samples[i].x :
                    (axis == 1) ? samples[i].y : samples[i].z;
        // 汉宁窗
        float window = 0.5f * (1.0f - arm_cos_f32(2.0f * PI * i / (FFT_SIZE - 1)));
        s_fft_input[i] = v * window;
    }
    // 补零
    for (int i = count; i < FFT_SIZE; i++) {
        s_fft_input[i] = 0.0f;
    }

    // 2. 执行 FFT
    arm_rfft_fast_f32(&s_fft_instance, s_fft_input, s_fft_output, 0);

    // 3. 计算幅值
    float32_t magnitudes[FFT_SIZE / 2];
    arm_cmplx_mag_f32(s_fft_output, magnitudes, FFT_SIZE / 2);

    // 4. 找前 5 个峰值
    // 简化实现:遍历找到最大的 5 个
    float peaks[5] = {0};
    for (int i = 1; i < FFT_SIZE / 2; i++) {
        float m = magnitudes[i];
        for (int j = 0; j < 5; j++) {
            if (m > peaks[j]) {
                for (int k = 4; k > j; k--) {
                    peaks[k] = peaks[k-1];
                }
                peaks[j] = m;
                break;
            }
        }
    }

    // 5. 计算频谱质心
    float sum_mag = 0, sum_weighted = 0;
    for (int i = 0; i < FFT_SIZE / 2; i++) {
        sum_mag += magnitudes[i];
        sum_weighted += i * magnitudes[i];
    }
    float spectral_centroid = sum_weighted / (sum_mag + 1e-8f);

    // 6. 输出特征
    for (int i = 0; i < 5; i++) features[i] = peaks[i];
    features[5] = spectral_centroid;
}

6.3 完整特征提取

复制代码
void ExtractFeatures(const accel_sample_t *samples, int count, float *features)
{
    for (int axis = 0; axis < 3; axis++) {
        extract_time_domain(samples, count, axis, features + axis * 12);
        extract_frequency_domain(samples, count, axis, features + axis * 12 + 6);
    }
}

实测耗时(STM32F407 @168MHz):

加上推理时间(约 2ms),总共约 8.5ms。 对每秒采一次的任务,完全够用。

七、CMSIS-NN 加速

CMSIS-NN 是 ARM 为 Cortex-M 优化的神经网络推理库,比 TFLite Micro 自带的算子快 2-5 倍。

7.1 使用方式

TFLite Micro 支持 CMSIS-NN 后端,只需在 Makefile 里加上:

复制代码
C_SOURCES += \
    tflite-micro/tensorflow/lite/micro/kernels/cmsis_nn/fully_connected.cc \
    tflite-micro/tensorflow/lite/micro/kernels/cmsis_nn/softmax.cc \
    tflite-micro/tensorflow/lite/micro/kernels/cmsis_nn/...

CFLAGS += -DCMSIS_NN
C_INCLUDES += -Ipath/to/CMSIS-NN/Include

同时在解析器里注册 CMSIS-NN 版本:

复制代码
#include "tensorflow/lite/micro/kernels/cmsis_nn/fully_connected.h"

resolver.AddFullyConnected(tflite::Register_FULLY_CONNECTED_INT8());

7.2 性能对比

在 STM32F407 @168MHz 上:

整体推理时间从 2ms 降到 0.8ms。

八、内存优化

8.1 Tensor Arena 大小

Tensor Arena 是 TFLite Micro 的工作内存,需要根据模型确定。

如果太小,AllocateTensors() 返回失败。如果太大,浪费 RAM。

确定方法:

  1. 从 4KB 开始尝试

  2. 失败就增加一倍

  3. 成功后再减 1KB,找到临界值

  4. 最终值加 20% 余量

本项目实测:

8.2 模型裁剪

如果 RAM 不够,可以进一步裁剪模型:

  • 减少神经元数量:32 → 16

  • 减少层数:3 层 → 2 层

  • 减少特征数:36 → 20(只保留最重要的特征)

但要注意精度损失。

8.3 Flash 占用

完整部署的 Flash 占用:

加上其他功能(UART、传感器、FreeRTOS),总计约 120KB Flash。 STM32F407 的 1MB Flash 绰绰有余。

九、实测效果

在 100 组测试数据上评估:

关键结论:

  • 在 STM32F407 上跑振动异常检测完全可行

  • 端到端延迟从云端的 2-5 秒降到本地的 10 毫秒

  • 流量成本降为零

  • 数据不出设备,隐私安全

整体方案价值:

  • 每台设备节省 4G 流量费约 50 元/年

  • 1000 台设备节省 5 万元/年

  • 故障预警时间提前 2-5 秒,避免重大事故

十、避坑清单

十一、小结与第二季预告

本文从一个真实的电机振动监测项目出发,完整演示了在 MCU 上部署 TinyML 模型的流程------数据采集、特征工程、模型训练、量化、转换、部署到 STM32。最终实现了 97.8% 的准确率、8.5ms 的推理延迟、55KB 的 Flash 占用。这个方案比云端推理延迟低 200 倍、流量成本为零、数据不出设备。

三个核心要点:

  1. MCU 上的 AI 不是"缩小版 GPT",而是特征工程 + 小模型 + 量化

  2. 特征工程比模型设计更重要------好的特征能让简单模型达到好效果

  3. 量化是必经之路------float32 模型在 MCU 上又大又慢

这是付费专栏第一季《嵌入式系统调优高手课》的收尾篇。 20 篇文章涵盖了 RTOS 内核、内存池、链接脚本、优先级反转、编译器优化、中断风暴、Cache 一致性、低功耗、日志系统、栈保护、固件瘦身、OOP、状态机、安全启动、防逆向、敏感数据存储、RISC-V 移植、TinyML。从底层汇编到系统架构,从性能优化到安全加固,构成一套完整的嵌入式系统调优方法论。

第二季《嵌入式边缘 AI 实战:从 STM32 到 TinyML 的模型部署与优化》即将推出,将深入以下主题:

  1. TFLite Micro 架构剖析(interpreter、arena、op resolver)

  2. 模型量化进阶(per-channel、混合量化、量化感知训练)

  3. CMSIS-NN 与 XNNPACK 深度对比

  4. 关键词唤醒(KWS)实战------音频流 + MFCC + CNN

  5. 手势识别------ToF 传感器 + 轻量级分类

  6. 模型剪枝与知识蒸馏

  7. 边缘 AI 的数据闭环与 OTA 更新

  8. NPU 初探------Ethos-U55 与 CMSIS-NN 性能对比

  9. 多模型协同------在单 MCU 上同时跑检测 + 分类 + 控制

  10. 从 TinyML 到 Edge Impulse------低代码平台的利与弊

相关推荐
新晨单片机设计2 小时前
S031A-基于STM32单片机指纹考勤系统【Proteus仿真+Keil程序+原理图】
stm32·单片机·嵌入式硬件·proteus·指纹考勤系统
新时代牛马3 小时前
`arch_timer`、`IPI5`、`HRTIMER`、`ksoftirqd` 的关系
网络·单片机·tcp/ip
元器件BOM超人3 小时前
STM32F103R8T6 中密度Cortex-M3 MCU 解析
stm32·单片机·嵌入式硬件
服务器硬件项目经理3 小时前
《超节点修仙录》· 第一卷·炼气期【第02话】 超节点的「身体构造」——CPU、GPU、内存、网卡都是干啥的
服务器·嵌入式硬件·gru·硬件工程·硬件
萌动的小火苗3 小时前
PCB设计中线宽、线距与过孔的基本概念
嵌入式硬件·pcb工艺
少卿.5554 小时前
wokwi搭建仿真超声波雷达测距判断(保姆级教学)
单片机·嵌入式硬件·arduino
C++ 老炮儿的技术栈4 小时前
机器人的坐标,像 XYZ 的坐标值,是用单精度浮点还是用双精度浮点?
c语言·c++·人工智能·单片机·c
深圳老胡5 小时前
STM32C562RET6 外部中断详解:从 EXTI 原理到 GPIO 外部中断实战
笔记·stm32·单片机·代码规范·1024程序员节
威嵌神州5 小时前
一台加固平板,能替掉几台现场设备?
嵌入式硬件·fpga开发·电脑·平板