Gas Sensor
STM32G431KB · Firmware v0.1.0
Calibration

Calibration System

Two-point coupled calibration — state machine, simultaneous ChA/ChB solution, and PPM conversion.

Principle

The firmware implements two-point linear calibration with hardware baseline subtraction.

  • ChB (DAC channel B) injects an analogue offset into the signal path to cancel the sensor’s clean-air baseline voltage at the ADC input.
  • ChA (DAC channel A) scales the remaining gas signal to achieve a target sensitivity of 10 mV/ppm.

PPM conversion is:

ppm = V_adc_mv / slope        slope = 10 mV/ppm

The Coupling Problem

A naive sequential calibration (set ChB at ZERO, set ChA at SPAN independently) produces a mathematical coupling error: ChA’s gain retroactively changes the effective offset seen at the ADC, so the zero point shifts when span is applied. The error compounds in both directions.

The firmware solves this with a simultaneous closed-form solution — ChA and ChB are computed together during the SPAN command and applied atomically via the DAC’s LDAC pin. The full algebraic derivation is given in the accompanying journal paper.


Calibration State Machine

┌──────────────┐
│ Uncalibrated │
└──────┬───────┘
       │  ZERO command (stability required)

┌─────────────────────┐
│  ZeroCalibrated     │
│  baseline_code      │
│  baseline_mv stored │
└──────┬──────────────┘
       │  SPAN:<ppm> command

┌──────────────────────────────────┐
│  FullyCalibrated                 │
│  ChA and ChB applied atomically  │
│  slope = 10 mV/ppm               │
└──────────────────────────────────┘
pub enum CalibrationState {
    Uncalibrated,
    ZeroCalibrated { data: CalibrationData },
    FullyCalibrated { data: CalibrationData },
}

pub struct CalibrationData {
    pub baseline_mv:   f32,   // Sensor voltage in clean air (mV)
    pub baseline_code: u16,   // ADC code at zero
    pub slope:         f32,   // mV per ppm (10.0 after span)
}

Step 1: Zero Calibration

Requirement: Sensor stable for ≥ 30 seconds (30-sample window, rate-of-change ≤ 0.1 mV).

Procedure:

  1. Place sensor in clean air (0 ppm)
  2. Wait until STABILITY reports is_stable=1
  3. Send ZERO

What happens:

  • Firmware reads the current mV mean from StabilityMonitor
  • Stores baseline_code and baseline_mv to flash and CalibrationState
  • No DAC values are written yet — ChB is deferred to the simultaneous SPAN step

Step 2: Span Calibration (Simultaneous Solution)

Requirement: Zero calibration already done.

Procedure:

  1. Expose sensor to a known concentration (e.g. 25 ppm certified cylinder)
  2. Wait for sensor to stabilise (T90 ≈ 30–35 s)
  3. Send SPAN:25

What happens:

  • Firmware reads current ADC mean (span_mv)
  • Computes delta_mv = span_mv (post-offset signal at span concentration)
  • Solves simultaneously for ChA and ChB:
// comms.rs — SPAN command handler
let cha_percent = (ppm * 1000.0 / delta_mv).clamp(1.0, 150.0);
let ga = cha_percent / 100.0;
let chb_percent = (50.0 * (1.0 + ga * baseline_mv / 1000.0)).clamp(0.0, 100.0);

In formula terms:

ChA% = (ppm × 1000) / delta_mv

ChB% = 50 × (1 + (ChA/100) × baseline_mv / 1000)

Both values are written to TARGET_DAC and applied atomically on the next bias_task cycle via an LDAC pulse.

Example

Known span: 25 ppm
span_mv at span gas = 350 mV
baseline_mv from ZERO = 1250 mV

ChA% = 25 × 1000 / 350        = 71.4%
ga   = 71.4 / 100             = 0.714
ChB% = 50 × (1 + 0.714 × 1.25) = 50 × 1.893 = 94.6%

Note: the simple approximation ChB% = baseline_mv / 20 is not used — it produces a coupling error of 15–20 mV when ChA ≠ 100%. The simultaneous formula above eliminates this error.


PPM Conversion

pub fn mv_to_ppm(&self, mv: f32) -> Option<f32> {
    match self {
        FullyCalibrated { data } => Some(mv / data.slope),
        _ => None,
    }
}

GAS responses use the 30-sample stability mean (mV) for noise rejection. If the stability buffer is empty, the latest raw ADC code is used as a fallback.


ADC Voltage Reference

VDD is determined at boot using the STM32G4 factory-calibrated VREFINT value stored at 0x1FFF75AA (measured at VDD = 3.0 V, 30 °C):

let cal = unsafe { (*(0x1FFF75AA as *const u16)) as u32 & 0xFFF };
let actual_vdd = (3000 * cal) / vrefint_code;

This eliminates the per-chip VREFINT offset (typically ±10 mV) and provides ratiometric VDD noise compensation.


Full Calibration Procedure

1. Power on → wait 30+ s for electrochemical warm-up
2. Apply zero air (0 ppm target gas)
3. Poll:  STABILITY  →  wait until is_stable=1
4. Send:  ZERO       →  stores baseline_code and baseline_mv
5. Apply known-concentration gas (e.g. 25 ppm cylinder)
6. Wait T90 (30–35 s)
7. Send:  SPAN:25    →  simultaneous ChA + ChB computed and applied
8. Send:  GAS        →  verify shows ≈ 25 ppm
9. Remove gas        →  verify return to ≈ 0 ppm

Data Persistence

Calibration is saved to flash (page 63, address 0x0801_F800) after both ZERO and SPAN.

FieldTypeDescription
magicu320xCAFEBABE — validity check
baseline_codeu16ADC code at zero
baseline_mvf32Baseline voltage (mV)
slopef32mV per ppm (10.0 after span)
cha_percentf32ChA gain applied at last span
is_calibratedu81 when span is done
fw_versionu16Firmware version at calibration
checksumu32XOR checksum over all fields

Last updated: March 2026