4、ADC采样与滤波:电池电压检测、电机电流采样、NTC温度检测、滑动平均滤波与中值滤波实现

做剃须刀固件,ADC采样这块儿,我敢说它是整个系统的「眼睛」和「耳朵」。电池还剩多少电?电机是不是堵转了?刀头温度是不是过高?这些信息全靠ADC来感知。如果采样不准,后面的逻辑全是白搭。

今天咱们就把ADC采样和滤波这件事彻底聊透。我会结合我在剃须刀项目里踩过的坑,把电池电压检测、电机电流采样、NTC温度检测,还有两种最常用的滤波算法——滑动平均滤波和中值滤波,一一讲清楚。

4.1 电池电压检测:别被「虚电」骗了

电池电压检测,听起来简单吧?分压电阻一接,ADC一读,完事。但实际做起来,坑多着呢。

第一个坑:分压电阻的选型。 锂电池满电4.2V,放电截止大概3.0V。MCU的ADC参考电压通常是3.3V或内部参考1.2V。所以必须用电阻分压,把电池电压降到ADC可测范围内。

我个人习惯用两个100kΩ的电阻做1:1分压。为什么选100k?因为功耗低。你想想看,剃须刀是电池供电的,分压电阻一直在耗电。如果用10kΩ,分压电流就是4.2V / 20kΩ = 0.21mA。看似不大,但待机时这就是个累赘。100kΩ的话,电流只有0.042mA,省了不少。

提示: 分压电阻精度建议用1%的。我见过有人用5%的,结果同一批产品测出来的电压偏差能达到0.2V,这会导致电量显示不准,用户投诉。

第二个坑:ADC采样时机。 电池电压在电机启动瞬间会剧烈跌落。如果你在电机启动时采样,读到的电压可能只有2.8V,但实际上电池还有3.7V。这就是「虚电」现象。

我的做法是:电机启动后延迟50ms再采样,或者干脆在电机停止时采样。代码里加个标志位,电机运行时禁止触发电压检测。

// 电池电压采样函数
uint16_t get_battery_voltage(void) {
    // 等待电机停止
    while(motor_running_flag) {
        // 可以加个超时保护,防止死等
        if(timeout_count++ > 100) break;
        delay_ms(1);
    }
    
    // 启动ADC转换
    ADC_StartConversion();
    while(!ADC_IsConversionComplete());
    
    uint16_t adc_value = ADC_GetResult();
    
    // 计算实际电压:分压比1:1,参考电压3.3V,12位ADC
    float voltage = (float)adc_value * 3.3f / 4096.0f * 2.0f;
    
    return (uint16_t)(voltage * 100); // 返回电压值,单位0.01V
}

4.2 电机电流采样:堵转检测的关键

电机电流采样,说白了就是检测电机有没有堵转。剃须刀堵转时,电流会飙升到正常值的3-5倍。如果不及时处理,轻则烧电机,重则把用户胡子烧焦(开个玩笑,但确实危险)。

采样方式有两种:高端采样低端采样。低端采样是在电机和GND之间串一个小电阻,用ADC测电阻两端的电压。高端采样是在电源和电机之间串电阻,需要差分放大器,成本高一些。

我建议用低端采样,简单、便宜。采样电阻选0.1Ω,功率1W。正常电流0.5A时,采样电压只有0.05V,需要运放放大。放大倍数设20倍,这样ADC读到1V对应1A电流。

警告: 采样电阻的功率一定要留够余量。我曾经用0.1Ω/0.25W的电阻,结果堵转时电流3A,功率P = I²R = 9 * 0.1 = 0.9W,直接冒烟了。后来换成1W的,再也没出过问题。

电流采样的ADC转换要快。电机电流变化很快,堵转可能在几毫秒内发生。我一般用ADC的连续转换模式,每100μs采样一次,然后做滑动平均滤波。

4.3 NTC温度检测:别让刀头烫到用户

NTC温度检测,用来监测刀头温度。剃须刀连续工作几分钟后,刀头温度可能升到60-70°C。虽然不会烫伤,但用户体验很差。更严重的是,如果电池温度过高,可能引发安全问题。

NTC的接法很简单:一个10kΩ的NTC和一个10kΩ的精密电阻串联,分压后接ADC。温度越高,NTC阻值越小,分压值越低。

但这里有个问题:NTC的阻值和温度不是线性关系。直接用ADC值换算温度,误差很大。我一般用查表法,把温度-ADC值对应关系做成表格,用线性插值计算。

// NTC温度查表,10k NTC + 10k 上拉电阻,12位ADC
const uint16_t temp_table[][2] = {
    {0,   4095},   // 0°C
    {10,  3800},   // 10°C
    {20,  3400},   // 20°C
    {30,  2900},   // 30°C
    {40,  2400},   // 40°C
    {50,  1900},   // 50°C
    {60,  1500},   // 60°C
    {70,  1150},   // 70°C
    {80,  900},    // 80°C
};

uint8_t get_temperature(uint16_t adc_value) {
    // 线性插值
    for(uint8_t i = 0; i < 8; i++) {
        if(adc_value >= temp_table[i+1][1] && adc_value <= temp_table[i][1]) {
            uint16_t delta_adc = temp_table[i][1] - temp_table[i+1][1];
            uint8_t delta_temp = temp_table[i+1][0] - temp_table[i][0];
            uint16_t offset = adc_value - temp_table[i+1][1];
            return temp_table[i+1][0] + (delta_temp * offset) / delta_adc;
        }
    }
    return 0xFF; // 超出范围
}

4.4 滑动平均滤波:平滑数据,消除噪声

ADC采样天然带有噪声。电源纹波、电机换向干扰、甚至手指触摸都会引入噪声。直接拿原始数据做判断,结果会忽高忽低,没法用。

滑动平均滤波,说白了就是取最近N次采样的平均值。N越大,数据越平滑,但响应越慢。我一般取8或16次。

实现上,用环形缓冲区最方便。每次新数据进来,覆盖最旧的数据,然后重新计算平均值。

#define FILTER_SIZE 16

static uint16_t filter_buffer[FILTER_SIZE];
static uint8_t filter_index = 0;
static uint32_t filter_sum = 0;

uint16_t moving_average_filter(uint16_t new_value) {
    // 减去最旧的数据
    filter_sum -= filter_buffer[filter_index];
    // 存入新数据
    filter_buffer[filter_index] = new_value;
    // 加上新数据
    filter_sum += new_value;
    // 更新索引
    filter_index = (filter_index + 1) % FILTER_SIZE;
    // 返回平均值
    return (uint16_t)(filter_sum / FILTER_SIZE);
}
重点: 初始化时,要把缓冲区全部填满,否则前几次平均值会偏小。我一般在系统初始化时连续采样16次填满缓冲区。

4.5 中值滤波:干掉毛刺,保留边缘

滑动平均滤波对高斯噪声效果好,但对突发性毛刺(比如电机换向产生的尖峰)效果一般。这时候中值滤波就派上用场了。

中值滤波的原理很简单:取N次采样值,排序后取中间值。N一般取奇数,比如3、5、7。N越大,去毛刺效果越好,但计算量也越大。

我一般用3点中值滤波,计算量小,效果也不错。对于剃须刀这种对实时性要求高的场景,3点中值滤波足够了。

uint16_t median_filter_3(uint16_t a, uint16_t b, uint16_t c) {
    // 比较三个值,取中间值
    if(a > b) {
        uint16_t temp = a;
        a = b;
        b = temp;
    }
    if(b > c) {
        uint16_t temp = b;
        b = c;
        c = temp;
    }
    if(a > b) {
        uint16_t temp = a;
        a = b;
        b = temp;
    }
    return b; // 中间值
}

实际使用时,我会把滑动平均滤波和中值滤波结合起来。先用中值滤波去掉毛刺,再用滑动平均滤波平滑数据。效果非常好。

4.6 实战建议:滤波策略的选择

不同场景用不同的滤波策略,这是我的经验总结:

采样对象 推荐滤波方式 说明
电池电压 滑动平均滤波(16点) 电压变化慢,需要平滑
电机电流 中值滤波(3点)+ 滑动平均(8点) 电流有毛刺,需要快速响应
NTC温度 滑动平均滤波(8点) 温度变化慢,简单平滑即可

嗯,这里要注意一点:滤波会引入延迟。滑动平均16点,如果采样率是1kHz,延迟就是16ms。对于电池电压和温度检测,这点延迟无所谓。但对于电机堵转检测,16ms可能就太长了。所以电流检测我用8点滑动平均,延迟只有8ms,够用。

好了,ADC采样与滤波这部分就讲到这里。下一章咱们聊聊PWM控制与电机调速,这可是剃须刀的核心功能之一。