第3章:数据采集与预处理

各位工程师朋友,大家好。今天我们来聊聊电池管理系统里最基础、也最容易被忽视的一环——数据采集与预处理。

你可能觉得,不就是测个电压、采个温度吗?有什么难的?

嗯,我当年也是这么想的。直到有一次,我在实验室里盯着一条剧烈抖动的电流曲线,死活找不到原因。后来才发现,是ADC采样时序和滤波参数没配好。那一次,我整整熬了两个通宵。

所以,这一章我会把踩过的坑、积累的经验,都摊开来跟你讲。

3.1 信号调理:让传感器信号“说人话”

传感器输出的信号,往往不是我们直接能用的。比如热电偶输出的是毫伏级电压,电流传感器输出的是4-20mA的电流。这些信号需要先“调理”一下。

信号调理主要做三件事:

  • 放大:把微弱的信号放大到ADC能识别的范围(通常是0-3.3V或0-5V)
  • 滤波:用硬件低通滤波器滤掉高频噪声。我习惯在运放后面加一个RC滤波器,截止频率设在100Hz左右
  • 电平转换:把双极性信号(比如±10V)转换成单极性信号
我的经验: 别小看信号调理。我曾经在一个项目中,因为运放选型不对,导致温漂严重,50℃时测量误差达到了5%。后来换成了仪表放大器,问题才解决。

3.2 ADC采样:精度与速度的博弈

ADC采样,说白了就是把模拟信号变成数字信号。这里有两个关键参数:分辨率和采样率。

参数 说明 我的建议
分辨率 12位、14位、16位等 电池电压检测建议用16位以上
采样率 每秒采样次数(SPS) 温度信号10Hz足够,电流信号需要100Hz以上
参考电压 内部参考或外部参考 我强烈建议用外部精密参考源

为什么会这样?因为电池电压的变化其实很慢,但电流变化很快(尤其是发生短路时)。你想想看,如果采样率太低,短路电流的峰值可能就漏掉了。

避坑指南: 我曾经遇到过ADC采样结果跳变的问题。排查了半天,发现是数字电源的开关噪声耦合到了模拟信号线上。后来把模拟地和数字地分开走线,问题就解决了。

3.3 滤波算法:把噪声“洗”干净

硬件滤波之后,信号里还是会有残留噪声。这时候就需要软件滤波上场了。我常用的有三种方法。

3.3.1 均值滤波

最简单,也最常用。就是把N次采样的值加起来,再除以N。

// 均值滤波示例
#define FILTER_N 10
uint16_t samples[FILTER_N];
uint8_t index = 0;

uint16_t mean_filter(uint16_t new_sample) {
    uint32_t sum = 0;
    samples[index++] = new_sample;
    if (index >= FILTER_N) index = 0;
    
    for (int i = 0; i < FILTER_N; i++) {
        sum += samples[i];
    }
    return (uint16_t)(sum / FILTER_N);
}

嗯,这里要注意:N值越大,滤波效果越好,但响应速度越慢。我个人习惯在温度检测上用N=20,电流检测上用N=5。

3.3.2 中值滤波

对付脉冲噪声特别有效。比如传感器偶尔受到电磁干扰,出现一个离谱的尖峰,均值滤波会被拉偏,但中值滤波完全不受影响。

// 中值滤波示例(冒泡排序法)
uint16_t median_filter(uint16_t *data, uint8_t len) {
    uint8_t i, j;
    uint16_t temp;
    
    // 复制数据
    uint16_t buf[len];
    for (i = 0; i < len; i++) buf[i] = data[i];
    
    // 冒泡排序
    for (i = 0; i < len-1; i++) {
        for (j = 0; j < len-1-i; j++) {
            if (buf[j] > buf[j+1]) {
                temp = buf[j];
                buf[j] = buf[j+1];
                buf[j+1] = temp;
            }
        }
    }
    return buf[len/2];
}
关键点: 中值滤波的窗口大小一般取奇数,比如3、5、7。窗口越大,对脉冲噪声的抑制效果越好,但计算量也越大。

3.3.3 卡尔曼滤波

这个就有点高级了。卡尔曼滤波能根据系统的动态模型,预测下一个值,然后用测量值来修正预测值。说白了,它是在“猜”真实值是多少。

我一般在SOC估算和电池内阻在线监测中用卡尔曼滤波。效果确实好,但调参是个技术活。

// 一维卡尔曼滤波简化版
typedef struct {
    float Q;  // 过程噪声协方差
    float R;  // 测量噪声协方差
    float P;  // 估计误差协方差
    float K;  // 卡尔曼增益
    float X;  // 状态估计值
} Kalman_t;

void Kalman_Init(Kalman_t *kf, float init_val) {
    kf->Q = 0.01;   // 根据经验调整
    kf->R = 0.1;    // 根据传感器精度调整
    kf->P = 1.0;
    kf->X = init_val;
}

float Kalman_Update(Kalman_t *kf, float measurement) {
    // 预测
    kf->P = kf->P + kf->Q;
    
    // 更新
    kf->K = kf->P / (kf->P + kf->R);
    kf->X = kf->X + kf->K * (measurement - kf->X);
    kf->P = (1 - kf->K) * kf->P;
    
    return kf->X;
}
避坑指南: 我曾经在卡尔曼滤波的参数调优上栽过跟头。Q值设得太小,滤波器反应迟钝;Q值设得太大,又会有噪声。后来我用了自适应卡尔曼滤波,让Q值根据残差动态调整,效果才满意。

3.4 数据归一化:让数据“站在同一起跑线”

归一化,就是把不同量纲的数据映射到同一个区间,通常是[0,1]或[-1,1]。

为什么要做归一化?你想想看,电压是3-4V,温度是-20到60℃,电流是-200到200A。如果不归一化,神经网络或者机器学习模型会天然地“偏爱”数值大的特征。

常用的归一化方法有两种:

  • Min-Max归一化:X_norm = (X - X_min) / (X_max - X_min)
  • Z-score标准化:X_norm = (X - μ) / σ

我个人习惯在热失控预警系统中用Min-Max归一化。因为电池的电压、温度范围是已知的,用Min-Max更直观。

我的经验: 归一化的参数(比如X_min、X_max)要在系统标定阶段就确定下来,并且固化到代码里。不要在运行过程中动态计算,否则模型会不稳定。

3.5 本章知识体系

下面这张图,是我自己梳理的数据采集与预处理流程。你可以把它当作一个检查清单。

数据采集与预处理流程 传感器信号 信号调理(放大+滤波+电平转换) ADC采样(分辨率+采样率) 软件滤波(均值/中值/卡尔曼) 数据归一化(Min-Max/Z-score)

好了,这一章的内容就到这里。数据采集与预处理,看似基础,但往往是整个系统成败的关键。希望我的这些经验,能帮你少走一些弯路。