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
This commit is contained in:
jneem 2023-10-30 12:16:45 -05:00 committed by GitHub
parent 0304a10b2f
commit ff80b69183
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 68 additions and 74 deletions

View File

@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Bumped MSRV to 1.67 (#798) - Bumped MSRV to 1.67 (#798)
- Optimised multi-core critical section implementation (#797) - Optimised multi-core critical section implementation (#797)
- Changed linear- and curve-calibrated ADC to provide readings in mV (#836)
### Fixed ### Fixed

View File

@ -11,10 +11,14 @@ use crate::adc::{
/// Basic ADC calibration scheme /// Basic ADC calibration scheme
/// ///
/// Basic calibration is related to setting some initial bias value in ADC. /// Basic calibration sets the initial ADC bias value so that a zero voltage
/// Such values usually is stored in efuse bit fields but also can be measured /// gives a reading of zero. The correct bias value is usually stored in efuse,
/// in runtime by connecting ADC input to ground internally a fallback when /// but can also be measured at runtime by connecting the ADC input to ground
/// it is not available. /// 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)] #[derive(Clone, Copy)]
pub struct AdcCalBasic<ADCI> { pub struct AdcCalBasic<ADCI> {
/// Calibration value to set to ADC unit /// Calibration value to set to ADC unit

View File

@ -11,6 +11,8 @@ use crate::adc::{
const COEFF_MUL: i64 = 1 << 52; 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; type CurveCoeff = i64;
/// Polynomial coefficients for specified attenuation. /// Polynomial coefficients for specified attenuation.
@ -35,8 +37,8 @@ pub trait AdcHasCurveCal {
/// Curve fitting ADC calibration scheme /// Curve fitting ADC calibration scheme
/// ///
/// This scheme implements final polynomial error correction using predefined /// This scheme implements polynomial error correction using predefined
/// coefficient sets for each attenuation. /// coefficient sets for each attenuation. It returns readings in mV.
/// ///
/// This scheme also includes basic calibration ([`super::AdcCalBasic`]) and /// This scheme also includes basic calibration ([`super::AdcCalBasic`]) and
/// line fitting ([`AdcCalLine`]). /// line fitting ([`AdcCalLine`]).
@ -44,7 +46,15 @@ pub trait AdcHasCurveCal {
pub struct AdcCalCurve<ADCI> { pub struct AdcCalCurve<ADCI> {
line: AdcCalLine<ADCI>, line: AdcCalLine<ADCI>,
/// 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], coeff: &'static [CurveCoeff],
_phantom: PhantomData<ADCI>, _phantom: PhantomData<ADCI>,
@ -104,7 +114,7 @@ macro_rules! coeff_tables {
$(CurveCoeffs { $(CurveCoeffs {
atten: Attenuation::$att, atten: Attenuation::$att,
coeff: &[ 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,)*
], ],
},)* },)*
]; ];

View File

@ -16,13 +16,13 @@ use crate::adc::{
/// See also [`AdcCalLine`]. /// See also [`AdcCalLine`].
pub trait AdcHasLineCal {} pub trait AdcHasLineCal {}
/// Coefficients is actually a fixed-point numbers. /// We store the gain as a u32, but it's really a fixed-point number.
/// It is scaled to put them into integer.
const GAIN_SCALE: u32 = 1 << 16; const GAIN_SCALE: u32 = 1 << 16;
/// Line fitting ADC calibration scheme /// 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 /// 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 /// 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<ADCI> { pub struct AdcCalLine<ADCI> {
basic: AdcCalBasic<ADCI>, basic: AdcCalBasic<ADCI>,
/// 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, gain: u32,
_phantom: PhantomData<ADCI>, _phantom: PhantomData<ADCI>,
@ -57,8 +61,8 @@ where
.map(|code| (code, ADCI::get_cal_mv(atten))) .map(|code| (code, ADCI::get_cal_mv(atten)))
.unwrap_or_else(|| { .unwrap_or_else(|| {
// As a fallback try to calibrate using reference voltage source. // As a fallback try to calibrate using reference voltage source.
// This methos is no to good because actual reference voltage may varies // This method is not too good because actual reference voltage may varies
// in range 1000..=1200 mV and this value currently cannot be given from efuse. // in range 1000..=1200 mV and this value currently cannot be read from efuse.
( (
AdcConfig::<ADCI>::adc_calibrate(atten, AdcCalSource::Ref), AdcConfig::<ADCI>::adc_calibrate(atten, AdcCalSource::Ref),
1100, // use 1100 mV as a middle of typical reference voltage range 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 // the voltage with the previously done measurement when the chip was
// manufactured. // manufactured.
// //
// Rounding formula: R = (OP(A * 2) + 1) / 2 // Note that the constant term is zero because the basic calibration takes care
// where R - result, A - argument, O - operation // of it already.
let gain = let gain = mv as u32 * GAIN_SCALE / code as u32;
((mv as u32 * GAIN_SCALE * 2 / code as u32 + 1) * 4096 / atten.ref_mv() as u32 + 1) / 2;
Self { Self {
basic, basic,
@ -88,7 +91,6 @@ where
fn adc_val(&self, val: u16) -> u16 { fn adc_val(&self, val: u16) -> u16 {
let val = self.basic.adc_val(val); let val = self.basic.adc_val(val);
// pointers are checked in the upper layer
(val as u32 * self.gain / GAIN_SCALE) as u16 (val as u32 * self.gain / GAIN_SCALE) as u16
} }
} }

View File

@ -149,25 +149,6 @@ impl Attenuation {
Attenuation::Attenuation6dB, Attenuation::Attenuation6dB,
Attenuation::Attenuation11dB, 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<PIN, ADCI, CS = ()> { pub struct AdcPin<PIN, ADCI, CS = ()> {

View File

@ -112,23 +112,6 @@ impl Attenuation {
Attenuation::Attenuation6dB, Attenuation::Attenuation6dB,
Attenuation::Attenuation11dB, 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<PIN, ADCI, CS = ()> { pub struct AdcPin<PIN, ADCI, CS = ()> {

View File

@ -84,12 +84,17 @@ pub mod adc;
#[cfg(dac)] #[cfg(dac)]
pub mod 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<ADCI>: Sized { pub trait AdcCalScheme<ADCI>: Sized {
/// Instantiate scheme /// Create a new calibration scheme for the given attenuation.
fn new_cal(atten: adc::Attenuation) -> Self; 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 { fn adc_cal(&self) -> u16 {
0 0
} }

View File

@ -32,7 +32,10 @@ fn main() -> ! {
let atten = Attenuation::Attenuation11dB; 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 = ();
// type AdcCal = adc::AdcCalBasic<ADC1>; // type AdcCal = adc::AdcCalBasic<ADC1>;
type AdcCal = adc::AdcCalLine<ADC1>; type AdcCal = adc::AdcCalLine<ADC1>;
@ -44,9 +47,8 @@ fn main() -> ! {
let mut delay = Delay::new(&clocks); let mut delay = Delay::new(&clocks);
loop { loop {
let pin_value: u16 = nb::block!(adc1.read(&mut pin)).unwrap(); let pin_mv = 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_mv} mV");
println!("PIN2 ADC reading = {pin_value} ({pin_value_mv} mV)");
delay.delay_ms(1500u32); delay.delay_ms(1500u32);
} }
} }

View File

@ -32,7 +32,10 @@ fn main() -> ! {
let atten = Attenuation::Attenuation11dB; 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 = ();
// type AdcCal = adc::AdcCalBasic<ADC1>; // type AdcCal = adc::AdcCalBasic<ADC1>;
// type AdcCal = adc::AdcCalLine<ADC1>; // type AdcCal = adc::AdcCalLine<ADC1>;
@ -45,9 +48,8 @@ fn main() -> ! {
let mut delay = Delay::new(&clocks); let mut delay = Delay::new(&clocks);
loop { loop {
let pin_value = nb::block!(adc1.read(&mut pin)).unwrap(); let pin_mv = 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_mv} mV");
println!("PIN2 ADC reading = {pin_value} ({pin_value_mv} mV)");
delay.delay_ms(1500u32); delay.delay_ms(1500u32);
} }
} }

View File

@ -32,7 +32,10 @@ fn main() -> ! {
let atten = Attenuation::Attenuation11dB; 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 = ();
// type AdcCal = adc::AdcCalBasic<ADC1>; // type AdcCal = adc::AdcCalBasic<ADC1>;
// type AdcCal = adc::AdcCalLine<ADC1>; // type AdcCal = adc::AdcCalLine<ADC1>;
@ -45,9 +48,8 @@ fn main() -> ! {
let mut delay = Delay::new(&clocks); let mut delay = Delay::new(&clocks);
loop { loop {
let pin_value: u16 = nb::block!(adc1.read(&mut pin)).unwrap(); let pin_mv = 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_mv} mV");
println!("PIN2 ADC reading = {pin_value} ({pin_value_mv} mV)");
delay.delay_ms(1500u32); delay.delay_ms(1500u32);
} }
} }

View File

@ -31,7 +31,10 @@ fn main() -> ! {
let atten = Attenuation::Attenuation11dB; 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 = ();
// type AdcCal = adc::AdcCalBasic<ADC1>; // type AdcCal = adc::AdcCalBasic<ADC1>;
// type AdcCal = adc::AdcCalLine<ADC1>; // type AdcCal = adc::AdcCalLine<ADC1>;
@ -44,9 +47,8 @@ fn main() -> ! {
let mut delay = Delay::new(&clocks); let mut delay = Delay::new(&clocks);
loop { loop {
let pin_value: u16 = nb::block!(adc1.read(&mut pin)).unwrap(); let pin_mv = 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_mv} mV");
println!("PIN2 ADC reading = {pin_value} ({pin_value_mv} mV)");
delay.delay_ms(1500u32); delay.delay_ms(1500u32);
} }
} }