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:
- Place sensor in clean air (0 ppm)
- Wait until
STABILITYreportsis_stable=1 - Send
ZERO
What happens:
- Firmware reads the current mV mean from
StabilityMonitor - Stores
baseline_codeandbaseline_mvto flash andCalibrationState - 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:
- Expose sensor to a known concentration (e.g. 25 ppm certified cylinder)
- Wait for sensor to stabilise (T90 ≈ 30–35 s)
- 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 / 20is 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.
| Field | Type | Description |
|---|---|---|
magic | u32 | 0xCAFEBABE — validity check |
baseline_code | u16 | ADC code at zero |
baseline_mv | f32 | Baseline voltage (mV) |
slope | f32 | mV per ppm (10.0 after span) |
cha_percent | f32 | ChA gain applied at last span |
is_calibrated | u8 | 1 when span is done |
fw_version | u16 | Firmware version at calibration |
checksum | u32 | XOR checksum over all fields |