Skip to main content

esp_hal/analog/adc/calibration/line/
s2.rs

1use core::marker::PhantomData;
2
3use super::AdcHasLineCal;
4use crate::analog::adc::{
5    AdcCalBasic,
6    AdcCalEfuse,
7    AdcCalScheme,
8    Attenuation,
9    CalibrationAccess,
10    Error,
11};
12
13const COEFF_A_SCALING: i64 = 65536;
14const COEFF_B_SCALING: i64 = 1024;
15const V_HIGH: [i64; 4] = [600, 800, 1000, 2000];
16const V_LOW: i64 = 250;
17
18/// Line fitting ADC calibration scheme.
19///
20/// ESP32-S2 uses two-point characterization (eFuse calibration v1) or
21/// one-point characterization (v2). Readings are in mV.
22///
23/// This scheme also includes basic calibration ([`AdcCalBasic`]).
24///
25/// See <https://github.com/espressif/esp-idf/blob/027613140/components/esp_adc/esp32s2/adc_cali_line_fitting.c#L87>
26#[derive(Clone, Copy)]
27pub struct AdcCalLine<ADCX> {
28    basic: AdcCalBasic<ADCX>,
29    coeff_a: u32,
30    coeff_b: i32,
31    _phantom: PhantomData<ADCX>,
32}
33
34fn characterize_two_point(atten: Attenuation, high: i64, low: i64) -> Result<(u32, i32), Error> {
35    let v_high = V_HIGH[atten as usize];
36    // Valid eFuse data always has high > low; a non-positive denominator means the table is
37    // corrupt, so report it instead of fabricating coefficients.
38    let denom = high - low;
39    if denom <= 0 {
40        return Err(Error::MissingCalibrationData);
41    }
42    let coeff_a = (COEFF_A_SCALING * (v_high - V_LOW) / denom) as u32;
43    let coeff_b = (COEFF_B_SCALING * (V_LOW * high - v_high * low) / denom) as i32;
44    Ok((coeff_a, coeff_b))
45}
46
47impl<ADCX> crate::private::Sealed for AdcCalLine<ADCX> {}
48
49impl<ADCX> AdcCalScheme<ADCX> for AdcCalLine<ADCX>
50where
51    ADCX: AdcCalEfuse + AdcHasLineCal + CalibrationAccess,
52{
53    fn new_cal(atten: Attenuation) -> Result<Self, Error> {
54        Self::new_cal_with_channel(atten, 0)
55    }
56
57    /// # Errors
58    ///
59    /// Returns [`Error::MissingCalibrationData`] if the chip carries no RTC
60    /// calibration block or the stored reference point is zero.
61    fn new_cal_with_channel(atten: Attenuation, channel: u8) -> Result<Self, Error> {
62        let basic = AdcCalBasic::<ADCX>::new_cal_with_channel(atten, channel)?;
63        let version = crate::efuse::rtc_calib_version();
64
65        let (coeff_a, coeff_b) = match version {
66            1 => {
67                let low = crate::efuse::rtc_calib_reading(
68                    version,
69                    ADCX::UNIT,
70                    atten,
71                    crate::efuse::RtcCalibParam::Vlow,
72                );
73                let high = crate::efuse::rtc_calib_reading(
74                    version,
75                    ADCX::UNIT,
76                    atten,
77                    crate::efuse::RtcCalibParam::Vhigh,
78                );
79                characterize_two_point(atten, high as i64, low as i64)?
80            }
81            2 => {
82                // A missing or zero reference point means the eFuse field was
83                // never burnt; fabricating a gain of `mv/1` would hide that.
84                let Some(code) = ADCX::cal_code(atten) else {
85                    return Err(Error::MissingCalibrationData);
86                };
87                if code == 0 {
88                    return Err(Error::MissingCalibrationData);
89                }
90                let Some(mv) = ADCX::cal_mv(atten) else {
91                    return Err(Error::MissingCalibrationData);
92                };
93                (((COEFF_A_SCALING * mv as i64) / code as i64) as u32, 0)
94            }
95            _ => {
96                // No RTC calibration block on this chip: report it instead of
97                // falling back to unburnt table defaults.
98                return Err(Error::MissingCalibrationData);
99            }
100        };
101
102        Ok(Self {
103            basic,
104            coeff_a,
105            coeff_b,
106            _phantom: PhantomData,
107        })
108    }
109
110    fn adc_val(&self, val: u16) -> u16 {
111        let val = self.basic.adc_val(val) as i64;
112        let voltage = (val * self.coeff_a as i64 / (COEFF_A_SCALING / COEFF_B_SCALING)
113            + self.coeff_b as i64)
114            / COEFF_B_SCALING;
115        voltage.clamp(0, u16::MAX as i64) as u16
116    }
117}