ZZZ-24-满量程校准

本篇继 [[ZZZ-23-零点校准]],演示的案例中【硬件】和【单片机配置】大体相同

如果说“零点校准”是修正坐标轴的起点,那么“满量程校准”就是修正坐标轴的斜率

1. 为什么要进行满量程校准?

即使你把零点对齐了(4mA 对应 0),传感器在测量高浓度或高压力时,依然可能产生偏差。

  • 原因:传感器内部的放大电路增益可能随温度变化,或者传感器探头灵敏度衰减。
  • 结果:原本 $20mA$ 应该代表 $1.6,MPa$,结果传感器只输出了 $19.5mA$。
  • 校准逻辑:我们需要给传感器一个“标准参考值”(如标准气体或已知压力),告诉单片机:“这就是满量程,请记住现在的斜率。”。

2. 数学模型:从 $y=x-b$ 到 $y=k(x-b)$

我们之前的压力计算公式可以抽象为:$$Pressure = k \cdot (V_{measured} - V_{zero})$$

  • $V_{zero}$:零点电压(通过零点校准获得)。
  • $k$ (斜率):这是满量程校准要修正的对象。

满量程校准的推导:当你施加一个标准满量程压力(如 $1.6,MPa$)时,记下当前的电压 $V_{full_measured}$。 那么新的斜率 $k$ 应该是:$$k = \frac{P_{nominal}}{V_{full_measured} - V_{zero}}$$ 其中 $P_{nominal}$ 是标准值的额定压力(如 1.6Mpa)

3. 代码实现方案

我们需要再增加一个按键逻辑(例如按键接到 PB1)来触发“满量程校准”。

1
2
3
4
/* USER CODE BEGIN Includes */
#include <stdio.h>
#include <string.h>
/* USER CODE END Includes */
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/* USER CODE BEGIN PV */
#define SAMPLES 10  					// 每个通道采样 10 次
#define CHANNELS 2 						// 共 2 个通道
uint16_t adc_buffer[SAMPLES * CHANNELS]; // 长度为 20 的数组
// --- 核心标定参数 ---
const float P_NOMINAL = 1.6f;             // 满量程标定时的标准压力值 (MPa)
const float R_SAMPLE = 150.0f;            // 采样电阻 150 欧
const float alpha = 0.15f;                // 一阶滞后滤波系数

// --- 标定状态变量 ---
float zero_offset_volt = 0.60f;           // 默认零点电压 (4mA 对应 0.6V)
float slope_k = 1.6f / (3.0f - 0.60f);    // 默认斜率
float filter_volt_A = 0.60f;              // 滤波后的电压变量
uint8_t calibration_flag = 0;             // 标定标志位
/* USER CODE END PV */
1
2
3
4
5
6
7
8
  /* USER CODE BEGIN 2 */
  // 为了精准,建议先进行 ADC 校准(F1系列特有)
  HAL_ADCEx_Calibration_Start(&hadc1);

  // 核心命令:启动 ADC + DMA
  // 参数:ADC句柄,存储的目标地址,要搬运的数据个数
  HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, SAMPLES * CHANNELS);
  /* USER CODE END 2 */
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
  while (1)
  {
      // --- 1. 信号采集与双重滤波 ---
      uint32_t sum_A = 0;
      for(int i = 0; i < SAMPLES; i++) {
          sum_A += adc_buffer[i * CHANNELS];
      }
      
      // 原始电压计算 (基于 3.3V 理论值,若需更准可换回 VREFINT 公式)
      float raw_volt_A = (float)sum_A / SAMPLES * 3.3f / 4095.0f;
      
      // 一阶滞后滤波
      filter_volt_A = alpha * raw_volt_A + (1.0f - alpha) * filter_volt_A;
      float volt = filter_volt_A;

      // --- 2. 零点校准逻辑 (PB0) ---
      if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET)
      {
          HAL_Delay(20); // 初步去抖
          if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET)
          {
              zero_offset_volt = volt; // 记录当前电压为新零点
              
              // 亮灯反馈
              HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
              HAL_Delay(500);
              HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
              
              while(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET); // 等待松开
          }
      }

      // --- 3. 满量程校准逻辑 (PB1) ---
      if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET)
      {
          HAL_Delay(20);
          if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET)
          {
              // 核心逻辑:斜率 k = 标准压力 / (当前满量程电压 - 零点电压)
              float v_diff = volt - zero_offset_volt;
              
              if (v_diff > 0.1f) // 安全检查:确保满位电压确实大于零点电压
              {
                  slope_k = P_NOMINAL / v_diff;
                  
                  // 成功后快闪两次灯
                  for(int i=0; i<4; i++) {
                      HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
                      HAL_Delay(100);
                  }
              }
              while(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET);
          }
      }

      // --- 4. 最终工业参数换算 ---
      // 计算电流 (mA)
      float current_mA = (volt / R_SAMPLE) * 1000.0f;
      
      // 计算校准后的压力
      float calibrated_pressure = (volt - zero_offset_volt) * slope_k;
      
      // 负数截断保护
      if (calibrated_pressure < 0.0f) calibrated_pressure = 0.0f;

      // --- 5. VOFA+ 数据输出 ---
      // 通道说明:1.电流(mA), 2.校准后压力(MPa), 3.零点偏置电压(V), 4.当前斜率k
      printf("%.2f,%.3f,%.3f,%.4f\n", 
              current_mA, 
              calibrated_pressure, 
              zero_offset_volt, 
              slope_k);

      HAL_Delay(100); // 10Hz 刷新率,适合观察
  }

4、校准实验

校准零点前

校准零点后
校准满量程后

1. 零点漂移被“零点偏置电压”吸收

  • 物理现象:传感器在没有压力时,输出原本应该是 $0.6V$,但因为环境温度或老化变成了 $0.65V$。这多出来的 $0.05V$ 就是“零点漂移”。
  • 数学处理:你通过校准,把变量 zero_offset_volt 从 $0.6$ 改成了 $0.65$。
  • 理解:在公式 $(volt - zero_offset_volt)$ 中,那个多出来的 $0.05V$ 确实被 zero_offset_volt 抵消(吸收)掉了。坐标轴的起点被你平移到了现在真实的位置上。

2. 量程漂移被“斜率”吸收

  • 物理现象:传感器现在的“力气”变小了。以前增加 $2.4V$ 代表增加 $1.6,MPa$,现在增加 $2.4V$ 只能代表增加 $1.5,MPa$ 了。
  • 数学处理:你通过满量程校准,重新计算了 slope_k
  • 理解:新的 slope_k 变大了,它通过补偿增益的方式,“吸收”了传感器灵敏度的下降。它把斜斜的曲线重新拉回到了正确的高度。

换句话说,在工业仪表中,我们把传感器的所有“不完美”都归结为两个参数:

  • 截距 (Intercept):由 zero_offset_volt 承载,解决“从哪开始”的问题。
  • 增益 (Gain/Slope):由 slope_k 承载,解决“走多快”的问题。

满量程校准中的安全检查

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// --- 3. 满量程校准逻辑 (PB1) ---
if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET)
{
   HAL_Delay(20);
   if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET)
   {
	// 核心逻辑:斜率 k = 标准压力 / (当前满量程电压 - 零点电压)
	float v_diff = volt - zero_offset_volt;
	
	if (v_diff > 0.1f) // 安全检查:确保满位电压确实大于零点电压
	{
	    slope_k = P_NOMINAL / v_diff;
	    
	    // 成功后快闪两次灯
	    for(int i=0; i<4; i++) {
		 HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
		 HAL_Delay(100);
	    }
	}
	while(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET);
   }
}

这里重点关注:if (v_diff > 0.1f) ,为什么要这么写:

预防“数学炸弹”:防止除以零

在满量程标定中,斜率的计算公式是:$$k = \frac{P_{NOMINAL}}{V_{full} - V_{zero}}$$

  • 风险:如果 $V_{full}$ 等于 $V_{zero}$(即 v_diff 为 0),程序在执行除法时会触发硬件异常或者得到一个无穷大(Inf)
  • 后果:这会导致你的 calibrated_pressure 变成乱码,甚至导致单片机程序卡死或复位。
  • 保护:0.1f 作为一个门槛,确保了分母永远是一个有意义的正数。

操作防错:防止“误触发”标定

在 CEMS(烟气在线监测系统)的现场维护中,操作员可能会忙中出错:

  • 误触:操作员本想按零点标定,却按成了满量程标定。此时传感器还没有通入标准压力或标气。
  • 逻辑:此时 $V_{full}$ 几乎等于 $V_{zero}$。如果没有这个检查,单片机就会把微小的噪声(比如 $0.001V$ 的波动)当成整个量程的跨度。
  • 结果:你会得到一个超级巨大的斜率 $k$。标定完后,哪怕空气稍微波动一下,显示屏上的压力数值都会疯狂飙升。
  • 保护:通过检查,如果差值不到 $0.1V$,程序就会认为“这次标定不合法”,从而拒绝更新 slope_k

为什么是 0.1f 这个值?

这个值不是绝对的,它是根据你的硬件环境估算的:

  • 0.1V 对应 12 位 ADC 约 124 个数值($4095 \times \frac{0.1}{3.3}$)。
  • 对于你的传感器($0.6V \sim 3.0V$),总跨度是 $2.4V$。$0.1V$ 约占满量程跨度的 4%
  • 建议:如果你在更复杂的工业现场发现干扰很大,可以将这个值调高到 0.2f0.3f

关于零点偏置电压和斜率

如果零点偏置电压和斜率,被校准得偏离初始值非常大,会出现什么现象

在 CEMS(烟气在线监测系统)的运维中,如果校准参数(零点和斜率)偏离初始值过大,通常意味着系统正在失去“测量能力”。这种现象在工业上被称为“量程压缩”或“分辨率退化”。我们可以把这种情况形象地比喻成“老花镜”失效了。

1. 如果“零点偏置电压”偏离过大

假设原本零点是 $0.6V$,现在被校准到了 $2.0V$。

  • 现象 A:有效量程变窄(Range Compression)
    • STM32 的 ADC 最高只能测到 $3.3V$。如果零点占用了 $2.0V$,那么留给实际信号的范围就只剩下 $3.3V - 2.0V = 1.3V$。原本你可以用 $2.4V$ 的跨度来表现 $1.6,MPa$,现在只能用一半的电压范围来表现,测量精度直接减半
  • 现象 B:提前出现“削顶”
    • 当压力还没达到满量程时,ADC 已经读到了 $4095$(满位)。在 VOFA+ 上你会看到压力曲线在还没到顶时就变成了一条平平的直线,数据丢了。

2. 如果“斜率 $k$”偏离过大

斜率 $k$ 本质上是一个“放大倍数”。

  • 情况一:$k$ 变得特别大(高增益)
    • 现象:系统变得极其敏感且神经质。哪怕 ADC 只有 1 个数值的微小跳动,乘以巨大的 $k$ 之后,显示的压力值都会在 VOFA+ 上剧烈抖动。
    • 本质:你正在放大噪声这种情况通常说明传感器已经“衰减”得很厉害了,给出的信号极其微弱,你不得不拼命放大它
  • 情况二:$k$ 变得特别小(低增益)
    • 现象:系统变得迟钝。无论你如何调节电位器,压力数值几乎不动,或者变化非常缓慢。
    • 本质:这通常发生在误操作标定时,或者传感器发生了“短路”性质的故障。

CEMS 工业实战中的判定逻辑

在专业的 CEMS 仪表中,我们不会任由参数无限偏离。我们会设置“标定限值(Calibration Limits)”**:

工业逻辑示例:

  • 如果 zero_offset_volt 偏离初始值超过 $\pm 20%$。
  • 或者 slope_k 偏离初始值超过 $\pm 30%$。
  • 结果:单片机直接报错 “Calibration Error”,并拒绝更新参数。

为什么要这样做?因为参数偏离这么大,说明不是简单的漂移,而是硬件出问题了:

  1. 传感器探头脏了(需要清洗)。
  2. 气路漏气了(标气没通进去)。
  3. 传感器彻底老化(需要更换)。

你的代码可以增加的“高级感”

你可以给你的代码加一个小小的“安全锁”,防止校准出荒唐的数据:

1
2
3
4
5
6
if (v_diff > 0.5f && v_diff < 2.8f) { // 检查有效跨度是否在合理范围内
    slope_k = P_NOMINAL / v_diff;
} else {
    // 报错:量程偏差过大,维持旧斜率不变
    printf("Calibration Failed: Sensor Out of Range!\n");
}

校零函数和满量程校准函数

1. 校零函数 (Zero Calibration)

核心逻辑:截距修正 (Offset Correction)

在传感器没有任何压力(或通入零点标准气)时,记录当前的输出。

  • 核心公式:$$V_{zero} = V_{current_filtered}$$
  • 代码动作:
    1. 确保系统处于“零状态”(如通入纯氮气)。
    2. 读取经过滤波后的稳定电压值。
    3. 将该值覆盖保存到 zero_offset_volt 变量中。
  • 物理意义:告诉单片机:“不管电路漂移到了多少伏,从这一刻起,这个电压就代表 0 MPa。”

2. 满量程校准函数 (Span Calibration)

核心逻辑:增益/斜率修正 (Gain/Slope Correction)

在传感器承受标准满量程压力(如 1.6MPa)时,调整输出的放大倍数。

  • 核心公式:$$Slope_K = \frac{P_{nominal}}{V_{full} - V_{zero}}$$ (其中 $P_{nominal}$ 是你预设的标准值,如 1.6)

  • 代码动作:

    1. 确保系统处于“满量程状态”(如通入 1.6MPa 压力)。
    2. 计算当前的有效压差:v_diff = V_current - V_zero
    3. 安全检查:判断 v_diff 是否足够大(防止除以 0 或误操作)。
    4. 计算并更新斜率 slope_k
  • 物理意义:告诉单片机:“现在压力是 1.6,电压差是这么大,请计算出每一伏电压代表多少 MPa。”

Licensed under CC BY-NC-SA 4.0