From ff80b69183739d04d1cb154b8232be01c0b26fd9 Mon Sep 17 00:00:00 2001 From: jneem Date: Mon, 30 Oct 2023 12:16:45 -0500 Subject: [PATCH] Provide ADC values in mV instead of requiring the user to scale them (#836) * Provide ADC values in mV instead of requiring the user to scale them * Changelog * Try converting poly calibration also * Update changelog and comments * Fix example --- CHANGELOG.md | 1 + esp-hal-common/src/analog/adc/cal_basic.rs | 12 +++++++---- esp-hal-common/src/analog/adc/cal_curve.rs | 18 ++++++++++++---- esp-hal-common/src/analog/adc/cal_line.rs | 24 ++++++++++++---------- esp-hal-common/src/analog/adc/riscv.rs | 19 ----------------- esp-hal-common/src/analog/adc/xtensa.rs | 17 --------------- esp-hal-common/src/analog/mod.rs | 11 +++++++--- esp32c2-hal/examples/adc_cal.rs | 10 +++++---- esp32c3-hal/examples/adc_cal.rs | 10 +++++---- esp32c6-hal/examples/adc_cal.rs | 10 +++++---- esp32s3-hal/examples/adc_cal.rs | 10 +++++---- 11 files changed, 68 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f579d25d7..228928c47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bumped MSRV to 1.67 (#798) - Optimised multi-core critical section implementation (#797) +- Changed linear- and curve-calibrated ADC to provide readings in mV (#836) ### Fixed diff --git a/esp-hal-common/src/analog/adc/cal_basic.rs b/esp-hal-common/src/analog/adc/cal_basic.rs index cfa3508fc..1dce929a7 100644 --- a/esp-hal-common/src/analog/adc/cal_basic.rs +++ b/esp-hal-common/src/analog/adc/cal_basic.rs @@ -11,10 +11,14 @@ use crate::adc::{ /// Basic ADC calibration scheme /// -/// Basic calibration is related to setting some initial bias value in ADC. -/// Such values usually is stored in efuse bit fields but also can be measured -/// in runtime by connecting ADC input to ground internally a fallback when -/// it is not available. +/// Basic calibration sets the initial ADC bias value so that a zero voltage +/// gives a reading of zero. The correct bias value is usually stored in efuse, +/// but can also be measured at runtime by connecting the ADC input to ground +/// internally. +/// +/// Failing to apply basic calibration can substantially reduce the ADC's output +/// range because bias correction is done *before* the ADC's output is truncated +/// to 12 bits. #[derive(Clone, Copy)] pub struct AdcCalBasic { /// Calibration value to set to ADC unit diff --git a/esp-hal-common/src/analog/adc/cal_curve.rs b/esp-hal-common/src/analog/adc/cal_curve.rs index ce7e10229..85432352b 100644 --- a/esp-hal-common/src/analog/adc/cal_curve.rs +++ b/esp-hal-common/src/analog/adc/cal_curve.rs @@ -11,6 +11,8 @@ use crate::adc::{ const COEFF_MUL: i64 = 1 << 52; +/// Integer type for the error polynomial's coefficients. Despite +/// the type, this is a fixed-point number with 52 fractional bits. type CurveCoeff = i64; /// Polynomial coefficients for specified attenuation. @@ -35,8 +37,8 @@ pub trait AdcHasCurveCal { /// Curve fitting ADC calibration scheme /// -/// This scheme implements final polynomial error correction using predefined -/// coefficient sets for each attenuation. +/// This scheme implements polynomial error correction using predefined +/// coefficient sets for each attenuation. It returns readings in mV. /// /// This scheme also includes basic calibration ([`super::AdcCalBasic`]) and /// line fitting ([`AdcCalLine`]). @@ -44,7 +46,15 @@ pub trait AdcHasCurveCal { pub struct AdcCalCurve { line: AdcCalLine, - /// Coefficients for each term (3..=5) + /// Coefficients of the error estimation polynomial. + /// + /// The constant coefficient comes first; the error polynomial is + /// `coeff[0] + coeff[1] * x + ... + coeff[n] * x^n`. + /// + /// This calibration works by first applying linear calibration. Then + /// the error polynomial is applied to the output of linear calibration. + /// The output of the polynomial is our estimate of the error; it gets + /// subtracted from linear calibration's output to get the final reading. coeff: &'static [CurveCoeff], _phantom: PhantomData, @@ -104,7 +114,7 @@ macro_rules! coeff_tables { $(CurveCoeffs { atten: Attenuation::$att, coeff: &[ - $(($val as f64 * COEFF_MUL as f64 * 4096f64 / Attenuation::$att.ref_mv() as f64) as CurveCoeff,)* + $(($val as f64 * COEFF_MUL as f64) as CurveCoeff,)* ], },)* ]; diff --git a/esp-hal-common/src/analog/adc/cal_line.rs b/esp-hal-common/src/analog/adc/cal_line.rs index e0bae18a9..354f59205 100644 --- a/esp-hal-common/src/analog/adc/cal_line.rs +++ b/esp-hal-common/src/analog/adc/cal_line.rs @@ -16,13 +16,13 @@ use crate::adc::{ /// See also [`AdcCalLine`]. pub trait AdcHasLineCal {} -/// Coefficients is actually a fixed-point numbers. -/// It is scaled to put them into integer. +/// We store the gain as a u32, but it's really a fixed-point number. const GAIN_SCALE: u32 = 1 << 16; /// Line fitting ADC calibration scheme /// -/// This scheme implements gain correction based on reference points. +/// This scheme implements gain correction based on reference points, and +/// returns readings in mV. /// /// A reference point is a pair of a reference voltage and the corresponding /// mean raw digital ADC value. Such values are usually stored in efuse bit @@ -38,7 +38,11 @@ const GAIN_SCALE: u32 = 1 << 16; pub struct AdcCalLine { basic: AdcCalBasic, - /// Gain of ADC-value + /// ADC gain. + /// + /// After being de-biased by the basic calibration, the reading is + /// multiplied by this value. Despite the type, it is a fixed-point + /// number with 16 fractional bits. gain: u32, _phantom: PhantomData, @@ -57,8 +61,8 @@ where .map(|code| (code, ADCI::get_cal_mv(atten))) .unwrap_or_else(|| { // As a fallback try to calibrate using reference voltage source. - // This methos is no to good because actual reference voltage may varies - // in range 1000..=1200 mV and this value currently cannot be given from efuse. + // This method is not too good because actual reference voltage may varies + // in range 1000..=1200 mV and this value currently cannot be read from efuse. ( AdcConfig::::adc_calibrate(atten, AdcCalSource::Ref), 1100, // use 1100 mV as a middle of typical reference voltage range @@ -69,10 +73,9 @@ where // the voltage with the previously done measurement when the chip was // manufactured. // - // Rounding formula: R = (OP(A * 2) + 1) / 2 - // where R - result, A - argument, O - operation - let gain = - ((mv as u32 * GAIN_SCALE * 2 / code as u32 + 1) * 4096 / atten.ref_mv() as u32 + 1) / 2; + // Note that the constant term is zero because the basic calibration takes care + // of it already. + let gain = mv as u32 * GAIN_SCALE / code as u32; Self { basic, @@ -88,7 +91,6 @@ where fn adc_val(&self, val: u16) -> u16 { let val = self.basic.adc_val(val); - // pointers are checked in the upper layer (val as u32 * self.gain / GAIN_SCALE) as u16 } } diff --git a/esp-hal-common/src/analog/adc/riscv.rs b/esp-hal-common/src/analog/adc/riscv.rs index 12d743895..130159c78 100644 --- a/esp-hal-common/src/analog/adc/riscv.rs +++ b/esp-hal-common/src/analog/adc/riscv.rs @@ -149,25 +149,6 @@ impl Attenuation { Attenuation::Attenuation6dB, Attenuation::Attenuation11dB, ]; - - /// Reference voltage in millivolts - /// - /// Vref = 10 ^ (Att / 20) * Vref0 - /// where Vref0 = 1.1 V, Att - attenuation in dB - /// - /// To convert raw value to millivolts use formula: - /// V = D * Vref / 2 ^ R - /// where D - raw ADC value, R - resolution in bits - pub const fn ref_mv(&self) -> u16 { - match self { - Attenuation::Attenuation0dB => 1100, - #[cfg(not(esp32c2))] - Attenuation::Attenuation2p5dB => 1467, - #[cfg(not(esp32c2))] - Attenuation::Attenuation6dB => 2195, - Attenuation::Attenuation11dB => 3903, - } - } } pub struct AdcPin { diff --git a/esp-hal-common/src/analog/adc/xtensa.rs b/esp-hal-common/src/analog/adc/xtensa.rs index 50baa4b34..c73b8b0b3 100644 --- a/esp-hal-common/src/analog/adc/xtensa.rs +++ b/esp-hal-common/src/analog/adc/xtensa.rs @@ -112,23 +112,6 @@ impl Attenuation { Attenuation::Attenuation6dB, Attenuation::Attenuation11dB, ]; - - /// Reference voltage in millivolts - /// - /// Vref = 10 ^ (Att / 20) * Vref0 - /// where Vref0 = 1.1 V, Att - attenuation in dB - /// - /// To convert raw value to millivolts use formula: - /// V = D * Vref / 2 ^ R - /// where D - raw ADC value, R - resolution in bits - pub const fn ref_mv(&self) -> u16 { - match self { - Attenuation::Attenuation0dB => 1100, - Attenuation::Attenuation2p5dB => 1467, - Attenuation::Attenuation6dB => 2195, - Attenuation::Attenuation11dB => 3903, - } - } } pub struct AdcPin { diff --git a/esp-hal-common/src/analog/mod.rs b/esp-hal-common/src/analog/mod.rs index 1a6d21065..73c3ce935 100644 --- a/esp-hal-common/src/analog/mod.rs +++ b/esp-hal-common/src/analog/mod.rs @@ -84,12 +84,17 @@ pub mod adc; #[cfg(dac)] pub mod dac; -/// A helper trait to do calibrated samples fitting +/// A trait abstracting over calibration methods. +/// +/// The methods in this trait are mostly for internal use. To get +/// calibrated ADC reads, all you need to do is call `enable_pin_with_cal` +/// and specify some implementor of this trait. pub trait AdcCalScheme: Sized { - /// Instantiate scheme + /// Create a new calibration scheme for the given attenuation. fn new_cal(atten: adc::Attenuation) -> Self; - /// Get ADC calibration value to set to ADC unit + /// Return the basic ADC bias value. See [`adc::AdcCalBasic`] for + /// details. fn adc_cal(&self) -> u16 { 0 } diff --git a/esp32c2-hal/examples/adc_cal.rs b/esp32c2-hal/examples/adc_cal.rs index 5a67f61ae..23a52a8f8 100644 --- a/esp32c2-hal/examples/adc_cal.rs +++ b/esp32c2-hal/examples/adc_cal.rs @@ -32,7 +32,10 @@ fn main() -> ! { let atten = Attenuation::Attenuation11dB; - // You can try any of the following calibration methods by uncommenting them + // You can try any of the following calibration methods by uncommenting + // them. Note that only AdcCalLine returns readings in mV; the other two + // return raw readings in some unspecified scale. + // // type AdcCal = (); // type AdcCal = adc::AdcCalBasic; type AdcCal = adc::AdcCalLine; @@ -44,9 +47,8 @@ fn main() -> ! { let mut delay = Delay::new(&clocks); loop { - let pin_value: u16 = nb::block!(adc1.read(&mut pin)).unwrap(); - let pin_value_mv = pin_value as u32 * atten.ref_mv() as u32 / 4096; - println!("PIN2 ADC reading = {pin_value} ({pin_value_mv} mV)"); + let pin_mv = nb::block!(adc1.read(&mut pin)).unwrap(); + println!("PIN2 ADC reading = {pin_mv} mV"); delay.delay_ms(1500u32); } } diff --git a/esp32c3-hal/examples/adc_cal.rs b/esp32c3-hal/examples/adc_cal.rs index 069f6b6e0..2558c455e 100644 --- a/esp32c3-hal/examples/adc_cal.rs +++ b/esp32c3-hal/examples/adc_cal.rs @@ -32,7 +32,10 @@ fn main() -> ! { let atten = Attenuation::Attenuation11dB; - // You can try any of the following calibration methods by uncommenting them + // You can try any of the following calibration methods by uncommenting them. + // Note that only AdcCalLine and AdcCalCurve return readings in mV; the other + // two return raw readings in some unspecified scale. + // // type AdcCal = (); // type AdcCal = adc::AdcCalBasic; // type AdcCal = adc::AdcCalLine; @@ -45,9 +48,8 @@ fn main() -> ! { let mut delay = Delay::new(&clocks); loop { - let pin_value = nb::block!(adc1.read(&mut pin)).unwrap(); - let pin_value_mv = pin_value as u32 * atten.ref_mv() as u32 / 4096; - println!("PIN2 ADC reading = {pin_value} ({pin_value_mv} mV)"); + let pin_mv = nb::block!(adc1.read(&mut pin)).unwrap(); + println!("PIN2 ADC reading = {pin_mv} mV"); delay.delay_ms(1500u32); } } diff --git a/esp32c6-hal/examples/adc_cal.rs b/esp32c6-hal/examples/adc_cal.rs index 16b414f32..55d6431e8 100644 --- a/esp32c6-hal/examples/adc_cal.rs +++ b/esp32c6-hal/examples/adc_cal.rs @@ -32,7 +32,10 @@ fn main() -> ! { let atten = Attenuation::Attenuation11dB; - // You can try any of the following calibration methods by uncommenting them + // You can try any of the following calibration methods by uncommenting them. + // Note that only AdcCalLine and AdcCalCurve return readings in mV; the other + // two return raw readings in some unspecified scale. + // // type AdcCal = (); // type AdcCal = adc::AdcCalBasic; // type AdcCal = adc::AdcCalLine; @@ -45,9 +48,8 @@ fn main() -> ! { let mut delay = Delay::new(&clocks); loop { - let pin_value: u16 = nb::block!(adc1.read(&mut pin)).unwrap(); - let pin_value_mv = pin_value as u32 * atten.ref_mv() as u32 / 4096; - println!("PIN2 ADC reading = {pin_value} ({pin_value_mv} mV)"); + let pin_mv = nb::block!(adc1.read(&mut pin)).unwrap(); + println!("PIN2 ADC reading = {pin_mv} mV"); delay.delay_ms(1500u32); } } diff --git a/esp32s3-hal/examples/adc_cal.rs b/esp32s3-hal/examples/adc_cal.rs index e128b67fd..ba1ed35a5 100644 --- a/esp32s3-hal/examples/adc_cal.rs +++ b/esp32s3-hal/examples/adc_cal.rs @@ -31,7 +31,10 @@ fn main() -> ! { let atten = Attenuation::Attenuation11dB; - // You can try any of the following calibration methods by uncommenting them + // You can try any of the following calibration methods by uncommenting them. + // Note that only AdcCalLine and AdcCalCurve return readings in mV; the other + // two return raw readings in some unspecified scale. + // // type AdcCal = (); // type AdcCal = adc::AdcCalBasic; // type AdcCal = adc::AdcCalLine; @@ -44,9 +47,8 @@ fn main() -> ! { let mut delay = Delay::new(&clocks); loop { - let pin_value: u16 = nb::block!(adc1.read(&mut pin)).unwrap(); - let pin_value_mv = pin_value as u32 * atten.ref_mv() as u32 / 4096; - println!("PIN2 ADC reading = {pin_value} ({pin_value_mv} mV)"); + let pin_mv = nb::block!(adc1.read(&mut pin)).unwrap(); + println!("PIN2 ADC reading = {pin_mv} mV"); delay.delay_ms(1500u32); } }