
目录1 引 言2 原 理3 代码实现4 工程应用5 总 结1引 言这里先提出一个问题如果调试过程遇到ADC采样值和标准值对不上你会怎么解决这个问题在采集卡上很常见最开始接触工控模拟量采集卡因为这个现象一度调试到怀疑人生调到最后甚至都开始怀疑是不是ADC调理电路焊错了料其实并不是料或设计有问题是因为实际应用中由于各种元件的特性和误差注定不可能和标准设计一模一样要解决这个问题就涉及到本期内容要提到的线性校准。2原 理以常见的12位分辨率、0-20mA量程的模拟量采集卡为例20mA对于标准码值4095理论上整个采集数据应该是一条通过原点斜率固定的直线如下图这样但正如开头所说实际因为元件的自有误差、ADC电压参考偏移等等这些因素综合影响下采样值就偏离了标准值采样值可能就像下图这样遇到这种情况如果直接用采样值进行转换会发现最终转换后的实际电流值完全对不上毫无精度可言这个时候就需要人为进行纠偏也就是通过线性拟合将采样值通过线性变换拟合到标准采样值上去最简单的就是进行线性校准它的工作原理非常简单分别取4mA、20mA的实际采样值与4mA、20mA对应的标准采样值进行计算求出增益系数k和偏移系数b最终会得到一个关系ykxb当每次输入实际采集值x时通过修正就会得到准确的标准采集值y转换出来就是没有系统误差的输入电流值这就是用的最多的两点式线性校准也叫零满度校准当然这里还有会一个问题那就是实际采集值不仅偏离标准值而且很可能还是非线性的偏差这时只需要用分段线性校准对每段符合线性的误差进行一个拟合最终也能实现预期的设计精度。3代码实现按照上述的工作原理将其转化为可以在MCU上运行的逻辑代码代码分为两个部分一是引用的头文件二是实现函数定义的源文件如下/*ADC线性校准头文件*/ #ifndef __ADC_CALIBRATION_H__ #define __ADC_CALIBRATION_H__ #include stdint.h /* 校准参数结构体 */ typedef struct { float k; //斜率增益系数 K float b; //截距偏移系数 B } ADC_Calibration_t; /* 返回状态码 */ #define ADC_CAL_OK 0 #define ADC_CAL_ERR_POINTS 1 //校准点数不足无法进行校准 #define ADC_CAL_ERR_DEGEN 2 //原始值无变化无法进行拟合 /** * brief 使用最小二乘法根据多组校准点计算斜率与截距 * param cal 校准参数结构体指针 * param raw 原始ADC读数数组输入值X * param ref 实际标准值数组输出值Y * param num_points 数组长度至少2 * return 状态码 */ int ADC_Calibration_Compute(ADC_Calibration_t *cal, const float *raw, const float *ref, int num_points); /** * brief 使用两点直接计算斜率与截距快速方式 * param cal 校准参数结构体指针 * param raw1 第一点原始值 * param ref1 第一点标准值 * param raw2 第二点原始值 * param ref2 第二点标准值 * return 状态码 */ int ADC_Calibration_ComputeTwoPoints(ADC_Calibration_t *cal, float raw1, float ref1, float raw2, float ref2); /** * brief 应用校准将原始ADC值转换为实际物理量 * param cal 校准参数结构体指针 * param raw_value 原始ADC读数 * return 校准后的值浮点数 */ float ADC_Calibration_Apply(const ADC_Calibration_t *cal, float raw_value); /** * brief 重置校准参数为直通slope1, intercept0 * param cal 校准参数结构体指针 */ void ADC_Calibration_Reset(ADC_Calibration_t *cal); #endif /*__ADC_CALIBRATION_H__*//*ADC线性校准-c源文件*/ #include adc_calibration.h #include math.h /* 最小二乘法拟合 y slope * x intercept */ int ADC_Calibration_Compute(ADC_Calibration_t *cal, const float *raw, const float *ref, int num_points) { if (cal NULL || raw NULL || ref NULL || num_points 2) { return ADC_CAL_ERR_POINTS; } double sum_x 0.0, sum_y 0.0, sum_xy 0.0, sum_xx 0.0; int i; for (i 0; i num_points; i) { double x (double)raw[i]; double y (double)ref[i]; sum_x x; sum_y y; sum_xy x * y; sum_xx x * x; } double n (double)num_points; double denominator n * sum_xx - sum_x * sum_x; /* 若原始值无变化无法拟合 */ if (fabs(denominator) 1e-12) { return ADC_CAL_ERR_DEGEN; } double k (n * sum_xy - sum_x * sum_y) / denominator; double b (sum_y - slope * sum_x) / n; cal-k (float)k; cal-b (float)b; return ADC_CAL_OK; } /* 两点直接计算 */ int ADC_Calibration_ComputeTwoPoints(ADC_Calibration_t *cal, float raw1, float ref1, float raw2, float ref2) { if (cal NULL) { return ADC_CAL_ERR_POINTS; } float dx raw2 - raw1; if (fabs(dx) 1e-12) { return ADC_CAL_ERR_DEGEN; } cal-k (ref2 - ref1) / dx; cal-b ref1 - cal-k * raw1; return ADC_CAL_OK; } /* 应用校准 */ float ADC_Calibration_Apply(const ADC_Calibration_t *cal, float raw_value) { if (cal NULL) { return raw_value; // 无校准则直通 } return cal-k * raw_value cal-intercept; } /* 重置为直通 */ void ADC_Calibration_Reset(ADC_Calibration_t *cal) { if (cal ! NULL) { cal-k 1.0f; cal-b 0.0f; } }此处给出简易的代码使用示例应用demo代码如下#include adc_calibration.h #include stdio.h int main(void) { ADC_Calibration_t cal; ADC_Calibration_Reset(cal); /*假设两组校准数据原始ADC值 和 标准电压(V)*/ float raw_vals[] { 512.0f, 1024.0f, 2048.0f, 3072.0f }; float ref_vals[] { 1.0f, 2.0f, 4.0f, 6.0f }; int num 4; if (ADC_Calibration_Compute(cal, raw_vals, ref_vals, num) ADC_CAL_OK) { printf(Calibration OK: k %.6f, b %.6f\n, cal.slope, cal.intercept); /*测量一个原始值*/ float raw_measure 1500.0f; float real_value ADC_Calibration_Apply(cal, raw_measure); printf(Raw: %.2f - Real: %.4f V\n, raw_measure, real_value); } else { printf(Calibration failed.\n); } /*也可用两点式快速校准*/ ADC_Calibration_t cal2; ADC_Calibration_ComputeTwoPoints(cal2, 512.0f, 1.0f, 3072.0f, 6.0f); float v ADC_Calibration_Apply(cal2, 1500.0f); printf(Two-point calibration: %.4f V\n, v); return 0; }在这个示例demo中只是针对一路ADC采集进行了线性校准实际采集卡一般都是多路ADC采集只要将校准参数结构体定义为一个结构体数组如下ADC_Calibration_t cal[16]; //意为有16个ADC通道共16组校准参数这样每一路ADC通道都能有自己独立的校准系数以上就是完整的线性校准的代码实现和简单应用。4工程应用实际工程应用除了要实现核心的线性校准逻辑还要实现一些重要的外围辅助工作线性校准一般是在产品出厂测试前进行这就涉及到产线上需要用到的上位机工具进行命令下发整个工作流程以下面的示例进行说明上图是我使用Qt写的一个简易的生产调试工具整个生产流程是①、先开启校准模式其实就是上位机给采集卡下发一个命令让采集卡开启校准防止采集卡校准参数被其它情况误篡改②、默认系数k1、b0也就是不经校准直通然后取4mA为零点点击零点校准这个过程实际是记录下4mA的采集值③、再取19mA为满量程点点击满度校准这个过程是记录下19mA的采集值并与4mA、19mA标准值进行计算求得系数k、b④、最终要将系数k、b进行保存同样是上位机给采集卡下发命令让MCU把校准参数写到Flash中以后每次采集卡上电直接加载校准系数通过这样一套流程校准后采集精度甚至可以达到万分之三以内所以实际应用时我们还需要实现MCU擦写Flash操作、响应上位机校准命令这些外围实现此处就不再展开只需要知道整个流程逐一实现即可。5总 结至此本期内容就完结了如果是从事工控DCS和PLC工作的朋友应该了解一些现场每隔一到两年就会让厂家去现场进行一次点检其实就是再做一次线性校准原因是经过一段时期的运行采集卡会产生一些温漂之类的新误差只需要再进行一次线性校准就又能恢复其精度。