3、ADC采样与滤波:电池电压采样、充电电流采样、温度采样、软件滤波算法

好,咱们进入第三个核心环节。ADC采样与滤波。

说实话,在剃须刀这种小家电里,ADC采样看着简单,但坑特别多。我最早做一款便携式剃须刀时,就因为在采样上偷了懒,结果产品在低温环境下充电电压读数飘了0.3V,差点导致过充保护误触发。从那以后,我对采样和滤波就格外上心。

这一节,咱们把三个关键的采样对象——电池电压、充电电流、温度,以及配套的软件滤波算法,一次性讲透。

3.1 电池电压采样

电池电压是判断充电状态的核心依据。你想想看,如果电压都测不准,那后面的恒流、恒压、充满判断全是空中楼阁。

我一般用电阻分压的方式,把电池电压降到ADC的参考电压范围内。比如电池最高4.2V,ADC参考电压3.3V,那就用两个电阻分压,比例大概1.27:1。分压后,ADC输入端的电压范围就在0~3.3V之间。

这里有个细节:分压电阻的取值不能太大,也不能太小。太大,采样电流太小,容易受后级电路漏电流影响;太小,分压电阻本身会消耗电池能量。我个人习惯用100kΩ和47kΩ的组合,功耗和精度都能接受。

关键点:分压电阻的精度直接影响采样精度。建议用1%精度的电阻,别为了省几分钱用5%的,否则校准起来很麻烦。

代码实现上,我通常这样写:

// 假设ADC为12位,参考电压3.3V
#define ADC_REF_VOLTAGE    3.3f
#define ADC_RESOLUTION     4096.0f
#define DIVIDER_RATIO      (100.0f + 47.0f) / 47.0f   // 分压比

float GetBatteryVoltage(uint16_t adc_value)
{
    float voltage_at_pin = (float)adc_value * ADC_REF_VOLTAGE / ADC_RESOLUTION;
    float battery_voltage = voltage_at_pin * DIVIDER_RATIO;
    return battery_voltage;
}

3.2 充电电流采样

充电电流的采样,通常用检流电阻加差分放大器的方式。检流电阻串联在充电回路里,电阻两端会产生一个微小的压降,这个压降经过放大后送入ADC。

我在项目中遇到过一个问题:检流电阻的功率选小了。刚开始用0805封装的0.1Ω电阻,充电电流1A时,功耗是0.1W,电阻烫得厉害,阻值漂移,电流读数也跟着飘。后来换成了2512封装的,才稳定下来。

嗯,这里要注意:检流电阻的布局要远离热源,尤其是MOS管和电感。否则温度变化会引起阻值变化,影响采样精度。

我的经验:检流电阻的压降一般设计在50mV~100mV之间。太小了信噪比差,太大了功耗又高。比如1A电流,用0.05Ω电阻,压降50mV,功耗50mW,比较合理。

电流采样的代码逻辑:

// 假设放大器增益为50倍,检流电阻0.05Ω
#define SHUNT_RESISTOR      0.05f
#define AMP_GAIN            50.0f

float GetChargeCurrent(uint16_t adc_value)
{
    float voltage_at_pin = (float)adc_value * ADC_REF_VOLTAGE / ADC_RESOLUTION;
    float shunt_voltage = voltage_at_pin / AMP_GAIN;
    float current = shunt_voltage / SHUNT_RESISTOR;
    return current;
}

3.3 温度采样

温度采样,说白了就是用NTC热敏电阻。NTC的阻值随温度变化,通过一个固定电阻分压,ADC读到的电压就能反映温度。

我常用的NTC是10kΩ的,B值3950。分压电阻也用10kΩ,这样在25℃时,分压点正好是1.65V,处于ADC量程的中间,线性度最好。

温度计算不能直接用线性公式,得查表或者用Steinhart-Hart方程。我一般用查表法,把-10℃到60℃的温度-ADC值对应关系做成一个表格,运行时插值计算。这样速度快,精度也够。

避坑指南:我曾经因为NTC的布局离充电MOS管太近,导致温度读数比实际电池温度高了8℃。结果充电提前进入恒压阶段,充电时间变长。后来我把NTC贴到了电池表面,用导热胶固定,问题才解决。

温度查表代码示例:

// 温度-ADC值对应表(简化版)
const uint16_t temp_table[] = { 3800, 3600, 3400, 3100, 2800, 2500, 2200, 1900, 1600, 1300 };
const int8_t temp_value[]   = { -10,   0,   10,   20,   30,   40,   50,   60,   70,   80 };

int8_t GetTemperature(uint16_t adc_value)
{
    // 查表并线性插值
    for(int i = 0; i < 9; i++)
    {
        if(adc_value >= temp_table[i+1] && adc_value <= temp_table[i])
        {
            float ratio = (float)(adc_value - temp_table[i]) / (temp_table[i+1] - temp_table[i]);
            return temp_value[i] + (int8_t)(ratio * (temp_value[i+1] - temp_value[i]));
        }
    }
    return -99; // 超出范围
}

3.4 软件滤波算法

ADC采样值如果不做滤波,直接拿来用,你会被噪声搞疯的。尤其是充电电流采样,PWM开关噪声会叠加在信号上,读数跳来跳去。

我常用的滤波算法有三种,根据场景选:

滤波算法 适用场景 优点 缺点
中值滤波 温度采样 抗脉冲干扰强 响应慢
滑动平均滤波 电池电压采样 平滑效果好 有滞后
一阶低通滤波 充电电流采样 实现简单,实时性好 对高频噪声抑制有限

我个人最常用的是滑动平均滤波。取最近N次采样值,去掉最大值和最小值,然后求平均。这样既能滤除毛刺,又能保留真实变化趋势。

代码实现:

#define FILTER_LENGTH   8

uint16_t adc_buffer[FILTER_LENGTH];
uint8_t buffer_index = 0;

uint16_t MovingAverageFilter(uint16_t new_sample)
{
    uint32_t sum = 0;
    uint16_t min_val = 0xFFFF, max_val = 0;
    
    // 存入新数据
    adc_buffer[buffer_index] = new_sample;
    buffer_index = (buffer_index + 1) % FILTER_LENGTH;
    
    // 找最大最小值并求和
    for(int i = 0; i < FILTER_LENGTH; i++)
    {
        sum += adc_buffer[i];
        if(adc_buffer[i] < min_val) min_val = adc_buffer[i];
        if(adc_buffer[i] > max_val) max_val = adc_buffer[i];
    }
    
    // 去掉最大最小值后求平均
    sum = sum - min_val - max_val;
    return (uint16_t)(sum / (FILTER_LENGTH - 2));
}

小技巧:滤波窗口长度选2的幂次方,比如4、8、16。这样求平均时可以用移位操作代替除法,速度更快。我一般用8点滤波,效果和性能比较均衡。

最后说一句,滤波不是越强越好。滤波太狠,响应变慢,充电状态切换会滞后。我曾经把滤波窗口设到32点,结果充满判断延迟了2秒,电池都快过充了才切换。嗯,这个度要把握好。

好了,ADC采样与滤波就讲到这里。下一节咱们聊聊充电状态机的设计,那才是整个充电管理的灵魂。