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,省了不少。
第二个坑: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电流。
电流采样的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);
}
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控制与电机调速,这可是剃须刀的核心功能之一。