摘要:工业设备的振动监测是边缘 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 个特征
特征有物理意义,可解释性强
本项目提取的特征:
# 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 数据集准备
# 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 层全连接就够
# 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()典型训练结果:
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_MEMORY5.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。
确定方法:
从 4KB 开始尝试
失败就增加一倍
成功后再减 1KB,找到临界值
最终值加 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 倍、流量成本为零、数据不出设备。
三个核心要点:
MCU 上的 AI 不是"缩小版 GPT",而是特征工程 + 小模型 + 量化
特征工程比模型设计更重要——好的特征能让简单模型达到好效果
量化是必经之路——float32 模型在 MCU 上又大又慢
这是付费专栏第一季《嵌入式系统调优高手课》的收尾篇。20 篇文章涵盖了 RTOS 内核、内存池、链接脚本、优先级反转、编译器优化、中断风暴、Cache 一致性、低功耗、日志系统、栈保护、固件瘦身、OOP、状态机、安全启动、防逆向、敏感数据存储、RISC-V 移植、TinyML。从底层汇编到系统架构,从性能优化到安全加固,构成一套完整的嵌入式系统调优方法论。
第二季《嵌入式边缘 AI 实战:从 STM32 到 TinyML 的模型部署与优化》即将推出,将深入以下主题:
TFLite Micro 架构剖析(interpreter、arena、op resolver)
模型量化进阶(per-channel、混合量化、量化感知训练)
CMSIS-NN 与 XNNPACK 深度对比
关键词唤醒(KWS)实战——音频流 + MFCC + CNN
手势识别——ToF 传感器 + 轻量级分类
模型剪枝与知识蒸馏
边缘 AI 的数据闭环与 OTA 更新
NPU 初探——Ethos-U55 与 CMSIS-NN 性能对比
多模型协同——在单 MCU 上同时跑检测 + 分类 + 控制
从 TinyML 到 Edge Impulse——低代码平台的利与弊