4. 边缘AI数据采集与预处理

边缘AI模型在STM32上能否跑出预期精度,往往不取决于网络结构本身,而取决于喂给模型的数据是否干净、时序是否对齐、量纲是否一致。本章围绕ADC、DCMI、I2S、IMU四类典型外设,讲清从物理信号到模型输入张量之间的完整处理链,并给出可复用的数据管道设计方法。

一、四类外设的采集特性与数据形态

STM32上常用的四类采集通道,其数据形态差异很大,预处理策略也必须区别对待:

  • ADC:采集振动、电流、温度等模拟量,输出12位或16位整数,采样率由定时器触发决定,典型用于1kHz~50kHz的振动信号。
  • DCMI:并行摄像头接口,配合DMA直接搬运图像帧到缓冲区,输出灰度或RGB565像素矩阵,典型用于96×96或128×128的视觉输入。
  • I2S:数字麦克风或音频编解码接口,输出PCM采样序列,典型16kHz、16位单声道,用于关键词唤醒或异常声音检测。
  • IMU:通过SPI/I2C读取加速度计和陀螺仪,输出三轴加速度与三轴角速度,典型采样率100Hz~1kHz,用于姿态与动作识别。

这四类数据的共同点是:原始值都是整数,且带有各自的量纲和偏置。ADC的零点可能不在2048,IMU存在零偏,麦克风有直流分量。如果直接把原始整数送进模型,精度会明显下降。

二、滑动窗口:把连续流切成模型可吃的样本

模型输入是固定长度的张量,而传感器输出是连续流,因此必须用滑动窗口切分。关键参数有三个:窗口长度W、步长S、以及是否重叠。

以IMU动作识别为例:采样率100Hz,窗口长度W=128(即1.28秒),步长S=64(50%重叠)。这样每个窗口包含128个六轴样本,重叠可以增加样本数量并保留动作边界信息。对于振动ADC,常用W=256、S=128;对于音频,常用W=512(16kHz下约32ms)、S=256。

实现上推荐用环形缓冲区(ring buffer)配合DMA半满/全满中断。DMA把新数据写入环形缓冲区尾部,当累积到S个新样本时,触发一次窗口提取。这样CPU只在窗口就绪时被唤醒,避免逐样本中断带来的开销。

#define WIN_LEN   128
#define HOP_LEN   64
#define CH_NUM    6

static int16_t ring[WIN_LEN * 2][CH_NUM];
static volatile uint32_t wr_idx = 0;

void imu_dma_callback(int16_t *new_samples, uint32_t n) {
    for (uint32_t i = 0; i < n; i++) {
        for (int c = 0; c < CH_NUM; c++)
            ring[wr_idx % (WIN_LEN * 2)][c] = new_samples[i * CH_NUM + c];
        wr_idx++;
        if (wr_idx % HOP_LEN == 0 && wr_idx >= WIN_LEN) {
            extract_window(ring, wr_idx, WIN_LEN, CH_NUM);
        }
    }
}

三、归一化与滤波:让数据落在模型熟悉的区间

归一化的目标是把不同量纲的通道映射到统一区间。常用两种方式:

  • Min-Max归一化:x' = (x - min) / (max - min),适合幅值范围已知且稳定的信号,如ADC振动。
  • Z-Score标准化:x' = (x - μ) / σ,适合存在零偏和增益差异的IMU,需要在线估计均值和标准差。

在STM32上,Min-Max可以用定点数实现,避免浮点开销。例如把min和max预先算成Q15格式,归一化后直接得到int8_t或int16_t的模型输入。Z-Score则建议用滑动均值/方差递推公式,每来一个新样本更新一次,避免存储整个窗口。

滤波方面,ADC振动信号常用二阶IIR低通或带通滤波器,截止频率根据机械故障特征频率设定;IMU常用一阶低通或互补滤波融合加速度与陀螺仪;音频常用预加重和Mel滤波器组。滤波系数可以用Python的scipy.signal设计后导出为C数组,在STM32上以direct form II transposed结构实现,每样本只需几次乘加。

滤波和归一化的顺序建议:先滤波去噪,再归一化。如果先归一化,噪声会被放大到与信号同一量级,滤波效果变差。

四、可复用数据管道的结构设计

把上述步骤串起来,就形成一条数据管道。管道的核心是“采集—缓冲—窗口—预处理—输出张量”五段,但每段都应该是可替换的模块,而不是写死的代码。推荐用函数指针或结构体描述管道:

typedef struct {
    void (*init)(void);
    int  (*read)(int16_t *buf, uint32_t n);
    void (*preprocess)(int16_t *win, int8_t *out, uint32_t len);
} data_pipe_t;

static const data_pipe_t imu_pipe = {
    .init = imu_init,
    .read = imu_read_dma,
    .preprocess = imu_norm_filter
};

这样更换传感器时只需替换read和preprocess,窗口逻辑和缓冲区管理可以复用。对于DCMI图像,预处理通常是缩放+灰度化+归一化;对于I2S音频,是分帧+加窗+Mel滤波。虽然具体算子不同,但管道骨架一致。

STM32边缘AI数据管道:采集 → 缓冲 → 窗口 → 预处理 → 张量 采集层 ADC · 振动/电流 DCMI · 图像帧 I2S · PCM音频 IMU · 六轴 缓冲层 DMA 环形缓冲区 半满/全满中断 写指针 wr_idx 窗口层 W = 128 样本 S = 64 步长 50% 重叠 预处理层 IIR 滤波 Min-Max / Z-Score 定点 Q15 输出 模型输入张量 IMU: [1,128,6] int8 | 音频: [1,512] int8 | 图像: [1,96,96,1] int8 形状与量化参数由模型输入层决定,管道末端直接对接 TFLite Micro 输入缓冲 可复用要点 ① 采集层用函数指针隔离外设差异;② 缓冲层统一用环形缓冲区 + DMA 中断; ③ 窗口层参数 W/S 可配置;④ 预处理层按信号类型替换算子,输出统一为 int8 张量。

五、工程实现中的关键细节

第一,时间戳对齐。多传感器融合时,IMU和麦克风的采样时刻必须对齐。建议在DMA中断中记录HAL_GetTick()或DWT周期计数,窗口提取时按时间戳插值对齐,而不是假设采样率绝对精确。

第二,内存布局。TFLite Micro的输入张量要求连续内存,且通常是[1, H, W, C]或[1, T, C]。窗口提取后应直接写入模型输入缓冲区,避免中间拷贝。对于IMU的[1,128,6],可以按时间步优先排列,即t0_c0, t0_c1, ..., t0_c5, t1_c0, ...。

第三,定点化。STM32F4/F7/H7带FPU,可以用float做预处理,但L0/G0等无FPU的型号建议全程定点。归一化时把scale和zero_point预先算好,用(x - zp) * scale的整数近似实现。

不要在中断里做滤波和归一化。中断只负责搬运数据到环形缓冲区,预处理放在主循环或RTOS任务中执行,否则会阻塞后续DMA请求,导致采样丢点。

第四,验证方法。在PC上用Python复现同样的窗口和预处理逻辑,把STM32输出的张量通过串口或SWO导出,与PC端结果逐元素对比。如果误差在量化允许范围内(通常int8下±1),说明管道正确。这一步能快速定位是采集丢点、窗口错位还是归一化参数不一致。

把以上四层——采集、缓冲、窗口、预处理——封装成独立模块后,更换传感器或调整模型输入形状时,只需修改配置参数和预处理算子,管道骨架保持不变。这正是边缘AI数据管道可复用性的核心价值。