2. 静态标定方法:最小二乘法线性拟合、多项式拟合、分段线性插值、查表法标定
静态标定,说白了就是给传感器“定标尺”。你拿一个标准量去怼它,看它输出什么,然后反过来建立输入和输出之间的数学关系。这步做不好,后面什么滤波、融合都是白搭。
我个人习惯把静态标定分成两大类:模型驱动和数据驱动。前者假设传感器特性符合某种数学形式,后者直接拿数据说话。今天聊的四种方法,正好覆盖了这两个方向。
核心观点:没有最好的标定方法,只有最适合你项目约束的方法。精度、计算资源、存储空间、实时性,你得自己权衡。
2.1 最小二乘法线性拟合
这是最基础、最常用的方法。假设传感器输出 y 和被测物理量 x 是线性关系:y = kx + b。我们通过实验拿到 N 组数据 (x_i, y_i),然后找一条直线,让所有点到这条直线的“垂直距离平方和”最小。
公式很简单,我就不推了。直接上代码:
// 最小二乘法线性拟合,输入x和y数组,长度n,输出斜率k和截距b
void linear_fit(float *x, float *y, int n, float *k, float *b) {
float sum_x = 0, sum_y = 0, sum_xy = 0, sum_x2 = 0;
for (int i = 0; i < n; i++) {
sum_x += x[i];
sum_y += y[i];
sum_xy += x[i] * y[i];
sum_x2 += x[i] * x[i];
}
float denom = n * sum_x2 - sum_x * sum_x;
if (fabs(denom) < 1e-12) {
// 防止除零,理论上不会发生
*k = 0; *b = sum_y / n;
return;
}
*k = (n * sum_xy - sum_x * sum_y) / denom;
*b = (sum_y * sum_x2 - sum_x * sum_xy) / denom;
}
我的经验:做线性拟合前,一定要先画散点图看一眼。如果数据明显弯曲还硬套直线,那误差会大得离谱。我曾经在一个压力传感器项目上吃过这个亏——数据点呈S形,我愣是拿直线去拟合,结果标定完精度只有0.5%,后来换成多项式拟合直接干到0.05%。
线性拟合的适用场景:
- 传感器本身线性度好(比如大部分应变式压力传感器)
- 测量范围窄,非线性可以忽略
- MCU资源紧张,算不了复杂模型
2.2 多项式拟合
当传感器特性明显弯曲时,多项式拟合就派上用场了。一般用二次或三次多项式就够了,次数太高反而容易过拟合。
多项式拟合的核心是构造范德蒙矩阵,然后解正规方程。我建议用二次多项式起步:y = a0 + a1*x + a2*x²。
// 二次多项式拟合,使用最小二乘法
// 输入x, y数组,长度n,输出系数a0, a1, a2
void poly2_fit(float *x, float *y, int n, float *a0, float *a1, float *a2) {
float s0 = n, s1 = 0, s2 = 0, s3 = 0, s4 = 0;
float t0 = 0, t1 = 0, t2 = 0;
for (int i = 0; i < n; i++) {
float xi = x[i], yi = y[i];
float xi2 = xi * xi;
s1 += xi; s2 += xi2;
s3 += xi2 * xi;
s4 += xi2 * xi2;
t0 += yi;
t1 += yi * xi;
t2 += yi * xi2;
}
// 解3元一次方程组(克莱姆法则或高斯消元)
// 这里省略具体求解代码,实际工程中建议用矩阵运算库
// ...
}
避坑指南:我曾经在一个温度传感器项目上用七次多项式拟合,结果在标定点之间振荡得像心电图一样。次数越高,数值稳定性越差。我的建议是:多项式次数不要超过5,能用3次就别用4次。
多项式拟合的优缺点:
| 优点 | 缺点 |
|---|---|
| 能拟合非线性特性 | 高次时数值不稳定 |
| 数学形式简单,计算量可控 | 全局拟合,局部误差可能很大 |
| 适合离线标定 | 对异常点敏感 |
2.3 分段线性插值
这个方法很实用。把整个量程切成若干段,每段内用直线连接。说白了就是“化曲为直”,用多段折线去逼近真实曲线。
分段线性插值的核心是分段点的选择。我一般这么干:
- 在曲率大的地方多取点,曲率小的地方少取点
- 保证每段内的非线性误差小于传感器本身精度的1/3
- 分段数通常取8~32段,具体看MCU的Flash和RAM
// 分段线性插值查表
// table_x: 分段点物理量值,table_y: 对应传感器输出值
// n: 分段点数量,x: 当前测量值
float linear_interp(float *table_x, float *table_y, int n, float x) {
// 边界处理
if (x <= table_x[0]) return table_y[0];
if (x >= table_x[n-1]) return table_y[n-1];
// 二分查找所在区间
int lo = 0, hi = n - 1;
while (hi - lo > 1) {
int mid = (lo + hi) / 2;
if (x < table_x[mid]) hi = mid;
else lo = mid;
}
// 线性插值
float t = (x - table_x[lo]) / (table_x[hi] - table_x[lo]);
return table_y[lo] + t * (table_y[hi] - table_y[lo]);
}
我的习惯:分段线性插值配合等距分段可以优化查找速度。如果分段点是等间隔的,直接用除法定位区间,不用二分查找,速度能快一个数量级。我在一个汽车氧传感器项目上就是这么干的,标定表128个点,每次查表只需要3条指令。
2.4 查表法标定
查表法是最直接的方法——把标定数据直接存成表格,用的时候查一下。适合传感器特性非常复杂、没法用简单数学公式描述的情况。
查表法的关键设计点:
- 表格密度:点越多精度越高,但占用的存储也越大
- 查找算法:顺序查找、二分查找、索引查找
- 插值方式:查表通常配合插值使用,纯查表(最近邻)精度太低
| 查找方式 | 时间复杂度 | 适用场景 |
|---|---|---|
| 顺序查找 | O(n) | 表格很小(<16点) |
| 二分查找 | O(log n) | 通用,表格有序 |
| 索引查找 | O(1) | 等距分段,速度要求极高 |
我曾经踩过的坑:在一个红外测温项目上,我把标定表做得特别密(每0.1℃一个点),结果Flash爆了。后来改成每1℃一个点,配合线性插值,精度只损失了0.05℃,但存储节省了90%。记住:表格密度不是越高越好,够用就行。
2.5 四种方法对比与选型
这四种方法各有各的脾气。我整理了一张对比表,方便你选型时参考:
| 方法 | 精度 | 计算量 | 存储需求 | 适用场景 |
|---|---|---|---|---|
| 线性拟合 | 低(依赖线性度) | 极低 | 极低(2个系数) | 线性传感器、资源受限 |
| 多项式拟合 | 中高 | 低(3~5次乘法) | 低(3~6个系数) | 缓变非线性、离线标定 |
| 分段线性插值 | 高(分段足够时) | 中(查找+插值) | 中(分段点×2) | 强非线性、实时性要求高 |
| 查表法 | 最高(表格够密时) | 低(查表+插值) | 高(取决于表格大小) | 任意特性、存储充足 |
选型建议:
- 如果MCU是8位机、Flash只有几KB,老老实实用线性拟合或低次多项式
- 如果传感器非线性严重但变化平缓,分段线性插值是性价比之王
- 如果精度要求极高且存储管够,查表法+线性插值是最稳妥的方案
- 如果既要精度又要速度,可以考虑混合方案:粗查表+细插值
总结一下:静态标定没有银弹。线性拟合快但糙,多项式拟合灵活但怕高次,分段线性插值均衡但需要调分段点,查表法精度高但吃存储。我的建议是——从最简单的开始,测完精度不够再升级。别一上来就搞七次多项式或者1024点大表,那是给自己找麻烦。