4、数据预处理与校准:数字滤波算法、线性校准与查表法、温度补偿策略
各位同学,咱们继续往下聊。上一节我们把蓝牙模块的硬件和底层驱动搞定了,数据也能收上来了。但问题来了——你拿万用表测个1.5V的电池,结果读数在1.48V到1.52V之间来回跳,这谁敢信?
嗯,这就是我们今天要解决的核心问题:数据预处理与校准。说白了,就是让采集到的数据更准、更稳、更可信。
4.1 数字滤波算法:让数据不再“抖”
我刚开始做嵌入式采集时,遇到过一件特尴尬的事。测一个稳定的5V电源,结果串口打印出来的数据像心电图一样上下波动。后来才明白,这不是硬件坏了,是噪声在捣乱。
数字滤波,就是软件层面的“降噪器”。这里我重点讲两种最实用的:中值滤波和滑动平均滤波。
4.1.1 中值滤波:专治“毛刺”
中值滤波的原理很简单:连续采样N次,排序后取中间那个值。比如你采了5次:1.48, 1.52, 1.50, 1.49, 1.51,排序后是1.48, 1.49, 1.50, 1.51, 1.52,中间值就是1.50。
这招对付突发性的脉冲干扰特别管用。比如电机启动瞬间的电磁干扰,或者开关电源的尖峰噪声。
核心要点:
- 采样次数N一般取奇数,3、5、7都行
- N越大,滤波效果越好,但响应速度变慢
- 适合变化缓慢的信号,比如温度、电压
// 中值滤波示例代码
#define FILTER_N 5
float median_filter(float new_value) {
static float buffer[FILTER_N];
static uint8_t index = 0;
float temp[FILTER_N];
float median;
// 存入新数据
buffer[index] = new_value;
index = (index + 1) % FILTER_N;
// 拷贝到临时数组并排序
memcpy(temp, buffer, sizeof(temp));
// 简单冒泡排序(N小的时候够用)
for (int i = 0; i < FILTER_N - 1; i++) {
for (int j = 0; j < FILTER_N - 1 - i; j++) {
if (temp[j] > temp[j + 1]) {
float t = temp[j];
temp[j] = temp[j + 1];
temp[j + 1] = t;
}
}
}
// 取中间值
median = temp[FILTER_N / 2];
return median;
}
我的经验:实际项目中,我一般把N设为5。再大就影响实时性了,尤其是蓝牙传输时,数据更新频率本来就有限。
4.1.2 滑动平均滤波:平滑如丝
滑动平均滤波,就是维护一个固定长度的队列,每次取队列里所有数据的平均值。新数据进来,旧数据出去,像个“滑动窗口”。
这招对周期性噪声和随机噪声都有不错的效果。我做过一个项目,测一个缓慢变化的液位信号,用了滑动平均后,曲线平滑得像画出来的一样。
// 滑动平均滤波示例
#define AVG_N 10
float moving_average(float new_value) {
static float buffer[AVG_N] = {0};
static uint8_t index = 0;
static float sum = 0;
static uint8_t count = 0;
// 减去旧值,加上新值
sum -= buffer[index];
sum += new_value;
buffer[index] = new_value;
index = (index + 1) % AVG_N;
// 未填满时按实际数量平均
if (count < AVG_N) count++;
return sum / count;
}
注意:滑动平均有个“滞后效应”。窗口越大,滞后越明显。如果你要测快速变化的信号(比如过零检测),窗口别设太大,3~5个点就够了。
4.2 线性校准与查表法:让读数“准”起来
滤波搞定了稳定性,接下来就是精度问题。你想想看,ADC采集到的原始值,跟实际物理量之间,往往不是完美的线性关系。这时候就需要校准了。
4.2.1 线性校准:两点定天下
最简单的校准方法,就是两点校准。比如你测电压,用标准源给两个已知电压值V1和V2,记录ADC读数为D1和D2。然后就能算出斜率和截距:
// 线性校准公式
// 实际值 = 斜率 * ADC值 + 截距
// 斜率 = (V2 - V1) / (D2 - D1)
// 截距 = V1 - 斜率 * D1
float calibrate_linear(float adc_value) {
// 假设校准参数已存储在EEPROM中
static float slope = 0.0;
static float offset = 0.0;
static uint8_t calibrated = 0;
if (!calibrated) {
// 从EEPROM读取校准参数
slope = read_eeprom_float(0x00);
offset = read_eeprom_float(0x04);
calibrated = 1;
}
return slope * adc_value + offset;
}
避坑指南:我曾经犯过一个低级错误——校准点的范围没覆盖实际测量范围。比如你只校准了1V和5V,结果去测0.1V的信号,误差大得离谱。记住:校准范围一定要覆盖你的实际测量范围。
4.2.2 查表法:非线性也不怕
有些传感器天生就是非线性的,比如热电偶、NTC热敏电阻。这时候线性校准就不够用了。怎么办?查表法。
查表法的思路很简单:把已知的标准值做成一个表格,实际测量时,根据ADC值去表格里找对应的物理量。如果落在两个点之间,就用线性插值。
// 查表法 + 线性插值示例
// 假设是NTC热敏电阻的温度-电阻对照表
typedef struct {
uint16_t adc_value; // ADC原始值
float temperature; // 对应温度(°C)
} lookup_table_t;
// 简化的查表数据(实际项目中可能有几十个点)
const lookup_table_t temp_table[] = {
{100, -20.0},
{200, 0.0},
{400, 25.0},
{600, 50.0},
{800, 75.0},
{1000, 100.0}
};
#define TABLE_SIZE (sizeof(temp_table) / sizeof(temp_table[0]))
float lookup_interpolate(uint16_t adc_val) {
// 边界处理
if (adc_val <= temp_table[0].adc_value) {
return temp_table[0].temperature;
}
if (adc_val >= temp_table[TABLE_SIZE - 1].adc_value) {
return temp_table[TABLE_SIZE - 1].temperature;
}
// 查找区间
for (int i = 0; i < TABLE_SIZE - 1; i++) {
if (adc_val >= temp_table[i].adc_value &&
adc_val <= temp_table[i + 1].adc_value) {
// 线性插值
float ratio = (float)(adc_val - temp_table[i].adc_value) /
(temp_table[i + 1].adc_value - temp_table[i].adc_value);
return temp_table[i].temperature +
ratio * (temp_table[i + 1].temperature - temp_table[i].temperature);
}
}
return 0.0; // 不应该到这里
}
我的建议:查表法的表格点不要太多,够用就行。我一般每10°C或每20°C取一个点,插值后的精度完全够用。表格太多反而浪费Flash空间。
4.3 温度补偿策略:让精度“稳”住
最后聊一个容易被忽略的问题——温度漂移。你想想看,万用表在25°C的实验室里校准得好好的,拿到零下10°C的户外,读数可能就偏了。
为什么会这样?因为ADC参考电压、运放偏置、电阻值都会随温度变化。这就是温度漂移。
4.3.1 硬件补偿 vs 软件补偿
硬件补偿成本高、电路复杂,我一般优先考虑软件补偿。软件补偿的核心思路是:先测温度,再根据温度修正读数。
具体做法是:在电路板上加一个温度传感器(比如DS18B20或NTC),实时监测环境温度。然后根据预先测好的温度-误差曲线,对测量结果进行修正。
// 温度补偿示例
// 假设在25°C时校准,温度每偏离1°C,读数偏差0.02%
float temperature_compensate(float measured_value, float current_temp) {
const float REF_TEMP = 25.0; // 参考温度
const float TC = 0.0002; // 温度系数 (0.02%/°C)
float temp_diff = current_temp - REF_TEMP;
float correction = measured_value * TC * temp_diff;
return measured_value - correction;
}
注意:温度系数不是一成不变的。不同量程、不同档位,温度特性可能不一样。我建议你针对每个量程单独做温度测试,建立各自的补偿参数。
4.3.2 实战中的温度补偿策略
我做过一个手持式万用表项目,温度补偿这块折腾了挺久。最后总结了几条经验:
- 分段补偿:把温度范围分成几段(比如-20~0°C、0~25°C、25~50°C、50~70°C),每段用不同的补偿系数
- 动态校准:每次开机时,如果检测到温度变化超过5°C,自动触发一次内部校准
- 记录温度:在蓝牙传输的数据包中,把当前温度也带上。这样上位机可以二次修正
总结一下:
- 数字滤波:中值滤波治毛刺,滑动平均治噪声
- 线性校准:两点搞定,简单粗暴
- 查表法:非线性传感器的救星
- 温度补偿:别让温度毁了你的精度
这些方法组合使用,你的万用表蓝牙模块就能输出稳定、准确的数据了。下一节我们聊聊蓝牙协议栈的配置和实际传输调试,到时候见。