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)
- Optimised multi-core critical section implementation (#797)
- Changed linear- and curve-calibrated ADC to provide readings in mV (#836)
### Fixed

View File

@ -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<ADCI> {
/// Calibration value to set to ADC unit

View File

@ -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<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],
_phantom: PhantomData<ADCI>,
@ -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,)*
],
},)*
];

View File

@ -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<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,
_phantom: PhantomData<ADCI>,
@ -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::<ADCI>::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
}
}

View File

@ -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<PIN, ADCI, CS = ()> {

View File

@ -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<PIN, ADCI, CS = ()> {

View File

@ -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<ADCI>: 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
}

View File

@ -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<ADC1>;
type AdcCal = adc::AdcCalLine<ADC1>;
@ -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);
}
}

View File

@ -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<ADC1>;
// type AdcCal = adc::AdcCalLine<ADC1>;
@ -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);
}
}

View File

@ -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<ADC1>;
// type AdcCal = adc::AdcCalLine<ADC1>;
@ -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);
}
}

View File

@ -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<ADC1>;
// type AdcCal = adc::AdcCalLine<ADC1>;
@ -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);
}
}