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. After zero calibration, the ADC reads ≈ 0 mV in clean air. ChA (DAC channel A) scales the remaining gas signal to achieve a target sensitivity of 10 mV/ppm.
PPM conversion is therefore:
ppm = V_adc / slope slope = 10 mV/ppm
No software baseline subtraction is needed — ChB handles it in hardware.
Calibration State Machine
┌──────────────┐
│ Uncalibrated │
└──────┬───────┘
│ ZERO command (stability required)
▼
┌─────────────────────┐
│ ZeroCalibrated │
│ baseline_code │
│ ChB offset applied │
└──────┬──────────────┘
│ SPAN:<ppm> command
▼
┌─────────────────────┐
│ FullyCalibrated │
│ slope = 10 mV/ppm │
│ ChA gain applied │
└─────────────────────┘
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 (target: 10.0)
}
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, converts to ADC code - Stores as
baseline_code - Calculates ChB percentage and writes it to
TARGET_DAC
ChB Offset Calculation
ChB full scale = 2000 mV. The required offset equals the sensor baseline voltage:
chb_percent = baseline_mv / 2000 × 100
= baseline_mv / 20
pub fn compute_chb_percent(baseline_code: u16) -> f32 {
(adc_code_to_mv(baseline_code) / 20.0).clamp(0.0, 100.0)
}
| Baseline (mV) | ChB % |
|---|---|
| 500 | 25% |
| 1000 | 50% |
| 1500 | 75% |
| 2000 | 100% |
After zero calibration the ADC should read ≈ 0 mV in clean air.
Step 2: Span Calibration
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 the current ADC code (
span_mv = code × VDD / 4095) - Calculates the ChA gain required to normalise to 10 mV/ppm:
cha_percent = ppm × 10 / span_mv × 100 - Stores slope =
span_mv × cha_percent/100 / ppm = 10.0 mV/ppm - Writes ChA to
TARGET_DACand resets the stability buffer
The sensor’s raw sensitivity is always > 10 mV/ppm, so ChA will always be ≤ 100%.
Example
span_mv at 25 ppm = 350 mV
cha_percent = 25 × 10 / 350 × 100 = 71.4%
slope = 350 × 0.714 / 25 = 10.0 mV/ppm ✓
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. VREFINT is sampled with CYCLES247_5 (≈ 15.5 µs) to meet the
STM32G4 minimum 4 µs stabilisation requirement.
Calibration Procedure
1. Power on → wait 30+ s for electrochemical warm-up
2. Apply zero air (0 ppm)
3. Poll: STABILITY → wait until is_stable=1
4. Send: ZERO → stores baseline, sets ChB
5. Apply known-concentration gas (e.g. 25 ppm cylinder)
6. Wait T90 (30–35 s)
7. Send: SPAN:25 → sets ChA, slope stored as 10 mV/ppm
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 |
slope | f32 | mV per ppm (10.0 after span) |
is_calibrated | u8 | 1 when span is done |