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采样与滤波就讲到这里。下一节咱们聊聊充电状态机的设计,那才是整个充电管理的灵魂。